@lovrozagar/oat 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 (145) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +401 -0
  3. package/bin/oat.js +2 -0
  4. package/dist/cli.js +386 -0
  5. package/dist/config/define-config.d.ts +165 -0
  6. package/dist/config/define-config.d.ts.map +1 -0
  7. package/dist/config/define-config.js +24 -0
  8. package/dist/config/define-config.js.map +1 -0
  9. package/dist/config/load.d.ts +10 -0
  10. package/dist/config/load.d.ts.map +1 -0
  11. package/dist/config/load.js +61 -0
  12. package/dist/config/load.js.map +1 -0
  13. package/dist/index.d.ts +17 -0
  14. package/dist/index.js +9 -0
  15. package/dist/reference/defects.d.ts +254 -0
  16. package/dist/reference/defects.d.ts.map +1 -0
  17. package/dist/reference/defects.js +265 -0
  18. package/dist/reference/defects.js.map +1 -0
  19. package/dist/reference/dialect.d.ts +104 -0
  20. package/dist/reference/dialect.d.ts.map +1 -0
  21. package/dist/reference/dialect.js +174 -0
  22. package/dist/reference/dialect.js.map +1 -0
  23. package/dist/reference/http.d.ts +79 -0
  24. package/dist/reference/http.d.ts.map +1 -0
  25. package/dist/reference/http.js +889 -0
  26. package/dist/reference/http.js.map +1 -0
  27. package/dist/reference/model.d.ts +56 -0
  28. package/dist/reference/model.d.ts.map +1 -0
  29. package/dist/reference/model.js +156 -0
  30. package/dist/reference/model.js.map +1 -0
  31. package/dist/reference/query.d.ts +45 -0
  32. package/dist/reference/query.d.ts.map +1 -0
  33. package/dist/reference/query.js +353 -0
  34. package/dist/reference/query.js.map +1 -0
  35. package/dist/reference/spec.d.ts +13 -0
  36. package/dist/reference/spec.d.ts.map +1 -0
  37. package/dist/reference/spec.js +547 -0
  38. package/dist/reference/spec.js.map +1 -0
  39. package/dist/reference/store-api.d.ts +59 -0
  40. package/dist/reference/store-api.d.ts.map +1 -0
  41. package/dist/reference/store-api.js +76 -0
  42. package/dist/reference/store-api.js.map +1 -0
  43. package/dist/reference/stores/memory.d.ts +27 -0
  44. package/dist/reference/stores/memory.d.ts.map +1 -0
  45. package/dist/reference/stores/memory.js +97 -0
  46. package/dist/reference/stores/memory.js.map +1 -0
  47. package/dist/reference/stores/postgres.d.ts +61 -0
  48. package/dist/reference/stores/postgres.d.ts.map +1 -0
  49. package/dist/reference/stores/postgres.js +406 -0
  50. package/dist/reference/stores/postgres.js.map +1 -0
  51. package/dist/reference/stores/sqlite-driver.d.ts +54 -0
  52. package/dist/reference/stores/sqlite-driver.d.ts.map +1 -0
  53. package/dist/reference/stores/sqlite-driver.js +99 -0
  54. package/dist/reference/stores/sqlite-driver.js.map +1 -0
  55. package/dist/reference/stores/sqlite.d.ts +68 -0
  56. package/dist/reference/stores/sqlite.d.ts.map +1 -0
  57. package/dist/reference/stores/sqlite.js +464 -0
  58. package/dist/reference/stores/sqlite.js.map +1 -0
  59. package/dist/report/console.d.ts +33 -0
  60. package/dist/report/console.d.ts.map +1 -0
  61. package/dist/report/console.js +237 -0
  62. package/dist/report/console.js.map +1 -0
  63. package/dist/report/matrix.d.ts +131 -0
  64. package/dist/report/matrix.d.ts.map +1 -0
  65. package/dist/report/matrix.js +865 -0
  66. package/dist/report/matrix.js.map +1 -0
  67. package/dist/report/render.d.ts +66 -0
  68. package/dist/report/render.d.ts.map +1 -0
  69. package/dist/report/render.js +423 -0
  70. package/dist/report/render.js.map +1 -0
  71. package/dist/runtime/async.d.ts +34 -0
  72. package/dist/runtime/async.d.ts.map +1 -0
  73. package/dist/runtime/async.js +130 -0
  74. package/dist/runtime/async.js.map +1 -0
  75. package/dist/runtime/auth.d.ts +46 -0
  76. package/dist/runtime/auth.d.ts.map +1 -0
  77. package/dist/runtime/auth.js +236 -0
  78. package/dist/runtime/auth.js.map +1 -0
  79. package/dist/runtime/checks.d.ts +89 -0
  80. package/dist/runtime/checks.d.ts.map +1 -0
  81. package/dist/runtime/checks.js +3400 -0
  82. package/dist/runtime/checks.js.map +1 -0
  83. package/dist/runtime/client.d.ts +65 -0
  84. package/dist/runtime/client.d.ts.map +1 -0
  85. package/dist/runtime/client.js +122 -0
  86. package/dist/runtime/client.js.map +1 -0
  87. package/dist/runtime/finding.d.ts +50 -0
  88. package/dist/runtime/finding.d.ts.map +1 -0
  89. package/dist/runtime/finding.js +50 -0
  90. package/dist/runtime/finding.js.map +1 -0
  91. package/dist/runtime/fixture.d.ts +20 -0
  92. package/dist/runtime/fixture.d.ts.map +1 -0
  93. package/dist/runtime/fixture.js +175 -0
  94. package/dist/runtime/fixture.js.map +1 -0
  95. package/dist/runtime/progress.d.ts +41 -0
  96. package/dist/runtime/progress.d.ts.map +1 -0
  97. package/dist/runtime/progress.js +186 -0
  98. package/dist/runtime/progress.js.map +1 -0
  99. package/dist/runtime/run.d.ts +69 -0
  100. package/dist/runtime/run.d.ts.map +1 -0
  101. package/dist/runtime/run.js +575 -0
  102. package/dist/runtime/run.js.map +1 -0
  103. package/dist/runtime/teardown.d.ts +38 -0
  104. package/dist/runtime/teardown.d.ts.map +1 -0
  105. package/dist/runtime/teardown.js +129 -0
  106. package/dist/runtime/teardown.js.map +1 -0
  107. package/dist/runtime/validate.d.ts +31 -0
  108. package/dist/runtime/validate.d.ts.map +1 -0
  109. package/dist/runtime/validate.js +111 -0
  110. package/dist/runtime/validate.js.map +1 -0
  111. package/dist/runtime/world.d.ts +49 -0
  112. package/dist/runtime/world.d.ts.map +1 -0
  113. package/dist/runtime/world.js +119 -0
  114. package/dist/runtime/world.js.map +1 -0
  115. package/dist/spec/collection.d.ts +31 -0
  116. package/dist/spec/collection.d.ts.map +1 -0
  117. package/dist/spec/collection.js +121 -0
  118. package/dist/spec/collection.js.map +1 -0
  119. package/dist/spec/conventions.d.ts +103 -0
  120. package/dist/spec/conventions.d.ts.map +1 -0
  121. package/dist/spec/conventions.js +275 -0
  122. package/dist/spec/conventions.js.map +1 -0
  123. package/dist/spec/extensions.d.ts +74 -0
  124. package/dist/spec/extensions.d.ts.map +1 -0
  125. package/dist/spec/extensions.js +335 -0
  126. package/dist/spec/extensions.js.map +1 -0
  127. package/dist/spec/graph.d.ts +89 -0
  128. package/dist/spec/graph.d.ts.map +1 -0
  129. package/dist/spec/graph.js +318 -0
  130. package/dist/spec/graph.js.map +1 -0
  131. package/dist/spec/load.d.ts +36 -0
  132. package/dist/spec/load.d.ts.map +1 -0
  133. package/dist/spec/load.js +166 -0
  134. package/dist/spec/load.js.map +1 -0
  135. package/dist/spec/types.d.ts +57 -0
  136. package/dist/spec/types.d.ts.map +1 -0
  137. package/dist/spec/types.js +24 -0
  138. package/dist/spec/types.js.map +1 -0
  139. package/labs/README.md +130 -0
  140. package/labs/annotated-openapi.yaml +311 -0
  141. package/labs/anyrow.config.ts +107 -0
  142. package/labs/demo.mjs +124 -0
  143. package/labs/local.config.ts +41 -0
  144. package/labs/minimal.config.ts +32 -0
  145. package/package.json +80 -0
