import * as plugins from '../../plugins.js'; import type { OpsServer } from '../classes.opsserver.js'; import * as interfaces from '../../../ts_interfaces/index.js'; export class WorkHosterHandler { public typedrouter = new plugins.typedrequest.TypedRouter(); constructor(private opsServerRef: OpsServer) { this.opsServerRef.typedrouter.addTypedRouter(this.typedrouter); this.registerHandlers(); } private async requireAuth( request: { identity?: interfaces.data.IIdentity; apiToken?: string }, requiredScope?: interfaces.data.TApiTokenScope, ): Promise { if (request.identity?.jwt) { try { const isAdmin = await this.opsServerRef.adminHandler.adminIdentityGuard.exec({ identity: request.identity, }); if (isAdmin) return request.identity.userId; } catch { /* fall through */ } } if (request.apiToken) { const tokenManager = this.opsServerRef.dcRouterRef.apiTokenManager; if (tokenManager) { const token = await tokenManager.validateToken(request.apiToken); if (token) { if (!requiredScope || tokenManager.hasScope(token, requiredScope)) { return token.createdBy; } throw new plugins.typedrequest.TypedResponseError('insufficient scope'); } } } throw new plugins.typedrequest.TypedResponseError('unauthorized'); } private registerHandlers(): void { this.typedrouter.addTypedHandler( new plugins.typedrequest.TypedHandler( 'getGatewayCapabilities', async (dataArg) => { await this.requireAuth(dataArg, 'workhosters:read'); return { capabilities: this.getGatewayCapabilities() }; }, ), ); this.typedrouter.addTypedHandler( new plugins.typedrequest.TypedHandler( 'getWorkHosterDomains', async (dataArg) => { await this.requireAuth(dataArg, 'workhosters:read'); const dnsManager = this.opsServerRef.dcRouterRef.dnsManager; if (!dnsManager) return { domains: [] }; const docs = await dnsManager.listDomains(); const domains = docs.map((domainDoc) => { const domain = dnsManager.toPublicDomain(domainDoc); const canManageDnsRecords = domain.source === 'dcrouter' || Boolean(domain.providerId); return { ...domain, capabilities: { canCreateSubdomains: canManageDnsRecords, canManageDnsRecords, canIssueCertificates: Boolean(this.opsServerRef.dcRouterRef.smartProxy), canHostEmail: Boolean(this.opsServerRef.dcRouterRef.emailDomainManager), }, } satisfies interfaces.data.IWorkHosterDomain; }); return { domains }; }, ), ); this.typedrouter.addTypedHandler( new plugins.typedrequest.TypedHandler( 'syncWorkAppRoute', async (dataArg) => { const userId = await this.requireAuth(dataArg, 'workhosters:write'); const manager = this.opsServerRef.dcRouterRef.routeConfigManager; if (!manager) { return { success: false, message: 'Route management not initialized' }; } const externalKey = this.buildExternalKey(dataArg.ownership); const existingRoute = manager.findApiRouteByExternalKey(externalKey); if (dataArg.delete) { if (!existingRoute) { return { success: true, action: 'unchanged' }; } const result = await manager.deleteRoute(existingRoute.id); return result.success ? { success: true, action: 'deleted', routeId: existingRoute.id } : { success: false, message: result.message }; } if (!dataArg.route) { return { success: false, message: 'route is required unless delete=true' }; } const metadata: interfaces.data.IRouteMetadata = { ownerType: 'workhoster', workHosterType: dataArg.ownership.workHosterType, workHosterId: dataArg.ownership.workHosterId, workAppId: dataArg.ownership.workAppId, externalKey, }; const route = this.normalizeWorkAppRoute(dataArg.route, dataArg.ownership, externalKey); if (existingRoute) { const result = await manager.updateRoute(existingRoute.id, { route, enabled: dataArg.enabled ?? true, metadata, }); return result.success ? { success: true, action: 'updated', routeId: existingRoute.id } : { success: false, message: result.message }; } const routeId = await manager.createRoute(route, userId, dataArg.enabled ?? true, metadata); return { success: true, action: 'created', routeId }; }, ), ); this.typedrouter.addTypedHandler( new plugins.typedrequest.TypedHandler( 'getWorkAppMailIdentities', async (dataArg) => { await this.requireAuth(dataArg, 'workhosters:read'); const manager = this.opsServerRef.dcRouterRef.workAppMailManager; if (!manager) return { identities: [] }; return { identities: await manager.listMailIdentities(dataArg.ownership) }; }, ), ); this.typedrouter.addTypedHandler( new plugins.typedrequest.TypedHandler( 'syncWorkAppMailIdentity', async (dataArg) => { const userId = await this.requireAuth(dataArg, 'workhosters:write'); const manager = this.opsServerRef.dcRouterRef.workAppMailManager; if (!manager) { return { success: false, message: 'WorkApp mail manager not initialized' }; } try { return await manager.syncMailIdentity(dataArg, userId); } catch (error) { return { success: false, message: (error as Error).message }; } }, ), ); } private getGatewayCapabilities(): interfaces.data.IGatewayCapabilities { const dcRouter = this.opsServerRef.dcRouterRef; return { routes: { read: Boolean(dcRouter.routeConfigManager), write: Boolean(dcRouter.routeConfigManager), idempotentSync: Boolean(dcRouter.routeConfigManager), }, domains: { read: Boolean(dcRouter.dnsManager), write: Boolean(dcRouter.dnsManager), }, certificates: { read: Boolean(dcRouter.smartProxy), export: Boolean(dcRouter.smartProxy), forceRenew: Boolean(dcRouter.smartProxy), }, email: { domains: Boolean(dcRouter.emailDomainManager), inbound: Boolean(dcRouter.emailServer), outbound: Boolean(dcRouter.emailServer), }, remoteIngress: { enabled: Boolean(dcRouter.options.remoteIngressConfig?.enabled), }, dns: { authoritative: Boolean(dcRouter.options.dnsScopes?.length), providerManaged: Boolean(dcRouter.dnsManager), }, http3: { enabled: dcRouter.options.http3?.enabled !== false, }, }; } private buildExternalKey(ownership: interfaces.data.IWorkAppRouteOwnership): string { return [ ownership.workHosterType, ownership.workHosterId, ownership.workAppId, ownership.hostname, ].map((part) => part.trim()).join(':'); } private normalizeWorkAppRoute( route: interfaces.data.IDcRouterRouteConfig, ownership: interfaces.data.IWorkAppRouteOwnership, externalKey: string, ): interfaces.data.IDcRouterRouteConfig { const normalizedRoute = { ...route }; if (!normalizedRoute.name) { normalizedRoute.name = `workapp-${externalKey.replace(/[^a-zA-Z0-9-]+/g, '-').slice(0, 80)}`; } return normalizedRoute; } }