@serve.zone/dcrouter 8.0.0 → 9.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/dist_serve/bundle.js +2420 -1227
  2. package/dist_ts/00_commitinfo_data.js +1 -1
  3. package/dist_ts/classes.dcrouter.d.ts +9 -0
  4. package/dist_ts/classes.dcrouter.js +27 -1
  5. package/dist_ts/config/classes.api-token-manager.d.ts +38 -0
  6. package/dist_ts/config/classes.api-token-manager.js +134 -0
  7. package/dist_ts/config/classes.route-config-manager.d.ts +35 -0
  8. package/dist_ts/config/classes.route-config-manager.js +231 -0
  9. package/dist_ts/config/index.d.ts +2 -0
  10. package/dist_ts/config/index.js +3 -1
  11. package/dist_ts/opsserver/classes.opsserver.d.ts +2 -0
  12. package/dist_ts/opsserver/classes.opsserver.js +5 -1
  13. package/dist_ts/opsserver/handlers/{config.handler.d.ts → api-token.handler.d.ts} +5 -2
  14. package/dist_ts/opsserver/handlers/api-token.handler.js +66 -0
  15. package/dist_ts/opsserver/handlers/index.d.ts +2 -0
  16. package/dist_ts/opsserver/handlers/index.js +3 -1
  17. package/dist_ts/opsserver/handlers/route-management.handler.d.ts +13 -0
  18. package/dist_ts/opsserver/handlers/route-management.handler.js +117 -0
  19. package/dist_ts_interfaces/data/index.d.ts +1 -0
  20. package/dist_ts_interfaces/data/index.js +2 -1
  21. package/dist_ts_interfaces/data/route-management.d.ts +68 -0
  22. package/dist_ts_interfaces/data/route-management.js +2 -0
  23. package/dist_ts_interfaces/requests/api-tokens.d.ts +63 -0
  24. package/dist_ts_interfaces/requests/api-tokens.js +2 -0
  25. package/dist_ts_interfaces/requests/config.d.ts +77 -1
  26. package/dist_ts_interfaces/requests/index.d.ts +2 -0
  27. package/dist_ts_interfaces/requests/index.js +3 -1
  28. package/dist_ts_interfaces/requests/route-management.d.ts +114 -0
  29. package/dist_ts_interfaces/requests/route-management.js +2 -0
  30. package/dist_ts_web/00_commitinfo_data.js +1 -1
  31. package/dist_ts_web/appstate.d.ts +37 -1
  32. package/dist_ts_web/appstate.js +220 -2
  33. package/dist_ts_web/elements/index.d.ts +2 -0
  34. package/dist_ts_web/elements/index.js +3 -1
  35. package/dist_ts_web/elements/ops-dashboard.js +23 -3
  36. package/dist_ts_web/elements/ops-view-apitokens.d.ts +12 -0
  37. package/dist_ts_web/elements/ops-view-apitokens.js +310 -0
  38. package/dist_ts_web/elements/ops-view-config.d.ts +10 -8
  39. package/dist_ts_web/elements/ops-view-config.js +215 -297
  40. package/dist_ts_web/elements/ops-view-routes.d.ts +12 -0
  41. package/dist_ts_web/elements/ops-view-routes.js +404 -0
  42. package/dist_ts_web/router.d.ts +1 -1
  43. package/dist_ts_web/router.js +2 -2
  44. package/package.json +2 -2
  45. package/ts/00_commitinfo_data.ts +1 -1
  46. package/ts/classes.dcrouter.ts +37 -1
  47. package/ts/config/classes.api-token-manager.ts +155 -0
  48. package/ts/config/classes.route-config-manager.ts +271 -0
  49. package/ts/config/index.ts +3 -1
  50. package/ts/opsserver/classes.opsserver.ts +4 -0
  51. package/ts/opsserver/handlers/api-token.handler.ts +96 -0
  52. package/ts/opsserver/handlers/config.handler.ts +154 -72
  53. package/ts/opsserver/handlers/index.ts +3 -1
  54. package/ts/opsserver/handlers/route-management.handler.ts +163 -0
  55. package/ts_web/00_commitinfo_data.ts +1 -1
  56. package/ts_web/appstate.ts +309 -2
  57. package/ts_web/elements/index.ts +2 -0
  58. package/ts_web/elements/ops-dashboard.ts +22 -2
  59. package/ts_web/elements/ops-view-apitokens.ts +285 -0
  60. package/ts_web/elements/ops-view-config.ts +237 -299
  61. package/ts_web/elements/ops-view-routes.ts +389 -0
  62. package/ts_web/router.ts +1 -1
  63. package/dist_ts/cache/classes.cache.cleaner.d.ts +0 -47
  64. package/dist_ts/cache/classes.cache.cleaner.js +0 -130
  65. package/dist_ts/cache/classes.cached.document.d.ts +0 -76
  66. package/dist_ts/cache/classes.cached.document.js +0 -100
  67. package/dist_ts/cache/classes.cachedb.d.ts +0 -60
  68. package/dist_ts/cache/classes.cachedb.js +0 -126
  69. package/dist_ts/cache/documents/classes.cached.email.d.ts +0 -125
  70. package/dist_ts/cache/documents/classes.cached.email.js +0 -337
  71. package/dist_ts/cache/documents/classes.cached.ip.reputation.d.ts +0 -119
  72. package/dist_ts/cache/documents/classes.cached.ip.reputation.js +0 -323
  73. package/dist_ts/cache/documents/index.d.ts +0 -2
  74. package/dist_ts/cache/documents/index.js +0 -3
  75. package/dist_ts/cache/index.d.ts +0 -4
  76. package/dist_ts/cache/index.js +0 -7
  77. package/dist_ts/monitoring/classes.metricscache.d.ts +0 -32
  78. package/dist_ts/monitoring/classes.metricscache.js +0 -63
  79. package/dist_ts/opsserver/handlers/admin.handler.d.ts +0 -31
  80. package/dist_ts/opsserver/handlers/admin.handler.js +0 -180
  81. package/dist_ts/opsserver/handlers/config.handler.js +0 -67
  82. package/dist_ts/opsserver/handlers/logs.handler.d.ts +0 -17
  83. package/dist_ts/opsserver/handlers/logs.handler.js +0 -215
  84. package/dist_ts/security/classes.securitylogger.js +0 -235
  85. package/dist_ts/storage/classes.storagemanager.d.ts +0 -82
  86. package/dist_ts/storage/classes.storagemanager.js +0 -344
  87. package/dist_ts/storage/index.d.ts +0 -1
  88. package/dist_ts/storage/index.js +0 -3
