@ant.sh/colony 0.1.0
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/LICENSE +21 -0
- package/README.md +172 -0
- package/dist/cjs/cli.js +281 -0
- package/dist/cjs/cli.js.map +7 -0
- package/dist/cjs/index.js +383 -0
- package/dist/cjs/index.js.map +7 -0
- package/dist/cjs/package.json +3 -0
- package/dist/cjs/parser.js +319 -0
- package/dist/cjs/parser.js.map +7 -0
- package/dist/cjs/providers/aws.js +115 -0
- package/dist/cjs/providers/aws.js.map +7 -0
- package/dist/cjs/providers/openbao.js +49 -0
- package/dist/cjs/providers/openbao.js.map +7 -0
- package/dist/cjs/providers/vault-base.js +98 -0
- package/dist/cjs/providers/vault-base.js.map +7 -0
- package/dist/cjs/providers/vault.js +49 -0
- package/dist/cjs/providers/vault.js.map +7 -0
- package/dist/cjs/resolver.js +247 -0
- package/dist/cjs/resolver.js.map +7 -0
- package/dist/cjs/secrets.js +238 -0
- package/dist/cjs/secrets.js.map +7 -0
- package/dist/cjs/strings.js +99 -0
- package/dist/cjs/strings.js.map +7 -0
- package/dist/cjs/util.js +74 -0
- package/dist/cjs/util.js.map +7 -0
- package/dist/esm/cli.js +281 -0
- package/dist/esm/cli.js.map +7 -0
- package/dist/esm/index.d.ts +342 -0
- package/dist/esm/index.js +347 -0
- package/dist/esm/index.js.map +7 -0
- package/dist/esm/package.json +3 -0
- package/dist/esm/parser.js +286 -0
- package/dist/esm/parser.js.map +7 -0
- package/dist/esm/providers/aws.js +82 -0
- package/dist/esm/providers/aws.js.map +7 -0
- package/dist/esm/providers/openbao.js +26 -0
- package/dist/esm/providers/openbao.js.map +7 -0
- package/dist/esm/providers/vault-base.js +75 -0
- package/dist/esm/providers/vault-base.js.map +7 -0
- package/dist/esm/providers/vault.js +26 -0
- package/dist/esm/providers/vault.js.map +7 -0
- package/dist/esm/resolver.js +224 -0
- package/dist/esm/resolver.js.map +7 -0
- package/dist/esm/secrets.js +209 -0
- package/dist/esm/secrets.js.map +7 -0
- package/dist/esm/strings.js +75 -0
- package/dist/esm/strings.js.map +7 -0
- package/dist/esm/util.js +47 -0
- package/dist/esm/util.js.map +7 -0
- package/package.json +66 -0
- package/src/cli.js +353 -0
- package/src/index.d.ts +342 -0
- package/src/index.js +473 -0
- package/src/parser.js +381 -0
- package/src/providers/aws.js +112 -0
- package/src/providers/openbao.js +32 -0
- package/src/providers/vault-base.js +92 -0
- package/src/providers/vault.js +31 -0
- package/src/resolver.js +286 -0
- package/src/secrets.js +313 -0
- package/src/strings.js +84 -0
- package/src/util.js +49 -0
package/src/parser.js
ADDED
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
import JSON5 from "json5";
|
|
2
|
+
|
|
3
|
+
const RX_DIMS = /^@dims\s+([^;]+);/;
|
|
4
|
+
const RX_INCLUDE = /^@include\s+(.+);/;
|
|
5
|
+
const RX_REQUIRE = /^@require\s+([^;]+);/;
|
|
6
|
+
const RX_ENVDEFAULTS = /^@envDefaults\s+([^;]+);/;
|
|
7
|
+
|
|
8
|
+
// Rule: <scoped.key.path> <op> <value>;
|
|
9
|
+
// Using [\s\S] instead of . to match newlines in multi-line values
|
|
10
|
+
const RX_RULE = /^(.+?)\s*(\:=|\|\=|\+\=|\-\=|\=)\s*([\s\S]+)\s*;$/;
|
|
11
|
+
|
|
12
|
+
// Heredoc: <<EOF ... EOF
|
|
13
|
+
const RX_HEREDOC_START = /<<([A-Z_][A-Z0-9_]*)\s*$/;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Remove block comments from text (slash-star to star-slash)
|
|
17
|
+
* @param {string} text
|
|
18
|
+
* @returns {{ text: string, lineMap: number[] }} - cleaned text and mapping from new line numbers to original
|
|
19
|
+
*/
|
|
20
|
+
function stripBlockComments(text) {
|
|
21
|
+
const result = [];
|
|
22
|
+
const lineMap = []; // lineMap[newLineNo] = originalLineNo
|
|
23
|
+
let inComment = false;
|
|
24
|
+
let commentDepth = 0;
|
|
25
|
+
const lines = text.split(/\r?\n/);
|
|
26
|
+
|
|
27
|
+
for (let i = 0; i < lines.length; i++) {
|
|
28
|
+
const originalLineNo = i + 1;
|
|
29
|
+
let line = lines[i];
|
|
30
|
+
let cleanLine = "";
|
|
31
|
+
let j = 0;
|
|
32
|
+
|
|
33
|
+
while (j < line.length) {
|
|
34
|
+
if (!inComment) {
|
|
35
|
+
// Check for comment start
|
|
36
|
+
if (line[j] === "/" && line[j + 1] === "*") {
|
|
37
|
+
inComment = true;
|
|
38
|
+
commentDepth = 1;
|
|
39
|
+
j += 2;
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
cleanLine += line[j];
|
|
43
|
+
j++;
|
|
44
|
+
} else {
|
|
45
|
+
// Inside block comment
|
|
46
|
+
if (line[j] === "/" && line[j + 1] === "*") {
|
|
47
|
+
commentDepth++;
|
|
48
|
+
j += 2;
|
|
49
|
+
} else if (line[j] === "*" && line[j + 1] === "/") {
|
|
50
|
+
commentDepth--;
|
|
51
|
+
if (commentDepth === 0) {
|
|
52
|
+
inComment = false;
|
|
53
|
+
}
|
|
54
|
+
j += 2;
|
|
55
|
+
} else {
|
|
56
|
+
j++;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
result.push(cleanLine);
|
|
62
|
+
lineMap.push(originalLineNo);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return { text: result.join("\n"), lineMap };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Parse a key path with escaped dots
|
|
70
|
+
* "foo\.bar.baz" => ["foo.bar", "baz"]
|
|
71
|
+
* @param {string} keyRaw
|
|
72
|
+
* @returns {string[]}
|
|
73
|
+
*/
|
|
74
|
+
function parseKeyPath(keyRaw) {
|
|
75
|
+
const segments = [];
|
|
76
|
+
let current = "";
|
|
77
|
+
let i = 0;
|
|
78
|
+
|
|
79
|
+
while (i < keyRaw.length) {
|
|
80
|
+
if (keyRaw[i] === "\\" && keyRaw[i + 1] === ".") {
|
|
81
|
+
// Escaped dot - include literal dot
|
|
82
|
+
current += ".";
|
|
83
|
+
i += 2;
|
|
84
|
+
} else if (keyRaw[i] === ".") {
|
|
85
|
+
// Segment separator
|
|
86
|
+
if (current.trim()) segments.push(current.trim());
|
|
87
|
+
current = "";
|
|
88
|
+
i++;
|
|
89
|
+
} else {
|
|
90
|
+
current += keyRaw[i];
|
|
91
|
+
i++;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (current.trim()) segments.push(current.trim());
|
|
96
|
+
return segments;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Format a parse error with line context and caret
|
|
101
|
+
* @param {string} message
|
|
102
|
+
* @param {string[]} lines
|
|
103
|
+
* @param {number} lineNo - 1-indexed line number
|
|
104
|
+
* @param {number=} col - 0-indexed column (optional)
|
|
105
|
+
* @param {string} filePath
|
|
106
|
+
* @returns {string}
|
|
107
|
+
*/
|
|
108
|
+
function formatParseError(message, lines, lineNo, col, filePath) {
|
|
109
|
+
const idx = lineNo - 1;
|
|
110
|
+
const contextLines = [];
|
|
111
|
+
|
|
112
|
+
// Show up to 2 lines before
|
|
113
|
+
for (let i = Math.max(0, idx - 2); i < idx; i++) {
|
|
114
|
+
contextLines.push(` ${String(i + 1).padStart(4)} | ${lines[i]}`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Show the error line
|
|
118
|
+
if (idx >= 0 && idx < lines.length) {
|
|
119
|
+
contextLines.push(`> ${String(lineNo).padStart(4)} | ${lines[idx]}`);
|
|
120
|
+
|
|
121
|
+
// Show caret if column is specified
|
|
122
|
+
if (typeof col === "number" && col >= 0) {
|
|
123
|
+
const padding = " ".repeat(col + 9); // account for "> XXXX | "
|
|
124
|
+
contextLines.push(`${padding}^`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Show up to 1 line after
|
|
129
|
+
if (idx + 1 < lines.length) {
|
|
130
|
+
contextLines.push(` ${String(idx + 2).padStart(4)} | ${lines[idx + 1]}`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return `${filePath}:${lineNo}: ${message}\n\n${contextLines.join("\n")}`;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function parseColony(text, { filePath = "<memory>", parseOnlyDirectives = false } = {}) {
|
|
137
|
+
// Strip block comments first, keeping track of original line numbers
|
|
138
|
+
const { text: cleanedText, lineMap } = stripBlockComments(text);
|
|
139
|
+
const lines = cleanedText.split(/\r?\n/);
|
|
140
|
+
const originalLines = text.split(/\r?\n/);
|
|
141
|
+
|
|
142
|
+
const rules = [];
|
|
143
|
+
const includes = [];
|
|
144
|
+
const requires = [];
|
|
145
|
+
const envDefaults = {};
|
|
146
|
+
let dims = null;
|
|
147
|
+
|
|
148
|
+
let buf = "";
|
|
149
|
+
let bufStartLine = 0;
|
|
150
|
+
let bufStartOriginalLine = 0;
|
|
151
|
+
|
|
152
|
+
// Heredoc state
|
|
153
|
+
let inHeredoc = false;
|
|
154
|
+
let heredocDelimiter = "";
|
|
155
|
+
let heredocContent = "";
|
|
156
|
+
let heredocStartLine = 0;
|
|
157
|
+
let heredocKey = "";
|
|
158
|
+
let heredocOp = "";
|
|
159
|
+
|
|
160
|
+
const getOriginalLine = (lineNo) => lineMap[lineNo - 1] || lineNo;
|
|
161
|
+
|
|
162
|
+
const flush = () => {
|
|
163
|
+
const raw = buf.trim();
|
|
164
|
+
buf = "";
|
|
165
|
+
if (!raw) return;
|
|
166
|
+
|
|
167
|
+
const origLine = bufStartOriginalLine;
|
|
168
|
+
|
|
169
|
+
const mDims = raw.match(RX_DIMS);
|
|
170
|
+
if (mDims) {
|
|
171
|
+
dims = mDims[1].split(",").map((s) => s.trim()).filter(Boolean);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const mInc = raw.match(RX_INCLUDE);
|
|
176
|
+
if (mInc) {
|
|
177
|
+
includes.push(stripQuotes(mInc[1].trim()));
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const mReq = raw.match(RX_REQUIRE);
|
|
182
|
+
if (mReq) {
|
|
183
|
+
const keys = mReq[1]
|
|
184
|
+
.split(",")
|
|
185
|
+
.map((s) => s.trim())
|
|
186
|
+
.filter(Boolean);
|
|
187
|
+
requires.push(...keys);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const mEnvDef = raw.match(RX_ENVDEFAULTS);
|
|
192
|
+
if (mEnvDef) {
|
|
193
|
+
const parts = mEnvDef[1]
|
|
194
|
+
.split(",")
|
|
195
|
+
.map((s) => s.trim())
|
|
196
|
+
.filter(Boolean);
|
|
197
|
+
|
|
198
|
+
for (const p of parts) {
|
|
199
|
+
const idx = p.indexOf("=");
|
|
200
|
+
if (idx === -1) {
|
|
201
|
+
throw new Error(formatParseError(
|
|
202
|
+
`Bad @envDefaults entry: ${p}`,
|
|
203
|
+
originalLines,
|
|
204
|
+
origLine,
|
|
205
|
+
undefined,
|
|
206
|
+
filePath
|
|
207
|
+
));
|
|
208
|
+
}
|
|
209
|
+
const k = p.slice(0, idx).trim();
|
|
210
|
+
const vRaw = p.slice(idx + 1).trim();
|
|
211
|
+
envDefaults[k] = stripQuotes(vRaw);
|
|
212
|
+
}
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (parseOnlyDirectives) return;
|
|
217
|
+
|
|
218
|
+
const mRule = raw.match(RX_RULE);
|
|
219
|
+
if (!mRule) {
|
|
220
|
+
throw new Error(formatParseError(
|
|
221
|
+
"Invalid statement",
|
|
222
|
+
originalLines,
|
|
223
|
+
origLine,
|
|
224
|
+
undefined,
|
|
225
|
+
filePath
|
|
226
|
+
));
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const keyRaw = mRule[1].trim();
|
|
230
|
+
const op = mRule[2];
|
|
231
|
+
const valueRaw = mRule[3].trim();
|
|
232
|
+
|
|
233
|
+
const keySegments = parseKeyPath(keyRaw);
|
|
234
|
+
if (keySegments.length === 0) {
|
|
235
|
+
throw new Error(formatParseError(
|
|
236
|
+
"Empty key",
|
|
237
|
+
originalLines,
|
|
238
|
+
origLine,
|
|
239
|
+
0,
|
|
240
|
+
filePath
|
|
241
|
+
));
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const value = parseValue(valueRaw, { filePath, line: origLine, lines: originalLines });
|
|
245
|
+
|
|
246
|
+
rules.push({
|
|
247
|
+
filePath,
|
|
248
|
+
line: origLine,
|
|
249
|
+
col: 0,
|
|
250
|
+
keyRaw,
|
|
251
|
+
keySegments,
|
|
252
|
+
op,
|
|
253
|
+
value,
|
|
254
|
+
});
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
for (let i = 0; i < lines.length; i++) {
|
|
258
|
+
const lineNo = i + 1;
|
|
259
|
+
const originalLineNo = getOriginalLine(lineNo);
|
|
260
|
+
const line = lines[i];
|
|
261
|
+
const trimmed = line.trim();
|
|
262
|
+
|
|
263
|
+
// Handle heredoc mode
|
|
264
|
+
if (inHeredoc) {
|
|
265
|
+
if (trimmed === heredocDelimiter) {
|
|
266
|
+
// End of heredoc
|
|
267
|
+
const keySegments = parseKeyPath(heredocKey);
|
|
268
|
+
if (keySegments.length === 0) {
|
|
269
|
+
throw new Error(formatParseError(
|
|
270
|
+
"Empty key in heredoc",
|
|
271
|
+
originalLines,
|
|
272
|
+
heredocStartLine,
|
|
273
|
+
0,
|
|
274
|
+
filePath
|
|
275
|
+
));
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
rules.push({
|
|
279
|
+
filePath,
|
|
280
|
+
line: heredocStartLine,
|
|
281
|
+
col: 0,
|
|
282
|
+
keyRaw: heredocKey,
|
|
283
|
+
keySegments,
|
|
284
|
+
op: heredocOp,
|
|
285
|
+
value: heredocContent,
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
inHeredoc = false;
|
|
289
|
+
heredocContent = "";
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Add line to heredoc content
|
|
294
|
+
heredocContent += (heredocContent ? "\n" : "") + line;
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Skip empty lines and line comments
|
|
299
|
+
if (!trimmed || trimmed.startsWith("#") || trimmed.startsWith("//")) continue;
|
|
300
|
+
|
|
301
|
+
// Check for heredoc start: key = <<EOF
|
|
302
|
+
const heredocMatch = trimmed.match(/^(.+?)\s*(\:=|\|\=|\+\=|\-\=|\=)\s*<<([A-Z_][A-Z0-9_]*)$/);
|
|
303
|
+
if (heredocMatch) {
|
|
304
|
+
inHeredoc = true;
|
|
305
|
+
heredocKey = heredocMatch[1].trim();
|
|
306
|
+
heredocOp = heredocMatch[2];
|
|
307
|
+
heredocDelimiter = heredocMatch[3];
|
|
308
|
+
heredocStartLine = originalLineNo;
|
|
309
|
+
heredocContent = "";
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (!buf) {
|
|
314
|
+
bufStartLine = lineNo;
|
|
315
|
+
bufStartOriginalLine = originalLineNo;
|
|
316
|
+
}
|
|
317
|
+
buf += (buf ? "\n" : "") + line;
|
|
318
|
+
|
|
319
|
+
if (trimmed.endsWith(";")) flush();
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (inHeredoc) {
|
|
323
|
+
throw new Error(formatParseError(
|
|
324
|
+
`Unterminated heredoc (missing ${heredocDelimiter})`,
|
|
325
|
+
originalLines,
|
|
326
|
+
heredocStartLine,
|
|
327
|
+
undefined,
|
|
328
|
+
filePath
|
|
329
|
+
));
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (buf.trim()) {
|
|
333
|
+
throw new Error(formatParseError(
|
|
334
|
+
"Unterminated statement (missing ';')",
|
|
335
|
+
originalLines,
|
|
336
|
+
bufStartOriginalLine,
|
|
337
|
+
undefined,
|
|
338
|
+
filePath
|
|
339
|
+
));
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return { dims, includes, requires, envDefaults, rules };
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function parseValue(raw, { filePath, line, lines }) {
|
|
346
|
+
const r = raw.trim();
|
|
347
|
+
|
|
348
|
+
if (/^(true|false|null)$/.test(r)) return JSON5.parse(r);
|
|
349
|
+
if (/^-?\d+(\.\d+)?$/.test(r)) return Number(r);
|
|
350
|
+
|
|
351
|
+
const starts = r[0];
|
|
352
|
+
const ends = r[r.length - 1];
|
|
353
|
+
const looksJsonish =
|
|
354
|
+
(starts === "{" && ends === "}") ||
|
|
355
|
+
(starts === "[" && ends === "]") ||
|
|
356
|
+
(starts === `"` && ends === `"`) ||
|
|
357
|
+
(starts === `'` && ends === `'`);
|
|
358
|
+
|
|
359
|
+
if (looksJsonish) {
|
|
360
|
+
try {
|
|
361
|
+
return JSON5.parse(r);
|
|
362
|
+
} catch (e) {
|
|
363
|
+
throw new Error(formatParseError(
|
|
364
|
+
`Bad JSON5 value: ${e.message}`,
|
|
365
|
+
lines,
|
|
366
|
+
line,
|
|
367
|
+
undefined,
|
|
368
|
+
filePath
|
|
369
|
+
));
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
return r; // bareword => string
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function stripQuotes(s) {
|
|
377
|
+
if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) {
|
|
378
|
+
return s.slice(1, -1);
|
|
379
|
+
}
|
|
380
|
+
return s;
|
|
381
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AWS Secrets Manager provider for colony
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @class AwsSecretsProvider
|
|
7
|
+
* @property {string} prefix - Provider prefix ("AWS")
|
|
8
|
+
*/
|
|
9
|
+
export class AwsSecretsProvider {
|
|
10
|
+
prefix = "AWS";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @param {object} options
|
|
14
|
+
* @param {string=} options.region - AWS region (default: process.env.AWS_REGION or "us-east-1")
|
|
15
|
+
*/
|
|
16
|
+
constructor(options = {}) {
|
|
17
|
+
this.region = options.region ?? process.env.AWS_REGION ?? "us-east-1";
|
|
18
|
+
this.client = null;
|
|
19
|
+
this.clientPromise = null;
|
|
20
|
+
this.GetSecretValueCommand = null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Get or create the AWS client (lazy initialization)
|
|
25
|
+
* @returns {Promise<object>}
|
|
26
|
+
*/
|
|
27
|
+
async getClient() {
|
|
28
|
+
if (this.client) return this.client;
|
|
29
|
+
if (this.clientPromise) return this.clientPromise;
|
|
30
|
+
|
|
31
|
+
this.clientPromise = (async () => {
|
|
32
|
+
// Dynamic import to avoid requiring AWS SDK if not used
|
|
33
|
+
const { SecretsManagerClient, GetSecretValueCommand } = await import(
|
|
34
|
+
"@aws-sdk/client-secrets-manager"
|
|
35
|
+
);
|
|
36
|
+
this.client = new SecretsManagerClient({ region: this.region });
|
|
37
|
+
this.GetSecretValueCommand = GetSecretValueCommand;
|
|
38
|
+
return this.client;
|
|
39
|
+
})();
|
|
40
|
+
|
|
41
|
+
return this.clientPromise;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Fetch a secret value
|
|
46
|
+
* @param {string} key - Secret key, optionally with JSON path: "secret-name#json.path"
|
|
47
|
+
* @returns {Promise<string>}
|
|
48
|
+
*/
|
|
49
|
+
async fetch(key) {
|
|
50
|
+
const client = await this.getClient();
|
|
51
|
+
|
|
52
|
+
// Support key with JSON path: secret-name#json.path
|
|
53
|
+
const [secretId, jsonPath] = key.split("#");
|
|
54
|
+
|
|
55
|
+
const command = new this.GetSecretValueCommand({ SecretId: secretId });
|
|
56
|
+
const response = await client.send(command);
|
|
57
|
+
|
|
58
|
+
let value = response.SecretString;
|
|
59
|
+
if (!value && response.SecretBinary) {
|
|
60
|
+
value = Buffer.from(response.SecretBinary).toString("utf-8");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Extract JSON path if specified
|
|
64
|
+
if (jsonPath && value) {
|
|
65
|
+
try {
|
|
66
|
+
const parsed = JSON.parse(value);
|
|
67
|
+
value = getJsonPath(parsed, jsonPath);
|
|
68
|
+
} catch {
|
|
69
|
+
// Not JSON or invalid path, return as-is
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return value ?? "";
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Validate provider configuration
|
|
78
|
+
* @returns {Promise<void>}
|
|
79
|
+
*/
|
|
80
|
+
async validate() {
|
|
81
|
+
// Try to initialize client to verify SDK is available
|
|
82
|
+
await this.getClient();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Cleanup resources
|
|
87
|
+
* @returns {Promise<void>}
|
|
88
|
+
*/
|
|
89
|
+
async dispose() {
|
|
90
|
+
if (this.client?.destroy) {
|
|
91
|
+
this.client.destroy();
|
|
92
|
+
}
|
|
93
|
+
this.client = null;
|
|
94
|
+
this.clientPromise = null;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Get a value from an object using dot notation path
|
|
100
|
+
* @param {object} obj - Object to traverse
|
|
101
|
+
* @param {string} path - Dot-separated path (e.g., "database.password")
|
|
102
|
+
* @returns {string|undefined}
|
|
103
|
+
*/
|
|
104
|
+
function getJsonPath(obj, path) {
|
|
105
|
+
const parts = path.split(".");
|
|
106
|
+
let current = obj;
|
|
107
|
+
for (const part of parts) {
|
|
108
|
+
if (current === null || current === undefined) return undefined;
|
|
109
|
+
current = current[part];
|
|
110
|
+
}
|
|
111
|
+
return typeof current === "string" ? current : JSON.stringify(current);
|
|
112
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenBao provider for colony
|
|
3
|
+
* OpenBao is an API-compatible fork of HashiCorp Vault
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { VaultCompatibleProvider } from "./vault-base.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @class OpenBaoProvider
|
|
10
|
+
* @property {string} prefix - Provider prefix ("OPENBAO")
|
|
11
|
+
*/
|
|
12
|
+
export class OpenBaoProvider extends VaultCompatibleProvider {
|
|
13
|
+
/**
|
|
14
|
+
* @param {object} options
|
|
15
|
+
* @param {string=} options.addr - OpenBao address (default: process.env.BAO_ADDR or "http://127.0.0.1:8200")
|
|
16
|
+
* @param {string=} options.token - OpenBao token (default: process.env.BAO_TOKEN)
|
|
17
|
+
* @param {string=} options.namespace - OpenBao namespace (default: process.env.BAO_NAMESPACE)
|
|
18
|
+
* @param {number=} options.timeout - Request timeout in ms (default: 30000)
|
|
19
|
+
*/
|
|
20
|
+
constructor(options = {}) {
|
|
21
|
+
super(
|
|
22
|
+
{
|
|
23
|
+
prefix: "OPENBAO",
|
|
24
|
+
addrEnvVar: "BAO_ADDR",
|
|
25
|
+
tokenEnvVar: "BAO_TOKEN",
|
|
26
|
+
namespaceEnvVar: "BAO_NAMESPACE",
|
|
27
|
+
errorPrefix: "OpenBao",
|
|
28
|
+
},
|
|
29
|
+
options
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base class for Vault-compatible secret providers (Vault, OpenBao)
|
|
3
|
+
* Both use the same HTTP API, differing only in environment variables and naming.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @class VaultCompatibleProvider
|
|
8
|
+
* @property {string} prefix - Provider prefix
|
|
9
|
+
*/
|
|
10
|
+
export class VaultCompatibleProvider {
|
|
11
|
+
/**
|
|
12
|
+
* @param {object} config
|
|
13
|
+
* @param {string} config.prefix - Provider prefix (e.g., "VAULT", "OPENBAO")
|
|
14
|
+
* @param {string} config.addrEnvVar - Environment variable for address
|
|
15
|
+
* @param {string} config.tokenEnvVar - Environment variable for token
|
|
16
|
+
* @param {string} config.namespaceEnvVar - Environment variable for namespace
|
|
17
|
+
* @param {string} config.errorPrefix - Prefix for error messages
|
|
18
|
+
* @param {object} options - User options
|
|
19
|
+
* @param {string=} options.addr - Server address
|
|
20
|
+
* @param {string=} options.token - Auth token
|
|
21
|
+
* @param {string=} options.namespace - Namespace
|
|
22
|
+
* @param {number=} options.timeout - Request timeout in ms (default: 30000)
|
|
23
|
+
*/
|
|
24
|
+
constructor(config, options = {}) {
|
|
25
|
+
this.prefix = config.prefix;
|
|
26
|
+
this.errorPrefix = config.errorPrefix;
|
|
27
|
+
this.addr = options.addr ?? process.env[config.addrEnvVar] ?? "http://127.0.0.1:8200";
|
|
28
|
+
this.token = options.token ?? process.env[config.tokenEnvVar];
|
|
29
|
+
this.namespace = options.namespace ?? process.env[config.namespaceEnvVar];
|
|
30
|
+
this.timeout = options.timeout ?? 30000;
|
|
31
|
+
this.tokenEnvVar = config.tokenEnvVar;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Fetch a secret value
|
|
36
|
+
* @param {string} key - Secret path, optionally with field: "secret/data/myapp#password"
|
|
37
|
+
* @returns {Promise<string>}
|
|
38
|
+
*/
|
|
39
|
+
async fetch(key) {
|
|
40
|
+
const [path, field] = key.split("#");
|
|
41
|
+
|
|
42
|
+
const url = `${this.addr}/v1/${path}`;
|
|
43
|
+
const headers = {
|
|
44
|
+
"X-Vault-Token": this.token,
|
|
45
|
+
};
|
|
46
|
+
if (this.namespace) {
|
|
47
|
+
headers["X-Vault-Namespace"] = this.namespace;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const response = await fetch(url, {
|
|
51
|
+
headers,
|
|
52
|
+
signal: AbortSignal.timeout(this.timeout),
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
if (response.status === 404) {
|
|
56
|
+
const err = new Error(`Secret not found: ${key}`);
|
|
57
|
+
err.code = "NOT_FOUND";
|
|
58
|
+
throw err;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (!response.ok) {
|
|
62
|
+
throw new Error(`${this.errorPrefix} error: ${response.status} ${response.statusText}`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const data = await response.json();
|
|
66
|
+
|
|
67
|
+
// KV v2 returns data.data.data, KV v1 returns data.data
|
|
68
|
+
const secretData = data.data?.data ?? data.data;
|
|
69
|
+
|
|
70
|
+
if (field) {
|
|
71
|
+
return String(secretData?.[field] ?? "");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (typeof secretData === "object" && secretData !== null) {
|
|
75
|
+
const values = Object.values(secretData);
|
|
76
|
+
if (values.length === 1) return String(values[0]);
|
|
77
|
+
return JSON.stringify(secretData);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return String(secretData ?? "");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Validate provider configuration
|
|
85
|
+
* @returns {Promise<void>}
|
|
86
|
+
*/
|
|
87
|
+
async validate() {
|
|
88
|
+
if (!this.token) {
|
|
89
|
+
throw new Error(`${this.tokenEnvVar} is required`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HashiCorp Vault provider for colony
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { VaultCompatibleProvider } from "./vault-base.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @class VaultProvider
|
|
9
|
+
* @property {string} prefix - Provider prefix ("VAULT")
|
|
10
|
+
*/
|
|
11
|
+
export class VaultProvider extends VaultCompatibleProvider {
|
|
12
|
+
/**
|
|
13
|
+
* @param {object} options
|
|
14
|
+
* @param {string=} options.addr - Vault address (default: process.env.VAULT_ADDR or "http://127.0.0.1:8200")
|
|
15
|
+
* @param {string=} options.token - Vault token (default: process.env.VAULT_TOKEN)
|
|
16
|
+
* @param {string=} options.namespace - Vault namespace (default: process.env.VAULT_NAMESPACE)
|
|
17
|
+
* @param {number=} options.timeout - Request timeout in ms (default: 30000)
|
|
18
|
+
*/
|
|
19
|
+
constructor(options = {}) {
|
|
20
|
+
super(
|
|
21
|
+
{
|
|
22
|
+
prefix: "VAULT",
|
|
23
|
+
addrEnvVar: "VAULT_ADDR",
|
|
24
|
+
tokenEnvVar: "VAULT_TOKEN",
|
|
25
|
+
namespaceEnvVar: "VAULT_NAMESPACE",
|
|
26
|
+
errorPrefix: "Vault",
|
|
27
|
+
},
|
|
28
|
+
options
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
}
|