import * as plugins from '../../plugins.js'; import type { OpsServer } from '../classes.opsserver.js'; import type { WebPushBindingDoc } from '../../db/index.js'; import type { IWebPushControllerMutation } from '../../webpush/classes.webpush-manager.js'; import { requireGatewayMachineAuth, type IGatewayMachineAuthContext, } from '../helpers/gateway-client-auth.js'; type IWebPushResourceOwner = plugins.servezoneInterfaces.data.IWebPushResourceOwner; export class WebPushControlHandler { public typedrouter = new plugins.typedrequest.TypedRouter(); public constructor(private readonly opsServerRef: OpsServer) { this.opsServerRef.typedrouter.addTypedRouter(this.typedrouter); this.registerHandlers(); } private registerHandlers(): void { this.typedrouter.addTypedHandler( new plugins.typedrequest.TypedHandler< plugins.servezoneInterfaces.requests.webpush.IReq_ListWebPushBindings >( 'listWebPushBindings', async (dataArg) => { const auth = await this.requireControlAuth(dataArg.auth, 'readWebPush'); const owner = this.scopeOwnerFilter(auth, dataArg.owner); const manager = this.opsServerRef.dcRouterRef.webPushManager; return { bindings: await manager.listBindings(owner), }; }, ), ); this.typedrouter.addTypedHandler( new plugins.typedrequest.TypedHandler< plugins.servezoneInterfaces.requests.webpush.IReq_SyncWebPushBinding >( 'syncWebPushBinding', async (dataArg) => { try { const request = dataArg as typeof dataArg & { controller: IWebPushControllerMutation; }; const auth = await this.requireControlAuth(dataArg.auth, 'manageWebPush'); this.assertExactOwner(auth, dataArg.binding.owner); const result = await this.opsServerRef.dcRouterRef.webPushManager.syncBinding( dataArg.binding, request.controller, auth.userId, ); return { success: true, binding: result.binding, credential: result.credential, }; } catch (error: unknown) { return { success: false, message: this.safeMessage(error), }; } }, ), ); this.typedrouter.addTypedHandler( new plugins.typedrequest.TypedHandler< plugins.servezoneInterfaces.requests.webpush.IReq_DeleteWebPushBinding >( 'deleteWebPushBinding', async (dataArg) => { try { const request = dataArg as typeof dataArg & { id?: string; owner: IWebPushResourceOwner; controller: IWebPushControllerMutation; }; const auth = await this.requireControlAuth(dataArg.auth, 'manageWebPush'); this.assertExactOwner(auth, request.owner); return { success: await this.opsServerRef.dcRouterRef.webPushManager.deleteBinding({ ...(request.id !== undefined ? { id: request.id } : {}), owner: request.owner, controller: request.controller, }, auth.userId), }; } catch (error: unknown) { return { success: false, message: this.safeMessage(error), }; } }, ), ); this.typedrouter.addTypedHandler( new plugins.typedrequest.TypedHandler< plugins.servezoneInterfaces.requests.webpush.IReq_RotateWebPushCredential >( 'rotateWebPushCredential', async (dataArg) => { try { const auth = await this.requireControlAuth(dataArg.auth, 'manageWebPush'); const manager = this.opsServerRef.dcRouterRef.webPushManager; const binding = await manager.getBindingByCredentialId(dataArg.credentialId); if (!binding) { throw new plugins.typedrequest.TypedResponseError('Web Push binding not found'); } this.assertExactOwner(auth, this.bindingOwner(binding)); return { success: true, credential: await manager.rotateCredential(dataArg.credentialId), }; } catch (error: unknown) { return { success: false, message: this.safeMessage(error), }; } }, ), ); this.typedrouter.addTypedHandler( new plugins.typedrequest.TypedHandler< plugins.servezoneInterfaces.requests.webpush.IReq_RotateWebPushVapidKey >( 'rotateWebPushVapidKey', async (dataArg) => { try { const auth = await this.requireControlAuth(dataArg.auth, 'manageWebPush'); const binding = await this.requireAuthorizedBinding(auth, dataArg.bindingId); return { success: true, binding: await this.opsServerRef.dcRouterRef.webPushManager.rotateVapidKey( binding.id, ), }; } catch (error: unknown) { return { success: false, message: this.safeMessage(error), }; } }, ), ); } private async requireControlAuth( authArg: plugins.servezoneInterfaces.requests.webpush.IWebPushControlRequestAuth, capabilityArg: 'readWebPush' | 'manageWebPush', ): Promise { const auth = await requireGatewayMachineAuth( this.opsServerRef, authArg, capabilityArg === 'manageWebPush' ? 'gateway-clients:write' : 'gateway-clients:read', ); if (auth.isAdmin) return auth; if ( auth.policy?.role !== 'gatewayClient' || auth.policy.capabilities?.[capabilityArg] !== true ) { throw new plugins.typedrequest.TypedResponseError( `token capability missing: ${capabilityArg}`, ); } return auth; } private scopeOwnerFilter( authArg: IGatewayMachineAuthContext, requestedArg: Partial | undefined, ): Partial { if (authArg.isAdmin) return requestedArg || {}; const gatewayClient = authArg.gatewayClient; if (!gatewayClient) { throw new plugins.typedrequest.TypedResponseError( 'gateway-client credential required', ); } if ( requestedArg?.gatewayClientType !== undefined && requestedArg.gatewayClientType !== gatewayClient.type ) { throw new plugins.typedrequest.TypedResponseError( 'gateway-client credential cannot access another Web Push owner', ); } if ( requestedArg?.gatewayClientId !== undefined && requestedArg.gatewayClientId !== gatewayClient.id ) { throw new plugins.typedrequest.TypedResponseError( 'gateway-client credential cannot access another Web Push owner', ); } return { gatewayClientType: gatewayClient.type, gatewayClientId: gatewayClient.id, ...(requestedArg?.appInstanceId !== undefined ? { appInstanceId: requestedArg.appInstanceId } : {}), }; } private assertExactOwner( authArg: IGatewayMachineAuthContext, ownerArg: IWebPushResourceOwner, ): void { if (authArg.isAdmin) return; const gatewayClient = authArg.gatewayClient; if ( !gatewayClient || ownerArg.gatewayClientType !== gatewayClient.type || ownerArg.gatewayClientId !== gatewayClient.id ) { throw new plugins.typedrequest.TypedResponseError( 'gateway-client credential cannot access another Web Push owner', ); } } private async requireAuthorizedBinding( authArg: IGatewayMachineAuthContext, bindingIdArg: string, ): Promise { const binding = await this.opsServerRef.dcRouterRef.webPushManager.getBindingById( bindingIdArg, ); if (!binding) { throw new plugins.typedrequest.TypedResponseError('Web Push binding not found'); } this.assertExactOwner(authArg, this.bindingOwner(binding)); return binding; } private bindingOwner(bindingArg: WebPushBindingDoc): IWebPushResourceOwner { return { gatewayClientType: bindingArg.ownerGatewayClientType, gatewayClientId: bindingArg.ownerGatewayClientId, appInstanceId: bindingArg.ownerAppInstanceId, }; } private safeMessage(errorArg: unknown): string { return errorArg instanceof plugins.typedrequest.TypedResponseError ? errorArg.message : 'Web Push control operation failed'; } }