fix(tokens): issue machine-ready defaults

This commit is contained in:
root 2026-03-15 06:19:48 +00:00
parent eac633e1e7
commit d1b3faae8b
4 changed files with 82 additions and 7 deletions

View File

@ -57,6 +57,7 @@ describe('tokenRoutes', () => {
afterEach(() => {
vi.restoreAllMocks();
delete process.env.PLATFORM_RUNTIME_ENV;
});
it('GET /tokens returns 401 when unauthenticated', async () => {
@ -121,13 +122,70 @@ describe('tokenRoutes', () => {
expect(data.rawToken.startsWith('wai_')).toBe(true);
expect(repoMock.create).toHaveBeenCalledWith(
expect.objectContaining({
tokenType: 'user_api',
environment: 'prod',
tokenType: 'product_api',
environment: 'dev',
})
);
expect(repoMock.hashToken).toHaveBeenCalled();
});
it('POST /tokens respects explicit environment overrides', async () => {
repoMock.hashToken.mockResolvedValue('hashed_token');
repoMock.create.mockResolvedValue(baseToken);
const app = await buildApp({
sub: 'admin_1',
productId: 'lysnrai',
role: 'admin',
email: 'admin@example.com',
});
const res = await app.inject({
method: 'POST',
url: '/api/tokens',
payload: {
name: 'staging-token',
tokenType: 'service_api',
environment: 'staging',
scopes: ['jobs:read'],
expiresInDays: 30,
},
});
expect(res.statusCode).toBe(201);
expect(repoMock.create).toHaveBeenCalledWith(
expect.objectContaining({
tokenType: 'service_api',
environment: 'staging',
})
);
});
it('POST /tokens defaults environment from runtime configuration', async () => {
process.env.PLATFORM_RUNTIME_ENV = 'staging';
repoMock.hashToken.mockResolvedValue('hashed_token');
repoMock.create.mockResolvedValue(baseToken);
const app = await buildApp({
sub: 'admin_1',
productId: 'lysnrai',
role: 'admin',
email: 'admin@example.com',
});
const res = await app.inject({
method: 'POST',
url: '/api/tokens',
payload: { name: 'runtime-default-token', scopes: ['jobs:read'], expiresInDays: 30 },
});
expect(res.statusCode).toBe(201);
expect(repoMock.create).toHaveBeenCalledWith(
expect.objectContaining({
tokenType: 'product_api',
environment: 'staging',
})
);
});
it('GET /tokens/count returns count for admin', async () => {
repoMock.countActive.mockResolvedValue(4);
const app = await buildApp({ sub: 'admin_1', productId: 'lysnrai', role: 'admin' });

View File

@ -14,6 +14,22 @@ import { BadRequestError, ForbiddenError, UnauthorizedError } from '../../lib/er
import { CreateTokenSchema, PatchTokenSchema, type ApiTokenDoc } from './types.js';
import * as repo from './repository.js';
function getRuntimeEnvironment(): 'dev' | 'staging' | 'prod' {
const explicit = process.env.PLATFORM_RUNTIME_ENV;
if (explicit === 'dev' || explicit === 'staging' || explicit === 'prod') {
return explicit;
}
switch (process.env.NODE_ENV) {
case 'production':
return 'prod';
case 'test':
return 'dev';
default:
return 'dev';
}
}
export async function tokenRoutes(app: FastifyInstance) {
function requireAuth(req: import('fastify').FastifyRequest) {
const payload = req.jwtPayload;
@ -51,6 +67,7 @@ export async function tokenRoutes(app: FastifyInstance) {
throw new BadRequestError(parsed.error.issues.map(i => i.message).join('; '));
}
const { name, tokenType, environment, scopes, expiresInDays } = parsed.data;
const resolvedEnvironment = environment ?? getRuntimeEnvironment();
const rawToken = `wai_${crypto.randomBytes(32).toString('hex')}`;
const prefix = rawToken.slice(0, 12);
@ -65,7 +82,7 @@ export async function tokenRoutes(app: FastifyInstance) {
userName: payload.email ?? '',
name,
tokenType,
environment,
environment: resolvedEnvironment,
prefix,
tokenHash,
status: 'active',

View File

@ -12,8 +12,8 @@ describe('CreateTokenSchema', () => {
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.name).toBe('CI Pipeline');
expect(result.data.tokenType).toBe('user_api');
expect(result.data.environment).toBe('prod');
expect(result.data.tokenType).toBe('product_api');
expect(result.data.environment).toBeUndefined();
expect(result.data.scopes).toEqual(['read']);
expect(result.data.expiresInDays).toBe(90);
}

View File

@ -26,8 +26,8 @@ export type ApiTokenResponse = Omit<ApiTokenDoc, 'tokenHash'>;
export const CreateTokenSchema = z.object({
name: z.string().min(1),
tokenType: z.enum(['user_api', 'product_api', 'service_api']).default('user_api'),
environment: z.enum(['dev', 'staging', 'prod']).default('prod'),
tokenType: z.enum(['user_api', 'product_api', 'service_api']).default('product_api'),
environment: z.enum(['dev', 'staging', 'prod']).optional(),
scopes: z.array(z.string()).default(['read']),
expiresInDays: z.number().int().min(1).max(365).default(90),
});