@mandateops/audit 0.0.0-stage → 0.2.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.
@@ -0,0 +1,122 @@
1
+ /** Terms a provider's request does not carry, declared once per test suite. */
2
+ export type LedgerTerms = Readonly<{
3
+ tenant: string;
4
+ agent: string;
5
+ delegation: string;
6
+ account: string;
7
+ asset: string;
8
+ network: string;
9
+ }>;
10
+ /** For a format whose request names the paying account. */
11
+ export type SourcedTerms = Omit<LedgerTerms, "account">;
12
+ /** For a format that names no destination in its request. */
13
+ export type OwnAccountTerms = LedgerTerms & Readonly<{
14
+ destination: string;
15
+ }>;
16
+ /** Bridge's request names the paying customer, the asset and the rail. */
17
+ export type BridgeTerms = Omit<LedgerTerms, "account" | "asset" | "network">;
18
+ /** Checkout names the paying entity but no currency. */
19
+ export type CheckoutTerms = Omit<CurrencyTerms, "account">;
20
+ /** Fireblocks' request names its asset and vault; the asset implies the network. */
21
+ export type FireblocksTerms = Omit<LedgerTerms, "account" | "asset">;
22
+ /** Mollie may leave out the amount, meaning the whole balance in `currency`. */
23
+ export type MollieTerms = OwnAccountTerms & Readonly<{
24
+ currency: string;
25
+ wholeBalance: string;
26
+ }>;
27
+ /** For a format that names no currency in its request. */
28
+ export type CurrencyTerms = LedgerTerms & Readonly<{
29
+ currency: string;
30
+ }>;
31
+ /** Wise's calls name the profile and target currency. */
32
+ export type WiseTerms = Omit<LedgerTerms, "account" | "asset">;
33
+ /** Terms a request may omit, and what absence means there. */
34
+ export type TermDefaults = Readonly<Record<string, string>>;
35
+ export type Reply = Readonly<{
36
+ status: number;
37
+ body: unknown;
38
+ headers?: Readonly<Record<string, string>>;
39
+ }>;
40
+ /** How a request body is encoded. */
41
+ export type WireEncoding = "json" | "form";
42
+ /** What a repeated key receives. */
43
+ export type WireReplay = "same_reply" | "duplicate_reply";
44
+ /** How a request proves its credential. */
45
+ export type WireCredential = Readonly<{
46
+ header: string;
47
+ prefix: string;
48
+ } | {
49
+ basic: true;
50
+ } | {
51
+ signedJwt: true;
52
+ maxAgeSeconds?: number;
53
+ }>;
54
+ /** A narrower form a format requires of its key. */
55
+ export type WireKeyShape = "uuid_v4" | "lowercase_slug";
56
+ /** Where a request carries its idempotency key. */
57
+ export type WireKey = Readonly<{
58
+ header: string;
59
+ maxLength?: number;
60
+ } | {
61
+ field: string;
62
+ maxLength?: number;
63
+ optional?: boolean;
64
+ shape?: WireKeyShape;
65
+ }>;
66
+ /** A second amount field and the field naming what it is counted in. */
67
+ export type WireAlternateAmount = Readonly<{
68
+ path: string;
69
+ currency: string;
70
+ }>;
71
+ /** How a decimal amount becomes minor units. */
72
+ export type WireAmount = Readonly<{
73
+ minorDigits: number;
74
+ } | {
75
+ scaleTerm: string;
76
+ scales: Readonly<Record<string, number>>;
77
+ networks?: Readonly<Record<string, readonly string[]>>;
78
+ /** Each token's EIP-55 contract, keyed `asset@network`. */
79
+ contracts?: Readonly<Record<string, string>>;
80
+ evmNetworks?: readonly string[];
81
+ }>;
82
+ /** Whether a declined key may be tried again, or its refusal stands. */
83
+ export type WireRefusals = "retryable" | "final";
84
+ /** The stand-in's declaration; `contracts/synthetic-provider.md` defines it. */
85
+ export type ProviderWire = Readonly<{
86
+ path: string;
87
+ encoding?: WireEncoding;
88
+ jsonType?: string;
89
+ credential?: WireCredential;
90
+ key: WireKey;
91
+ fields: Readonly<Record<string, string>>;
92
+ joined?: Readonly<Record<string, readonly string[]>>;
93
+ defaults?: TermDefaults;
94
+ fixed: Readonly<Record<string, string>>;
95
+ urlFields?: Readonly<Record<string, string>>;
96
+ singleEntry?: readonly string[];
97
+ amount?: WireAmount;
98
+ amountDefault?: string;
99
+ alternateAmount?: WireAlternateAmount;
100
+ pegged?: Readonly<Record<string, Readonly<Record<string, string>>>>;
101
+ replay?: WireReplay;
102
+ refusals?: WireRefusals;
103
+ replies: Readonly<{
104
+ paid: Reply;
105
+ declined: Reply;
106
+ conflict: Reply;
107
+ duplicate?: Reply;
108
+ }>;
109
+ }>;
110
+ /** A format of several calls, which the stand-in holds as its own flow. */
111
+ export type StagedWire = Readonly<{
112
+ flow: "wise";
113
+ fixed: WiseTerms;
114
+ scales: Readonly<Record<string, number>>;
115
+ routes: WiseRoutes;
116
+ }>;
117
+ /** Where Wise's three calls sit; a client with a dated base overrides them. */
118
+ export type WiseRoutes = Readonly<{
119
+ quotes: string;
120
+ transfers: string;
121
+ fund: string;
122
+ }>;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,117 @@
1
+ /**
2
+ * The adapter readings of a run, and the one place they are checked. The
3
+ * shape is `contracts/history-reading.md`; the host's mapping reads the same
4
+ * shape, and `contracts/history-reading.vectors.json` holds both to it.
5
+ */
6
+ declare const STATES: readonly ["reserved", "executing", "unknown", "needs_review", "failed", "confirmed"];
7
+ declare const RESULTS: readonly ["confirmed", "rejected", "unknown"];
8
+ declare const ENDS: readonly ["response", "timeout", "connection_lost", "error_status", "invalid_response"];
9
+ declare const INSTRUCTION_NAMES: readonly ["payment", "tenant", "agent", "delegation", "account", "destination", "currency", "asset", "network"];
10
+ export type PayoutState = (typeof STATES)[number];
11
+ export type AttemptResult = (typeof RESULTS)[number];
12
+ export type AttemptEnd = (typeof ENDS)[number];
13
+ /** Integer minor units as a decimal string, so no language loses precision. */
14
+ export type MinorUnits = string;
15
+ /** The payout's terms; `amount` is at least 1. */
16
+ export type Instruction = Readonly<Record<(typeof INSTRUCTION_NAMES)[number], string> & {
17
+ amount: MinorUnits;
18
+ }>;
19
+ /** One movement of the payout's amount. */
20
+ export type Flow = Readonly<{
21
+ amount: MinorUnits;
22
+ account: string;
23
+ }>;
24
+ export type HistoryEvent = Readonly<{
25
+ kind: "reserved";
26
+ flow: Flow;
27
+ }> | Readonly<{
28
+ kind: "submitted";
29
+ attempt?: string | null;
30
+ key: string;
31
+ terms?: Instruction | null;
32
+ }> | Readonly<{
33
+ kind: "outcome";
34
+ attempt?: string | null;
35
+ result: AttemptResult;
36
+ endedBy?: AttemptEnd | null;
37
+ effectId?: string | null;
38
+ }> | Readonly<{
39
+ kind: "consumed";
40
+ effectId?: string | null;
41
+ flow: Flow;
42
+ }> | Readonly<{
43
+ kind: "released";
44
+ flow: Flow;
45
+ }> | Readonly<{
46
+ kind: "recovered";
47
+ attempt?: string | null;
48
+ }> | Readonly<{
49
+ kind: "late_outcome";
50
+ attempt?: string | null;
51
+ effectId?: string | null;
52
+ }>;
53
+ export type Payout = Readonly<{
54
+ /** With the run's reference as `payment`. */
55
+ instruction: Instruction;
56
+ /** What the backend sends the provider as `payment`. */
57
+ providerPayment: string;
58
+ state: PayoutState;
59
+ /** Absent unless the backend reports it apart from the lifecycle. */
60
+ committed?: boolean | null;
61
+ attempts: number;
62
+ effectId?: string | null;
63
+ }>;
64
+ /** The order a history was read in. */
65
+ export type HistoryConsistency = "commit_order" | "logging_order";
66
+ /** Whether a capacity reading was taken at one instant. */
67
+ export type CapacityConsistency = "atomic" | "non_atomic";
68
+ /** A reading in commit order or stating otherwise; only commit order is usable. */
69
+ export type HistoryReading = Readonly<{
70
+ source: "adapter";
71
+ units: "minor";
72
+ consistency: HistoryConsistency;
73
+ complete: boolean;
74
+ payout: Payout;
75
+ events: readonly HistoryEvent[];
76
+ }>;
77
+ export type CapacityReading = Readonly<{
78
+ source: "adapter";
79
+ units: "minor";
80
+ consistency: CapacityConsistency;
81
+ payoutsInScope: number;
82
+ available: MinorUnits;
83
+ committed: MinorUnits;
84
+ consumed: MinorUnits;
85
+ }>;
86
+ /** The profiles an adapter reading is taken under. */
87
+ export type ReadingProfile = "payout-effects" | "payout-commitment";
88
+ /**
89
+ * A reading the host could not interpret. `field` is its path in the reading
90
+ * and `expected` what belongs there. The value found is described by type
91
+ * only, since a reading may hold customer data.
92
+ */
93
+ export declare class ReadingError extends TypeError {
94
+ readonly field: string;
95
+ readonly expected: string;
96
+ constructor(field: string, expected: string, found?: string);
97
+ }
98
+ /**
99
+ * Check an untrusted history reading against the contract.
100
+ *
101
+ * @throws {ReadingError} naming the first field that cannot be interpreted.
102
+ */
103
+ export declare function parseHistoryReading(value: unknown): HistoryReading;
104
+ /**
105
+ * Check an untrusted capacity reading against the contract.
106
+ *
107
+ * @throws {ReadingError} naming the first field that cannot be interpreted.
108
+ */
109
+ export declare function parseCapacityReading(value: unknown): CapacityReading;
110
+ /**
111
+ * Check a well-formed reading against its run: it is of `reference`, and a
112
+ * commitment was held before anything was sent, which `profile` models.
113
+ *
114
+ * @throws {ReadingError} when it does not fit.
115
+ */
116
+ export declare function requireHistoryFits(reading: HistoryReading, reference: string, profile: ReadingProfile): void;
117
+ export {};
@@ -0,0 +1,280 @@
1
+ /**
2
+ * The adapter readings of a run, and the one place they are checked. The
3
+ * shape is `contracts/history-reading.md`; the host's mapping reads the same
4
+ * shape, and `contracts/history-reading.vectors.json` holds both to it.
5
+ */
6
+ const STATES = [
7
+ "reserved",
8
+ "executing",
9
+ "unknown",
10
+ "needs_review",
11
+ "failed",
12
+ "confirmed",
13
+ ];
14
+ const RESULTS = ["confirmed", "rejected", "unknown"];
15
+ const ENDS = [
16
+ "response",
17
+ "timeout",
18
+ "connection_lost",
19
+ "error_status",
20
+ "invalid_response",
21
+ ];
22
+ const KINDS = [
23
+ "reserved",
24
+ "submitted",
25
+ "outcome",
26
+ "consumed",
27
+ "released",
28
+ "recovered",
29
+ "late_outcome",
30
+ ];
31
+ const INSTRUCTION_NAMES = [
32
+ "payment",
33
+ "tenant",
34
+ "agent",
35
+ "delegation",
36
+ "account",
37
+ "destination",
38
+ "currency",
39
+ "asset",
40
+ "network",
41
+ ];
42
+ const MINOR_UNITS = /^(0|[1-9][0-9]*)$/;
43
+ /** The host's amounts are 128 bits wide. */
44
+ const MAX_AMOUNT = 2n ** 128n - 1n;
45
+ const MAX_ATTEMPTS = 2 ** 32 - 1;
46
+ /** How many lifecycle events the host reads from one history. */
47
+ const MAX_EVENTS = 200;
48
+ /**
49
+ * A reading the host could not interpret. `field` is its path in the reading
50
+ * and `expected` what belongs there. The value found is described by type
51
+ * only, since a reading may hold customer data.
52
+ */
53
+ export class ReadingError extends TypeError {
54
+ field;
55
+ expected;
56
+ constructor(field, expected, found) {
57
+ super(`${field}: expected ${expected}${found === undefined ? "" : `, found ${found}`}`);
58
+ this.field = field;
59
+ this.expected = expected;
60
+ this.name = "ReadingError";
61
+ }
62
+ }
63
+ const describe = (value) => {
64
+ if (value === undefined)
65
+ return "nothing";
66
+ if (value === null)
67
+ return "null";
68
+ return Array.isArray(value) ? "an array" : `a ${typeof value}`;
69
+ };
70
+ const refuse = (field, expected, value) => {
71
+ throw new ReadingError(field, expected, describe(value));
72
+ };
73
+ const quoted = (allowed) => `one of ${allowed.map((name) => `"${name}"`).join(", ")}`;
74
+ const at = (parent, name) => parent === "" ? name : `${parent}.${name}`;
75
+ /** `value` as an object holding only `allowed` members. */
76
+ function members(value, field, allowed) {
77
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
78
+ return refuse(field || "reading", `an object`, value);
79
+ }
80
+ const unknown = Object.keys(value).find((name) => !allowed.includes(name));
81
+ if (unknown !== undefined) {
82
+ throw new ReadingError(at(field, unknown), `no such member: the members here are ${allowed.join(", ")}`);
83
+ }
84
+ return value;
85
+ }
86
+ function requireText(value, field) {
87
+ if (typeof value !== "string")
88
+ refuse(field, "a string", value);
89
+ }
90
+ function requireOneOf(allowed, value, field) {
91
+ if (typeof value !== "string" || !allowed.includes(value)) {
92
+ refuse(field, quoted(allowed), value);
93
+ }
94
+ }
95
+ function requireExactly(wanted, value, field) {
96
+ if (value !== wanted)
97
+ refuse(field, `"${wanted}"`, value);
98
+ }
99
+ function requireUnits(value, field, least) {
100
+ const expected = `a canonical decimal string of minor units, at least ${String(least)}, such as "10000"`;
101
+ if (typeof value !== "string" || !MINOR_UNITS.test(value)) {
102
+ refuse(field, expected, value);
103
+ return;
104
+ }
105
+ const units = BigInt(value);
106
+ if (units < least || units > MAX_AMOUNT)
107
+ refuse(field, expected, "a string");
108
+ }
109
+ /** Absent and `null` both mean the backend did not record it. */
110
+ const recorded = (value) => value !== undefined && value !== null;
111
+ function optionalText(value, field) {
112
+ if (recorded(value) && typeof value !== "string") {
113
+ refuse(field, "a string, null or nothing", value);
114
+ }
115
+ }
116
+ function requireInstruction(value, field) {
117
+ const found = members(value, field, [...INSTRUCTION_NAMES, "amount"]);
118
+ for (const name of INSTRUCTION_NAMES) {
119
+ requireText(found[name], at(field, name));
120
+ }
121
+ requireUnits(found["amount"], at(field, "amount"), 1n);
122
+ }
123
+ function requireFlow(value, field) {
124
+ const found = members(value, field, ["amount", "account"]);
125
+ requireUnits(found["amount"], at(field, "amount"), 0n);
126
+ requireText(found["account"], at(field, "account"));
127
+ }
128
+ function requirePayout(value) {
129
+ const found = members(value, "payout", [
130
+ "instruction",
131
+ "providerPayment",
132
+ "state",
133
+ "committed",
134
+ "attempts",
135
+ "effectId",
136
+ ]);
137
+ requireInstruction(found["instruction"], "payout.instruction");
138
+ requireText(found["providerPayment"], "payout.providerPayment");
139
+ requireOneOf(STATES, found["state"], "payout.state");
140
+ const { committed, attempts } = found;
141
+ if (recorded(committed) && typeof committed !== "boolean") {
142
+ refuse("payout.committed", "a boolean, null or nothing", committed);
143
+ }
144
+ if (!Number.isInteger(attempts) ||
145
+ attempts < 0 ||
146
+ attempts > MAX_ATTEMPTS) {
147
+ refuse("payout.attempts", "the number of attempts as an integer", attempts);
148
+ }
149
+ optionalText(found["effectId"], "payout.effectId");
150
+ }
151
+ function requireEvent(value, field) {
152
+ const kind = value?.["kind"];
153
+ requireOneOf(KINDS, kind, at(field, "kind"));
154
+ switch (kind) {
155
+ case "reserved":
156
+ case "released": {
157
+ const found = members(value, field, ["kind", "flow"]);
158
+ requireFlow(found["flow"], at(field, "flow"));
159
+ return;
160
+ }
161
+ case "submitted": {
162
+ const found = members(value, field, ["kind", "attempt", "key", "terms"]);
163
+ optionalText(found["attempt"], at(field, "attempt"));
164
+ requireText(found["key"], at(field, "key"));
165
+ if (recorded(found["terms"])) {
166
+ requireInstruction(found["terms"], at(field, "terms"));
167
+ }
168
+ return;
169
+ }
170
+ case "outcome": {
171
+ const found = members(value, field, [
172
+ "kind",
173
+ "attempt",
174
+ "result",
175
+ "endedBy",
176
+ "effectId",
177
+ ]);
178
+ optionalText(found["attempt"], at(field, "attempt"));
179
+ requireOneOf(RESULTS, found["result"], at(field, "result"));
180
+ if (recorded(found["endedBy"])) {
181
+ requireOneOf(ENDS, found["endedBy"], at(field, "endedBy"));
182
+ }
183
+ optionalText(found["effectId"], at(field, "effectId"));
184
+ return;
185
+ }
186
+ case "consumed": {
187
+ const found = members(value, field, ["kind", "effectId", "flow"]);
188
+ optionalText(found["effectId"], at(field, "effectId"));
189
+ requireFlow(found["flow"], at(field, "flow"));
190
+ return;
191
+ }
192
+ case "recovered": {
193
+ const found = members(value, field, ["kind", "attempt"]);
194
+ optionalText(found["attempt"], at(field, "attempt"));
195
+ return;
196
+ }
197
+ case "late_outcome": {
198
+ const found = members(value, field, ["kind", "attempt", "effectId"]);
199
+ optionalText(found["attempt"], at(field, "attempt"));
200
+ optionalText(found["effectId"], at(field, "effectId"));
201
+ }
202
+ }
203
+ }
204
+ /**
205
+ * Check an untrusted history reading against the contract.
206
+ *
207
+ * @throws {ReadingError} naming the first field that cannot be interpreted.
208
+ */
209
+ export function parseHistoryReading(value) {
210
+ const found = members(value, "", [
211
+ "source",
212
+ "units",
213
+ "consistency",
214
+ "complete",
215
+ "payout",
216
+ "events",
217
+ ]);
218
+ requireExactly("adapter", found["source"], "source");
219
+ requireExactly("minor", found["units"], "units");
220
+ requireExactly("commit_order", found["consistency"], "consistency");
221
+ if (typeof found["complete"] !== "boolean") {
222
+ refuse("complete", "a boolean", found["complete"]);
223
+ }
224
+ requirePayout(found["payout"]);
225
+ const { events } = found;
226
+ if (!Array.isArray(events) || events.length > MAX_EVENTS) {
227
+ return refuse("events", `an array of at most ${String(MAX_EVENTS)} events`, events);
228
+ }
229
+ events.forEach((event, position) => {
230
+ requireEvent(event, `events[${String(position)}]`);
231
+ });
232
+ return value;
233
+ }
234
+ /**
235
+ * Check an untrusted capacity reading against the contract.
236
+ *
237
+ * @throws {ReadingError} naming the first field that cannot be interpreted.
238
+ */
239
+ export function parseCapacityReading(value) {
240
+ const found = members(value, "", [
241
+ "source",
242
+ "units",
243
+ "consistency",
244
+ "payoutsInScope",
245
+ "available",
246
+ "committed",
247
+ "consumed",
248
+ ]);
249
+ requireExactly("adapter", found["source"], "source");
250
+ requireExactly("minor", found["units"], "units");
251
+ requireExactly("atomic", found["consistency"], "consistency");
252
+ const scope = found["payoutsInScope"];
253
+ if (!Number.isInteger(scope) ||
254
+ scope < 0 ||
255
+ scope > MAX_ATTEMPTS) {
256
+ refuse("payoutsInScope", "the number of payouts as an integer", scope);
257
+ }
258
+ for (const name of ["available", "committed", "consumed"]) {
259
+ requireUnits(found[name], name, 0n);
260
+ }
261
+ return value;
262
+ }
263
+ /**
264
+ * Check a well-formed reading against its run: it is of `reference`, and a
265
+ * commitment was held before anything was sent, which `profile` models.
266
+ *
267
+ * @throws {ReadingError} when it does not fit.
268
+ */
269
+ export function requireHistoryFits(reading, reference, profile) {
270
+ const payment = reading.payout.instruction.payment;
271
+ if (payment !== reference) {
272
+ throw new ReadingError("payout.instruction.payment", `the run's reference "${reference}"`, "another payout's");
273
+ }
274
+ const kinds = reading.events.map((event) => event.kind);
275
+ const submitted = kinds.indexOf("submitted");
276
+ const reserved = kinds.indexOf("reserved");
277
+ if (submitted >= 0 && (reserved < 0 || reserved > submitted)) {
278
+ throw new ReadingError("events", `a "reserved" event before the first "submitted": ${profile} cannot be used by a backend without reservations; use payout-effects-only`);
279
+ }
280
+ }
package/package.json CHANGED
@@ -1,6 +1,66 @@
1
1
  {
2
2
  "name": "@mandateops/audit",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.2.0",
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
+ "license": "Apache-2.0",
6
+ "homepage": "https://getmandateops.com",
7
+ "keywords": [
8
+ "payments",
9
+ "payouts",
10
+ "idempotency",
11
+ "testing",
12
+ "fault-injection",
13
+ "audit"
14
+ ],
15
+ "type": "module",
16
+ "files": [
17
+ "dist",
18
+ "README.md",
19
+ "LICENSE"
20
+ ],
21
+ "packageManager": "bun@1.4.4",
22
+ "engines": {
23
+ "node": ">=22",
24
+ "bun": ">=1.4.4"
25
+ },
26
+ "exports": {
27
+ ".": {
28
+ "types": "./dist/index.d.ts",
29
+ "default": "./dist/index.js"
30
+ },
31
+ "./bun": {
32
+ "types": "./dist/bun.d.ts",
33
+ "default": "./dist/bun.js"
34
+ }
35
+ },
36
+ "peerDependencies": {
37
+ "@types/bun": ">=1.4.4"
38
+ },
39
+ "peerDependenciesMeta": {
40
+ "@types/bun": {
41
+ "optional": true
42
+ }
43
+ },
44
+ "scripts": {
45
+ "build": "rm -rf dist && bun --bun run tsc --project tsconfig.build.json",
46
+ "lint": "bun --bun run eslint . --max-warnings 0",
47
+ "typecheck": "bun --bun run tsc --noEmit",
48
+ "format:check": "bun --bun run prettier --check .",
49
+ "test": "bun test ./test/reading.test.ts ./test/session.test.ts ./test/index.test.ts ./test/bun.test.ts ./test/vectors.test.ts ./scripts/gates.test.mjs",
50
+ "test:audit": "bun test ./audit/remit.audit.test.ts ./audit/settle.audit.test.ts ./audit/effects-only.audit.test.ts ./audit/hostile.audit.test.ts ./audit/paystack.audit.test.ts ./audit/bridge.audit.test.ts ./audit/circle.audit.test.ts ./audit/fireblocks.audit.test.ts ./audit/stripe.audit.test.ts ./audit/wise.audit.test.ts --timeout 400000",
51
+ "check": "bun run format:check && bun run lint && bun run typecheck && bun run test",
52
+ "prepublishOnly": "bun run check && bun run build"
53
+ },
54
+ "devDependencies": {
55
+ "@types/bun": "1.4.4",
56
+ "@types/node": "26.6.6",
57
+ "eslint": "10.14.0",
58
+ "parse-diff": "0.12.0",
59
+ "prettier": "3.9.10",
60
+ "typescript": "5.9.3",
61
+ "typescript-eslint": "8.72.1"
62
+ },
63
+ "publishConfig": {
64
+ "access": "public"
65
+ }
66
+ }