mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-07 19:22:14 +09:00
feat(web): prepare the fluxer.com domain migration (#2952)
This commit is contained in:
@@ -11,6 +11,10 @@ import {ADMIN_ACL_COUNT, AdminAclType} from '@fluxer/schema/src/domains/admin/Ad
|
||||
import {AdminArchiveResponseSchema} from '@fluxer/schema/src/domains/admin/AdminArchiveSchemas';
|
||||
import {GuildAdminResponse} from '@fluxer/schema/src/domains/admin/AdminGuildSchemas';
|
||||
import {UserAdminResponseSchema} from '@fluxer/schema/src/domains/admin/AdminUserSchemas';
|
||||
import {
|
||||
DomainMigrationConfigResponse,
|
||||
DomainMigrationConfigUpdateRequest,
|
||||
} from '@fluxer/schema/src/domains/admin/DomainMigrationSchemas';
|
||||
import {
|
||||
GatewayRolloutConfigResponse,
|
||||
GatewayRolloutConfigUpdateRequest,
|
||||
@@ -649,6 +653,7 @@ export const InstanceConfigResponse = z.object({
|
||||
gateway_rollout: GatewayRolloutConfigResponse,
|
||||
voice_noise_suppression: VoiceNoiseSuppressionConfigResponse,
|
||||
push_service_delivery: PushServiceDeliveryConfigResponse,
|
||||
domain_migration: DomainMigrationConfigResponse,
|
||||
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(),
|
||||
push_service_delivery: PushServiceDeliveryConfigUpdateRequest.nullish(),
|
||||
domain_migration: DomainMigrationConfigUpdateRequest.nullish(),
|
||||
experiment_delivery: ExperimentDeliveryConfigUpdateRequest.nullish(),
|
||||
registration: z
|
||||
.object({
|
||||
|
||||
@@ -0,0 +1,234 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {
|
||||
DEFAULT_DOMAIN_MIGRATION_CONFIG,
|
||||
type DomainMigrationConfig,
|
||||
DomainMigrationConfigSchema,
|
||||
DomainMigrationConfigUpdateRequest,
|
||||
INERT_DOMAIN_MIGRATION_ASSIGNMENT,
|
||||
resolveDomainMigrationAssignment,
|
||||
toDomainMigrationDiscovery,
|
||||
} from '@fluxer/schema/src/domains/admin/DomainMigrationSchemas';
|
||||
import {experimentBucket} from '@fluxer/schema/src/domains/experiment/ExperimentBucket';
|
||||
import {describe, expect, test} from 'vitest';
|
||||
|
||||
const TARGETED_USER_ID = '1000000000000000001';
|
||||
const OTHER_USER_ID = '1000000000000000002';
|
||||
|
||||
function createConfig(overrides: Partial<DomainMigrationConfig> = {}): DomainMigrationConfig {
|
||||
return {
|
||||
...DEFAULT_DOMAIN_MIGRATION_CONFIG,
|
||||
included_user_ids: [],
|
||||
excluded_user_ids: [],
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function syntheticUserIds(count: number): Array<string> {
|
||||
const ids: Array<string> = [];
|
||||
for (let index = 0; index < count; index++) {
|
||||
ids.push((1400000000000000000n + BigInt(index)).toString());
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
function targetedUserIds(config: DomainMigrationConfig, userIds: ReadonlyArray<string>): Set<string> {
|
||||
const targeted = new Set<string>();
|
||||
for (const userId of userIds) {
|
||||
if (resolveDomainMigrationAssignment(config, userId).enabled) {
|
||||
targeted.add(userId);
|
||||
}
|
||||
}
|
||||
return targeted;
|
||||
}
|
||||
|
||||
describe('domain migration configuration', () => {
|
||||
test('derives defaults from the schema with independently owned arrays', () => {
|
||||
const first = DomainMigrationConfigSchema.parse({});
|
||||
const second = DomainMigrationConfigSchema.parse({});
|
||||
expect(first).toEqual(DEFAULT_DOMAIN_MIGRATION_CONFIG);
|
||||
first.included_user_ids.push(TARGETED_USER_ID);
|
||||
first.excluded_user_ids.push(OTHER_USER_ID);
|
||||
expect(second).toEqual(DEFAULT_DOMAIN_MIGRATION_CONFIG);
|
||||
});
|
||||
|
||||
test('defaults to disabled with an empty rollout', () => {
|
||||
expect(DEFAULT_DOMAIN_MIGRATION_CONFIG.enabled).toBe(false);
|
||||
expect(DEFAULT_DOMAIN_MIGRATION_CONFIG.rollout_basis_points).toBe(0);
|
||||
expect(DEFAULT_DOMAIN_MIGRATION_CONFIG.anonymous_rollout_basis_points).toBe(0);
|
||||
expect(DEFAULT_DOMAIN_MIGRATION_CONFIG.rollout_salt).toBe('domain-migration-v1');
|
||||
expect(DEFAULT_DOMAIN_MIGRATION_CONFIG.standalone_forwarding).toBe(false);
|
||||
});
|
||||
|
||||
test('fills standalone forwarding in for configs stored before it existed', () => {
|
||||
const stored = DomainMigrationConfigSchema.parse({enabled: true, config_version: 4, rollout_basis_points: 100});
|
||||
expect(stored.standalone_forwarding).toBe(false);
|
||||
});
|
||||
|
||||
test.each([{}, {enabled: false}, {enabled: undefined}, {rollout_basis_points: 2500}, {standalone_forwarding: true}])(
|
||||
'keeps partial updates free of configuration defaults: %j',
|
||||
(patch) => {
|
||||
expect(DomainMigrationConfigUpdateRequest.parse(patch)).toEqual(patch);
|
||||
},
|
||||
);
|
||||
|
||||
test('does not accept a client-provided configuration version', () => {
|
||||
expect(DomainMigrationConfigUpdateRequest.parse({config_version: 12})).toEqual({});
|
||||
});
|
||||
|
||||
test.each([
|
||||
{rollout_basis_points: -1},
|
||||
{rollout_basis_points: 10001},
|
||||
{anonymous_rollout_basis_points: -1},
|
||||
{anonymous_rollout_basis_points: 10001},
|
||||
{anonymous_rollout_basis_points: 1.5},
|
||||
{rollout_salt: ' '},
|
||||
{rollout_salt: 'domain-migration-\u00e9'},
|
||||
{included_user_ids: ['not-an-id']},
|
||||
{excluded_user_ids: ['not-an-id']},
|
||||
{standalone_forwarding: 'yes'},
|
||||
])('applies the same validation to stored configuration and updates: %j', (value) => {
|
||||
expect(DomainMigrationConfigSchema.safeParse(value).success).toBe(false);
|
||||
expect(DomainMigrationConfigUpdateRequest.safeParse(value).success).toBe(false);
|
||||
});
|
||||
|
||||
test('rejects more than a thousand targeted user ids', () => {
|
||||
const ids = syntheticUserIds(1001);
|
||||
expect(DomainMigrationConfigSchema.safeParse({included_user_ids: ids}).success).toBe(false);
|
||||
expect(DomainMigrationConfigSchema.safeParse({included_user_ids: ids.slice(0, 1000)}).success).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveDomainMigrationAssignment', () => {
|
||||
test('the inert assignment is disabled', () => {
|
||||
expect(INERT_DOMAIN_MIGRATION_ASSIGNMENT.enabled).toBe(false);
|
||||
});
|
||||
|
||||
test('returns the inert assignment when the master switch is off', () => {
|
||||
const config = createConfig({
|
||||
enabled: false,
|
||||
rollout_basis_points: 10000,
|
||||
included_user_ids: [TARGETED_USER_ID],
|
||||
});
|
||||
expect(resolveDomainMigrationAssignment(config, TARGETED_USER_ID)).toEqual(INERT_DOMAIN_MIGRATION_ASSIGNMENT);
|
||||
});
|
||||
|
||||
test('returns the inert assignment for the default config', () => {
|
||||
expect(resolveDomainMigrationAssignment(createConfig(), TARGETED_USER_ID)).toEqual(
|
||||
INERT_DOMAIN_MIGRATION_ASSIGNMENT,
|
||||
);
|
||||
});
|
||||
|
||||
test('never hands back the shared inert object', () => {
|
||||
const assignment = resolveDomainMigrationAssignment(createConfig(), TARGETED_USER_ID);
|
||||
expect(assignment).not.toBe(INERT_DOMAIN_MIGRATION_ASSIGNMENT);
|
||||
});
|
||||
|
||||
test('denylist beats allowlist', () => {
|
||||
const config = createConfig({
|
||||
enabled: true,
|
||||
included_user_ids: [TARGETED_USER_ID],
|
||||
excluded_user_ids: [TARGETED_USER_ID],
|
||||
});
|
||||
expect(resolveDomainMigrationAssignment(config, TARGETED_USER_ID).enabled).toBe(false);
|
||||
});
|
||||
|
||||
test('denylist beats the bucket', () => {
|
||||
const config = createConfig({
|
||||
enabled: true,
|
||||
rollout_basis_points: 10000,
|
||||
excluded_user_ids: [TARGETED_USER_ID],
|
||||
});
|
||||
expect(resolveDomainMigrationAssignment(config, TARGETED_USER_ID).enabled).toBe(false);
|
||||
expect(resolveDomainMigrationAssignment(config, OTHER_USER_ID).enabled).toBe(true);
|
||||
});
|
||||
|
||||
test('allowlist beats the bucket', () => {
|
||||
const config = createConfig({
|
||||
enabled: true,
|
||||
rollout_basis_points: 0,
|
||||
included_user_ids: [TARGETED_USER_ID],
|
||||
});
|
||||
expect(resolveDomainMigrationAssignment(config, TARGETED_USER_ID).enabled).toBe(true);
|
||||
expect(resolveDomainMigrationAssignment(config, OTHER_USER_ID).enabled).toBe(false);
|
||||
});
|
||||
|
||||
test.each([
|
||||
{basisPoints: 0, enabled: false},
|
||||
{basisPoints: 10000, enabled: true},
|
||||
])('a rollout of $basisPoints basis points targets $enabled', ({basisPoints, enabled}) => {
|
||||
const config = createConfig({enabled: true, rollout_basis_points: basisPoints});
|
||||
for (const userId of syntheticUserIds(200)) {
|
||||
expect(resolveDomainMigrationAssignment(config, userId).enabled).toBe(enabled);
|
||||
}
|
||||
});
|
||||
|
||||
test('the bucket boundary is exclusive at the low end and inclusive one point above', () => {
|
||||
const salt = DEFAULT_DOMAIN_MIGRATION_CONFIG.rollout_salt;
|
||||
const bucket = experimentBucket(TARGETED_USER_ID, salt);
|
||||
expect(
|
||||
resolveDomainMigrationAssignment(createConfig({enabled: true, rollout_basis_points: bucket}), TARGETED_USER_ID)
|
||||
.enabled,
|
||||
).toBe(false);
|
||||
expect(
|
||||
resolveDomainMigrationAssignment(
|
||||
createConfig({enabled: true, rollout_basis_points: bucket + 1}),
|
||||
TARGETED_USER_ID,
|
||||
).enabled,
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test('raising the rollout basis points only ever adds users', () => {
|
||||
const userIds = syntheticUserIds(2000);
|
||||
const atOneThousand = targetedUserIds(createConfig({enabled: true, rollout_basis_points: 1000}), userIds);
|
||||
const atTwoThousand = targetedUserIds(createConfig({enabled: true, rollout_basis_points: 2000}), userIds);
|
||||
expect(atOneThousand.size).toBeGreaterThan(0);
|
||||
for (const userId of atOneThousand) {
|
||||
expect(atTwoThousand.has(userId)).toBe(true);
|
||||
}
|
||||
expect(atTwoThousand.size).toBeGreaterThan(atOneThousand.size);
|
||||
});
|
||||
|
||||
test('the targeted set follows the salt', () => {
|
||||
const userIds = syntheticUserIds(2000);
|
||||
const first = targetedUserIds(
|
||||
createConfig({enabled: true, rollout_basis_points: 5000, rollout_salt: 'domain-migration-v1'}),
|
||||
userIds,
|
||||
);
|
||||
const second = targetedUserIds(
|
||||
createConfig({enabled: true, rollout_basis_points: 5000, rollout_salt: 'domain-migration-v2'}),
|
||||
userIds,
|
||||
);
|
||||
expect(first).not.toEqual(second);
|
||||
});
|
||||
});
|
||||
|
||||
describe('toDomainMigrationDiscovery', () => {
|
||||
test('projects only the public fields of the config', () => {
|
||||
const config = createConfig({
|
||||
enabled: true,
|
||||
config_version: 3,
|
||||
rollout_basis_points: 500,
|
||||
rollout_salt: 'domain-migration-v2',
|
||||
anonymous_rollout_basis_points: 250,
|
||||
included_user_ids: [TARGETED_USER_ID],
|
||||
excluded_user_ids: [OTHER_USER_ID],
|
||||
standalone_forwarding: true,
|
||||
});
|
||||
expect(toDomainMigrationDiscovery(config)).toEqual({
|
||||
enabled: true,
|
||||
anonymous_rollout_basis_points: 250,
|
||||
rollout_salt: 'domain-migration-v2',
|
||||
standalone_forwarding: true,
|
||||
});
|
||||
});
|
||||
|
||||
test('reports the kill switch for the default config', () => {
|
||||
expect(toDomainMigrationDiscovery(DEFAULT_DOMAIN_MIGRATION_CONFIG)).toEqual({
|
||||
enabled: false,
|
||||
anonymous_rollout_basis_points: 0,
|
||||
rollout_salt: 'domain-migration-v1',
|
||||
standalone_forwarding: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,94 @@
|
||||
// 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 DOMAIN_MIGRATION_ROLLOUT_BASIS_POINTS_MAX = EXPERIMENT_BUCKET_RESOLUTION;
|
||||
const DOMAIN_MIGRATION_MAX_TARGETED_USERS = 1000;
|
||||
const DEFAULT_DOMAIN_MIGRATION_SALT = 'domain-migration-v1';
|
||||
|
||||
const DOMAIN_MIGRATION_SALT_PATTERN = /^[\x20-\x7e]+$/u;
|
||||
|
||||
const DomainMigrationTargetIdSchema = z.string().regex(/^\d{1,20}$/u);
|
||||
const DomainMigrationTargetedUserIdsSchema = z
|
||||
.array(DomainMigrationTargetIdSchema)
|
||||
.max(DOMAIN_MIGRATION_MAX_TARGETED_USERS);
|
||||
|
||||
const domainMigrationConfigFields = {
|
||||
enabled: z.boolean(),
|
||||
config_version: z.number().int().min(0),
|
||||
rollout_basis_points: z.number().int().min(0).max(DOMAIN_MIGRATION_ROLLOUT_BASIS_POINTS_MAX),
|
||||
rollout_salt: z.string().trim().min(1).max(64).regex(DOMAIN_MIGRATION_SALT_PATTERN),
|
||||
included_user_ids: DomainMigrationTargetedUserIdsSchema,
|
||||
excluded_user_ids: DomainMigrationTargetedUserIdsSchema,
|
||||
anonymous_rollout_basis_points: z.number().int().min(0).max(DOMAIN_MIGRATION_ROLLOUT_BASIS_POINTS_MAX),
|
||||
standalone_forwarding: z.boolean(),
|
||||
};
|
||||
|
||||
export const DomainMigrationConfigSchema = z.object({
|
||||
enabled: domainMigrationConfigFields.enabled.default(false),
|
||||
config_version: domainMigrationConfigFields.config_version.default(0),
|
||||
rollout_basis_points: domainMigrationConfigFields.rollout_basis_points.default(0),
|
||||
rollout_salt: domainMigrationConfigFields.rollout_salt.default(DEFAULT_DOMAIN_MIGRATION_SALT),
|
||||
included_user_ids: domainMigrationConfigFields.included_user_ids.default([]),
|
||||
excluded_user_ids: domainMigrationConfigFields.excluded_user_ids.default([]),
|
||||
anonymous_rollout_basis_points: domainMigrationConfigFields.anonymous_rollout_basis_points.default(0),
|
||||
standalone_forwarding: domainMigrationConfigFields.standalone_forwarding.default(false),
|
||||
});
|
||||
|
||||
export type DomainMigrationConfig = z.infer<typeof DomainMigrationConfigSchema>;
|
||||
|
||||
export const DEFAULT_DOMAIN_MIGRATION_CONFIG: DomainMigrationConfig = DomainMigrationConfigSchema.parse({});
|
||||
|
||||
export const DomainMigrationConfigUpdateRequest = z
|
||||
.object(domainMigrationConfigFields)
|
||||
.omit({config_version: true})
|
||||
.partial();
|
||||
|
||||
export type DomainMigrationConfigUpdateRequest = z.infer<typeof DomainMigrationConfigUpdateRequest>;
|
||||
|
||||
export const DomainMigrationConfigResponse = DomainMigrationConfigSchema;
|
||||
|
||||
export type DomainMigrationConfigResponse = z.infer<typeof DomainMigrationConfigResponse>;
|
||||
|
||||
export const DomainMigrationAssignmentResponse = z.object({
|
||||
enabled: domainMigrationConfigFields.enabled,
|
||||
});
|
||||
|
||||
export type DomainMigrationAssignmentResponse = z.infer<typeof DomainMigrationAssignmentResponse>;
|
||||
|
||||
export const INERT_DOMAIN_MIGRATION_ASSIGNMENT: DomainMigrationAssignmentResponse = {
|
||||
enabled: false,
|
||||
};
|
||||
|
||||
export function resolveDomainMigrationAssignment(
|
||||
config: DomainMigrationConfig,
|
||||
userId: string,
|
||||
): DomainMigrationAssignmentResponse {
|
||||
if (!config.enabled) return {...INERT_DOMAIN_MIGRATION_ASSIGNMENT};
|
||||
if (config.excluded_user_ids.includes(userId)) return {...INERT_DOMAIN_MIGRATION_ASSIGNMENT};
|
||||
if (config.included_user_ids.includes(userId)) return {enabled: true};
|
||||
return {enabled: experimentBucket(userId, config.rollout_salt) < config.rollout_basis_points};
|
||||
}
|
||||
|
||||
export const DomainMigrationDiscoveryResponse = z.object({
|
||||
enabled: domainMigrationConfigFields.enabled.describe('Whether the domain migration is switched on'),
|
||||
anonymous_rollout_basis_points: domainMigrationConfigFields.anonymous_rollout_basis_points.describe(
|
||||
'Share of logged-out devices, in basis points, that move to the new domain',
|
||||
),
|
||||
rollout_salt: domainMigrationConfigFields.rollout_salt.describe('Salt used to bucket devices and users'),
|
||||
standalone_forwarding: domainMigrationConfigFields.standalone_forwarding.describe(
|
||||
'Whether installed desktop web apps forward to the new domain after moving their session',
|
||||
),
|
||||
});
|
||||
|
||||
export type DomainMigrationDiscoveryResponse = z.infer<typeof DomainMigrationDiscoveryResponse>;
|
||||
|
||||
export function toDomainMigrationDiscovery(config: DomainMigrationConfig): DomainMigrationDiscoveryResponse {
|
||||
return {
|
||||
enabled: config.enabled,
|
||||
anonymous_rollout_basis_points: config.anonymous_rollout_basis_points,
|
||||
rollout_salt: config.rollout_salt,
|
||||
standalone_forwarding: config.standalone_forwarding,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {z} from 'zod';
|
||||
|
||||
export const ORIGIN_HANDOFF_MAX_PAYLOAD_LENGTH = 8 * 1024 * 1024;
|
||||
|
||||
const Base64UrlPattern = /^[A-Za-z0-9_-]+$/u;
|
||||
|
||||
export const OriginHandoffCreateRequest = z.object({
|
||||
nonce_hash: z
|
||||
.string()
|
||||
.regex(/^[0-9a-f]{64}$/u)
|
||||
.describe('Lowercase hex SHA-256 digest of the nonce the receiving origin holds'),
|
||||
payload: z
|
||||
.string()
|
||||
.min(1)
|
||||
.max(ORIGIN_HANDOFF_MAX_PAYLOAD_LENGTH)
|
||||
.regex(Base64UrlPattern)
|
||||
.describe('Encrypted client state encoded as base64url'),
|
||||
});
|
||||
|
||||
export type OriginHandoffCreateRequest = z.infer<typeof OriginHandoffCreateRequest>;
|
||||
|
||||
export const OriginHandoffCreateResponse = z.object({
|
||||
handoff_id: z.string().describe('Single-use identifier the receiving origin redeems'),
|
||||
});
|
||||
|
||||
export type OriginHandoffCreateResponse = z.infer<typeof OriginHandoffCreateResponse>;
|
||||
|
||||
export const OriginHandoffRedeemRequest = z.object({
|
||||
handoff_id: z
|
||||
.string()
|
||||
.regex(/^[A-Za-z0-9_-]{43}$/u)
|
||||
.describe('Identifier returned when the handoff was created'),
|
||||
nonce: z
|
||||
.string()
|
||||
.min(16)
|
||||
.max(256)
|
||||
.regex(Base64UrlPattern)
|
||||
.describe('Nonce whose SHA-256 digest was sent when the handoff was created'),
|
||||
});
|
||||
|
||||
export type OriginHandoffRedeemRequest = z.infer<typeof OriginHandoffRedeemRequest>;
|
||||
|
||||
export const OriginHandoffRedeemResponse = z.object({
|
||||
payload: z.string().describe('Encrypted client state encoded as base64url'),
|
||||
});
|
||||
|
||||
export type OriginHandoffRedeemResponse = z.infer<typeof OriginHandoffRedeemResponse>;
|
||||
@@ -1,5 +1,9 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {
|
||||
DomainMigrationAssignmentResponse,
|
||||
INERT_DOMAIN_MIGRATION_ASSIGNMENT,
|
||||
} from '@fluxer/schema/src/domains/admin/DomainMigrationSchemas';
|
||||
import {
|
||||
INERT_VOICE_NOISE_SUPPRESSION_ASSIGNMENT,
|
||||
VoiceNoiseSuppressionAssignmentResponse,
|
||||
@@ -42,6 +46,7 @@ export type ExperimentDeliveryConfigResponse = z.infer<typeof ExperimentDelivery
|
||||
|
||||
const ExperimentAssignmentsSchema = z.object({
|
||||
voice_noise_suppression: VoiceNoiseSuppressionAssignmentResponse.optional(),
|
||||
domain_migration: DomainMigrationAssignmentResponse.optional(),
|
||||
});
|
||||
|
||||
export const ExperimentAssignmentsResponse = z.object({
|
||||
@@ -63,3 +68,9 @@ export function readVoiceNoiseSuppressionAssignment(
|
||||
): VoiceNoiseSuppressionAssignmentResponse {
|
||||
return response.assignments.voice_noise_suppression ?? INERT_VOICE_NOISE_SUPPRESSION_ASSIGNMENT;
|
||||
}
|
||||
|
||||
export function readDomainMigrationAssignment(
|
||||
response: ExperimentAssignmentsResponse,
|
||||
): DomainMigrationAssignmentResponse {
|
||||
return response.assignments.domain_migration ?? INERT_DOMAIN_MIGRATION_ASSIGNMENT;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {DomainMigrationDiscoveryResponse} from '@fluxer/schema/src/domains/admin/DomainMigrationSchemas';
|
||||
import {SsoStatusResponse} from '@fluxer/schema/src/domains/auth/AuthSchemas';
|
||||
import {createNamedStringLiteralUnion} from '@fluxer/schema/src/primitives/SchemaPrimitives';
|
||||
import {z} from 'zod';
|
||||
@@ -187,6 +188,9 @@ export const WellKnownFluxerResponse = z.object({
|
||||
limits: LimitConfigResponse.describe('Limit configuration with rules and trait definitions'),
|
||||
push: InstancePushSchema,
|
||||
app_public: InstanceAppPublicSchema.describe('Public application configuration for client-side features'),
|
||||
domain_migration: DomainMigrationDiscoveryResponse.optional().describe(
|
||||
'Web domain migration switch and anonymous rollout, only acted on by official instance clients',
|
||||
),
|
||||
});
|
||||
|
||||
export type WellKnownFluxerResponse = z.infer<typeof WellKnownFluxerResponse>;
|
||||
|
||||
@@ -615,6 +615,7 @@ export const PushSubscribeRequest = z.object({
|
||||
})
|
||||
.describe('Encryption keys for the push subscription'),
|
||||
user_agent: createStringType(1, 1024).optional().describe('The user agent string identifying the client'),
|
||||
installed_app: z.boolean().optional().describe('Whether the client runs in an installed web app window'),
|
||||
});
|
||||
|
||||
export type PushSubscribeRequest = z.infer<typeof PushSubscribeRequest>;
|
||||
@@ -629,6 +630,7 @@ export const PushRotateRequest = z.object({
|
||||
})
|
||||
.describe('Encryption keys for the new push subscription'),
|
||||
user_agent: createStringType(1, 1024).optional().describe('The user agent string identifying the client'),
|
||||
installed_app: z.boolean().optional().describe('Whether the client runs in an installed web app window'),
|
||||
});
|
||||
|
||||
export type PushRotateRequest = z.infer<typeof PushRotateRequest>;
|
||||
|
||||
Reference in New Issue
Block a user