@mandujs/core 0.53.3 → 0.54.1
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 +1 -1
- 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 -451
- package/src/diagnose/checks.ts +832 -832
- package/src/diagnose/index.ts +17 -17
- package/src/diagnose/run.ts +93 -93
- 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/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,193 +1,193 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Content-Security-Policy builder
|
|
3
|
-
*
|
|
4
|
-
* Standalone because CSP has many directives, user overrides need to merge
|
|
5
|
-
* deterministically with defaults, and per-request nonce interpolation needs
|
|
6
|
-
* a single authoritative code path so `script-src` and `style-src` are
|
|
7
|
-
* guaranteed to share the same nonce (OWASP recommendation — one nonce per
|
|
8
|
-
* request, reused across directives).
|
|
9
|
-
*
|
|
10
|
-
* Consumers:
|
|
11
|
-
* - `secure()` middleware (via `buildCsp`)
|
|
12
|
-
* - Callers who want ONLY CSP (public re-export from the secure barrel)
|
|
13
|
-
*
|
|
14
|
-
* @see https://www.w3.org/TR/CSP3/
|
|
15
|
-
* @see https://owasp.org/www-project-secure-headers/
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
// ========== Types ==========
|
|
19
|
-
|
|
20
|
-
export interface CspOptions {
|
|
21
|
-
/** Directive map. Values are joined with space. Merged over defaults. */
|
|
22
|
-
directives?: Record<string, string[]>;
|
|
23
|
-
/** When true, emits `Content-Security-Policy-Report-Only` instead. */
|
|
24
|
-
reportOnly?: boolean;
|
|
25
|
-
/**
|
|
26
|
-
* Nonce source:
|
|
27
|
-
* - `true` → generate a fresh 128-bit random nonce per call
|
|
28
|
-
* - string → use the provided literal nonce value (must be base64url-safe)
|
|
29
|
-
* - `false` / undefined → no nonce; any `{NONCE}` placeholder is stripped
|
|
30
|
-
*/
|
|
31
|
-
nonce?: boolean | string | false;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export interface BuiltCsp {
|
|
35
|
-
/** Final header value. */
|
|
36
|
-
header: string;
|
|
37
|
-
/** Which header name to set. */
|
|
38
|
-
name: "Content-Security-Policy" | "Content-Security-Policy-Report-Only";
|
|
39
|
-
/** Populated only when `nonce` resolved to a concrete value. */
|
|
40
|
-
nonce?: string;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// ========== Defaults ==========
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Production-safe SSR + islands defaults.
|
|
47
|
-
*
|
|
48
|
-
* Key choices:
|
|
49
|
-
* - `'strict-dynamic'` on `script-src` means: once a trusted nonce-authorized
|
|
50
|
-
* script loads, further scripts it creates inherit trust. This allows
|
|
51
|
-
* hydration bundles to lazy-load chunks without reloading their nonce.
|
|
52
|
-
* Modern browsers (CSP 3+) honor this; older browsers fall back to the
|
|
53
|
-
* nonce allowlist, which still works for the SSR-emitted scripts.
|
|
54
|
-
* - `frame-ancestors 'none'` supersedes `X-Frame-Options: DENY` for modern
|
|
55
|
-
* browsers. We still emit XFO for legacy clients.
|
|
56
|
-
* - `upgrade-insecure-requests` silently rewrites `http:` subresources to
|
|
57
|
-
* `https:`. Safe default for modern apps; cheap insurance against mixed
|
|
58
|
-
* content from third-party embeds.
|
|
59
|
-
* - NO `'unsafe-inline'` / `'unsafe-eval'`. If your app needs them, opt in
|
|
60
|
-
* explicitly via `directives` — don't let them sneak in as defaults.
|
|
61
|
-
*/
|
|
62
|
-
export const DEFAULT_CSP_DIRECTIVES: Readonly<Record<string, readonly string[]>> = Object.freeze({
|
|
63
|
-
"default-src": ["'self'"],
|
|
64
|
-
"script-src": ["'self'", "'nonce-{NONCE}'", "'strict-dynamic'"],
|
|
65
|
-
"style-src": ["'self'", "'nonce-{NONCE}'", "https://fonts.googleapis.com"],
|
|
66
|
-
"font-src": ["'self'", "https://fonts.gstatic.com", "data:"],
|
|
67
|
-
"img-src": ["'self'", "data:", "https:"],
|
|
68
|
-
"connect-src": ["'self'"],
|
|
69
|
-
"frame-ancestors": ["'none'"],
|
|
70
|
-
"base-uri": ["'self'"],
|
|
71
|
-
"form-action": ["'self'"],
|
|
72
|
-
"object-src": ["'none'"],
|
|
73
|
-
"upgrade-insecure-requests": [],
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
/** Placeholder literal that `buildCsp` rewrites to the real nonce value. */
|
|
77
|
-
export const NONCE_PLACEHOLDER = "{NONCE}";
|
|
78
|
-
|
|
79
|
-
// ========== Public API ==========
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Build a single `Content-Security-Policy` header from defaults + overrides.
|
|
83
|
-
*
|
|
84
|
-
* Merge semantics:
|
|
85
|
-
* - User-supplied directives REPLACE the default values for that directive
|
|
86
|
-
* (caller owns the full value list). We do not concatenate, because CSP
|
|
87
|
-
* parsing is order-sensitive and partial merges surprise callers.
|
|
88
|
-
* - Directive names are normalized to lowercase-kebab. Caller may supply
|
|
89
|
-
* either `"script-src"` or `"scriptSrc"`; both map to `script-src`.
|
|
90
|
-
* - Empty-array directives emit as bare directive names (e.g.
|
|
91
|
-
* `upgrade-insecure-requests`) — required by the CSP grammar for flag
|
|
92
|
-
* directives.
|
|
93
|
-
*
|
|
94
|
-
* Nonce handling:
|
|
95
|
-
* - If `nonce === true`, we generate a per-call nonce via
|
|
96
|
-
* `crypto.getRandomValues` (16 bytes → 128 bits of entropy, base64url).
|
|
97
|
-
* - If `nonce === string`, we trust the caller's value verbatim. Useful for
|
|
98
|
-
* integrating with a pre-existing per-request nonce generator.
|
|
99
|
-
* - The SAME nonce is substituted into every `{NONCE}` occurrence across
|
|
100
|
-
* ALL directives (script-src, style-src, ...). Per OWASP guidance.
|
|
101
|
-
* - If no nonce, `{NONCE}` placeholders are stripped along with their
|
|
102
|
-
* surrounding `'nonce-...'` wrapper to avoid emitting broken tokens.
|
|
103
|
-
*/
|
|
104
|
-
export function buildCsp(options: CspOptions = {}): BuiltCsp {
|
|
105
|
-
const nonce = resolveNonce(options.nonce);
|
|
106
|
-
|
|
107
|
-
// Normalize + merge.
|
|
108
|
-
const merged: Record<string, string[]> = {};
|
|
109
|
-
for (const [k, v] of Object.entries(DEFAULT_CSP_DIRECTIVES)) {
|
|
110
|
-
merged[normalizeDirectiveName(k)] = [...v];
|
|
111
|
-
}
|
|
112
|
-
if (options.directives) {
|
|
113
|
-
for (const [k, v] of Object.entries(options.directives)) {
|
|
114
|
-
if (!Array.isArray(v)) continue;
|
|
115
|
-
merged[normalizeDirectiveName(k)] = [...v];
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
const parts: string[] = [];
|
|
120
|
-
for (const [name, values] of Object.entries(merged)) {
|
|
121
|
-
const processed = values
|
|
122
|
-
.map((v) => interpolateNonce(v, nonce))
|
|
123
|
-
// Strip broken nonce tokens when no nonce was produced.
|
|
124
|
-
.filter((v): v is string => v !== null);
|
|
125
|
-
|
|
126
|
-
if (processed.length === 0) {
|
|
127
|
-
// Flag-style directive (no value list) — emit bare name.
|
|
128
|
-
parts.push(name);
|
|
129
|
-
} else {
|
|
130
|
-
parts.push(`${name} ${processed.join(" ")}`);
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
return {
|
|
135
|
-
header: parts.join("; "),
|
|
136
|
-
name: options.reportOnly
|
|
137
|
-
? "Content-Security-Policy-Report-Only"
|
|
138
|
-
: "Content-Security-Policy",
|
|
139
|
-
nonce,
|
|
140
|
-
};
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// ========== Internal ==========
|
|
144
|
-
|
|
145
|
-
/**
|
|
146
|
-
* Resolve the `nonce` option to a concrete string (or undefined for "off").
|
|
147
|
-
* Generation uses `crypto.getRandomValues` rather than `crypto.randomUUID`
|
|
148
|
-
* to match OWASP's recommendation of ≥128 bits of entropy with raw random
|
|
149
|
-
* bytes (UUIDs have lower effective entropy due to version/variant bits).
|
|
150
|
-
*/
|
|
151
|
-
function resolveNonce(opt: CspOptions["nonce"]): string | undefined {
|
|
152
|
-
if (opt === true) {
|
|
153
|
-
const bytes = new Uint8Array(16);
|
|
154
|
-
crypto.getRandomValues(bytes);
|
|
155
|
-
return base64UrlEncode(bytes);
|
|
156
|
-
}
|
|
157
|
-
if (typeof opt === "string" && opt.length > 0) {
|
|
158
|
-
return opt;
|
|
159
|
-
}
|
|
160
|
-
return undefined;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
/**
|
|
164
|
-
* Replace `{NONCE}` in a single directive value.
|
|
165
|
-
*
|
|
166
|
-
* Returns `null` to signal "this value depended on a nonce that was never
|
|
167
|
-
* produced — drop it entirely" so callers don't emit `'nonce-'` (broken
|
|
168
|
-
* token). Returns the original value when no placeholder is present.
|
|
169
|
-
*/
|
|
170
|
-
function interpolateNonce(value: string, nonce: string | undefined): string | null {
|
|
171
|
-
if (!value.includes(NONCE_PLACEHOLDER)) return value;
|
|
172
|
-
if (!nonce) return null;
|
|
173
|
-
return value.split(NONCE_PLACEHOLDER).join(nonce);
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/**
|
|
177
|
-
* Accepts `"scriptSrc"` / `"script-src"` / `"Script-Src"` → `"script-src"`.
|
|
178
|
-
* camelCase → kebab-case via the standard regex; already-kebab inputs are
|
|
179
|
-
* lowercased without further transformation.
|
|
180
|
-
*/
|
|
181
|
-
function normalizeDirectiveName(name: string): string {
|
|
182
|
-
return name
|
|
183
|
-
.replace(/([a-z0-9])([A-Z])/g, "$1-$2")
|
|
184
|
-
.toLowerCase();
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
function base64UrlEncode(bytes: Uint8Array): string {
|
|
188
|
-
let binary = "";
|
|
189
|
-
for (let i = 0; i < bytes.length; i++) {
|
|
190
|
-
binary += String.fromCharCode(bytes[i]);
|
|
191
|
-
}
|
|
192
|
-
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
193
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Content-Security-Policy builder
|
|
3
|
+
*
|
|
4
|
+
* Standalone because CSP has many directives, user overrides need to merge
|
|
5
|
+
* deterministically with defaults, and per-request nonce interpolation needs
|
|
6
|
+
* a single authoritative code path so `script-src` and `style-src` are
|
|
7
|
+
* guaranteed to share the same nonce (OWASP recommendation — one nonce per
|
|
8
|
+
* request, reused across directives).
|
|
9
|
+
*
|
|
10
|
+
* Consumers:
|
|
11
|
+
* - `secure()` middleware (via `buildCsp`)
|
|
12
|
+
* - Callers who want ONLY CSP (public re-export from the secure barrel)
|
|
13
|
+
*
|
|
14
|
+
* @see https://www.w3.org/TR/CSP3/
|
|
15
|
+
* @see https://owasp.org/www-project-secure-headers/
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
// ========== Types ==========
|
|
19
|
+
|
|
20
|
+
export interface CspOptions {
|
|
21
|
+
/** Directive map. Values are joined with space. Merged over defaults. */
|
|
22
|
+
directives?: Record<string, string[]>;
|
|
23
|
+
/** When true, emits `Content-Security-Policy-Report-Only` instead. */
|
|
24
|
+
reportOnly?: boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Nonce source:
|
|
27
|
+
* - `true` → generate a fresh 128-bit random nonce per call
|
|
28
|
+
* - string → use the provided literal nonce value (must be base64url-safe)
|
|
29
|
+
* - `false` / undefined → no nonce; any `{NONCE}` placeholder is stripped
|
|
30
|
+
*/
|
|
31
|
+
nonce?: boolean | string | false;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface BuiltCsp {
|
|
35
|
+
/** Final header value. */
|
|
36
|
+
header: string;
|
|
37
|
+
/** Which header name to set. */
|
|
38
|
+
name: "Content-Security-Policy" | "Content-Security-Policy-Report-Only";
|
|
39
|
+
/** Populated only when `nonce` resolved to a concrete value. */
|
|
40
|
+
nonce?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ========== Defaults ==========
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Production-safe SSR + islands defaults.
|
|
47
|
+
*
|
|
48
|
+
* Key choices:
|
|
49
|
+
* - `'strict-dynamic'` on `script-src` means: once a trusted nonce-authorized
|
|
50
|
+
* script loads, further scripts it creates inherit trust. This allows
|
|
51
|
+
* hydration bundles to lazy-load chunks without reloading their nonce.
|
|
52
|
+
* Modern browsers (CSP 3+) honor this; older browsers fall back to the
|
|
53
|
+
* nonce allowlist, which still works for the SSR-emitted scripts.
|
|
54
|
+
* - `frame-ancestors 'none'` supersedes `X-Frame-Options: DENY` for modern
|
|
55
|
+
* browsers. We still emit XFO for legacy clients.
|
|
56
|
+
* - `upgrade-insecure-requests` silently rewrites `http:` subresources to
|
|
57
|
+
* `https:`. Safe default for modern apps; cheap insurance against mixed
|
|
58
|
+
* content from third-party embeds.
|
|
59
|
+
* - NO `'unsafe-inline'` / `'unsafe-eval'`. If your app needs them, opt in
|
|
60
|
+
* explicitly via `directives` — don't let them sneak in as defaults.
|
|
61
|
+
*/
|
|
62
|
+
export const DEFAULT_CSP_DIRECTIVES: Readonly<Record<string, readonly string[]>> = Object.freeze({
|
|
63
|
+
"default-src": ["'self'"],
|
|
64
|
+
"script-src": ["'self'", "'nonce-{NONCE}'", "'strict-dynamic'"],
|
|
65
|
+
"style-src": ["'self'", "'nonce-{NONCE}'", "https://fonts.googleapis.com"],
|
|
66
|
+
"font-src": ["'self'", "https://fonts.gstatic.com", "data:"],
|
|
67
|
+
"img-src": ["'self'", "data:", "https:"],
|
|
68
|
+
"connect-src": ["'self'"],
|
|
69
|
+
"frame-ancestors": ["'none'"],
|
|
70
|
+
"base-uri": ["'self'"],
|
|
71
|
+
"form-action": ["'self'"],
|
|
72
|
+
"object-src": ["'none'"],
|
|
73
|
+
"upgrade-insecure-requests": [],
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
/** Placeholder literal that `buildCsp` rewrites to the real nonce value. */
|
|
77
|
+
export const NONCE_PLACEHOLDER = "{NONCE}";
|
|
78
|
+
|
|
79
|
+
// ========== Public API ==========
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Build a single `Content-Security-Policy` header from defaults + overrides.
|
|
83
|
+
*
|
|
84
|
+
* Merge semantics:
|
|
85
|
+
* - User-supplied directives REPLACE the default values for that directive
|
|
86
|
+
* (caller owns the full value list). We do not concatenate, because CSP
|
|
87
|
+
* parsing is order-sensitive and partial merges surprise callers.
|
|
88
|
+
* - Directive names are normalized to lowercase-kebab. Caller may supply
|
|
89
|
+
* either `"script-src"` or `"scriptSrc"`; both map to `script-src`.
|
|
90
|
+
* - Empty-array directives emit as bare directive names (e.g.
|
|
91
|
+
* `upgrade-insecure-requests`) — required by the CSP grammar for flag
|
|
92
|
+
* directives.
|
|
93
|
+
*
|
|
94
|
+
* Nonce handling:
|
|
95
|
+
* - If `nonce === true`, we generate a per-call nonce via
|
|
96
|
+
* `crypto.getRandomValues` (16 bytes → 128 bits of entropy, base64url).
|
|
97
|
+
* - If `nonce === string`, we trust the caller's value verbatim. Useful for
|
|
98
|
+
* integrating with a pre-existing per-request nonce generator.
|
|
99
|
+
* - The SAME nonce is substituted into every `{NONCE}` occurrence across
|
|
100
|
+
* ALL directives (script-src, style-src, ...). Per OWASP guidance.
|
|
101
|
+
* - If no nonce, `{NONCE}` placeholders are stripped along with their
|
|
102
|
+
* surrounding `'nonce-...'` wrapper to avoid emitting broken tokens.
|
|
103
|
+
*/
|
|
104
|
+
export function buildCsp(options: CspOptions = {}): BuiltCsp {
|
|
105
|
+
const nonce = resolveNonce(options.nonce);
|
|
106
|
+
|
|
107
|
+
// Normalize + merge.
|
|
108
|
+
const merged: Record<string, string[]> = {};
|
|
109
|
+
for (const [k, v] of Object.entries(DEFAULT_CSP_DIRECTIVES)) {
|
|
110
|
+
merged[normalizeDirectiveName(k)] = [...v];
|
|
111
|
+
}
|
|
112
|
+
if (options.directives) {
|
|
113
|
+
for (const [k, v] of Object.entries(options.directives)) {
|
|
114
|
+
if (!Array.isArray(v)) continue;
|
|
115
|
+
merged[normalizeDirectiveName(k)] = [...v];
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const parts: string[] = [];
|
|
120
|
+
for (const [name, values] of Object.entries(merged)) {
|
|
121
|
+
const processed = values
|
|
122
|
+
.map((v) => interpolateNonce(v, nonce))
|
|
123
|
+
// Strip broken nonce tokens when no nonce was produced.
|
|
124
|
+
.filter((v): v is string => v !== null);
|
|
125
|
+
|
|
126
|
+
if (processed.length === 0) {
|
|
127
|
+
// Flag-style directive (no value list) — emit bare name.
|
|
128
|
+
parts.push(name);
|
|
129
|
+
} else {
|
|
130
|
+
parts.push(`${name} ${processed.join(" ")}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
header: parts.join("; "),
|
|
136
|
+
name: options.reportOnly
|
|
137
|
+
? "Content-Security-Policy-Report-Only"
|
|
138
|
+
: "Content-Security-Policy",
|
|
139
|
+
nonce,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ========== Internal ==========
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Resolve the `nonce` option to a concrete string (or undefined for "off").
|
|
147
|
+
* Generation uses `crypto.getRandomValues` rather than `crypto.randomUUID`
|
|
148
|
+
* to match OWASP's recommendation of ≥128 bits of entropy with raw random
|
|
149
|
+
* bytes (UUIDs have lower effective entropy due to version/variant bits).
|
|
150
|
+
*/
|
|
151
|
+
function resolveNonce(opt: CspOptions["nonce"]): string | undefined {
|
|
152
|
+
if (opt === true) {
|
|
153
|
+
const bytes = new Uint8Array(16);
|
|
154
|
+
crypto.getRandomValues(bytes);
|
|
155
|
+
return base64UrlEncode(bytes);
|
|
156
|
+
}
|
|
157
|
+
if (typeof opt === "string" && opt.length > 0) {
|
|
158
|
+
return opt;
|
|
159
|
+
}
|
|
160
|
+
return undefined;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Replace `{NONCE}` in a single directive value.
|
|
165
|
+
*
|
|
166
|
+
* Returns `null` to signal "this value depended on a nonce that was never
|
|
167
|
+
* produced — drop it entirely" so callers don't emit `'nonce-'` (broken
|
|
168
|
+
* token). Returns the original value when no placeholder is present.
|
|
169
|
+
*/
|
|
170
|
+
function interpolateNonce(value: string, nonce: string | undefined): string | null {
|
|
171
|
+
if (!value.includes(NONCE_PLACEHOLDER)) return value;
|
|
172
|
+
if (!nonce) return null;
|
|
173
|
+
return value.split(NONCE_PLACEHOLDER).join(nonce);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Accepts `"scriptSrc"` / `"script-src"` / `"Script-Src"` → `"script-src"`.
|
|
178
|
+
* camelCase → kebab-case via the standard regex; already-kebab inputs are
|
|
179
|
+
* lowercased without further transformation.
|
|
180
|
+
*/
|
|
181
|
+
function normalizeDirectiveName(name: string): string {
|
|
182
|
+
return name
|
|
183
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1-$2")
|
|
184
|
+
.toLowerCase();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function base64UrlEncode(bytes: Uint8Array): string {
|
|
188
|
+
let binary = "";
|
|
189
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
190
|
+
binary += String.fromCharCode(bytes[i]);
|
|
191
|
+
}
|
|
192
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
193
|
+
}
|