create-svc 0.1.12 → 0.1.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/vault.test.ts +1 -61
- package/src/vault.ts +15 -77
package/package.json
CHANGED
package/src/vault.test.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { afterEach, expect, mock, test } from "bun:test";
|
|
2
2
|
import { mkdir } from "node:fs/promises";
|
|
3
|
-
import { readVaultSecret, resolveNeonApiKey
|
|
3
|
+
import { readVaultSecret, resolveNeonApiKey } from "./vault";
|
|
4
4
|
|
|
5
5
|
const originalEnv = { ...process.env };
|
|
6
6
|
|
|
@@ -97,63 +97,3 @@ test("readVaultSecret falls back to ~/.vault-token", async () => {
|
|
|
97
97
|
})
|
|
98
98
|
).resolves.toBe("vault-token");
|
|
99
99
|
});
|
|
100
|
-
|
|
101
|
-
test("upsertVaultSecretFields writes merged KV v2 data", async () => {
|
|
102
|
-
process.env.VAULT_ADDR = "https://vault.example.com";
|
|
103
|
-
process.env.VAULT_TOKEN = "token-123";
|
|
104
|
-
|
|
105
|
-
const requests: Array<{ method: string; url: string; body?: unknown }> = [];
|
|
106
|
-
const fetchMock = mock(async (input: string | URL | Request, init?: RequestInit) => {
|
|
107
|
-
const url = String(input);
|
|
108
|
-
requests.push({
|
|
109
|
-
method: init?.method ?? "GET",
|
|
110
|
-
url,
|
|
111
|
-
body: init?.body ? JSON.parse(String(init.body)) : undefined,
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
if ((init?.method ?? "GET") === "GET") {
|
|
115
|
-
return new Response(
|
|
116
|
-
JSON.stringify({
|
|
117
|
-
data: {
|
|
118
|
-
data: {
|
|
119
|
-
existing_field: "keep-me",
|
|
120
|
-
},
|
|
121
|
-
},
|
|
122
|
-
}),
|
|
123
|
-
{ status: 200 }
|
|
124
|
-
);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
return new Response(JSON.stringify({}), { status: 200 });
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
globalThis.fetch = fetchMock as unknown as typeof fetch;
|
|
131
|
-
|
|
132
|
-
await upsertVaultSecretFields({
|
|
133
|
-
path: "prod/providers/clerk",
|
|
134
|
-
fields: {
|
|
135
|
-
publishable_key: "pk_live_example",
|
|
136
|
-
secret_key: "sk_live_example",
|
|
137
|
-
webhook_secret: "whsec_example",
|
|
138
|
-
},
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
expect(requests).toEqual([
|
|
142
|
-
{
|
|
143
|
-
method: "GET",
|
|
144
|
-
url: "https://vault.example.com/v1/secret/data/prod/providers/clerk",
|
|
145
|
-
},
|
|
146
|
-
{
|
|
147
|
-
method: "POST",
|
|
148
|
-
url: "https://vault.example.com/v1/secret/data/prod/providers/clerk",
|
|
149
|
-
body: {
|
|
150
|
-
data: {
|
|
151
|
-
existing_field: "keep-me",
|
|
152
|
-
publishable_key: "pk_live_example",
|
|
153
|
-
secret_key: "sk_live_example",
|
|
154
|
-
webhook_secret: "whsec_example",
|
|
155
|
-
},
|
|
156
|
-
},
|
|
157
|
-
},
|
|
158
|
-
]);
|
|
159
|
-
});
|
package/src/vault.ts
CHANGED
|
@@ -13,14 +13,6 @@ type VaultSecretOptions = {
|
|
|
13
13
|
field?: string;
|
|
14
14
|
};
|
|
15
15
|
|
|
16
|
-
type VaultWriteOptions = {
|
|
17
|
-
addr?: string;
|
|
18
|
-
token?: string;
|
|
19
|
-
mount?: string;
|
|
20
|
-
path: string;
|
|
21
|
-
fields: Record<string, string>;
|
|
22
|
-
};
|
|
23
|
-
|
|
24
16
|
export async function resolveNeonApiKey() {
|
|
25
17
|
const direct = process.env.NEON_API_KEY?.trim();
|
|
26
18
|
if (direct) {
|
|
@@ -34,59 +26,24 @@ export async function resolveNeonApiKey() {
|
|
|
34
26
|
}
|
|
35
27
|
|
|
36
28
|
export async function readVaultSecret(options: VaultSecretOptions = {}) {
|
|
37
|
-
const
|
|
38
|
-
const
|
|
29
|
+
const addr = options.addr ?? process.env.VAULT_ADDR?.trim() ?? "";
|
|
30
|
+
const token = options.token ?? (await resolveVaultToken());
|
|
39
31
|
const mount = options.mount ?? process.env.VAULT_SECRET_MOUNT?.trim() ?? DEFAULT_VAULT_SECRET_MOUNT;
|
|
40
32
|
const path = options.path?.trim() ?? "";
|
|
41
|
-
const
|
|
42
|
-
const normalizedPath = path.replace(/^\/+/g, "");
|
|
43
|
-
const value = payload[field]?.trim();
|
|
44
|
-
if (!value) {
|
|
45
|
-
throw new Error(`Vault secret field ${field} is empty at ${normalizedMount}/${normalizedPath}`);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
return value;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export async function readVaultSecretFields(options: VaultSecretOptions = {}) {
|
|
52
|
-
return readVaultSecretData(options);
|
|
53
|
-
}
|
|
33
|
+
const field = options.field?.trim() ?? "value";
|
|
54
34
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
35
|
+
if (!addr || !token || !path) {
|
|
36
|
+
throw new Error("Vault secret resolution requires VAULT_ADDR, a Vault token, and a secret path");
|
|
37
|
+
}
|
|
58
38
|
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
throw error;
|
|
64
|
-
});
|
|
39
|
+
const normalizedAddr = addr.replace(/\/+$/g, "");
|
|
40
|
+
const normalizedMount = mount.replace(/^\/+|\/+$/g, "");
|
|
41
|
+
const normalizedPath = path.replace(/^\/+/g, "");
|
|
42
|
+
const url = `${normalizedAddr}/v1/${normalizedMount}/data/${normalizedPath}`;
|
|
65
43
|
|
|
66
44
|
const response = await fetch(url, {
|
|
67
|
-
method: "POST",
|
|
68
45
|
headers: {
|
|
69
|
-
"
|
|
70
|
-
"X-Vault-Token": connection.token,
|
|
71
|
-
},
|
|
72
|
-
body: JSON.stringify({
|
|
73
|
-
data: {
|
|
74
|
-
...existing,
|
|
75
|
-
...trimFields(options.fields),
|
|
76
|
-
},
|
|
77
|
-
}),
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
if (!response.ok) {
|
|
81
|
-
throw new Error(`Vault write failed: ${response.status} ${response.statusText}`);
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
async function readVaultSecretData(options: VaultSecretOptions = {}) {
|
|
86
|
-
const connection = await resolveVaultConnection(options);
|
|
87
|
-
const response = await fetch(vaultKv2Url(connection), {
|
|
88
|
-
headers: {
|
|
89
|
-
"X-Vault-Token": connection.token,
|
|
46
|
+
"X-Vault-Token": token,
|
|
90
47
|
},
|
|
91
48
|
});
|
|
92
49
|
|
|
@@ -100,31 +57,12 @@ async function readVaultSecretData(options: VaultSecretOptions = {}) {
|
|
|
100
57
|
};
|
|
101
58
|
};
|
|
102
59
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
async function resolveVaultConnection(options: Omit<VaultWriteOptions, "fields"> | VaultSecretOptions) {
|
|
107
|
-
const addr = options.addr ?? process.env.VAULT_ADDR?.trim() ?? "";
|
|
108
|
-
const token = options.token ?? (await resolveVaultToken());
|
|
109
|
-
const mount = options.mount ?? process.env.VAULT_SECRET_MOUNT?.trim() ?? DEFAULT_VAULT_SECRET_MOUNT;
|
|
110
|
-
const path = options.path?.trim() ?? "";
|
|
111
|
-
|
|
112
|
-
if (!addr || !token || !path) {
|
|
113
|
-
throw new Error("Vault secret resolution requires VAULT_ADDR, a Vault token, and a secret path");
|
|
60
|
+
const value = payload.data?.data?.[field]?.trim();
|
|
61
|
+
if (!value) {
|
|
62
|
+
throw new Error(`Vault secret field ${field} is empty at ${normalizedMount}/${normalizedPath}`);
|
|
114
63
|
}
|
|
115
64
|
|
|
116
|
-
|
|
117
|
-
const normalizedMount = mount.replace(/^\/+|\/+$/g, "");
|
|
118
|
-
const normalizedPath = path.replace(/^\/+/g, "");
|
|
119
|
-
return { normalizedAddr, normalizedMount, normalizedPath, token };
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
function vaultKv2Url(connection: Awaited<ReturnType<typeof resolveVaultConnection>>) {
|
|
123
|
-
return `${connection.normalizedAddr}/v1/${connection.normalizedMount}/data/${connection.normalizedPath}`;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function trimFields(fields: Record<string, string>) {
|
|
127
|
-
return Object.fromEntries(Object.entries(fields).map(([key, value]) => [key, value.trim()]));
|
|
65
|
+
return value;
|
|
128
66
|
}
|
|
129
67
|
|
|
130
68
|
async function resolveVaultToken() {
|