diff --git a/.github/workflows/native-addon-functions.yaml b/.github/workflows/native-addon-functions.yaml index 02f195bfd..4252b704b 100644 --- a/.github/workflows/native-addon-functions.yaml +++ b/.github/workflows/native-addon-functions.yaml @@ -21,6 +21,17 @@ jobs: fail-fast: false matrix: node: [24.22.1, 26.13.0] + next: ['16.4.3', '16.4.4'] + include: + - next: '16.4.3' + next-commit: 29bbeab09981148ade186e8599ecdede15abc281 + - next: '16.4.4' + next-commit: b175f52b05a5f6c6c39ca72565ea178542be323b + env: + # The official fixtures build with this release; the SDK's own tests pin theirs. + UPWIND_NATIVE_NEXT: ${{ matrix.next }} + # Release-independent checks run once per Node.js version, in the newest release's leg. + RELEASE_INDEPENDENT: ${{ matrix.next == '16.4.4' }} steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 @@ -34,29 +45,42 @@ jobs: - name: Create an isolated artifact mount run: sudo install -d -m 0777 /bundle - name: Compile, package and invoke a real native addon + if: env.RELEASE_INDEPENDENT == 'true' run: pnpm exec vitest run --project adapter packages/adapter/test/native-container-integration.test.ts env: UPWIND_NATIVE_FUNCTION_INTEGRATION: '1' - name: Verify failure diagnostic filesystem boundaries + if: env.RELEASE_INDEPENDENT == 'true' run: pnpm exec vitest run --project adapter packages/adapter/test/native-next-diagnostics.test.ts - name: Verify the fixed Node dependency evaluation guard + if: env.RELEASE_INDEPENDENT == 'true' run: pnpm exec vitest run --project adapter packages/adapter/test/node-jwt-guard-integration.test.ts env: UPWIND_NATIVE_FUNCTION_INTEGRATION: '1' + - name: Verify the fixed Node fast-jwt evaluation guard (Next.js 16.4.4) + if: env.RELEASE_INDEPENDENT == 'true' + run: pnpm exec vitest run --project adapter packages/adapter/test/node-fast-jwt-guard-integration.test.ts + env: + UPWIND_NATIVE_FUNCTION_INTEGRATION: '1' - name: Verify the fixed WebSocket optional dependency guard + if: env.RELEASE_INDEPENDENT == 'true' run: pnpm exec vitest run --project adapter packages/adapter/test/node-ws-guard-integration.test.ts env: UPWIND_NATIVE_FUNCTION_INTEGRATION: '1' - name: Verify the fixed Turbopack selector specialization + if: env.RELEASE_INDEPENDENT == 'true' run: pnpm exec vitest run --project adapter packages/adapter/test/node-turbopack-selector-integration.test.ts env: UPWIND_NATIVE_FUNCTION_INTEGRATION: '1' - name: Verify complete original Node export closures + if: env.RELEASE_INDEPENDENT == 'true' run: pnpm exec vitest run --project adapter packages/adapter/test/node-raw-closure-integration.test.ts env: UPWIND_NATIVE_FUNCTION_INTEGRATION: '1' - - name: Check out the fixed Next.js 16.4.3 native fixtures + - name: Check out the fixed Next.js ${{ matrix.next }} native fixtures shell: bash + env: + NEXT_COMMIT: ${{ matrix.next-commit }} run: | set -euo pipefail fixture_source="$RUNNER_TEMP/upwind-official-native-next" @@ -64,9 +88,9 @@ jobs: git -C "$fixture_source" remote add origin https://github.com/vercel/next.js.git git -C "$fixture_source" sparse-checkout init --no-cone git -C "$fixture_source" sparse-checkout set --no-cone '/test/e2e/prerender-native-module/' '/test/e2e/app-dir/turbopack-reports/' - git -C "$fixture_source" fetch --no-tags --depth=1 origin 29bbeab09981148ade186e8599ecdede15abc281 + git -C "$fixture_source" fetch --no-tags --depth=1 origin "$NEXT_COMMIT" git -C "$fixture_source" checkout --detach FETCH_HEAD - test "$(git -C "$fixture_source" rev-parse HEAD)" = 29bbeab09981148ade186e8599ecdede15abc281 + test "$(git -C "$fixture_source" rev-parse HEAD)" = "$NEXT_COMMIT" - name: Build and invoke the official native fixture and fallback path id: official_native run: node tools/native-addon-next-ci.mts "$RUNNER_TEMP/upwind-official-native-next/test/e2e/prerender-native-module" @@ -74,7 +98,7 @@ jobs: if: failure() uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: - name: native-next-compiled-js-${{ matrix.node }}-${{ github.run_id }}-${{ github.run_attempt }} + name: native-next-compiled-js-${{ matrix.node }}-next-${{ matrix.next }}-${{ github.run_id }}-${{ github.run_attempt }} path: ${{ runner.temp }}/upwind-native-next-diagnostics if-no-files-found: warn retention-days: 1 @@ -92,7 +116,7 @@ jobs: if: failure() && steps.official_native_diagnostic.outcome == 'failure' uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: - name: native-next-server-minification-off-js-${{ matrix.node }}-${{ github.run_id }}-${{ github.run_attempt }} + name: native-next-server-minification-off-js-${{ matrix.node }}-next-${{ matrix.next }}-${{ github.run_id }}-${{ github.run_attempt }} path: ${{ runner.temp }}/upwind-native-next-server-minification-off/upwind-native-next-diagnostics if-no-files-found: warn retention-days: 1 @@ -110,12 +134,12 @@ jobs: if: failure() && steps.official_native_app.outcome == 'failure' uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: - name: native-next-app-router-js-${{ matrix.node }}-${{ github.run_id }}-${{ github.run_attempt }} + name: native-next-app-router-js-${{ matrix.node }}-next-${{ matrix.next }}-${{ github.run_id }}-${{ github.run_attempt }} path: ${{ runner.temp }}/upwind-native-next-app-router/upwind-native-next-diagnostics if-no-files-found: warn retention-days: 1 - name: Verify the fixed Next require hook bootstrap - if: always() && !cancelled() && steps.build.outcome == 'success' + if: always() && !cancelled() && steps.build.outcome == 'success' && env.RELEASE_INDEPENDENT == 'true' run: pnpm exec vitest run --project runtime packages/runtime/test/node-next-hook-bootstrap-integration.test.ts env: UPWIND_NATIVE_FUNCTION_INTEGRATION: '1' diff --git a/.github/workflows/native-pages-handler-profile.yaml b/.github/workflows/native-pages-handler-profile.yaml index ba90bb7b4..070635d16 100644 --- a/.github/workflows/native-pages-handler-profile.yaml +++ b/.github/workflows/native-pages-handler-profile.yaml @@ -34,13 +34,22 @@ jobs: option: '--server-minification=off' output: independent-pages-handler-profile-server-minification-off artifact: UNQUALIFIED-public-pages-handler-server-minification-off + - server-minification: default + option: '--next=16.4.4' + output: independent-pages-handler-profile-next-16.4.4 + artifact: UNQUALIFIED-public-pages-handler-next-16.4.4 + - server-minification: off + option: '--next=16.4.4 --server-minification=off' + output: independent-pages-handler-profile-next-16.4.4-server-minification-off + artifact: UNQUALIFIED-public-pages-handler-next-16.4.4-server-minification-off steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false + # The producer's own pnpm, from its fixture: the repository's `packageManager` names another. - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 with: - version: 12.15.0 + package_json_file: tools/native-pages-handler-profile/fixture/package.json - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: node-version: 24.22.1 diff --git a/docs/native-functions.md b/docs/native-functions.md index 621aaed98..06845384b 100644 --- a/docs/native-functions.md +++ b/docs/native-functions.md @@ -43,8 +43,8 @@ a proved closure of them. A project with an `app/` directory gives every Functio is in a Pages Router route. Only the native loads are proved, by the same loader classification, binary selection, ABI and digests as for any Node Function, and a Cloudflare platform import in the Function's own graph is refused. The shared-cache rewrites still apply; the instrumentation loader -and the custom-cache guard are rewritten only over their reviewed Next.js 16.4.3 bytes, and any -other bytes at those paths are refused. The Turbopack selectors are left to the bootstrap's +and the custom-cache guard are rewritten only over the reviewed bytes of the Next.js release the +package is, 16.4.3 or 16.4.4, and any other bytes at those paths are refused. The Turbopack selectors are left to the bootstrap's `TURBOPACK`. Such an artifact does not have the manifest-consumer and closure proofs, the JWT, WebSocket and environment initialization guards, or an alias proof for packages that only server chunks load: a missing package link shows when the route runs, not at build. A distDir that would be @@ -67,8 +67,14 @@ Node ABI and Linux x64 glibc target before publishing it. Worker loader hooks, p and native shims are not applied to Node files. The existing shared cache and request runtime is retained through a Node transport, rather than replaced with an independent `next start` cache. -The original Next.js manifest consumers and Turbopack runtime target Next.js 16.4.3. Changed -compiler or template shapes are refused until their source contracts are reviewed. A minified +The original Next.js manifest consumers and Turbopack runtime target Next.js 16.4.3 and 16.4.4. The +two publish the same reviewed sources but for the page template factory and the server runtimes, +which are pinned for each release (`node-next-release.ts`), and a package holding another release's +copy of one is refused. 16.4.4's Turbopack runtime installs no leading strict factory table, which +16.4.3's chunks may carry, so a chunk with one beside that runtime is refused. Each release has its +own reviewed getHandler references (`node-manifest-handler-catalog.ts`), with and without server +minification. Changed compiler or template shapes are refused until their source contracts are +reviewed. A minified CommonJS Pages factory can retain its original sloppy arrow function when its physical module mode and closed userland partition are proved alongside the protected template. This inspection does not insert a strict directive or change the shipped JavaScript. The adapter leaves the @@ -113,8 +119,12 @@ in the Node archive, so a runtime override cannot invoke a dependency omitted by The default shared cache and rendering paths remain available. This policy does not replace a missing dependency with a stub or allow unresolved application imports. -The fixed Next.js JWT dependency has a separate, source-reviewed initialization profile. It -permits only its exact global-recovery compiler site after proving the original package, closed +The fixed Next.js JWT dependency has a separate, source-reviewed initialization profile for each +release: Next.js 16.4.3 compiles in jsonwebtoken, and 16.4.4 compiles in fast-jwt. The jsonwebtoken +profile permits only its exact global-recovery compiler site. The fast-jwt profile permits no +compiler site and only one `process.binding("buffer")` read, in its copy of safer-buffer, behind a +`!o.kStringMaxLength` check. Node.js 24 and 26 define `buffer.kStringMaxLength`, so that read +never runs on them. Either is permitted only after proving the original package, closed dependencies, CommonJS callers and completed route closure. The Node bootstrap checks the original bytes and required builtin state immediately before CommonJS evaluation, including after cache eviction. Changed sources, loader customization and unsupported compiler operations @@ -122,8 +132,8 @@ remain publication errors. This profile requires the default Node.js 24.22.1 loa make arbitrary dynamic compilation portable. These restrictions apply to native Node artifacts; ordinary Worker output keeps its existing version support. -The same guard observes the original `semver-noop` dependency evaluation and checks its genuine -cached namespace before JWT initialization. Scoped load and require boundaries preserve Node's +For jsonwebtoken, the same guard observes the original `semver-noop` dependency evaluation and +checks its genuine cached namespace before JWT initialization; fast-jwt has no such dependency. Scoped load and require boundaries preserve Node's monitoring hooks while checking that fixed dependencies return the qualified module values. The fixed Next.js WebSocket dependency has a separate absent-only profile for its two literal @@ -158,10 +168,11 @@ refuse artifacts they cannot execute. These errors preserve the application's ru The dedicated Linux CI control compiles and executes a real N-API binary, creates the actual adapter artifact, starts its Node HTTP entry, and checks that independent Worker and Edge routes remain on -Workers. A separate CI step verifies a pinned, unchanged Next.js 16.4.3 native fixture, builds it -with the actual adapter and invokes its emitted Node entry on loopback, including `/blog/second` -with `fallback: true`. The unchanged App Router fixture `test/e2e/app-dir/turbopack-reports` is built -and invoked the same way. Its pages are prerendered, so the control adds one page rendered for each -request, and checks that a route left on Workers is refused by the Node entry. This checks the -artifact and Node transport; hosted cache and deployment -integration still require operator rollout verification. No CI result is implied by this document. +Workers. A separate CI step verifies the pinned, unchanged native fixture of each supported Next.js +release, 16.4.3 and 16.4.4. It builds the fixture with the actual adapter and invokes its emitted +Node entry on loopback, including `/blog/second` with `fallback: true`. The unchanged App Router +fixture `test/e2e/app-dir/turbopack-reports` is built and invoked the same way. Its pages are +prerendered, so the control adds one page rendered for each request, and checks that a route left +on Workers is refused by the Node entry. This checks the artifact and Node transport; hosted cache +and deployment integration still require operator rollout verification. No CI result is implied by +this document. diff --git a/docs/node26.md b/docs/node26.md index d280f1efe..eb232c839 100644 --- a/docs/node26.md +++ b/docs/node26.md @@ -12,7 +12,8 @@ Runtime initialization rejects a different version; unknown versions cannot sele The existing source hashes, loader ownership, optional dependency absence and realm checks remain mandatory. The version is selected before application evaluation, with no request-time I/O. -Node 26 removed `buffer.SlowBuffer`, which the original fixed JWT dependency still reads. +Node 26 removed `buffer.SlowBuffer`, which the fixed JWT dependency of Next.js 16.4.3, jsonwebtoken, +still reads; 16.4.4's fast-jwt does not. Trusted initialization installs a compatibility constructor before capturing the builtin realm. It delegates to `Buffer.allocUnsafeSlow`, with the constructor/prototype chains from Node 24's [official buffer implementation](https://github.com/nodejs/node/blob/v24.22.1/lib/buffer.js). diff --git a/fixtures/next-native/package.json b/fixtures/next-native/package.json index 5a8a9c4a4..7e1412867 100644 --- a/fixtures/next-native/package.json +++ b/fixtures/next-native/package.json @@ -5,7 +5,7 @@ "type": "module", "scripts": { "build": "upwind build --target=node" }, "dependencies": { - "next": "16.4.3", + "next": "16.4.4", "react": "^19.0.0", "react-dom": "^19.0.0", "sharp": "0.35.5" diff --git a/packages/adapter/src/node-closure-proof.ts b/packages/adapter/src/node-closure-proof.ts index 83c88c427..8751bd2e1 100644 --- a/packages/adapter/src/node-closure-proof.ts +++ b/packages/adapter/src/node-closure-proof.ts @@ -26,6 +26,7 @@ import { nodeClosureReference as isReference, nodeCompilerMetadataSites, } from './node-compiler-metadata.ts'; +import { withNodeJwtLoader } from './node-jwt-compiler-profiles.ts'; import { nodeJwtCompilerSite, type NodeJwtCompilerProof } from './node-jwt-compiler-proof.ts'; import { isGlobalCompiler } from './node-jwt-compiler-sites.ts'; import { nodeNextCacheWriteSites } from './node-next-cache-write-proof.ts'; @@ -521,10 +522,11 @@ export function dynamicNodeClosureRefusal( return 'an unproved Turbopack factory context'; const jwtSite = nodeJwtCompilerSite(jwt, file, code, program); const compilerSites = new Set(nodeCompilerMetadataSites(all)); - if (jwtSite !== undefined) compilerSites.add(jwtSite); + if (jwtSite?.kind === 'compiler') compilerSites.add(jwtSite.node); const wsSites = nodeWsLoaderSites(loaders?.ws, file, code, program); for (const site of wsSites) compilerSites.add(site); - const hookSites = nodeNextRequireHookSites(loaders?.nextRequireHook, file, code, program); + const hooks = nodeNextRequireHookSites(loaders?.nextRequireHook, file, code, program); + const hookSites = withNodeJwtLoader(hooks, jwtSite); for (const visit of all) { if (serverChunk && (isContextLoader(visit, contexts) || isContextEscape(visit, contexts))) return 'an unproved Turbopack external import'; diff --git a/packages/adapter/src/node-jwt-compiler-profiles.ts b/packages/adapter/src/node-jwt-compiler-profiles.ts new file mode 100644 index 000000000..2101a57fa --- /dev/null +++ b/packages/adapter/src/node-jwt-compiler-profiles.ts @@ -0,0 +1,60 @@ +import type { Node } from './lexical-scope.ts'; +import { nodeJwtGlobalRecoverySite } from './node-jwt-compiler-sites.ts'; +import { nodeFastJwtBindingSite } from './node-jwt-fast-sites.ts'; +import type { NodeNextRequireHookSites } from './node-next-require-hook-sites.ts'; + +/** The one site of a qualified source the ordinary closure walker refuses, and how it reads it. */ +export interface NodeJwtCompilerSite { + /** A code compiler call, or a Node loader read. */ + readonly kind: 'compiler' | 'loader'; + readonly node: Node; +} + +/** The JWT library a release of Next.js compiles in, by its exact published bytes. */ +export interface NodeJwtCompilerProfile { + readonly name: 'next-16.4.3-jsonwebtoken-ncc-v1' | 'next-16.4.4-fast-jwt-ncc-v1'; + readonly release: '16.4.3' | '16.4.4'; + readonly suffix: string; + readonly sha256: string; + readonly bytes: number; + /** The builtins it loads, each by the exact specifier it writes. */ + readonly builtins: readonly string[]; + /** The no-op module Next.js substitutes for its `semver`, where it has one. */ + readonly semverSha256?: string; + site(program: unknown): NodeJwtCompilerSite | undefined; +} + +/** The require hook's exempt loader operations, with a qualified source's own loader read. */ +export function withNodeJwtLoader( + hooks: NodeNextRequireHookSites, + site: NodeJwtCompilerSite | undefined, +): NodeNextRequireHookSites { + if (site?.kind !== 'loader') return hooks; + return { ...hooks, operations: new Set([...hooks.operations, site.node]) }; +} + +function siteOf(kind: NodeJwtCompilerSite['kind'], node: Node | undefined) { + return node === undefined ? undefined : Object.freeze({ kind, node }); +} + +export const NODE_JWT_COMPILER_PROFILES: readonly NodeJwtCompilerProfile[] = Object.freeze([ + Object.freeze({ + name: 'next-16.4.3-jsonwebtoken-ncc-v1', + release: '16.4.3', + suffix: '/dist/compiled/jsonwebtoken/index.js', + sha256: '056c2dddba96faa35f4dc0740228a241990561290ff3bd5456c7c7dbc5a7a6b9', + bytes: 121_051, + builtins: Object.freeze(['buffer', 'crypto', 'stream', 'util']), + semverSha256: '34c5853dd094229baa28a8b15b86e2143eeb0de0360530798ef3e9a7890942b6', + site: (program: unknown) => siteOf('compiler', nodeJwtGlobalRecoverySite(program)), + }), + Object.freeze({ + name: 'next-16.4.4-fast-jwt-ncc-v1', + release: '16.4.4', + suffix: '/dist/compiled/fast-jwt/index.js', + sha256: '8cc4cea3ed6b85a6771a2546bbb88dcd1ca21e6061d89057728d3da4ecccfabf', + bytes: 131_737, + builtins: Object.freeze(['buffer', 'node:crypto', 'node:util', 'util']), + site: (program: unknown) => siteOf('loader', nodeFastJwtBindingSite(program)), + }), +]); diff --git a/packages/adapter/src/node-jwt-compiler-proof.ts b/packages/adapter/src/node-jwt-compiler-proof.ts index 1e4c1f11e..341995c82 100644 --- a/packages/adapter/src/node-jwt-compiler-proof.ts +++ b/packages/adapter/src/node-jwt-compiler-proof.ts @@ -5,9 +5,14 @@ import path from 'node:path'; import { parseAst } from 'rolldown/parseAst'; import { AuditError } from './dependencies.ts'; -import { isNode, type Node } from './lexical-scope.ts'; +import { isNode } from './lexical-scope.ts'; import { nodeGuardVersion, type GuardedNodeVersion } from './node-guard-version.ts'; -import { nodeJwtGlobalRecoverySite, nodeJwtRequireOnly } from './node-jwt-compiler-sites.ts'; +import { + NODE_JWT_COMPILER_PROFILES, + type NodeJwtCompilerProfile, + type NodeJwtCompilerSite, +} from './node-jwt-compiler-profiles.ts'; +import { nodeJwtRequireOnly } from './node-jwt-compiler-sites.ts'; import { nodeJwtCallerInspection, type NodeJwtCallerPolicyProof, @@ -24,6 +29,7 @@ import { type NodeManifestIdentitySnapshot, } from './node-manifest-identity-snapshot.ts'; import type { NodeManifestProof } from './node-manifest-proof.ts'; +import { nativeNextReleaseAt } from './node-next-release.ts'; import type { NodeNextRewriteInput, NodeNextRewriteProof } from './node-next-rewrite-proof.ts'; import { assertOriginalCommonJsMode, @@ -36,12 +42,7 @@ import { checkedNodeRouteRewriteSources } from './node-route-rewrite-approval.ts import type { AppliedPatch } from './patches/rolldown.ts'; const PROOF: unique symbol = Symbol('fixed Node JWT compiler sites'); -const JWT_SUFFIX = '/dist/compiled/jsonwebtoken/index.js'; -const JWT_SHA256 = '056c2dddba96faa35f4dc0740228a241990561290ff3bd5456c7c7dbc5a7a6b9'; -const JWT_BYTES = 121_051; -const SEMVER_SHA256 = '34c5853dd094229baa28a8b15b86e2143eeb0de0360530798ef3e9a7890942b6'; -const NEXT_PACKAGE_SHA256 = 'b67bad7944d603b27dd2d2719b57203fd3b3e5c147b2efa6dea5ceb135274caf'; -const BUILTINS = new Set(['buffer', 'crypto', 'stream', 'util']); +const SEMVER = 'next/dist/lib/semver-noop'; const MAX_METADATA_BYTES = 65_536; const MAX_SOURCE_BYTES = 4_194_304; const MAX_SCOPE_DEPTH = 64; @@ -61,11 +62,11 @@ export interface NodeJwtCompilerInput { } export interface NodeJwtGuardRecord { readonly schema: 1; - readonly profile: 'next-16.4.3-jsonwebtoken-ncc-v1'; + readonly profile: NodeJwtCompilerProfile['name']; readonly nodeVersion: GuardedNodeVersion; readonly archivePath: string; readonly sha256: string; - readonly bytes: typeof JWT_BYTES; + readonly bytes: number; } interface Mode { readonly id: string; @@ -81,6 +82,7 @@ interface Caller extends ManifestGraphFile { readonly policy?: NodeJwtCallerPolicyProof | undefined; } interface State { + readonly profile: NodeJwtCompilerProfile; readonly input: NodeJwtCompilerInput; readonly snapshot: NodeManifestIdentitySnapshot; readonly data: ManifestModuleGraph; @@ -274,16 +276,28 @@ async function incoming(state: State, ids: ReadonlySet): Promise { const graph = state.data; const own = graph.sources.get(id); + const expected = state.profile.semverSha256; if ( + expected === undefined || own?.file !== path.join(root, 'dist/lib/semver-noop.js') || - own.sha256 !== SEMVER_SHA256 || + own.sha256 !== expected || !graph.retained.has(id) ) return false; if (state.patched.has(id) || state.patched.has(own.file)) return false; - await source(state, own.file, SEMVER_SHA256); + await source(state, own.file, expected); return await commonjs(state, id, own); } +/** A builtin the profile names by this exact specifier, or Next.js's semver no-op where it has one. */ +async function dependencyEdge( + state: State, + edge: { readonly specifier: string; readonly target: string }, + root: string, +): Promise { + if (state.profile.builtins.includes(edge.specifier)) + return edge.target.replace(/^node:/u, '') === edge.specifier.replace(/^node:/u, ''); + return edge.specifier === SEMVER && (await semverDependency(state, edge.target, root)); +} async function dependencies( state: State, ids: ReadonlySet, @@ -291,17 +305,13 @@ async function dependencies( ): Promise { for (const id of ids) { const edges = state.data.imports?.get(id); - if (edges?.some((edge) => edge.specifier === 'next/dist/lib/semver-noop') !== true) + if (edges === undefined) return false; + if ( + state.profile.semverSha256 !== undefined && + edges.every((edge) => edge.specifier !== SEMVER) + ) return false; - for (const edge of edges) { - if (BUILTINS.has(edge.specifier)) { - if (edge.target.replace(/^node:/u, '') !== edge.specifier) return false; - } else if ( - edge.specifier !== 'next/dist/lib/semver-noop' || - !(await semverDependency(state, edge.target, root)) - ) - return false; - } + for (const edge of edges) if (!(await dependencyEdge(state, edge, root))) return false; } return true; } @@ -314,10 +324,10 @@ async function renderedCodes( const code = state.data.rendered?.get(id); if ( code === undefined || - own.sha256 !== JWT_SHA256 || + own.sha256 !== state.profile.sha256 || !state.data.retained.has(id) || !(await commonjs(state, id, own)) || - nodeJwtGlobalRecoverySite(parseAst(code)) === undefined + state.profile.site(parseAst(code)) === undefined ) return undefined; codes.set(id, code); @@ -343,6 +353,18 @@ function graphData(graph: ManifestModuleGraph): ManifestModuleGraph { }; } +function profileOf(file: string): NodeJwtCompilerProfile | undefined { + const posix = file.split(path.sep).join('/'); + return NODE_JWT_COMPILER_PROFILES.find((profile) => posix.endsWith(profile.suffix)); +} +/** The library of the release the package is: jsonwebtoken for 16.4.3, fast-jwt for 16.4.4. */ +function releaseProfile(file: string) { + const profile = profileOf(file); + if (profile === undefined) return; + const root = file.slice(0, -profile.suffix.length); + const release = nativeNextReleaseAt(root); + return release?.version === profile.release ? { profile, release, root } : undefined; +} /** * Manually qualified PRIMARY Next16.4.3 NCC source (Git blob 33a1c8901b96d04376c1649e2f144f0c335d1762). * Entry532 exposes verify/sign/three errors/decode, never dispatcher or Lodash464. Its sole @@ -355,6 +377,12 @@ function graphData(graph: ManifestModuleGraph): ManifestModuleGraph { * REQUIRED bootstrap guard closes earlier AMD/global publication, global recovery, prototype * overrides and util.types accessors immediately before ORIGINAL CJS evaluation. Without that * guard this proof cannot authorize publication. No source is rewritten or skipped by the audit. + * + * Manually qualified Next16.4.4 NCC fast-jwt source (b175f52b05a5f6c6c39ca72565ea178542be323b), which + * replaces jsonwebtoken there. It names no eval or Function and loads buffer, node:crypto, node:util + * and util alone. Its sole Node loader read is safer-buffer's process.binding('buffer'), guarded by + * `!o.kStringMaxLength`, where o copies buffer's own fields: Node24 and Node26 export that field, so + * the read never runs, and the bootstrap guard seals process.binding regardless. */ export async function qualifyNodeJwtCompiler( input: NodeJwtCompilerInput, @@ -362,14 +390,19 @@ export async function qualifyNodeJwtCompiler( const snapshot = captureNodeManifestIdentity([input]); const data = graphData(input.graph); const candidates = [...data.sources].filter( - ([id, own]) => data.retained.has(id) && own.file.split(path.sep).join('/').endsWith(JWT_SUFFIX), + ([id, own]) => data.retained.has(id) && profileOf(own.file) !== undefined, ); if (candidates.length === 0) return undefined; const first = candidates[0]?.[1]; if (first === undefined || candidates.some(([, own]) => own.file !== first.file)) return refuse(UNSUPPORTED); const routes = approved(input); + const { profile, release, root } = releaseProfile(first.file) ?? refuse(UNSUPPORTED); + if (path.basename(root) !== 'next' || path.basename(path.dirname(root)) !== 'node_modules') + return refuse(UNSUPPORTED); + if (routes.every((route) => route.root !== root)) return refuse(UNSUPPORTED); const state: State = { + profile, input, snapshot, data, @@ -386,16 +419,12 @@ export async function qualifyNodeJwtCompiler( codes: new Map(), file: first.file, }; - const root = first.file.slice(0, -JWT_SUFFIX.length); - if (path.basename(root) !== 'next' || path.basename(path.dirname(root)) !== 'node_modules') - return refuse(UNSUPPORTED); - if (routes.every((route) => route.root !== root)) return refuse(UNSUPPORTED); - const metadata = json(await source(state, path.join(root, 'package.json'), NEXT_PACKAGE_SHA256)); - if (metadata['name'] !== 'next' || metadata['version'] !== '16.4.3') return refuse(UNSUPPORTED); - const raw = await source(state, first.file, JWT_SHA256); - if (Buffer.byteLength(raw) !== JWT_BYTES) return refuse(UNSUPPORTED); + // The release's exact package bytes, whose name and version `nativeNextReleaseAt` read. + await source(state, path.join(root, 'package.json'), release.packageJsonSha256); + const raw = await source(state, first.file, profile.sha256); + if (Buffer.byteLength(raw) !== profile.bytes) return refuse(UNSUPPORTED); const rawProgram = parseAst(raw, { sourceType: 'commonjs' }); - if (nodeJwtGlobalRecoverySite(rawProgram) === undefined) return refuse(UNSUPPORTED); + if (profile.site(rawProgram) === undefined) return refuse(UNSUPPORTED); const ids = new Set(candidates.map(([id]) => id)); for (const file of state.patched) if (ids.has(file) || file === first.file) return refuse(UNSUPPORTED); @@ -411,13 +440,16 @@ export async function qualifyNodeJwtCompiler( return proof; } -/** The ordinary closure walker receives one exact original-program node, not a module waiver. */ +/** + * The ordinary closure walker receives one exact original-program node, not a module waiver: the + * code compiler jsonwebtoken calls, or the Node loader fast-jwt reads. + */ export function nodeJwtCompilerSite( proof: NodeJwtCompilerProof | undefined, file: string, code: string, program: unknown, -): Node | undefined { +): NodeJwtCompilerSite | undefined { if (proof === undefined) return undefined; const state = PROOFS.get(proof) ?? refuse(COPIED_PROOF); if (!state.codes.has(file)) return undefined; @@ -426,7 +458,7 @@ export function nodeJwtCompilerSite( // Bind the supplied original parse, not a caller-constructed equivalent tree. if (!isNode(program) || JSON.stringify(program) !== JSON.stringify(parseAst(code))) refuse('changed audit program'); - return nodeJwtGlobalRecoverySite(program) ?? refuse('changed rendered compiler site'); + return state.profile.site(program) ?? refuse('changed rendered compiler site'); } export async function nodeJwtCompilerMetadata(proof: NodeJwtCompilerProof): Promise<{ @@ -463,10 +495,10 @@ export async function nodeJwtGuardRecord( await fresh(state); return Object.freeze({ schema: 1, - profile: 'next-16.4.3-jsonwebtoken-ncc-v1', + profile: state.profile.name, nodeVersion: nodeGuardVersion(), archivePath, - sha256: JWT_SHA256, - bytes: JWT_BYTES, + sha256: state.profile.sha256, + bytes: state.profile.bytes, }); } diff --git a/packages/adapter/src/node-jwt-fast-sites.ts b/packages/adapter/src/node-jwt-fast-sites.ts new file mode 100644 index 000000000..4df740b88 --- /dev/null +++ b/packages/adapter/src/node-jwt-fast-sites.ts @@ -0,0 +1,139 @@ +import { isNode, listOf, type Node } from './lexical-scope.ts'; +import { + declaration, + identifier, + propertyName, + reference, + visitTree, + type Visit, +} from './node-manifest-consumer-ast.ts'; + +const MAX_NODES = 100_000; +const LOADERS = new Set(['_linkedBinding', 'binding', 'dlopen', 'getBuiltinModule', 'mainModule']); +type Parents = ReadonlyMap; + +function literal(value: unknown, expected: string): boolean { + return isNode(value) && value.type === 'Literal' && value['value'] === expected; +} +function member(value: unknown, name: string): value is Node { + return ( + isNode(value) && + value.type === 'MemberExpression' && + value['computed'] !== true && + value['optional'] !== true && + propertyName(value) === name + ); +} +/** `.kStringMaxLength`, of `local` where it is given. */ +function maxLengthOf(value: unknown, local?: string): string | undefined { + if (!member(value, 'kStringMaxLength')) return undefined; + const object = value['object']; + if (!identifier(object) || (local !== undefined && object.name !== local)) return undefined; + return object.name; +} +/** A reference to one of the global `names`, which no local declares. */ +function globalReference(visit: Visit, names: readonly string[]): boolean { + const { node, scopes } = visit; + return ( + identifier(node) && + names.includes(node.name) && + reference(visit) && + declaration(node.name, scopes) === undefined + ); +} +/** Every read of a loader member of the global `process`, computed reads included. */ +function loaderReads(visits: readonly Visit[]): readonly Visit[] { + return visits.filter((visit) => { + const parent = visit.parent; + return ( + globalReference(visit, ['process']) && + parent?.type === 'MemberExpression' && + parent['object'] === visit.node && + (parent['computed'] === true || LOADERS.has(propertyName(parent) ?? '')) + ); + }); +} +/** `process.binding("buffer")`, called once with that literal alone. */ +function bufferBinding(binding: Node, parents: Parents): Node | undefined { + const call = parents.get(binding); + if (call?.type !== 'CallExpression' || call['callee'] !== binding || call['optional'] === true) + return undefined; + const args = listOf(call, 'arguments'); + return args.length === 1 && literal(args[0], 'buffer') ? call : undefined; +} +/** `.kStringMaxLength = .kStringMaxLength`, and the local it assigns. */ +function maxLengthAssignment( + call: Node, + parents: Parents, +): { readonly assignment: Node; readonly local: string } | undefined { + const read = parents.get(call); + if (read?.['object'] !== call || !member(read, 'kStringMaxLength')) return undefined; + const assignment = parents.get(read); + if ( + assignment?.type !== 'AssignmentExpression' || + assignment['operator'] !== '=' || + assignment['right'] !== read + ) + return undefined; + const local = maxLengthOf(assignment['left']); + return local === undefined ? undefined : { assignment, local }; +} +/** The assignment alone in a `try` block, with an empty `catch` and no `finally`. */ +function attemptOf(assignment: Node, parents: Parents): Node | undefined { + const statement = parents.get(assignment); + const block = statement === undefined ? undefined : parents.get(statement); + const attempt = block === undefined ? undefined : parents.get(block); + if ( + statement?.type !== 'ExpressionStatement' || + block?.type !== 'BlockStatement' || + listOf(block, 'body').length !== 1 || + attempt?.type !== 'TryStatement' || + attempt['block'] !== block || + attempt['finalizer'] !== null + ) + return undefined; + const handler = attempt['handler']; + const body = isNode(handler) ? handler['body'] : undefined; + return isNode(body) && listOf(body, 'body').length === 0 ? attempt : undefined; +} +/** That `try` alone under `if (!.kStringMaxLength)`, with no `else`. */ +function guarded(attempt: Node, local: string, parents: Parents): boolean { + let condition = parents.get(attempt); + if (condition?.type === 'BlockStatement' && listOf(condition, 'body').length === 1) + condition = parents.get(condition); + if (condition?.type !== 'IfStatement' || condition['alternate'] !== null) return false; + const test = condition['test']; + return ( + isNode(test) && + test.type === 'UnaryExpression' && + test['operator'] === '!' && + maxLengthOf(test['argument'], local) === local + ); +} + +/** + * The one Node loader read in Next.js 16.4.4's fast-jwt bundle, from the copy of safer-buffer it + * compiles in: `if (!o.kStringMaxLength) { try { o.kStringMaxLength = + * process.binding("buffer").kStringMaxLength } catch (e) {} }`, where `o` copies the fields of + * `buffer`. Node.js 24 and 26 export `kStringMaxLength` from `buffer`, so the read never runs. + * + * Shape DATA only: the `process.binding` member, where it is the bundle's only loader read and no + * code compiler is named at all. It grants nothing without the private whole-source proof. + */ +export function nodeFastJwtBindingSite(program: unknown): Node | undefined { + if (!isNode(program)) return undefined; + const visits = visitTree(program); + if (visits.length > MAX_NODES) return undefined; + if (visits.some((visit) => globalReference(visit, ['eval', 'Function']))) return undefined; + const parents: Parents = new Map(visits.map((visit) => [visit.node, visit.parent])); + const reads = loaderReads(visits); + const process = reads[0]?.node; + const binding = process === undefined ? undefined : parents.get(process); + if (reads.length !== 1 || !member(binding, 'binding')) return undefined; + const call = bufferBinding(binding, parents); + const write = call === undefined ? undefined : maxLengthAssignment(call, parents); + const attempt = write === undefined ? undefined : attemptOf(write.assignment, parents); + return attempt !== undefined && write !== undefined && guarded(attempt, write.local, parents) + ? binding + : undefined; +} diff --git a/packages/adapter/src/node-manifest-app-page-factory.ts b/packages/adapter/src/node-manifest-app-page-factory.ts index 6696af9cc..e60be9bfe 100644 --- a/packages/adapter/src/node-manifest-app-page-factory.ts +++ b/packages/adapter/src/node-manifest-app-page-factory.ts @@ -18,7 +18,7 @@ import { swcHeader, visitTree, } from './node-manifest-consumer-ast.ts'; -import { reviewedFrameworkSource } from './node-manifest-framework.ts'; +import { isReviewedFrameworkSource } from './node-manifest-framework.ts'; import { projectedTemplateExports, type ManifestTemplateExports, @@ -29,9 +29,9 @@ import { type NodeManifestTurbopackModuleId, type NodeManifestTurbopackProjection, } from './node-manifest-turbopack.ts'; +import { nativeNextReleaseAt } from './node-next-release.ts'; const TEMPLATE = 'dist/build/templates/app-page-runtime.js'; -const TEMPLATE_SHA = '0a6a5c726f95e2df2600b2fd6d71e37fd72f936f8f4dc5b94fa38d40fcd62a6b'; const CLASS_MODULE = 'dist/server/route-modules/app-page/module.compiled.js'; const CLASS_SOURCES = [ CLASS_MODULE, @@ -85,7 +85,7 @@ async function bytesOf(file: string): Promise { function constructorIdentity(file: string, sha256: string): { readonly root: string } | undefined { const normal = file.replaceAll('\\', '/'); for (const relative of CLASS_SOURCES) { - if (normal.endsWith(`/${relative}`) && reviewedFrameworkSource(relative) === sha256) + if (normal.endsWith(`/${relative}`) && isReviewedFrameworkSource(relative, sha256)) return { root: file.slice(0, -relative.length - 1) }; } return undefined; @@ -249,7 +249,8 @@ async function primaryFor( const template = await realpath(path.join(root, TEMPLATE)); if (beneath(root, template) === undefined) return refuse('template path'); const bytes = await bytesOf(template); - if (digest(bytes) !== TEMPLATE_SHA) return refuse('template bytes'); + if (nativeNextReleaseAt(root)?.source(TEMPLATE) !== digest(bytes)) + return refuse('template bytes'); return { root, template, bytes }; } function view( diff --git a/packages/adapter/src/node-manifest-coalesced-pages.ts b/packages/adapter/src/node-manifest-coalesced-pages.ts index 57e55acd9..0df93f35a 100644 --- a/packages/adapter/src/node-manifest-coalesced-pages.ts +++ b/packages/adapter/src/node-manifest-coalesced-pages.ts @@ -13,7 +13,7 @@ import { COALESCED_NAMESPACE, coalescedPagesSource } from './node-manifest-coale import { EVALUATOR, manifestConstructorImport, - reviewedFrameworkSource, + isReviewedFrameworkPath, } from './node-manifest-framework.ts'; import { nodeManifestPagesConjunctionIdentity, @@ -137,10 +137,7 @@ function privilegedImports(input: NodeManifestCoalescedPagesInput): ReadonlySet< const normalized = imported.file.replaceAll('\\', '/'); const start = normalized.lastIndexOf('/dist/'); const relative = start === -1 ? undefined : normalized.slice(start + 1); - if ( - relative === EVALUATOR || - (relative !== undefined && reviewedFrameworkSource(relative) !== undefined) - ) + if (relative === EVALUATOR || (relative !== undefined && isReviewedFrameworkPath(relative))) result.add(imported.specifier); } return result; diff --git a/packages/adapter/src/node-manifest-consumers.ts b/packages/adapter/src/node-manifest-consumers.ts index 0c255d962..d2a4646d9 100644 --- a/packages/adapter/src/node-manifest-consumers.ts +++ b/packages/adapter/src/node-manifest-consumers.ts @@ -27,11 +27,11 @@ import { type Visit, } from './node-manifest-consumer-ast.ts'; import { assertNodeManifestRuntimeInputs } from './node-manifest-runtime-inputs.ts'; +import { isReleaseSource } from './node-next-release.ts'; const PRODUCER_SUFFIX = '/dist/build/templates/app-page-runtime.js'; -// Published next@16.4.3, registry-integrity-verified source. Producer TS: -// 29bbeab09981148ade186e8599ecdede15abc281 / app-page-runtime.ts. -const PRODUCER_SHA = '0a6a5c726f95e2df2600b2fd6d71e37fd72f936f8f4dc5b94fa38d40fcd62a6b'; +// Published next@16.4.3 and next@16.4.4, registry-integrity-verified sources (`node-next-release.ts`). +const PRODUCER = 'dist/build/templates/app-page-runtime.js'; const PRODUCER_EXPORT = 'createAppPageEntrypoint'; type Field = (typeof FIELDS)[number]; @@ -98,7 +98,7 @@ class Consumer { if ( entry.kind !== 'app-page-factory' || entry.exportName !== PRODUCER_EXPORT || - entry.sha256 !== PRODUCER_SHA || + !isReleaseSource(PRODUCER, entry.sha256) || !entry.file.replaceAll('\\', '/').endsWith(PRODUCER_SUFFIX) ) refuse('unknown producer or direct class/evaluator use'); diff --git a/packages/adapter/src/node-manifest-framework.ts b/packages/adapter/src/node-manifest-framework.ts index 7a8911139..9e75ea853 100644 --- a/packages/adapter/src/node-manifest-framework.ts +++ b/packages/adapter/src/node-manifest-framework.ts @@ -1,3 +1,5 @@ +import { isReleasePath, isReleaseSource } from './node-next-release.ts'; + export const ROUTE_CLASS = 'route-class'; export const APP_PAGE_CLASS = 'app-page-class'; export const APP_PAGE_FACTORY = 'app-page-factory'; @@ -12,72 +14,40 @@ export const PAGES_API_MODULE = 'dist/server/route-modules/pages-api/module.comp const PAGES_API_SOURCE = 'dist/server/route-modules/pages-api/module.js'; export const APP_ROUTE_MODULE = 'dist/server/route-modules/app-route/module.compiled.js'; const APP_ROUTE_SOURCE = 'dist/server/route-modules/app-route/module.js'; -const PAGES_RUNTIMES: ReadonlyMap = new Map([ - [ - 'dist/compiled/next-server/pages-turbo.runtime.prod.js', - '6a6fb036d30a72fcd4cb10b4ffc13318f5b0df6a205fd3fa1ce97cda1717d9c1', - ], - [ - 'dist/compiled/next-server/pages.runtime.prod.js', - '28f90af862ee5ae750fddea94d36a55cda89e4636993569d0e60c36f2813dc98', - ], -]); -const PAGES_API_RUNTIMES: ReadonlyMap = new Map([ - [ - 'dist/compiled/next-server/pages-api-turbo.runtime.prod.js', - '9bda335900891fa996b194d830883cb833a869a86e59f33ec7c80cf018309424', - ], - [ - 'dist/compiled/next-server/pages-api.runtime.prod.js', - '322a597b07fdc559b86562fd68f1c828b554deac36e1e84a10e6687daa01a00f', - ], -]); -const ROUTE_RUNTIMES: ReadonlyMap = new Map([ - [ - 'dist/compiled/next-server/app-route-experimental.runtime.prod.js', - '356d9c9be870fac47a341221841c76341c37a17eb038d6151e22a0788f87a906', - ], - [ - 'dist/compiled/next-server/app-route-turbo-experimental.runtime.prod.js', - '4755bc19f3115dc66c2e818cf7cee5c0979a6821a5fec9513beb808cebe8a0bc', - ], - [ - 'dist/compiled/next-server/app-route-turbo.runtime.prod.js', - '4010a14b45b58154eaf01034845c5003de0f7c3945657e9d84290fe66ea3d41c', - ], - [ - 'dist/compiled/next-server/app-route.runtime.prod.js', - 'ff6aea920fbe3a9a4cf4179b0bc80bf20127136754cd9225586edb77f96dc254', - ], -]); +const RUNTIME = 'dist/compiled/next-server/'; +const PAGES_RUNTIME_PATHS = [ + `${RUNTIME}pages-turbo.runtime.prod.js`, + `${RUNTIME}pages.runtime.prod.js`, +]; +const PAGES_API_RUNTIME_PATHS = [ + `${RUNTIME}pages-api-turbo.runtime.prod.js`, + `${RUNTIME}pages-api.runtime.prod.js`, +]; +const ROUTE_RUNTIME_PATHS = [ + `${RUNTIME}app-route-experimental.runtime.prod.js`, + `${RUNTIME}app-route-turbo-experimental.runtime.prod.js`, + `${RUNTIME}app-route-turbo.runtime.prod.js`, + `${RUNTIME}app-route.runtime.prod.js`, +]; +const APP_RUNTIME_PATHS = [ + `${RUNTIME}app-page-experimental.runtime.prod.js`, + `${RUNTIME}app-page-turbo-experimental.runtime.prod.js`, + `${RUNTIME}app-page-turbo.runtime.prod.js`, + `${RUNTIME}app-page.runtime.prod.js`, +]; +const PAGES_RUNTIMES: ReadonlySet = new Set(PAGES_RUNTIME_PATHS); +const PAGES_API_RUNTIMES: ReadonlySet = new Set(PAGES_API_RUNTIME_PATHS); +const ROUTE_RUNTIMES: ReadonlySet = new Set(ROUTE_RUNTIME_PATHS); +const APP_RUNTIMES: ReadonlySet = new Set(APP_RUNTIME_PATHS); export const EVALUATOR = 'dist/server/load-manifest.external.js'; const LOAD_COMPONENTS = 'dist/server/load-components.js'; -const APP_RUNTIMES: ReadonlyMap = new Map([ - [ - 'dist/compiled/next-server/app-page-experimental.runtime.prod.js', - '2974a9e688b53a8b9bf61ea8535f1b70f4fb4154fc60c64a231d337f556d99c7', - ], - [ - 'dist/compiled/next-server/app-page-turbo-experimental.runtime.prod.js', - 'd0d80c2b545d96bfdebb2a3d5100c368a18a0757db211eba55abf977e37ee23f', - ], - [ - 'dist/compiled/next-server/app-page-turbo.runtime.prod.js', - '914353e0cf478e617e8a3b408fa74d9f526e9c338c8bda817cc05f05bfcce7a5', - ], - [ - 'dist/compiled/next-server/app-page.runtime.prod.js', - '9f24cd66e38f57c3144cbc456fe0f52853836d6218f449f54abba8e8c17c3921', - ], -]); -// Exact published next@16.4.3 sources, from its integrity-verified registry package. +/** + * Exact published sources, the same bytes in next@16.4.3 and next@16.4.4, from their + * integrity-verified registry packages. The factory template and the runtimes differ between them + * (`node-next-release.ts`). + */ const REVIEWED: ReadonlyMap = new Map([ - ...APP_RUNTIMES, - ...PAGES_RUNTIMES, - ...PAGES_API_RUNTIMES, - ...ROUTE_RUNTIMES, [APP_PAGE_MODULE, '3b55f56f8c70bbadffdacea23a1c19689c3889bab7e78da6c8121ada787bfe99'], - [APP_PAGE_RUNTIME, '0a6a5c726f95e2df2600b2fd6d71e37fd72f936f8f4dc5b94fa38d40fcd62a6b'], [APP_PAGE_SOURCE, '0d658c55994938d4ffe32bbddb4e6009513248e1659e90e248d33bf3a98e0eed'], [APP_ROUTE_MODULE, 'aeab4882c08e62a742aa67daa0d4a434b3330f389c38ffdf4262b6143c80b5ed'], [APP_ROUTE_SOURCE, '9f9ae9defd45e773888c948e844d7a401ca79ff5c09f14211c4abd9b725e3b1b'], @@ -95,7 +65,7 @@ export type ManifestProtectedKind = | typeof APP_PAGE_FACTORY | typeof ROUTE_EXPORTS; -const RUNTIME_WRAPPERS: ReadonlyMap> = new Map([ +const RUNTIME_WRAPPERS: ReadonlyMap> = new Map([ [APP_PAGE_MODULE, APP_RUNTIMES], [APP_ROUTE_MODULE, ROUTE_RUNTIMES], [PAGES_API_MODULE, PAGES_API_RUNTIMES], @@ -103,11 +73,11 @@ const RUNTIME_WRAPPERS: ReadonlyMap> = new M ]); // eslint-disable-next-line unicorn/prefer-iterator-concat -- The supported Node.js 24 runtime does not provide Iterator.concat. -const RUNTIMES = new Set([ - ...APP_RUNTIMES.keys(), - ...PAGES_RUNTIMES.keys(), - ...PAGES_API_RUNTIMES.keys(), - ...ROUTE_RUNTIMES.keys(), +const RUNTIMES: ReadonlySet = new Set([ + ...APP_RUNTIME_PATHS, + ...PAGES_RUNTIME_PATHS, + ...PAGES_API_RUNTIME_PATHS, + ...ROUTE_RUNTIME_PATHS, ]); const FRAMEWORK_TRANSITIONS: ReadonlyMap< string, @@ -147,8 +117,14 @@ export function frameworkParent(parent: string, child: string): ManifestProtecte return FRAMEWORK_TRANSITIONS.get(child)?.get(parent); } -export function reviewedFrameworkSource(relative: string): string | undefined { - return REVIEWED.get(relative); +/** Whether `sha256` is the published bytes of the reviewed `relative` in a supported release. */ +export function isReviewedFrameworkSource(relative: string, sha256: string): boolean { + return REVIEWED.get(relative) === sha256 || isReleaseSource(relative, sha256); +} + +/** Whether `relative` names a reviewed source of a supported release. */ +export function isReviewedFrameworkPath(relative: string): boolean { + return REVIEWED.has(relative) || isReleasePath(relative); } export interface ManifestConstructorIdentity { @@ -171,10 +147,11 @@ export function reviewedRouteConstructorSource( sha256: string, ): ManifestConstructorIdentity | undefined { for (const profile of ROUTE_CONSTRUCTORS) { - const expected = - relative === profile.module ? REVIEWED.get(profile.module) : profile.runtimes.get(relative); - if (expected !== undefined && expected === sha256) - return Object.freeze({ kind: profile.kind, module: profile.module, relative }); + const reviewed = + relative === profile.module + ? REVIEWED.get(profile.module) === sha256 + : profile.runtimes.has(relative) && isReleaseSource(relative, sha256); + if (reviewed) return Object.freeze({ kind: profile.kind, module: profile.module, relative }); } return undefined; } @@ -185,7 +162,7 @@ export function manifestConstructorImport( ): (ManifestConstructorIdentity & { readonly root: string }) | undefined { const normalized = file.replaceAll('\\', '/'); for (const profile of ROUTE_CONSTRUCTORS) { - const candidates = [profile.module, ...profile.runtimes.keys()]; + const candidates = [profile.module, ...profile.runtimes]; for (const relative of candidates) { if (!normalized.endsWith(`/${relative}`)) continue; const identity = reviewedRouteConstructorSource(relative, sha256); diff --git a/packages/adapter/src/node-manifest-graph.ts b/packages/adapter/src/node-manifest-graph.ts index c2272651a..2c14089a2 100644 --- a/packages/adapter/src/node-manifest-graph.ts +++ b/packages/adapter/src/node-manifest-graph.ts @@ -21,7 +21,7 @@ import { APP_PAGE_CLASS, EVALUATOR, frameworkParent, - reviewedFrameworkSource, + isReviewedFrameworkSource, reviewedRouteConstructorSource, ROUTE_CLASS, ROUTE_EXPORTS, @@ -104,10 +104,10 @@ interface ProtectedModule { function turbopackKind(child: ProtectedModule, sha256: string): SealedRoute['kind'] | undefined { // Class authority remains a distinct role until a whole fixed producer body is certified. if (child.kind === APP_PAGE_CLASS) - return reviewedFrameworkSource(child.relative) === sha256 ? 'app-page' : undefined; + return isReviewedFrameworkSource(child.relative, sha256) ? 'app-page' : undefined; if (child.kind === APP_PAGE_FACTORY) { return child.relative === 'dist/build/templates/app-page-runtime.js' && - reviewedFrameworkSource(child.relative) === sha256 + isReviewedFrameworkSource(child.relative, sha256) ? 'app-page' : undefined; } @@ -202,7 +202,7 @@ class ConsumerGraph { const relative = path.relative(this.root, source.file).split(path.sep).join('/'); const kind = frameworkParent(relative, child.relative); if (kind !== undefined) { - if (reviewedFrameworkSource(relative) !== source.sha256) refuse('unknown or changed caller'); + if (!isReviewedFrameworkSource(relative, source.sha256)) refuse('unknown or changed caller'); this.files.set(source.file, source); return { id: parent, relative, kind }; } @@ -398,7 +398,7 @@ class ConsumerGraph { this.complete.add(child.id); if (child.kind === ROUTE_CLASS || child.kind === APP_PAGE_CLASS) { const source = sourceOf(this.graph, child.id); - if (reviewedFrameworkSource(child.relative) !== source.sha256) + if (!isReviewedFrameworkSource(child.relative, source.sha256)) refuse('changed completed route source'); this.routeSources.set( source.file, diff --git a/packages/adapter/src/node-manifest-handler-catalog.ts b/packages/adapter/src/node-manifest-handler-catalog.ts index 07f3db065..bfa2a6018 100644 --- a/packages/adapter/src/node-manifest-handler-catalog.ts +++ b/packages/adapter/src/node-manifest-handler-catalog.ts @@ -2,7 +2,9 @@ import type { NodeManifestHandlerLiteralSlot, NodeManifestHandlerReferenceGraphData, } from './node-manifest-handler-graph.ts'; +import { NEXT_16_4_4_HANDLER_REFERENCE } from './node-manifest-handler-reference-16-4-4.ts'; import { NEXT_16_4_3_HANDLER_REFERENCE } from './node-manifest-handler-reference.ts'; +import { NEXT_16_4_4_UNMINIFIED_HANDLER_REFERENCE } from './node-manifest-handler-unminified-16-4-4-reference.ts'; import { NEXT_16_4_3_UNMINIFIED_HANDLER_REFERENCE } from './node-manifest-handler-unminified-reference.ts'; export const NODE_MANIFEST_HANDLER_MODULE = 'dist/server/route-modules/pages/pages-handler.js'; @@ -43,4 +45,6 @@ function immutable(value: T): T { export const NODE_MANIFEST_HANDLER_PROFILES: readonly NodeManifestHandlerProfile[] = immutable([ NEXT_16_4_3_HANDLER_REFERENCE, NEXT_16_4_3_UNMINIFIED_HANDLER_REFERENCE, + NEXT_16_4_4_HANDLER_REFERENCE, + NEXT_16_4_4_UNMINIFIED_HANDLER_REFERENCE, ]); diff --git a/packages/adapter/src/node-manifest-handler-reference-16-4-4-factories.ts b/packages/adapter/src/node-manifest-handler-reference-16-4-4-factories.ts new file mode 100644 index 000000000..86eb76436 --- /dev/null +++ b/packages/adapter/src/node-manifest-handler-reference-16-4-4-factories.ts @@ -0,0 +1,308 @@ +/** Next.js 16.4.4: fixed reference DATA from the marker-free, independently compiled public Pages fixture. */ +import type { NodeManifestWholeFactoryData } from './node-manifest-whole-factory-data.ts'; + +export const HANDLER_REFERENCE_IDS_16_4_4 = { + handler: 4651, + hoist: 7511, + routeKind: 6084, + noFallback: 3695, + fresh: 7284, + path: 4747, + stream: 1111, + escape: 3859, + telemetry: 4309, + telemetryExternal: 406, +}; + +export const HANDLER_REFERENCE_FACTORIES_16_4_4: Readonly< + Record> +> = { + handler: { + version: 1, + originalSha256: '01419aace9d59237fd817479d45ed3ba99e18e4fa48313ae618d4d9e6b8704e7', + factory: { + ids: [ + HANDLER_REFERENCE_IDS_16_4_4.routeKind, + HANDLER_REFERENCE_IDS_16_4_4.hoist, + HANDLER_REFERENCE_IDS_16_4_4.handler, + ], + start: 6306, + end: 40_437, + }, + strictContext: { + effective: true, + scopes: [ + { + type: 'Program', + directives: [], + }, + { + type: 'ArrowFunctionExpression', + directives: [ + { + raw: '"use strict"', + value: 'use strict', + }, + ], + }, + ], + }, + ports: [ + { + ordinal: 0, + id: HANDLER_REFERENCE_IDS_16_4_4.telemetryExternal, + idType: 'number', + occurrences: [ + { + kind: 'require', + start: 11_447, + end: 11_455, + }, + ], + }, + { + ordinal: 1, + id: HANDLER_REFERENCE_IDS_16_4_4.telemetry, + idType: 'number', + occurrences: [ + { + kind: 'require', + start: 11_467, + end: 11_476, + }, + ], + }, + { + ordinal: 2, + id: HANDLER_REFERENCE_IDS_16_4_4.escape, + idType: 'number', + occurrences: [ + { + kind: 'import', + start: 22_603, + end: 22_612, + }, + ], + }, + { + ordinal: 3, + id: HANDLER_REFERENCE_IDS_16_4_4.stream, + idType: 'number', + occurrences: [ + { + kind: 'require', + start: 26_360, + end: 26_369, + }, + { + kind: 'require', + start: 26_618, + end: 26_627, + }, + ], + }, + { + ordinal: 4, + id: HANDLER_REFERENCE_IDS_16_4_4.path, + idType: 'number', + occurrences: [ + { + kind: 'import', + start: 29_431, + end: 29_440, + }, + ], + }, + { + ordinal: 5, + id: HANDLER_REFERENCE_IDS_16_4_4.fresh, + idType: 'number', + occurrences: [ + { + kind: 'import', + start: 29_444, + end: 29_453, + }, + ], + }, + { + ordinal: 6, + id: HANDLER_REFERENCE_IDS_16_4_4.noFallback, + idType: 'number', + occurrences: [ + { + kind: 'import', + start: 30_458, + end: 30_467, + }, + ], + }, + ], + }, + noFallback: { + version: 1, + originalSha256: '01419aace9d59237fd817479d45ed3ba99e18e4fa48313ae618d4d9e6b8704e7', + factory: { + ids: [HANDLER_REFERENCE_IDS_16_4_4.noFallback], + start: 83, + end: 227, + }, + strictContext: { + effective: false, + scopes: [ + { + type: 'Program', + directives: [], + }, + { + type: 'ArrowFunctionExpression', + directives: [], + }, + ], + }, + ports: [], + }, + fresh: { + version: 1, + originalSha256: '01419aace9d59237fd817479d45ed3ba99e18e4fa48313ae618d4d9e6b8704e7', + factory: { + ids: [HANDLER_REFERENCE_IDS_16_4_4.fresh], + start: 5237, + end: 6290, + }, + strictContext: { + effective: false, + scopes: [ + { + type: 'Program', + directives: [], + }, + { + type: 'ArrowFunctionExpression', + directives: [], + }, + ], + }, + ports: [], + }, + path: { + version: 1, + originalSha256: 'b5c9dc82fec7831e8999540a43771024e2a023f671f7c5e3b39b07d573732961', + factory: { + ids: [HANDLER_REFERENCE_IDS_16_4_4.path], + start: 310, + end: 362, + }, + strictContext: { + effective: false, + scopes: [ + { + type: 'Program', + directives: [], + }, + { + type: 'ArrowFunctionExpression', + directives: [], + }, + ], + }, + ports: [], + }, + stream: { + version: 1, + originalSha256: '01419aace9d59237fd817479d45ed3ba99e18e4fa48313ae618d4d9e6b8704e7', + factory: { + ids: [HANDLER_REFERENCE_IDS_16_4_4.stream], + start: 233, + end: 299, + }, + strictContext: { + effective: false, + scopes: [ + { + type: 'Program', + directives: [], + }, + { + type: 'ArrowFunctionExpression', + directives: [], + }, + ], + }, + ports: [], + }, + escape: { + version: 1, + originalSha256: '01419aace9d59237fd817479d45ed3ba99e18e4fa48313ae618d4d9e6b8704e7', + factory: { + ids: [HANDLER_REFERENCE_IDS_16_4_4.escape], + start: 305, + end: 5231, + }, + strictContext: { + effective: true, + scopes: [ + { + type: 'Program', + directives: [], + }, + { + type: 'ArrowFunctionExpression', + directives: [ + { + raw: '"use strict"', + value: 'use strict', + }, + ], + }, + ], + }, + ports: [], + }, + telemetry: { + version: 1, + originalSha256: 'b5c9dc82fec7831e8999540a43771024e2a023f671f7c5e3b39b07d573732961', + factory: { + ids: [HANDLER_REFERENCE_IDS_16_4_4.telemetry], + start: 368, + end: 24_653, + }, + strictContext: { + effective: false, + scopes: [ + { + type: 'Program', + directives: [], + }, + { + type: 'ArrowFunctionExpression', + directives: [], + }, + ], + }, + ports: [], + }, + telemetryExternal: { + version: 1, + originalSha256: 'b5c9dc82fec7831e8999540a43771024e2a023f671f7c5e3b39b07d573732961', + factory: { + ids: [HANDLER_REFERENCE_IDS_16_4_4.telemetryExternal], + start: 20, + end: 138, + }, + strictContext: { + effective: false, + scopes: [ + { + type: 'Program', + directives: [], + }, + { + type: 'ArrowFunctionExpression', + directives: [], + }, + ], + }, + ports: [], + }, +}; diff --git a/packages/adapter/src/node-manifest-handler-reference-16-4-4-graph.ts b/packages/adapter/src/node-manifest-handler-reference-16-4-4-graph.ts new file mode 100644 index 000000000..1ac908a8d --- /dev/null +++ b/packages/adapter/src/node-manifest-handler-reference-16-4-4-graph.ts @@ -0,0 +1,167 @@ +/** Next.js 16.4.4: fixed reference DATA from the marker-free, independently compiled public Pages fixture. */ +import type { + NodeManifestHandlerFingerprintRowData, + NodeManifestHandlerImportData, + NodeManifestHandlerReferenceGraphData, + TargetData, +} from './node-manifest-handler-graph.ts'; +import { + HANDLER_REFERENCE_FACTORIES_16_4_4, + HANDLER_REFERENCE_IDS_16_4_4, +} from './node-manifest-handler-reference-16-4-4-factories.ts'; + +const HANDLER_REFERENCE_RUNTIME = 'next-16.4.4-public-pages'; +const streamSpecifier = 'node:stream'; +const nextRoot = + '/work/node_modules/.pnpm/next@16.4.4_@types+node@24.19.2_react-dom@19.3.1_react@19.3.1__react@19.3.1/node_modules/next'; + +function internal(kind: 'import' | 'require', moduleId: number): NodeManifestHandlerImportData { + return { + kind, + specifier: `turbopack:${moduleId}`, + targetId: moduleId, + target: { kind: 'module', runtime: HANDLER_REFERENCE_RUNTIME, moduleId }, + }; +} +function external(specifier: string, target: TargetData): NodeManifestHandlerImportData { + return { kind: 'external', specifier, target }; +} +function source(relative: string, sha256: string): TargetData { + return { kind: 'next-source', root: nextRoot, relative, file: `${nextRoot}/${relative}`, sha256 }; +} +function row( + factoryName: string, + moduleId: number, + canonicalSha256: string, + imports: readonly NodeManifestHandlerImportData[], +): NodeManifestHandlerFingerprintRowData { + const whole = HANDLER_REFERENCE_FACTORIES_16_4_4[factoryName]; + if (whole?.factory.ids.includes(moduleId) !== true) + throw new Error('Invalid fixed handler reference row'); + return { + mode: 'fingerprint', + runtime: HANDLER_REFERENCE_RUNTIME, + moduleId, + physicalId: whole.originalSha256, + canonicalSha256, + whole: { ...whole, selectedOrdinal: whole.factory.ids.indexOf(moduleId) }, + imports, + operations: { cacheInitializers: [], asyncLoads: [], boundRuntimeInputs: [] }, + }; +} + +export const HANDLER_REFERENCE_GRAPH_16_4_4: NodeManifestHandlerReferenceGraphData = { + root: { runtime: HANDLER_REFERENCE_RUNTIME, moduleId: HANDLER_REFERENCE_IDS_16_4_4.handler }, + nextRoot, + rows: [ + row( + 'handler', + HANDLER_REFERENCE_IDS_16_4_4.handler, + 'bc9ce822e11fdc156f44e26eaab52c0c5d31a1583dec61464f0854443cb90269', + [ + internal('import', HANDLER_REFERENCE_IDS_16_4_4.noFallback), + internal('import', HANDLER_REFERENCE_IDS_16_4_4.fresh), + internal('import', HANDLER_REFERENCE_IDS_16_4_4.path), + internal('require', HANDLER_REFERENCE_IDS_16_4_4.stream), + internal('require', HANDLER_REFERENCE_IDS_16_4_4.stream), + internal('import', HANDLER_REFERENCE_IDS_16_4_4.escape), + internal('require', HANDLER_REFERENCE_IDS_16_4_4.telemetry), + internal('require', HANDLER_REFERENCE_IDS_16_4_4.telemetryExternal), + ], + ), + row( + 'handler', + HANDLER_REFERENCE_IDS_16_4_4.hoist, + 'b50118e7ebe01c0773b0dcede63bcb75e8b8451d029b0b879603c23b1669e6fa', + [ + internal('import', HANDLER_REFERENCE_IDS_16_4_4.noFallback), + internal('import', HANDLER_REFERENCE_IDS_16_4_4.fresh), + internal('import', HANDLER_REFERENCE_IDS_16_4_4.path), + internal('require', HANDLER_REFERENCE_IDS_16_4_4.stream), + internal('require', HANDLER_REFERENCE_IDS_16_4_4.stream), + internal('import', HANDLER_REFERENCE_IDS_16_4_4.escape), + internal('require', HANDLER_REFERENCE_IDS_16_4_4.telemetry), + internal('require', HANDLER_REFERENCE_IDS_16_4_4.telemetryExternal), + ], + ), + row( + 'handler', + HANDLER_REFERENCE_IDS_16_4_4.routeKind, + 'f5cce7e923cce033cafbb7e6928af995dc7b0cca34514043ea14b9969a63933e', + [ + internal('import', HANDLER_REFERENCE_IDS_16_4_4.noFallback), + internal('import', HANDLER_REFERENCE_IDS_16_4_4.fresh), + internal('import', HANDLER_REFERENCE_IDS_16_4_4.path), + internal('require', HANDLER_REFERENCE_IDS_16_4_4.stream), + internal('require', HANDLER_REFERENCE_IDS_16_4_4.stream), + internal('import', HANDLER_REFERENCE_IDS_16_4_4.escape), + internal('require', HANDLER_REFERENCE_IDS_16_4_4.telemetry), + internal('require', HANDLER_REFERENCE_IDS_16_4_4.telemetryExternal), + ], + ), + row( + 'noFallback', + HANDLER_REFERENCE_IDS_16_4_4.noFallback, + 'd0c9bb47bd09769057828ccce13c8b5669f8437e5e10ae78bceb95648631a7bc', + [ + external( + 'next/dist/shared/lib/no-fallback-error.external.js', + source( + 'dist/shared/lib/no-fallback-error.external.js', + '2e641c2ce7efe466d03751134fefec1cfd752bb00060fc7273d6d6d26e723392', + ), + ), + ], + ), + row( + 'fresh', + HANDLER_REFERENCE_IDS_16_4_4.fresh, + '69ba72a2952da2213d8881bb18a78879c3d58995ecbe59a7c1dfb5c9bdeb7cc4', + [], + ), + row( + 'path', + HANDLER_REFERENCE_IDS_16_4_4.path, + 'c312d717beb20459ca0ca249fc054036cae236296c3d18ca34c7c8d37984bb21', + [external('path', { kind: 'builtin', id: 'path', aliases: ['path', 'node:path'] })], + ), + row( + 'stream', + HANDLER_REFERENCE_IDS_16_4_4.stream, + 'c8cf04f72cc181c5de0fba6cfef393a4ec2a4727f9e89f1bdb0cdc5f8512d293', + [ + external(streamSpecifier, { + kind: 'builtin', + id: streamSpecifier, + aliases: [streamSpecifier, 'stream'], + }), + ], + ), + row( + 'escape', + HANDLER_REFERENCE_IDS_16_4_4.escape, + 'e62cc46b7145bbef2bc4af4b0833c588175e849bd877d702305546c970a25af5', + [], + ), + row( + 'telemetry', + HANDLER_REFERENCE_IDS_16_4_4.telemetry, + '4d9ee502852ca6d8c01e500cf31a9a2d1b4e33a51b1048eb5b3a8dfd98f3711c', + [], + ), + row( + 'telemetryExternal', + HANDLER_REFERENCE_IDS_16_4_4.telemetryExternal, + '3c59b282e8ec6fc0784683abd8e94ebc952015f6a2988b0b8cbf8d4502d706c1', + [ + external( + 'next/dist/compiled/@opentelemetry/api', + source( + 'dist/compiled/@opentelemetry/api/index.js', + '79cdf1ce026102bf94b8b9beb667d8c8635d0f702c2f118f441699684e92c2de', + ), + ), + ], + ), + ], +}; diff --git a/packages/adapter/src/node-manifest-handler-reference-16-4-4.ts b/packages/adapter/src/node-manifest-handler-reference-16-4-4.ts new file mode 100644 index 000000000..8d672105e --- /dev/null +++ b/packages/adapter/src/node-manifest-handler-reference-16-4-4.ts @@ -0,0 +1,104 @@ +import type { NodeManifestHandlerProfile } from './node-manifest-handler-catalog.ts'; +/** + * Next.js 16.4.4 getHandler reference: whole factories, never an export-name shortcut. + * The public fixture has no SDK import, marker, or custom Turbopack configuration. + * https://github.com/arkorlab/upwind/actions/runs/41623398928 + * Two offline Linux builds from the fixed image and the 16.4.4 lock produced identical source bytes. + * Reference artifact ZIP SHA-256: f74eb2871029897c34e73abfa9582330a7469c90b35f711a0b8362bd372fc62b. + * An agent wrote this data from that artifact at the owner's request, an exception to the + * producer README. The same steps first regenerated the 16.4.3 data from that run's 16.4.3 + * artifact, byte for byte. + * Only the two exact NCC __dirname + '/' expressions have input-relative literal slots. + * Their original published source pins and canonical AST paths are fixed below; every + * other statement, directive, port, alias, and effect stays in the whole-factory digest. + * Original source: https://github.com/vercel/next.js/tree/b175f52b05a5f6c6c39ca72565ea178542be323b + */ +import { HANDLER_REFERENCE_IDS_16_4_4 } from './node-manifest-handler-reference-16-4-4-factories.ts'; +import { HANDLER_REFERENCE_GRAPH_16_4_4 } from './node-manifest-handler-reference-16-4-4-graph.ts'; + +const freshAssignmentOrdinal = 3; +const telemetryAssignmentOrdinal = 4; + +export const NEXT_16_4_4_HANDLER_REFERENCE: NodeManifestHandlerProfile = { + version: 1, + name: 'next-16.4.4-public-pages-handler', + exportName: 'getHandler', + declaredExport: 'f', + reference: HANDLER_REFERENCE_GRAPH_16_4_4, + sources: [ + { + relative: 'dist/shared/lib/no-fallback-error.external.js', + sha256: '2e641c2ce7efe466d03751134fefec1cfd752bb00060fc7273d6d6d26e723392', + }, + { + relative: 'dist/compiled/@opentelemetry/api/index.js', + sha256: '79cdf1ce026102bf94b8b9beb667d8c8635d0f702c2f118f441699684e92c2de', + }, + { + relative: 'dist/compiled/fresh/index.js', + sha256: '9cf9478dc7648e4ba9a973dcd928bd4a87600edb5c02f0eaf67dd42ae845da8e', + }, + { + relative: 'dist/server/route-modules/pages/pages-handler.js', + sha256: 'b20671fb66060f58b98fe7677adf844d10697a97b463eb2390eb00831500bda1', + }, + ], + literalSlots: [ + { + key: 'next-dir:dist/compiled/fresh/index.js', + row: { + runtime: 'next-16.4.4-public-pages', + moduleId: HANDLER_REFERENCE_IDS_16_4_4.fresh, + }, + path: [ + 'callback', + 'body', + 'body', + 0, + 'expression', + 'callee', + 'body', + 'body', + freshAssignmentOrdinal, + 'expression', + 'expressions', + 0, + 'right', + 'value', + ], + referenceValue: + '/ROOT/node_modules/.pnpm/next@16.4.4_@types+node@24.19.2_react-dom@19.3.1_react@19.3.1__react@19.3.1/node_modules/next/dist/compiled/fresh/', + origin: { + kind: 'compiler-relative-next-source-directory-with-trailing-slash', + relative: 'dist/compiled/fresh/index.js', + }, + }, + { + key: 'next-dir:dist/compiled/@opentelemetry/api/index.js', + row: { + runtime: 'next-16.4.4-public-pages', + moduleId: HANDLER_REFERENCE_IDS_16_4_4.telemetry, + }, + path: [ + 'callback', + 'body', + 'body', + 0, + 'expression', + 'callee', + 'body', + 'body', + telemetryAssignmentOrdinal, + 'expression', + 'right', + 'value', + ], + referenceValue: + '/ROOT/node_modules/.pnpm/next@16.4.4_@types+node@24.19.2_react-dom@19.3.1_react@19.3.1__react@19.3.1/node_modules/next/dist/compiled/@opentelemetry/api/', + origin: { + kind: 'compiler-relative-next-source-directory-with-trailing-slash', + relative: 'dist/compiled/@opentelemetry/api/index.js', + }, + }, + ], +}; diff --git a/packages/adapter/src/node-manifest-handler-unminified-16-4-4-factories.ts b/packages/adapter/src/node-manifest-handler-unminified-16-4-4-factories.ts new file mode 100644 index 000000000..fc66ac2e9 --- /dev/null +++ b/packages/adapter/src/node-manifest-handler-unminified-16-4-4-factories.ts @@ -0,0 +1,359 @@ +/** Next.js 16.4.4: fixed whole-factory DATA from two clean offline public server-minification-off builds. */ +import type { NodeManifestWholeFactoryData } from './node-manifest-whole-factory-data.ts'; + +export const HANDLER_UNMINIFIED_IDS_16_4_4 = { + handler: 4651, + hoist: 7511, + routeKind: 6084, + noFallback: 3695, + fresh: 7284, + path: 4747, + stream: 1111, + escape: 3859, + telemetry: 4309, + telemetryExternal: 406, + crypto: 4799, +}; + +export const HANDLER_UNMINIFIED_FACTORIES_16_4_4: Readonly< + Record> +> = { + handler: { + version: 1, + originalSha256: '0d1c836fd1208b1b9e935c5aedfad00b82f45c90471e50bf17b7bb5e2ffc3b60', + factory: { + ids: [ + HANDLER_UNMINIFIED_IDS_16_4_4.routeKind, + HANDLER_UNMINIFIED_IDS_16_4_4.hoist, + HANDLER_UNMINIFIED_IDS_16_4_4.handler, + ], + start: 15_809, + end: 236_852, + }, + strictContext: { + effective: true, + scopes: [ + { + type: 'Program', + directives: [], + }, + { + type: 'ArrowFunctionExpression', + directives: [ + { + raw: '"use strict"', + value: 'use strict', + }, + ], + }, + ], + }, + ports: [ + { + ordinal: 0, + id: HANDLER_UNMINIFIED_IDS_16_4_4.telemetryExternal, + idType: 'number', + occurrences: [ + { + kind: 'require', + start: 28_792, + end: 28_820, + }, + ], + }, + { + ordinal: 1, + id: HANDLER_UNMINIFIED_IDS_16_4_4.telemetry, + idType: 'number', + occurrences: [ + { + kind: 'require', + start: 28_856, + end: 28_885, + }, + ], + }, + { + ordinal: 2, + id: HANDLER_UNMINIFIED_IDS_16_4_4.stream, + idType: 'number', + occurrences: [ + { + kind: 'require', + start: 76_713, + end: 76_742, + }, + { + kind: 'require', + start: 77_397, + end: 77_426, + }, + { + kind: 'require', + start: 150_991, + end: 151_020, + }, + { + kind: 'require', + start: 152_277, + end: 152_306, + }, + ], + }, + { + ordinal: 3, + id: HANDLER_UNMINIFIED_IDS_16_4_4.escape, + idType: 'number', + occurrences: [ + { + kind: 'import', + start: 132_254, + end: 132_283, + }, + ], + }, + { + ordinal: 4, + id: HANDLER_UNMINIFIED_IDS_16_4_4.path, + idType: 'number', + occurrences: [ + { + kind: 'import', + start: 190_744, + end: 190_773, + }, + { + kind: 'require', + start: 196_410, + end: 196_439, + }, + ], + }, + { + ordinal: 5, + id: HANDLER_UNMINIFIED_IDS_16_4_4.fresh, + idType: 'number', + occurrences: [ + { + kind: 'import', + start: 191_986, + end: 192_015, + }, + ], + }, + { + ordinal: 6, + id: HANDLER_UNMINIFIED_IDS_16_4_4.noFallback, + idType: 'number', + occurrences: [ + { + kind: 'import', + start: 193_978, + end: 194_007, + }, + ], + }, + { + ordinal: 7, + id: HANDLER_UNMINIFIED_IDS_16_4_4.crypto, + idType: 'number', + occurrences: [ + { + kind: 'require', + start: 204_953, + end: 204_982, + }, + ], + }, + ], + }, + noFallback: { + version: 1, + originalSha256: '0d1c836fd1208b1b9e935c5aedfad00b82f45c90471e50bf17b7bb5e2ffc3b60', + factory: { + ids: [HANDLER_UNMINIFIED_IDS_16_4_4.noFallback], + start: 177, + end: 406, + }, + strictContext: { + effective: false, + scopes: [ + { + type: 'Program', + directives: [], + }, + { + type: 'ArrowFunctionExpression', + directives: [], + }, + ], + }, + ports: [], + }, + fresh: { + version: 1, + originalSha256: '0d1c836fd1208b1b9e935c5aedfad00b82f45c90471e50bf17b7bb5e2ffc3b60', + factory: { + ids: [HANDLER_UNMINIFIED_IDS_16_4_4.fresh], + start: 12_463, + end: 15_785, + }, + strictContext: { + effective: false, + scopes: [ + { + type: 'Program', + directives: [], + }, + { + type: 'ArrowFunctionExpression', + directives: [], + }, + ], + }, + ports: [], + }, + path: { + version: 1, + originalSha256: '3c4327fdf5df7c1e19a5963e8ed3b8cde2cb1f1bd6c9b097225e45e0d7549dfb', + factory: { + ids: [HANDLER_UNMINIFIED_IDS_16_4_4.path], + start: 493, + end: 630, + }, + strictContext: { + effective: false, + scopes: [ + { + type: 'Program', + directives: [], + }, + { + type: 'ArrowFunctionExpression', + directives: [], + }, + ], + }, + ports: [], + }, + stream: { + version: 1, + originalSha256: '0d1c836fd1208b1b9e935c5aedfad00b82f45c90471e50bf17b7bb5e2ffc3b60', + factory: { + ids: [HANDLER_UNMINIFIED_IDS_16_4_4.stream], + start: 416, + end: 567, + }, + strictContext: { + effective: false, + scopes: [ + { + type: 'Program', + directives: [], + }, + { + type: 'ArrowFunctionExpression', + directives: [], + }, + ], + }, + ports: [], + }, + escape: { + version: 1, + originalSha256: '0d1c836fd1208b1b9e935c5aedfad00b82f45c90471e50bf17b7bb5e2ffc3b60', + factory: { + ids: [HANDLER_UNMINIFIED_IDS_16_4_4.escape], + start: 577, + end: 12_453, + }, + strictContext: { + effective: true, + scopes: [ + { + type: 'Program', + directives: [], + }, + { + type: 'ArrowFunctionExpression', + directives: [ + { + raw: '"use strict"', + value: 'use strict', + }, + ], + }, + ], + }, + ports: [], + }, + telemetry: { + version: 1, + originalSha256: '3c4327fdf5df7c1e19a5963e8ed3b8cde2cb1f1bd6c9b097225e45e0d7549dfb', + factory: { + ids: [HANDLER_UNMINIFIED_IDS_16_4_4.telemetry], + start: 640, + end: 55_514, + }, + strictContext: { + effective: false, + scopes: [ + { + type: 'Program', + directives: [], + }, + { + type: 'ArrowFunctionExpression', + directives: [], + }, + ], + }, + ports: [], + }, + telemetryExternal: { + version: 1, + originalSha256: '3c4327fdf5df7c1e19a5963e8ed3b8cde2cb1f1bd6c9b097225e45e0d7549dfb', + factory: { + ids: [HANDLER_UNMINIFIED_IDS_16_4_4.telemetryExternal], + start: 25, + end: 228, + }, + strictContext: { + effective: false, + scopes: [ + { + type: 'Program', + directives: [], + }, + { + type: 'ArrowFunctionExpression', + directives: [], + }, + ], + }, + ports: [], + }, + crypto: { + version: 1, + originalSha256: '0d1c836fd1208b1b9e935c5aedfad00b82f45c90471e50bf17b7bb5e2ffc3b60', + factory: { + ids: [HANDLER_UNMINIFIED_IDS_16_4_4.crypto], + start: 26, + end: 167, + }, + strictContext: { + effective: false, + scopes: [ + { + type: 'Program', + directives: [], + }, + { + type: 'ArrowFunctionExpression', + directives: [], + }, + ], + }, + ports: [], + }, +}; diff --git a/packages/adapter/src/node-manifest-handler-unminified-16-4-4-graph.ts b/packages/adapter/src/node-manifest-handler-unminified-16-4-4-graph.ts new file mode 100644 index 000000000..651b7ad54 --- /dev/null +++ b/packages/adapter/src/node-manifest-handler-unminified-16-4-4-graph.ts @@ -0,0 +1,182 @@ +/** Next.js 16.4.4: fixed whole-factory DATA from two clean offline public server-minification-off builds. */ +import type { + NodeManifestHandlerFingerprintRowData, + NodeManifestHandlerImportData, + NodeManifestHandlerReferenceGraphData, + TargetData, +} from './node-manifest-handler-graph.ts'; +import { + HANDLER_UNMINIFIED_FACTORIES_16_4_4, + HANDLER_UNMINIFIED_IDS_16_4_4, +} from './node-manifest-handler-unminified-16-4-4-factories.ts'; +const streamSpecifier = 'node:stream'; +const RUNTIME = 'next-16.4.4-public-pages-server-minification-off'; +const nextRoot = + '/work/node_modules/.pnpm/next@16.4.4_@types+node@24.19.2_react-dom@19.3.1_react@19.3.1__react@19.3.1/node_modules/next'; +function internal(kind: 'import' | 'require', moduleId: number): NodeManifestHandlerImportData { + return { + kind, + specifier: `turbopack:${moduleId}`, + targetId: moduleId, + target: { kind: 'module', runtime: RUNTIME, moduleId }, + }; +} +function external(specifier: string, target: TargetData): NodeManifestHandlerImportData { + return { kind: 'external', specifier, target }; +} +function source(relative: string, sha256: string): TargetData { + return { kind: 'next-source', root: nextRoot, relative, file: `${nextRoot}/${relative}`, sha256 }; +} +function row( + factoryName: string, + moduleId: number, + canonicalSha256: string, + imports: readonly NodeManifestHandlerImportData[], +): NodeManifestHandlerFingerprintRowData { + const whole = HANDLER_UNMINIFIED_FACTORIES_16_4_4[factoryName]; + if (whole?.factory.ids.includes(moduleId) !== true) + throw new Error('Invalid fixed unminified handler reference row'); + return { + mode: 'fingerprint', + runtime: RUNTIME, + moduleId, + physicalId: whole.originalSha256, + canonicalSha256, + whole: { ...whole, selectedOrdinal: whole.factory.ids.indexOf(moduleId) }, + imports, + operations: { cacheInitializers: [], asyncLoads: [], boundRuntimeInputs: [] }, + }; +} +export const HANDLER_UNMINIFIED_GRAPH_16_4_4: NodeManifestHandlerReferenceGraphData = { + root: { runtime: RUNTIME, moduleId: HANDLER_UNMINIFIED_IDS_16_4_4.handler }, + nextRoot, + rows: [ + row( + 'handler', + HANDLER_UNMINIFIED_IDS_16_4_4.handler, + '39191958d16d33cd4933ebaff55637dc89927d86e7c23cea9bca07fe4d012cfb', + [ + internal('require', HANDLER_UNMINIFIED_IDS_16_4_4.crypto), + internal('require', HANDLER_UNMINIFIED_IDS_16_4_4.path), + internal('import', HANDLER_UNMINIFIED_IDS_16_4_4.noFallback), + internal('import', HANDLER_UNMINIFIED_IDS_16_4_4.fresh), + internal('import', HANDLER_UNMINIFIED_IDS_16_4_4.path), + internal('require', HANDLER_UNMINIFIED_IDS_16_4_4.stream), + internal('require', HANDLER_UNMINIFIED_IDS_16_4_4.stream), + internal('import', HANDLER_UNMINIFIED_IDS_16_4_4.escape), + internal('require', HANDLER_UNMINIFIED_IDS_16_4_4.stream), + internal('require', HANDLER_UNMINIFIED_IDS_16_4_4.stream), + internal('require', HANDLER_UNMINIFIED_IDS_16_4_4.telemetry), + internal('require', HANDLER_UNMINIFIED_IDS_16_4_4.telemetryExternal), + ], + ), + row( + 'handler', + HANDLER_UNMINIFIED_IDS_16_4_4.hoist, + '72d9733353a7ce3a242a6f1d60b5d55c39185f35bea0eb01face7fd373e642c5', + [ + internal('require', HANDLER_UNMINIFIED_IDS_16_4_4.crypto), + internal('require', HANDLER_UNMINIFIED_IDS_16_4_4.path), + internal('import', HANDLER_UNMINIFIED_IDS_16_4_4.noFallback), + internal('import', HANDLER_UNMINIFIED_IDS_16_4_4.fresh), + internal('import', HANDLER_UNMINIFIED_IDS_16_4_4.path), + internal('require', HANDLER_UNMINIFIED_IDS_16_4_4.stream), + internal('require', HANDLER_UNMINIFIED_IDS_16_4_4.stream), + internal('import', HANDLER_UNMINIFIED_IDS_16_4_4.escape), + internal('require', HANDLER_UNMINIFIED_IDS_16_4_4.stream), + internal('require', HANDLER_UNMINIFIED_IDS_16_4_4.stream), + internal('require', HANDLER_UNMINIFIED_IDS_16_4_4.telemetry), + internal('require', HANDLER_UNMINIFIED_IDS_16_4_4.telemetryExternal), + ], + ), + row( + 'handler', + HANDLER_UNMINIFIED_IDS_16_4_4.routeKind, + 'a3cf075ec47a713bbcc3de062c5e1ff81e3558c3464d19ac04f706d41efb9224', + [ + internal('require', HANDLER_UNMINIFIED_IDS_16_4_4.crypto), + internal('require', HANDLER_UNMINIFIED_IDS_16_4_4.path), + internal('import', HANDLER_UNMINIFIED_IDS_16_4_4.noFallback), + internal('import', HANDLER_UNMINIFIED_IDS_16_4_4.fresh), + internal('import', HANDLER_UNMINIFIED_IDS_16_4_4.path), + internal('require', HANDLER_UNMINIFIED_IDS_16_4_4.stream), + internal('require', HANDLER_UNMINIFIED_IDS_16_4_4.stream), + internal('import', HANDLER_UNMINIFIED_IDS_16_4_4.escape), + internal('require', HANDLER_UNMINIFIED_IDS_16_4_4.stream), + internal('require', HANDLER_UNMINIFIED_IDS_16_4_4.stream), + internal('require', HANDLER_UNMINIFIED_IDS_16_4_4.telemetry), + internal('require', HANDLER_UNMINIFIED_IDS_16_4_4.telemetryExternal), + ], + ), + row( + 'noFallback', + HANDLER_UNMINIFIED_IDS_16_4_4.noFallback, + 'd726a589de48f375531c5dbd1a45736c56a08df13bea0efaea952bdff2537647', + [ + external( + 'next/dist/shared/lib/no-fallback-error.external.js', + source( + 'dist/shared/lib/no-fallback-error.external.js', + '2e641c2ce7efe466d03751134fefec1cfd752bb00060fc7273d6d6d26e723392', + ), + ), + ], + ), + row( + 'fresh', + HANDLER_UNMINIFIED_IDS_16_4_4.fresh, + '85b87b39e16e4ec38eeccc04ea551f4a961b3588aea75fbac8b0d3a41ccb921c', + [], + ), + row( + 'path', + HANDLER_UNMINIFIED_IDS_16_4_4.path, + '72409d063712412d2e319a2896b8549cd03a53acb12adab4e012870fe515e39f', + [external('path', { kind: 'builtin', id: 'path', aliases: ['path', 'node:path'] })], + ), + row( + 'stream', + HANDLER_UNMINIFIED_IDS_16_4_4.stream, + '9769ab632f35abe111a7eb813d7655ec3feb6cb42a23089c6b9998ad59cd2174', + [ + external(streamSpecifier, { + kind: 'builtin', + id: streamSpecifier, + aliases: [streamSpecifier, 'stream'], + }), + ], + ), + row( + 'escape', + HANDLER_UNMINIFIED_IDS_16_4_4.escape, + '0e937bbeac2e8ad16312d92704eff1fb346ff54b484c1273bee989ed6c55ec86', + [], + ), + row( + 'telemetry', + HANDLER_UNMINIFIED_IDS_16_4_4.telemetry, + '773a0aba1981bcf99312917f6189e0123e1937e0999162e055d7af6fce8fe4f5', + [], + ), + row( + 'telemetryExternal', + HANDLER_UNMINIFIED_IDS_16_4_4.telemetryExternal, + '43583dae876f2f9c1a8980a6c381b753c2e72e12be1b22d8bb940fc47771b036', + [ + external( + 'next/dist/compiled/@opentelemetry/api', + source( + 'dist/compiled/@opentelemetry/api/index.js', + '79cdf1ce026102bf94b8b9beb667d8c8635d0f702c2f118f441699684e92c2de', + ), + ), + ], + ), + row( + 'crypto', + HANDLER_UNMINIFIED_IDS_16_4_4.crypto, + 'f17314affc1fbecec029556aa7f3ecd070a8ad15151f1265a05e1e23fc9e65f0', + [external('crypto', { kind: 'builtin', id: 'crypto', aliases: ['crypto', 'node:crypto'] })], + ), + ], +}; diff --git a/packages/adapter/src/node-manifest-handler-unminified-16-4-4-reference.ts b/packages/adapter/src/node-manifest-handler-unminified-16-4-4-reference.ts new file mode 100644 index 000000000..d9c76db61 --- /dev/null +++ b/packages/adapter/src/node-manifest-handler-unminified-16-4-4-reference.ts @@ -0,0 +1,110 @@ +/** + * Next.js 16.4.4 getHandler reference with server minification disabled. + * Fixed public inputs, no SDK, no marker, two byte-identical clean offline Linux builds. + * https://github.com/arkorlab/upwind/actions/runs/41623398928 + * Public artifact ZIP SHA-256: 467d7f0e897c7c533e0e81c1f83409716c34dff39cbba4fb01bf29fe15bb1c8f. + * An agent wrote this data from that artifact at the owner's request, an exception to the + * producer README. The same steps first regenerated the 16.4.3 data from that run's 16.4.3 + * artifact, byte for byte. + * Complete original callbacks, aliases, directives and ports remain in the graph. + * Only two independently pinned NCC __dirname literals are input-relative slots; + * their original + '/' expressions remain in the whole-factory fingerprints. + * Catalog selection grants no runtime or publication authority without the private + * actual consumer, compiler-root, raw-source, SDK and final closed-graph checks. + * Original source: https://github.com/vercel/next.js/tree/b175f52b05a5f6c6c39ca72565ea178542be323b + */ +import type { NodeManifestHandlerProfile } from './node-manifest-handler-catalog.ts'; +import { HANDLER_UNMINIFIED_GRAPH_16_4_4 } from './node-manifest-handler-unminified-16-4-4-graph.ts'; + +const profileName = 'next-16.4.4-public-pages-server-minification-off'; +const nccDirectoryAssignmentOrdinal = 4; + +export const NEXT_16_4_4_UNMINIFIED_HANDLER_REFERENCE: NodeManifestHandlerProfile = { + version: 1, + name: profileName, + exportName: 'getHandler', + declaredExport: 'f', + reference: HANDLER_UNMINIFIED_GRAPH_16_4_4, + sources: [ + { + relative: 'dist/shared/lib/no-fallback-error.external.js', + sha256: '2e641c2ce7efe466d03751134fefec1cfd752bb00060fc7273d6d6d26e723392', + }, + { + relative: 'dist/compiled/@opentelemetry/api/index.js', + sha256: '79cdf1ce026102bf94b8b9beb667d8c8635d0f702c2f118f441699684e92c2de', + }, + { + relative: 'dist/compiled/fresh/index.js', + sha256: '9cf9478dc7648e4ba9a973dcd928bd4a87600edb5c02f0eaf67dd42ae845da8e', + }, + { + relative: 'dist/server/route-modules/pages/pages-handler.js', + sha256: 'b20671fb66060f58b98fe7677adf844d10697a97b463eb2390eb00831500bda1', + }, + ], + literalSlots: [ + { + key: 'next-dir:dist/compiled/@opentelemetry/api/index.js', + row: { + runtime: profileName, + moduleId: 4309, + }, + path: [ + 'callback', + 'body', + 'body', + 0, + 'expression', + 'callee', + 'body', + 'body', + nccDirectoryAssignmentOrdinal, + 'consequent', + 'expression', + 'right', + 'left', + 'expressions', + 1, + 'value', + ], + referenceValue: + '/ROOT/node_modules/.pnpm/next@16.4.4_@types+node@24.19.2_react-dom@19.3.1_react@19.3.1__react@19.3.1/node_modules/next/dist/compiled/@opentelemetry/api', + origin: { + kind: 'compiler-relative-next-source-directory', + relative: 'dist/compiled/@opentelemetry/api/index.js', + }, + }, + { + key: 'next-dir:dist/compiled/fresh/index.js', + row: { + runtime: profileName, + moduleId: 7284, + }, + path: [ + 'callback', + 'body', + 'body', + 0, + 'expression', + 'callee', + 'body', + 'body', + nccDirectoryAssignmentOrdinal, + 'consequent', + 'expression', + 'right', + 'left', + 'expressions', + 1, + 'value', + ], + referenceValue: + '/ROOT/node_modules/.pnpm/next@16.4.4_@types+node@24.19.2_react-dom@19.3.1_react@19.3.1__react@19.3.1/node_modules/next/dist/compiled/fresh', + origin: { + kind: 'compiler-relative-next-source-directory', + relative: 'dist/compiled/fresh/index.js', + }, + }, + ], +}; diff --git a/packages/adapter/src/node-manifest-proof.ts b/packages/adapter/src/node-manifest-proof.ts index b062ca9be..cb98126a5 100644 --- a/packages/adapter/src/node-manifest-proof.ts +++ b/packages/adapter/src/node-manifest-proof.ts @@ -17,6 +17,11 @@ import { type ManifestRouteSource, type NodeManifestNormalization, } from './node-manifest-graph.ts'; +import { + isForeignReleaseSource, + nativeNextReleaseOf, + type NativeNextRelease, +} from './node-next-release.ts'; import { combineNodeRouteRewriteApprovals, type NodeRouteRewriteApproval, @@ -208,18 +213,37 @@ async function packageSource(root: string): Promise { } catch { refuse('package metadata'); } - if ( - typeof metadata !== 'object' || - metadata === null || - !('name' in metadata) || - metadata.name !== 'next' || - !('version' in metadata) || - metadata.version !== '16.4.3' - ) - refuse('version or package identity'); + const release = + typeof metadata === 'object' && + metadata !== null && + 'name' in metadata && + metadata.name === 'next' && + 'version' in metadata + ? nativeNextReleaseOf(sha256(bytes), metadata.version) + : undefined; + if (release === undefined) refuse('version or package identity'); + await releaseSources(root, release); return { file, sha256: sha256(bytes) }; } +/** + * No release-dependent source the package holds is another release's copy: the graph accepts the + * reviewed bytes of either, and refuses any other bytes itself. + */ +async function releaseSources(root: string, release: NativeNextRelease): Promise { + for (const [relative, expected] of release.sources) { + const file = path.join(root, relative); + let held = true; + try { + await stat(file); + } catch { + held = false; + } + const actual = held ? sha256(await boundedSource(file, MAX_MANIFEST_BYTES)) : expected; + if (isForeignReleaseSource(release, relative, actual)) refuse('release source identity'); + } +} + async function ownedScripts(input: ManifestProofInput): Promise> { const scripts = new Map(); const candidates = (input.entryTraces ?? []).flatMap((trace) => diff --git a/packages/adapter/src/node-manifest-registration-views.ts b/packages/adapter/src/node-manifest-registration-views.ts index 5b4f7fd10..4d3618703 100644 --- a/packages/adapter/src/node-manifest-registration-views.ts +++ b/packages/adapter/src/node-manifest-registration-views.ts @@ -83,6 +83,7 @@ function graphIdentity(graph: Snapshot['graph']): string { graph.evaluations, graph.initialChunks, graph.lookupTables, + graph.installedTables, ]); } diff --git a/packages/adapter/src/node-manifest-runtime-cohort.ts b/packages/adapter/src/node-manifest-runtime-cohort.ts index fe4fe64fc..8e154c28e 100644 --- a/packages/adapter/src/node-manifest-runtime-cohort.ts +++ b/packages/adapter/src/node-manifest-runtime-cohort.ts @@ -22,6 +22,7 @@ import { type NodeManifestTurbopackSealedEntry, } from './node-manifest-turbopack-graph.ts'; import { assertNodeManifestTurbopackRuntime } from './node-manifest-turbopack-runtime.ts'; +import { nodeManifestTurbopackNestedTable } from './node-manifest-turbopack.ts'; export interface NodeManifestRuntimeProfile { readonly file: string; @@ -54,6 +55,8 @@ export interface NodeManifestRuntimeCohort { /** Owned callers only; these are not protected route or normalization terminals. */ readonly boots: readonly BootIdentity[]; readonly lookupEdges: readonly LookupEdge[]; + /** Whether every runtime installs a chunk's leading strict factory table (16.4.3's). */ + readonly strictFactoryTables: boolean; } const CERTIFIED = new WeakSet(); const PROFILE_KINDS = new Set(['app-page', 'app-route', 'pages', 'pages-api']); @@ -167,6 +170,31 @@ function lookupEdges( return Object.freeze(edges); } +/** A graph without its installed inventory keeps the full lookup inventory. */ +function installedTables(graph: NodeManifestTurbopackBootGraph): readonly string[] { + return [...graph.initialChunks, ...(graph.installedTables ?? graph.lookupTables ?? [])]; +} + +/** + * Beside a runtime that installs no strict factory table (16.4.4's), no table it installs leads + * with one: only the chunks its graphs install, never a lookup-only chunk or another CommonJS + * source that looks like one. + */ +function flatTables( + strict: boolean, + graphs: readonly NodeManifestTurbopackBootGraph[], + sources: readonly NodeManifestTurbopackGraphFile[], +): void { + if (strict) return; + const installed = new Set(graphs.flatMap((graph) => installedTables(graph))); + if ( + sources.some( + (source) => installed.has(source.id) && nodeManifestTurbopackNestedTable(source.source), + ) + ) + refuse('strict factory table beside a runtime that installs none'); +} + /** Sealed owned runtime identities must be known before any factory-table interpretation. */ export function certifyNodeManifestRuntimeCohort(input: { readonly inventory: NodeManifestRuntimeCallers; @@ -180,6 +208,7 @@ export function certifyNodeManifestRuntimeCohort(input: { const sources = sourcesById(input.sources); const boots: BootIdentity[] = []; const runtimes = new Map(); + let strict = true; for (const caller of callers) { const source = sources.get(caller.id); if (source === undefined) refuse('missing caller source'); @@ -195,6 +224,7 @@ export function certifyNodeManifestRuntimeCohort(input: { distDir: input.distDir, ...(profile.assetPrefix !== undefined && { assetPrefix: profile.assetPrefix }), }); + strict &&= proof.strictFactoryTables; const identity = { id: runtime.id, file: runtime.file, @@ -223,6 +253,7 @@ export function certifyNodeManifestRuntimeCohort(input: { runtimes: Object.freeze([...runtimes.values()]), boots: Object.freeze(boots), lookupEdges: lookupEdges(runtimes, sources), + strictFactoryTables: strict, }); CERTIFIED.add(cohort); return cohort; @@ -331,15 +362,20 @@ export function prepareNodeManifestRuntimeGraphs(input: { registrationScope, }); }) ?? []; + let strict = cohort?.strictFactoryTables ?? true; if (cohort === undefined) { - for (const graph of graphs) - assertNodeManifestTurbopackRuntime({ + for (const graph of graphs) { + const proof = assertNodeManifestTurbopackRuntime({ source: graph.runtime.source, file: graph.runtime.file, distDir: input.distDir, rootDir: input.rootDir, ...(input.assetPrefix !== undefined && { assetPrefix: input.assetPrefix }), }); + strict &&= proof.strictFactoryTables; + } } - return { cohort, graphs, allGraphs: [...graphs, ...other] }; + const allGraphs = [...graphs, ...other]; + flatTables(strict, allGraphs, input.sources); + return { cohort, graphs, allGraphs }; } diff --git a/packages/adapter/src/node-manifest-turbopack-graph.ts b/packages/adapter/src/node-manifest-turbopack-graph.ts index 0f4bb1adc..5226fb72b 100644 --- a/packages/adapter/src/node-manifest-turbopack-graph.ts +++ b/packages/adapter/src/node-manifest-turbopack-graph.ts @@ -66,6 +66,8 @@ export interface NodeManifestTurbopackBootGraph { readonly runtime: NodeManifestTurbopackGraphFile; /** Full lookup inventory for physical caller checks, including uninstalled table targets. */ readonly lookupTables?: readonly string[]; + /** Tables this runtime installs into its ModuleId Map; the full inventory when unproved. */ + readonly installedTables?: readonly string[]; /** Topology alone is never a manifest exemption or an SDK normalization terminal. */ readonly registrationIndex?: NodeManifestRegistrationIndex | undefined; readonly requiresReviewedRuntime: true; @@ -348,6 +350,7 @@ export function nodeManifestTurbopackBootGraph(input: { registrationIndex, runtime, lookupTables: Object.freeze(lookup.map((table) => table.id)), + installedTables: Object.freeze(tables.map((table) => table.id)), requiresReviewedRuntime: true, }; } diff --git a/packages/adapter/src/node-manifest-turbopack-runtime.ts b/packages/adapter/src/node-manifest-turbopack-runtime.ts index 6ce00daf0..9e0f22618 100644 --- a/packages/adapter/src/node-manifest-turbopack-runtime.ts +++ b/packages/adapter/src/node-manifest-turbopack-runtime.ts @@ -12,13 +12,39 @@ const MAX_RUNTIME_BYTES = 131_072; const CONFIGURATION_STATEMENTS = 3; const MAX_CONFIG_STRING_CHARACTERS = 4096; const REVIEWED_RUNTIME = Symbol('reviewed Next.js Node production runtime'); -// Normalized whole-body AST fingerprints from fixed Next.js 16.4.3 / 29bbeab09981148ade186e8599ecdede15abc281. -// The primary Node snapshot supplies the body. The exact shared async-module block may be -// inserted at the one position specified by nodejs_runtime.rs. Parsed using pinned Rolldown 1.2.15. -const REVIEWED_BODIES: ReadonlyMap = new Map([ - ['0ec227e1c5e05e7469cde4e6703b36e121c5f0cf7747b9e24b3320a772d70494', true], - ['940f4e1becca78a27a4773bf7517dd432909ed5db926aedc82857cc0379c82b6', false], - ['bc70d80ec247f42eba92cc0376770310bf96aade6ceeefdcb44bcac0ce51edff', true], +// Normalized whole-body AST fingerprints from fixed Next.js 16.4.3 / 29bbeab09981148ade186e8599ecdede15abc281 +// and 16.4.4 / b175f52b05a5f6c6c39ca72565ea178542be323b. The primary Node snapshot supplies the +// body. The exact shared async-module block, the same in both, may be inserted at the one position +// specified by nodejs_runtime.rs. Parsed using pinned Rolldown 1.2.15. 16.4.4's runtime installs no +// strict factory table: Next.js reverted the strict module factory chunks that 16.4.3 emits. +const REVIEWED_BODIES: ReadonlyMap< + string, + { readonly asyncModules: boolean; readonly strictFactoryTables: boolean } +> = new Map([ + [ + '0ec227e1c5e05e7469cde4e6703b36e121c5f0cf7747b9e24b3320a772d70494', + { asyncModules: true, strictFactoryTables: true }, + ], + [ + '3fc93fc7034f45c50cf6a6e4f938300945a6331a1a9cb75e1f3bc6d18ba34e0d', + { asyncModules: true, strictFactoryTables: false }, + ], + [ + '5ecdb993b3e5f2dded315e847cd905d4f1893444dab990314b7bf2d3fa32880b', + { asyncModules: true, strictFactoryTables: false }, + ], + [ + '76ff1b00a4a57f91805a433f0e1c1f72de3425c65477afcd1df55d7babd45675', + { asyncModules: false, strictFactoryTables: false }, + ], + [ + '940f4e1becca78a27a4773bf7517dd432909ed5db926aedc82857cc0379c82b6', + { asyncModules: false, strictFactoryTables: true }, + ], + [ + 'bc70d80ec247f42eba92cc0376770310bf96aade6ceeefdcb44bcac0ce51edff', + { asyncModules: true, strictFactoryTables: true }, + ], ]); // The sole optional preamble written by node/entry/runtime.rs when forwarded globals are configured. // This is inert syntax, never evaluated by the verifier. @@ -48,6 +74,8 @@ export interface NodeManifestTurbopackRuntimeProof { readonly rootDir: string; readonly assetPrefix: string; readonly asyncModules: boolean; + /** Whether the runtime installs a chunk's leading strict factory table (16.4.3's). */ + readonly strictFactoryTables: boolean; } function refuse(reason: string): never { throw new AuditError(`Unproved Next.js Turbopack runtime ${reason}; refusing native publication`); @@ -150,13 +178,13 @@ export function assertNodeManifestTurbopackRuntime( const coordinates = configPaths(input, body); const normalizedBody = runtimeBody(body.slice(CONFIGURATION_STATEMENTS)); const fingerprint = digest(astShape(normalizedBody)); - const asyncModules = REVIEWED_BODIES.get(fingerprint); - if (asyncModules === undefined) return refuse('changed production context semantics'); + const reviewed = REVIEWED_BODIES.get(fingerprint); + if (reviewed === undefined) return refuse('changed production context semantics'); return Object.freeze({ [REVIEWED_RUNTIME]: true, file: path.resolve(input.file), sha256: input.source.sha256, ...coordinates, - asyncModules, + ...reviewed, }); } diff --git a/packages/adapter/src/node-manifest-turbopack.ts b/packages/adapter/src/node-manifest-turbopack.ts index c2dba179c..f90533d36 100644 --- a/packages/adapter/src/node-manifest-turbopack.ts +++ b/packages/adapter/src/node-manifest-turbopack.ts @@ -228,6 +228,19 @@ function checkedTurbopackProgram(source: NodeManifestTurbopackSource): Node { return program; } +/** Whether a chunk's registration leads with a strict factory table; false for any other program. */ +export function nodeManifestTurbopackNestedTable(source: NodeManifestTurbopackSource): boolean { + try { + const first = listOf(registration(checkedTurbopackProgram(source)), 'elements')[0]; + return ( + isNode(first) && + (first.type === 'ArrayExpression' || strictWrapper(first, true) !== undefined) + ); + } catch { + return false; + } +} + /** This validates registration only; no factory body receives a manifest exemption here. */ export function nodeManifestTurbopackFactories( source: NodeManifestTurbopackSource, diff --git a/packages/adapter/src/node-next-cache-write-proof.ts b/packages/adapter/src/node-next-cache-write-proof.ts index eb7afac51..d95569d35 100644 --- a/packages/adapter/src/node-next-cache-write-proof.ts +++ b/packages/adapter/src/node-next-cache-write-proof.ts @@ -19,14 +19,14 @@ import { type NodeManifestIdentitySnapshot, } from './node-manifest-identity-snapshot.ts'; import { nodeNextCacheWriteData } from './node-next-cache-write-data.ts'; +import { isReleaseSource, nativeNextReleaseAt } from './node-next-release.ts'; import { previewCertifiedNodeCachePolicy } from './node-next-rewrite-proof.ts'; import { assertOriginalNodeModesGraph } from './node-original-load-mode.ts'; import { checkedNodeRouteRewriteSources } from './node-route-rewrite-approval.ts'; const PROOF = Symbol('fixed Next cache state write'); -const SUFFIX = '/dist/compiled/next-server/pages-turbo.runtime.prod.js'; -const SOURCE_SHA = '6a6fb036d30a72fcd4cb10b4ffc13318f5b0df6a205fd3fa1ce97cda1717d9c1'; -const PACKAGE_SHA = 'b67bad7944d603b27dd2d2719b57203fd3b3e5c147b2efa6dea5ceb135274caf'; +const RUNTIME = 'dist/compiled/next-server/pages-turbo.runtime.prod.js'; +const SUFFIX = `/${RUNTIME}`; const MAX_BYTES = 4_194_304; export interface NodeNextCacheWriteProof { readonly [PROOF]: true; @@ -80,8 +80,10 @@ async function candidate(state: State, id: string, own: ManifestGraphFile): Prom ) refuse('canonical completed SDK route root'); state.roots.add(root); - await source(state, path.join(root, 'package.json'), PACKAGE_SHA); - const raw = await source(state, own.file, SOURCE_SHA); + // The runtime of the release the package is, never another release's copy. + const release = nativeNextReleaseAt(root) ?? refuse('fixed package identity'); + await source(state, path.join(root, 'package.json'), release.packageJsonSha256); + const raw = await source(state, own.file, release.source(RUNTIME) ?? ''); if ( input.rewriteInput === undefined || previewCertifiedNodeCachePolicy(input.rewrites, input.rewriteInput, { @@ -109,7 +111,8 @@ export async function qualifyNodeNextCacheWrites( input.graph.retained.has(id) && own.file.split(path.sep).join('/').endsWith(SUFFIX), ); if (rows.length === 0) return undefined; - if (rows.some(([, own]) => own.sha256 !== SOURCE_SHA)) refuse('fixed source identity'); + if (rows.some(([, own]) => !isReleaseSource(RUNTIME, own.sha256))) + refuse('fixed source identity'); const state: State = { input, snapshot: captureNodeManifestIdentity([input]), diff --git a/packages/adapter/src/node-next-pinned-rewrite.ts b/packages/adapter/src/node-next-pinned-rewrite.ts index 030a49302..a4150305e 100644 --- a/packages/adapter/src/node-next-pinned-rewrite.ts +++ b/packages/adapter/src/node-next-pinned-rewrite.ts @@ -3,7 +3,7 @@ import path from 'node:path'; import { AuditError } from './dependencies.ts'; import { nodeCachePolicyPatch } from './node-cache-policy.ts'; -import { reviewedFrameworkSource } from './node-manifest-framework.ts'; +import { nativeNextReleaseAt } from './node-next-release.ts'; import { LOADER, LOADER_SHA, type NodeNextRewriteInput } from './node-next-rewrite-proof.ts'; import { fetchCacheWaitUntilPatch } from './patches/fetch-cache-wait-until.ts'; import { instrumentationPatch } from './patches/instrumentation.ts'; @@ -51,7 +51,10 @@ export function rewritePinnedNodeNextFile( contents = result.contents; } else if (nodeCachePolicyPatch.target.test(file)) { const relative = nextRelative(file); - if (relative === undefined || reviewedFrameworkSource(relative) !== digest(code)) + // The bytes of the release the package is, never another release's copy of the same file. + const release = + relative === undefined ? undefined : nativeNextReleaseAt(file.slice(0, -relative.length - 1)); + if (relative === undefined || release?.source(relative) !== digest(code)) refuse('pinned cache policy runtime'); const result = nodeCachePolicyPatch.apply(code, file, input.patch); applied.push({ patch: nodeCachePolicyPatch.name, file, result }); diff --git a/packages/adapter/src/node-next-release.ts b/packages/adapter/src/node-next-release.ts new file mode 100644 index 000000000..d0044a2bf --- /dev/null +++ b/packages/adapter/src/node-next-release.ts @@ -0,0 +1,155 @@ +import { createHash } from 'node:crypto'; +import { readFileSync, statSync } from 'node:fs'; +import path from 'node:path'; + +/** A published Next.js release whose native sources the SDK qualifies, by their exact bytes. */ +export interface NativeNextRelease { + readonly version: '16.4.3' | '16.4.4'; + /** The published `package.json`, by its exact bytes. */ + readonly packageJsonSha256: string; + /** + * The reviewed sources that differ between the releases, as frozen `[path, sha256]` pairs. Every + * other reviewed source is the same published bytes in each (`node-manifest-framework.ts`). + */ + readonly sources: readonly (readonly [string, string])[]; + /** The reviewed sha256 of a release-dependent source, from a lookup no caller can change. */ + readonly source: (relative: string) => string | undefined; +} + +const MAX_PACKAGE_BYTES = 65_536; +const APP_PAGE_RUNTIME = 'dist/build/templates/app-page-runtime.js'; +const RUNTIME = 'dist/compiled/next-server/'; + +/** Each release's sources, in one order: the factory template, then the twelve runtimes. */ +const PATHS = [ + APP_PAGE_RUNTIME, + `${RUNTIME}app-page-experimental.runtime.prod.js`, + `${RUNTIME}app-page-turbo-experimental.runtime.prod.js`, + `${RUNTIME}app-page-turbo.runtime.prod.js`, + `${RUNTIME}app-page.runtime.prod.js`, + `${RUNTIME}pages-turbo.runtime.prod.js`, + `${RUNTIME}pages.runtime.prod.js`, + `${RUNTIME}pages-api-turbo.runtime.prod.js`, + `${RUNTIME}pages-api.runtime.prod.js`, + `${RUNTIME}app-route-experimental.runtime.prod.js`, + `${RUNTIME}app-route-turbo-experimental.runtime.prod.js`, + `${RUNTIME}app-route-turbo.runtime.prod.js`, + `${RUNTIME}app-route.runtime.prod.js`, +] as const; + +function defineRelease( + version: NativeNextRelease['version'], + packageJsonSha256: string, + sources: readonly string[], +): NativeNextRelease { + if (sources.length !== PATHS.length) throw new TypeError(`Incomplete Next.js ${version} sources`); + const pairs = Object.freeze( + PATHS.map((relative, index) => Object.freeze([relative, sources[index] ?? ''] as const)), + ); + const byPath: ReadonlyMap = new Map(pairs); + return Object.freeze({ + version, + packageJsonSha256, + sources: pairs, + source: (relative: string) => byPath.get(relative), + }); +} + +// Exact published next@16.4.3 and next@16.4.4 sources, from their integrity-verified registry packages. +export const NATIVE_NEXT_RELEASES: readonly NativeNextRelease[] = Object.freeze([ + defineRelease('16.4.3', 'b67bad7944d603b27dd2d2719b57203fd3b3e5c147b2efa6dea5ceb135274caf', [ + '0a6a5c726f95e2df2600b2fd6d71e37fd72f936f8f4dc5b94fa38d40fcd62a6b', + '2974a9e688b53a8b9bf61ea8535f1b70f4fb4154fc60c64a231d337f556d99c7', + 'd0d80c2b545d96bfdebb2a3d5100c368a18a0757db211eba55abf977e37ee23f', + '914353e0cf478e617e8a3b408fa74d9f526e9c338c8bda817cc05f05bfcce7a5', + '9f24cd66e38f57c3144cbc456fe0f52853836d6218f449f54abba8e8c17c3921', + '6a6fb036d30a72fcd4cb10b4ffc13318f5b0df6a205fd3fa1ce97cda1717d9c1', + '28f90af862ee5ae750fddea94d36a55cda89e4636993569d0e60c36f2813dc98', + '9bda335900891fa996b194d830883cb833a869a86e59f33ec7c80cf018309424', + '322a597b07fdc559b86562fd68f1c828b554deac36e1e84a10e6687daa01a00f', + '356d9c9be870fac47a341221841c76341c37a17eb038d6151e22a0788f87a906', + '4755bc19f3115dc66c2e818cf7cee5c0979a6821a5fec9513beb808cebe8a0bc', + '4010a14b45b58154eaf01034845c5003de0f7c3945657e9d84290fe66ea3d41c', + 'ff6aea920fbe3a9a4cf4179b0bc80bf20127136754cd9225586edb77f96dc254', + ]), + defineRelease('16.4.4', '30706a767e4f0c93dd18f073d4856641c2f9d135cbb182ee232c553e4c7c323d', [ + '5c33bd724b1310148a0e128351c877b987ea91893cd77750d072322df90a4150', + 'f0927c6bd58e33907c7574979516f3fdf12e047412c334b9b88c0d7cc0f34b32', + '999a2f4623c0e9f23ba925f9ed51d9a37bf860fa1bf7229112ceb75d8787d549', + '244c3d5778a3009aa96bd87590acecf356cf6df71155cdb92b67057dfcbed299', + 'b0640fff1e7bfd7b6cb2452cd65f58020fb1b5e0fc4e70508ac2b8ef0fd2014f', + 'c942ce0e1225d5173eb2c42dd9958727ae30c6a0e522b0ca939a2f9576c30f39', + '0adbf685547722a714dd900f2981897f841d424024202d91e6931a54e98110d7', + '4fbed6b321caa34a790fd6ee671a3ba8135d97dc8f49181f34c3c648f6124b6b', + '8d0dd3380764b15950850af06f229d666471e0c92cb31960d45c9be4e0d750c9', + 'b0f88dacfa8f35841e4cc80faf8cee94716da591e31a68e0159b3b61db4d4723', + '071075f8dec3ceeee13700b11accd19f93503c5ede4e052a4a25324a220000ad', + 'f85e8b56387771d0e9833ef879a2f5f38fd19ba4a1bfc5e8e3d8c68f8d9f769d', + '5487d86709ea8de206e57dad2dca618008b436b3c0027f821382d51daee72704', + ]), +]); + +/** The release a `package.json` is, by its exact bytes and the version it names. */ +export function nativeNextReleaseOf( + packageJsonSha256: string, + version: unknown, +): NativeNextRelease | undefined { + return NATIVE_NEXT_RELEASES.find( + (candidate) => + candidate.packageJsonSha256 === packageJsonSha256 && candidate.version === version, + ); +} + +/** Whether `sha256` is the published bytes of a release-dependent `relative` in some release. */ +export function isReleaseSource(relative: string, sha256: string): boolean { + return NATIVE_NEXT_RELEASES.some((candidate) => candidate.source(relative) === sha256); +} + +/** Whether `sha256` is another release's bytes of `relative`, not those of `release`. */ +export function isForeignReleaseSource( + release: NativeNextRelease, + relative: string, + sha256: string, +): boolean { + return release.source(relative) !== sha256 && isReleaseSource(relative, sha256); +} + +/** Whether `relative` names a source whose reviewed bytes depend on the release. */ +export function isReleasePath(relative: string): boolean { + return (PATHS as readonly string[]).includes(relative); +} + +const RELEASE_AT = new Map(); + +/** + * The release of the `next` package at `root`, from its `package.json`, or undefined where that is + * no supported release. Read once for each unchanged file; a build reads the same package often. + */ +export function nativeNextReleaseAt(root: string): NativeNextRelease | undefined { + const file = path.join(root, 'package.json'); + let key: string; + try { + const info = statSync(file, { bigint: true }); + if (!info.isFile() || info.size > MAX_PACKAGE_BYTES) return undefined; + key = `${info.dev}:${info.ino}:${info.size}:${info.mtimeNs}`; + } catch { + return undefined; + } + const known = RELEASE_AT.get(file); + if (known?.key === key) return known.release; + const bytes = readFileSync(file); + let metadata: unknown; + try { + metadata = JSON.parse(bytes.toString('utf8')); + } catch { + return undefined; + } + if (typeof metadata !== 'object' || metadata === null || !('name' in metadata)) return undefined; + if (metadata.name !== 'next' || !('version' in metadata)) return undefined; + const found = nativeNextReleaseOf( + createHash('sha256').update(bytes).digest('hex'), + metadata.version, + ); + if (found !== undefined) RELEASE_AT.set(file, { key, release: found }); + return found; +} diff --git a/packages/adapter/src/node-next-require-hook-proof.ts b/packages/adapter/src/node-next-require-hook-proof.ts index 0ab0f9218..5b473f576 100644 --- a/packages/adapter/src/node-next-require-hook-proof.ts +++ b/packages/adapter/src/node-next-require-hook-proof.ts @@ -27,6 +27,7 @@ import { } from './node-manifest-identity-snapshot.ts'; import { nodeNextHookAliasRoleData } from './node-next-hook-alias-role.ts'; import { nodeNextHookRawClosureData } from './node-next-hook-raw-role.ts'; +import { nativeNextReleaseAt } from './node-next-release.ts'; import { nodeNextStaticRequireHookSites, type NodeNextRequireHookSites, @@ -43,7 +44,6 @@ const PROOF = Symbol('fixed original Next require hook'); const SUFFIX = '/dist/server/require-hook.js'; const SHA256 = 'e82f0ceba88d025c725ea3062974ad9d7a54a892df8c34a525cb129202387db8'; const BYTES = 3066; -const NEXT_PACKAGE_SHA256 = 'b67bad7944d603b27dd2d2719b57203fd3b3e5c147b2efa6dea5ceb135274caf'; const SHARED_RUNTIME = '.shared-runtime'; const STYLE = 'styled-jsx/style'; const CONTEXTS = 'next/dist/server/route-modules/pages/vendored/contexts/'; @@ -384,8 +384,9 @@ export async function qualifyNodeNextRequireHook( sharedRuntimeEdges: [], aliasMetadata: undefined, }; - const metadata = json(await source(state, path.join(root, 'package.json'), NEXT_PACKAGE_SHA256)); - if (metadata['name'] !== 'next' || metadata['version'] !== '16.4.3') refuse('fixed Next package'); + // The release's exact package bytes, whose name and version `nativeNextReleaseAt` read. + const release = nativeNextReleaseAt(root) ?? refuse('fixed Next package'); + await source(state, path.join(root, 'package.json'), release.packageJsonSha256); const raw = await source(state, first.file, SHA256); if ( Buffer.byteLength(raw) !== BYTES || diff --git a/packages/adapter/src/node-next-rewrite-proof.ts b/packages/adapter/src/node-next-rewrite-proof.ts index e12a37eb1..b94d80dc2 100644 --- a/packages/adapter/src/node-next-rewrite-proof.ts +++ b/packages/adapter/src/node-next-rewrite-proof.ts @@ -18,7 +18,7 @@ import { nodeCachePolicyPatch, nodeCachePolicyRemovedRanges, } from './node-cache-policy.ts'; -import { reviewedFrameworkSource } from './node-manifest-framework.ts'; +import { isReviewedFrameworkSource } from './node-manifest-framework.ts'; import type { ManifestModuleGraph, ManifestRouteSource } from './node-manifest-graph.ts'; import type { NodeManifestNormalization } from './node-manifest-normalization.ts'; import type { NodeManifestProof } from './node-manifest-proof.ts'; @@ -115,7 +115,7 @@ function checkedRoutes( original?.sha256 !== route.sha256 || !graph.retained.has(original.id) || path.resolve(route.root, route.relative) !== route.file || - reviewedFrameworkSource(route.relative) !== route.sha256 + !isReviewedFrameworkSource(route.relative, route.sha256) ) refuse('completed route source identity'); checked.set(route.file, Object.freeze({ ...route })); diff --git a/packages/adapter/src/node-turbopack-selector.ts b/packages/adapter/src/node-turbopack-selector.ts index a3ac1ddd8..50b016cdd 100644 --- a/packages/adapter/src/node-turbopack-selector.ts +++ b/packages/adapter/src/node-turbopack-selector.ts @@ -7,7 +7,7 @@ import { parseAst } from 'rolldown/parseAst'; import { AuditError } from './dependencies.ts'; import { isNode, type Node } from './lexical-scope.ts'; import { declaration, visitTree } from './node-manifest-consumer-ast.ts'; -import { reviewedFrameworkSource } from './node-manifest-framework.ts'; +import { isReviewedFrameworkSource } from './node-manifest-framework.ts'; import type { ManifestModuleGraph, ManifestRouteSource } from './node-manifest-graph.ts'; import { originalNodeSpecifierInventory } from './node-source-specifiers.ts'; import type { Patch } from './patches/types.ts'; @@ -160,7 +160,7 @@ export function nativeTurbopackSelectorSource(file: string, code: string): strin const selected = coordinate(file); if (selected === undefined) return code; const [relative, expected] = selected; - if (createHash('sha256').update(code).digest('hex') !== reviewedFrameworkSource(relative)) + if (!isReviewedFrameworkSource(relative, createHash('sha256').update(code).digest('hex'))) return code; if (!canonical(file)) return code; const reads = selectorReads(code); diff --git a/packages/adapter/src/node-ws-loader-proof.ts b/packages/adapter/src/node-ws-loader-proof.ts index 77da2946f..ef286755c 100644 --- a/packages/adapter/src/node-ws-loader-proof.ts +++ b/packages/adapter/src/node-ws-loader-proof.ts @@ -25,6 +25,7 @@ import { captureNodeManifestIdentity, type NodeManifestIdentitySnapshot, } from './node-manifest-identity-snapshot.ts'; +import { nativeNextReleaseAt } from './node-next-release.ts'; import type { NodeNextRewriteInput, NodeNextRewriteProof } from './node-next-rewrite-proof.ts'; import { assertOriginalCommonJsMode, @@ -40,7 +41,6 @@ const PROOF: unique symbol = Symbol('fixed Node WS optional loaders'); const WS_SUFFIX = '/dist/compiled/ws/index.js'; const WS_SHA256 = 'af354cb13a11abe910cdbcb70943cb450b01b24f322026ae3f88d428d148395d'; const WS_BYTES = 45_840; -const NEXT_PACKAGE_SHA256 = 'b67bad7944d603b27dd2d2719b57203fd3b3e5c147b2efa6dea5ceb135274caf'; const BUILTINS = new Set([ 'crypto', 'events', @@ -388,8 +388,10 @@ export async function qualifyNodeWsLoaders( if (path.basename(root) !== 'next' || path.basename(path.dirname(root)) !== 'node_modules') return refuse(UNSUPPORTED); if (routes.every((route) => route.root !== root)) return refuse(UNSUPPORTED); - const metadata = json(await source(state, path.join(root, 'package.json'), NEXT_PACKAGE_SHA256)); - if (metadata['name'] !== 'next' || metadata['version'] !== '16.4.3') return refuse(UNSUPPORTED); + // The same bundle in every supported release; the profile is named for the first reviewed. The + // release's exact package bytes, whose name and version `nativeNextReleaseAt` read. + const release = nativeNextReleaseAt(root) ?? refuse(UNSUPPORTED); + await source(state, path.join(root, 'package.json'), release.packageJsonSha256); const raw = await source(state, first.file, WS_SHA256); if (Buffer.byteLength(raw) !== WS_BYTES) return refuse(UNSUPPORTED); const rawProgram = parseAst(raw, { sourceType: 'commonjs' }); diff --git a/packages/adapter/test/fixtures/node-manifest-bound-batch.ts b/packages/adapter/test/fixtures/node-manifest-bound-batch.ts index 660228697..e6c3b5d86 100644 --- a/packages/adapter/test/fixtures/node-manifest-bound-batch.ts +++ b/packages/adapter/test/fixtures/node-manifest-bound-batch.ts @@ -14,6 +14,7 @@ import { manifestTemplateSource, projectedTemplateFixture, } from './node-manifest-turbopack-template.ts'; +import { PINNED_NEXT_PACKAGE_JSON } from './pinned-next.ts'; const SPECIFIER = 'next/dist/build/templates/app-page-runtime.js'; const PRODUCER_KINDS = { @@ -106,7 +107,7 @@ export async function boundBatchFixture(directory: string): Promise +/// +/** + * Describes why a module was instantiated. + * Shared between browser and Node.js runtimes. + */ var SourceType = /*#__PURE__*/ function(SourceType) { + /** + * The module was instantiated because it was included in an evaluated chunk's + * runtime. + * SourceData is a ChunkPath. + */ SourceType[SourceType["Runtime"] = 0] = "Runtime"; + /** + * The module was instantiated because a parent module imported it. + * SourceData is a ModuleId. + */ SourceType[SourceType["Parent"] = 1] = "Parent"; + /** + * The module was instantiated because it was included in a chunk's hot module + * update. + * SourceData is an array of ModuleIds or undefined. + */ SourceType[SourceType["Update"] = 2] = "Update"; + return SourceType; +}(SourceType || {}); +/** + * Flag indicating which module object type to create when a module is merged. Set to `true` + * by each runtime that uses ModuleWithDirection (browser dev-base.ts, nodejs dev-base.ts, + * nodejs build-base.ts). Browser production (build-base.ts) leaves it as `false` since it + * uses plain Module objects. + */ let createModuleWithDirectionFlag = false; +const REEXPORTED_OBJECTS = new WeakMap(); +/** + * Constructs the `__turbopack_context__` object for a module. + */ function Context(module, exports) { + this.m = module; + // We need to store this here instead of accessing it from the module object to: + // 1. Make it available to factories directly, since we rewrite `this` to + // `__turbopack_context__.e` in CJS modules. + // 2. Support async modules which rewrite `module.exports` to a promise, so we + // can still access the original exports object from functions like + // `esmExport` + // Ideally we could find a new approach for async modules and drop this property altogether. + this.e = exports; +} +const contextPrototype = Context.prototype; +const hasOwnProperty = Object.prototype.hasOwnProperty; +const toStringTag = typeof Symbol !== 'undefined' && Symbol.toStringTag; +function defineProp(obj, name, options) { + if (!hasOwnProperty.call(obj, name)) Object.defineProperty(obj, name, options); +} +function getOverwrittenModule(moduleCache, id) { + let module = moduleCache.get(id); + if (module === undefined) { + if (createModuleWithDirectionFlag) { + // set in development modes for hmr support + module = createModuleWithDirection(id); + } else { + module = createModuleObject(id); + } + moduleCache.set(id, module); + } + return module; +} +/** + * Creates the module object. Only done here to ensure all module objects have the same shape. + */ function createModuleObject(id) { + return { + exports: {}, + error: undefined, + id, + namespaceObject: undefined + }; +} +function createModuleWithDirection(id) { + return { + exports: {}, + error: undefined, + id, + namespaceObject: undefined, + parents: [], + children: [] + }; +} +const BindingTag_Value = 0; +/** + * Terminates a module's group of entries in an {@link EsmReexports} list. + */ const REEXPORT_GROUP_END = 0; +/** + * Adds the getters to the exports object. + */ function esm(exports, bindings, dynamic) { + defineProp(exports, '__esModule', { + value: true + }); + if (toStringTag) defineProp(exports, toStringTag, { + value: 'Module' + }); + let i = 0; + while(i < bindings.length){ + const propName = bindings[i++]; + const tagOrFunction = bindings[i++]; + if (typeof tagOrFunction === 'number') { + if (tagOrFunction === BindingTag_Value) { + defineProp(exports, propName, { + value: bindings[i++], + enumerable: true, + writable: false + }); + } else { + throw new Error(`unexpected tag: ${tagOrFunction}`); + } + } else { + const getterFn = tagOrFunction; + if (typeof bindings[i] === 'function') { + const setterFn = bindings[i++]; + defineProp(exports, propName, { + get: getterFn, + set: setterFn, + enumerable: true + }); + } else { + defineProp(exports, propName, { + get: getterFn, + enumerable: true + }); + } + } + } + // The properties defined above are already non-configurable and + // non-writable, so the namespace's existing exports are effectively + // immutable. Sealing additionally makes the object non-extensible, matching + // real ESM-namespace semantics. Modules with dynamic re-exports + // (`export *` from a CommonJS module) must stay extensible so the dynamic + // export proxy can surface keys discovered at runtime, so skip the seal for + // them. + if (!dynamic) Object.seal(exports); +} +/** + * Makes the module an ESM with exports + */ function esmExport(bindings, id, dynamic) { + let module; + let exports; + if (id != null) { + module = getOverwrittenModule(this.c, id); + exports = module.exports; + } else { + module = this.m; + exports = this.e; + } + module.namespaceObject = exports; + esm(exports, bindings, dynamic); +} +contextPrototype.s = esmExport; +/** + * Registers re-exports that all forward to properties of other modules. + * + * This is a compact spelling of the pattern + * + * ```js + * var ns = context.i(moduleId) + * context.s([exportName, () => ns[importedName], ...]) + * ``` + * + * The list is a flat sequence of groups. Each group starts with the source the exports come from, + * followed by that group's entries, and is terminated by the `0` sentinel (or the end of the list). + * + * The group head is either a **module id**, which is instantiated here: + * + * ```js + * context.S([ + * 76061, 'default', 'f', 'named', 'A', 0, + * 29842, 'otherModule', 'f', + * ]) + * ``` + * + * or the **namespace value** of a module that has already been imported, which is used directly: + * + * ```js + * var ns1 = context.i(76061) + * context.S([ns1, 'default', 'f', 'named', 'A']) + * ``` + * + * The producer picks the namespace form when it has generated the import anyway -- because some + * later import must not be reordered past it -- so nothing is instantiated twice. The two are told + * apart by type: a module id is always a string or number. A CommonJS function export produces a + * callable namespace value, so namespace heads can be functions as well as objects. + * + * Entries are `exportName, importedName` pairs, except when a group holds exactly one string. That + * string is then a comma-joined list of the same pairs, which saves the repeated quoting: + * + * ```js + * context.S([ + * 76061, 'default,f,named,A', 0, + * 29842, 'otherModule,f', + * ]) + * ``` + * + * The producer picks that spelling independently for each group whose names contain no commas, + * since that group's names are recovered by splitting on them. + * + * Groups whose head is a module id are instantiated in list order, at the point where the call + * appears, so the producer must not merge such a group across an import of another module. The + * destination reuses a source data value or getter descriptor when one exists, falling back to a + * wrapper getter for dynamic/proxy/inherited properties. + * + * `id` names the module the exports belong to when this module was merged into a scope-hoisting + * group, exactly as it does for {@link EsmExport}. + * + * Only the source descriptor's payload (value or getter) is reused. {@link esm} still defines a + * fresh enumerable, non-configurable destination property, and no source setter is ever forwarded. + */ function esmReexport(list, id) { + const bindings = []; + let i = 0; + while(i < list.length){ + const head = list[i++]; + const start = i; + while(i < list.length && list[i] !== REEXPORT_GROUP_END)i++; + const end = i; + // Skip the sentinel, if this group was terminated by one rather than by the end of the list. + i++; + // Module ids are always strings or numbers. Other values are already-imported namespaces; + // notably, interop with a CommonJS function export produces a callable namespace function. + // `esmImport` may return a promise for an async module, but re-exports of async modules keep + // going through `context.s`, so the producer never routes them here and this stays synchronous. + const namespace = typeof head === 'string' || typeof head === 'number' ? esmImport.call(this, head) : head; + if (end - start === 1) { + const pairs = list[start].split(','); + for(let j = 0; j < pairs.length; j += 2){ + appendReexportBinding(bindings, pairs[j], namespace, pairs[j + 1]); + } + } else { + for(let j = start; j < end; j += 2){ + appendReexportBinding(bindings, list[j], namespace, list[j + 1]); + } + } + } + esmExport.call(this, bindings, id); +} +contextPrototype.S = esmReexport; +function appendReexportBinding(bindings, exportedName, namespace, importedName) { + const descriptor = Reflect.getOwnPropertyDescriptor(namespace, importedName); + if (descriptor) { + if ('value' in descriptor) { + // Code generation only routes immutable imported bindings through this helper, so a data + // descriptor is a constant export and can be captured once. + bindings.push(exportedName, BindingTag_Value, descriptor.value); + return; + } + if (descriptor.get) { + // Accessors remain live by reusing the source getter. `esmReexport` is only called by + // generated code: every group head is either produced by + // `this.i` or is the namespace variable from a generated `this.i` call. Every getter on such + // a namespace is receiver-independent: ESM bindings are compiler-generated arrow functions, + // and the CommonJS/dynamic-namespace paths create arrows in `createGetter` and + // `getOwnPropertyDescriptor`. The destination can therefore reuse the exact function instead + // of allocating another wrapper getter. + bindings.push(exportedName, descriptor.get); + return; + } + } + // Dynamic/proxy/inherited CommonJS edge cases may not expose a usable own descriptor. + bindings.push(exportedName, ()=>namespace[importedName]); +} +function ensureDynamicExports(module, exports) { + let reexportedObjects = REEXPORTED_OBJECTS.get(module); + if (!reexportedObjects) { + REEXPORTED_OBJECTS.set(module, reexportedObjects = []); + // Returns the re-exported object that provides `prop` as an own property, + // or `undefined` if none does. The traps share this logic so they always + // agree on which keys are synthesized from `reexportedObjects`. `default` + // is never re-exported by `export *`, so it is never synthesized. + const reexportOwning = (prop)=>{ + if (prop !== 'default') { + for (const obj of reexportedObjects){ + if (hasOwnProperty.call(obj, prop)) return obj; + } + } + return undefined; + }; + // Modules with dynamic re-exports are not sealed by `esm()`, so the + // target beneath the namespace stays extensible. That is what lets the + // `ownKeys` and `getOwnPropertyDescriptor` traps legally report keys that + // exist on `reexportedObjects` but not on the target itself. + module.exports = module.namespaceObject = new Proxy(exports, { + get (target, prop) { + if (hasOwnProperty.call(target, prop) || prop === 'default' || prop === '__esModule') { + return Reflect.get(target, prop); + } + const obj = reexportOwning(prop); + return obj && Reflect.get(obj, prop); + }, + // The namespace is read-only, like a real esm namespace object. The + // re-exported modules can still mutate their own exports (exposed live + // via `get`), but mutating the namespace itself is rejected. Refusing + // here, rather than forwarding to the extensible target, also prevents an + // assignment/definition from shadowing a dynamic re-export. It also + // prevents delete from removing a static export. + set () { + return false; + }, + defineProperty () { + return false; + }, + deleteProperty () { + return false; + }, + // The `has` trap ensures that `'exportName' in starImports` will reflect + // the truth of whether a key is exported. + has (target, prop) { + if (Reflect.has(target, prop)) return true; + if (prop === 'default' || prop === '__esModule') return false; + return reexportOwning(prop) !== undefined; + }, + // ownKeys and getOwnPropertyDescriptor together make the keys enumerable. + // If a value is returned from `ownKeys` but its property descriptor is + // not enumerable, it will not be visible to iterator methods. + // Collectively, they allow code like the following: + // + // ``` + // // module.js re-exports dynamic CJS exports + // export * from './legacyModule.cjs' + // + // // from another JS file, reference the re-exported dynamic values + // import * as Namespace from './module.js' + // Object.keys(Namespace) + // ``` + ownKeys (target) { + const keys = Reflect.ownKeys(target); + for (const obj of reexportedObjects){ + for (const key of Reflect.ownKeys(obj)){ + if (key !== 'default' && !keys.includes(key)) keys.push(key); + } + } + return keys; + }, + getOwnPropertyDescriptor (target, prop) { + const own = Reflect.getOwnPropertyDescriptor(target, prop); + if (own || prop === 'default' || prop === '__esModule') return own; + const obj = reexportOwning(prop); + if (obj) { + // Synthetic keys don't exist on the target, so they MUST be + // reported as configurable. However the set/delete traps above will + // prevent them from actually being changed + return { + enumerable: true, + configurable: true, + get: ()=>Reflect.get(obj, prop) + }; + } + return undefined; + } + }); + } + return reexportedObjects; +} +/** + * Dynamically exports properties from an object + */ function dynamicExport(object, id) { + let module; + let exports; + if (id != null) { + module = getOverwrittenModule(this.c, id); + exports = module.exports; + } else { + module = this.m; + exports = this.e; + } + const reexportedObjects = ensureDynamicExports(module, exports); + if (typeof object === 'object' && object !== null) { + reexportedObjects.push(object); + } +} +contextPrototype.j = dynamicExport; +function exportValue(value, id) { + let module; + if (id != null) { + module = getOverwrittenModule(this.c, id); + } else { + module = this.m; + } + module.exports = value; +} +contextPrototype.v = exportValue; +function exportNamespace(namespace, id) { + let module; + if (id != null) { + module = getOverwrittenModule(this.c, id); + } else { + module = this.m; + } + module.exports = module.namespaceObject = namespace; +} +contextPrototype.n = exportNamespace; +function createGetter(obj, key) { + return ()=>obj[key]; +} +/** + * @returns prototype of the object + */ const getProto = Object.getPrototypeOf ? (obj)=>Object.getPrototypeOf(obj) : (obj)=>obj.__proto__; +/** Prototypes that are not expanded for exports */ const LEAF_PROTOTYPES = [ + null, + getProto({}), + getProto([]), + getProto(getProto) +]; +/** + * @param raw + * @param ns + * @param allowExportDefault + * * `false`: will have the raw module as default export + * * `true`: will have the default property as default export + */ function interopEsm(raw, ns, allowExportDefault) { + const bindings = []; + let defaultLocation = -1; + for(let current = raw; (typeof current === 'object' || typeof current === 'function') && !LEAF_PROTOTYPES.includes(current); current = getProto(current)){ + for (const key of Object.getOwnPropertyNames(current)){ + bindings.push(key, createGetter(raw, key)); + if (defaultLocation === -1 && key === 'default') { + defaultLocation = bindings.length - 1; + } + } + } + // this is not really correct + // we should set the `default` getter if the imported module is a `.cjs file` + if (!(allowExportDefault && defaultLocation >= 0)) { + // Replace the binding with one for the namespace itself in order to preserve iteration order. + if (defaultLocation >= 0) { + // Replace the getter with the value + bindings.splice(defaultLocation, 1, BindingTag_Value, raw); + } else { + bindings.push('default', BindingTag_Value, raw); + } + } + esm(ns, bindings); + return ns; +} +function createNS(raw) { + if (typeof raw === 'function') { + return function(...args) { + return raw.apply(this, args); + }; + } else { + return Object.create(null); + } +} +function esmImport(id) { + const module = getOrInstantiateModuleFromParent(id, this.m); + // any ES module has to have `module.namespaceObject` defined. + if (module.namespaceObject) return module.namespaceObject; + // only ESM can be an async module, so we don't need to worry about exports being a promise here. + const raw = module.exports; + return module.namespaceObject = interopEsm(raw, createNS(raw), raw && raw.__esModule); +} +contextPrototype.i = esmImport; +function asyncLoader(moduleId) { + const loader = this.r(moduleId); + return loader(esmImport.bind(this)); +} +contextPrototype.A = asyncLoader; +// Add a simple runtime require so that environments without one can still pass +// `typeof require` CommonJS checks so that exports are correctly registered. +const runtimeRequire = // @ts-ignore +typeof require === 'function' ? require : function require1() { + throw new Error('Unexpected use of runtime require'); +}; +contextPrototype.t = runtimeRequire; +function commonJsRequire(id) { + return getOrInstantiateModuleFromParent(id, this.m).exports; +} +contextPrototype.r = commonJsRequire; +/** + * Remove fragments and query parameters since they are never part of the context map keys + * + * This matches how we parse patterns at resolving time. Arguably we should only do this for + * strings passed to `import` but the resolve does it for `import` and `require` and so we do + * here as well. + */ function parseRequest(request) { + // Per the URI spec fragments can contain `?` characters, so we should trim it off first + // https://datatracker.ietf.org/doc/html/rfc3986#section-3.5 + const hashIndex = request.indexOf('#'); + if (hashIndex !== -1) { + request = request.substring(0, hashIndex); + } + const queryIndex = request.indexOf('?'); + if (queryIndex !== -1) { + request = request.substring(0, queryIndex); + } + return request; +} +/** + * `require.context` and require/import expression runtime. + */ function moduleContext(map) { + function moduleContext(id) { + id = parseRequest(id); + if (hasOwnProperty.call(map, id)) { + return map[id].module(); + } + const e = new Error(`Cannot find module '${id}'`); + e.code = 'MODULE_NOT_FOUND'; + throw e; + } + moduleContext.keys = ()=>{ + return Object.keys(map); + }; + moduleContext.resolve = (id)=>{ + id = parseRequest(id); + if (hasOwnProperty.call(map, id)) { + return map[id].id(); + } + const e = new Error(`Cannot find module '${id}'`); + e.code = 'MODULE_NOT_FOUND'; + throw e; + }; + moduleContext.import = async (id)=>{ + return await moduleContext(id); + }; + return moduleContext; +} +contextPrototype.f = moduleContext; +/** + * Returns the path of a chunk defined by its data. + */ function getChunkPath(chunkData) { + return typeof chunkData === 'string' ? chunkData : chunkData.path; +} +// Load the CompressedmoduleFactories of a chunk into the `moduleFactories` Map. +// The CompressedModuleFactories format is +// - 1 or more module ids +// - a module factory function +// So walking this is a little complex but the flat structure is also fast to +// traverse, we can use `typeof` operators to distinguish the two cases. +function installCompressedModuleFactories(chunkModules, offset, moduleFactories, newModuleId) { + let i = offset; + while(i < chunkModules.length){ + let end = i + 1; + // Find our factory function + while(end < chunkModules.length && typeof chunkModules[end] !== 'function'){ + end++; + } + if (end === chunkModules.length) { + throw new Error('malformed chunk format, expected a factory function'); + } + // Install the factory for each module ID that doesn't already have one. + // When some IDs in this group already have a factory, reuse that existing + // group factory for the missing IDs to keep all IDs in the group consistent. + // Otherwise, install the factory from this chunk. + const moduleFactoryFn = chunkModules[end]; + let existingGroupFactory = undefined; + for(let j = i; j < end; j++){ + const id = chunkModules[j]; + const existingFactory = moduleFactories.get(id); + if (existingFactory) { + existingGroupFactory = existingFactory; + break; + } + } + const factoryToInstall = existingGroupFactory ?? moduleFactoryFn; + let didInstallFactory = false; + for(let j = i; j < end; j++){ + const id = chunkModules[j]; + if (!moduleFactories.has(id)) { + if (!didInstallFactory) { + if (factoryToInstall === moduleFactoryFn) { + applyModuleFactoryName(moduleFactoryFn); + } + didInstallFactory = true; + } + moduleFactories.set(id, factoryToInstall); + newModuleId?.(id); + } + } + i = end + 1; // end is pointing at the last factory advance to the next id or the end of the array. + } +} +/** + * A pseudo "fake" URL object to resolve to its relative path. + * + * When UrlRewriteBehavior is set to relative, calls to the `new URL()` will construct url without base using this + * runtime function to generate context-agnostic urls between different rendering context, i.e ssr / client to avoid + * hydration mismatch. + * + * This is based on webpack's existing implementation: + * https://github.com/webpack/webpack/blob/87660921808566ef3b8796f8df61bd79fc026108/lib/runtime/RelativeUrlRuntimeModule.js + */ const relativeURL = function relativeURL(inputUrl) { + const realUrl = new URL(inputUrl, 'x:/'); + const values = {}; + for(const key in realUrl)values[key] = realUrl[key]; + values.href = inputUrl; + values.pathname = inputUrl.replace(/[?#].*/, ''); + values.origin = values.protocol = ''; + values.toString = values.toJSON = (..._args)=>inputUrl; + for(const key in values)Object.defineProperty(this, key, { + enumerable: true, + configurable: true, + value: values[key] + }); +}; +relativeURL.prototype = URL.prototype; +contextPrototype.U = relativeURL; +/** + * Utility function to ensure all variants of an enum are handled. + */ function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} +/** + * Constructs an error message for when a module factory is not available. + */ function factoryNotAvailableMessage(moduleId, sourceType, sourceData) { + let instantiationReason; + switch(sourceType){ + case 0: + instantiationReason = `as a runtime entry of chunk ${sourceData}`; + break; + case 1: + instantiationReason = `because it was required from module ${sourceData}`; + break; + case 2: + instantiationReason = 'because of an HMR update'; + break; + default: + invariant(sourceType, (sourceType)=>`Unknown source type: ${sourceType}`); + } + return `Module ${moduleId} was instantiated ${instantiationReason}, but the module factory is not available.`; +} +/** + * Returns a `file://` URL under a synthetic directory named after `root` + * (`ROOT` for the project root), for when the real filesystem path is unknown. + * The root name and path segments are percent-encoded so the result is always + * a valid file URI. + */ function placeholderFileUrl(modulePath, root) { + return `file:///${encodeURIComponent(root ?? 'ROOT')}/${modulePath.split('/').map(encodeURIComponent).join('/')}`; +} +/** + * A stub function to make `require` available but non-functional in ESM. + */ function requireStub(_moduleId) { + throw new Error('dynamic usage of require is not supported'); +} +contextPrototype.z = requireStub; +// Make `globalThis` available to the module in a way that cannot be shadowed by a local variable. +contextPrototype.g = globalThis; +function applyModuleFactoryName(factory) { + // Give the module factory a nice name to improve stack traces. + Object.defineProperty(factory, 'name', { + value: 'module evaluation' + }); +} +/// +/// A 'base' utilities to support runtime can have externals. +/// Currently this is for node.js / edge runtime both. +/// If a fn requires node.js specific behavior, it should be placed in `node-external-utils` instead. +async function externalImport(id) { + let raw; + try { + raw = await import(id); + } catch (err) { + // TODO(alexkirsz) This can happen when a client-side module tries to load + // an external module we don't provide a shim for (e.g. querystring, url). + // For now, we fail semi-silently, but in the future this should be a + // compilation error. + throw new Error(`Failed to load external module ${id}: ${err}`); + } + if (raw && raw.__esModule && raw.default && 'default' in raw.default) { + return interopEsm(raw.default, createNS(raw), true); + } + return raw; +} +contextPrototype.y = externalImport; +function externalRequire(id, thunk, esm = false) { + let raw; + try { + raw = thunk(); + } catch (err) { + // TODO(alexkirsz) This can happen when a client-side module tries to load + // an external module we don't provide a shim for (e.g. querystring, url). + // For now, we fail semi-silently, but in the future this should be a + // compilation error. + throw new Error(`Failed to load external module ${id}: ${err}`); + } + if (!esm || raw.__esModule) { + return raw; + } + return interopEsm(raw, createNS(raw), true); +} +externalRequire.resolve = (id, options)=>{ + return require.resolve(id, options); +}; +contextPrototype.x = externalRequire; +/* eslint-disable @typescript-eslint/no-unused-vars */ const path = require('path'); +const relativePathToRuntimeRoot = path.relative(RUNTIME_PUBLIC_PATH, '.'); +// Compute the relative path to the `distDir`. +const relativePathToDistRoot = path.join(relativePathToRuntimeRoot, RELATIVE_ROOT_PATH); +const RUNTIME_ROOT = path.resolve(__filename, relativePathToRuntimeRoot); +// Compute the absolute path to the root, by stripping distDir from the absolute path to this file. +const ABSOLUTE_ROOT = path.resolve(__filename, relativePathToDistRoot); +/** + * Returns an absolute path to the given module path. + * Module path should be relative, either path to a file or a directory. + * + * This fn allows to calculate an absolute path for some global static values, such as + * `__dirname` or `import.meta.url` that Turbopack will not embeds in compile time. + * See ImportMetaBinding::code_generation for the usage. + */ function resolveAbsolutePath(modulePath) { + if (modulePath) { + return path.join(ABSOLUTE_ROOT, modulePath); + } + return ABSOLUTE_ROOT; +} +Context.prototype.P = resolveAbsolutePath; +/** + * Returns an absolute `file://` URL for the given module path, which is + * relative to the project root or the named `root`. + * + * Uses `url.pathToFileURL` so that the resulting URL is a valid file URI on + * all platforms (forward slashes on Windows, drive letters handled + * correctly, path segments URL-encoded). + * + * The location of a named `root` isn't known at runtime (the output may have + * been moved away from the sources), so this returns a placeholder URL for it. + */ function resolveFileUrl(modulePath, root) { + if (root !== undefined) { + return placeholderFileUrl(modulePath, root); + } + return require('url').pathToFileURL(resolveAbsolutePath(modulePath)).href; +} +Context.prototype.F = resolveFileUrl; +/* eslint-disable @typescript-eslint/no-unused-vars */ /// +/// +/// +/// +/** + * Base Node.js runtime shared between production and development. + * Contains chunk loading, module caching, and other non-HMR functionality. + */ process.env.TURBOPACK = '1'; +const url = require('url'); +const moduleFactories = new Map(); +const moduleCache = new Map(); +/** + * Returns an absolute path to the given module's id. + */ function resolvePathFromModule(moduleId) { + const exported = this.r(moduleId); + const exportedPath = exported?.default ?? exported; + if (typeof exportedPath !== 'string') { + return exported; + } + const strippedAssetPrefix = exportedPath.slice(ASSET_PREFIX.length); + const resolved = path.resolve(RUNTIME_ROOT, strippedAssetPrefix); + return url.pathToFileURL(resolved).href; +} +/** + * Exports a URL value. No suffix is added in Node.js runtime. + */ function exportUrl(urlValue, id) { + exportValue.call(this, urlValue, id); +} +function loadRuntimeChunk(sourcePath, chunkData) { + if (typeof chunkData === 'string') { + loadRuntimeChunkPath(sourcePath, chunkData); + } else { + loadRuntimeChunkPath(sourcePath, chunkData.path); + } +} +const loadedChunks = new Set(); +const unsupportedLoadChunk = Promise.resolve(undefined); +const loadedChunk = Promise.resolve(undefined); +const chunkCache = new Map(); +function clearChunkCache() { + chunkCache.clear(); + loadedChunks.clear(); +} +function loadRuntimeChunkPath(sourcePath, chunkPath) { + if (!isJs(chunkPath)) { + // We only support loading JS chunks in Node.js. + // This branch can be hit when trying to load a CSS chunk. + return; + } + if (loadedChunks.has(chunkPath)) { + return; + } + try { + const resolved = path.resolve(RUNTIME_ROOT, chunkPath); + const chunkModules = require(resolved); + installCompressedModuleFactories(chunkModules, 0, moduleFactories); + loadedChunks.add(chunkPath); + } catch (cause) { + let errorMessage = `Failed to load chunk ${chunkPath}`; + if (sourcePath) { + errorMessage += ` from runtime for chunk ${sourcePath}`; + } + const error = new Error(errorMessage, { + cause + }); + error.name = 'ChunkLoadError'; + throw error; + } +} +function loadChunkAsync(chunkData) { + const chunkPath = typeof chunkData === 'string' ? chunkData : chunkData.path; + if (!isJs(chunkPath)) { + // We only support loading JS chunks in Node.js. + // This branch can be hit when trying to load a CSS chunk. + return unsupportedLoadChunk; + } + let entry = chunkCache.get(chunkPath); + if (entry === undefined) { + try { + // resolve to an absolute path to simplify `require` handling + const resolved = path.resolve(RUNTIME_ROOT, chunkPath); + // TODO: consider switching to `import()` to enable concurrent chunk loading and async file io + // However this is incompatible with hot reloading (since `import` doesn't use the require cache) + const chunkModules = require(resolved); + installCompressedModuleFactories(chunkModules, 0, moduleFactories); + entry = loadedChunk; + } catch (cause) { + const errorMessage = `Failed to load chunk ${chunkPath} from module ${this.m.id}`; + const error = new Error(errorMessage, { + cause + }); + error.name = 'ChunkLoadError'; + // Cache the failure promise, future requests will also get this same rejection + entry = Promise.reject(error); + } + chunkCache.set(chunkPath, entry); + } + // TODO: Return an instrumented Promise that React can use instead of relying on referential equality. + return entry; +} +contextPrototype.l = loadChunkAsync; +function loadChunkAsyncByUrl(chunkUrl) { + const path1 = url.fileURLToPath(new URL(chunkUrl, RUNTIME_ROOT)); + return loadChunkAsync.call(this, path1); +} +contextPrototype.L = loadChunkAsyncByUrl; +// Shared runtime primitive: the root that on-disk chunk paths are resolved +// against. Used by the bundled wasm helper (exposed as `__turbopack_runtime_root__`). +contextPrototype.w = RUNTIME_ROOT; +const regexJsUrl = /\.js(?:\?[^#]*)?(?:#.*)?$/; +/** + * Checks if a given path/URL ends with .js, optionally followed by ?query or #fragment. + */ function isJs(chunkUrlOrPath) { + return regexJsUrl.test(chunkUrlOrPath); +} +/* eslint-disable @typescript-eslint/no-unused-vars */ /// +/** + * Production Node.js runtime. + * Uses ModuleWithDirection and simple module instantiation without HMR support. + */ // moduleCache and moduleFactories are declared in runtime-base.ts +// this is read in runtime-utils.ts so it creates a module with direction for hmr +createModuleWithDirectionFlag = true; +const nodeContextPrototype = Context.prototype; +nodeContextPrototype.q = exportUrl; +nodeContextPrototype.M = moduleFactories; +// Cast moduleCache to ModuleWithDirection for production mode +nodeContextPrototype.c = moduleCache; +nodeContextPrototype.R = resolvePathFromModule; +nodeContextPrototype.C = clearChunkCache; +function instantiateModule(id, sourceType, sourceData) { + const moduleFactory = moduleFactories.get(id); + if (typeof moduleFactory !== 'function') { + // This can happen if modules incorrectly handle HMR disposes/updates, + // e.g. when they keep a `setTimeout` around which still executes old code + // and contains e.g. a `require("something")` call. + throw new Error(factoryNotAvailableMessage(id, sourceType, sourceData)); + } + const module1 = createModuleWithDirection(id); + const exports = module1.exports; + moduleCache.set(id, module1); + const context = new Context(module1, exports); + // NOTE(alexkirsz) This can fail when the module encounters a runtime error. + try { + moduleFactory(context, module1, exports); + } catch (error) { + module1.error = error; + throw error; + } + ; + module1.loaded = true; + if (module1.namespaceObject && module1.exports !== module1.namespaceObject) { + // in case of a circular dependency: cjs1 -> esm2 -> cjs1 + interopEsm(module1.exports, module1.namespaceObject); + } + return module1; +} +/** + * Retrieves a module from the cache, or instantiate it if it is not cached. + */ // @ts-ignore +function getOrInstantiateModuleFromParent(id, sourceModule) { + const module1 = moduleCache.get(id); + if (module1) { + if (module1.error) { + throw module1.error; + } + return module1; + } + return instantiateModule(id, SourceType.Parent, sourceModule.id); +} +/** + * Instantiates a runtime module. + */ function instantiateRuntimeModule(chunkPath, moduleId) { + return instantiateModule(moduleId, SourceType.Runtime, chunkPath); +} +/** + * Retrieves a module from the cache, or instantiate it as a runtime module if it is not cached. + */ // @ts-ignore TypeScript doesn't separate this module space from the browser runtime +function getOrInstantiateRuntimeModule(chunkPath, moduleId) { + const module1 = moduleCache.get(moduleId); + if (module1) { + if (module1.error) { + throw module1.error; + } + return module1; + } + return instantiateRuntimeModule(chunkPath, moduleId); +} +module.exports = (sourcePath)=>({ + m: (id)=>getOrInstantiateRuntimeModule(sourcePath, id), + c: (chunkData)=>loadRuntimeChunk(sourcePath, chunkData) + }); + + +//# sourceMappingURL=%5Bturbopack%5D_runtime.js.map \ No newline at end of file diff --git a/packages/adapter/test/fixtures/pinned-next.ts b/packages/adapter/test/fixtures/pinned-next.ts index 6667600b1..1ba3097f5 100644 --- a/packages/adapter/test/fixtures/pinned-next.ts +++ b/packages/adapter/test/fixtures/pinned-next.ts @@ -1,3 +1,4 @@ +import { readFileSync } from 'node:fs'; import { createRequire } from 'node:module'; const require = createRequire(import.meta.url); @@ -9,3 +10,6 @@ const require = createRequire(import.meta.url); export function resolvePinnedNext(specifier: string): string { return require.resolve(specifier.replace(/^next\//u, 'next-16-4-3/')); } + +/** The published next@16.4.3 `package.json`, byte for byte, for a package root a suite builds. */ +export const PINNED_NEXT_PACKAGE_JSON = readFileSync(resolvePinnedNext('next/package.json')); diff --git a/packages/adapter/test/node-fast-jwt-guard-integration.test.ts b/packages/adapter/test/node-fast-jwt-guard-integration.test.ts new file mode 100644 index 000000000..c524aa5c1 --- /dev/null +++ b/packages/adapter/test/node-fast-jwt-guard-integration.test.ts @@ -0,0 +1,94 @@ +import { execFile } from 'node:child_process'; +import { copyFile, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; +import { createRequire } from 'node:module'; +import { tmpdir } from 'node:os'; +import path from 'node:path'; +import { promisify } from 'node:util'; + +import { describe, expect, it } from 'vitest'; + +const execute = promisify(execFile); +const ENABLED = process.env['UPWIND_NATIVE_FUNCTION_INTEGRATION'] === '1'; +const require = createRequire(import.meta.url); +const GUARD = new URL('../../runtime/src/node-jwt-guard.ts', import.meta.url).href; +const RECORD = { + schema: 1, + profile: 'next-16.4.4-fast-jwt-ncc-v1', + nodeVersion: process.versions.node, + archivePath: 'node_modules/next/dist/compiled/fast-jwt/index.js', + sha256: '8cc4cea3ed6b85a6771a2546bbb88dcd1ca21e6061d89057728d3da4ecccfabf', + bytes: 131_737, +}; + +/** Real loader/source integration: CI only; never part of local source-only controls. */ +async function scenario(before: string, after: string, changed = false): Promise { + const directory = await mkdtemp(path.join(tmpdir(), 'upwind-fast-jwt-guard-')); + try { + const vendor = path.join(directory, 'node_modules/next'); + const upwind = path.join(directory, 'upwind'); + await mkdir(path.join(vendor, 'dist/compiled/fast-jwt'), { recursive: true }); + await mkdir(upwind); + const jwt = path.join(vendor, 'dist/compiled/fast-jwt/index.js'); + await copyFile(require.resolve('next/dist/compiled/fast-jwt'), jwt); + await copyFile(require.resolve('next/package.json'), path.join(vendor, 'package.json')); + if (changed) await writeFile(jwt, `${await readFile(jwt, 'utf8')}\nglobalThis.changed=true;`); + const server = path.join(upwind, 'server.mjs'); + await writeFile( + server, + `import {createRequire} from 'node:module'; +import {installNodeJwtGuards} from ${JSON.stringify(GUARD)}; +const require=createRequire(import.meta.url); +const filename=new URL('../node_modules/next/dist/compiled/fast-jwt/index.js',import.meta.url); +const record=${JSON.stringify(RECORD)}; +${before} +installNodeJwtGuards(import.meta.url,[record]); +${after} +`, + ); + const { stdout } = await execute(process.execPath, [server], { + env: { NODE_ENV: 'production' }, + timeout: 10_000, + maxBuffer: 65_536, + }); + return stdout.trim(); + } finally { + await rm(directory, { recursive: true, force: true }); + } +} + +const LOAD = `const {createSigner,createVerifier}=require(filename.pathname);`; + +describe.skipIf(!ENABLED)('fixed Node fast-jwt evaluation guard (dedicated Linux CI)', () => { + it('loads the original source and signs and verifies preview data as Next.js does', async () => { + const result = await scenario( + '', + `${LOAD} +const token=createSigner({key:'authored-ci-key',algorithm:'HS256',expiresIn:60_000})({data:'native'}); +if(createVerifier({key:'authored-ci-key',algorithms:['HS256']})(token).data!=='native') + throw new Error('verify mismatch'); +console.log('unchanged-fast-jwt');`, + ); + + expect(result).toBe('unchanged-fast-jwt'); + }); + + it('refuses changed source bytes before they are evaluated', async () => { + await expect(scenario('', LOAD, true)).rejects.toThrow( + 'qualified source changed before evaluation', + ); + }); + + it('rejects a record for the other reviewed Node release', async () => { + const other = process.versions.node === '24.22.1' ? '26.13.0' : '24.22.1'; + + await expect(scenario(`record.nodeVersion=${JSON.stringify(other)};`, '')).rejects.toThrow( + 'invalid build-produced source record', + ); + }); + + it('refuses a record that names jsonwebtoken’s profile for fast-jwt’s bytes', async () => { + await expect(scenario(`record.profile='next-16.4.3-jsonwebtoken-ncc-v1';`, '')).rejects.toThrow( + 'invalid fixed source profile', + ); + }); +}); diff --git a/packages/adapter/test/node-jwt-compiler-proof.test.ts b/packages/adapter/test/node-jwt-compiler-proof.test.ts index ff0395e98..4ae99c623 100644 --- a/packages/adapter/test/node-jwt-compiler-proof.test.ts +++ b/packages/adapter/test/node-jwt-compiler-proof.test.ts @@ -17,6 +17,7 @@ import { import { nodeJwtGlobalRecoverySite, nodeJwtRequireOnly } from '../src/node-jwt-compiler-sites.ts'; import type { ManifestModuleGraph } from '../src/node-manifest-graph.ts'; import { prepareOriginalNodeModes, originalNodeModeHash } from '../src/node-original-load-mode.ts'; +import { PINNED_NEXT_PACKAGE_JSON } from './fixtures/pinned-next.ts'; describe('fixed JWT compiler site DATA', () => { it.each([undefined, null, 0, 'source text', {}])( @@ -173,39 +174,52 @@ describe('fixed JWT proof is private and conditional', () => { ).toContain('loader'); }); - it('does not accept a canonical filename, package declaration or source hash as route approval', async () => { - const root = await mkdtemp(path.join(os.tmpdir(), 'jwt-site-proof-')); - try { - const packageRoot = path.join(root, 'node_modules/next'); - const file = path.join(packageRoot, 'dist/compiled/jsonwebtoken/index.js'); - await mkdir(path.dirname(file), { recursive: true }); - await writeFile(path.join(packageRoot, 'package.json'), '{"name":"next","version":"16.4.3"}'); - const code = "module.exports={sign:()=>Function('return this')()};"; - await writeFile(file, code); - const graph: ManifestModuleGraph = { - sources: new Map([[file, { file, sha256: originalNodeModeHash(code) }]]), - importers: new Map([[file, []]]), - retained: new Set([file]), - rendered: new Map([[file, code]]), - }; - const originalModes = await prepareOriginalNodeModes( - { rootDir: root, projectDir: root, distDir: root }, - graph, - [file], - ); - - await expect( - qualifyNodeJwtCompiler({ + it.each([ + { + library: 'jsonwebtoken', + code: "module.exports={sign:()=>Function('return this')()};", + refusal: 'compiler', + }, + { + library: 'fast-jwt', + code: 'module.exports={sign:()=>process.binding("buffer")};', + refusal: 'loader', + }, + ])( + 'does not accept a canonical filename, package declaration or source hash as route approval for $library', + async ({ library, code, refusal }) => { + const root = await mkdtemp(path.join(os.tmpdir(), 'jwt-site-proof-')); + try { + const packageRoot = path.join(root, 'node_modules/next'); + const file = path.join(packageRoot, `dist/compiled/${library}/index.js`); + await mkdir(path.dirname(file), { recursive: true }); + await writeFile(path.join(packageRoot, 'package.json'), PINNED_NEXT_PACKAGE_JSON); + await writeFile(file, code); + const graph: ManifestModuleGraph = { + sources: new Map([[file, { file, sha256: originalNodeModeHash(code) }]]), + importers: new Map([[file, []]]), + retained: new Set([file]), + rendered: new Map([[file, code]]), + }; + const originalModes = await prepareOriginalNodeModes( + { rootDir: root, projectDir: root, distDir: root }, graph, - originalModes, - patches: [], - context: { distDir: root }, - manifests: {}, - }), - ).rejects.toThrow('completed original SDK route closure'); - expect(dynamicNodeClosureRefusal(file, code)).toContain('compiler'); - } finally { - await rm(root, { recursive: true, force: true }); - } - }); + [file], + ); + + await expect( + qualifyNodeJwtCompiler({ + graph, + originalModes, + patches: [], + context: { distDir: root }, + manifests: {}, + }), + ).rejects.toThrow('completed original SDK route closure'); + expect(dynamicNodeClosureRefusal(file, code)).toContain(refusal); + } finally { + await rm(root, { recursive: true, force: true }); + } + }, + ); }); diff --git a/packages/adapter/test/node-jwt-fast-sites.test.ts b/packages/adapter/test/node-jwt-fast-sites.test.ts new file mode 100644 index 000000000..bd7e68a65 --- /dev/null +++ b/packages/adapter/test/node-jwt-fast-sites.test.ts @@ -0,0 +1,84 @@ +import { createHash } from 'node:crypto'; +import { readFileSync } from 'node:fs'; +import { createRequire } from 'node:module'; +import path from 'node:path'; + +import { parseAst } from 'rolldown/parseAst'; +import { describe, expect, it, vi } from 'vitest'; + +import { dynamicNodeClosureRefusal } from '../src/node-closure-proof.ts'; +import { NODE_JWT_COMPILER_PROFILES } from '../src/node-jwt-compiler-profiles.ts'; +import type { NodeJwtCompilerProof } from '../src/node-jwt-compiler-proof.ts'; +import { nodeFastJwtBindingSite } from '../src/node-jwt-fast-sites.ts'; + +// The proof itself needs a whole qualified graph; here it stands for one that reads fast-jwt's site. +vi.mock(import('../src/node-jwt-compiler-proof.ts'), async () => { + const sites = await import('../src/node-jwt-fast-sites.ts'); + return { + nodeJwtCompilerSite: (proof: unknown, _file: string, _code: string, program: unknown) => { + const node = proof === undefined ? undefined : sites.nodeFastJwtBindingSite(program); + return node === undefined ? undefined : { kind: 'loader', node }; + }, + }; +}); + +const require = createRequire(import.meta.url); +const ROOTS = { + '16.4.3': path.dirname(require.resolve('next-16-4-3/package.json')), + '16.4.4': path.dirname(require.resolve('next/package.json')), +} as const; +const FILE = path.join(ROOTS['16.4.4'], 'dist/compiled/fast-jwt/index.js'); +const CODE = readFileSync(FILE, 'utf8'); +const GUARD = + 'if(!o.kStringMaxLength){try{o.kStringMaxLength=process.binding("buffer").kStringMaxLength}catch(e){}}'; +/** The same read as Rolldown may render it, with no braces and no catch binding. */ +const BRACELESS = + 'if(!o.kStringMaxLength)try{o.kStringMaxLength=process.binding("buffer").kStringMaxLength}catch{}'; + +function withGuard(replacement: string): string { + return CODE.replace(GUARD, () => replacement); +} +function siteIn(code: string): string | undefined { + const site = nodeFastJwtBindingSite(parseAst(code, { sourceType: 'commonjs' })); + return site === undefined ? undefined : code.slice(site.start, site.end); +} + +describe("fast-jwt's one Node loader read, in Next.js 16.4.4", () => { + it('holds each JWT profile to its release’s installed bytes', () => { + for (const profile of NODE_JWT_COMPILER_PROFILES) { + const bytes = readFileSync(path.join(ROOTS[profile.release], profile.suffix)); + + expect([profile.name, bytes.byteLength]).toStrictEqual([profile.name, profile.bytes]); + expect(createHash('sha256').update(bytes).digest('hex')).toBe(profile.sha256); + } + }); + + it('finds the guarded process.binding("buffer") read of safer-buffer, and only that', () => { + expect(CODE.split(GUARD)).toHaveLength(2); + expect(siteIn(CODE)).toBe('process.binding'); + expect(siteIn(withGuard(BRACELESS))).toBe('process.binding'); + }); + + it.each([ + ['another binding', GUARD.replace('"buffer"', '"util"')], + ['a second read', `${GUARD};process.binding("buffer");`], + ['a computed read', `${GUARD};process["binding"];`], + ['a code compiler', `${GUARD};eval("1");`], + ['a handled failure', GUARD.replace('catch(e){}', 'catch(e){o.failed=1}')], + ['a finalizer', GUARD.replace('catch(e){}', 'catch(e){}finally{}')], + ['an alternative', `${GUARD.slice(0, -1)}}else{}`], + ['another guard', GUARD.replace('if(!o.', 'if(!a.')], + ['an unguarded read', GUARD.replace('if(!o.kStringMaxLength)', '')], + ])('refuses %s', (_name, replacement) => { + expect(siteIn(withGuard(replacement))).toBeUndefined(); + }); + + it('leaves nothing else of the bundle to the closure walker to refuse', () => { + const proof = {} as NodeJwtCompilerProof; + + expect(dynamicNodeClosureRefusal(FILE, CODE)).toBe( + 'an unproved Node module loader or code compiler', + ); + expect(dynamicNodeClosureRefusal(FILE, CODE, proof)).toBeUndefined(); + }); +}); diff --git a/packages/adapter/test/node-manifest-handler-integration.test.ts b/packages/adapter/test/node-manifest-handler-integration.test.ts index 9657d58ee..c940570a4 100644 --- a/packages/adapter/test/node-manifest-handler-integration.test.ts +++ b/packages/adapter/test/node-manifest-handler-integration.test.ts @@ -69,6 +69,8 @@ describe('private Pages handler source integration', () => { expect(NODE_MANIFEST_HANDLER_PROFILES.map((profile) => profile.name)).toStrictEqual([ 'next-16.4.3-public-pages-handler', 'next-16.4.3-public-pages-server-minification-off', + 'next-16.4.4-public-pages-handler', + 'next-16.4.4-public-pages-server-minification-off', ]); expect( NODE_MANIFEST_HANDLER_PROFILES.every((profile) => { diff --git a/packages/adapter/test/node-manifest-proof.test.ts b/packages/adapter/test/node-manifest-proof.test.ts index c3fc15340..9baf259be 100644 --- a/packages/adapter/test/node-manifest-proof.test.ts +++ b/packages/adapter/test/node-manifest-proof.test.ts @@ -12,6 +12,7 @@ import { appEntrySource } from '../src/app-entry.ts'; import { dynamicNodeClosureRefusal } from '../src/node-closure-proof.ts'; import { assertClientReferenceManifest, proveNodeManifests } from '../src/node-manifest-proof.ts'; import { assertNodeProofFiles, proveNodeFunction } from '../src/node-proof.ts'; +import { PINNED_NEXT_PACKAGE_JSON } from './fixtures/pinned-next.ts'; const PAGE = '/native/page'; const SUFFIX = '_client-reference-manifest.js'; @@ -95,7 +96,7 @@ async function fixture(directory: string, directRuntime?: (typeof APP_PAGE_RUNTI appEntrySource(entries, { external: true, runtimeDistDir: distDir, nodeRequestMeta: true }), ].join('\n'); await mkdir(path.dirname(manifest), { recursive: true }); - await writeFile(path.join(root, 'package.json'), '{"name":"next","version":"16.4.3"}'); + await writeFile(path.join(root, 'package.json'), PINNED_NEXT_PACKAGE_JSON); await writeFile(manifest, withDeploymentId()); await writeFile(route, routeSource(producer)); await writeFile(analysisEntry, rootCode); diff --git a/packages/adapter/test/node-manifest-registration-pass.test.ts b/packages/adapter/test/node-manifest-registration-pass.test.ts index ec0ffca8e..2efcbf54e 100644 --- a/packages/adapter/test/node-manifest-registration-pass.test.ts +++ b/packages/adapter/test/node-manifest-registration-pass.test.ts @@ -229,7 +229,13 @@ describe('scoped immutable registration passes', () => { ); }); - it.each(['exportedId', 'evaluations', 'initialChunks', 'lookupTables'] as const)( + it.each([ + 'exportedId', + 'evaluations', + 'initialChunks', + 'lookupTables', + 'installedTables', + ] as const)( 'rejects changed secondary boot %s before the result leaves its scope', async (key) => { const own = await fixture(); diff --git a/packages/adapter/test/node-manifest-runtime-cohort.test.ts b/packages/adapter/test/node-manifest-runtime-cohort.test.ts index 368402664..2d0301c59 100644 --- a/packages/adapter/test/node-manifest-runtime-cohort.test.ts +++ b/packages/adapter/test/node-manifest-runtime-cohort.test.ts @@ -37,11 +37,14 @@ const ENTRIES = [ function boot(file: string, runtimeFile: string, id: number): string { return `var R=require(${jsLiteral(path.posix.relative(path.posix.dirname(file), runtimeFile))})(${jsLiteral(path.posix.relative(DIST, file))});R.c("server/chunks/app.js");R.m(${id});module.exports=R.m(${id}).exports;`; } -async function runtime(file: string, prefix: string): Promise { - const original = await readFile( - new URL('fixtures/node-runtime/production.txt', import.meta.url), - 'utf8', - ); +/** Each release's production runtime snapshot: 16.4.4's installs no strict factory table. */ +const RUNTIMES = { + '16.4.3': 'fixtures/node-runtime/production.txt', + '16.4.4': 'fixtures/node-runtime-16.4.4/production.txt', +} as const; +type Release = keyof typeof RUNTIMES; +async function runtime(file: string, prefix: string, release: Release = '16.4.3'): Promise { + const original = await readFile(new URL(RUNTIMES[release], import.meta.url), 'utf8'); return original .replace( 'var RUNTIME_PUBLIC_PATH = "output/[turbopack]_runtime.js";', @@ -64,7 +67,7 @@ async function inventory(sources: readonly NodeManifestTurbopackGraphFile[]) { retained: new Set(sources.map((item) => item.id)), }); } -async function fixture() { +async function fixture(release: Release = '16.4.3') { const sources: NodeManifestTurbopackGraphFile[] = [ { id: ENTRY, @@ -81,7 +84,7 @@ async function fixture() { { id: RUNTIME, file: RUNTIME, - source: source(await runtime(RUNTIME, PREFIX)), + source: source(await runtime(RUNTIME, PREFIX, release)), imports: [ { specifier: CHUNK, target: CHUNK }, { specifier: RUNTIME, target: RUNTIME }, @@ -91,7 +94,7 @@ async function fixture() { { id: API_RUNTIME, file: API_RUNTIME, - source: source(await runtime(API_RUNTIME, '/')), + source: source(await runtime(API_RUNTIME, '/', release)), imports: [ { specifier: CHUNK, target: CHUNK }, { specifier: RUNTIME, target: RUNTIME }, @@ -377,4 +380,59 @@ describe('owned mixed runtime cohort', () => { expect(() => prepare(input)).toThrow('Unproved Next.js Turbopack'); }); + + const NESTED = 'module.exports=[[1,(ctx)=>{ctx.v(42);}],9,(ctx)=>{ctx.v(43);}];'; + + it.each(['16.4.3', '16.4.4'] as const)( + 'leaves a nested array alone in a CommonJS source no %s runtime loads', + async (release) => { + const input = await fixture(release); + const unrelated = `${ROOT}/node_modules/matrix/index.js`; + input.sources.push({ + id: unrelated, + file: unrelated, + source: source('module.exports=[[1,2],[3,4]];'), + imports: [], + }); + + expect(prepare(input).graphs).toHaveLength(1); + }, + ); + + it.each(['16.4.3', '16.4.4'] as const)( + 'leaves a nested chunk alone that a %s runtime can look up but never installs', + async (release) => { + const input = await fixture(release); + const lookup = `${DIST}/server/chunks/lookup-only.js`; + for (const [index, item] of input.sources.entries()) + if (item.id === RUNTIME || item.id === API_RUNTIME) + input.sources[index] = { + ...item, + imports: [...item.imports, { specifier: lookup, target: lookup }], + }; + input.sources.push({ id: lookup, file: lookup, source: source(NESTED), imports: [] }); + const owned = { ...input, inventory: await inventory(input.sources) }; + + const prepared = prepare(owned); + + expect(prepared.allGraphs.flatMap((graph) => graph.lookupTables ?? [])).toContain(lookup); + expect(prepared.allGraphs.flatMap((graph) => graph.installedTables ?? [])).not.toContain( + lookup, + ); + }, + ); + + it('reads a leading strict factory table where the 16.4.3 runtime installs it', async () => { + const input = edit(await fixture(), CHUNK, () => NESTED); + + expect(prepare(input).graphs).toHaveLength(1); + }); + + it('refuses a leading strict factory table the 16.4.4 runtime would not install', async () => { + const input = edit(await fixture('16.4.4'), CHUNK, () => NESTED); + + expect(() => prepare(input)).toThrow( + 'strict factory table beside a runtime that installs none', + ); + }); }); diff --git a/packages/adapter/test/node-manifest-turbopack-registration.test.ts b/packages/adapter/test/node-manifest-turbopack-registration.test.ts index ee2a8e3f2..f2df1c42b 100644 --- a/packages/adapter/test/node-manifest-turbopack-registration.test.ts +++ b/packages/adapter/test/node-manifest-turbopack-registration.test.ts @@ -5,6 +5,7 @@ import { describe, expect, it } from 'vitest'; import { certifyNodeManifestAppPageFactory } from '../src/node-manifest-app-page-factory.ts'; import { nodeManifestTurbopackFactories, + nodeManifestTurbopackNestedTable, projectNodeManifestTurbopackFactory, } from '../src/node-manifest-turbopack.ts'; @@ -147,3 +148,26 @@ describe('first grouping array in fixed Node factory registration', () => { expect(() => nodeManifestTurbopackFactories(source(code))).toThrow('Unproved Next.js'); }); }); + +describe('a leading strict factory table, which only 16.4.3 installs', () => { + it.each([ + ['plain table', 'module.exports=[[1,((ctx)=>{})],2,((ctx)=>{})];'], + [ + 'strict wrapper', + 'module.exports=[(()=>{"use strict";return[1,((ctx)=>{})];})(),2,((ctx)=>{})];', + ], + ])('is found in a %s', (_name, code) => { + expect(nodeManifestTurbopackNestedTable(source(code))).toBe(true); + }); + + it.each([ + ['flat table', 'module.exports=[1,((ctx)=>{"use strict";}),2,((ctx)=>{})];'], + ['wrapped flat table', '(()=>{"use strict";module.exports=[1,((ctx)=>{}),2,((ctx)=>{})];})();'], + [ + 'runtime or boot program', + 'const RUNTIME_PUBLIC_PATH = "server/chunks/[turbopack]_runtime.js";', + ], + ])('is not found in a %s', (_name, code) => { + expect(nodeManifestTurbopackNestedTable(source(code))).toBe(false); + }); +}); diff --git a/packages/adapter/test/node-manifest-turbopack-runtime.test.ts b/packages/adapter/test/node-manifest-turbopack-runtime.test.ts index e9a48c964..d8b06834d 100644 --- a/packages/adapter/test/node-manifest-turbopack-runtime.test.ts +++ b/packages/adapter/test/node-manifest-turbopack-runtime.test.ts @@ -28,15 +28,21 @@ const FORWARDED_GLOBALS = `if (typeof require !== 'undefined') { function source(code: string): { code: string; sha256: string } { return { code, sha256: createHash('sha256').update(code).digest('hex') }; } +/** The production runtime each release's snapshot holds; the async-module section is shared. */ +const PRODUCTION = { + '16.4.3': 'fixtures/node-runtime/production.txt', + '16.4.4': 'fixtures/node-runtime-16.4.4/production.txt', +} as const; async function input( options: { readonly asyncModules?: boolean; readonly renamedModule?: boolean; readonly forwardedGlobals?: boolean; + readonly release?: keyof typeof PRODUCTION; } = {}, ): Promise { const primary = await readFile( - new URL('fixtures/node-runtime/production.txt', import.meta.url), + new URL(PRODUCTION[options.release ?? '16.4.3'], import.meta.url), 'utf8', ); let body = primary.split('\n').slice(3).join('\n'); @@ -66,12 +72,16 @@ function changed( } describe('fixed producer-derived Turbopack Node runtime', () => { - it.each([ - { asyncModules: false }, - { asyncModules: true }, - { asyncModules: true, renamedModule: true }, - { asyncModules: true, forwardedGlobals: true }, - ])('seals the complete fixed runtime semantics for %j', async (options) => { + it.each( + (['16.4.3', '16.4.4'] as const).flatMap((release) => { + return [ + { asyncModules: false }, + { asyncModules: true }, + { asyncModules: true, renamedModule: true }, + { asyncModules: true, forwardedGlobals: true }, + ].map((options) => ({ ...options, release })); + }), + )('seals the complete fixed runtime semantics for %j', async (options) => { const own = await input(options); const proof = assertNodeManifestTurbopackRuntime(own); @@ -86,6 +96,8 @@ describe('fixed producer-derived Turbopack Node runtime', () => { options.asyncModules, true, ]); + // 16.4.4 reverted the strict module factory chunks, and its runtime installs no such table. + expect(proof.strictFactoryTables).toBe(options.release === '16.4.3'); }); it('accepts source formatting without allowing a changed runtime operation', async () => { diff --git a/packages/adapter/test/node-next-release.test.ts b/packages/adapter/test/node-next-release.test.ts new file mode 100644 index 000000000..274d7ea14 --- /dev/null +++ b/packages/adapter/test/node-next-release.test.ts @@ -0,0 +1,103 @@ +import { createHash } from 'node:crypto'; +import { readFileSync } from 'node:fs'; +import { createRequire } from 'node:module'; +import path from 'node:path'; + +import { describe, expect, it } from 'vitest'; + +import { isReviewedFrameworkSource } from '../src/node-manifest-framework.ts'; +import { + isForeignReleaseSource, + isReleaseSource, + NATIVE_NEXT_RELEASES, + nativeNextReleaseAt, + nativeNextReleaseOf, +} from '../src/node-next-release.ts'; + +const require = createRequire(import.meta.url); +/** Each supported release, installed: 16.4.3 under its alias, 16.4.4 as the catalog's Next.js. */ +const INSTALLED = [ + { version: '16.4.3', root: path.dirname(require.resolve('next-16-4-3/package.json')) }, + { version: '16.4.4', root: path.dirname(require.resolve('next/package.json')) }, +] as const; +/** Reviewed sources the two releases publish with the same bytes. */ +const SHARED = [ + 'dist/server/route-modules/app-page/module.compiled.js', + 'dist/server/route-modules/app-page/module.js', + 'dist/server/route-modules/app-route/module.compiled.js', + 'dist/server/route-modules/app-route/module.js', + 'dist/server/load-components.js', + 'dist/server/route-modules/pages-api/module.compiled.js', + 'dist/server/route-modules/pages-api/module.js', + 'dist/server/route-modules/pages/module.compiled.js', + 'dist/server/route-modules/pages/module.js', + 'dist/server/route-modules/route-module.js', +]; + +function sha256(file: string): string { + return createHash('sha256').update(readFileSync(file)).digest('hex'); +} + +describe('the Next.js releases the native Node path qualifies', () => { + it.each(INSTALLED)( + 'pins $version to its installed package, byte for byte', + ({ version, root }) => { + const release = NATIVE_NEXT_RELEASES.find((candidate) => candidate.version === version); + const sources = [...(release?.sources ?? [])].map(([relative, expected]) => { + return { + relative, + expected, + actual: sha256(path.join(root, relative)), + }; + }); + const shared = SHARED.filter((relative) => { + const actual = sha256(path.join(root, relative)); + return !isReviewedFrameworkSource(relative, actual); + }); + + expect(release?.packageJsonSha256).toBe(sha256(path.join(root, 'package.json'))); + expect(nativeNextReleaseAt(root)).toBe(release); + expect(sources).toHaveLength(13); + expect(sources.filter(({ expected, actual }) => expected !== actual)).toStrictEqual([]); + expect(shared).toStrictEqual([]); + }, + ); + + it('holds a release to its own bytes and refuses another release’s', () => { + const [older, newer] = NATIVE_NEXT_RELEASES; + const runtime = 'dist/compiled/next-server/pages-turbo.runtime.prod.js'; + const olderRuntime = older?.source(runtime) ?? ''; + + expect(newer?.source(runtime)).not.toBe(olderRuntime); + expect(isReleaseSource(runtime, olderRuntime)).toBe(true); + expect(isForeignReleaseSource(older!, runtime, olderRuntime)).toBe(false); + expect(isForeignReleaseSource(newer!, runtime, olderRuntime)).toBe(true); + expect(isForeignReleaseSource(newer!, runtime, 'f'.repeat(64))).toBe(false); + }); + + it('keeps every release pin immutable to the code that imports it', () => { + const [older] = NATIVE_NEXT_RELEASES; + const runtime = 'dist/compiled/next-server/pages-turbo.runtime.prod.js'; + const pinned = older?.source(runtime); + const pairs = older?.sources ?? []; + + expect(Object.isFrozen(NATIVE_NEXT_RELEASES)).toBe(true); + expect(NATIVE_NEXT_RELEASES.every((release) => Object.isFrozen(release))).toBe(true); + expect(Object.isFrozen(pairs)).toBe(true); + expect(pairs.every((pair) => Object.isFrozen(pair))).toBe(true); + expect(() => { + (pairs.find(([relative]) => relative === runtime) as [string, string])[1] = 'f'.repeat(64); + }).toThrow(TypeError); + expect(older?.source(runtime)).toBe(pinned); + }); + + it('names a release by its exact package bytes and version together', () => { + const [older, newer] = NATIVE_NEXT_RELEASES; + + expect(nativeNextReleaseOf(older!.packageJsonSha256, '16.4.3')).toBe(older); + expect(nativeNextReleaseOf(newer!.packageJsonSha256, '16.4.4')).toBe(newer); + expect(nativeNextReleaseOf(older!.packageJsonSha256, '16.4.4')).toBeUndefined(); + expect(nativeNextReleaseOf('f'.repeat(64), '16.4.3')).toBeUndefined(); + expect(nativeNextReleaseAt('/no/such/next')).toBeUndefined(); + }); +}); diff --git a/packages/adapter/test/node-pages-handler-inputs.test.ts b/packages/adapter/test/node-pages-handler-inputs.test.ts index f674340bd..07aedee52 100644 --- a/packages/adapter/test/node-pages-handler-inputs.test.ts +++ b/packages/adapter/test/node-pages-handler-inputs.test.ts @@ -6,6 +6,7 @@ import { describe, expect, it } from 'vitest'; import { FIXTURE_FILES, + NEXT_RELEASES, SERVER_MINIFICATION_OFF_CONFIG, } from '../../../tools/native-pages-handler-profile/inputs.mts'; import { @@ -15,6 +16,18 @@ import { const INPUTS = new URL('../../../tools/native-pages-handler-profile/', import.meta.url); const DEFAULT_FIXTURE = '2527f3cdc79784d6a1fd003332682d651f2b1b3bce5859eb2731e4f71b7a12d4'; +const INTEGRITY = /(integrity: )sha512-[\w+/]+=*/u; +/** A lock entry of `next` or an `@next/*` package at 16.4.4. */ +const NEXT_ENTRY = /^ {2}"?(?:next|@next\/[\w-]+)@16\.4\.4[:"(]/u; + +/** The lock entry, at two spaces, that a line belongs to. */ +function ownerOf(lines: readonly string[], index: number): string { + for (let at = index; at >= 0; at -= 1) { + const line = lines[at] ?? ''; + if (/^ {2}\S/u.test(line)) return line; + } + return ''; +} function digest(value: string | Uint8Array): string { return createHash('sha256').update(value).digest('hex'); @@ -37,6 +50,12 @@ function withoutConfig(profile: PagesHandlerProfileInputs) { return profile.files.filter((file) => file.target !== 'next.config.mjs'); } +function withoutRelease(profile: PagesHandlerProfileInputs) { + return profile.files.filter( + (file) => file.target !== 'package.json' && file.target !== 'pnpm-lock.yaml', + ); +} + describe('fixed public Pages producer input selection without running the producer', () => { it('keeps empty argv, all original fixture bytes and the original ordered identity', async () => { const profile = pagesHandlerProfileInputs([]); @@ -113,6 +132,71 @@ describe('fixed public Pages producer input selection without running the produc expect(digest(adapter)).toBe(FIXTURE_FILES['adapter.mjs']); }); + it('compiles with 16.4.3 unless asked, and replaces only the manifest and lock for 16.4.4', async () => { + const original = pagesHandlerProfileInputs([]); + const later = pagesHandlerProfileInputs(['--next=16.4.4']); + const off = pagesHandlerProfileInputs(['--next=16.4.4', '--server-minification=off']); + + expect(original.next).toStrictEqual({ version: '16.4.3', swc: NEXT_RELEASES['16.4.3'].swc }); + expect(later.next).toStrictEqual({ version: '16.4.4', swc: NEXT_RELEASES['16.4.4'].swc }); + expect(withoutRelease(later)).toStrictEqual(withoutRelease(original)); + expect( + later.files + .filter((file) => file.source.startsWith('fixture-16.4.4/')) + .map((file) => [file.target, file.sha256]), + ).toStrictEqual(Object.entries(NEXT_RELEASES['16.4.4'].files)); + await expect(bytePins(later)).resolves.toStrictEqual( + later.files.map((file) => [file.target, file.sha256]), + ); + expect(identity(later)).not.toBe(DEFAULT_FIXTURE); + expect(withoutConfig(off)).toStrictEqual(withoutConfig(later)); + }); + + it('names 16.4.4 output and artifacts apart from the unchanged 16.4.3 names', () => { + const later = pagesHandlerProfileInputs(['--next=16.4.4']); + const off = pagesHandlerProfileInputs(['--next=16.4.4', '--server-minification=off']); + + expect(later.outputName).toBe('independent-pages-handler-profile-next-16.4.4'); + expect(later.artifactName).toBe('UNQUALIFIED-public-pages-handler-next-16.4.4'); + expect(off.outputName).toBe( + 'independent-pages-handler-profile-next-16.4.4-server-minification-off', + ); + expect(off.artifactName).toBe( + 'UNQUALIFIED-public-pages-handler-next-16.4.4-server-minification-off', + ); + }); + + it.each(['package.json', 'pnpm-lock.yaml'])( + 'holds the 16.4.4 %s to 16.4.3 but for the release and its integrities', + async (target) => { + const original = (await readFile(new URL(`fixture/${target}`, INPUTS), 'utf8')).split('\n'); + const later = (await readFile(new URL(`fixture-16.4.4/${target}`, INPUTS), 'utf8')).split( + '\n', + ); + const changed = later.flatMap((line, index) => { + const before = original[index] ?? ''; + return line === before ? [] : [{ before, line, owner: ownerOf(later, index) }]; + }); + + expect(later).toHaveLength(original.length); + expect(changed.length).toBeGreaterThan(0); + + for (const { before, line, owner } of changed) { + const released = before.replaceAll('16.4.3', '16.4.4'); + // An integrity changes only in the entry of a package the release itself publishes. + const integrity = + NEXT_ENTRY.test(owner) && + line.replace(INTEGRITY, '$1') === released.replace(INTEGRITY, '$1'); + + expect({ line, owner, kept: line === released || integrity }).toStrictEqual({ + line, + owner, + kept: true, + }); + } + }, + ); + it.each([ [''], ['--server-minification=default'], @@ -120,15 +204,24 @@ describe('fixed public Pages producer input selection without running the produc ['--server-minification', 'off'], ['--server-minification=off', '--server-minification=off'], ['--server-minification=off', '--output=elsewhere'], + ['--server-minification=off', '--next=16.4.4'], + ['--next=16.4.3'], + ['--next=16.4.5'], + ['--next=16.4.4', '--next=16.4.4'], ['/candidate/next.config.mjs'], ['--help'], ])('rejects an unknown or additional argument sequence %j', (...args) => { expect(() => pagesHandlerProfileInputs(args)).toThrow( - 'Pass no arguments or exactly --server-minification=off', + 'Pass no arguments, or --next=16.4.4 and/or --server-minification=off in that order', ); }); - it.each([[], ['--server-minification=off']])('freezes the selected input plan %j', (...args) => { + it.each([ + [], + ['--server-minification=off'], + ['--next=16.4.4'], + ['--next=16.4.4', '--server-minification=off'], + ])('freezes the selected input plan %j', (...args) => { const profile = pagesHandlerProfileInputs(args); expect(Object.isFrozen(profile)).toBe(true); diff --git a/packages/adapter/test/node-proof.test.ts b/packages/adapter/test/node-proof.test.ts index 5715919ee..2d843fb77 100644 --- a/packages/adapter/test/node-proof.test.ts +++ b/packages/adapter/test/node-proof.test.ts @@ -11,6 +11,7 @@ import { proveNodeFunction, } from '../src/node-proof.ts'; import { vercelOgImageResponsePatch } from '../src/patches/vercel-og.ts'; +import { PINNED_NEXT_PACKAGE_JSON } from './fixtures/pinned-next.ts'; describe('original physical Node Function proof', () => { let directory: string; @@ -132,7 +133,7 @@ describe('original physical Node Function proof', () => { const second = path.join(path.dirname(original), 'second.node'); await mkdir(path.dirname(response), { recursive: true }); await mkdir(path.dirname(original), { recursive: true }); - await writeFile(path.join(next, 'package.json'), '{"name":"next","version":"16.4.3"}'); + await writeFile(path.join(next, 'package.json'), PINNED_NEXT_PACKAGE_JSON); const source = "exports.load=()=>import(process.env.NEXT_RUNTIME === 'edge' ? 'next/dist/compiled/@vercel/og/index.edge.js' : 'next/dist/compiled/@vercel/og/index.node.js');\n"; await writeFile(response, source); diff --git a/packages/adapter/test/node-route-rewrite-approvals.test.ts b/packages/adapter/test/node-route-rewrite-approvals.test.ts index 72c23d01f..20de84cc1 100644 --- a/packages/adapter/test/node-route-rewrite-approvals.test.ts +++ b/packages/adapter/test/node-route-rewrite-approvals.test.ts @@ -15,6 +15,7 @@ import { completedNodeRouteRewriteApproval, type NodeRouteRewriteApprovalBinding, } from '../src/node-route-rewrite-approval.ts'; +import { PINNED_NEXT_PACKAGE_JSON } from './fixtures/pinned-next.ts'; function fixture() { const entries = [{ id: '/page', filePath: '/authored/.next/page.js' }]; @@ -381,7 +382,7 @@ describe('multiple evaluator source aggregation', () => { const file = path.join(root, 'dist/server/load-manifest.external.js'); await mkdir(path.dirname(file), { recursive: true }); await writeFile(file, bytes); - await writeFile(path.join(root, 'package.json'), '{"name":"next","version":"16.4.3"}'); + await writeFile(path.join(root, 'package.json'), PINNED_NEXT_PACKAGE_JSON); sources.set(name, { file, sha256 }); } const graph: ManifestModuleGraph = { diff --git a/packages/adapter/test/node-turbopack-selector-integration.test.ts b/packages/adapter/test/node-turbopack-selector-integration.test.ts index bfbc7effd..1d62a9c4b 100644 --- a/packages/adapter/test/node-turbopack-selector-integration.test.ts +++ b/packages/adapter/test/node-turbopack-selector-integration.test.ts @@ -5,7 +5,7 @@ import path from 'node:path'; import { describe, expect, it } from 'vitest'; -import { reviewedFrameworkSource } from '../src/node-manifest-framework.ts'; +import { isReviewedFrameworkSource } from '../src/node-manifest-framework.ts'; import { nativeTurbopackSelectorPatch, nativeTurbopackSelectorSource, @@ -23,7 +23,9 @@ describe.skipIf(!ENABLED)('fixed native Turbopack selectors (dedicated CI)', () const file = path.join(root, relative); const code = await readFile(file, 'utf8'); - expect(createHash('sha256').update(code).digest('hex')).toBe(reviewedFrameworkSource(relative)); + expect( + isReviewedFrameworkSource(relative, createHash('sha256').update(code).digest('hex')), + ).toBe(true); expect([...code.matchAll(/process\.env\.TURBOPACK/gu)]).toHaveLength( route.startsWith('app-') ? 4 : 2, ); diff --git a/packages/runtime/src/node-initialization-builtins.ts b/packages/runtime/src/node-initialization-builtins.ts index 90bb79839..fc1c4d1d9 100644 --- a/packages/runtime/src/node-initialization-builtins.ts +++ b/packages/runtime/src/node-initialization-builtins.ts @@ -33,8 +33,10 @@ export function nodeInitializationBuiltins(require: NodeJS.Require): NodeInitial values: [ ['buffer', buffer], ['crypto', crypto], + ['node:crypto', crypto], ['stream', stream], ['util', util], + ['node:util', util], ['events', websocket.events], ['http', websocket.http], ['https', websocket.https], diff --git a/packages/runtime/src/node-jwt-guard.ts b/packages/runtime/src/node-jwt-guard.ts index 9acfcf666..d8acba909 100644 --- a/packages/runtime/src/node-jwt-guard.ts +++ b/packages/runtime/src/node-jwt-guard.ts @@ -52,6 +52,9 @@ const proxyCheck = isProxy; const PROFILE = 'next-16.4.3-jsonwebtoken-ncc-v1'; const SHA256 = '056c2dddba96faa35f4dc0740228a241990561290ff3bd5456c7c7dbc5a7a6b9'; const SOURCE_BYTES = 121_051; +const FAST_PROFILE = 'next-16.4.4-fast-jwt-ncc-v1'; +const FAST_SHA256 = '8cc4cea3ed6b85a6771a2546bbb88dcd1ca21e6061d89057728d3da4ecccfabf'; +const FAST_BYTES = 131_737; const WS_PROFILE = 'next-16.4.3-ws-ncc-v1'; const WS_SHA256 = 'af354cb13a11abe910cdbcb70943cb450b01b24f322026ae3f88d428d148395d'; const WS_BYTES = 45_840; @@ -61,6 +64,7 @@ const SEMVER_PROFILE = 'next-16.4.3-semver-noop-v1'; const SEMVER_SHA256 = '34c5853dd094229baa28a8b15b86e2143eeb0de0360530798ef3e9a7890942b6'; const SEMVER_BYTES = 471; const JWT_BUILTINS = ['buffer', 'crypto', 'stream', 'util'] as const; +const FAST_BUILTINS = ['buffer', 'node:crypto', 'node:util', 'util'] as const; const WS_BUILTINS = [ 'crypto', 'events', @@ -88,6 +92,11 @@ export type NodeJwtGuardRecord = GuardRecord & readonly sha256: typeof SHA256; readonly bytes: typeof SOURCE_BYTES; } + | { + readonly profile: typeof FAST_PROFILE; + readonly sha256: typeof FAST_SHA256; + readonly bytes: typeof FAST_BYTES; + } | { readonly profile: typeof WS_PROFILE; readonly sha256: typeof WS_SHA256; @@ -157,6 +166,12 @@ function sourceRecord(value: unknown): NodeJwtGuardRecord { record['bytes'] === SOURCE_BYTES ) return freeze({ ...common, profile: PROFILE, sha256: SHA256, bytes: SOURCE_BYTES }); + if ( + record['profile'] === FAST_PROFILE && + record['sha256'] === FAST_SHA256 && + record['bytes'] === FAST_BYTES + ) + return freeze({ ...common, profile: FAST_PROFILE, sha256: FAST_SHA256, bytes: FAST_BYTES }); if ( record['profile'] === WS_PROFILE && record['sha256'] === WS_SHA256 && @@ -300,6 +315,7 @@ function assertSource( function loadRequests(source: GuardedSource): readonly string[] { if (source.record.profile === PROFILE) return [...JWT_BUILTINS, SEMVER_SPECIFIER]; + if (source.record.profile === FAST_PROFILE) return FAST_BUILTINS; if (source.record.profile === WS_PROFILE) return [...WS_BUILTINS, ...WS_OPTIONAL_TARGETS]; return []; } diff --git a/packages/runtime/src/node-jwt-realm-fields.ts b/packages/runtime/src/node-jwt-realm-fields.ts index 4dc2e3567..84fffbc14 100644 --- a/packages/runtime/src/node-jwt-realm-fields.ts +++ b/packages/runtime/src/node-jwt-realm-fields.ts @@ -51,3 +51,17 @@ export const TYPE_PREDICATES = [ 'isSet', 'isTypedArray', ] as const; +/** What jsonwebtoken (16.4.3) uses of `crypto`, and fast-jwt (16.4.4) takes as it loads. */ +export const CRYPTO_FUNCTIONS = [ + 'KeyObject', + 'createSecretKey', + 'createPrivateKey', + 'createPublicKey', + 'createHash', + 'createHmac', + 'createSign', + 'createVerify', + 'timingSafeEqual', + 'sign', + 'verify', +] as const; diff --git a/packages/runtime/src/node-jwt-realm.ts b/packages/runtime/src/node-jwt-realm.ts index f5968889e..529804fde 100644 --- a/packages/runtime/src/node-jwt-realm.ts +++ b/packages/runtime/src/node-jwt-realm.ts @@ -9,7 +9,13 @@ import { withPristineNodeErrorHooks, type NodeErrorHooks, } from './node-error-hooks.ts'; -import { LOADER_ENV, PROCESS_KEYS, ROOTS, TYPE_PREDICATES } from './node-jwt-realm-fields.ts'; +import { + CRYPTO_FUNCTIONS, + LOADER_ENV, + PROCESS_KEYS, + ROOTS, + TYPE_PREDICATES, +} from './node-jwt-realm-fields.ts'; import { assertNodeWsRealm, assertNodeWsRealmStructure, @@ -356,13 +362,7 @@ function sealBuiltinBasis( capture.seal(requiredObject(data(stream, 'prototype'))); const crypto = requiredObject(data(basis, 'crypto')); for (const key of ['randomBytes', 'randomFillSync']) capture.optional(crypto, key); - capture.functions(crypto, [ - 'KeyObject', - 'createSecretKey', - 'createPrivateKey', - 'createPublicKey', - 'createHash', - ]); + capture.functions(crypto, CRYPTO_FUNCTIONS); const constructor = capture.functions(crypto, ['Hash'])[0]; if (constructor === undefined) fail('missing Hash constructor'); capture.seal(constructor); diff --git a/packages/runtime/test/node-jwt-realm.test.ts b/packages/runtime/test/node-jwt-realm.test.ts index 8fdc1c698..fb3486b38 100644 --- a/packages/runtime/test/node-jwt-realm.test.ts +++ b/packages/runtime/test/node-jwt-realm.test.ts @@ -119,6 +119,12 @@ function fixture( createPrivateKey: callback, createPublicKey: callback, createHash: callback, + createHmac: callback, + createSign: callback, + createVerify: callback, + timingSafeEqual: callback, + sign: callback, + verify: callback, Hash: authoredHash, }, stream: { Stream: stream }, diff --git a/packages/runtime/test/node-next-hooks.test.ts b/packages/runtime/test/node-next-hooks.test.ts index 4888333af..c7dcfb9f6 100644 --- a/packages/runtime/test/node-next-hooks.test.ts +++ b/packages/runtime/test/node-next-hooks.test.ts @@ -74,6 +74,12 @@ function fixture() { createPrivateKey: callback, createPublicKey: callback, createHash: callback, + createHmac: callback, + createSign: callback, + createVerify: callback, + timingSafeEqual: callback, + sign: callback, + verify: callback, Hash: authoredHash, randomBytes: callback, randomFillSync: callback, diff --git a/packages/runtime/test/node-ws-realm.test.ts b/packages/runtime/test/node-ws-realm.test.ts index c622abc94..dd951509d 100644 --- a/packages/runtime/test/node-ws-realm.test.ts +++ b/packages/runtime/test/node-ws-realm.test.ts @@ -106,6 +106,12 @@ function fixture(websocket = true) { createSecretKey: callback(), createPrivateKey: callback(), createPublicKey: callback(), + createHmac: callback(), + createSign: callback(), + createVerify: callback(), + timingSafeEqual: callback(), + sign: callback(), + verify: callback(), randomBytes: callback(), randomFillSync: callback(), }; diff --git a/tools/native-addon-next-ci.mts b/tools/native-addon-next-ci.mts index afc87e96f..5e8d7ec10 100644 --- a/tools/native-addon-next-ci.mts +++ b/tools/native-addon-next-ci.mts @@ -35,8 +35,14 @@ import { NativeNextProcesses, pause } from './native-addon-next-process.mts'; import { prepareNode26Addon } from './native-addon-node26.mts'; const ROOT = fileURLToPath(new URL('..', import.meta.url)); -const NEXT_VERSION = '16.4.3'; -const NEXT_COMMIT = '29bbeab09981148ade186e8599ecdede15abc281'; +/** The reviewed releases a run may build the official fixtures with, by their commit. */ +const NEXT_RELEASES: Readonly> = { + '16.4.3': '29bbeab09981148ade186e8599ecdede15abc281', + '16.4.4': 'b175f52b05a5f6c6c39ca72565ea178542be323b', +}; +const NEXT_VERSION = process.env['UPWIND_NATIVE_NEXT'] ?? '16.4.3'; +const NEXT_COMMIT = + NEXT_RELEASES[NEXT_VERSION] ?? assert.fail(`Unreviewed Next.js ${NEXT_VERSION}`); const NODE_VERSION = process.versions.node; const PNPM_VERSION = '12.16.1'; const REACT_VERSION = '19.3.0'; diff --git a/tools/native-pages-handler-profile-ci.mts b/tools/native-pages-handler-profile-ci.mts index 37377c162..5cbc4d0c0 100644 --- a/tools/native-pages-handler-profile-ci.mts +++ b/tools/native-pages-handler-profile-ci.mts @@ -6,12 +6,10 @@ import { fileURLToPath } from 'node:url'; import { publicPagesCorpus, sha256 } from './native-pages-handler-profile/format.mts'; import { - FIXTURE_FILES, IMAGE, IMAGE_ID, NEXT_SOURCE, PACKAGE_MANAGER, - SWC_INTEGRITY, } from './native-pages-handler-profile/inputs.mts'; import { pagesHandlerProfileInputs, @@ -87,7 +85,7 @@ async function prepare( typeof packageJson === 'object' && packageJson !== null && 'version' in packageJson && - packageJson.version === '16.4.3', + packageJson.version === profile.next.version, 'Installed public Next version changed', ); const originalHandler = await readFile(path.join(project, NEXT_HANDLER)); @@ -208,10 +206,10 @@ async function produce(temporary: string, profile: PagesHandlerProfileInputs): P inputs: { fixture, image: IMAGE_ID.slice('sha256:'.length), - lock: FIXTURE_FILES['pnpm-lock.yaml'], + lock: profile.files.find((file) => file.target === 'pnpm-lock.yaml')?.sha256, next: NEXT_SOURCE, packageManager: sha256(PACKAGE_MANAGER), - swc: sha256(SWC_INTEGRITY), + swc: sha256(profile.next.swc), }, schema: 1, semanticClosure: 'unmapped:getHandler', diff --git a/tools/native-pages-handler-profile/README.md b/tools/native-pages-handler-profile/README.md index 920778f97..93c7dcf30 100644 --- a/tools/native-pages-handler-profile/README.md +++ b/tools/native-pages-handler-profile/README.md @@ -1,4 +1,4 @@ -This CI producer independently compiles a fixed public dynamic Pages fixture with Next.js 16.4.3 twice. It does not load the deployment SDK or apply adapter transformations. Its official NextAdapter input has only a name and a no-op `onBuildComplete`; `adapterPath` is a fixed fixture path. Next's pinned Pages template imports `getHandler` and exports `handler = getHandler(...)`. This stage collects the emitted code; it does not infer which minified declaration is that handler. +This CI producer independently compiles a fixed public dynamic Pages fixture twice with a reviewed Next.js release: 16.4.3 by default, or 16.4.4 (below). It does not load the deployment SDK or apply adapter transformations. Its official NextAdapter input has only a name and a no-op `onBuildComplete`; `adapterPath` is a fixed fixture path. Next's pinned Pages template imports `getHandler` and exports `handler = getHandler(...)`. This stage collects the emitted code; it does not infer which minified declaration is that handler. The active input is `pages/blog/[slug].js`. Its `getStaticPaths` returns the fixed `first` path with `fallback: true`; `getStaticProps` returns the requested slug, and the page renders that value. This follows the dynamic Pages shape of Next's public [native-addon fixture](https://github.com/vercel/next.js/tree/29bbeab09981148ade186e8599ecdede15abc281/test/e2e/prerender-native-module), without its native dependency. The page imports only React. It does not import a Next internal helper, assign a global selection marker or expose the raw `getHandler` function. @@ -8,7 +8,9 @@ Each compilation starts from a separate project, dependency store and empty home With no arguments, the producer keeps the original six fixture files, pins, ordered `inputs.fixture` hash and default server minification. `--server-minification=off` selects a separately authored and pinned config as the project's `next.config.mjs`; it adds only `experimental.serverMinification: false`. The page, adapter, build ID, package/lock inputs, compiler image, environment and two clean offline compilations remain the same. Unknown or extra arguments are rejected. Neither config is imported by the input-selection controls. -The workflow runs default and off as separate matrix jobs with `fail-fast: false`. Default retains `independent-pages-handler-profile/UNQUALIFIED.json` as `UNQUALIFIED-public-pages-handler`; off retains `independent-pages-handler-profile-server-minification-off/UNQUALIFIED.json` as `UNQUALIFIED-public-pages-handler-server-minification-off`. Schema 1 is unchanged, and the config pin gives off a different `inputs.fixture` hash. The two conditions must be reviewed separately; adding the off producer does not qualify its output, alter the existing catalog or activate a new profile. +`--next=16.4.4`, given first, compiles the same fixture with Next.js 16.4.4. It replaces only `package.json` and `pnpm-lock.yaml` with the pinned copies in `fixture-16.4.4/`, which differ from 16.4.3's in the release and its integrities alone, and records the 16.4.4 `@next/swc-linux-x64-gnu` integrity. Next.js 16.4.4 publishes the same `getHandler` source (`NEXT_SOURCE`). It combines with `--server-minification=off`. + +The workflow runs default and off for each release as separate matrix jobs with `fail-fast: false`; a 16.4.4 job's output and artifact names carry `-next-16.4.4`. Default retains `independent-pages-handler-profile/UNQUALIFIED.json` as `UNQUALIFIED-public-pages-handler`; off retains `independent-pages-handler-profile-server-minification-off/UNQUALIFIED.json` as `UNQUALIFIED-public-pages-handler-server-minification-off`. Schema 1 is unchanged, and the config pin gives off a different `inputs.fixture` hash. The two conditions must be reviewed separately; adding the off producer does not qualify its output, alter the existing catalog or activate a new profile. The only retained output is `UNQUALIFIED.json`: bounded original UTF-8 JavaScript from `.next/server/pages/**` and `.next/server/chunks/**`, excluding manifests, maps, inline source maps, root metadata, links and binary payloads. The collector limits entries, depth, paths, files, each file's bytes and total bytes. Compiler stdout/stderr are bounded and are not retained. Only the controlled public fixture is supported; this is not a general customer-code sanitizer. @@ -24,6 +26,6 @@ The fixed inputs are pinned in `inputs.mts`, including every fixture file, the l - [`next@16.4.3`](https://registry.npmjs.org/next/16.4.3) and [`@next/swc-linux-x64-gnu@16.4.3`](https://registry.npmjs.org/@next%2fswc-linux-x64-gnu/16.4.3), with exact tarball integrity in the standalone lock. React and React DOM are pinned to 19.3.1. The lock includes the complete selected dependency snapshots from the reviewed public workspace lock; no workspace links or SDK patches are included. - Docker's [`official Node24.22.1 bookworm-slim image metadata`](https://github.com/docker-library/repo-info/blob/master/repos/node/remote/24.22.1-bookworm-slim.md). The producer pulls the fixed linux/amd64 manifest digest, checks its image ID and runs that immutable image ID. Neither a mutable tag nor an application-provided image chooses the compiler. -Agents author and review this producer's source and inert controls. They do not read its actual compilation outputs or qualify the resulting corpus. +Agents author and review this producer's source and inert controls. They do not read its actual compilation outputs or qualify the resulting corpus. The one exception is the Next.js 16.4.4 reference below, which an agent wrote from this producer's artifacts at the owner's request. -The adapter has a separate private whole-factory catalog in `node-manifest-handler-catalog.ts`. Its Next.js 16.4.3 reference comes from the marker-free fixture below, compiled independently twice from fixed inputs. It compares all registrations, directives, statements, dependencies, and external source pins; it parameterizes only two exact compiler-emitted source-directory literals against sealed persisted compiler-root metadata. The private consumer owner, complete Pages constructor conjunction, SDK terminal certificates and final closed-graph commit remain mandatory. The extractor above cannot populate this catalog. Native fixture and hosted acceptance are separate CI/deployment checks; reference comparison alone does not claim either has passed. +The adapter has a separate private whole-factory catalog in `node-manifest-handler-catalog.ts`. Its Next.js 16.4.3 reference comes from the marker-free fixture below, compiled independently twice from fixed inputs, and its 16.4.4 reference from the same fixture compiled with `--next=16.4.4`. The steps that wrote the 16.4.4 reference first regenerated the 16.4.3 data, byte for byte, from the same run's 16.4.3 artifacts. The catalog compares all registrations, directives, statements, dependencies, and external source pins; it parameterizes only two exact compiler-emitted source-directory literals against sealed persisted compiler-root metadata. The private consumer owner, complete Pages constructor conjunction, SDK terminal certificates and final closed-graph commit remain mandatory. The extractor above cannot populate this catalog. Native fixture and hosted acceptance are separate CI/deployment checks; reference comparison alone does not claim either has passed. diff --git a/tools/native-pages-handler-profile/fixture-16.4.4/package.json b/tools/native-pages-handler-profile/fixture-16.4.4/package.json new file mode 100644 index 000000000..aa8c09961 --- /dev/null +++ b/tools/native-pages-handler-profile/fixture-16.4.4/package.json @@ -0,0 +1,15 @@ +{ + "name": "independent-public-pages-handler-profile", + "version": "1.0.0", + "private": true, + "dependencies": { + "@next/swc-linux-x64-gnu": "16.4.4", + "next": "16.4.4", + "react": "19.3.1", + "react-dom": "19.3.1" + }, + "devDependencies": { + "@types/node": "24.19.2" + }, + "packageManager": "pnpm@12.15.0+sha512.b3951d6d6ff4a9f13dc7cf56cd28d2b8eb37dbcc9c68e52e1e1191873ea9f82a017287ab50baadc5ccc494f036c67c0e47eb13608c2fa36f47711b61ea128e61" +} diff --git a/tools/native-pages-handler-profile/fixture-16.4.4/pnpm-lock.yaml b/tools/native-pages-handler-profile/fixture-16.4.4/pnpm-lock.yaml new file mode 100644 index 000000000..590c6aff8 --- /dev/null +++ b/tools/native-pages-handler-profile/fixture-16.4.4/pnpm-lock.yaml @@ -0,0 +1,834 @@ +--- +lockfileVersion: "9.0" +importers: + .: + configDependencies: {} + packageManagerDependencies: + pnpm: + specifier: 12.15.0 + version: 12.15.0 +packages: + "@pnpm/exe.android-arm64@12.15.0": + resolution: + integrity: sha512-eVCqmscY70LGvgRzu3mi8IBWBFGix+a6mNZoWmXxESsBdeiKEF9xQSuaeel10yn+zI7TXy2jlI63vFLzETk+aQ== + cpu: + - arm64 + os: + - android + "@pnpm/exe.android-x64@12.15.0": + resolution: + integrity: sha512-Gx/WbkFcdUFUg8g8AShezcS9EiQxfYvdAm2hfKLO8sfJmJvKOCt5fzlgHWDzmGhoclibgW+tUGZ/KF4F4wOTCA== + cpu: + - x64 + os: + - android + "@pnpm/exe.darwin-arm64@12.15.0": + resolution: + integrity: sha512-N0ZykiDWCvqSBJvt71j7cSoBR993FqvYZFtmpCtqvW3pguiI+NAgraLPA5OTwu5mV+/d8hYpVcPktJA3xFDLtA== + cpu: + - arm64 + os: + - darwin + "@pnpm/exe.darwin-x64@12.15.0": + resolution: + integrity: sha512-7Ntr2HlBUxzwWOg7NwDxCbRW/3vXuqKO+hcXm+w3SBi9rrHY53li1wLqWVT7Gjxd+lVEHM1QJZo6WJEGFU8C6A== + cpu: + - x64 + os: + - darwin + "@pnpm/exe.freebsd-x64@12.15.0": + resolution: + integrity: sha512-btOiyL7fL7W4uQGH0aqFai2TNC3IoKcZ7gNHBVbEFZt2dWrnrD0SufL3iGs3fLlmb4OtuU35uf4Zpr9V9Jh9mA== + cpu: + - x64 + os: + - freebsd + "@pnpm/exe.linux-arm64-musl@12.15.0": + resolution: + integrity: sha512-pNC38k+O+90FOjbPNSnw0E7apq1PKTmz3fFe1amn+lys/U5kL++uzir8bRiho8ONcyA3d9EU7wdqcY0DKkj+JA== + cpu: + - arm64 + os: + - linux + libc: + - musl + "@pnpm/exe.linux-arm64@12.15.0": + resolution: + integrity: sha512-qML/8aCXslh8td/w4U+HJNqUcZ2+m6EfTbWScIzZRmyPsRFN+xB4LP1U/hymxc1iSJiKtG56HJx2fvUErWFIGw== + cpu: + - arm64 + os: + - linux + libc: + - glibc + "@pnpm/exe.linux-ppc64@12.15.0": + resolution: + integrity: sha512-tjRDUUl7RCJ6T/6eoNPxOqkwU4Mzi5RtTXmSAxujyz37OTwwsMU7Ps087Zk4gHcUvRSeyF40ZFsew3vtE+nmUA== + cpu: + - ppc64 + os: + - linux + libc: + - glibc + "@pnpm/exe.linux-riscv64@12.15.0": + resolution: + integrity: sha512-bF1uBWEfbwaeryz6lIiaJ8AWD082otZfvzGX5l+6LZRpRwUEpJHUsJrORaYP5/cNMv5ZR2p6TvvcJcQzwtDzmw== + cpu: + - riscv64 + os: + - linux + libc: + - glibc + "@pnpm/exe.linux-s390x@12.15.0": + resolution: + integrity: sha512-zKMhIzsexYMToeLmqV5jPsvV/f9vwsJb2kUH2NFYzalbxImeBrlA4kL7GwF8gD4pagV6jYYcJb8RjbZIdjHptg== + cpu: + - s390x + os: + - linux + libc: + - glibc + "@pnpm/exe.linux-x64-musl@12.15.0": + resolution: + integrity: sha512-A9J0Jg0Rdx+qQU4qXtwyST5tto9IOu1zi4fDk35nbc6DWahggbcKtFvZPKFwL2qgUXGXOX//hXv6z/sp6emO8A== + cpu: + - x64 + os: + - linux + libc: + - musl + "@pnpm/exe.linux-x64@12.15.0": + resolution: + integrity: sha512-yc+QlZR+9+nr29J4fRzhMoapgxTBDndq/+peyoq1XUufnQFnfbXUMFudDIKzHi545linGqmOmXfn0NoufNgcOg== + cpu: + - x64 + os: + - linux + libc: + - glibc + "@pnpm/exe.win32-arm64@12.15.0": + resolution: + integrity: sha512-fPXZxuY1VclkOga9bc53qNCXcegfeoBR8+Ue+AospDwKzuGGSHll0oDxkLfkmpjyRqpe7bH5DyQPYp/xSMaIVA== + cpu: + - arm64 + os: + - win32 + "@pnpm/exe.win32-x64@12.15.0": + resolution: + integrity: sha512-nrd+Tp1We1DGj4DGG5Bea9lZNh9y1PjlLBdEwdInS4D0h0UIDtM7JaGGGDJhNzIwcHASyihWI7HLV+dtG+fhYg== + cpu: + - x64 + os: + - win32 + pnpm@12.15.0: + resolution: + integrity: sha512-s5UdbW/0qfE9x89WzSjSuOs328ycaOUuHhGRhz6p+CoBcoerULqtxczElPA2xnwOR+sTYIwvo29HcRth6hKOYQ== + engines: + node: ">=18" + hasBin: true +snapshots: + "@pnpm/exe.android-arm64@12.15.0": + optional: true + "@pnpm/exe.android-x64@12.15.0": + optional: true + "@pnpm/exe.darwin-arm64@12.15.0": + optional: true + "@pnpm/exe.darwin-x64@12.15.0": + optional: true + "@pnpm/exe.freebsd-x64@12.15.0": + optional: true + "@pnpm/exe.linux-arm64-musl@12.15.0": + optional: true + "@pnpm/exe.linux-arm64@12.15.0": + optional: true + "@pnpm/exe.linux-ppc64@12.15.0": + optional: true + "@pnpm/exe.linux-riscv64@12.15.0": + optional: true + "@pnpm/exe.linux-s390x@12.15.0": + optional: true + "@pnpm/exe.linux-x64-musl@12.15.0": + optional: true + "@pnpm/exe.linux-x64@12.15.0": + optional: true + "@pnpm/exe.win32-arm64@12.15.0": + optional: true + "@pnpm/exe.win32-x64@12.15.0": + optional: true + pnpm@12.15.0: + optionalDependencies: + "@pnpm/exe.android-arm64": 12.15.0 + "@pnpm/exe.android-x64": 12.15.0 + "@pnpm/exe.darwin-arm64": 12.15.0 + "@pnpm/exe.darwin-x64": 12.15.0 + "@pnpm/exe.freebsd-x64": 12.15.0 + "@pnpm/exe.linux-arm64": 12.15.0 + "@pnpm/exe.linux-arm64-musl": 12.15.0 + "@pnpm/exe.linux-ppc64": 12.15.0 + "@pnpm/exe.linux-riscv64": 12.15.0 + "@pnpm/exe.linux-s390x": 12.15.0 + "@pnpm/exe.linux-x64": 12.15.0 + "@pnpm/exe.linux-x64-musl": 12.15.0 + "@pnpm/exe.win32-arm64": 12.15.0 + "@pnpm/exe.win32-x64": 12.15.0 +--- +lockfileVersion: "9.0" +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false +overrides: + "@opentelemetry/api": 1.9.1 + undici@>=7.0.0 <7.30.1: 7.30.1 + sharp@>=0.35.0 <0.35.5: 0.35.5 + source-map-js@>=1.0.0 <1.2.2: 1.2.2 + zod@>=4.0.0 <4.6.6: 4.6.7 +importers: + .: + dependencies: + "@next/swc-linux-x64-gnu": + specifier: 16.4.4 + version: 16.4.4 + next: + specifier: 16.4.4 + version: 16.4.4(@types/node@24.19.2)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + react: + specifier: 19.3.1 + version: 19.3.1 + react-dom: + specifier: 19.3.1 + version: 19.3.1(react@19.3.1) + devDependencies: + "@types/node": + specifier: 24.19.2 + version: 24.19.2 +packages: + "@next/swc-linux-x64-gnu@16.4.4": + resolution: + integrity: sha512-fg3Wr030Y3BDkCXi6HGsPz0BUPTGZJS8TZSrCO0tS3O/KBsCyx4eP6aeaizCpGmcUKhtb4l7JoMc88QO2qBl2g== + engines: + node: ">= 10" + cpu: + - x64 + os: + - linux + libc: + - glibc + next@16.4.4: + resolution: + integrity: sha512-Hu4pOw2x0RQ0TQabNWbpsNg/3+SZf+rLQwvfhBLnptv98GS4dcIVMlfEyn9jb2/ZJDSahhnUKEs0YrfarjGX3A== + engines: + node: ">=20.9.0" + hasBin: true + peerDependencies: + "@opentelemetry/api": ^1.1.0 + "@playwright/test": ^1.51.1 + babel-plugin-react-compiler: "*" + react: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 + react-dom: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 + sass: ^1.3.0 + peerDependenciesMeta: + "@opentelemetry/api": + optional: true + "@playwright/test": + optional: true + babel-plugin-react-compiler: + optional: true + sass: + optional: true + react@19.3.1: + resolution: + integrity: sha512-QNhWdt7nrgVWWQG58QuGsAR3jv3Hbe4mb4vz7w2csnHavciZJgda1CPNaldF6eVxv90ud3jUXpKlHL69gO38yw== + engines: + node: ">=0.10.0" + react-dom@19.3.1: + resolution: + integrity: sha512-Y0bARz53lhEC1NMCASs0/j+faB/zqt3R8hpvg1e8+61ZVJ4UL0UdC4cBbXfZMcIuhrfgeILTLe+ZeBCavQPtKQ== + peerDependencies: + react: ^19.3.1 + "@types/node@24.19.2": + resolution: + integrity: sha512-93d49wCWJkCyN87MxLpNN6KXPCwFw7bVUg79mGUV1OS40JGWwVCr8n61DXCSqz7CV6/VfPkb7xec7uP2M+QHWQ== + "@next/env@16.4.4": + resolution: + integrity: sha512-6/3ZuELR0tPLgNigvv8d/LAmfLIo1IpNfvrI/AHCAl9w9D0DNVGQGvo+OLPzAWR+WMW3vVyO7HaW0kfL97huBg== + "@swc/helpers@0.5.23": + resolution: + integrity: sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw== + baseline-browser-mapping@2.11.32: + resolution: + integrity: sha512-UvuxFU9P3hM29LrQdwu0cD6w6/+SFr+UlB9s/knI7+O2rK9xFxKUwSDWxGdbbqQ9/V7BJ5s0NI5nQAE4uJp3gQ== + engines: + node: ">=6.0.0" + hasBin: true + caniuse-lite@1.0.30001819: + resolution: + integrity: sha512-LFUqIIAwpzsIuevnKnsR9qnStBcgzPShTX61lBwN9uFkkf8o8zLiebEjbW8AYtZiF+1YKDWfdt/pWDtMuG8Xwg== + postcss@8.5.23: + resolution: + integrity: sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg== + engines: + node: ^10 || ^12 || >=14 + styled-jsx@5.1.6: + resolution: + integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA== + engines: + node: ">= 12.0.0" + peerDependencies: + "@babel/core": "*" + babel-plugin-macros: "*" + react: ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" + peerDependenciesMeta: + "@babel/core": + optional: true + babel-plugin-macros: + optional: true + "@next/swc-darwin-arm64@16.4.4": + resolution: + integrity: sha512-1KlZ5IJ7zcLBZvqG1q7nuRWm2LAAN4C3iBqqoBCRRl1UEv0A4k6Rek+x3j23UiWf8Gz+VsN+M/LylAWkxpm2mg== + engines: + node: ">= 10" + cpu: + - arm64 + os: + - darwin + "@next/swc-darwin-x64@16.4.4": + resolution: + integrity: sha512-Tb7efFdhbS/ph74RS7wob84gXGM58qxoO/WrkiMsTmy/JZktKOKBeiM45W0E/b8JfBzZJdexQqSmvedBwDK0PA== + engines: + node: ">= 10" + cpu: + - x64 + os: + - darwin + "@next/swc-linux-arm64-gnu@16.4.4": + resolution: + integrity: sha512-wojSfLnH8RRImhtbcn3Dlx9JICh6aQHgDA33XDr+EPua2tt7Q/yjDn0FTAbqfNlLrBmOASAGhLh6NvBk9L3gzw== + engines: + node: ">= 10" + cpu: + - arm64 + os: + - linux + libc: + - glibc + "@next/swc-linux-arm64-musl@16.4.4": + resolution: + integrity: sha512-zgxpGOZaR+2O6dOk5ZAUkKgH92ZtqNg3ohzyg2HmWc2N+AZH4dEpTwdpB0e5MR9YRVdVDUkEM5g6c3TWWn8g1g== + engines: + node: ">= 10" + cpu: + - arm64 + os: + - linux + libc: + - musl + "@next/swc-linux-x64-musl@16.4.4": + resolution: + integrity: sha512-40cr71PSdCV9zU2g0B1XG27af10tYU+9g8F+FtF3qSguQg0jSZDoxn0NrAvgFQ0cy9s0ZBA/00P9g/XL1O43xg== + engines: + node: ">= 10" + cpu: + - x64 + os: + - linux + libc: + - musl + "@next/swc-win32-arm64-msvc@16.4.4": + resolution: + integrity: sha512-vFMiEbRZ+LYNeswslf4LuGuoPHE8JbtqV0GnmzbpnGNMs0uWaR2q98hu8M/UWGycNJERBXIUss+BGd3PIJ8VZg== + engines: + node: ">= 10" + cpu: + - arm64 + os: + - win32 + "@next/swc-win32-x64-msvc@16.4.4": + resolution: + integrity: sha512-jy2P4V8BDZ+CFCzYABHYRZ1CmyFhfmYe22i8KA4uEafdYPrUjJ3Ot+lx265mLWLHiZRIfBzCn3V1F17m0vzl9Q== + engines: + node: ">= 10" + cpu: + - x64 + os: + - win32 + sharp@0.35.5: + resolution: + integrity: sha512-Ywn4OnzGukp7CDMrp08RQ50YKmuwG47brZgIVPTvBaaAfQlRlygrRqSrxdCiL9M+LlzLBiJ68IR1QqvzHyjC7g== + engines: + node: ">=20.9.0" + peerDependencies: + "@types/node": "*" + peerDependenciesMeta: + "@types/node": + optional: true + scheduler@0.28.0: + resolution: + integrity: sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw== + undici-types@7.24.6: + resolution: + integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg== + tslib@2.8.1: + resolution: + integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== + nanoid@3.3.20: + resolution: + integrity: sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ== + engines: + node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1 + hasBin: true + picocolors@1.1.1: + resolution: + integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== + source-map-js@1.2.2: + resolution: + integrity: sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw== + engines: + node: ">=0.10.0" + client-only@0.0.1: + resolution: + integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA== + "@img/colour@1.1.0": + resolution: + integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ== + engines: + node: ">=18" + detect-libc@2.1.2: + resolution: + integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ== + engines: + node: ">=8" + semver@7.8.5: + resolution: + integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA== + engines: + node: ">=10" + hasBin: true + "@img/sharp-darwin-arm64@0.35.5": + resolution: + integrity: sha512-QRUlFQ0WxvdWyqqG/WtI3iupfD5rBzmCHXSdPsY91sAtVtTo7Q4cb6zOccZ3gqEqkr0f1As1ehLqmEpDsRf+lg== + engines: + node: ">=20.9.0" + cpu: + - arm64 + os: + - darwin + "@img/sharp-darwin-x64@0.35.5": + resolution: + integrity: sha512-+BR255RhDlpygUpOc/Jdt1nT6DQ3XG/ERo5wbcdOf5Q320dKtPCKPLR1LJs9VGXRaMa8l1uUa0tkCNOXiAxZUw== + engines: + node: ">=20.9.0" + cpu: + - x64 + os: + - darwin + "@img/sharp-freebsd-wasm32@0.35.5": + resolution: + integrity: sha512-Y/z91nEZ4uIBX5X3nfTovjU9lHNKFYbL2lpHCLVNmXQK03VIZvXBBt0KxbPGp2SdGSF+2mQU4e+hQaWOt86iAw== + engines: + node: ">=20.9.0" + os: + - freebsd + "@img/sharp-libvips-darwin-arm64@1.3.4": + resolution: + integrity: sha512-5R89nBYiRdUlSWJxPhO+GVtaXzXSxKnRu/xqMn3KTA3L9EB9Oy/P+Nn2f2vlhPuUdy/Zusb2DarbyTpGCfEDuw== + cpu: + - arm64 + os: + - darwin + "@img/sharp-libvips-darwin-x64@1.3.4": + resolution: + integrity: sha512-iR2OKH80yi0U+dUplyh3/xdpFvps6YkCwsXenIJxqxR1v9o+xtKTGbS9H7cps+2Vxjc8B1j96p75NmTGjIhtpQ== + cpu: + - x64 + os: + - darwin + "@img/sharp-libvips-linux-arm@1.3.4": + resolution: + integrity: sha512-LmRtTsOHuvM2+wlO2Db37dx5MiZhB0FvSunciw48YjdOkZz9KAiRbm8ujeMOA1INqmei5NapFxYEK1D1ZSidmw== + cpu: + - arm + os: + - linux + libc: + - glibc + "@img/sharp-libvips-linux-arm64@1.3.4": + resolution: + integrity: sha512-Y3dgX/6lE2QhQb+Gxy0WZxfg9MEm/JBjamZpS2IklP7xIQoKN4hzAm7KcMVGtaVDt3neE9OKBC7vAfonA/Lr1A== + cpu: + - arm64 + os: + - linux + libc: + - glibc + "@img/sharp-libvips-linux-ppc64@1.3.4": + resolution: + integrity: sha512-Le6boB8Tai0Nis+gIxIpKx68UDVVIqdR8Tin5Yf1z2LJJQLDJvCDRqRu+jC2qCoD+eIomonmOwB4smBRxfVpYQ== + cpu: + - ppc64 + os: + - linux + libc: + - glibc + "@img/sharp-libvips-linux-riscv64@1.3.4": + resolution: + integrity: sha512-aHkkIEHPRdQEegJN20MLmGtxYD9R2wQr3Cwpddnu5+YKMt6Uzax7S9h5gpZTo8wyrGuZSlfQ63OevL5mTyOC7Q== + cpu: + - riscv64 + os: + - linux + libc: + - glibc + "@img/sharp-libvips-linux-s390x@1.3.4": + resolution: + integrity: sha512-ra/mB6MikESDUO7Yg+Mi95bFBb9GsObURuhnOv3OqknjGe9sZrG8tCe9q0xSIGrtLgvgw0gKnFWcK4blSgQOuQ== + cpu: + - s390x + os: + - linux + libc: + - glibc + "@img/sharp-libvips-linux-x64@1.3.4": + resolution: + integrity: sha512-GJ//SSXbnwSDes02umB3nDJLFcQzw8a18V8fyhqr6tV515tOEMdImjjxj1AoafMRz56F3PHgftnj1QEKSU1zkw== + cpu: + - x64 + os: + - linux + libc: + - glibc + "@img/sharp-libvips-linuxmusl-arm64@1.3.4": + resolution: + integrity: sha512-hvulFwtjUcagsis6BBxHwGFwWoNZjgYmULGVrZcyfNbjA8hKILbRxGg15/7w5HDyXHXUos/j6baAWqnCyQ2DWA== + cpu: + - arm64 + os: + - linux + libc: + - musl + "@img/sharp-libvips-linuxmusl-x64@1.3.4": + resolution: + integrity: sha512-6zXKeE/p39I1AmA3cJG35eyBGNqNddLnUXjhwBnsGjFPWqf5VKkDBEqaEkPDoTEtkxwi2vv8Tcr2mDyP4So7Fg== + cpu: + - x64 + os: + - linux + libc: + - musl + "@img/sharp-linux-arm@0.35.5": + resolution: + integrity: sha512-LEaXK2WdXVK5ykcw0buWyPMsmLLL2vpHLD6yrNSW+JGEL3BZPA4tpKN6iaMc4AxTTAoaX/sU1rOL51lcIz48ZQ== + engines: + node: ">=20.9.0" + cpu: + - arm + os: + - linux + libc: + - glibc + "@img/sharp-linux-arm64@0.35.5": + resolution: + integrity: sha512-LYVx5JTsOM2CBzmxreh+nl64/3H6Xb09iSLknqH47z2T2DFFxDeFLP5y4dJwe6H7uGQlHPyEEtIqyo3DYsRwdQ== + engines: + node: ">=20.9.0" + cpu: + - arm64 + os: + - linux + libc: + - glibc + "@img/sharp-linux-ppc64@0.35.5": + resolution: + integrity: sha512-QVxAAq8evVRI9ia2vqgwrmWucn5Dfv+JdWzj75pD8omHLPSP7f8p20O8jxzjCcuCEQEOtYOZUmX1hkiZ0kdevA== + engines: + node: ">=20.9.0" + cpu: + - ppc64 + os: + - linux + libc: + - glibc + "@img/sharp-linux-riscv64@0.35.5": + resolution: + integrity: sha512-LtdreXguaavKODPIfzJ4kffx7UNt1omwtK0rch4EBbbSTXPnxWmYSayXdLJw0fJzQ97kHt1gL/yh4tvU+nCyRQ== + engines: + node: ">=20.9.0" + cpu: + - riscv64 + os: + - linux + libc: + - glibc + "@img/sharp-linux-s390x@0.35.5": + resolution: + integrity: sha512-UZasTOFiYzotTsGOCu42BfUzP6Tu6Do/947iRm1RsLKvlllxwGcn4RN27LibGWceix4Y+Pmw3jsnTcCQIgWjqA== + engines: + node: ">=20.9.0" + cpu: + - s390x + os: + - linux + libc: + - glibc + "@img/sharp-linux-x64@0.35.5": + resolution: + integrity: sha512-SxFtLTeJInhAA9Q836kux2vZNeOBQEx658qvbboZScr0wIARym3IcGmW7KpVD5sbVg0Ojy+udFQdayYIZyoNog== + engines: + node: ">=20.9.0" + cpu: + - x64 + os: + - linux + libc: + - glibc + "@img/sharp-linuxmusl-arm64@0.35.5": + resolution: + integrity: sha512-9HbMclmI1zlNkFRs3z9/eBtDjfD0sGlrX1z6b1qwmiFY5ElDLh4BC0LPBdVp7z1DXFiKlIcznf+ZlsuZzLxQqg== + engines: + node: ">=20.9.0" + cpu: + - arm64 + os: + - linux + libc: + - musl + "@img/sharp-linuxmusl-x64@0.35.5": + resolution: + integrity: sha512-4KOphqB035HrVdqLZfCgMzzERrQkkzOwRhl4OAkRO1YCldbaFjySXMaK534Mo0V+LndnlJk+sbUyLeU0ULyD1A== + engines: + node: ">=20.9.0" + cpu: + - x64 + os: + - linux + libc: + - musl + "@img/sharp-webcontainers-wasm32@0.35.5": + resolution: + integrity: sha512-hfhF/FmoQyTUkA0bIKFOtw536BQSeBMe6BF6QyWlrPxT754+TFLaZ7sKKTfvvM0yJgKgaYTwnFCIZ/GuDw5SUA== + engines: + node: ">=20.9.0" + cpu: + - wasm32 + "@img/sharp-win32-arm64@0.35.5": + resolution: + integrity: sha512-X4t7g+7ZA5DKblCBEXGjUqqemj4vczING/5viFwAL8h4N3qYeyjwdCvRLHi4EdOUI+2Z7UFlp1VM+p/AuEtm6Q== + engines: + node: ">=20.9.0" + cpu: + - arm64 + os: + - win32 + "@img/sharp-win32-ia32@0.35.5": + resolution: + integrity: sha512-5Zm82LoBc43nhwNybZlG7Y1KO//Zhsn306fQl29ZOuStHLGTo3BWL83q3cznX0poxSAMuYL1On/BHBxkBeKr6A== + engines: + node: ^20.9.0 + cpu: + - ia32 + os: + - win32 + "@img/sharp-win32-x64@0.35.5": + resolution: + integrity: sha512-x76eH0vEiHlcMQu8Y8IenntaACtddpT6W0wmXtWrnKcnKI7ME5DdgqhAD6SEWOEl1v2zDvkZDhFA9KnURwpfqg== + engines: + node: ">=20.9.0" + cpu: + - x64 + os: + - win32 + "@img/sharp-wasm32@0.35.5": + resolution: + integrity: sha512-Ptsga1su4tQx+LLF1ECS9U6nz5kmrXKo6XVbtR48Ke3ZRxxgaWBu7IDtEe1quo8hiupwm6WFqxVlXaSf7IINGQ== + engines: + node: ">=20.9.0" + "@emnapi/runtime@1.11.3": + resolution: + integrity: sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA== +snapshots: + "@next/swc-linux-x64-gnu@16.4.4": + optional: true + next@16.4.4(@types/node@24.19.2)(react-dom@19.3.1(react@19.3.1))(react@19.3.1): + dependencies: + "@next/env": 16.4.4 + "@swc/helpers": 0.5.23 + baseline-browser-mapping: 2.11.32 + caniuse-lite: 1.0.30001819 + postcss: 8.5.23 + react: 19.3.1 + react-dom: 19.3.1(react@19.3.1) + styled-jsx: 5.1.6(react@19.3.1) + optionalDependencies: + "@next/swc-darwin-arm64": 16.4.4 + "@next/swc-darwin-x64": 16.4.4 + "@next/swc-linux-arm64-gnu": 16.4.4 + "@next/swc-linux-arm64-musl": 16.4.4 + "@next/swc-linux-x64-gnu": 16.4.4 + "@next/swc-linux-x64-musl": 16.4.4 + "@next/swc-win32-arm64-msvc": 16.4.4 + "@next/swc-win32-x64-msvc": 16.4.4 + sharp: 0.35.5(@types/node@24.19.2) + transitivePeerDependencies: + - "@babel/core" + - "@types/node" + - babel-plugin-macros + react@19.3.1: {} + react-dom@19.3.1(react@19.3.1): + dependencies: + react: 19.3.1 + scheduler: 0.28.0 + "@types/node@24.19.2": + dependencies: + undici-types: 7.24.6 + "@next/env@16.4.4": {} + "@swc/helpers@0.5.23": + dependencies: + tslib: 2.8.1 + baseline-browser-mapping@2.11.32: {} + caniuse-lite@1.0.30001819: {} + postcss@8.5.23: + dependencies: + nanoid: 3.3.20 + picocolors: 1.1.1 + source-map-js: 1.2.2 + styled-jsx@5.1.6(react@19.3.1): + dependencies: + client-only: 0.0.1 + react: 19.3.1 + "@next/swc-darwin-arm64@16.4.4": + optional: true + "@next/swc-darwin-x64@16.4.4": + optional: true + "@next/swc-linux-arm64-gnu@16.4.4": + optional: true + "@next/swc-linux-arm64-musl@16.4.4": + optional: true + "@next/swc-linux-x64-musl@16.4.4": + optional: true + "@next/swc-win32-arm64-msvc@16.4.4": + optional: true + "@next/swc-win32-x64-msvc@16.4.4": + optional: true + sharp@0.35.5(@types/node@24.19.2): + dependencies: + "@img/colour": 1.1.0 + detect-libc: 2.1.2 + semver: 7.8.5 + optionalDependencies: + "@img/sharp-darwin-arm64": 0.35.5 + "@img/sharp-darwin-x64": 0.35.5 + "@img/sharp-freebsd-wasm32": 0.35.5 + "@img/sharp-libvips-darwin-arm64": 1.3.4 + "@img/sharp-libvips-darwin-x64": 1.3.4 + "@img/sharp-libvips-linux-arm": 1.3.4 + "@img/sharp-libvips-linux-arm64": 1.3.4 + "@img/sharp-libvips-linux-ppc64": 1.3.4 + "@img/sharp-libvips-linux-riscv64": 1.3.4 + "@img/sharp-libvips-linux-s390x": 1.3.4 + "@img/sharp-libvips-linux-x64": 1.3.4 + "@img/sharp-libvips-linuxmusl-arm64": 1.3.4 + "@img/sharp-libvips-linuxmusl-x64": 1.3.4 + "@img/sharp-linux-arm": 0.35.5 + "@img/sharp-linux-arm64": 0.35.5 + "@img/sharp-linux-ppc64": 0.35.5 + "@img/sharp-linux-riscv64": 0.35.5 + "@img/sharp-linux-s390x": 0.35.5 + "@img/sharp-linux-x64": 0.35.5 + "@img/sharp-linuxmusl-arm64": 0.35.5 + "@img/sharp-linuxmusl-x64": 0.35.5 + "@img/sharp-webcontainers-wasm32": 0.35.5 + "@img/sharp-win32-arm64": 0.35.5 + "@img/sharp-win32-ia32": 0.35.5 + "@img/sharp-win32-x64": 0.35.5 + "@types/node": 24.19.2 + scheduler@0.28.0: {} + undici-types@7.24.6: {} + tslib@2.8.1: {} + nanoid@3.3.20: {} + picocolors@1.1.1: {} + source-map-js@1.2.2: {} + client-only@0.0.1: {} + "@img/colour@1.1.0": {} + detect-libc@2.1.2: {} + semver@7.8.5: {} + "@img/sharp-darwin-arm64@0.35.5": + optionalDependencies: + "@img/sharp-libvips-darwin-arm64": 1.3.4 + optional: true + "@img/sharp-darwin-x64@0.35.5": + optionalDependencies: + "@img/sharp-libvips-darwin-x64": 1.3.4 + optional: true + "@img/sharp-freebsd-wasm32@0.35.5": + dependencies: + "@img/sharp-wasm32": 0.35.5 + optional: true + "@img/sharp-libvips-darwin-arm64@1.3.4": + optional: true + "@img/sharp-libvips-darwin-x64@1.3.4": + optional: true + "@img/sharp-libvips-linux-arm@1.3.4": + optional: true + "@img/sharp-libvips-linux-arm64@1.3.4": + optional: true + "@img/sharp-libvips-linux-ppc64@1.3.4": + optional: true + "@img/sharp-libvips-linux-riscv64@1.3.4": + optional: true + "@img/sharp-libvips-linux-s390x@1.3.4": + optional: true + "@img/sharp-libvips-linux-x64@1.3.4": + optional: true + "@img/sharp-libvips-linuxmusl-arm64@1.3.4": + optional: true + "@img/sharp-libvips-linuxmusl-x64@1.3.4": + optional: true + "@img/sharp-linux-arm@0.35.5": + optionalDependencies: + "@img/sharp-libvips-linux-arm": 1.3.4 + optional: true + "@img/sharp-linux-arm64@0.35.5": + optionalDependencies: + "@img/sharp-libvips-linux-arm64": 1.3.4 + optional: true + "@img/sharp-linux-ppc64@0.35.5": + optionalDependencies: + "@img/sharp-libvips-linux-ppc64": 1.3.4 + optional: true + "@img/sharp-linux-riscv64@0.35.5": + optionalDependencies: + "@img/sharp-libvips-linux-riscv64": 1.3.4 + optional: true + "@img/sharp-linux-s390x@0.35.5": + optionalDependencies: + "@img/sharp-libvips-linux-s390x": 1.3.4 + optional: true + "@img/sharp-linux-x64@0.35.5": + optionalDependencies: + "@img/sharp-libvips-linux-x64": 1.3.4 + optional: true + "@img/sharp-linuxmusl-arm64@0.35.5": + optionalDependencies: + "@img/sharp-libvips-linuxmusl-arm64": 1.3.4 + optional: true + "@img/sharp-linuxmusl-x64@0.35.5": + optionalDependencies: + "@img/sharp-libvips-linuxmusl-x64": 1.3.4 + optional: true + "@img/sharp-webcontainers-wasm32@0.35.5": + dependencies: + "@img/sharp-wasm32": 0.35.5 + optional: true + "@img/sharp-win32-arm64@0.35.5": + optional: true + "@img/sharp-win32-ia32@0.35.5": + optional: true + "@img/sharp-win32-x64@0.35.5": + optional: true + "@img/sharp-wasm32@0.35.5": + dependencies: + "@emnapi/runtime": 1.11.3 + optional: true + "@emnapi/runtime@1.11.3": + dependencies: + tslib: 2.8.1 + optional: true diff --git a/tools/native-pages-handler-profile/inputs.mts b/tools/native-pages-handler-profile/inputs.mts index 55a6dacc4..8648e717a 100644 --- a/tools/native-pages-handler-profile/inputs.mts +++ b/tools/native-pages-handler-profile/inputs.mts @@ -16,3 +16,17 @@ export const FIXTURE_FILES = { } as const; export const SERVER_MINIFICATION_OFF_CONFIG = 'd3c5b5fab607b9300eb21e847197612fd64b5137490e328f26c33ea44267fcb0'; +/** + * The releases a run may compile with. 16.4.3 is the original input; a later one replaces only the + * manifest and lock (`fixture-/`), and publishes the same `NEXT_SOURCE`. + */ +export const NEXT_RELEASES = { + '16.4.3': { swc: SWC_INTEGRITY, files: {} }, + '16.4.4': { + swc: 'sha512-fg3Wr030Y3BDkCXi6HGsPz0BUPTGZJS8TZSrCO0tS3O/KBsCyx4eP6aeaizCpGmcUKhtb4l7JoMc88QO2qBl2g==', + files: { + 'package.json': '3d78dd3baf4b7bf415268c2a2176c562121c5661083a9e0e6bcec23b9f5f9730', + 'pnpm-lock.yaml': '7691c72d7d1718f8b657ef32d2cdea20f870ef69e80477df2ad9164ef558098e', + }, + }, +} as const; diff --git a/tools/native-pages-handler-profile/options.mts b/tools/native-pages-handler-profile/options.mts index fb6d03818..0f205cad6 100644 --- a/tools/native-pages-handler-profile/options.mts +++ b/tools/native-pages-handler-profile/options.mts @@ -1,8 +1,9 @@ import assert from 'node:assert/strict'; -import { FIXTURE_FILES, SERVER_MINIFICATION_OFF_CONFIG } from './inputs.mts'; +import { FIXTURE_FILES, NEXT_RELEASES, SERVER_MINIFICATION_OFF_CONFIG } from './inputs.mts'; export interface PagesHandlerProfileInputs { + readonly next: { readonly version: keyof typeof NEXT_RELEASES; readonly swc: string }; readonly serverMinification: 'default' | 'off'; readonly outputName: string; readonly artifactName: string; @@ -13,29 +14,39 @@ export interface PagesHandlerProfileInputs { }[]; } +const OFF = '--server-minification=off'; +const LATER_RELEASE = '--next=16.4.4'; + /** Fixed public source selection only; this does not compile, collect or qualify any output. */ export function pagesHandlerProfileInputs(args: readonly string[]): PagesHandlerProfileInputs { + const later = args[0] === LATER_RELEASE; + const rest = later ? args.slice(1) : args; assert.ok( - args.length === 0 || (args.length === 1 && args[0] === '--server-minification=off'), - 'Pass no arguments or exactly --server-minification=off', + rest.length === 0 || (rest.length === 1 && rest[0] === OFF), + `Pass no arguments, or ${LATER_RELEASE} and/or ${OFF} in that order`, ); - const off = args.length === 1; + const off = rest.length === 1; + const version = later ? '16.4.4' : '16.4.3'; + const release: { readonly files: Readonly> } = NEXT_RELEASES[version]; const files = Object.entries(FIXTURE_FILES).map(([target, digest]) => { const replacement = off && target === 'next.config.mjs'; + const released = release.files[target]; + if (released !== undefined) { + return Object.freeze({ source: `fixture-${version}/${target}`, target, sha256: released }); + } return Object.freeze({ source: replacement ? 'fixture/server-minification-off.next.config.mjs' : `fixture/${target}`, target, sha256: replacement ? SERVER_MINIFICATION_OFF_CONFIG : digest, }); }); + const named = later ? `-next-${version}` : ''; + const suffix = `${named}${off ? '-server-minification-off' : ''}`; return Object.freeze({ + next: Object.freeze({ version, swc: NEXT_RELEASES[version].swc }), serverMinification: off ? 'off' : 'default', - outputName: off - ? 'independent-pages-handler-profile-server-minification-off' - : 'independent-pages-handler-profile', - artifactName: off - ? 'UNQUALIFIED-public-pages-handler-server-minification-off' - : 'UNQUALIFIED-public-pages-handler', + outputName: `independent-pages-handler-profile${suffix}`, + artifactName: `UNQUALIFIED-public-pages-handler${suffix}`, files: Object.freeze(files), }); }