package/dist/cli.js ADDED
@@ -0,0 +1,386 @@
1
+ #!/usr/bin/env node
2
+ import { createWriteStream, writeFileSync } from "node:fs";
3
+ import { mkdir, rm, writeFile } from "node:fs/promises";
4
+ import { resolve } from "node:path";
5
+ import { interpolate, loadConfig } from "./config/load.js";
6
+ import { report } from "./report/console.js";
7
+ import { renderMatrixGraph, renderMatrixHtml } from "./report/matrix.js";
8
+ import { ISSUE_REPRO_DIR, renderConsole, renderJson, renderMarkdown, renderRepros } from "./report/render.js";
9
+ import { createStderrProgress, formatProgressJsonl, formatProgressLine, formatProgressTsv, PROGRESS_GLOSSARY, PROGRESS_TSV_HEADER, } from "./runtime/progress.js";
10
+ import { renderTeardown } from "./runtime/teardown.js";
11
+ import { buildModel } from "./spec/graph.js";
12
+ import { dereference, loadSpec } from "./spec/load.js";
13
+ function parseArgs(argv) {
14
+ const [command = "help", ...rest] = argv;
15
+ const flags = {};
16
+ for (let i = 0; i < rest.length; i++) {
17
+ const token = rest[i];
18
+ if (token === undefined || !token.startsWith("--"))
19
+ continue;
20
+ const key = token.slice(2);
21
+ const next = rest[i + 1];
22
+ if (next !== undefined && !next.startsWith("--")) {
23
+ flags[key] = next;
24
+ i++;
25
+ }
26
+ else {
27
+ flags[key] = true;
28
+ }
29
+ }
30
+ return { command, flags };
31
+ }
32
+ const USAGE = `oat — OpenAPI Tester
33
+
34
+ oat run --config <file> test a live backend and write a report
35
+ oat plan --spec <url|file> derive and print the test model (offline)
36
+ oat doctor --spec <url|file> report what oat can and cannot test, and why
37
+ oat serve [--defects A,B] run a demo API to point oat at
38
+ oat conformance self-test: injected defects vs detection
39
+
40
+ Flags
41
+ --config oat config module (.ts/.js/.mjs/.json) with a default export
42
+ --spec OpenAPI document, http(s) URL or filesystem path
43
+ --base-url backend under test, overriding the config
44
+ --only comma-separated entity names to restrict the run to
45
+ --seed integer seed for fixture generation (default 1)
46
+ --out output directory for the report (default ./oat-out)
47
+ --keep-fixtures leave created records in place instead of tearing them down
48
+ --concurrency entities tested in parallel (default 1)
49
+ --max-in-flight requests allowed in flight at once (default 4)
50
+ --quiet no live progress on stderr (progress.log still written)
51
+ --untagged serve (or test) a document with every x-* tag stripped
52
+ --backend conformance storage: memory | sqlite | postgres | d1
53
+ (default: all local; d1 is remote and opt-in)
54
+ --dialect API shape: postgrest | classic | linked | jsonapi | plain (default: all)
55
+ --fuzz [n] inject random defect *combinations* instead of one at a time
56
+ --precision [n] vary cohort data against a correct backend; any finding is a false positive
57
+ --max-defects most defects per combination (default: 4)
58
+ --seed fuzz seed, so a failing combination replays exactly
59
+ --json machine-readable output, for plan and doctor
60
+ `;
61
+ function str(flags, key) {
62
+ const value = flags[key];
63
+ return typeof value === "string" ? value : undefined;
64
+ }
65
+ async function commandRun(flags) {
66
+ const configPath = str(flags, "config");
67
+ if (configPath === undefined) {
68
+ process.stderr.write("oat: run requires --config\n\n" + USAGE);
69
+ return 2;
70
+ }
71
+ const { run } = await import("./runtime/run.js");
72
+ const config = interpolate(await loadConfig(configPath));
73
+ const baseUrl = str(flags, "base-url") ?? config.baseUrl;
74
+ const seedFlag = str(flags, "seed");
75
+ const only = str(flags, "only")?.split(",").map((s) => s.trim()).filter(Boolean);
76
+ /* No cast: the config's principal type *is* the runtime's, so a mistake here is a compile
77
+ * error in the user's own config rather than a surprise mid-run. */
78
+ const principals = config.principals ?? [];
79
+ if (principals.length === 0) {
80
+ process.stderr.write("oat: config declares no principals. At least one is required so oat can authenticate; " +
81
+ "a second in a different tenant enables the isolation checks.\n");
82
+ return 2;
83
+ }
84
+ const startedAt = new Date();
85
+ const began = performance.now();
86
+ const outDir = resolve(str(flags, "out") ?? config.outDir ?? "oat-out");
87
+ await mkdir(outDir, { recursive: true });
88
+ const stderrProgress = flags.quiet === true ? undefined : createStderrProgress(startedAt.getTime());
89
+ const progressLog = createWriteStream(resolve(outDir, "progress.log"));
90
+ const progressTsv = createWriteStream(resolve(outDir, "progress.tsv"));
91
+ const progressJsonl = createWriteStream(resolve(outDir, "progress.jsonl"));
92
+ progressLog.write(`${PROGRESS_GLOSSARY}\n`);
93
+ progressTsv.write(`${PROGRESS_TSV_HEADER}\n`);
94
+ let lastLogKey = "";
95
+ let lastLogAt = 0;
96
+ let lastJsonAt = 0;
97
+ const onProgress = (snap) => {
98
+ stderrProgress?.emit(snap);
99
+ const key = `${snap.phase}|${snap.entity ?? ""}|${snap.check ?? ""}|${snap.message ?? ""}`;
100
+ const due = Date.now() - lastLogAt >= 2_000;
101
+ if (key !== lastLogKey || due || snap.phase === "done" || snap.phase === "load") {
102
+ lastLogKey = key;
103
+ lastLogAt = Date.now();
104
+ const now = Date.now();
105
+ progressLog.write(`${formatProgressLine(snap, now)}\n`);
106
+ progressTsv.write(`${formatProgressTsv(snap, now)}\n`);
107
+ progressJsonl.write(`${formatProgressJsonl(snap, now)}\n`);
108
+ }
109
+ if (Date.now() - lastJsonAt >= 1_000 || snap.phase === "done") {
110
+ lastJsonAt = Date.now();
111
+ const payload = {
112
+ ...snap,
113
+ lastAgoMs: snap.last === undefined ? null : Date.now() - snap.last.at,
114
+ updatedAt: new Date().toISOString(),
115
+ };
116
+ writeFileSync(resolve(outDir, "progress.json"), `${JSON.stringify(payload, null, 2)}\n`);
117
+ }
118
+ };
119
+ let result;
120
+ try {
121
+ result = await run({
122
+ baseUrl,
123
+ principals,
124
+ spec: config.spec,
125
+ ...(config.globalHeaders === undefined ? {} : { globalHeaders: config.globalHeaders }),
126
+ ...(config.hooks === undefined ? {} : { hooks: config.hooks }),
127
+ ...(config.roots === undefined ? {} : { roots: config.roots }),
128
+ ...(config.cohortSize === undefined ? {} : { cohortSize: config.cohortSize }),
129
+ ...(only === undefined ? {} : { only }),
130
+ ...(flags["keep-fixtures"] === true ? { keepFixtures: true } : {}),
131
+ concurrency: Number.parseInt(str(flags, "concurrency") ?? "", 10) || config.concurrency || 1,
132
+ maxInFlight: Number.parseInt(str(flags, "max-in-flight") ?? "", 10) || 4,
133
+ seed: seedFlag === undefined ? (config.seed ?? 1) : Number.parseInt(seedFlag, 10),
134
+ onProgress,
135
+ });
136
+ }
137
+ finally {
138
+ stderrProgress?.stop();
139
+ progressLog.end();
140
+ progressTsv.end();
141
+ progressJsonl.end();
142
+ }
143
+ const durationMs = performance.now() - began;
144
+ const input = {
145
+ baseUrl,
146
+ checksRun: result.checksRun,
147
+ checksSkipped: result.checksSkipped,
148
+ checksSuppressed: result.checksSuppressed,
149
+ inconclusive: result.inconclusive,
150
+ client: result.client,
151
+ durationMs,
152
+ entitiesTested: result.entitiesTested,
153
+ findings: result.findings,
154
+ model: result.model,
155
+ startedAt,
156
+ };
157
+ /* Wipe both the old name and the current one. A leftover script from a previous
158
+ * finding reads as a current defect. The folder is only created when there is an issue. */
159
+ await rm(resolve(outDir, "repro"), { force: true, recursive: true });
160
+ await rm(resolve(outDir, ISSUE_REPRO_DIR), { force: true, recursive: true });
161
+ await writeFile(resolve(outDir, "oat-report.md"), renderMarkdown(input));
162
+ await writeFile(resolve(outDir, "oat-report.json"), renderJson(input));
163
+ await writeFile(resolve(outDir, "matrix.html"), renderMatrixHtml(input));
164
+ await writeFile(resolve(outDir, "matrix.json"), renderMatrixGraph(input));
165
+ const scripts = renderRepros(result.findings, baseUrl);
166
+ if (scripts.length > 0) {
167
+ const dir = resolve(outDir, ISSUE_REPRO_DIR);
168
+ await mkdir(dir, { recursive: true });
169
+ for (const script of scripts) {
170
+ await writeFile(resolve(dir, script.filename), script.content, { mode: 0o755 });
171
+ }
172
+ }
173
+ process.stdout.write(renderConsole(input));
174
+ for (const line of renderTeardown(result.teardown ?? { failed: [], removed: 0, unsupported: [] }, result.created)) {
175
+ process.stdout.write(`${line}\n`);
176
+ }
177
+ process.stdout.write(` report: ${resolve(outDir, "oat-report.md")}\n`);
178
+ process.stdout.write(` matrix: ${resolve(outDir, "matrix.html")}\n`);
179
+ process.stdout.write(` graph: ${resolve(outDir, "matrix.json")}\n`);
180
+ process.stdout.write(` progress: ${resolve(outDir, "progress.log")} · ${resolve(outDir, "progress.jsonl")}\n\n`);
181
+ /* Exit code counts root causes, not raw findings: gaps and blocked entries are information,
182
+ * not failures, and a CI gate should react to defects only. */
183
+ return result.findings.filter((f) => f.verdict !== "COVERAGE_GAP" && f.verdict !== "BLOCKED")
184
+ .length > 0
185
+ ? 1
186
+ : 0;
187
+ }
188
+ async function main() {
189
+ const { command, flags } = parseArgs(process.argv.slice(2));
190
+ if (command === "help" || flags.help === true) {
191
+ process.stdout.write(USAGE);
192
+ return 0;
193
+ }
194
+ if (command === "conformance") {
195
+ const { postgresAvailable, renderParserSuite, renderSuite, runExampleSpecSuite, runTagUnlockSuite, runParserSuite, runCoverageReportSuite, runSuite, sqliteAvailable, d1Available, } = await import("./conformance/suite.js");
196
+ const parser = renderParserSuite(runParserSuite());
197
+ process.stdout.write(parser.text);
198
+ /* The documented example is checked in the same breath: it is the only place a reader
199
+ * sees where each tag goes, and documentation that drifts teaches a shape that no longer
200
+ * works. */
201
+ const example = renderParserSuite(await runExampleSpecSuite());
202
+ process.stdout.write(example.text);
203
+ parser.failures += example.failures;
204
+ const unlocks = renderParserSuite(await runTagUnlockSuite());
205
+ process.stdout.write(unlocks.text);
206
+ parser.failures += unlocks.failures;
207
+ const coverage = renderParserSuite(runCoverageReportSuite());
208
+ process.stdout.write(coverage.text);
209
+ parser.failures += coverage.failures;
210
+ if (flags.parser === true)
211
+ return parser.failures > 0 ? 1 : 0;
212
+ if (flags.precision !== undefined) {
213
+ /* Varies the data rather than the faults, against a backend with nothing wrong with it.
214
+ * Any finding here is a false positive by construction. */
215
+ const { renderPrecision, runPrecision } = await import("./conformance/fuzz.js");
216
+ const n = typeof flags.precision === "string" ? Number.parseInt(flags.precision, 10) : 50;
217
+ const rendered = renderPrecision(await runPrecision({
218
+ backend: (str(flags, "backend") ?? "memory"),
219
+ cases: Number.isFinite(n) && n > 0 ? n : 50,
220
+ dialect: str(flags, "dialect") ?? "postgrest",
221
+ seed: Number.parseInt(str(flags, "seed") ?? "1", 10) || 1,
222
+ }));
223
+ process.stdout.write(rendered.text);
224
+ return parser.failures + rendered.failures > 0 ? 1 : 0;
225
+ }
226
+ if (flags.fuzz !== undefined) {
227
+ /* Combination fuzzing is its own question — whether the diagnosis survives several
228
+ * simultaneous faults — so it replaces the matrix rather than padding it. */
229
+ const { renderFuzz, runFuzz } = await import("./conformance/fuzz.js");
230
+ const count = typeof flags.fuzz === "string" ? Number.parseInt(flags.fuzz, 10) : 25;
231
+ const fuzzBackend = str(flags, "backend");
232
+ const results = await runFuzz({
233
+ backend: (fuzzBackend ?? "memory"),
234
+ cases: Number.isFinite(count) && count > 0 ? count : 25,
235
+ dialect: str(flags, "dialect") ?? "postgrest",
236
+ maxDefects: Number.parseInt(str(flags, "max-defects") ?? "4", 10) || 4,
237
+ seed: Number.parseInt(str(flags, "seed") ?? "1", 10) || 1,
238
+ });
239
+ const rendered = renderFuzz(results);
240
+ process.stdout.write(rendered.text);
241
+ return parser.failures + rendered.failures > 0 ? 1 : 0;
242
+ }
243
+ const only = str(flags, "only")?.split(",");
244
+ const requested = str(flags, "backend");
245
+ /* Both by default. A check that passes against the in-memory store but not against real
246
+ * SQL was relying on JavaScript semantics — NULL ordering, collation, LIKE escaping — that
247
+ * a database does not share, and running only one backend hides exactly that. */
248
+ const all = ["memory", "sqlite", "postgres", "d1"];
249
+ const available = ["memory"];
250
+ if (await sqliteAvailable())
251
+ available.push("sqlite");
252
+ if (await postgresAvailable())
253
+ available.push("postgres");
254
+ /* Present but never default: D1 is remote, so a pass costs minutes and consumes someone's
255
+ * quota. It runs when asked for by name. */
256
+ const d1Ready = d1Available();
257
+ const backends = all.includes(requested)
258
+ ? [requested]
259
+ : available;
260
+ const skipped = all.filter((b) => b !== "d1" && !available.includes(b));
261
+ const dialect = str(flags, "dialect");
262
+ /* Backends vary the storage engine; dialects vary the API's conventions. Running the full
263
+ * cross product would be mostly redundant, so every backend is exercised on the default
264
+ * dialect and one extra pass covers a second dialect that shares no parameter names. */
265
+ const passes = dialect === undefined && requested === undefined
266
+ ? [
267
+ ...backends.map((backend) => ({ backend, dialect: "postgrest" })),
268
+ /* Two extra shapes on the cheapest backend. `classic` renames everything;
269
+ * `linked` changes the pagination *model* — root array, Link header, row
270
+ * offsets — which is the stronger test of whether checks read the document
271
+ * or the fixture's habits. */
272
+ { backend: "memory", dialect: "classic" },
273
+ { backend: "memory", dialect: "linked" },
274
+ { backend: "memory", dialect: "jsonapi" },
275
+ { backend: "memory", dialect: "plain" },
276
+ ]
277
+ : backends.map((backend) => ({ backend, dialect: dialect ?? "postgrest" }));
278
+ let failures = parser.failures;
279
+ for (const pass of passes) {
280
+ process.stdout.write(`\n ── ${pass.backend} · ${pass.dialect} ${"─".repeat(46)}\n`);
281
+ const result = renderSuite(await runSuite(only, pass.backend, pass.dialect), pass.dialect);
282
+ process.stdout.write(result.text);
283
+ failures += result.failures;
284
+ }
285
+ if (requested === undefined && skipped.length > 0) {
286
+ for (const backend of skipped) {
287
+ const why = backend === "sqlite"
288
+ ? "node:sqlite unavailable (Node 22 needs --experimental-sqlite)"
289
+ : "no Postgres server reachable on the default connection";
290
+ process.stdout.write(` note: ${backend} backend skipped — ${why}\n`);
291
+ }
292
+ process.stdout.write("\n");
293
+ }
294
+ if (requested === undefined && only === undefined) {
295
+ /* A short combination pass runs as part of the standard self-test. Single-defect recall
296
+ * says nothing about how the diagnosis holds up when several faults overlap, and every
297
+ * bug the fuzzer has found so far — silent bail-outs, non-transitive suppression, a
298
+ * probe writing into a constrained field — was invisible to the one-at-a-time matrix. */
299
+ const { renderFuzz, runFuzz } = await import("./conformance/fuzz.js");
300
+ process.stdout.write(`\n ── combinations ${"─".repeat(46)}\n`);
301
+ const smoke = renderFuzz(await runFuzz({ backend: "memory", cases: 40, maxDefects: 6, seed: 1 }));
302
+ process.stdout.write(smoke.text);
303
+ failures += smoke.failures;
304
+ }
305
+ if (requested === undefined) {
306
+ process.stdout.write(d1Ready
307
+ ? " note: d1 backend available — run with --backend d1 (remote, ~2min per defect)\n\n"
308
+ : " note: d1 backend needs CLOUDFLARE_ACCOUNT_ID, CLOUDFLARE_D1_DATABASE_ID, CLOUDFLARE_API_TOKEN\n\n");
309
+ }
310
+ return failures > 0 ? 1 : 0;
311
+ }
312
+ if (command === "serve") {
313
+ /* A built-in demo API so oat can be tried before wiring it to anything real. The
314
+ * reference backend doubles as the conformance fixture, so this is the same server the
315
+ * self-test runs against. */
316
+ const backend = str(flags, "backend") ?? "memory";
317
+ const defects = str(flags, "defects")?.split(",").filter(Boolean) ?? [];
318
+ const { createMemoryServer, createPostgresServer, createSqliteServer } = await import("./reference/http.js");
319
+ const factory = backend === "sqlite"
320
+ ? createSqliteServer
321
+ : backend === "postgres"
322
+ ? createPostgresServer
323
+ : createMemoryServer;
324
+ /* `--untagged` serves the same API behind a document stripped of every x-* tag, which is
325
+ * what `oat doctor` should be pointed at to see what a plain OpenAPI document costs. */
326
+ const untagged = flags.untagged === true;
327
+ const dialect = str(flags, "dialect");
328
+ const server = await factory({
329
+ defects,
330
+ untagged,
331
+ ...(dialect === undefined ? {} : { dialect }),
332
+ });
333
+ process.stdout.write(`\n oat demo API — ${backend}${untagged ? " (untagged spec)" : ""}\n` +
334
+ ` url ${server.url}\n` +
335
+ ` spec ${server.url}/v1/openapi/spec\n` +
336
+ ` dialect ${dialect ?? "postgrest"}\n` +
337
+ ` defects ${defects.length > 0 ? defects.join(", ") : "none — a correct backend"}\n` +
338
+ ` keys key_alpha (proj_alpha) · key_beta (proj_beta)\n\n` +
339
+ " point a config at it:\n" +
340
+ ` oat run --config labs/local.config.ts --base-url ${server.url}\n\n` +
341
+ " ctrl-c to stop\n\n");
342
+ /* Node buffers stdout when it is not a TTY, so a piped `oat serve` would print nothing
343
+ * until exit — indistinguishable from a hang. */
344
+ if (typeof process.stdout.write === "function")
345
+ process.stdout.uncork?.();
346
+ /* Resolves only on signal — the process should stay up serving. */
347
+ await new Promise((resolve) => {
348
+ process.on("SIGINT", () => resolve());
349
+ process.on("SIGTERM", () => resolve());
350
+ });
351
+ await server.close();
352
+ return 0;
353
+ }
354
+ if (command === "run")
355
+ return commandRun(flags);
356
+ const specFlag = str(flags, "spec");
357
+ const configPath = str(flags, "config");
358
+ const specSource = specFlag ?? (configPath === undefined ? undefined : (await loadConfig(configPath)).spec);
359
+ if (specSource === undefined) {
360
+ process.stderr.write("oat: --spec (or --config) is required\n\n" + USAGE);
361
+ return 2;
362
+ }
363
+ const raw = await loadSpec(specSource, str(flags, "base-url"));
364
+ const { doc, externalRefs } = dereference(raw);
365
+ const model = buildModel(doc);
366
+ switch (command) {
367
+ case "plan":
368
+ process.stdout.write(report.plan(model, flags.json === true));
369
+ return 0;
370
+ case "doctor": {
371
+ const output = report.doctor(model, externalRefs, flags.json === true);
372
+ process.stdout.write(output.text);
373
+ return output.blocking > 0 ? 1 : 0;
374
+ }
375
+ default:
376
+ process.stderr.write(`oat: unknown command "${command}"\n\n${USAGE}`);
377
+ return 2;
378
+ }
379
+ }
380
+ main().then((code) => {
381
+ process.exitCode = code;
382
+ }, (error) => {
383
+ process.stderr.write(`oat: ${error instanceof Error ? error.message : String(error)}\n`);
384
+ process.exitCode = 1;
385
+ });
386
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1,165 @@
1
+ /**
2
+ * The public configuration surface — and the single source of truth for its types.
3
+ *
4
+ * The runtime imports these definitions rather than declaring its own, so a mistake in a config
5
+ * is a compile error rather than a surprise at request time. Everything backend-specific lives
6
+ * here or in `x-*` spec tags; oat itself knows nothing about any particular API.
7
+ */
8
+ interface StepBase {
9
+ /**
10
+ * Binds parts of the response into the flow's scope. Later steps interpolate them as
11
+ * `{name}`, and `rootsFromFlow` can promote them to path parameters. Values are addressed by
12
+ * path: `$.access_token`, `$.orgs.0.id`.
13
+ */
14
+ saveAs?: Record<string, string>;
15
+ /**
16
+ * Binds values out of a JWT's claims. Tenancy commonly rides in the token rather than the
17
+ * response body, and a principal that can name its own tenant needs no roots configured. The
18
+ * signature is not verified — oat is reading its own credential, not trusting a third party.
19
+ */
20
+ saveClaimsFrom?: {
21
+ token: string;
22
+ bind: Record<string, string>;
23
+ };
24
+ /** Literal values bound before the step runs, with `{name}` interpolation. */
25
+ bind?: Record<string, string>;
26
+ /** Accepted statuses. Defaults to any 2xx. */
27
+ expect?: number[];
28
+ }
29
+ /** Calls an operation named in the document. Prefer this — it survives the path moving. */
30
+ export interface OperationStep extends StepBase {
31
+ operationId: string;
32
+ body?: unknown;
33
+ headers?: Record<string, string>;
34
+ query?: Record<string, string>;
35
+ }
36
+ /** Calls a raw path, for endpoints the document does not describe. */
37
+ export interface RequestStep extends StepBase {
38
+ method: string;
39
+ path: string;
40
+ body?: unknown;
41
+ headers?: Record<string, string>;
42
+ query?: Record<string, string>;
43
+ }
44
+ /**
45
+ * A value that never travels over HTTP — an emailed verification token, an OTP, a magic link.
46
+ *
47
+ * oat declares the need and calls `hooks.resolveOutOfBand` to satisfy it, polling with backoff
48
+ * because such stores are usually eventually consistent. That hook is the entire coupling
49
+ * surface between oat and one specific backend.
50
+ */
51
+ export interface OutOfBandStep {
52
+ outOfBand: {
53
+ address: string;
54
+ kind: string;
55
+ as: string;
56
+ };
57
+ }
58
+ export type AuthStep = OperationStep | RequestStep | OutOfBandStep;
59
+ export interface AuthFlow {
60
+ /** Run in order; values flow forward through the scope. */
61
+ steps: [AuthStep, ...AuthStep[]];
62
+ /** Where the credential is in the final response, e.g. `$.access_token`. */
63
+ credentialFrom: string;
64
+ /** Lifetime in seconds, e.g. `$.expires_in`. Falls back to a JWT `exp`, then `assumeTtlMs`. */
65
+ expiresInFrom?: string;
66
+ /** Header the credential is sent in. Defaults to `authorization`. */
67
+ header?: string;
68
+ /** Defaults to `Bearer {credential}`. */
69
+ template?: string;
70
+ /** Used only when neither the response nor the credential reveals an expiry. */
71
+ assumeTtlMs?: number;
72
+ }
73
+ export interface Principal {
74
+ id: string;
75
+ /** Sent on every request this principal makes — for a long-lived key, this is all you need. */
76
+ headers?: Record<string, string>;
77
+ /** How to obtain a credential. Omit for a principal that authenticates by static header. */
78
+ auth?: AuthFlow;
79
+ /** Path parameters this principal owns — its tenant. */
80
+ roots?: Record<string, string>;
81
+ /**
82
+ * Path parameters the flow discovers rather than the config supplying them, mapping a
83
+ * parameter to the scope key the flow bound. A registration that provisions a tenant then
84
+ * needs no fixture identifiers configured at all.
85
+ */
86
+ rootsFromFlow?: Record<string, string>;
87
+ /**
88
+ * Name of this principal's role (`owner`, `admin`, `member`, `viewer`, …). Free-form.
89
+ * Isolation still keys off `roots`, not this name: two `owner`s in different tenants are
90
+ * peers; an `owner` and a `viewer` sharing roots are a lattice.
91
+ */
92
+ role?: string;
93
+ /**
94
+ * Position in the authorization lattice. Higher can do everything a lower rank can.
95
+ * Same rank + different `roots` is today's two-tenant isolation pair. Defaults to 0.
96
+ */
97
+ rank?: number;
98
+ /**
99
+ * How the owner names this principal when inviting them — an API key, an email, whatever
100
+ * the invite operation's body field accepts. Required for `auth.invite-grants-then-revokes`.
101
+ */
102
+ inviteAs?: string;
103
+ }
104
+ export interface OutOfBandRequest {
105
+ address: string;
106
+ kind: string;
107
+ /** 1-based; oat retries with backoff until a value arrives or the attempts run out. */
108
+ attempt: number;
109
+ }
110
+ export interface Hooks {
111
+ /** Return `null` to ask oat to retry — the value may simply not have arrived yet. */
112
+ resolveOutOfBand?: (request: OutOfBandRequest) => Promise<string | null>;
113
+ /**
114
+ * Removes a principal the run provisioned, and everything that principal created with it.
115
+ * Usually the only handle that works: per-record deletes are typically owner-scoped, and the
116
+ * credential dies with the run.
117
+ */
118
+ teardownPrincipal?: (address: string) => Promise<void>;
119
+ }
120
+ export interface OatConfig {
121
+ /** OpenAPI document — an http(s) URL or a filesystem path. JSON or YAML. */
122
+ spec: string;
123
+ /** Backend under test. */
124
+ baseUrl: string;
125
+ /**
126
+ * At least one principal. Isolation needs a second with different `roots`. A lattice
127
+ * needs several that share `roots` and differ in `rank`. Extra principals are not ignored.
128
+ */
129
+ principals: [Principal, ...Principal[]];
130
+ hooks?: Hooks;
131
+ /** Sent on every request. Opaque to oat — bypass headers, API versioning, tracing. */
132
+ globalHeaders?: Record<string, string>;
133
+ /** Path parameters oat cannot create. Also declarable in-spec via `x-root`. */
134
+ roots?: Record<string, string>;
135
+ /** Fixture generation derives from this, so a failing run is exactly reproducible. */
136
+ seed?: number;
137
+ /** Instances seeded per entity. Larger cohorts discriminate more, and cost more. */
138
+ cohortSize?: number;
139
+ /** Entities tested in parallel. Checks within one entity always stay ordered. */
140
+ concurrency?: number;
141
+ /** Requests allowed in flight at once. Past a server's comfort this makes runs slower. */
142
+ maxInFlight?: number;
143
+ /** Restrict the run to these entity names. */
144
+ only?: string[];
145
+ /** Leave created records in place instead of tearing them down. */
146
+ keepFixtures?: boolean;
147
+ /** Report destination. Defaults to `./oat-out`. */
148
+ outDir?: string;
149
+ }
150
+ /**
151
+ * Identity function that gives a config file full type checking and editor completion.
152
+ *
153
+ * ```ts
154
+ * import { defineConfig } from "@lovrozagar/oat"
155
+ *
156
+ * export default defineConfig({
157
+ * spec: "https://api.example.com/openapi.json",
158
+ * baseUrl: "https://api.example.com",
159
+ * principals: [{ id: "alpha", headers: { authorization: `Bearer ${process.env.TOKEN}` } }],
160
+ * })
161
+ * ```
162
+ */
163
+ export declare function defineConfig(config: OatConfig): OatConfig;
164
+ export {};
165
+ //# sourceMappingURL=define-config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"define-config.d.ts","sourceRoot":"","sources":["../../src/config/define-config.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,24 @@
1
+ /**
2
+ * The public configuration surface — and the single source of truth for its types.
3
+ *
4
+ * The runtime imports these definitions rather than declaring its own, so a mistake in a config
5
+ * is a compile error rather than a surprise at request time. Everything backend-specific lives
6
+ * here or in `x-*` spec tags; oat itself knows nothing about any particular API.
7
+ */
8
+ /**
9
+ * Identity function that gives a config file full type checking and editor completion.
10
+ *
11
+ * ```ts
12
+ * import { defineConfig } from "@lovrozagar/oat"
13
+ *
14
+ * export default defineConfig({
15
+ * spec: "https://api.example.com/openapi.json",
16
+ * baseUrl: "https://api.example.com",
17
+ * principals: [{ id: "alpha", headers: { authorization: `Bearer ${process.env.TOKEN}` } }],
18
+ * })
19
+ * ```
20
+ */
21
+ export function defineConfig(config) {
22
+ return config;
23
+ }
24
+ //# sourceMappingURL=define-config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"define-config.js","sourceRoot":"","sources":["../../src/config/define-config.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AA2JH;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,YAAY,CAAC,MAAiB;IAC7C,OAAO,MAAM,CAAA;AACd,CAAC"}
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Config loading. A config is a `.js`/`.mjs`/`.ts` module with a default export, or a `.json`
3
+ * file. TypeScript configs load only where the runtime can strip types (Node 22.6+ with
4
+ * `--experimental-strip-types`, or Node 23+); the error says so rather than failing obscurely.
5
+ */
6
+ import type { OatConfig } from "./define-config.ts";
7
+ export declare function loadConfig(path: string): Promise<OatConfig>;
8
+ /** Env-var interpolation so configs can carry `${API_TOKEN}` without embedding secrets. */
9
+ export declare function interpolate<T>(value: T, env?: NodeJS.ProcessEnv): T;
10
+ //# sourceMappingURL=load.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"load.d.ts","sourceRoot":"","sources":["../../src/config/load.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAKH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAA;AAEnD,wBAAsB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,CAAC,CA0BjE;AAED,2FAA2F;AAC3F,wBAAgB,WAAW,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,CAAC,CAmBhF"}
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Config loading. A config is a `.js`/`.mjs`/`.ts` module with a default export, or a `.json`
3
+ * file. TypeScript configs load only where the runtime can strip types (Node 22.6+ with
4
+ * `--experimental-strip-types`, or Node 23+); the error says so rather than failing obscurely.
5
+ */
6
+ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
7
+ if (typeof path === "string" && /^\.\.?\//.test(path)) {
8
+ return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
9
+ return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
10
+ });
11
+ }
12
+ return path;
13
+ };
14
+ import { readFile } from "node:fs/promises";
15
+ import { isAbsolute, resolve } from "node:path";
16
+ import { pathToFileURL } from "node:url";
17
+ export async function loadConfig(path) {
18
+ const absolute = isAbsolute(path) ? path : resolve(process.cwd(), path);
19
+ if (absolute.endsWith(".json")) {
20
+ return JSON.parse(await readFile(absolute, "utf8"));
21
+ }
22
+ try {
23
+ const module = (await import(__rewriteRelativeImportExtension(pathToFileURL(absolute).href)));
24
+ const config = module.default ?? module;
25
+ if (typeof config !== "object" || config === null) {
26
+ throw new Error("config module has no default export");
27
+ }
28
+ return config;
29
+ }
30
+ catch (error) {
31
+ const message = error instanceof Error ? error.message : String(error);
32
+ if (absolute.endsWith(".ts") && /Unknown file extension|strip-types/.test(message)) {
33
+ throw new Error(`oat: cannot load ${path} — this Node build will not import TypeScript directly. ` +
34
+ "Run with `node --experimental-strip-types`, or use a .js/.mjs config.");
35
+ }
36
+ throw new Error(`oat: failed to load config ${path}: ${message}`);
37
+ }
38
+ }
39
+ /** Env-var interpolation so configs can carry `${API_TOKEN}` without embedding secrets. */
40
+ export function interpolate(value, env = process.env) {
41
+ if (typeof value === "string") {
42
+ return value.replace(/\$\{([A-Z0-9_]+)\}/gi, (match, name) => {
43
+ const resolved = env[name];
44
+ if (resolved === undefined) {
45
+ throw new Error(`oat: config references \${${name}}, which is not set in the environment`);
46
+ }
47
+ return resolved;
48
+ });
49
+ }
50
+ if (Array.isArray(value))
51
+ return value.map((item) => interpolate(item, env));
52
+ if (value !== null && typeof value === "object") {
53
+ const out = {};
54
+ for (const [key, item] of Object.entries(value)) {
55
+ out[key] = interpolate(item, env);
56
+ }
57
+ return out;
58
+ }
59
+ return value;
60
+ }
61
+ //# sourceMappingURL=load.js.map