Initial commit

This commit is contained in:
2025-07-25 19:00:06 -04:00
commit e604c7a437
52 changed files with 96098 additions and 0 deletions

View File

@@ -0,0 +1,7 @@
import { createHonoRoute, routeImporter } from "../../util/import.ts";
export const route = createHonoRoute('/accounts');
await routeImporter(route.app, 'src/routes/accounts/', [
'routes'
]);

View File

@@ -0,0 +1,38 @@
import { createHonoRoute } from "../../../util/import.ts";
import { authenticate } from "../../../util/api.ts";
import Server from "../../../server/server.ts";
import z from "zod";
import { typedZValidator } from "../../../util/validators.ts";
export const route = createHonoRoute('/account');
const transformNumber = (arg: string, ctx: z.RefinementCtx<string>) => {
const int = parseInt(arg);
if (isNaN(int) || !Number.isSafeInteger(int)) ctx.addIssue('Number is not valid');
else return int;
}
const bulkAccountQuerySchema = z.object({
id: z.union([ z.string().transform(transformNumber), z.array(z.string().transform(transformNumber)) ])
});
route.app.get('/bulk', typedZValidator('query', bulkAccountQuerySchema), async c => {
const { id } = c.req.valid('query');
let ids: number[] = [];
if (!id) return c.json([]);
if (typeof id == 'number') ids = [id];
else ids = id.filter(val => typeof val == 'number');
return c.json((await Promise.all(ids.map(id => Server.Profiles.get(id))))
.filter(val => val !== null)
.map(prof => prof.export())
.filter(val => val !== null)
);
});
route.app.use('*', authenticate);
route.app.get('/me', c => {
const profile = c.get('profile');
return c.json(profile.selfExport());
});

7
src/routes/api/root.ts Normal file
View File

@@ -0,0 +1,7 @@
import { createHonoRoute, routeImporter } from "../../util/import.ts";
export const route = createHonoRoute('/api');
await routeImporter(route.app, 'src/routes/api/', [
'routes'
]);

View File

@@ -0,0 +1,7 @@
import { createHonoRoute } from "../../../util/import.ts";
export const route = createHonoRoute("/avatar");
route.app.get('/v1/defaultunlocked', c => {
return c.json([]);
});

View File

@@ -0,0 +1,7 @@
import { createHonoRoute } from "../../../util/import.ts";
export const route = createHonoRoute("/config");
route.app.get('/v1/amplitude', c => {
return c.json({AmplitudeKey: ""});
});

View File

@@ -0,0 +1,9 @@
import Server from "../../../server/server.ts";
import { createHonoRoute } from "../../../util/import.ts";
export const route = createHonoRoute('/gameconfigs');
route.app.get('/v1/all', async c => {
const configs = await Server.GameConfigs.getAllGameConfigs();
return c.json(configs);
});

View File

@@ -0,0 +1,6 @@
import { successResponse } from "../../../util/api.ts";
import { createHonoRoute } from "../../../util/import.ts";
export const route = createHonoRoute('/gamesight');
route.app.post('/event', successResponse(true, ""));

View File

@@ -0,0 +1,29 @@
import { z } from "zod";
import { createHonoRoute } from "../../../util/import.ts";
import { typedZValidator } from "../../../util/validators.ts";
export const route = createHonoRoute("/versioncheck");
const versionCheckSchema = z.object({
v: z.string(),
p: z.string().transform(Number),
});
enum VersionStatus {
ValidForPlay,
ValidForMenu,
UpdateRequired
}
export const gameVerString = '20220118';
route.app.get('/v4', typedZValidator('query', versionCheckSchema), c => {
const { v, p } = c.req.valid('query');
if (v === gameVerString && p == 0) return c.json({
VersionStatus: VersionStatus.ValidForPlay
});
else return c.json({
VersionStatus: VersionStatus.UpdateRequired
});
});

7
src/routes/auth/root.ts Normal file
View File

