refactor(api): make listeners mutable and add dedicated dynamic config endpoint

API Changes:
- Rename PUT /server/listeners-for-new-access-keys → PUT /server/listeners
- Add `applyToExisting` parameter to optionally update all existing keys
- Add PUT /access-keys/{id}/listeners for per-key listener updates
- Add GET /access-keys/{id}/dynamic-config for YAML transport config
- Make GET /access-keys/{id} always return JSON (use dynamic-config for YAML)

Type Renames:
- ListenersForNewAccessKeys → ListenersConfig
- listenersForNewAccessKeys config field → listeners

This simplifies the mental model by:
1. Allowing listener updates on any key, not just at creation
2. Providing a clear global vs per-key control pattern
3. Separating JSON metadata from YAML transport config
This commit is contained in:
lunarthegrey 2026-01-16 23:40:30 -06:00
parent bec26bd2b3
commit be31e617fd
9 changed files with 302 additions and 150 deletions

View file

@ -121,9 +121,9 @@ The Outline Server supports Shadowsocks over WebSocket (SS over WSS) for improve
### Enabling WebSocket Support
1. **Configure Listeners for New Access Keys:**
1. **Configure Listeners:**
Set up listener configuration including WebSocket paths:
Set defaults for new keys (add `"applyToExisting": true` to update all existing keys):
```sh
curl --insecure -X PUT -H "Content-Type: application/json" \
@ -133,7 +133,7 @@ The Outline Server supports Shadowsocks over WebSocket (SS over WSS) for improve
"websocketStream": {"path": "/tcp", "webServerPort": 8080},
"websocketPacket": {"path": "/udp", "webServerPort": 8080}
}' \
$API_URL/server/listeners-for-new-access-keys
$API_URL/server/listeners
```
2. **Enable the Caddy Web Server (for automatic HTTPS):**
@ -149,23 +149,20 @@ The Outline Server supports Shadowsocks over WebSocket (SS over WSS) for improve
$API_URL/server/web-server
```
3. **Create WebSocket-Enabled Access Keys:**
3. **Update a Specific Key's Listeners:**
```sh
curl --insecure -X POST -H "Content-Type: application/json" \
-d '{
"name": "WebSocket User",
"listeners": ["tcp", "udp", "websocket-stream", "websocket-packet"]
}' \
$API_URL/access-keys
curl --insecure -X PUT -H "Content-Type: application/json" \
-d '{"listeners": ["tcp", "udp", "websocket-stream", "websocket-packet"]}' \
$API_URL/access-keys/0/listeners
```
4. **Retrieve WebSocket Access Key Configuration:**
4. **Get Dynamic Config (YAML):**
For WebSocket-enabled keys, `GET /access-keys/{id}` returns YAML configuration (Outline Client v1.15.0+):
Use the dedicated endpoint to retrieve YAML transport configuration (Outline Client v1.15.0+):
```sh
curl --insecure $API_URL/access-keys/0
curl --insecure $API_URL/access-keys/0/dynamic-config
```
Example response (`Content-Type: text/yaml`):
@ -189,6 +186,8 @@ The Outline Server supports Shadowsocks over WebSocket (SS over WSS) for improve
secret: XxXxXx
```
> [!NOTE] > `GET /access-keys/{id}` always returns JSON. Use `/access-keys/{id}/dynamic-config` for YAML transport configuration.
### Listener Types
- `tcp` - Traditional TCP Shadowsocks

View file

@ -90,4 +90,8 @@ export interface AccessKeyRepository {
setAccessKeyDataLimit(id: AccessKeyId, limit: DataLimit): void;
// Removes the custom data limit from access key `id`.
removeAccessKeyDataLimit(id: AccessKeyId): void;
// Updates the listeners for access key `id`.
setAccessKeyListeners(id: AccessKeyId, listeners: ListenerType[]): void;
// Updates the listeners for all access keys.
setListenersForAllKeys(listeners: ListenerType[]): void;
}

View file

