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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-svc",
3
- "version": "0.1.12",
3
+ "version": "0.1.13",
4
4
  "description": "Local microservice bootstrap CLI for Cloud Run and Workers services with Neon-backed data.",
5
5
  "module": "index.ts",
6
6
  "type": "module",
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, upsertVaultSecretFields } from "./vault";
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 field = options.field?.trim() ?? "value";
38
- const payload = await readVaultSecretData(options);
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 normalizedMount = mount.replace(/^\/+|\/+$/g, "");
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
- export async function upsertVaultSecretFields(options: VaultWriteOptions) {
56
- const connection = await resolveVaultConnection(options);
57
- const url = vaultKv2Url(connection);
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 existing = await readVaultSecretData({ ...options, path: connection.normalizedPath }).catch((error) => {
60
- if (error instanceof Error && error.message.startsWith("Vault read failed: 404")) {
61
- return {};
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
- "Content-Type": "application/json",
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
- return payload.data?.data ?? {};
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
- const normalizedAddr = addr.replace(/\/+$/g, "");
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() {