From ed398ba4e17d5d7afa4abc886f499c0a268a0fe4 Mon Sep 17 00:00:00 2001 From: kpj2006 <24ucs074@lnmiit.ac.in> Date: Sun, 30 Aug 2026 15:22:46 +0530 Subject: [PATCH] feat: implement idempotency handling and intent parsing - Add idempotency key generation and canonical string formatting in `idempotency.ts`. - Introduce intent parsing logic in `intent.ts` to validate and structure incoming requests. - Define core types for intents and idempotency keys in `types.ts`. - Create a driver registry in `registry.ts` to manage settlement drivers and enforce tier-based restrictions. - Implement mock settlement driver for testing purposes in `mock/index.ts`. - Add resolver chain and inline address resolver for payout target resolution in `resolvers/index.ts` and `inline-address.ts`. - Establish end-to-end tests for settlement process and resolver functionality in `mock-settlement.test.ts` and `inline-address.test.ts`. - Introduce boundary tests to ensure compliance with import restrictions in `i1.test.ts`. - Set default settlement mode and error handling in `defaults.test.ts`. - Configure TypeScript settings in `tsconfig.json`. --- .gitattributes | 5 + .github/workflows/ci.yml | 63 + .gitignore | 5 + action.yml | 50 + dist/index.js | 420 ++++++ dist/package.json | 3 + eslint.config.mjs | 78 ++ package-lock.json | 1686 +++++++++++++++++++++++++ package.json | 33 + scripts/check-boundary.mjs | 112 ++ scripts/check-deps.mjs | 30 + scripts/run-tests.mjs | 31 + src/adapters/github/outputs.ts | 25 + src/core/defaults.ts | 11 + src/core/errors.ts | 158 +++ src/core/idempotency.ts | 32 + src/core/intent.ts | 61 + src/core/types.ts | 61 + src/drivers/registry.ts | 84 ++ src/drivers/types.ts | 50 + src/main.ts | 61 + src/resolvers/index.ts | 31 + src/resolvers/inline-address.ts | 34 + src/resolvers/types.ts | 16 + test/boundary/i1.test.ts | 90 ++ test/core/defaults.test.ts | 39 + test/core/idempotency.test.ts | 98 ++ test/drivers/mock/index.ts | 187 +++ test/drivers/registry.test.ts | 86 ++ test/e2e/mock-settlement.test.ts | 147 +++ test/resolvers/inline-address.test.ts | 40 + tsconfig.json | 21 + 32 files changed, 3848 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100644 action.yml create mode 100644 dist/index.js create mode 100644 dist/package.json create mode 100644 eslint.config.mjs create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 scripts/check-boundary.mjs create mode 100644 scripts/check-deps.mjs create mode 100644 scripts/run-tests.mjs create mode 100644 src/adapters/github/outputs.ts create mode 100644 src/core/defaults.ts create mode 100644 src/core/errors.ts create mode 100644 src/core/idempotency.ts create mode 100644 src/core/intent.ts create mode 100644 src/core/types.ts create mode 100644 src/drivers/registry.ts create mode 100644 src/drivers/types.ts create mode 100644 src/main.ts create mode 100644 src/resolvers/index.ts create mode 100644 src/resolvers/inline-address.ts create mode 100644 src/resolvers/types.ts create mode 100644 test/boundary/i1.test.ts create mode 100644 test/core/defaults.test.ts create mode 100644 test/core/idempotency.test.ts create mode 100644 test/drivers/mock/index.ts create mode 100644 test/drivers/registry.test.ts create mode 100644 test/e2e/mock-settlement.test.ts create mode 100644 test/resolvers/inline-address.test.ts create mode 100644 tsconfig.json diff --git a/.gitattributes b/.gitattributes index 7d1465d..6e89ad9 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,7 @@ +# I12 depends on dist/ being byte-identical across platforms. +* text=auto eol=lf +*.png binary +*.jpg binary + .github/workflows/*.yml linguist-detectable -linguist-vendored .github/workflows/*.yaml linguist-detectable -linguist-vendored \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..7150d8a --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,63 @@ +name: CI + +on: + push: + branches: ["**"] + pull_request: + +permissions: + contents: read + +jobs: + ground-rules: + name: Ground rules + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: "20" + cache: npm + - run: npm ci + + - name: I1 — layer boundary lint + run: npm run lint + + - name: I1 — layer boundary grep + run: npm run check:boundary + + - name: I11 — runtime dependency count + run: npm run check:deps + + - name: Typecheck + run: npm run typecheck + + - name: Tests (I3, I5, I7, I8, I9 and the mock end-to-end) + run: npm test + + - name: I12 — dist/ is reproducible from src/ + run: | + npm run build + if ! git diff --exit-code -- dist; then + echo "::error::dist/ does not match a rebuild from src/. Run 'npm run build' and commit dist/." + exit 1 + fi + + action: + name: Action runs and parses an Intent + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Dry-run with no secrets configured + id: xops + uses: ./ + with: + recipient: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" + amount: "2500000" + + - name: Assert the run stayed in dry-run and produced a key + run: | + test "${{ steps.xops.outputs.STATUS }}" = "dry-run" + test -n "${{ steps.xops.outputs.IDEMPOTENCY_KEY }}" + echo "key: ${{ steps.xops.outputs.IDEMPOTENCY_KEY }}" diff --git a/.gitignore b/.gitignore index 9308a4b..64b5c4e 100644 --- a/.gitignore +++ b/.gitignore @@ -324,3 +324,8 @@ TSWLatexianTemp* # option is specified. Footnotes are the stored in a file with suffix Notes.bib. # Uncomment the next line to have this generated file ignored. #*Notes.bib + +node_modules/ +build/ +*.log +.DS_Store diff --git a/action.yml b/action.yml new file mode 100644 index 0000000..a62a59c --- /dev/null +++ b/action.yml @@ -0,0 +1,50 @@ +name: "XOps" +description: "CI/CD-native value transfer. A repository event produces a payment intent, a human signs it, the workflow settles it." +author: "AOSSIE-Org" +branding: + icon: "git-merge" + color: "purple" + +inputs: + recipient: + description: "Opaque payout identity. An inline address, or anything a resolver understands." + required: true + amount: + description: "Atomic units, as a string. USDC has 6 decimals, so 2500000 is 2.50 USDC." + required: true + asset: + description: "Opaque asset identifier from the chain registry." + required: false + default: "USDC" + network: + description: "CAIP-2 network identifier." + required: false + default: "eip155:84532" + scheme: + description: "x402 settlement scheme." + required: false + default: "exact" + round: + description: "Bump to deliberately re-pay the same recipient for the same ref." + required: false + default: "0" + mode: + description: "dry-run | facilitator | self | auto. Real settlement is always explicit." + required: false + default: "dry-run" + +outputs: + TX_HASH: + description: "Settlement transaction identifier, when one exists." + EXPLORER_URL: + description: "Human-readable link to the settlement." + STATUS: + description: "dry-run | settled | already-paid | error" + ERROR_CODE: + description: "Code from the XOps error taxonomy, empty on success." + IDEMPOTENCY_KEY: + description: "Canonical key this run derived. Identical inputs reproduce it." + +runs: + using: "node20" + main: "dist/index.js" diff --git a/dist/index.js b/dist/index.js new file mode 100644 index 0000000..b3bec7d --- /dev/null +++ b/dist/index.js @@ -0,0 +1,420 @@ +import { createRequire as __WEBPACK_EXTERNAL_createRequire } from "module"; +/******/ /* webpack/runtime/compat */ +/******/ +/******/ if (typeof __nccwpck_require__ !== 'undefined') __nccwpck_require__.ab = new URL('.', import.meta.url).pathname.slice(import.meta.url.match(/^file:\/\/\/\w:/) ? 1 : 0, -1) + "/"; +/******/ +/************************************************************************/ +var __webpack_exports__ = {}; + +;// CONCATENATED MODULE: external "node:fs" +const external_node_fs_namespaceObject = __WEBPACK_EXTERNAL_createRequire(import.meta.url)("node:fs"); +;// CONCATENATED MODULE: external "node:crypto" +const external_node_crypto_namespaceObject = __WEBPACK_EXTERNAL_createRequire(import.meta.url)("node:crypto"); +;// CONCATENATED MODULE: ./src/adapters/github/outputs.ts + + +/** + * Written on every path, including failures. The delimiter is random per call so + * a value containing a newline cannot forge another output. + */ +function writeOutputs(values) { + const file = process.env["GITHUB_OUTPUT"]; + if (!file) + return; + const delimiter = `XOPS_EOF_${(0,external_node_crypto_namespaceObject.randomUUID)()}`; + let block = ""; + for (const [name, value] of Object.entries(values)) { + block += `${name}<<${delimiter}\n${value ?? ""}\n${delimiter}\n`; + } + if (block) + (0,external_node_fs_namespaceObject.appendFileSync)(file, block, "utf8"); +} + +;// CONCATENATED MODULE: ./src/core/defaults.ts +/** I5. Real settlement is always an explicit opt-in. */ +const DEFAULT_SETTLEMENT_MODE = "dry-run"; +/** Kill switch default. Honored before policy evaluation. */ +const DEFAULT_SETTLEMENT_ENABLED = true; +/** Clock-skew allowance and authorization window, in seconds. */ +const VALID_AFTER_SKEW_SECONDS = 60; +const VALID_BEFORE_WINDOW_SECONDS = 900; + +;// CONCATENATED MODULE: ./src/core/errors.ts +const ERROR_CODES = (/* unused pure expression or super */ null && ([ + "AUTH_ALREADY_USED", + "AUTH_EXPIRED", + "AUTH_NOT_YET_VALID", + "SIGNER_MISMATCH", + "DOMAIN_MISMATCH", + "AMOUNT_MISMATCH", + "RECIPIENT_MISMATCH", + "IDENTITY_UNRESOLVED", + "POLICY_DENIED", + "AMOUNT_CAP_EXCEEDED", + "INSUFFICIENT_BALANCE", + "INSUFFICIENT_GAS", + "SIMULATION_REVERT", + "RPC_UNAVAILABLE", + "DRIVER_NOT_FOUND", + "TIER_VIOLATION", + "NO_REPLAY_PROTECTION", +])); +const ERRORS = { + AUTH_ALREADY_USED: { + meaning: "Authorization nonce already consumed", + comment: "Already paid — see the original transaction.", + retry: "no", + success: true, + }, + AUTH_EXPIRED: { + meaning: "Past validBefore", + comment: "Authorization expired. Re-sign; the window is 15 minutes.", + retry: "user", + success: false, + }, + AUTH_NOT_YET_VALID: { + meaning: "Before validAfter", + comment: "Clock skew between signer and node. Wait 60 seconds and retry.", + retry: "auto", + success: false, + }, + SIGNER_MISMATCH: { + meaning: "Recovered signer is not the expected payer", + comment: "Wrong wallet connected. Sign with the treasury account.", + retry: "user", + success: false, + }, + DOMAIN_MISMATCH: { + meaning: "Typed-data domain does not match the asset registry", + comment: "Check the network and asset in `.xops.yml`.", + retry: "no", + success: false, + }, + AMOUNT_MISMATCH: { + meaning: "Payload amount differs from requirements", + comment: "The payload was tampered with and was not settled.", + retry: "no", + success: false, + }, + RECIPIENT_MISMATCH: { + meaning: "Payload recipient differs from requirements", + comment: "The payload was tampered with and was not settled.", + retry: "no", + success: false, + }, + IDENTITY_UNRESOLVED: { + meaning: "No resolver matched the recipient identity", + comment: "Register a payout address or use an inline address.", + retry: "user", + success: false, + }, + POLICY_DENIED: { + meaning: "A policy condition failed", + comment: "A required policy condition was not met. Nothing was settled.", + retry: "no", + success: false, + }, + AMOUNT_CAP_EXCEEDED: { + meaning: "Amount above policy.max_per_payout", + comment: "Above the configured per-payout cap. Raise the cap or split the payout.", + retry: "no", + success: false, + }, + INSUFFICIENT_BALANCE: { + meaning: "Treasury balance short of the payout", + comment: "The treasury does not hold enough of the asset.", + retry: "user", + success: false, + }, + INSUFFICIENT_GAS: { + meaning: "Broadcasting account cannot pay fees", + comment: "Fund the broadcasting account with the network's native token.", + retry: "user", + success: false, + }, + SIMULATION_REVERT: { + meaning: "Pre-broadcast simulation reverted", + comment: "Simulation reverted before broadcast, so no fees were spent.", + retry: "no", + success: false, + }, + RPC_UNAVAILABLE: { + meaning: "Every configured endpoint failed", + comment: "No endpoint responded. Retryable; set `rpc_url` to override.", + retry: "auto", + success: false, + }, + DRIVER_NOT_FOUND: { + meaning: "No driver registered for this network and scheme", + comment: "Unsupported network/scheme combination.", + retry: "no", + success: false, + }, + TIER_VIOLATION: { + meaning: "Driver requires secrets or custody and cannot run in Tier 0", + comment: "This driver must run as a separate service you operate.", + retry: "no", + success: false, + }, + NO_REPLAY_PROTECTION: { + meaning: "Driver does not declare an exactly-once guarantee", + comment: "Cannot settle without replay protection.", + retry: "no", + success: false, + }, +}; +class XOpsError extends Error { + code; + details; + constructor(code, message, details = {}) { + super(message ?? `${code}: ${ERRORS[code].meaning}`); + this.name = "XOpsError"; + this.code = code; + this.details = details; + } +} +function isErrorCode(value) { + return typeof value === "string" && ERROR_CODES.includes(value); +} +/** I8 lives here: one code in the taxonomy is a success. */ +function isSuccessCode(code) { + return ERRORS[code].success; +} + +;// CONCATENATED MODULE: ./src/core/idempotency.ts +/** + * Returns a canonical string. No hashing, no rail primitives — a driver derives + * its rail's replay token from this string. + * + * `amount` is excluded on purpose: with amount in the key, `/send alice 50` + * corrected to `/send alice 500` yields two keys and Alice receives 550. + * Excluded, the correction collides and requires an explicit `round` bump. + */ +function canonical(k) { + return (`xops:v${k.v}|${k.source.platform}:${k.source.repo}#${k.source.ref}` + + `|${k.recipient.toLowerCase()}|${k.network}|${k.asset}|${k.round}`); +} +function keyFor(intent) { + const { source } = intent; + return { + v: 1, + source: { + platform: source.platform, + repo: source.platform === "github" ? source.repo : source.project, + ref: source.ref, + }, + recipient: intent.recipient, + asset: intent.asset, + network: intent.network, + round: intent.round, + }; +} + +;// CONCATENATED MODULE: ./src/core/intent.ts +// CAIP-2: namespace:reference. Says nothing about what the namespace means. +const CAIP2 = /^[-a-z0-9]{3,8}:[-_a-zA-Z0-9]{1,32}$/; +const ATOMIC_AMOUNT = /^[0-9]+$/; +function required(raw, field) { + const value = raw[field]?.trim(); + if (!value) + throw new Error(`Missing required intent field: ${field}`); + return value; +} +function parseIntent(raw) { + const platform = (raw.platform ?? "github").trim(); + if (platform !== "github" && platform !== "gitlab") { + throw new Error(`Unsupported platform: ${platform}`); + } + const repo = required(raw, "repo"); + const ref = required(raw, "ref"); + const actor = required(raw, "actor"); + const recipient = required(raw, "recipient"); + const amount = required(raw, "amount"); + const asset = required(raw, "asset"); + const network = required(raw, "network"); + const scheme = required(raw, "scheme"); + if (!ATOMIC_AMOUNT.test(amount)) { + throw new Error(`amount must be a whole number of atomic units as a string, got "${amount}"`); + } + if (!CAIP2.test(network)) { + throw new Error(`network must be a CAIP-2 identifier, got "${network}"`); + } + const round = Number(raw.round ?? "0"); + if (!Number.isInteger(round) || round < 0) { + throw new Error(`round must be a non-negative integer, got "${raw.round}"`); + } + const source = platform === "github" + ? { platform, repo, ref, actor } + : { platform, project: repo, ref, actor }; + return { source, recipient, amount, asset, network, scheme, round }; +} + +;// CONCATENATED MODULE: ./src/drivers/registry.ts + +function tierViolationMessage(id) { + return [ + `Driver ${id} takes custody of funds and cannot run in-process.`, + "Custodial settlement must run as a separate service you operate:", + " settlement: { mode: facilitator, url: https://your-service }", + ].join("\n"); +} +/** + * The only layer that knows rails exist is the driver behind this registry. + * Everything above resolves `(network × scheme)` and gets an interface back. + */ +class DriverRegistry { + tier; + drivers = []; + constructor(tier = 0) { + this.tier = tier; + } + /** I3: a driver needing secrets or custody cannot register in Tier 0. */ + register(driver) { + const { needsSecret, custodial } = driver.capabilities; + if (this.tier === 0 && (needsSecret || custodial)) { + throw new XOpsError("TIER_VIOLATION", tierViolationMessage(driver.id), { + driver: driver.id, + needsSecret, + custodial, + tier: this.tier, + }); + } + if (this.drivers.some((d) => d.id === driver.id)) { + throw new Error(`Driver ${driver.id} is already registered`); + } + this.drivers.push(driver); + } + list() { + return this.drivers; + } + resolve(network, scheme) { + const driver = this.drivers.find((d) => d.supports(network, scheme)); + if (!driver) { + throw new XOpsError("DRIVER_NOT_FOUND", `No driver registered for scheme "${scheme}" on network "${network}"`, { network, scheme, registered: this.drivers.map((d) => d.id) }); + } + return driver; + } + buildRequirements(ctx) { + const driver = this.resolve(ctx.intent.network, ctx.intent.scheme); + return driver.buildRequirements(ctx); + } + async verify(p, r) { + return this.resolve(r.network, r.scheme).verify(p, r); + } + /** I9: refuse before the driver is reached if it declares no exactly-once guarantee. */ + async settle(p, r) { + const driver = this.resolve(r.network, r.scheme); + if (this.tier === 0 && !driver.capabilities.nativeReplayProtection) { + throw new XOpsError("NO_REPLAY_PROTECTION", `Driver ${driver.id} declares no replay protection and cannot settle in Tier 0`, { driver: driver.id, tier: this.tier }); + } + return driver.settle(p, r); + } +} + +;// CONCATENATED MODULE: ./src/resolvers/inline-address.ts +const INLINE_PREFIX = "inline:"; +/** + * Takes the payout address verbatim from the identity string. It does not + * validate the address format — the format belongs to a rail, and only the + * driver for that rail may judge it. + * + * Accepts `inline:` and bare values. `@handle` identities are left for + * lookup-based resolvers. + */ +class InlineAddressResolver { + id = "inline-address"; + canResolve(identity) { + const value = strip(identity); + return value.length > 0 && !value.startsWith("@") && !/\s/.test(value); + } + resolve(identity, ctx) { + return Promise.resolve({ + rail: ctx.rail, + address: strip(identity), + resolvedBy: this.id, + }); + } +} +function strip(identity) { + const trimmed = identity.trim(); + return trimmed.startsWith(INLINE_PREFIX) ? trimmed.slice(INLINE_PREFIX.length).trim() : trimmed; +} + +;// CONCATENATED MODULE: ./src/resolvers/index.ts + + +class ResolverChain { + resolvers; + constructor(resolvers = []) { + this.resolvers = resolvers; + } + use(resolver) { + this.resolvers.push(resolver); + return this; + } + async resolve(identity, ctx) { + for (const resolver of this.resolvers) { + if (resolver.canResolve(identity)) { + return resolver.resolve(identity, ctx); + } + } + throw new XOpsError("IDENTITY_UNRESOLVED", `No resolver matched "${identity}"`, { + identity, + tried: this.resolvers.map((r) => r.id), + }); + } +} + +;// CONCATENATED MODULE: ./src/main.ts + + + + + + + +function input(name) { + return process.env[`INPUT_${name.toUpperCase().replace(/ /g, "_")}`]; +} +async function run() { + const intent = parseIntent({ + platform: "github", + repo: input("repo") ?? process.env["GITHUB_REPOSITORY"], + ref: input("ref") ?? process.env["GITHUB_REF"], + actor: input("actor") ?? process.env["GITHUB_ACTOR"], + recipient: input("recipient"), + amount: input("amount"), + asset: input("asset"), + network: input("network"), + scheme: input("scheme"), + round: input("round"), + }); + const idempotencyKey = canonical(keyFor(intent)); + const resolvers = new ResolverChain([new InlineAddressResolver()]); + const target = await resolvers.resolve(intent.recipient, { rail: intent.network }); + console.log("intent:"); + console.log(JSON.stringify(intent, null, 2)); + console.log("payout target:"); + console.log(JSON.stringify(target, null, 2)); + console.log(`idempotency key: ${idempotencyKey}`); + const mode = input("mode") ?? DEFAULT_SETTLEMENT_MODE; + if (mode === "dry-run") { + console.log("mode: dry-run — nothing was settled."); + writeOutputs({ STATUS: "dry-run", IDEMPOTENCY_KEY: idempotencyKey, ERROR_CODE: "" }); + return 0; + } + // No settlement driver ships yet. Registry resolution is the honest failure. + const registry = new DriverRegistry(0); + registry.resolve(intent.network, intent.scheme); + return 0; +} +run().then((code) => { + process.exitCode = code; +}, (err) => { + const code = err instanceof XOpsError ? err.code : ""; + const message = err instanceof Error ? err.message : String(err); + console.error(message); + writeOutputs({ STATUS: "error", ERROR_CODE: code }); + process.exitCode = 1; +}); + diff --git a/dist/package.json b/dist/package.json new file mode 100644 index 0000000..3dbc1ca --- /dev/null +++ b/dist/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..b9e7883 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,78 @@ +import js from "@eslint/js"; +import tseslint from "typescript-eslint"; + +// I1 — THE ONE RULE, as a lint rule. Written before there was anything to lint. +const CHAIN_LIBRARIES = [ + "ethers", + "ethers/*", + "@ethersproject/*", + "viem", + "viem/*", + "thirdweb", + "thirdweb/*", + "web3", + "web3-*", + "ox", + "ox/*", + "x402", + "@x402/*", + "@noble/*", + "@scure/*", + "@solana/*", + "bn.js", + "elliptic", + "keccak", + "js-sha3", +]; + +const WRONG_DIRECTION = ["**/drivers", "**/drivers/*", "**/drivers/**", "**/assets/*"]; + +export default tseslint.config( + { ignores: ["dist/", "build/", "node_modules/", "signer/", "dangerfile.js"] }, + js.configs.recommended, + ...tseslint.configs.recommended, + { + files: ["**/*.ts"], + languageOptions: { + parserOptions: { ecmaVersion: 2022, sourceType: "module" }, + }, + rules: { + "@typescript-eslint/consistent-type-imports": "error", + "@typescript-eslint/no-explicit-any": "error", + eqeqeq: ["error", "always"], + "no-console": "off", + }, + }, + { + files: ["src/core/**/*.ts", "src/adapters/**/*.ts", "src/resolvers/**/*.ts"], + rules: { + "@typescript-eslint/no-restricted-imports": [ + "error", + { + patterns: [ + { + group: CHAIN_LIBRARIES, + message: + "I1: core, adapters and resolvers may not import a chain library or payment SDK. This belongs in a driver. See AGENTS.md.", + allowTypeImports: false, + }, + { + group: WRONG_DIRECTION, + message: + "I1: the boundary points one way — drivers import core, never the reverse. See AGENTS.md.", + allowTypeImports: false, + }, + ], + }, + ], + }, + }, + { + files: ["scripts/**/*.mjs"], + languageOptions: { + ecmaVersion: "latest", + sourceType: "module", + globals: { process: "readonly", console: "readonly" }, + }, + }, +); diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..27d52a6 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1686 @@ +{ + "name": "xops", + "version": "0.1.0-dev", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "xops", + "version": "0.1.0-dev", + "license": "MIT", + "bin": { + "xops": "dist/cli.js" + }, + "devDependencies": { + "@eslint/js": "9.39.0", + "@types/node": "22.19.0", + "@vercel/ncc": "0.38.4", + "eslint": "9.39.0", + "typescript": "5.9.3", + "typescript-eslint": "8.46.2" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", + "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.6.tgz", + "integrity": "sha512-l2Ul9PrHsPCKcEY/ac7VgFj9D80C7S68sOKc618SyHDPK36s1XcFebXY0iTzUVn4Yq+YbwvSnDmCz9yxjX+QrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.3.0", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.0.tgz", + "integrity": "sha512-BIhe0sW91JGPiaF1mOuPy5v8NflqfjIcDNpC+LbW9f609WVRX1rArrhi6Z2ymvrAry9jw+5POTj4t2t62o8Bmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.19.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.0.tgz", + "integrity": "sha512-xpr/lmLPQEj+TUnHmR+Ab91/glhJvsqcjB+yY0Ix9GO70H6Lb4FHH5GeqdOE5btAx7eIMwuHkp4H2MSkLcqWbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.2.tgz", + "integrity": "sha512-ZGBMToy857/NIPaaCucIUQgqueOiq7HeAKkhlvqVV4lm089zUFW6ikRySx2v+cAhKeUCPuWVHeimyk6Dw1iY3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.46.2", + "@typescript-eslint/type-utils": "8.46.2", + "@typescript-eslint/utils": "8.46.2", + "@typescript-eslint/visitor-keys": "8.46.2", + "graphemer": "^1.4.0", + "ignore": "^7.0.0", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.46.2", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.6.tgz", + "integrity": "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.46.2.tgz", + "integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.46.2", + "@typescript-eslint/types": "8.46.2", + "@typescript-eslint/typescript-estree": "8.46.2", + "@typescript-eslint/visitor-keys": "8.46.2", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.46.2.tgz", + "integrity": "sha512-PULOLZ9iqwI7hXcmL4fVfIsBi6AN9YxRc0frbvmg8f+4hQAjQ5GYNKK0DIArNo+rOKmR/iBYwkpBmnIwin4wBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.46.2", + "@typescript-eslint/types": "^8.46.2", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.46.2.tgz", + "integrity": "sha512-LF4b/NmGvdWEHD2H4MsHD8ny6JpiVNDzrSZr3CsckEgCbAGZbYM4Cqxvi9L+WqDMT+51Ozy7lt2M+d0JLEuBqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.46.2", + "@typescript-eslint/visitor-keys": "8.46.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.2.tgz", + "integrity": "sha512-a7QH6fw4S57+F5y2FIxxSDyi5M4UfGF+Jl1bCGd7+L4KsaUY80GsiF/t0UoRFDHAguKlBaACWJRmdrc6Xfkkag==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.46.2.tgz", + "integrity": "sha512-HbPM4LbaAAt/DjxXaG9yiS9brOOz6fabal4uvUmaUYe6l3K1phQDMQKBRUrr06BQkxkvIZVVHttqiybM9nJsLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.46.2", + "@typescript-eslint/typescript-estree": "8.46.2", + "@typescript-eslint/utils": "8.46.2", + "debug": "^4.3.4", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.2.tgz", + "integrity": "sha512-lNCWCbq7rpg7qDsQrd3D6NyWYu+gkTENkG5IKYhUIcxSb59SQC/hEQ+MrG4sTgBVghTonNWq42bA/d4yYumldQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.2.tgz", + "integrity": "sha512-f7rW7LJ2b7Uh2EiQ+7sza6RDZnajbNbemn54Ob6fRwQbgcIn+GWfyuHDHRYgRoZu1P4AayVScrRW+YfbTvPQoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.46.2", + "@typescript-eslint/tsconfig-utils": "8.46.2", + "@typescript-eslint/types": "8.46.2", + "@typescript-eslint/visitor-keys": "8.46.2", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", + "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.46.2.tgz", + "integrity": "sha512-sExxzucx0Tud5tE0XqR0lT0psBQvEpnpiul9XbGUB1QwpWJJAps1O/Z7hJxLGiZLBKMCutjTzDgmd1muEhBnVg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.46.2", + "@typescript-eslint/types": "8.46.2", + "@typescript-eslint/typescript-estree": "8.46.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.2.tgz", + "integrity": "sha512-tUFMXI4gxzzMXt4xpGJEsBsTox0XbNQ1y94EwlD/CuZwFcQP79xfQqMhau9HsRc/J0cAPA/HZt1dZPtGn9V/7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.46.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@vercel/ncc": { + "version": "0.38.4", + "resolved": "https://registry.npmjs.org/@vercel/ncc/-/ncc-0.38.4.tgz", + "integrity": "sha512-8LwjnlP39s08C08J5NstzriPvW1SP8Zfpp1BvC2sI35kPeZnHfxVkCwu4/+Wodgnd60UtT1n8K8zw+Mp7J9JmQ==", + "dev": true, + "license": "MIT", + "bin": { + "ncc": "dist/ncc/cli.js" + } + }, + "node_modules/acorn": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.0.tgz", + "integrity": "sha512-iy2GE3MHrYTL5lrCtMZ0X1KLEKKUjmK0kzwcnefhR66txcEmXZD2YWgR5GNdcEwkNx3a0siYkSvl0vIC+Svjmg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.39.0", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-yaml": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz", + "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.46.2", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.46.2.tgz", + "integrity": "sha512-vbw8bOmiuYNdzzV3lsiWv6sRwjyuKJMQqWulBOU7M0RrxedXledX8G8kBbQeiOYDnTfiXz0Y4081E1QMNB6iQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.46.2", + "@typescript-eslint/parser": "8.46.2", + "@typescript-eslint/typescript-estree": "8.46.2", + "@typescript-eslint/utils": "8.46.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..c5f1854 --- /dev/null +++ b/package.json @@ -0,0 +1,33 @@ +{ + "name": "xops", + "version": "0.1.0-dev", + "private": true, + "description": "CI/CD-native value transfer engine. A repository event produces a payment intent, a human signs it, the workflow settles it.", + "license": "MIT", + "type": "module", + "engines": { + "node": ">=20" + }, + "bin": { + "xops": "dist/cli.js" + }, + "scripts": { + "build": "ncc build src/main.ts -o dist", + "typecheck": "tsc --noEmit", + "compile": "tsc", + "lint": "eslint .", + "test": "npm run compile && node scripts/run-tests.mjs", + "check:boundary": "node scripts/check-boundary.mjs", + "check:deps": "node scripts/check-deps.mjs", + "check": "npm run lint && npm run check:boundary && npm run check:deps && npm run test" + }, + "dependencies": {}, + "devDependencies": { + "@eslint/js": "9.39.0", + "@types/node": "22.19.0", + "@vercel/ncc": "0.38.4", + "eslint": "9.39.0", + "typescript": "5.9.3", + "typescript-eslint": "8.46.2" + } +} diff --git a/scripts/check-boundary.mjs b/scripts/check-boundary.mjs new file mode 100644 index 0000000..d2ea192 --- /dev/null +++ b/scripts/check-boundary.mjs @@ -0,0 +1,112 @@ +#!/usr/bin/env node +// I1 — nothing under these roots may import a chain library or name a chain +// primitive. The lint rule catches imports; this catches everything else. +// +// Usage: node scripts/check-boundary.mjs [roots...] + +import { readFileSync } from "node:fs"; +import { readdir, stat } from "node:fs/promises"; +import { join, relative, resolve, extname } from "node:path"; + +const DEFAULT_ROOTS = ["src/core", "src/adapters", "src/resolvers"]; +const EXTENSIONS = new Set([".ts", ".mts", ".cts", ".js", ".mjs", ".cjs", ".json"]); + +const RULES = [ + { + id: "chain-library-import", + why: "chain libraries and payment SDKs belong to a driver", + re: /\b(?:from|import|require\s*\()\s*["'](?:ethers|viem|thirdweb|web3|ox|x402|bn\.js|elliptic|keccak|js-sha3|@ethersproject\/[^"']+|@noble\/[^"']+|@x402\/[^"']+|@scure\/[^"']+|@solana\/[^"']+)["']/, + }, + { + id: "driver-or-asset-import", + why: "the boundary points one way: drivers may import core, never the reverse", + re: /\b(?:from|import|require\s*\()\s*["'][^"']*(?:\/drivers\/|\/assets\/|chains\.json)/, + }, + { + id: "hash-primitive", + why: "hashing is a rail primitive", + re: /\bkeccak_?256\b|\bsha3_\d+\b|\bsecp256k1\b|\bblake2b\b/i, + }, + { + id: "chain-primitive-type", + why: "bytes32 and friends are rail types", + re: /\bbytes32\b|\buint256\b/, + }, + { + id: "hex-address-literal", + why: "token and treasury addresses live in the chain registry", + re: /["']0x[0-9a-fA-F]{40}["']/, + }, + { + id: "chain-identity-literal", + why: "core must never branch on a specific chain", + re: /\beip155\b|\bchainId\b|\bverifyingContract\b|\bdomainSeparator\b/, + }, + { + id: "abi-encoding", + why: "calldata encoding is driver work", + re: /\bencodeFunctionData\b|\bencodeAbiParameters\b|\babiEncode\b|\btransferWithAuthorization\b/, + }, + { + id: "network-endpoint", + why: "core and adapters make no rail network calls", + re: /https?:\/\/[^\s"']*(?:rpc|infura|alchemy|quicknode|basescan|etherscan|facilitator)/i, + }, +]; + +async function walk(dir, out = []) { + for (const entry of await readdir(dir, { withFileTypes: true })) { + const path = join(dir, entry.name); + if (entry.isDirectory()) { + if (entry.name === "node_modules" || entry.name === ".git") continue; + await walk(path, out); + } else if (EXTENSIONS.has(extname(entry.name))) { + out.push(path); + } + } + return out; +} + +const roots = process.argv.slice(2).length ? process.argv.slice(2) : DEFAULT_ROOTS; +const violations = []; +let scanned = 0; + +for (const root of roots) { + const absolute = resolve(root); + try { + await stat(absolute); + } catch { + console.error(`boundary: root not found: ${root}`); + process.exit(2); + } + + for (const file of await walk(absolute)) { + scanned += 1; + const lines = readFileSync(file, "utf8").split(/\r?\n/); + lines.forEach((line, index) => { + for (const rule of RULES) { + if (rule.re.test(line)) { + violations.push({ + file: relative(process.cwd(), file), + line: index + 1, + rule: rule.id, + why: rule.why, + text: line.trim(), + }); + } + } + }); + } +} + +if (violations.length) { + console.error(`I1 violated — ${violations.length} finding(s):\n`); + for (const v of violations) { + console.error(` ${v.file}:${v.line} [${v.rule}] ${v.why}`); + console.error(` ${v.text}\n`); + } + console.error("Move this into a driver. See AGENTS.md — THE ONE RULE."); + process.exit(1); +} + +console.log(`I1 clean — ${scanned} file(s) across ${roots.join(", ")}`); diff --git a/scripts/check-deps.mjs b/scripts/check-deps.mjs new file mode 100644 index 0000000..f5963eb --- /dev/null +++ b/scripts/check-deps.mjs @@ -0,0 +1,30 @@ +#!/usr/bin/env node +// I11 — core runtime dependencies stay at or below two packages. +// Week 1 expects zero: if the boundary is right, nothing needs a crypto library yet. + +import { execFileSync } from "node:child_process"; + +const LIMIT = Number(process.env["XOPS_DEP_LIMIT"] ?? 2); + +const raw = execFileSync("npm", ["ls", "--omit=dev", "--all", "--json"], { + encoding: "utf8", + shell: process.platform === "win32", + stdio: ["ignore", "pipe", "ignore"], +}); + +const names = new Set(); +(function collect(node) { + for (const [name, child] of Object.entries(node.dependencies ?? {})) { + names.add(name); + collect(child); + } +})(JSON.parse(raw)); + +const count = names.size; +console.log(`runtime dependencies: ${count} (limit ${LIMIT})`); +for (const name of [...names].sort()) console.log(` ${name}`); + +if (count > LIMIT) { + console.error(`\nI11 violated: ${count} runtime dependencies exceeds the limit of ${LIMIT}.`); + process.exit(1); +} diff --git a/scripts/run-tests.mjs b/scripts/run-tests.mjs new file mode 100644 index 0000000..7f0fcd0 --- /dev/null +++ b/scripts/run-tests.mjs @@ -0,0 +1,31 @@ +#!/usr/bin/env node +// `node --test` accepts glob patterns only on Node 22+ and directory arguments +// inconsistently across platforms. CI runs Node 20 to match the action runtime, +// so enumerate the compiled test files and hand them over explicitly. + +import { readdir } from "node:fs/promises"; +import { spawnSync } from "node:child_process"; +import { join, resolve } from "node:path"; + +const ROOT = resolve("build/test"); + +async function collect(dir, out = []) { + for (const entry of await readdir(dir, { withFileTypes: true })) { + const path = join(dir, entry.name); + if (entry.isDirectory()) await collect(path, out); + else if (entry.name.endsWith(".test.js")) out.push(path); + } + return out; +} + +const files = (await collect(ROOT)).sort(); +if (files.length === 0) { + console.error(`No compiled test files under ${ROOT}. Run 'npm run compile' first.`); + process.exit(1); +} + +const run = spawnSync(process.execPath, ["--test", ...process.argv.slice(2), ...files], { + stdio: "inherit", +}); + +process.exit(run.status ?? 1); diff --git a/src/adapters/github/outputs.ts b/src/adapters/github/outputs.ts new file mode 100644 index 0000000..766e48b --- /dev/null +++ b/src/adapters/github/outputs.ts @@ -0,0 +1,25 @@ +import { appendFileSync } from "node:fs"; +import { randomUUID } from "node:crypto"; + +export type OutputName = + | "TX_HASH" + | "EXPLORER_URL" + | "STATUS" + | "ERROR_CODE" + | "IDEMPOTENCY_KEY"; + +/** + * Written on every path, including failures. The delimiter is random per call so + * a value containing a newline cannot forge another output. + */ +export function writeOutputs(values: Partial>): void { + const file = process.env["GITHUB_OUTPUT"]; + if (!file) return; + + const delimiter = `XOPS_EOF_${randomUUID()}`; + let block = ""; + for (const [name, value] of Object.entries(values)) { + block += `${name}<<${delimiter}\n${value ?? ""}\n${delimiter}\n`; + } + if (block) appendFileSync(file, block, "utf8"); +} diff --git a/src/core/defaults.ts b/src/core/defaults.ts new file mode 100644 index 0000000..9a578de --- /dev/null +++ b/src/core/defaults.ts @@ -0,0 +1,11 @@ +import type { SettlementMode } from "./types.js"; + +/** I5. Real settlement is always an explicit opt-in. */ +export const DEFAULT_SETTLEMENT_MODE: SettlementMode = "dry-run"; + +/** Kill switch default. Honored before policy evaluation. */ +export const DEFAULT_SETTLEMENT_ENABLED = true; + +/** Clock-skew allowance and authorization window, in seconds. */ +export const VALID_AFTER_SKEW_SECONDS = 60; +export const VALID_BEFORE_WINDOW_SECONDS = 900; diff --git a/src/core/errors.ts b/src/core/errors.ts new file mode 100644 index 0000000..955795a --- /dev/null +++ b/src/core/errors.ts @@ -0,0 +1,158 @@ +export const ERROR_CODES = [ + "AUTH_ALREADY_USED", + "AUTH_EXPIRED", + "AUTH_NOT_YET_VALID", + "SIGNER_MISMATCH", + "DOMAIN_MISMATCH", + "AMOUNT_MISMATCH", + "RECIPIENT_MISMATCH", + "IDENTITY_UNRESOLVED", + "POLICY_DENIED", + "AMOUNT_CAP_EXCEEDED", + "INSUFFICIENT_BALANCE", + "INSUFFICIENT_GAS", + "SIMULATION_REVERT", + "RPC_UNAVAILABLE", + "DRIVER_NOT_FOUND", + "TIER_VIOLATION", + "NO_REPLAY_PROTECTION", +] as const; + +export type ErrorCode = (typeof ERROR_CODES)[number]; + +export type Retry = "no" | "user" | "auto"; + +export interface ErrorSpec { + meaning: string; + /** Safe to render in a PR comment. Never a stack trace. */ + comment: string; + retry: Retry; + /** AUTH_ALREADY_USED is the one code that means the payout happened (I8). */ + success: boolean; +} + +export const ERRORS: Record = { + AUTH_ALREADY_USED: { + meaning: "Authorization nonce already consumed", + comment: "Already paid — see the original transaction.", + retry: "no", + success: true, + }, + AUTH_EXPIRED: { + meaning: "Past validBefore", + comment: "Authorization expired. Re-sign; the window is 15 minutes.", + retry: "user", + success: false, + }, + AUTH_NOT_YET_VALID: { + meaning: "Before validAfter", + comment: "Clock skew between signer and node. Wait 60 seconds and retry.", + retry: "auto", + success: false, + }, + SIGNER_MISMATCH: { + meaning: "Recovered signer is not the expected payer", + comment: "Wrong wallet connected. Sign with the treasury account.", + retry: "user", + success: false, + }, + DOMAIN_MISMATCH: { + meaning: "Typed-data domain does not match the asset registry", + comment: "Check the network and asset in `.xops.yml`.", + retry: "no", + success: false, + }, + AMOUNT_MISMATCH: { + meaning: "Payload amount differs from requirements", + comment: "The payload was tampered with and was not settled.", + retry: "no", + success: false, + }, + RECIPIENT_MISMATCH: { + meaning: "Payload recipient differs from requirements", + comment: "The payload was tampered with and was not settled.", + retry: "no", + success: false, + }, + IDENTITY_UNRESOLVED: { + meaning: "No resolver matched the recipient identity", + comment: "Register a payout address or use an inline address.", + retry: "user", + success: false, + }, + POLICY_DENIED: { + meaning: "A policy condition failed", + comment: "A required policy condition was not met. Nothing was settled.", + retry: "no", + success: false, + }, + AMOUNT_CAP_EXCEEDED: { + meaning: "Amount above policy.max_per_payout", + comment: "Above the configured per-payout cap. Raise the cap or split the payout.", + retry: "no", + success: false, + }, + INSUFFICIENT_BALANCE: { + meaning: "Treasury balance short of the payout", + comment: "The treasury does not hold enough of the asset.", + retry: "user", + success: false, + }, + INSUFFICIENT_GAS: { + meaning: "Broadcasting account cannot pay fees", + comment: "Fund the broadcasting account with the network's native token.", + retry: "user", + success: false, + }, + SIMULATION_REVERT: { + meaning: "Pre-broadcast simulation reverted", + comment: "Simulation reverted before broadcast, so no fees were spent.", + retry: "no", + success: false, + }, + RPC_UNAVAILABLE: { + meaning: "Every configured endpoint failed", + comment: "No endpoint responded. Retryable; set `rpc_url` to override.", + retry: "auto", + success: false, + }, + DRIVER_NOT_FOUND: { + meaning: "No driver registered for this network and scheme", + comment: "Unsupported network/scheme combination.", + retry: "no", + success: false, + }, + TIER_VIOLATION: { + meaning: "Driver requires secrets or custody and cannot run in Tier 0", + comment: "This driver must run as a separate service you operate.", + retry: "no", + success: false, + }, + NO_REPLAY_PROTECTION: { + meaning: "Driver does not declare an exactly-once guarantee", + comment: "Cannot settle without replay protection.", + retry: "no", + success: false, + }, +}; + +export class XOpsError extends Error { + readonly code: ErrorCode; + readonly details: Record; + + constructor(code: ErrorCode, message?: string, details: Record = {}) { + super(message ?? `${code}: ${ERRORS[code].meaning}`); + this.name = "XOpsError"; + this.code = code; + this.details = details; + } +} + +export function isErrorCode(value: unknown): value is ErrorCode { + return typeof value === "string" && (ERROR_CODES as readonly string[]).includes(value); +} + +/** I8 lives here: one code in the taxonomy is a success. */ +export function isSuccessCode(code: ErrorCode): boolean { + return ERRORS[code].success; +} diff --git a/src/core/idempotency.ts b/src/core/idempotency.ts new file mode 100644 index 0000000..31e9087 --- /dev/null +++ b/src/core/idempotency.ts @@ -0,0 +1,32 @@ +import type { IdempotencyKey, Intent } from "./types.js"; + +/** + * Returns a canonical string. No hashing, no rail primitives — a driver derives + * its rail's replay token from this string. + * + * `amount` is excluded on purpose: with amount in the key, `/send alice 50` + * corrected to `/send alice 500` yields two keys and Alice receives 550. + * Excluded, the correction collides and requires an explicit `round` bump. + */ +export function canonical(k: IdempotencyKey): string { + return ( + `xops:v${k.v}|${k.source.platform}:${k.source.repo}#${k.source.ref}` + + `|${k.recipient.toLowerCase()}|${k.network}|${k.asset}|${k.round}` + ); +} + +export function keyFor(intent: Intent): IdempotencyKey { + const { source } = intent; + return { + v: 1, + source: { + platform: source.platform, + repo: source.platform === "github" ? source.repo : source.project, + ref: source.ref, + }, + recipient: intent.recipient, + asset: intent.asset, + network: intent.network, + round: intent.round, + }; +} diff --git a/src/core/intent.ts b/src/core/intent.ts new file mode 100644 index 0000000..175995d --- /dev/null +++ b/src/core/intent.ts @@ -0,0 +1,61 @@ +import type { Intent } from "./types.js"; + +// CAIP-2: namespace:reference. Says nothing about what the namespace means. +const CAIP2 = /^[-a-z0-9]{3,8}:[-_a-zA-Z0-9]{1,32}$/; +const ATOMIC_AMOUNT = /^[0-9]+$/; + +export interface RawIntent { + platform?: string | undefined; + repo?: string | undefined; + ref?: string | undefined; + actor?: string | undefined; + recipient?: string | undefined; + amount?: string | undefined; + asset?: string | undefined; + network?: string | undefined; + scheme?: string | undefined; + round?: string | undefined; +} + +function required(raw: RawIntent, field: keyof RawIntent): string { + const value = raw[field]?.trim(); + if (!value) throw new Error(`Missing required intent field: ${field}`); + return value; +} + +export function parseIntent(raw: RawIntent): Intent { + const platform = (raw.platform ?? "github").trim(); + if (platform !== "github" && platform !== "gitlab") { + throw new Error(`Unsupported platform: ${platform}`); + } + + const repo = required(raw, "repo"); + const ref = required(raw, "ref"); + const actor = required(raw, "actor"); + const recipient = required(raw, "recipient"); + const amount = required(raw, "amount"); + const asset = required(raw, "asset"); + const network = required(raw, "network"); + const scheme = required(raw, "scheme"); + + if (!ATOMIC_AMOUNT.test(amount)) { + throw new Error( + `amount must be a whole number of atomic units as a string, got "${amount}"`, + ); + } + if (!CAIP2.test(network)) { + throw new Error(`network must be a CAIP-2 identifier, got "${network}"`); + } + + const round = Number(raw.round ?? "0"); + if (!Number.isInteger(round) || round < 0) { + throw new Error(`round must be a non-negative integer, got "${raw.round}"`); + } + + const source = + platform === "github" + ? ({ platform, repo, ref, actor } as const) + : ({ platform, project: repo, ref, actor } as const); + + return { source, recipient, amount, asset, network, scheme, round }; +} diff --git a/src/core/types.ts b/src/core/types.ts new file mode 100644 index 0000000..88de3a6 --- /dev/null +++ b/src/core/types.ts @@ -0,0 +1,61 @@ +// Mirrors x402 v2. Do not invent fields. +// L0-L5 all speak these types. None of them knows what a chain is. + +export interface PaymentRequirements { + scheme: string; + network: string; + amount: string; + asset: string; + payTo: string; + maxTimeoutSeconds: number; + extra?: Record; +} + +export interface PaymentPayload { + x402Version: 2; + scheme: string; + network: string; + // Scheme-defined. Core NEVER reads a field inside this. + payload: Record; +} + +export interface SettlementResponse { + success: boolean; + transaction?: string; + network?: string; + payer?: string; + errorReason?: string; +} + +export type IntentSource = + | { platform: "github"; repo: string; ref: string; actor: string } + | { platform: "gitlab"; project: string; ref: string; actor: string }; + +export interface Intent { + source: IntentSource; + recipient: string; + amount: string; + asset: string; + network: string; + scheme: string; + round: number; +} + +export interface IdempotencyKey { + v: 1; + source: { platform: string; repo: string; ref: string }; + recipient: string; + asset: string; + network: string; + round: number; + // `amount` deliberately absent — see AGENTS.md +} + +export interface PayoutTarget { + rail: string; + address: string; + attestations?: unknown[]; + resolvedBy: string; +} + +export type SettlementMode = "dry-run" | "facilitator" | "self" | "auto"; diff --git a/src/drivers/registry.ts b/src/drivers/registry.ts new file mode 100644 index 0000000..06be9ce --- /dev/null +++ b/src/drivers/registry.ts @@ -0,0 +1,84 @@ +import { XOpsError } from "../core/errors.js"; +import type { PaymentPayload, PaymentRequirements, SettlementResponse } from "../core/types.js"; +import type { + RequirementsContext, + SettlementDriver, + Tier, + VerifyResult, +} from "./types.js"; + +function tierViolationMessage(id: string): string { + return [ + `Driver ${id} takes custody of funds and cannot run in-process.`, + "Custodial settlement must run as a separate service you operate:", + " settlement: { mode: facilitator, url: https://your-service }", + ].join("\n"); +} + +/** + * The only layer that knows rails exist is the driver behind this registry. + * Everything above resolves `(network × scheme)` and gets an interface back. + */ +export class DriverRegistry { + readonly tier: Tier; + private readonly drivers: SettlementDriver[] = []; + + constructor(tier: Tier = 0) { + this.tier = tier; + } + + /** I3: a driver needing secrets or custody cannot register in Tier 0. */ + register(driver: SettlementDriver): void { + const { needsSecret, custodial } = driver.capabilities; + if (this.tier === 0 && (needsSecret || custodial)) { + throw new XOpsError("TIER_VIOLATION", tierViolationMessage(driver.id), { + driver: driver.id, + needsSecret, + custodial, + tier: this.tier, + }); + } + if (this.drivers.some((d) => d.id === driver.id)) { + throw new Error(`Driver ${driver.id} is already registered`); + } + this.drivers.push(driver); + } + + list(): readonly SettlementDriver[] { + return this.drivers; + } + + resolve(network: string, scheme: string): SettlementDriver { + const driver = this.drivers.find((d) => d.supports(network, scheme)); + if (!driver) { + throw new XOpsError( + "DRIVER_NOT_FOUND", + `No driver registered for scheme "${scheme}" on network "${network}"`, + { network, scheme, registered: this.drivers.map((d) => d.id) }, + ); + } + return driver; + } + + buildRequirements(ctx: RequirementsContext): PaymentRequirements { + const driver = this.resolve(ctx.intent.network, ctx.intent.scheme); + return driver.buildRequirements(ctx); + } + + async verify(p: PaymentPayload, r: PaymentRequirements): Promise { + return this.resolve(r.network, r.scheme).verify(p, r); + } + + /** I9: refuse before the driver is reached if it declares no exactly-once guarantee. */ + async settle(p: PaymentPayload, r: PaymentRequirements): Promise { + const driver = this.resolve(r.network, r.scheme); + if (this.tier === 0 && !driver.capabilities.nativeReplayProtection) { + throw new XOpsError( + "NO_REPLAY_PROTECTION", + `Driver ${driver.id} declares no replay protection and cannot settle in Tier 0`, + { driver: driver.id, tier: this.tier }, + ); + } + return driver.settle(p, r); + } +} diff --git a/src/drivers/types.ts b/src/drivers/types.ts new file mode 100644 index 0000000..f25b0b4 --- /dev/null +++ b/src/drivers/types.ts @@ -0,0 +1,50 @@ +import type { + Intent, + PaymentPayload, + PaymentRequirements, + PayoutTarget, + SettlementResponse, +} from "../core/types.js"; +import type { ErrorCode } from "../core/errors.js"; + +export interface Capabilities { + offlineVerify: boolean; + needsGas: boolean; + needsSecret: boolean; + custodial: boolean; + /** The rail enforces exactly-once, or the driver does. Tier 0 refuses to settle without it. */ + nativeReplayProtection: boolean; +} + +export interface VerifyResult { + isValid: boolean; + payer?: string; + reason?: ErrorCode; +} + +/** + * `target` and `idempotencyKey` are passed in rather than derived from the intent: + * core resolves the identity and emits the canonical key, and the driver turns + * both into rail-specific values. Core never learns what the address means. + */ +export interface RequirementsContext { + intent: Intent; + target: PayoutTarget; + idempotencyKey: string; +} + +export interface SettlementDriver { + readonly id: string; + readonly capabilities: Capabilities; + supports(network: string, scheme: string): boolean; + buildRequirements(ctx: RequirementsContext): PaymentRequirements; + verify(p: PaymentPayload, r: PaymentRequirements): Promise; + settle(p: PaymentPayload, r: PaymentRequirements): Promise; +} + +/** + * 0 — runs in the adopter's CI, holds nothing. + * 1 — adopter-operated process with its own credentials. + * 2 — a separate legal entity that may take custody. + */ +export type Tier = 0 | 1 | 2; diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..3d4bac1 --- /dev/null +++ b/src/main.ts @@ -0,0 +1,61 @@ +import { writeOutputs } from "./adapters/github/outputs.js"; +import { DEFAULT_SETTLEMENT_MODE } from "./core/defaults.js"; +import { XOpsError } from "./core/errors.js"; +import { canonical, keyFor } from "./core/idempotency.js"; +import { parseIntent } from "./core/intent.js"; +import { DriverRegistry } from "./drivers/registry.js"; +import { InlineAddressResolver, ResolverChain } from "./resolvers/index.js"; + +function input(name: string): string | undefined { + return process.env[`INPUT_${name.toUpperCase().replace(/ /g, "_")}`]; +} + +async function run(): Promise { + const intent = parseIntent({ + platform: "github", + repo: input("repo") ?? process.env["GITHUB_REPOSITORY"], + ref: input("ref") ?? process.env["GITHUB_REF"], + actor: input("actor") ?? process.env["GITHUB_ACTOR"], + recipient: input("recipient"), + amount: input("amount"), + asset: input("asset"), + network: input("network"), + scheme: input("scheme"), + round: input("round"), + }); + + const idempotencyKey = canonical(keyFor(intent)); + const resolvers = new ResolverChain([new InlineAddressResolver()]); + const target = await resolvers.resolve(intent.recipient, { rail: intent.network }); + + console.log("intent:"); + console.log(JSON.stringify(intent, null, 2)); + console.log("payout target:"); + console.log(JSON.stringify(target, null, 2)); + console.log(`idempotency key: ${idempotencyKey}`); + + const mode = input("mode") ?? DEFAULT_SETTLEMENT_MODE; + if (mode === "dry-run") { + console.log("mode: dry-run — nothing was settled."); + writeOutputs({ STATUS: "dry-run", IDEMPOTENCY_KEY: idempotencyKey, ERROR_CODE: "" }); + return 0; + } + + // No settlement driver ships yet. Registry resolution is the honest failure. + const registry = new DriverRegistry(0); + registry.resolve(intent.network, intent.scheme); + return 0; +} + +run().then( + (code) => { + process.exitCode = code; + }, + (err: unknown) => { + const code = err instanceof XOpsError ? err.code : ""; + const message = err instanceof Error ? err.message : String(err); + console.error(message); + writeOutputs({ STATUS: "error", ERROR_CODE: code }); + process.exitCode = 1; + }, +); diff --git a/src/resolvers/index.ts b/src/resolvers/index.ts new file mode 100644 index 0000000..647daf2 --- /dev/null +++ b/src/resolvers/index.ts @@ -0,0 +1,31 @@ +import { XOpsError } from "../core/errors.js"; +import type { PayoutTarget } from "../core/types.js"; +import type { Resolver, ResolveContext } from "./types.js"; + +export type { Resolver, ResolveContext } from "./types.js"; +export { InlineAddressResolver } from "./inline-address.js"; + +export class ResolverChain { + private readonly resolvers: Resolver[]; + + constructor(resolvers: Resolver[] = []) { + this.resolvers = resolvers; + } + + use(resolver: Resolver): this { + this.resolvers.push(resolver); + return this; + } + + async resolve(identity: string, ctx: ResolveContext): Promise { + for (const resolver of this.resolvers) { + if (resolver.canResolve(identity)) { + return resolver.resolve(identity, ctx); + } + } + throw new XOpsError("IDENTITY_UNRESOLVED", `No resolver matched "${identity}"`, { + identity, + tried: this.resolvers.map((r) => r.id), + }); + } +} diff --git a/src/resolvers/inline-address.ts b/src/resolvers/inline-address.ts new file mode 100644 index 0000000..e77bde5 --- /dev/null +++ b/src/resolvers/inline-address.ts @@ -0,0 +1,34 @@ +import type { PayoutTarget } from "../core/types.js"; +import type { Resolver, ResolveContext } from "./types.js"; + +const INLINE_PREFIX = "inline:"; + +/** + * Takes the payout address verbatim from the identity string. It does not + * validate the address format — the format belongs to a rail, and only the + * driver for that rail may judge it. + * + * Accepts `inline:` and bare values. `@handle` identities are left for + * lookup-based resolvers. + */ +export class InlineAddressResolver implements Resolver { + readonly id = "inline-address"; + + canResolve(identity: string): boolean { + const value = strip(identity); + return value.length > 0 && !value.startsWith("@") && !/\s/.test(value); + } + + resolve(identity: string, ctx: ResolveContext): Promise { + return Promise.resolve({ + rail: ctx.rail, + address: strip(identity), + resolvedBy: this.id, + }); + } +} + +function strip(identity: string): string { + const trimmed = identity.trim(); + return trimmed.startsWith(INLINE_PREFIX) ? trimmed.slice(INLINE_PREFIX.length).trim() : trimmed; +} diff --git a/src/resolvers/types.ts b/src/resolvers/types.ts new file mode 100644 index 0000000..81144bd --- /dev/null +++ b/src/resolvers/types.ts @@ -0,0 +1,16 @@ +import type { PayoutTarget } from "../core/types.js"; + +export interface ResolveContext { + /** CAIP-2 network or a non-chain rail id. Opaque to the resolver. */ + rail: string; +} + +/** + * Identity (string) → PayoutTarget. ENS, `.well-known` lookup and ERC-8004 + * agent ids are all just resolvers behind this interface. + */ +export interface Resolver { + readonly id: string; + canResolve(identity: string): boolean; + resolve(identity: string, ctx: ResolveContext): Promise; +} diff --git a/test/boundary/i1.test.ts b/test/boundary/i1.test.ts new file mode 100644 index 0000000..c85b97a --- /dev/null +++ b/test/boundary/i1.test.ts @@ -0,0 +1,90 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { execFileSync, spawnSync } from "node:child_process"; +import { mkdtempSync, rmSync, writeFileSync, mkdirSync } from "node:fs"; +import { join } from "node:path"; +import { tmpdir } from "node:os"; +import { ESLint } from "eslint"; + +const BOUNDARY = join("scripts", "check-boundary.mjs"); +const RESTRICTED = "@typescript-eslint/no-restricted-imports"; + +const eslint = new ESLint({ cwd: process.cwd() }); + +async function probe(filePath: string, source: string) { + const [result] = await eslint.lintText(source, { filePath }); + assert.ok(result, `eslint produced no result for ${filePath}`); + return { + errorCount: result.errorCount, + restricted: result.messages.find((m) => m.ruleId === RESTRICTED), + messages: result.messages, + }; +} + +const CHAIN_IMPORT = 'import { keccak_256 } from "@noble/hashes/sha3.js";\nexport const h = keccak_256;\n'; + +test("I1: the lint rule rejects a chain import added to src/core", async () => { + const { errorCount, restricted, messages } = await probe( + join("src", "core", "__i1_probe__.ts"), + CHAIN_IMPORT, + ); + + assert.ok(errorCount > 0, "a chain import in src/core must fail the lint"); + assert.ok(restricted, `expected ${RESTRICTED}, got ${JSON.stringify(messages)}`); + assert.match(restricted.message, /I1/); +}); + +test("I1: the rule covers adapters and resolvers too", async () => { + for (const dir of ["adapters", "resolvers"]) { + const { restricted } = await probe(join("src", dir, "__i1_probe__.ts"), CHAIN_IMPORT); + assert.ok(restricted, `${dir} must be covered by I1`); + } +}); + +test("I1: the same import is allowed inside a driver", async () => { + const { restricted } = await probe( + join("src", "drivers", "exact-eip155", "__i1_probe__.ts"), + CHAIN_IMPORT, + ); + assert.equal(restricted, undefined, "drivers are where rails are allowed to live"); +}); + +test("I1: core may not import a driver", async () => { + const { restricted } = await probe( + join("src", "core", "__i1_probe__.ts"), + 'import { DriverRegistry } from "../drivers/registry.js";\nexport const r = DriverRegistry;\n', + ); + + assert.ok(restricted, "the boundary must point one way"); + assert.match(restricted.message, /boundary points one way/); +}); + +test("the boundary grep catches primitives no import rule would see", () => { + const dir = mkdtempSync(join(tmpdir(), "xops-i1-")); + try { + mkdirSync(join(dir, "core"), { recursive: true }); + writeFileSync( + join(dir, "core", "leak.ts"), + [ + "export const NONCE_TYPE = 'bytes32';", + "export const USDC = '0x036CbD53842c5426634e7929541eC2318f3dCF7e';", + "export const isBase = (n: string) => n === 'eip155:84532';", + ].join("\n"), + "utf8", + ); + + const run = spawnSync(process.execPath, [BOUNDARY, dir], { encoding: "utf8" }); + + assert.equal(run.status, 1, run.stdout + run.stderr); + assert.match(run.stderr, /chain-primitive-type/); + assert.match(run.stderr, /hex-address-literal/); + assert.match(run.stderr, /chain-identity-literal/); + } finally { + rmSync(dir, { recursive: true, force: true }); + } +}); + +test("the boundary grep is clean on the real tree", () => { + const out = execFileSync(process.execPath, [BOUNDARY], { encoding: "utf8" }); + assert.match(out, /I1 clean/); +}); diff --git a/test/core/defaults.test.ts b/test/core/defaults.test.ts new file mode 100644 index 0000000..09357d4 --- /dev/null +++ b/test/core/defaults.test.ts @@ -0,0 +1,39 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { readFileSync } from "node:fs"; + +import { DEFAULT_SETTLEMENT_MODE, DEFAULT_SETTLEMENT_ENABLED } from "../../src/core/defaults.js"; +import { ERRORS, ERROR_CODES, XOpsError, isSuccessCode } from "../../src/core/errors.js"; + +test("I5: the default settlement mode is dry-run", () => { + assert.equal(DEFAULT_SETTLEMENT_MODE, "dry-run"); + assert.equal(DEFAULT_SETTLEMENT_ENABLED, true); +}); + +test("I5: the action declares dry-run as its default too", () => { + const action = readFileSync("action.yml", "utf8"); + const mode = /mode:\s*[\s\S]*?default:\s*"([^"]+)"/.exec(action); + assert.ok(mode, "action.yml must declare a mode input with a default"); + assert.equal(mode[1], "dry-run"); +}); + +test("I8: AUTH_ALREADY_USED is the only success code", () => { + const successes = ERROR_CODES.filter(isSuccessCode); + assert.deepEqual(successes, ["AUTH_ALREADY_USED"]); +}); + +test("every code carries a comment safe to render in a PR", () => { + for (const code of ERROR_CODES) { + const spec = ERRORS[code]; + assert.ok(spec.comment.length > 0, `${code} has no comment`); + assert.ok(!spec.comment.includes("at "), `${code} looks like it leaks a stack frame`); + assert.ok(["no", "user", "auto"].includes(spec.retry), `${code} has no retry policy`); + } +}); + +test("XOpsError carries the code and structured details, not a stack for humans", () => { + const err = new XOpsError("AMOUNT_CAP_EXCEEDED", undefined, { cap: "100.00" }); + assert.equal(err.code, "AMOUNT_CAP_EXCEEDED"); + assert.equal(err.details["cap"], "100.00"); + assert.match(err.message, /AMOUNT_CAP_EXCEEDED/); +}); diff --git a/test/core/idempotency.test.ts b/test/core/idempotency.test.ts new file mode 100644 index 0000000..8e4b0c2 --- /dev/null +++ b/test/core/idempotency.test.ts @@ -0,0 +1,98 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { canonical, keyFor } from "../../src/core/idempotency.js"; +import type { IdempotencyKey, Intent } from "../../src/core/types.js"; + +function intent(overrides: Partial = {}): Intent { + return { + source: { platform: "github", repo: "AOSSIE-Org/xops", ref: "pull/42", actor: "maintainer" }, + recipient: "0xF39FD6E51AAD88F6F4CE6AB8827279CFFFB92266", + amount: "2500000", + asset: "USDC", + network: "eip155:84532", + scheme: "exact", + round: 0, + ...overrides, + }; +} + +// Deterministic generator — a property test that reproduces exactly on every run. +function lcg(seed: number): () => number { + let state = seed; + return () => { + state = (state * 1664525 + 1013904223) % 4294967296; + return state / 4294967296; + }; +} + +test("I7: identical inputs produce an identical string", () => { + const rand = lcg(20260821); + const pick = (xs: readonly T[]): T => xs[Math.floor(rand() * xs.length)] as T; + + for (let i = 0; i < 200; i += 1) { + const key: IdempotencyKey = { + v: 1, + source: { + platform: pick(["github", "gitlab"]), + repo: pick(["a/b", "AOSSIE-Org/xops", "org/repo-with-dash"]), + ref: pick(["pull/1", "pull/42", "refs/heads/main"]), + }, + recipient: pick(["0xabc", "0xABC", "alice.eth"]), + asset: pick(["USDC", "EURC"]), + network: pick(["eip155:84532", "mock:ledger"]), + round: Math.floor(rand() * 4), + }; + assert.equal(canonical(key), canonical(structuredClone(key))); + } +}); + +test("I7: amount is not part of the key", () => { + const cheap = canonical(keyFor(intent({ amount: "50" }))); + const expensive = canonical(keyFor(intent({ amount: "500000000" }))); + + assert.equal(cheap, expensive); + assert.ok(!cheap.includes("50000"), "the amount must not appear in the key"); +}); + +test("timestamps and comment ids cannot leak in — the key is a closed set of fields", () => { + const key = keyFor(intent()); + assert.deepEqual(Object.keys(key).sort(), [ + "asset", + "network", + "recipient", + "round", + "source", + "v", + ]); +}); + +test("round is the deliberate escape hatch for re-paying the same ref", () => { + assert.notEqual(canonical(keyFor(intent({ round: 0 }))), canonical(keyFor(intent({ round: 1 })))); +}); + +test("recipient is case-folded so wallet casing cannot double-pay", () => { + const upper = canonical(keyFor(intent({ recipient: "0xABCDEF" }))); + const lower = canonical(keyFor(intent({ recipient: "0xabcdef" }))); + assert.equal(upper, lower); +}); + +test("asset and network are in the key, so multi-asset payouts need no core change", () => { + const base = canonical(keyFor(intent())); + assert.notEqual(base, canonical(keyFor(intent({ asset: "EURC" })))); + assert.notEqual(base, canonical(keyFor(intent({ network: "mock:ledger" })))); +}); + +test("the key is versioned and returns a plain string", () => { + const key = canonical(keyFor(intent())); + assert.equal(typeof key, "string"); + assert.ok(key.startsWith("xops:v1|")); +}); + +test("gitlab projects map onto the same key shape as github repos", () => { + const key = keyFor( + intent({ source: { platform: "gitlab", project: "group/proj", ref: "mr/7", actor: "dev" } }), + ); + assert.equal(key.source.repo, "group/proj"); + assert.ok(canonical(key).startsWith("xops:v1|gitlab:group/proj#mr/7|")); +}); diff --git a/test/drivers/mock/index.ts b/test/drivers/mock/index.ts new file mode 100644 index 0000000..4d4787a --- /dev/null +++ b/test/drivers/mock/index.ts @@ -0,0 +1,187 @@ +import { VALID_BEFORE_WINDOW_SECONDS } from "../../../src/core/defaults.js"; +import type { + PaymentPayload, + PaymentRequirements, + SettlementResponse, +} from "../../../src/core/types.js"; +import type { + Capabilities, + RequirementsContext, + SettlementDriver, + VerifyResult, +} from "../../../src/drivers/types.js"; + +export const MOCK_NETWORK = "mock:ledger"; +export const MOCK_SCHEME = "exact"; + +interface MockAuthorization { + from: string; + to: string; + value: string; + nonce: string; +} + +const BASE_CAPABILITIES: Capabilities = { + offlineVerify: true, + needsGas: false, + needsSecret: false, + custodial: false, + nativeReplayProtection: true, +}; + +/** + * An in-memory SettlementDriver. It proves the same three things a second chain + * would — the registry resolves (network × scheme), the x402 types carry no EVM + * assumptions, and the whole path runs against a driver that has never heard of + * a hash function — and it costs no faucet and no RPC to keep running. + */ +export class MockDriver implements SettlementDriver { + readonly id: string; + readonly capabilities: Capabilities; + + private readonly balances = new Map(); + private readonly usedNonces = new Map(); + private txCounter = 0; + + /** Test-only witness for "the driver was never reached". */ + settleCalls = 0; + + constructor(overrides: Partial = {}, id = "mock/test") { + this.id = id; + this.capabilities = { ...BASE_CAPABILITIES, ...overrides }; + } + + fund(address: string, amount: bigint): void { + this.balances.set(address, (this.balances.get(address) ?? 0n) + amount); + } + + balanceOf(address: string): bigint { + return this.balances.get(address) ?? 0n; + } + + supports(network: string, scheme: string): boolean { + return network === MOCK_NETWORK && scheme === MOCK_SCHEME; + } + + buildRequirements(ctx: RequirementsContext): PaymentRequirements { + return { + scheme: ctx.intent.scheme, + network: ctx.intent.network, + amount: ctx.intent.amount, + asset: ctx.intent.asset, + payTo: ctx.target.address, + maxTimeoutSeconds: VALID_BEFORE_WINDOW_SECONDS, + extra: { nonce: this.toNonce(ctx.idempotencyKey) }, + }; + } + + /** The rail's replay token, derived from the canonical key. Lives in the driver. */ + private toNonce(idempotencyKey: string): string { + return `mock-nonce:${idempotencyKey}`; + } + + /** Stands in for the human signing in their own wallet. */ + authorize(from: string, r: PaymentRequirements): PaymentPayload { + return { + x402Version: 2, + scheme: r.scheme, + network: r.network, + payload: { + signature: `mock-sig:${from}`, + authorization: { + from, + to: r.payTo, + value: r.amount, + nonce: String(r.extra?.["nonce"]), + }, + }, + }; + } + + verify(p: PaymentPayload, r: PaymentRequirements): Promise { + if (p.scheme !== r.scheme || p.network !== r.network) { + return fail("DOMAIN_MISMATCH"); + } + const auth = readAuthorization(p); + if (!auth || typeof p.payload["signature"] !== "string") { + return fail("SIGNER_MISMATCH"); + } + if (auth.to !== r.payTo) return fail("RECIPIENT_MISMATCH"); + if (auth.value !== r.amount) return fail("AMOUNT_MISMATCH"); + if (p.payload["signature"] !== `mock-sig:${auth.from}`) return fail("SIGNER_MISMATCH"); + return Promise.resolve({ isValid: true, payer: auth.from }); + } + + settle(p: PaymentPayload, r: PaymentRequirements): Promise { + this.settleCalls += 1; + + const auth = readAuthorization(p); + if (!auth) { + return Promise.resolve({ success: false, network: r.network, errorReason: "SIGNER_MISMATCH" }); + } + + // I8: the rail rejecting a consumed nonce means the payout already happened. + const original = this.usedNonces.get(auth.nonce); + if (original) { + return Promise.resolve({ + success: true, + transaction: original, + network: r.network, + payer: auth.from, + errorReason: "AUTH_ALREADY_USED", + }); + } + + const value = BigInt(r.amount); + if (this.balanceOf(auth.from) < value) { + return Promise.resolve({ + success: false, + network: r.network, + payer: auth.from, + errorReason: "INSUFFICIENT_BALANCE", + }); + } + + this.balances.set(auth.from, this.balanceOf(auth.from) - value); + this.balances.set(auth.to, this.balanceOf(auth.to) + value); + + this.txCounter += 1; + const tx = `mocktx-${this.txCounter}`; + this.usedNonces.set(auth.nonce, tx); + + return Promise.resolve({ + success: true, + transaction: tx, + network: r.network, + payer: auth.from, + }); + } +} + +function readAuthorization(p: PaymentPayload): MockAuthorization | undefined { + const auth = p.payload["authorization"]; + if (!auth || typeof auth !== "object") return undefined; + const { from, to, value, nonce } = auth as Record; + if ( + typeof from !== "string" || + typeof to !== "string" || + typeof value !== "string" || + typeof nonce !== "string" + ) { + return undefined; + } + return { from, to, value, nonce }; +} + +function fail(reason: VerifyResult["reason"]): Promise { + return Promise.resolve({ isValid: false, reason }); +} + +export const custodialDriver = (): MockDriver => + new MockDriver({ custodial: true }, "mock/custodial"); + +export const secretDriver = (): MockDriver => + new MockDriver({ needsSecret: true }, "mock/needs-secret"); + +export const noReplayDriver = (): MockDriver => + new MockDriver({ nativeReplayProtection: false }, "mock/no-replay"); diff --git a/test/drivers/registry.test.ts b/test/drivers/registry.test.ts new file mode 100644 index 0000000..57aeb65 --- /dev/null +++ b/test/drivers/registry.test.ts @@ -0,0 +1,86 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { XOpsError } from "../../src/core/errors.js"; +import { DriverRegistry } from "../../src/drivers/registry.js"; +import { + MOCK_NETWORK, + MOCK_SCHEME, + MockDriver, + custodialDriver, + noReplayDriver, + secretDriver, +} from "./mock/index.js"; + +function requirements() { + return { + scheme: MOCK_SCHEME, + network: MOCK_NETWORK, + amount: "1", + asset: "USDC", + payTo: "recipient-1", + maxTimeoutSeconds: 900, + extra: { nonce: "mock-nonce:x" }, + }; +} + +test("a Tier 0 driver registers and resolves by (network x scheme)", () => { + const registry = new DriverRegistry(0); + const driver = new MockDriver(); + registry.register(driver); + + assert.equal(registry.resolve(MOCK_NETWORK, MOCK_SCHEME), driver); +}); + +test("I3: a custodial driver throws at tier-0 registration", () => { + const registry = new DriverRegistry(0); + + assert.throws( + () => registry.register(custodialDriver()), + (err: unknown) => { + assert.ok(err instanceof XOpsError); + assert.equal(err.code, "TIER_VIOLATION"); + assert.match(err.message, /takes custody of funds and cannot run in-process/); + assert.match(err.message, /mode: facilitator/); + return true; + }, + ); + assert.equal(registry.list().length, 0); +}); + +test("I3: a driver needing a secret also throws at tier-0 registration", () => { + const registry = new DriverRegistry(0); + assert.throws(() => registry.register(secretDriver()), { code: "TIER_VIOLATION" }); +}); + +test("I3 is a tier rule, not a ban — Tier 2 may run a custodial driver", () => { + const registry = new DriverRegistry(2); + registry.register(custodialDriver()); + assert.equal(registry.list().length, 1); +}); + +test("I9: a driver without native replay protection refuses to settle", async () => { + const registry = new DriverRegistry(0); + const driver = noReplayDriver(); + registry.register(driver); + + const payload = driver.authorize("payer-1", requirements()); + + await assert.rejects(registry.settle(payload, requirements()), { + code: "NO_REPLAY_PROTECTION", + }); + assert.equal(driver.settleCalls, 0, "the driver must never be reached"); +}); + +test("an unresolvable pair fails with DRIVER_NOT_FOUND, not a crash", () => { + const registry = new DriverRegistry(0); + registry.register(new MockDriver()); + + assert.throws(() => registry.resolve("eip155:84532", "exact"), { code: "DRIVER_NOT_FOUND" }); +}); + +test("registering the same driver id twice is a programming error", () => { + const registry = new DriverRegistry(0); + registry.register(new MockDriver()); + assert.throws(() => registry.register(new MockDriver()), /already registered/); +}); diff --git a/test/e2e/mock-settlement.test.ts b/test/e2e/mock-settlement.test.ts new file mode 100644 index 0000000..ad7ebda --- /dev/null +++ b/test/e2e/mock-settlement.test.ts @@ -0,0 +1,147 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { canonical, keyFor } from "../../src/core/idempotency.js"; +import { parseIntent } from "../../src/core/intent.js"; +import type { PaymentPayload } from "../../src/core/types.js"; +import { DriverRegistry } from "../../src/drivers/registry.js"; +import { InlineAddressResolver, ResolverChain } from "../../src/resolvers/index.js"; +import { MOCK_NETWORK, MockDriver } from "../drivers/mock/index.js"; + +const TREASURY = "treasury-account"; +const CONTRIBUTOR = "contributor-account"; + +async function harness(amount = "2500000") { + const intent = parseIntent({ + platform: "github", + repo: "AOSSIE-Org/xops", + ref: "pull/42", + actor: "maintainer", + recipient: CONTRIBUTOR, + amount, + asset: "USDC", + network: MOCK_NETWORK, + scheme: "exact", + round: "0", + }); + + const driver = new MockDriver(); + const registry = new DriverRegistry(0); + registry.register(driver); + + const resolvers = new ResolverChain([new InlineAddressResolver()]); + const target = await resolvers.resolve(intent.recipient, { rail: intent.network }); + const idempotencyKey = canonical(keyFor(intent)); + const requirements = registry.buildRequirements({ intent, target, idempotencyKey }); + + return { intent, driver, registry, target, idempotencyKey, requirements }; +} + +function tamper(payload: PaymentPayload, patch: Record): PaymentPayload { + const authorization = payload.payload["authorization"] as Record; + return { + ...payload, + payload: { ...payload.payload, authorization: { ...authorization, ...patch } }, + }; +} + +test("the mock driver settles end to end through the registry", async () => { + const { driver, registry, requirements, target } = await harness(); + driver.fund(TREASURY, 10_000_000n); + + const payload = driver.authorize(TREASURY, requirements); + + const verified = await registry.verify(payload, requirements); + assert.equal(verified.isValid, true); + assert.equal(verified.payer, TREASURY); + + const settled = await registry.settle(payload, requirements); + assert.equal(settled.success, true); + assert.ok(settled.transaction); + assert.equal(settled.errorReason, undefined); + + assert.equal(driver.balanceOf(target.address), 2_500_000n); + assert.equal(driver.balanceOf(TREASURY), 7_500_000n); +}); + +test("I8: re-running identical inputs is a success with AUTH_ALREADY_USED", async () => { + const { driver, registry, requirements, intent, target, idempotencyKey } = await harness(); + driver.fund(TREASURY, 10_000_000n); + + const first = await registry.settle(driver.authorize(TREASURY, requirements), requirements); + assert.equal(first.success, true); + + // A workflow re-run rebuilds the same key, so the driver rebuilds the same nonce. + const replayKey = canonical(keyFor(intent)); + assert.equal(replayKey, idempotencyKey); + const replayRequirements = registry.buildRequirements({ + intent, + target, + idempotencyKey: replayKey, + }); + + const second = await registry.settle( + driver.authorize(TREASURY, replayRequirements), + replayRequirements, + ); + + assert.equal(second.success, true, "a consumed nonce is not a failure"); + assert.equal(second.errorReason, "AUTH_ALREADY_USED"); + assert.equal(second.transaction, first.transaction, "the original tx must be reported"); + assert.equal(driver.balanceOf(target.address), 2_500_000n, "the recipient is paid once"); +}); + +test("a tampered recipient is caught before settlement", async () => { + const { driver, registry, requirements } = await harness(); + driver.fund(TREASURY, 10_000_000n); + + const attacked = tamper(driver.authorize(TREASURY, requirements), { to: "attacker" }); + const verified = await registry.verify(attacked, requirements); + + assert.equal(verified.isValid, false); + assert.equal(verified.reason, "RECIPIENT_MISMATCH"); +}); + +test("a tampered amount is caught before settlement", async () => { + const { driver, registry, requirements } = await harness(); + driver.fund(TREASURY, 10_000_000n); + + const attacked = tamper(driver.authorize(TREASURY, requirements), { value: "999999999" }); + const verified = await registry.verify(attacked, requirements); + + assert.equal(verified.isValid, false); + assert.equal(verified.reason, "AMOUNT_MISMATCH"); +}); + +test("a payload for another network never reaches the ledger", async () => { + const { driver, registry, requirements } = await harness(); + const foreign = { ...driver.authorize(TREASURY, requirements), network: "eip155:84532" }; + + const verified = await registry.verify(foreign, requirements); + assert.equal(verified.reason, "DOMAIN_MISMATCH"); +}); + +test("an unfunded treasury fails with INSUFFICIENT_BALANCE and moves nothing", async () => { + const { driver, registry, requirements, target } = await harness(); + + const settled = await registry.settle(driver.authorize(TREASURY, requirements), requirements); + + assert.equal(settled.success, false); + assert.equal(settled.errorReason, "INSUFFICIENT_BALANCE"); + assert.equal(driver.balanceOf(target.address), 0n); +}); + +test("the requirements the core hands a driver carry no rail assumptions", async () => { + const { requirements } = await harness(); + + assert.deepEqual(Object.keys(requirements).sort(), [ + "amount", + "asset", + "extra", + "maxTimeoutSeconds", + "network", + "payTo", + "scheme", + ]); + assert.equal(typeof requirements.amount, "string"); +}); diff --git a/test/resolvers/inline-address.test.ts b/test/resolvers/inline-address.test.ts new file mode 100644 index 0000000..6acd1f4 --- /dev/null +++ b/test/resolvers/inline-address.test.ts @@ -0,0 +1,40 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { InlineAddressResolver, ResolverChain } from "../../src/resolvers/index.js"; + +const chain = () => new ResolverChain([new InlineAddressResolver()]); + +test("a bare identity resolves to a payout target on the requested rail", async () => { + const target = await chain().resolve("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", { + rail: "eip155:84532", + }); + + assert.equal(target.address, "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"); + assert.equal(target.rail, "eip155:84532"); + assert.equal(target.resolvedBy, "inline-address"); +}); + +test("the inline: prefix is stripped", async () => { + const target = await chain().resolve("inline:abc123", { rail: "mock:ledger" }); + assert.equal(target.address, "abc123"); +}); + +test("I4: the resolver does not judge address format — that belongs to the rail", async () => { + const target = await chain().resolve("GA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJVSGZ", { + rail: "stellar:pubnet", + }); + assert.equal(target.address.startsWith("GA"), true); +}); + +test("an unmatched identity fails with IDENTITY_UNRESOLVED", async () => { + await assert.rejects(chain().resolve("@alice", { rail: "eip155:84532" }), { + code: "IDENTITY_UNRESOLVED", + }); +}); + +test("an empty chain resolves nothing", async () => { + await assert.rejects(new ResolverChain().resolve("anything", { rail: "mock:ledger" }), { + code: "IDENTITY_UNRESOLVED", + }); +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..d962007 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2022"], + "module": "NodeNext", + "moduleResolution": "NodeNext", + "verbatimModuleSyntax": true, + "strict": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + "noFallthroughCasesInSwitch": true, + "isolatedModules": true, + "skipLibCheck": true, + "declaration": false, + "sourceMap": false, + "outDir": "build", + "rootDir": ".", + "types": ["node"] + }, + "include": ["src", "test"] +}