Make eslint/typescript more strict and fix getGroup type mismatch

Co-authored-by: Scott Nonnenberg <scott@signal.org>
This commit is contained in:
Jamie Kyle 2025-09-25 10:57:57 -07:00 committed by GitHub
parent 60b906a5b0
commit 870624186f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 905 additions and 698 deletions

View File

@ -1,9 +0,0 @@
src/**/*.js
src/**/*.d.ts
test/**/*.js
test/**/*.d.ts
bin/**/*.js
bin/**/*.d.ts
scripts/**/*.js
scripts/**/*.d.ts
protos/*

View File

@ -1,72 +0,0 @@
env:
browser: true
es2021: true
extends:
- 'eslint:recommended'
- 'plugin:@typescript-eslint/recommended'
parser: '@typescript-eslint/parser'
parserOptions:
ecmaVersion: 12
sourceType: module
plugins:
- '@typescript-eslint'
rules:
linebreak-style:
- error
- unix
quotes:
- error
- single
-
avoidEscape: true
semi:
- error
- always
comma-dangle:
- error
- always-multiline
curly:
- error
- all
dot-location:
- error
- property
dot-notation:
- error
eqeqeq:
- error
- always
no-constructor-return:
- error
no-implicit-globals:
- error
space-infix-ops:
- error
no-duplicate-imports:
- error
no-var:
- error
prefer-const:
- error
prefer-template:
- error
sort-imports:
- error
-
ignoreDeclarationSort: true
arrow-spacing:
- error
-
before: true
after: true
comma-spacing:
- error
-
before: false
after: true
no-multiple-empty-lines:
- error
no-trailing-spaces:
- error
no-tabs:
- error

View File

@ -27,6 +27,9 @@ jobs:
- name: Install node_modules
run: npm ci
- name: Lint
run: npm run lint
- name: Test
run: npm test

View File

@ -24,5 +24,8 @@ jobs:
- name: Install node_modules
run: npm install
- name: Run lint
run: npm run lint
- name: Run tests
run: npm test

60
eslint.config.mjs Normal file
View File

@ -0,0 +1,60 @@
// @ts-check
import eslint from '@eslint/js';
import { defineConfig } from 'eslint/config';
import tseslint from 'typescript-eslint';
export default defineConfig(
eslint.configs.recommended,
tseslint.configs.strictTypeChecked,
tseslint.configs.stylisticTypeChecked,
{
languageOptions: {
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname,
},
},
rules: {
'@typescript-eslint/array-type': ['error', { default: 'generic' }],
'@typescript-eslint/consistent-type-definitions': 'off',
'@typescript-eslint/prefer-literal-enum-member': 'off',
'@typescript-eslint/no-invalid-void-type': 'off',
'@typescript-eslint/require-await': 'off',
'@typescript-eslint/no-confusing-void-expression': [
'error',
{ ignoreArrowShorthand: true },
],
'@typescript-eslint/restrict-template-expressions': [
'error',
{ allowNumber: true, allowNullish: true },
],
'@typescript-eslint/no-unsafe-enum-comparison': 'off',
'@typescript-eslint/method-signature-style': ['error', 'property'],
'@typescript-eslint/explicit-module-boundary-types': 'error',
'@typescript-eslint/strict-boolean-expressions': [
'error',
{
allowNullableBoolean: true,
allowNullableNumber: true,
allowNullableString: true,
},
],
},
},
{
ignores: [
// ignore config files
'eslint.config.mjs',
'.prettierrc.js',
// ignore scripts
'certs/generate-trust-root.js',
'certs/generate-zk-params.js',
// ignore tsc compiled files
'{src,test}/**/*.js',
'{src,test}/**/*.d.ts',
// ignore compiled protobuf files
'protos/**',
],
},
);

1117
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -19,7 +19,9 @@
"build": "npm run build:protobuf && npm run build:protobuf-ts && npm run build:tsc",
"format": "pprettier --write '**/*.ts'",
"mocha": "mocha test/**/*-test.js",
"lint": "eslint --cache src test && pprettier --check '**/*.ts'",
"lint:eslint": "eslint .",
"lint:prettier": "pprettier --check '**/*.ts'",
"lint": "npm run lint:eslint && npm run lint:prettier",
"test": "npm run mocha && npm run lint",
"prepare": "npm run build"
},
@ -60,6 +62,7 @@
"zod": "^3.20.2"
},
"devDependencies": {
"@eslint/js": "^9.36.0",
"@types/debug": "^4.1.7",
"@types/long": "^4.0.1",
"@types/micro": "^7.3.6",
@ -68,11 +71,10 @@
"@types/node": "^20.19.9",
"@types/uuid": "^8.3.0",
"@types/ws": "^8.2.2",
"@typescript-eslint/eslint-plugin": "^5.10.1",
"@typescript-eslint/parser": "^5.10.1",
"eslint": "^8.7.0",
"eslint": "^9.36.0",
"mocha": "^9.2.0",
"protobufjs-cli": "^1.1.1",
"typescript": "^5.1.0"
"typescript": "^5.1.0",
"typescript-eslint": "^8.44.1"
}
}

View File

