diff --git a/src/lib/functions/server.ts b/src/lib/functions/server.ts index 05aefd9e9ff..de4ea543ef7 100644 --- a/src/lib/functions/server.ts +++ b/src/lib/functions/server.ts @@ -34,21 +34,35 @@ const buildClientContext = function (headers) { const parts = headers.authorization.split(' ') if (parts.length !== 2 || parts[0] !== 'Bearer') return + const identity = { + url: 'https://netlify-dev-locally-emulated-identity.netlify.com/.netlify/identity', + token: + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzb3VyY2UiOiJuZXRsaWZ5IGRldiIsInRlc3REYXRhIjoiTkVUTElGWV9ERVZfTE9DQUxMWV9FTVVMQVRFRF9JREVOVElUWSJ9.2eSDqUOZAOBsx39FHFePjYj12k0LrxldvGnlvDu3GMI', + // you can decode this with https://jwt.io/ + // just says + // { + // "source": "netlify dev", + // "testData": "NETLIFY_DEV_LOCALLY_EMULATED_IDENTITY" + // } + } + + // This data is available on both the context root and under custom.netlify for retro-compatibility. + // In the future it will only be available in custom.netlify. + // @ts-expect-error + const user = jwtDecode(parts[1]) + + const netlifyContext = JSON.stringify({ + identity: identity, + user: user, + }) + try { return { - identity: { - url: 'https://netlify-dev-locally-emulated-identity.netlify.com/.netlify/identity', - token: - 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzb3VyY2UiOiJuZXRsaWZ5IGRldiIsInRlc3REYXRhIjoiTkVUTElGWV9ERVZfTE9DQUxMWV9FTVVMQVRFRF9JREVOVElUWSJ9.2eSDqUOZAOBsx39FHFePjYj12k0LrxldvGnlvDu3GMI', - // you can decode this with https://jwt.io/ - // just says - // { - // "source": "netlify dev", - // "testData": "NETLIFY_DEV_LOCALLY_EMULATED_IDENTITY" - // } + identity: identity, + user: user, + custom: { + netlify: Buffer.from(netlifyContext).toString('base64'), }, - // @ts-expect-error - user: jwtDecode(parts[1]), } } catch { // Ignore errors - bearer token is not a JWT, probably not intended for us diff --git a/tests/integration/commands/dev/dev-miscellaneous.test.js b/tests/integration/commands/dev/dev-miscellaneous.test.js index 56e2104346b..5b3e40440de 100644 --- a/tests/integration/commands/dev/dev-miscellaneous.test.js +++ b/tests/integration/commands/dev/dev-miscellaneous.test.js @@ -1,3 +1,4 @@ +import { Buffer } from 'buffer' import path from 'path' import { fileURLToPath } from 'url' @@ -151,6 +152,35 @@ describe.concurrent('commands/dev-miscellaneous', () => { }) }) + test('function clientContext.custom.netlify should be set', async (t) => { + await withSiteBuilder('site-with-function', async (builder) => { + await builder + .withNetlifyToml({ config: { functions: { directory: 'functions' } } }) + .withFunction({ + path: 'hello.js', + handler: async (_, context) => ({ + statusCode: 200, + body: JSON.stringify(context), + }), + }) + .buildAsync() + + await withDevServer({ cwd: builder.directory }, async (server) => { + const response = await fetch(`${server.url}/.netlify/functions/hello`, { + headers: { + Authorization: + 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzb3VyY2UiOiJuZXRsaWZ5IGRldiIsInRlc3REYXRhIjoiTkVUTElGWV9ERVZfTE9DQUxMWV9FTVVMQVRFRF9JREVOVElUWSJ9.2eSDqUOZAOBsx39FHFePjYj12k0LrxldvGnlvDu3GMI', + }, + }).then((res) => res.json()) + + const netlifyContext = Buffer.from(response.clientContext.custom.netlify, 'base64').toString() + t.expect(JSON.parse(netlifyContext).identity.url).toEqual( + 'https://netlify-dev-locally-emulated-identity.netlify.com/.netlify/identity', + ) + }) + }) + }) + test('should enforce role based redirects with default secret and role path', async (t) => { await withSiteBuilder('site-with-default-role-based-redirects', async (builder) => { setupRoleBasedRedirectsSite(builder)