| | |
| | import 'server-only' |
| |
|
| | |
| | import { renderToReadableStream } from 'react-server-dom-webpack/server' |
| | |
| | import { createFromReadableStream } from 'react-server-dom-webpack/client' |
| |
|
| | import { streamToString } from '../stream-utils/node-web-streams-helper' |
| | import { |
| | arrayBufferToString, |
| | decrypt, |
| | encrypt, |
| | getActionEncryptionKey, |
| | stringToUint8Array, |
| | } from './encryption-utils' |
| | import { |
| | getClientReferenceManifest, |
| | getServerModuleMap, |
| | } from './manifests-singleton' |
| | import { |
| | getCacheSignal, |
| | getPrerenderResumeDataCache, |
| | getRenderResumeDataCache, |
| | workUnitAsyncStorage, |
| | } from './work-unit-async-storage.external' |
| | import { createHangingInputAbortSignal } from './dynamic-rendering' |
| | import React from 'react' |
| |
|
| | const isEdgeRuntime = process.env.NEXT_RUNTIME === 'edge' |
| |
|
| | const textEncoder = new TextEncoder() |
| | const textDecoder = new TextDecoder() |
| |
|
| | const filterStackFrame = |
| | process.env.NODE_ENV !== 'production' |
| | ? (require('../lib/source-maps') as typeof import('../lib/source-maps')) |
| | .filterStackFrameDEV |
| | : undefined |
| | const findSourceMapURL = |
| | process.env.NODE_ENV !== 'production' |
| | ? (require('../lib/source-maps') as typeof import('../lib/source-maps')) |
| | .findSourceMapURLDEV |
| | : undefined |
| |
|
| | |
| | |
| | |
| | async function decodeActionBoundArg(actionId: string, arg: string) { |
| | const key = await getActionEncryptionKey() |
| | if (typeof key === 'undefined') { |
| | throw new Error( |
| | `Missing encryption key for Server Action. This is a bug in Next.js` |
| | ) |
| | } |
| |
|
| | |
| | const originalPayload = atob(arg) |
| | const ivValue = originalPayload.slice(0, 16) |
| | const payload = originalPayload.slice(16) |
| |
|
| | const decrypted = textDecoder.decode( |
| | await decrypt(key, stringToUint8Array(ivValue), stringToUint8Array(payload)) |
| | ) |
| |
|
| | if (!decrypted.startsWith(actionId)) { |
| | throw new Error('Invalid Server Action payload: failed to decrypt.') |
| | } |
| |
|
| | return decrypted.slice(actionId.length) |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | async function encodeActionBoundArg(actionId: string, arg: string) { |
| | const key = await getActionEncryptionKey() |
| | if (key === undefined) { |
| | throw new Error( |
| | `Missing encryption key for Server Action. This is a bug in Next.js` |
| | ) |
| | } |
| |
|
| | |
| | const randomBytes = new Uint8Array(16) |
| | workUnitAsyncStorage.exit(() => crypto.getRandomValues(randomBytes)) |
| | const ivValue = arrayBufferToString(randomBytes.buffer) |
| |
|
| | const encrypted = await encrypt( |
| | key, |
| | randomBytes, |
| | textEncoder.encode(actionId + arg) |
| | ) |
| |
|
| | return btoa(ivValue + arrayBufferToString(encrypted)) |
| | } |
| |
|
| | enum ReadStatus { |
| | Ready, |
| | Pending, |
| | Complete, |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | export const encryptActionBoundArgs = React.cache( |
| | async function encryptActionBoundArgs(actionId: string, ...args: any[]) { |
| | const workUnitStore = workUnitAsyncStorage.getStore() |
| | const cacheSignal = workUnitStore |
| | ? getCacheSignal(workUnitStore) |
| | : undefined |
| |
|
| | const { clientModules } = getClientReferenceManifest() |
| |
|
| | |
| | |
| | const error = new Error() |
| | Error.captureStackTrace(error, encryptActionBoundArgs) |
| |
|
| | let didCatchError = false |
| |
|
| | const hangingInputAbortSignal = workUnitStore |
| | ? createHangingInputAbortSignal(workUnitStore) |
| | : undefined |
| |
|
| | let readStatus = ReadStatus.Ready |
| | function startReadOnce() { |
| | if (readStatus === ReadStatus.Ready) { |
| | readStatus = ReadStatus.Pending |
| | cacheSignal?.beginRead() |
| | } |
| | } |
| |
|
| | function endReadIfStarted() { |
| | if (readStatus === ReadStatus.Pending) { |
| | cacheSignal?.endRead() |
| | } |
| | readStatus = ReadStatus.Complete |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | if (hangingInputAbortSignal && cacheSignal) { |
| | hangingInputAbortSignal.addEventListener('abort', startReadOnce, { |
| | once: true, |
| | }) |
| | } |
| |
|
| | |
| | const serialized = await streamToString( |
| | renderToReadableStream(args, clientModules, { |
| | filterStackFrame, |
| | signal: hangingInputAbortSignal, |
| | onError(err) { |
| | if (hangingInputAbortSignal?.aborted) { |
| | return |
| | } |
| |
|
| | |
| | if (didCatchError) { |
| | return |
| | } |
| |
|
| | didCatchError = true |
| |
|
| | |
| | |
| | error.message = err instanceof Error ? err.message : String(err) |
| | }, |
| | }), |
| | |
| | |
| | |
| | hangingInputAbortSignal |
| | ) |
| |
|
| | if (didCatchError) { |
| | if (process.env.NODE_ENV === 'development') { |
| | |
| | |
| | |
| | console.error(error) |
| | } |
| |
|
| | endReadIfStarted() |
| | throw error |
| | } |
| |
|
| | if (!workUnitStore) { |
| | |
| | |
| | return encodeActionBoundArg(actionId, serialized) |
| | } |
| |
|
| | startReadOnce() |
| |
|
| | const prerenderResumeDataCache = getPrerenderResumeDataCache(workUnitStore) |
| | const renderResumeDataCache = getRenderResumeDataCache(workUnitStore) |
| | const cacheKey = actionId + serialized |
| |
|
| | const cachedEncrypted = |
| | prerenderResumeDataCache?.encryptedBoundArgs.get(cacheKey) ?? |
| | renderResumeDataCache?.encryptedBoundArgs.get(cacheKey) |
| |
|
| | if (cachedEncrypted) { |
| | return cachedEncrypted |
| | } |
| |
|
| | const encrypted = await encodeActionBoundArg(actionId, serialized) |
| |
|
| | endReadIfStarted() |
| | prerenderResumeDataCache?.encryptedBoundArgs.set(cacheKey, encrypted) |
| |
|
| | return encrypted |
| | } |
| | ) |
| |
|
| | |
| | export async function decryptActionBoundArgs( |
| | actionId: string, |
| | encryptedPromise: Promise<string> |
| | ) { |
| | const encrypted = await encryptedPromise |
| | const workUnitStore = workUnitAsyncStorage.getStore() |
| |
|
| | let decrypted: string | undefined |
| |
|
| | if (workUnitStore) { |
| | const cacheSignal = getCacheSignal(workUnitStore) |
| | const prerenderResumeDataCache = getPrerenderResumeDataCache(workUnitStore) |
| | const renderResumeDataCache = getRenderResumeDataCache(workUnitStore) |
| |
|
| | decrypted = |
| | prerenderResumeDataCache?.decryptedBoundArgs.get(encrypted) ?? |
| | renderResumeDataCache?.decryptedBoundArgs.get(encrypted) |
| |
|
| | if (!decrypted) { |
| | cacheSignal?.beginRead() |
| | decrypted = await decodeActionBoundArg(actionId, encrypted) |
| | cacheSignal?.endRead() |
| | prerenderResumeDataCache?.decryptedBoundArgs.set(encrypted, decrypted) |
| | } |
| | } else { |
| | decrypted = await decodeActionBoundArg(actionId, encrypted) |
| | } |
| |
|
| | const { edgeRscModuleMapping, rscModuleMapping } = |
| | getClientReferenceManifest() |
| |
|
| | |
| | const deserialized = await createFromReadableStream( |
| | new ReadableStream({ |
| | start(controller) { |
| | controller.enqueue(textEncoder.encode(decrypted)) |
| |
|
| | switch (workUnitStore?.type) { |
| | case 'prerender': |
| | case 'prerender-runtime': |
| | |
| | |
| | if (workUnitStore.renderSignal.aborted) { |
| | controller.close() |
| | } else { |
| | workUnitStore.renderSignal.addEventListener( |
| | 'abort', |
| | () => controller.close(), |
| | { once: true } |
| | ) |
| | } |
| | break |
| | case 'prerender-client': |
| | case 'prerender-ppr': |
| | case 'prerender-legacy': |
| | case 'request': |
| | case 'cache': |
| | case 'private-cache': |
| | case 'unstable-cache': |
| | case undefined: |
| | return controller.close() |
| | default: |
| | workUnitStore satisfies never |
| | } |
| | }, |
| | }), |
| | { |
| | findSourceMapURL, |
| | serverConsumerManifest: { |
| | |
| | |
| | |
| | moduleLoading: null, |
| | moduleMap: isEdgeRuntime ? edgeRscModuleMapping : rscModuleMapping, |
| | serverModuleMap: getServerModuleMap(), |
| | }, |
| | } |
| | ) |
| |
|
| | return deserialized |
| | } |
| |
|