@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.
Files changed (62) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +172 -0
  3. package/dist/cjs/cli.js +281 -0
  4. package/dist/cjs/cli.js.map +7 -0
  5. package/dist/cjs/index.js +383 -0
  6. package/dist/cjs/index.js.map +7 -0
  7. package/dist/cjs/package.json +3 -0
  8. package/dist/cjs/parser.js +319 -0
  9. package/dist/cjs/parser.js.map +7 -0
  10. package/dist/cjs/providers/aws.js +115 -0
  11. package/dist/cjs/providers/aws.js.map +7 -0
  12. package/dist/cjs/providers/openbao.js +49 -0
  13. package/dist/cjs/providers/openbao.js.map +7 -0
  14. package/dist/cjs/providers/vault-base.js +98 -0
  15. package/dist/cjs/providers/vault-base.js.map +7 -0
  16. package/dist/cjs/providers/vault.js +49 -0
  17. package/dist/cjs/providers/vault.js.map +7 -0
  18. package/dist/cjs/resolver.js +247 -0
  19. package/dist/cjs/resolver.js.map +7 -0
  20. package/dist/cjs/secrets.js +238 -0
  21. package/dist/cjs/secrets.js.map +7 -0
  22. package/dist/cjs/strings.js +99 -0
  23. package/dist/cjs/strings.js.map +7 -0
  24. package/dist/cjs/util.js +74 -0
  25. package/dist/cjs/util.js.map +7 -0
  26. package/dist/esm/cli.js +281 -0
  27. package/dist/esm/cli.js.map +7 -0
  28. package/dist/esm/index.d.ts +342 -0
  29. package/dist/esm/index.js +347 -0
  30. package/dist/esm/index.js.map +7 -0
  31. package/dist/esm/package.json +3 -0
  32. package/dist/esm/parser.js +286 -0
  33. package/dist/esm/parser.js.map +7 -0
  34. package/dist/esm/providers/aws.js +82 -0
  35. package/dist/esm/providers/aws.js.map +7 -0
  36. package/dist/esm/providers/openbao.js +26 -0
  37. package/dist/esm/providers/openbao.js.map +7 -0
  38. package/dist/esm/providers/vault-base.js +75 -0
  39. package/dist/esm/providers/vault-base.js.map +7 -0
  40. package/dist/esm/providers/vault.js +26 -0
  41. package/dist/esm/providers/vault.js.map +7 -0
  42. package/dist/esm/resolver.js +224 -0
  43. package/dist/esm/resolver.js.map +7 -0
  44. package/dist/esm/secrets.js +209 -0
  45. package/dist/esm/secrets.js.map +7 -0
  46. package/dist/esm/strings.js +75 -0
  47. package/dist/esm/strings.js.map +7 -0
  48. package/dist/esm/util.js +47 -0
  49. package/dist/esm/util.js.map +7 -0
  50. package/package.json +66 -0
  51. package/src/cli.js +353 -0
  52. package/src/index.d.ts +342 -0
  53. package/src/index.js +473 -0
  54. package/src/parser.js +381 -0
  55. package/src/providers/aws.js +112 -0
  56. package/src/providers/openbao.js +32 -0
  57. package/src/providers/vault-base.js +92 -0
  58. package/src/providers/vault.js +31 -0
  59. package/src/resolver.js +286 -0
  60. package/src/secrets.js +313 -0
  61. package/src/strings.js +84 -0
  62. package/src/util.js +49 -0
