Add iOS node push-token registration foundation

This commit is contained in:
William Valentin
2026-02-16 12:47:34 -08:00
parent bea4c54f3b
commit 58c4b0b9bb
19 changed files with 448 additions and 7 deletions
+64
View File
@@ -8,6 +8,7 @@ import {
parseNodeLocationSetParams,
parseNodeLocationGetParams,
parseNodeStatusSetParams,
parseNodePushTokenSetParams,
} from '../protocol.js';
export interface NodeRegistration {
@@ -23,6 +24,7 @@ export interface NodeConnectionState {
node?: NodeRegistration;
location?: NodeLocation;
status?: NodeStatus;
pushToken?: NodePushToken;
}
export interface NodeLocation {
@@ -47,15 +49,25 @@ export interface NodeStatus {
reportedAt: number;
}
export interface NodePushToken {
provider: 'apns';
token: string;
topic?: string;
environment: 'sandbox' | 'production';
registeredAt: number;
}
export interface NodeHandlerDeps {
enabled: boolean;
locationEnabled: boolean;
pushEnabled: boolean;
allowedRoles: string[];
featureGates: Record<string, boolean>;
getConnectionState: (connectionId: string) => NodeConnectionState | undefined;
setNodeRegistration: (connectionId: string, registration: NodeRegistration) => void;
setNodeLocation: (connectionId: string, location: NodeLocation) => void;
setNodeStatus: (connectionId: string, status: NodeStatus) => void;
setNodePushToken: (connectionId: string, pushToken: NodePushToken) => void;
}
export function createNodeHandlers(deps: NodeHandlerDeps) {
@@ -248,6 +260,49 @@ export function createNodeHandlers(deps: NodeHandlerDeps) {
});
},
'node.push_token.set': async (request: GatewayRequest): Promise<OutboundMessage> => {
if (!deps.enabled) {
return makeError(request.id, ErrorCode.AuthFailed, 'Node RPC is disabled');
}
if (!deps.pushEnabled) {
return makeError(request.id, ErrorCode.AuthFailed, 'Node push token registration is disabled');
}
const parsed = parseNodePushTokenSetParams(request.params);
if (!parsed) {
return makeError(request.id, ErrorCode.InvalidRequest, 'Invalid node.push_token.set params');
}
const state = deps.getConnectionState(parsed.connectionId);
if (!state?.node) {
return makeError(request.id, ErrorCode.AuthFailed, 'Node is not registered for this connection');
}
const pushToken: NodePushToken = {
provider: 'apns',
token: parsed.token,
topic: parsed.topic || undefined,
environment: parsed.environment ?? 'production',
registeredAt: Date.now(),
};
deps.setNodePushToken(parsed.connectionId, pushToken);
return makeResponse(request.id, {
updated: true,
node: {
id: state.node.nodeId,
role: state.node.role,
},
push: {
provider: pushToken.provider,
tokenPreview: maskToken(pushToken.token),
topic: pushToken.topic,
environment: pushToken.environment,
registeredAt: pushToken.registeredAt,
},
});
},
'system.capabilities': async (request: GatewayRequest): Promise<OutboundMessage> => {
const params = request.params as { connectionId?: string } | undefined;
const connectionId = params?.connectionId;
@@ -259,6 +314,7 @@ export function createNodeHandlers(deps: NodeHandlerDeps) {
nodes: {
enabled: deps.enabled,
locationEnabled: deps.locationEnabled,
pushEnabled: deps.pushEnabled,
allowedRoles: deps.allowedRoles,
registered: Boolean(state?.node),
role: state?.node?.role,
@@ -269,3 +325,11 @@ export function createNodeHandlers(deps: NodeHandlerDeps) {
},
};
}
function maskToken(token: string): string {
if (token.length <= 8) {
return '****';
}
const suffix = token.slice(-8);
return `***${suffix}`;
}