@lovrozagar/oat 0.2.2 → 0.3.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/README.md +15 -1
- package/dist/cli.js +13 -1
- package/dist/runtime/checks.d.ts.map +1 -1
- package/dist/runtime/checks.js +38 -21
- package/dist/runtime/checks.js.map +1 -1
- package/dist/runtime/fixture.d.ts +28 -1
- package/dist/runtime/fixture.d.ts.map +1 -1
- package/dist/runtime/fixture.js +551 -90
- package/dist/runtime/fixture.js.map +1 -1
- package/dist/runtime/run.d.ts.map +1 -1
- package/dist/runtime/run.js +106 -34
- package/dist/runtime/run.js.map +1 -1
- package/dist/runtime/world.d.ts +5 -0
- package/dist/runtime/world.d.ts.map +1 -1
- package/dist/runtime/world.js +43 -3
- package/dist/runtime/world.js.map +1 -1
- package/dist/spec/graph.d.ts.map +1 -1
- package/dist/spec/graph.js +5 -1
- package/dist/spec/graph.js.map +1 -1
- package/dist/spec/load.d.ts.map +1 -1
- package/dist/spec/load.js +19 -4
- package/dist/spec/load.js.map +1 -1
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -736,6 +736,14 @@ Default cohort is **7** records, one of each variant, sliced by `cohortSize`:
|
|
|
736
736
|
|
|
737
737
|
Numbers use the ladder `1, 2, 5, 10, 20, 50, 100` so **lexical order ≠ numeric order** (otherwise a TEXT compare looks correct). Enums walk `index % enum.length`. `readOnly` / `x-generated` fields are omitted. Required fields that cannot be generated get a type fallback (`0`, `false`, `[]`, `{}`, `"value"`). Arrays honour `minItems` (never send `[]` when `minItems ≥ 1`). Nested objects stop at depth 4.
|
|
738
738
|
|
|
739
|
+
Empty schemas (`{}`, `true`, `additionalProperties: {}`) and cyclic `$ref`s after inlining stop the walk — they become a scalar or `{}`, never another object descent. A `RangeError` during generation is a `COVERAGE_GAP` on that entity naming the operationId and JSON pointer (`fixture generation overflow on table.create (/)`), not `blocked by unknown`.
|
|
740
|
+
|
|
741
|
+
String fields honour `format` and `pattern`: `email` → `oat-{variant}-{index}@example.test`, `uri` / `url` → `https://example.test/...`, `uuid` → a fixed-shape UUID, `pattern` → a string that matches (or the field is omitted / the entity is a gap). `"Quarterly Report N"` is only used when the document does not constrain the string.
|
|
742
|
+
|
|
743
|
+
An operation with `x-invite` is not `entity.create`. oat does not POST a generated invitee. The invite check sends `granteeField` = the peer's `inviteAs`. Missing `inviteAs` is a coverage gap naming the tag.
|
|
744
|
+
|
|
745
|
+
A create whose `operationId` appears in any principal `auth.steps`, or that declares `x-fresh-principal`, is not seeded. Those rows were provisioned by the auth flow.
|
|
746
|
+
|
|
739
747
|
Parent path parameters are created first (depth-first through the owning entity's create). Config / principal `roots` fill parameters oat cannot create.
|
|
740
748
|
|
|
741
749
|
A create that returns `>= 300` on the first variant fails the entity (downstream checks `BLOCKED`). Later variants that fail just shorten the cohort — a partial cohort is still used.
|
|
@@ -892,6 +900,8 @@ x-invite:
|
|
|
892
900
|
|
|
893
901
|
Put this on the invite operation. Config must give the invitee `inviteAs`. Defaults if omitted: `granteeField: key`, `tokenPointer: $.token`, `grantPointer: $.grant_id`. All three of `invite` / `accept` / `revoke` (operationIds) are required or the tag is ignored.
|
|
894
902
|
|
|
903
|
+
An invite operation is **not** the entity's fixture create, even when it is `POST` on the collection. oat will not seed it with a generated email. The invite check (and only that check) creates the grant, using `inviteAs` as `granteeField`. The check still runs when there is no non-invite create, as long as an item or list route exists.
|
|
904
|
+
|
|
895
905
|
Timeline asserted: cannot read → invite → still cannot → accept → can → revoke → cannot.
|
|
896
906
|
|
|
897
907
|
**Fallback:** the check does not run.
|
|
@@ -1071,7 +1081,11 @@ export default defineConfig({
|
|
|
1071
1081
|
})
|
|
1072
1082
|
```
|
|
1073
1083
|
|
|
1074
|
-
An excluded operation never silently narrows what gets reported. Excluding an entity's `create` degrades that entity the same way a real seeding failure does — a `COVERAGE_GAP` naming the reason, then read-only checks run against whatever the list route already returns; `BLOCKED` if nothing exists to fall back on. Excluding `read`/`update`/`delete` individually stands down just the checks that need that one operation.
|
|
1084
|
+
An excluded operation never silently narrows what gets reported. Excluding an entity's `create` degrades that entity the same way a real seeding failure does — a `COVERAGE_GAP` naming the reason, then read-only checks run against whatever the list route already returns; `BLOCKED` if nothing exists to fall back on. Excluding `read`/`update`/`delete` individually stands down just the checks that need that one operation.
|
|
1085
|
+
|
|
1086
|
+
Exclusion applies to every invocation, not only create/read/update/delete. `effects.declared-effect-occurs` and `async.reaches-terminal-state` drop ops the profile forbids and record `profile.skip` — they do not POST `extract.once` under `--profile cheap`. A check whose only targets were excluded did not apply / names the profile; it never looks like the backend returned 500.
|
|
1087
|
+
|
|
1088
|
+
The run summary states what a profile skipped: `skipped 12 operation(s) under --profile cheap (12 high-cost)` — the same principle as `did not apply` for a coverage gap: a report that only shows what ran invites the reader to assume the rest was verified.
|
|
1075
1089
|
|
|
1076
1090
|
### `x-rate-limit` — pacing oat's own traffic
|
|
1077
1091
|
|
package/dist/cli.js
CHANGED
|
@@ -200,7 +200,7 @@ async function main() {
|
|
|
200
200
|
return 0;
|
|
201
201
|
}
|
|
202
202
|
if (command === "conformance") {
|
|
203
|
-
const { postgresAvailable, renderParserSuite, renderSuite, runExampleSpecSuite, runTagUnlockSuite, runParserSuite, runCoverageReportSuite, runTenantScopeSuite, runSuite, sqliteAvailable, d1Available, } = await import("./conformance/suite.js");
|
|
203
|
+
const { postgresAvailable, renderParserSuite, renderSuite, runExampleSpecSuite, runTagUnlockSuite, runParserSuite, runCoverageReportSuite, runSeedContractSuite, runTenantScopeSuite, runSuite, sqliteAvailable, d1Available, } = await import("./conformance/suite.js");
|
|
204
204
|
const { runFeatureGateSuite } = await import("./conformance/feature-gate.js");
|
|
205
205
|
const parser = renderParserSuite(runParserSuite());
|
|
206
206
|
process.stdout.write(parser.text);
|
|
@@ -222,6 +222,9 @@ async function main() {
|
|
|
222
222
|
const featureGate = renderParserSuite(await runFeatureGateSuite());
|
|
223
223
|
process.stdout.write(featureGate.text);
|
|
224
224
|
parser.failures += featureGate.failures;
|
|
225
|
+
const seedContract = renderParserSuite(await runSeedContractSuite());
|
|
226
|
+
process.stdout.write(seedContract.text);
|
|
227
|
+
parser.failures += seedContract.failures;
|
|
225
228
|
if (flags.parser === true)
|
|
226
229
|
return parser.failures > 0 ? 1 : 0;
|
|
227
230
|
if (flags.precision !== undefined) {
|
|
@@ -372,6 +375,15 @@ async function main() {
|
|
|
372
375
|
const raw = await loadSpec(specSource, str(flags, "base-url"));
|
|
373
376
|
const { doc, externalRefs } = dereference(raw);
|
|
374
377
|
const model = buildModel(doc);
|
|
378
|
+
try {
|
|
379
|
+
const { probeCreateFixtures } = await import("./runtime/world.js");
|
|
380
|
+
probeCreateFixtures(model);
|
|
381
|
+
}
|
|
382
|
+
catch (error) {
|
|
383
|
+
const { isOverflowError } = await import("./runtime/fixture.js");
|
|
384
|
+
if (!isOverflowError(error))
|
|
385
|
+
throw error;
|
|
386
|
+
}
|
|
375
387
|
switch (command) {
|
|
376
388
|
case "plan":
|
|
377
389
|
process.stdout.write(report.plan(model, flags.json === true));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"checks.d.ts","sourceRoot":"","sources":["../../src/runtime/checks.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAA;AACvD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAA;AAC5D,OAAO,KAAK,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAA;AACjE,OAAO,KAAK,EAAE,MAAM,EAAY,MAAM,aAAa,CAAA;AAEnD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAA;AAGpD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAA;AACpD,OAAO,EAAE,KAAK,OAAO,EAAY,MAAM,YAAY,CAAA;AAEnD,kGAAkG;AAClG,MAAM,WAAW,KAAK;IACrB,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,GAAG,SAAS,CAAA;IACxB,oEAAoE;IACpE,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IACrC,6EAA6E;IAC7E,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC7B,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC7B,iEAAiE;IACjE,QAAQ,EAAE,MAAM,GAAG,SAAS,CAAA;CAC5B;AAED,MAAM,WAAW,YAAY;IAC5B,UAAU,EAAE,MAAM,CAAA;IAClB,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,EAAE,SAAS,CAAA;IAChB,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,EAAE,gBAAgB,CAAA;IAC1B,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC7B,MAAM,EAAE,cAAc,CAAA;IACtB,MAAM,EAAE,cAAc,GAAG,SAAS,CAAA;IAClC,QAAQ,EAAE,cAAc,GAAG,SAAS,CAAA;IACpC,QAAQ,EAAE,cAAc,GAAG,SAAS,CAAA;IACpC,QAAQ,EAAE,cAAc,GAAG,SAAS,CAAA;IACpC,aAAa,EAAE,MAAM,GAAG,IAAI,CAAA;IAC5B,OAAO,EAAE,OAAO,EAAE,CAAA;IAClB,KAAK,EAAE,eAAe,GAAG,IAAI,CAAA;IAC7B,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,MAAM,EAAE,UAAU,GAAG,IAAI,CAAA;IACzB,IAAI,EAAE,MAAM,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAClC;;;OAGG;IACH,MAAM,EAAE,KAAK,EAAE,CAAA;IACf,6FAA6F;IAC7F,OAAO,EAAE,CAAC,MAAM,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,GAAG,SAAS,CAAA;IACnD,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS,CAAA;IAC5C,SAAS,EAAE,eAAe,GAAG,SAAS,CAAA;IACtC,IAAI,EAAE,MAAM,CAAA;IACZ,8DAA8D;IAC9D,QAAQ,EAAE,cAAc,EAAE,CAAA;IAC1B,0DAA0D;IAC1D,SAAS,EAAE,cAAc,EAAE,CAAA;CAC3B;AAED,MAAM,WAAW,KAAK;IACrB,EAAE,EAAE,MAAM,CAAA;IACV,8EAA8E;IAC9E,UAAU,EAAE,CAAC,GAAG,EAAE,YAAY,KAAK,OAAO,CAAA;IAC1C;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,EAAE,CAAA;IACpB;;;;;;OAMG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;IACd;;;;;OAKG;IACH,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,GAAG,EAAE,CAAC,GAAG,EAAE,YAAY,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;CACzC;
|
|
1
|
+
{"version":3,"file":"checks.d.ts","sourceRoot":"","sources":["../../src/runtime/checks.ts"],"names":[],"mappings":"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"}
|
package/dist/runtime/checks.js
CHANGED
|
@@ -2543,25 +2543,20 @@ function pointerValue(body, pointer) {
|
|
|
2543
2543
|
* at the same check — the timeline is the property.
|
|
2544
2544
|
*/
|
|
2545
2545
|
const inviteGrantsThenRevokes = {
|
|
2546
|
-
applicable: (ctx) => ctx.invite !== null &&
|
|
2547
|
-
ctx.readOp !== undefined &&
|
|
2548
|
-
ctx.records.length > 0 &&
|
|
2549
|
-
ctx.actors.some((actor) => actor.inviteAs !== undefined &&
|
|
2550
|
-
ctx.actors[0] !== undefined &&
|
|
2551
|
-
!sameTenantScope(actor.roots, ctx.actors[0].roots)),
|
|
2546
|
+
applicable: (ctx) => ctx.invite !== null && (ctx.readOp !== undefined || ctx.listOp !== undefined),
|
|
2552
2547
|
dependsOn: ["list.read-after-write", "tenant.item-not-readable-cross-tenant"],
|
|
2553
2548
|
id: "auth.invite-grants-then-revokes",
|
|
2554
|
-
needs: "x-invite naming invite/accept/revoke, a peer principal with inviteAs, and an item route",
|
|
2549
|
+
needs: "x-invite naming invite/accept/revoke, a peer principal with inviteAs, and an item or list route",
|
|
2555
2550
|
async run(ctx) {
|
|
2556
2551
|
const spec = ctx.invite;
|
|
2557
2552
|
const owner = ctx.actors[0];
|
|
2558
|
-
|
|
2559
|
-
if (spec === null || owner === undefined || target === undefined || ctx.readOp === undefined) {
|
|
2553
|
+
if (spec === null || owner === undefined) {
|
|
2560
2554
|
return;
|
|
2561
2555
|
}
|
|
2562
|
-
const delegate = ctx.actors.find((actor) => actor.inviteAs !== undefined && !sameTenantScope(actor.roots, owner.roots));
|
|
2556
|
+
const delegate = ctx.actors.find((actor) => actor !== owner && actor.inviteAs !== undefined && !sameTenantScope(actor.roots, owner.roots)) ?? ctx.actors.find((actor) => actor !== owner && actor.inviteAs !== undefined);
|
|
2563
2557
|
if (delegate === undefined || delegate.inviteAs === undefined) {
|
|
2564
|
-
return ctx.findings.
|
|
2558
|
+
return ctx.findings.gap(this.id, ctx.entityName, "x-invite requires a peer principal with inviteAs", "x-invite names the grant flow, but no peer principal declares inviteAs, so oat " +
|
|
2559
|
+
"cannot fill the grantee field without inventing an email");
|
|
2565
2560
|
}
|
|
2566
2561
|
const inviteOp = ctx.model.byOperationId.get(spec.invite);
|
|
2567
2562
|
const acceptOp = ctx.model.byOperationId.get(spec.accept);
|
|
@@ -2569,16 +2564,23 @@ const inviteGrantsThenRevokes = {
|
|
|
2569
2564
|
if (inviteOp === undefined || acceptOp === undefined || revokeOp === undefined) {
|
|
2570
2565
|
return ctx.findings.unresolved(this.id, ctx.entityName, "x-invite names an operation that is not in the document");
|
|
2571
2566
|
}
|
|
2572
|
-
const
|
|
2573
|
-
const
|
|
2574
|
-
const
|
|
2567
|
+
const target = ctx.records[0];
|
|
2568
|
+
const id = target === undefined ? undefined : String(target[ctx.identity]);
|
|
2569
|
+
const resource = {
|
|
2570
|
+
...ctx.scope,
|
|
2571
|
+
...(id === undefined ? {} : itemParamFor(ctx, id)),
|
|
2572
|
+
};
|
|
2573
|
+
const itemPath = ctx.readOp?.path;
|
|
2575
2574
|
const canRead = async () => {
|
|
2575
|
+
if (itemPath === undefined || id === undefined)
|
|
2576
|
+
return null;
|
|
2576
2577
|
const exchange = await ctx.client.get(fillPath(itemPath, { ...delegate.roots, ...resource }), {
|
|
2577
2578
|
headers: delegate.headers(),
|
|
2578
2579
|
});
|
|
2579
2580
|
return exchange.status < 300;
|
|
2580
2581
|
};
|
|
2581
|
-
|
|
2582
|
+
const alreadyReadable = await canRead();
|
|
2583
|
+
if (alreadyReadable === true) {
|
|
2582
2584
|
return ctx.findings.unresolved(this.id, ctx.entityName, "the delegate could already read the record, so invite cannot be distinguished from a leak");
|
|
2583
2585
|
}
|
|
2584
2586
|
const invited = await ctx.client.request(inviteOp.method, fillPath(inviteOp.path, resource), {
|
|
@@ -2590,7 +2592,7 @@ const inviteGrantsThenRevokes = {
|
|
|
2590
2592
|
if (invited.status >= 300) {
|
|
2591
2593
|
return ctx.findings.unresolved(this.id, ctx.entityName, `invite returned ${invited.status}, so accept/revoke were never exercised`);
|
|
2592
2594
|
}
|
|
2593
|
-
if (await canRead()) {
|
|
2595
|
+
if ((await canRead()) === true) {
|
|
2594
2596
|
ctx.findings.backend(this.id, ctx.entityName, "an invite grants access before it is accepted", `inviting ${delegate.id} made ${ctx.entityName} ${id} readable immediately. The accept ` +
|
|
2595
2597
|
"step is then theatre — anyone who can be named in an invite body is already in.", [invited]);
|
|
2596
2598
|
return;
|
|
@@ -2608,7 +2610,7 @@ const inviteGrantsThenRevokes = {
|
|
|
2608
2610
|
if (accepted.status >= 300) {
|
|
2609
2611
|
return ctx.findings.unresolved(this.id, ctx.entityName, `accept returned ${accepted.status}`);
|
|
2610
2612
|
}
|
|
2611
|
-
if (
|
|
2613
|
+
if ((await canRead()) === false) {
|
|
2612
2614
|
ctx.findings.backend(this.id, ctx.entityName, "accepting an invite does not grant access", `${delegate.id} accepted an invite to ${ctx.entityName} ${id} and still cannot read it. ` +
|
|
2613
2615
|
"The invite flow completed; the grant did not.", [invited, accepted]);
|
|
2614
2616
|
return;
|
|
@@ -2622,7 +2624,7 @@ const inviteGrantsThenRevokes = {
|
|
|
2622
2624
|
if (revoked.status >= 300) {
|
|
2623
2625
|
return ctx.findings.unresolved(this.id, ctx.entityName, `revoke returned ${revoked.status}`);
|
|
2624
2626
|
}
|
|
2625
|
-
if (await canRead()) {
|
|
2627
|
+
if ((await canRead()) === true) {
|
|
2626
2628
|
ctx.findings.backend(this.id, ctx.entityName, "revoking an invite does not remove access", `${delegate.id} can still read ${ctx.entityName} ${id} after the grant was revoked. ` +
|
|
2627
2629
|
"A share that cannot be taken back is a standing leak.", [invited, accepted, revoked]);
|
|
2628
2630
|
}
|
|
@@ -2983,9 +2985,24 @@ const successSchemaHonoured = {
|
|
|
2983
2985
|
};
|
|
2984
2986
|
/** Builds a body that should be accepted, for use as the base of a negative probe. */
|
|
2985
2987
|
function validBody(ctx, schema) {
|
|
2986
|
-
const [member] = buildCohort(schema, ctx.seed, ["baseline"]);
|
|
2988
|
+
const [member] = buildCohort(schema, ctx.seed, ["baseline"], ctx.createOp?.operationId ?? ctx.entityName);
|
|
2987
2989
|
return member?.body ?? {};
|
|
2988
2990
|
}
|
|
2991
|
+
/** Invite bodies must carry the peer's `inviteAs`, never a generated email. */
|
|
2992
|
+
function bodyForOp(ctx, op) {
|
|
2993
|
+
const body = validBody(ctx, requestSchemaOf(ctx, op) ?? {});
|
|
2994
|
+
const spec = op.invite ?? ctx.invite;
|
|
2995
|
+
if (spec === null || spec === undefined)
|
|
2996
|
+
return body;
|
|
2997
|
+
const owner = ctx.actors[0];
|
|
2998
|
+
const delegate = ctx.actors.find((actor) => actor !== owner &&
|
|
2999
|
+
actor.inviteAs !== undefined &&
|
|
3000
|
+
owner !== undefined &&
|
|
3001
|
+
!sameTenantScope(actor.roots, owner.roots)) ?? ctx.actors.find((actor) => actor !== owner && actor.inviteAs !== undefined);
|
|
3002
|
+
if (delegate?.inviteAs !== undefined)
|
|
3003
|
+
body[spec.granteeField] = delegate.inviteAs;
|
|
3004
|
+
return body;
|
|
3005
|
+
}
|
|
2989
3006
|
function requestSchemaOf(ctx, op) {
|
|
2990
3007
|
const raw = ctx.model.rawOperations.get(op.operationId);
|
|
2991
3008
|
const content = raw?.requestBody?.content;
|
|
@@ -3245,7 +3262,7 @@ const declaredEffectsOccur = {
|
|
|
3245
3262
|
const before = await observe(ctx, listOp);
|
|
3246
3263
|
if (before === null)
|
|
3247
3264
|
continue;
|
|
3248
|
-
const body = op.hasRequestBody ?
|
|
3265
|
+
const body = op.hasRequestBody ? bodyForOp(ctx, op) : undefined;
|
|
3249
3266
|
const invoked = await ctx.client.request(op.method, fillPath(op.path, ctx.scope), {
|
|
3250
3267
|
headers: ctx.auth(),
|
|
3251
3268
|
...(body === undefined ? {} : { body }),
|
|
@@ -3317,7 +3334,7 @@ const asyncReachesTerminalState = {
|
|
|
3317
3334
|
const spec = op.async;
|
|
3318
3335
|
if (spec === null)
|
|
3319
3336
|
continue;
|
|
3320
|
-
const body = op.hasRequestBody ?
|
|
3337
|
+
const body = op.hasRequestBody ? bodyForOp(ctx, op) : undefined;
|
|
3321
3338
|
const start = await ctx.client.request(op.method, fillPath(op.path, ctx.scope), {
|
|
3322
3339
|
headers: ctx.auth(),
|
|
3323
3340
|
...(body === undefined ? {} : { body }),
|