Make eslint/typescript more strict and fix getGroup type mismatch
Co-authored-by: Scott Nonnenberg <scott@signal.org>
This commit is contained in:
parent
60b906a5b0
commit
870624186f
@ -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/*
|
||||
@ -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
|
||||
3
.github/workflows/publish.yaml
vendored
3
.github/workflows/publish.yaml
vendored
@ -27,6 +27,9 @@ jobs:
|
||||
- name: Install node_modules
|
||||
run: npm ci
|
||||
|
||||
- name: Lint
|
||||
run: npm run lint
|
||||
|
||||
- name: Test
|
||||
run: npm test
|
||||
|
||||
|
||||
3
.github/workflows/test.yaml
vendored
3
.github/workflows/test.yaml
vendored
@ -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
60
eslint.config.mjs
Normal 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
1117
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
12
package.json
12
package.json
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@ -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();
|
||||
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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');
|
||||
}
|
||||
|
||||
@ -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}`);
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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');
|
||||
}
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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);
|
||||
|
||||
21
src/util.ts
21
src/util.ts
@ -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');
|
||||
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user