{"_id":"@asafarim/appsafe","_rev":"7-5313236f307ef7321ecefb8a82456949","name":"@asafarim/appsafe","dist-tags":{"latest":"0.5.0"},"versions":{"0.1.0":{"name":"@asafarim/appsafe","version":"0.1.0","license":"MIT","_id":"@asafarim/appsafe@0.1.0","maintainers":[{"name":"asafarim.be","email":"asafarim.it@gmail.com"}],"dist":{"shasum":"e84bd49d69d3458acb7634546fff761fdb04ff36","tarball":"https://registry.npmjs.org/@asafarim/appsafe/-/appsafe-0.1.0.tgz","fileCount":6,"integrity":"sha512-gKrqNcc6kPC/9r2biukMrIiuTq8VMwjKRKXRByAEVc2PvfIzHlqS2R8P6SjdxWfpTYHwp7y2eao2wUNlTXko1w==","signatures":[{"sig":"MEUCIAreWt9aVzHEWH/9n/lSthcNsegw8PfTCuTidGSApnArAiEA9DanNArmeGbc7XDvcNnf421o1z15m5yu9n8JgJihB5w=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":13627},"main":"./dist/index.js","type":"module","_from":"file:asafarim-appsafe-0.1.0.tgz","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=20.0.0"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","default":"./dist/index.js"}},"scripts":{"test":"tsx --test test/*.test.ts","build":"tsc -p tsconfig.json","check":"tsc --noEmit -p tsconfig.json"},"_npmUser":{"name":"asafarim.be","email":"asafarim.it@gmail.com"},"_resolved":"/tmp/5493fa5d325d358fe2a630f558fb4521/asafarim-appsafe-0.1.0.tgz","_integrity":"sha512-gKrqNcc6kPC/9r2biukMrIiuTq8VMwjKRKXRByAEVc2PvfIzHlqS2R8P6SjdxWfpTYHwp7y2eao2wUNlTXko1w==","_npmVersion":"10.8.2","description":"Browser-native password-based AES-256-GCM encryption for files and data","directories":{},"sideEffects":false,"_nodeVersion":"20.20.2","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsx":"4.23.12","typescript":"7.0.2"},"_npmOperationalInternal":{"tmp":"tmp/appsafe_0.1.0_1787519614226_0.10719462975953897","host":"s3://npm-registry-packages-npm-production"}},"0.2.0":{"name":"@asafarim/appsafe","version":"0.2.0","license":"MIT","_id":"@asafarim/appsafe@0.2.0","maintainers":[{"name":"asafarim.be","email":"asafarim.it@gmail.com"}],"homepage":"https://alisafari-it.github.io/asafarim-appsafe/","dist":{"shasum":"81574ac3ff740e6f5e4cc94f29dbffbdbbf068b0","tarball":"https://registry.npmjs.org/@asafarim/appsafe/-/appsafe-0.2.0.tgz","fileCount":6,"integrity":"sha512-N9aq3YzEbHNaIQiS5/hhAWUDt5Ie8hsiPfk4ZYUG1W4ZujSJrRPaCajZXDCCn6pIyyg/IyZtGCR8nq8B+XLqaQ==","signatures":[{"sig":"MEUCIGjiKrBsUhH41s9OFhQWhBtQRrTxwrgHHwj1UO4kZoKwAiEAreSOTfUzwHkFJ9ox/5OaUDfvVvKAOLvT29JBPlXXGQI=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":13693},"main":"./dist/index.js","type":"module","_from":"file:asafarim-appsafe-0.2.0.tgz","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=20.0.0"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","default":"./dist/index.js"}},"scripts":{"test":"tsx --test test/*.test.ts","build":"tsc -p tsconfig.json","check":"tsc --noEmit -p tsconfig.json"},"_npmUser":{"name":"asafarim.be","email":"asafarim.it@gmail.com"},"_resolved":"/tmp/8619c3824c050612bd0c2e6114b50308/asafarim-appsafe-0.2.0.tgz","_integrity":"sha512-N9aq3YzEbHNaIQiS5/hhAWUDt5Ie8hsiPfk4ZYUG1W4ZujSJrRPaCajZXDCCn6pIyyg/IyZtGCR8nq8B+XLqaQ==","_npmVersion":"10.8.2","description":"Browser-native password-based AES-256-GCM encryption for files and data","directories":{},"sideEffects":false,"_nodeVersion":"20.20.2","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsx":"4.23.12","typescript":"7.0.2"},"_npmOperationalInternal":{"tmp":"tmp/appsafe_0.2.0_1787520196645_0.03919658231741452","host":"s3://npm-registry-packages-npm-production"}},"0.2.1":{"name":"@asafarim/appsafe","version":"0.2.1","license":"MIT","_id":"@asafarim/appsafe@0.2.1","maintainers":[{"name":"asafarim.be","email":"asafarim.it@gmail.com"}],"homepage":"https://alisafari-it.github.io/asafarim-appsafe/","dist":{"shasum":"971c0844db11acd1c1d2228bc0e6dcb93bd8fd4c","tarball":"https://registry.npmjs.org/@asafarim/appsafe/-/appsafe-0.2.1.tgz","fileCount":6,"integrity":"sha512-b08CXPJahcxISnWT/v/bbasxOGYBiYstvIAbSYA2KAfjq4+LQY4C+kFg7UWXRvfQXd8pChdsF5QqcFe8L8v5mQ==","signatures":[{"sig":"MEUCICXr+pCRFf9bmI9eZ6mET7itff1yND9seTbK1xuIlDiqAiEAgLI+450ZwF8FnI6oXhjVZFfTJWbvJatA3ClRrqSO414=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":13693},"main":"./dist/index.js","type":"module","_from":"file:asafarim-appsafe-0.2.1.tgz","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=20.0.0"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","default":"./dist/index.js"}},"scripts":{"test":"tsx --test test/*.test.ts","build":"tsc -p tsconfig.json","check":"tsc --noEmit -p tsconfig.json"},"_npmUser":{"name":"asafarim.be","email":"asafarim.it@gmail.com"},"_resolved":"/tmp/2e479ba03e2266c5859acfb16125c6c8/asafarim-appsafe-0.2.1.tgz","_integrity":"sha512-b08CXPJahcxISnWT/v/bbasxOGYBiYstvIAbSYA2KAfjq4+LQY4C+kFg7UWXRvfQXd8pChdsF5QqcFe8L8v5mQ==","_npmVersion":"10.8.2","description":"Browser-native password-based AES-256-GCM encryption for files and data","directories":{},"sideEffects":false,"_nodeVersion":"20.20.2","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsx":"4.23.12","typescript":"7.0.2"},"_npmOperationalInternal":{"tmp":"tmp/appsafe_0.2.1_1787521074296_0.12290767293320548","host":"s3://npm-registry-packages-npm-production"}},"0.3.0":{"name":"@asafarim/appsafe","version":"0.3.0","license":"MIT","_id":"@asafarim/appsafe@0.3.0","maintainers":[{"name":"asafarim.be","email":"asafarim.it@gmail.com"}],"homepage":"https://alisafari-it.github.io/asafarim-appsafe/","dist":{"shasum":"63f78c04f1cba8dd1de14ac98d91d8487bf6dfc2","tarball":"https://registry.npmjs.org/@asafarim/appsafe/-/appsafe-0.3.0.tgz","fileCount":6,"integrity":"sha512-ejMd0PgXMs2cKr9xkhU+qgM6TWYmLGe2lBAK5paVxxYspaYbVCBOm22T9m23q2SBhpbTlfw/E5UWsVxvZ/ppFA==","signatures":[{"sig":"MEUCIARnK2XEAx0cqHrmuunjucPDb1dP7slFRPyR3fb/fWUGAiEA29vYgnmN5BlGwTpQpP3EwEgfm666cE6wL0Ta4iSVbog=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":13864},"main":"./dist/index.js","type":"module","_from":"file:asafarim-appsafe-0.3.0.tgz","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=20.0.0"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","default":"./dist/index.js"}},"scripts":{"test":"tsx --test test/*.test.ts","build":"tsc -p tsconfig.json","check":"tsc --noEmit -p tsconfig.json"},"_npmUser":{"name":"asafarim.be","email":"asafarim.it@gmail.com"},"_resolved":"/tmp/11ffbb39b07aed4f7d4eb9fd5161e950/asafarim-appsafe-0.3.0.tgz","_integrity":"sha512-ejMd0PgXMs2cKr9xkhU+qgM6TWYmLGe2lBAK5paVxxYspaYbVCBOm22T9m23q2SBhpbTlfw/E5UWsVxvZ/ppFA==","_npmVersion":"10.8.2","description":"Browser-native password-based AES-256-GCM encryption for files and data","directories":{},"sideEffects":false,"_nodeVersion":"20.20.2","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsx":"4.23.12","typescript":"7.0.2"},"_npmOperationalInternal":{"tmp":"tmp/appsafe_0.3.0_1787521499461_0.30460046489541504","host":"s3://npm-registry-packages-npm-production"}},"0.3.1":{"name":"@asafarim/appsafe","version":"0.3.1","license":"MIT","_id":"@asafarim/appsafe@0.3.1","maintainers":[{"name":"asafarim.be","email":"asafarim.it@gmail.com"}],"homepage":"https://alisafari-it.github.io/asafarim-appsafe/","dist":{"shasum":"621dfa90551131148f7efcb5b1ad91cbfff555cc","tarball":"https://registry.npmjs.org/@asafarim/appsafe/-/appsafe-0.3.1.tgz","fileCount":6,"integrity":"sha512-dwbG0CMcXHBFEWxecpf6Afvt6UmSxPSLxdzGHndVEn2ZWLI9HpKOr4eDKdM2KfrVNCKoUplcqkOZ6AJrQS29uw==","signatures":[{"sig":"MEYCIQCoMbQDIVugW2RtCD2EvR/bnUimqdi40btB19okEXkVywIhALdxllEzKWdaVtqiaV2jLjWK+jOggtlcrAUZ72uInYA9","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":13864},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=20.0.0"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","default":"./dist/index.js"}},"scripts":{"test":"tsx --test test/*.test.ts","build":"tsc -p tsconfig.json","check":"tsc --noEmit -p tsconfig.json"},"_npmUser":{"name":"asafarim.be","email":"asafarim.it@gmail.com"},"description":"Browser-native password-based AES-256-GCM encryption for files and data","directories":{},"sideEffects":false,"_nodeVersion":"22.23.2","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsx":"4.23.12","typescript":"7.0.2"},"_npmOperationalInternal":{"tmp":"tmp/appsafe_0.3.1_1787605027643_0.06068583798494398","host":"s3://npm-registry-packages-npm-production"}},"0.4.0":{"name":"@asafarim/appsafe","version":"0.4.0","license":"MIT","_id":"@asafarim/appsafe@0.4.0","maintainers":[{"name":"asafarim.be","email":"asafarim.it@gmail.com"}],"homepage":"https://alisafari-it.github.io/asafarim-appsafe/","dist":{"shasum":"b650edad4f47bb72a339ad610953c751fb6e873b","tarball":"https://registry.npmjs.org/@asafarim/appsafe/-/appsafe-0.4.0.tgz","fileCount":6,"integrity":"sha512-QWbivuCEStylAeY+4+DScExl3pwZF3TevVE4vFPGCSbG1rZGoyRtmDECwPzRXG0lzjof36renK3zOEENkYSWfQ==","signatures":[{"sig":"MEUCIQDWS6G32Xk9ROj2oIY1GY4ZFvpwidQcdlaCaOWbOPkuJwIgGIxdWqL0sr2za26Jmdjw0+bOQm+Ib1drOG/l2LduWJI=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":13864},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=20.0.0"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","default":"./dist/index.js"}},"scripts":{"test":"tsx --test test/*.test.ts","build":"tsc -p tsconfig.json","check":"tsc --noEmit -p tsconfig.json"},"_npmUser":{"name":"asafarim.be","email":"asafarim.it@gmail.com"},"description":"Browser-native password-based AES-256-GCM encryption for files and data","directories":{},"sideEffects":false,"_nodeVersion":"22.23.2","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsx":"4.23.12","typescript":"7.0.2"},"_npmOperationalInternal":{"tmp":"tmp/appsafe_0.4.0_1787690606441_0.3381598787039102","host":"s3://npm-registry-packages-npm-production"}},"0.5.0":{"_id":"@asafarim/appsafe@0.5.0","dist":{"shasum":"57a17b97758f66f3d9a36fa0ac2f8050b233ff0c","tarball":"https://registry.npmjs.org/@asafarim/appsafe/-/appsafe-0.5.0.tgz","fileCount":6,"integrity":"sha512-XBZfv3w/XDFxfWF1lWLKtrF2eZ+maMvP1k9RokOe8E6padRaPS0DdH8glUs7U/ou5rMtdei6GoYyE4srSjj1ng==","signatures":[{"sig":"MEUCIQDDyIc5aPP8IZ0zd0GC1vIEgwfvRBYNQMMsYFZcoCOMvwIgNK03NiALr0DNH+PY7JN0vLtYZJ9AC4WNntRR0pQHKHo=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCIBf+Q/DzajeywwMYGysI/p4xpKTURVP5RAgLJg6AITaKAiB9ExBnjgS+kJxeSMV0Ah30yePjSPwZMj3A2EGhhLW0wQ=="}],"unpackedSize":33629},"main":"./dist/index.js","name":"@asafarim/appsafe","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=20.0.0"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","default":"./dist/index.js"}},"license":"MIT","scripts":{"test":"tsx --test test/*.test.ts","build":"tsc -p tsconfig.json","check":"tsc --noEmit -p tsconfig.json"},"version":"0.5.0","_npmUser":{"name":"asafarim.be","email":"asafarim.it@gmail.com"},"homepage":"https://alisafari-it.github.io/asafarim-appsafe/","description":"Browser-native AES-256-GCM encryption for files and data with password or public-key modes","directories":{},"maintainers":[{"name":"asafarim.be","email":"asafarim.it@gmail.com"}],"sideEffects":false,"_nodeVersion":"22.23.2","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsx":"4.23.12","typescript":"7.0.2"},"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/appsafe_0.5.0_1790537916861_0.62079665995148"}}},"time":{"created":"2026-08-23T21:13:34.089Z","modified":"2026-09-27T19:38:37.132Z","0.1.0":"2026-08-23T21:13:34.370Z","0.2.0":"2026-08-23T21:23:16.785Z","0.2.1":"2026-08-23T21:37:54.449Z","0.3.0":"2026-08-23T21:44:59.586Z","0.3.1":"2026-08-24T20:57:07.803Z","0.4.0":"2026-08-25T20:43:26.589Z","0.5.0":"2026-09-27T19:38:36.959Z"},"license":"MIT","homepage":"https://alisafari-it.github.io/asafarim-appsafe/","description":"Browser-native AES-256-GCM encryption for files and data with password or public-key modes","maintainers":[{"name":"asafarim.be","email":"asafarim.it@gmail.com"}],"readme":"# @asafarim/appsafe\n\nA small browser-first encryption package built on the Web Crypto API. It encrypts arbitrary bytes or UTF-8 text with AES-256-GCM using one of two selectable methods:\n\n- **Password mode** — the key is derived from a user-supplied password with PBKDF2-HMAC-SHA-256. The same password encrypts and decrypts.\n- **Public-key mode** — data is encrypted for one or more recipient public keys (ECDH P-256 + HKDF-SHA-256 key wrapping). Only a matching private key decrypts.\n\nPasswords, private keys, and plaintext stay in the calling runtime; this package performs no network requests.\n\n## Install\n\n```bash\npnpm add @asafarim/appsafe\n# or\nnpm install @asafarim/appsafe\n```\n\nRequires a runtime that provides `globalThis.crypto.subtle` with AES-GCM, PBKDF2, HKDF, and ECDH P-256: all evergreen browsers, Node 20+, and Deno. Both methods, including key generation, work in the browser.\n\n## Choosing a method\n\n| Concern | Password mode | Public-key mode |\n| --- | --- | --- |\n| Who can encrypt | Anyone with the password | Anyone with a recipient public key |\n| Who can decrypt | Anyone with the password | Only holders of a listed private key |\n| Secret material | The password | Private keys only |\n| Safe to commit | Ciphertext | Ciphertext and public keys |\n| Multiple people | Everyone shares one secret | Up to 16 recipients per payload, each with their own key |\n| CI / deployment | Inject the password; the job can also re-encrypt | Encrypt-only jobs need no secret; decrypting jobs receive a private key |\n| Rotation | Choose a new password and re-encrypt every artifact | Generate a new key pair, re-encrypt for it, verify, then retire the old key |\n| Loss of the secret | Unrecoverable | Unrecoverable unless another listed recipient's key still works |\n\nUse password mode for personal workflows and quick sharing. Use public-key mode for teams, CI, and private application sources committed as ciphertext.\n\n## Password API\n\n### `encryptBytes(input, password, options?): Promise<Uint8Array>`\n\nEncrypts an `ArrayBuffer` or `Uint8Array` and returns a self-describing binary payload.\n\n### `decryptBytes(input, password): Promise<Uint8Array>`\n\nDecrypts a payload produced by `encryptBytes`. Throws `INVALID_PASSWORD_OR_DATA` if the password or payload is wrong, and `MODE_MISMATCH` for a public-key payload.\n\n### `encryptText(input, password, options?)` / `decryptText(input, password)`\n\nUTF-8 wrappers around the byte functions. `decryptText` throws `INVALID_TEXT` if the decrypted bytes are not valid UTF-8.\n\n### `EncryptOptions` and `DEFAULT_PBKDF2_ITERATIONS`\n\n```ts\ninterface EncryptOptions {\n  iterations?: number; // default 600_000; must be within [100_000, 2_000_000]\n}\n```\n\n## Public-key API\n\n### `generateKeyPair(): Promise<AppSafeKeyPair>`\n\nGenerates a P-256 key pair in memory.\n\n```ts\ninterface AppSafeKeyPair {\n  publicKey: string;   // \"appsafe-pub-p256:…\"          safe to share and commit\n  privateKey: string;  // \"APPSAFE-PRIVATE-KEY-P256:…\"  secret\n  fingerprint: string; // 32 hex characters identifying the public key\n}\n```\n\n### `encryptBytesForRecipients(input, recipients): Promise<Uint8Array>`\n\nEncrypts for one public key or an array of 1–16 public keys. Duplicate keys are ignored. A fresh random content key encrypts the data once, and a separately wrapped copy of that key is stored for each recipient.\n\n### `decryptBytesWithPrivateKey(input, privateKey): Promise<Uint8Array>`\n\nDecrypts a public-key payload. Throws `NO_MATCHING_KEY` if the private key is not a recipient, `INVALID_KEY_OR_DATA` if unwrapping or authentication fails, and `MODE_MISMATCH` for a password payload.\n\n### `encryptTextForRecipients(input, recipients)` / `decryptTextWithPrivateKey(input, privateKey)`\n\nUTF-8 wrappers around the public-key byte functions.\n\n### `getPublicKey(privateKey): string` and `getKeyFingerprint(publicKey): Promise<string>`\n\nDerive the public key from a private key, and compute a public key's fingerprint. Fingerprints are public identifiers and never reveal private material. `getKeyFingerprint` rejects private keys so a private key cannot be mistaken for a public one.\n\n## Payload inspection\n\n### `inspectAppSafePayload(input): AppSafePayloadInfo`\n\nParses and validates the header without decrypting. Throws `INVALID_PAYLOAD` for malformed input.\n\n```ts\ntype AppSafePayloadInfo =\n  | { mode: \"password\"; version: 1; iterations: number }\n  | {\n      mode: \"public-key\";\n      version: 2;\n      algorithm: \"ECDH-P256+HKDF-SHA256+A256GCM\";\n      recipients: string[]; // recipient fingerprints\n    };\n```\n\n### `isAppSafePayload(input): boolean`\n\nReturns `true` when `inspectAppSafePayload` would succeed.\n\n## Errors\n\nEvery function throws `AppSafeCryptoError` with a typed `code`. Messages never contain passwords, keys, or plaintext.\n\n| Code | Meaning |\n| --- | --- |\n| `EMPTY_PASSWORD` | No password was provided. |\n| `INVALID_OPTIONS` | Iterations are out of range, or the recipient count is not 1–16. |\n| `INVALID_PAYLOAD` | The input is not a supported, complete AppSafe payload. |\n| `INVALID_PASSWORD_OR_DATA` | Password-mode authentication failed. |\n| `INVALID_KEY` | A key is malformed, not on the curve, or the wrong kind (public vs private). |\n| `NO_MATCHING_KEY` | The private key is not a recipient of the payload. |\n| `INVALID_KEY_OR_DATA` | Public-key unwrapping or authentication failed. |\n| `MODE_MISMATCH` | A password was used on a public-key payload, or vice versa. |\n| `INVALID_TEXT` | Decrypted bytes are not valid UTF-8. |\n| `UNSUPPORTED_RUNTIME` | The runtime lacks the required Web Crypto support. |\n\n## Recipes\n\n### Password text round-trip\n\n```ts\nimport { decryptText, encryptText } from \"@asafarim/appsafe\";\n\nconst encrypted = await encryptText(\"private note\", password);\nconst plaintext = await decryptText(encrypted, password);\n```\n\n### Password file round-trip\n\n```ts\nimport { decryptBytes, encryptBytes } from \"@asafarim/appsafe\";\n\nconst input = new Uint8Array(await file.arrayBuffer());\nconst payload = await encryptBytes(input, password);\nconst original = await decryptBytes(payload, password);\n\nconst blob = new Blob([payload], { type: \"application/octet-stream\" });\n```\n\n### Public-key round-trip\n\n```ts\nimport {\n  decryptBytesWithPrivateKey,\n  encryptBytesForRecipients,\n  generateKeyPair,\n} from \"@asafarim/appsafe\";\n\nconst { publicKey, privateKey } = await generateKeyPair();\nconst payload = await encryptBytesForRecipients(input, publicKey);\nconst original = await decryptBytesWithPrivateKey(payload, privateKey);\n```\n\n### Multiple recipients\n\n```ts\nimport { encryptTextForRecipients } from \"@asafarim/appsafe\";\n\nconst payload = await encryptTextForRecipients(note, [ownerPublicKey, deployPublicKey]);\n```\n\n### Detect the mode and handle failures\n\n```ts\nimport {\n  AppSafeCryptoError,\n  decryptBytes,\n  decryptBytesWithPrivateKey,\n  inspectAppSafePayload,\n} from \"@asafarim/appsafe\";\n\ntry {\n  const info = inspectAppSafePayload(payload);\n  const plaintext = info.mode === \"password\"\n    ? await decryptBytes(payload, password)\n    : await decryptBytesWithPrivateKey(payload, privateKey);\n} catch (error) {\n  if (error instanceof AppSafeCryptoError) {\n    console.warn(error.code);\n  }\n}\n```\n\n## Payload formats\n\nBoth formats begin with the `ASAFE` magic and a version byte, so decryption selects the method from the payload itself rather than guessing.\n\n### Version 1 — password\n\n```\n[ \"ASAFE\" (5) ][ 0x01 (1) ][ salt (16) ][ iv (12) ][ iterations (4, big-endian) ][ ciphertext + GCM tag ]\n```\n\nThe full header is AES-GCM additional data. Version-1 payloads created by earlier releases remain fully supported.\n\n### Version 2 — public key\n\n```\n[ \"ASAFE\" (5) ][ 0x02 (1) ][ algorithm 0x01 (1) ][ recipient count (1) ][ content iv (12) ]\n[ recipient stanza × count ][ ciphertext + GCM tag ]\n\nrecipient stanza (141 bytes):\n[ key id (16) ][ ephemeral P-256 public key (65) ][ wrap iv (12) ][ wrapped content key + tag (48) ]\n```\n\nFor each recipient:\n\n1. Generate an ephemeral P-256 key pair and compute ECDH with the recipient public key.\n2. Derive a wrap key with HKDF-SHA-256 (salt: ephemeral public key ‖ recipient public key; info: `AppSafe v2 ECDH-P256 HKDF-SHA-256 A256GCM key wrap`).\n3. Wrap the random 256-bit content key with AES-256-GCM, authenticating the header prefix, key id, and ephemeral public key.\n\nThe content is encrypted once with AES-256-GCM, and the entire header — version, algorithm, recipient count, IV, and every stanza — is its additional data. The key id is the first 16 bytes of SHA-256 over the recipient's uncompressed public key; it is displayed as the key fingerprint.\n\nThe construction follows the ECIES / JWE `ECDH-ES+A256KW` pattern using only standard Web Crypto primitives. It uses the same recipient/identity model as [Age](https://age-encryption.org/), but it is **not Age-compatible**: Age relies on X25519 and ChaCha20-Poly1305, which are not uniformly available through Web Crypto.\n\n### Key text format\n\n```\nappsafe-pub-p256:<base64url of the 65-byte uncompressed public point>\nAPPSAFE-PRIVATE-KEY-P256:<base64url of the public point followed by the 32-byte private scalar>\n```\n\nLeading and trailing whitespace is ignored.\n\n## Security notes\n\n- Uses standardized primitives already implemented by browsers — no custom cryptography.\n- Every operation uses fresh random salts, IVs, content keys, and ephemeral keys.\n- Wrong passwords, wrong private keys, truncated payloads, and any modification to the header or ciphertext fail closed with a typed error.\n- The package never reads files, stores keys, creates downloads, or makes network requests. Key storage is the caller's responsibility; the [`@asafarim/appsafe-cli`](../appsafe-cli) package manages key files.\n- Losing the password, or every private key a payload was encrypted for, makes that payload unrecoverable. Back up private keys and encrypt for a second recovery key where appropriate.\n- The PBKDF2 work factor can be raised via `EncryptOptions.iterations` (max 2,000,000).\n\n## License\n\nMIT.\n","readmeFilename":""}