@timber-js/app 0.2.0-alpha.97 → 0.2.0-alpha.99
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/_chunks/actions-CQ8Z8VGL.js +1061 -0
- package/dist/_chunks/actions-CQ8Z8VGL.js.map +1 -0
- package/dist/_chunks/build-output-helper-DXnW0qjz.js +61 -0
- package/dist/_chunks/build-output-helper-DXnW0qjz.js.map +1 -0
- package/dist/_chunks/{define-Itxvcd7F.js → define-B-Q_UMOD.js} +19 -23
- package/dist/_chunks/define-B-Q_UMOD.js.map +1 -0
- package/dist/_chunks/{define-C77ScO0m.js → define-CfBPoJb0.js} +24 -7
- package/dist/_chunks/define-CfBPoJb0.js.map +1 -0
- package/dist/_chunks/define-cookie-BjpIt4UC.js +194 -0
- package/dist/_chunks/define-cookie-BjpIt4UC.js.map +1 -0
- package/dist/_chunks/{format-CYBGxKtc.js → format-Bcn-Iv1x.js} +1 -1
- package/dist/_chunks/{format-CYBGxKtc.js.map → format-Bcn-Iv1x.js.map} +1 -1
- package/dist/_chunks/handler-store-B-lqaGyh.js +54 -0
- package/dist/_chunks/handler-store-B-lqaGyh.js.map +1 -0
- package/dist/_chunks/logger-0m8MsKdc.js +291 -0
- package/dist/_chunks/logger-0m8MsKdc.js.map +1 -0
- package/dist/_chunks/merge-search-params-BphMdht_.js +122 -0
- package/dist/_chunks/merge-search-params-BphMdht_.js.map +1 -0
- package/dist/_chunks/{metadata-routes-DS3eKNmf.js → metadata-routes-BU684ls2.js} +1 -1
- package/dist/_chunks/{metadata-routes-DS3eKNmf.js.map → metadata-routes-BU684ls2.js.map} +1 -1
- package/dist/_chunks/navigation-root-BCYczjml.js +96 -0
- package/dist/_chunks/navigation-root-BCYczjml.js.map +1 -0
- package/dist/_chunks/registry-I2ss-lvy.js +20 -0
- package/dist/_chunks/registry-I2ss-lvy.js.map +1 -0
- package/dist/_chunks/router-ref-h3-UaCQv.js +28 -0
- package/dist/_chunks/router-ref-h3-UaCQv.js.map +1 -0
- package/dist/_chunks/{schema-bridge-C3xl_vfb.js → schema-bridge-Cxu4l-7p.js} +1 -1
- package/dist/_chunks/{schema-bridge-C3xl_vfb.js.map → schema-bridge-Cxu4l-7p.js.map} +1 -1
- package/dist/_chunks/segment-classify-BjfuctV2.js +137 -0
- package/dist/_chunks/segment-classify-BjfuctV2.js.map +1 -0
- package/dist/_chunks/{segment-context-fHFLF1PE.js → segment-context-Dx_OizxD.js} +1 -1
- package/dist/_chunks/{segment-context-fHFLF1PE.js.map → segment-context-Dx_OizxD.js.map} +1 -1
- package/dist/_chunks/{router-ref-C8OCm7g7.js → ssr-data-B4CdH7rE.js} +2 -26
- package/dist/_chunks/ssr-data-B4CdH7rE.js.map +1 -0
- package/dist/_chunks/{stale-reload-BX5gL1r-.js → stale-reload-Bab885FO.js} +1 -1
- package/dist/_chunks/{stale-reload-BX5gL1r-.js.map → stale-reload-Bab885FO.js.map} +1 -1
- package/dist/_chunks/tracing-C8V-YGsP.js +329 -0
- package/dist/_chunks/tracing-C8V-YGsP.js.map +1 -0
- package/dist/_chunks/{use-query-states-BiV5GJgm.js → use-query-states-B2XTqxDR.js} +3 -19
- package/dist/_chunks/use-query-states-B2XTqxDR.js.map +1 -0
- package/dist/_chunks/{use-params-IOPu7E8t.js → use-segment-params-BkpKAQ7D.js} +9 -95
- package/dist/_chunks/use-segment-params-BkpKAQ7D.js.map +1 -0
- package/dist/_chunks/{interception-BbqMCVXa.js → walkers-Tg0Alwcg.js} +66 -87
- package/dist/_chunks/walkers-Tg0Alwcg.js.map +1 -0
- package/dist/_chunks/{dev-warnings-DpGRGoDi.js → warnings-Cg47l5sk.js} +3 -3
- package/dist/_chunks/warnings-Cg47l5sk.js.map +1 -0
- package/dist/adapters/build-output-helper.d.ts +28 -0
- package/dist/adapters/build-output-helper.d.ts.map +1 -0
- package/dist/adapters/cloudflare.d.ts.map +1 -1
- package/dist/adapters/cloudflare.js +8 -28
- package/dist/adapters/cloudflare.js.map +1 -1
- package/dist/adapters/nitro.d.ts.map +1 -1
- package/dist/adapters/nitro.js +63 -31
- package/dist/adapters/nitro.js.map +1 -1
- package/dist/adapters/shared.d.ts +16 -0
- package/dist/adapters/shared.d.ts.map +1 -0
- package/dist/cache/index.js +9 -2
- package/dist/cache/index.js.map +1 -1
- package/dist/cache/timber-cache.d.ts.map +1 -1
- package/dist/client/error-boundary.js +2 -1
- package/dist/client/error-boundary.js.map +1 -1
- package/dist/client/form.d.ts +10 -24
- package/dist/client/form.d.ts.map +1 -1
- package/dist/client/index.d.ts +1 -5
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +41 -91
- package/dist/client/index.js.map +1 -1
- package/dist/client/internal.d.ts +2 -1
- package/dist/client/internal.d.ts.map +1 -1
- package/dist/client/internal.js +81 -7
- package/dist/client/internal.js.map +1 -1
- package/dist/client/rsc-fetch.d.ts.map +1 -1
- package/dist/client/state.d.ts +1 -1
- package/dist/client/use-cookie.d.ts +8 -0
- package/dist/client/use-cookie.d.ts.map +1 -1
- package/dist/client/{use-params.d.ts → use-segment-params.d.ts} +1 -1
- package/dist/client/use-segment-params.d.ts.map +1 -0
- package/dist/codec.d.ts +1 -1
- package/dist/codec.d.ts.map +1 -1
- package/dist/codec.js +2 -2
- package/dist/config-types.d.ts +28 -0
- package/dist/config-types.d.ts.map +1 -1
- package/dist/cookies/define-cookie.d.ts +87 -35
- package/dist/cookies/define-cookie.d.ts.map +1 -1
- package/dist/cookies/index.d.ts +2 -1
- package/dist/cookies/index.d.ts.map +1 -1
- package/dist/cookies/index.js +48 -2
- package/dist/cookies/index.js.map +1 -0
- package/dist/cookies/json-cookie.d.ts +64 -0
- package/dist/cookies/json-cookie.d.ts.map +1 -0
- package/dist/cookies/validation.d.ts +46 -0
- package/dist/cookies/validation.d.ts.map +1 -0
- package/dist/{plugins/dev-404-page.d.ts → dev-tools/404-page.d.ts} +9 -19
- package/dist/dev-tools/404-page.d.ts.map +1 -0
- package/dist/{plugins/dev-browser-logs.d.ts → dev-tools/browser-logs.d.ts} +1 -1
- package/dist/dev-tools/browser-logs.d.ts.map +1 -0
- package/dist/{plugins/dev-error-page.d.ts → dev-tools/error-page.d.ts} +2 -2
- package/dist/dev-tools/error-page.d.ts.map +1 -0
- package/dist/{server/dev-holding-server.d.ts → dev-tools/holding-server.d.ts} +5 -3
- package/dist/dev-tools/holding-server.d.ts.map +1 -0
- package/dist/dev-tools/index.d.ts +31 -0
- package/dist/dev-tools/index.d.ts.map +1 -0
- package/dist/{server/dev-span-processor.d.ts → dev-tools/instrumentation.d.ts} +26 -6
- package/dist/dev-tools/instrumentation.d.ts.map +1 -0
- package/dist/{server/dev-logger.d.ts → dev-tools/logger.d.ts} +1 -1
- package/dist/dev-tools/logger.d.ts.map +1 -0
- package/dist/{plugins/dev-logs.d.ts → dev-tools/logs.d.ts} +1 -1
- package/dist/dev-tools/logs.d.ts.map +1 -0
- package/dist/{plugins/dev-error-overlay.d.ts → dev-tools/overlay.d.ts} +3 -12
- package/dist/dev-tools/overlay.d.ts.map +1 -0
- package/dist/dev-tools/stack-classifier.d.ts +34 -0
- package/dist/dev-tools/stack-classifier.d.ts.map +1 -0
- package/dist/{plugins/dev-terminal-error.d.ts → dev-tools/terminal.d.ts} +2 -2
- package/dist/dev-tools/terminal.d.ts.map +1 -0
- package/dist/{server/dev-warnings.d.ts → dev-tools/warnings.d.ts} +1 -1
- package/dist/dev-tools/warnings.d.ts.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +285 -133
- package/dist/index.js.map +1 -1
- package/dist/plugin-context.d.ts +1 -1
- package/dist/plugin-context.d.ts.map +1 -1
- package/dist/plugins/adapter-build.d.ts.map +1 -1
- package/dist/plugins/build-report.d.ts +6 -4
- package/dist/plugins/build-report.d.ts.map +1 -1
- package/dist/routing/convention-lint.d.ts.map +1 -1
- package/dist/routing/index.d.ts +5 -3
- package/dist/routing/index.d.ts.map +1 -1
- package/dist/routing/index.js +3 -3
- package/dist/routing/scanner.d.ts +1 -10
- package/dist/routing/scanner.d.ts.map +1 -1
- package/dist/routing/segment-classify.d.ts +37 -8
- package/dist/routing/segment-classify.d.ts.map +1 -1
- package/dist/routing/status-file-lint.d.ts.map +1 -1
- package/dist/routing/types.d.ts +63 -23
- package/dist/routing/types.d.ts.map +1 -1
- package/dist/routing/walkers.d.ts +51 -0
- package/dist/routing/walkers.d.ts.map +1 -0
- package/dist/search-params/define.d.ts +25 -7
- package/dist/search-params/define.d.ts.map +1 -1
- package/dist/search-params/index.js +5 -3
- package/dist/search-params/index.js.map +1 -1
- package/dist/search-params/wrappers.d.ts +2 -2
- package/dist/search-params/wrappers.d.ts.map +1 -1
- package/dist/segment-params/define.d.ts +23 -6
- package/dist/segment-params/define.d.ts.map +1 -1
- package/dist/segment-params/index.js +1 -1
- package/dist/server/access-gate.d.ts +4 -3
- package/dist/server/access-gate.d.ts.map +1 -1
- package/dist/server/action-handler.d.ts +15 -6
- package/dist/server/action-handler.d.ts.map +1 -1
- package/dist/server/als-registry.d.ts +5 -5
- package/dist/server/als-registry.d.ts.map +1 -1
- package/dist/server/asset-headers.d.ts +1 -15
- package/dist/server/asset-headers.d.ts.map +1 -1
- package/dist/server/cookie-context.d.ts +170 -0
- package/dist/server/cookie-context.d.ts.map +1 -0
- package/dist/server/cookie-parsing.d.ts +51 -0
- package/dist/server/cookie-parsing.d.ts.map +1 -0
- package/dist/server/deny-boundary.d.ts +90 -0
- package/dist/server/deny-boundary.d.ts.map +1 -0
- package/dist/server/deny-renderer.d.ts.map +1 -1
- package/dist/server/early-hints-sender.d.ts.map +1 -1
- package/dist/server/html-injector-core.d.ts +212 -0
- package/dist/server/html-injector-core.d.ts.map +1 -0
- package/dist/server/html-injectors.d.ts +59 -59
- package/dist/server/html-injectors.d.ts.map +1 -1
- package/dist/server/index.d.ts +5 -4
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +4 -149
- package/dist/server/index.js.map +1 -1
- package/dist/server/internal.d.ts +6 -4
- package/dist/server/internal.d.ts.map +1 -1
- package/dist/server/internal.js +852 -852
- package/dist/server/internal.js.map +1 -1
- package/dist/server/logger.d.ts +14 -0
- package/dist/server/logger.d.ts.map +1 -1
- package/dist/server/middleware-runner.d.ts +17 -0
- package/dist/server/middleware-runner.d.ts.map +1 -1
- package/dist/server/node-stream-transforms.d.ts +46 -49
- package/dist/server/node-stream-transforms.d.ts.map +1 -1
- package/dist/server/param-coercion.d.ts +26 -0
- package/dist/server/param-coercion.d.ts.map +1 -0
- package/dist/server/pipeline-helpers.d.ts +95 -0
- package/dist/server/pipeline-helpers.d.ts.map +1 -0
- package/dist/server/pipeline-outcome.d.ts +49 -0
- package/dist/server/pipeline-outcome.d.ts.map +1 -0
- package/dist/server/pipeline-phases.d.ts +52 -0
- package/dist/server/pipeline-phases.d.ts.map +1 -0
- package/dist/server/pipeline.d.ts +51 -32
- package/dist/server/pipeline.d.ts.map +1 -1
- package/dist/server/port-resolution.d.ts +117 -0
- package/dist/server/port-resolution.d.ts.map +1 -0
- package/dist/server/request-context.d.ts +22 -159
- package/dist/server/request-context.d.ts.map +1 -1
- package/dist/server/route-element-builder.d.ts.map +1 -1
- package/dist/server/route-matcher.d.ts +20 -47
- package/dist/server/route-matcher.d.ts.map +1 -1
- package/dist/server/rsc-entry/action-middleware-runner.d.ts +66 -0
- package/dist/server/rsc-entry/action-middleware-runner.d.ts.map +1 -0
- package/dist/server/rsc-entry/helpers.d.ts +1 -1
- package/dist/server/rsc-entry/helpers.d.ts.map +1 -1
- package/dist/server/rsc-entry/index.d.ts.map +1 -1
- package/dist/server/rsc-entry/render-route.d.ts +50 -0
- package/dist/server/rsc-entry/render-route.d.ts.map +1 -0
- package/dist/server/rsc-entry/wrap-action-dispatch.d.ts +119 -0
- package/dist/server/rsc-entry/wrap-action-dispatch.d.ts.map +1 -0
- package/dist/server/state-tree-diff.d.ts.map +1 -1
- package/dist/server/status-code-resolver.d.ts +16 -11
- package/dist/server/status-code-resolver.d.ts.map +1 -1
- package/dist/server/tracing.d.ts +1 -1
- package/dist/server/tracing.d.ts.map +1 -1
- package/dist/server/tree-builder.d.ts +45 -16
- package/dist/server/tree-builder.d.ts.map +1 -1
- package/dist/server/types.d.ts +48 -0
- package/dist/server/types.d.ts.map +1 -1
- package/dist/server/utils/escape-html.d.ts +14 -0
- package/dist/server/utils/escape-html.d.ts.map +1 -0
- package/dist/shims/headers.d.ts +2 -2
- package/dist/shims/headers.d.ts.map +1 -1
- package/dist/shims/navigation-client.d.ts +3 -1
- package/dist/shims/navigation-client.d.ts.map +1 -1
- package/dist/shims/navigation.d.ts +9 -4
- package/dist/shims/navigation.d.ts.map +1 -1
- package/dist/utils/directive-parser.d.ts +0 -45
- package/dist/utils/directive-parser.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/adapters/build-output-helper.ts +77 -0
- package/src/adapters/cloudflare.ts +10 -50
- package/src/adapters/nitro.ts +66 -50
- package/src/adapters/shared.ts +40 -0
- package/src/cache/timber-cache.ts +3 -2
- package/src/client/form.tsx +17 -25
- package/src/client/index.ts +16 -9
- package/src/client/internal.ts +3 -2
- package/src/client/router.ts +1 -1
- package/src/client/rsc-fetch.ts +15 -0
- package/src/client/state.ts +2 -2
- package/src/client/use-cookie.ts +29 -0
- package/src/codec.ts +3 -7
- package/src/config-types.ts +28 -0
- package/src/cookies/define-cookie.ts +271 -78
- package/src/cookies/index.ts +11 -8
- package/src/cookies/json-cookie.ts +105 -0
- package/src/cookies/validation.ts +134 -0
- package/src/{plugins/dev-404-page.ts → dev-tools/404-page.ts} +17 -48
- package/src/{plugins/dev-error-page.ts → dev-tools/error-page.ts} +5 -32
- package/src/{server/dev-holding-server.ts → dev-tools/holding-server.ts} +4 -2
- package/src/dev-tools/index.ts +90 -0
- package/src/dev-tools/instrumentation.ts +176 -0
- package/src/{plugins/dev-logs.ts → dev-tools/logs.ts} +2 -2
- package/src/{plugins/dev-error-overlay.ts → dev-tools/overlay.ts} +5 -23
- package/src/dev-tools/stack-classifier.ts +75 -0
- package/src/{plugins/dev-terminal-error.ts → dev-tools/terminal.ts} +4 -38
- package/src/{server/dev-warnings.ts → dev-tools/warnings.ts} +1 -1
- package/src/index.ts +95 -34
- package/src/plugin-context.ts +1 -1
- package/src/plugins/adapter-build.ts +3 -1
- package/src/plugins/build-report.ts +13 -22
- package/src/plugins/dev-server.ts +3 -3
- package/src/plugins/routing.ts +14 -12
- package/src/plugins/shims.ts +1 -1
- package/src/plugins/static-build.ts +1 -1
- package/src/routing/codegen.ts +1 -1
- package/src/routing/convention-lint.ts +9 -8
- package/src/routing/index.ts +5 -3
- package/src/routing/interception.ts +1 -1
- package/src/routing/scanner.ts +22 -95
- package/src/routing/segment-classify.ts +107 -8
- package/src/routing/status-file-lint.ts +7 -5
- package/src/routing/types.ts +63 -23
- package/src/routing/walkers.ts +90 -0
- package/src/search-params/define.ts +71 -15
- package/src/search-params/wrappers.ts +9 -2
- package/src/segment-params/define.ts +66 -13
- package/src/server/access-gate.tsx +9 -8
- package/src/server/action-handler.ts +34 -38
- package/src/server/als-registry.ts +5 -5
- package/src/server/asset-headers.ts +8 -34
- package/src/server/cookie-context.ts +468 -0
- package/src/server/cookie-parsing.ts +135 -0
- package/src/server/{deny-page-resolver.ts → deny-boundary.ts} +78 -14
- package/src/server/deny-renderer.ts +7 -12
- package/src/server/early-hints-sender.ts +3 -2
- package/src/server/fallback-error.ts +2 -2
- package/src/server/html-injector-core.ts +403 -0
- package/src/server/html-injectors.ts +158 -297
- package/src/server/index.ts +13 -14
- package/src/server/internal.ts +10 -3
- package/src/server/logger.ts +23 -0
- package/src/server/middleware-runner.ts +44 -0
- package/src/server/node-stream-transforms.ts +108 -248
- package/src/server/param-coercion.ts +76 -0
- package/src/server/pipeline-helpers.ts +204 -0
- package/src/server/pipeline-outcome.ts +167 -0
- package/src/server/pipeline-phases.ts +409 -0
- package/src/server/pipeline.ts +70 -540
- package/src/server/port-resolution.ts +215 -0
- package/src/server/request-context.ts +46 -451
- package/src/server/route-element-builder.ts +8 -4
- package/src/server/route-matcher.ts +28 -60
- package/src/server/rsc-entry/action-middleware-runner.ts +167 -0
- package/src/server/rsc-entry/api-handler.ts +2 -2
- package/src/server/rsc-entry/error-renderer.ts +2 -2
- package/src/server/rsc-entry/helpers.ts +2 -7
- package/src/server/rsc-entry/index.ts +81 -366
- package/src/server/rsc-entry/render-route.ts +304 -0
- package/src/server/rsc-entry/rsc-payload.ts +1 -1
- package/src/server/rsc-entry/ssr-renderer.ts +2 -2
- package/src/server/rsc-entry/wrap-action-dispatch.ts +449 -0
- package/src/server/sitemap-generator.ts +1 -1
- package/src/server/slot-resolver.ts +1 -1
- package/src/server/ssr-entry.ts +1 -1
- package/src/server/state-tree-diff.ts +4 -1
- package/src/server/status-code-resolver.ts +112 -128
- package/src/server/tracing.ts +3 -3
- package/src/server/tree-builder.ts +134 -56
- package/src/server/types.ts +52 -0
- package/src/server/utils/escape-html.ts +20 -0
- package/src/shims/headers.ts +3 -3
- package/src/shims/navigation-client.ts +4 -3
- package/src/shims/navigation.ts +9 -7
- package/src/utils/directive-parser.ts +0 -392
- package/dist/_chunks/actions-DLnUaR65.js +0 -421
- package/dist/_chunks/actions-DLnUaR65.js.map +0 -1
- package/dist/_chunks/als-registry-HS0LGUl2.js +0 -41
- package/dist/_chunks/als-registry-HS0LGUl2.js.map +0 -1
- package/dist/_chunks/debug-ECi_61pb.js +0 -108
- package/dist/_chunks/debug-ECi_61pb.js.map +0 -1
- package/dist/_chunks/define-C77ScO0m.js.map +0 -1
- package/dist/_chunks/define-Itxvcd7F.js.map +0 -1
- package/dist/_chunks/define-cookie-BowvzoP0.js +0 -94
- package/dist/_chunks/define-cookie-BowvzoP0.js.map +0 -1
- package/dist/_chunks/dev-warnings-DpGRGoDi.js.map +0 -1
- package/dist/_chunks/interception-BbqMCVXa.js.map +0 -1
- package/dist/_chunks/merge-search-params-Cm_KIWDX.js +0 -41
- package/dist/_chunks/merge-search-params-Cm_KIWDX.js.map +0 -1
- package/dist/_chunks/request-context-CK5tZqIP.js +0 -478
- package/dist/_chunks/request-context-CK5tZqIP.js.map +0 -1
- package/dist/_chunks/router-ref-C8OCm7g7.js.map +0 -1
- package/dist/_chunks/segment-classify-BDNn6EzD.js +0 -65
- package/dist/_chunks/segment-classify-BDNn6EzD.js.map +0 -1
- package/dist/_chunks/tracing-CCYbKn5n.js +0 -238
- package/dist/_chunks/tracing-CCYbKn5n.js.map +0 -1
- package/dist/_chunks/use-params-IOPu7E8t.js.map +0 -1
- package/dist/_chunks/use-query-states-BiV5GJgm.js.map +0 -1
- package/dist/client/use-params.d.ts.map +0 -1
- package/dist/plugins/dev-404-page.d.ts.map +0 -1
- package/dist/plugins/dev-browser-logs.d.ts.map +0 -1
- package/dist/plugins/dev-error-overlay.d.ts.map +0 -1
- package/dist/plugins/dev-error-page.d.ts.map +0 -1
- package/dist/plugins/dev-logs.d.ts.map +0 -1
- package/dist/plugins/dev-terminal-error.d.ts.map +0 -1
- package/dist/server/deny-page-resolver.d.ts +0 -52
- package/dist/server/deny-page-resolver.d.ts.map +0 -1
- package/dist/server/dev-fetch-instrumentation.d.ts +0 -22
- package/dist/server/dev-fetch-instrumentation.d.ts.map +0 -1
- package/dist/server/dev-holding-server.d.ts.map +0 -1
- package/dist/server/dev-logger.d.ts.map +0 -1
- package/dist/server/dev-span-processor.d.ts.map +0 -1
- package/dist/server/dev-warnings.d.ts.map +0 -1
- package/dist/server/manifest-status-resolver.d.ts +0 -58
- package/dist/server/manifest-status-resolver.d.ts.map +0 -1
- package/dist/server/page-deny-boundary.d.ts +0 -31
- package/dist/server/page-deny-boundary.d.ts.map +0 -1
- package/src/server/dev-fetch-instrumentation.ts +0 -96
- package/src/server/dev-span-processor.ts +0 -78
- package/src/server/manifest-status-resolver.ts +0 -215
- package/src/server/page-deny-boundary.tsx +0 -56
- /package/src/client/{use-params.ts → use-segment-params.ts} +0 -0
- /package/src/{plugins/dev-browser-logs.ts → dev-tools/browser-logs.ts} +0 -0
- /package/src/{server/dev-logger.ts → dev-tools/logger.ts} +0 -0
package/src/config-types.ts
CHANGED
|
@@ -60,6 +60,34 @@ export interface TimberUserConfig {
|
|
|
60
60
|
* See design/08-forms-and-actions.md §"Validation errors" and
|
|
61
61
|
* design/13-security.md §"Sensitive field stripping".
|
|
62
62
|
*/
|
|
63
|
+
/**
|
|
64
|
+
* Server action runtime behavior.
|
|
65
|
+
*
|
|
66
|
+
* See design/08-forms-and-actions.md §"Middleware for Server Actions".
|
|
67
|
+
*/
|
|
68
|
+
actions?: {
|
|
69
|
+
/**
|
|
70
|
+
* Run `middleware.ts` on server action requests before dispatching.
|
|
71
|
+
*
|
|
72
|
+
* **Default: `true`** — middleware runs on every action POST so
|
|
73
|
+
* authentication, rate limiting, tenant isolation, IP allow-listing,
|
|
74
|
+
* and request-header injection apply uniformly to page renders, route
|
|
75
|
+
* handlers, and server actions. This closes the auth-bypass class of
|
|
76
|
+
* issue identified by Next.js CVE-2025-29927: developers can put a
|
|
77
|
+
* single auth check in `middleware.ts` and trust that it gates every
|
|
78
|
+
* unsafe-method request.
|
|
79
|
+
*
|
|
80
|
+
* Set to `false` to restore the legacy behavior where actions skip
|
|
81
|
+
* middleware entirely. This is **not recommended** outside of niche
|
|
82
|
+
* cases (e.g. middleware that rewrites POST bodies and would corrupt
|
|
83
|
+
* action submissions). When false, you are responsible for placing
|
|
84
|
+
* auth, rate limiting, and other cross-cutting checks inside every
|
|
85
|
+
* action — typically via `createActionClient({ middleware: ... })`.
|
|
86
|
+
*
|
|
87
|
+
* See TIM-871.
|
|
88
|
+
*/
|
|
89
|
+
runMiddleware?: boolean;
|
|
90
|
+
};
|
|
63
91
|
forms?: {
|
|
64
92
|
/**
|
|
65
93
|
* Strip sensitive fields (passwords, tokens, CVV, SSN, etc.) from the
|
|
@@ -2,31 +2,27 @@
|
|
|
2
2
|
* defineCookie — typed cookie definitions.
|
|
3
3
|
*
|
|
4
4
|
* Bundles name + codec + options into a reusable CookieDefinition<T>
|
|
5
|
-
* with
|
|
6
|
-
* and a
|
|
5
|
+
* with sync .get(), .set(), .delete() isomorphic methods (server + client)
|
|
6
|
+
* and a .useCookie() client hook.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
8
|
+
* Uses the registration pattern: server entry registers serverCookieImpl,
|
|
9
|
+
* client entry registers clientCookieImpl. This avoids top-level value
|
|
10
|
+
* imports from either environment and makes defineCookie isomorphic
|
|
11
|
+
* without `typeof window` checks.
|
|
10
12
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* IMPORTANT: This module must NOT have top-level value imports from either
|
|
15
|
-
* server or client modules. Server methods lazy-import request-context;
|
|
16
|
-
* useCookie() lazy-imports use-cookie. This ensures:
|
|
17
|
-
* - Client bundles don't pull in ALS/server code
|
|
18
|
-
* - RSC bundles don't pull in useSyncExternalStore/client code
|
|
19
|
-
* - Tree-shaking is not required for correctness
|
|
13
|
+
* Standard Schema objects (Zod, Valibot, ArkType) are auto-detected in
|
|
14
|
+
* the codec option — no explicit fromSchema() wrapper needed.
|
|
20
15
|
*
|
|
21
16
|
* See design/29-cookies.md §"Typed Cookies with Schema Validation"
|
|
22
17
|
*/
|
|
23
18
|
|
|
24
|
-
import type { CookieOptions } from '../server/
|
|
19
|
+
import type { CookieOptions } from '../server/cookie-context.js';
|
|
25
20
|
import type { ClientCookieOptions } from '../client/use-cookie.js';
|
|
26
21
|
|
|
27
22
|
// ─── Types ────────────────────────────────────────────────────────────────
|
|
28
23
|
|
|
29
24
|
import type { Codec } from '../codec.js';
|
|
25
|
+
import type { StandardSchemaV1 } from '../schema-bridge.js';
|
|
30
26
|
|
|
31
27
|
/**
|
|
32
28
|
* A codec that converts between string cookie values and typed values.
|
|
@@ -35,46 +31,110 @@ import type { Codec } from '../codec.js';
|
|
|
35
31
|
export type CookieCodec<T> = Codec<T>;
|
|
36
32
|
|
|
37
33
|
/** Options for defineCookie: codec + CookieOptions merged. */
|
|
38
|
-
export interface DefineCookieOptions<T> extends CookieOptions {
|
|
39
|
-
/**
|
|
40
|
-
|
|
34
|
+
export interface DefineCookieOptions<T, HttpOnly extends boolean = boolean> extends CookieOptions {
|
|
35
|
+
/**
|
|
36
|
+
* Codec for parsing/serializing the cookie value.
|
|
37
|
+
* Accepts a Codec<T> or a Standard Schema object (Zod, Valibot, ArkType)
|
|
38
|
+
* which is auto-wrapped via fromSchema.
|
|
39
|
+
*/
|
|
40
|
+
codec: CookieCodec<T> | StandardSchemaV1<T>;
|
|
41
|
+
/**
|
|
42
|
+
* Prevent client-side JS access. Default: true.
|
|
43
|
+
* When true (or omitted), client methods (useCookie, get/set/delete on
|
|
44
|
+
* client) are omitted from the return type and throw at runtime.
|
|
45
|
+
*/
|
|
46
|
+
httpOnly?: HttpOnly;
|
|
41
47
|
}
|
|
42
48
|
|
|
43
|
-
/**
|
|
44
|
-
|
|
45
|
-
|
|
49
|
+
/**
|
|
50
|
+
* Server-only cookie definition. Returned when httpOnly is true or omitted.
|
|
51
|
+
* Client methods are absent from the type — accessing them is a TS error.
|
|
52
|
+
*/
|
|
53
|
+
export interface ServerOnlyCookieDefinition<T> {
|
|
46
54
|
readonly name: string;
|
|
47
|
-
/** The resolved cookie options (without codec). */
|
|
48
55
|
readonly options: CookieOptions;
|
|
49
|
-
/** The codec used for parsing/serializing. */
|
|
50
56
|
readonly codec: CookieCodec<T>;
|
|
51
57
|
|
|
52
|
-
/**
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
|
|
58
|
+
/** Read the typed value. Sync, isomorphic (server + client). */
|
|
59
|
+
get(): T;
|
|
60
|
+
/** Set the typed value. Sync, isomorphic (server + client). */
|
|
61
|
+
set(value: T): void;
|
|
62
|
+
/** Delete the cookie. Sync, isomorphic (server + client). */
|
|
63
|
+
delete(): void;
|
|
64
|
+
}
|
|
58
65
|
|
|
66
|
+
/**
|
|
67
|
+
* Full cookie definition with client methods. Returned when httpOnly: false.
|
|
68
|
+
*/
|
|
69
|
+
export interface CookieDefinition<T> extends ServerOnlyCookieDefinition<T> {
|
|
59
70
|
/** Client: React hook for reading/writing this cookie. Returns [value, setter, deleter]. */
|
|
60
71
|
useCookie(): [T, (value: T) => void, () => void];
|
|
61
72
|
}
|
|
62
73
|
|
|
63
|
-
// ───
|
|
74
|
+
// ─── Registration Pattern ─────────────────────────────────────────────────
|
|
64
75
|
//
|
|
65
|
-
//
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
|
|
76
|
+
// Server and client entries register their implementations at module load
|
|
77
|
+
// time. This avoids dynamic imports (which lose ALS context) and typeof
|
|
78
|
+
// window checks (which are fragile).
|
|
79
|
+
|
|
80
|
+
/** Server-side cookie impl: reads from ALS-backed cookie jar. */
|
|
81
|
+
export interface ServerCookieImpl {
|
|
82
|
+
getCookieJar(): {
|
|
83
|
+
get(name: string): string | undefined;
|
|
84
|
+
set(name: string, value: string, options?: CookieOptions): void;
|
|
85
|
+
delete(name: string, options?: Pick<CookieOptions, 'path' | 'domain'>): void;
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Client-side cookie impl: reads/writes document.cookie via useCookie hook. */
|
|
90
|
+
export interface ClientCookieImpl {
|
|
91
|
+
useCookie(
|
|
92
|
+
name: string,
|
|
93
|
+
options?: ClientCookieOptions
|
|
94
|
+
): [string | undefined, (value: string, options?: ClientCookieOptions) => void, () => void];
|
|
95
|
+
getCookieValue(name: string): string | undefined;
|
|
96
|
+
setCookieValue(name: string, value: string, options?: ClientCookieOptions): void;
|
|
97
|
+
deleteCookieValue(name: string, options?: ClientCookieOptions): void;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
let _serverImpl: ServerCookieImpl | undefined;
|
|
101
|
+
let _clientImpl: ClientCookieImpl | undefined;
|
|
102
|
+
let _fromSchemaFn: ((schema: StandardSchemaV1<unknown>) => CookieCodec<unknown>) | undefined;
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Register the server-side cookie implementation.
|
|
106
|
+
* Called by server entry at module load time.
|
|
107
|
+
* @internal
|
|
108
|
+
*/
|
|
109
|
+
export function _registerServerCookieImpl(impl: ServerCookieImpl): void {
|
|
110
|
+
_serverImpl = impl;
|
|
111
|
+
}
|
|
69
112
|
|
|
113
|
+
/**
|
|
114
|
+
* Register the client-side cookie implementation.
|
|
115
|
+
* Called by client entry at module load time.
|
|
116
|
+
* @internal
|
|
117
|
+
*/
|
|
118
|
+
export function _registerClientCookieImpl(impl: ClientCookieImpl): void {
|
|
119
|
+
_clientImpl = impl;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Register the fromSchema bridge function.
|
|
124
|
+
* Called by server/client entry at module load time.
|
|
125
|
+
* @internal
|
|
126
|
+
*/
|
|
127
|
+
export function _registerFromSchema(
|
|
128
|
+
fn: (schema: StandardSchemaV1<unknown>) => CookieCodec<unknown>
|
|
129
|
+
): void {
|
|
130
|
+
_fromSchemaFn = fn;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Legacy registration for useCookie module — still used by client/index.ts
|
|
70
134
|
let _useCookieModule: typeof import('../client/use-cookie.js') | undefined;
|
|
71
135
|
|
|
72
136
|
function getUseCookieModule(): typeof import('../client/use-cookie.js') {
|
|
73
137
|
if (!_useCookieModule) {
|
|
74
|
-
// In the client/SSR environment, this module is already in the module
|
|
75
|
-
// graph (imported by the client entry). The throw is a safeguard —
|
|
76
|
-
// if useCookie() is somehow called before the module is available,
|
|
77
|
-
// the developer gets a clear error instead of a silent failure.
|
|
78
138
|
throw new Error(
|
|
79
139
|
'[timber] defineCookie().useCookie() requires @timber-js/app/client to be loaded. ' +
|
|
80
140
|
'This hook can only be used in client components.'
|
|
@@ -91,6 +151,68 @@ function getUseCookieModule(): typeof import('../client/use-cookie.js') {
|
|
|
91
151
|
*/
|
|
92
152
|
export function _registerUseCookieModule(mod: typeof import('../client/use-cookie.js')): void {
|
|
93
153
|
_useCookieModule = mod;
|
|
154
|
+
// Also register the client impl from the module
|
|
155
|
+
_clientImpl = {
|
|
156
|
+
useCookie: mod.useCookie,
|
|
157
|
+
getCookieValue: (name: string) => {
|
|
158
|
+
if (typeof document === 'undefined') return undefined;
|
|
159
|
+
const match = document.cookie.match(
|
|
160
|
+
new RegExp('(?:^|;\\s*)' + name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\s*=\\s*([^;]*)')
|
|
161
|
+
);
|
|
162
|
+
return match ? decodeURIComponent(match[1]) : undefined;
|
|
163
|
+
},
|
|
164
|
+
setCookieValue: (name: string, value: string, options?: ClientCookieOptions) => {
|
|
165
|
+
const parts: string[] = [`${name}=${encodeURIComponent(value)}`];
|
|
166
|
+
const path = options?.path ?? '/';
|
|
167
|
+
parts.push(`Path=${path}`);
|
|
168
|
+
if (options?.domain) parts.push(`Domain=${options.domain}`);
|
|
169
|
+
if (options?.maxAge !== undefined) parts.push(`Max-Age=${options.maxAge}`);
|
|
170
|
+
if (options?.expires) parts.push(`Expires=${options.expires.toUTCString()}`);
|
|
171
|
+
const sameSite = options?.sameSite ?? 'lax';
|
|
172
|
+
parts.push(`SameSite=${sameSite.charAt(0).toUpperCase()}${sameSite.slice(1)}`);
|
|
173
|
+
if (options?.secure) parts.push('Secure');
|
|
174
|
+
document.cookie = parts.join('; ');
|
|
175
|
+
// Notify useCookie subscribers so mounted hooks re-render
|
|
176
|
+
mod.notifyCookieChange(name);
|
|
177
|
+
},
|
|
178
|
+
deleteCookieValue: (name: string, options?: ClientCookieOptions) => {
|
|
179
|
+
const path = options?.path ?? '/';
|
|
180
|
+
const domain = options?.domain;
|
|
181
|
+
let cookieStr = `${name}=; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:00 GMT; Path=${path}`;
|
|
182
|
+
if (domain) cookieStr += `; Domain=${domain}`;
|
|
183
|
+
document.cookie = cookieStr;
|
|
184
|
+
mod.notifyCookieChange(name);
|
|
185
|
+
},
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ─── Standard Schema Auto-Detection ───────────────────────────────────────
|
|
190
|
+
|
|
191
|
+
function resolveCodec<T>(codecOrSchema: CookieCodec<T> | StandardSchemaV1<T>): CookieCodec<T> {
|
|
192
|
+
// If it has parse + serialize, it's already a Codec
|
|
193
|
+
if (
|
|
194
|
+
typeof (codecOrSchema as Codec<T>).parse === 'function' &&
|
|
195
|
+
typeof (codecOrSchema as Codec<T>).serialize === 'function'
|
|
196
|
+
) {
|
|
197
|
+
return codecOrSchema as CookieCodec<T>;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Auto-detect Standard Schema
|
|
201
|
+
if (typeof codecOrSchema === 'object' && codecOrSchema !== null && '~standard' in codecOrSchema) {
|
|
202
|
+
if (!_fromSchemaFn) {
|
|
203
|
+
throw new Error(
|
|
204
|
+
'[timber] defineCookie: Standard Schema auto-detection requires @timber-js/app/server or ' +
|
|
205
|
+
'@timber-js/app/client to be loaded. Pass an explicit Codec<T> with parse/serialize methods, ' +
|
|
206
|
+
'or ensure the framework entry module is imported.'
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
return _fromSchemaFn(codecOrSchema as StandardSchemaV1<unknown>) as CookieCodec<T>;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
throw new Error(
|
|
213
|
+
'[timber] defineCookie: codec must be a Codec<T> (with parse/serialize methods) ' +
|
|
214
|
+
'or a Standard Schema object (Zod, Valibot, ArkType).'
|
|
215
|
+
);
|
|
94
216
|
}
|
|
95
217
|
|
|
96
218
|
// ─── Factory ──────────────────────────────────────────────────────────────
|
|
@@ -100,78 +222,147 @@ export function _registerUseCookieModule(mod: typeof import('../client/use-cooki
|
|
|
100
222
|
*
|
|
101
223
|
* ```ts
|
|
102
224
|
* import { defineCookie } from '@timber-js/app/cookies';
|
|
103
|
-
* import { fromSchema } from '@timber-js/app/codec';
|
|
104
225
|
* import { z } from 'zod/v4';
|
|
105
226
|
*
|
|
227
|
+
* // httpOnly: false — client methods available
|
|
106
228
|
* export const themeCookie = defineCookie('theme', {
|
|
107
|
-
* codec:
|
|
229
|
+
* codec: z.enum(['light', 'dark', 'system']).default('system'),
|
|
108
230
|
* httpOnly: false,
|
|
109
231
|
* maxAge: 60 * 60 * 24 * 365,
|
|
110
232
|
* });
|
|
111
233
|
*
|
|
112
|
-
* // Server
|
|
113
|
-
* const theme =
|
|
114
|
-
*
|
|
234
|
+
* // Server or client
|
|
235
|
+
* const theme = themeCookie.get();
|
|
236
|
+
* themeCookie.set('dark');
|
|
115
237
|
*
|
|
116
|
-
* // Client
|
|
238
|
+
* // Client hook
|
|
117
239
|
* const [theme, setTheme] = themeCookie.useCookie();
|
|
240
|
+
*
|
|
241
|
+
* // httpOnly: true (default) — server-only, no client methods
|
|
242
|
+
* export const sessionCookie = defineCookie('session', {
|
|
243
|
+
* codec: z.string(),
|
|
244
|
+
* });
|
|
245
|
+
* sessionCookie.get(); // works on server
|
|
246
|
+
* sessionCookie.useCookie(); // TS error — httpOnly cookie
|
|
118
247
|
* ```
|
|
119
248
|
*/
|
|
120
|
-
export function defineCookie<T>(
|
|
249
|
+
export function defineCookie<T, HttpOnly extends boolean = true>(
|
|
121
250
|
name: string,
|
|
122
|
-
options: DefineCookieOptions<T>
|
|
123
|
-
): CookieDefinition<T> {
|
|
124
|
-
const { codec, ...cookieOpts } = options;
|
|
251
|
+
options: DefineCookieOptions<T, HttpOnly>
|
|
252
|
+
): HttpOnly extends false ? CookieDefinition<T> : ServerOnlyCookieDefinition<T> {
|
|
253
|
+
const { codec: codecOrSchema, ...cookieOpts } = options;
|
|
254
|
+
const codec = resolveCodec(codecOrSchema);
|
|
125
255
|
const resolvedOptions: CookieOptions = { ...cookieOpts };
|
|
256
|
+
const isHttpOnly = options.httpOnly !== false; // default true
|
|
257
|
+
|
|
258
|
+
function getClientOpts(): ClientCookieOptions {
|
|
259
|
+
return {
|
|
260
|
+
path: resolvedOptions.path,
|
|
261
|
+
domain: resolvedOptions.domain,
|
|
262
|
+
maxAge: resolvedOptions.maxAge,
|
|
263
|
+
expires: resolvedOptions.expires,
|
|
264
|
+
sameSite: resolvedOptions.sameSite,
|
|
265
|
+
secure: resolvedOptions.secure,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function assertNotHttpOnlyClient(method: string): void {
|
|
270
|
+
if (isHttpOnly && !_serverImpl) {
|
|
271
|
+
throw new Error(
|
|
272
|
+
`[timber] defineCookie('${name}').${method}() cannot be used with httpOnly cookies — ` +
|
|
273
|
+
`the browser cannot access them. Set httpOnly: false to enable client access.`
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
126
277
|
|
|
127
|
-
|
|
278
|
+
const base: ServerOnlyCookieDefinition<T> = {
|
|
128
279
|
name,
|
|
129
280
|
options: resolvedOptions,
|
|
130
281
|
codec,
|
|
131
282
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
283
|
+
get(): T {
|
|
284
|
+
if (_serverImpl) {
|
|
285
|
+
const jar = _serverImpl.getCookieJar();
|
|
286
|
+
const raw = jar.get(name);
|
|
287
|
+
return codec.parse(raw);
|
|
288
|
+
}
|
|
289
|
+
// Client path
|
|
290
|
+
assertNotHttpOnlyClient('get');
|
|
291
|
+
if (_clientImpl) {
|
|
292
|
+
const raw = _clientImpl.getCookieValue(name);
|
|
293
|
+
return codec.parse(raw);
|
|
294
|
+
}
|
|
295
|
+
throw new Error(
|
|
296
|
+
`[timber] defineCookie('${name}').get() — no environment registered. ` +
|
|
297
|
+
'Ensure @timber-js/app/server or @timber-js/app/client is imported.'
|
|
298
|
+
);
|
|
137
299
|
},
|
|
138
300
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
301
|
+
set(value: T): void {
|
|
302
|
+
if (_serverImpl) {
|
|
303
|
+
const jar = _serverImpl.getCookieJar();
|
|
304
|
+
const serialized = codec.serialize(value);
|
|
305
|
+
if (serialized === null) {
|
|
306
|
+
jar.delete(name, {
|
|
307
|
+
path: resolvedOptions.path,
|
|
308
|
+
domain: resolvedOptions.domain,
|
|
309
|
+
});
|
|
310
|
+
} else {
|
|
311
|
+
jar.set(name, serialized, resolvedOptions);
|
|
312
|
+
}
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
// Client path
|
|
316
|
+
assertNotHttpOnlyClient('set');
|
|
317
|
+
if (_clientImpl) {
|
|
318
|
+
const serialized = codec.serialize(value);
|
|
319
|
+
if (serialized === null) {
|
|
320
|
+
_clientImpl.deleteCookieValue(name, getClientOpts());
|
|
321
|
+
} else {
|
|
322
|
+
_clientImpl.setCookieValue(name, serialized, getClientOpts());
|
|
323
|
+
}
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
throw new Error(
|
|
327
|
+
`[timber] defineCookie('${name}').set() — no environment registered. ` +
|
|
328
|
+
'Ensure @timber-js/app/server or @timber-js/app/client is imported.'
|
|
329
|
+
);
|
|
330
|
+
},
|
|
331
|
+
|
|
332
|
+
delete(): void {
|
|
333
|
+
if (_serverImpl) {
|
|
334
|
+
const jar = _serverImpl.getCookieJar();
|
|
144
335
|
jar.delete(name, {
|
|
145
336
|
path: resolvedOptions.path,
|
|
146
337
|
domain: resolvedOptions.domain,
|
|
147
338
|
});
|
|
148
|
-
|
|
149
|
-
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
// Client path
|
|
342
|
+
assertNotHttpOnlyClient('delete');
|
|
343
|
+
if (_clientImpl) {
|
|
344
|
+
_clientImpl.deleteCookieValue(name, getClientOpts());
|
|
345
|
+
return;
|
|
150
346
|
}
|
|
347
|
+
throw new Error(
|
|
348
|
+
`[timber] defineCookie('${name}').delete() — no environment registered. ` +
|
|
349
|
+
'Ensure @timber-js/app/server or @timber-js/app/client is imported.'
|
|
350
|
+
);
|
|
151
351
|
},
|
|
352
|
+
};
|
|
152
353
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
},
|
|
354
|
+
if (isHttpOnly) {
|
|
355
|
+
return base as HttpOnly extends false ? CookieDefinition<T> : ServerOnlyCookieDefinition<T>;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// httpOnly: false — add useCookie client hook
|
|
359
|
+
const full: CookieDefinition<T> = {
|
|
360
|
+
...base,
|
|
161
361
|
|
|
162
362
|
useCookie(): [T, (value: T) => void, () => void] {
|
|
163
363
|
const { useCookie: useRawCookie } = getUseCookieModule();
|
|
164
364
|
|
|
165
|
-
|
|
166
|
-
const clientOpts: ClientCookieOptions = {
|
|
167
|
-
path: resolvedOptions.path,
|
|
168
|
-
domain: resolvedOptions.domain,
|
|
169
|
-
maxAge: resolvedOptions.maxAge,
|
|
170
|
-
expires: resolvedOptions.expires,
|
|
171
|
-
sameSite: resolvedOptions.sameSite,
|
|
172
|
-
secure: resolvedOptions.secure,
|
|
173
|
-
};
|
|
174
|
-
|
|
365
|
+
const clientOpts = getClientOpts();
|
|
175
366
|
const [raw, setRaw, deleteRaw] = useRawCookie(name, clientOpts);
|
|
176
367
|
const parsed = codec.parse(raw);
|
|
177
368
|
|
|
@@ -187,4 +378,6 @@ export function defineCookie<T>(
|
|
|
187
378
|
return [parsed, setTyped, deleteRaw];
|
|
188
379
|
},
|
|
189
380
|
};
|
|
381
|
+
|
|
382
|
+
return full as HttpOnly extends false ? CookieDefinition<T> : ServerOnlyCookieDefinition<T>;
|
|
190
383
|
}
|
package/src/cookies/index.ts
CHANGED
|
@@ -2,12 +2,15 @@
|
|
|
2
2
|
// See design/29-cookies.md §"Typed Cookies with Schema Validation"
|
|
3
3
|
|
|
4
4
|
export { defineCookie } from './define-cookie.js';
|
|
5
|
-
export type {
|
|
5
|
+
export type {
|
|
6
|
+
CookieDefinition,
|
|
7
|
+
ServerOnlyCookieDefinition,
|
|
8
|
+
CookieCodec,
|
|
9
|
+
DefineCookieOptions,
|
|
10
|
+
} from './define-cookie.js';
|
|
6
11
|
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
|
|
10
|
-
//
|
|
11
|
-
|
|
12
|
-
// here so that client-side code importing defineCookie doesn't pull in
|
|
13
|
-
// server ALS code. See TIM-704.
|
|
12
|
+
// JSON-in-cookie helper. Wraps any JSON-serializable value with URL-encoding
|
|
13
|
+
// so the stored form satisfies RFC 6265 §4.1.1 cookie-octet (the rule that
|
|
14
|
+
// the framework now enforces in `getCookieJar().set()` to close TIM-868).
|
|
15
|
+
// See ONGOING_SECURITY.md H-3 and design/29-cookies.md.
|
|
16
|
+
export { jsonCookieCodec } from './json-cookie.js';
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* jsonCookieCodec — Codec helper for storing JSON-serializable values in
|
|
3
|
+
* cookies.
|
|
4
|
+
*
|
|
5
|
+
* The codec is intentionally minimal: `JSON.stringify` on serialize,
|
|
6
|
+
* `JSON.parse` on parse. The framework handles the URL encoding /
|
|
7
|
+
* decoding around it (see design/29-cookies.md §"Encoding Contract"):
|
|
8
|
+
*
|
|
9
|
+
* defineCookie.setCookie(value)
|
|
10
|
+
* → codec.serialize: JSON.stringify → '{"a":1}'
|
|
11
|
+
* → cookies().set: encodeURIComponent → '%7B%22a%22%3A1%7D'
|
|
12
|
+
* → wire: Set-Cookie: prefs=%7B%22a%22%3A1%7D
|
|
13
|
+
*
|
|
14
|
+
* defineCookie.getCookie()
|
|
15
|
+
* → wire: Cookie: prefs=%7B%22a%22%3A1%7D
|
|
16
|
+
* → parseCookieHeader: decodeURIComponent → '{"a":1}'
|
|
17
|
+
* → codec.parse: JSON.parse → { a: 1 }
|
|
18
|
+
*
|
|
19
|
+
* The same chain works on the client: `useCookie` auto-encodes on writes
|
|
20
|
+
* and auto-decodes on reads, so `jsonCookieCodec` composes with both the
|
|
21
|
+
* server `defineCookie` methods and the client `useCookie` hook with no
|
|
22
|
+
* environment-specific branches.
|
|
23
|
+
*
|
|
24
|
+
* Parsing is total: any non-JSON value (or `undefined`) returns the
|
|
25
|
+
* supplied default. This matches the "never crash on user-controlled
|
|
26
|
+
* cookie input" semantics that `fromSchema` uses for typed search params.
|
|
27
|
+
*
|
|
28
|
+
* ```ts
|
|
29
|
+
* import { defineCookie, jsonCookieCodec } from '@timber-js/app/cookies';
|
|
30
|
+
*
|
|
31
|
+
* interface Prefs { lang: string; fontSize: number }
|
|
32
|
+
*
|
|
33
|
+
* export const prefsCookie = defineCookie('prefs', {
|
|
34
|
+
* codec: jsonCookieCodec<Prefs>({ lang: 'en', fontSize: 16 }),
|
|
35
|
+
* httpOnly: false,
|
|
36
|
+
* maxAge: 60 * 60 * 24 * 365,
|
|
37
|
+
* });
|
|
38
|
+
*
|
|
39
|
+
* await prefsCookie.setCookie({ lang: 'fr', fontSize: 18 });
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
import type { Codec } from '../codec.js';
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Build a CookieCodec that JSON-encodes the value. The framework's
|
|
47
|
+
* cookie write path then URL-encodes the JSON string into a valid
|
|
48
|
+
* `cookie-octet` form, and the read path reverses both transforms.
|
|
49
|
+
*
|
|
50
|
+
* @param defaultValue - Returned by `parse` when the cookie is missing
|
|
51
|
+
* or the stored value cannot be JSON-parsed. Optional — if omitted,
|
|
52
|
+
* `parse` returns `undefined` on missing/malformed input.
|
|
53
|
+
*
|
|
54
|
+
* The default is **deep-cloned** on every fallback return via
|
|
55
|
+
* `structuredClone`, so mutating the parsed result in one request
|
|
56
|
+
* cannot leak into later requests that fall back to the same
|
|
57
|
+
* default. This matters for long-lived server processes where a
|
|
58
|
+
* codec is defined once at module load and shared across all
|
|
59
|
+
* requests — without the clone, a user doing
|
|
60
|
+
* `const prefs = prefsCookie.get(); prefs.lang = 'fr'` on a missing
|
|
61
|
+
* cookie would mutate the module-level default and poison every
|
|
62
|
+
* later fallback. See the "shared mutable default" regression test
|
|
63
|
+
* in `tests/define-cookie.test.ts`.
|
|
64
|
+
*/
|
|
65
|
+
export function jsonCookieCodec<T>(defaultValue?: T): Codec<T> {
|
|
66
|
+
// Resolve the fallback at call time, not capture time, so every
|
|
67
|
+
// fallback return produces a fresh deep copy. structuredClone handles
|
|
68
|
+
// nested objects, arrays, dates, maps, sets — everything
|
|
69
|
+
// JSON-serializable plus more. For primitives and undefined,
|
|
70
|
+
// structuredClone is effectively a no-op.
|
|
71
|
+
const cloneDefault = (): T => {
|
|
72
|
+
if (defaultValue === undefined || defaultValue === null) {
|
|
73
|
+
return defaultValue as T;
|
|
74
|
+
}
|
|
75
|
+
// structuredClone is a Node built-in since Node 17, available in
|
|
76
|
+
// all supported runtimes.
|
|
77
|
+
return structuredClone(defaultValue);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
parse(value: string | string[] | undefined): T {
|
|
82
|
+
if (value === undefined || value === '') {
|
|
83
|
+
return cloneDefault();
|
|
84
|
+
}
|
|
85
|
+
// Cookies are single-valued by name; defensively pick the last
|
|
86
|
+
// entry if a Codec consumer somehow passes an array.
|
|
87
|
+
const raw = Array.isArray(value) ? value[value.length - 1] : value;
|
|
88
|
+
if (raw === undefined || raw === '') {
|
|
89
|
+
return cloneDefault();
|
|
90
|
+
}
|
|
91
|
+
try {
|
|
92
|
+
return JSON.parse(raw) as T;
|
|
93
|
+
} catch {
|
|
94
|
+
return cloneDefault();
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
serialize(value: T): string | null {
|
|
99
|
+
if (value === null || value === undefined) {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
return JSON.stringify(value);
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
}
|