@mandateops/audit 0.4.2 → 0.6.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/dist/index.d.ts CHANGED
@@ -1,8 +1,8 @@
1
- import type { CapacityReading, HistoryReading, ReadingProfile } from "./reading.js";
1
+ import type { CapacityReading, HistoryReading, ReadingProfile, StatusReading } from "./reading.js";
2
2
  import type { ProviderWire, StagedWire } from "./providers/index.js";
3
3
  export * from "./providers/index.js";
4
- export { parseCapacityReading, parseHistoryReading, ReadingError, requireHistoryFits, } from "./reading.js";
5
- export type { AttemptEnd, AttemptResult, CapacityReading, Flow, HistoryEvent, HistoryReading, Instruction, MinorUnits, Payout, PayoutState, ReadingProfile, HistoryConsistency, CapacityConsistency, } from "./reading.js";
4
+ export { parseCapacityReading, parseHistoryReading, parseStatusReading, ReadingError, requireHistoryFits, } from "./reading.js";
5
+ export type { AttemptEnd, AttemptResult, CapacityReading, Flow, HistoryEvent, HistoryReading, Instruction, MinorUnits, Payout, PayoutState, ReadingProfile, StatusReading, HistoryConsistency, CapacityConsistency, } from "./reading.js";
6
6
  /**
7
7
  * The largest amount the stand-in holds, the audit core's `u128` maximum.
8
8
  * `fixtures/wire/vectors.json` pins it for every reader.
@@ -10,7 +10,7 @@ export type { AttemptEnd, AttemptResult, CapacityReading, Flow, HistoryEvent, Hi
10
10
  export declare const SYNTHETIC_MAX_MINOR_UNITS = 340282366920938463463374607431768211455n;
11
11
  /** Integer minor units as a decimal string, so no language loses precision. */
12
12
  export declare function minorUnits(value: string): string;
13
- declare const PROPERTIES: readonly ["instruction_binding", "commitment_retention", "retry_identity", "effect_within_authorisation", "reconciliation_consistency", "external_outcome"];
13
+ declare const PROPERTIES: readonly ["instruction_binding", "commitment_retention", "retry_identity", "effect_within_authorisation", "reconciliation_consistency", "external_outcome", "reported_outcome"];
14
14
  export type Property = (typeof PROPERTIES)[number];
15
15
  declare const PROPERTY_RESULTS: readonly ["satisfied", "violated", "inconclusive", "not_applicable"];
16
16
  declare const ASSURANCES: readonly ["backend_and_provider", "backend_and_provider_commitments_not_checked", "money_out_only_commitments_not_checked", "no_assurance"];
@@ -20,11 +20,23 @@ export type ReadContext = Readonly<{
20
20
  viewerCredential: string;
21
21
  providerPayment: string;
22
22
  }>;
