@decocms/bindings 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/README.md +680 -0
- package/dist/index.d.ts +90 -0
- package/dist/index.js +79 -0
- package/dist/index.js.map +1 -0
- package/package.json +41 -0
- package/src/core/binder.ts +221 -0
- package/src/index.ts +16 -0
- package/src/well-known/collections.ts +403 -0
- package/src/well-known/models.ts +69 -0
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { ToolBinder } from "../core/binder";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Collection Bindings
|
|
6
|
+
*
|
|
7
|
+
* This module provides standardized tool bindings for Collections, representing
|
|
8
|
+
* SQL table-like structures with CRUD + Search operations compatible with TanStack DB.
|
|
9
|
+
*
|
|
10
|
+
* Key Features:
|
|
11
|
+
* - Generic collection bindings that work with any entity type
|
|
12
|
+
* - Standardized tool naming: `DECO_COLLECTION_{COLLECTION}_*`
|
|
13
|
+
* - Compatible with TanStack DB query-collection
|
|
14
|
+
* - Full TypeScript support with proper type constraints
|
|
15
|
+
* - Support for filtering, sorting, and pagination
|
|
16
|
+
* - Collection URI format: rsc://{connectionid}/{collection_name}/{item_id}
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Collection URI format validation
|
|
21
|
+
* Format: rsc://{connectionid}/{collection_name}/{item_id}
|
|
22
|
+
*/
|
|
23
|
+
export const CollectionUriSchema = z
|
|
24
|
+
.string()
|
|
25
|
+
.regex(
|
|
26
|
+
/^rsc:\/\/[^/]+\/[^/]+\/.+$/,
|
|
27
|
+
"Invalid collection URI format. Expected format: rsc://{connectionid}/{collection_name}/{item_id}",
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Utility function to validate a collection URI format
|
|
32
|
+
*
|
|
33
|
+
* @param uri - The URI to validate
|
|
34
|
+
* @returns True if the URI is valid, false otherwise
|
|
35
|
+
*/
|
|
36
|
+
export function validateCollectionUri(uri: string): boolean {
|
|
37
|
+
return CollectionUriSchema.safeParse(uri).success;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Utility function to parse a collection URI into its components
|
|
42
|
+
*
|
|
43
|
+
* @param uri - The URI to parse
|
|
44
|
+
* @returns Object containing the parsed components or null if invalid
|
|
45
|
+
*/
|
|
46
|
+
export function parseCollectionUri(uri: string): {
|
|
47
|
+
connectionId: string;
|
|
48
|
+
collectionName: string;
|
|
49
|
+
itemId: string;
|
|
50
|
+
} | null {
|
|
51
|
+
const result = CollectionUriSchema.safeParse(uri);
|
|
52
|
+
if (!result.success) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const match = result.data.match(/^rsc:\/\/([^/]+)\/([^/]+)\/(.+)$/);
|
|
57
|
+
if (!match) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
connectionId: decodeURIComponent(match[1]),
|
|
63
|
+
collectionName: decodeURIComponent(match[2]),
|
|
64
|
+
itemId: decodeURIComponent(match[3]),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Utility function to construct a collection URI from components
|
|
70
|
+
*
|
|
71
|
+
* @param connectionId - The connection identifier
|
|
72
|
+
* @param collectionName - The collection name
|
|
73
|
+
* @param itemId - The item identifier
|
|
74
|
+
* @returns The constructed collection URI
|
|
75
|
+
*/
|
|
76
|
+
export function constructCollectionUri(
|
|
77
|
+
connectionId: string,
|
|
78
|
+
collectionName: string,
|
|
79
|
+
itemId: string,
|
|
80
|
+
): string {
|
|
81
|
+
return `rsc://${encodeURIComponent(connectionId)}/${
|
|
82
|
+
encodeURIComponent(collectionName)
|
|
83
|
+
}/${encodeURIComponent(itemId)}`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Base schema for collection entities
|
|
88
|
+
* All collection entities must have a uri field and audit trail fields
|
|
89
|
+
*/
|
|
90
|
+
export const BaseCollectionEntitySchema = z.object({
|
|
91
|
+
uri: z.string(),
|
|
92
|
+
created_at: z.string().datetime(),
|
|
93
|
+
updated_at: z.string().datetime(),
|
|
94
|
+
created_by: z.string().optional(),
|
|
95
|
+
updated_by: z.string().optional(),
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Type helper for BaseCollectionEntitySchema
|
|
100
|
+
*/
|
|
101
|
+
export type BaseCollectionEntitySchemaType = typeof BaseCollectionEntitySchema;
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Where expression type for filtering
|
|
105
|
+
* Supports TanStack DB predicate push-down patterns
|
|
106
|
+
*/
|
|
107
|
+
export type WhereExpression =
|
|
108
|
+
| {
|
|
109
|
+
field: string[];
|
|
110
|
+
operator: "eq" | "gt" | "gte" | "lt" | "lte" | "in" | "like" | "contains";
|
|
111
|
+
value: unknown;
|
|
112
|
+
}
|
|
113
|
+
| {
|
|
114
|
+
operator: "and" | "or" | "not";
|
|
115
|
+
conditions: WhereExpression[];
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Where expression schema for filtering
|
|
120
|
+
* Supports TanStack DB predicate push-down patterns
|
|
121
|
+
*/
|
|
122
|
+
export const WhereExpressionSchema = z.lazy(() =>
|
|
123
|
+
z.union([
|
|
124
|
+
// Simple comparison
|
|
125
|
+
z.object({
|
|
126
|
+
field: z.array(z.string()),
|
|
127
|
+
operator: z.enum([
|
|
128
|
+
"eq",
|
|
129
|
+
"gt",
|
|
130
|
+
"gte",
|
|
131
|
+
"lt",
|
|
132
|
+
"lte",
|
|
133
|
+
"in",
|
|
134
|
+
"like",
|
|
135
|
+
"contains",
|
|
136
|
+
]),
|
|
137
|
+
value: z.unknown(),
|
|
138
|
+
}),
|
|
139
|
+
// Logical operators
|
|
140
|
+
z.object({
|
|
141
|
+
operator: z.enum(["and", "or", "not"]),
|
|
142
|
+
conditions: z.array(WhereExpressionSchema),
|
|
143
|
+
}),
|
|
144
|
+
])
|
|
145
|
+
) as z.ZodType<WhereExpression>;
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Order by expression for sorting
|
|
149
|
+
*/
|
|
150
|
+
export const OrderByExpressionSchema = z.object({
|
|
151
|
+
field: z.array(z.string()),
|
|
152
|
+
direction: z.enum(["asc", "desc"]),
|
|
153
|
+
nulls: z.enum(["first", "last"]).optional(),
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* List/Query input schema for collections
|
|
158
|
+
* Compatible with TanStack DB LoadSubsetOptions
|
|
159
|
+
*/
|
|
160
|
+
export const CollectionListInputSchema = z.object({
|
|
161
|
+
where: WhereExpressionSchema.optional().describe("Filter expression"),
|
|
162
|
+
orderBy: z
|
|
163
|
+
.array(OrderByExpressionSchema)
|
|
164
|
+
.optional()
|
|
165
|
+
.describe("Sort expressions"),
|
|
166
|
+
limit: z
|
|
167
|
+
.number()
|
|
168
|
+
.int()
|
|
169
|
+
.min(1)
|
|
170
|
+
.max(1000)
|
|
171
|
+
.optional()
|
|
172
|
+
.describe("Maximum number of items to return"),
|
|
173
|
+
offset: z
|
|
174
|
+
.number()
|
|
175
|
+
.int()
|
|
176
|
+
.min(0)
|
|
177
|
+
.optional()
|
|
178
|
+
.describe("Number of items to skip"),
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Factory function to create list output schema for a specific collection type
|
|
183
|
+
*/
|
|
184
|
+
export function createCollectionListOutputSchema<T extends z.ZodTypeAny>(
|
|
185
|
+
entitySchema: T,
|
|
186
|
+
) {
|
|
187
|
+
return z.object({
|
|
188
|
+
items: z.array(entitySchema).describe("Array of collection items"),
|
|
189
|
+
totalCount: z
|
|
190
|
+
.number()
|
|
191
|
+
.int()
|
|
192
|
+
.min(0)
|
|
193
|
+
.optional()
|
|
194
|
+
.describe("Total number of matching items (if available)"),
|
|
195
|
+
hasMore: z
|
|
196
|
+
.boolean()
|
|
197
|
+
.optional()
|
|
198
|
+
.describe("Whether there are more items available"),
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Get by URI input schema
|
|
204
|
+
*/
|
|
205
|
+
export const CollectionGetInputSchema = z.object({
|
|
206
|
+
uri: CollectionUriSchema.describe("URI of the entity to retrieve"),
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Factory function to create get output schema
|
|
211
|
+
*/
|
|
212
|
+
export function createCollectionGetOutputSchema<T extends z.ZodTypeAny>(
|
|
213
|
+
entitySchema: T,
|
|
214
|
+
) {
|
|
215
|
+
return z.object({
|
|
216
|
+
item: entitySchema
|
|
217
|
+
.nullable()
|
|
218
|
+
.describe("The retrieved item, or null if not found"),
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Factory function to create insert input schema
|
|
224
|
+
*/
|
|
225
|
+
export function createCollectionInsertInputSchema<T extends z.ZodTypeAny>(
|
|
226
|
+
entitySchema: T,
|
|
227
|
+
) {
|
|
228
|
+
// Remove uri field since it's auto-generated from connectionId/collectionName/itemId
|
|
229
|
+
return z.object({
|
|
230
|
+
data: entitySchema.describe("Data for the new entity (without uri)"),
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Factory function to create insert output schema
|
|
236
|
+
*/
|
|
237
|
+
export function createCollectionInsertOutputSchema<T extends z.ZodTypeAny>(
|
|
238
|
+
entitySchema: T,
|
|
239
|
+
) {
|
|
240
|
+
return z.object({
|
|
241
|
+
item: entitySchema.describe("The created entity with generated URI"),
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Factory function to create update input schema
|
|
247
|
+
*/
|
|
248
|
+
export function createCollectionUpdateInputSchema<T extends z.ZodTypeAny>(
|
|
249
|
+
entitySchema: T,
|
|
250
|
+
) {
|
|
251
|
+
return z.object({
|
|
252
|
+
uri: CollectionUriSchema.describe("URI of the entity to update"),
|
|
253
|
+
data: (entitySchema as unknown as z.AnyZodObject)
|
|
254
|
+
.partial()
|
|
255
|
+
.describe("Partial entity data to update"),
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Factory function to create update output schema
|
|
261
|
+
*/
|
|
262
|
+
export function createCollectionUpdateOutputSchema<T extends z.ZodTypeAny>(
|
|
263
|
+
entitySchema: T,
|
|
264
|
+
) {
|
|
265
|
+
return z.object({
|
|
266
|
+
item: entitySchema.describe("The updated entity"),
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Delete input schema
|
|
272
|
+
*/
|
|
273
|
+
export const CollectionDeleteInputSchema = z.object({
|
|
274
|
+
uri: CollectionUriSchema.describe("URI of the entity to delete"),
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Delete output schema
|
|
279
|
+
*/
|
|
280
|
+
export const CollectionDeleteOutputSchema = z.object({
|
|
281
|
+
success: z.boolean().describe("Whether the deletion was successful"),
|
|
282
|
+
uri: CollectionUriSchema.describe("URI of the deleted entity"),
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Options for creating collection bindings
|
|
287
|
+
*/
|
|
288
|
+
export interface CollectionBindingOptions {
|
|
289
|
+
/**
|
|
290
|
+
* If true, only LIST and GET operations will be included (read-only collection)
|
|
291
|
+
* @default false
|
|
292
|
+
*/
|
|
293
|
+
readOnly?: boolean;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Creates generic collection bindings for a specific entity type
|
|
298
|
+
*
|
|
299
|
+
* This function generates standardized tool bindings that work with any collection/table
|
|
300
|
+
* by accepting a custom entity schema and collection name. The bindings provide:
|
|
301
|
+
* - DECO_COLLECTION_{NAME}_LIST - Query/search entities with filtering and sorting (required)
|
|
302
|
+
* - DECO_COLLECTION_{NAME}_GET - Get a single entity by URI (required)
|
|
303
|
+
* - DECO_COLLECTION_{NAME}_INSERT - Create a new entity (optional, excluded if readOnly=true)
|
|
304
|
+
* - DECO_COLLECTION_{NAME}_UPDATE - Update an existing entity (optional, excluded if readOnly=true)
|
|
305
|
+
* - DECO_COLLECTION_{NAME}_DELETE - Delete an entity (optional, excluded if readOnly=true)
|
|
306
|
+
*
|
|
307
|
+
* @param collectionName - The name of the collection/table (e.g., "users", "products", "orders")
|
|
308
|
+
* @param entitySchema - The Zod schema for the entity type (must extend BaseCollectionEntitySchema)
|
|
309
|
+
* @param options - Optional configuration for the collection bindings
|
|
310
|
+
* @returns Array of tool bindings for Collection CRUD + Query operations
|
|
311
|
+
*
|
|
312
|
+
* @example
|
|
313
|
+
* ```typescript
|
|
314
|
+
* const UserSchema = z.object({
|
|
315
|
+
* uri: z.string(),
|
|
316
|
+
* created_at: z.string().datetime(),
|
|
317
|
+
* updated_at: z.string().datetime(),
|
|
318
|
+
* created_by: z.string().optional(),
|
|
319
|
+
* updated_by: z.string().optional(),
|
|
320
|
+
* name: z.string(),
|
|
321
|
+
* email: z.string().email(),
|
|
322
|
+
* });
|
|
323
|
+
*
|
|
324
|
+
* // Full CRUD collection
|
|
325
|
+
* const USER_COLLECTION_BINDING = createCollectionBindings("users", UserSchema);
|
|
326
|
+
*
|
|
327
|
+
* // Read-only collection (only LIST and GET)
|
|
328
|
+
* const READONLY_COLLECTION_BINDING = createCollectionBindings("products", ProductSchema, { readOnly: true });
|
|
329
|
+
* ```
|
|
330
|
+
*/
|
|
331
|
+
export function createCollectionBindings<
|
|
332
|
+
TEntitySchema extends BaseCollectionEntitySchemaType,
|
|
333
|
+
>(
|
|
334
|
+
collectionName: string,
|
|
335
|
+
entitySchema: TEntitySchema,
|
|
336
|
+
options?: CollectionBindingOptions,
|
|
337
|
+
) {
|
|
338
|
+
const upperName = collectionName.toUpperCase();
|
|
339
|
+
const readOnly = options?.readOnly ?? false;
|
|
340
|
+
|
|
341
|
+
const bindings: ToolBinder[] = [
|
|
342
|
+
{
|
|
343
|
+
name: `DECO_COLLECTION_${upperName}_LIST` as const,
|
|
344
|
+
inputSchema: CollectionListInputSchema,
|
|
345
|
+
outputSchema: createCollectionListOutputSchema(entitySchema),
|
|
346
|
+
},
|
|
347
|
+
{
|
|
348
|
+
name: `DECO_COLLECTION_${upperName}_GET` as const,
|
|
349
|
+
inputSchema: CollectionGetInputSchema,
|
|
350
|
+
outputSchema: createCollectionGetOutputSchema(entitySchema),
|
|
351
|
+
},
|
|
352
|
+
];
|
|
353
|
+
|
|
354
|
+
// Only include mutation operations if not read-only
|
|
355
|
+
if (!readOnly) {
|
|
356
|
+
bindings.push(
|
|
357
|
+
{
|
|
358
|
+
name: `DECO_COLLECTION_${upperName}_INSERT` as const,
|
|
359
|
+
inputSchema: createCollectionInsertInputSchema(entitySchema),
|
|
360
|
+
outputSchema: createCollectionInsertOutputSchema(entitySchema),
|
|
361
|
+
opt: true,
|
|
362
|
+
},
|
|
363
|
+
{
|
|
364
|
+
name: `DECO_COLLECTION_${upperName}_UPDATE` as const,
|
|
365
|
+
inputSchema: createCollectionUpdateInputSchema(entitySchema),
|
|
366
|
+
outputSchema: createCollectionUpdateOutputSchema(entitySchema),
|
|
367
|
+
opt: true,
|
|
368
|
+
},
|
|
369
|
+
{
|
|
370
|
+
name: `DECO_COLLECTION_${upperName}_DELETE` as const,
|
|
371
|
+
inputSchema: CollectionDeleteInputSchema,
|
|
372
|
+
outputSchema: CollectionDeleteOutputSchema,
|
|
373
|
+
opt: true,
|
|
374
|
+
},
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
return bindings satisfies readonly ToolBinder[];
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Type helper to extract the collection binding type
|
|
383
|
+
*/
|
|
384
|
+
export type CollectionBinding<
|
|
385
|
+
TEntitySchema extends BaseCollectionEntitySchemaType,
|
|
386
|
+
> = ReturnType<typeof createCollectionBindings<TEntitySchema>>;
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Type helper to extract tool names from a collection binding
|
|
390
|
+
*/
|
|
391
|
+
export type CollectionTools<
|
|
392
|
+
TEntitySchema extends BaseCollectionEntitySchemaType,
|
|
393
|
+
> = CollectionBinding<TEntitySchema>[number]["name"];
|
|
394
|
+
|
|
395
|
+
// Export types for TypeScript usage
|
|
396
|
+
export type CollectionUri = z.infer<typeof CollectionUriSchema>;
|
|
397
|
+
export type CollectionListInput = z.infer<typeof CollectionListInputSchema>;
|
|
398
|
+
export type CollectionGetInput = z.infer<typeof CollectionGetInputSchema>;
|
|
399
|
+
export type CollectionDeleteInput = z.infer<typeof CollectionDeleteInputSchema>;
|
|
400
|
+
export type CollectionDeleteOutput = z.infer<
|
|
401
|
+
typeof CollectionDeleteOutputSchema
|
|
402
|
+
>;
|
|
403
|
+
export type OrderByExpression = z.infer<typeof OrderByExpressionSchema>;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Models Well-Known Binding
|
|
3
|
+
*
|
|
4
|
+
* Defines the interface for AI model providers.
|
|
5
|
+
* Any MCP that implements this binding can provide AI models and streaming endpoints.
|
|
6
|
+
*
|
|
7
|
+
* This binding uses collection bindings for LIST and GET operations (read-only),
|
|
8
|
+
* and adds a GET_STREAM_ENDPOINT tool for streaming functionality.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { z } from "zod";
|
|
12
|
+
import type { Binder } from "../core/binder";
|
|
13
|
+
import {
|
|
14
|
+
BaseCollectionEntitySchema,
|
|
15
|
+
createCollectionBindings,
|
|
16
|
+
} from "./collections";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Model entity schema for AI models
|
|
20
|
+
* Extends BaseCollectionEntitySchema with model-specific fields
|
|
21
|
+
*/
|
|
22
|
+
const ModelSchema = BaseCollectionEntitySchema.extend({
|
|
23
|
+
// Model-specific fields
|
|
24
|
+
id: z.string(),
|
|
25
|
+
model: z.string(),
|
|
26
|
+
name: z.string(),
|
|
27
|
+
logo: z.string().nullable(),
|
|
28
|
+
capabilities: z.array(z.string()),
|
|
29
|
+
contextWindow: z.number().nullable(),
|
|
30
|
+
inputCost: z.number().nullable(),
|
|
31
|
+
outputCost: z.number().nullable(),
|
|
32
|
+
outputLimit: z.number().nullable(),
|
|
33
|
+
description: z.string().nullable(),
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* MODELS Collection Binding
|
|
38
|
+
*
|
|
39
|
+
* Collection bindings for models (read-only).
|
|
40
|
+
* Provides LIST and GET operations for AI models.
|
|
41
|
+
*/
|
|
42
|
+
export const MODELS_COLLECTION_BINDING = createCollectionBindings(
|
|
43
|
+
"models",
|
|
44
|
+
ModelSchema,
|
|
45
|
+
{ readOnly: true },
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* MODELS Binding
|
|
50
|
+
*
|
|
51
|
+
* Defines the interface for AI model providers.
|
|
52
|
+
* Any MCP that implements this binding can provide AI models and streaming endpoints.
|
|
53
|
+
*
|
|
54
|
+
* Required tools:
|
|
55
|
+
* - DECO_COLLECTION_MODELS_LIST: List available AI models with their capabilities
|
|
56
|
+
* - DECO_COLLECTION_MODELS_GET: Get a single model by URI
|
|
57
|
+
* - GET_STREAM_ENDPOINT: Get the streaming endpoint URL for chat completions
|
|
58
|
+
*/
|
|
59
|
+
export const MODELS_BINDING = [
|
|
60
|
+
...MODELS_COLLECTION_BINDING,
|
|
61
|
+
{
|
|
62
|
+
name: "GET_STREAM_ENDPOINT" as const,
|
|
63
|
+
inputSchema: z.object({}).passthrough(),
|
|
64
|
+
outputSchema: z.object({
|
|
65
|
+
url: z.string().url(),
|
|
66
|
+
}).partial(),
|
|
67
|
+
},
|
|
68
|
+
] as const satisfies Binder;
|
|
69
|
+
|