adfinem 0.0.0 → 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 (107) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/CODE_OF_CONDUCT.md +21 -0
  3. package/CONTRIBUTING.md +29 -0
  4. package/LICENSE +21 -0
  5. package/README.md +86 -2
  6. package/SECURITY.md +13 -0
  7. package/catalogs/.gitkeep +0 -0
  8. package/catalogs/api-operations.yaml +21 -0
  9. package/catalogs/batches.yaml +74 -0
  10. package/catalogs/queries.yaml +75 -0
  11. package/config/environments.yaml +13 -0
  12. package/dist/actions/assert-db.js +3 -0
  13. package/dist/actions/run-eod.js +3 -0
  14. package/dist/adapters/api/api-collections.js +296 -0
  15. package/dist/adapters/api/body-utils.js +9 -0
  16. package/dist/adapters/api/rest-client.js +557 -0
  17. package/dist/adapters/api/soap-client.js +5 -0
  18. package/dist/adapters/db/assertions.js +87 -0
  19. package/dist/adapters/db/oracle-client.js +115 -0
  20. package/dist/adapters/db/query-catalog.js +75 -0
  21. package/dist/adapters/unix/batch-catalog.js +71 -0
  22. package/dist/adapters/unix/batch-input-files.js +36 -0
  23. package/dist/adapters/unix/batch-runner.js +382 -0
  24. package/dist/adapters/unix/ssh-client.js +228 -0
  25. package/dist/app/server.js +826 -0
  26. package/dist/cli.js +465 -0
  27. package/dist/config/environments.js +138 -0
  28. package/dist/config/registry.js +18 -0
  29. package/dist/config/secrets.js +123 -0
  30. package/dist/dsl/parser.js +20 -0
  31. package/dist/dsl/schema.js +182 -0
  32. package/dist/dsl/types.js +1 -0
  33. package/dist/dsl/validator.js +264 -0
  34. package/dist/engine/captures.js +68 -0
  35. package/dist/engine/context.js +69 -0
  36. package/dist/engine/evidence.js +33 -0
  37. package/dist/engine/known-errors.js +129 -0
  38. package/dist/engine/retry.js +13 -0
  39. package/dist/engine/runner.js +710 -0
  40. package/dist/engine/step-result.js +58 -0
  41. package/dist/flows/catalog-normalizer.js +72 -0
  42. package/dist/flows/compiler.js +237 -0
  43. package/dist/flows/concat.js +130 -0
  44. package/dist/flows/parser.js +21 -0
  45. package/dist/flows/schema.js +142 -0
  46. package/dist/flows/types.js +1 -0
  47. package/dist/flows/validator.js +470 -0
  48. package/dist/reports/html-report.js +112 -0
  49. package/dist/reports/junit-report.js +48 -0
  50. package/docs/.gitkeep +0 -0
  51. package/docs/DB_UNIX_OPERATIONS.md +118 -0
  52. package/docs/FLOW_BUILDER.md +87 -0
  53. package/flows/account_processing_cycle.flow.yaml +88 -0
  54. package/flows/new_flow.flow.yaml +22 -0
  55. package/package.json +92 -7
  56. package/scenarios/smoke/account-processing-smoke.yaml +44 -0
  57. package/scenarios/smoke/api-db-batch-check.yaml +40 -0
  58. package/src/actions/assert-db.ts +6 -0
  59. package/src/actions/run-eod.ts +6 -0
  60. package/src/adapters/api/api-collections.ts +375 -0
  61. package/src/adapters/api/body-utils.ts +10 -0
  62. package/src/adapters/api/rest-client.ts +587 -0
  63. package/src/adapters/api/soap-client.ts +7 -0
  64. package/src/adapters/db/assertions.ts +83 -0
  65. package/src/adapters/db/oracle-client.ts +133 -0
  66. package/src/adapters/db/query-catalog.ts +80 -0
  67. package/src/adapters/unix/batch-catalog.ts +81 -0
  68. package/src/adapters/unix/batch-input-files.ts +39 -0
  69. package/src/adapters/unix/batch-runner.ts +456 -0
  70. package/src/adapters/unix/ssh-client.ts +248 -0
  71. package/src/app/server.ts +913 -0
  72. package/src/cli.ts +466 -0
  73. package/src/config/environments.ts +193 -0
  74. package/src/config/registry.ts +23 -0
  75. package/src/config/secrets.ts +128 -0
  76. package/src/dsl/parser.ts +24 -0
  77. package/src/dsl/schema.ts +189 -0
  78. package/src/dsl/types.ts +371 -0
  79. package/src/dsl/validator.ts +282 -0
  80. package/src/engine/captures.ts +66 -0
  81. package/src/engine/context.ts +76 -0
  82. package/src/engine/evidence.ts +35 -0
  83. package/src/engine/known-errors.ts +145 -0
  84. package/src/engine/retry.ts +11 -0
  85. package/src/engine/runner.ts +746 -0
  86. package/src/engine/step-result.ts +64 -0
  87. package/src/flows/catalog-normalizer.ts +86 -0
  88. package/src/flows/compiler.ts +247 -0
  89. package/src/flows/concat.ts +149 -0
  90. package/src/flows/parser.ts +27 -0
  91. package/src/flows/schema.ts +154 -0
  92. package/src/flows/types.ts +130 -0
  93. package/src/flows/validator.ts +468 -0
  94. package/src/llm/system-prompt.md +9 -0
  95. package/src/reports/html-report.ts +113 -0
  96. package/src/reports/junit-report.ts +55 -0
  97. package/src/types/oracledb.d.ts +1 -0
  98. package/templates/.gitkeep +0 -0
  99. package/templates/api/create-test-case.json +5 -0
  100. package/templates/api/record-test-activity.json +6 -0
  101. package/tsconfig.json +15 -0
  102. package/vite.config.ts +17 -0
  103. package/web/index.html +12 -0
  104. package/web/src/App.tsx +6588 -0
  105. package/web/src/main.tsx +10 -0
  106. package/web/src/styles.css +3147 -0
  107. package/index.js +0 -1
