@lacspace/crypto 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Lacspace
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,96 @@
1
+ <div align="center">
2
+
3
+ # @lacspace/crypto
4
+
5
+ **Safe, boring cryptography — authenticated AES-256-GCM, key derivation, hashing.**
6
+
7
+ [![npm version](https://img.shields.io/npm/v/@lacspace/crypto?color=%23a855f7&label=npm)](https://www.npmjs.com/package/@lacspace/crypto)
8
+ [![install size](https://packagephobia.com/badge?p=@lacspace/crypto)](https://packagephobia.com/result?p=@lacspace/crypto)
9
+ [![minzipped](https://img.shields.io/bundlephobia/minzip/@lacspace/crypto?label=minzip)](https://bundlephobia.com/package/@lacspace/crypto)
10
+ [![types](https://img.shields.io/badge/types-included-blue)](https://www.npmjs.com/package/@lacspace/crypto)
11
+ [![license](https://img.shields.io/npm/l/@lacspace/crypto?color=green)](https://github.com/lacspace/npm-packages/blob/main/LICENSE)
12
+
13
+ </div>
14
+
15
+ > A thin, correct layer over the **Web Crypto API** — no hand-rolled crypto. Authenticated **AES-256-GCM**, PBKDF2 key derivation, SHA-256/384/512, HMAC, secure random and constant-time compare. Same code on Node 18+, edge, browsers and React Native. Encrypt database fields, S3 payloads, cookies and tokens.
16
+
17
+ - 🔐 `encrypt` / `decrypt` — AES-256-GCM (authenticated: tampering is rejected)
18
+ - 🔑 `encryptWithPassword` / `decryptWithPassword` — PBKDF2-derived key, self-contained
19
+ - #️⃣ `sha256` / `digest` / `hmac` / `hmacVerify`
20
+ - 🎲 `randomBytes` · `generateKey` · `constantTimeEqual`
21
+ - 🧰 hex / base64url helpers
22
+ - ⚡ Zero dependencies · 🌍 isomorphic · 📦 ESM + CJS · fully typed
23
+
24
+ ## Install
25
+
26
+ ```bash
27
+ npm install @lacspace/crypto # or pnpm add / yarn add / bun add
28
+ ```
29
+
30
+ ## Encrypt with a key
31
+
32
+ ```ts
33
+ import { generateKey, encrypt, decrypt } from "@lacspace/crypto";
34
+
35
+ const key = generateKey(); // 256-bit base64url key — store securely
36
+ const blob = await encrypt("card: 4242…", key);
37
+ // "v1:<iv>:<ciphertext+tag>" — safe to store in Mongo / S3
38
+ const plain = await decrypt(blob, key); // "card: 4242…"
39
+ ```
40
+
41
+ Encrypt a field before saving to MongoDB, or an object before putting it on S3:
42
+
43
+ ```ts
44
+ await s3.putObject({ Bucket, Key, Body: await encrypt(JSON.stringify(doc), key) });
45
+ ```
46
+
47
+ ## Encrypt with a passphrase
48
+
49
+ ```ts
50
+ import { encryptWithPassword, decryptWithPassword } from "@lacspace/crypto";
51
+
52
+ const sealed = await encryptWithPassword("secret", userPassphrase);
53
+ // "v1p:<iterations>:<salt>:<iv>:<ciphertext>" — fully self-describing
54
+ const opened = await decryptWithPassword(sealed, userPassphrase);
55
+ ```
56
+
57
+ ## Hashing, HMAC & helpers
58
+
59
+ ```ts
60
+ import { sha256, hmac, hmacVerify, constantTimeEqual, randomBytes } from "@lacspace/crypto";
61
+
62
+ await sha256("hello"); // hex digest
63
+ const sig = await hmac(secret, "payload"); // Uint8Array
64
+ await hmacVerify(secret, "payload", sig); // true (constant-time)
65
+ constantTimeEqual(a, b); // timing-safe compare
66
+ randomBytes(16); // CSPRNG bytes
67
+ ```
68
+
69
+ ## API
70
+
71
+ | Export | Description |
72
+ | --- | --- |
73
+ | `encrypt` / `decrypt` | AES-256-GCM with a 32-byte key |
74
+ | `encryptWithPassword` / `decryptWithPassword` | passphrase (PBKDF2 + AES-GCM) |
75
+ | `generateKey` | random 256-bit key (base64url) |
76
+ | `sha256` / `digest` / `hmac` / `hmacVerify` | hashing & MAC |
77
+ | `deriveBits` | PBKDF2 key derivation |
78
+ | `randomBytes` / `constantTimeEqual` | primitives |
79
+ | `toHex` / `fromHex` / `toBase64url` / `fromBase64url` | encoding |
80
+
81
+ ## The Lacspace Security Kit
82
+
83
+ | Package | For |
84
+ | --- | --- |
85
+ | **`@lacspace/crypto`** | AES encryption & hashing (this package) |
86
+ | [`@lacspace/password`](https://www.npmjs.com/package/@lacspace/password) | Password hashing |
87
+ | [`@lacspace/jwt`](https://www.npmjs.com/package/@lacspace/jwt) | JWTs & tokens |
88
+ | [`@lacspace/apikey`](https://www.npmjs.com/package/@lacspace/apikey) | API keys |
89
+ | [`@lacspace/otp`](https://www.npmjs.com/package/@lacspace/otp) | TOTP/HOTP 2FA |
90
+ | [`@lacspace/webauthn`](https://www.npmjs.com/package/@lacspace/webauthn) | Passkeys / biometric |
91
+ | [`@lacspace/mfa`](https://www.npmjs.com/package/@lacspace/mfa) | 2FA/3FA orchestration |
92
+ | [`@lacspace/lock`](https://www.npmjs.com/package/@lacspace/lock) | Account lockout |
93
+ | [`@lacspace/headers`](https://www.npmjs.com/package/@lacspace/headers) | Secure headers / CSP |
94
+ | [`@lacspace/redact`](https://www.npmjs.com/package/@lacspace/redact) | Log redaction |
95
+
96
+ <div align="center"><sub>Built with care by <a href="https://lacspace.com">Lacspace</a> · MIT licensed · <a href="https://github.com/lacspace/npm-packages">source</a></sub></div>
package/dist/index.cjs ADDED
@@ -0,0 +1,170 @@
1
+ 'use strict';
2
+
3
+ // src/index.ts
4
+ function getCrypto() {
5
+ const c = globalThis.crypto;
6
+ if (!c || !c.subtle) {
7
+ throw new Error("Web Crypto unavailable \u2014 @lacspace/crypto needs Node 18+, an edge runtime or a browser");
8
+ }
9
+ return c;
10
+ }
11
+ function randomBytes(length) {
12
+ const buf = new Uint8Array(length);
13
+ getCrypto().getRandomValues(buf);
14
+ return buf;
15
+ }
16
+ function toHex(bytes) {
17
+ let out = "";
18
+ for (const b of bytes) out += b.toString(16).padStart(2, "0");
19
+ return out;
20
+ }
21
+ function fromHex(hex) {
22
+ const clean = hex.length % 2 ? "0" + hex : hex;
23
+ const out = new Uint8Array(clean.length / 2);
24
+ for (let i = 0; i < out.length; i++) out[i] = parseInt(clean.slice(i * 2, i * 2 + 2), 16);
25
+ return out;
26
+ }
27
+ function toBase64url(bytes) {
28
+ let bin = "";
29
+ for (const b of bytes) bin += String.fromCharCode(b);
30
+ const b64 = typeof btoa !== "undefined" ? btoa(bin) : Buffer.from(bytes).toString("base64");
31
+ return b64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
32
+ }
33
+ function fromBase64url(s) {
34
+ const b64 = s.replace(/-/g, "+").replace(/_/g, "/") + "===".slice((s.length + 3) % 4);
35
+ if (typeof atob !== "undefined") {
36
+ const bin = atob(b64);
37
+ const out = new Uint8Array(bin.length);
38
+ for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
39
+ return out;
40
+ }
41
+ return new Uint8Array(Buffer.from(b64, "base64"));
42
+ }
43
+ var enc = new TextEncoder();
44
+ var dec = new TextDecoder();
45
+ function toBytes(data) {
46
+ return typeof data === "string" ? enc.encode(data) : data;
47
+ }
48
+ function constantTimeEqual(a, b) {
49
+ const x = toBytes(a);
50
+ const y = toBytes(b);
51
+ if (x.length !== y.length) return false;
52
+ let diff = 0;
53
+ for (let i = 0; i < x.length; i++) diff |= x[i] ^ y[i];
54
+ return diff === 0;
55
+ }
56
+ async function digest(data, algorithm = "SHA-256") {
57
+ const buf = await getCrypto().subtle.digest(algorithm, toBytes(data));
58
+ return new Uint8Array(buf);
59
+ }
60
+ async function sha256(data) {
61
+ return toHex(await digest(data, "SHA-256"));
62
+ }
63
+ async function hmac(key, data, algorithm = "SHA-256") {
64
+ const c = getCrypto();
65
+ const cryptoKey = await c.subtle.importKey(
66
+ "raw",
67
+ toBytes(key),
68
+ { name: "HMAC", hash: algorithm },
69
+ false,
70
+ ["sign"]
71
+ );
72
+ const sig = await c.subtle.sign("HMAC", cryptoKey, toBytes(data));
73
+ return new Uint8Array(sig);
74
+ }
75
+ async function hmacVerify(key, data, signature, algorithm = "SHA-256") {
76
+ return constantTimeEqual(await hmac(key, data, algorithm), signature);
77
+ }
78
+ async function deriveBits(password, salt, opts = {}) {
79
+ const c = getCrypto();
80
+ const baseKey = await c.subtle.importKey(
81
+ "raw",
82
+ toBytes(password),
83
+ "PBKDF2",
84
+ false,
85
+ ["deriveBits"]
86
+ );
87
+ const bits = await c.subtle.deriveBits(
88
+ {
89
+ name: "PBKDF2",
90
+ salt,
91
+ iterations: opts.iterations ?? 21e4,
92
+ hash: opts.hash ?? "SHA-256"
93
+ },
94
+ baseKey,
95
+ (opts.length ?? 32) * 8
96
+ );
97
+ return new Uint8Array(bits);
98
+ }
99
+ var AES_PREFIX = "v1";
100
+ var AES_PW_PREFIX = "v1p";
101
+ var DEFAULT_PW_ITERATIONS = 21e4;
102
+ function generateKey() {
103
+ return toBase64url(randomBytes(32));
104
+ }
105
+ async function importAesKey(key) {
106
+ const raw = typeof key === "string" ? fromBase64url(key) : key;
107
+ if (raw.length !== 32) throw new Error("AES key must be 32 bytes (256-bit)");
108
+ return getCrypto().subtle.importKey("raw", raw, { name: "AES-GCM" }, false, [
109
+ "encrypt",
110
+ "decrypt"
111
+ ]);
112
+ }
113
+ async function encrypt(plaintext, key) {
114
+ const cryptoKey = await importAesKey(key);
115
+ const iv = randomBytes(12);
116
+ const ct = await getCrypto().subtle.encrypt(
117
+ { name: "AES-GCM", iv },
118
+ cryptoKey,
119
+ toBytes(plaintext)
120
+ );
121
+ return `${AES_PREFIX}:${toBase64url(iv)}:${toBase64url(new Uint8Array(ct))}`;
122
+ }
123
+ async function decrypt(payload, key) {
124
+ const parts = payload.split(":");
125
+ if (parts.length !== 3 || parts[0] !== AES_PREFIX) throw new Error("invalid ciphertext format");
126
+ const cryptoKey = await importAesKey(key);
127
+ const iv = fromBase64url(parts[1]);
128
+ 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);
135
+ }
136
+ async function encryptWithPassword(plaintext, password, opts = {}) {
137
+ const iterations = opts.iterations ?? DEFAULT_PW_ITERATIONS;
138
+ const salt = randomBytes(16);
139
+ const key = await deriveBits(password, salt, { iterations, length: 32 });
140
+ const inner = await encrypt(plaintext, key);
141
+ const [, iv, ct] = inner.split(":");
142
+ return `${AES_PW_PREFIX}:${iterations}:${toBase64url(salt)}:${iv}:${ct}`;
143
+ }
144
+ async function decryptWithPassword(payload, password) {
145
+ const parts = payload.split(":");
146
+ if (parts.length !== 5 || parts[0] !== AES_PW_PREFIX) throw new Error("invalid ciphertext format");
147
+ const iterations = parseInt(parts[1], 10);
148
+ const salt = fromBase64url(parts[2]);
149
+ const key = await deriveBits(password, salt, { iterations, length: 32 });
150
+ return decrypt(`${AES_PREFIX}:${parts[3]}:${parts[4]}`, key);
151
+ }
152
+
153
+ exports.constantTimeEqual = constantTimeEqual;
154
+ exports.decrypt = decrypt;
155
+ exports.decryptWithPassword = decryptWithPassword;
156
+ exports.deriveBits = deriveBits;
157
+ exports.digest = digest;
158
+ exports.encrypt = encrypt;
159
+ exports.encryptWithPassword = encryptWithPassword;
160
+ exports.fromBase64url = fromBase64url;
161
+ exports.fromHex = fromHex;
162
+ exports.generateKey = generateKey;
163
+ exports.hmac = hmac;
164
+ exports.hmacVerify = hmacVerify;
165
+ exports.randomBytes = randomBytes;
166
+ exports.sha256 = sha256;
167
+ exports.toBase64url = toBase64url;
168
+ exports.toHex = toHex;
169
+ //# sourceMappingURL=index.cjs.map
170
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +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"]}
@@ -0,0 +1,53 @@
1
+ /**
2
+ * @lacspace/crypto
3
+ * Safe, boring cryptography — authenticated AES-256-GCM, key derivation, hashing.
4
+ *
5
+ * A thin, correct layer over the Web Crypto API (no hand-rolled crypto), so the
6
+ * same code runs on Node 18+, edge runtimes, browsers and React Native. Encrypt
7
+ * database fields, S3 object payloads, cookies and tokens with confidence.
8
+ *
9
+ * Zero dependencies · isomorphic · fully typed.
10
+ */
11
+ declare function randomBytes(length: number): Uint8Array;
12
+ declare function toHex(bytes: Uint8Array): string;
13
+ declare function fromHex(hex: string): Uint8Array;
14
+ declare function toBase64url(bytes: Uint8Array): string;
15
+ declare function fromBase64url(s: string): Uint8Array;
16
+ /** Constant-time comparison of two byte arrays or strings. */
17
+ declare function constantTimeEqual(a: Uint8Array | string, b: Uint8Array | string): boolean;
18
+ type HashAlgorithm = "SHA-256" | "SHA-384" | "SHA-512";
19
+ declare function digest(data: string | Uint8Array, algorithm?: HashAlgorithm): Promise<Uint8Array>;
20
+ /** SHA-256 hex digest. */
21
+ declare function sha256(data: string | Uint8Array): Promise<string>;
22
+ /** HMAC signature (bytes). */
23
+ declare function hmac(key: string | Uint8Array, data: string | Uint8Array, algorithm?: HashAlgorithm): Promise<Uint8Array>;
24
+ /** Verify an HMAC in constant time. */
25
+ declare function hmacVerify(key: string | Uint8Array, data: string | Uint8Array, signature: Uint8Array, algorithm?: HashAlgorithm): Promise<boolean>;
26
+ interface DeriveOptions {
27
+ iterations?: number;
28
+ hash?: HashAlgorithm;
29
+ /** Derived key length in bytes. Default 32. */
30
+ length?: number;
31
+ }
32
+ /** Derive raw key bytes from a password with PBKDF2. */
33
+ declare function deriveBits(password: string | Uint8Array, salt: Uint8Array, opts?: DeriveOptions): Promise<Uint8Array>;
34
+ /** Generate a random 256-bit AES key as a base64url string. */
35
+ declare function generateKey(): string;
36
+ /**
37
+ * Encrypt with AES-256-GCM using a 32-byte key (base64url or bytes).
38
+ * Returns a compact self-describing string: `v1:<iv>:<ciphertext>`.
39
+ */
40
+ declare function encrypt(plaintext: string | Uint8Array, key: string | Uint8Array): Promise<string>;
41
+ /** Decrypt a string produced by {@link encrypt}. Returns the UTF-8 plaintext. */
42
+ declare function decrypt(payload: string, key: string | Uint8Array): Promise<string>;
43
+ /**
44
+ * Encrypt with a passphrase (PBKDF2-derived key + AES-256-GCM).
45
+ * Returns `v1p:<iterations>:<salt>:<iv>:<ciphertext>` — self-contained.
46
+ */
47
+ declare function encryptWithPassword(plaintext: string | Uint8Array, password: string, opts?: {
48
+ iterations?: number;
49
+ }): Promise<string>;
50
+ /** Decrypt a string produced by {@link encryptWithPassword}. */
51
+ declare function decryptWithPassword(payload: string, password: string): Promise<string>;
52
+
53
+ export { type DeriveOptions, type HashAlgorithm, constantTimeEqual, decrypt, decryptWithPassword, deriveBits, digest, encrypt, encryptWithPassword, fromBase64url, fromHex, generateKey, hmac, hmacVerify, randomBytes, sha256, toBase64url, toHex };
@@ -0,0 +1,53 @@
1
+ /**
2
+ * @lacspace/crypto
3
+ * Safe, boring cryptography — authenticated AES-256-GCM, key derivation, hashing.
4
+ *
5
+ * A thin, correct layer over the Web Crypto API (no hand-rolled crypto), so the
6
+ * same code runs on Node 18+, edge runtimes, browsers and React Native. Encrypt
7
+ * database fields, S3 object payloads, cookies and tokens with confidence.
8
+ *
9
+ * Zero dependencies · isomorphic · fully typed.
10
+ */
11
+ declare function randomBytes(length: number): Uint8Array;
12
+ declare function toHex(bytes: Uint8Array): string;
13
+ declare function fromHex(hex: string): Uint8Array;
14
+ declare function toBase64url(bytes: Uint8Array): string;
15
+ declare function fromBase64url(s: string): Uint8Array;
16
+ /** Constant-time comparison of two byte arrays or strings. */
17
+ declare function constantTimeEqual(a: Uint8Array | string, b: Uint8Array | string): boolean;
18
+ type HashAlgorithm = "SHA-256" | "SHA-384" | "SHA-512";
19
+ declare function digest(data: string | Uint8Array, algorithm?: HashAlgorithm): Promise<Uint8Array>;
20
+ /** SHA-256 hex digest. */
21
+ declare function sha256(data: string | Uint8Array): Promise<string>;
22
+ /** HMAC signature (bytes). */
23
+ declare function hmac(key: string | Uint8Array, data: string | Uint8Array, algorithm?: HashAlgorithm): Promise<Uint8Array>;
24
+ /** Verify an HMAC in constant time. */
25
+ declare function hmacVerify(key: string | Uint8Array, data: string | Uint8Array, signature: Uint8Array, algorithm?: HashAlgorithm): Promise<boolean>;
26
+ interface DeriveOptions {
27
+ iterations?: number;
28
+ hash?: HashAlgorithm;
29
+ /** Derived key length in bytes. Default 32. */
30
+ length?: number;
31
+ }
32
+ /** Derive raw key bytes from a password with PBKDF2. */
33
+ declare function deriveBits(password: string | Uint8Array, salt: Uint8Array, opts?: DeriveOptions): Promise<Uint8Array>;
34
+ /** Generate a random 256-bit AES key as a base64url string. */
35
+ declare function generateKey(): string;
36
+ /**
37
+ * Encrypt with AES-256-GCM using a 32-byte key (base64url or bytes).
38
+ * Returns a compact self-describing string: `v1:<iv>:<ciphertext>`.
39
+ */
40
+ declare function encrypt(plaintext: string | Uint8Array, key: string | Uint8Array): Promise<string>;
41
+ /** Decrypt a string produced by {@link encrypt}. Returns the UTF-8 plaintext. */
42
+ declare function decrypt(payload: string, key: string | Uint8Array): Promise<string>;
43
+ /**
44
+ * Encrypt with a passphrase (PBKDF2-derived key + AES-256-GCM).
45
+ * Returns `v1p:<iterations>:<salt>:<iv>:<ciphertext>` — self-contained.
46
+ */
47
+ declare function encryptWithPassword(plaintext: string | Uint8Array, password: string, opts?: {
48
+ iterations?: number;
49
+ }): Promise<string>;
50
+ /** Decrypt a string produced by {@link encryptWithPassword}. */
51
+ declare function decryptWithPassword(payload: string, password: string): Promise<string>;
52
+
53
+ export { type DeriveOptions, type HashAlgorithm, constantTimeEqual, decrypt, decryptWithPassword, deriveBits, digest, encrypt, encryptWithPassword, fromBase64url, fromHex, generateKey, hmac, hmacVerify, randomBytes, sha256, toBase64url, toHex };
package/dist/index.js ADDED
@@ -0,0 +1,153 @@
1
+ // src/index.ts
2
+ function getCrypto() {
3
+ const c = globalThis.crypto;
4
+ if (!c || !c.subtle) {
5
+ throw new Error("Web Crypto unavailable \u2014 @lacspace/crypto needs Node 18+, an edge runtime or a browser");
6
+ }
7
+ return c;
8
+ }
9
+ function randomBytes(length) {
10
+ const buf = new Uint8Array(length);
11
+ getCrypto().getRandomValues(buf);
12
+ return buf;
13
+ }
14
+ function toHex(bytes) {
15
+ let out = "";
16
+ for (const b of bytes) out += b.toString(16).padStart(2, "0");
17
+ return out;
18
+ }
19
+ function fromHex(hex) {
20
+ const clean = hex.length % 2 ? "0" + hex : hex;
21
+ const out = new Uint8Array(clean.length / 2);
22
+ for (let i = 0; i < out.length; i++) out[i] = parseInt(clean.slice(i * 2, i * 2 + 2), 16);
23
+ return out;
24
+ }
25
+ function toBase64url(bytes) {
26
+ let bin = "";
27
+ for (const b of bytes) bin += String.fromCharCode(b);
28
+ const b64 = typeof btoa !== "undefined" ? btoa(bin) : Buffer.from(bytes).toString("base64");
29
+ return b64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
30
+ }
31
+ function fromBase64url(s) {
32
+ const b64 = s.replace(/-/g, "+").replace(/_/g, "/") + "===".slice((s.length + 3) % 4);
33
+ if (typeof atob !== "undefined") {
34
+ const bin = atob(b64);
35
+ const out = new Uint8Array(bin.length);
36
+ for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
37
+ return out;
38
+ }
39
+ return new Uint8Array(Buffer.from(b64, "base64"));
40
+ }
41
+ var enc = new TextEncoder();
42
+ var dec = new TextDecoder();
43
+ function toBytes(data) {
44
+ return typeof data === "string" ? enc.encode(data) : data;
45
+ }
46
+ function constantTimeEqual(a, b) {
47
+ const x = toBytes(a);
48
+ const y = toBytes(b);
49
+ if (x.length !== y.length) return false;
50
+ let diff = 0;
51
+ for (let i = 0; i < x.length; i++) diff |= x[i] ^ y[i];
52
+ return diff === 0;
53
+ }
54
+ async function digest(data, algorithm = "SHA-256") {
55
+ const buf = await getCrypto().subtle.digest(algorithm, toBytes(data));
56
+ return new Uint8Array(buf);
57
+ }
58
+ async function sha256(data) {
59
+ return toHex(await digest(data, "SHA-256"));
60
+ }
61
+ async function hmac(key, data, algorithm = "SHA-256") {
62
+ const c = getCrypto();
63
+ const cryptoKey = await c.subtle.importKey(
64
+ "raw",
65
+ toBytes(key),
66
+ { name: "HMAC", hash: algorithm },
67
+ false,
68
+ ["sign"]
69
+ );
70
+ const sig = await c.subtle.sign("HMAC", cryptoKey, toBytes(data));
71
+ return new Uint8Array(sig);
72
+ }
73
+ async function hmacVerify(key, data, signature, algorithm = "SHA-256") {
74
+ return constantTimeEqual(await hmac(key, data, algorithm), signature);
75
+ }
76
+ async function deriveBits(password, salt, opts = {}) {
77
+ const c = getCrypto();
78
+ const baseKey = await c.subtle.importKey(
79
+ "raw",
80
+ toBytes(password),
81
+ "PBKDF2",
82
+ false,
83
+ ["deriveBits"]
84
+ );
85
+ const bits = await c.subtle.deriveBits(
86
+ {
87
+ name: "PBKDF2",
88
+ salt,
89
+ iterations: opts.iterations ?? 21e4,
90
+ hash: opts.hash ?? "SHA-256"
91
+ },
92
+ baseKey,
93
+ (opts.length ?? 32) * 8
94
+ );
95
+ return new Uint8Array(bits);
96
+ }
97
+ var AES_PREFIX = "v1";
98
+ var AES_PW_PREFIX = "v1p";
99
+ var DEFAULT_PW_ITERATIONS = 21e4;
100
+ function generateKey() {
101
+ return toBase64url(randomBytes(32));
102
+ }
103
+ async function importAesKey(key) {
104
+ const raw = typeof key === "string" ? fromBase64url(key) : key;
105
+ if (raw.length !== 32) throw new Error("AES key must be 32 bytes (256-bit)");
106
+ return getCrypto().subtle.importKey("raw", raw, { name: "AES-GCM" }, false, [
107
+ "encrypt",
108
+ "decrypt"
109
+ ]);
110
+ }
111
+ async function encrypt(plaintext, key) {
112
+ const cryptoKey = await importAesKey(key);
113
+ const iv = randomBytes(12);
114
+ const ct = await getCrypto().subtle.encrypt(
115
+ { name: "AES-GCM", iv },
116
+ cryptoKey,
117
+ toBytes(plaintext)
118
+ );
119
+ return `${AES_PREFIX}:${toBase64url(iv)}:${toBase64url(new Uint8Array(ct))}`;
120
+ }
121
+ async function decrypt(payload, key) {
122
+ const parts = payload.split(":");
123
+ if (parts.length !== 3 || parts[0] !== AES_PREFIX) throw new Error("invalid ciphertext format");
124
+ const cryptoKey = await importAesKey(key);
125
+ const iv = fromBase64url(parts[1]);
126
+ 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);
133
+ }
134
+ async function encryptWithPassword(plaintext, password, opts = {}) {
135
+ const iterations = opts.iterations ?? DEFAULT_PW_ITERATIONS;
136
+ const salt = randomBytes(16);
137
+ const key = await deriveBits(password, salt, { iterations, length: 32 });
138
+ const inner = await encrypt(plaintext, key);
139
+ const [, iv, ct] = inner.split(":");
140
+ return `${AES_PW_PREFIX}:${iterations}:${toBase64url(salt)}:${iv}:${ct}`;
141
+ }
142
+ async function decryptWithPassword(payload, password) {
143
+ const parts = payload.split(":");
144
+ if (parts.length !== 5 || parts[0] !== AES_PW_PREFIX) throw new Error("invalid ciphertext format");
145
+ const iterations = parseInt(parts[1], 10);
146
+ const salt = fromBase64url(parts[2]);
147
+ const key = await deriveBits(password, salt, { iterations, length: 32 });
148
+ return decrypt(`${AES_PREFIX}:${parts[3]}:${parts[4]}`, key);
149
+ }
150
+
151
+ export { constantTimeEqual, decrypt, decryptWithPassword, deriveBits, digest, encrypt, encryptWithPassword, fromBase64url, fromHex, generateKey, hmac, hmacVerify, randomBytes, sha256, toBase64url, toHex };
152
+ //# sourceMappingURL=index.js.map
153
+ //# sourceMappingURL=index.js.map
@@ -0,0 +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"]}
package/package.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "@lacspace/crypto",
3
+ "version": "1.0.0",
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
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": { ".": { "import": { "types": "./dist/index.d.ts", "default": "./dist/index.js" }, "require": { "types": "./dist/index.d.cts", "default": "./dist/index.cjs" } } },
10
+ "files": ["dist"],
11
+ "sideEffects": false,
12
+ "scripts": { "build": "tsup", "prepublishOnly": "npm run build" },
13
+ "keywords": ["crypto", "aes", "aes-256-gcm", "encryption", "web-crypto", "pbkdf2", "hmac", "constant-time", "isomorphic", "typescript"],
14
+ "author": "Lacspace <contact@lacspace.com>",
15
+ "license": "MIT",
16
+ "homepage": "https://lacspace.com/packages",
17
+ "repository": { "type": "git", "url": "git+https://github.com/lacspace/npm-packages.git", "directory": "crypto" },
18
+ "bugs": { "url": "https://github.com/lacspace/npm-packages/issues" },
19
+ "engines": { "node": ">=18" }
20
+ }