From 90714f81882638e107ef4fcecd52bc31ca449d7b Mon Sep 17 00:00:00 2001 From: ben-fornefeld Date: Mon, 18 May 2026 17:26:01 -0700 Subject: [PATCH 1/4] Hydrate dashboard creator emails from Supabase auth Co-authored-by: Cursor --- src/core/modules/keys/repository.server.ts | 43 ++++- .../modules/templates/repository.server.ts | 12 +- .../modules/users/auth-user-emails.server.ts | 72 ++++++++ tests/unit/keys-repository.test.ts | 156 ++++++++++++++++++ 4 files changed, 279 insertions(+), 4 deletions(-) create mode 100644 src/core/modules/users/auth-user-emails.server.ts create mode 100644 tests/unit/keys-repository.test.ts diff --git a/src/core/modules/keys/repository.server.ts b/src/core/modules/keys/repository.server.ts index 030b2c4c7..05f4138cf 100644 --- a/src/core/modules/keys/repository.server.ts +++ b/src/core/modules/keys/repository.server.ts @@ -2,14 +2,20 @@ import 'server-only' import { SUPABASE_AUTH_HEADERS } from '@/configs/api' import type { CreatedTeamAPIKey, TeamAPIKey } from '@/core/modules/keys/models' +import { + type AuthUserEmailResolver, + getAuthUserEmailsById, + resolveMissingCreatorEmails, +} from '@/core/modules/users/auth-user-emails.server' import { infra } from '@/core/shared/clients/api' -import { repoErrorFromHttp } from '@/core/shared/errors' +import { createRepoError, repoErrorFromHttp } from '@/core/shared/errors' import type { TeamRequestScope } from '@/core/shared/repository-scope' import { err, ok, type RepoResult } from '@/core/shared/result' type KeysRepositoryDeps = { infraClient: typeof infra authHeaders: typeof SUPABASE_AUTH_HEADERS + resolveAuthUserEmailsById: AuthUserEmailResolver } export type KeysScope = TeamRequestScope @@ -25,6 +31,7 @@ export function createKeysRepository( deps: KeysRepositoryDeps = { infraClient: infra, authHeaders: SUPABASE_AUTH_HEADERS, + resolveAuthUserEmailsById: getAuthUserEmailsById, } ): KeysRepository { return { @@ -45,7 +52,12 @@ export function createKeysRepository( ) } - return ok(res.data ?? []) + return ok( + await resolveMissingCreatorEmails( + res.data ?? [], + deps.resolveAuthUserEmailsById + ) + ) }, async createApiKey(name) { const res = await deps.infraClient.POST('/api-keys', { @@ -67,7 +79,32 @@ export function createKeysRepository( ) } - return ok(res.data) + if (!res.data) { + return err( + createRepoError({ + code: 'internal', + status: 500, + message: 'Failed to create API key', + }) + ) + } + + const [apiKey] = await resolveMissingCreatorEmails( + [res.data], + deps.resolveAuthUserEmailsById + ) + + if (!apiKey) { + return err( + createRepoError({ + code: 'internal', + status: 500, + message: 'Failed to create API key', + }) + ) + } + + return ok(apiKey) }, async deleteApiKey(apiKeyId) { const res = await deps.infraClient.DELETE('/api-keys/{apiKeyID}', { diff --git a/src/core/modules/templates/repository.server.ts b/src/core/modules/templates/repository.server.ts index c432b6a57..f21edf1fb 100644 --- a/src/core/modules/templates/repository.server.ts +++ b/src/core/modules/templates/repository.server.ts @@ -8,6 +8,11 @@ import { MOCK_TEMPLATES_DATA, } from '@/configs/mock-data' import type { DefaultTemplate, Template } from '@/core/modules/templates/models' +import { + type AuthUserEmailResolver, + getAuthUserEmailsById, + resolveMissingCreatorEmails, +} from '@/core/modules/users/auth-user-emails.server' import { api, infra } from '@/core/shared/clients/api' import { repoErrorFromHttp } from '@/core/shared/errors' import type { @@ -20,6 +25,7 @@ type TemplatesRepositoryDeps = { apiClient: typeof api infraClient: typeof infra authHeaders: typeof SUPABASE_AUTH_HEADERS + resolveAuthUserEmailsById: AuthUserEmailResolver } export interface TeamTemplatesRepository { @@ -43,6 +49,7 @@ export function createTemplatesRepository( apiClient: api, infraClient: infra, authHeaders: SUPABASE_AUTH_HEADERS, + resolveAuthUserEmailsById: getAuthUserEmailsById, } ): TeamTemplatesRepository { return { @@ -74,7 +81,10 @@ export function createTemplatesRepository( } return ok({ - templates: res.data, + templates: await resolveMissingCreatorEmails( + res.data ?? [], + deps.resolveAuthUserEmailsById + ), }) }, async deleteTemplate(templateId) { diff --git a/src/core/modules/users/auth-user-emails.server.ts b/src/core/modules/users/auth-user-emails.server.ts new file mode 100644 index 000000000..d69f7db24 --- /dev/null +++ b/src/core/modules/users/auth-user-emails.server.ts @@ -0,0 +1,72 @@ +import 'server-only' + +import { supabaseAdmin } from '@/core/shared/clients/supabase/admin' + +export type AuthUserEmailResolver = ( + userIds: string[] +) => Promise> + +export async function getAuthUserEmailsById( + userIds: string[] +): Promise> { + const uniqueUserIds = [...new Set(userIds.filter(Boolean))] + if (uniqueUserIds.length === 0) { + return new Map() + } + + const { data, error } = await supabaseAdmin + .from('auth_users') + .select('id,email') + .in('id', uniqueUserIds) + + if (error) { + throw error + } + + return new Map( + data + ?.filter((user) => user.id) + .map((user) => [user.id as string, user.email]) ?? [] + ) +} + +export async function resolveMissingCreatorEmails< + T extends { + createdBy?: { id: string; email?: string | null } | null + }, +>(items: T[], resolveEmails: AuthUserEmailResolver): Promise { + const missingEmailUserIds = items.flatMap((item) => { + const createdBy = item.createdBy + if (!createdBy || createdBy.email?.trim()) { + return [] + } + + return [createdBy.id] + }) + + if (missingEmailUserIds.length === 0) { + return items + } + + let emailByUserId: Map + try { + emailByUserId = await resolveEmails(missingEmailUserIds) + } catch { + return items + } + + return items.map((item) => { + const createdBy = item.createdBy + if (!createdBy || createdBy.email?.trim()) { + return item + } + + return { + ...item, + createdBy: { + ...createdBy, + email: emailByUserId.get(createdBy.id) ?? createdBy.email ?? null, + }, + } + }) +} diff --git a/tests/unit/keys-repository.test.ts b/tests/unit/keys-repository.test.ts new file mode 100644 index 000000000..73b0d241e --- /dev/null +++ b/tests/unit/keys-repository.test.ts @@ -0,0 +1,156 @@ +import { describe, expect, it, vi } from 'vitest' +import { createKeysRepository } from '@/core/modules/keys/repository.server' + +vi.mock('@/core/shared/clients/supabase/admin', () => ({ + supabaseAdmin: { + from: vi.fn(), + }, +})) + +function createApiResponse(input: { + ok: boolean + status: number + data?: T + error?: { message?: string } | null +}) { + return { + data: input.data, + error: input.error ?? null, + response: { + ok: input.ok, + status: input.status, + }, + } +} + +const baseApiKey = { + createdAt: '2026-01-01T00:00:00Z', + id: 'api-key-id', + mask: { + prefix: 'e2b', + valueLength: 16, + maskedValuePrefix: 'abc', + maskedValueSuffix: 'xyz', + }, + name: 'Key', +} + +describe('createKeysRepository', () => { + it('hydrates only missing creator emails when listing API keys', async () => { + const missingEmailUserId = '11111111-1111-1111-1111-111111111111' + const existingEmailUserId = '22222222-2222-2222-2222-222222222222' + const resolveAuthUserEmailsById = vi + .fn() + .mockResolvedValue(new Map([[missingEmailUserId, 'resolved@e2b.dev']])) + + const infraClient = { + GET: vi.fn().mockResolvedValue( + createApiResponse({ + ok: true, + status: 200, + data: [ + { + ...baseApiKey, + id: 'missing-email-key', + createdBy: { + id: missingEmailUserId, + email: null, + }, + }, + { + ...baseApiKey, + id: 'existing-email-key', + createdBy: { + id: existingEmailUserId, + email: 'existing@e2b.dev', + }, + }, + ], + }) + ), + POST: vi.fn(), + DELETE: vi.fn(), + } + + const repository = createKeysRepository( + { + accessToken: 'token', + teamId: 'team-id', + }, + { + infraClient: + infraClient as unknown as typeof import('@/core/shared/clients/api').infra, + authHeaders: vi.fn(() => ({ 'X-Supabase-Token': 'token' })), + resolveAuthUserEmailsById, + } + ) + + const result = await repository.listTeamApiKeys() + + expect(resolveAuthUserEmailsById).toHaveBeenCalledWith([missingEmailUserId]) + expect(result).toEqual({ + ok: true, + data: [ + expect.objectContaining({ + id: 'missing-email-key', + createdBy: { + id: missingEmailUserId, + email: 'resolved@e2b.dev', + }, + }), + expect.objectContaining({ + id: 'existing-email-key', + createdBy: { + id: existingEmailUserId, + email: 'existing@e2b.dev', + }, + }), + ], + }) + }) + + it('keeps API key listing usable when creator email lookup fails', async () => { + const userId = '11111111-1111-1111-1111-111111111111' + const resolveAuthUserEmailsById = vi + .fn() + .mockRejectedValue(new Error('lookup failed')) + + const apiKey = { + ...baseApiKey, + createdBy: { + id: userId, + email: null, + }, + } + + const infraClient = { + GET: vi.fn().mockResolvedValue( + createApiResponse({ + ok: true, + status: 200, + data: [apiKey], + }) + ), + POST: vi.fn(), + DELETE: vi.fn(), + } + + const repository = createKeysRepository( + { + accessToken: 'token', + teamId: 'team-id', + }, + { + infraClient: + infraClient as unknown as typeof import('@/core/shared/clients/api').infra, + authHeaders: vi.fn(() => ({ 'X-Supabase-Token': 'token' })), + resolveAuthUserEmailsById, + } + ) + + await expect(repository.listTeamApiKeys()).resolves.toEqual({ + ok: true, + data: [apiKey], + }) + }) +}) From fb3c5473f5c13517bc183f82900bf9b74abd9603 Mon Sep 17 00:00:00 2001 From: ben-fornefeld Date: Mon, 18 May 2026 17:52:41 -0700 Subject: [PATCH 2/4] Prefer Supabase auth for dashboard creator emails Co-authored-by: Cursor --- src/core/modules/keys/repository.server.ts | 6 +-- .../modules/templates/repository.server.ts | 4 +- .../modules/users/auth-user-emails.server.ts | 14 +++---- tests/unit/keys-repository.test.ts | 42 +++++++++++-------- 4 files changed, 36 insertions(+), 30 deletions(-) diff --git a/src/core/modules/keys/repository.server.ts b/src/core/modules/keys/repository.server.ts index 05f4138cf..724d27dea 100644 --- a/src/core/modules/keys/repository.server.ts +++ b/src/core/modules/keys/repository.server.ts @@ -5,7 +5,7 @@ import type { CreatedTeamAPIKey, TeamAPIKey } from '@/core/modules/keys/models' import { type AuthUserEmailResolver, getAuthUserEmailsById, - resolveMissingCreatorEmails, + resolveCreatorEmails, } from '@/core/modules/users/auth-user-emails.server' import { infra } from '@/core/shared/clients/api' import { createRepoError, repoErrorFromHttp } from '@/core/shared/errors' @@ -53,7 +53,7 @@ export function createKeysRepository( } return ok( - await resolveMissingCreatorEmails( + await resolveCreatorEmails( res.data ?? [], deps.resolveAuthUserEmailsById ) @@ -89,7 +89,7 @@ export function createKeysRepository( ) } - const [apiKey] = await resolveMissingCreatorEmails( + const [apiKey] = await resolveCreatorEmails( [res.data], deps.resolveAuthUserEmailsById ) diff --git a/src/core/modules/templates/repository.server.ts b/src/core/modules/templates/repository.server.ts index f21edf1fb..9d282af3e 100644 --- a/src/core/modules/templates/repository.server.ts +++ b/src/core/modules/templates/repository.server.ts @@ -11,7 +11,7 @@ import type { DefaultTemplate, Template } from '@/core/modules/templates/models' import { type AuthUserEmailResolver, getAuthUserEmailsById, - resolveMissingCreatorEmails, + resolveCreatorEmails, } from '@/core/modules/users/auth-user-emails.server' import { api, infra } from '@/core/shared/clients/api' import { repoErrorFromHttp } from '@/core/shared/errors' @@ -81,7 +81,7 @@ export function createTemplatesRepository( } return ok({ - templates: await resolveMissingCreatorEmails( + templates: await resolveCreatorEmails( res.data ?? [], deps.resolveAuthUserEmailsById ), diff --git a/src/core/modules/users/auth-user-emails.server.ts b/src/core/modules/users/auth-user-emails.server.ts index d69f7db24..5f6d44fd2 100644 --- a/src/core/modules/users/auth-user-emails.server.ts +++ b/src/core/modules/users/auth-user-emails.server.ts @@ -30,34 +30,34 @@ export async function getAuthUserEmailsById( ) } -export async function resolveMissingCreatorEmails< +export async function resolveCreatorEmails< T extends { createdBy?: { id: string; email?: string | null } | null }, >(items: T[], resolveEmails: AuthUserEmailResolver): Promise { - const missingEmailUserIds = items.flatMap((item) => { + const creatorUserIds = items.flatMap((item) => { const createdBy = item.createdBy - if (!createdBy || createdBy.email?.trim()) { + if (!createdBy) { return [] } return [createdBy.id] }) - if (missingEmailUserIds.length === 0) { + if (creatorUserIds.length === 0) { return items } let emailByUserId: Map try { - emailByUserId = await resolveEmails(missingEmailUserIds) + emailByUserId = await resolveEmails(creatorUserIds) } catch { return items } return items.map((item) => { const createdBy = item.createdBy - if (!createdBy || createdBy.email?.trim()) { + if (!createdBy) { return item } @@ -65,7 +65,7 @@ export async function resolveMissingCreatorEmails< ...item, createdBy: { ...createdBy, - email: emailByUserId.get(createdBy.id) ?? createdBy.email ?? null, + email: emailByUserId.get(createdBy.id) ?? null, }, } }) diff --git a/tests/unit/keys-repository.test.ts b/tests/unit/keys-repository.test.ts index 73b0d241e..614789929 100644 --- a/tests/unit/keys-repository.test.ts +++ b/tests/unit/keys-repository.test.ts @@ -36,12 +36,15 @@ const baseApiKey = { } describe('createKeysRepository', () => { - it('hydrates only missing creator emails when listing API keys', async () => { - const missingEmailUserId = '11111111-1111-1111-1111-111111111111' - const existingEmailUserId = '22222222-2222-2222-2222-222222222222' - const resolveAuthUserEmailsById = vi - .fn() - .mockResolvedValue(new Map([[missingEmailUserId, 'resolved@e2b.dev']])) + it('hydrates creator emails from Supabase Auth when listing API keys', async () => { + const firstUserId = '11111111-1111-1111-1111-111111111111' + const secondUserId = '22222222-2222-2222-2222-222222222222' + const resolveAuthUserEmailsById = vi.fn().mockResolvedValue( + new Map([ + [firstUserId, 'first@e2b.dev'], + [secondUserId, 'second@e2b.dev'], + ]) + ) const infraClient = { GET: vi.fn().mockResolvedValue( @@ -51,18 +54,18 @@ describe('createKeysRepository', () => { data: [ { ...baseApiKey, - id: 'missing-email-key', + id: 'first-key', createdBy: { - id: missingEmailUserId, + id: firstUserId, email: null, }, }, { ...baseApiKey, - id: 'existing-email-key', + id: 'second-key', createdBy: { - id: existingEmailUserId, - email: 'existing@e2b.dev', + id: secondUserId, + email: 'deprecated-response-value@e2b.dev', }, }, ], @@ -87,22 +90,25 @@ describe('createKeysRepository', () => { const result = await repository.listTeamApiKeys() - expect(resolveAuthUserEmailsById).toHaveBeenCalledWith([missingEmailUserId]) + expect(resolveAuthUserEmailsById).toHaveBeenCalledWith([ + firstUserId, + secondUserId, + ]) expect(result).toEqual({ ok: true, data: [ expect.objectContaining({ - id: 'missing-email-key', + id: 'first-key', createdBy: { - id: missingEmailUserId, - email: 'resolved@e2b.dev', + id: firstUserId, + email: 'first@e2b.dev', }, }), expect.objectContaining({ - id: 'existing-email-key', + id: 'second-key', createdBy: { - id: existingEmailUserId, - email: 'existing@e2b.dev', + id: secondUserId, + email: 'second@e2b.dev', }, }), ], From 7c7cf04d3aa7e6bc79d0038d1744b1135bea4730 Mon Sep 17 00:00:00 2001 From: ben-fornefeld Date: Mon, 18 May 2026 17:56:07 -0700 Subject: [PATCH 3/4] Log dashboard creator email lookup failures Co-authored-by: Cursor --- .../modules/users/auth-user-emails.server.ts | 14 +++++++++- tests/unit/keys-repository.test.ts | 26 ++++++++++++++++--- 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/src/core/modules/users/auth-user-emails.server.ts b/src/core/modules/users/auth-user-emails.server.ts index 5f6d44fd2..c9c870a1b 100644 --- a/src/core/modules/users/auth-user-emails.server.ts +++ b/src/core/modules/users/auth-user-emails.server.ts @@ -1,5 +1,6 @@ import 'server-only' +import { l, serializeErrorForLog } from '@/core/shared/clients/logger/logger' import { supabaseAdmin } from '@/core/shared/clients/supabase/admin' export type AuthUserEmailResolver = ( @@ -51,7 +52,18 @@ export async function resolveCreatorEmails< let emailByUserId: Map try { emailByUserId = await resolveEmails(creatorUserIds) - } catch { + } catch (error) { + l.warn( + { + key: 'auth_user_emails:resolve_failed', + error: serializeErrorForLog(error), + context: { + userCount: new Set(creatorUserIds).size, + }, + }, + 'Failed to resolve creator emails from Supabase Auth' + ) + return items } diff --git a/tests/unit/keys-repository.test.ts b/tests/unit/keys-repository.test.ts index 614789929..15ca35b6b 100644 --- a/tests/unit/keys-repository.test.ts +++ b/tests/unit/keys-repository.test.ts @@ -1,12 +1,21 @@ import { describe, expect, it, vi } from 'vitest' import { createKeysRepository } from '@/core/modules/keys/repository.server' +const loggerMocks = vi.hoisted(() => ({ + warn: vi.fn(), +})) + vi.mock('@/core/shared/clients/supabase/admin', () => ({ supabaseAdmin: { from: vi.fn(), }, })) +vi.mock('@/core/shared/clients/logger/logger', () => ({ + l: loggerMocks, + serializeErrorForLog: vi.fn((error: unknown) => error), +})) + function createApiResponse(input: { ok: boolean status: number @@ -116,10 +125,10 @@ describe('createKeysRepository', () => { }) it('keeps API key listing usable when creator email lookup fails', async () => { + loggerMocks.warn.mockClear() const userId = '11111111-1111-1111-1111-111111111111' - const resolveAuthUserEmailsById = vi - .fn() - .mockRejectedValue(new Error('lookup failed')) + const lookupError = new Error('lookup failed') + const resolveAuthUserEmailsById = vi.fn().mockRejectedValue(lookupError) const apiKey = { ...baseApiKey, @@ -158,5 +167,16 @@ describe('createKeysRepository', () => { ok: true, data: [apiKey], }) + + expect(loggerMocks.warn).toHaveBeenCalledWith( + { + key: 'auth_user_emails:resolve_failed', + error: lookupError, + context: { + userCount: 1, + }, + }, + 'Failed to resolve creator emails from Supabase Auth' + ) }) }) From 5077f0273c84c5474bb829433cc3e3a058dc2ed2 Mon Sep 17 00:00:00 2001 From: ben-fornefeld Date: Mon, 18 May 2026 18:04:32 -0700 Subject: [PATCH 4/4] Remove unreachable API key hydration guard Co-authored-by: Cursor --- src/core/modules/keys/repository.server.ts | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/src/core/modules/keys/repository.server.ts b/src/core/modules/keys/repository.server.ts index 724d27dea..a0f24f510 100644 --- a/src/core/modules/keys/repository.server.ts +++ b/src/core/modules/keys/repository.server.ts @@ -94,17 +94,7 @@ export function createKeysRepository( deps.resolveAuthUserEmailsById ) - if (!apiKey) { - return err( - createRepoError({ - code: 'internal', - status: 500, - message: 'Failed to create API key', - }) - ) - } - - return ok(apiKey) + return ok(apiKey ?? res.data) }, async deleteApiKey(apiKeyId) { const res = await deps.infraClient.DELETE('/api-keys/{apiKeyID}', {