@@ -0,0 +1,155 @@
1
+ import * as plugins from '../plugins.js';
2
+ import { logger } from '../logger.js';
3
+ import type { StorageManager } from '../storage/index.js';
4
+ import type {
5
+ IStoredApiToken,
6
+ IApiTokenInfo,
7
+ TApiTokenScope,
8
+ } from '../../ts_interfaces/data/route-management.js';
9
+
10
+ const TOKENS_PREFIX = '/config-api/tokens/';
11
+ const TOKEN_PREFIX_STR = 'dcr_';
12
+
13
+ export class ApiTokenManager {
14
+ private tokens = new Map<string, IStoredApiToken>();
15
+
16
+ constructor(private storageManager: StorageManager) {}
17
+
18
+ public async initialize(): Promise<void> {
19
+ await this.loadTokens();
20
+ if (this.tokens.size > 0) {
21
+ logger.log('info', `Loaded ${this.tokens.size} API token(s) from storage`);
22
+ }
23
+ }
24
+
25
+ // =========================================================================
26
+ // Token lifecycle
27
+ // =========================================================================
28
+
29
+ /**
30
+ * Create a new API token. Returns the raw token value (shown once).
31
+ */
32
+ public async createToken(
33
+ name: string,
34
+ scopes: TApiTokenScope[],
35
+ expiresInDays: number | null,
36
+ createdBy: string,
37
+ ): Promise<{ id: string; rawToken: string }> {
38
+ const id = plugins.uuid.v4();
39
+ const randomBytes = plugins.crypto.randomBytes(32);
40
+ const rawPayload = `${id}:${randomBytes.toString('base64url')}`;
41
+ const rawToken = `${TOKEN_PREFIX_STR}${rawPayload}`;
42
+
43
+ const tokenHash = plugins.crypto.createHash('sha256').update(rawToken).digest('hex');
44
+
45
+ const now = Date.now();
46
+ const stored: IStoredApiToken = {
47
+ id,
48
+ name,
49
+ tokenHash,
50
+ scopes,
51
+ createdAt: now,
52
+ expiresAt: expiresInDays != null ? now + expiresInDays * 86400000 : null,
53
+ lastUsedAt: null,
54
+ createdBy,
55
+ enabled: true,
56
+ };
57
+
58
+ this.tokens.set(id, stored);
59
+ await this.persistToken(stored);
60
+ logger.log('info', `API token '${name}' created (id: ${id})`);
61
+ return { id, rawToken };
62
+ }
63
+
64
+ /**
65
+ * Validate a raw token string. Returns the stored token if valid, null otherwise.
66
+ * Also updates lastUsedAt.
67
+ */
68
+ public async validateToken(rawToken: string): Promise<IStoredApiToken | null> {
69
+ if (!rawToken.startsWith(TOKEN_PREFIX_STR)) return null;
70
+
71
+ const hash = plugins.crypto.createHash('sha256').update(rawToken).digest('hex');
72
+
73
+ for (const stored of this.tokens.values()) {
74
+ if (stored.tokenHash === hash) {
75
+ if (!stored.enabled) return null;
76
+ if (stored.expiresAt !== null && stored.expiresAt < Date.now()) return null;
77
+
78
+ // Update lastUsedAt (fire and forget)
79
+ stored.lastUsedAt = Date.now();
80
+ this.persistToken(stored).catch(() => {});
81
+ return stored;
82
+ }
83
+ }
84
+ return null;
85
+ }
86
+
87
+ /**
88
+ * Check if a token has a specific scope.
89
+ */
90
+ public hasScope(token: IStoredApiToken, scope: TApiTokenScope): boolean {
91
+ return token.scopes.includes(scope);
92
+ }
93
+
94
+ /**
95
+ * List all tokens (safe info only, no hashes).
96
+ */
97
+ public listTokens(): IApiTokenInfo[] {
98
+ const result: IApiTokenInfo[] = [];
99
+ for (const stored of this.tokens.values()) {
100
+ result.push({
101
+ id: stored.id,
102
+ name: stored.name,
103
+ scopes: stored.scopes,
104
+ createdAt: stored.createdAt,
105
+ expiresAt: stored.expiresAt,
106
+ lastUsedAt: stored.lastUsedAt,
107
+ enabled: stored.enabled,
108
+ });
109
+ }
110
+ return result;
111
+ }
112
+
113
+ /**
114
+ * Revoke (delete) a token.
115
+ */
116
+ public async revokeToken(id: string): Promise<boolean> {
117
+ if (!this.tokens.has(id)) return false;
118
+ const token = this.tokens.get(id)!;
119
+ this.tokens.delete(id);
120
+ await this.storageManager.delete(`${TOKENS_PREFIX}${id}.json`);
121
+ logger.log('info', `API token '${token.name}' revoked (id: ${id})`);
122
+ return true;
123
+ }
124
+
125
+ /**
126
+ * Enable or disable a token.
127
+ */
128
+ public async toggleToken(id: string, enabled: boolean): Promise<boolean> {
129
+ const stored = this.tokens.get(id);
130
+ if (!stored) return false;
131
+ stored.enabled = enabled;
132
+ await this.persistToken(stored);
133
+ logger.log('info', `API token '${stored.name}' ${enabled ? 'enabled' : 'disabled'} (id: ${id})`);
134
+ return true;
135
+ }
136
+
137
+ // =========================================================================
138
+ // Private
139
+ // =========================================================================
140
+
141
+ private async loadTokens(): Promise<void> {
142
+ const keys = await this.storageManager.list(TOKENS_PREFIX);
143
+ for (const key of keys) {
144
+ if (!key.endsWith('.json')) continue;
145
+ const stored = await this.storageManager.getJSON<IStoredApiToken>(key);
146
+ if (stored?.id) {
147
+ this.tokens.set(stored.id, stored);
148
+ }
149
+ }
150
+ }
151
+
152
+ private async persistToken(stored: IStoredApiToken): Promise<void> {
153
+ await this.storageManager.setJSON(`${TOKENS_PREFIX}${stored.id}.json`, stored);
154
+ }
155
+ }
@@ -0,0 +1,271 @@
1
+ import * as plugins from '../plugins.js';
2
+ import { logger } from '../logger.js';
3
+ import type { StorageManager } from '../storage/index.js';
4
+ import type {
5
+ IStoredRoute,
6
+ IRouteOverride,
7
+ IMergedRoute,
8
+ IRouteWarning,
9
+ } from '../../ts_interfaces/data/route-management.js';
10
+
11
+ const ROUTES_PREFIX = '/config-api/routes/';
12
+ const OVERRIDES_PREFIX = '/config-api/overrides/';
13
+
14
+ export class RouteConfigManager {
15
+ private storedRoutes = new Map<string, IStoredRoute>();
16
+ private overrides = new Map<string, IRouteOverride>();
17
+ private warnings: IRouteWarning[] = [];
18
+
19
+ constructor(
20
+ private storageManager: StorageManager,
21
+ private getHardcodedRoutes: () => plugins.smartproxy.IRouteConfig[],
22
+ private getSmartProxy: () => plugins.smartproxy.SmartProxy | undefined,
23
+ ) {}
24
+
25
+ /**
26
+ * Load persisted routes and overrides, compute warnings, apply to SmartProxy.
27
+ */
28
+ public async initialize(): Promise<void> {
29
+ await this.loadStoredRoutes();
30
+ await this.loadOverrides();
31
+ this.computeWarnings();
32
+ this.logWarnings();
33
+ await this.applyRoutes();
34
+ }
35
+
36
+ // =========================================================================
37
+ // Merged view
38
+ // =========================================================================
39
+
40
+ public getMergedRoutes(): { routes: IMergedRoute[]; warnings: IRouteWarning[] } {
41
+ const merged: IMergedRoute[] = [];
42
+
43
+ // Hardcoded routes
44
+ for (const route of this.getHardcodedRoutes()) {
45
+ const name = route.name || '';
46
+ const override = this.overrides.get(name);
47
+ merged.push({
48
+ route,
49
+ source: 'hardcoded',
50
+ enabled: override ? override.enabled : true,
51
+ overridden: !!override,
52
+ });
53
+ }
54
+
55
+ // Programmatic routes
56
+ for (const stored of this.storedRoutes.values()) {
57
+ merged.push({
58
+ route: stored.route,
59
+ source: 'programmatic',
60
+ enabled: stored.enabled,
61
+ overridden: false,
62
+ storedRouteId: stored.id,
63
+ createdAt: stored.createdAt,
64
+ updatedAt: stored.updatedAt,
65
+ });
66
+ }
67
+
68
+ return { routes: merged, warnings: [...this.warnings] };
69
+ }
70
+
71
+ // =========================================================================
72
+ // Programmatic route CRUD
73
+ // =========================================================================
74
+
75
+ public async createRoute(
76
+ route: plugins.smartproxy.IRouteConfig,
77
+ createdBy: string,
78
+ enabled = true,
79
+ ): Promise<string> {
80
+ const id = plugins.uuid.v4();
81
+ const now = Date.now();
82
+
83
+ // Ensure route has a name
84
+ if (!route.name) {
85
+ route.name = `programmatic-${id.slice(0, 8)}`;
86
+ }
87
+
88
+ const stored: IStoredRoute = {
89
+ id,
90
+ route,
91
+ enabled,
92
+ createdAt: now,
93
+ updatedAt: now,
94
+ createdBy,
95
+ };
96
+
97
+ this.storedRoutes.set(id, stored);
98
+ await this.persistRoute(stored);
99
+ await this.applyRoutes();
100
+ return id;
101
+ }
102
+
103
+ public async updateRoute(
104
+ id: string,
105
+ patch: { route?: Partial<plugins.smartproxy.IRouteConfig>; enabled?: boolean },
106
+ ): Promise<boolean> {
107
+ const stored = this.storedRoutes.get(id);
108
+ if (!stored) return false;
109
+
110
+ if (patch.route) {
111
+ stored.route = { ...stored.route, ...patch.route } as plugins.smartproxy.IRouteConfig;
112
+ }
113
+ if (patch.enabled !== undefined) {
114
+ stored.enabled = patch.enabled;
115
+ }
116
+ stored.updatedAt = Date.now();
117
+
118
+ await this.persistRoute(stored);
119
+ await this.applyRoutes();
120
+ return true;
121
+ }
122
+
123
+ public async deleteRoute(id: string): Promise<boolean> {
124
+ if (!this.storedRoutes.has(id)) return false;
125
+ this.storedRoutes.delete(id);
126
+ await this.storageManager.delete(`${ROUTES_PREFIX}${id}.json`);
127
+ await this.applyRoutes();
128
+ return true;
129
+ }
130
+
131
+ public async toggleRoute(id: string, enabled: boolean): Promise<boolean> {
132
+ return this.updateRoute(id, { enabled });
133
+ }
134
+
135
+ // =========================================================================
136
+ // Hardcoded route overrides
137
+ // =========================================================================
138
+
139
+ public async setOverride(routeName: string, enabled: boolean, updatedBy: string): Promise<void> {
140
+ const override: IRouteOverride = {
141
+ routeName,
142
+ enabled,
143
+ updatedAt: Date.now(),
144
+ updatedBy,
145
+ };
146
+ this.overrides.set(routeName, override);
147
+ await this.storageManager.setJSON(`${OVERRIDES_PREFIX}${routeName}.json`, override);
148
+ this.computeWarnings();
149
+ await this.applyRoutes();
150
+ }
151
+
152
+ public async removeOverride(routeName: string): Promise<boolean> {
153
+ if (!this.overrides.has(routeName)) return false;
154
+ this.overrides.delete(routeName);
155
+ await this.storageManager.delete(`${OVERRIDES_PREFIX}${routeName}.json`);
156
+ this.computeWarnings();
157
+ await this.applyRoutes();
158
+ return true;
159
+ }
160
+
161
+ // =========================================================================
162
+ // Private: persistence
163
+ // =========================================================================
164
+
165
+ private async loadStoredRoutes(): Promise<void> {
166
+ const keys = await this.storageManager.list(ROUTES_PREFIX);
167
+ for (const key of keys) {
168
+ if (!key.endsWith('.json')) continue;
169
+ const stored = await this.storageManager.getJSON<IStoredRoute>(key);
170
+ if (stored?.id) {
171
+ this.storedRoutes.set(stored.id, stored);
172
+ }
173
+ }
174
+ if (this.storedRoutes.size > 0) {
175
+ logger.log('info', `Loaded ${this.storedRoutes.size} programmatic route(s) from storage`);
176
+ }
177
+ }
178
+
179
+ private async loadOverrides(): Promise<void> {
180
+ const keys = await this.storageManager.list(OVERRIDES_PREFIX);
181
+ for (const key of keys) {
182
+ if (!key.endsWith('.json')) continue;
183
+ const override = await this.storageManager.getJSON<IRouteOverride>(key);
184
+ if (override?.routeName) {
185
+ this.overrides.set(override.routeName, override);
186
+ }
187
+ }
188
+ if (this.overrides.size > 0) {
189
+ logger.log('info', `Loaded ${this.overrides.size} route override(s) from storage`);
190
+ }
191
+ }
192
+
193
+ private async persistRoute(stored: IStoredRoute): Promise<void> {
194
+ await this.storageManager.setJSON(`${ROUTES_PREFIX}${stored.id}.json`, stored);
195
+ }
196
+
197
+ // =========================================================================
198
+ // Private: warnings
199
+ // =========================================================================
200
+
201
+ private computeWarnings(): void {
202
+ this.warnings = [];
203
+ const hardcodedNames = new Set(this.getHardcodedRoutes().map((r) => r.name || ''));
204
+
205
+ // Check overrides
206
+ for (const [routeName, override] of this.overrides) {
207
+ if (!hardcodedNames.has(routeName)) {
208
+ this.warnings.push({
209
+ type: 'orphaned-override',
210
+ routeName,
211
+ message: `Orphaned override for route '${routeName}' — hardcoded route no longer exists`,
212
+ });
213
+ } else if (!override.enabled) {
214
+ this.warnings.push({
215
+ type: 'disabled-hardcoded',
216
+ routeName,
217
+ message: `Route '${routeName}' is disabled via API override`,
218
+ });
219
+ }
220
+ }
221
+
222
+ // Check disabled programmatic routes
223
+ for (const stored of this.storedRoutes.values()) {
224
+ if (!stored.enabled) {
225
+ const name = stored.route.name || stored.id;
226
+ this.warnings.push({
227
+ type: 'disabled-programmatic',
228
+ routeName: name,
229
+ message: `Programmatic route '${name}' (id: ${stored.id}) is disabled`,
230
+ });
231
+ }
232
+ }
233
+ }
234
+
235
+ private logWarnings(): void {
236
+ for (const w of this.warnings) {
237
+ logger.log('warn', w.message);
238
+ }
239
+ }
240
+
241
+ // =========================================================================
242
+ // Private: apply merged routes to SmartProxy
243
+ // =========================================================================
244
+
245
+ private async applyRoutes(): Promise<void> {
246
+ const smartProxy = this.getSmartProxy();
247
+ if (!smartProxy) return;
248
+
249
+ const enabledRoutes: plugins.smartproxy.IRouteConfig[] = [];
250
+
251
+ // Add enabled hardcoded routes (respecting overrides)
252
+ for (const route of this.getHardcodedRoutes()) {
253
+ const name = route.name || '';
254
+ const override = this.overrides.get(name);
255
+ if (override && !override.enabled) {
256
+ continue; // Skip disabled hardcoded route
257
+ }
258
+ enabledRoutes.push(route);
259
+ }
260
+
261
+ // Add enabled programmatic routes
262
+ for (const stored of this.storedRoutes.values()) {
263
+ if (stored.enabled) {
264
+ enabledRoutes.push(stored.route);
265
+ }
266
+ }
267
+
268
+ await smartProxy.updateRoutes(enabledRoutes);
269
+ logger.log('info', `Applied ${enabledRoutes.length} routes to SmartProxy (${this.storedRoutes.size} programmatic, ${this.overrides.size} overrides)`);
270
+ }
271
+ }
@@ -1,2 +1,4 @@
1
1
  // Export validation tools only
