@mandateops/audit 0.3.0 → 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 +19 -8
- package/dist/index.js +36 -9
- package/dist/providers/fiat.d.ts +9 -0
- package/dist/providers/fiat.js +17 -0
- package/dist/providers/index.d.ts +1 -1
- package/dist/providers/index.js +1 -1
- package/dist/providers/types.d.ts +9 -0
- package/dist/reading.d.ts +17 -0
- package/dist/reading.js +16 -0
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +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
|
-
export
|
|
4
|
-
export
|
|
5
|
-
export {
|
|
6
|
-
export type { AttemptEnd, AttemptResult, CapacityReading, Flow, HistoryEvent, HistoryReading, Instruction, MinorUnits, Payout, PayoutState, ReadingProfile, HistoryConsistency, CapacityConsistency, } from "./reading.js";
|
|
3
|
+
export * from "./providers/index.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";
|
|
7
6
|
/**
|
|
8
7
|
* The largest amount the stand-in holds, the audit core's `u128` maximum.
|
|
9
8
|
* `fixtures/wire/vectors.json` pins it for every reader.
|
|
@@ -21,11 +20,23 @@ export type ReadContext = Readonly<{
|
|
|
21
20
|
viewerCredential: string;
|
|
22
21
|
providerPayment: string;
|
|
23
22
|
}>;
|
|
24
|
-
/**
|
|
25
|
-
|
|
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<{
|
|
26
28
|
history: (context: ReadContext) => Promise<HistoryReading>;
|
|
27
29
|
capacity: (context: ReadContext) => Promise<CapacityReading>;
|
|
28
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;
|
|
29
40
|
export declare const defineAdapter: (adapter: Adapter) => Adapter;
|
|
30
41
|
/** The provider identity every effect must carry. */
|
|
31
42
|
export type ProviderTerms = Readonly<{
|
|
@@ -213,7 +224,7 @@ type Credentials = Readonly<Record<ServiceRole, string>>;
|
|
|
213
224
|
* The session protocol this SDK speaks. The host announces its own when
|
|
214
225
|
* ready; the two must match, as they do for an SDK shipped with its CLI.
|
|
215
226
|
*/
|
|
216
|
-
export declare const SESSION_PROTOCOL =
|
|
227
|
+
export declare const SESSION_PROTOCOL = 4;
|
|
217
228
|
/** Why a host of another protocol is refused, and how to fix it. */
|
|
218
229
|
export declare function protocolMismatch(announced: number | undefined): string;
|
|
219
230
|
/** A sealed result, checked before anything reads it. */
|
package/dist/index.js
CHANGED
|
@@ -12,9 +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";
|
|
16
|
-
|
|
17
|
-
export
|
|
15
|
+
import { parseCapacityReading, parseHistoryReading, parseStatusReading, requireHistoryFits, } from "./reading.js";
|
|
16
|
+
// Every provider format and its types, from the one list that names them.
|
|
17
|
+
export * from "./providers/index.js";
|
|
18
|
+
export { parseCapacityReading, parseHistoryReading, parseStatusReading, ReadingError, requireHistoryFits, } from "./reading.js";
|
|
18
19
|
const MINOR_UNITS = /^[1-9][0-9]*$/;
|
|
19
20
|
/**
|
|
20
21
|
* The largest amount the stand-in holds, the audit core's `u128` maximum.
|
|
@@ -75,7 +76,7 @@ const isRecord = (value) => typeof value === "object" && value !== null && !Arra
|
|
|
75
76
|
* The session protocol this SDK speaks. The host announces its own when
|
|
76
77
|
* ready; the two must match, as they do for an SDK shipped with its CLI.
|
|
77
78
|
*/
|
|
78
|
-
export const SESSION_PROTOCOL =
|
|
79
|
+
export const SESSION_PROTOCOL = 4;
|
|
79
80
|
/** Why a host of another protocol is refused, and how to fix it. */
|
|
80
81
|
export function protocolMismatch(announced) {
|
|
81
82
|
const host = announced === undefined
|
|
@@ -252,8 +253,15 @@ const readingProfile = (config) => config.financialProfile === "payout-effects-o
|
|
|
252
253
|
*/
|
|
253
254
|
export function requireRunnable(config) {
|
|
254
255
|
const profile = readingProfile(config);
|
|
255
|
-
if (profile === undefined &&
|
|
256
|
-
|
|
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");
|
|
257
265
|
}
|
|
258
266
|
if (profile !== undefined && !config.adapter) {
|
|
259
267
|
throw new AuditUsageError(`${profile} needs an adapter with history and capacity readings ` +
|
|
@@ -390,10 +398,12 @@ async function session(config, body, channel, backend, credentials) {
|
|
|
390
398
|
const context = () => providerPayment === undefined
|
|
391
399
|
? undefined
|
|
392
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;
|
|
393
403
|
const readHistory = async (slot) => {
|
|
394
404
|
const read = context();
|
|
395
|
-
if (
|
|
396
|
-
const { history } =
|
|
405
|
+
if (readings && profile && read && authorised) {
|
|
406
|
+
const { history } = readings;
|
|
397
407
|
const reference = authorised.expected.reference;
|
|
398
408
|
await reading(slot, async () => {
|
|
399
409
|
const taken = parseHistoryReading(await history(read));
|
|
@@ -404,11 +414,27 @@ async function session(config, body, channel, backend, credentials) {
|
|
|
404
414
|
};
|
|
405
415
|
const readCapacity = async (slot) => {
|
|
406
416
|
const read = context();
|
|
407
|
-
const capacity =
|
|
417
|
+
const capacity = readings?.capacity;
|
|
408
418
|
if (capacity && read) {
|
|
409
419
|
await reading(slot, async () => parseCapacityReading(await capacity(read)));
|
|
410
420
|
}
|
|
411
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
|
+
};
|
|
412
438
|
const requireBound = (doing) => {
|
|
413
439
|
if (profile && (authorised || competing) && providerPayment === undefined) {
|
|
414
440
|
throw new AuditUsageError(`${doing} under ${profile} needs bindProviderPayment first: adapters read by that identifier`);
|
|
@@ -582,6 +608,7 @@ async function session(config, body, channel, backend, credentials) {
|
|
|
582
608
|
await readCapacity("capture_capacity");
|
|
583
609
|
await channel.send({ type: "read_effects" });
|
|
584
610
|
await readHistory("recapture_history");
|
|
611
|
+
await readStatus();
|
|
585
612
|
const result = {
|
|
586
613
|
...parseResult(await channel.send({ type: "seal" })),
|
|
587
614
|
reading_problems: problems,
|
package/dist/providers/fiat.d.ts
CHANGED
|
@@ -16,6 +16,15 @@ export declare const stripePayouts: (terms: LedgerTerms) => ProviderWire;
|
|
|
16
16
|
* replay with `Idempotent-Replayed`, which this format does not send.
|
|
17
17
|
*/
|
|
18
18
|
export declare const stripeTransfers: (terms: LedgerTerms) => ProviderWire;
|
|
19
|
+
/**
|
|
20
|
+
* Stripe Connect payouts: `POST /v1/accounts` and `POST /v1/accounts/:accountId/external_accounts`
|
|
21
|
+
* set up the recipient, `POST /v1/transfers` moves the money to it (the
|
|
22
|
+
* payment, as `stripeTransfers`), and `POST /v1/payouts` with a
|
|
23
|
+
* `Stripe-Account` header pays out of the connected account's balance. The
|
|
24
|
+
* key is optional, as at Stripe: a transfer sent without `Idempotency-Key`
|
|
25
|
+
* is a new transfer each time, so a retry after a lost reply pays again.
|
|
26
|
+
*/
|
|
27
|
+
export declare const stripeConnect: (terms: LedgerTerms) => ProviderWire;
|
|
19
28
|
/**
|
|
20
29
|
* `POST /v3/transfers`. Keyed on the unique `reference`, as v3 coverage of
|
|
21
30
|
* `X-Idempotency-Key` and v3 reference reuse are unverified. The amount is a
|
package/dist/providers/fiat.js
CHANGED
|
@@ -152,6 +152,23 @@ export const stripeTransfers = (terms) => stripe("/v1/transfers", terms, {
|
|
|
152
152
|
reversals: { object: "list", data: [], has_more: false },
|
|
153
153
|
reversed: false,
|
|
154
154
|
});
|
|
155
|
+
/**
|
|
156
|
+
* Stripe Connect payouts: `POST /v1/accounts` and `POST /v1/accounts/:accountId/external_accounts`
|
|
157
|
+
* set up the recipient, `POST /v1/transfers` moves the money to it (the
|
|
158
|
+
* payment, as `stripeTransfers`), and `POST /v1/payouts` with a
|
|
159
|
+
* `Stripe-Account` header pays out of the connected account's balance. The
|
|
160
|
+
* key is optional, as at Stripe: a transfer sent without `Idempotency-Key`
|
|
161
|
+
* is a new transfer each time, so a retry after a lost reply pays again.
|
|
162
|
+
*/
|
|
163
|
+
export const stripeConnect = (terms) => ({
|
|
164
|
+
...stripeTransfers(terms),
|
|
165
|
+
key: { header: "idempotency-key", maxLength: 255, optional: true },
|
|
166
|
+
connect: {
|
|
167
|
+
accounts: "/v1/accounts",
|
|
168
|
+
externalAccounts: "/v1/accounts/:accountId/external_accounts",
|
|
169
|
+
payouts: "/v1/payouts",
|
|
170
|
+
},
|
|
171
|
+
});
|
|
155
172
|
/**
|
|
156
173
|
* `POST /v3/transfers`. Keyed on the unique `reference`, as v3 coverage of
|
|
157
174
|
* `X-Idempotency-Key` and v3 reference reuse are unverified. The amount is a
|
|
@@ -5,5 +5,5 @@
|
|
|
5
5
|
* that provider behaves.
|
|
6
6
|
*/
|
|
7
7
|
export { bridge, circleMint, fireblocks } from "./crypto.js";
|
|
8
|
-
export { adyen, airwallex, checkout, dwolla, flutterwave, modernTreasury, molliePayouts, paypal, paystack, peach, razorpayx, revolut, stitch, stripePayouts, stripeTransfers, wise, } from "./fiat.js";
|
|
8
|
+
export { adyen, airwallex, checkout, dwolla, flutterwave, modernTreasury, molliePayouts, paypal, paystack, peach, razorpayx, revolut, stitch, stripeConnect, stripePayouts, stripeTransfers, wise, } from "./fiat.js";
|
|
9
9
|
export type * from "./types.js";
|
package/dist/providers/index.js
CHANGED
|
@@ -5,4 +5,4 @@
|
|
|
5
5
|
* that provider behaves.
|
|
6
6
|
*/
|
|
7
7
|
export { bridge, circleMint, fireblocks } from "./crypto.js";
|
|
8
|
-
export { adyen, airwallex, checkout, dwolla, flutterwave, modernTreasury, molliePayouts, paypal, paystack, peach, razorpayx, revolut, stitch, stripePayouts, stripeTransfers, wise, } from "./fiat.js";
|
|
8
|
+
export { adyen, airwallex, checkout, dwolla, flutterwave, modernTreasury, molliePayouts, paypal, paystack, peach, razorpayx, revolut, stitch, stripeConnect, stripePayouts, stripeTransfers, wise, } from "./fiat.js";
|
|
@@ -57,6 +57,7 @@ export type WireKeyShape = "uuid_v4" | "lowercase_slug";
|
|
|
57
57
|
export type WireKey = Readonly<{
|
|
58
58
|
header: string;
|
|
59
59
|
maxLength?: number;
|
|
60
|
+
optional?: boolean;
|
|
60
61
|
} | {
|
|
61
62
|
field: string;
|
|
62
63
|
maxLength?: number;
|
|
@@ -106,6 +107,14 @@ export type ProviderWire = Readonly<{
|
|
|
106
107
|
conflict: Reply;
|
|
107
108
|
duplicate?: Reply;
|
|
108
109
|
}>;
|
|
110
|
+
/** Stripe Connect's setup and payout calls around a transfer. */
|
|
111
|
+
connect?: ConnectRoutes;
|
|
112
|
+
}>;
|
|
113
|
+
/** Where Stripe Connect's account setup and payout calls sit. */
|
|
114
|
+
export type ConnectRoutes = Readonly<{
|
|
115
|
+
accounts: string;
|
|
116
|
+
externalAccounts: string;
|
|
117
|
+
payouts: string;
|
|
109
118
|
}>;
|
|
110
119
|
/** A format of several calls, which the stand-in holds as its own flow. */
|
|
111
120
|
export type StagedWire = Readonly<{
|
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.
|
|
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",
|