@ -99,24 +99,36 @@ paths:
'409':
description: The requested port was already in use by another service.
/server/listeners-for-new-access-keys:
/server/listeners:
put:
description: Sets the listeners configuration for newly created access keys. Supports different ports for TCP and UDP, and WebSocket paths.
description: |
Sets the listeners configuration for access keys.
- By default, only affects newly created keys
- Set `applyToExisting: true` to also update all existing keys
Use PUT /access-keys/{id}/listeners for per-key overrides.
tags:
- Access Key
- Server
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/ListenersConfig"
allOf:
- $ref: "#/components/schemas/ListenersConfig"
- type: object
properties:
applyToExisting:
type: boolean
default: false
description: If true, also update all existing access keys
examples:
'Basic TCP/UDP':
'Defaults only':
value: '{"tcp": {"port": 443}, "udp": {"port": 443}}'
'With WebSocket':
value: '{"tcp": {"port": 443}, "udp": {"port": 443}, "websocketStream": {"path": "/tcp", "webServerPort": 8080}, "websocketPacket": {"path": "/udp", "webServerPort": 8080}}'
'Different TCP/UDP ports':
value: '{"tcp": {"port": 443}, "udp": {"port": 8443}}'
value: '{"tcp": {"port": 443}, "websocketStream": {"path": "/tcp", "webServerPort": 8080}, "websocketPacket": {"path": "/udp", "webServerPort": 8080}}'
'Update all existing keys':
value: '{"tcp": {"port": 443}, "websocketStream": {"path": "/tcp"}, "applyToExisting": true}'
responses:
'204':
description: The listeners configuration was successfully updated.
@ -421,7 +433,7 @@ paths:
value: >-
{"id":"my-identifier","name":"First","password":"XxXxXx","port":9795,"method":"chacha20-ietf-poly1305","accessUrl":"ss://SADFJSKADFJAKSD@0.0.0.0:9795/?outline=1"}
get:
description: Get an access key
description: Get an access key (always returns JSON)
tags:
- Access Key
parameters:
@ -433,10 +445,7 @@ paths:
type: string
responses:
'200':
description: |
The access key. Response format depends on key type:
- Traditional keys (TCP/UDP only): JSON with AccessKey schema
- WebSocket-enabled keys: YAML with dynamic transport configuration
description: The access key
content:
application/json:
schema:
@ -444,28 +453,8 @@ paths:
examples:
'Traditional key':
value: '{"id":"0","name":"Admin","password":"XxXxXx","port":18162,"method":"chacha20-ietf-poly1305","accessUrl":"ss://SADFJSKADFJAKSD@0.0.0.0:18162/?outline=1"}'
text/yaml:
schema:
type: string
examples:
'WebSocket-enabled key':
value: |
transport:
$type: tcpudp
tcp:
$type: shadowsocks
endpoint:
$type: websocket
url: wss://example.com/tcp
cipher: chacha20-ietf-poly1305
secret: XxXxXx
udp:
$type: shadowsocks
endpoint:
$type: websocket
url: wss://example.com/udp
cipher: chacha20-ietf-poly1305
secret: XxXxXx
'WebSocket key':
value: '{"id":"1","name":"WSS User","listeners":["websocket-stream","websocket-packet"],"dynamicConfig":{"transport":{"$type":"tcpudp"}}}'
'404':
description: Access key inexistent
content:
@ -540,6 +529,91 @@ paths:
description: Access key renamed successfully
'404':
description: Access key inexistent
/access-keys/{id}/listeners:
put:
description: |
Updates the listeners for a specific access key.
This overrides the global /server/listeners setting for this key.
tags:
- Access Key
parameters:
- name: id
in: path
required: true
description: The id of the access key
schema:
type: string
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- listeners
properties:
listeners:
type: array
items:
type: string
enum: ['tcp', 'udp', 'websocket-stream', 'websocket-packet']
examples:
'All transports':
value: '{"listeners": ["tcp", "udp", "websocket-stream", "websocket-packet"]}'
'WebSocket only':
value: '{"listeners": ["websocket-stream", "websocket-packet"]}'
'Traditional only':
value: '{"listeners": ["tcp", "udp"]}'
responses:
'204':
description: Listeners updated successfully
'400':
description: Invalid listeners configuration
'404':
description: Access key not found
/access-keys/{id}/dynamic-config:
get:
description: |
Returns the dynamic transport configuration for WebSocket-enabled keys.
Returns YAML content compatible with Outline Client v1.15.0+.
Returns 404 if the key has no WebSocket listeners.
tags:
- Access Key
parameters:
- name: id
in: path
required: true
description: The id of the access key
schema:
type: string
responses:
'200':
description: Dynamic transport configuration in YAML format
content:
text/yaml:
schema:
type: string
examples:
'WebSocket config':
value: |
transport:
$type: tcpudp
tcp:
$type: shadowsocks
endpoint:
$type: websocket
url: wss://example.com/tcp
cipher: chacha20-ietf-poly1305
secret: XxXxXx
udp:
$type: shadowsocks
endpoint:
$type: websocket
url: wss://example.com/udp
cipher: chacha20-ietf-poly1305
secret: XxXxXx
'404':
description: Access key not found or has no WebSocket listeners
/access-keys/{id}/data-limit:
put:
description: Sets a data limit for the given access key

