Skip to content

[flags] accept Vercel-signed override tokens - #587

Open
luismeyer wants to merge 6 commits into
luismeyer/flag-values-platform-encryptionfrom
luismeyer/kms-signed-overrides
Open

luismeyer wants to merge 6 commits into
luismeyer/flag-values-platform-encryptionfrom
luismeyer/kms-signed-overrides

Conversation

@luismeyer

@luismeyer luismeyer commented Nov 3, 2026 •

Copy link
Copy Markdown
Member

Stacked on #586. Merge #586 first. This PR's diff is against luismeyer/flag-values-platform-encryption.

Summary

Lets v4 apps accept flag overrides that the Vercel Toolbar signs with Vercel KMS, so overrides work on Vercel without a FLAGS_SECRET. This is the SDK half of vercel/next-live-mode#3898.

  • Same cookie, same API. The SDK still reads only vercel-flag-overrides, and no exports change.
  • Dispatch by token shape. On Vercel (VERCEL), or locally after vercel env pull (VERCEL_OIDC_TOKEN), a 3-part JWS is verified as a Vercel-signed token. Everything else goes to the existing decryptOverrides, unchanged. Signed and FLAGS_SECRET tokens are dual-read, so the Toolbar can roll signing out to projects that still have a FLAGS_SECRET.
  • SvelteKit without a secret (enabled by [flags] Encrypt FlagValues with the public key of Vercel without FLAGS_SECRET #586) now accepts signed overrides. Before, it had no way to read overrides at all.
  • Verification (src/lib/flag-overrides-platform.ts, shaped like flag-values-platform.ts: exported issuer, JWKS URL and frozen FLAG_OVERRIDES_POLICY; isFlagOverridesFromPlatform / verifyFlagOverridesFromPlatform):
    • Checks: ES256 only; issuer https://kms.vercel.com/64af96a2-ba60-42ef-91af-459fcef6f864; aud vercel:flag-overrides; pur flag-overrides; ver 1; required iat/exp; at most 7-day lifetime.
    • The token header must have a kid and no jku/jwk/x5u/crit. The token is limited to 16 KB.
    • The host's project must be in scope.projectIds (all projects of a microfrontends group share the cookie), its environment in environments, and the team must match when known. Unknown scope keys are rejected. projectIds differs from scope.projectId in the v5 standard, which needs the same change.
    • An invalid token or unavailable keys means no overrides; it never throws.
  • Shared with [flags] Encrypt FlagValues with the public key of Vercel without FLAGS_SECRET #586 (src/lib/vercel-platform.ts):
    • findVercelProject(): the host project from VERCEL_PROJECT_ID/VERCEL_TARGET_ENV, the VERCEL_OIDC_TOKEN claims, or .vercel/project.json.
    • createKeySetCache(): a JWKS fetched with no-store and kept in memory for 5 minutes. Concurrent loads share one request, and it reloads at most once per 30s for an unknown kid or after an error. It keeps the size, key-count and private-key checks.
    • flag-values-platform.ts from [flags] Encrypt FlagValues with the public key of Vercel without FLAGS_SECRET #586 now uses both. Its behavior is unchanged, except that a failed reload keeps serving the last good key instead of throwing.
    • So there is no per-request network I/O.
  • Signed tokens aren't memoized, so they stop working when they expire.

Notes for reconciling with standard (v5)

  • standard rejects signed tokens when FLAGS_SECRET is set. This PR intentionally accepts them, because the Toolbar signs for all projects behind its rollout flag. v5 needs the same rule before it ships.
  • standard reads flag-overrides first, then vercel-flag-overrides, so v5 already works with the Toolbar writing vercel-flag-overrides.
  • The token contract matches standard's vercel.ts. This module is a reduced copy: no options object, and ES256 only.

Test plan

🤖 Generated with Claude Code

@vercel

vercel Bot commented Nov 3, 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

@vercel
vercel Bot temporarily deployed to Preview – flags-sdk-openfeature November 3, 2026 12:11 Inactive
@vercel
vercel Bot temporarily deployed to Preview – flags-sdk-posthog November 3, 2026 12:11 Inactive
@vercel
vercel Bot temporarily deployed to Preview – flags-sdk-growthbook November 3, 2026 12:11 Inactive
@vercel
vercel Bot temporarily deployed to Preview – flags-sdk-statsig November 3, 2026 12:11 Inactive
@vercel
vercel Bot temporarily deployed to Preview – flags-sdk-vercel November 3, 2026 12:11 Inactive
@luismeyer luismeyer changed the title feat(flags): accept Vercel-signed override tokens [flags] accept Vercel-signed override tokens Nov 3, 2026
@luismeyer
luismeyer force-pushed the luismeyer/kms-signed-overrides branch from 104bfaf to ed43299 Compare November 3, 2026 18:58
@luismeyer
luismeyer changed the base branch from main to luismeyer/flag-values-platform-encryption November 3, 2026 18:58
luismeyer and others added 6 commits November 10, 2026 16:33
On Vercel, or locally after `vercel env pull`, verify override tokens which
the Vercel Toolbar signs with Vercel KMS, alongside FLAGS_SECRET tokens. Share
the host project lookup and the JWKS cache with the platform encryption of the
flag values.

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

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

Rename vercel-overrides to flag-overrides-platform, and use the same shape as
flag-values-platform: an exported issuer, JWKS URL and frozen policy, a
publicKeys cache, and the FromPlatform/ForPlatform names.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Apps which read the vercel-flag-overrides cookie themselves call
decryptOverrides, so it verifies overrides signed by Vercel KMS too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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! Two things to fix before approval:

  • SvelteKit: signed overrides do not work in local development, because vite dev does not put .env.local values in process.env
  • Replace the nested ternary in SvelteKit flag() and add braces to the if in isInScope (AGENTS.md)

The other comments are non-blocking. The "Shared with #586" part and the test count in the description are out of date.

*/
export function isVercelSignedToken(token: string): boolean {
return (
Boolean(process.env.VERCEL || process.env.VERCEL_OIDC_TOKEN) &&

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: SvelteKit local development never accepts a signed token

In vite dev, values from .env.local are in $env/* but not in process.env. That is why sveltekit/env.ts imports $env/static/private for the secret. This check and findVercelProject() read process.env only. So after vercel env pull, a SvelteKit app still ignores each signed token locally, with no log.

I checked this in examples/sveltekit-example with a temporary env file and route: process.env.VERCEL and process.env.VERCEL_OIDC_TOKEN were not set, and $env/dynamic/private had both.

Could the SvelteKit entry supply these variables, as it does for the secret? If not, the changeset must say that the local case is for Next.js only. findVercelProject() comes from #586, so the fix can go there.

Comment on lines +203 to +209
const overrides = !overridesCookie
? undefined
: isFlagOverridesFromPlatform(overridesCookie)
? await verifyFlagOverridesFromPlatform(overridesCookie)
: store.secret
? await _decryptOverrides(overridesCookie, store.secret)
: undefined;

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: Nested ternary

AGENTS.md says to avoid nested ternaries. _decryptOverrides already sends a signed token to the verifier, so one guard is sufficient:

let overrides: FlagOverridesType | undefined;
if (
  overridesCookie &&
  (store.secret || isFlagOverridesFromPlatform(overridesCookie))
) {
  overrides = await _decryptOverrides(overridesCookie, store.secret);
}

I tried this locally: the type check and the SvelteKit tests pass.

scope: unknown,
host: { projectId: string; environment: string; teamId?: string },
): boolean {
if (!isRecord(scope)) return false;

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: Missing braces

AGENTS.md asks for curly braces on each control-flow body in new code:

if (!isRecord(scope)) {
  return false;
}

Comment on lines +17 to +19
const cookieOverrides = isFlagOverridesFromPlatform(cookie)
? await verifyFlagOverridesFromPlatform(cookie)
: await memoizedDecrypt(cookie);

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): Ignore a cookie that the host can not read when there is no secret

This is not new. On main, each non-empty cookie makes flag() throw flags: Missing FLAGS_SECRET when there is no secret. But this PR makes an app with no secret a supported setup, and there the throw still occurs in two cases:

  • A signed cookie when the host can not verify it, e.g. locally before vercel env pull
  • An older encrypted cookie that is still in the browser after the project removes FLAGS_SECRET

The message then points to the wrong fix. SvelteKit flag() ignores the cookie in the same cases. Could Next do the same?

Comment on lines +309 to +311
} catch {
return undefined;
}

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): Log why a signed token is ignored

A signed cookie that the host can not use is ignored with no log. The user then sees no override and no reason. Cases that I checked:

  • A local Next app with FLAGS_SECRET in its env file but no vercel env pull (the token goes to the decrypt path and fails there)
  • The key set does not load
  • The token is for a different project or environment

A console.warn (not more than one per cooldown) that names the cause would help. For the first case, it can name vercel env pull as the fix.

publicKeys.set(issuer, keys);
return async (header, token) => {
try {
return await (await keys.get())(header, token);

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): The key fetch has no timeout

createKeySetCache (from #586) calls fetch with no signal. With this PR, a flag call for a request with a signed cookie waits on that fetch. If the key set endpoint does not answer, the flag call does not settle. This also occurs when keys are in memory but older than 5 minutes, because get() waits for the reload.

An AbortSignal.timeout(...) on the fetch, and the keys in memory as the result during a reload, would fix it. This can go in #586.

},
}),
],
])('rejects %s', async (_, token) => {

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): Some more rejection cases

The verifier rejects these correctly (I checked locally), and the policy depends on them, but they have no test:

  • A future iat and a future nbf
  • A missing iat and a missing exp
  • A token longer than maxTokenLength
  • x5u and crit in the header

Comment on lines +352 to +359
const keys =
publicKeys.get(issuer) ??
createKeySetCache(
`${issuer}/jwks.json`,
'the public keys of Vercel KMS',
(keySet) => createLocalJWKSet(keySet as unknown as JSONWebKeySet),
);
publicKeys.set(issuer, keys);

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.

nitpick (non-blocking): Set the cache entry only when it is missing

publicKeys.set runs on each call, also when the entry exists:

let keys = publicKeys.get(issuer);
if (!keys) {
  keys = createKeySetCache(/* ... */);
  publicKeys.set(issuer, keys);
}


/**
* Whether the token is signed by Vercel (a JWS, which has three parts) and the
* host can verify it: on Vercel, or locally after `vercel env pull`.

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.

question (non-blocking): What occurs when system environment variables are not exposed?

The docs describe VERCEL as "An indicator to show that system environment variables have been exposed to your project's Deployments". If a project has that setting off, I think this check is false on Vercel, and VERCEL_PROJECT_ID is also missing. Is that correct? If yes, a note in the docs or the changeset would help.

This branch was successfully deployed

14 active deployments
Preview – flags-sdk-dev — 2cda6d1e Deployed Nov 10, 2026 by vercel[bot]
Preview – flags-sdk-sveltekit-snippets — 2cda6d1e Deployed Nov 10, 2026 by vercel[bot]
Preview – flags-playground — 2cda6d1e Deployed Nov 10, 2026 by vercel[bot]
Preview – flags-sdk-vercel — 2cda6d1e Deployed Nov 10, 2026 by vercel[bot]
Preview – flags-sdk-posthog — 2cda6d1e Deployed Nov 10, 2026 by vercel[bot]
Preview – flags-sdk-snippets — 2cda6d1e Deployed Nov 10, 2026 by vercel[bot]
Preview – flags-sdk-growthbook — 2cda6d1e Deployed Nov 10, 2026 by vercel[bot]
Preview – flags-sdk-launchdarkly — 2cda6d1e Deployed Nov 10, 2026 by vercel[bot]
Preview – flags-sdk-reflag — 2cda6d1e Deployed Nov 10, 2026 by vercel[bot]
Preview – shirt-shop — 2cda6d1e Deployed Nov 10, 2026 by vercel[bot]
Preview – flags-sdk-statsig — 2cda6d1e Deployed Nov 10, 2026 by vercel[bot]
Preview – flags-sdk-flagsmith — 2cda6d1e Deployed Nov 10, 2026 by vercel[bot]
Preview – flags-sdk-openfeature — 2cda6d1e Deployed Nov 10, 2026 by vercel[bot]
Preview – shirt-shop-api — 2cda6d1e 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