@lovrozagar/oat 0.6.2 → 0.7.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 +141 -20
- package/dist/cli.js +3 -0
- package/dist/config/define-config.d.ts +95 -1
- package/dist/config/define-config.d.ts.map +1 -1
- package/dist/config/define-config.js +6 -0
- package/dist/config/define-config.js.map +1 -1
- package/dist/index.d.ts +5 -2
- package/dist/index.js +3 -1
- package/dist/report/console.d.ts.map +1 -1
- package/dist/report/console.js +2 -0
- package/dist/report/console.js.map +1 -1
- package/dist/report/render.d.ts.map +1 -1
- package/dist/report/render.js +4 -2
- package/dist/report/render.js.map +1 -1
- package/dist/runtime/auth.d.ts +22 -2
- package/dist/runtime/auth.d.ts.map +1 -1
- package/dist/runtime/auth.js +129 -16
- package/dist/runtime/auth.js.map +1 -1
- package/dist/runtime/body.d.ts.map +1 -1
- package/dist/runtime/body.js +6 -3
- package/dist/runtime/body.js.map +1 -1
- package/dist/runtime/checks.d.ts +6 -0
- package/dist/runtime/checks.d.ts.map +1 -1
- package/dist/runtime/checks.js +80 -3
- package/dist/runtime/checks.js.map +1 -1
- package/dist/runtime/client.d.ts +6 -0
- package/dist/runtime/client.d.ts.map +1 -1
- package/dist/runtime/client.js +14 -1
- package/dist/runtime/client.js.map +1 -1
- package/dist/runtime/finding.d.ts +2 -0
- package/dist/runtime/finding.d.ts.map +1 -1
- package/dist/runtime/finding.js.map +1 -1
- package/dist/runtime/input.d.ts +7 -0
- package/dist/runtime/input.d.ts.map +1 -0
- package/dist/runtime/input.js +46 -0
- package/dist/runtime/input.js.map +1 -0
- package/dist/runtime/poll.d.ts +29 -0
- package/dist/runtime/poll.d.ts.map +1 -0
- package/dist/runtime/poll.js +59 -0
- package/dist/runtime/poll.js.map +1 -0
- package/dist/runtime/principals.d.ts +29 -0
- package/dist/runtime/principals.d.ts.map +1 -0
- package/dist/runtime/principals.js +97 -0
- package/dist/runtime/principals.js.map +1 -0
- package/dist/runtime/run.d.ts +10 -1
- package/dist/runtime/run.d.ts.map +1 -1
- package/dist/runtime/run.js +96 -5
- package/dist/runtime/run.js.map +1 -1
- package/dist/runtime/upload.d.ts +1 -0
- package/dist/runtime/upload.d.ts.map +1 -1
- package/dist/runtime/upload.js.map +1 -1
- package/dist/runtime/wait.d.ts +32 -0
- package/dist/runtime/wait.d.ts.map +1 -0
- package/dist/runtime/wait.js +125 -0
- package/dist/runtime/wait.js.map +1 -0
- package/dist/runtime/world.d.ts.map +1 -1
- package/dist/runtime/world.js +2 -0
- package/dist/runtime/world.js.map +1 -1
- package/dist/spec/extensions.d.ts +16 -0
- package/dist/spec/extensions.d.ts.map +1 -1
- package/dist/spec/extensions.js +24 -1
- package/dist/spec/extensions.js.map +1 -1
- package/dist/spec/graph.d.ts +2 -1
- package/dist/spec/graph.d.ts.map +1 -1
- package/dist/spec/graph.js +2 -1
- package/dist/spec/graph.js.map +1 -1
- package/package.json +4 -2
package/dist/runtime/checks.js
CHANGED
|
@@ -12,7 +12,9 @@ import { encodeForOperation } from "./body.js";
|
|
|
12
12
|
import { STRING_PAYLOADS, payloadFits } from "./payloads.js";
|
|
13
13
|
import { describeFeatureGate, isDocumentedFeatureGateDenial, reportFeatureGateSchemaDrift } from "./feature-gate.js";
|
|
14
14
|
import { driveAsync, inspectStreamAsync, matchesPredicate, resolveAsyncId } from "./async.js";
|
|
15
|
+
import { resolveOutOfBandValue } from "./auth.js";
|
|
15
16
|
import { buildCohort } from "./fixture.js";
|
|
17
|
+
import { driveWait } from "./wait.js";
|
|
16
18
|
import { fillPath } from "./world.js";
|
|
17
19
|
/* ------------------------------------------------------------------- helpers */
|
|
18
20
|
/**
|
|
@@ -161,6 +163,7 @@ function linkRelations(exchange) {
|
|
|
161
163
|
async function list(ctx, query = {}, auth = ctx.auth, scope = ctx.scope) {
|
|
162
164
|
const exchange = await ctx.client.get(fillPath(ctx.listOp.path, scope), {
|
|
163
165
|
headers: auth(),
|
|
166
|
+
operationId: ctx.listOp.operationId,
|
|
164
167
|
query,
|
|
165
168
|
});
|
|
166
169
|
const body = exchange.responseBody;
|
|
@@ -2601,6 +2604,7 @@ const inviteGrantsThenRevokes = {
|
|
|
2601
2604
|
const invited = await ctx.client.request(inviteOp.method, fillPath(inviteOp.path, resource), {
|
|
2602
2605
|
body: { [spec.granteeField]: delegate.inviteAs },
|
|
2603
2606
|
headers: { ...owner.headers(), "content-type": "application/json" },
|
|
2607
|
+
operationId: inviteOp.operationId,
|
|
2604
2608
|
});
|
|
2605
2609
|
if (standDownForFeatureGate(ctx, inviteOp, invited, this.id))
|
|
2606
2610
|
return;
|
|
@@ -2612,10 +2616,25 @@ const inviteGrantsThenRevokes = {
|
|
|
2612
2616
|
"step is then theatre — anyone who can be named in an invite body is already in.", [invited]);
|
|
2613
2617
|
return;
|
|
2614
2618
|
}
|
|
2615
|
-
const token = pointerValue(invited.responseBody, spec.tokenPointer);
|
|
2616
2619
|
const grantId = pointerValue(invited.responseBody, spec.grantPointer);
|
|
2617
|
-
|
|
2618
|
-
|
|
2620
|
+
let token;
|
|
2621
|
+
if (spec.tokenFrom === "outOfBand") {
|
|
2622
|
+
const kind = spec.tokenKind ?? `${ctx.entityName}-invite`;
|
|
2623
|
+
try {
|
|
2624
|
+
token = await resolveOutOfBandValue(ctx.hooks.resolveOutOfBand, delegate.inviteAs, kind, {
|
|
2625
|
+
label: `invite to ${ctx.entityName}`,
|
|
2626
|
+
outOfBand: ctx.outOfBand,
|
|
2627
|
+
});
|
|
2628
|
+
}
|
|
2629
|
+
catch (error) {
|
|
2630
|
+
return ctx.findings.unresolved(this.id, ctx.entityName, error instanceof Error ? error.message : String(error));
|
|
2631
|
+
}
|
|
2632
|
+
}
|
|
2633
|
+
else {
|
|
2634
|
+
token = pointerValue(invited.responseBody, spec.tokenPointer);
|
|
2635
|
+
if (token === undefined) {
|
|
2636
|
+
return ctx.findings.unresolved(this.id, ctx.entityName, `invite response has no token at ${spec.tokenPointer}`);
|
|
2637
|
+
}
|
|
2619
2638
|
}
|
|
2620
2639
|
const acceptScope = { ...resource, token };
|
|
2621
2640
|
const acceptBody = documentedJsonBody(ctx, acceptOp, acceptScope);
|
|
@@ -2625,6 +2644,7 @@ const inviteGrantsThenRevokes = {
|
|
|
2625
2644
|
...delegate.headers(),
|
|
2626
2645
|
...(acceptBody === undefined ? {} : { "content-type": "application/json" }),
|
|
2627
2646
|
},
|
|
2647
|
+
operationId: acceptOp.operationId,
|
|
2628
2648
|
});
|
|
2629
2649
|
if (standDownForFeatureGate(ctx, acceptOp, accepted, this.id))
|
|
2630
2650
|
return;
|
|
@@ -2647,6 +2667,7 @@ const inviteGrantsThenRevokes = {
|
|
|
2647
2667
|
...owner.headers(),
|
|
2648
2668
|
...(revokeBody === undefined ? {} : { "content-type": "application/json" }),
|
|
2649
2669
|
},
|
|
2670
|
+
operationId: revokeOp.operationId,
|
|
2650
2671
|
});
|
|
2651
2672
|
if (standDownForFeatureGate(ctx, revokeOp, revoked, this.id))
|
|
2652
2673
|
return;
|
|
@@ -3396,6 +3417,61 @@ const declaredEffectsOccur = {
|
|
|
3396
3417
|
}
|
|
3397
3418
|
},
|
|
3398
3419
|
};
|
|
3420
|
+
/**
|
|
3421
|
+
* After a write that declares `x-wait`, poll the named GET until a JSON path is occupied
|
|
3422
|
+
* (or `awaitSideEffect` returns true). Timeout is a backend finding, not a coverage gap.
|
|
3423
|
+
*/
|
|
3424
|
+
const sideEffectArrives = {
|
|
3425
|
+
applicable: (ctx) => ctx.waitOps.length > 0,
|
|
3426
|
+
dependsOn: ["list.read-after-write"],
|
|
3427
|
+
mutates: true,
|
|
3428
|
+
id: "effects.side-effect-arrives",
|
|
3429
|
+
needs: "an operation declaring x-wait",
|
|
3430
|
+
async run(ctx) {
|
|
3431
|
+
for (const op of ctx.waitOps) {
|
|
3432
|
+
const spec = op.wait;
|
|
3433
|
+
if (spec === null)
|
|
3434
|
+
continue;
|
|
3435
|
+
const pollOp = ctx.model.byOperationId.get(spec.operationId);
|
|
3436
|
+
if (pollOp === undefined) {
|
|
3437
|
+
ctx.findings.gap(this.id, ctx.entityName, `${op.operationId} x-wait names "${spec.operationId}", which is not in the document`, "the side effect cannot be observed, so it is not verified");
|
|
3438
|
+
continue;
|
|
3439
|
+
}
|
|
3440
|
+
const body = op.hasRequestBody ? bodyForOp(ctx, op) : undefined;
|
|
3441
|
+
const invoked = await ctx.client.request(op.method, fillPath(op.path, ctx.scope), {
|
|
3442
|
+
headers: ctx.auth(),
|
|
3443
|
+
operationId: op.operationId,
|
|
3444
|
+
...(body === undefined ? {} : await encodeOpBody(ctx, op, body)),
|
|
3445
|
+
});
|
|
3446
|
+
if (standDownForFeatureGate(ctx, op, invoked, this.id))
|
|
3447
|
+
continue;
|
|
3448
|
+
if (standDownForRateLimit(ctx, invoked, this.id))
|
|
3449
|
+
continue;
|
|
3450
|
+
if (invoked.status >= 400) {
|
|
3451
|
+
ctx.findings.gap(this.id, ctx.entityName, `${op.operationId} could not be invoked`, `returned ${invoked.status}; its declared x-wait is unverified`);
|
|
3452
|
+
continue;
|
|
3453
|
+
}
|
|
3454
|
+
const outcome = await driveWait({
|
|
3455
|
+
awaitSideEffect: ctx.hooks.awaitSideEffect,
|
|
3456
|
+
client: ctx.client,
|
|
3457
|
+
headers: ctx.auth,
|
|
3458
|
+
pollOp,
|
|
3459
|
+
record: invoked.responseBody,
|
|
3460
|
+
scope: ctx.scope,
|
|
3461
|
+
spec,
|
|
3462
|
+
writeOpId: op.operationId,
|
|
3463
|
+
...(ctx.refreshIfStale === undefined ? {} : { refreshIfStale: ctx.refreshIfStale }),
|
|
3464
|
+
});
|
|
3465
|
+
if (!outcome.timedOut)
|
|
3466
|
+
continue;
|
|
3467
|
+
ctx.findings.backend(this.id, ctx.entityName, `${op.operationId} side effect did not appear within ${spec.timeoutMs}ms`, `x-wait polls ${spec.operationId}` +
|
|
3468
|
+
(spec.until === undefined ? "" : ` until ${spec.until} is non-empty`) +
|
|
3469
|
+
` and the path was still empty after ${outcome.polls} poll(s) / ${Math.round(outcome.elapsedMs)}ms. ` +
|
|
3470
|
+
"Queue consumers and webhook inboxes are not the same request; a timeout here is a " +
|
|
3471
|
+
"missed delivery, not a coverage gap.", [invoked, ...outcome.exchanges.slice(-2)]);
|
|
3472
|
+
}
|
|
3473
|
+
},
|
|
3474
|
+
};
|
|
3399
3475
|
async function observe(ctx, listOp) {
|
|
3400
3476
|
let path;
|
|
3401
3477
|
try {
|
|
@@ -3849,6 +3925,7 @@ export const CHECKS = [
|
|
|
3849
3925
|
/* declared side effects and async lifecycles, last: both invoke operations that change the
|
|
3850
3926
|
* world, and both are meaningless if the read surface above is already known broken */
|
|
3851
3927
|
declaredEffectsOccur,
|
|
3928
|
+
sideEffectArrives,
|
|
3852
3929
|
asyncReachesTerminalState,
|
|
3853
3930
|
asyncReceiptIsResolvable,
|
|
3854
3931
|
/* After every other check has written to the transcript — create is owned by
|