sync: update from private repo (f6b8c40)
Some checks failed
CI / build-and-test (push) Has been cancelled

This commit is contained in:
oss-sync 2026-06-15 22:47:24 +00:00
parent a0923abc40
commit e0c03ef10b
8 changed files with 129 additions and 9 deletions

View File

@ -319,6 +319,7 @@ tools:
# プロキシが TLS を終端しているので、ここで有効にすると二重終端になり接続が壊れる。 # プロキシが TLS を終端しているので、ここで有効にすると二重終端になり接続が壊れる。
# #
# server: # server:
# port: 9876 # HTTP(S) 待ち受けポート (default 9876)。環境変数 PORT (.env 含む) が優先。反映には再起動が必要
# tls: # tls:
# enabled: true # フレッシュインストールのデフォルト; ブロック未記載=アップグレード時 false # enabled: true # フレッシュインストールのデフォルト; ブロック未記載=アップグレード時 false
# cert_file: null # PEM 証明書パス (任意); cert_file と key_file は両方設定するか両方省略 # cert_file: null # PEM 証明書パス (任意); cert_file と key_file は両方設定するか両方省略

View File

@ -107,7 +107,7 @@ import { readGatewayConfig } from '../gateway/config.js';
import { createAdminGatewayStatusRouter } from './admin-gateway-status-api.js'; import { createAdminGatewayStatusRouter } from './admin-gateway-status-api.js';
import { createServer as createHttpsServer } from 'https'; import { createServer as createHttpsServer } from 'https';
import { X509Certificate } from 'crypto'; import { X509Certificate } from 'crypto';
import { mergeServerConfig } from '../server/config.js'; import { mergeServerConfig, resolveListenPort } from '../server/config.js';
import { resolveTlsOptions } from '../net/tls-options.js'; import { resolveTlsOptions } from '../net/tls-options.js';
import { createHttpRedirectServer } from '../net/http-redirect.js'; import { createHttpRedirectServer } from '../net/http-redirect.js';
@ -297,7 +297,9 @@ export function createCoreServer(opts: CoreServerOptions): {
// tls.enabled and the cookie secure flag. // tls.enabled and the cookie secure flag.
const serverCfg = mergeServerConfig(loadConfig().server, { const serverCfg = mergeServerConfig(loadConfig().server, {
freshInstall: false, freshInstall: false,
httpsPort: opts.listenPort ?? Number(process.env['PORT'] ?? 9876), // startCoreServer threads the resolved port in via listenPort; the fallback
// (for callers that bypass it) applies the same PORT-env > config precedence.
httpsPort: opts.listenPort ?? resolveListenPort(process.env['PORT'], loadConfig().server?.port),
}); });
if (authActive) { if (authActive) {
@ -1152,9 +1154,7 @@ export function createCoreServer(opts: CoreServerOptions): {
// (startCoreServer always sets it) over the PORT env-var guess so // (startCoreServer always sets it) over the PORT env-var guess so
// the status endpoint matches what the bridge actually bound. // the status endpoint matches what the bridge actually bound.
// env-var fallback is kept for callers that bypass startCoreServer. // env-var fallback is kept for callers that bypass startCoreServer.
const envPortRaw = Number(process.env['PORT']); const actualPort = opts.listenPort ?? resolveListenPort(process.env['PORT'], loadConfig().server?.port);
const envPort = Number.isFinite(envPortRaw) && envPortRaw > 0 ? envPortRaw : 9876;
const actualPort = opts.listenPort ?? envPort;
const statusRouter = createAdminGatewayStatusRouter({ const statusRouter = createAdminGatewayStatusRouter({
mount: gatewayMount, mount: gatewayMount,
configManager: opts.configManager ?? null, configManager: opts.configManager ?? null,
@ -1267,7 +1267,9 @@ function deriveCallbackBaseUrl(authConfig: AuthConfig | undefined): string {
} }
} }
} }
const port = Number(process.env.PORT ?? 9876); // Same precedence as the listener (PORT env > server.port > default) so the
// derived OAuth callback origin matches the port the bridge actually binds.
const port = resolveListenPort(process.env.PORT, loadConfig().server?.port);
return `http://localhost:${port}`; return `http://localhost:${port}`;
} }

View File

@ -1,5 +1,5 @@
import { describe, it, expect } from 'vitest'; import { describe, it, expect } from 'vitest';
import { mergeServerConfig, SERVER_TLS_DEFAULTS } from './config.js'; import { mergeServerConfig, SERVER_TLS_DEFAULTS, resolveListenPort, DEFAULT_SERVER_PORT } from './config.js';
describe('mergeServerConfig', () => { describe('mergeServerConfig', () => {
it('upgrade-safe: an absent server block disables TLS', () => { it('upgrade-safe: an absent server block disables TLS', () => {
@ -58,4 +58,56 @@ describe('mergeServerConfig', () => {
mergeServerConfig({ tls: { enabled: false, certFile: '/x/c.pem' } }, { freshInstall: false }), mergeServerConfig({ tls: { enabled: false, certFile: '/x/c.pem' } }, { freshInstall: false }),
).not.toThrow(); ).not.toThrow();
}); });
it('resolves port: httpsPort wins, else config port, else default', () => {
expect(mergeServerConfig({ port: 3000 }, { freshInstall: false }).port).toBe(3000);
expect(mergeServerConfig({ port: 3000 }, { freshInstall: false, httpsPort: 8443 }).port).toBe(8443);
expect(mergeServerConfig(undefined, { freshInstall: false }).port).toBe(DEFAULT_SERVER_PORT);
});
});
describe('resolveListenPort', () => {
it('PORT env wins over config port and default', () => {
expect(resolveListenPort('4000', 5000)).toBe(4000);
});
it('falls back to config port when env is unset/empty', () => {
expect(resolveListenPort(undefined, 5000)).toBe(5000);
expect(resolveListenPort('', 5000)).toBe(5000);
});
it('falls back to the default when neither is set', () => {
expect(resolveListenPort(undefined, undefined)).toBe(DEFAULT_SERVER_PORT);
});
it('ignores an invalid env PORT and warns, then uses config', () => {
const warns: string[] = [];
expect(resolveListenPort('not-a-port', 5000, (m) => warns.push(m))).toBe(5000);
expect(resolveListenPort('70000', 5000, (m) => warns.push(m))).toBe(5000); // out of range
expect(warns.length).toBe(2);
});
it('ignores an invalid config port and warns, then uses the default', () => {
const warns: string[] = [];
expect(resolveListenPort(undefined, 0, (m) => warns.push(m))).toBe(DEFAULT_SERVER_PORT);
expect(resolveListenPort(undefined, 99999, (m) => warns.push(m))).toBe(DEFAULT_SERVER_PORT);
expect(resolveListenPort(undefined, 1.5, (m) => warns.push(m))).toBe(DEFAULT_SERVER_PORT); // non-integer
expect(warns.length).toBe(3);
});
it('accepts the boundary ports 1 and 65535', () => {
expect(resolveListenPort('1', undefined)).toBe(1);
expect(resolveListenPort('65535', undefined)).toBe(65535);
});
it('rejects env "0" and falls through', () => {
expect(resolveListenPort('0', 5000)).toBe(5000);
expect(resolveListenPort('0', undefined)).toBe(DEFAULT_SERVER_PORT);
});
it('accepts a quoted-string config port symmetrically with env', () => {
expect(resolveListenPort(undefined, '5000' as unknown as number)).toBe(5000);
expect(resolveListenPort(undefined, '' as unknown as number)).toBe(DEFAULT_SERVER_PORT);
expect(resolveListenPort(undefined, 'abc' as unknown as number)).toBe(DEFAULT_SERVER_PORT);
});
}); });

View File

@ -16,9 +16,42 @@ export interface ServerTlsConfig {
} }
export interface ServerConfig { export interface ServerConfig {
/** HTTP(S) listen port. Resolved by mergeServerConfig (default 9876). */
port: number;
tls: ServerTlsConfig; tls: ServerTlsConfig;
} }
/** Default HTTP(S) listen port when neither PORT nor server.port is set. */
export const DEFAULT_SERVER_PORT = 9876;
/**
* Resolve the effective listen port from the precedence
* `PORT env > server.port (config) > default`. Invalid values (non-integer or
* out of the 165535 range) are ignored with a caller-supplied warn hook so a
* typo can't bind port 0 / NaN. Pure: the env string is passed in.
*/
export function resolveListenPort(
envPort: string | undefined,
configPort: number | string | undefined,
warn?: (msg: string) => void,
): number {
const isValid = (n: unknown): n is number =>
typeof n === 'number' && Number.isInteger(n) && n >= 1 && n <= 65535;
if (envPort !== undefined && envPort !== '') {
const n = Number(envPort);
if (isValid(n)) return n;
warn?.(`[server] ignoring invalid PORT env "${envPort}" (want an integer 165535)`);
}
if (configPort !== undefined && configPort !== '') {
// Accept a numeric YAML value or a quoted "9876" string symmetrically with env.
const n = typeof configPort === 'string' ? Number(configPort) : configPort;
if (isValid(n)) return n;
warn?.(`[server] ignoring invalid server.port "${configPort}" (want an integer 165535)`);
}
return DEFAULT_SERVER_PORT;
}
export const SERVER_TLS_DEFAULTS: ServerTlsConfig = { export const SERVER_TLS_DEFAULTS: ServerTlsConfig = {
enabled: false, enabled: false,
certFile: null, certFile: null,
@ -33,6 +66,11 @@ export const SERVER_TLS_DEFAULTS: ServerTlsConfig = {
export interface MergeServerOpts { export interface MergeServerOpts {
freshInstall: boolean; freshInstall: boolean;
/**
* The already-resolved effective listen port (PORT env > server.port >
* default), threaded in by the caller. Becomes the merged `port` and is the
* value the http_redirect_port collision check compares against.
*/
httpsPort?: number; httpsPort?: number;
} }
@ -59,5 +97,8 @@ export function mergeServerConfig(
throw new Error(`server.tls: http_redirect_port (${tls.httpRedirectPort}) must differ from the HTTPS port`); throw new Error(`server.tls: http_redirect_port (${tls.httpRedirectPort}) must differ from the HTTPS port`);
} }
} }
return { tls }; // opts.httpsPort is the already-resolved listen port in production; the
// fallback validates/coerces a raw config value for callers that don't pass it.
const port = opts.httpsPort ?? resolveListenPort(undefined, partial?.port);
return { port, tls };
} }