23
- /** Reads the backend, in the shape of `contracts/history-reading.md`. */
24
- export type Adapter = Readonly<{
23
+ /**
24
+ * Reads a backend that reserves funds: its lifecycle and capacity, in the
25
+ * shape of `contracts/history-reading.md`.
26
+ */
27
+ export type ReadingAdapter = Readonly<{
25
28
  history: (context: ReadContext) => Promise<HistoryReading>;
26
29
  capacity: (context: ReadContext) => Promise<CapacityReading>;
27
30
  }>;
31
+ /**
32
+ * Reads the outcome a backend without reservations reports for the payout,
33
+ * judged against the provider's records under `payout-effects-only`.
34
+ */
35
+ export type StatusAdapter = Readonly<{
36
+ status: (context: ReadContext) => Promise<StatusReading>;
37
+ }>;
38
+ /** Reads the backend: lifecycle and capacity, or only its reported status. */
39
+ export type Adapter = ReadingAdapter | StatusAdapter;
28
40
  export declare const defineAdapter: (adapter: Adapter) => Adapter;
29
41
  /** The provider identity every effect must carry. */
30
42
  export type ProviderTerms = Readonly<{
@@ -212,7 +224,7 @@ type Credentials = Readonly<Record<ServiceRole, string>>;
212
224
  * The session protocol this SDK speaks. The host announces its own when
213
225
  * ready; the two must match, as they do for an SDK shipped with its CLI.
214
226
  */
215
- export declare const SESSION_PROTOCOL = 3;
227
+ export declare const SESSION_PROTOCOL = 4;
216
228
  /** Why a host of another protocol is refused, and how to fix it. */
217
229
  export declare function protocolMismatch(announced: number | undefined): string;
218
230
  /** A sealed result, checked before anything reads it. */
package/dist/index.js CHANGED
@@ -12,10 +12,10 @@ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
12
12
  import { tmpdir } from "node:os";
13
13
  import { dirname, join, resolve } from "node:path";
14
14
  import { setTimeout as sleep } from "node:timers/promises";
15
- import { parseCapacityReading, parseHistoryReading, requireHistoryFits, } from "./reading.js";
15
+ import { parseCapacityReading, parseHistoryReading, parseStatusReading, requireHistoryFits, } from "./reading.js";
16
16
  // Every provider format and its types, from the one list that names them.
17
17
  export * from "./providers/index.js";
18
- export { parseCapacityReading, parseHistoryReading, ReadingError, requireHistoryFits, } from "./reading.js";
18
+ export { parseCapacityReading, parseHistoryReading, parseStatusReading, ReadingError, requireHistoryFits, } from "./reading.js";
19
19
  const MINOR_UNITS = /^[1-9][0-9]*$/;
20
20
  /**
21
21
  * The largest amount the stand-in holds, the audit core's `u128` maximum.
@@ -42,6 +42,7 @@ const PROPERTIES = [
42
42
  "effect_within_authorisation",
43
43
  "reconciliation_consistency",
44
44
  "external_outcome",
45
+ "reported_outcome",
45
46
  ];
46
47
  const PROPERTY_RESULTS = [
47
48
  "satisfied",
@@ -76,7 +77,7 @@ const isRecord = (value) => typeof value === "object" && value !== null && !Arra
76
77
  * The session protocol this SDK speaks. The host announces its own when
77
78
  * ready; the two must match, as they do for an SDK shipped with its CLI.
78
79
  */
79
- export const SESSION_PROTOCOL = 3;
80
+ export const SESSION_PROTOCOL = 4;
80
81
  /** Why a host of another protocol is refused, and how to fix it. */
81
82
  export function protocolMismatch(announced) {
82
83
  const host = announced === undefined
@@ -253,8 +254,15 @@ const readingProfile = (config) => config.financialProfile === "payout-effects-o
253
254
  */
254
255
  export function requireRunnable(config) {
255
256
  const profile = readingProfile(config);
256
- if (profile === undefined && config.adapter) {
257
- throw new AuditUsageError("payout-effects-only is judged on provider records alone: remove the adapter");
257
+ if (profile === undefined &&
258
+ config.adapter &&
259
+ !("status" in config.adapter)) {
260
+ throw new AuditUsageError("payout-effects-only reads no lifecycle or capacity: give it a status " +
261
+ "adapter (contracts/history-reading.md), or none");
262
+ }
263
+ if (profile !== undefined && config.adapter && "status" in config.adapter) {
264
+ throw new AuditUsageError(`${profile} reads lifecycle and capacity, not a status: a status ` +
265
+ "adapter is for payout-effects-only");
258
266
  }
259
267
  if (profile !== undefined && !config.adapter) {
260
268
  throw new AuditUsageError(`${profile} needs an adapter with history and capacity readings ` +
@@ -391,10 +399,12 @@ async function session(config, body, channel, backend, credentials) {
391
399
  const context = () => providerPayment === undefined
392
400
  ? undefined
393
401
  : { backend, viewerCredential, providerPayment };
402
+ const readings = config.adapter && "history" in config.adapter ? config.adapter : undefined;
403
+ const statusAdapter = config.adapter && "status" in config.adapter ? config.adapter : undefined;
394
404
  const readHistory = async (slot) => {
395
405
  const read = context();
396
- if (config.adapter && profile && read && authorised) {
397
- const { history } = config.adapter;
406
+ if (readings && profile && read && authorised) {
407
+ const { history } = readings;
398
408
  const reference = authorised.expected.reference;
399
409
  await reading(slot, async () => {
400
410
  const taken = parseHistoryReading(await history(read));
@@ -405,11 +415,27 @@ async function session(config, body, channel, backend, credentials) {
405
415
  };
406
416
  const readCapacity = async (slot) => {
407
417
  const read = context();
408
- const capacity = config.adapter?.capacity;
418
+ const capacity = readings?.capacity;
409
419
  if (capacity && read) {
410
420
  await reading(slot, async () => parseCapacityReading(await capacity(read)));
411
421
  }
412
422
  };
423
+ /**
424
+ * The backend's reported outcome, after the provider's records. Without a
425
+ * binding there is nothing to read by, which is reported, never skipped.
426
+ */
427
+ const readStatus = async () => {
428
+ if (!statusAdapter || !authorised)
429
+ return;
430
+ const reference = authorised.expected.reference;
431
+ const read = context();
432
+ await reading("capture_status", async () => {
433
+ if (!read) {
434
+ throw new AuditUsageError("a status adapter reads by the provider payment: call bindProviderPayment");
435
+ }
436
+ return parseStatusReading(await statusAdapter.status(read), reference);
437
+ });
438
+ };
413
439
  const requireBound = (doing) => {
414
440
  if (profile && (authorised || competing) && providerPayment === undefined) {
415
441
  throw new AuditUsageError(`${doing} under ${profile} needs bindProviderPayment first: adapters read by that identifier`);
@@ -583,6 +609,7 @@ async function session(config, body, channel, backend, credentials) {
583
609
  await readCapacity("capture_capacity");
584
610
  await channel.send({ type: "read_effects" });
585
611
  await readHistory("recapture_history");
612
+ await readStatus();
586
613
  const result = {
587
614
  ...parseResult(await channel.send({ type: "seal" })),
588
615
  reading_problems: problems,
package/dist/reading.d.ts CHANGED
@@ -83,6 +83,16 @@ export type CapacityReading = Readonly<{
83
83
  committed: MinorUnits;
84
84
  consumed: MinorUnits;
85
85
  }>;
86
+ /**
87
+ * The outcome the backend itself reports for one payout, taken once after
88
+ * the provider's records under `payout-effects-only`.
89
+ */
90
+ export type StatusReading = Readonly<{
91
+ source: "adapter";
92
+ /** The reference given to `expectPayment`. */
93
+ payment: string;
94
+ state: PayoutState;
95
+ }>;
86
96
  /** The profiles an adapter reading is taken under. */
87
97
  export type ReadingProfile = "payout-effects" | "payout-commitment";
88
98
  /**
@@ -101,6 +111,13 @@ export declare class ReadingError extends TypeError {
101
111
  * @throws {ReadingError} naming the first field that cannot be interpreted.
102
112
  */
103
113
  export declare function parseHistoryReading(value: unknown): HistoryReading;
114
+ /**
115
+ * Check an untrusted status reading against the contract, and that it names
116
+ * the payout the test expects.
117
+ *
118
+ * @throws {ReadingError} naming the first field that cannot be interpreted.
119
+ */
120
+ export declare function parseStatusReading(value: unknown, reference: string): StatusReading;
104
121
  /**
105
122
  * Check an untrusted capacity reading against the contract.
106
123
  *
package/dist/reading.js CHANGED
@@ -231,6 +231,22 @@ export function parseHistoryReading(value) {
231
231
  });
232
232
  return value;
233
233
  }
234
+ /**
235
+ * Check an untrusted status reading against the contract, and that it names
236
+ * the payout the test expects.
237
+ *
238
+ * @throws {ReadingError} naming the first field that cannot be interpreted.
239
+ */
240
+ export function parseStatusReading(value, reference) {
241
+ const found = members(value, "", ["source", "payment", "state"]);
242
+ requireExactly("adapter", found["source"], "source");
243
+ requireText(found["payment"], "payment");
244
+ if (found["payment"] !== reference) {
245
+ refuse("payment", "the reference given to expectPayment", found["payment"]);
246
+ }
247
+ requireOneOf(STATES, found["state"], "state");
248
+ return value;
249
+ }
234
250
  /**
235
251
  * Check an untrusted capacity reading against the contract.
236
252
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mandateops/audit",
3
- "version": "0.4.2",
3
+ "version": "0.6.0",
4
4
  "description": "Write audit tests that check a payout backend keeps one payment identity, money limits and commitments when a provider commits but the reply is lost. Needs the mandateops CLI.",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://getmandateops.com",