@geekmidas/constructs 10.0.0-alpha.115 → 10.0.0-alpha.117

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"cipher-C1lEFX63.mjs","names":[],"sources":["../src/encryption/cipher.ts"],"sourcesContent":["/**\n * The cipher behind an `Encryption` construct, and the format it writes.\n *\n * Every ciphertext names the key that wrote it — `gkm1.<keyId>.<payload>` — so\n * `decrypt` never has to guess, a rotated key still opens what it wrote, and\n * retiring a key is a decision made against data rather than a hope. The\n * format version leads, so the day it has to change, both can be read.\n *\n * Two backends, picked by the scheme of the construct's URL, the way a queue's\n * connection string picks its broker:\n *\n * - `aes256gcm://` — a keyring this process holds: AES-256-GCM with the newest\n * key, HMAC-SHA256 for the blind index. Local, test and server stages.\n * - `kms://` — envelope encryption under an AWS KMS key, and a KMS HMAC key for\n * the index. The key material never leaves KMS. See `./kms`.\n */\n\nimport {\n\tcreateCipheriv,\n\tcreateDecipheriv,\n\tcreateHmac,\n\trandomBytes,\n} from 'node:crypto';\n\n/** What a handler is given: `services.pii.encrypt(…)` and the rest. */\nexport interface Cipher {\n\t/** Plaintext in, a self-describing ciphertext string out. */\n\tencrypt(plaintext: string): Promise<string>;\n\t/** Opens any ciphertext this construct wrote, under whichever key wrote it. */\n\tdecrypt(ciphertext: string): Promise<string>;\n\t/**\n\t * A blind index: the same value always gives the same string, so an\n\t * encrypted column can still be looked up by an indexed column beside it.\n\t *\n\t * Deterministic on purpose, which is also why its key never rotates — a\n\t * rotated index key would make every stored index miss.\n\t */\n\tindex(value: string): Promise<string>;\n\t/**\n\t * The same plaintext under the current key — what a sweep runs over a\n\t * column before an old key is retired. Unchanged when it is already current.\n\t */\n\treencrypt(ciphertext: string): Promise<string>;\n}\n\n/** The format every ciphertext leads with. */\nexport const CIPHERTEXT_VERSION = 'gkm1';\n\n/** AES-256-GCM's nonce and tag, in bytes. */\nconst IV_BYTES = 12;\nconst TAG_BYTES = 16;\nconst KEY_BYTES = 32;\n\nexport const LOCAL_SCHEME = 'aes256gcm:';\n\nexport class EncryptionUrlInvalid extends Error {\n\tconstructor(\n\t\treadonly construct: string,\n\t\treadonly reason: string,\n\t) {\n\t\tsuper(\n\t\t\t`${construct}'s encryption URL is not usable: ${reason}. gkm composes it — derived locally, generated into a server stage's secrets on its first deploy, provisioned on AWS — so it should not be set by hand.`,\n\t\t);\n\t\tthis.name = 'EncryptionUrlInvalid';\n\t}\n}\n\nexport class UnknownEncryptionScheme extends Error {\n\tconstructor(\n\t\treadonly construct: string,\n\t\treadonly scheme: string,\n\t) {\n\t\tsuper(\n\t\t\t`${construct}'s encryption URL uses '${scheme}', which no cipher handles. Expected 'aes256gcm:' (a local keyring) or 'kms:' (AWS KMS).`,\n\t\t);\n\t\tthis.name = 'UnknownEncryptionScheme';\n\t}\n}\n\nexport class CiphertextMalformed extends Error {\n\tconstructor(readonly construct: string) {\n\t\tsuper(\n\t\t\t`${construct} was asked to decrypt something it did not write: a ciphertext starts '${CIPHERTEXT_VERSION}.<key>.'. Check the column holds what \\`encrypt\\` returned, untrimmed.`,\n\t\t);\n\t\tthis.name = 'CiphertextMalformed';\n\t}\n}\n\nexport class EncryptionKeyRetired extends Error {\n\tconstructor(\n\t\treadonly construct: string,\n\t\treadonly keyId: string,\n\t) {\n\t\tsuper(\n\t\t\t`${construct} has no key '${keyId}' any more, so a value it wrote cannot be read. It was retired before every value was re-encrypted; restore it from the stage's secrets history, run the sweep, then retire it again.`,\n\t\t);\n\t\tthis.name = 'EncryptionKeyRetired';\n\t}\n}\n\nexport class CiphertextNotAuthentic extends Error {\n\tconstructor(readonly construct: string) {\n\t\tsuper(\n\t\t\t`${construct} could not authenticate a ciphertext: it was altered, or written by a different Encryption construct. Ciphertexts are bound to the construct that wrote them.`,\n\t\t);\n\t\tthis.name = 'CiphertextNotAuthentic';\n\t}\n}\n\nexport class CurrentKeyCannotRetire extends Error {\n\tconstructor(\n\t\treadonly construct: string,\n\t\treadonly keyId: string,\n\t) {\n\t\tsuper(\n\t\t\t`'${keyId}' is ${construct}'s current key — everything new is written with it. Rotate first, sweep, then retire '${keyId}'.`,\n\t\t);\n\t\tthis.name = 'CurrentKeyCannotRetire';\n\t}\n}\n\n/** A ciphertext's three parts. */\nexport function parseCiphertext(\n\tconstruct: string,\n\tciphertext: string,\n): { keyId: string; payload: Buffer } {\n\tconst [version, keyId, payload, ...rest] = ciphertext.split('.');\n\tif (version !== CIPHERTEXT_VERSION || !keyId || !payload || rest.length) {\n\t\tthrow new CiphertextMalformed(construct);\n\t}\n\treturn { keyId, payload: Buffer.from(payload, 'base64url') };\n}\n\nexport function formatCiphertext(keyId: string, payload: Buffer): string {\n\treturn `${CIPHERTEXT_VERSION}.${keyId}.${payload.toString('base64url')}`;\n}\n\n/**\n * What the cipher authenticates alongside the data: the construct and the key.\n *\n * Binding the construct means a value copied from one `Encryption` into\n * another's column fails instead of decrypting under the wrong policy.\n */\nexport function associatedData(construct: string, keyId: string): Buffer {\n\treturn Buffer.from(`${CIPHERTEXT_VERSION}:${construct}:${keyId}`);\n}\n\n/** `iv | tag | ciphertext`, under one AES-256-GCM key. */\nexport function seal(key: Buffer, plaintext: Buffer, aad: Buffer): Buffer {\n\tconst iv = randomBytes(IV_BYTES);\n\tconst cipher = createCipheriv('aes-256-gcm', key, iv);\n\tcipher.setAAD(aad);\n\tconst body = Buffer.concat([cipher.update(plaintext), cipher.final()]);\n\treturn Buffer.concat([iv, cipher.getAuthTag(), body]);\n}\n\n/** The inverse of {@link seal}; `undefined` when it does not authenticate. */\nexport function open(\n\tkey: Buffer,\n\tsealed: Buffer,\n\taad: Buffer,\n): Buffer | undefined {\n\tif (sealed.length < IV_BYTES + TAG_BYTES) return undefined;\n\n\tconst decipher = createDecipheriv(\n\t\t'aes-256-gcm',\n\t\tkey,\n\t\tsealed.subarray(0, IV_BYTES),\n\t);\n\tdecipher.setAAD(aad);\n\tdecipher.setAuthTag(sealed.subarray(IV_BYTES, IV_BYTES + TAG_BYTES));\n\n\ttry {\n\t\treturn Buffer.concat([\n\t\t\tdecipher.update(sealed.subarray(IV_BYTES + TAG_BYTES)),\n\t\t\tdecipher.final(),\n\t\t]);\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\n/** A local keyring: newest key first, and the index key that never rotates. */\nexport interface Keyring {\n\tkeys: { id: string; key: Buffer }[];\n\tindex: Buffer;\n}\n\n/** Parse `aes256gcm://local?keys=k2.<key>,k1.<key>&index=<key>`. */\nexport function parseKeyring(construct: string, url: string): Keyring {\n\tlet parsed: URL;\n\ttry {\n\t\tparsed = new URL(url);\n\t} catch {\n\t\tthrow new EncryptionUrlInvalid(construct, 'it is not a URL');\n\t}\n\n\tconst keys = (parsed.searchParams.get('keys') ?? '')\n\t\t.split(',')\n\t\t.filter(Boolean)\n\t\t.map((entry) => {\n\t\t\tconst [id, encoded] = entry.split('.');\n\t\t\tconst key = Buffer.from(encoded ?? '', 'base64url');\n\t\t\tif (!id || key.length !== KEY_BYTES) {\n\t\t\t\tthrow new EncryptionUrlInvalid(\n\t\t\t\t\tconstruct,\n\t\t\t\t\t`'${id ?? entry}' is not a 256-bit key`,\n\t\t\t\t);\n\t\t\t}\n\t\t\treturn { id, key };\n\t\t});\n\tif (keys.length === 0) {\n\t\tthrow new EncryptionUrlInvalid(construct, 'it holds no keys');\n\t}\n\n\tconst index = Buffer.from(\n\t\tparsed.searchParams.get('index') ?? '',\n\t\t'base64url',\n\t);\n\tif (index.length !== KEY_BYTES) {\n\t\tthrow new EncryptionUrlInvalid(construct, 'its index key is missing');\n\t}\n\n\treturn { keys, index };\n}\n\nexport function formatKeyring(keyring: Keyring): string {\n\tconst keys = keyring.keys\n\t\t.map(({ id, key }) => `${id}.${key.toString('base64url')}`)\n\t\t.join(',');\n\treturn `${LOCAL_SCHEME}//local?keys=${keys}&index=${keyring.index.toString('base64url')}`;\n}\n\n/** A keyring with one fresh key, `k1`, and a fresh index key. */\nexport function generateKeyring(): string {\n\treturn formatKeyring({\n\t\tkeys: [{ id: 'k1', key: randomBytes(KEY_BYTES) }],\n\t\tindex: randomBytes(KEY_BYTES),\n\t});\n}\n\n/**\n * The keyring with a new current key in front, and every older one kept.\n *\n * Nothing is dropped: what the old keys wrote still has to open until a sweep\n * has re-encrypted it — see {@link retireKey}.\n */\nexport function rotateKeyring(construct: string, url: string): string {\n\tconst keyring = parseKeyring(construct, url);\n\tconst next =\n\t\tMath.max(...keyring.keys.map(({ id }) => Number(id.slice(1)) || 0)) + 1;\n\n\treturn formatKeyring({\n\t\t...keyring,\n\t\tkeys: [{ id: `k${next}`, key: randomBytes(KEY_BYTES) }, ...keyring.keys],\n\t});\n}\n\n/** The keyring without `keyId`, which must not be the current key. */\nexport function retireKey(\n\tconstruct: string,\n\turl: string,\n\tkeyId: string,\n): string {\n\tconst keyring = parseKeyring(construct, url);\n\tif (keyring.keys[0]?.id === keyId) {\n\t\tthrow new CurrentKeyCannotRetire(construct, keyId);\n\t}\n\tif (!keyring.keys.some(({ id }) => id === keyId)) {\n\t\tthrow new EncryptionKeyRetired(construct, keyId);\n\t}\n\n\treturn formatKeyring({\n\t\t...keyring,\n\t\tkeys: keyring.keys.filter(({ id }) => id !== keyId),\n\t});\n}\n\n/** Says, once per key per process, that a value is still under an old key. */\nexport type StaleKeyReporter = (keyId: string, current: string) => void;\n\n/** The `aes256gcm://` cipher: a keyring held in process. */\nexport function keyringCipher(\n\tconstruct: string,\n\turl: string,\n\tonStaleKey: StaleKeyReporter = () => {},\n): Cipher {\n\tconst keyring = parseKeyring(construct, url);\n\tconst current = keyring.keys[0]!;\n\tconst byId = new Map(keyring.keys.map(({ id, key }) => [id, key]));\n\tconst reported = new Set<string>();\n\n\tconst decrypt = async (ciphertext: string) => {\n\t\tconst { keyId, payload } = parseCiphertext(construct, ciphertext);\n\t\tconst key = byId.get(keyId);\n\t\tif (!key) throw new EncryptionKeyRetired(construct, keyId);\n\n\t\tconst plaintext = open(key, payload, associatedData(construct, keyId));\n\t\tif (!plaintext) throw new CiphertextNotAuthentic(construct);\n\n\t\tif (keyId !== current.id && !reported.has(keyId)) {\n\t\t\treported.add(keyId);\n\t\t\tonStaleKey(keyId, current.id);\n\t\t}\n\n\t\treturn plaintext.toString('utf8');\n\t};\n\n\tconst encrypt = async (plaintext: string) =>\n\t\tformatCiphertext(\n\t\t\tcurrent.id,\n\t\t\tseal(\n\t\t\t\tcurrent.key,\n\t\t\t\tBuffer.from(plaintext, 'utf8'),\n\t\t\t\tassociatedData(construct, current.id),\n\t\t\t),\n\t\t);\n\n\treturn {\n\t\tencrypt,\n\t\tdecrypt,\n\t\tindex: async (value) =>\n\t\t\tcreateHmac('sha256', keyring.index)\n\t\t\t\t.update(value, 'utf8')\n\t\t\t\t.digest('base64url'),\n\t\treencrypt: async (ciphertext) => {\n\t\t\tconst { keyId } = parseCiphertext(construct, ciphertext);\n\t\t\tif (keyId === current.id) return ciphertext;\n\t\t\treturn encrypt(await decrypt(ciphertext));\n\t\t},\n\t};\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"cipher-C1lEFX63.mjs","names":[],"sources":["../src/encryption/cipher.ts"],"sourcesContent":["/**\n * The cipher behind an `Encryption` construct, and the format it writes.\n *\n * Every ciphertext names the key that wrote it — `gkm1.<keyId>.<payload>` — so\n * `decrypt` never has to guess, a rotated key still opens what it wrote, and\n * retiring a key is a decision made against data rather than a hope. The\n * format version leads, so the day it has to change, both can be read.\n *\n * Two backends, picked by the scheme of the construct's URL, the way a queue's\n * connection string picks its broker:\n *\n * - `aes256gcm://` — a keyring this process holds: AES-256-GCM with the newest\n * key, HMAC-SHA256 for the blind index. Local, test and server stages.\n * - `kms://` — envelope encryption under an AWS KMS key, and a KMS HMAC key for\n * the index. The key material never leaves KMS. See `./kms`.\n */\n\nimport {\n\tcreateCipheriv,\n\tcreateDecipheriv,\n\tcreateHmac,\n\trandomBytes,\n} from 'node:crypto';\n\n/** What a handler is given: `services.pii.encrypt(…)` and the rest. */\nexport interface Cipher {\n\t/** Plaintext in, a self-describing ciphertext string out. */\n\tencrypt(plaintext: string): Promise<string>;\n\t/** Opens any ciphertext this construct wrote, under whichever key wrote it. */\n\tdecrypt(ciphertext: string): Promise<string>;\n\t/**\n\t * A blind index: the same value always gives the same string, so an\n\t * encrypted column can still be looked up by an indexed column beside it.\n\t *\n\t * Deterministic on purpose, which is also why its key never rotates — a\n\t * rotated index key would make every stored index miss.\n\t */\n\tindex(value: string): Promise<string>;\n\t/**\n\t * The same plaintext under the current key — what a sweep runs over a\n\t * column before an old key is retired. Unchanged when it is already current.\n\t */\n\treencrypt(ciphertext: string): Promise<string>;\n}\n\n/** The format every ciphertext leads with. */\nexport const CIPHERTEXT_VERSION = 'gkm1';\n\n/** AES-256-GCM's nonce and tag, in bytes. */\nconst IV_BYTES = 12;\nconst TAG_BYTES = 16;\nconst KEY_BYTES = 32;\n\nexport const LOCAL_SCHEME = 'aes256gcm:';\n\nexport class EncryptionUrlInvalid extends Error {\n\tconstructor(\n\t\treadonly construct: string,\n\t\treadonly reason: string,\n\t) {\n\t\tsuper(\n\t\t\t`${construct}'s encryption URL is not usable: ${reason}. gkm composes it — derived locally, generated into a server stage's secrets on its first deploy, provisioned on AWS — so it should not be set by hand.`,\n\t\t);\n\t\tthis.name = 'EncryptionUrlInvalid';\n\t}\n}\n\nexport class UnknownEncryptionScheme extends Error {\n\tconstructor(\n\t\treadonly construct: string,\n\t\treadonly scheme: string,\n\t) {\n\t\tsuper(\n\t\t\t`${construct}'s encryption URL uses '${scheme}', which no cipher handles. Expected 'aes256gcm:' (a local keyring) or 'kms:' (AWS KMS).`,\n\t\t);\n\t\tthis.name = 'UnknownEncryptionScheme';\n\t}\n}\n\nexport class CiphertextMalformed extends Error {\n\tconstructor(readonly construct: string) {\n\t\tsuper(\n\t\t\t`${construct} was asked to decrypt something it did not write: a ciphertext starts '${CIPHERTEXT_VERSION}.<key>.'. Check the column holds what \\`encrypt\\` returned, untrimmed.`,\n\t\t);\n\t\tthis.name = 'CiphertextMalformed';\n\t}\n}\n\nexport class EncryptionKeyRetired extends Error {\n\tconstructor(\n\t\treadonly construct: string,\n\t\treadonly keyId: string,\n\t) {\n\t\tsuper(\n\t\t\t`${construct} has no key '${keyId}' any more, so a value it wrote cannot be read. It was retired before every value was re-encrypted; restore it from the stage's secrets history, run the sweep, then retire it again.`,\n\t\t);\n\t\tthis.name = 'EncryptionKeyRetired';\n\t}\n}\n\nexport class CiphertextNotAuthentic extends Error {\n\tconstructor(readonly construct: string) {\n\t\tsuper(\n\t\t\t`${construct} could not authenticate a ciphertext: it was altered, or written by a different Encryption construct. Ciphertexts are bound to the construct that wrote them.`,\n\t\t);\n\t\tthis.name = 'CiphertextNotAuthentic';\n\t}\n}\n\nexport class CurrentKeyCannotRetire extends Error {\n\tconstructor(\n\t\treadonly construct: string,\n\t\treadonly keyId: string,\n\t) {\n\t\tsuper(\n\t\t\t`'${keyId}' is ${construct}'s current key — everything new is written with it. Rotate first, sweep, then retire '${keyId}'.`,\n\t\t);\n\t\tthis.name = 'CurrentKeyCannotRetire';\n\t}\n}\n\n/** A ciphertext's three parts. */\nexport function parseCiphertext(\n\tconstruct: string,\n\tciphertext: string,\n): { keyId: string; payload: Buffer } {\n\tconst [version, keyId, payload, ...rest] = ciphertext.split('.');\n\tif (version !== CIPHERTEXT_VERSION || !keyId || !payload || rest.length) {\n\t\tthrow new CiphertextMalformed(construct);\n\t}\n\treturn { keyId, payload: Buffer.from(payload, 'base64url') };\n}\n\nexport function formatCiphertext(keyId: string, payload: Buffer): string {\n\treturn `${CIPHERTEXT_VERSION}.${keyId}.${payload.toString('base64url')}`;\n}\n\n/**\n * What the cipher authenticates alongside the data: the construct and the key.\n *\n * Binding the construct means a value copied from one `Encryption` into\n * another's column fails instead of decrypting under the wrong policy.\n */\nexport function associatedData(construct: string, keyId: string): Buffer {\n\treturn Buffer.from(`${CIPHERTEXT_VERSION}:${construct}:${keyId}`);\n}\n\n/** `iv | tag | ciphertext`, under one AES-256-GCM key. */\nexport function seal(key: Buffer, plaintext: Buffer, aad: Buffer): Buffer {\n\tconst iv = randomBytes(IV_BYTES);\n\tconst cipher = createCipheriv('aes-256-gcm', key, iv);\n\tcipher.setAAD(aad);\n\tconst body = Buffer.concat([cipher.update(plaintext), cipher.final()]);\n\treturn Buffer.concat([iv, cipher.getAuthTag(), body]);\n}\n\n/** The inverse of {@link seal}; `undefined` when it does not authenticate. */\nexport function open(\n\tkey: Buffer,\n\tsealed: Buffer,\n\taad: Buffer,\n): Buffer | undefined {\n\tif (sealed.length < IV_BYTES + TAG_BYTES) return undefined;\n\n\tconst decipher = createDecipheriv(\n\t\t'aes-256-gcm',\n\t\tkey,\n\t\tsealed.subarray(0, IV_BYTES),\n\t);\n\tdecipher.setAAD(aad);\n\tdecipher.setAuthTag(sealed.subarray(IV_BYTES, IV_BYTES + TAG_BYTES));\n\n\ttry {\n\t\treturn Buffer.concat([\n\t\t\tdecipher.update(sealed.subarray(IV_BYTES + TAG_BYTES)),\n\t\t\tdecipher.final(),\n\t\t]);\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\n/** A local keyring: newest key first, and the index key that never rotates. */\nexport interface Keyring {\n\tkeys: { id: string; key: Buffer }[];\n\tindex: Buffer;\n}\n\n/** Parse `aes256gcm://local?keys=k2.<key>,k1.<key>&index=<key>`. */\nexport function parseKeyring(construct: string, url: string): Keyring {\n\tlet parsed: URL;\n\ttry {\n\t\tparsed = new URL(url);\n\t} catch {\n\t\tthrow new EncryptionUrlInvalid(construct, 'it is not a URL');\n\t}\n\n\tconst keys = (parsed.searchParams.get('keys') ?? '')\n\t\t.split(',')\n\t\t.filter(Boolean)\n\t\t.map((entry) => {\n\t\t\tconst [id, encoded] = entry.split('.');\n\t\t\tconst key = Buffer.from(encoded ?? '', 'base64url');\n\t\t\tif (!id || key.length !== KEY_BYTES) {\n\t\t\t\tthrow new EncryptionUrlInvalid(\n\t\t\t\t\tconstruct,\n\t\t\t\t\t`'${id ?? entry}' is not a 256-bit key`,\n\t\t\t\t);\n\t\t\t}\n\t\t\treturn { id, key };\n\t\t});\n\tif (keys.length === 0) {\n\t\tthrow new EncryptionUrlInvalid(construct, 'it holds no keys');\n\t}\n\n\tconst index = Buffer.from(\n\t\tparsed.searchParams.get('index') ?? '',\n\t\t'base64url',\n\t);\n\tif (index.length !== KEY_BYTES) {\n\t\tthrow new EncryptionUrlInvalid(construct, 'its index key is missing');\n\t}\n\n\treturn { keys, index };\n}\n\nexport function formatKeyring(keyring: Keyring): string {\n\tconst keys = keyring.keys\n\t\t.map(({ id, key }) => `${id}.${key.toString('base64url')}`)\n\t\t.join(',');\n\treturn `${LOCAL_SCHEME}//local?keys=${keys}&index=${keyring.index.toString('base64url')}`;\n}\n\n/** A keyring with one fresh key, `k1`, and a fresh index key. */\nexport function generateKeyring(): string {\n\treturn formatKeyring({\n\t\tkeys: [{ id: 'k1', key: randomBytes(KEY_BYTES) }],\n\t\tindex: randomBytes(KEY_BYTES),\n\t});\n}\n\n/**\n * The keyring with a new current key in front, and every older one kept.\n *\n * Nothing is dropped: what the old keys wrote still has to open until a sweep\n * has re-encrypted it — see {@link retireKey}.\n */\nexport function rotateKeyring(construct: string, url: string): string {\n\tconst keyring = parseKeyring(construct, url);\n\tconst next =\n\t\tMath.max(...keyring.keys.map(({ id }) => Number(id.slice(1)) || 0)) + 1;\n\n\treturn formatKeyring({\n\t\t...keyring,\n\t\tkeys: [{ id: `k${next}`, key: randomBytes(KEY_BYTES) }, ...keyring.keys],\n\t});\n}\n\n/** The keyring without `keyId`, which must not be the current key. */\nexport function retireKey(\n\tconstruct: string,\n\turl: string,\n\tkeyId: string,\n): string {\n\tconst keyring = parseKeyring(construct, url);\n\tif (keyring.keys[0]?.id === keyId) {\n\t\tthrow new CurrentKeyCannotRetire(construct, keyId);\n\t}\n\tif (!keyring.keys.some(({ id }) => id === keyId)) {\n\t\tthrow new EncryptionKeyRetired(construct, keyId);\n\t}\n\n\treturn formatKeyring({\n\t\t...keyring,\n\t\tkeys: keyring.keys.filter(({ id }) => id !== keyId),\n\t});\n}\n\n/** Says, once per key per process, that a value is still under an old key. */\nexport type StaleKeyReporter = (keyId: string, current: string) => void;\n\n/** The `aes256gcm://` cipher: a keyring held in process. */\nexport function keyringCipher(\n\tconstruct: string,\n\turl: string,\n\tonStaleKey: StaleKeyReporter = () => {},\n): Cipher {\n\tconst keyring = parseKeyring(construct, url);\n\tconst current = keyring.keys[0]!;\n\tconst byId = new Map(keyring.keys.map(({ id, key }) => [id, key]));\n\tconst reported = new Set<string>();\n\n\tconst decrypt = async (ciphertext: string) => {\n\t\tconst { keyId, payload } = parseCiphertext(construct, ciphertext);\n\t\tconst key = byId.get(keyId);\n\t\tif (!key) throw new EncryptionKeyRetired(construct, keyId);\n\n\t\tconst plaintext = open(key, payload, associatedData(construct, keyId));\n\t\tif (!plaintext) throw new CiphertextNotAuthentic(construct);\n\n\t\tif (keyId !== current.id && !reported.has(keyId)) {\n\t\t\treported.add(keyId);\n\t\t\tonStaleKey(keyId, current.id);\n\t\t}\n\n\t\treturn plaintext.toString('utf8');\n\t};\n\n\tconst encrypt = async (plaintext: string) =>\n\t\tformatCiphertext(\n\t\t\tcurrent.id,\n\t\t\tseal(\n\t\t\t\tcurrent.key,\n\t\t\t\tBuffer.from(plaintext, 'utf8'),\n\t\t\t\tassociatedData(construct, current.id),\n\t\t\t),\n\t\t);\n\n\treturn {\n\t\tencrypt,\n\t\tdecrypt,\n\t\tindex: async (value) =>\n\t\t\tcreateHmac('sha256', keyring.index)\n\t\t\t\t.update(value, 'utf8')\n\t\t\t\t.digest('base64url'),\n\t\treencrypt: async (ciphertext) => {\n\t\t\tconst { keyId } = parseCiphertext(construct, ciphertext);\n\t\t\tif (keyId === current.id) return ciphertext;\n\t\t\treturn encrypt(await decrypt(ciphertext));\n\t\t},\n\t};\n}\n\n/**\n * A cipher for a URL scheme other than `aes256gcm:` — `kms:` on AWS.\n *\n * Registered by the entry point a build generates, and only for a target\n * whose keys live there, so a process that never holds such a key never\n * imports the SDK it would need.\n */\nexport interface CipherDriver {\n\t/** The scheme it opens, with its colon: `'kms:'`. */\n\treadonly scheme: string;\n\topen(construct: string, url: string): Cipher;\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"cipher-CVxfwLjk.cjs","names":["randomBytes","createCipheriv","createDecipheriv","createHmac"],"sources":["../src/encryption/cipher.ts"],"sourcesContent":["/**\n * The cipher behind an `Encryption` construct, and the format it writes.\n *\n * Every ciphertext names the key that wrote it — `gkm1.<keyId>.<payload>` — so\n * `decrypt` never has to guess, a rotated key still opens what it wrote, and\n * retiring a key is a decision made against data rather than a hope. The\n * format version leads, so the day it has to change, both can be read.\n *\n * Two backends, picked by the scheme of the construct's URL, the way a queue's\n * connection string picks its broker:\n *\n * - `aes256gcm://` — a keyring this process holds: AES-256-GCM with the newest\n * key, HMAC-SHA256 for the blind index. Local, test and server stages.\n * - `kms://` — envelope encryption under an AWS KMS key, and a KMS HMAC key for\n * the index. The key material never leaves KMS. See `./kms`.\n */\n\nimport {\n\tcreateCipheriv,\n\tcreateDecipheriv,\n\tcreateHmac,\n\trandomBytes,\n} from 'node:crypto';\n\n/** What a handler is given: `services.pii.encrypt(…)` and the rest. */\nexport interface Cipher {\n\t/** Plaintext in, a self-describing ciphertext string out. */\n\tencrypt(plaintext: string): Promise<string>;\n\t/** Opens any ciphertext this construct wrote, under whichever key wrote it. */\n\tdecrypt(ciphertext: string): Promise<string>;\n\t/**\n\t * A blind index: the same value always gives the same string, so an\n\t * encrypted column can still be looked up by an indexed column beside it.\n\t *\n\t * Deterministic on purpose, which is also why its key never rotates — a\n\t * rotated index key would make every stored index miss.\n\t */\n\tindex(value: string): Promise<string>;\n\t/**\n\t * The same plaintext under the current key — what a sweep runs over a\n\t * column before an old key is retired. Unchanged when it is already current.\n\t */\n\treencrypt(ciphertext: string): Promise<string>;\n}\n\n/** The format every ciphertext leads with. */\nexport const CIPHERTEXT_VERSION = 'gkm1';\n\n/** AES-256-GCM's nonce and tag, in bytes. */\nconst IV_BYTES = 12;\nconst TAG_BYTES = 16;\nconst KEY_BYTES = 32;\n\nexport const LOCAL_SCHEME = 'aes256gcm:';\n\nexport class EncryptionUrlInvalid extends Error {\n\tconstructor(\n\t\treadonly construct: string,\n\t\treadonly reason: string,\n\t) {\n\t\tsuper(\n\t\t\t`${construct}'s encryption URL is not usable: ${reason}. gkm composes it — derived locally, generated into a server stage's secrets on its first deploy, provisioned on AWS — so it should not be set by hand.`,\n\t\t);\n\t\tthis.name = 'EncryptionUrlInvalid';\n\t}\n}\n\nexport class UnknownEncryptionScheme extends Error {\n\tconstructor(\n\t\treadonly construct: string,\n\t\treadonly scheme: string,\n\t) {\n\t\tsuper(\n\t\t\t`${construct}'s encryption URL uses '${scheme}', which no cipher handles. Expected 'aes256gcm:' (a local keyring) or 'kms:' (AWS KMS).`,\n\t\t);\n\t\tthis.name = 'UnknownEncryptionScheme';\n\t}\n}\n\nexport class CiphertextMalformed extends Error {\n\tconstructor(readonly construct: string) {\n\t\tsuper(\n\t\t\t`${construct} was asked to decrypt something it did not write: a ciphertext starts '${CIPHERTEXT_VERSION}.<key>.'. Check the column holds what \\`encrypt\\` returned, untrimmed.`,\n\t\t);\n\t\tthis.name = 'CiphertextMalformed';\n\t}\n}\n\nexport class EncryptionKeyRetired extends Error {\n\tconstructor(\n\t\treadonly construct: string,\n\t\treadonly keyId: string,\n\t) {\n\t\tsuper(\n\t\t\t`${construct} has no key '${keyId}' any more, so a value it wrote cannot be read. It was retired before every value was re-encrypted; restore it from the stage's secrets history, run the sweep, then retire it again.`,\n\t\t);\n\t\tthis.name = 'EncryptionKeyRetired';\n\t}\n}\n\nexport class CiphertextNotAuthentic extends Error {\n\tconstructor(readonly construct: string) {\n\t\tsuper(\n\t\t\t`${construct} could not authenticate a ciphertext: it was altered, or written by a different Encryption construct. Ciphertexts are bound to the construct that wrote them.`,\n\t\t);\n\t\tthis.name = 'CiphertextNotAuthentic';\n\t}\n}\n\nexport class CurrentKeyCannotRetire extends Error {\n\tconstructor(\n\t\treadonly construct: string,\n\t\treadonly keyId: string,\n\t) {\n\t\tsuper(\n\t\t\t`'${keyId}' is ${construct}'s current key — everything new is written with it. Rotate first, sweep, then retire '${keyId}'.`,\n\t\t);\n\t\tthis.name = 'CurrentKeyCannotRetire';\n\t}\n}\n\n/** A ciphertext's three parts. */\nexport function parseCiphertext(\n\tconstruct: string,\n\tciphertext: string,\n): { keyId: string; payload: Buffer } {\n\tconst [version, keyId, payload, ...rest] = ciphertext.split('.');\n\tif (version !== CIPHERTEXT_VERSION || !keyId || !payload || rest.length) {\n\t\tthrow new CiphertextMalformed(construct);\n\t}\n\treturn { keyId, payload: Buffer.from(payload, 'base64url') };\n}\n\nexport function formatCiphertext(keyId: string, payload: Buffer): string {\n\treturn `${CIPHERTEXT_VERSION}.${keyId}.${payload.toString('base64url')}`;\n}\n\n/**\n * What the cipher authenticates alongside the data: the construct and the key.\n *\n * Binding the construct means a value copied from one `Encryption` into\n * another's column fails instead of decrypting under the wrong policy.\n */\nexport function associatedData(construct: string, keyId: string): Buffer {\n\treturn Buffer.from(`${CIPHERTEXT_VERSION}:${construct}:${keyId}`);\n}\n\n/** `iv | tag | ciphertext`, under one AES-256-GCM key. */\nexport function seal(key: Buffer, plaintext: Buffer, aad: Buffer): Buffer {\n\tconst iv = randomBytes(IV_BYTES);\n\tconst cipher = createCipheriv('aes-256-gcm', key, iv);\n\tcipher.setAAD(aad);\n\tconst body = Buffer.concat([cipher.update(plaintext), cipher.final()]);\n\treturn Buffer.concat([iv, cipher.getAuthTag(), body]);\n}\n\n/** The inverse of {@link seal}; `undefined` when it does not authenticate. */\nexport function open(\n\tkey: Buffer,\n\tsealed: Buffer,\n\taad: Buffer,\n): Buffer | undefined {\n\tif (sealed.length < IV_BYTES + TAG_BYTES) return undefined;\n\n\tconst decipher = createDecipheriv(\n\t\t'aes-256-gcm',\n\t\tkey,\n\t\tsealed.subarray(0, IV_BYTES),\n\t);\n\tdecipher.setAAD(aad);\n\tdecipher.setAuthTag(sealed.subarray(IV_BYTES, IV_BYTES + TAG_BYTES));\n\n\ttry {\n\t\treturn Buffer.concat([\n\t\t\tdecipher.update(sealed.subarray(IV_BYTES + TAG_BYTES)),\n\t\t\tdecipher.final(),\n\t\t]);\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\n/** A local keyring: newest key first, and the index key that never rotates. */\nexport interface Keyring {\n\tkeys: { id: string; key: Buffer }[];\n\tindex: Buffer;\n}\n\n/** Parse `aes256gcm://local?keys=k2.<key>,k1.<key>&index=<key>`. */\nexport function parseKeyring(construct: string, url: string): Keyring {\n\tlet parsed: URL;\n\ttry {\n\t\tparsed = new URL(url);\n\t} catch {\n\t\tthrow new EncryptionUrlInvalid(construct, 'it is not a URL');\n\t}\n\n\tconst keys = (parsed.searchParams.get('keys') ?? '')\n\t\t.split(',')\n\t\t.filter(Boolean)\n\t\t.map((entry) => {\n\t\t\tconst [id, encoded] = entry.split('.');\n\t\t\tconst key = Buffer.from(encoded ?? '', 'base64url');\n\t\t\tif (!id || key.length !== KEY_BYTES) {\n\t\t\t\tthrow new EncryptionUrlInvalid(\n\t\t\t\t\tconstruct,\n\t\t\t\t\t`'${id ?? entry}' is not a 256-bit key`,\n\t\t\t\t);\n\t\t\t}\n\t\t\treturn { id, key };\n\t\t});\n\tif (keys.length === 0) {\n\t\tthrow new EncryptionUrlInvalid(construct, 'it holds no keys');\n\t}\n\n\tconst index = Buffer.from(\n\t\tparsed.searchParams.get('index') ?? '',\n\t\t'base64url',\n\t);\n\tif (index.length !== KEY_BYTES) {\n\t\tthrow new EncryptionUrlInvalid(construct, 'its index key is missing');\n\t}\n\n\treturn { keys, index };\n}\n\nexport function formatKeyring(keyring: Keyring): string {\n\tconst keys = keyring.keys\n\t\t.map(({ id, key }) => `${id}.${key.toString('base64url')}`)\n\t\t.join(',');\n\treturn `${LOCAL_SCHEME}//local?keys=${keys}&index=${keyring.index.toString('base64url')}`;\n}\n\n/** A keyring with one fresh key, `k1`, and a fresh index key. */\nexport function generateKeyring(): string {\n\treturn formatKeyring({\n\t\tkeys: [{ id: 'k1', key: randomBytes(KEY_BYTES) }],\n\t\tindex: randomBytes(KEY_BYTES),\n\t});\n}\n\n/**\n * The keyring with a new current key in front, and every older one kept.\n *\n * Nothing is dropped: what the old keys wrote still has to open until a sweep\n * has re-encrypted it — see {@link retireKey}.\n */\nexport function rotateKeyring(construct: string, url: string): string {\n\tconst keyring = parseKeyring(construct, url);\n\tconst next =\n\t\tMath.max(...keyring.keys.map(({ id }) => Number(id.slice(1)) || 0)) + 1;\n\n\treturn formatKeyring({\n\t\t...keyring,\n\t\tkeys: [{ id: `k${next}`, key: randomBytes(KEY_BYTES) }, ...keyring.keys],\n\t});\n}\n\n/** The keyring without `keyId`, which must not be the current key. */\nexport function retireKey(\n\tconstruct: string,\n\turl: string,\n\tkeyId: string,\n): string {\n\tconst keyring = parseKeyring(construct, url);\n\tif (keyring.keys[0]?.id === keyId) {\n\t\tthrow new CurrentKeyCannotRetire(construct, keyId);\n\t}\n\tif (!keyring.keys.some(({ id }) => id === keyId)) {\n\t\tthrow new EncryptionKeyRetired(construct, keyId);\n\t}\n\n\treturn formatKeyring({\n\t\t...keyring,\n\t\tkeys: keyring.keys.filter(({ id }) => id !== keyId),\n\t});\n}\n\n/** Says, once per key per process, that a value is still under an old key. */\nexport type StaleKeyReporter = (keyId: string, current: string) => void;\n\n/** The `aes256gcm://` cipher: a keyring held in process. */\nexport function keyringCipher(\n\tconstruct: string,\n\turl: string,\n\tonStaleKey: StaleKeyReporter = () => {},\n): Cipher {\n\tconst keyring = parseKeyring(construct, url);\n\tconst current = keyring.keys[0]!;\n\tconst byId = new Map(keyring.keys.map(({ id, key }) => [id, key]));\n\tconst reported = new Set<string>();\n\n\tconst decrypt = async (ciphertext: string) => {\n\t\tconst { keyId, payload } = parseCiphertext(construct, ciphertext);\n\t\tconst key = byId.get(keyId);\n\t\tif (!key) throw new EncryptionKeyRetired(construct, keyId);\n\n\t\tconst plaintext = open(key, payload, associatedData(construct, keyId));\n\t\tif (!plaintext) throw new CiphertextNotAuthentic(construct);\n\n\t\tif (keyId !== current.id && !reported.has(keyId)) {\n\t\t\treported.add(keyId);\n\t\t\tonStaleKey(keyId, current.id);\n\t\t}\n\n\t\treturn plaintext.toString('utf8');\n\t};\n\n\tconst encrypt = async (plaintext: string) =>\n\t\tformatCiphertext(\n\t\t\tcurrent.id,\n\t\t\tseal(\n\t\t\t\tcurrent.key,\n\t\t\t\tBuffer.from(plaintext, 'utf8'),\n\t\t\t\tassociatedData(construct, current.id),\n\t\t\t),\n\t\t);\n\n\treturn {\n\t\tencrypt,\n\t\tdecrypt,\n\t\tindex: async (value) =>\n\t\t\tcreateHmac('sha256', keyring.index)\n\t\t\t\t.update(value, 'utf8')\n\t\t\t\t.digest('base64url'),\n\t\treencrypt: async (ciphertext) => {\n\t\t\tconst { keyId } = parseCiphertext(construct, ciphertext);\n\t\t\tif (keyId === current.id) return ciphertext;\n\t\t\treturn encrypt(await decrypt(ciphertext));\n\t\t},\n\t};\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"cipher-CVxfwLjk.cjs","names":["randomBytes","createCipheriv","createDecipheriv","createHmac"],"sources":["../src/encryption/cipher.ts"],"sourcesContent":["/**\n * The cipher behind an `Encryption` construct, and the format it writes.\n *\n * Every ciphertext names the key that wrote it — `gkm1.<keyId>.<payload>` — so\n * `decrypt` never has to guess, a rotated key still opens what it wrote, and\n * retiring a key is a decision made against data rather than a hope. The\n * format version leads, so the day it has to change, both can be read.\n *\n * Two backends, picked by the scheme of the construct's URL, the way a queue's\n * connection string picks its broker:\n *\n * - `aes256gcm://` — a keyring this process holds: AES-256-GCM with the newest\n * key, HMAC-SHA256 for the blind index. Local, test and server stages.\n * - `kms://` — envelope encryption under an AWS KMS key, and a KMS HMAC key for\n * the index. The key material never leaves KMS. See `./kms`.\n */\n\nimport {\n\tcreateCipheriv,\n\tcreateDecipheriv,\n\tcreateHmac,\n\trandomBytes,\n} from 'node:crypto';\n\n/** What a handler is given: `services.pii.encrypt(…)` and the rest. */\nexport interface Cipher {\n\t/** Plaintext in, a self-describing ciphertext string out. */\n\tencrypt(plaintext: string): Promise<string>;\n\t/** Opens any ciphertext this construct wrote, under whichever key wrote it. */\n\tdecrypt(ciphertext: string): Promise<string>;\n\t/**\n\t * A blind index: the same value always gives the same string, so an\n\t * encrypted column can still be looked up by an indexed column beside it.\n\t *\n\t * Deterministic on purpose, which is also why its key never rotates — a\n\t * rotated index key would make every stored index miss.\n\t */\n\tindex(value: string): Promise<string>;\n\t/**\n\t * The same plaintext under the current key — what a sweep runs over a\n\t * column before an old key is retired. Unchanged when it is already current.\n\t */\n\treencrypt(ciphertext: string): Promise<string>;\n}\n\n/** The format every ciphertext leads with. */\nexport const CIPHERTEXT_VERSION = 'gkm1';\n\n/** AES-256-GCM's nonce and tag, in bytes. */\nconst IV_BYTES = 12;\nconst TAG_BYTES = 16;\nconst KEY_BYTES = 32;\n\nexport const LOCAL_SCHEME = 'aes256gcm:';\n\nexport class EncryptionUrlInvalid extends Error {\n\tconstructor(\n\t\treadonly construct: string,\n\t\treadonly reason: string,\n\t) {\n\t\tsuper(\n\t\t\t`${construct}'s encryption URL is not usable: ${reason}. gkm composes it — derived locally, generated into a server stage's secrets on its first deploy, provisioned on AWS — so it should not be set by hand.`,\n\t\t);\n\t\tthis.name = 'EncryptionUrlInvalid';\n\t}\n}\n\nexport class UnknownEncryptionScheme extends Error {\n\tconstructor(\n\t\treadonly construct: string,\n\t\treadonly scheme: string,\n\t) {\n\t\tsuper(\n\t\t\t`${construct}'s encryption URL uses '${scheme}', which no cipher handles. Expected 'aes256gcm:' (a local keyring) or 'kms:' (AWS KMS).`,\n\t\t);\n\t\tthis.name = 'UnknownEncryptionScheme';\n\t}\n}\n\nexport class CiphertextMalformed extends Error {\n\tconstructor(readonly construct: string) {\n\t\tsuper(\n\t\t\t`${construct} was asked to decrypt something it did not write: a ciphertext starts '${CIPHERTEXT_VERSION}.<key>.'. Check the column holds what \\`encrypt\\` returned, untrimmed.`,\n\t\t);\n\t\tthis.name = 'CiphertextMalformed';\n\t}\n}\n\nexport class EncryptionKeyRetired extends Error {\n\tconstructor(\n\t\treadonly construct: string,\n\t\treadonly keyId: string,\n\t) {\n\t\tsuper(\n\t\t\t`${construct} has no key '${keyId}' any more, so a value it wrote cannot be read. It was retired before every value was re-encrypted; restore it from the stage's secrets history, run the sweep, then retire it again.`,\n\t\t);\n\t\tthis.name = 'EncryptionKeyRetired';\n\t}\n}\n\nexport class CiphertextNotAuthentic extends Error {\n\tconstructor(readonly construct: string) {\n\t\tsuper(\n\t\t\t`${construct} could not authenticate a ciphertext: it was altered, or written by a different Encryption construct. Ciphertexts are bound to the construct that wrote them.`,\n\t\t);\n\t\tthis.name = 'CiphertextNotAuthentic';\n\t}\n}\n\nexport class CurrentKeyCannotRetire extends Error {\n\tconstructor(\n\t\treadonly construct: string,\n\t\treadonly keyId: string,\n\t) {\n\t\tsuper(\n\t\t\t`'${keyId}' is ${construct}'s current key — everything new is written with it. Rotate first, sweep, then retire '${keyId}'.`,\n\t\t);\n\t\tthis.name = 'CurrentKeyCannotRetire';\n\t}\n}\n\n/** A ciphertext's three parts. */\nexport function parseCiphertext(\n\tconstruct: string,\n\tciphertext: string,\n): { keyId: string; payload: Buffer } {\n\tconst [version, keyId, payload, ...rest] = ciphertext.split('.');\n\tif (version !== CIPHERTEXT_VERSION || !keyId || !payload || rest.length) {\n\t\tthrow new CiphertextMalformed(construct);\n\t}\n\treturn { keyId, payload: Buffer.from(payload, 'base64url') };\n}\n\nexport function formatCiphertext(keyId: string, payload: Buffer): string {\n\treturn `${CIPHERTEXT_VERSION}.${keyId}.${payload.toString('base64url')}`;\n}\n\n/**\n * What the cipher authenticates alongside the data: the construct and the key.\n *\n * Binding the construct means a value copied from one `Encryption` into\n * another's column fails instead of decrypting under the wrong policy.\n */\nexport function associatedData(construct: string, keyId: string): Buffer {\n\treturn Buffer.from(`${CIPHERTEXT_VERSION}:${construct}:${keyId}`);\n}\n\n/** `iv | tag | ciphertext`, under one AES-256-GCM key. */\nexport function seal(key: Buffer, plaintext: Buffer, aad: Buffer): Buffer {\n\tconst iv = randomBytes(IV_BYTES);\n\tconst cipher = createCipheriv('aes-256-gcm', key, iv);\n\tcipher.setAAD(aad);\n\tconst body = Buffer.concat([cipher.update(plaintext), cipher.final()]);\n\treturn Buffer.concat([iv, cipher.getAuthTag(), body]);\n}\n\n/** The inverse of {@link seal}; `undefined` when it does not authenticate. */\nexport function open(\n\tkey: Buffer,\n\tsealed: Buffer,\n\taad: Buffer,\n): Buffer | undefined {\n\tif (sealed.length < IV_BYTES + TAG_BYTES) return undefined;\n\n\tconst decipher = createDecipheriv(\n\t\t'aes-256-gcm',\n\t\tkey,\n\t\tsealed.subarray(0, IV_BYTES),\n\t);\n\tdecipher.setAAD(aad);\n\tdecipher.setAuthTag(sealed.subarray(IV_BYTES, IV_BYTES + TAG_BYTES));\n\n\ttry {\n\t\treturn Buffer.concat([\n\t\t\tdecipher.update(sealed.subarray(IV_BYTES + TAG_BYTES)),\n\t\t\tdecipher.final(),\n\t\t]);\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\n/** A local keyring: newest key first, and the index key that never rotates. */\nexport interface Keyring {\n\tkeys: { id: string; key: Buffer }[];\n\tindex: Buffer;\n}\n\n/** Parse `aes256gcm://local?keys=k2.<key>,k1.<key>&index=<key>`. */\nexport function parseKeyring(construct: string, url: string): Keyring {\n\tlet parsed: URL;\n\ttry {\n\t\tparsed = new URL(url);\n\t} catch {\n\t\tthrow new EncryptionUrlInvalid(construct, 'it is not a URL');\n\t}\n\n\tconst keys = (parsed.searchParams.get('keys') ?? '')\n\t\t.split(',')\n\t\t.filter(Boolean)\n\t\t.map((entry) => {\n\t\t\tconst [id, encoded] = entry.split('.');\n\t\t\tconst key = Buffer.from(encoded ?? '', 'base64url');\n\t\t\tif (!id || key.length !== KEY_BYTES) {\n\t\t\t\tthrow new EncryptionUrlInvalid(\n\t\t\t\t\tconstruct,\n\t\t\t\t\t`'${id ?? entry}' is not a 256-bit key`,\n\t\t\t\t);\n\t\t\t}\n\t\t\treturn { id, key };\n\t\t});\n\tif (keys.length === 0) {\n\t\tthrow new EncryptionUrlInvalid(construct, 'it holds no keys');\n\t}\n\n\tconst index = Buffer.from(\n\t\tparsed.searchParams.get('index') ?? '',\n\t\t'base64url',\n\t);\n\tif (index.length !== KEY_BYTES) {\n\t\tthrow new EncryptionUrlInvalid(construct, 'its index key is missing');\n\t}\n\n\treturn { keys, index };\n}\n\nexport function formatKeyring(keyring: Keyring): string {\n\tconst keys = keyring.keys\n\t\t.map(({ id, key }) => `${id}.${key.toString('base64url')}`)\n\t\t.join(',');\n\treturn `${LOCAL_SCHEME}//local?keys=${keys}&index=${keyring.index.toString('base64url')}`;\n}\n\n/** A keyring with one fresh key, `k1`, and a fresh index key. */\nexport function generateKeyring(): string {\n\treturn formatKeyring({\n\t\tkeys: [{ id: 'k1', key: randomBytes(KEY_BYTES) }],\n\t\tindex: randomBytes(KEY_BYTES),\n\t});\n}\n\n/**\n * The keyring with a new current key in front, and every older one kept.\n *\n * Nothing is dropped: what the old keys wrote still has to open until a sweep\n * has re-encrypted it — see {@link retireKey}.\n */\nexport function rotateKeyring(construct: string, url: string): string {\n\tconst keyring = parseKeyring(construct, url);\n\tconst next =\n\t\tMath.max(...keyring.keys.map(({ id }) => Number(id.slice(1)) || 0)) + 1;\n\n\treturn formatKeyring({\n\t\t...keyring,\n\t\tkeys: [{ id: `k${next}`, key: randomBytes(KEY_BYTES) }, ...keyring.keys],\n\t});\n}\n\n/** The keyring without `keyId`, which must not be the current key. */\nexport function retireKey(\n\tconstruct: string,\n\turl: string,\n\tkeyId: string,\n): string {\n\tconst keyring = parseKeyring(construct, url);\n\tif (keyring.keys[0]?.id === keyId) {\n\t\tthrow new CurrentKeyCannotRetire(construct, keyId);\n\t}\n\tif (!keyring.keys.some(({ id }) => id === keyId)) {\n\t\tthrow new EncryptionKeyRetired(construct, keyId);\n\t}\n\n\treturn formatKeyring({\n\t\t...keyring,\n\t\tkeys: keyring.keys.filter(({ id }) => id !== keyId),\n\t});\n}\n\n/** Says, once per key per process, that a value is still under an old key. */\nexport type StaleKeyReporter = (keyId: string, current: string) => void;\n\n/** The `aes256gcm://` cipher: a keyring held in process. */\nexport function keyringCipher(\n\tconstruct: string,\n\turl: string,\n\tonStaleKey: StaleKeyReporter = () => {},\n): Cipher {\n\tconst keyring = parseKeyring(construct, url);\n\tconst current = keyring.keys[0]!;\n\tconst byId = new Map(keyring.keys.map(({ id, key }) => [id, key]));\n\tconst reported = new Set<string>();\n\n\tconst decrypt = async (ciphertext: string) => {\n\t\tconst { keyId, payload } = parseCiphertext(construct, ciphertext);\n\t\tconst key = byId.get(keyId);\n\t\tif (!key) throw new EncryptionKeyRetired(construct, keyId);\n\n\t\tconst plaintext = open(key, payload, associatedData(construct, keyId));\n\t\tif (!plaintext) throw new CiphertextNotAuthentic(construct);\n\n\t\tif (keyId !== current.id && !reported.has(keyId)) {\n\t\t\treported.add(keyId);\n\t\t\tonStaleKey(keyId, current.id);\n\t\t}\n\n\t\treturn plaintext.toString('utf8');\n\t};\n\n\tconst encrypt = async (plaintext: string) =>\n\t\tformatCiphertext(\n\t\t\tcurrent.id,\n\t\t\tseal(\n\t\t\t\tcurrent.key,\n\t\t\t\tBuffer.from(plaintext, 'utf8'),\n\t\t\t\tassociatedData(construct, current.id),\n\t\t\t),\n\t\t);\n\n\treturn {\n\t\tencrypt,\n\t\tdecrypt,\n\t\tindex: async (value) =>\n\t\t\tcreateHmac('sha256', keyring.index)\n\t\t\t\t.update(value, 'utf8')\n\t\t\t\t.digest('base64url'),\n\t\treencrypt: async (ciphertext) => {\n\t\t\tconst { keyId } = parseCiphertext(construct, ciphertext);\n\t\t\tif (keyId === current.id) return ciphertext;\n\t\t\treturn encrypt(await decrypt(ciphertext));\n\t\t},\n\t};\n}\n\n/**\n * A cipher for a URL scheme other than `aes256gcm:` — `kms:` on AWS.\n *\n * Registered by the entry point a build generates, and only for a target\n * whose keys live there, so a process that never holds such a key never\n * imports the SDK it would need.\n */\nexport interface CipherDriver {\n\t/** The scheme it opens, with its colon: `'kms:'`. */\n\treadonly scheme: string;\n\topen(construct: string, url: string): Cipher;\n}\n"],"mappings":"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"}
@@ -0,0 +1,106 @@
1
+ //#region src/encryption/cipher.d.ts
2
+ /**
3
+ * The cipher behind an `Encryption` construct, and the format it writes.
4
+ *
5
+ * Every ciphertext names the key that wrote it — `gkm1.<keyId>.<payload>` — so
6
+ * `decrypt` never has to guess, a rotated key still opens what it wrote, and
7
+ * retiring a key is a decision made against data rather than a hope. The
8
+ * format version leads, so the day it has to change, both can be read.
9
+ *
10
+ * Two backends, picked by the scheme of the construct's URL, the way a queue's
11
+ * connection string picks its broker:
12
+ *
13
+ * - `aes256gcm://` — a keyring this process holds: AES-256-GCM with the newest
14
+ * key, HMAC-SHA256 for the blind index. Local, test and server stages.
15
+ * - `kms://` — envelope encryption under an AWS KMS key, and a KMS HMAC key for
16
+ * the index. The key material never leaves KMS. See `./kms`.
17
+ */
18
+ /** What a handler is given: `services.pii.encrypt(…)` and the rest. */
19
+ interface Cipher {
20
+ /** Plaintext in, a self-describing ciphertext string out. */
21
+ encrypt(plaintext: string): Promise<string>;
22
+ /** Opens any ciphertext this construct wrote, under whichever key wrote it. */
23
+ decrypt(ciphertext: string): Promise<string>;
24
+ /**
25
+ * A blind index: the same value always gives the same string, so an
26
+ * encrypted column can still be looked up by an indexed column beside it.
27
+ *
28
+ * Deterministic on purpose, which is also why its key never rotates — a
29
+ * rotated index key would make every stored index miss.
30
+ */
31
+ index(value: string): Promise<string>;
32
+ /**
33
+ * The same plaintext under the current key — what a sweep runs over a
34
+ * column before an old key is retired. Unchanged when it is already current.
35
+ */
36
+ reencrypt(ciphertext: string): Promise<string>;
37
+ }
38
+ declare class EncryptionUrlInvalid extends Error {
39
+ readonly construct: string;
40
+ readonly reason: string;
41
+ constructor(construct: string, reason: string);
42
+ }
43
+ declare class UnknownEncryptionScheme extends Error {
44
+ readonly construct: string;
45
+ readonly scheme: string;
46
+ constructor(construct: string, scheme: string);
47
+ }
48
+ declare class CiphertextMalformed extends Error {
49
+ readonly construct: string;
50
+ constructor(construct: string);
51
+ }
52
+ declare class EncryptionKeyRetired extends Error {
53
+ readonly construct: string;
54
+ readonly keyId: string;
55
+ constructor(construct: string, keyId: string);
56
+ }
57
+ declare class CiphertextNotAuthentic extends Error {
58
+ readonly construct: string;
59
+ constructor(construct: string);
60
+ }
61
+ declare class CurrentKeyCannotRetire extends Error {
62
+ readonly construct: string;
63
+ readonly keyId: string;
64
+ constructor(construct: string, keyId: string);
65
+ }
66
+ /** A local keyring: newest key first, and the index key that never rotates. */
67
+ interface Keyring {
68
+ keys: {
69
+ id: string;
70
+ key: Buffer;
71
+ }[];
72
+ index: Buffer;
73
+ }
74
+ /** Parse `aes256gcm://local?keys=k2.<key>,k1.<key>&index=<key>`. */
75
+ declare function parseKeyring(construct: string, url: string): Keyring;
76
+ declare function formatKeyring(keyring: Keyring): string;
77
+ /** A keyring with one fresh key, `k1`, and a fresh index key. */
78
+ declare function generateKeyring(): string;
79
+ /**
80
+ * The keyring with a new current key in front, and every older one kept.
81
+ *
82
+ * Nothing is dropped: what the old keys wrote still has to open until a sweep
83
+ * has re-encrypted it — see {@link retireKey}.
84
+ */
85
+ declare function rotateKeyring(construct: string, url: string): string;
86
+ /** The keyring without `keyId`, which must not be the current key. */
87
+ declare function retireKey(construct: string, url: string, keyId: string): string;
88
+ /** Says, once per key per process, that a value is still under an old key. */
89
+ type StaleKeyReporter = (keyId: string, current: string) => void;
90
+ /** The `aes256gcm://` cipher: a keyring held in process. */
91
+ declare function keyringCipher(construct: string, url: string, onStaleKey?: StaleKeyReporter): Cipher;
92
+ /**
93
+ * A cipher for a URL scheme other than `aes256gcm:` — `kms:` on AWS.
94
+ *
95
+ * Registered by the entry point a build generates, and only for a target
96
+ * whose keys live there, so a process that never holds such a key never
97
+ * imports the SDK it would need.
98
+ */
99
+ interface CipherDriver {
100
+ /** The scheme it opens, with its colon: `'kms:'`. */
101
+ readonly scheme: string;
102
+ open(construct: string, url: string): Cipher;
103
+ }
104
+ //#endregion
105
+ export { CurrentKeyCannotRetire as a, Keyring as c, generateKeyring as d, keyringCipher as f, rotateKeyring as h, CiphertextNotAuthentic as i, UnknownEncryptionScheme as l, retireKey as m, CipherDriver as n, EncryptionKeyRetired as o, parseKeyring as p, CiphertextMalformed as r, EncryptionUrlInvalid as s, Cipher as t, formatKeyring as u };
106
+ //# sourceMappingURL=cipher-DSDxPRIp.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cipher-DSDxPRIp.d.cts","names":[],"sources":["../src/encryption/cipher.ts"],"mappings":";;;;;;;;;;;;;;;;;;UAyBiB;;EAEhB,QAAQ,oBAAoB;;EAE5B,QAAQ,qBAAqB;;;;;;;;EAQ7B,MAAM,gBAAgB;;;;;EAKtB,UAAU,qBAAqB;;cAanB,6BAA6B;WAE/B;WACA;EAFV,YACU,mBACA;;cASE,gCAAgC;WAElC;WACA;EAFV,YACU,mBACA;;cASE,4BAA4B;WACnB;EAArB,YAAqB;;cAQT,6BAA6B;WAE/B;WACA;EAFV,YACU,mBACA;;cASE,+BAA+B;WACtB;EAArB,YAAqB;;cAQT,+BAA+B;WAEjC;WACA;EAFV,YACU,mBACA;;;UAuEM;EAChB;IAAQ;IAAY,KAAK;;EACzB,OAAO;;;iBAIQ,aAAa,mBAAmB,cAAc;iBAqC9C,cAAc,SAAS;;iBAQvB;;;;;;;iBAaA,cAAc,mBAAmB;;iBAYjC,UACf,mBACA,aACA;;KAiBW,oBAAoB,eAAe;;iBAG/B,cACf,mBACA,aACA,aAAY,mBACV;;;;;;;;UAsDc;;WAEP;EACT,KAAK,mBAAmB,cAAc"}
@@ -0,0 +1,106 @@
1
+ //#region src/encryption/cipher.d.ts
2
+ /**
3
+ * The cipher behind an `Encryption` construct, and the format it writes.
4
+ *
5
+ * Every ciphertext names the key that wrote it — `gkm1.<keyId>.<payload>` — so
6
+ * `decrypt` never has to guess, a rotated key still opens what it wrote, and
7
+ * retiring a key is a decision made against data rather than a hope. The
8
+ * format version leads, so the day it has to change, both can be read.
9
+ *
10
+ * Two backends, picked by the scheme of the construct's URL, the way a queue's
11
+ * connection string picks its broker:
12
+ *
13
+ * - `aes256gcm://` — a keyring this process holds: AES-256-GCM with the newest
14
+ * key, HMAC-SHA256 for the blind index. Local, test and server stages.
15
+ * - `kms://` — envelope encryption under an AWS KMS key, and a KMS HMAC key for
16
+ * the index. The key material never leaves KMS. See `./kms`.
17
+ */
18
+ /** What a handler is given: `services.pii.encrypt(…)` and the rest. */
19
+ interface Cipher {
20
+ /** Plaintext in, a self-describing ciphertext string out. */
21
+ encrypt(plaintext: string): Promise<string>;
22
+ /** Opens any ciphertext this construct wrote, under whichever key wrote it. */
23
+ decrypt(ciphertext: string): Promise<string>;
24
+ /**
25
+ * A blind index: the same value always gives the same string, so an
26
+ * encrypted column can still be looked up by an indexed column beside it.
27
+ *
28
+ * Deterministic on purpose, which is also why its key never rotates — a
29
+ * rotated index key would make every stored index miss.
30
+ */
31
+ index(value: string): Promise<string>;
32
+ /**
33
+ * The same plaintext under the current key — what a sweep runs over a
34
+ * column before an old key is retired. Unchanged when it is already current.
35
+ */
36
+ reencrypt(ciphertext: string): Promise<string>;
37
+ }
38
+ declare class EncryptionUrlInvalid extends Error {
39
+ readonly construct: string;
40
+ readonly reason: string;
41
+ constructor(construct: string, reason: string);
42
+ }
43
+ declare class UnknownEncryptionScheme extends Error {
44
+ readonly construct: string;
45
+ readonly scheme: string;
46
+ constructor(construct: string, scheme: string);
47
+ }
48
+ declare class CiphertextMalformed extends Error {
49
+ readonly construct: string;
50
+ constructor(construct: string);
51
+ }
52
+ declare class EncryptionKeyRetired extends Error {
53
+ readonly construct: string;
54
+ readonly keyId: string;
55
+ constructor(construct: string, keyId: string);
56
+ }
57
+ declare class CiphertextNotAuthentic extends Error {
58
+ readonly construct: string;
59
+ constructor(construct: string);
60
+ }
61
+ declare class CurrentKeyCannotRetire extends Error {
62
+ readonly construct: string;
63
+ readonly keyId: string;
64
+ constructor(construct: string, keyId: string);
65
+ }
66
+ /** A local keyring: newest key first, and the index key that never rotates. */
67
+ interface Keyring {
68
+ keys: {
69
+ id: string;
70
+ key: Buffer;
71
+ }[];
72
+ index: Buffer;
73
+ }
74
+ /** Parse `aes256gcm://local?keys=k2.<key>,k1.<key>&index=<key>`. */
75
+ declare function parseKeyring(construct: string, url: string): Keyring;
76
+ declare function formatKeyring(keyring: Keyring): string;
77
+ /** A keyring with one fresh key, `k1`, and a fresh index key. */
78
+ declare function generateKeyring(): string;
79
+ /**
80
+ * The keyring with a new current key in front, and every older one kept.
81
+ *
82
+ * Nothing is dropped: what the old keys wrote still has to open until a sweep
83
+ * has re-encrypted it — see {@link retireKey}.
84
+ */
85
+ declare function rotateKeyring(construct: string, url: string): string;
86
+ /** The keyring without `keyId`, which must not be the current key. */
87
+ declare function retireKey(construct: string, url: string, keyId: string): string;
88
+ /** Says, once per key per process, that a value is still under an old key. */
89
+ type StaleKeyReporter = (keyId: string, current: string) => void;
90
+ /** The `aes256gcm://` cipher: a keyring held in process. */
91
+ declare function keyringCipher(construct: string, url: string, onStaleKey?: StaleKeyReporter): Cipher;
92
+ /**
93
+ * A cipher for a URL scheme other than `aes256gcm:` — `kms:` on AWS.
94
+ *
95
+ * Registered by the entry point a build generates, and only for a target
96
+ * whose keys live there, so a process that never holds such a key never
97
+ * imports the SDK it would need.
98
+ */
99
+ interface CipherDriver {
100
+ /** The scheme it opens, with its colon: `'kms:'`. */
101
+ readonly scheme: string;
102
+ open(construct: string, url: string): Cipher;
103
+ }
104
+ //#endregion
105
+ export { CurrentKeyCannotRetire as a, Keyring as c, generateKeyring as d, keyringCipher as f, rotateKeyring as h, CiphertextNotAuthentic as i, UnknownEncryptionScheme as l, retireKey as m, CipherDriver as n, EncryptionKeyRetired as o, parseKeyring as p, CiphertextMalformed as r, EncryptionUrlInvalid as s, Cipher as t, formatKeyring as u };
106
+ //# sourceMappingURL=cipher-DSDxPRIp.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cipher-DSDxPRIp.d.mts","names":[],"sources":["../src/encryption/cipher.ts"],"mappings":";;;;;;;;;;;;;;;;;;UAyBiB;;EAEhB,QAAQ,oBAAoB;;EAE5B,QAAQ,qBAAqB;;;;;;;;EAQ7B,MAAM,gBAAgB;;;;;EAKtB,UAAU,qBAAqB;;cAanB,6BAA6B;WAE/B;WACA;EAFV,YACU,mBACA;;cASE,gCAAgC;WAElC;WACA;EAFV,YACU,mBACA;;cASE,4BAA4B;WACnB;EAArB,YAAqB;;cAQT,6BAA6B;WAE/B;WACA;EAFV,YACU,mBACA;;cASE,+BAA+B;WACtB;EAArB,YAAqB;;cAQT,+BAA+B;WAEjC;WACA;EAFV,YACU,mBACA;;;UAuEM;EAChB;IAAQ;IAAY,KAAK;;EACzB,OAAO;;;iBAIQ,aAAa,mBAAmB,cAAc;iBAqC9C,cAAc,SAAS;;iBAQvB;;;;;;;iBAaA,cAAc,mBAAmB;;iBAYjC,UACf,mBACA,aACA;;KAiBW,oBAAoB,eAAe;;iBAG/B,cACf,mBACA,aACA,aAAY,mBACV;;;;;;;;UAsDc;;WAEP;EACT,KAAK,mBAAmB,cAAc"}
@@ -1,4 +1,6 @@
1
- const require_cipher = require("./cipher-CVxfwLjk.cjs");
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_cipher = require("../cipher-CVxfwLjk.cjs");
3
+ let _geekmidas_manifest = require("@geekmidas/manifest");
2
4
  let _aws_sdk_client_kms = require("@aws-sdk/client-kms");
3
5
  //#region src/encryption/kms.ts
4
6
  /**
@@ -17,8 +19,10 @@ let _aws_sdk_client_kms = require("@aws-sdk/client-kms");
17
19
  * The blind index is a KMS HMAC key's `GenerateMac`: deterministic, and the
18
20
  * key is as unextractable as the encryption key.
19
21
  *
20
- * In its own module, loaded only when a URL says `kms://`, so a project that
21
- * never deploys to AWS never needs `@aws-sdk/client-kms` installed.
22
+ * In its own entry, `@geekmidas/constructs/encryption/kms`, which only an
23
+ * AWS build's generated entry imports — to register {@link kmsCipherDriver}
24
+ * — so a project that never deploys to AWS never needs
25
+ * `@aws-sdk/client-kms` installed, and its bundle never mentions it.
22
26
  */
23
27
  /** The ciphertext's key field: the version lives inside KMS's own blob. */
24
28
  const KMS_KEY_ID = "kms";
@@ -79,7 +83,13 @@ function kmsCipher(construct, url) {
79
83
  }
80
84
  };
81
85
  }
86
+ /** The `kms:` cipher, as an AWS build's entry registers it. */
87
+ const kmsCipherDriver = {
88
+ scheme: _geekmidas_manifest.KMS_SCHEME,
89
+ open: kmsCipher
90
+ };
82
91
  //#endregion
83
92
  exports.kmsCipher = kmsCipher;
93
+ exports.kmsCipherDriver = kmsCipherDriver;
84
94
 
85
- //# sourceMappingURL=kms-B-Hm3d5r.cjs.map
95
+ //# sourceMappingURL=kms.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"kms.cjs","names":["EncryptionUrlInvalid","KMSClient","associatedData","GenerateDataKeyCommand","seal","formatCiphertext","parseCiphertext","CiphertextNotAuthentic","DecryptCommand","open","GenerateMacCommand","KMS_SCHEME"],"sources":["../../src/encryption/kms.ts"],"sourcesContent":["/**\n * The `kms://` cipher: envelope encryption under an AWS KMS key.\n *\n * Each `encrypt` asks KMS for a fresh data key, seals the plaintext with it\n * locally, and stores the data key *as KMS encrypted it* beside the data. The\n * plaintext data key is used once and dropped, and the master key never leaves\n * KMS — every decrypt is a KMS call IAM can refuse and CloudTrail records.\n *\n * Rotation is KMS's: it keeps every backing version of the key, and the\n * encrypted data key names the one that wrapped it, so nothing here tracks a\n * version and `reencrypt` has nothing to move. The ciphertext's key field is\n * `kms` for that reason.\n *\n * The blind index is a KMS HMAC key's `GenerateMac`: deterministic, and the\n * key is as unextractable as the encryption key.\n *\n * In its own entry, `@geekmidas/constructs/encryption/kms`, which only an\n * AWS build's generated entry imports — to register {@link kmsCipherDriver}\n * — so a project that never deploys to AWS never needs\n * `@aws-sdk/client-kms` installed, and its bundle never mentions it.\n */\n\nimport {\n\tDecryptCommand,\n\tGenerateDataKeyCommand,\n\tGenerateMacCommand,\n\tKMSClient,\n} from '@aws-sdk/client-kms';\nimport { KMS_SCHEME } from '@geekmidas/manifest';\nimport {\n\tassociatedData,\n\ttype Cipher,\n\ttype CipherDriver,\n\tCiphertextNotAuthentic,\n\tEncryptionUrlInvalid,\n\tformatCiphertext,\n\topen,\n\tparseCiphertext,\n\tseal,\n} from './cipher';\n\n/** The ciphertext's key field: the version lives inside KMS's own blob. */\nconst KMS_KEY_ID = 'kms';\n\nexport function kmsCipher(construct: string, url: string): Cipher {\n\tconst parsed = new URL(url);\n\tconst region = parsed.hostname;\n\tconst keyId = parsed.searchParams.get('key');\n\tconst indexKeyId = parsed.searchParams.get('index');\n\tif (!region || !keyId || !indexKeyId) {\n\t\tthrow new EncryptionUrlInvalid(\n\t\t\tconstruct,\n\t\t\t'a kms:// URL names a region, a key and an index key',\n\t\t);\n\t}\n\n\tconst endpoint = parsed.searchParams.get('endpoint');\n\t// Credentials are not part of the address: the default chain — a Lambda's\n\t// role deployed, the environment against an emulator.\n\tconst kms = new KMSClient({ region, ...(endpoint ? { endpoint } : {}) });\n\t// What KMS checks on every decrypt, and what CloudTrail shows each call was\n\t// for — a data key minted for one construct will not open for another.\n\tconst context = { construct };\n\tconst aad = associatedData(construct, KMS_KEY_ID);\n\n\treturn {\n\t\tasync encrypt(plaintext) {\n\t\t\tconst dataKey = await kms.send(\n\t\t\t\tnew GenerateDataKeyCommand({\n\t\t\t\t\tKeyId: keyId,\n\t\t\t\t\tKeySpec: 'AES_256',\n\t\t\t\t\tEncryptionContext: context,\n\t\t\t\t}),\n\t\t\t);\n\t\t\tconst wrapped = Buffer.from(dataKey.CiphertextBlob!);\n\t\t\tconst sealed = seal(\n\t\t\t\tBuffer.from(dataKey.Plaintext!),\n\t\t\t\tBuffer.from(plaintext, 'utf8'),\n\t\t\t\taad,\n\t\t\t);\n\n\t\t\tconst length = Buffer.alloc(2);\n\t\t\tlength.writeUInt16BE(wrapped.length);\n\t\t\treturn formatCiphertext(\n\t\t\t\tKMS_KEY_ID,\n\t\t\t\tBuffer.concat([length, wrapped, sealed]),\n\t\t\t);\n\t\t},\n\n\t\tasync decrypt(ciphertext) {\n\t\t\tconst { payload } = parseCiphertext(construct, ciphertext);\n\t\t\tconst length = payload.length >= 2 ? payload.readUInt16BE(0) : 0;\n\t\t\tconst wrapped = payload.subarray(2, 2 + length);\n\t\t\tif (!length || wrapped.length !== length) {\n\t\t\t\tthrow new CiphertextNotAuthentic(construct);\n\t\t\t}\n\n\t\t\tconst dataKey = await kms.send(\n\t\t\t\tnew DecryptCommand({\n\t\t\t\t\tCiphertextBlob: wrapped,\n\t\t\t\t\tEncryptionContext: context,\n\t\t\t\t}),\n\t\t\t);\n\t\t\tconst plaintext = open(\n\t\t\t\tBuffer.from(dataKey.Plaintext!),\n\t\t\t\tpayload.subarray(2 + length),\n\t\t\t\taad,\n\t\t\t);\n\t\t\tif (!plaintext) throw new CiphertextNotAuthentic(construct);\n\n\t\t\treturn plaintext.toString('utf8');\n\t\t},\n\n\t\tasync index(value) {\n\t\t\tconst { Mac } = await kms.send(\n\t\t\t\tnew GenerateMacCommand({\n\t\t\t\t\tKeyId: indexKeyId,\n\t\t\t\t\tMacAlgorithm: 'HMAC_SHA_256',\n\t\t\t\t\tMessage: Buffer.from(value, 'utf8'),\n\t\t\t\t}),\n\t\t\t);\n\t\t\treturn Buffer.from(Mac!).toString('base64url');\n\t\t},\n\n\t\t// KMS keeps every version it rotated through, so a value is never under\n\t\t// a key that could be retired from under it.\n\t\tasync reencrypt(ciphertext) {\n\t\t\tparseCiphertext(construct, ciphertext);\n\t\t\treturn ciphertext;\n\t\t},\n\t};\n}\n\n/** The `kms:` cipher, as an AWS build's entry registers it. */\nexport const kmsCipherDriver: CipherDriver = {\n\tscheme: KMS_SCHEME,\n\topen: kmsCipher,\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;AA0CA,MAAM,aAAa;AAEnB,SAAgB,UAAU,WAAmB,KAAqB;CACjE,MAAM,SAAS,IAAI,IAAI,GAAG;CAC1B,MAAM,SAAS,OAAO;CACtB,MAAM,QAAQ,OAAO,aAAa,IAAI,KAAK;CAC3C,MAAM,aAAa,OAAO,aAAa,IAAI,OAAO;CAClD,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,YACzB,MAAM,IAAIA,eAAAA,qBACT,WACA,qDACD;CAGD,MAAM,WAAW,OAAO,aAAa,IAAI,UAAU;CAGnD,MAAM,MAAM,IAAIC,oBAAAA,UAAU;EAAE;EAAQ,GAAI,WAAW,EAAE,SAAS,IAAI,CAAC;CAAG,CAAC;CAGvE,MAAM,UAAU,EAAE,UAAU;CAC5B,MAAM,MAAMC,eAAAA,eAAe,WAAW,UAAU;CAEhD,OAAO;EACN,MAAM,QAAQ,WAAW;GACxB,MAAM,UAAU,MAAM,IAAI,KACzB,IAAIC,oBAAAA,uBAAuB;IAC1B,OAAO;IACP,SAAS;IACT,mBAAmB;GACpB,CAAC,CACF;GACA,MAAM,UAAU,OAAO,KAAK,QAAQ,cAAe;GACnD,MAAM,SAASC,eAAAA,KACd,OAAO,KAAK,QAAQ,SAAU,GAC9B,OAAO,KAAK,WAAW,MAAM,GAC7B,GACD;GAEA,MAAM,SAAS,OAAO,MAAM,CAAC;GAC7B,OAAO,cAAc,QAAQ,MAAM;GACnC,OAAOC,eAAAA,iBACN,YACA,OAAO,OAAO;IAAC;IAAQ;IAAS;GAAM,CAAC,CACxC;EACD;EAEA,MAAM,QAAQ,YAAY;GACzB,MAAM,EAAE,YAAYC,eAAAA,gBAAgB,WAAW,UAAU;GACzD,MAAM,SAAS,QAAQ,UAAU,IAAI,QAAQ,aAAa,CAAC,IAAI;GAC/D,MAAM,UAAU,QAAQ,SAAS,GAAG,IAAI,MAAM;GAC9C,IAAI,CAAC,UAAU,QAAQ,WAAW,QACjC,MAAM,IAAIC,eAAAA,uBAAuB,SAAS;GAG3C,MAAM,UAAU,MAAM,IAAI,KACzB,IAAIC,oBAAAA,eAAe;IAClB,gBAAgB;IAChB,mBAAmB;GACpB,CAAC,CACF;GACA,MAAM,YAAYC,eAAAA,KACjB,OAAO,KAAK,QAAQ,SAAU,GAC9B,QAAQ,SAAS,IAAI,MAAM,GAC3B,GACD;GACA,IAAI,CAAC,WAAW,MAAM,IAAIF,eAAAA,uBAAuB,SAAS;GAE1D,OAAO,UAAU,SAAS,MAAM;EACjC;EAEA,MAAM,MAAM,OAAO;GAClB,MAAM,EAAE,QAAQ,MAAM,IAAI,KACzB,IAAIG,oBAAAA,mBAAmB;IACtB,OAAO;IACP,cAAc;IACd,SAAS,OAAO,KAAK,OAAO,MAAM;GACnC,CAAC,CACF;GACA,OAAO,OAAO,KAAK,GAAI,CAAC,CAAC,SAAS,WAAW;EAC9C;EAIA,MAAM,UAAU,YAAY;GAC3B,eAAA,gBAAgB,WAAW,UAAU;GACrC,OAAO;EACR;CACD;AACD;;AAGA,MAAa,kBAAgC;CAC5C,QAAQC,oBAAAA;CACR,MAAM;AACP"}
@@ -0,0 +1,7 @@
1
+ import { n as CipherDriver, t as Cipher } from "../cipher-DSDxPRIp.cjs";
2
+ //#region src/encryption/kms.d.ts
3
+ export declare function kmsCipher(construct: string, url: string): Cipher;
4
+ /** The `kms:` cipher, as an AWS build's entry registers it. */
5
+ export declare const kmsCipherDriver: CipherDriver;
6
+ //#endregion
7
+ //# sourceMappingURL=kms.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"kms.d.cts","names":[],"sources":["../../src/encryption/kms.ts"],"mappings":";;wBA4CgB,UAAU,mBAAmB,cAAc;;qBA0F9C,iBAAiB"}
@@ -0,0 +1,7 @@
1
+ import { n as CipherDriver, t as Cipher } from "../cipher-DSDxPRIp.mjs";
2
+ //#region src/encryption/kms.d.ts
3
+ export declare function kmsCipher(construct: string, url: string): Cipher;
4
+ /** The `kms:` cipher, as an AWS build's entry registers it. */
5
+ export declare const kmsCipherDriver: CipherDriver;
6
+ //#endregion
7
+ //# sourceMappingURL=kms.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"kms.d.mts","names":[],"sources":["../../src/encryption/kms.ts"],"mappings":";;wBA4CgB,UAAU,mBAAmB,cAAc;;qBA0F9C,iBAAiB"}
@@ -1,4 +1,5 @@
1
- import { a as EncryptionUrlInvalid, c as associatedData, l as formatCiphertext, m as parseCiphertext, n as CiphertextNotAuthentic, p as open, v as seal } from "./cipher-C1lEFX63.mjs";
1
+ import { a as EncryptionUrlInvalid, c as associatedData, l as formatCiphertext, m as parseCiphertext, n as CiphertextNotAuthentic, p as open, v as seal } from "../cipher-C1lEFX63.mjs";
2
+ import { KMS_SCHEME } from "@geekmidas/manifest";
2
3
  import { DecryptCommand, GenerateDataKeyCommand, GenerateMacCommand, KMSClient } from "@aws-sdk/client-kms";
3
4
  //#region src/encryption/kms.ts
4
5
  /**
@@ -17,8 +18,10 @@ import { DecryptCommand, GenerateDataKeyCommand, GenerateMacCommand, KMSClient }
17
18
  * The blind index is a KMS HMAC key's `GenerateMac`: deterministic, and the
18
19
  * key is as unextractable as the encryption key.
19
20
  *
20
- * In its own module, loaded only when a URL says `kms://`, so a project that
21
- * never deploys to AWS never needs `@aws-sdk/client-kms` installed.
21
+ * In its own entry, `@geekmidas/constructs/encryption/kms`, which only an
22
+ * AWS build's generated entry imports — to register {@link kmsCipherDriver}
23
+ * — so a project that never deploys to AWS never needs
24
+ * `@aws-sdk/client-kms` installed, and its bundle never mentions it.
22
25
  */
23
26
  /** The ciphertext's key field: the version lives inside KMS's own blob. */
24
27
  const KMS_KEY_ID = "kms";
@@ -79,7 +82,12 @@ function kmsCipher(construct, url) {
79
82
  }
80
83
  };
81
84
  }
85
+ /** The `kms:` cipher, as an AWS build's entry registers it. */
86
+ const kmsCipherDriver = {
87
+ scheme: KMS_SCHEME,
88
+ open: kmsCipher
89
+ };
82
90
  //#endregion
83
- export { kmsCipher };
91
+ export { kmsCipher, kmsCipherDriver };
84
92
 
85
- //# sourceMappingURL=kms-BdUlMJEZ.mjs.map
93
+ //# sourceMappingURL=kms.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"kms.mjs","names":[],"sources":["../../src/encryption/kms.ts"],"sourcesContent":["/**\n * The `kms://` cipher: envelope encryption under an AWS KMS key.\n *\n * Each `encrypt` asks KMS for a fresh data key, seals the plaintext with it\n * locally, and stores the data key *as KMS encrypted it* beside the data. The\n * plaintext data key is used once and dropped, and the master key never leaves\n * KMS — every decrypt is a KMS call IAM can refuse and CloudTrail records.\n *\n * Rotation is KMS's: it keeps every backing version of the key, and the\n * encrypted data key names the one that wrapped it, so nothing here tracks a\n * version and `reencrypt` has nothing to move. The ciphertext's key field is\n * `kms` for that reason.\n *\n * The blind index is a KMS HMAC key's `GenerateMac`: deterministic, and the\n * key is as unextractable as the encryption key.\n *\n * In its own entry, `@geekmidas/constructs/encryption/kms`, which only an\n * AWS build's generated entry imports — to register {@link kmsCipherDriver}\n * — so a project that never deploys to AWS never needs\n * `@aws-sdk/client-kms` installed, and its bundle never mentions it.\n */\n\nimport {\n\tDecryptCommand,\n\tGenerateDataKeyCommand,\n\tGenerateMacCommand,\n\tKMSClient,\n} from '@aws-sdk/client-kms';\nimport { KMS_SCHEME } from '@geekmidas/manifest';\nimport {\n\tassociatedData,\n\ttype Cipher,\n\ttype CipherDriver,\n\tCiphertextNotAuthentic,\n\tEncryptionUrlInvalid,\n\tformatCiphertext,\n\topen,\n\tparseCiphertext,\n\tseal,\n} from './cipher';\n\n/** The ciphertext's key field: the version lives inside KMS's own blob. */\nconst KMS_KEY_ID = 'kms';\n\nexport function kmsCipher(construct: string, url: string): Cipher {\n\tconst parsed = new URL(url);\n\tconst region = parsed.hostname;\n\tconst keyId = parsed.searchParams.get('key');\n\tconst indexKeyId = parsed.searchParams.get('index');\n\tif (!region || !keyId || !indexKeyId) {\n\t\tthrow new EncryptionUrlInvalid(\n\t\t\tconstruct,\n\t\t\t'a kms:// URL names a region, a key and an index key',\n\t\t);\n\t}\n\n\tconst endpoint = parsed.searchParams.get('endpoint');\n\t// Credentials are not part of the address: the default chain — a Lambda's\n\t// role deployed, the environment against an emulator.\n\tconst kms = new KMSClient({ region, ...(endpoint ? { endpoint } : {}) });\n\t// What KMS checks on every decrypt, and what CloudTrail shows each call was\n\t// for — a data key minted for one construct will not open for another.\n\tconst context = { construct };\n\tconst aad = associatedData(construct, KMS_KEY_ID);\n\n\treturn {\n\t\tasync encrypt(plaintext) {\n\t\t\tconst dataKey = await kms.send(\n\t\t\t\tnew GenerateDataKeyCommand({\n\t\t\t\t\tKeyId: keyId,\n\t\t\t\t\tKeySpec: 'AES_256',\n\t\t\t\t\tEncryptionContext: context,\n\t\t\t\t}),\n\t\t\t);\n\t\t\tconst wrapped = Buffer.from(dataKey.CiphertextBlob!);\n\t\t\tconst sealed = seal(\n\t\t\t\tBuffer.from(dataKey.Plaintext!),\n\t\t\t\tBuffer.from(plaintext, 'utf8'),\n\t\t\t\taad,\n\t\t\t);\n\n\t\t\tconst length = Buffer.alloc(2);\n\t\t\tlength.writeUInt16BE(wrapped.length);\n\t\t\treturn formatCiphertext(\n\t\t\t\tKMS_KEY_ID,\n\t\t\t\tBuffer.concat([length, wrapped, sealed]),\n\t\t\t);\n\t\t},\n\n\t\tasync decrypt(ciphertext) {\n\t\t\tconst { payload } = parseCiphertext(construct, ciphertext);\n\t\t\tconst length = payload.length >= 2 ? payload.readUInt16BE(0) : 0;\n\t\t\tconst wrapped = payload.subarray(2, 2 + length);\n\t\t\tif (!length || wrapped.length !== length) {\n\t\t\t\tthrow new CiphertextNotAuthentic(construct);\n\t\t\t}\n\n\t\t\tconst dataKey = await kms.send(\n\t\t\t\tnew DecryptCommand({\n\t\t\t\t\tCiphertextBlob: wrapped,\n\t\t\t\t\tEncryptionContext: context,\n\t\t\t\t}),\n\t\t\t);\n\t\t\tconst plaintext = open(\n\t\t\t\tBuffer.from(dataKey.Plaintext!),\n\t\t\t\tpayload.subarray(2 + length),\n\t\t\t\taad,\n\t\t\t);\n\t\t\tif (!plaintext) throw new CiphertextNotAuthentic(construct);\n\n\t\t\treturn plaintext.toString('utf8');\n\t\t},\n\n\t\tasync index(value) {\n\t\t\tconst { Mac } = await kms.send(\n\t\t\t\tnew GenerateMacCommand({\n\t\t\t\t\tKeyId: indexKeyId,\n\t\t\t\t\tMacAlgorithm: 'HMAC_SHA_256',\n\t\t\t\t\tMessage: Buffer.from(value, 'utf8'),\n\t\t\t\t}),\n\t\t\t);\n\t\t\treturn Buffer.from(Mac!).toString('base64url');\n\t\t},\n\n\t\t// KMS keeps every version it rotated through, so a value is never under\n\t\t// a key that could be retired from under it.\n\t\tasync reencrypt(ciphertext) {\n\t\t\tparseCiphertext(construct, ciphertext);\n\t\t\treturn ciphertext;\n\t\t},\n\t};\n}\n\n/** The `kms:` cipher, as an AWS build's entry registers it. */\nexport const kmsCipherDriver: CipherDriver = {\n\tscheme: KMS_SCHEME,\n\topen: kmsCipher,\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AA0CA,MAAM,aAAa;AAEnB,SAAgB,UAAU,WAAmB,KAAqB;CACjE,MAAM,SAAS,IAAI,IAAI,GAAG;CAC1B,MAAM,SAAS,OAAO;CACtB,MAAM,QAAQ,OAAO,aAAa,IAAI,KAAK;CAC3C,MAAM,aAAa,OAAO,aAAa,IAAI,OAAO;CAClD,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,YACzB,MAAM,IAAI,qBACT,WACA,qDACD;CAGD,MAAM,WAAW,OAAO,aAAa,IAAI,UAAU;CAGnD,MAAM,MAAM,IAAI,UAAU;EAAE;EAAQ,GAAI,WAAW,EAAE,SAAS,IAAI,CAAC;CAAG,CAAC;CAGvE,MAAM,UAAU,EAAE,UAAU;CAC5B,MAAM,MAAM,eAAe,WAAW,UAAU;CAEhD,OAAO;EACN,MAAM,QAAQ,WAAW;GACxB,MAAM,UAAU,MAAM,IAAI,KACzB,IAAI,uBAAuB;IAC1B,OAAO;IACP,SAAS;IACT,mBAAmB;GACpB,CAAC,CACF;GACA,MAAM,UAAU,OAAO,KAAK,QAAQ,cAAe;GACnD,MAAM,SAAS,KACd,OAAO,KAAK,QAAQ,SAAU,GAC9B,OAAO,KAAK,WAAW,MAAM,GAC7B,GACD;GAEA,MAAM,SAAS,OAAO,MAAM,CAAC;GAC7B,OAAO,cAAc,QAAQ,MAAM;GACnC,OAAO,iBACN,YACA,OAAO,OAAO;IAAC;IAAQ;IAAS;GAAM,CAAC,CACxC;EACD;EAEA,MAAM,QAAQ,YAAY;GACzB,MAAM,EAAE,YAAY,gBAAgB,WAAW,UAAU;GACzD,MAAM,SAAS,QAAQ,UAAU,IAAI,QAAQ,aAAa,CAAC,IAAI;GAC/D,MAAM,UAAU,QAAQ,SAAS,GAAG,IAAI,MAAM;GAC9C,IAAI,CAAC,UAAU,QAAQ,WAAW,QACjC,MAAM,IAAI,uBAAuB,SAAS;GAG3C,MAAM,UAAU,MAAM,IAAI,KACzB,IAAI,eAAe;IAClB,gBAAgB;IAChB,mBAAmB;GACpB,CAAC,CACF;GACA,MAAM,YAAY,KACjB,OAAO,KAAK,QAAQ,SAAU,GAC9B,QAAQ,SAAS,IAAI,MAAM,GAC3B,GACD;GACA,IAAI,CAAC,WAAW,MAAM,IAAI,uBAAuB,SAAS;GAE1D,OAAO,UAAU,SAAS,MAAM;EACjC;EAEA,MAAM,MAAM,OAAO;GAClB,MAAM,EAAE,QAAQ,MAAM,IAAI,KACzB,IAAI,mBAAmB;IACtB,OAAO;IACP,cAAc;IACd,SAAS,OAAO,KAAK,OAAO,MAAM;GACnC,CAAC,CACF;GACA,OAAO,OAAO,KAAK,GAAI,CAAC,CAAC,SAAS,WAAW;EAC9C;EAIA,MAAM,UAAU,YAAY;GAC3B,gBAAgB,WAAW,UAAU;GACrC,OAAO;EACR;CACD;AACD;;AAGA,MAAa,kBAAgC;CAC5C,QAAQ;CACR,MAAM;AACP"}
@@ -34,6 +34,25 @@ let _geekmidas_manifest = require("@geekmidas/manifest");
34
34
  * `gkm encryption:retire` — and a value still under an old key says so when it
35
35
  * is decrypted, once per key per process.
36
36
  */
37
+ const drivers = /* @__PURE__ */ new Map();
38
+ /** Make a cipher available for its scheme. Idempotent. */
39
+ function registerCipherDriver(driver) {
40
+ drivers.set(driver.scheme, driver);
41
+ }
42
+ /**
43
+ * A key whose URL names a cipher this process registered no driver for —
44
+ * `kms:` in an entry built for a target whose keys are local.
45
+ */
46
+ var UnregisteredEncryptionScheme = class extends Error {
47
+ construct;
48
+ scheme;
49
+ constructor(construct, scheme) {
50
+ super(`${construct}'s key is a '${scheme}' URL, and this build registered no cipher for it. ` + (scheme === _geekmidas_manifest.KMS_SCHEME ? "A KMS key is an AWS target's: build for it (gkm build --provider aws), which registers the KMS cipher and needs @aws-sdk/client-kms installed." : "Register its driver with registerCipherDriver before the first request."));
51
+ this.construct = construct;
52
+ this.scheme = scheme;
53
+ this.name = "UnregisteredEncryptionScheme";
54
+ }
55
+ };
37
56
  var Encryption = class {
38
57
  id;
39
58
  service;
@@ -71,10 +90,9 @@ var Encryption = class {
71
90
  const { url } = options.envParser.create((get) => ({ url: get(this.key).string() })).parse();
72
91
  const scheme = url.slice(0, url.indexOf(":") + 1);
73
92
  if (scheme === "aes256gcm:") return require_cipher.keyringCipher(this.id, url, (stale, current) => console.warn(`${this.id} decrypted a value still under '${stale}' (current: '${current}'). reencrypt it, and retire '${stale}' once nothing is left under it.`));
74
- if (scheme === _geekmidas_manifest.KMS_SCHEME) {
75
- const { kmsCipher } = await Promise.resolve().then(() => require("./kms-B-Hm3d5r.cjs"));
76
- return kmsCipher(this.id, url);
77
- }
93
+ const driver = drivers.get(scheme);
94
+ if (driver) return driver.open(this.id, url);
95
+ if (scheme === _geekmidas_manifest.KMS_SCHEME) throw new UnregisteredEncryptionScheme(this.id, scheme);
78
96
  throw new require_cipher.UnknownEncryptionScheme(this.id, scheme);
79
97
  }
80
98
  };
@@ -86,10 +104,12 @@ exports.Encryption = Encryption;
86
104
  exports.EncryptionKeyRetired = require_cipher.EncryptionKeyRetired;
87
105
  exports.EncryptionUrlInvalid = require_cipher.EncryptionUrlInvalid;
88
106
  exports.UnknownEncryptionScheme = require_cipher.UnknownEncryptionScheme;
107
+ exports.UnregisteredEncryptionScheme = UnregisteredEncryptionScheme;
89
108
  exports.formatKeyring = require_cipher.formatKeyring;
90
109
  exports.generateKeyring = require_cipher.generateKeyring;
91
110
  exports.keyringCipher = require_cipher.keyringCipher;
92
111
  exports.parseKeyring = require_cipher.parseKeyring;
112
+ exports.registerCipherDriver = registerCipherDriver;
93
113
  exports.retireKey = require_cipher.retireKey;
94
114
  exports.rotateKeyring = require_cipher.rotateKeyring;
95
115
 
@@ -1 +1 @@
1
- {"version":3,"file":"encryption.cjs","names":["canonicalId","provideKey","serviceKey","keyringCipher","KMS_SCHEME","UnknownEncryptionScheme"],"sources":["../src/encryption.ts"],"sourcesContent":["/**\n * `Encryption` — a key that encrypts what the application stores.\n *\n * ```ts\n * export const pii = new Encryption('Pii');\n *\n * // an endpoint that `.dependsOn([pii])`\n * const email = await services.pii.encrypt(body.email);\n * const emailIndex = await services.pii.index(body.email);\n * await db.insertInto('users').values({ email, emailIndex }).execute();\n *\n * // and to find them again, without decrypting a column\n * db.selectFrom('users').where('emailIndex', '=', await services.pii.index(q));\n * ```\n *\n * The app names no cipher and holds no key. The construct provides one URL,\n * `<ID>_URL`, and its scheme picks the backend the way a queue's connection\n * string picks its broker:\n *\n * - **AWS** — `kms://`: envelope encryption under a KMS key that rotates\n * yearly, and a KMS HMAC key for the index. Only a function that depends on\n * the construct is granted either key.\n * - **Server, local, tests** — `aes256gcm://`: a keyring the process holds.\n * Derived locally, so `gkm dev` and `gkm test` need nothing set; generated\n * into a server stage's secrets on its first deploy, and rotated there with\n * `gkm encryption:rotate`.\n *\n * Every ciphertext names the key that wrote it, so a rotated key still opens\n * what it wrote. Retiring one is explicit — `reencrypt` the column, then\n * `gkm encryption:retire` — and a value still under an old key says so when it\n * is decrypted, once per key per process.\n */\n\nimport {\n\ttype ConstructName,\n\tcanonicalId,\n\ttype Declaration,\n\tKMS_SCHEME,\n\tprovideKey,\n\tserviceKey,\n} from '@geekmidas/manifest';\nimport type { Service, ServiceRegisterOptions } from '@geekmidas/services';\nimport type { Construct } from './construct-interface';\nimport {\n\ttype Cipher,\n\tkeyringCipher,\n\tLOCAL_SCHEME,\n\tUnknownEncryptionScheme,\n} from './encryption/cipher';\n\nexport type { Cipher } from './encryption/cipher';\nexport {\n\tCiphertextMalformed,\n\tCiphertextNotAuthentic,\n\tCurrentKeyCannotRetire,\n\tEncryptionKeyRetired,\n\tEncryptionUrlInvalid,\n\tformatKeyring,\n\tgenerateKeyring,\n\ttype Keyring,\n\tkeyringCipher,\n\tparseKeyring,\n\tretireKey,\n\trotateKeyring,\n\tUnknownEncryptionScheme,\n} from './encryption/cipher';\n\nexport class Encryption<TName extends string = string>\n\timplements Construct<TName, Cipher>\n{\n\treadonly id: TName;\n\treadonly service: Service<Uncapitalize<TName>, Cipher>;\n\n\t/**\n\t * Declared once and read by both `declare()` and `connect()`, so the key the\n\t * target publishes and the key the client reads cannot drift.\n\t */\n\tprivate readonly key: string;\n\n\t/**\n\t * One cipher per process, held as the promise: a keyring is parsed once and\n\t * a KMS client is built once, however many handlers register it at once.\n\t */\n\tprivate cipher?: Promise<Cipher>;\n\n\tconstructor(id: ConstructName<TName>) {\n\t\tconst canonical = canonicalId(id as string);\n\n\t\tthis.id = canonical as TName;\n\t\tthis.key = provideKey(canonical, 'url');\n\n\t\t// A field, not a getter: consumers cache services by object identity.\n\t\tthis.service = {\n\t\t\tserviceName: serviceKey(canonical) as Uncapitalize<TName>,\n\t\t\tregister: (options) => this.connect(options),\n\t\t};\n\t}\n\n\tdeclare(): Declaration[] {\n\t\treturn [{ kind: 'encryption', id: this.id, provides: [this.key] }];\n\t}\n\n\tprivate connect(options: ServiceRegisterOptions): Promise<Cipher> {\n\t\tthis.cipher ??= this.open(options);\n\t\treturn this.cipher;\n\t}\n\n\tprivate async open(options: ServiceRegisterOptions): Promise<Cipher> {\n\t\tconst { url } = options.envParser\n\t\t\t.create((get) => ({ url: get(this.key).string() }))\n\t\t\t.parse();\n\t\tconst scheme = url.slice(0, url.indexOf(':') + 1);\n\n\t\tif (scheme === LOCAL_SCHEME) {\n\t\t\treturn keyringCipher(this.id, url, (stale, current) =>\n\t\t\t\tconsole.warn(\n\t\t\t\t\t`${this.id} decrypted a value still under '${stale}' (current: '${current}'). reencrypt it, and retire '${stale}' once nothing is left under it.`,\n\t\t\t\t),\n\t\t\t);\n\t\t}\n\n\t\tif (scheme === KMS_SCHEME) {\n\t\t\t// Loaded only here, so a project that never deploys to AWS never\n\t\t\t// needs `@aws-sdk/client-kms` installed.\n\t\t\tconst { kmsCipher } = await import('./encryption/kms');\n\t\t\treturn kmsCipher(this.id, url);\n\t\t}\n\n\t\tthrow new UnknownEncryptionScheme(this.id, scheme);\n\t}\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmEA,IAAa,aAAb,MAEA;CACC;CACA;;;;;CAMA;;;;;CAMA;CAEA,YAAY,IAA0B;EACrC,MAAM,aAAA,GAAYA,oBAAAA,YAAAA,CAAY,EAAY;EAE1C,KAAK,KAAK;EACV,KAAK,OAAA,GAAMC,oBAAAA,WAAAA,CAAW,WAAW,KAAK;EAGtC,KAAK,UAAU;GACd,cAAA,GAAaC,oBAAAA,WAAAA,CAAW,SAAS;GACjC,WAAW,YAAY,KAAK,QAAQ,OAAO;EAC5C;CACD;CAEA,UAAyB;EACxB,OAAO,CAAC;GAAE,MAAM;GAAc,IAAI,KAAK;GAAI,UAAU,CAAC,KAAK,GAAG;EAAE,CAAC;CAClE;CAEA,QAAgB,SAAkD;EACjE,KAAK,WAAW,KAAK,KAAK,OAAO;EACjC,OAAO,KAAK;CACb;CAEA,MAAc,KAAK,SAAkD;EACpE,MAAM,EAAE,QAAQ,QAAQ,UACtB,QAAQ,SAAS,EAAE,KAAK,IAAI,KAAK,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAClD,MAAM;EACR,MAAM,SAAS,IAAI,MAAM,GAAG,IAAI,QAAQ,GAAG,IAAI,CAAC;EAEhD,IAAI,WAAA,cACH,OAAOC,eAAAA,cAAc,KAAK,IAAI,MAAM,OAAO,YAC1C,QAAQ,KACP,GAAG,KAAK,GAAG,kCAAkC,MAAM,eAAe,QAAQ,gCAAgC,MAAM,iCACjH,CACD;EAGD,IAAI,WAAWC,oBAAAA,YAAY;GAG1B,MAAM,EAAE,cAAc,MAAA,QAAA,QAAA,CAAA,CAAA,WAAA,QAAM,oBAAA,CAAA;GAC5B,OAAO,UAAU,KAAK,IAAI,GAAG;EAC9B;EAEA,MAAM,IAAIC,eAAAA,wBAAwB,KAAK,IAAI,MAAM;CAClD;AACD"}
1
+ {"version":3,"file":"encryption.cjs","names":["KMS_SCHEME","canonicalId","provideKey","serviceKey","keyringCipher","UnknownEncryptionScheme"],"sources":["../src/encryption.ts"],"sourcesContent":["/**\n * `Encryption` — a key that encrypts what the application stores.\n *\n * ```ts\n * export const pii = new Encryption('Pii');\n *\n * // an endpoint that `.dependsOn([pii])`\n * const email = await services.pii.encrypt(body.email);\n * const emailIndex = await services.pii.index(body.email);\n * await db.insertInto('users').values({ email, emailIndex }).execute();\n *\n * // and to find them again, without decrypting a column\n * db.selectFrom('users').where('emailIndex', '=', await services.pii.index(q));\n * ```\n *\n * The app names no cipher and holds no key. The construct provides one URL,\n * `<ID>_URL`, and its scheme picks the backend the way a queue's connection\n * string picks its broker:\n *\n * - **AWS** — `kms://`: envelope encryption under a KMS key that rotates\n * yearly, and a KMS HMAC key for the index. Only a function that depends on\n * the construct is granted either key.\n * - **Server, local, tests** — `aes256gcm://`: a keyring the process holds.\n * Derived locally, so `gkm dev` and `gkm test` need nothing set; generated\n * into a server stage's secrets on its first deploy, and rotated there with\n * `gkm encryption:rotate`.\n *\n * Every ciphertext names the key that wrote it, so a rotated key still opens\n * what it wrote. Retiring one is explicit — `reencrypt` the column, then\n * `gkm encryption:retire` — and a value still under an old key says so when it\n * is decrypted, once per key per process.\n */\n\nimport {\n\ttype ConstructName,\n\tcanonicalId,\n\ttype Declaration,\n\tKMS_SCHEME,\n\tprovideKey,\n\tserviceKey,\n} from '@geekmidas/manifest';\nimport type { Service, ServiceRegisterOptions } from '@geekmidas/services';\nimport type { Construct } from './construct-interface';\nimport {\n\ttype Cipher,\n\ttype CipherDriver,\n\tkeyringCipher,\n\tLOCAL_SCHEME,\n\tUnknownEncryptionScheme,\n} from './encryption/cipher';\n\nexport type { Cipher, CipherDriver } from './encryption/cipher';\nexport {\n\tCiphertextMalformed,\n\tCiphertextNotAuthentic,\n\tCurrentKeyCannotRetire,\n\tEncryptionKeyRetired,\n\tEncryptionUrlInvalid,\n\tformatKeyring,\n\tgenerateKeyring,\n\ttype Keyring,\n\tkeyringCipher,\n\tparseKeyring,\n\tretireKey,\n\trotateKeyring,\n\tUnknownEncryptionScheme,\n} from './encryption/cipher';\n\nconst drivers = new Map<string, CipherDriver>();\n\n/** Make a cipher available for its scheme. Idempotent. */\nexport function registerCipherDriver(driver: CipherDriver): void {\n\tdrivers.set(driver.scheme, driver);\n}\n\n/**\n * A key whose URL names a cipher this process registered no driver for —\n * `kms:` in an entry built for a target whose keys are local.\n */\nexport class UnregisteredEncryptionScheme extends Error {\n\tconstructor(\n\t\treadonly construct: string,\n\t\treadonly scheme: string,\n\t) {\n\t\tsuper(\n\t\t\t`${construct}'s key is a '${scheme}' URL, and this build registered no cipher for it. ` +\n\t\t\t\t(scheme === KMS_SCHEME\n\t\t\t\t\t? \"A KMS key is an AWS target's: build for it (gkm build --provider aws), which registers the KMS cipher and needs @aws-sdk/client-kms installed.\"\n\t\t\t\t\t: 'Register its driver with registerCipherDriver before the first request.'),\n\t\t);\n\t\tthis.name = 'UnregisteredEncryptionScheme';\n\t}\n}\n\nexport class Encryption<TName extends string = string>\n\timplements Construct<TName, Cipher>\n{\n\treadonly id: TName;\n\treadonly service: Service<Uncapitalize<TName>, Cipher>;\n\n\t/**\n\t * Declared once and read by both `declare()` and `connect()`, so the key the\n\t * target publishes and the key the client reads cannot drift.\n\t */\n\tprivate readonly key: string;\n\n\t/**\n\t * One cipher per process, held as the promise: a keyring is parsed once and\n\t * a KMS client is built once, however many handlers register it at once.\n\t */\n\tprivate cipher?: Promise<Cipher>;\n\n\tconstructor(id: ConstructName<TName>) {\n\t\tconst canonical = canonicalId(id as string);\n\n\t\tthis.id = canonical as TName;\n\t\tthis.key = provideKey(canonical, 'url');\n\n\t\t// A field, not a getter: consumers cache services by object identity.\n\t\tthis.service = {\n\t\t\tserviceName: serviceKey(canonical) as Uncapitalize<TName>,\n\t\t\tregister: (options) => this.connect(options),\n\t\t};\n\t}\n\n\tdeclare(): Declaration[] {\n\t\treturn [{ kind: 'encryption', id: this.id, provides: [this.key] }];\n\t}\n\n\tprivate connect(options: ServiceRegisterOptions): Promise<Cipher> {\n\t\tthis.cipher ??= this.open(options);\n\t\treturn this.cipher;\n\t}\n\n\tprivate async open(options: ServiceRegisterOptions): Promise<Cipher> {\n\t\tconst { url } = options.envParser\n\t\t\t.create((get) => ({ url: get(this.key).string() }))\n\t\t\t.parse();\n\t\tconst scheme = url.slice(0, url.indexOf(':') + 1);\n\n\t\tif (scheme === LOCAL_SCHEME) {\n\t\t\treturn keyringCipher(this.id, url, (stale, current) =>\n\t\t\t\tconsole.warn(\n\t\t\t\t\t`${this.id} decrypted a value still under '${stale}' (current: '${current}'). reencrypt it, and retire '${stale}' once nothing is left under it.`,\n\t\t\t\t),\n\t\t\t);\n\t\t}\n\n\t\t// Registered by the entry a build generates, only for a target whose\n\t\t// keys live there — so no other process imports that cipher's SDK.\n\t\tconst driver = drivers.get(scheme);\n\t\tif (driver) return driver.open(this.id, url);\n\t\tif (scheme === KMS_SCHEME) {\n\t\t\tthrow new UnregisteredEncryptionScheme(this.id, scheme);\n\t\t}\n\n\t\tthrow new UnknownEncryptionScheme(this.id, scheme);\n\t}\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoEA,MAAM,0BAAU,IAAI,IAA0B;;AAG9C,SAAgB,qBAAqB,QAA4B;CAChE,QAAQ,IAAI,OAAO,QAAQ,MAAM;AAClC;;;;;AAMA,IAAa,+BAAb,cAAkD,MAAM;CAE7C;CACA;CAFV,YACC,WACA,QACC;EACD,MACC,GAAG,UAAU,eAAe,OAAO,wDACjC,WAAWA,oBAAAA,aACT,mJACA,0EACL;EARS,KAAA,YAAA;EACA,KAAA,SAAA;EAQT,KAAK,OAAO;CACb;AACD;AAEA,IAAa,aAAb,MAEA;CACC;CACA;;;;;CAMA;;;;;CAMA;CAEA,YAAY,IAA0B;EACrC,MAAM,aAAA,GAAYC,oBAAAA,YAAAA,CAAY,EAAY;EAE1C,KAAK,KAAK;EACV,KAAK,OAAA,GAAMC,oBAAAA,WAAAA,CAAW,WAAW,KAAK;EAGtC,KAAK,UAAU;GACd,cAAA,GAAaC,oBAAAA,WAAAA,CAAW,SAAS;GACjC,WAAW,YAAY,KAAK,QAAQ,OAAO;EAC5C;CACD;CAEA,UAAyB;EACxB,OAAO,CAAC;GAAE,MAAM;GAAc,IAAI,KAAK;GAAI,UAAU,CAAC,KAAK,GAAG;EAAE,CAAC;CAClE;CAEA,QAAgB,SAAkD;EACjE,KAAK,WAAW,KAAK,KAAK,OAAO;EACjC,OAAO,KAAK;CACb;CAEA,MAAc,KAAK,SAAkD;EACpE,MAAM,EAAE,QAAQ,QAAQ,UACtB,QAAQ,SAAS,EAAE,KAAK,IAAI,KAAK,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAClD,MAAM;EACR,MAAM,SAAS,IAAI,MAAM,GAAG,IAAI,QAAQ,GAAG,IAAI,CAAC;EAEhD,IAAI,WAAA,cACH,OAAOC,eAAAA,cAAc,KAAK,IAAI,MAAM,OAAO,YAC1C,QAAQ,KACP,GAAG,KAAK,GAAG,kCAAkC,MAAM,eAAe,QAAQ,gCAAgC,MAAM,iCACjH,CACD;EAKD,MAAM,SAAS,QAAQ,IAAI,MAAM;EACjC,IAAI,QAAQ,OAAO,OAAO,KAAK,KAAK,IAAI,GAAG;EAC3C,IAAI,WAAWJ,oBAAAA,YACd,MAAM,IAAI,6BAA6B,KAAK,IAAI,MAAM;EAGvD,MAAM,IAAIK,eAAAA,wBAAwB,KAAK,IAAI,MAAM;CAClD;AACD"}
@@ -1,99 +1,19 @@
1
1
  import { n as Construct } from "./construct-interface-B1AfqRl4.cjs";
2
+ import { a as CurrentKeyCannotRetire, c as Keyring, d as generateKeyring, f as keyringCipher, h as rotateKeyring, i as CiphertextNotAuthentic, l as UnknownEncryptionScheme, m as retireKey, n as CipherDriver, o as EncryptionKeyRetired, p as parseKeyring, r as CiphertextMalformed, s as EncryptionUrlInvalid, t as Cipher, u as formatKeyring } from "./cipher-DSDxPRIp.cjs";
2
3
  import { Service } from "@geekmidas/services";
3
4
  import { ConstructName, Declaration } from "@geekmidas/manifest";
4
- //#region src/encryption/cipher.d.ts
5
+ //#region src/encryption.d.ts
6
+ /** Make a cipher available for its scheme. Idempotent. */
7
+ export declare function registerCipherDriver(driver: CipherDriver): void;
5
8
  /**
6
- * The cipher behind an `Encryption` construct, and the format it writes.
7
- *
8
- * Every ciphertext names the key that wrote it — `gkm1.<keyId>.<payload>` — so
9
- * `decrypt` never has to guess, a rotated key still opens what it wrote, and
10
- * retiring a key is a decision made against data rather than a hope. The
11
- * format version leads, so the day it has to change, both can be read.
12
- *
13
- * Two backends, picked by the scheme of the construct's URL, the way a queue's
14
- * connection string picks its broker:
15
- *
16
- * - `aes256gcm://` — a keyring this process holds: AES-256-GCM with the newest
17
- * key, HMAC-SHA256 for the blind index. Local, test and server stages.
18
- * - `kms://` — envelope encryption under an AWS KMS key, and a KMS HMAC key for
19
- * the index. The key material never leaves KMS. See `./kms`.
9
+ * A key whose URL names a cipher this process registered no driver for —
10
+ * `kms:` in an entry built for a target whose keys are local.
20
11
  */
21
- /** What a handler is given: `services.pii.encrypt(…)` and the rest. */
22
- interface Cipher {
23
- /** Plaintext in, a self-describing ciphertext string out. */
24
- encrypt(plaintext: string): Promise<string>;
25
- /** Opens any ciphertext this construct wrote, under whichever key wrote it. */
26
- decrypt(ciphertext: string): Promise<string>;
27
- /**
28
- * A blind index: the same value always gives the same string, so an
29
- * encrypted column can still be looked up by an indexed column beside it.
30
- *
31
- * Deterministic on purpose, which is also why its key never rotates — a
32
- * rotated index key would make every stored index miss.
33
- */
34
- index(value: string): Promise<string>;
35
- /**
36
- * The same plaintext under the current key — what a sweep runs over a
37
- * column before an old key is retired. Unchanged when it is already current.
38
- */
39
- reencrypt(ciphertext: string): Promise<string>;
40
- }
41
- export declare class EncryptionUrlInvalid extends Error {
42
- readonly construct: string;
43
- readonly reason: string;
44
- constructor(construct: string, reason: string);
45
- }
46
- export declare class UnknownEncryptionScheme extends Error {
12
+ export declare class UnregisteredEncryptionScheme extends Error {
47
13
  readonly construct: string;
48
14
  readonly scheme: string;
49
15
  constructor(construct: string, scheme: string);
50
16
  }
51
- export declare class CiphertextMalformed extends Error {
52
- readonly construct: string;
53
- constructor(construct: string);
54
- }
55
- export declare class EncryptionKeyRetired extends Error {
56
- readonly construct: string;
57
- readonly keyId: string;
58
- constructor(construct: string, keyId: string);
59
- }
60
- export declare class CiphertextNotAuthentic extends Error {
61
- readonly construct: string;
62
- constructor(construct: string);
63
- }
64
- export declare class CurrentKeyCannotRetire extends Error {
65
- readonly construct: string;
66
- readonly keyId: string;
67
- constructor(construct: string, keyId: string);
68
- }
69
- /** A local keyring: newest key first, and the index key that never rotates. */
70
- interface Keyring {
71
- keys: {
72
- id: string;
73
- key: Buffer;
74
- }[];
75
- index: Buffer;
76
- }
77
- /** Parse `aes256gcm://local?keys=k2.<key>,k1.<key>&index=<key>`. */
78
- export declare function parseKeyring(construct: string, url: string): Keyring;
79
- export declare function formatKeyring(keyring: Keyring): string;
80
- /** A keyring with one fresh key, `k1`, and a fresh index key. */
81
- export declare function generateKeyring(): string;
82
- /**
83
- * The keyring with a new current key in front, and every older one kept.
84
- *
85
- * Nothing is dropped: what the old keys wrote still has to open until a sweep
86
- * has re-encrypted it — see {@link retireKey}.
87
- */
88
- export declare function rotateKeyring(construct: string, url: string): string;
89
- /** The keyring without `keyId`, which must not be the current key. */
90
- export declare function retireKey(construct: string, url: string, keyId: string): string;
91
- /** Says, once per key per process, that a value is still under an old key. */
92
- type StaleKeyReporter = (keyId: string, current: string) => void;
93
- /** The `aes256gcm://` cipher: a keyring held in process. */
94
- export declare function keyringCipher(construct: string, url: string, onStaleKey?: StaleKeyReporter): Cipher;
95
- //#endregion
96
- //#region src/encryption.d.ts
97
17
  export declare class Encryption<TName extends string = string> implements Construct<TName, Cipher> {
98
18
  readonly id: TName;
99
19
  readonly service: Service<Uncapitalize<TName>, Cipher>;
@@ -113,5 +33,5 @@ export declare class Encryption<TName extends string = string> implements Constr
113
33
  private open;
114
34
  }
115
35
  //#endregion
116
- export type { Cipher, Keyring };
36
+ export { type Cipher, type CipherDriver, CiphertextMalformed, CiphertextNotAuthentic, CurrentKeyCannotRetire, EncryptionKeyRetired, EncryptionUrlInvalid, type Keyring, UnknownEncryptionScheme, formatKeyring, generateKeyring, keyringCipher, parseKeyring, retireKey, rotateKeyring };
117
37
  //# sourceMappingURL=encryption.d.cts.map
@@ -1 +1 @@
1
- {"version":3,"file":"encryption.d.cts","names":[],"sources":["../src/encryption/cipher.ts","../src/encryption.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;UAyBiB;;EAEhB,QAAQ,oBAAoB;;EAE5B,QAAQ,qBAAqB;;;;;;;;EAQ7B,MAAM,gBAAgB;;;;;EAKtB,UAAU,qBAAqB;;qBAanB,6BAA6B;WAE/B;WACA;EAFV,YACU,mBACA;;qBASE,gCAAgC;WAElC;WACA;EAFV,YACU,mBACA;;qBASE,4BAA4B;WACnB;EAArB,YAAqB;;qBAQT,6BAA6B;WAE/B;WACA;EAFV,YACU,mBACA;;qBASE,+BAA+B;WACtB;EAArB,YAAqB;;qBAQT,+BAA+B;WAEjC;WACA;EAFV,YACU,mBACA;;;UAuEM;EAChB;IAAQ;IAAY,KAAK;;EACzB,OAAO;;;wBAIQ,aAAa,mBAAmB,cAAc;wBAqC9C,cAAc,SAAS;;wBAQvB;;;;;;;wBAaA,cAAc,mBAAmB;;wBAYjC,UACf,mBACA,aACA;;KAiBW,oBAAoB,eAAe;;wBAG/B,cACf,mBACA,aACA,aAAY,mBACV;;;qBC3NU,WAAW,0CACZ,UAAU,OAAO;WAEnB,IAAI;WACJ,SAAS,QAAQ,aAAa,QAAQ;;;;;mBAM9B;;;;;UAMT;EAER,YAAY,IAAI,cAAc;EAa9B,WAAW;UAIH;UAKM"}
1
+ {"version":3,"file":"encryption.d.cts","names":[],"sources":["../src/encryption.ts"],"mappings":";;;;;;wBAuEgB,qBAAqB,QAAQ;;;;;qBAQhC,qCAAqC;WAEvC;WACA;EAFV,YACU,mBACA;;qBAYE,WAAW,0CACZ,UAAU,OAAO;WAEnB,IAAI;WACJ,SAAS,QAAQ,aAAa,QAAQ;;;;;mBAM9B;;;;;UAMT;EAER,YAAY,IAAI,cAAc;EAa9B,WAAW;UAIH;UAKM"}
@@ -1,99 +1,19 @@
1
1
  import { n as Construct } from "./construct-interface-ZvYQbivO.mjs";
2
+ import { a as CurrentKeyCannotRetire, c as Keyring, d as generateKeyring, f as keyringCipher, h as rotateKeyring, i as CiphertextNotAuthentic, l as UnknownEncryptionScheme, m as retireKey, n as CipherDriver, o as EncryptionKeyRetired, p as parseKeyring, r as CiphertextMalformed, s as EncryptionUrlInvalid, t as Cipher, u as formatKeyring } from "./cipher-DSDxPRIp.mjs";
2
3
  import { ConstructName, Declaration } from "@geekmidas/manifest";
3
4
  import { Service } from "@geekmidas/services";
4
- //#region src/encryption/cipher.d.ts
5
+ //#region src/encryption.d.ts
6
+ /** Make a cipher available for its scheme. Idempotent. */
7
+ export declare function registerCipherDriver(driver: CipherDriver): void;
5
8
  /**
6
- * The cipher behind an `Encryption` construct, and the format it writes.
7
- *
8
- * Every ciphertext names the key that wrote it — `gkm1.<keyId>.<payload>` — so
9
- * `decrypt` never has to guess, a rotated key still opens what it wrote, and
10
- * retiring a key is a decision made against data rather than a hope. The
11
- * format version leads, so the day it has to change, both can be read.
12
- *
13
- * Two backends, picked by the scheme of the construct's URL, the way a queue's
14
- * connection string picks its broker:
15
- *
16
- * - `aes256gcm://` — a keyring this process holds: AES-256-GCM with the newest
17
- * key, HMAC-SHA256 for the blind index. Local, test and server stages.
18
- * - `kms://` — envelope encryption under an AWS KMS key, and a KMS HMAC key for
19
- * the index. The key material never leaves KMS. See `./kms`.
9
+ * A key whose URL names a cipher this process registered no driver for —
10
+ * `kms:` in an entry built for a target whose keys are local.
20
11
  */
21
- /** What a handler is given: `services.pii.encrypt(…)` and the rest. */
22
- interface Cipher {
23
- /** Plaintext in, a self-describing ciphertext string out. */
24
- encrypt(plaintext: string): Promise<string>;
25
- /** Opens any ciphertext this construct wrote, under whichever key wrote it. */
26
- decrypt(ciphertext: string): Promise<string>;
27
- /**
28
- * A blind index: the same value always gives the same string, so an
29
- * encrypted column can still be looked up by an indexed column beside it.
30
- *
31
- * Deterministic on purpose, which is also why its key never rotates — a
32
- * rotated index key would make every stored index miss.
33
- */
34
- index(value: string): Promise<string>;
35
- /**
36
- * The same plaintext under the current key — what a sweep runs over a
37
- * column before an old key is retired. Unchanged when it is already current.
38
- */
39
- reencrypt(ciphertext: string): Promise<string>;
40
- }
41
- export declare class EncryptionUrlInvalid extends Error {
42
- readonly construct: string;
43
- readonly reason: string;
44
- constructor(construct: string, reason: string);
45
- }
46
- export declare class UnknownEncryptionScheme extends Error {
12
+ export declare class UnregisteredEncryptionScheme extends Error {
47
13
  readonly construct: string;
48
14
  readonly scheme: string;
49
15
  constructor(construct: string, scheme: string);
50
16
  }
51
- export declare class CiphertextMalformed extends Error {
52
- readonly construct: string;
53
- constructor(construct: string);
54
- }
55
- export declare class EncryptionKeyRetired extends Error {
56
- readonly construct: string;
57
- readonly keyId: string;
58
- constructor(construct: string, keyId: string);
59
- }
60
- export declare class CiphertextNotAuthentic extends Error {
61
- readonly construct: string;
62
- constructor(construct: string);
63
- }
64
- export declare class CurrentKeyCannotRetire extends Error {
65
- readonly construct: string;
66
- readonly keyId: string;
67
- constructor(construct: string, keyId: string);
68
- }
69
- /** A local keyring: newest key first, and the index key that never rotates. */
70
- interface Keyring {
71
- keys: {
72
- id: string;
73
- key: Buffer;
74
- }[];
75
- index: Buffer;
76
- }
77
- /** Parse `aes256gcm://local?keys=k2.<key>,k1.<key>&index=<key>`. */
78
- export declare function parseKeyring(construct: string, url: string): Keyring;
79
- export declare function formatKeyring(keyring: Keyring): string;
80
- /** A keyring with one fresh key, `k1`, and a fresh index key. */
81
- export declare function generateKeyring(): string;
82
- /**
83
- * The keyring with a new current key in front, and every older one kept.
84
- *
85
- * Nothing is dropped: what the old keys wrote still has to open until a sweep
86
- * has re-encrypted it — see {@link retireKey}.
87
- */
88
- export declare function rotateKeyring(construct: string, url: string): string;
89
- /** The keyring without `keyId`, which must not be the current key. */
90
- export declare function retireKey(construct: string, url: string, keyId: string): string;
91
- /** Says, once per key per process, that a value is still under an old key. */
92
- type StaleKeyReporter = (keyId: string, current: string) => void;
93
- /** The `aes256gcm://` cipher: a keyring held in process. */
94
- export declare function keyringCipher(construct: string, url: string, onStaleKey?: StaleKeyReporter): Cipher;
95
- //#endregion
96
- //#region src/encryption.d.ts
97
17
  export declare class Encryption<TName extends string = string> implements Construct<TName, Cipher> {
98
18
  readonly id: TName;
99
19
  readonly service: Service<Uncapitalize<TName>, Cipher>;
@@ -113,5 +33,5 @@ export declare class Encryption<TName extends string = string> implements Constr
113
33
  private open;
114
34
  }
115
35
  //#endregion
116
- export type { Cipher, Keyring };
36
+ export { type Cipher, type CipherDriver, CiphertextMalformed, CiphertextNotAuthentic, CurrentKeyCannotRetire, EncryptionKeyRetired, EncryptionUrlInvalid, type Keyring, UnknownEncryptionScheme, formatKeyring, generateKeyring, keyringCipher, parseKeyring, retireKey, rotateKeyring };
117
37
  //# sourceMappingURL=encryption.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"encryption.d.mts","names":[],"sources":["../src/encryption/cipher.ts","../src/encryption.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;UAyBiB;;EAEhB,QAAQ,oBAAoB;;EAE5B,QAAQ,qBAAqB;;;;;;;;EAQ7B,MAAM,gBAAgB;;;;;EAKtB,UAAU,qBAAqB;;qBAanB,6BAA6B;WAE/B;WACA;EAFV,YACU,mBACA;;qBASE,gCAAgC;WAElC;WACA;EAFV,YACU,mBACA;;qBASE,4BAA4B;WACnB;EAArB,YAAqB;;qBAQT,6BAA6B;WAE/B;WACA;EAFV,YACU,mBACA;;qBASE,+BAA+B;WACtB;EAArB,YAAqB;;qBAQT,+BAA+B;WAEjC;WACA;EAFV,YACU,mBACA;;;UAuEM;EAChB;IAAQ;IAAY,KAAK;;EACzB,OAAO;;;wBAIQ,aAAa,mBAAmB,cAAc;wBAqC9C,cAAc,SAAS;;wBAQvB;;;;;;;wBAaA,cAAc,mBAAmB;;wBAYjC,UACf,mBACA,aACA;;KAiBW,oBAAoB,eAAe;;wBAG/B,cACf,mBACA,aACA,aAAY,mBACV;;;qBC3NU,WAAW,0CACZ,UAAU,OAAO;WAEnB,IAAI;WACJ,SAAS,QAAQ,aAAa,QAAQ;;;;;mBAM9B;;;;;UAMT;EAER,YAAY,IAAI,cAAc;EAa9B,WAAW;UAIH;UAKM"}
1
+ {"version":3,"file":"encryption.d.mts","names":[],"sources":["../src/encryption.ts"],"mappings":";;;;;;wBAuEgB,qBAAqB,QAAQ;;;;;qBAQhC,qCAAqC;WAEvC;WACA;EAFV,YACU,mBACA;;qBAYE,WAAW,0CACZ,UAAU,OAAO;WAEnB,IAAI;WACJ,SAAS,QAAQ,aAAa,QAAQ;;;;;mBAM9B;;;;;UAMT;EAER,YAAY,IAAI,cAAc;EAa9B,WAAW;UAIH;UAKM"}
@@ -33,6 +33,25 @@ import { KMS_SCHEME, canonicalId, provideKey, serviceKey } from "@geekmidas/mani
33
33
  * `gkm encryption:retire` — and a value still under an old key says so when it
34
34
  * is decrypted, once per key per process.
35
35
  */
36
+ const drivers = /* @__PURE__ */ new Map();
37
+ /** Make a cipher available for its scheme. Idempotent. */
38
+ function registerCipherDriver(driver) {
39
+ drivers.set(driver.scheme, driver);
40
+ }
41
+ /**
42
+ * A key whose URL names a cipher this process registered no driver for —
43
+ * `kms:` in an entry built for a target whose keys are local.
44
+ */
45
+ var UnregisteredEncryptionScheme = class extends Error {
46
+ construct;
47
+ scheme;
48
+ constructor(construct, scheme) {
49
+ super(`${construct}'s key is a '${scheme}' URL, and this build registered no cipher for it. ` + (scheme === KMS_SCHEME ? "A KMS key is an AWS target's: build for it (gkm build --provider aws), which registers the KMS cipher and needs @aws-sdk/client-kms installed." : "Register its driver with registerCipherDriver before the first request."));
50
+ this.construct = construct;
51
+ this.scheme = scheme;
52
+ this.name = "UnregisteredEncryptionScheme";
53
+ }
54
+ };
36
55
  var Encryption = class {
37
56
  id;
38
57
  service;
@@ -70,14 +89,13 @@ var Encryption = class {
70
89
  const { url } = options.envParser.create((get) => ({ url: get(this.key).string() })).parse();
71
90
  const scheme = url.slice(0, url.indexOf(":") + 1);
72
91
  if (scheme === "aes256gcm:") return keyringCipher(this.id, url, (stale, current) => console.warn(`${this.id} decrypted a value still under '${stale}' (current: '${current}'). reencrypt it, and retire '${stale}' once nothing is left under it.`));
73
- if (scheme === KMS_SCHEME) {
74
- const { kmsCipher } = await import("./kms-BdUlMJEZ.mjs");
75
- return kmsCipher(this.id, url);
76
- }
92
+ const driver = drivers.get(scheme);
93
+ if (driver) return driver.open(this.id, url);
94
+ if (scheme === KMS_SCHEME) throw new UnregisteredEncryptionScheme(this.id, scheme);
77
95
  throw new UnknownEncryptionScheme(this.id, scheme);
78
96
  }
79
97
  };
80
98
  //#endregion
81
- export { CiphertextMalformed, CiphertextNotAuthentic, CurrentKeyCannotRetire, Encryption, EncryptionKeyRetired, EncryptionUrlInvalid, UnknownEncryptionScheme, formatKeyring, generateKeyring, keyringCipher, parseKeyring, retireKey, rotateKeyring };
99
+ export { CiphertextMalformed, CiphertextNotAuthentic, CurrentKeyCannotRetire, Encryption, EncryptionKeyRetired, EncryptionUrlInvalid, UnknownEncryptionScheme, UnregisteredEncryptionScheme, formatKeyring, generateKeyring, keyringCipher, parseKeyring, registerCipherDriver, retireKey, rotateKeyring };
82
100
 
83
101
  //# sourceMappingURL=encryption.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"encryption.mjs","names":[],"sources":["../src/encryption.ts"],"sourcesContent":["/**\n * `Encryption` — a key that encrypts what the application stores.\n *\n * ```ts\n * export const pii = new Encryption('Pii');\n *\n * // an endpoint that `.dependsOn([pii])`\n * const email = await services.pii.encrypt(body.email);\n * const emailIndex = await services.pii.index(body.email);\n * await db.insertInto('users').values({ email, emailIndex }).execute();\n *\n * // and to find them again, without decrypting a column\n * db.selectFrom('users').where('emailIndex', '=', await services.pii.index(q));\n * ```\n *\n * The app names no cipher and holds no key. The construct provides one URL,\n * `<ID>_URL`, and its scheme picks the backend the way a queue's connection\n * string picks its broker:\n *\n * - **AWS** — `kms://`: envelope encryption under a KMS key that rotates\n * yearly, and a KMS HMAC key for the index. Only a function that depends on\n * the construct is granted either key.\n * - **Server, local, tests** — `aes256gcm://`: a keyring the process holds.\n * Derived locally, so `gkm dev` and `gkm test` need nothing set; generated\n * into a server stage's secrets on its first deploy, and rotated there with\n * `gkm encryption:rotate`.\n *\n * Every ciphertext names the key that wrote it, so a rotated key still opens\n * what it wrote. Retiring one is explicit — `reencrypt` the column, then\n * `gkm encryption:retire` — and a value still under an old key says so when it\n * is decrypted, once per key per process.\n */\n\nimport {\n\ttype ConstructName,\n\tcanonicalId,\n\ttype Declaration,\n\tKMS_SCHEME,\n\tprovideKey,\n\tserviceKey,\n} from '@geekmidas/manifest';\nimport type { Service, ServiceRegisterOptions } from '@geekmidas/services';\nimport type { Construct } from './construct-interface';\nimport {\n\ttype Cipher,\n\tkeyringCipher,\n\tLOCAL_SCHEME,\n\tUnknownEncryptionScheme,\n} from './encryption/cipher';\n\nexport type { Cipher } from './encryption/cipher';\nexport {\n\tCiphertextMalformed,\n\tCiphertextNotAuthentic,\n\tCurrentKeyCannotRetire,\n\tEncryptionKeyRetired,\n\tEncryptionUrlInvalid,\n\tformatKeyring,\n\tgenerateKeyring,\n\ttype Keyring,\n\tkeyringCipher,\n\tparseKeyring,\n\tretireKey,\n\trotateKeyring,\n\tUnknownEncryptionScheme,\n} from './encryption/cipher';\n\nexport class Encryption<TName extends string = string>\n\timplements Construct<TName, Cipher>\n{\n\treadonly id: TName;\n\treadonly service: Service<Uncapitalize<TName>, Cipher>;\n\n\t/**\n\t * Declared once and read by both `declare()` and `connect()`, so the key the\n\t * target publishes and the key the client reads cannot drift.\n\t */\n\tprivate readonly key: string;\n\n\t/**\n\t * One cipher per process, held as the promise: a keyring is parsed once and\n\t * a KMS client is built once, however many handlers register it at once.\n\t */\n\tprivate cipher?: Promise<Cipher>;\n\n\tconstructor(id: ConstructName<TName>) {\n\t\tconst canonical = canonicalId(id as string);\n\n\t\tthis.id = canonical as TName;\n\t\tthis.key = provideKey(canonical, 'url');\n\n\t\t// A field, not a getter: consumers cache services by object identity.\n\t\tthis.service = {\n\t\t\tserviceName: serviceKey(canonical) as Uncapitalize<TName>,\n\t\t\tregister: (options) => this.connect(options),\n\t\t};\n\t}\n\n\tdeclare(): Declaration[] {\n\t\treturn [{ kind: 'encryption', id: this.id, provides: [this.key] }];\n\t}\n\n\tprivate connect(options: ServiceRegisterOptions): Promise<Cipher> {\n\t\tthis.cipher ??= this.open(options);\n\t\treturn this.cipher;\n\t}\n\n\tprivate async open(options: ServiceRegisterOptions): Promise<Cipher> {\n\t\tconst { url } = options.envParser\n\t\t\t.create((get) => ({ url: get(this.key).string() }))\n\t\t\t.parse();\n\t\tconst scheme = url.slice(0, url.indexOf(':') + 1);\n\n\t\tif (scheme === LOCAL_SCHEME) {\n\t\t\treturn keyringCipher(this.id, url, (stale, current) =>\n\t\t\t\tconsole.warn(\n\t\t\t\t\t`${this.id} decrypted a value still under '${stale}' (current: '${current}'). reencrypt it, and retire '${stale}' once nothing is left under it.`,\n\t\t\t\t),\n\t\t\t);\n\t\t}\n\n\t\tif (scheme === KMS_SCHEME) {\n\t\t\t// Loaded only here, so a project that never deploys to AWS never\n\t\t\t// needs `@aws-sdk/client-kms` installed.\n\t\t\tconst { kmsCipher } = await import('./encryption/kms');\n\t\t\treturn kmsCipher(this.id, url);\n\t\t}\n\n\t\tthrow new UnknownEncryptionScheme(this.id, scheme);\n\t}\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmEA,IAAa,aAAb,MAEA;CACC;CACA;;;;;CAMA;;;;;CAMA;CAEA,YAAY,IAA0B;EACrC,MAAM,YAAY,YAAY,EAAY;EAE1C,KAAK,KAAK;EACV,KAAK,MAAM,WAAW,WAAW,KAAK;EAGtC,KAAK,UAAU;GACd,aAAa,WAAW,SAAS;GACjC,WAAW,YAAY,KAAK,QAAQ,OAAO;EAC5C;CACD;CAEA,UAAyB;EACxB,OAAO,CAAC;GAAE,MAAM;GAAc,IAAI,KAAK;GAAI,UAAU,CAAC,KAAK,GAAG;EAAE,CAAC;CAClE;CAEA,QAAgB,SAAkD;EACjE,KAAK,WAAW,KAAK,KAAK,OAAO;EACjC,OAAO,KAAK;CACb;CAEA,MAAc,KAAK,SAAkD;EACpE,MAAM,EAAE,QAAQ,QAAQ,UACtB,QAAQ,SAAS,EAAE,KAAK,IAAI,KAAK,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAClD,MAAM;EACR,MAAM,SAAS,IAAI,MAAM,GAAG,IAAI,QAAQ,GAAG,IAAI,CAAC;EAEhD,IAAI,WAAA,cACH,OAAO,cAAc,KAAK,IAAI,MAAM,OAAO,YAC1C,QAAQ,KACP,GAAG,KAAK,GAAG,kCAAkC,MAAM,eAAe,QAAQ,gCAAgC,MAAM,iCACjH,CACD;EAGD,IAAI,WAAW,YAAY;GAG1B,MAAM,EAAE,cAAc,MAAM,OAAO;GACnC,OAAO,UAAU,KAAK,IAAI,GAAG;EAC9B;EAEA,MAAM,IAAI,wBAAwB,KAAK,IAAI,MAAM;CAClD;AACD"}
1
+ {"version":3,"file":"encryption.mjs","names":[],"sources":["../src/encryption.ts"],"sourcesContent":["/**\n * `Encryption` — a key that encrypts what the application stores.\n *\n * ```ts\n * export const pii = new Encryption('Pii');\n *\n * // an endpoint that `.dependsOn([pii])`\n * const email = await services.pii.encrypt(body.email);\n * const emailIndex = await services.pii.index(body.email);\n * await db.insertInto('users').values({ email, emailIndex }).execute();\n *\n * // and to find them again, without decrypting a column\n * db.selectFrom('users').where('emailIndex', '=', await services.pii.index(q));\n * ```\n *\n * The app names no cipher and holds no key. The construct provides one URL,\n * `<ID>_URL`, and its scheme picks the backend the way a queue's connection\n * string picks its broker:\n *\n * - **AWS** — `kms://`: envelope encryption under a KMS key that rotates\n * yearly, and a KMS HMAC key for the index. Only a function that depends on\n * the construct is granted either key.\n * - **Server, local, tests** — `aes256gcm://`: a keyring the process holds.\n * Derived locally, so `gkm dev` and `gkm test` need nothing set; generated\n * into a server stage's secrets on its first deploy, and rotated there with\n * `gkm encryption:rotate`.\n *\n * Every ciphertext names the key that wrote it, so a rotated key still opens\n * what it wrote. Retiring one is explicit — `reencrypt` the column, then\n * `gkm encryption:retire` — and a value still under an old key says so when it\n * is decrypted, once per key per process.\n */\n\nimport {\n\ttype ConstructName,\n\tcanonicalId,\n\ttype Declaration,\n\tKMS_SCHEME,\n\tprovideKey,\n\tserviceKey,\n} from '@geekmidas/manifest';\nimport type { Service, ServiceRegisterOptions } from '@geekmidas/services';\nimport type { Construct } from './construct-interface';\nimport {\n\ttype Cipher,\n\ttype CipherDriver,\n\tkeyringCipher,\n\tLOCAL_SCHEME,\n\tUnknownEncryptionScheme,\n} from './encryption/cipher';\n\nexport type { Cipher, CipherDriver } from './encryption/cipher';\nexport {\n\tCiphertextMalformed,\n\tCiphertextNotAuthentic,\n\tCurrentKeyCannotRetire,\n\tEncryptionKeyRetired,\n\tEncryptionUrlInvalid,\n\tformatKeyring,\n\tgenerateKeyring,\n\ttype Keyring,\n\tkeyringCipher,\n\tparseKeyring,\n\tretireKey,\n\trotateKeyring,\n\tUnknownEncryptionScheme,\n} from './encryption/cipher';\n\nconst drivers = new Map<string, CipherDriver>();\n\n/** Make a cipher available for its scheme. Idempotent. */\nexport function registerCipherDriver(driver: CipherDriver): void {\n\tdrivers.set(driver.scheme, driver);\n}\n\n/**\n * A key whose URL names a cipher this process registered no driver for —\n * `kms:` in an entry built for a target whose keys are local.\n */\nexport class UnregisteredEncryptionScheme extends Error {\n\tconstructor(\n\t\treadonly construct: string,\n\t\treadonly scheme: string,\n\t) {\n\t\tsuper(\n\t\t\t`${construct}'s key is a '${scheme}' URL, and this build registered no cipher for it. ` +\n\t\t\t\t(scheme === KMS_SCHEME\n\t\t\t\t\t? \"A KMS key is an AWS target's: build for it (gkm build --provider aws), which registers the KMS cipher and needs @aws-sdk/client-kms installed.\"\n\t\t\t\t\t: 'Register its driver with registerCipherDriver before the first request.'),\n\t\t);\n\t\tthis.name = 'UnregisteredEncryptionScheme';\n\t}\n}\n\nexport class Encryption<TName extends string = string>\n\timplements Construct<TName, Cipher>\n{\n\treadonly id: TName;\n\treadonly service: Service<Uncapitalize<TName>, Cipher>;\n\n\t/**\n\t * Declared once and read by both `declare()` and `connect()`, so the key the\n\t * target publishes and the key the client reads cannot drift.\n\t */\n\tprivate readonly key: string;\n\n\t/**\n\t * One cipher per process, held as the promise: a keyring is parsed once and\n\t * a KMS client is built once, however many handlers register it at once.\n\t */\n\tprivate cipher?: Promise<Cipher>;\n\n\tconstructor(id: ConstructName<TName>) {\n\t\tconst canonical = canonicalId(id as string);\n\n\t\tthis.id = canonical as TName;\n\t\tthis.key = provideKey(canonical, 'url');\n\n\t\t// A field, not a getter: consumers cache services by object identity.\n\t\tthis.service = {\n\t\t\tserviceName: serviceKey(canonical) as Uncapitalize<TName>,\n\t\t\tregister: (options) => this.connect(options),\n\t\t};\n\t}\n\n\tdeclare(): Declaration[] {\n\t\treturn [{ kind: 'encryption', id: this.id, provides: [this.key] }];\n\t}\n\n\tprivate connect(options: ServiceRegisterOptions): Promise<Cipher> {\n\t\tthis.cipher ??= this.open(options);\n\t\treturn this.cipher;\n\t}\n\n\tprivate async open(options: ServiceRegisterOptions): Promise<Cipher> {\n\t\tconst { url } = options.envParser\n\t\t\t.create((get) => ({ url: get(this.key).string() }))\n\t\t\t.parse();\n\t\tconst scheme = url.slice(0, url.indexOf(':') + 1);\n\n\t\tif (scheme === LOCAL_SCHEME) {\n\t\t\treturn keyringCipher(this.id, url, (stale, current) =>\n\t\t\t\tconsole.warn(\n\t\t\t\t\t`${this.id} decrypted a value still under '${stale}' (current: '${current}'). reencrypt it, and retire '${stale}' once nothing is left under it.`,\n\t\t\t\t),\n\t\t\t);\n\t\t}\n\n\t\t// Registered by the entry a build generates, only for a target whose\n\t\t// keys live there — so no other process imports that cipher's SDK.\n\t\tconst driver = drivers.get(scheme);\n\t\tif (driver) return driver.open(this.id, url);\n\t\tif (scheme === KMS_SCHEME) {\n\t\t\tthrow new UnregisteredEncryptionScheme(this.id, scheme);\n\t\t}\n\n\t\tthrow new UnknownEncryptionScheme(this.id, scheme);\n\t}\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoEA,MAAM,0BAAU,IAAI,IAA0B;;AAG9C,SAAgB,qBAAqB,QAA4B;CAChE,QAAQ,IAAI,OAAO,QAAQ,MAAM;AAClC;;;;;AAMA,IAAa,+BAAb,cAAkD,MAAM;CAE7C;CACA;CAFV,YACC,WACA,QACC;EACD,MACC,GAAG,UAAU,eAAe,OAAO,wDACjC,WAAW,aACT,mJACA,0EACL;EARS,KAAA,YAAA;EACA,KAAA,SAAA;EAQT,KAAK,OAAO;CACb;AACD;AAEA,IAAa,aAAb,MAEA;CACC;CACA;;;;;CAMA;;;;;CAMA;CAEA,YAAY,IAA0B;EACrC,MAAM,YAAY,YAAY,EAAY;EAE1C,KAAK,KAAK;EACV,KAAK,MAAM,WAAW,WAAW,KAAK;EAGtC,KAAK,UAAU;GACd,aAAa,WAAW,SAAS;GACjC,WAAW,YAAY,KAAK,QAAQ,OAAO;EAC5C;CACD;CAEA,UAAyB;EACxB,OAAO,CAAC;GAAE,MAAM;GAAc,IAAI,KAAK;GAAI,UAAU,CAAC,KAAK,GAAG;EAAE,CAAC;CAClE;CAEA,QAAgB,SAAkD;EACjE,KAAK,WAAW,KAAK,KAAK,OAAO;EACjC,OAAO,KAAK;CACb;CAEA,MAAc,KAAK,SAAkD;EACpE,MAAM,EAAE,QAAQ,QAAQ,UACtB,QAAQ,SAAS,EAAE,KAAK,IAAI,KAAK,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAClD,MAAM;EACR,MAAM,SAAS,IAAI,MAAM,GAAG,IAAI,QAAQ,GAAG,IAAI,CAAC;EAEhD,IAAI,WAAA,cACH,OAAO,cAAc,KAAK,IAAI,MAAM,OAAO,YAC1C,QAAQ,KACP,GAAG,KAAK,GAAG,kCAAkC,MAAM,eAAe,QAAQ,gCAAgC,MAAM,iCACjH,CACD;EAKD,MAAM,SAAS,QAAQ,IAAI,MAAM;EACjC,IAAI,QAAQ,OAAO,OAAO,KAAK,KAAK,IAAI,GAAG;EAC3C,IAAI,WAAW,YACd,MAAM,IAAI,6BAA6B,KAAK,IAAI,MAAM;EAGvD,MAAM,IAAI,wBAAwB,KAAK,IAAI,MAAM;CAClD;AACD"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geekmidas/constructs",
3
- "version": "10.0.0-alpha.115",
3
+ "version": "10.0.0-alpha.117",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "exports": {
@@ -174,6 +174,16 @@
174
174
  "default": "./dist/encryption.cjs"
175
175
  }
176
176
  },
177
+ "./encryption/kms": {
178
+ "import": {
179
+ "types": "./dist/encryption/kms.d.mts",
180
+ "default": "./dist/encryption/kms.mjs"
181
+ },
182
+ "require": {
183
+ "types": "./dist/encryption/kms.d.cts",
184
+ "default": "./dist/encryption/kms.cjs"
185
+ }
186
+ },
177
187
  "./random-password": {
178
188
  "import": {
179
189
  "types": "./dist/random-password.d.mts",
@@ -297,13 +307,13 @@
297
307
  "lodash.uniqby": "~4.7.0",
298
308
  "openapi-types": "~12.1.3",
299
309
  "qs": "~6.16.0",
300
- "@geekmidas/envkit": "^10.0.0-alpha.115",
301
- "@geekmidas/events": "^10.0.0-alpha.115",
302
- "@geekmidas/errors": "^10.0.0-alpha.115",
303
- "@geekmidas/logger": "^10.0.0-alpha.115",
304
- "@geekmidas/manifest": "^10.0.0-alpha.115",
305
- "@geekmidas/schema": "^10.0.0-alpha.115",
306
- "@geekmidas/services": "^10.0.0-alpha.115"
310
+ "@geekmidas/envkit": "^10.0.0-alpha.117",
311
+ "@geekmidas/errors": "^10.0.0-alpha.117",
312
+ "@geekmidas/events": "^10.0.0-alpha.117",
313
+ "@geekmidas/logger": "^10.0.0-alpha.117",
314
+ "@geekmidas/manifest": "^10.0.0-alpha.117",
315
+ "@geekmidas/schema": "^10.0.0-alpha.117",
316
+ "@geekmidas/services": "^10.0.0-alpha.117"
307
317
  },
308
318
  "devDependencies": {
309
319
  "@aws-sdk/client-kms": "~3.1136.0",
@@ -321,20 +331,20 @@
321
331
  "msw": "~2.15.0",
322
332
  "pg": "~8.23.0",
323
333
  "zod": "~4.6.5",
324
- "@geekmidas/audit": "^10.0.0-alpha.115",
325
- "@geekmidas/auth": "10.0.0-alpha.115",
326
- "@geekmidas/cache": "^10.0.0-alpha.115",
327
- "@geekmidas/db": "^10.0.0-alpha.115",
328
- "@geekmidas/emailkit": "^10.0.0-alpha.115",
329
- "@geekmidas/envkit": "^10.0.0-alpha.115",
330
- "@geekmidas/errors": "^10.0.0-alpha.115",
331
- "@geekmidas/events": "^10.0.0-alpha.115",
332
- "@geekmidas/logger": "^10.0.0-alpha.115",
333
- "@geekmidas/rate-limit": "^10.0.0-alpha.115",
334
- "@geekmidas/schema": "^10.0.0-alpha.115",
335
- "@geekmidas/services": "^10.0.0-alpha.115",
336
- "@geekmidas/telescope": "10.0.0-alpha.115",
337
- "@geekmidas/testkit": "^10.0.0-alpha.115"
334
+ "@geekmidas/audit": "^10.0.0-alpha.117",
335
+ "@geekmidas/auth": "10.0.0-alpha.117",
336
+ "@geekmidas/cache": "^10.0.0-alpha.117",
337
+ "@geekmidas/db": "^10.0.0-alpha.117",
338
+ "@geekmidas/emailkit": "^10.0.0-alpha.117",
339
+ "@geekmidas/envkit": "^10.0.0-alpha.117",
340
+ "@geekmidas/errors": "^10.0.0-alpha.117",
341
+ "@geekmidas/events": "^10.0.0-alpha.117",
342
+ "@geekmidas/logger": "^10.0.0-alpha.117",
343
+ "@geekmidas/rate-limit": "^10.0.0-alpha.117",
344
+ "@geekmidas/schema": "^10.0.0-alpha.117",
345
+ "@geekmidas/services": "^10.0.0-alpha.117",
346
+ "@geekmidas/telescope": "10.0.0-alpha.117",
347
+ "@geekmidas/testkit": "^10.0.0-alpha.117"
338
348
  },
339
349
  "peerDependencies": {
340
350
  "@aws-sdk/client-kms": ">=3.1136.0",
@@ -345,15 +355,15 @@
345
355
  "kysely": "~0.29.6",
346
356
  "msw": ">=2.15.0",
347
357
  "pg": ">=8.23.0",
348
- "@geekmidas/audit": "^10.0.0-alpha.115",
349
- "@geekmidas/auth": "10.0.0-alpha.115",
350
- "@geekmidas/cache": "^10.0.0-alpha.115",
351
- "@geekmidas/db": "^10.0.0-alpha.115",
352
- "@geekmidas/emailkit": "^10.0.0-alpha.115",
353
- "@geekmidas/rate-limit": "^10.0.0-alpha.115",
354
- "@geekmidas/storage": "^10.0.0-alpha.115",
355
- "@geekmidas/telescope": "10.0.0-alpha.115",
356
- "@geekmidas/testkit": "^10.0.0-alpha.115"
358
+ "@geekmidas/audit": "^10.0.0-alpha.117",
359
+ "@geekmidas/auth": "10.0.0-alpha.117",
360
+ "@geekmidas/cache": "^10.0.0-alpha.117",
361
+ "@geekmidas/db": "^10.0.0-alpha.117",
362
+ "@geekmidas/emailkit": "^10.0.0-alpha.117",
363
+ "@geekmidas/rate-limit": "^10.0.0-alpha.117",
364
+ "@geekmidas/storage": "^10.0.0-alpha.117",
365
+ "@geekmidas/telescope": "10.0.0-alpha.117",
366
+ "@geekmidas/testkit": "^10.0.0-alpha.117"
357
367
  },
358
368
  "peerDependenciesMeta": {
359
369
  "@aws-sdk/client-kms": {
@@ -1 +0,0 @@
1
- {"version":3,"file":"kms-B-Hm3d5r.cjs","names":["EncryptionUrlInvalid","KMSClient","associatedData","GenerateDataKeyCommand","seal","formatCiphertext","parseCiphertext","CiphertextNotAuthentic","DecryptCommand","open","GenerateMacCommand"],"sources":["../src/encryption/kms.ts"],"sourcesContent":["/**\n * The `kms://` cipher: envelope encryption under an AWS KMS key.\n *\n * Each `encrypt` asks KMS for a fresh data key, seals the plaintext with it\n * locally, and stores the data key *as KMS encrypted it* beside the data. The\n * plaintext data key is used once and dropped, and the master key never leaves\n * KMS — every decrypt is a KMS call IAM can refuse and CloudTrail records.\n *\n * Rotation is KMS's: it keeps every backing version of the key, and the\n * encrypted data key names the one that wrapped it, so nothing here tracks a\n * version and `reencrypt` has nothing to move. The ciphertext's key field is\n * `kms` for that reason.\n *\n * The blind index is a KMS HMAC key's `GenerateMac`: deterministic, and the\n * key is as unextractable as the encryption key.\n *\n * In its own module, loaded only when a URL says `kms://`, so a project that\n * never deploys to AWS never needs `@aws-sdk/client-kms` installed.\n */\n\nimport {\n\tDecryptCommand,\n\tGenerateDataKeyCommand,\n\tGenerateMacCommand,\n\tKMSClient,\n} from '@aws-sdk/client-kms';\nimport {\n\tassociatedData,\n\ttype Cipher,\n\tCiphertextNotAuthentic,\n\tEncryptionUrlInvalid,\n\tformatCiphertext,\n\topen,\n\tparseCiphertext,\n\tseal,\n} from './cipher';\n\n/** The ciphertext's key field: the version lives inside KMS's own blob. */\nconst KMS_KEY_ID = 'kms';\n\nexport function kmsCipher(construct: string, url: string): Cipher {\n\tconst parsed = new URL(url);\n\tconst region = parsed.hostname;\n\tconst keyId = parsed.searchParams.get('key');\n\tconst indexKeyId = parsed.searchParams.get('index');\n\tif (!region || !keyId || !indexKeyId) {\n\t\tthrow new EncryptionUrlInvalid(\n\t\t\tconstruct,\n\t\t\t'a kms:// URL names a region, a key and an index key',\n\t\t);\n\t}\n\n\tconst endpoint = parsed.searchParams.get('endpoint');\n\t// Credentials are not part of the address: the default chain — a Lambda's\n\t// role deployed, the environment against an emulator.\n\tconst kms = new KMSClient({ region, ...(endpoint ? { endpoint } : {}) });\n\t// What KMS checks on every decrypt, and what CloudTrail shows each call was\n\t// for — a data key minted for one construct will not open for another.\n\tconst context = { construct };\n\tconst aad = associatedData(construct, KMS_KEY_ID);\n\n\treturn {\n\t\tasync encrypt(plaintext) {\n\t\t\tconst dataKey = await kms.send(\n\t\t\t\tnew GenerateDataKeyCommand({\n\t\t\t\t\tKeyId: keyId,\n\t\t\t\t\tKeySpec: 'AES_256',\n\t\t\t\t\tEncryptionContext: context,\n\t\t\t\t}),\n\t\t\t);\n\t\t\tconst wrapped = Buffer.from(dataKey.CiphertextBlob!);\n\t\t\tconst sealed = seal(\n\t\t\t\tBuffer.from(dataKey.Plaintext!),\n\t\t\t\tBuffer.from(plaintext, 'utf8'),\n\t\t\t\taad,\n\t\t\t);\n\n\t\t\tconst length = Buffer.alloc(2);\n\t\t\tlength.writeUInt16BE(wrapped.length);\n\t\t\treturn formatCiphertext(\n\t\t\t\tKMS_KEY_ID,\n\t\t\t\tBuffer.concat([length, wrapped, sealed]),\n\t\t\t);\n\t\t},\n\n\t\tasync decrypt(ciphertext) {\n\t\t\tconst { payload } = parseCiphertext(construct, ciphertext);\n\t\t\tconst length = payload.length >= 2 ? payload.readUInt16BE(0) : 0;\n\t\t\tconst wrapped = payload.subarray(2, 2 + length);\n\t\t\tif (!length || wrapped.length !== length) {\n\t\t\t\tthrow new CiphertextNotAuthentic(construct);\n\t\t\t}\n\n\t\t\tconst dataKey = await kms.send(\n\t\t\t\tnew DecryptCommand({\n\t\t\t\t\tCiphertextBlob: wrapped,\n\t\t\t\t\tEncryptionContext: context,\n\t\t\t\t}),\n\t\t\t);\n\t\t\tconst plaintext = open(\n\t\t\t\tBuffer.from(dataKey.Plaintext!),\n\t\t\t\tpayload.subarray(2 + length),\n\t\t\t\taad,\n\t\t\t);\n\t\t\tif (!plaintext) throw new CiphertextNotAuthentic(construct);\n\n\t\t\treturn plaintext.toString('utf8');\n\t\t},\n\n\t\tasync index(value) {\n\t\t\tconst { Mac } = await kms.send(\n\t\t\t\tnew GenerateMacCommand({\n\t\t\t\t\tKeyId: indexKeyId,\n\t\t\t\t\tMacAlgorithm: 'HMAC_SHA_256',\n\t\t\t\t\tMessage: Buffer.from(value, 'utf8'),\n\t\t\t\t}),\n\t\t\t);\n\t\t\treturn Buffer.from(Mac!).toString('base64url');\n\t\t},\n\n\t\t// KMS keeps every version it rotated through, so a value is never under\n\t\t// a key that could be retired from under it.\n\t\tasync reencrypt(ciphertext) {\n\t\t\tparseCiphertext(construct, ciphertext);\n\t\t\treturn ciphertext;\n\t\t},\n\t};\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAsCA,MAAM,aAAa;AAEnB,SAAgB,UAAU,WAAmB,KAAqB;CACjE,MAAM,SAAS,IAAI,IAAI,GAAG;CAC1B,MAAM,SAAS,OAAO;CACtB,MAAM,QAAQ,OAAO,aAAa,IAAI,KAAK;CAC3C,MAAM,aAAa,OAAO,aAAa,IAAI,OAAO;CAClD,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,YACzB,MAAM,IAAIA,eAAAA,qBACT,WACA,qDACD;CAGD,MAAM,WAAW,OAAO,aAAa,IAAI,UAAU;CAGnD,MAAM,MAAM,IAAIC,oBAAAA,UAAU;EAAE;EAAQ,GAAI,WAAW,EAAE,SAAS,IAAI,CAAC;CAAG,CAAC;CAGvE,MAAM,UAAU,EAAE,UAAU;CAC5B,MAAM,MAAMC,eAAAA,eAAe,WAAW,UAAU;CAEhD,OAAO;EACN,MAAM,QAAQ,WAAW;GACxB,MAAM,UAAU,MAAM,IAAI,KACzB,IAAIC,oBAAAA,uBAAuB;IAC1B,OAAO;IACP,SAAS;IACT,mBAAmB;GACpB,CAAC,CACF;GACA,MAAM,UAAU,OAAO,KAAK,QAAQ,cAAe;GACnD,MAAM,SAASC,eAAAA,KACd,OAAO,KAAK,QAAQ,SAAU,GAC9B,OAAO,KAAK,WAAW,MAAM,GAC7B,GACD;GAEA,MAAM,SAAS,OAAO,MAAM,CAAC;GAC7B,OAAO,cAAc,QAAQ,MAAM;GACnC,OAAOC,eAAAA,iBACN,YACA,OAAO,OAAO;IAAC;IAAQ;IAAS;GAAM,CAAC,CACxC;EACD;EAEA,MAAM,QAAQ,YAAY;GACzB,MAAM,EAAE,YAAYC,eAAAA,gBAAgB,WAAW,UAAU;GACzD,MAAM,SAAS,QAAQ,UAAU,IAAI,QAAQ,aAAa,CAAC,IAAI;GAC/D,MAAM,UAAU,QAAQ,SAAS,GAAG,IAAI,MAAM;GAC9C,IAAI,CAAC,UAAU,QAAQ,WAAW,QACjC,MAAM,IAAIC,eAAAA,uBAAuB,SAAS;GAG3C,MAAM,UAAU,MAAM,IAAI,KACzB,IAAIC,oBAAAA,eAAe;IAClB,gBAAgB;IAChB,mBAAmB;GACpB,CAAC,CACF;GACA,MAAM,YAAYC,eAAAA,KACjB,OAAO,KAAK,QAAQ,SAAU,GAC9B,QAAQ,SAAS,IAAI,MAAM,GAC3B,GACD;GACA,IAAI,CAAC,WAAW,MAAM,IAAIF,eAAAA,uBAAuB,SAAS;GAE1D,OAAO,UAAU,SAAS,MAAM;EACjC;EAEA,MAAM,MAAM,OAAO;GAClB,MAAM,EAAE,QAAQ,MAAM,IAAI,KACzB,IAAIG,oBAAAA,mBAAmB;IACtB,OAAO;IACP,cAAc;IACd,SAAS,OAAO,KAAK,OAAO,MAAM;GACnC,CAAC,CACF;GACA,OAAO,OAAO,KAAK,GAAI,CAAC,CAAC,SAAS,WAAW;EAC9C;EAIA,MAAM,UAAU,YAAY;GAC3B,eAAA,gBAAgB,WAAW,UAAU;GACrC,OAAO;EACR;CACD;AACD"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"kms-BdUlMJEZ.mjs","names":[],"sources":["../src/encryption/kms.ts"],"sourcesContent":["/**\n * The `kms://` cipher: envelope encryption under an AWS KMS key.\n *\n * Each `encrypt` asks KMS for a fresh data key, seals the plaintext with it\n * locally, and stores the data key *as KMS encrypted it* beside the data. The\n * plaintext data key is used once and dropped, and the master key never leaves\n * KMS — every decrypt is a KMS call IAM can refuse and CloudTrail records.\n *\n * Rotation is KMS's: it keeps every backing version of the key, and the\n * encrypted data key names the one that wrapped it, so nothing here tracks a\n * version and `reencrypt` has nothing to move. The ciphertext's key field is\n * `kms` for that reason.\n *\n * The blind index is a KMS HMAC key's `GenerateMac`: deterministic, and the\n * key is as unextractable as the encryption key.\n *\n * In its own module, loaded only when a URL says `kms://`, so a project that\n * never deploys to AWS never needs `@aws-sdk/client-kms` installed.\n */\n\nimport {\n\tDecryptCommand,\n\tGenerateDataKeyCommand,\n\tGenerateMacCommand,\n\tKMSClient,\n} from '@aws-sdk/client-kms';\nimport {\n\tassociatedData,\n\ttype Cipher,\n\tCiphertextNotAuthentic,\n\tEncryptionUrlInvalid,\n\tformatCiphertext,\n\topen,\n\tparseCiphertext,\n\tseal,\n} from './cipher';\n\n/** The ciphertext's key field: the version lives inside KMS's own blob. */\nconst KMS_KEY_ID = 'kms';\n\nexport function kmsCipher(construct: string, url: string): Cipher {\n\tconst parsed = new URL(url);\n\tconst region = parsed.hostname;\n\tconst keyId = parsed.searchParams.get('key');\n\tconst indexKeyId = parsed.searchParams.get('index');\n\tif (!region || !keyId || !indexKeyId) {\n\t\tthrow new EncryptionUrlInvalid(\n\t\t\tconstruct,\n\t\t\t'a kms:// URL names a region, a key and an index key',\n\t\t);\n\t}\n\n\tconst endpoint = parsed.searchParams.get('endpoint');\n\t// Credentials are not part of the address: the default chain — a Lambda's\n\t// role deployed, the environment against an emulator.\n\tconst kms = new KMSClient({ region, ...(endpoint ? { endpoint } : {}) });\n\t// What KMS checks on every decrypt, and what CloudTrail shows each call was\n\t// for — a data key minted for one construct will not open for another.\n\tconst context = { construct };\n\tconst aad = associatedData(construct, KMS_KEY_ID);\n\n\treturn {\n\t\tasync encrypt(plaintext) {\n\t\t\tconst dataKey = await kms.send(\n\t\t\t\tnew GenerateDataKeyCommand({\n\t\t\t\t\tKeyId: keyId,\n\t\t\t\t\tKeySpec: 'AES_256',\n\t\t\t\t\tEncryptionContext: context,\n\t\t\t\t}),\n\t\t\t);\n\t\t\tconst wrapped = Buffer.from(dataKey.CiphertextBlob!);\n\t\t\tconst sealed = seal(\n\t\t\t\tBuffer.from(dataKey.Plaintext!),\n\t\t\t\tBuffer.from(plaintext, 'utf8'),\n\t\t\t\taad,\n\t\t\t);\n\n\t\t\tconst length = Buffer.alloc(2);\n\t\t\tlength.writeUInt16BE(wrapped.length);\n\t\t\treturn formatCiphertext(\n\t\t\t\tKMS_KEY_ID,\n\t\t\t\tBuffer.concat([length, wrapped, sealed]),\n\t\t\t);\n\t\t},\n\n\t\tasync decrypt(ciphertext) {\n\t\t\tconst { payload } = parseCiphertext(construct, ciphertext);\n\t\t\tconst length = payload.length >= 2 ? payload.readUInt16BE(0) : 0;\n\t\t\tconst wrapped = payload.subarray(2, 2 + length);\n\t\t\tif (!length || wrapped.length !== length) {\n\t\t\t\tthrow new CiphertextNotAuthentic(construct);\n\t\t\t}\n\n\t\t\tconst dataKey = await kms.send(\n\t\t\t\tnew DecryptCommand({\n\t\t\t\t\tCiphertextBlob: wrapped,\n\t\t\t\t\tEncryptionContext: context,\n\t\t\t\t}),\n\t\t\t);\n\t\t\tconst plaintext = open(\n\t\t\t\tBuffer.from(dataKey.Plaintext!),\n\t\t\t\tpayload.subarray(2 + length),\n\t\t\t\taad,\n\t\t\t);\n\t\t\tif (!plaintext) throw new CiphertextNotAuthentic(construct);\n\n\t\t\treturn plaintext.toString('utf8');\n\t\t},\n\n\t\tasync index(value) {\n\t\t\tconst { Mac } = await kms.send(\n\t\t\t\tnew GenerateMacCommand({\n\t\t\t\t\tKeyId: indexKeyId,\n\t\t\t\t\tMacAlgorithm: 'HMAC_SHA_256',\n\t\t\t\t\tMessage: Buffer.from(value, 'utf8'),\n\t\t\t\t}),\n\t\t\t);\n\t\t\treturn Buffer.from(Mac!).toString('base64url');\n\t\t},\n\n\t\t// KMS keeps every version it rotated through, so a value is never under\n\t\t// a key that could be retired from under it.\n\t\tasync reencrypt(ciphertext) {\n\t\t\tparseCiphertext(construct, ciphertext);\n\t\t\treturn ciphertext;\n\t\t},\n\t};\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAsCA,MAAM,aAAa;AAEnB,SAAgB,UAAU,WAAmB,KAAqB;CACjE,MAAM,SAAS,IAAI,IAAI,GAAG;CAC1B,MAAM,SAAS,OAAO;CACtB,MAAM,QAAQ,OAAO,aAAa,IAAI,KAAK;CAC3C,MAAM,aAAa,OAAO,aAAa,IAAI,OAAO;CAClD,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,YACzB,MAAM,IAAI,qBACT,WACA,qDACD;CAGD,MAAM,WAAW,OAAO,aAAa,IAAI,UAAU;CAGnD,MAAM,MAAM,IAAI,UAAU;EAAE;EAAQ,GAAI,WAAW,EAAE,SAAS,IAAI,CAAC;CAAG,CAAC;CAGvE,MAAM,UAAU,EAAE,UAAU;CAC5B,MAAM,MAAM,eAAe,WAAW,UAAU;CAEhD,OAAO;EACN,MAAM,QAAQ,WAAW;GACxB,MAAM,UAAU,MAAM,IAAI,KACzB,IAAI,uBAAuB;IAC1B,OAAO;IACP,SAAS;IACT,mBAAmB;GACpB,CAAC,CACF;GACA,MAAM,UAAU,OAAO,KAAK,QAAQ,cAAe;GACnD,MAAM,SAAS,KACd,OAAO,KAAK,QAAQ,SAAU,GAC9B,OAAO,KAAK,WAAW,MAAM,GAC7B,GACD;GAEA,MAAM,SAAS,OAAO,MAAM,CAAC;GAC7B,OAAO,cAAc,QAAQ,MAAM;GACnC,OAAO,iBACN,YACA,OAAO,OAAO;IAAC;IAAQ;IAAS;GAAM,CAAC,CACxC;EACD;EAEA,MAAM,QAAQ,YAAY;GACzB,MAAM,EAAE,YAAY,gBAAgB,WAAW,UAAU;GACzD,MAAM,SAAS,QAAQ,UAAU,IAAI,QAAQ,aAAa,CAAC,IAAI;GAC/D,MAAM,UAAU,QAAQ,SAAS,GAAG,IAAI,MAAM;GAC9C,IAAI,CAAC,UAAU,QAAQ,WAAW,QACjC,MAAM,IAAI,uBAAuB,SAAS;GAG3C,MAAM,UAAU,MAAM,IAAI,KACzB,IAAI,eAAe;IAClB,gBAAgB;IAChB,mBAAmB;GACpB,CAAC,CACF;GACA,MAAM,YAAY,KACjB,OAAO,KAAK,QAAQ,SAAU,GAC9B,QAAQ,SAAS,IAAI,MAAM,GAC3B,GACD;GACA,IAAI,CAAC,WAAW,MAAM,IAAI,uBAAuB,SAAS;GAE1D,OAAO,UAAU,SAAS,MAAM;EACjC;EAEA,MAAM,MAAM,OAAO;GAClB,MAAM,EAAE,QAAQ,MAAM,IAAI,KACzB,IAAI,mBAAmB;IACtB,OAAO;IACP,cAAc;IACd,SAAS,OAAO,KAAK,OAAO,MAAM;GACnC,CAAC,CACF;GACA,OAAO,OAAO,KAAK,GAAI,CAAC,CAAC,SAAS,WAAW;EAC9C;EAIA,MAAM,UAAU,YAAY;GAC3B,gBAAgB,WAAW,UAAU;GACrC,OAAO;EACR;CACD;AACD"}