View File

@ -14,6 +14,7 @@
*/ */
import { Repository, BrowserSessionRepo } from './db/repository.js'; import { Repository, BrowserSessionRepo } from './db/repository.js';
import { startCoreServer } from './bridge/server.js'; import { startCoreServer } from './bridge/server.js';
import { resolveListenPort } from './server/config.js';
import { runMigrations } from './db/migrate.js'; import { runMigrations } from './db/migrate.js';
import { logger } from './logger.js'; import { logger } from './logger.js';
import { accessSync, existsSync, mkdirSync, constants } from 'fs'; import { accessSync, existsSync, mkdirSync, constants } from 'fs';
@ -147,7 +148,10 @@ export async function start(opts: StartWorkerOptions = {}): Promise<void> {
const workerManager = new WorkerManager(repo, configManager); const workerManager = new WorkerManager(repo, configManager);
workerManager.start(); workerManager.start();
const port = parseInt(process.env['PORT'] ?? '9876', 10); // Listen port precedence: PORT env (incl. .env loaded by scripts/server.sh)
// > config.yaml server.port > default 9876. UI/config changes apply on the
// next restart (no live re-bind), same as the TLS settings.
const port = resolveListenPort(process.env['PORT'], config.server?.port, (m) => logger.warn(m));
// タイトル自動生成関数を作成roles に 'title' を持つ worker を優先、なければ最初の worker // タイトル自動生成関数を作成roles に 'title' を持つ worker を優先、なければ最初の worker
const titleWorker = const titleWorker =

View File

@ -23,6 +23,7 @@ export function ServerTlsForm({ config, onChange }: SectionFormProps) {
// Navigate to the server.tls sub-object; fall back to empty object if absent. // Navigate to the server.tls sub-object; fall back to empty object if absent.
const tls = (config?.server?.tls) ?? {}; const tls = (config?.server?.tls) ?? {};
const server = (config?.server) ?? {};
return ( return (
<div className="space-y-5"> <div className="space-y-5">
@ -35,6 +36,21 @@ export function ServerTlsForm({ config, onChange }: SectionFormProps) {
{t('serverTls.restartBanner')} {t('serverTls.restartBanner')}
</div> </div>
{/* HTTP(S) listen port */}
<div>
<FieldLabel>{t('serverTls.port')}</FieldLabel>
<FieldInput
type="number"
value={server.port != null ? String(server.port) : ''}
onChange={v => {
const n = parseInt(v, 10);
onChange('server.port', isNaN(n) ? undefined : n);
}}
placeholder="9876"
/>
<HelpText>{t('serverTls.portHelp')}</HelpText>
</div>
{/* Enable HTTPS */} {/* Enable HTTPS */}
<div> <div>
<label className="inline-flex items-center gap-2 text-[13px] text-slate-700 dark:text-slate-200"> <label className="inline-flex items-center gap-2 text-[13px] text-slate-700 dark:text-slate-200">

View File

@ -782,6 +782,8 @@
}, },
"serverTls": { "serverTls": {
"title": "HTTPS / TLS", "title": "HTTPS / TLS",
"port": "HTTP(S) port",
"portHelp": "Port the server listens on (default 9876). The PORT environment variable (incl. .env) takes precedence over this value. Requires a restart to apply.",
"enabled": "Serve over HTTPS", "enabled": "Serve over HTTPS",
"enabledHelp": "Terminate TLS in the app. Self-signed by default — browsers (and the noVNC / SSH console over wss) will warn until you install a real certificate. Requires a restart to apply.", "enabledHelp": "Terminate TLS in the app. Self-signed by default — browsers (and the noVNC / SSH console over wss) will warn until you install a real certificate. Requires a restart to apply.",
"certFile": "Certificate file (PEM)", "certFile": "Certificate file (PEM)",

View File

@ -782,6 +782,8 @@
}, },
"serverTls": { "serverTls": {
"title": "HTTPS / TLS", "title": "HTTPS / TLS",
"port": "HTTP(S) ポート",
"portHelp": "サーバーが待ち受けるポート番号(デフォルト 9876。環境変数 PORT.env 含む)がこの値より優先されます。反映にはサーバーの再起動が必要です。",
"enabled": "HTTPS で配信", "enabled": "HTTPS で配信",
"enabledHelp": "アプリ内で TLS を終端します。デフォルトは自己署名証明書のため、正式な証明書を導入するまでブラウザ(および wss 経由の noVNC・SSH コンソール)に警告が表示されます。反映にはサーバーの再起動が必要です。", "enabledHelp": "アプリ内で TLS を終端します。デフォルトは自己署名証明書のため、正式な証明書を導入するまでブラウザ(および wss 経由の noVNC・SSH コンソール)に警告が表示されます。反映にはサーバーの再起動が必要です。",
"certFile": "証明書ファイルPEM", "certFile": "証明書ファイルPEM",