From 21cdc39bbf5df65ee5710cd4ef677eeb0615e50c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Feb 2026 10:17:46 +0000 Subject: [PATCH 1/7] Initial plan From 6abed80278d1f5de777451e6a7e0f8fd634b2ff3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Feb 2026 10:21:44 +0000 Subject: [PATCH 2/7] =?UTF-8?q?=F0=9F=94=A7=20update:=20fix=20IntegrityErr?= =?UTF-8?q?or=20on=20reopen=20due=20to=20SQLite=20WAL=20checkpoint=20race?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: warengonzaga <15052701+warengonzaga@users.noreply.github.com> --- src/engine.ts | 12 ++++++++---- src/integrity.ts | 17 ++++++++++++++--- tests/engine.test.ts | 23 +++++++++++++++++++++++ 3 files changed, 45 insertions(+), 7 deletions(-) diff --git a/src/engine.ts b/src/engine.ts index 19e8020..7a3c794 100644 --- a/src/engine.ts +++ b/src/engine.ts @@ -103,7 +103,7 @@ export class SecretsEngine { // 6. Verify integrity (skip for brand-new stores) if (!isNewStore) { - await verifyIntegrity(masterKey, store.filePath, dirPath); + await verifyIntegrity(masterKey, store.filePath, dirPath, () => store.checkpoint()); } // 7. Build the instance @@ -114,7 +114,7 @@ export class SecretsEngine { // 9. Write initial integrity HMAC for new stores if (isNewStore) { - await updateIntegrity(masterKey, store.filePath, dirPath, salt); + await updateIntegrity(masterKey, store.filePath, dirPath, salt, () => store.checkpoint()); } return engine; @@ -184,7 +184,9 @@ export class SecretsEngine { this.keyIndex.set(keyHash, key); // Update integrity HMAC - await updateIntegrity(this.masterKey, this.store.filePath, this.dirPath, this.salt); + await updateIntegrity(this.masterKey, this.store.filePath, this.dirPath, this.salt, () => + this.store.checkpoint(), + ); } /** @@ -209,7 +211,9 @@ export class SecretsEngine { if (deleted) { this.keyIndex.delete(keyHash); - await updateIntegrity(this.masterKey, this.store.filePath, this.dirPath, this.salt); + await updateIntegrity(this.masterKey, this.store.filePath, this.dirPath, this.salt, () => + this.store.checkpoint(), + ); } return deleted; diff --git a/src/integrity.ts b/src/integrity.ts index 786c4a3..555b7e5 100644 --- a/src/integrity.ts +++ b/src/integrity.ts @@ -19,7 +19,16 @@ import { CONSTANTS } from "./types.ts"; * integrity_hmac = HMAC-SHA256(master_key, SHA256(store.db)) * ``` */ -export async function computeIntegrityHmac(masterKey: Buffer, dbFilePath: string): Promise { +export async function computeIntegrityHmac( + masterKey: Buffer, + dbFilePath: string, + checkpointFn?: () => void, +): Promise { + // Checkpoint WAL to ensure all data is flushed to the main database file + if (checkpointFn) { + checkpointFn(); + } + const dbBytes = Buffer.from(await readFile(dbFilePath)); const dbHash = sha256(dbBytes); return hmac(masterKey, dbHash); @@ -33,6 +42,7 @@ export async function verifyIntegrity( masterKey: Buffer, dbFilePath: string, dirPath: string, + checkpointFn?: () => void, ): Promise { const metaRaw = await readMetaFile(dirPath); @@ -53,7 +63,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(); @@ -70,8 +80,9 @@ export async function updateIntegrity( dbFilePath: string, dirPath: string, salt: string, + checkpointFn?: () => void, ): Promise { - const integrity = await computeIntegrityHmac(masterKey, dbFilePath); + const integrity = await computeIntegrityHmac(masterKey, dbFilePath, checkpointFn); const meta: StoreMeta = { version: CONSTANTS.STORE_VERSION, diff --git a/tests/engine.test.ts b/tests/engine.test.ts index 55ff85f..fe3528a 100644 --- a/tests/engine.test.ts +++ b/tests/engine.test.ts @@ -56,6 +56,29 @@ describe("SecretsEngine.open", () => { expect(engine2.size).toBe(3); engine2.close(); }); + + test("integrity verification succeeds after WAL checkpoint on reopen", async () => { + // Regression test for WAL checkpoint race condition + // Write data, close, and reopen multiple times to ensure integrity verification + // works correctly regardless of WAL checkpoint timing + + const engine1 = await SecretsEngine.open({ path: testDir }); + await engine1.set("test.key", "test-value"); + engine1.close(); + + // This should not throw IntegrityError + const engine2 = await SecretsEngine.open({ path: testDir }); + expect(await engine2.get("test.key")).toBe("test-value"); + + // Update the value and reopen again + await engine2.set("test.key", "updated-value"); + engine2.close(); + + // This should also not throw IntegrityError + const engine3 = await SecretsEngine.open({ path: testDir }); + expect(await engine3.get("test.key")).toBe("updated-value"); + engine3.close(); + }); }); describe("set / get", () => { From 7c29c8e5130c2350d98b21376b81e8ac23700902 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Feb 2026 10:55:39 +0000 Subject: [PATCH 3/7] =?UTF-8?q?=F0=9F=94=A7=20update:=20address=20review?= =?UTF-8?q?=20feedback=20on=20WAL=20checkpoint=20performance=20and=20error?= =?UTF-8?q?=20handling?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: warengonzaga <15052701+warengonzaga@users.noreply.github.com> --- README.md | 2 +- src/engine.ts | 52 ++++++++++++++++------------ src/integrity.ts | 7 +++- tests/engine.test.ts | 80 +++++++++++++++++++++++++------------------- 4 files changed, 84 insertions(+), 57 deletions(-) diff --git a/README.md b/README.md index f6c2bc5..5d5d1cd 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ await secrets.keys("openai.*"); // ["openai.apiKey"] await secrets.delete("openai.apiKey"); // Clean up -secrets.close(); +await secrets.close(); ``` ## Storage Location diff --git a/src/engine.ts b/src/engine.ts index 7a3c794..9026141 100644 --- a/src/engine.ts +++ b/src/engine.ts @@ -101,23 +101,29 @@ 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, () => store.checkpoint()); - } + 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, () => store.checkpoint()); - } + // 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 + store.close(); + throw error; + } } // ----------------------------------------------------------------------- @@ -183,10 +189,8 @@ 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, () => - this.store.checkpoint(), - ); + // Update integrity HMAC (without checkpoint to avoid write amplification) + await updateIntegrity(this.masterKey, this.store.filePath, this.dirPath, this.salt); } /** @@ -211,9 +215,8 @@ export class SecretsEngine { if (deleted) { this.keyIndex.delete(keyHash); - await updateIntegrity(this.masterKey, this.store.filePath, this.dirPath, this.salt, () => - this.store.checkpoint(), - ); + // Update integrity HMAC (without checkpoint to avoid write amplification) + await updateIntegrity(this.masterKey, this.store.filePath, this.dirPath, this.salt); } return deleted; @@ -261,10 +264,17 @@ 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 { if (!this.closed) { + // 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); + this.store.close(); this.keyIndex.clear(); this.closed = true; diff --git a/src/integrity.ts b/src/integrity.ts index 555b7e5..4e90b14 100644 --- a/src/integrity.ts +++ b/src/integrity.ts @@ -26,7 +26,12 @@ export async function computeIntegrityHmac( ): Promise { // Checkpoint WAL to ensure all data is flushed to the main database file if (checkpointFn) { - 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)); diff --git a/tests/engine.test.ts b/tests/engine.test.ts index fe3528a..1cfdc76 100644 --- a/tests/engine.test.ts +++ b/tests/engine.test.ts @@ -26,19 +26,19 @@ describe("SecretsEngine.open", () => { expect(engine).toBeDefined(); expect(engine.size).toBe(0); - engine.close(); + await engine.close(); }); test("reopens an existing store", async () => { const engine1 = await SecretsEngine.open({ path: testDir }); await engine1.set("test.key", "test-value"); - engine1.close(); + await engine1.close(); const engine2 = await SecretsEngine.open({ path: testDir }); const value = await engine2.get("test.key"); expect(value).toBe("test-value"); - engine2.close(); + await engine2.close(); }); test("preserves secrets across reopens", async () => { @@ -46,7 +46,7 @@ describe("SecretsEngine.open", () => { await engine1.set("key.a", "value-a"); await engine1.set("key.b", "value-b"); await engine1.set("key.c", "value-c"); - engine1.close(); + await engine1.close(); const engine2 = await SecretsEngine.open({ path: testDir }); @@ -54,30 +54,42 @@ describe("SecretsEngine.open", () => { expect(await engine2.get("key.b")).toBe("value-b"); expect(await engine2.get("key.c")).toBe("value-c"); expect(engine2.size).toBe(3); - engine2.close(); + await engine2.close(); }); test("integrity verification succeeds after WAL checkpoint on reopen", async () => { // Regression test for WAL checkpoint race condition - // Write data, close, and reopen multiple times to ensure integrity verification - // works correctly regardless of WAL checkpoint timing + // Deterministically forces the condition by manually checkpointing the WAL + // between close and reopen to ensure the test reliably validates the fix + const { Database } = await import("bun:sqlite"); const engine1 = await SecretsEngine.open({ path: testDir }); await engine1.set("test.key", "test-value"); - engine1.close(); + await engine1.close(); + + // Force a checkpoint using a separate connection to deterministically + // trigger the race condition that this fix addresses + const dbPath = join(testDir, "store.db"); + const checkpointDb = new Database(dbPath); + checkpointDb.exec("PRAGMA wal_checkpoint(TRUNCATE);"); + checkpointDb.close(); // This should not throw IntegrityError const engine2 = await SecretsEngine.open({ path: testDir }); expect(await engine2.get("test.key")).toBe("test-value"); - // Update the value and reopen again + // Update the value and reopen again with another forced checkpoint await engine2.set("test.key", "updated-value"); - engine2.close(); + await engine2.close(); + + const checkpointDb2 = new Database(dbPath); + checkpointDb2.exec("PRAGMA wal_checkpoint(TRUNCATE);"); + checkpointDb2.close(); // This should also not throw IntegrityError const engine3 = await SecretsEngine.open({ path: testDir }); expect(await engine3.get("test.key")).toBe("updated-value"); - engine3.close(); + await engine3.close(); }); }); @@ -89,7 +101,7 @@ describe("set / get", () => { const value = await engine.get("openai.apiKey"); expect(value).toBe("sk-abc123"); - engine.close(); + await engine.close(); }); test("returns null for non-existent key", async () => { @@ -98,7 +110,7 @@ describe("set / get", () => { const value = await engine.get("nonexistent"); expect(value).toBeNull(); - engine.close(); + await engine.close(); }); test("overwrites existing key", async () => { @@ -109,7 +121,7 @@ describe("set / get", () => { const value = await engine.get("key"); expect(value).toBe("updated"); - engine.close(); + await engine.close(); }); test("handles empty string values", async () => { @@ -119,7 +131,7 @@ describe("set / get", () => { const value = await engine.get("empty"); expect(value).toBe(""); - engine.close(); + await engine.close(); }); test("handles unicode values", async () => { @@ -130,7 +142,7 @@ describe("set / get", () => { const value = await engine.get("unicode.key"); expect(value).toBe(unicode); - engine.close(); + await engine.close(); }); test("handles long values", async () => { @@ -141,7 +153,7 @@ describe("set / get", () => { const value = await engine.get("long.key"); expect(value).toBe(longValue); - engine.close(); + await engine.close(); }); }); @@ -153,14 +165,14 @@ describe("getOrThrow", () => { const value = await engine.getOrThrow("exists"); expect(value).toBe("value"); - engine.close(); + await engine.close(); }); test("throws KeyNotFoundError for missing key", async () => { const engine = await SecretsEngine.open({ path: testDir }); expect(engine.getOrThrow("missing")).rejects.toThrow(KeyNotFoundError); - engine.close(); + await engine.close(); }); }); @@ -171,14 +183,14 @@ describe("has", () => { await engine.set("exists", "value"); expect(await engine.has("exists")).toBe(true); - engine.close(); + await engine.close(); }); test("returns false for non-existent key", async () => { const engine = await SecretsEngine.open({ path: testDir }); expect(await engine.has("missing")).toBe(false); - engine.close(); + await engine.close(); }); }); @@ -192,7 +204,7 @@ describe("delete", () => { expect(deleted).toBe(true); expect(await engine.has("to-delete")).toBe(false); expect(await engine.get("to-delete")).toBeNull(); - engine.close(); + await engine.close(); }); test("returns false for non-existent key", async () => { @@ -201,19 +213,19 @@ describe("delete", () => { const deleted = await engine.delete("nonexistent"); expect(deleted).toBe(false); - engine.close(); + await engine.close(); }); test("persists deletion across reopens", async () => { const engine1 = await SecretsEngine.open({ path: testDir }); await engine1.set("key", "value"); await engine1.delete("key"); - engine1.close(); + await engine1.close(); const engine2 = await SecretsEngine.open({ path: testDir }); expect(await engine2.has("key")).toBe(false); - engine2.close(); + await engine2.close(); }); }); @@ -224,7 +236,7 @@ describe("keys", () => { const keys = await engine.keys(); expect(keys).toEqual([]); - engine.close(); + await engine.close(); }); test("returns all keys sorted", async () => { @@ -237,7 +249,7 @@ describe("keys", () => { const keys = await engine.keys(); expect(keys).toEqual(["a.key", "b.key", "c.key"]); - engine.close(); + await engine.close(); }); test("filters keys by glob pattern", async () => { @@ -250,7 +262,7 @@ describe("keys", () => { const openaiKeys = await engine.keys("openai.*"); expect(openaiKeys).toEqual(["openai.apiKey", "openai.orgId"]); - engine.close(); + await engine.close(); }); test("returns empty for non-matching pattern", async () => { @@ -261,7 +273,7 @@ describe("keys", () => { const keys = await engine.keys("nonexistent.*"); expect(keys).toEqual([]); - engine.close(); + await engine.close(); }); }); @@ -292,7 +304,7 @@ describe("size", () => { await engine.delete("a"); expect(engine.size).toBe(1); - engine.close(); + await engine.close(); }); }); @@ -301,14 +313,14 @@ describe("storagePath", () => { const engine = await SecretsEngine.open({ path: testDir }); expect(engine.storagePath).toBe(testDir); - engine.close(); + await engine.close(); }); }); describe("closed instance guard", () => { test("throws on operations after close()", async () => { const engine = await SecretsEngine.open({ path: testDir }); - engine.close(); + await engine.close(); expect(engine.get("key")).rejects.toThrow("closed"); expect(engine.set("key", "value")).rejects.toThrow("closed"); @@ -326,7 +338,7 @@ describe("dot-notation namespacing", () => { const value = await engine.get("provider.openai.v1.apiKey"); expect(value).toBe("sk-deep"); - engine.close(); + await engine.close(); }); test("treats different namespaces as independent", async () => { @@ -337,6 +349,6 @@ describe("dot-notation namespacing", () => { expect(await engine.get("openai.apiKey")).toBe("sk-openai"); expect(await engine.get("anthropic.apiKey")).toBe("sk-anthropic"); - engine.close(); + await engine.close(); }); }); From edc7f136825d86e7325f64a457ec63a8150e4cab Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Feb 2026 10:57:13 +0000 Subject: [PATCH 4/7] =?UTF-8?q?=F0=9F=94=A7=20update:=20extract=20forceChe?= =?UTF-8?q?ckpoint=20helper=20to=20reduce=20test=20duplication?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: warengonzaga <15052701+warengonzaga@users.noreply.github.com> --- tests/engine.test.ts | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/tests/engine.test.ts b/tests/engine.test.ts index 1cfdc76..252739c 100644 --- a/tests/engine.test.ts +++ b/tests/engine.test.ts @@ -4,6 +4,7 @@ * These tests use isolated temporary directories to avoid interference. */ +import { Database } from "bun:sqlite"; import { afterEach, beforeEach, describe, expect, test } from "bun:test"; import { mkdtemp, rm } from "node:fs/promises"; import { tmpdir } from "node:os"; @@ -20,6 +21,16 @@ afterEach(async () => { await rm(testDir, { recursive: true, force: true }).catch(() => {}); }); +/** + * Helper function to force a WAL checkpoint using a separate SQLite connection. + * Used in regression tests to deterministically trigger checkpoint-related race conditions. + */ +function forceCheckpoint(dbPath: string): void { + const db = new Database(dbPath); + db.exec("PRAGMA wal_checkpoint(TRUNCATE);"); + db.close(); +} + describe("SecretsEngine.open", () => { test("creates a new store with no errors", async () => { const engine = await SecretsEngine.open({ path: testDir }); @@ -61,7 +72,7 @@ describe("SecretsEngine.open", () => { // Regression test for WAL checkpoint race condition // Deterministically forces the condition by manually checkpointing the WAL // between close and reopen to ensure the test reliably validates the fix - const { Database } = await import("bun:sqlite"); + const dbPath = join(testDir, "store.db"); const engine1 = await SecretsEngine.open({ path: testDir }); await engine1.set("test.key", "test-value"); @@ -69,10 +80,7 @@ describe("SecretsEngine.open", () => { // Force a checkpoint using a separate connection to deterministically // trigger the race condition that this fix addresses - const dbPath = join(testDir, "store.db"); - const checkpointDb = new Database(dbPath); - checkpointDb.exec("PRAGMA wal_checkpoint(TRUNCATE);"); - checkpointDb.close(); + forceCheckpoint(dbPath); // This should not throw IntegrityError const engine2 = await SecretsEngine.open({ path: testDir }); @@ -82,9 +90,7 @@ describe("SecretsEngine.open", () => { await engine2.set("test.key", "updated-value"); await engine2.close(); - const checkpointDb2 = new Database(dbPath); - checkpointDb2.exec("PRAGMA wal_checkpoint(TRUNCATE);"); - checkpointDb2.close(); + forceCheckpoint(dbPath); // This should also not throw IntegrityError const engine3 = await SecretsEngine.open({ path: testDir }); From 88f32ab790411e35890eac4e040f53fd51d6ff91 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Feb 2026 11:02:42 +0000 Subject: [PATCH 5/7] =?UTF-8?q?=F0=9F=9A=80=20release:=20bump=20version=20?= =?UTF-8?q?to=202.0.0=20for=20breaking=20changes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: warengonzaga <15052701+warengonzaga@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 04c8bf2..dda2fe3 100644 --- a/package.json +++ b/package.json @@ -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", From da6a9de90b156b23ca0a047e7d98298931013948 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Feb 2026 11:20:12 +0000 Subject: [PATCH 6/7] =?UTF-8?q?=F0=9F=94=A7=20update:=20fix=20integrity=20?= =?UTF-8?q?checkpoint=20issues=20and=20improve=20error=20handling?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: warengonzaga <15052701+warengonzaga@users.noreply.github.com> --- src/engine.ts | 39 +++++++++++++++++++++++++-------------- tests/engine.test.ts | 10 +++++++--- 2 files changed, 32 insertions(+), 17 deletions(-) diff --git a/src/engine.ts b/src/engine.ts index 9026141..42718d5 100644 --- a/src/engine.ts +++ b/src/engine.ts @@ -121,7 +121,11 @@ export class SecretsEngine { return engine; } catch (error) { // Cleanup: close the store if initialization fails - store.close(); + try { + store.close(); + } catch { + // Intentionally ignore errors during close to preserve original error + } throw error; } } @@ -189,8 +193,10 @@ export class SecretsEngine { // Update in-memory key index this.keyIndex.set(keyHash, key); - // Update integrity HMAC (without checkpoint to avoid write amplification) - 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(), + ); } /** @@ -215,8 +221,10 @@ export class SecretsEngine { if (deleted) { this.keyIndex.delete(keyHash); - // Update integrity HMAC (without checkpoint to avoid write amplification) - 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; @@ -269,15 +277,18 @@ export class SecretsEngine { */ async close(): Promise { if (!this.closed) { - // 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); - - 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; + } } } diff --git a/tests/engine.test.ts b/tests/engine.test.ts index 252739c..986d439 100644 --- a/tests/engine.test.ts +++ b/tests/engine.test.ts @@ -10,6 +10,7 @@ import { mkdtemp, rm } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { KeyNotFoundError, SecretsEngine } from "../src/index.ts"; +import { CONSTANTS } from "../src/types.ts"; let testDir: string; @@ -27,8 +28,11 @@ afterEach(async () => { */ function forceCheckpoint(dbPath: string): void { const db = new Database(dbPath); - db.exec("PRAGMA wal_checkpoint(TRUNCATE);"); - db.close(); + try { + db.exec("PRAGMA wal_checkpoint(TRUNCATE);"); + } finally { + db.close(); + } } describe("SecretsEngine.open", () => { @@ -72,7 +76,7 @@ describe("SecretsEngine.open", () => { // Regression test for WAL checkpoint race condition // Deterministically forces the condition by manually checkpointing the WAL // between close and reopen to ensure the test reliably validates the fix - const dbPath = join(testDir, "store.db"); + const dbPath = join(testDir, CONSTANTS.DB_NAME); const engine1 = await SecretsEngine.open({ path: testDir }); await engine1.set("test.key", "test-value"); From 8066a0473675c79620b931e7503aaf3c462f2370 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Feb 2026 11:40:10 +0000 Subject: [PATCH 7/7] =?UTF-8?q?=F0=9F=94=A7=20update:=20fix=20missing=20aw?= =?UTF-8?q?ait=20in=20test=20and=20update=20close()=20docs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: warengonzaga <15052701+warengonzaga@users.noreply.github.com> --- README.md | 6 +++++- tests/engine.test.ts | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 5d5d1cd..49d48a1 100644 --- a/README.md +++ b/README.md @@ -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` 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` diff --git a/tests/engine.test.ts b/tests/engine.test.ts index 986d439..9ac8d13 100644 --- a/tests/engine.test.ts +++ b/tests/engine.test.ts @@ -181,7 +181,7 @@ describe("getOrThrow", () => { test("throws KeyNotFoundError for missing key", async () => { const engine = await SecretsEngine.open({ path: testDir }); - expect(engine.getOrThrow("missing")).rejects.toThrow(KeyNotFoundError); + await expect(engine.getOrThrow("missing")).rejects.toThrow(KeyNotFoundError); await engine.close(); }); });