@mandateops/audit 0.4.2 → 0.5.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.
@@ -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.
@@ -76,7 +76,7 @@ const isRecord = (value) => typeof value === "object" && value !== null && !Arra
76
76
  * The session protocol this SDK speaks. The host announces its own when
77
77
  * ready; the two must match, as they do for an SDK shipped with its CLI.
78
78
  */
79
- export const SESSION_PROTOCOL = 3;
79
+ export const SESSION_PROTOCOL = 4;
80
80
  /** Why a host of another protocol is refused, and how to fix it. */
81
81
  export function protocolMismatch(announced) {
82
82
  const host = announced === undefined
@@ -253,8 +253,15 @@ const readingProfile = (config) => config.financialProfile === "payout-effects-o
253
253
  */
254
254
  export function requireRunnable(config) {
255
255
  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");
256
+ if (profile === undefined &&
257
+ config.adapter &&
258
+ !("status" in config.adapter)) {
259
+ throw new AuditUsageError("payout-effects-only reads no lifecycle or capacity: give it a status " +
260
+ "adapter (contracts/history-reading.md), or none");
261
+ }
262
+ if (profile !== undefined && config.adapter && "status" in config.adapter) {
263
+ throw new AuditUsageError(`${profile} reads lifecycle and capacity, not a status: a status ` +
264
+ "adapter is for payout-effects-only");
258
265
  }
259
266
  if (profile !== undefined && !config.adapter) {
260
267
  throw new AuditUsageError(`${profile} needs an adapter with history and capacity readings ` +
@@ -391,10 +398,12 @@ async function session(config, body, channel, backend, credentials) {
391
398
  const context = () => providerPayment === undefined
392
399
  ? undefined
393
400
  : { backend, viewerCredential, providerPayment };
401
+ const readings = config.adapter && "history" in config.adapter ? config.adapter : undefined;
402
+ const statusAdapter = config.adapter && "status" in config.adapter ? config.adapter : undefined;
394
403
  const readHistory = async (slot) => {
395
404
  const read = context();
396
- if (config.adapter && profile && read && authorised) {
397
- const { history } = config.adapter;
405
+ if (readings && profile && read && authorised) {
406
+ const { history } = readings;
398
407
  const reference = authorised.expected.reference;
399
408
  await reading(slot, async () => {
400
409
  const taken = parseHistoryReading(await history(read));
@@ -405,11 +414,27 @@ async function session(config, body, channel, backend, credentials) {
405
414
  };
406
415
  const readCapacity = async (slot) => {
407
416
  const read = context();
408
- const capacity = config.adapter?.capacity;
417
+ const capacity = readings?.capacity;
409
418
  if (capacity && read) {
410
419
  await reading(slot, async () => parseCapacityReading(await capacity(read)));
411
420
  }
412
421
  };
422
+ /**
423
+ * The backend's reported outcome, after the provider's records. Without a
424
+ * binding there is nothing to read by, which is reported, never skipped.
425
+ */
426
+ const readStatus = async () => {
427
+ if (!statusAdapter || !authorised)
428
+ return;
429
+ const reference = authorised.expected.reference;
430
+ const read = context();
431
+ await reading("capture_status", async () => {
432
+ if (!read) {
433
+ throw new AuditUsageError("a status adapter reads by the provider payment: call bindProviderPayment");
434
+ }
435
+ return parseStatusReading(await statusAdapter.status(read), reference);
436
+ });
437
+ };
413
438
  const requireBound = (doing) => {
414
439
  if (profile && (authorised || competing) && providerPayment === undefined) {
415
440
  throw new AuditUsageError(`${doing} under ${profile} needs bindProviderPayment first: adapters read by that identifier`);
@@ -583,6 +608,7 @@ async function session(config, body, channel, backend, credentials) {
583
608
  await readCapacity("capture_capacity");
584
609
  await channel.send({ type: "read_effects" });
585
610
  await readHistory("recapture_history");
611
+ await readStatus();
586
612
  const result = {
587
613
  ...parseResult(await channel.send({ type: "seal" })),
588
614
  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.5.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",