@@ -0,0 +1,342 @@
1
+ /**
2
+ * Type definitions for colony
3
+ */
4
+
5
+ export interface SandboxOptions {
6
+ /** Restrict @include paths to this directory */
7
+ basePath?: string;
8
+ /** Whitelist of allowed environment variables for ${ENV:*} (null = allow all) */
9
+ allowedEnvVars?: string[] | null;
10
+ /** Whitelist of allowed custom variables for ${VAR:*} (null = allow all) */
11
+ allowedVars?: string[] | null;
12
+ /** Maximum depth for nested includes (default: 50) */
13
+ maxIncludeDepth?: number;
14
+ /** Maximum file size in bytes for included files */
15
+ maxFileSize?: number;
16
+ }
17
+
18
+ /**
19
+ * Secret provider interface for custom integrations
20
+ */
21
+ export interface SecretProvider {
22
+ /** Unique prefix for this provider (e.g., "AWS", "VAULT") */
23
+ readonly prefix: string;
24
+ /** Fetch a secret value by key/path */
25
+ fetch(key: string): Promise<string>;
26
+ /** Optional: validate configuration on registration */
27
+ validate?(): Promise<void>;
28
+ /** Optional: cleanup resources */
29
+ dispose?(): Promise<void>;
30
+ }
31
+
32
+ export interface SecretCacheOptions {
33
+ /** Enable caching (default: true) */
34
+ enabled?: boolean;
35
+ /** Cache TTL in milliseconds (default: 300000 = 5 minutes) */
36
+ ttl?: number;
37
+ /** Maximum number of cached secrets (default: 100) */
38
+ maxSize?: number;
39
+ }
40
+
41
+ export interface SecretsOptions {
42
+ /** Secret providers to use (e.g., AwsSecretsProvider) */
43
+ providers?: SecretProvider[];
44
+ /** Whitelist of allowed secret patterns (glob supported, null = allow all) */
45
+ allowedSecrets?: string[] | null;
46
+ /** Cache settings */
47
+ cache?: SecretCacheOptions;
48
+ /** Behavior when secret not found: 'empty' returns "", 'warn' adds warning, 'error' throws */
49
+ onNotFound?: "empty" | "warn" | "error";
50
+ }
51
+
52
+ export interface LoadColonyOptions {
53
+ /** Entry colony file path */
54
+ entry: string;
55
+ /** Dimension names (e.g., ["env", "realm", "region"]) */
56
+ dims?: string[];
57
+ /** Context values for scope matching */
58
+ ctx?: Record<string, string>;
59
+ /** Custom variables for ${VAR:*} interpolation */
60
+ vars?: Record<string, string>;
61
+ /** Schema validation hook (supports sync and async) */
62
+ schema?: (cfg: ColonyConfig) => ColonyConfig | Promise<ColonyConfig>;
63
+ /** Security sandbox options */
64
+ sandbox?: SandboxOptions;
65
+ /** Warn when skipping already-visited includes */
66
+ warnOnSkippedIncludes?: boolean;
67
+ /** Secrets provider options */
68
+ secrets?: SecretsOptions;
69
+ }
70
+
71
+ export interface Warning {
72
+ type:
73
+ | "blocked_env_var"
74
+ | "blocked_var"
75
+ | "unknown_var"
76
+ | "unknown_ctx"
77
+ | "unknown_interpolation"
78
+ | "skipped_include"
79
+ | "blocked_secret"
80
+ | "secret_not_found"
81
+ | "secret_fetch_error"
82
+ | "unknown_provider";
83
+ message: string;
84
+ var?: string;
85
+ file?: string;
86
+ pattern?: string;
87
+ /** Provider name (for secret warnings) */
88
+ provider?: string;
89
+ /** Secret key (for secret warnings) */
90
+ key?: string;
91
+ }
92
+
93
+ export interface TraceInfo {
94
+ /** Operator used (=, :=, |=, +=, -=) */
95
+ op: string;
96
+ /** Scope segments that matched */
97
+ scope: string[];
98
+ /** Specificity score (number of non-* segments) */
99
+ specificity: number;
100
+ /** File path where the rule was defined */
101
+ filePath: string;
102
+ /** Line number in the file */
103
+ line: number;
104
+ /** Column number in the file */
105
+ col: number;
106
+ /** Raw key from the rule */
107
+ keyRaw: string;
108
+ /** Source location string (filePath:line:col) */
109
+ source: string;
110
+ }
111
+
112
+ export interface DiffResult {
113
+ /** Keys present in other but not in this config */
114
+ added: string[];
115
+ /** Keys present in this config but not in other */
116
+ removed: string[];
117
+ /** Keys present in both but with different values */
118
+ changed: Array<{
119
+ key: string;
120
+ from: unknown;
121
+ to: unknown;
122
+ }>;
123
+ }
124
+
125
+ export interface ColonyConfig {
126
+ /** Get a value by dot-notation path */
127
+ get(path: string): unknown;
128
+ /** Get trace info for how a key was set */
129
+ explain(path: string): TraceInfo | null;
130
+ /** Serialize to plain object */
131
+ toJSON(): Record<string, unknown>;
132
+ /** List all leaf keys in dot notation */
133
+ keys(): string[];
134
+ /** Compare with another config */
135
+ diff(other: ColonyConfig | Record<string, unknown>): DiffResult;
136
+ /** Internal trace data */
137
+ readonly _trace: Map<string, TraceInfo>;
138
+ /** Warnings generated during resolution */
139
+ readonly _warnings: Warning[];
140
+ /** Allow indexing with any string key */
141
+ [key: string]: unknown;
142
+ }
143
+
144
+ export interface ValidationResult {
145
+ /** Whether all files are valid */
146
+ valid: boolean;
147
+ /** List of all files that were checked */
148
+ files: string[];
149
+ /** List of errors found */
150
+ errors: Array<{
151
+ file: string;
152
+ error: string;
153
+ }>;
154
+ }
155
+
156
+ export interface DiffColonyOptions extends Omit<LoadColonyOptions, "ctx"> {
157
+ /** First context to compare */
158
+ ctx1: Record<string, string>;
159
+ /** Second context to compare */
160
+ ctx2: Record<string, string>;
161
+ }
162
+
163
+ export interface DiffColonyResult {
164
+ /** Config resolved with ctx1 */
165
+ cfg1: ColonyConfig;
166
+ /** Config resolved with ctx2 */
167
+ cfg2: ColonyConfig;
168
+ /** Differences between the two configs */
169
+ diff: DiffResult;
170
+ }
171
+
172
+ /**
173
+ * Load and resolve a colony configuration file
174
+ */
175
+ export function loadColony(options: LoadColonyOptions): Promise<ColonyConfig>;
176
+
177
+ /**
178
+ * Validate syntax of colony files without resolving
179
+ */
180
+ export function validateColony(entry: string): Promise<ValidationResult>;
181
+
182
+ /**
183
+ * List all files that would be included (dry run)
184
+ */
185
+ export function dryRunIncludes(entry: string): Promise<string[]>;
186
+
187
+ /**
188
+ * Compare configs loaded with different contexts
189
+ */
190
+ export function diffColony(options: DiffColonyOptions): Promise<DiffColonyResult>;
191
+
192
+ export interface LintIssue {
193
+ /** Type of issue found */
194
+ type: "parse_error" | "shadowed_rule" | "overridden_wildcard" | "empty_include";
195
+ /** Severity level */
196
+ severity: "error" | "warning" | "info";
197
+ /** Human-readable message */
198
+ message: string;
199
+ /** File where the issue was found */
200
+ file?: string;
201
+ /** Line number in the file */
202
+ line?: number;
203
+ }
204
+
205
+ export interface LintColonyOptions {
206
+ /** Entry colony file path */
207
+ entry: string;
208
+ /** Dimension names (e.g., ["env", "realm", "region"]) */
209
+ dims?: string[];
210
+ }
211
+
212
+ export interface LintColonyResult {
213
+ /** List of issues found */
214
+ issues: LintIssue[];
215
+ }
216
+
217
+ /**
218
+ * Lint colony files for potential issues
219
+ */
220
+ export function lintColony(options: LintColonyOptions): Promise<LintColonyResult>;
221
+
222
+ // ============================================================================
223
+ // Secrets Management
224
+ // ============================================================================
225
+
226
+ /**
227
+ * Register a secret provider globally (available to all loadColony calls)
228
+ */
229
+ export function registerSecretProvider(provider: SecretProvider): void;
230
+
231
+ /**
232
+ * Unregister a secret provider by prefix
233
+ */
234
+ export function unregisterSecretProvider(prefix: string): boolean;
235
+
236
+ /**
237
+ * Clear all globally registered secret providers
238
+ */
239
+ export function clearSecretProviders(): void;
240
+
241
+ /**
242
+ * AWS Secrets Manager provider
243
+ *
244
+ * @example
245
+ * ```ts
246
+ * import { loadColony, AwsSecretsProvider } from "@ant.sh/colony";
247
+ *
248
+ * const cfg = await loadColony({
249
+ * entry: "./config/app.colony",
250
+ * secrets: {
251
+ * providers: [new AwsSecretsProvider({ region: "us-east-1" })],
252
+ * },
253
+ * });
254
+ * ```
255
+ *
256
+ * Config usage:
257
+ * ```
258
+ * *.db.password = "${AWS:myapp/db#password}";
259
+ * ```
260
+ */
261
+ export class AwsSecretsProvider implements SecretProvider {
262
+ readonly prefix: "AWS";
263
+
264
+ /**
265
+ * @param options.region - AWS region (default: process.env.AWS_REGION or "us-east-1")
266
+ */
267
+ constructor(options?: { region?: string });
268
+
269
+ fetch(key: string): Promise<string>;
270
+ validate(): Promise<void>;
271
+ dispose(): Promise<void>;
272
+ }
273
+
274
+ /**
275
+ * HashiCorp Vault provider
276
+ *
277
+ * @example
278
+ * ```ts
279
+ * import { loadColony, VaultProvider } from "@ant.sh/colony";
280
+ *
281
+ * const cfg = await loadColony({
282
+ * entry: "./config/app.colony",
283
+ * secrets: {
284
+ * providers: [new VaultProvider({ addr: "https://vault.example.com" })],
285
+ * },
286
+ * });
287
+ * ```
288
+ *
289
+ * Config usage:
290
+ * ```
291
+ * *.api.key = "${VAULT:secret/data/myapp#api_key}";
292
+ * ```
293
+ */
294
+ export class VaultProvider implements SecretProvider {
295
+ readonly prefix: "VAULT";
296
+
297
+ /**
298
+ * @param options.addr - Vault address (default: process.env.VAULT_ADDR or "http://127.0.0.1:8200")
299
+ * @param options.token - Vault token (default: process.env.VAULT_TOKEN)
300
+ * @param options.namespace - Vault namespace (default: process.env.VAULT_NAMESPACE)
301
+ * @param options.timeout - Request timeout in ms (default: 30000)
302
+ */
303
+ constructor(options?: { addr?: string; token?: string; namespace?: string; timeout?: number });
304
+
305
+ fetch(key: string): Promise<string>;
306
+ validate(): Promise<void>;
307
+ }
308
+
309
+ /**
310
+ * OpenBao provider (API-compatible Vault fork)
311
+ *
312
+ * @example
313
+ * ```ts
314
+ * import { loadColony, OpenBaoProvider } from "@ant.sh/colony";
315
+ *
316
+ * const cfg = await loadColony({
317
+ * entry: "./config/app.colony",
318
+ * secrets: {
319
+ * providers: [new OpenBaoProvider({ addr: "https://bao.example.com" })],
320
+ * },
321
+ * });
322
+ * ```
323
+ *
324
+ * Config usage:
325
+ * ```
326
+ * *.api.key = "${OPENBAO:secret/data/myapp#api_key}";
327
+ * ```
328
+ */
329
+ export class OpenBaoProvider implements SecretProvider {
330
+ readonly prefix: "OPENBAO";
331
+
332
+ /**
333
+ * @param options.addr - OpenBao address (default: process.env.BAO_ADDR or "http://127.0.0.1:8200")
334
+ * @param options.token - OpenBao token (default: process.env.BAO_TOKEN)
335
+ * @param options.namespace - OpenBao namespace (default: process.env.BAO_NAMESPACE)
336
+ * @param options.timeout - Request timeout in ms (default: 30000)
337
+ */
338
+ constructor(options?: { addr?: string; token?: string; namespace?: string; timeout?: number });
339
+
340
+ fetch(key: string): Promise<string>;
341
+ validate(): Promise<void>;
342
+ }
@@ -0,0 +1,347 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import fg from "fast-glob";
4
+ import { parseColony } from "./parser.js";
5
+ import { resolveRules } from "./resolver.js";
6
+ import {
7
+ applySecretsDeep,
8
+ SecretCache,
9
+ hasGlobalProviders,
10
+ registerSecretProvider,
11
+ unregisterSecretProvider,
12
+ clearSecretProviders
13
+ } from "./secrets.js";
14
+ import { AwsSecretsProvider } from "./providers/aws.js";
15
+ import { VaultProvider } from "./providers/vault.js";
16
+ import { OpenBaoProvider } from "./providers/openbao.js";
17
+ async function loadColony(opts) {
18
+ const entry = opts?.entry;
19
+ if (!entry) throw new Error("loadColony: opts.entry is required");
20
+ const sandbox = opts.sandbox ?? {};
21
+ const basePath = sandbox.basePath ? path.resolve(sandbox.basePath) : null;
22
+ const maxIncludeDepth = sandbox.maxIncludeDepth ?? 50;
23
+ const maxFileSize = sandbox.maxFileSize ?? null;
24
+ const warnOnSkippedIncludes = opts.warnOnSkippedIncludes ?? false;
25
+ const visited = /* @__PURE__ */ new Set();
26
+ const warnings = [];
27
+ const files = await expandIncludes(entry, visited, {
28
+ basePath,
29
+ maxIncludeDepth,
30
+ maxFileSize,
31
+ warnOnSkippedIncludes,
32
+ warnings
33
+ });
34
+ const parsed = [];
35
+ for (const file of files) {
36
+ const text = await fs.readFile(file, "utf8");
37
+ parsed.push(parseColony(text, { filePath: file }));
38
+ }
39
+ const dims = (Array.isArray(opts.dims) && opts.dims.length ? opts.dims : null) ?? parsed.find((p) => p.dims?.length)?.dims ?? ["env"];
40
+ const envDefaults = mergeEnvDefaults(parsed.map((p) => p.envDefaults ?? {}));
41
+ const ctx = {
42
+ ...envDefaults,
43
+ env: process.env.NODE_ENV ?? "dev",
44
+ ...opts.ctx
45
+ };
46
+ const vars = { ROOT: process.cwd(), ...opts.vars ?? {} };
47
+ const requires = parsed.flatMap((p) => p.requires ?? []);
48
+ const allRules = parsed.flatMap((p) => p.rules);
49
+ const allowedEnvVars = sandbox.allowedEnvVars ?? null;
50
+ const allowedVars = sandbox.allowedVars ?? null;
51
+ let cfg = resolveRules({ rules: allRules, dims, ctx, vars, allowedEnvVars, allowedVars, warnings });
52
+ const secretsOpts = opts.secrets ?? {};
53
+ if (secretsOpts.providers?.length || hasGlobalProviders()) {
54
+ const cacheOpts = secretsOpts.cache ?? {};
55
+ const cache = cacheOpts.enabled !== false ? new SecretCache(cacheOpts.maxSize ?? 100) : null;
56
+ const secretified = await applySecretsDeep(cfg, {
57
+ providers: secretsOpts.providers ?? [],
58
+ allowedSecrets: secretsOpts.allowedSecrets ?? null,
59
+ cache,
60
+ cacheTtl: cacheOpts.ttl ?? 3e5,
61
+ onNotFound: secretsOpts.onNotFound ?? "warn",
62
+ warnings
63
+ });
64
+ copyConfigMethods(secretified, cfg, warnings);
65
+ cfg = secretified;
66
+ }
67
+ const missing = [];
68
+ for (const reqKey of requires) {
69
+ if (cfg.get(reqKey) === void 0) missing.push(reqKey);
70
+ }
71
+ if (missing.length) {
72
+ throw new Error(
73
+ `COLONY @require failed (missing keys):
74
+ ` + missing.map((k) => ` - ${k}`).join("\n")
75
+ );
76
+ }
77
+ Object.defineProperty(cfg, "_warnings", { enumerable: false, value: warnings });
78
+ if (typeof opts.schema === "function") {
79
+ const result = opts.schema(cfg);
80
+ if (result && typeof result.then === "function") {
81
+ const validated = await result;
82
+ if (validated && validated !== cfg) {
83
+ copyConfigMethods(validated, cfg, warnings);
84
+ return validated;
85
+ }
86
+ } else if (result && result !== cfg) {
87
+ copyConfigMethods(result, cfg, warnings);
88
+ return result;
89
+ }
90
+ }
91
+ return cfg;
92
+ }
93
+ function copyConfigMethods(target, source, warnings) {
94
+ Object.defineProperties(target, {
95
+ get: { enumerable: false, value: source.get },
96
+ explain: { enumerable: false, value: source.explain },
97
+ toJSON: { enumerable: false, value: source.toJSON },
98
+ keys: { enumerable: false, value: source.keys },
99
+ diff: { enumerable: false, value: source.diff },
100
+ _trace: { enumerable: false, value: source._trace },
101
+ _warnings: { enumerable: false, value: warnings }
102
+ });
103
+ }
104
+ function mergeEnvDefaults(list) {
105
+ const out = {};
106
+ for (const m of list) {
107
+ for (const [k, v] of Object.entries(m)) out[k] = v;
108
+ }
109
+ return out;
110
+ }
111
+ async function expandIncludes(entry, visited, { basePath, maxIncludeDepth, maxFileSize, warnOnSkippedIncludes, warnings }) {
112
+ const absEntry = path.resolve(entry);
113
+ const out = [];
114
+ await dfs(absEntry, 0);
115
+ return out;
116
+ async function dfs(file, depth) {
117
+ if (depth > maxIncludeDepth) {
118
+ throw new Error(`COLONY: Max include depth (${maxIncludeDepth}) exceeded at: ${file}`);
119
+ }
120
+ const abs = path.resolve(file);
121
+ if (visited.has(abs)) {
122
+ if (warnOnSkippedIncludes) {
123
+ warnings.push({ type: "skipped_include", file: abs, message: `Skipping already-visited include: ${abs}` });
124
+ }
125
+ return;
126
+ }
127
+ visited.add(abs);
128
+ if (maxFileSize !== null) {
129
+ const stat = await fs.stat(abs);
130
+ if (stat.size > maxFileSize) {
131
+ throw new Error(`COLONY: File size (${stat.size} bytes) exceeds maxFileSize (${maxFileSize} bytes): ${abs}`);
132
+ }
133
+ }
134
+ const text = await fs.readFile(abs, "utf8");
135
+ const { includes } = parseColony(text, { filePath: abs, parseOnlyDirectives: true });
136
+ for (const inc of includes) {
137
+ const incAbs = path.resolve(path.dirname(abs), inc);
138
+ if (basePath !== null) {
139
+ const normalizedInc = path.normalize(incAbs);
140
+ if (!normalizedInc.startsWith(basePath + path.sep) && normalizedInc !== basePath) {
141
+ throw new Error(
142
+ `COLONY: Path traversal blocked. Include "${inc}" resolves to "${normalizedInc}" which is outside basePath "${basePath}"`
143
+ );
144
+ }
145
+ }
146
+ const matches = await fg(incAbs.replace(/\\/g, "/"), { dot: true });
147
+ for (const m of matches.sort((a, b) => a.localeCompare(b))) {
148
+ if (basePath !== null) {
149
+ const normalizedMatch = path.normalize(m);
150
+ if (!normalizedMatch.startsWith(basePath + path.sep) && normalizedMatch !== basePath) {
151
+ throw new Error(
152
+ `COLONY: Path traversal blocked. Glob match "${m}" is outside basePath "${basePath}"`
153
+ );
154
+ }
155
+ }
156
+ await dfs(m, depth + 1);
157
+ }
158
+ }
159
+ out.push(abs);
160
+ }
161
+ }
162
+ async function validateColony(entry) {
163
+ const visited = /* @__PURE__ */ new Set();
164
+ const files = [];
165
+ const errors = [];
166
+ await validateDfs(path.resolve(entry));
167
+ return {
168
+ valid: errors.length === 0,
169
+ files,
170
+ errors
171
+ };
172
+ async function validateDfs(file) {
173
+ const abs = path.resolve(file);
174
+ if (visited.has(abs)) return;
175
+ visited.add(abs);
176
+ try {
177
+ const text = await fs.readFile(abs, "utf8");
178
+ const { includes } = parseColony(text, { filePath: abs });
179
+ files.push(abs);
180
+ for (const inc of includes) {
181
+ const incAbs = path.resolve(path.dirname(abs), inc);
182
+ const matches = await fg(incAbs.replace(/\\/g, "/"), { dot: true });
183
+ for (const m of matches.sort((a, b) => a.localeCompare(b))) {
184
+ await validateDfs(m);
185
+ }
186
+ }
187
+ } catch (e) {
188
+ errors.push({ file: abs, error: e.message });
189
+ }
190
+ }
191
+ }
192
+ async function dryRunIncludes(entry) {
193
+ const visited = /* @__PURE__ */ new Set();
194
+ const files = [];
195
+ await dryRunDfs(path.resolve(entry));
196
+ return files;
197
+ async function dryRunDfs(file) {
198
+ const abs = path.resolve(file);
199
+ if (visited.has(abs)) return;
200
+ visited.add(abs);
201
+ const text = await fs.readFile(abs, "utf8");
202
+ const { includes } = parseColony(text, { filePath: abs, parseOnlyDirectives: true });
203
+ for (const inc of includes) {
204
+ const incAbs = path.resolve(path.dirname(abs), inc);
205
+ const matches = await fg(incAbs.replace(/\\/g, "/"), { dot: true });
206
+ for (const m of matches.sort((a, b) => a.localeCompare(b))) {
207
+ await dryRunDfs(m);
208
+ }
209
+ }
210
+ files.push(abs);
211
+ }
212
+ }
213
+ async function diffColony(opts) {
214
+ const { ctx1, ctx2, ...baseOpts } = opts;
215
+ if (!ctx1 || !ctx2) {
216
+ throw new Error("diffColony: both ctx1 and ctx2 are required");
217
+ }
218
+ const cfg1 = await loadColony({ ...baseOpts, ctx: ctx1 });
219
+ const cfg2 = await loadColony({ ...baseOpts, ctx: ctx2 });
220
+ return {
221
+ cfg1,
222
+ cfg2,
223
+ diff: cfg1.diff(cfg2)
224
+ };
225
+ }
226
+ async function lintColony(opts) {
227
+ const entry = opts?.entry;
228
+ if (!entry) throw new Error("lintColony: opts.entry is required");
229
+ const issues = [];
230
+ const visited = /* @__PURE__ */ new Set();
231
+ const allRules = [];
232
+ const allFiles = [];
233
+ let foundDims = null;
234
+ await collectRules(path.resolve(entry));
235
+ async function collectRules(file) {
236
+ const abs = path.resolve(file);
237
+ if (visited.has(abs)) return;
238
+ visited.add(abs);
239
+ try {
240
+ const text = await fs.readFile(abs, "utf8");
241
+ const parsed = parseColony(text, { filePath: abs });
242
+ allFiles.push(abs);
243
+ if (!foundDims && parsed.dims?.length) {
244
+ foundDims = parsed.dims;
245
+ }
246
+ for (const rule of parsed.rules) {
247
+ allRules.push({ ...rule, filePath: abs });
248
+ }
249
+ for (const inc of parsed.includes) {
250
+ const incAbs = path.resolve(path.dirname(abs), inc);
251
+ const matches = await fg(incAbs.replace(/\\/g, "/"), { dot: true });
252
+ for (const m of matches.sort((a, b) => a.localeCompare(b))) {
253
+ await collectRules(m);
254
+ }
255
+ }
256
+ } catch (e) {
257
+ issues.push({
258
+ type: "parse_error",
259
+ severity: "error",
260
+ message: e.message,
261
+ file: abs
262
+ });
263
+ }
264
+ }
265
+ const dims = opts.dims ?? foundDims ?? ["env"];
266
+ const rulesByKey = /* @__PURE__ */ new Map();
267
+ for (const rule of allRules) {
268
+ const scope = rule.keySegments.slice(0, dims.length).join(".");
269
+ const keyPath = rule.keySegments.slice(dims.length).join(".");
270
+ const key = `${scope}|${keyPath}`;
271
+ if (!rulesByKey.has(key)) {
272
+ rulesByKey.set(key, []);
273
+ }
274
+ rulesByKey.get(key).push(rule);
275
+ }
276
+ for (const [key, rules] of rulesByKey.entries()) {
277
+ if (rules.length > 1) {
278
+ const locations = rules.map((r) => `${r.filePath}:${r.line}`);
279
+ const uniqueLocations = new Set(locations);
280
+ if (uniqueLocations.size > 1) {
281
+ const [scope, keyPath] = key.split("|");
282
+ issues.push({
283
+ type: "shadowed_rule",
284
+ severity: "warning",
285
+ message: `Rule "${scope}.${keyPath}" is defined ${rules.length} times. Later rule wins.`,
286
+ file: rules[rules.length - 1].filePath,
287
+ line: rules[rules.length - 1].line
288
+ });
289
+ }
290
+ }
291
+ }
292
+ for (const rule of allRules) {
293
+ const scope = rule.keySegments.slice(0, dims.length);
294
+ const keyPath = rule.keySegments.slice(dims.length).join(".");
295
+ if (scope.every((s) => s === "*")) {
296
+ const moreSpecific = allRules.filter((r) => {
297
+ const rKeyPath = r.keySegments.slice(dims.length).join(".");
298
+ if (rKeyPath !== keyPath) return false;
299
+ const rScope = r.keySegments.slice(0, dims.length);
300
+ return rScope.some((s) => s !== "*") && r !== rule;
301
+ });
302
+ if (moreSpecific.length > 0) {
303
+ issues.push({
304
+ type: "overridden_wildcard",
305
+ severity: "info",
306
+ message: `Wildcard rule for "${keyPath}" is overridden by ${moreSpecific.length} more specific rule(s)`,
307
+ file: rule.filePath,
308
+ line: rule.line
309
+ });
310
+ }
311
+ }
312
+ }
313
+ for (const file of allFiles) {
314
+ try {
315
+ const text = await fs.readFile(file, "utf8");
316
+ const parsed = parseColony(text, { filePath: file });
317
+ for (const inc of parsed.includes) {
318
+ const incAbs = path.resolve(path.dirname(file), inc);
319
+ const matches = await fg(incAbs.replace(/\\/g, "/"), { dot: true });
320
+ if (matches.length === 0) {
321
+ issues.push({
322
+ type: "empty_include",
323
+ severity: "warning",
324
+ message: `Include pattern "${inc}" matches no files`,
325
+ file
326
+ });
327
+ }
328
+ }
329
+ } catch {
330
+ }
331
+ }
332
+ return { issues };
333
+ }
334
+ export {
335
+ AwsSecretsProvider,
336
+ OpenBaoProvider,
337
+ VaultProvider,
338
+ clearSecretProviders,
339
+ diffColony,
340
+ dryRunIncludes,
341
+ lintColony,
342
+ loadColony,
343
+ registerSecretProvider,
344
+ unregisterSecretProvider,
345
+ validateColony
346
+ };
347
+ //# sourceMappingURL=index.js.map