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.
@@ -81,7 +81,28 @@ function emitWriteOp(meta: (typeof OPERATION_METADATA)[number], modelName: strin
81
81
  }
82
82
 
83
83
  /**
84
- * Guard dropping is a GENERATION-TIME decision, never a runtime one.
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. It bypasses
103
- * guard shapes by design — the endpoint is a batch of `{ where, data }` items
104
- * applied directly — and the only thing between it and an unguarded mass
105
- * mutation was a `console.warn` suppressed in production. A warning is not a
106
- * security boundary; it is advice to whoever happens to be reading a development
107
- * log. Refused at construction rather than silently dropped, so a deployment
108
- * expecting the route learns at boot instead of at the first 404.
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. Never read from the environment — see
180
- // generateRouterHono.ts.
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
- * Resolved ONCE, here, and the validated value is what travels onward —
285
- * see resolveGuardShapeOnce. Passing the function on would let it return a
286
- * different shape when it is enforced than when it was checked.
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
- const resolution = await resolveGuardShapeOnce(opConfig.guardShape, resolvedKey, resolveCtx)
289
- if (!resolution.ok) {
290
- c.set('guardShapeFailure', resolution.problem)
291
- return
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 (!DROP_GUARD) {
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
- * Variant resolution is settled BEFORE any operation hook runs.
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 before any operation hook — see the read handler above.
513
- const failure = c.get('guardVariantFailure')
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
- throw new Error(
568
- '${modelName}.updateEach: this target does not register updateEach. It bypasses ' +
569
- 'guard shapes entirely, so there is no configuration that makes it safe to ' +
570
- 'expose. Perform batch updates through a guarded operation, or behind your own ' +
571
- 'authenticated route outside the generated router.',
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,