View file

@ -170,7 +170,7 @@ async function main() {
);
// Configure listener defaults (e.g., WebSocket paths/ports) based on server configuration.
const listenersConfig = serverConfig.data().listenersForNewAccessKeys;
const listenersConfig = serverConfig.data().listeners;
shadowsocksServer.configureListeners(
listenersConfig
? {
@ -239,7 +239,7 @@ async function main() {
try {
await caddyServer.applyConfig({
accessKeys: accessKeyRepository.listAccessKeys(),
listeners: serverConfig.data().listenersForNewAccessKeys,
listeners: serverConfig.data().listeners,
caddyConfig: serverConfig.data().caddyWebServer,
hostname: serverConfig.data().hostname,
apiPort: apiProxyEnabled ? apiPortNumber : undefined,

View file

@ -758,7 +758,7 @@ describe('ShadowsocksManagerService', () => {
});
});
describe('setListenersForNewAccessKeys', () => {
describe('setListeners', () => {
it('persists configuration and updates the Shadowsocks server', async () => {
const repo = getAccessKeyRepository();
const serverConfig = new InMemoryConfig({} as ServerConfigJson);
@ -785,9 +785,9 @@ describe('ShadowsocksManagerService', () => {
},
};
await service.setListenersForNewAccessKeys({params: listeners}, res, () => {});
await service.setListeners({params: listeners}, res, () => {});
expect(serverConfig.data().listenersForNewAccessKeys).toEqual(listeners);
expect(serverConfig.data().listeners).toEqual(listeners);
expect(fakeServer.getListenerSettings()).toEqual({
websocketStream: listeners.websocketStream,
websocketPacket: listeners.websocketPacket,
@ -814,11 +814,7 @@ describe('ShadowsocksManagerService', () => {
websocketStream: {path: '/tcp', webServerPort: 8080},
websocketPacket: {path: '/udp', webServerPort: 8080},
};
await service.setListenersForNewAccessKeys(
{params: listenersWithWebsocket},
{send: () => {}},
() => {}
);
await service.setListeners({params: listenersWithWebsocket}, {send: () => {}}, () => {});
expect(fakeServer.getListenerSettings()).toEqual({
websocketStream: listenersWithWebsocket.websocketStream,
websocketPacket: listenersWithWebsocket.websocketPacket,
@ -834,13 +830,9 @@ describe('ShadowsocksManagerService', () => {
responseProcessed = true;
},
};
await service.setListenersForNewAccessKeys(
{params: listenersWithoutWebsocket},
res,
() => {}
);
await service.setListeners({params: listenersWithoutWebsocket}, res, () => {});
expect(serverConfig.data().listenersForNewAccessKeys).toEqual(listenersWithoutWebsocket);
expect(serverConfig.data().listeners).toEqual(listenersWithoutWebsocket);
expect(fakeServer.getListenerSettings()).toBeUndefined();
expect(fakeCaddy.applyCalls.length).toEqual(2);
expect(fakeCaddy.applyCalls[1].listeners).toEqual(listenersWithoutWebsocket);

View file

@ -25,7 +25,7 @@ import * as errors from '../model/errors';
import * as version from './version';
import {ManagerMetrics} from './manager_metrics';
import {ServerConfigJson, ListenersForNewAccessKeys, WebServerConfig} from './server_config';
import {ServerConfigJson, ListenersConfig, WebServerConfig} from './server_config';
import {SharedMetricsPublisher} from './shared_metrics';
import {ShadowsocksServer} from '../model/shadowsocks_server';
import type {OutlineCaddyController} from './outline_caddy_server';
@ -135,10 +135,7 @@ export function bindService(
`${apiPrefix}/server/port-for-new-access-keys`,
service.setPortForNewAccessKeys.bind(service)
);
apiServer.put(
`${apiPrefix}/server/listeners-for-new-access-keys`,
service.setListenersForNewAccessKeys.bind(service)
);
apiServer.put(`${apiPrefix}/server/listeners`, service.setListeners.bind(service));
apiServer.put(`${apiPrefix}/server/web-server`, service.configureCaddyWebServer.bind(service));
apiServer.post(`${apiPrefix}/access-keys`, service.createNewAccessKey.bind(service));
@ -146,6 +143,14 @@ export function bindService(
apiServer.get(`${apiPrefix}/access-keys`, service.listAccessKeys.bind(service));
apiServer.get(`${apiPrefix}/access-keys/:id`, service.getAccessKey.bind(service));
apiServer.put(
`${apiPrefix}/access-keys/:id/listeners`,
service.setAccessKeyListeners.bind(service)
);
apiServer.get(
`${apiPrefix}/access-keys/:id/dynamic-config`,
service.getAccessKeyDynamicConfig.bind(service)
);
apiServer.del(`${apiPrefix}/access-keys/:id`, service.removeAccessKey.bind(service));
apiServer.put(`${apiPrefix}/access-keys/:id/name`, service.renameAccessKey.bind(service));
apiServer.put(
@ -320,7 +325,7 @@ export class ShadowsocksManagerService {
const domain = configData?.caddyWebServer?.domain || configData?.hostname;
if (domain) {
const listenersConfig = configData?.listenersForNewAccessKeys;
const listenersConfig = configData?.listeners;
// Cast to access generateDynamicAccessKeyConfig method
const serverWithDynamicConfig = this.shadowsocksServer as ShadowsocksServer & {
@ -443,72 +448,14 @@ export class ShadowsocksManagerService {
next();
}
// Get an access key
// Get an access key (always returns JSON)
getAccessKey(req: RequestType, res: ResponseType, next: restify.Next): void {
try {
logging.debug(`getAccessKey request ${JSON.stringify(req.params)}`);
const accessKeyId = validateAccessKeyId(req.params.id);
const accessKey = this.accessKeys.getAccessKey(accessKeyId);
// Check if this key uses WebSocket listeners
const hasWebSocketListeners =
accessKey.listeners &&
(accessKey.listeners.indexOf('websocket-stream') !== -1 ||
accessKey.listeners.indexOf('websocket-packet') !== -1);
if (hasWebSocketListeners) {
// Generate and return YAML for WebSocket keys
const configData = this.serverConfig.data();
const domain = configData?.caddyWebServer?.domain || configData?.hostname;
const listenersConfig = configData?.listenersForNewAccessKeys;
logging.debug(
`WebSocket key detected. Domain: ${domain}, Listeners config: ${JSON.stringify(
listenersConfig
)}`
);
// Generate YAML even if listenersConfig is not fully configured, using defaults
if (domain) {
const serverWithWebSocket = this.shadowsocksServer as ShadowsocksServer & {
generateDynamicAccessKeyYaml?: (
proxyParams: {encryptionMethod: string; password: string},
domain: string,
tcpPath: string,
udpPath: string,
tls: boolean,
listeners?: ListenerType[]
) => string | null;
};
const yamlConfig = serverWithWebSocket.generateDynamicAccessKeyYaml?.(
accessKey.proxyParams,
domain,
listenersConfig?.websocketStream?.path || '/tcp',
listenersConfig?.websocketPacket?.path || '/udp',
this.serverConfig.data()?.caddyWebServer?.autoHttps !== false,
accessKey.listeners as ListenerType[] | undefined
);
if (yamlConfig) {
// Return raw YAML for WebSocket keys
const nodeResponse = res as unknown as {
setHeader: (name: string, value: string) => void;
statusCode: number;
write: (data: string) => void;
end: () => void;
};
nodeResponse.setHeader('Content-Type', 'text/yaml; charset=utf-8');
nodeResponse.statusCode = HttpSuccess.OK;
nodeResponse.write(yamlConfig);
nodeResponse.end();
return;
}
}
}
// Return JSON for traditional (non-WebSocket) keys
// Always return JSON - use /access-keys/{id}/dynamic-config for YAML
const accessKeyJson = this.accessKeyToApiJson(accessKey);
logging.debug(`getAccessKey response ${JSON.stringify(accessKeyJson)}`);
res.send(HttpSuccess.OK, accessKeyJson);
@ -562,7 +509,7 @@ export class ShadowsocksManagerService {
}
} else {
// If no listeners specified, use default listeners based on server config
const serverListeners = this.serverConfig.data()?.listenersForNewAccessKeys;
const serverListeners = this.serverConfig.data()?.listeners;
if (serverListeners) {
listeners = [];
if (serverListeners.tcp) listeners.push('tcp');
@ -664,11 +611,11 @@ export class ShadowsocksManagerService {
if (configData) {
configData.portForNewAccessKeys = port;
// Also update listeners config for backward compatibility
if (!configData.listenersForNewAccessKeys) {
configData.listenersForNewAccessKeys = {};
if (!configData.listeners) {
configData.listeners = {};
}
configData.listenersForNewAccessKeys.tcp = {port};
configData.listenersForNewAccessKeys.udp = {port};
configData.listeners.tcp = {port};
configData.listeners.udp = {port};
}
this.serverConfig.write();
await this.updateCaddyConfig();
@ -687,16 +634,14 @@ export class ShadowsocksManagerService {
}
}
// Sets the listeners for new access keys
async setListenersForNewAccessKeys(
req: RequestType,
res: ResponseType,
next: restify.Next
): Promise<void> {
// Sets the listeners configuration (defaults for new keys, optionally updates all keys)
async setListeners(req: RequestType, res: ResponseType, next: restify.Next): Promise<void> {
try {
logging.debug(`setListenersForNewAccessKeys request ${JSON.stringify(req.params)}`);
logging.debug(`setListeners request ${JSON.stringify(req.params)}`);
const listeners = req.params as unknown as ListenersForNewAccessKeys;
const {applyToExisting, ...listeners} = req.params as unknown as ListenersConfig & {
applyToExisting?: boolean;
};
if (!listeners || typeof listeners !== 'object') {
return next(
new restifyErrors.InvalidArgumentError(
@ -807,7 +752,7 @@ export class ShadowsocksManagerService {
// Store the listeners configuration
const configData = this.serverConfig.data();
if (configData) {
configData.listenersForNewAccessKeys = listeners;
configData.listeners = listeners;
// Update legacy portForNewAccessKeys if TCP port is set
if (listeners.tcp?.port) {
@ -816,6 +761,17 @@ export class ShadowsocksManagerService {
}
}
// If applyToExisting is true, update all existing keys
if (applyToExisting) {
const derivedListeners: ListenerType[] = [];
if (listeners.tcp) derivedListeners.push('tcp');
if (listeners.udp) derivedListeners.push('udp');
if (listeners.websocketStream) derivedListeners.push('websocket-stream');
if (listeners.websocketPacket) derivedListeners.push('websocket-packet');
this.accessKeys.setListenersForAllKeys(derivedListeners);
}
// Update the underlying Shadowsocks server with the new listener defaults.
const listenerSettings =
listeners.websocketStream || listeners.websocketPacket
@ -914,6 +870,119 @@ export class ShadowsocksManagerService {
}
}
// Updates listeners for a specific access key
async setAccessKeyListeners(
req: RequestType,
res: ResponseType,
next: restify.Next
): Promise<void> {
try {
logging.debug(`setAccessKeyListeners request ${JSON.stringify(req.params)}`);
const accessKeyId = validateAccessKeyId(req.params.id);
const listenersParam = req.params.listeners as string[] | undefined;
if (!listenersParam || !Array.isArray(listenersParam)) {
return next(
new restifyErrors.InvalidArgumentError({statusCode: 400}, 'listeners must be an array')
);
}
const validListeners = ['tcp', 'udp', 'websocket-stream', 'websocket-packet'];
for (const listener of listenersParam) {
if (!validListeners.includes(listener)) {
return next(
new restifyErrors.InvalidArgumentError(
{statusCode: 400},
`Invalid listener type: ${listener}`
)
);
}
}
this.accessKeys.setAccessKeyListeners(accessKeyId, listenersParam as ListenerType[]);
await this.updateCaddyConfig();
res.send(HttpSuccess.NO_CONTENT);
return next();
} catch (error) {
logging.error(error);
if (error instanceof errors.AccessKeyNotFound) {
return next(new restifyErrors.NotFoundError(error.message));
}
return next(new restifyErrors.InternalServerError());
}
}
// Returns dynamic config YAML for WebSocket-enabled keys
getAccessKeyDynamicConfig(req: RequestType, res: ResponseType, next: restify.Next): void {
try {
logging.debug(`getAccessKeyDynamicConfig request ${JSON.stringify(req.params)}`);
const accessKeyId = validateAccessKeyId(req.params.id);
const accessKey = this.accessKeys.getAccessKey(accessKeyId);
const hasWebSocketListeners =
accessKey.listeners &&
(accessKey.listeners.includes('websocket-stream') ||
accessKey.listeners.includes('websocket-packet'));
if (!hasWebSocketListeners) {
return next(
new restifyErrors.NotFoundError('Access key has no WebSocket listeners configured')
);
}
const configData = this.serverConfig.data();
const domain = configData?.caddyWebServer?.domain || configData?.hostname;
if (!domain) {
return next(
new restifyErrors.InternalServerError('No domain configured for WebSocket URLs')
);
}
const listenersConfig = configData?.listeners;
const serverWithWebSocket = this.shadowsocksServer as ShadowsocksServer & {
generateDynamicAccessKeyYaml?: (
proxyParams: {encryptionMethod: string; password: string},
domain: string,
tcpPath: string,
udpPath: string,
tls: boolean,
listeners?: ListenerType[]
) => string | null;
};
const yamlConfig = serverWithWebSocket.generateDynamicAccessKeyYaml?.(
accessKey.proxyParams,
domain,
listenersConfig?.websocketStream?.path || '/tcp',
listenersConfig?.websocketPacket?.path || '/udp',
configData?.caddyWebServer?.autoHttps !== false,
accessKey.listeners
);
if (!yamlConfig) {
return next(new restifyErrors.InternalServerError('Failed to generate dynamic config'));
}
const nodeResponse = res as unknown as {
setHeader: (name: string, value: string) => void;
statusCode: number;
write: (data: string) => void;
end: () => void;
};
nodeResponse.setHeader('Content-Type', 'text/yaml; charset=utf-8');
nodeResponse.statusCode = HttpSuccess.OK;
nodeResponse.write(yamlConfig);
nodeResponse.end();
} catch (error) {
logging.error(error);
if (error instanceof errors.AccessKeyNotFound) {
return next(new restifyErrors.NotFoundError(error.message));
}
return next(error);
}
}
// Removes an existing access key
async removeAccessKey(req: RequestType, res: ResponseType, next: restify.Next): Promise<void> {
try {
@ -1134,7 +1203,7 @@ export class ShadowsocksManagerService {
const apiPort = configData.caddyWebServer?.apiProxyPath ? apiPortNumber : undefined;
await this.caddyServer.applyConfig({
accessKeys: this.accessKeys.listAccessKeys(),
listeners: configData.listenersForNewAccessKeys,
listeners: configData.listeners,
caddyConfig: configData.caddyWebServer,
hostname: configData.hostname,
apiPort,

View file

@ -21,11 +21,11 @@ import * as yaml from 'js-yaml';
import * as file from '../infrastructure/file';
import * as logging from '../infrastructure/logging';
import {AccessKey, ListenerType} from '../model/access_key';
import {WebServerConfig, ListenerConfig, ListenersForNewAccessKeys} from './server_config';
import {WebServerConfig, ListenerConfig, ListenersConfig} from './server_config';
export interface OutlineCaddyConfigPayload {
accessKeys: AccessKey[];
listeners?: ListenersForNewAccessKeys;
listeners?: ListenersConfig;
caddyConfig?: WebServerConfig;
hostname?: string;
apiPort?: number; // Internal API port for reverse proxy

View file

@ -305,6 +305,20 @@ export class ServerAccessKeyRepository implements AccessKeyRepository {
this.enforceAccessKeyDataLimits();
}
setAccessKeyListeners(id: AccessKeyId, listeners: ListenerType[]): void {
this.getAccessKey(id).listeners = listeners;
this.saveAccessKeys();
this.updateServer();
}
setListenersForAllKeys(listeners: ListenerType[]): void {
for (const accessKey of this.accessKeys) {
accessKey.listeners = listeners;
}
this.saveAccessKeys();
this.updateServer();
}
// Compares access key usage with collected metrics, marking them as under or over limit.
// Updates access key data usage.
async enforceAccessKeyDataLimits() {

View file

@ -24,7 +24,7 @@ export interface ListenerConfig {
webServerPort?: number;
}
export interface ListenersForNewAccessKeys {
export interface ListenersConfig {
tcp?: ListenerConfig;
udp?: ListenerConfig;
websocketStream?: ListenerConfig;
@ -53,8 +53,8 @@ export interface ServerConfigJson {
createdTimestampMs?: number;
// What port number should we use for new access keys?
portForNewAccessKeys?: number;
// Listeners configuration for new access keys (supersedes portForNewAccessKeys)
listenersForNewAccessKeys?: ListenersForNewAccessKeys;
// Listeners configuration for access keys
listeners?: ListenersConfig;
// Caddy web server configuration for automatic HTTPS
caddyWebServer?: WebServerConfig;
// Which staged rollouts we should force enabled or disabled.