fix: various fixes to things + simply app proxy sentry setup

This commit is contained in:
Hampus Kraft
2026-02-19 00:29:58 +00:00
parent ff1d15f7aa
commit 528e4e0d7f
44 changed files with 441 additions and 1042 deletions

View File

@@ -184,7 +184,7 @@ export const DiscoveryPage: FC<DiscoveryPageProps> = ({
<TableRow key={app.guild_id}>
<TableCell>
<a
href={`${config.basePath}/guilds?query=${app.guild_id}`}
href={`${config.basePath}/guilds/${app.guild_id}`}
class="font-mono text-blue-600 text-sm hover:underline"
>
{app.guild_id}

View File

@@ -308,10 +308,6 @@ export function buildAPIConfigFromMaster(master: MasterConfig): APIConfig {
appPublic: {
sentryDsn: master.app_public.sentry_dsn,
sentryProxyPath: master.app_public.sentry_proxy_path,
sentryReportHost: master.app_public.sentry_report_host,
sentryProjectId: master.app_public.sentry_project_id,
sentryPublicKey: master.app_public.sentry_public_key,
},
auth: {

View File

@@ -96,10 +96,6 @@ export interface APIConfig {
appPublic: {
sentryDsn: string;
sentryProxyPath: string;
sentryReportHost: string;
sentryProjectId: string;
sentryPublicKey: string;
};
email: {

View File

@@ -95,10 +95,6 @@ export function InstanceController(app: Hono<HonoEnv>) {
},
app_public: {
sentry_dsn: Config.appPublic.sentryDsn,
sentry_proxy_path: Config.appPublic.sentryProxyPath,
sentry_report_host: Config.appPublic.sentryReportHost,
sentry_project_id: Config.appPublic.sentryProjectId,
sentry_public_key: Config.appPublic.sentryPublicKey,
},
};

View File

@@ -30,15 +30,10 @@ export interface AppProxyResult {
export async function createAppProxyApp(options: CreateAppProxyAppOptions): Promise<AppProxyResult> {
const {
assetsPath = '/assets',
config,
cspDirectives,
customMiddleware = [],
logger,
metricsCollector,
rateLimitService = null,
sentryProxyEnabled = true,
sentryProxyPath = '/sentry',
sentryProxyRouteEnabled = true,
staticCDNEndpoint,
staticDir,
tracing,
@@ -48,13 +43,9 @@ export async function createAppProxyApp(options: CreateAppProxyAppOptions): Prom
applyAppProxyMiddleware({
app,
config,
customMiddleware,
logger,
metricsCollector,
rateLimitService,
sentryProxyEnabled,
sentryProxyPath,
tracing,
});
@@ -63,9 +54,6 @@ export async function createAppProxyApp(options: CreateAppProxyAppOptions): Prom
assetsPath,
cspDirectives,
logger,
sentryProxy: config.sentry_proxy,
sentryProxyPath,
sentryProxyRouteEnabled,
staticCDNEndpoint,
staticDir,
});

View File

@@ -22,34 +22,13 @@ import type {SentryConfig, TelemetryConfig} from '@fluxer/config/src/MasterZodSc
import type {MetricsCollector} from '@fluxer/hono_types/src/MetricsTypes';
import type {TracingOptions} from '@fluxer/hono_types/src/TracingTypes';
import type {Logger} from '@fluxer/logger/src/Logger';
import type {IRateLimitService} from '@fluxer/rate_limit/src/IRateLimitService';
import type {MiddlewareHandler} from 'hono';
export interface AppProxySentryProxyConfig {
project_id: string;
public_key: string;
target_url: string;
path_prefix: string;
}
export interface AppProxyConfig {
env: string;
port: number;
static_cdn_endpoint: string;
sentry_proxy_path: string;
sentry_report_host: string;
sentry_proxy: AppProxySentryProxyConfig | null;
assets_dir: string;
kv: {
url: string;
timeout_ms: number;
};
rate_limit: {
sentry: {
limit: number;
window_ms: number;
};
};
telemetry: TelemetryConfig;
sentry: SentryConfig;
}
@@ -57,7 +36,6 @@ export interface AppProxyConfig {
export interface AppProxyContext {
config: AppProxyConfig;
logger: Logger;
rateLimitService: IRateLimitService | null;
}
export interface AppProxyHonoEnv {
@@ -69,15 +47,11 @@ export type AppProxyMiddleware = MiddlewareHandler<AppProxyHonoEnv>;
export interface CreateAppProxyAppOptions {
config: AppProxyConfig;
logger: Logger;
rateLimitService?: IRateLimitService | null;
metricsCollector?: MetricsCollector;
tracing?: TracingOptions;
customMiddleware?: Array<AppProxyMiddleware>;
sentryProxyPath?: string;
assetsPath?: string;
staticCDNEndpoint?: string;
staticDir?: string;
cspDirectives?: CSPOptions;
sentryProxyEnabled?: boolean;
sentryProxyRouteEnabled?: boolean;
}

View File

@@ -17,44 +17,25 @@
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
import type {AppProxyConfig, AppProxyHonoEnv, AppProxyMiddleware} from '@fluxer/app_proxy/src/AppProxyTypes';
import {createProxyRateLimitMiddleware} from '@fluxer/app_proxy/src/app_proxy/middleware/ProxyRateLimit';
import {createSentryHostProxyMiddleware} from '@fluxer/app_proxy/src/app_proxy/middleware/SentryHostProxy';
import {proxySentry} from '@fluxer/app_proxy/src/app_proxy/proxy/SentryProxy';
import {resolveSentryHost} from '@fluxer/app_proxy/src/app_proxy/utils/Host';
import type {AppProxyHonoEnv, AppProxyMiddleware} from '@fluxer/app_proxy/src/AppProxyTypes';
import {isExpectedError} from '@fluxer/app_proxy/src/ErrorClassification';
import {applyMiddlewareStack} from '@fluxer/hono/src/middleware/MiddlewareStack';
import type {MetricsCollector} from '@fluxer/hono_types/src/MetricsTypes';
import type {TracingOptions} from '@fluxer/hono_types/src/TracingTypes';
import type {Logger} from '@fluxer/logger/src/Logger';
import type {IRateLimitService} from '@fluxer/rate_limit/src/IRateLimitService';
import {captureException} from '@fluxer/sentry/src/Sentry';
import type {Context, Hono} from 'hono';
interface ApplyAppProxyMiddlewareOptions {
app: Hono<AppProxyHonoEnv>;
config: AppProxyConfig;
customMiddleware: Array<AppProxyMiddleware>;
logger: Logger;
metricsCollector?: MetricsCollector;
rateLimitService: IRateLimitService | null;
sentryProxyEnabled: boolean;
sentryProxyPath: string;
tracing?: TracingOptions;
}
export function applyAppProxyMiddleware(options: ApplyAppProxyMiddlewareOptions): void {
const {
app,
config,
customMiddleware,
logger,
metricsCollector,
rateLimitService,
sentryProxyEnabled,
sentryProxyPath,
tracing,
} = options;
const {app, customMiddleware, logger, metricsCollector, tracing} = options;
applyMiddlewareStack(app, {
requestId: {},
@@ -81,7 +62,7 @@ export function applyAppProxyMiddleware(options: ApplyAppProxyMiddlewareOptions)
skip: ['/_health'],
},
errorHandler: {
includeStack: !sentryProxyEnabled,
includeStack: true,
logger: (error: Error, ctx: Context) => {
if (!isExpectedError(error)) {
captureException(error, {
@@ -103,58 +84,4 @@ export function applyAppProxyMiddleware(options: ApplyAppProxyMiddlewareOptions)
},
customMiddleware,
});
applySentryHostProxyMiddleware({
app,
config,
logger,
rateLimitService,
sentryProxyEnabled,
sentryProxyPath,
});
}
interface ApplySentryHostProxyMiddlewareOptions {
app: Hono<AppProxyHonoEnv>;
config: AppProxyConfig;
logger: Logger;
rateLimitService: IRateLimitService | null;
sentryProxyEnabled: boolean;
sentryProxyPath: string;
}
function applySentryHostProxyMiddleware(options: ApplySentryHostProxyMiddlewareOptions): void {
const {app, config, logger, rateLimitService, sentryProxyEnabled, sentryProxyPath} = options;
const sentryHost = resolveSentryHost(config);
const sentryProxy = config.sentry_proxy;
if (!sentryProxyEnabled || !sentryHost || !sentryProxy) {
return;
}
const sentryRateLimitMiddleware = createProxyRateLimitMiddleware({
rateLimitService,
bucketPrefix: 'sentry-proxy',
config: {
enabled: config.rate_limit.sentry.limit > 0,
limit: config.rate_limit.sentry.limit,
windowMs: config.rate_limit.sentry.window_ms,
skipPaths: ['/_health'],
},
logger,
});
const sentryHostMiddleware = createSentryHostProxyMiddleware({
sentryHost,
rateLimitMiddleware: sentryRateLimitMiddleware,
proxyHandler: (c) =>
proxySentry(c, {
enabled: true,
logger,
sentryProxy,
sentryProxyPath,
}),
});
app.use('*', sentryHostMiddleware);
}

View File

@@ -18,9 +18,8 @@
*/
import {resolve} from 'node:path';
import type {AppProxyHonoEnv, AppProxySentryProxyConfig} from '@fluxer/app_proxy/src/AppProxyTypes';
import type {AppProxyHonoEnv} from '@fluxer/app_proxy/src/AppProxyTypes';
import {proxyAssets} from '@fluxer/app_proxy/src/app_proxy/proxy/AssetsProxy';
import {proxySentry} from '@fluxer/app_proxy/src/app_proxy/proxy/SentryProxy';
import {createSpaIndexRoute} from '@fluxer/app_proxy/src/app_server/routes/SpaIndexRoute';
import type {CSPOptions} from '@fluxer/app_proxy/src/app_server/utils/CSP';
import type {Logger} from '@fluxer/logger/src/Logger';
@@ -31,46 +30,15 @@ interface RegisterAppProxyRoutesOptions {
assetsPath: string;
cspDirectives?: CSPOptions;
logger: Logger;
sentryProxy: AppProxySentryProxyConfig | null;
sentryProxyPath: string;
sentryProxyRouteEnabled: boolean;
staticCDNEndpoint: string | undefined;
staticDir?: string;
}
export function registerAppProxyRoutes(options: RegisterAppProxyRoutesOptions): void {
const {
app,
assetsPath,
cspDirectives,
logger,
sentryProxy,
sentryProxyPath,
sentryProxyRouteEnabled,
staticCDNEndpoint,
staticDir,
} = options;
const {app, assetsPath, cspDirectives, logger, staticCDNEndpoint, staticDir} = options;
app.get('/_health', (c) => c.text('OK'));
app.all(sentryProxyPath, (c) =>
proxySentry(c, {
enabled: sentryProxyRouteEnabled,
logger,
sentryProxy,
sentryProxyPath,
}),
);
app.all(`${sentryProxyPath}/*`, (c) =>
proxySentry(c, {
enabled: sentryProxyRouteEnabled,
logger,
sentryProxy,
sentryProxyPath,
}),
);
if (staticCDNEndpoint) {
app.get(`${assetsPath}/*`, (c) =>
proxyAssets(c, {

View File

@@ -1,72 +0,0 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
import {HttpStatus} from '@fluxer/constants/src/HttpConstants';
import {extractClientIp} from '@fluxer/ip_utils/src/ClientIp';
import type {Logger} from '@fluxer/logger/src/Logger';
import type {IRateLimitService} from '@fluxer/rate_limit/src/IRateLimitService';
import {createRateLimitMiddleware} from '@fluxer/rate_limit/src/middleware/RateLimitMiddleware';
interface ProxyRateLimitConfig {
enabled?: boolean;
limit: number;
windowMs: number;
skipPaths?: Array<string>;
}
export interface CreateProxyRateLimitMiddlewareOptions {
rateLimitService: IRateLimitService | null;
bucketPrefix: string;
config: ProxyRateLimitConfig;
logger: Logger;
}
export function createProxyRateLimitMiddleware(options: CreateProxyRateLimitMiddlewareOptions) {
const {bucketPrefix, config, logger, rateLimitService} = options;
return createRateLimitMiddleware({
rateLimitService: () => rateLimitService,
config: {
get enabled() {
const hasRateLimitService = Boolean(rateLimitService);
return config.enabled !== undefined ? hasRateLimitService && config.enabled : hasRateLimitService;
},
limit: config.limit,
windowMs: config.windowMs,
skipPaths: config.skipPaths,
},
getClientIdentifier: (req) => {
const realIp = extractClientIp(req);
return realIp || 'unknown';
},
getBucketName: (identifier, _ctx) => `${bucketPrefix}:ip:${identifier}`,
onRateLimitExceeded: (c, retryAfter) => {
const identifier = extractClientIp(c.req.raw) || 'unknown';
logger.warn(
{
ip: identifier,
path: c.req.path,
retryAfter,
},
'proxy rate limit exceeded',
);
return c.text('Too Many Requests', HttpStatus.TOO_MANY_REQUESTS);
},
});
}

View File

@@ -1,48 +0,0 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
import {normalizeHost} from '@fluxer/app_proxy/src/app_proxy/utils/Host';
import type {Context, Next} from 'hono';
interface SentryHostProxyMiddlewareOptions {
sentryHost: string;
// biome-ignore lint/suspicious/noConfusingVoidType: hono middleware may return void
rateLimitMiddleware: (c: Context, next: Next) => Promise<Response | undefined | void>;
proxyHandler: (c: Context) => Promise<Response>;
}
export function createSentryHostProxyMiddleware(options: SentryHostProxyMiddlewareOptions) {
const {proxyHandler, rateLimitMiddleware, sentryHost} = options;
return async function sentryHostProxy(c: Context, next: Next): Promise<Response | undefined> {
const incomingHost = normalizeHost(c.req.raw.headers.get('host') ?? '');
if (incomingHost && incomingHost === sentryHost) {
const result = await rateLimitMiddleware(c, async () => {
c.res = await proxyHandler(c);
});
if (result) {
return result;
}
return c.res;
}
await next();
return undefined;
};
}

View File

@@ -1,77 +0,0 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
import type {AppProxySentryProxyConfig} from '@fluxer/app_proxy/src/AppProxyTypes';
import {createProxyRequestHeaders, forwardProxyRequest} from '@fluxer/app_proxy/src/app_proxy/proxy/ProxyRequest';
import {HttpStatus} from '@fluxer/constants/src/HttpConstants';
import type {Logger} from '@fluxer/logger/src/Logger';
import type {Context} from 'hono';
export interface ProxySentryOptions {
enabled: boolean;
sentryProxy: AppProxySentryProxyConfig | null;
sentryProxyPath: string;
logger: Logger;
}
export async function proxySentry(c: Context, options: ProxySentryOptions): Promise<Response> {
const {enabled, logger, sentryProxy, sentryProxyPath} = options;
if (!enabled || !sentryProxy) {
return c.text('Sentry proxy not configured', HttpStatus.SERVICE_UNAVAILABLE);
}
const targetPath = resolveSentryTargetPath(c.req.path, sentryProxyPath, sentryProxy.path_prefix);
const targetUrl = new URL(targetPath, sentryProxy.target_url);
const upstreamSentryEndpoint = new URL(sentryProxy.target_url);
const headers = createProxyRequestHeaders({
incomingHeaders: c.req.raw.headers,
upstreamHost: upstreamSentryEndpoint.host,
});
try {
return await forwardProxyRequest({
targetUrl,
method: c.req.method,
headers,
body: c.req.raw.body,
bufferResponseBody: true,
});
} catch (error) {
logger.error(
{
path: c.req.path,
targetUrl: targetUrl.toString(),
error,
},
'sentry proxy error',
);
return c.text('Bad Gateway', HttpStatus.BAD_GATEWAY);
}
}
function resolveSentryTargetPath(requestPath: string, sentryProxyPath: string, sentryPathPrefix: string): string {
let normalizedPath = requestPath;
if (normalizedPath.startsWith(sentryProxyPath)) {
normalizedPath = normalizedPath.slice(sentryProxyPath.length) || '/';
}
if (!normalizedPath.startsWith('/')) {
normalizedPath = `/${normalizedPath}`;
}
return `${sentryPathPrefix}${normalizedPath}`;
}

View File

@@ -1,51 +0,0 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
export function normalizeHost(value: string): string {
const trimmed = value.trim().toLowerCase();
if (!trimmed) {
return '';
}
const primary = trimmed.split(',')[0]?.trim() ?? '';
if (primary.startsWith('[')) {
const end = primary.indexOf(']');
return end > 0 ? primary.slice(1, end) : primary;
}
return primary.split(':')[0] ?? primary;
}
export function resolveSentryHost(config: {
sentry_proxy: {target_url: string} | null;
sentry_report_host: string;
}): string | null {
if (!config.sentry_proxy || !config.sentry_report_host) {
return null;
}
const normalizedSentryHost = normalizeHostValue(config.sentry_report_host);
return normalizedSentryHost || null;
}
function normalizeHostValue(rawHost: string): string {
try {
return normalizeHost(new URL(rawHost).host);
} catch {
return normalizeHost(rawHost);
}
}

View File

@@ -18,7 +18,7 @@
*/
import {randomBytes} from 'node:crypto';
import type {SentryDSN} from '@fluxer/app_proxy/src/app_server/utils/SentryDSN';
import {parseSentryDSN} from '@fluxer/app_proxy/src/app_server/utils/SentryDSN';
export const CSP_HOSTS = {
FRAME: [
@@ -71,8 +71,6 @@ export const CSP_HOSTS = {
'https://*.fluxer.workers.dev',
'https://fluxerusercontent.com',
'https://fluxerstatic.com',
'https://sentry.web.fluxer.app',
'https://sentry.web.canary.fluxer.app',
'https://fluxer.media',
'http://127.0.0.1:21863',
'http://127.0.0.1:21864',
@@ -95,33 +93,26 @@ export interface CSPOptions {
reportUri?: string;
}
export interface SentryProxyConfig {
sentryProxy: SentryDSN | null;
sentryProxyPath: string;
sentryReportHost: string;
export interface SentryCSPConfig {
sentryDsn: string;
}
export function generateNonce(): string {
return randomBytes(16).toString('hex');
}
export function buildSentryReportURI(config: SentryProxyConfig): string {
const sentry = config.sentryProxy;
export function buildSentryReportURI(config: SentryCSPConfig): string {
const sentry = parseSentryDSN(config.sentryDsn);
if (!sentry) {
return '';
}
const pathPrefix = config.sentryProxyPath.replace(/\/+$/, '');
let uri = `${pathPrefix}/api/${sentry.projectId}/security/?sentry_version=7`;
let uri = `${sentry.targetUrl}${sentry.pathPrefix}/api/${sentry.projectId}/security/?sentry_version=7`;
if (sentry.publicKey) {
uri += `&sentry_key=${sentry.publicKey}`;
}
if (config.sentryReportHost) {
return config.sentryReportHost + uri;
}
return uri;
}
@@ -160,8 +151,13 @@ export function buildCSP(nonce: string, options?: CSPOptions): string {
return directives.join('; ');
}
export function buildFluxerCSPOptions(config: SentryProxyConfig): CSPOptions {
export function buildFluxerCSPOptions(config: SentryCSPConfig): CSPOptions {
const reportURI = buildSentryReportURI(config);
const sentry = parseSentryDSN(config.sentryDsn);
const connectSrc: Array<string> = [...CSP_HOSTS.CONNECT];
if (sentry) {
connectSrc.push(sentry.targetUrl);
}
return {
scriptSrc: [...CSP_HOSTS.SCRIPT],
@@ -169,7 +165,7 @@ export function buildFluxerCSPOptions(config: SentryProxyConfig): CSPOptions {
imgSrc: [...CSP_HOSTS.IMAGE],
mediaSrc: [...CSP_HOSTS.MEDIA],
fontSrc: [...CSP_HOSTS.FONT],
connectSrc: [...CSP_HOSTS.CONNECT],
connectSrc: Array.from(new Set(connectSrc)),
frameSrc: [...CSP_HOSTS.FRAME],
workerSrc: [...CSP_HOSTS.WORKER],
manifestSrc: [...CSP_HOSTS.MANIFEST],
@@ -177,6 +173,6 @@ export function buildFluxerCSPOptions(config: SentryProxyConfig): CSPOptions {
};
}
export function buildFluxerCSP(nonce: string, config: SentryProxyConfig): string {
export function buildFluxerCSP(nonce: string, config: SentryCSPConfig): string {
return buildCSP(nonce, buildFluxerCSPOptions(config));
}

View File

@@ -1587,58 +1587,9 @@
}
}
},
"app_proxy_kv": {
"type": "object",
"description": "Valkey/Redis settings for the App Proxy.",
"required": [
"url"
],
"properties": {
"url": {
"type": "string",
"description": "Full URL to Valkey/Redis."
},
"timeout_ms": {
"type": "number",
"description": "Request timeout for Valkey/Redis in milliseconds.",
"default": 5000
}
}
},
"app_proxy_sentry_rate_limit": {
"type": "object",
"description": "Rate limiting for Sentry error reporting requests.",
"properties": {
"limit": {
"type": "number",
"description": "Number of Sentry requests allowed per window.",
"default": 100
},
"window_ms": {
"type": "number",
"description": "Time window for Sentry rate limiting in milliseconds.",
"default": 1000
}
}
},
"app_proxy_rate_limit": {
"type": "object",
"description": "Rate limit settings for the App Proxy.",
"properties": {
"sentry": {
"description": "Sentry reporting rate limit configuration.",
"$ref": "#/$defs/app_proxy_sentry_rate_limit",
"default": {}
}
}
},
"app_proxy_service": {
"type": "object",
"description": "Configuration for the App Proxy service (frontend server).",
"required": [
"sentry_report_host",
"sentry_dsn"
],
"properties": {
"port": {
"type": "number",
@@ -1650,32 +1601,10 @@
"description": "URL endpoint for serving static assets via CDN.",
"default": ""
},
"sentry_proxy_path": {
"type": "string",
"description": "URL path for proxying Sentry requests.",
"default": "/error-reporting-proxy"
},
"sentry_report_host": {
"type": "string",
"description": "Hostname to which Sentry reports should be sent."
},
"sentry_dsn": {
"type": "string",
"description": "Sentry DSN (Data Source Name) for frontend error tracking."
},
"assets_dir": {
"type": "string",
"description": "Filesystem directory containing static assets.",
"default": "./assets"
},
"kv": {
"description": "Valkey/Redis configuration for the proxy.",
"$ref": "#/$defs/app_proxy_kv"
},
"rate_limit": {
"description": "Rate limiting configuration for the App Proxy.",
"$ref": "#/$defs/app_proxy_rate_limit",
"default": {}
}
}
},
@@ -2120,26 +2049,6 @@
"type": "string",
"default": "",
"description": "Frontend Sentry DSN."
},
"sentry_proxy_path": {
"type": "string",
"default": "/error-reporting-proxy",
"description": "Path to proxy Sentry requests."
},
"sentry_report_host": {
"type": "string",
"default": "",
"description": "Host for Sentry reporting."
},
"sentry_project_id": {
"type": "string",
"default": "",
"description": "Sentry Project ID."
},
"sentry_public_key": {
"type": "string",
"default": "",
"description": "Sentry Public Key."
}
}
}

View File

@@ -54,7 +54,7 @@ function makeMinimalConfig(overrides: Record<string, unknown> = {}): Record<stri
secret_key_base: 'abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789',
oauth_client_secret: 'fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210',
},
app_proxy: {port: 8773, sentry_report_host: 'sentry.io', sentry_dsn: 'https://test@sentry.io/1'},
app_proxy: {port: 8773},
marketing: {
enabled: false,
port: 8774,

View File

@@ -1,55 +1,8 @@
{
"app_proxy_kv": {
"type": "object",
"description": "Valkey/Redis settings for the App Proxy.",
"additionalProperties": false,
"required": ["url"],
"properties": {
"url": {
"type": "string",
"description": "Full URL to Valkey/Redis."
},
"timeout_ms": {
"type": "number",
"description": "Request timeout for Valkey/Redis in milliseconds.",
"default": 5000
}
}
},
"app_proxy_sentry_rate_limit": {
"type": "object",
"description": "Rate limiting for Sentry error reporting requests.",
"additionalProperties": false,
"properties": {
"limit": {
"type": "number",
"description": "Number of Sentry requests allowed per window.",
"default": 100
},
"window_ms": {
"type": "number",
"description": "Time window for Sentry rate limiting in milliseconds.",
"default": 1000
}
}
},
"app_proxy_rate_limit": {
"type": "object",
"description": "Rate limit settings for the App Proxy.",
"additionalProperties": false,
"properties": {
"sentry": {
"description": "Sentry reporting rate limit configuration.",
"$ref": "#/$defs/app_proxy_sentry_rate_limit",
"default": {}
}
}
},
"app_proxy_service": {
"type": "object",
"description": "Configuration for the App Proxy service (frontend server).",
"additionalProperties": false,
"required": ["sentry_report_host", "sentry_dsn"],
"properties": {
"port": {
"type": "number",
@@ -61,32 +14,10 @@
"description": "URL endpoint for serving static assets via CDN.",
"default": ""
},
"sentry_proxy_path": {
"type": "string",
"description": "URL path for proxying Sentry requests.",
"default": "/error-reporting-proxy"
},
"sentry_report_host": {
"type": "string",
"description": "Hostname to which Sentry reports should be sent."
},
"sentry_dsn": {
"type": "string",
"description": "Sentry DSN (Data Source Name) for frontend error tracking."
},
"assets_dir": {
"type": "string",
"description": "Filesystem directory containing static assets.",
"default": "./assets"
},
"kv": {
"description": "Valkey/Redis configuration for the proxy.",
"$ref": "#/$defs/app_proxy_kv"
},
"rate_limit": {
"description": "Rate limiting configuration for the App Proxy.",
"$ref": "#/$defs/app_proxy_rate_limit",
"default": {}
}
}
}

View File

@@ -111,26 +111,6 @@
"type": "string",
"default": "",
"description": "Frontend Sentry DSN."
},
"sentry_proxy_path": {
"type": "string",
"default": "/error-reporting-proxy",
"description": "Path to proxy Sentry requests."
},
"sentry_report_host": {
"type": "string",
"default": "",
"description": "Host for Sentry reporting."
},
"sentry_project_id": {
"type": "string",
"default": "",
"description": "Sentry Project ID."
},
"sentry_public_key": {
"type": "string",
"default": "",
"description": "Sentry Public Key."
}
}
}

View File

@@ -219,6 +219,37 @@ describe('Fluxer Markdown Parser', () => {
]);
});
test('timestamp at max js date boundary parses', () => {
const input = '<t:8640000000000>';
const parser = new Parser(input, 0);
const {nodes: ast} = parser.parse();
expect(ast).toEqual([
{
type: NodeType.Timestamp,
timestamp: 8640000000000,
style: TimestampStyle.ShortDateTime,
},
]);
});
test('timestamp above max js date boundary does not parse', () => {
const input = '<t:8640000000001>';
const parser = new Parser(input, 0);
const {nodes: ast} = parser.parse();
expect(ast).toEqual([{type: NodeType.Text, content: input}]);
});
test('timestamp that overflows to infinity does not parse', () => {
const largeDigits = '9'.repeat(400);
const input = `<t:${largeDigits}>`;
const parser = new Parser(input, 0);
const {nodes: ast} = parser.parse();
expect(ast).toEqual([{type: NodeType.Text, content: input}]);
});
test('timestamp with leading zeros', () => {
const input = '<t:0001618953630>';
const flags = 0;

View File

@@ -58,6 +58,15 @@ export function parseTimestamp(text: string): ParserResult | null {
return null;
}
const timestampMillis = timestamp * 1000;
if (!Number.isFinite(timestampMillis)) {
return null;
}
if (Number.isNaN(new Date(timestampMillis).getTime())) {
return null;
}
let style: TimestampStyle;
if (stylePart !== undefined) {
if (stylePart === '') {

View File

@@ -169,6 +169,10 @@ We work with trusted third-party service providers who process data on our behal
- **hCaptcha** backup CAPTCHA provider; users can choose hCaptcha instead of Cloudflare Turnstile for bot prevention challenges.
- **Arachnid Shield API** CSAM scanning for user-uploaded media, operated by the Canadian Centre for Child Protection (C3P), as described in [Section 5](#5-content-scanning-for-safety).
#### Observability and error reporting
- **Sentry** application error monitoring. We send error and crash diagnostics directly to Sentry over HTTPS so we can investigate reliability and security issues. This may include stack traces, runtime metadata (for example browser, OS, and device details), release/build identifiers, and account identifiers associated with the active session (for example user ID, username, and email). We do not use this data for advertising, and routine error reports do not include private message content or uploaded file attachments.
#### Third-Party Content
- **Google** YouTube embeds.

View File

@@ -45,10 +45,6 @@ export type LimitConfigResponse = z.infer<typeof LimitConfigResponse>;
export const AppPublicConfigResponse = z.object({
sentry_dsn: z.string().describe('Sentry DSN for client-side error reporting'),
sentry_proxy_path: z.string().describe('Proxy path for Sentry requests'),
sentry_report_host: z.string().describe('Host for Sentry error reports'),
sentry_project_id: z.string().describe('Sentry project ID'),
sentry_public_key: z.string().describe('Sentry public key'),
});
export type AppPublicConfigResponse = z.infer<typeof AppPublicConfigResponse>;