@@ -0,0 +1,66 @@
1
+ import { JSONPath } from "jsonpath-plus";
2
+
3
+ export function extractCaptures(payload: unknown, specs: Record<string, string> | undefined): Record<string, unknown> {
4
+ const captures: Record<string, unknown> = {};
5
+ for (const [name, expression] of Object.entries(specs ?? {})) {
6
+ const value = extractCaptureValue(payload, expression);
7
+ if (value !== undefined) captures[name] = value;
8
+ }
9
+ return captures;
10
+ }
11
+
12
+ export function mergeCaptureSpecs(...specs: Array<Record<string, string> | undefined>): Record<string, string> {
13
+ return Object.assign({}, ...specs.filter(Boolean));
14
+ }
15
+
16
+ function extractCaptureValue(payload: unknown, expression: string): unknown {
17
+ const { expression: trimmed, required } = parseCaptureExpression(expression);
18
+ if (!trimmed) throw new Error("Capture expression must not be empty.");
19
+ try {
20
+ if (trimmed.startsWith("literal:")) return trimmed.slice("literal:".length);
21
+ if (trimmed.startsWith("regex:")) return extractRegexCapture(payload, trimmed);
22
+ return extractJsonPathCapture(payload, normalizeJsonPath(trimmed), trimmed);
23
+ } catch (error) {
24
+ if (!required && isCaptureNoMatch(error)) return undefined;
25
+ throw error;
26
+ }
27
+ }
28
+
29
+ function parseCaptureExpression(expression: string): { expression: string; required: boolean } {
30
+ const trimmed = expression.trim();
31
+ if (trimmed.startsWith("optional:")) {
32
+ return { expression: trimmed.slice("optional:".length).trim(), required: false };
33
+ }
34
+ return { expression: trimmed, required: true };
35
+ }
36
+
37
+ function extractJsonPathCapture(payload: unknown, jsonPath: string, original: string): unknown {
38
+ const json = payload as string | number | boolean | object | unknown[] | null;
39
+ const values = JSONPath({ path: jsonPath, json, wrap: true }) as unknown as unknown[];
40
+ if (values.length === 0) throw new Error(`Capture expression '${original}' did not match any value.`);
41
+ return values.length === 1 ? values[0] : values;
42
+ }
43
+
44
+ function extractRegexCapture(payload: unknown, expression: string): string {
45
+ const parts = expression.split(":");
46
+ if (parts.length < 3) {
47
+ throw new Error(`Invalid regex capture '${expression}'. Use regex:<jsonpath>:<pattern>.`);
48
+ }
49
+ const sourcePath = parts[1]?.trim() || "$";
50
+ const pattern = parts.slice(2).join(":");
51
+ const source = extractJsonPathCapture(payload, normalizeJsonPath(sourcePath), sourcePath);
52
+ const match = String(source ?? "").match(new RegExp(pattern, "m"));
53
+ if (!match) throw new Error(`Regex capture '${expression}' did not match.`);
54
+ return match[1] ?? match[0];
55
+ }
56
+
57
+ function normalizeJsonPath(expression: string): string {
58
+ if (expression.startsWith("$")) return expression;
59
+ if (/^[A-Za-z_][A-Za-z0-9_]*(?:[.[].*)?$/.test(expression)) return `$.${expression}`;
60
+ throw new Error(`Unsupported capture expression '${expression}'. Use JSONPath, simple property path, literal:, or regex:.`);
61
+ }
62
+
63
+ function isCaptureNoMatch(error: unknown): boolean {
64
+ const message = error instanceof Error ? error.message : String(error);
65
+ return /did not match any value|did not match\./i.test(message);
66
+ }
@@ -0,0 +1,76 @@
1
+ import type { Scenario } from "../dsl/types.js";
2
+
3
+ export class RunContext {
4
+ private readonly values = new Map<string, unknown>();
5
+
6
+ constructor(scenario: Scenario) {
7
+ for (const [key, value] of Object.entries(scenario.variables ?? {})) {
8
+ this.values.set(key, value);
9
+ }
10
+ for (const [key, value] of Object.entries(scenario.tenant ?? {})) {
11
+ this.values.set(`tenant.${key}`, value);
12
+ }
13
+ }
14
+
15
+ set(name: string, value: unknown): void {
16
+ this.values.set(name, value);
17
+ }
18
+
19
+ get(name: string): unknown {
20
+ return this.values.get(name);
21
+ }
22
+
23
+ snapshot(): Record<string, unknown> {
24
+ return Object.fromEntries(this.values);
25
+ }
26
+
27
+ restore(snapshot: Record<string, unknown>): void {
28
+ this.values.clear();
29
+ for (const [key, value] of Object.entries(snapshot)) this.values.set(key, value);
30
+ }
31
+
32
+ resolve<T>(value: T): T {
33
+ return this.resolveValue(value) as T;
34
+ }
35
+
36
+ private resolveValue(value: unknown): unknown {
37
+ if (typeof value === "string") {
38
+ const exact = /^\$\{([^}]+)\}$/.exec(value);
39
+ if (exact) {
40
+ const resolved = this.values.get(exact[1]);
41
+ if (resolved === undefined || resolved === null) {
42
+ throw new Error(`Variable '${exact[1]}' is not available in run context.`);
43
+ }
44
+ return resolved;
45
+ }
46
+ const postmanExact = /^\{\{([A-Za-z0-9_.-]+)\}\}$/.exec(value);
47
+ if (postmanExact) {
48
+ const resolved = this.values.get(postmanExact[1]);
49
+ if (resolved === undefined || resolved === null) {
50
+ throw new Error(`Variable '${postmanExact[1]}' is not available in run context.`);
51
+ }
52
+ return resolved;
53
+ }
54
+ return value
55
+ .replace(/\$\{([^}]+)\}/g, (_match, name: string) => {
56
+ const resolved = this.values.get(name);
57
+ if (resolved === undefined || resolved === null) {
58
+ throw new Error(`Variable '${name}' is not available in run context.`);
59
+ }
60
+ return String(resolved);
61
+ })
62
+ .replace(/\{\{([A-Za-z0-9_.-]+)\}\}/g, (_match, name: string) => {
63
+ const resolved = this.values.get(name);
64
+ if (resolved === undefined || resolved === null) {
65
+ throw new Error(`Variable '${name}' is not available in run context.`);
66
+ }
67
+ return String(resolved);
68
+ });
69
+ }
70
+ if (Array.isArray(value)) return value.map((item) => this.resolveValue(item));
71
+ if (value && typeof value === "object") {
72
+ return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, this.resolveValue(entry)]));
73
+ }
74
+ return value;
75
+ }
76
+ }
@@ -0,0 +1,35 @@
1
+ import { mkdir, writeFile } from "node:fs/promises";
2
+ import { dirname, join } from "node:path";
3
+ import { applyEvidenceVisibility, evidenceVisibilityMode } from "../config/secrets.js";
4
+ import type { EvidenceVisibilityMode } from "../dsl/types.js";
5
+
6
+ export class EvidenceWriter {
7
+ constructor(public readonly runDir: string, private readonly visibility: EvidenceVisibilityMode = evidenceVisibilityMode()) {}
8
+
9
+ async init(): Promise<void> {
10
+ await mkdir(this.runDir, { recursive: true });
11
+ }
12
+
13
+ async writeJson(name: string, value: unknown): Promise<string> {
14
+ const path = join(this.runDir, sanitizeName(name));
15
+ await writeFile(path, JSON.stringify(applyEvidenceVisibility(value, this.visibility), null, 2), "utf8");
16
+ return path;
17
+ }
18
+
19
+ async writeJsonPath(relativePath: string, value: unknown): Promise<string> {
20
+ const path = join(this.runDir, ...relativePath.split(/[\\/]+/).map(sanitizeName));
21
+ await mkdir(dirname(path), { recursive: true });
22
+ await writeFile(path, JSON.stringify(applyEvidenceVisibility(value, this.visibility), null, 2), "utf8");
23
+ return path;
24
+ }
25
+
26
+ async writeText(name: string, value: string): Promise<string> {
27
+ const path = join(this.runDir, sanitizeName(name));
28
+ await writeFile(path, value, "utf8");
29
+ return path;
30
+ }
31
+ }
32
+
33
+ function sanitizeName(name: string): string {
34
+ return name.replace(/[^A-Za-z0-9_.-]/g, "_");
35
+ }
@@ -0,0 +1,145 @@
1
+ export interface KnownErrorMessage {
2
+ message: string;
3
+ hints: string[];
4
+ }
5
+
6
+ export function explainKnownError(error: unknown): KnownErrorMessage {
7
+ const err = error instanceof Error ? error : new Error(String(error));
8
+ const text = `${err.name}: ${err.message}`;
9
+ const hints: string[] = [];
10
+ let message = err.message;
11
+
12
+ if (/Invalid JSON for --params|Invalid JSON value for --param/i.test(text)) {
13
+ hints.push("In PowerShell, prefer repeated --param name=value options instead of --params JSON.");
14
+ hints.push("Example: --param case_id=CASE-1001 --param case_id=CASE-1002");
15
+ }
16
+ if (/Invalid JSON for --input/i.test(text)) {
17
+ hints.push("In PowerShell, wrap JSON input in single quotes or escape double quotes.");
18
+ hints.push("Example: --input '{\"application_id\":\"000000000000000000001452\"}'");
19
+ }
20
+
21
+ if (err instanceof SyntaxError && (/json/i.test(err.message) || /Expected property name|Unexpected token|Unexpected end/.test(err.message))) {
22
+ message = `Invalid JSON argument: ${err.message}`;
23
+ hints.push("In PowerShell, prefer repeated --param name=value options instead of --params JSON.");
24
+ hints.push("Example: --param case_id=CASE-1001 --param case_id=CASE-1002");
25
+ }
26
+
27
+ if (/ADFINEM_DB_USER|ADFINEM_DB_PASSWORD|ADFINEM_DB_CONNECT_STRING/.test(text)) {
28
+ hints.push("Set ADFINEM_DB_USER, ADFINEM_DB_PASSWORD, and ADFINEM_DB_CONNECT_STRING in .env or your shell.");
29
+ hints.push("Example connect string: <host>:1521/<service>");
30
+ }
31
+
32
+ if (/DPI-1047|Cannot locate an Oracle Client library/i.test(text)) {
33
+ message = "Oracle client libraries are not available to node-oracledb.";
34
+ hints.push("Install/configure Oracle Instant Client and make sure its directory is on PATH.");
35
+ }
36
+
37
+ if (/oracledb\.getConnection is not a function|did not expose getConnection/i.test(text)) {
38
+ message = "The oracledb module loaded in an unexpected shape.";
39
+ hints.push("Run npm install again, then npm run build. This runner supports both ESM default and direct oracledb exports.");
40
+ }
41
+
42
+ addOracleHint(text, hints);
43
+ addSshHint(text, hints);
44
+ addCatalogHint(text, hints);
45
+
46
+ return { message, hints: unique(hints) };
47
+ }
48
+
49
+ export function formatKnownError(error: unknown): string {
50
+ const explained = explainKnownError(error);
51
+ if (!explained.hints.length) return explained.message;
52
+ return [
53
+ explained.message,
54
+ "",
55
+ "Hints:",
56
+ ...explained.hints.map((hint) => `- ${hint}`)
57
+ ].join("\n");
58
+ }
59
+
60
+ function addOracleHint(text: string, hints: string[]): void {
61
+ if (/ORA-01017/.test(text)) {
62
+ hints.push("Oracle rejected the username/password. Check ADFINEM_DB_USER and ADFINEM_DB_PASSWORD.");
63
+ }
64
+ if (/ORA-28000/.test(text)) {
65
+ hints.push("Oracle account is locked. Unlock/reset the DB user before rerunning.");
66
+ }
67
+ if (/ORA-12154/.test(text)) {
68
+ hints.push("Oracle could not resolve the connect identifier. Check ADFINEM_DB_CONNECT_STRING.");
69
+ }
70
+ if (/ORA-12514/.test(text)) {
71
+ hints.push("Oracle listener does not know the requested service. Check the service name in ADFINEM_DB_CONNECT_STRING.");
72
+ }
73
+ if (/ORA-12541|ECONNREFUSED/.test(text)) {
74
+ hints.push("Oracle listener connection was refused. Check host, port, VPN, and listener status.");
75
+ }
76
+ if (/ORA-12170|ETIMEDOUT|NJS-510|NJS-511/.test(text)) {
77
+ hints.push("Oracle connection timed out. Check network/VPN/firewall and DB host reachability.");
78
+ }
79
+ if (/ORA-00942/.test(text)) {
80
+ hints.push("Table or view does not exist for this schema, or the DB user lacks privileges.");
81
+ }
82
+ if (/ORA-00904/.test(text)) {
83
+ hints.push("Invalid column name. Check the Action Library SQL against the target environment schema.");
84
+ }
85
+ if (/ORA-01036|NJS-098/.test(text)) {
86
+ hints.push("SQL bind mismatch. Check Action Library parameter names and supplied --param values.");
87
+ }
88
+ if (/ORA-01722/.test(text)) {
89
+ hints.push("Invalid number. Use json: for numeric --param values, for example --param amount=json:111.");
90
+ }
91
+ }
92
+
93
+ function addSshHint(text: string, hints: string[]): void {
94
+ if (/Unknown SSH hostRef/.test(text)) {
95
+ hints.push("Add the hostRef under sshHosts in config/environments.yaml.");
96
+ }
97
+ if (/requires host and username/.test(text)) {
98
+ hints.push("Set the SSH host and username in config/environments.yaml/.env.");
99
+ }
100
+ if (/All configured authentication methods failed|Permission denied/i.test(text)) {
101
+ hints.push("SSH authentication failed. Check ADFINEM_SSH_USER, ADFINEM_SSH_PASSWORD, or ADFINEM_SSH_PRIVATE_KEY_PATH.");
102
+ }
103
+ if (/ENOTFOUND|getaddrinfo/.test(text)) {
104
+ hints.push("SSH host name could not be resolved. Check the host value and VPN/DNS.");
105
+ }
106
+ if (/ECONNREFUSED/.test(text)) {
107
+ hints.push("SSH connection was refused. Check host, port, firewall, and sshd status.");
108
+ }
109
+ if (/timed out|ETIMEDOUT/i.test(text)) {
110
+ hints.push("SSH command or connection timed out. Check server load, VPN, and the Action Library timeoutSeconds.");
111
+ }
112
+ if (/exit code 127|No such file or directory|command not found|not found/i.test(text)) {
113
+ hints.push("The Unix command did not find the batch script. Set ADFINEM_BATCH_WORKDIR in .env, or put an absolute script path in the Action Library batch template.");
114
+ hints.push("If the script lives on the remote server, ADFINEM_BATCH_WORKDIR must be the remote directory that contains reconcile_nightly.sh, generate_report.sh, and the other batch scripts.");
115
+ hints.push("If the command works only after an interactive SSH login, enable 'Run commands in login shell' for the SSH host in Environments.");
116
+ }
117
+ if (/ENOENT.*private|no such file/i.test(text)) {
118
+ hints.push("SSH private key path does not exist. Check ADFINEM_SSH_PRIVATE_KEY_PATH.");
119
+ }
120
+ }
121
+
122
+ function addCatalogHint(text: string, hints: string[]): void {
123
+ if (/Unknown API operation/.test(text)) {
124
+ hints.push("Run against an API operation present in the Action Library file catalogs/api-operations.yaml.");
125
+ }
126
+ if (/Unknown query/.test(text)) {
127
+ hints.push("Run against a query template present in the Action Library file catalogs/queries.yaml.");
128
+ }
129
+ if (/Unknown batch/.test(text)) {
130
+ hints.push("Run against a batch template present in the Action Library file catalogs/batches.yaml.");
131
+ }
132
+ if (/Missing required query param|Missing required batch arg/.test(text)) {
133
+ hints.push("Supply missing values with --param name=value or scenario params/input.");
134
+ }
135
+ if (/Capture expression .* did not match/i.test(text)) {
136
+ hints.push("A required capture did not exist in the action output. If this value is legitimately optional for the flow, prefix the capture expression with optional: in the Action Library template.");
137
+ }
138
+ if (/must be string\[\]|must contain at least one value/.test(text)) {
139
+ hints.push("For list params, pass one value or repeat --param. Example: --param case_id=CASE-1001 --param case_id=CASE-1002");
140
+ }
141
+ }
142
+
143
+ function unique(values: string[]): string[] {
144
+ return [...new Set(values)];
145
+ }
@@ -0,0 +1,11 @@
1
+ export async function withTimeout<T>(promise: Promise<T>, timeoutMs: number, label: string): Promise<T> {
2
+ let timeout: NodeJS.Timeout | undefined;
3
+ const timeoutPromise = new Promise<never>((_resolve, reject) => {
4
+ timeout = setTimeout(() => reject(new Error(`${label} timed out after ${timeoutMs}ms.`)), timeoutMs);
5
+ });
6
+ try {
7
+ return await Promise.race([promise, timeoutPromise]);
8
+ } finally {
9
+ if (timeout) clearTimeout(timeout);
10
+ }
11
+ }