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.
- package/dist/generators/generateRouteConfigType.js +2 -1
- package/dist/generators/generateRouteConfigType.js.map +1 -1
- package/dist/generators/generateRouterHono.js +116 -29
- 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/projectionDefaults.ts +6 -1
- package/src/copy/routeConfig.hono.ts +85 -0
- package/src/copy/routeConfig.ts +700 -361
- package/src/generators/generateRouteConfigType.ts +6 -1
- package/src/generators/generateRouterHono.ts +165 -29
- package/src/guardOptions.ts +226 -0
- package/src/index.ts +19 -0
|
@@ -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
|
-
|
|
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
|
|
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: '
|
|
269
|
+
throw new HTTPException(400, { message: 'updateEach body must be an array of { where, data } items' })
|
|
216
270
|
}
|
|
217
|
-
if (!
|
|
218
|
-
throw new HTTPException(400, { message: '
|
|
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
|
|
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: '
|
|
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: '
|
|
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
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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
|
-
|
|
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
|
-
|
|
426
|
-
if (
|
|
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
|
-
|
|
461
|
-
if (
|
|
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,
|