mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 21:15:32 +09:00
feat(push): relay notifications as encrypted web push (#2906)
This commit is contained in:
1 parent
7021a58090
commit
18c303abf6
111 files changed
+12473
-1177
No files matched your search
@@ -15,6 +15,10 @@ import {
|
||||
GatewayRolloutConfigResponse,
|
||||
GatewayRolloutConfigUpdateRequest,
|
||||
} from '@fluxer/schema/src/domains/admin/GatewayRolloutSchemas';
|
||||
import {
|
||||
PushServiceDeliveryConfigResponse,
|
||||
PushServiceDeliveryConfigUpdateRequest,
|
||||
} from '@fluxer/schema/src/domains/admin/PushServiceDeliverySchemas';
|
||||
import {
|
||||
ScreenShareDeliveryConfigResponse,
|
||||
ScreenShareDeliveryConfigUpdateRequest,
|
||||
@@ -649,6 +653,7 @@ export const InstanceConfigResponse = z.object({
|
||||
gateway_rollout: GatewayRolloutConfigResponse,
|
||||
voice_noise_suppression: VoiceNoiseSuppressionConfigResponse,
|
||||
screen_share_delivery: ScreenShareDeliveryConfigResponse,
|
||||
push_service_delivery: PushServiceDeliveryConfigResponse,
|
||||
experiment_delivery: ExperimentDeliveryConfigResponse,
|
||||
registration: InstanceRegistrationResponse,
|
||||
self_hosted: z.boolean(),
|
||||
@@ -686,6 +691,7 @@ export const InstanceConfigUpdateRequest = z.object({
|
||||
gateway_rollout: GatewayRolloutConfigUpdateRequest.nullish(),
|
||||
voice_noise_suppression: VoiceNoiseSuppressionConfigUpdateRequest.nullish(),
|
||||
screen_share_delivery: ScreenShareDeliveryConfigUpdateRequest.nullish(),
|
||||
push_service_delivery: PushServiceDeliveryConfigUpdateRequest.nullish(),
|
||||
experiment_delivery: ExperimentDeliveryConfigUpdateRequest.nullish(),
|
||||
registration: z
|
||||
.object({
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {EXPERIMENT_BUCKET_RESOLUTION, experimentBucket} from '@fluxer/schema/src/domains/experiment/ExperimentBucket';
|
||||
import {z} from 'zod';
|
||||
|
||||
const PUSH_SERVICE_DELIVERY_ROLLOUT_BASIS_POINTS_MAX = EXPERIMENT_BUCKET_RESOLUTION;
|
||||
const PUSH_SERVICE_DELIVERY_MAX_TARGETED_USERS = 1000;
|
||||
const DEFAULT_PUSH_SERVICE_DELIVERY_SALT = 'push-service-delivery-v1';
|
||||
|
||||
const PUSH_SERVICE_DELIVERY_SALT_PATTERN = /^[\x20-\x7e]+$/u;
|
||||
|
||||
const PushServiceDeliveryTargetIdSchema = z.string().regex(/^\d{1,20}$/u);
|
||||
const PushServiceDeliveryTargetedUserIdsSchema = z
|
||||
.array(PushServiceDeliveryTargetIdSchema)
|
||||
.max(PUSH_SERVICE_DELIVERY_MAX_TARGETED_USERS);
|
||||
|
||||
const pushServiceDeliveryConfigFields = {
|
||||
enabled: z.boolean(),
|
||||
config_version: z.number().int().min(0),
|
||||
rollout_basis_points: z.number().int().min(0).max(PUSH_SERVICE_DELIVERY_ROLLOUT_BASIS_POINTS_MAX),
|
||||
rollout_salt: z.string().trim().min(1).max(64).regex(PUSH_SERVICE_DELIVERY_SALT_PATTERN),
|
||||
included_user_ids: PushServiceDeliveryTargetedUserIdsSchema,
|
||||
excluded_user_ids: PushServiceDeliveryTargetedUserIdsSchema,
|
||||
};
|
||||
|
||||
export const PushServiceDeliveryConfigSchema = z.object({
|
||||
enabled: pushServiceDeliveryConfigFields.enabled.default(false),
|
||||
config_version: pushServiceDeliveryConfigFields.config_version.default(0),
|
||||
rollout_basis_points: pushServiceDeliveryConfigFields.rollout_basis_points.default(0),
|
||||
rollout_salt: pushServiceDeliveryConfigFields.rollout_salt.default(DEFAULT_PUSH_SERVICE_DELIVERY_SALT),
|
||||
included_user_ids: pushServiceDeliveryConfigFields.included_user_ids.default([]),
|
||||
excluded_user_ids: pushServiceDeliveryConfigFields.excluded_user_ids.default([]),
|
||||
});
|
||||
|
||||
export type PushServiceDeliveryConfig = z.infer<typeof PushServiceDeliveryConfigSchema>;
|
||||
|
||||
export const DEFAULT_PUSH_SERVICE_DELIVERY_CONFIG: PushServiceDeliveryConfig = PushServiceDeliveryConfigSchema.parse(
|
||||
{},
|
||||
);
|
||||
|
||||
export const PushServiceDeliveryConfigUpdateRequest = z
|
||||
.object(pushServiceDeliveryConfigFields)
|
||||
.omit({config_version: true})
|
||||
.partial();
|
||||
|
||||
export type PushServiceDeliveryConfigUpdateRequest = z.infer<typeof PushServiceDeliveryConfigUpdateRequest>;
|
||||
|
||||
export const PushServiceDeliveryConfigResponse = PushServiceDeliveryConfigSchema;
|
||||
|
||||
export type PushServiceDeliveryConfigResponse = z.infer<typeof PushServiceDeliveryConfigResponse>;
|
||||
|
||||
export function pushServiceDeliveryEnrols(config: PushServiceDeliveryConfig, userId: string): boolean {
|
||||
if (!config.enabled) return false;
|
||||
if (config.excluded_user_ids.includes(userId)) return false;
|
||||
if (config.included_user_ids.includes(userId)) return true;
|
||||
return experimentBucket(userId, config.rollout_salt) < config.rollout_basis_points;
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import {RTC_REGION_ID_MAX_LENGTH, RTC_REGION_ID_MIN_LENGTH} from '@fluxer/constants/src/LimitConstants';
|
||||
import {GatewayRolloutConfigResponse} from '@fluxer/schema/src/domains/admin/GatewayRolloutSchemas';
|
||||
import {PushServiceDeliveryConfigResponse} from '@fluxer/schema/src/domains/admin/PushServiceDeliverySchemas';
|
||||
import {WebAuthnCredentialResponse} from '@fluxer/schema/src/domains/auth/AuthSchemas';
|
||||
import {ChannelResponse, RtcRegionResponse} from '@fluxer/schema/src/domains/channel/ChannelSchemas';
|
||||
import {VoiceStateResponse} from '@fluxer/schema/src/domains/gateway/GatewaySchemas';
|
||||
@@ -212,6 +213,11 @@ export const RpcRequest = z.discriminatedUnion('type', [
|
||||
z.object({
|
||||
type: z.literal('get_gateway_rollout_config').describe('Request type for fetching gateway rollout configuration'),
|
||||
}),
|
||||
z.object({
|
||||
type: z
|
||||
.literal('get_push_service_delivery_config')
|
||||
.describe('Request type for fetching push service delivery configuration'),
|
||||
}),
|
||||
]);
|
||||
|
||||
export type RpcRequest = z.infer<typeof RpcRequest>;
|
||||
@@ -517,6 +523,16 @@ export const RpcResponse = z.discriminatedUnion('type', [
|
||||
})
|
||||
.describe('Gateway rollout config result'),
|
||||
}),
|
||||
z.object({
|
||||
type: z
|
||||
.literal('get_push_service_delivery_config')
|
||||
.describe('Response type for push service delivery configuration'),
|
||||
data: z
|
||||
.object({
|
||||
config: PushServiceDeliveryConfigResponse.describe('Push service delivery configuration'),
|
||||
})
|
||||
.describe('Push service delivery config result'),
|
||||
}),
|
||||
]);
|
||||
|
||||
export type RpcResponse = z.infer<typeof RpcResponse>;
|
||||
@@ -481,7 +481,9 @@ const MobilePushProviderEnvironmentSchema = createNamedStringLiteralUnion(
|
||||
export const RegisterMobileDeviceRequest = z
|
||||
.object({
|
||||
platform: MobilePushPlatformSchema.describe('The mobile push notification platform'),
|
||||
token: createStringType(1, 4096).describe('The platform-specific push notification token or endpoint URL'),
|
||||
token: createStringType(1, 4096).describe(
|
||||
'The Web Push endpoint URL when encryption keys are supplied, otherwise the raw platform push token',
|
||||
),
|
||||
user_agent: createStringType(1, 1024).optional().describe('The user agent string identifying the device'),
|
||||
app_id: createStringType(1, 128)
|
||||
.optional()
|
||||
@@ -491,32 +493,44 @@ export const RegisterMobileDeviceRequest = z
|
||||
),
|
||||
encryption_key: createStringType(1, 1024)
|
||||
.optional()
|
||||
.describe('The P-256 ECDH public key for UnifiedPush encryption (base64url)'),
|
||||
.describe('The P-256 ECDH public key for Web Push encryption (base64url)'),
|
||||
auth_secret: createStringType(1, 1024)
|
||||
.optional()
|
||||
.describe('The authentication secret for UnifiedPush encryption (base64url)'),
|
||||
.describe('The authentication secret for Web Push encryption (base64url)'),
|
||||
})
|
||||
.superRefine((value, ctx) => {
|
||||
if (value.platform !== 'android_unified_push') return;
|
||||
if (!URLType.safeParse(value.token).success) {
|
||||
const tokenIsUrl = URLType.safeParse(value.token).success;
|
||||
const isWebPushRegistration =
|
||||
value.platform === 'android_unified_push' || value.encryption_key != null || value.auth_secret != null;
|
||||
if (!isWebPushRegistration) {
|
||||
if (tokenIsUrl) {
|
||||
ctx.addIssue({
|
||||
code: 'custom',
|
||||
path: ['token'],
|
||||
message: 'Endpoint URL registrations require encryption_key and auth_secret',
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!tokenIsUrl) {
|
||||
ctx.addIssue({
|
||||
code: 'custom',
|
||||
path: ['token'],
|
||||
message: 'UnifiedPush registrations require a valid endpoint URL',
|
||||
message: 'Web Push registrations require a valid endpoint URL',
|
||||
});
|
||||
}
|
||||
if (!value.encryption_key) {
|
||||
ctx.addIssue({
|
||||
code: 'custom',
|
||||
path: ['encryption_key'],
|
||||
message: 'UnifiedPush registrations require encryption_key',
|
||||
message: 'Web Push registrations require encryption_key',
|
||||
});
|
||||
}
|
||||
if (!value.auth_secret) {
|
||||
ctx.addIssue({
|
||||
code: 'custom',
|
||||
path: ['auth_secret'],
|
||||
message: 'UnifiedPush registrations require auth_secret',
|
||||
message: 'Web Push registrations require auth_secret',
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -525,7 +539,9 @@ export type RegisterMobileDeviceRequest = z.infer<typeof RegisterMobileDeviceReq
|
||||
|
||||
export const UnregisterMobileDeviceRequest = z.object({
|
||||
platform: MobilePushPlatformSchema.describe('The mobile push notification platform'),
|
||||
token: createStringType(1, 4096).describe('The platform-specific push notification token to unregister'),
|
||||
token: createStringType(1, 4096).describe(
|
||||
'The Web Push endpoint URL or raw platform push token used at registration',
|
||||
),
|
||||
app_id: createStringType(1, 128)
|
||||
.optional()
|
||||
.describe('Client app channel or bundle mapping identifier, such as stable, beta, or canary'),
|
||||
|
||||
Reference in new issue
Block a user