Files
netris-nestri/apps/api/app/index.ts
T
Wanjohi 543fd3f361 fix(steam): link a Steam account only through Steam's own sign-in
POST /steam/link took a Steam id from the request body and linked it. A
Steam id is public, so anyone could claim any account, its library, and
the one free weekend that is keyed on it.

Linking is now POST /steam/link/start, which returns a Steam sign-in URL
tied to a one-time, ten-minute request, and GET /steam/link/callback,
where the browser comes back. The callback accepts the id only if Steam
confirms the assertion, the assertion was issued for exactly this
callback, and the id is read from Steam's claimed identity URL in the
one shape Steam issues it. The request is spent on use.
2026-10-07 10:52:19 +03:00

145 lines
4.3 KiB
TypeScript

import type { Hyperdrive } from '@cloudflare/workers-types';
import { Env } from '@nestri/core/env';
import { ErrorCodes, VisibleError } from '@nestri/core/error';
import { Hono } from 'hono';
import { openAPISpecs } from 'hono-openapi';
import { cors } from 'hono/cors';
import { HTTPException } from 'hono/http-exception';
import { logger } from 'hono/logger';
import { type ContentfulStatusCode } from 'hono/utils/http-status';
import { auth } from './middleware/auth.js';
import { AccessTokenApi } from './routes/access-token.js';
import { BillingApi } from './routes/billing.js';
import { BoxApi } from './routes/box.js';
import { EnrolmentApi } from './routes/enrolment.js';
import { GameApi } from './routes/game.js';
import { IndexApi } from './routes/index.js';
import { InstallApi } from './routes/install.js';
import { LibraryApi } from './routes/library.js';
import { MachineApi } from './routes/machine.js';
import { OrganisationApi } from './routes/organisation.js';
import { SessionApi } from './routes/session.js';
import { SteamApi } from './routes/steam.js';
import { TeamApi } from './routes/team.js';
import { UserApi } from './routes/user.js';
import { WaitlistApi } from './routes/waitlist.js';
export const app = new Hono();
app
.use(logger())
.use(async (c, next) => {
c.header('Cache-Control', 'no-store');
return next();
})
.use(
cors({
origin: () => 'http://localhost:5173',
credentials: true
})
)
.use(auth);
const routes = app
.route('/', IndexApi.route)
.route('/', InstallApi.route)
.route('/user', UserApi.route)
.route('/steam', SteamApi.publicRoute)
.route('/steam', SteamApi.route)
.route('/library', LibraryApi.route)
.route('/games', GameApi.route)
.route('/billing', BillingApi.route)
.route('/organisation', OrganisationApi.route)
.route('/machine', MachineApi.route)
.route('/machine', SessionApi.machineRoute)
.route('/machine', EnrolmentApi.route)
.route('/team', TeamApi.route)
.route('/box', BoxApi.route)
.route('/session', SessionApi.route)
.route('/access-token', AccessTokenApi.route)
.route('/waitlist', WaitlistApi.route)
.onError((error, c) => {
if (error instanceof VisibleError) {
// eslint-disable-next-line no-console
console.error('api error:', error);
return c.json(error.toResponse(), error.statusCode() as ContentfulStatusCode);
}
if (error instanceof HTTPException) {
// eslint-disable-next-line no-console
console.error('http error:', error);
return c.json(
{
type: 'validation',
code: ErrorCodes.Validation.INVALID_PARAMETER,
message: 'Invalid request'
},
error.status
);
}
// eslint-disable-next-line no-console
console.error('unhandled error:', error);
return c.json(
{
type: 'internal',
code: ErrorCodes.Server.INTERNAL_ERROR,
message: 'Internal server error'
},
500
);
});
app.get(
'/doc',
openAPISpecs(routes, {
documentation: {
info: {
title: 'Nestri API',
description: 'API',
version: '0.0.1'
},
components: {
securitySchemes: {
Bearer: {
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT'
}
}
},
security: [{ Bearer: [] }]
}
})
);
/**
* Everything this app is handed, from a binding or from the environment.
*
* Two things here arrive one of two ways, and neither is a special case.
* `HYPERDRIVE` carries a connection string on a platform that pools
* connections for us, and `DATABASE_URL` says the same thing where nothing
* does. An `AUTH` binding is a route to the issuer that skips the internet,
* and `AUTH_INTERNAL_URL` is that route written out. Each pair is two
* spellings of one fact rather than two deployments, which is why nothing
* below branches on the runtime it is under.
*
* `AUTH_ISSUER_URL` is not part of either pair. It is the issuer's public
* *name*, it is required, and it is the same value however the issuer is
* reached — because it is what every token's `iss` claim is checked against.
*/
export type ApiEnv = {
AUTH?: { fetch: typeof fetch };
AUTH_ISSUER_URL?: string;
AUTH_INTERNAL_URL?: string;
HYPERDRIVE?: Hyperdrive;
DATABASE_URL?: string;
};
export default {
fetch(request: Request, env: ApiEnv, ctx?: ExecutionContext) {
Env.init(env as unknown as Record<string, unknown>);
return app.fetch(request, env, ctx);
}
};