@kirrosh/zond 0.22.0 → 0.23.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/CHANGELOG.md +648 -0
- package/README.md +58 -6
- package/package.json +9 -6
- package/src/cli/argv.ts +122 -0
- package/src/cli/commands/add-api.ts +134 -0
- package/src/cli/commands/api/annotate/idempotency.ts +59 -0
- package/src/cli/commands/api/annotate/index.ts +525 -0
- package/src/cli/commands/api/annotate/lifecycle.ts +74 -0
- package/src/cli/commands/api/annotate/overlay.ts +206 -0
- package/src/cli/commands/api/annotate/pagination.ts +60 -0
- package/src/cli/commands/api/annotate/prompts.ts +183 -0
- package/src/cli/commands/api/annotate/readback.ts +58 -0
- package/src/cli/commands/api/annotate/resources.ts +91 -0
- package/src/cli/commands/api/annotate/seed-bodies.ts +61 -0
- package/src/cli/commands/audit.ts +480 -0
- package/src/cli/commands/bootstrap.ts +710 -0
- package/src/cli/commands/catalog.ts +35 -0
- package/src/cli/commands/check.ts +348 -0
- package/src/cli/commands/checks.ts +756 -0
- package/src/cli/commands/ci-init.ts +43 -0
- package/src/cli/commands/clean.ts +212 -0
- package/src/cli/commands/cleanup.ts +262 -0
- package/src/cli/commands/completions.ts +16 -0
- package/src/cli/commands/coverage.ts +605 -132
- package/src/cli/commands/db.ts +178 -7
- package/src/cli/commands/describe.ts +37 -2
- package/src/cli/commands/discover.ts +1236 -0
- package/src/cli/commands/doctor.ts +607 -0
- package/src/cli/commands/fixtures.ts +402 -0
- package/src/cli/commands/generate.ts +420 -46
- package/src/cli/commands/init/bootstrap.ts +30 -1
- package/src/cli/commands/{init.ts → init/index.ts} +99 -5
- package/src/cli/commands/init/skills.ts +56 -3
- package/src/cli/commands/init/templates/agents.md +65 -61
- package/src/cli/commands/init/templates/skills/zond-checks.md +397 -0
- package/src/cli/commands/init/templates/skills/zond-triage.md +210 -0
- package/src/cli/commands/init/templates/skills/zond.md +592 -125
- package/src/cli/commands/init/templates/zond-config.yml +8 -9
- package/src/cli/commands/prepare-fixtures.ts +135 -0
- package/src/cli/commands/probe/mass-assignment.ts +503 -0
- package/src/cli/commands/probe/security.ts +454 -0
- package/src/cli/commands/probe/static.ts +255 -0
- package/src/cli/commands/probe/webhooks.ts +161 -0
- package/src/cli/commands/probe.ts +459 -0
- package/src/cli/commands/reference.ts +87 -0
- package/src/cli/commands/refresh-api.ts +169 -0
- package/src/cli/commands/remove-api.ts +150 -0
- package/src/cli/commands/report-bundle.ts +318 -0
- package/src/cli/commands/report.ts +241 -0
- package/src/cli/commands/request.ts +379 -4
- package/src/cli/commands/run.ts +842 -53
- package/src/cli/commands/session.ts +244 -0
- package/src/cli/commands/use.ts +18 -1
- package/src/cli/index.ts +20 -3
- package/src/cli/json-envelope.ts +112 -3
- package/src/cli/json-schemas.ts +263 -0
- package/src/cli/program.ts +198 -635
- package/src/cli/resolve.ts +105 -0
- package/src/cli/status-filter.ts +124 -0
- package/src/cli/util/api-context.ts +85 -0
- package/src/cli/version.ts +5 -0
- package/src/core/anti-fp/bootstrap.ts +34 -0
- package/src/core/anti-fp/index.ts +33 -0
- package/src/core/anti-fp/registry.ts +44 -0
- package/src/core/anti-fp/rules/baseline-echo.ts +74 -0
- package/src/core/anti-fp/rules/schemathesis/body_negation_becomes_valid.ts +52 -0
- package/src/core/anti-fp/rules/schemathesis/coverage_phase_boundary_positive.ts +38 -0
- package/src/core/anti-fp/rules/schemathesis/has_unverifiable_mutations.ts +35 -0
- package/src/core/anti-fp/rules/schemathesis/index.ts +24 -0
- package/src/core/anti-fp/rules/schemathesis/string_type_mutation_becomes_valid.ts +53 -0
- package/src/core/anti-fp/rules/subscription-gated/index.ts +11 -0
- package/src/core/anti-fp/rules/subscription-gated/paid-plan-403.ts +75 -0
- package/src/core/anti-fp/types.ts +68 -0
- package/src/core/checks/checks/_crud-helpers.ts +133 -0
- package/src/core/checks/checks/_negative_mutator.ts +133 -0
- package/src/core/checks/checks/_readback-helpers.ts +133 -0
- package/src/core/checks/checks/content_type_conformance.ts +39 -0
- package/src/core/checks/checks/cross_call_references.ts +134 -0
- package/src/core/checks/checks/ensure_resource_availability.ts +62 -0
- package/src/core/checks/checks/idempotency_replay.ts +246 -0
- package/src/core/checks/checks/ignored_auth.ts +211 -0
- package/src/core/checks/checks/index.ts +65 -0
- package/src/core/checks/checks/lifecycle_transitions.ts +273 -0
- package/src/core/checks/checks/missing_required_header.ts +40 -0
- package/src/core/checks/checks/negative_data_rejection.ts +45 -0
- package/src/core/checks/checks/not_a_server_error.ts +27 -0
- package/src/core/checks/checks/open_cors_on_sensitive.ts +131 -0
- package/src/core/checks/checks/pagination_invariants.ts +238 -0
- package/src/core/checks/checks/positive_data_acceptance.ts +36 -0
- package/src/core/checks/checks/rate_limit_headers_absent.ts +77 -0
- package/src/core/checks/checks/response_headers_conformance.ts +74 -0
- package/src/core/checks/checks/response_schema_conformance.ts +30 -0
- package/src/core/checks/checks/status_code_conformance.ts +61 -0
- package/src/core/checks/checks/unsupported_method.ts +63 -0
- package/src/core/checks/checks/use_after_free.ts +78 -0
- package/src/core/checks/index.ts +30 -0
- package/src/core/checks/mode.ts +79 -0
- package/src/core/checks/recommended-action.ts +64 -0
- package/src/core/checks/registry.ts +78 -0
- package/src/core/checks/runner.ts +874 -0
- package/src/core/checks/sarif.ts +230 -0
- package/src/core/checks/stateful.ts +121 -0
- package/src/core/checks/types.ts +189 -0
- package/src/core/classifier/recommended-action.ts +222 -0
- package/src/core/context/current.ts +22 -6
- package/src/core/context/session.ts +78 -0
- package/src/core/coverage/loader.ts +185 -0
- package/src/core/coverage/reasons.ts +300 -0
- package/src/core/diagnostics/db-analysis.ts +151 -11
- package/src/core/diagnostics/failure-class.ts +120 -0
- package/src/core/diagnostics/failure-hints.ts +212 -9
- package/src/core/diagnostics/spec-pointer.ts +99 -0
- package/src/core/diagnostics/suggested-fixes.ts +156 -0
- package/src/core/exporter/case-study/index.ts +270 -0
- package/src/core/exporter/curl.ts +40 -0
- package/src/core/exporter/exporter.ts +48 -0
- package/src/core/exporter/html-report/escape.ts +24 -0
- package/src/core/exporter/html-report/index.ts +479 -0
- package/src/core/exporter/html-report/script.ts +100 -0
- package/src/core/exporter/html-report/styles.ts +408 -0
- package/src/core/generator/chunker.ts +42 -16
- package/src/core/generator/coverage-phase.ts +0 -0
- package/src/core/generator/create-body.ts +89 -0
- package/src/core/generator/data-factory.ts +445 -19
- package/src/core/generator/describe.ts +1 -1
- package/src/core/generator/fixtures-builder.ts +325 -0
- package/src/core/generator/index.ts +7 -5
- package/src/core/generator/openapi-reader.ts +37 -3
- package/src/core/generator/path-param-disambig.ts +114 -0
- package/src/core/generator/resources-builder.ts +648 -0
- package/src/core/generator/schema-utils.ts +11 -3
- package/src/core/generator/serializer.ts +103 -13
- package/src/core/generator/suite-generator.ts +419 -111
- package/src/core/generator/types.ts +8 -0
- package/src/core/identity/identity-file.ts +129 -0
- package/src/core/lint/affects.ts +28 -0
- package/src/core/lint/config.ts +96 -0
- package/src/core/lint/format.ts +42 -0
- package/src/core/lint/index.ts +94 -0
- package/src/core/lint/reporter.ts +128 -0
- package/src/core/lint/rules/consistency.ts +158 -0
- package/src/core/lint/rules/heuristics.ts +97 -0
- package/src/core/lint/rules/strictness.ts +109 -0
- package/src/core/lint/types.ts +96 -0
- package/src/core/lint/walker.ts +248 -0
- package/src/core/meta/meta-store.ts +6 -73
- package/src/core/output/README.md +91 -0
- package/src/core/output/index.ts +13 -0
- package/src/core/output/run.ts +126 -0
- package/src/core/output/types.ts +129 -0
- package/src/core/parser/env-interpolation.ts +104 -0
- package/src/core/parser/filter.ts +57 -0
- package/src/core/parser/schema.ts +129 -4
- package/src/core/parser/types.ts +19 -1
- package/src/core/parser/variables.ts +0 -0
- package/src/core/parser/yaml-parser.ts +58 -12
- package/src/core/probe/bootstrap.ts +34 -0
- package/src/core/probe/dry-run-envelope.ts +57 -0
- package/src/core/probe/mass-assignment-probe-class.ts +198 -0
- package/src/core/probe/mass-assignment-probe.ts +1122 -0
- package/src/core/probe/mass-assignment-template.ts +212 -0
- package/src/core/probe/method-probe.ts +43 -76
- package/src/core/probe/method-shared.ts +69 -0
- package/src/core/probe/negative-probe.ts +183 -149
- package/src/core/probe/orphan-tracker.ts +188 -0
- package/src/core/probe/path-discovery.ts +440 -0
- package/src/core/probe/probe-harness.ts +120 -0
- package/src/core/probe/registry.ts +89 -0
- package/src/core/probe/runner.ts +136 -0
- package/src/core/probe/security-probe-class.ts +201 -0
- package/src/core/probe/security-probe.ts +1453 -0
- package/src/core/probe/shared.ts +505 -0
- package/src/core/probe/static-probe-class.ts +125 -0
- package/src/core/probe/types.ts +165 -0
- package/src/core/probe/verdict-aggregator.ts +33 -0
- package/src/core/probe/webhooks-probe.ts +284 -0
- package/src/core/reporter/console.ts +41 -2
- package/src/core/reporter/index.ts +2 -3
- package/src/core/reporter/json.ts +11 -1
- package/src/core/reporter/junit.ts +27 -12
- package/src/core/reporter/ndjson.ts +37 -0
- package/src/core/reporter/types.ts +3 -0
- package/src/core/runner/assertions.ts +58 -1
- package/src/core/runner/async-pool.ts +108 -0
- package/src/core/runner/auth-path.ts +8 -0
- package/src/core/runner/ci-context.ts +72 -0
- package/src/core/runner/executor.ts +264 -20
- package/src/core/runner/form-encode.ts +51 -0
- package/src/core/runner/http-client.ts +75 -2
- package/src/core/runner/learn-drift.ts +293 -0
- package/src/core/runner/preflight-vars.ts +149 -0
- package/src/core/runner/progress-tracker.ts +73 -0
- package/src/core/runner/rate-limiter.ts +89 -17
- package/src/core/runner/run-kind.ts +39 -0
- package/src/core/runner/schema-validator.ts +312 -0
- package/src/core/runner/send-request.ts +153 -20
- package/src/core/runner/types.ts +38 -0
- package/src/core/secrets/registry.ts +164 -0
- package/src/core/secrets/secrets-file.ts +115 -0
- package/src/core/selectors/operation-filter.ts +144 -0
- package/src/core/setup-api.ts +415 -16
- package/src/core/severity/category.ts +94 -0
- package/src/core/severity/index.ts +121 -0
- package/src/core/spec/layers.ts +154 -0
- package/src/core/util/format-eta.ts +21 -0
- package/src/core/utils.ts +5 -1
- package/src/core/workspace/config.ts +129 -0
- package/src/core/workspace/manifest.ts +283 -0
- package/src/core/workspace/output-rotation.ts +62 -0
- package/src/core/workspace/triage-path.ts +87 -0
- package/src/db/lint-runs.ts +47 -0
- package/src/db/migrate.ts +126 -0
- package/src/db/migrations/0001_run_kind.sql +25 -0
- package/src/db/migrations/sql.d.ts +4 -0
- package/src/db/queries/collections.ts +133 -0
- package/src/db/queries/coverage.ts +9 -0
- package/src/db/queries/dashboard.ts +59 -0
- package/src/db/queries/results.ts +128 -0
- package/src/db/queries/runs.ts +235 -0
- package/src/db/queries/sessions.ts +42 -0
- package/src/db/queries/settings.ts +28 -0
- package/src/db/queries/types.ts +172 -0
- package/src/db/queries.ts +72 -802
- package/src/db/schema.ts +178 -50
- package/src/cli/commands/export.ts +0 -144
- package/src/cli/commands/guide.ts +0 -127
- package/src/cli/commands/init/templates/skills/scenarios.md +0 -97
- package/src/cli/commands/probe-methods.ts +0 -108
- package/src/cli/commands/probe-validation.ts +0 -124
- package/src/cli/commands/serve.ts +0 -114
- package/src/cli/commands/sync.ts +0 -268
- package/src/cli/commands/update.ts +0 -189
- package/src/cli/commands/validate.ts +0 -34
- package/src/core/diagnostics/render-md.ts +0 -112
- package/src/core/exporter/postman.ts +0 -963
- package/src/core/generator/guide-builder.ts +0 -253
- package/src/core/meta/types.ts +0 -19
- package/src/core/parser/index.ts +0 -21
- package/src/core/runner/execute-run.ts +0 -132
- package/src/core/runner/index.ts +0 -12
- package/src/core/sync/spec-differ.ts +0 -38
- package/src/web/data/collection-state.ts +0 -362
- package/src/web/routes/api.ts +0 -314
- package/src/web/routes/dashboard.ts +0 -350
- package/src/web/routes/runs.ts +0 -64
- package/src/web/schemas.ts +0 -121
- package/src/web/server.ts +0 -134
- package/src/web/static/htmx.min.cjs +0 -1
- package/src/web/static/style.css +0 -1148
- package/src/web/views/endpoints-tab.ts +0 -174
- package/src/web/views/explorer-tab.ts +0 -402
- package/src/web/views/health-strip.ts +0 -92
- package/src/web/views/layout.ts +0 -48
- package/src/web/views/results.ts +0 -210
- package/src/web/views/runs-tab.ts +0 -126
- package/src/web/views/suites-tab.ts +0 -181
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Probe umbrella.
|
|
3
|
+
*
|
|
4
|
+
* TASK-182 (m-11) introduced `zond probe <class>` as the canonical entry
|
|
5
|
+
* point. TASK-300 (m-13) consolidated the two static-input classes —
|
|
6
|
+
* validation and methods — under `zond probe static [--include …]`; the
|
|
7
|
+
* old `probe validation` / `probe methods` subcommands were removed
|
|
8
|
+
* outright (no deprecation alias).
|
|
9
|
+
*
|
|
10
|
+
* Extracted from program.ts (TASK-190 round 2e) so the registration tree
|
|
11
|
+
* lives next to the action functions it dispatches into.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { Command } from "commander";
|
|
15
|
+
|
|
16
|
+
// ARV-129: action handlers relocated from top-level commands/probe-*.ts
|
|
17
|
+
// into commands/probe/ — the orchestrator (this file) stays at top level,
|
|
18
|
+
// the per-subcommand modules are no longer mistaken for siblings.
|
|
19
|
+
import { probeStaticCommand, resolveStaticClasses } from "./probe/static.ts";
|
|
20
|
+
import { probeMassAssignmentCommand, emitMassAssignmentTemplateCommand } from "./probe/mass-assignment.ts";
|
|
21
|
+
import { probeSecurityCommand } from "./probe/security.ts";
|
|
22
|
+
import { probeWebhooksCommand } from "./probe/webhooks.ts";
|
|
23
|
+
import { SECURITY_CLASSES } from "../../core/probe/security-probe.ts";
|
|
24
|
+
import { globalJson, resolveSpecArg, resolveApiEnv, resolveApiCollection } from "../resolve.ts";
|
|
25
|
+
import { getApi } from "../util/api-context.ts";
|
|
26
|
+
import { existsSync } from "fs";
|
|
27
|
+
import { join, dirname } from "node:path";
|
|
28
|
+
import { parsePositiveInt } from "../argv.ts";
|
|
29
|
+
import { printError } from "../output.ts";
|
|
30
|
+
import { jsonError, printJson } from "../json-envelope.ts";
|
|
31
|
+
import { loadEnvMeta } from "../../core/parser/variables.ts";
|
|
32
|
+
import { resolveTimeoutMs } from "../../core/workspace/config.ts";
|
|
33
|
+
import { resolveOutput, OutputSpecError, type OutputSpec, type ResolvedOutput } from "../../core/output/index.ts";
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* ARV-119 (m-19): typed declaration of the `--report` / `--output`
|
|
37
|
+
* surface shared by the live-probe subcommands (mass-assignment +
|
|
38
|
+
* security). Both render a markdown digest by default, with `--report
|
|
39
|
+
* json` switching to a structured JSON file body. `--output <path>`
|
|
40
|
+
* routes the rendered body to a file; without it the body lands on
|
|
41
|
+
* stdout (when `--json` is not set — see m-17 / ARV-51: the `--json`
|
|
42
|
+
* envelope is a separate channel that wraps the structured result).
|
|
43
|
+
*
|
|
44
|
+
* `probe static` is *not* on this spec — its `--output` is a directory
|
|
45
|
+
* where YAML probe suites are written, semantics not output-format.
|
|
46
|
+
*/
|
|
47
|
+
export const PROBE_OUTPUT_SPEC: OutputSpec<unknown> = {
|
|
48
|
+
command: "probe",
|
|
49
|
+
defaultFormat: "markdown",
|
|
50
|
+
formats: {
|
|
51
|
+
markdown: { defaultChannel: "stdout", description: "Human-readable digest (default)" },
|
|
52
|
+
json: { defaultChannel: "stdout", description: "Structured JSON body — same shape as the --json envelope's data." },
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* ARV-119: shared `--report` / `--output` / `--overwrite` option set
|
|
58
|
+
* for the two live-probe subcommands. Resolution goes through
|
|
59
|
+
* `resolveProbeOutputFlags` so unknown formats and mutually-exclusive
|
|
60
|
+
* combinations surface the same error for both subcommands instead of
|
|
61
|
+
* each one reimplementing the validation.
|
|
62
|
+
*/
|
|
63
|
+
function addProbeReportOutputOptions(cmd: Command): Command {
|
|
64
|
+
return cmd
|
|
65
|
+
.option("--output <file>", "ARV-119: write the rendered report to this file (default: stdout). Pairs with --report to pick the format.")
|
|
66
|
+
.option(
|
|
67
|
+
"--report <format>",
|
|
68
|
+
"ARV-119: format for --output / non-json stdout (markdown|json). Default markdown. The --json envelope (m-17 ARV-51) is a separate channel and is always structured.",
|
|
69
|
+
"markdown",
|
|
70
|
+
)
|
|
71
|
+
.option("--overwrite", "Overwrite existing --output file in place (default: rotate to <stem>-vN.<ext>)");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
interface ProbeReportOutputOpts {
|
|
75
|
+
report?: string;
|
|
76
|
+
output?: string;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* ARV-119: resolve --report / --output through PROBE_OUTPUT_SPEC. On
|
|
81
|
+
* unknown format / mutual-exclusion violation, prints the consistent
|
|
82
|
+
* error (envelope when --json) and returns null — caller exits 2.
|
|
83
|
+
*/
|
|
84
|
+
function resolveProbeOutputFlags(
|
|
85
|
+
command: string,
|
|
86
|
+
opts: ProbeReportOutputOpts,
|
|
87
|
+
json: boolean,
|
|
88
|
+
): { resolved: ResolvedOutput; report: "markdown" | "json"; output?: string } | null {
|
|
89
|
+
let resolved: ResolvedOutput;
|
|
90
|
+
try {
|
|
91
|
+
resolved = resolveOutput(PROBE_OUTPUT_SPEC, { report: opts.report, output: opts.output });
|
|
92
|
+
} catch (err) {
|
|
93
|
+
if (err instanceof OutputSpecError) {
|
|
94
|
+
if (json) printJson(jsonError(command, [err.message]));
|
|
95
|
+
else printError(err.message);
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
throw err;
|
|
99
|
+
}
|
|
100
|
+
const report: "markdown" | "json" = resolved.format === "json" ? "json" : "markdown";
|
|
101
|
+
const output = resolved.channel === "file" ? resolved.path : undefined;
|
|
102
|
+
return { resolved, report, output };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* ARV-53: thin wrapper kept for the existing call-sites — the real chain
|
|
107
|
+
* (local → ancestor → ZOND_API_GLOBAL/ZOND_API/.zond/current-api) lives in
|
|
108
|
+
* cli/util/api-context.ts. `resolveProbeApi` predates that helper (ARV-33).
|
|
109
|
+
*/
|
|
110
|
+
export function resolveProbeApi(
|
|
111
|
+
optsApi: string | undefined,
|
|
112
|
+
cmd: { opts?: () => Record<string, unknown>; parent?: { opts(): Record<string, unknown> } | null } | undefined,
|
|
113
|
+
): string | undefined {
|
|
114
|
+
// Adapt the loose mock shape used at the old call-sites to CommandLike.
|
|
115
|
+
if (cmd === undefined) {
|
|
116
|
+
return getApi(undefined, { api: optsApi });
|
|
117
|
+
}
|
|
118
|
+
const adapted = {
|
|
119
|
+
opts: () => (typeof cmd.opts === "function" ? cmd.opts() : {}),
|
|
120
|
+
parent: cmd.parent ?? null,
|
|
121
|
+
};
|
|
122
|
+
return getApi(adapted, { api: optsApi });
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* TASK-233: pick the env file to feed live-probe commands.
|
|
127
|
+
* - Explicit --env wins (legacy behaviour).
|
|
128
|
+
* - Otherwise --api <name> derives `apis/<name>/.env.yaml` via the registered
|
|
129
|
+
* collection's base_dir.
|
|
130
|
+
* - With `tolerateMissing` (probe security/--dry-run), an absent file is
|
|
131
|
+
* quietly turned into "no env" — the command will fall back to cwd.
|
|
132
|
+
*/
|
|
133
|
+
function resolveProbeEnv(
|
|
134
|
+
envFlag: string | undefined,
|
|
135
|
+
apiFlag: string | undefined,
|
|
136
|
+
dbPath: string | undefined,
|
|
137
|
+
opts: { tolerateMissing?: boolean } = {},
|
|
138
|
+
): { env: string | undefined } | { error: string } {
|
|
139
|
+
if (envFlag) return { env: envFlag };
|
|
140
|
+
if (!apiFlag) {
|
|
141
|
+
if (opts.tolerateMissing) return { env: undefined };
|
|
142
|
+
return { error: "Missing --env <file> (or pass --api <name> to derive it from apis/<name>/.env.yaml)" };
|
|
143
|
+
}
|
|
144
|
+
const resolved = resolveApiEnv(apiFlag, dbPath);
|
|
145
|
+
if ("error" in resolved) return resolved;
|
|
146
|
+
if (!existsSync(resolved.env)) {
|
|
147
|
+
if (opts.tolerateMissing) return { env: undefined };
|
|
148
|
+
return { error: `Env file not found: ${resolved.env} (derived from --api ${apiFlag})` };
|
|
149
|
+
}
|
|
150
|
+
return { env: resolved.env };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Resolve `--timeout` for live-probe commands. Reads the per-API
|
|
155
|
+
* `.env.yaml` `timeoutMs:` meta when `--api` is set (or when the env
|
|
156
|
+
* file is on disk), then falls back to workspace `defaults.timeout_ms`.
|
|
157
|
+
*/
|
|
158
|
+
async function resolveProbeTimeout(
|
|
159
|
+
cliFlag: number | undefined,
|
|
160
|
+
apiFlag: string | undefined,
|
|
161
|
+
envFile: string | undefined,
|
|
162
|
+
): Promise<number> {
|
|
163
|
+
let envTimeout: number | undefined;
|
|
164
|
+
try {
|
|
165
|
+
if (apiFlag) {
|
|
166
|
+
const meta = await loadEnvMeta(undefined, `apis/${apiFlag}`);
|
|
167
|
+
envTimeout = meta.timeoutMs;
|
|
168
|
+
} else if (envFile) {
|
|
169
|
+
const meta = await loadEnvMeta(undefined, dirname(envFile));
|
|
170
|
+
envTimeout = meta.timeoutMs;
|
|
171
|
+
}
|
|
172
|
+
} catch { /* meta is best-effort */ }
|
|
173
|
+
return resolveTimeoutMs(cliFlag, envTimeout);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function defineProbeStatic(parent: Command, name: string): void {
|
|
177
|
+
parent
|
|
178
|
+
.command(`${name} [spec]`)
|
|
179
|
+
.description(
|
|
180
|
+
"Generate static-input probe suites (validation: bogus types/values; methods: undeclared HTTP methods). Defaults to both; restrict via --include or --exclude.",
|
|
181
|
+
)
|
|
182
|
+
.option("--api <name>", "Use the registered API's spec (apis/<name>/spec.json)")
|
|
183
|
+
.option("--db <path>", "Path to SQLite database file")
|
|
184
|
+
// ARV-30: --output is optional when --api (or current-api) is set —
|
|
185
|
+
// probes land in apis/<name>/probes/static/ alongside generate's tests/.
|
|
186
|
+
// Required only when probing a bare spec with no registered collection.
|
|
187
|
+
.option("--output <dir>", "Output directory for generated probe files (default: apis/<api>/probes/static when --api / current-api is set)")
|
|
188
|
+
.option("--tag <tag>", "Probe only endpoints with this tag")
|
|
189
|
+
.option("--list-tags", "List available tags from spec and exit")
|
|
190
|
+
.option("--max-per-endpoint <N>", "Cap negative-input probes per endpoint (default 50)", parsePositiveInt("--max-per-endpoint"))
|
|
191
|
+
.option("--no-cleanup", "Skip emission of follow-up DELETE cleanup steps for mutating probes (use in namespace-isolated test envs)")
|
|
192
|
+
.option("--use-synthetic-parents", "Bake synthetic-by-type values into all path params (legacy). By default, non-attacked path params are emitted as {{name}} and resolved from .env.yaml at run time — needed to reach the leaf validator on nested paths (TASK-135).")
|
|
193
|
+
.option("--include <classes>", "Comma-separated subset of {validation, methods} (default: both)")
|
|
194
|
+
.option("--exclude <classes>", "Comma-separated subset to skip (mutually exclusive with --include)")
|
|
195
|
+
.action(async (specPos: string | undefined, optsArg, cmdRef: Command) => {
|
|
196
|
+
// ARV-33: see resolveProbeApi — keep the chain consistent with the
|
|
197
|
+
// sibling subcommands (mass-assignment, security).
|
|
198
|
+
const apiName = resolveProbeApi(optsArg.api, cmdRef);
|
|
199
|
+
const resolved = resolveSpecArg(specPos, apiName, optsArg.db);
|
|
200
|
+
if ("error" in resolved) { printError(resolved.error); process.exitCode = 2; return; }
|
|
201
|
+
|
|
202
|
+
const r = resolveStaticClasses(optsArg.include, optsArg.exclude);
|
|
203
|
+
if ("error" in r) { printError(r.error); process.exitCode = 2; return; }
|
|
204
|
+
|
|
205
|
+
// ARV-30: derive --output from the registered API's base_dir when the
|
|
206
|
+
// user didn't pass one. Bare-spec invocations (positional only, no --api,
|
|
207
|
+
// no current-api) still must pass --output explicitly.
|
|
208
|
+
let outputDir: string | undefined = optsArg.output;
|
|
209
|
+
if (!outputDir && apiName) {
|
|
210
|
+
const col = resolveApiCollection(apiName, optsArg.db);
|
|
211
|
+
if (!("error" in col) && col.baseDir) outputDir = join(col.baseDir, "probes", "static");
|
|
212
|
+
}
|
|
213
|
+
if (!outputDir) {
|
|
214
|
+
printError("--output <dir> is required when no --api / current-api can resolve apis/<name>/probes/static.");
|
|
215
|
+
process.exitCode = 2;
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const useReal = optsArg.useSyntheticParents !== true;
|
|
220
|
+
process.exitCode = await probeStaticCommand({
|
|
221
|
+
specPath: resolved.spec,
|
|
222
|
+
output: outputDir,
|
|
223
|
+
tag: optsArg.tag,
|
|
224
|
+
maxPerEndpoint: optsArg.maxPerEndpoint,
|
|
225
|
+
noCleanup: optsArg.cleanup === false,
|
|
226
|
+
useRealParents: useReal,
|
|
227
|
+
json: globalJson(cmdRef),
|
|
228
|
+
listTags: optsArg.listTags,
|
|
229
|
+
include: r.classes,
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function defineProbeMassAssignment(parent: Command, name: string): void {
|
|
235
|
+
const sub = parent
|
|
236
|
+
.command(`${name} [spec]`)
|
|
237
|
+
.description(
|
|
238
|
+
"Live probe for mass-assignment / privilege-escalation: classifies POST/PATCH/PUT against suspected extra fields (is_admin, role, account_id, owner_id, user_id, verified, is_system) as rejected (4xx) | accepted-and-applied (HIGH) | accepted-and-ignored (LOW) via follow-up GET",
|
|
239
|
+
)
|
|
240
|
+
.option("--api <name>", "Use the registered API's spec (apis/<name>/spec.json)")
|
|
241
|
+
.option("--db <path>", "Path to SQLite database file")
|
|
242
|
+
.option("--env <file>", "Env YAML with base_url + auth_token (live calls require this; auto-derived from apis/<name>/.env.yaml when --api is given)")
|
|
243
|
+
.option("--emit-tests <dir>", "Also emit YAML regression suites locking in safe behaviour for CI")
|
|
244
|
+
.option("--tag <tag>", "Probe only endpoints with this tag")
|
|
245
|
+
.option("--list-tags", "List available tags from spec and exit")
|
|
246
|
+
.option("--no-cleanup", "Skip follow-up DELETE for resources accidentally created by 2xx probes")
|
|
247
|
+
.option("--no-discover", "Disable auto-discovery of path-param fixtures via GET-on-list (TASK-92)")
|
|
248
|
+
.option("--dry-run", "Print which endpoints/fields would be attacked without sending requests (m-17 ARV-52)")
|
|
249
|
+
.option(
|
|
250
|
+
"--include <selector>",
|
|
251
|
+
"Filter operations (m-15 ARV-9 grammar: path:/users/.* | tag:Webhooks | method:POST,PATCH | operation-id:create.*). Repeatable.",
|
|
252
|
+
(v: string, prev: string[] = []) => prev.concat(v),
|
|
253
|
+
[] as string[],
|
|
254
|
+
)
|
|
255
|
+
.option(
|
|
256
|
+
"--exclude <selector>",
|
|
257
|
+
"Drop operations matching <selector>. Repeatable. Same grammar as --include.",
|
|
258
|
+
(v: string, prev: string[] = []) => prev.concat(v),
|
|
259
|
+
[] as string[],
|
|
260
|
+
)
|
|
261
|
+
.option("--timeout <ms>", "Per-request timeout in ms (overrides apis/<name>/.env.yaml `timeoutMs` and zond.config.yml `defaults.timeout_ms`; default 30000)", parsePositiveInt("--timeout"))
|
|
262
|
+
.option(
|
|
263
|
+
"--verbose",
|
|
264
|
+
"ARV-252: surface INFO-severity inconclusive verdicts (absent-but-unverifiable). Silently-ignored verdicts (correct framework behaviour) stay hidden even with this flag — they're never finding-worthy.",
|
|
265
|
+
)
|
|
266
|
+
.option(
|
|
267
|
+
"--suspect-field <name=value>",
|
|
268
|
+
"ARV-252: extend the curated suspect-fields list (is_admin, role, owner_id, …) with a custom field. Repeatable. Full per-api spec-extension support tracked in ARV-189.",
|
|
269
|
+
(v: string, prev: string[] = []) => prev.concat(v),
|
|
270
|
+
[] as string[],
|
|
271
|
+
)
|
|
272
|
+
.option("--emit-template <method:path>", "TASK-146: emit a ready-to-edit YAML probe template for one endpoint (e.g. \"POST:/users\") instead of running the live probe. Pairs `--output <file>` to write to disk (default: stdout). Use to drop down to manual catch-up after INCONCLUSIVE / INCONCLUSIVE-5XX verdicts without copy-pasting boilerplate from the skill.");
|
|
273
|
+
addProbeReportOutputOptions(sub);
|
|
274
|
+
sub.action(async (specPos: string | undefined, opts, cmd: Command) => {
|
|
275
|
+
// ARV-33: resolve --api via the same fallback chain as prepare-fixtures /
|
|
276
|
+
// ARV-29 — direct opts, then parent opts, then ZOND_API_GLOBAL /
|
|
277
|
+
// .zond/current-api. Otherwise `zond probe mass-assignment --api foo`
|
|
278
|
+
// hits commander's global-option absorption and `opts.api` is empty.
|
|
279
|
+
const apiName = resolveProbeApi(opts.api, cmd);
|
|
280
|
+
const resolved = resolveSpecArg(specPos, apiName, opts.db);
|
|
281
|
+
if ("error" in resolved) { printError(resolved.error); process.exitCode = 2; return; }
|
|
282
|
+
|
|
283
|
+
// --emit-template short-circuits the live probe.
|
|
284
|
+
if (opts.emitTemplate) {
|
|
285
|
+
process.exitCode = await emitMassAssignmentTemplateCommand({
|
|
286
|
+
specPath: resolved.spec,
|
|
287
|
+
methodPath: opts.emitTemplate,
|
|
288
|
+
output: opts.output,
|
|
289
|
+
json: globalJson(cmd),
|
|
290
|
+
});
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// m-17 / ARV-52 + ARV-58: dry-run and list-tags paths tolerate a
|
|
295
|
+
// missing env file the way probe-security does — the user wants
|
|
296
|
+
// to inspect the plan / available tags, not hit a live API.
|
|
297
|
+
const envFile = resolveProbeEnv(opts.env, apiName, opts.db, {
|
|
298
|
+
tolerateMissing: opts.dryRun === true || opts.listTags === true,
|
|
299
|
+
});
|
|
300
|
+
if ("error" in envFile) { printError(envFile.error); process.exitCode = 2; return; }
|
|
301
|
+
const timeoutMs = await resolveProbeTimeout(opts.timeout, apiName, envFile.env);
|
|
302
|
+
const json = globalJson(cmd);
|
|
303
|
+
const rep = resolveProbeOutputFlags("probe-mass-assignment", opts, json);
|
|
304
|
+
if (!rep) { process.exitCode = 2; return; }
|
|
305
|
+
process.exitCode = await probeMassAssignmentCommand({
|
|
306
|
+
specPath: resolved.spec,
|
|
307
|
+
env: envFile.env,
|
|
308
|
+
output: rep.output,
|
|
309
|
+
emitTests: opts.emitTests,
|
|
310
|
+
tag: opts.tag,
|
|
311
|
+
listTags: opts.listTags,
|
|
312
|
+
noCleanup: opts.cleanup === false,
|
|
313
|
+
noDiscover: opts.discover === false,
|
|
314
|
+
timeoutMs,
|
|
315
|
+
overwrite: opts.overwrite === true,
|
|
316
|
+
json,
|
|
317
|
+
dryRun: opts.dryRun === true,
|
|
318
|
+
include: Array.isArray(opts.include) && opts.include.length > 0 ? opts.include : undefined,
|
|
319
|
+
exclude: Array.isArray(opts.exclude) && opts.exclude.length > 0 ? opts.exclude : undefined,
|
|
320
|
+
report: rep.report,
|
|
321
|
+
verbose: opts.verbose === true,
|
|
322
|
+
suspectField: Array.isArray(opts.suspectField) && opts.suspectField.length > 0 ? opts.suspectField : undefined,
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function defineProbeSecurity(parent: Command, name: string): void {
|
|
328
|
+
const sub = parent
|
|
329
|
+
// ARV-36: classes is technically required but kept optional in commander
|
|
330
|
+
// so the missing-arg branch can produce the same actionable list of
|
|
331
|
+
// available classes that --unknown-class already prints (data already in
|
|
332
|
+
// SECURITY_CLASSES — no reason to force a --help read for first-time users).
|
|
333
|
+
.command(`${name} [classes] [spec]`)
|
|
334
|
+
.description(
|
|
335
|
+
"Live security probes (TASK-138): SSRF / CRLF / open-redirect. Detects vulnerable fields by name+format, sends a baseline-OK then per-field payloads, classifies HIGH (5xx or echo) / LOW (2xx no echo) / OK (4xx). <classes> is a comma-separated subset of: ssrf, crlf, open-redirect.",
|
|
336
|
+
)
|
|
337
|
+
.option("--api <name>", "Use the registered API's spec (apis/<name>/spec.json)")
|
|
338
|
+
.option("--db <path>", "Path to SQLite database file")
|
|
339
|
+
.option("--env <file>", "Env YAML with base_url + auth_token (live calls require this; --dry-run can run without; auto-derived from apis/<name>/.env.yaml when --api is given)")
|
|
340
|
+
.option("--emit-tests <dir>", "Also emit YAML regression suites locking in safe behaviour for CI")
|
|
341
|
+
.option("--tag <tag>", "Probe only endpoints with this tag")
|
|
342
|
+
.option("--list-tags", "List available tags from spec and exit")
|
|
343
|
+
.option("--no-cleanup", "Skip follow-up DELETE on resources created by baseline / 2xx attacks")
|
|
344
|
+
.option("--isolated", "TASK-264: refuse to attack PUT/PATCH endpoints whose path-params come from .env.yaml — protects seeded fixtures from probe-induced mutation. Lower coverage in exchange for guaranteed fixture safety.")
|
|
345
|
+
.option("--allow-leaks", "ARV-140: attack POST endpoints even when the spec has no DELETE counterpart. Default: skip — without DELETE there is no cleanup path and resources accumulate in the target tenant (round-01/02 Sentry left 18 manual orphans). Use when you've vetted manual cleanup or are in a throwaway env.")
|
|
346
|
+
.option(
|
|
347
|
+
"--include <selector>",
|
|
348
|
+
"Filter operations (m-15 ARV-9 grammar: path:/users/.* | tag:Webhooks | method:POST,PATCH). Repeatable.",
|
|
349
|
+
(v: string, prev: string[] = []) => prev.concat(v),
|
|
350
|
+
[] as string[],
|
|
351
|
+
)
|
|
352
|
+
.option(
|
|
353
|
+
"--exclude <selector>",
|
|
354
|
+
"Drop operations matching <selector>. Repeatable.",
|
|
355
|
+
(v: string, prev: string[] = []) => prev.concat(v),
|
|
356
|
+
[] as string[],
|
|
357
|
+
)
|
|
358
|
+
.option("--dry-run", "Print which endpoints/fields would be attacked without sending requests")
|
|
359
|
+
.option("--timeout <ms>", "Per-request timeout in ms (overrides apis/<name>/.env.yaml `timeoutMs` and zond.config.yml `defaults.timeout_ms`; default 30000)", parsePositiveInt("--timeout"))
|
|
360
|
+
.option(
|
|
361
|
+
"--verbose",
|
|
362
|
+
"ARV-253: surface INFO-severity findings (sanitization-signal-only, e.g. CRLF accepted but no reflection observed). Default hides them — they're single-signal proof with no exploit pathway.",
|
|
363
|
+
);
|
|
364
|
+
addProbeReportOutputOptions(sub);
|
|
365
|
+
sub.action(async (classes: string | undefined, specPos: string | undefined, opts, cmd: Command) => {
|
|
366
|
+
// ARV-36: missing-arg path should list the available classes (parity
|
|
367
|
+
// with the unknown-class error). Commander's default `missing required
|
|
368
|
+
// argument` doesn't include them; once we made <classes> optional, we
|
|
369
|
+
// surface the same hint here.
|
|
370
|
+
if (typeof classes !== "string" || classes.length === 0) {
|
|
371
|
+
printError(`Missing required argument <classes>. Available: ${SECURITY_CLASSES.join(", ")}`);
|
|
372
|
+
process.exitCode = 2;
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
// ARV-33: same fallback chain as mass-assignment so `zond probe security
|
|
376
|
+
// ssrf --api foo` doesn't fall through to a confusing "base_url is
|
|
377
|
+
// required" when commander absorbs --api at the global scope.
|
|
378
|
+
const apiName = resolveProbeApi(opts.api, cmd);
|
|
379
|
+
const resolved = resolveSpecArg(specPos, apiName, opts.db);
|
|
380
|
+
if ("error" in resolved) { printError(resolved.error); process.exitCode = 2; return; }
|
|
381
|
+
// probe-security tolerates a missing env (--dry-run path), so don't
|
|
382
|
+
// fail when --api is given but the env file isn't on disk yet.
|
|
383
|
+
const envFile = resolveProbeEnv(opts.env, apiName, opts.db, { tolerateMissing: true });
|
|
384
|
+
if ("error" in envFile) { printError(envFile.error); process.exitCode = 2; return; }
|
|
385
|
+
const timeoutMs = await resolveProbeTimeout(opts.timeout, apiName, envFile.env);
|
|
386
|
+
const json = globalJson(cmd);
|
|
387
|
+
const rep = resolveProbeOutputFlags("probe-security", opts, json);
|
|
388
|
+
if (!rep) { process.exitCode = 2; return; }
|
|
389
|
+
process.exitCode = await probeSecurityCommand({
|
|
390
|
+
specPath: resolved.spec,
|
|
391
|
+
classes,
|
|
392
|
+
env: envFile.env,
|
|
393
|
+
output: rep.output,
|
|
394
|
+
emitTests: opts.emitTests,
|
|
395
|
+
tag: opts.tag,
|
|
396
|
+
listTags: opts.listTags,
|
|
397
|
+
noCleanup: opts.cleanup === false,
|
|
398
|
+
dryRun: opts.dryRun === true,
|
|
399
|
+
timeoutMs,
|
|
400
|
+
overwrite: opts.overwrite === true,
|
|
401
|
+
json,
|
|
402
|
+
apiName,
|
|
403
|
+
isolated: opts.isolated === true,
|
|
404
|
+
allowLeaks: opts.allowLeaks === true,
|
|
405
|
+
report: rep.report,
|
|
406
|
+
include: Array.isArray(opts.include) && opts.include.length > 0 ? opts.include : undefined,
|
|
407
|
+
exclude: Array.isArray(opts.exclude) && opts.exclude.length > 0 ? opts.exclude : undefined,
|
|
408
|
+
verbose: opts.verbose === true,
|
|
409
|
+
});
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* ARV-173 (m-20): `zond probe webhooks` — offline shape-conformance for
|
|
415
|
+
* webhook events captured by `docs/recipes/webhook-receiver.md`.
|
|
416
|
+
*
|
|
417
|
+
* Live HTTP infrastructure (tunnels, listeners) lives in the recipe,
|
|
418
|
+
* not in core zond. The CLI takes a pre-captured ndjson log + the
|
|
419
|
+
* API's spec and validates each event's payload against
|
|
420
|
+
* `spec.webhooks.<event>.post.requestBody`. Same recipe/probe split as
|
|
421
|
+
* m-18's quicktype and interactsh.
|
|
422
|
+
*/
|
|
423
|
+
function defineProbeWebhooks(parent: Command, name: string): void {
|
|
424
|
+
const sub = parent
|
|
425
|
+
.command(`${name} [spec]`)
|
|
426
|
+
.description("Shape-conform captured webhook events (ndjson) against spec.webhooks. Recipe: docs/recipes/webhook-receiver.md")
|
|
427
|
+
.option("--api <name>", "Use the registered API's spec (apis/<name>/spec.json)")
|
|
428
|
+
.option("--db <path>", "Path to SQLite database file")
|
|
429
|
+
.requiredOption("--event-log <file>", "ndjson event log captured by the recipe (one JSON event per line)")
|
|
430
|
+
.option("--only <types>", "Comma-separated event types to validate (default: all declared)");
|
|
431
|
+
addProbeReportOutputOptions(sub);
|
|
432
|
+
sub.action(async (specPos: string | undefined, opts, cmd: Command) => {
|
|
433
|
+
const apiName = resolveProbeApi(opts.api, cmd);
|
|
434
|
+
const resolved = resolveSpecArg(specPos, apiName, opts.db);
|
|
435
|
+
if ("error" in resolved) { printError(resolved.error); process.exitCode = 2; return; }
|
|
436
|
+
const json = globalJson(cmd);
|
|
437
|
+
const rep = resolveProbeOutputFlags("probe-webhooks", opts, json);
|
|
438
|
+
if (!rep) { process.exitCode = 2; return; }
|
|
439
|
+
process.exitCode = await probeWebhooksCommand({
|
|
440
|
+
specPath: resolved.spec,
|
|
441
|
+
eventLog: opts.eventLog,
|
|
442
|
+
only: opts.only,
|
|
443
|
+
report: rep.report,
|
|
444
|
+
output: rep.output,
|
|
445
|
+
json,
|
|
446
|
+
});
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
export function registerProbes(program: Command): void {
|
|
451
|
+
const probeCmd = program
|
|
452
|
+
.command("probe")
|
|
453
|
+
.description("Run a probe class — pick one of: static, mass-assignment, security, webhooks");
|
|
454
|
+
|
|
455
|
+
defineProbeStatic(probeCmd, "static");
|
|
456
|
+
defineProbeMassAssignment(probeCmd, "mass-assignment");
|
|
457
|
+
defineProbeSecurity(probeCmd, "security");
|
|
458
|
+
defineProbeWebhooks(probeCmd, "webhooks");
|
|
459
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `zond reference <topic>` — printable cheat-sheets for built-ins that aren't
|
|
3
|
+
* surfaced anywhere else (TASK-267).
|
|
4
|
+
*
|
|
5
|
+
* Currently:
|
|
6
|
+
* reference random-helpers # all `{{$random*}}` generators with examples
|
|
7
|
+
*
|
|
8
|
+
* Designed to be discoverable from `--help` AND scriptable: `--json` returns
|
|
9
|
+
* an array of `{ name, example }` entries so the `zond-triage` / generator
|
|
10
|
+
* skills can prompt with concrete values.
|
|
11
|
+
*/
|
|
12
|
+
import type { Command } from "commander";
|
|
13
|
+
import { GENERATORS } from "../../core/parser/variables.ts";
|
|
14
|
+
import { jsonOk, printJson } from "../json-envelope.ts";
|
|
15
|
+
import { globalJson } from "../resolve.ts";
|
|
16
|
+
|
|
17
|
+
interface HelperEntry {
|
|
18
|
+
name: string;
|
|
19
|
+
example: string;
|
|
20
|
+
use_for: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const HELPER_NOTES: Record<string, string> = {
|
|
24
|
+
"$uuid": "RFC 4122 v4 — Idempotency-Key, opaque resource ids",
|
|
25
|
+
"$timestamp": "UNIX seconds — created_at, monotonic seeds",
|
|
26
|
+
"$isoTimestamp": "RFC 3339 timestamps",
|
|
27
|
+
"$randomName": "display names",
|
|
28
|
+
"$randomEmail": "unique e-mail body fields",
|
|
29
|
+
"$randomInt": "0–9999, small numeric ids",
|
|
30
|
+
"$randomString": "8 chars mixed-case + digits — opaque tokens",
|
|
31
|
+
"$randomSlug": "8 chars lowercase + digits — slug / handle / URL-safe ids",
|
|
32
|
+
"$randomUrl": "webhook / callback URL fields",
|
|
33
|
+
"$randomFqdn": "DNS / hostname inputs",
|
|
34
|
+
"$randomDomain": "alias of $randomFqdn",
|
|
35
|
+
"$randomIpv4": "RFC 1918 range — client_ip / source_ip",
|
|
36
|
+
"$randomDate": "calendar dates (YYYY-MM-DD)",
|
|
37
|
+
"$randomIsoDate": "ISO-8601 datetime",
|
|
38
|
+
"$nullByte": "single space — placeholder for fields that reject empty strings",
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
function collectEntries(): HelperEntry[] {
|
|
42
|
+
return Object.keys(GENERATORS)
|
|
43
|
+
.sort()
|
|
44
|
+
.map((name) => ({
|
|
45
|
+
name,
|
|
46
|
+
example: String(GENERATORS[name]!()),
|
|
47
|
+
use_for: HELPER_NOTES[name] ?? "",
|
|
48
|
+
}));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function renderTable(entries: HelperEntry[]): string {
|
|
52
|
+
const nameWidth = Math.max(8, ...entries.map((e) => e.name.length));
|
|
53
|
+
const exampleWidth = Math.max(10, ...entries.map((e) => e.example.length));
|
|
54
|
+
const lines: string[] = [];
|
|
55
|
+
lines.push(`${pad("Helper", nameWidth)} ${pad("Example", exampleWidth)} Use for`);
|
|
56
|
+
lines.push(`${"-".repeat(nameWidth)} ${"-".repeat(exampleWidth)} ${"-".repeat(20)}`);
|
|
57
|
+
for (const e of entries) {
|
|
58
|
+
lines.push(`${pad(`{{${e.name}}}`, nameWidth + 4)} ${pad(e.example, exampleWidth)} ${e.use_for}`);
|
|
59
|
+
}
|
|
60
|
+
return lines.join("\n");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function pad(s: string, width: number): string {
|
|
64
|
+
return s.length >= width ? s : s + " ".repeat(width - s.length);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function registerReference(program: Command): void {
|
|
68
|
+
const ref = program
|
|
69
|
+
.command("reference")
|
|
70
|
+
.description("Printable references for built-ins (e.g. `random-helpers`).");
|
|
71
|
+
|
|
72
|
+
ref
|
|
73
|
+
.command("random-helpers")
|
|
74
|
+
.description("List every `{{$random*}}` / `{{$uuid}}` / `{{$timestamp}}` helper, with a sample value and typical use. Pair `--json` for machine-readable output (TASK-267).")
|
|
75
|
+
.action((opts: unknown, cmd: Command) => {
|
|
76
|
+
const entries = collectEntries();
|
|
77
|
+
if (globalJson(cmd)) {
|
|
78
|
+
printJson(jsonOk("reference random-helpers", { helpers: entries }));
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
process.stdout.write(renderTable(entries) + "\n");
|
|
82
|
+
process.stdout.write(
|
|
83
|
+
"\nFor field-name → helper mapping used by `zond generate`, see docs/random-helpers.md.\n",
|
|
84
|
+
);
|
|
85
|
+
void opts;
|
|
86
|
+
});
|
|
87
|
+
}
|