// Copyright 2023 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import { v4 as generateUuid } from 'uuid'; import { z } from 'zod'; import type { ServiceId, Aci, Pni } from '@signalapp/libsignal-client'; import { isValidUuid } from '../util/isValidUuid'; import * as log from '../logging/log'; import type { LoggerType } from './Logging'; import { isAciString } from '../util/isAciString'; export enum ServiceIdKind { ACI = 'ACI', PNI = 'PNI', Unknown = 'Unknown', } export type PniString = string & { __pni: never }; export type UntaggedPniString = string & { __untagged_pni: never }; export type AciString = string & { __aci: never }; export type ServiceIdString = PniString | AciString; export function isServiceIdString( value?: string | null ): value is ServiceIdString { return isAciString(value) || isPniString(value); } export function isPniString(value?: string | null): value is PniString { if (value == null) { return false; } if (value.startsWith('PNI:')) { return true; } return false; } export function isUntaggedPniString( value?: string | null ): value is UntaggedPniString { return isValidUuid(value); } export function toTaggedPni(untagged: UntaggedPniString): PniString { return `PNI:${untagged}` as PniString; } export function toUntaggedPni(pni: PniString): UntaggedPniString { return pni.replace(/^PNI:/i, '') as UntaggedPniString; } export function normalizeServiceId( rawServiceId: string, context: string, logger?: Pick ): ServiceIdString; export function normalizeServiceId( rawServiceId: string | undefined | null, context: string, logger?: Pick ): ServiceIdString | undefined; export function normalizeServiceId( rawServiceId: string | undefined | null, context: string, logger: Pick = log ): ServiceIdString | undefined { if (rawServiceId == null) { return undefined; } const result = rawServiceId.toLowerCase().replace(/^pni:/, 'PNI:'); if (!isAciString(result) && !isPniString(result)) { logger.warn( `Normalizing invalid serviceId: ${rawServiceId} to ${result} in context "${context}"` ); // Cast anyway we don't want to throw here return result as ServiceIdString; } return result; } export function normalizePni( rawPni: string, context: string, logger?: Pick ): PniString; export function normalizePni( rawPni: string | undefined | null, context: string, logger?: Pick ): PniString | undefined; export function normalizePni( rawPni: string | undefined | null, context: string, logger: Pick = log ): PniString | undefined { if (rawPni == null) { return undefined; } const result = rawPni.toLowerCase().replace(/^pni:/, 'PNI:'); if (!isPniString(result)) { logger.warn( `Normalizing invalid pni: ${rawPni} to ${result} in context "${context}"` ); // Cast anyway we don't want to throw here return result as PniString; } return result; } // For tests export function generateAci(): AciString { return generateUuid() as AciString; } export function generatePni(): PniString { return `PNI:${generateUuid()}` as PniString; } export function getAciFromPrefix(prefix: string): AciString { let padded = prefix; while (padded.length < 8) { padded += '0'; } return `${padded}-0000-4000-8000-${'0'.repeat(12)}` as AciString; } export const aciSchema = z .string() .refine(isAciString) .transform(x => { if (!isAciString(x)) { throw new Error('Refine did not throw!'); } return x; }); export const untaggedPniSchema = z .string() .refine(isUntaggedPniString) .transform(x => { if (!isUntaggedPniString(x)) { throw new Error('Refine did not throw!'); } return x; }); export const serviceIdSchema = z .string() .refine(isServiceIdString) .transform(x => { if (!isServiceIdString(x)) { throw new Error('Refine did not throw!'); } return x; }); // Note: getServiceIdString() returns normalized string so we can cast it // without normalizing. export function fromServiceIdObject(obj: ServiceId): ServiceIdString { return obj.getServiceIdString() as ServiceIdString; } export function fromAciObject(obj: Aci): AciString { return obj.getServiceIdString() as AciString; } export function fromPniObject(obj: Pni): PniString { return obj.getServiceIdString() as PniString; }