Skip to content

[flags] Encrypt FlagValues with the public key of Vercel without FLAGS_SECRET - #586

Open
luismeyer wants to merge 4 commits into
mainfrom
luismeyer/flag-values-platform-encryption
Open

luismeyer wants to merge 4 commits into
mainfrom
luismeyer/flag-values-platform-encryption

Conversation

@luismeyer

@luismeyer luismeyer commented Nov 2, 2026 •

Copy link
Copy Markdown
Member

Why

  • encryptFlagValues throws without FLAGS_SECRET, so a project needs a secret just to keep its flag values confidential.
  • The Flags Explorer can now decrypt values encrypted with the public key of Vercel through the API (vercel/api#103736, vercel/next-live-mode#3893 behind flags-explorer-api-decrypt).
  • This brings the platform encryption of the standard branch ([flags] FlagValues: projectIds in the protected header, flags-keys JWKS #575) to main, with the signature of encryptFlagValues unchanged.

What

  • With a secret (argument or FLAGS_SECRET), nothing changes.
  • Without one, encryptFlagValues encrypts with the public key from https://keys.vercel-flags.com/.well-known/jwks.json (ECDH-ES, A256GCM, projectIds and kid in the protected header, pur: flag-values, ver: 1, aud: vercel:flag-values). This is the format that @api/flag-values decrypts.
  • The project ID comes from VERCEL_PROJECT_ID, the VERCEL_OIDC_TOKEN of vercel env pull, or .vercel/project.json.
  • In SvelteKit, createHandle and encryptFlagValues no longer require a secret. Without one, the data-flag-values of a page use the public key, the overrides cookie is ignored, /.well-known/vercel/flags responds with 401, and precompute still requires a secret.
  • The key set is cached for 5 minutes. Concurrent loads share one request, and after a failed load there is a 30 s cooldown.

Validation

  • New tests in flag-values-platform.test.ts cover the token format, finding the project ID, key caching and rotation, the error cooldown, and that a set FLAGS_SECRET is still used. without-secret.test.ts covers SvelteKit's createHandle and encryptFlagValues without a secret.

🤖 Generated with Claude Code

@vercel

vercel Bot commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
flags-playground Ready Ready Preview, v0 Nov 10, 2026 3:34pm UTC
flags-sdk-dev Ready Ready Preview, v0 Nov 10, 2026 3:34pm UTC
flags-sdk-flagsmith Ready Ready Preview, v0 Nov 10, 2026 3:34pm UTC
flags-sdk-growthbook Ready Ready Preview, v0 Nov 10, 2026 3:34pm UTC
flags-sdk-launchdarkly Ready Ready Preview, v0 Nov 10, 2026 3:34pm UTC
flags-sdk-openfeature Ready Ready Preview, v0 Nov 10, 2026 3:34pm UTC
flags-sdk-posthog Ready Ready Preview, v0 Nov 10, 2026 3:34pm UTC
flags-sdk-reflag Ready Ready Preview, v0 Nov 10, 2026 3:34pm UTC
flags-sdk-snippets Ready Ready Preview, v0 Nov 10, 2026 3:34pm UTC
flags-sdk-statsig Ready Ready Preview, v0 Nov 10, 2026 3:34pm UTC
flags-sdk-sveltekit-snippets Ready Ready Preview, v0 Nov 10, 2026 3:34pm UTC
shirt-shop Ready Ready Preview, v0 Nov 10, 2026 3:34pm UTC
shirt-shop-api Ready Ready Preview, v0 Nov 10, 2026 3:34pm UTC

luismeyer and others added 4 commits November 10, 2026 16:33
…S_SECRET

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ut new options

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…secret

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rcel-platform

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@chriswdmr chriswdmr left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! Three things to fix before merge:

  • SvelteKit without a secret: a failure to load the key must not break the page render
  • Add a timeout to the key set fetch
  • Next.js middleware: remove the two new Edge Runtime build warnings

The other comments are suggestions and nits.

}): Handle {
return async function handle({ event, resolve }) {
secret ??= await tryGetSecret(secret);
secret ??= await getSecret();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (blocking): A key load failure breaks the page

Without a secret, _encryptFlagValues in transformPageChunk can throw. A 503 or a network error from the key host rejects the handle for each page that uses a flag. The error stays for the 30 s cooldown. A missing project ID has the same result. The values are only for the toolbar. Can we catch the error on this path, leave out the script tag, and log one warning?

This is not a regression: before, createHandle threw without a secret. But it is the weak point of the new mode.

try {
// Read the global fetch at each load, because a framework can replace
// it. no-store, so a framework which caches fetch never keeps old keys.
const response = await globalThis.fetch(url, {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (blocking): No timeout on the key set fetch

If the key host does not respond, the render waits for the default of the runtime, which is 300 s in Node.js. Concurrent requests share pending, so they all wait. A key older than 5 minutes is not served during the reload, so warm instances wait too. prepare-flags-definitions uses AbortSignal.timeout(5_000) for its fetch. Can we do the same here?


function readLinkedProjectId(): string | undefined {
// getBuiltinModule needs no import, so runtimes other than Node.js work
const fs = process.getBuiltinModule?.('node:fs');

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (blocking): New Edge Runtime build warnings

process.getBuiltinModule and process.cwd cause warnings in the Next.js webpack build when middleware.ts imports flags/next. This also occurs with FLAGS_SECRET set. I built tests/next-15: main gives 0 warnings, and this branch gives 2:

A Node.js API is used (process.getBuiltinModule at line: 35) which is not supported in the Edge Runtime.
A Node.js API is used (process.cwd at line: 38) which is not supported in the Edge Runtime.

This change gave 0 warnings in my local build:

const proc = globalThis.process;
const fs = proc?.getBuiltinModule?.('node:fs');
// ...
fs.readFileSync(`${proc.cwd()}/.vercel/project.json`, 'utf8'),

loadedAt = Date.now();
error = undefined;
return value;
} catch (cause) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (non-blocking): The cache hides the control-flow errors of Next.js

In Next.js 15, a static page that calls encryptFlagValues without a secret fails next build. The fetch uses cache: 'no-store', so Next.js throws DynamicServerError to make the page dynamic. This catch wraps the error, the digest is lost, and the build stops. The example in core.mdx is such a page. With a secret, the same page builds as static.

With this line before the wrap, my local build passed and the page became dynamic:

if (cause instanceof Error && 'digest' in cause) throw cause;

*/
export function findVercelProject(): VercelProject {
const env = process.env;
const oidc = readOidcClaims();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit (non-blocking): Work on each call

readOidcClaims() runs also when VERCEL_PROJECT_ID is set. When the environment has no project ID, .vercel/project.json is read with readFileSync on each call: three calls gave three reads in my test. Could we decode the token only when it is necessary, or memoize the project?

) {
if (!secret) throw new Error('flags: Missing FLAGS_SECRET');
return encryptJwe({ v: flagValues, pur: 'values' }, secret, expirationTime);
if (!secret) return encryptFlagValuesForPlatform(flagValues, expirationTime);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (non-blocking): Say that the fallback is silent

Before, encryptFlagValues(values, process.env.TYPO) or an empty secret threw an error. Now it uses the public key, and decryptFlagValues cannot read that token. Could the changeset and the JSDoc say this?

}): Handle {
return async function handle({ event, resolve }) {
secret ??= await tryGetSecret(secret);
secret ??= await getSecret();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit (non-blocking): SvelteKit docs

sveltekit.mdx still shows secret: string as necessary for createHandle. Could we update it in this PR?

});
});

it('does not serve the flags discovery endpoint', async () => {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (non-blocking): Two more tests

  • The changeset says that the overrides cookie is ignored without a secret. A test for that would be good
  • A test for the .vercel/project.json source (readLinkedProjectId)

This branch was successfully deployed

14 active deployments
Preview – flags-sdk-dev — 888794bc Deployed Nov 10, 2026 by vercel[bot]
Preview – flags-sdk-sveltekit-snippets — 888794bc Deployed Nov 10, 2026 by vercel[bot]
Preview – flags-sdk-launchdarkly — 888794bc Deployed Nov 10, 2026 by vercel[bot]
Preview – flags-playground — 888794bc Deployed Nov 10, 2026 by vercel[bot]
Preview – flags-sdk-reflag — 888794bc Deployed Nov 10, 2026 by vercel[bot]
Preview – flags-sdk-snippets — 888794bc Deployed Nov 10, 2026 by vercel[bot]
Preview – flags-sdk-growthbook — 888794bc Deployed Nov 10, 2026 by vercel[bot]
Preview – shirt-shop — 888794bc Deployed Nov 10, 2026 by vercel[bot]
Preview – flags-sdk-openfeature — 888794bc Deployed Nov 10, 2026 by vercel[bot]
Preview – flags-sdk-posthog — 888794bc Deployed Nov 10, 2026 by vercel[bot]
Preview – flags-sdk-vercel — 888794bc Deployed Nov 10, 2026 by vercel[bot]
Preview – flags-sdk-flagsmith — 888794bc Deployed Nov 10, 2026 by vercel[bot]
Preview – flags-sdk-statsig — 888794bc Deployed Nov 10, 2026 by vercel[bot]
Preview – shirt-shop-api — 888794bc Deployed Nov 10, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants