adfinem 0.0.0 → 0.1.1

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 (112) hide show
  1. package/.env.example +13 -0
  2. package/CHANGELOG.md +17 -0
  3. package/CODE_OF_CONDUCT.md +21 -0
  4. package/CONTRIBUTING.md +29 -0
  5. package/LICENSE +21 -0
  6. package/README.md +97 -3
  7. package/SECURITY.md +13 -0
  8. package/catalogs/.gitkeep +0 -0
  9. package/catalogs/api-operations.yaml +21 -0
  10. package/catalogs/batches.yaml +74 -0
  11. package/catalogs/queries.yaml +75 -0
  12. package/config/environments.yaml +13 -0
  13. package/dist/actions/assert-db.js +3 -0
  14. package/dist/actions/run-eod.js +3 -0
  15. package/dist/adapters/api/api-collections.js +296 -0
  16. package/dist/adapters/api/body-utils.js +9 -0
  17. package/dist/adapters/api/rest-client.js +557 -0
  18. package/dist/adapters/api/soap-client.js +5 -0
  19. package/dist/adapters/db/assertions.js +87 -0
  20. package/dist/adapters/db/oracle-client.js +115 -0
  21. package/dist/adapters/db/query-catalog.js +75 -0
  22. package/dist/adapters/unix/batch-catalog.js +71 -0
  23. package/dist/adapters/unix/batch-input-files.js +36 -0
  24. package/dist/adapters/unix/batch-runner.js +382 -0
  25. package/dist/adapters/unix/ssh-client.js +228 -0
  26. package/dist/app/server.js +827 -0
  27. package/dist/cli.js +516 -0
  28. package/dist/config/environments.js +138 -0
  29. package/dist/config/registry.js +18 -0
  30. package/dist/config/secrets.js +123 -0
  31. package/dist/dsl/parser.js +20 -0
  32. package/dist/dsl/schema.js +182 -0
  33. package/dist/dsl/types.js +1 -0
  34. package/dist/dsl/validator.js +264 -0
  35. package/dist/engine/captures.js +68 -0
  36. package/dist/engine/context.js +69 -0
  37. package/dist/engine/evidence.js +33 -0
  38. package/dist/engine/known-errors.js +129 -0
  39. package/dist/engine/retry.js +13 -0
  40. package/dist/engine/runner.js +710 -0
  41. package/dist/engine/step-result.js +58 -0
  42. package/dist/flows/catalog-normalizer.js +72 -0
  43. package/dist/flows/compiler.js +237 -0
  44. package/dist/flows/concat.js +130 -0
  45. package/dist/flows/parser.js +21 -0
  46. package/dist/flows/schema.js +142 -0
  47. package/dist/flows/types.js +1 -0
  48. package/dist/flows/validator.js +470 -0
  49. package/dist/reports/html-report.js +112 -0
  50. package/dist/reports/junit-report.js +48 -0
  51. package/docs/.gitkeep +0 -0
  52. package/docs/DB_UNIX_OPERATIONS.md +118 -0
  53. package/docs/FLOW_BUILDER.md +87 -0
  54. package/flows/account_processing_cycle.flow.yaml +88 -0
  55. package/flows/new_flow.flow.yaml +22 -0
  56. package/package.json +98 -11
  57. package/scenarios/smoke/account-processing-smoke.yaml +44 -0
  58. package/scenarios/smoke/api-db-batch-check.yaml +40 -0
  59. package/src/actions/assert-db.ts +6 -0
  60. package/src/actions/run-eod.ts +6 -0
  61. package/src/adapters/api/api-collections.ts +375 -0
  62. package/src/adapters/api/body-utils.ts +10 -0
  63. package/src/adapters/api/rest-client.ts +587 -0
  64. package/src/adapters/api/soap-client.ts +7 -0
  65. package/src/adapters/db/assertions.ts +83 -0
  66. package/src/adapters/db/oracle-client.ts +133 -0
  67. package/src/adapters/db/query-catalog.ts +80 -0
  68. package/src/adapters/unix/batch-catalog.ts +81 -0
  69. package/src/adapters/unix/batch-input-files.ts +39 -0
  70. package/src/adapters/unix/batch-runner.ts +456 -0
  71. package/src/adapters/unix/ssh-client.ts +248 -0
  72. package/src/app/server.ts +914 -0
  73. package/src/cli.ts +517 -0
  74. package/src/config/environments.ts +193 -0
  75. package/src/config/registry.ts +23 -0
  76. package/src/config/secrets.ts +128 -0
  77. package/src/dsl/parser.ts +24 -0
  78. package/src/dsl/schema.ts +189 -0
  79. package/src/dsl/types.ts +371 -0
  80. package/src/dsl/validator.ts +282 -0
  81. package/src/engine/captures.ts +66 -0
  82. package/src/engine/context.ts +76 -0
  83. package/src/engine/evidence.ts +35 -0
  84. package/src/engine/known-errors.ts +145 -0
  85. package/src/engine/retry.ts +11 -0
  86. package/src/engine/runner.ts +746 -0
  87. package/src/engine/step-result.ts +64 -0
  88. package/src/flows/catalog-normalizer.ts +86 -0
  89. package/src/flows/compiler.ts +247 -0
  90. package/src/flows/concat.ts +149 -0
  91. package/src/flows/parser.ts +27 -0
  92. package/src/flows/schema.ts +154 -0
  93. package/src/flows/types.ts +130 -0
  94. package/src/flows/validator.ts +468 -0
  95. package/src/llm/system-prompt.md +9 -0
  96. package/src/reports/html-report.ts +113 -0
  97. package/src/reports/junit-report.ts +55 -0
  98. package/src/types/oracledb.d.ts +1 -0
  99. package/templates/.gitkeep +0 -0
  100. package/templates/api/create-test-case.json +5 -0
  101. package/templates/api/record-test-activity.json +6 -0
  102. package/tsconfig.json +15 -0
  103. package/vite.config.ts +17 -0
  104. package/web/index.html +12 -0
  105. package/web/src/App.tsx +6588 -0
  106. package/web/src/main.tsx +10 -0
  107. package/web/src/styles.css +3147 -0
  108. package/web-dist/assets/elk.bundled-ChwRCIWJ.js +24 -0
  109. package/web-dist/assets/index-CArbX4zm.css +1 -0
  110. package/web-dist/assets/index-vDCbj8xB.js +28 -0
  111. package/web-dist/index.html +13 -0
  112. package/index.js +0 -1
