rate limit

This commit is contained in:
evanpelle
2025-12-23 10:40:32 -08:00
parent a810e0ad34
commit ea833a3c2a
10 changed files with 59 additions and 51 deletions
+1
View File
@@ -108,6 +108,7 @@ jobs:
ADMIN_TOKEN: ${{ secrets.ADMIN_TOKEN }}
CF_ACCOUNT_ID: ${{ secrets.CF_ACCOUNT_ID }}
CF_API_TOKEN: ${{ secrets.CF_API_TOKEN }}
CF_RATE_LIMIT_BYPASS_TOKEN: ${{ secrets.CF_RATE_LIMIT_BYPASS_TOKEN }}
DOCKER_REPO: ${{ vars.DOCKERHUB_REPO }}
DOCKER_USERNAME: ${{ vars.DOCKERHUB_USERNAME }}
ENV: ${{ inputs.target_domain == 'openfront.io' && 'prod' || 'staging' }}
+4
View File
@@ -66,6 +66,7 @@ jobs:
ADMIN_TOKEN: ${{ secrets.ADMIN_TOKEN }}
CF_ACCOUNT_ID: ${{ secrets.CF_ACCOUNT_ID }}
CF_API_TOKEN: ${{ secrets.CF_API_TOKEN }}
CF_RATE_LIMIT_BYPASS_TOKEN: ${{ secrets.CF_RATE_LIMIT_BYPASS_TOKEN }}
DOCKER_REPO: openfront-prod
DOCKER_USERNAME: ${{ vars.DOCKERHUB_USERNAME }}
DOMAIN: ${{ vars.DOMAIN }}
@@ -124,6 +125,7 @@ jobs:
ADMIN_TOKEN: ${{ secrets.ADMIN_TOKEN }}
CF_ACCOUNT_ID: ${{ secrets.CF_ACCOUNT_ID }}
CF_API_TOKEN: ${{ secrets.CF_API_TOKEN }}
CF_RATE_LIMIT_BYPASS_TOKEN: ${{ secrets.CF_RATE_LIMIT_BYPASS_TOKEN }}
DOCKER_REPO: ${{ vars.DOCKERHUB_REPO }}
DOCKER_USERNAME: ${{ vars.DOCKERHUB_USERNAME }}
DOMAIN: ${{ vars.DOMAIN }}
@@ -182,6 +184,7 @@ jobs:
ADMIN_TOKEN: ${{ secrets.ADMIN_TOKEN }}
CF_ACCOUNT_ID: ${{ secrets.CF_ACCOUNT_ID }}
CF_API_TOKEN: ${{ secrets.CF_API_TOKEN }}
CF_RATE_LIMIT_BYPASS_TOKEN: ${{ secrets.CF_RATE_LIMIT_BYPASS_TOKEN }}
DOCKER_REPO: ${{ vars.DOCKERHUB_REPO }}
DOCKER_USERNAME: ${{ vars.DOCKERHUB_USERNAME }}
DOMAIN: ${{ vars.DOMAIN }}
@@ -240,6 +243,7 @@ jobs:
ADMIN_TOKEN: ${{ secrets.ADMIN_TOKEN }}
CF_ACCOUNT_ID: ${{ secrets.CF_ACCOUNT_ID }}
CF_API_TOKEN: ${{ secrets.CF_API_TOKEN }}
CF_RATE_LIMIT_BYPASS_TOKEN: ${{ secrets.CF_RATE_LIMIT_BYPASS_TOKEN }}
DOCKER_REPO: ${{ vars.DOCKERHUB_REPO }}
DOCKER_USERNAME: ${{ vars.DOCKERHUB_USERNAME }}
DOMAIN: ${{ vars.DOMAIN }}
+1
View File
@@ -176,6 +176,7 @@ R2_ACCESS_KEY=$R2_ACCESS_KEY
R2_SECRET_KEY=$R2_SECRET_KEY
R2_BUCKET=$R2_BUCKET
CF_API_TOKEN=$CF_API_TOKEN
CF_RATE_LIMIT_BYPASS_TOKEN=$CF_RATE_LIMIT_BYPASS_TOKEN
TURNSTILE_SECRET_KEY=$TURNSTILE_SECRET_KEY
API_KEY=$API_KEY
DOMAIN=$DOMAIN
+1
View File
@@ -12,6 +12,7 @@ ADMIN_TOKEN=your_admin_token_here
# Cloudflare Configuration
CF_ACCOUNT_ID=your_cloudflare_account_id
CF_API_TOKEN=your_cloudflare_api_token
CF_RATE_LIMIT_BYPASS_TOKEN=your_rate_limit_bypass_token
DOMAIN=your-domain.com
# R2 Configuration
+1
View File
@@ -61,6 +61,7 @@ export interface ServerConfig {
subdomain(): string;
cloudflareAccountId(): string;
cloudflareApiToken(): string;
cloudflareRateLimitBypassToken(): string;
cloudflareConfigPath(): string;
cloudflareCredsPath(): string;
stripePublishableKey(): string;
+3
View File
@@ -108,6 +108,9 @@ export abstract class DefaultServerConfig implements ServerConfig {
cloudflareApiToken(): string {
return process.env.CF_API_TOKEN ?? "";
}
cloudflareRateLimitBypassToken(): string {
return process.env.CF_RATE_LIMIT_BYPASS_TOKEN ?? "";
}
cloudflareConfigPath(): string {
return process.env.CF_CONFIG_PATH ?? "";
}
+43 -5
View File
@@ -1,4 +1,6 @@
import z from "zod";
import { z } from "zod/v4/classic/external.cjs";
import { UserMeResponse, UserMeResponseSchema } from "../core/ApiSchemas";
import { ServerConfig } from "../core/configuration/Config";
import { getServerConfigFromServer } from "../core/configuration/ConfigLoader";
import {
GameID,
@@ -11,9 +13,45 @@ import { replacer } from "../core/Util";
import { logger } from "./Logger";
const config = getServerConfigFromServer();
const log = logger.child({ component: "Api" });
const log = logger.child({ component: "Archive" });
export async function getUserMe(
token: string,
config: ServerConfig,
): Promise<
| { type: "success"; response: UserMeResponse }
| { type: "error"; message: string }
> {
try {
// Get the user object
const response = await fetch(config.jwtIssuer() + "/users/@me", {
headers: {
authorization: `Bearer ${token}`,
"x-service-bypass": config.cloudflareRateLimitBypassToken(),
},
});
if (response.status !== 200) {
return {
type: "error",
message: `Failed to fetch user me: ${response.statusText}`,
};
}
const body = await response.json();
const result = UserMeResponseSchema.safeParse(body);
if (!result.success) {
return {
type: "error",
message: `Invalid response: ${z.prettifyError(result.error)}`,
};
}
return { type: "success", response: result.data };
} catch (e) {
return {
type: "error",
message: `Failed to fetch user me: ${e}`,
};
}
}
export async function archive(gameRecord: GameRecord) {
try {
const parsed = GameRecordSchema.safeParse(gameRecord);
@@ -30,6 +68,7 @@ export async function archive(gameRecord: GameRecord) {
headers: {
"Content-Type": "application/json",
"x-api-key": config.apiKey(),
"x-service-bypass": config.cloudflareRateLimitBypassToken(),
},
});
if (!response.ok) {
@@ -45,7 +84,6 @@ export async function archive(gameRecord: GameRecord) {
return;
}
}
export async function readGameRecord(
gameId: GameID,
): Promise<GameRecord | null> {
@@ -59,6 +97,7 @@ export async function readGameRecord(
method: "GET",
headers: {
"Content-Type": "application/json",
"x-service-bypass": config.cloudflareRateLimitBypassToken(),
},
});
const record = await response.json();
@@ -76,7 +115,6 @@ export async function readGameRecord(
return null;
}
}
export function finalizeGameRecord(
clientRecord: PartialGameRecord,
): GameRecord {
+2 -43
View File
@@ -1,12 +1,8 @@
import { jwtVerify } from "jose";
import { z } from "zod";
import {
TokenPayload,
TokenPayloadSchema,
UserMeResponse,
UserMeResponseSchema,
} from "../core/ApiSchemas";
import { TokenPayload, TokenPayloadSchema } from "../core/ApiSchemas";
import { GameEnv, ServerConfig } from "../core/configuration/Config";
import { getServerConfigFromServer } from "../core/configuration/ConfigLoader";
import { PersistentIdSchema } from "../core/Schemas";
type TokenVerificationResult =
@@ -61,40 +57,3 @@ export async function verifyClientToken(
return { type: "error", message };
}
}
export async function getUserMe(
token: string,
config: ServerConfig,
): Promise<
| { type: "success"; response: UserMeResponse }
| { type: "error"; message: string }
> {
try {
// Get the user object
const response = await fetch(config.jwtIssuer() + "/users/@me", {
headers: {
authorization: `Bearer ${token}`,
},
});
if (response.status !== 200) {
return {
type: "error",
message: `Failed to fetch user me: ${response.statusText}`,
};
}
const body = await response.json();
const result = UserMeResponseSchema.safeParse(body);
if (!result.success) {
return {
type: "error",
message: `Invalid response: ${z.prettifyError(result.error)}`,
};
}
return { type: "success", response: result.data };
} catch (e) {
return {
type: "error",
message: `Failed to fetch user me: ${e}`,
};
}
}
+1 -1
View File
@@ -23,7 +23,7 @@ import {
Turn,
} from "../core/Schemas";
import { createPartialGameRecord, getClanTag } from "../core/Util";
import { archive, finalizeGameRecord } from "./Archive";
import { archive, finalizeGameRecord } from "./Api";
import { Client } from "./Client";
export enum GamePhase {
Lobby = "LOBBY",
+2 -2
View File
@@ -18,10 +18,10 @@ import {
} from "../core/Schemas";
import { generateID, replacer } from "../core/Util";
import { CreateGameInputSchema, GameInputSchema } from "../core/WorkerSchemas";
import { archive, finalizeGameRecord } from "./Archive";
import { archive, finalizeGameRecord, getUserMe } from "./Api";
import { verifyClientToken } from "./Auth";
import { Client } from "./Client";
import { GameManager } from "./GameManager";
import { getUserMe, verifyClientToken } from "./jwt";
import { logger } from "./Logger";
import { GameEnv } from "../core/configuration/Config";