2
- export * from './validator.js';
2
+ export * from './validator.js';
3
+ export { RouteConfigManager } from './classes.route-config-manager.js';
4
+ export { ApiTokenManager } from './classes.api-token-manager.js';
@@ -20,6 +20,8 @@ export class OpsServer {
20
20
  private emailOpsHandler: handlers.EmailOpsHandler;
21
21
  private certificateHandler: handlers.CertificateHandler;
22
22
  private remoteIngressHandler: handlers.RemoteIngressHandler;
23
+ private routeManagementHandler: handlers.RouteManagementHandler;
24
+ private apiTokenHandler: handlers.ApiTokenHandler;
23
25
 
24
26
  constructor(dcRouterRefArg: DcRouter) {
25
27
  this.dcRouterRef = dcRouterRefArg;
@@ -61,6 +63,8 @@ export class OpsServer {
61
63
  this.emailOpsHandler = new handlers.EmailOpsHandler(this);
62
64
  this.certificateHandler = new handlers.CertificateHandler(this);
63
65
  this.remoteIngressHandler = new handlers.RemoteIngressHandler(this);
66
+ this.routeManagementHandler = new handlers.RouteManagementHandler(this);
67
+ this.apiTokenHandler = new handlers.ApiTokenHandler(this);
64
68
 
65
69
  console.log('✅ OpsServer TypedRequest handlers initialized');
66
70
  }
@@ -0,0 +1,96 @@
1
+ import * as plugins from '../../plugins.js';
2
+ import type { OpsServer } from '../classes.opsserver.js';
3
+ import * as interfaces from '../../../ts_interfaces/index.js';
4
+
5
+ export class ApiTokenHandler {
6
+ public typedrouter = new plugins.typedrequest.TypedRouter();
7
+
8
+ constructor(private opsServerRef: OpsServer) {
9
+ this.opsServerRef.typedrouter.addTypedRouter(this.typedrouter);
10
+ this.registerHandlers();
11
+ }
12
+
13
+ /**
14
+ * Token management requires admin JWT only (tokens cannot manage tokens).
15
+ */
16
+ private async requireAdmin(identity?: interfaces.data.IIdentity): Promise<string> {
17
+ if (!identity?.jwt) {
18
+ throw new plugins.typedrequest.TypedResponseError('unauthorized');
19
+ }
20
+ const isAdmin = await this.opsServerRef.adminHandler.adminIdentityGuard.exec({ identity });
21
+ if (!isAdmin) {
22
+ throw new plugins.typedrequest.TypedResponseError('admin access required');
23
+ }
24
+ return identity.userId;
25
+ }
26
+
27
+ private registerHandlers(): void {
28
+ // Create API token
29
+ this.typedrouter.addTypedHandler(
30
+ new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_CreateApiToken>(
31
+ 'createApiToken',
32
+ async (dataArg) => {
33
+ const userId = await this.requireAdmin(dataArg.identity);
34
+ const manager = this.opsServerRef.dcRouterRef.apiTokenManager;
35
+ if (!manager) {
36
+ return { success: false, message: 'Token management not initialized' };
37
+ }
38
+ const result = await manager.createToken(
39
+ dataArg.name,
40
+ dataArg.scopes,
41
+ dataArg.expiresInDays ?? null,
42
+ userId,
43
+ );
44
+ return { success: true, tokenId: result.id, tokenValue: result.rawToken };
45
+ },
46
+ ),
47
+ );
48
+
49
+ // List API tokens
50
+ this.typedrouter.addTypedHandler(
51
+ new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ListApiTokens>(
52
+ 'listApiTokens',
53
+ async (dataArg) => {
54
+ await this.requireAdmin(dataArg.identity);
55
+ const manager = this.opsServerRef.dcRouterRef.apiTokenManager;
56
+ if (!manager) {
57
+ return { tokens: [] };
58
+ }
59
+ return { tokens: manager.listTokens() };
60
+ },
61
+ ),
62
+ );
63
+
64
+ // Revoke API token
65
+ this.typedrouter.addTypedHandler(
66
+ new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_RevokeApiToken>(
67
+ 'revokeApiToken',
68
+ async (dataArg) => {
69
+ await this.requireAdmin(dataArg.identity);
70
+ const manager = this.opsServerRef.dcRouterRef.apiTokenManager;
71
+ if (!manager) {
72
+ return { success: false, message: 'Token management not initialized' };
73
+ }
74
+ const ok = await manager.revokeToken(dataArg.id);
75
+ return { success: ok, message: ok ? undefined : 'Token not found' };
76
+ },
77
+ ),
78
+ );
79
+
80
+ // Toggle API token
81
+ this.typedrouter.addTypedHandler(
82
+ new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ToggleApiToken>(
83
+ 'toggleApiToken',
84
+ async (dataArg) => {
85
+ await this.requireAdmin(dataArg.identity);
86
+ const manager = this.opsServerRef.dcRouterRef.apiTokenManager;
87
+ if (!manager) {
88
+ return { success: false, message: 'Token management not initialized' };
89
+ }
90
+ const ok = await manager.toggleToken(dataArg.id, dataArg.enabled);
91
+ return { success: ok, message: ok ? undefined : 'Token not found' };
92
+ },
93
+ ),
94
+ );
95
+ }
96
+ }