diff --git a/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/.last-refresh.log b/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/.last-refresh.log index 95cf354b..04024980 100644 --- a/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/.last-refresh.log +++ b/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/.last-refresh.log @@ -1,9 +1,9 @@ -Last refresh: 2026-04-13T06:13:49Z (2026-04-12 23:13:49 PDT) -Cascade conversations: 50 (421M) -Memories: 130 +Last refresh: 2026-04-14T06:00:06Z (2026-04-13 23:00:06 PDT) +Cascade conversations: 50 (428M) +Memories: 132 Implicit context: 20 -Code tracker dirs: 108 -File edit history: 4591 entries +Code tracker dirs: 151 +File edit history: 4640 entries Workspace storage: 41 workspaces Repo docs: 7 files across 2 repos Repo workflows: 55 files across 12 repos diff --git a/packages/field-encrypt/src/dek-store-cosmos.ts b/packages/field-encrypt/src/dek-store-cosmos.ts new file mode 100644 index 00000000..4d322aa8 --- /dev/null +++ b/packages/field-encrypt/src/dek-store-cosmos.ts @@ -0,0 +1,75 @@ +/** + * @bytelyst/field-encrypt — Cosmos DB DEK store + * + * Production DEK store backed by Azure Cosmos DB. + * Container: `_encryption_keys` (partition key: /dekId) + * + * Each WrappedDek is stored as a document with dekId as both id and partition key. + */ + +import type { Container } from '@azure/cosmos'; +import type { DekStore, WrappedDek } from './types.js'; + +interface CosmosDekDoc { + id: string; + dekId: string; + wrappedKey: string; + mekVersion: string; + createdAt: string; +} + +export class CosmosDekStore implements DekStore { + constructor(private readonly container: Container) {} + + async get(dekId: string): Promise { + try { + const { resource } = await this.container.item(dekId, dekId).read(); + if (!resource) return null; + return { + dekId: resource.dekId, + wrappedKey: resource.wrappedKey, + mekVersion: resource.mekVersion, + createdAt: resource.createdAt, + }; + } catch (err: unknown) { + if (isNotFound(err)) return null; + throw err; + } + } + + async put(dek: WrappedDek): Promise { + const doc: CosmosDekDoc = { + id: dek.dekId, + dekId: dek.dekId, + wrappedKey: dek.wrappedKey, + mekVersion: dek.mekVersion, + createdAt: dek.createdAt, + }; + await this.container.items.upsert(doc); + } + + async listIds(): Promise { + const { resources } = await this.container.items + .query<{ dekId: string }>('SELECT c.dekId FROM c') + .fetchAll(); + return resources.map(r => r.dekId); + } + + async delete(dekId: string): Promise { + try { + await this.container.item(dekId, dekId).delete(); + } catch (err: unknown) { + if (isNotFound(err)) return; // Already deleted — idempotent + throw err; + } + } +} + +function isNotFound(err: unknown): boolean { + return ( + typeof err === 'object' && + err !== null && + 'code' in err && + (err as { code: number }).code === 404 + ); +} diff --git a/packages/field-encrypt/src/index.test.ts b/packages/field-encrypt/src/index.test.ts index aabd3140..bff9494d 100644 --- a/packages/field-encrypt/src/index.test.ts +++ b/packages/field-encrypt/src/index.test.ts @@ -20,6 +20,7 @@ import { rewrapAllDeks, DekCache, MemoryDekStore, + CosmosDekStore, MemoryKeyProvider, EnvKeyProvider, migrateDocuments, @@ -489,3 +490,119 @@ describe('migrateDocuments', () => { expect(writeCount).toBe(0); }); }); + +// ── CosmosDekStore ────────────────────────────────── + +describe('CosmosDekStore', () => { + function createMockContainer() { + const docs = new Map>(); + + const container = { + item: (id: string, _pk: string) => ({ + read: async () => { + const resource = docs.get(id) as T | undefined; + if (!resource) { + const err = new Error('Not found') as Error & { code: number }; + err.code = 404; + throw err; + } + return { resource }; + }, + delete: async () => { + if (!docs.has(id)) { + const err = new Error('Not found') as Error & { code: number }; + err.code = 404; + throw err; + } + docs.delete(id); + }, + }), + items: { + upsert: async (doc: Record) => { + docs.set(doc.id as string, doc); + }, + query: (_sql: string) => ({ + fetchAll: async () => ({ + resources: [...docs.values()].map(d => ({ dekId: d.dekId })), + }), + }), + }, + }; + return container as unknown as import('@azure/cosmos').Container; + } + + it('put and get a DEK', async () => { + const store = new CosmosDekStore(createMockContainer()); + const dek = { + dekId: 'dek_test', + wrappedKey: 'aabbcc', + mekVersion: 'v1', + createdAt: '2026-01-01T00:00:00Z', + }; + await store.put(dek); + const got = await store.get('dek_test'); + expect(got).toEqual(dek); + }); + + it('get returns null for missing DEK', async () => { + const store = new CosmosDekStore(createMockContainer()); + const got = await store.get('nonexistent'); + expect(got).toBeNull(); + }); + + it('listIds returns all stored DEK IDs', async () => { + const store = new CosmosDekStore(createMockContainer()); + await store.put({ + dekId: 'dek_a', + wrappedKey: '11', + mekVersion: 'v1', + createdAt: '2026-01-01T00:00:00Z', + }); + await store.put({ + dekId: 'dek_b', + wrappedKey: '22', + mekVersion: 'v1', + createdAt: '2026-01-01T00:00:00Z', + }); + const ids = await store.listIds(); + expect(ids).toContain('dek_a'); + expect(ids).toContain('dek_b'); + expect(ids.length).toBe(2); + }); + + it('delete removes a DEK', async () => { + const store = new CosmosDekStore(createMockContainer()); + await store.put({ + dekId: 'dek_del', + wrappedKey: '33', + mekVersion: 'v1', + createdAt: '2026-01-01T00:00:00Z', + }); + await store.delete('dek_del'); + expect(await store.get('dek_del')).toBeNull(); + }); + + it('delete is idempotent (no error on missing)', async () => { + const store = new CosmosDekStore(createMockContainer()); + await expect(store.delete('nonexistent')).resolves.toBeUndefined(); + }); + + it('put overwrites existing DEK (upsert)', async () => { + const store = new CosmosDekStore(createMockContainer()); + await store.put({ + dekId: 'dek_up', + wrappedKey: 'old', + mekVersion: 'v1', + createdAt: '2026-01-01T00:00:00Z', + }); + await store.put({ + dekId: 'dek_up', + wrappedKey: 'new', + mekVersion: 'v2', + createdAt: '2026-01-01T00:00:00Z', + }); + const got = await store.get('dek_up'); + expect(got?.wrappedKey).toBe('new'); + expect(got?.mekVersion).toBe('v2'); + }); +}); diff --git a/packages/field-encrypt/src/index.ts b/packages/field-encrypt/src/index.ts index 30a8a00f..7ea2e0d1 100644 --- a/packages/field-encrypt/src/index.ts +++ b/packages/field-encrypt/src/index.ts @@ -46,6 +46,7 @@ export { encryptField, decryptField, generateAesKey } from './aes-gcm.js'; export { buildDekId, getOrCreateDek, rewrapAllDeks } from './envelope.js'; export { DekCache } from './key-cache.js'; export { MemoryDekStore } from './dek-store-memory.js'; +export { CosmosDekStore } from './dek-store-cosmos.js'; // ── Key providers (for direct use / testing) ──────── export { MemoryKeyProvider } from './key-provider-memory.js';