Enable TS noImplicitAny in web (#31636)

GitOrigin-RevId: 18881694770f2476c475f8fef4c6a2678a2a12fe
This commit is contained in:
Anna Claire Fields
2026-03-26 13:53:59 +01:00
committed by Copybot
parent b3489a6792
commit 6113c6c291
58 changed files with 1423 additions and 281 deletions

View File

@@ -8,13 +8,20 @@ const READ_PREFERENCE_SECONDARY =
: ReadPreference.secondaryPreferred.mode
const ONE_MONTH_IN_MS = 1000 * 60 * 60 * 24 * 31
/** @type {ObjectId | null} */
let ID_EDGE_PAST
const ID_EDGE_FUTURE = objectIdFromMs(Date.now() + 1000)
/** @type {boolean} */
let BATCH_DESCENDING
/** @type {number} */
let BATCH_SIZE
/** @type {boolean} */
let VERBOSE_LOGGING
/** @type {ObjectId} */
let BATCH_RANGE_START
/** @type {ObjectId} */
let BATCH_RANGE_END
/** @type {number} */
let BATCH_MAX_TIME_SPAN_IN_MS
let BATCHED_UPDATE_RUNNING = false
@@ -55,7 +62,7 @@ function refreshGlobalOptionsForBatchedUpdate(options = {}) {
if (BATCH_DESCENDING) {
BATCH_RANGE_START = ID_EDGE_FUTURE
} else {
BATCH_RANGE_START = ID_EDGE_PAST
BATCH_RANGE_START = /** @type {ObjectId} */ (ID_EDGE_PAST)
}
}
BATCH_MAX_TIME_SPAN_IN_MS = parseInt(
@@ -66,7 +73,7 @@ function refreshGlobalOptionsForBatchedUpdate(options = {}) {
BATCH_RANGE_END = objectIdFromInput(options.BATCH_RANGE_END)
} else {
if (BATCH_DESCENDING) {
BATCH_RANGE_END = ID_EDGE_PAST
BATCH_RANGE_END = /** @type {ObjectId} */ (ID_EDGE_PAST)
} else {
BATCH_RANGE_END = ID_EDGE_FUTURE
}

View File

@@ -7,3 +7,4 @@ mongo-utils
--node-version=24.13.0
--pipeline-owner=32
--public-repo=False
--tsconfig-no-implicit-any=True

View File

@@ -1,8 +1,8 @@
// @ts-check
/**
* @import { MongoClient } from 'mongodb'
* @import { MongoClient as LegacyMongoClient } from 'mongodb-legacy'
* @typedef {import('mongodb').MongoClient} MongoClient
* @typedef {import('mongodb-legacy').MongoClient} LegacyMongoClient
*/
/**

View File

@@ -1,4 +1,7 @@
{
"extends": "../../tsconfig.backend.json",
"compilerOptions": {
"noImplicitAny": true
},
"include": ["**/*.js", "**/*.cjs", "**/*.ts"]
}

View File

@@ -7,3 +7,4 @@ object-persistor
--node-version=24.13.0
--pipeline-owner=32
--public-repo=False
--tsconfig-no-implicit-any=True

View File

@@ -21,7 +21,7 @@ const hkdf = promisify(Crypto.hkdf)
const AES256_KEY_LENGTH = 32
/**
* @typedef {Object} Settings
* @typedef {Object} EncryptionSettings
* @property {boolean} automaticallyRotateDEKEncryption
* @property {string} dataEncryptionKeyBucketName
* @property {boolean} ignoreErrorsFromDEKReEncryption
@@ -29,6 +29,26 @@ const AES256_KEY_LENGTH = 32
* @property {() => Promise<Array<RootKeyEncryptionKey>>} getRootKeyEncryptionKeys
*/
/**
* @typedef {Object} S3PersistorSettings
* @property {Object<string, import('@aws-sdk/client-s3').StorageClass>} [storageClass]
* @property {string} [key]
* @property {string} [secret]
* @property {string} [region]
* @property {string} [endpoint]
* @property {boolean} [pathStyle]
* @property {number} [maxRetries]
* @property {Object} [httpOptions]
* @property {string} [ca]
* @property {number} [signedUrlExpiryInMs]
* @property {number} [partSize]
* @property {Object<string, {auth_key: string, auth_secret: string}>} [bucketCreds]
*/
/**
* @typedef {S3PersistorSettings & EncryptionSettings} Settings
*/
/**
* @typedef {import('./types').ListDirectoryResult} ListDirectoryResult
*/
@@ -105,10 +125,6 @@ class PerProjectEncryptedS3Persistor extends S3Persistor {
})
}
async ensureKeyEncryptionKeysLoaded() {
await this.#availableKeyEncryptionKeysPromise
}
/**
* @param {string} bucketName
* @param {string} path
@@ -289,6 +305,13 @@ class PerProjectEncryptedS3Persistor extends S3Persistor {
}
}
/**
* @param {string} bucketName
* @param {string} path
* @param {NodeJS.ReadableStream} sourceStream
* @param {import('./S3Persistor.js').StreamOptions} opts
* @return {Promise<void>}
*/
async sendStream(bucketName, path, sourceStream, opts = {}) {
const ssecOptions =
opts.ssecOptions ||
@@ -299,6 +322,13 @@ class PerProjectEncryptedS3Persistor extends S3Persistor {
})
}
/**
* @param {string} bucketName
* @param {string} path
* @param {Object} opts
* @param {SSECOptions} [opts.ssecOptions]
* @return {Promise<NodeJS.ReadableStream>}
*/
async getObjectStream(bucketName, path, opts = {}) {
const ssecOptions =
opts.ssecOptions ||
@@ -309,6 +339,13 @@ class PerProjectEncryptedS3Persistor extends S3Persistor {
})
}
/**
* @param {string} bucketName
* @param {string} path
* @param {Object} opts
* @param {SSECOptions} [opts.ssecOptions]
* @return {Promise<number>}
*/
async getObjectSize(bucketName, path, opts = {}) {
const ssecOptions =
opts.ssecOptions ||
@@ -316,6 +353,13 @@ class PerProjectEncryptedS3Persistor extends S3Persistor {
return await super.getObjectSize(bucketName, path, { ...opts, ssecOptions })
}
/**
* @param {string} bucketName
* @param {string} path
* @param {Object} opts
* @param {SSECOptions} [opts.ssecOptions]
* @return {Promise<string | undefined>}
*/
async getObjectStorageClass(bucketName, path, opts = {}) {
const ssecOptions =
opts.ssecOptions ||
@@ -326,11 +370,23 @@ class PerProjectEncryptedS3Persistor extends S3Persistor {
})
}
/**
* @param {string} bucketName
* @param {string} path
* @param {string} [continuationToken]
* @return {Promise<number>}
*/
async directorySize(bucketName, path, continuationToken) {
// Note: Listing a bucket does not require SSE-C credentials.
return await super.directorySize(bucketName, path, continuationToken)
}
/**
* @param {string} bucketName
* @param {string} path
* @param {string} [continuationToken]
* @return {Promise<void>}
*/
async deleteDirectory(bucketName, path, continuationToken) {
// Let [Settings.pathToProjectFolder] validate the project path before deleting things.
const { projectFolder, dekPath } = this.#buildProjectPaths(bucketName, path)
@@ -344,12 +400,27 @@ class PerProjectEncryptedS3Persistor extends S3Persistor {
}
}
/**
* @param {string} bucketName
* @param {string} path
* @param {Object} opts
* @return {Promise<string>}
*/
async getObjectMd5Hash(bucketName, path, opts = {}) {
// The ETag in object metadata is not the MD5 content hash, skip the HEAD request.
opts = { ...opts, etagIsNotMD5: true }
return await super.getObjectMd5Hash(bucketName, path, opts)
}
/**
* @param {string} bucketName
* @param {string} sourcePath
* @param {string} destinationPath
* @param {Object} opts
* @param {SSECOptions} [opts.ssecOptions]
* @param {SSECOptions} [opts.ssecSrcOptions]
* @return {Promise<void>}
*/
async copyObject(bucketName, sourcePath, destinationPath, opts = {}) {
const ssecOptions =
opts.ssecOptions ||

View File

@@ -77,14 +77,53 @@ class SSECOptions {
}
}
/**
* @typedef {import('@aws-sdk/client-s3').StorageClass} StorageClass
* @typedef {import('@aws-sdk/client-s3').PutObjectCommandInput} PutObjectCommandInput
* @typedef {import('@aws-sdk/client-s3').HeadObjectCommandOutput} HeadObjectCommandOutput
* @typedef {import('@aws-sdk/client-s3').GetObjectCommandOutput} GetObjectCommandOutput
* @typedef {import('@aws-sdk/client-s3').S3ClientConfig} S3ClientConfig
*/
/**
* @typedef {Object} StreamOptions
* @property {string} [contentType]
* @property {string} [contentEncoding]
* @property {number} [contentLength]
* @property {'*'} [ifNoneMatch]
* @property {SSECOptions} [ssecOptions]
*/
/**
* @typedef {Object} S3PersistorSettings
* @property {Object<string, StorageClass>} [storageClass]
* @property {string} [key]
* @property {string} [secret]
* @property {string} [region]
* @property {string} [endpoint]
* @property {boolean} [pathStyle]
* @property {number} [maxRetries]
* @property {Object} [httpOptions]
* @property {string} [ca]
* @property {number} [signedUrlExpiryInMs]
* @property {number} [partSize]
* @property {Object<string, {auth_key: string, auth_secret: string}>} [bucketCreds]
*/
class S3Persistor extends AbstractPersistor {
/** @type {Map<string, S3Client>} */
#clients = new Map()
/** @type {S3PersistorSettings} */
settings
/**
* @param {S3PersistorSettings} [settings]
*/
constructor(settings = {}) {
super()
settings.storageClass = settings.storageClass || {}
settings.storageClass = /** @type {Object<string, StorageClass>} */ (
settings.storageClass || {}
)
this.settings = settings
}
@@ -102,12 +141,7 @@ class S3Persistor extends AbstractPersistor {
* @param {string} bucketName
* @param {string} key
* @param {NodeJS.ReadableStream} readStream
* @param {Object} opts
* @param {string} [opts.contentType]
* @param {string} [opts.contentEncoding]
* @param {number} [opts.contentLength]
* @param {'*'} [opts.ifNoneMatch]
* @param {SSECOptions} [opts.ssecOptions]
* @param {StreamOptions} opts
* @return {Promise<void>}
*/
async sendStream(bucketName, key, readStream, opts = {}) {
@@ -128,8 +162,11 @@ class S3Persistor extends AbstractPersistor {
Body: observer,
}
if (this.settings.storageClass[bucketName]) {
uploadOptions.StorageClass = this.settings.storageClass[bucketName]
const storageClass = /** @type {Object<string, StorageClass>} */ (
this.settings.storageClass
)
if (storageClass[bucketName]) {
uploadOptions.StorageClass = storageClass[bucketName]
}
if ('sourceMd5' in opts) {
@@ -239,7 +276,9 @@ class S3Persistor extends AbstractPersistor {
* @return {Promise<string>}
*/
async getRedirectUrl(bucketName, key) {
const expiresSeconds = Math.round(this.settings.signedUrlExpiryInMs / 1000)
const expiresSeconds = Math.round(
/** @type {number} */ (this.settings.signedUrlExpiryInMs) / 1000
)
try {
return await getSignedUrl(
this._getClientForBucket(bucketName),
@@ -311,6 +350,7 @@ class S3Persistor extends AbstractPersistor {
*/
async #listDirectory(bucketName, key, continuationToken) {
let response
/** @type {{ Bucket: string, Prefix: string, ContinuationToken?: string }} */
const options = { Bucket: bucketName, Prefix: key }
if (continuationToken) {
options.ContinuationToken = continuationToken
@@ -501,6 +541,7 @@ class S3Persistor extends AbstractPersistor {
*/
async directorySize(bucketName, key, continuationToken) {
try {
/** @type {{ Bucket: string, Prefix: string, ContinuationToken?: string }} */
const options = {
Bucket: bucketName,
Prefix: key,
@@ -621,7 +662,7 @@ class S3Persistor extends AbstractPersistor {
}
/**
* @param {Object} bucketCredentials
* @param {{auth_key: string, auth_secret: string} | undefined} bucketCredentials
* @param {import('@aws-sdk/client-s3').S3ClientConfig} clientOptions
* @return {import('@aws-sdk/client-s3').S3ClientConfig}
* @private
@@ -637,7 +678,7 @@ class S3Persistor extends AbstractPersistor {
} else if (this.settings.key) {
options.credentials = {
accessKeyId: this.settings.key,
secretAccessKey: this.settings.secret,
secretAccessKey: /** @type {string} */ (this.settings.secret),
}
} else {
// Use the default credentials provider (process.env -> SSP -> ini -> IAM)
@@ -709,6 +750,9 @@ class S3Persistor extends AbstractPersistor {
}
// test-only
/**
* @param {string} bucketName
*/
_createBucket(bucketName) {
return this._getClientForBucket(bucketName).send(
new CreateBucketCommand({ Bucket: bucketName })
@@ -716,6 +760,10 @@ class S3Persistor extends AbstractPersistor {
}
// test-only
/**
* @param {string} bucketName
* @param {import('@aws-sdk/client-s3').PutObjectCommandInput} uploadOptions
*/
_upload(bucketName, uploadOptions) {
return this._getClientForBucket(bucketName).send(
new PutObjectCommand(uploadOptions)

View File

@@ -1,4 +1,7 @@
{
"extends": "../../tsconfig.backend.json",
"compilerOptions": {
"noImplicitAny": true
},
"include": ["**/*.js", "**/*.cjs", "**/*.ts"]
}

View File

@@ -4,6 +4,7 @@ const crypto = require('node:crypto')
const os = require('node:os')
const { promisify } = require('node:util')
// @ts-ignore - ioredis types may not be available in all contexts
const Redis = require('ioredis')
const {
@@ -20,6 +21,9 @@ const PID = process.pid
const RND = crypto.randomBytes(4).toString('hex')
let COUNT = 0
/**
* @param {any} opts
*/
function createClient(opts) {
const standardOpts = Object.assign({}, opts)
delete standardOpts.key_schema
@@ -42,6 +46,7 @@ function createClient(opts) {
client = new Redis(standardOpts)
}
monkeyPatchIoRedisExec(client)
/** @param {any} callback */
client.healthCheck = callback => {
if (callback) {
// callback based invocation
@@ -54,6 +59,9 @@ function createClient(opts) {
return client
}
/**
* @param {any} client
*/
async function healthCheck(client) {
// check the redis connection by storing and retrieving a unique key/value pair
const uniqueToken = `host=${HOST}:pid=${PID}:random=${RND}:time=${Date.now()}:count=${COUNT++}`
@@ -71,6 +79,11 @@ async function healthCheck(client) {
})
}
/**
* @param {any} client
* @param {any} uniqueToken
* @param {any} context
*/
async function runCheck(client, uniqueToken, context) {
const healthCheckKey = `_redis-wrapper:healthCheckKey:{${uniqueToken}}`
const healthCheckValue = `_redis-wrapper:healthCheckValue:{${uniqueToken}}`
@@ -79,9 +92,11 @@ async function runCheck(client, uniqueToken, context) {
context.stage = 'write'
const writeAck = await client
.set(healthCheckKey, healthCheckValue, 'EX', 60)
.catch(err => {
throw new RedisHealthCheckWriteError('write errored', context, err)
})
.catch(
/** @param {any} err */ err => {
throw new RedisHealthCheckWriteError('write errored', context, err)
}
)
if (writeAck !== 'OK') {
context.writeAck = writeAck
throw new RedisHealthCheckWriteError('write failed', context)
@@ -94,9 +109,15 @@ async function runCheck(client, uniqueToken, context) {
.get(healthCheckKey)
.del(healthCheckKey)
.exec()
.catch(err => {
throw new RedisHealthCheckVerifyError('read/delete errored', context, err)
})
.catch(
/** @param {any} err */ err => {
throw new RedisHealthCheckVerifyError(
'read/delete errored',
context,
err
)
}
)
if (roundTrippedHealthCheckValue !== healthCheckValue) {
context.roundTrippedHealthCheckValue = roundTrippedHealthCheckValue
throw new RedisHealthCheckVerifyError('read failed', context)
@@ -107,6 +128,10 @@ async function runCheck(client, uniqueToken, context) {
}
}
/**
* @param {any} result
* @param {any} callback
*/
function unwrapMultiResult(result, callback) {
// ioredis exec returns a results like:
// [ [null, 42], [null, "foo"] ]
@@ -130,19 +155,30 @@ function unwrapMultiResult(result, callback) {
}
const unwrapMultiResultPromisified = promisify(unwrapMultiResult)
/**
* @param {any} client
*/
function monkeyPatchIoRedisExec(client) {
const _multi = client.multi
client.multi = function () {
const multi = _multi.apply(client, arguments)
const _exec = multi.exec
/** @param {any} callback */
multi.exec = callback => {
if (callback) {
// callback based invocation
_exec.call(multi, (error, result) => {
// The command can fail all-together due to syntax errors
if (error) return callback(error)
unwrapMultiResult(result, callback)
})
_exec.call(
multi,
/**
* @param {any} error
* @param {any} result
*/
(error, result) => {
// The command can fail all-together due to syntax errors
if (error) return callback(error)
unwrapMultiResult(result, callback)
}
)
} else {
// Promise based invocation
return _exec.call(multi).then(unwrapMultiResultPromisified)
@@ -152,7 +188,11 @@ function monkeyPatchIoRedisExec(client) {
}
}
/**
* @param {{runner: any, timeout: any, context: any}} param0
*/
async function runWithTimeout({ runner, timeout, context }) {
/** @type {any} */
let healthCheckDeadline
await Promise.race([
new Promise((resolve, reject) => {

View File

@@ -45,7 +45,7 @@ if (!settingsExist) {
console.warn("No settings or defaults found. I'm flying blind.")
}
module.exports = settings
module.exports = /** @type {any} */ (settings)
function pathIfExists(path) {
if (path && fs.existsSync(path)) {

110
package-lock.json generated
View File

@@ -16409,6 +16409,13 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/async": {
"version": "3.2.25",
"resolved": "https://registry.npmjs.org/@types/async/-/async-3.2.25.tgz",
"integrity": "sha512-O6Th/DI18XjrL9TX8LO9F/g26qAz5vynmQqlXt/qLGrskvzCKXKc5/tATz3G2N6lM8eOf3M8/StB14FncAmocg==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/aws-lambda": {
"version": "8.10.160",
"resolved": "https://registry.npmjs.org/@types/aws-lambda/-/aws-lambda-8.10.160.tgz",
@@ -17176,6 +17183,95 @@
"integrity": "sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==",
"dev": true
},
"node_modules/@types/sanitize-html": {
"version": "2.16.0",
"resolved": "https://registry.npmjs.org/@types/sanitize-html/-/sanitize-html-2.16.0.tgz",
"integrity": "sha512-l6rX1MUXje5ztPT0cAFtUayXF06DqPhRyfVXareEN5gGCFaP/iwsxIyKODr9XDhfxPpN6vXUFNfo5kZMXCxBtw==",
"dev": true,
"license": "MIT",
"dependencies": {
"htmlparser2": "^8.0.0"
}
},
"node_modules/@types/sanitize-html/node_modules/dom-serializer": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
"integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
"dev": true,
"license": "MIT",
"dependencies": {
"domelementtype": "^2.3.0",
"domhandler": "^5.0.2",
"entities": "^4.2.0"
},
"funding": {
"url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
}
},
"node_modules/@types/sanitize-html/node_modules/domhandler": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
"integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"domelementtype": "^2.3.0"
},
"engines": {
"node": ">= 4"
},
"funding": {
"url": "https://github.com/fb55/domhandler?sponsor=1"
}
},
"node_modules/@types/sanitize-html/node_modules/domutils": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
"integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"dom-serializer": "^2.0.0",
"domelementtype": "^2.3.0",
"domhandler": "^5.0.3"
},
"funding": {
"url": "https://github.com/fb55/domutils?sponsor=1"
}
},
"node_modules/@types/sanitize-html/node_modules/entities": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
"dev": true,
"license": "BSD-2-Clause",
"engines": {
"node": ">=0.12"
},
"funding": {
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/@types/sanitize-html/node_modules/htmlparser2": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz",
"integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==",
"dev": true,
"funding": [
"https://github.com/fb55/htmlparser2?sponsor=1",
{
"type": "github",
"url": "https://github.com/sponsors/fb55"
}
],
"license": "MIT",
"dependencies": {
"domelementtype": "^2.3.0",
"domhandler": "^5.0.3",
"domutils": "^3.0.1",
"entities": "^4.4.0"
}
},
"node_modules/@types/semver": {
"version": "7.5.0",
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.0.tgz",
@@ -17305,6 +17401,16 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/utf-8-validate": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/@types/utf-8-validate/-/utf-8-validate-5.0.2.tgz",
"integrity": "sha512-ta7cOkEiNr0RGKARljNBaI7E1GBIr3VwS9RrSoQRmbdv1RVq7Q6VhjSGmQHYNt3nHn051qZBKKrpnw7cnEMDuQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/uuid": {
"version": "9.0.8",
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz",
@@ -53195,6 +53301,7 @@
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^14.4.3",
"@types/algoliasearch": "^3.34.11",
"@types/async": "^3.2.25",
"@types/bootstrap": "^5.2.10",
"@types/chai": "^4.3.0",
"@types/dateformat": "^5.0.3",
@@ -53202,6 +53309,7 @@
"@types/dom-speech-recognition": "^0.0.7",
"@types/events": "^3.0.3",
"@types/express": "^4.17.23",
"@types/minimist": "^1.2.5",
"@types/mocha": "^9.1.0",
"@types/mocha-each": "^2.0.4",
"@types/node": "^24.5.2",
@@ -53212,7 +53320,9 @@
"@types/react-linkify": "^1.0.4",
"@types/react-syntax-highlighter": "^15.5.11",
"@types/recurly__recurly-js": "^4.38.0",
"@types/sanitize-html": "^2.14.0",
"@types/sinon-chai": "^3.2.12",
"@types/utf-8-validate": "^5.0.2",
"@types/uuid": "^9.0.8",
"@uppy/core": "^3.8.0",
"@uppy/dashboard": "^3.7.1",

View File

@@ -50,7 +50,9 @@ async function registerEmailUpdate(userId, email, eventData = {}) {
*/
async function makeEmailChangeEvent(userId, email, eventData) {
const userEmails = await UserGetter.promises.getUserFullEmails(userId)
const emailData = userEmails.find(userEmail => userEmail.email === email)
const emailData = userEmails.find(
/** @param {any} userEmail */ userEmail => userEmail.email === email
)
const filledEventData = fillMissingEventData(eventData, emailData)

View File

@@ -25,10 +25,16 @@ const {
* @returns {() => (req: Request, res: Response, next: NextFunction) => void} The middleware function that adds the `assertPermission` function to the request object.
*/
function useCapabilities() {
/**
* @param {any} req
* @param {any} res
* @param {any} next
*/
const middleware = async function (req, res, next) {
// attach the user's capabilities to the request object
req.capabilitySet = new Set()
// provide a function to assert that a capability is present
/** @param {any} capability */
req.assertPermission = capability => {
if (!req.capabilitySet.has(capability)) {
throw new ForbiddenError(
@@ -52,7 +58,9 @@ function useCapabilities() {
if (results.length > 0) {
// get the combined group policy applying to the user
const groupPolicies = results.map(result => result.groupPolicy)
const groupPolicies = results.map(
/** @param {any} result */ result => result.groupPolicy
)
const combinedGroupPolicy = combineGroupPolicies(groupPolicies)
// attach the new capabilities to the request object
for (const cap of getUserCapabilities(combinedGroupPolicy)) {
@@ -68,7 +76,7 @@ function useCapabilities() {
}
}
next()
} catch (error) {
} catch (/** @type {any} */ error) {
if (error instanceof UserNotFoundError) {
// the user is logged in but doesn't exist in the database
// this can happen if the user has just deleted their account
@@ -89,7 +97,10 @@ function useCapabilities() {
function requirePermission(...requiredCapabilities) {
if (
requiredCapabilities.length === 0 ||
requiredCapabilities.some(capability => typeof capability !== 'string')
requiredCapabilities.some(
/** @param {any} capability */ capability =>
typeof capability !== 'string'
)
) {
throw new Error('invalid required capabilities')
}
@@ -102,16 +113,14 @@ function requirePermission(...requiredCapabilities) {
if (!Features.hasFeature('saas')) {
return next()
}
if (!req.user && !req.oauth_user) {
const user = req.user || req.oauth_user
if (!user) {
return next(new Error('no user'))
}
try {
await assertUserPermissions(
req.user || req.oauth_user,
requiredCapabilities
)
await assertUserPermissions(user, requiredCapabilities)
next()
} catch (error) {
} catch (/** @type {any} */ error) {
next(error)
}
}

View File

@@ -5,24 +5,44 @@ import { fetchJson, fetchNothing } from '@overleaf/fetch-utils'
import settings from '@overleaf/settings'
import { callbackify } from 'node:util'
/**
* @param {any} projectId
* @param {any} threadId
*/
async function getThread(projectId, threadId) {
return await fetchJson(chatApiUrl(`/project/${projectId}/thread/${threadId}`))
}
/**
* @param {any} projectId
* @param {any} threadId
* @param {any} messageId
*/
async function getThreadMessage(projectId, threadId, messageId) {
return await fetchJson(
chatApiUrl(`/project/${projectId}/thread/${threadId}/messages/${messageId}`)
)
}
/**
* @param {any} projectId
*/
async function getThreads(projectId) {
return await fetchJson(chatApiUrl(`/project/${projectId}/threads`))
}
/**
* @param {any} projectId
*/
async function destroyProject(projectId) {
await fetchNothing(chatApiUrl(`/project/${projectId}`), { method: 'DELETE' })
}
/**
* @param {any} projectId
* @param {any} userId
* @param {any} content
*/
async function sendGlobalMessage(projectId, userId, content) {
const message = await fetchJson(
chatApiUrl(`/project/${projectId}/messages`),
@@ -34,6 +54,11 @@ async function sendGlobalMessage(projectId, userId, content) {
return message
}
/**
* @param {any} projectId
* @param {any} limit
* @param {any} before
*/
async function getGlobalMessages(projectId, limit, before) {
const url = chatApiUrl(`/project/${projectId}/messages`)
if (limit != null) {
@@ -46,12 +71,22 @@ async function getGlobalMessages(projectId, limit, before) {
return await fetchJson(url)
}
/**
* @param {any} projectId
* @param {any} messageId
*/
async function getGlobalMessage(projectId, messageId) {
return await fetchJson(
chatApiUrl(`/project/${projectId}/messages/${messageId}`)
)
}
/**
* @param {any} projectId
* @param {any} threadId
* @param {any} userId
* @param {any} content
*/
async function sendComment(projectId, threadId, userId, content) {
const comment = await fetchJson(
chatApiUrl(`/project/${projectId}/thread/${threadId}/messages`),
@@ -63,6 +98,11 @@ async function sendComment(projectId, threadId, userId, content) {
return comment
}
/**
* @param {any} projectId
* @param {any} threadId
* @param {any} userId
*/
async function resolveThread(projectId, threadId, userId) {
await fetchNothing(
chatApiUrl(`/project/${projectId}/thread/${threadId}/resolve`),
@@ -73,6 +113,10 @@ async function resolveThread(projectId, threadId, userId) {
)
}
/**
* @param {any} projectId
* @param {any} threadId
*/
async function reopenThread(projectId, threadId) {
await fetchNothing(
chatApiUrl(`/project/${projectId}/thread/${threadId}/reopen`),
@@ -80,12 +124,23 @@ async function reopenThread(projectId, threadId) {
)
}
/**
* @param {any} projectId
* @param {any} threadId
*/
async function deleteThread(projectId, threadId) {
await fetchNothing(chatApiUrl(`/project/${projectId}/thread/${threadId}`), {
method: 'DELETE',
})
}
/**
* @param {any} projectId
* @param {any} threadId
* @param {any} messageId
* @param {any} userId
* @param {any} content
*/
async function editMessage(projectId, threadId, messageId, userId, content) {
await fetchNothing(
chatApiUrl(
@@ -98,6 +153,12 @@ async function editMessage(projectId, threadId, messageId, userId, content) {
)
}
/**
* @param {any} projectId
* @param {any} messageId
* @param {any} userId
* @param {any} content
*/
async function editGlobalMessage(projectId, messageId, userId, content) {
await fetchNothing(
chatApiUrl(`/project/${projectId}/messages/${messageId}/edit`),
@@ -108,6 +169,11 @@ async function editGlobalMessage(projectId, messageId, userId, content) {
)
}
/**
* @param {any} projectId
* @param {any} threadId
* @param {any} messageId
*/
async function deleteMessage(projectId, threadId, messageId) {
await fetchNothing(
chatApiUrl(
@@ -117,6 +183,12 @@ async function deleteMessage(projectId, threadId, messageId) {
)
}
/**
* @param {any} projectId
* @param {any} threadId
* @param {any} userId
* @param {any} messageId
*/
async function deleteUserMessage(projectId, threadId, userId, messageId) {
await fetchNothing(
chatApiUrl(
@@ -126,6 +198,10 @@ async function deleteUserMessage(projectId, threadId, userId, messageId) {
)
}
/**
* @param {any} projectId
* @param {any} messageId
*/
async function deleteGlobalMessage(projectId, messageId) {
await fetchNothing(
chatApiUrl(`/project/${projectId}/messages/${messageId}`),
@@ -133,6 +209,9 @@ async function deleteGlobalMessage(projectId, messageId) {
)
}
/**
* @param {any} projectId
*/
async function getResolvedThreadIds(projectId) {
const body = await fetchJson(
chatApiUrl(`/project/${projectId}/resolved-thread-ids`)
@@ -140,6 +219,10 @@ async function getResolvedThreadIds(projectId) {
return body.resolvedThreadIds
}
/**
* @param {any} projectId
* @param {any} threads
*/
async function duplicateCommentThreads(projectId, threads) {
return await fetchJson(
chatApiUrl(`/project/${projectId}/duplicate-comment-threads`),
@@ -152,6 +235,10 @@ async function duplicateCommentThreads(projectId, threads) {
)
}
/**
* @param {any} projectId
* @param {any} threads
*/
async function generateThreadData(projectId, threads) {
return await fetchJson(
chatApiUrl(`/project/${projectId}/generate-thread-data`),
@@ -162,6 +249,9 @@ async function generateThreadData(projectId, threads) {
)
}
/**
* @param {any} path
*/
function chatApiUrl(path) {
return new URL(path, settings.apis.chat.internal_url)
}

View File

@@ -212,6 +212,9 @@ class ProjectAccess {
}
}
/**
* @param {any} projectId
*/
async function getProjectAccess(projectId) {
const project = await ProjectGetter.promises.getProject(projectId, {
owner_ref: 1,
@@ -230,18 +233,33 @@ async function getProjectAccess(projectId) {
return new ProjectAccess(project)
}
/**
* @param {any} projectId
*/
async function getMemberIdsWithPrivilegeLevels(projectId) {
return (await getProjectAccess(projectId)).allMembers()
}
/**
* @param {any} projectId
*/
async function getMemberIds(projectId) {
return (await getProjectAccess(projectId)).memberIds()
}
/**
* @param {any} projectId
*/
async function getInvitedMemberIds(projectId) {
return (await getProjectAccess(projectId)).invitedMemberIds()
}
/**
* @param {any} ownerId
* @param {any} collaboratorIds
* @param {any} readOnlyIds
* @param {any} reviewerIds
*/
async function getInvitedMembersWithPrivilegeLevelsFromFields(
ownerId,
collaboratorIds,
@@ -262,6 +280,10 @@ async function getInvitedMembersWithPrivilegeLevelsFromFields(
return _loadMembers(members)
}
/**
* @param {any} userId
* @param {any} projectId
*/
async function getMemberIdPrivilegeLevel(userId, projectId) {
// In future if the schema changes and getting all member ids is more expensive (multiple documents)
// then optimise this.
@@ -271,14 +293,24 @@ async function getMemberIdPrivilegeLevel(userId, projectId) {
return (await getProjectAccess(projectId)).privilegeLevelForUser(userId)
}
/**
* @param {any} projectId
*/
async function getInvitedEditCollaboratorCount(projectId) {
return (await getProjectAccess(projectId)).countInvitedEditCollaborators()
}
/**
* @param {any} projectId
*/
async function getInvitedPendingEditorCount(projectId) {
return (await getProjectAccess(projectId)).countInvitedPendingEditors()
}
/**
* @param {any} userId
* @param {any} projectId
*/
async function isUserInvitedMemberOfProject(userId, projectId) {
if (!userId) {
return false
@@ -286,6 +318,10 @@ async function isUserInvitedMemberOfProject(userId, projectId) {
return (await getProjectAccess(projectId)).isUserInvitedMember(userId)
}
/**
* @param {any} userId
* @param {any} projectId
*/
async function isUserInvitedReadWriteMemberOfProject(userId, projectId) {
if (!userId) {
return false
@@ -295,6 +331,10 @@ async function isUserInvitedReadWriteMemberOfProject(userId, projectId) {
)
}
/**
* @param {any} userId
* @param {any} projectId
*/
async function getPublicShareTokens(userId, projectId) {
const memberInfo = await Project.findOne(
{
@@ -335,6 +375,10 @@ async function getPublicShareTokens(userId, projectId) {
// This function returns all the projects that a user currently has access to,
// excluding projects where the user is listed in the token access fields when
// token access has been disabled.
/**
* @param {any} userId
* @param {any} fields
*/
async function getProjectsUserIsMemberOf(userId, fields) {
// @ts-ignore
const limit = pLimit(2)
@@ -368,6 +412,10 @@ async function getProjectsUserIsMemberOf(userId, fields) {
// This function returns all the projects that a user is a member of, regardless of
// the current state of the project, so it includes those projects where token access
// has been disabled.
/**
* @param {any} userId
* @param {any} fields
*/
async function dangerouslyGetAllProjectsUserIsMemberOf(userId, fields) {
const readAndWrite = await Project.find(
{ collaberator_refs: userId },
@@ -385,6 +433,9 @@ async function dangerouslyGetAllProjectsUserIsMemberOf(userId, fields) {
return { readAndWrite, readOnly, tokenReadAndWrite, tokenReadOnly }
}
/**
* @param {any} projectId
*/
async function getAllInvitedMembers(projectId) {
try {
const projectAccess = await getProjectAccess(projectId)
@@ -395,6 +446,10 @@ async function getAllInvitedMembers(projectId) {
}
}
/**
* @param {any} userId
* @param {any} projectId
*/
async function userIsTokenMember(userId, projectId) {
userId = new ObjectId(userId.toString())
projectId = new ObjectId(projectId.toString())
@@ -413,6 +468,10 @@ async function userIsTokenMember(userId, projectId) {
return project != null
}
/**
* @param {any} userId
* @param {any} projectId
*/
async function userIsReadWriteTokenMember(userId, projectId) {
userId = new ObjectId(userId.toString())
projectId = new ObjectId(projectId.toString())
@@ -476,15 +535,20 @@ function _getMemberIdsWithPrivilegeLevelsFromFields(
}
for (const memberId of readOnlyIds || []) {
/** @type {ProjectMember} */
const record = {
id: memberId.toString(),
privilegeLevel: PrivilegeLevels.READ_ONLY,
source: Sources.INVITE,
}
if (pendingEditorIds?.some(pe => memberId.equals(pe))) {
if (
pendingEditorIds?.some(/** @param {any} pe */ pe => memberId.equals(pe))
) {
record.pendingEditor = true
} else if (pendingReviewerIds?.some(pr => memberId.equals(pr))) {
} else if (
pendingReviewerIds?.some(/** @param {any} pr */ pr => memberId.equals(pr))
) {
record.pendingReviewer = true
}
members.push(record)
@@ -533,6 +597,7 @@ async function _loadMembers(members) {
.map(member => {
const user = users.get(member.id)
if (!user) return null
/** @type {any} */
const record = {
user,
privilegeLevel: member.privilegeLevel,

View File

@@ -238,7 +238,13 @@ async function isDocDeleted(projectId, docId) {
* @param ranges
* @return {Promise<{modified: *, rev: *}>}
*/
async function updateDoc(projectId, docId, lines, version, ranges) {
async function updateDoc(
projectId,
docId,
lines,
version,
/** @type {any} */ ranges
) {
const url = new URL(settings.apis.docstore.url)
url.pathname = path.posix.join('project', projectId, 'doc', docId)
try {

View File

@@ -9,6 +9,10 @@ import HistoryManager from '../History/HistoryManager.mjs'
import Errors from '../Errors/Errors.js'
import { preparePlainTextResponse } from '../../infrastructure/Response.mjs'
/**
* @param {any} req
* @param {any} res
*/
async function getFile(req, res) {
const projectId = req.params.Project_id
const fileId = req.params.File_id
@@ -101,6 +105,10 @@ async function getFile(req, res) {
}
}
/**
* @param {any} req
* @param {any} res
*/
async function getFileHead(req, res) {
const projectId = req.params.Project_id
const fileId = req.params.File_id
@@ -159,6 +167,9 @@ async function getFileHead(req, res) {
res.status(200).end()
}
/**
* @param {any} file
*/
function isHtml(file) {
return (
fileEndsWith(file, '.html') ||
@@ -167,6 +178,10 @@ function isHtml(file) {
)
}
/**
* @param {any} file
* @param {any} ext
*/
function fileEndsWith(file, ext) {
return (
file.name != null &&
@@ -175,6 +190,9 @@ function fileEndsWith(file, ext) {
)
}
/**
* @param {any} userAgent
*/
function isMobileSafari(userAgent) {
return (
userAgent &&

View File

@@ -15,17 +15,24 @@ import {
} from '@overleaf/fetch-utils'
import settings from '@overleaf/settings'
/** @type {any} */
import SessionManager from '../Authentication/SessionManager.mjs'
import UserGetter from '../User/UserGetter.mjs'
/** @type {any} */
import ProjectGetter from '../Project/ProjectGetter.mjs'
import Errors from '../Errors/Errors.js'
/** @type {any} */
import HistoryManager from './HistoryManager.mjs'
/** @type {any} */
import ProjectDetailsHandler from '../Project/ProjectDetailsHandler.mjs'
/** @type {any} */
import ProjectEntityUpdateHandler from '../Project/ProjectEntityUpdateHandler.mjs'
/** @type {any} */
import RestoreManager from './RestoreManager.mjs'
import { prepareZipAttachment } from '../../infrastructure/Response.mjs'
import Features from '../../infrastructure/Features.mjs'
import { z, zz, parseReq } from '../../infrastructure/Validation.mjs'
/** @type {any} */
import ProjectAuditLogHandler from '../Project/ProjectAuditLogHandler.mjs'
// Number of seconds after which the browser should send a request to revalidate
@@ -38,10 +45,18 @@ const STALE_WHILE_REVALIDATE_SECONDS = 365 * 86400 // 1 year
const MAX_HISTORY_ZIP_ATTEMPTS = 40
/**
* @param {any} req
* @param {any} res
*/
async function getBlob(req, res) {
await requestBlob('GET', req, res)
}
/**
* @param {any} req
* @param {any} res
*/
async function headBlob(req, res) {
await requestBlob('HEAD', req, res)
}
@@ -56,6 +71,11 @@ const requestBlobSchema = z.object({
}),
})
/**
* @param {any} method
* @param {any} req
* @param {any} res
*/
async function requestBlob(method, req, res) {
const { params } = parseReq(req, requestBlobSchema)
const { project_id: projectId, hash } = params
@@ -76,7 +96,7 @@ async function requestBlob(method, req, res) {
method,
range
))
} catch (err) {
} catch (/** @type {any} */ err) {
if (err instanceof Errors.NotFoundError) return res.status(404).end()
throw err
}
@@ -94,7 +114,7 @@ async function requestBlob(method, req, res) {
try {
await pipeline(stream, res)
} catch (err) {
} catch (/** @type {any} */ err) {
// If the downstream request is cancelled, we get an
// ERR_STREAM_PREMATURE_CLOSE, ignore these "errors".
if (!isPrematureClose(err)) {
@@ -103,6 +123,10 @@ async function requestBlob(method, req, res) {
}
}
/**
* @param {any} res
* @param {any} etag
*/
function setBlobCacheHeaders(res, etag) {
// Blobs are immutable, so they can in principle be cached indefinitely. Here,
// we ask the browser to cache them for some time, but then check back
@@ -115,6 +139,11 @@ function setBlobCacheHeaders(res, etag) {
res.set('ETag', etag)
}
/**
* @param {any} req
* @param {any} res
* @param {any} next
*/
async function proxyToHistoryApi(req, res, next) {
const userId = SessionManager.getLoggedInUserId(req.session)
const url = settings.apis.project_history.url + req.url
@@ -135,7 +164,7 @@ async function proxyToHistoryApi(req, res, next) {
try {
await pipeline(stream, res)
} catch (err) {
} catch (/** @type {any} */ err) {
// If the downstream request is cancelled, we get an
// ERR_STREAM_PREMATURE_CLOSE.
if (!isPrematureClose(err)) {
@@ -144,6 +173,11 @@ async function proxyToHistoryApi(req, res, next) {
}
}
/**
* @param {any} req
* @param {any} res
* @param {any} next
*/
async function proxyToHistoryApiAndInjectUserDetails(req, res, next) {
const userId = SessionManager.getLoggedInUserId(req.session)
const url = settings.apis.project_history.url + req.url
@@ -155,6 +189,11 @@ async function proxyToHistoryApiAndInjectUserDetails(req, res, next) {
res.json(data)
}
/**
* @param {any} req
* @param {any} res
* @param {any} next
*/
async function resyncProjectHistory(req, res, next) {
// increase timeout to 6 minutes
res.setTimeout(6 * 60 * 1000)
@@ -173,7 +212,7 @@ async function resyncProjectHistory(req, res, next) {
projectId,
opts
)
} catch (err) {
} catch (/** @type {any} */ err) {
if (err instanceof Errors.ProjectHistoryDisabledError) {
return res.sendStatus(404)
} else {
@@ -184,6 +223,11 @@ async function resyncProjectHistory(req, res, next) {
res.sendStatus(204)
}
/**
* @param {any} req
* @param {any} res
* @param {any} next
*/
async function restoreFileFromV2(req, res, next) {
const { project_id: projectId } = req.params
const { version, pathname } = req.body
@@ -215,6 +259,11 @@ async function restoreFileFromV2(req, res, next) {
})
}
/**
* @param {any} req
* @param {any} res
* @param {any} next
*/
async function revertFile(req, res, next) {
const { project_id: projectId } = req.params
const { version, pathname } = req.body
@@ -247,6 +296,11 @@ async function revertFile(req, res, next) {
})
}
/**
* @param {any} req
* @param {any} res
* @param {any} next
*/
async function revertProject(req, res, next) {
const { project_id: projectId } = req.params
const { version } = req.body
@@ -273,6 +327,11 @@ async function revertProject(req, res, next) {
res.json(reverted)
}
/**
* @param {any} req
* @param {any} res
* @param {any} next
*/
async function getLabels(req, res, next) {
const projectId = req.params.Project_id
@@ -284,6 +343,11 @@ async function getLabels(req, res, next) {
res.json(labels)
}
/**
* @param {any} req
* @param {any} res
* @param {any} next
*/
async function createLabel(req, res, next) {
const projectId = req.params.Project_id
const { comment, version } = req.body
@@ -301,6 +365,9 @@ async function createLabel(req, res, next) {
res.json(label)
}
/**
* @param {any} label
*/
async function _enrichLabel(label) {
const newLabel = Object.assign({}, label)
if (!label.user_id) {
@@ -317,11 +384,16 @@ async function _enrichLabel(label) {
return newLabel
}
/**
* @param {any} labels
*/
async function _enrichLabels(labels) {
if (!labels || !labels.length) {
return []
}
const uniqueUsers = new Set(labels.map(label => label.user_id))
const uniqueUsers = new Set(
labels.map(/** @param {any} label */ label => label.user_id)
)
// For backwards compatibility, and for anonymously created labels in SP
// expect missing user_id fields
@@ -336,15 +408,22 @@ async function _enrichLabels(labels) {
last_name: 1,
email: 1,
})
const users = new Map(rawUsers.map(user => [String(user._id), user]))
const users = new Map(
rawUsers.map(/** @param {any} user */ user => [String(user._id), user])
)
labels.forEach(label => {
const user = users.get(label.user_id)
label.user_display_name = _displayNameForUser(user)
})
labels.forEach(
/** @param {any} label */ label => {
const user = users.get(label.user_id)
label.user_display_name = _displayNameForUser(user)
}
)
return labels
}
/**
* @param {any} user
*/
function _displayNameForUser(user) {
if (user == null) {
return 'Anonymous'
@@ -365,6 +444,11 @@ function _displayNameForUser(user) {
return name
}
/**
* @param {any} req
* @param {any} res
* @param {any} next
*/
async function deleteLabel(req, res, next) {
const { Project_id: projectId, label_id: labelId } = req.params
const userId = SessionManager.getLoggedInUserId(req.session)
@@ -393,11 +477,17 @@ const downloadZipOfVersionSchema = z.object({
}),
})
/**
* @param {any} req
* @param {any} res
* @param {any} next
*/
async function downloadZipOfVersion(req, res, next) {
const { params } = parseReq(req, downloadZipOfVersionSchema)
const { project_id: projectId, version } = params
const userId = SessionManager.getLoggedInUserId(req.session)
/** @type {any} */
const project = await ProjectDetailsHandler.promises.getDetails(projectId)
const v1Id =
project.overleaf && project.overleaf.history && project.overleaf.history.id
@@ -430,6 +520,13 @@ async function downloadZipOfVersion(req, res, next) {
)
}
/**
* @param {any} v1ProjectId
* @param {any} version
* @param {any} name
* @param {any} req
* @param {any} res
*/
async function _pipeHistoryZipToResponse(v1ProjectId, version, name, req, res) {
if (req.destroyed) {
// client has disconnected -- skip project history api call and download
@@ -447,7 +544,7 @@ async function _pipeHistoryZipToResponse(v1ProjectId, version, name, req, res) {
let stream
try {
stream = await fetchStream(url, { basicAuth })
} catch (err) {
} catch (/** @type {any} */ err) {
if (err instanceof RequestFailedError && err.response.status === 404) {
return res.sendStatus(404)
} else {
@@ -459,7 +556,7 @@ async function _pipeHistoryZipToResponse(v1ProjectId, version, name, req, res) {
try {
await pipeline(stream, res)
} catch (err) {
} catch (/** @type {any} */ err) {
// If the downstream request is cancelled, we get an
// ERR_STREAM_PREMATURE_CLOSE.
if (!isPrematureClose(err)) {
@@ -472,7 +569,7 @@ async function _pipeHistoryZipToResponse(v1ProjectId, version, name, req, res) {
let body
try {
body = await fetchJson(url, { method: 'POST', basicAuth })
} catch (err) {
} catch (/** @type {any} */ err) {
if (err instanceof RequestFailedError && err.response.status === 404) {
throw new Errors.NotFoundError('zip not found')
} else {
@@ -513,7 +610,7 @@ async function _pipeHistoryZipToResponse(v1ProjectId, version, name, req, res) {
const stream = await fetchStream(body.zipUrl)
prepareZipAttachment(res, `${name}.zip`)
await pipeline(stream, res)
} catch (err) {
} catch (/** @type {any} */ err) {
if (attempt > MAX_HISTORY_ZIP_ATTEMPTS) {
throw err
}
@@ -545,6 +642,11 @@ const getLatestHistorySchema = z.object({
}),
})
/**
* @param {any} req
* @param {any} res
* @param {any} next
*/
async function getLatestHistory(req, res, next) {
const { params } = parseReq(req, getLatestHistorySchema)
const projectId = params.project_id
@@ -562,6 +664,11 @@ const getChangesSchema = z.object({
}),
})
/**
* @param {any} req
* @param {any} res
* @param {any} next
*/
async function getChanges(req, res, next) {
const { params, query } = parseReq(req, getChangesSchema)
const projectId = params.project_id
@@ -598,6 +705,9 @@ async function getChanges(req, res, next) {
}
}
/**
* @param {any} err
*/
function isPrematureClose(err) {
return (
err instanceof Error &&

View File

@@ -71,6 +71,9 @@ async function migrateProjects(opts = {}) {
.sort({ _id: -1 })
let terminating = false
/**
* @param {any} signal
*/
const handleSignal = signal => {
logger.info({ signal }, 'History ranges support migration received signal')
terminating = true
@@ -78,6 +81,7 @@ async function migrateProjects(opts = {}) {
process.on('SIGINT', handleSignal)
process.on('SIGTERM', handleSignal)
/** @type {{ quick: number; skipped: number; resync: number; total: number; [key: string]: number }} */
const projectsProcessed = {
quick: 0,
skipped: 0,
@@ -120,35 +124,39 @@ async function migrateProjects(opts = {}) {
}
const job = processProject(projectId, direction, quickOnly)
.then(info => {
jobsByProjectId.delete(projectId)
projectsProcessed[info.migrationType] += 1
projectsProcessed.total += 1
logger.debug(
{
projectId,
direction,
projectsProcessed,
errors,
...info,
},
'History ranges support migration'
)
if (projectsProcessed.total % 10000 === 0) {
logger.info(
{ projectsProcessed, errors, lastProjectId: projectId },
'History ranges support migration progress'
.then(
/** @param {any} info */ info => {
jobsByProjectId.delete(projectId)
projectsProcessed[info.migrationType] += 1
projectsProcessed.total += 1
logger.debug(
{
projectId,
direction,
projectsProcessed,
errors,
...info,
},
'History ranges support migration'
)
if (projectsProcessed.total % 10000 === 0) {
logger.info(
{ projectsProcessed, errors, lastProjectId: projectId },
'History ranges support migration progress'
)
}
}
)
.catch(
/** @param {any} err */ err => {
jobsByProjectId.delete(projectId)
errors += 1
logger.error(
{ err, projectId, direction, projectsProcessed, errors },
'Failed to migrate history ranges support'
)
}
})
.catch(err => {
jobsByProjectId.delete(projectId)
errors += 1
logger.error(
{ err, projectId, direction, projectsProcessed, errors },
'Failed to migrate history ranges support'
)
})
)
jobsByProjectId.set(projectId, job)
}
@@ -198,7 +206,7 @@ async function quickMigration(projectId, direction = 'forwards') {
try {
projectHasRanges =
await DocstoreManager.promises.projectHasRanges(projectId)
} catch (err) {
} catch (/** @type {any} */ err) {
// Docstore request probably timed out. Assume the project has ranges
logger.warn(
{ err, projectId },
@@ -216,7 +224,7 @@ async function quickMigration(projectId, direction = 'forwards') {
projectId,
direction === 'forwards'
)
} catch (err) {
} catch (/** @type {any} */ err) {
await DocumentUpdaterHandler.promises.unblockProject(projectId)
await hardResyncProject(projectId)
throw err
@@ -225,7 +233,7 @@ async function quickMigration(projectId, direction = 'forwards') {
let wasBlocked
try {
wasBlocked = await DocumentUpdaterHandler.promises.unblockProject(projectId)
} catch (err) {
} catch (/** @type {any} */ err) {
await hardResyncProject(projectId)
throw err
}

View File

@@ -23,6 +23,10 @@ const rateLimiters = {
}),
}
/**
* @param {any} webRouter
* @param {any} privateApiRouter
*/
function apply(webRouter, privateApiRouter) {
// Blobs

View File

@@ -260,6 +260,7 @@ function oldDebugProjects(userId) {
}
}
/** @type {Record<string, any>} */
const NotificationsBuilder = {
// Note: notification keys should be url-safe
dropboxUnlinkedDueToLapsedReconfirmation(userId) {
@@ -291,6 +292,7 @@ const NotificationsBuilder = {
},
}
/** @type {Record<string, any>} */
NotificationsBuilder.promises = {
dropboxUnlinkedDueToLapsedReconfirmation,
redundantPersonalSubscription,

View File

@@ -42,8 +42,8 @@ import UserSettingsHelper from './UserSettingsHelper.mjs'
/**
* @param {Affiliation} affiliation
* @param session
* @param linkedInstitutionIds
* @param {any} session
* @param {string[]} linkedInstitutionIds
* @returns {boolean}
* @private
*/
@@ -99,6 +99,9 @@ const _buildPortalTemplatesList = affiliations => {
return portalTemplates
}
/**
* @param {any} req
*/
function cleanupSession(req) {
// cleanup redirects at the end of the redirect chain
delete req.session.postCheckoutRedirect
@@ -124,10 +127,12 @@ async function projectListPage(req, res, next) {
// - object - the subscription data object
let usersBestSubscription
let usersIndividualSubscription
/** @type {any[]} */
let usersGroupSubscriptions = []
let usersManagedGroupSubscriptions = []
let survey
let userIsMemberOfGroupSubscription = false
/** @type {any[]} */
let groupSubscriptionsPendingEnrollment = []
const isSaas = Features.hasFeature('saas')
@@ -153,7 +158,8 @@ async function projectListPage(req, res, next) {
if (groupsWithEmails && groupsWithEmails.length > 0) {
if (
groupsWithEmails.some(
({ subscription }) => subscription.managedUsersEnabled
(/** @type {any} */ { subscription }) =>
subscription.managedUsersEnabled
)
) {
return res.redirect('/domain-capture')
@@ -338,6 +344,7 @@ async function projectListPage(req, res, next) {
reconfirmedViaSAML = _.get(req.session, ['saml', 'reconfirmed'])
const samlSession = req.session.saml
// Notification: SSO Available
/** @type {string[]} */
const linkedInstitutionIds = []
userEmails.forEach(email => {
if (email.samlProviderId) {
@@ -906,6 +913,9 @@ function _hasActiveFilter(filters) {
)
}
/**
* @param {any} user
*/
// todo: quota clean-up: rename function and vars
async function _userHasAIAssist(user) {
let hasPremiumAiFeatures
@@ -937,6 +947,9 @@ async function _userHasAIAssist(user) {
// Determines if user is able to enable AI assist
// based on their permissions and settings
// It does NOT determine if the user has AI Assist enabled
/**
* @param {any} user
*/
async function _canUseAIAssist(user) {
// Check if the assistant has been manually disabled by the user
// post https://github.com/overleaf/internal/pull/31273 we can rely on user.aiFeatures being populated

View File

@@ -17,23 +17,41 @@ const unlearnSchema = z.object({
})
export default {
/**
* @param {any} req
* @param {any} res
* @param {any} next
*/
learn(req, res, next) {
const { body } = parseReq(req, learnSchema)
const { word } = body
const userId = SessionManager.getLoggedInUserId(req.session)
LearnedWordsManager.learnWord(userId, word, err => {
if (err) return next(err)
res.sendStatus(204)
})
LearnedWordsManager.learnWord(
userId,
word,
/** @param {any} err */ err => {
if (err) return next(err)
res.sendStatus(204)
}
)
},
/**
* @param {any} req
* @param {any} res
* @param {any} next
*/
unlearn(req, res, next) {
const { body } = parseReq(req, unlearnSchema)
const { word } = body
const userId = SessionManager.getLoggedInUserId(req.session)
LearnedWordsManager.unlearnWord(userId, word, err => {
if (err) return next(err)
res.sendStatus(204)
})
LearnedWordsManager.unlearnWord(
userId,
word,
/** @param {any} err */ err => {
if (err) return next(err)
res.sendStatus(204)
}
)
},
}

View File

@@ -22,7 +22,7 @@ export function isStandaloneAiAddOnPlanCode(planCode) {
/**
* Returns whether subscription change will have have the ai bundle once the change is processed
*
* @param {Object} subscriptionChange The subscription change object coming from payment provider
* @param {Record<string, any>} subscriptionChange The subscription change object coming from payment provider
* type should be PaymentProviderSubscriptionChange but if imported here, it creates a circular dependency
* TODO: fix this when moved to es modules
*
@@ -31,7 +31,9 @@ export function isStandaloneAiAddOnPlanCode(planCode) {
export function subscriptionChangeIsAiAssistUpgrade(subscriptionChange) {
return Boolean(
isStandaloneAiAddOnPlanCode(subscriptionChange.nextPlanCode) ||
subscriptionChange.nextAddOns?.some(addOn => addOn.code === AI_ADD_ON_CODE)
subscriptionChange.nextAddOns?.some(
/** @param {any} addOn */ addOn => addOn.code === AI_ADD_ON_CODE
)
)
}

View File

@@ -5,6 +5,9 @@ import Errors from '../Errors/Errors.js'
import OError from '@overleaf/o-error'
export class RecurlyTransactionError extends Errors.BackwardCompatibleError {
/**
* @param {any} options
*/
constructor(options) {
super({
message: 'Unknown transaction error',

View File

@@ -11,6 +11,9 @@ import CollaboratorsInvitesGetter from '../Collaborators/CollaboratorsInviteGett
import PrivilegeLevels from '../Authorization/PrivilegeLevels.mjs'
import { callbackify, callbackifyMultiResult } from '@overleaf/promise-utils'
/**
* @param {any} projectId
*/
async function allowedNumberOfCollaboratorsInProject(projectId) {
const project = await ProjectGetter.promises.getProject(projectId, {
owner_ref: true,
@@ -18,15 +21,21 @@ async function allowedNumberOfCollaboratorsInProject(projectId) {
return await allowedNumberOfCollaboratorsForUser(project.owner_ref)
}
/**
* @param {any} userId
*/
async function allowedNumberOfCollaboratorsForUser(userId) {
const user = await UserGetter.promises.getUser(userId, { features: 1 })
if (user.features && user.features.collaborators) {
if (user?.features?.collaborators) {
return user.features.collaborators
} else {
return Settings.defaultFeatures.collaborators
}
}
/**
* @param {any} projectId
*/
async function canAcceptEditCollaboratorInvite(projectId) {
const allowedNumber = await allowedNumberOfCollaboratorsInProject(projectId)
if (allowedNumber < 0) {
@@ -39,6 +48,10 @@ async function canAcceptEditCollaboratorInvite(projectId) {
return currentEditors + 1 <= allowedNumber
}
/**
* @param {any} projectId
* @param {any} numberOfNewEditCollaborators
*/
async function canAddXEditCollaborators(
projectId,
numberOfNewEditCollaborators
@@ -106,6 +119,9 @@ async function canChangeCollaboratorPrivilegeLevel(
return slotsTaken + inviteCount < allowedNumber
}
/**
* @param {any} user
*/
async function hasPaidSubscription(user) {
const { hasSubscription, subscription } = await userHasSubscription(user)
const { isMember } = await userIsMemberOfGroupSubscription(user)
@@ -115,11 +131,17 @@ async function hasPaidSubscription(user) {
}
}
/**
* @param {any} user
*/
// alias for backward-compatibility with modules. Use `haspaidsubscription` instead
async function userHasSubscriptionOrIsGroupMember(user) {
return await hasPaidSubscription(user)
}
/**
* @param {any} user
*/
async function userHasSubscription(user) {
const subscription = await SubscriptionLocator.promises.getUsersSubscription(
user._id
@@ -140,12 +162,18 @@ async function userHasSubscription(user) {
}
}
/**
* @param {any} user
*/
async function userIsMemberOfGroupSubscription(user) {
const subscriptions =
(await SubscriptionLocator.promises.getMemberSubscriptions(user._id)) || []
return { isMember: subscriptions.length > 0, subscriptions }
}
/**
* @param {any} subscription
*/
function teamHasReachedMemberLimit(subscription) {
const currentTotal =
(subscription.member_ids || []).length +
@@ -155,6 +183,10 @@ function teamHasReachedMemberLimit(subscription) {
return currentTotal >= subscription.membersLimit
}
/**
* @param {any} subscriptionId
* @param {any} callback
*/
async function hasGroupMembersLimitReached(subscriptionId, callback) {
const subscription =
await SubscriptionLocator.promises.getSubscription(subscriptionId)

View File

@@ -601,6 +601,9 @@ export class PaymentProviderSubscriptionChange {
this.total = this.subtotal + this.tax
}
/**
* @param {any} addOnCode
*/
getAddOn(addOnCode) {
return this.nextAddOns.find(addOn => addOn.code === addOnCode)
}

View File

@@ -14,20 +14,22 @@ import logger from '@overleaf/logger'
*/
function ensurePlansAreSetupCorrectly() {
Settings.plans.forEach(plan => {
if (typeof plan.price_in_cents !== 'number') {
logger.fatal({ plan }, 'missing price on plan')
process.exit(1)
Settings.plans.forEach(
/** @param {any} plan */ plan => {
if (typeof plan.price_in_cents !== 'number') {
logger.fatal({ plan }, 'missing price on plan')
process.exit(1)
}
if (plan.price) {
logger.fatal({ plan }, 'unclear price attribute on plan')
process.exit(1)
}
if (plan.price_in_unit) {
logger.fatal({ plan }, 'deprecated price_in_unit attribute on plan')
process.exit(1)
}
}
if (plan.price) {
logger.fatal({ plan }, 'unclear price attribute on plan')
process.exit(1)
}
if (plan.price_in_unit) {
logger.fatal({ plan }, 'deprecated price_in_unit attribute on plan')
process.exit(1)
}
})
)
}
/**

View File

@@ -36,7 +36,7 @@ import _ from 'lodash'
class RecurlyClientWithErrorHandling extends recurly.Client {
/**
* @param {import('recurly/lib/recurly/Http').Response} response
* @param {any} response
* @return {Error | null}
* @private
*/
@@ -78,6 +78,9 @@ async function getAccountForUserId(userId) {
}
}
/**
* @param {any} userId
*/
async function createAccountForUserId(userId) {
const user = await UserGetter.promises.getUser(userId, {
_id: 1,
@@ -345,6 +348,7 @@ async function _reapplyPendingChangeAfterImmediateUpdate(
)
// Merge: start with pending add-ons, add any new add-ons from immediate update
/** @type {Record<string, any>} */
const mergedAddOns = { ...preUpdatePendingAddOns }
for (const [code, details] of Object.entries(postUpdateAddOns)) {
// include any add-ons that were added via immediate update just now
@@ -446,20 +450,32 @@ async function previewSubscriptionChange(changeRequest) {
}
}
/**
* @param {any} subscriptionId
*/
async function removeSubscriptionChange(subscriptionId) {
const removed = await client.removeSubscriptionChange(subscriptionId)
logger.debug({ subscriptionId }, 'removed pending subscription change')
return removed
}
/**
* @param {any} subscriptionUuid
*/
async function removeSubscriptionChangeByUuid(subscriptionUuid) {
return await removeSubscriptionChange('uuid-' + subscriptionUuid)
}
/**
* @param {any} subscriptionUuid
*/
async function reactivateSubscriptionByUuid(subscriptionUuid) {
return await client.reactivateSubscription('uuid-' + subscriptionUuid)
}
/**
* @param {any} subscriptionUuid
*/
async function cancelSubscriptionByUuid(subscriptionUuid) {
try {
return await client.cancelSubscription('uuid-' + subscriptionUuid)
@@ -493,12 +509,19 @@ async function cancelSubscriptionByUuid(subscriptionUuid) {
}
}
/**
* @param {any} subscriptionUuid
* @param {any} pauseCycles
*/
async function pauseSubscriptionByUuid(subscriptionUuid, pauseCycles) {
return await client.pauseSubscription('uuid-' + subscriptionUuid, {
remainingPauseCycles: pauseCycles,
})
}
/**
* @param {any} subscriptionUuid
*/
async function resumeSubscriptionByUuid(subscriptionUuid) {
return await client.resumeSubscription('uuid-' + subscriptionUuid)
}
@@ -552,6 +575,9 @@ async function getPlan(planCode) {
return planFromApi(plan)
}
/**
* @param {any} subscription
*/
function subscriptionIsCanceledOrExpired(subscription) {
const state = subscription?.recurlyStatus?.state
return state === 'canceled' || state === 'expired'
@@ -884,6 +910,9 @@ function subscriptionUpdateRequestToApi(updateRequest) {
return requestBody
}
/**
* @param {any} subscriptionUuid
*/
async function terminateSubscriptionByUuid(subscriptionUuid) {
const subscription = await client.terminateSubscription(
'uuid-' + subscriptionUuid,

View File

@@ -14,8 +14,8 @@ import RecurlyMetrics from './RecurlyMetrics.mjs'
/**
* Updates the email address of a Recurly account
*
* @param userId
* @param newAccountEmail - the new email address to set for the Recurly account
* @param {any} userId
* @param {any} newAccountEmail - the new email address to set for the Recurly account
*/
async function updateAccountEmailAddress(userId, newAccountEmail) {
const data = {
@@ -903,11 +903,10 @@ const RecurlyWrapper = {
getSubscription: callbackify(promises.getSubscription),
getSubscriptions: callbackify(promises.getSubscriptions),
updateAccountEmailAddress: callbackify(promises.updateAccountEmailAddress),
}
RecurlyWrapper.promises = {
...promises,
updateAccountEmailAddress,
promises: {
...promises,
updateAccountEmailAddress,
},
}
export default RecurlyWrapper

View File

@@ -54,7 +54,7 @@ const SUBSCRIPTION_PAUSED_REDIRECT_PATH =
/**
* Check if a Stripe subscription is currently paused
* @param {Object} subscription - The subscription object
* @param {Record<string, any>} subscription - The subscription object
* @returns {Promise<boolean>}
*/
async function _checkStripeSubscriptionPauseStatus(subscription) {
@@ -78,7 +78,7 @@ async function _checkStripeSubscriptionPauseStatus(subscription) {
/**
* Check if a Recurly subscription is currently paused
* @param {Object} subscription - The subscription object
* @param {Record<string, any>} subscription - The subscription object
* @returns {Promise<boolean>}
*/
async function _checkRecurlySubscriptionPauseStatus(subscription) {
@@ -102,7 +102,7 @@ async function _checkRecurlySubscriptionPauseStatus(subscription) {
}
/** Check if a user's subscription is manual or custom
* @param {Object} user - The user object
* @param {Record<string, any>} user - The user object
* @returns {Promise<boolean>}
*/
async function _isManualOrCustomSubscription(user) {
@@ -120,7 +120,7 @@ async function _isManualOrCustomSubscription(user) {
/**
* Check if a user's subscription is currently paused
* @param {Object} user - The user object
* @param {Record<string, any>} user - The user object
* @returns {Promise<{isPaused: boolean, redirectPath?: string}>}
*/
async function checkSubscriptionPauseStatus(user) {
@@ -176,6 +176,10 @@ function formatGroupPlansDataForDash() {
}
}
/**
* @param {any} req
* @param {any} res
*/
async function userSubscriptionPage(req, res) {
const user = SessionManager.getSessionUser(req.session)
await SplitTestHandler.promises.getAssignment(req, res, 'sharing-updates')
@@ -242,6 +246,7 @@ async function userSubscriptionPage(req, res) {
try {
const managedGroups = await async.filter(
managedGroupSubscriptions || [],
/** @param {any} subscription */
async subscription => {
const managedUsersResults = await Modules.promises.hooks.fire(
'hasManagedUsersFeature',
@@ -261,8 +266,8 @@ async function userSubscriptionPage(req, res) {
)
}
)
groupSettingsEnabledFor = managedGroups.map(subscription =>
subscription._id.toString()
groupSettingsEnabledFor = managedGroups.map(
(/** @type {any} */ subscription) => subscription._id.toString()
)
} catch (error) {
logger.error(
@@ -275,7 +280,7 @@ async function userSubscriptionPage(req, res) {
try {
const managedGroups = await async.filter(
managedGroupSubscriptions || [],
async subscription => {
async (/** @type {any} */ subscription) => {
const managedUsersResults = await Modules.promises.hooks.fire(
'hasManagedUsersFeatureOnNonProfessionalPlan',
subscription
@@ -296,8 +301,8 @@ async function userSubscriptionPage(req, res) {
)
}
)
groupSettingsAdvertisedFor = managedGroups.map(subscription =>
subscription._id.toString()
groupSettingsAdvertisedFor = managedGroups.map(
(/** @type {any} */ subscription) => subscription._id.toString()
)
} catch (error) {
logger.error(
@@ -339,6 +344,10 @@ async function userSubscriptionPage(req, res) {
res.render('subscriptions/dashboard-react', data)
}
/**
* @param {any} req
* @param {any} res
*/
async function successfulSubscription(req, res) {
const user = SessionManager.getSessionUser(req.session)
if (!user) {
@@ -382,6 +391,11 @@ const pauseSubscriptionSchema = z.object({
}),
})
/**
* @param {any} req
* @param {any} res
* @param {any} next
*/
async function pauseSubscription(req, res, next) {
const user = SessionManager.getSessionUser(req.session)
const { params } = parseReq(req, pauseSubscriptionSchema)
@@ -425,6 +439,11 @@ async function pauseSubscription(req, res, next) {
}
}
/**
* @param {any} req
* @param {any} res
* @param {any} next
*/
async function resumeSubscription(req, res, next) {
const user = SessionManager.getSessionUser(req.session)
logger.debug({ userId: user._id }, `resuming subscription`)
@@ -441,6 +460,11 @@ async function resumeSubscription(req, res, next) {
}
}
/**
* @param {any} req
* @param {any} res
* @param {any} next
*/
async function cancelSubscription(req, res, next) {
const user = SessionManager.getSessionUser(req.session)
logger.debug({ userId: user._id }, 'canceling subscription')
@@ -456,6 +480,9 @@ async function cancelSubscription(req, res, next) {
}
/**
* @param {any} req
* @param {any} res
* @param {any} next
* @returns {Promise<void>}
*/
async function canceledSubscription(req, res, next) {
@@ -467,20 +494,32 @@ async function canceledSubscription(req, res, next) {
})
}
/**
* @param {any} req
* @param {any} res
* @param {any} next
*/
function cancelV1Subscription(req, res, next) {
const userId = SessionManager.getLoggedInUserId(req.session)
logger.debug({ userId }, 'canceling v1 subscription')
V1SubscriptionManager.cancelV1Subscription(userId, function (err) {
if (err) {
OError.tag(err, 'something went wrong canceling v1 subscription', {
userId,
})
return next(err)
V1SubscriptionManager.cancelV1Subscription(
userId,
/** @param {any} err */ function (err) {
if (err) {
OError.tag(err, 'something went wrong canceling v1 subscription', {
userId,
})
return next(err)
}
res.redirect('/user/subscription')
}
res.redirect('/user/subscription')
})
)
}
/**
* @param {any} req
* @param {any} res
*/
async function previewAddonPurchase(req, res) {
const user = SessionManager.getSessionUser(req.session)
const userId = user._id
@@ -600,6 +639,11 @@ const purchaseAddonSchema = z.object({
}),
})
/**
* @param {any} req
* @param {any} res
* @param {any} next
*/
async function purchaseAddon(req, res, next) {
const user = SessionManager.getSessionUser(req.session)
const { params } = parseReq(req, purchaseAddonSchema)
@@ -642,22 +686,22 @@ async function purchaseAddon(req, res, next) {
)
return res.status(402).json({
message: 'Payment action required',
clientSecret: err.info.clientSecret,
publicKey: err.info.publicKey,
clientSecret: /** @type {any} */ (err).info.clientSecret,
publicKey: /** @type {any} */ (err).info.publicKey,
})
} else if (err instanceof PaymentFailedError) {
logger.debug(
{
userId: user._id,
reason: err.info.reason,
adviceCode: err.info.adviceCode,
reason: /** @type {any} */ (err).info.reason,
adviceCode: /** @type {any} */ (err).info.adviceCode,
},
'Payment failed for transaction'
)
return res.status(402).json({
message: 'Payment failed',
reason: err.info.reason,
adviceCode: err.info.adviceCode,
reason: /** @type {any} */ (err).info.reason,
adviceCode: /** @type {any} */ (err).info.adviceCode,
})
} else if (err instanceof MultiplePendingChangesError) {
logger.warn(
@@ -695,6 +739,11 @@ const removeAddonSchema = z.object({
}),
})
/**
* @param {any} req
* @param {any} res
* @param {any} next
*/
async function removeAddon(req, res, next) {
const user = SessionManager.getSessionUser(req.session)
const { params } = parseReq(req, removeAddonSchema)
@@ -749,6 +798,8 @@ const reactivateAddonSchema = z.object({
* Reactivate an add-on pending cancellation
*
* This "cancels" the cancellation.
* @param {any} req
* @param {any} res
*/
async function reactivateAddon(req, res) {
const user = SessionManager.getSessionUser(req.session)
@@ -776,6 +827,11 @@ async function reactivateAddon(req, res) {
}
}
/**
* @param {any} req
* @param {any} res
* @param {any} next
*/
async function previewSubscription(req, res, next) {
const planCode = req.query.planCode
if (!planCode) {
@@ -822,24 +878,37 @@ async function previewSubscription(req, res, next) {
})
}
/**
* @param {any} req
* @param {any} res
* @param {any} next
*/
function cancelPendingSubscriptionChange(req, res, next) {
const user = SessionManager.getSessionUser(req.session)
logger.debug({ userId: user._id }, 'canceling pending subscription change')
SubscriptionHandler.cancelPendingSubscriptionChange(user, function (err) {
if (err) {
OError.tag(
err,
'something went wrong canceling pending subscription change',
{
user_id: user._id,
}
)
return next(err)
SubscriptionHandler.cancelPendingSubscriptionChange(
user,
/** @param {any} err */ function (err) {
if (err) {
OError.tag(
err,
'something went wrong canceling pending subscription change',
{
user_id: user._id,
}
)
return next(err)
}
res.redirect('/user/subscription')
}
res.redirect('/user/subscription')
})
)
}
/**
* @param {any} req
* @param {any} res
* @param {any} next
*/
async function updateAccountEmailAddress(req, res, next) {
const user = SessionManager.getSessionUser(req.session)
try {
@@ -854,6 +923,11 @@ async function updateAccountEmailAddress(req, res, next) {
}
}
/**
* @param {any} req
* @param {any} res
* @param {any} next
*/
function reactivateSubscription(req, res, next) {
const user = SessionManager.getSessionUser(req.session)
logger.debug({ userId: user._id }, 'reactivating subscription')
@@ -878,6 +952,11 @@ function reactivateSubscription(req, res, next) {
})
}
/**
* @param {any} req
* @param {any} res
* @param {any} next
*/
function recurlyCallback(req, res, next) {
logger.debug({ data: req.body }, 'received recurly callback')
const event = Object.keys(req.body)[0]
@@ -926,6 +1005,10 @@ function recurlyCallback(req, res, next) {
}
}
/**
* @param {any} req
* @param {any} res
*/
async function extendTrial(req, res) {
const user = SessionManager.getSessionUser(req.session)
const { subscription } =
@@ -951,20 +1034,36 @@ async function extendTrial(req, res) {
res.sendStatus(200)
}
/**
* @param {any} req
* @param {any} res
* @param {any} next
*/
function recurlyNotificationParser(req, res, next) {
let xml = ''
req.on('data', chunk => (xml += chunk))
req.on('data', /** @param {any} chunk */ chunk => (xml += chunk))
req.on('end', () =>
RecurlyWrapper._parseXml(xml, function (error, body) {
if (error) {
return next(error)
RecurlyWrapper._parseXml(
xml,
/**
* @param {any} error
* @param {any} body
*/
function (error, body) {
if (error) {
return next(error)
}
req.body = body
next()
}
req.body = body
next()
})
)
)
}
/**
* @param {any} req
* @param {any} res
*/
async function refreshUserFeatures(req, res) {
const { user_id: userId } = req.params
await FeaturesUpdater.promises.refreshFeatures(userId, 'acceptance-test')
@@ -972,6 +1071,8 @@ async function refreshUserFeatures(req, res) {
}
/**
* @param {any} req
* @param {any} res
* @returns {Promise<{currency: CurrencyCode, recommendedCurrency: CurrencyCode, countryCode: string|undefined}>}
*/
async function getRecommendedCurrency(req, res) {
@@ -1002,6 +1103,10 @@ async function getRecommendedCurrency(req, res) {
}
}
/**
* @param {any} req
* @param {any} res
*/
async function getLatamCountryBannerDetails(req, res) {
const userId = SessionManager.getLoggedInUserId(req.session)
let ip = req.ip

View File

@@ -17,6 +17,9 @@ import { AI_ADD_ON_CODE } from './AiHelper.mjs'
* @import { PaymentProviderSubscriptionChange } from './PaymentProviderEntities.mjs'
*/
/**
* @param {any} userId
*/
async function validateNoSubscriptionInRecurly(userId) {
let subscriptions =
await RecurlyWrapper.promises.listAccountActiveSubscriptions(userId)
@@ -37,6 +40,11 @@ async function validateNoSubscriptionInRecurly(userId) {
return true
}
/**
* @param {any} user
* @param {any} subscriptionDetails
* @param {any} recurlyTokenIds
*/
async function createSubscription(user, subscriptionDetails, recurlyTokenIds) {
const valid = await validateNoSubscriptionInRecurly(user._id)
@@ -82,7 +90,8 @@ async function previewSubscriptionChange(userId, planCode) {
/**
* @param user
* @param planCode
* @param {any} user
* @param {any} planCode
*/
async function updateSubscription(user, planCode) {
let hasSubscription = false
@@ -115,7 +124,7 @@ async function updateSubscription(user, planCode) {
}
/**
* @param user
* @param {any} user
*/
async function cancelPendingSubscriptionChange(user) {
const { hasSubscription, subscription } =
@@ -151,7 +160,7 @@ async function cancelPendingSubscriptionChange(user) {
/**
* Send cancellation email to user with split test for AI Assist addon
* @param user
* @param {any} user
*/
async function _sendCancellationEmail(user) {
const emailOpts = {
@@ -174,7 +183,7 @@ async function _sendCancellationEmail(user) {
}
/**
* @param user
* @param {any} user
*/
async function cancelSubscription(user) {
const { hasSubscription, subscription } =
@@ -187,7 +196,7 @@ async function cancelSubscription(user) {
}
/**
* @param user
* @param {any} user
*/
async function reactivateSubscription(user) {
try {
@@ -220,8 +229,8 @@ async function reactivateSubscription(user) {
}
/**
* @param recurlySubscription
* @param requesterData
* @param {any} recurlySubscription
* @param {any} requesterData
*/
async function syncSubscription(recurlySubscription, requesterData) {
const storedSubscription = await RecurlyWrapper.promises.getSubscription(
@@ -250,7 +259,7 @@ async function syncSubscription(recurlySubscription, requesterData) {
* This is used because Recurly doesn't always attempt collection of paast due
* invoices after Paypal billing info were updated.
*
* @param recurlyAccountCode
* @param {any} recurlyAccountCode
*/
async function attemptPaypalInvoiceCollection(recurlyAccountCode) {
const billingInfo =
@@ -274,6 +283,10 @@ async function attemptPaypalInvoiceCollection(recurlyAccountCode) {
)
}
/**
* @param {any} subscription
* @param {any} daysToExtend
*/
async function extendTrial(subscription, daysToExtend) {
await Modules.promises.hooks.fire('extendTrial', subscription, daysToExtend)
}
@@ -313,7 +326,7 @@ async function purchaseAddon(userId, addOnCode, quantity) {
/**
* Cancels an add-on for a user
*
* @param user
* @param {any} user
* @param {string} addOnCode
*/
async function removeAddon(user, addOnCode) {
@@ -334,6 +347,10 @@ async function reactivateAddon(userId, addOnCode) {
await Modules.promises.hooks.fire('reactivateAddOn', userId, addOnCode)
}
/**
* @param {any} user
* @param {any} pauseCycles
*/
async function pauseSubscription(user, pauseCycles) {
// only allow pausing on monthly plans not in a trial
const { subscription } =
@@ -367,8 +384,9 @@ async function pauseSubscription(user, pauseCycles) {
pauseCycles
)
}
async function resumeSubscription(user) {
/**
* @param {any} user
*/ async function resumeSubscription(user) {
const { subscription } =
await LimitationsManager.promises.userHasSubscription(user)
if (

View File

@@ -12,8 +12,8 @@ const MILLISECONDS = 1_000
* This function checks if a subscription should transition between 'active' and 'paused'
* states based on the current time and pause period metadata.
*
* @param {Object} subscription - The MongoDB subscription document
* @returns {Promise<Object>} - The updated subscription document with recomputed state
* @param {any} subscription - The MongoDB subscription document
* @returns {Promise<any>} - The updated subscription document with recomputed state
*/
async function recomputeSubscriptionState(subscription) {
if (
@@ -65,6 +65,9 @@ async function recomputeSubscriptionState(subscription) {
/**
* If the user changes to a less expensive plan, we shouldn't apply the change immediately.
* This is to avoid unintended/artifical credits on users Recurly accounts.
* @param {any} oldPlan
* @param {any} newPlan
* @param {any} isInTrial
*/
function shouldPlanChangeAtTermEnd(oldPlan, newPlan, isInTrial) {
if (isInTrial) {
@@ -138,6 +141,9 @@ function generateInitialLocalizedGroupPrice(recommendedCurrency, locale) {
}
}
/**
* @param {any} subscription
*/
function isPaidSubscription(subscription) {
const hasRecurlySubscription =
subscription?.recurlySubscription_id &&
@@ -148,6 +154,9 @@ function isPaidSubscription(subscription) {
return !!(subscription && (hasRecurlySubscription || hasStripeSubscription))
}
/**
* @param {any} subscription
*/
function isIndividualActivePaidSubscription(subscription) {
return (
isPaidSubscription(subscription) &&
@@ -157,6 +166,9 @@ function isIndividualActivePaidSubscription(subscription) {
)
}
/**
* @param {any} subscription
*/
function getPaymentProviderSubscriptionId(subscription) {
if (subscription?.recurlySubscription_id) {
return subscription.recurlySubscription_id
@@ -167,6 +179,9 @@ function getPaymentProviderSubscriptionId(subscription) {
return null
}
/**
* @param {any} subscription
*/
function getPaidSubscriptionState(subscription) {
if (subscription?.recurlyStatus?.state) {
return subscription.recurlyStatus.state
@@ -177,6 +192,9 @@ function getPaidSubscriptionState(subscription) {
return null
}
/**
* @param {any} subscription
*/
function getSubscriptionTrialStartedAt(subscription) {
if (subscription?.recurlyStatus?.trialStartedAt) {
return subscription.recurlyStatus?.trialStartedAt
@@ -184,6 +202,9 @@ function getSubscriptionTrialStartedAt(subscription) {
return subscription?.paymentProvider?.trialStartedAt
}
/**
* @param {any} subscription
*/
function getSubscriptionTrialEndsAt(subscription) {
if (subscription?.recurlyStatus?.trialEndsAt) {
return subscription.recurlyStatus?.trialEndsAt
@@ -191,6 +212,9 @@ function getSubscriptionTrialEndsAt(subscription) {
return subscription?.paymentProvider?.trialEndsAt
}
/**
* @param {any} trialEndsAt
*/
function isInTrial(trialEndsAt) {
if (!trialEndsAt) {
return false

View File

@@ -25,6 +25,11 @@ const rateLimiters = {
}),
}
/**
* @param {any} req
* @param {any} res
* @param {any} next
*/
async function createInvite(req, res, next) {
const teamManagerId = SessionManager.getLoggedInUserId(req.session)
const subscription = req.entity
@@ -70,6 +75,11 @@ async function createInvite(req, res, next) {
}
}
/**
* @param {any} req
* @param {any} res
* @param {any} next
*/
async function viewInvite(req, res, next) {
const { token } = req.params
const sessionUser = SessionManager.getSessionUser(req.session)
@@ -186,6 +196,11 @@ async function viewInvite(req, res, next) {
}
}
/**
* @param {any} req
* @param {any} res
* @param {any} next
*/
async function viewInvites(req, res, next) {
const user = SessionManager.getSessionUser(req.session)
const groupSubscriptions =
@@ -201,6 +216,11 @@ async function viewInvites(req, res, next) {
})
}
/**
* @param {any} req
* @param {any} res
* @param {any} next
*/
async function acceptInvite(req, res, next) {
const { token } = req.params
const userId = SessionManager.getLoggedInUserId(req.session)
@@ -232,6 +252,11 @@ async function acceptInvite(req, res, next) {
res.json({ groupSSOActive })
}
/**
* @param {any} req
* @param {any} res
* @param {any} next
*/
function revokeInvite(req, res, next) {
const subscription = req.entity
const email = EmailHelper.parseEmail(req.params.email)
@@ -244,6 +269,10 @@ function revokeInvite(req, res, next) {
teamManagerId,
subscription,
email,
/**
* @param {any} err
* @param {any} results
*/
function (err, results) {
if (err) {
return next(err)
@@ -253,6 +282,11 @@ function revokeInvite(req, res, next) {
)
}
/**
* @param {any} req
* @param {any} res
* @param {any} next
*/
async function resendInvite(req, res, next) {
const { entity: subscription } = req
const userEmail = EmailHelper.parseEmail(req.body.email)

View File

@@ -17,12 +17,17 @@ const FILE_IGNORE_MATCHER = new Minimatch(Settings.fileIgnorePattern, {
dot: true,
})
const TEXT_EXTENSIONS = new Set(Settings.textExtensions.map(ext => `.${ext}`))
const TEXT_EXTENSIONS = new Set(
Settings.textExtensions.map((/** @type {string} */ ext) => `.${ext}`)
)
const EDITABLE_FILENAMES = Settings.editableFilenames
// allow 3 bytes for every character
const MAX_TEXT_FILE_SIZE = 3 * Settings.max_doc_length
/**
* @param {string} path
*/
async function isDirectory(path) {
const stats = await fs.stat(path)
return stats.isDirectory()
@@ -86,11 +91,17 @@ async function getType(name, fsPath, existingFileType) {
}
}
/**
* @param {string} path
*/
function shouldIgnore(path) {
// use minimatch file matching to check if the path should be ignored
return FILE_IGNORE_MATCHER.match(path)
}
/**
* @param {string} filename
*/
function _isTextFilename(filename) {
const basename = Path.basename(filename)
const extension = Path.extname(filename).toLowerCase()
@@ -100,6 +111,9 @@ function _isTextFilename(filename) {
)
}
/**
* @param {Buffer<ArrayBufferLike>} bytes
*/
function _detectEncoding(bytes) {
if (isUtf8(bytes)) {
return 'utf-8'

View File

@@ -28,6 +28,11 @@ const upload = multer(
)
)
/**
* @param {any} req
* @param {any} res
* @param {any} next
*/
function uploadProject(req, res, next) {
const timer = new metrics.Timer('project-upload')
const userId = SessionManager.getLoggedInUserId(req.session)
@@ -63,6 +68,11 @@ function uploadProject(req, res, next) {
)
}
/**
* @param {any} req
* @param {any} res
* @param {any} next
*/
async function uploadFile(req, res, next) {
const timer = new metrics.Timer('file-upload')
const name = req.body.name
@@ -156,27 +166,36 @@ async function uploadFile(req, res, next) {
)
}
/**
* @param {any} req
* @param {any} res
* @param {any} next
*/
function multerMiddleware(req, res, next) {
if (upload == null) {
return res
.status(500)
.json({ success: false, error: req.i18n.translate('upload_failed') })
}
return upload.single('qqfile')(req, res, function (err) {
if (err instanceof multer.MulterError && err.code === 'LIMIT_FILE_SIZE') {
return res
.status(422)
.json({ success: false, error: req.i18n.translate('file_too_large') })
return upload.single('qqfile')(
req,
res,
/** @param {any} err */ function (err) {
if (err instanceof multer.MulterError && err.code === 'LIMIT_FILE_SIZE') {
return res
.status(422)
.json({ success: false, error: req.i18n.translate('file_too_large') })
}
if (err) return next(err)
if (!req.file?.path) {
logger.info({ req }, 'missing req.file.path on upload')
return res
.status(400)
.json({ success: false, error: 'invalid_upload_request' })
}
next()
}
if (err) return next(err)
if (!req.file?.path) {
logger.info({ req }, 'missing req.file.path on upload')
return res
.status(400)
.json({ success: false, error: 'invalid_upload_request' })
}
next()
})
)
}
export default {

View File

@@ -274,6 +274,24 @@ const UserGetter = {
// check for duplicate email address. This is also enforced at the DB level
ensureUniqueEmailAddress: callbackify(ensureUniqueEmailAddress),
getWritefullData: callbackify(getWritefullData),
promises: {
getSsoUsersAtInstitution,
getUser,
getUserFeatures,
getUserEmail,
getUserFullEmails,
getUserConfirmedEmails,
getUserByMainEmail,
getUserByAnyEmail,
getUsersByAnyConfirmedEmail,
getUsersByV1Ids,
getUsersByHostname,
getInstitutionUsersByHostname,
getUsers,
ensureUniqueEmailAddress,
getWritefullData,
},
}
const decorateFullEmails = (
@@ -352,22 +370,4 @@ const decorateFullEmails = (
return emailsData
}
UserGetter.promises = {
getSsoUsersAtInstitution,
getUser,
getUserFeatures,
getUserEmail,
getUserFullEmails,
getUserConfirmedEmails,
getUserByMainEmail,
getUserByAnyEmail,
getUsersByAnyConfirmedEmail,
getUsersByV1Ids,
getUsersByHostname,
getInstitutionUsersByHostname,
getUsers,
ensureUniqueEmailAddress,
getWritefullData,
}
export default UserGetter

View File

@@ -36,7 +36,12 @@ const UserMembershipMiddleware = {
requireEntity(),
],
requireEntityAccess: ({ entityName, adminCapability }) => [
requireEntityAccess: (
/** @type {{ entityName: any; adminCapability?: any }} */ {
entityName,
adminCapability,
}
) => [
AuthenticationController.requireLogin(),
fetchEntityConfig(entityName),
fetchEntity(),
@@ -50,7 +55,7 @@ const UserMembershipMiddleware = {
),
],
requireEntityAccessOrAdminAccess: entityName => [
requireEntityAccessOrAdminAccess: (/** @type {any} */ entityName) => [
AuthenticationController.requireLogin(),
fetchEntityConfig(entityName),
fetchEntity(),
@@ -61,7 +66,7 @@ const UserMembershipMiddleware = {
]),
],
requireGroupMemberManagement: entityName => [
requireGroupMemberManagement: (/** @type {any} */ entityName) => [
AuthenticationController.requireLogin(),
fetchEntityConfig(entityName),
fetchEntity(),
@@ -223,10 +228,20 @@ const UserMembershipMiddleware = {
export default UserMembershipMiddleware
// fetch entity config and set it in the request
/**
* fetch entity config and set it in the request
*
* @param {any} entityName
*/
function fetchEntityConfig(entityName) {
return (req, res, next) => {
const entityConfig = EntityConfigs[entityName]
return (
/** @type {any} */ req,
/** @type {any} */ res,
/** @type {any} */ next
) => {
const entityConfig = /** @type {Record<string, any>} */ (EntityConfigs)[
entityName
]
req.entityName = entityName
req.entityConfig = entityConfig
next()
@@ -271,6 +286,11 @@ const fetchEntitySchema = z.discriminatedUnion('entityName', [
// `req.params.id`
// - the entity name is in `req.query.resource_type` and is used to find the
// require middleware depending on the entity name
/**
* @param {any} req
* @param {any} res
* @param {any} next
*/
function requireGraphAccess(req, res, next) {
const entityName = req.query.resource_type
if (!entityName) {
@@ -279,8 +299,9 @@ function requireGraphAccess(req, res, next) {
const middleWareName =
entityName.charAt(0).toUpperCase() + entityName.slice(1)
const middlewares =
UserMembershipMiddleware[`require${middleWareName}MetricsAccess`]
const middlewares = /** @type {Record<string, any>} */ (
UserMembershipMiddleware
)[`require${middleWareName}MetricsAccess`]
if (!middlewares) {
return HttpErrorHandler.notFound(
req,
@@ -305,19 +326,29 @@ function requireGraphAccess(req, res, next) {
// fetch the entity with id and config, and set it in the request
function fetchEntity() {
return expressify(async (req, res, next) => {
const { params } = parseReq(req, fetchEntitySchema)
req.entity =
await UserMembershipHandler.promises.getEntityWithoutAuthorizationCheck(
params.id,
req.entityConfig
)
next()
})
return expressify(
async (
/** @type {any} */ req,
/** @type {any} */ res,
/** @type {any} */ next
) => {
const { params } = parseReq(req, fetchEntitySchema)
req.entity =
await UserMembershipHandler.promises.getEntityWithoutAuthorizationCheck(
params.id,
req.entityConfig
)
next()
}
)
}
function fetchPublisherFromTemplate() {
return (req, res, next) => {
return (
/** @type {any} */ req,
/** @type {any} */ res,
/** @type {any} */ next
) => {
if (req.template.brand.slug) {
// set the id as the publisher's id as it's the entity used for access
// control
@@ -331,7 +362,11 @@ function fetchPublisherFromTemplate() {
// ensure an entity was found, or fail with 404
function requireEntity() {
return (req, res, next) => {
return (
/** @type {any} */ req,
/** @type {any} */ res,
/** @type {any} */ next
) => {
if (req.entity) {
return next()
}
@@ -342,10 +377,16 @@ function requireEntity() {
}
}
// ensure an entity was found or redirect to entity creation page if the user
// has permissions to create the entity, or fail with 404
/**
* ensure an entity was found or redirect to entity creation page if the user
* has permissions to create the entity, or fail with 404
*/
function requireEntityOrCreate() {
return (req, res, next) => {
return (
/** @type {any} */ req,
/** @type {any} */ res,
/** @type {any} */ next
) => {
if (req.entity) {
return next()
}
@@ -363,21 +404,31 @@ function requireEntityOrCreate() {
// fetch the template from v1, and set it in the request
function fetchV1Template() {
return expressify(async (req, res, next) => {
const templateId = req.params.id
const body = await TemplatesManager.promises.fetchFromV1(templateId)
req.template = {
id: body.id,
title: body.title,
brand: body.brand,
return expressify(
async (
/** @type {any} */ req,
/** @type {any} */ res,
/** @type {any} */ next
) => {
const templateId = req.params.id
const body = await TemplatesManager.promises.fetchFromV1(templateId)
req.template = {
id: body.id,
title: body.title,
brand: body.brand,
}
next()
}
next()
})
)
}
// ensure a template was found, or fail with 404
function requireV1Template() {
return (req, res, next) => {
return (
/** @type {any} */ req,
/** @type {any} */ res,
/** @type {any} */ next
) => {
if (req.template.id) {
return next()
}
@@ -386,10 +437,18 @@ function requireV1Template() {
}
}
// run a serie of synchronous access functions and call `next` if any of the
// retur values is truly. Redirect to restricted otherwise
/**
* run a series of synchronous access functions and call `next` if any of the
* return values is truly. Redirect to restricted otherwise
*
* @param {any} accessFunctions
*/
function allowAccessIfAny(accessFunctions) {
return (req, res, next) => {
return (
/** @type {any} */ req,
/** @type {any} */ res,
/** @type {any} */ next
) => {
for (const accessFunction of accessFunctions) {
if (accessFunction(req)) {
return next()

View File

@@ -2,8 +2,8 @@
import { AsyncLocalStorage } from 'node:async_hooks'
/**
* @typedef {Object} RequestContext
* @property {Object.<string, array>} [userFullEmails] - Dictionary mapping userId to an array of full emails
* @typedef {Record<string, any>} RequestContext
* @property {Record<string, any[]>} [userFullEmails] - Dictionary mapping userId to an array of full emails
*/
/** @type {AsyncLocalStorage<RequestContext>} */

View File

@@ -73,6 +73,9 @@ for (const country of EuroCountries) {
currencyMappings[country] = 'EUR'
}
/**
* @param {any} currency
*/
function isValidCurrencyParam(currency) {
if (!currency) {
return false
@@ -80,6 +83,10 @@ function isValidCurrencyParam(currency) {
return validCurrencyParams.includes(currency)
}
/**
* @param {any} ip
* @param {any} [callback]
*/
async function getDetails(ip, callback) {
if (!ip) {
return callback(new Error('no ip passed'))
@@ -92,6 +99,7 @@ async function getDetails(ip, callback) {
}
/**
* @param {any} ip
* @returns {Promise<{currencyCode: CurrencyCode, countryCode: string|undefined}>}
*/
async function getCurrencyCode(ip) {

View File

@@ -72,11 +72,16 @@ class HttpPermissionsPolicyMiddleware {
return policyElements.join(', ')
}
/**
* @param {any} req
* @param {any} res
* @param {any} next
*/
middleware(req, res, next) {
if (this.policy && Settings.useHttpPermissionsPolicy) {
const originalRender = res.render
res.render = (...args) => {
res.render = (/** @type {any} */ ...args) => {
res.setHeader('Permissions-Policy', this.policy)
originalRender.apply(res, args)
}

View File

@@ -17,10 +17,12 @@ const MODULE_BASE_PATH = Path.join(import.meta.dirname, '/../../../modules')
/** @type {WebModule[]} */
const _modules = []
let _modulesLoaded = false
/** @type {Record<string, any>} */
const _hooks = {}
/** @type {Record<string, RequestHandler[]>} */
const _middleware = {}
/** @type {Record<string, any>} */
let _viewIncludes = {}
async function modules() {
@@ -74,6 +76,11 @@ async function loadModulesImpl() {
const loadModules = _.memoize(loadModulesImpl)
/**
* @param {any} webRouter
* @param {any} privateApiRouter
* @param {any} publicApiRouter
*/
async function applyRouter(webRouter, privateApiRouter, publicApiRouter) {
for (const module of await modules()) {
if (module.router && module.router.apply) {
@@ -82,6 +89,11 @@ async function applyRouter(webRouter, privateApiRouter, publicApiRouter) {
}
}
/**
* @param {any} webRouter
* @param {any} privateApiRouter
* @param {any} publicApiRouter
*/
async function applyNonCsrfRouter(
webRouter,
privateApiRouter,
@@ -107,10 +119,18 @@ async function start() {
}
}
/**
* @param {any} app
*/
function loadViewIncludes(app) {
_viewIncludes = Views.compileViewIncludes(app)
}
/**
* @param {any} appOrRouter
* @param {any} middlewareName
* @param {any} [options]
*/
async function applyMiddleware(appOrRouter, middlewareName, options) {
if (!middlewareName) {
throw new Error(
@@ -118,30 +138,42 @@ async function applyMiddleware(appOrRouter, middlewareName, options) {
)
}
for (const module of await modules()) {
if (module[middlewareName]) {
module[middlewareName](appOrRouter, options)
/** @type {Record<string, any>} */
const typedModule = module
if (typedModule[middlewareName]) {
typedModule[middlewareName](appOrRouter, options)
}
}
}
/**
* @param {any} view
* @param {any} locals
*/
function moduleIncludes(view, locals) {
const compiledPartials = _viewIncludes[view] || []
let html = ''
for (const compiledPartial of compiledPartials) {
for (const /** @type {any} */ compiledPartial of compiledPartials) {
html += compiledPartial(locals)
}
return html
}
/**
* @param {any} view
*/
function moduleIncludesAvailable(view) {
return (_viewIncludes[view] || []).length > 0
}
async function linkedFileAgentsIncludes() {
/** @type {Record<string, any>} */
const agents = {}
for (const module of await modules()) {
for (const name in module.linkedFileAgents) {
const agentFunction = module.linkedFileAgents[name]
const agentFunction = /** @type {Record<string, any>} */ (
module.linkedFileAgents
)[name]
agents[name] = agentFunction()
}
}
@@ -155,12 +187,16 @@ async function attachHooks() {
attachHook(hook, method)
}
for (const hook in hooks || {}) {
const method = hooks[hook]
const method = /** @type {Record<string, any>} */ (hooks)[hook]
attachHook(hook, promisify(method))
}
}
}
/**
* @param {any} name
* @param {any} method
*/
function attachHook(name, method) {
if (_hooks[name] == null) {
_hooks[name] = []
@@ -172,7 +208,9 @@ async function attachMiddleware() {
for (const module of await modules()) {
if (module.middleware) {
for (const middleware in module.middleware) {
const method = module.middleware[middleware]
const method = /** @type {Record<string, any>} */ (module.middleware)[
middleware
]
if (_middleware[middleware] == null) {
_middleware[middleware] = []
}
@@ -182,6 +220,10 @@ async function attachMiddleware() {
}
}
/**
* @param {any} name
* @param {...any} args
*/
async function fireHook(name, ...args) {
// ensure that modules are loaded if we need to fire a hook
// this can happen if a script calls a method that fires a hook

View File

@@ -11,10 +11,14 @@ import {
export { z, zz } from '@overleaf/validation-tools'
/**
* @param {any} req
* @param {any} schema
*/
export const parseReq = (req, schema) => {
try {
return parseReqBase(req, schema)
} catch (err) {
} catch (/** @type {any} */ err) {
if (err instanceof ParamsError) {
// convert into a NotFoundError that web understands
throw new NotFoundError('Not found').withCause(err)

View File

@@ -88,7 +88,12 @@ export default class FeatureUsageRateLimiter {
upsert: true,
}
).exec()
const featureUsage = featureUsages.features?.[this.featureName] ?? {}
const featureUsage =
/** @type {Record<string, any>} */ (featureUsages.features ?? {})[
this.featureName
] ?? {}
setRateLimitHeaders(res, featureUsage, allowance)
this._checkRateLimit(featureUsage, allowance)
}
@@ -118,7 +123,10 @@ export default class FeatureUsageRateLimiter {
}
).exec()
const featureUsage = featureUsages.features?.[this.featureName] ?? {}
const featureUsage =
/** @type {Record<string, any>} */ (featureUsages.features ?? {})[
this.featureName
] ?? {}
setRateLimitHeaders(res, featureUsage, allowance)
}
@@ -129,7 +137,10 @@ export default class FeatureUsageRateLimiter {
async getRemainingFeatureUses(userId) {
const allowance = await this._getAllowance(userId)
const reportedUsage = await UserFeatureUsage.findOne({ _id: userId }).exec()
const featureUsage = reportedUsage?.features?.[this.featureName] ?? {}
const featureUsage =
/** @type {Record<string, any>} */ (reportedUsage?.features ?? {})[
this.featureName
] ?? {}
const periodStart = featureUsage.periodStart ?? new Date()
const usage = featureUsage.usage ?? 0
const usesLeft = allowance - usage

View File

@@ -57,9 +57,15 @@ export default class TokenUsageRateLimiter {
throw new Error('_getAllowance must be implemented by subclasses')
}
/**
* @param {any} userId
* @param {any} res
* @param {any} amount
*/
async recordUsage(userId, res, amount) {
const allowance = await this._getAllowance(userId)
/** @type {any} */
const featureUsages = await UserFeatureUsage.findOneAndUpdate(
{ _id: userId },
[
@@ -93,7 +99,8 @@ export default class TokenUsageRateLimiter {
*/
async getCurrentUsage(userId) {
const reportedUsage = await UserFeatureUsage.findOne({ _id: userId }).exec()
const featureUsage = reportedUsage?.features?.[this.featureName] ?? {}
const featureUsage =
/** @type {any} */ (reportedUsage)?.features?.[this.featureName] ?? {}
return {
usage: featureUsage.usage ?? 0,
periodStart: featureUsage.periodStart ?? new Date(),
@@ -112,7 +119,8 @@ export default class TokenUsageRateLimiter {
async getRemainingTokens(userId) {
const allowance = await this._getAllowance(userId)
const reportedUsage = await UserFeatureUsage.findOne({ _id: userId }).exec()
const featureUsage = reportedUsage?.features?.[this.featureName] ?? {}
const featureUsage =
/** @type {any} */ (reportedUsage)?.features?.[this.featureName] ?? {}
const periodStart = featureUsage.periodStart ?? new Date()
const usage = featureUsage.usage ?? 0
const usesLeft = allowance - usage

View File

@@ -1,3 +1,5 @@
@use 'sass:list';
// Button
@mixin ol-button-size(
@@ -105,7 +107,7 @@
}
@mixin theme($name) {
@if index($themes, $name) {
@if list.index($themes, $name) {
[data-theme='#{$name}'] {
@content;
}

View File

@@ -253,6 +253,7 @@
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^14.4.3",
"@types/algoliasearch": "^3.34.11",
"@types/async": "^3.2.25",
"@types/bootstrap": "^5.2.10",
"@types/chai": "^4.3.0",
"@types/dateformat": "^5.0.3",
@@ -260,6 +261,7 @@
"@types/dom-speech-recognition": "^0.0.7",
"@types/events": "^3.0.3",
"@types/express": "^4.17.23",
"@types/minimist": "^1.2.5",
"@types/mocha": "^9.1.0",
"@types/mocha-each": "^2.0.4",
"@types/node": "^24.5.2",
@@ -270,7 +272,9 @@
"@types/react-linkify": "^1.0.4",
"@types/react-syntax-highlighter": "^15.5.11",
"@types/recurly__recurly-js": "^4.38.0",
"@types/sanitize-html": "^2.14.0",
"@types/sinon-chai": "^3.2.12",
"@types/utf-8-validate": "^5.0.2",
"@types/uuid": "^9.0.8",
"@uppy/core": "^3.8.0",
"@uppy/dashboard": "^3.7.1",

View File

@@ -65,6 +65,10 @@ async function getDanglingThreads(projectId) {
return danglingThreads
}
/**
* @param {any} projectId
* @param {any} docId
*/
const ensureDocExists = async (projectId, docId) => {
const doc = await DocstoreManager.promises.getDoc(projectId, docId)
if (!doc) {

View File

@@ -13,6 +13,9 @@ const WRITE_CONCURRENCY = parseInt(process.env.WRITE_CONCURRENCY || '10', 10)
const mixpanelSinkQueue = getQueue('analytics-mixpanel-sink')
/**
* @param {any} user
*/
async function processUser(user) {
const analyticsId = user.analyticsId || user._id
@@ -58,6 +61,9 @@ async function processUser(user) {
}
}
/**
* @param {any} userId
*/
async function _getGroupSubscriptionPlanCode(userId) {
const subscriptions =
await SubscriptionLocator.promises.getMemberSubscriptions(userId)
@@ -77,6 +83,12 @@ async function _getGroupSubscriptionPlanCode(userId) {
return bestPlanCode
}
/**
* @param {any} analyticsId
* @param {any} propertyName
* @param {any} propertyValue
* @param {any} [createdAt]
*/
async function _sendPropertyToQueue(
analyticsId,
propertyName,
@@ -94,6 +106,10 @@ async function _sendPropertyToQueue(
})
}
/**
* @param {any} _
* @param {any} users
*/
async function processBatch(_, users) {
await promiseMapWithLimit(WRITE_CONCURRENCY, users, async user => {
await processUser(user)

View File

@@ -149,6 +149,9 @@ function getProjectIds() {
.map(x => x._id.toString())
}
/**
* @param {any} projectId
*/
async function getDocs(projectId) {
const mongoDocs = db.docs.find(
{
@@ -192,6 +195,10 @@ async function getDocs(projectId) {
return docs
}
/**
* @param {any} projectId
* @param {any} docs
*/
async function findDanglingThreadIds(projectId, docs) {
const threadIds = new Set()
for (const doc of docs) {
@@ -219,6 +226,9 @@ async function findDanglingThreadIds(projectId, docs) {
return Array.from(threadIds)
}
/**
* @param {any} docs
*/
function docsHaveTrackedChanges(docs) {
for (const doc of docs) {
const changes = doc.ranges?.changes ?? []
@@ -229,6 +239,9 @@ function docsHaveTrackedChanges(docs) {
return false
}
/**
* @param {any} docs
*/
function docsHaveAnyComments(docs) {
for (const doc of docs) {
const comments = doc.ranges?.comments ?? []

View File

@@ -48,6 +48,9 @@ async function main(trackProgress) {
)
}
/**
* @param {any} project
*/
async function processProject(project) {
if (DEBUG) {
console.log(

View File

@@ -29,6 +29,9 @@ function parseArgs() {
}
}
/**
* @param {any} projectId
*/
async function processProject(projectId) {
console.log(`Processing project ${projectId}...`)
await DocumentUpdaterHandler.promises.flushProjectToMongoAndDelete(projectId)
@@ -49,6 +52,11 @@ async function processProject(projectId) {
}
}
/**
* @param {any} projectId
* @param {any} doc
* @param {any} threadIds
*/
async function processDoc(projectId, doc, threadIds) {
let commentsDeleted = 0
for (const comment of doc.ranges?.comments ?? []) {
@@ -66,6 +74,10 @@ async function processDoc(projectId, doc, threadIds) {
return commentsDeleted
}
/**
* @param {any} docId
* @param {any} threadId
*/
async function deleteComment(docId, threadId) {
await db.docs.updateOne(
{ _id: new ObjectId(docId) },

View File

@@ -24,6 +24,9 @@ function parseArgs() {
}
}
/**
* @param {any} projectId
*/
async function processProject(projectId) {
console.log(`Processing project ${projectId}...`)
const docRanges = await DocstoreManager.promises.getAllRanges(projectId)
@@ -38,6 +41,9 @@ async function processProject(projectId) {
}
}
/**
* @param {any} doc
*/
async function processDoc(doc) {
let commentsUpdated = 0
for (const comment of doc.ranges.comments ?? []) {

View File

@@ -42,6 +42,9 @@ if (!doNotListUsers) {
*/
const parseAsync = promisify(csv.parse)
/**
* @param {any} userId
*/
async function getV1Affiliations(userId) {
const url = `${Settings.apis.v1.url}/api/v2/users/${userId}/affiliations`
@@ -56,6 +59,10 @@ async function getV1Affiliations(userId) {
return affiliations
}
/**
* @param {any} userId
* @param {any} email
*/
async function removeAffiliationV1(userId, email) {
const url = `${Settings.apis.v1.url}/api/v2/users/${userId}/affiliations/remove`
@@ -92,6 +99,9 @@ const results = {
errorRemovingAffiliationInV1: [],
}
/**
* @param {any} trackProgress
*/
async function main(trackProgress) {
console.time('check_removed_emails')
@@ -99,6 +109,7 @@ async function main(trackProgress) {
const csvContent = await fs.readFile(filePath, 'utf8')
const rows = await parseAsync(csvContent)
rows.shift() // Remove header row
/** @type {Record<string, string[]>} */
const emailsByUserId = {}
for (const [userId, email] of rows) {
@@ -124,12 +135,12 @@ async function main(trackProgress) {
// nothing to cleanup in v1 if no affiliations for the user
continue
}
} catch (e) {
} catch (/** @type {any} */ e) {
results.errorCheckingAffiliations.push(userId)
}
const affiliationsEmailsInV1 = affiliations.map(
affiliation => affiliation.email
(/** @type {any} */ affiliation) => affiliation.email
)
const user = await db.users.findOne(
@@ -151,7 +162,9 @@ async function main(trackProgress) {
continue
}
const emailOnAccount = user?.emails?.find(e => e.email === email)
const emailOnAccount = user?.emails?.find(
(/** @type {any} */ e) => e.email === email
)
if (emailOnAccount) {
// the email is still on the user account, we should not remove the affiliation in v1
@@ -181,7 +194,7 @@ async function main(trackProgress) {
// remove the affiliation in v1
await removeAffiliationV1(userId, email)
results.successfullyRemovedEmailInV1ForUser.push({ userId, email })
} catch (e) {
} catch (/** @type {any} */ e) {
results.errorRemovingAffiliationInV1.push({
userId,
email,
@@ -200,18 +213,18 @@ async function main(trackProgress) {
console.log('Results:')
for (const key in results) {
console.log(` ${key}:`, results[key].length)
console.log(` ${key}:`, /** @type {any} */ (results)[key].length)
}
for (const key in results) {
if (
!doNotListUsers &&
results[key].length > 0 &&
/** @type {any} */ (results)[key].length > 0 &&
key !== 'needToRemoveEmailInV1'
) {
// skip needToRemoveEmailInV1 since we'll only output that if this list length does not match success list length
console.log('----------------------------')
console.log(`${key}:`)
console.log(results[key])
console.log(/** @type {any} */ (results)[key])
}
}

View File

@@ -14,6 +14,7 @@ async function main() {
process.exit(1)
}
/** @type {Record<string, any>} */
const localizedAddOnsPricing = {}
const monthlyPlan = await getPlan(ADD_ON_CODE)
@@ -22,10 +23,12 @@ async function main() {
process.exit(1)
}
for (const { currency, unitAmount } of monthlyPlan.currencies ?? []) {
if (!localizedAddOnsPricing[currency]) {
localizedAddOnsPricing[currency] = { [ADD_ON_CODE]: {} }
/** @type {any} */
const curr = currency
if (!localizedAddOnsPricing[curr]) {
localizedAddOnsPricing[curr] = { [ADD_ON_CODE]: {} }
}
localizedAddOnsPricing[currency][ADD_ON_CODE].monthly = unitAmount
localizedAddOnsPricing[curr][ADD_ON_CODE].monthly = unitAmount
}
const annualPlan = await getPlan(`${ADD_ON_CODE}-annual`)
@@ -34,11 +37,13 @@ async function main() {
process.exit(1)
}
for (const { currency, unitAmount } of annualPlan.currencies ?? []) {
if (!localizedAddOnsPricing[currency]) {
localizedAddOnsPricing[currency] = { [ADD_ON_CODE]: {} }
/** @type {any} */
const curr = currency
if (!localizedAddOnsPricing[curr]) {
localizedAddOnsPricing[curr] = { [ADD_ON_CODE]: {} }
}
localizedAddOnsPricing[currency][ADD_ON_CODE].annual = unitAmount
localizedAddOnsPricing[currency][ADD_ON_CODE].annualDividedByTwelve =
localizedAddOnsPricing[curr][ADD_ON_CODE].annual = unitAmount
localizedAddOnsPricing[curr][ADD_ON_CODE].annualDividedByTwelve =
(unitAmount || 0) / 12
}

View File

@@ -73,6 +73,9 @@ function usage() {
where FILE contains the list of subscription ids that are manually collected`)
}
/**
* @param {any} filename
*/
function readFile(filename) {
const contents = fs.readFileSync(filename, { encoding: 'utf-8' })
const subscriptionIds = contents.split('\n').filter(id => id.length > 0)

View File

@@ -62,6 +62,7 @@ async function getExistingPrices(stripe) {
let startingAfter
do {
/** @type {any} */
const response = await stripe.prices.list({
limit: 100,
starting_after: startingAfter,
@@ -89,6 +90,7 @@ async function getExistingProducts(stripe) {
let startingAfter
do {
/** @type {any} */
const response = await stripe.products.list({
limit: 100,
starting_after: startingAfter,
@@ -104,6 +106,9 @@ async function getExistingProducts(stripe) {
return productsById
}
/**
* @param {any} trackProgress
*/
export async function main(trackProgress) {
const args = minimist(process.argv.slice(2), {
boolean: ['commit'],
@@ -175,7 +180,10 @@ export async function main(trackProgress) {
record.productName ||
planCode
.split(/[_-]/) // Handle underscores or hyphens
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.map(
/** @param {any} word */
word => word.charAt(0).toUpperCase() + word.slice(1)
)
.join(' ')
if (commit) {
@@ -203,7 +211,7 @@ export async function main(trackProgress) {
// 2. Handle Prices for each currency column
for (const currency of currencyKeys) {
const amountValue = parseFloat(record[currency])
const amountValue = parseFloat(/** @type {any} */ (record)[currency])
if (isNaN(amountValue) || amountValue <= 0) continue
const currencyLower = currency.toLowerCase()

View File

@@ -1,5 +1,8 @@
{
"extends": "../../tsconfig.backend.json",
"compilerOptions": {
"noImplicitAny": true
},
"include": [
"app/src/**/*",
"modules/*/app/src/**/*",