feat: 계정 Web Push 전달 기반과 사건 판정을 추가한다

게임 상태 변경과 같은 트랜잭션에 내구성 outbox를 기록하고 Gateway가 계정 설정과 기기 구독으로 fan-out하도록 한다. 전역 기본값은 비활성으로 유지하며 migration과 회귀 테스트를 함께 추가한다.
This commit is contained in:
2026-08-23 13:16:07 +00:00
parent f858806295
commit 7bc3213b03
34 changed files with 1964 additions and 9 deletions
+32
View File
@@ -1,3 +1,4 @@
import { readFileSync } from 'node:fs';
import path from 'node:path';
import { parseBooleanWithFallback, parseNumberWithFallback } from '@sammo-ts/common';
import { resolveFrontendServeMode, type FrontendServeMode } from './orchestrator/frontendArtifactManager.js';
@@ -41,6 +42,11 @@ export interface GatewayApiConfig {
frontendArtifactRoot: string;
frontendReadinessOrigin: string;
releaseBuilderUrl?: string;
webPushEnabled: boolean;
webPushVapidSubject?: string;
webPushVapidPublicKey?: string;
webPushVapidPrivateKey?: string;
webPushPollIntervalMs: number;
}
export interface GatewayOrchestratorConfig {
@@ -82,6 +88,23 @@ export const resolveGatewayApiConfigFromEnv = (env: NodeJS.ProcessEnv = process.
const redisKeyPrefix = env.GATEWAY_REDIS_PREFIX ?? 'sammo:gateway';
const port = parseNumberWithFallback(env.GATEWAY_API_PORT, 13000, 'GATEWAY_API_PORT');
const workspaceRootHint = env.GATEWAY_WORKSPACE_ROOT ?? process.cwd();
const webPushEnabled = parseBooleanWithFallback(env.WEB_PUSH_ENABLED, false);
const webPushVapidSubject = env.WEB_PUSH_VAPID_SUBJECT?.trim() || undefined;
const webPushVapidPublicKey = env.WEB_PUSH_VAPID_PUBLIC_KEY?.trim() || undefined;
let webPushVapidPrivateKey = env.WEB_PUSH_VAPID_PRIVATE_KEY?.trim() || undefined;
const webPushVapidPrivateKeyFile = env.WEB_PUSH_VAPID_PRIVATE_KEY_FILE?.trim();
if (webPushEnabled && !webPushVapidPrivateKey && webPushVapidPrivateKeyFile) {
try {
webPushVapidPrivateKey = readFileSync(webPushVapidPrivateKeyFile, 'utf8').trim() || undefined;
} catch (error) {
throw new Error('WEB_PUSH_VAPID_PRIVATE_KEY_FILE could not be read.', { cause: error });
}
}
if (webPushEnabled && (!webPushVapidSubject || !webPushVapidPublicKey || !webPushVapidPrivateKey)) {
throw new Error(
'WEB_PUSH_ENABLED requires WEB_PUSH_VAPID_SUBJECT, WEB_PUSH_VAPID_PUBLIC_KEY, and a VAPID private key.'
);
}
return {
host: env.GATEWAY_API_HOST ?? '0.0.0.0',
port,
@@ -149,6 +172,15 @@ export const resolveGatewayApiConfigFromEnv = (env: NodeJS.ProcessEnv = process.
frontendArtifactRoot: path.resolve(env.FRONTEND_ARTIFACT_ROOT ?? '/srv/frontend-artifacts'),
frontendReadinessOrigin: env.FRONTEND_READINESS_ORIGIN?.trim() || 'http://caddy',
releaseBuilderUrl: env.RELEASE_BUILDER_URL?.trim() || undefined,
webPushEnabled,
webPushVapidSubject,
webPushVapidPublicKey,
webPushVapidPrivateKey,
webPushPollIntervalMs: parseNumberWithFallback(
env.WEB_PUSH_POLL_INTERVAL_MS,
1_000,
'WEB_PUSH_POLL_INTERVAL_MS'
),
};
};