|
1 | 1 | import { Secret } from "../types"; |
2 | 2 |
|
3 | 3 | type SecretReference = { |
| 4 | + app: string | null; |
4 | 5 | env: string | null; |
5 | | - path: string | null; |
| 6 | + path: string; |
6 | 7 | key: string; |
7 | 8 | }; |
8 | 9 |
|
9 | 10 | export type SecretFetcher = ( |
10 | 11 | env: string, |
11 | 12 | path: string, |
12 | | - key: string |
| 13 | + key: string, |
| 14 | + app?: string | null |
13 | 15 | ) => Promise<Secret>; |
14 | 16 |
|
15 | 17 | // Regex pattern for secret references |
16 | 18 | const REFERENCE_REGEX = |
17 | | - /\${(?:(?<env>[^.\/}]+)\.)?(?:(?<path>[^}]+)\/)?(?<key>[^}]+)}/g; |
| 19 | + /\${(?:(?<app>[^:}]+)::)?(?:(?<env>[^.\/}]+)\.)?(?:(?<path>[^}]+)\/)?(?<key>[^}]+)}/g; |
18 | 20 |
|
19 | | -export const normalizeKey = (env: string, path: string, key: string) => |
20 | | - `${env.toLowerCase()}:${path.replace(/\/+$/, "")}:${key}`; |
| 21 | +export const normalizeKey = (env: string, path: string, key: string, app?: string) => |
| 22 | + `${app ? `${app}:` : ''}${env.toLowerCase()}:${path.replace(/\/+$/, "")}:${key}`; |
21 | 23 |
|
22 | 24 | export function parseSecretReference(reference: string): SecretReference { |
23 | 25 | const match = new RegExp(REFERENCE_REGEX.source).exec(reference); |
24 | 26 | if (!match?.groups) { |
25 | 27 | throw new Error(`Invalid secret reference format: ${reference}`); |
26 | 28 | } |
27 | 29 |
|
28 | | - let { env, path, key } = match.groups; |
29 | | - env = env?.trim() || ""; |
30 | | - key = key.trim(); |
31 | | - path = path ? `/${path.replace(/\.+/g, "/")}`.replace(/\/+/g, "/") : "/"; |
| 30 | + const { app: appMatch, env: envMatch, path: pathMatch, key: keyMatch } = match.groups; |
| 31 | + const app = appMatch?.trim() || null; |
| 32 | + const env = envMatch?.trim() || null; |
| 33 | + const key = keyMatch.trim(); |
| 34 | + const path = pathMatch ? `/${pathMatch.replace(/\.+/g, "/")}`.replace(/\/+/g, "/") : "/"; |
32 | 35 |
|
33 | | - return { env, path, key }; |
| 36 | + return { app, env, path, key }; |
34 | 37 | } |
35 | 38 |
|
36 | 39 | export async function resolveSecretReferences( |
37 | 40 | value: string, |
38 | 41 | currentEnv: string, |
39 | 42 | currentPath: string, |
40 | 43 | fetcher: SecretFetcher, |
41 | | - cache: Map<string, string> = new Map(), |
42 | | - resolutionStack: Set<string> = new Set() |
| 44 | + currentApp?: string | null, |
| 45 | + cache = new Map<string, string>(), |
| 46 | + resolutionStack = new Set<string>() |
43 | 47 | ): Promise<string> { |
| 48 | + // Skip processing if there are no references to resolve |
| 49 | + if (!value.includes("${")) { |
| 50 | + return value; |
| 51 | + } |
| 52 | + |
44 | 53 | const references = Array.from(value.matchAll(REFERENCE_REGEX)); |
45 | 54 | let resolvedValue = value; |
46 | 55 |
|
47 | 56 | for (const ref of references) { |
48 | 57 | try { |
49 | 58 | const { |
| 59 | + app: refApp, |
50 | 60 | env: refEnv, |
51 | 61 | path: refPath, |
52 | 62 | key: refKey, |
53 | 63 | } = parseSecretReference(ref[0]); |
| 64 | + |
| 65 | + const targetApp = refApp || currentApp; |
54 | 66 | const targetEnv = refEnv || currentEnv; |
55 | | - const targetPath = refPath || currentPath; |
56 | | - const cacheKey = normalizeKey(targetEnv, targetPath, refKey); |
| 67 | + const targetPath = refPath || currentPath || "/"; |
| 68 | + |
| 69 | + // Create cache key from normalized values |
| 70 | + const cacheKey = normalizeKey( |
| 71 | + targetEnv || "", |
| 72 | + targetPath, |
| 73 | + refKey, |
| 74 | + targetApp || undefined |
| 75 | + ); |
57 | 76 |
|
| 77 | + // Check for circular references |
58 | 78 | if (resolutionStack.has(cacheKey)) { |
59 | | - throw new Error(`Circular reference detected: ${cacheKey}`); |
| 79 | + console.warn(`Circular reference detected: ${ref[0]} → ${cacheKey}`); |
| 80 | + continue; |
60 | 81 | } |
61 | 82 |
|
| 83 | + // Resolve the reference if not in cache |
62 | 84 | if (!cache.has(cacheKey)) { |
63 | 85 | resolutionStack.add(cacheKey); |
64 | 86 | try { |
65 | | - const secret = await fetcher(targetEnv, targetPath, refKey); |
| 87 | + // Fetch the referenced secret |
| 88 | + const secret = await fetcher(targetEnv || "", targetPath, refKey, targetApp); |
| 89 | + |
| 90 | + // Recursively resolve any references in the secret value |
66 | 91 | const resolvedSecretValue = await resolveSecretReferences( |
67 | 92 | secret.value, |
68 | 93 | targetEnv, |
69 | 94 | targetPath, |
70 | 95 | fetcher, |
| 96 | + targetApp, |
71 | 97 | cache, |
72 | 98 | resolutionStack |
73 | 99 | ); |
| 100 | + |
74 | 101 | cache.set(cacheKey, resolvedSecretValue); |
| 102 | + } catch (error: any) { |
| 103 | + console.warn(`Failed to resolve reference ${ref[0]}: ${error.message || error}`); |
| 104 | + resolutionStack.delete(cacheKey); |
| 105 | + continue; |
75 | 106 | } finally { |
76 | 107 | resolutionStack.delete(cacheKey); |
77 | 108 | } |
78 | 109 | } |
79 | 110 |
|
| 111 | + // Replace the reference with its resolved value |
80 | 112 | resolvedValue = resolvedValue.replace(ref[0], cache.get(cacheKey)!); |
81 | | - } catch (error) { |
82 | | - console.error(`Error resolving reference ${ref[0]}:`, error); |
83 | | - throw error; |
| 113 | + } catch (error: any) { |
| 114 | + console.warn(`Error resolving reference ${ref[0]}: ${error.message || error}`); |
84 | 115 | } |
85 | 116 | } |
86 | 117 |
|
|
0 commit comments