From 0989258cbdf0b9974e5a88bed4b97e5350faf715 Mon Sep 17 00:00:00 2001 From: Tim Perry Date: Tue, 13 Aug 2024 14:50:23 +0200 Subject: [PATCH 001/168] Add a context menu to the send tabs --- src/components/send/send-page.tsx | 12 +++++++-- src/components/send/send-tabs.tsx | 42 ++++++++++++++++++++++++++++--- 2 files changed, 49 insertions(+), 5 deletions(-) diff --git a/src/components/send/send-page.tsx b/src/components/send/send-page.tsx index f1497a8a..59510dd0 100644 --- a/src/components/send/send-page.tsx +++ b/src/components/send/send-page.tsx @@ -8,6 +8,7 @@ import { WithInjected } from '../../types'; import { ApiError } from '../../services/server-api-types'; import { SendStore } from '../../model/send/send-store'; +import { UiStore } from '../../model/ui/ui-store'; import { ContainerSizedEditor } from '../editor/base-editor'; @@ -48,9 +49,11 @@ const SendPageKeyboardShortcuts = (props: { }; @inject('sendStore') +@inject('uiStore') @observer class SendPage extends React.Component<{ - sendStore: SendStore + sendStore: SendStore, + uiStore: UiStore, navigate: (path: string) => void }> { @@ -95,6 +98,10 @@ class SendPage extends React.Component<{ addRequestInput } = this.props.sendStore; + const { + handleContextMenuEvent + } = this.props.uiStore; + return ; export { InjectedSendPage as SendPage }; \ No newline at end of file diff --git a/src/components/send/send-tabs.tsx b/src/components/send/send-tabs.tsx index 99606057..04694380 100644 --- a/src/components/send/send-tabs.tsx +++ b/src/components/send/send-tabs.tsx @@ -1,10 +1,12 @@ +import * as _ from 'lodash'; import * as React from 'react'; import { observer } from 'mobx-react-lite'; import { css, styled } from '../../styles'; -import { SendRequest } from '../../model/send/send-request-model'; +import { RequestInput, SendRequest } from '../../model/send/send-request-model'; import { getMethodColor } from '../../model/events/categorization'; +import { ContextMenuItem } from '../../model/ui/context-menu'; import { UnstyledButton } from '../common/inputs'; import { IconButton } from '../common/icon-button'; @@ -114,7 +116,8 @@ const SendTab = observer((props: { sendRequest: SendRequest, isSelectedTab: boolean, onSelectTab: (request: SendRequest) => void, - onCloseTab: (request: SendRequest) => void + onCloseTab: (request: SendRequest) => void, + onContextMenu: (event: React.MouseEvent, request: SendRequest) => void }) => { const { request } = props.sendRequest; @@ -133,10 +136,15 @@ const SendTab = observer((props: { event.stopPropagation(); }, [props.onCloseTab, props.sendRequest]); + const onContextMenu = React.useCallback((event: React.MouseEvent) => { + props.onContextMenu(event, props.sendRequest); + }, [props.onContextMenu, props.sendRequest]); + return void; onMoveSelection: (distance: number) => void; onCloseTab: (sendRequest: SendRequest) => void; - onAddTab: () => void; + onAddTab: (requestInput?: RequestInput) => void; + onContextMenu: (event: React.MouseEvent, items: readonly ContextMenuItem[]) => void; }) => { const containerRef = React.useRef(null); @@ -205,6 +214,32 @@ export const SendTabs = observer((props: { event.stopPropagation(); }, []); + const onContextMenu = React.useCallback((event: React.MouseEvent, request: SendRequest) => { + event.preventDefault(); + props.onContextMenu(event, [ + { + type: 'option', + label: 'Duplicate Tab', + callback: () => props.onAddTab(_.cloneDeep(request.request)) + }, + { + type: 'option', + label: 'Close Tab', + callback: () => props.onCloseTab(request) + }, + { + type: 'option', + label: 'Close Other Tabs', + callback: () => { + const tabs = [...props.sendRequests]; + for (let tab of tabs) { + if (tab !== request) props.onCloseTab(tab); + } + } + } + ]); + }, [props.onAddTab, props.onCloseTab, props.sendRequests]); + return }) } From d44a53db7a28b756abca006073701155f1a24efe Mon Sep 17 00:00:00 2001 From: Tim Perry Date: Fri, 16 Aug 2024 12:20:22 +0200 Subject: [PATCH 002/168] Fix error message typo --- src/components/send/sent-response-error.tsx | 2 +- src/components/view/http/http-error-header.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/send/sent-response-error.tsx b/src/components/send/sent-response-error.tsx index 5bef28ae..8e6e89f4 100644 --- a/src/components/send/sent-response-error.tsx +++ b/src/components/send/sent-response-error.tsx @@ -74,7 +74,7 @@ export const SentResponseError = (props: { : errorType === 'host-unreachable' ? 'was not reachable on your network connection' : errorType === 'host-not-found' || errorType === 'dns-error' - ? 'hostname could be not found' + ? 'hostname could not be found' : errorType === 'connection-refused' ? 'refused the connection' : unreachableCheck(errorType) diff --git a/src/components/view/http/http-error-header.tsx b/src/components/view/http/http-error-header.tsx index 1e925cb5..cd24d022 100644 --- a/src/components/view/http/http-error-header.tsx +++ b/src/components/view/http/http-error-header.tsx @@ -92,7 +92,7 @@ export const HttpErrorHeader = (p: { : p.type === 'host-unreachable' ? 'was not reachable on your network connection' : p.type === 'host-not-found' || p.type === 'dns-error' - ? 'hostname could be not found' + ? 'hostname could not be found' : p.type === 'connection-refused' ? 'refused the connection' : unreachableCheck(p.type) From a139825595b90693a3c9899781e3c4e646dfbfc3 Mon Sep 17 00:00:00 2001 From: Blue Date: Sun, 18 Aug 2024 15:34:29 -0400 Subject: [PATCH 003/168] Update Protobuf Content Types Adds 'application/x-protobuffer' and 'application/grpc'. --- src/model/events/content-types.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/model/events/content-types.ts b/src/model/events/content-types.ts index 814ac737..bb5b4449 100644 --- a/src/model/events/content-types.ts +++ b/src/model/events/content-types.ts @@ -94,6 +94,8 @@ const mimeTypeToContentTypeMap: { [mimeType: string]: ViewableContentType } = { 'application/vnd.google.protobuf': 'protobuf', 'application/x-google-protobuf': 'protobuf', 'application/proto': 'protobuf', // N.b. this covers all application/XXX+proto values + 'application/grpc': 'protobuf', // Used in GRPC requests + 'application/x-protobuffer': 'protobuf' // Commonly seen in Google apps 'application/octet-stream': 'raw' } as const; @@ -192,4 +194,4 @@ export function getCompatibleTypes( types.add('raw'); return Array.from(types); -} \ No newline at end of file +} From c2c9c2e904879a40ce112254166fc68d520ab627 Mon Sep 17 00:00:00 2001 From: Tim Perry Date: Tue, 20 Aug 2024 11:09:50 +0200 Subject: [PATCH 004/168] Switch to our new SCW deploy action --- .github/workflows/ci.yml | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 432e87d9..479835eb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -122,7 +122,7 @@ jobs: needs: publish-docker steps: - name: Redeploy container - uses: thibaultchazal/scaleway-serverless-container-deploy-action@0d290edda0c3359e51442bd8bf730eafef4e290f + uses: httptoolkit/scaleway-serverless-container-deploy-action@v1 with: container_id: ${{ vars.SCW_API_CONTAINER_ID }} region: ${{ vars.SCW_API_CONTAINER_REGION }} @@ -130,7 +130,7 @@ jobs: registry_image_url: "registry.hub.docker.com/httptoolkit/ui:prod" - name: Redeploy failover container - uses: thibaultchazal/scaleway-serverless-container-deploy-action@0d290edda0c3359e51442bd8bf730eafef4e290f + uses: httptoolkit/scaleway-serverless-container-deploy-action@v1 with: container_id: ${{ vars.SCW_FAILOVER_API_CONTAINER_ID }} region: ${{ vars.SCW_FAILOVER_API_CONTAINER_REGION }} @@ -139,9 +139,6 @@ jobs: - name: Flush CDN cache run: | - # Wait a little - the reploy commands don't wait for the container to start up - sleep 30 - # Clear CDN cache to re-request content: curl -f --request POST \ --url https://api.bunny.net/pullzone/$PULL_ZONE_ID/purgeCache \ From 844b26788b66d6ff7b3b12ad0dfb89be55bd2a91 Mon Sep 17 00:00:00 2001 From: Tim Perry Date: Tue, 20 Aug 2024 14:27:37 +0200 Subject: [PATCH 005/168] Make TLS error info a little clearer that it could be _our_ fault Most of the time it's not, but in some niche cases your TLS configuration on the settings page could be broken (loading a PFX containing certs with legacy algorithms) in which case we do need to consider the possibility that might be the issue. --- src/components/send/sent-response-error.tsx | 4 ++-- src/components/view/http/http-error-header.tsx | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/components/send/sent-response-error.tsx b/src/components/send/sent-response-error.tsx index 8e6e89f4..81a87778 100644 --- a/src/components/send/sent-response-error.tsx +++ b/src/components/send/sent-response-error.tsx @@ -70,7 +70,7 @@ export const SentResponseError = (props: { : errorType === 'untrusted' ? 'has an untrusted HTTPS certificate' : errorType === 'tls-error' - ? 'failed to complete a TLS handshake' + ? 'could not complete a TLS handshake' : errorType === 'host-unreachable' ? 'was not reachable on your network connection' : errorType === 'host-not-found' || errorType === 'dns-error' @@ -78,7 +78,7 @@ export const SentResponseError = (props: { : errorType === 'connection-refused' ? 'refused the connection' : unreachableCheck(errorType) - }, so HTTP Toolkit did not send the request. + }, so HTTP Toolkit didn't send the request. : wasServerIssue(errorType) ? diff --git a/src/components/view/http/http-error-header.tsx b/src/components/view/http/http-error-header.tsx index cd24d022..348895f5 100644 --- a/src/components/view/http/http-error-header.tsx +++ b/src/components/view/http/http-error-header.tsx @@ -88,7 +88,7 @@ export const HttpErrorHeader = (p: { : p.type === 'untrusted' ? 'has an untrusted HTTPS certificate' : p.type === 'tls-error' - ? 'failed to complete a TLS handshake' + ? 'could not complete a TLS handshake' : p.type === 'host-unreachable' ? 'was not reachable on your network connection' : p.type === 'host-not-found' || p.type === 'dns-error' @@ -96,7 +96,7 @@ export const HttpErrorHeader = (p: { : p.type === 'connection-refused' ? 'refused the connection' : unreachableCheck(p.type) - }, so HTTP Toolkit did not forward the request. + }, so HTTP Toolkit didn't forward the request. : wasTimeout(p.type) ? <> @@ -135,15 +135,15 @@ export const HttpErrorHeader = (p: { { p.type === 'tls-error' ? <> - This could be caused by the server not supporting modern cipher - standards or TLS versions, requiring a client certificate that hasn't - been provided, or other TLS configuration issues. + This could be caused by the server not supporting modern ciphers or + TLS versions, expecting a client certificate that wasn't provided, + or TLS configuration issues in either the server or HTTP Toolkit. { p.isPaidUser ? <> From the Settings page you can configure client certificates, or - whitelist this host to relax HTTPS requirements and allow + whitelist this host to relax security requirements and allow self-signed certificates, which may resolve some TLS issues. : <> From c8b074141c5328beac37914a16404837c12a6dc1 Mon Sep 17 00:00:00 2001 From: Blue Date: Mon, 26 Aug 2024 23:45:38 -0400 Subject: [PATCH 006/168] Add missing comma --- src/model/events/content-types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/model/events/content-types.ts b/src/model/events/content-types.ts index bb5b4449..e65a2206 100644 --- a/src/model/events/content-types.ts +++ b/src/model/events/content-types.ts @@ -95,7 +95,7 @@ const mimeTypeToContentTypeMap: { [mimeType: string]: ViewableContentType } = { 'application/x-google-protobuf': 'protobuf', 'application/proto': 'protobuf', // N.b. this covers all application/XXX+proto values 'application/grpc': 'protobuf', // Used in GRPC requests - 'application/x-protobuffer': 'protobuf' // Commonly seen in Google apps + 'application/x-protobuffer': 'protobuf', // Commonly seen in Google apps 'application/octet-stream': 'raw' } as const; From 35b4bc6461a1a0901924f2f4a0aebc0b3a04aff9 Mon Sep 17 00:00:00 2001 From: Blue Date: Tue, 27 Aug 2024 20:47:38 -0400 Subject: [PATCH 007/168] Added gRPC parsing support for display in the UI --- src/model/events/body-formatting.ts | 6 ++++++ src/model/events/content-types.ts | 11 +++++++++-- src/services/ui-worker-formatters.ts | 20 +++++++++++++++++++- src/util/protobuf.ts | 19 +++++++++++++++++++ 4 files changed, 53 insertions(+), 3 deletions(-) diff --git a/src/model/events/body-formatting.ts b/src/model/events/body-formatting.ts index b4a2ad9b..f07957bd 100644 --- a/src/model/events/body-formatting.ts +++ b/src/model/events/body-formatting.ts @@ -142,6 +142,12 @@ export const Formatters: { [key in ViewableContentType]: Formatter } = { isEditApplicable: false, render: buildAsyncRenderer('protobuf') }, + grpc: { + language: 'grpc', + cacheKey: Symbol('grpc'), + isEditApplicable: false, + render: buildAsyncRenderer('grpc') + }, 'url-encoded': { layout: 'scrollable', Component: styled(ReadOnlyParams).attrs((p: FormatComponentProps) => ({ diff --git a/src/model/events/content-types.ts b/src/model/events/content-types.ts index e65a2206..f721aabf 100644 --- a/src/model/events/content-types.ts +++ b/src/model/events/content-types.ts @@ -33,7 +33,8 @@ export type ViewableContentType = | 'markdown' | 'yaml' | 'image' - | 'protobuf'; + | 'protobuf' + | 'grpc'; export const EditableContentTypes = [ 'text', @@ -94,9 +95,10 @@ const mimeTypeToContentTypeMap: { [mimeType: string]: ViewableContentType } = { 'application/vnd.google.protobuf': 'protobuf', 'application/x-google-protobuf': 'protobuf', 'application/proto': 'protobuf', // N.b. this covers all application/XXX+proto values - 'application/grpc': 'protobuf', // Used in GRPC requests 'application/x-protobuffer': 'protobuf', // Commonly seen in Google apps + 'application/grpc': 'grpc', // Used in GRPC requests (protobuf with special headers) + 'application/octet-stream': 'raw' } as const; @@ -122,6 +124,7 @@ export function getContentEditorName(contentType: ViewableContentType): string { : contentType === 'json' ? 'JSON' : contentType === 'css' ? 'CSS' : contentType === 'url-encoded' ? 'URL-Encoded' + : contentType === 'grpc' ? 'gRPC' : _.capitalize(contentType); } @@ -163,10 +166,14 @@ export function getCompatibleTypes( types.add('xml'); } + if (!types.has('grpc') && rawContentType && rawContentType.startsWith('application/grpc')) { + types.add('grpc') + } if ( body && isProbablyProtobuf(body) && !types.has('protobuf') && + !types.has('grpc') && // If it's probably unmarked protobuf, and it's a manageable size, try // parsing it just to check: (body.length < 100_000 && isValidProtobuf(body)) diff --git a/src/services/ui-worker-formatters.ts b/src/services/ui-worker-formatters.ts index 260d3aad..e6e0e099 100644 --- a/src/services/ui-worker-formatters.ts +++ b/src/services/ui-worker-formatters.ts @@ -6,7 +6,7 @@ import { import * as beautifyXml from 'xml-beautifier'; import { bufferToHex, bufferToString, getReadableSize } from '../util/buffer'; -import { parseRawProtobuf } from '../util/protobuf'; +import { parseRawProtobuf, parseGrpcProtobuf } from '../util/protobuf'; const truncationMarker = (size: string) => `\n[-- Truncated to ${size} --]`; const FIVE_MB = 1024 * 1024 * 5; @@ -78,6 +78,24 @@ const WorkerFormatters = { prefix: '' }); + return JSON.stringify(data, (_key, value) => { + // Buffers have toJSON defined, so arrive here in JSONified form: + if (value.type === 'Buffer' && Array.isArray(value.data)) { + const buffer = Buffer.from(value.data); + + return { + "Type": `Buffer (${getReadableSize(buffer)})`, + "As string": bufferToString(buffer, 'detect-encoding'), + "As hex": bufferToHex(buffer) + } + } else { + return value; + } + }, 2); + }, + grpc: (content: Buffer) => { + const data = parseGrpcProtobuf(content); + return JSON.stringify(data, (_key, value) => { // Buffers have toJSON defined, so arrive here in JSONified form: if (value.type === 'Buffer' && Array.isArray(value.data)) { diff --git a/src/util/protobuf.ts b/src/util/protobuf.ts index dd3eac1a..ca4d4167 100644 --- a/src/util/protobuf.ts +++ b/src/util/protobuf.ts @@ -28,6 +28,25 @@ export function isProbablyProtobuf(input: Uint8Array) { export const parseRawProtobuf = parseRawProto; +// The repeated sequence of Length-Prefixed-Message items is delivered in DATA frames + +// Length-Prefixed-Message → Compressed-Flag Message-Length Message +// Compressed-Flag → 0 / 1 ; encoded as 1 byte unsigned integer +// Message-Length → {length of Message} ; encoded as 4 byte unsigned integer (big endian) +// Message → *{binary octet} +export const parseGrpcProtobuf = (input: Buffer) => { + if (input.readInt8() != 0) { + // TODO support compressed gRPC messages? + throw new Error("compressed gRPC messages not yet supported") + } + const length = input.readInt32BE(1); + input = input.slice(5, 5+length); + + return parseRawProtobuf(input, { + prefix: '' + }); +} + export const isValidProtobuf = (input: Uint8Array) => { try { parseRawProtobuf(input); From 186acbd98b187e068b65f921185ba69c71c49d38 Mon Sep 17 00:00:00 2001 From: Blue Date: Tue, 27 Aug 2024 20:59:00 -0400 Subject: [PATCH 008/168] Tighten application/grpc check to avoid including JSON --- src/model/events/content-types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/model/events/content-types.ts b/src/model/events/content-types.ts index f721aabf..bb6dcaa4 100644 --- a/src/model/events/content-types.ts +++ b/src/model/events/content-types.ts @@ -166,7 +166,7 @@ export function getCompatibleTypes( types.add('xml'); } - if (!types.has('grpc') && rawContentType && rawContentType.startsWith('application/grpc')) { + if (!types.has('grpc') && rawContentType && rawContentType === 'application/grpc') { types.add('grpc') } if ( From 5f2fdfde29cf05896f211e38897bfae43e03a490 Mon Sep 17 00:00:00 2001 From: Tim Perry Date: Wed, 28 Aug 2024 17:37:13 +0200 Subject: [PATCH 009/168] Upgrade to the new accounts module to prepare for upcoming changes --- package-lock.json | 14 +++++++------- package.json | 2 +- src/components/settings/settings-page.tsx | 6 +++--- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/package-lock.json b/package-lock.json index daee4bad..0a6152aa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,7 @@ "@fortawesome/free-regular-svg-icons": "^5.12.1", "@fortawesome/free-solid-svg-icons": "^5.12.1", "@fortawesome/react-fontawesome": "^0.1.8", - "@httptoolkit/accounts": "^2.1.1", + "@httptoolkit/accounts": "^2.2.0", "@httptoolkit/httpsnippet": "^2.1.7", "@open-rpc/meta-schema": "^1.14.2", "@phosphor-icons/react": "^2.1.5", @@ -2552,9 +2552,9 @@ "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" }, "node_modules/@httptoolkit/accounts": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@httptoolkit/accounts/-/accounts-2.1.1.tgz", - "integrity": "sha512-XYpojjFQsOmS902M6U3KFt5saoMRQxVIPooFMbsVTLZ4VM6dbSp4oiBBuXWPa3BFxixtFTWXD9qXs8ZclG5/3Q==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@httptoolkit/accounts/-/accounts-2.2.0.tgz", + "integrity": "sha512-vIDy4eXySOC7D/73SoBGdLN3fRlW/QKCYdbjCA9e3fmVNlsoyc1pJqz18yuGcBi1CzSBrSfIKmlEc9GkFe788w==", "dependencies": { "@httptoolkit/auth0-lock": "^12.4.1", "@httptoolkit/util": "^0.1.1", @@ -23181,9 +23181,9 @@ } }, "@httptoolkit/accounts": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@httptoolkit/accounts/-/accounts-2.1.1.tgz", - "integrity": "sha512-XYpojjFQsOmS902M6U3KFt5saoMRQxVIPooFMbsVTLZ4VM6dbSp4oiBBuXWPa3BFxixtFTWXD9qXs8ZclG5/3Q==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@httptoolkit/accounts/-/accounts-2.2.0.tgz", + "integrity": "sha512-vIDy4eXySOC7D/73SoBGdLN3fRlW/QKCYdbjCA9e3fmVNlsoyc1pJqz18yuGcBi1CzSBrSfIKmlEc9GkFe788w==", "requires": { "@httptoolkit/auth0-lock": "^12.4.1", "@httptoolkit/util": "^0.1.1", diff --git a/package.json b/package.json index b13cb7e7..fa303b3c 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "@fortawesome/free-regular-svg-icons": "^5.12.1", "@fortawesome/free-solid-svg-icons": "^5.12.1", "@fortawesome/react-fontawesome": "^0.1.8", - "@httptoolkit/accounts": "^2.1.1", + "@httptoolkit/accounts": "^2.2.0", "@httptoolkit/httpsnippet": "^2.1.7", "@open-rpc/meta-schema": "^1.14.2", "@phosphor-icons/react": "^2.1.5", diff --git a/src/components/settings/settings-page.tsx b/src/components/settings/settings-page.tsx index fc2bb1c3..62a3c2e3 100644 --- a/src/components/settings/settings-page.tsx +++ b/src/components/settings/settings-page.tsx @@ -193,10 +193,10 @@ class SettingsPage extends React.Component { { subscriptionPlans.state === 'fulfilled' - ? (subscriptionPlans.value as SubscriptionPlans)[sub.plan]?.name + ? (subscriptionPlans.value as SubscriptionPlans)[sub.sku]?.name // If the accounts API is unavailable for plan metadata for some reason, we can just // format the raw SKU to get something workable, no worries: - : _.startCase(sub.plan) + : _.startCase(sub.sku) } @@ -357,7 +357,7 @@ class SettingsPage extends React.Component { throw new Error("Can't cancel without a subscription"); } - const planName = SubscriptionPlans[subscription.plan].name; + const planName = SubscriptionPlans[subscription.sku].name; let cancelEffect: string; From 59d2a9c4bfa6153582306b76126d3d9db9d98e62 Mon Sep 17 00:00:00 2001 From: Tim Perry Date: Fri, 30 Aug 2024 13:14:27 +0200 Subject: [PATCH 010/168] Update loading bar logo --- src/images/loading-logo.png | Bin 0 -> 18064 bytes src/images/logo-large.png | Bin 13472 -> 0 bytes src/index.html | 7 ++++--- 3 files changed, 4 insertions(+), 3 deletions(-) create mode 100644 src/images/loading-logo.png delete mode 100644 src/images/logo-large.png diff --git a/src/images/loading-logo.png b/src/images/loading-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..84a31e1a6695f9cfa54a0a55d46c76af438642de GIT binary patch literal 18064 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 P00000NkvXXu0mjf<9;)C literal 0 HcmV?d00001 diff --git a/src/images/logo-large.png b/src/images/logo-large.png deleted file mode 100644 index b8114010ab81de0e4ea5c59ac7a7408db8facf48..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 13472 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
- - + +
From 1a72d2ef63a04e2f099f0dcc4fb7818cb7f99901 Mon Sep 17 00:00:00 2001 From: Tim Perry Date: Fri, 30 Aug 2024 13:35:13 +0200 Subject: [PATCH 011/168] Fix erroring (but still working) Scaleway deployment step --- .github/workflows/ci.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 479835eb..62db4885 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -118,7 +118,6 @@ jobs: name: Deploy to Scaleway if: github.event_name == 'push' && github.ref == 'refs/heads/main' runs-on: ubuntu-latest - container: httptoolkit/act-build-base needs: publish-docker steps: - name: Redeploy container From e187bf7ca2fa2ac9c2d509a6c6583fc631eb9124 Mon Sep 17 00:00:00 2001 From: Tim Perry Date: Fri, 30 Aug 2024 18:54:40 +0200 Subject: [PATCH 012/168] Add UI support for existing-chromium --- src/model/interception/interceptors.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/model/interception/interceptors.ts b/src/model/interception/interceptors.ts index edb1f9ef..8641a581 100644 --- a/src/model/interception/interceptors.ts +++ b/src/model/interception/interceptors.ts @@ -128,6 +128,20 @@ const INTERCEPT_OPTIONS: _.Dictionary = { tags: BROWSER_TAGS, getActivationOptions: getChromiumOptions }, + 'existing-chromium': { + name: 'Global Chromium', + description: [ + "Intercept your main Chromium profile globally", + "This captures all default Chromium traffic, so may interfere with normal usage" + ], + customActivation: onActivateExistingBrowser, + iconProps: [ + SourceIcons.Chromium, + { icon: ['fas', 'globe'], color: '#fafafa', size: '2x' } + ], + tags: BROWSER_TAGS, + getActivationOptions: getChromiumOptions + }, 'fresh-chromium-dev': { name: 'Chromium (Dev)', description: ["Intercept a fresh independent Chromium window"], From 8c31a0532d68da9d73da7e764828d666d32ef163 Mon Sep 17 00:00:00 2001 From: Tim Perry Date: Mon, 2 Sep 2024 19:49:27 +0200 Subject: [PATCH 013/168] Ensure grpc+json is handled as JSON, not protobuf --- src/model/events/body-formatting.ts | 8 ++--- src/model/events/content-types.ts | 35 +++++++++++++++------- src/services/ui-worker-formatters.ts | 2 +- test/unit/model/http/content-types.spec.ts | 15 ++++++++++ 4 files changed, 45 insertions(+), 15 deletions(-) diff --git a/src/model/events/body-formatting.ts b/src/model/events/body-formatting.ts index f07957bd..b626afc1 100644 --- a/src/model/events/body-formatting.ts +++ b/src/model/events/body-formatting.ts @@ -142,11 +142,11 @@ export const Formatters: { [key in ViewableContentType]: Formatter } = { isEditApplicable: false, render: buildAsyncRenderer('protobuf') }, - grpc: { - language: 'grpc', - cacheKey: Symbol('grpc'), + 'grpc-proto': { + language: 'protobuf', + cacheKey: Symbol('grpc-proto'), isEditApplicable: false, - render: buildAsyncRenderer('grpc') + render: buildAsyncRenderer('grpc-proto') }, 'url-encoded': { layout: 'scrollable', diff --git a/src/model/events/content-types.ts b/src/model/events/content-types.ts index bb6dcaa4..20d566a2 100644 --- a/src/model/events/content-types.ts +++ b/src/model/events/content-types.ts @@ -8,18 +8,32 @@ import { // Simplify a mime type as much as we can, without throwing any errors export const getBaseContentType = (mimeType: string | undefined) => { const typeWithoutParams = (mimeType || '').split(';')[0]; - const [type, combinedSubTypes] = typeWithoutParams.split(/\/(.+)/); + let [type, combinedSubTypes] = typeWithoutParams.split(/\/(.+)/); if (!combinedSubTypes) return type; - // A list of types from most specific to most generic: [svg, xml] for image/svg+xml - const subTypes = combinedSubTypes.split('+'); + if (DEFAULT_SUBTYPE[combinedSubTypes]) { + combinedSubTypes = `${combinedSubTypes}+${DEFAULT_SUBTYPE[combinedSubTypes]}`; + } + + // If this is a known type with an exact match, return that directly: + if (mimeTypeToContentTypeMap[type + '/' + combinedSubTypes]) { + return type + '/' + combinedSubTypes; + } + // Otherwise, wr collect a list of types from most specific to most generic: [svg, xml] for image/svg+xml + // and then look through in order to see if there are any matches here: + const subTypes = combinedSubTypes.split('+'); const possibleTypes = subTypes.map(st => type + '/' + st); - return _.find(possibleTypes, t => !!mimeTypeToContentTypeMap[t]) || + + return _.find(possibleTypes, t => !!mimeTypeToContentTypeMap[t]) || // Subtype match _.last(possibleTypes)!; // If we recognize none - return the most generic } +const DEFAULT_SUBTYPE: { [type: string]: string } = { + 'grpc': 'proto' // Protobuf is the default gRPC content type (but not the only one!) +}; + export type ViewableContentType = | 'raw' | 'text' @@ -34,7 +48,7 @@ export type ViewableContentType = | 'yaml' | 'image' | 'protobuf' - | 'grpc'; + | 'grpc-proto'; export const EditableContentTypes = [ 'text', @@ -97,7 +111,7 @@ const mimeTypeToContentTypeMap: { [mimeType: string]: ViewableContentType } = { 'application/proto': 'protobuf', // N.b. this covers all application/XXX+proto values 'application/x-protobuffer': 'protobuf', // Commonly seen in Google apps - 'application/grpc': 'grpc', // Used in GRPC requests (protobuf with special headers) + 'application/grpc+proto': 'grpc-proto', // Used in GRPC requests (protobuf but with special headers) 'application/octet-stream': 'raw' } as const; @@ -124,7 +138,7 @@ export function getContentEditorName(contentType: ViewableContentType): string { : contentType === 'json' ? 'JSON' : contentType === 'css' ? 'CSS' : contentType === 'url-encoded' ? 'URL-Encoded' - : contentType === 'grpc' ? 'gRPC' + : contentType === 'grpc-proto' ? 'gRPC' : _.capitalize(contentType); } @@ -166,14 +180,15 @@ export function getCompatibleTypes( types.add('xml'); } - if (!types.has('grpc') && rawContentType && rawContentType === 'application/grpc') { - types.add('grpc') + if (!types.has('grpc-proto') && rawContentType === 'application/grpc') { + types.add('grpc-proto') } + if ( body && isProbablyProtobuf(body) && !types.has('protobuf') && - !types.has('grpc') && + !types.has('grpc-proto') && // If it's probably unmarked protobuf, and it's a manageable size, try // parsing it just to check: (body.length < 100_000 && isValidProtobuf(body)) diff --git a/src/services/ui-worker-formatters.ts b/src/services/ui-worker-formatters.ts index e6e0e099..50ab5ba7 100644 --- a/src/services/ui-worker-formatters.ts +++ b/src/services/ui-worker-formatters.ts @@ -93,7 +93,7 @@ const WorkerFormatters = { } }, 2); }, - grpc: (content: Buffer) => { + 'grpc-proto': (content: Buffer) => { const data = parseGrpcProtobuf(content); return JSON.stringify(data, (_key, value) => { diff --git a/test/unit/model/http/content-types.spec.ts b/test/unit/model/http/content-types.spec.ts index 026ac58b..defdc826 100644 --- a/test/unit/model/http/content-types.spec.ts +++ b/test/unit/model/http/content-types.spec.ts @@ -49,6 +49,21 @@ describe('Content type parsing', () => { expect(ct).to.equal('raw'); }); + it('should render application/grpc as protobuf grpc', () => { + const ct = getContentType('application/grpc'); + expect(ct).to.equal('grpc-proto'); + }); + + it('should render application/grpc+proto as protobuf grpc', () => { + const ct = getContentType('application/grpc+proto'); + expect(ct).to.equal('grpc-proto'); + }); + + it('should render application/grpc+json as JSON', () => { + const ct = getContentType('application/grpc+json'); + expect(ct).to.equal('json'); + }); + it('should return undefined for unknown content', () => { const ct = getContentType('application/unknownsomething'); expect(ct).to.equal(undefined); From b896ddf943c1f786581020c6c44d89b625b15102 Mon Sep 17 00:00:00 2001 From: Tim Perry Date: Mon, 2 Sep 2024 19:49:48 +0200 Subject: [PATCH 014/168] Handle multiple protobuf messages in the same grpc request --- src/services/ui-worker-formatters.ts | 7 +++++-- src/util/protobuf.ts | 23 +++++++++++++---------- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/src/services/ui-worker-formatters.ts b/src/services/ui-worker-formatters.ts index 50ab5ba7..0907fec2 100644 --- a/src/services/ui-worker-formatters.ts +++ b/src/services/ui-worker-formatters.ts @@ -6,7 +6,7 @@ import { import * as beautifyXml from 'xml-beautifier'; import { bufferToHex, bufferToString, getReadableSize } from '../util/buffer'; -import { parseRawProtobuf, parseGrpcProtobuf } from '../util/protobuf'; +import { parseRawProtobuf, extractProtobufFromGrpc } from '../util/protobuf'; const truncationMarker = (size: string) => `\n[-- Truncated to ${size} --]`; const FIVE_MB = 1024 * 1024 * 5; @@ -94,7 +94,10 @@ const WorkerFormatters = { }, 2); }, 'grpc-proto': (content: Buffer) => { - const data = parseGrpcProtobuf(content); + const protobufMessages = extractProtobufFromGrpc(content); + + let data = protobufMessages.map((msg) => parseRawProtobuf(msg, { prefix: '' })); + if (data.length === 1) data = data[0]; return JSON.stringify(data, (_key, value) => { // Buffers have toJSON defined, so arrive here in JSONified form: diff --git a/src/util/protobuf.ts b/src/util/protobuf.ts index ca4d4167..aae5347d 100644 --- a/src/util/protobuf.ts +++ b/src/util/protobuf.ts @@ -28,23 +28,26 @@ export function isProbablyProtobuf(input: Uint8Array) { export const parseRawProtobuf = parseRawProto; +// GRPC message structure: // The repeated sequence of Length-Prefixed-Message items is delivered in DATA frames - // Length-Prefixed-Message → Compressed-Flag Message-Length Message // Compressed-Flag → 0 / 1 ; encoded as 1 byte unsigned integer // Message-Length → {length of Message} ; encoded as 4 byte unsigned integer (big endian) // Message → *{binary octet} -export const parseGrpcProtobuf = (input: Buffer) => { - if (input.readInt8() != 0) { - // TODO support compressed gRPC messages? - throw new Error("compressed gRPC messages not yet supported") +export const extractProtobufFromGrpc = (input: Buffer) => { + const protobufMessasges: Buffer[] = []; + + while (input.length > 0) { + if (input.readInt8() != 0) { + throw new Error("Compressed gRPC messages not yet supported") + } + + const length = input.readInt32BE(1); + protobufMessasges.push(input.slice(5, 5 + length)); + input = input.subarray(5 + length); } - const length = input.readInt32BE(1); - input = input.slice(5, 5+length); - return parseRawProtobuf(input, { - prefix: '' - }); + return protobufMessasges; } export const isValidProtobuf = (input: Uint8Array) => { From c771afa21aa8e3580a729969009fc63791e633ab Mon Sep 17 00:00:00 2001 From: Tim Perry Date: Mon, 9 Sep 2024 13:37:09 +0200 Subject: [PATCH 015/168] Fix bug in rule serialization for transform match/replace --- src/model/rules/definitions/http-rule-definitions.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/model/rules/definitions/http-rule-definitions.ts b/src/model/rules/definitions/http-rule-definitions.ts index cdc73f63..94176f8f 100644 --- a/src/model/rules/definitions/http-rule-definitions.ts +++ b/src/model/rules/definitions/http-rule-definitions.ts @@ -228,6 +228,14 @@ serializr.createModelSchema(TransformingHandler, { updateHeaders: serializeWithUndefineds, updateJsonBody: serializeWithUndefineds, replaceBody: serializeBuffer, + matchReplaceBody: serializr.list( + serializr.custom( + ([key, value]: [RegExp, string]) => + [{ source: key.source, flags: key.flags }, value], + ([key, value]: [{ source: string, flags: string }, string]) => + [new RegExp(key.source, key.flags), value] + ) + ), '*': Object.assign(serializr.raw(), { pattern: { test: () => true } }) }) ) From ac5a7f9ae17b6898d1a49c5431c12078e363cbc3 Mon Sep 17 00:00:00 2001 From: Tim Perry Date: Mon, 9 Sep 2024 14:48:14 +0200 Subject: [PATCH 016/168] Convert theme picker to Phosphor icons --- src/components/common/tabbed-options.tsx | 8 +++++--- src/components/settings/settings-page.tsx | 8 ++++---- src/icons.tsx | 8 ++++++++ 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/src/components/common/tabbed-options.tsx b/src/components/common/tabbed-options.tsx index 33c4478e..92e3aa9e 100644 --- a/src/components/common/tabbed-options.tsx +++ b/src/components/common/tabbed-options.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; -import { Icon, IconProp } from '../../icons'; +import { Icon, IconKey } from '../../icons'; import { styled, css } from '../../styles'; import { omit } from 'lodash'; import { UnstyledButton } from './inputs'; @@ -45,7 +45,7 @@ export const TabsContainer = styled((p: { export const Tab = styled((p: { className?: string, selected?: boolean, - icon: IconProp, + icon: IconKey, value: any, children: React.ReactNode }) => @@ -56,7 +56,7 @@ export const Tab = styled((p: { event.tabValue = p.value; }} > - + { p.children } )` @@ -89,5 +89,7 @@ export const Tab = styled((p: { > svg { margin-bottom: 10px; + width: 2em; + height: auto; } `; \ No newline at end of file diff --git a/src/components/settings/settings-page.tsx b/src/components/settings/settings-page.tsx index 62a3c2e3..b9d182c8 100644 --- a/src/components/settings/settings-page.tsx +++ b/src/components/settings/settings-page.tsx @@ -300,25 +300,25 @@ class SettingsPage extends React.Component { }} > Automatic Light Dark High Contrast diff --git a/src/icons.tsx b/src/icons.tsx index 0cea0257..86334cb6 100644 --- a/src/icons.tsx +++ b/src/icons.tsx @@ -19,6 +19,10 @@ import { Plug, QuestionMark, ArrowLeft, + MagicWand, + Sun, + Moon, + CircleHalf } from '@phosphor-icons/react'; export type IconKey = keyof typeof Icons; @@ -46,6 +50,10 @@ const Icons = { Plug, QuestionMark, ArrowLeft, + MagicWand, + Sun, + Moon, + CircleHalf } as const; // Import required FA icons: From f545851c910795f2adc9fd105ca6c75910d30009 Mon Sep 17 00:00:00 2001 From: Tim Perry Date: Mon, 9 Sep 2024 14:49:24 +0200 Subject: [PATCH 017/168] Add support for custom themes --- src/components/settings/settings-page.tsx | 26 ++++++++++++++++++----- src/icons.tsx | 6 ++++-- src/model/ui/ui-store.ts | 20 +++++++++++++++++ 3 files changed, 45 insertions(+), 7 deletions(-) diff --git a/src/components/settings/settings-page.tsx b/src/components/settings/settings-page.tsx index b9d182c8..bec37755 100644 --- a/src/components/settings/settings-page.tsx +++ b/src/components/settings/settings-page.tsx @@ -10,6 +10,7 @@ import { SubscriptionPlans } from '@httptoolkit/accounts'; import { WithInjected } from '../../types'; import { styled, Theme, ThemeName } from '../../styles'; import { Icon, WarningIcon } from '../../icons'; +import { uploadFile } from '../../util/ui'; import { AccountStore } from '../../model/account/account-store'; import { UiStore } from '../../model/ui/ui-store'; @@ -290,14 +291,23 @@ class SettingsPage extends React.Component { uiStore.setTheme(value)} - isSelected={(value: ThemeName | 'automatic' | Theme) => { - if (typeof value === 'string') { - return uiStore.themeName === value; + onClick={async (value: ThemeName | 'automatic' | 'custom') => { + if (value === 'custom') { + const themeFile = await uploadFile('text', ['.htktheme', '.htk-theme', '.json']); + if (!themeFile) return; + try { + const customTheme = uiStore.buildCustomTheme(themeFile); + uiStore.setTheme(customTheme); + } catch (e: any) { + alert(e.message || e); + } } else { - return _.isEqual(value, uiStore.theme); + uiStore.setTheme(value); } }} + isSelected={(value: ThemeName | 'automatic' | 'custom') => + uiStore.themeName === value + } > { > High Contrast + + Custom + diff --git a/src/icons.tsx b/src/icons.tsx index 86334cb6..52a16901 100644 --- a/src/icons.tsx +++ b/src/icons.tsx @@ -22,7 +22,8 @@ import { MagicWand, Sun, Moon, - CircleHalf + CircleHalf, + Swatches } from '@phosphor-icons/react'; export type IconKey = keyof typeof Icons; @@ -53,7 +54,8 @@ const Icons = { MagicWand, Sun, Moon, - CircleHalf + CircleHalf, + Swatches } as const; // Import required FA icons: diff --git a/src/model/ui/ui-store.ts b/src/model/ui/ui-store.ts index b1853493..b7d67faf 100644 --- a/src/model/ui/ui-store.ts +++ b/src/model/ui/ui-store.ts @@ -16,6 +16,7 @@ import { ContextMenuOption, buildNativeContextMenuItems } from './context-menu'; +import { tryParseJson } from '../../util'; const VIEW_CARD_KEYS = [ 'api', @@ -93,6 +94,11 @@ const SETTINGS_CARD_KEYS =[ ] as const; type SettingsCardKey = typeof SETTINGS_CARD_KEYS[number]; +type CustomTheme = Partial & { + name: string; + extends: ThemeName; +}; + export class UiStore { constructor( @@ -146,6 +152,20 @@ export class UiStore { } } + buildCustomTheme(themeFile: string) { + const themeData: Partial | undefined = tryParseJson(themeFile); + if (!themeData) throw new Error("Could not parse theme JSON"); + + if (!themeData.name) throw new Error('Theme must contain a `name` field'); + if (!themeData.extends) throw new Error('Theme must contain an `extends` field with a built-in theme name (dark/light/high-contrast)'); + + const baseTheme = Themes[themeData.extends]; + return { + ...baseTheme, + ...themeData + } as Theme; + } + @persist @observable private _themeName: ThemeName | 'automatic' | 'custom' = 'automatic'; From f5ec9ee2a668930564499212cbc12b0f63fe00ee Mon Sep 17 00:00:00 2001 From: Tim Perry Date: Mon, 9 Sep 2024 14:58:51 +0200 Subject: [PATCH 018/168] Actually check the theme extends field value --- src/model/ui/ui-store.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/model/ui/ui-store.ts b/src/model/ui/ui-store.ts index b7d67faf..557c85cb 100644 --- a/src/model/ui/ui-store.ts +++ b/src/model/ui/ui-store.ts @@ -157,7 +157,12 @@ export class UiStore { if (!themeData) throw new Error("Could not parse theme JSON"); if (!themeData.name) throw new Error('Theme must contain a `name` field'); - if (!themeData.extends) throw new Error('Theme must contain an `extends` field with a built-in theme name (dark/light/high-contrast)'); + if ( + !themeData.extends || + Themes[themeData.extends as ThemeName] === undefined + ) { + throw new Error('Theme must contain an `extends` field with a built-in theme name (dark/light/high-contrast)'); + } const baseTheme = Themes[themeData.extends]; return { From 8e5a706da7a48d23634006f96a8e93e670117947 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Sep 2024 21:24:34 +0000 Subject: [PATCH 019/168] Bump dompurify from 2.4.9 to 2.5.6 Bumps [dompurify](https://github.com/cure53/DOMPurify) from 2.4.9 to 2.5.6. - [Release notes](https://github.com/cure53/DOMPurify/releases) - [Commits](https://github.com/cure53/DOMPurify/compare/2.4.9...2.5.6) --- updated-dependencies: - dependency-name: dompurify dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- package-lock.json | 14 +++++++------- package.json | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0a6152aa..a31e2f89 100644 --- a/package-lock.json +++ b/package-lock.json @@ -67,7 +67,7 @@ "date-fns": "^1.30.1", "dedent": "^0.7.0", "deserialize-error": "0.0.3", - "dompurify": "^2.1.1", + "dompurify": "^2.5.6", "fast-json-patch": "^3.1.1", "graphql": "^15.8.0", "har-validator": "^5.1.3", @@ -7589,9 +7589,9 @@ } }, "node_modules/dompurify": { - "version": "2.4.9", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.4.9.tgz", - "integrity": "sha512-iHtnxYMotKgOTvxIqq677JsKHvCOkAFqj9x8Mek2zdeHW1XjuFKwjpmZeMaXQRQ8AbJZDbcRz/+r1QhwvFtmQg==" + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.5.6.tgz", + "integrity": "sha512-zUTaUBO8pY4+iJMPE1B9XlO2tXVYIcEA4SNGtvDELzTSCQO7RzH+j7S180BmhmJId78lqGU2z19vgVx2Sxs/PQ==" }, "node_modules/domutils": { "version": "2.8.0", @@ -27272,9 +27272,9 @@ } }, "dompurify": { - "version": "2.4.9", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.4.9.tgz", - "integrity": "sha512-iHtnxYMotKgOTvxIqq677JsKHvCOkAFqj9x8Mek2zdeHW1XjuFKwjpmZeMaXQRQ8AbJZDbcRz/+r1QhwvFtmQg==" + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.5.6.tgz", + "integrity": "sha512-zUTaUBO8pY4+iJMPE1B9XlO2tXVYIcEA4SNGtvDELzTSCQO7RzH+j7S180BmhmJId78lqGU2z19vgVx2Sxs/PQ==" }, "domutils": { "version": "2.8.0", diff --git a/package.json b/package.json index fa303b3c..718dda0f 100644 --- a/package.json +++ b/package.json @@ -92,7 +92,7 @@ "date-fns": "^1.30.1", "dedent": "^0.7.0", "deserialize-error": "0.0.3", - "dompurify": "^2.1.1", + "dompurify": "^2.5.6", "fast-json-patch": "^3.1.1", "graphql": "^15.8.0", "har-validator": "^5.1.3", From b3e28aad38198044c27f784d0517714f03ed4f1d Mon Sep 17 00:00:00 2001 From: Etienne Maheux Date: Thu, 26 Sep 2024 21:51:54 +0200 Subject: [PATCH 020/168] Minor improvements for base64 content + HAR _content is always base64-encoded --- .../config/android-device-config.tsx | 5 +--- src/model/events/content-types.ts | 25 +++++++++++++------ src/model/http/har.ts | 16 ++++++------ src/services/ui-worker-formatters.ts | 4 ++- test/unit/model/http/content-types.spec.ts | 19 +++++++++++++- 5 files changed, 48 insertions(+), 21 deletions(-) diff --git a/src/components/intercept/config/android-device-config.tsx b/src/components/intercept/config/android-device-config.tsx index 605d9b22..a1f681c7 100644 --- a/src/components/intercept/config/android-device-config.tsx +++ b/src/components/intercept/config/android-device-config.tsx @@ -64,10 +64,7 @@ const Spacer = styled.div` `; function urlSafeBase64(content: string) { - return stringToBuffer(content) - .toString('base64') - .replace(/\+/g, '-') - .replace(/\//g, '_'); + return stringToBuffer(content).toString('base64url'); } function getConfigRequestIds(eventsStore: EventsStore) { diff --git a/src/model/events/content-types.ts b/src/model/events/content-types.ts index 20d566a2..80d8c4ef 100644 --- a/src/model/events/content-types.ts +++ b/src/model/events/content-types.ts @@ -147,15 +147,25 @@ export function getDefaultMimeType(contentType: ViewableContentType): string { return _.findKey(mimeTypeToContentTypeMap, (c) => c === contentType)!; } -function isValidBase64Byte(byte: number) { +function isValidAlphaNumOrSpace(byte: number) { return (byte >= 65 && byte <= 90) || // A-Z (byte >= 97 && byte <= 122) || // a-z (byte >= 48 && byte <= 57) || // 0-9 - byte === 43 || // + - byte === 47 || // / byte === 61; // = } +function isValidStandardBase64Byte(byte: number) { + // + / (standard) + return byte === 43 || byte === 47 + || isValidAlphaNumOrSpace(byte); +} + +function isValidURLSafeBase64Byte(byte: number) { + // - _ (URL-safe version) + return byte === 45 || byte === 95 + || isValidAlphaNumOrSpace(byte); +} + export function getCompatibleTypes( contentType: ViewableContentType, rawContentType: string | undefined, @@ -203,10 +213,11 @@ export function getCompatibleTypes( if ( body && - body.length > 0 && - body.length % 4 === 0 && // Multiple of 4 bytes - body.length < 1000 * 100 && // < 100 KB of content - body.every(isValidBase64Byte) + !types.has('base64') && + body.length >= 8 && + // body.length % 4 === 0 && // Multiple of 4 bytes (final padding may be omitted) + body.length < 100_000 && // < 100 KB of content + (body.every(isValidStandardBase64Byte) || body.every(isValidURLSafeBase64Byte)) ) { types.add('base64'); } diff --git a/src/model/http/har.ts b/src/model/http/har.ts index 7a53bae3..d159bad3 100644 --- a/src/model/http/har.ts +++ b/src/model/http/har.ts @@ -51,15 +51,15 @@ interface HarLog extends HarFormat.Log { export type RequestContentData = { text: string; size: number; - encoding?: 'base64'; + encoding: 'base64'; comment?: string; }; export interface ExtendedHarRequest extends HarFormat.Request { _requestBodyStatus?: - | 'discarded:too-large' - | 'discarded:not-representable' - | 'discarded:not-decodable'; + | 'discarded:too-large' + | 'discarded:not-representable' // to indicate that extended field `_content` is populated with base64 `postData` + | 'discarded:not-decodable'; _content?: RequestContentData; _trailers?: HarFormat.Header[]; } @@ -302,7 +302,7 @@ async function generateHarResponse( const decoded = await response.body.decodedPromise; - let responseContent: { text: string, encoding?: string } | { comment: string}; + let responseContent: { text: string, encoding?: string } | { comment: string }; try { if (!decoded || decoded.byteLength > options.bodySizeLimit) { // If no body or the body is too large, don't include it @@ -435,10 +435,10 @@ function generateHarWebSocketMessage( return { // Note that msg.direction is from the perspective of Mockttp, not the client. type: message.direction === 'sent' - ? 'receive' + ? 'receive' : message.direction === 'received' ? 'send' - : unreachableCheck(message.direction), + : unreachableCheck(message.direction), opcode: message.isBinary ? 2 : 1, data: message.isBinary @@ -751,7 +751,7 @@ function parseHttpVersion( } function parseHarRequestContents(data: RequestContentData): Buffer { - if (data.encoding && Buffer.isEncoding(data.encoding)) { + if (Buffer.isEncoding(data.encoding)) { return Buffer.from(data.text, data.encoding); } diff --git a/src/services/ui-worker-formatters.ts b/src/services/ui-worker-formatters.ts index 0907fec2..67fef007 100644 --- a/src/services/ui-worker-formatters.ts +++ b/src/services/ui-worker-formatters.ts @@ -39,7 +39,9 @@ const WorkerFormatters = { } }, base64: (content: Buffer) => { - return Buffer.from(content.toString('utf8'), 'base64').toString('utf8'); + const b64 = content.toString('ascii'); + const encoding = b64.match(/[-_]/) ? 'base64url' : 'base64'; + return Buffer.from(b64, encoding).toString('utf8'); }, markdown: (content: Buffer) => { return content.toString('utf8'); diff --git a/test/unit/model/http/content-types.spec.ts b/test/unit/model/http/content-types.spec.ts index defdc826..3d098665 100644 --- a/test/unit/model/http/content-types.spec.ts +++ b/test/unit/model/http/content-types.spec.ts @@ -1,6 +1,6 @@ import { expect } from '../../../test-setup'; -import { getContentType, getEditableContentType } from '../../../../src/model/events/content-types'; +import { getContentType, getEditableContentType, getCompatibleTypes } from '../../../../src/model/events/content-types'; describe('Content type parsing', () => { describe('getContentType', () => { @@ -81,4 +81,21 @@ describe('Content type parsing', () => { expect(ct).to.equal(undefined); }); }); + + describe('getCompatibleTypes', () => { + it('should detect standard base64 text', () => { + const cts = getCompatibleTypes('text', 'text/plain', Buffer.from('FWTkm2+ZvMo=', 'ascii')); + expect(cts).to.deep.equal(['text', 'base64', 'raw']); + }); + + it('should detect URL-safe (without padding) base64 text', () => { + const cts = getCompatibleTypes('text', 'text/plain', Buffer.from('FWTkm2-ZvMo', 'ascii')); + expect(cts).to.deep.equal(['text', 'base64', 'raw']); + }); + + it('should work even if first character is not ASCII', () => { + const cts = getCompatibleTypes('raw', 'application/octet-stream', Buffer.from('1f8d08', 'hex')); // GZIP magic bytes + expect(cts).to.deep.equal(['raw', 'text']); + }); + }); }); \ No newline at end of file From 3d713b3b8f482a96e12afac398dc63f8f5b65009 Mon Sep 17 00:00:00 2001 From: Etienne Maheux Date: Thu, 26 Sep 2024 19:56:36 +0200 Subject: [PATCH 021/168] Handle gRPC compressed payloads + headers are sent to formatters + improve Protobuf/gRPC tests coverage --- src/components/editor/content-viewer.tsx | 5 +- src/components/editor/monaco.ts | 2 +- src/components/send/sent-response-body.tsx | 5 +- src/components/view/http/http-body-card.tsx | 5 +- src/model/events/body-formatting.ts | 25 +-- src/model/events/content-types.ts | 32 ++-- src/services/ui-worker-api.ts | 7 +- src/services/ui-worker-formatters.ts | 59 +++---- src/services/ui-worker.ts | 4 +- src/util/protobuf.ts | 104 ++++++++++-- test/unit/model/http/content-types.spec.ts | 35 ++++ test/unit/util/protobuf.spec.ts | 167 ++++++++++++++++++-- 12 files changed, 352 insertions(+), 98 deletions(-) diff --git a/src/components/editor/content-viewer.tsx b/src/components/editor/content-viewer.tsx index b9ea19df..999825e6 100644 --- a/src/components/editor/content-viewer.tsx +++ b/src/components/editor/content-viewer.tsx @@ -5,6 +5,7 @@ import { observer } from 'mobx-react'; import { SchemaObject } from 'openapi3-ts'; import * as portals from 'react-reverse-portal'; +import { Headers } from '../../types'; import { styled } from '../../styles'; import { ObservablePromise, isObservablePromise } from '../../util/observable'; import { asError, unreachableCheck } from '../../util/error'; @@ -22,7 +23,7 @@ interface ContentViewerProps { children: Buffer | string; schema?: SchemaObject; expanded: boolean; - rawContentType?: string; + headers?: Headers; contentType: ViewableContentType; editorNode: portals.HtmlPortalNode; cache: Map; @@ -199,7 +200,7 @@ export class ContentViewer extends React.Component { return ; } diff --git a/src/components/editor/monaco.ts b/src/components/editor/monaco.ts index 2c933a64..b79f4183 100644 --- a/src/components/editor/monaco.ts +++ b/src/components/editor/monaco.ts @@ -62,7 +62,7 @@ async function loadMonacoEditor(retries = 5): Promise { id: 'protobuf-decoding-header', command: { id: '', // No actual command defined here - title: "Automatically decoded from raw Protobuf data", + title: "Automatically decoded from raw Protobuf/gRPC data", }, }, ], diff --git a/src/components/send/sent-response-body.tsx b/src/components/send/sent-response-body.tsx index c8daedf0..ad06d87e 100644 --- a/src/components/send/sent-response-body.tsx +++ b/src/components/send/sent-response-body.tsx @@ -81,7 +81,8 @@ export class SentResponseBodyCard extends React.Component; + render(content: Buffer, headers?: Headers): string | ObservablePromise; } type FormatComponentProps = { content: Buffer; - rawContentType: string | undefined; + headers?: Headers; }; type FormatComponent = React.ComponentType; @@ -35,8 +36,8 @@ export function isEditorFormatter(input: any): input is EditorFormatter { } const buildAsyncRenderer = (formatKey: WorkerFormatterKey) => - (input: Buffer) => observablePromise( - formatBufferAsync(input, formatKey) + (input: Buffer, headers?: Headers) => observablePromise( + formatBufferAsync(input, formatKey, headers) ); export const Formatters: { [key in ViewableContentType]: Formatter } = { @@ -44,8 +45,8 @@ export const Formatters: { [key in ViewableContentType]: Formatter } = { language: 'text', cacheKey: Symbol('raw'), isEditApplicable: false, - render: (input: Buffer) => { - if (input.byteLength < 2000) { + render: (input: Buffer, headers?: Headers) => { + if (input.byteLength < 2_000) { try { // For short-ish inputs, we return synchronously - conveniently this avoids // showing the loading spinner that churns the layout in short content cases. @@ -55,7 +56,7 @@ export const Formatters: { [key in ViewableContentType]: Formatter } = { } } else { return observablePromise( - formatBufferAsync(input, 'raw') + formatBufferAsync(input, 'raw', headers) ); } } @@ -64,7 +65,7 @@ export const Formatters: { [key in ViewableContentType]: Formatter } = { language: 'text', cacheKey: Symbol('text'), isEditApplicable: false, - render: (input: Buffer) => { + render: (input: Buffer, headers?: Headers) => { return bufferToString(input); } }, @@ -102,8 +103,8 @@ export const Formatters: { [key in ViewableContentType]: Formatter } = { language: 'json', cacheKey: Symbol('json'), isEditApplicable: true, - render: (input: Buffer) => { - if (input.byteLength < 10000) { + render: (input: Buffer, headers?: Headers) => { + if (input.byteLength < 10_000) { const inputAsString = bufferToString(input); try { @@ -111,7 +112,7 @@ export const Formatters: { [key in ViewableContentType]: Formatter } = { // showing the loading spinner that churns the layout in short content cases. return JSON.stringify( JSON.parse(inputAsString), - null, 2); + null, 2); // ^ Same logic as in UI-worker-formatter } catch (e) { // Fallback to showing the raw un-formatted JSON: @@ -119,7 +120,7 @@ export const Formatters: { [key in ViewableContentType]: Formatter } = { } } else { return observablePromise( - formatBufferAsync(input, 'json') + formatBufferAsync(input, 'json', headers) ); } } diff --git a/src/model/events/content-types.ts b/src/model/events/content-types.ts index 80d8c4ef..ef85bbd9 100644 --- a/src/model/events/content-types.ts +++ b/src/model/events/content-types.ts @@ -1,8 +1,11 @@ import * as _ from 'lodash'; -import { MessageBody } from '../../types'; + +import { Headers, MessageBody } from '../../types'; import { isProbablyProtobuf, - isValidProtobuf + isValidProtobuf, + isProbablyGrpcProto, + isValidGrpcProto, } from '../../util/protobuf'; // Simplify a mime type as much as we can, without throwing any errors @@ -21,7 +24,7 @@ export const getBaseContentType = (mimeType: string | undefined) => { return type + '/' + combinedSubTypes; } - // Otherwise, wr collect a list of types from most specific to most generic: [svg, xml] for image/svg+xml + // Otherwise, we collect a list of types from most specific to most generic: [svg, xml] for image/svg+xml // and then look through in order to see if there are any matches here: const subTypes = combinedSubTypes.split('+'); const possibleTypes = subTypes.map(st => type + '/' + st); @@ -112,6 +115,9 @@ const mimeTypeToContentTypeMap: { [mimeType: string]: ViewableContentType } = { 'application/x-protobuffer': 'protobuf', // Commonly seen in Google apps 'application/grpc+proto': 'grpc-proto', // Used in GRPC requests (protobuf but with special headers) + 'application/grpc+protobuf': 'grpc-proto', + 'application/grpc-proto': 'grpc-proto', + 'application/grpc-protobuf': 'grpc-proto', 'application/octet-stream': 'raw' } as const; @@ -169,7 +175,8 @@ function isValidURLSafeBase64Byte(byte: number) { export function getCompatibleTypes( contentType: ViewableContentType, rawContentType: string | undefined, - body: MessageBody | Buffer | undefined + body: MessageBody | Buffer | undefined, + headers?: Headers, ): ViewableContentType[] { let types = new Set([contentType]); @@ -190,15 +197,11 @@ export function getCompatibleTypes( types.add('xml'); } - if (!types.has('grpc-proto') && rawContentType === 'application/grpc') { - types.add('grpc-proto') - } - if ( body && - isProbablyProtobuf(body) && !types.has('protobuf') && !types.has('grpc-proto') && + isProbablyProtobuf(body) && // If it's probably unmarked protobuf, and it's a manageable size, try // parsing it just to check: (body.length < 100_000 && isValidProtobuf(body)) @@ -206,6 +209,17 @@ export function getCompatibleTypes( types.add('protobuf'); } + if ( + body && + !types.has('grpc-proto') && + isProbablyGrpcProto(body, headers ?? {}) && + // If it's probably unmarked gRPC, and it's a manageable size, try + // parsing it just to check: + (body.length < 100_000 && isValidGrpcProto(body, headers ?? {})) + ) { + types.add('grpc-proto'); + } + // SVGs can always be shown as XML if (rawContentType && rawContentType.startsWith('image/svg')) { types.add('xml'); diff --git a/src/services/ui-worker-api.ts b/src/services/ui-worker-api.ts index 5b9770af..e08ba6f4 100644 --- a/src/services/ui-worker-api.ts +++ b/src/services/ui-worker-api.ts @@ -21,7 +21,7 @@ import type { ParseCertResponse } from './ui-worker'; -import { Omit } from '../types'; +import { Headers, Omit } from '../types'; import type { ApiMetadata, ApiSpec } from '../model/api/api-interfaces'; import { WorkerFormatterKey } from './ui-worker-formatters'; @@ -149,10 +149,11 @@ export async function parseCert(buffer: ArrayBuffer) { })).result; } -export async function formatBufferAsync(buffer: ArrayBuffer, format: WorkerFormatterKey) { +export async function formatBufferAsync(buffer: ArrayBuffer, format: WorkerFormatterKey, headers?: Headers) { return (await callApi({ type: 'format', buffer, - format + format, + headers, })).formatted; } \ No newline at end of file diff --git a/src/services/ui-worker-formatters.ts b/src/services/ui-worker-formatters.ts index 67fef007..dfb360c3 100644 --- a/src/services/ui-worker-formatters.ts +++ b/src/services/ui-worker-formatters.ts @@ -5,6 +5,7 @@ import { } from 'js-beautify/js/lib/beautifier'; import * as beautifyXml from 'xml-beautifier'; +import { Headers } from '../types'; import { bufferToHex, bufferToString, getReadableSize } from '../util/buffer'; import { parseRawProtobuf, extractProtobufFromGrpc } from '../util/protobuf'; @@ -13,10 +14,25 @@ const FIVE_MB = 1024 * 1024 * 5; export type WorkerFormatterKey = keyof typeof WorkerFormatters; -export function formatBuffer(buffer: ArrayBuffer, format: WorkerFormatterKey): string { - return WorkerFormatters[format](Buffer.from(buffer)); +export function formatBuffer(buffer: ArrayBuffer, format: WorkerFormatterKey, headers?: Headers): string { + return WorkerFormatters[format](Buffer.from(buffer), headers); } +const prettyProtobufView = (data: any) => JSON.stringify(data, (_key, value) => { + // Buffers have toJSON defined, so arrive here in JSONified form: + if (value.type === 'Buffer' && Array.isArray(value.data)) { + const buffer = Buffer.from(value.data); + + return { + "Type": `Buffer (${getReadableSize(buffer)})`, + "As string": bufferToString(buffer, 'detect-encoding'), + "As hex": bufferToHex(buffer) + } + } else { + return value; + } +}, 2); + // A subset of all possible formatters (those allowed by body-formatting), which require // non-trivial processing, and therefore need to be processed async. const WorkerFormatters = { @@ -76,44 +92,15 @@ const WorkerFormatters = { }); }, protobuf: (content: Buffer) => { - const data = parseRawProtobuf(content, { - prefix: '' - }); - - return JSON.stringify(data, (_key, value) => { - // Buffers have toJSON defined, so arrive here in JSONified form: - if (value.type === 'Buffer' && Array.isArray(value.data)) { - const buffer = Buffer.from(value.data); - - return { - "Type": `Buffer (${getReadableSize(buffer)})`, - "As string": bufferToString(buffer, 'detect-encoding'), - "As hex": bufferToHex(buffer) - } - } else { - return value; - } - }, 2); + const data = parseRawProtobuf(content, { prefix: '' }); + return prettyProtobufView(data); }, - 'grpc-proto': (content: Buffer) => { - const protobufMessages = extractProtobufFromGrpc(content); + 'grpc-proto': (content: Buffer, headers?: Headers) => { + const protobufMessages = extractProtobufFromGrpc(content, headers ?? {}); let data = protobufMessages.map((msg) => parseRawProtobuf(msg, { prefix: '' })); if (data.length === 1) data = data[0]; - return JSON.stringify(data, (_key, value) => { - // Buffers have toJSON defined, so arrive here in JSONified form: - if (value.type === 'Buffer' && Array.isArray(value.data)) { - const buffer = Buffer.from(value.data); - - return { - "Type": `Buffer (${getReadableSize(buffer)})`, - "As string": bufferToString(buffer, 'detect-encoding'), - "As hex": bufferToHex(buffer) - } - } else { - return value; - } - }, 2); + return prettyProtobufView(data); } } as const; \ No newline at end of file diff --git a/src/services/ui-worker.ts b/src/services/ui-worker.ts index 631caba6..817fe814 100644 --- a/src/services/ui-worker.ts +++ b/src/services/ui-worker.ts @@ -13,6 +13,7 @@ import { } from 'http-encoding'; import { OpenAPIObject } from 'openapi-directory'; +import { Headers } from '../types'; import { ApiMetadata, ApiSpec } from '../model/api/api-interfaces'; import { buildOpenApiMetadata, buildOpenRpcMetadata } from '../model/api/build-api-metadata'; import { parseCert, ParsedCertificate, validatePKCS12, ValidationResult } from '../model/crypto'; @@ -91,6 +92,7 @@ export interface FormatRequest extends Message { type: 'format'; buffer: ArrayBuffer; format: WorkerFormatterKey; + headers?: Headers; } export interface FormatResponse extends Message { @@ -217,7 +219,7 @@ ctx.addEventListener('message', async (event: { data: BackgroundRequest }) => { break; case 'format': - const formatted = formatBuffer(event.data.buffer, event.data.format); + const formatted = formatBuffer(event.data.buffer, event.data.format, event.data.headers); ctx.postMessage({ id: event.data.id, formatted }); break; diff --git a/src/util/protobuf.ts b/src/util/protobuf.ts index aae5347d..a0a5df7c 100644 --- a/src/util/protobuf.ts +++ b/src/util/protobuf.ts @@ -1,53 +1,113 @@ import parseRawProto from 'rawprotoparse'; +import { gunzipSync, inflateSync } from 'zlib'; + +import { Headers } from '../types'; +import { lastHeader } from './headers'; export function isProbablyProtobuf(input: Uint8Array) { - // Protobuf data starts with a varint, consisting of a field - // number (1 - 2^29-1) and a field type (0, 1, 2, 3, 4, 5) + // Protobuf data starts with a varint, consisting of a + // field number in [1, 2^29[ and a field type in [0, 5]*. // Unfortunately, that matches a very wide set of values, - // including things like '<' and '{' that are widely used - // elsewhere. + // including things like '<', '[' and '{' that are widely + // used in other contexts. + // * Hopefully, field types 3 & 4 have been deprecated for a while, + // we thus consider them as invalid for this quick inference. // To handle that, we're more strict here, and we assume that - // field 1 will be first (very common, but not guaranteed). + // first field is tiny (<= 3) (very common, but not guaranteed). // This is a best-efforts check for messages with no other // indicators (no matching content-type) so that's OK. - // This implies a first byte from 08 to 0D, which has no obvious + // This implies a first byte from 08 to 1D, which is not + // in range of printable ASCII characters and has no obvious // conflicts in https://en.wikipedia.org/wiki/List_of_file_signatures // but does notably conflict with tab/cr/lf. // That makes this good as a very quick first check, but confirming // actual parsing is required to check more thoroughly. + if (input.length < 2) { + return false; + } - const fieldNumber = input[0] >>> 3; + const fieldNumberTrunc = input[0] >>> 3; const fieldType = input[0] & 0b111; - return fieldNumber === 1 && - fieldType >= 1 && - fieldType <= 6; + return fieldNumberTrunc >= 1 && + fieldNumberTrunc <= 3 && + [0, 1, 2, 5].includes(fieldType); } export const parseRawProtobuf = parseRawProto; // GRPC message structure: +// Ref: https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md +// // The repeated sequence of Length-Prefixed-Message items is delivered in DATA frames // Length-Prefixed-Message → Compressed-Flag Message-Length Message // Compressed-Flag → 0 / 1 ; encoded as 1 byte unsigned integer // Message-Length → {length of Message} ; encoded as 4 byte unsigned integer (big endian) // Message → *{binary octet} -export const extractProtobufFromGrpc = (input: Buffer) => { - const protobufMessasges: Buffer[] = []; +// +// A Compressed-Flag value of 1 indicates that the binary octet sequence of Message is +// compressed using the mechanism declared by the Message-Encoding header. +// A value of 0 indicates that no encoding of Message bytes has occurred. +// If the Message-Encoding header is omitted then the Compressed-Flag must be 0. +export const extractProtobufFromGrpc = (input: Buffer, headers: Headers) => { + const grpcEncoding = lastHeader(headers['grpc-encoding'] ?? 'identity').toLocaleLowerCase(); + const grpcDecoder = grpcEncoding == 'gzip' ? gunzipSync : grpcEncoding == 'deflate' ? inflateSync : undefined; + const protobufMessages: Buffer[] = []; + + // useful indices for debugging + let offset = 0; + let msgIndex = 0; while (input.length > 0) { - if (input.readInt8() != 0) { - throw new Error("Compressed gRPC messages not yet supported") + const errorPrefix = `gRPC message #${msgIndex} @${offset}: ` + const compressionFlag = input.readUInt8(); + const length = input.readUInt32BE(1); + let message = input.slice(5, 5 + length); + if (message.length != length) { + throw new Error(`${errorPrefix}length of message is corrupted`); } - const length = input.readInt32BE(1); - protobufMessasges.push(input.slice(5, 5 + length)); + switch (compressionFlag) { + case 0: // may happen even if grpc-encoding != identity according to specs + break; + case 1: + if (!grpcDecoder) { + throw new Error(`${errorPrefix}not expected to be compressed`); + } + try { + message = grpcDecoder(message); + } catch (err) { + throw new Error(`${errorPrefix}failed decompression (from ${grpcEncoding})`); + } + break; + default: + throw new Error(`${errorPrefix}unsupported compression flag (0x${compressionFlag.toString(16).padStart(2, '0')})`); + } + + protobufMessages.push(message); input = input.subarray(5 + length); + offset += 5 + length; + msgIndex++; } - return protobufMessasges; + return protobufMessages; +} + +export const isProbablyGrpcProto = (input: Buffer, headers: Headers) => { + if (input.byteLength < 7) { + return false; + } + const compressionFlag = input.readUInt8(); + const length = input.readUInt32BE(1); + const firstMessage = input.slice(5, 5 + length); + return length >= 2 && // at least two bytes for Protobuf message (tag & value) + firstMessage.length == length && + ( + (compressionFlag == 0 && isProbablyProtobuf(firstMessage)) || + (compressionFlag == 1 && Object.keys(headers).includes('grpc-encoding')) + ) } export const isValidProtobuf = (input: Uint8Array) => { @@ -57,4 +117,14 @@ export const isValidProtobuf = (input: Uint8Array) => { } catch (e) { return false; } +} + +export const isValidGrpcProto = (input: Buffer, headers: Headers) => { + try { + const protobufMessages = extractProtobufFromGrpc(input, headers); + protobufMessages.forEach((msg) => parseRawProtobuf(msg)); + return true; + } catch (e) { + return false; + } } \ No newline at end of file diff --git a/test/unit/model/http/content-types.spec.ts b/test/unit/model/http/content-types.spec.ts index 3d098665..eec28b53 100644 --- a/test/unit/model/http/content-types.spec.ts +++ b/test/unit/model/http/content-types.spec.ts @@ -59,6 +59,21 @@ describe('Content type parsing', () => { expect(ct).to.equal('grpc-proto'); }); + it('should render application/grpc+protobuf as protobuf grpc', () => { + const ct = getContentType('application/grpc+protobuf'); + expect(ct).to.equal('grpc-proto'); + }); + + it('should render application/grpc-proto as protobuf grpc', () => { + const ct = getContentType('application/grpc-proto'); + expect(ct).to.equal('grpc-proto'); + }); + + it('should render application/grpc-protobuf as protobuf grpc', () => { + const ct = getContentType('application/grpc-protobuf'); + expect(ct).to.equal('grpc-proto'); + }); + it('should render application/grpc+json as JSON', () => { const ct = getContentType('application/grpc+json'); expect(ct).to.equal('json'); @@ -97,5 +112,25 @@ describe('Content type parsing', () => { const cts = getCompatibleTypes('raw', 'application/octet-stream', Buffer.from('1f8d08', 'hex')); // GZIP magic bytes expect(cts).to.deep.equal(['raw', 'text']); }); + + it('should flag application/grpc as compatible with [grpc-proto,text,raw]', () => { + const cts = getCompatibleTypes('grpc-proto', 'application/grpc', undefined); + expect(cts).to.deep.equal(['grpc-proto', 'text', 'raw']); + }); + + it('should flag application/grpc+proto as compatible with [grpc-proto,text,raw]', () => { + const cts = getCompatibleTypes('grpc-proto', 'application/grpc+proto', undefined); + expect(cts).to.deep.equal(['grpc-proto', 'text', 'raw']); + }); + + it('should flag application/grpc+json as compatible with [grpc-proto,text,raw]', () => { + const cts = getCompatibleTypes('json', 'application/grpc+json', undefined); + expect(cts).to.deep.equal(['json', 'text', 'raw']); + }); + + it('should detect undeclared grpc+proto', () => { + const cts = getCompatibleTypes('raw', 'application/octet-stream', Buffer.from('AAAAAAIIAQ==', 'base64')); + expect(cts).to.deep.equal(['raw', 'grpc-proto', 'text']); + }); }); }); \ No newline at end of file diff --git a/test/unit/util/protobuf.spec.ts b/test/unit/util/protobuf.spec.ts index ab032c62..ea50673c 100644 --- a/test/unit/util/protobuf.spec.ts +++ b/test/unit/util/protobuf.spec.ts @@ -1,6 +1,43 @@ import { expect } from "../../test-setup"; -import { isProbablyProtobuf } from "../../../src/util/protobuf"; +import { Headers } from '../../../src/types'; +import { isProbablyProtobuf, parseRawProtobuf, extractProtobufFromGrpc } from "../../../src/util/protobuf"; + +const bufferFromHex = (hex: string) => Buffer.from(hex.replace(/:/g, ''), 'hex'); +const uint32HexLengthFromHexColon = (hex: string) => ((hex.length + 1) / 3).toString(16).padStart(8, '0'); // no overflow check + +const _M1 = `syntax = "proto2"; +message M1 { + optional string msg = 1; +}`; + +const m1 = '0a:0b:48:65:6c:6c:6f:20:57:6f:72:6c:64'; +const m1Js = { "1": "Hello World" }; + +const mLastFieldNb = `fa:ff:ff:ff:0f:${m1.slice(3)}`; // #536870911(=2^29-1): "Hello World" + +const m1b = '0a:09:46:72:6f:6d:20:67:52:50:43'; +const m1bJs = { "1": "From gRPC" }; + +const m1Deflate = '78:9c:05:80:31:09:00:00:08:04:77:2d:61:1c:1b:58:40:b7:83:07:fb:0f:4f:64:1f:a8:46:cf:1a:19:13:04:32'; +const m1bDeflate = '78:5e:e3:e2:74:2b:ca:cf:55:48:0f:0a:70:06:00:10:85:03:14'; + +const _M2 = `syntax = "proto3"; +message M2 { + uint64 id = 3; + string name = 42; + double timestamp = 99; +}`; + +const m2 = '18:7b:d2:02:19:48:65:6c:6c:6f:20:57:6f:72:6c:64:20:77:69:74:68:20:55:54:46:38:20:e2:86:90:99:06:b9:c7:ad:df:47:bd:d9:41'; +const m2Js = { + "3": 123, + "42": "Hello World with UTF8 ←", + "99": bufferFromHex(m2.slice(-8 * 3 + 1)), // 1727340414.715315 as double ( often interpreted as fixed64 instead of double without schema) +} + +// Fixed Huffman coding (with checksum) +const m2Gzip = '1f:8b:08:02:88:94:f5:66:00:ff:f2:8f:93:a8:be:c4:24:e9:91:9a:93:93:af:10:9e:5f:94:93:a2:50:9e:59:92:a1:10:1a:e2:66:a1:f0:a8:6d:c2:4c:b6:9d:c7:d7:de:77:df:7b:d3:11:00:7f:e5:0c:b7:28:00:00:00'; describe("isProbablyProtobuf", () => { @@ -10,12 +47,18 @@ describe("isProbablyProtobuf", () => { ).to.equal(false); }); - it("should not recognize JSON as Protobuf", () => { + it("should not recognize JSON dict as Protobuf", () => { expect( isProbablyProtobuf(Buffer.from('{}', 'utf8')) ).to.equal(false); }); + it("should not recognize JSON array as Protobuf", () => { + expect( + isProbablyProtobuf(Buffer.from('[]', 'utf8')) + ).to.equal(false); + }); + it("should not recognize HTML as Protobuf", () => { expect( isProbablyProtobuf(Buffer.from('', 'utf8')) @@ -24,22 +67,120 @@ describe("isProbablyProtobuf", () => { it("should recognize basic protobuf", () => { expect( - isProbablyProtobuf(Buffer.from( - // Field 1 - string - Hello World - '0a 0b 48 65 6c 6c 6f 20 57 6f 72 6c 64', - 'hex' - )) + isProbablyProtobuf(bufferFromHex(m1)) ).to.equal(true); }); - it("should not recognize protobuf with invalid field numbers", () => { + it("should recognize more complex protobuf", () => { expect( - isProbablyProtobuf(Buffer.from( - // Field 2^28 (invalid) - 'fa ff ff ff 08 0b 48 65 6c 6c 6f 20 77 6f 72 6c 64', - 'hex' - )) + isProbablyProtobuf(bufferFromHex(m2)) + ).to.equal(true); + }); + + it("should not recognize protobuf with first field number too high", () => { + expect( + isProbablyProtobuf(bufferFromHex(mLastFieldNb)) ).to.equal(false); }); +}); + +const GRPCFixtures: { [key: string]: [string, Headers, any[]] } = { + // No compression + "should handle simplest gRPC payload (basic mono-message, uncompressed)": [ + `00:${uint32HexLengthFromHexColon(m1)}:${m1}`, + { 'grpc-encoding': 'identity' }, + [m1Js], + ], + "should handle usual gRPC payload (more complex mono-message, uncompressed without explicit encoding)": [ + `00:${uint32HexLengthFromHexColon(m2)}:${m2}`, + {}, // no grpc-encoding (identity by default) + [m2Js], + ], + "should handle multiple uncompressed gRPC messages": [ + `00:${uint32HexLengthFromHexColon(m1)}:${m1}:00:${uint32HexLengthFromHexColon(m1b)}:${m1b}`, // 2 uncompressed messages + { 'grpc-encoding': 'identity' }, + [m1Js, m1bJs], + ], + // Compressed + "should handle basic compressed (with deflate) gRPC payload": [ + `01:${uint32HexLengthFromHexColon(m1Deflate)}:${m1Deflate}`, + { 'grpc-encoding': 'deflate' }, + [m1Js], + ], + "should handle basic compressed (with gzip) gRPC payload": [ + `01:${uint32HexLengthFromHexColon(m2Gzip)}:${m2Gzip}`, + { 'grpc-encoding': 'gzip' }, + [m2Js], + ], + "should handle multiple compressed gRPC messages": [ + `00:${uint32HexLengthFromHexColon(m1)}:${m1}:01:${uint32HexLengthFromHexColon(m1bDeflate)}:${m1bDeflate}`, // per-message compression is optional + { 'grpc-encoding': 'deflate' }, + [m1Js, m1bJs], + ], + +}; + +describe("extractProtobufFromGrpc", () => { + + Object.entries(GRPCFixtures).forEach(([testName, [hexGrpc, headers, expectedMsgs]]) => it(testName, () => { + const protoMsgs = extractProtobufFromGrpc(bufferFromHex(hexGrpc), headers).map((msg) => parseRawProtobuf(msg, { prefix: '' })); + expect(protoMsgs).to.deep.equal(expectedMsgs); + })); + + it("should fail for compression flag != {0,1}", () => { + const f = extractProtobufFromGrpc.bind(null, bufferFromHex(`02:${uint32HexLengthFromHexColon(m1)}:${m1}`), {}); + expect(f).to.throw(Error); + }); + + it("should reject compressed payload when grpc-encoding is identity", () => { + const f = extractProtobufFromGrpc.bind( + null, + bufferFromHex(`01:${uint32HexLengthFromHexColon(m1)}:${m1}`), + { 'grpc-encoding': 'identity' }, + ); + expect(f).to.throw(Error); + }); + + it("should reject compressed payload when grpc-encoding is not provided", () => { + const f = extractProtobufFromGrpc.bind(null, bufferFromHex(`01:${uint32HexLengthFromHexColon(m1)}:${m1}`), {}); + expect(f).to.throw(Error); + }); + + it("should fail for wrongly declared grpc-encoding (gzip)", () => { + const f = extractProtobufFromGrpc.bind( + null, + bufferFromHex(`01:${uint32HexLengthFromHexColon(m1Deflate)}:${m1Deflate}`), + { 'grpc-encoding': 'gzip' }, + ); + expect(f).to.throw(Error); + }); + + it("should fail for wrongly declared grpc-encoding (deflate)", () => { + const f = extractProtobufFromGrpc.bind( + null, + bufferFromHex(`01:${uint32HexLengthFromHexColon(m2Gzip)}:${m2Gzip}`), + { 'grpc-encoding': 'deflate' }, + ); + expect(f).to.throw(Error); + }); + + it("should fail for corrupted deflate payload", () => { + const f = extractProtobufFromGrpc.bind( + null, + bufferFromHex(`01:${uint32HexLengthFromHexColon(m1Deflate.slice(0, -6))}:${m1Deflate}`), + { 'grpc-encoding': 'deflate' }, + ); + expect(f).to.throw(Error); + }); + + it("should fail for corrupted gzip payload", () => { + const f = extractProtobufFromGrpc.bind( + null, + bufferFromHex(`01:${uint32HexLengthFromHexColon(m2Gzip.slice(0, -6))}:${m2Gzip}`), + { 'grpc-encoding': 'gzip' }, + ); + expect(f).to.throw(Error); + }); + }); \ No newline at end of file From 2fdcab7e23c1ab82e130b135839e4c65bb771f0a Mon Sep 17 00:00:00 2001 From: Tim Perry Date: Wed, 2 Oct 2024 18:06:07 +0200 Subject: [PATCH 022/168] Add some last small fixes/tweaks for gRPC compression change --- src/components/editor/content-viewer.tsx | 3 ++- .../intercept/config/android-device-config.tsx | 5 ++++- src/model/events/body-formatting.ts | 8 +++++--- src/model/events/content-types.ts | 12 +++++++----- src/model/http/har.ts | 10 +++++----- src/services/ui-worker-formatters.ts | 3 +-- 6 files changed, 24 insertions(+), 17 deletions(-) diff --git a/src/components/editor/content-viewer.tsx b/src/components/editor/content-viewer.tsx index 999825e6..69f7cea7 100644 --- a/src/components/editor/content-viewer.tsx +++ b/src/components/editor/content-viewer.tsx @@ -10,6 +10,7 @@ import { styled } from '../../styles'; import { ObservablePromise, isObservablePromise } from '../../util/observable'; import { asError, unreachableCheck } from '../../util/error'; import { stringToBuffer } from '../../util/buffer'; +import { lastHeader } from '../../util/headers'; import { ViewableContentType } from '../../model/events/content-types'; import { Formatters, isEditorFormatter } from '../../model/events/body-formatting'; @@ -200,7 +201,7 @@ export class ContentViewer extends React.Component { return ; } diff --git a/src/components/intercept/config/android-device-config.tsx b/src/components/intercept/config/android-device-config.tsx index a1f681c7..605d9b22 100644 --- a/src/components/intercept/config/android-device-config.tsx +++ b/src/components/intercept/config/android-device-config.tsx @@ -64,7 +64,10 @@ const Spacer = styled.div` `; function urlSafeBase64(content: string) { - return stringToBuffer(content).toString('base64url'); + return stringToBuffer(content) + .toString('base64') + .replace(/\+/g, '-') + .replace(/\//g, '_'); } function getConfigRequestIds(eventsStore: EventsStore) { diff --git a/src/model/events/body-formatting.ts b/src/model/events/body-formatting.ts index ad092238..147673be 100644 --- a/src/model/events/body-formatting.ts +++ b/src/model/events/body-formatting.ts @@ -19,7 +19,7 @@ export interface EditorFormatter { type FormatComponentProps = { content: Buffer; - headers?: Headers; + rawContentType: string | undefined; }; type FormatComponent = React.ComponentType; @@ -65,7 +65,7 @@ export const Formatters: { [key in ViewableContentType]: Formatter } = { language: 'text', cacheKey: Symbol('text'), isEditApplicable: false, - render: (input: Buffer, headers?: Headers) => { + render: (input: Buffer) => { return bufferToString(input); } }, @@ -112,7 +112,9 @@ export const Formatters: { [key in ViewableContentType]: Formatter } = { // showing the loading spinner that churns the layout in short content cases. return JSON.stringify( JSON.parse(inputAsString), - null, 2); + null, + 2 + ); // ^ Same logic as in UI-worker-formatter } catch (e) { // Fallback to showing the raw un-formatted JSON: diff --git a/src/model/events/content-types.ts b/src/model/events/content-types.ts index ef85bbd9..d632be1e 100644 --- a/src/model/events/content-types.ts +++ b/src/model/events/content-types.ts @@ -153,7 +153,7 @@ export function getDefaultMimeType(contentType: ViewableContentType): string { return _.findKey(mimeTypeToContentTypeMap, (c) => c === contentType)!; } -function isValidAlphaNumOrSpace(byte: number) { +function isAlphaNumOrEquals(byte: number) { return (byte >= 65 && byte <= 90) || // A-Z (byte >= 97 && byte <= 122) || // a-z (byte >= 48 && byte <= 57) || // 0-9 @@ -162,14 +162,16 @@ function isValidAlphaNumOrSpace(byte: number) { function isValidStandardBase64Byte(byte: number) { // + / (standard) - return byte === 43 || byte === 47 - || isValidAlphaNumOrSpace(byte); + return byte === 43 || + byte === 47 || + isAlphaNumOrEquals(byte); } function isValidURLSafeBase64Byte(byte: number) { // - _ (URL-safe version) - return byte === 45 || byte === 95 - || isValidAlphaNumOrSpace(byte); + return byte === 45 || + byte === 95 || + isAlphaNumOrEquals(byte); } export function getCompatibleTypes( diff --git a/src/model/http/har.ts b/src/model/http/har.ts index d159bad3..c7c52c04 100644 --- a/src/model/http/har.ts +++ b/src/model/http/har.ts @@ -57,9 +57,9 @@ export type RequestContentData = { export interface ExtendedHarRequest extends HarFormat.Request { _requestBodyStatus?: - | 'discarded:too-large' - | 'discarded:not-representable' // to indicate that extended field `_content` is populated with base64 `postData` - | 'discarded:not-decodable'; + | 'discarded:too-large' + | 'discarded:not-representable' // to indicate that extended field `_content` is populated with base64 `postData` + | 'discarded:not-decodable'; _content?: RequestContentData; _trailers?: HarFormat.Header[]; } @@ -435,10 +435,10 @@ function generateHarWebSocketMessage( return { // Note that msg.direction is from the perspective of Mockttp, not the client. type: message.direction === 'sent' - ? 'receive' + ? 'receive' : message.direction === 'received' ? 'send' - : unreachableCheck(message.direction), + : unreachableCheck(message.direction), opcode: message.isBinary ? 2 : 1, data: message.isBinary diff --git a/src/services/ui-worker-formatters.ts b/src/services/ui-worker-formatters.ts index dfb360c3..a6cd9390 100644 --- a/src/services/ui-worker-formatters.ts +++ b/src/services/ui-worker-formatters.ts @@ -56,8 +56,7 @@ const WorkerFormatters = { }, base64: (content: Buffer) => { const b64 = content.toString('ascii'); - const encoding = b64.match(/[-_]/) ? 'base64url' : 'base64'; - return Buffer.from(b64, encoding).toString('utf8'); + return Buffer.from(b64, 'base64').toString('utf8'); }, markdown: (content: Buffer) => { return content.toString('utf8'); From c44a37a2e4bd39e03b7b773391419557af295be5 Mon Sep 17 00:00:00 2001 From: Tim Perry Date: Thu, 24 Oct 2024 18:10:35 +0200 Subject: [PATCH 023/168] Fix mockttp admin stream monitoring and actively restart on disconnection This isn't great, but it's better than the current result where the app effectively breaks silently, and then crashes and resets your rules whenever any rule change is saved. --- src/model/proxy-store.ts | 20 +++++++++++++++++--- src/services/desktop-api.ts | 1 + 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/src/model/proxy-store.ts b/src/model/proxy-store.ts index c444d836..e0340b53 100644 --- a/src/model/proxy-store.ts +++ b/src/model/proxy-store.ts @@ -37,6 +37,7 @@ import { lazyObservablePromise } from '../util/observable'; import { persist, hydrate } from '../util/mobx-persist/persist'; import { isValidPort } from './network'; import { serverVersion } from '../services/service-versions'; +import { DesktopApi } from '../services/desktop-api'; type HtkAdminClient = // WebRTC is only supported for new servers: @@ -224,16 +225,29 @@ export class ProxyStore { }); private monitorRemoteClientConnection(client: PluggableAdmin.AdminClient<{}>) { - client.on('admin-client:stream-error', (err) => { + client.on('stream-error', (err) => { console.log('Admin client stream error'); logError(err.message ? err : new Error('Client stream error'), { cause: err }); }); - client.on('admin-client:subscription-error', (err) => { + client.on('subscription-error', (err) => { console.log('Admin client subscription error'); logError(err.message ? err : new Error('Client subscription error'), { cause: err }); }); - client.on('admin-client:stream-reconnect-failed', (err) => { + client.on('stream-reconnect-failed', (err) => { logError(err.message ? err : new Error('Client reconnect error'), { cause: err }); + + alert("Server disconnected unexpectedly, app restart required.\n\nPlease report this at github.com/httptoolkit/httptoolkit."); + setTimeout(() => { // Tiny wait for any other UI events to fire (error reporting/logging/other UI responsiveness) + if (DesktopApi.restartApp) { + // Where possible (recent desktop release) we restart the whole app directly + DesktopApi.restartApp(); + } else if (!navigator.platform?.startsWith('Mac')) { + // If not, on Windows & Linux we just close the window (which restarts) + window.close(); + } + // On Mac, app exit is independent from window exit, so we can't force that here, + // but hopefully this alert will lead the user to do so themselves. + }, 10); }); } diff --git a/src/services/desktop-api.ts b/src/services/desktop-api.ts index b6cc8996..8312083d 100644 --- a/src/services/desktop-api.ts +++ b/src/services/desktop-api.ts @@ -30,6 +30,7 @@ declare global { interface DesktopApi { selectApplication?: () => Promise; openContextMenu?: (options: NativeContextMenuDefinition) => Promise; + restartApp?: () => Promise; } interface NativeContextMenuDefinition { From 6671b63023f3ea5471e6a59694e9054609d1a72d Mon Sep 17 00:00:00 2001 From: Tim Perry Date: Thu, 24 Oct 2024 18:25:53 +0200 Subject: [PATCH 024/168] Explicitly report failed SW startup --- src/services/update-management.ts | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/services/update-management.ts b/src/services/update-management.ts index d0e367aa..a6e4e68c 100644 --- a/src/services/update-management.ts +++ b/src/services/update-management.ts @@ -2,10 +2,22 @@ import { triggerServerUpdate } from './server-api'; import packageMetadata from '../../package.json'; import { desktopVersion, serverVersion, versionSatisfies } from './service-versions'; +import { logError } from '../errors'; export const attemptServerUpdate = () => triggerServerUpdate().catch(console.warn); +let swFailureAlerted = false; +const alertFailedSWStartup = (e: any) => { + if (swFailureAlerted) return; // Only alert once + swFailureAlerted = true; + + const msg = "Initialization failed - background updates & cached/offline startup will be unavailable"; + + logError(msg, { cause: e }); + alert(msg); +}; + // Set up a SW in the background, to add offline support & instant startup. // This also checks for new UI & server versions at intervals. export async function runBackgroundUpdates() { @@ -19,14 +31,19 @@ export async function runBackgroundUpdates() { try { if (!navigator?.serviceWorker?.register) { - console.warn('Service worker not supported - cached & offline startup will not be available'); + console.warn('Service worker not supported'); + alertFailedSWStartup("Not supported"); return; } const registration = await navigator.serviceWorker.register( '/ui-update-worker.js', { scope: '/' } - ); + ).catch((e) => { + console.warn('Service worker registration failed'); + alertFailedSWStartup(e); + throw e; + }); console.log('Service worker loaded'); registration.update().catch(console.log); From d5f6a61d017a48175e5e90dc2ed28b7fc6837e6a Mon Sep 17 00:00:00 2001 From: Tim Perry Date: Thu, 24 Oct 2024 18:34:27 +0200 Subject: [PATCH 025/168] Don't report minor stream errors (just full disconnect) --- src/model/proxy-store.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/model/proxy-store.ts b/src/model/proxy-store.ts index e0340b53..5dc734f3 100644 --- a/src/model/proxy-store.ts +++ b/src/model/proxy-store.ts @@ -4,8 +4,7 @@ import { action, flow, computed, - observe, - runInAction, + observe } from 'mobx'; import { @@ -226,12 +225,10 @@ export class ProxyStore { private monitorRemoteClientConnection(client: PluggableAdmin.AdminClient<{}>) { client.on('stream-error', (err) => { - console.log('Admin client stream error'); - logError(err.message ? err : new Error('Client stream error'), { cause: err }); + console.log('Admin client stream error', err); }); client.on('subscription-error', (err) => { - console.log('Admin client subscription error'); - logError(err.message ? err : new Error('Client subscription error'), { cause: err }); + console.log('Admin client subscription error', err); }); client.on('stream-reconnect-failed', (err) => { logError(err.message ? err : new Error('Client reconnect error'), { cause: err }); From dea838d340dff67ea7454095f94a28dd681c0110 Mon Sep 17 00:00:00 2001 From: Tim Perry Date: Wed, 30 Oct 2024 16:31:17 +0100 Subject: [PATCH 026/168] Update Sentry & disable session tracking --- package-lock.json | 265 +++++++++++++++++++++------------------------- package.json | 2 +- src/errors.ts | 1 + 3 files changed, 121 insertions(+), 147 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0a6152aa..0266210f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,7 +22,7 @@ "@open-rpc/meta-schema": "^1.14.2", "@phosphor-icons/react": "^2.1.5", "@reach/router": "^1.2.1", - "@sentry/browser": "^7.112.2", + "@sentry/browser": "^8.35.0", "@sentry/webpack-plugin": "^2.16.1", "@types/auth0-js": "^8.11.8", "@types/auth0-lock": "^11.4.10", @@ -2890,44 +2890,58 @@ "rollup": "^1.20.0||^2.0.0" } }, + "node_modules/@sentry-internal/browser-utils": { + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-8.35.0.tgz", + "integrity": "sha512-uj9nwERm7HIS13f/Q52hF/NUS5Al8Ma6jkgpfYGeppYvU0uSjPkwMogtqoJQNbOoZg973tV8qUScbcWY616wNA==", + "dependencies": { + "@sentry/core": "8.35.0", + "@sentry/types": "8.35.0", + "@sentry/utils": "8.35.0" + }, + "engines": { + "node": ">=14.18" + } + }, "node_modules/@sentry-internal/feedback": { - "version": "7.112.2", - "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-7.112.2.tgz", - "integrity": "sha512-z+XP8BwB8B3pa+i8xqbrPsbtDWUFUS6wo+FJbmOYUqOusJJbVFDAhBoEdKoo5ZjOcsAZG7XR6cA9zrhJynIWBA==", + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-8.35.0.tgz", + "integrity": "sha512-7bjSaUhL0bDArozre6EiIhhdWdT/1AWNWBC1Wc5w1IxEi5xF7nvF/FfvjQYrONQzZAI3HRxc45J2qhLUzHBmoQ==", "dependencies": { - "@sentry/core": "7.112.2", - "@sentry/types": "7.112.2", - "@sentry/utils": "7.112.2" + "@sentry/core": "8.35.0", + "@sentry/types": "8.35.0", + "@sentry/utils": "8.35.0" }, "engines": { - "node": ">=12" + "node": ">=14.18" } }, - "node_modules/@sentry-internal/replay-canvas": { - "version": "7.112.2", - "resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-7.112.2.tgz", - "integrity": "sha512-BCCCxrZ1wJvN6La5gg1JJbKitAhJI5MATCnhtklsZbUcHkHB9iZoj19J65+P56gwssvHz5xh63AjNiITaetIRg==", + "node_modules/@sentry-internal/replay": { + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-8.35.0.tgz", + "integrity": "sha512-3wkW03vXYMyWtTLxl9yrtkV+qxbnKFgfASdoGWhXzfLjycgT6o4/04eb3Gn71q9aXqRwH17ISVQbVswnRqMcmA==", "dependencies": { - "@sentry/core": "7.112.2", - "@sentry/replay": "7.112.2", - "@sentry/types": "7.112.2", - "@sentry/utils": "7.112.2" + "@sentry-internal/browser-utils": "8.35.0", + "@sentry/core": "8.35.0", + "@sentry/types": "8.35.0", + "@sentry/utils": "8.35.0" }, "engines": { - "node": ">=12" + "node": ">=14.18" } }, - "node_modules/@sentry-internal/tracing": { - "version": "7.112.2", - "resolved": "https://registry.npmjs.org/@sentry-internal/tracing/-/tracing-7.112.2.tgz", - "integrity": "sha512-fT1Y46J4lfXZkgFkb03YMNeIEs2xS6jdKMoukMFQfRfVvL9fSWEbTgZpHPd/YTT8r2i082XzjtAoQNgklm/0Hw==", + "node_modules/@sentry-internal/replay-canvas": { + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-8.35.0.tgz", + "integrity": "sha512-TUrH6Piv19kvHIiRyIuapLdnuwxk/Un/l1WDCQfq7mK9p1Pac0FkQ7Uufjp6zY3lyhDDZQ8qvCS4ioCMibCwQg==", "dependencies": { - "@sentry/core": "7.112.2", - "@sentry/types": "7.112.2", - "@sentry/utils": "7.112.2" + "@sentry-internal/replay": "8.35.0", + "@sentry/core": "8.35.0", + "@sentry/types": "8.35.0", + "@sentry/utils": "8.35.0" }, "engines": { - "node": ">=8" + "node": ">=14.18" } }, "node_modules/@sentry/babel-plugin-component-annotate": { @@ -2939,21 +2953,20 @@ } }, "node_modules/@sentry/browser": { - "version": "7.112.2", - "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-7.112.2.tgz", - "integrity": "sha512-wULwavCch84+d0bueAdFm6CDm1u0TfOjN91VgY+sj/vxUV2vesvDgI8zRZfmbZEor3MYA90zerkZT3ehZQKbYw==", + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-8.35.0.tgz", + "integrity": "sha512-WHfI+NoZzpCsmIvtr6ChOe7yWPLQyMchPnVhY3Z4UeC70bkYNdKcoj/4XZbX3m0D8+71JAsm0mJ9s9OC3Ue6MQ==", "dependencies": { - "@sentry-internal/feedback": "7.112.2", - "@sentry-internal/replay-canvas": "7.112.2", - "@sentry-internal/tracing": "7.112.2", - "@sentry/core": "7.112.2", - "@sentry/integrations": "7.112.2", - "@sentry/replay": "7.112.2", - "@sentry/types": "7.112.2", - "@sentry/utils": "7.112.2" + "@sentry-internal/browser-utils": "8.35.0", + "@sentry-internal/feedback": "8.35.0", + "@sentry-internal/replay": "8.35.0", + "@sentry-internal/replay-canvas": "8.35.0", + "@sentry/core": "8.35.0", + "@sentry/types": "8.35.0", + "@sentry/utils": "8.35.0" }, "engines": { - "node": ">=8" + "node": ">=14.18" } }, "node_modules/@sentry/bundler-plugin-core": { @@ -3248,62 +3261,34 @@ } }, "node_modules/@sentry/core": { - "version": "7.112.2", - "resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.112.2.tgz", - "integrity": "sha512-gHPCcJobbMkk0VR18J65WYQTt3ED4qC6X9lHKp27Ddt63E+MDGkG6lvYBU1LS8cV7CdyBGC1XXDCfor61GvLsA==", + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-8.35.0.tgz", + "integrity": "sha512-Ci0Nmtw5ETWLqQJGY4dyF+iWh7PWKy6k303fCEoEmqj2czDrKJCp7yHBNV0XYbo00prj2ZTbCr6I7albYiyONA==", "dependencies": { - "@sentry/types": "7.112.2", - "@sentry/utils": "7.112.2" + "@sentry/types": "8.35.0", + "@sentry/utils": "8.35.0" }, "engines": { - "node": ">=8" - } - }, - "node_modules/@sentry/integrations": { - "version": "7.112.2", - "resolved": "https://registry.npmjs.org/@sentry/integrations/-/integrations-7.112.2.tgz", - "integrity": "sha512-ioC2yyU6DqtLkdmWnm87oNvdn2+9oKctJeA4t+jkS6JaJ10DcezjCwiLscX4rhB9aWJV3IWF7Op0O6K3w0t2Hg==", - "dependencies": { - "@sentry/core": "7.112.2", - "@sentry/types": "7.112.2", - "@sentry/utils": "7.112.2", - "localforage": "^1.8.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@sentry/replay": { - "version": "7.112.2", - "resolved": "https://registry.npmjs.org/@sentry/replay/-/replay-7.112.2.tgz", - "integrity": "sha512-7Ns/8D54WPsht1nlVj93Inf6rXyve2AZoibYN0YfcM2w3lI4NO51gPPHJU0lFEfMwzwK4ZBJWzOeW9098a+uEg==", - "dependencies": { - "@sentry-internal/tracing": "7.112.2", - "@sentry/core": "7.112.2", - "@sentry/types": "7.112.2", - "@sentry/utils": "7.112.2" - }, - "engines": { - "node": ">=12" + "node": ">=14.18" } }, "node_modules/@sentry/types": { - "version": "7.112.2", - "resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.112.2.tgz", - "integrity": "sha512-kCMLt7yhY5OkWE9MeowlTNmox9pqDxcpvqguMo4BDNZM5+v9SEb1AauAdR78E1a1V8TyCzjBD7JDfXWhvpYBcQ==", + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-8.35.0.tgz", + "integrity": "sha512-AVEZjb16MlYPifiDDvJ19dPQyDn0jlrtC1PHs6ZKO+Rzyz+2EX2BRdszvanqArldexPoU1p5Bn2w81XZNXThBA==", "engines": { - "node": ">=8" + "node": ">=14.18" } }, "node_modules/@sentry/utils": { - "version": "7.112.2", - "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.112.2.tgz", - "integrity": "sha512-OjLh0hx0t1EcL4ZIjf+4svlmmP+tHUDGcr5qpFWH78tjmkPW4+cqPuZCZfHSuWcDdeiaXi8TnYoVRqDcJKK/eQ==", + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-8.35.0.tgz", + "integrity": "sha512-MdMb6+uXjqND7qIPWhulubpSeHzia6HtxeJa8jYI09OCvIcmNGPydv/Gx/LZBwosfMHrLdTWcFH7Y7aCxrq7cg==", "dependencies": { - "@sentry/types": "7.112.2" + "@sentry/types": "8.35.0" }, "engines": { - "node": ">=8" + "node": ">=14.18" } }, "node_modules/@sentry/webpack-plugin": { @@ -23443,35 +23428,46 @@ "picomatch": "^2.2.2" } }, + "@sentry-internal/browser-utils": { + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-8.35.0.tgz", + "integrity": "sha512-uj9nwERm7HIS13f/Q52hF/NUS5Al8Ma6jkgpfYGeppYvU0uSjPkwMogtqoJQNbOoZg973tV8qUScbcWY616wNA==", + "requires": { + "@sentry/core": "8.35.0", + "@sentry/types": "8.35.0", + "@sentry/utils": "8.35.0" + } + }, "@sentry-internal/feedback": { - "version": "7.112.2", - "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-7.112.2.tgz", - "integrity": "sha512-z+XP8BwB8B3pa+i8xqbrPsbtDWUFUS6wo+FJbmOYUqOusJJbVFDAhBoEdKoo5ZjOcsAZG7XR6cA9zrhJynIWBA==", + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-8.35.0.tgz", + "integrity": "sha512-7bjSaUhL0bDArozre6EiIhhdWdT/1AWNWBC1Wc5w1IxEi5xF7nvF/FfvjQYrONQzZAI3HRxc45J2qhLUzHBmoQ==", "requires": { - "@sentry/core": "7.112.2", - "@sentry/types": "7.112.2", - "@sentry/utils": "7.112.2" + "@sentry/core": "8.35.0", + "@sentry/types": "8.35.0", + "@sentry/utils": "8.35.0" } }, - "@sentry-internal/replay-canvas": { - "version": "7.112.2", - "resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-7.112.2.tgz", - "integrity": "sha512-BCCCxrZ1wJvN6La5gg1JJbKitAhJI5MATCnhtklsZbUcHkHB9iZoj19J65+P56gwssvHz5xh63AjNiITaetIRg==", + "@sentry-internal/replay": { + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-8.35.0.tgz", + "integrity": "sha512-3wkW03vXYMyWtTLxl9yrtkV+qxbnKFgfASdoGWhXzfLjycgT6o4/04eb3Gn71q9aXqRwH17ISVQbVswnRqMcmA==", "requires": { - "@sentry/core": "7.112.2", - "@sentry/replay": "7.112.2", - "@sentry/types": "7.112.2", - "@sentry/utils": "7.112.2" + "@sentry-internal/browser-utils": "8.35.0", + "@sentry/core": "8.35.0", + "@sentry/types": "8.35.0", + "@sentry/utils": "8.35.0" } }, - "@sentry-internal/tracing": { - "version": "7.112.2", - "resolved": "https://registry.npmjs.org/@sentry-internal/tracing/-/tracing-7.112.2.tgz", - "integrity": "sha512-fT1Y46J4lfXZkgFkb03YMNeIEs2xS6jdKMoukMFQfRfVvL9fSWEbTgZpHPd/YTT8r2i082XzjtAoQNgklm/0Hw==", + "@sentry-internal/replay-canvas": { + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-8.35.0.tgz", + "integrity": "sha512-TUrH6Piv19kvHIiRyIuapLdnuwxk/Un/l1WDCQfq7mK9p1Pac0FkQ7Uufjp6zY3lyhDDZQ8qvCS4ioCMibCwQg==", "requires": { - "@sentry/core": "7.112.2", - "@sentry/types": "7.112.2", - "@sentry/utils": "7.112.2" + "@sentry-internal/replay": "8.35.0", + "@sentry/core": "8.35.0", + "@sentry/types": "8.35.0", + "@sentry/utils": "8.35.0" } }, "@sentry/babel-plugin-component-annotate": { @@ -23480,18 +23476,17 @@ "integrity": "sha512-pJka66URsqQbk6hTs9H1XFpUeI0xxuqLYf9Dy5pRGNHSJMtfv91U+CaYSWt03aRRMGDXMduh62zAAY7Wf0HO+A==" }, "@sentry/browser": { - "version": "7.112.2", - "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-7.112.2.tgz", - "integrity": "sha512-wULwavCch84+d0bueAdFm6CDm1u0TfOjN91VgY+sj/vxUV2vesvDgI8zRZfmbZEor3MYA90zerkZT3ehZQKbYw==", + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-8.35.0.tgz", + "integrity": "sha512-WHfI+NoZzpCsmIvtr6ChOe7yWPLQyMchPnVhY3Z4UeC70bkYNdKcoj/4XZbX3m0D8+71JAsm0mJ9s9OC3Ue6MQ==", "requires": { - "@sentry-internal/feedback": "7.112.2", - "@sentry-internal/replay-canvas": "7.112.2", - "@sentry-internal/tracing": "7.112.2", - "@sentry/core": "7.112.2", - "@sentry/integrations": "7.112.2", - "@sentry/replay": "7.112.2", - "@sentry/types": "7.112.2", - "@sentry/utils": "7.112.2" + "@sentry-internal/browser-utils": "8.35.0", + "@sentry-internal/feedback": "8.35.0", + "@sentry-internal/replay": "8.35.0", + "@sentry-internal/replay-canvas": "8.35.0", + "@sentry/core": "8.35.0", + "@sentry/types": "8.35.0", + "@sentry/utils": "8.35.0" } }, "@sentry/bundler-plugin-core": { @@ -23661,47 +23656,25 @@ "optional": true }, "@sentry/core": { - "version": "7.112.2", - "resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.112.2.tgz", - "integrity": "sha512-gHPCcJobbMkk0VR18J65WYQTt3ED4qC6X9lHKp27Ddt63E+MDGkG6lvYBU1LS8cV7CdyBGC1XXDCfor61GvLsA==", - "requires": { - "@sentry/types": "7.112.2", - "@sentry/utils": "7.112.2" - } - }, - "@sentry/integrations": { - "version": "7.112.2", - "resolved": "https://registry.npmjs.org/@sentry/integrations/-/integrations-7.112.2.tgz", - "integrity": "sha512-ioC2yyU6DqtLkdmWnm87oNvdn2+9oKctJeA4t+jkS6JaJ10DcezjCwiLscX4rhB9aWJV3IWF7Op0O6K3w0t2Hg==", - "requires": { - "@sentry/core": "7.112.2", - "@sentry/types": "7.112.2", - "@sentry/utils": "7.112.2", - "localforage": "^1.8.1" - } - }, - "@sentry/replay": { - "version": "7.112.2", - "resolved": "https://registry.npmjs.org/@sentry/replay/-/replay-7.112.2.tgz", - "integrity": "sha512-7Ns/8D54WPsht1nlVj93Inf6rXyve2AZoibYN0YfcM2w3lI4NO51gPPHJU0lFEfMwzwK4ZBJWzOeW9098a+uEg==", + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-8.35.0.tgz", + "integrity": "sha512-Ci0Nmtw5ETWLqQJGY4dyF+iWh7PWKy6k303fCEoEmqj2czDrKJCp7yHBNV0XYbo00prj2ZTbCr6I7albYiyONA==", "requires": { - "@sentry-internal/tracing": "7.112.2", - "@sentry/core": "7.112.2", - "@sentry/types": "7.112.2", - "@sentry/utils": "7.112.2" + "@sentry/types": "8.35.0", + "@sentry/utils": "8.35.0" } }, "@sentry/types": { - "version": "7.112.2", - "resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.112.2.tgz", - "integrity": "sha512-kCMLt7yhY5OkWE9MeowlTNmox9pqDxcpvqguMo4BDNZM5+v9SEb1AauAdR78E1a1V8TyCzjBD7JDfXWhvpYBcQ==" + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-8.35.0.tgz", + "integrity": "sha512-AVEZjb16MlYPifiDDvJ19dPQyDn0jlrtC1PHs6ZKO+Rzyz+2EX2BRdszvanqArldexPoU1p5Bn2w81XZNXThBA==" }, "@sentry/utils": { - "version": "7.112.2", - "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.112.2.tgz", - "integrity": "sha512-OjLh0hx0t1EcL4ZIjf+4svlmmP+tHUDGcr5qpFWH78tjmkPW4+cqPuZCZfHSuWcDdeiaXi8TnYoVRqDcJKK/eQ==", + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-8.35.0.tgz", + "integrity": "sha512-MdMb6+uXjqND7qIPWhulubpSeHzia6HtxeJa8jYI09OCvIcmNGPydv/Gx/LZBwosfMHrLdTWcFH7Y7aCxrq7cg==", "requires": { - "@sentry/types": "7.112.2" + "@sentry/types": "8.35.0" } }, "@sentry/webpack-plugin": { diff --git a/package.json b/package.json index fa303b3c..5f4ebf02 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "@open-rpc/meta-schema": "^1.14.2", "@phosphor-icons/react": "^2.1.5", "@reach/router": "^1.2.1", - "@sentry/browser": "^7.112.2", + "@sentry/browser": "^8.35.0", "@sentry/webpack-plugin": "^2.16.1", "@types/auth0-js": "^8.11.8", "@types/auth0-lock": "^11.4.10", diff --git a/src/errors.ts b/src/errors.ts index 5d973bc1..87578b52 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -18,6 +18,7 @@ export function initSentry(dsn: string | undefined) { 'ResizeObserver loop limit exceeded', // No visible effect: https://stackoverflow.com/a/50387233/68051 'ResizeObserver loop completed with undelivered notifications.' ], + autoSessionTracking: false, // Don't send requests for every session (URL change) integrations: [ Sentry.dedupeIntegration(), Sentry.extraErrorDataIntegration(), From 86304ab8e1dc46e760dfe678f91c47549150443a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 30 Oct 2024 15:34:55 +0000 Subject: [PATCH 027/168] Bump webpack from 5.89.0 to 5.95.0 Bumps [webpack](https://github.com/webpack/webpack) from 5.89.0 to 5.95.0. - [Release notes](https://github.com/webpack/webpack/releases) - [Commits](https://github.com/webpack/webpack/compare/v5.89.0...v5.95.0) --- updated-dependencies: - dependency-name: webpack dependency-type: direct:development ... Signed-off-by: dependabot[bot] --- package-lock.json | 300 ++++++++++++++++++++-------------------------- package.json | 2 +- 2 files changed, 130 insertions(+), 172 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0266210f..7210a5c6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -191,7 +191,7 @@ "umd-compat-loader": "^2.1.1", "url": "^0.11.3", "util": "^0.12.5", - "webpack": "^5.89.0", + "webpack": "^5.95.0", "webpack-bundle-analyzer": "^4.10.1", "webpack-cli": "^5.1.4", "webpack-dev-server": "^4.15.1", @@ -3618,26 +3618,6 @@ "@types/enzyme": "*" } }, - "node_modules/@types/eslint": { - "version": "8.56.2", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.2.tgz", - "integrity": "sha512-uQDwm1wFHmbBbCZCqAlq6Do9LYwByNZHWzXppSnay9SuwJ+VRbjkbLABer54kcPnMSlG6Fdiy2yaFXm/z9Z5gw==", - "dev": true, - "dependencies": { - "@types/estree": "*", - "@types/json-schema": "*" - } - }, - "node_modules/@types/eslint-scope": { - "version": "3.7.7", - "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", - "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", - "dev": true, - "dependencies": { - "@types/eslint": "*", - "@types/estree": "*" - } - }, "node_modules/@types/estree": { "version": "0.0.39", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", @@ -4396,9 +4376,9 @@ } }, "node_modules/@webassemblyjs/ast": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.6.tgz", - "integrity": "sha512-IN1xI7PwOvLPgjcf180gC1bqn3q/QaOCwYUahIOhbYUu8KA/3tw2RT/T0Gidi1l7Hhj5D/INhJxiICObqpMu4Q==", + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.12.1.tgz", + "integrity": "sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg==", "dev": true, "dependencies": { "@webassemblyjs/helper-numbers": "1.11.6", @@ -4418,9 +4398,9 @@ "dev": true }, "node_modules/@webassemblyjs/helper-buffer": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.6.tgz", - "integrity": "sha512-z3nFzdcp1mb8nEOFFk8DrYLpHvhKC3grJD2ardfKOzmbmJvEf/tPIqCY+sNcwZIY8ZD7IkB2l7/pqhUhqm7hLA==", + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.12.1.tgz", + "integrity": "sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw==", "dev": true }, "node_modules/@webassemblyjs/helper-numbers": { @@ -4441,15 +4421,15 @@ "dev": true }, "node_modules/@webassemblyjs/helper-wasm-section": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.6.tgz", - "integrity": "sha512-LPpZbSOwTpEC2cgn4hTydySy1Ke+XEu+ETXuoyvuyezHO3Kjdu90KK95Sh9xTbmjrCsUwvWwCOQQNta37VrS9g==", + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.12.1.tgz", + "integrity": "sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g==", "dev": true, "dependencies": { - "@webassemblyjs/ast": "1.11.6", - "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-buffer": "1.12.1", "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/wasm-gen": "1.11.6" + "@webassemblyjs/wasm-gen": "1.12.1" } }, "node_modules/@webassemblyjs/ieee754": { @@ -4477,28 +4457,28 @@ "dev": true }, "node_modules/@webassemblyjs/wasm-edit": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.6.tgz", - "integrity": "sha512-Ybn2I6fnfIGuCR+Faaz7YcvtBKxvoLV3Lebn1tM4o/IAJzmi9AWYIPWpyBfU8cC+JxAO57bk4+zdsTjJR+VTOw==", + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.12.1.tgz", + "integrity": "sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g==", "dev": true, "dependencies": { - "@webassemblyjs/ast": "1.11.6", - "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-buffer": "1.12.1", "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/helper-wasm-section": "1.11.6", - "@webassemblyjs/wasm-gen": "1.11.6", - "@webassemblyjs/wasm-opt": "1.11.6", - "@webassemblyjs/wasm-parser": "1.11.6", - "@webassemblyjs/wast-printer": "1.11.6" + "@webassemblyjs/helper-wasm-section": "1.12.1", + "@webassemblyjs/wasm-gen": "1.12.1", + "@webassemblyjs/wasm-opt": "1.12.1", + "@webassemblyjs/wasm-parser": "1.12.1", + "@webassemblyjs/wast-printer": "1.12.1" } }, "node_modules/@webassemblyjs/wasm-gen": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.6.tgz", - "integrity": "sha512-3XOqkZP/y6B4F0PBAXvI1/bky7GryoogUtfwExeP/v7Nzwo1QLcq5oQmpKlftZLbT+ERUOAZVQjuNVak6UXjPA==", + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.12.1.tgz", + "integrity": "sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w==", "dev": true, "dependencies": { - "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/ast": "1.12.1", "@webassemblyjs/helper-wasm-bytecode": "1.11.6", "@webassemblyjs/ieee754": "1.11.6", "@webassemblyjs/leb128": "1.11.6", @@ -4506,24 +4486,24 @@ } }, "node_modules/@webassemblyjs/wasm-opt": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.6.tgz", - "integrity": "sha512-cOrKuLRE7PCe6AsOVl7WasYf3wbSo4CeOk6PkrjS7g57MFfVUF9u6ysQBBODX0LdgSvQqRiGz3CXvIDKcPNy4g==", + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.12.1.tgz", + "integrity": "sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg==", "dev": true, "dependencies": { - "@webassemblyjs/ast": "1.11.6", - "@webassemblyjs/helper-buffer": "1.11.6", - "@webassemblyjs/wasm-gen": "1.11.6", - "@webassemblyjs/wasm-parser": "1.11.6" + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-buffer": "1.12.1", + "@webassemblyjs/wasm-gen": "1.12.1", + "@webassemblyjs/wasm-parser": "1.12.1" } }, "node_modules/@webassemblyjs/wasm-parser": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.6.tgz", - "integrity": "sha512-6ZwPeGzMJM3Dqp3hCsLgESxBGtT/OeCvCZ4TA1JUPYgmhAx38tTPR9JaKy0S5H3evQpO/h2uWs2j6Yc/fjkpTQ==", + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.12.1.tgz", + "integrity": "sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ==", "dev": true, "dependencies": { - "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/ast": "1.12.1", "@webassemblyjs/helper-api-error": "1.11.6", "@webassemblyjs/helper-wasm-bytecode": "1.11.6", "@webassemblyjs/ieee754": "1.11.6", @@ -4532,12 +4512,12 @@ } }, "node_modules/@webassemblyjs/wast-printer": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.6.tgz", - "integrity": "sha512-JM7AhRcE+yW2GWYaKeHL5vt4xqee5N2WcezptmgyhNS+ScggqcT1OtXykhAb13Sn5Yas0j2uv9tHgrjwvzAP4A==", + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.12.1.tgz", + "integrity": "sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA==", "dev": true, "dependencies": { - "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/ast": "1.12.1", "@xtuc/long": "4.2.2" } }, @@ -4625,10 +4605,10 @@ "node": ">=0.4.0" } }, - "node_modules/acorn-import-assertions": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz", - "integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==", + "node_modules/acorn-import-attributes": { + "version": "1.9.5", + "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", + "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", "dev": true, "peerDependencies": { "acorn": "^8" @@ -20128,9 +20108,9 @@ } }, "node_modules/watchpack": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", - "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz", + "integrity": "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==", "dev": true, "dependencies": { "glob-to-regexp": "^0.4.1", @@ -20155,34 +20135,33 @@ "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=" }, "node_modules/webpack": { - "version": "5.89.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.89.0.tgz", - "integrity": "sha512-qyfIC10pOr70V+jkmud8tMfajraGCZMBWJtrmuBymQKCrLTRejBI8STDp1MCyZu/QTdZSeacCQYpYNQVOzX5kw==", + "version": "5.95.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.95.0.tgz", + "integrity": "sha512-2t3XstrKULz41MNMBF+cJ97TyHdyQ8HCt//pqErqDvNjU9YQBnZxIHa11VXsi7F3mb5/aO2tuDxdeTPdU7xu9Q==", "dev": true, "dependencies": { - "@types/eslint-scope": "^3.7.3", - "@types/estree": "^1.0.0", - "@webassemblyjs/ast": "^1.11.5", - "@webassemblyjs/wasm-edit": "^1.11.5", - "@webassemblyjs/wasm-parser": "^1.11.5", + "@types/estree": "^1.0.5", + "@webassemblyjs/ast": "^1.12.1", + "@webassemblyjs/wasm-edit": "^1.12.1", + "@webassemblyjs/wasm-parser": "^1.12.1", "acorn": "^8.7.1", - "acorn-import-assertions": "^1.9.0", - "browserslist": "^4.14.5", + "acorn-import-attributes": "^1.9.5", + "browserslist": "^4.21.10", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.15.0", + "enhanced-resolve": "^5.17.1", "es-module-lexer": "^1.2.1", "eslint-scope": "5.1.1", "events": "^3.2.0", "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.2.9", + "graceful-fs": "^4.2.11", "json-parse-even-better-errors": "^2.3.1", "loader-runner": "^4.2.0", "mime-types": "^2.1.27", "neo-async": "^2.6.2", "schema-utils": "^3.2.0", "tapable": "^2.1.1", - "terser-webpack-plugin": "^5.3.7", - "watchpack": "^2.4.0", + "terser-webpack-plugin": "^5.3.10", + "watchpack": "^2.4.1", "webpack-sources": "^3.2.3" }, "bin": { @@ -20736,9 +20715,9 @@ } }, "node_modules/webpack/node_modules/enhanced-resolve": { - "version": "5.15.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz", - "integrity": "sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg==", + "version": "5.17.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz", + "integrity": "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==", "dev": true, "dependencies": { "graceful-fs": "^4.2.4", @@ -23982,26 +23961,6 @@ "@types/enzyme": "*" } }, - "@types/eslint": { - "version": "8.56.2", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.2.tgz", - "integrity": "sha512-uQDwm1wFHmbBbCZCqAlq6Do9LYwByNZHWzXppSnay9SuwJ+VRbjkbLABer54kcPnMSlG6Fdiy2yaFXm/z9Z5gw==", - "dev": true, - "requires": { - "@types/estree": "*", - "@types/json-schema": "*" - } - }, - "@types/eslint-scope": { - "version": "3.7.7", - "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", - "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", - "dev": true, - "requires": { - "@types/eslint": "*", - "@types/estree": "*" - } - }, "@types/estree": { "version": "0.0.39", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", @@ -24744,9 +24703,9 @@ } }, "@webassemblyjs/ast": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.6.tgz", - "integrity": "sha512-IN1xI7PwOvLPgjcf180gC1bqn3q/QaOCwYUahIOhbYUu8KA/3tw2RT/T0Gidi1l7Hhj5D/INhJxiICObqpMu4Q==", + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.12.1.tgz", + "integrity": "sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg==", "dev": true, "requires": { "@webassemblyjs/helper-numbers": "1.11.6", @@ -24766,9 +24725,9 @@ "dev": true }, "@webassemblyjs/helper-buffer": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.6.tgz", - "integrity": "sha512-z3nFzdcp1mb8nEOFFk8DrYLpHvhKC3grJD2ardfKOzmbmJvEf/tPIqCY+sNcwZIY8ZD7IkB2l7/pqhUhqm7hLA==", + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.12.1.tgz", + "integrity": "sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw==", "dev": true }, "@webassemblyjs/helper-numbers": { @@ -24789,15 +24748,15 @@ "dev": true }, "@webassemblyjs/helper-wasm-section": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.6.tgz", - "integrity": "sha512-LPpZbSOwTpEC2cgn4hTydySy1Ke+XEu+ETXuoyvuyezHO3Kjdu90KK95Sh9xTbmjrCsUwvWwCOQQNta37VrS9g==", + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.12.1.tgz", + "integrity": "sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g==", "dev": true, "requires": { - "@webassemblyjs/ast": "1.11.6", - "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-buffer": "1.12.1", "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/wasm-gen": "1.11.6" + "@webassemblyjs/wasm-gen": "1.12.1" } }, "@webassemblyjs/ieee754": { @@ -24825,28 +24784,28 @@ "dev": true }, "@webassemblyjs/wasm-edit": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.6.tgz", - "integrity": "sha512-Ybn2I6fnfIGuCR+Faaz7YcvtBKxvoLV3Lebn1tM4o/IAJzmi9AWYIPWpyBfU8cC+JxAO57bk4+zdsTjJR+VTOw==", + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.12.1.tgz", + "integrity": "sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g==", "dev": true, "requires": { - "@webassemblyjs/ast": "1.11.6", - "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-buffer": "1.12.1", "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/helper-wasm-section": "1.11.6", - "@webassemblyjs/wasm-gen": "1.11.6", - "@webassemblyjs/wasm-opt": "1.11.6", - "@webassemblyjs/wasm-parser": "1.11.6", - "@webassemblyjs/wast-printer": "1.11.6" + "@webassemblyjs/helper-wasm-section": "1.12.1", + "@webassemblyjs/wasm-gen": "1.12.1", + "@webassemblyjs/wasm-opt": "1.12.1", + "@webassemblyjs/wasm-parser": "1.12.1", + "@webassemblyjs/wast-printer": "1.12.1" } }, "@webassemblyjs/wasm-gen": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.6.tgz", - "integrity": "sha512-3XOqkZP/y6B4F0PBAXvI1/bky7GryoogUtfwExeP/v7Nzwo1QLcq5oQmpKlftZLbT+ERUOAZVQjuNVak6UXjPA==", + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.12.1.tgz", + "integrity": "sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w==", "dev": true, "requires": { - "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/ast": "1.12.1", "@webassemblyjs/helper-wasm-bytecode": "1.11.6", "@webassemblyjs/ieee754": "1.11.6", "@webassemblyjs/leb128": "1.11.6", @@ -24854,24 +24813,24 @@ } }, "@webassemblyjs/wasm-opt": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.6.tgz", - "integrity": "sha512-cOrKuLRE7PCe6AsOVl7WasYf3wbSo4CeOk6PkrjS7g57MFfVUF9u6ysQBBODX0LdgSvQqRiGz3CXvIDKcPNy4g==", + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.12.1.tgz", + "integrity": "sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg==", "dev": true, "requires": { - "@webassemblyjs/ast": "1.11.6", - "@webassemblyjs/helper-buffer": "1.11.6", - "@webassemblyjs/wasm-gen": "1.11.6", - "@webassemblyjs/wasm-parser": "1.11.6" + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-buffer": "1.12.1", + "@webassemblyjs/wasm-gen": "1.12.1", + "@webassemblyjs/wasm-parser": "1.12.1" } }, "@webassemblyjs/wasm-parser": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.6.tgz", - "integrity": "sha512-6ZwPeGzMJM3Dqp3hCsLgESxBGtT/OeCvCZ4TA1JUPYgmhAx38tTPR9JaKy0S5H3evQpO/h2uWs2j6Yc/fjkpTQ==", + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.12.1.tgz", + "integrity": "sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ==", "dev": true, "requires": { - "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/ast": "1.12.1", "@webassemblyjs/helper-api-error": "1.11.6", "@webassemblyjs/helper-wasm-bytecode": "1.11.6", "@webassemblyjs/ieee754": "1.11.6", @@ -24880,12 +24839,12 @@ } }, "@webassemblyjs/wast-printer": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.6.tgz", - "integrity": "sha512-JM7AhRcE+yW2GWYaKeHL5vt4xqee5N2WcezptmgyhNS+ScggqcT1OtXykhAb13Sn5Yas0j2uv9tHgrjwvzAP4A==", + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.12.1.tgz", + "integrity": "sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA==", "dev": true, "requires": { - "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/ast": "1.12.1", "@xtuc/long": "4.2.2" } }, @@ -24938,10 +24897,10 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==" }, - "acorn-import-assertions": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz", - "integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==", + "acorn-import-attributes": { + "version": "1.9.5", + "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", + "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", "dev": true }, "acorn-walk": { @@ -36909,9 +36868,9 @@ } }, "watchpack": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", - "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz", + "integrity": "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==", "dev": true, "requires": { "glob-to-regexp": "^0.4.1", @@ -36933,34 +36892,33 @@ "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=" }, "webpack": { - "version": "5.89.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.89.0.tgz", - "integrity": "sha512-qyfIC10pOr70V+jkmud8tMfajraGCZMBWJtrmuBymQKCrLTRejBI8STDp1MCyZu/QTdZSeacCQYpYNQVOzX5kw==", + "version": "5.95.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.95.0.tgz", + "integrity": "sha512-2t3XstrKULz41MNMBF+cJ97TyHdyQ8HCt//pqErqDvNjU9YQBnZxIHa11VXsi7F3mb5/aO2tuDxdeTPdU7xu9Q==", "dev": true, "requires": { - "@types/eslint-scope": "^3.7.3", - "@types/estree": "^1.0.0", - "@webassemblyjs/ast": "^1.11.5", - "@webassemblyjs/wasm-edit": "^1.11.5", - "@webassemblyjs/wasm-parser": "^1.11.5", + "@types/estree": "^1.0.5", + "@webassemblyjs/ast": "^1.12.1", + "@webassemblyjs/wasm-edit": "^1.12.1", + "@webassemblyjs/wasm-parser": "^1.12.1", "acorn": "^8.7.1", - "acorn-import-assertions": "^1.9.0", - "browserslist": "^4.14.5", + "acorn-import-attributes": "^1.9.5", + "browserslist": "^4.21.10", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.15.0", + "enhanced-resolve": "^5.17.1", "es-module-lexer": "^1.2.1", "eslint-scope": "5.1.1", "events": "^3.2.0", "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.2.9", + "graceful-fs": "^4.2.11", "json-parse-even-better-errors": "^2.3.1", "loader-runner": "^4.2.0", "mime-types": "^2.1.27", "neo-async": "^2.6.2", "schema-utils": "^3.2.0", "tapable": "^2.1.1", - "terser-webpack-plugin": "^5.3.7", - "watchpack": "^2.4.0", + "terser-webpack-plugin": "^5.3.10", + "watchpack": "^2.4.1", "webpack-sources": "^3.2.3" }, "dependencies": { @@ -36983,9 +36941,9 @@ } }, "enhanced-resolve": { - "version": "5.15.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz", - "integrity": "sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg==", + "version": "5.17.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz", + "integrity": "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==", "dev": true, "requires": { "graceful-fs": "^4.2.4", diff --git a/package.json b/package.json index 5f4ebf02..8354c420 100644 --- a/package.json +++ b/package.json @@ -216,7 +216,7 @@ "umd-compat-loader": "^2.1.1", "url": "^0.11.3", "util": "^0.12.5", - "webpack": "^5.89.0", + "webpack": "^5.95.0", "webpack-bundle-analyzer": "^4.10.1", "webpack-cli": "^5.1.4", "webpack-dev-server": "^4.15.1", From c31b7ee53813d3be29eb63be4cb4fadea2a0c02f Mon Sep 17 00:00:00 2001 From: Tim Perry Date: Fri, 8 Nov 2024 15:17:10 +0100 Subject: [PATCH 028/168] Update Mockttp for Windows sleep fix --- package-lock.json | 14 +++++++------- package.json | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index 558bc5b4..112991a4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -83,7 +83,7 @@ "mobx-shallow-undo": "^1.0.0", "mobx-utils": "^5.1.0", "mockrtc": "^0.3.1", - "mockttp": "^3.15.1", + "mockttp": "^3.15.4", "monaco-editor": "^0.27.0", "node-forge": "^1.3.0", "openapi-directory": "^1.3.0", @@ -13258,9 +13258,9 @@ "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" }, "node_modules/mockttp": { - "version": "3.15.1", - "resolved": "https://registry.npmjs.org/mockttp/-/mockttp-3.15.1.tgz", - "integrity": "sha512-Yyn5/41gWffYyPs340z7hFcegLQDRc4mODPupJxaTycZTMB1ui9Joz0lEZNEi/XqWuVwFIuB+Ft52O5WGXI2rg==", + "version": "3.15.4", + "resolved": "https://registry.npmjs.org/mockttp/-/mockttp-3.15.4.tgz", + "integrity": "sha512-38Q5oZUBY4CNMCl2FHMcW2igVZM0IjgnmLcmNzQDc+hn5Hbdrsd7fz4tzXP64cw53T8PERPMM8s6TH4xlakUZw==", "dependencies": { "@graphql-tools/schema": "^8.5.0", "@graphql-tools/utils": "^8.8.0", @@ -31627,9 +31627,9 @@ } }, "mockttp": { - "version": "3.15.1", - "resolved": "https://registry.npmjs.org/mockttp/-/mockttp-3.15.1.tgz", - "integrity": "sha512-Yyn5/41gWffYyPs340z7hFcegLQDRc4mODPupJxaTycZTMB1ui9Joz0lEZNEi/XqWuVwFIuB+Ft52O5WGXI2rg==", + "version": "3.15.4", + "resolved": "https://registry.npmjs.org/mockttp/-/mockttp-3.15.4.tgz", + "integrity": "sha512-38Q5oZUBY4CNMCl2FHMcW2igVZM0IjgnmLcmNzQDc+hn5Hbdrsd7fz4tzXP64cw53T8PERPMM8s6TH4xlakUZw==", "requires": { "@graphql-tools/schema": "^8.5.0", "@graphql-tools/utils": "^8.8.0", diff --git a/package.json b/package.json index 1700329d..e21e6d33 100644 --- a/package.json +++ b/package.json @@ -108,7 +108,7 @@ "mobx-shallow-undo": "^1.0.0", "mobx-utils": "^5.1.0", "mockrtc": "^0.3.1", - "mockttp": "^3.15.1", + "mockttp": "^3.15.4", "monaco-editor": "^0.27.0", "node-forge": "^1.3.0", "openapi-directory": "^1.3.0", From 5ddef60c30799fe05acf12fd8cdc8b47cab3315b Mon Sep 17 00:00:00 2001 From: Tim Perry Date: Fri, 8 Nov 2024 15:49:07 +0100 Subject: [PATCH 029/168] Remove various google fonts leftovers Seems like the fonts.css specifically was breaking some SW usage, so good to fix that! --- custom-typings/@beyonk__google-fonts-webpack-plugin.d.ts | 1 - src/index.html | 1 - 2 files changed, 2 deletions(-) delete mode 100644 custom-typings/@beyonk__google-fonts-webpack-plugin.d.ts diff --git a/custom-typings/@beyonk__google-fonts-webpack-plugin.d.ts b/custom-typings/@beyonk__google-fonts-webpack-plugin.d.ts deleted file mode 100644 index b40e7582..00000000 --- a/custom-typings/@beyonk__google-fonts-webpack-plugin.d.ts +++ /dev/null @@ -1 +0,0 @@ -declare module '@beyonk/google-fonts-webpack-plugin'; \ No newline at end of file diff --git a/src/index.html b/src/index.html index 391e1ba5..34c1b329 100644 --- a/src/index.html +++ b/src/index.html @@ -5,7 +5,6 @@ - HTTP Toolkit