chore: import upstream snapshot with attribution
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
/**
|
||||
* @vitest-environment node
|
||||
*
|
||||
* Regression test: the credentials response must expose only display metadata,
|
||||
* never the connected account's OAuth access/refresh token.
|
||||
*/
|
||||
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const SECRET_ACCESS_TOKEN = 'ya29.a0SECRET_GOOGLE_BEARER_TOKEN_DO_NOT_LEAK'
|
||||
|
||||
const { selectMock, getAllOAuthServicesMock, getPersonalAndWorkspaceEnvMock, decodeJwtMock } =
|
||||
vi.hoisted(() => ({
|
||||
selectMock: vi.fn(),
|
||||
getAllOAuthServicesMock: vi.fn(),
|
||||
getPersonalAndWorkspaceEnvMock: vi.fn(),
|
||||
decodeJwtMock: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@sim/db', () => ({
|
||||
db: { select: selectMock },
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/oauth', () => ({
|
||||
getAllOAuthServices: getAllOAuthServicesMock,
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/environment/utils', () => ({
|
||||
getPersonalAndWorkspaceEnv: getPersonalAndWorkspaceEnvMock,
|
||||
}))
|
||||
|
||||
vi.mock('jose', () => ({
|
||||
decodeJwt: decodeJwtMock,
|
||||
}))
|
||||
|
||||
import { getCredentialsServerTool } from './get-credentials'
|
||||
|
||||
/**
|
||||
* Wires the two sequential `db.select()` reads the tool performs:
|
||||
* 1. `select().from(account).where()` → account rows (awaited directly)
|
||||
* 2. `select({...}).from(user).where().limit(1)` → user row
|
||||
*/
|
||||
function wireDb(accountRows: unknown[], userRows: Array<{ email: string }>) {
|
||||
const whereThenable = {
|
||||
then: (resolve: (rows: unknown[]) => unknown) => resolve(accountRows),
|
||||
limit: () => Promise.resolve(userRows),
|
||||
}
|
||||
const builder = { from: () => builder, where: () => whereThenable }
|
||||
selectMock.mockReturnValue(builder)
|
||||
}
|
||||
|
||||
describe('getCredentialsServerTool', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
|
||||
wireDb(
|
||||
[
|
||||
{
|
||||
id: 'acct-google-1',
|
||||
providerId: 'google-default',
|
||||
accountId: '1234567890',
|
||||
idToken: 'jwt-token',
|
||||
accessToken: SECRET_ACCESS_TOKEN,
|
||||
refreshToken: 'refresh-secret',
|
||||
updatedAt: new Date('2026-04-17T02:26:05.546Z'),
|
||||
},
|
||||
],
|
||||
[{ email: 'brent@cellular.so' }]
|
||||
)
|
||||
|
||||
getAllOAuthServicesMock.mockReturnValue([
|
||||
{
|
||||
providerId: 'google-default',
|
||||
name: 'Google',
|
||||
description: 'Google account',
|
||||
baseProvider: 'google',
|
||||
},
|
||||
{
|
||||
providerId: 'slack',
|
||||
name: 'Slack',
|
||||
description: 'Slack workspace',
|
||||
baseProvider: 'slack',
|
||||
},
|
||||
])
|
||||
|
||||
getPersonalAndWorkspaceEnvMock.mockResolvedValue({
|
||||
personalEncrypted: {},
|
||||
workspaceEncrypted: {},
|
||||
conflicts: [],
|
||||
})
|
||||
|
||||
decodeJwtMock.mockReturnValue({ email: 'brent@cellular.so' })
|
||||
})
|
||||
|
||||
it('never returns access tokens for connected OAuth credentials', async () => {
|
||||
const result = await getCredentialsServerTool.execute({}, { userId: 'user-1' })
|
||||
|
||||
const credentials = result.oauth.connected.credentials
|
||||
expect(credentials).toHaveLength(1)
|
||||
|
||||
for (const credential of credentials) {
|
||||
expect(credential).not.toHaveProperty('accessToken')
|
||||
expect(credential).not.toHaveProperty('refreshToken')
|
||||
expect(credential).not.toHaveProperty('idToken')
|
||||
}
|
||||
})
|
||||
|
||||
it('returns only masked display metadata for each credential', async () => {
|
||||
const result = await getCredentialsServerTool.execute({}, { userId: 'user-1' })
|
||||
|
||||
expect(result.oauth.connected.credentials[0]).toEqual({
|
||||
id: 'acct-google-1',
|
||||
name: 'brent@cellular.so',
|
||||
provider: 'google-default',
|
||||
serviceName: 'Google',
|
||||
lastUsed: '2026-04-17T02:26:05.546Z',
|
||||
isDefault: true,
|
||||
})
|
||||
})
|
||||
|
||||
it('does not leak the token value anywhere in the serialized response', async () => {
|
||||
const result = await getCredentialsServerTool.execute({}, { userId: 'user-1' })
|
||||
|
||||
expect(JSON.stringify(result)).not.toContain(SECRET_ACCESS_TOKEN)
|
||||
expect(JSON.stringify(result)).not.toContain('refresh-secret')
|
||||
})
|
||||
|
||||
it('rejects unauthenticated callers without touching the database', async () => {
|
||||
await expect(getCredentialsServerTool.execute({}, undefined)).rejects.toThrow(
|
||||
'Authentication required'
|
||||
)
|
||||
expect(selectMock).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user