signal-desktop/ts/textsecure/WebAPI.ts

3509 lines
96 KiB
TypeScript
Raw Normal View History

2023-01-03 19:55:46 +00:00
// Copyright 2020 Signal Messenger, LLC
2020-10-30 20:34:04 +00:00
// SPDX-License-Identifier: AGPL-3.0-only
/* eslint-disable no-param-reassign */
/* eslint-disable guard-for-in */
/* eslint-disable no-restricted-syntax */
/* eslint-disable @typescript-eslint/no-explicit-any */
import type { Response } from 'node-fetch';
import fetch from 'node-fetch';
import type { Agent } from 'https';
2023-01-12 20:58:53 +00:00
import { escapeRegExp, isNumber, isString, isObject } from 'lodash';
import PQueue from 'p-queue';
import { v4 as getGuid } from 'uuid';
2021-05-25 22:40:04 +00:00
import { z } from 'zod';
import type { Readable } from 'stream';
2023-02-27 22:34:43 +00:00
import type { connection as WebSocket } from 'websocket';
import { assertDev, strictAssert } from '../util/assert';
import { isRecord } from '../util/isRecord';
import * as durations from '../util/durations';
import type { ExplodePromiseResultType } from '../util/explodePromise';
import { explodePromise } from '../util/explodePromise';
import { getUserAgent } from '../util/getUserAgent';
import {
getTimeoutStream,
getStreamWithTimeout,
} from '../util/getStreamWithTimeout';
2021-11-02 23:01:13 +00:00
import { formatAcceptLanguageHeader } from '../util/userLanguages';
2023-02-08 17:14:59 +00:00
import { toWebSafeBase64, fromWebSafeBase64 } from '../util/webSafeBase64';
2022-03-09 19:28:40 +00:00
import { getBasicAuth } from '../util/getBasicAuth';
import { createHTTPSAgent } from '../util/createHTTPSAgent';
2023-08-29 23:58:48 +00:00
import { createProxyAgent } from '../util/createProxyAgent';
import type { SocketStatus } from '../types/SocketStatus';
2023-08-29 00:41:32 +00:00
import { VerificationTransport } from '../types/VerificationTransport';
2021-09-24 00:49:05 +00:00
import { toLogFormat } from '../types/errors';
2021-07-09 19:36:10 +00:00
import { isPackIdValid, redactPackId } from '../types/Stickers';
2023-08-16 20:54:39 +00:00
import type {
ServiceIdString,
AciString,
UntaggedPniString,
} from '../types/ServiceId';
2023-08-29 00:41:32 +00:00
import {
ServiceIdKind,
serviceIdSchema,
aciSchema,
untaggedPniSchema,
} from '../types/ServiceId';
2022-08-19 00:31:12 +00:00
import type { DirectoryConfigType } from '../types/RendererConfig';
2021-07-13 18:54:53 +00:00
import * as Bytes from '../Bytes';
2022-10-05 16:35:56 +00:00
import { randomInt } from '../Crypto';
2020-09-28 23:46:31 +00:00
import * as linkPreviewFetch from '../linkPreviews/linkPreviewFetch';
2021-11-02 23:01:13 +00:00
import { isBadgeImageFileUrlValid } from '../badges/isBadgeImageFileUrlValid';
2020-09-28 23:46:31 +00:00
import { SocketManager } from './SocketManager';
2022-10-26 23:17:14 +00:00
import type { CDSAuthType, CDSResponseType } from './cds/Types.d';
2022-06-15 01:15:33 +00:00
import { CDSI } from './cds/CDSI';
import type WebSocketResource from './WebsocketResources';
2021-06-22 14:46:42 +00:00
import { SignalService as Proto } from '../protobuf';
import { HTTPError } from './Errors';
import type MessageSender from './SendMessage';
import type {
WebAPICredentials,
IRequestHandler,
StorageServiceCallOptionsType,
StorageServiceCredentials,
} from './Types.d';
import { handleStatusCode, translateError } from './Utils';
import * as log from '../logging/log';
import { maybeParseUrl, urlPathFromComponents } from '../util/url';
import { SECOND } from '../util/durations';
// Note: this will break some code that expects to be able to use err.response when a
// web request fails, because it will force it to text. But it is very useful for
// debugging failed requests.
const DEBUG = false;
const DEFAULT_TIMEOUT = 30 * SECOND;
2020-08-20 22:15:50 +00:00
function _createRedactor(
...toReplace: ReadonlyArray<string | undefined>
): RedactUrl {
// NOTE: It would be nice to remove this cast, but TypeScript doesn't support
// it. However, there is [an issue][0] that discusses this in more detail.
// [0]: https://github.com/Microsoft/TypeScript/issues/16069
const stringsToReplace = toReplace.filter(Boolean) as Array<string>;
return href =>
stringsToReplace.reduce((result: string, stringToReplace: string) => {
const pattern = RegExp(escapeRegExp(stringToReplace), 'g');
const replacement = `[REDACTED]${stringToReplace.slice(-3)}`;
return result.replace(pattern, replacement);
}, href);
}
function _validateResponse(response: any, schema: any) {
try {
for (const i in schema) {
switch (schema[i]) {
case 'object':
case 'string':
case 'number':
// eslint-disable-next-line valid-typeof
if (typeof response[i] !== schema[i]) {
return false;
}
break;
default:
}
}
} catch (ex) {
return false;
}
return true;
}
const FIVE_MINUTES = 5 * durations.MINUTE;
const GET_ATTACHMENT_CHUNK_TIMEOUT = 10 * durations.SECOND;
type AgentCacheType = {
[name: string]: {
timestamp: number;
2023-08-29 23:58:48 +00:00
agent: ReturnType<typeof createProxyAgent> | Agent;
};
2018-11-07 19:20:43 +00:00
};
const agents: AgentCacheType = {};
2018-11-07 19:20:43 +00:00
function getContentType(response: Response) {
2019-01-16 03:03:56 +00:00
if (response.headers && response.headers.get) {
return response.headers.get('content-type');
}
return null;
}
type FetchHeaderListType = { [name: string]: string };
export type HeaderListType = { [name: string]: string | ReadonlyArray<string> };
2021-11-30 19:33:51 +00:00
type HTTPCodeType = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD';
2020-08-20 22:15:50 +00:00
type RedactUrl = (url: string) => string;
type PromiseAjaxOptionsType = {
socketManager?: SocketManager;
2020-09-09 02:25:05 +00:00
basicAuth?: string;
certificateAuthority?: string;
contentType?: string;
2021-09-24 00:49:05 +00:00
data?: Uint8Array | string;
disableRetries?: boolean;
disableSessionResumption?: boolean;
headers?: HeaderListType;
host?: string;
password?: string;
path?: string;
proxyUrl?: string;
2020-08-20 22:15:50 +00:00
redactUrl?: RedactUrl;
redirect?: 'error' | 'follow' | 'manual';
responseType?:
| 'json'
| 'jsonwithdetails'
| 'bytes'
| 'byteswithdetails'
| 'stream';
serverUrl?: string;
stack?: string;
timeout?: number;
type: HTTPCodeType;
user?: string;
validateResponse?: any;
version: string;
abortSignal?: AbortSignal;
} & (
| {
unauthenticated?: false;
accessKey?: string;
}
| {
unauthenticated: true;
accessKey: undefined | string;
}
);
2022-06-15 01:15:33 +00:00
type JSONWithDetailsType<Data = unknown> = {
data: Data;
2020-09-09 02:25:05 +00:00
contentType: string | null;
response: Response;
};
2021-09-24 00:49:05 +00:00
type BytesWithDetailsType = {
data: Uint8Array;
2020-09-09 02:25:05 +00:00
contentType: string | null;
2020-09-04 01:25:19 +00:00
response: Response;
};
2022-10-18 17:12:02 +00:00
export const multiRecipient200ResponseSchema = z.object({
uuids404: z.array(serviceIdSchema).optional(),
2022-10-18 17:12:02 +00:00
needsSync: z.boolean().optional(),
});
2021-05-25 22:40:04 +00:00
export type MultiRecipient200ResponseType = z.infer<
typeof multiRecipient200ResponseSchema
>;
export const multiRecipient409ResponseSchema = z.array(
2022-10-18 17:12:02 +00:00
z.object({
uuid: serviceIdSchema,
2022-10-18 17:12:02 +00:00
devices: z.object({
missingDevices: z.array(z.number()).optional(),
extraDevices: z.array(z.number()).optional(),
}),
})
2021-05-25 22:40:04 +00:00
);
export type MultiRecipient409ResponseType = z.infer<
typeof multiRecipient409ResponseSchema
>;
export const multiRecipient410ResponseSchema = z.array(
2022-10-18 17:12:02 +00:00
z.object({
uuid: serviceIdSchema,
2022-10-18 17:12:02 +00:00
devices: z.object({
staleDevices: z.array(z.number()).optional(),
}),
})
2021-05-25 22:40:04 +00:00
);
export type MultiRecipient410ResponseType = z.infer<
typeof multiRecipient410ResponseSchema
>;
function isSuccess(status: number): boolean {
return status >= 0 && status < 400;
}
function getHostname(url: string): string {
const urlObject = new URL(url);
return urlObject.hostname;
}
async function _promiseAjax(
providedUrl: string | null,
options: PromiseAjaxOptionsType
2021-09-24 00:49:05 +00:00
): Promise<unknown> {
const { proxyUrl, socketManager } = options;
const url = providedUrl || `${options.host}/${options.path}`;
const logType = socketManager ? '(WS)' : '(REST)';
const redactedURL = options.redactUrl ? options.redactUrl(url) : url;
const unauthLabel = options.unauthenticated ? ' (unauth)' : '';
const logId = `${options.type} ${logType} ${redactedURL}${unauthLabel}`;
log.info(logId);
2021-06-09 22:28:54 +00:00
const timeout =
typeof options.timeout === 'number' ? options.timeout : DEFAULT_TIMEOUT;
const agentType = options.unauthenticated ? 'unauth' : 'auth';
const cacheKey = `${proxyUrl}-${agentType}`;
const { timestamp } = agents[cacheKey] || { timestamp: null };
if (!timestamp || timestamp + FIVE_MINUTES < Date.now()) {
if (timestamp) {
log.info(`Cycling agent for type ${cacheKey}`);
}
agents[cacheKey] = {
agent: proxyUrl
2023-08-29 23:58:48 +00:00
? createProxyAgent(proxyUrl)
: createHTTPSAgent({
keepAlive: !options.disableSessionResumption,
maxCachedSessions: options.disableSessionResumption ? 0 : undefined,
}),
timestamp: Date.now(),
};
2021-06-09 22:28:54 +00:00
}
const agentEntry = agents[cacheKey];
const agent = agentEntry?.agent ?? null;
const fetchOptions = {
method: options.type,
body: options.data,
headers: {
'User-Agent': getUserAgent(options.version),
'X-Signal-Agent': 'OWD',
...options.headers,
} as FetchHeaderListType,
redirect: options.redirect,
agent,
ca: options.certificateAuthority,
timeout,
abortSignal: options.abortSignal,
};
2021-06-09 22:28:54 +00:00
2021-09-24 00:49:05 +00:00
if (fetchOptions.body instanceof Uint8Array) {
// node-fetch doesn't support Uint8Array, only node Buffer
const contentLength = fetchOptions.body.byteLength;
fetchOptions.body = Buffer.from(fetchOptions.body);
// node-fetch doesn't set content-length like S3 requires
fetchOptions.headers['Content-Length'] = contentLength.toString();
}
const { accessKey, basicAuth, unauthenticated } = options;
if (basicAuth) {
fetchOptions.headers.Authorization = `Basic ${basicAuth}`;
} else if (unauthenticated) {
if (accessKey) {
// Access key is already a Base64 string
fetchOptions.headers['Unidentified-Access-Key'] = accessKey;
2019-01-16 03:03:56 +00:00
}
} else if (options.user && options.password) {
2022-03-09 19:28:40 +00:00
fetchOptions.headers.Authorization = getBasicAuth({
username: options.user,
password: options.password,
});
}
if (options.contentType) {
fetchOptions.headers['Content-Type'] = options.contentType;
}
let response: Response;
let result: string | Uint8Array | Readable | unknown;
try {
response = socketManager
? await socketManager.fetch(url, fetchOptions)
: await fetch(url, fetchOptions);
if (
options.serverUrl &&
getHostname(options.serverUrl) === getHostname(url)
) {
await handleStatusCode(response.status);
2018-11-07 19:20:43 +00:00
if (!unauthenticated && response.status === 401) {
log.error('Got 401 from Signal Server. We might be unlinked.');
window.Whisper.events.trigger('mightBeUnlinked');
2018-11-07 19:20:43 +00:00
}
}
if (DEBUG && !isSuccess(response.status)) {
result = await response.text();
} else if (
(options.responseType === 'json' ||
options.responseType === 'jsonwithdetails') &&
/^application\/json(;.*)?$/.test(
response.headers.get('Content-Type') || ''
)
) {
result = await response.json();
} else if (
2021-09-24 00:49:05 +00:00
options.responseType === 'bytes' ||
options.responseType === 'byteswithdetails'
) {
2021-09-24 00:49:05 +00:00
result = await response.buffer();
} else if (options.responseType === 'stream') {
result = response.body;
} else {
result = await response.textConverted();
}
} catch (e) {
log.error(logId, 0, 'Error');
const stack = `${e.stack}\nInitial stack:\n${options.stack}`;
throw makeHTTPError('promiseAjax catch', 0, {}, e.toString(), stack);
}
if (!isSuccess(response.status)) {
log.error(logId, response.status, 'Error');
throw makeHTTPError(
'promiseAjax: error response',
response.status,
response.headers.raw(),
result,
options.stack
);
}
if (
options.responseType === 'json' ||
options.responseType === 'jsonwithdetails'
) {
if (options.validateResponse) {
if (!_validateResponse(result, options.validateResponse)) {
log.error(logId, response.status, 'Error');
throw makeHTTPError(
'promiseAjax: invalid response',
response.status,
response.headers.raw(),
result,
options.stack
);
}
}
}
2019-01-16 03:03:56 +00:00
log.info(logId, response.status, 'Success');
2021-09-24 00:49:05 +00:00
if (options.responseType === 'byteswithdetails') {
assertDev(result instanceof Uint8Array, 'Expected Uint8Array result');
2021-09-24 00:49:05 +00:00
const fullResult: BytesWithDetailsType = {
data: result,
contentType: getContentType(response),
response,
};
2020-09-09 02:25:05 +00:00
return fullResult;
}
if (options.responseType === 'jsonwithdetails') {
const fullResult: JSONWithDetailsType = {
data: result,
contentType: getContentType(response),
response,
};
2019-01-16 03:03:56 +00:00
return fullResult;
}
return result;
}
async function _retryAjax(
url: string | null,
options: PromiseAjaxOptionsType,
providedLimit?: number,
providedCount?: number
2021-09-24 00:49:05 +00:00
): Promise<unknown> {
const count = (providedCount || 0) + 1;
const limit = providedLimit || 3;
2021-09-24 00:49:05 +00:00
try {
return await _promiseAjax(url, options);
} catch (e) {
2021-09-22 00:58:03 +00:00
if (e instanceof HTTPError && e.code === -1 && count < limit) {
return new Promise(resolve => {
setTimeout(() => {
resolve(_retryAjax(url, options, limit, count));
}, 1000);
});
}
throw e;
2021-09-24 00:49:05 +00:00
}
}
2021-09-24 00:49:05 +00:00
function _outerAjax(
providedUrl: string | null,
options: PromiseAjaxOptionsType & { responseType: 'json' }
): Promise<unknown>;
function _outerAjax(
providedUrl: string | null,
options: PromiseAjaxOptionsType & { responseType: 'jsonwithdetails' }
): Promise<JSONWithDetailsType>;
function _outerAjax(
providedUrl: string | null,
options: PromiseAjaxOptionsType & { responseType?: 'bytes' }
): Promise<Uint8Array>;
function _outerAjax(
providedUrl: string | null,
options: PromiseAjaxOptionsType & { responseType: 'byteswithdetails' }
): Promise<BytesWithDetailsType>;
function _outerAjax(
providedUrl: string | null,
options: PromiseAjaxOptionsType & { responseType?: 'stream' }
): Promise<Readable>;
2021-09-24 00:49:05 +00:00
function _outerAjax(
providedUrl: string | null,
options: PromiseAjaxOptionsType
): Promise<unknown>;
async function _outerAjax(
url: string | null,
options: PromiseAjaxOptionsType
): Promise<unknown> {
options.stack = new Error().stack; // just in case, save stack here.
if (options.disableRetries) {
return _promiseAjax(url, options);
}
return _retryAjax(url, options);
}
function makeHTTPError(
message: string,
providedCode: number,
headers: HeaderListType,
2021-09-24 00:49:05 +00:00
response: unknown,
stack?: string
) {
return new HTTPError(message, {
code: providedCode,
headers,
response,
stack,
});
}
const URL_CALLS = {
2021-11-30 19:33:51 +00:00
accountExistence: 'v1/accounts/account',
2023-05-04 20:58:53 +00:00
attachmentId: 'v3/attachments/form/upload',
2020-09-09 02:25:05 +00:00
attestation: 'v1/attestation',
batchIdentityCheck: 'v1/profile/identity_check/batch',
challenge: 'v1/challenge',
2020-09-09 02:25:05 +00:00
config: 'v1/config',
deliveryCert: 'v1/certificate/delivery',
2021-11-12 20:45:30 +00:00
directoryAuthV2: 'v2/directory/auth',
2020-09-09 02:25:05 +00:00
discovery: 'v1/discovery',
2020-11-20 17:30:45 +00:00
getGroupAvatarUpload: 'v1/groups/avatar/form',
2022-07-08 20:46:25 +00:00
getGroupCredentials: 'v1/certificate/auth/group',
2020-09-09 02:25:05 +00:00
getIceServers: 'v1/accounts/turn',
2022-11-09 02:38:19 +00:00
getOnboardingStoryManifest:
'dynamic/desktop/stories/onboarding/manifest.json',
2020-09-09 02:25:05 +00:00
getStickerPackUpload: 'v1/sticker/pack/form',
2023-02-27 22:34:43 +00:00
getArtAuth: 'v1/art/auth',
2020-09-09 02:25:05 +00:00
groupLog: 'v1/groups/logs',
groupJoinedAtVersion: 'v1/groups/joined_at_version',
2020-09-09 02:25:05 +00:00
groups: 'v1/groups',
groupsViaLink: 'v1/groups/join/',
2020-11-13 19:57:55 +00:00
groupToken: 'v1/groups/token',
keys: 'v2/keys',
2023-08-29 00:41:32 +00:00
linkDevice: 'v1/devices/link',
messages: 'v1/messages',
2021-05-25 22:40:04 +00:00
multiRecipient: 'v1/messages/multi_recipient',
2023-02-23 21:32:19 +00:00
phoneNumberDiscoverability: 'v2/accounts/phone_number_discoverability',
profile: 'v1/profile',
2023-08-29 00:41:32 +00:00
registration: 'v1/registration',
registerCapabilities: 'v1/devices/capabilities',
reportMessage: 'v1/messages/report',
signed: 'v2/keys/signed',
storageManifest: 'v1/storage/manifest',
storageModify: 'v1/storage/',
storageRead: 'v1/storage/read',
storageToken: 'v1/storage/auth',
subscriptions: 'v1/subscription',
2023-10-06 21:31:17 +00:00
subscriptionConfiguration: 'v1/subscription/configuration',
supportUnauthenticatedDelivery: 'v1/devices/unauthenticated_delivery',
2020-09-09 02:25:05 +00:00
updateDeviceName: 'v1/accounts/name',
2023-02-08 17:14:59 +00:00
username: 'v1/accounts/username_hash',
reserveUsername: 'v1/accounts/username_hash/reserve',
confirmUsername: 'v1/accounts/username_hash/confirm',
2023-07-20 03:14:08 +00:00
usernameLink: 'v1/accounts/username_link',
2023-08-29 00:41:32 +00:00
verificationSession: 'v1/verification/session',
whoami: 'v1/accounts/whoami',
};
const WEBSOCKET_CALLS = new Set<keyof typeof URL_CALLS>([
// MessageController
'messages',
'multiRecipient',
'reportMessage',
// ProfileController
'profile',
2023-08-16 17:44:33 +00:00
// AttachmentControllerV3
'attachmentId',
// RemoteConfigController
'config',
2021-08-04 00:37:17 +00:00
// Certificate
'deliveryCert',
'getGroupCredentials',
// Devices
2023-08-29 00:41:32 +00:00
'linkDevice',
2021-08-04 00:37:17 +00:00
'registerCapabilities',
'supportUnauthenticatedDelivery',
// Directory
2021-11-12 20:45:30 +00:00
'directoryAuthV2',
2021-08-04 00:37:17 +00:00
// Storage
'storageToken',
2023-02-23 21:32:19 +00:00
// Account V2
'phoneNumberDiscoverability',
]);
type InitializeOptionsType = {
url: string;
storageUrl: string;
2021-11-02 23:01:13 +00:00
updatesUrl: string;
2022-11-09 02:38:19 +00:00
resourcesUrl: string;
2023-02-27 22:34:43 +00:00
artCreatorUrl: string;
cdnUrlObject: {
readonly '0': string;
readonly [propName: string]: string;
};
certificateAuthority: string;
contentProxyUrl: string;
proxyUrl: string | undefined;
version: string;
2022-08-19 00:31:12 +00:00
directoryConfig: DirectoryConfigType;
};
2021-09-28 23:38:55 +00:00
export type MessageType = Readonly<{
type: number;
destinationDeviceId: number;
destinationRegistrationId: number;
content: string;
}>;
type AjaxOptionsType = {
2020-09-09 02:25:05 +00:00
basicAuth?: string;
call: keyof typeof URL_CALLS;
contentType?: string;
2021-09-24 00:49:05 +00:00
data?: Uint8Array | Buffer | Uint8Array | string;
disableSessionResumption?: boolean;
2021-05-25 22:40:04 +00:00
headers?: HeaderListType;
host?: string;
httpType: HTTPCodeType;
2021-09-24 00:49:05 +00:00
jsonData?: unknown;
password?: string;
2020-08-20 22:15:50 +00:00
redactUrl?: RedactUrl;
responseType?: 'json' | 'bytes' | 'byteswithdetails' | 'stream';
2021-09-22 00:58:03 +00:00
schema?: unknown;
timeout?: number;
urlParameters?: string;
username?: string;
validateResponse?: any;
isRegistration?: true;
2022-10-18 17:12:02 +00:00
abortSignal?: AbortSignal;
} & (
| {
unauthenticated?: false;
accessKey?: string;
}
| {
unauthenticated: true;
accessKey: undefined | string;
}
);
2021-08-19 00:13:32 +00:00
export type WebAPIConnectOptionsType = WebAPICredentials & {
useWebSocket?: boolean;
2022-10-05 00:48:25 +00:00
hasStoriesDisabled: boolean;
2021-08-19 00:13:32 +00:00
};
export type WebAPIConnectType = {
2021-08-19 00:13:32 +00:00
connect: (options: WebAPIConnectOptionsType) => WebAPIType;
};
2024-02-16 19:49:48 +00:00
export type CapabilitiesType = Record<string, never>;
export type CapabilitiesUploadType = Record<string, never>;
2020-11-20 17:30:45 +00:00
2021-09-24 00:49:05 +00:00
type StickerPackManifestType = Uint8Array;
2020-09-09 02:25:05 +00:00
export type GroupCredentialType = {
credential: string;
redemptionTime: number;
};
export type GroupCredentialsType = {
groupPublicParamsHex: string;
authCredentialPresentationHex: string;
};
export type GetGroupLogOptionsType = Readonly<{
startVersion: number | undefined;
includeFirstState: boolean;
includeLastState: boolean;
maxSupportedChangeEpoch: number;
}>;
2020-09-09 02:25:05 +00:00
export type GroupLogResponseType = {
currentRevision?: number;
start?: number;
end?: number;
2021-06-22 14:46:42 +00:00
changes: Proto.GroupChanges;
2020-09-09 02:25:05 +00:00
};
2021-07-19 19:26:06 +00:00
export type ProfileRequestDataType = {
about: string | null;
aboutEmoji: string | null;
avatar: boolean;
sameAvatar: boolean;
2021-07-19 19:26:06 +00:00
commitment: string;
name: string;
paymentAddress: string | null;
phoneNumberSharing: string | null;
2021-07-19 19:26:06 +00:00
version: string;
};
2022-10-18 17:12:02 +00:00
const uploadAvatarHeadersZod = z.object({
acl: z.string(),
algorithm: z.string(),
credential: z.string(),
date: z.string(),
key: z.string(),
policy: z.string(),
signature: z.string(),
});
2021-07-19 19:26:06 +00:00
export type UploadAvatarHeadersType = z.infer<typeof uploadAvatarHeadersZod>;
const remoteConfigResponseZod = z.object({
config: z
.object({
name: z.string(),
enabled: z.boolean(),
value: z.string().or(z.null()).optional(),
})
.array(),
serverEpochTime: z.number(),
});
export type RemoteConfigResponseType = z.infer<typeof remoteConfigResponseZod>;
2021-09-22 00:58:03 +00:00
export type ProfileType = Readonly<{
identityKey?: string;
name?: string;
about?: string;
aboutEmoji?: string;
avatar?: string;
phoneNumberSharing?: string;
2021-09-22 00:58:03 +00:00
unidentifiedAccess?: string;
unrestrictedUnidentifiedAccess?: string;
uuid?: string;
credential?: string;
2022-07-08 20:46:25 +00:00
capabilities?: CapabilitiesType;
paymentAddress?: string;
2021-11-02 23:01:13 +00:00
badges?: unknown;
2021-09-22 00:58:03 +00:00
}>;
2023-02-08 17:14:59 +00:00
export type GetAccountForUsernameOptionsType = Readonly<{
hash: Uint8Array;
}>;
2023-02-08 17:14:59 +00:00
const getAccountForUsernameResultZod = z.object({
uuid: aciSchema,
2023-02-08 17:14:59 +00:00
});
export type GetAccountForUsernameResultType = z.infer<
typeof getAccountForUsernameResultZod
>;
2021-09-24 00:49:05 +00:00
export type GetIceServersResultType = Readonly<{
username: string;
password: string;
urls: ReadonlyArray<string>;
}>;
export type GetDevicesResultType = ReadonlyArray<
Readonly<{
id: number;
name: string;
lastSeen: number;
created: number;
}>
>;
export type GetSenderCertificateResultType = Readonly<{ certificate: string }>;
export type MakeProxiedRequestResultType =
| Uint8Array
| {
result: BytesWithDetailsType;
totalSize: number;
};
2022-10-18 17:12:02 +00:00
const whoamiResultZod = z.object({
uuid: z.string(),
pni: z.string(),
number: z.string(),
username: z.string().or(z.null()).optional(),
});
2022-07-18 22:32:00 +00:00
export type WhoamiResultType = z.infer<typeof whoamiResultZod>;
2021-09-24 00:49:05 +00:00
export type CdsLookupOptionsType = Readonly<{
2021-12-06 22:54:20 +00:00
e164s: ReadonlyArray<string>;
acisAndAccessKeys?: ReadonlyArray<{ aci: AciString; accessKey: string }>;
2022-08-19 00:31:12 +00:00
returnAcisWithoutUaks?: boolean;
useLibsignal?: boolean;
2021-12-06 22:54:20 +00:00
}>;
type GetProfileCommonOptionsType = Readonly<
{
userLanguages: ReadonlyArray<string>;
} & (
| {
profileKeyVersion?: undefined;
profileKeyCredentialRequest?: undefined;
}
| {
profileKeyVersion: string;
profileKeyCredentialRequest?: string;
}
)
>;
export type GetProfileOptionsType = GetProfileCommonOptionsType &
Readonly<{
accessKey?: undefined;
}>;
export type GetProfileUnauthOptionsType = GetProfileCommonOptionsType &
Readonly<{
accessKey: string;
}>;
2022-07-08 20:46:25 +00:00
export type GetGroupCredentialsOptionsType = Readonly<{
startDayInMs: number;
endDayInMs: number;
}>;
2022-07-28 16:35:29 +00:00
export type GetGroupCredentialsResultType = Readonly<{
2023-08-16 20:54:39 +00:00
pni?: UntaggedPniString | null;
2022-07-28 16:35:29 +00:00
credentials: ReadonlyArray<GroupCredentialType>;
}>;
const verifyServiceIdResponse = z.object({
2022-10-18 17:12:02 +00:00
elements: z.array(
z.object({
uuid: serviceIdSchema,
2022-10-18 17:12:02 +00:00
identityKey: z.string(),
})
),
});
export type VerifyServiceIdRequestType = Array<{
uuid: ServiceIdString;
fingerprint: string;
}>;
export type VerifyServiceIdResponseType = z.infer<
typeof verifyServiceIdResponse
>;
2022-10-18 17:12:02 +00:00
export type ReserveUsernameOptionsType = Readonly<{
2023-02-08 17:14:59 +00:00
hashes: ReadonlyArray<Uint8Array>;
2022-10-18 17:12:02 +00:00
abortSignal?: AbortSignal;
}>;
2023-07-20 03:14:08 +00:00
export type ReplaceUsernameLinkOptionsType = Readonly<{
encryptedUsername: Uint8Array;
keepLinkHandle: boolean;
2023-07-20 03:14:08 +00:00
}>;
2022-10-18 17:12:02 +00:00
export type ConfirmUsernameOptionsType = Readonly<{
2023-02-08 17:14:59 +00:00
hash: Uint8Array;
proof: Uint8Array;
2023-07-20 23:19:32 +00:00
encryptedUsername: Uint8Array;
2022-10-18 17:12:02 +00:00
abortSignal?: AbortSignal;
}>;
const reserveUsernameResultZod = z.object({
2023-02-08 17:14:59 +00:00
usernameHash: z
.string()
.transform(x => Bytes.fromBase64(fromWebSafeBase64(x))),
2022-10-18 17:12:02 +00:00
});
export type ReserveUsernameResultType = z.infer<
typeof reserveUsernameResultZod
>;
2023-07-20 23:19:32 +00:00
const confirmUsernameResultZod = z.object({
usernameLinkHandle: z.string(),
});
export type ConfirmUsernameResultType = z.infer<
typeof confirmUsernameResultZod
>;
2023-07-20 03:14:08 +00:00
const replaceUsernameLinkResultZod = z.object({
usernameLinkHandle: z.string(),
});
export type ReplaceUsernameLinkResultType = z.infer<
typeof replaceUsernameLinkResultZod
>;
const resolveUsernameLinkResultZod = z.object({
2023-07-20 23:19:32 +00:00
usernameLinkEncryptedValue: z
.string()
.transform(x => Bytes.fromBase64(fromWebSafeBase64(x))),
2023-07-20 03:14:08 +00:00
});
export type ResolveUsernameLinkResultType = z.infer<
typeof resolveUsernameLinkResultZod
>;
2023-08-29 00:41:32 +00:00
export type CreateAccountOptionsType = Readonly<{
sessionId: string;
number: string;
code: string;
newPassword: string;
registrationId: number;
pniRegistrationId: number;
2023-08-29 00:41:32 +00:00
accessKey: Uint8Array;
aciPublicKey: Uint8Array;
pniPublicKey: Uint8Array;
aciSignedPreKey: UploadSignedPreKeyType;
pniSignedPreKey: UploadSignedPreKeyType;
aciPqLastResortPreKey: UploadSignedPreKeyType;
pniPqLastResortPreKey: UploadSignedPreKeyType;
}>;
2023-08-29 00:41:32 +00:00
const linkDeviceResultZod = z.object({
uuid: aciSchema,
pni: untaggedPniSchema,
deviceId: z.number(),
});
export type LinkDeviceResultType = z.infer<typeof linkDeviceResultZod>;
2023-02-08 00:55:12 +00:00
export type ReportMessageOptionsType = Readonly<{
2023-08-16 20:54:39 +00:00
senderAci: AciString;
2023-02-08 00:55:12 +00:00
serverGuid: string;
token?: string;
}>;
2023-02-27 22:34:43 +00:00
const artAuthZod = z.object({
username: z.string(),
password: z.string(),
});
export type ArtAuthType = z.infer<typeof artAuthZod>;
2023-05-04 20:58:53 +00:00
const attachmentV3Response = z.object({
cdn: z.literal(2),
key: z.string(),
headers: z.record(z.string()),
signedUploadLocation: z.string(),
});
export type AttachmentV3ResponseType = z.infer<typeof attachmentV3Response>;
export type ServerKeyCountType = {
count: number;
pqCount: number;
};
2023-08-29 00:41:32 +00:00
export type LinkDeviceOptionsType = Readonly<{
number: string;
verificationCode: string;
encryptedDeviceName?: string;
newPassword: string;
registrationId: number;
pniRegistrationId: number;
aciSignedPreKey: UploadSignedPreKeyType;
pniSignedPreKey: UploadSignedPreKeyType;
aciPqLastResortPreKey: UploadSignedPreKeyType;
pniPqLastResortPreKey: UploadSignedPreKeyType;
}>;
const createAccountResultZod = z.object({
uuid: aciSchema,
pni: untaggedPniSchema,
});
export type CreateAccountResultType = z.infer<typeof createAccountResultZod>;
const verificationSessionZod = z.object({
id: z.string(),
allowedToRequestCode: z.boolean(),
verified: z.boolean(),
});
export type RequestVerificationResultType = Readonly<{
sessionId: string;
}>;
export type WebAPIType = {
startRegistration(): unknown;
finishRegistration(baton: unknown): void;
cancelInflightRequests: (reason: string) => void;
cdsLookup: (options: CdsLookupOptionsType) => Promise<CDSResponseType>;
2023-08-29 00:41:32 +00:00
createAccount: (
options: CreateAccountOptionsType
) => Promise<CreateAccountResultType>;
2020-09-09 02:25:05 +00:00
createGroup: (
2021-06-22 14:46:42 +00:00
group: Proto.IGroup,
2020-09-09 02:25:05 +00:00
options: GroupCredentialsType
) => Promise<void>;
2022-10-18 17:12:02 +00:00
deleteUsername: (abortSignal?: AbortSignal) => Promise<void>;
2022-11-09 02:38:19 +00:00
downloadOnboardingStories: (
version: string,
imageFiles: Array<string>
) => Promise<Array<Uint8Array>>;
2023-02-27 22:34:43 +00:00
getArtAuth: () => Promise<ArtAuthType>;
getAttachment: (
cdnKey: string,
cdnNumber?: number,
options?: {
disableRetries?: boolean;
timeout?: number;
}
) => Promise<Uint8Array>;
getAttachmentV2: (
cdnKey: string,
cdnNumber?: number,
options?: {
disableRetries?: boolean;
timeout?: number;
}
) => Promise<Readable>;
2021-09-24 00:49:05 +00:00
getAvatar: (path: string) => Promise<Uint8Array>;
getHasSubscription: (subscriberId: Uint8Array) => Promise<boolean>;
2021-06-22 14:46:42 +00:00
getGroup: (options: GroupCredentialsType) => Promise<Proto.Group>;
getGroupFromLink: (
inviteLinkPassword: string | undefined,
auth: GroupCredentialsType
2021-06-22 14:46:42 +00:00
) => Promise<Proto.GroupJoinInfo>;
2021-09-24 00:49:05 +00:00
getGroupAvatar: (key: string) => Promise<Uint8Array>;
2020-09-09 02:25:05 +00:00
getGroupCredentials: (
2022-07-08 20:46:25 +00:00
options: GetGroupCredentialsOptionsType
2022-07-28 16:35:29 +00:00
) => Promise<GetGroupCredentialsResultType>;
2020-11-13 19:57:55 +00:00
getGroupExternalCredential: (
options: GroupCredentialsType
2021-06-22 14:46:42 +00:00
) => Promise<Proto.GroupExternalCredential>;
2020-09-09 02:25:05 +00:00
getGroupLog: (
options: GetGroupLogOptionsType,
credentials: GroupCredentialsType
2020-09-09 02:25:05 +00:00
) => Promise<GroupLogResponseType>;
2021-09-24 00:49:05 +00:00
getIceServers: () => Promise<GetIceServersResultType>;
getKeysForServiceId: (
serviceId: ServiceIdString,
deviceId?: number
) => Promise<ServerKeysType>;
getKeysForServiceIdUnauth: (
serviceId: ServiceIdString,
deviceId?: number,
options?: { accessKey?: string }
) => Promise<ServerKeysType>;
getMyKeyCounts: (serviceIdKind: ServiceIdKind) => Promise<ServerKeyCountType>;
2022-11-09 02:38:19 +00:00
getOnboardingStoryManifest: () => Promise<{
version: string;
languages: Record<string, Array<string>>;
}>;
getProfile: (
serviceId: ServiceIdString,
options: GetProfileOptionsType
2021-09-22 00:58:03 +00:00
) => Promise<ProfileType>;
2023-02-08 17:14:59 +00:00
getAccountForUsername: (
options: GetAccountForUsernameOptionsType
) => Promise<GetAccountForUsernameResultType>;
getProfileUnauth: (
serviceId: ServiceIdString,
options: GetProfileUnauthOptionsType
2021-09-22 00:58:03 +00:00
) => Promise<ProfileType>;
2021-11-02 23:01:13 +00:00
getBadgeImageFile: (imageUrl: string) => Promise<Uint8Array>;
2023-10-06 21:31:17 +00:00
getSubscriptionConfiguration: (
2022-05-11 20:59:58 +00:00
userLanguages: ReadonlyArray<string>
) => Promise<unknown>;
getProvisioningResource: (
handler: IRequestHandler
) => Promise<WebSocketResource>;
2023-02-27 22:34:43 +00:00
getArtProvisioningSocket: (token: string) => Promise<WebSocket>;
2021-04-08 16:24:21 +00:00
getSenderCertificate: (
withUuid?: boolean
2021-09-24 00:49:05 +00:00
) => Promise<GetSenderCertificateResultType>;
getSticker: (packId: string, stickerId: number) => Promise<Uint8Array>;
getStickerPackManifest: (packId: string) => Promise<StickerPackManifestType>;
getStorageCredentials: MessageSender['getStorageCredentials'];
getStorageManifest: MessageSender['getStorageManifest'];
getStorageRecords: MessageSender['getStorageRecords'];
2020-09-28 23:46:31 +00:00
fetchLinkPreviewMetadata: (
href: string,
abortSignal: AbortSignal
) => Promise<null | linkPreviewFetch.LinkPreviewMetadata>;
fetchLinkPreviewImage: (
href: string,
abortSignal: AbortSignal
) => Promise<null | linkPreviewFetch.LinkPreviewImage>;
2023-08-29 00:41:32 +00:00
linkDevice: (options: LinkDeviceOptionsType) => Promise<LinkDeviceResultType>;
makeProxiedRequest: (
targetUrl: string,
options?: ProxiedRequestOptionsType
2021-09-24 00:49:05 +00:00
) => Promise<MakeProxiedRequestResultType>;
2020-11-13 19:57:55 +00:00
makeSfuRequest: (
targetUrl: string,
type: HTTPCodeType,
headers: HeaderListType,
2021-09-24 00:49:05 +00:00
body: Uint8Array | undefined
) => Promise<BytesWithDetailsType>;
2020-09-09 02:25:05 +00:00
modifyGroup: (
2021-06-22 14:46:42 +00:00
changes: Proto.GroupChange.IActions,
options: GroupCredentialsType,
inviteLinkBase64?: string
2021-06-22 14:46:42 +00:00
) => Promise<Proto.IGroupChange>;
2020-09-09 00:56:23 +00:00
modifyStorageRecords: MessageSender['modifyStorageRecords'];
postBatchIdentityCheck: (
elements: VerifyServiceIdRequestType
) => Promise<VerifyServiceIdResponseType>;
putEncryptedAttachment: (encryptedBin: Uint8Array) => Promise<string>;
2021-07-19 19:26:06 +00:00
putProfile: (
jsonData: ProfileRequestDataType
) => Promise<UploadAvatarHeadersType | undefined>;
putStickers: (
2021-09-24 00:49:05 +00:00
encryptedManifest: Uint8Array,
encryptedStickers: Array<Uint8Array>,
onProgress?: () => void
) => Promise<string>;
2022-10-18 17:12:02 +00:00
reserveUsername: (
options: ReserveUsernameOptionsType
) => Promise<ReserveUsernameResultType>;
2023-07-20 23:19:32 +00:00
confirmUsername(
options: ConfirmUsernameOptionsType
): Promise<ConfirmUsernameResultType>;
2023-07-20 03:14:08 +00:00
replaceUsernameLink: (
options: ReplaceUsernameLinkOptionsType
) => Promise<ReplaceUsernameLinkResultType>;
deleteUsernameLink: () => Promise<void>;
resolveUsernameLink: (
serverId: string
) => Promise<ResolveUsernameLinkResultType>;
registerCapabilities: (capabilities: CapabilitiesUploadType) => Promise<void>;
registerKeys: (
genKeys: UploadKeysType,
serviceIdKind: ServiceIdKind
) => Promise<void>;
2021-09-22 00:58:03 +00:00
registerSupportForUnauthenticatedDelivery: () => Promise<void>;
2023-02-08 00:55:12 +00:00
reportMessage: (options: ReportMessageOptionsType) => Promise<void>;
2023-08-29 00:41:32 +00:00
requestVerification: (
number: string,
captcha: string,
transport: VerificationTransport
) => Promise<RequestVerificationResultType>;
checkAccountExistence: (serviceId: ServiceIdString) => Promise<boolean>;
sendMessages: (
destination: ServiceIdString,
2021-09-28 23:38:55 +00:00
messageArray: ReadonlyArray<MessageType>,
timestamp: number,
options: { online?: boolean; story?: boolean; urgent?: boolean }
) => Promise<void>;
sendMessagesUnauth: (
destination: ServiceIdString,
2021-09-28 23:38:55 +00:00
messageArray: ReadonlyArray<MessageType>,
timestamp: number,
options: {
accessKey?: string;
online?: boolean;
story?: boolean;
urgent?: boolean;
}
) => Promise<void>;
2021-05-25 22:40:04 +00:00
sendWithSenderKey: (
2021-09-24 00:49:05 +00:00
payload: Uint8Array,
accessKeys: Uint8Array,
2021-05-25 22:40:04 +00:00
timestamp: number,
options: {
online?: boolean;
story?: boolean;
urgent?: boolean;
}
2021-05-25 22:40:04 +00:00
) => Promise<MultiRecipient200ResponseType>;
2023-02-23 21:32:19 +00:00
setPhoneNumberDiscoverability: (newValue: boolean) => Promise<void>;
updateDeviceName: (deviceName: string) => Promise<void>;
2021-07-19 19:26:06 +00:00
uploadAvatar: (
uploadAvatarRequestHeaders: UploadAvatarHeadersType,
2021-09-24 00:49:05 +00:00
avatarData: Uint8Array
2021-07-19 19:26:06 +00:00
) => Promise<string>;
2020-09-09 02:25:05 +00:00
uploadGroupAvatar: (
2021-06-22 14:46:42 +00:00
avatarData: Uint8Array,
2020-09-09 02:25:05 +00:00
options: GroupCredentialsType
) => Promise<string>;
2021-09-24 00:49:05 +00:00
whoami: () => Promise<WhoamiResultType>;
2021-09-22 00:58:03 +00:00
sendChallengeResponse: (challengeResponse: ChallengeType) => Promise<void>;
getConfig: () => Promise<RemoteConfigResponseType>;
authenticate: (credentials: WebAPICredentials) => Promise<void>;
logout: () => Promise<void>;
getSocketStatus: () => SocketStatus;
registerRequestHandler: (handler: IRequestHandler) => void;
unregisterRequestHandler: (handler: IRequestHandler) => void;
2022-10-05 00:48:25 +00:00
onHasStoriesDisabledChange: (newValue: boolean) => void;
checkSockets: () => void;
onOnline: () => Promise<void>;
2022-10-05 00:48:25 +00:00
onOffline: () => void;
reconnect: () => Promise<void>;
};
export type UploadSignedPreKeyType = {
keyId: number;
2021-09-24 00:49:05 +00:00
publicKey: Uint8Array;
signature: Uint8Array;
};
export type UploadPreKeyType = {
keyId: number;
publicKey: Uint8Array;
};
export type UploadKyberPreKeyType = UploadSignedPreKeyType;
2023-08-29 00:41:32 +00:00
type SerializedSignedPreKeyType = Readonly<{
keyId: number;
publicKey: string;
signature: string;
}>;
export type UploadKeysType = {
2021-09-24 00:49:05 +00:00
identityKey: Uint8Array;
// If a field is not provided, the server won't update its data.
preKeys?: Array<UploadPreKeyType>;
pqPreKeys?: Array<UploadSignedPreKeyType>;
pqLastResortPreKey?: UploadSignedPreKeyType;
signedPreKey?: UploadSignedPreKeyType;
};
export type ServerKeysType = {
devices: Array<{
deviceId: number;
registrationId: number;
// We'll get a 404 if none of these keys are provided; we'll have at least one
preKey?: {
keyId: number;
publicKey: Uint8Array;
};
signedPreKey?: {
keyId: number;
2021-09-24 00:49:05 +00:00
publicKey: Uint8Array;
signature: Uint8Array;
};
pqPreKey?: {
keyId: number;
2021-09-24 00:49:05 +00:00
publicKey: Uint8Array;
signature: Uint8Array;
};
}>;
2021-09-24 00:49:05 +00:00
identityKey: Uint8Array;
};
export type ChallengeType = {
readonly type: 'recaptcha';
readonly token: string;
readonly captcha: string;
};
export type ProxiedRequestOptionsType = {
2021-09-24 00:49:05 +00:00
returnUint8Array?: boolean;
start?: number;
end?: number;
};
export type TopLevelType = {
multiRecipient200ResponseSchema: typeof multiRecipient200ResponseSchema;
multiRecipient409ResponseSchema: typeof multiRecipient409ResponseSchema;
multiRecipient410ResponseSchema: typeof multiRecipient410ResponseSchema;
initialize: (options: InitializeOptionsType) => WebAPIConnectType;
};
type InflightCallback = (error: Error) => unknown;
// We first set up the data that won't change during this session of the app
export function initialize({
2019-01-16 03:03:56 +00:00
url,
storageUrl,
2021-11-02 23:01:13 +00:00
updatesUrl,
2022-11-09 02:38:19 +00:00
resourcesUrl,
2023-02-27 22:34:43 +00:00
artCreatorUrl,
2022-06-15 01:15:33 +00:00
directoryConfig,
cdnUrlObject,
2019-01-16 03:03:56 +00:00
certificateAuthority,
contentProxyUrl,
proxyUrl,
version,
}: InitializeOptionsType): WebAPIConnectType {
2023-01-12 20:58:53 +00:00
if (!isString(url)) {
throw new Error('WebAPI.initialize: Invalid server url');
}
2023-01-12 20:58:53 +00:00
if (!isString(storageUrl)) {
throw new Error('WebAPI.initialize: Invalid storageUrl');
}
2023-01-12 20:58:53 +00:00
if (!isString(updatesUrl)) {
2021-11-02 23:01:13 +00:00
throw new Error('WebAPI.initialize: Invalid updatesUrl');
}
2023-01-12 20:58:53 +00:00
if (!isString(resourcesUrl)) {
2022-11-09 02:38:19 +00:00
throw new Error('WebAPI.initialize: Invalid updatesUrl (general)');
}
2023-02-27 22:34:43 +00:00
if (!isString(artCreatorUrl)) {
throw new Error('WebAPI.initialize: Invalid artCreatorUrl');
}
2023-01-12 20:58:53 +00:00
if (!isObject(cdnUrlObject)) {
throw new Error('WebAPI.initialize: Invalid cdnUrlObject');
}
2023-01-12 20:58:53 +00:00
if (!isString(cdnUrlObject['0'])) {
throw new Error('WebAPI.initialize: Missing CDN 0 configuration');
}
2023-01-12 20:58:53 +00:00
if (!isString(cdnUrlObject['2'])) {
throw new Error('WebAPI.initialize: Missing CDN 2 configuration');
}
2023-07-26 22:15:05 +00:00
if (!isString(cdnUrlObject['3'])) {
throw new Error('WebAPI.initialize: Missing CDN 3 configuration');
}
2023-01-12 20:58:53 +00:00
if (!isString(certificateAuthority)) {
throw new Error('WebAPI.initialize: Invalid certificateAuthority');
}
2023-01-12 20:58:53 +00:00
if (!isString(contentProxyUrl)) {
2019-01-16 03:03:56 +00:00
throw new Error('WebAPI.initialize: Invalid contentProxyUrl');
}
2023-01-12 20:58:53 +00:00
if (proxyUrl && !isString(proxyUrl)) {
throw new Error('WebAPI.initialize: Invalid proxyUrl');
}
2023-01-12 20:58:53 +00:00
if (!isString(version)) {
throw new Error('WebAPI.initialize: Invalid version');
}
// Thanks to function-hoisting, we can put this return statement before all of the
// below function definitions.
return {
connect,
};
// Then we connect to the server with user-specific information. This is the only API
// exposed to the browser context, ensuring that it can't connect to arbitrary
// locations.
function connect({
username: initialUsername,
password: initialPassword,
useWebSocket = true,
2022-10-05 00:48:25 +00:00
hasStoriesDisabled,
2021-08-19 00:13:32 +00:00
}: WebAPIConnectOptionsType) {
let username = initialUsername;
let password = initialPassword;
const PARSE_RANGE_HEADER = /\/(\d+)$/;
2021-11-11 22:43:05 +00:00
const PARSE_GROUP_LOG_RANGE_HEADER =
/^versions\s+(\d{1,10})-(\d{1,10})\/(\d{1,10})/;
let activeRegistration: ExplodePromiseResultType<void> | undefined;
const socketManager = new SocketManager({
url,
2023-02-27 22:34:43 +00:00
artCreatorUrl,
certificateAuthority,
version,
proxyUrl,
2022-10-05 00:48:25 +00:00
hasStoriesDisabled,
});
2021-09-16 20:18:42 +00:00
socketManager.on('statusChange', () => {
window.Whisper.events.trigger('socketStatusChange');
});
socketManager.on('authError', () => {
window.Whisper.events.trigger('unlinkAndDisconnect');
});
if (useWebSocket) {
void socketManager.authenticate({ username, password });
2021-08-19 00:13:32 +00:00
}
2022-10-26 23:17:14 +00:00
const { directoryUrl, directoryMRENCLAVE } = directoryConfig;
2022-08-19 00:31:12 +00:00
2022-10-26 23:17:14 +00:00
const cds = new CDSI({
logger: log,
proxyUrl,
2022-06-15 01:15:33 +00:00
2022-10-26 23:17:14 +00:00
url: directoryUrl,
mrenclave: directoryMRENCLAVE,
certificateAuthority,
version,
2022-06-15 01:15:33 +00:00
2022-10-26 23:17:14 +00:00
async getAuth() {
return (await _ajax({
call: 'directoryAuthV2',
httpType: 'GET',
responseType: 'json',
})) as CDSAuthType;
},
});
2021-11-08 23:32:31 +00:00
const inflightRequests = new Set<(error: Error) => unknown>();
function registerInflightRequest(request: InflightCallback) {
inflightRequests.add(request);
}
function unregisterInFlightRequest(request: InflightCallback) {
inflightRequests.delete(request);
}
function cancelInflightRequests(reason: string) {
const logId = `cancelInflightRequests/${reason}`;
log.warn(`${logId}: Cancelling ${inflightRequests.size} requests`);
for (const request of inflightRequests) {
try {
request(new Error(`${logId}: Cancelled!`));
} catch (error: unknown) {
log.error(
`${logId}: Failed to cancel request: ${toLogFormat(error)}`
);
}
}
inflightRequests.clear();
log.warn(`${logId}: Done`);
}
2023-12-12 22:57:09 +00:00
let fetchAgent: Agent;
if (proxyUrl) {
2023-12-12 22:57:09 +00:00
fetchAgent = createProxyAgent(proxyUrl);
} else {
2023-12-12 22:57:09 +00:00
fetchAgent = createHTTPSAgent({
keepAlive: false,
maxCachedSessions: 0,
});
}
2023-12-12 22:57:09 +00:00
const fetchForLinkPreviews: linkPreviewFetch.FetchFn = (href, init) =>
fetch(href, { ...init, agent: fetchAgent });
// Thanks, function hoisting!
return {
authenticate,
cancelInflightRequests,
cdsLookup,
2021-11-30 19:33:51 +00:00
checkAccountExistence,
2022-11-09 02:38:19 +00:00
checkSockets,
2023-08-29 00:41:32 +00:00
createAccount,
2022-11-09 02:38:19 +00:00
confirmUsername,
2020-09-09 02:25:05 +00:00
createGroup,
deleteUsername,
2023-07-20 03:14:08 +00:00
deleteUsernameLink,
2022-11-09 02:38:19 +00:00
downloadOnboardingStories,
fetchLinkPreviewImage,
fetchLinkPreviewMetadata,
2022-11-09 02:38:19 +00:00
finishRegistration,
getAccountForUsername,
2023-02-27 22:34:43 +00:00
getArtAuth,
getArtProvisioningSocket,
getAttachment,
getAttachmentV2,
getAvatar,
2022-11-09 02:38:19 +00:00
getBadgeImageFile,
2020-09-09 02:25:05 +00:00
getConfig,
getGroup,
getGroupAvatar,
getGroupCredentials,
2020-11-13 19:57:55 +00:00
getGroupExternalCredential,
getGroupFromLink,
2020-09-09 02:25:05 +00:00
getGroupLog,
getHasSubscription,
2020-06-04 18:16:19 +00:00
getIceServers,
getKeysForServiceId,
getKeysForServiceIdUnauth,
getMyKeyCounts,
2022-11-09 02:38:19 +00:00
getOnboardingStoryManifest,
getProfile,
getProfileUnauth,
getProvisioningResource,
2019-01-16 03:03:56 +00:00
getSenderCertificate,
2022-11-09 02:38:19 +00:00
getSocketStatus,
getSticker,
getStickerPackManifest,
getStorageCredentials,
getStorageManifest,
getStorageRecords,
2023-10-06 21:31:17 +00:00
getSubscriptionConfiguration,
2023-08-29 00:41:32 +00:00
linkDevice,
2022-11-09 02:38:19 +00:00
logout,
2019-01-16 03:03:56 +00:00
makeProxiedRequest,
2020-11-13 19:57:55 +00:00
makeSfuRequest,
2020-09-09 02:25:05 +00:00
modifyGroup,
2020-09-09 00:56:23 +00:00
modifyStorageRecords,
2022-11-09 02:38:19 +00:00
onHasStoriesDisabledChange,
onOffline,
onOnline,
postBatchIdentityCheck,
putEncryptedAttachment,
2021-07-19 19:26:06 +00:00
putProfile,
2019-12-17 20:25:57 +00:00
putStickers,
2022-11-09 02:38:19 +00:00
reconnect,
2020-09-09 02:25:05 +00:00
registerCapabilities,
registerKeys,
2022-11-09 02:38:19 +00:00
registerRequestHandler,
2019-01-16 03:03:56 +00:00
registerSupportForUnauthenticatedDelivery,
2023-07-20 03:14:08 +00:00
resolveUsernameLink,
replaceUsernameLink,
reportMessage,
2023-08-29 00:41:32 +00:00
requestVerification,
2022-11-09 02:38:19 +00:00
reserveUsername,
sendChallengeResponse,
sendMessages,
sendMessagesUnauth,
2021-05-25 22:40:04 +00:00
sendWithSenderKey,
2023-02-23 21:32:19 +00:00
setPhoneNumberDiscoverability,
startRegistration,
2022-11-09 02:38:19 +00:00
unregisterRequestHandler,
updateDeviceName,
2021-07-19 19:26:06 +00:00
uploadAvatar,
2020-09-09 02:25:05 +00:00
uploadGroupAvatar,
whoami,
};
2021-09-24 00:49:05 +00:00
function _ajax(
param: AjaxOptionsType & { responseType?: 'bytes' }
): Promise<Uint8Array>;
function _ajax(
param: AjaxOptionsType & { responseType: 'byteswithdetails' }
): Promise<BytesWithDetailsType>;
function _ajax(
param: AjaxOptionsType & { responseType: 'stream' }
): Promise<Readable>;
2021-09-24 00:49:05 +00:00
function _ajax(
param: AjaxOptionsType & { responseType: 'json' }
): Promise<unknown>;
async function _ajax(param: AjaxOptionsType): Promise<unknown> {
if (
!param.unauthenticated &&
activeRegistration &&
!param.isRegistration
) {
log.info('WebAPI: request blocked by active registration');
const start = Date.now();
await activeRegistration.promise;
const duration = Date.now() - start;
log.info(`WebAPI: request unblocked after ${duration}ms`);
}
if (!param.urlParameters) {
param.urlParameters = '';
}
const useWebSocketForEndpoint =
useWebSocket && WEBSOCKET_CALLS.has(param.call);
2021-09-24 00:49:05 +00:00
const outerParams = {
socketManager: useWebSocketForEndpoint ? socketManager : undefined,
2020-09-09 02:25:05 +00:00
basicAuth: param.basicAuth,
certificateAuthority,
contentType: param.contentType || 'application/json; charset=utf-8',
2021-09-24 00:49:05 +00:00
data:
param.data ||
2021-09-28 23:38:55 +00:00
(param.jsonData ? JSON.stringify(param.jsonData) : undefined),
2021-05-25 22:40:04 +00:00
headers: param.headers,
host: param.host || url,
2021-11-30 19:33:51 +00:00
password: param.password ?? password,
path: URL_CALLS[param.call] + param.urlParameters,
proxyUrl,
responseType: param.responseType,
timeout: param.timeout,
type: param.httpType,
2021-11-30 19:33:51 +00:00
user: param.username ?? username,
2020-08-20 22:15:50 +00:00
redactUrl: param.redactUrl,
serverUrl: url,
validateResponse: param.validateResponse,
version,
unauthenticated: param.unauthenticated,
accessKey: param.accessKey,
2022-10-18 17:12:02 +00:00
abortSignal: param.abortSignal,
2021-09-24 00:49:05 +00:00
};
try {
return await _outerAjax(null, outerParams);
} catch (e) {
2021-09-22 00:58:03 +00:00
if (!(e instanceof HTTPError)) {
throw e;
}
const translatedError = translateError(e);
2021-06-09 22:28:54 +00:00
if (translatedError) {
throw translatedError;
}
2021-09-24 00:49:05 +00:00
throw e;
}
}
2023-08-29 00:41:32 +00:00
function serializeSignedPreKey(
preKey?: UploadSignedPreKeyType
): SerializedSignedPreKeyType | undefined {
if (preKey == null) {
return undefined;
}
const { keyId, publicKey, signature } = preKey;
return {
keyId,
publicKey: Bytes.toBase64(publicKey),
signature: Bytes.toBase64(signature),
};
}
function serviceIdKindToQuery(kind: ServiceIdKind): string {
2021-11-30 19:33:51 +00:00
let value: string;
if (kind === ServiceIdKind.ACI) {
2021-11-30 19:33:51 +00:00
value = 'aci';
} else if (kind === ServiceIdKind.PNI) {
2021-11-30 19:33:51 +00:00
value = 'pni';
} else {
throw new Error(`Unsupported ServiceIdKind: ${kind}`);
2021-11-30 19:33:51 +00:00
}
return `identity=${value}`;
}
2022-01-13 21:25:20 +00:00
async function whoami(): Promise<WhoamiResultType> {
const response = await _ajax({
call: 'whoami',
httpType: 'GET',
responseType: 'json',
2022-01-13 21:25:20 +00:00
});
2022-07-18 22:32:00 +00:00
return whoamiResultZod.parse(response);
}
async function sendChallengeResponse(challengeResponse: ChallengeType) {
2021-09-24 00:49:05 +00:00
await _ajax({
call: 'challenge',
httpType: 'PUT',
jsonData: challengeResponse,
});
}
async function authenticate({
username: newUsername,
password: newPassword,
}: WebAPICredentials) {
username = newUsername;
password = newPassword;
if (useWebSocket) {
2021-08-19 00:13:32 +00:00
await socketManager.authenticate({ username, password });
}
}
async function logout() {
username = '';
password = '';
if (useWebSocket) {
await socketManager.logout();
}
}
function getSocketStatus(): SocketStatus {
return socketManager.getStatus();
}
function checkSockets(): void {
// Intentionally not awaiting
void socketManager.check();
}
async function onOnline(): Promise<void> {
await socketManager.onOnline();
}
2022-10-05 00:48:25 +00:00
function onOffline(): void {
socketManager.onOffline();
}
async function reconnect(): Promise<void> {
await socketManager.reconnect();
}
function registerRequestHandler(handler: IRequestHandler): void {
socketManager.registerRequestHandler(handler);
}
function unregisterRequestHandler(handler: IRequestHandler): void {
socketManager.unregisterRequestHandler(handler);
}
2022-10-05 00:48:25 +00:00
function onHasStoriesDisabledChange(newValue: boolean): void {
void socketManager.onHasStoriesDisabledChange(newValue);
2022-10-05 00:48:25 +00:00
}
2020-05-27 21:37:06 +00:00
async function getConfig() {
const rawRes = await _ajax({
2020-05-27 21:37:06 +00:00
call: 'config',
httpType: 'GET',
responseType: 'json',
});
const res = remoteConfigResponseZod.parse(rawRes);
return {
...res,
config: res.config.filter(
({ name }: { name: string }) =>
2023-07-19 00:03:39 +00:00
name.startsWith('desktop.') ||
name.startsWith('global.') ||
name.startsWith('cds.')
),
};
2020-05-27 21:37:06 +00:00
}
async function getSenderCertificate(omitE164?: boolean) {
2021-09-24 00:49:05 +00:00
return (await _ajax({
call: 'deliveryCert',
httpType: 'GET',
responseType: 'json',
validateResponse: { certificate: 'string' },
...(omitE164 ? { urlParameters: '?includeE164=false' } : {}),
2021-09-24 00:49:05 +00:00
})) as GetSenderCertificateResultType;
}
async function getStorageCredentials(): Promise<StorageServiceCredentials> {
2021-09-24 00:49:05 +00:00
return (await _ajax({
call: 'storageToken',
httpType: 'GET',
responseType: 'json',
schema: { username: 'string', password: 'string' },
2021-09-24 00:49:05 +00:00
})) as StorageServiceCredentials;
}
2022-11-09 02:38:19 +00:00
async function getOnboardingStoryManifest() {
const res = await _ajax({
call: 'getOnboardingStoryManifest',
host: resourcesUrl,
httpType: 'GET',
responseType: 'json',
});
return res as {
version: string;
languages: Record<string, Array<string>>;
};
}
async function getStorageManifest(
options: StorageServiceCallOptionsType = {}
2021-09-24 00:49:05 +00:00
): Promise<Uint8Array> {
const { credentials, greaterThanVersion } = options;
const { data, response } = await _ajax({
call: 'storageManifest',
contentType: 'application/x-protobuf',
host: storageUrl,
httpType: 'GET',
responseType: 'byteswithdetails',
urlParameters: greaterThanVersion
? `/version/${greaterThanVersion}`
: '',
...credentials,
});
if (response.status === 204) {
throw makeHTTPError(
'promiseAjax: error response',
response.status,
response.headers.raw(),
data,
new Error().stack
);
}
return data;
}
async function getStorageRecords(
2021-09-24 00:49:05 +00:00
data: Uint8Array,
options: StorageServiceCallOptionsType = {}
2021-09-24 00:49:05 +00:00
): Promise<Uint8Array> {
const { credentials } = options;
return _ajax({
call: 'storageRead',
contentType: 'application/x-protobuf',
data,
host: storageUrl,
httpType: 'PUT',
2021-09-24 00:49:05 +00:00
responseType: 'bytes',
...credentials,
});
}
2020-09-09 00:56:23 +00:00
async function modifyStorageRecords(
2021-09-24 00:49:05 +00:00
data: Uint8Array,
2020-09-09 00:56:23 +00:00
options: StorageServiceCallOptionsType = {}
2021-09-24 00:49:05 +00:00
): Promise<Uint8Array> {
2020-09-09 00:56:23 +00:00
const { credentials } = options;
return _ajax({
call: 'storageModify',
contentType: 'application/x-protobuf',
data,
host: storageUrl,
httpType: 'PUT',
// If we run into a conflict, the current manifest is returned -
2021-09-24 00:49:05 +00:00
// it will will be an Uint8Array at the response key on the Error
responseType: 'bytes',
2020-09-09 00:56:23 +00:00
...credentials,
});
}
async function registerSupportForUnauthenticatedDelivery() {
2021-09-24 00:49:05 +00:00
await _ajax({
call: 'supportUnauthenticatedDelivery',
httpType: 'PUT',
responseType: 'json',
});
}
2020-11-20 17:30:45 +00:00
async function registerCapabilities(capabilities: CapabilitiesUploadType) {
2021-09-24 00:49:05 +00:00
await _ajax({
call: 'registerCapabilities',
httpType: 'PUT',
jsonData: capabilities,
});
}
async function postBatchIdentityCheck(
elements: VerifyServiceIdRequestType
) {
const res = await _ajax({
data: JSON.stringify({ elements }),
call: 'batchIdentityCheck',
httpType: 'POST',
responseType: 'json',
});
const result = verifyServiceIdResponse.safeParse(res);
if (result.success) {
return result.data;
}
log.warn(
'WebAPI: invalid response from postBatchIdentityCheck',
toLogFormat(result.error)
);
throw result.error;
}
function getProfileUrl(
serviceId: ServiceIdString,
{
profileKeyVersion,
profileKeyCredentialRequest,
}: GetProfileCommonOptionsType
) {
let profileUrl = `/${serviceId}`;
if (profileKeyVersion !== undefined) {
profileUrl += `/${profileKeyVersion}`;
if (profileKeyCredentialRequest !== undefined) {
profileUrl +=
`/${profileKeyCredentialRequest}` +
2022-09-21 16:18:48 +00:00
'?credentialType=expiringProfileKey';
}
} else {
strictAssert(
profileKeyCredentialRequest === undefined,
'getProfileUrl called without version, but with request'
);
}
return profileUrl;
}
async function getProfile(
serviceId: ServiceIdString,
options: GetProfileOptionsType
) {
const { profileKeyVersion, profileKeyCredentialRequest, userLanguages } =
options;
2021-09-24 00:49:05 +00:00
return (await _ajax({
call: 'profile',
httpType: 'GET',
urlParameters: getProfileUrl(serviceId, options),
2021-11-02 23:01:13 +00:00
headers: {
'Accept-Language': formatAcceptLanguageHeader(userLanguages),
},
responseType: 'json',
2020-08-20 22:15:50 +00:00
redactUrl: _createRedactor(
serviceId,
2020-08-20 22:15:50 +00:00
profileKeyVersion,
profileKeyCredentialRequest
),
2021-09-24 00:49:05 +00:00
})) as ProfileType;
}
2023-02-08 17:14:59 +00:00
async function getAccountForUsername({
hash,
}: GetAccountForUsernameOptionsType) {
const hashBase64 = toWebSafeBase64(Bytes.toBase64(hash));
return getAccountForUsernameResultZod.parse(
await _ajax({
call: 'username',
httpType: 'GET',
urlParameters: `/${hashBase64}`,
responseType: 'json',
redactUrl: _createRedactor(hashBase64),
unauthenticated: true,
accessKey: undefined,
})
);
2021-11-12 01:17:29 +00:00
}
2021-07-19 19:26:06 +00:00
async function putProfile(
jsonData: ProfileRequestDataType
): Promise<UploadAvatarHeadersType | undefined> {
const res = await _ajax({
call: 'profile',
httpType: 'PUT',
2021-09-24 00:49:05 +00:00
responseType: 'json',
2021-07-19 19:26:06 +00:00
jsonData,
});
if (!res) {
return;
}
2021-09-24 00:49:05 +00:00
return uploadAvatarHeadersZod.parse(res);
2021-07-19 19:26:06 +00:00
}
async function getProfileUnauth(
serviceId: ServiceIdString,
options: GetProfileUnauthOptionsType
) {
const {
accessKey,
profileKeyVersion,
profileKeyCredentialRequest,
2021-11-02 23:01:13 +00:00
userLanguages,
} = options;
2021-09-24 00:49:05 +00:00
return (await _ajax({
call: 'profile',
httpType: 'GET',
urlParameters: getProfileUrl(serviceId, options),
2021-11-02 23:01:13 +00:00
headers: {
'Accept-Language': formatAcceptLanguageHeader(userLanguages),
},
responseType: 'json',
unauthenticated: true,
accessKey,
2020-08-20 22:15:50 +00:00
redactUrl: _createRedactor(
serviceId,
2020-08-20 22:15:50 +00:00
profileKeyVersion,
profileKeyCredentialRequest
),
2021-09-24 00:49:05 +00:00
})) as ProfileType;
}
2021-11-02 23:01:13 +00:00
async function getBadgeImageFile(
imageFileUrl: string
): Promise<Uint8Array> {
strictAssert(
isBadgeImageFileUrlValid(imageFileUrl, updatesUrl),
'getBadgeImageFile got an invalid URL. Was bad data saved?'
);
return _outerAjax(imageFileUrl, {
certificateAuthority,
contentType: 'application/octet-stream',
proxyUrl,
responseType: 'bytes',
timeout: 0,
type: 'GET',
redactUrl: (href: string) => {
const parsedUrl = maybeParseUrl(href);
if (!parsedUrl) {
return href;
}
const { pathname } = parsedUrl;
const pattern = RegExp(escapeRegExp(pathname), 'g');
return href.replace(pattern, `[REDACTED]${pathname.slice(-3)}`);
},
version,
});
}
2022-11-09 02:38:19 +00:00
async function downloadOnboardingStories(
manifestVersion: string,
imageFiles: Array<string>
): Promise<Array<Uint8Array>> {
return Promise.all(
imageFiles.map(fileName =>
_outerAjax(
`${resourcesUrl}/static/desktop/stories/onboarding/${manifestVersion}/${fileName}.jpg`,
{
certificateAuthority,
contentType: 'application/octet-stream',
proxyUrl,
responseType: 'bytes',
timeout: 0,
type: 'GET',
version,
}
)
)
);
}
2023-10-06 21:31:17 +00:00
async function getSubscriptionConfiguration(
2022-05-11 20:59:58 +00:00
userLanguages: ReadonlyArray<string>
): Promise<unknown> {
return _ajax({
2023-10-06 21:31:17 +00:00
call: 'subscriptionConfiguration',
2022-05-11 20:59:58 +00:00
httpType: 'GET',
headers: {
'Accept-Language': formatAcceptLanguageHeader(userLanguages),
},
responseType: 'json',
});
}
async function getAvatar(path: string) {
// Using _outerAJAX, since it's not hardcoded to the Signal Server. Unlike our
// attachment CDN, it uses our self-signed certificate, so we pass it in.
2021-09-24 00:49:05 +00:00
return _outerAjax(`${cdnUrlObject['0']}/${path}`, {
certificateAuthority,
contentType: 'application/octet-stream',
proxyUrl,
2021-09-24 00:49:05 +00:00
responseType: 'bytes',
timeout: 0,
type: 'GET',
2020-08-20 22:15:50 +00:00
redactUrl: (href: string) => {
const pattern = RegExp(escapeRegExp(path), 'g');
return href.replace(pattern, `[REDACTED]${path.slice(-3)}`);
},
version,
2021-09-24 00:49:05 +00:00
});
}
2022-10-18 17:12:02 +00:00
async function deleteUsername(abortSignal?: AbortSignal) {
await _ajax({
call: 'username',
httpType: 'DELETE',
2022-10-18 17:12:02 +00:00
abortSignal,
});
}
2023-02-08 17:14:59 +00:00
2022-10-18 17:12:02 +00:00
async function reserveUsername({
2023-02-08 17:14:59 +00:00
hashes,
2022-10-18 17:12:02 +00:00
abortSignal,
}: ReserveUsernameOptionsType) {
const response = await _ajax({
2023-02-08 17:14:59 +00:00
call: 'reserveUsername',
2022-10-18 17:12:02 +00:00
httpType: 'PUT',
jsonData: {
2023-02-08 17:14:59 +00:00
usernameHashes: hashes.map(hash =>
toWebSafeBase64(Bytes.toBase64(hash))
),
2022-10-18 17:12:02 +00:00
},
responseType: 'json',
abortSignal,
});
return reserveUsernameResultZod.parse(response);
}
async function confirmUsername({
2023-02-08 17:14:59 +00:00
hash,
proof,
2023-07-20 23:19:32 +00:00
encryptedUsername,
2022-10-18 17:12:02 +00:00
abortSignal,
}: ConfirmUsernameOptionsType) {
2023-07-20 23:19:32 +00:00
const response = await _ajax({
2022-10-18 17:12:02 +00:00
call: 'confirmUsername',
httpType: 'PUT',
2022-10-18 17:12:02 +00:00
jsonData: {
2023-02-08 17:14:59 +00:00
usernameHash: toWebSafeBase64(Bytes.toBase64(hash)),
zkProof: toWebSafeBase64(Bytes.toBase64(proof)),
2023-07-20 23:19:32 +00:00
encryptedUsername: toWebSafeBase64(Bytes.toBase64(encryptedUsername)),
2022-10-18 17:12:02 +00:00
},
2023-07-20 23:19:32 +00:00
responseType: 'json',
2022-10-18 17:12:02 +00:00
abortSignal,
});
2023-07-20 23:19:32 +00:00
return confirmUsernameResultZod.parse(response);
}
2023-07-20 03:14:08 +00:00
async function replaceUsernameLink({
encryptedUsername,
keepLinkHandle,
2023-07-20 03:14:08 +00:00
}: ReplaceUsernameLinkOptionsType): Promise<ReplaceUsernameLinkResultType> {
return replaceUsernameLinkResultZod.parse(
await _ajax({
call: 'usernameLink',
httpType: 'PUT',
responseType: 'json',
jsonData: {
2023-07-20 23:19:32 +00:00
usernameLinkEncryptedValue: toWebSafeBase64(
Bytes.toBase64(encryptedUsername)
),
keepLinkHandle,
2023-07-20 03:14:08 +00:00
},
})
);
}
async function deleteUsernameLink(): Promise<void> {
await _ajax({
call: 'usernameLink',
httpType: 'DELETE',
});
}
async function resolveUsernameLink(
serverId: string
): Promise<ResolveUsernameLinkResultType> {
return resolveUsernameLinkResultZod.parse(
await _ajax({
httpType: 'GET',
call: 'usernameLink',
urlParameters: `/${encodeURIComponent(serverId)}`,
responseType: 'json',
unauthenticated: true,
accessKey: undefined,
})
);
}
2023-02-08 00:55:12 +00:00
async function reportMessage({
2023-08-16 20:54:39 +00:00
senderAci,
2023-02-08 00:55:12 +00:00
serverGuid,
token,
}: ReportMessageOptionsType): Promise<void> {
const jsonData = { token };
await _ajax({
call: 'reportMessage',
httpType: 'POST',
2023-08-16 20:54:39 +00:00
urlParameters: urlPathFromComponents([senderAci, serverGuid]),
2021-09-24 00:49:05 +00:00
responseType: 'bytes',
2023-02-08 00:55:12 +00:00
jsonData,
});
}
2023-08-29 00:41:32 +00:00
async function requestVerification(
number: string,
captcha: string,
transport: VerificationTransport
) {
// Create a new blank session using just a E164
let session = verificationSessionZod.parse(
await _ajax({
call: 'verificationSession',
httpType: 'POST',
responseType: 'json',
jsonData: {
number,
},
unauthenticated: true,
accessKey: undefined,
})
);
2023-08-29 00:41:32 +00:00
// Submit a captcha solution to the session
session = verificationSessionZod.parse(
await _ajax({
call: 'verificationSession',
httpType: 'PATCH',
urlParameters: `/${encodeURIComponent(session.id)}`,
responseType: 'json',
jsonData: {
captcha,
},
unauthenticated: true,
accessKey: undefined,
})
);
// Verify that captcha was accepted
if (!session.allowedToRequestCode) {
throw new Error('requestVerification: Not allowed to send code');
}
// Request an SMS or Voice confirmation
session = verificationSessionZod.parse(
await _ajax({
call: 'verificationSession',
httpType: 'POST',
urlParameters: `/${encodeURIComponent(session.id)}/code`,
responseType: 'json',
jsonData: {
client: 'ios',
transport:
transport === VerificationTransport.SMS ? 'sms' : 'voice',
},
unauthenticated: true,
accessKey: undefined,
})
);
// Return sessionId to be used in `createAccount`
return { sessionId: session.id };
}
async function checkAccountExistence(serviceId: ServiceIdString) {
2021-11-30 19:33:51 +00:00
try {
await _ajax({
httpType: 'HEAD',
call: 'accountExistence',
urlParameters: `/${serviceId}`,
2021-11-30 19:33:51 +00:00
unauthenticated: true,
accessKey: undefined,
});
return true;
} catch (error) {
if (error instanceof HTTPError && error.code === 404) {
return false;
}
throw error;
}
}
function startRegistration() {
strictAssert(
activeRegistration === undefined,
'Registration already in progress'
);
activeRegistration = explodePromise<void>();
log.info('WebAPI: starting registration');
return activeRegistration;
}
function finishRegistration(registration: unknown) {
strictAssert(activeRegistration !== undefined, 'No active registration');
strictAssert(
activeRegistration === registration,
'Invalid registration baton'
);
log.info('WebAPI: finishing registration');
const current = activeRegistration;
activeRegistration = undefined;
current.resolve();
}
2023-08-29 00:41:32 +00:00
async function _withNewCredentials<
Result extends { uuid: AciString; deviceId?: number }
>(
{ username: newUsername, password: newPassword }: WebAPICredentials,
callback: () => Promise<Result>
): Promise<Result> {
// Reset old websocket credentials and disconnect.
// AccountManager is our only caller and it will trigger
// `registration_done` which will update credentials.
await logout();
// Update REST credentials, though. We need them for the call below
2023-08-29 00:41:32 +00:00
username = newUsername;
password = newPassword;
2023-08-29 00:41:32 +00:00
const result = await callback();
const { uuid: aci = newUsername, deviceId = 1 } = result;
// Set final REST credentials to let `registerKeys` succeed.
2023-08-29 00:41:32 +00:00
username = `${aci}.${deviceId}`;
password = newPassword;
2023-08-29 00:41:32 +00:00
return result;
}
async function createAccount({
sessionId,
number,
code,
newPassword,
registrationId,
pniRegistrationId,
accessKey,
aciPublicKey,
pniPublicKey,
aciSignedPreKey,
pniSignedPreKey,
aciPqLastResortPreKey,
pniPqLastResortPreKey,
}: CreateAccountOptionsType) {
const session = verificationSessionZod.parse(
await _ajax({
isRegistration: true,
call: 'verificationSession',
httpType: 'PUT',
urlParameters: `/${encodeURIComponent(sessionId)}/code`,
responseType: 'json',
jsonData: {
code,
},
unauthenticated: true,
accessKey: undefined,
})
);
if (!session.verified) {
throw new Error('createAccount: invalid code');
}
const jsonData = {
sessionId: session.id,
accountAttributes: {
fetchesMessages: true,
registrationId,
pniRegistrationId,
2024-02-16 19:49:48 +00:00
capabilities: {},
2023-08-29 00:41:32 +00:00
unidentifiedAccessKey: Bytes.toBase64(accessKey),
},
requireAtomic: true,
skipDeviceTransfer: true,
aciIdentityKey: Bytes.toBase64(aciPublicKey),
pniIdentityKey: Bytes.toBase64(pniPublicKey),
aciSignedPreKey: serializeSignedPreKey(aciSignedPreKey),
pniSignedPreKey: serializeSignedPreKey(pniSignedPreKey),
aciPqLastResortPreKey: serializeSignedPreKey(aciPqLastResortPreKey),
pniPqLastResortPreKey: serializeSignedPreKey(pniPqLastResortPreKey),
};
return _withNewCredentials(
{
username: number,
password: newPassword,
},
async () => {
const responseJson = await _ajax({
isRegistration: true,
call: 'registration',
httpType: 'POST',
responseType: 'json',
jsonData,
});
return createAccountResultZod.parse(responseJson);
}
);
}
async function linkDevice({
number,
verificationCode,
encryptedDeviceName,
newPassword,
registrationId,
pniRegistrationId,
aciSignedPreKey,
pniSignedPreKey,
aciPqLastResortPreKey,
pniPqLastResortPreKey,
}: LinkDeviceOptionsType) {
const jsonData = {
verificationCode,
accountAttributes: {
fetchesMessages: true,
name: encryptedDeviceName,
registrationId,
pniRegistrationId,
2024-02-16 19:49:48 +00:00
capabilities: {},
2023-08-29 00:41:32 +00:00
},
aciSignedPreKey: serializeSignedPreKey(aciSignedPreKey),
pniSignedPreKey: serializeSignedPreKey(pniSignedPreKey),
aciPqLastResortPreKey: serializeSignedPreKey(aciPqLastResortPreKey),
pniPqLastResortPreKey: serializeSignedPreKey(pniPqLastResortPreKey),
};
return _withNewCredentials(
{
username: number,
password: newPassword,
},
async () => {
const responseJson = await _ajax({
isRegistration: true,
call: 'linkDevice',
httpType: 'PUT',
responseType: 'json',
jsonData,
});
return linkDeviceResultZod.parse(responseJson);
}
);
}
async function updateDeviceName(deviceName: string) {
2021-09-24 00:49:05 +00:00
await _ajax({
call: 'updateDeviceName',
httpType: 'PUT',
jsonData: {
deviceName,
},
});
}
2020-06-04 18:16:19 +00:00
async function getIceServers() {
2021-09-24 00:49:05 +00:00
return (await _ajax({
2020-06-04 18:16:19 +00:00
call: 'getIceServers',
httpType: 'GET',
2021-09-22 00:58:03 +00:00
responseType: 'json',
2021-09-24 00:49:05 +00:00
})) as GetIceServersResultType;
2020-06-04 18:16:19 +00:00
}
type JSONSignedPreKeyType = {
keyId: number;
publicKey: string;
signature: string;
};
type JSONPreKeyType = {
keyId: number;
publicKey: string;
};
type JSONKyberPreKeyType = {
keyId: number;
publicKey: string;
signature: string;
};
type JSONKeysType = {
preKeys?: Array<JSONPreKeyType>;
pqPreKeys?: Array<JSONKyberPreKeyType>;
pqLastResortPreKey?: JSONKyberPreKeyType;
signedPreKey?: JSONSignedPreKeyType;
};
async function registerKeys(
genKeys: UploadKeysType,
serviceIdKind: ServiceIdKind
) {
const preKeys = genKeys.preKeys?.map(key => ({
keyId: key.keyId,
publicKey: Bytes.toBase64(key.publicKey),
}));
const pqPreKeys = genKeys.pqPreKeys?.map(key => ({
keyId: key.keyId,
2021-09-24 00:49:05 +00:00
publicKey: Bytes.toBase64(key.publicKey),
signature: Bytes.toBase64(key.signature),
}));
if (
!preKeys?.length &&
!pqPreKeys?.length &&
!genKeys.pqLastResortPreKey &&
!genKeys.signedPreKey
) {
throw new Error(
'registerKeys: None of the four potential key types were provided!'
);
}
if (preKeys && preKeys.length === 0) {
throw new Error('registerKeys: Attempting to upload zero preKeys!');
}
if (pqPreKeys && pqPreKeys.length === 0) {
throw new Error('registerKeys: Attempting to upload zero pqPreKeys!');
}
const keys: JSONKeysType = {
preKeys,
pqPreKeys,
2023-08-29 00:41:32 +00:00
pqLastResortPreKey: serializeSignedPreKey(genKeys.pqLastResortPreKey),
signedPreKey: serializeSignedPreKey(genKeys.signedPreKey),
};
2021-09-24 00:49:05 +00:00
await _ajax({
isRegistration: true,
call: 'keys',
urlParameters: `?${serviceIdKindToQuery(serviceIdKind)}`,
httpType: 'PUT',
jsonData: keys,
});
}
2023-02-23 21:32:19 +00:00
async function setPhoneNumberDiscoverability(newValue: boolean) {
await _ajax({
call: 'phoneNumberDiscoverability',
httpType: 'PUT',
jsonData: {
discoverableByPhoneNumber: newValue,
},
});
}
async function getMyKeyCounts(
serviceIdKind: ServiceIdKind
): Promise<ServerKeyCountType> {
2021-09-24 00:49:05 +00:00
const result = (await _ajax({
call: 'keys',
urlParameters: `?${serviceIdKindToQuery(serviceIdKind)}`,
httpType: 'GET',
responseType: 'json',
validateResponse: { count: 'number', pqCount: 'number' },
2021-09-24 00:49:05 +00:00
})) as ServerKeyCountType;
return result;
}
type ServerKeyResponseType = {
devices: Array<{
deviceId: number;
registrationId: number;
// We'll get a 404 if none of these keys are provided; we'll have at least one
preKey?: {
keyId: number;
publicKey: string;
};
signedPreKey?: {
keyId: number;
publicKey: string;
signature: string;
};
pqPreKey?: {
keyId: number;
publicKey: string;
signature: string;
};
}>;
identityKey: string;
};
function handleKeys(res: ServerKeyResponseType): ServerKeysType {
if (!Array.isArray(res.devices)) {
throw new Error('Invalid response');
}
const devices = res.devices.map(device => {
if (
!_validateResponse(device, { signedPreKey: 'object' }) ||
!_validateResponse(device.signedPreKey, {
publicKey: 'string',
signature: 'string',
})
) {
throw new Error('Invalid signedPreKey');
}
let preKey;
if (device.preKey) {
if (
!_validateResponse(device, { preKey: 'object' }) ||
!_validateResponse(device.preKey, { publicKey: 'string' })
) {
throw new Error('Invalid preKey');
}
preKey = {
keyId: device.preKey.keyId,
2021-09-24 00:49:05 +00:00
publicKey: Bytes.fromBase64(device.preKey.publicKey),
};
}
return {
deviceId: device.deviceId,
registrationId: device.registrationId,
...(preKey ? { preKey } : null),
...(device.signedPreKey
? {
signedPreKey: {
keyId: device.signedPreKey.keyId,
publicKey: Bytes.fromBase64(device.signedPreKey.publicKey),
signature: Bytes.fromBase64(device.signedPreKey.signature),
},
}
: null),
...(device.pqPreKey
? {
pqPreKey: {
keyId: device.pqPreKey.keyId,
publicKey: Bytes.fromBase64(device.pqPreKey.publicKey),
signature: Bytes.fromBase64(device.pqPreKey.signature),
},
}
: null),
};
});
return {
devices,
2021-09-24 00:49:05 +00:00
identityKey: Bytes.fromBase64(res.identityKey),
};
}
async function getKeysForServiceId(
serviceId: ServiceIdString,
deviceId?: number
) {
2021-09-24 00:49:05 +00:00
const keys = (await _ajax({
call: 'keys',
httpType: 'GET',
2024-01-25 23:21:06 +00:00
urlParameters: `/${serviceId}/${deviceId || '*'}`,
responseType: 'json',
validateResponse: { identityKey: 'string', devices: 'object' },
2021-09-24 00:49:05 +00:00
})) as ServerKeyResponseType;
return handleKeys(keys);
}
async function getKeysForServiceIdUnauth(
serviceId: ServiceIdString,
deviceId?: number,
{ accessKey }: { accessKey?: string } = {}
) {
2021-09-24 00:49:05 +00:00
const keys = (await _ajax({
call: 'keys',
httpType: 'GET',
2024-01-25 23:21:06 +00:00
urlParameters: `/${serviceId}/${deviceId || '*'}`,
responseType: 'json',
validateResponse: { identityKey: 'string', devices: 'object' },
unauthenticated: true,
accessKey,
2021-09-24 00:49:05 +00:00
})) as ServerKeyResponseType;
return handleKeys(keys);
}
async function sendMessagesUnauth(
destination: ServiceIdString,
2021-09-28 23:38:55 +00:00
messages: ReadonlyArray<MessageType>,
timestamp: number,
{
accessKey,
online,
urgent = true,
story = false,
}: {
accessKey?: string;
online?: boolean;
story?: boolean;
urgent?: boolean;
}
) {
const jsonData = {
messages,
timestamp,
online: Boolean(online),
urgent,
};
2021-09-24 00:49:05 +00:00
await _ajax({
call: 'messages',
httpType: 'PUT',
urlParameters: `/${destination}?story=${booleanToString(story)}`,
jsonData,
responseType: 'json',
unauthenticated: true,
accessKey,
});
}
async function sendMessages(
destination: ServiceIdString,
2021-09-28 23:38:55 +00:00
messages: ReadonlyArray<MessageType>,
timestamp: number,
{
online,
urgent = true,
story = false,
}: { online?: boolean; story?: boolean; urgent?: boolean }
2018-11-14 19:10:32 +00:00
) {
const jsonData = {
messages,
timestamp,
online: Boolean(online),
urgent,
};
2021-09-24 00:49:05 +00:00
await _ajax({
call: 'messages',
httpType: 'PUT',
urlParameters: `/${destination}?story=${booleanToString(story)}`,
jsonData,
responseType: 'json',
});
}
function booleanToString(value: boolean | undefined): string {
return value ? 'true' : 'false';
}
2021-05-25 22:40:04 +00:00
async function sendWithSenderKey(
2021-09-24 00:49:05 +00:00
data: Uint8Array,
accessKeys: Uint8Array,
2021-05-25 22:40:04 +00:00
timestamp: number,
{
online,
urgent = true,
story = false,
}: {
online?: boolean;
story?: boolean;
urgent?: boolean;
}
2021-05-25 22:40:04 +00:00
): Promise<MultiRecipient200ResponseType> {
const onlineParam = `&online=${booleanToString(online)}`;
const urgentParam = `&urgent=${booleanToString(urgent)}`;
const storyParam = `&story=${booleanToString(story)}`;
2021-09-24 00:49:05 +00:00
const response = await _ajax({
2021-05-25 22:40:04 +00:00
call: 'multiRecipient',
httpType: 'PUT',
contentType: 'application/vnd.signal-messenger.mrm',
data,
urlParameters: `?ts=${timestamp}${onlineParam}${urgentParam}${storyParam}`,
2021-05-25 22:40:04 +00:00
responseType: 'json',
unauthenticated: true,
2021-09-24 00:49:05 +00:00
accessKey: Bytes.toBase64(accessKeys),
2021-05-25 22:40:04 +00:00
});
2021-09-24 00:49:05 +00:00
const parseResult = multiRecipient200ResponseSchema.safeParse(response);
if (parseResult.success) {
return parseResult.data;
}
log.warn(
'WebAPI: invalid response from sendWithSenderKey',
toLogFormat(parseResult.error)
);
return response as MultiRecipient200ResponseType;
2021-05-25 22:40:04 +00:00
}
function redactStickerUrl(stickerUrl: string) {
return stickerUrl.replace(
/(\/stickers\/)([^/]+)(\/)/,
(_, begin: string, packId: string, end: string) =>
`${begin}${redactPackId(packId)}${end}`
);
}
async function getSticker(packId: string, stickerId: number) {
2020-08-31 16:29:22 +00:00
if (!isPackIdValid(packId)) {
throw new Error('getSticker: pack ID was invalid');
}
2021-09-24 00:49:05 +00:00
return _outerAjax(
`${cdnUrlObject['0']}/stickers/${packId}/full/${stickerId}`,
{
certificateAuthority,
proxyUrl,
2021-09-24 00:49:05 +00:00
responseType: 'bytes',
type: 'GET',
redactUrl: redactStickerUrl,
version,
}
2021-09-24 00:49:05 +00:00
);
}
async function getStickerPackManifest(packId: string) {
2020-08-31 16:29:22 +00:00
if (!isPackIdValid(packId)) {
throw new Error('getStickerPackManifest: pack ID was invalid');
}
2021-09-24 00:49:05 +00:00
return _outerAjax(
`${cdnUrlObject['0']}/stickers/${packId}/manifest.proto`,
{
certificateAuthority,
proxyUrl,
2021-09-24 00:49:05 +00:00
responseType: 'bytes',
type: 'GET',
redactUrl: redactStickerUrl,
version,
}
2021-09-24 00:49:05 +00:00
);
}
2023-05-04 20:58:53 +00:00
type ServerV2AttachmentType = {
key: string;
credential: string;
acl: string;
algorithm: string;
date: string;
policy: string;
signature: string;
};
2019-12-17 20:25:57 +00:00
function makePutParams(
{
key,
credential,
acl,
algorithm,
date,
policy,
signature,
2023-05-04 20:58:53 +00:00
}: ServerV2AttachmentType,
2021-09-24 00:49:05 +00:00
encryptedBin: Uint8Array
2019-12-17 20:25:57 +00:00
) {
// Note: when using the boundary string in the POST body, it needs to be prefixed by
// an extra --, and the final boundary string at the end gets a -- prefix and a --
// suffix.
const boundaryString = `----------------${getGuid().replace(/-/g, '')}`;
const CRLF = '\r\n';
const getSection = (name: string, value: string) =>
[
`--${boundaryString}`,
`Content-Disposition: form-data; name="${name}"${CRLF}`,
value,
].join(CRLF);
const start = [
getSection('key', key),
getSection('x-amz-credential', credential),
getSection('acl', acl),
getSection('x-amz-algorithm', algorithm),
getSection('x-amz-date', date),
getSection('policy', policy),
getSection('x-amz-signature', signature),
getSection('Content-Type', 'application/octet-stream'),
`--${boundaryString}`,
'Content-Disposition: form-data; name="file"',
`Content-Type: application/octet-stream${CRLF}${CRLF}`,
].join(CRLF);
const end = `${CRLF}--${boundaryString}--${CRLF}`;
const startBuffer = Buffer.from(start, 'utf8');
const attachmentBuffer = Buffer.from(encryptedBin);
const endBuffer = Buffer.from(end, 'utf8');
const contentLength =
startBuffer.length + attachmentBuffer.length + endBuffer.length;
const data = Buffer.concat(
[startBuffer, attachmentBuffer, endBuffer],
contentLength
);
2019-12-17 20:25:57 +00:00
return {
data,
contentType: `multipart/form-data; boundary=${boundaryString}`,
headers: {
'Content-Length': contentLength.toString(),
2019-12-17 20:25:57 +00:00
},
};
}
async function putStickers(
2021-09-24 00:49:05 +00:00
encryptedManifest: Uint8Array,
encryptedStickers: Array<Uint8Array>,
onProgress?: () => void
2019-12-17 20:25:57 +00:00
) {
// Get manifest and sticker upload parameters
2021-09-24 00:49:05 +00:00
const { packId, manifest, stickers } = (await _ajax({
2019-12-17 20:25:57 +00:00
call: 'getStickerPackUpload',
responseType: 'json',
httpType: 'GET',
2019-12-17 20:25:57 +00:00
urlParameters: `/${encryptedStickers.length}`,
2021-09-24 00:49:05 +00:00
})) as {
packId: string;
2023-05-04 20:58:53 +00:00
manifest: ServerV2AttachmentType;
stickers: ReadonlyArray<ServerV2AttachmentType>;
2021-09-24 00:49:05 +00:00
};
2019-12-17 20:25:57 +00:00
// Upload manifest
const manifestParams = makePutParams(manifest, encryptedManifest);
// This is going to the CDN, not the service, so we use _outerAjax
await _outerAjax(`${cdnUrlObject['0']}/`, {
2019-12-17 20:25:57 +00:00
...manifestParams,
certificateAuthority,
proxyUrl,
timeout: 0,
type: 'POST',
version,
2019-12-17 20:25:57 +00:00
});
// Upload stickers
2021-11-23 22:01:03 +00:00
const queue = new PQueue({
concurrency: 3,
timeout: durations.MINUTE * 30,
2021-11-23 22:01:03 +00:00
throwOnTimeout: true,
});
2019-12-17 20:25:57 +00:00
await Promise.all(
2023-05-04 20:58:53 +00:00
stickers.map(async (sticker: ServerV2AttachmentType, index: number) => {
const stickerParams = makePutParams(
sticker,
encryptedStickers[index]
);
await queue.add(async () =>
_outerAjax(`${cdnUrlObject['0']}/`, {
...stickerParams,
certificateAuthority,
proxyUrl,
timeout: 0,
type: 'POST',
version,
})
);
2019-12-17 20:25:57 +00:00
if (onProgress) {
onProgress();
}
})
);
// Done!
return packId;
}
async function getAttachment(
cdnKey: string,
cdnNumber?: number,
options?: {
disableRetries?: boolean;
timeout?: number;
}
) {
const abortController = new AbortController();
const cdnUrl = isNumber(cdnNumber)
2023-07-26 22:15:05 +00:00
? cdnUrlObject[cdnNumber] ?? cdnUrlObject['0']
: cdnUrlObject['0'];
2019-12-17 20:25:57 +00:00
// This is going to the CDN, not the service, so we use _outerAjax
const stream = await _outerAjax(`${cdnUrl}/attachments/${cdnKey}`, {
2019-12-17 20:25:57 +00:00
certificateAuthority,
disableRetries: options?.disableRetries,
2019-12-17 20:25:57 +00:00
proxyUrl,
responseType: 'stream',
timeout: options?.timeout || 0,
2019-12-17 20:25:57 +00:00
type: 'GET',
2020-08-20 22:15:50 +00:00
redactUrl: _createRedactor(cdnKey),
version,
abortSignal: abortController.signal,
});
const streamPromise = getStreamWithTimeout(stream, {
name: `getAttachment(${cdnKey})`,
timeout: GET_ATTACHMENT_CHUNK_TIMEOUT,
abortController,
2021-09-24 00:49:05 +00:00
});
// Add callback to central store that would reject a promise
const { promise: cancelPromise, reject } = explodePromise<Uint8Array>();
const inflightRequest = (error: Error) => {
reject(error);
abortController.abort();
};
registerInflightRequest(inflightRequest);
try {
return Promise.race([streamPromise, cancelPromise]);
} finally {
unregisterInFlightRequest(inflightRequest);
}
2019-12-17 20:25:57 +00:00
}
async function getAttachmentV2(
cdnKey: string,
cdnNumber?: number,
options?: {
disableRetries?: boolean;
timeout?: number;
}
): Promise<Readable> {
const abortController = new AbortController();
const cdnUrl = isNumber(cdnNumber)
? cdnUrlObject[cdnNumber] ?? cdnUrlObject['0']
: cdnUrlObject['0'];
// This is going to the CDN, not the service, so we use _outerAjax
const downloadStream = await _outerAjax(
`${cdnUrl}/attachments/${cdnKey}`,
{
certificateAuthority,
disableRetries: options?.disableRetries,
proxyUrl,
responseType: 'stream',
timeout: options?.timeout || 0,
type: 'GET',
redactUrl: _createRedactor(cdnKey),
version,
abortSignal: abortController.signal,
}
);
const timeoutStream = getTimeoutStream({
name: `getAttachment(${cdnKey})`,
timeout: GET_ATTACHMENT_CHUNK_TIMEOUT,
abortController,
});
const combinedStream = downloadStream
// We do this manually; pipe() doesn't flow errors through the streams for us
.on('error', (error: Error) => {
timeoutStream.emit('error', error);
})
.pipe(timeoutStream);
const cancelRequest = (error: Error) => {
combinedStream.emit('error', error);
abortController.abort();
};
registerInflightRequest(cancelRequest);
combinedStream.on('done', () => {
unregisterInFlightRequest(cancelRequest);
});
return combinedStream;
}
async function putEncryptedAttachment(encryptedBin: Uint8Array) {
2023-05-04 20:58:53 +00:00
const response = attachmentV3Response.parse(
await _ajax({
call: 'attachmentId',
httpType: 'GET',
responseType: 'json',
})
);
const { signedUploadLocation, key: cdnKey, headers } = response;
2019-12-17 20:25:57 +00:00
2023-05-04 20:58:53 +00:00
// This is going to the CDN, not the service, so we use _outerAjax
const { response: uploadResponse } = await _outerAjax(
signedUploadLocation,
{
responseType: 'byteswithdetails',
certificateAuthority,
proxyUrl,
timeout: 0,
type: 'POST',
version,
headers,
redactUrl: () => {
const tmp = new URL(signedUploadLocation);
tmp.search = '';
tmp.pathname = '';
return `${tmp}[REDACTED]`;
},
2023-05-04 20:58:53 +00:00
}
);
2019-12-17 20:25:57 +00:00
2023-05-04 20:58:53 +00:00
const uploadLocation = uploadResponse.headers.get('location');
strictAssert(
uploadLocation,
'attachment v3 response header has no location'
);
2019-12-17 20:25:57 +00:00
// This is going to the CDN, not the service, so we use _outerAjax
2023-05-04 20:58:53 +00:00
await _outerAjax(uploadLocation, {
certificateAuthority,
proxyUrl,
timeout: 0,
2023-05-04 20:58:53 +00:00
type: 'PUT',
version,
2023-05-04 20:58:53 +00:00
data: encryptedBin,
2023-08-16 17:44:33 +00:00
redactUrl: () => {
const tmp = new URL(uploadLocation);
tmp.search = '';
tmp.pathname = '';
return `${tmp}[REDACTED]`;
},
});
2023-05-04 20:58:53 +00:00
return cdnKey;
}
function getHeaderPadding() {
2022-10-05 16:35:56 +00:00
const max = randomInt(1, 64);
let characters = '';
2019-01-16 03:03:56 +00:00
for (let i = 0; i < max; i += 1) {
2022-10-05 16:35:56 +00:00
characters += String.fromCharCode(randomInt(65, 122));
2019-01-16 03:03:56 +00:00
}
return characters;
2019-01-16 03:03:56 +00:00
}
2020-09-28 23:46:31 +00:00
async function fetchLinkPreviewMetadata(
href: string,
abortSignal: AbortSignal
) {
return linkPreviewFetch.fetchLinkPreviewMetadata(
fetchForLinkPreviews,
2020-09-28 23:46:31 +00:00
href,
abortSignal
);
}
async function fetchLinkPreviewImage(
href: string,
abortSignal: AbortSignal
) {
return linkPreviewFetch.fetchLinkPreviewImage(
fetchForLinkPreviews,
href,
abortSignal
);
2020-09-28 23:46:31 +00:00
}
async function makeProxiedRequest(
targetUrl: string,
options: ProxiedRequestOptionsType = {}
2021-09-24 00:49:05 +00:00
): Promise<MakeProxiedRequestResultType> {
const { returnUint8Array, start, end } = options;
const headers: HeaderListType = {
'X-SignalPadding': getHeaderPadding(),
};
2019-01-16 03:03:56 +00:00
2023-01-12 20:58:53 +00:00
if (isNumber(start) && isNumber(end)) {
headers.Range = `bytes=${start}-${end}`;
2019-01-16 03:03:56 +00:00
}
2021-09-24 00:49:05 +00:00
const result = await _outerAjax(targetUrl, {
responseType: returnUint8Array ? 'byteswithdetails' : undefined,
2019-01-16 03:03:56 +00:00
proxyUrl: contentProxyUrl,
type: 'GET',
redirect: 'follow',
redactUrl: () => '[REDACTED_URL]',
2019-01-16 03:03:56 +00:00
headers,
version,
2021-09-24 00:49:05 +00:00
});
2021-09-24 00:49:05 +00:00
if (!returnUint8Array) {
return result as Uint8Array;
}
2021-09-24 00:49:05 +00:00
const { response } = result as BytesWithDetailsType;
if (!response.headers || !response.headers.get) {
throw new Error('makeProxiedRequest: Problem retrieving header value');
}
const range = response.headers.get('content-range');
2020-09-09 02:25:05 +00:00
const match = PARSE_RANGE_HEADER.exec(range || '');
if (!match || !match[1]) {
throw new Error(
`makeProxiedRequest: Unable to parse total size from ${range}`
);
}
const totalSize = parseInt(match[1], 10);
return {
totalSize,
2021-09-24 00:49:05 +00:00
result: result as BytesWithDetailsType,
};
2019-01-16 03:03:56 +00:00
}
2020-11-13 19:57:55 +00:00
async function makeSfuRequest(
targetUrl: string,
type: HTTPCodeType,
headers: HeaderListType,
2021-09-24 00:49:05 +00:00
body: Uint8Array | undefined
): Promise<BytesWithDetailsType> {
2020-11-13 19:57:55 +00:00
return _outerAjax(targetUrl, {
certificateAuthority,
data: body,
headers,
proxyUrl,
2021-09-24 00:49:05 +00:00
responseType: 'byteswithdetails',
2020-11-13 19:57:55 +00:00
timeout: 0,
type,
version,
2021-09-24 00:49:05 +00:00
});
2020-11-13 19:57:55 +00:00
}
2020-09-09 02:25:05 +00:00
// Groups
function generateGroupAuth(
groupPublicParamsHex: string,
authCredentialPresentationHex: string
) {
2021-09-24 00:49:05 +00:00
return Bytes.toBase64(
Bytes.fromString(
`${groupPublicParamsHex}:${authCredentialPresentationHex}`
)
);
2020-09-09 02:25:05 +00:00
}
type CredentialResponseType = {
credentials: Array<GroupCredentialType>;
};
2022-07-08 20:46:25 +00:00
async function getGroupCredentials({
startDayInMs,
endDayInMs,
2022-07-28 16:35:29 +00:00
}: GetGroupCredentialsOptionsType): Promise<GetGroupCredentialsResultType> {
2022-07-08 20:46:25 +00:00
const startDayInSeconds = startDayInMs / durations.SECOND;
const endDayInSeconds = endDayInMs / durations.SECOND;
2021-09-24 00:49:05 +00:00
const response = (await _ajax({
2020-09-09 02:25:05 +00:00
call: 'getGroupCredentials',
2022-07-08 20:46:25 +00:00
urlParameters:
`?redemptionStartSeconds=${startDayInSeconds}&` +
2023-08-16 20:54:39 +00:00
`redemptionEndSeconds=${endDayInSeconds}&` +
'pniAsServiceId=true',
2020-09-09 02:25:05 +00:00
httpType: 'GET',
responseType: 'json',
2021-09-24 00:49:05 +00:00
})) as CredentialResponseType;
2020-09-09 02:25:05 +00:00
2022-07-28 16:35:29 +00:00
return response;
2020-09-09 02:25:05 +00:00
}
2020-11-13 19:57:55 +00:00
async function getGroupExternalCredential(
options: GroupCredentialsType
2021-06-22 14:46:42 +00:00
): Promise<Proto.GroupExternalCredential> {
2020-11-13 19:57:55 +00:00
const basicAuth = generateGroupAuth(
options.groupPublicParamsHex,
options.authCredentialPresentationHex
);
2021-09-24 00:49:05 +00:00
const response = await _ajax({
2020-11-13 19:57:55 +00:00
basicAuth,
call: 'groupToken',
httpType: 'GET',
contentType: 'application/x-protobuf',
2021-09-24 00:49:05 +00:00
responseType: 'bytes',
2020-11-13 19:57:55 +00:00
host: storageUrl,
disableSessionResumption: true,
2020-11-13 19:57:55 +00:00
});
2021-09-24 00:49:05 +00:00
return Proto.GroupExternalCredential.decode(response);
2020-11-13 19:57:55 +00:00
}
2021-07-09 19:36:10 +00:00
function verifyAttributes(attributes: Proto.IAvatarUploadAttributes) {
2021-11-11 22:43:05 +00:00
const { key, credential, acl, algorithm, date, policy, signature } =
attributes;
2020-09-09 02:25:05 +00:00
if (
!key ||
!credential ||
!acl ||
!algorithm ||
!date ||
!policy ||
!signature
) {
throw new Error(
'verifyAttributes: Missing value from AvatarUploadAttributes'
);
}
return {
key,
credential,
acl,
algorithm,
date,
policy,
signature,
};
}
2021-07-19 19:26:06 +00:00
async function uploadAvatar(
uploadAvatarRequestHeaders: UploadAvatarHeadersType,
2021-09-24 00:49:05 +00:00
avatarData: Uint8Array
2021-07-19 19:26:06 +00:00
): Promise<string> {
const verified = verifyAttributes(uploadAvatarRequestHeaders);
const { key } = verified;
const manifestParams = makePutParams(verified, avatarData);
await _outerAjax(`${cdnUrlObject['0']}/`, {
...manifestParams,
certificateAuthority,
proxyUrl,
timeout: 0,
type: 'POST',
version,
});
return key;
}
2020-09-09 02:25:05 +00:00
async function uploadGroupAvatar(
2021-06-22 14:46:42 +00:00
avatarData: Uint8Array,
2020-09-09 02:25:05 +00:00
options: GroupCredentialsType
): Promise<string> {
const basicAuth = generateGroupAuth(
options.groupPublicParamsHex,
options.authCredentialPresentationHex
);
2021-09-24 00:49:05 +00:00
const response = await _ajax({
2020-09-09 02:25:05 +00:00
basicAuth,
call: 'getGroupAvatarUpload',
httpType: 'GET',
2021-09-24 00:49:05 +00:00
responseType: 'bytes',
2020-09-09 02:25:05 +00:00
host: storageUrl,
disableSessionResumption: true,
2020-09-09 02:25:05 +00:00
});
2021-09-24 00:49:05 +00:00
const attributes = Proto.AvatarUploadAttributes.decode(response);
2020-09-09 02:25:05 +00:00
const verified = verifyAttributes(attributes);
const { key } = verified;
2021-09-24 00:49:05 +00:00
const manifestParams = makePutParams(verified, avatarData);
2020-09-09 02:25:05 +00:00
await _outerAjax(`${cdnUrlObject['0']}/`, {
...manifestParams,
certificateAuthority,
proxyUrl,
timeout: 0,
type: 'POST',
version,
});
return key;
}
2021-09-24 00:49:05 +00:00
async function getGroupAvatar(key: string): Promise<Uint8Array> {
2020-09-09 02:25:05 +00:00
return _outerAjax(`${cdnUrlObject['0']}/${key}`, {
certificateAuthority,
proxyUrl,
2021-09-24 00:49:05 +00:00
responseType: 'bytes',
2020-09-09 02:25:05 +00:00
timeout: 0,
type: 'GET',
version,
redactUrl: _createRedactor(key),
2021-09-24 00:49:05 +00:00
});
2020-09-09 02:25:05 +00:00
}
async function createGroup(
2021-06-22 14:46:42 +00:00
group: Proto.IGroup,
2020-09-09 02:25:05 +00:00
options: GroupCredentialsType
): Promise<void> {
const basicAuth = generateGroupAuth(
options.groupPublicParamsHex,
options.authCredentialPresentationHex
);
2021-06-22 14:46:42 +00:00
const data = Proto.Group.encode(group).finish();
2020-09-09 02:25:05 +00:00
await _ajax({
basicAuth,
call: 'groups',
2020-11-20 17:30:45 +00:00
contentType: 'application/x-protobuf',
2020-09-09 02:25:05 +00:00
data,
host: storageUrl,
disableSessionResumption: true,
2020-11-20 17:30:45 +00:00
httpType: 'PUT',
2020-09-09 02:25:05 +00:00
});
}
async function getGroup(
options: GroupCredentialsType
2021-06-22 14:46:42 +00:00
): Promise<Proto.Group> {
2020-09-09 02:25:05 +00:00
const basicAuth = generateGroupAuth(
options.groupPublicParamsHex,
options.authCredentialPresentationHex
);
2021-09-24 00:49:05 +00:00
const response = await _ajax({
2020-09-09 02:25:05 +00:00
basicAuth,
call: 'groups',
contentType: 'application/x-protobuf',
host: storageUrl,
disableSessionResumption: true,
2020-11-20 17:30:45 +00:00
httpType: 'GET',
2021-09-24 00:49:05 +00:00
responseType: 'bytes',
2020-09-09 02:25:05 +00:00
});
2021-09-24 00:49:05 +00:00
return Proto.Group.decode(response);
2020-09-09 02:25:05 +00:00
}
async function getGroupFromLink(
inviteLinkPassword: string | undefined,
auth: GroupCredentialsType
2021-06-22 14:46:42 +00:00
): Promise<Proto.GroupJoinInfo> {
const basicAuth = generateGroupAuth(
auth.groupPublicParamsHex,
auth.authCredentialPresentationHex
);
const safeInviteLinkPassword = inviteLinkPassword
? toWebSafeBase64(inviteLinkPassword)
: undefined;
2021-09-24 00:49:05 +00:00
const response = await _ajax({
basicAuth,
call: 'groupsViaLink',
contentType: 'application/x-protobuf',
host: storageUrl,
disableSessionResumption: true,
httpType: 'GET',
2021-09-24 00:49:05 +00:00
responseType: 'bytes',
urlParameters: safeInviteLinkPassword
? `${safeInviteLinkPassword}`
: undefined,
redactUrl: _createRedactor(safeInviteLinkPassword),
});
2021-09-24 00:49:05 +00:00
return Proto.GroupJoinInfo.decode(response);
}
2020-09-09 02:25:05 +00:00
async function modifyGroup(
2021-06-22 14:46:42 +00:00
changes: Proto.GroupChange.IActions,
options: GroupCredentialsType,
inviteLinkBase64?: string
2021-06-22 14:46:42 +00:00
): Promise<Proto.IGroupChange> {
2020-09-09 02:25:05 +00:00
const basicAuth = generateGroupAuth(
options.groupPublicParamsHex,
options.authCredentialPresentationHex
);
2021-06-22 14:46:42 +00:00
const data = Proto.GroupChange.Actions.encode(changes).finish();
const safeInviteLinkPassword = inviteLinkBase64
? toWebSafeBase64(inviteLinkBase64)
: undefined;
2020-09-09 02:25:05 +00:00
2021-09-24 00:49:05 +00:00
const response = await _ajax({
2020-09-09 02:25:05 +00:00
basicAuth,
call: 'groups',
contentType: 'application/x-protobuf',
2020-11-20 17:30:45 +00:00
data,
2020-09-09 02:25:05 +00:00
host: storageUrl,
disableSessionResumption: true,
2020-11-20 17:30:45 +00:00
httpType: 'PATCH',
2021-09-24 00:49:05 +00:00
responseType: 'bytes',
urlParameters: safeInviteLinkPassword
? `?inviteLinkPassword=${safeInviteLinkPassword}`
: undefined,
redactUrl: safeInviteLinkPassword
? _createRedactor(safeInviteLinkPassword)
: undefined,
2020-09-09 02:25:05 +00:00
});
2021-09-24 00:49:05 +00:00
return Proto.GroupChange.decode(response);
2020-09-09 02:25:05 +00:00
}
async function getGroupLog(
options: GetGroupLogOptionsType,
credentials: GroupCredentialsType
2020-09-09 02:25:05 +00:00
): Promise<GroupLogResponseType> {
const basicAuth = generateGroupAuth(
credentials.groupPublicParamsHex,
credentials.authCredentialPresentationHex
2020-09-09 02:25:05 +00:00
);
const {
startVersion,
includeFirstState,
includeLastState,
maxSupportedChangeEpoch,
} = options;
// If we don't know starting revision - fetch it from the server
if (startVersion === undefined) {
const { data: joinedData } = await _ajax({
basicAuth,
call: 'groupJoinedAtVersion',
contentType: 'application/x-protobuf',
host: storageUrl,
disableSessionResumption: true,
httpType: 'GET',
responseType: 'byteswithdetails',
});
const { joinedAtVersion } = Proto.Member.decode(joinedData);
return getGroupLog(
{
...options,
startVersion: joinedAtVersion ?? 0,
},
credentials
);
}
2021-09-24 00:49:05 +00:00
const withDetails = await _ajax({
2020-09-09 02:25:05 +00:00
basicAuth,
call: 'groupLog',
contentType: 'application/x-protobuf',
host: storageUrl,
disableSessionResumption: true,
2020-11-20 17:30:45 +00:00
httpType: 'GET',
2021-09-24 00:49:05 +00:00
responseType: 'byteswithdetails',
urlParameters:
`/${startVersion}?` +
`includeFirstState=${Boolean(includeFirstState)}&` +
`includeLastState=${Boolean(includeLastState)}&` +
`maxSupportedChangeEpoch=${Number(maxSupportedChangeEpoch)}`,
2020-09-09 02:25:05 +00:00
});
const { data, response } = withDetails;
2021-09-24 00:49:05 +00:00
const changes = Proto.GroupChanges.decode(data);
2020-09-09 02:25:05 +00:00
if (response && response.status === 206) {
const range = response.headers.get('Content-Range');
const match = PARSE_GROUP_LOG_RANGE_HEADER.exec(range || '');
const start = match ? parseInt(match[1], 10) : undefined;
const end = match ? parseInt(match[2], 10) : undefined;
const currentRevision = match ? parseInt(match[3], 10) : undefined;
2020-09-09 02:25:05 +00:00
if (
match &&
2023-01-12 20:58:53 +00:00
isNumber(start) &&
isNumber(end) &&
isNumber(currentRevision)
2020-09-09 02:25:05 +00:00
) {
return {
changes,
start,
end,
currentRevision,
};
}
}
return {
changes,
};
}
async function getHasSubscription(
subscriberId: Uint8Array
): Promise<boolean> {
const formattedId = toWebSafeBase64(Bytes.toBase64(subscriberId));
const data = await _ajax({
call: 'subscriptions',
httpType: 'GET',
urlParameters: `/${formattedId}`,
responseType: 'json',
unauthenticated: true,
accessKey: undefined,
redactUrl: _createRedactor(formattedId),
});
return (
isRecord(data) &&
isRecord(data.subscription) &&
Boolean(data.subscription.active)
);
}
function getProvisioningResource(
handler: IRequestHandler
): Promise<WebSocketResource> {
return socketManager.getProvisioningResource(handler);
}
2020-09-04 01:25:19 +00:00
2023-02-27 22:34:43 +00:00
function getArtProvisioningSocket(token: string): Promise<WebSocket> {
return socketManager.connectExternalSocket({
url: `${artCreatorUrl}/api/socket?token=${token}`,
extraHeaders: {
origin: artCreatorUrl,
},
});
}
async function cdsLookup({
2021-12-06 22:54:20 +00:00
e164s,
acisAndAccessKeys = [],
2022-08-19 00:31:12 +00:00
returnAcisWithoutUaks,
useLibsignal,
}: CdsLookupOptionsType): Promise<CDSResponseType> {
2022-10-26 23:17:14 +00:00
return cds.request({
2021-11-12 20:45:30 +00:00
e164s,
acisAndAccessKeys,
2022-08-19 00:31:12 +00:00
returnAcisWithoutUaks,
useLibsignal,
2022-10-26 23:17:14 +00:00
});
2021-11-08 23:32:31 +00:00
}
2023-02-27 22:34:43 +00:00
//
// Art
//
async function getArtAuth(): Promise<ArtAuthType> {
const response = await _ajax({
call: 'getArtAuth',
httpType: 'GET',
responseType: 'json',
});
return artAuthZod.parse(response);
}
}
}