Skip to content

Commit b7e399b

Browse files
authored
Merge pull request #3 from git-stunts/deno-and-bun
Deno and bun
2 parents 6f23392 + 41e42f9 commit b7e399b

32 files changed

Lines changed: 1015 additions & 143 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
node_modules/
22
.DS_Store
33
.vite/
4+
*.tgz

ARCHITECTURE.md

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,18 @@ The core logic for managing secrets, independent of the underlying OS.
1010
- **Services**: `VaultService` orchestrates secret retrieval, storage, and resolution strategies (env vars vs vault).
1111
- **Errors**: Domain-specific errors (`VaultError`, `SecretNotFoundError`) to abstract low-level failures.
1212

13+
### Ports (`src/ports/`)
14+
Interfaces for the domain to talk to hosting platforms without knowing the details.
15+
16+
- **CommandRunnerPort**: Synchronous command execution (used by `KeychainAdapter`).
17+
1318
### Infrastructure Layer (`src/infrastructure/`)
1419
Adapters for external systems.
1520

16-
- **Adapters**: `KeychainAdapter` handles the specific OS commands (`security`, `secret-tool`, `PowerShell`) to interact with the native keychain.
21+
- **Adapters**: `KeychainAdapter` orchestrates platform-agnostic command flow while relying on injected ports.
22+
- **Node adapter**: `NodeCommandRunner` plus `createNodeKeychainAdapter` wire up the Node runtime (child_process, default platform detection).
23+
- **Bun adapter**: `BunCommandRunner`/`createBunKeychainAdapter` execute commands via `Bun.spawnSync` when Bun is detected.
24+
- **Deno adapter**: `DenoCommandRunner`/`createDenoKeychainAdapter` rely on `Deno.Command` so the same domain logic can run inside Deno.
1725

1826
## 📂 Directory Structure
1927

@@ -22,8 +30,13 @@ src/
2230
├── domain/
2331
│ ├── errors/ # VaultError, etc.
2432
│ └── services/ # VaultService
33+
├── ports/ # CommandRunnerPort
2534
└── infrastructure/
26-
└── adapters/ # KeychainAdapter
35+
└── adapters/
36+
├── KeychainAdapter.js
37+
├── node/ # NodeCommandRunner, factories
38+
├── bun/ # BunCommandRunner, factories
39+
└── deno/ # DenoCommandRunner, factories
2740
```
2841

2942
## 🔐 Security Principles

Dockerfile.bun

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
FROM oven/bun:1.3.4
2+
RUN apt-get update && apt-get install -y git && rm -rf /var/lib/apt/lists/*
3+
WORKDIR /app
4+
COPY plumbing /plumbing
5+
COPY vault /app
6+
RUN bun install --ignore-scripts
7+
ENV GIT_STUNTS_DOCKER=1
8+
CMD ["bun", "run", "vitest", "run", "test/unit"]

Dockerfile.deno

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
FROM denoland/deno:2.6.3@sha256:075c8d994cf1e44f10d98ea86f6693037e9c66eb83e9b5fa6a534147372de3fb
2+
3+
ARG NODE_VERSION=20.19.6
4+
ENV NODE_VERSION=${NODE_VERSION}
5+
RUN deno run --allow-net --allow-write --allow-env - <<'EOF'
6+
const version = Deno.env.get('NODE_VERSION');
7+
const arch = Deno.build.arch === 'x86_64' ? 'x64' : 'arm64';
8+
const url = `https://nodejs.org/dist/v${version}/node-v${version}-linux-${arch}.tar.gz`;
9+
const response = await fetch(url);
10+
if (!response.ok) {
11+
throw new Error(`Failed to download Node ${version}`);
12+
}
13+
const arrayBuffer = await response.arrayBuffer();
14+
await Deno.writeFile('/tmp/node.tar.gz', new Uint8Array(arrayBuffer));
15+
EOF
16+
RUN tar -xzf /tmp/node.tar.gz -C /usr/local --strip-components=1 && rm -f /tmp/node.tar.gz
17+
18+
WORKDIR /app
19+
20+
COPY package*.json ./
21+
RUN npm ci --ignore-scripts
22+
23+
COPY . ./
24+
25+
ENV GIT_STUNTS_DOCKER=1
26+
CMD ["deno", "task", "test"]

