@nifrajs/web 4.0.0 → 5.0.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 +11 -0
- package/dist/build-vite.d.ts +19 -0
- package/dist/build-vite.d.ts.map +1 -1
- package/dist/build-vite.js +31 -10
- package/dist/build-vite.js.map +1 -1
- package/dist/build.d.ts +8 -10
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +90 -31
- package/dist/build.js.map +1 -1
- package/dist/cdn.d.ts +8 -0
- package/dist/cdn.d.ts.map +1 -1
- package/dist/cdn.js +14 -15
- package/dist/cdn.js.map +1 -1
- package/dist/client.d.ts +1 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +49 -26
- package/dist/client.js.map +1 -1
- package/dist/deferred.d.ts +2 -2
- package/dist/deferred.d.ts.map +1 -1
- package/dist/deferred.js +28 -7
- package/dist/deferred.js.map +1 -1
- package/dist/dev-feed.d.ts.map +1 -1
- package/dist/dev-feed.js +24 -5
- package/dist/dev-feed.js.map +1 -1
- package/dist/dev-session.d.ts.map +1 -1
- package/dist/dev-session.js +4 -14
- package/dist/dev-session.js.map +1 -1
- package/dist/dev.d.ts +7 -17
- package/dist/dev.d.ts.map +1 -1
- package/dist/dev.js +13 -1
- package/dist/dev.js.map +1 -1
- package/dist/draft.d.ts +8 -5
- package/dist/draft.d.ts.map +1 -1
- package/dist/draft.js +30 -13
- package/dist/draft.js.map +1 -1
- package/dist/fonts-google.d.ts.map +1 -1
- package/dist/fonts-google.js +7 -0
- package/dist/fonts-google.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/internal/codegen.d.ts +1 -0
- package/dist/internal/codegen.d.ts.map +1 -1
- package/dist/internal/codegen.js +56 -29
- package/dist/internal/codegen.js.map +1 -1
- package/dist/internal/disposable-dir.d.ts +8 -0
- package/dist/internal/disposable-dir.d.ts.map +1 -0
- package/dist/internal/disposable-dir.js +25 -0
- package/dist/internal/disposable-dir.js.map +1 -0
- package/dist/internal/entry-chunks.d.ts +9 -0
- package/dist/internal/entry-chunks.d.ts.map +1 -0
- package/dist/internal/entry-chunks.js +15 -0
- package/dist/internal/entry-chunks.js.map +1 -0
- package/dist/internal/evidence-traversal.d.ts +12 -0
- package/dist/internal/evidence-traversal.d.ts.map +1 -0
- package/dist/internal/evidence-traversal.js +48 -0
- package/dist/internal/evidence-traversal.js.map +1 -0
- package/dist/internal/head-merge.d.ts +6 -0
- package/dist/internal/head-merge.d.ts.map +1 -1
- package/dist/internal/head-merge.js +9 -0
- package/dist/internal/head-merge.js.map +1 -1
- package/dist/internal/js-string.d.ts +2 -0
- package/dist/internal/js-string.d.ts.map +1 -1
- package/dist/internal/js-string.js +5 -1
- package/dist/internal/js-string.js.map +1 -1
- package/dist/internal/mount-shadow.js +1 -1
- package/dist/internal/mount-shadow.js.map +1 -1
- package/dist/internal/output-guard.d.ts +7 -0
- package/dist/internal/output-guard.d.ts.map +1 -1
- package/dist/internal/output-guard.js +80 -8
- package/dist/internal/output-guard.js.map +1 -1
- package/dist/internal/page-execution.d.ts +6 -0
- package/dist/internal/page-execution.d.ts.map +1 -1
- package/dist/internal/page-execution.js +377 -108
- package/dist/internal/page-execution.js.map +1 -1
- package/dist/internal/progressive-cookies.d.ts +14 -0
- package/dist/internal/progressive-cookies.d.ts.map +1 -0
- package/dist/internal/progressive-cookies.js +43 -0
- package/dist/internal/progressive-cookies.js.map +1 -0
- package/dist/internal/public-env.d.ts +2 -0
- package/dist/internal/public-env.d.ts.map +1 -0
- package/dist/internal/public-env.js +13 -0
- package/dist/internal/public-env.js.map +1 -0
- package/dist/internal/render-document.d.ts +1 -0
- package/dist/internal/render-document.d.ts.map +1 -1
- package/dist/internal/render-document.js +28 -5
- package/dist/internal/render-document.js.map +1 -1
- package/dist/internal/response-controls.d.ts +2 -0
- package/dist/internal/response-controls.d.ts.map +1 -1
- package/dist/internal/response-controls.js +26 -16
- package/dist/internal/response-controls.js.map +1 -1
- package/dist/internal/revive-dates.d.ts +16 -0
- package/dist/internal/revive-dates.d.ts.map +1 -0
- package/dist/internal/revive-dates.js +32 -0
- package/dist/internal/revive-dates.js.map +1 -0
- package/dist/internal/route-path.d.ts +8 -0
- package/dist/internal/route-path.d.ts.map +1 -0
- package/dist/internal/route-path.js +8 -0
- package/dist/internal/route-path.js.map +1 -0
- package/dist/internal/secret-scan.d.ts.map +1 -1
- package/dist/internal/secret-scan.js +105 -19
- package/dist/internal/secret-scan.js.map +1 -1
- package/dist/internal/server-boundary.d.ts.map +1 -1
- package/dist/internal/server-boundary.js +33 -5
- package/dist/internal/server-boundary.js.map +1 -1
- package/dist/internal/web-app.d.ts +21 -16
- package/dist/internal/web-app.d.ts.map +1 -1
- package/dist/internal/web-app.js +73 -21
- package/dist/internal/web-app.js.map +1 -1
- package/dist/internal/wire-date.d.ts +3 -0
- package/dist/internal/wire-date.d.ts.map +1 -0
- package/dist/internal/wire-date.js +14 -0
- package/dist/internal/wire-date.js.map +1 -0
- package/dist/internal/zone-graph.d.ts +4 -0
- package/dist/internal/zone-graph.d.ts.map +1 -1
- package/dist/internal/zone-graph.js +54 -14
- package/dist/internal/zone-graph.js.map +1 -1
- package/dist/isr.d.ts +58 -2
- package/dist/isr.d.ts.map +1 -1
- package/dist/isr.js +296 -39
- package/dist/isr.js.map +1 -1
- package/dist/llms-txt.d.ts +2 -0
- package/dist/llms-txt.d.ts.map +1 -1
- package/dist/llms-txt.js +29 -3
- package/dist/llms-txt.js.map +1 -1
- package/dist/manifest.d.ts +28 -4
- package/dist/manifest.d.ts.map +1 -1
- package/dist/manifest.js +14 -3
- package/dist/manifest.js.map +1 -1
- package/dist/open-resolved.d.ts +18 -0
- package/dist/open-resolved.d.ts.map +1 -0
- package/dist/open-resolved.js +62 -0
- package/dist/open-resolved.js.map +1 -0
- package/dist/plugins/kit.d.ts +1 -0
- package/dist/plugins/kit.d.ts.map +1 -1
- package/dist/plugins/kit.js +26 -11
- package/dist/plugins/kit.js.map +1 -1
- package/dist/plugins/vite-leak-guard.d.ts +3 -1
- package/dist/plugins/vite-leak-guard.d.ts.map +1 -1
- package/dist/plugins/vite-leak-guard.js +4 -0
- package/dist/plugins/vite-leak-guard.js.map +1 -1
- package/dist/plugins/zone-guard.d.ts.map +1 -1
- package/dist/plugins/zone-guard.js +10 -2
- package/dist/plugins/zone-guard.js.map +1 -1
- package/dist/public-dir.d.ts +8 -1
- package/dist/public-dir.d.ts.map +1 -1
- package/dist/public-dir.js +17 -6
- package/dist/public-dir.js.map +1 -1
- package/dist/query.d.ts +13 -4
- package/dist/query.d.ts.map +1 -1
- package/dist/query.js +89 -26
- package/dist/query.js.map +1 -1
- package/dist/render-seam.d.ts +2 -0
- package/dist/render-seam.d.ts.map +1 -1
- package/dist/render-seam.js +1 -0
- package/dist/render-seam.js.map +1 -1
- package/dist/route-types.d.ts.map +1 -1
- package/dist/route-types.js +20 -9
- package/dist/route-types.js.map +1 -1
- package/dist/router.d.ts +9 -2
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +39 -46
- package/dist/router.js.map +1 -1
- package/dist/server-components/build-info.d.ts +3 -0
- package/dist/server-components/build-info.d.ts.map +1 -0
- package/dist/server-components/build-info.js +7 -0
- package/dist/server-components/build-info.js.map +1 -0
- package/dist/server-components/build-target.d.ts +12 -0
- package/dist/server-components/build-target.d.ts.map +1 -0
- package/dist/server-components/build-target.js +153 -0
- package/dist/server-components/build-target.js.map +1 -0
- package/dist/server-components/build.d.ts +24 -0
- package/dist/server-components/build.d.ts.map +1 -0
- package/dist/server-components/build.js +366 -0
- package/dist/server-components/build.js.map +1 -0
- package/dist/server-components/bundle-proof.d.ts +3 -0
- package/dist/server-components/bundle-proof.d.ts.map +1 -0
- package/dist/server-components/bundle-proof.js +23 -0
- package/dist/server-components/bundle-proof.js.map +1 -0
- package/dist/server-components/client.d.ts +46 -0
- package/dist/server-components/client.d.ts.map +1 -0
- package/dist/server-components/client.js +594 -0
- package/dist/server-components/client.js.map +1 -0
- package/dist/server-components/definition.d.ts +11 -0
- package/dist/server-components/definition.d.ts.map +1 -0
- package/dist/server-components/definition.js +94 -0
- package/dist/server-components/definition.js.map +1 -0
- package/dist/server-components/dev-http.d.ts +5 -0
- package/dist/server-components/dev-http.d.ts.map +1 -0
- package/dist/server-components/dev-http.js +71 -0
- package/dist/server-components/dev-http.js.map +1 -0
- package/dist/server-components/error.d.ts +9 -0
- package/dist/server-components/error.d.ts.map +1 -0
- package/dist/server-components/error.js +24 -0
- package/dist/server-components/error.js.map +1 -0
- package/dist/server-components/frames.d.ts +7 -0
- package/dist/server-components/frames.d.ts.map +1 -0
- package/dist/server-components/frames.js +169 -0
- package/dist/server-components/frames.js.map +1 -0
- package/dist/server-components/html-policy.d.ts +6 -0
- package/dist/server-components/html-policy.d.ts.map +1 -0
- package/dist/server-components/html-policy.js +75 -0
- package/dist/server-components/html-policy.js.map +1 -0
- package/dist/server-components/html.d.ts +4 -0
- package/dist/server-components/html.d.ts.map +1 -0
- package/dist/server-components/html.js +67 -0
- package/dist/server-components/html.js.map +1 -0
- package/dist/server-components/islands.d.ts +44 -0
- package/dist/server-components/islands.d.ts.map +1 -0
- package/dist/server-components/islands.js +42 -0
- package/dist/server-components/islands.js.map +1 -0
- package/dist/server-components/native.d.ts +4 -0
- package/dist/server-components/native.d.ts.map +1 -0
- package/dist/server-components/native.js +81 -0
- package/dist/server-components/native.js.map +1 -0
- package/dist/server-components/portal.d.ts +6 -0
- package/dist/server-components/portal.d.ts.map +1 -0
- package/dist/server-components/portal.js +35 -0
- package/dist/server-components/portal.js.map +1 -0
- package/dist/server-components/props.d.ts +3 -0
- package/dist/server-components/props.d.ts.map +1 -0
- package/dist/server-components/props.js +14 -0
- package/dist/server-components/props.js.map +1 -0
- package/dist/server-components/protocol.d.ts +44 -0
- package/dist/server-components/protocol.d.ts.map +1 -0
- package/dist/server-components/protocol.js +139 -0
- package/dist/server-components/protocol.js.map +1 -0
- package/dist/server-components/range.d.ts +24 -0
- package/dist/server-components/range.d.ts.map +1 -0
- package/dist/server-components/range.js +263 -0
- package/dist/server-components/range.js.map +1 -0
- package/dist/server-components/registry.d.ts +6 -0
- package/dist/server-components/registry.d.ts.map +1 -0
- package/dist/server-components/registry.js +30 -0
- package/dist/server-components/registry.js.map +1 -0
- package/dist/server-components/response.d.ts +4 -0
- package/dist/server-components/response.d.ts.map +1 -0
- package/dist/server-components/response.js +32 -0
- package/dist/server-components/response.js.map +1 -0
- package/dist/server-components/router-integration.d.ts +9 -0
- package/dist/server-components/router-integration.d.ts.map +1 -0
- package/dist/server-components/router-integration.js +29 -0
- package/dist/server-components/router-integration.js.map +1 -0
- package/dist/server-components/runtime.d.ts +14 -0
- package/dist/server-components/runtime.d.ts.map +1 -0
- package/dist/server-components/runtime.js +276 -0
- package/dist/server-components/runtime.js.map +1 -0
- package/dist/server-components/scheduler.d.ts +14 -0
- package/dist/server-components/scheduler.d.ts.map +1 -0
- package/dist/server-components/scheduler.js +74 -0
- package/dist/server-components/scheduler.js.map +1 -0
- package/dist/server-components/schema.d.ts +4 -0
- package/dist/server-components/schema.d.ts.map +1 -0
- package/dist/server-components/schema.js +77 -0
- package/dist/server-components/schema.js.map +1 -0
- package/dist/server-components/server-entry.d.ts +12 -0
- package/dist/server-components/server-entry.d.ts.map +1 -0
- package/dist/server-components/server-entry.js +90 -0
- package/dist/server-components/server-entry.js.map +1 -0
- package/dist/server-components/source.d.ts +15 -0
- package/dist/server-components/source.d.ts.map +1 -0
- package/dist/server-components/source.js +269 -0
- package/dist/server-components/source.js.map +1 -0
- package/dist/server-components/state.d.ts +4 -0
- package/dist/server-components/state.d.ts.map +1 -0
- package/dist/server-components/state.js +123 -0
- package/dist/server-components/state.js.map +1 -0
- package/dist/server-components/stream.d.ts +6 -0
- package/dist/server-components/stream.d.ts.map +1 -0
- package/dist/server-components/stream.js +70 -0
- package/dist/server-components/stream.js.map +1 -0
- package/dist/server-components/trusted-html.d.ts +3 -0
- package/dist/server-components/trusted-html.d.ts.map +1 -0
- package/dist/server-components/trusted-html.js +14 -0
- package/dist/server-components/trusted-html.js.map +1 -0
- package/dist/server-components/types.d.ts +107 -0
- package/dist/server-components/types.d.ts.map +1 -0
- package/dist/server-components/types.js +2 -0
- package/dist/server-components/types.js.map +1 -0
- package/dist/server-components/typescript.d.ts +11 -0
- package/dist/server-components/typescript.d.ts.map +1 -0
- package/dist/server-components/typescript.js +28 -0
- package/dist/server-components/typescript.js.map +1 -0
- package/dist/server-components/value.d.ts +5 -0
- package/dist/server-components/value.d.ts.map +1 -0
- package/dist/server-components/value.js +77 -0
- package/dist/server-components/value.js.map +1 -0
- package/dist/server-components/vite.d.ts +12 -0
- package/dist/server-components/vite.d.ts.map +1 -0
- package/dist/server-components/vite.js +197 -0
- package/dist/server-components/vite.js.map +1 -0
- package/dist/server-components.d.ts +5 -0
- package/dist/server-components.d.ts.map +1 -0
- package/dist/server-components.js +4 -0
- package/dist/server-components.js.map +1 -0
- package/dist/service-worker.d.ts +8 -1
- package/dist/service-worker.d.ts.map +1 -1
- package/dist/service-worker.js +20 -6
- package/dist/service-worker.js.map +1 -1
- package/dist/vite.d.ts +12 -0
- package/dist/vite.d.ts.map +1 -1
- package/dist/vite.js +77 -17
- package/dist/vite.js.map +1 -1
- package/dist/zones.d.ts +40 -0
- package/dist/zones.d.ts.map +1 -1
- package/dist/zones.js +170 -6
- package/dist/zones.js.map +1 -1
- package/package.json +97 -4
- package/src/build-vite.ts +31 -9
- package/src/build.ts +109 -31
- package/src/cdn.ts +18 -12
- package/src/client.ts +48 -23
- package/src/deferred.ts +27 -9
- package/src/dev-feed.ts +24 -5
- package/src/dev-session.ts +4 -13
- package/src/dev.ts +29 -1
- package/src/draft.ts +35 -14
- package/src/fonts-google.ts +10 -0
- package/src/index.ts +3 -2
- package/src/internal/codegen.ts +60 -29
- package/src/internal/disposable-dir.ts +24 -0
- package/src/internal/entry-chunks.ts +18 -0
- package/src/internal/evidence-traversal.ts +57 -0
- package/src/internal/head-merge.ts +16 -0
- package/src/internal/js-string.ts +6 -1
- package/src/internal/mount-shadow.ts +1 -1
- package/src/internal/output-guard.ts +88 -7
- package/src/internal/page-execution.ts +506 -108
- package/src/internal/progressive-cookies.ts +60 -0
- package/src/internal/public-env.ts +14 -0
- package/src/internal/render-document.ts +26 -11
- package/src/internal/response-controls.ts +31 -14
- package/src/internal/revive-dates.ts +35 -0
- package/src/internal/route-path.ts +8 -0
- package/src/internal/secret-scan.ts +104 -17
- package/src/internal/server-boundary.ts +29 -5
- package/src/internal/web-app.ts +116 -37
- package/src/internal/wire-date.ts +16 -0
- package/src/internal/zone-graph.ts +59 -19
- package/src/isr.ts +351 -43
- package/src/llms-txt.ts +32 -3
- package/src/manifest.ts +43 -7
- package/src/open-resolved.ts +64 -0
- package/src/plugins/kit.ts +31 -11
- package/src/plugins/vite-leak-guard.ts +5 -1
- package/src/plugins/zone-guard.ts +11 -2
- package/src/public-dir.ts +27 -8
- package/src/query.ts +107 -26
- package/src/render-seam.ts +2 -0
- package/src/route-types.ts +34 -7
- package/src/router.ts +58 -55
- package/src/server-components/build-info.ts +11 -0
- package/src/server-components/build-target.ts +194 -0
- package/src/server-components/build.ts +419 -0
- package/src/server-components/bundle-proof.ts +33 -0
- package/src/server-components/client.ts +663 -0
- package/src/server-components/definition.ts +125 -0
- package/src/server-components/dev-http.ts +75 -0
- package/src/server-components/error.ts +24 -0
- package/src/server-components/frames.ts +181 -0
- package/src/server-components/html-policy.ts +94 -0
- package/src/server-components/html.ts +81 -0
- package/src/server-components/islands.ts +111 -0
- package/src/server-components/native.ts +101 -0
- package/src/server-components/portal.ts +42 -0
- package/src/server-components/props.ts +19 -0
- package/src/server-components/protocol.ts +167 -0
- package/src/server-components/range.ts +313 -0
- package/src/server-components/registry.ts +34 -0
- package/src/server-components/response.ts +44 -0
- package/src/server-components/router-integration.ts +40 -0
- package/src/server-components/runtime.ts +376 -0
- package/src/server-components/scheduler.ts +84 -0
- package/src/server-components/schema.ts +69 -0
- package/src/server-components/server-entry.ts +98 -0
- package/src/server-components/source.ts +324 -0
- package/src/server-components/state.ts +143 -0
- package/src/server-components/stream.ts +71 -0
- package/src/server-components/trusted-html.ts +21 -0
- package/src/server-components/types.ts +138 -0
- package/src/server-components/typescript.ts +39 -0
- package/src/server-components/value.ts +82 -0
- package/src/server-components/vite.ts +242 -0
- package/src/server-components.ts +32 -0
- package/src/service-worker.ts +24 -7
- package/src/vite.ts +68 -17
- package/src/zones.ts +208 -7
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { CookieOptions } from "@nifrajs/core/server"
|
|
2
|
+
|
|
3
|
+
export interface ProgressiveCookie {
|
|
4
|
+
readonly name: string
|
|
5
|
+
readonly value: string
|
|
6
|
+
readonly options?: CookieOptions | undefined
|
|
7
|
+
readonly deleted?: boolean
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Parent-domain acceptance belongs to the browser's cookie jar and public-suffix policy. A Secure
|
|
11
|
+
* cookie on a request seen as `http:` does too: behind a TLS terminator only the browser knows its
|
|
12
|
+
* scheme, so it decides whether the cookie is kept. */
|
|
13
|
+
export function progressiveCookieRedirect(
|
|
14
|
+
request: Request,
|
|
15
|
+
changes: readonly ProgressiveCookie[],
|
|
16
|
+
): boolean {
|
|
17
|
+
const url = new URL(request.url)
|
|
18
|
+
const host = url.hostname.toLowerCase()
|
|
19
|
+
return changes.some((change) => {
|
|
20
|
+
const domain = change.options?.domain?.replace(/^\./, "").toLowerCase()
|
|
21
|
+
if (domain !== undefined && domain !== host && host.endsWith("." + domain)) return true
|
|
22
|
+
return secureOnPlainHttp(url, change)
|
|
23
|
+
})
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const secureOnPlainHttp = (url: URL, change: ProgressiveCookie): boolean =>
|
|
27
|
+
(change.options?.secure ?? !change.deleted) && url.protocol !== "https:"
|
|
28
|
+
|
|
29
|
+
/** Reauthorize the internal GET with action cookies that are valid for this exact owner URL. */
|
|
30
|
+
export function progressiveCookieHeader(
|
|
31
|
+
request: Request,
|
|
32
|
+
changes: readonly ProgressiveCookie[],
|
|
33
|
+
): string {
|
|
34
|
+
const url = new URL(request.url)
|
|
35
|
+
let pairs = (request.headers.get("cookie") ?? "")
|
|
36
|
+
.split(";")
|
|
37
|
+
.map((part) => part.trim())
|
|
38
|
+
.filter(Boolean)
|
|
39
|
+
for (const change of changes) {
|
|
40
|
+
const options = change.options
|
|
41
|
+
const domain = options?.domain?.replace(/^\./, "").toLowerCase()
|
|
42
|
+
// Parent-domain cookies need a browser cookie jar/public-suffix decision; never synthesize them.
|
|
43
|
+
if (domain !== undefined && domain !== url.hostname.toLowerCase()) continue
|
|
44
|
+
if (secureOnPlainHttp(url, change)) continue
|
|
45
|
+
const path = options?.path ?? "/"
|
|
46
|
+
if (
|
|
47
|
+
url.pathname !== path &&
|
|
48
|
+
!(url.pathname.startsWith(path) && (path.endsWith("/") || url.pathname[path.length] === "/"))
|
|
49
|
+
)
|
|
50
|
+
continue
|
|
51
|
+
pairs = pairs.filter((pair) => pair.slice(0, pair.indexOf("=")).trim() !== change.name)
|
|
52
|
+
const expired =
|
|
53
|
+
options?.maxAge !== undefined
|
|
54
|
+
? options.maxAge <= 0
|
|
55
|
+
: options?.expires !== undefined && options.expires.getTime() <= Date.now()
|
|
56
|
+
if (!change.deleted && !expired)
|
|
57
|
+
pairs.push(change.name + "=" + encodeURIComponent(change.value))
|
|
58
|
+
}
|
|
59
|
+
return pairs.join("; ")
|
|
60
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export function publicEnvDefines(
|
|
2
|
+
prefix: string,
|
|
3
|
+
env: Readonly<Record<string, string | undefined>>,
|
|
4
|
+
): Record<string, string> {
|
|
5
|
+
const defines: Record<string, string> = {}
|
|
6
|
+
if (prefix === "") return defines // opt-out: bake in nothing
|
|
7
|
+
for (const [name, value] of Object.entries(env)) {
|
|
8
|
+
// Skip `undefined` (a deleted/unset key can still enumerate) so we never bake in `"undefined"`.
|
|
9
|
+
if (name.startsWith(prefix) && value !== undefined) {
|
|
10
|
+
defines[`process.env.${name}`] = JSON.stringify(value)
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
return defines
|
|
14
|
+
}
|
|
@@ -76,6 +76,7 @@ export interface RenderedPage {
|
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
export interface RenderPageOptions {
|
|
79
|
+
readonly serverComponents?: import("../server-components/types.ts").ComponentState
|
|
79
80
|
readonly adapter: RenderAdapter
|
|
80
81
|
/** The layout chain to render - outermost layout → page (opaque; the adapter renders it). */
|
|
81
82
|
readonly chain: readonly unknown[]
|
|
@@ -343,6 +344,10 @@ export function renderPageResult(options: RenderPageInput): MaybePromise<Rendere
|
|
|
343
344
|
boundarySplit === undefined
|
|
344
345
|
? ""
|
|
345
346
|
: `,"${BOUNDARY_GLOBAL}":${serializeData(boundarySplit.forClient)}`
|
|
347
|
+
const componentTail =
|
|
348
|
+
options.serverComponents === undefined
|
|
349
|
+
? ""
|
|
350
|
+
: `,"__NIFRA_COMPONENTS__":${serializeData(options.serverComponents)}`
|
|
346
351
|
const deferredRuntime =
|
|
347
352
|
allDeferred.length > 0 ? `<script${nonceAttr}>${DEFERRED_RUNTIME}</script>` : ""
|
|
348
353
|
// The regex only injects the CSP nonce into the (constant) hydration head; with no nonce it's a
|
|
@@ -443,7 +448,7 @@ export function renderPageResult(options: RenderPageInput): MaybePromise<Rendere
|
|
|
443
448
|
// Tail - the page-state handover + the client module. Module scripts defer (run after parse), so
|
|
444
449
|
// the handover + every streamed deferred resolution are in the document before the entry hydrates.
|
|
445
450
|
const tailHtml = hydrate
|
|
446
|
-
? `${slot.tailPre}${serializeData(forClient)}${action}${layoutTail}${boundaryTail}${slot.tailPost}`
|
|
451
|
+
? `${slot.tailPre}${serializeData(forClient)}${action}${layoutTail}${boundaryTail}${componentTail}${slot.tailPost}`
|
|
447
452
|
: (slot.tailPost as string)
|
|
448
453
|
const headers: Record<string, string> = { "content-type": "text/html; charset=utf-8" }
|
|
449
454
|
// Caller-supplied headers (a terminal status page's `cache-control`, say). Applied before the
|
|
@@ -469,6 +474,9 @@ export function renderPageResult(options: RenderPageInput): MaybePromise<Rendere
|
|
|
469
474
|
if (policy !== undefined) headers["content-security-policy"] = policy
|
|
470
475
|
}
|
|
471
476
|
const renderProps: RenderProps = {
|
|
477
|
+
...(options.serverComponents === undefined
|
|
478
|
+
? {}
|
|
479
|
+
: { serverComponents: options.serverComponents }),
|
|
472
480
|
data: forComponent,
|
|
473
481
|
actionData: actionSplit?.forComponent,
|
|
474
482
|
// `params`/`path` thread the matched route + URL to the adapter's `useParams`/`useLocation` so they
|
|
@@ -616,13 +624,17 @@ function streamDocument(
|
|
|
616
624
|
enc: TextEncoder,
|
|
617
625
|
nonceAttr: string,
|
|
618
626
|
): ReadableStream<Uint8Array> {
|
|
627
|
+
const reader = stream.getReader()
|
|
628
|
+
// A client that goes away cancels the body: the renderer stops, and nothing more is written.
|
|
629
|
+
let cancelled = false
|
|
630
|
+
let released = false
|
|
619
631
|
return new ReadableStream({
|
|
620
632
|
async start(controller) {
|
|
621
|
-
const reader = stream.getReader()
|
|
622
633
|
try {
|
|
623
634
|
controller.enqueue(shell)
|
|
624
635
|
for (;;) {
|
|
625
636
|
const { done, value } = await reader.read()
|
|
637
|
+
if (cancelled) return
|
|
626
638
|
if (done) break
|
|
627
639
|
controller.enqueue(value)
|
|
628
640
|
}
|
|
@@ -631,34 +643,37 @@ function streamDocument(
|
|
|
631
643
|
// defer() must not block a faster one; each script self-addresses by id, so order is irrelevant.
|
|
632
644
|
await Promise.all(
|
|
633
645
|
deferred.map(async (d) => {
|
|
646
|
+
let script: string
|
|
634
647
|
try {
|
|
635
648
|
const value = serializeData(await d.promise)
|
|
636
649
|
// `serializeData` JSON-serializes the value and escapes `<`, `>`, and Unicode line
|
|
637
650
|
// separators before this framework-owned script is emitted.
|
|
638
|
-
|
|
639
|
-
enc.encode(`<script${nonceAttr}>window.__nifraResolve(${d.id},${value})</script>`), // nosemgrep: javascript.lang.security.audit.unknown-value-with-script-tag.unknown-value-with-script-tag
|
|
640
|
-
)
|
|
651
|
+
script = `<script${nonceAttr}>window.__nifraResolve(${d.id},${value})</script>` // nosemgrep: javascript.lang.security.audit.unknown-value-with-script-tag.unknown-value-with-script-tag
|
|
641
652
|
} catch (err) {
|
|
642
653
|
// A rejected deferred streams __nifraReject (the client `<Await>` surfaces it) - it must
|
|
643
654
|
// not break the whole body. Redact: stream a stable opaque code, never the raw error
|
|
644
655
|
// text; log the real reason server-side.
|
|
645
656
|
console.error("[nifra/web] deferred value rejected:", err)
|
|
646
|
-
|
|
647
|
-
enc.encode(
|
|
648
|
-
`<script${nonceAttr}>window.__nifraReject(${d.id},${serializeData(DEFERRED_ERROR_CODE)})</script>`,
|
|
649
|
-
),
|
|
650
|
-
)
|
|
657
|
+
script = `<script${nonceAttr}>window.__nifraReject(${d.id},${serializeData(DEFERRED_ERROR_CODE)})</script>`
|
|
651
658
|
}
|
|
659
|
+
if (!cancelled) controller.enqueue(enc.encode(script))
|
|
652
660
|
}),
|
|
653
661
|
)
|
|
662
|
+
if (cancelled) return
|
|
654
663
|
controller.enqueue(tail)
|
|
655
664
|
controller.close()
|
|
656
665
|
} catch (err) {
|
|
657
|
-
controller.error(err)
|
|
666
|
+
if (!cancelled) controller.error(err)
|
|
658
667
|
} finally {
|
|
668
|
+
released = true
|
|
659
669
|
reader.releaseLock()
|
|
660
670
|
}
|
|
661
671
|
},
|
|
672
|
+
cancel(reason) {
|
|
673
|
+
cancelled = true
|
|
674
|
+
// Once the renderer is drained the reader is released, and there is nothing left to stop.
|
|
675
|
+
return released ? undefined : reader.cancel(reason)
|
|
676
|
+
},
|
|
662
677
|
})
|
|
663
678
|
}
|
|
664
679
|
|
|
@@ -2,9 +2,15 @@ import {
|
|
|
2
2
|
type CookieOptions,
|
|
3
3
|
type ResponseResult,
|
|
4
4
|
status as statusResult,
|
|
5
|
+
writableResponse,
|
|
5
6
|
} from "@nifrajs/core/server"
|
|
6
7
|
import type { LoaderResponseControls } from "../manifest.ts"
|
|
7
8
|
import { DATA_HEADER } from "../router.ts"
|
|
9
|
+
import {
|
|
10
|
+
type ProgressiveCookie,
|
|
11
|
+
progressiveCookieHeader,
|
|
12
|
+
progressiveCookieRedirect,
|
|
13
|
+
} from "./progressive-cookies.ts"
|
|
8
14
|
import { isResponseResult } from "./render-document.ts"
|
|
9
15
|
|
|
10
16
|
/** The serving app's response controls - the structural subset of a route handler's `c.set` used here. */
|
|
@@ -107,19 +113,9 @@ export function privateOutcome(outcome: unknown): unknown {
|
|
|
107
113
|
|
|
108
114
|
function privateResponse(response: Response): Response {
|
|
109
115
|
if (response.headers.get(CACHE_CONTROL) === PRIVATE_NO_STORE) return response
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
} catch {
|
|
114
|
-
// Immutable headers (`Response.redirect()`, a `fetch()` result): answer with a copy.
|
|
115
|
-
const headers = new Headers(response.headers)
|
|
116
|
-
headers.set(CACHE_CONTROL, PRIVATE_NO_STORE)
|
|
117
|
-
return new Response(response.body, {
|
|
118
|
-
status: response.status,
|
|
119
|
-
statusText: response.statusText,
|
|
120
|
-
headers,
|
|
121
|
-
})
|
|
122
|
-
}
|
|
116
|
+
const target = writableResponse(response)
|
|
117
|
+
target.headers.set(CACHE_CONTROL, PRIVATE_NO_STORE)
|
|
118
|
+
return target
|
|
123
119
|
}
|
|
124
120
|
|
|
125
121
|
interface WrittenHeaders {
|
|
@@ -140,6 +136,7 @@ export class PageResponseControls {
|
|
|
140
136
|
#written: WrittenHeaders[] | undefined
|
|
141
137
|
#sealed = false
|
|
142
138
|
#personalized = false
|
|
139
|
+
#cookieChanges: ProgressiveCookie[] | undefined
|
|
143
140
|
|
|
144
141
|
constructor(c: { readonly set: CoreResponseControls }) {
|
|
145
142
|
this.#c = c
|
|
@@ -167,7 +164,27 @@ export class PageResponseControls {
|
|
|
167
164
|
cookies(member: string): CoreResponseControls {
|
|
168
165
|
this.assertOpen(member)
|
|
169
166
|
this.personalize()
|
|
170
|
-
|
|
167
|
+
const controls = this.#c.set
|
|
168
|
+
return {
|
|
169
|
+
headers: controls.headers,
|
|
170
|
+
cookie: (name, value, options) => {
|
|
171
|
+
controls.cookie(name, value, options)
|
|
172
|
+
this.#cookieChanges ??= []
|
|
173
|
+
this.#cookieChanges.push({ name, value, options })
|
|
174
|
+
},
|
|
175
|
+
deleteCookie: (name, options) => {
|
|
176
|
+
controls.deleteCookie(name, options)
|
|
177
|
+
this.#cookieChanges ??= []
|
|
178
|
+
this.#cookieChanges.push({ name, value: "", options, deleted: true })
|
|
179
|
+
},
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
progressiveCookies(request: Request): string {
|
|
184
|
+
return progressiveCookieHeader(request, this.#cookieChanges ?? [])
|
|
185
|
+
}
|
|
186
|
+
progressiveRedirect(request: Request): boolean {
|
|
187
|
+
return progressiveCookieRedirect(request, this.#cookieChanges ?? [])
|
|
171
188
|
}
|
|
172
189
|
|
|
173
190
|
/** Mark the response as one visitor's - a queued cookie, or a draft render - so no cache stores it. */
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `t.date()` across the wire, browser half: every place the browser parses page data rebuilds the
|
|
3
|
+
* `{ "__nifra_date": iso }` tags the server's `wireDate()` wrote into `Date`s. Kept apart from
|
|
4
|
+
* `wire-date.ts` so client bundles never carry the server class.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export const DATE_TAG = "__nifra_date"
|
|
8
|
+
const TAGGED = `"${DATE_TAG}"`
|
|
9
|
+
|
|
10
|
+
// A JSON.parse reviver runs leaves first and defines each result as an own property, so a key named
|
|
11
|
+
// `__proto__` never becomes the prototype.
|
|
12
|
+
const revive = (_key: string, value: unknown): unknown => {
|
|
13
|
+
if (typeof value !== "object" || value === null) return value
|
|
14
|
+
const tagged: unknown = Reflect.get(value, DATE_TAG)
|
|
15
|
+
return typeof tagged === "string" && Object.keys(value).length === 1 ? new Date(tagged) : value
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** `JSON.parse` that rebuilds every `{ "__nifra_date": iso }` into a `Date`, when the text has one. */
|
|
19
|
+
export const parsePageData = (text: string): unknown =>
|
|
20
|
+
text.includes(TAGGED) ? JSON.parse(text, revive) : JSON.parse(text)
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* {@link parsePageData}'s reviver as source declaring `__nifraDates`, for client code nifra generates rather than
|
|
24
|
+
* bundles (server-function stubs; `DEFERRED_RUNTIME` carries the same text). A literal with nothing
|
|
25
|
+
* interpolated: Bun keeps a template that interpolates an import, and with it every string built from
|
|
26
|
+
* it, in client bundles that never use them.
|
|
27
|
+
*/
|
|
28
|
+
export const REVIVE_DATES_SOURCE = `const __nifraDates = (d) => {
|
|
29
|
+
if (!d || typeof d !== "object") return d
|
|
30
|
+
if (Array.isArray(d)) { for (let i = 0; i < d.length; i++) d[i] = __nifraDates(d[i]); return d }
|
|
31
|
+
const k = Object.keys(d)
|
|
32
|
+
if (k.length === 1 && k[0] === "__nifra_date" && typeof d.__nifra_date === "string") return new Date(d.__nifra_date)
|
|
33
|
+
for (const x of k) { const v = __nifraDates(d[x]); if (v !== d[x]) Object.defineProperty(d, x, { value: v, enumerable: true, writable: true, configurable: true }) }
|
|
34
|
+
return d
|
|
35
|
+
}`
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A route pattern's literal text as a request path spells it. A route file named `café.tsx` is
|
|
3
|
+
* requested as `/caf%C3%A9`, so the routers, which compare the raw request path, are given the pattern
|
|
4
|
+
* that way. Only controls, space and non-ASCII are encoded (what a browser encodes in a path), so ASCII
|
|
5
|
+
* punctuation such as a param constraint keeps its meaning. Runs once per pattern, never per request.
|
|
6
|
+
*/
|
|
7
|
+
export const requestPattern = (pattern: string): string =>
|
|
8
|
+
pattern.normalize("NFC").replace(/[^!-~]/gu, encodeURIComponent)
|
|
@@ -148,15 +148,54 @@ const JWT = /\beyJ[A-Za-z0-9_-]{8,}\.(eyJ[A-Za-z0-9_-]{8,})\.[A-Za-z0-9_-]{16,}/
|
|
|
148
148
|
const PLACEHOLDER =
|
|
149
149
|
/^(?:user(?:name)?|pass(?:word)?|pwd|secret|admin|root|guest|test|demo|example|changeme|change-me|xxx+|\*+|\.\.\.+|my-?password|your-?password|<[^>]*>|\$\{[^}]*\}|\{\{[^}]*\}\}|%[A-Za-z_]+%?)$/i
|
|
150
150
|
|
|
151
|
-
/** A literal assigned to a name: `apiKey: "..."`, `const secret = '...'
|
|
152
|
-
* interpolation are not literals. The lookbehind starts a name only where
|
|
153
|
-
* match from inside a long word run backtracks the rest of that run,
|
|
151
|
+
/** A literal assigned to a name: `apiKey: "..."`, `const secret = '...'`, `"x-api-key": "..."`.
|
|
152
|
+
* Template literals with an interpolation are not literals. The lookbehind starts a name only where
|
|
153
|
+
* an identifier starts: a match from inside a long word run backtracks the rest of that run,
|
|
154
|
+
* quadratic in its length. A hyphenated name starts only right after a quote, for the same reason. */
|
|
154
155
|
const ASSIGNED =
|
|
155
|
-
/(?<![\w$])([A-Za-z_$][\w$]*)["']
|
|
156
|
+
/(?<![\w$])((?<=["'])[A-Za-z_$][\w$-]*(?=["'])|[A-Za-z_$][\w$]*)(["']?)\s*([:=]|\?\?=|\|\|=)\s*(?:"([^"\\\r\n]{16,256})"|'([^'\\\r\n]{16,256})'|`([^`\\$\r\n]{16,256})`)/g
|
|
157
|
+
|
|
158
|
+
/** Whether a match's name is what the literal is assigned to. A quoted name must be the whole string,
|
|
159
|
+
* not the tail of one (`? "new-password" : "current-password"`), and a key is never preceded by a
|
|
160
|
+
* ternary's `?` or a member access's `.` (`ready ? config.apiKey : "..."`). */
|
|
161
|
+
function namesTheTarget(text: string, start: number, quote: string, operator: string): boolean {
|
|
162
|
+
let before = start - 1
|
|
163
|
+
if (quote !== "") {
|
|
164
|
+
if (text[before] !== quote) return false
|
|
165
|
+
before--
|
|
166
|
+
}
|
|
167
|
+
if (operator !== ":") return true
|
|
168
|
+
while (before >= 0 && /\s/.test(text[before] ?? "")) before--
|
|
169
|
+
return text[before] !== "?" && text[before] !== "."
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** A quoted name and a literal written as a pair: `headers.set("x-api-key", "...")`,
|
|
173
|
+
* `new Headers([["x-api-key", "..."]])`. The last group is what follows the literal. */
|
|
174
|
+
const PAIRED =
|
|
175
|
+
/(["'])([A-Za-z_$][\w$-]*)\1\s*,\s*(?:"([^"\\\r\n]{16,256})"|'([^'\\\r\n]{16,256})'|`([^`\\$\r\n]{16,256})`)\s*([\]),]?)/g
|
|
176
|
+
|
|
177
|
+
/** A value spelled as words or as a constant's name: another field (`["password",
|
|
178
|
+
* "passwordConfirmation"]`), an environment variable, a stand-in token (`"Bearer not-a-token"`). */
|
|
179
|
+
const NAME_LIKE =
|
|
180
|
+
/^(?:[A-Za-z][a-z]*(?:[A-Z][a-z]+|[_.-][A-Za-z][a-z]*)+|[A-Z][A-Z0-9]*(?:_[A-Z][A-Z0-9]*)+)$/
|
|
181
|
+
|
|
182
|
+
/** Whether a pair is a whole two-element array, or the first two arguments of `.set(` or `.append(`:
|
|
183
|
+
* an element in a longer list is not a pair. */
|
|
184
|
+
function opensAPair(text: string, quoteAt: number, after: string): boolean {
|
|
185
|
+
let before = quoteAt - 1
|
|
186
|
+
while (before >= 0 && /\s/.test(text[before] ?? "")) before--
|
|
187
|
+
if (text[before] === "[") return after === "]"
|
|
188
|
+
if (text[before] !== "(" || (after !== ")" && after !== ",")) return false
|
|
189
|
+
return /\.(?:set|append)$/.test(text.slice(Math.max(0, before - 7), before))
|
|
190
|
+
}
|
|
156
191
|
|
|
157
192
|
const EXTRA_SECRET_NAME =
|
|
158
193
|
/(?:accesskey|signingkey|encryptionkey|masterkey|serviceaccountkey|connectionstring)$/
|
|
159
194
|
|
|
195
|
+
/** Header names whose value puts a scheme before the credential: `"Bearer <token>"`. */
|
|
196
|
+
const AUTHORIZATION_NAME = /^(?:proxy)?authorization$/
|
|
197
|
+
const AUTHORIZATION_VALUE = /^(?:(bearer)|basic) +(\S+)$/i
|
|
198
|
+
|
|
160
199
|
/** Values that look like keys but are public by design. */
|
|
161
200
|
const PUBLIC_VALUE =
|
|
162
201
|
/^(?:AIza[0-9A-Za-z_-]{35}|pk_(?:live|test)_[0-9A-Za-z]+|6L[0-9A-Za-z_-]{38}|G-[A-Z0-9]{6,}|UA-\d+-\d+)$/
|
|
@@ -186,6 +225,32 @@ function looksRandom(value: string, minLength: number): boolean {
|
|
|
186
225
|
return classes >= 2 && entropy(value) >= 3.5
|
|
187
226
|
}
|
|
188
227
|
|
|
228
|
+
/** A password the scanned text spells as a placeholder or a dev default. */
|
|
229
|
+
function placeholderPassword(user: string, password: string): boolean {
|
|
230
|
+
// nifra-expect NF-S002: a password equal to its user name is a placeholder in the text being
|
|
231
|
+
// scanned; nothing is authenticated here.
|
|
232
|
+
return PLACEHOLDER.test(password) || password === user || /[${}<>*]/.test(password)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** The credential an `Authorization` literal carries: a Bearer token, or a Basic `user:password`
|
|
236
|
+
* that is not a placeholder. */
|
|
237
|
+
function authorizationCredential(value: string): string | undefined {
|
|
238
|
+
const found = AUTHORIZATION_VALUE.exec(value)
|
|
239
|
+
if (found === null) return undefined
|
|
240
|
+
const credential = found[2] ?? ""
|
|
241
|
+
if (found[1] !== undefined) return NAME_LIKE.test(credential) ? undefined : credential
|
|
242
|
+
let decoded: string
|
|
243
|
+
try {
|
|
244
|
+
decoded = atob(credential)
|
|
245
|
+
} catch {
|
|
246
|
+
return undefined
|
|
247
|
+
}
|
|
248
|
+
const colon = decoded.indexOf(":")
|
|
249
|
+
if (colon === -1 || placeholderPassword(decoded.slice(0, colon), decoded.slice(colon + 1)))
|
|
250
|
+
return undefined
|
|
251
|
+
return credential
|
|
252
|
+
}
|
|
253
|
+
|
|
189
254
|
function jwtRole(payload: string): unknown {
|
|
190
255
|
try {
|
|
191
256
|
const json = atob(payload.replaceAll("-", "+").replaceAll("_", "/"))
|
|
@@ -217,11 +282,7 @@ function formatMatches(text: string): Match[] {
|
|
|
217
282
|
matches.push({ rule: "private-key", index: found.index, text: found[0], what: "private key" })
|
|
218
283
|
}
|
|
219
284
|
for (const found of text.matchAll(CREDENTIAL_URL)) {
|
|
220
|
-
|
|
221
|
-
const password = found[2] ?? ""
|
|
222
|
-
// @nifra-gate-reviewed: a password equal to its user name is a placeholder in the text being
|
|
223
|
-
// scanned; nothing is authenticated here.
|
|
224
|
-
if (PLACEHOLDER.test(password) || password === user || /[${}<>*]/.test(password)) continue
|
|
285
|
+
if (placeholderPassword(found[1] ?? "", found[2] ?? "")) continue
|
|
225
286
|
matches.push({
|
|
226
287
|
rule: "credential-url",
|
|
227
288
|
index: found.index,
|
|
@@ -232,20 +293,46 @@ function formatMatches(text: string): Match[] {
|
|
|
232
293
|
return matches
|
|
233
294
|
}
|
|
234
295
|
|
|
296
|
+
/** The credential a literal given to `name` carries, when the name is secret-like and the literal
|
|
297
|
+
* looks random. */
|
|
298
|
+
function credentialOf(name: string, value: string): string | undefined {
|
|
299
|
+
const key = name.toLowerCase().replace(/[-_]/g, "")
|
|
300
|
+
const header = AUTHORIZATION_NAME.test(key)
|
|
301
|
+
if (!header && !isSensitiveFieldName(name) && !EXTRA_SECRET_NAME.test(key)) return undefined
|
|
302
|
+
const secret = header ? authorizationCredential(value) : value
|
|
303
|
+
if (
|
|
304
|
+
secret === undefined ||
|
|
305
|
+
PUBLIC_VALUE.test(secret) ||
|
|
306
|
+
PLACEHOLDER.test(secret) ||
|
|
307
|
+
!looksRandom(secret, 16)
|
|
308
|
+
)
|
|
309
|
+
return undefined
|
|
310
|
+
return secret
|
|
311
|
+
}
|
|
312
|
+
|
|
235
313
|
function assignedMatches(text: string): Match[] {
|
|
236
314
|
const matches: Match[] = []
|
|
237
|
-
|
|
238
|
-
const
|
|
239
|
-
if (
|
|
240
|
-
const value = found[2] ?? found[3] ?? found[4] ?? ""
|
|
241
|
-
if (PUBLIC_VALUE.test(value) || PLACEHOLDER.test(value) || !looksRandom(value, 16)) continue
|
|
315
|
+
const add = (name: string, value: string, valueAt: number): void => {
|
|
316
|
+
const secret = credentialOf(name, value)
|
|
317
|
+
if (secret === undefined) return
|
|
242
318
|
matches.push({
|
|
243
319
|
rule: "assigned-secret",
|
|
244
|
-
|
|
245
|
-
|
|
320
|
+
// The credential is the value's tail: a header value's scheme is not part of it.
|
|
321
|
+
index: valueAt + value.length - secret.length,
|
|
322
|
+
text: secret,
|
|
246
323
|
what: `literal assigned to "${name}"`,
|
|
247
324
|
})
|
|
248
325
|
}
|
|
326
|
+
for (const found of text.matchAll(ASSIGNED)) {
|
|
327
|
+
if (!namesTheTarget(text, found.index, found[2] ?? "", found[3] ?? "")) continue
|
|
328
|
+
const value = found[4] ?? found[5] ?? found[6] ?? ""
|
|
329
|
+
add(found[1] ?? "", value, found.index + found[0].lastIndexOf(value))
|
|
330
|
+
}
|
|
331
|
+
for (const found of text.matchAll(PAIRED)) {
|
|
332
|
+
const value = found[3] ?? found[4] ?? found[5] ?? ""
|
|
333
|
+
if (NAME_LIKE.test(value) || !opensAPair(text, found.index, found[6] ?? "")) continue
|
|
334
|
+
add(found[2] ?? "", value, found.index + found[0].lastIndexOf(value))
|
|
335
|
+
}
|
|
249
336
|
return matches
|
|
250
337
|
}
|
|
251
338
|
|
|
@@ -493,7 +580,7 @@ export function createRedactor(
|
|
|
493
580
|
for (const { what, pattern } of KEY_FORMATS) out = out.replace(pattern, `[redacted:${what}]`)
|
|
494
581
|
out = out.replace(JWT, "[redacted:JWT]")
|
|
495
582
|
out = out.replace(CREDENTIAL_URL, (match, user: string, password: string) =>
|
|
496
|
-
//
|
|
583
|
+
// nifra-expect NF-S002: the same placeholder test as the scan; nothing is authenticated here.
|
|
497
584
|
PLACEHOLDER.test(password) || password === user
|
|
498
585
|
? match
|
|
499
586
|
: match.replace(`:${password}@`, ":[redacted]@"),
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
export const SERVER_FN_MODULE = /\.fn(\.[cm]?[jt]sx?)?$/
|
|
15
15
|
|
|
16
16
|
import { jsStringLiteral } from "./js-string.ts"
|
|
17
|
+
import { DATE_TAG, REVIVE_DATES_SOURCE } from "./revive-dates.ts"
|
|
17
18
|
|
|
18
19
|
// ---------------------------------------------------------------------------------------------------
|
|
19
20
|
// `.fn` - turn a server-function module into the client stubs that call it.
|
|
@@ -50,15 +51,18 @@ const DECLARATION =
|
|
|
50
51
|
const ANY_CALL = /\bserverFn\s*[<(]/g
|
|
51
52
|
/** Strip line and block comments by forward index scan, so prose cannot register as code. Not a
|
|
52
53
|
* regex: every regex shape for comment stripping either backtracks polynomially on adversarial
|
|
53
|
-
* source or is opaque enough that reviewers cannot tell.
|
|
54
|
-
*
|
|
55
|
-
* swallows the rest of the source, matching a real parser. */
|
|
54
|
+
* source or is opaque enough that reviewers cannot tell. String literals are stepped over, so a
|
|
55
|
+
* `"image/*"` cannot open a comment; a `//` preceded by `:` is kept (a URL outside a string); an
|
|
56
|
+
* unterminated block comment swallows the rest of the source, matching a real parser. */
|
|
56
57
|
function stripComments(source: string): string {
|
|
57
58
|
const parts: string[] = []
|
|
58
59
|
let keepFrom = 0
|
|
59
60
|
let i = 0
|
|
60
61
|
while (i < source.length) {
|
|
61
|
-
|
|
62
|
+
const char = source[i]
|
|
63
|
+
if (char === '"' || char === "'" || char === "`") {
|
|
64
|
+
i = stringEnd(source, i)
|
|
65
|
+
} else if (source.startsWith("/*", i)) {
|
|
62
66
|
const close = source.indexOf("*/", i + 2)
|
|
63
67
|
parts.push(source.slice(keepFrom, i))
|
|
64
68
|
i = close === -1 ? source.length : close + 2
|
|
@@ -76,6 +80,21 @@ function stripComments(source: string): string {
|
|
|
76
80
|
return parts.join("")
|
|
77
81
|
}
|
|
78
82
|
|
|
83
|
+
/** The index just past the string literal opening at `start`. A quote or apostrophe string also ends
|
|
84
|
+
* at a line break, so a stray one (JSX text, a regex literal) misreads one line at most. */
|
|
85
|
+
function stringEnd(source: string, start: number): number {
|
|
86
|
+
const quote = source[start]
|
|
87
|
+
let i = start + 1
|
|
88
|
+
while (i < source.length) {
|
|
89
|
+
const char = source[i]
|
|
90
|
+
if (char === "\\") i += 2
|
|
91
|
+
else if (char === quote) return i + 1
|
|
92
|
+
else if (char === "\n" && quote !== "`") return i
|
|
93
|
+
else i++
|
|
94
|
+
}
|
|
95
|
+
return source.length
|
|
96
|
+
}
|
|
97
|
+
|
|
79
98
|
/**
|
|
80
99
|
* The namespace a `*.fn.ts` module is mounted under: its filename without the `.fn` suffix.
|
|
81
100
|
*
|
|
@@ -143,9 +162,14 @@ async function __nifraCall(name, input) {
|
|
|
143
162
|
}
|
|
144
163
|
throw new Error(\`[nifra/fn] \${name} failed: \${response.status}\${detail ? \` \${detail}\` : ""}\`)
|
|
145
164
|
}
|
|
146
|
-
|
|
165
|
+
if (response.status === 204) return undefined
|
|
166
|
+
const text = await response.text()
|
|
167
|
+
const value = JSON.parse(text)
|
|
168
|
+
return text.includes('"${DATE_TAG}"') ? __nifraDates(value) : value
|
|
147
169
|
}
|
|
148
170
|
|
|
171
|
+
${REVIVE_DATES_SOURCE}
|
|
172
|
+
|
|
149
173
|
${calls}
|
|
150
174
|
`
|
|
151
175
|
}
|