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
10 changes: 9 additions & 1 deletion platform/packages/api/src/operator/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,9 +117,17 @@ operatorSessionRoutes.post('/session', async (context) => {
export const authenticatedSessionRoutes = new Hono<ApiEnv>();
authenticatedSessionRoutes.get('/session', (context) => {
const user = context.get('user');
// operator_sessions.expires_at is a timestamp without time zone whose wall
// time is UTC by convention: the POST above inserts a UTC ISO string. The
// string-mode column mapping returns it in SQL text form
// ("YYYY-MM-DD HH:MM:SS.sss"), so parse it explicitly as UTC and serialize
// with toISOString() — the response then carries the same instant and the
// same ISO text the POST returned.
const storedExpiry: string = context.get('operatorSession').expiresAt;
const expiresAt = new Date(`${storedExpiry.replace(' ', 'T')}+00:00`).toISOString();
return context.json({
apiVersion: 1,
expiresAt: context.get('operatorSession').expiresAt,
expiresAt,
user: { id: user.id, login: user.login, role: user.role },
capabilities: getPlatformCapabilities(context.env),
hostedWorkerOperations: !context.env.PLATFORM_INSTALLATION_ID,
Expand Down
50 changes: 50 additions & 0 deletions platform/packages/api/test/integration/operator-auth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,56 @@ describe('operator authentication', () => {
).toBe(401);
});

test('confirmation repeats the exact ISO UTC expiry the elevation issued', async () => {
const admin = await createTestUser(
env.db,
'operator-expiry',
'github',
undefined,
'free',
'admin',
);
const created = await request('/session', admin.jwt, {});
expect(created.status).toBe(200);
const createdBody = (await created.json()) as { token: string; expiresAt: string };

const confirmed = await request('/session', createdBody.token);
expect(confirmed.status).toBe(200);
const confirmedBody = (await confirmed.json()) as {
apiVersion: number;
expiresAt: string;
user: { id: string; login: string; role: string };
capabilities: Record<string, unknown>;
hostedWorkerOperations: unknown;
};

// The expiry round-trips through Postgres as a timestamp without time zone,
// and the string-mode column mapping reformats it as SQL text; the handler
// must serialize it back to the ISO UTC text the admin app pins. Assert the
// real conversion result, not a fixture.
const isoUtc = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
expect(createdBody.expiresAt).toMatch(isoUtc);
expect(confirmedBody.expiresAt).toBe(createdBody.expiresAt);
expect(Date.parse(confirmedBody.expiresAt)).toBe(Date.parse(createdBody.expiresAt));

// The remaining shape the admin app validates before accepting a session.
expect(confirmedBody.apiVersion).toBe(1);
expect(confirmedBody.user).toEqual({ id: admin.id, login: admin.login, role: 'admin' });
for (const name of [
'ai',
'customDomains',
'email',
'githubActions',
'githubOAuth',
'googleOAuth',
'managedBuilds',
'sandbox',
]) {
expect(typeof confirmedBody.capabilities[name]).toBe('boolean');
}
expect(typeof confirmedBody.hostedWorkerOperations).toBe('boolean');
});

test('keeps login, elevation, authenticated drain, session, and logout usable in drain maintenance', async () => {
const admin = await createTestUser(
env.db,
Expand Down
Loading