README.md

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,19 @@ Storing API keys or encryption secrets in `.env` files is a security risk. `vaul
2121
- **Linux**: Requires `libsecret` (e.g., `sudo apt install libsecret-tools`).
2222
- **Windows**: Requires the `CredentialManager` PowerShell module.
2323

24+
## Runtime-specific adapters
25+
26+
- **Node (default)**: `Vault` auto-detects Bun and Deno globals and falls back to the Node adapter when neither is present.
27+
- **Bun**: Import `createBunKeychainAdapter` (or rely on the auto-detection) to execute commands with `Bun.spawnSync` when running under Bun.
28+
- **Deno**: Import `createDenoKeychainAdapter` and run via `Deno.Command`. See `deno.json` and `Dockerfile.deno` for a working setup.
29+
30+
The `plumbing/` folder contains the reference Dockerfiles for each runtime (`Dockerfile.bun`, `Dockerfile.deno`, etc.), so you can see how the ports are wired together in an end-to-end image.
31+
32+
## Docker-based tests
33+
34+
- `npm test` runs `scripts/run-multi-runtime-tests.sh`, which in turn brings up the `node-test`, `bun-test`, and `deno-test` containers defined in `docker-compose.yml`.
35+
- Each container uses the respective Dockerfile (`Dockerfile`, `Dockerfile.bun`, `Dockerfile.deno`) so you can reproduce the same setup locally or in CI.
36+
2437
## Usage
2538

2639
```javascript
@@ -44,8 +57,13 @@ const apiKey = vault.resolveSecret({
4457
});
4558
```
4659

60+
## Docker images
61+
62+
- `Dockerfile` (Node) mirrors the repository root workflow and runs `npm test`.
63+
- `Dockerfile.bun` copies both projects, installs with Bun, and runs `bun run vitest test/unit`.
64+
- `Dockerfile.deno` relies on `deno task test` defined in `deno.json`, which proxies back to the npm test stack via the shared script.
65+
4766
## License
4867

4968
Apache-2.0
5069
Copyright © 2026 [James Ross](https://github.com/flyingrobots)
51-

deno.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"tasks": {
3+
"test": "npm run test:local"
4+
}
5+
}

docker-compose.yml

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,21 @@
11
services:
2-
test:
2+
node-test:
33
build:
44
context: ..
55
dockerfile: vault/Dockerfile
66
environment:
77
- GIT_STUNTS_DOCKER=1
8+
9+
bun-test:
10+
build:
11+
context: ..
12+
dockerfile: vault/Dockerfile.bun
13+
environment:
14+
- GIT_STUNTS_DOCKER=1
15+
16+
deno-test:
17+
build:
18+
context: ..
19+
dockerfile: vault/Dockerfile.deno
20+
environment:
21+
- GIT_STUNTS_DOCKER=1

index.js

Lines changed: 82 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,61 @@ import VaultService from './src/domain/services/VaultService.js';
66
import VaultError from './src/domain/errors/VaultError.js';
77
import PlatformNotSupportedError from './src/domain/errors/PlatformNotSupportedError.js';
88
import SecretNotFoundError from './src/domain/errors/SecretNotFoundError.js';
9+
import { createBunKeychainAdapter } from './src/infrastructure/adapters/bun/index.js';
10+
import { createDenoKeychainAdapter } from './src/infrastructure/adapters/deno/index.js';
11+
import { defaultRuntime } from './src/runtime/index.js';
12+
13+
const isNodeRuntime =
14+
typeof process !== 'undefined' &&
15+
typeof process.versions?.node === 'string' &&
16+
process.release?.name === 'node';
17+
18+
let nodeRequire;
19+
if (isNodeRuntime) {
20+
const { createRequire } = await import('module');
21+
nodeRequire = createRequire(import.meta.url);
22+
}
23+
24+
const loadNodeAdapterModule = () => {
25+
if (!nodeRequire) {
26+
throw new Error('Node runtime is required for the Node keychain adapter');
27+
}
28+
return nodeRequire('./src/infrastructure/adapters/node/index.js');
29+
};
30+
31+
/**
32+
* Create a keychain adapter backed by the Node runtime.
33+
* @param {Object} [options]
34+
* @param {string} [options.account] - Account/scope for the secrets.
35+
* @returns {KeychainAdapter}
36+
*/
37+
export function createNodeKeychainAdapter(options) {
38+
return loadNodeAdapterModule().createNodeKeychainAdapter(options);
39+
}
40+
41+
/**
42+
* Detect the environment and instantiate the matching keychain adapter.
43+
* @param {Object} params
44+
* @param {string} [params.account]
45+
* @returns {KeychainAdapter}
46+
*/
47+
const detectAdapter = ({ account }) => {
48+
if (typeof Bun !== 'undefined' && typeof Bun.spawnSync === 'function') {
49+
return createBunKeychainAdapter({ account });
50+
}
51+
if (typeof Deno !== 'undefined' && typeof Deno.Command === 'function') {
52+
return createDenoKeychainAdapter({ account });
53+
}
54+
return createNodeKeychainAdapter({ account });
55+
};
956

1057
export {
1158
VaultService,
1259
VaultError,
1360
PlatformNotSupportedError,
14-
SecretNotFoundError
61+
SecretNotFoundError,
62+
createBunKeychainAdapter,
63+
createDenoKeychainAdapter
1564
};
1665

1766
/**
@@ -22,25 +71,31 @@ export default class Vault {
2271
/**
2372
* @param {Object} options
2473
* @param {string} [options.account='git-stunts']
74+
* @param {Function} [options.adapterFactory]
2575
*/
26-
constructor({ account = 'git-stunts' } = {}) {
27-
this.service = new VaultService({ account });
76+
constructor({ account = 'git-stunts', adapterFactory } = {}) {
77+
if (adapterFactory != null && typeof adapterFactory !== 'function') {
78+
throw new TypeError('adapterFactory must be a function');
79+
}
80+
const adapter =
81+
adapterFactory ? adapterFactory({ account }) : detectAdapter({ account });
82+
this.service = new VaultService({ account, adapter });
2883
}
2984

3085
get account() {
3186
return this.service.account;
3287
}
3388

3489
get isMac() {
35-
return process.platform === 'darwin';
90+
return defaultRuntime.getPlatform() === 'darwin';
3691
}
3792

3893
get isLinux() {
39-
return process.platform === 'linux';
94+
return defaultRuntime.getPlatform() === 'linux';
4095
}
4196

4297
get isWindows() {
43-
return process.platform === 'win32';
98+
return defaultRuntime.getPlatform() === 'win32';
4499
}
45100

46101
getSecret({ target }) {
@@ -51,15 +106,35 @@ export default class Vault {
51106
this.service.setSecret(target, value);
52107
}
53108

109+
/**
110+
* Remove the secret for the requested target.
111+
* @param {Object} params
112+
* @param {string} params.target
113+
* @returns {boolean}
114+
*/
54115
deleteSecret({ target }) {
55116
return this.service.deleteSecret(target);
56117
}
57118

119+
/**
120+
* Resolve a value from the vault or environment variables.
121+
* @param {Object} params
122+
* @param {string} params.envKey
123+
* @param {string} params.vaultTarget
124+
* @returns {string|undefined}
125+
*/
58126
resolveSecret({ envKey, vaultTarget }) {
59127
return this.service.resolveSecret({ envKey, vaultTarget });
60128
}
61129

130+
/**
131+
* Ensure a secret exists, prompting if configured.
132+
* @param {Object} params
133+
* @param {string} params.target
134+
* @param {string} [params.promptMessage]
135+
* @returns {Promise<string>}
136+
*/
62137
async ensureSecret({ target, promptMessage }) {
63138
return this.service.ensureSecret({ target, promptMessage });
64139
}
65-
}
140+
}

package-lock.json

Lines changed: 13 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)