From f956d5228b74d07928efdecbd32ab05d9dc4a68a Mon Sep 17 00:00:00 2001 From: jvbriones <1674192+jvbriones@users.noreply.github.com> Date: Mon, 28 Sep 2026 15:38:47 +0200 Subject: [PATCH 1/7] chore(MMQA-1951): Use typed analytics schema contract - Evaluation --- .../workflows/check-analytics-contract.yml | 143 +++ .../QuickBuyAmountSelected.test-d.ts | 41 + .../generated/QuickBuyAmountSelected.ts | 131 +++ docs/readme/typed-analytics-contracts.md | 159 +++ package.json | 5 + scripts/analytics/typed-events/README.md | 63 ++ .../typed-events/check-contract.test.ts | 59 ++ .../analytics/typed-events/check-contract.ts | 530 ++++++++++ .../typed-events/compare-generators.ts | 221 ++++ .../typed-events/contract-lock.test.ts | 93 ++ .../analytics/typed-events/contract-lock.ts | 193 ++++ .../analytics/typed-events/contract.lock.json | 13 + .../analytics/typed-events/contract.test.ts | 228 ++++ scripts/analytics/typed-events/contract.ts | 503 +++++++++ .../analytics/typed-events/generate-pilot.ts | 99 ++ .../analytics/typed-events/generator.test.ts | 168 +++ scripts/analytics/typed-events/generator.ts | 391 +++++++ .../analytics/typed-events/quick-buy-pilot.ts | 58 ++ .../typewriter-comparison.test.ts | 81 ++ .../typed-events/typewriter-comparison.ts | 112 ++ .../analytics/typed-events/verify-contract.ts | 98 ++ yarn.lock | 970 +++++++++++++++++- 22 files changed, 4320 insertions(+), 39 deletions(-) create mode 100644 .github/workflows/check-analytics-contract.yml create mode 100644 app/util/analytics/generated/QuickBuyAmountSelected.test-d.ts create mode 100644 app/util/analytics/generated/QuickBuyAmountSelected.ts create mode 100644 docs/readme/typed-analytics-contracts.md create mode 100644 scripts/analytics/typed-events/README.md create mode 100644 scripts/analytics/typed-events/check-contract.test.ts create mode 100644 scripts/analytics/typed-events/check-contract.ts create mode 100644 scripts/analytics/typed-events/compare-generators.ts create mode 100644 scripts/analytics/typed-events/contract-lock.test.ts create mode 100644 scripts/analytics/typed-events/contract-lock.ts create mode 100644 scripts/analytics/typed-events/contract.lock.json create mode 100644 scripts/analytics/typed-events/contract.test.ts create mode 100644 scripts/analytics/typed-events/contract.ts create mode 100644 scripts/analytics/typed-events/generate-pilot.ts create mode 100644 scripts/analytics/typed-events/generator.test.ts create mode 100644 scripts/analytics/typed-events/generator.ts create mode 100644 scripts/analytics/typed-events/quick-buy-pilot.ts create mode 100644 scripts/analytics/typed-events/typewriter-comparison.test.ts create mode 100644 scripts/analytics/typed-events/typewriter-comparison.ts create mode 100644 scripts/analytics/typed-events/verify-contract.ts diff --git a/.github/workflows/check-analytics-contract.yml b/.github/workflows/check-analytics-contract.yml new file mode 100644 index 000000000000..9015baa4f162 --- /dev/null +++ b/.github/workflows/check-analytics-contract.yml @@ -0,0 +1,143 @@ +name: Check deployed analytics contract + +# periodically/manual -> download newest deployed analytics contract -> verify it -> +# generate pilot -> type-check -> notify success/failure -> change nothing. +# +# more details in /docs/readme/typed-analytics-contracts.md + +on: + schedule: + # Hourly from 06:00 through 22:00 UTC on weekdays. + - cron: '0 6-22 * * 1-5' + workflow_dispatch: + inputs: + release-tag: + description: Optional exact analytics contract release tag + required: false + type: string + +permissions: + contents: read + id-token: write + +concurrency: + group: check-deployed-analytics-contract + cancel-in-progress: true + +jobs: + verify-mobile-contract: + name: Verify latest deployed Mobile contract + environment: default-branch + runs-on: ubuntu-latest + timeout-minutes: 15 + outputs: + release-tag: ${{ steps.check.outputs.release-tag }} + release-url: ${{ steps.check.outputs.release-url }} + steps: + - name: Checkout Mobile + uses: actions/checkout@v6 + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version-file: '.nvmrc' + cache: yarn + + - name: Install dependencies + uses: nick-fields/retry@ad984534de44a9489a53aefd81eb77f87c70dc60 # v4 + with: + timeout_minutes: 10 + max_attempts: 3 + retry_wait_seconds: 30 + command: yarn install --immutable + + - name: Download contract with shared action + id: shared-contract + uses: MetaMask/github-tools/.github/actions/download-analytics-contract@v1 + with: + token-exchange-url: ${{ vars.TOKEN_EXCHANGE_URL }} + target-repository: Consensys/segment-schema + asset-name: metamask-mobile.json + release-tag: ${{ inputs.release-tag }} + output-directory: ${{ runner.temp }}/analytics-contract + + - name: Check downloaded analytics contract + id: check + env: + SHARED_RELEASE_TAG: ${{ steps.shared-contract.outputs.release-tag }} + SHARED_RELEASE_URL: ${{ steps.shared-contract.outputs.release-url }} + SHARED_MANIFEST_PATH: ${{ steps.shared-contract.outputs.manifest-path }} + SHARED_ASSET_PATH: ${{ steps.shared-contract.outputs.asset-path }} + SHARED_ASSET_DIGEST: ${{ steps.shared-contract.outputs.asset-digest }} + JSON_OUTPUT: ${{ runner.temp }}/analytics-contract-check.json + GENERATED_OUTPUT_DIRECTORY: ${{ runner.temp }}/analytics-contract-check + shell: bash + run: | + set -euo pipefail + yarn analytics:contract:check \ + --manifest "${SHARED_MANIFEST_PATH}" \ + --contract "${SHARED_ASSET_PATH}" \ + --asset-sha256 "${SHARED_ASSET_DIGEST}" \ + --release-tag "${SHARED_RELEASE_TAG}" \ + --release-url "${SHARED_RELEASE_URL}" \ + --output-directory "${GENERATED_OUTPUT_DIRECTORY}" \ + --json-output "${JSON_OUTPUT}" + diff -u \ + app/util/analytics/generated/QuickBuyAmountSelected.ts \ + "${GENERATED_OUTPUT_DIRECTORY}/generated/QuickBuyAmountSelected.ts" + diff -u \ + app/util/analytics/generated/QuickBuyAmountSelected.test-d.ts \ + "${GENERATED_OUTPUT_DIRECTORY}/generated/QuickBuyAmountSelected.test-d.ts" + echo "release-tag=$(jq -r '.release.tag' "${JSON_OUTPUT}")" >> "${GITHUB_OUTPUT}" + echo "release-url=$(jq -r '.release.url' "${JSON_OUTPUT}")" >> "${GITHUB_OUTPUT}" + + - name: Summarize verification + if: always() + env: + RELEASE_TAG: ${{ steps.shared-contract.outputs.release-tag }} + RELEASE_URL: ${{ steps.shared-contract.outputs.release-url }} + CHECK_RESULT: ${{ steps.check.outcome }} + CHECK_FILE: ${{ runner.temp }}/analytics-contract-check.json + shell: bash + run: | + { + echo "### Deployed Mobile analytics contract" + echo + if [ -n "${RELEASE_TAG}" ]; then + echo "- Release: [\`${RELEASE_TAG}\`](${RELEASE_URL})" + else + echo "- Release: unresolved" + fi + echo "- Contract, generation, and type-check result: \`${CHECK_RESULT}\`" + + if [ -f "${CHECK_FILE}" ]; then + echo "- Source SHA: \`$(jq -r '.verification.sourceSha' "${CHECK_FILE}")\`" + echo "- Rule count: \`$(jq -r '.verification.ruleCount' "${CHECK_FILE}")\`" + echo "- Contract hash: \`$(jq -r '.verification.contractHash' "${CHECK_FILE}")\`" + echo "- Checked event versions: \`$(jq -r '.generated.versions | join(", ")' "${CHECK_FILE}")\`" + fi + } >> "${GITHUB_STEP_SUMMARY}" + + notify-slack: + name: Notify Slack + needs: verify-mobile-contract + if: >- + ${{ + always() && + !cancelled() + }} + environment: default-branch + runs-on: ubuntu-latest + env: + SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK_PLAYWRIGHT_HEALTH_REPORT }} + steps: + - name: Send Slack notification + if: env.SLACK_WEBHOOK != '' + uses: slackapi/slack-github-action@91efab103c0de0a537f72a35f6b8cda0ee76bf0a + with: + webhook: ${{ env.SLACK_WEBHOOK }} + webhook-type: incoming-webhook + payload: | + { + "text": "Mobile deployed analytics contract check: ${{ needs.verify-mobile-contract.result }}\nRelease: ${{ needs.verify-mobile-contract.outputs.release-tag || 'unresolved' }}\nRun: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" + } diff --git a/app/util/analytics/generated/QuickBuyAmountSelected.test-d.ts b/app/util/analytics/generated/QuickBuyAmountSelected.test-d.ts new file mode 100644 index 000000000000..f2577bba1cdf --- /dev/null +++ b/app/util/analytics/generated/QuickBuyAmountSelected.test-d.ts @@ -0,0 +1,41 @@ +import { createQuickBuyAmountSelected } from './QuickBuyAmountSelected'; + +declare const transport: Parameters[0]; + +const event = createQuickBuyAmountSelected(transport); + +event.v1.track({ + amount_selection_method: 'preset', + amount_usd: 1, + source: 'notification', +}); + +event.v2.track({ + amount_selection_method: 'preset', + amount_usd: 1, + source: 'notification', +}); + +// @ts-expect-error source is required +event.v2.track({ + amount_selection_method: 'custom_input', + amount_usd: 1, +}); + +event.v2.track({ + amount_selection_method: 'preset', + amount_usd: 1, + source: 'notification', + // @ts-expect-error unknown properties are rejected + unknown_property: true, +}); + +event.v1.track({ + // @ts-expect-error slider is not a v1 amount selection method + amount_selection_method: 'slider', + amount_usd: 1, + source: 'notification', +}); + +// @ts-expect-error only generated versions are available +event.v3.track({}); diff --git a/app/util/analytics/generated/QuickBuyAmountSelected.ts b/app/util/analytics/generated/QuickBuyAmountSelected.ts new file mode 100644 index 000000000000..0451629ab036 --- /dev/null +++ b/app/util/analytics/generated/QuickBuyAmountSelected.ts @@ -0,0 +1,131 @@ +/** + * GENERATED FILE — DO NOT EDIT. + * + * Generated from the verified deployed Mobile analytics contract. + */ + +export interface AnalyticsEventContext { + readonly protocols: { + readonly event_version: number; + }; +} + +export interface AnalyticsEventTransport { + track( + eventName: string, + properties: object, + context: AnalyticsEventContext, + ): void; +} + +type ExactProperties = Actual & + Record, never>; + +interface EventVersion { + track( + properties: ExactProperties, + ): void; +} + +const createEventVersion = ( + transport: AnalyticsEventTransport, + eventName: string, + version: number, +): EventVersion => ({ + track: (properties) => { + transport.track(eventName, properties as object, { + protocols: { + event_version: version, + }, + }); + }, +}); + +const EVENT_NAME = 'Quick Buy Amount Selected'; + +export interface QuickBuyAmountSelectedV1Properties { + /** How the user picked the amount. */ + readonly amount_selection_method: 'preset' | 'custom_input'; + /** Selected USD amount. */ + readonly amount_usd: number; + /** CAIP-19 identifier of the token associated with the event. */ + readonly caip19?: string; + /** Symbol of the source token currently selected to pay with (may be undefined before token resolution finishes). */ + readonly pay_with_token?: string; + /** Preset chip value, when amount_selection_method is preset. */ + readonly preset_value?: 20 | 50 | 100 | 250; + /** Surface from which the QuickBuy sheet was opened. */ + readonly source: + | 'notification' + | 'profile_position' + | 'leaderboard' + | 'asset_details' + | 'market_insights' + | 'security_trust' + | 'explore_search' + | 'explore_crypto' + | 'explore_now' + | 'explore_rwas' + | 'explore_trending' + | 'explore_stocks'; + /** Wallet address of the trader. May be absent on cold-start notification deeplinks where only a profile UUID is available. */ + readonly trader_address?: string; +} + +export interface QuickBuyAmountSelectedV2Properties { + /** How the user picked the amount. */ + readonly amount_selection_method: 'preset' | 'custom_input' | 'slider'; + /** Selected USD amount. */ + readonly amount_usd: number; + /** CAIP-19 identifier of the spot token associated with the event, including chain ID. Sent for spot positions only; absent for perps. */ + readonly caip19?: string; + /** Lowercase chain slug for the position. Spot examples: base, ethereum, solana. Perps: hyperliquid. Present on follow-trading token events when a position context is resolved. */ + readonly chain_name?: string; + /** How the user reached the follow-trading token (trade) screen before opening Quick Buy. Only set when Quick Buy is opened from the trade screen. */ + readonly original_entry_point?: + | 'leaderboard' + | 'trader_profile' + | 'notification' + | 'deep_link' + | 'home_carousel'; + /** Symbol of the source token currently selected to pay with (may be undefined before token resolution finishes). */ + readonly pay_with_token?: string; + /** Tradable Hyperliquid perps market symbol. Sent for perp positions only; absent for spot. */ + readonly perps_market?: string; + /** Preset chip value, when amount_selection_method is preset. */ + readonly preset_value?: 10 | 50 | 100 | 250; + /** Slider position (0–100), when amount_selection_method is slider. */ + readonly slider_percent?: number; + /** Surface from which the QuickBuy sheet was opened. */ + readonly source: + | 'notification' + | 'profile_position' + | 'leaderboard' + | 'trader_feed' + | 'asset_details' + | 'market_insights' + | 'security_trust' + | 'explore_search' + | 'explore_crypto' + | 'explore_now' + | 'explore_rwas' + | 'explore_trending' + | 'explore_stocks'; + /** Wallet address of the trader. May be absent on cold-start notification deeplinks where only a profile UUID is available. */ + readonly trader_address?: string; +} + +export const createQuickBuyAmountSelected = ( + transport: AnalyticsEventTransport, +) => ({ + v1: createEventVersion( + transport, + EVENT_NAME, + 1, + ), + v2: createEventVersion( + transport, + EVENT_NAME, + 2, + ), +}); diff --git a/docs/readme/typed-analytics-contracts.md b/docs/readme/typed-analytics-contracts.md new file mode 100644 index 000000000000..1bcc464e2f29 --- /dev/null +++ b/docs/readme/typed-analytics-contracts.md @@ -0,0 +1,159 @@ +# Typed analytics contracts + +## Purpose + +Mobile analytics contracts are produced from the rules deployed to Segment. +The deployed contract release is the source of truth for event names, +versions, properties, primitive types, and enums. + +This work adds a client-side verification and generation path without changing +Mobile's production analytics dispatch. + +## Contract lifecycle + +```text +segment-schema change + -> Segment deployment + -> private immutable-by-convention contract release + -> Mobile downloads metamask-mobile.json + -> Mobile verifies hashes and selected event versions + -> Mobile generates and type-checks the pilot API +``` + +The schema repository release contains: + +- `manifest.json` — provenance, tracking-plan IDs, rule counts, and semantic + hashes. +- `metamask-mobile.json` — the deployed Mobile contract. +- `metamask-extension.json` — the deployed Extension contract. + +Mobile must use an exact release tag and verify both the release asset digest +and the semantic contract hash. It must not consume a mutable `latest` asset in +application builds. + +The raw contract is private. It is downloaded only to temporary local or CI +storage and must not be committed or included in generated artifacts unless +the visibility policy changes. + +## Local verification + +Prerequisites: + +- Node and Yarn versions from `.nvmrc` and `package.json`. +- `gh` authenticated with read access to `Consensys/segment-schema`. + +Check the newest published contract: + +```sh +yarn analytics:contract:check +``` + +Check a specific release: + +```sh +yarn analytics:contract:check \ + --release-tag analytics-contracts-67390efdfde3-8db9e9f80441 +``` + +Check the pinned contract release: + +```sh +yarn analytics:contract:check:locked +``` + +Local runs retain the downloaded contract and generated pilot in the +gitignored `temp/local-analytics-contract-check/` directory by default. Use +`--output-directory` to choose another location. + +The command: + +1. Resolves the newest published analytics release, or the requested tag. +2. Downloads `manifest.json` and `metamask-mobile.json`. +3. Verifies source provenance, platform, tracking-plan ID, rule count, asset + digest, manifest hash, and semantic hash. +4. Selects Quick Buy Amount Selected v1 and v2. +5. Generates a temporary strict TypeScript facade. +6. Type-checks valid and compile-fail fixtures. +7. Retains local files in the default temporary directory; CI-provided input + paths are cleaned unless an output directory or `--keep` is used. + +The pinned metadata in +[`contract.lock.json`](../../scripts/analytics/typed-events/contract.lock.json) +contains only public release identifiers and hashes. It does not contain the +private contract files. + +The reviewed Quick Buy facade is public and tracked at +[`QuickBuyAmountSelected.ts`](../../app/util/analytics/generated/QuickBuyAmountSelected.ts). +CI regenerates the facade from the downloaded contract and fails if the tracked +types drift from the deployed contract. + +## Typewriter comparison + +Run the local comparison against the same pinned Quick Buy v1/v2 fixture: + +```sh +yarn analytics:contract:compare +``` + +This uses pinned Typewriter 9.2.0 with TypeScript and +`analytics-react-native`. It reads a temporary local `plan.json`, does not +update Segment, and does not require Segment credentials. Typewriter output, +the adapted plan, and `comparison.json` remain under the ignored +`temp/analytics-contract-comparison/` directory. + +The comparison is reference material, not production output. Stock Typewriter +normalizes Track rules and retains only the highest version for a repeated event +key. The strict pilot preserves explicit v1 and v2 APIs and adds the version +context. The comparison also records permissive index signatures, event-version +context, documentation/deprecation markers, and progressive-builder support. + +## Quick Buy pilot and versions + +The pilot event family is `Quick Buy Amount Selected`. + +The same Segment event name has multiple schema versions: + +- v1 supports `preset` and `custom_input`. +- v2 adds `slider`, `slider_percent`, `trader_feed`, and changed preset + values. +- Both versions currently require `amount_usd`, + `amount_selection_method`, and `source`. + +The generated API requires explicit version selection: + +```ts +QuickBuyAmountSelected.v1.track(properties); +QuickBuyAmountSelected.v2.track(properties); +``` + +The generated wrapper also supplies +`context.protocols.event_version`. Existing Mobile calls do not use this +facade yet, so production behavior is unchanged. + +## Generator boundary + +The deployed contract currently flattens event-owned properties together with +SDK and destination properties. The pilot therefore uses an explicit reviewed +allowlist in +[`quick-buy-pilot.ts`](../../scripts/analytics/typed-events/quick-buy-pilot.ts). + +The narrow generator supports the pilot's primitive types, enums, required and +optional properties, exact object properties, and explicit versions. It fails +on unsupported JSON Schema features instead of silently generating an +incomplete type. + +This is intentional. A general generator must first establish property +provenance and a broader JSON Schema support matrix. + +## Planned progression + +1. Merge and observe the incubation workflow. +2. Review the pinned lock and local Typewriter comparison. +3. Add optional `AnalyticsContext` plumbing through Mobile's existing analytics + queue and adapter. +4. Integrate the generated facade into one reviewed Quick Buy production call. +5. Expand the generic generator and event coverage incrementally. + +The analytics controller remains a generic runtime transport. Event-specific +property typing belongs in the generated Mobile facade, not in +`@metamask/analytics-controller`. diff --git a/package.json b/package.json index 64dcadae435c..92971ca52ff8 100644 --- a/package.json +++ b/package.json @@ -112,6 +112,9 @@ "build:attribution": "./scripts/generate-attributions.sh", "test": "yarn test:unit", "test:unit": "jest ./app/ ./locales/ ./tests/**/*.test.ts .github/**/*.test.ts ./scripts/**/*.test.ts --testPathIgnorePatterns='.*/tests/smoke-appium/.*\\.spec\\.(ts|tsx|js)$|.*/e2e/.*\\.spec\\.(ts|tsx|js)$|.*\\.integration\\.test\\.(ts|tsx|js|jsx)$|.*/tests/llm-workflow/.*\\.test\\.(ts|tsx|js)$|.*\\.view(\\..*)?\\.test\\.(ts|tsx|js|jsx)$'", + "analytics:contract:check": "tsx scripts/analytics/typed-events/check-contract.ts", + "analytics:contract:check:locked": "tsx scripts/analytics/typed-events/check-contract.ts --lock-file scripts/analytics/typed-events/contract.lock.json", + "analytics:contract:compare": "tsx scripts/analytics/typed-events/compare-generators.ts", "test:unit:llm-workflow": "jest -c jest.config.llm-workflow.js", "test:smart-e2e": "tsx --test .ai-pr-analyzer/modes/smart-e2e/*.test.ts .github/actions/smart-e2e-selection/*.test.mjs", "mm:doctor": "tsx tests/llm-workflow/doctor-entry.ts", @@ -213,6 +216,7 @@ "ganache/secp256k1": "^4.0.4", "@expo/image-utils/semver": "7.5.2", "eas-cli/nanoid": "3.3.18", + "typescript": "~5.8.3", "nanoid@npm:^3.0.0": "3.3.18", "nanoid@npm:^5.0.0": "5.1.16", "fast-uri": "3.1.5", @@ -792,6 +796,7 @@ "ts-node": "^10.9.2", "tsx": "^4.21.0", "typescript": "~5.8.3", + "typewriter": "9.2.0", "webdriverio": "^9.27.0", "webextension-polyfill": "^0.12.0", "webpack": "^5.88.2", diff --git a/scripts/analytics/typed-events/README.md b/scripts/analytics/typed-events/README.md new file mode 100644 index 000000000000..209243cb299f --- /dev/null +++ b/scripts/analytics/typed-events/README.md @@ -0,0 +1,63 @@ +# Typed analytics contract check + +This tooling downloads the deployed Mobile analytics contract, verifies its +release metadata, generates the Quick Buy v1/v2 pilot in a temporary directory, +and type-checks the generated API. Local runs retain these files in the +gitignored `temp/local-analytics-contract-check/` directory by default. + +It does not modify application code. CI runs that receive already-downloaded +paths clean their generated temporary files unless `--keep` or +`--output-directory` is provided. + +## Local usage + +The local command uses the GitHub CLI authentication already configured on the +machine. Confirm access first: + +```sh +gh auth status +``` + +Check the newest published analytics release: + +```sh +yarn analytics:contract:check +``` + +Check a specific release: + +```sh +yarn analytics:contract:check \ + --release-tag analytics-contracts-67390efdfde3-8db9e9f80441 +``` + +Check the pinned release metadata: + +```sh +yarn analytics:contract:check:locked +``` + +Use a different directory for the retained files: + +```sh +yarn analytics:contract:check \ + --output-directory temp/my-analytics-contract-check +``` + +Compare the strict pilot with pinned stock Typewriter locally: + +```sh +yarn analytics:contract:compare +``` + +The comparison uses TypeScript with `analytics-react-native`, the exact lock +fixture, and a local `plan.json`. It does not update Segment or require a +Segment API token. Typewriter output and the comparison report remain under +the ignored `temp/analytics-contract-comparison/` directory. + +The reviewed Quick Buy facade is tracked publicly under +`app/util/analytics/generated/`. CI regenerates it and compares the result to +the tracked files. + +The command fails if release verification, Quick Buy v1/v2 generation, or the +generated TypeScript checks fail. diff --git a/scripts/analytics/typed-events/check-contract.test.ts b/scripts/analytics/typed-events/check-contract.test.ts new file mode 100644 index 000000000000..69341eff16b3 --- /dev/null +++ b/scripts/analytics/typed-events/check-contract.test.ts @@ -0,0 +1,59 @@ +import { + flattenReleasePages, + selectLatestAnalyticsRelease, + type AnalyticsContractRelease, +} from './check-contract'; + +const createRelease = ( + tag: string, + publishedAt: string, +): AnalyticsContractRelease => ({ + assets: [], + publishedAt, + tag, + url: `https://github.com/Consensys/segment-schema/releases/tag/${tag}`, +}); + +describe('selectLatestAnalyticsRelease', () => { + it('selects the newest analytics contract release', () => { + const releases = [ + createRelease( + 'analytics-contracts-old', + '2026-09-25T14:00:00.000Z', + ), + createRelease( + 'other-release', + '2026-09-25T16:00:00.000Z', + ), + createRelease( + 'analytics-contracts-new', + '2026-09-25T15:00:00.000Z', + ), + ]; + + const result = selectLatestAnalyticsRelease(releases); + + expect(result.tag).toBe('analytics-contracts-new'); + }); + + it('rejects a release list without analytics contract releases', () => { + const releases = [createRelease('other-release', '2026-09-25T16:00:00.000Z')]; + + expect(() => selectLatestAnalyticsRelease(releases)).toThrow( + 'No published releases with prefix', + ); + }); +}); + +describe('flattenReleasePages', () => { + it('combines all paginated release pages', () => { + const releases = [ + [{ tag_name: 'first' }], + [{ tag_name: 'second' }], + ]; + + const result = flattenReleasePages(releases); + + expect(result).toEqual([{ tag_name: 'first' }, { tag_name: 'second' }]); + }); +}); diff --git a/scripts/analytics/typed-events/check-contract.ts b/scripts/analytics/typed-events/check-contract.ts new file mode 100644 index 000000000000..017b2af9994b --- /dev/null +++ b/scripts/analytics/typed-events/check-contract.ts @@ -0,0 +1,530 @@ +import { execFileSync } from 'node:child_process'; +import { + mkdtempSync, + mkdirSync, + rmSync, + writeFileSync, +} from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { format } from 'prettier'; +import { + verifyMobileContract, + type VerifiedMobileContract, +} from './contract'; +import { + assertContractMatchesLock, + readContractLock, + type ContractLock, +} from './contract-lock'; +import { generateEventPilot } from './generator'; +import { QUICK_BUY_AMOUNT_SELECTED_PILOT } from './quick-buy-pilot'; + +const CONTRACT_REPOSITORY = 'Consensys/segment-schema'; +const ANALYTICS_RELEASE_PREFIX = 'analytics-contracts-'; +const DEFAULT_LOCAL_OUTPUT_DIRECTORY = join( + process.cwd(), + 'temp', + 'local-analytics-contract-check', +); +const log = (message: string): void => { + console.error(`[analytics-contract] ${message}`); +}; + +interface ReleaseAsset { + readonly digest: string | null; + readonly name: string; +} + +export interface AnalyticsContractRelease { + readonly assets: readonly ReleaseAsset[]; + readonly publishedAt: string; + readonly tag: string; + readonly url: string; +} + +export interface ContractCheckOptions { + readonly assetSha256?: string; + readonly contractPath?: string; + readonly keep: boolean; + readonly jsonOutput?: string; + readonly lockFilePath?: string; + readonly manifestPath?: string; + readonly outputDirectory?: string; + readonly releaseTag?: string; + readonly releaseUrl?: string; +} + +export interface ContractCheckResult { + readonly generated: { + readonly eventName: string; + readonly versions: readonly number[]; + }; + readonly release: { + readonly publishedAt: string; + readonly tag: string; + readonly url: string; + }; + readonly verification: { + readonly assetSha256: string; + readonly contractHash: string; + readonly ruleCount: number; + readonly sourceSha: string; + readonly trackingPlanId: string; + }; + readonly workDirectory?: string; +} + +const isRecord = (value: unknown): value is Record => + typeof value === 'object' && value !== null && !Array.isArray(value); + +const readString = ( + object: Record, + key: string, + path: string, +): string => { + const value = object[key]; + + if (typeof value !== 'string') { + throw new Error(`Expected string at ${path}.${key}`); + } + + return value; +}; + +const readRelease = (value: unknown, path: string): AnalyticsContractRelease => { + if (!isRecord(value)) { + throw new Error(`Expected release object at ${path}`); + } + + const assetsValue = value.assets; + + if (!Array.isArray(assetsValue)) { + throw new Error(`Expected release assets array at ${path}.assets`); + } + + const assets = assetsValue.map((asset, index) => { + const assetPath = `${path}.assets[${index}]`; + + if (!isRecord(asset)) { + throw new Error(`Expected release asset object at ${assetPath}`); + } + + const digest = asset.digest; + + if (digest !== null && typeof digest !== 'string') { + throw new Error(`Expected string or null at ${assetPath}.digest`); + } + + return { + digest, + name: readString(asset, 'name', assetPath), + }; + }); + + return { + assets, + publishedAt: readString(value, 'published_at', path), + tag: readString(value, 'tag_name', path), + url: readString(value, 'html_url', path), + }; +}; + +const runGhJson = (args: readonly string[]): unknown => { + const output = execFileSync('gh', args, { + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'inherit'], + }); + + return JSON.parse(output) as unknown; +}; + +/** + * Select the newest published analytics contract release. + * + * @param releases - Release metadata returned by GitHub. + * @returns The newest analytics contract release. + */ +export const selectLatestAnalyticsRelease = ( + releases: readonly AnalyticsContractRelease[], +): AnalyticsContractRelease => { + const candidates = releases + .filter((release) => release.tag.startsWith(ANALYTICS_RELEASE_PREFIX)) + .toSorted( + (left, right) => + Date.parse(right.publishedAt) - Date.parse(left.publishedAt), + ); + const [latest] = candidates; + + if (!latest) { + throw new Error( + `No published releases with prefix '${ANALYTICS_RELEASE_PREFIX}' were found`, + ); + } + + return latest; +}; + +/** + * Flatten the paginated GitHub releases response produced by `gh api + * --paginate --slurp`. + * + * @param value - GitHub API response pages. + * @returns Release values from all pages. + */ +export const flattenReleasePages = (value: unknown): readonly unknown[] => { + if (!Array.isArray(value)) { + throw new Error('Expected GitHub releases response to be an array'); + } + + if (value.every((page) => Array.isArray(page))) { + return value.flat(); + } + + return value; +}; + +const resolveRelease = ( + requestedTag: string | undefined, +): AnalyticsContractRelease => { + if (requestedTag) { + const releaseValue = runGhJson([ + 'api', + `repos/${CONTRACT_REPOSITORY}/releases/tags/${requestedTag}`, + ]); + + if (!isRecord(releaseValue)) { + throw new Error(`Expected release object for '${requestedTag}'`); + } + + const release = readRelease( + releaseValue, + `release ${requestedTag}`, + ); + + if (!release.tag.startsWith(ANALYTICS_RELEASE_PREFIX)) { + throw new Error( + `Release ${release.tag} is not an analytics contract release`, + ); + } + + if (releaseValue.draft !== false || releaseValue.prerelease !== false) { + throw new Error(`Release ${release.tag} is not published`); + } + + return release; + } + + const releasesValue = runGhJson([ + 'api', + '--paginate', + '--slurp', + `repos/${CONTRACT_REPOSITORY}/releases?per_page=100`, + ]); + const releases = flattenReleasePages(releasesValue); + + const parsedReleases = releases + .filter((release) => isRecord(release)) + .filter((release) => release.draft === false && release.prerelease === false) + .map((release, index) => readRelease(release, `releases[${index}]`)); + + return selectLatestAnalyticsRelease(parsedReleases); +}; + +const readAssetDigest = (release: AnalyticsContractRelease): string => { + const asset = release.assets.find( + ({ name }) => name === 'metamask-mobile.json', + ); + + if (!asset?.digest || !/^sha256:[0-9a-f]{64}$/u.test(asset.digest)) { + throw new Error( + `Release ${release.tag} has no valid metamask-mobile.json SHA-256 digest`, + ); + } + + return asset.digest; +}; + +const downloadRelease = ( + release: AnalyticsContractRelease, + directory: string, +): void => { + mkdirSync(directory, { recursive: true }); + rmSync(join(directory, 'manifest.json'), { force: true }); + rmSync(join(directory, 'metamask-mobile.json'), { force: true }); + execFileSync( + 'gh', + [ + 'release', + 'download', + release.tag, + '--repo', + CONTRACT_REPOSITORY, + '--pattern', + 'manifest.json', + '--pattern', + 'metamask-mobile.json', + '--dir', + directory, + ], + { stdio: 'inherit' }, + ); +}; + +const writeGeneratedFiles = async ( + verifiedContract: VerifiedMobileContract, + outputDirectory: string, +): Promise => { + const generated = generateEventPilot( + verifiedContract.eventRules, + QUICK_BUY_AMOUNT_SELECTED_PILOT, + ); + + mkdirSync(outputDirectory, { recursive: true }); + writeFileSync( + join(outputDirectory, 'QuickBuyAmountSelected.ts'), + await format(generated.source, { + parser: 'typescript', + singleQuote: true, + }), + ); + writeFileSync( + join(outputDirectory, 'QuickBuyAmountSelected.test-d.ts'), + await format(generated.typeTests, { + parser: 'typescript', + singleQuote: true, + }), + ); +}; + +const typeCheckGeneratedFiles = (directory: string): void => { + const yarnCommand = process.platform === 'win32' ? 'yarn.cmd' : 'yarn'; + + execFileSync( + yarnCommand, + [ + 'tsc', + '--noEmit', + '--skipLibCheck', + '--target', + 'esnext', + '--module', + 'preserve', + '--moduleResolution', + 'bundler', + '--strict', + join(directory, 'QuickBuyAmountSelected.ts'), + join(directory, 'QuickBuyAmountSelected.test-d.ts'), + ], + { stdio: 'inherit' }, + ); +}; + +const readOptions = (args: readonly string[]): ContractCheckOptions => { + const options = new Map(); + + for (let index = 0; index < args.length; index++) { + const argument = args[index]; + + if (!argument?.startsWith('--')) { + throw new Error(`Unexpected argument '${argument ?? ''}'`); + } + + if (argument === '--keep') { + options.set('keep', true); + continue; + } + + const value = args[index + 1]; + + if (!value || value.startsWith('--')) { + throw new Error(`Missing value for '${argument}'`); + } + + options.set(argument.slice(2), value); + index++; + } + + const outputDirectory = options.get('output-directory'); + const jsonOutput = options.get('json-output'); + const lockFilePath = options.get('lock-file'); + const assetSha256 = options.get('asset-sha256'); + const contractPath = options.get('contract'); + const manifestPath = options.get('manifest'); + const releaseTag = options.get('release-tag'); + const releaseUrl = options.get('release-url'); + + const localInputs = [assetSha256, contractPath, manifestPath].filter( + (value) => value !== undefined, + ); + + if (localInputs.length !== 0 && localInputs.length !== 3) { + throw new Error( + 'The --manifest, --contract, and --asset-sha256 options must be provided together', + ); + } + + return { + assetSha256: typeof assetSha256 === 'string' ? assetSha256 : undefined, + contractPath: typeof contractPath === 'string' ? contractPath : undefined, + keep: options.get('keep') === true, + jsonOutput: typeof jsonOutput === 'string' ? jsonOutput : undefined, + lockFilePath: + typeof lockFilePath === 'string' ? lockFilePath : undefined, + manifestPath: typeof manifestPath === 'string' ? manifestPath : undefined, + outputDirectory: + typeof outputDirectory === 'string' ? outputDirectory : undefined, + releaseTag: typeof releaseTag === 'string' ? releaseTag : undefined, + releaseUrl: typeof releaseUrl === 'string' ? releaseUrl : undefined, + }; +}; + +/** + * Download, verify, generate, and type-check the latest Mobile contract. + * + * @param options - Release selection and temporary-output options. + * @returns A sanitized result suitable for CI summaries. + */ +export const checkAnalyticsContract = async ( + options: ContractCheckOptions, +): Promise => { + const contractLock: ContractLock | undefined = options.lockFilePath + ? readContractLock(options.lockFilePath) + : undefined; + const usesProvidedContract = + options.manifestPath !== undefined && + options.contractPath !== undefined && + options.assetSha256 !== undefined; + if ( + contractLock && + options.releaseTag !== undefined && + options.releaseTag !== contractLock.releaseTag + ) { + throw new Error( + `Release tag '${options.releaseTag}' does not match contract lock release '${contractLock.releaseTag}'`, + ); + } + const requestedReleaseTag = options.releaseTag ?? contractLock?.releaseTag; + const release = usesProvidedContract + ? { + assets: [], + publishedAt: '', + tag: requestedReleaseTag ?? 'local', + url: options.releaseUrl ?? '', + } + : resolveRelease(requestedReleaseTag); + const assetDigest = usesProvidedContract + ? options.assetSha256 + : readAssetDigest(release); + const workDirectory = + options.outputDirectory ?? + (usesProvidedContract + ? mkdtempSync(join(tmpdir(), 'metamask-mobile-analytics-')) + : DEFAULT_LOCAL_OUTPUT_DIRECTORY); + const contractDirectory = join(workDirectory, 'contract'); + const generatedDirectory = join(workDirectory, 'generated'); + const shouldKeepWorkDirectory = + options.keep || + options.outputDirectory !== undefined || + !usesProvidedContract; + + try { + if (contractLock) { + log( + `Using contract lock '${options.lockFilePath}' for release '${contractLock.releaseTag}'.`, + ); + } + + if (usesProvidedContract) { + log( + `Using the contract downloaded by the shared action for release '${release.tag}'; skipping download.`, + ); + } else { + log(`Downloading analytics contract release '${release.tag}'.`); + downloadRelease(release, contractDirectory); + } + + if ( + contractLock && + assetDigest !== undefined && + assetDigest !== contractLock.assetSha256 + ) { + throw new Error( + `Contract lock mismatch for assetSha256: expected '${contractLock.assetSha256}', got '${assetDigest}'`, + ); + } + + const verifiedContract = verifyMobileContract({ + manifestPath: + options.manifestPath ?? join(contractDirectory, 'manifest.json'), + contractPath: + options.contractPath ?? join(contractDirectory, 'metamask-mobile.json'), + expectedAssetSha256: assetDigest, + eventName: QUICK_BUY_AMOUNT_SELECTED_PILOT.eventName, + versions: QUICK_BUY_AMOUNT_SELECTED_PILOT.versions.map( + ({ version }) => version, + ), + }); + + if (contractLock) { + assertContractMatchesLock(contractLock, { + assetSha256: verifiedContract.assetSha256, + contract: verifiedContract.contract, + releaseTag: release.tag, + releaseUrl: release.url, + }); + } + + await writeGeneratedFiles(verifiedContract, generatedDirectory); + typeCheckGeneratedFiles(generatedDirectory); + + const result: ContractCheckResult = { + generated: { + eventName: QUICK_BUY_AMOUNT_SELECTED_PILOT.eventName, + versions: QUICK_BUY_AMOUNT_SELECTED_PILOT.versions.map( + ({ version }) => version, + ), + }, + release: { + publishedAt: release.publishedAt, + tag: release.tag, + url: release.url, + }, + verification: { + assetSha256: verifiedContract.assetSha256, + contractHash: verifiedContract.contract.hash, + ruleCount: verifiedContract.contract.ruleCount, + sourceSha: verifiedContract.manifest.sourceSha, + trackingPlanId: verifiedContract.contract.trackingPlan.id, + }, + ...(shouldKeepWorkDirectory ? { workDirectory } : {}), + }; + + if (options.jsonOutput) { + writeFileSync(options.jsonOutput, `${JSON.stringify(result, null, 2)}\n`); + } + + if (shouldKeepWorkDirectory) { + log(`Retained analytics contract artifacts at '${workDirectory}'.`); + } + + return result; + } finally { + if (!shouldKeepWorkDirectory) { + log('Cleaning temporary analytics contract artifacts.'); + rmSync(workDirectory, { recursive: true, force: true }); + } + } +}; + +const main = async (): Promise => { + const result = await checkAnalyticsContract(readOptions(process.argv.slice(2))); + console.log(JSON.stringify(result, null, 2)); +}; + +if (process.argv[1]?.endsWith('check-contract.ts')) { + main().catch((error: unknown) => { + console.error(String(error)); + process.exitCode = 1; + }); +} diff --git a/scripts/analytics/typed-events/compare-generators.ts b/scripts/analytics/typed-events/compare-generators.ts new file mode 100644 index 000000000000..664b7ff5367f --- /dev/null +++ b/scripts/analytics/typed-events/compare-generators.ts @@ -0,0 +1,221 @@ +import { execFileSync } from 'node:child_process'; +import { mkdirSync, rmSync, writeFileSync } from 'node:fs'; +import { join, relative } from 'node:path'; +import { + checkAnalyticsContract, + type ContractCheckResult, +} from './check-contract'; +import { verifyMobileContract } from './contract'; +import { QUICK_BUY_AMOUNT_SELECTED_PILOT } from './quick-buy-pilot'; +import { + readTypewriterOutput, + writeTypewriterFixture, +} from './typewriter-comparison'; + +const DEFAULT_LOCK_FILE = join( + process.cwd(), + 'scripts', + 'analytics', + 'typed-events', + 'contract.lock.json', +); +const DEFAULT_OUTPUT_DIRECTORY = join( + process.cwd(), + 'temp', + 'analytics-contract-comparison', +); + +interface ComparisonOptions { + readonly lockFilePath: string; + readonly outputDirectory: string; +} + +interface GeneratorComparisonReport { + readonly contract: { + readonly contractHash: string; + readonly releaseTag: string; + readonly sourceSha: string; + }; + readonly event: { + readonly name: string; + readonly versions: readonly number[]; + }; + readonly narrowGenerator: { + readonly exactPropertyCheck: boolean; + readonly eventVersionContext: boolean; + readonly sourcePath: string; + readonly typeTestsPath: string; + }; + readonly typewriter: { + readonly deprecationAnnotationsFound: boolean; + readonly eventVersionContextGenerated: boolean; + readonly generatedFiles: readonly string[]; + readonly generatedSourceBytes: number; + readonly eventNameFound: boolean; + readonly permissiveIndexSignature: boolean; + readonly expectedRetainedVersions: readonly number[]; + readonly progressiveBuilderApiFound: boolean; + readonly sdk: 'analytics-react-native'; + }; + readonly limitations: readonly string[]; +} + +const readOptions = (args: readonly string[]): ComparisonOptions => { + const options = new Map(); + + for (let index = 0; index < args.length; index++) { + const argument = args[index]; + + if (!argument?.startsWith('--')) { + throw new Error(`Unexpected argument '${argument ?? ''}'`); + } + + const value = args[index + 1]; + + if (!value || value.startsWith('--')) { + throw new Error(`Missing value for '${argument}'`); + } + + options.set(argument.slice(2), value); + index++; + } + + return { + lockFilePath: options.get('lock-file') ?? DEFAULT_LOCK_FILE, + outputDirectory: options.get('output-directory') ?? DEFAULT_OUTPUT_DIRECTORY, + }; +}; + +const toRelativePath = (filePath: string): string => + relative(process.cwd(), filePath); + +const createReport = ( + checkResult: ContractCheckResult, + outputDirectory: string, + typewriterSource: string, + typewriterFiles: readonly string[], +): GeneratorComparisonReport => { + const versions = QUICK_BUY_AMOUNT_SELECTED_PILOT.versions.map( + ({ version }) => version, + ); + const latestVersion = Math.max(...versions); + + return { + contract: { + contractHash: checkResult.verification.contractHash, + releaseTag: checkResult.release.tag, + sourceSha: checkResult.verification.sourceSha, + }, + event: { + name: QUICK_BUY_AMOUNT_SELECTED_PILOT.eventName, + versions, + }, + narrowGenerator: { + exactPropertyCheck: true, + eventVersionContext: true, + sourcePath: toRelativePath( + join(outputDirectory, 'generated', 'QuickBuyAmountSelected.ts'), + ), + typeTestsPath: toRelativePath( + join(outputDirectory, 'generated', 'QuickBuyAmountSelected.test-d.ts'), + ), + }, + typewriter: { + deprecationAnnotationsFound: typewriterSource.includes('@deprecated'), + eventVersionContextGenerated: typewriterSource.includes('event_version'), + generatedFiles: typewriterFiles.map(toRelativePath), + generatedSourceBytes: Buffer.byteLength(typewriterSource, 'utf8'), + eventNameFound: typewriterSource.includes( + QUICK_BUY_AMOUNT_SELECTED_PILOT.eventName, + ), + permissiveIndexSignature: typewriterSource.includes( + '[property: string]: any;', + ), + expectedRetainedVersions: [latestVersion], + progressiveBuilderApiFound: + typewriterSource.includes('.create(') && + typewriterSource.includes('setProperties'), + sdk: 'analytics-react-native', + }, + limitations: [ + 'Stock Typewriter filters the fixture to TRACK rules.', + 'Stock Typewriter retains only the highest version for one event key.', + 'The comparison does not treat Typewriter output as production code.', + 'Progressive event construction and runtime completeness remain follow-up API work.', + ], + }; +}; + +const main = async (): Promise => { + const options = readOptions(process.argv.slice(2)); + + mkdirSync(options.outputDirectory, { recursive: true }); + rmSync(join(options.outputDirectory, 'typewriter'), { + force: true, + recursive: true, + }); + + const checkResult = await checkAnalyticsContract({ + keep: true, + lockFilePath: options.lockFilePath, + outputDirectory: options.outputDirectory, + }); + const workDirectory = checkResult.workDirectory; + + if (!workDirectory) { + throw new Error('Expected retained contract artifacts for comparison'); + } + + const verifiedContract = verifyMobileContract({ + manifestPath: join(workDirectory, 'contract', 'manifest.json'), + contractPath: join(workDirectory, 'contract', 'metamask-mobile.json'), + expectedAssetSha256: checkResult.verification.assetSha256, + eventName: QUICK_BUY_AMOUNT_SELECTED_PILOT.eventName, + versions: QUICK_BUY_AMOUNT_SELECTED_PILOT.versions.map( + ({ version }) => version, + ), + }); + const typewriterDirectory = join(workDirectory, 'typewriter'); + const typewriterFixture = writeTypewriterFixture( + verifiedContract.contract, + verifiedContract.eventRules, + typewriterDirectory, + ); + const yarnCommand = process.platform === 'win32' ? 'yarn.cmd' : 'yarn'; + + execFileSync( + yarnCommand, + [ + 'typewriter', + 'build', + '--config', + typewriterFixture.configPath, + '--mode', + 'prod', + ], + { + stdio: 'inherit', + }, + ); + + const typewriterOutput = readTypewriterOutput( + typewriterFixture.generatedDirectory, + ); + const report = createReport( + checkResult, + workDirectory, + typewriterOutput.source, + typewriterOutput.files, + ); + const reportPath = join(workDirectory, 'comparison.json'); + + writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); + console.log(JSON.stringify({ ...report, reportPath }, null, 2)); +}; + +if (process.argv[1]?.endsWith('compare-generators.ts')) { + main().catch((error: unknown) => { + console.error(String(error)); + process.exitCode = 1; + }); +} diff --git a/scripts/analytics/typed-events/contract-lock.test.ts b/scripts/analytics/typed-events/contract-lock.test.ts new file mode 100644 index 000000000000..91b24ed7977e --- /dev/null +++ b/scripts/analytics/typed-events/contract-lock.test.ts @@ -0,0 +1,93 @@ +import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { + assertContractMatchesLock, + readContractLock, + type ContractLock, + type ContractLockVerificationInput, +} from './contract-lock'; + +const createLock = (): ContractLock => ({ + assetName: 'metamask-mobile.json', + assetSha256: `sha256:${'a'.repeat(64)}`, + contractFormatVersion: 1, + contractHash: `sha256:${'b'.repeat(64)}`, + lockVersion: 1, + platform: 'mobile', + releaseTag: 'analytics-contracts-test', + releaseUrl: + 'https://github.com/Consensys/segment-schema/releases/tag/analytics-contracts-test', + ruleCount: 2, + sourceSha: 'a'.repeat(40), + trackingPlanId: 'tp_test', +}); + +const createInput = (): ContractLockVerificationInput => ({ + assetSha256: `sha256:${'a'.repeat(64)}`, + contract: { + formatVersion: 1, + hash: `sha256:${'b'.repeat(64)}`, + platform: 'mobile', + ruleCount: 2, + sourceSha: 'a'.repeat(40), + trackingPlan: { + id: 'tp_test', + }, + }, + releaseTag: 'analytics-contracts-test', + releaseUrl: + 'https://github.com/Consensys/segment-schema/releases/tag/analytics-contracts-test', +}); + +describe('contract lock', () => { + const roots: string[] = []; + + afterEach(() => { + roots.splice(0).forEach((root) => rmSync(root, { recursive: true, force: true })); + }); + + it('reads valid lock metadata', () => { + const root = mkdtempSync(join(tmpdir(), 'analytics-contract-lock-')); + roots.push(root); + const filePath = join(root, 'contract.lock.json'); + const lock = createLock(); + + writeFileSync(filePath, JSON.stringify(lock)); + + expect(readContractLock(filePath)).toEqual(lock); + }); + + it('rejects a lock for a non-Mobile asset', () => { + const root = mkdtempSync(join(tmpdir(), 'analytics-contract-lock-')); + roots.push(root); + const filePath = join(root, 'contract.lock.json'); + + writeFileSync( + filePath, + JSON.stringify({ + ...createLock(), + assetName: 'metamask-extension.json', + }), + ); + + expect(() => readContractLock(filePath)).toThrow( + 'Expected Mobile contract asset', + ); + }); + + it('rejects a verified contract that differs from the lock', () => { + const lock = createLock(); + const input = { + ...createInput(), + contract: { + ...createInput().contract, + ruleCount: 3, + }, + }; + + expect(() => assertContractMatchesLock(lock, input)).toThrow( + 'Contract lock mismatch for ruleCount', + ); + }); +}); diff --git a/scripts/analytics/typed-events/contract-lock.ts b/scripts/analytics/typed-events/contract-lock.ts new file mode 100644 index 000000000000..44f69917245c --- /dev/null +++ b/scripts/analytics/typed-events/contract-lock.ts @@ -0,0 +1,193 @@ +import { readFileSync } from 'node:fs'; +import type { MobileContract, TrackingPlanMetadata } from './contract'; + +const SHA256_PATTERN = /^sha256:[0-9a-f]{64}$/u; +const SOURCE_SHA_PATTERN = /^[0-9a-f]{40}$/u; +const RELEASE_PREFIX = 'analytics-contracts-'; + +export interface ContractLock { + readonly assetName: 'metamask-mobile.json'; + readonly assetSha256: string; + readonly contractFormatVersion: number; + readonly contractHash: string; + readonly lockVersion: 1; + readonly platform: 'mobile'; + readonly releaseTag: string; + readonly releaseUrl: string; + readonly ruleCount: number; + readonly sourceSha: string; + readonly trackingPlanId: string; +} + +export interface ContractLockVerificationInput { + readonly assetSha256: string; + readonly contract: Pick< + MobileContract, + | 'formatVersion' + | 'platform' + | 'sourceSha' + | 'ruleCount' + | 'hash' + > & { + readonly trackingPlan: Pick; + }; + readonly releaseTag: string; + readonly releaseUrl: string; +} + +const isRecord = (value: unknown): value is Record => + typeof value === 'object' && value !== null && !Array.isArray(value); + +const readString = ( + object: Record, + key: string, + path: string, +): string => { + const value = object[key]; + + if (typeof value !== 'string') { + throw new Error(`Expected string at ${path}.${key}`); + } + + return value; +}; + +const readInteger = ( + object: Record, + key: string, + path: string, +): number => { + const value = object[key]; + + if (typeof value !== 'number' || !Number.isInteger(value)) { + throw new Error(`Expected integer at ${path}.${key}`); + } + + return value; +}; + +const readJsonObject = (filePath: string): Record => { + let parsed: unknown; + + try { + parsed = JSON.parse(readFileSync(filePath, 'utf8')) as unknown; + } catch (error) { + throw new Error(`Unable to parse contract lock '${filePath}': ${String(error)}`); + } + + if (!isRecord(parsed)) { + throw new Error(`Expected contract lock '${filePath}' to contain an object`); + } + + return parsed; +}; + +const readSha256 = ( + object: Record, + key: string, + path: string, +): string => { + const value = readString(object, key, path); + + if (!SHA256_PATTERN.test(value)) { + throw new Error(`Expected SHA-256 digest at ${path}.${key}`); + } + + return value; +}; + +const readSourceSha = ( + object: Record, + key: string, + path: string, +): string => { + const value = readString(object, key, path); + + if (!SOURCE_SHA_PATTERN.test(value)) { + throw new Error(`Expected Git commit SHA at ${path}.${key}`); + } + + return value; +}; + +/** + * Read and validate public metadata for one immutable Mobile contract release. + * + * @param filePath - Path to the lock metadata. + * @returns The validated lock. + */ +export const readContractLock = (filePath: string): ContractLock => { + const object = readJsonObject(filePath); + const path = 'contractLock'; + const lockVersion = readInteger(object, 'lockVersion', path); + const assetName = readString(object, 'assetName', path); + const platform = readString(object, 'platform', path); + const releaseTag = readString(object, 'releaseTag', path); + + if (lockVersion !== 1) { + throw new Error(`Unsupported contract lock version '${lockVersion}'`); + } + + if (assetName !== 'metamask-mobile.json') { + throw new Error(`Expected Mobile contract asset, got '${assetName}'`); + } + + if (platform !== 'mobile') { + throw new Error(`Expected Mobile contract platform, got '${platform}'`); + } + + if (!releaseTag.startsWith(RELEASE_PREFIX)) { + throw new Error(`Expected analytics contract release tag, got '${releaseTag}'`); + } + + return { + assetName, + assetSha256: readSha256(object, 'assetSha256', path), + contractFormatVersion: readInteger(object, 'contractFormatVersion', path), + contractHash: readSha256(object, 'contractHash', path), + lockVersion: 1, + platform: 'mobile', + releaseTag, + releaseUrl: readString(object, 'releaseUrl', path), + ruleCount: readInteger(object, 'ruleCount', path), + sourceSha: readSourceSha(object, 'sourceSha', path), + trackingPlanId: readString(object, 'trackingPlanId', path), + }; +}; + +const assertEqual = ( + label: string, + actual: string | number, + expected: string | number, +): void => { + if (actual !== expected) { + throw new Error( + `Contract lock mismatch for ${label}: expected '${expected}', got '${actual}'`, + ); + } +}; + +/** + * Verify downloaded release metadata and contract content against a lock. + * + * @param lock - Expected immutable release metadata. + * @param input - Resolved release and verified contract values. + */ +export const assertContractMatchesLock = ( + lock: ContractLock, + input: ContractLockVerificationInput, +): void => { + assertEqual('releaseTag', input.releaseTag, lock.releaseTag); + assertEqual('releaseUrl', input.releaseUrl, lock.releaseUrl); + assertEqual('assetSha256', input.assetSha256, lock.assetSha256); + assertEqual( + 'contractFormatVersion', + input.contract.formatVersion, + lock.contractFormatVersion, + ); + assertEqual('platform', input.contract.platform, lock.platform); + assertEqual('sourceSha', input.contract.sourceSha, lock.sourceSha); + assertEqual('trackingPlanId', input.contract.trackingPlan.id, lock.trackingPlanId); + assertEqual('ruleCount', input.contract.ruleCount, lock.ruleCount); + assertEqual('contractHash', input.contract.hash, lock.contractHash); +}; diff --git a/scripts/analytics/typed-events/contract.lock.json b/scripts/analytics/typed-events/contract.lock.json new file mode 100644 index 000000000000..11c67b63b130 --- /dev/null +++ b/scripts/analytics/typed-events/contract.lock.json @@ -0,0 +1,13 @@ +{ + "lockVersion": 1, + "assetName": "metamask-mobile.json", + "assetSha256": "sha256:9e07f2ac4c23fa37fb69001364f79981b7f7c3fcb126ee1e39ab457f02ecead7", + "contractFormatVersion": 1, + "contractHash": "sha256:f75e241c1a832b22c80f9537765e51dac0ba380133f1ec90a6f3e7c77235c4c3", + "platform": "mobile", + "releaseTag": "analytics-contracts-67390efdfde3-8db9e9f80441", + "releaseUrl": "https://github.com/Consensys/segment-schema/releases/tag/analytics-contracts-67390efdfde3-8db9e9f80441", + "ruleCount": 784, + "sourceSha": "67390efdfde300fec50596c149eb02e46f91c5eb", + "trackingPlanId": "tp_2iOKYXh3y11VP5nMhQJAfB5McDH" +} diff --git a/scripts/analytics/typed-events/contract.test.ts b/scripts/analytics/typed-events/contract.test.ts new file mode 100644 index 000000000000..5ee8f21c0334 --- /dev/null +++ b/scripts/analytics/typed-events/contract.test.ts @@ -0,0 +1,228 @@ +import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { + computeSemanticContractHash, + sha256File, + verifyMobileContract, + type ContractManifest, + type ContractRule, + type MobileContract, +} from './contract'; + +const EVENT_NAME = 'Quick Buy Amount Selected'; +const SOURCE_SHA = '67390efdfde300fec50596c149eb02e46f91c5eb'; +const TRACKING_PLAN_ID = 'tp_mobile_test'; + +interface FixtureOptions { + readonly versions?: readonly number[]; + readonly deprecatedVersions?: readonly number[]; +} + +interface ContractFixture { + readonly root: string; + readonly manifestPath: string; + readonly contractPath: string; + readonly assetSha256: string; + readonly manifest: ContractManifest; + readonly contract: MobileContract; +} + +const createRule = ( + version: number, + deprecatedAt: string | null, +): ContractRule => ({ + key: EVENT_NAME, + type: 'TRACK', + version, + jsonSchema: { + type: 'object', + properties: { + properties: { + type: 'object', + properties: { + amount_usd: { type: ['number'] }, + amount_selection_method: { + type: ['string'], + enum: ['preset', 'custom_input'], + }, + source: { type: ['string'] }, + }, + required: ['amount_usd', 'amount_selection_method', 'source'], + }, + }, + }, + createdAt: null, + updatedAt: null, + deprecatedAt, +}); + +const createFixture = ({ + versions = [1, 2], + deprecatedVersions = [], +}: FixtureOptions = {}): ContractFixture => { + const root = mkdtempSync(join(tmpdir(), 'typed-analytics-contract-')); + const manifestPath = join(root, 'manifest.json'); + const contractPath = join(root, 'metamask-mobile.json'); + const rules = versions.map((version) => + createRule( + version, + deprecatedVersions.includes(version) + ? '2026-09-25T00:00:00.000Z' + : null, + ), + ); + const contractWithoutHash = { + formatVersion: 1, + platform: 'mobile' as const, + sourceSha: SOURCE_SHA, + generatedAt: '2026-09-25T00:00:00.000Z', + trackingPlan: { + id: TRACKING_PLAN_ID, + name: 'MetaMask Mobile', + type: 'LIVE', + updatedAt: '2026-09-25T00:00:00.000Z', + }, + ruleCount: rules.length, + rules, + }; + const contract: MobileContract = { + ...contractWithoutHash, + hash: computeSemanticContractHash(contractWithoutHash), + }; + const manifest: ContractManifest = { + formatVersion: 1, + source: 'segment-public-api', + sourceRepository: 'Consensys/segment-schema', + sourceSha: SOURCE_SHA, + generatedAt: '2026-09-25T00:00:00.000Z', + contracts: { + mobile: { + slug: 'metamask-mobile', + trackingPlanId: TRACKING_PLAN_ID, + trackingPlanUpdatedAt: '2026-09-25T00:00:00.000Z', + ruleCount: rules.length, + pageCount: 1, + hash: contract.hash, + }, + }, + }; + + writeFileSync(contractPath, JSON.stringify(contract)); + writeFileSync(manifestPath, JSON.stringify(manifest)); + + return { + root, + manifestPath, + contractPath, + assetSha256: sha256File(contractPath), + manifest, + contract, + }; +}; + +const cleanupFixture = (fixture: ContractFixture): void => { + rmSync(fixture.root, { recursive: true, force: true }); +}; + +describe('verifyMobileContract', () => { + const fixtures: ContractFixture[] = []; + + afterEach(() => { + fixtures.splice(0).forEach(cleanupFixture); + }); + + it('returns both requested Quick Buy versions for a verified Mobile contract', () => { + const fixture = createFixture(); + fixtures.push(fixture); + + const result = verifyMobileContract({ + manifestPath: fixture.manifestPath, + contractPath: fixture.contractPath, + expectedAssetSha256: fixture.assetSha256, + }); + + expect(result.eventRules.map((rule) => rule.version)).toEqual([1, 2]); + expect(result.assetSha256).toBe(fixture.assetSha256); + }); + + it('rejects a manifest hash that differs from the contract', () => { + const fixture = createFixture(); + fixtures.push(fixture); + const mismatchedManifest: ContractManifest = { + ...fixture.manifest, + contracts: { + mobile: { + ...fixture.manifest.contracts.mobile, + hash: `sha256:${'0'.repeat(64)}`, + }, + }, + }; + + writeFileSync(fixture.manifestPath, JSON.stringify(mismatchedManifest)); + + expect(() => + verifyMobileContract({ + manifestPath: fixture.manifestPath, + contractPath: fixture.contractPath, + }), + ).toThrow('does not match contract hash'); + }); + + it('rejects semantic changes without a regenerated contract hash', () => { + const fixture = createFixture(); + fixtures.push(fixture); + const changedContract: MobileContract = { + ...fixture.contract, + rules: fixture.contract.rules.map((rule, index) => + index === 0 ? { ...rule, key: 'Different Event' } : rule, + ), + }; + + writeFileSync(fixture.contractPath, JSON.stringify(changedContract)); + + expect(() => + verifyMobileContract({ + manifestPath: fixture.manifestPath, + contractPath: fixture.contractPath, + }), + ).toThrow('does not match computed hash'); + }); + + it('rejects a missing requested event version', () => { + const fixture = createFixture({ versions: [1] }); + fixtures.push(fixture); + + expect(() => + verifyMobileContract({ + manifestPath: fixture.manifestPath, + contractPath: fixture.contractPath, + }), + ).toThrow('version 2 was not found'); + }); + + it('rejects a deprecated requested event version', () => { + const fixture = createFixture({ deprecatedVersions: [2] }); + fixtures.push(fixture); + + expect(() => + verifyMobileContract({ + manifestPath: fixture.manifestPath, + contractPath: fixture.contractPath, + }), + ).toThrow('version 2 is deprecated'); + }); + + it('rejects an asset digest mismatch', () => { + const fixture = createFixture(); + fixtures.push(fixture); + + expect(() => + verifyMobileContract({ + manifestPath: fixture.manifestPath, + contractPath: fixture.contractPath, + expectedAssetSha256: `sha256:${'0'.repeat(64)}`, + }), + ).toThrow('does not match expected digest'); + }); +}); diff --git a/scripts/analytics/typed-events/contract.ts b/scripts/analytics/typed-events/contract.ts new file mode 100644 index 000000000000..5ecd36496eaa --- /dev/null +++ b/scripts/analytics/typed-events/contract.ts @@ -0,0 +1,503 @@ +import { createHash } from 'node:crypto'; +import { readFileSync } from 'node:fs'; + +export type JsonValue = + | null + | boolean + | number + | string + | readonly JsonValue[] + | JsonObject; + +export interface JsonObject { + readonly [key: string]: JsonValue; +} + +export interface ContractSummary { + readonly slug: string; + readonly trackingPlanId: string; + readonly trackingPlanUpdatedAt: string | null; + readonly ruleCount: number; + readonly pageCount: number; + readonly hash: string; +} + +export interface ContractManifest { + readonly formatVersion: number; + readonly source: string; + readonly sourceRepository: string; + readonly sourceSha: string; + readonly generatedAt: string; + readonly contracts: { + readonly mobile: ContractSummary; + }; +} + +export interface TrackingPlanMetadata { + readonly id: string; + readonly name: string; + readonly type: string; + readonly updatedAt: string | null; +} + +export interface ContractRule { + readonly key: string; + readonly type: string; + readonly version: number; + readonly jsonSchema: JsonObject; + readonly createdAt?: string | null; + readonly updatedAt?: string | null; + readonly deprecatedAt?: string | null; +} + +export interface MobileContract { + readonly formatVersion: number; + readonly platform: 'mobile'; + readonly sourceSha: string; + readonly generatedAt: string; + readonly trackingPlan: TrackingPlanMetadata; + readonly ruleCount: number; + readonly rules: readonly ContractRule[]; + readonly hash: string; +} + +export interface VerifyMobileContractOptions { + readonly manifestPath: string; + readonly contractPath: string; + readonly expectedAssetSha256?: string; + readonly eventName?: string; + readonly versions?: readonly number[]; +} + +export interface VerifiedMobileContract { + readonly manifest: ContractManifest; + readonly contract: MobileContract; + readonly eventRules: readonly ContractRule[]; + readonly assetSha256: string; +} + +const DEFAULT_EVENT_NAME = 'Quick Buy Amount Selected'; +const DEFAULT_VERSIONS = [1, 2] as const; +const SHA256_PATTERN = /^sha256:[0-9a-f]{64}$/; + +const isJsonValue = (value: unknown): value is JsonValue => { + if (value === null) { + return true; + } + + if ( + typeof value === 'boolean' || + typeof value === 'number' || + typeof value === 'string' + ) { + return true; + } + + if (Array.isArray(value)) { + return value.every(isJsonValue); + } + + if (typeof value === 'object') { + return Object.values(value).every(isJsonValue); + } + + return false; +}; + +const isJsonObject = (value: unknown): value is JsonObject => + isJsonValue(value) && + typeof value === 'object' && + value !== null && + !Array.isArray(value); + +const readJsonObject = (filePath: string): JsonObject => { + let parsed: unknown; + + try { + parsed = JSON.parse(readFileSync(filePath, 'utf8')) as unknown; + } catch (error) { + throw new Error(`Unable to parse JSON file '${filePath}': ${String(error)}`); + } + + if (!isJsonObject(parsed)) { + throw new Error(`Expected JSON object in '${filePath}'`); + } + + return parsed; +}; + +const readObject = ( + value: JsonValue | undefined, + path: string, +): JsonObject => { + if (!isJsonObject(value)) { + throw new Error(`Expected object at ${path}`); + } + + return value; +}; + +const readString = ( + object: JsonObject, + key: string, + path: string, +): string => { + const value = object[key]; + + if (typeof value !== 'string') { + throw new Error(`Expected string at ${path}.${key}`); + } + + return value; +}; + +const readInteger = ( + object: JsonObject, + key: string, + path: string, +): number => { + const value = object[key]; + + if (typeof value !== 'number' || !Number.isInteger(value)) { + throw new Error(`Expected integer at ${path}.${key}`); + } + + return value; +}; + +const readNullableString = ( + object: JsonObject, + key: string, + path: string, +): string | null => { + const value = object[key]; + + if (value === null) { + return null; + } + + if (typeof value !== 'string') { + throw new Error(`Expected string or null at ${path}.${key}`); + } + + return value; +}; + +const readOptionalNullableString = ( + object: JsonObject, + key: string, + path: string, +): string | null | undefined => { + if (!(key in object)) { + return undefined; + } + + return readNullableString(object, key, path); +}; + +const readHash = ( + object: JsonObject, + key: string, + path: string, +): string => { + const value = readString(object, key, path); + + if (!SHA256_PATTERN.test(value)) { + throw new Error(`Expected SHA-256 digest at ${path}.${key}`); + } + + return value; +}; + +const parseContractSummary = ( + value: JsonValue | undefined, + path: string, +): ContractSummary => { + const object = readObject(value, path); + + return { + slug: readString(object, 'slug', path), + trackingPlanId: readString(object, 'trackingPlanId', path), + trackingPlanUpdatedAt: readNullableString( + object, + 'trackingPlanUpdatedAt', + path, + ), + ruleCount: readInteger(object, 'ruleCount', path), + pageCount: readInteger(object, 'pageCount', path), + hash: readHash(object, 'hash', path), + }; +}; + +const parseManifest = (value: JsonObject): ContractManifest => { + const contracts = readObject(value.contracts, 'manifest.contracts'); + + return { + formatVersion: readInteger(value, 'formatVersion', 'manifest'), + source: readString(value, 'source', 'manifest'), + sourceRepository: readString( + value, + 'sourceRepository', + 'manifest', + ), + sourceSha: readString(value, 'sourceSha', 'manifest'), + generatedAt: readString(value, 'generatedAt', 'manifest'), + contracts: { + mobile: parseContractSummary( + contracts.mobile, + 'manifest.contracts.mobile', + ), + }, + }; +}; + +const parseRule = (value: JsonValue, index: number): ContractRule => { + const path = `contract.rules[${index}]`; + const object = readObject(value, path); + + return { + key: readString(object, 'key', path), + type: readString(object, 'type', path), + version: readInteger(object, 'version', path), + jsonSchema: readObject(object.jsonSchema, `${path}.jsonSchema`), + createdAt: readOptionalNullableString(object, 'createdAt', path), + updatedAt: readOptionalNullableString(object, 'updatedAt', path), + deprecatedAt: readOptionalNullableString(object, 'deprecatedAt', path), + }; +}; + +const parseContract = (value: JsonObject): MobileContract => { + const platform = readString(value, 'platform', 'contract'); + + if (platform !== 'mobile') { + throw new Error(`Expected contract.platform to be 'mobile', got '${platform}'`); + } + + const rulesValue = value.rules; + + if (!Array.isArray(rulesValue)) { + throw new Error('Expected contract.rules to be an array'); + } + + const trackingPlan = readObject( + value.trackingPlan, + 'contract.trackingPlan', + ); + + return { + formatVersion: readInteger(value, 'formatVersion', 'contract'), + platform, + sourceSha: readString(value, 'sourceSha', 'contract'), + generatedAt: readString(value, 'generatedAt', 'contract'), + trackingPlan: { + id: readString(trackingPlan, 'id', 'contract.trackingPlan'), + name: readString(trackingPlan, 'name', 'contract.trackingPlan'), + type: readString(trackingPlan, 'type', 'contract.trackingPlan'), + updatedAt: readNullableString( + trackingPlan, + 'updatedAt', + 'contract.trackingPlan', + ), + }, + ruleCount: readInteger(value, 'ruleCount', 'contract'), + rules: rulesValue.map(parseRule), + hash: readHash(value, 'hash', 'contract'), + }; +}; + +const sortJsonDeep = (value: JsonValue): JsonValue => { + if (Array.isArray(value)) { + return value.map(sortJsonDeep); + } + + if (isJsonObject(value)) { + return Object.fromEntries( + Object.keys(value) + .sort() + .map((key) => [key, sortJsonDeep(value[key])]), + ); + } + + return value; +}; + +const stableStringify = (value: JsonValue): string => + JSON.stringify(sortJsonDeep(value)); + +const sha256 = (value: string | Buffer): string => + `sha256:${createHash('sha256').update(value).digest('hex')}`; + +/** + * Calculate the SHA-256 digest for a downloaded contract asset. + * + * @param filePath - Path to the downloaded contract asset. + * @returns The digest in the release manifest format. + */ +export const sha256File = (filePath: string): string => + sha256(readFileSync(filePath)); + +/** + * Recompute the semantic hash used by the contract release producer. + * + * @param contract - Contract data without relying on its stored hash. + * @returns The recomputed semantic hash. + */ +export const computeSemanticContractHash = ( + contract: Pick< + MobileContract, + 'formatVersion' | 'platform' | 'trackingPlan' | 'rules' + >, +): string => + sha256( + stableStringify({ + formatVersion: contract.formatVersion, + platform: contract.platform, + trackingPlanId: contract.trackingPlan.id, + rules: contract.rules.map((rule) => ({ + key: rule.key, + type: rule.type, + version: rule.version, + jsonSchema: rule.jsonSchema, + deprecated: Boolean(rule.deprecatedAt), + })), + }), + ); + +const selectEventRules = ( + contract: MobileContract, + eventName: string, + versions: readonly number[], +): readonly ContractRule[] => { + const requestedVersions = [...new Set(versions)]; + + if (requestedVersions.length === 0) { + throw new Error('At least one event version is required'); + } + + return requestedVersions.map((version) => { + const matches = contract.rules.filter( + (rule) => + rule.key === eventName && + rule.type === 'TRACK' && + rule.version === version, + ); + + if (matches.length === 0) { + throw new Error( + `Event '${eventName}' version ${version} was not found in the Mobile contract`, + ); + } + + if (matches.length > 1) { + throw new Error( + `Event '${eventName}' version ${version} appears more than once in the Mobile contract`, + ); + } + + const [rule] = matches; + + if (rule.deprecatedAt) { + throw new Error( + `Event '${eventName}' version ${version} is deprecated in the Mobile contract`, + ); + } + + return rule; + }); +}; + +/** + * Verify a deployed Mobile contract and select required event versions. + * + * @param options - Paths, optional asset digest, and event selection criteria. + * @returns Verified release metadata and the selected event rules. + */ +export const verifyMobileContract = ({ + manifestPath, + contractPath, + expectedAssetSha256, + eventName = DEFAULT_EVENT_NAME, + versions = DEFAULT_VERSIONS, +}: VerifyMobileContractOptions): VerifiedMobileContract => { + const manifest = parseManifest(readJsonObject(manifestPath)); + const contract = parseContract(readJsonObject(contractPath)); + const mobileSummary = manifest.contracts.mobile; + const assetSha256 = sha256File(contractPath); + + if (manifest.formatVersion !== contract.formatVersion) { + throw new Error( + `Manifest and contract format versions differ: ${manifest.formatVersion} vs ${contract.formatVersion}`, + ); + } + + if (manifest.source !== 'segment-public-api') { + throw new Error( + `Expected manifest.source to be 'segment-public-api', got '${manifest.source}'`, + ); + } + + if (manifest.sourceRepository !== 'Consensys/segment-schema') { + throw new Error( + `Unexpected manifest.sourceRepository '${manifest.sourceRepository}'`, + ); + } + + if (contract.sourceSha !== manifest.sourceSha) { + throw new Error( + `Manifest sourceSha '${manifest.sourceSha}' does not match contract sourceSha '${contract.sourceSha}'`, + ); + } + + if (contract.ruleCount !== contract.rules.length) { + throw new Error( + `Contract ruleCount ${contract.ruleCount} does not match ${contract.rules.length} rules`, + ); + } + + if (mobileSummary.ruleCount !== contract.ruleCount) { + throw new Error( + `Manifest Mobile ruleCount ${mobileSummary.ruleCount} does not match contract ruleCount ${contract.ruleCount}`, + ); + } + + if (mobileSummary.trackingPlanId !== contract.trackingPlan.id) { + throw new Error( + `Manifest trackingPlanId '${mobileSummary.trackingPlanId}' does not match contract trackingPlan.id '${contract.trackingPlan.id}'`, + ); + } + + if (mobileSummary.hash !== contract.hash) { + throw new Error( + `Manifest Mobile hash '${mobileSummary.hash}' does not match contract hash '${contract.hash}'`, + ); + } + + const computedSemanticHash = computeSemanticContractHash(contract); + + if (computedSemanticHash !== contract.hash) { + throw new Error( + `Contract semantic hash '${contract.hash}' does not match computed hash '${computedSemanticHash}'`, + ); + } + + if (expectedAssetSha256 !== undefined) { + if (!SHA256_PATTERN.test(expectedAssetSha256)) { + throw new Error( + `Expected asset digest '${expectedAssetSha256}' is not a SHA-256 digest`, + ); + } + + if (expectedAssetSha256 !== assetSha256) { + throw new Error( + `Contract asset digest '${assetSha256}' does not match expected digest '${expectedAssetSha256}'`, + ); + } + } + + return { + manifest, + contract, + eventRules: selectEventRules(contract, eventName, versions), + assetSha256, + }; +}; diff --git a/scripts/analytics/typed-events/generate-pilot.ts b/scripts/analytics/typed-events/generate-pilot.ts new file mode 100644 index 000000000000..77f2027a4559 --- /dev/null +++ b/scripts/analytics/typed-events/generate-pilot.ts @@ -0,0 +1,99 @@ +import { mkdirSync, writeFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { format } from 'prettier'; +import { verifyMobileContract } from './contract'; +import { generateEventPilot } from './generator'; +import { QUICK_BUY_AMOUNT_SELECTED_PILOT } from './quick-buy-pilot'; + +const readOptions = ( + args: readonly string[], +): ReadonlyMap => { + const options = new Map(); + + for (let index = 0; index < args.length; index++) { + const argument = args[index]; + + if (!argument?.startsWith('--')) { + throw new Error(`Unexpected argument '${argument ?? ''}'`); + } + + const value = args[index + 1]; + + if (!value || value.startsWith('--')) { + throw new Error(`Missing value for '${argument}'`); + } + + options.set(argument.slice(2), value); + index++; + } + + return options; +}; + +const readRequiredOption = ( + options: ReadonlyMap, + name: string, +): string => { + const value = options.get(name); + + if (!value) { + throw new Error(`Missing required option '--${name}'`); + } + + return value; +}; + +const main = async (): Promise => { + const options = readOptions(process.argv.slice(2)); + const outputDirectory = readRequiredOption(options, 'output-directory'); + const pilot = QUICK_BUY_AMOUNT_SELECTED_PILOT; + const verifiedContract = verifyMobileContract({ + manifestPath: readRequiredOption(options, 'manifest'), + contractPath: readRequiredOption(options, 'contract'), + expectedAssetSha256: readRequiredOption(options, 'asset-sha256'), + eventName: pilot.eventName, + versions: pilot.versions.map(({ version }) => version), + }); + const generated = generateEventPilot(verifiedContract.eventRules, pilot); + const sourcePath = join(outputDirectory, `${pilot.exportName}.ts`); + const typeTestsPath = join( + outputDirectory, + `${pilot.exportName}.test-d.ts`, + ); + + mkdirSync(outputDirectory, { recursive: true }); + writeFileSync( + sourcePath, + await format(generated.source, { + parser: 'typescript', + singleQuote: true, + }), + ); + writeFileSync( + typeTestsPath, + await format(generated.typeTests, { + parser: 'typescript', + singleQuote: true, + }), + ); + + console.log( + JSON.stringify( + { + event: pilot.eventName, + versions: pilot.versions.map(({ version }) => version), + sourcePath, + typeTestsPath, + }, + null, + 2, + ), + ); +}; + +if (process.argv[1]?.endsWith('generate-pilot.ts')) { + main().catch((error: unknown) => { + console.error(String(error)); + process.exitCode = 1; + }); +} diff --git a/scripts/analytics/typed-events/generator.test.ts b/scripts/analytics/typed-events/generator.test.ts new file mode 100644 index 000000000000..03cc90122d5d --- /dev/null +++ b/scripts/analytics/typed-events/generator.test.ts @@ -0,0 +1,168 @@ +import type { ContractRule, JsonObject } from './contract'; +import { generateEventPilot } from './generator'; +import { + QUICK_BUY_AMOUNT_SELECTED_PILOT, + type EventPilotDefinition, +} from './quick-buy-pilot'; + +const property = ( + type: 'integer' | 'number' | 'string', + enumValues?: readonly (number | string)[], +): JsonObject => ({ + type: [type], + ...(enumValues ? { enum: enumValues } : {}), +}); + +const V1_PROPERTIES: JsonObject = { + amount_selection_method: property('string', ['preset', 'custom_input']), + amount_usd: property('number'), + caip19: property('string'), + pay_with_token: property('string'), + preset_value: property('integer', [20, 50, 100, 250]), + source: property('string', ['leaderboard']), + trader_address: property('string'), +}; + +const V2_PROPERTIES: JsonObject = { + ...V1_PROPERTIES, + amount_selection_method: property('string', [ + 'preset', + 'custom_input', + 'slider', + ]), + chain_name: property('string'), + original_entry_point: property('string', ['leaderboard']), + perps_market: property('string'), + preset_value: property('integer', [10, 50, 100, 250]), + slider_percent: property('integer'), +}; + +const createRule = ( + version: number, + properties: JsonObject, +): ContractRule => ({ + key: 'Quick Buy Amount Selected', + type: 'TRACK', + version, + jsonSchema: { + type: 'object', + properties: { + properties: { + type: 'object', + properties, + required: ['amount_usd', 'amount_selection_method', 'source'], + }, + }, + }, + deprecatedAt: null, +}); + +const createRules = (): readonly ContractRule[] => [ + createRule(1, V1_PROPERTIES), + createRule(2, V2_PROPERTIES), +]; + +describe('generateEventPilot', () => { + it('generates distinct exact v1 and v2 APIs with version context', () => { + const rules = createRules(); + + const result = generateEventPilot( + rules, + QUICK_BUY_AMOUNT_SELECTED_PILOT, + ); + + expect(result.source).toContain( + 'export interface QuickBuyAmountSelectedV1Properties', + ); + expect(result.source).toContain( + 'export interface QuickBuyAmountSelectedV2Properties', + ); + expect(result.source).toContain( + 'readonly amount_selection_method: "preset" | "custom_input";', + ); + expect(result.source).toContain( + 'readonly amount_selection_method: "preset" | "custom_input" | "slider";', + ); + expect(result.source).toContain( + 'Record, never>', + ); + expect(result.source).toContain('event_version: version'); + expect(result.source).toContain( + 'v2: createEventVersion<\n QuickBuyAmountSelectedV2Properties', + ); + }); + + it('generates compile-fail checks for required, unknown, enum, and version errors', () => { + const rules = createRules(); + + const result = generateEventPilot( + rules, + QUICK_BUY_AMOUNT_SELECTED_PILOT, + ); + + expect(result.typeTests).toContain( + '@ts-expect-error source is required', + ); + expect(result.typeTests).toContain( + '@ts-expect-error unknown properties are rejected', + ); + expect(result.typeTests).toContain( + '@ts-expect-error slider is not a v1 amount selection method', + ); + expect(result.typeTests).toContain( + '@ts-expect-error only generated versions are available', + ); + }); + + it('rejects a required contract property omitted by the pilot allowlist', () => { + const rules = createRules(); + const pilot: EventPilotDefinition = { + ...QUICK_BUY_AMOUNT_SELECTED_PILOT, + versions: QUICK_BUY_AMOUNT_SELECTED_PILOT.versions.map((definition) => + definition.version === 2 + ? { + ...definition, + properties: definition.properties.filter( + (name) => name !== 'source', + ), + } + : definition, + ), + }; + + expect(() => generateEventPilot(rules, pilot)).toThrow( + 'allowlist omits required v2 properties: source', + ); + }); + + it('rejects a pilot property missing from the deployed version', () => { + const rules = [ + createRule(1, V1_PROPERTIES), + createRule(2, { + ...V2_PROPERTIES, + slider_percent: undefined, + } as unknown as JsonObject), + ]; + + expect(() => + generateEventPilot(rules, QUICK_BUY_AMOUNT_SELECTED_PILOT), + ).toThrow('Expected object at rule v2.properties.slider_percent'); + }); + + it('rejects schema constraints the narrow generator does not implement', () => { + const rules = [ + createRule(1, V1_PROPERTIES), + createRule(2, { + ...V2_PROPERTIES, + slider_percent: { + type: ['integer'], + minimum: 0, + }, + }), + ]; + + expect(() => + generateEventPilot(rules, QUICK_BUY_AMOUNT_SELECTED_PILOT), + ).toThrow('Unsupported JSON Schema keys'); + }); +}); diff --git a/scripts/analytics/typed-events/generator.ts b/scripts/analytics/typed-events/generator.ts new file mode 100644 index 000000000000..0054c734b82d --- /dev/null +++ b/scripts/analytics/typed-events/generator.ts @@ -0,0 +1,391 @@ +import type { + ContractRule, + JsonObject, + JsonValue, +} from './contract'; +import type { EventPilotDefinition } from './quick-buy-pilot'; + +interface PropertyDefinition { + readonly description?: string; + readonly name: string; + readonly required: boolean; + readonly type: string; +} + +interface VersionDefinition { + readonly properties: readonly PropertyDefinition[]; + readonly version: number; +} + +export interface GeneratedEventFiles { + readonly source: string; + readonly typeTests: string; +} + +const SUPPORTED_PROPERTY_KEYS = new Set(['description', 'enum', 'type']); + +const isJsonObject = (value: JsonValue | undefined): value is JsonObject => + typeof value === 'object' && value !== null && !Array.isArray(value); + +const readObject = ( + value: JsonValue | undefined, + path: string, +): JsonObject => { + if (!isJsonObject(value)) { + throw new Error(`Expected object at ${path}`); + } + + return value; +}; + +const readStringArray = ( + value: JsonValue | undefined, + path: string, +): readonly string[] => { + if (!Array.isArray(value) || !value.every((item) => typeof item === 'string')) { + throw new Error(`Expected string array at ${path}`); + } + + return value; +}; + +const readPropertyType = ( + schema: JsonObject, + path: string, +): 'boolean' | 'integer' | 'number' | 'string' => { + const rawType = schema.type; + const type = + typeof rawType === 'string' + ? rawType + : Array.isArray(rawType) && rawType.length === 1 + ? rawType[0] + : undefined; + + if ( + type !== 'boolean' && + type !== 'integer' && + type !== 'number' && + type !== 'string' + ) { + throw new Error(`Unsupported JSON Schema type at ${path}.type`); + } + + return type; +}; + +const readEnumType = ( + schema: JsonObject, + primitiveType: 'boolean' | 'integer' | 'number' | 'string', + path: string, +): string | undefined => { + const enumValues = schema.enum; + + if (enumValues === undefined) { + return undefined; + } + + if (!Array.isArray(enumValues) || enumValues.length === 0) { + throw new Error(`Expected non-empty enum at ${path}.enum`); + } + + const isExpectedType = (value: JsonValue): boolean => { + switch (primitiveType) { + case 'boolean': + return typeof value === 'boolean'; + case 'integer': + return typeof value === 'number' && Number.isInteger(value); + case 'number': + return typeof value === 'number'; + case 'string': + return typeof value === 'string'; + default: + return false; + } + }; + + if (!enumValues.every(isExpectedType)) { + throw new Error(`Enum values do not match ${primitiveType} at ${path}.enum`); + } + + return enumValues.map((value) => JSON.stringify(value)).join(' | '); +}; + +const toTypeScriptType = (schema: JsonObject, path: string): string => { + const unsupportedKeys = Object.keys(schema).filter( + (key) => !SUPPORTED_PROPERTY_KEYS.has(key), + ); + + if (unsupportedKeys.length > 0) { + throw new Error( + `Unsupported JSON Schema keys at ${path}: ${unsupportedKeys.join(', ')}`, + ); + } + + const primitiveType = readPropertyType(schema, path); + const enumType = readEnumType(schema, primitiveType, path); + + if (enumType) { + return enumType; + } + + return primitiveType === 'integer' ? 'number' : primitiveType; +}; + +const readDescription = (schema: JsonObject, path: string): string | undefined => { + const description = schema.description; + + if (description === undefined) { + return undefined; + } + + if (typeof description !== 'string') { + throw new Error(`Expected string at ${path}.description`); + } + + return description.trim(); +}; + +const readRuleProperties = ( + rule: ContractRule, +): { + readonly properties: JsonObject; + readonly required: readonly string[]; +} => { + const schemaProperties = readObject( + rule.jsonSchema.properties, + `rule v${rule.version}.jsonSchema.properties`, + ); + const eventProperties = readObject( + schemaProperties.properties, + `rule v${rule.version}.jsonSchema.properties.properties`, + ); + + return { + properties: readObject( + eventProperties.properties, + `rule v${rule.version}.properties`, + ), + required: readStringArray( + eventProperties.required, + `rule v${rule.version}.required`, + ), + }; +}; + +const buildVersionDefinition = ( + rule: ContractRule, + propertyAllowlist: readonly string[], +): VersionDefinition => { + const { properties, required } = readRuleProperties(rule); + const omittedRequiredProperties = required.filter( + (property) => !propertyAllowlist.includes(property), + ); + + if (omittedRequiredProperties.length > 0) { + throw new Error( + `Pilot allowlist omits required v${rule.version} properties: ${omittedRequiredProperties.join(', ')}`, + ); + } + + const definitions = [...propertyAllowlist] + .sort() + .map((name): PropertyDefinition => { + const path = `rule v${rule.version}.properties.${name}`; + const schema = readObject(properties[name], path); + + return { + description: readDescription(schema, path), + name, + required: required.includes(name), + type: toTypeScriptType(schema, path), + }; + }); + + return { + properties: definitions, + version: rule.version, + }; +}; + +const escapeComment = (value: string): string => + value.replaceAll('*/', '*\\/').replaceAll('\n', ' '); + +const renderProperty = (property: PropertyDefinition): string => { + const comment = property.description + ? ` /** ${escapeComment(property.description)} */\n` + : ''; + const optional = property.required ? '' : '?'; + + return `${comment} readonly ${property.name}${optional}: ${property.type};`; +}; + +const renderInterface = ( + exportName: string, + definition: VersionDefinition, +): string => `export interface ${exportName}V${definition.version}Properties { +${definition.properties.map(renderProperty).join('\n')} +}`; + +const renderVersion = ( + exportName: string, + definition: VersionDefinition, +): string => ` v${definition.version}: createEventVersion< + ${exportName}V${definition.version}Properties + >(transport, EVENT_NAME, ${definition.version}),`; + +const renderValidProperties = (definition: VersionDefinition): string => + definition.properties + .filter((property) => property.required) + .map((property) => { + const firstType = property.type.split(' | ')[0]; + const value = + firstType.startsWith('"') || + /^-?\d+(?:\.\d+)?$/u.test(firstType) || + firstType === 'true' || + firstType === 'false' + ? firstType + : property.type === 'number' + ? '1' + : property.type === 'boolean' + ? 'true' + : "'value'"; + + return ` ${property.name}: ${value},`; + }) + .join('\n'); + +const renderTypeTests = ( + pilot: EventPilotDefinition, + versions: readonly VersionDefinition[], +): string => { + const [firstVersion, secondVersion] = versions; + + if (!firstVersion || !secondVersion) { + throw new Error('The pilot type test requires two event versions'); + } + + return `import { create${pilot.exportName} } from './${pilot.exportName}'; + +declare const transport: Parameters[0]; + +const event = create${pilot.exportName}(transport); + +event.v${firstVersion.version}.track({ +${renderValidProperties(firstVersion)} +}); + +event.v${secondVersion.version}.track({ +${renderValidProperties(secondVersion)} +}); + +// @ts-expect-error source is required +event.v${secondVersion.version}.track({ + amount_selection_method: 'custom_input', + amount_usd: 1, +}); + +event.v${secondVersion.version}.track({ +${renderValidProperties(secondVersion)} + // @ts-expect-error unknown properties are rejected + unknown_property: true, +}); + +event.v${firstVersion.version}.track({ +${renderValidProperties(firstVersion).replace( + /amount_selection_method: [^,]+,/u, + "// @ts-expect-error slider is not a v1 amount selection method\n amount_selection_method: 'slider',", +)} +}); + +// @ts-expect-error only generated versions are available +event.v3.track({}); +`; +}; + +/** + * Generate a transport-neutral, versioned TypeScript analytics facade. + * + * @param rules - Verified rules selected from the deployed contract. + * @param pilot - Reviewed caller-property boundary for the pilot event. + * @returns Generated implementation and compile-time conformance tests. + */ +export const generateEventPilot = ( + rules: readonly ContractRule[], + pilot: EventPilotDefinition, +): GeneratedEventFiles => { + const versions = pilot.versions.map((versionDefinition) => { + const rule = rules.find( + (candidate) => + candidate.key === pilot.eventName && + candidate.type === 'TRACK' && + candidate.version === versionDefinition.version, + ); + + if (!rule) { + throw new Error( + `Missing verified rule for '${pilot.eventName}' v${versionDefinition.version}`, + ); + } + + return buildVersionDefinition(rule, versionDefinition.properties); + }); + + const source = `/** + * GENERATED FILE — DO NOT EDIT. + * + * Generated from the verified deployed Mobile analytics contract. + */ + +export interface AnalyticsEventContext { + readonly protocols: { + readonly event_version: number; + }; +} + +export interface AnalyticsEventTransport { + track( + eventName: string, + properties: object, + context: AnalyticsEventContext, + ): void; +} + +type ExactProperties = Actual & + Record, never>; + +interface EventVersion { + track( + properties: ExactProperties, + ): void; +} + +const createEventVersion = ( + transport: AnalyticsEventTransport, + eventName: string, + version: number, +): EventVersion => ({ + track: (properties) => { + transport.track(eventName, properties as object, { + protocols: { + event_version: version, + }, + }); + }, +}); + +const EVENT_NAME = ${JSON.stringify(pilot.eventName)}; + +${versions.map((definition) => renderInterface(pilot.exportName, definition)).join('\n\n')} + +export const create${pilot.exportName} = ( + transport: AnalyticsEventTransport, +) => ({ +${versions.map((definition) => renderVersion(pilot.exportName, definition)).join('\n')} +}); +`; + + return { + source, + typeTests: renderTypeTests(pilot, versions), + }; +}; diff --git a/scripts/analytics/typed-events/quick-buy-pilot.ts b/scripts/analytics/typed-events/quick-buy-pilot.ts new file mode 100644 index 000000000000..ea6d06e90fd4 --- /dev/null +++ b/scripts/analytics/typed-events/quick-buy-pilot.ts @@ -0,0 +1,58 @@ +export interface EventVersionDefinition { + readonly version: number; + readonly properties: readonly string[]; +} + +export interface EventPilotDefinition { + readonly eventName: string; + readonly exportName: string; + readonly versions: readonly EventVersionDefinition[]; +} + +/** + * Explicit caller-owned property boundary for the first typed analytics pilot. + * + * The deployed Segment contract flattens event properties together with + * automatically collected SDK and destination properties. Until the contract + * exposes property provenance, this reviewed allowlist prevents generated + * callers from setting those global properties. + * + * This is intentionally a temporary, narrow pilot configuration rather than + * the final all-events registry. A future generic generator can derive event + * definitions from the contract, while explicit client policy can remain + * where caller ownership cannot be inferred safely. + */ +export const QUICK_BUY_AMOUNT_SELECTED_PILOT: EventPilotDefinition = { + eventName: 'Quick Buy Amount Selected', + exportName: 'QuickBuyAmountSelected', + versions: [ + { + version: 1, + properties: [ + 'amount_selection_method', + 'amount_usd', + 'caip19', + 'pay_with_token', + 'preset_value', + 'source', + 'trader_address', + ], + }, + { + version: 2, + properties: [ + 'amount_selection_method', + 'amount_usd', + 'caip19', + 'chain_name', + 'original_entry_point', + 'pay_with_token', + 'perps_market', + 'preset_value', + 'slider_percent', + 'source', + 'trader_address', + ], + }, + ], +}; diff --git a/scripts/analytics/typed-events/typewriter-comparison.test.ts b/scripts/analytics/typed-events/typewriter-comparison.test.ts new file mode 100644 index 000000000000..f9093a791e08 --- /dev/null +++ b/scripts/analytics/typed-events/typewriter-comparison.test.ts @@ -0,0 +1,81 @@ +import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { basename, join } from 'node:path'; +import type { ContractRule, MobileContract } from './contract'; +import { + readTypewriterOutput, + writeTypewriterFixture, +} from './typewriter-comparison'; + +const createContract = (): MobileContract => ({ + formatVersion: 1, + generatedAt: '2026-09-25T00:00:00.000Z', + hash: `sha256:${'a'.repeat(64)}`, + platform: 'mobile', + ruleCount: 2, + rules: [], + sourceSha: 'a'.repeat(40), + trackingPlan: { + id: 'tp_test', + name: 'MetaMask Mobile', + type: 'LIVE', + updatedAt: '2026-09-25T00:00:00.000Z', + }, +}); + +const createRule = (version: number): ContractRule => ({ + key: 'Quick Buy Amount Selected', + type: 'TRACK', + version, + jsonSchema: { + type: 'object', + properties: {}, + }, + deprecatedAt: null, +}); + +describe('Typewriter comparison fixture', () => { + const roots: string[] = []; + + afterEach(() => { + roots.splice(0).forEach((root) => rmSync(root, { recursive: true, force: true })); + }); + + it('writes a local React Native Typewriter fixture with both versions', () => { + const root = mkdtempSync(join(tmpdir(), 'analytics-typewriter-')); + roots.push(root); + const paths = writeTypewriterFixture( + createContract(), + [createRule(1), createRule(2)], + root, + ); + + expect(paths.configPath).toContain('typewriter.yml'); + expect(paths.generatedDirectory).toContain('/generated'); + expect(paths.planPath).toContain('/generated/plan.json'); + expect(readFileSync(paths.configPath, 'utf8')).toContain( + 'sdk: analytics-react-native', + ); + expect( + (JSON.parse(readFileSync(paths.planPath, 'utf8')) as { rules: unknown[] }) + .rules, + ).toHaveLength(2); + }); + + it('reads TypeScript and TSX Typewriter output deterministically', () => { + const root = mkdtempSync(join(tmpdir(), 'analytics-typewriter-')); + roots.push(root); + writeFileSync(join(root, 'z.ts'), 'export const z = true;'); + writeFileSync(join(root, 'a.tsx'), 'export const a = true;'); + + const result = readTypewriterOutput(root); + + expect(result.files.map((filePath) => basename(filePath))).toEqual([ + 'a.tsx', + 'z.ts', + ]); + expect(result.source).toBe( + 'export const a = true;\nexport const z = true;', + ); + }); +}); diff --git a/scripts/analytics/typed-events/typewriter-comparison.ts b/scripts/analytics/typed-events/typewriter-comparison.ts new file mode 100644 index 000000000000..616ec63130c1 --- /dev/null +++ b/scripts/analytics/typed-events/typewriter-comparison.ts @@ -0,0 +1,112 @@ +import { + mkdirSync, + readFileSync, + readdirSync, + writeFileSync, +} from 'node:fs'; +import { join } from 'node:path'; +import type { ContractRule, MobileContract } from './contract'; + +export interface TypewriterFixturePaths { + readonly configPath: string; + readonly generatedDirectory: string; + readonly planPath: string; +} + +export interface TypewriterOutput { + readonly files: readonly string[]; + readonly source: string; +} + +interface TypewriterPlan { + readonly createdAt: string; + readonly description: string; + readonly id: string; + readonly name: string; + readonly rules: readonly ContractRule[]; + readonly slug: string; + readonly updatedAt: string | null; +} + +const readFileContents = (filePath: string): string => + readFileSync(filePath, 'utf8'); + +const TYPEWRITER_CONFIG = (trackingPlanId: string): string => `client: + language: typescript + sdk: analytics-react-native + languageOptions: {} +trackingPlans: + - id: ${JSON.stringify(trackingPlanId)} + path: ./generated +`; + +const createTypewriterPlan = ( + contract: MobileContract, + rules: readonly ContractRule[], +): TypewriterPlan => ({ + createdAt: contract.generatedAt, + description: '', + id: contract.trackingPlan.id, + name: contract.trackingPlan.name, + rules, + slug: 'metamask-mobile', + updatedAt: contract.trackingPlan.updatedAt, +}); + +/** + * Write the local Typewriter inputs for one verified contract fixture. + * + * @param contract - Verified Mobile contract. + * @param rules - The exact event/version fixture being compared. + * @param directory - Temporary Typewriter workspace. + * @returns Paths consumed by the Typewriter CLI. + */ +export const writeTypewriterFixture = ( + contract: MobileContract, + rules: readonly ContractRule[], + directory: string, +): TypewriterFixturePaths => { + const generatedDirectory = join(directory, 'generated'); + const configPath = join(directory, 'typewriter.yml'); + const planPath = join(generatedDirectory, 'plan.json'); + + mkdirSync(generatedDirectory, { recursive: true }); + writeFileSync( + configPath, + TYPEWRITER_CONFIG(contract.trackingPlan.id), + ); + writeFileSync( + planPath, + `${JSON.stringify(createTypewriterPlan(contract, rules), null, 2)}\n`, + ); + + return { + configPath, + generatedDirectory, + planPath, + }; +}; + +/** + * Read TypeScript files generated by Typewriter. + * + * @param directory - Typewriter output directory. + * @returns Generated file paths and concatenated source. + */ +export const readTypewriterOutput = (directory: string): TypewriterOutput => { + const files = readdirSync(directory) + .filter( + (fileName) => fileName.endsWith('.ts') || fileName.endsWith('.tsx'), + ) + .sort() + .map((fileName) => join(directory, fileName)); + + if (files.length === 0) { + throw new Error(`Typewriter generated no TypeScript files in '${directory}'`); + } + + return { + files, + source: files.map((filePath) => readFileContents(filePath)).join('\n'), + }; +}; diff --git a/scripts/analytics/typed-events/verify-contract.ts b/scripts/analytics/typed-events/verify-contract.ts new file mode 100644 index 000000000000..9b35128a7fd3 --- /dev/null +++ b/scripts/analytics/typed-events/verify-contract.ts @@ -0,0 +1,98 @@ +import { + verifyMobileContract, + type VerifiedMobileContract, +} from './contract'; + +const readOptions = ( + args: readonly string[], +): ReadonlyMap => { + const options = new Map(); + + for (let index = 0; index < args.length; index++) { + const argument = args[index]; + + if (!argument?.startsWith('--')) { + throw new Error(`Unexpected argument '${argument ?? ''}'`); + } + + const value = args[index + 1]; + + if (!value || value.startsWith('--')) { + throw new Error(`Missing value for '${argument}'`); + } + + options.set(argument.slice(2), value); + index++; + } + + return options; +}; + +const readRequiredOption = ( + options: ReadonlyMap, + name: string, +): string => { + const value = options.get(name); + + if (!value) { + throw new Error(`Missing required option '--${name}'`); + } + + return value; +}; + +const readVersions = (value: string | undefined): readonly number[] => { + if (!value) { + return [1, 2]; + } + + const versions = value.split(',').map((version) => Number(version)); + + if ( + versions.some( + (version) => !Number.isInteger(version) || version < 1, + ) + ) { + throw new Error( + `Expected comma-separated positive integer versions, got '${value}'`, + ); + } + + return versions; +}; + +const summarize = ( + result: VerifiedMobileContract, +): Record => ({ + sourceSha: result.manifest.sourceSha, + trackingPlanId: result.contract.trackingPlan.id, + ruleCount: result.contract.ruleCount, + contractHash: result.contract.hash, + assetSha256: result.assetSha256, + event: { + name: result.eventRules[0]?.key, + versions: result.eventRules.map((rule) => rule.version), + }, +}); + +const main = (): void => { + const options = readOptions(process.argv.slice(2)); + const result = verifyMobileContract({ + manifestPath: readRequiredOption(options, 'manifest'), + contractPath: readRequiredOption(options, 'contract'), + expectedAssetSha256: options.get('asset-sha256'), + eventName: options.get('event'), + versions: readVersions(options.get('versions')), + }); + + console.log(JSON.stringify(summarize(result), null, 2)); +}; + +if (process.argv[1]?.endsWith('verify-contract.ts')) { + try { + main(); + } catch (error) { + console.error(String(error)); + process.exitCode = 1; + } +} diff --git a/yarn.lock b/yarn.lock index b83fb8b0ef4e..d9ce1bfb2679 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7695,6 +7695,21 @@ __metadata: languageName: node linkType: hard +"@inquirer/external-editor@npm:^1.0.0": + version: 1.0.3 + resolution: "@inquirer/external-editor@npm:1.0.3" + dependencies: + chardet: "npm:^2.1.1" + iconv-lite: "npm:^0.7.0" + peerDependencies: + "@types/node": ">=18" + peerDependenciesMeta: + "@types/node": + optional: true + checksum: 10/c95d7237a885b32031715089f92820525731d4d3c2bd7afdb826307dc296cc2b39e7a644b0bb265441963348cca42e7785feb29c3aaf18fd2b63131769bf6587 + languageName: node + linkType: hard + "@inversifyjs/common@npm:1.5.0": version: 1.5.0 resolution: "@inversifyjs/common@npm:1.5.0" @@ -9002,6 +9017,22 @@ __metadata: languageName: node linkType: hard +"@lukeed/csprng@npm:^1.1.0": + version: 1.1.0 + resolution: "@lukeed/csprng@npm:1.1.0" + checksum: 10/926f5f7fc629470ca9a8af355bfcd0271d34535f7be3890f69902432bddc3262029bb5dbe9025542cf6c9883d878692eef2815fc2f3ba5b92e9da1f9eba2e51b + languageName: node + linkType: hard + +"@lukeed/uuid@npm:^2.0.0": + version: 2.0.1 + resolution: "@lukeed/uuid@npm:2.0.1" + dependencies: + "@lukeed/csprng": "npm:^1.1.0" + checksum: 10/f5e71e4da852dbff49b93cad27d5a2f61c2241e307bbe89b3b54b889ecb7927f2487246467f90ebb6cbdb7e0ac2a213e2e58b1182cb7990cef6e049aa7c39e7b + languageName: node + linkType: hard + "@mantine/core@npm:^7.8.0": version: 7.8.0 resolution: "@mantine/core@npm:7.8.0" @@ -9038,6 +9069,16 @@ __metadata: languageName: node linkType: hard +"@mark.probst/unicode-properties@npm:~1.1.0": + version: 1.1.0 + resolution: "@mark.probst/unicode-properties@npm:1.1.0" + dependencies: + brfs: "npm:^1.4.0" + unicode-trie: "npm:^0.3.0" + checksum: 10/ea42bd3c17d72ddafff34009ff08e7478e89d1b7b24b06ac18ec0c1d70f62889b0fe255695427b9f2251c2a73e19b0f091eda3af44cef12c50d443e009849e7c + languageName: node + linkType: hard + "@mdx-js/mdx@npm:^3.0.0": version: 3.1.1 resolution: "@mdx-js/mdx@npm:3.1.1" @@ -13030,6 +13071,13 @@ __metadata: languageName: node linkType: hard +"@npmcli/ci-detect@npm:^2.0.0": + version: 2.0.0 + resolution: "@npmcli/ci-detect@npm:2.0.0" + checksum: 10/26e964eca908706c1a612915cbc5614860ac7dbfacbb07870396c82b1377794f123a7aaa821c4a68575b67ff7e3ad170e296d3aa6a5e03dbab9b3f1e61491812 + languageName: node + linkType: hard + "@npmcli/fs@npm:^3.1.0": version: 3.1.0 resolution: "@npmcli/fs@npm:3.1.0" @@ -13108,7 +13156,7 @@ __metadata: languageName: node linkType: hard -"@oclif/core@npm:^1.26.2": +"@oclif/core@npm:^1.26.2, @oclif/core@npm:^1.7": version: 1.26.2 resolution: "@oclif/core@npm:1.26.2" dependencies: @@ -15897,6 +15945,41 @@ __metadata: languageName: node linkType: hard +"@segment/analytics-core@npm:1.4.1": + version: 1.4.1 + resolution: "@segment/analytics-core@npm:1.4.1" + dependencies: + "@lukeed/uuid": "npm:^2.0.0" + "@segment/analytics-generic-utils": "npm:1.1.1" + dset: "npm:^3.1.2" + tslib: "npm:^2.4.1" + checksum: 10/15b2279a48ee10be30d5ae164d338b882c75e031d55472eab22debaa6ff7f55df0d38f10197b760c1cbafec97e243400fec50ecf21587a88170c94346b2ed397 + languageName: node + linkType: hard + +"@segment/analytics-generic-utils@npm:1.1.1": + version: 1.1.1 + resolution: "@segment/analytics-generic-utils@npm:1.1.1" + dependencies: + tslib: "npm:^2.4.1" + checksum: 10/994181cfd2cb9492ac2bcf51b6347a4f2cc5ea5c96522f39d3a431efcbe9f6c1912171e1d7153d086f614703fac5e2026226c07cec8965d60678c9e984501e82 + languageName: node + linkType: hard + +"@segment/analytics-node@npm:^1.0.0-beta.26": + version: 1.3.0 + resolution: "@segment/analytics-node@npm:1.3.0" + dependencies: + "@lukeed/uuid": "npm:^2.0.0" + "@segment/analytics-core": "npm:1.4.1" + "@segment/analytics-generic-utils": "npm:1.1.1" + buffer: "npm:^6.0.3" + node-fetch: "npm:^2.6.7" + tslib: "npm:^2.4.1" + checksum: 10/438f5c029355314ece4373399e338849afc8df74fea96399737d4a0e367295b3880b394338441216ac791777b72fdfa9839d12194a09839cc10b260a4c4aab9c + languageName: node + linkType: hard + "@segment/analytics-react-native@npm:2.23.0": version: 2.23.0 resolution: "@segment/analytics-react-native@npm:2.23.0" @@ -16460,7 +16543,14 @@ __metadata: languageName: node linkType: hard -"@sindresorhus/is@npm:^4.6.0": +"@sindresorhus/is@npm:^0.14.0": + version: 0.14.0 + resolution: "@sindresorhus/is@npm:0.14.0" + checksum: 10/789cd128f0b43e158e657c4505539c8997905fcb5c06d750b7df778cab2b6887bc1eb8878026a20d84524528786ef69fc3d12a964ae56a478a87bcfc7f8272f3 + languageName: node + linkType: hard + +"@sindresorhus/is@npm:^4.0.0, @sindresorhus/is@npm:^4.6.0": version: 4.6.0 resolution: "@sindresorhus/is@npm:4.6.0" checksum: 10/e7f36ed72abfcd5e0355f7423a72918b9748bb1ef370a59f3e5ad8d40b728b85d63b272f65f63eec1faf417cda89dcb0aeebe94015647b6054659c1442fe5ce0 @@ -20373,6 +20463,24 @@ __metadata: languageName: node linkType: hard +"@szmarczak/http-timer@npm:^1.1.2": + version: 1.1.2 + resolution: "@szmarczak/http-timer@npm:1.1.2" + dependencies: + defer-to-connect: "npm:^1.0.1" + checksum: 10/9b63853bd53bff72c4990ebc9cd3f625bbab757247099af172564da6649a27a1d41b1a70cd849dd65b2a078300029c1c80bf3079e6a91e285da7b259eb147146 + languageName: node + linkType: hard + +"@szmarczak/http-timer@npm:^4.0.5": + version: 4.0.6 + resolution: "@szmarczak/http-timer@npm:4.0.6" + dependencies: + defer-to-connect: "npm:^2.0.0" + checksum: 10/c29df3bcec6fc3bdec2b17981d89d9c9fc9bd7d0c9bcfe92821dc533f4440bc890ccde79971838b4ceed1921d456973c4180d7175ee1d0023ad0562240a58d95 + languageName: node + linkType: hard + "@szmarczak/http-timer@npm:^5.0.1": version: 5.0.1 resolution: "@szmarczak/http-timer@npm:5.0.1" @@ -21242,6 +21350,18 @@ __metadata: languageName: node linkType: hard +"@types/cacheable-request@npm:^6.0.1": + version: 6.0.3 + resolution: "@types/cacheable-request@npm:6.0.3" + dependencies: + "@types/http-cache-semantics": "npm:*" + "@types/keyv": "npm:^3.1.4" + "@types/node": "npm:*" + "@types/responselike": "npm:^1.0.0" + checksum: 10/159f9fdb2a1b7175eef453ae2ced5ea04c0d2b9610cc9ccd9f9abb066d36dacb1f37acd879ace10ad7cbb649490723feb396fb7307004c9670be29636304b988 + languageName: node + linkType: hard + "@types/cli-progress@npm:^3.11.0": version: 3.11.6 resolution: "@types/cli-progress@npm:3.11.6" @@ -21527,7 +21647,7 @@ __metadata: languageName: node linkType: hard -"@types/http-cache-semantics@npm:^4.0.2": +"@types/http-cache-semantics@npm:*, @types/http-cache-semantics@npm:^4.0.2": version: 4.2.0 resolution: "@types/http-cache-semantics@npm:4.2.0" checksum: 10/01ea0dc9c1852267f5f9a0190846ac158707994b8e325bca190385ec97aa773d4a99cd333e22e838bde3c9aba59e2b5a6ac399b494c203587c6d8f28d21d6326 @@ -21645,6 +21765,15 @@ __metadata: languageName: node linkType: hard +"@types/keyv@npm:^3.1.1, @types/keyv@npm:^3.1.4": + version: 3.1.4 + resolution: "@types/keyv@npm:3.1.4" + dependencies: + "@types/node": "npm:*" + checksum: 10/e009a2bfb50e90ca9b7c6e8f648f8464067271fd99116f881073fa6fa76dc8d0133181dd65e6614d5fb1220d671d67b0124aef7d97dc02d7e342ab143a47779d + languageName: node + linkType: hard + "@types/koa-compose@npm:*": version: 3.2.8 resolution: "@types/koa-compose@npm:3.2.8" @@ -22019,6 +22148,15 @@ __metadata: languageName: node linkType: hard +"@types/responselike@npm:^1.0.0": + version: 1.0.3 + resolution: "@types/responselike@npm:1.0.3" + dependencies: + "@types/node": "npm:*" + checksum: 10/6ac4b35723429b11b117e813c7acc42c3af8b5554caaf1fc750404c1ae59f9b7376bc69b9e9e194a5a97357a597c2228b7173d317320f0360d617b6425212f58 + languageName: node + linkType: hard + "@types/retry@npm:0.12.2": version: 0.12.2 resolution: "@types/retry@npm:0.12.2" @@ -23915,6 +24053,15 @@ __metadata: languageName: node linkType: hard +"acorn@npm:^7.1.1": + version: 7.4.1 + resolution: "acorn@npm:7.4.1" + bin: + acorn: bin/acorn + checksum: 10/8be2a40714756d713dfb62544128adce3b7102c6eb94bc312af196c2cc4af76e5b93079bd66b05e9ca31b35a9b0ce12171d16bc55f366cafdb794fdab9d753ec + languageName: node + linkType: hard + "acorn@npm:^8.0.0, acorn@npm:^8.0.4, acorn@npm:^8.1.0, acorn@npm:^8.10.0, acorn@npm:^8.11.0, acorn@npm:^8.15.0, acorn@npm:^8.16.0, acorn@npm:^8.4.1, acorn@npm:^8.8.1, acorn@npm:^8.9.0": version: 8.18.0 resolution: "acorn@npm:8.18.0" @@ -24093,7 +24240,7 @@ __metadata: languageName: node linkType: hard -"ajv@npm:8.20.0, ajv@npm:^8.0.0, ajv@npm:^8.17.1, ajv@npm:^8.9.0": +"ajv@npm:8.20.0, ajv@npm:^8.0.0, ajv@npm:^8.11.0, ajv@npm:^8.17.1, ajv@npm:^8.9.0": version: 8.20.0 resolution: "ajv@npm:8.20.0" dependencies: @@ -26221,6 +26368,20 @@ __metadata: languageName: node linkType: hard +"brfs@npm:^1.4.0": + version: 1.6.1 + resolution: "brfs@npm:1.6.1" + dependencies: + quote-stream: "npm:^1.0.1" + resolve: "npm:^1.1.5" + static-module: "npm:^2.2.0" + through2: "npm:^2.0.0" + bin: + brfs: bin/cmd.js + checksum: 10/5c4458714851aaffbe2aa30d50f4bdb1da8fd2d69a92abe99d2aa3140e758b1dc62932edf6f89f0dae4b8df60af72917fdd2c19c0e866eb6c53f860cb40dd766 + languageName: node + linkType: hard + "brorand@npm:^1.0.1, brorand@npm:^1.1.0": version: 1.1.0 resolution: "brorand@npm:1.1.0" @@ -26242,6 +26403,13 @@ __metadata: languageName: node linkType: hard +"browser-or-node@npm:^1.2.1": + version: 1.3.0 + resolution: "browser-or-node@npm:1.3.0" + checksum: 10/14a7e3f7bd2dfeac0d1e8fed378a22c7e3c943c30e84ce09ba0636c82f79f78d321536fd2846dd505b6d7ee6fb0fdb8d7f084afe40f2378eee5533cb8e7cd456 + languageName: node + linkType: hard + "browser-resolve@npm:^2.0.0": version: 2.0.0 resolution: "browser-resolve@npm:2.0.0" @@ -26427,6 +26595,13 @@ __metadata: languageName: node linkType: hard +"buffer-equal@npm:0.0.1": + version: 0.0.1 + resolution: "buffer-equal@npm:0.0.1" + checksum: 10/ca4b52e6c01143529d957a78cb9a93e4257f172bbab30d9eb87c20ae085ed23c5e07f236ac051202dacbf3d17aba42e1455f84cba21ea79b67d57f2b05e9a613 + languageName: node + linkType: hard + "buffer-from@npm:^1.0.0": version: 1.1.1 resolution: "buffer-from@npm:1.1.1" @@ -26628,6 +26803,13 @@ __metadata: languageName: node linkType: hard +"cacheable-lookup@npm:^5.0.3": + version: 5.0.4 + resolution: "cacheable-lookup@npm:5.0.4" + checksum: 10/618a8b3eea314060e74cb3285a6154e8343c244a34235acf91cfe626ee0705c24e3cd11e4b1a7b3900bd749ee203ae65afe13adf610c8ab173e99d4a208faf75 + languageName: node + linkType: hard + "cacheable-lookup@npm:^6.0.0": version: 6.1.0 resolution: "cacheable-lookup@npm:6.1.0" @@ -26657,6 +26839,36 @@ __metadata: languageName: node linkType: hard +"cacheable-request@npm:^6.0.0": + version: 6.1.0 + resolution: "cacheable-request@npm:6.1.0" + dependencies: + clone-response: "npm:^1.0.2" + get-stream: "npm:^5.1.0" + http-cache-semantics: "npm:^4.0.0" + keyv: "npm:^3.0.0" + lowercase-keys: "npm:^2.0.0" + normalize-url: "npm:^4.1.0" + responselike: "npm:^1.0.2" + checksum: 10/804f6c377ce6fef31c584babde31d55c69305569058ad95c24a41bb7b33d0ea188d388467a9da6cb340e95a3a1f8a94e1f3a709fef5eaf9c6b88e62448fa29be + languageName: node + linkType: hard + +"cacheable-request@npm:^7.0.2": + version: 7.0.4 + resolution: "cacheable-request@npm:7.0.4" + dependencies: + clone-response: "npm:^1.0.2" + get-stream: "npm:^5.1.0" + http-cache-semantics: "npm:^4.0.0" + keyv: "npm:^4.0.0" + lowercase-keys: "npm:^2.0.0" + normalize-url: "npm:^6.0.1" + responselike: "npm:^2.0.0" + checksum: 10/0f4f2001260ecca78b9f64fc8245e6b5a5dcde24ea53006daab71f5e0e1338095aa1512ec099c4f9895a9e5acfac9da423cb7c079e131485891e9214aca46c41 + languageName: node + linkType: hard + "caching-transform@npm:^4.0.0": version: 4.0.0 resolution: "caching-transform@npm:4.0.0" @@ -26845,7 +27057,7 @@ __metadata: languageName: node linkType: hard -"chalk@npm:4.1.2, chalk@npm:^4.0.0, chalk@npm:^4.0.2, chalk@npm:^4.1.0, chalk@npm:^4.1.2": +"chalk@npm:4.1.2, chalk@npm:^4, chalk@npm:^4.0.0, chalk@npm:^4.0.2, chalk@npm:^4.1.0, chalk@npm:^4.1.1, chalk@npm:^4.1.2": version: 4.1.2 resolution: "chalk@npm:4.1.2" dependencies: @@ -26922,6 +27134,13 @@ __metadata: languageName: node linkType: hard +"chardet@npm:^2.1.1": + version: 2.2.0 + resolution: "chardet@npm:2.2.0" + checksum: 10/78d1e5ba7bdc7db23511a194eedca805918df90a511f9c57f9cc7c1fc03702ed4ad053e485a766ace930362869269c4a731feaf460ae24bd3ed7d190cde8087d + languageName: node + linkType: hard + "charenc@npm:0.0.2": version: 0.0.2 resolution: "charenc@npm:0.0.2" @@ -27230,6 +27449,13 @@ __metadata: languageName: node linkType: hard +"cli-width@npm:^3.0.0": + version: 3.0.0 + resolution: "cli-width@npm:3.0.0" + checksum: 10/8730848b04fb189666ab037a35888d191c8f05b630b1d770b0b0e4c920b47bb5cc14bddf6b8ffe5bfc66cee97c8211d4d18e756c1ffcc75d7dbe7e1186cd7826 + languageName: node + linkType: hard + "clipboardy@npm:^3.0.0": version: 3.0.0 resolution: "clipboardy@npm:3.0.0" @@ -27308,6 +27534,15 @@ __metadata: languageName: node linkType: hard +"clone-response@npm:^1.0.2": + version: 1.0.3 + resolution: "clone-response@npm:1.0.3" + dependencies: + mimic-response: "npm:^1.0.0" + checksum: 10/4e671cac39b11c60aa8ba0a450657194a5d6504df51bca3fac5b3bd0145c4f8e8464898f87c8406b83232e3bc5cca555f51c1f9c8ac023969ebfbf7f6bdabb2e + languageName: node + linkType: hard + "clone@npm:^1.0.2": version: 1.0.4 resolution: "clone@npm:1.0.4" @@ -27385,6 +27620,13 @@ __metadata: languageName: node linkType: hard +"collection-utils@npm:^1.0.1": + version: 1.0.1 + resolution: "collection-utils@npm:1.0.1" + checksum: 10/264d67a9eb187fbbf15f13b87d27a0f3b8df7ff432a70a7d3f6116bb00fa30116c23a450dda622803aedb276fedfbcf09b35429ef16244b1ba075eeb138e2a5e + languageName: node + linkType: hard + "color-convert@npm:^1.9.0, color-convert@npm:^1.9.1": version: 1.9.3 resolution: "color-convert@npm:1.9.3" @@ -27763,7 +28005,7 @@ __metadata: languageName: node linkType: hard -"concat-stream@npm:^1.4.4": +"concat-stream@npm:^1.4.4, concat-stream@npm:~1.6.0": version: 1.6.2 resolution: "concat-stream@npm:1.6.2" dependencies: @@ -27939,7 +28181,7 @@ __metadata: languageName: node linkType: hard -"convert-source-map@npm:^1.6.0, convert-source-map@npm:^1.7.0": +"convert-source-map@npm:^1.5.1, convert-source-map@npm:^1.6.0, convert-source-map@npm:^1.7.0": version: 1.9.0 resolution: "convert-source-map@npm:1.9.0" checksum: 10/dc55a1f28ddd0e9485ef13565f8f756b342f9a46c4ae18b843fe3c30c675d058d6a4823eff86d472f187b176f0adf51ea7b69ea38be34be4a63cbbf91b0593c8 @@ -28983,6 +29225,15 @@ __metadata: languageName: node linkType: hard +"decompress-response@npm:^3.3.0": + version: 3.3.0 + resolution: "decompress-response@npm:3.3.0" + dependencies: + mimic-response: "npm:^1.0.0" + checksum: 10/952552ac3bd7de2fc18015086b09468645c9638d98a551305e485230ada278c039c91116e946d07894b39ee53c0f0d5b6473f25a224029344354513b412d7380 + languageName: node + linkType: hard + "decompress-response@npm:^6.0.0": version: 6.0.0 resolution: "decompress-response@npm:6.0.0" @@ -29079,6 +29330,13 @@ __metadata: languageName: node linkType: hard +"deep-is@npm:~0.1.3": + version: 0.1.4 + resolution: "deep-is@npm:0.1.4" + checksum: 10/ec12d074aef5ae5e81fa470b9317c313142c9e8e2afe3f8efa124db309720db96d1d222b82b84c834e5f87e7a614b44a4684b6683583118b87c833b3be40d4d8 + languageName: node + linkType: hard + "deepmerge-ts@npm:^7.0.3": version: 7.1.5 resolution: "deepmerge-ts@npm:7.1.5" @@ -29128,7 +29386,14 @@ __metadata: languageName: node linkType: hard -"defer-to-connect@npm:^2.0.1": +"defer-to-connect@npm:^1.0.1": + version: 1.1.3 + resolution: "defer-to-connect@npm:1.1.3" + checksum: 10/9491b301dcfa04956f989481ba7a43c2231044206269eb4ab64a52d6639ee15b1252262a789eb4239fb46ab63e44d4e408641bae8e0793d640aee55398cb3930 + languageName: node + linkType: hard + +"defer-to-connect@npm:^2.0.0, defer-to-connect@npm:^2.0.1": version: 2.0.1 resolution: "defer-to-connect@npm:2.0.1" checksum: 10/8a9b50d2f25446c0bfefb55a48e90afd58f85b21bcf78e9207cd7b804354f6409032a1705c2491686e202e64fc05f147aa5aa45f9aa82627563f045937f5791b @@ -29754,7 +30019,7 @@ __metadata: languageName: node linkType: hard -"dset@npm:^3.1.1": +"dset@npm:^3.1.1, dset@npm:^3.1.2": version: 3.1.4 resolution: "dset@npm:3.1.4" checksum: 10/6268c9e2049c8effe6e5a1952f02826e8e32468b5ced781f15f8f3b1c290da37626246fec014fbdd1503413f981dff6abd8a4c718ec9952fd45fccb6ac9de43f @@ -29791,6 +30056,13 @@ __metadata: languageName: node linkType: hard +"duplexer3@npm:^0.1.4": + version: 0.1.5 + resolution: "duplexer3@npm:0.1.5" + checksum: 10/e677cb4c48f031ca728601d6a20bf6aed4c629d69ef9643cb89c67583d673c4ec9317cc6427501f38bd8c368d3a18f173987cc02bd99d8cf8fe3d94259a22a20 + languageName: node + linkType: hard + "duplexer@npm:^0.1.2, duplexer@npm:~0.1.1": version: 0.1.2 resolution: "duplexer@npm:0.1.2" @@ -30117,7 +30389,7 @@ __metadata: languageName: node linkType: hard -"encoding@npm:^0.1.13": +"encoding@npm:^0.1.11, encoding@npm:^0.1.13": version: 0.1.13 resolution: "encoding@npm:0.1.13" dependencies: @@ -30912,6 +31184,25 @@ __metadata: languageName: node linkType: hard +"escodegen@npm:~1.9.0": + version: 1.9.1 + resolution: "escodegen@npm:1.9.1" + dependencies: + esprima: "npm:^3.1.3" + estraverse: "npm:^4.2.0" + esutils: "npm:^2.0.2" + optionator: "npm:^0.8.1" + source-map: "npm:~0.6.1" + dependenciesMeta: + source-map: + optional: true + bin: + escodegen: ./bin/escodegen.js + esgenerate: ./bin/esgenerate.js + checksum: 10/546be0474a0ae10f0adef82a1553cce183dc15aecb410c62923b1b431d841a897b5b40b26b2a7fa9af6eb70d48f97325dc94f9e89550c69552d2139fef6a5bf2 + languageName: node + linkType: hard + "eslint-config-prettier@npm:^8.1.0, eslint-config-prettier@npm:^8.5.0": version: 8.10.0 resolution: "eslint-config-prettier@npm:8.10.0" @@ -31296,6 +31587,16 @@ __metadata: languageName: node linkType: hard +"esprima@npm:^3.1.3": + version: 3.1.3 + resolution: "esprima@npm:3.1.3" + bin: + esparse: ./bin/esparse.js + esvalidate: ./bin/esvalidate.js + checksum: 10/27c9ea08582d6ece009d6ab3cdf5dc3e382f9bf50200c8b4c9a0d048563df2e2fdb0d20338fb7b651cbc00a8faf4b07fc3b2679907ae0758f239d675cde55336 + languageName: node + linkType: hard + "esprima@npm:^4.0.0, esprima@npm:^4.0.1, esprima@npm:~4.0.0": version: 4.0.1 resolution: "esprima@npm:4.0.1" @@ -31324,7 +31625,7 @@ __metadata: languageName: node linkType: hard -"estraverse@npm:^4.1.1": +"estraverse@npm:^4.1.1, estraverse@npm:^4.2.0": version: 4.3.0 resolution: "estraverse@npm:4.3.0" checksum: 10/3f67ad02b6dbfaddd9ea459cf2b6ef4ecff9a6082a7af9d22e445b9abc082ad9ca47e1825557b293fcdae477f4714e561123e30bb6a5b2f184fb2bad4a9497eb @@ -32633,6 +32934,16 @@ __metadata: languageName: node linkType: hard +"falafel@npm:^2.1.0": + version: 2.2.5 + resolution: "falafel@npm:2.2.5" + dependencies: + acorn: "npm:^7.1.1" + isarray: "npm:^2.0.1" + checksum: 10/22d4d2e3e75d889e447064e8756979936ed4559ed2c723217b7096bae968ac07235587f5d9ef87908cf8ff5c0c9c2203703773ef613576d0e3b36caf30720745 + languageName: node + linkType: hard + "fast-base64-decode@npm:^1.0.0": version: 1.0.0 resolution: "fast-base64-decode@npm:1.0.0" @@ -32729,7 +33040,7 @@ __metadata: languageName: node linkType: hard -"fast-levenshtein@npm:^2.0.6": +"fast-levenshtein@npm:^2.0.6, fast-levenshtein@npm:~2.0.6": version: 2.0.6 resolution: "fast-levenshtein@npm:2.0.6" checksum: 10/eb7e220ecf2bab5159d157350b81d01f75726a4382f5a9266f42b9150c4523b9795f7f5d9fbbbeaeac09a441b2369f05ee02db48ea938584205530fe5693cfe1 @@ -32926,7 +33237,7 @@ __metadata: languageName: node linkType: hard -"figures@npm:3.2.0": +"figures@npm:3.2.0, figures@npm:^3, figures@npm:^3.0.0": version: 3.2.0 resolution: "figures@npm:3.2.0" dependencies: @@ -33828,6 +34139,15 @@ __metadata: languageName: node linkType: hard +"get-stream@npm:^4.1.0": + version: 4.1.0 + resolution: "get-stream@npm:4.1.0" + dependencies: + pump: "npm:^3.0.0" + checksum: 10/12673e8aebc79767d187b203e5bfabb8266304037815d3bcc63b6f8c67c6d4ad0d98d4d4528bcdc1cbea68f1dd91bcbd87827aa3cdcfa9c5fa4a4644716d72c2 + languageName: node + linkType: hard + "get-stream@npm:^5.0.0, get-stream@npm:^5.1.0": version: 5.2.0 resolution: "get-stream@npm:5.2.0" @@ -34130,6 +34450,25 @@ __metadata: languageName: node linkType: hard +"got@npm:^11": + version: 11.8.6 + resolution: "got@npm:11.8.6" + dependencies: + "@sindresorhus/is": "npm:^4.0.0" + "@szmarczak/http-timer": "npm:^4.0.5" + "@types/cacheable-request": "npm:^6.0.1" + "@types/responselike": "npm:^1.0.0" + cacheable-lookup: "npm:^5.0.3" + cacheable-request: "npm:^7.0.2" + decompress-response: "npm:^6.0.0" + http2-wrapper: "npm:^1.0.0-beta.5.2" + lowercase-keys: "npm:^2.0.0" + p-cancelable: "npm:^2.0.0" + responselike: "npm:^2.0.0" + checksum: 10/a30c74029d81bd5fe50dea1a0c970595d792c568e188ff8be254b5bc11e6158d1b014570772d4a30d0a97723e7dd34e7c8cc1a2f23018f60aece3070a7a5c2a5 + languageName: node + linkType: hard + "got@npm:^12.1.0": version: 12.6.1 resolution: "got@npm:12.6.1" @@ -34149,6 +34488,25 @@ __metadata: languageName: node linkType: hard +"got@npm:^9.6.0": + version: 9.6.0 + resolution: "got@npm:9.6.0" + dependencies: + "@sindresorhus/is": "npm:^0.14.0" + "@szmarczak/http-timer": "npm:^1.1.2" + cacheable-request: "npm:^6.0.0" + decompress-response: "npm:^3.3.0" + duplexer3: "npm:^0.1.4" + get-stream: "npm:^4.1.0" + lowercase-keys: "npm:^1.0.1" + mimic-response: "npm:^1.0.1" + p-cancelable: "npm:^1.0.0" + to-readable-stream: "npm:^1.0.0" + url-parse-lax: "npm:^3.0.0" + checksum: 10/fae3273b44392b6b1d88071d04ea984784e63dbf8ba3f70b04cb7edda53c7668ee17288ac46af507a9f2aa60c183c5ea1732339141d253dda3eb19f92985c771 + languageName: node + linkType: hard + "graceful-fs@npm:4.2.10": version: 4.2.10 resolution: "graceful-fs@npm:4.2.10" @@ -34345,6 +34703,13 @@ __metadata: languageName: node linkType: hard +"has@npm:^1.0.1": + version: 1.0.4 + resolution: "has@npm:1.0.4" + checksum: 10/c245f332fe78c7b6b8753857240ac12b3286f995f656a33c77e0f5baab7d0157e6ddb1c34940ffd2bffc51f75ede50cd8b29ff65c13e336376aca8cf3df58043 + languageName: node + linkType: hard + "hash-base@npm:^2.0.0": version: 2.0.2 resolution: "hash-base@npm:2.0.2" @@ -34891,6 +35256,13 @@ __metadata: languageName: node linkType: hard +"http-cache-semantics@npm:^4.0.0": + version: 4.2.0 + resolution: "http-cache-semantics@npm:4.2.0" + checksum: 10/4efd2dfcfeea9d5e88c84af450b9980be8a43c2c8179508b1c57c7b4421c855f3e8efe92fa53e0b3f4a43c85824ada930eabbc306d1b3beab750b6dcc5187693 + languageName: node + linkType: hard + "http-cache-semantics@npm:^4.1.1": version: 4.1.1 resolution: "http-cache-semantics@npm:4.1.1" @@ -35027,6 +35399,16 @@ __metadata: languageName: node linkType: hard +"http2-wrapper@npm:^1.0.0-beta.5.2": + version: 1.0.3 + resolution: "http2-wrapper@npm:1.0.3" + dependencies: + quick-lru: "npm:^5.1.1" + resolve-alpn: "npm:^1.0.0" + checksum: 10/8097ee2699440c2e64bda52124990cc5b0fb347401c7797b1a0c1efd5a0f79a4ebaa68e8a6ac3e2dde5f09460c1602764da6da2412bad628ed0a3b0ae35e72d4 + languageName: node + linkType: hard + "http2-wrapper@npm:^2.1.10, http2-wrapper@npm:^2.2.1": version: 2.2.1 resolution: "http2-wrapper@npm:2.2.1" @@ -35154,6 +35536,15 @@ __metadata: languageName: node linkType: hard +"iconv-lite@npm:^0.7.0": + version: 0.7.3 + resolution: "iconv-lite@npm:0.7.3" + dependencies: + safer-buffer: "npm:>= 2.1.2 < 3.0.0" + checksum: 10/b88a3f4527795c96854743bcdcdd03e0083ff5f1c072cb13d3e27c67cd276efd9a2a364922cb836538e976456f82323d645f7ea91d5bc8da1f000dc16742a0aa + languageName: node + linkType: hard + "iconv-lite@npm:^0.7.2, iconv-lite@npm:~0.7.0": version: 0.7.2 resolution: "iconv-lite@npm:0.7.2" @@ -35394,6 +35785,29 @@ __metadata: languageName: node linkType: hard +"inquirer@npm:^8.2.3": + version: 8.2.7 + resolution: "inquirer@npm:8.2.7" + dependencies: + "@inquirer/external-editor": "npm:^1.0.0" + ansi-escapes: "npm:^4.2.1" + chalk: "npm:^4.1.1" + cli-cursor: "npm:^3.1.0" + cli-width: "npm:^3.0.0" + figures: "npm:^3.0.0" + lodash: "npm:^4.17.21" + mute-stream: "npm:0.0.8" + ora: "npm:^5.4.1" + run-async: "npm:^2.4.0" + rxjs: "npm:^7.5.5" + string-width: "npm:^4.1.0" + strip-ansi: "npm:^6.0.0" + through: "npm:^2.3.6" + wrap-ansi: "npm:^6.0.1" + checksum: 10/526fb5ca55a29decda9b67c7b2bd437730152104c6e7c5f0d7ade90af6dc999371e1602ce86eb4a39ee3d91993501cddec32e4fe3f599723f2b653b02b685e3b + languageName: node + linkType: hard + "int64-buffer@npm:^1.1.0": version: 1.1.0 resolution: "int64-buffer@npm:1.1.0" @@ -35941,7 +36355,7 @@ __metadata: languageName: node linkType: hard -"is-plain-obj@npm:^2.1.0": +"is-plain-obj@npm:^2.0.0, is-plain-obj@npm:^2.1.0": version: 2.1.0 resolution: "is-plain-obj@npm:2.1.0" checksum: 10/cec9100678b0a9fe0248a81743041ed990c2d4c99f893d935545cfbc42876cbe86d207f3b895700c690ad2fa520e568c44afc1605044b535a7820c1d40e38daa @@ -36055,6 +36469,13 @@ __metadata: languageName: node linkType: hard +"is-stream@npm:^1.0.1": + version: 1.1.0 + resolution: "is-stream@npm:1.1.0" + checksum: 10/351aa77c543323c4e111204482808cfad68d2e940515949e31ccd0b010fc13d5fba4b9c230e4887fd24284713040f43e542332fbf172f6b9944b7d62e389c0ec + languageName: node + linkType: hard + "is-stream@npm:^2.0.0, is-stream@npm:^2.0.1": version: 2.0.1 resolution: "is-stream@npm:2.0.1" @@ -36224,7 +36645,7 @@ __metadata: languageName: node linkType: hard -"isarray@npm:^2.0.5": +"isarray@npm:^2.0.1, isarray@npm:^2.0.5": version: 2.0.5 resolution: "isarray@npm:2.0.5" checksum: 10/1d8bc7911e13bb9f105b1b3e0b396c787a9e63046af0b8fe0ab1414488ab06b2b099b87a2d8a9e31d21c9a6fad773c7fc8b257c4880f2d957274479d28ca3414 @@ -36266,6 +36687,16 @@ __metadata: languageName: node linkType: hard +"isomorphic-fetch@npm:^2.2.1": + version: 2.2.1 + resolution: "isomorphic-fetch@npm:2.2.1" + dependencies: + node-fetch: "npm:^1.0.1" + whatwg-fetch: "npm:>=0.10.0" + checksum: 10/bb5daa7c3785d6742f4379a81e55b549a469503f7c9bf9411b48592e86632cf5e8fe8ea878dba185c0f33eb7c510c23abdeb55aebfdf5d3c70f031ced68c5424 + languageName: node + linkType: hard + "isomorphic-fetch@npm:^3.0.0": version: 3.0.0 resolution: "isomorphic-fetch@npm:3.0.0" @@ -37069,6 +37500,19 @@ __metadata: languageName: node linkType: hard +"joi@npm:^17.6.0": + version: 17.13.8 + resolution: "joi@npm:17.13.8" + dependencies: + "@hapi/hoek": "npm:^9.3.0" + "@hapi/topo": "npm:^5.1.0" + "@sideway/address": "npm:^4.1.5" + "@sideway/formula": "npm:^3.0.1" + "@sideway/pinpoint": "npm:^2.0.0" + checksum: 10/7d8ccb06cee958516748519cb46430b1bf5b732f54428a5daf8f8630290c928be1abb7d3c3c2506d68ba76d1c1abe480e087255c34b839aae47b2ee2c76f587d + languageName: node + linkType: hard + "join-component@npm:^1.1.0": version: 1.1.0 resolution: "join-component@npm:1.1.0" @@ -37098,6 +37542,13 @@ __metadata: languageName: node linkType: hard +"js-base64@npm:^2.4.3": + version: 2.6.4 + resolution: "js-base64@npm:2.6.4" + checksum: 10/c1a740a34fbb0ad0a528c2ab8749d7f873b1856a0638826306fcd98502e3c8c833334dff233085407e3201be543e5e71bf9692da7891ca680d9b03d027247a6a + languageName: node + linkType: hard + "js-base64@npm:^3.7.5, js-base64@npm:^3.7.8": version: 3.8.1 resolution: "js-base64@npm:3.8.1" @@ -37262,6 +37713,13 @@ __metadata: languageName: node linkType: hard +"json-buffer@npm:3.0.0": + version: 3.0.0 + resolution: "json-buffer@npm:3.0.0" + checksum: 10/6e364585600598c42f1cc85d1305569aeb1a6a13e7c67960f17b403f087e2700104ec8e49fc681ab6d6278ee4d132ac033f2625c22a9777ed9b83b403b40f23e + languageName: node + linkType: hard + "json-buffer@npm:3.0.1": version: 3.0.1 resolution: "json-buffer@npm:3.0.1" @@ -37381,7 +37839,7 @@ __metadata: languageName: node linkType: hard -"json-stable-stringify@npm:^1.1.1": +"json-stable-stringify@npm:^1.0.1, json-stable-stringify@npm:^1.1.1": version: 1.3.0 resolution: "json-stable-stringify@npm:1.3.0" dependencies: @@ -37583,7 +38041,16 @@ __metadata: languageName: node linkType: hard -"keyv@npm:^4.5.3": +"keyv@npm:^3.0.0": + version: 3.1.0 + resolution: "keyv@npm:3.1.0" + dependencies: + json-buffer: "npm:3.0.0" + checksum: 10/6de272b3f78975a9a0b12259953c09d5bbe9de9acfd845471ebd758928b523f70563462f0c16a866fe9b447ff5bdebda72c62bc23734eb72cd1fb8f1d7076843 + languageName: node + linkType: hard + +"keyv@npm:^4.0.0, keyv@npm:^4.5.3": version: 4.5.4 resolution: "keyv@npm:4.5.4" dependencies: @@ -37714,6 +38181,15 @@ __metadata: languageName: node linkType: hard +"latest-version@npm:^5": + version: 5.1.0 + resolution: "latest-version@npm:5.1.0" + dependencies: + package-json: "npm:^6.3.0" + checksum: 10/fbc72b071eb66c40f652441fd783a9cca62f08bf42433651937f078cd9ef94bf728ec7743992777826e4e89305aef24f234b515e6030503a2cbee7fc9bdc2c0f + languageName: node + linkType: hard + "latest-version@npm:^7.0.0": version: 7.0.0 resolution: "latest-version@npm:7.0.0" @@ -37904,6 +38380,16 @@ __metadata: languageName: node linkType: hard +"levn@npm:~0.3.0": + version: 0.3.0 + resolution: "levn@npm:0.3.0" + dependencies: + prelude-ls: "npm:~1.1.2" + type-check: "npm:~0.3.2" + checksum: 10/e1c3e75b5c430d9aa4c32c83c8a611e4ca53608ca78e3ea3bf6bbd9d017e4776d05d86e27df7901baebd3afa732abede9f26f715b8c1be19e95505c7a3a7b589 + languageName: node + linkType: hard + "lie@npm:~3.3.0": version: 3.3.0 resolution: "lie@npm:3.3.0" @@ -38522,6 +39008,20 @@ __metadata: languageName: node linkType: hard +"lowercase-keys@npm:^1.0.0, lowercase-keys@npm:^1.0.1": + version: 1.0.1 + resolution: "lowercase-keys@npm:1.0.1" + checksum: 10/12ba64572dc25ae9ee30d37a11f3a91aea046c1b6b905fdf8ac77e2f268f153ed36e60d39cb3bfa47a89f31d981dae9a8cc9915124a56fe51ff01ed6e8bb68fa + languageName: node + linkType: hard + +"lowercase-keys@npm:^2.0.0": + version: 2.0.0 + resolution: "lowercase-keys@npm:2.0.0" + checksum: 10/1c233d2da35056e8c49fae8097ee061b8c799b2f02e33c2bf32f9913c7de8fb481ab04dab7df35e94156c800f5f34e99acbf32b21781d87c3aa43ef7b748b79e + languageName: node + linkType: hard + "lowercase-keys@npm:^3.0.0": version: 3.0.0 resolution: "lowercase-keys@npm:3.0.0" @@ -38605,6 +39105,15 @@ __metadata: languageName: node linkType: hard +"magic-string@npm:^0.22.4": + version: 0.22.5 + resolution: "magic-string@npm:0.22.5" + dependencies: + vlq: "npm:^0.2.2" + checksum: 10/a5514579480f5fb6cc24d87073b431aa647f02d6a1fd2972dd2116e5ac73f9a3bb1e2bd0d635b5f59a1ecbf9ad825262385a5f192514acbba7cd597eff86e726 + languageName: node + linkType: hard + "magic-string@npm:^0.30.0, magic-string@npm:^0.30.3, magic-string@npm:~0.30.8": version: 0.30.21 resolution: "magic-string@npm:0.30.21" @@ -39155,6 +39664,15 @@ __metadata: languageName: node linkType: hard +"merge-source-map@npm:1.0.4": + version: 1.0.4 + resolution: "merge-source-map@npm:1.0.4" + dependencies: + source-map: "npm:^0.5.6" + checksum: 10/86a4e60d83980393e61f069c7ae33e7899c4c012c3cd2cf50e01482e7a284bbe9c8cd08d37adbf241fc9eacfa4425241432e7461cf6559f7e9902587889660de + languageName: node + linkType: hard + "merge-stream@npm:^2.0.0": version: 2.0.0 resolution: "merge-stream@npm:2.0.0" @@ -39692,6 +40210,7 @@ __metadata: ts-node: "npm:^10.9.2" tsx: "npm:^4.21.0" typescript: "npm:~5.8.3" + typewriter: "npm:9.2.0" unicode-confusables: "npm:^0.1.1" uri-js: "npm:^4.4.1" url: "npm:0.11.0" @@ -40583,6 +41102,13 @@ __metadata: languageName: node linkType: hard +"mimic-response@npm:^1.0.0, mimic-response@npm:^1.0.1": + version: 1.0.1 + resolution: "mimic-response@npm:1.0.1" + checksum: 10/034c78753b0e622bc03c983663b1cdf66d03861050e0c8606563d149bc2b02d63f62ce4d32be4ab50d0553ae0ffe647fc34d1f5281184c6e1e8cf4d85e8d9823 + languageName: node + linkType: hard + "mimic-response@npm:^3.1.0": version: 3.1.0 resolution: "mimic-response@npm:3.1.0" @@ -40702,7 +41228,7 @@ __metadata: languageName: node linkType: hard -"minimist@npm:^1.2.0, minimist@npm:^1.2.5, minimist@npm:^1.2.6": +"minimist@npm:^1.1.3, minimist@npm:^1.2.0, minimist@npm:^1.2.5, minimist@npm:^1.2.6": version: 1.2.8 resolution: "minimist@npm:1.2.8" checksum: 10/908491b6cc15a6c440ba5b22780a0ba89b9810e1aea684e253e43c4e3b8d56ec1dcdd7ea96dde119c29df59c936cde16062159eae4225c691e19c70b432b6e6f @@ -41417,6 +41943,16 @@ __metadata: languageName: node linkType: hard +"node-fetch@npm:^1.0.1": + version: 1.7.3 + resolution: "node-fetch@npm:1.7.3" + dependencies: + encoding: "npm:^0.1.11" + is-stream: "npm:^1.0.1" + checksum: 10/17be1a182eec37ba43c1b02673329ab1fa1c2e3ddbdf9b5f62d5157aa118c253dcd42f338eb1b6fe84fb0689c4dd8eb5aaebf46a9b5a066fae88c74e36522688 + languageName: node + linkType: hard + "node-fetch@npm:^2.0.0, node-fetch@npm:^2.6.0, node-fetch@npm:^2.6.1, node-fetch@npm:^2.6.7, node-fetch@npm:^2.7.0": version: 2.7.0 resolution: "node-fetch@npm:2.7.0" @@ -41525,6 +42061,13 @@ __metadata: languageName: node linkType: hard +"node-machine-id@npm:^1.1.12": + version: 1.1.12 + resolution: "node-machine-id@npm:1.1.12" + checksum: 10/46bf3d4fab8d0e63b24c42bcec2b6975c7ec5bc16e53d7a589d095668d0fdf0bfcbcdc28246dd1ef74cf95a37fbd774cd4b17b41f518d79dfad7fdc99f995903 + languageName: node + linkType: hard + "node-preload@npm:^0.2.1": version: 0.2.1 resolution: "node-preload@npm:0.2.1" @@ -41658,6 +42201,20 @@ __metadata: languageName: node linkType: hard +"normalize-url@npm:^4.1.0": + version: 4.5.1 + resolution: "normalize-url@npm:4.5.1" + checksum: 10/20ced2845fcfaa46da74efc0aa39b7bed22f3db39e6e8b844261613082a36a2dcd468decad89fa9313b5464bebab4034f96bda7880e8fc468027fecf6a6fa254 + languageName: node + linkType: hard + +"normalize-url@npm:^6.0.1": + version: 6.1.0 + resolution: "normalize-url@npm:6.1.0" + checksum: 10/5ae699402c9d5ffa330adc348fcd6fc6e6a155ab7c811b96e30b7ecab60ceef821d8f86443869671dda71bbc47f4b9625739c82ad247e883e9aefe875bfb8659 + languageName: node + linkType: hard + "normalize-url@npm:^8.0.0": version: 8.1.1 resolution: "normalize-url@npm:8.1.1" @@ -41906,6 +42463,13 @@ __metadata: languageName: node linkType: hard +"object-inspect@npm:~1.4.0": + version: 1.4.1 + resolution: "object-inspect@npm:1.4.1" + checksum: 10/f49b58baf3d82692cf3dac7e969f742f4618caa4d3dd84819bb095438bea1b31ffb5b6896d1227091b60d7d641752a07c822f113ce505cefd8adadfd6d9602e9 + languageName: node + linkType: hard + "object-is@npm:^1.0.1, object-is@npm:^1.1.5": version: 1.1.5 resolution: "object-is@npm:1.1.5" @@ -42240,6 +42804,20 @@ __metadata: languageName: node linkType: hard +"optionator@npm:^0.8.1": + version: 0.8.3 + resolution: "optionator@npm:0.8.3" + dependencies: + deep-is: "npm:~0.1.3" + fast-levenshtein: "npm:~2.0.6" + levn: "npm:~0.3.0" + prelude-ls: "npm:~1.1.2" + type-check: "npm:~0.3.2" + word-wrap: "npm:~1.2.3" + checksum: 10/6fa3c841b520f10aec45563962922215180e8cfbc59fde3ecd4ba2644ad66ca96bd19ad0e853f22fefcb7fc10e7612a5215b412cc66c5588f9a3138b38f6b5ff + languageName: node + linkType: hard + "optionator@npm:^0.9.3": version: 0.9.4 resolution: "optionator@npm:0.9.4" @@ -42392,6 +42970,20 @@ __metadata: languageName: node linkType: hard +"p-cancelable@npm:^1.0.0": + version: 1.1.0 + resolution: "p-cancelable@npm:1.1.0" + checksum: 10/2db3814fef6d9025787f30afaee4496a8857a28be3c5706432cbad76c688a6db1874308f48e364a42f5317f5e41e8e7b4f2ff5c8ff2256dbb6264bc361704ece + languageName: node + linkType: hard + +"p-cancelable@npm:^2.0.0": + version: 2.1.1 + resolution: "p-cancelable@npm:2.1.1" + checksum: 10/7f1b64db17fc54acf359167d62898115dcf2a64bf6b3b038e4faf36fc059e5ed762fb9624df8ed04b25bee8de3ab8d72dea9879a2a960cd12e23c420a4aca6ed + languageName: node + linkType: hard + "p-cancelable@npm:^3.0.0": version: 3.0.0 resolution: "p-cancelable@npm:3.0.0" @@ -42592,6 +43184,18 @@ __metadata: languageName: node linkType: hard +"package-json@npm:^6.3.0": + version: 6.5.0 + resolution: "package-json@npm:6.5.0" + dependencies: + got: "npm:^9.6.0" + registry-auth-token: "npm:^4.0.0" + registry-url: "npm:^5.0.0" + semver: "npm:^6.2.0" + checksum: 10/adb8e49f352ea0d71a4d351732c3870d57f21e6f3921d69a83dd9ef04b45cdb0a035495826fbe9fb2cb9a7e521484404b7d527c181133867b126588efa1996c6 + languageName: node + linkType: hard + "package-json@npm:^8.1.0": version: 8.1.1 resolution: "package-json@npm:8.1.1" @@ -42604,20 +43208,27 @@ __metadata: languageName: node linkType: hard -"pako@npm:^2.1.0": - version: 2.1.0 - resolution: "pako@npm:2.1.0" - checksum: 10/38a04991d0ec4f4b92794a68b8c92bf7340692c5d980255c92148da96eb3e550df7a86a7128b5ac0c65ecddfe5ef3bbe9c6dab13e1bc315086e759b18f7c1401 +"pako@npm:^0.2.5": + version: 0.2.9 + resolution: "pako@npm:0.2.9" + checksum: 10/627c6842e90af0b3a9ee47345bd66485a589aff9514266f4fa9318557ad819c46fedf97510f2cef9b6224c57913777966a05cb46caf6a9b31177a5401a06fe15 languageName: node linkType: hard -"pako@npm:~1.0.2, pako@npm:~1.0.5": +"pako@npm:^1.0.6, pako@npm:~1.0.2, pako@npm:~1.0.5": version: 1.0.11 resolution: "pako@npm:1.0.11" checksum: 10/1ad07210e894472685564c4d39a08717e84c2a68a70d3c1d9e657d32394ef1670e22972a433cbfe48976cb98b154ba06855dcd3fcfba77f60f1777634bec48c0 languageName: node linkType: hard +"pako@npm:^2.1.0": + version: 2.1.0 + resolution: "pako@npm:2.1.0" + checksum: 10/38a04991d0ec4f4b92794a68b8c92bf7340692c5d980255c92148da96eb3e550df7a86a7128b5ac0c65ecddfe5ef3bbe9c6dab13e1bc315086e759b18f7c1401 + languageName: node + linkType: hard + "param-case@npm:^3.0.4": version: 3.0.4 resolution: "param-case@npm:3.0.4" @@ -43283,6 +43894,13 @@ __metadata: languageName: node linkType: hard +"pluralize@npm:^7.0.0": + version: 7.0.0 + resolution: "pluralize@npm:7.0.0" + checksum: 10/905274e679d3802650fdfdd977434757d4680082da7a23c0938a608d1d5c8246790b62dc15ff1f737b0d57baa6ad2f6ebb0857b1950435a583e32af76ee58e1f + languageName: node + linkType: hard + "pngjs@npm:7.0.0, pngjs@npm:^7.0.0": version: 7.0.0 resolution: "pngjs@npm:7.0.0" @@ -44233,6 +44851,20 @@ __metadata: languageName: node linkType: hard +"prelude-ls@npm:~1.1.2": + version: 1.1.2 + resolution: "prelude-ls@npm:1.1.2" + checksum: 10/946a9f60d3477ca6b7d4c5e8e452ad1b98dc8aaa992cea939a6b926ac16cc4129d7217c79271dc808b5814b1537ad0af37f29a942e2eafbb92cfc5a1c87c38cb + languageName: node + linkType: hard + +"prepend-http@npm:^2.0.0": + version: 2.0.0 + resolution: "prepend-http@npm:2.0.0" + checksum: 10/7694a9525405447662c1ffd352fcb41b6410c705b739b6f4e3a3e21cf5fdede8377890088e8934436b8b17ba55365a615f153960f30877bf0d0392f9e93503ea + languageName: node + linkType: hard + "prettier-2@npm:prettier@^2.8.8, prettier@npm:^2.0.5, prettier@npm:^2.4.1": version: 2.8.8 resolution: "prettier@npm:2.8.8" @@ -44358,6 +44990,13 @@ __metadata: languageName: node linkType: hard +"process-nextick-args@npm:~1.0.6": + version: 1.0.7 + resolution: "process-nextick-args@npm:1.0.7" + checksum: 10/f3b0e2f762e4fc03d02779fbf434caff82d27439ba2ecd82f7f95439e56dc23e367a8c1d3919533bd961b8e447d8ad0d941d6a3acda48ddcb80fe1b45b423579 + languageName: node + linkType: hard + "process-nextick-args@npm:~2.0.0": version: 2.0.1 resolution: "process-nextick-args@npm:2.0.1" @@ -44856,6 +45495,39 @@ __metadata: languageName: node linkType: hard +"quicktype-core@npm:^6.0.71": + version: 6.1.12 + resolution: "quicktype-core@npm:6.1.12" + dependencies: + "@mark.probst/unicode-properties": "npm:~1.1.0" + browser-or-node: "npm:^1.2.1" + collection-utils: "npm:^1.0.1" + is-url: "npm:^1.2.4" + isomorphic-fetch: "npm:^2.2.1" + js-base64: "npm:^2.4.3" + pako: "npm:^1.0.6" + pluralize: "npm:^7.0.0" + readable-stream: "npm:2.3.0" + urijs: "npm:^1.19.1" + wordwrap: "npm:^1.0.0" + yaml: "npm:^1.5.0" + checksum: 10/5c12680aeb052316d5e5c49cfd014ac8af1f93f5ed6f61f315c79ed2943517e35414c8456b490f3ec8629db4ec45180872be3be63d7d5425b51b8cc547af9d1e + languageName: node + linkType: hard + +"quote-stream@npm:^1.0.1, quote-stream@npm:~1.0.2": + version: 1.0.2 + resolution: "quote-stream@npm:1.0.2" + dependencies: + buffer-equal: "npm:0.0.1" + minimist: "npm:^1.1.3" + through2: "npm:^2.0.0" + bin: + quote-stream: bin/cmd.js + checksum: 10/25d615ea6dc406bb7fd865cb83bc897667e81974e6ff3f473f360f0ff2f3f6e00c07d0bd382b719b3588e52252e3b5565a4edc494a7fd190198fd4241eaa4180 + languageName: node + linkType: hard + "radix3@npm:^1.1.0": version: 1.1.0 resolution: "radix3@npm:1.1.0" @@ -44934,7 +45606,7 @@ __metadata: languageName: node linkType: hard -"rc@npm:1.2.8": +"rc@npm:1.2.8, rc@npm:^1.2.8": version: 1.2.8 resolution: "rc@npm:1.2.8" dependencies: @@ -46631,6 +47303,21 @@ __metadata: languageName: node linkType: hard +"readable-stream@npm:2.3.0": + version: 2.3.0 + resolution: "readable-stream@npm:2.3.0" + dependencies: + core-util-is: "npm:~1.0.0" + inherits: "npm:~2.0.3" + isarray: "npm:~1.0.0" + process-nextick-args: "npm:~1.0.6" + safe-buffer: "npm:~5.1.0" + string_decoder: "npm:~1.0.0" + util-deprecate: "npm:~1.0.1" + checksum: 10/26662d74fc66a38d833bf5bd26107217f63cbdcc05988f88e7e738caa32b8c45006d78d8bc2a008f175970e01a7469f71d21932d4c8eb4f2fc7f1c69c9c4870c + languageName: node + linkType: hard + "readable-stream@npm:2.3.7": version: 2.3.7 resolution: "readable-stream@npm:2.3.7" @@ -46671,7 +47358,7 @@ __metadata: languageName: node linkType: hard -"readable-stream@npm:^2.0.0, readable-stream@npm:^2.0.1, readable-stream@npm:^2.0.2, readable-stream@npm:^2.0.5, readable-stream@npm:^2.2.2, readable-stream@npm:^2.3.3, readable-stream@npm:^2.3.8, readable-stream@npm:~2.3.6": +"readable-stream@npm:^2.0.0, readable-stream@npm:^2.0.1, readable-stream@npm:^2.0.2, readable-stream@npm:^2.0.5, readable-stream@npm:^2.2.2, readable-stream@npm:^2.3.3, readable-stream@npm:^2.3.8, readable-stream@npm:~2.3.3, readable-stream@npm:~2.3.6": version: 2.3.8 resolution: "readable-stream@npm:2.3.8" dependencies: @@ -47051,6 +47738,15 @@ __metadata: languageName: node linkType: hard +"registry-auth-token@npm:^4.0.0": + version: 4.2.2 + resolution: "registry-auth-token@npm:4.2.2" + dependencies: + rc: "npm:1.2.8" + checksum: 10/00d1b1c69f09df52a0bfbaecee71f2ba094d8fd8d1abc325090655b2c6c8a69c969b31525086c10f95126c3452cd4a0c5c9a6832fb08bec5a32a4e224b790cf8 + languageName: node + linkType: hard + "registry-auth-token@npm:^5.0.1": version: 5.1.1 resolution: "registry-auth-token@npm:5.1.1" @@ -47060,6 +47756,15 @@ __metadata: languageName: node linkType: hard +"registry-url@npm:^5.0.0": + version: 5.1.0 + resolution: "registry-url@npm:5.1.0" + dependencies: + rc: "npm:^1.2.8" + checksum: 10/bcea86c84a0dbb66467b53187fadebfea79017cddfb4a45cf27530d7275e49082fe9f44301976eb0164c438e395684bcf3dae4819b36ff9d1640d8cc60c73df9 + languageName: node + linkType: hard + "registry-url@npm:^6.0.0": version: 6.0.1 resolution: "registry-url@npm:6.0.1" @@ -47317,7 +48022,7 @@ __metadata: languageName: node linkType: hard -"resolve-alpn@npm:^1.2.0": +"resolve-alpn@npm:^1.0.0, resolve-alpn@npm:^1.2.0": version: 1.2.1 resolution: "resolve-alpn@npm:1.2.1" checksum: 10/744e87888f0b6fa0b256ab454ca0b9c0b80808715e2ef1f3672773665c92a941f6181194e30ccae4a8cd0adbe0d955d3f133102636d2ee0cca0119fec0bc9aec @@ -47398,7 +48103,7 @@ __metadata: languageName: node linkType: hard -"resolve@npm:^1.1.7, resolve@npm:^1.17.0, resolve@npm:^1.20.0, resolve@npm:^1.22.11, resolve@npm:^1.22.3, resolve@npm:^1.22.4, resolve@npm:^1.22.8": +"resolve@npm:^1.1.5, resolve@npm:^1.1.7, resolve@npm:^1.17.0, resolve@npm:^1.20.0, resolve@npm:^1.22.11, resolve@npm:^1.22.3, resolve@npm:^1.22.4, resolve@npm:^1.22.8": version: 1.22.12 resolution: "resolve@npm:1.22.12" dependencies: @@ -47438,7 +48143,7 @@ __metadata: languageName: node linkType: hard -"resolve@patch:resolve@npm%3A^1.1.7#optional!builtin, resolve@patch:resolve@npm%3A^1.17.0#optional!builtin, resolve@patch:resolve@npm%3A^1.20.0#optional!builtin, resolve@patch:resolve@npm%3A^1.22.11#optional!builtin, resolve@patch:resolve@npm%3A^1.22.3#optional!builtin, resolve@patch:resolve@npm%3A^1.22.4#optional!builtin, resolve@patch:resolve@npm%3A^1.22.8#optional!builtin": +"resolve@patch:resolve@npm%3A^1.1.5#optional!builtin, resolve@patch:resolve@npm%3A^1.1.7#optional!builtin, resolve@patch:resolve@npm%3A^1.17.0#optional!builtin, resolve@patch:resolve@npm%3A^1.20.0#optional!builtin, resolve@patch:resolve@npm%3A^1.22.11#optional!builtin, resolve@patch:resolve@npm%3A^1.22.3#optional!builtin, resolve@patch:resolve@npm%3A^1.22.4#optional!builtin, resolve@patch:resolve@npm%3A^1.22.8#optional!builtin": version: 1.22.12 resolution: "resolve@patch:resolve@npm%3A1.22.12#optional!builtin::version=1.22.12&hash=c3c19d" dependencies: @@ -47465,6 +48170,24 @@ __metadata: languageName: node linkType: hard +"responselike@npm:^1.0.2": + version: 1.0.2 + resolution: "responselike@npm:1.0.2" + dependencies: + lowercase-keys: "npm:^1.0.0" + checksum: 10/2e9e70f1dcca3da621a80ce71f2f9a9cad12c047145c6ece20df22f0743f051cf7c73505e109814915f23f9e34fb0d358e22827723ee3d56b623533cab8eafcd + languageName: node + linkType: hard + +"responselike@npm:^2.0.0": + version: 2.0.1 + resolution: "responselike@npm:2.0.1" + dependencies: + lowercase-keys: "npm:^2.0.0" + checksum: 10/b122535466e9c97b55e69c7f18e2be0ce3823c5d47ee8de0d9c0b114aa55741c6db8bfbfce3766a94d1272e61bfb1ebf0a15e9310ac5629fbb7446a861b4fd3a + languageName: node + linkType: hard + "responselike@npm:^3.0.0": version: 3.0.0 resolution: "responselike@npm:3.0.0" @@ -47739,6 +48462,13 @@ __metadata: languageName: node linkType: hard +"run-async@npm:^2.4.0": + version: 2.4.1 + resolution: "run-async@npm:2.4.1" + checksum: 10/c79551224dafa26ecc281cb1efad3510c82c79116aaf681f8a931ce70fdf4ca880d58f97d3b930a38992c7aad7955a08e065b32ec194e1dd49d7790c874ece50 + languageName: node + linkType: hard + "run-parallel@npm:^1.1.9": version: 1.2.0 resolution: "run-parallel@npm:1.2.0" @@ -47748,7 +48478,7 @@ __metadata: languageName: node linkType: hard -"rxjs@npm:7.8.2, rxjs@npm:>= 6, rxjs@npm:^7.8.1, rxjs@npm:^7.8.2": +"rxjs@npm:7.8.2, rxjs@npm:>= 6, rxjs@npm:^7.5.5, rxjs@npm:^7.8.1, rxjs@npm:^7.8.2": version: 7.8.2 resolution: "rxjs@npm:7.8.2" dependencies: @@ -48094,7 +48824,7 @@ __metadata: languageName: node linkType: hard -"semver@npm:7.8.5, semver@npm:^7.0.0, semver@npm:^7.1.1, semver@npm:^7.1.3, semver@npm:^7.3.5, semver@npm:^7.3.7, semver@npm:^7.3.8, semver@npm:^7.5.0, semver@npm:^7.5.2, semver@npm:^7.5.3, semver@npm:^7.5.4, semver@npm:^7.6.0, semver@npm:^7.6.2, semver@npm:^7.6.3, semver@npm:^7.7.1, semver@npm:^7.7.3, semver@npm:^7.7.4, semver@npm:^7.8.5": +"semver@npm:7.8.5, semver@npm:^7.0.0, semver@npm:^7.1.1, semver@npm:^7.1.3, semver@npm:^7.3, semver@npm:^7.3.5, semver@npm:^7.3.7, semver@npm:^7.3.8, semver@npm:^7.5.0, semver@npm:^7.5.2, semver@npm:^7.5.3, semver@npm:^7.5.4, semver@npm:^7.6.0, semver@npm:^7.6.2, semver@npm:^7.6.3, semver@npm:^7.7.1, semver@npm:^7.7.3, semver@npm:^7.7.4, semver@npm:^7.8.5": version: 7.8.5 resolution: "semver@npm:7.8.5" bin: @@ -48112,7 +48842,7 @@ __metadata: languageName: node linkType: hard -"semver@npm:^6.0.0, semver@npm:^6.3.0, semver@npm:^6.3.1": +"semver@npm:^6.0.0, semver@npm:^6.2.0, semver@npm:^6.3.0, semver@npm:^6.3.1": version: 6.3.1 resolution: "semver@npm:6.3.1" bin: @@ -48385,6 +49115,13 @@ __metadata: languageName: node linkType: hard +"shallow-copy@npm:~0.0.1": + version: 0.0.1 + resolution: "shallow-copy@npm:0.0.1" + checksum: 10/916c3a8d687f4eea9f257f08b8c5c2e55aa092fee0f22ca86f241047a45a974af7df86a9169cfb7b7919a827f5095885928ec98e5bf8e17f076bac4f321639a6 + languageName: node + linkType: hard + "shallowequal@npm:^1.1.0": version: 1.1.0 resolution: "shallowequal@npm:1.1.0" @@ -48971,6 +49708,15 @@ __metadata: languageName: node linkType: hard +"sort-keys@npm:^4.2.0": + version: 4.2.0 + resolution: "sort-keys@npm:4.2.0" + dependencies: + is-plain-obj: "npm:^2.0.0" + checksum: 10/cbc5adf02eae3f9e2ccbd94eafd6019d705fb217ffd77bc9ecd71b6e4aee617bac93dae91c580fb62f0deb9b0b53f105ba2ae89d253168adf16b7cb3e6cef25a + languageName: node + linkType: hard + "source-map-js@npm:^1.0.1, source-map-js@npm:^1.0.2, source-map-js@npm:^1.2.1": version: 1.2.1 resolution: "source-map-js@npm:1.2.1" @@ -49232,6 +49978,37 @@ __metadata: languageName: node linkType: hard +"static-eval@npm:^2.0.0": + version: 2.1.1 + resolution: "static-eval@npm:2.1.1" + dependencies: + escodegen: "npm:^2.1.0" + checksum: 10/b47e8238014745ea457f2dc9e14130298ce5dbec142a5c93a943f338fe1b47d03086d1897114e15d8603a9a7a39fed95bb49ccd3fa95dfc1550952eca0688417 + languageName: node + linkType: hard + +"static-module@npm:^2.2.0": + version: 2.2.5 + resolution: "static-module@npm:2.2.5" + dependencies: + concat-stream: "npm:~1.6.0" + convert-source-map: "npm:^1.5.1" + duplexer2: "npm:~0.1.4" + escodegen: "npm:~1.9.0" + falafel: "npm:^2.1.0" + has: "npm:^1.0.1" + magic-string: "npm:^0.22.4" + merge-source-map: "npm:1.0.4" + object-inspect: "npm:~1.4.0" + quote-stream: "npm:~1.0.2" + readable-stream: "npm:~2.3.3" + shallow-copy: "npm:~0.0.1" + static-eval: "npm:^2.0.0" + through2: "npm:~2.0.3" + checksum: 10/2d2920052248cbeb2f7c06ad726b9b9359d00780156f838c7149790558de1900d64bfef286d07e84ffeb1c2efba7403cf383ff54a662c36c11e3f16b932fb994 + languageName: node + linkType: hard + "statuses@npm:>= 1.5.0 < 2, statuses@npm:^1.5.0, statuses@npm:~1.5.0": version: 1.5.0 resolution: "statuses@npm:1.5.0" @@ -49535,6 +50312,15 @@ __metadata: languageName: node linkType: hard +"string_decoder@npm:~1.0.0": + version: 1.0.3 + resolution: "string_decoder@npm:1.0.3" + dependencies: + safe-buffer: "npm:~5.1.0" + checksum: 10/8689f666b5c6045f125fc6202eebd28f790606bc7962cfcc27eec54cfdcd19c3222ae6a4d5a3a911ef71574d8b2f9b607f99922a0db9837f1ff132465cc519f2 + languageName: node + linkType: hard + "string_decoder@npm:~1.1.1": version: 1.1.1 resolution: "string_decoder@npm:1.1.1" @@ -50086,7 +50872,7 @@ __metadata: languageName: node linkType: hard -"terminal-link@npm:2.1.1, terminal-link@npm:^2.1.1": +"terminal-link@npm:2.1.1, terminal-link@npm:^2, terminal-link@npm:^2.1.1": version: 2.1.1 resolution: "terminal-link@npm:2.1.1" dependencies: @@ -50274,7 +51060,17 @@ __metadata: languageName: node linkType: hard -"through@npm:2, through@npm:^2.3.8, through@npm:~2.3.4": +"through2@npm:^2.0.0, through2@npm:~2.0.3": + version: 2.0.5 + resolution: "through2@npm:2.0.5" + dependencies: + readable-stream: "npm:~2.3.6" + xtend: "npm:~4.0.1" + checksum: 10/cd71f7dcdc7a8204fea003a14a433ef99384b7d4e31f5497e1f9f622b3cf3be3691f908455f98723bdc80922a53af7fa10c3b7abbe51c6fd3d536dbc7850e2c4 + languageName: node + linkType: hard + +"through@npm:2, through@npm:^2.3.6, through@npm:^2.3.8, through@npm:~2.3.4": version: 2.3.8 resolution: "through@npm:2.3.8" checksum: 10/5da78346f70139a7d213b65a0106f3c398d6bc5301f9248b5275f420abc2c4b1e77c2abc72d218dedc28c41efb2e7c312cb76a7730d04f9c2d37d247da3f4198 @@ -50311,6 +51107,13 @@ __metadata: languageName: node linkType: hard +"tiny-inflate@npm:^1.0.0": + version: 1.0.3 + resolution: "tiny-inflate@npm:1.0.3" + checksum: 10/f620114fb51ea4a16ea7b4c62d6dd753f8faf41808a133c53d431ed4bf2ca377b21443653a0096894f2be22ca11bb327f148e7e5431f9246068917724ec01ffc + languageName: node + linkType: hard + "tiny-invariant@npm:^1.0.2, tiny-invariant@npm:^1.3.1": version: 1.3.3 resolution: "tiny-invariant@npm:1.3.3" @@ -50415,6 +51218,13 @@ __metadata: languageName: node linkType: hard +"to-readable-stream@npm:^1.0.0": + version: 1.0.0 + resolution: "to-readable-stream@npm:1.0.0" + checksum: 10/a99e23d49777d9d03686f03cc0bbbcb4648d991648990a98bc93b55cf91a2ae830c41b5efa36802f1c00a34bba93bd33b10346772fd3f49bcf1667a99c85f354 + languageName: node + linkType: hard + "to-regex-range@npm:^5.0.1": version: 5.0.1 resolution: "to-regex-range@npm:5.0.1" @@ -50778,6 +51588,15 @@ __metadata: languageName: node linkType: hard +"type-check@npm:~0.3.2": + version: 0.3.2 + resolution: "type-check@npm:0.3.2" + dependencies: + prelude-ls: "npm:~1.1.2" + checksum: 10/11dec0b50d7c3fd2e630b4b074ba36918ed2b1efbc87dfbd40ba9429d49c58d12dad5c415ece69fcf358fa083f33466fc370f23ab91aa63295c45d38b3a60dda + languageName: node + linkType: hard + "type-detect@npm:4.0.8": version: 4.0.8 resolution: "type-detect@npm:4.0.8" @@ -51000,7 +51819,7 @@ __metadata: languageName: node linkType: hard -"typescript@npm:^5.2.2, typescript@npm:~5.8.3": +"typescript@npm:~5.8.3": version: 5.8.3 resolution: "typescript@npm:5.8.3" bin: @@ -51010,7 +51829,7 @@ __metadata: languageName: node linkType: hard -"typescript@patch:typescript@npm%3A^5.2.2#optional!builtin, typescript@patch:typescript@npm%3A~5.8.3#optional!builtin": +"typescript@patch:typescript@npm%3A~5.8.3#optional!builtin": version: 5.8.3 resolution: "typescript@patch:typescript@npm%3A5.8.3#optional!builtin::version=5.8.3&hash=5786d5" bin: @@ -51020,6 +51839,37 @@ __metadata: languageName: node linkType: hard +"typewriter@npm:9.2.0": + version: 9.2.0 + resolution: "typewriter@npm:9.2.0" + dependencies: + "@npmcli/ci-detect": "npm:^2.0.0" + "@oclif/core": "npm:^1.7" + "@segment/analytics-node": "npm:^1.0.0-beta.26" + ajv: "npm:^8.11.0" + chalk: "npm:^4" + debug: "npm:^4.3.4" + figures: "npm:^3" + got: "npm:^11" + handlebars: "npm:^4.7.7" + inquirer: "npm:^8.2.3" + joi: "npm:^17.6.0" + json-stable-stringify: "npm:^1.0.1" + latest-version: "npm:^5" + lodash: "npm:^4.17" + node-machine-id: "npm:^1.1.12" + quicktype-core: "npm:^6.0.71" + semver: "npm:^7.3" + sort-keys: "npm:^4.2.0" + terminal-link: "npm:^2" + typescript: "npm:*" + yaml: "npm:^2.3.1" + bin: + typewriter: bin/run + checksum: 10/5a2c2b651eb77e8b5eb75e363202a5e083648978f8fdb182e6b951c2835e2ac65f743564ef42b18f7e358ad36c1053d6561a07f208fecffab5cb45fefb11f83d + languageName: node + linkType: hard + "ua-is-frozen@npm:^0.1.2": version: 0.1.2 resolution: "ua-is-frozen@npm:0.1.2" @@ -51234,6 +52084,16 @@ __metadata: languageName: node linkType: hard +"unicode-trie@npm:^0.3.0": + version: 0.3.1 + resolution: "unicode-trie@npm:0.3.1" + dependencies: + pako: "npm:^0.2.5" + tiny-inflate: "npm:^1.0.0" + checksum: 10/5b3d6beb85d75984011d3795ff7b1168c3b8b8fcc5a7042f3f51d8d71a0db6a99efe7a0d4407517577ea8fbcb2c948b84944d0386c0809a4d9db04dc246c2d51 + languageName: node + linkType: hard + "unidragger@npm:^3.0.0": version: 3.0.1 resolution: "unidragger@npm:3.0.1" @@ -51665,6 +52525,15 @@ __metadata: languageName: node linkType: hard +"url-parse-lax@npm:^3.0.0": + version: 3.0.0 + resolution: "url-parse-lax@npm:3.0.0" + dependencies: + prepend-http: "npm:^2.0.0" + checksum: 10/1040e357750451173132228036aff1fd04abbd43eac1fb3e4fca7495a078bcb8d33cb765fe71ad7e473d9c94d98fd67adca63bd2716c815a2da066198dd37217 + languageName: node + linkType: hard + "url-parse@npm:1.5.9": version: 1.5.9 resolution: "url-parse@npm:1.5.9" @@ -52215,6 +53084,13 @@ __metadata: languageName: node linkType: hard +"vlq@npm:^0.2.2": + version: 0.2.3 + resolution: "vlq@npm:0.2.3" + checksum: 10/2231d8caeb5b2c1a438677ab029e9a94aa6fb61ab05819c72691b792aea0456dab29576aff5ae29309ee45bad0a309e832dc45173119bca1393f3b87709d8f8d + languageName: node + linkType: hard + "vlq@npm:^1.0.0": version: 1.0.1 resolution: "vlq@npm:1.0.1" @@ -52690,7 +53566,7 @@ __metadata: languageName: node linkType: hard -"whatwg-fetch@npm:^3.0.0, whatwg-fetch@npm:^3.4.1": +"whatwg-fetch@npm:>=0.10.0, whatwg-fetch@npm:^3.0.0, whatwg-fetch@npm:^3.4.1": version: 3.6.20 resolution: "whatwg-fetch@npm:3.6.20" checksum: 10/2b4ed92acd6a7ad4f626a6cb18b14ec982bbcaf1093e6fe903b131a9c6decd14d7f9c9ca3532663c2759d1bdf01d004c77a0adfb2716a5105465c20755a8c57c @@ -52943,7 +53819,7 @@ __metadata: languageName: node linkType: hard -"word-wrap@npm:^1.2.5": +"word-wrap@npm:^1.2.5, word-wrap@npm:~1.2.3": version: 1.2.5 resolution: "word-wrap@npm:1.2.5" checksum: 10/1ec6f6089f205f83037be10d0c4b34c9183b0b63fca0834a5b3cee55dd321429d73d40bb44c8fc8471b5203d6e8f8275717f49a8ff4b2b0ab41d7e1b563e0854 @@ -52981,7 +53857,7 @@ __metadata: languageName: node linkType: hard -"wrap-ansi@npm:^6.2.0": +"wrap-ansi@npm:^6.0.1, wrap-ansi@npm:^6.2.0": version: 6.2.0 resolution: "wrap-ansi@npm:6.2.0" dependencies: @@ -53320,7 +54196,7 @@ __metadata: languageName: node linkType: hard -"xtend@npm:^4.0.0, xtend@npm:^4.0.1, xtend@npm:^4.0.2, xtend@npm:~4.0.0": +"xtend@npm:^4.0.0, xtend@npm:^4.0.1, xtend@npm:^4.0.2, xtend@npm:~4.0.0, xtend@npm:~4.0.1": version: 4.0.2 resolution: "xtend@npm:4.0.2" checksum: 10/ac5dfa738b21f6e7f0dd6e65e1b3155036d68104e67e5d5d1bde74892e327d7e5636a076f625599dc394330a731861e87343ff184b0047fef1360a7ec0a5a36a @@ -53395,6 +54271,22 @@ __metadata: languageName: node linkType: hard +"yaml@npm:^1.5.0": + version: 1.10.3 + resolution: "yaml@npm:1.10.3" + checksum: 10/e2ef2feb92c708138f016c69777a0f1e45f6d3c5e7cbcda30807a98a37eda2e008bd4fa57352b043c65245a4c799d0c99d1f9b3425de40e70929e26d2ea38215 + languageName: node + linkType: hard + +"yaml@npm:^2.3.1": + version: 2.9.1 + resolution: "yaml@npm:2.9.1" + bin: + yaml: bin.mjs + checksum: 10/123c5e56a26f4962d3d5954971b2071a533d17041e00ae9ff6a5e634aed2503639bbdeea143b1610eac29613aad95a3a1d10f5fbda87b92203c039ce63934295 + languageName: node + linkType: hard + "yargs-parser@npm:22.0.0, yargs-parser@npm:^22.0.0": version: 22.0.0 resolution: "yargs-parser@npm:22.0.0" From e2c57d0766baf6f99f9c6b98a81182ff8675b8db Mon Sep 17 00:00:00 2001 From: jvbriones <1674192+jvbriones@users.noreply.github.com> Date: Mon, 28 Sep 2026 16:25:36 +0200 Subject: [PATCH 2/7] chore(MMQA-1951): Use typed analytics schema contract - Evaluation --- .github/workflows/check-analytics-contract.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/check-analytics-contract.yml b/.github/workflows/check-analytics-contract.yml index 9015baa4f162..1bdcd2b3d1fc 100644 --- a/.github/workflows/check-analytics-contract.yml +++ b/.github/workflows/check-analytics-contract.yml @@ -31,8 +31,8 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 15 outputs: - release-tag: ${{ steps.check.outputs.release-tag }} - release-url: ${{ steps.check.outputs.release-url }} + release-tag: ${{ steps.shared-contract.outputs.release-tag }} + release-url: ${{ steps.shared-contract.outputs.release-url }} steps: - name: Checkout Mobile uses: actions/checkout@v6 From 198cb7e15e86584780b11ab4a0e3879d033ce2c7 Mon Sep 17 00:00:00 2001 From: jvbriones <1674192+jvbriones@users.noreply.github.com> Date: Mon, 28 Sep 2026 16:46:32 +0200 Subject: [PATCH 3/7] chore(MMQA-1951): Use typed analytics schema contract - Evaluation --- .../generated/QuickBuyAmountSelected.test.ts | 52 +++++++++++++++++++ .../generated/QuickBuyAmountSelected.ts | 2 +- scripts/analytics/typed-events/README.md | 4 +- .../analytics/typed-events/check-contract.ts | 2 + .../analytics/typed-events/generate-pilot.ts | 2 + 5 files changed, 60 insertions(+), 2 deletions(-) create mode 100644 app/util/analytics/generated/QuickBuyAmountSelected.test.ts diff --git a/app/util/analytics/generated/QuickBuyAmountSelected.test.ts b/app/util/analytics/generated/QuickBuyAmountSelected.test.ts new file mode 100644 index 000000000000..b14b7d038230 --- /dev/null +++ b/app/util/analytics/generated/QuickBuyAmountSelected.test.ts @@ -0,0 +1,52 @@ +import { createQuickBuyAmountSelected } from './QuickBuyAmountSelected'; + +describe('createQuickBuyAmountSelected', () => { + it('tracks v1 with the event version context', () => { + const transport = { + track: jest.fn(), + }; + const event = createQuickBuyAmountSelected(transport); + const properties = { + amount_selection_method: 'preset' as const, + amount_usd: 50, + source: 'notification' as const, + }; + + event.v1.track(properties); + + expect(transport.track).toHaveBeenCalledWith( + 'Quick Buy Amount Selected', + properties, + { + protocols: { + event_version: 1, + }, + }, + ); + }); + + it('tracks v2 with the event version context', () => { + const transport = { + track: jest.fn(), + }; + const event = createQuickBuyAmountSelected(transport); + const properties = { + amount_selection_method: 'slider' as const, + amount_usd: 50, + slider_percent: 25, + source: 'trader_feed' as const, + }; + + event.v2.track(properties); + + expect(transport.track).toHaveBeenCalledWith( + 'Quick Buy Amount Selected', + properties, + { + protocols: { + event_version: 2, + }, + }, + ); + }); +}); diff --git a/app/util/analytics/generated/QuickBuyAmountSelected.ts b/app/util/analytics/generated/QuickBuyAmountSelected.ts index 0451629ab036..cda6c3c93f07 100644 --- a/app/util/analytics/generated/QuickBuyAmountSelected.ts +++ b/app/util/analytics/generated/QuickBuyAmountSelected.ts @@ -27,7 +27,7 @@ interface EventVersion { ): void; } -const createEventVersion = ( +const createEventVersion = ( transport: AnalyticsEventTransport, eventName: string, version: number, diff --git a/scripts/analytics/typed-events/README.md b/scripts/analytics/typed-events/README.md index 209243cb299f..359431342d07 100644 --- a/scripts/analytics/typed-events/README.md +++ b/scripts/analytics/typed-events/README.md @@ -57,7 +57,9 @@ the ignored `temp/analytics-contract-comparison/` directory. The reviewed Quick Buy facade is tracked publicly under `app/util/analytics/generated/`. CI regenerates it and compares the result to -the tracked files. +the tracked files. The `.test-d.ts` file checks compile-time contracts; the +generated `.test.ts` file covers runtime event-name and version-context +forwarding. The command fails if release verification, Quick Buy v1/v2 generation, or the generated TypeScript checks fail. diff --git a/scripts/analytics/typed-events/check-contract.ts b/scripts/analytics/typed-events/check-contract.ts index 017b2af9994b..c3d5ee207dd9 100644 --- a/scripts/analytics/typed-events/check-contract.ts +++ b/scripts/analytics/typed-events/check-contract.ts @@ -286,6 +286,7 @@ const writeGeneratedFiles = async ( await format(generated.source, { parser: 'typescript', singleQuote: true, + filepath: join(outputDirectory, 'QuickBuyAmountSelected.ts'), }), ); writeFileSync( @@ -293,6 +294,7 @@ const writeGeneratedFiles = async ( await format(generated.typeTests, { parser: 'typescript', singleQuote: true, + filepath: join(outputDirectory, 'QuickBuyAmountSelected.test-d.ts'), }), ); }; diff --git a/scripts/analytics/typed-events/generate-pilot.ts b/scripts/analytics/typed-events/generate-pilot.ts index 77f2027a4559..aa5020f1468f 100644 --- a/scripts/analytics/typed-events/generate-pilot.ts +++ b/scripts/analytics/typed-events/generate-pilot.ts @@ -67,6 +67,7 @@ const main = async (): Promise => { await format(generated.source, { parser: 'typescript', singleQuote: true, + filepath: sourcePath, }), ); writeFileSync( @@ -74,6 +75,7 @@ const main = async (): Promise => { await format(generated.typeTests, { parser: 'typescript', singleQuote: true, + filepath: typeTestsPath, }), ); From 88c8ea20d87ab5eb84a3864798ef9e6ffc6b843a Mon Sep 17 00:00:00 2001 From: jvbriones <1674192+jvbriones@users.noreply.github.com> Date: Tue, 29 Sep 2026 12:04:58 +0200 Subject: [PATCH 4/7] chore(MMQA-1951): remove Typewriter evaluation --- docs/readme/typed-analytics-contracts.md | 25 +- package.json | 3 - scripts/analytics/typed-events/README.md | 11 - .../typed-events/compare-generators.ts | 221 ---- .../typewriter-comparison.test.ts | 81 -- .../typed-events/typewriter-comparison.ts | 112 -- yarn.lock | 970 +----------------- 7 files changed, 45 insertions(+), 1378 deletions(-) delete mode 100644 scripts/analytics/typed-events/compare-generators.ts delete mode 100644 scripts/analytics/typed-events/typewriter-comparison.test.ts delete mode 100644 scripts/analytics/typed-events/typewriter-comparison.ts diff --git a/docs/readme/typed-analytics-contracts.md b/docs/readme/typed-analytics-contracts.md index 1bcc464e2f29..ecf657c5d62e 100644 --- a/docs/readme/typed-analytics-contracts.md +++ b/docs/readme/typed-analytics-contracts.md @@ -87,25 +87,12 @@ The reviewed Quick Buy facade is public and tracked at CI regenerates the facade from the downloaded contract and fails if the tracked types drift from the deployed contract. -## Typewriter comparison +## Generator decision -Run the local comparison against the same pinned Quick Buy v1/v2 fixture: - -```sh -yarn analytics:contract:compare -``` - -This uses pinned Typewriter 9.2.0 with TypeScript and -`analytics-react-native`. It reads a temporary local `plan.json`, does not -update Segment, and does not require Segment credentials. Typewriter output, -the adapted plan, and `comparison.json` remain under the ignored -`temp/analytics-contract-comparison/` directory. - -The comparison is reference material, not production output. Stock Typewriter -normalizes Track rules and retains only the highest version for a repeated event -key. The strict pilot preserves explicit v1 and v2 APIs and adds the version -context. The comparison also records permissive index signatures, event-version -context, documentation/deprecation markers, and progressive-builder support. +The Quick Buy pilot uses the narrow strict generator. It preserves explicit +event versions, rejects unknown properties, and adds the event-version context. +The generator comparison was a time-boxed design spike; its temporary +comparison tooling is not part of the production dependency graph. ## Quick Buy pilot and versions @@ -148,7 +135,7 @@ provenance and a broader JSON Schema support matrix. ## Planned progression 1. Merge and observe the incubation workflow. -2. Review the pinned lock and local Typewriter comparison. +2. Review the pinned lock and generator decision. 3. Add optional `AnalyticsContext` plumbing through Mobile's existing analytics queue and adapter. 4. Integrate the generated facade into one reviewed Quick Buy production call. diff --git a/package.json b/package.json index 92971ca52ff8..b1114a48a407 100644 --- a/package.json +++ b/package.json @@ -114,7 +114,6 @@ "test:unit": "jest ./app/ ./locales/ ./tests/**/*.test.ts .github/**/*.test.ts ./scripts/**/*.test.ts --testPathIgnorePatterns='.*/tests/smoke-appium/.*\\.spec\\.(ts|tsx|js)$|.*/e2e/.*\\.spec\\.(ts|tsx|js)$|.*\\.integration\\.test\\.(ts|tsx|js|jsx)$|.*/tests/llm-workflow/.*\\.test\\.(ts|tsx|js)$|.*\\.view(\\..*)?\\.test\\.(ts|tsx|js|jsx)$'", "analytics:contract:check": "tsx scripts/analytics/typed-events/check-contract.ts", "analytics:contract:check:locked": "tsx scripts/analytics/typed-events/check-contract.ts --lock-file scripts/analytics/typed-events/contract.lock.json", - "analytics:contract:compare": "tsx scripts/analytics/typed-events/compare-generators.ts", "test:unit:llm-workflow": "jest -c jest.config.llm-workflow.js", "test:smart-e2e": "tsx --test .ai-pr-analyzer/modes/smart-e2e/*.test.ts .github/actions/smart-e2e-selection/*.test.mjs", "mm:doctor": "tsx tests/llm-workflow/doctor-entry.ts", @@ -216,7 +215,6 @@ "ganache/secp256k1": "^4.0.4", "@expo/image-utils/semver": "7.5.2", "eas-cli/nanoid": "3.3.18", - "typescript": "~5.8.3", "nanoid@npm:^3.0.0": "3.3.18", "nanoid@npm:^5.0.0": "5.1.16", "fast-uri": "3.1.5", @@ -796,7 +794,6 @@ "ts-node": "^10.9.2", "tsx": "^4.21.0", "typescript": "~5.8.3", - "typewriter": "9.2.0", "webdriverio": "^9.27.0", "webextension-polyfill": "^0.12.0", "webpack": "^5.88.2", diff --git a/scripts/analytics/typed-events/README.md b/scripts/analytics/typed-events/README.md index 359431342d07..579f0b337ceb 100644 --- a/scripts/analytics/typed-events/README.md +++ b/scripts/analytics/typed-events/README.md @@ -44,17 +44,6 @@ yarn analytics:contract:check \ --output-directory temp/my-analytics-contract-check ``` -Compare the strict pilot with pinned stock Typewriter locally: - -```sh -yarn analytics:contract:compare -``` - -The comparison uses TypeScript with `analytics-react-native`, the exact lock -fixture, and a local `plan.json`. It does not update Segment or require a -Segment API token. Typewriter output and the comparison report remain under -the ignored `temp/analytics-contract-comparison/` directory. - The reviewed Quick Buy facade is tracked publicly under `app/util/analytics/generated/`. CI regenerates it and compares the result to the tracked files. The `.test-d.ts` file checks compile-time contracts; the diff --git a/scripts/analytics/typed-events/compare-generators.ts b/scripts/analytics/typed-events/compare-generators.ts deleted file mode 100644 index 664b7ff5367f..000000000000 --- a/scripts/analytics/typed-events/compare-generators.ts +++ /dev/null @@ -1,221 +0,0 @@ -import { execFileSync } from 'node:child_process'; -import { mkdirSync, rmSync, writeFileSync } from 'node:fs'; -import { join, relative } from 'node:path'; -import { - checkAnalyticsContract, - type ContractCheckResult, -} from './check-contract'; -import { verifyMobileContract } from './contract'; -import { QUICK_BUY_AMOUNT_SELECTED_PILOT } from './quick-buy-pilot'; -import { - readTypewriterOutput, - writeTypewriterFixture, -} from './typewriter-comparison'; - -const DEFAULT_LOCK_FILE = join( - process.cwd(), - 'scripts', - 'analytics', - 'typed-events', - 'contract.lock.json', -); -const DEFAULT_OUTPUT_DIRECTORY = join( - process.cwd(), - 'temp', - 'analytics-contract-comparison', -); - -interface ComparisonOptions { - readonly lockFilePath: string; - readonly outputDirectory: string; -} - -interface GeneratorComparisonReport { - readonly contract: { - readonly contractHash: string; - readonly releaseTag: string; - readonly sourceSha: string; - }; - readonly event: { - readonly name: string; - readonly versions: readonly number[]; - }; - readonly narrowGenerator: { - readonly exactPropertyCheck: boolean; - readonly eventVersionContext: boolean; - readonly sourcePath: string; - readonly typeTestsPath: string; - }; - readonly typewriter: { - readonly deprecationAnnotationsFound: boolean; - readonly eventVersionContextGenerated: boolean; - readonly generatedFiles: readonly string[]; - readonly generatedSourceBytes: number; - readonly eventNameFound: boolean; - readonly permissiveIndexSignature: boolean; - readonly expectedRetainedVersions: readonly number[]; - readonly progressiveBuilderApiFound: boolean; - readonly sdk: 'analytics-react-native'; - }; - readonly limitations: readonly string[]; -} - -const readOptions = (args: readonly string[]): ComparisonOptions => { - const options = new Map(); - - for (let index = 0; index < args.length; index++) { - const argument = args[index]; - - if (!argument?.startsWith('--')) { - throw new Error(`Unexpected argument '${argument ?? ''}'`); - } - - const value = args[index + 1]; - - if (!value || value.startsWith('--')) { - throw new Error(`Missing value for '${argument}'`); - } - - options.set(argument.slice(2), value); - index++; - } - - return { - lockFilePath: options.get('lock-file') ?? DEFAULT_LOCK_FILE, - outputDirectory: options.get('output-directory') ?? DEFAULT_OUTPUT_DIRECTORY, - }; -}; - -const toRelativePath = (filePath: string): string => - relative(process.cwd(), filePath); - -const createReport = ( - checkResult: ContractCheckResult, - outputDirectory: string, - typewriterSource: string, - typewriterFiles: readonly string[], -): GeneratorComparisonReport => { - const versions = QUICK_BUY_AMOUNT_SELECTED_PILOT.versions.map( - ({ version }) => version, - ); - const latestVersion = Math.max(...versions); - - return { - contract: { - contractHash: checkResult.verification.contractHash, - releaseTag: checkResult.release.tag, - sourceSha: checkResult.verification.sourceSha, - }, - event: { - name: QUICK_BUY_AMOUNT_SELECTED_PILOT.eventName, - versions, - }, - narrowGenerator: { - exactPropertyCheck: true, - eventVersionContext: true, - sourcePath: toRelativePath( - join(outputDirectory, 'generated', 'QuickBuyAmountSelected.ts'), - ), - typeTestsPath: toRelativePath( - join(outputDirectory, 'generated', 'QuickBuyAmountSelected.test-d.ts'), - ), - }, - typewriter: { - deprecationAnnotationsFound: typewriterSource.includes('@deprecated'), - eventVersionContextGenerated: typewriterSource.includes('event_version'), - generatedFiles: typewriterFiles.map(toRelativePath), - generatedSourceBytes: Buffer.byteLength(typewriterSource, 'utf8'), - eventNameFound: typewriterSource.includes( - QUICK_BUY_AMOUNT_SELECTED_PILOT.eventName, - ), - permissiveIndexSignature: typewriterSource.includes( - '[property: string]: any;', - ), - expectedRetainedVersions: [latestVersion], - progressiveBuilderApiFound: - typewriterSource.includes('.create(') && - typewriterSource.includes('setProperties'), - sdk: 'analytics-react-native', - }, - limitations: [ - 'Stock Typewriter filters the fixture to TRACK rules.', - 'Stock Typewriter retains only the highest version for one event key.', - 'The comparison does not treat Typewriter output as production code.', - 'Progressive event construction and runtime completeness remain follow-up API work.', - ], - }; -}; - -const main = async (): Promise => { - const options = readOptions(process.argv.slice(2)); - - mkdirSync(options.outputDirectory, { recursive: true }); - rmSync(join(options.outputDirectory, 'typewriter'), { - force: true, - recursive: true, - }); - - const checkResult = await checkAnalyticsContract({ - keep: true, - lockFilePath: options.lockFilePath, - outputDirectory: options.outputDirectory, - }); - const workDirectory = checkResult.workDirectory; - - if (!workDirectory) { - throw new Error('Expected retained contract artifacts for comparison'); - } - - const verifiedContract = verifyMobileContract({ - manifestPath: join(workDirectory, 'contract', 'manifest.json'), - contractPath: join(workDirectory, 'contract', 'metamask-mobile.json'), - expectedAssetSha256: checkResult.verification.assetSha256, - eventName: QUICK_BUY_AMOUNT_SELECTED_PILOT.eventName, - versions: QUICK_BUY_AMOUNT_SELECTED_PILOT.versions.map( - ({ version }) => version, - ), - }); - const typewriterDirectory = join(workDirectory, 'typewriter'); - const typewriterFixture = writeTypewriterFixture( - verifiedContract.contract, - verifiedContract.eventRules, - typewriterDirectory, - ); - const yarnCommand = process.platform === 'win32' ? 'yarn.cmd' : 'yarn'; - - execFileSync( - yarnCommand, - [ - 'typewriter', - 'build', - '--config', - typewriterFixture.configPath, - '--mode', - 'prod', - ], - { - stdio: 'inherit', - }, - ); - - const typewriterOutput = readTypewriterOutput( - typewriterFixture.generatedDirectory, - ); - const report = createReport( - checkResult, - workDirectory, - typewriterOutput.source, - typewriterOutput.files, - ); - const reportPath = join(workDirectory, 'comparison.json'); - - writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); - console.log(JSON.stringify({ ...report, reportPath }, null, 2)); -}; - -if (process.argv[1]?.endsWith('compare-generators.ts')) { - main().catch((error: unknown) => { - console.error(String(error)); - process.exitCode = 1; - }); -} diff --git a/scripts/analytics/typed-events/typewriter-comparison.test.ts b/scripts/analytics/typed-events/typewriter-comparison.test.ts deleted file mode 100644 index f9093a791e08..000000000000 --- a/scripts/analytics/typed-events/typewriter-comparison.test.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; -import { tmpdir } from 'node:os'; -import { basename, join } from 'node:path'; -import type { ContractRule, MobileContract } from './contract'; -import { - readTypewriterOutput, - writeTypewriterFixture, -} from './typewriter-comparison'; - -const createContract = (): MobileContract => ({ - formatVersion: 1, - generatedAt: '2026-09-25T00:00:00.000Z', - hash: `sha256:${'a'.repeat(64)}`, - platform: 'mobile', - ruleCount: 2, - rules: [], - sourceSha: 'a'.repeat(40), - trackingPlan: { - id: 'tp_test', - name: 'MetaMask Mobile', - type: 'LIVE', - updatedAt: '2026-09-25T00:00:00.000Z', - }, -}); - -const createRule = (version: number): ContractRule => ({ - key: 'Quick Buy Amount Selected', - type: 'TRACK', - version, - jsonSchema: { - type: 'object', - properties: {}, - }, - deprecatedAt: null, -}); - -describe('Typewriter comparison fixture', () => { - const roots: string[] = []; - - afterEach(() => { - roots.splice(0).forEach((root) => rmSync(root, { recursive: true, force: true })); - }); - - it('writes a local React Native Typewriter fixture with both versions', () => { - const root = mkdtempSync(join(tmpdir(), 'analytics-typewriter-')); - roots.push(root); - const paths = writeTypewriterFixture( - createContract(), - [createRule(1), createRule(2)], - root, - ); - - expect(paths.configPath).toContain('typewriter.yml'); - expect(paths.generatedDirectory).toContain('/generated'); - expect(paths.planPath).toContain('/generated/plan.json'); - expect(readFileSync(paths.configPath, 'utf8')).toContain( - 'sdk: analytics-react-native', - ); - expect( - (JSON.parse(readFileSync(paths.planPath, 'utf8')) as { rules: unknown[] }) - .rules, - ).toHaveLength(2); - }); - - it('reads TypeScript and TSX Typewriter output deterministically', () => { - const root = mkdtempSync(join(tmpdir(), 'analytics-typewriter-')); - roots.push(root); - writeFileSync(join(root, 'z.ts'), 'export const z = true;'); - writeFileSync(join(root, 'a.tsx'), 'export const a = true;'); - - const result = readTypewriterOutput(root); - - expect(result.files.map((filePath) => basename(filePath))).toEqual([ - 'a.tsx', - 'z.ts', - ]); - expect(result.source).toBe( - 'export const a = true;\nexport const z = true;', - ); - }); -}); diff --git a/scripts/analytics/typed-events/typewriter-comparison.ts b/scripts/analytics/typed-events/typewriter-comparison.ts deleted file mode 100644 index 616ec63130c1..000000000000 --- a/scripts/analytics/typed-events/typewriter-comparison.ts +++ /dev/null @@ -1,112 +0,0 @@ -import { - mkdirSync, - readFileSync, - readdirSync, - writeFileSync, -} from 'node:fs'; -import { join } from 'node:path'; -import type { ContractRule, MobileContract } from './contract'; - -export interface TypewriterFixturePaths { - readonly configPath: string; - readonly generatedDirectory: string; - readonly planPath: string; -} - -export interface TypewriterOutput { - readonly files: readonly string[]; - readonly source: string; -} - -interface TypewriterPlan { - readonly createdAt: string; - readonly description: string; - readonly id: string; - readonly name: string; - readonly rules: readonly ContractRule[]; - readonly slug: string; - readonly updatedAt: string | null; -} - -const readFileContents = (filePath: string): string => - readFileSync(filePath, 'utf8'); - -const TYPEWRITER_CONFIG = (trackingPlanId: string): string => `client: - language: typescript - sdk: analytics-react-native - languageOptions: {} -trackingPlans: - - id: ${JSON.stringify(trackingPlanId)} - path: ./generated -`; - -const createTypewriterPlan = ( - contract: MobileContract, - rules: readonly ContractRule[], -): TypewriterPlan => ({ - createdAt: contract.generatedAt, - description: '', - id: contract.trackingPlan.id, - name: contract.trackingPlan.name, - rules, - slug: 'metamask-mobile', - updatedAt: contract.trackingPlan.updatedAt, -}); - -/** - * Write the local Typewriter inputs for one verified contract fixture. - * - * @param contract - Verified Mobile contract. - * @param rules - The exact event/version fixture being compared. - * @param directory - Temporary Typewriter workspace. - * @returns Paths consumed by the Typewriter CLI. - */ -export const writeTypewriterFixture = ( - contract: MobileContract, - rules: readonly ContractRule[], - directory: string, -): TypewriterFixturePaths => { - const generatedDirectory = join(directory, 'generated'); - const configPath = join(directory, 'typewriter.yml'); - const planPath = join(generatedDirectory, 'plan.json'); - - mkdirSync(generatedDirectory, { recursive: true }); - writeFileSync( - configPath, - TYPEWRITER_CONFIG(contract.trackingPlan.id), - ); - writeFileSync( - planPath, - `${JSON.stringify(createTypewriterPlan(contract, rules), null, 2)}\n`, - ); - - return { - configPath, - generatedDirectory, - planPath, - }; -}; - -/** - * Read TypeScript files generated by Typewriter. - * - * @param directory - Typewriter output directory. - * @returns Generated file paths and concatenated source. - */ -export const readTypewriterOutput = (directory: string): TypewriterOutput => { - const files = readdirSync(directory) - .filter( - (fileName) => fileName.endsWith('.ts') || fileName.endsWith('.tsx'), - ) - .sort() - .map((fileName) => join(directory, fileName)); - - if (files.length === 0) { - throw new Error(`Typewriter generated no TypeScript files in '${directory}'`); - } - - return { - files, - source: files.map((filePath) => readFileContents(filePath)).join('\n'), - }; -}; diff --git a/yarn.lock b/yarn.lock index d9ce1bfb2679..b83fb8b0ef4e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7695,21 +7695,6 @@ __metadata: languageName: node linkType: hard -"@inquirer/external-editor@npm:^1.0.0": - version: 1.0.3 - resolution: "@inquirer/external-editor@npm:1.0.3" - dependencies: - chardet: "npm:^2.1.1" - iconv-lite: "npm:^0.7.0" - peerDependencies: - "@types/node": ">=18" - peerDependenciesMeta: - "@types/node": - optional: true - checksum: 10/c95d7237a885b32031715089f92820525731d4d3c2bd7afdb826307dc296cc2b39e7a644b0bb265441963348cca42e7785feb29c3aaf18fd2b63131769bf6587 - languageName: node - linkType: hard - "@inversifyjs/common@npm:1.5.0": version: 1.5.0 resolution: "@inversifyjs/common@npm:1.5.0" @@ -9017,22 +9002,6 @@ __metadata: languageName: node linkType: hard -"@lukeed/csprng@npm:^1.1.0": - version: 1.1.0 - resolution: "@lukeed/csprng@npm:1.1.0" - checksum: 10/926f5f7fc629470ca9a8af355bfcd0271d34535f7be3890f69902432bddc3262029bb5dbe9025542cf6c9883d878692eef2815fc2f3ba5b92e9da1f9eba2e51b - languageName: node - linkType: hard - -"@lukeed/uuid@npm:^2.0.0": - version: 2.0.1 - resolution: "@lukeed/uuid@npm:2.0.1" - dependencies: - "@lukeed/csprng": "npm:^1.1.0" - checksum: 10/f5e71e4da852dbff49b93cad27d5a2f61c2241e307bbe89b3b54b889ecb7927f2487246467f90ebb6cbdb7e0ac2a213e2e58b1182cb7990cef6e049aa7c39e7b - languageName: node - linkType: hard - "@mantine/core@npm:^7.8.0": version: 7.8.0 resolution: "@mantine/core@npm:7.8.0" @@ -9069,16 +9038,6 @@ __metadata: languageName: node linkType: hard -"@mark.probst/unicode-properties@npm:~1.1.0": - version: 1.1.0 - resolution: "@mark.probst/unicode-properties@npm:1.1.0" - dependencies: - brfs: "npm:^1.4.0" - unicode-trie: "npm:^0.3.0" - checksum: 10/ea42bd3c17d72ddafff34009ff08e7478e89d1b7b24b06ac18ec0c1d70f62889b0fe255695427b9f2251c2a73e19b0f091eda3af44cef12c50d443e009849e7c - languageName: node - linkType: hard - "@mdx-js/mdx@npm:^3.0.0": version: 3.1.1 resolution: "@mdx-js/mdx@npm:3.1.1" @@ -13071,13 +13030,6 @@ __metadata: languageName: node linkType: hard -"@npmcli/ci-detect@npm:^2.0.0": - version: 2.0.0 - resolution: "@npmcli/ci-detect@npm:2.0.0" - checksum: 10/26e964eca908706c1a612915cbc5614860ac7dbfacbb07870396c82b1377794f123a7aaa821c4a68575b67ff7e3ad170e296d3aa6a5e03dbab9b3f1e61491812 - languageName: node - linkType: hard - "@npmcli/fs@npm:^3.1.0": version: 3.1.0 resolution: "@npmcli/fs@npm:3.1.0" @@ -13156,7 +13108,7 @@ __metadata: languageName: node linkType: hard -"@oclif/core@npm:^1.26.2, @oclif/core@npm:^1.7": +"@oclif/core@npm:^1.26.2": version: 1.26.2 resolution: "@oclif/core@npm:1.26.2" dependencies: @@ -15945,41 +15897,6 @@ __metadata: languageName: node linkType: hard -"@segment/analytics-core@npm:1.4.1": - version: 1.4.1 - resolution: "@segment/analytics-core@npm:1.4.1" - dependencies: - "@lukeed/uuid": "npm:^2.0.0" - "@segment/analytics-generic-utils": "npm:1.1.1" - dset: "npm:^3.1.2" - tslib: "npm:^2.4.1" - checksum: 10/15b2279a48ee10be30d5ae164d338b882c75e031d55472eab22debaa6ff7f55df0d38f10197b760c1cbafec97e243400fec50ecf21587a88170c94346b2ed397 - languageName: node - linkType: hard - -"@segment/analytics-generic-utils@npm:1.1.1": - version: 1.1.1 - resolution: "@segment/analytics-generic-utils@npm:1.1.1" - dependencies: - tslib: "npm:^2.4.1" - checksum: 10/994181cfd2cb9492ac2bcf51b6347a4f2cc5ea5c96522f39d3a431efcbe9f6c1912171e1d7153d086f614703fac5e2026226c07cec8965d60678c9e984501e82 - languageName: node - linkType: hard - -"@segment/analytics-node@npm:^1.0.0-beta.26": - version: 1.3.0 - resolution: "@segment/analytics-node@npm:1.3.0" - dependencies: - "@lukeed/uuid": "npm:^2.0.0" - "@segment/analytics-core": "npm:1.4.1" - "@segment/analytics-generic-utils": "npm:1.1.1" - buffer: "npm:^6.0.3" - node-fetch: "npm:^2.6.7" - tslib: "npm:^2.4.1" - checksum: 10/438f5c029355314ece4373399e338849afc8df74fea96399737d4a0e367295b3880b394338441216ac791777b72fdfa9839d12194a09839cc10b260a4c4aab9c - languageName: node - linkType: hard - "@segment/analytics-react-native@npm:2.23.0": version: 2.23.0 resolution: "@segment/analytics-react-native@npm:2.23.0" @@ -16543,14 +16460,7 @@ __metadata: languageName: node linkType: hard -"@sindresorhus/is@npm:^0.14.0": - version: 0.14.0 - resolution: "@sindresorhus/is@npm:0.14.0" - checksum: 10/789cd128f0b43e158e657c4505539c8997905fcb5c06d750b7df778cab2b6887bc1eb8878026a20d84524528786ef69fc3d12a964ae56a478a87bcfc7f8272f3 - languageName: node - linkType: hard - -"@sindresorhus/is@npm:^4.0.0, @sindresorhus/is@npm:^4.6.0": +"@sindresorhus/is@npm:^4.6.0": version: 4.6.0 resolution: "@sindresorhus/is@npm:4.6.0" checksum: 10/e7f36ed72abfcd5e0355f7423a72918b9748bb1ef370a59f3e5ad8d40b728b85d63b272f65f63eec1faf417cda89dcb0aeebe94015647b6054659c1442fe5ce0 @@ -20463,24 +20373,6 @@ __metadata: languageName: node linkType: hard -"@szmarczak/http-timer@npm:^1.1.2": - version: 1.1.2 - resolution: "@szmarczak/http-timer@npm:1.1.2" - dependencies: - defer-to-connect: "npm:^1.0.1" - checksum: 10/9b63853bd53bff72c4990ebc9cd3f625bbab757247099af172564da6649a27a1d41b1a70cd849dd65b2a078300029c1c80bf3079e6a91e285da7b259eb147146 - languageName: node - linkType: hard - -"@szmarczak/http-timer@npm:^4.0.5": - version: 4.0.6 - resolution: "@szmarczak/http-timer@npm:4.0.6" - dependencies: - defer-to-connect: "npm:^2.0.0" - checksum: 10/c29df3bcec6fc3bdec2b17981d89d9c9fc9bd7d0c9bcfe92821dc533f4440bc890ccde79971838b4ceed1921d456973c4180d7175ee1d0023ad0562240a58d95 - languageName: node - linkType: hard - "@szmarczak/http-timer@npm:^5.0.1": version: 5.0.1 resolution: "@szmarczak/http-timer@npm:5.0.1" @@ -21350,18 +21242,6 @@ __metadata: languageName: node linkType: hard -"@types/cacheable-request@npm:^6.0.1": - version: 6.0.3 - resolution: "@types/cacheable-request@npm:6.0.3" - dependencies: - "@types/http-cache-semantics": "npm:*" - "@types/keyv": "npm:^3.1.4" - "@types/node": "npm:*" - "@types/responselike": "npm:^1.0.0" - checksum: 10/159f9fdb2a1b7175eef453ae2ced5ea04c0d2b9610cc9ccd9f9abb066d36dacb1f37acd879ace10ad7cbb649490723feb396fb7307004c9670be29636304b988 - languageName: node - linkType: hard - "@types/cli-progress@npm:^3.11.0": version: 3.11.6 resolution: "@types/cli-progress@npm:3.11.6" @@ -21647,7 +21527,7 @@ __metadata: languageName: node linkType: hard -"@types/http-cache-semantics@npm:*, @types/http-cache-semantics@npm:^4.0.2": +"@types/http-cache-semantics@npm:^4.0.2": version: 4.2.0 resolution: "@types/http-cache-semantics@npm:4.2.0" checksum: 10/01ea0dc9c1852267f5f9a0190846ac158707994b8e325bca190385ec97aa773d4a99cd333e22e838bde3c9aba59e2b5a6ac399b494c203587c6d8f28d21d6326 @@ -21765,15 +21645,6 @@ __metadata: languageName: node linkType: hard -"@types/keyv@npm:^3.1.1, @types/keyv@npm:^3.1.4": - version: 3.1.4 - resolution: "@types/keyv@npm:3.1.4" - dependencies: - "@types/node": "npm:*" - checksum: 10/e009a2bfb50e90ca9b7c6e8f648f8464067271fd99116f881073fa6fa76dc8d0133181dd65e6614d5fb1220d671d67b0124aef7d97dc02d7e342ab143a47779d - languageName: node - linkType: hard - "@types/koa-compose@npm:*": version: 3.2.8 resolution: "@types/koa-compose@npm:3.2.8" @@ -22148,15 +22019,6 @@ __metadata: languageName: node linkType: hard -"@types/responselike@npm:^1.0.0": - version: 1.0.3 - resolution: "@types/responselike@npm:1.0.3" - dependencies: - "@types/node": "npm:*" - checksum: 10/6ac4b35723429b11b117e813c7acc42c3af8b5554caaf1fc750404c1ae59f9b7376bc69b9e9e194a5a97357a597c2228b7173d317320f0360d617b6425212f58 - languageName: node - linkType: hard - "@types/retry@npm:0.12.2": version: 0.12.2 resolution: "@types/retry@npm:0.12.2" @@ -24053,15 +23915,6 @@ __metadata: languageName: node linkType: hard -"acorn@npm:^7.1.1": - version: 7.4.1 - resolution: "acorn@npm:7.4.1" - bin: - acorn: bin/acorn - checksum: 10/8be2a40714756d713dfb62544128adce3b7102c6eb94bc312af196c2cc4af76e5b93079bd66b05e9ca31b35a9b0ce12171d16bc55f366cafdb794fdab9d753ec - languageName: node - linkType: hard - "acorn@npm:^8.0.0, acorn@npm:^8.0.4, acorn@npm:^8.1.0, acorn@npm:^8.10.0, acorn@npm:^8.11.0, acorn@npm:^8.15.0, acorn@npm:^8.16.0, acorn@npm:^8.4.1, acorn@npm:^8.8.1, acorn@npm:^8.9.0": version: 8.18.0 resolution: "acorn@npm:8.18.0" @@ -24240,7 +24093,7 @@ __metadata: languageName: node linkType: hard -"ajv@npm:8.20.0, ajv@npm:^8.0.0, ajv@npm:^8.11.0, ajv@npm:^8.17.1, ajv@npm:^8.9.0": +"ajv@npm:8.20.0, ajv@npm:^8.0.0, ajv@npm:^8.17.1, ajv@npm:^8.9.0": version: 8.20.0 resolution: "ajv@npm:8.20.0" dependencies: @@ -26368,20 +26221,6 @@ __metadata: languageName: node linkType: hard -"brfs@npm:^1.4.0": - version: 1.6.1 - resolution: "brfs@npm:1.6.1" - dependencies: - quote-stream: "npm:^1.0.1" - resolve: "npm:^1.1.5" - static-module: "npm:^2.2.0" - through2: "npm:^2.0.0" - bin: - brfs: bin/cmd.js - checksum: 10/5c4458714851aaffbe2aa30d50f4bdb1da8fd2d69a92abe99d2aa3140e758b1dc62932edf6f89f0dae4b8df60af72917fdd2c19c0e866eb6c53f860cb40dd766 - languageName: node - linkType: hard - "brorand@npm:^1.0.1, brorand@npm:^1.1.0": version: 1.1.0 resolution: "brorand@npm:1.1.0" @@ -26403,13 +26242,6 @@ __metadata: languageName: node linkType: hard -"browser-or-node@npm:^1.2.1": - version: 1.3.0 - resolution: "browser-or-node@npm:1.3.0" - checksum: 10/14a7e3f7bd2dfeac0d1e8fed378a22c7e3c943c30e84ce09ba0636c82f79f78d321536fd2846dd505b6d7ee6fb0fdb8d7f084afe40f2378eee5533cb8e7cd456 - languageName: node - linkType: hard - "browser-resolve@npm:^2.0.0": version: 2.0.0 resolution: "browser-resolve@npm:2.0.0" @@ -26595,13 +26427,6 @@ __metadata: languageName: node linkType: hard -"buffer-equal@npm:0.0.1": - version: 0.0.1 - resolution: "buffer-equal@npm:0.0.1" - checksum: 10/ca4b52e6c01143529d957a78cb9a93e4257f172bbab30d9eb87c20ae085ed23c5e07f236ac051202dacbf3d17aba42e1455f84cba21ea79b67d57f2b05e9a613 - languageName: node - linkType: hard - "buffer-from@npm:^1.0.0": version: 1.1.1 resolution: "buffer-from@npm:1.1.1" @@ -26803,13 +26628,6 @@ __metadata: languageName: node linkType: hard -"cacheable-lookup@npm:^5.0.3": - version: 5.0.4 - resolution: "cacheable-lookup@npm:5.0.4" - checksum: 10/618a8b3eea314060e74cb3285a6154e8343c244a34235acf91cfe626ee0705c24e3cd11e4b1a7b3900bd749ee203ae65afe13adf610c8ab173e99d4a208faf75 - languageName: node - linkType: hard - "cacheable-lookup@npm:^6.0.0": version: 6.1.0 resolution: "cacheable-lookup@npm:6.1.0" @@ -26839,36 +26657,6 @@ __metadata: languageName: node linkType: hard -"cacheable-request@npm:^6.0.0": - version: 6.1.0 - resolution: "cacheable-request@npm:6.1.0" - dependencies: - clone-response: "npm:^1.0.2" - get-stream: "npm:^5.1.0" - http-cache-semantics: "npm:^4.0.0" - keyv: "npm:^3.0.0" - lowercase-keys: "npm:^2.0.0" - normalize-url: "npm:^4.1.0" - responselike: "npm:^1.0.2" - checksum: 10/804f6c377ce6fef31c584babde31d55c69305569058ad95c24a41bb7b33d0ea188d388467a9da6cb340e95a3a1f8a94e1f3a709fef5eaf9c6b88e62448fa29be - languageName: node - linkType: hard - -"cacheable-request@npm:^7.0.2": - version: 7.0.4 - resolution: "cacheable-request@npm:7.0.4" - dependencies: - clone-response: "npm:^1.0.2" - get-stream: "npm:^5.1.0" - http-cache-semantics: "npm:^4.0.0" - keyv: "npm:^4.0.0" - lowercase-keys: "npm:^2.0.0" - normalize-url: "npm:^6.0.1" - responselike: "npm:^2.0.0" - checksum: 10/0f4f2001260ecca78b9f64fc8245e6b5a5dcde24ea53006daab71f5e0e1338095aa1512ec099c4f9895a9e5acfac9da423cb7c079e131485891e9214aca46c41 - languageName: node - linkType: hard - "caching-transform@npm:^4.0.0": version: 4.0.0 resolution: "caching-transform@npm:4.0.0" @@ -27057,7 +26845,7 @@ __metadata: languageName: node linkType: hard -"chalk@npm:4.1.2, chalk@npm:^4, chalk@npm:^4.0.0, chalk@npm:^4.0.2, chalk@npm:^4.1.0, chalk@npm:^4.1.1, chalk@npm:^4.1.2": +"chalk@npm:4.1.2, chalk@npm:^4.0.0, chalk@npm:^4.0.2, chalk@npm:^4.1.0, chalk@npm:^4.1.2": version: 4.1.2 resolution: "chalk@npm:4.1.2" dependencies: @@ -27134,13 +26922,6 @@ __metadata: languageName: node linkType: hard -"chardet@npm:^2.1.1": - version: 2.2.0 - resolution: "chardet@npm:2.2.0" - checksum: 10/78d1e5ba7bdc7db23511a194eedca805918df90a511f9c57f9cc7c1fc03702ed4ad053e485a766ace930362869269c4a731feaf460ae24bd3ed7d190cde8087d - languageName: node - linkType: hard - "charenc@npm:0.0.2": version: 0.0.2 resolution: "charenc@npm:0.0.2" @@ -27449,13 +27230,6 @@ __metadata: languageName: node linkType: hard -"cli-width@npm:^3.0.0": - version: 3.0.0 - resolution: "cli-width@npm:3.0.0" - checksum: 10/8730848b04fb189666ab037a35888d191c8f05b630b1d770b0b0e4c920b47bb5cc14bddf6b8ffe5bfc66cee97c8211d4d18e756c1ffcc75d7dbe7e1186cd7826 - languageName: node - linkType: hard - "clipboardy@npm:^3.0.0": version: 3.0.0 resolution: "clipboardy@npm:3.0.0" @@ -27534,15 +27308,6 @@ __metadata: languageName: node linkType: hard -"clone-response@npm:^1.0.2": - version: 1.0.3 - resolution: "clone-response@npm:1.0.3" - dependencies: - mimic-response: "npm:^1.0.0" - checksum: 10/4e671cac39b11c60aa8ba0a450657194a5d6504df51bca3fac5b3bd0145c4f8e8464898f87c8406b83232e3bc5cca555f51c1f9c8ac023969ebfbf7f6bdabb2e - languageName: node - linkType: hard - "clone@npm:^1.0.2": version: 1.0.4 resolution: "clone@npm:1.0.4" @@ -27620,13 +27385,6 @@ __metadata: languageName: node linkType: hard -"collection-utils@npm:^1.0.1": - version: 1.0.1 - resolution: "collection-utils@npm:1.0.1" - checksum: 10/264d67a9eb187fbbf15f13b87d27a0f3b8df7ff432a70a7d3f6116bb00fa30116c23a450dda622803aedb276fedfbcf09b35429ef16244b1ba075eeb138e2a5e - languageName: node - linkType: hard - "color-convert@npm:^1.9.0, color-convert@npm:^1.9.1": version: 1.9.3 resolution: "color-convert@npm:1.9.3" @@ -28005,7 +27763,7 @@ __metadata: languageName: node linkType: hard -"concat-stream@npm:^1.4.4, concat-stream@npm:~1.6.0": +"concat-stream@npm:^1.4.4": version: 1.6.2 resolution: "concat-stream@npm:1.6.2" dependencies: @@ -28181,7 +27939,7 @@ __metadata: languageName: node linkType: hard -"convert-source-map@npm:^1.5.1, convert-source-map@npm:^1.6.0, convert-source-map@npm:^1.7.0": +"convert-source-map@npm:^1.6.0, convert-source-map@npm:^1.7.0": version: 1.9.0 resolution: "convert-source-map@npm:1.9.0" checksum: 10/dc55a1f28ddd0e9485ef13565f8f756b342f9a46c4ae18b843fe3c30c675d058d6a4823eff86d472f187b176f0adf51ea7b69ea38be34be4a63cbbf91b0593c8 @@ -29225,15 +28983,6 @@ __metadata: languageName: node linkType: hard -"decompress-response@npm:^3.3.0": - version: 3.3.0 - resolution: "decompress-response@npm:3.3.0" - dependencies: - mimic-response: "npm:^1.0.0" - checksum: 10/952552ac3bd7de2fc18015086b09468645c9638d98a551305e485230ada278c039c91116e946d07894b39ee53c0f0d5b6473f25a224029344354513b412d7380 - languageName: node - linkType: hard - "decompress-response@npm:^6.0.0": version: 6.0.0 resolution: "decompress-response@npm:6.0.0" @@ -29330,13 +29079,6 @@ __metadata: languageName: node linkType: hard -"deep-is@npm:~0.1.3": - version: 0.1.4 - resolution: "deep-is@npm:0.1.4" - checksum: 10/ec12d074aef5ae5e81fa470b9317c313142c9e8e2afe3f8efa124db309720db96d1d222b82b84c834e5f87e7a614b44a4684b6683583118b87c833b3be40d4d8 - languageName: node - linkType: hard - "deepmerge-ts@npm:^7.0.3": version: 7.1.5 resolution: "deepmerge-ts@npm:7.1.5" @@ -29386,14 +29128,7 @@ __metadata: languageName: node linkType: hard -"defer-to-connect@npm:^1.0.1": - version: 1.1.3 - resolution: "defer-to-connect@npm:1.1.3" - checksum: 10/9491b301dcfa04956f989481ba7a43c2231044206269eb4ab64a52d6639ee15b1252262a789eb4239fb46ab63e44d4e408641bae8e0793d640aee55398cb3930 - languageName: node - linkType: hard - -"defer-to-connect@npm:^2.0.0, defer-to-connect@npm:^2.0.1": +"defer-to-connect@npm:^2.0.1": version: 2.0.1 resolution: "defer-to-connect@npm:2.0.1" checksum: 10/8a9b50d2f25446c0bfefb55a48e90afd58f85b21bcf78e9207cd7b804354f6409032a1705c2491686e202e64fc05f147aa5aa45f9aa82627563f045937f5791b @@ -30019,7 +29754,7 @@ __metadata: languageName: node linkType: hard -"dset@npm:^3.1.1, dset@npm:^3.1.2": +"dset@npm:^3.1.1": version: 3.1.4 resolution: "dset@npm:3.1.4" checksum: 10/6268c9e2049c8effe6e5a1952f02826e8e32468b5ced781f15f8f3b1c290da37626246fec014fbdd1503413f981dff6abd8a4c718ec9952fd45fccb6ac9de43f @@ -30056,13 +29791,6 @@ __metadata: languageName: node linkType: hard -"duplexer3@npm:^0.1.4": - version: 0.1.5 - resolution: "duplexer3@npm:0.1.5" - checksum: 10/e677cb4c48f031ca728601d6a20bf6aed4c629d69ef9643cb89c67583d673c4ec9317cc6427501f38bd8c368d3a18f173987cc02bd99d8cf8fe3d94259a22a20 - languageName: node - linkType: hard - "duplexer@npm:^0.1.2, duplexer@npm:~0.1.1": version: 0.1.2 resolution: "duplexer@npm:0.1.2" @@ -30389,7 +30117,7 @@ __metadata: languageName: node linkType: hard -"encoding@npm:^0.1.11, encoding@npm:^0.1.13": +"encoding@npm:^0.1.13": version: 0.1.13 resolution: "encoding@npm:0.1.13" dependencies: @@ -31184,25 +30912,6 @@ __metadata: languageName: node linkType: hard -"escodegen@npm:~1.9.0": - version: 1.9.1 - resolution: "escodegen@npm:1.9.1" - dependencies: - esprima: "npm:^3.1.3" - estraverse: "npm:^4.2.0" - esutils: "npm:^2.0.2" - optionator: "npm:^0.8.1" - source-map: "npm:~0.6.1" - dependenciesMeta: - source-map: - optional: true - bin: - escodegen: ./bin/escodegen.js - esgenerate: ./bin/esgenerate.js - checksum: 10/546be0474a0ae10f0adef82a1553cce183dc15aecb410c62923b1b431d841a897b5b40b26b2a7fa9af6eb70d48f97325dc94f9e89550c69552d2139fef6a5bf2 - languageName: node - linkType: hard - "eslint-config-prettier@npm:^8.1.0, eslint-config-prettier@npm:^8.5.0": version: 8.10.0 resolution: "eslint-config-prettier@npm:8.10.0" @@ -31587,16 +31296,6 @@ __metadata: languageName: node linkType: hard -"esprima@npm:^3.1.3": - version: 3.1.3 - resolution: "esprima@npm:3.1.3" - bin: - esparse: ./bin/esparse.js - esvalidate: ./bin/esvalidate.js - checksum: 10/27c9ea08582d6ece009d6ab3cdf5dc3e382f9bf50200c8b4c9a0d048563df2e2fdb0d20338fb7b651cbc00a8faf4b07fc3b2679907ae0758f239d675cde55336 - languageName: node - linkType: hard - "esprima@npm:^4.0.0, esprima@npm:^4.0.1, esprima@npm:~4.0.0": version: 4.0.1 resolution: "esprima@npm:4.0.1" @@ -31625,7 +31324,7 @@ __metadata: languageName: node linkType: hard -"estraverse@npm:^4.1.1, estraverse@npm:^4.2.0": +"estraverse@npm:^4.1.1": version: 4.3.0 resolution: "estraverse@npm:4.3.0" checksum: 10/3f67ad02b6dbfaddd9ea459cf2b6ef4ecff9a6082a7af9d22e445b9abc082ad9ca47e1825557b293fcdae477f4714e561123e30bb6a5b2f184fb2bad4a9497eb @@ -32934,16 +32633,6 @@ __metadata: languageName: node linkType: hard -"falafel@npm:^2.1.0": - version: 2.2.5 - resolution: "falafel@npm:2.2.5" - dependencies: - acorn: "npm:^7.1.1" - isarray: "npm:^2.0.1" - checksum: 10/22d4d2e3e75d889e447064e8756979936ed4559ed2c723217b7096bae968ac07235587f5d9ef87908cf8ff5c0c9c2203703773ef613576d0e3b36caf30720745 - languageName: node - linkType: hard - "fast-base64-decode@npm:^1.0.0": version: 1.0.0 resolution: "fast-base64-decode@npm:1.0.0" @@ -33040,7 +32729,7 @@ __metadata: languageName: node linkType: hard -"fast-levenshtein@npm:^2.0.6, fast-levenshtein@npm:~2.0.6": +"fast-levenshtein@npm:^2.0.6": version: 2.0.6 resolution: "fast-levenshtein@npm:2.0.6" checksum: 10/eb7e220ecf2bab5159d157350b81d01f75726a4382f5a9266f42b9150c4523b9795f7f5d9fbbbeaeac09a441b2369f05ee02db48ea938584205530fe5693cfe1 @@ -33237,7 +32926,7 @@ __metadata: languageName: node linkType: hard -"figures@npm:3.2.0, figures@npm:^3, figures@npm:^3.0.0": +"figures@npm:3.2.0": version: 3.2.0 resolution: "figures@npm:3.2.0" dependencies: @@ -34139,15 +33828,6 @@ __metadata: languageName: node linkType: hard -"get-stream@npm:^4.1.0": - version: 4.1.0 - resolution: "get-stream@npm:4.1.0" - dependencies: - pump: "npm:^3.0.0" - checksum: 10/12673e8aebc79767d187b203e5bfabb8266304037815d3bcc63b6f8c67c6d4ad0d98d4d4528bcdc1cbea68f1dd91bcbd87827aa3cdcfa9c5fa4a4644716d72c2 - languageName: node - linkType: hard - "get-stream@npm:^5.0.0, get-stream@npm:^5.1.0": version: 5.2.0 resolution: "get-stream@npm:5.2.0" @@ -34450,25 +34130,6 @@ __metadata: languageName: node linkType: hard -"got@npm:^11": - version: 11.8.6 - resolution: "got@npm:11.8.6" - dependencies: - "@sindresorhus/is": "npm:^4.0.0" - "@szmarczak/http-timer": "npm:^4.0.5" - "@types/cacheable-request": "npm:^6.0.1" - "@types/responselike": "npm:^1.0.0" - cacheable-lookup: "npm:^5.0.3" - cacheable-request: "npm:^7.0.2" - decompress-response: "npm:^6.0.0" - http2-wrapper: "npm:^1.0.0-beta.5.2" - lowercase-keys: "npm:^2.0.0" - p-cancelable: "npm:^2.0.0" - responselike: "npm:^2.0.0" - checksum: 10/a30c74029d81bd5fe50dea1a0c970595d792c568e188ff8be254b5bc11e6158d1b014570772d4a30d0a97723e7dd34e7c8cc1a2f23018f60aece3070a7a5c2a5 - languageName: node - linkType: hard - "got@npm:^12.1.0": version: 12.6.1 resolution: "got@npm:12.6.1" @@ -34488,25 +34149,6 @@ __metadata: languageName: node linkType: hard -"got@npm:^9.6.0": - version: 9.6.0 - resolution: "got@npm:9.6.0" - dependencies: - "@sindresorhus/is": "npm:^0.14.0" - "@szmarczak/http-timer": "npm:^1.1.2" - cacheable-request: "npm:^6.0.0" - decompress-response: "npm:^3.3.0" - duplexer3: "npm:^0.1.4" - get-stream: "npm:^4.1.0" - lowercase-keys: "npm:^1.0.1" - mimic-response: "npm:^1.0.1" - p-cancelable: "npm:^1.0.0" - to-readable-stream: "npm:^1.0.0" - url-parse-lax: "npm:^3.0.0" - checksum: 10/fae3273b44392b6b1d88071d04ea984784e63dbf8ba3f70b04cb7edda53c7668ee17288ac46af507a9f2aa60c183c5ea1732339141d253dda3eb19f92985c771 - languageName: node - linkType: hard - "graceful-fs@npm:4.2.10": version: 4.2.10 resolution: "graceful-fs@npm:4.2.10" @@ -34703,13 +34345,6 @@ __metadata: languageName: node linkType: hard -"has@npm:^1.0.1": - version: 1.0.4 - resolution: "has@npm:1.0.4" - checksum: 10/c245f332fe78c7b6b8753857240ac12b3286f995f656a33c77e0f5baab7d0157e6ddb1c34940ffd2bffc51f75ede50cd8b29ff65c13e336376aca8cf3df58043 - languageName: node - linkType: hard - "hash-base@npm:^2.0.0": version: 2.0.2 resolution: "hash-base@npm:2.0.2" @@ -35256,13 +34891,6 @@ __metadata: languageName: node linkType: hard -"http-cache-semantics@npm:^4.0.0": - version: 4.2.0 - resolution: "http-cache-semantics@npm:4.2.0" - checksum: 10/4efd2dfcfeea9d5e88c84af450b9980be8a43c2c8179508b1c57c7b4421c855f3e8efe92fa53e0b3f4a43c85824ada930eabbc306d1b3beab750b6dcc5187693 - languageName: node - linkType: hard - "http-cache-semantics@npm:^4.1.1": version: 4.1.1 resolution: "http-cache-semantics@npm:4.1.1" @@ -35399,16 +35027,6 @@ __metadata: languageName: node linkType: hard -"http2-wrapper@npm:^1.0.0-beta.5.2": - version: 1.0.3 - resolution: "http2-wrapper@npm:1.0.3" - dependencies: - quick-lru: "npm:^5.1.1" - resolve-alpn: "npm:^1.0.0" - checksum: 10/8097ee2699440c2e64bda52124990cc5b0fb347401c7797b1a0c1efd5a0f79a4ebaa68e8a6ac3e2dde5f09460c1602764da6da2412bad628ed0a3b0ae35e72d4 - languageName: node - linkType: hard - "http2-wrapper@npm:^2.1.10, http2-wrapper@npm:^2.2.1": version: 2.2.1 resolution: "http2-wrapper@npm:2.2.1" @@ -35536,15 +35154,6 @@ __metadata: languageName: node linkType: hard -"iconv-lite@npm:^0.7.0": - version: 0.7.3 - resolution: "iconv-lite@npm:0.7.3" - dependencies: - safer-buffer: "npm:>= 2.1.2 < 3.0.0" - checksum: 10/b88a3f4527795c96854743bcdcdd03e0083ff5f1c072cb13d3e27c67cd276efd9a2a364922cb836538e976456f82323d645f7ea91d5bc8da1f000dc16742a0aa - languageName: node - linkType: hard - "iconv-lite@npm:^0.7.2, iconv-lite@npm:~0.7.0": version: 0.7.2 resolution: "iconv-lite@npm:0.7.2" @@ -35785,29 +35394,6 @@ __metadata: languageName: node linkType: hard -"inquirer@npm:^8.2.3": - version: 8.2.7 - resolution: "inquirer@npm:8.2.7" - dependencies: - "@inquirer/external-editor": "npm:^1.0.0" - ansi-escapes: "npm:^4.2.1" - chalk: "npm:^4.1.1" - cli-cursor: "npm:^3.1.0" - cli-width: "npm:^3.0.0" - figures: "npm:^3.0.0" - lodash: "npm:^4.17.21" - mute-stream: "npm:0.0.8" - ora: "npm:^5.4.1" - run-async: "npm:^2.4.0" - rxjs: "npm:^7.5.5" - string-width: "npm:^4.1.0" - strip-ansi: "npm:^6.0.0" - through: "npm:^2.3.6" - wrap-ansi: "npm:^6.0.1" - checksum: 10/526fb5ca55a29decda9b67c7b2bd437730152104c6e7c5f0d7ade90af6dc999371e1602ce86eb4a39ee3d91993501cddec32e4fe3f599723f2b653b02b685e3b - languageName: node - linkType: hard - "int64-buffer@npm:^1.1.0": version: 1.1.0 resolution: "int64-buffer@npm:1.1.0" @@ -36355,7 +35941,7 @@ __metadata: languageName: node linkType: hard -"is-plain-obj@npm:^2.0.0, is-plain-obj@npm:^2.1.0": +"is-plain-obj@npm:^2.1.0": version: 2.1.0 resolution: "is-plain-obj@npm:2.1.0" checksum: 10/cec9100678b0a9fe0248a81743041ed990c2d4c99f893d935545cfbc42876cbe86d207f3b895700c690ad2fa520e568c44afc1605044b535a7820c1d40e38daa @@ -36469,13 +36055,6 @@ __metadata: languageName: node linkType: hard -"is-stream@npm:^1.0.1": - version: 1.1.0 - resolution: "is-stream@npm:1.1.0" - checksum: 10/351aa77c543323c4e111204482808cfad68d2e940515949e31ccd0b010fc13d5fba4b9c230e4887fd24284713040f43e542332fbf172f6b9944b7d62e389c0ec - languageName: node - linkType: hard - "is-stream@npm:^2.0.0, is-stream@npm:^2.0.1": version: 2.0.1 resolution: "is-stream@npm:2.0.1" @@ -36645,7 +36224,7 @@ __metadata: languageName: node linkType: hard -"isarray@npm:^2.0.1, isarray@npm:^2.0.5": +"isarray@npm:^2.0.5": version: 2.0.5 resolution: "isarray@npm:2.0.5" checksum: 10/1d8bc7911e13bb9f105b1b3e0b396c787a9e63046af0b8fe0ab1414488ab06b2b099b87a2d8a9e31d21c9a6fad773c7fc8b257c4880f2d957274479d28ca3414 @@ -36687,16 +36266,6 @@ __metadata: languageName: node linkType: hard -"isomorphic-fetch@npm:^2.2.1": - version: 2.2.1 - resolution: "isomorphic-fetch@npm:2.2.1" - dependencies: - node-fetch: "npm:^1.0.1" - whatwg-fetch: "npm:>=0.10.0" - checksum: 10/bb5daa7c3785d6742f4379a81e55b549a469503f7c9bf9411b48592e86632cf5e8fe8ea878dba185c0f33eb7c510c23abdeb55aebfdf5d3c70f031ced68c5424 - languageName: node - linkType: hard - "isomorphic-fetch@npm:^3.0.0": version: 3.0.0 resolution: "isomorphic-fetch@npm:3.0.0" @@ -37500,19 +37069,6 @@ __metadata: languageName: node linkType: hard -"joi@npm:^17.6.0": - version: 17.13.8 - resolution: "joi@npm:17.13.8" - dependencies: - "@hapi/hoek": "npm:^9.3.0" - "@hapi/topo": "npm:^5.1.0" - "@sideway/address": "npm:^4.1.5" - "@sideway/formula": "npm:^3.0.1" - "@sideway/pinpoint": "npm:^2.0.0" - checksum: 10/7d8ccb06cee958516748519cb46430b1bf5b732f54428a5daf8f8630290c928be1abb7d3c3c2506d68ba76d1c1abe480e087255c34b839aae47b2ee2c76f587d - languageName: node - linkType: hard - "join-component@npm:^1.1.0": version: 1.1.0 resolution: "join-component@npm:1.1.0" @@ -37542,13 +37098,6 @@ __metadata: languageName: node linkType: hard -"js-base64@npm:^2.4.3": - version: 2.6.4 - resolution: "js-base64@npm:2.6.4" - checksum: 10/c1a740a34fbb0ad0a528c2ab8749d7f873b1856a0638826306fcd98502e3c8c833334dff233085407e3201be543e5e71bf9692da7891ca680d9b03d027247a6a - languageName: node - linkType: hard - "js-base64@npm:^3.7.5, js-base64@npm:^3.7.8": version: 3.8.1 resolution: "js-base64@npm:3.8.1" @@ -37713,13 +37262,6 @@ __metadata: languageName: node linkType: hard -"json-buffer@npm:3.0.0": - version: 3.0.0 - resolution: "json-buffer@npm:3.0.0" - checksum: 10/6e364585600598c42f1cc85d1305569aeb1a6a13e7c67960f17b403f087e2700104ec8e49fc681ab6d6278ee4d132ac033f2625c22a9777ed9b83b403b40f23e - languageName: node - linkType: hard - "json-buffer@npm:3.0.1": version: 3.0.1 resolution: "json-buffer@npm:3.0.1" @@ -37839,7 +37381,7 @@ __metadata: languageName: node linkType: hard -"json-stable-stringify@npm:^1.0.1, json-stable-stringify@npm:^1.1.1": +"json-stable-stringify@npm:^1.1.1": version: 1.3.0 resolution: "json-stable-stringify@npm:1.3.0" dependencies: @@ -38041,16 +37583,7 @@ __metadata: languageName: node linkType: hard -"keyv@npm:^3.0.0": - version: 3.1.0 - resolution: "keyv@npm:3.1.0" - dependencies: - json-buffer: "npm:3.0.0" - checksum: 10/6de272b3f78975a9a0b12259953c09d5bbe9de9acfd845471ebd758928b523f70563462f0c16a866fe9b447ff5bdebda72c62bc23734eb72cd1fb8f1d7076843 - languageName: node - linkType: hard - -"keyv@npm:^4.0.0, keyv@npm:^4.5.3": +"keyv@npm:^4.5.3": version: 4.5.4 resolution: "keyv@npm:4.5.4" dependencies: @@ -38181,15 +37714,6 @@ __metadata: languageName: node linkType: hard -"latest-version@npm:^5": - version: 5.1.0 - resolution: "latest-version@npm:5.1.0" - dependencies: - package-json: "npm:^6.3.0" - checksum: 10/fbc72b071eb66c40f652441fd783a9cca62f08bf42433651937f078cd9ef94bf728ec7743992777826e4e89305aef24f234b515e6030503a2cbee7fc9bdc2c0f - languageName: node - linkType: hard - "latest-version@npm:^7.0.0": version: 7.0.0 resolution: "latest-version@npm:7.0.0" @@ -38380,16 +37904,6 @@ __metadata: languageName: node linkType: hard -"levn@npm:~0.3.0": - version: 0.3.0 - resolution: "levn@npm:0.3.0" - dependencies: - prelude-ls: "npm:~1.1.2" - type-check: "npm:~0.3.2" - checksum: 10/e1c3e75b5c430d9aa4c32c83c8a611e4ca53608ca78e3ea3bf6bbd9d017e4776d05d86e27df7901baebd3afa732abede9f26f715b8c1be19e95505c7a3a7b589 - languageName: node - linkType: hard - "lie@npm:~3.3.0": version: 3.3.0 resolution: "lie@npm:3.3.0" @@ -39008,20 +38522,6 @@ __metadata: languageName: node linkType: hard -"lowercase-keys@npm:^1.0.0, lowercase-keys@npm:^1.0.1": - version: 1.0.1 - resolution: "lowercase-keys@npm:1.0.1" - checksum: 10/12ba64572dc25ae9ee30d37a11f3a91aea046c1b6b905fdf8ac77e2f268f153ed36e60d39cb3bfa47a89f31d981dae9a8cc9915124a56fe51ff01ed6e8bb68fa - languageName: node - linkType: hard - -"lowercase-keys@npm:^2.0.0": - version: 2.0.0 - resolution: "lowercase-keys@npm:2.0.0" - checksum: 10/1c233d2da35056e8c49fae8097ee061b8c799b2f02e33c2bf32f9913c7de8fb481ab04dab7df35e94156c800f5f34e99acbf32b21781d87c3aa43ef7b748b79e - languageName: node - linkType: hard - "lowercase-keys@npm:^3.0.0": version: 3.0.0 resolution: "lowercase-keys@npm:3.0.0" @@ -39105,15 +38605,6 @@ __metadata: languageName: node linkType: hard -"magic-string@npm:^0.22.4": - version: 0.22.5 - resolution: "magic-string@npm:0.22.5" - dependencies: - vlq: "npm:^0.2.2" - checksum: 10/a5514579480f5fb6cc24d87073b431aa647f02d6a1fd2972dd2116e5ac73f9a3bb1e2bd0d635b5f59a1ecbf9ad825262385a5f192514acbba7cd597eff86e726 - languageName: node - linkType: hard - "magic-string@npm:^0.30.0, magic-string@npm:^0.30.3, magic-string@npm:~0.30.8": version: 0.30.21 resolution: "magic-string@npm:0.30.21" @@ -39664,15 +39155,6 @@ __metadata: languageName: node linkType: hard -"merge-source-map@npm:1.0.4": - version: 1.0.4 - resolution: "merge-source-map@npm:1.0.4" - dependencies: - source-map: "npm:^0.5.6" - checksum: 10/86a4e60d83980393e61f069c7ae33e7899c4c012c3cd2cf50e01482e7a284bbe9c8cd08d37adbf241fc9eacfa4425241432e7461cf6559f7e9902587889660de - languageName: node - linkType: hard - "merge-stream@npm:^2.0.0": version: 2.0.0 resolution: "merge-stream@npm:2.0.0" @@ -40210,7 +39692,6 @@ __metadata: ts-node: "npm:^10.9.2" tsx: "npm:^4.21.0" typescript: "npm:~5.8.3" - typewriter: "npm:9.2.0" unicode-confusables: "npm:^0.1.1" uri-js: "npm:^4.4.1" url: "npm:0.11.0" @@ -41102,13 +40583,6 @@ __metadata: languageName: node linkType: hard -"mimic-response@npm:^1.0.0, mimic-response@npm:^1.0.1": - version: 1.0.1 - resolution: "mimic-response@npm:1.0.1" - checksum: 10/034c78753b0e622bc03c983663b1cdf66d03861050e0c8606563d149bc2b02d63f62ce4d32be4ab50d0553ae0ffe647fc34d1f5281184c6e1e8cf4d85e8d9823 - languageName: node - linkType: hard - "mimic-response@npm:^3.1.0": version: 3.1.0 resolution: "mimic-response@npm:3.1.0" @@ -41228,7 +40702,7 @@ __metadata: languageName: node linkType: hard -"minimist@npm:^1.1.3, minimist@npm:^1.2.0, minimist@npm:^1.2.5, minimist@npm:^1.2.6": +"minimist@npm:^1.2.0, minimist@npm:^1.2.5, minimist@npm:^1.2.6": version: 1.2.8 resolution: "minimist@npm:1.2.8" checksum: 10/908491b6cc15a6c440ba5b22780a0ba89b9810e1aea684e253e43c4e3b8d56ec1dcdd7ea96dde119c29df59c936cde16062159eae4225c691e19c70b432b6e6f @@ -41943,16 +41417,6 @@ __metadata: languageName: node linkType: hard -"node-fetch@npm:^1.0.1": - version: 1.7.3 - resolution: "node-fetch@npm:1.7.3" - dependencies: - encoding: "npm:^0.1.11" - is-stream: "npm:^1.0.1" - checksum: 10/17be1a182eec37ba43c1b02673329ab1fa1c2e3ddbdf9b5f62d5157aa118c253dcd42f338eb1b6fe84fb0689c4dd8eb5aaebf46a9b5a066fae88c74e36522688 - languageName: node - linkType: hard - "node-fetch@npm:^2.0.0, node-fetch@npm:^2.6.0, node-fetch@npm:^2.6.1, node-fetch@npm:^2.6.7, node-fetch@npm:^2.7.0": version: 2.7.0 resolution: "node-fetch@npm:2.7.0" @@ -42061,13 +41525,6 @@ __metadata: languageName: node linkType: hard -"node-machine-id@npm:^1.1.12": - version: 1.1.12 - resolution: "node-machine-id@npm:1.1.12" - checksum: 10/46bf3d4fab8d0e63b24c42bcec2b6975c7ec5bc16e53d7a589d095668d0fdf0bfcbcdc28246dd1ef74cf95a37fbd774cd4b17b41f518d79dfad7fdc99f995903 - languageName: node - linkType: hard - "node-preload@npm:^0.2.1": version: 0.2.1 resolution: "node-preload@npm:0.2.1" @@ -42201,20 +41658,6 @@ __metadata: languageName: node linkType: hard -"normalize-url@npm:^4.1.0": - version: 4.5.1 - resolution: "normalize-url@npm:4.5.1" - checksum: 10/20ced2845fcfaa46da74efc0aa39b7bed22f3db39e6e8b844261613082a36a2dcd468decad89fa9313b5464bebab4034f96bda7880e8fc468027fecf6a6fa254 - languageName: node - linkType: hard - -"normalize-url@npm:^6.0.1": - version: 6.1.0 - resolution: "normalize-url@npm:6.1.0" - checksum: 10/5ae699402c9d5ffa330adc348fcd6fc6e6a155ab7c811b96e30b7ecab60ceef821d8f86443869671dda71bbc47f4b9625739c82ad247e883e9aefe875bfb8659 - languageName: node - linkType: hard - "normalize-url@npm:^8.0.0": version: 8.1.1 resolution: "normalize-url@npm:8.1.1" @@ -42463,13 +41906,6 @@ __metadata: languageName: node linkType: hard -"object-inspect@npm:~1.4.0": - version: 1.4.1 - resolution: "object-inspect@npm:1.4.1" - checksum: 10/f49b58baf3d82692cf3dac7e969f742f4618caa4d3dd84819bb095438bea1b31ffb5b6896d1227091b60d7d641752a07c822f113ce505cefd8adadfd6d9602e9 - languageName: node - linkType: hard - "object-is@npm:^1.0.1, object-is@npm:^1.1.5": version: 1.1.5 resolution: "object-is@npm:1.1.5" @@ -42804,20 +42240,6 @@ __metadata: languageName: node linkType: hard -"optionator@npm:^0.8.1": - version: 0.8.3 - resolution: "optionator@npm:0.8.3" - dependencies: - deep-is: "npm:~0.1.3" - fast-levenshtein: "npm:~2.0.6" - levn: "npm:~0.3.0" - prelude-ls: "npm:~1.1.2" - type-check: "npm:~0.3.2" - word-wrap: "npm:~1.2.3" - checksum: 10/6fa3c841b520f10aec45563962922215180e8cfbc59fde3ecd4ba2644ad66ca96bd19ad0e853f22fefcb7fc10e7612a5215b412cc66c5588f9a3138b38f6b5ff - languageName: node - linkType: hard - "optionator@npm:^0.9.3": version: 0.9.4 resolution: "optionator@npm:0.9.4" @@ -42970,20 +42392,6 @@ __metadata: languageName: node linkType: hard -"p-cancelable@npm:^1.0.0": - version: 1.1.0 - resolution: "p-cancelable@npm:1.1.0" - checksum: 10/2db3814fef6d9025787f30afaee4496a8857a28be3c5706432cbad76c688a6db1874308f48e364a42f5317f5e41e8e7b4f2ff5c8ff2256dbb6264bc361704ece - languageName: node - linkType: hard - -"p-cancelable@npm:^2.0.0": - version: 2.1.1 - resolution: "p-cancelable@npm:2.1.1" - checksum: 10/7f1b64db17fc54acf359167d62898115dcf2a64bf6b3b038e4faf36fc059e5ed762fb9624df8ed04b25bee8de3ab8d72dea9879a2a960cd12e23c420a4aca6ed - languageName: node - linkType: hard - "p-cancelable@npm:^3.0.0": version: 3.0.0 resolution: "p-cancelable@npm:3.0.0" @@ -43184,18 +42592,6 @@ __metadata: languageName: node linkType: hard -"package-json@npm:^6.3.0": - version: 6.5.0 - resolution: "package-json@npm:6.5.0" - dependencies: - got: "npm:^9.6.0" - registry-auth-token: "npm:^4.0.0" - registry-url: "npm:^5.0.0" - semver: "npm:^6.2.0" - checksum: 10/adb8e49f352ea0d71a4d351732c3870d57f21e6f3921d69a83dd9ef04b45cdb0a035495826fbe9fb2cb9a7e521484404b7d527c181133867b126588efa1996c6 - languageName: node - linkType: hard - "package-json@npm:^8.1.0": version: 8.1.1 resolution: "package-json@npm:8.1.1" @@ -43208,27 +42604,20 @@ __metadata: languageName: node linkType: hard -"pako@npm:^0.2.5": - version: 0.2.9 - resolution: "pako@npm:0.2.9" - checksum: 10/627c6842e90af0b3a9ee47345bd66485a589aff9514266f4fa9318557ad819c46fedf97510f2cef9b6224c57913777966a05cb46caf6a9b31177a5401a06fe15 +"pako@npm:^2.1.0": + version: 2.1.0 + resolution: "pako@npm:2.1.0" + checksum: 10/38a04991d0ec4f4b92794a68b8c92bf7340692c5d980255c92148da96eb3e550df7a86a7128b5ac0c65ecddfe5ef3bbe9c6dab13e1bc315086e759b18f7c1401 languageName: node linkType: hard -"pako@npm:^1.0.6, pako@npm:~1.0.2, pako@npm:~1.0.5": +"pako@npm:~1.0.2, pako@npm:~1.0.5": version: 1.0.11 resolution: "pako@npm:1.0.11" checksum: 10/1ad07210e894472685564c4d39a08717e84c2a68a70d3c1d9e657d32394ef1670e22972a433cbfe48976cb98b154ba06855dcd3fcfba77f60f1777634bec48c0 languageName: node linkType: hard -"pako@npm:^2.1.0": - version: 2.1.0 - resolution: "pako@npm:2.1.0" - checksum: 10/38a04991d0ec4f4b92794a68b8c92bf7340692c5d980255c92148da96eb3e550df7a86a7128b5ac0c65ecddfe5ef3bbe9c6dab13e1bc315086e759b18f7c1401 - languageName: node - linkType: hard - "param-case@npm:^3.0.4": version: 3.0.4 resolution: "param-case@npm:3.0.4" @@ -43894,13 +43283,6 @@ __metadata: languageName: node linkType: hard -"pluralize@npm:^7.0.0": - version: 7.0.0 - resolution: "pluralize@npm:7.0.0" - checksum: 10/905274e679d3802650fdfdd977434757d4680082da7a23c0938a608d1d5c8246790b62dc15ff1f737b0d57baa6ad2f6ebb0857b1950435a583e32af76ee58e1f - languageName: node - linkType: hard - "pngjs@npm:7.0.0, pngjs@npm:^7.0.0": version: 7.0.0 resolution: "pngjs@npm:7.0.0" @@ -44851,20 +44233,6 @@ __metadata: languageName: node linkType: hard -"prelude-ls@npm:~1.1.2": - version: 1.1.2 - resolution: "prelude-ls@npm:1.1.2" - checksum: 10/946a9f60d3477ca6b7d4c5e8e452ad1b98dc8aaa992cea939a6b926ac16cc4129d7217c79271dc808b5814b1537ad0af37f29a942e2eafbb92cfc5a1c87c38cb - languageName: node - linkType: hard - -"prepend-http@npm:^2.0.0": - version: 2.0.0 - resolution: "prepend-http@npm:2.0.0" - checksum: 10/7694a9525405447662c1ffd352fcb41b6410c705b739b6f4e3a3e21cf5fdede8377890088e8934436b8b17ba55365a615f153960f30877bf0d0392f9e93503ea - languageName: node - linkType: hard - "prettier-2@npm:prettier@^2.8.8, prettier@npm:^2.0.5, prettier@npm:^2.4.1": version: 2.8.8 resolution: "prettier@npm:2.8.8" @@ -44990,13 +44358,6 @@ __metadata: languageName: node linkType: hard -"process-nextick-args@npm:~1.0.6": - version: 1.0.7 - resolution: "process-nextick-args@npm:1.0.7" - checksum: 10/f3b0e2f762e4fc03d02779fbf434caff82d27439ba2ecd82f7f95439e56dc23e367a8c1d3919533bd961b8e447d8ad0d941d6a3acda48ddcb80fe1b45b423579 - languageName: node - linkType: hard - "process-nextick-args@npm:~2.0.0": version: 2.0.1 resolution: "process-nextick-args@npm:2.0.1" @@ -45495,39 +44856,6 @@ __metadata: languageName: node linkType: hard -"quicktype-core@npm:^6.0.71": - version: 6.1.12 - resolution: "quicktype-core@npm:6.1.12" - dependencies: - "@mark.probst/unicode-properties": "npm:~1.1.0" - browser-or-node: "npm:^1.2.1" - collection-utils: "npm:^1.0.1" - is-url: "npm:^1.2.4" - isomorphic-fetch: "npm:^2.2.1" - js-base64: "npm:^2.4.3" - pako: "npm:^1.0.6" - pluralize: "npm:^7.0.0" - readable-stream: "npm:2.3.0" - urijs: "npm:^1.19.1" - wordwrap: "npm:^1.0.0" - yaml: "npm:^1.5.0" - checksum: 10/5c12680aeb052316d5e5c49cfd014ac8af1f93f5ed6f61f315c79ed2943517e35414c8456b490f3ec8629db4ec45180872be3be63d7d5425b51b8cc547af9d1e - languageName: node - linkType: hard - -"quote-stream@npm:^1.0.1, quote-stream@npm:~1.0.2": - version: 1.0.2 - resolution: "quote-stream@npm:1.0.2" - dependencies: - buffer-equal: "npm:0.0.1" - minimist: "npm:^1.1.3" - through2: "npm:^2.0.0" - bin: - quote-stream: bin/cmd.js - checksum: 10/25d615ea6dc406bb7fd865cb83bc897667e81974e6ff3f473f360f0ff2f3f6e00c07d0bd382b719b3588e52252e3b5565a4edc494a7fd190198fd4241eaa4180 - languageName: node - linkType: hard - "radix3@npm:^1.1.0": version: 1.1.0 resolution: "radix3@npm:1.1.0" @@ -45606,7 +44934,7 @@ __metadata: languageName: node linkType: hard -"rc@npm:1.2.8, rc@npm:^1.2.8": +"rc@npm:1.2.8": version: 1.2.8 resolution: "rc@npm:1.2.8" dependencies: @@ -47303,21 +46631,6 @@ __metadata: languageName: node linkType: hard -"readable-stream@npm:2.3.0": - version: 2.3.0 - resolution: "readable-stream@npm:2.3.0" - dependencies: - core-util-is: "npm:~1.0.0" - inherits: "npm:~2.0.3" - isarray: "npm:~1.0.0" - process-nextick-args: "npm:~1.0.6" - safe-buffer: "npm:~5.1.0" - string_decoder: "npm:~1.0.0" - util-deprecate: "npm:~1.0.1" - checksum: 10/26662d74fc66a38d833bf5bd26107217f63cbdcc05988f88e7e738caa32b8c45006d78d8bc2a008f175970e01a7469f71d21932d4c8eb4f2fc7f1c69c9c4870c - languageName: node - linkType: hard - "readable-stream@npm:2.3.7": version: 2.3.7 resolution: "readable-stream@npm:2.3.7" @@ -47358,7 +46671,7 @@ __metadata: languageName: node linkType: hard -"readable-stream@npm:^2.0.0, readable-stream@npm:^2.0.1, readable-stream@npm:^2.0.2, readable-stream@npm:^2.0.5, readable-stream@npm:^2.2.2, readable-stream@npm:^2.3.3, readable-stream@npm:^2.3.8, readable-stream@npm:~2.3.3, readable-stream@npm:~2.3.6": +"readable-stream@npm:^2.0.0, readable-stream@npm:^2.0.1, readable-stream@npm:^2.0.2, readable-stream@npm:^2.0.5, readable-stream@npm:^2.2.2, readable-stream@npm:^2.3.3, readable-stream@npm:^2.3.8, readable-stream@npm:~2.3.6": version: 2.3.8 resolution: "readable-stream@npm:2.3.8" dependencies: @@ -47738,15 +47051,6 @@ __metadata: languageName: node linkType: hard -"registry-auth-token@npm:^4.0.0": - version: 4.2.2 - resolution: "registry-auth-token@npm:4.2.2" - dependencies: - rc: "npm:1.2.8" - checksum: 10/00d1b1c69f09df52a0bfbaecee71f2ba094d8fd8d1abc325090655b2c6c8a69c969b31525086c10f95126c3452cd4a0c5c9a6832fb08bec5a32a4e224b790cf8 - languageName: node - linkType: hard - "registry-auth-token@npm:^5.0.1": version: 5.1.1 resolution: "registry-auth-token@npm:5.1.1" @@ -47756,15 +47060,6 @@ __metadata: languageName: node linkType: hard -"registry-url@npm:^5.0.0": - version: 5.1.0 - resolution: "registry-url@npm:5.1.0" - dependencies: - rc: "npm:^1.2.8" - checksum: 10/bcea86c84a0dbb66467b53187fadebfea79017cddfb4a45cf27530d7275e49082fe9f44301976eb0164c438e395684bcf3dae4819b36ff9d1640d8cc60c73df9 - languageName: node - linkType: hard - "registry-url@npm:^6.0.0": version: 6.0.1 resolution: "registry-url@npm:6.0.1" @@ -48022,7 +47317,7 @@ __metadata: languageName: node linkType: hard -"resolve-alpn@npm:^1.0.0, resolve-alpn@npm:^1.2.0": +"resolve-alpn@npm:^1.2.0": version: 1.2.1 resolution: "resolve-alpn@npm:1.2.1" checksum: 10/744e87888f0b6fa0b256ab454ca0b9c0b80808715e2ef1f3672773665c92a941f6181194e30ccae4a8cd0adbe0d955d3f133102636d2ee0cca0119fec0bc9aec @@ -48103,7 +47398,7 @@ __metadata: languageName: node linkType: hard -"resolve@npm:^1.1.5, resolve@npm:^1.1.7, resolve@npm:^1.17.0, resolve@npm:^1.20.0, resolve@npm:^1.22.11, resolve@npm:^1.22.3, resolve@npm:^1.22.4, resolve@npm:^1.22.8": +"resolve@npm:^1.1.7, resolve@npm:^1.17.0, resolve@npm:^1.20.0, resolve@npm:^1.22.11, resolve@npm:^1.22.3, resolve@npm:^1.22.4, resolve@npm:^1.22.8": version: 1.22.12 resolution: "resolve@npm:1.22.12" dependencies: @@ -48143,7 +47438,7 @@ __metadata: languageName: node linkType: hard -"resolve@patch:resolve@npm%3A^1.1.5#optional!builtin, resolve@patch:resolve@npm%3A^1.1.7#optional!builtin, resolve@patch:resolve@npm%3A^1.17.0#optional!builtin, resolve@patch:resolve@npm%3A^1.20.0#optional!builtin, resolve@patch:resolve@npm%3A^1.22.11#optional!builtin, resolve@patch:resolve@npm%3A^1.22.3#optional!builtin, resolve@patch:resolve@npm%3A^1.22.4#optional!builtin, resolve@patch:resolve@npm%3A^1.22.8#optional!builtin": +"resolve@patch:resolve@npm%3A^1.1.7#optional!builtin, resolve@patch:resolve@npm%3A^1.17.0#optional!builtin, resolve@patch:resolve@npm%3A^1.20.0#optional!builtin, resolve@patch:resolve@npm%3A^1.22.11#optional!builtin, resolve@patch:resolve@npm%3A^1.22.3#optional!builtin, resolve@patch:resolve@npm%3A^1.22.4#optional!builtin, resolve@patch:resolve@npm%3A^1.22.8#optional!builtin": version: 1.22.12 resolution: "resolve@patch:resolve@npm%3A1.22.12#optional!builtin::version=1.22.12&hash=c3c19d" dependencies: @@ -48170,24 +47465,6 @@ __metadata: languageName: node linkType: hard -"responselike@npm:^1.0.2": - version: 1.0.2 - resolution: "responselike@npm:1.0.2" - dependencies: - lowercase-keys: "npm:^1.0.0" - checksum: 10/2e9e70f1dcca3da621a80ce71f2f9a9cad12c047145c6ece20df22f0743f051cf7c73505e109814915f23f9e34fb0d358e22827723ee3d56b623533cab8eafcd - languageName: node - linkType: hard - -"responselike@npm:^2.0.0": - version: 2.0.1 - resolution: "responselike@npm:2.0.1" - dependencies: - lowercase-keys: "npm:^2.0.0" - checksum: 10/b122535466e9c97b55e69c7f18e2be0ce3823c5d47ee8de0d9c0b114aa55741c6db8bfbfce3766a94d1272e61bfb1ebf0a15e9310ac5629fbb7446a861b4fd3a - languageName: node - linkType: hard - "responselike@npm:^3.0.0": version: 3.0.0 resolution: "responselike@npm:3.0.0" @@ -48462,13 +47739,6 @@ __metadata: languageName: node linkType: hard -"run-async@npm:^2.4.0": - version: 2.4.1 - resolution: "run-async@npm:2.4.1" - checksum: 10/c79551224dafa26ecc281cb1efad3510c82c79116aaf681f8a931ce70fdf4ca880d58f97d3b930a38992c7aad7955a08e065b32ec194e1dd49d7790c874ece50 - languageName: node - linkType: hard - "run-parallel@npm:^1.1.9": version: 1.2.0 resolution: "run-parallel@npm:1.2.0" @@ -48478,7 +47748,7 @@ __metadata: languageName: node linkType: hard -"rxjs@npm:7.8.2, rxjs@npm:>= 6, rxjs@npm:^7.5.5, rxjs@npm:^7.8.1, rxjs@npm:^7.8.2": +"rxjs@npm:7.8.2, rxjs@npm:>= 6, rxjs@npm:^7.8.1, rxjs@npm:^7.8.2": version: 7.8.2 resolution: "rxjs@npm:7.8.2" dependencies: @@ -48824,7 +48094,7 @@ __metadata: languageName: node linkType: hard -"semver@npm:7.8.5, semver@npm:^7.0.0, semver@npm:^7.1.1, semver@npm:^7.1.3, semver@npm:^7.3, semver@npm:^7.3.5, semver@npm:^7.3.7, semver@npm:^7.3.8, semver@npm:^7.5.0, semver@npm:^7.5.2, semver@npm:^7.5.3, semver@npm:^7.5.4, semver@npm:^7.6.0, semver@npm:^7.6.2, semver@npm:^7.6.3, semver@npm:^7.7.1, semver@npm:^7.7.3, semver@npm:^7.7.4, semver@npm:^7.8.5": +"semver@npm:7.8.5, semver@npm:^7.0.0, semver@npm:^7.1.1, semver@npm:^7.1.3, semver@npm:^7.3.5, semver@npm:^7.3.7, semver@npm:^7.3.8, semver@npm:^7.5.0, semver@npm:^7.5.2, semver@npm:^7.5.3, semver@npm:^7.5.4, semver@npm:^7.6.0, semver@npm:^7.6.2, semver@npm:^7.6.3, semver@npm:^7.7.1, semver@npm:^7.7.3, semver@npm:^7.7.4, semver@npm:^7.8.5": version: 7.8.5 resolution: "semver@npm:7.8.5" bin: @@ -48842,7 +48112,7 @@ __metadata: languageName: node linkType: hard -"semver@npm:^6.0.0, semver@npm:^6.2.0, semver@npm:^6.3.0, semver@npm:^6.3.1": +"semver@npm:^6.0.0, semver@npm:^6.3.0, semver@npm:^6.3.1": version: 6.3.1 resolution: "semver@npm:6.3.1" bin: @@ -49115,13 +48385,6 @@ __metadata: languageName: node linkType: hard -"shallow-copy@npm:~0.0.1": - version: 0.0.1 - resolution: "shallow-copy@npm:0.0.1" - checksum: 10/916c3a8d687f4eea9f257f08b8c5c2e55aa092fee0f22ca86f241047a45a974af7df86a9169cfb7b7919a827f5095885928ec98e5bf8e17f076bac4f321639a6 - languageName: node - linkType: hard - "shallowequal@npm:^1.1.0": version: 1.1.0 resolution: "shallowequal@npm:1.1.0" @@ -49708,15 +48971,6 @@ __metadata: languageName: node linkType: hard -"sort-keys@npm:^4.2.0": - version: 4.2.0 - resolution: "sort-keys@npm:4.2.0" - dependencies: - is-plain-obj: "npm:^2.0.0" - checksum: 10/cbc5adf02eae3f9e2ccbd94eafd6019d705fb217ffd77bc9ecd71b6e4aee617bac93dae91c580fb62f0deb9b0b53f105ba2ae89d253168adf16b7cb3e6cef25a - languageName: node - linkType: hard - "source-map-js@npm:^1.0.1, source-map-js@npm:^1.0.2, source-map-js@npm:^1.2.1": version: 1.2.1 resolution: "source-map-js@npm:1.2.1" @@ -49978,37 +49232,6 @@ __metadata: languageName: node linkType: hard -"static-eval@npm:^2.0.0": - version: 2.1.1 - resolution: "static-eval@npm:2.1.1" - dependencies: - escodegen: "npm:^2.1.0" - checksum: 10/b47e8238014745ea457f2dc9e14130298ce5dbec142a5c93a943f338fe1b47d03086d1897114e15d8603a9a7a39fed95bb49ccd3fa95dfc1550952eca0688417 - languageName: node - linkType: hard - -"static-module@npm:^2.2.0": - version: 2.2.5 - resolution: "static-module@npm:2.2.5" - dependencies: - concat-stream: "npm:~1.6.0" - convert-source-map: "npm:^1.5.1" - duplexer2: "npm:~0.1.4" - escodegen: "npm:~1.9.0" - falafel: "npm:^2.1.0" - has: "npm:^1.0.1" - magic-string: "npm:^0.22.4" - merge-source-map: "npm:1.0.4" - object-inspect: "npm:~1.4.0" - quote-stream: "npm:~1.0.2" - readable-stream: "npm:~2.3.3" - shallow-copy: "npm:~0.0.1" - static-eval: "npm:^2.0.0" - through2: "npm:~2.0.3" - checksum: 10/2d2920052248cbeb2f7c06ad726b9b9359d00780156f838c7149790558de1900d64bfef286d07e84ffeb1c2efba7403cf383ff54a662c36c11e3f16b932fb994 - languageName: node - linkType: hard - "statuses@npm:>= 1.5.0 < 2, statuses@npm:^1.5.0, statuses@npm:~1.5.0": version: 1.5.0 resolution: "statuses@npm:1.5.0" @@ -50312,15 +49535,6 @@ __metadata: languageName: node linkType: hard -"string_decoder@npm:~1.0.0": - version: 1.0.3 - resolution: "string_decoder@npm:1.0.3" - dependencies: - safe-buffer: "npm:~5.1.0" - checksum: 10/8689f666b5c6045f125fc6202eebd28f790606bc7962cfcc27eec54cfdcd19c3222ae6a4d5a3a911ef71574d8b2f9b607f99922a0db9837f1ff132465cc519f2 - languageName: node - linkType: hard - "string_decoder@npm:~1.1.1": version: 1.1.1 resolution: "string_decoder@npm:1.1.1" @@ -50872,7 +50086,7 @@ __metadata: languageName: node linkType: hard -"terminal-link@npm:2.1.1, terminal-link@npm:^2, terminal-link@npm:^2.1.1": +"terminal-link@npm:2.1.1, terminal-link@npm:^2.1.1": version: 2.1.1 resolution: "terminal-link@npm:2.1.1" dependencies: @@ -51060,17 +50274,7 @@ __metadata: languageName: node linkType: hard -"through2@npm:^2.0.0, through2@npm:~2.0.3": - version: 2.0.5 - resolution: "through2@npm:2.0.5" - dependencies: - readable-stream: "npm:~2.3.6" - xtend: "npm:~4.0.1" - checksum: 10/cd71f7dcdc7a8204fea003a14a433ef99384b7d4e31f5497e1f9f622b3cf3be3691f908455f98723bdc80922a53af7fa10c3b7abbe51c6fd3d536dbc7850e2c4 - languageName: node - linkType: hard - -"through@npm:2, through@npm:^2.3.6, through@npm:^2.3.8, through@npm:~2.3.4": +"through@npm:2, through@npm:^2.3.8, through@npm:~2.3.4": version: 2.3.8 resolution: "through@npm:2.3.8" checksum: 10/5da78346f70139a7d213b65a0106f3c398d6bc5301f9248b5275f420abc2c4b1e77c2abc72d218dedc28c41efb2e7c312cb76a7730d04f9c2d37d247da3f4198 @@ -51107,13 +50311,6 @@ __metadata: languageName: node linkType: hard -"tiny-inflate@npm:^1.0.0": - version: 1.0.3 - resolution: "tiny-inflate@npm:1.0.3" - checksum: 10/f620114fb51ea4a16ea7b4c62d6dd753f8faf41808a133c53d431ed4bf2ca377b21443653a0096894f2be22ca11bb327f148e7e5431f9246068917724ec01ffc - languageName: node - linkType: hard - "tiny-invariant@npm:^1.0.2, tiny-invariant@npm:^1.3.1": version: 1.3.3 resolution: "tiny-invariant@npm:1.3.3" @@ -51218,13 +50415,6 @@ __metadata: languageName: node linkType: hard -"to-readable-stream@npm:^1.0.0": - version: 1.0.0 - resolution: "to-readable-stream@npm:1.0.0" - checksum: 10/a99e23d49777d9d03686f03cc0bbbcb4648d991648990a98bc93b55cf91a2ae830c41b5efa36802f1c00a34bba93bd33b10346772fd3f49bcf1667a99c85f354 - languageName: node - linkType: hard - "to-regex-range@npm:^5.0.1": version: 5.0.1 resolution: "to-regex-range@npm:5.0.1" @@ -51588,15 +50778,6 @@ __metadata: languageName: node linkType: hard -"type-check@npm:~0.3.2": - version: 0.3.2 - resolution: "type-check@npm:0.3.2" - dependencies: - prelude-ls: "npm:~1.1.2" - checksum: 10/11dec0b50d7c3fd2e630b4b074ba36918ed2b1efbc87dfbd40ba9429d49c58d12dad5c415ece69fcf358fa083f33466fc370f23ab91aa63295c45d38b3a60dda - languageName: node - linkType: hard - "type-detect@npm:4.0.8": version: 4.0.8 resolution: "type-detect@npm:4.0.8" @@ -51819,7 +51000,7 @@ __metadata: languageName: node linkType: hard -"typescript@npm:~5.8.3": +"typescript@npm:^5.2.2, typescript@npm:~5.8.3": version: 5.8.3 resolution: "typescript@npm:5.8.3" bin: @@ -51829,7 +51010,7 @@ __metadata: languageName: node linkType: hard -"typescript@patch:typescript@npm%3A~5.8.3#optional!builtin": +"typescript@patch:typescript@npm%3A^5.2.2#optional!builtin, typescript@patch:typescript@npm%3A~5.8.3#optional!builtin": version: 5.8.3 resolution: "typescript@patch:typescript@npm%3A5.8.3#optional!builtin::version=5.8.3&hash=5786d5" bin: @@ -51839,37 +51020,6 @@ __metadata: languageName: node linkType: hard -"typewriter@npm:9.2.0": - version: 9.2.0 - resolution: "typewriter@npm:9.2.0" - dependencies: - "@npmcli/ci-detect": "npm:^2.0.0" - "@oclif/core": "npm:^1.7" - "@segment/analytics-node": "npm:^1.0.0-beta.26" - ajv: "npm:^8.11.0" - chalk: "npm:^4" - debug: "npm:^4.3.4" - figures: "npm:^3" - got: "npm:^11" - handlebars: "npm:^4.7.7" - inquirer: "npm:^8.2.3" - joi: "npm:^17.6.0" - json-stable-stringify: "npm:^1.0.1" - latest-version: "npm:^5" - lodash: "npm:^4.17" - node-machine-id: "npm:^1.1.12" - quicktype-core: "npm:^6.0.71" - semver: "npm:^7.3" - sort-keys: "npm:^4.2.0" - terminal-link: "npm:^2" - typescript: "npm:*" - yaml: "npm:^2.3.1" - bin: - typewriter: bin/run - checksum: 10/5a2c2b651eb77e8b5eb75e363202a5e083648978f8fdb182e6b951c2835e2ac65f743564ef42b18f7e358ad36c1053d6561a07f208fecffab5cb45fefb11f83d - languageName: node - linkType: hard - "ua-is-frozen@npm:^0.1.2": version: 0.1.2 resolution: "ua-is-frozen@npm:0.1.2" @@ -52084,16 +51234,6 @@ __metadata: languageName: node linkType: hard -"unicode-trie@npm:^0.3.0": - version: 0.3.1 - resolution: "unicode-trie@npm:0.3.1" - dependencies: - pako: "npm:^0.2.5" - tiny-inflate: "npm:^1.0.0" - checksum: 10/5b3d6beb85d75984011d3795ff7b1168c3b8b8fcc5a7042f3f51d8d71a0db6a99efe7a0d4407517577ea8fbcb2c948b84944d0386c0809a4d9db04dc246c2d51 - languageName: node - linkType: hard - "unidragger@npm:^3.0.0": version: 3.0.1 resolution: "unidragger@npm:3.0.1" @@ -52525,15 +51665,6 @@ __metadata: languageName: node linkType: hard -"url-parse-lax@npm:^3.0.0": - version: 3.0.0 - resolution: "url-parse-lax@npm:3.0.0" - dependencies: - prepend-http: "npm:^2.0.0" - checksum: 10/1040e357750451173132228036aff1fd04abbd43eac1fb3e4fca7495a078bcb8d33cb765fe71ad7e473d9c94d98fd67adca63bd2716c815a2da066198dd37217 - languageName: node - linkType: hard - "url-parse@npm:1.5.9": version: 1.5.9 resolution: "url-parse@npm:1.5.9" @@ -53084,13 +52215,6 @@ __metadata: languageName: node linkType: hard -"vlq@npm:^0.2.2": - version: 0.2.3 - resolution: "vlq@npm:0.2.3" - checksum: 10/2231d8caeb5b2c1a438677ab029e9a94aa6fb61ab05819c72691b792aea0456dab29576aff5ae29309ee45bad0a309e832dc45173119bca1393f3b87709d8f8d - languageName: node - linkType: hard - "vlq@npm:^1.0.0": version: 1.0.1 resolution: "vlq@npm:1.0.1" @@ -53566,7 +52690,7 @@ __metadata: languageName: node linkType: hard -"whatwg-fetch@npm:>=0.10.0, whatwg-fetch@npm:^3.0.0, whatwg-fetch@npm:^3.4.1": +"whatwg-fetch@npm:^3.0.0, whatwg-fetch@npm:^3.4.1": version: 3.6.20 resolution: "whatwg-fetch@npm:3.6.20" checksum: 10/2b4ed92acd6a7ad4f626a6cb18b14ec982bbcaf1093e6fe903b131a9c6decd14d7f9c9ca3532663c2759d1bdf01d004c77a0adfb2716a5105465c20755a8c57c @@ -53819,7 +52943,7 @@ __metadata: languageName: node linkType: hard -"word-wrap@npm:^1.2.5, word-wrap@npm:~1.2.3": +"word-wrap@npm:^1.2.5": version: 1.2.5 resolution: "word-wrap@npm:1.2.5" checksum: 10/1ec6f6089f205f83037be10d0c4b34c9183b0b63fca0834a5b3cee55dd321429d73d40bb44c8fc8471b5203d6e8f8275717f49a8ff4b2b0ab41d7e1b563e0854 @@ -53857,7 +52981,7 @@ __metadata: languageName: node linkType: hard -"wrap-ansi@npm:^6.0.1, wrap-ansi@npm:^6.2.0": +"wrap-ansi@npm:^6.2.0": version: 6.2.0 resolution: "wrap-ansi@npm:6.2.0" dependencies: @@ -54196,7 +53320,7 @@ __metadata: languageName: node linkType: hard -"xtend@npm:^4.0.0, xtend@npm:^4.0.1, xtend@npm:^4.0.2, xtend@npm:~4.0.0, xtend@npm:~4.0.1": +"xtend@npm:^4.0.0, xtend@npm:^4.0.1, xtend@npm:^4.0.2, xtend@npm:~4.0.0": version: 4.0.2 resolution: "xtend@npm:4.0.2" checksum: 10/ac5dfa738b21f6e7f0dd6e65e1b3155036d68104e67e5d5d1bde74892e327d7e5636a076f625599dc394330a731861e87343ff184b0047fef1360a7ec0a5a36a @@ -54271,22 +53395,6 @@ __metadata: languageName: node linkType: hard -"yaml@npm:^1.5.0": - version: 1.10.3 - resolution: "yaml@npm:1.10.3" - checksum: 10/e2ef2feb92c708138f016c69777a0f1e45f6d3c5e7cbcda30807a98a37eda2e008bd4fa57352b043c65245a4c799d0c99d1f9b3425de40e70929e26d2ea38215 - languageName: node - linkType: hard - -"yaml@npm:^2.3.1": - version: 2.9.1 - resolution: "yaml@npm:2.9.1" - bin: - yaml: bin.mjs - checksum: 10/123c5e56a26f4962d3d5954971b2071a533d17041e00ae9ff6a5e634aed2503639bbdeea143b1610eac29613aad95a3a1d10f5fbda87b92203c039ce63934295 - languageName: node - linkType: hard - "yargs-parser@npm:22.0.0, yargs-parser@npm:^22.0.0": version: 22.0.0 resolution: "yargs-parser@npm:22.0.0" From 9fbb29b84758bd6170e4b5a314bf7fed84b74c66 Mon Sep 17 00:00:00 2001 From: jvbriones <1674192+jvbriones@users.noreply.github.com> Date: Tue, 29 Sep 2026 12:45:34 +0200 Subject: [PATCH 5/7] chore(MMQA-1951): remove Typewriter evaluation --- .github/workflows/check-analytics-contract.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/check-analytics-contract.yml b/.github/workflows/check-analytics-contract.yml index 1bdcd2b3d1fc..f8374c944088 100644 --- a/.github/workflows/check-analytics-contract.yml +++ b/.github/workflows/check-analytics-contract.yml @@ -29,7 +29,7 @@ jobs: name: Verify latest deployed Mobile contract environment: default-branch runs-on: ubuntu-latest - timeout-minutes: 15 + timeout-minutes: 40 outputs: release-tag: ${{ steps.shared-contract.outputs.release-tag }} release-url: ${{ steps.shared-contract.outputs.release-url }} From a4e9131377a962cd20fdcca0306e7fdd214de041 Mon Sep 17 00:00:00 2001 From: jvbriones <1674192+jvbriones@users.noreply.github.com> Date: Tue, 29 Sep 2026 13:21:48 +0200 Subject: [PATCH 6/7] chore(MMQA-1951): remove Typewriter evaluation --- sonar-project.properties | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/sonar-project.properties b/sonar-project.properties index 34b55ae13dea..d05ca20d8a9d 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -9,8 +9,9 @@ sonar.sources=app/ # Binary assets are excluded so the scanner does not probe them as UTF-8 text. sonar.exclusions=**.stories.**, **/*.perf-test.tsx, e2e/**, tests/**, wdio/**, app/components/UI/Perps/utils/e2eBridgePerps.ts, **/*.png, **/*.jpg, **/*.jpeg, **/*.gif, **/*.webp, **/*.mp4, **/*.otf, **/*.ttf, **/*.woff, **/*.woff2, **/*.riv -# Inclusions for test files. -sonar.test.inclusions=**.test.** +# Inclusions for test files. Type-only `.test-d.ts` fixtures are validated by +# TypeScript and do not produce runtime coverage. +sonar.test.inclusions=**.test.**, **.test-d.** # Excluded project files from coverage. sonar.coverage.exclusions=**util/test**, **/__mocks__/**, **NetworkConnectMultiSelector.tsx, **TransactionNotification/index.js, **BrowserTab.tsx, **RPCMethodMiddleware.ts, **PermissionsSummary.tsx, **useSwitchNetworks.ts, **ethereum-chain-utils.js, **app/selectors/selectedNetworkController.ts, **useAddressBalance.ts, **AccountPermissions.tsx, **AddressFrom.tsx, **TransactionDetails**, **TransactionElement**, **networkController.ts, **util/networks/index.js, **AccountFromToInfoCard.tsx, **Engine/Engine.ts, **TransactionReview/index.js, **smart-publish-hook.ts, **AccountFromToInfoCard.types.tsx, **BrowserTab.tsx, **NetworkSelector.tsx, app/components/UI/Perps/utils/e2eBridgePerps.ts, **/MarketInsightsEntryCardOriginal.tsx, **/AnimatedGradientBorder.tsx, app/components/UI/Ramp/debug/**, app/components/UI/Perps/Debug/** From 0b54ef43b9ff2552888a9d477c7426864db09a81 Mon Sep 17 00:00:00 2001 From: jvbriones <1674192+jvbriones@users.noreply.github.com> Date: Tue, 29 Sep 2026 13:55:39 +0200 Subject: [PATCH 7/7] chore(MMQA-1951): fix lint error --- scripts/analytics/typed-events/check-contract.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/analytics/typed-events/check-contract.ts b/scripts/analytics/typed-events/check-contract.ts index c3d5ee207dd9..eaae6b865c33 100644 --- a/scripts/analytics/typed-events/check-contract.ts +++ b/scripts/analytics/typed-events/check-contract.ts @@ -150,7 +150,8 @@ export const selectLatestAnalyticsRelease = ( ): AnalyticsContractRelease => { const candidates = releases .filter((release) => release.tag.startsWith(ANALYTICS_RELEASE_PREFIX)) - .toSorted( + .slice() + .sort( (left, right) => Date.parse(right.publishedAt) - Date.parse(left.publishedAt), );