prisma-generator-express 1.64.1 → 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.
@@ -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')
@@ -80,6 +80,55 @@ function emitWriteOp(meta: (typeof OPERATION_METADATA)[number], modelName: strin
80
80
  }`
81
81
  }
82
82
 
83
+ /**
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.
106
+ *
107
+ * The emitted router used to compute `DROP_GUARD = <flag> || _env.E2E === 'true'`,
108
+ * so setting `E2E=true` in a deployed environment downgraded enforcement even
109
+ * when the generator had been told to keep the guard. The two modes are not
110
+ * equivalent: with the guard, the shape goes to prisma-guard; with it dropped,
111
+ * `applyDroppedGuard` applies projection defaults and forced `where` clauses and
112
+ * nothing else is validated against the shape.
113
+ *
114
+ * On an edge runtime that variable is an ordinary config var — set on a staging
115
+ * deployment, copied forward, flagged as security-relevant nowhere. A
116
+ * deployment's guard behaviour has to be a property of the artifact, not of the
117
+ * environment it happens to land in.
118
+ *
119
+ * The rationale lives here rather than in the emitted file on purpose: generated
120
+ * output is an artifact, and a paragraph about a bypass that no longer exists
121
+ * would be copied into every router this generator writes.
122
+ *
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.
131
+ */
83
132
  export function generateHonoRouterFunction({
84
133
  model,
85
134
  enums,
@@ -133,6 +182,8 @@ import { buildModelOpenApi } from '../buildModelOpenApi${ext}'
133
182
  import {
134
183
  normalizeOperation,
135
184
  resolveOperationVariantKey,
185
+ resolveGuardPolicy,
186
+ resolveGuardShapeOnce,
136
187
  validateCountSourceWhere,
137
188
  validateOperationConfig,
138
189
  validateUpdateEachConfig,
@@ -149,7 +200,10 @@ import { MODEL_FIELDS, MODEL_ENUMS } from './${modelName}Metadata${ext}'
149
200
  ${generateRouteConfigType(modelName, 'HonoBeforeHook', guardShapesImport, importStyle, 'hono')}
150
201
  const _env = getEnv()
151
202
 
152
- const DROP_GUARD = ${dropGuard} || _env.E2E === 'true'
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.
206
+ const DROP_GUARD = ${dropGuard}
153
207
 
154
208
  type JsonLike =
155
209
  | string
@@ -207,28 +261,28 @@ async function parseBodyAsQueryMiddleware(c: HandlerContext): Promise<void> {
207
261
  c.set('parsedQuery', sanitizeKeys(body as Record<string, unknown>))
208
262
  }
209
263
 
210
- async function parseWriteBodyMiddleware(c: HandlerContext): Promise<void> {
264
+ async function parseUpdateEachBodyMiddleware(c: HandlerContext): Promise<void> {
211
265
  let body: unknown
212
266
  try {
213
267
  body = await c.req.json()
214
268
  } catch {
215
- throw new HTTPException(400, { message: 'Request body must be a JSON object' })
269
+ throw new HTTPException(400, { message: 'updateEach body must be an array of { where, data } items' })
216
270
  }
217
- if (!body || typeof body !== 'object' || Array.isArray(body)) {
218
- throw new HTTPException(400, { message: 'Request body must be a JSON object' })
271
+ if (!Array.isArray(body)) {
272
+ throw new HTTPException(400, { message: 'updateEach body must be an array of { where, data } items' })
219
273
  }
220
274
  c.set('body', body)
221
275
  }
222
276
 
223
- async function parseUpdateEachBodyMiddleware(c: HandlerContext): Promise<void> {
277
+ async function parseWriteBodyMiddleware(c: HandlerContext): Promise<void> {
224
278
  let body: unknown
225
279
  try {
226
280
  body = await c.req.json()
227
281
  } catch {
228
- throw new HTTPException(400, { message: 'updateEach body must be an array of { where, data } items' })
282
+ throw new HTTPException(400, { message: 'Request body must be a JSON object' })
229
283
  }
230
- if (!Array.isArray(body)) {
231
- throw new HTTPException(400, { message: 'updateEach body must be an array of { where, data } items' })
284
+ if (!body || typeof body !== 'object' || Array.isArray(body)) {
285
+ throw new HTTPException(400, { message: 'Request body must be a JSON object' })
232
286
  }
233
287
  c.set('body', body)
234
288
  }
@@ -238,6 +292,17 @@ function makeShapeMiddleware<TCtx, TPrisma, TEnv extends HonoEnvBase>(
238
292
  opConfig: NormalizedOp<TEnv>,
239
293
  opKind: OpKind,
240
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
+
241
306
  return async (c: Context<GeneratedHonoEnv<TEnv>>): Promise<void> => {
242
307
  const merged = mergePaginationConfig(config.pagination, opConfig.pagination)
243
308
  if (merged) c.set('routeConfig', { pagination: merged })
@@ -260,15 +325,35 @@ function makeShapeMiddleware<TCtx, TPrisma, TEnv extends HonoEnvBase>(
260
325
  if (resolvedKey !== undefined) c.set('guardVariantKey', resolvedKey)
261
326
 
262
327
  if (opConfig.guardShape) {
263
- if (!DROP_GUARD) {
264
- c.set('guardShape', opConfig.guardShape)
265
- } else {
266
- const resolveCtx = typeof config.resolveContext === 'function'
267
- ? () => (config.resolveContext as (ctx: Context<GeneratedHonoEnv<TEnv>>) => unknown | Promise<unknown>)(c)
268
- : undefined
328
+ const resolveCtx = typeof config.resolveContext === 'function'
329
+ ? () => (config.resolveContext as (ctx: Context<GeneratedHonoEnv<TEnv>>) => unknown | Promise<unknown>)(c)
330
+ : undefined
331
+
332
+ /**
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.
341
+ */
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
350
+ }
269
351
 
352
+ if (!dropGuard) {
353
+ c.set('guardShape', effectiveShape)
354
+ } else {
270
355
  await applyDroppedGuard(
271
- opConfig.guardShape,
356
+ effectiveShape,
272
357
  resolvedKey,
273
358
  resolveCtx,
274
359
  opKind,
@@ -410,6 +495,38 @@ export function ${routerFunctionName}<TCtx = unknown, TPrisma = any, TEnv extend
410
495
  })
411
496
  }
412
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
+
413
530
  const handleRead = (
414
531
  opConfig: NormalizedOp<TEnv>,
415
532
  handlerFn: (c: HandlerContext) => Promise<void>,
@@ -419,15 +536,15 @@ export function ${routerFunctionName}<TCtx = unknown, TPrisma = any, TEnv extend
419
536
  try {
420
537
  await parseFn(c as unknown as HandlerContext)
421
538
  await makeShapeMiddleware<TCtx, TPrisma, TEnv>(config, opConfig, opKind)(c)
539
+
540
+ // Settled BEFORE any hook can answer the request, when asked for.
541
+ if (SETTLE_BEFORE_HOOKS) settleGuard(c)
542
+
422
543
  const operationBefore = await runBeforeHooks<TEnv>(opConfig.operationBefore, c)
423
544
  if (operationBefore) return operationBefore
424
545
 
425
- const failure = c.get('guardVariantFailure')
426
- if (failure) {
427
- throw new HTTPException(400, {
428
- message: formatGuardVariantResolutionError(failure),
429
- })
430
- }
546
+ // Upstream order: hooks first, guard failure after.
547
+ if (!SETTLE_BEFORE_HOOKS) settleGuard(c)
431
548
 
432
549
  const key = c.get('guardVariantKey')
433
550
  const variantHooks =
@@ -454,15 +571,15 @@ export function ${routerFunctionName}<TCtx = unknown, TPrisma = any, TEnv extend
454
571
  try {
455
572
  await parseWriteBodyMiddleware(c as unknown as HandlerContext)
456
573
  await makeShapeMiddleware<TCtx, TPrisma, TEnv>(config, opConfig, opKind)(c)
574
+
575
+ // Settled BEFORE any hook can answer the request, when asked for.
576
+ if (SETTLE_BEFORE_HOOKS) settleGuard(c)
577
+
457
578
  const operationBefore = await runBeforeHooks<TEnv>(opConfig.operationBefore, c)
458
579
  if (operationBefore) return operationBefore
459
580
 
460
- const failure = c.get('guardVariantFailure')
461
- if (failure) {
462
- throw new HTTPException(400, {
463
- message: formatGuardVariantResolutionError(failure),
464
- })
465
- }
581
+ // Upstream order: hooks first, guard failure after.
582
+ if (!SETTLE_BEFORE_HOOKS) settleGuard(c)
466
583
 
467
584
  const key = c.get('guardVariantKey')
468
585
  const variantHooks =
@@ -485,7 +602,7 @@ export function ${routerFunctionName}<TCtx = unknown, TPrisma = any, TEnv extend
485
602
  key: K,
486
603
  ): NormalizedOp<TEnv> => {
487
604
  const raw = config[key] as unknown as OperationConfigLike<TEnv> | undefined
488
- validateOperationConfig(raw, '${modelName}.' + String(key))
605
+ validateOperationConfig(raw, '${modelName}.' + String(key), POLICY)
489
606
  return normalizeHonoOperation(raw)
490
607
  }
491
608
 
@@ -494,6 +611,25 @@ ${readOpBlocks}
494
611
  ${writeOpBlocks}
495
612
 
496
613
  if (config.updateEach) {
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
+
497
633
  const rawUpdateEach = config.updateEach as unknown as OperationConfigLike<TEnv>
498
634
  validateUpdateEachConfig(rawUpdateEach, '${modelName}.updateEach')
499
635
  const opConfig = normalizeHonoOperation(rawUpdateEach)
@@ -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,