@@ -0,0 +1,7 @@
import { createHonoRoute, routeImporter } from "../../util/import.ts";
export const route = createHonoRoute('/auth');
await routeImporter(route.app, 'src/routes/auth/', [
'routes'
]);

View File

@@ -0,0 +1,18 @@
import z from "zod";
import { createHonoRoute } from "../../../util/import.ts";
import { transformStringToEnum, typedZValidator } from "../../../util/validators.ts";
import { PlatformType } from "../../../server/platforms/base.ts";
import Server from "../../../server/server.ts";
export const route = createHonoRoute("/cachedlogin");
const cachedLoginFetchParamSchema = z.object({
platformType: z.string().transform(transformStringToEnum<PlatformType>(PlatformType)),
platformId: z.string().min(4)
});
route.app.get('/forplatformid/:platformType/:platformId', typedZValidator('param', cachedLoginFetchParamSchema), async c => {
const { platformType, platformId } = c.req.valid('param');
return c.json((await Server.Platforms.getCachedLogins(platformType || PlatformType.Steam, platformId, true)) || []);
});

View File

@@ -0,0 +1,128 @@
import { createHonoRoute } from "../../../util/import.ts";
import z from "zod";
import { transformStringToEnum, typedZValidator } from "../../../util/validators.ts";
import { DeviceClass, PlatformType, steamAuthTicketSchema } from "../../../server/platforms/base.ts";
import { gameVerString } from "../../api/routes/versioncheck.ts";
import Steam from "../../../util/steam/steam.ts";
import { SteamAuthResult } from "../../../util/steam/SteamAuthTypes.ts";
import Server from "../../../server/server.ts";
import Logging from "@proxnet/undead-logging";
import { verify } from "@hono/hono/jwt";
const log = new Logging("ConnectRouteDebug");
export const route = createHonoRoute("/connect");
const authBodyBaseSchema = z.object({
client_id: z.literal("recroom"),
platform: z.string().transform(Number).transform((arg, ctx) => { // we only support steam right now
if (arg !== PlatformType.Steam) ctx.addIssue("platform was not Steam");
else return PlatformType.Steam;
}),
platform_id: z.string().min(4),
device_id: z.string().min(4),
device_class: z.string().transform(transformStringToEnum<DeviceClass>(DeviceClass)),
time: z.string().transform(Date),
ver: z.literal(gameVerString),
build_key: z.string().min(4),
asid: z.string().transform(Number),
eac_challenge: z.literal("who said it"),
eac_response: z.literal("who_said_it"),
platform_auth: z.string().transform((arg, ctx) => {
try {
const parsed = steamAuthTicketSchema.safeParse(JSON.parse(arg))
if (parsed.success) return parsed.data.Ticket;
else ctx.addIssue("Steam Auth Ticket could not be parsed")
} catch {
ctx.addIssue("Steam Auth Ticket could not be parsed");
}
})
});
const createAccountGrantSchema = authBodyBaseSchema.extend({
grant_type: z.literal("create_account")
});
const cachedLoginGrantSchema = authBodyBaseSchema.extend({
grant_type: z.literal('cached_login'),
account_id: z.string().transform(Number),
});
const refreshTokenGrantSchema = authBodyBaseSchema.extend({
grant_type: z.literal('refresh_token'),
refresh_token: z.string(),
});
const tokenGrantSchema = z.discriminatedUnion('grant_type', [
createAccountGrantSchema,
cachedLoginGrantSchema,
refreshTokenGrantSchema
]);
enum TokenRequestError {
InvalidRequest = "invalid_request",
InvalidGrant = "invalid_grant",
InvalidClient = "invalid_client",
InvalidUsernameOrPassword = "invalid_username_or_password",
InvalidTime = "invalid time",
InvalidPlatform = "invalid platform",
AccessDenied = "access_denied",
SlowDown = "slow_down",
PlatformVerificationFailed = "platform verification failed"
}
route.app.post('/token', typedZValidator('form', tokenGrantSchema), async c => {
function error(error?: TokenRequestError, desc?: string) {
return c.json({ error, error_description: desc });
}
const form = c.req.valid('form');
if (typeof form.platform_auth == 'undefined' || typeof form.platform == 'undefined') return error(TokenRequestError.InvalidPlatform);
const { valid } = await Steam.AuthenticateUserTicket(form.platform_auth, form.platform_id);
if (valid == SteamAuthResult.Failure) return error(TokenRequestError.PlatformVerificationFailed);
if (Math.abs(Date.now() - new Date(form.time).getTime()) > 3_600_000) return error(TokenRequestError.InvalidTime);
const logins = await Server.Platforms.getCachedLogins(form.platform, form.platform_id, false);
if (form.grant_type == 'create_account' && logins && logins.length > 0) return error(TokenRequestError.InvalidRequest);
else if (form.grant_type == 'create_account') {
const profile = await Server.Profiles.create();
if (!profile) return error(TokenRequestError.AccessDenied);
await Server.Platforms.addCachedLogin(form.platform, form.platform_id, profile?.getId());
const token = await Server.Platforms.getToken(profile.getId(), await profile.getRole() || 'user');
return c.json({
access_token: token,
refresh_token: token,
key: "aHVo"
});
} else if (form.grant_type == 'refresh_token') {
const secret = Deno.env.get('SECRET');
if (!secret) {
log.w(`Secret not set!`);
return error(TokenRequestError.InvalidRequest);
}
try {
await verify(form.refresh_token, secret);
return c.json({
access_token: form.refresh_token,
refresh_token: form.refresh_token,
key: "aHVo"
});
} catch (err) {
log.w(`Authentication error (token req): ${(err as Error).stack}`);
return error(TokenRequestError.InvalidRequest);
}
}
if (logins && logins.find(login => login.accountId === form.account_id)) {
const profile = await Server.Profiles.get(form.account_id);
if (!profile) return error(TokenRequestError.InvalidRequest, "No such profile");
const token = await Server.Platforms.getToken(profile.getId(), await profile.getRole() || 'user');
return c.json({
access_token: token,
refresh_token: token,
key: "aHVo"
});
} else return error(TokenRequestError.InvalidRequest, "No such profile");
});

