fix(tokens): issue machine-ready defaults
This commit is contained in:
parent
eac633e1e7
commit
d1b3faae8b
@ -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' });
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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),
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user