@ -63,7 +63,7 @@ export class Group extends GroupData {
this.secretParams = secretParams;
const cipher = new ClientZkGroupCipher(secretParams);
this.title = decryptBlob(cipher, groupState.title)?.title ?? '';
this.title = decryptBlob(cipher, groupState.title).title ?? '';
this.privPublicParams = this.secretParams.getPublicParams();

View File

@ -270,7 +270,7 @@ enum SyncState {
type SyncEntry = {
state: SyncState;
onComplete: Promise<void>;
complete(): void;
complete: () => void;
};
type DecryptResult = Readonly<{
@ -417,7 +417,7 @@ class IdentityStore extends IdentityKeyStore {
}
async getIdentity(name: ProtocolAddress): Promise<PublicKey | null> {
return this.knownIdentities.get(addressToString(name)) || null;
return this.knownIdentities.get(addressToString(name)) ?? null;
}
// Not part of IdentityKeyStore API
@ -432,7 +432,7 @@ class IdentityStore extends IdentityKeyStore {
}
export class SessionStore extends SessionStoreBase {
private readonly sessions: Map<string, SessionRecord> = new Map();
private readonly sessions = new Map<string, SessionRecord>();
async saveSession(
name: ProtocolAddress,
@ -442,12 +442,12 @@ export class SessionStore extends SessionStoreBase {
}
async getSession(name: ProtocolAddress): Promise<SessionRecord | null> {
return this.sessions.get(addressToString(name)) || null;
return this.sessions.get(addressToString(name)) ?? null;
}
async getExistingSessions(
addresses: ProtocolAddress[],
): Promise<SessionRecord[]> {
addresses: Array<ProtocolAddress>,
): Promise<Array<SessionRecord>> {
return addresses.map((name) => {
const existing = this.sessions.get(addressToString(name));
if (!existing) {
@ -459,21 +459,25 @@ export class SessionStore extends SessionStoreBase {
}
export class SenderKeyStore extends SenderKeyStoreBase {
private readonly keys: Map<string, SenderKeyRecord> = new Map();
private readonly keys = new Map<string, SenderKeyRecord>();
async saveSenderKey(
sender: ProtocolAddress,
distributionId: Uuid,
record: SenderKeyRecord,
): Promise<void> {
this.keys.set(`${sender.serviceId}.${distributionId}`, record);
const serviceId = sender.serviceId();
assert(serviceId != null, 'Missing serviceId for sender');
this.keys.set(`${serviceId.toString()}.${distributionId}`, record);
}
async getSenderKey(
sender: ProtocolAddress,
distributionId: Uuid,
): Promise<SenderKeyRecord | null> {
const key = this.keys.get(`${sender.serviceId}.${distributionId}`);
return key || null;
const serviceId = sender.serviceId();
assert(serviceId != null, 'Missing serviceId for sender');
const key = this.keys.get(`${serviceId.toString()}.${distributionId}`);
return key ?? null;
}
}
@ -619,7 +623,7 @@ export class PrimaryDevice {
signedPreKey.getPublicKey().serialize(),
);
const signedPreKeyId =
this.signedPreKeys.get(serviceIdKind)?.getNextId() || 1;
this.signedPreKeys.get(serviceIdKind)?.getNextId() ?? 1;
const signedPreKeyRecord = SignedPreKeyRecord.new(
signedPreKeyId,
Date.now(),
@ -634,7 +638,7 @@ export class PrimaryDevice {
}
const lastResortKeyId =
this.kyberPreKeys.get(serviceIdKind)?.getNextId() || 1;
this.kyberPreKeys.get(serviceIdKind)?.getNextId() ?? 1;
const lastResortKeyRecord = this.generateKyberPreKey(
lastResortKeyId,
serviceIdKind,
@ -668,7 +672,7 @@ export class PrimaryDevice {
private async *getPreKeyIterator(
device: Device,
serviceIdKind: ServiceIdKind,
): AsyncIterator<PreKey> {
): AsyncIterator<PreKey, undefined> {
const preKeyStore = this.preKeys.get(serviceIdKind);
assert.ok(preKeyStore, 'Missing preKey store');
@ -677,6 +681,7 @@ export class PrimaryDevice {
return;
}
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
while (true) {
const preKey = PrivateKey.generate();
const publicKey = preKey.getPublicKey();
@ -710,7 +715,7 @@ export class PrimaryDevice {
private async *getKyberPreKeyIterator(
device: Device,
serviceIdKind: ServiceIdKind,
): AsyncIterator<KyberPreKey> {
): AsyncIterator<KyberPreKey, undefined> {
const kyberPreKeyStore = this.kyberPreKeys.get(serviceIdKind);
assert.ok(kyberPreKeyStore, 'Missing kyberPreKeyStore store');
@ -719,6 +724,7 @@ export class PrimaryDevice {
return;
}
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
while (true) {
const keyId = kyberPreKeyStore.getNextId();
const record = this.generateKyberPreKey(keyId, serviceIdKind);
@ -799,7 +805,7 @@ export class PrimaryDevice {
): Promise<ReadonlyArray<Group>> {
const records = storage.getAllGroupRecords();
return await Promise.all(
return Promise.all(
records.map(async ({ record }) => {
const { groupV2 } = record;
assert.ok(groupV2, 'Not a group v2 record!');
@ -911,7 +917,7 @@ export class PrimaryDevice {
groupState: serverGroup.state,
});
if (sendUpdateTo?.length) {
if (sendUpdateTo.length) {
const groupV2 = {
...updatedGroup.toContext(),
groupChange: Proto.GroupChange.encode(
@ -1075,6 +1081,7 @@ export class PrimaryDevice {
false,
);
if (!updated) {
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
throw new Error(`setStorageState: failed to update, ${error}`);
}
@ -1124,7 +1131,7 @@ export class PrimaryDevice {
content,
envelopeType: unsealedType,
} = await this.lock(async () => {
return await this.decrypt(source, serviceIdKind, envelopeType, encrypted);
return this.decrypt(source, serviceIdKind, envelopeType, encrypted);
});
let handled = true;
@ -1137,7 +1144,7 @@ export class PrimaryDevice {
ServiceIdKind.ACI,
'Got sync message on PNI',
);
await this.handleResendRequest(
this.handleResendRequest(
unsealedSource,
serviceIdKind,
unsealedType,
@ -1236,7 +1243,7 @@ export class PrimaryDevice {
},
pniSignatureMessage,
};
return await this.encryptContent(target, content, encryptOptions);
return this.encryptContent(target, content, encryptOptions);
}
public async encryptSyncSent(
@ -1260,7 +1267,7 @@ export class PrimaryDevice {
},
},
};
return await this.encryptContent(target, content, options);
return this.encryptContent(target, content, options);
}
public async encryptSyncRead(
@ -1278,7 +1285,7 @@ export class PrimaryDevice {
}),
},
};
return await this.encryptContent(target, content, options);
return this.encryptContent(target, content, options);
}
public async sendFetchStorage(options: FetchStorageOptions): Promise<void> {
@ -1333,7 +1340,7 @@ export class PrimaryDevice {
),
},
};
return await this.encryptContent(target, content, options);
return this.encryptContent(target, content, options);
}
public async sendReceipt(
@ -1498,7 +1505,7 @@ export class PrimaryDevice {
);
if (!options?.skipSkdmSend) {
this.sendRaw(
void this.sendRaw(
target,
{
senderKeyDistributionMessage: skdm.serialize(),
@ -1541,11 +1548,11 @@ export class PrimaryDevice {
throw new Error('Unsupported envelope type');
}
const serviceIdKind = envelope.destinationServiceIdBinary?.length
const serviceIdKind = envelope.destinationServiceIdBinary.length
? this.device.getServiceIdBinaryKind(envelope.destinationServiceIdBinary)
: ServiceIdKind.ACI;
return await this.handleEnvelope(
return this.handleEnvelope(
source,
serviceIdKind,
envelopeType,
@ -1646,8 +1653,8 @@ export class PrimaryDevice {
): Promise<Buffer> {
const encoded = Buffer.from(Proto.Content.encode(content).finish());
return await this.lock(async () => {
return await this.encrypt(target, encoded, options);
return this.lock(async () => {
return this.encrypt(target, encoded, options);
});
}
@ -2040,7 +2047,10 @@ export class PrimaryDevice {
senderKeys,
encrypted,
);
} else if (envelopeType === EnvelopeType.SealedSender) {
} else if (
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
envelopeType === EnvelopeType.SealedSender
) {
assert(source === undefined, 'Sealed sender must have no source');
const usmc = await SignalClient.sealedSenderDecryptToUsmc(
@ -2091,6 +2101,7 @@ export class PrimaryDevice {
return result;
} else {
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
throw new Error(`Unsupported envelope type: ${envelopeType}`);
}
@ -2159,7 +2170,7 @@ export class PrimaryDevice {
const version = decryptedManifest.version.toNumber();
const items = await Promise.all(
(decryptedManifest.identifiers || []).map(async ({ type, raw: key }) => {
(decryptedManifest.identifiers ?? []).map(async ({ type, raw: key }) => {
assert(
type !== null && type !== undefined,
'Missing manifestRecord.keys.type',

View File

@ -103,7 +103,7 @@ export type CreatePrimaryDeviceOptions = Readonly<{
}>;
export type PendingProvision = {
complete(response: PendingProvisionResponse): Promise<Device>;
complete: (response: PendingProvisionResponse) => Promise<Device>;
};
export type PendingProvisionResponse = Readonly<{
@ -127,9 +127,11 @@ const CERTS_DIR = path.join(__dirname, '..', '..', 'certs');
const CERT = fs.readFileSync(path.join(CERTS_DIR, 'full-cert.pem'));
const KEY = fs.readFileSync(path.join(CERTS_DIR, 'key.pem'));
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const TRUST_ROOT: TrustRoot = JSON.parse(
fs.readFileSync(path.join(CERTS_DIR, 'trust-root.json')).toString(),
);
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const ZK_PARAMS: ZKParams = JSON.parse(
fs.readFileSync(path.join(CERTS_DIR, 'zk-params.json')).toString(),
);
@ -177,7 +179,7 @@ export class Server extends BaseServer {
https: {
key: KEY,
cert: CERT,
...(config.https || {}),
...(config.https ?? {}),
},
};
@ -222,18 +224,19 @@ export class Server extends BaseServer {
updates2Path: this.config.updates2Path,
});
const server = https.createServer(this.config.https || {}, (req, res) => {
run(req, res, httpHandler);
const server = https.createServer(this.config.https, (req, res) => {
void run(req, res, httpHandler);
});
const wss = new WebSocket.Server({
server,
// eslint-disable-next-line @typescript-eslint/no-misused-promises
verifyClient: async (info, callback) => {
const { url } = info.req;
assert(url, 'verifyClient: expected a URL on incoming request');
const query = parseURL(url, true).query || {};
const { query } = parseURL(url, true);
if (!query.login && !query.password) {
if (query.login == null && query.password == null) {
debug('verifyClient: Allowing connection with no credentials');
callback(true);
return;
@ -241,7 +244,7 @@ export class Server extends BaseServer {
// Note: when a device has been unlinked, it will use '' as its password
if (
!query.login ||
query.login == null ||
Array.isArray(query.login) ||
typeof query.password !== 'string' ||
Array.isArray(query.password)
@ -265,9 +268,9 @@ export class Server extends BaseServer {
wss.on('connection', (ws, request) => {
const conn = new WSConnection(request, ws, this);
conn.start().catch((error) => {
conn.start().catch((error: unknown) => {
ws.close();
debug('Websocket handling error', error.stack);
debug('Websocket handling error', error);
});
});
@ -281,7 +284,7 @@ export class Server extends BaseServer {
this.https = server;
return await new Promise((resolve) => {
return new Promise((resolve) => {
server.listen(port, host, () => resolve());
});
}
@ -302,7 +305,7 @@ export class Server extends BaseServer {
//
public async waitForProvision(): Promise<PendingProvision> {
return await this.provisionQueue.shift();
return this.provisionQueue.shift();
}
private async waitForStorageManifest(
@ -346,8 +349,8 @@ export class Server extends BaseServer {
}: CreatePrimaryDeviceOptions): Promise<PrimaryDevice> {
const number = await this.generateNumber();
const registrationId = await generateRegistrationId();
const pniRegistrationId = await generateRegistrationId();
const registrationId = generateRegistrationId();
const pniRegistrationId = generateRegistrationId();
const devicePassword = password ?? generateDevicePassword();
const device = await this.registerDevice({
number,
@ -417,8 +420,8 @@ export class Server extends BaseServer {
}
public async createSecondaryDevice(primary: PrimaryDevice): Promise<Device> {
const registrationId = await generateRegistrationId();
const pniRegistrationId = await generateRegistrationId();
const registrationId = generateRegistrationId();
const pniRegistrationId = generateRegistrationId();
const device = await this.registerDevice({
primary: primary.device,
@ -550,7 +553,7 @@ export class Server extends BaseServer {
await this.provisionQueue.pushAndWait({
complete: async (response) => {
await responseQueue.pushAndWait(response);
return await resultQueue.shift();
return resultQueue.shift();
},
});
@ -560,10 +563,10 @@ export class Server extends BaseServer {
primaryDevice,
} = await responseQueue.shift();
const query = parseURL(provisionURL, true).query || {};
const { query } = parseURL(provisionURL, true);
assert.strictEqual(query.uuid, id, 'id mismatch');
if (!query.pub_key || Array.isArray(query.pub_key)) {
if (query.pub_key == null || Array.isArray(query.pub_key)) {
throw new Error('Expected `pub_key` in provision URL');
}
@ -627,10 +630,9 @@ export class Server extends BaseServer {
encrypted: Buffer,
timestamp: number,
): Promise<void> {
assert(
source || envelopeType === EnvelopeType.SealedSender,
'No source for non-sealed sender envelope',
);
if (envelopeType !== EnvelopeType.SealedSender) {
assert(source, 'No source for non-sealed sender envelope');
}
debug('got message for %s.%d', target.aci, target.deviceId);
@ -654,7 +656,7 @@ export class Server extends BaseServer {
default:
throw new Error(`Unsupported envelope type: ${envelopeType}`);
}
this.send(
void this.send(
target,
Buffer.from(
Proto.Envelope.encode({
@ -889,7 +891,7 @@ export class Server extends BaseServer {
await fsPromises.writeFile(finalPath, reencrypted.blob);
this.onNewBackupMediaObject(backupId, {
void this.onNewBackupMediaObject(backupId, {
cdn: 3,
mediaId: item.mediaId,
objectLength: reencrypted.blob.length,

View File

@ -89,7 +89,7 @@ class StorageStateItem {
return false;
}
const masterKey = this.record?.groupV2?.masterKey;
const masterKey = this.record.groupV2?.masterKey;
if (!masterKey) {
return false;
}
@ -103,7 +103,7 @@ class StorageStateItem {
}
if (serviceIdKind === ServiceIdKind.ACI) {
const existingAci = this.record?.contact?.aciBinary;
const existingAci = this.record.contact?.aciBinary;
if (!existingAci?.length) {
return false;
}
@ -111,7 +111,7 @@ class StorageStateItem {
return Buffer.compare(existingAci, device.aciRawUuid) === 0;
}
const existingPni = this.record?.contact?.pniBinary;
const existingPni = this.record.contact?.pniBinary;
if (!existingPni?.length) {
return false;
}
@ -239,7 +239,7 @@ export class StorageState {
const account = this.getAccountRecord();
assert(account, 'No account record found');
return (account.pinnedConversations || []).some((convo) => {
return (account.pinnedConversations ?? []).some((convo) => {
if (!convo.groupMasterKey) {
return false;
}
@ -343,8 +343,8 @@ export class StorageState {
const account = this.getAccountRecord();
assert(account, 'No account record found');
return (account.pinnedConversations || []).some((convo) => {
const existing = convo?.contact?.serviceIdBinary;
return (account.pinnedConversations ?? []).some((convo) => {
const existing = convo.contact?.serviceIdBinary;
return existing && Buffer.compare(existing, device.aciRawUuid) === 0;
});
}
@ -389,7 +389,7 @@ export class StorageState {
}
public hasKey(storageKey: Buffer): boolean {
return this.hasRecord(({ key }) => key?.equals(storageKey));
return this.hasRecord(({ key }) => key.equals(storageKey));
}
//
@ -533,10 +533,10 @@ export class StorageState {
const { pinnedConversations } = account;
const newPinnedConversations = pinnedConversations?.slice() || [];
const newPinnedConversations = pinnedConversations?.slice() ?? [];
const existingIndex = newPinnedConversations.findIndex((convo) => {
const existing = convo?.contact?.serviceIdBinary;
const existing = convo.contact?.serviceIdBinary;
return (
existing && Buffer.compare(existing, deviceServiceIdBinary) === 0
);
@ -568,7 +568,7 @@ export class StorageState {
const { pinnedConversations } = account;
const newPinnedConversations = pinnedConversations?.slice() || [];
const newPinnedConversations = pinnedConversations?.slice() ?? [];
const existingIndex = newPinnedConversations.findIndex((convo) => {
if (!convo.groupMasterKey) {

View File

@ -159,7 +159,7 @@ export function generateSenderCertificate(
senderE164: sender.number,
senderUuid: sender.aci,
senderDevice: sender.deviceId,
expires: Long.fromNumber(sender.expires || NEVER_EXPIRES),
expires: Long.fromNumber(sender.expires ?? NEVER_EXPIRES),
identityKey: sender.identityKey.serialize(),
signer: serverCert.certificate,
}).finish(),
@ -206,7 +206,7 @@ export function deriveStorageKey(masterKey: Buffer): Buffer {
function deriveStorageManifestKey(storageKey: Buffer, version: Long): Buffer {
const hash = crypto.createHmac('sha256', storageKey);
hash.update(`Manifest_${version}`);
hash.update(`Manifest_${version.toString()}`);
return hash.digest();
}
@ -242,9 +242,9 @@ export function deriveStorageItemKey({
}
function decryptAESGCM(ciphertext: Buffer, key: Buffer): Buffer {
const iv = ciphertext.slice(0, AESGCM_IV_SIZE);
const tag = ciphertext.slice(ciphertext.length - AUTH_TAG_SIZE);
const rest = ciphertext.slice(iv.length, ciphertext.length - tag.length);
const iv = ciphertext.subarray(0, AESGCM_IV_SIZE);
const tag = ciphertext.subarray(ciphertext.length - AUTH_TAG_SIZE);
const rest = ciphertext.subarray(iv.length, ciphertext.length - tag.length);
const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
@ -281,6 +281,7 @@ export function decryptStorageManifest(
decryptAESGCM(Buffer.from(manifest.value), manifestKey),
);
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition, @typescript-eslint/strict-boolean-expressions
if (!decoded.version) {
throw new Error('Missing manifestRecord.version');
}

View File

@ -25,7 +25,9 @@ async function loadJSONProperty(
property: string,
): Promise<string> {
const raw = await fs.readFile(path.join(CERTS_DIR, file));
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const obj = JSON.parse(raw.toString());
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment
const value = obj[property];
assert(typeof value === 'string', `Expected string at: ${file}/${property}`);

View File

@ -51,8 +51,8 @@ export type DeviceKeys = Readonly<{
lastResortKey?: KyberPreKey;
signedPreKey?: SignedPreKey;
preKeyIterator?: AsyncIterator<PreKey>;
kyberPreKeyIterator?: AsyncIterator<KyberPreKey>;
preKeyIterator?: AsyncIterator<PreKey, undefined>;
kyberPreKeyIterator?: AsyncIterator<KyberPreKey, undefined>;
}>;
export type SingleUseKey = Readonly<{
@ -69,8 +69,8 @@ type InternalDeviceKeys = Readonly<{
lastResortKey: KyberPreKey;
preKeys: Array<PreKey>;
kyberPreKeys: Array<KyberPreKey>;
preKeyIterator?: AsyncIterator<PreKey>;
kyberPreKeyIterator?: AsyncIterator<KyberPreKey>;
preKeyIterator?: AsyncIterator<PreKey, undefined>;
kyberPreKeyIterator?: AsyncIterator<KyberPreKey, undefined>;
}>;
// Technically, it is infinite.
@ -232,21 +232,17 @@ export class Device {
const { value } = await keys.preKeyIterator.next();
preKey = value;
}
if (!preKey) {
preKey = keys.preKeys.shift();
}
preKey ??= keys.preKeys.shift();
let pqPreKey: KyberPreKey | undefined;
if (keys.kyberPreKeyIterator) {
const { value } = await keys.kyberPreKeyIterator.next();
pqPreKey = value;
}
if (!pqPreKey) {
pqPreKey = keys.kyberPreKeys.shift();
}
if (!pqPreKey) {
pqPreKey = keys.lastResortKey;
}
pqPreKey ??= keys.kyberPreKeys.shift();
pqPreKey ??= keys.lastResortKey;
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition, @typescript-eslint/strict-boolean-expressions
if (!pqPreKey) {
throw new Error(
'popSingleUseKey: Missing pqPreKey; checked iterator/array/lastResort',

View File

@ -26,7 +26,7 @@ export abstract class Group {
public get state(): Readonly<Proto.IGroup> {
const { groupChanges } = this.changes;
assert(groupChanges, 'Missing group changes in the group state');
const state = groupChanges[groupChanges.length - 1].groupState;
const state = groupChanges.at(-1)?.groupState;
assert(state, 'Group must have the last state');
return state;
}

View File

@ -205,11 +205,11 @@ export type IsSendRateLimitedOptions = Readonly<{
export { type ModifyGroupResult };
interface WebSocket {
sendMessage(message: Buffer | 'empty'): Promise<void>;
sendMessage: (message: Buffer | 'empty') => Promise<void>;
}
interface SerializableCredential {
serialize(): Uint8Array;
serialize: () => Uint8Array;
}
type AuthEntry = Readonly<{
@ -225,8 +225,8 @@ type StorageAuthEntry = Readonly<{
type MessageQueueEntry = {
readonly message: Buffer;
resolve(): void;
reject(error: Error): void;
resolve: () => void;
reject: (error: Error) => void;
};
export type CallLinkEntry = Readonly<{
@ -369,7 +369,7 @@ export abstract class Server {
}
const result = this.https.address();
if (!result || typeof result !== 'object') {
if (result == null || typeof result !== 'object') {
throw new Error('Invalid .address() result');
}
return result;
@ -511,7 +511,7 @@ export abstract class Server {
}
entry.delete(provisioningCode);
const [primary] = this.devices.get(number) || [];
const [primary] = this.devices.get(number) ?? [];
assert(primary !== undefined, 'Missing primary device when provisioning');
const device = await this.registerDevice({
@ -622,11 +622,11 @@ export abstract class Server {
//
// Remote config
//
public setRemoteConfig(key: string, value: RemoteConfigValueType) {
public setRemoteConfig(key: string, value: RemoteConfigValueType): void {
this.remoteConfig.set(key, value);
}
public getRemoteConfig() {
public getRemoteConfig(): Map<string, RemoteConfigValueType> {
return this.remoteConfig;
}
@ -844,6 +844,7 @@ export abstract class Server {
);
// At least one send should succeed, if not - queue
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (success) {
return;
}
@ -998,7 +999,7 @@ export abstract class Server {
await this.clearStorageItems(device);
}
const inserts = (insertItem || []).map(async (item) => {
const inserts = (insertItem ?? []).map(async (item) => {
assert(item.key instanceof Uint8Array, 'insertItem.key must be a Buffer');
assert(
item.value instanceof Uint8Array,
@ -1012,7 +1013,7 @@ export abstract class Server {
});
await Promise.all(inserts);
const deletes = (deleteKey || []).map(async (key) => {
const deletes = (deleteKey ?? []).map(async (key) => {
return this.deleteStorageItem(device, Buffer.from(key));
});
await Promise.all(deletes);
@ -1260,7 +1261,7 @@ export abstract class Server {
);
}
public hasCallLink(roomId: string) {
public hasCallLink(roomId: string): boolean {
return this.callLinksByRoomId.has(roomId);
}
@ -1355,7 +1356,8 @@ export abstract class Server {
const device = list[deviceId - 1];
debug('removeDevice %j.%j (%j)', device.aci, deviceId, number);
debug('removeDevice %j.%j (%j)', device?.aci, deviceId, number);
assert(device != null, `Missing device for deviceId ${deviceId}`);
const copy = [...list];
copy.splice(deviceId - 1, 1);
@ -1379,7 +1381,7 @@ export abstract class Server {
if (primary.deviceId !== PRIMARY_DEVICE_ID) {
return undefined;
}
return await this.getDevice(primary.number, deviceId);
return this.getDevice(primary.number, deviceId);
}
public async getAllDevicesByServiceId(
@ -1390,7 +1392,7 @@ export abstract class Server {
return [];
}
return this.devices.get(primary.number) || [];
return this.devices.get(primary.number) ?? [];
}
public async getSenderCertificate(
@ -1810,7 +1812,7 @@ export abstract class Server {
'base64url',
);
const validatingKey = this.backupKeyById.get(backupId) || newPublicKey;
const validatingKey = this.backupKeyById.get(backupId) ?? newPublicKey;
if (!validatingKey) {
throw new Error('No backup public key to validate against');
}

View File

@ -170,7 +170,7 @@ export class ServerGroup extends Group {
const { userId, role } = member;
assert.ok(userId, 'Missing addPendingMembers.added.member.userId');
assert.ok(role, 'Missing addPendingMembers.added.member.role');
assert.ok(role != null, 'Missing addPendingMembers.added.member.role');
this.verifyAccess(
'pendingMembers',

View File

@ -90,7 +90,7 @@ export const createHandler = (
);
if (!thePath) {
send(res, 400, { error: 'Missing path' });
void send(res, 400, { error: 'Missing path' });
return;
}
@ -127,7 +127,7 @@ export const createHandler = (
);
if (!thePath) {
send(res, 400, { error: 'Missing path' });
void send(res, 400, { error: 'Missing path' });
return;
}
@ -156,6 +156,8 @@ export const createHandler = (
const getCdn3Attachment = get('/cdn3/:folder/*', async (req, res) => {
assert(cdn3Path, 'cdn3Path must be set');
assert(req.params.folder != null, 'Missing folder param');
assert(req.params._ != null, 'Missing extra params');
if (req.params.folder === 'backups') {
const { username, password, error } = parsePassword(req);
@ -166,16 +168,16 @@ export const createHandler = (
req.url,
error,
);
send(res, 401, { error });
void send(res, 401, { error });
return;
}
if (!username || !password) {
send(res, 401, { error: 'Missing username and/or password' });
void send(res, 401, { error: 'Missing username and/or password' });
return;
}
const authorized = await server.authorizeBackupCDN(username, password);
if (!authorized) {
send(res, 403, { error: 'Invalid password' });
void send(res, 403, { error: 'Invalid password' });
return;
}
}
@ -214,6 +216,7 @@ export const createHandler = (
const getStickerPack = get(
'/stickers/:pack/manifest.proto',
async (req, res) => {
assert(req.params.pack != null, 'Missing pack param');
const { pack } = req.params;
const result = await server.fetchStickerPack(pack);
if (!result) {
@ -224,6 +227,8 @@ export const createHandler = (
);
const getSticker = get('/stickers/:pack/full/:sticker', async (req, res) => {
assert(req.params.pack != null, 'Missing pack param');
assert(req.params.sticker != null, 'Missing sticker param');
const { pack, sticker } = req.params;
const result = await server.fetchSticker(pack, parseInt(sticker, 10));
if (!result) {
@ -244,7 +249,7 @@ export const createHandler = (
function toCallLinkResponse(callLink: CallLinkEntry) {
return {
name: callLink.encryptedName,
restrictions: String(callLink.restrictions),
restrictions: callLink.restrictions,
revoked: callLink.revoked,
expiration: Math.floor(callLink.expiration / 1000), // unix
};
@ -270,7 +275,7 @@ export const createHandler = (
return send(res, 400, { error: 'Missing room ID' });
}
const body = await json(req);
const body: unknown = await json(req);
let callLink: CallLinkEntry;
if (!server.hasCallLink(roomId)) {
@ -305,14 +310,14 @@ export const createHandler = (
const { username, password, error } = parsePassword(req);
if (error) {
debug('%s %s auth failed, error %j', req.method, req.url, error);
send(res, 401, { error });
void send(res, 401, { error });
return;
}
const device = await server.auth(username ?? '', password ?? '');
if (!device) {
debug('%s %s auth failed, need re-provisioning', req.method, req.url);
send(res, 401, { error: 'Need re-provisioning' });
void send(res, 401, { error: 'Need re-provisioning' });
return;
}
@ -332,11 +337,11 @@ export const createHandler = (
const { error, username, password } = parsePassword(req);
if (error) {
send(res, 400, { error });
void send(res, 400, { error });
return undefined;
}
if (!username || !password) {
send(res, 400, { error: 'Invalid authorization header' });
void send(res, 400, { error: 'Invalid authorization header' });
return undefined;
}
@ -353,10 +358,11 @@ export const createHandler = (
aciCiphertext = auth.getUuidCiphertext();
const maybePni = auth.getPniCiphertext();
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
assert(maybePni, 'Auth credentials must have PNI');
pniCiphertext = maybePni;
} catch (_) {
send(res, 403, { error: 'Invalid credentials' });
} catch {
void send(res, 403, { error: 'Invalid credentials' });
return undefined;
}
@ -380,7 +386,7 @@ export const createHandler = (
const group = await server.getGroup(auth.publicParams);
if (!group) {
send(res, 404, { error: 'Group not found' });
void send(res, 404, { error: 'Group not found' });
return undefined;
}
@ -394,18 +400,18 @@ export const createHandler = (
const { error, username, password } = parsePassword(req);
if (error) {
send(res, 400, { error });
void send(res, 400, { error });
return undefined;
}
if (!username || !password) {
send(res, 400, { error: 'Invalid authorization header' });
void send(res, 400, { error: 'Invalid authorization header' });
return undefined;
}
const device = await server.storageAuth(username, password);
if (!device) {
debug('%s %s storage auth failed', req.method, req.url);
send(res, 403, { error: 'Invalid authorization' });
void send(res, 403, { error: 'Invalid authorization' });
return undefined;
}
@ -490,6 +496,7 @@ export const createHandler = (
return send(res, 403, { error: 'Not a member of this group' });
}
assert(req.params.since != null, 'Missing since param');
const since = parseInt(req.params.since, 10);
if (since < (member.joinedAtVersion ?? 0)) {
return send(res, 403, { error: '`since` is before joinedAtVersion' });
@ -582,11 +589,11 @@ export const createHandler = (
}
const groupData = Proto.Group.decode(Buffer.from(await buffer(req)));
if (!groupData.title) {
if (!groupData.title.length) {
return send(res, 400, { error: 'Missing group title' });
}
if (
!groupData.publicKey ||
!groupData.publicKey.length ||
!auth.publicParams.equals(groupData.publicKey)
) {
return send(res, 400, { error: 'Invalid group public key' });
@ -656,7 +663,8 @@ export const createHandler = (
});
if (modifyResult.conflict) {
return send(res, 409, { error: 'Conflict' });
await send(res, 409, { error: 'Conflict' });
return;
}
return {
@ -726,6 +734,7 @@ export const createHandler = (
return;
}
assert(req.params.after != null, 'Missing after param');
const after = Long.fromString(req.params.after);
const manifest = await server.getStorageManifest(device);
if (!manifest?.version?.gt(after)) {
@ -772,7 +781,7 @@ export const createHandler = (
Buffer.from(await buffer(req)),
);
const keys = (readOperation.readKey || []).map((key) => Buffer.from(key));
const keys = readOperation.readKey.map((key) => Buffer.from(key));
const items = await server.getStorageItems(device, keys);
if (!items) {
@ -854,6 +863,7 @@ export const createHandler = (
res.once('finish', () => {
debug('response %s %s', req.method, req.url, res.statusCode);
});
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return routes(req, res);
} catch (error) {
assert(error instanceof Error);

View File

@ -98,7 +98,7 @@ export class Connection extends Service {
let credential: Buffer | undefined;
if (params.request) {
const request = new ProfileKeyCredentialRequest(
Buffer.from(params.request as string, 'hex'),
Buffer.from(params.request, 'hex'),
);
if (credentialType === 'expiringProfileKey') {
credential = await this.server.issueExpiringProfileKeyCredential(
@ -195,7 +195,7 @@ export class Connection extends Service {
if (!body) {
return [400, { error: 'Missing body' }];
}
if (!query.ts) {
if (query.ts == null) {
return [400, { error: 'Missing ts' }];
}
@ -417,7 +417,7 @@ export class Connection extends Service {
const body = DeviceKeysSchema.parse(JSON.parse(rawBody.toString()));
try {
await server.updateDeviceKeys(this.getDevice(), serviceIdKind, {
preKeys: body.preKeys?.map(decodePreKey),
preKeys: body.preKeys.map(decodePreKey),
kyberPreKeys: body.pqPreKeys?.map(decodeKyberPreKey),
lastResortKey: body.pqLastResortPreKey
? decodeKyberPreKey(body.pqLastResortPreKey)
@ -453,8 +453,8 @@ export class Connection extends Service {
);
this.router.get('/v2/keys/:serviceId/:deviceId', async (params) => {
const serviceId = params.serviceId as ServiceIdString;
const deviceId = parseInt(params.deviceId || '', 10) as DeviceId;
const serviceId = params.serviceId as ServiceIdString | undefined;
const deviceId = parseInt(params.deviceId ?? '', 10) as DeviceId;
if (!serviceId || deviceId.toString() !== params.deviceId) {
return [400, { error: 'Invalid request parameters' }];
}
@ -469,7 +469,7 @@ export class Connection extends Service {
});
this.router.get('/v2/keys/:serviceId(/\\*)', async (params) => {
const serviceId = params.serviceId as ServiceIdString;
const serviceId = params.serviceId as ServiceIdString | undefined;
if (!serviceId) {
return [400, { error: 'Invalid request parameters' }];
}
@ -479,7 +479,9 @@ export class Connection extends Service {
return [404, { error: 'Account not found' }];
}
const serviceIdKind = devices[0].getServiceIdKind(serviceId);
const device = devices[0];
assert(device != null, `Missing first device for serviceId ${serviceId}`);
const serviceIdKind = device.getServiceIdKind(serviceId);
return [200, await getDevicesKeysResult(serviceIdKind, devices)];
});
@ -733,7 +735,7 @@ export class Connection extends Service {
200,
await this.server.listBackupMedia(
BackupHeadersSchema.parse(headers),
{ cursor: cursor ? String(cursor) : undefined, limit },
{ cursor: cursor != null ? String(cursor) : undefined, limit },
),
];
},
@ -1009,7 +1011,7 @@ export class Connection extends Service {
}
if (path === '/v1/websocket/') {
return await this.handleNormal(this.request);
return this.handleNormal(this.request);
} else {
debug('websocket connection has unexpected URL %s', url);
}

View File

@ -10,6 +10,7 @@ import { WSRequest, WSResponse } from './service';
import { JsonValue, PartialDeep } from 'type-fest';
import URLPattern from 'url-pattern';
import { assertJsonValue } from '../../util';
import assert from 'assert';
const debug = createDebug('mock:ws:router');
@ -66,7 +67,7 @@ export class Router {
const headers: Record<string, string> = {};
for (const pair of request.headers ?? []) {
const [field, value = ''] = pair.split(/\s*:\s*/, 2);
assert(field != null, 'Missing field name for header');
headers[field.toLowerCase()] = value;
}
@ -79,6 +80,7 @@ export class Router {
request.path,
);
// eslint-disable-next-line @typescript-eslint/no-deprecated
const { pathname, query } = parseURL(request.path ?? '');
for (const { method, pattern, handler } of this.routes) {
@ -86,14 +88,14 @@ export class Router {
continue;
}
const params = pattern.match(pathname ?? '');
if (!params) {
const params: unknown = pattern.match(pathname ?? '');
if (params == null) {
continue;
}
const decodedParams: Record<string, string> = {};
for (const [key, value] of Object.entries(params)) {
decodedParams[String(key)] = decodeURIComponent(String(value));
decodedParams[key] = decodeURIComponent(String(value));
}
response = await handler(

View File

@ -21,12 +21,13 @@ interface RequestOptions {
}
export abstract class Service {
private readonly requests: Map<number, (res: WSResponse) => void> = new Map();
private readonly requests = new Map<number, (res: WSResponse) => void>();
private lastSentId = 0;
constructor(protected readonly ws: WebSocket) {
this.ws = ws;
// eslint-disable-next-line @typescript-eslint/no-misused-promises
this.ws.on('message', async (message) => {
try {
await this.onMessage(message);
@ -57,7 +58,7 @@ export abstract class Service {
this.ws.send(packet);
return await new Promise((resolve) => this.requests.set(id, resolve));
return new Promise((resolve) => this.requests.set(id, resolve));
}
private async onMessage(raw: WebSocket.Data): Promise<void> {
@ -79,7 +80,7 @@ export abstract class Service {
const id = parseInt(response.id.toString(), 10);
if (isNaN(id)) {
throw new Error(`Invalid response.id: ${response.id}`);
throw new Error(`Invalid response.id: ${response.id.toString()}`);
}
const resolve = this.requests.get(id);

View File

@ -52,20 +52,21 @@ export function parseAuthHeader(
}
const [basic, base64] = header.split(/\s+/g, 2);
if (basic.toLowerCase() !== 'basic') {
if (basic?.toLowerCase() !== 'basic') {
return { error: `Unsupported authorization type ${basic}` };
}
let username: string;
let password: string;
let decoded: string;
try {
const decoded = Buffer.from(base64, 'base64').toString();
[username, password] = decoded.split(':', 2);
assert(base64 != null, 'Missing base64 for basic authorization');
decoded = Buffer.from(base64, 'base64').toString();
} catch (error) {
assert(error instanceof Error);
return { error: error.message };
}
const [username, password = ''] = decoded.split(':', 2);
if (!username) {
return { error: 'Missing username' };
}
@ -86,7 +87,7 @@ export class PromiseQueue<T> {
this.defaultTimeout = config.timeout;
}
public get size() {
public get size(): number {
return this.entries.length;
}
@ -102,7 +103,7 @@ export class PromiseQueue<T> {
}
// Not waiting for `.shift()` - queue.
return await new Promise((resolve, reject) => {
return new Promise((resolve, reject) => {
let timer: NodeJS.Timeout | undefined;
const entry = {
@ -158,7 +159,7 @@ export class PromiseQueue<T> {
return entry.value;
}
return await new Promise((resolve, reject) => {
return new Promise((resolve, reject) => {
let timer: NodeJS.Timeout | undefined;
const resolveEntry = (value: T) => {
@ -235,7 +236,7 @@ export function fromURLSafeBase64(base64: string): Buffer {
}
export function assertJsonValue(root: unknown): asserts root is JsonValue {
const issues: string[] = [];
const issues: Array<string> = [];
function visit(node: unknown, path: ReadonlyArray<PropertyKey>) {
if (
@ -282,7 +283,7 @@ export function serviceIdKindFromQuery(
export async function getDevicesKeysResult(
serviceIdKind: ServiceIdKind,
devices: ReadonlyArray<Device>,
) {
): Promise<JsonValue> {
const [primary] = devices;
assert(primary !== undefined, 'Empty device list');

View File

@ -148,7 +148,9 @@ describe('util', () => {
invalid(() => 'hi', /value: \[Function \(anonymous\)\]/);
invalid(Buffer.from('hi'), /value: <Buffer 68 69>/);
invalid(new Uint8Array([68, 69]), /value: Uint8Array\(2\) \[ 68, 69 \]/);
// eslint-disable-next-line @typescript-eslint/no-extraneous-class
invalid(class Foo {}, /value: \[class Foo\]/);
// eslint-disable-next-line @typescript-eslint/no-extraneous-class
invalid(new (class Foo {})(), /value: Foo {}/);
invalid(
Long.fromNumber(42),

View File

@ -10,7 +10,8 @@
/* Strict Type-Checking Options */
"strict": true, /* Enable all strict type-checking options. */
"noUncheckedIndexedAccess": true,
/* Additional Checks */
"noUnusedLocals": true, /* Report errors on unused locals. */
"noImplicitOverride": true,