View File

@@ -0,0 +1,7 @@
import { createHonoRoute } from "../../../util/import.ts";
export const route = createHonoRoute("/eac");
route.app.get('/challenge', c => {
return c.text(`"who said it"`);
});

31
src/routes/root/root.ts Normal file
View File

@@ -0,0 +1,31 @@
import { createHonoRoute } from "../../util/import.ts";
export const route = createHonoRoute('/');
route.app.get('/', async (c, next) => {
if (c.req.query('v') == '2') return c.json({
Accounts: "https://wsi.proxnet.dev/accounts",
API: "https://wsi.proxnet.dev/",
Auth: "https://wsi.proxnet.dev/auth",
BugReporting: "https://wsi.proxnet.dev/bugs",
CDN: "https://wsi.proxnet.dev/cdn",
Chat: "https://wsi.proxnet.dev/chat",
Clubs: "https://wsi.proxnet.dev/clubs",
Commerce: "https://wsi.proxnet.dev/commerce",
DataCollection: "https://wsi.proxnet.dev/datacol",
Discovery: "https://wsi.proxnet.dev/disc",
Images: "https://wsi.proxnet.dev/img",
Leaderboard: "https://wsi.proxnet.dev/leaderboard",
Link: "https://wsi.proxnet.dev/link",
Matchmaking: "https://wsi.proxnet.dev/match",
Moderation: "https://wsi.proxnet.dev/mod",
Notifications: "https://wsi.proxnet.dev/notify",
PlatformNotifications: "https://wsi.proxnet.dev/platnotify",
PlayerSettings: "https://wsi.proxnet.dev/plsettings",
RoomComments: "https://wsi.proxnet.dev/roomcomments",
Rooms: "https://wsi.proxnet.dev/rooms",
Storage: "https://wsi.proxnet.dev/storage",
WWW: "https://wsi.proxnet.dev/www",
});
return await next();
});