- dek-store-cosmos.ts: Cosmos DB-backed DekStore implementation - Uses _encryption_keys container with dekId partition key - Upsert semantics, idempotent delete, query-based listIds - index.ts: export CosmosDekStore - index.test.ts: 6 new tests with mock container (56 total) This completes E2EE Phase 3 — production multi-instance DEK storage. Previously only MemoryDekStore was available, losing DEKs on restart.
76 lines
1.9 KiB
TypeScript
76 lines
1.9 KiB
TypeScript
/**
|
|
* @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<WrappedDek | null> {
|
|
try {
|
|
const { resource } = await this.container.item(dekId, dekId).read<CosmosDekDoc>();
|
|
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<void> {
|
|
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<string[]> {
|
|
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<void> {
|
|
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
|
|
);
|
|
}
|