Files
bun.sh/test/js/bun/secrets-error-codes.test.ts
Jarred Sumner 707fc4c3a2 Introduce Bun.secrets API (#21973)
This PR adds `Bun.secrets`, a new API for securely storing and
retrieving credentials using the operating system's native credential
storage locally. This helps developers avoid storing sensitive data in
plaintext config files.

```javascript
// Store a GitHub token securely
await Bun.secrets.set({
  service: "my-cli-tool",
  name: "github-token",
  value: "ghp_xxxxxxxxxxxxxxxxxxxx"
});

// Retrieve it when needed
const token = await Bun.secrets.get({
  service: "my-cli-tool",
  name: "github-token"
});

// Use with fallback to environment variable
const apiKey = await Bun.secrets.get({
  service: "my-app",
  name: "api-key"
}) || process.env.API_KEY;
```

Marking this as a draft because Linux and Windows have not been manually
tested yet. This API is only really meant for local development usecases
right now, but it would be nice if in the future to support adapters for
production or CI usecases.

### Core API
- `Bun.secrets.get({ service, name })` - Retrieve a stored credential
- `Bun.secrets.set({ service, name, value })` - Store or update a
credential
- `Bun.secrets.delete({ service, name })` - Delete a stored credential

### Platform Support
- **macOS**: Uses Keychain Services via Security.framework
- **Linux**: Uses libsecret (works with GNOME Keyring, KWallet, etc.)
- **Windows**: Uses Windows Credential Manager via advapi32.dll

### Implementation Highlights
- Non-blocking - all operations run on the threadpool
- Dynamic loading - no hard dependencies on system libraries
- Sensitive data is zeroed after use
- Consistent API across all platforms

## Use Cases

This API is particularly useful for:
- CLI tools that need to store authentication tokens
- Development tools that manage API keys
- Any tool that currently stores credentials in `~/.npmrc`,
`~/.aws/credentials` or in environment variables that're globally loaded

## Testing

Comprehensive test suite included with coverage for:
- Basic CRUD operations
- Empty strings and special characters
- Unicode support
- Concurrent operations
- Error handling

All tests pass on macOS. Linux and Windows implementations are complete
but would benefit from additional platform testing.

## Documentation

- Complete API documentation in `docs/api/secrets.md`
- TypeScript definitions with detailed JSDoc comments and examples

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
2025-08-23 06:57:00 -07:00

98 lines
2.9 KiB
TypeScript

import { describe, expect, test } from "bun:test";
import { isCI, isMacOS, isWindows } from "harness";
describe.todoIf(isCI && !isWindows)("Bun.secrets error codes", () => {
test("non-existent secret returns null without error", async () => {
const result = await Bun.secrets.get({
service: "non-existent-service-" + Date.now(),
name: "non-existent-name",
});
expect(result).toBeNull();
});
test("delete non-existent returns false without error", async () => {
const result = await Bun.secrets.delete({
service: "non-existent-service-" + Date.now(),
name: "non-existent-name",
});
expect(result).toBe(false);
});
test("invalid arguments throw with proper error codes", async () => {
// Missing service
try {
// @ts-expect-error
await Bun.secrets.get({ name: "test" });
expect.unreachable();
} catch (error: any) {
expect(error.code).toBe("ERR_INVALID_ARG_TYPE");
expect(error.message).toContain("Expected service and name to be strings");
}
// Empty service
try {
await Bun.secrets.get({ service: "", name: "test" });
expect.unreachable();
} catch (error: any) {
expect(error.code).toBe("ERR_INVALID_ARG_TYPE");
expect(error.message).toContain("Expected service and name to not be empty");
}
// Missing value in set
try {
// @ts-expect-error
await Bun.secrets.set({ service: "test", name: "test" });
expect.unreachable();
} catch (error: any) {
expect(error.code).toBe("ERR_INVALID_ARG_TYPE");
expect(error.message).toContain("Expected 'value' to be a string");
}
});
test("successful operations work correctly", async () => {
const service = "bun-test-codes-" + Date.now();
const name = "test-name";
const value = "test-password";
// Set a secret
await Bun.secrets.set({ service, name, value, allowUnrestrictedAccess: isMacOS });
// Get it back
const retrieved = await Bun.secrets.get({ service, name });
expect(retrieved).toBe(value);
// Delete it
const deleted = await Bun.secrets.delete({ service, name });
expect(deleted).toBe(true);
// Verify it's gone
const afterDelete = await Bun.secrets.get({ service, name });
expect(afterDelete).toBeNull();
});
test("error messages have no null bytes", async () => {
// Test various error conditions
const errorTests = [
{ service: "", name: "test" },
{ service: "test", name: "" },
];
for (const testCase of errorTests) {
try {
await Bun.secrets.get(testCase);
expect.unreachable();
} catch (error: any) {
// Check for null bytes
expect(error.message).toBeDefined();
expect(error.message.includes("\0")).toBe(false);
// Check error has a code
expect(error.code).toBeDefined();
expect(typeof error.code).toBe("string");
}
}
});
});