@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.
Files changed (67) hide show
  1. package/README.md +141 -20
  2. package/dist/cli.js +3 -0
  3. package/dist/config/define-config.d.ts +95 -1
  4. package/dist/config/define-config.d.ts.map +1 -1
  5. package/dist/config/define-config.js +6 -0
  6. package/dist/config/define-config.js.map +1 -1
  7. package/dist/index.d.ts +5 -2
  8. package/dist/index.js +3 -1
  9. package/dist/report/console.d.ts.map +1 -1
  10. package/dist/report/console.js +2 -0
  11. package/dist/report/console.js.map +1 -1
  12. package/dist/report/render.d.ts.map +1 -1
  13. package/dist/report/render.js +4 -2
  14. package/dist/report/render.js.map +1 -1
  15. package/dist/runtime/auth.d.ts +22 -2
  16. package/dist/runtime/auth.d.ts.map +1 -1
  17. package/dist/runtime/auth.js +129 -16
  18. package/dist/runtime/auth.js.map +1 -1
  19. package/dist/runtime/body.d.ts.map +1 -1
  20. package/dist/runtime/body.js +6 -3
  21. package/dist/runtime/body.js.map +1 -1
  22. package/dist/runtime/checks.d.ts +6 -0
  23. package/dist/runtime/checks.d.ts.map +1 -1
  24. package/dist/runtime/checks.js +80 -3
  25. package/dist/runtime/checks.js.map +1 -1
  26. package/dist/runtime/client.d.ts +6 -0
  27. package/dist/runtime/client.d.ts.map +1 -1
  28. package/dist/runtime/client.js +14 -1
  29. package/dist/runtime/client.js.map +1 -1
  30. package/dist/runtime/finding.d.ts +2 -0
  31. package/dist/runtime/finding.d.ts.map +1 -1
  32. package/dist/runtime/finding.js.map +1 -1
  33. package/dist/runtime/input.d.ts +7 -0
  34. package/dist/runtime/input.d.ts.map +1 -0
  35. package/dist/runtime/input.js +46 -0
  36. package/dist/runtime/input.js.map +1 -0
  37. package/dist/runtime/poll.d.ts +29 -0
  38. package/dist/runtime/poll.d.ts.map +1 -0
  39. package/dist/runtime/poll.js +59 -0
  40. package/dist/runtime/poll.js.map +1 -0
  41. package/dist/runtime/principals.d.ts +29 -0
  42. package/dist/runtime/principals.d.ts.map +1 -0
  43. package/dist/runtime/principals.js +97 -0
  44. package/dist/runtime/principals.js.map +1 -0
  45. package/dist/runtime/run.d.ts +10 -1
  46. package/dist/runtime/run.d.ts.map +1 -1
  47. package/dist/runtime/run.js +96 -5
  48. package/dist/runtime/run.js.map +1 -1
  49. package/dist/runtime/upload.d.ts +1 -0
  50. package/dist/runtime/upload.d.ts.map +1 -1
  51. package/dist/runtime/upload.js.map +1 -1
  52. package/dist/runtime/wait.d.ts +32 -0
  53. package/dist/runtime/wait.d.ts.map +1 -0
  54. package/dist/runtime/wait.js +125 -0
  55. package/dist/runtime/wait.js.map +1 -0
  56. package/dist/runtime/world.d.ts.map +1 -1
  57. package/dist/runtime/world.js +2 -0
  58. package/dist/runtime/world.js.map +1 -1
  59. package/dist/spec/extensions.d.ts +16 -0
  60. package/dist/spec/extensions.d.ts.map +1 -1
  61. package/dist/spec/extensions.js +24 -1
  62. package/dist/spec/extensions.js.map +1 -1
  63. package/dist/spec/graph.d.ts +2 -1
  64. package/dist/spec/graph.d.ts.map +1 -1
  65. package/dist/spec/graph.js +2 -1
  66. package/dist/spec/graph.js.map +1 -1
  67. package/package.json +4 -2
@@ -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
- if (token === undefined) {
2618
- return ctx.findings.unresolved(this.id, ctx.entityName, `invite response has no token at ${spec.tokenPointer}`);
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