@geekmidas/constructs 10.0.0-alpha.116 → 10.0.0-alpha.118

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.
Files changed (45) hide show
  1. package/dist/adaptors/testing.cjs +1 -1
  2. package/dist/adaptors/testing.d.cts +1 -1
  3. package/dist/adaptors/testing.d.mts +1 -1
  4. package/dist/adaptors/testing.mjs +1 -1
  5. package/dist/cipher-C1lEFX63.mjs.map +1 -1
  6. package/dist/cipher-CVxfwLjk.cjs.map +1 -1
  7. package/dist/cipher-DSDxPRIp.d.cts +106 -0
  8. package/dist/cipher-DSDxPRIp.d.cts.map +1 -0
  9. package/dist/cipher-DSDxPRIp.d.mts +106 -0
  10. package/dist/cipher-DSDxPRIp.d.mts.map +1 -0
  11. package/dist/database/kysely.cjs +1 -1
  12. package/dist/database/kysely.d.cts +2 -2
  13. package/dist/database/kysely.d.mts +2 -2
  14. package/dist/database/kysely.mjs +1 -1
  15. package/dist/{kms-B-Hm3d5r.cjs → encryption/kms.cjs} +14 -4
  16. package/dist/encryption/kms.cjs.map +1 -0
  17. package/dist/encryption/kms.d.cts +7 -0
  18. package/dist/encryption/kms.d.cts.map +1 -0
  19. package/dist/encryption/kms.d.mts +7 -0
  20. package/dist/encryption/kms.d.mts.map +1 -0
  21. package/dist/{kms-BdUlMJEZ.mjs → encryption/kms.mjs} +13 -5
  22. package/dist/encryption/kms.mjs.map +1 -0
  23. package/dist/encryption.cjs +24 -4
  24. package/dist/encryption.cjs.map +1 -1
  25. package/dist/encryption.d.cts +8 -88
  26. package/dist/encryption.d.cts.map +1 -1
  27. package/dist/encryption.d.mts +8 -88
  28. package/dist/encryption.d.mts.map +1 -1
  29. package/dist/encryption.mjs +23 -5
  30. package/dist/encryption.mjs.map +1 -1
  31. package/dist/{kysely-D0bpzgPs.d.cts → kysely-BHYV-WC-.d.cts} +53 -12
  32. package/dist/kysely-BHYV-WC-.d.cts.map +1 -0
  33. package/dist/{kysely-DKwnl0nf.d.mts → kysely-CAW3QSqp.d.mts} +53 -12
  34. package/dist/kysely-CAW3QSqp.d.mts.map +1 -0
  35. package/dist/{kysely-B_0BAr0T.mjs → kysely-Cs4uPO8k.mjs} +6 -5
  36. package/dist/kysely-Cs4uPO8k.mjs.map +1 -0
  37. package/dist/{kysely-B3zfg9ti.cjs → kysely-CukArHJ0.cjs} +6 -5
  38. package/dist/kysely-CukArHJ0.cjs.map +1 -0
  39. package/package.json +41 -31
  40. package/dist/kms-B-Hm3d5r.cjs.map +0 -1
  41. package/dist/kms-BdUlMJEZ.mjs.map +0 -1
  42. package/dist/kysely-B3zfg9ti.cjs.map +0 -1
  43. package/dist/kysely-B_0BAr0T.mjs.map +0 -1
  44. package/dist/kysely-D0bpzgPs.d.cts.map +0 -1
  45. package/dist/kysely-DKwnl0nf.d.mts.map +0 -1
@@ -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"}
@@ -21,7 +21,7 @@ declare function closeDatabasePools(): Promise<void>;
21
21
  * straight through, so anything Kysely adds later arrives without being
22
22
  * tracked here.
23
23
  */
