prisma-generator-express 1.64.2 → 1.64.4
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/generators/generateQueryBuilderHelper.d.ts +1 -1
- package/dist/generators/generateQueryBuilderHelper.js +5 -4
- package/dist/generators/generateQueryBuilderHelper.js.map +1 -1
- package/dist/generators/generateRouteConfigType.js +2 -1
- package/dist/generators/generateRouteConfigType.js.map +1 -1
- package/dist/generators/generateRouterHono.js +132 -70
- package/dist/generators/generateRouterHono.js.map +1 -1
- package/dist/guardOptions.d.ts +30 -0
- package/dist/guardOptions.js +123 -0
- package/dist/guardOptions.js.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/copy/routeConfig.hono.ts +85 -0
- package/src/copy/routeConfig.ts +148 -20
- package/src/generators/generateQueryBuilderHelper.ts +9 -6
- package/src/generators/generateRouteConfigType.ts +6 -1
- package/src/generators/generateRouterHono.ts +162 -78
- package/src/guardOptions.ts +226 -0
- package/src/index.ts +19 -0
|
@@ -81,7 +81,28 @@ function emitWriteOp(meta: (typeof OPERATION_METADATA)[number], modelName: strin
|
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
/**
|
|
84
|
-
* Guard
|
|
84
|
+
* Guard behaviour is SEVEN INDEPENDENT OPT-IN CONTROLS on the route config.
|
|
85
|
+
*
|
|
86
|
+
* 1.64.2 shipped all of it as the default, which was a breaking change to a
|
|
87
|
+
* published package. The first correction put all of it behind one `requireGuard`
|
|
88
|
+
* switch, which was also wrong: these are unrelated decisions, and one switch
|
|
89
|
+
* forces a consumer who wants a missing-guard refusal to also accept a
|
|
90
|
+
* hook-ordering change and lose a route they may be using.
|
|
91
|
+
*
|
|
92
|
+
* Each is now its own option, each defaulting to the pre-1.64.2 behaviour:
|
|
93
|
+
*
|
|
94
|
+
* - `requireGuardShape` refuse an operation with no guard
|
|
95
|
+
* - `validateGuardShapes` refuse an empty or key-mixing shape
|
|
96
|
+
* - `requireDefaultVariantOptIn` confirm a `default` variant
|
|
97
|
+
* - `enableUpdateEach` register the batch route (default true)
|
|
98
|
+
* - `guardResolutionOrder` settle the guard before or after hooks
|
|
99
|
+
* - `allowE2EGuardBypass` honour E2E=true (default true)
|
|
100
|
+
* - `validateResolvedShapes` check what a shape function returned
|
|
101
|
+
*
|
|
102
|
+
* `HARDENED_GUARD_PROFILE` selects all seven, as values to spread rather than a
|
|
103
|
+
* mode to store. See routeConfig.ts.
|
|
104
|
+
*
|
|
105
|
+
* Guard dropping is then a GENERATION-TIME decision, never a runtime one.
|
|
85
106
|
*
|
|
86
107
|
* The emitted router used to compute `DROP_GUARD = <flag> || _env.E2E === 'true'`,
|
|
87
108
|
* so setting `E2E=true` in a deployed environment downgraded enforcement even
|
|
@@ -99,13 +120,14 @@ function emitWriteOp(meta: (typeof OPERATION_METADATA)[number], modelName: strin
|
|
|
99
120
|
* output is an artifact, and a paragraph about a bypass that no longer exists
|
|
100
121
|
* would be copied into every router this generator writes.
|
|
101
122
|
*
|
|
102
|
-
* `updateEach` is refused for the same reason, one step further on
|
|
103
|
-
* guard shapes by design — the endpoint is a
|
|
104
|
-
* applied directly — and the only thing between
|
|
105
|
-
* mutation
|
|
106
|
-
* security boundary; it is advice to whoever
|
|
107
|
-
* log. Refused at construction rather than
|
|
108
|
-
* expecting the route learns at boot instead
|
|
123
|
+
* `updateEach` is refused for the same reason, one step further on — and, again,
|
|
124
|
+
* only under the flag. It bypasses guard shapes by design — the endpoint is a
|
|
125
|
+
* batch of `{ where, data }` items applied directly — and the only thing between
|
|
126
|
+
* it and an unguarded mass mutation is a `console.warn` suppressed in
|
|
127
|
+
* production. A warning is not a security boundary; it is advice to whoever
|
|
128
|
+
* happens to be reading a development log. Refused at construction rather than
|
|
129
|
+
* silently dropped, so a deployment expecting the route learns at boot instead
|
|
130
|
+
* of at the first 404.
|
|
109
131
|
*/
|
|
110
132
|
export function generateHonoRouterFunction({
|
|
111
133
|
model,
|
|
@@ -160,9 +182,11 @@ import { buildModelOpenApi } from '../buildModelOpenApi${ext}'
|
|
|
160
182
|
import {
|
|
161
183
|
normalizeOperation,
|
|
162
184
|
resolveOperationVariantKey,
|
|
185
|
+
resolveGuardPolicy,
|
|
163
186
|
resolveGuardShapeOnce,
|
|
164
187
|
validateCountSourceWhere,
|
|
165
188
|
validateOperationConfig,
|
|
189
|
+
validateUpdateEachConfig,
|
|
166
190
|
} from '../routeConfig${ext}'
|
|
167
191
|
import type { NormalizedOperationConfig } from '../routeConfig${ext}'
|
|
168
192
|
import { transformResult } from '../operationRuntime${ext}'
|
|
@@ -176,8 +200,9 @@ import { MODEL_FIELDS, MODEL_ENUMS } from './${modelName}Metadata${ext}'
|
|
|
176
200
|
${generateRouteConfigType(modelName, 'HonoBeforeHook', guardShapesImport, importStyle, 'hono')}
|
|
177
201
|
const _env = getEnv()
|
|
178
202
|
|
|
179
|
-
// Fixed at generation time.
|
|
180
|
-
//
|
|
203
|
+
// Fixed at generation time. The \`allowE2EGuardBypass\` control decides whether the
|
|
204
|
+
// environment can additionally drop the guard at runtime; it defaults to true,
|
|
205
|
+
// which is upstream behaviour. See generateRouterHono.ts.
|
|
181
206
|
const DROP_GUARD = ${dropGuard}
|
|
182
207
|
|
|
183
208
|
type JsonLike =
|
|
@@ -236,6 +261,19 @@ async function parseBodyAsQueryMiddleware(c: HandlerContext): Promise<void> {
|
|
|
236
261
|
c.set('parsedQuery', sanitizeKeys(body as Record<string, unknown>))
|
|
237
262
|
}
|
|
238
263
|
|
|
264
|
+
async function parseUpdateEachBodyMiddleware(c: HandlerContext): Promise<void> {
|
|
265
|
+
let body: unknown
|
|
266
|
+
try {
|
|
267
|
+
body = await c.req.json()
|
|
268
|
+
} catch {
|
|
269
|
+
throw new HTTPException(400, { message: 'updateEach body must be an array of { where, data } items' })
|
|
270
|
+
}
|
|
271
|
+
if (!Array.isArray(body)) {
|
|
272
|
+
throw new HTTPException(400, { message: 'updateEach body must be an array of { where, data } items' })
|
|
273
|
+
}
|
|
274
|
+
c.set('body', body)
|
|
275
|
+
}
|
|
276
|
+
|
|
239
277
|
async function parseWriteBodyMiddleware(c: HandlerContext): Promise<void> {
|
|
240
278
|
let body: unknown
|
|
241
279
|
try {
|
|
@@ -254,6 +292,17 @@ function makeShapeMiddleware<TCtx, TPrisma, TEnv extends HonoEnvBase>(
|
|
|
254
292
|
opConfig: NormalizedOp<TEnv>,
|
|
255
293
|
opKind: OpKind,
|
|
256
294
|
) {
|
|
295
|
+
const policy = resolveGuardPolicy(config)
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* The environment bypass, honoured unless the consumer turned it off.
|
|
299
|
+
*
|
|
300
|
+
* \`E2E=true\` downgrading enforcement in a deployed environment is a real
|
|
301
|
+
* hazard, and it is also upstream behaviour — so it is a control, not a
|
|
302
|
+
* decision made here.
|
|
303
|
+
*/
|
|
304
|
+
const dropGuard = DROP_GUARD || (policy.allowE2EGuardBypass && _env.E2E === 'true')
|
|
305
|
+
|
|
257
306
|
return async (c: Context<GeneratedHonoEnv<TEnv>>): Promise<void> => {
|
|
258
307
|
const merged = mergePaginationConfig(config.pagination, opConfig.pagination)
|
|
259
308
|
if (merged) c.set('routeConfig', { pagination: merged })
|
|
@@ -281,18 +330,26 @@ function makeShapeMiddleware<TCtx, TPrisma, TEnv extends HonoEnvBase>(
|
|
|
281
330
|
: undefined
|
|
282
331
|
|
|
283
332
|
/**
|
|
284
|
-
*
|
|
285
|
-
* see resolveGuardShapeOnce. Passing the function
|
|
286
|
-
* different shape when it is enforced than when it
|
|
333
|
+
* With \`validateResolvedShapes\`, resolved ONCE here and the validated value
|
|
334
|
+
* is what travels onward — see resolveGuardShapeOnce. Passing the function
|
|
335
|
+
* on would let it return a different shape when it is enforced than when it
|
|
336
|
+
* was checked.
|
|
337
|
+
*
|
|
338
|
+
* Without it the raw shape is passed through untouched, which is upstream
|
|
339
|
+
* behaviour: it is resolved at the point of use, and a function returning
|
|
340
|
+
* something unusable is not this router's business to refuse.
|
|
287
341
|
*/
|
|
288
|
-
|
|
289
|
-
if (
|
|
290
|
-
|
|
291
|
-
|
|
342
|
+
let effectiveShape: unknown = opConfig.guardShape
|
|
343
|
+
if (policy.validateResolvedShapes) {
|
|
344
|
+
const resolution = await resolveGuardShapeOnce(opConfig.guardShape, resolvedKey, resolveCtx)
|
|
345
|
+
if (!resolution.ok) {
|
|
346
|
+
c.set('guardShapeFailure', resolution.problem)
|
|
347
|
+
return
|
|
348
|
+
}
|
|
349
|
+
effectiveShape = resolution.shape
|
|
292
350
|
}
|
|
293
|
-
const effectiveShape = resolution.shape
|
|
294
351
|
|
|
295
|
-
if (!
|
|
352
|
+
if (!dropGuard) {
|
|
296
353
|
c.set('guardShape', effectiveShape)
|
|
297
354
|
} else {
|
|
298
355
|
await applyDroppedGuard(
|
|
@@ -438,6 +495,38 @@ export function ${routerFunctionName}<TCtx = unknown, TPrisma = any, TEnv extend
|
|
|
438
495
|
})
|
|
439
496
|
}
|
|
440
497
|
|
|
498
|
+
const POLICY = resolveGuardPolicy(config)
|
|
499
|
+
const SETTLE_BEFORE_HOOKS = POLICY.guardResolutionOrder === 'before-hooks'
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* The guard-resolution failures, raised in one place so both handlers and both
|
|
503
|
+
* orderings share exactly one definition of what a failure is.
|
|
504
|
+
*
|
|
505
|
+
* WHERE it is called is the behavioural difference, and it is the
|
|
506
|
+
* \`guardResolutionOrder\` control. \`'before-hooks'\` runs it before any operation
|
|
507
|
+
* hook, because a hook that returns a Response — an auth gate, a cache, a
|
|
508
|
+
* short-circuit for a known caller — would otherwise answer a request whose
|
|
509
|
+
* guard was never established. \`'after-hooks'\` is upstream behaviour.
|
|
510
|
+
*
|
|
511
|
+
* \`guardShapeFailure\` is only ever set when \`validateResolvedShapes\` is on, so
|
|
512
|
+
* that branch is inert rather than merely unreached when it is off.
|
|
513
|
+
*/
|
|
514
|
+
const settleGuard = (c: Context<GeneratedHonoEnv<TEnv>>): void => {
|
|
515
|
+
const failure = c.get('guardVariantFailure')
|
|
516
|
+
if (failure) {
|
|
517
|
+
throw new HTTPException(400, {
|
|
518
|
+
message: formatGuardVariantResolutionError(failure),
|
|
519
|
+
})
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
const shapeFailure = c.get('guardShapeFailure')
|
|
523
|
+
if (shapeFailure) {
|
|
524
|
+
throw new HTTPException(500, {
|
|
525
|
+
message: 'guard shape could not be resolved: ' + shapeFailure,
|
|
526
|
+
})
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
441
530
|
const handleRead = (
|
|
442
531
|
opConfig: NormalizedOp<TEnv>,
|
|
443
532
|
handlerFn: (c: HandlerContext) => Promise<void>,
|
|
@@ -448,41 +537,15 @@ export function ${routerFunctionName}<TCtx = unknown, TPrisma = any, TEnv extend
|
|
|
448
537
|
await parseFn(c as unknown as HandlerContext)
|
|
449
538
|
await makeShapeMiddleware<TCtx, TPrisma, TEnv>(config, opConfig, opKind)(c)
|
|
450
539
|
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
*
|
|
454
|
-
* The check used to sit after \`operationBefore\`, so a hook that returned a
|
|
455
|
-
* Response — an auth gate, a cache, a short-circuit for a known caller —
|
|
456
|
-
* answered the request before anyone established which guard applied to
|
|
457
|
-
* it. A cached response served for a request whose variant could not be
|
|
458
|
-
* resolved is a response served under a guard nobody chose.
|
|
459
|
-
*
|
|
460
|
-
* Hooks that must run first belong outside the generated router, where
|
|
461
|
-
* they are visibly not part of guard resolution.
|
|
462
|
-
*/
|
|
463
|
-
const failure = c.get('guardVariantFailure')
|
|
464
|
-
if (failure) {
|
|
465
|
-
throw new HTTPException(400, {
|
|
466
|
-
message: formatGuardVariantResolutionError(failure),
|
|
467
|
-
})
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
/**
|
|
471
|
-
* A shape function that returned something unusable is a DEPLOYMENT fault,
|
|
472
|
-
* not a caller fault — 500, and refused here, before any hook can answer
|
|
473
|
-
* the request and before Prisma is reached. Running unguarded because the
|
|
474
|
-
* guard failed to produce a guard is the outcome this exists to prevent.
|
|
475
|
-
*/
|
|
476
|
-
const shapeFailure = c.get('guardShapeFailure')
|
|
477
|
-
if (shapeFailure) {
|
|
478
|
-
throw new HTTPException(500, {
|
|
479
|
-
message: 'guard shape could not be resolved: ' + shapeFailure,
|
|
480
|
-
})
|
|
481
|
-
}
|
|
540
|
+
// Settled BEFORE any hook can answer the request, when asked for.
|
|
541
|
+
if (SETTLE_BEFORE_HOOKS) settleGuard(c)
|
|
482
542
|
|
|
483
543
|
const operationBefore = await runBeforeHooks<TEnv>(opConfig.operationBefore, c)
|
|
484
544
|
if (operationBefore) return operationBefore
|
|
485
545
|
|
|
546
|
+
// Upstream order: hooks first, guard failure after.
|
|
547
|
+
if (!SETTLE_BEFORE_HOOKS) settleGuard(c)
|
|
548
|
+
|
|
486
549
|
const key = c.get('guardVariantKey')
|
|
487
550
|
const variantHooks =
|
|
488
551
|
key !== undefined ? opConfig.variantHooks[key] : undefined
|
|
@@ -509,30 +572,15 @@ export function ${routerFunctionName}<TCtx = unknown, TPrisma = any, TEnv extend
|
|
|
509
572
|
await parseWriteBodyMiddleware(c as unknown as HandlerContext)
|
|
510
573
|
await makeShapeMiddleware<TCtx, TPrisma, TEnv>(config, opConfig, opKind)(c)
|
|
511
574
|
|
|
512
|
-
// Settled
|
|
513
|
-
|
|
514
|
-
if (failure) {
|
|
515
|
-
throw new HTTPException(400, {
|
|
516
|
-
message: formatGuardVariantResolutionError(failure),
|
|
517
|
-
})
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
/**
|
|
521
|
-
* A shape function that returned something unusable is a DEPLOYMENT fault,
|
|
522
|
-
* not a caller fault — 500, and refused here, before any hook can answer
|
|
523
|
-
* the request and before Prisma is reached. Running unguarded because the
|
|
524
|
-
* guard failed to produce a guard is the outcome this exists to prevent.
|
|
525
|
-
*/
|
|
526
|
-
const shapeFailure = c.get('guardShapeFailure')
|
|
527
|
-
if (shapeFailure) {
|
|
528
|
-
throw new HTTPException(500, {
|
|
529
|
-
message: 'guard shape could not be resolved: ' + shapeFailure,
|
|
530
|
-
})
|
|
531
|
-
}
|
|
575
|
+
// Settled BEFORE any hook can answer the request, when asked for.
|
|
576
|
+
if (SETTLE_BEFORE_HOOKS) settleGuard(c)
|
|
532
577
|
|
|
533
578
|
const operationBefore = await runBeforeHooks<TEnv>(opConfig.operationBefore, c)
|
|
534
579
|
if (operationBefore) return operationBefore
|
|
535
580
|
|
|
581
|
+
// Upstream order: hooks first, guard failure after.
|
|
582
|
+
if (!SETTLE_BEFORE_HOOKS) settleGuard(c)
|
|
583
|
+
|
|
536
584
|
const key = c.get('guardVariantKey')
|
|
537
585
|
const variantHooks =
|
|
538
586
|
key !== undefined ? opConfig.variantHooks[key] : undefined
|
|
@@ -554,7 +602,7 @@ export function ${routerFunctionName}<TCtx = unknown, TPrisma = any, TEnv extend
|
|
|
554
602
|
key: K,
|
|
555
603
|
): NormalizedOp<TEnv> => {
|
|
556
604
|
const raw = config[key] as unknown as OperationConfigLike<TEnv> | undefined
|
|
557
|
-
validateOperationConfig(raw, '${modelName}.' + String(key))
|
|
605
|
+
validateOperationConfig(raw, '${modelName}.' + String(key), POLICY)
|
|
558
606
|
return normalizeHonoOperation(raw)
|
|
559
607
|
}
|
|
560
608
|
|
|
@@ -562,14 +610,50 @@ ${readOpBlocks}
|
|
|
562
610
|
|
|
563
611
|
${writeOpBlocks}
|
|
564
612
|
|
|
565
|
-
// Not registered on this target — see generateRouterHono.ts for why.
|
|
566
613
|
if (config.updateEach) {
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
614
|
+
/**
|
|
615
|
+
* Refused only when \`enableUpdateEach\` is false. It bypasses guard shapes by design — the
|
|
616
|
+
* endpoint is a batch of { where, data } applied directly — and the only thing
|
|
617
|
+
* between it and an unguarded mass mutation is a console.warn suppressed in
|
|
618
|
+
* production. A warning is not a security boundary.
|
|
619
|
+
*
|
|
620
|
+
* Removing the route outright was a breaking change to a published package,
|
|
621
|
+
* so by default it registers exactly as it did, warning and all.
|
|
622
|
+
*/
|
|
623
|
+
if (!POLICY.enableUpdateEach) {
|
|
624
|
+
throw new Error(
|
|
625
|
+
'${modelName}.updateEach: enableUpdateEach is false, so this router ' +
|
|
626
|
+
'does not register updateEach. It bypasses guard shapes entirely, so ' +
|
|
627
|
+
'there is no configuration that makes it safe to expose. Perform batch ' +
|
|
628
|
+
'updates through a guarded operation, or behind your own authenticated ' +
|
|
629
|
+
'route outside the generated router.',
|
|
630
|
+
)
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
const rawUpdateEach = config.updateEach as unknown as OperationConfigLike<TEnv>
|
|
634
|
+
validateUpdateEachConfig(rawUpdateEach, '${modelName}.updateEach')
|
|
635
|
+
const opConfig = normalizeHonoOperation(rawUpdateEach)
|
|
636
|
+
if (opConfig.operationBefore.length === 0 && _env.NODE_ENV !== 'production') {
|
|
637
|
+
console.warn(
|
|
638
|
+
'[${modelName}Router] updateEach is enabled without a before hook. ' +
|
|
639
|
+
'This endpoint bypasses guard shapes and should be protected by authentication middleware.',
|
|
640
|
+
)
|
|
641
|
+
}
|
|
642
|
+
const path = basePath ? \`\${basePath}/each\` : '/each'
|
|
643
|
+
app.post(path, async (c: Context<GeneratedHonoEnv<TEnv>>): Promise<Response> => {
|
|
644
|
+
try {
|
|
645
|
+
await parseUpdateEachBodyMiddleware(c as unknown as HandlerContext)
|
|
646
|
+
await makeShapeMiddleware<TCtx, TPrisma, TEnv>(config, opConfig, 'noop')(c)
|
|
647
|
+
const beforeResponse = await runBeforeHooks<TEnv>(opConfig.operationBefore, c)
|
|
648
|
+
if (beforeResponse) return beforeResponse
|
|
649
|
+
await ${modelName}UpdateEach(c as unknown as HandlerContext)
|
|
650
|
+
const afterResponse = await runAfterHooks<TEnv>(opConfig.operationAfter, c)
|
|
651
|
+
if (afterResponse) return afterResponse
|
|
652
|
+
return sendResult(c as unknown as HandlerContext)
|
|
653
|
+
} catch (error: unknown) {
|
|
654
|
+
return sendError(c as unknown as HandlerContext, error)
|
|
655
|
+
}
|
|
656
|
+
})
|
|
573
657
|
}
|
|
574
658
|
|
|
575
659
|
return app
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PUBLIC, COMPILED METADATA for the Hono guard controls.
|
|
3
|
+
*
|
|
4
|
+
* **Why this file exists separately from `src/copy/routeConfig.ts`.**
|
|
5
|
+
* `src/copy/**` is excluded from the TypeScript build (`tsconfig.json`) because
|
|
6
|
+
* those files are copied verbatim into a generated project rather than compiled
|
|
7
|
+
* into this package. So anything defined only there is absent from `dist` and
|
|
8
|
+
* unreachable through the package entry point — which made the first version of
|
|
9
|
+
* this metadata unusable by the very consumer it was written for.
|
|
10
|
+
*
|
|
11
|
+
* This module is compiled, exported from `src/index.ts`, and therefore importable
|
|
12
|
+
* as `require('prisma-generator-express').GUARD_OPTION_METADATA` from an
|
|
13
|
+
* installed package. `test/consumer/packedMetadata.test.ts` proves that by packing
|
|
14
|
+
* the tarball and importing it the way a consumer would.
|
|
15
|
+
*
|
|
16
|
+
* **Scope, stated narrowly.** This is authoritative for the seven M8.7 GUARD
|
|
17
|
+
* controls and nothing else. The generator has many other public settings that
|
|
18
|
+
* change the emitted API — `dropGuard`, the target, the write strategy, the
|
|
19
|
+
* pagination mode, prefixes, OpenAPI options, the query-builder toggle — and none
|
|
20
|
+
* of them are described here yet. A configuration UI cannot build a complete
|
|
21
|
+
* screen from this file alone, and should not pretend otherwise.
|
|
22
|
+
*
|
|
23
|
+
* **Duplication is deliberate and guarded.** The runtime defaults live in
|
|
24
|
+
* `src/copy/routeConfig.ts` because the generated project needs them with no
|
|
25
|
+
* dependency on this package. They cannot import each other across the compile
|
|
26
|
+
* boundary, so the values appear twice and
|
|
27
|
+
* `test/unit/copy/guardOptionIndependence.test.ts` asserts the two agree, against
|
|
28
|
+
* the resolver the router actually runs. Drift fails a test rather than shipping a
|
|
29
|
+
* UI that advertises a default the code does not apply.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
export type GuardResolutionOrder = 'after-hooks' | 'before-hooks'
|
|
33
|
+
|
|
34
|
+
export type GuardPolicy = {
|
|
35
|
+
requireGuardShape: boolean
|
|
36
|
+
validateGuardShapes: boolean
|
|
37
|
+
requireDefaultVariantOptIn: boolean
|
|
38
|
+
enableUpdateEach: boolean
|
|
39
|
+
guardResolutionOrder: GuardResolutionOrder
|
|
40
|
+
allowE2EGuardBypass: boolean
|
|
41
|
+
validateResolvedShapes: boolean
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The public package's pre-1.64.2 behaviour, exactly.
|
|
46
|
+
*
|
|
47
|
+
* An unset option resolves to the value here. Changing one changes what every
|
|
48
|
+
* consumer who configured nothing gets on upgrade, which is the failure 1.64.2
|
|
49
|
+
* shipped and this table exists to prevent.
|
|
50
|
+
*/
|
|
51
|
+
export const UPSTREAM_GUARD_DEFAULTS: Readonly<GuardPolicy> = Object.freeze({
|
|
52
|
+
requireGuardShape: false,
|
|
53
|
+
validateGuardShapes: false,
|
|
54
|
+
requireDefaultVariantOptIn: false,
|
|
55
|
+
enableUpdateEach: true,
|
|
56
|
+
guardResolutionOrder: 'after-hooks',
|
|
57
|
+
allowE2EGuardBypass: true,
|
|
58
|
+
validateResolvedShapes: false,
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Every control at its most restrictive — a SHORTCUT, not a mode.
|
|
63
|
+
*
|
|
64
|
+
* Spread it, change what you disagree with, store the result. There is
|
|
65
|
+
* deliberately no `preset: 'hardened'` option anywhere in this package: a stored
|
|
66
|
+
* preset NAME would let a future version silently change what an existing
|
|
67
|
+
* configuration does, which is the same class of mistake as 1.64.2.
|
|
68
|
+
*/
|
|
69
|
+
export const HARDENED_GUARD_PROFILE: Readonly<GuardPolicy> = Object.freeze({
|
|
70
|
+
requireGuardShape: true,
|
|
71
|
+
validateGuardShapes: true,
|
|
72
|
+
requireDefaultVariantOptIn: true,
|
|
73
|
+
enableUpdateEach: false,
|
|
74
|
+
guardResolutionOrder: 'before-hooks',
|
|
75
|
+
allowE2EGuardBypass: false,
|
|
76
|
+
validateResolvedShapes: true,
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
export type GuardOptionMetadata = {
|
|
80
|
+
name: keyof GuardPolicy
|
|
81
|
+
type: 'boolean' | 'enum'
|
|
82
|
+
values?: readonly string[]
|
|
83
|
+
default: boolean | string
|
|
84
|
+
hardened: boolean | string
|
|
85
|
+
label: string
|
|
86
|
+
description: string
|
|
87
|
+
/** Present where the permissive value can emit an unguarded or caller-selected route. */
|
|
88
|
+
warning: string
|
|
89
|
+
/** The target whose generated router reads this control. */
|
|
90
|
+
target: 'hono'
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Machine-readable description of the seven guard controls.
|
|
95
|
+
*
|
|
96
|
+
* `target: 'hono'` on every entry is not decoration. These options are declared
|
|
97
|
+
* on the Hono route configuration only, because only the Hono generator reads
|
|
98
|
+
* them; declaring them on the shared base made Express and Fastify accept them in
|
|
99
|
+
* TypeScript and ignore them at runtime, which is a false public API.
|
|
100
|
+
*/
|
|
101
|
+
export const GUARD_OPTION_METADATA: readonly GuardOptionMetadata[] = Object.freeze([
|
|
102
|
+
{
|
|
103
|
+
name: 'requireGuardShape',
|
|
104
|
+
type: 'boolean',
|
|
105
|
+
default: UPSTREAM_GUARD_DEFAULTS.requireGuardShape,
|
|
106
|
+
hardened: HARDENED_GUARD_PROFILE.requireGuardShape,
|
|
107
|
+
label: 'Require a guard on every enabled operation',
|
|
108
|
+
description:
|
|
109
|
+
'Refuses at router construction when an operation defines neither "shape" nor "variants".',
|
|
110
|
+
warning:
|
|
111
|
+
"Off, an operation nobody configured passes the caller's own where/select/include straight to Prisma.",
|
|
112
|
+
target: 'hono',
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
name: 'validateGuardShapes',
|
|
116
|
+
type: 'boolean',
|
|
117
|
+
default: UPSTREAM_GUARD_DEFAULTS.validateGuardShapes,
|
|
118
|
+
hardened: HARDENED_GUARD_PROFILE.validateGuardShapes,
|
|
119
|
+
label: 'Validate guard shapes',
|
|
120
|
+
description:
|
|
121
|
+
'Refuses an empty shape, and one mixing guard keys with non-guard keys, at the top level and inside variants.',
|
|
122
|
+
warning:
|
|
123
|
+
'Off, "shape: {}" reads as guarded and constrains nothing. On, strict validation may reject existing route configs.',
|
|
124
|
+
target: 'hono',
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
name: 'requireDefaultVariantOptIn',
|
|
128
|
+
type: 'boolean',
|
|
129
|
+
default: UPSTREAM_GUARD_DEFAULTS.requireDefaultVariantOptIn,
|
|
130
|
+
hardened: HARDENED_GUARD_PROFILE.requireDefaultVariantOptIn,
|
|
131
|
+
label: 'Require confirmation for a "default" variant',
|
|
132
|
+
description:
|
|
133
|
+
'A variant named "default" must set allowDefaultVariant: true to be accepted.',
|
|
134
|
+
warning: 'A "default" variant answers every unrecognised, blank and missing caller.',
|
|
135
|
+
target: 'hono',
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
name: 'enableUpdateEach',
|
|
139
|
+
type: 'boolean',
|
|
140
|
+
default: UPSTREAM_GUARD_DEFAULTS.enableUpdateEach,
|
|
141
|
+
hardened: HARDENED_GUARD_PROFILE.enableUpdateEach,
|
|
142
|
+
label: 'Register the updateEach batch route',
|
|
143
|
+
description: 'Mounts POST <base>/each. Set false to refuse it at router construction.',
|
|
144
|
+
warning:
|
|
145
|
+
'updateEach bypasses guard shapes entirely; only a development-only warning stands between it and an unguarded mass update.',
|
|
146
|
+
target: 'hono',
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
name: 'guardResolutionOrder',
|
|
150
|
+
type: 'enum',
|
|
151
|
+
values: Object.freeze(['after-hooks', 'before-hooks']),
|
|
152
|
+
default: UPSTREAM_GUARD_DEFAULTS.guardResolutionOrder,
|
|
153
|
+
hardened: HARDENED_GUARD_PROFILE.guardResolutionOrder,
|
|
154
|
+
label: 'When guard failures are raised',
|
|
155
|
+
description:
|
|
156
|
+
'after-hooks is the upstream behaviour; before-hooks settles variant and shape resolution before any operation hook runs.',
|
|
157
|
+
warning:
|
|
158
|
+
'With after-hooks, a hook that returns a Response can answer a request whose guard was never established.',
|
|
159
|
+
target: 'hono',
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
name: 'allowE2EGuardBypass',
|
|
163
|
+
type: 'boolean',
|
|
164
|
+
default: UPSTREAM_GUARD_DEFAULTS.allowE2EGuardBypass,
|
|
165
|
+
hardened: HARDENED_GUARD_PROFILE.allowE2EGuardBypass,
|
|
166
|
+
label: 'Honour the E2E environment guard bypass',
|
|
167
|
+
description: 'Treats E2E=true in the environment as a request to drop the guard.',
|
|
168
|
+
warning:
|
|
169
|
+
'A runtime variable can weaken a deployed artifact after generation. On an edge runtime this is an ordinary config var.',
|
|
170
|
+
target: 'hono',
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
name: 'validateResolvedShapes',
|
|
174
|
+
type: 'boolean',
|
|
175
|
+
default: UPSTREAM_GUARD_DEFAULTS.validateResolvedShapes,
|
|
176
|
+
hardened: HARDENED_GUARD_PROFILE.validateResolvedShapes,
|
|
177
|
+
label: 'Validate what a shape function returns',
|
|
178
|
+
description:
|
|
179
|
+
'Checks a dynamically resolved shape before use and fails the request with 500 if it is unusable. Also resolves it exactly once.',
|
|
180
|
+
warning:
|
|
181
|
+
'Off, a shape function returning {} or undefined runs the operation unguarded, once per request.',
|
|
182
|
+
target: 'hono',
|
|
183
|
+
},
|
|
184
|
+
])
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* What this metadata does NOT cover.
|
|
188
|
+
*
|
|
189
|
+
* Exported so a consumer can display the gap rather than infer completeness from
|
|
190
|
+
* silence, and so a test can assert the claim is narrow. Every name here is a
|
|
191
|
+
* public setting that changes the emitted API and has no entry above.
|
|
192
|
+
*/
|
|
193
|
+
export const GUARD_METADATA_SCOPE = Object.freeze({
|
|
194
|
+
covers: 'the seven M8.7 guard controls on the Hono target',
|
|
195
|
+
complete: false,
|
|
196
|
+
notDescribedYet: Object.freeze([
|
|
197
|
+
'dropGuard',
|
|
198
|
+
'target',
|
|
199
|
+
'writeStrategy',
|
|
200
|
+
'findManyPaginated / pagination mode',
|
|
201
|
+
'pagination.countSource',
|
|
202
|
+
'addModelPrefix / customUrlPrefix / specBasePath',
|
|
203
|
+
'disableOpenApi and the OpenAPI metadata options',
|
|
204
|
+
'disablePostReads',
|
|
205
|
+
'queryBuilder',
|
|
206
|
+
'guard.resolveVariant / guard.variantHeader',
|
|
207
|
+
]),
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
/** Fill in the defaults. Absent means upstream, never an opinion. */
|
|
211
|
+
export function resolveGuardPolicy(config: Partial<GuardPolicy> | undefined): GuardPolicy {
|
|
212
|
+
return {
|
|
213
|
+
requireGuardShape: config?.requireGuardShape ?? UPSTREAM_GUARD_DEFAULTS.requireGuardShape,
|
|
214
|
+
validateGuardShapes:
|
|
215
|
+
config?.validateGuardShapes ?? UPSTREAM_GUARD_DEFAULTS.validateGuardShapes,
|
|
216
|
+
requireDefaultVariantOptIn:
|
|
217
|
+
config?.requireDefaultVariantOptIn ?? UPSTREAM_GUARD_DEFAULTS.requireDefaultVariantOptIn,
|
|
218
|
+
enableUpdateEach: config?.enableUpdateEach ?? UPSTREAM_GUARD_DEFAULTS.enableUpdateEach,
|
|
219
|
+
guardResolutionOrder:
|
|
220
|
+
config?.guardResolutionOrder ?? UPSTREAM_GUARD_DEFAULTS.guardResolutionOrder,
|
|
221
|
+
allowE2EGuardBypass:
|
|
222
|
+
config?.allowE2EGuardBypass ?? UPSTREAM_GUARD_DEFAULTS.allowE2EGuardBypass,
|
|
223
|
+
validateResolvedShapes:
|
|
224
|
+
config?.validateResolvedShapes ?? UPSTREAM_GUARD_DEFAULTS.validateResolvedShapes,
|
|
225
|
+
}
|
|
226
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,3 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public, compiled configuration metadata (M8.7).
|
|
3
|
+
*
|
|
4
|
+
* Re-exported here because `src/copy/**` is excluded from the build, so anything
|
|
5
|
+
* defined only there never reaches `dist` and cannot be imported from an
|
|
6
|
+
* installed package. See src/guardOptions.ts.
|
|
7
|
+
*
|
|
8
|
+
* Authoritative for the seven Hono guard controls only — `GUARD_METADATA_SCOPE`
|
|
9
|
+
* lists what is not described yet.
|
|
10
|
+
*/
|
|
11
|
+
export {
|
|
12
|
+
GUARD_OPTION_METADATA,
|
|
13
|
+
GUARD_METADATA_SCOPE,
|
|
14
|
+
UPSTREAM_GUARD_DEFAULTS,
|
|
15
|
+
HARDENED_GUARD_PROFILE,
|
|
16
|
+
resolveGuardPolicy,
|
|
17
|
+
} from './guardOptions'
|
|
18
|
+
export type { GuardPolicy, GuardOptionMetadata, GuardResolutionOrder } from './guardOptions'
|
|
19
|
+
|
|
1
20
|
import {
|
|
2
21
|
generatorHandler,
|
|
3
22
|
GeneratorOptions,
|