@noy-db/at-env 0.1.0-pre.16

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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 vLannaAi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,84 @@
1
+ # @noy-db/at-env
2
+
3
+ **Env-var sealing key provider for noy-db [managed-passphrase mode](https://github.com/vLannaAi/noy-db/issues/14).**
4
+
5
+ The smallest production-shape provider in the `at-*` family. Reads a 32-byte AES-256-GCM key from an environment variable (base64-encoded) and uses it to seal the hub-generated random passphrase — so your users never see or type a passphrase, but the encryption keys are still under your control.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pnpm add @noy-db/hub @noy-db/at-env
11
+ # or: npm install @noy-db/hub @noy-db/at-env
12
+ ```
13
+
14
+ ## Setup
15
+
16
+ ```bash
17
+ # 1. Generate a 256-bit key once. Store in your platform's secret manager
18
+ # (Kubernetes Secrets, Heroku env, Doppler, AWS Secrets Manager, etc.).
19
+ export NOYDB_SEALING_KEY=$(openssl rand -base64 32)
20
+ ```
21
+
22
+ ```ts
23
+ // 2. In your app:
24
+ import { createNoydb } from '@noy-db/hub'
25
+ import { envSealingProvider } from '@noy-db/at-env'
26
+
27
+ const db = await createNoydb({
28
+ store,
29
+ user: 'alice',
30
+ passphraseMode: 'managed',
31
+ sealingKey: envSealingProvider(), // reads NOYDB_SEALING_KEY by default
32
+ })
33
+
34
+ const vault = await db.openVault('acme')
35
+ // Hub generated a 256-bit random on first open, sealed it under your env
36
+ // key, and persisted to _meta/sealed-passphrase. The user never sees a
37
+ // passphrase. On reopen, at-env unseals transparently.
38
+ ```
39
+
40
+ ## When to use this provider
41
+
42
+ - ✅ Single-node SaaS where the env var comes from Kubernetes Secrets, Heroku env, Doppler, AWS Secrets Manager mounted at boot, systemd env, etc.
43
+ - ✅ Local dev / CI where you want persistence across restarts (which `MemorySealingKeyProvider` can't do — it's in-process only).
44
+ - ✅ Prototypes where setting up AWS KMS / GCP KMS isn't worth the effort.
45
+
46
+ ## When NOT to use this provider
47
+
48
+ - ❌ Laptops or shared dev machines where other users have shell access. They can `echo $NOYDB_SEALING_KEY` and exfiltrate the key. Use [`@noy-db/at-macos-keychain`](../at-macos-keychain) / [`@noy-db/at-wincred`](../at-wincred) / [`@noy-db/at-libsecret`](../at-libsecret) for desktop apps. *(coming soon)*
49
+ - ❌ Compliance regimes requiring auditable key access logs (FedRAMP, HIPAA with managed-encryption requirements). Use [`@noy-db/at-aws-kms`](../at-aws-kms) for KMS-backed key access auditing. *(coming soon)*
50
+
51
+ ## Key rotation
52
+
53
+ Rotating the env-var key is currently manual:
54
+
55
+ 1. Open the vault under the OLD env key.
56
+ 2. Generate a new key: `NEW=$(openssl rand -base64 32)`.
57
+ 3. Read `_meta/sealed-passphrase` from the store.
58
+ 4. Unseal under the OLD provider, re-seal under a NEW provider (`envSealingProvider({ envVar: 'NOYDB_NEW_KEY' })`).
59
+ 5. Overwrite `_meta/sealed-passphrase`.
60
+ 6. Swap the env var to the new value.
61
+
62
+ An automated `noydb seal rotate` CLI command is tracked as a follow-up. For lower-touch rotation, use `@noy-db/at-aws-kms` once it lands — KMS handles CMK rotation automatically.
63
+
64
+ ## Threat model
65
+
66
+ The env var IS the security boundary. If an attacker gains read access to your process's environment, they can decrypt every record sealed under that key.
67
+
68
+ This provider is suitable when your deployment platform's secret management is trusted (you trust Kubernetes Secrets, you trust Heroku's env injection, etc.) and unsuitable when the env var sits in a shell profile, a `.env` file in version control, or a multi-tenant host where other tenants can read your environment.
69
+
70
+ ## API
71
+
72
+ ```ts
73
+ function envSealingProvider(opts?: {
74
+ envVar?: string // default 'NOYDB_SEALING_KEY'
75
+ }): SealingKeyProvider
76
+ ```
77
+
78
+ Returns a [`SealingKeyProvider`](../hub/src/team/managed-passphrase.ts) — the contract `@noy-db/hub`'s managed-passphrase mode consumes. The provider validates the env var at construction time and caches the imported `CryptoKey` for the lifetime of the instance.
79
+
80
+ Throws at construction when the env var is unset, not valid base64, or not 32 bytes.
81
+
82
+ ## License
83
+
84
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,106 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ envSealingProvider: () => envSealingProvider
24
+ });
25
+ module.exports = __toCommonJS(index_exports);
26
+ function envSealingProvider(opts = {}) {
27
+ const envVar = opts.envVar ?? "NOYDB_SEALING_KEY";
28
+ const raw = process.env[envVar];
29
+ if (!raw) {
30
+ throw new Error(
31
+ `@noy-db/at-env: env var "${envVar}" is not set. Generate a key via \`openssl rand -base64 32\` and export it before starting the process.`
32
+ );
33
+ }
34
+ let keyBytes;
35
+ try {
36
+ keyBytes = base64ToBytes(raw);
37
+ } catch (err) {
38
+ throw new Error(
39
+ `@noy-db/at-env: env var "${envVar}" is not valid base64 \u2014 ` + (err instanceof Error ? err.message : String(err))
40
+ );
41
+ }
42
+ if (keyBytes.length !== 32) {
43
+ throw new Error(
44
+ `@noy-db/at-env: env var "${envVar}" decodes to ${keyBytes.length} bytes; expected 32 bytes (256-bit AES key). Regenerate via \`openssl rand -base64 32\`.`
45
+ );
46
+ }
47
+ let cachedKey = null;
48
+ const getKey = () => {
49
+ if (!cachedKey) {
50
+ cachedKey = globalThis.crypto.subtle.importKey(
51
+ "raw",
52
+ keyBytes,
53
+ "AES-GCM",
54
+ false,
55
+ ["encrypt", "decrypt"]
56
+ );
57
+ }
58
+ return cachedKey;
59
+ };
60
+ return {
61
+ id: `env:${envVar}`,
62
+ async seal(passphrase) {
63
+ const key = await getKey();
64
+ const iv = globalThis.crypto.getRandomValues(new Uint8Array(12));
65
+ const ciphertext = await globalThis.crypto.subtle.encrypt(
66
+ { name: "AES-GCM", iv },
67
+ key,
68
+ passphrase
69
+ );
70
+ const out = new Uint8Array(12 + ciphertext.byteLength);
71
+ out.set(iv, 0);
72
+ out.set(new Uint8Array(ciphertext), 12);
73
+ return out;
74
+ },
75
+ async unseal(sealed) {
76
+ if (sealed.length < 12 + 16) {
77
+ throw new Error(
78
+ `@noy-db/at-env: sealed bytes too short (${sealed.length} < 28). Input is not a valid at-env-sealed envelope.`
79
+ );
80
+ }
81
+ const iv = sealed.subarray(0, 12);
82
+ const body = sealed.subarray(12);
83
+ const key = await getKey();
84
+ const plaintext = await globalThis.crypto.subtle.decrypt(
85
+ { name: "AES-GCM", iv },
86
+ key,
87
+ body
88
+ );
89
+ return new Uint8Array(plaintext);
90
+ }
91
+ };
92
+ }
93
+ function base64ToBytes(b64) {
94
+ if (!/^[A-Za-z0-9+/]+={0,2}$/.test(b64.trim())) {
95
+ throw new Error("input contains characters outside the base64 alphabet");
96
+ }
97
+ const binary = atob(b64);
98
+ const out = new Uint8Array(binary.length);
99
+ for (let i = 0; i < binary.length; i++) out[i] = binary.charCodeAt(i);
100
+ return out;
101
+ }
102
+ // Annotate the CommonJS export names for ESM import in node:
103
+ 0 && (module.exports = {
104
+ envSealingProvider
105
+ });
106
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * **@noy-db/at-env** — env-var sealing key provider for noy-db\n * managed-passphrase mode (#14).\n *\n * The smallest production-shape provider in the `at-*` family. Reads a\n * 32-byte AES-256-GCM key from an environment variable (base64-encoded)\n * and uses it to seal / unseal the hub-generated random passphrase.\n *\n * ## When to use\n *\n * - Single-node SaaS where the env var comes from Kubernetes Secrets,\n * Heroku env, Doppler, AWS Secrets Manager (mounted at boot), systemd\n * service env, etc. — anywhere your platform already manages secrets.\n * - Local dev / CI where you want persistence across process restarts\n * (which `MemorySealingKeyProvider` from `@noy-db/hub` can't do).\n * - Prototypes where setting up AWS KMS / GCP KMS isn't worth the effort.\n *\n * ## When NOT to use\n *\n * - Laptops / shared dev machines where other users have shell access.\n * They can `echo $NOYDB_SEALING_KEY` and exfiltrate the key. Use\n * `@noy-db/at-macos-keychain` / `@noy-db/at-wincred` /\n * `@noy-db/at-libsecret` for desktop apps.\n * - Compliance regimes requiring auditable key access (FedRAMP, HIPAA\n * with managed-encryption requirements). Use `@noy-db/at-aws-kms` for\n * KMS-backed key access logs.\n *\n * ## Setup\n *\n * ```bash\n * # 1. Generate a 256-bit key once. Store in your platform's secret manager.\n * export NOYDB_SEALING_KEY=$(openssl rand -base64 32)\n * ```\n *\n * ```ts\n * // 2. In your app:\n * import { createNoydb } from '@noy-db/hub'\n * import { envSealingProvider } from '@noy-db/at-env'\n *\n * const db = await createNoydb({\n * store,\n * user: 'alice',\n * passphraseMode: 'managed',\n * sealingKey: envSealingProvider(), // reads NOYDB_SEALING_KEY by default\n * })\n * ```\n *\n * @packageDocumentation\n */\n\nimport type { SealingKeyProvider } from '@noy-db/hub'\n\n/** Options for {@link envSealingProvider}. */\nexport interface EnvSealingProviderOptions {\n /**\n * Environment variable name holding the base64-encoded 32-byte AES-256\n * key. Default `'NOYDB_SEALING_KEY'`.\n */\n readonly envVar?: string\n}\n\n/**\n * Build a {@link SealingKeyProvider} backed by an environment variable.\n *\n * The env var must contain a base64-encoded 32-byte (256-bit) key. The\n * provider validates the value at construction time and caches the\n * imported `CryptoKey` for the lifetime of the instance.\n *\n * @throws Error when the env var is unset, not base64, or not 32 bytes.\n */\nexport function envSealingProvider(\n opts: EnvSealingProviderOptions = {},\n): SealingKeyProvider {\n const envVar = opts.envVar ?? 'NOYDB_SEALING_KEY'\n const raw = process.env[envVar]\n if (!raw) {\n throw new Error(\n `@noy-db/at-env: env var \"${envVar}\" is not set. Generate a key via `\n + '`openssl rand -base64 32` and export it before starting the process.',\n )\n }\n\n let keyBytes: Uint8Array\n try {\n keyBytes = base64ToBytes(raw)\n } catch (err) {\n throw new Error(\n `@noy-db/at-env: env var \"${envVar}\" is not valid base64 — `\n + (err instanceof Error ? err.message : String(err)),\n )\n }\n\n if (keyBytes.length !== 32) {\n throw new Error(\n `@noy-db/at-env: env var \"${envVar}\" decodes to ${keyBytes.length} bytes; `\n + 'expected 32 bytes (256-bit AES key). Regenerate via `openssl rand -base64 32`.',\n )\n }\n\n // Lazy + cached import — the CryptoKey is created on first seal/unseal\n // and reused for the lifetime of the provider instance.\n let cachedKey: Promise<CryptoKey> | null = null\n const getKey = (): Promise<CryptoKey> => {\n if (!cachedKey) {\n cachedKey = globalThis.crypto.subtle.importKey(\n 'raw',\n keyBytes as unknown as BufferSource,\n 'AES-GCM',\n false,\n ['encrypt', 'decrypt'],\n )\n }\n return cachedKey\n }\n\n return {\n id: `env:${envVar}`,\n\n async seal(passphrase: Uint8Array): Promise<Uint8Array> {\n const key = await getKey()\n const iv = globalThis.crypto.getRandomValues(new Uint8Array(12))\n const ciphertext = await globalThis.crypto.subtle.encrypt(\n { name: 'AES-GCM', iv: iv as BufferSource },\n key,\n passphrase as unknown as BufferSource,\n )\n // Output format: [12-byte IV][ciphertext + 16-byte GCM tag]\n const out = new Uint8Array(12 + ciphertext.byteLength)\n out.set(iv, 0)\n out.set(new Uint8Array(ciphertext), 12)\n return out\n },\n\n async unseal(sealed: Uint8Array): Promise<Uint8Array> {\n // 12-byte IV + ≥ 16-byte GCM tag minimum.\n if (sealed.length < 12 + 16) {\n throw new Error(\n `@noy-db/at-env: sealed bytes too short (${sealed.length} < 28). `\n + 'Input is not a valid at-env-sealed envelope.',\n )\n }\n const iv = sealed.subarray(0, 12)\n const body = sealed.subarray(12)\n const key = await getKey()\n const plaintext = await globalThis.crypto.subtle.decrypt(\n { name: 'AES-GCM', iv: iv as BufferSource },\n key,\n body as unknown as BufferSource,\n )\n return new Uint8Array(plaintext)\n },\n }\n}\n\n// ─── base64 helpers (browser + node + cf-workers compatible) ──────────\n\nfunction base64ToBytes(b64: string): Uint8Array {\n // Reject obviously malformed strings before atob throws InvalidCharacterError,\n // so the wrapping error message is informative.\n if (!/^[A-Za-z0-9+/]+={0,2}$/.test(b64.trim())) {\n throw new Error('input contains characters outside the base64 alphabet')\n }\n const binary = atob(b64)\n const out = new Uint8Array(binary.length)\n for (let i = 0; i < binary.length; i++) out[i] = binary.charCodeAt(i)\n return out\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAsEO,SAAS,mBACd,OAAkC,CAAC,GACf;AACpB,QAAM,SAAS,KAAK,UAAU;AAC9B,QAAM,MAAM,QAAQ,IAAI,MAAM;AAC9B,MAAI,CAAC,KAAK;AACR,UAAM,IAAI;AAAA,MACR,4BAA4B,MAAM;AAAA,IAEpC;AAAA,EACF;AAEA,MAAI;AACJ,MAAI;AACF,eAAW,cAAc,GAAG;AAAA,EAC9B,SAAS,KAAK;AACZ,UAAM,IAAI;AAAA,MACR,4BAA4B,MAAM,mCAC/B,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAAA,IACpD;AAAA,EACF;AAEA,MAAI,SAAS,WAAW,IAAI;AAC1B,UAAM,IAAI;AAAA,MACR,4BAA4B,MAAM,gBAAgB,SAAS,MAAM;AAAA,IAEnE;AAAA,EACF;AAIA,MAAI,YAAuC;AAC3C,QAAM,SAAS,MAA0B;AACvC,QAAI,CAAC,WAAW;AACd,kBAAY,WAAW,OAAO,OAAO;AAAA,QACnC;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA,CAAC,WAAW,SAAS;AAAA,MACvB;AAAA,IACF;AACA,WAAO;AAAA,EACT;AAEA,SAAO;AAAA,IACL,IAAI,OAAO,MAAM;AAAA,IAEjB,MAAM,KAAK,YAA6C;AACtD,YAAM,MAAM,MAAM,OAAO;AACzB,YAAM,KAAK,WAAW,OAAO,gBAAgB,IAAI,WAAW,EAAE,CAAC;AAC/D,YAAM,aAAa,MAAM,WAAW,OAAO,OAAO;AAAA,QAChD,EAAE,MAAM,WAAW,GAAuB;AAAA,QAC1C;AAAA,QACA;AAAA,MACF;AAEA,YAAM,MAAM,IAAI,WAAW,KAAK,WAAW,UAAU;AACrD,UAAI,IAAI,IAAI,CAAC;AACb,UAAI,IAAI,IAAI,WAAW,UAAU,GAAG,EAAE;AACtC,aAAO;AAAA,IACT;AAAA,IAEA,MAAM,OAAO,QAAyC;AAEpD,UAAI,OAAO,SAAS,KAAK,IAAI;AAC3B,cAAM,IAAI;AAAA,UACR,2CAA2C,OAAO,MAAM;AAAA,QAE1D;AAAA,MACF;AACA,YAAM,KAAK,OAAO,SAAS,GAAG,EAAE;AAChC,YAAM,OAAO,OAAO,SAAS,EAAE;AAC/B,YAAM,MAAM,MAAM,OAAO;AACzB,YAAM,YAAY,MAAM,WAAW,OAAO,OAAO;AAAA,QAC/C,EAAE,MAAM,WAAW,GAAuB;AAAA,QAC1C;AAAA,QACA;AAAA,MACF;AACA,aAAO,IAAI,WAAW,SAAS;AAAA,IACjC;AAAA,EACF;AACF;AAIA,SAAS,cAAc,KAAyB;AAG9C,MAAI,CAAC,yBAAyB,KAAK,IAAI,KAAK,CAAC,GAAG;AAC9C,UAAM,IAAI,MAAM,uDAAuD;AAAA,EACzE;AACA,QAAM,SAAS,KAAK,GAAG;AACvB,QAAM,MAAM,IAAI,WAAW,OAAO,MAAM;AACxC,WAAS,IAAI,GAAG,IAAI,OAAO,QAAQ,IAAK,KAAI,CAAC,IAAI,OAAO,WAAW,CAAC;AACpE,SAAO;AACT;","names":[]}
@@ -0,0 +1,72 @@
1
+ import { SealingKeyProvider } from '@noy-db/hub';
2
+
3
+ /**
4
+ * **@noy-db/at-env** — env-var sealing key provider for noy-db
5
+ * managed-passphrase mode (#14).
6
+ *
7
+ * The smallest production-shape provider in the `at-*` family. Reads a
8
+ * 32-byte AES-256-GCM key from an environment variable (base64-encoded)
9
+ * and uses it to seal / unseal the hub-generated random passphrase.
10
+ *
11
+ * ## When to use
12
+ *
13
+ * - Single-node SaaS where the env var comes from Kubernetes Secrets,
14
+ * Heroku env, Doppler, AWS Secrets Manager (mounted at boot), systemd
15
+ * service env, etc. — anywhere your platform already manages secrets.
16
+ * - Local dev / CI where you want persistence across process restarts
17
+ * (which `MemorySealingKeyProvider` from `@noy-db/hub` can't do).
18
+ * - Prototypes where setting up AWS KMS / GCP KMS isn't worth the effort.
19
+ *
20
+ * ## When NOT to use
21
+ *
22
+ * - Laptops / shared dev machines where other users have shell access.
23
+ * They can `echo $NOYDB_SEALING_KEY` and exfiltrate the key. Use
24
+ * `@noy-db/at-macos-keychain` / `@noy-db/at-wincred` /
25
+ * `@noy-db/at-libsecret` for desktop apps.
26
+ * - Compliance regimes requiring auditable key access (FedRAMP, HIPAA
27
+ * with managed-encryption requirements). Use `@noy-db/at-aws-kms` for
28
+ * KMS-backed key access logs.
29
+ *
30
+ * ## Setup
31
+ *
32
+ * ```bash
33
+ * # 1. Generate a 256-bit key once. Store in your platform's secret manager.
34
+ * export NOYDB_SEALING_KEY=$(openssl rand -base64 32)
35
+ * ```
36
+ *
37
+ * ```ts
38
+ * // 2. In your app:
39
+ * import { createNoydb } from '@noy-db/hub'
40
+ * import { envSealingProvider } from '@noy-db/at-env'
41
+ *
42
+ * const db = await createNoydb({
43
+ * store,
44
+ * user: 'alice',
45
+ * passphraseMode: 'managed',
46
+ * sealingKey: envSealingProvider(), // reads NOYDB_SEALING_KEY by default
47
+ * })
48
+ * ```
49
+ *
50
+ * @packageDocumentation
51
+ */
52
+
53
+ /** Options for {@link envSealingProvider}. */
54
+ interface EnvSealingProviderOptions {
55
+ /**
56
+ * Environment variable name holding the base64-encoded 32-byte AES-256
57
+ * key. Default `'NOYDB_SEALING_KEY'`.
58
+ */
59
+ readonly envVar?: string;
60
+ }
61
+ /**
62
+ * Build a {@link SealingKeyProvider} backed by an environment variable.
63
+ *
64
+ * The env var must contain a base64-encoded 32-byte (256-bit) key. The
65
+ * provider validates the value at construction time and caches the
66
+ * imported `CryptoKey` for the lifetime of the instance.
67
+ *
68
+ * @throws Error when the env var is unset, not base64, or not 32 bytes.
69
+ */
70
+ declare function envSealingProvider(opts?: EnvSealingProviderOptions): SealingKeyProvider;
71
+
72
+ export { type EnvSealingProviderOptions, envSealingProvider };
@@ -0,0 +1,72 @@
1
+ import { SealingKeyProvider } from '@noy-db/hub';
2
+
3
+ /**
4
+ * **@noy-db/at-env** — env-var sealing key provider for noy-db
5
+ * managed-passphrase mode (#14).
6
+ *
7
+ * The smallest production-shape provider in the `at-*` family. Reads a
8
+ * 32-byte AES-256-GCM key from an environment variable (base64-encoded)
9
+ * and uses it to seal / unseal the hub-generated random passphrase.
10
+ *
11
+ * ## When to use
12
+ *
13
+ * - Single-node SaaS where the env var comes from Kubernetes Secrets,
14
+ * Heroku env, Doppler, AWS Secrets Manager (mounted at boot), systemd
15
+ * service env, etc. — anywhere your platform already manages secrets.
16
+ * - Local dev / CI where you want persistence across process restarts
17
+ * (which `MemorySealingKeyProvider` from `@noy-db/hub` can't do).
18
+ * - Prototypes where setting up AWS KMS / GCP KMS isn't worth the effort.
19
+ *
20
+ * ## When NOT to use
21
+ *
22
+ * - Laptops / shared dev machines where other users have shell access.
23
+ * They can `echo $NOYDB_SEALING_KEY` and exfiltrate the key. Use
24
+ * `@noy-db/at-macos-keychain` / `@noy-db/at-wincred` /
25
+ * `@noy-db/at-libsecret` for desktop apps.
26
+ * - Compliance regimes requiring auditable key access (FedRAMP, HIPAA
27
+ * with managed-encryption requirements). Use `@noy-db/at-aws-kms` for
28
+ * KMS-backed key access logs.
29
+ *
30
+ * ## Setup
31
+ *
32
+ * ```bash
33
+ * # 1. Generate a 256-bit key once. Store in your platform's secret manager.
34
+ * export NOYDB_SEALING_KEY=$(openssl rand -base64 32)
35
+ * ```
36
+ *
37
+ * ```ts
38
+ * // 2. In your app:
39
+ * import { createNoydb } from '@noy-db/hub'
40
+ * import { envSealingProvider } from '@noy-db/at-env'
41
+ *
42
+ * const db = await createNoydb({
43
+ * store,
44
+ * user: 'alice',
45
+ * passphraseMode: 'managed',
46
+ * sealingKey: envSealingProvider(), // reads NOYDB_SEALING_KEY by default
47
+ * })
48
+ * ```
49
+ *
50
+ * @packageDocumentation
51
+ */
52
+
53
+ /** Options for {@link envSealingProvider}. */
54
+ interface EnvSealingProviderOptions {
55
+ /**
56
+ * Environment variable name holding the base64-encoded 32-byte AES-256
57
+ * key. Default `'NOYDB_SEALING_KEY'`.
58
+ */
59
+ readonly envVar?: string;
60
+ }
61
+ /**
62
+ * Build a {@link SealingKeyProvider} backed by an environment variable.
63
+ *
64
+ * The env var must contain a base64-encoded 32-byte (256-bit) key. The
65
+ * provider validates the value at construction time and caches the
66
+ * imported `CryptoKey` for the lifetime of the instance.
67
+ *
68
+ * @throws Error when the env var is unset, not base64, or not 32 bytes.
69
+ */
70
+ declare function envSealingProvider(opts?: EnvSealingProviderOptions): SealingKeyProvider;
71
+
72
+ export { type EnvSealingProviderOptions, envSealingProvider };
package/dist/index.js ADDED
@@ -0,0 +1,81 @@
1
+ // src/index.ts
2
+ function envSealingProvider(opts = {}) {
3
+ const envVar = opts.envVar ?? "NOYDB_SEALING_KEY";
4
+ const raw = process.env[envVar];
5
+ if (!raw) {
6
+ throw new Error(
7
+ `@noy-db/at-env: env var "${envVar}" is not set. Generate a key via \`openssl rand -base64 32\` and export it before starting the process.`
8
+ );
9
+ }
10
+ let keyBytes;
11
+ try {
12
+ keyBytes = base64ToBytes(raw);
13
+ } catch (err) {
14
+ throw new Error(
15
+ `@noy-db/at-env: env var "${envVar}" is not valid base64 \u2014 ` + (err instanceof Error ? err.message : String(err))
16
+ );
17
+ }
18
+ if (keyBytes.length !== 32) {
19
+ throw new Error(
20
+ `@noy-db/at-env: env var "${envVar}" decodes to ${keyBytes.length} bytes; expected 32 bytes (256-bit AES key). Regenerate via \`openssl rand -base64 32\`.`
21
+ );
22
+ }
23
+ let cachedKey = null;
24
+ const getKey = () => {
25
+ if (!cachedKey) {
26
+ cachedKey = globalThis.crypto.subtle.importKey(
27
+ "raw",
28
+ keyBytes,
29
+ "AES-GCM",
30
+ false,
31
+ ["encrypt", "decrypt"]
32
+ );
33
+ }
34
+ return cachedKey;
35
+ };
36
+ return {
37
+ id: `env:${envVar}`,
38
+ async seal(passphrase) {
39
+ const key = await getKey();
40
+ const iv = globalThis.crypto.getRandomValues(new Uint8Array(12));
41
+ const ciphertext = await globalThis.crypto.subtle.encrypt(
42
+ { name: "AES-GCM", iv },
43
+ key,
44
+ passphrase
45
+ );
46
+ const out = new Uint8Array(12 + ciphertext.byteLength);
47
+ out.set(iv, 0);
48
+ out.set(new Uint8Array(ciphertext), 12);
49
+ return out;
50
+ },
51
+ async unseal(sealed) {
52
+ if (sealed.length < 12 + 16) {
53
+ throw new Error(
54
+ `@noy-db/at-env: sealed bytes too short (${sealed.length} < 28). Input is not a valid at-env-sealed envelope.`
55
+ );
56
+ }
57
+ const iv = sealed.subarray(0, 12);
58
+ const body = sealed.subarray(12);
59
+ const key = await getKey();
60
+ const plaintext = await globalThis.crypto.subtle.decrypt(
61
+ { name: "AES-GCM", iv },
62
+ key,
63
+ body
64
+ );
65
+ return new Uint8Array(plaintext);
66
+ }
67
+ };
68
+ }
69
+ function base64ToBytes(b64) {
70
+ if (!/^[A-Za-z0-9+/]+={0,2}$/.test(b64.trim())) {
71
+ throw new Error("input contains characters outside the base64 alphabet");
72
+ }
73
+ const binary = atob(b64);
74
+ const out = new Uint8Array(binary.length);
75
+ for (let i = 0; i < binary.length; i++) out[i] = binary.charCodeAt(i);
76
+ return out;
77
+ }
78
+ export {
79
+ envSealingProvider
80
+ };
81
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * **@noy-db/at-env** — env-var sealing key provider for noy-db\n * managed-passphrase mode (#14).\n *\n * The smallest production-shape provider in the `at-*` family. Reads a\n * 32-byte AES-256-GCM key from an environment variable (base64-encoded)\n * and uses it to seal / unseal the hub-generated random passphrase.\n *\n * ## When to use\n *\n * - Single-node SaaS where the env var comes from Kubernetes Secrets,\n * Heroku env, Doppler, AWS Secrets Manager (mounted at boot), systemd\n * service env, etc. — anywhere your platform already manages secrets.\n * - Local dev / CI where you want persistence across process restarts\n * (which `MemorySealingKeyProvider` from `@noy-db/hub` can't do).\n * - Prototypes where setting up AWS KMS / GCP KMS isn't worth the effort.\n *\n * ## When NOT to use\n *\n * - Laptops / shared dev machines where other users have shell access.\n * They can `echo $NOYDB_SEALING_KEY` and exfiltrate the key. Use\n * `@noy-db/at-macos-keychain` / `@noy-db/at-wincred` /\n * `@noy-db/at-libsecret` for desktop apps.\n * - Compliance regimes requiring auditable key access (FedRAMP, HIPAA\n * with managed-encryption requirements). Use `@noy-db/at-aws-kms` for\n * KMS-backed key access logs.\n *\n * ## Setup\n *\n * ```bash\n * # 1. Generate a 256-bit key once. Store in your platform's secret manager.\n * export NOYDB_SEALING_KEY=$(openssl rand -base64 32)\n * ```\n *\n * ```ts\n * // 2. In your app:\n * import { createNoydb } from '@noy-db/hub'\n * import { envSealingProvider } from '@noy-db/at-env'\n *\n * const db = await createNoydb({\n * store,\n * user: 'alice',\n * passphraseMode: 'managed',\n * sealingKey: envSealingProvider(), // reads NOYDB_SEALING_KEY by default\n * })\n * ```\n *\n * @packageDocumentation\n */\n\nimport type { SealingKeyProvider } from '@noy-db/hub'\n\n/** Options for {@link envSealingProvider}. */\nexport interface EnvSealingProviderOptions {\n /**\n * Environment variable name holding the base64-encoded 32-byte AES-256\n * key. Default `'NOYDB_SEALING_KEY'`.\n */\n readonly envVar?: string\n}\n\n/**\n * Build a {@link SealingKeyProvider} backed by an environment variable.\n *\n * The env var must contain a base64-encoded 32-byte (256-bit) key. The\n * provider validates the value at construction time and caches the\n * imported `CryptoKey` for the lifetime of the instance.\n *\n * @throws Error when the env var is unset, not base64, or not 32 bytes.\n */\nexport function envSealingProvider(\n opts: EnvSealingProviderOptions = {},\n): SealingKeyProvider {\n const envVar = opts.envVar ?? 'NOYDB_SEALING_KEY'\n const raw = process.env[envVar]\n if (!raw) {\n throw new Error(\n `@noy-db/at-env: env var \"${envVar}\" is not set. Generate a key via `\n + '`openssl rand -base64 32` and export it before starting the process.',\n )\n }\n\n let keyBytes: Uint8Array\n try {\n keyBytes = base64ToBytes(raw)\n } catch (err) {\n throw new Error(\n `@noy-db/at-env: env var \"${envVar}\" is not valid base64 — `\n + (err instanceof Error ? err.message : String(err)),\n )\n }\n\n if (keyBytes.length !== 32) {\n throw new Error(\n `@noy-db/at-env: env var \"${envVar}\" decodes to ${keyBytes.length} bytes; `\n + 'expected 32 bytes (256-bit AES key). Regenerate via `openssl rand -base64 32`.',\n )\n }\n\n // Lazy + cached import — the CryptoKey is created on first seal/unseal\n // and reused for the lifetime of the provider instance.\n let cachedKey: Promise<CryptoKey> | null = null\n const getKey = (): Promise<CryptoKey> => {\n if (!cachedKey) {\n cachedKey = globalThis.crypto.subtle.importKey(\n 'raw',\n keyBytes as unknown as BufferSource,\n 'AES-GCM',\n false,\n ['encrypt', 'decrypt'],\n )\n }\n return cachedKey\n }\n\n return {\n id: `env:${envVar}`,\n\n async seal(passphrase: Uint8Array): Promise<Uint8Array> {\n const key = await getKey()\n const iv = globalThis.crypto.getRandomValues(new Uint8Array(12))\n const ciphertext = await globalThis.crypto.subtle.encrypt(\n { name: 'AES-GCM', iv: iv as BufferSource },\n key,\n passphrase as unknown as BufferSource,\n )\n // Output format: [12-byte IV][ciphertext + 16-byte GCM tag]\n const out = new Uint8Array(12 + ciphertext.byteLength)\n out.set(iv, 0)\n out.set(new Uint8Array(ciphertext), 12)\n return out\n },\n\n async unseal(sealed: Uint8Array): Promise<Uint8Array> {\n // 12-byte IV + ≥ 16-byte GCM tag minimum.\n if (sealed.length < 12 + 16) {\n throw new Error(\n `@noy-db/at-env: sealed bytes too short (${sealed.length} < 28). `\n + 'Input is not a valid at-env-sealed envelope.',\n )\n }\n const iv = sealed.subarray(0, 12)\n const body = sealed.subarray(12)\n const key = await getKey()\n const plaintext = await globalThis.crypto.subtle.decrypt(\n { name: 'AES-GCM', iv: iv as BufferSource },\n key,\n body as unknown as BufferSource,\n )\n return new Uint8Array(plaintext)\n },\n }\n}\n\n// ─── base64 helpers (browser + node + cf-workers compatible) ──────────\n\nfunction base64ToBytes(b64: string): Uint8Array {\n // Reject obviously malformed strings before atob throws InvalidCharacterError,\n // so the wrapping error message is informative.\n if (!/^[A-Za-z0-9+/]+={0,2}$/.test(b64.trim())) {\n throw new Error('input contains characters outside the base64 alphabet')\n }\n const binary = atob(b64)\n const out = new Uint8Array(binary.length)\n for (let i = 0; i < binary.length; i++) out[i] = binary.charCodeAt(i)\n return out\n}\n"],"mappings":";AAsEO,SAAS,mBACd,OAAkC,CAAC,GACf;AACpB,QAAM,SAAS,KAAK,UAAU;AAC9B,QAAM,MAAM,QAAQ,IAAI,MAAM;AAC9B,MAAI,CAAC,KAAK;AACR,UAAM,IAAI;AAAA,MACR,4BAA4B,MAAM;AAAA,IAEpC;AAAA,EACF;AAEA,MAAI;AACJ,MAAI;AACF,eAAW,cAAc,GAAG;AAAA,EAC9B,SAAS,KAAK;AACZ,UAAM,IAAI;AAAA,MACR,4BAA4B,MAAM,mCAC/B,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAAA,IACpD;AAAA,EACF;AAEA,MAAI,SAAS,WAAW,IAAI;AAC1B,UAAM,IAAI;AAAA,MACR,4BAA4B,MAAM,gBAAgB,SAAS,MAAM;AAAA,IAEnE;AAAA,EACF;AAIA,MAAI,YAAuC;AAC3C,QAAM,SAAS,MAA0B;AACvC,QAAI,CAAC,WAAW;AACd,kBAAY,WAAW,OAAO,OAAO;AAAA,QACnC;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA,CAAC,WAAW,SAAS;AAAA,MACvB;AAAA,IACF;AACA,WAAO;AAAA,EACT;AAEA,SAAO;AAAA,IACL,IAAI,OAAO,MAAM;AAAA,IAEjB,MAAM,KAAK,YAA6C;AACtD,YAAM,MAAM,MAAM,OAAO;AACzB,YAAM,KAAK,WAAW,OAAO,gBAAgB,IAAI,WAAW,EAAE,CAAC;AAC/D,YAAM,aAAa,MAAM,WAAW,OAAO,OAAO;AAAA,QAChD,EAAE,MAAM,WAAW,GAAuB;AAAA,QAC1C;AAAA,QACA;AAAA,MACF;AAEA,YAAM,MAAM,IAAI,WAAW,KAAK,WAAW,UAAU;AACrD,UAAI,IAAI,IAAI,CAAC;AACb,UAAI,IAAI,IAAI,WAAW,UAAU,GAAG,EAAE;AACtC,aAAO;AAAA,IACT;AAAA,IAEA,MAAM,OAAO,QAAyC;AAEpD,UAAI,OAAO,SAAS,KAAK,IAAI;AAC3B,cAAM,IAAI;AAAA,UACR,2CAA2C,OAAO,MAAM;AAAA,QAE1D;AAAA,MACF;AACA,YAAM,KAAK,OAAO,SAAS,GAAG,EAAE;AAChC,YAAM,OAAO,OAAO,SAAS,EAAE;AAC/B,YAAM,MAAM,MAAM,OAAO;AACzB,YAAM,YAAY,MAAM,WAAW,OAAO,OAAO;AAAA,QAC/C,EAAE,MAAM,WAAW,GAAuB;AAAA,QAC1C;AAAA,QACA;AAAA,MACF;AACA,aAAO,IAAI,WAAW,SAAS;AAAA,IACjC;AAAA,EACF;AACF;AAIA,SAAS,cAAc,KAAyB;AAG9C,MAAI,CAAC,yBAAyB,KAAK,IAAI,KAAK,CAAC,GAAG;AAC9C,UAAM,IAAI,MAAM,uDAAuD;AAAA,EACzE;AACA,QAAM,SAAS,KAAK,GAAG;AACvB,QAAM,MAAM,IAAI,WAAW,OAAO,MAAM;AACxC,WAAS,IAAI,GAAG,IAAI,OAAO,QAAQ,IAAK,KAAI,CAAC,IAAI,OAAO,WAAW,CAAC;AACpE,SAAO;AACT;","names":[]}
package/package.json ADDED
@@ -0,0 +1,70 @@
1
+ {
2
+ "name": "@noy-db/at-env",
3
+ "version": "0.1.0-pre.16",
4
+ "description": "Env-var sealing key provider for noy-db managed-passphrase mode — AES-256-GCM under a key from process.env. Smallest production-shape provider; pair with Kubernetes Secrets / Heroku env / AWS Secrets Manager.",
5
+ "license": "MIT",
6
+ "author": "vLannaAi <vicio@lanna.ai>",
7
+ "homepage": "https://github.com/vLannaAi/noy-db/tree/main/packages/at-env#readme",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/vLannaAi/noy-db.git",
11
+ "directory": "packages/at-env"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/vLannaAi/noy-db/issues"
15
+ },
16
+ "type": "module",
17
+ "sideEffects": false,
18
+ "exports": {
19
+ ".": {
20
+ "import": {
21
+ "types": "./dist/index.d.ts",
22
+ "default": "./dist/index.js"
23
+ },
24
+ "require": {
25
+ "types": "./dist/index.d.cts",
26
+ "default": "./dist/index.cjs"
27
+ }
28
+ }
29
+ },
30
+ "main": "./dist/index.cjs",
31
+ "module": "./dist/index.js",
32
+ "types": "./dist/index.d.ts",
33
+ "files": [
34
+ "dist",
35
+ "README.md",
36
+ "LICENSE"
37
+ ],
38
+ "engines": {
39
+ "node": ">=18.0.0"
40
+ },
41
+ "peerDependencies": {
42
+ "@noy-db/hub": "0.1.0-pre.16"
43
+ },
44
+ "devDependencies": {
45
+ "@types/node": "^22.0.0",
46
+ "@noy-db/hub": "0.1.0-pre.16",
47
+ "@noy-db/to-memory": "0.1.0-pre.16"
48
+ },
49
+ "keywords": [
50
+ "noy-db",
51
+ "at-env",
52
+ "sealing-key-provider",
53
+ "managed-passphrase",
54
+ "env",
55
+ "rubber-hose-resistant",
56
+ "aes-256-gcm",
57
+ "encryption",
58
+ "zero-knowledge"
59
+ ],
60
+ "publishConfig": {
61
+ "access": "public",
62
+ "tag": "latest"
63
+ },
64
+ "scripts": {
65
+ "build": "tsup",
66
+ "test": "vitest run",
67
+ "lint": "eslint src/",
68
+ "typecheck": "tsc --noEmit"
69
+ }
70
+ }