Skip to content

Commit b09fda7

Browse files
committed
update
1 parent d00ca75 commit b09fda7

12 files changed

+315
-47
lines changed

.npmignore

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
src
2+
tests
3+
.ignoreme

README.md

+66-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,71 @@
11
# node-vault
22

3-
Client for HashiCorp's Vault
3+
A Javascript client for the HTTP API of HashiCorp's [vault](https://vaultproject.io/) with a focus on ease of use.
44

55
```bash
6-
npm install node-vault@git://github.com/shahradelahi/node-vault.git
6+
npm install @litehex/node-vault
77
```
8+
9+
### Usage
10+
11+
##### Init and unseal vault
12+
13+
```js
14+
import { Client } from '@litehex/node-vault';
15+
16+
// Get a new instance of the client
17+
const vc = new Client({
18+
apiVersion: 'v1', // default
19+
endpoint: 'http://127.0.0.1:8200', // default
20+
token: 'hv.xxxxxxxxxxxxxxxxxxxxx' // Optional unless you want to initialize the vault
21+
});
22+
23+
// Init vault
24+
vc.init({ secret_shares: 1, secret_threshold: 1 }).then((res) => {
25+
const { keys, root_token } = res;
26+
vc.token = root_token;
27+
// Unseal vault
28+
vc.unseal({ secret_shares: 1, key: keys[0] });
29+
});
30+
```
31+
32+
##### Write, read and delete secrets
33+
34+
```js
35+
vc.write({ path: 'secret/hello', data: { foo: 'bar' } }).then(() => {
36+
vc.read({ path: 'secret/hello' }).then(({ data }) => {
37+
console.log(data); // { value: 'world' }
38+
});
39+
});
40+
41+
vc.delete({ path: 'secret/hello' });
42+
```
43+
44+
### Docs
45+
46+
- HashCorp's Vault [API docs](https://developer.hashicorp.com/vault/api-docs)
47+
48+
### Examples
49+
50+
##### Using a proxy or having the ability to modify the outgoing request.
51+
52+
```js
53+
import { Client } from '@litehex/node-vault';
54+
import { ProxyAgent } from 'undici';
55+
56+
const agent = new ProxyAgent('http://localhost:8080');
57+
58+
const vc = new Client({
59+
// ... other params
60+
request: {
61+
agent: agent,
62+
headers: {
63+
'X-Custom-Header': 'value'
64+
}
65+
}
66+
});
67+
```
68+
69+
### License
70+
71+
This project is licensed under the GPLv3 License - see the [LICENSE](LICENSE) file for details

package.json

