Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 30 additions & 3 deletions src/core/modules/keys/repository.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
resolveCreatorEmails,
} 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
Expand All @@ -25,6 +31,7 @@ export function createKeysRepository(
deps: KeysRepositoryDeps = {
infraClient: infra,
authHeaders: SUPABASE_AUTH_HEADERS,
resolveAuthUserEmailsById: getAuthUserEmailsById,
}
): KeysRepository {
return {
Expand All @@ -45,7 +52,12 @@ export function createKeysRepository(
)
}

return ok(res.data ?? [])
return ok(
await resolveCreatorEmails(
res.data ?? [],
deps.resolveAuthUserEmailsById
)
)
},
async createApiKey(name) {
const res = await deps.infraClient.POST('/api-keys', {
Expand All @@ -67,7 +79,22 @@ 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 resolveCreatorEmails(
[res.data],
deps.resolveAuthUserEmailsById
)

return ok(apiKey ?? res.data)
},
async deleteApiKey(apiKeyId) {
const res = await deps.infraClient.DELETE('/api-keys/{apiKeyID}', {
Expand Down
12 changes: 11 additions & 1 deletion src/core/modules/templates/repository.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
resolveCreatorEmails,
} from '@/core/modules/users/auth-user-emails.server'
import { api, infra } from '@/core/shared/clients/api'
import { repoErrorFromHttp } from '@/core/shared/errors'
import type {
Expand All @@ -20,6 +25,7 @@ type TemplatesRepositoryDeps = {
apiClient: typeof api
infraClient: typeof infra
authHeaders: typeof SUPABASE_AUTH_HEADERS
resolveAuthUserEmailsById: AuthUserEmailResolver
}

export interface TeamTemplatesRepository {
Expand All @@ -43,6 +49,7 @@ export function createTemplatesRepository(
apiClient: api,
infraClient: infra,
authHeaders: SUPABASE_AUTH_HEADERS,
resolveAuthUserEmailsById: getAuthUserEmailsById,
}
): TeamTemplatesRepository {
return {
Expand Down Expand Up @@ -74,7 +81,10 @@ export function createTemplatesRepository(
}

return ok({
templates: res.data,
templates: await resolveCreatorEmails(
res.data ?? [],
deps.resolveAuthUserEmailsById
),
})
},
async deleteTemplate(templateId) {
Expand Down
84 changes: 84 additions & 0 deletions src/core/modules/users/auth-user-emails.server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import 'server-only'

import { l, serializeErrorForLog } from '@/core/shared/clients/logger/logger'
import { supabaseAdmin } from '@/core/shared/clients/supabase/admin'

export type AuthUserEmailResolver = (
userIds: string[]
) => Promise<Map<string, string | null>>

export async function getAuthUserEmailsById(
userIds: string[]
): Promise<Map<string, string | null>> {
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 resolveCreatorEmails<
T extends {
createdBy?: { id: string; email?: string | null } | null
},
>(items: T[], resolveEmails: AuthUserEmailResolver): Promise<T[]> {
const creatorUserIds = items.flatMap((item) => {
const createdBy = item.createdBy
if (!createdBy) {
return []
}

return [createdBy.id]
})

if (creatorUserIds.length === 0) {
return items
}

let emailByUserId: Map<string, string | null>
try {
emailByUserId = await resolveEmails(creatorUserIds)
} 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
}

return items.map((item) => {
const createdBy = item.createdBy
if (!createdBy) {
return item
}

return {
...item,
createdBy: {
...createdBy,
email: emailByUserId.get(createdBy.id) ?? null,
Comment thread
ben-fornefeld marked this conversation as resolved.
},
}
})
}
182 changes: 182 additions & 0 deletions tests/unit/keys-repository.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
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<T>(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 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(
createApiResponse({
ok: true,
status: 200,
data: [
{
...baseApiKey,
id: 'first-key',
createdBy: {
id: firstUserId,
email: null,
},
},
{
...baseApiKey,
id: 'second-key',
createdBy: {
id: secondUserId,
email: 'deprecated-response-value@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([
firstUserId,
secondUserId,
])
expect(result).toEqual({
ok: true,
data: [
expect.objectContaining({
id: 'first-key',
createdBy: {
id: firstUserId,
email: 'first@e2b.dev',
},
}),
expect.objectContaining({
id: 'second-key',
createdBy: {
id: secondUserId,
email: 'second@e2b.dev',
},
}),
],
})
})

it('keeps API key listing usable when creator email lookup fails', async () => {
loggerMocks.warn.mockClear()
const userId = '11111111-1111-1111-1111-111111111111'
const lookupError = new Error('lookup failed')
const resolveAuthUserEmailsById = vi.fn().mockRejectedValue(lookupError)

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],
})

expect(loggerMocks.warn).toHaveBeenCalledWith(
{
key: 'auth_user_emails:resolve_failed',
error: lookupError,
context: {
userCount: 1,
},
},
'Failed to resolve creator emails from Supabase Auth'
)
})
})
Loading