prisma-generator-express 1.64.2 → 1.64.3
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/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/generateRouteConfigType.ts +6 -1
- package/src/generators/generateRouterHono.ts +162 -78
- package/src/guardOptions.ts +226 -0
- package/src/index.ts +19 -0
|
@@ -107,6 +107,91 @@ export type RouteConfig<
|
|
|
107
107
|
>,
|
|
108
108
|
HonoOpKeys
|
|
109
109
|
> & {
|
|
110
|
+
/**
|
|
111
|
+
* SEVEN INDEPENDENT GUARD CONTROLS. Every default reproduces the public
|
|
112
|
+
* package's pre-1.64.2 behaviour exactly.
|
|
113
|
+
*
|
|
114
|
+
* 1.64.2 shipped these behaviours ON by default, which was a breaking change
|
|
115
|
+
* to a published package. The first correction put all seven behind one
|
|
116
|
+
* `requireGuard` switch, which was also wrong: they are unrelated decisions,
|
|
117
|
+
* and bundling them means a consumer who wants a missing-guard refusal is also
|
|
118
|
+
* forced into a hook-ordering change and the removal of a route they may use.
|
|
119
|
+
*
|
|
120
|
+
* So each is its own option. An unset option means the upstream default — never
|
|
121
|
+
* an opinionated value chosen on the consumer's behalf. `HARDENED_GUARD_PROFILE`
|
|
122
|
+
* selects all seven together for those who want them, but it is a set of values
|
|
123
|
+
* to spread, not a mode: what gets stored is the individual settings.
|
|
124
|
+
*
|
|
125
|
+
* `GUARD_OPTION_METADATA` describes these machine-readably, so a UI can render
|
|
126
|
+
* them without a hand-maintained second copy of the defaults.
|
|
127
|
+
*/
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Refuse an operation that configures no guard at all. **Default `false`.**
|
|
131
|
+
*
|
|
132
|
+
* With it off — the upstream default — an unconfigured operation is emitted and
|
|
133
|
+
* passes the caller's own where/select/include straight to Prisma. On, router
|
|
134
|
+
* construction throws, which on a Worker is boot.
|
|
135
|
+
*/
|
|
136
|
+
requireGuardShape?: boolean
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Validate that a shape constrains something. **Default `false`.**
|
|
140
|
+
*
|
|
141
|
+
* On, `shape: {}` and a shape mixing guard keys with non-guard keys (usually a
|
|
142
|
+
* typo: `wheer`, `iclude`) are refused, at the top level and inside variants.
|
|
143
|
+
*/
|
|
144
|
+
validateGuardShapes?: boolean
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Require `allowDefaultVariant: true` on an operation carrying a variant named
|
|
148
|
+
* `default`. **Default `false`.**
|
|
149
|
+
*
|
|
150
|
+
* A `default` variant answers every unrecognised, blank and missing caller.
|
|
151
|
+
* That is safe when it is the most restrictive variant and dangerous when it is
|
|
152
|
+
* the most permissive, and nothing can tell which from the shape.
|
|
153
|
+
*/
|
|
154
|
+
requireDefaultVariantOptIn?: boolean
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Register the `updateEach` batch route. **Default `true`.**
|
|
158
|
+
*
|
|
159
|
+
* It bypasses guard shapes by design — a batch of `{ where, data }` applied
|
|
160
|
+
* directly — and is protected only by a development-only warning. Set `false`
|
|
161
|
+
* to refuse it at router construction.
|
|
162
|
+
*/
|
|
163
|
+
enableUpdateEach?: boolean
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* When guard failures are raised relative to operation `before` hooks.
|
|
167
|
+
* **Default `'after-hooks'`.**
|
|
168
|
+
*
|
|
169
|
+
* `'after-hooks'` is upstream behaviour: a hook may answer the request before
|
|
170
|
+
* variant resolution is settled. `'before-hooks'` settles the guard first, so a
|
|
171
|
+
* hook cannot answer a request whose guard was never established.
|
|
172
|
+
*/
|
|
173
|
+
guardResolutionOrder?: 'after-hooks' | 'before-hooks'
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Honour `E2E=true` in the environment as a guard bypass. **Default `true`.**
|
|
177
|
+
*
|
|
178
|
+
* Upstream behaviour, and a real hazard on an edge runtime where that variable
|
|
179
|
+
* is an ordinary config var — set on staging, copied forward, flagged as
|
|
180
|
+
* security-relevant nowhere. `false` makes guard behaviour a property of the
|
|
181
|
+
* artifact alone.
|
|
182
|
+
*/
|
|
183
|
+
allowE2EGuardBypass?: boolean
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Check what a shape FUNCTION returned before using it. **Default `false`.**
|
|
187
|
+
*
|
|
188
|
+
* A function shape is opaque at construction time. On, a resolved value that is
|
|
189
|
+
* empty, not an object, or carrying non-guard keys fails the request with 500 —
|
|
190
|
+
* a deployment fault, not a caller fault — instead of running unguarded. It
|
|
191
|
+
* also resolves the function exactly once and passes the validated value on.
|
|
192
|
+
*/
|
|
193
|
+
validateResolvedShapes?: boolean
|
|
194
|
+
|
|
110
195
|
findUnique?: OperationConfig<TShape, TEnv> | false
|
|
111
196
|
findUniqueOrThrow?: OperationConfig<TShape, TEnv> | false
|
|
112
197
|
findFirst?: OperationConfig<TShape, TEnv> | false
|
package/src/copy/routeConfig.ts
CHANGED
|
@@ -119,6 +119,14 @@ export type BaseOperationConfig<
|
|
|
119
119
|
before?: TBefore[]
|
|
120
120
|
after?: TAfter[]
|
|
121
121
|
pagination?: Partial<PaginationConfig>
|
|
122
|
+
/**
|
|
123
|
+
* Confirms that a variant literally named `default` is meant to answer every
|
|
124
|
+
* unrecognised, blank and missing caller.
|
|
125
|
+
*
|
|
126
|
+
* Only consulted when `requireDefaultVariantOptIn` is on. Otherwise the key
|
|
127
|
+
* behaves as it always has, so this is inert for existing configurations.
|
|
128
|
+
*/
|
|
129
|
+
allowDefaultVariant?: boolean
|
|
122
130
|
}
|
|
123
131
|
|
|
124
132
|
export interface BaseUpdateEachConfig<TBefore, TAfter = TBefore> {
|
|
@@ -216,6 +224,77 @@ type OperationConfigInput<TBefore, TAfter> = {
|
|
|
216
224
|
pagination?: Partial<PaginationConfig>
|
|
217
225
|
}
|
|
218
226
|
|
|
227
|
+
/**
|
|
228
|
+
* The seven guard controls, resolved — the RUNTIME half.
|
|
229
|
+
*
|
|
230
|
+
* This lives here, not in the compiled `src/guardOptions.ts`, because a generated
|
|
231
|
+
* project gets these files copied in and must run with no dependency on this
|
|
232
|
+
* package. The two cannot import each other across that boundary, so the values
|
|
233
|
+
* appear twice and `guardOptionIndependence.test.ts` asserts they agree — against
|
|
234
|
+
* this resolver, which is the one the router actually runs.
|
|
235
|
+
*
|
|
236
|
+
* The machine-readable METADATA is compiled and public: see src/guardOptions.ts.
|
|
237
|
+
* Defining it here would put it in a file that never reaches `dist`.
|
|
238
|
+
*/
|
|
239
|
+
export type GuardPolicy = {
|
|
240
|
+
requireGuardShape: boolean
|
|
241
|
+
validateGuardShapes: boolean
|
|
242
|
+
requireDefaultVariantOptIn: boolean
|
|
243
|
+
enableUpdateEach: boolean
|
|
244
|
+
guardResolutionOrder: 'after-hooks' | 'before-hooks'
|
|
245
|
+
allowE2EGuardBypass: boolean
|
|
246
|
+
validateResolvedShapes: boolean
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* PRE-1.64.2 BEHAVIOUR, exactly. Changing any value here changes what an
|
|
251
|
+
* unconfigured consumer gets on upgrade, which is the thing that must not happen.
|
|
252
|
+
*/
|
|
253
|
+
export const UPSTREAM_GUARD_DEFAULTS: Readonly<GuardPolicy> = Object.freeze({
|
|
254
|
+
requireGuardShape: false,
|
|
255
|
+
validateGuardShapes: false,
|
|
256
|
+
requireDefaultVariantOptIn: false,
|
|
257
|
+
enableUpdateEach: true,
|
|
258
|
+
guardResolutionOrder: 'after-hooks',
|
|
259
|
+
allowE2EGuardBypass: true,
|
|
260
|
+
validateResolvedShapes: false,
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Every control at its most restrictive — a SHORTCUT, not a mode.
|
|
265
|
+
*
|
|
266
|
+
* Spread it, edit what you disagree with, and store the result. There is
|
|
267
|
+
* deliberately no `preset: 'hardened'` option: a stored preset name would mean a
|
|
268
|
+
* future version of this package could silently change what a consumer's
|
|
269
|
+
* configuration does, which is the same class of mistake as 1.64.2 itself.
|
|
270
|
+
*/
|
|
271
|
+
export const HARDENED_GUARD_PROFILE: Readonly<GuardPolicy> = Object.freeze({
|
|
272
|
+
requireGuardShape: true,
|
|
273
|
+
validateGuardShapes: true,
|
|
274
|
+
requireDefaultVariantOptIn: true,
|
|
275
|
+
enableUpdateEach: false,
|
|
276
|
+
guardResolutionOrder: 'before-hooks',
|
|
277
|
+
allowE2EGuardBypass: false,
|
|
278
|
+
validateResolvedShapes: true,
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
/** Fill in the defaults. Absent means upstream, never an opinion. */
|
|
282
|
+
export function resolveGuardPolicy(config: Partial<GuardPolicy> | undefined): GuardPolicy {
|
|
283
|
+
return {
|
|
284
|
+
requireGuardShape: config?.requireGuardShape ?? UPSTREAM_GUARD_DEFAULTS.requireGuardShape,
|
|
285
|
+
validateGuardShapes: config?.validateGuardShapes ?? UPSTREAM_GUARD_DEFAULTS.validateGuardShapes,
|
|
286
|
+
requireDefaultVariantOptIn:
|
|
287
|
+
config?.requireDefaultVariantOptIn ?? UPSTREAM_GUARD_DEFAULTS.requireDefaultVariantOptIn,
|
|
288
|
+
enableUpdateEach: config?.enableUpdateEach ?? UPSTREAM_GUARD_DEFAULTS.enableUpdateEach,
|
|
289
|
+
guardResolutionOrder:
|
|
290
|
+
config?.guardResolutionOrder ?? UPSTREAM_GUARD_DEFAULTS.guardResolutionOrder,
|
|
291
|
+
allowE2EGuardBypass:
|
|
292
|
+
config?.allowE2EGuardBypass ?? UPSTREAM_GUARD_DEFAULTS.allowE2EGuardBypass,
|
|
293
|
+
validateResolvedShapes:
|
|
294
|
+
config?.validateResolvedShapes ?? UPSTREAM_GUARD_DEFAULTS.validateResolvedShapes,
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
219
298
|
function classifyGuardRouting(shape: unknown): NormalizedGuardRouting {
|
|
220
299
|
if (shape === undefined) return { kind: 'none' }
|
|
221
300
|
if (typeof shape === 'function') return { kind: 'single' }
|
|
@@ -295,34 +374,79 @@ function validateShapeConfig(shape: unknown, location: string): void {
|
|
|
295
374
|
export function validateOperationConfig(
|
|
296
375
|
config: { shape?: unknown; variants?: unknown; allowDefaultVariant?: unknown } | undefined,
|
|
297
376
|
location: string,
|
|
377
|
+
policy: Partial<GuardPolicy> = {},
|
|
298
378
|
): void {
|
|
379
|
+
const { requireGuardShape, validateGuardShapes, requireDefaultVariantOptIn } =
|
|
380
|
+
resolveGuardPolicy(policy)
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* THE CHECKS THAT PREDATE 1.64.2 RUN UNCONDITIONALLY.
|
|
384
|
+
*
|
|
385
|
+
* Everything below them is gated per option. Note each gate stands alone: a
|
|
386
|
+
* consumer who enables `validateGuardShapes` gets shape validation and NOT a
|
|
387
|
+
* missing-guard refusal, and vice versa. That independence is the point, and
|
|
388
|
+
* `guardOptionIndependence.test.ts` asserts it option by option.
|
|
389
|
+
*/
|
|
299
390
|
if (!config) {
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
391
|
+
if (requireGuardShape) {
|
|
392
|
+
throw new Error(
|
|
393
|
+
location + ': no guard configured. Every generated operation must define ' +
|
|
394
|
+
'"shape" or "variants"; an unguarded operation would pass the caller\'s ' +
|
|
395
|
+
'own where/select/include straight to Prisma.',
|
|
396
|
+
)
|
|
397
|
+
}
|
|
398
|
+
return
|
|
305
399
|
}
|
|
306
400
|
|
|
307
401
|
const hasShape = config.shape !== undefined
|
|
308
402
|
const hasVariants = config.variants !== undefined
|
|
309
403
|
|
|
404
|
+
// Pre-1.64.2. Not gated.
|
|
310
405
|
if (hasShape && hasVariants) {
|
|
311
406
|
throw new Error(location + ': shape and variants cannot both be defined')
|
|
312
407
|
}
|
|
313
408
|
|
|
314
409
|
if (!hasShape && !hasVariants) {
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
410
|
+
if (requireGuardShape) {
|
|
411
|
+
throw new Error(
|
|
412
|
+
location + ': no guard configured. Define "shape" or "variants".',
|
|
413
|
+
)
|
|
414
|
+
}
|
|
415
|
+
return
|
|
318
416
|
}
|
|
319
417
|
|
|
320
418
|
if (hasShape) {
|
|
321
|
-
validateShapeConfig(config.shape, location)
|
|
419
|
+
if (validateGuardShapes) validateShapeConfig(config.shape, location)
|
|
322
420
|
return
|
|
323
421
|
}
|
|
324
422
|
|
|
325
|
-
|
|
423
|
+
validateVariantMap(
|
|
424
|
+
config.variants,
|
|
425
|
+
location,
|
|
426
|
+
{ validateGuardShapes, requireDefaultVariantOptIn },
|
|
427
|
+
config.allowDefaultVariant,
|
|
428
|
+
)
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* The variant-map checks.
|
|
433
|
+
*
|
|
434
|
+
* The four that predate 1.64.2 — non-array object, at least one entry, no
|
|
435
|
+
* reserved-key collision, every entry an object carrying a `shape` — run
|
|
436
|
+
* ALWAYS, because they are not new and removing them would be its own breaking
|
|
437
|
+
* change.
|
|
438
|
+
*
|
|
439
|
+
* The two the hardening added are gated on their OWN options, independently:
|
|
440
|
+
* `requireDefaultVariantOptIn` confirms a `default` variant,
|
|
441
|
+
* `validateGuardShapes` validates each variant's shape the way a top-level
|
|
442
|
+
* `shape` is validated. Enabling either does not enable the other.
|
|
443
|
+
*/
|
|
444
|
+
function validateVariantMap(
|
|
445
|
+
variants: unknown,
|
|
446
|
+
location: string,
|
|
447
|
+
flags: { validateGuardShapes: boolean; requireDefaultVariantOptIn: boolean },
|
|
448
|
+
allowDefaultVariant?: unknown,
|
|
449
|
+
): void {
|
|
326
450
|
if (!isPlainObject(variants)) {
|
|
327
451
|
throw new Error(location + ': variants must be a non-array object')
|
|
328
452
|
}
|
|
@@ -343,18 +467,20 @@ export function validateOperationConfig(
|
|
|
343
467
|
* writes on purpose; a key called `default` is one they may have typed without
|
|
344
468
|
* noticing what it catches.
|
|
345
469
|
*/
|
|
346
|
-
if (
|
|
347
|
-
if (
|
|
470
|
+
if (flags.requireDefaultVariantOptIn) {
|
|
471
|
+
if (Object.prototype.hasOwnProperty.call(variants, 'default')) {
|
|
472
|
+
if (allowDefaultVariant !== true) {
|
|
473
|
+
throw new Error(
|
|
474
|
+
location + ': a variant named "default" answers every unrecognised, ' +
|
|
475
|
+
'blank and missing caller. Set allowDefaultVariant: true to confirm ' +
|
|
476
|
+
'that is intended, and make sure it is the most restrictive variant.',
|
|
477
|
+
)
|
|
478
|
+
}
|
|
479
|
+
} else if (allowDefaultVariant !== undefined) {
|
|
348
480
|
throw new Error(
|
|
349
|
-
location + ':
|
|
350
|
-
'blank and missing caller. Set allowDefaultVariant: true to confirm ' +
|
|
351
|
-
'that is intended, and make sure it is the most restrictive variant.',
|
|
481
|
+
location + ': allowDefaultVariant is set but no "default" variant exists.',
|
|
352
482
|
)
|
|
353
483
|
}
|
|
354
|
-
} else if (config.allowDefaultVariant !== undefined) {
|
|
355
|
-
throw new Error(
|
|
356
|
-
location + ': allowDefaultVariant is set but no "default" variant exists.',
|
|
357
|
-
)
|
|
358
484
|
}
|
|
359
485
|
|
|
360
486
|
for (const [key, rawEntry] of entries) {
|
|
@@ -384,7 +510,9 @@ export function validateOperationConfig(
|
|
|
384
510
|
* different key. A guard is not more trustworthy for being written inside a
|
|
385
511
|
* variant.
|
|
386
512
|
*/
|
|
387
|
-
|
|
513
|
+
if (flags.validateGuardShapes) {
|
|
514
|
+
validateShapeConfig(rawEntry.shape, location + ' variant "' + key + '"')
|
|
515
|
+
}
|
|
388
516
|
}
|
|
389
517
|
}
|
|
390
518
|
|
|
@@ -117,10 +117,15 @@ export function generateRouteConfigType(
|
|
|
117
117
|
` after?: ${afterRef}[]\n` +
|
|
118
118
|
` }>`
|
|
119
119
|
|
|
120
|
+
// Only consulted when `requireDefaultVariantOptIn` is on; inert otherwise.
|
|
121
|
+
// Emitted so a TypeScript consumer who opts in can confirm a `default`
|
|
122
|
+
// variant without a cast — 1.64.2 demanded it and never added it to the type.
|
|
123
|
+
const allowDefault = ` allowDefaultVariant?: boolean`
|
|
124
|
+
|
|
120
125
|
return (
|
|
121
126
|
` ${routerOp}?: (${commonConfig} & (\n` +
|
|
122
127
|
` | { shape?: ${m}${c}ShapeInput<TCtx>; variants?: never }\n` +
|
|
123
|
-
` | { shape?: never; variants: ${variantsConfig} }\n` +
|
|
128
|
+
` | { shape?: never; variants: ${variantsConfig}\n${allowDefault} }\n` +
|
|
124
129
|
` )) | false`
|
|
125
130
|
)
|
|
126
131
|
}).join('\n')
|
|
@@ -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
|