diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index 0447bce75..3700a5280 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -19,7 +19,7 @@ jobs: fetch-depth: 0 - uses: actions/setup-node@v4 with: - node-version: '22' + node-version: '24' cache: yarn - name: install dependencies run: yarn install --frozen-lockfile --network-concurrency 1 diff --git a/.github/workflows/tests-kmip.yaml b/.github/workflows/tests-kmip.yaml index c93f36285..d27ff0f60 100644 --- a/.github/workflows/tests-kmip.yaml +++ b/.github/workflows/tests-kmip.yaml @@ -31,7 +31,7 @@ jobs: uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version: '22' + node-version: '24' cache: 'yarn' - name: Install socat (tunnel to help create disconnect and reconnect to pykmip) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index f41c15200..682cb3f3d 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -9,6 +9,12 @@ on: jobs: test: runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + # 24 is the target runtime; 22 is what engines.node still allows and + # what cloudserver/backbeat/scuba run until they move over. + node-version: ['22', '24'] services: # Label used to access the service container redis: @@ -28,7 +34,7 @@ jobs: uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version: '22' + node-version: ${{ matrix.node-version }} cache: 'yarn' - name: install dependencies run: yarn install --frozen-lockfile --prefer-offline --network-concurrency 1 @@ -46,6 +52,7 @@ jobs: - name: run functional tests run: yarn ft_test - uses: codecov/codecov-action@v4 + if: matrix.node-version == '24' with: token: ${{ secrets.CODECOV_TOKEN }} - name: run executables tests @@ -62,7 +69,7 @@ jobs: - name: Install NodeJS uses: actions/setup-node@v4 with: - node-version: '22' + node-version: '24' cache: yarn - name: Install dependencies run: yarn install --frozen-lockfile --prefer-offline diff --git a/documentation/BucketInfoModelVersion.md b/documentation/BucketInfoModelVersion.md index c2ab8d311..9d8065b26 100644 --- a/documentation/BucketInfoModelVersion.md +++ b/documentation/BucketInfoModelVersion.md @@ -4,30 +4,31 @@ - v7 to v11 can mean different features between S3C & Zenko. - Avoid using model version to check features - - used by model < v2 (for splitter) - - used in some tests (v5 and v10) - -| Version | S3C (dev/7.X) | Zenko (dev/8.X) | Status | -|---------|-------------------------|--------------------------|-------------| -| v2 - v6 | ✅ | ✅ | ✅ Common | -| v7 | ObjectLock | UID | ❌ Collision| -| v8 | BucketNotification | ReadLocationConstraints | ❌ Collision| -| v9 | SSEConfiguredMasterKey | isNFS | ❌ Collision| -| v10 | UID | Ingestion | ❌ Collision| -| v11 | Tags | AzureInfo | ❌ Collision| -| v12 | - | ObjectLock | ↪️ Forward | -| v13 | - | BucketNotification | ↪️ Forward | -| v14 | - | SSEConfiguredMasterKey | ↪️ Forward | -| v15 | - | Tags | ↪️ Forward | -| v16 | - | Capabilities(VeeamSOSApi)| ✅ New | -| v17 | - | QuotaMax | ✅ New | -| **Unified at v17** | + - used by model < v2 (for splitter) + - used in some tests (v5 and v10) + +| Version | S3C (dev/7.X) | Zenko (dev/8.X) | Status | +| ------- | ---------------------- | ------------------------- | ------------ | +| v2 - v6 | ✅ | ✅ | ✅ Common | +| v7 | ObjectLock | UID | ❌ Collision | +| v8 | BucketNotification | ReadLocationConstraints | ❌ Collision | +| v9 | SSEConfiguredMasterKey | isNFS | ❌ Collision | +| v10 | UID | Ingestion | ❌ Collision | +| v11 | Tags | AzureInfo | ❌ Collision | +| v12 | - | ObjectLock | ↪️ Forward | +| v13 | - | BucketNotification | ↪️ Forward | +| v14 | - | SSEConfiguredMasterKey | ↪️ Forward | +| v15 | - | Tags | ↪️ Forward | +| v16 | - | Capabilities(VeeamSOSApi) | ✅ New | +| v17 | - | QuotaMax | ✅ New | + +Model versions are unified across S3C and Zenko starting at v17. ## Model Version 0/1 ### Properties -``` javascript +```javascript this._acl = aclInstance; this._name = name; this._owner = owner; @@ -44,8 +45,8 @@ property not added until Model Version 2 ### Properties Added -``` javascript -this._mdBucketModelVersion = mdBucketModelVersion || 0 +```javascript +this._mdBucketModelVersion = mdBucketModelVersion || 0; this._transient = transient || false; this._deleted = deleted || false; ``` @@ -216,9 +217,9 @@ The Tag Set of a bucket is an array of objects with Key and Value: [ { Key: 'something', - Value: 'some_data' - } -] + Value: 'some_data', + }, +]; ``` ### Usage diff --git a/index.ts b/index.ts index 6ae4db40f..0c1632399 100644 --- a/index.ts +++ b/index.ts @@ -40,6 +40,7 @@ export * as s3routes from './lib/s3routes'; export * as versioning from './lib/versioning'; export * as stream from './lib/stream'; export * as jsutil from './lib/jsutil'; +export * as requestUrl from './lib/utils/requestUrl'; export { default as stringHash } from './lib/stringHash'; export * as db from './lib/db'; export * as errorUtils from './lib/errorUtils'; diff --git a/lib/auth/v2/getCanonicalizedResource.ts b/lib/auth/v2/getCanonicalizedResource.ts index 0d951df9b..cbde01a4d 100644 --- a/lib/auth/v2/getCanonicalizedResource.ts +++ b/lib/auth/v2/getCanonicalizedResource.ts @@ -1,4 +1,4 @@ -import * as url from 'url'; +import { parseRequestTarget } from '../../utils/requestUrl'; import { type ArsenalRequest } from '../../types/ArsenalRequest'; const gcpSubresources = new Set([ @@ -50,7 +50,7 @@ export default function getCanonicalizedResource(request: ArsenalRequest, client // If bucket specified in hostname, add to resourceString let resourceString = request.gotBucketNameFromHost ? `/${request.bucketName}` : ''; // Add the path to the resourceString - resourceString += url.parse(request.url || '').pathname || ''; + resourceString += parseRequestTarget(request.url || '').pathname; /* If request includes a specified subresource, diff --git a/lib/network/rest/RESTServer.ts b/lib/network/rest/RESTServer.ts index 15f442050..4efe3e1cb 100644 --- a/lib/network/rest/RESTServer.ts +++ b/lib/network/rest/RESTServer.ts @@ -1,10 +1,10 @@ import assert from 'assert'; -import * as url from 'url'; import * as werelogs from 'werelogs'; import * as http from 'http'; import httpServer from '../http/server'; import * as constants from '../../constants'; import { parseURL } from './utils'; +import { parseRequestTarget } from '../../utils/requestUrl'; import * as httpUtils from '../http/utils'; import errors, { ArsenalError, errorInstances } from '../../errors'; @@ -19,23 +19,14 @@ function setContentRange( ) { const [start, end] = byteRange; assert(start !== undefined && end !== undefined); - response.setHeader('Content-Range', - `bytes ${start}-${end}/${objectSize}`); + response.setHeader('Content-Range', `bytes ${start}-${end}/${objectSize}`); } -function sendError( - res: http.ServerResponse, - log: werelogs.RequestLogger, - error: ArsenalError, - optMessage?: string, -) { +function sendError(res: http.ServerResponse, log: werelogs.RequestLogger, error: ArsenalError, optMessage?: string) { res.writeHead(error.code); const message = optMessage ?? error.description ?? ''; - log.debug('sending back error response', { httpCode: error.code, - errorType: error.message, - error: message }); - res.end(`${JSON.stringify({ errorType: error.message, - errorMessage: message })}\n`); + log.debug('sending back error response', { httpCode: error.code, errorType: error.message, error: message }); + res.end(`${JSON.stringify({ errorType: error.message, errorMessage: message })}\n`); } /** @@ -64,7 +55,7 @@ export default class RESTServer extends httpServer { port: number; dataStore: any; bindAddress?: string; - log: { logLevel: any; dumpLevel: any; }; + log: { logLevel: any; dumpLevel: any }; }) { assert(params.port); @@ -105,9 +96,7 @@ export default class RESTServer extends httpServer { * @return new request logger */ createLogger(reqUids?: string) { - return reqUids ? - this.logging.newRequestLoggerFromSerializedUids(reqUids) : - this.logging.newRequestLogger(); + return reqUids ? this.logging.newRequestLoggerFromSerializedUids(reqUids) : this.logging.newRequestLogger(); } /** @@ -120,8 +109,7 @@ export default class RESTServer extends httpServer { _onRequest(req: http.IncomingMessage, res: http.ServerResponse) { const reqUids = req.headers['x-scal-request-uids']; const log = this.createLogger(reqUids as string); - log.debug('request received', { method: req.method, - url: req.url }); + log.debug('request received', { method: req.method, url: req.url }); const method = req.method ?? ''; if (method in this.reqMethods) { this.reqMethods[method](req, res, log); @@ -138,11 +126,7 @@ export default class RESTServer extends httpServer { * @param res - HTTP response object * @param log - logger object */ - _onPut( - req: http.IncomingMessage, - res: http.ServerResponse, - log: werelogs.RequestLogger, - ) { + _onPut(req: http.IncomingMessage, res: http.ServerResponse, log: werelogs.RequestLogger) { let size: number; try { parseURL(req.url ?? '', false); @@ -152,8 +136,7 @@ export default class RESTServer extends httpServer { } size = Number.parseInt(contentLength, 10); if (Number.isNaN(size)) { - throw errorInstances.InvalidInput.customizeDescription( - 'bad Content-Length'); + throw errorInstances.InvalidInput.customizeDescription('bad Content-Length'); } } catch (err: any) { return sendError(res, log, err); @@ -180,18 +163,13 @@ export default class RESTServer extends httpServer { * @param res - HTTP response object * @param log - logger object */ - _onGet( - req: http.IncomingMessage, - res: http.ServerResponse, - log: werelogs.RequestLogger, - ) { + _onGet(req: http.IncomingMessage, res: http.ServerResponse, log: werelogs.RequestLogger) { let pathInfo: ReturnType; - let rangeSpec: ReturnType | undefined = - undefined; + let rangeSpec: ReturnType | undefined = undefined; // Get request on the toplevel endpoint with ?action if (req.url?.startsWith(`${constants.dataFileURL}?`)) { - const queryParam = url.parse(req.url).query; + const queryParam = parseRequestTarget(req.url).query; if (queryParam === 'diskUsage') { return this.dataStore.getDiskUsage((err, result) => { if (err) { @@ -237,8 +215,7 @@ export default class RESTServer extends httpServer { if (err) { return sendError(res, log, err); } - log.debug('sending back 200/206 response with contents', - { key: pathInfo.key }); + log.debug('sending back 200/206 response with contents', { key: pathInfo.key }); setContentLength(res, contentLength); res.setHeader('Accept-Ranges', 'bytes'); if (byteRange) { @@ -263,11 +240,7 @@ export default class RESTServer extends httpServer { * @param res - HTTP response object * @param log - logger object */ - _onDelete( - req: http.IncomingMessage, - res: http.ServerResponse, - log: werelogs.RequestLogger, - ) { + _onDelete(req: http.IncomingMessage, res: http.ServerResponse, log: werelogs.RequestLogger) { let pathInfo: ReturnType; try { pathInfo = parseURL(req.url ?? '', true); @@ -278,8 +251,7 @@ export default class RESTServer extends httpServer { if (err) { return sendError(res, log, err); } - log.debug('sending back 204 response to DELETE', - { key: pathInfo.key }); + log.debug('sending back 204 response to DELETE', { key: pathInfo.key }); res.writeHead(204); return res.end(() => { log.debug('DELETE response sent', { key: pathInfo.key }); diff --git a/lib/network/rest/utils.ts b/lib/network/rest/utils.ts index 616ffc1a4..94def686e 100644 --- a/lib/network/rest/utils.ts +++ b/lib/network/rest/utils.ts @@ -1,6 +1,6 @@ import { errorInstances } from '../../errors'; import * as constants from '../../constants'; -import * as url from 'url'; +import { parseRequestTarget } from '../../utils/requestUrl'; const passthroughPrefixLength = constants.passthroughFileURL.length; export function explodePath(path: string) { @@ -15,8 +15,7 @@ export function explodePath(path: string) { if (pathMatch) { return { service: pathMatch[1], - key: (pathMatch[3] !== undefined && pathMatch[3].length > 0 ? - pathMatch[3] : undefined), + key: pathMatch[3] !== undefined && pathMatch[3].length > 0 ? pathMatch[3] : undefined, }; } throw errorInstances.InvalidURI.customizeDescription('malformed URI'); @@ -35,16 +34,12 @@ export function explodePath(path: string) { * - pathInfo.key {String} - The requested key */ export function parseURL(urlStr: string, expectKey: boolean) { - const urlObj = url.parse(urlStr); - const pathInfo = explodePath(decodeURI(urlObj.path!)); - if ((pathInfo.service !== constants.dataFileURL) - && (pathInfo.service !== constants.passthroughFileURL)) { - throw errorInstances.InvalidAction.customizeDescription( - `unsupported service '${pathInfo.service}'`); + const pathInfo = explodePath(decodeURI(parseRequestTarget(urlStr).path)); + if (pathInfo.service !== constants.dataFileURL && pathInfo.service !== constants.passthroughFileURL) { + throw errorInstances.InvalidAction.customizeDescription(`unsupported service '${pathInfo.service}'`); } if (expectKey && pathInfo.key === undefined) { - throw errorInstances.MissingParameter.customizeDescription( - 'URL is missing key'); + throw errorInstances.MissingParameter.customizeDescription('URL is missing key'); } if (!expectKey && pathInfo.key !== undefined) { // note: we may implement rewrite functionality by allowing a @@ -53,8 +48,7 @@ export function parseURL(urlStr: string, expectKey: boolean) { // atomicity of the update (we would just remove the old // object when the new one has been written entirely in this // case, saving a request over an equivalent PUT + DELETE). - throw errorInstances.InvalidURI.customizeDescription( - 'PUT url cannot contain a key'); + throw errorInstances.InvalidURI.customizeDescription('PUT url cannot contain a key'); } return pathInfo; } diff --git a/lib/s3routes/routesUtils.ts b/lib/s3routes/routesUtils.ts index 60da18f57..eeb992f84 100644 --- a/lib/s3routes/routesUtils.ts +++ b/lib/s3routes/routesUtils.ts @@ -1,4 +1,5 @@ -import * as url from 'url'; +import * as querystring from 'querystring'; +import { parseRequestTarget } from '../utils/requestUrl'; import * as http from 'http'; import { eachSeries } from 'async'; @@ -1119,12 +1120,12 @@ export function getBucketNameFromHost(request: http.IncomingMessage, validHosts: * @return request object with additional attributes */ export function normalizeRequest(request: ArsenalRequest, validHosts: string[]) { - const parsedUrl = url.parse(request.url!, true); - request.query = parsedUrl.query as Record; + const parsedUrl = parseRequestTarget(request.url!); + request.query = querystring.parse(parsedUrl.query ?? '') as Record; // TODO: make the namespace come from a config variable. request.namespace = 'default'; // Parse bucket and/or object names from request - const resources = getResourceNames(request, parsedUrl.pathname!, validHosts); + const resources = getResourceNames(request, parsedUrl.pathname, validHosts); request.gotBucketNameFromHost = resources.gotBucketNameFromHost ?? false; request.bucketName = resources.bucket; request.objectKey = resources.object; diff --git a/lib/storage/data/LocationConstraintParser.js b/lib/storage/data/LocationConstraintParser.js index 6a15eef6b..84066d9d1 100644 --- a/lib/storage/data/LocationConstraintParser.js +++ b/lib/storage/data/LocationConstraintParser.js @@ -1,5 +1,4 @@ const { http, https } = require('httpagent'); -const url = require('url'); const { NodeHttpHandler } = require('@smithy/node-http-handler'); const { fromIni } = require('@aws-sdk/credential-providers'); const Sproxy = require('sproxydclient'); @@ -15,6 +14,31 @@ const GcpClient = require('./external/GcpClient'); const PfsClient = require('./external/PfsClient'); const backendUtils = require('./external/utils'); +/** + * Describe a proxy URL as a mutable plain object, so the caller can merge cert + * options into it before handing it to the agent. + * @param {string} proxyUrl - absolute URL of the outbound proxy + * @return {object} agent options describing the proxy + */ +function proxyAgentOptions(proxyUrl) { + const parsed = new URL(proxyUrl); + const auth = parsed.username ? `${parsed.username}${parsed.password ? `:${parsed.password}` : ''}` : null; + return { + protocol: parsed.protocol, + slashes: true, + auth, + host: parsed.host, + port: parsed.port || null, + hostname: parsed.hostname, + hash: parsed.hash || null, + search: parsed.search || null, + query: parsed.search ? parsed.search.slice(1) : null, + pathname: parsed.pathname, + path: `${parsed.pathname}${parsed.search}`, + href: parsed.href, + }; +} + function parseLC(config, vault) { const clients = {}; @@ -29,8 +53,7 @@ function parseLC(config, vault) { if (locationObj.type === 'scality') { if (locationObj.details.connector.sproxyd) { clients[location] = new Sproxy({ - bootstrap: locationObj.details.connector - .sproxyd.bootstrap, + bootstrap: locationObj.details.connector.sproxyd.bootstrap, // Might be undefined which is ok since there is a default // set in sproxydclient if chordCos is undefined chordCos: locationObj.details.connector.sproxyd.chordCos, @@ -42,31 +65,28 @@ function parseLC(config, vault) { }); clients[location].clientType = 'scality'; } else if (locationObj.details.connector.hdclient) { - clients[location] = new Hyperdrive.hdcontroller.HDProxydClient( - locationObj.details.connector.hdclient); + clients[location] = new Hyperdrive.hdcontroller.HDProxydClient(locationObj.details.connector.hdclient); clients[location].clientType = 'scality'; } } if (locationObj.type === 'aws_s3' || locationObj.type === 'gcp') { let connectionAgent; - const endpoint = locationObj.type === 'gcp' ? - locationObj.details.gcpEndpoint : - locationObj.details.awsEndpoint; + const endpoint = + locationObj.type === 'gcp' ? locationObj.details.gcpEndpoint : locationObj.details.awsEndpoint; const region = locationObj.details.region || 'us-east-1'; const protocol = locationObj.details.https ? 'https' : 'http'; const sslEnabled = locationObj.details.https === true; const signatureVersion = !sslEnabled ? 'v2' : 'v4'; const pathStyle = locationObj.details.pathStyle; // keepalive config - const httpAgentConfig = - config.externalBackends[locationObj.type].httpAgent; + const httpAgentConfig = config.externalBackends[locationObj.type].httpAgent; // max sockets is infinity by default and expressed as null if (httpAgentConfig.maxSockets === null) { httpAgentConfig.maxSockets = undefined; } const proxyMatch = backendUtils.proxyCompareUrl(endpoint); if (config.outboundProxy.url && !proxyMatch) { - const options = url.parse(config.outboundProxy.url); + const options = proxyAgentOptions(config.outboundProxy.url); if (config.outboundProxy.certs) { Object.assign(options, config.outboundProxy.certs); options.secureProxy = true; @@ -78,9 +98,7 @@ function parseLC(config, vault) { } else { const agentOptions = { ...httpAgentConfig, maxSockets: false }; - connectionAgent = sslEnabled - ? new https.Agent(agentOptions) - : new http.Agent(agentOptions); + connectionAgent = sslEnabled ? new https.Agent(agentOptions) : new http.Agent(agentOptions); } const httpOptions = { agent: connectionAgent, timeout: 0 }; const s3Params = { @@ -88,9 +106,7 @@ function parseLC(config, vault) { region, requestHandler: new NodeHttpHandler({ requestTimeout: 0, - ...(sslEnabled - ? { httpsAgent: connectionAgent } - : { httpAgent: connectionAgent }), + ...(sslEnabled ? { httpsAgent: connectionAgent } : { httpAgent: connectionAgent }), }), forcePathStyle: pathStyle, customUserAgent: constants.productName, @@ -118,19 +134,15 @@ function parseLC(config, vault) { if (locationObj.type === 'gcp') { clientConfig.mpuBucket = locationObj.details.mpuBucketName; } - clients[location] = locationObj.type === 'gcp' ? - new GcpClient(clientConfig) : new AwsClient(clientConfig); + clients[location] = locationObj.type === 'gcp' ? new GcpClient(clientConfig) : new AwsClient(clientConfig); if (locationObj.type === 'aws_s3') { clients[location].setup(() => {}); } } if (locationObj.type === 'azure') { const azureStorageEndpoint = config.getAzureEndpoint(location); - const proxyParams = - backendUtils.proxyCompareUrl(azureStorageEndpoint) ? - {} : config.outboundProxy; - const azureStorageCredentials = - config.getAzureStorageCredentials(location); + const proxyParams = backendUtils.proxyCompareUrl(azureStorageEndpoint) ? {} : config.outboundProxy; + const azureStorageCredentials = config.getAzureStorageCredentials(location); clients[location] = new AzureClient({ azureStorageEndpoint, azureStorageCredentials, diff --git a/lib/storage/metadata/proxy/utils.js b/lib/storage/metadata/proxy/utils.js index 19e13e3b5..9e52c237c 100644 --- a/lib/storage/metadata/proxy/utils.js +++ b/lib/storage/metadata/proxy/utils.js @@ -1,4 +1,4 @@ -const url = require('url'); +const { parseRequestTarget } = require('../../../utils/requestUrl'); const querystring = require('querystring'); const errors = require('../../../errors').default; const errorInstances = require('../../../errors').errorInstances; @@ -22,7 +22,7 @@ function getURIComponents(uri, logger) { return {}; } - const { pathname, query } = url.parse(uri); + const { pathname, query } = parseRequestTarget(uri); const options = query ? querystring.parse(query) : {}; const typeIndex = pathname.indexOf('/', 1); const bucketIndex = pathname.indexOf('/', typeIndex + 1); @@ -67,9 +67,7 @@ function getURIComponents(uri, logger) { options, }; } catch (ex) { - logger.error('Invalid URI: failed to parse', - { uri, error: ex, errorStack: ex.stack, - message: ex.message }); + logger.error('Invalid URI: failed to parse', { uri, error: ex, errorStack: ex.stack, message: ex.message }); return null; } } @@ -87,18 +85,22 @@ function getURIComponents(uri, logger) { function getRequestBody(logger, request, cb) { const body = []; let bodyLen = 0; - request.on('data', data => { - body.push(data); - bodyLen += data.length; - }).on('error', cb).on('end', () => { - cb(null, Buffer.concat(body, bodyLen).toString()); - }).on('close', () => { - logger.error('Connection closed by remote peer'); - /* Execution will eventually reach the sendResponse code which will - * trigger the proper cleanup as the remote peer already hung up and - * nobody is on the line to get the message */ - cb(errors.BadRequest); - }); + request + .on('data', data => { + body.push(data); + bodyLen += data.length; + }) + .on('error', cb) + .on('end', () => { + cb(null, Buffer.concat(body, bodyLen).toString()); + }) + .on('close', () => { + logger.error('Connection closed by remote peer'); + /* Execution will eventually reach the sendResponse code which will + * trigger the proper cleanup as the remote peer already hung up and + * nobody is on the line to get the message */ + cb(errors.BadRequest); + }); } /** @@ -156,15 +158,15 @@ function sendResponse(req, res, log, err, data) { resData = resData.toString(); } /* - * Encoding data to binary provides a hot path to write data - * directly to the socket, without node.js trying to encode the data - * over and over again. - */ + * Encoding data to binary provides a hot path to write data + * directly to the socket, without node.js trying to encode the data + * over and over again. + */ const rawData = Buffer.from(resData, 'utf8'); /* - * Using Buffer.bytelength is not required here because data is binary - * encoded, data.length would give us the exact byte length - */ + * Using Buffer.bytelength is not required here because data is binary + * encoded, data.length would give us the exact byte length + */ res.writeHead(statusCode, statusMessage, { 'content-length': rawData.length, }); diff --git a/lib/utils/requestUrl.ts b/lib/utils/requestUrl.ts new file mode 100644 index 000000000..af65dcdb4 --- /dev/null +++ b/lib/utils/requestUrl.ts @@ -0,0 +1,110 @@ +/** + * An S3 object key is an opaque byte string, and almost every character is + * valid in one. Request targets are therefore split without being normalized: + * the key a request addresses has to come out exactly as the client wrote it. + * + * This is why `new URL()` is not used. WHATWG parsing collapses `.` and `..` + * segments, reads a leading `//` as an authority and percent-encodes + * non-ASCII, each of which names a different object than the request did: + * `//bucket/key` loses its bucket, `/bucket/a/../b` becomes `/bucket/b`. On + * the v2 auth path it also changes the string being signed, so a request + * would be verified against a resource it never addressed. + * + * The input is always read as a path, never as an absolute URL: no scheme or + * host is resolved. Callers handling client-supplied references such as + * `x-amz-copy-source` therefore see `http://host/x` as a whole key, and can + * reject it rather than silently reading it as `/x`. + */ + +/** Whitespace and zero-width characters stripped from either end of a target. */ +function isTrimmable(code: number): boolean { + return code < 33 || code === 0xa0 || code === 0xfeff; +} + +/** + * Targets matching this are taken verbatim; anything else is percent-escaped. + * A leading `///`, a `#`, or a `@` ahead of the query also force escaping. + */ +const VERBATIM = /^(\/\/?(?!\/)[^?\s]*)(\?[^\s]*)?$/; + +const ESCAPES: Record = { + '\t': '%09', + '\n': '%0A', + '\r': '%0D', + ' ': '%20', + '"': '%22', + "'": '%27', + '<': '%3C', + '>': '%3E', + '\\': '%5C', + '^': '%5E', + '`': '%60', + '{': '%7B', + '|': '%7C', + '}': '%7D', +}; + +function escapeDelimiters(part: string): string { + return part.replace(/[\t\n\r "'<>\\^`{|}]/g, ch => ESCAPES[ch]); +} + +export interface ParsedRequestTarget { + /** Query and fragment removed, and `\` folded to `/`. */ + pathname: string; + /** Query without its leading `?`, or null when the target carries none. */ + query: string | null; + /** `pathname` followed by the query, ready to route on. */ + path: string; +} + +/** + * Splits a request target into its path and query, without normalizing either. + * + * @param requestUrl - origin-form request target, e.g. `/bucket/key?acl` + * @returns the path and query, with delimiters escaped where the target is not + * already a plain path + * + * @example + * parseRequestTarget('/bucket/key?acl') + * // => { pathname: '/bucket/key', query: 'acl', path: '/bucket/key?acl' } + * @example + * parseRequestTarget('/bucket/my key') + * // => { pathname: '/bucket/my%20key', query: null, path: '/bucket/my%20key' } + * @example + * parseRequestTarget('/bucket/a/../b') + * // dot segments are kept as-is, unlike `new URL()`: + * // => { pathname: '/bucket/a/../b', query: null, path: '/bucket/a/../b' } + */ +export function parseRequestTarget(requestUrl: string): ParsedRequestTarget { + let start = 0; + let end = requestUrl.length; + while (start < end && isTrimmable(requestUrl.charCodeAt(start))) { + start += 1; + } + while (end > start && isTrimmable(requestUrl.charCodeAt(end - 1))) { + end -= 1; + } + const target = requestUrl.slice(start, end); + + // A '?' sitting after a '#' belongs to the fragment, so cut the fragment first. + const hashIndex = target.indexOf('#'); + const beforeHash = hashIndex === -1 ? target : target.slice(0, hashIndex); + const queryIndex = beforeHash.indexOf('?'); + + // Backslashes count as separators in the path, but stay literal in the query. + const rawPath = (queryIndex === -1 ? beforeHash : beforeHash.slice(0, queryIndex)).replace(/\\/g, '/'); + const rawQuery = queryIndex === -1 ? null : beforeHash.slice(queryIndex + 1); + + const candidate = rawQuery === null ? rawPath : `${rawPath}?${rawQuery}`; + const verbatim = hashIndex === -1 && !rawPath.includes('@') && VERBATIM.test(candidate); + const encode = verbatim ? (part: string) => part : escapeDelimiters; + + const pathname = encode(rawPath); + const query = rawQuery === null ? null : encode(rawQuery); + + return { + pathname, + query, + path: query === null ? pathname : `${pathname}?${query}`, + }; +} diff --git a/package.json b/package.json index bf218fc6f..b0b719562 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@scality/arsenal", "engines": { - "node": ">=20" + "node": ">=22" }, "version": "8.5.17", "config": { @@ -52,8 +52,8 @@ "base-x": "3.0.8", "base62": "^2.0.2", "debug": "^4.4.3", - "fcntl": "github:scality/node-fcntl#0.3.0", - "httpagent": "scality/httpagent#1.1.0", + "fcntl": "github:scality/node-fcntl#0.3.1", + "httpagent": "scality/httpagent#1.1.1", "https-proxy-agent": "^7.0.6", "ioredis": "^5.8.1", "ipaddr.js": "^2.2.0", @@ -66,10 +66,10 @@ "simple-glob": "^0.2.0", "socket.io": "^4.8.0", "socket.io-client": "^4.8.0", - "sproxydclient": "github:scality/sproxydclient#8.2.2", + "sproxydclient": "github:scality/sproxydclient#8.2.3", "utf8": "^3.0.0", "uuid": "^10.0.0", - "werelogs": "scality/werelogs#8.2.4", + "werelogs": "scality/werelogs#8.2.5", "xml2js": "^0.6.2" }, "optionalDependencies": { @@ -82,7 +82,7 @@ "@eslint/plugin-kit": "^0.4.0", "@opentelemetry/context-async-hooks": "^2.8.0", "@opentelemetry/core": "^2.8.0", - "@scality/eslint-config-scality": "scality/Guidelines#8.3.2", + "@scality/eslint-config-scality": "scality/Guidelines#8.3.3", "@sinonjs/fake-timers": "^15.0.0", "@types/async": "^3.2.24", "@types/ioredis": "^5.0.0", @@ -146,7 +146,7 @@ }, "resolutions": { "tar": "^7.5.6", - "werelogs": "scality/werelogs#8.2.4" + "werelogs": "scality/werelogs#8.2.5" }, "nyc": { "tempDirectory": "coverage", diff --git a/tests/unit/auth/v2/canonicalization.spec.js b/tests/unit/auth/v2/canonicalization.spec.js index c25fe2879..e033a6065 100644 --- a/tests/unit/auth/v2/canonicalization.spec.js +++ b/tests/unit/auth/v2/canonicalization.spec.js @@ -11,15 +11,15 @@ const gcpCanonicalizedResource = request => getCanonicalizedResource(request, 'G describe('canonicalization', () => { it('should construct a canonicalized header in the correct order for AWS', () => { const headers = { - date: 'Mon, 21 Sep 2015 22:29:27 GMT', + 'date': 'Mon, 21 Sep 2015 22:29:27 GMT', 'x-amz-request-payer': 'requester', 'x-amz-meta-meta': 'something very meta', 'x-amz-meta-bits': '0', 'x-amz-meta-blksize': '2097152', 'x-amz-meta-compress': '0', - authorization: 'AWS accessKey1:V8g5UJUFmMzruMqUHVT6ZwvUw+M=', - host: 's3.amazonaws.com:80', - connection: 'Keep-Alive', + 'authorization': 'AWS accessKey1:V8g5UJUFmMzruMqUHVT6ZwvUw+M=', + 'host': 's3.amazonaws.com:80', + 'connection': 'Keep-Alive', 'user-agent': 'Cyberduck/4.7.2.18004 (Mac OS X/10.10.5) (x86_64)', }; const canonicalizedHeader = getCanonicalizedAmzHeaders(headers); @@ -35,10 +35,10 @@ describe('canonicalization', () => { it('should return an empty string as the canonicalized ' + 'header if no amz headers', () => { const headers = { - date: 'Mon, 21 Sep 2015 22:29:27 GMT', - authorization: 'AWS accessKey1:V8g5UJUFmMzruMqUHVT6ZwvUw+M=', - host: 's3.amazonaws.com:80', - connection: 'Keep-Alive', + 'date': 'Mon, 21 Sep 2015 22:29:27 GMT', + 'authorization': 'AWS accessKey1:V8g5UJUFmMzruMqUHVT6ZwvUw+M=', + 'host': 's3.amazonaws.com:80', + 'connection': 'Keep-Alive', 'user-agent': 'Cyberduck/4.7.2.18004 (Mac OS X/10.10.5) (x86_64)', }; const canonicalizedHeader = getCanonicalizedAmzHeaders(headers); @@ -89,15 +89,15 @@ describe('canonicalization', () => { it('should construct a canonicalized header in the correct order for GCP', () => { const headers = { - date: 'Mon, 21 Sep 2015 22:29:27 GMT', + 'date': 'Mon, 21 Sep 2015 22:29:27 GMT', 'x-goog-request-payer': 'requester', 'x-goog-meta-meta': 'something very meta', 'x-goog-meta-bits': '0', 'x-goog-meta-blksize': '2097152', 'x-goog-meta-compress': '0', - authorization: 'GOOG1 accessKey1:V8g5UJUFmMzruMqUHVT6ZwvUw+M=', - host: 's3.amazonaws.com:80', - connection: 'Keep-Alive', + 'authorization': 'GOOG1 accessKey1:V8g5UJUFmMzruMqUHVT6ZwvUw+M=', + 'host': 's3.amazonaws.com:80', + 'connection': 'Keep-Alive', 'user-agent': 'Cyberduck/4.7.2.18004 (Mac OS X/10.10.5) (x86_64)', }; const canonicalizedHeader = getCanonicalizedGcpHeaders(headers); @@ -113,10 +113,10 @@ describe('canonicalization', () => { it('should return an empty string as the canonicalized ' + 'header if no goog headers', () => { const headers = { - date: 'Mon, 21 Sep 2015 22:29:27 GMT', - authorization: 'GOOG1 accessKey1:V8g5UJUFmMzruMqUHVT6ZwvUw+M=', - host: 'storage.googleapis.com:80', - connection: 'Keep-Alive', + 'date': 'Mon, 21 Sep 2015 22:29:27 GMT', + 'authorization': 'GOOG1 accessKey1:V8g5UJUFmMzruMqUHVT6ZwvUw+M=', + 'host': 'storage.googleapis.com:80', + 'connection': 'Keep-Alive', 'user-agent': 'Cyberduck/4.7.2.18004 (Mac OS X/10.10.5) (x86_64)', }; const canonicalizedHeader = getCanonicalizedGcpHeaders(headers); @@ -167,11 +167,11 @@ describe('canonicalization', () => { it('should handle header values that are arrays (AWS SDK v3 compatibility) for AWS', () => { const headers = { - date: 'Mon, 21 Sep 2015 22:29:27 GMT', + 'date': 'Mon, 21 Sep 2015 22:29:27 GMT', 'x-amz-array-header': ['value1', 'value2', 'value3'], 'x-amz-meta-meta': 'something very meta', - authorization: 'AWS accessKey1:V8g5UJUFmMzruMqUHVT6ZwvUw+M=', - host: 's3.amazonaws.com:80', + 'authorization': 'AWS accessKey1:V8g5UJUFmMzruMqUHVT6ZwvUw+M=', + 'host': 's3.amazonaws.com:80', }; const canonicalizedHeader = getCanonicalizedAmzHeaders(headers); assert.strictEqual( @@ -182,11 +182,11 @@ describe('canonicalization', () => { it('should handle header values that are numbers for AWS', () => { const headers = { - date: 'Mon, 21 Sep 2015 22:29:27 GMT', + 'date': 'Mon, 21 Sep 2015 22:29:27 GMT', 'x-amz-number-header': 12345, 'x-amz-meta-meta': 'something very meta', - authorization: 'AWS accessKey1:V8g5UJUFmMzruMqUHVT6ZwvUw+M=', - host: 's3.amazonaws.com:80', + 'authorization': 'AWS accessKey1:V8g5UJUFmMzruMqUHVT6ZwvUw+M=', + 'host': 's3.amazonaws.com:80', }; const canonicalizedHeader = getCanonicalizedAmzHeaders(headers); assert.strictEqual( @@ -197,11 +197,11 @@ describe('canonicalization', () => { it('should handle header values that are arrays for GCP', () => { const headers = { - date: 'Mon, 21 Sep 2015 22:29:27 GMT', + 'date': 'Mon, 21 Sep 2015 22:29:27 GMT', 'x-goog-array-header': ['gcp-val1', 'gcp-val2'], 'x-goog-meta-meta': 'something very meta', - authorization: 'GOOG1 accessKey1:V8g5UJUFmMzruMqUHVT6ZwvUw+M=', - host: 'storage.googleapis.com:80', + 'authorization': 'GOOG1 accessKey1:V8g5UJUFmMzruMqUHVT6ZwvUw+M=', + 'host': 'storage.googleapis.com:80', }; const canonicalizedHeader = getCanonicalizedGcpHeaders(headers); assert.strictEqual( @@ -212,11 +212,11 @@ describe('canonicalization', () => { it('should handle header values with extra whitespace in arrays', () => { const headers = { - date: 'Mon, 21 Sep 2015 22:29:27 GMT', + 'date': 'Mon, 21 Sep 2015 22:29:27 GMT', 'x-amz-spaced-header': [' value1 ', ' value2 '], 'x-amz-meta-meta': 'something very meta', - authorization: 'AWS accessKey1:V8g5UJUFmMzruMqUHVT6ZwvUw+M=', - host: 's3.amazonaws.com:80', + 'authorization': 'AWS accessKey1:V8g5UJUFmMzruMqUHVT6ZwvUw+M=', + 'host': 's3.amazonaws.com:80', }; const canonicalizedHeader = getCanonicalizedAmzHeaders(headers); assert.strictEqual( @@ -224,4 +224,38 @@ describe('canonicalization', () => { 'x-amz-meta-meta:something very meta\n' + 'x-amz-spaced-header:value1 , value2\n', ); }); + + /* + * The signed resource has to be the path exactly as the client sent it: + * rewriting it here would check the signature against a different object + * than the one the request addresses. + */ + describe('canonicalized resource path', () => { + const resourceFor = url => getCanonicalizedResource({ headers: {}, url, query: {} }); + + it('should keep dot segments in the key', () => { + assert.strictEqual(resourceFor('/bucket/a/../b'), '/bucket/a/../b'); + assert.strictEqual(resourceFor('/bucket/a/./b'), '/bucket/a/./b'); + }); + + it('should keep a key that itself starts with a slash', () => { + assert.strictEqual(resourceFor('//bucket/key'), '//bucket/key'); + }); + + it('should leave a non-ASCII key unencoded', () => { + assert.strictEqual(resourceFor('/bucket/ключ'), '/bucket/ключ'); + }); + + it('should escape a space, matching what the signing client sends', () => { + assert.strictEqual(resourceFor('/bucket/my file.txt'), '/bucket/my%20file.txt'); + }); + + it('should keep percent-encoding untouched', () => { + assert.strictEqual(resourceFor('/bucket/key%2Fnested'), '/bucket/key%2Fnested'); + }); + + it('should drop the query string', () => { + assert.strictEqual(resourceFor('/bucket/key?uploads=1'), '/bucket/key'); + }); + }); }); diff --git a/tests/unit/network/probe/HealthProbeServer.spec.js b/tests/unit/network/probe/HealthProbeServer.spec.js index f234f7b58..30056ce17 100644 --- a/tests/unit/network/probe/HealthProbeServer.spec.js +++ b/tests/unit/network/probe/HealthProbeServer.spec.js @@ -1,6 +1,5 @@ const assert = require('assert'); -const HealthProbeServer = - require('../../../../lib/network/probe/HealthProbeServer').default; +const HealthProbeServer = require('../../../../lib/network/probe/HealthProbeServer').default; const http = require('http'); function makeRequest(meth, uri) { @@ -15,15 +14,9 @@ function makeRequest(meth, uri) { return req; } -const healthcheckEndpoints = [ - '/_/health/liveness', - '/_/health/readiness', -]; +const healthcheckEndpoints = ['/_/health/liveness', '/_/health/readiness']; -const badHealthcheckEndpoints = [ - '/_/health/liveness_thisiswrong', - '/_/health/readiness_thisiswrong', -]; +const badHealthcheckEndpoints = ['/_/health/liveness_thisiswrong', '/_/health/readiness_thisiswrong']; describe('network.probe.HealthProbeServer', () => { describe('service is "up"', () => { @@ -43,8 +36,7 @@ describe('network.probe.HealthProbeServer', () => { done(); }); healthcheckEndpoints.forEach(ep => { - it('should perform a GET and ' + - 'return 200 OK', done => { + it('should perform a GET and ' + 'return 200 OK', done => { makeRequest('GET', ep) .on('response', res => { assert(res.statusCode === 200); @@ -53,7 +45,8 @@ describe('network.probe.HealthProbeServer', () => { .on('error', err => { assert.ifError(err); done(); - }).end(); + }) + .end(); }); }); }); @@ -69,8 +62,8 @@ describe('network.probe.HealthProbeServer', () => { livenessCheck: falseStub, readinessCheck: falseStub, }); + server._cbOnListening = done; server.start(); - done(); } beforeAll(done => { @@ -83,8 +76,7 @@ describe('network.probe.HealthProbeServer', () => { }); healthcheckEndpoints.forEach(ep => { - it('should perform a GET and ' + - 'return 503 ServiceUnavailable', done => { + it('should perform a GET and ' + 'return 503 ServiceUnavailable', done => { makeRequest('GET', ep) .on('response', res => { assert(res.statusCode === 503); @@ -93,7 +85,8 @@ describe('network.probe.HealthProbeServer', () => { .on('error', err => { assert.ifError(err); done(); - }).end(); + }) + .end(); }); }); }); @@ -105,8 +98,8 @@ describe('network.probe.HealthProbeServer', () => { server = new HealthProbeServer({ port: 4042, }); + server._cbOnListening = done; server.start(); - done(); } beforeAll(done => { @@ -119,8 +112,7 @@ describe('network.probe.HealthProbeServer', () => { }); healthcheckEndpoints.forEach(ep => { - it('should perform a POST and ' + - 'return 405 MethodNotAllowed', done => { + it('should perform a POST and ' + 'return 405 MethodNotAllowed', done => { makeRequest('POST', ep) .on('response', res => { assert(res.statusCode === 405); @@ -129,7 +121,8 @@ describe('network.probe.HealthProbeServer', () => { .on('error', err => { assert.ifError(err); done(); - }).end(); + }) + .end(); }); }); }); @@ -140,8 +133,8 @@ describe('network.probe.HealthProbeServer', () => { server = new HealthProbeServer({ port: 4042, }); + server._cbOnListening = done; server.start(); - done(); } beforeAll(done => { @@ -154,8 +147,7 @@ describe('network.probe.HealthProbeServer', () => { }); badHealthcheckEndpoints.forEach(ep => { - it('should perform a GET and ' + - 'return 400 InvalidURI', done => { + it('should perform a GET and ' + 'return 400 InvalidURI', done => { makeRequest('GET', ep) .on('response', res => { assert(res.statusCode === 400); @@ -164,7 +156,8 @@ describe('network.probe.HealthProbeServer', () => { .on('error', err => { assert.ifError(err); done(); - }).end(); + }) + .end(); }); }); }); @@ -206,7 +199,8 @@ describe('network.probe.HealthProbeServer', () => { .on('error', err => { assert.ifError(err); done(); - }).end(); + }) + .end(); }); }); }); diff --git a/tests/unit/utils/requestUrl.spec.js b/tests/unit/utils/requestUrl.spec.js new file mode 100644 index 000000000..2d066edfe --- /dev/null +++ b/tests/unit/utils/requestUrl.spec.js @@ -0,0 +1,92 @@ +'use strict'; + +const assert = require('assert'); + +const { parseRequestTarget } = require('../../../lib/utils/requestUrl'); + +/* + * These values pin the behaviour the v2 signature path and the key + * derivation in routesUtils rely on: any change here would change what + * gets parsed as the bucket/key or what gets signed. + */ +const cases = [ + // plain targets + ['/bucket/key', '/bucket/key', null], + ['/bucket/key?acl', '/bucket/key', 'acl'], + ['/bucket/key?versionId=abc&acl', '/bucket/key', 'versionId=abc&acl'], + ['/', '/', null], + ['', '', null], + ['/bucket/', '/bucket/', null], + ['/bucket//double//slash', '/bucket//double//slash', null], + + // delimiters + ['/bucket/key?', '/bucket/key', ''], + ['/bucket/key?x=1#frag', '/bucket/key', 'x=1'], + ['/bucket/key#frag?x=1', '/bucket/key', null], + ['/bucket/key#hash', '/bucket/key', null], + ['?onlyquery', '', 'onlyquery'], + + // percent-encoding is left exactly as the client sent it + ['/bucket/my%20key', '/bucket/my%20key', null], + ['/bucket/key%2Fencoded', '/bucket/key%2Fencoded', null], + ['/bucket/key%2fencoded', '/bucket/key%2fencoded', null], + ['/bucket/key%23hash', '/bucket/key%23hash', null], + + // whitespace and backslashes + ['/bucket/my key', '/bucket/my%20key', null], + ['/bucket/tab\tkey', '/bucket/tab%09key', null], + ['/bucket/back\\slash', '/bucket/back/slash', null], + [' /bucket/key ', '/bucket/key', null], + + // a '@' in the key drops the parser out of its verbatim path + ['/bucket/key"quote', '/bucket/key"quote', null], + ['/bucket/user@host/key"quote', '/bucket/user@host/key%22quote', null], + ['/bucket/user@host/key{brace}', '/bucket/user@host/key%7Bbrace%7D', null], + + // backslashes separate in the path but stay literal in the query + ['/bucket/key?a\\b', '/bucket/key', 'a\\b'], +]; + +describe('parseRequestTarget', () => { + cases.forEach(([input, pathname, query]) => { + it(`should parse ${JSON.stringify(input)}`, () => { + const parsed = parseRequestTarget(input); + assert.strictEqual(parsed.pathname, pathname); + assert.strictEqual(parsed.query, query); + assert.strictEqual(parsed.path, query === null ? pathname : `${pathname}?${query}`); + }); + }); + + /* + * These are the cases where `new URL()` would rewrite the target. Each one + * would change the object a request resolves to, and with it the string the + * v2 signature is checked against. + */ + describe('keys that WHATWG parsing would rewrite', () => { + it('should not collapse dot segments', () => { + assert.strictEqual(parseRequestTarget('/bucket/a/../b').pathname, '/bucket/a/../b'); + assert.strictEqual(parseRequestTarget('/bucket/a/./b').pathname, '/bucket/a/./b'); + assert.strictEqual(parseRequestTarget('/bucket/..').pathname, '/bucket/..'); + }); + + it('should keep a leading // rather than read it as a host', () => { + assert.strictEqual(parseRequestTarget('//bucket/key').pathname, '//bucket/key'); + assert.strictEqual(parseRequestTarget('///bucket/key').pathname, '///bucket/key'); + }); + + it('should leave non-ASCII keys unencoded', () => { + assert.strictEqual(parseRequestTarget('/bucket/ключ').pathname, '/bucket/ключ'); + assert.strictEqual(parseRequestTarget('/bucket/ünïcode').pathname, '/bucket/ünïcode'); + }); + + it('should not drop control characters from a key', () => { + assert.strictEqual(parseRequestTarget('/bucket/a\tb').pathname, '/bucket/a%09b'); + }); + }); + + it('should never throw on input the legacy parser rejected', () => { + ['//', ':', '\u0000', 'http://x', '/b/k?a=%'].forEach(input => { + assert.doesNotThrow(() => parseRequestTarget(input)); + }); + }); +}); diff --git a/yarn.lock b/yarn.lock index 2153c1bee..d20c5d6b8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2835,12 +2835,12 @@ resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.1.tgz#eaee5900122c110a3dbcb728c0597014a2621774" integrity sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg== -"@scality/eslint-config-scality@scality/Guidelines#8.3.2": - version "8.3.2" - resolved "https://codeload.github.com/scality/Guidelines/tar.gz/8ae4ae7142af24c28e2d92f463f2a375d5130e0f" +"@scality/eslint-config-scality@scality/Guidelines#8.3.3": + version "8.3.3" + resolved "https://codeload.github.com/scality/Guidelines/tar.gz/4f3f39e7def07df51264bb013e0978f02d110414" dependencies: - commander "11.1.0" - markdownlint "0.31.1" + globals "^14.0.0" + markdownlint "0.38.0" "@scality/hdclient@^1.3.2": version "1.3.2" @@ -3524,6 +3524,13 @@ dependencies: "@types/node" "*" +"@types/debug@^4.0.0": + version "4.1.13" + resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.13.tgz#22d1cc9d542d3593caea764f974306ab36286ee7" + integrity sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw== + dependencies: + "@types/ms" "*" + "@types/estree@^1.0.6": version "1.0.8" resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.8.tgz#958b91c991b1867ced318bedea0e215ee050726e" @@ -3575,6 +3582,16 @@ resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== +"@types/katex@^0.16.0": + version "0.16.8" + resolved "https://registry.yarnpkg.com/@types/katex/-/katex-0.16.8.tgz#80bf3e0814d09a846412a0b0f140946b79c36c3e" + integrity sha512-trgaNyfU+Xh2Tc+ABIb44a5AYUpicB3uwirOioeOkNPPbmgRNtcWyDeeFRzjPZENO9Vq8gvVqfhaaXWLlevVwg== + +"@types/ms@*": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@types/ms/-/ms-2.1.0.tgz#052aa67a48eccc4309d7f0191b7e41434b90bb78" + integrity sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA== + "@types/node@*", "@types/node@>=10.0.0": version "24.7.0" resolved "https://registry.yarnpkg.com/@types/node/-/node-24.7.0.tgz#a34c9f0d3401db396782e440317dd5d8373c286f" @@ -3601,6 +3618,11 @@ resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.3.tgz#6209321eb2c1712a7e7466422b8cb1fc0d9dd5d8" integrity sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw== +"@types/unist@^2.0.0": + version "2.0.11" + resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.11.tgz#11af57b127e32487774841f7a4e54eab166d03c4" + integrity sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA== + "@types/utf8@^3.0.3": version "3.0.3" resolved "https://registry.yarnpkg.com/@types/utf8/-/utf8-3.0.3.tgz#e153ada15157477a9e0d2bcc638c34284a1d82ea" @@ -4399,6 +4421,21 @@ char-regex@^1.0.2: resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf" integrity sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw== +character-entities-legacy@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz#76bc83a90738901d7bc223a9e93759fdd560125b" + integrity sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ== + +character-entities@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/character-entities/-/character-entities-2.0.2.tgz#2d09c2e72cd9523076ccb21157dff66ad43fcc22" + integrity sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ== + +character-reference-invalid@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz#85c66b041e43b47210faf401278abf808ac45cb9" + integrity sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw== + chokidar@^3.5.2: version "3.6.0" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" @@ -4484,10 +4521,10 @@ color-name@~1.1.4: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== -commander@11.1.0: - version "11.1.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-11.1.0.tgz#62fdce76006a68e5c1ab3314dc92e800eb83d906" - integrity sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ== +commander@^8.3.0: + version "8.3.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66" + integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww== commondir@^1.0.1: version "1.0.1" @@ -4588,7 +4625,7 @@ data-view-byte-offset@^1.0.1: es-errors "^1.3.0" is-data-view "^1.0.1" -debug@4, debug@^4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@^4.3.5, debug@^4.4.1, debug@^4.4.3: +debug@4, debug@^4, debug@^4.0.0, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@^4.3.5, debug@^4.4.1, debug@^4.4.3: version "4.4.3" resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a" integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA== @@ -4607,6 +4644,13 @@ decamelize@^1.2.0: resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" integrity sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA== +decode-named-character-reference@^1.0.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz#3e40603760874c2e5867691b599d73a7da25b53f" + integrity sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q== + dependencies: + character-entities "^2.0.0" + dedent@^1.0.0: version "1.7.0" resolved "https://registry.yarnpkg.com/dedent/-/dedent-1.7.0.tgz#c1f9445335f0175a96587be245a282ff451446ca" @@ -4685,11 +4729,23 @@ denque@^2.1.0: resolved "https://registry.yarnpkg.com/denque/-/denque-2.1.0.tgz#e93e1a6569fb5e66f16a3c2a2964617d349d6ab1" integrity sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw== +dequal@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be" + integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA== + detect-newline@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651" integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA== +devlop@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/devlop/-/devlop-1.1.0.tgz#4db7c2ca4dc6e0e834c30be70c94bbc976dc7018" + integrity sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA== + dependencies: + dequal "^2.0.0" + diff-sequences@^29.6.3: version "29.6.3" resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.6.3.tgz#4deaf894d11407c51efc8418012f9e70b84ea921" @@ -4801,11 +4857,6 @@ engine.io@~6.6.0: engine.io-parser "~5.2.1" ws "~8.17.1" -entities@~3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/entities/-/entities-3.0.1.tgz#2b887ca62585e96db3903482d336c1006c3001d4" - integrity sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q== - env-paths@^2.2.0: version "2.2.1" resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.1.tgz#420399d416ce1fbe9bc0a07c62fa68d67fd0f8f2" @@ -5213,9 +5264,9 @@ fb-watchman@^2.0.0: dependencies: bser "2.1.1" -"fcntl@github:scality/node-fcntl#0.3.0": - version "0.3.0" - resolved "https://codeload.github.com/scality/node-fcntl/tar.gz/397840a8842e6d8067b6be8c22f1a2b81486f123" +"fcntl@github:scality/node-fcntl#0.3.1": + version "0.3.1" + resolved "https://codeload.github.com/scality/node-fcntl/tar.gz/bbeb7109bcdaac277d742d19d5ddbcafa633ffdb" dependencies: bindings "^1.1.1" node-addon-api "^8.3.0" @@ -5570,12 +5621,19 @@ http-proxy-agent@^7.0.0: agent-base "^7.1.0" debug "^4.3.4" -"httpagent@github:scality/httpagent#1.1.0", httpagent@scality/httpagent#1.1.0: +"httpagent@github:scality/httpagent#1.1.0": version "1.1.0" resolved "https://codeload.github.com/scality/httpagent/tar.gz/8f9958eb9cde086db7819a86582fba640a5f8876" dependencies: agentkeepalive "^4.5.0" +"httpagent@github:scality/httpagent#1.1.1", httpagent@scality/httpagent#1.1.1: + version "1.1.1" + uid "7c6ef8be1cd89b56651f4a922ea5c92d7673ee2e" + resolved "https://codeload.github.com/scality/httpagent/tar.gz/7c6ef8be1cd89b56651f4a922ea5c92d7673ee2e" + dependencies: + agentkeepalive "^4.6.0" + https-proxy-agent@^7.0.0, https-proxy-agent@^7.0.1, https-proxy-agent@^7.0.6: version "7.0.6" resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz#da8dfeac7da130b05c2ba4b59c9b6cd66611a6b9" @@ -5724,6 +5782,19 @@ ipaddr.js@^2.2.0: resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-2.2.0.tgz#d33fa7bac284f4de7af949638c9d68157c6b92e8" integrity sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA== +is-alphabetical@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-alphabetical/-/is-alphabetical-2.0.1.tgz#01072053ea7c1036df3c7d19a6daaec7f19e789b" + integrity sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ== + +is-alphanumerical@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz#7c03fbe96e3e931113e57f964b0a368cc2dfd875" + integrity sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw== + dependencies: + is-alphabetical "^2.0.0" + is-decimal "^2.0.0" + is-array-buffer@^3.0.4, is-array-buffer@^3.0.5: version "3.0.5" resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.5.tgz#65742e1e687bd2cc666253068fd8707fe4d44280" @@ -5800,6 +5871,11 @@ is-date-object@^1.0.5, is-date-object@^1.1.0: call-bound "^1.0.2" has-tostringtag "^1.0.2" +is-decimal@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-decimal/-/is-decimal-2.0.1.tgz#9469d2dc190d0214fd87d78b78caecc0cc14eef7" + integrity sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A== + is-docker@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-3.0.0.tgz#90093aa3106277d8a77a5910dbae71747e15a200" @@ -5845,6 +5921,11 @@ is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: dependencies: is-extglob "^2.1.1" +is-hexadecimal@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz#86b5bf668fca307498d319dfc03289d781a90027" + integrity sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg== + is-inside-container@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-inside-container/-/is-inside-container-1.0.0.tgz#e81fba699662eb31dbdaf26766a61d4814717ea4" @@ -6545,6 +6626,13 @@ jws@^3.2.2: jwa "^1.4.1" safe-buffer "^5.0.1" +katex@^0.16.0: + version "0.16.47" + resolved "https://registry.yarnpkg.com/katex/-/katex-0.16.47.tgz#0a13a42c2deb4f74e61f162d440b9165a548030f" + integrity sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg== + dependencies: + commander "^8.3.0" + keyv@^4.5.4: version "4.5.4" resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93" @@ -6693,13 +6781,6 @@ lines-and-columns@^1.1.6: resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== -linkify-it@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-4.0.1.tgz#01f1d5e508190d06669982ba31a7d9f56a5751ec" - integrity sha512-C7bfi1UZmoj8+PQx22XyeXCuBlokoyWQL5pWSP+EI6nzRylyThouddufc2c1NDIcP9k5agmN9fLpA7VNJfIiqw== - dependencies: - uc.micro "^1.0.1" - locate-path@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" @@ -6891,40 +6972,25 @@ makeerror@1.0.12: dependencies: tmpl "1.0.5" -markdown-it@13.0.1: - version "13.0.1" - resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-13.0.1.tgz#c6ecc431cacf1a5da531423fc6a42807814af430" - integrity sha512-lTlxriVoy2criHP0JKRhO2VDG9c2ypWCsT237eDiLqi09rmbKoUetyGHq2uOIRoRS//kfoJckS0eUzzkDR+k2Q== +markdownlint@0.38.0: + version "0.38.0" + resolved "https://registry.yarnpkg.com/markdownlint/-/markdownlint-0.38.0.tgz#862ca9d08f3a28f4149bd388ac369bb95865534e" + integrity sha512-xaSxkaU7wY/0852zGApM8LdlIfGCW8ETZ0Rr62IQtAnUMlMuifsg09vWJcNYeL4f0anvr8Vo4ZQar8jGpV0btQ== dependencies: - argparse "^2.0.1" - entities "~3.0.1" - linkify-it "^4.0.1" - mdurl "^1.0.1" - uc.micro "^1.0.5" - -markdownlint-micromark@0.1.7: - version "0.1.7" - resolved "https://registry.yarnpkg.com/markdownlint-micromark/-/markdownlint-micromark-0.1.7.tgz#c465091b30d61a56027ccbfb981c80c96448c165" - integrity sha512-BbRPTC72fl5vlSKv37v/xIENSRDYL/7X/XoFzZ740FGEbs9vZerLrIkFRY0rv7slQKxDczToYuMmqQFN61fi4Q== - -markdownlint@0.31.1: - version "0.31.1" - resolved "https://registry.yarnpkg.com/markdownlint/-/markdownlint-0.31.1.tgz#f014ed2d3614c5dbc351b7f65641ccc0a5facdb7" - integrity sha512-CKMR2hgcIBrYlIUccDCOvi966PZ0kJExDrUi1R+oF9PvqQmCrTqjOsgIvf2403OmJ+CWomuzDoylr6KbuMyvHA== - dependencies: - markdown-it "13.0.1" - markdownlint-micromark "0.1.7" + micromark "4.0.2" + micromark-core-commonmark "2.0.3" + micromark-extension-directive "4.0.0" + micromark-extension-gfm-autolink-literal "2.1.0" + micromark-extension-gfm-footnote "2.1.0" + micromark-extension-gfm-table "2.1.1" + micromark-extension-math "3.1.0" + micromark-util-types "2.0.2" math-intrinsics@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9" integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g== -mdurl@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e" - integrity sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g== - memory-pager@^1.0.2: version "1.5.0" resolved "https://registry.yarnpkg.com/memory-pager/-/memory-pager-1.5.0.tgz#d8751655d22d384682741c972f2c3d6dfa3e66b5" @@ -6940,6 +7006,251 @@ merge2@^1.3.0: resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== +micromark-core-commonmark@2.0.3, micromark-core-commonmark@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz#c691630e485021a68cf28dbc2b2ca27ebf678cd4" + integrity sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg== + dependencies: + decode-named-character-reference "^1.0.0" + devlop "^1.0.0" + micromark-factory-destination "^2.0.0" + micromark-factory-label "^2.0.0" + micromark-factory-space "^2.0.0" + micromark-factory-title "^2.0.0" + micromark-factory-whitespace "^2.0.0" + micromark-util-character "^2.0.0" + micromark-util-chunked "^2.0.0" + micromark-util-classify-character "^2.0.0" + micromark-util-html-tag-name "^2.0.0" + micromark-util-normalize-identifier "^2.0.0" + micromark-util-resolve-all "^2.0.0" + micromark-util-subtokenize "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-extension-directive@4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/micromark-extension-directive/-/micromark-extension-directive-4.0.0.tgz#af389e33fe0654c15f8466b73a0f5af598d00368" + integrity sha512-/C2nqVmXXmiseSSuCdItCMho7ybwwop6RrrRPk0KbOHW21JKoCldC+8rFOaundDoRBUWBnJJcxeA/Kvi34WQXg== + dependencies: + devlop "^1.0.0" + micromark-factory-space "^2.0.0" + micromark-factory-whitespace "^2.0.0" + micromark-util-character "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + parse-entities "^4.0.0" + +micromark-extension-gfm-autolink-literal@2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz#6286aee9686c4462c1e3552a9d505feddceeb935" + integrity sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw== + dependencies: + micromark-util-character "^2.0.0" + micromark-util-sanitize-uri "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-extension-gfm-footnote@2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz#4dab56d4e398b9853f6fe4efac4fc9361f3e0750" + integrity sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw== + dependencies: + devlop "^1.0.0" + micromark-core-commonmark "^2.0.0" + micromark-factory-space "^2.0.0" + micromark-util-character "^2.0.0" + micromark-util-normalize-identifier "^2.0.0" + micromark-util-sanitize-uri "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-extension-gfm-table@2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz#fac70bcbf51fe65f5f44033118d39be8a9b5940b" + integrity sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg== + dependencies: + devlop "^1.0.0" + micromark-factory-space "^2.0.0" + micromark-util-character "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-extension-math@3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/micromark-extension-math/-/micromark-extension-math-3.1.0.tgz#c42ee3b1dd5a9a03584e83dd8f08e3de510212c1" + integrity sha512-lvEqd+fHjATVs+2v/8kg9i5Q0AP2k85H0WUOwpIVvUML8BapsMvh1XAogmQjOCsLpoKRCVQqEkQBB3NhVBcsOg== + dependencies: + "@types/katex" "^0.16.0" + devlop "^1.0.0" + katex "^0.16.0" + micromark-factory-space "^2.0.0" + micromark-util-character "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-factory-destination@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz#8fef8e0f7081f0474fbdd92deb50c990a0264639" + integrity sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA== + dependencies: + micromark-util-character "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-factory-label@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz#5267efa97f1e5254efc7f20b459a38cb21058ba1" + integrity sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg== + dependencies: + devlop "^1.0.0" + micromark-util-character "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-factory-space@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz#36d0212e962b2b3121f8525fc7a3c7c029f334fc" + integrity sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg== + dependencies: + micromark-util-character "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-factory-title@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz#237e4aa5d58a95863f01032d9ee9b090f1de6e94" + integrity sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw== + dependencies: + micromark-factory-space "^2.0.0" + micromark-util-character "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-factory-whitespace@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz#06b26b2983c4d27bfcc657b33e25134d4868b0b1" + integrity sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ== + dependencies: + micromark-factory-space "^2.0.0" + micromark-util-character "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-util-character@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/micromark-util-character/-/micromark-util-character-2.1.1.tgz#2f987831a40d4c510ac261e89852c4e9703ccda6" + integrity sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q== + dependencies: + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-util-chunked@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz#47fbcd93471a3fccab86cff03847fc3552db1051" + integrity sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA== + dependencies: + micromark-util-symbol "^2.0.0" + +micromark-util-classify-character@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz#d399faf9c45ca14c8b4be98b1ea481bced87b629" + integrity sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q== + dependencies: + micromark-util-character "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-util-combine-extensions@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz#2a0f490ab08bff5cc2fd5eec6dd0ca04f89b30a9" + integrity sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg== + dependencies: + micromark-util-chunked "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-util-decode-numeric-character-reference@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz#fcf15b660979388e6f118cdb6bf7d79d73d26fe5" + integrity sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw== + dependencies: + micromark-util-symbol "^2.0.0" + +micromark-util-encode@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz#0d51d1c095551cfaac368326963cf55f15f540b8" + integrity sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw== + +micromark-util-html-tag-name@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz#e40403096481986b41c106627f98f72d4d10b825" + integrity sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA== + +micromark-util-normalize-identifier@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz#c30d77b2e832acf6526f8bf1aa47bc9c9438c16d" + integrity sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q== + dependencies: + micromark-util-symbol "^2.0.0" + +micromark-util-resolve-all@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz#e1a2d62cdd237230a2ae11839027b19381e31e8b" + integrity sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg== + dependencies: + micromark-util-types "^2.0.0" + +micromark-util-sanitize-uri@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz#ab89789b818a58752b73d6b55238621b7faa8fd7" + integrity sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ== + dependencies: + micromark-util-character "^2.0.0" + micromark-util-encode "^2.0.0" + micromark-util-symbol "^2.0.0" + +micromark-util-subtokenize@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz#d8ade5ba0f3197a1cf6a2999fbbfe6357a1a19ee" + integrity sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA== + dependencies: + devlop "^1.0.0" + micromark-util-chunked "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-util-symbol@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz#e5da494e8eb2b071a0d08fb34f6cefec6c0a19b8" + integrity sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q== + +micromark-util-types@2.0.2, micromark-util-types@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/micromark-util-types/-/micromark-util-types-2.0.2.tgz#f00225f5f5a0ebc3254f96c36b6605c4b393908e" + integrity sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA== + +micromark@4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/micromark/-/micromark-4.0.2.tgz#91395a3e1884a198e62116e33c9c568e39936fdb" + integrity sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA== + dependencies: + "@types/debug" "^4.0.0" + debug "^4.0.0" + decode-named-character-reference "^1.0.0" + devlop "^1.0.0" + micromark-core-commonmark "^2.0.0" + micromark-factory-space "^2.0.0" + micromark-util-character "^2.0.0" + micromark-util-chunked "^2.0.0" + micromark-util-combine-extensions "^2.0.0" + micromark-util-decode-numeric-character-reference "^2.0.0" + micromark-util-encode "^2.0.0" + micromark-util-normalize-identifier "^2.0.0" + micromark-util-resolve-all "^2.0.0" + micromark-util-sanitize-uri "^2.0.0" + micromark-util-subtokenize "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + micromatch@^4.0.4, micromatch@^4.0.8: version "4.0.8" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202" @@ -7429,6 +7740,19 @@ parent-module@^1.0.0: dependencies: callsites "^3.0.0" +parse-entities@^4.0.0: + version "4.0.2" + resolved "https://registry.yarnpkg.com/parse-entities/-/parse-entities-4.0.2.tgz#61d46f5ed28e4ee62e9ddc43d6b010188443f159" + integrity sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw== + dependencies: + "@types/unist" "^2.0.0" + character-entities-legacy "^3.0.0" + character-reference-invalid "^2.0.0" + decode-named-character-reference "^1.0.0" + is-alphanumerical "^2.0.0" + is-decimal "^2.0.0" + is-hexadecimal "^2.0.0" + parse-json@^5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" @@ -8172,13 +8496,13 @@ sprintf-js@~1.0.2: resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g== -"sproxydclient@github:scality/sproxydclient#8.2.2": - version "8.2.2" - resolved "https://codeload.github.com/scality/sproxydclient/tar.gz/62d3aac1c844beda2286f33a1a7c7f2a31937579" +"sproxydclient@github:scality/sproxydclient#8.2.3": + version "8.2.3" + resolved "https://codeload.github.com/scality/sproxydclient/tar.gz/31cdab50c1c17104d8f009be87a2863df6a697a6" dependencies: async "^3.2.6" - httpagent "github:scality/httpagent#1.1.0" - werelogs scality/werelogs#8.2.0 + httpagent "github:scality/httpagent#1.1.1" + werelogs scality/werelogs#8.2.5 ssri@^12.0.0: version "12.0.0" @@ -8653,11 +8977,6 @@ typewiselite@~1.0.0: resolved "https://registry.yarnpkg.com/typewiselite/-/typewiselite-1.0.0.tgz#c8882fa1bb1092c06005a97f34ef5c8508e3664e" integrity sha512-J9alhjVHupW3Wfz6qFRGgQw0N3gr8hOkw6zm7FZ6UR1Cse/oD9/JVok7DNE9TT9IbciDHX2Ex9+ksE6cRmtymw== -uc.micro@^1.0.1, uc.micro@^1.0.5: - version "1.0.6" - resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.6.tgz#9c411a802a409a91fc6cf74081baba34b24499ac" - integrity sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA== - uglify-js@^3.1.4: version "3.19.3" resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.19.3.tgz#82315e9bbc6f2b25888858acd1fff8441035b77f" @@ -8796,9 +9115,10 @@ webidl-conversions@^7.0.0: resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-7.0.0.tgz#256b4e1882be7debbf01d05f0aa2039778ea080a" integrity sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g== -"werelogs@github:scality/werelogs#8.2.2", werelogs@scality/werelogs#8.2.0, werelogs@scality/werelogs#8.2.4: - version "8.2.4" - resolved "https://codeload.github.com/scality/werelogs/tar.gz/a7bbb5917a08b035d3763b24b070d517483d6982" +"werelogs@github:scality/werelogs#8.2.2", werelogs@scality/werelogs#8.2.5: + version "8.2.5" + uid "41188826d661227f98ebaaeda7df5e5862809bc7" + resolved "https://codeload.github.com/scality/werelogs/tar.gz/41188826d661227f98ebaaeda7df5e5862809bc7" dependencies: fast-safe-stringify "^2.1.1" safe-json-stringify "^1.2.0"