Skip to content

Commit 7973ff9

Browse files
authored
Store preview id for two weeks (#79972)
Create a file `.previewinfo` (like `.rscinfo`) to store the preview mode keys (which are stored in `prerender-manifest.json` and were previously changing on every single commit) ``` "preview": { "previewModeId": "6ec938745bce9b5d51329802d1a9788a", "previewModeSigningKey": "f3df5e714dd35c14fedb15b396566ae9530e058f5dde09a82b12941214d96c4d", "previewModeEncryptionKey": "675f97882ebc73c55bc51903f4e5c8d3a17ac90108847ed1b1b90a46d84f368e" } ```
1 parent 4b06e62 commit 7973ff9

File tree

2 files changed

+105
-6
lines changed

2 files changed

+105
-6
lines changed

packages/next/src/build/index.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import '../lib/setup-exception-listeners'
99

1010
import { loadEnvConfig, type LoadedEnvFiles } from '@next/env'
1111
import { bold, yellow } from '../lib/picocolors'
12-
import crypto from 'crypto'
1312
import { makeRe } from 'next/dist/compiled/picomatch'
1413
import { existsSync, promises as fs } from 'fs'
1514
import os from 'os'
@@ -210,6 +209,7 @@ import { durationToString } from './duration-to-string'
210209
import { traceGlobals } from '../trace/shared'
211210
import { extractNextErrorCode } from '../lib/error-telemetry-utils'
212211
import { runAfterProductionCompile } from './after-production-compile'
212+
import { generatePreviewKeys } from './preview-key-utils'
213213

214214
type Fallback = null | boolean | string
215215

@@ -1093,11 +1093,10 @@ export default async function build(
10931093

10941094
NextBuildContext.hasInstrumentationHook = hasInstrumentationHook
10951095

1096-
const previewProps: __ApiPreviewProps = {
1097-
previewModeId: crypto.randomBytes(16).toString('hex'),
1098-
previewModeSigningKey: crypto.randomBytes(32).toString('hex'),
1099-
previewModeEncryptionKey: crypto.randomBytes(32).toString('hex'),
1100-
}
1096+
const previewProps: __ApiPreviewProps = await generatePreviewKeys({
1097+
isBuild: true,
1098+
distDir,
1099+
})
11011100
NextBuildContext.previewProps = previewProps
11021101

11031102
const mappedPages = await nextBuildSpan
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import path from 'path'
2+
import fs from 'fs'
3+
import crypto from 'crypto'
4+
import type { __ApiPreviewProps } from '../server/api-utils'
5+
import { getStorageDirectory } from '../server/cache-dir'
6+
7+
const CONFIG_FILE = '.previewinfo'
8+
const PREVIEW_ID = 'previewModeId'
9+
const PREVIEW_SIGNING_KEY = 'previewModeSigningKey'
10+
const PREVIEW_ENCRYPTION_KEY = 'previewModeEncryptionKey'
11+
const PREVIEW_EXPIRE_AT = 'expireAt'
12+
const EXPIRATION = 1000 * 60 * 60 * 24 * 14 // 14 days
13+
14+
async function writeCache(distDir: string, config: __ApiPreviewProps) {
15+
const cacheBaseDir = getStorageDirectory(distDir)
16+
if (!cacheBaseDir) return
17+
18+
const configPath = path.join(cacheBaseDir, CONFIG_FILE)
19+
if (!fs.existsSync(cacheBaseDir)) {
20+
await fs.promises.mkdir(cacheBaseDir, { recursive: true })
21+
}
22+
await fs.promises.writeFile(
23+
configPath,
24+
JSON.stringify({
25+
[PREVIEW_ID]: config.previewModeId,
26+
[PREVIEW_SIGNING_KEY]: config.previewModeSigningKey,
27+
[PREVIEW_ENCRYPTION_KEY]: config.previewModeEncryptionKey,
28+
[PREVIEW_EXPIRE_AT]: Date.now() + EXPIRATION,
29+
})
30+
)
31+
}
32+
33+
function generateConfig() {
34+
return {
35+
previewModeId: crypto.randomBytes(16).toString('hex'),
36+
previewModeSigningKey: crypto.randomBytes(32).toString('hex'),
37+
previewModeEncryptionKey: crypto.randomBytes(32).toString('hex'),
38+
}
39+
}
40+
41+
// This utility is used to get a key for the cache directory. If the
42+
// key is not present, it will generate a new one and store it in the
43+
// cache directory inside dist.
44+
// The key will also expire after a certain amount of time. Once it
45+
// expires, a new one will be generated.
46+
export async function generatePreviewKeys({
47+
distDir,
48+
isBuild,
49+
}: {
50+
distDir: string
51+
isBuild: boolean
52+
}): Promise<__ApiPreviewProps> {
53+
const cacheBaseDir = getStorageDirectory(distDir)
54+
55+
if (!cacheBaseDir) {
56+
// There's no persistent storage available. We generate a new config.
57+
// This also covers development time.
58+
return generateConfig()
59+
}
60+
61+
const configPath = path.join(cacheBaseDir, CONFIG_FILE)
62+
async function tryReadCachedConfig(): Promise<false | __ApiPreviewProps> {
63+
if (!fs.existsSync(configPath)) return false
64+
try {
65+
const config = JSON.parse(await fs.promises.readFile(configPath, 'utf8'))
66+
if (!config) return false
67+
if (
68+
typeof config[PREVIEW_ID] !== 'string' ||
69+
typeof config[PREVIEW_ENCRYPTION_KEY] !== 'string' ||
70+
typeof config[PREVIEW_SIGNING_KEY] !== 'string' ||
71+
typeof config[PREVIEW_EXPIRE_AT] !== 'number'
72+
) {
73+
return false
74+
}
75+
// For build time, we need to rotate the key if it's expired. Otherwise
76+
// (next start) we have to keep the key as it is so the runtime key matches
77+
// the build time key.
78+
if (isBuild && config[PREVIEW_EXPIRE_AT] < Date.now()) {
79+
return false
80+
}
81+
82+
return {
83+
previewModeId: config[PREVIEW_ID],
84+
previewModeSigningKey: config[PREVIEW_SIGNING_KEY],
85+
previewModeEncryptionKey: config[PREVIEW_ENCRYPTION_KEY],
86+
}
87+
} catch (e) {
88+
// Broken config file. We should generate a new key and overwrite it.
89+
return false
90+
}
91+
}
92+
const maybeValidConfig = await tryReadCachedConfig()
93+
if (maybeValidConfig !== false) {
94+
return maybeValidConfig
95+
}
96+
const config = generateConfig()
97+
await writeCache(distDir, config)
98+
99+
return config
100+
}

0 commit comments

Comments
 (0)