+18-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{
2-
"name": "node-vault",
3-
"version": "0.1.0",
2+
"name": "@litehex/node-vault",
3+
"version": "0.1.0-alpha.1",
4+
"private": false,
45
"description": "Client for HashiCorp's Vault",
56
"author": "Shahrad Elahi <shahrad@litehex.com> (https://github.com/shahradelahi)",
67
"main": "build/src/index.js",
@@ -11,15 +12,30 @@
1112
},
1213
"scripts": {
1314
"build": "rimraf build && tsc",
15+
"prepublish": "npm run build",
1416
"format:check": "prettier --plugin-search-dir . --check .",
1517
"format:write": "prettier --plugin-search-dir . --write ."
1618
},
19+
"keywords": [
20+
"hashcorp",
21+
"vault",
22+
"client",
23+
"secrets",
24+
"node",
25+
"bun"
26+
],
27+
"files": [
28+
"build/src"
29+
],
30+
"packageManager": "pnpm@8.9.2",
1731
"dependencies": {
32+
"lodash.pick": "^4.4.0",
1833
"mustache": "^4.2.0",
1934
"undici": "^5.27.2",
2035
"zod": "^3.22.4"
2136
},
2237
"devDependencies": {
38+
"@types/lodash.pick": "^4.4.9",
2339
"@types/mocha": "^10.0.6",
2440
"@types/mustache": "^4.2.5",
2541
"@types/node": "^20.9.4",

pnpm-lock.yaml

+20
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/errors.ts

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import type { Response } from 'undici';
2+
import { z } from 'zod';
3+
4+
export class ApiResponseError extends Error {
5+
readonly response: Response;
6+
7+
constructor(message: string, response: Response) {
8+
message = `VaultError: ${message}`;
9+
super(message);
10+
this.response = response;
11+
}
12+
}
13+
14+
export const ApiErrorSchema = z.object({
15+
errors: z.array(z.string())
16+
});

src/index.ts

+86-12
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { z } from 'zod';
22
import { CommandInit, generateCommand } from './utils/generate-command';
3-
import { RequestSchema } from './typings';
3+
import { RequestInit, RequestSchema } from './typings';
4+
import { PartialDeep } from 'type-fest';
45

56
const ClientOptionsSchema = z.object({
67
endpoint: z.string().optional(),
@@ -10,33 +11,40 @@ const ClientOptionsSchema = z.object({
1011
namespace: z.string().optional()
1112
});
1213

13-
export type ClientOptions = z.infer<typeof ClientOptionsSchema>;
14+
export type ClientOptions = z.infer<typeof ClientOptionsSchema> & {
15+
request?: PartialDeep<RequestInit>;
16+
};
1417

1518
export class Client {
16-
readonly endpoint: string;
17-
readonly apiVersion: string;
18-
readonly pathPrefix: string;
19-
readonly token: string | undefined;
20-
readonly namespace: string | undefined;
19+
endpoint: string;
20+
apiVersion: string;
21+
pathPrefix: string;
22+
namespace: string | undefined;
23+
token: string | undefined;
24+
request: PartialDeep<Omit<RequestInit, 'url'>> | undefined;
2125

22-
constructor({ ...restOpts }: ClientOptions) {
26+
constructor({ request, ...restOpts }: ClientOptions = {}) {
2327
const options = ClientOptionsSchema.parse(restOpts);
2428

2529
this.endpoint = options.endpoint || process.env.VAULT_ADDR || 'http://127.0.0.1:8200';
2630
this.apiVersion = options.apiVersion || 'v1';
2731
this.pathPrefix = options.pathPrefix || '';
2832
this.namespace = options.namespace || process.env.VAULT_NAMESPACE;
2933
this.token = options.token || process.env.VAULT_TOKEN;
34+
35+
this.request = request;
3036
}
3137

32-
private assignCommands<T extends RequestSchema>(commands: Record<string, CommandInit<T>>) {
33-
for (const [name, cmd] of Object.entries(commands)) {
38+
private assignCommands<T extends RequestSchema>(
39+
commands: Record<string, Omit<CommandInit<T>, 'client'>>
40+
) {
41+
for (const [name, init] of Object.entries(commands)) {
3442
// @ts-ignore
35-
this[name] = this.generateFunction(cmd);
43+
this[name] = generateCommand({ ...init, client: this });
3644
}
3745
}
3846

39-
readonly status = generateCommand({
47+
status = generateCommand({
4048
method: 'GET',
4149
path: '/sys/seal-status',
4250
client: this,
@@ -49,6 +57,72 @@ export class Client {
4957
})
5058
}
5159
});
60+
61+
init = generateCommand({
62+
method: 'PUT',
63+
path: '/sys/init',
64+
client: this,
65+
schema: {
66+
body: z.object({
67+
secret_shares: z.number(),
68+
secret_threshold: z.number()
69+
}),
70+
response: z.object({
71+
keys: z.array(z.string()),
72+
keys_base64: z.array(z.string()),
73+
root_token: z.string()
74+
})
75+
}
76+
});
77+
78+
read = generateCommand({
79+
method: 'GET',
80+
path: '/{{path}}',
81+
client: this,
82+
schema: {
83+
path: z.object({
84+
path: z.string()
85+
}),
86+
response: z.record(z.any())
87+
}
88+
});
89+
90+
write = generateCommand({
91+
method: 'POST',
92+
path: '/{{path}}',
93+
client: this,
94+
refine: (init, params) => {
95+
console.log('refine', init, params);
96+
return init;
97+
},
98+
schema: {
99+
path: z.object({
100+
path: z.string()
101+
}),
102+
body: z.any(),
103+
response: z.record(z.any())
104+
}
105+
});
106+
107+
delete = generateCommand({
108+
method: 'DELETE',
109+
path: '/{{path}}',
110+
client: this,
111+
schema: {
112+
path: z.object({
113+
path: z.string()
114+
}),
115+
response: z.record(z.any())
116+
}
117+
});
52118
}
53119

120+
const AuthSchema = z.object({
121+
client_token: z.string(),
122+
policies: z.array(z.string()),
123+
metadata: z.any(),
124+
lease_duration: z.number(),
125+
renewable: z.boolean()
126+
});
127+
54128
export type * from './typings';

src/request.ts

+21-5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { fetch } from 'undici';
22
import type { RequestInit, RequestSchema, ValidatedResponse } from './typings';
3+
import { ApiResponseError } from './errors';
34

45
export async function request<Schema extends RequestSchema>(
56
init: RequestInit,
@@ -8,24 +9,39 @@ export async function request<Schema extends RequestSchema>(
89
if (schema.searchParams) {
910
const valid = schema.searchParams.safeParse(init.url);
1011
if (!valid.success) {
11-
throw new Error(valid.error.message);
12+
throw new Error('ErrorSearchPrams: Invalid Args. ' + valid.error.message);
1213
}
1314
}
1415

1516
if (schema.body) {
1617
const valid = schema.body.safeParse(init.body);
1718
if (!valid.success) {
18-
throw new Error(valid.error.message);
19+
throw new Error('ErrorBody: Invalid Args. ' + valid.error.message);
1920
}
2021
}
2122

22-
const response = await fetch(init.url, init);
23+
let body: string | undefined = undefined;
24+
if (init.body) {
25+
body = JSON.stringify(init.body);
26+
}
27+
28+
const { url, strictSchema, ...restInit } = init;
29+
const response = await fetch(url, {
30+
...restInit,
31+
body
32+
});
33+
34+
if (!response.ok) {
35+
const text = await response.text();
36+
throw new ApiResponseError(`${response.statusText}\n${text}`, response);
37+
}
38+
2339
const json = await response.json();
2440

25-
if (schema.response) {
41+
if (false !== strictSchema && schema.response) {
2642
const valid = schema.response.safeParse(json);
2743
if (!valid.success) {
28-
throw new Error(valid.error.message);
44+
throw new ApiResponseError('Server response did not match client schema.', response);
2945
}
3046
return valid.data;
3147
}

0 commit comments

Comments
 (0)