diff --git a/.github/workflows/node.yaml b/.github/workflows/node.yaml new file mode 100644 index 0000000..a59ab70 --- /dev/null +++ b/.github/workflows/node.yaml @@ -0,0 +1,145 @@ +name: Node + +on: + push: + tags: ["v*"] + pull_request: + paths: + - "node/**" + - ".github/workflows/node.yaml" + +env: + CARGO_TERM_COLOR: always + +jobs: + lint: + name: Lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + node-version: "22" + - name: Lint + working-directory: node + run: | + npm install + npx biome check + + test: + name: Test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + - uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable + - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + node-version: "22" + + - name: Build release binary (for test fixture) + run: cargo build --release + + - name: Install and test + working-directory: node + run: | + npm install + npx napi build --platform --release + node __test__/index.test.mjs + + build: + name: Build (${{ matrix.settings.target }}) + needs: [lint, test] + runs-on: ${{ matrix.settings.host }} + strategy: + fail-fast: false + matrix: + settings: + - host: macos-latest + target: aarch64-apple-darwin + - host: macos-14 + target: x86_64-apple-darwin + - host: ubuntu-latest + target: x86_64-unknown-linux-gnu + - host: ubuntu-latest + target: aarch64-unknown-linux-gnu + docker: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian-aarch64 + - host: ubuntu-latest + target: x86_64-unknown-linux-musl + docker: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-alpine + - host: windows-latest + target: x86_64-pc-windows-msvc + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + if: ${{ !matrix.settings.docker }} + with: + node-version: "22" + + - uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable + if: ${{ !matrix.settings.docker }} + with: + targets: ${{ matrix.settings.target }} + + - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2 + if: ${{ !matrix.settings.docker }} + + - name: Install dependencies + working-directory: node + run: npm install + + - name: Build (native) + if: ${{ !matrix.settings.docker }} + working-directory: node + run: npx napi build --platform --release --target ${{ matrix.settings.target }} + + - name: Build (docker) + if: ${{ matrix.settings.docker }} + uses: addnab/docker-run-action@4f65fabd2431ebc8d299f8e5a018d79a769ae185 # v3 + with: + image: ${{ matrix.settings.docker }} + options: --user 0:0 -v ${{ github.workspace }}:/build -w /build/node + run: | + apk add --no-cache curl || true + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable + export PATH="$HOME/.cargo/bin:$PATH" + rustc --version + npx napi build --platform --release --target ${{ matrix.settings.target }} + + - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + with: + name: bindings-${{ matrix.settings.target }} + path: node/*.node + if-no-files-found: error + + publish: + name: Publish to npm + if: startsWith(github.ref, 'refs/tags/v') + needs: build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + node-version: "22" + registry-url: "https://registry.npmjs.org" + + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + pattern: bindings-* + merge-multiple: true + path: node/ + + - name: List artifacts + run: ls -la node/*.node + + - name: Publish + working-directory: node + run: | + npm install + npx napi prepublish -t npm --skip-gh-release + npm publish --access public + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.gitignore b/.gitignore index 60fdc3c..755bf7e 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,7 @@ CLAUDE.md __pycache__/ *.pyc .pytest_cache/ +node/*.node +node/index.js +node/index.d.ts +node/node_modules/ diff --git a/Cargo.lock b/Cargo.lock index 718a4b3..13eeb53 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -393,9 +393,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.57" +version = "1.2.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423" +checksum = "e1e928d4b69e3077709075a938a05ffbedfa53a84c8f766efbf8220bb1ff60e1" dependencies = [ "find-msvc-tools", "shlex", @@ -546,6 +546,15 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" +[[package]] +name = "convert_case" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "cookie-factory" version = "0.3.3" @@ -633,6 +642,16 @@ dependencies = [ "hybrid-array", ] +[[package]] +name = "ctor" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" +dependencies = [ + "quote", + "syn", +] + [[package]] name = "ctr" version = "0.9.2" @@ -1249,9 +1268,9 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "js-sys" -version = "0.3.91" +version = "0.3.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" +checksum = "cc4c90f45aa2e6eacbe8645f77fdea542ac97a494bcd117a67df9ff4d611f995" dependencies = [ "once_cell", "wasm-bindgen", @@ -1278,6 +1297,16 @@ version = "0.2.183" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + [[package]] name = "libm" version = "0.2.16" @@ -1355,6 +1384,73 @@ dependencies = [ "ureq", ] +[[package]] +name = "murk-napi" +version = "0.4.1" +dependencies = [ + "murk-cli", + "napi", + "napi-build", + "napi-derive", +] + +[[package]] +name = "napi" +version = "2.16.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55740c4ae1d8696773c78fdafd5d0e5fe9bc9f1b071c7ba493ba5c413a9184f3" +dependencies = [ + "bitflags", + "ctor", + "napi-derive", + "napi-sys", + "once_cell", +] + +[[package]] +name = "napi-build" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d376940fd5b723c6893cd1ee3f33abbfd86acb1cd1ec079f3ab04a2a3bc4d3b1" + +[[package]] +name = "napi-derive" +version = "2.16.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cbe2585d8ac223f7d34f13701434b9d5f4eb9c332cccce8dee57ea18ab8ab0c" +dependencies = [ + "cfg-if", + "convert_case", + "napi-derive-backend", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "napi-derive-backend" +version = "1.0.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1639aaa9eeb76e91c6ae66da8ce3e89e921cd3885e99ec85f4abacae72fc91bf" +dependencies = [ + "convert_case", + "once_cell", + "proc-macro2", + "quote", + "regex", + "semver", + "syn", +] + +[[package]] +name = "napi-sys" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "427802e8ec3a734331fec1035594a210ce1ff4dc5bc1950530920ab717964ea3" +dependencies = [ + "libloading", +] + [[package]] name = "nom" version = "7.1.3" @@ -1885,9 +1981,9 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "rustc-hash" -version = "2.1.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" [[package]] name = "rustc_version" @@ -2100,9 +2196,9 @@ dependencies = [ [[package]] name = "simd-adler32" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" [[package]] name = "slab" @@ -2241,7 +2337,7 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb30dbbd9036155e74adad6812e9898d03ec374946234fbcebd5dfc7b9187b90" dependencies = [ - "rustc-hash 2.1.1", + "rustc-hash 2.1.2", ] [[package]] @@ -2284,6 +2380,12 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "unicode-segmentation" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" + [[package]] name = "unicode-xid" version = "0.2.6" @@ -2398,9 +2500,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.114" +version = "0.2.115" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" +checksum = "6523d69017b7633e396a89c5efab138161ed5aafcbc8d3e5c5a42ae38f50495a" dependencies = [ "cfg-if", "once_cell", @@ -2411,9 +2513,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.114" +version = "0.2.115" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" +checksum = "4e3a6c758eb2f701ed3d052ff5737f5bfe6614326ea7f3bbac7156192dc32e67" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2421,9 +2523,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.114" +version = "0.2.115" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" +checksum = "921de2737904886b52bcbb237301552d05969a6f9c40d261eb0533c8b055fedf" dependencies = [ "bumpalo", "proc-macro2", @@ -2434,9 +2536,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.114" +version = "0.2.115" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" +checksum = "a93e946af942b58934c604527337bad9ae33ba1d5c6900bbb41c2c07c2364a93" dependencies = [ "unicode-ident", ] diff --git a/Cargo.toml b/Cargo.toml index 3d7cacb..a322741 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,6 @@ +[workspace] +members = [".", "node"] + [package] name = "murk-cli" version = "0.4.1" diff --git a/hooks/pre-commit b/hooks/pre-commit new file mode 100755 index 0000000..216b660 --- /dev/null +++ b/hooks/pre-commit @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +# Pre-commit hook: check formatting and linting before commit. +# Install: cp hooks/pre-commit .git/hooks/pre-commit && chmod +x .git/hooks/pre-commit +# Or: git config core.hooksPath hooks + +set -e + +# Rust formatting +if git diff --cached --name-only | grep -qE '\.rs$'; then + printf " %-12s" "rust fmt" + cargo fmt --check 2>&1 | head -20 + if [ ${PIPESTATUS[0]} -ne 0 ]; then + echo "pre-commit: formatting check failed. Run 'cargo fmt' and try again." + exit 1 + fi + echo "ok" +fi + +# Python linting +if git diff --cached --name-only | grep -qE 'python/|\.py$'; then + if command -v ruff >/dev/null 2>&1; then + printf " %-12s" "ruff" + ruff check python/ && echo "ok" || { + echo "pre-commit: ruff check failed. Run 'ruff check --fix python/' and try again." + exit 1 + } + fi +fi + +# Node linting +if git diff --cached --name-only | grep -qE 'node/'; then + if [ -f node/node_modules/.bin/biome ]; then + printf " %-12s" "biome" + (cd node && npx biome check) && echo "ok" || { + echo "pre-commit: biome check failed. Run 'cd node && npm run format' and try again." + exit 1 + } + fi +fi diff --git a/node/Cargo.toml b/node/Cargo.toml new file mode 100644 index 0000000..6e0c243 --- /dev/null +++ b/node/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "murk-napi" +version = "0.4.1" +edition = "2024" +license = "MIT OR Apache-2.0" +publish = false + +[lib] +crate-type = ["cdylib"] + +[dependencies] +murk-cli = { path = ".." } +napi = { version = "2", default-features = false, features = ["napi9"] } +napi-derive = "2" + +[build-dependencies] +napi-build = "2" diff --git a/node/README.md b/node/README.md new file mode 100644 index 0000000..59dd242 --- /dev/null +++ b/node/README.md @@ -0,0 +1,90 @@ +# @iicky/murk-secrets + +[![npm](https://img.shields.io/npm/v/@iicky/murk-secrets)](https://www.npmjs.com/package/@iicky/murk-secrets) + +Node.js/TypeScript bindings for [murk](https://github.com/iicky/murk) — an encrypted secrets manager for developers. + +## Prerequisites + +You need the [murk CLI](https://github.com/iicky/murk) to create and manage vaults. This package only reads them. + +```bash +# Install the CLI first +brew tap iicky/murk && brew install murk + +# Initialize a vault and add secrets +murk init +murk add DATABASE_URL +murk add API_KEY +``` + +Then add the Node package to your project: + +```bash +npm install @iicky/murk-secrets +``` + +## Quick start + +```bash +# Load your key (created by murk init) +source .env +``` + +```typescript +import { load, get, exportAll } from '@iicky/murk-secrets' + +// Load the vault (reads MURK_KEY from environment) +const vault = load() + +// Get a single secret +const dbUrl = vault.get('DATABASE_URL') + +// Get all secrets as an object +const secrets = vault.export() + +// One-liners +get('DATABASE_URL') +exportAll() +``` + +## API + +### `load(vaultPath?: string): Vault` + +Load and decrypt a murk vault. Reads `MURK_KEY` or `MURK_KEY_FILE` from the environment. + +### `get(key: string, vaultPath?: string): string | null` + +One-liner: load the vault and get a single value. + +### `exportAll(vaultPath?: string): Record` + +One-liner: load the vault and export all secrets as an object. + +### `hasKey(): boolean` + +Check if a `MURK_KEY` is available in the environment. + +### `Vault` + +| Method | Returns | Description | +|--------|---------|-------------| +| `vault.get(key)` | `string \| null` | Get a single decrypted value | +| `vault.export()` | `Record` | All secrets as an object | +| `vault.keys()` | `string[]` | List of key names | +| `vault.has(key)` | `boolean` | Check if a key exists | +| `vault.length` | `number` | Number of secrets | + +Scoped (per-user) overrides are applied automatically — if you have a scoped value for a key, it takes priority over the shared value. + +## Requirements + +- Node.js >= 16 +- [murk CLI](https://github.com/iicky/murk) installed (to create and manage vaults) +- A `.murk` vault file in your project (created with `murk init`) +- `MURK_KEY` or `MURK_KEY_FILE` in the environment (created by `murk init`, loaded via `source .env`) + +## License + +MIT OR Apache-2.0 diff --git a/node/__test__/index.test.mjs b/node/__test__/index.test.mjs new file mode 100644 index 0000000..3c730df --- /dev/null +++ b/node/__test__/index.test.mjs @@ -0,0 +1,166 @@ +import assert from 'node:assert' +import { execSync } from 'node:child_process' +import { mkdtempSync, readFileSync, rmSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { exportAll, get, hasKey, load } from '../index.js' + +// Find the murk binary. +const murkBin = join(process.cwd(), '..', 'target', 'release', 'murk') + +function setupVault() { + const dir = mkdtempSync(join(tmpdir(), 'murk-node-test-')) + + const run = (cmd, input) => + execSync(cmd, { + cwd: dir, + input, + env: { + ...process.env, + PATH: `${join(process.cwd(), '..', 'target', 'release')}:${process.env.PATH}`, + }, + stdio: ['pipe', 'pipe', 'pipe'], + }) + + // Init vault. + run(`${murkBin} init --vault .murk`, 'testuser\n') + + // Read key from .env. + const dotenv = readFileSync(join(dir, '.env'), 'utf8') + let murkKey + for (const line of dotenv.split('\n')) { + if (line.startsWith('export MURK_KEY_FILE=')) { + const keyFile = line.split('=')[1].trim() + murkKey = readFileSync(keyFile, 'utf8').trim() + break + } + if (line.startsWith('export MURK_KEY=')) { + murkKey = line.split('=')[1].trim() + break + } + } + + // Add secrets. + const env = { ...process.env, MURK_KEY: murkKey } + execSync(`${murkBin} add DATABASE_URL --vault .murk`, { + cwd: dir, + input: 'postgres://localhost/mydb\n', + env, + stdio: ['pipe', 'pipe', 'pipe'], + }) + execSync(`${murkBin} add API_KEY --vault .murk`, { + cwd: dir, + input: 'sk-test-123\n', + env, + stdio: ['pipe', 'pipe', 'pipe'], + }) + execSync(`${murkBin} add STRIPE_SECRET --vault .murk`, { + cwd: dir, + input: 'sk_live_abc\n', + env, + stdio: ['pipe', 'pipe', 'pipe'], + }) + + return { dir, murkKey } +} + +let testDir, testKey + +// Setup +console.log('Setting up test vault...') +const setup = setupVault() +testDir = setup.dir +testKey = setup.murkKey +process.env.MURK_KEY = testKey +process.chdir(testDir) + +// Tests +let passed = 0 +let failed = 0 + +function test(name, fn) { + try { + fn() + console.log(` ✓ ${name}`) + passed++ + } catch (e) { + console.log(` ✗ ${name}: ${e.message}`) + failed++ + } +} + +console.log('\nRunning tests...\n') + +test('load returns a vault', () => { + const vault = load() + assert.ok(vault) +}) + +test('load with explicit path', () => { + const vault = load(join(testDir, '.murk')) + assert.ok(vault) +}) + +test('vault.get returns correct value', () => { + const vault = load() + assert.strictEqual(vault.get('DATABASE_URL'), 'postgres://localhost/mydb') + assert.strictEqual(vault.get('API_KEY'), 'sk-test-123') +}) + +test('vault.get returns null for missing key', () => { + const vault = load() + assert.strictEqual(vault.get('NONEXISTENT'), null) +}) + +test('vault.export returns all secrets', () => { + const vault = load() + const secrets = vault.export() + assert.strictEqual(secrets.DATABASE_URL, 'postgres://localhost/mydb') + assert.strictEqual(secrets.API_KEY, 'sk-test-123') + assert.strictEqual(secrets.STRIPE_SECRET, 'sk_live_abc') + assert.strictEqual(Object.keys(secrets).length, 3) +}) + +test('vault.keys returns all key names', () => { + const vault = load() + const keys = vault.keys().sort() + assert.deepStrictEqual(keys, ['API_KEY', 'DATABASE_URL', 'STRIPE_SECRET']) +}) + +test('vault.length returns count', () => { + const vault = load() + assert.strictEqual(vault.length, 3) +}) + +test('vault.has returns true for existing key', () => { + const vault = load() + assert.strictEqual(vault.has('DATABASE_URL'), true) + assert.strictEqual(vault.has('NONEXISTENT'), false) +}) + +test('get one-liner works', () => { + assert.strictEqual(get('DATABASE_URL'), 'postgres://localhost/mydb') +}) + +test('get one-liner returns null for missing', () => { + assert.strictEqual(get('NONEXISTENT'), null) +}) + +test('exportAll one-liner works', () => { + const secrets = exportAll() + assert.strictEqual(Object.keys(secrets).length, 3) +}) + +test('hasKey returns true when key set', () => { + assert.strictEqual(hasKey(), true) +}) + +test('load with missing vault throws', () => { + assert.throws(() => load('/nonexistent/.murk')) +}) + +// Cleanup +rmSync(testDir, { recursive: true, force: true }) + +console.log(`\n${passed} passed, ${failed} failed`) +if (failed > 0) process.exit(1) diff --git a/node/biome.json b/node/biome.json new file mode 100644 index 0000000..9c48bef --- /dev/null +++ b/node/biome.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://biomejs.dev/schemas/2.4.9/schema.json", + "formatter": { + "indentStyle": "space", + "indentWidth": 2, + "lineWidth": 100 + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true + } + }, + "javascript": { + "formatter": { + "quoteStyle": "single", + "semicolons": "asNeeded" + } + }, + "files": { + "includes": ["__test__/**"] + } +} diff --git a/node/build.rs b/node/build.rs new file mode 100644 index 0000000..9fc2367 --- /dev/null +++ b/node/build.rs @@ -0,0 +1,5 @@ +extern crate napi_build; + +fn main() { + napi_build::setup(); +} diff --git a/node/package-lock.json b/node/package-lock.json new file mode 100644 index 0000000..26ff052 --- /dev/null +++ b/node/package-lock.json @@ -0,0 +1,212 @@ +{ + "name": "@iicky/murk-secrets", + "version": "0.4.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@iicky/murk-secrets", + "version": "0.4.1", + "license": "MIT OR Apache-2.0", + "devDependencies": { + "@biomejs/biome": "^2.4.9", + "@napi-rs/cli": "^2" + }, + "engines": { + "node": ">= 16" + } + }, + "node_modules/@biomejs/biome": { + "version": "2.4.9", + "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.4.9.tgz", + "integrity": "sha512-wvZW92FrwitTcacvCBT8xdAbfbxWfDLwjYMmU3djjqQTh7Ni4ZdiWIT/x5VcZ+RQuxiKzIOzi5D+dcyJDFZMsA==", + "dev": true, + "license": "MIT OR Apache-2.0", + "bin": { + "biome": "bin/biome" + }, + "engines": { + "node": ">=14.21.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/biome" + }, + "optionalDependencies": { + "@biomejs/cli-darwin-arm64": "2.4.9", + "@biomejs/cli-darwin-x64": "2.4.9", + "@biomejs/cli-linux-arm64": "2.4.9", + "@biomejs/cli-linux-arm64-musl": "2.4.9", + "@biomejs/cli-linux-x64": "2.4.9", + "@biomejs/cli-linux-x64-musl": "2.4.9", + "@biomejs/cli-win32-arm64": "2.4.9", + "@biomejs/cli-win32-x64": "2.4.9" + } + }, + "node_modules/@biomejs/cli-darwin-arm64": { + "version": "2.4.9", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.4.9.tgz", + "integrity": "sha512-d5G8Gf2RpH5pYwiHLPA+UpG3G9TLQu4WM+VK6sfL7K68AmhcEQ9r+nkj/DvR/GYhYox6twsHUtmWWWIKfcfQQA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-darwin-x64": { + "version": "2.4.9", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.4.9.tgz", + "integrity": "sha512-LNCLNgqDMG7BLdc3a8aY/dwKPK7+R8/JXJoXjCvZh2gx8KseqBdFDKbhrr7HCWF8SzNhbTaALhTBoh/I6rf9lA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64": { + "version": "2.4.9", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.4.9.tgz", + "integrity": "sha512-4adnkAUi6K4C/emPRgYznMOcLlUqZdXWM6aIui4VP4LraE764g6Q4YguygnAUoxKjKIXIWPteKMgRbN0wsgwcg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64-musl": { + "version": "2.4.9", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.4.9.tgz", + "integrity": "sha512-8RCww5xnPn2wpK4L/QDGDOW0dq80uVWfppPxHIUg6mOs9B6gRmqPp32h1Ls3T8GnW8Wo5A8u7vpTwz4fExN+sw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64": { + "version": "2.4.9", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.4.9.tgz", + "integrity": "sha512-L10na7POF0Ks/cgLFNF1ZvIe+X4onLkTi5oP9hY+Rh60Q+7fWzKDDCeGyiHUFf1nGIa9dQOOUPGe2MyYg8nMSQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64-musl": { + "version": "2.4.9", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.4.9.tgz", + "integrity": "sha512-5TD+WS9v5vzXKzjetF0hgoaNFHMcpQeBUwKKVi3JbG1e9UCrFuUK3Gt185fyTzvRdwYkJJEMqglRPjmesmVv4A==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-arm64": { + "version": "2.4.9", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.4.9.tgz", + "integrity": "sha512-aDZr0RBC3sMGJOU10BvG7eZIlWLK/i51HRIfScE2lVhfts2dQTreowLiJJd+UYg/tHKxS470IbzpuKmd0MiD6g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-x64": { + "version": "2.4.9", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.4.9.tgz", + "integrity": "sha512-NS4g/2G9SoQ4ktKtz31pvyc/rmgzlcIDCGU/zWbmHJAqx6gcRj2gj5Q/guXhoWTzCUaQZDIqiCQXHS7BcGYc0w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@napi-rs/cli": { + "version": "2.18.4", + "resolved": "https://registry.npmjs.org/@napi-rs/cli/-/cli-2.18.4.tgz", + "integrity": "sha512-SgJeA4df9DE2iAEpr3M2H0OKl/yjtg1BnRI5/JyowS71tUWhrfSu2LT0V3vlHET+g1hBVlrO60PmEXwUEKp8Mg==", + "dev": true, + "license": "MIT", + "bin": { + "napi": "scripts/index.js" + }, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + } + } +} diff --git a/node/package.json b/node/package.json new file mode 100644 index 0000000..a6c4999 --- /dev/null +++ b/node/package.json @@ -0,0 +1,50 @@ +{ + "name": "@iicky/murk-secrets", + "version": "0.4.1", + "description": "Node.js/TypeScript bindings for murk — encrypted secrets manager", + "main": "index.js", + "types": "index.d.ts", + "license": "MIT OR Apache-2.0", + "repository": { + "type": "git", + "url": "https://github.com/iicky/murk" + }, + "keywords": [ + "secrets", + "encryption", + "age", + "dotenv", + "security", + "murk" + ], + "napi": { + "name": "murk-secrets", + "triples": { + "defaults": true, + "additional": [ + "aarch64-apple-darwin", + "aarch64-unknown-linux-gnu", + "aarch64-unknown-linux-musl" + ] + } + }, + "scripts": { + "artifacts": "napi artifacts", + "build": "napi build --platform --release", + "prepublishOnly": "napi prepublish -t npm", + "test": "node __test__/index.test.mjs", + "lint": "biome check", + "format": "biome check --write" + }, + "devDependencies": { + "@biomejs/biome": "^2.4.9", + "@napi-rs/cli": "^2" + }, + "engines": { + "node": ">= 16" + }, + "files": [ + "index.js", + "index.d.ts" + ] +} diff --git a/node/src/lib.rs b/node/src/lib.rs new file mode 100644 index 0000000..6b5e62d --- /dev/null +++ b/node/src/lib.rs @@ -0,0 +1,95 @@ +//! Node.js/TypeScript bindings for murk via napi-rs. +//! +//! ```typescript +//! import { load, get, exportAll, hasKey } from '@iicky/murk-secrets' +//! +//! const vault = load() // reads MURK_KEY from env, .murk from cwd +//! vault.get('DATABASE_URL') // decrypt a single value +//! vault.export() // Record of all secrets +//! get('DATABASE_URL') // one-liner convenience +//! ``` + +use std::collections::HashMap; + +use napi_derive::napi; + +/// A loaded and decrypted murk vault. +#[napi] +pub struct Vault { + vault: murk_cli::types::Vault, + murk: murk_cli::types::Murk, + pubkey: String, +} + +#[napi] +impl Vault { + /// Get a single decrypted secret value. + /// Returns the scoped override if one exists, otherwise the shared value. + #[napi] + pub fn get(&self, key: String) -> Option { + if let Some(value) = self.murk.scoped.get(&key).and_then(|m| m.get(&self.pubkey)) { + return Some(value.clone()); + } + self.murk.values.get(&key).cloned() + } + + /// Export all secrets as an object. Scoped values override shared values. + #[napi] + pub fn export(&self) -> HashMap { + murk_cli::resolve_secrets(&self.vault, &self.murk, &self.pubkey, &[]) + .into_iter() + .collect() + } + + /// List all key names. + #[napi] + pub fn keys(&self) -> Vec { + self.vault.schema.keys().cloned().collect() + } + + /// Number of secrets in the vault. + #[napi(getter)] + pub fn length(&self) -> u32 { + self.vault.schema.len() as u32 + } + + /// Check if a key exists. + #[napi] + pub fn has(&self, key: String) -> bool { + self.vault.schema.contains_key(&key) + } +} + +/// Load a murk vault. Reads MURK_KEY from the environment. +#[napi] +pub fn load(vault_path: Option) -> napi::Result { + let path = vault_path.as_deref().unwrap_or(".murk"); + let (vault, murk, identity) = + murk_cli::load_vault(path).map_err(|e| napi::Error::from_reason(e.to_string()))?; + let pubkey = identity + .pubkey_string() + .map_err(|e| napi::Error::from_reason(e.to_string()))?; + Ok(Vault { + vault, + murk, + pubkey, + }) +} + +/// One-liner: load the vault and get a single key. +#[napi] +pub fn get(key: String, vault_path: Option) -> napi::Result> { + Ok(load(vault_path)?.get(key)) +} + +/// One-liner: load the vault and export all secrets as an object. +#[napi] +pub fn export_all(vault_path: Option) -> napi::Result> { + Ok(load(vault_path)?.export()) +} + +/// Check if a MURK_KEY is available in the environment. +#[napi] +pub fn has_key() -> bool { + murk_cli::resolve_key().is_ok() +}