@@ -0,0 +1,193 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { mkdir, writeFile } from "node:fs/promises";
3
+ import { dirname, join } from "node:path";
4
+ import { config as loadDotEnv } from "dotenv";
5
+ import YAML from "yaml";
6
+
7
+ export interface EnvironmentConfig {
8
+ name: string;
9
+ apiBaseUrl?: string;
10
+ apiTlsInsecure?: boolean;
11
+ oracle: {
12
+ user?: string;
13
+ password?: string;
14
+ connectString?: string;
15
+ };
16
+ sshHosts: Record<string, {
17
+ host?: string;
18
+ username?: string;
19
+ password?: string;
20
+ privateKeyPath?: string;
21
+ shell?: string;
22
+ loginShell?: boolean;
23
+ }>;
24
+ }
25
+
26
+ export interface EditableEnvironmentConfig {
27
+ apiBaseUrl?: string;
28
+ apiTlsInsecure?: boolean;
29
+ oracle?: {
30
+ user?: string;
31
+ password?: string;
32
+ connectString?: string;
33
+ };
34
+ sshHosts?: Record<string, {
35
+ host?: string;
36
+ username?: string;
37
+ password?: string;
38
+ privateKeyPath?: string;
39
+ shell?: string;
40
+ loginShell?: boolean;
41
+ }>;
42
+ }
43
+
44
+ export type EnvironmentFile = Record<string, EditableEnvironmentConfig>;
45
+
46
+ export function getEnvironment(name?: string, rootDir = process.cwd()): EnvironmentConfig {
47
+ loadWorkspaceDotEnv(rootDir);
48
+ const resolvedName = name || process.env.ADFINEM_ENV || "local";
49
+ const raw = readFileSync(environmentConfigPath(rootDir), "utf8");
50
+ const configs = interpolateEnv(YAML.parse(raw)) as Record<string, EnvironmentConfig>;
51
+ const config = configs[resolvedName];
52
+
53
+ if (!config) {
54
+ const available = Object.keys(configs).filter((key) => key && typeof configs[key] === "object");
55
+ const suffix = available.length
56
+ ? ` Available: ${available.join(", ")}.`
57
+ : " No environments are defined yet.";
58
+ throw new Error(`Unknown environment '${resolvedName}'.${suffix} Add it to config/environments.yaml or pass --env.`);
59
+ }
60
+
61
+ return {
62
+ name: resolvedName,
63
+ apiBaseUrl: blankToUndefined(config.apiBaseUrl),
64
+ apiTlsInsecure: booleanValue(config.apiTlsInsecure),
65
+ oracle: {
66
+ user: blankToUndefined(config.oracle?.user),
67
+ password: blankToUndefined(config.oracle?.password),
68
+ connectString: blankToUndefined(config.oracle?.connectString)
69
+ },
70
+ sshHosts: Object.fromEntries(Object.entries(config.sshHosts ?? {}).map(([hostRef, host]) => [hostRef, {
71
+ host: blankToUndefined(host.host),
72
+ username: blankToUndefined(host.username),
73
+ password: blankToUndefined(host.password),
74
+ privateKeyPath: blankToUndefined(host.privateKeyPath),
75
+ shell: blankToUndefined(host.shell),
76
+ loginShell: booleanValue(host.loginShell)
77
+ }]))
78
+ };
79
+ }
80
+
81
+ export function listEnvironmentNames(rootDir = process.cwd()): string[] {
82
+ try {
83
+ return Object.keys(loadEnvironmentFile(rootDir));
84
+ } catch {
85
+ return [];
86
+ }
87
+ }
88
+
89
+ export function environmentConfigPath(rootDir = process.cwd()): string {
90
+ return join(rootDir, "config", "environments.yaml");
91
+ }
92
+
93
+ export function loadEnvironmentFile(rootDir = process.cwd()): EnvironmentFile {
94
+ loadWorkspaceDotEnv(rootDir);
95
+ const raw = readFileSync(environmentConfigPath(rootDir), "utf8");
96
+ const parsed = YAML.parse(raw) as Record<string, unknown> | null;
97
+ if (!parsed || typeof parsed !== "object") return {};
98
+ return Object.fromEntries(
99
+ Object.entries(parsed)
100
+ .filter(([name, value]) => name && value && typeof value === "object" && !Array.isArray(value))
101
+ .map(([name, value]) => [name, normalizeEditableEnvironment(value as Record<string, unknown>)])
102
+ );
103
+ }
104
+
105
+ export async function writeEnvironmentFile(rootDir: string, environments: EnvironmentFile): Promise<void> {
106
+ const outputPath = environmentConfigPath(rootDir);
107
+ const normalized = Object.fromEntries(
108
+ Object.entries(environments)
109
+ .filter(([name]) => isValidEnvironmentName(name))
110
+ .map(([name, config]) => [name, normalizeEditableEnvironment(config as Record<string, unknown>)])
111
+ );
112
+ await mkdir(dirname(outputPath), { recursive: true });
113
+ await writeFile(outputPath, YAML.stringify(normalized, { defaultKeyType: "PLAIN", defaultStringType: "QUOTE_DOUBLE" }), "utf8");
114
+ }
115
+
116
+ export function isValidEnvironmentName(name: string): boolean {
117
+ return /^[A-Za-z][A-Za-z0-9_-]*$/.test(name);
118
+ }
119
+
120
+ export function assertValidEnvironmentName(name: string): void {
121
+ if (!isValidEnvironmentName(name)) {
122
+ throw new Error("Environment name must start with a letter and contain only letters, numbers, underscore, or hyphen.");
123
+ }
124
+ }
125
+
126
+ function interpolateEnv(value: unknown): unknown {
127
+ if (typeof value === "string") {
128
+ return value.replace(/\$\{([A-Za-z0-9_]+)\}/g, (_match, name: string) => process.env[name] ?? "");
129
+ }
130
+ if (Array.isArray(value)) return value.map(interpolateEnv);
131
+ if (value && typeof value === "object") {
132
+ return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, interpolateEnv(entry)]));
133
+ }
134
+ return value;
135
+ }
136
+
137
+ function loadWorkspaceDotEnv(rootDir: string): void {
138
+ loadDotEnv({ path: join(rootDir, ".env"), override: true });
139
+ }
140
+
141
+ function blankToUndefined(value: string | undefined): string | undefined {
142
+ return value && value.trim() ? value : undefined;
143
+ }
144
+
145
+ function normalizeEditableEnvironment(value: Record<string, unknown>): EditableEnvironmentConfig {
146
+ const oracle = objectValue(value.oracle);
147
+ const sshHosts = objectValue(value.sshHosts);
148
+ return {
149
+ apiBaseUrl: optionalString(value.apiBaseUrl),
150
+ apiTlsInsecure: optionalBoolean(value.apiTlsInsecure),
151
+ oracle: {
152
+ user: optionalString(oracle?.user),
153
+ password: optionalString(oracle?.password),
154
+ connectString: optionalString(oracle?.connectString)
155
+ },
156
+ sshHosts: Object.fromEntries(
157
+ Object.entries(sshHosts ?? {})
158
+ .filter(([, entry]) => entry && typeof entry === "object" && !Array.isArray(entry))
159
+ .map(([hostRef, entry]) => {
160
+ const host = entry as Record<string, unknown>;
161
+ return [hostRef, {
162
+ host: optionalString(host.host),
163
+ username: optionalString(host.username),
164
+ password: optionalString(host.password),
165
+ privateKeyPath: optionalString(host.privateKeyPath),
166
+ shell: optionalString(host.shell),
167
+ loginShell: optionalBoolean(host.loginShell)
168
+ }];
169
+ })
170
+ )
171
+ };
172
+ }
173
+
174
+ function objectValue(value: unknown): Record<string, unknown> | undefined {
175
+ return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : undefined;
176
+ }
177
+
178
+ function optionalString(value: unknown): string | undefined {
179
+ return value === undefined || value === null ? undefined : String(value);
180
+ }
181
+
182
+ function optionalBoolean(value: unknown): boolean | undefined {
183
+ if (value === undefined || value === null || value === "") return undefined;
184
+ if (typeof value === "boolean") return value;
185
+ const normalized = String(value).trim().toLowerCase();
186
+ if (["1", "true", "yes", "on"].includes(normalized)) return true;
187
+ if (["0", "false", "no", "off"].includes(normalized)) return false;
188
+ return undefined;
189
+ }
190
+
191
+ function booleanValue(value: unknown): boolean | undefined {
192
+ return optionalBoolean(value);
193
+ }
@@ -0,0 +1,23 @@
1
+ export interface RegisteredAction {
2
+ layer: "api" | "db" | "unix" | "engine";
3
+ supportedVia: string[];
4
+ }
5
+
6
+ export const registeredActions: Record<string, RegisteredAction> = {
7
+ db_assert: {
8
+ layer: "db",
9
+ supportedVia: ["db"]
10
+ },
11
+ db_query: {
12
+ layer: "db",
13
+ supportedVia: ["db"]
14
+ },
15
+ db_execute: {
16
+ layer: "db",
17
+ supportedVia: ["db"]
18
+ },
19
+ unix_batch: {
20
+ layer: "unix",
21
+ supportedVia: ["unix"]
22
+ }
23
+ };
@@ -0,0 +1,128 @@
1
+ import type { EvidenceVisibilityMode } from "../dsl/types.js";
2
+
3
+ const secretKeyFragments = [
4
+ "password",
5
+ "passwd",
6
+ "pwd",
7
+ "token",
8
+ "authorization",
9
+ "apikey",
10
+ "api_key",
11
+ "privatekey",
12
+ "private_key",
13
+ "jwt",
14
+ "secret",
15
+ "credential",
16
+ "bearer",
17
+ "access_token",
18
+ "refreshtoken",
19
+ "refresh_token",
20
+ "servicepassword",
21
+ "apikey",
22
+ "session",
23
+ "cookie",
24
+ "pin",
25
+ "cvv",
26
+ "cvc",
27
+ "cvv2"
28
+ ];
29
+
30
+ const cardKeyExact = new Set([
31
+ "pan",
32
+ "pans",
33
+ "cardnumber",
34
+ "cardnumbers",
35
+ "cardno",
36
+ "primaryaccountnumber",
37
+ "primaryaccountnumbers"
38
+ ]);
39
+
40
+ const REDACTED = "<redacted>";
41
+
42
+ export function redactSecrets<T>(value: T): T {
43
+ return redact(value) as T;
44
+ }
45
+
46
+ export function evidenceVisibilityMode(): EvidenceVisibilityMode {
47
+ return process.env.ADFINEM_EVIDENCE_VISIBILITY?.toLowerCase() === "redacted" ? "redacted" : "raw";
48
+ }
49
+
50
+ export function applyEvidenceVisibility<T>(value: T, mode: EvidenceVisibilityMode = evidenceVisibilityMode()): T {
51
+ return mode === "redacted" ? redactSecrets(value) : value;
52
+ }
53
+
54
+ /**
55
+ * Heavier redaction intended for shared evidence artifacts.
56
+ * Combines key-based redaction with inline PAN-pattern scrubbing on every string.
57
+ */
58
+ export function redactEvidence<T>(value: T): T {
59
+ return redactDeep(value, true) as T;
60
+ }
61
+
62
+ export function maskInlinePans(text: string): string {
63
+ if (!text) return text;
64
+ return text.replace(/(?<!\d)(\d[\d\s.-]{10,21}\d)(?!\d)/g, (match) => {
65
+ const digits = match.replace(/\D+/g, "");
66
+ if (digits.length < 12 || digits.length > 19) return match;
67
+ if (!isLuhnValid(digits)) return match;
68
+ return `****${digits.slice(-4)}`;
69
+ });
70
+ }
71
+
72
+ export function isLuhnValid(digits: string): boolean {
73
+ if (!/^\d+$/.test(digits)) return false;
74
+ let sum = 0;
75
+ let alt = false;
76
+ for (let index = digits.length - 1; index >= 0; index--) {
77
+ let n = digits.charCodeAt(index) - 48;
78
+ if (alt) {
79
+ n *= 2;
80
+ if (n > 9) n -= 9;
81
+ }
82
+ sum += n;
83
+ alt = !alt;
84
+ }
85
+ return sum > 0 && sum % 10 === 0;
86
+ }
87
+
88
+ function redact(value: unknown): unknown {
89
+ return redactDeep(value, false);
90
+ }
91
+
92
+ function redactDeep(value: unknown, scrubStrings: boolean): unknown {
93
+ if (typeof value === "string") return scrubStrings ? maskInlinePans(value) : value;
94
+ if (Array.isArray(value)) return value.map((entry) => redactDeep(entry, scrubStrings));
95
+ if (!value || typeof value !== "object") return value;
96
+
97
+ return Object.fromEntries(Object.entries(value).map(([key, entry]) => {
98
+ const normalized = key.toLowerCase().replace(/[^a-z0-9]/g, "");
99
+ if (secretKeyFragments.some((fragment) => normalized.includes(fragment))) {
100
+ return [key, REDACTED];
101
+ }
102
+ if (cardKeyExact.has(normalized)) {
103
+ return [key, maskCardLike(entry, scrubStrings)];
104
+ }
105
+ return [key, redactDeep(entry, scrubStrings)];
106
+ }));
107
+ }
108
+
109
+ function maskCardLike(value: unknown, scrubStrings: boolean): unknown {
110
+ if (Array.isArray(value)) return value.map((entry) => maskCardLike(entry, scrubStrings));
111
+ if (typeof value === "string" || typeof value === "number") {
112
+ return maskPan(String(value));
113
+ }
114
+ if (value && typeof value === "object") {
115
+ return redactDeep(value, scrubStrings);
116
+ }
117
+ return value;
118
+ }
119
+
120
+ function maskPan(value: string): string {
121
+ const digits = value.replace(/\D+/g, "");
122
+ if (digits.length < 12) return value;
123
+ if (digits.length >= 12 && digits.length <= 19) {
124
+ return `${digits.slice(0, 4)}${"*".repeat(Math.max(4, digits.length - 8))}${digits.slice(-4)}`;
125
+ }
126
+ const last4 = digits.slice(-4);
127
+ return `****${last4}`;
128
+ }
@@ -0,0 +1,24 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import YAML from "yaml";
3
+ import { scenarioSchema, queryCatalogSchema, batchCatalogSchema, apiOperationsCatalogSchema } from "./schema.js";
4
+ import type { Catalogs, QueryCatalogEntry, Scenario } from "./types.js";
5
+ import { importedOperationsFromCollections, loadApiCollections } from "../adapters/api/api-collections.js";
6
+ import { normalizeQueryCatalog } from "../adapters/db/query-catalog.js";
7
+
8
+ export async function loadYamlFile<T>(path: string): Promise<T> {
9
+ const raw = await readFile(path, "utf8");
10
+ return YAML.parse(raw) as T;
11
+ }
12
+
13
+ export async function loadScenario(path: string): Promise<Scenario> {
14
+ const parsed = await loadYamlFile<unknown>(path);
15
+ return scenarioSchema.parse(parsed) as Scenario;
16
+ }
17
+
18
+ export async function loadCatalogs(rootDir: string): Promise<Catalogs> {
19
+ const queries = normalizeQueryCatalog(queryCatalogSchema.parse(await loadYamlFile<unknown>(`${rootDir}/catalogs/queries.yaml`)) as Record<string, QueryCatalogEntry>);
20
+ const batches = batchCatalogSchema.parse(await loadYamlFile<unknown>(`${rootDir}/catalogs/batches.yaml`));
21
+ const apiOperations = apiOperationsCatalogSchema.parse(await loadYamlFile<unknown>(`${rootDir}/catalogs/api-operations.yaml`));
22
+ const importedApiOperations = importedOperationsFromCollections(await loadApiCollections(rootDir));
23
+ return { queries, batches, apiOperations: { ...apiOperations, ...importedApiOperations } } as Catalogs;
24
+ }
@@ -0,0 +1,189 @@
1
+ import { z } from "zod";
2
+
3
+ const value = z.union([z.string(), z.number(), z.boolean(), z.null()]);
4
+ const catalogParamType = z.enum(["string", "number", "boolean", "string[]", "number[]", "boolean[]"]);
5
+ const apiMethod = z.enum(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]);
6
+ const apiBodyMode = z.enum(["none", "json", "raw", "formdata", "urlencoded"]);
7
+ const apiRequestSpecSchema = z.object({
8
+ method: apiMethod.optional(),
9
+ path: z.string().optional(),
10
+ headers: z.record(z.string()).optional(),
11
+ query: z.record(z.unknown()).optional(),
12
+ body: z.unknown().optional(),
13
+ rawBody: z.string().optional(),
14
+ bodyMode: apiBodyMode.optional(),
15
+ auth: z.unknown().optional(),
16
+ acceptStatuses: z.array(z.number().int().min(100).max(599)).optional()
17
+ }).strict();
18
+ const apiAssertionSchema = z.discriminatedUnion("type", [
19
+ z.object({
20
+ type: z.literal("status"),
21
+ operator: z.enum(["in", "="]).optional(),
22
+ value: z.union([z.number().int().min(100).max(599), z.array(z.number().int().min(100).max(599))])
23
+ }).strict(),
24
+ z.object({ type: z.literal("jsonpath_exists"), path: z.string().min(1) }).strict(),
25
+ z.object({ type: z.literal("jsonpath_equals"), path: z.string().min(1), value: z.unknown() }).strict(),
26
+ z.object({ type: z.literal("jsonpath_contains"), path: z.string().min(1), value: z.unknown() }).strict(),
27
+ z.object({ type: z.literal("header_exists"), header: z.string().min(1) }).strict(),
28
+ z.object({ type: z.literal("header_equals"), header: z.string().min(1), value: z.string() }).strict(),
29
+ z.object({ type: z.literal("body_contains"), value: z.string() }).strict(),
30
+ z.object({ type: z.literal("body_not_contains"), value: z.string() }).strict()
31
+ ]);
32
+ const expectedOutcomeSchema = z.enum(["positive", "negative", "setup", "teardown"]);
33
+ const parallelJoinModeSchema = z.enum(["all", "any", "fail_fast"]);
34
+ const loopDateFormatSchema = z.enum(["YYYY-MM-DD", "DD/MM/YYYY", "MM/DD/YYYY"]);
35
+ const loopDateCursorSchema = z.object({
36
+ outputName: z.string().min(1).optional(),
37
+ start: z.string().min(1).optional(),
38
+ inputFormat: loopDateFormatSchema.optional(),
39
+ outputFormat: loopDateFormatSchema.optional(),
40
+ advance: z.object({
41
+ mode: z.enum(["days", "months", "nth_day_of_month", "first_of_month", "end_of_month"]),
42
+ amount: z.number().int().positive().optional(),
43
+ day: z.number().int().min(1).max(31).optional()
44
+ }).strict().optional()
45
+ }).strict();
46
+ const loopSpecSchema = z.object({
47
+ mode: z.enum(["count", "foreach"]),
48
+ count: z.union([z.number().int().nonnegative(), z.string()]).optional(),
49
+ items: z.unknown().optional(),
50
+ itemName: z.string().min(1).optional(),
51
+ maxIterations: z.number().int().positive().optional(),
52
+ dateCursor: loopDateCursorSchema.optional()
53
+ }).strict();
54
+ const catalogParamSchema = z.object({
55
+ required: z.boolean().optional(),
56
+ type: catalogParamType.optional(),
57
+ pattern: z.string().optional(),
58
+ luhn: z.boolean().optional()
59
+ }).strict();
60
+ const batchInputFileSchema = z.object({
61
+ name: z.string().min(1),
62
+ required: z.boolean().optional(),
63
+ remotePath: z.string().min(1).optional(),
64
+ paramName: z.string().min(1).optional(),
65
+ appendAsArg: z.boolean().optional()
66
+ }).strict();
67
+ const batchOutputFileSchema = z.object({
68
+ name: z.string().min(1),
69
+ required: z.boolean().optional(),
70
+ source: z.enum(["stdout", "stderr", "both", "explicit"]).optional(),
71
+ pathPattern: z.string().min(1).optional(),
72
+ remotePath: z.string().min(1).optional(),
73
+ download: z.boolean().optional(),
74
+ decrypt: z.object({
75
+ command: z.string().min(1).optional(),
76
+ outputRemotePath: z.string().min(1).optional(),
77
+ required: z.boolean().optional()
78
+ }).strict().optional()
79
+ }).strict();
80
+
81
+ export const scenarioStepSchema: z.ZodType<unknown> = z.lazy(() => z.object({
82
+ id: z.string().min(1).regex(/^[A-Za-z0-9_-]+$/),
83
+ action: z.string().min(1),
84
+ via: z.string().optional(),
85
+ retry: z.object({
86
+ attempts: z.number().int().positive().optional(),
87
+ delaySeconds: z.number().nonnegative().optional()
88
+ }).strict().optional(),
89
+ input: z.record(z.unknown()).optional(),
90
+ params: z.record(z.unknown()).optional(),
91
+ query: z.string().optional(),
92
+ batch: z.string().optional(),
93
+ request: apiRequestSpecSchema.optional(),
94
+ assertions: z.array(apiAssertionSchema).optional(),
95
+ capture: z.record(z.string()).optional(),
96
+ continueOnFailure: z.boolean().optional(),
97
+ expectedOutcome: expectedOutcomeSchema.optional(),
98
+ captureOnFailure: z.boolean().optional(),
99
+ control: z.enum(["parallel", "loop"]).optional(),
100
+ branches: z.array(z.object({
101
+ id: z.string().min(1).regex(/^[A-Za-z0-9_-]+$/),
102
+ label: z.string().min(1).optional(),
103
+ steps: z.array(scenarioStepSchema)
104
+ }).strict()).optional(),
105
+ steps: z.array(scenarioStepSchema).optional(),
106
+ loop: loopSpecSchema.optional(),
107
+ join: parallelJoinModeSchema.optional()
108
+ }).strict());
109
+
110
+ export const scenarioSchema = z.object({
111
+ id: z.string().min(1).regex(/^[A-Za-z0-9_-]+$/),
112
+ environment: z.string().min(1),
113
+ tenant: z.record(z.string()).optional(),
114
+ variables: z.record(z.union([value, z.array(value), z.record(value)])).optional(),
115
+ steps: z.array(scenarioStepSchema).min(1)
116
+ }).strict();
117
+
118
+ export const queryCatalogSchema = z.record(z.object({
119
+ description: z.string().optional(),
120
+ mode: z.enum(["query", "execute"]).optional(),
121
+ sql: z.string().min(1),
122
+ params: z.record(catalogParamSchema).optional(),
123
+ expect: z.object({
124
+ type: z.enum(["number", "string", "boolean", "rowCount"]),
125
+ column: z.string().optional(),
126
+ operator: z.enum(["=", "!=", ">", ">=", "<", "<=", "contains"]),
127
+ value: z.unknown()
128
+ }).strict()
129
+ .refine((expect) => expect.type === "rowCount" || Boolean(expect.column), {
130
+ message: "expect.column is required when expect.type is not rowCount."
131
+ })
132
+ .optional(),
133
+ captures: z.record(z.string()).optional(),
134
+ maxRows: z.number().int().positive().optional()
135
+ }).strict());
136
+
137
+ export const batchCatalogSchema = z.record(z.object({
138
+ description: z.string().optional(),
139
+ hostRef: z.string().min(1),
140
+ command: z.string().min(1).refine((value) => !/[\r\n]/.test(value), {
141
+ message: "command must be a single executable token; put arguments in fixedArgs."
142
+ }),
143
+ fixedArgs: z.array(z.union([z.string(), z.number(), z.boolean()])).optional(),
144
+ workingDirectory: z.string().min(1).optional(),
145
+ useWorkingDirectory: z.boolean().optional(),
146
+ environment: z.record(z.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/), z.union([z.string(), z.number(), z.boolean()])).optional(),
147
+ args: z.array(z.object({
148
+ name: z.string().min(1),
149
+ required: z.boolean().optional(),
150
+ type: catalogParamType.optional(),
151
+ pattern: z.string().optional(),
152
+ luhn: z.boolean().optional()
153
+ }).strict()).optional(),
154
+ inputFiles: z.array(batchInputFileSchema).optional(),
155
+ outputFiles: z.array(batchOutputFileSchema).optional(),
156
+ timeoutSeconds: z.number().int().positive().optional(),
157
+ success: z.object({
158
+ exitCodes: z.array(z.number().int()).optional(),
159
+ requiredOutput: z.array(z.string()).optional()
160
+ }).strict().optional(),
161
+ captures: z.record(z.string()).optional()
162
+ }).strict());
163
+
164
+ export const apiOperationsCatalogSchema = z.record(z.object({
165
+ description: z.string().optional(),
166
+ type: z.enum(["rest", "soap"]),
167
+ method: apiMethod.optional(),
168
+ path: z.string().optional(),
169
+ headers: z.record(z.string()).optional(),
170
+ query: z.record(z.unknown()).optional(),
171
+ body: z.unknown().optional(),
172
+ rawBody: z.string().optional(),
173
+ bodyMode: apiBodyMode.optional(),
174
+ auth: z.unknown().optional(),
175
+ params: z.record(catalogParamSchema).optional(),
176
+ assertions: z.array(apiAssertionSchema).optional(),
177
+ requestTemplate: z.string().optional(),
178
+ captures: z.record(z.string()).optional(),
179
+ acceptStatuses: z.array(z.number().int().min(100).max(599)).optional(),
180
+ idempotent: z.boolean().optional(),
181
+ source: z.object({
182
+ collectionId: z.string().optional(),
183
+ collectionName: z.string().optional(),
184
+ requestId: z.string().optional(),
185
+ folderPath: z.array(z.string()).optional()
186
+ }).strict().optional()
187
+ }).strict());
188
+
189
+ export { apiRequestSpecSchema, apiAssertionSchema };