24
- interface KyselyDatabaseOptions extends Omit<KyselyConfig, 'dialect'> {
24
+ interface KyselyDatabaseBaseOptions extends Omit<KyselyConfig, 'dialect'> {
25
25
  /**
26
26
  * The schema, pinned on both roles' `search_path`.
27
27
  *
@@ -39,16 +39,15 @@ interface KyselyDatabaseOptions extends Omit<KyselyConfig, 'dialect'> {
39
39
  */
40
40
  roles?: boolean;
41
41
  /**
42
- * The Postgres major version to provision.
42
+ * Extensions to create in this database, as the cluster master, before the
43
+ * first migration — for the ones the owner role a migration runs as may not
44
+ * create (PostGIS is not a trusted extension). They are created in
45
+ * `public`, which joins this database's roles' `search_path`, so their
46
+ * types resolve unqualified.
43
47
  *
44
- * Declared here so one statement reaches both targets. It was previously
45
- * set in two unrelated places — a container tag locally, nothing at all on
46
- * AWS (so Aurora chose) — which is how local came to run 18 while deployed
47
- * ran 17.7 with nothing recording the difference.
48
- *
49
- * Defaults to {@link DEFAULT_POSTGRES_VERSION}.
48
+ * Only attempted: the image has to ship them.
50
49
  */
51
- version?: PostgresVersion;
50
+ extensions?: readonly string[];
52
51
  /**
53
52
  * End each query run inside a request with a comment naming what ran it —
54
53
  * `/*operation='POST /orders',request_id='…'*\/` — so `pg_stat_activity`
@@ -58,6 +57,48 @@ interface KyselyDatabaseOptions extends Omit<KyselyConfig, 'dialect'> {
58
57
  */
59
58
  queryTags?: boolean;
60
59
  }
60
+ /**
61
+ * Which Postgres the database runs: a major on the stock image, or an image
62
+ * the project names. One or the other, never both — an image's tag already
63
+ * says its major, and a second statement of it is one that can disagree.
64
+ */
65
+ type PostgresRuntime = {
66
+ /**
67
+ * The Postgres major version to provision, on the stock image.
68
+ *
69
+ * Declared here so one statement reaches every target. It was
70
+ * previously set in two unrelated places — a container tag locally,
71
+ * nothing at all on AWS (so Aurora chose) — which is how local came to
72
+ * run 18 while deployed ran 17.7 with nothing recording the difference.
73
+ *
74
+ * Defaults to {@link DEFAULT_POSTGRES_VERSION}.
75
+ */
76
+ version?: PostgresVersion;
77
+ image?: never;
78
+ } | {
79
+ /**
80
+ * The container image to run, wherever a target runs one (local,
81
+ * Dokploy), in place of `postgres:<version>`.
82
+ *
83
+ * Yours to choose and used as written: its tag is the major, and it
84
+ * should ship every extension in `extensions`. AWS runs RDS, ignores
85
+ * it, and provisions {@link DEFAULT_POSTGRES_VERSION}.
86
+ *
87
+ * ```ts
88
+ * new KyselyDatabase<GeoDB>('Geo', {
89
+ * image: 'postgis/postgis:18-3.6-alpine',
90
+ * extensions: ['postgis'],
91
+ * });
92
+ * ```
93
+ */
94
+ image: string;
95
+ version?: never;
96
+ };
97
+ /**
98
+ * Everything Kysely takes, plus what this construct declares — including
99
+ * which Postgres it runs, as a version or an image.
100
+ */
101
+ type KyselyDatabaseOptions = KyselyDatabaseBaseOptions & PostgresRuntime;
61
102
  /** What makes a construct derived rather than a database in its own right. */
62
103
  interface DerivedFrom {
63
104
  kind: 'database-reader' | 'database-schema';
@@ -82,7 +123,7 @@ declare class KyselyDatabase<DB = unknown, TName extends string = string> implem
82
123
  private readonly config;
83
124
  private readonly schemaName;
84
125
  private readonly roles;
85
- private readonly version;
126
+ private readonly runtime;
86
127
  private readonly derivedFrom?;
87
128
  constructor(id: ConstructName<TName>, options?: KyselyDatabaseOptions, derivedFrom?: DerivedFrom);
88
129
  declare(): Declaration[];
@@ -171,5 +212,5 @@ declare class KyselyDatabase<DB = unknown, TName extends string = string> implem
171
212
  */
172
213
  type ReadOnlyDatabase<DB, TName extends string> = Omit<KyselyDatabase<DB, TName>, 'reader' | 'cache' | 'schema'>;
173
214
  //#endregion
174
- export { closeDatabasePools as i, KyselyDatabaseOptions as n, ReadOnlyDatabase as r, KyselyDatabase as t };
175
- //# sourceMappingURL=kysely-D0bpzgPs.d.cts.map
215
+ export { closeDatabasePools as a, ReadOnlyDatabase as i, KyselyDatabaseOptions as n, PostgresRuntime as r, KyselyDatabase as t };
216
+ //# sourceMappingURL=kysely-BHYV-WC-.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"kysely-BHYV-WC-.d.cts","names":[],"sources":["../src/database/pool.ts","../src/database/kysely.ts"],"mappings":";;;;;;;;;;;iBAwGsB,sBAAsB;;;;;;;;;;;;UChElC,kCAAkC,KAAK;;;;;;;;EAQhD;;;;;;;;EAQA;;;;;;;;;;EAUA;;;;;;;;EAQA;;;;;;;KAQW;;;;;;;;;;;EAYT,UAAU;EACV;;;;;;;;;;;;;;;;;EAkBA;EACA;;;;;;KAOS,wBAAwB,4BAA4B;;UAGtD;EACT;EACA;EACA;;;;;;;;;cAUY,eAAe,cAAc,0CAC9B,UAAU,OAAO,OAAO;mBAiBjB;WAfT,IAAI;WACJ,SAAS,QAAQ,aAAa,QAAQ,OAAO;;;;;mBAMrC;mBACA;mBACA;mBACA;mBACA;EAEjB,YACC,IAAI,cAAc,QACD,UAAS,uBAC1B,cAAc;EA2Bf,WAAW;;;;;;;;;;;;;EAyDX,UAAU,iBAAiB,OAAO;;;;;;;;;;;;;;;;;;EAyBlC,MAAM,2BAA2B,cAChC,KAAI,cAAc,SAClB;IAAW;MACT,MAAM;;;;;;;;;EAeT,OAAO,UAAU,wBAChB,IAAI,cAAc,UAClB,UAAS,wBACP,eAAe,UAAU;;;;;;;;;;;;;MAoBxB,SAAS,WAAW,aAAa,eAAe,OAAO;;;;;;;;;MAgBvD,gBAAgB,KAAK;UAiBX;;;;;;;;;;;;;;;;;;;KAuCH,iBAAiB,IAAI,wBAAwB,KACxD,eAAe,IAAI"}
@@ -21,7 +21,7 @@ declare function closeDatabasePools(): Promise<void>;
21
21
  * straight through, so anything Kysely adds later arrives without being
22
22
  * tracked here.
23
23
  */
24
- interface KyselyDatabaseOptions extends Omit<KyselyConfig, 'dialect'> {
24
+ interface KyselyDatabaseBaseOptions extends Omit<KyselyConfig, 'dialect'> {
25
25
  /**
26
26
  * The schema, pinned on both roles' `search_path`.
27
27
  *
@@ -39,16 +39,15 @@ interface KyselyDatabaseOptions extends Omit<KyselyConfig, 'dialect'> {
39
39
  */
40
40
  roles?: boolean;
41
41
  /**
42
- * The Postgres major version to provision.
42
+ * Extensions to create in this database, as the cluster master, before the
43
+ * first migration — for the ones the owner role a migration runs as may not
44
+ * create (PostGIS is not a trusted extension). They are created in
45
+ * `public`, which joins this database's roles' `search_path`, so their
46
+ * types resolve unqualified.
43
47
  *
44
- * Declared here so one statement reaches both targets. It was previously
45
- * set in two unrelated places — a container tag locally, nothing at all on
46
- * AWS (so Aurora chose) — which is how local came to run 18 while deployed
47
- * ran 17.7 with nothing recording the difference.
48
- *
49
- * Defaults to {@link DEFAULT_POSTGRES_VERSION}.
48
+ * Only attempted: the image has to ship them.
50
49
  */
51
- version?: PostgresVersion;
50
+ extensions?: readonly string[];
52
51
  /**
53
52
  * End each query run inside a request with a comment naming what ran it —
54
53
  * `/*operation='POST /orders',request_id='…'*\/` — so `pg_stat_activity`
@@ -58,6 +57,48 @@ interface KyselyDatabaseOptions extends Omit<KyselyConfig, 'dialect'> {
58
57
  */
59
58
  queryTags?: boolean;
60
59
  }
60
+ /**
61
+ * Which Postgres the database runs: a major on the stock image, or an image
62
+ * the project names. One or the other, never both — an image's tag already
63
+ * says its major, and a second statement of it is one that can disagree.
64
+ */
65
+ type PostgresRuntime = {
66
+ /**
67
+ * The Postgres major version to provision, on the stock image.
68
+ *
69
+ * Declared here so one statement reaches every target. It was
70
+ * previously set in two unrelated places — a container tag locally,
71
+ * nothing at all on AWS (so Aurora chose) — which is how local came to
72
+ * run 18 while deployed ran 17.7 with nothing recording the difference.
73
+ *
74
+ * Defaults to {@link DEFAULT_POSTGRES_VERSION}.
75
+ */
76
+ version?: PostgresVersion;
77
+ image?: never;
78
+ } | {
79
+ /**
80
+ * The container image to run, wherever a target runs one (local,
81
+ * Dokploy), in place of `postgres:<version>`.
82
+ *
83
+ * Yours to choose and used as written: its tag is the major, and it
84
+ * should ship every extension in `extensions`. AWS runs RDS, ignores
85
+ * it, and provisions {@link DEFAULT_POSTGRES_VERSION}.
86
+ *
87
+ * ```ts
88
+ * new KyselyDatabase<GeoDB>('Geo', {
89
+ * image: 'postgis/postgis:18-3.6-alpine',
90
+ * extensions: ['postgis'],
91
+ * });
92
+ * ```
93
+ */
94
+ image: string;
95
+ version?: never;
96
+ };
97
+ /**
98
+ * Everything Kysely takes, plus what this construct declares — including
99
+ * which Postgres it runs, as a version or an image.
100
+ */
101
+ type KyselyDatabaseOptions = KyselyDatabaseBaseOptions & PostgresRuntime;
61
102
  /** What makes a construct derived rather than a database in its own right. */
62
103
  interface DerivedFrom {
63
104
  kind: 'database-reader' | 'database-schema';
@@ -82,7 +123,7 @@ declare class KyselyDatabase<DB = unknown, TName extends string = string> implem
82
123
  private readonly config;
83
124
  private readonly schemaName;
84
125
  private readonly roles;
85
- private readonly version;
126
+ private readonly runtime;
86
127
  private readonly derivedFrom?;
87
128
  constructor(id: ConstructName<TName>, options?: KyselyDatabaseOptions, derivedFrom?: DerivedFrom);
88
129
  declare(): Declaration[];
@@ -171,5 +212,5 @@ declare class KyselyDatabase<DB = unknown, TName extends string = string> implem
171
212
  */
172
213
  type ReadOnlyDatabase<DB, TName extends string> = Omit<KyselyDatabase<DB, TName>, 'reader' | 'cache' | 'schema'>;
173
214
  //#endregion
174
- export { closeDatabasePools as i, KyselyDatabaseOptions as n, ReadOnlyDatabase as r, KyselyDatabase as t };
175
- //# sourceMappingURL=kysely-DKwnl0nf.d.mts.map
215
+ export { closeDatabasePools as a, ReadOnlyDatabase as i, KyselyDatabaseOptions as n, PostgresRuntime as r, KyselyDatabase as t };
216
+ //# sourceMappingURL=kysely-CAW3QSqp.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"kysely-CAW3QSqp.d.mts","names":[],"sources":["../src/database/pool.ts","../src/database/kysely.ts"],"mappings":";;;;;;;;;;;iBAwGsB,sBAAsB;;;;;;;;;;;;UChElC,kCAAkC,KAAK;;;;;;;;EAQhD;;;;;;;;EAQA;;;;;;;;;;EAUA;;;;;;;;EAQA;;;;;;;KAQW;;;;;;;;;;;EAYT,UAAU;EACV;;;;;;;;;;;;;;;;;EAkBA;EACA;;;;;;KAOS,wBAAwB,4BAA4B;;UAGtD;EACT;EACA;EACA;;;;;;;;;cAUY,eAAe,cAAc,0CAC9B,UAAU,OAAO,OAAO;mBAiBjB;WAfT,IAAI;WACJ,SAAS,QAAQ,aAAa,QAAQ,OAAO;;;;;mBAMrC;mBACA;mBACA;mBACA;mBACA;EAEjB,YACC,IAAI,cAAc,QACD,UAAS,uBAC1B,cAAc;EA2Bf,WAAW;;;;;;;;;;;;;EAyDX,UAAU,iBAAiB,OAAO;;;;;;;;;;;;;;;;;;EAyBlC,MAAM,2BAA2B,cAChC,KAAI,cAAc,SAClB;IAAW;MACT,MAAM;;;;;;;;;EAeT,OAAO,UAAU,wBAChB,IAAI,cAAc,UAClB,UAAS,wBACP,eAAe,UAAU;;;;;;;;;;;;;MAoBxB,SAAS,WAAW,aAAa,eAAe,OAAO;;;;;;;;;MAgBvD,gBAAgB,KAAK;UAiBX;;;;;;;;;;;;;;;;;;;KAuCH,iBAAiB,IAAI,wBAAwB,KACxD,eAAe,IAAI"}