@beignet/core 0.0.52 → 0.0.53
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/CHANGELOG.md +11 -0
- package/README.md +53 -1
- package/dist/encryption/index.d.ts +40 -0
- package/dist/encryption/index.d.ts.map +1 -0
- package/dist/encryption/index.js +134 -0
- package/dist/encryption/index.js.map +1 -0
- package/dist/server/request-executor.d.ts.map +1 -1
- package/dist/server/request-executor.js +13 -0
- package/dist/server/request-executor.js.map +1 -1
- package/dist/server/use-case-route.d.ts +82 -17
- package/dist/server/use-case-route.d.ts.map +1 -1
- package/dist/server/use-case-route.js +40 -4
- package/dist/server/use-case-route.js.map +1 -1
- package/package.json +5 -1
- package/skills/app-architecture/SKILL.md +22 -1
- package/src/encryption/index.ts +198 -0
- package/src/server/request-executor.ts +19 -0
- package/src/server/use-case-route.ts +234 -27
|
@@ -149,20 +149,143 @@ export type UseCaseRouteInputParts<C extends HttpContractConfig> = {
|
|
|
149
149
|
body: InferBody<C>;
|
|
150
150
|
};
|
|
151
151
|
|
|
152
|
+
type SegmentPathParam<Segment extends string> = Segment extends `:${infer Name}`
|
|
153
|
+
? Name
|
|
154
|
+
: Segment extends `[${infer Name}]`
|
|
155
|
+
? Name
|
|
156
|
+
: never;
|
|
157
|
+
|
|
158
|
+
type PathParamNames<Path extends string> = string extends Path
|
|
159
|
+
? never
|
|
160
|
+
: Path extends `${infer Segment}/${infer Rest}`
|
|
161
|
+
? SegmentPathParam<Segment> | PathParamNames<Rest>
|
|
162
|
+
: SegmentPathParam<Path>;
|
|
163
|
+
|
|
164
|
+
type EmptyBinderInput = Record<never, never>;
|
|
165
|
+
|
|
166
|
+
declare const UNMERGEABLE_BINDER_INPUT: unique symbol;
|
|
167
|
+
|
|
168
|
+
type UnmergeableBinderInput = {
|
|
169
|
+
readonly [UNMERGEABLE_BINDER_INPUT]: true;
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
type IsAny<T> = 0 extends 1 & T ? true : false;
|
|
173
|
+
|
|
174
|
+
type BinderObject<T> =
|
|
175
|
+
IsAny<T> extends true
|
|
176
|
+
? UnmergeableBinderInput
|
|
177
|
+
: [T] extends [object]
|
|
178
|
+
? [Extract<T, readonly unknown[]>] extends [never]
|
|
179
|
+
? T
|
|
180
|
+
: UnmergeableBinderInput
|
|
181
|
+
: UnmergeableBinderInput;
|
|
182
|
+
|
|
183
|
+
type MergeBinderObjects<LowerPrecedence, HigherPrecedence> = [
|
|
184
|
+
BinderObject<LowerPrecedence>,
|
|
185
|
+
] extends [UnmergeableBinderInput]
|
|
186
|
+
? UnmergeableBinderInput
|
|
187
|
+
: [BinderObject<HigherPrecedence>] extends [UnmergeableBinderInput]
|
|
188
|
+
? UnmergeableBinderInput
|
|
189
|
+
: Omit<
|
|
190
|
+
BinderObject<LowerPrecedence>,
|
|
191
|
+
keyof BinderObject<HigherPrecedence>
|
|
192
|
+
> &
|
|
193
|
+
BinderObject<HigherPrecedence>;
|
|
194
|
+
|
|
195
|
+
type InferredPathInput<C extends HttpContractConfig> = string extends C["path"]
|
|
196
|
+
? UnmergeableBinderInput
|
|
197
|
+
: [PathParamNames<C["path"]>] extends [never]
|
|
198
|
+
? EmptyBinderInput
|
|
199
|
+
: { [K in PathParamNames<C["path"]>]: string };
|
|
200
|
+
|
|
201
|
+
type BinderPathInput<C extends HttpContractConfig> =
|
|
202
|
+
C["pathParams"] extends StandardSchemaV1
|
|
203
|
+
? InferOutput<C["pathParams"]>
|
|
204
|
+
: InferredPathInput<C>;
|
|
205
|
+
|
|
206
|
+
type BinderQueryInput<C extends HttpContractConfig> =
|
|
207
|
+
C["query"] extends StandardSchemaV1
|
|
208
|
+
? InferOutput<C["query"]>
|
|
209
|
+
: EmptyBinderInput;
|
|
210
|
+
|
|
211
|
+
type BinderBodyInput<C extends HttpContractConfig> =
|
|
212
|
+
C["body"] extends StandardSchemaV1
|
|
213
|
+
? InferOutput<C["body"]>
|
|
214
|
+
: EmptyBinderInput;
|
|
215
|
+
|
|
216
|
+
type HasPathSchema<C extends HttpContractConfig> =
|
|
217
|
+
C["pathParams"] extends StandardSchemaV1 ? true : false;
|
|
218
|
+
|
|
219
|
+
type HasQuerySchema<C extends HttpContractConfig> =
|
|
220
|
+
C["query"] extends StandardSchemaV1 ? true : false;
|
|
221
|
+
|
|
222
|
+
type HasBodySchema<C extends HttpContractConfig> =
|
|
223
|
+
C["body"] extends StandardSchemaV1 ? true : false;
|
|
224
|
+
|
|
225
|
+
type HasInferredPathInput<C extends HttpContractConfig> =
|
|
226
|
+
string extends C["path"]
|
|
227
|
+
? true
|
|
228
|
+
: [PathParamNames<C["path"]>] extends [never]
|
|
229
|
+
? false
|
|
230
|
+
: true;
|
|
231
|
+
|
|
232
|
+
type MergedBinderInput<C extends HttpContractConfig> = MergeBinderObjects<
|
|
233
|
+
MergeBinderObjects<BinderQueryInput<C>, BinderBodyInput<C>>,
|
|
234
|
+
BinderPathInput<C>
|
|
235
|
+
>;
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Input produced when a binder route omits an explicit `input` mapper.
|
|
239
|
+
*
|
|
240
|
+
* A sole declared request schema passes through unchanged when the literal
|
|
241
|
+
* path has no additional inferred parameters. Every other supported default
|
|
242
|
+
* binding merges object inputs with path over body over query precedence.
|
|
243
|
+
*/
|
|
244
|
+
type DefaultBinderRouteInput<C extends HttpContractConfig> =
|
|
245
|
+
HasPathSchema<C> extends true
|
|
246
|
+
? HasQuerySchema<C> extends true
|
|
247
|
+
? MergedBinderInput<C>
|
|
248
|
+
: HasBodySchema<C> extends true
|
|
249
|
+
? MergedBinderInput<C>
|
|
250
|
+
: BinderPathInput<C>
|
|
251
|
+
: HasQuerySchema<C> extends true
|
|
252
|
+
? HasBodySchema<C> extends true
|
|
253
|
+
? MergedBinderInput<C>
|
|
254
|
+
: HasInferredPathInput<C> extends true
|
|
255
|
+
? MergedBinderInput<C>
|
|
256
|
+
: BinderQueryInput<C>
|
|
257
|
+
: HasBodySchema<C> extends true
|
|
258
|
+
? HasInferredPathInput<C> extends true
|
|
259
|
+
? MergedBinderInput<C>
|
|
260
|
+
: BinderBodyInput<C>
|
|
261
|
+
: BinderPathInput<C>;
|
|
262
|
+
|
|
263
|
+
type UseCaseRouteInputMode = "default" | "mapped";
|
|
264
|
+
|
|
152
265
|
/**
|
|
153
266
|
* Constraint that checks a use case against the route that binds it.
|
|
154
267
|
*
|
|
155
268
|
* Produces a readable branded mismatch object on the `useCase` property when
|
|
156
|
-
* the use case requires a context the server does not provide,
|
|
157
|
-
* output does not match the contract's declared success response schema
|
|
269
|
+
* the use case requires a context the server does not provide, when its
|
|
270
|
+
* output does not match the contract's declared success response schema, or
|
|
271
|
+
* when the default binder input does not satisfy the use case input.
|
|
158
272
|
*/
|
|
159
|
-
export type UseCaseFitsRoute<
|
|
273
|
+
export type UseCaseFitsRoute<
|
|
160
274
|
Ctx,
|
|
161
|
-
|
|
275
|
+
C extends HttpContractConfig,
|
|
276
|
+
UC,
|
|
277
|
+
InputMode extends UseCaseRouteInputMode = "default",
|
|
278
|
+
> = [Ctx] extends [UseCaseRouteCtx<UC>]
|
|
162
279
|
? [UseCaseRouteOutput<UC>] extends [
|
|
163
280
|
SuccessBodyFromKeys<C["responses"], Success2xxKeys<C["responses"]>>,
|
|
164
281
|
]
|
|
165
|
-
?
|
|
282
|
+
? InputMode extends "mapped"
|
|
283
|
+
? unknown
|
|
284
|
+
: [DefaultBinderRouteInput<C>] extends [UseCaseRouteInput<UC>]
|
|
285
|
+
? unknown
|
|
286
|
+
: {
|
|
287
|
+
"~beignetError": "default binder input does not match the use case input; add an input mapper";
|
|
288
|
+
}
|
|
166
289
|
: {
|
|
167
290
|
"~beignetError": "useCase output does not match the contract's success response schema";
|
|
168
291
|
}
|
|
@@ -185,21 +308,34 @@ type UseCaseRouteShape<
|
|
|
185
308
|
* Route-scoped hooks that run after group hooks and before the use case.
|
|
186
309
|
*/
|
|
187
310
|
hooks?: Hooks;
|
|
188
|
-
/**
|
|
189
|
-
* Use case bound directly to the contract.
|
|
190
|
-
*/
|
|
191
|
-
useCase: UC & UseCaseFitsRoute<HandlerCtx, C, UC>;
|
|
192
|
-
/**
|
|
193
|
-
* Map parsed request parts to the use case input.
|
|
194
|
-
*
|
|
195
|
-
* A sole declared path, query, or body schema is passed through unchanged
|
|
196
|
-
* when no additional path, query, or object body values are present.
|
|
197
|
-
* Otherwise `defaultBinderInput` merges query, body, and path objects (path
|
|
198
|
-
* wins collisions) and never merges headers.
|
|
199
|
-
*/
|
|
200
|
-
input?: (parts: UseCaseRouteInputParts<C>) => UseCaseRouteInput<UC>;
|
|
201
311
|
handle?: never;
|
|
202
|
-
} &
|
|
312
|
+
} & (
|
|
313
|
+
| {
|
|
314
|
+
/**
|
|
315
|
+
* Use case bound directly to the contract. The default binder input
|
|
316
|
+
* must satisfy the use case input type.
|
|
317
|
+
*/
|
|
318
|
+
useCase: UC & UseCaseFitsRoute<HandlerCtx, C, UC>;
|
|
319
|
+
input?: never;
|
|
320
|
+
}
|
|
321
|
+
| {
|
|
322
|
+
/**
|
|
323
|
+
* Use case bound directly to the contract through an explicit input
|
|
324
|
+
* mapper.
|
|
325
|
+
*/
|
|
326
|
+
useCase: UC & UseCaseFitsRoute<HandlerCtx, C, UC, "mapped">;
|
|
327
|
+
/**
|
|
328
|
+
* Map parsed request parts to the use case input.
|
|
329
|
+
*
|
|
330
|
+
* A sole declared path, query, or body schema is passed through
|
|
331
|
+
* unchanged when no additional path, query, or object body values are
|
|
332
|
+
* present. Otherwise `defaultBinderInput` merges query, body, and path
|
|
333
|
+
* objects (path wins collisions) and never merges headers.
|
|
334
|
+
*/
|
|
335
|
+
input: (parts: UseCaseRouteInputParts<C>) => UseCaseRouteInput<UC>;
|
|
336
|
+
}
|
|
337
|
+
) &
|
|
338
|
+
BinderStatusOption<C>;
|
|
203
339
|
|
|
204
340
|
/**
|
|
205
341
|
* Route registration that binds a contract directly to a use case.
|
|
@@ -271,11 +407,25 @@ export type ValidatedRouteInput<Ctx, E> = E extends {
|
|
|
271
407
|
? {
|
|
272
408
|
contract: CL;
|
|
273
409
|
hooks?: HooksOf<E>;
|
|
274
|
-
useCase: UC &
|
|
275
|
-
UseCaseFitsRoute<Ctx & AddedCtxFromHooks<HooksOf<E>>, C, UC>;
|
|
276
|
-
input?: (parts: UseCaseRouteInputParts<C>) => UseCaseRouteInput<UC>;
|
|
277
410
|
handle?: never;
|
|
278
|
-
} &
|
|
411
|
+
} & (
|
|
412
|
+
| {
|
|
413
|
+
useCase: UC &
|
|
414
|
+
UseCaseFitsRoute<Ctx & AddedCtxFromHooks<HooksOf<E>>, C, UC>;
|
|
415
|
+
input?: never;
|
|
416
|
+
}
|
|
417
|
+
| {
|
|
418
|
+
useCase: UC &
|
|
419
|
+
UseCaseFitsRoute<
|
|
420
|
+
Ctx & AddedCtxFromHooks<HooksOf<E>>,
|
|
421
|
+
C,
|
|
422
|
+
UC,
|
|
423
|
+
"mapped"
|
|
424
|
+
>;
|
|
425
|
+
input: (parts: UseCaseRouteInputParts<C>) => UseCaseRouteInput<UC>;
|
|
426
|
+
}
|
|
427
|
+
) &
|
|
428
|
+
BinderStatusOption<C>
|
|
279
429
|
: unknown
|
|
280
430
|
: unknown;
|
|
281
431
|
|
|
@@ -322,6 +472,49 @@ export type RuntimeUseCaseRouteDef = {
|
|
|
322
472
|
status?: number;
|
|
323
473
|
};
|
|
324
474
|
|
|
475
|
+
type UseCaseInputValidationFailure = Error & {
|
|
476
|
+
name: "UseCaseValidationError";
|
|
477
|
+
phase: "input";
|
|
478
|
+
useCaseName: string;
|
|
479
|
+
};
|
|
480
|
+
|
|
481
|
+
function isUseCaseInputValidationFailure(
|
|
482
|
+
error: unknown,
|
|
483
|
+
useCaseName: string,
|
|
484
|
+
): error is UseCaseInputValidationFailure {
|
|
485
|
+
if (!(error instanceof Error)) return false;
|
|
486
|
+
const candidate = error as Partial<UseCaseInputValidationFailure>;
|
|
487
|
+
return (
|
|
488
|
+
candidate.name === "UseCaseValidationError" &&
|
|
489
|
+
candidate.phase === "input" &&
|
|
490
|
+
candidate.useCaseName === useCaseName
|
|
491
|
+
);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Internal framework error raised when a type-erased binder route produces an
|
|
496
|
+
* input that the bound use case rejects.
|
|
497
|
+
*/
|
|
498
|
+
export class UseCaseRouteInputValidationError extends Error {
|
|
499
|
+
readonly code = "USE_CASE_INPUT_VALIDATION_ERROR";
|
|
500
|
+
readonly contractName: string;
|
|
501
|
+
readonly useCaseName: string;
|
|
502
|
+
|
|
503
|
+
constructor(args: {
|
|
504
|
+
contractName: string;
|
|
505
|
+
useCaseName: string;
|
|
506
|
+
cause: UseCaseInputValidationFailure;
|
|
507
|
+
}) {
|
|
508
|
+
super(
|
|
509
|
+
`Default binder input for contract "${args.contractName}" does not satisfy use case "${args.useCaseName}". Add an explicit input mapper.`,
|
|
510
|
+
{ cause: args.cause },
|
|
511
|
+
);
|
|
512
|
+
this.name = "UseCaseRouteInputValidationError";
|
|
513
|
+
this.contractName = args.contractName;
|
|
514
|
+
this.useCaseName = args.useCaseName;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
325
518
|
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
326
519
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
327
520
|
}
|
|
@@ -458,10 +651,24 @@ export function createUseCaseRouteHandler<Ctx, C extends HttpContractConfig>(
|
|
|
458
651
|
: defaultBinderInput(parts);
|
|
459
652
|
const run = passSingle && trustedRun ? trustedRun : def.useCase.run;
|
|
460
653
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
654
|
+
try {
|
|
655
|
+
return {
|
|
656
|
+
status,
|
|
657
|
+
body: await run.call(def.useCase, { ctx, input }),
|
|
658
|
+
};
|
|
659
|
+
} catch (error) {
|
|
660
|
+
if (
|
|
661
|
+
!def.input &&
|
|
662
|
+
isUseCaseInputValidationFailure(error, def.useCase.name)
|
|
663
|
+
) {
|
|
664
|
+
throw new UseCaseRouteInputValidationError({
|
|
665
|
+
contractName: contract.name,
|
|
666
|
+
useCaseName: def.useCase.name,
|
|
667
|
+
cause: error,
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
throw error;
|
|
671
|
+
}
|
|
465
672
|
};
|
|
466
673
|
|
|
467
674
|
return {
|