@lacspace/crypto 1.0.2 → 1.1.0

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/README.md CHANGED
@@ -93,6 +93,27 @@ randomBytes(16); // CSPRNG bytes
93
93
  | [`@lacspace/headers`](https://www.npmjs.com/package/@lacspace/headers) | Secure headers / CSP |
94
94
  | [`@lacspace/redact`](https://www.npmjs.com/package/@lacspace/redact) | Log redaction |
95
95
 
96
+ ## New in 1.1 — AAD, HKDF & key rotation
97
+
98
+ ```ts
99
+ import { encrypt, decrypt, hkdf, Keyring } from "@lacspace/crypto";
100
+
101
+ // Bind ciphertext to a context so it can't be relocated to another row
102
+ const blob = await encrypt(secret, key, { aad: `user:${id}` });
103
+ await decrypt(blob, key, { aad: `user:${id}` }); // must match
104
+
105
+ // Derive many purpose-bound sub-keys from one master key
106
+ const encKey = await hkdf(master, { info: "field-encryption", length: 32 });
107
+
108
+ // Zero-downtime key rotation — new writes use the primary, old blobs still decrypt
109
+ const ring = new Keyring([{ id: "2025", key: oldKey }, { id: "2026", key: newKey }]);
110
+ const fresh = await ring.encrypt("secret"); // v2:2026:…
111
+ const text = await ring.decrypt(oldBlob); // finds the key by id
112
+ const migrated = await ring.reEncrypt(oldBlob); // re-key to primary
113
+ ```
114
+
115
+ Also `decryptBytes()` for binary-safe payloads (files, protobufs).
116
+
96
117
  ## Licensing
97
118
 
98
119
  This package is **free** under the **[Lacspace Free Licence](https://lacspace.com/licenses/lacspace-free-1.0)** — MIT-equivalent freedoms. Use it in personal and commercial projects at no cost; just keep the notice.
package/dist/index.cjs CHANGED
@@ -110,28 +110,35 @@ async function importAesKey(key) {
110
110
  "decrypt"
111
111
  ]);
112
112
  }
113
- async function encrypt(plaintext, key) {
113
+ function gcmParams(iv, aad) {
114
+ const p = { name: "AES-GCM", iv };
115
+ if (aad !== void 0) p.additionalData = toBytes(aad);
116
+ return p;
117
+ }
118
+ async function encrypt(plaintext, key, opts = {}) {
114
119
  const cryptoKey = await importAesKey(key);
115
120
  const iv = randomBytes(12);
116
121
  const ct = await getCrypto().subtle.encrypt(
117
- { name: "AES-GCM", iv },
122
+ gcmParams(iv, opts.aad),
118
123
  cryptoKey,
119
124
  toBytes(plaintext)
120
125
  );
121
126
  return `${AES_PREFIX}:${toBase64url(iv)}:${toBase64url(new Uint8Array(ct))}`;
122
127
  }
123
- async function decrypt(payload, key) {
128
+ async function decryptToBytes(payload, key, opts) {
124
129
  const parts = payload.split(":");
125
130
  if (parts.length !== 3 || parts[0] !== AES_PREFIX) throw new Error("invalid ciphertext format");
126
131
  const cryptoKey = await importAesKey(key);
127
132
  const iv = fromBase64url(parts[1]);
128
133
  const ct = fromBase64url(parts[2]);
129
- const pt = await getCrypto().subtle.decrypt(
130
- { name: "AES-GCM", iv },
131
- cryptoKey,
132
- ct
133
- );
134
- return dec.decode(pt);
134
+ const pt = await getCrypto().subtle.decrypt(gcmParams(iv, opts.aad), cryptoKey, ct);
135
+ return new Uint8Array(pt);
136
+ }
137
+ async function decrypt(payload, key, opts = {}) {
138
+ return dec.decode(await decryptToBytes(payload, key, opts));
139
+ }
140
+ async function decryptBytes(payload, key, opts = {}) {
141
+ return decryptToBytes(payload, key, opts);
135
142
  }
136
143
  async function encryptWithPassword(plaintext, password, opts = {}) {
137
144
  const iterations = opts.iterations ?? DEFAULT_PW_ITERATIONS;
@@ -149,9 +156,73 @@ async function decryptWithPassword(payload, password) {
149
156
  const key = await deriveBits(password, salt, { iterations, length: 32 });
150
157
  return decrypt(`${AES_PREFIX}:${parts[3]}:${parts[4]}`, key);
151
158
  }
159
+ async function hkdf(keyMaterial, opts = {}) {
160
+ const c = getCrypto();
161
+ const baseKey = await c.subtle.importKey("raw", toBytes(keyMaterial), "HKDF", false, [
162
+ "deriveBits"
163
+ ]);
164
+ const bits = await c.subtle.deriveBits(
165
+ {
166
+ name: "HKDF",
167
+ hash: opts.hash ?? "SHA-256",
168
+ salt: opts.salt ?? new Uint8Array(0),
169
+ info: toBytes(opts.info ?? "")
170
+ },
171
+ baseKey,
172
+ (opts.length ?? 32) * 8
173
+ );
174
+ return new Uint8Array(bits);
175
+ }
176
+ var KEYRING_PREFIX = "v2";
177
+ var Keyring = class {
178
+ constructor(entries, primaryId) {
179
+ this.keys = /* @__PURE__ */ new Map();
180
+ if (!entries.length) throw new Error("Keyring needs at least one key");
181
+ for (const e of entries) {
182
+ if (e.id.includes(":")) throw new Error(`key id "${e.id}" must not contain ":"`);
183
+ this.keys.set(e.id, e.key);
184
+ }
185
+ this.primaryId = primaryId ?? entries[entries.length - 1].id;
186
+ if (!this.keys.has(this.primaryId)) throw new Error(`primary key "${this.primaryId}" is not in the keyring`);
187
+ }
188
+ /** Encrypt under the primary key. */
189
+ async encrypt(plaintext, opts = {}) {
190
+ const inner = await encrypt(plaintext, this.keys.get(this.primaryId), opts);
191
+ const [, iv, ct] = inner.split(":");
192
+ return `${KEYRING_PREFIX}:${this.primaryId}:${iv}:${ct}`;
193
+ }
194
+ resolve(payload) {
195
+ const parts = payload.split(":");
196
+ if (parts[0] === KEYRING_PREFIX) {
197
+ const kid = parts[1];
198
+ const key = this.keys.get(kid);
199
+ if (!key) throw new Error(`unknown key id "${kid}"`);
200
+ return { key, inner: `${AES_PREFIX}:${parts[2]}:${parts[3]}` };
201
+ }
202
+ if (parts[0] === AES_PREFIX) return { key: this.keys.get(this.primaryId), inner: payload };
203
+ throw new Error("invalid ciphertext format");
204
+ }
205
+ async decrypt(payload, opts = {}) {
206
+ const { key, inner } = this.resolve(payload);
207
+ return decrypt(inner, key, opts);
208
+ }
209
+ async decryptBytes(payload, opts = {}) {
210
+ const { key, inner } = this.resolve(payload);
211
+ return decryptBytes(inner, key, opts);
212
+ }
213
+ /** Re-encrypt under the primary key if it isn't already. Returns the (possibly new) payload. */
214
+ async reEncrypt(payload, opts = {}) {
215
+ const parts = payload.split(":");
216
+ if (parts[0] === KEYRING_PREFIX && parts[1] === this.primaryId) return payload;
217
+ const pt = await this.decryptBytes(payload, opts);
218
+ return this.encrypt(pt, opts);
219
+ }
220
+ };
152
221
 
222
+ exports.Keyring = Keyring;
153
223
  exports.constantTimeEqual = constantTimeEqual;
154
224
  exports.decrypt = decrypt;
225
+ exports.decryptBytes = decryptBytes;
155
226
  exports.decryptWithPassword = decryptWithPassword;
156
227
  exports.deriveBits = deriveBits;
157
228
  exports.digest = digest;
@@ -160,6 +231,7 @@ exports.encryptWithPassword = encryptWithPassword;
160
231
  exports.fromBase64url = fromBase64url;
161
232
  exports.fromHex = fromHex;
162
233
  exports.generateKey = generateKey;
234
+ exports.hkdf = hkdf;
163
235
  exports.hmac = hmac;
164
236
  exports.hmacVerify = hmacVerify;
165
237
  exports.randomBytes = randomBytes;
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * @lacspace/crypto\n * Safe, boring cryptography — authenticated AES-256-GCM, key derivation, hashing.\n *\n * A thin, correct layer over the Web Crypto API (no hand-rolled crypto), so the\n * same code runs on Node 18+, edge runtimes, browsers and React Native. Encrypt\n * database fields, S3 object payloads, cookies and tokens with confidence.\n *\n * Zero dependencies · isomorphic · fully typed.\n */\n\nfunction getCrypto(): Crypto {\n const c = (globalThis as { crypto?: Crypto }).crypto;\n if (!c || !c.subtle) {\n throw new Error(\"Web Crypto unavailable — @lacspace/crypto needs Node 18+, an edge runtime or a browser\");\n }\n return c;\n}\n\n/* ------------------------------ encoding ------------------------------ */\n\nexport function randomBytes(length: number): Uint8Array {\n const buf = new Uint8Array(length);\n getCrypto().getRandomValues(buf);\n return buf;\n}\n\nexport function toHex(bytes: Uint8Array): string {\n let out = \"\";\n for (const b of bytes) out += b.toString(16).padStart(2, \"0\");\n return out;\n}\n\nexport function fromHex(hex: string): Uint8Array {\n const clean = hex.length % 2 ? \"0\" + hex : hex;\n const out = new Uint8Array(clean.length / 2);\n for (let i = 0; i < out.length; i++) out[i] = parseInt(clean.slice(i * 2, i * 2 + 2), 16);\n return out;\n}\n\nexport function toBase64url(bytes: Uint8Array): string {\n let bin = \"\";\n for (const b of bytes) bin += String.fromCharCode(b);\n const b64 = typeof btoa !== \"undefined\" ? btoa(bin) : Buffer.from(bytes).toString(\"base64\");\n return b64.replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=+$/, \"\");\n}\n\nexport function fromBase64url(s: string): Uint8Array {\n const b64 = s.replace(/-/g, \"+\").replace(/_/g, \"/\") + \"===\".slice((s.length + 3) % 4);\n if (typeof atob !== \"undefined\") {\n const bin = atob(b64);\n const out = new Uint8Array(bin.length);\n for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);\n return out;\n }\n return new Uint8Array(Buffer.from(b64, \"base64\"));\n}\n\nconst enc = new TextEncoder();\nconst dec = new TextDecoder();\n\nfunction toBytes(data: string | Uint8Array): Uint8Array {\n return typeof data === \"string\" ? enc.encode(data) : data;\n}\n\n/** Constant-time comparison of two byte arrays or strings. */\nexport function constantTimeEqual(a: Uint8Array | string, b: Uint8Array | string): boolean {\n const x = toBytes(a);\n const y = toBytes(b);\n if (x.length !== y.length) return false;\n let diff = 0;\n for (let i = 0; i < x.length; i++) diff |= x[i]! ^ y[i]!;\n return diff === 0;\n}\n\n/* ------------------------------ hashing ------------------------------ */\n\nexport type HashAlgorithm = \"SHA-256\" | \"SHA-384\" | \"SHA-512\";\n\nexport async function digest(\n data: string | Uint8Array,\n algorithm: HashAlgorithm = \"SHA-256\",\n): Promise<Uint8Array> {\n const buf = await getCrypto().subtle.digest(algorithm, toBytes(data) as unknown as BufferSource);\n return new Uint8Array(buf);\n}\n\n/** SHA-256 hex digest. */\nexport async function sha256(data: string | Uint8Array): Promise<string> {\n return toHex(await digest(data, \"SHA-256\"));\n}\n\n/** HMAC signature (bytes). */\nexport async function hmac(\n key: string | Uint8Array,\n data: string | Uint8Array,\n algorithm: HashAlgorithm = \"SHA-256\",\n): Promise<Uint8Array> {\n const c = getCrypto();\n const cryptoKey = await c.subtle.importKey(\n \"raw\",\n toBytes(key) as unknown as BufferSource,\n { name: \"HMAC\", hash: algorithm },\n false,\n [\"sign\"],\n );\n const sig = await c.subtle.sign(\"HMAC\", cryptoKey, toBytes(data) as unknown as BufferSource);\n return new Uint8Array(sig);\n}\n\n/** Verify an HMAC in constant time. */\nexport async function hmacVerify(\n key: string | Uint8Array,\n data: string | Uint8Array,\n signature: Uint8Array,\n algorithm: HashAlgorithm = \"SHA-256\",\n): Promise<boolean> {\n return constantTimeEqual(await hmac(key, data, algorithm), signature);\n}\n\n/* ------------------------------ key derivation ------------------------------ */\n\nexport interface DeriveOptions {\n iterations?: number;\n hash?: HashAlgorithm;\n /** Derived key length in bytes. Default 32. */\n length?: number;\n}\n\n/** Derive raw key bytes from a password with PBKDF2. */\nexport async function deriveBits(\n password: string | Uint8Array,\n salt: Uint8Array,\n opts: DeriveOptions = {},\n): Promise<Uint8Array> {\n const c = getCrypto();\n const baseKey = await c.subtle.importKey(\n \"raw\",\n toBytes(password) as unknown as BufferSource,\n \"PBKDF2\",\n false,\n [\"deriveBits\"],\n );\n const bits = await c.subtle.deriveBits(\n {\n name: \"PBKDF2\",\n salt: salt as unknown as BufferSource,\n iterations: opts.iterations ?? 210000,\n hash: opts.hash ?? \"SHA-256\",\n },\n baseKey,\n (opts.length ?? 32) * 8,\n );\n return new Uint8Array(bits);\n}\n\n/* ------------------------------ AES-256-GCM ------------------------------ */\n\nconst AES_PREFIX = \"v1\";\nconst AES_PW_PREFIX = \"v1p\";\nconst DEFAULT_PW_ITERATIONS = 210000;\n\n/** Generate a random 256-bit AES key as a base64url string. */\nexport function generateKey(): string {\n return toBase64url(randomBytes(32));\n}\n\nasync function importAesKey(key: string | Uint8Array): Promise<CryptoKey> {\n const raw = typeof key === \"string\" ? fromBase64url(key) : key;\n if (raw.length !== 32) throw new Error(\"AES key must be 32 bytes (256-bit)\");\n return getCrypto().subtle.importKey(\"raw\", raw as unknown as BufferSource, { name: \"AES-GCM\" }, false, [\n \"encrypt\",\n \"decrypt\",\n ]);\n}\n\n/**\n * Encrypt with AES-256-GCM using a 32-byte key (base64url or bytes).\n * Returns a compact self-describing string: `v1:<iv>:<ciphertext>`.\n */\nexport async function encrypt(plaintext: string | Uint8Array, key: string | Uint8Array): Promise<string> {\n const cryptoKey = await importAesKey(key);\n const iv = randomBytes(12);\n const ct = await getCrypto().subtle.encrypt(\n { name: \"AES-GCM\", iv: iv as unknown as BufferSource },\n cryptoKey,\n toBytes(plaintext) as unknown as BufferSource,\n );\n return `${AES_PREFIX}:${toBase64url(iv)}:${toBase64url(new Uint8Array(ct))}`;\n}\n\n/** Decrypt a string produced by {@link encrypt}. Returns the UTF-8 plaintext. */\nexport async function decrypt(payload: string, key: string | Uint8Array): Promise<string> {\n const parts = payload.split(\":\");\n if (parts.length !== 3 || parts[0] !== AES_PREFIX) throw new Error(\"invalid ciphertext format\");\n const cryptoKey = await importAesKey(key);\n const iv = fromBase64url(parts[1]!);\n const ct = fromBase64url(parts[2]!);\n const pt = await getCrypto().subtle.decrypt(\n { name: \"AES-GCM\", iv: iv as unknown as BufferSource },\n cryptoKey,\n ct as unknown as BufferSource,\n );\n return dec.decode(pt);\n}\n\n/**\n * Encrypt with a passphrase (PBKDF2-derived key + AES-256-GCM).\n * Returns `v1p:<iterations>:<salt>:<iv>:<ciphertext>` — self-contained.\n */\nexport async function encryptWithPassword(\n plaintext: string | Uint8Array,\n password: string,\n opts: { iterations?: number } = {},\n): Promise<string> {\n const iterations = opts.iterations ?? DEFAULT_PW_ITERATIONS;\n const salt = randomBytes(16);\n const key = await deriveBits(password, salt, { iterations, length: 32 });\n const inner = await encrypt(plaintext, key);\n const [, iv, ct] = inner.split(\":\");\n return `${AES_PW_PREFIX}:${iterations}:${toBase64url(salt)}:${iv}:${ct}`;\n}\n\n/** Decrypt a string produced by {@link encryptWithPassword}. */\nexport async function decryptWithPassword(payload: string, password: string): Promise<string> {\n const parts = payload.split(\":\");\n if (parts.length !== 5 || parts[0] !== AES_PW_PREFIX) throw new Error(\"invalid ciphertext format\");\n const iterations = parseInt(parts[1]!, 10);\n const salt = fromBase64url(parts[2]!);\n const key = await deriveBits(password, salt, { iterations, length: 32 });\n return decrypt(`${AES_PREFIX}:${parts[3]}:${parts[4]}`, key);\n}\n"]}
1
+ {"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * @lacspace/crypto\n * Safe, boring cryptography — authenticated AES-256-GCM, key derivation, hashing.\n *\n * A thin, correct layer over the Web Crypto API (no hand-rolled crypto), so the\n * same code runs on Node 18+, edge runtimes, browsers and React Native. Encrypt\n * database fields, S3 object payloads, cookies and tokens with confidence.\n *\n * Zero dependencies · isomorphic · fully typed.\n */\n\nfunction getCrypto(): Crypto {\n const c = (globalThis as { crypto?: Crypto }).crypto;\n if (!c || !c.subtle) {\n throw new Error(\"Web Crypto unavailable — @lacspace/crypto needs Node 18+, an edge runtime or a browser\");\n }\n return c;\n}\n\n/* ------------------------------ encoding ------------------------------ */\n\nexport function randomBytes(length: number): Uint8Array {\n const buf = new Uint8Array(length);\n getCrypto().getRandomValues(buf);\n return buf;\n}\n\nexport function toHex(bytes: Uint8Array): string {\n let out = \"\";\n for (const b of bytes) out += b.toString(16).padStart(2, \"0\");\n return out;\n}\n\nexport function fromHex(hex: string): Uint8Array {\n const clean = hex.length % 2 ? \"0\" + hex : hex;\n const out = new Uint8Array(clean.length / 2);\n for (let i = 0; i < out.length; i++) out[i] = parseInt(clean.slice(i * 2, i * 2 + 2), 16);\n return out;\n}\n\nexport function toBase64url(bytes: Uint8Array): string {\n let bin = \"\";\n for (const b of bytes) bin += String.fromCharCode(b);\n const b64 = typeof btoa !== \"undefined\" ? btoa(bin) : Buffer.from(bytes).toString(\"base64\");\n return b64.replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=+$/, \"\");\n}\n\nexport function fromBase64url(s: string): Uint8Array {\n const b64 = s.replace(/-/g, \"+\").replace(/_/g, \"/\") + \"===\".slice((s.length + 3) % 4);\n if (typeof atob !== \"undefined\") {\n const bin = atob(b64);\n const out = new Uint8Array(bin.length);\n for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);\n return out;\n }\n return new Uint8Array(Buffer.from(b64, \"base64\"));\n}\n\nconst enc = new TextEncoder();\nconst dec = new TextDecoder();\n\nfunction toBytes(data: string | Uint8Array): Uint8Array {\n return typeof data === \"string\" ? enc.encode(data) : data;\n}\n\n/** Constant-time comparison of two byte arrays or strings. */\nexport function constantTimeEqual(a: Uint8Array | string, b: Uint8Array | string): boolean {\n const x = toBytes(a);\n const y = toBytes(b);\n if (x.length !== y.length) return false;\n let diff = 0;\n for (let i = 0; i < x.length; i++) diff |= x[i]! ^ y[i]!;\n return diff === 0;\n}\n\n/* ------------------------------ hashing ------------------------------ */\n\nexport type HashAlgorithm = \"SHA-256\" | \"SHA-384\" | \"SHA-512\";\n\nexport async function digest(\n data: string | Uint8Array,\n algorithm: HashAlgorithm = \"SHA-256\",\n): Promise<Uint8Array> {\n const buf = await getCrypto().subtle.digest(algorithm, toBytes(data) as unknown as BufferSource);\n return new Uint8Array(buf);\n}\n\n/** SHA-256 hex digest. */\nexport async function sha256(data: string | Uint8Array): Promise<string> {\n return toHex(await digest(data, \"SHA-256\"));\n}\n\n/** HMAC signature (bytes). */\nexport async function hmac(\n key: string | Uint8Array,\n data: string | Uint8Array,\n algorithm: HashAlgorithm = \"SHA-256\",\n): Promise<Uint8Array> {\n const c = getCrypto();\n const cryptoKey = await c.subtle.importKey(\n \"raw\",\n toBytes(key) as unknown as BufferSource,\n { name: \"HMAC\", hash: algorithm },\n false,\n [\"sign\"],\n );\n const sig = await c.subtle.sign(\"HMAC\", cryptoKey, toBytes(data) as unknown as BufferSource);\n return new Uint8Array(sig);\n}\n\n/** Verify an HMAC in constant time. */\nexport async function hmacVerify(\n key: string | Uint8Array,\n data: string | Uint8Array,\n signature: Uint8Array,\n algorithm: HashAlgorithm = \"SHA-256\",\n): Promise<boolean> {\n return constantTimeEqual(await hmac(key, data, algorithm), signature);\n}\n\n/* ------------------------------ key derivation ------------------------------ */\n\nexport interface DeriveOptions {\n iterations?: number;\n hash?: HashAlgorithm;\n /** Derived key length in bytes. Default 32. */\n length?: number;\n}\n\n/** Derive raw key bytes from a password with PBKDF2. */\nexport async function deriveBits(\n password: string | Uint8Array,\n salt: Uint8Array,\n opts: DeriveOptions = {},\n): Promise<Uint8Array> {\n const c = getCrypto();\n const baseKey = await c.subtle.importKey(\n \"raw\",\n toBytes(password) as unknown as BufferSource,\n \"PBKDF2\",\n false,\n [\"deriveBits\"],\n );\n const bits = await c.subtle.deriveBits(\n {\n name: \"PBKDF2\",\n salt: salt as unknown as BufferSource,\n iterations: opts.iterations ?? 210000,\n hash: opts.hash ?? \"SHA-256\",\n },\n baseKey,\n (opts.length ?? 32) * 8,\n );\n return new Uint8Array(bits);\n}\n\n/* ------------------------------ AES-256-GCM ------------------------------ */\n\nconst AES_PREFIX = \"v1\";\nconst AES_PW_PREFIX = \"v1p\";\nconst DEFAULT_PW_ITERATIONS = 210000;\n\n/** Generate a random 256-bit AES key as a base64url string. */\nexport function generateKey(): string {\n return toBase64url(randomBytes(32));\n}\n\nasync function importAesKey(key: string | Uint8Array): Promise<CryptoKey> {\n const raw = typeof key === \"string\" ? fromBase64url(key) : key;\n if (raw.length !== 32) throw new Error(\"AES key must be 32 bytes (256-bit)\");\n return getCrypto().subtle.importKey(\"raw\", raw as unknown as BufferSource, { name: \"AES-GCM\" }, false, [\n \"encrypt\",\n \"decrypt\",\n ]);\n}\n\nexport interface AesOptions {\n /** Additional Authenticated Data — bound to the ciphertext (must match on decrypt). */\n aad?: string | Uint8Array;\n}\n\nfunction gcmParams(iv: Uint8Array, aad?: string | Uint8Array): AesGcmParams {\n const p: AesGcmParams = { name: \"AES-GCM\", iv: iv as unknown as BufferSource };\n if (aad !== undefined) p.additionalData = toBytes(aad) as unknown as BufferSource;\n return p;\n}\n\n/**\n * Encrypt with AES-256-GCM using a 32-byte key (base64url or bytes).\n * Returns a compact self-describing string: `v1:<iv>:<ciphertext>`.\n * Pass `opts.aad` to bind the ciphertext to a context (row id, tenant…).\n */\nexport async function encrypt(\n plaintext: string | Uint8Array,\n key: string | Uint8Array,\n opts: AesOptions = {},\n): Promise<string> {\n const cryptoKey = await importAesKey(key);\n const iv = randomBytes(12);\n const ct = await getCrypto().subtle.encrypt(\n gcmParams(iv, opts.aad),\n cryptoKey,\n toBytes(plaintext) as unknown as BufferSource,\n );\n return `${AES_PREFIX}:${toBase64url(iv)}:${toBase64url(new Uint8Array(ct))}`;\n}\n\nasync function decryptToBytes(payload: string, key: string | Uint8Array, opts: AesOptions): Promise<Uint8Array> {\n const parts = payload.split(\":\");\n if (parts.length !== 3 || parts[0] !== AES_PREFIX) throw new Error(\"invalid ciphertext format\");\n const cryptoKey = await importAesKey(key);\n const iv = fromBase64url(parts[1]!);\n const ct = fromBase64url(parts[2]!);\n const pt = await getCrypto().subtle.decrypt(gcmParams(iv, opts.aad), cryptoKey, ct as unknown as BufferSource);\n return new Uint8Array(pt);\n}\n\n/** Decrypt a string produced by {@link encrypt}. Returns the UTF-8 plaintext. */\nexport async function decrypt(payload: string, key: string | Uint8Array, opts: AesOptions = {}): Promise<string> {\n return dec.decode(await decryptToBytes(payload, key, opts));\n}\n\n/** Decrypt to raw bytes — binary-safe (files, protobufs, images). */\nexport async function decryptBytes(\n payload: string,\n key: string | Uint8Array,\n opts: AesOptions = {},\n): Promise<Uint8Array> {\n return decryptToBytes(payload, key, opts);\n}\n\n/**\n * Encrypt with a passphrase (PBKDF2-derived key + AES-256-GCM).\n * Returns `v1p:<iterations>:<salt>:<iv>:<ciphertext>` — self-contained.\n */\nexport async function encryptWithPassword(\n plaintext: string | Uint8Array,\n password: string,\n opts: { iterations?: number } = {},\n): Promise<string> {\n const iterations = opts.iterations ?? DEFAULT_PW_ITERATIONS;\n const salt = randomBytes(16);\n const key = await deriveBits(password, salt, { iterations, length: 32 });\n const inner = await encrypt(plaintext, key);\n const [, iv, ct] = inner.split(\":\");\n return `${AES_PW_PREFIX}:${iterations}:${toBase64url(salt)}:${iv}:${ct}`;\n}\n\n/** Decrypt a string produced by {@link encryptWithPassword}. */\nexport async function decryptWithPassword(payload: string, password: string): Promise<string> {\n const parts = payload.split(\":\");\n if (parts.length !== 5 || parts[0] !== AES_PW_PREFIX) throw new Error(\"invalid ciphertext format\");\n const iterations = parseInt(parts[1]!, 10);\n const salt = fromBase64url(parts[2]!);\n const key = await deriveBits(password, salt, { iterations, length: 32 });\n return decrypt(`${AES_PREFIX}:${parts[3]}:${parts[4]}`, key);\n}\n\n/* ------------------------------ HKDF ------------------------------ */\n\nexport interface HkdfOptions {\n /** Optional salt (recommended). */\n salt?: Uint8Array;\n /** Context/label so the same master key yields different sub-keys per purpose. */\n info?: string | Uint8Array;\n /** Output length in bytes. Default 32. */\n length?: number;\n hash?: HashAlgorithm;\n}\n\n/**\n * HKDF: derive one or many purpose-bound sub-keys from a single master key.\n * @example const encKey = await hkdf(master, { info: \"field-encryption\", length: 32 });\n */\nexport async function hkdf(keyMaterial: string | Uint8Array, opts: HkdfOptions = {}): Promise<Uint8Array> {\n const c = getCrypto();\n const baseKey = await c.subtle.importKey(\"raw\", toBytes(keyMaterial) as unknown as BufferSource, \"HKDF\", false, [\n \"deriveBits\",\n ]);\n const bits = await c.subtle.deriveBits(\n {\n name: \"HKDF\",\n hash: opts.hash ?? \"SHA-256\",\n salt: (opts.salt ?? new Uint8Array(0)) as unknown as BufferSource,\n info: toBytes(opts.info ?? \"\") as unknown as BufferSource,\n },\n baseKey,\n (opts.length ?? 32) * 8,\n );\n return new Uint8Array(bits);\n}\n\n/* ------------------------------ key rotation ------------------------------ */\n\nconst KEYRING_PREFIX = \"v2\";\n\nexport interface KeyringEntry {\n /** Stable key id (embedded in ciphertext; must not contain \":\"). */\n id: string;\n /** 32-byte AES key (base64url string or bytes). */\n key: string | Uint8Array;\n}\n\n/**\n * A set of versioned AES keys for zero-downtime rotation. New data is encrypted\n * under the primary key; old ciphertext is decrypted by the key its `id` names.\n * Envelope: `v2:<keyId>:<iv>:<ciphertext>`.\n *\n * @example\n * const ring = new Keyring([{ id: \"2025\", key: oldKey }, { id: \"2026\", key: newKey }]);\n * const blob = await ring.encrypt(\"secret\"); // uses \"2026\" (primary = last)\n * const text = await ring.decrypt(oldBlob); // finds the right key by id\n * const fresh = await ring.reEncrypt(oldBlob); // migrate to the primary key\n */\nexport class Keyring {\n private readonly keys = new Map<string, string | Uint8Array>();\n readonly primaryId: string;\n\n constructor(entries: KeyringEntry[], primaryId?: string) {\n if (!entries.length) throw new Error(\"Keyring needs at least one key\");\n for (const e of entries) {\n if (e.id.includes(\":\")) throw new Error(`key id \"${e.id}\" must not contain \":\"`);\n this.keys.set(e.id, e.key);\n }\n this.primaryId = primaryId ?? entries[entries.length - 1]!.id;\n if (!this.keys.has(this.primaryId)) throw new Error(`primary key \"${this.primaryId}\" is not in the keyring`);\n }\n\n /** Encrypt under the primary key. */\n async encrypt(plaintext: string | Uint8Array, opts: AesOptions = {}): Promise<string> {\n const inner = await encrypt(plaintext, this.keys.get(this.primaryId)!, opts);\n const [, iv, ct] = inner.split(\":\");\n return `${KEYRING_PREFIX}:${this.primaryId}:${iv}:${ct}`;\n }\n\n private resolve(payload: string): { key: string | Uint8Array; inner: string } {\n const parts = payload.split(\":\");\n if (parts[0] === KEYRING_PREFIX) {\n const kid = parts[1]!;\n const key = this.keys.get(kid);\n if (!key) throw new Error(`unknown key id \"${kid}\"`);\n return { key, inner: `${AES_PREFIX}:${parts[2]}:${parts[3]}` };\n }\n if (parts[0] === AES_PREFIX) return { key: this.keys.get(this.primaryId)!, inner: payload }; // legacy v1\n throw new Error(\"invalid ciphertext format\");\n }\n\n async decrypt(payload: string, opts: AesOptions = {}): Promise<string> {\n const { key, inner } = this.resolve(payload);\n return decrypt(inner, key, opts);\n }\n\n async decryptBytes(payload: string, opts: AesOptions = {}): Promise<Uint8Array> {\n const { key, inner } = this.resolve(payload);\n return decryptBytes(inner, key, opts);\n }\n\n /** Re-encrypt under the primary key if it isn't already. Returns the (possibly new) payload. */\n async reEncrypt(payload: string, opts: AesOptions = {}): Promise<string> {\n const parts = payload.split(\":\");\n if (parts[0] === KEYRING_PREFIX && parts[1] === this.primaryId) return payload;\n const pt = await this.decryptBytes(payload, opts);\n return this.encrypt(pt, opts);\n }\n}\n"]}
package/dist/index.d.cts CHANGED
@@ -33,13 +33,20 @@ interface DeriveOptions {
33
33
  declare function deriveBits(password: string | Uint8Array, salt: Uint8Array, opts?: DeriveOptions): Promise<Uint8Array>;
34
34
  /** Generate a random 256-bit AES key as a base64url string. */
35
35
  declare function generateKey(): string;
36
+ interface AesOptions {
37
+ /** Additional Authenticated Data — bound to the ciphertext (must match on decrypt). */
38
+ aad?: string | Uint8Array;
39
+ }
36
40
  /**
37
41
  * Encrypt with AES-256-GCM using a 32-byte key (base64url or bytes).
38
42
  * Returns a compact self-describing string: `v1:<iv>:<ciphertext>`.
43
+ * Pass `opts.aad` to bind the ciphertext to a context (row id, tenant…).
39
44
  */
40
- declare function encrypt(plaintext: string | Uint8Array, key: string | Uint8Array): Promise<string>;
45
+ declare function encrypt(plaintext: string | Uint8Array, key: string | Uint8Array, opts?: AesOptions): Promise<string>;
41
46
  /** Decrypt a string produced by {@link encrypt}. Returns the UTF-8 plaintext. */
42
- declare function decrypt(payload: string, key: string | Uint8Array): Promise<string>;
47
+ declare function decrypt(payload: string, key: string | Uint8Array, opts?: AesOptions): Promise<string>;
48
+ /** Decrypt to raw bytes — binary-safe (files, protobufs, images). */
49
+ declare function decryptBytes(payload: string, key: string | Uint8Array, opts?: AesOptions): Promise<Uint8Array>;
43
50
  /**
44
51
  * Encrypt with a passphrase (PBKDF2-derived key + AES-256-GCM).
45
52
  * Returns `v1p:<iterations>:<salt>:<iv>:<ciphertext>` — self-contained.
@@ -49,5 +56,48 @@ declare function encryptWithPassword(plaintext: string | Uint8Array, password: s
49
56
  }): Promise<string>;
50
57
  /** Decrypt a string produced by {@link encryptWithPassword}. */
51
58
  declare function decryptWithPassword(payload: string, password: string): Promise<string>;
59
+ interface HkdfOptions {
60
+ /** Optional salt (recommended). */
61
+ salt?: Uint8Array;
62
+ /** Context/label so the same master key yields different sub-keys per purpose. */
63
+ info?: string | Uint8Array;
64
+ /** Output length in bytes. Default 32. */
65
+ length?: number;
66
+ hash?: HashAlgorithm;
67
+ }
68
+ /**
69
+ * HKDF: derive one or many purpose-bound sub-keys from a single master key.
70
+ * @example const encKey = await hkdf(master, { info: "field-encryption", length: 32 });
71
+ */
72
+ declare function hkdf(keyMaterial: string | Uint8Array, opts?: HkdfOptions): Promise<Uint8Array>;
73
+ interface KeyringEntry {
74
+ /** Stable key id (embedded in ciphertext; must not contain ":"). */
75
+ id: string;
76
+ /** 32-byte AES key (base64url string or bytes). */
77
+ key: string | Uint8Array;
78
+ }
79
+ /**
80
+ * A set of versioned AES keys for zero-downtime rotation. New data is encrypted
81
+ * under the primary key; old ciphertext is decrypted by the key its `id` names.
82
+ * Envelope: `v2:<keyId>:<iv>:<ciphertext>`.
83
+ *
84
+ * @example
85
+ * const ring = new Keyring([{ id: "2025", key: oldKey }, { id: "2026", key: newKey }]);
86
+ * const blob = await ring.encrypt("secret"); // uses "2026" (primary = last)
87
+ * const text = await ring.decrypt(oldBlob); // finds the right key by id
88
+ * const fresh = await ring.reEncrypt(oldBlob); // migrate to the primary key
89
+ */
90
+ declare class Keyring {
91
+ private readonly keys;
92
+ readonly primaryId: string;
93
+ constructor(entries: KeyringEntry[], primaryId?: string);
94
+ /** Encrypt under the primary key. */
95
+ encrypt(plaintext: string | Uint8Array, opts?: AesOptions): Promise<string>;
96
+ private resolve;
97
+ decrypt(payload: string, opts?: AesOptions): Promise<string>;
98
+ decryptBytes(payload: string, opts?: AesOptions): Promise<Uint8Array>;
99
+ /** Re-encrypt under the primary key if it isn't already. Returns the (possibly new) payload. */
100
+ reEncrypt(payload: string, opts?: AesOptions): Promise<string>;
101
+ }
52
102
 
53
- export { type DeriveOptions, type HashAlgorithm, constantTimeEqual, decrypt, decryptWithPassword, deriveBits, digest, encrypt, encryptWithPassword, fromBase64url, fromHex, generateKey, hmac, hmacVerify, randomBytes, sha256, toBase64url, toHex };
103
+ export { type AesOptions, type DeriveOptions, type HashAlgorithm, type HkdfOptions, Keyring, type KeyringEntry, constantTimeEqual, decrypt, decryptBytes, decryptWithPassword, deriveBits, digest, encrypt, encryptWithPassword, fromBase64url, fromHex, generateKey, hkdf, hmac, hmacVerify, randomBytes, sha256, toBase64url, toHex };
package/dist/index.d.ts CHANGED
@@ -33,13 +33,20 @@ interface DeriveOptions {
33
33
  declare function deriveBits(password: string | Uint8Array, salt: Uint8Array, opts?: DeriveOptions): Promise<Uint8Array>;
34
34
  /** Generate a random 256-bit AES key as a base64url string. */
35
35
  declare function generateKey(): string;
36
+ interface AesOptions {
37
+ /** Additional Authenticated Data — bound to the ciphertext (must match on decrypt). */
38
+ aad?: string | Uint8Array;
39
+ }
36
40
  /**
37
41
  * Encrypt with AES-256-GCM using a 32-byte key (base64url or bytes).
38
42
  * Returns a compact self-describing string: `v1:<iv>:<ciphertext>`.
43
+ * Pass `opts.aad` to bind the ciphertext to a context (row id, tenant…).
39
44
  */
40
- declare function encrypt(plaintext: string | Uint8Array, key: string | Uint8Array): Promise<string>;
45
+ declare function encrypt(plaintext: string | Uint8Array, key: string | Uint8Array, opts?: AesOptions): Promise<string>;
41
46
  /** Decrypt a string produced by {@link encrypt}. Returns the UTF-8 plaintext. */
42
- declare function decrypt(payload: string, key: string | Uint8Array): Promise<string>;
47
+ declare function decrypt(payload: string, key: string | Uint8Array, opts?: AesOptions): Promise<string>;
48
+ /** Decrypt to raw bytes — binary-safe (files, protobufs, images). */
49
+ declare function decryptBytes(payload: string, key: string | Uint8Array, opts?: AesOptions): Promise<Uint8Array>;
43
50
  /**
44
51
  * Encrypt with a passphrase (PBKDF2-derived key + AES-256-GCM).
45
52
  * Returns `v1p:<iterations>:<salt>:<iv>:<ciphertext>` — self-contained.
@@ -49,5 +56,48 @@ declare function encryptWithPassword(plaintext: string | Uint8Array, password: s
49
56
  }): Promise<string>;
50
57
  /** Decrypt a string produced by {@link encryptWithPassword}. */
51
58
  declare function decryptWithPassword(payload: string, password: string): Promise<string>;
59
+ interface HkdfOptions {
60
+ /** Optional salt (recommended). */
61
+ salt?: Uint8Array;
62
+ /** Context/label so the same master key yields different sub-keys per purpose. */
63
+ info?: string | Uint8Array;
64
+ /** Output length in bytes. Default 32. */
65
+ length?: number;
66
+ hash?: HashAlgorithm;
67
+ }
68
+ /**
69
+ * HKDF: derive one or many purpose-bound sub-keys from a single master key.
70
+ * @example const encKey = await hkdf(master, { info: "field-encryption", length: 32 });
71
+ */
72
+ declare function hkdf(keyMaterial: string | Uint8Array, opts?: HkdfOptions): Promise<Uint8Array>;
73
+ interface KeyringEntry {
74
+ /** Stable key id (embedded in ciphertext; must not contain ":"). */
75
+ id: string;
76
+ /** 32-byte AES key (base64url string or bytes). */
77
+ key: string | Uint8Array;
78
+ }
79
+ /**
80
+ * A set of versioned AES keys for zero-downtime rotation. New data is encrypted
81
+ * under the primary key; old ciphertext is decrypted by the key its `id` names.
82
+ * Envelope: `v2:<keyId>:<iv>:<ciphertext>`.
83
+ *
84
+ * @example
85
+ * const ring = new Keyring([{ id: "2025", key: oldKey }, { id: "2026", key: newKey }]);
86
+ * const blob = await ring.encrypt("secret"); // uses "2026" (primary = last)
87
+ * const text = await ring.decrypt(oldBlob); // finds the right key by id
88
+ * const fresh = await ring.reEncrypt(oldBlob); // migrate to the primary key
89
+ */
90
+ declare class Keyring {
91
+ private readonly keys;
92
+ readonly primaryId: string;
93
+ constructor(entries: KeyringEntry[], primaryId?: string);
94
+ /** Encrypt under the primary key. */
95
+ encrypt(plaintext: string | Uint8Array, opts?: AesOptions): Promise<string>;
96
+ private resolve;
97
+ decrypt(payload: string, opts?: AesOptions): Promise<string>;
98
+ decryptBytes(payload: string, opts?: AesOptions): Promise<Uint8Array>;
99
+ /** Re-encrypt under the primary key if it isn't already. Returns the (possibly new) payload. */
100
+ reEncrypt(payload: string, opts?: AesOptions): Promise<string>;
101
+ }
52
102
 
53
- export { type DeriveOptions, type HashAlgorithm, constantTimeEqual, decrypt, decryptWithPassword, deriveBits, digest, encrypt, encryptWithPassword, fromBase64url, fromHex, generateKey, hmac, hmacVerify, randomBytes, sha256, toBase64url, toHex };
103
+ export { type AesOptions, type DeriveOptions, type HashAlgorithm, type HkdfOptions, Keyring, type KeyringEntry, constantTimeEqual, decrypt, decryptBytes, decryptWithPassword, deriveBits, digest, encrypt, encryptWithPassword, fromBase64url, fromHex, generateKey, hkdf, hmac, hmacVerify, randomBytes, sha256, toBase64url, toHex };
package/dist/index.js CHANGED
@@ -108,28 +108,35 @@ async function importAesKey(key) {
108
108
  "decrypt"
109
109
  ]);
110
110
  }
111
- async function encrypt(plaintext, key) {
111
+ function gcmParams(iv, aad) {
112
+ const p = { name: "AES-GCM", iv };
113
+ if (aad !== void 0) p.additionalData = toBytes(aad);
114
+ return p;
115
+ }
116
+ async function encrypt(plaintext, key, opts = {}) {
112
117
  const cryptoKey = await importAesKey(key);
113
118
  const iv = randomBytes(12);
114
119
  const ct = await getCrypto().subtle.encrypt(
115
- { name: "AES-GCM", iv },
120
+ gcmParams(iv, opts.aad),
116
121
  cryptoKey,
117
122
  toBytes(plaintext)
118
123
  );
119
124
  return `${AES_PREFIX}:${toBase64url(iv)}:${toBase64url(new Uint8Array(ct))}`;
120
125
  }
121
- async function decrypt(payload, key) {
126
+ async function decryptToBytes(payload, key, opts) {
122
127
  const parts = payload.split(":");
123
128
  if (parts.length !== 3 || parts[0] !== AES_PREFIX) throw new Error("invalid ciphertext format");
124
129
  const cryptoKey = await importAesKey(key);
125
130
  const iv = fromBase64url(parts[1]);
126
131
  const ct = fromBase64url(parts[2]);
127
- const pt = await getCrypto().subtle.decrypt(
128
- { name: "AES-GCM", iv },
129
- cryptoKey,
130
- ct
131
- );
132
- return dec.decode(pt);
132
+ const pt = await getCrypto().subtle.decrypt(gcmParams(iv, opts.aad), cryptoKey, ct);
133
+ return new Uint8Array(pt);
134
+ }
135
+ async function decrypt(payload, key, opts = {}) {
136
+ return dec.decode(await decryptToBytes(payload, key, opts));
137
+ }
138
+ async function decryptBytes(payload, key, opts = {}) {
139
+ return decryptToBytes(payload, key, opts);
133
140
  }
134
141
  async function encryptWithPassword(plaintext, password, opts = {}) {
135
142
  const iterations = opts.iterations ?? DEFAULT_PW_ITERATIONS;
@@ -147,7 +154,69 @@ async function decryptWithPassword(payload, password) {
147
154
  const key = await deriveBits(password, salt, { iterations, length: 32 });
148
155
  return decrypt(`${AES_PREFIX}:${parts[3]}:${parts[4]}`, key);
149
156
  }
157
+ async function hkdf(keyMaterial, opts = {}) {
158
+ const c = getCrypto();
159
+ const baseKey = await c.subtle.importKey("raw", toBytes(keyMaterial), "HKDF", false, [
160
+ "deriveBits"
161
+ ]);
162
+ const bits = await c.subtle.deriveBits(
163
+ {
164
+ name: "HKDF",
165
+ hash: opts.hash ?? "SHA-256",
166
+ salt: opts.salt ?? new Uint8Array(0),
167
+ info: toBytes(opts.info ?? "")
168
+ },
169
+ baseKey,
170
+ (opts.length ?? 32) * 8
171
+ );
172
+ return new Uint8Array(bits);
173
+ }
174
+ var KEYRING_PREFIX = "v2";
175
+ var Keyring = class {
176
+ constructor(entries, primaryId) {
177
+ this.keys = /* @__PURE__ */ new Map();
178
+ if (!entries.length) throw new Error("Keyring needs at least one key");
179
+ for (const e of entries) {
180
+ if (e.id.includes(":")) throw new Error(`key id "${e.id}" must not contain ":"`);
181
+ this.keys.set(e.id, e.key);
182
+ }
183
+ this.primaryId = primaryId ?? entries[entries.length - 1].id;
184
+ if (!this.keys.has(this.primaryId)) throw new Error(`primary key "${this.primaryId}" is not in the keyring`);
185
+ }
186
+ /** Encrypt under the primary key. */
187
+ async encrypt(plaintext, opts = {}) {
188
+ const inner = await encrypt(plaintext, this.keys.get(this.primaryId), opts);
189
+ const [, iv, ct] = inner.split(":");
190
+ return `${KEYRING_PREFIX}:${this.primaryId}:${iv}:${ct}`;
191
+ }
192
+ resolve(payload) {
193
+ const parts = payload.split(":");
194
+ if (parts[0] === KEYRING_PREFIX) {
195
+ const kid = parts[1];
196
+ const key = this.keys.get(kid);
197
+ if (!key) throw new Error(`unknown key id "${kid}"`);
198
+ return { key, inner: `${AES_PREFIX}:${parts[2]}:${parts[3]}` };
199
+ }
200
+ if (parts[0] === AES_PREFIX) return { key: this.keys.get(this.primaryId), inner: payload };
201
+ throw new Error("invalid ciphertext format");
202
+ }
203
+ async decrypt(payload, opts = {}) {
204
+ const { key, inner } = this.resolve(payload);
205
+ return decrypt(inner, key, opts);
206
+ }
207
+ async decryptBytes(payload, opts = {}) {
208
+ const { key, inner } = this.resolve(payload);
209
+ return decryptBytes(inner, key, opts);
210
+ }
211
+ /** Re-encrypt under the primary key if it isn't already. Returns the (possibly new) payload. */
212
+ async reEncrypt(payload, opts = {}) {
213
+ const parts = payload.split(":");
214
+ if (parts[0] === KEYRING_PREFIX && parts[1] === this.primaryId) return payload;
215
+ const pt = await this.decryptBytes(payload, opts);
216
+ return this.encrypt(pt, opts);
217
+ }
218
+ };
150
219
 
151
- export { constantTimeEqual, decrypt, decryptWithPassword, deriveBits, digest, encrypt, encryptWithPassword, fromBase64url, fromHex, generateKey, hmac, hmacVerify, randomBytes, sha256, toBase64url, toHex };
220
+ export { Keyring, constantTimeEqual, decrypt, decryptBytes, decryptWithPassword, deriveBits, digest, encrypt, encryptWithPassword, fromBase64url, fromHex, generateKey, hkdf, hmac, hmacVerify, randomBytes, sha256, toBase64url, toHex };
152
221
  //# sourceMappingURL=index.js.map
153
222
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * @lacspace/crypto\n * Safe, boring cryptography — authenticated AES-256-GCM, key derivation, hashing.\n *\n * A thin, correct layer over the Web Crypto API (no hand-rolled crypto), so the\n * same code runs on Node 18+, edge runtimes, browsers and React Native. Encrypt\n * database fields, S3 object payloads, cookies and tokens with confidence.\n *\n * Zero dependencies · isomorphic · fully typed.\n */\n\nfunction getCrypto(): Crypto {\n const c = (globalThis as { crypto?: Crypto }).crypto;\n if (!c || !c.subtle) {\n throw new Error(\"Web Crypto unavailable — @lacspace/crypto needs Node 18+, an edge runtime or a browser\");\n }\n return c;\n}\n\n/* ------------------------------ encoding ------------------------------ */\n\nexport function randomBytes(length: number): Uint8Array {\n const buf = new Uint8Array(length);\n getCrypto().getRandomValues(buf);\n return buf;\n}\n\nexport function toHex(bytes: Uint8Array): string {\n let out = \"\";\n for (const b of bytes) out += b.toString(16).padStart(2, \"0\");\n return out;\n}\n\nexport function fromHex(hex: string): Uint8Array {\n const clean = hex.length % 2 ? \"0\" + hex : hex;\n const out = new Uint8Array(clean.length / 2);\n for (let i = 0; i < out.length; i++) out[i] = parseInt(clean.slice(i * 2, i * 2 + 2), 16);\n return out;\n}\n\nexport function toBase64url(bytes: Uint8Array): string {\n let bin = \"\";\n for (const b of bytes) bin += String.fromCharCode(b);\n const b64 = typeof btoa !== \"undefined\" ? btoa(bin) : Buffer.from(bytes).toString(\"base64\");\n return b64.replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=+$/, \"\");\n}\n\nexport function fromBase64url(s: string): Uint8Array {\n const b64 = s.replace(/-/g, \"+\").replace(/_/g, \"/\") + \"===\".slice((s.length + 3) % 4);\n if (typeof atob !== \"undefined\") {\n const bin = atob(b64);\n const out = new Uint8Array(bin.length);\n for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);\n return out;\n }\n return new Uint8Array(Buffer.from(b64, \"base64\"));\n}\n\nconst enc = new TextEncoder();\nconst dec = new TextDecoder();\n\nfunction toBytes(data: string | Uint8Array): Uint8Array {\n return typeof data === \"string\" ? enc.encode(data) : data;\n}\n\n/** Constant-time comparison of two byte arrays or strings. */\nexport function constantTimeEqual(a: Uint8Array | string, b: Uint8Array | string): boolean {\n const x = toBytes(a);\n const y = toBytes(b);\n if (x.length !== y.length) return false;\n let diff = 0;\n for (let i = 0; i < x.length; i++) diff |= x[i]! ^ y[i]!;\n return diff === 0;\n}\n\n/* ------------------------------ hashing ------------------------------ */\n\nexport type HashAlgorithm = \"SHA-256\" | \"SHA-384\" | \"SHA-512\";\n\nexport async function digest(\n data: string | Uint8Array,\n algorithm: HashAlgorithm = \"SHA-256\",\n): Promise<Uint8Array> {\n const buf = await getCrypto().subtle.digest(algorithm, toBytes(data) as unknown as BufferSource);\n return new Uint8Array(buf);\n}\n\n/** SHA-256 hex digest. */\nexport async function sha256(data: string | Uint8Array): Promise<string> {\n return toHex(await digest(data, \"SHA-256\"));\n}\n\n/** HMAC signature (bytes). */\nexport async function hmac(\n key: string | Uint8Array,\n data: string | Uint8Array,\n algorithm: HashAlgorithm = \"SHA-256\",\n): Promise<Uint8Array> {\n const c = getCrypto();\n const cryptoKey = await c.subtle.importKey(\n \"raw\",\n toBytes(key) as unknown as BufferSource,\n { name: \"HMAC\", hash: algorithm },\n false,\n [\"sign\"],\n );\n const sig = await c.subtle.sign(\"HMAC\", cryptoKey, toBytes(data) as unknown as BufferSource);\n return new Uint8Array(sig);\n}\n\n/** Verify an HMAC in constant time. */\nexport async function hmacVerify(\n key: string | Uint8Array,\n data: string | Uint8Array,\n signature: Uint8Array,\n algorithm: HashAlgorithm = \"SHA-256\",\n): Promise<boolean> {\n return constantTimeEqual(await hmac(key, data, algorithm), signature);\n}\n\n/* ------------------------------ key derivation ------------------------------ */\n\nexport interface DeriveOptions {\n iterations?: number;\n hash?: HashAlgorithm;\n /** Derived key length in bytes. Default 32. */\n length?: number;\n}\n\n/** Derive raw key bytes from a password with PBKDF2. */\nexport async function deriveBits(\n password: string | Uint8Array,\n salt: Uint8Array,\n opts: DeriveOptions = {},\n): Promise<Uint8Array> {\n const c = getCrypto();\n const baseKey = await c.subtle.importKey(\n \"raw\",\n toBytes(password) as unknown as BufferSource,\n \"PBKDF2\",\n false,\n [\"deriveBits\"],\n );\n const bits = await c.subtle.deriveBits(\n {\n name: \"PBKDF2\",\n salt: salt as unknown as BufferSource,\n iterations: opts.iterations ?? 210000,\n hash: opts.hash ?? \"SHA-256\",\n },\n baseKey,\n (opts.length ?? 32) * 8,\n );\n return new Uint8Array(bits);\n}\n\n/* ------------------------------ AES-256-GCM ------------------------------ */\n\nconst AES_PREFIX = \"v1\";\nconst AES_PW_PREFIX = \"v1p\";\nconst DEFAULT_PW_ITERATIONS = 210000;\n\n/** Generate a random 256-bit AES key as a base64url string. */\nexport function generateKey(): string {\n return toBase64url(randomBytes(32));\n}\n\nasync function importAesKey(key: string | Uint8Array): Promise<CryptoKey> {\n const raw = typeof key === \"string\" ? fromBase64url(key) : key;\n if (raw.length !== 32) throw new Error(\"AES key must be 32 bytes (256-bit)\");\n return getCrypto().subtle.importKey(\"raw\", raw as unknown as BufferSource, { name: \"AES-GCM\" }, false, [\n \"encrypt\",\n \"decrypt\",\n ]);\n}\n\n/**\n * Encrypt with AES-256-GCM using a 32-byte key (base64url or bytes).\n * Returns a compact self-describing string: `v1:<iv>:<ciphertext>`.\n */\nexport async function encrypt(plaintext: string | Uint8Array, key: string | Uint8Array): Promise<string> {\n const cryptoKey = await importAesKey(key);\n const iv = randomBytes(12);\n const ct = await getCrypto().subtle.encrypt(\n { name: \"AES-GCM\", iv: iv as unknown as BufferSource },\n cryptoKey,\n toBytes(plaintext) as unknown as BufferSource,\n );\n return `${AES_PREFIX}:${toBase64url(iv)}:${toBase64url(new Uint8Array(ct))}`;\n}\n\n/** Decrypt a string produced by {@link encrypt}. Returns the UTF-8 plaintext. */\nexport async function decrypt(payload: string, key: string | Uint8Array): Promise<string> {\n const parts = payload.split(\":\");\n if (parts.length !== 3 || parts[0] !== AES_PREFIX) throw new Error(\"invalid ciphertext format\");\n const cryptoKey = await importAesKey(key);\n const iv = fromBase64url(parts[1]!);\n const ct = fromBase64url(parts[2]!);\n const pt = await getCrypto().subtle.decrypt(\n { name: \"AES-GCM\", iv: iv as unknown as BufferSource },\n cryptoKey,\n ct as unknown as BufferSource,\n );\n return dec.decode(pt);\n}\n\n/**\n * Encrypt with a passphrase (PBKDF2-derived key + AES-256-GCM).\n * Returns `v1p:<iterations>:<salt>:<iv>:<ciphertext>` — self-contained.\n */\nexport async function encryptWithPassword(\n plaintext: string | Uint8Array,\n password: string,\n opts: { iterations?: number } = {},\n): Promise<string> {\n const iterations = opts.iterations ?? DEFAULT_PW_ITERATIONS;\n const salt = randomBytes(16);\n const key = await deriveBits(password, salt, { iterations, length: 32 });\n const inner = await encrypt(plaintext, key);\n const [, iv, ct] = inner.split(\":\");\n return `${AES_PW_PREFIX}:${iterations}:${toBase64url(salt)}:${iv}:${ct}`;\n}\n\n/** Decrypt a string produced by {@link encryptWithPassword}. */\nexport async function decryptWithPassword(payload: string, password: string): Promise<string> {\n const parts = payload.split(\":\");\n if (parts.length !== 5 || parts[0] !== AES_PW_PREFIX) throw new Error(\"invalid ciphertext format\");\n const iterations = parseInt(parts[1]!, 10);\n const salt = fromBase64url(parts[2]!);\n const key = await deriveBits(password, salt, { iterations, length: 32 });\n return decrypt(`${AES_PREFIX}:${parts[3]}:${parts[4]}`, key);\n}\n"]}
1
+ {"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * @lacspace/crypto\n * Safe, boring cryptography — authenticated AES-256-GCM, key derivation, hashing.\n *\n * A thin, correct layer over the Web Crypto API (no hand-rolled crypto), so the\n * same code runs on Node 18+, edge runtimes, browsers and React Native. Encrypt\n * database fields, S3 object payloads, cookies and tokens with confidence.\n *\n * Zero dependencies · isomorphic · fully typed.\n */\n\nfunction getCrypto(): Crypto {\n const c = (globalThis as { crypto?: Crypto }).crypto;\n if (!c || !c.subtle) {\n throw new Error(\"Web Crypto unavailable — @lacspace/crypto needs Node 18+, an edge runtime or a browser\");\n }\n return c;\n}\n\n/* ------------------------------ encoding ------------------------------ */\n\nexport function randomBytes(length: number): Uint8Array {\n const buf = new Uint8Array(length);\n getCrypto().getRandomValues(buf);\n return buf;\n}\n\nexport function toHex(bytes: Uint8Array): string {\n let out = \"\";\n for (const b of bytes) out += b.toString(16).padStart(2, \"0\");\n return out;\n}\n\nexport function fromHex(hex: string): Uint8Array {\n const clean = hex.length % 2 ? \"0\" + hex : hex;\n const out = new Uint8Array(clean.length / 2);\n for (let i = 0; i < out.length; i++) out[i] = parseInt(clean.slice(i * 2, i * 2 + 2), 16);\n return out;\n}\n\nexport function toBase64url(bytes: Uint8Array): string {\n let bin = \"\";\n for (const b of bytes) bin += String.fromCharCode(b);\n const b64 = typeof btoa !== \"undefined\" ? btoa(bin) : Buffer.from(bytes).toString(\"base64\");\n return b64.replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=+$/, \"\");\n}\n\nexport function fromBase64url(s: string): Uint8Array {\n const b64 = s.replace(/-/g, \"+\").replace(/_/g, \"/\") + \"===\".slice((s.length + 3) % 4);\n if (typeof atob !== \"undefined\") {\n const bin = atob(b64);\n const out = new Uint8Array(bin.length);\n for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);\n return out;\n }\n return new Uint8Array(Buffer.from(b64, \"base64\"));\n}\n\nconst enc = new TextEncoder();\nconst dec = new TextDecoder();\n\nfunction toBytes(data: string | Uint8Array): Uint8Array {\n return typeof data === \"string\" ? enc.encode(data) : data;\n}\n\n/** Constant-time comparison of two byte arrays or strings. */\nexport function constantTimeEqual(a: Uint8Array | string, b: Uint8Array | string): boolean {\n const x = toBytes(a);\n const y = toBytes(b);\n if (x.length !== y.length) return false;\n let diff = 0;\n for (let i = 0; i < x.length; i++) diff |= x[i]! ^ y[i]!;\n return diff === 0;\n}\n\n/* ------------------------------ hashing ------------------------------ */\n\nexport type HashAlgorithm = \"SHA-256\" | \"SHA-384\" | \"SHA-512\";\n\nexport async function digest(\n data: string | Uint8Array,\n algorithm: HashAlgorithm = \"SHA-256\",\n): Promise<Uint8Array> {\n const buf = await getCrypto().subtle.digest(algorithm, toBytes(data) as unknown as BufferSource);\n return new Uint8Array(buf);\n}\n\n/** SHA-256 hex digest. */\nexport async function sha256(data: string | Uint8Array): Promise<string> {\n return toHex(await digest(data, \"SHA-256\"));\n}\n\n/** HMAC signature (bytes). */\nexport async function hmac(\n key: string | Uint8Array,\n data: string | Uint8Array,\n algorithm: HashAlgorithm = \"SHA-256\",\n): Promise<Uint8Array> {\n const c = getCrypto();\n const cryptoKey = await c.subtle.importKey(\n \"raw\",\n toBytes(key) as unknown as BufferSource,\n { name: \"HMAC\", hash: algorithm },\n false,\n [\"sign\"],\n );\n const sig = await c.subtle.sign(\"HMAC\", cryptoKey, toBytes(data) as unknown as BufferSource);\n return new Uint8Array(sig);\n}\n\n/** Verify an HMAC in constant time. */\nexport async function hmacVerify(\n key: string | Uint8Array,\n data: string | Uint8Array,\n signature: Uint8Array,\n algorithm: HashAlgorithm = \"SHA-256\",\n): Promise<boolean> {\n return constantTimeEqual(await hmac(key, data, algorithm), signature);\n}\n\n/* ------------------------------ key derivation ------------------------------ */\n\nexport interface DeriveOptions {\n iterations?: number;\n hash?: HashAlgorithm;\n /** Derived key length in bytes. Default 32. */\n length?: number;\n}\n\n/** Derive raw key bytes from a password with PBKDF2. */\nexport async function deriveBits(\n password: string | Uint8Array,\n salt: Uint8Array,\n opts: DeriveOptions = {},\n): Promise<Uint8Array> {\n const c = getCrypto();\n const baseKey = await c.subtle.importKey(\n \"raw\",\n toBytes(password) as unknown as BufferSource,\n \"PBKDF2\",\n false,\n [\"deriveBits\"],\n );\n const bits = await c.subtle.deriveBits(\n {\n name: \"PBKDF2\",\n salt: salt as unknown as BufferSource,\n iterations: opts.iterations ?? 210000,\n hash: opts.hash ?? \"SHA-256\",\n },\n baseKey,\n (opts.length ?? 32) * 8,\n );\n return new Uint8Array(bits);\n}\n\n/* ------------------------------ AES-256-GCM ------------------------------ */\n\nconst AES_PREFIX = \"v1\";\nconst AES_PW_PREFIX = \"v1p\";\nconst DEFAULT_PW_ITERATIONS = 210000;\n\n/** Generate a random 256-bit AES key as a base64url string. */\nexport function generateKey(): string {\n return toBase64url(randomBytes(32));\n}\n\nasync function importAesKey(key: string | Uint8Array): Promise<CryptoKey> {\n const raw = typeof key === \"string\" ? fromBase64url(key) : key;\n if (raw.length !== 32) throw new Error(\"AES key must be 32 bytes (256-bit)\");\n return getCrypto().subtle.importKey(\"raw\", raw as unknown as BufferSource, { name: \"AES-GCM\" }, false, [\n \"encrypt\",\n \"decrypt\",\n ]);\n}\n\nexport interface AesOptions {\n /** Additional Authenticated Data — bound to the ciphertext (must match on decrypt). */\n aad?: string | Uint8Array;\n}\n\nfunction gcmParams(iv: Uint8Array, aad?: string | Uint8Array): AesGcmParams {\n const p: AesGcmParams = { name: \"AES-GCM\", iv: iv as unknown as BufferSource };\n if (aad !== undefined) p.additionalData = toBytes(aad) as unknown as BufferSource;\n return p;\n}\n\n/**\n * Encrypt with AES-256-GCM using a 32-byte key (base64url or bytes).\n * Returns a compact self-describing string: `v1:<iv>:<ciphertext>`.\n * Pass `opts.aad` to bind the ciphertext to a context (row id, tenant…).\n */\nexport async function encrypt(\n plaintext: string | Uint8Array,\n key: string | Uint8Array,\n opts: AesOptions = {},\n): Promise<string> {\n const cryptoKey = await importAesKey(key);\n const iv = randomBytes(12);\n const ct = await getCrypto().subtle.encrypt(\n gcmParams(iv, opts.aad),\n cryptoKey,\n toBytes(plaintext) as unknown as BufferSource,\n );\n return `${AES_PREFIX}:${toBase64url(iv)}:${toBase64url(new Uint8Array(ct))}`;\n}\n\nasync function decryptToBytes(payload: string, key: string | Uint8Array, opts: AesOptions): Promise<Uint8Array> {\n const parts = payload.split(\":\");\n if (parts.length !== 3 || parts[0] !== AES_PREFIX) throw new Error(\"invalid ciphertext format\");\n const cryptoKey = await importAesKey(key);\n const iv = fromBase64url(parts[1]!);\n const ct = fromBase64url(parts[2]!);\n const pt = await getCrypto().subtle.decrypt(gcmParams(iv, opts.aad), cryptoKey, ct as unknown as BufferSource);\n return new Uint8Array(pt);\n}\n\n/** Decrypt a string produced by {@link encrypt}. Returns the UTF-8 plaintext. */\nexport async function decrypt(payload: string, key: string | Uint8Array, opts: AesOptions = {}): Promise<string> {\n return dec.decode(await decryptToBytes(payload, key, opts));\n}\n\n/** Decrypt to raw bytes — binary-safe (files, protobufs, images). */\nexport async function decryptBytes(\n payload: string,\n key: string | Uint8Array,\n opts: AesOptions = {},\n): Promise<Uint8Array> {\n return decryptToBytes(payload, key, opts);\n}\n\n/**\n * Encrypt with a passphrase (PBKDF2-derived key + AES-256-GCM).\n * Returns `v1p:<iterations>:<salt>:<iv>:<ciphertext>` — self-contained.\n */\nexport async function encryptWithPassword(\n plaintext: string | Uint8Array,\n password: string,\n opts: { iterations?: number } = {},\n): Promise<string> {\n const iterations = opts.iterations ?? DEFAULT_PW_ITERATIONS;\n const salt = randomBytes(16);\n const key = await deriveBits(password, salt, { iterations, length: 32 });\n const inner = await encrypt(plaintext, key);\n const [, iv, ct] = inner.split(\":\");\n return `${AES_PW_PREFIX}:${iterations}:${toBase64url(salt)}:${iv}:${ct}`;\n}\n\n/** Decrypt a string produced by {@link encryptWithPassword}. */\nexport async function decryptWithPassword(payload: string, password: string): Promise<string> {\n const parts = payload.split(\":\");\n if (parts.length !== 5 || parts[0] !== AES_PW_PREFIX) throw new Error(\"invalid ciphertext format\");\n const iterations = parseInt(parts[1]!, 10);\n const salt = fromBase64url(parts[2]!);\n const key = await deriveBits(password, salt, { iterations, length: 32 });\n return decrypt(`${AES_PREFIX}:${parts[3]}:${parts[4]}`, key);\n}\n\n/* ------------------------------ HKDF ------------------------------ */\n\nexport interface HkdfOptions {\n /** Optional salt (recommended). */\n salt?: Uint8Array;\n /** Context/label so the same master key yields different sub-keys per purpose. */\n info?: string | Uint8Array;\n /** Output length in bytes. Default 32. */\n length?: number;\n hash?: HashAlgorithm;\n}\n\n/**\n * HKDF: derive one or many purpose-bound sub-keys from a single master key.\n * @example const encKey = await hkdf(master, { info: \"field-encryption\", length: 32 });\n */\nexport async function hkdf(keyMaterial: string | Uint8Array, opts: HkdfOptions = {}): Promise<Uint8Array> {\n const c = getCrypto();\n const baseKey = await c.subtle.importKey(\"raw\", toBytes(keyMaterial) as unknown as BufferSource, \"HKDF\", false, [\n \"deriveBits\",\n ]);\n const bits = await c.subtle.deriveBits(\n {\n name: \"HKDF\",\n hash: opts.hash ?? \"SHA-256\",\n salt: (opts.salt ?? new Uint8Array(0)) as unknown as BufferSource,\n info: toBytes(opts.info ?? \"\") as unknown as BufferSource,\n },\n baseKey,\n (opts.length ?? 32) * 8,\n );\n return new Uint8Array(bits);\n}\n\n/* ------------------------------ key rotation ------------------------------ */\n\nconst KEYRING_PREFIX = \"v2\";\n\nexport interface KeyringEntry {\n /** Stable key id (embedded in ciphertext; must not contain \":\"). */\n id: string;\n /** 32-byte AES key (base64url string or bytes). */\n key: string | Uint8Array;\n}\n\n/**\n * A set of versioned AES keys for zero-downtime rotation. New data is encrypted\n * under the primary key; old ciphertext is decrypted by the key its `id` names.\n * Envelope: `v2:<keyId>:<iv>:<ciphertext>`.\n *\n * @example\n * const ring = new Keyring([{ id: \"2025\", key: oldKey }, { id: \"2026\", key: newKey }]);\n * const blob = await ring.encrypt(\"secret\"); // uses \"2026\" (primary = last)\n * const text = await ring.decrypt(oldBlob); // finds the right key by id\n * const fresh = await ring.reEncrypt(oldBlob); // migrate to the primary key\n */\nexport class Keyring {\n private readonly keys = new Map<string, string | Uint8Array>();\n readonly primaryId: string;\n\n constructor(entries: KeyringEntry[], primaryId?: string) {\n if (!entries.length) throw new Error(\"Keyring needs at least one key\");\n for (const e of entries) {\n if (e.id.includes(\":\")) throw new Error(`key id \"${e.id}\" must not contain \":\"`);\n this.keys.set(e.id, e.key);\n }\n this.primaryId = primaryId ?? entries[entries.length - 1]!.id;\n if (!this.keys.has(this.primaryId)) throw new Error(`primary key \"${this.primaryId}\" is not in the keyring`);\n }\n\n /** Encrypt under the primary key. */\n async encrypt(plaintext: string | Uint8Array, opts: AesOptions = {}): Promise<string> {\n const inner = await encrypt(plaintext, this.keys.get(this.primaryId)!, opts);\n const [, iv, ct] = inner.split(\":\");\n return `${KEYRING_PREFIX}:${this.primaryId}:${iv}:${ct}`;\n }\n\n private resolve(payload: string): { key: string | Uint8Array; inner: string } {\n const parts = payload.split(\":\");\n if (parts[0] === KEYRING_PREFIX) {\n const kid = parts[1]!;\n const key = this.keys.get(kid);\n if (!key) throw new Error(`unknown key id \"${kid}\"`);\n return { key, inner: `${AES_PREFIX}:${parts[2]}:${parts[3]}` };\n }\n if (parts[0] === AES_PREFIX) return { key: this.keys.get(this.primaryId)!, inner: payload }; // legacy v1\n throw new Error(\"invalid ciphertext format\");\n }\n\n async decrypt(payload: string, opts: AesOptions = {}): Promise<string> {\n const { key, inner } = this.resolve(payload);\n return decrypt(inner, key, opts);\n }\n\n async decryptBytes(payload: string, opts: AesOptions = {}): Promise<Uint8Array> {\n const { key, inner } = this.resolve(payload);\n return decryptBytes(inner, key, opts);\n }\n\n /** Re-encrypt under the primary key if it isn't already. Returns the (possibly new) payload. */\n async reEncrypt(payload: string, opts: AesOptions = {}): Promise<string> {\n const parts = payload.split(\":\");\n if (parts[0] === KEYRING_PREFIX && parts[1] === this.primaryId) return payload;\n const pt = await this.decryptBytes(payload, opts);\n return this.encrypt(pt, opts);\n }\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lacspace/crypto",
3
- "version": "1.0.2",
3
+ "version": "1.1.0",
4
4
  "description": "Safe, boring cryptography over Web Crypto — authenticated AES-256-GCM, PBKDF2 key derivation, SHA-256, HMAC, secure random and constant-time compare. Isomorphic (Node, edge, browser, RN).",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",