@forgezero/access 0.1.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/LICENSE +21 -0
- package/README.md +75 -0
- package/dist/authenticator.d.ts +118 -0
- package/dist/authenticator.js +186 -0
- package/dist/ceremony-modes.d.ts +71 -0
- package/dist/ceremony-modes.js +55 -0
- package/dist/client.d.ts +76 -0
- package/dist/client.js +107 -0
- package/dist/conditions.d.ts +280 -0
- package/dist/conditions.js +491 -0
- package/dist/effects.d.ts +168 -0
- package/dist/effects.js +505 -0
- package/dist/elysia.d.ts +78 -0
- package/dist/elysia.js +575 -0
- package/dist/fetch.d.ts +41 -0
- package/dist/fetch.js +587 -0
- package/dist/index.d.ts +379 -0
- package/dist/index.js +298 -0
- package/dist/pipeline.d.ts +92 -0
- package/dist/pipeline.js +528 -0
- package/dist/rate-limit.d.ts +54 -0
- package/dist/rate-limit.js +89 -0
- package/dist/security.d.ts +102 -0
- package/dist/security.js +141 -0
- package/dist/testing.d.ts +63 -0
- package/dist/testing.js +357 -0
- package/package.json +93 -0
package/dist/client.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// src/client.ts
|
|
2
|
+
function buildPath(routeKey, params = {}) {
|
|
3
|
+
return "/" + routeKey.split("/").map((segment) => {
|
|
4
|
+
const named = /^\[(?:\.\.\.)?(\w+)\]$/.exec(segment);
|
|
5
|
+
if (!named)
|
|
6
|
+
return segment;
|
|
7
|
+
const value = params[named[1]];
|
|
8
|
+
if (value === undefined) {
|
|
9
|
+
throw new Error(`Missing route parameter "${named[1]}" for ${routeKey}`);
|
|
10
|
+
}
|
|
11
|
+
return encodeURIComponent(value);
|
|
12
|
+
}).join("/");
|
|
13
|
+
}
|
|
14
|
+
function withQuery(path, query) {
|
|
15
|
+
if (!query)
|
|
16
|
+
return path;
|
|
17
|
+
const search = new URLSearchParams;
|
|
18
|
+
for (const [name, value] of Object.entries(query)) {
|
|
19
|
+
if (value !== undefined && value !== null)
|
|
20
|
+
search.set(name, String(value));
|
|
21
|
+
}
|
|
22
|
+
const encoded = search.toString();
|
|
23
|
+
return encoded ? `${path}?${encoded}` : path;
|
|
24
|
+
}
|
|
25
|
+
function createClient(routes, options) {
|
|
26
|
+
const doFetch = options.fetch ?? globalThis.fetch;
|
|
27
|
+
const fulfilPath = options.fulfilPath ?? "/api/security/fulfil";
|
|
28
|
+
async function send(routeKey, method, args, requestKey) {
|
|
29
|
+
const path = withQuery(buildPath(routeKey, args.params), args.query);
|
|
30
|
+
const response = await doFetch(options.baseUrl + path, {
|
|
31
|
+
method,
|
|
32
|
+
credentials: "include",
|
|
33
|
+
headers: {
|
|
34
|
+
"content-type": "application/json",
|
|
35
|
+
...options.headers,
|
|
36
|
+
...args.headers,
|
|
37
|
+
...requestKey ? { "x-security-request-key": requestKey } : {}
|
|
38
|
+
},
|
|
39
|
+
body: args.body === undefined ? undefined : JSON.stringify(args.body)
|
|
40
|
+
});
|
|
41
|
+
const payload = await response.json().catch(() => {
|
|
42
|
+
return;
|
|
43
|
+
});
|
|
44
|
+
if (response.ok) {
|
|
45
|
+
return { ok: true, status: response.status, data: payload };
|
|
46
|
+
}
|
|
47
|
+
if (response.status === 428) {
|
|
48
|
+
const key = response.headers.get("x-security-request-key");
|
|
49
|
+
if (key) {
|
|
50
|
+
return {
|
|
51
|
+
ok: false,
|
|
52
|
+
status: 428,
|
|
53
|
+
error: payload?.error ?? { code: "SECURITY_REQUIRED", message: "Security check required." },
|
|
54
|
+
errors: payload?.security ? [{ path: "", message: JSON.stringify({ ...payload.security, requestKey: key }) }] : undefined
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
ok: false,
|
|
60
|
+
status: response.status,
|
|
61
|
+
error: payload?.error ?? { code: "REQUEST_FAILED", message: response.statusText },
|
|
62
|
+
errors: payload?.errors
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
async function call(routeKey, method, args) {
|
|
66
|
+
const first = await send(routeKey, method, args);
|
|
67
|
+
if (first.ok || first.status !== 428 || !options.onChallenge)
|
|
68
|
+
return first;
|
|
69
|
+
const encoded = first.errors?.[0]?.message;
|
|
70
|
+
if (!encoded)
|
|
71
|
+
return first;
|
|
72
|
+
const challenge = JSON.parse(encoded);
|
|
73
|
+
const proofs = await options.onChallenge(challenge);
|
|
74
|
+
if (!proofs?.length)
|
|
75
|
+
return first;
|
|
76
|
+
const fulfilled = await doFetch(options.baseUrl + fulfilPath, {
|
|
77
|
+
method: "POST",
|
|
78
|
+
credentials: "include",
|
|
79
|
+
headers: { "content-type": "application/json", ...options.headers },
|
|
80
|
+
body: JSON.stringify({ requestKey: challenge.requestKey, proofs })
|
|
81
|
+
});
|
|
82
|
+
if (!fulfilled.ok) {
|
|
83
|
+
const payload = await fulfilled.json().catch(() => {
|
|
84
|
+
return;
|
|
85
|
+
});
|
|
86
|
+
return {
|
|
87
|
+
ok: false,
|
|
88
|
+
status: fulfilled.status,
|
|
89
|
+
error: payload?.error ?? { code: "SECURITY_FAILED", message: "The security check was not accepted." }
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
return send(routeKey, method, args, challenge.requestKey);
|
|
93
|
+
}
|
|
94
|
+
const api = {};
|
|
95
|
+
for (const key of Object.keys(routes)) {
|
|
96
|
+
const route = routes[key];
|
|
97
|
+
if (route.kind !== "action")
|
|
98
|
+
continue;
|
|
99
|
+
const method = route.method;
|
|
100
|
+
api[key] = (args = {}) => call(key, method, args);
|
|
101
|
+
}
|
|
102
|
+
return api;
|
|
103
|
+
}
|
|
104
|
+
export {
|
|
105
|
+
createClient,
|
|
106
|
+
buildPath
|
|
107
|
+
};
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import { type BeforeHandler, type RequestContext } from './index';
|
|
2
|
+
/**
|
|
3
|
+
* Conditions — the reusable `before` catalog.
|
|
4
|
+
*
|
|
5
|
+
* Every project rewrites the same twelve guards: load the record or 404, check
|
|
6
|
+
* it belongs to the caller, check its status allows this transition, check the
|
|
7
|
+
* balance, check the idempotency key, check the version. Written per route they
|
|
8
|
+
* drift; written per project they drift between projects. Written once here
|
|
9
|
+
* they are one thing to get right, and every project that adopts them inherits
|
|
10
|
+
* the fixes.
|
|
11
|
+
*
|
|
12
|
+
* ## What a condition is
|
|
13
|
+
*
|
|
14
|
+
* A `BeforeHandler` that either merges state or throws a `Refusal` carrying the
|
|
15
|
+
* status the client deserves. It never returns a response and never decides
|
|
16
|
+
* business outcomes — it decides whether the handler is allowed to run at all.
|
|
17
|
+
*
|
|
18
|
+
* ## Why they declare their refusals
|
|
19
|
+
*
|
|
20
|
+
* Each carries `refusals`, so the set of statuses a route can produce is
|
|
21
|
+
* derivable rather than hand-maintained. `assertDeclaredRefusals` uses it to
|
|
22
|
+
* fail the build when a route's `response` map omits a status its own
|
|
23
|
+
* conditions can throw — the drift that produces a client crashing on a 409 it
|
|
24
|
+
* was never told about.
|
|
25
|
+
*
|
|
26
|
+
* ## Order matters and is not enforced here
|
|
27
|
+
*
|
|
28
|
+
* `loadTarget` must precede anything reading the record. The pipeline runs the
|
|
29
|
+
* chain in declaration order and merges state as it goes, so the ordering lives
|
|
30
|
+
* in the declaration list where it is visible, not in a dependency graph nobody
|
|
31
|
+
* reads.
|
|
32
|
+
*/
|
|
33
|
+
export interface RefusalSpec {
|
|
34
|
+
status: number;
|
|
35
|
+
code: string;
|
|
36
|
+
/** One clause, present tense: what has to be true for this to fire. */
|
|
37
|
+
when: string;
|
|
38
|
+
}
|
|
39
|
+
export interface Condition<K extends string> extends BeforeHandler<K> {
|
|
40
|
+
/** Stable name — appears in flow documentation and telemetry. */
|
|
41
|
+
condition: string;
|
|
42
|
+
refusals: readonly RefusalSpec[];
|
|
43
|
+
}
|
|
44
|
+
type State = Record<string, unknown>;
|
|
45
|
+
type Routes<K extends string> = {
|
|
46
|
+
routes: readonly K[];
|
|
47
|
+
};
|
|
48
|
+
export interface LoadTargetOptions<K extends string> extends Routes<K> {
|
|
49
|
+
/** State key the record lands under. Defaults to `target`. */
|
|
50
|
+
as?: string;
|
|
51
|
+
/** Where the identifier comes from. Defaults to a `id` path parameter. */
|
|
52
|
+
id?: (context: RequestContext) => string | undefined;
|
|
53
|
+
load: (id: string, context: RequestContext) => Promise<unknown> | unknown;
|
|
54
|
+
/** Refusal code. Defaults to NOT_FOUND — see the note on 404 below. */
|
|
55
|
+
code?: string;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Load the record this request acts on, or 404.
|
|
59
|
+
*
|
|
60
|
+
* First in almost every chain, for two reasons beyond the obvious. Step 8 binds
|
|
61
|
+
* a step-up proof to a specific record, which requires the record; and a
|
|
62
|
+
* condition that reads the database twice — once to check, once in the handler —
|
|
63
|
+
* is a TOCTOU window somebody will eventually land in.
|
|
64
|
+
*/
|
|
65
|
+
export declare function loadTarget<K extends string>(options: LoadTargetOptions<K>): Condition<K>;
|
|
66
|
+
export interface RequireOwnerOptions<K extends string> extends Routes<K> {
|
|
67
|
+
of?: string;
|
|
68
|
+
owner: (record: never) => string | undefined;
|
|
69
|
+
/** Defaults to the session user. A tenant app passes the realm instead. */
|
|
70
|
+
subject?: (context: RequestContext) => string | undefined;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* The record belongs to the caller — otherwise 404, deliberately not 403.
|
|
74
|
+
*
|
|
75
|
+
* A 403 confirms the record exists, which turns any sequential identifier into
|
|
76
|
+
* an enumeration oracle: an attacker walks the range and learns exactly which
|
|
77
|
+
* accounts, orders or entries are real. Answering 404 makes "not yours" and
|
|
78
|
+
* "not there" indistinguishable, which is the only honest answer to give
|
|
79
|
+
* somebody who should not know either way.
|
|
80
|
+
*/
|
|
81
|
+
export declare function requireOwner<K extends string>(options: RequireOwnerOptions<K>): Condition<K>;
|
|
82
|
+
export interface RequireStateOptions<K extends string> extends Routes<K> {
|
|
83
|
+
of?: string;
|
|
84
|
+
status: (record: never) => string;
|
|
85
|
+
allowed: readonly string[];
|
|
86
|
+
code?: string;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* The record is in a status this transition accepts, or 409.
|
|
90
|
+
*
|
|
91
|
+
* The guard that stops a withdrawal being approved twice, an invoice being
|
|
92
|
+
* settled after it was voided, a ceremony activating from a state it never
|
|
93
|
+
* reached. Checking status inside the handler works right up to the second
|
|
94
|
+
* concurrent request, and this at least fails them both loudly and with the
|
|
95
|
+
* allowed set in the payload so the UI can explain itself.
|
|
96
|
+
*/
|
|
97
|
+
export declare function requireState<K extends string>(options: RequireStateOptions<K>): Condition<K>;
|
|
98
|
+
export interface RequireVersionOptions<K extends string> extends Routes<K> {
|
|
99
|
+
of?: string;
|
|
100
|
+
version: (record: never) => string | number;
|
|
101
|
+
/** Defaults to `If-Match`, falling back to a `version` body field. */
|
|
102
|
+
expected?: (context: RequestContext) => string | number | undefined;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Optimistic concurrency — 412 when the record moved under the caller.
|
|
106
|
+
*
|
|
107
|
+
* Last-write-wins is not a decision anybody makes on purpose; it is what you get
|
|
108
|
+
* by not making one. Two operators editing the same policy silently lose one of
|
|
109
|
+
* the edits, and nothing anywhere records that it happened.
|
|
110
|
+
*/
|
|
111
|
+
export declare function requireVersion<K extends string>(options: RequireVersionOptions<K>): Condition<K>;
|
|
112
|
+
export interface IdempotencyStore {
|
|
113
|
+
/**
|
|
114
|
+
* Claim the key. `fresh` means proceed; `replay` carries the stored answer;
|
|
115
|
+
* `pending` means an identical request is still running.
|
|
116
|
+
*
|
|
117
|
+
* Must be atomic — a get-then-set implementation reintroduces exactly the
|
|
118
|
+
* double-spend this exists to prevent.
|
|
119
|
+
*/
|
|
120
|
+
claim(key: string, route: string): Promise<{
|
|
121
|
+
state: 'fresh';
|
|
122
|
+
} | {
|
|
123
|
+
state: 'replay';
|
|
124
|
+
value: unknown;
|
|
125
|
+
status: number;
|
|
126
|
+
} | {
|
|
127
|
+
state: 'pending';
|
|
128
|
+
}>;
|
|
129
|
+
}
|
|
130
|
+
export interface RequireIdempotencyOptions<K extends string> extends Routes<K> {
|
|
131
|
+
store: IdempotencyStore;
|
|
132
|
+
/** Defaults to the `Idempotency-Key` header. */
|
|
133
|
+
key?: (context: RequestContext) => string | undefined;
|
|
134
|
+
/** True refuses a request without a key. Correct for anything that moves money. */
|
|
135
|
+
strict?: boolean;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* At-most-once, for requests that must not happen twice.
|
|
139
|
+
*
|
|
140
|
+
* A retry after a timeout is not a client bug — it is what every HTTP client and
|
|
141
|
+
* every mobile network does. Without a claim the second attempt places a second
|
|
142
|
+
* order, and the user sees one confirmation for two withdrawals.
|
|
143
|
+
*
|
|
144
|
+
* A replay throws `Settled`, so the caller gets the ORIGINAL answer rather than
|
|
145
|
+
* an error. A 409 would be defensible and useless: a retrying client still would
|
|
146
|
+
* not learn the transfer succeeded.
|
|
147
|
+
*/
|
|
148
|
+
export declare function requireIdempotency<K extends string>(options: RequireIdempotencyOptions<K>): Condition<K>;
|
|
149
|
+
export interface RequireUnlockedOptions<K extends string> extends Routes<K> {
|
|
150
|
+
unlocked: () => boolean | Promise<boolean>;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* The vault is unlocked, or 423.
|
|
154
|
+
*
|
|
155
|
+
* 423 rather than 503 because the distinction is actionable: 503 says wait, 423
|
|
156
|
+
* says a human must unlock something. A client that retries a 423 forever is a
|
|
157
|
+
* client nobody told the difference.
|
|
158
|
+
*/
|
|
159
|
+
export declare function requireUnlocked<K extends string>(options: RequireUnlockedOptions<K>): Condition<K>;
|
|
160
|
+
export interface RequireServiceOptions<K extends string> extends Routes<K> {
|
|
161
|
+
service: string;
|
|
162
|
+
/** Satisfied by a `@forgezero/providers` registry health check. */
|
|
163
|
+
healthy: (service: string) => boolean | Promise<boolean>;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* A provider for this service is reachable, or 503 — checked before the handler
|
|
167
|
+
* does work it will have to undo.
|
|
168
|
+
*
|
|
169
|
+
* The failure this prevents is subtle: a route that writes a record and then
|
|
170
|
+
* sends an email leaves the record behind when every relay is down, and the
|
|
171
|
+
* user retries into a duplicate.
|
|
172
|
+
*/
|
|
173
|
+
export declare function requireService<K extends string>(options: RequireServiceOptions<K>): Condition<K>;
|
|
174
|
+
export interface RequireNotFrozenOptions<K extends string> extends Routes<K> {
|
|
175
|
+
/** Return a reason to freeze; undefined to allow. */
|
|
176
|
+
frozen: (context: RequestContext, state: State) => Promise<string | undefined> | string | undefined;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Nothing has administratively halted this subject, or 423.
|
|
180
|
+
*
|
|
181
|
+
* Separate from status because it is orthogonal: an order can be perfectly valid
|
|
182
|
+
* and still not permitted to move because the account is under review, the asset
|
|
183
|
+
* is halted, or an operator pulled the switch. Folding it into a status enum
|
|
184
|
+
* means every state machine grows a `frozen` member and every transition has to
|
|
185
|
+
* remember it.
|
|
186
|
+
*/
|
|
187
|
+
export declare function requireNotFrozen<K extends string>(options: RequireNotFrozenOptions<K>): Condition<K>;
|
|
188
|
+
export interface RequireTierOptions<K extends string> extends Routes<K> {
|
|
189
|
+
atLeast: number;
|
|
190
|
+
tier: (context: RequestContext, state: State) => Promise<number> | number;
|
|
191
|
+
/** Names the ladder, so the client knows where to send the user. */
|
|
192
|
+
ladder?: string;
|
|
193
|
+
code?: string;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* The subject has reached a level, or 403 naming the gap.
|
|
197
|
+
*
|
|
198
|
+
* One mechanism, two uses that are the same shape: a plan tier gating a feature,
|
|
199
|
+
* and a verification tier gating an amount. Both are "a monotonic level the
|
|
200
|
+
* subject must have reached", and both need the CURRENT level in the refusal or
|
|
201
|
+
* the UI cannot render anything better than "denied".
|
|
202
|
+
*/
|
|
203
|
+
export declare function requireTier<K extends string>(options: RequireTierOptions<K>): Condition<K>;
|
|
204
|
+
export interface RequireBalanceOptions<K extends string> extends Routes<K> {
|
|
205
|
+
/** Minor units. Never a float — see the note below. */
|
|
206
|
+
amount: (context: RequestContext, state: State) => Promise<bigint> | bigint;
|
|
207
|
+
available: (context: RequestContext, state: State) => Promise<bigint> | bigint;
|
|
208
|
+
asset?: (context: RequestContext, state: State) => string | undefined;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Enough available balance, or 409.
|
|
212
|
+
*
|
|
213
|
+
* `bigint` minor units, not `number`. A double cannot hold 0.1 + 0.2, and it
|
|
214
|
+
* cannot hold satoshi precision above ~90 million BTC-equivalents at all; the
|
|
215
|
+
* rounding shows up as a ledger that fails to balance by a few units a month
|
|
216
|
+
* and nobody can say why. Refusing a float at the type level is cheaper than
|
|
217
|
+
* finding that later.
|
|
218
|
+
*
|
|
219
|
+
* This checks AVAILABLE — free of holds — not total. A balance check against
|
|
220
|
+
* total lets two concurrent withdrawals each pass and the second overdraws.
|
|
221
|
+
*/
|
|
222
|
+
export declare function requireBalance<K extends string>(options: RequireBalanceOptions<K>): Condition<K>;
|
|
223
|
+
export interface RequireQuotaOptions<K extends string> extends Routes<K> {
|
|
224
|
+
/** `24h`, `30d` — documentation for the client, the store owns the window. */
|
|
225
|
+
window: string;
|
|
226
|
+
cap: (context: RequestContext, state: State) => Promise<bigint> | bigint;
|
|
227
|
+
amount: (context: RequestContext, state: State) => Promise<bigint> | bigint;
|
|
228
|
+
used: (context: RequestContext, state: State) => Promise<bigint> | bigint;
|
|
229
|
+
code?: string;
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* A value cap over a window, which is NOT a rate limit.
|
|
233
|
+
*
|
|
234
|
+
* Rate limiting counts requests and protects the service; a quota counts value
|
|
235
|
+
* and protects the account. One withdrawal of the entire balance passes every
|
|
236
|
+
* rate limit ever written. They also fail differently — 429 means slow down,
|
|
237
|
+
* this means the answer will be the same until the window rolls — so they get
|
|
238
|
+
* different codes and the reset time is in the payload.
|
|
239
|
+
*/
|
|
240
|
+
export declare function requireQuota<K extends string>(options: RequireQuotaOptions<K>): Condition<K>;
|
|
241
|
+
export interface ApprovalState {
|
|
242
|
+
status: 'pending' | 'approved' | 'rejected';
|
|
243
|
+
approvals?: number;
|
|
244
|
+
required?: number;
|
|
245
|
+
reason?: string;
|
|
246
|
+
}
|
|
247
|
+
export interface RequireApprovalOptions<K extends string> extends Routes<K> {
|
|
248
|
+
approval: (context: RequestContext, state: State) => Promise<ApprovalState> | ApprovalState;
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Somebody ELSE approved this — which is not what action factors check.
|
|
252
|
+
*
|
|
253
|
+
* Action factors make the actor prove themselves freshly. This makes a second
|
|
254
|
+
* party sign off, and no amount of the first kind substitutes for the second:
|
|
255
|
+
* that is the entire point of four-eyes on a large withdrawal or a custodian
|
|
256
|
+
* change. Two mechanisms because they answer different questions, and merging
|
|
257
|
+
* them would let a sufficiently authenticated operator approve their own move.
|
|
258
|
+
*/
|
|
259
|
+
export declare function requireApproval<K extends string>(options: RequireApprovalOptions<K>): Condition<K>;
|
|
260
|
+
/**
|
|
261
|
+
* Every status a route's conditions can throw is declared in its contract.
|
|
262
|
+
*
|
|
263
|
+
* The drift this catches: somebody adds `requireQuota` to a withdrawal route,
|
|
264
|
+
* the generated client never learns 409 is possible, and the UI renders "an
|
|
265
|
+
* unexpected error occurred" for the one refusal a user can actually act on.
|
|
266
|
+
* Cheap to check, invisible until a customer hits it.
|
|
267
|
+
*/
|
|
268
|
+
export declare function assertDeclaredRefusals(args: {
|
|
269
|
+
routes: Record<string, {
|
|
270
|
+
response?: Record<number, unknown>;
|
|
271
|
+
}>;
|
|
272
|
+
conditions: readonly Condition<string>[];
|
|
273
|
+
/** Statuses handled centrally rather than per route. */
|
|
274
|
+
ignore?: readonly number[];
|
|
275
|
+
}): void;
|
|
276
|
+
/** Every refusal a route can produce, for documentation and generated clients. */
|
|
277
|
+
export declare function refusalsFor(routeKey: string, conditions: readonly Condition<string>[]): readonly (RefusalSpec & {
|
|
278
|
+
condition: string;
|
|
279
|
+
})[];
|
|
280
|
+
export {};
|