@mandujs/core 0.53.2 → 0.54.0
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/README.md +654 -654
- package/package.json +2 -2
- package/src/a11y/__tests__/run-audit.test.ts +333 -333
- package/src/a11y/fix-hints.ts +76 -76
- package/src/a11y/index.ts +18 -18
- package/src/a11y/run-audit.ts +394 -394
- package/src/a11y/types.ts +125 -125
- package/src/auth/__tests__/login.test.ts +1 -1
- package/src/auth/__tests__/password.test.ts +122 -122
- package/src/auth/__tests__/tokens.test.ts +274 -274
- package/src/auth/__tests__/verification.test.ts +274 -274
- package/src/auth/index.ts +76 -76
- package/src/auth/login.ts +225 -225
- package/src/auth/password.ts +120 -120
- package/src/auth/reset.ts +243 -243
- package/src/auth/tokens.ts +612 -612
- package/src/auth/verification.ts +253 -253
- package/src/brain/__tests__/redactor.test.ts +94 -94
- package/src/brain/adapters/__tests__/_helpers.ts +83 -83
- package/src/brain/adapters/__tests__/anthropic-oauth.test.ts +196 -196
- package/src/brain/adapters/__tests__/chatgpt-auth.test.ts +193 -193
- package/src/brain/adapters/__tests__/openai-oauth.test.ts +209 -209
- package/src/brain/adapters/__tests__/resolver.test.ts +143 -143
- package/src/brain/adapters/anthropic-oauth.ts +1 -1
- package/src/brain/adapters/chatgpt-auth.ts +300 -300
- package/src/brain/adapters/index.ts +319 -319
- package/src/brain/adapters/oauth-flow.ts +439 -439
- package/src/brain/consent.ts +240 -240
- package/src/brain/credentials.ts +396 -396
- package/src/bundler/__tests__/build-runner.ts +113 -113
- package/src/bundler/__tests__/dev-reliability.test.ts +619 -619
- package/src/bundler/__tests__/extended-watch.test.ts +711 -711
- package/src/bundler/__tests__/fast-refresh.test.ts +10 -10
- package/src/bundler/__tests__/hdr.test.ts +353 -353
- package/src/bundler/__tests__/hmr-client.test.ts +532 -532
- package/src/bundler/__tests__/manifest-schema.test.ts +266 -266
- package/src/bundler/__tests__/prod-smoke.test.ts +138 -138
- package/src/bundler/__tests__/reverse-import-graph.test.ts +519 -519
- package/src/bundler/__tests__/slot-dispatch.test.ts +573 -573
- package/src/bundler/__tests__/url-cap-and-slot-regex.test.ts +286 -286
- package/src/bundler/__tests__/vendor-cache.test.ts +455 -455
- package/src/bundler/budget.ts +404 -404
- package/src/bundler/build.test.ts +179 -179
- package/src/bundler/build.ts +55 -55
- package/src/bundler/dev.ts +42 -42
- package/src/bundler/manifest-schema.ts +301 -301
- package/src/bundler/plugins/__tests__/block-generated-imports.test.ts +263 -263
- package/src/bundler/plugins/__tests__/react-compiler-config.test.ts +83 -83
- package/src/bundler/plugins/__tests__/react-compiler-lint.test.ts +110 -110
- package/src/bundler/plugins/block-generated-imports.ts +155 -155
- package/src/bundler/plugins/index.ts +83 -83
- package/src/bundler/plugins/react-compiler-config.ts +108 -108
- package/src/bundler/plugins/react-compiler-lint.ts +253 -253
- package/src/bundler/plugins/react-compiler.ts +162 -162
- package/src/bundler/reverse-import-graph.ts +339 -339
- package/src/bundler/safe-build.test.ts +201 -201
- package/src/bundler/safe-build.ts +103 -103
- package/src/bundler/scenario-matrix.ts +229 -229
- package/src/bundler/types.ts +10 -10
- package/src/bundler/vendor-cache-types.ts +130 -130
- package/src/bundler/vendor-cache.ts +526 -526
- package/src/change/snapshot.ts +18 -18
- package/src/client/Form.tsx +105 -105
- package/src/client/Link.tsx +9 -9
- package/src/client/__tests__/use-sse.test.ts +153 -153
- package/src/client/globals.ts +1 -1
- package/src/client/hooks.ts +362 -362
- package/src/client/hydrate.ts +340 -340
- package/src/client/prefetch-helper.ts +55 -55
- package/src/client/router.ts +11 -11
- package/src/client/runtime.ts +47 -47
- package/src/client/serialize.ts +404 -404
- package/src/client/use-fetch.ts +6 -6
- package/src/client/use-head.ts +197 -197
- package/src/client/use-sse.ts +378 -378
- package/src/client/window-state.ts +101 -101
- package/src/components/Image.tsx +162 -162
- package/src/config/validate.ts +3 -3
- package/src/config/watcher.ts +311 -311
- package/src/constants.ts +40 -40
- package/src/content/collection.ts +8 -8
- package/src/content/content-layer.ts +7 -7
- package/src/content/data-store.ts +245 -245
- package/src/content/frontmatter.ts +189 -189
- package/src/content/loader-context.ts +171 -171
- package/src/content/loaders/api.ts +216 -216
- package/src/content/loaders/file.ts +172 -172
- package/src/content/loaders/glob.ts +253 -253
- package/src/content/loaders/index.ts +34 -34
- package/src/content/loaders/types.ts +137 -137
- package/src/content/meta-store.ts +209 -209
- package/src/content/prebuild.test.ts +571 -571
- package/src/content/prebuild.ts +636 -636
- package/src/content/schema.ts +20 -20
- package/src/content/sidebar.ts +630 -630
- package/src/content/slug.ts +110 -110
- package/src/content/types.ts +282 -282
- package/src/content/watcher.ts +135 -135
- package/src/contract/client-safe.test.ts +42 -42
- package/src/contract/client-safe.ts +114 -114
- package/src/contract/define.ts +11 -11
- package/src/contract/index.ts +1 -1
- package/src/contract/normalize.test.ts +276 -276
- package/src/contract/normalize.ts +410 -410
- package/src/contract/registry.test.ts +206 -206
- package/src/contract/route-helpers.ts +1 -1
- package/src/contract/rpc.ts +443 -443
- package/src/contract/schema.ts +48 -48
- package/src/contract/types.ts +58 -58
- package/src/db/__tests__/db.test.ts +482 -482
- package/src/db/index.ts +138 -138
- package/src/db/migrations/history-table.ts +345 -345
- package/src/db/migrations/index.ts +3 -3
- package/src/db/migrations/lock.ts +324 -324
- package/src/db/migrations/runner.ts +650 -650
- package/src/deploy/cache.ts +140 -140
- package/src/deploy/compile/vercel.ts +344 -344
- package/src/deploy/index.ts +87 -87
- package/src/deploy/inference/brain.ts +268 -268
- package/src/deploy/inference/context.ts +182 -182
- package/src/deploy/inference/filling-extract.ts +245 -245
- package/src/deploy/inference/heuristic.ts +182 -182
- package/src/deploy/intent.ts +173 -173
- package/src/deploy/plan.ts +178 -178
- package/src/design/__tests__/agents-link.test.ts +109 -109
- package/src/design/__tests__/extract-patch-diff.test.ts +265 -265
- package/src/design/__tests__/lint.test.ts +110 -110
- package/src/design/__tests__/parser.test.ts +195 -195
- package/src/design/__tests__/tailwind-theme.test.ts +229 -229
- package/src/design/agents-link.ts +165 -165
- package/src/design/diff.ts +138 -138
- package/src/design/extract.ts +285 -285
- package/src/design/index.ts +102 -102
- package/src/design/lint.ts +209 -209
- package/src/design/parser.ts +555 -555
- package/src/design/patch.ts +241 -241
- package/src/design/scaffold.ts +147 -147
- package/src/design/tailwind-theme.ts +441 -441
- package/src/design/types.ts +210 -210
- package/src/desktop/__tests__/webview-fallback.test.ts +254 -254
- package/src/desktop/__tests__/window.test.ts +248 -248
- package/src/desktop/__tests__/worker.test.ts +266 -266
- package/src/desktop/index.ts +43 -43
- package/src/desktop/types.ts +158 -158
- package/src/desktop/worker.ts +180 -180
- package/src/dev-error-overlay/__tests__/overlay-injector.test.ts +241 -241
- package/src/dev-error-overlay/index.ts +30 -30
- package/src/dev-error-overlay/overlay-injector.ts +243 -243
- package/src/dev-error-overlay/overlay-styles.ts +52 -52
- package/src/dev-error-overlay/types.ts +66 -66
- package/src/devtools/ai/context-builder.ts +375 -375
- package/src/devtools/ai/index.ts +25 -25
- package/src/devtools/ai/mcp-connector.ts +25 -25
- package/src/devtools/client/catchers/error-catcher.ts +344 -344
- package/src/devtools/client/catchers/index.ts +18 -18
- package/src/devtools/client/components/index.ts +39 -39
- package/src/devtools/client/components/mandu-character.tsx +331 -331
- package/src/devtools/client/components/overlay.tsx +368 -368
- package/src/devtools/client/components/panel/errors-panel.tsx +259 -259
- package/src/devtools/client/components/panel/guard-panel.tsx +30 -30
- package/src/devtools/client/components/panel/islands-panel.tsx +320 -320
- package/src/devtools/client/components/panel/network-panel.tsx +291 -291
- package/src/devtools/client/components/panel/panel-container.tsx +500 -500
- package/src/devtools/client/components/panel/preview-panel.tsx +46 -46
- package/src/devtools/client/filters/context-filters.ts +282 -282
- package/src/devtools/client/filters/index.ts +16 -16
- package/src/devtools/client/index.ts +63 -63
- package/src/devtools/client/persistence.ts +335 -335
- package/src/devtools/hook/create-hook.ts +207 -207
- package/src/devtools/hook/index.ts +13 -13
- package/src/devtools/index.ts +439 -439
- package/src/devtools/init.ts +265 -265
- package/src/devtools/protocol.ts +237 -237
- package/src/devtools/server/index.ts +17 -17
- package/src/devtools/server/source-context.ts +450 -450
- package/src/devtools/types.ts +35 -35
- package/src/devtools/worker/index.ts +25 -25
- package/src/devtools/worker/redaction-worker.ts +233 -233
- package/src/devtools/worker/worker-manager.ts +410 -410
- package/src/diagnose/__tests__/checks.test.ts +451 -380
- package/src/diagnose/checks.ts +832 -720
- package/src/diagnose/index.ts +17 -16
- package/src/diagnose/run.ts +93 -91
- package/src/diagnose/types.ts +53 -53
- package/src/email/__tests__/email.test.ts +355 -355
- package/src/email/index.ts +282 -282
- package/src/email/smtp.ts +64 -64
- package/src/error/domains.ts +265 -265
- package/src/error/result.ts +60 -60
- package/src/error/types.ts +6 -6
- package/src/errors/extractor.ts +409 -409
- package/src/errors/index.ts +19 -19
- package/src/filling/__tests__/session-sqlite.test.ts +454 -454
- package/src/filling/auth.ts +308 -308
- package/src/filling/cookie-codec.ts +299 -299
- package/src/filling/deps.ts +265 -265
- package/src/filling/session-sqlite.ts +617 -617
- package/src/filling/sse.ts +5 -5
- package/src/filling/ws.ts +78 -78
- package/src/generator/index.ts +3 -3
- package/src/guard/__tests__/design-inline-class.test.ts +219 -219
- package/src/guard/__tests__/tsgolint-bridge.test.ts +347 -347
- package/src/guard/analyzer.ts +360 -360
- package/src/guard/auto-correct.ts +1 -1
- package/src/guard/contract-guard.ts +9 -9
- package/src/guard/define-rule.ts +243 -243
- package/src/guard/design-inline-class.ts +393 -393
- package/src/guard/file-type.test.ts +24 -24
- package/src/guard/graph.ts +1 -1
- package/src/guard/healing.ts +36 -36
- package/src/guard/presets/atomic.ts +70 -70
- package/src/guard/presets/clean.ts +77 -77
- package/src/guard/presets/fsd.ts +79 -79
- package/src/guard/presets/hexagonal.ts +68 -68
- package/src/guard/reporter.ts +442 -442
- package/src/guard/rule-presets.ts +379 -379
- package/src/guard/semantic-slots.ts +1 -1
- package/src/guard/suggestions.ts +358 -358
- package/src/guard/tsgolint-bridge.ts +512 -512
- package/src/guard/types.ts +348 -348
- package/src/guard/watcher.ts +405 -405
- package/src/i18n/define.ts +126 -126
- package/src/i18n/index.ts +52 -52
- package/src/i18n/message-registry.ts +173 -173
- package/src/i18n/types.ts +112 -112
- package/src/id/index.ts +105 -105
- package/src/index.ts +2 -2
- package/src/kitchen/api/agent-devtools-api.ts +779 -544
- package/src/kitchen/api/errors-grouping.ts +126 -0
- package/src/kitchen/kitchen-handler.ts +192 -63
- package/src/kitchen/kitchen-ui.ts +842 -464
- package/src/logging/index.ts +22 -22
- package/src/logging/transports.ts +365 -365
- package/src/middleware/bridge.ts +147 -147
- package/src/middleware/compose.ts +134 -134
- package/src/middleware/compress.ts +62 -62
- package/src/middleware/cors.ts +47 -47
- package/src/middleware/csrf.ts +328 -328
- package/src/middleware/define.ts +132 -132
- package/src/middleware/jwt.ts +134 -134
- package/src/middleware/logger.ts +58 -58
- package/src/middleware/oauth/__tests__/oauth.test.ts +1 -1
- package/src/middleware/oauth/index.ts +505 -505
- package/src/middleware/oauth/providers.ts +115 -115
- package/src/middleware/rate-limit/index.ts +522 -522
- package/src/middleware/rate-limit/sqlite-store.ts +382 -382
- package/src/middleware/scheduler-cron.ts +96 -96
- package/src/middleware/secure/__tests__/secure.test.ts +360 -360
- package/src/middleware/secure/csp.ts +193 -193
- package/src/middleware/session.ts +174 -174
- package/src/middleware/timeout.ts +55 -55
- package/src/observability/logger-adapter.ts +36 -36
- package/src/observability/sqlite-store.ts +254 -254
- package/src/openapi/generator.ts +1 -1
- package/src/openapi/openapi.test.ts +43 -43
- package/src/perf/__tests__/user-marks.test.ts +354 -354
- package/src/perf/index.ts +133 -133
- package/src/plugins/__tests__/lifecycle-integration.test.ts +272 -272
- package/src/plugins/__tests__/runner.test.ts +409 -409
- package/src/plugins/define.ts +124 -124
- package/src/plugins/examples/dep-check-plugin.ts +80 -80
- package/src/plugins/examples/prerender-cache-plugin.ts +111 -111
- package/src/plugins/examples/sitemap-plugin.ts +65 -65
- package/src/plugins/runner.ts +361 -361
- package/src/plugins/types.ts +368 -368
- package/src/report/index.ts +1 -1
- package/src/resource/__tests__/generator.test.ts +32 -32
- package/src/resource/ddl/__tests__/diff.test.ts +639 -639
- package/src/resource/ddl/__tests__/emit.test.ts +823 -823
- package/src/resource/ddl/__tests__/snapshot.test.ts +499 -499
- package/src/resource/ddl/emit.ts +559 -559
- package/src/resource/ddl/persistence-types.ts +218 -218
- package/src/resource/ddl/type-map.ts +223 -223
- package/src/resource/ddl/types.ts +232 -232
- package/src/resource/generator-repo.ts +630 -630
- package/src/resource/schema.ts +1 -1
- package/src/router/fs-patterns.test.ts +96 -96
- package/src/router/fs-routes.ts +6 -6
- package/src/routes/index.ts +74 -74
- package/src/routes/metadata-routes.ts +427 -427
- package/src/routes/types.ts +341 -341
- package/src/runtime/__tests__/error-boundary-redaction.test.ts +141 -141
- package/src/runtime/__tests__/hdr-client.test.ts +223 -223
- package/src/runtime/__tests__/http-errors.test.ts +117 -117
- package/src/runtime/__tests__/not-found.test.ts +152 -152
- package/src/runtime/adapter.ts +47 -47
- package/src/runtime/boundary.tsx +252 -252
- package/src/runtime/cache.ts +494 -494
- package/src/runtime/compose.ts +222 -222
- package/src/runtime/fast-refresh-runtime.ts +322 -322
- package/src/runtime/handler.ts +65 -65
- package/src/runtime/handlers.ts +300 -300
- package/src/runtime/http-errors.ts +113 -113
- package/src/runtime/image-handler.ts +1 -1
- package/src/runtime/lifecycle.ts +381 -381
- package/src/runtime/logger.test.ts +345 -345
- package/src/runtime/middleware.ts +264 -264
- package/src/runtime/not-found.ts +93 -93
- package/src/runtime/openapi-endpoint.ts +236 -236
- package/src/runtime/ppr.ts +74 -74
- package/src/runtime/registry.ts +171 -171
- package/src/runtime/router.ts +105 -105
- package/src/runtime/server.ts +67 -67
- package/src/runtime/shims.ts +48 -48
- package/src/runtime/ssr.ts +24 -31
- package/src/runtime/streaming-ssr.ts +32 -37
- package/src/runtime/trace.ts +144 -144
- package/src/scheduler/validate.ts +169 -169
- package/src/seo/index.ts +219 -219
- package/src/seo/integration/ssr.ts +306 -306
- package/src/seo/render/basic.ts +435 -435
- package/src/seo/render/index.ts +143 -143
- package/src/seo/render/jsonld.ts +539 -539
- package/src/seo/render/opengraph.ts +197 -197
- package/src/seo/render/robots.ts +116 -116
- package/src/seo/render/sitemap.ts +137 -137
- package/src/seo/render/twitter.ts +127 -127
- package/src/seo/resolve/opengraph.ts +143 -143
- package/src/seo/resolve/robots.ts +73 -73
- package/src/seo/resolve/title.ts +94 -94
- package/src/seo/resolve/twitter.ts +73 -73
- package/src/seo/resolve/url.ts +104 -104
- package/src/seo/routes/index.ts +290 -290
- package/src/seo/types.ts +588 -588
- package/src/slot/validator.ts +39 -39
- package/src/storage/s3/__tests__/s3.test.ts +479 -479
- package/src/storage/s3/index.ts +412 -412
- package/src/testing/__tests__/assertions.test.ts +632 -632
- package/src/testing/__tests__/reporter.test.ts +454 -454
- package/src/testing/assertions.ts +986 -986
- package/src/testing/db.ts +157 -157
- package/src/testing/mocks.ts +203 -203
- package/src/testing/session.ts +190 -190
- package/src/types/branded.ts +56 -56
- package/src/types/index.ts +1 -1
- package/src/utils/safe-io.ts +188 -188
- package/src/utils/string-safe.ts +298 -298
- package/src/watcher/watcher.ts +18 -18
|
@@ -1,301 +1,301 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Phase 7.2 R1 Agent C (H2) — BundleManifest schema validation.
|
|
3
|
-
*
|
|
4
|
-
* Background (see `docs/security/phase-7-1-audit.md` §M-02):
|
|
5
|
-
* Prior to Phase 7.2, callers read `.mandu/manifest.json` with a bare
|
|
6
|
-
* `JSON.parse(raw) as BundleManifest` cast. That was enough for happy-path
|
|
7
|
-
* dev but left an injection vector: an attacker with filesystem write to
|
|
8
|
-
* `.mandu/manifest.json` could rewrite `shared.fastRefresh.{glue,runtime}`
|
|
9
|
-
* to point at an external URL and the SSR preamble would fetch it (the
|
|
10
|
-
* browser does no cross-origin check for dynamic `import()`). With CSP off
|
|
11
|
-
* — Mandu's dev default — this is full RCE in the browser context.
|
|
12
|
-
*
|
|
13
|
-
* M-02 was scored Medium because the attack requires filesystem write,
|
|
14
|
-
* which is already a game-over pre-condition. The fix nonetheless closes
|
|
15
|
-
* the stealth window: a tampered manifest now throws at load time rather
|
|
16
|
-
* than silently serving evil URLs.
|
|
17
|
-
*
|
|
18
|
-
* This module exposes:
|
|
19
|
-
*
|
|
20
|
-
* - `BundleManifestSchema` — Zod schema for the manifest shape.
|
|
21
|
-
* - `validateBundleManifest(raw)` — strict validator. Throws
|
|
22
|
-
* `ManifestValidationError` on any
|
|
23
|
-
* schema mismatch, including the
|
|
24
|
-
* safe-URL constraints below.
|
|
25
|
-
* - `isSafeManduUrl(url)` — predicate used both inside the
|
|
26
|
-
* schema and at SSR preamble emit
|
|
27
|
-
* time for defense-in-depth.
|
|
28
|
-
*
|
|
29
|
-
* URL safety model (applies to `shared.runtime`, `shared.vendor`,
|
|
30
|
-
* `shared.router`, `shared.fastRefresh.glue`, `shared.fastRefresh.runtime`,
|
|
31
|
-
* `bundles[].js`, `bundles[].css`, `islands[].js`, `importMap.imports[*]`):
|
|
32
|
-
*
|
|
33
|
-
* ALLOW: absolute paths rooted at `/.mandu/client/` ending in `.js` or `.css`.
|
|
34
|
-
* The bundler itself only ever emits this shape.
|
|
35
|
-
* DENY: protocol URIs (`http://`, `https://`, `data:`, `javascript:`, ...),
|
|
36
|
-
* path traversal (`..`), backslashes, newlines, angle brackets,
|
|
37
|
-
* quotes, or anything longer than `MAX_URL_LEN` (4 KB).
|
|
38
|
-
*
|
|
39
|
-
* The ruleset is deliberately narrower than RFC 3986 — it is an allowlist,
|
|
40
|
-
* not a blocklist. Any URL format the bundler does not emit gets rejected.
|
|
41
|
-
* If a future bundler change introduces a new URL shape, update the schema
|
|
42
|
-
* first and let the tests fail-closed.
|
|
43
|
-
*
|
|
44
|
-
* References:
|
|
45
|
-
* docs/security/phase-7-1-audit.md §2 M-02
|
|
46
|
-
* docs/bun/phase-7-2-team-plan.md §3 Agent C H2
|
|
47
|
-
* packages/core/src/bundler/types.ts — `BundleManifest` runtime type
|
|
48
|
-
*/
|
|
49
|
-
|
|
50
|
-
import { z } from "zod";
|
|
51
|
-
|
|
52
|
-
import type { BundleManifest } from "./types";
|
|
53
|
-
|
|
54
|
-
// ============================================================================
|
|
55
|
-
// Constants — Exported for direct tests / downstream re-use.
|
|
56
|
-
// ============================================================================
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Maximum length any single manifest URL may take. 4 KB is ~8× the worst
|
|
60
|
-
* real-world path we have seen in tmpdir fixtures; anything larger is
|
|
61
|
-
* almost certainly a DoS probe or a malformed entry. Keep in sync with
|
|
62
|
-
* `appendBoundary` URL cap in `fast-refresh-plugin.ts` (both 2 KB).
|
|
63
|
-
*/
|
|
64
|
-
export const MAX_MANIFEST_URL_LEN = 4096;
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Shape constraint for Mandu-authored client assets. The bundler always
|
|
68
|
-
* writes to `/.mandu/client/{name}.js|.css`. We do not allow query strings
|
|
69
|
-
* in the manifest itself (callers add cache-bust `?t=<ts>` at emit time).
|
|
70
|
-
*/
|
|
71
|
-
export const SAFE_MANDU_URL_REGEX = /^\/\.mandu\/client\/[A-Za-z0-9_./-]+\.(js|css|mjs)$/;
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Characters we forbid anywhere inside a manifest URL — their presence
|
|
75
|
-
* signals either a serialization bug or an injection attempt (newlines
|
|
76
|
-
* break out of an inline `<script>` tag, `<`/`>` open new HTML contexts,
|
|
77
|
-
* quotes break out of attribute contexts).
|
|
78
|
-
*/
|
|
79
|
-
export const FORBIDDEN_URL_CHARS = /[\x00-\x1f\x7f"<>`\\\n\r\t]/;
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Substrings that indicate cross-origin / protocol escape attempts. The
|
|
83
|
-
* bundler never writes these; rejecting them makes `.mandu/manifest.json`
|
|
84
|
-
* tamper detection trivial.
|
|
85
|
-
*/
|
|
86
|
-
export const FORBIDDEN_URL_SUBSTRINGS = [
|
|
87
|
-
"://",
|
|
88
|
-
"//",
|
|
89
|
-
"..",
|
|
90
|
-
"javascript:",
|
|
91
|
-
"data:",
|
|
92
|
-
"vbscript:",
|
|
93
|
-
"file:",
|
|
94
|
-
] as const;
|
|
95
|
-
|
|
96
|
-
// ============================================================================
|
|
97
|
-
// Errors
|
|
98
|
-
// ============================================================================
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Thrown when `validateBundleManifest` rejects input. Carries the full Zod
|
|
102
|
-
* issue list so callers (build.ts, dev.ts) can surface actionable messages
|
|
103
|
-
* instead of the default `ZodError` noise.
|
|
104
|
-
*/
|
|
105
|
-
export class ManifestValidationError extends Error {
|
|
106
|
-
readonly issues: readonly { path: string; message: string }[];
|
|
107
|
-
|
|
108
|
-
constructor(
|
|
109
|
-
message: string,
|
|
110
|
-
issues: readonly { path: string; message: string }[] = [],
|
|
111
|
-
) {
|
|
112
|
-
super(message);
|
|
113
|
-
this.name = "ManifestValidationError";
|
|
114
|
-
this.issues = issues;
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// ============================================================================
|
|
119
|
-
// Primitives
|
|
120
|
-
// ============================================================================
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* Runtime predicate for Mandu-managed URLs. Surfaced separately from the
|
|
124
|
-
* schema so SSR callsites (`ssr.ts`, `streaming-ssr.ts`) can run the same
|
|
125
|
-
* check at preamble emit time — defense in depth against a manifest that
|
|
126
|
-
* slipped past validation (e.g. skipFrameworkBundles fallback path).
|
|
127
|
-
*/
|
|
128
|
-
export function isSafeManduUrl(url: unknown): url is string {
|
|
129
|
-
if (typeof url !== "string") return false;
|
|
130
|
-
if (url.length === 0 || url.length > MAX_MANIFEST_URL_LEN) return false;
|
|
131
|
-
if (FORBIDDEN_URL_CHARS.test(url)) return false;
|
|
132
|
-
for (const s of FORBIDDEN_URL_SUBSTRINGS) {
|
|
133
|
-
if (url.includes(s)) return false;
|
|
134
|
-
}
|
|
135
|
-
return SAFE_MANDU_URL_REGEX.test(url);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* Zod refinement that runs `isSafeManduUrl` with a helpful error message.
|
|
140
|
-
* We use a single shared factory so the same message text appears for
|
|
141
|
-
* every URL field — grepping support logs becomes one-pattern-fits-all.
|
|
142
|
-
*/
|
|
143
|
-
const safeManduUrl = (fieldLabel: string) =>
|
|
144
|
-
z
|
|
145
|
-
.string()
|
|
146
|
-
.max(MAX_MANIFEST_URL_LEN, `${fieldLabel}: URL exceeds ${MAX_MANIFEST_URL_LEN} bytes`)
|
|
147
|
-
.refine(isSafeManduUrl, {
|
|
148
|
-
message: `${fieldLabel}: URL must match /.mandu/client/*.{js,css,mjs} with no protocol or traversal`,
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
// ============================================================================
|
|
152
|
-
// Shape schemas
|
|
153
|
-
// ============================================================================
|
|
154
|
-
|
|
155
|
-
const PrioritySchema = z.enum(["immediate", "visible", "idle", "interaction"]);
|
|
156
|
-
|
|
157
|
-
const BundleEntrySchema = z.object({
|
|
158
|
-
js: safeManduUrl("bundles[].js"),
|
|
159
|
-
css: safeManduUrl("bundles[].css").optional(),
|
|
160
|
-
dependencies: z.array(z.string()).default([]),
|
|
161
|
-
priority: PrioritySchema,
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
const IslandEntrySchema = z.object({
|
|
165
|
-
js: safeManduUrl("islands[].js"),
|
|
166
|
-
route: z.string().min(1),
|
|
167
|
-
priority: PrioritySchema,
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
const FastRefreshSchema = z.object({
|
|
171
|
-
runtime: safeManduUrl("shared.fastRefresh.runtime"),
|
|
172
|
-
glue: safeManduUrl("shared.fastRefresh.glue"),
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
const SharedSchema = z.object({
|
|
176
|
-
runtime: safeManduUrl("shared.runtime"),
|
|
177
|
-
vendor: safeManduUrl("shared.vendor"),
|
|
178
|
-
router: safeManduUrl("shared.router").optional(),
|
|
179
|
-
fastRefresh: FastRefreshSchema.optional(),
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
const ImportMapEntrySchema = z
|
|
183
|
-
.string()
|
|
184
|
-
.refine(
|
|
185
|
-
(url) => {
|
|
186
|
-
// Import map values may be bare specifiers OR Mandu-client URLs.
|
|
187
|
-
// Bare specifiers are strings without path-like characters (no
|
|
188
|
-
// leading slash). Mandu URLs follow the same rules as `safeManduUrl`.
|
|
189
|
-
if (url.length === 0 || url.length > MAX_MANIFEST_URL_LEN) return false;
|
|
190
|
-
if (FORBIDDEN_URL_CHARS.test(url)) return false;
|
|
191
|
-
// Accept absolute Mandu paths
|
|
192
|
-
if (url.startsWith("/")) return isSafeManduUrl(url);
|
|
193
|
-
// Reject protocol URIs everywhere in the map
|
|
194
|
-
for (const s of FORBIDDEN_URL_SUBSTRINGS) {
|
|
195
|
-
if (url.includes(s)) return false;
|
|
196
|
-
}
|
|
197
|
-
return true;
|
|
198
|
-
},
|
|
199
|
-
{ message: "importMap value must be a bare specifier or /.mandu/client/* URL" },
|
|
200
|
-
);
|
|
201
|
-
|
|
202
|
-
const ImportMapSchema = z.object({
|
|
203
|
-
imports: z.record(z.string(), ImportMapEntrySchema),
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
/**
|
|
207
|
-
* Public BundleManifest schema. Matches `BundleManifest` in `./types.ts`
|
|
208
|
-
* but tightens URL / structural constraints that the TS type cannot
|
|
209
|
-
* express.
|
|
210
|
-
*
|
|
211
|
-
* Fields explicitly omitted from schema:
|
|
212
|
-
* - `buildTime` — validated as ISO string only.
|
|
213
|
-
*
|
|
214
|
-
* Optional fields (`css`, `router`, `fastRefresh`, `islands`, `importMap`)
|
|
215
|
-
* are allowed to be absent, matching the production manifest shape that
|
|
216
|
-
* `build.ts` emits when dev-only assets are not generated.
|
|
217
|
-
*/
|
|
218
|
-
export const BundleManifestSchema = z
|
|
219
|
-
.object({
|
|
220
|
-
version: z.number().int().min(1),
|
|
221
|
-
buildTime: z.string().min(1),
|
|
222
|
-
env: z.enum(["development", "production"]),
|
|
223
|
-
bundles: z.record(z.string(), BundleEntrySchema),
|
|
224
|
-
islands: z.record(z.string(), IslandEntrySchema).optional(),
|
|
225
|
-
shared: SharedSchema,
|
|
226
|
-
importMap: ImportMapSchema.optional(),
|
|
227
|
-
})
|
|
228
|
-
.strict();
|
|
229
|
-
|
|
230
|
-
export type ValidatedBundleManifest = z.infer<typeof BundleManifestSchema>;
|
|
231
|
-
|
|
232
|
-
// ============================================================================
|
|
233
|
-
// Validator
|
|
234
|
-
// ============================================================================
|
|
235
|
-
|
|
236
|
-
/**
|
|
237
|
-
* Parse+validate a raw manifest object. Throws `ManifestValidationError`
|
|
238
|
-
* on any shape mismatch. The return value carries the same runtime type
|
|
239
|
-
* as `BundleManifest` so callers can keep the existing TS type without
|
|
240
|
-
* a cast.
|
|
241
|
-
*
|
|
242
|
-
* Intentional design points:
|
|
243
|
-
*
|
|
244
|
-
* 1. `BundleManifestSchema.strict()` rejects unknown top-level keys.
|
|
245
|
-
* That catches schema drift (a field the bundler started emitting
|
|
246
|
-
* but the validator didn't learn about) at build time rather than
|
|
247
|
-
* in production. If a legitimate new field shows up, update this
|
|
248
|
-
* file first — fail closed.
|
|
249
|
-
* 2. The thrown error's `.issues` array exposes the full list of
|
|
250
|
-
* violations rather than just the first. Build / dev paths log the
|
|
251
|
-
* whole set so the developer fixes them in one pass.
|
|
252
|
-
* 3. Validators are pure — no filesystem / console IO. Callers own
|
|
253
|
-
* logging. This keeps the function easy to unit-test and makes it
|
|
254
|
-
* safe to call from the SSR preamble (no startup overhead).
|
|
255
|
-
*/
|
|
256
|
-
export function validateBundleManifest(raw: unknown): BundleManifest {
|
|
257
|
-
const result = BundleManifestSchema.safeParse(raw);
|
|
258
|
-
if (!result.success) {
|
|
259
|
-
const issues = result.error.issues.map((issue) => ({
|
|
260
|
-
path: issue.path.join("."),
|
|
261
|
-
message: issue.message,
|
|
262
|
-
}));
|
|
263
|
-
const firstFew = issues
|
|
264
|
-
.slice(0, 3)
|
|
265
|
-
.map((i) => `${i.path}: ${i.message}`)
|
|
266
|
-
.join("; ");
|
|
267
|
-
throw new ManifestValidationError(
|
|
268
|
-
`BundleManifest failed schema validation (${issues.length} issue${
|
|
269
|
-
issues.length === 1 ? "" : "s"
|
|
270
|
-
}): ${firstFew}`,
|
|
271
|
-
issues,
|
|
272
|
-
);
|
|
273
|
-
}
|
|
274
|
-
// Zod's inferred type matches BundleManifest by structural compat;
|
|
275
|
-
// the cast re-attaches the nominal TS name without runtime cost.
|
|
276
|
-
return result.data as unknown as BundleManifest;
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
/**
|
|
280
|
-
* Non-throwing variant. Returns either a validated manifest or a list of
|
|
281
|
-
* issues — convenient for callsites that want to log and continue with a
|
|
282
|
-
* fallback (e.g. `skipFrameworkBundles` path in build.ts that falls back
|
|
283
|
-
* to a full rebuild on any validation failure).
|
|
284
|
-
*/
|
|
285
|
-
export function safeValidateBundleManifest(
|
|
286
|
-
raw: unknown,
|
|
287
|
-
):
|
|
288
|
-
| { ok: true; manifest: BundleManifest }
|
|
289
|
-
| { ok: false; issues: { path: string; message: string }[] } {
|
|
290
|
-
const result = BundleManifestSchema.safeParse(raw);
|
|
291
|
-
if (!result.success) {
|
|
292
|
-
return {
|
|
293
|
-
ok: false,
|
|
294
|
-
issues: result.error.issues.map((issue) => ({
|
|
295
|
-
path: issue.path.join("."),
|
|
296
|
-
message: issue.message,
|
|
297
|
-
})),
|
|
298
|
-
};
|
|
299
|
-
}
|
|
300
|
-
return { ok: true, manifest: result.data as unknown as BundleManifest };
|
|
301
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Phase 7.2 R1 Agent C (H2) — BundleManifest schema validation.
|
|
3
|
+
*
|
|
4
|
+
* Background (see `docs/security/phase-7-1-audit.md` §M-02):
|
|
5
|
+
* Prior to Phase 7.2, callers read `.mandu/manifest.json` with a bare
|
|
6
|
+
* `JSON.parse(raw) as BundleManifest` cast. That was enough for happy-path
|
|
7
|
+
* dev but left an injection vector: an attacker with filesystem write to
|
|
8
|
+
* `.mandu/manifest.json` could rewrite `shared.fastRefresh.{glue,runtime}`
|
|
9
|
+
* to point at an external URL and the SSR preamble would fetch it (the
|
|
10
|
+
* browser does no cross-origin check for dynamic `import()`). With CSP off
|
|
11
|
+
* — Mandu's dev default — this is full RCE in the browser context.
|
|
12
|
+
*
|
|
13
|
+
* M-02 was scored Medium because the attack requires filesystem write,
|
|
14
|
+
* which is already a game-over pre-condition. The fix nonetheless closes
|
|
15
|
+
* the stealth window: a tampered manifest now throws at load time rather
|
|
16
|
+
* than silently serving evil URLs.
|
|
17
|
+
*
|
|
18
|
+
* This module exposes:
|
|
19
|
+
*
|
|
20
|
+
* - `BundleManifestSchema` — Zod schema for the manifest shape.
|
|
21
|
+
* - `validateBundleManifest(raw)` — strict validator. Throws
|
|
22
|
+
* `ManifestValidationError` on any
|
|
23
|
+
* schema mismatch, including the
|
|
24
|
+
* safe-URL constraints below.
|
|
25
|
+
* - `isSafeManduUrl(url)` — predicate used both inside the
|
|
26
|
+
* schema and at SSR preamble emit
|
|
27
|
+
* time for defense-in-depth.
|
|
28
|
+
*
|
|
29
|
+
* URL safety model (applies to `shared.runtime`, `shared.vendor`,
|
|
30
|
+
* `shared.router`, `shared.fastRefresh.glue`, `shared.fastRefresh.runtime`,
|
|
31
|
+
* `bundles[].js`, `bundles[].css`, `islands[].js`, `importMap.imports[*]`):
|
|
32
|
+
*
|
|
33
|
+
* ALLOW: absolute paths rooted at `/.mandu/client/` ending in `.js` or `.css`.
|
|
34
|
+
* The bundler itself only ever emits this shape.
|
|
35
|
+
* DENY: protocol URIs (`http://`, `https://`, `data:`, `javascript:`, ...),
|
|
36
|
+
* path traversal (`..`), backslashes, newlines, angle brackets,
|
|
37
|
+
* quotes, or anything longer than `MAX_URL_LEN` (4 KB).
|
|
38
|
+
*
|
|
39
|
+
* The ruleset is deliberately narrower than RFC 3986 — it is an allowlist,
|
|
40
|
+
* not a blocklist. Any URL format the bundler does not emit gets rejected.
|
|
41
|
+
* If a future bundler change introduces a new URL shape, update the schema
|
|
42
|
+
* first and let the tests fail-closed.
|
|
43
|
+
*
|
|
44
|
+
* References:
|
|
45
|
+
* docs/security/phase-7-1-audit.md §2 M-02
|
|
46
|
+
* docs/bun/phase-7-2-team-plan.md §3 Agent C H2
|
|
47
|
+
* packages/core/src/bundler/types.ts — `BundleManifest` runtime type
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
import { z } from "zod";
|
|
51
|
+
|
|
52
|
+
import type { BundleManifest } from "./types";
|
|
53
|
+
|
|
54
|
+
// ============================================================================
|
|
55
|
+
// Constants — Exported for direct tests / downstream re-use.
|
|
56
|
+
// ============================================================================
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Maximum length any single manifest URL may take. 4 KB is ~8× the worst
|
|
60
|
+
* real-world path we have seen in tmpdir fixtures; anything larger is
|
|
61
|
+
* almost certainly a DoS probe or a malformed entry. Keep in sync with
|
|
62
|
+
* `appendBoundary` URL cap in `fast-refresh-plugin.ts` (both 2 KB).
|
|
63
|
+
*/
|
|
64
|
+
export const MAX_MANIFEST_URL_LEN = 4096;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Shape constraint for Mandu-authored client assets. The bundler always
|
|
68
|
+
* writes to `/.mandu/client/{name}.js|.css`. We do not allow query strings
|
|
69
|
+
* in the manifest itself (callers add cache-bust `?t=<ts>` at emit time).
|
|
70
|
+
*/
|
|
71
|
+
export const SAFE_MANDU_URL_REGEX = /^\/\.mandu\/client\/[A-Za-z0-9_./-]+\.(js|css|mjs)$/;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Characters we forbid anywhere inside a manifest URL — their presence
|
|
75
|
+
* signals either a serialization bug or an injection attempt (newlines
|
|
76
|
+
* break out of an inline `<script>` tag, `<`/`>` open new HTML contexts,
|
|
77
|
+
* quotes break out of attribute contexts).
|
|
78
|
+
*/
|
|
79
|
+
export const FORBIDDEN_URL_CHARS = /[\x00-\x1f\x7f"<>`\\\n\r\t]/;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Substrings that indicate cross-origin / protocol escape attempts. The
|
|
83
|
+
* bundler never writes these; rejecting them makes `.mandu/manifest.json`
|
|
84
|
+
* tamper detection trivial.
|
|
85
|
+
*/
|
|
86
|
+
export const FORBIDDEN_URL_SUBSTRINGS = [
|
|
87
|
+
"://",
|
|
88
|
+
"//",
|
|
89
|
+
"..",
|
|
90
|
+
"javascript:",
|
|
91
|
+
"data:",
|
|
92
|
+
"vbscript:",
|
|
93
|
+
"file:",
|
|
94
|
+
] as const;
|
|
95
|
+
|
|
96
|
+
// ============================================================================
|
|
97
|
+
// Errors
|
|
98
|
+
// ============================================================================
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Thrown when `validateBundleManifest` rejects input. Carries the full Zod
|
|
102
|
+
* issue list so callers (build.ts, dev.ts) can surface actionable messages
|
|
103
|
+
* instead of the default `ZodError` noise.
|
|
104
|
+
*/
|
|
105
|
+
export class ManifestValidationError extends Error {
|
|
106
|
+
readonly issues: readonly { path: string; message: string }[];
|
|
107
|
+
|
|
108
|
+
constructor(
|
|
109
|
+
message: string,
|
|
110
|
+
issues: readonly { path: string; message: string }[] = [],
|
|
111
|
+
) {
|
|
112
|
+
super(message);
|
|
113
|
+
this.name = "ManifestValidationError";
|
|
114
|
+
this.issues = issues;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ============================================================================
|
|
119
|
+
// Primitives
|
|
120
|
+
// ============================================================================
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Runtime predicate for Mandu-managed URLs. Surfaced separately from the
|
|
124
|
+
* schema so SSR callsites (`ssr.ts`, `streaming-ssr.ts`) can run the same
|
|
125
|
+
* check at preamble emit time — defense in depth against a manifest that
|
|
126
|
+
* slipped past validation (e.g. skipFrameworkBundles fallback path).
|
|
127
|
+
*/
|
|
128
|
+
export function isSafeManduUrl(url: unknown): url is string {
|
|
129
|
+
if (typeof url !== "string") return false;
|
|
130
|
+
if (url.length === 0 || url.length > MAX_MANIFEST_URL_LEN) return false;
|
|
131
|
+
if (FORBIDDEN_URL_CHARS.test(url)) return false;
|
|
132
|
+
for (const s of FORBIDDEN_URL_SUBSTRINGS) {
|
|
133
|
+
if (url.includes(s)) return false;
|
|
134
|
+
}
|
|
135
|
+
return SAFE_MANDU_URL_REGEX.test(url);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Zod refinement that runs `isSafeManduUrl` with a helpful error message.
|
|
140
|
+
* We use a single shared factory so the same message text appears for
|
|
141
|
+
* every URL field — grepping support logs becomes one-pattern-fits-all.
|
|
142
|
+
*/
|
|
143
|
+
const safeManduUrl = (fieldLabel: string) =>
|
|
144
|
+
z
|
|
145
|
+
.string()
|
|
146
|
+
.max(MAX_MANIFEST_URL_LEN, `${fieldLabel}: URL exceeds ${MAX_MANIFEST_URL_LEN} bytes`)
|
|
147
|
+
.refine(isSafeManduUrl, {
|
|
148
|
+
message: `${fieldLabel}: URL must match /.mandu/client/*.{js,css,mjs} with no protocol or traversal`,
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// ============================================================================
|
|
152
|
+
// Shape schemas
|
|
153
|
+
// ============================================================================
|
|
154
|
+
|
|
155
|
+
const PrioritySchema = z.enum(["immediate", "visible", "idle", "interaction"]);
|
|
156
|
+
|
|
157
|
+
const BundleEntrySchema = z.object({
|
|
158
|
+
js: safeManduUrl("bundles[].js"),
|
|
159
|
+
css: safeManduUrl("bundles[].css").optional(),
|
|
160
|
+
dependencies: z.array(z.string()).default([]),
|
|
161
|
+
priority: PrioritySchema,
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
const IslandEntrySchema = z.object({
|
|
165
|
+
js: safeManduUrl("islands[].js"),
|
|
166
|
+
route: z.string().min(1),
|
|
167
|
+
priority: PrioritySchema,
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
const FastRefreshSchema = z.object({
|
|
171
|
+
runtime: safeManduUrl("shared.fastRefresh.runtime"),
|
|
172
|
+
glue: safeManduUrl("shared.fastRefresh.glue"),
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
const SharedSchema = z.object({
|
|
176
|
+
runtime: safeManduUrl("shared.runtime"),
|
|
177
|
+
vendor: safeManduUrl("shared.vendor"),
|
|
178
|
+
router: safeManduUrl("shared.router").optional(),
|
|
179
|
+
fastRefresh: FastRefreshSchema.optional(),
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
const ImportMapEntrySchema = z
|
|
183
|
+
.string()
|
|
184
|
+
.refine(
|
|
185
|
+
(url) => {
|
|
186
|
+
// Import map values may be bare specifiers OR Mandu-client URLs.
|
|
187
|
+
// Bare specifiers are strings without path-like characters (no
|
|
188
|
+
// leading slash). Mandu URLs follow the same rules as `safeManduUrl`.
|
|
189
|
+
if (url.length === 0 || url.length > MAX_MANIFEST_URL_LEN) return false;
|
|
190
|
+
if (FORBIDDEN_URL_CHARS.test(url)) return false;
|
|
191
|
+
// Accept absolute Mandu paths
|
|
192
|
+
if (url.startsWith("/")) return isSafeManduUrl(url);
|
|
193
|
+
// Reject protocol URIs everywhere in the map
|
|
194
|
+
for (const s of FORBIDDEN_URL_SUBSTRINGS) {
|
|
195
|
+
if (url.includes(s)) return false;
|
|
196
|
+
}
|
|
197
|
+
return true;
|
|
198
|
+
},
|
|
199
|
+
{ message: "importMap value must be a bare specifier or /.mandu/client/* URL" },
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
const ImportMapSchema = z.object({
|
|
203
|
+
imports: z.record(z.string(), ImportMapEntrySchema),
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Public BundleManifest schema. Matches `BundleManifest` in `./types.ts`
|
|
208
|
+
* but tightens URL / structural constraints that the TS type cannot
|
|
209
|
+
* express.
|
|
210
|
+
*
|
|
211
|
+
* Fields explicitly omitted from schema:
|
|
212
|
+
* - `buildTime` — validated as ISO string only.
|
|
213
|
+
*
|
|
214
|
+
* Optional fields (`css`, `router`, `fastRefresh`, `islands`, `importMap`)
|
|
215
|
+
* are allowed to be absent, matching the production manifest shape that
|
|
216
|
+
* `build.ts` emits when dev-only assets are not generated.
|
|
217
|
+
*/
|
|
218
|
+
export const BundleManifestSchema = z
|
|
219
|
+
.object({
|
|
220
|
+
version: z.number().int().min(1),
|
|
221
|
+
buildTime: z.string().min(1),
|
|
222
|
+
env: z.enum(["development", "production"]),
|
|
223
|
+
bundles: z.record(z.string(), BundleEntrySchema),
|
|
224
|
+
islands: z.record(z.string(), IslandEntrySchema).optional(),
|
|
225
|
+
shared: SharedSchema,
|
|
226
|
+
importMap: ImportMapSchema.optional(),
|
|
227
|
+
})
|
|
228
|
+
.strict();
|
|
229
|
+
|
|
230
|
+
export type ValidatedBundleManifest = z.infer<typeof BundleManifestSchema>;
|
|
231
|
+
|
|
232
|
+
// ============================================================================
|
|
233
|
+
// Validator
|
|
234
|
+
// ============================================================================
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Parse+validate a raw manifest object. Throws `ManifestValidationError`
|
|
238
|
+
* on any shape mismatch. The return value carries the same runtime type
|
|
239
|
+
* as `BundleManifest` so callers can keep the existing TS type without
|
|
240
|
+
* a cast.
|
|
241
|
+
*
|
|
242
|
+
* Intentional design points:
|
|
243
|
+
*
|
|
244
|
+
* 1. `BundleManifestSchema.strict()` rejects unknown top-level keys.
|
|
245
|
+
* That catches schema drift (a field the bundler started emitting
|
|
246
|
+
* but the validator didn't learn about) at build time rather than
|
|
247
|
+
* in production. If a legitimate new field shows up, update this
|
|
248
|
+
* file first — fail closed.
|
|
249
|
+
* 2. The thrown error's `.issues` array exposes the full list of
|
|
250
|
+
* violations rather than just the first. Build / dev paths log the
|
|
251
|
+
* whole set so the developer fixes them in one pass.
|
|
252
|
+
* 3. Validators are pure — no filesystem / console IO. Callers own
|
|
253
|
+
* logging. This keeps the function easy to unit-test and makes it
|
|
254
|
+
* safe to call from the SSR preamble (no startup overhead).
|
|
255
|
+
*/
|
|
256
|
+
export function validateBundleManifest(raw: unknown): BundleManifest {
|
|
257
|
+
const result = BundleManifestSchema.safeParse(raw);
|
|
258
|
+
if (!result.success) {
|
|
259
|
+
const issues = result.error.issues.map((issue) => ({
|
|
260
|
+
path: issue.path.join("."),
|
|
261
|
+
message: issue.message,
|
|
262
|
+
}));
|
|
263
|
+
const firstFew = issues
|
|
264
|
+
.slice(0, 3)
|
|
265
|
+
.map((i) => `${i.path}: ${i.message}`)
|
|
266
|
+
.join("; ");
|
|
267
|
+
throw new ManifestValidationError(
|
|
268
|
+
`BundleManifest failed schema validation (${issues.length} issue${
|
|
269
|
+
issues.length === 1 ? "" : "s"
|
|
270
|
+
}): ${firstFew}`,
|
|
271
|
+
issues,
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
// Zod's inferred type matches BundleManifest by structural compat;
|
|
275
|
+
// the cast re-attaches the nominal TS name without runtime cost.
|
|
276
|
+
return result.data as unknown as BundleManifest;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Non-throwing variant. Returns either a validated manifest or a list of
|
|
281
|
+
* issues — convenient for callsites that want to log and continue with a
|
|
282
|
+
* fallback (e.g. `skipFrameworkBundles` path in build.ts that falls back
|
|
283
|
+
* to a full rebuild on any validation failure).
|
|
284
|
+
*/
|
|
285
|
+
export function safeValidateBundleManifest(
|
|
286
|
+
raw: unknown,
|
|
287
|
+
):
|
|
288
|
+
| { ok: true; manifest: BundleManifest }
|
|
289
|
+
| { ok: false; issues: { path: string; message: string }[] } {
|
|
290
|
+
const result = BundleManifestSchema.safeParse(raw);
|
|
291
|
+
if (!result.success) {
|
|
292
|
+
return {
|
|
293
|
+
ok: false,
|
|
294
|
+
issues: result.error.issues.map((issue) => ({
|
|
295
|
+
path: issue.path.join("."),
|
|
296
|
+
message: issue.message,
|
|
297
|
+
})),
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
return { ok: true, manifest: result.data as unknown as BundleManifest };
|
|
301
|
+
}
|