diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 3b13de45e..ed8397e51 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -482,6 +482,9 @@ jobs: - name: Install dependencies run: cd docs && npm ci + - name: Test docs + run: cd docs && npm test + - name: Build docs run: cd docs && npm run build @@ -536,6 +539,21 @@ jobs: npm publish --access public fi + - name: Publish octobot-client if version not already on npm + working-directory: packages/client/octobot_client_ts + run: | + npm ci + npm run build + npm test + NAME=$(node -p "require('./package.json').name") + VERSION=$(node -p "require('./package.json').version") + if npm view "$NAME@$VERSION" version >/dev/null 2>&1; then + echo "$NAME@$VERSION is already published — nothing to do." + else + echo "Publishing $NAME@$VERSION..." + npm publish --access public + fi + version: needs: [ build, tests ] if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') diff --git a/.gitignore b/.gitignore index 5cb35f47c..751ae6cc7 100644 --- a/.gitignore +++ b/.gitignore @@ -73,6 +73,7 @@ docs/_build/ docs/node_modules/ docs/build/ docs/.docusaurus/ +docs/tsconfig.tsbuildinfo # Auto-generated tentacle docs (from collect-tentacles.mjs) docs/content/creators/ docs/content/guides/exchanges.md diff --git a/docs/content/client-sdk/_category_.json b/docs/content/client-sdk/_category_.json new file mode 100644 index 000000000..97eb4367e --- /dev/null +++ b/docs/content/client-sdk/_category_.json @@ -0,0 +1 @@ +{"label": "Client SDK"} diff --git a/docs/content/client-sdk/accounts.md b/docs/content/client-sdk/accounts.md new file mode 100644 index 000000000..1a2b19085 --- /dev/null +++ b/docs/content/client-sdk/accounts.md @@ -0,0 +1,70 @@ +--- +title: "Accounts" +description: "The protocol account graph (Account, AccountAuthentication, ExchangeConfig), create/update emit ordering, account kinds, and holdings." +sidebar_position: 5 +--- + +# Accounts + +Every write below returns an `ActionHandle`, not an immediate result — see [User actions](user-actions.md) +if you haven't read it yet. + +## The protocol-0.4.0 account graph + +An account is not one object on the wire — it's three, linked by derived ids: + +``` +Account { authentication_id → AccountAuthentication.id + specifics.exchange_config_ids[0] → ExchangeConfig.id } +``` + +- `AccountAuthentication` carries credentials (`api_key`/`api_secret` for an exchange, `public_key` + for a wallet address). Id: `auth_{accountId}`. +- `ExchangeConfig` carries the venue (`exchange`, `sandboxed`). Id: `cfg_{accountId}`. Exchange + accounts only. +- `Account` itself carries display fields and asset quantities, and references the other two. + +The ids are **derived** (`auth_` / `cfg_` + the account id), not stored separately, so a client can +always reconstruct them from the account id alone — see `protocol/actions.ts::accountAuthIdFor` / +`exchangeConfigIdFor` and their inverses. + +**The node does not cascade deletes.** `client.accounts.delete(id)` emits `account_delete` plus +the two companion deletes itself — a raw `account_delete` alone leaves orphaned auth/config items. + +## `client.accounts.create()` + +```ts +const action = await octobot.accounts.create({ + name: 'Binance', type: 'exchange', exchange: 'binance', + credentials: { apiKey, apiSecret }, +}) +const account = await action.settled() +``` + +Emits, in order: `account_auth_create`, `exchange_config_create`, `account_create`. Three actions, +one `ActionHandle` — `settled()` resolves once the node confirms all three (in practice, the last one +appended, `account_create`, since it references the other two and the node applies them in order). + +## `client.accounts.update()` + +Emits credential/exchange-config edits **before** the account edit: `account_auth_edit`, +`exchange_config_edit` (exchange accounts only), then `account_edit`. This ordering matters — the +node's account re-validation on `account_edit` reads whatever credentials/exchange config are +current at that point, so rotating them first means the account edit is validated against the *new* +keys, not the ones about to be replaced. `update()` also preserves the account's original +`created_at` by pulling the existing record before building the edit — it does not re-stamp it to +the current time. + +## Kinds + +| `AccountInput.type` | Node `specifics.account_type` | Notes | +|---|---|---| +| `'exchange'` | `'exchange'` | Real credentials, a real venue. | +| `'wallet'` | `'generic'` | The node's `'blockchain'` account type isn't supported yet — wallets ride as generic with the address in `AccountAuthentication.public_key`. | +| `'generic'` | `'generic'` | No credentials — a manually-tracked balance. | + +## Holdings + +`AccountView.holdings` carries quantities only (`symbol`/`total`/`free`/`used`) — no fiat valuation. +The node's `DetailedAsset` schema has no per-asset value field; pricing holdings against a quote +currency is presentation logic that belongs one layer up, not in this package. diff --git a/docs/content/client-sdk/advanced-primitives.md b/docs/content/client-sdk/advanced-primitives.md new file mode 100644 index 000000000..7b7897188 --- /dev/null +++ b/docs/content/client-sdk/advanced-primitives.md @@ -0,0 +1,69 @@ +--- +title: "Advanced: Building an Offline Layer on Top" +description: "The tier-1 subpath exports (identity, transport, crypto, collections, protocol, node-api), the layering rule, and the DI seam for the two-phase automation race, for callers building their own persistence/offline/CRDT layer." +sidebar_position: 13 +--- + +# Advanced: building an offline layer on top + +This page is for a caller building persistence, an offline queue, or CRDT merge across devices on +top of this package — `@drakkar.software/octobot-sdk` (the OctoBot Cloud app's own sync engine) is +the worked example. + +## Use the subpath exports, not `client/` + +``` +@drakkar.software/octobot-client → the facade (connectOctoBot, strategy, errors) +@drakkar.software/octobot-client/identity → WalletCapProvider, key derivation, mnemonic tools +@drakkar.software/octobot-client/transport → node REST client, sync client factory, node detection +@drakkar.software/octobot-client/crypto → the secret encryptor, wire constants +@drakkar.software/octobot-client/collections → the node collection registry, path helpers +@drakkar.software/octobot-client/protocol → pure builders/parsers, the strategy module, the DI'd + two-phase automation orchestration +@drakkar.software/octobot-client/node-api → the raw node REST fetchers +``` + +**The layering rule this package enforces on itself** (see `tests/layering.test.ts`): nothing under +these subpaths ever imports `client/`. Building your own offline layer on `client/` instead of these +would give you a second key-derivation cache, a second cap-provider lifecycle, and a second request +path to the same node running alongside whatever caching/retry logic you build — two systems talking +to one node with different retry semantics. Consume the primitives directly and own your own +caching/lifecycle, the way `octobot-sdk` does. + +## The DI seam for the two-phase automation race + +`protocol/orchestration/createAutomation.ts::runCreateAutomation(io, input, opts)` takes an +`ActionEmitter — { emit, poll }` you implement: + +```ts +const io: ActionEmitter = { + emit: (configuration) => myOutbox.append(configuration), // returns the action's id + poll: async () => { + await myOutbox.drain() + await myUserDataStore.pull() + return parseNodeUserActions(myUserDataStore.data) + }, +} +const { automationId } = await runCreateAutomation(io, input) +``` + +This is the SAME state machine `connectOctoBot()`'s facade uses (wired to a direct append + pull) — +one implementation of the strategy-then-automation race fix, reused instead of re-derived. + +## Reference stability matters for `useSyncExternalStore` + +`protocol/state.ts`'s parsers (`parseNodeAutomationStates`, `parseNodeAccounts`, etc) are memoized +per input document reference (`cachedByDoc`, an internal `WeakMap`). If you read through a +`useSyncExternalStore`-style selector, a fresh `.filter()`/`.map()` on every call trips React's +"getSnapshot should be cached" infinite-loop guard — these parsers return the SAME array reference +for the same document object, and a new reference only appears when your store actually replaces the +document. Preserve this property in anything you build on top: don't rebuild an array from these +parsers' output on every render. + +## Local-domain adapters are your job, not this package's + +Functions that merge node state into a caller's own local, CRDT-tombstoned domain objects (an +`Account` type with `deletedAt`/`editedAt`, UI-only display fields, kinds the protocol doesn't model) +are deliberately NOT in this package — see `protocol/state.ts`'s exports for the pure half and write +your own `accountFromNodeState(protocolAccount, priorLocalAccount)`-shaped adapter for the merge half. +`octobot-sdk`'s `src/adapters/` is the reference implementation. diff --git a/docs/content/client-sdk/automations.md b/docs/content/client-sdk/automations.md new file mode 100644 index 000000000..fe493b543 --- /dev/null +++ b/docs/content/client-sdk/automations.md @@ -0,0 +1,72 @@ +--- +title: "Automations" +description: "Create, read, stop, and update running bots (automations) with the OctoBot client SDK — progress reporting, status mapping, and strategy versioning on edit." +sidebar_position: 6 +--- + +# Automations + +An automation is a running bot: a strategy configuration bound to one or more accounts. See +[User actions](user-actions.md) for the two-phase create race this package sequences +around, and [Strategies](strategies.md) for building the `strategy` argument itself. + +## Create + +```ts +import { strategy } from '@drakkar.software/octobot-client' + +const dca = strategy.dca({ pairs: ['BTC/USDT'], buyOrderAmount: '25' }) +const action = await octobot.automations.create({ + name: 'My DCA', + strategy: dca, + accountIds: [account.id], +}) +const automation = await action.settled() +``` + +Progress reporting, since this is a two-phase operation under the hood: + +```ts +await octobot.automations.create(input, { + onProgress: (p) => console.log(p.phase, p.done ? 'done' : 'waiting'), + // p.phase: 'strategy' | 'automation' +}) +``` + +## Reading state + +```ts +const automations = await octobot.automations.list() +for (const a of automations) { + console.log(a.id, a.status, a.accountIds, a.error) +} +``` + +`AutomationView.status` is a coarse `'live' | 'draft' | 'stopped'` — collapsed from the node's own +`WorkflowStatus` enum (`scheduled`/`periodic`/`running` → `live`; `pending` → `draft`; everything +else, including `canceled`/`failed`/`completed`, → `stopped`, so a non-running workflow never renders +as live). + +`AutomationView.strategy` is recovered from the action history, not the node's own state — the +node's `AutomationState` carries no strategy reference at all. If the automation was created by a +different client (or a client that's since lost its action history), this can come back `null`. + +## Stop + +```ts +const action = await octobot.automations.stop(automation.id) +await action.settled() +``` + +## Update + +```ts +const edited = strategy.dca({ pairs: ['BTC/USDT'], buyOrderAmount: '50' }, { id: dca.id, version: strategy.bumpVersion(dca.version!) }) +const action = await octobot.automations.update(automation.id, { + name: automation.name, strategy: edited, accountIds: automation.accountIds, +}) +await action.settled() +``` + +The node treats strategies as replace-by-id — an edit is a `strategy_edit` action carrying the same +`id` with a bumped `version`, followed by `automation_edit`. `update()` emits both. diff --git a/docs/content/client-sdk/collections-and-encryption.md b/docs/content/client-sdk/collections-and-encryption.md new file mode 100644 index 000000000..b4780f05c --- /dev/null +++ b/docs/content/client-sdk/collections-and-encryption.md @@ -0,0 +1,49 @@ +--- +title: "Collections and Encryption" +description: "The node's collections and their paths, the AES-256-GCM document envelope, and the raw documents escape hatch for unmodeled collections." +sidebar_position: 10 +--- + +# Collections and encryption + +## The node's collections + +| Key | Path | Encryption | Pull/push | +|---|---|---|---| +| `userData` | `users/{identity}/data` | identity | pull; automations/user_actions are node-computed, other fields are whatever else you store there | +| `accounts` | `users/{identity}/accounts` | identity | pull-only from the node's side (`accounts`/`exchange_configs`) | +| `settings` | `users/{identity}/settings` | identity | pull + push, opaque | +| `strategies` | `users/{identity}/strategies` | identity | legacy/unused by the node directly — strategies live in the action history instead | +| `actions` | `users/{identity}/actions` | identity | push/append-only | +| `accountTrading` | `users/{identity}/accounts/{accountId}/trading` | identity | pull-only, one document per account | + +Full path→HKDF-info table: [Wire contract](wire-contract.md). + +## The envelope + +Every document body is `{ iv: base64, data: base64 }`: + +1. `deriveKey(encryptionSecret, salt, info)` → HKDF-SHA256 → a 256-bit AES key. `salt` is the fixed + `STARFISH_ENCRYPTION_SALT`; `info` is the per-collection string (`'octobot-sync-user-accounts'`, + etc) — this is what makes each collection's key independent even though they all derive from the + same wallet secret. +2. A random 12-byte IV, AES-256-GCM encrypt the JSON-serialized document. +3. Base64-encode both, wrap as `{ iv, data }`. + +Decryption is the inverse. `crypto/secretEncryptor.ts::createSecretEncryptor(secret, salt, info)` +returns an `Encryptor` with `.encrypt()`/`.decrypt()`, memoizing the derived key. + +## Using the escape hatch for a collection this package doesn't model + +```ts +const { data, hash } = await octobot.documents.pull('settings') +await octobot.documents.push('settings', { ...data, myField: 1 }, { baseHash: hash }) +``` + +For a collection outside the `NodeCollectionKey` union entirely, use `documents.raw`: + +```ts +const encryptor = await octobot.documents.raw.encryptorFor('settings') // or build your own info string +const path = octobot.documents.raw.pullPath('settings') +const result = await octobot.documents.raw.sync.pull(path) +``` diff --git a/docs/content/client-sdk/errors.md b/docs/content/client-sdk/errors.md new file mode 100644 index 000000000..593b75f9a --- /dev/null +++ b/docs/content/client-sdk/errors.md @@ -0,0 +1,80 @@ +--- +title: "Errors" +description: "The full OctoBotError taxonomy — every class, its .code, its extra fields, and when it throws — plus handling patterns: switching on .code across package boundaries and AbortError passthrough." +sidebar_position: 4 +mdx: + format: mdx +--- + +import DemoEmbed from '@site/src/components/demo/Embed'; + +# Errors + +Nine ways a call into this SDK can end badly — eight typed `OctoBotError` subclasses, plus one you +have to catch yourself. Every method throws one of the eight (or lets an `AbortError` `DOMException` +through unwrapped, per the platform convention). + + + +## The taxonomy + +| Class | `code` | Extra fields | When | +|---|---|---|---| +| `OctoBotConfigError` | `'config'` | — | Bad `ConnectOptions` — an unparseable `url`, or a `client.node.*` call made without `basicAuth`. | +| `OctoBotConnectionError` | `'unreachable'` \| `'timeout'` \| `'aborted'` | — | The node could not be reached at all — offline, wrong port, the connect-time budget expired, or the caller's own `AbortSignal` fired during connect. | +| `OctoBotAuthError` | `'unauthorized'` | `.address` `.userId` `.derivation` | The node answered but didn't authorize this wallet — the fields name exactly what was tried. | +| `OctoBotHttpError` | `'http'` | `.status` | A `client.node.*` REST call answered non-2xx. | +| `OctoBotConflictError` | `'conflict'` | `.serverHash` | A document push raced another writer — the `baseHash` you pushed against is no longer current. `.serverHash` carries the node's current hash so you can pull-and-retry without a round trip. | +| `OctoBotActionError` | `'action_failed'` | `.detail` `.phase` | The node executed a queued action and rejected it. Not retriable by resubmitting unchanged. | +| `OctoBotTimeoutError` | `'action_timeout'` | `.phase` | `ActionHandle.settled()` gave up waiting — the action may still complete. | +| `OctoBotScopeError` | `'forbidden_collection'` | `.collection` | A read-only session reached a collection its pairing grant doesn't cover — thrown client-side, before any network request. See [Read-only devices](read-only-pairing.md). | +| `AbortError` (`DOMException`) | — not an `OctoBotError` | — | A caller's own `AbortSignal` fired. Passed through unwrapped, matching how `fetch` itself behaves — `isOctoBotError()` on it is `false`. | + +## Switch on `.code`, not `instanceof`, across package boundaries + +```ts +import { isOctoBotError } from '@drakkar.software/octobot-client' + +try { + await octobot.accounts.create(input) +} catch (err) { + if (isOctoBotError(err)) { + switch (err.code) { + case 'unauthorized': + // re-derive with a different seedDerivation + break + case 'conflict': + // pull again, retry the write + break + default: + console.error(err.code, err.message) + } + } else { + throw err // an AbortError, or something outside this package entirely + } +} +``` + +`instanceof OctoBotError` works fine within a single install of this package. It can silently fail +across a duplicated package instance (a monorepo hoisting quirk, a bundler that doesn't dedupe) — +`.code` is a plain string and survives that. + +## `AbortError` is not wrapped + +Every method that accepts `{ signal }` lets an aborted call's `DOMException` (`name === 'AbortError'`) +through unwrapped, matching how `fetch` itself behaves. Check for it separately if you care: + +```ts +catch (err) { + if (err instanceof DOMException && err.name === 'AbortError') return // cancelled, not a failure + throw err +} +``` + +## `OctoBotActionError` vs `OctoBotTimeoutError` + +These only come from `ActionHandle.settled()` (or the underlying two-phase automation orchestration +— see [User actions](user-actions.md)). `OctoBotActionError` means the node executed the action and +rejected it — resubmitting the same configuration will fail the same way. `OctoBotTimeoutError` +means the node never confirmed within the timeout budget; the action may still be pending or +running, so a fresh `settled()`-driving call (not a resubmit) is the right retry. diff --git a/docs/content/client-sdk/escape-hatches.md b/docs/content/client-sdk/escape-hatches.md new file mode 100644 index 000000000..fded37323 --- /dev/null +++ b/docs/content/client-sdk/escape-hatches.md @@ -0,0 +1,63 @@ +--- +title: "Escape Hatches" +description: "The paved path's five ways out: custom fetch for proxies/mTLS/React Native, verify:false, raw documents for unmodeled collections, seedDerivation:'auto', and the version compatibility table." +sidebar_position: 12 +mdx: + format: mdx +--- + +import DemoEmbed from '@site/src/components/demo/Embed'; + +# Escape hatches + +Everything in the other pages is the paved path. These are the ways out of it — for proxies, for +collections this package hasn't wrapped yet, for skipping I/O you don't want, and for knowing which +node you can actually talk to. + + + +## Custom `fetch` + +`ConnectOptions.fetch` — for proxies, mTLS, or a React Native crypto/fetch polyfill. + +```ts +const octobot = await connectOctoBot({ url, seed, fetch: myProxyAwareFetch }) +``` + +## `verify: false` + +Skips the connect-time probe. `connectOctoBot()` then does zero I/O, and the first real call (e.g. +`accounts.list()`) surfaces any connectivity or auth problem lazily instead. + +```ts +const octobot = await connectOctoBot({ url, seed, verify: false }) +``` + +## Raw documents — `client.documents` + +Escape hatch for any collection this package's typed facades don't cover, typed loosely. See +[Collections and encryption](collections-and-encryption.md) for the full shape and an example. +`client.documents.raw` exposes the underlying `StarfishClient` and cap provider directly for anyone +building a lower-level integration. + +## `seedDerivation: 'auto'` + +Tries every scheme currently registered in the derivation-scheme registry — `bip44` is the only one +this package ships by default — and keeps whichever one the node authorizes. It's really only useful +once a consumer has registered a second scheme via `registerDerivationScheme` (from +`@drakkar.software/octobot-client/identity`, see [Identity](identity.md)) for a different wallet +type; with only `bip44` registered, `'auto'` and `'bip44'` behave identically except `'auto'` costs +an extra round-trip. + +```ts +const octobot = await connectOctoBot({ url, seed, seedDerivation: 'auto' }) +``` + +## Compatibility + +| Package | Required version | +|---|---| +| This package | `0.3.0` | +| `@drakkar.software/octobot-protocol` | `^0.6.0` | +| Minimum node | protocol `0.4.0` | +| Minimum sync server (website pairing only) | must define the `joinsessions` collection (`_pairing/session/{code}`) — the `pairingrequests`/`pairingsnapshots` collections this package used before `0.3.0` no longer exist in this package's own code. Only [Website pairing](website-pairing.md) needs this; everything else on this page works against any compatible node/sync server regardless of this collection. | diff --git a/docs/content/client-sdk/getting-started.md b/docs/content/client-sdk/getting-started.md new file mode 100644 index 000000000..0a569076f --- /dev/null +++ b/docs/content/client-sdk/getting-started.md @@ -0,0 +1,115 @@ +--- +title: "Getting Started" +description: "Install @drakkar.software/octobot-client, connect to a self-hosted OctoBot node with a wallet private key, and the five things worth knowing before you build anything." +sidebar_position: 1 +mdx: + format: mdx +--- + +import DemoEmbed from '@site/src/components/demo/Embed'; + +# Getting started + +A TypeScript client for a self-hosted OctoBot trading node: wallet identity, accounts, automations, +strategies, and an append-only action queue, over the Starfish sync transport. No cloud account, no +registration — anyone running their own node can be your `url`. + + + +## What this is not + +- **No local state.** Every read is a fresh pull from the node; nothing is cached. +- **No persistence.** There is no store, no database, no `AsyncStorage`. +- **No offline queue.** If the node is unreachable, a call fails — it does not queue for later. +- **No CRDT merge.** This package has no concept of "local edits reconciled with remote state." +- **No React.** No hooks, no components, no dependency on any UI framework. + +If you need any of the above (an offline-first mobile app syncing across devices, say), build that +layer on top of this package — see [Advanced primitives](advanced-primitives.md) for the subpath +exports meant for exactly that. + +## Install + +```bash +npm install @drakkar.software/octobot-client +``` + +**Runtime requirements:** WebCrypto (`crypto.subtle` — SHA-256/512, HMAC, PBKDF2, AES-GCM, HKDF), +`fetch`, `btoa`/`atob`, `AbortController`. Available natively in Node ≥18, all modern browsers, and +Deno/Bun. React Native needs a crypto polyfill (`react-native-quick-crypto` or similar) — this +package does not bundle one. + +**License: GPL-3.0** — worth knowing before week three, not after. + +## Find your node's address + +An OctoBot node listens on `http://:5001` by default (`5001` is the default REST/sync port). +On the same LAN as your node, this is usually the machine's local IP: `http://192.168.1.10:5001`. + +## Get a private key + +A raw `0x`-prefixed secp256k1 private key is the primary way to authenticate — pass it as-is, no +derivation needed. A BIP39 mnemonic also works (`connectOctoBot`'s `seed` option accepts either), +deterministically deriving a private key via real BIP44 — see [Identity](identity.md) for that path. +Either way, don't paste a real wallet's key/phrase into example code; generate a fresh throwaway one +while you're getting the connection working. + +## The five things to know before you build anything + +1. **A node is a server you point at.** `connectOctoBot({ url, seed })` — no registration, no + cloud account. Anyone running their own OctoBot node can be your `url`. +2. **The wallet IS the identity.** There's no separate login. The private key (or a BIP39 mnemonic, + which derives one) deterministically derives both the address the node authorizes and the + encryption key for everything synced to it. See [Identity](identity.md) — this is the single most + important thing to get right, and the easiest to get subtly wrong. +3. **Collections, not tables.** `accounts`, `settings`, `strategies`, `user-data` are documents at + fixed paths under `users/{identity}/...`, pulled and pushed as whole blobs, encrypted + per-collection. See [Collections and encryption](collections-and-encryption.md). +4. **User actions are a queue, not an RPC call.** Creating an account or starting an automation + doesn't happen synchronously — it appends one element to an append-only queue the node consumes + and executes. See [User actions](user-actions.md) — this is the thing most likely to make your + first integration behave unexpectedly if you skip it. +5. **`ActionHandle` work starts eagerly.** The moment `accounts.create()`/`automations.create()` + returns a handle, the underlying action is already appended (and, for automations, the two-phase + orchestration is already running). `settled()` just lets you observe the outcome — a caller who + never awaits it still leaves nothing half-done. + +## Connect, and make your first calls + +```ts +import { connectOctoBot, strategy } from '@drakkar.software/octobot-client' + +const octobot = await connectOctoBot({ + url: 'http://192.168.1.10:5001', + seed: process.env.OCTOBOT_PRIVATE_KEY!, // a raw 0x-prefixed private key +}) + +console.log(octobot.address) // the EIP-55 checksummed address the node authorized + +const [account] = await octobot.accounts.list() + +const dca = strategy.dca({ pairs: ['BTC/USDT'], buyOrderAmount: '25' }) +const action = await octobot.automations.create({ + name: 'My DCA', + strategy: dca, + accountIds: [account.id], +}) + +const automation = await action.settled() // polls the node until it confirms +console.log(automation.id, automation.status) // 'live' +``` + +By default, `connectOctoBot` probes the node and verifies the wallet is authorized before resolving +— if it isn't, you get an `OctoBotAuthError` immediately, with a clear message about which +derivation was tried. Pass `verify: false` to skip this and do the check lazily on the first real +call instead — see [Escape hatches](escape-hatches.md). + +## Next + +- [Identity](identity.md) — the wallet/derivation model, and the #1 way to get stuck. +- [User actions](user-actions.md) — what `ActionHandle` is actually doing, and why `create()` + resolving is not the same thing as "created." +- [Errors](errors.md) — every way a call can fail, and how to branch on it. +- [Read-only devices](read-only-pairing.md) — pair a less-trusted client with a scoped, node-enforced + credential, and let it propose writes for a privileged device to review. +- [Live demo](/demo) — every panel on this and the following pages, in one place. diff --git a/docs/content/client-sdk/identity.md b/docs/content/client-sdk/identity.md new file mode 100644 index 000000000..7db0518c1 --- /dev/null +++ b/docs/content/client-sdk/identity.md @@ -0,0 +1,124 @@ +--- +title: "Identity" +description: "How a private key (or a BIP39 mnemonic that derives one) deterministically derives your node identity, address, and encryption key in the OctoBot client SDK — and the chain, walked step by step." +sidebar_position: 2 +mdx: + format: mdx +--- + +import DemoEmbed from '@site/src/components/demo/Embed'; + +# Identity + +This is the page to read before anything mysteriously doesn't sync. + + + +## There is no login — the wallet IS the identity + +A raw secp256k1 private key (`0x`-prefixed, 64 hex characters) resolves through one deterministic +chain, entirely in your process, with zero network calls: + +``` +private key → secp256k1 pubkey → EIP-55 address + → EIP-191 personal_sign('octobot:sync-bootstrap') + → HKDF-expand → Ed25519 + X25519 root identity + → userId = hex(sha256(rootEdPub))[:32] ← 32 HEX CHARS = 16 bytes, not 32 bytes + → users/{userId}/accounts ← the literal path every read uses +``` + +You can also start from a BIP39 mnemonic instead of a raw key — `connectOctoBot`'s `seed` option +accepts either. A mnemonic derives a private key first, via standard `m/44'/60'/0'/0/0`, then joins +the exact same chain above — see "Deriving from a mnemonic instead" below. + +The node authorizes requests by the Starfish identity (the Ed25519/X25519 keypair), which is +entirely determined by which private key came out of the first arrow. Two different derivations of +the "same" key material produce two completely different identities, two different addresses, and +two completely disjoint sets of synced data — see "The failure mode is silent" below. + +**Concretely, in code:** + +```ts +import { deriveBip44PrivateKey, deriveEvmAddress, deriveRoot } from '@drakkar.software/octobot-client/identity' + +const privateKey = await deriveBip44PrivateKey(rawPrivateKeyHex) // already a key — passed through unchanged +const address = deriveEvmAddress(hexToBytes(privateKey)) // EIP-55 checksummed +const root = await deriveRoot(rawPrivateKeyHex, 'bip44') // signs the bootstrap challenge, HKDF-expands +console.log(root.userId) // hex(sha256(root.keys.edPub)).slice(0, 32) +``` + +`root.userId` is NOT the EVM address — it's the `{identity}` URL segment every collection path uses +(`users/{identity}/accounts`, etc). And the same EVM private key, hex-encoded, doubles as the +**encryption secret**: every document is AES-256-GCM encrypted with a key HKDF-derived from it. One +chain produces both "who the node thinks you are" and "what can decrypt your data" — there's no +second key to lose track of. + +## The failure mode is silent + +This only applies if you start from a mnemonic — a raw private key has no scheme to get wrong (see +"Deriving from a mnemonic instead" below). `connectOctoBot` doesn't crash if the wrong derivation is +picked — it authenticates against the node under a wallet the node has never seen, and every read +comes back **empty**, not with an error. The same mnemonic, derived two different ways, looks exactly +like this: + +``` +bip44 → userId a3f9c2e1b6d84f07c5e0912ab34fd678 (the node knows this wallet) +some-other-way → userId 7c0491de5a3fb2891066cd45e9021acf (the node has never seen this one) +``` + +Pick the second one against a real node and every call still succeeds — it just authenticates as a +wallet the node has never heard of. With `verify: true` (the default) this surfaces immediately as an +`OctoBotAuthError`, whose message names the address that was tried and suggests the fix. With +`verify: false` you won't find out until your first real call returns nothing: + +```ts +try { + await connectOctoBot({ url, seed }) +} catch (err) { + if (err instanceof OctoBotAuthError) { + console.error(`node did not authorize ${err.address} (${err.derivation})`) + // retry with { seedDerivation: 'auto' } if more than one scheme is registered + } +} +``` + +## Derivation schemes + +`seedDerivation` names a scheme registered in a small registry (`identity/derivationSchemes.ts`), +not a fixed enum. `'bip44'` (standard `m/44'/60'/0'/0/0`) is the only one this package ships, and +the default — it's what an OctoBot node's own pairing QR, MetaMask, or any standard wallet uses. + +| `seedDerivation` | Path | Use it when | +|---|---|---| +| `'bip44'` (default) | Standard `m/44'/60'/0'/0/0` | Always, unless you've registered another scheme. | +| `'auto'` | Tries every registered scheme in turn | You don't know which one applies — only useful once more than one scheme is registered. Costs one extra round-trip per scheme tried; requires `verify: true` (the default). | + +A caller integrating a different wallet type (another chain, a hardware-wallet-derived key, …) +registers its own scheme via `registerDerivationScheme({ id, derive })` and passes that `id` as +`seedDerivation`. An unregistered id throws `unknown derivation scheme` immediately, rather than +silently deriving under the wrong scheme. + +## Deriving from a mnemonic instead + +If `seed` is already a `0x`-prefixed 64-hex private key, every derivation scheme is a no-op — the +key passes through unchanged, and `seedDerivation` is irrelevant. If instead you pass a BIP39 +mnemonic, the scheme picked by `seedDerivation` is what turns it into that private key — +`deriveBip44PrivateKey`/standard `m/44'/60'/0'/0/0` by default. Both inputs join the exact same chain +from that point on. + +## What a device cap-cert actually is + +Every sync request is signed with a **short-lived device capability**, minted fresh per request from +the root Ed25519 key (`scopes.rootAll()` — full access under this identity). This mirrors the node's +own `WalletCapProvider` (`packages/sync/octobot_sync/auth/provider.py`) exactly: the client never +sends a long-lived bearer token, and a captured cap-cert is worthless after it expires. + +## Pairing-QR shapes + +A node's "Pair mobile device" QR encodes `{ url, address, password }` or, on an older node, +`{ url, address, passphrase }`. The field name is what distinguishes them: + +- `password` — the wallet itself (a phrase or key). No network round-trip needed to import it. +- `passphrase` — an HTTP Basic password for the node's REST API; the wallet still has to be fetched + separately via `client.node.exportWallet()` (which needs that same Basic auth) — see + [Node REST API](node-rest-api.md). diff --git a/docs/content/client-sdk/node-rest-api.md b/docs/content/client-sdk/node-rest-api.md new file mode 100644 index 000000000..b351fb925 --- /dev/null +++ b/docs/content/client-sdk/node-rest-api.md @@ -0,0 +1,44 @@ +--- +title: "Node REST API" +description: "client.node.* — the OctoBot node's direct REST API for market-data lookups and Basic-auth-gated setup endpoints (DSL keywords, wallet export, generic-process bots)." +sidebar_position: 11 +--- + +# Node REST API + +Alongside the Starfish sync transport, a node exposes a direct REST API at `{origin}/api/v1`, used +for market-data lookups and a handful of authenticated setup endpoints. `client.node.*` wraps it. + +## Unauthenticated + +```ts +await octobot.node.status() +// { reachable: boolean, configured: boolean } + +await octobot.node.tradedPairs({ id: 'x', name: 'x', exchange: 'binance' }, { withVolume: true }) +await octobot.node.predictedOrderBook(exchangeConfig, marketMakingConfig) +await octobot.node.requiredFunds(exchangeConfig, marketMakingConfig) +``` + +## Authenticated — requires `ConnectOptions.basicAuth` + +Only a node paired by an **older** pairing QR hands you an HTTP Basic password (a current QR carries +the wallet directly and never routes through Basic auth). Without `basicAuth`, these throw +`OctoBotConfigError` immediately rather than making a request that would 401: + +```ts +const octobot = await connectOctoBot({ + url, seed, + basicAuth: { address: '0x...', password: '...' }, +}) + +await octobot.node.dslKeywords() +await octobot.node.exportWallet() +await octobot.node.createGenericProcessBot('My Bot') +``` + +## Errors + +A non-2xx response throws `OctoBotHttpError` with `.status` set — branch on it rather than parsing +the message (e.g. `tradedPairs` internally retries once without `withVolume` on a 501, since that +means the exchange doesn't support the volume lookup, not that the call itself failed). diff --git a/docs/content/client-sdk/read-only-pairing.md b/docs/content/client-sdk/read-only-pairing.md new file mode 100644 index 000000000..ea8ff3b31 --- /dev/null +++ b/docs/content/client-sdk/read-only-pairing.md @@ -0,0 +1,116 @@ +--- +title: "Read-Only Devices" +description: "Pair a less-trusted client as a read-only companion: a real, node-enforced scoped credential, plus offline action proposals for a privileged device to review and execute." +sidebar_position: 8 +mdx: + format: mdx +--- + +import DemoEmbed from '@site/src/components/demo/Embed'; + +# Read-only devices + +A less-trusted client — a CLI, an AI agent, a second phone, anything embedding this package without +holding the real wallet seed — can act as a **read-only companion** to a node. It can read accounts +and automations, but any write attempt builds the action and hands you back a **proposal** instead of +sending it: a QR-encodable payload for a privileged device (one that actually holds the seed) to scan, +review, and execute. + +This page covers the case where the **privileged device initiates** — it mints a credential and shows +a QR for the other device to scan. If your less-trusted client is a **website**, it can't scan a QR the +phone displays; see [Website pairing](website-pairing.md) for the reverse flow, which uses a short +device code instead of a QR and delivers a sealed data snapshot rather than a node credential. + + + +## The loop + +1. The privileged device (the one with the seed) calls `createReadOnlyPairing()` and shows the + resulting payload as a QR code. +2. The other client scans it and calls `connectReadOnlyDevice()` — no seed anywhere on this path. +3. That client reads normally (`accounts.list()`, `automations.list()`, ...) and, on any write call, + gets a `ProposedAction` back instead of an `ActionHandle` — it shows that as a QR code too. +4. The privileged device scans the proposal, reviews it, and executes it with its own real client. + +## Minting a pairing + +```ts +import { createReadOnlyPairing } from '@drakkar.software/octobot-client/identity' + +const { payload } = await createReadOnlyPairing(seed, 'bip44', { host: '192.168.1.10', port: 5001 }) +// render `payload` as a QR code with whatever QR library you already have — +// this package never renders one itself, it only returns the string to encode. +``` + +The payload is fully self-contained (it carries the node's endpoint), so the scanning side needs +nothing else to connect. + +**Default scope**: `ops: ['read', 'list']` — never `'write'` — restricted to the `userData` and +`accounts` collections. That's enough to reconstruct `accounts.list()`, `automations.list()`, *and* +`strategies.list()` (the last one is implemented via a `userData` pull, not the legacy `strategies` +collection), with no access to `settings` or `accountTrading`. Override with the `collections` option +if you need a different subset — `ops` is always exactly `['read', 'list']`, this function has no +option to widen it. + +**The cap's `ops` restriction is not yet node-enforced — this is a client-side guarantee today, stated +plainly rather than overclaimed.** The pairing mints an ephemeral Ed25519+X25519 keypair (the scanning +device's own, generated fresh — never the wallet's root key) and a cap-cert the wallet's root key signs +for it, restricted to `ops: ['read', 'list']`. But an OctoBot node currently authorizes every collection +by identity alone (`readRoles=["self"], writeRoles=["self"]`), not by the cap's `ops`/`collections` +scope — so a device holding this payload's *cap* is not, today, physically prevented by the node from +writing. What this package guarantees instead: `connectReadOnlyDevice()`'s `accounts`/`automations`/ +`strategies` write methods never call the node's append endpoint on this session's behalf — they always +build a `ProposedAction` and return it. See `OctoBotScopeError` for the related collection-level gate +this package does enforce (below). + +**What actually decides what a read-only device can decrypt**: each granted collection gets its own +derived AES-256 key (`collectionKeys`, one entry per collection in `scope.collections`), computed as +`HKDF-SHA256(the wallet's derived encryption secret, salt, collection-specific info)`. That derivation +is one-way and collection-independent — holding the `userData` key reveals nothing about the `accounts` +key, and neither reveals the wallet's secret, let alone its private key. `connectReadOnlyDevice()` +throws `OctoBotScopeError` for any collection outside the grant, client-side, before any network +request — so even though the node doesn't yet enforce scope, this package never even tries to reach for +key material it wasn't given. A device holding this payload can decrypt exactly the granted collections +and can never mint a broader grant (it never touches the root private key). Don't pair a device you +don't trust to read your data, and treat the collections you grant as the real security boundary today +— not the cap's `ops` field. + +## Connecting read-only + +```ts +import { connectReadOnlyDevice } from '@drakkar.software/octobot-client' + +const octobot = await connectReadOnlyDevice(pairingPayload) +const accounts = await octobot.accounts.list() // works, real pull +const proposed = await octobot.automations.stop(automationId) // builds, does not send +console.log(proposed.payload) // render this as a QR +``` + +`ReadOnlyOctoBotClient` has the same method names as the full `OctoBotClient` — `accounts.create/ +update/delete/refresh`, `automations.create/update/stop`, `strategies.create/update/delete` — so +nothing renames when a caller migrates between the two. The difference is only in what each write +method returns: a `ProposedAction` (`{ actions, payload }`) instead of an `ActionHandle`. + +`automations.create()`'s proposal carries **two** actions, `strategy_create` then `automation_create`, +the second tagged `after: 'previous-confirmed'` — the same node-side race `connectOctoBot()`'s facade +sequences around (see [Automations](automations.md)) applies here too. This read-only session has no +append rights to sequence it itself; the executing side must honor that ordering when it processes the +proposal. + +## Executing a proposal + +```ts +import { decodeActionProposal } from '@drakkar.software/octobot-client/protocol' + +const proposal = decodeActionProposal(scannedPayload) +// proposal.label — a human-readable summary for a confirm screen +// proposal.actions — [{ configuration, after? }], in append order +``` + +This package's facade doesn't ship an "execute a proposal" method — appending is exactly what +`connectOctoBot()`'s own `accounts`/`automations`/`strategies` methods already do. Walk +`proposal.actions` in order using your own `OctoBotClient`'s underlying append mechanism (or the +`protocol/actions.js` builders directly, via the lower-level primitives — see +[Advanced primitives](advanced-primitives.md)), honoring `after: 'previous-confirmed'` by polling the +prior action to completion (the same pattern `runCreateAutomation` uses internally) before appending +the next one. diff --git a/docs/content/client-sdk/strategies.md b/docs/content/client-sdk/strategies.md new file mode 100644 index 000000000..5897504c7 --- /dev/null +++ b/docs/content/client-sdk/strategies.md @@ -0,0 +1,63 @@ +--- +title: "Strategies" +description: "The protocol/strategy/ module map, building strategies with the strategy.* facade, reading them back from action history, and editing via patch/toInput." +sidebar_position: 7 +--- + +# Strategies + +## The module map + +`protocol/strategy/` is deliberately split into four files with four different jobs — this replaced +an earlier design (`strategyConfig.ts` + `strategyPatch.ts` + `strategyDoc.ts`) that mixed all four +in two files and ended up with `StrategyKind` declared three separate times across the codebase. + +| File | Job | +|---|---| +| `kinds.ts` | The ONE `StrategyKind` definition. Everything else imports it. | +| `builders.ts` | Per-kind pure builders: `buildDCAConfig`, `buildGridConfig`, `buildMarketMakingConfig`, `buildIndexConfig`, `buildCopyConfig`, `buildSignalConfig`, `buildGenericProcessConfig`. Each takes a friendly input shape and returns a protocol `configuration`. | +| `build.ts` | The `StrategyInput` discriminated union and `buildStrategy()` — wraps a builder's output into a full `Strategy` (id, version, timestamps, `reference_market`). | +| `patch.ts` | The inverse: `protocolStrategyToInput()` recovers an editable input from a `Strategy` the node returned. Kept separate from `build.ts` on purpose — construction and incremental editing are different concerns with different failure modes (patch must tolerate configs written by older or foreign clients; build never has to). | + +The public facade (`strategy.dca()`, `.grid()`, etc in the root export) is a thin wrapper over +`builders.ts` + `build.ts`; `strategy.toInput()` wraps `patch.ts`. + +## Building + +```ts +import { strategy } from '@drakkar.software/octobot-client' + +strategy.dca({ pairs: ['BTC/USDT'], buyOrderAmount: '25' }) +strategy.grid({ pairs: ['BTC/USDT'], lower: 60000, upper: 70000, levels: 20, currentPrice: 65000 }) +strategy.marketMaking({ exchange: 'binance', pairs: ['BTC/USDT'], refsByPair: {}, spreadBp: 50, perSide: 5, sizeBase: 0.1, shape: 'flat' }) +strategy.index({ pairs: ['BTC', 'ETH'], basketWeights: { BTC: 70, ETH: 30 } }) +strategy.copy({ sourceId: 'strategy-id-to-mirror' }) +strategy.signal({ webhookId, webhookSecret, pair: 'BTC/USDT', sideMode: 'buy', orderType: 'market', sizeMode: 'percent', sizeValue: 10 }) +strategy.genericProcess() +``` + +Every builder returns a complete protocol `Strategy` (`id`, `version: '1.0.0'`, `reference_market` +derived from the traded pairs' quote currency, `created_at`/`updated_at`). Override any of that with +the second argument: `strategy.dca(input, { id, version, name, description, referenceMarket })`. + +## Reading + +```ts +const strategies = await octobot.strategies.list() +const one = await octobot.strategies.get(id, version) +``` + +Reconstructed from `strategy_create`/`strategy_edit` user actions in the action history — the node +exposes no strategies collection of its own. For a given `(id, version)`, the newest action wins. + +## Editing + +```ts +const input = strategy.toInput(existing) // -> StrategyInputPatch +// ...mutate the relevant fields of `input`... +const edited = strategy.build(input, { id: existing.id, version: strategy.bumpVersion(existing.version!) }) +await (await octobot.strategies.update(edited)).settled() +``` + +`toInput()` never throws on a config from an older or foreign client — an unrecognized shape falls +back to `{ kind: 'custom' }` rather than crashing an editor. diff --git a/docs/content/client-sdk/user-actions.md b/docs/content/client-sdk/user-actions.md new file mode 100644 index 000000000..361622898 --- /dev/null +++ b/docs/content/client-sdk/user-actions.md @@ -0,0 +1,88 @@ +--- +title: "User Actions" +description: "Writes are a queue, not an RPC call: the append-only user-actions collection, ActionHandle semantics, action statuses, and the two-phase automation-create race the SDK sequences around." +sidebar_position: 3 +mdx: + format: mdx +--- + +import DemoEmbed from '@site/src/components/demo/Embed'; + +# User actions + + + +## Writes are a queue, not an RPC call + +`accounts.create()` and `automations.create()` don't make an RPC call that finishes when the promise +resolves. They append an action to `users/{identity}/actions` — a push-only, append-only collection — +and hand back an `ActionHandle` **the instant that append happens**: + +```ts +const action = await client.automations.create({ name, strategy, accountIds }) +// `action` already has work running — appending happened before create() returned. +const automation = await action.settled() +``` + +A caller who reads the resolved `create()` promise as "it's created now" is already wrong. +`settled()` only lets you watch what the node does with the append afterward — a caller who never +calls `settled()` at all hasn't left anything half-done; the append still happened without them +watching. + +Every appended element is a **command the node consumes and executes exactly once**. There is no +"PUT the current state" here — appending the same configuration twice creates the resource twice (or +fails the second time, depending on the action). Results never come back through the `actions` +collection itself — pulling it always returns empty. The node reports execution status back through +the `user-data` pull, correlated by the action's `id`. + +## Statuses + +| `UserAction.status` | Meaning | +|---|---| +| `pending` | Appended, not yet picked up by the node. | +| `running` | The node is executing it. | +| `completed` | Done. `settled()` resolves. | +| `failed` | The node rejected it. `settled()` rejects with `OctoBotActionError`, carrying `.detail`. | + +A `failed` action is **not retriable by resubmitting the same configuration** — whatever the node +objected to (a validation error, an already-existing id, a missing dependency) is still true. Fix the +input and emit a new action. + +## `ActionHandle` + +- **`settled()` is memoized.** Await it from two places; the underlying poll only runs once. +- **`ids` grows as phases start.** A single-action call (`automations.stop`) has one id from the + start; a multi-phase call (`automations.create`) adds its second id once the first phase confirms — + reading `action.ids` right after the call resolves can legitimately show fewer ids than the action + will eventually have. +- **`status()` is a one-shot peek**, independent of the ongoing `settled()` work — useful for a + progress UI that polls on its own cadence without driving the actual wait. + +## The two-phase automation race + +Creating an automation from a fresh strategy is two actions, not one: `strategy_create` then +`automation_create`. This is sequenced deliberately. The node executes queued actions **concurrently**, +and `automation_create` resolves its strategy by `(id, version)` against the node's own +StrategyProvider — which is populated *only* by strategy actions. If `automation_create` ran before +`strategy_create` registered the strategy, it would fail non-retriably with `strategy_not_found`. + +`client.automations.create()` handles this for you: it appends `strategy_create`, polls until the +node confirms it, *then* appends `automation_create`. Watch it happen with `onProgress`: + +```ts +await octobot.automations.create(input, { + onProgress: (p) => console.log(p.phase, p.done ? 'done' : 'waiting'), + // p.phase: 'strategy' | 'automation' +}) +``` + +See `protocol/orchestration/createAutomation.ts` if you're building your own orchestration on the +lower-level `protocol`/`transport` exports instead of the facade — see +[Advanced primitives](advanced-primitives.md). + +## Account deletes don't cascade + +Deleting an account is three actions, not one — `account_delete` plus the two companion items the +protocol account graph splits out (`account_auth_delete`, `exchange_config_delete`). The node does +not cascade; `client.accounts.delete()` emits whichever of the three actually apply. See +[Accounts](accounts.md) for the full account graph. diff --git a/docs/content/client-sdk/website-pairing.md b/docs/content/client-sdk/website-pairing.md new file mode 100644 index 000000000..6ec5d261e --- /dev/null +++ b/docs/content/client-sdk/website-pairing.md @@ -0,0 +1,302 @@ +--- +title: "Website Pairing" +description: "Let a third-party website pair read-only with a user's OctoBot without scanning a site-rendered QR: a short device code, approved on the phone, mints a live read-only grant against the user's cloud mirror." +sidebar_position: 9 +mdx: + format: mdx +--- + +import DemoEmbed from '@site/src/components/demo/Embed'; + +# Website pairing + +[Read-only devices](read-only-pairing.md) covers the case where the privileged device (the one with +the seed) initiates: it shows a QR, a less-trusted device scans it. This page covers the reverse — a +**website** is the less-trusted party, and it cannot scan a QR the phone displays. The website has to +initiate instead, and the phone approves. + + + +**Why this can't just be the same QR flow with the roles swapped.** If the website rendered a QR and +the phone scanned it, there would be no channel binding that scan to the browser session that rendered +the QR. An attacker can load the real site's pairing page themselves, get a real signed request, and +re-render that exact QR anywhere — their own page, an email, a poster. Every check on the payload +itself (a proof-of-possession signature, an origin string) passes cleanly, because it genuinely is the +real site's real request, just relayed. This is the same class of attack that has hit WhatsApp Web's QR +login in the wild. + +**The fix is a device code, not different crypto.** The website displays a short, human-typeable code +instead of a QR. The user reads it off the site they are actively looking at and types it into their +own OctoBot app. This removes the passive, at-scale version of the relay attack — a QR image posted +anywhere, scanned later, by anyone — because the code only has value for the few minutes it's valid, +read directly off a live page. It does not make a live, real-time relay attempt impossible in the +abstract, which is why the request/code carries a short expiry (`ttlSec`, default 5 minutes) rather +than a long one. **`expiresAt`/`createdAt` are not covered by `popSig`** — anyone with the code can +rewrite them — so "short" is enforced independent of what a request claims, and independent of +`createdAt` too: `createPairingRequest()` clamps `ttlSec` to a 1-hour maximum, and +`parsePairingRequest()` separately rejects any request whose `expiresAt` is more than that same +maximum away from the **real wall clock at verification time** — not from the request's own claimed +`createdAt`, which a party rewriting the record could otherwise co-forge alongside `expiresAt` to +keep the *declared* window narrow while placing both timestamps arbitrarily far in the future (making +an old, indefinitely-reusable code look freshly issued no matter when it's actually redeemed). + +**A live per-node grant, not a data snapshot.** The website never receives a node credential, and it +never receives a one-time export either. Approving a request mints one read-only invite per shared +collection into the user's **cloud mirror** — a dedicated Starfish space the wallet (or its node, when +one is configured) keeps continuously synced with a read-only projection of the user's own data. Each +invite reaches exactly one node, through that node's own keyring. Those invites are what the website +reads through: every poll is a live pull against the mirror, decrypted client-side, never a value +handed over once and then stale. Populating the mirror itself (which collections sync, how often, from which side — +wallet or node) is a separate concern from pairing a website to read it; this page only covers the +latter. `syncCloudMirror()` (exported from this package) and `MIRROR_COLLECTIONS` are the entry points +if you need to look at how the mirror gets written. + +## The loop + +1. The website calls `startPairingRequest()`, publishes the request, and displays the code. +2. The user opens their OctoBot app, enters the code. +3. The app calls `fetchPairingRequestByCode()` — which returns both the request and the pulled + document's `hash` — shows the user what site is asking, and — on approval — calls + `mintPairingGrant()` (inviting the website's device into the mirror space) and + `publishPairingGrant()` (sealing the invite bundle to the website's ephemeral key, published + with `baseHash` set to the request's `hash` from step 3's lookup — this is what makes "claim + this exact request" atomic; see [Transport](#transport)). +4. The website, having been polling with `awaitPairingGrant()`, unseals the grant and immediately does + a live read of every mirror collection it covers. + +```ts +// Website side +import { startPairingRequest, awaitPairingGrant } from '@drakkar.software/octobot-client' + +const rendezvous = { baseUrl: 'https://sync.drakkar.software/sync', namespace: 'dk' } +const session = await startPairingRequest({ + origin: 'https://myapp.example', + rendezvous, +}) +await session.publish() +showCodeToUser(session.code) // an 8-character code, e.g. "K7M3PQXR" + +// `session` already carries its own `rendezvous` (spread it, or pass session +// directly — both work). +const result = await awaitPairingGrant(session, { timeoutMs: 5 * 60_000 }) +console.log(result.collections['user-accounts'], result.collections['user-strategies']) + +// Poll again any time to see the latest write — this is a live read, not a +// one-time export. `fetchPairingGrant` does the same live pull `awaitPairingGrant` +// did, just without the wait loop; pass the sealer you recorded above to pin it. +import { fetchPairingGrant } from '@drakkar.software/octobot-client' +const refreshed = await fetchPairingGrant(session, { expectedSealer: result.sealedBy }) +``` + +`session` holds live key material (`session.device.kemPriv`, needed both to unseal the grant bundle +and to open the mirror space's keyring afterward) as a plain in-memory object — treat it with the same +care as any other secret if your app needs it to survive past the current request (e.g. an SSR round +trip). Don't persist it under a predictable key; tie it to an already-authenticated visitor identity if +it must be stored at all. + +```ts +// Phone side (what an app embedding octobot-client + octobot-sdk does) +const request = await sync.websitePairing.lookupRequest(codeTheUserTyped) +// show request.origin, request.label to the user for confirmation +const paired = await sync.websitePairing.approve(request) +console.log(paired.grantedCollections) // e.g. ['user-accounts', 'user-strategies'] +``` + +`approve()` throws `NothingToShareError` if the wallet has never mirrored anything yet — there is +nothing worth inviting the website to read. The SDK's own `approve()` self-heals this: it triggers one +mirror sync of the default collections and retries the mint, so from the app's point of view "approve" +just works the first time too, as long as the wallet is online. A bare `octobot-client` integration +that calls `mintPairingGrant()` directly does not get this retry for free — run +`syncCloudMirror()`/your own mirror writer at least once first. + +## What a grant covers, and what it deliberately does not + +A grant is a set of per-node invites, one per granted collection, each carrying two caps: one for that +node's content (`objinv`) and one for that node's own keyring. It covers every collection that is both +third-party eligible — `visibility` other than `"private"`, i.e. `isThirdPartyEligible(id)` (see +`MIRROR_COLLECTIONS`) — and **actually has a mirror node at mint time**: a collection the user has +enabled for cloud sync but that hasn't synced yet simply isn't there to invite into. It never covers +`user-accounts-auth` (exchange credentials), which the mirror never writes anywhere, at any layer. + +**The website is never added to the space roster.** That is what bounds the grant. A `space:member` +cap's scope covers `spaces/{spaceId}/**`, so the earlier design had to keep `user-settings` +(`visibility: "private"`) in a *separate space* or any grant would have decrypted it too. Now each +shared collection's node carries its own keyring, so a grant reaches exactly the nodes it names — and +because the holder is not on the roster, they also cannot read `objindex`, so they cannot even +enumerate what other collections exist. `user-settings` stays on the space keyring and is unreachable +by construction rather than by policy. + +**Revocation is per collection.** `revokePairingGrant` removes the website's KEM key from each named +node's keyring and rotates that keyring's epoch, so everything written afterwards is sealed to an epoch +the site is not a recipient of. Revoking one collection leaves the others working — something the old +space-wide grant could not express. Two honest limits: the site keeps a valid `objinv` cap, so it can +still *fetch* those nodes' bytes (it just cannot decrypt anything written after revocation); and no +revocation can erase what it already fetched and decrypted. + +**Mirrored data is the raw synced document, not a curated field allowlist.** Unlike the old +sealed-snapshot design this replaces, the mirror does not project through +`ACCOUNT_SNAPSHOT_FIELDS`/`AUTOMATION_SNAPSHOT_FIELDS`/`STRATEGY_SNAPSHOT_FIELDS`-style allowlists — +each collection ships the same document the writer's own local store holds. Decide what to mirror at +the `cloudSyncEnabled`/`cloudSyncCollections` layer (per collection, before anything is written), not +by assuming a website only ever sees a hand-picked subset of a collection's fields once that +collection is enabled. + +**This is a live feed, not a point-in-time export.** There is no `generatedAt` watermark to render as +"data as of …" — call `readMirrorCollections()` again whenever you want the latest state. Freshness is +bounded by whether the wallet (or its node) is online and has recently run its mirror sync, not by +anything the grant itself carries. + +## Origin verification is the caller's responsibility, and is not yet built into this package + +`origin` in a pairing request is an attacker-authorable string — anyone can put any value there. This +package does not verify it. An app embedding this on the phone side should not present `origin` as a +verified identity without doing that verification itself, and should say plainly and prominently in +its UI when it hasn't — not as a small aside easy to miss. + +**The `.well-known` convention.** A site can serve `/.well-known/octobot-pairing.json` over HTTPS +from the exact origin it declares in its pairing request: + +```json +{ + "octobotPairing": true, + "label": "My Trading Dashboard" +} +``` + +`label` should match the `label` the site passes to `startPairingRequest()`. This is a same-origin +reachability and label-consistency check, not a cryptographic proof of identity — it confirms the +declared origin is reachable and self-consistent, not that it's trustworthy. It rules out the +simplest form of spoofing (a site that declares an origin it doesn't actually control, and never +serves this file from it) without claiming to solve origin verification in general. + +**This file is not yet checked by any client in this package.** It's documented now so site operators +can start serving it ahead of the verifying code landing, and so an embedding app's future +verification step has a fixed target to implement against, rather than needing to invent (and every +integrator separately reinventing) its own convention. + +## `requesterKind`: a website isn't the only thing that can be on the other end + +The device-code flow was designed for a website, but nothing about it is actually website-specific — +the requester just needs to publish a request, show the code, and unseal whatever grant comes back. +`PairingRequestPayload.requesterKind: 'website' | 'device'` names which kind of thing is asking: + +- `'website'` — the original case. `origin` is a URL, and the trust question is "does this domain + really control the origin it claims" (see above). +- `'device'` — another OctoBot client (e.g. a second phone) pairing as a read-only viewer of this + wallet's cloud mirror. There is no domain to spoof here — the human relaying the code between two + devices they hold *is* the trust anchor, the same way typing a pairing code into a website is. + `origin` verification (and the `.well-known` convention above) simply doesn't apply to this case. + +`createPairingRequest`/`startPairingRequest` default `requesterKind` to `'website'` when not passed, +but the built payload always carries the field — `parsePairingRequest` rejects one that's missing it. +An approving UI should branch its copy on this field rather than assuming every request is a website; +mobile2's `website-pairing-approve.tsx` is the reference implementation (device requests skip the +origin-unverified warning and show the request's `label` instead of a URL). + +## Trust-on-first-use pinning across refreshes + +`fetchPairingGrant()` returns `sealedBy` — the Ed25519 pubkey that actually sealed the grant, verified +via the wrap entry's signature, never merely claimed. A website should record this after its first +successful read and pass it back as `expectedSealer` on every later poll for the same session. +`_pairing`-style rendezvous collections are public-write, so a second party who somehow learns the code +could otherwise overwrite an already-established pairing's grant slot with their own — the pin turns +that into a hard failure instead of a silent identity switch. + +**A replayed grant blob is far less dangerous here than a replayed snapshot was.** The old +sealed-snapshot design needed an explicit freshness watermark (`afterGeneratedAt`) because a replayed +old blob was, on its own, indistinguishable from a legitimate update — the snapshot carried no live +authority check, only its own signature. A grant is different: the cap it carries only works while the +website's ephemeral device is still a member of the mirror space. `unpairWebsite()`/`revokePairingGrant()` +removes that membership immediately and directly — so even a perfectly replayed, correctly-signed old +grant blob fails the moment a website actually tries to use it to read, because the read itself is a +live, node-enforced space-membership check, not just a check on the blob. There is currently no +`afterGeneratedAt`-equivalent on `fetchPairingGrant()`/`awaitPairingGrant()` — none is needed for this +reason, not because the check was overlooked. + +**The very first resolution has nothing to pin against yet.** `expectedSealer` protects every read +*after* the first one — the first successful `fetchPairingGrant()` call for a session trusts whatever +`sealedBy` it sees outright, because there is no prior pin to check it against. If two different +parties race to answer a session's very first request, whichever one's publish is read first wins, +silently. This package does not add extra latency (e.g. waiting an extra poll cycle to catch a second, +different `sealedBy` arriving right after the first) to close that window — a real design option, but +one that changes `awaitPairingGrant()`'s documented behavior and timing for a narrow race that's +already bounded by the code's short live window. + +**Every publish after the first is now tamper-evident, not tamper-proof.** `pushRendezvousDoc()` +writes against the caller's *own* remembered hash (`baseHash`), not "whatever the server currently +has" — so a third party who overwrites the `joinsessions` slot is no longer silently adopted as the +new legitimate baseline by the next legitimate write. That write now fails with a named "modified" +error instead. This is also exactly the mechanism `publishPairingGrant()` relies on to claim a request +atomically (see [Transport](#transport)): the phone's grant write uses the request's own pulled +`hash` as `baseHash`, so if the request was swapped between the phone's read and its grant write, the +write fails loudly instead of sealing a grant to the wrong device. The residual gap: a hostile write +landing between two legitimate writes is invisible *until* that next write is attempted — this +converts what used to be a silent, undetectable hijack forever into one race window, then a loud, +unmissable failure. **This is a compare-and-swap against a specific document version, not proof of +who wrote it** — anyone can still publish a *self-consistent* replacement request (freshly generated +keys, correctly self-signed, same `origin`/`label` text) to a slot before the legitimate phone reads +it; `baseHash` only protects against tampering *after* a caller's own read, not against a swap that +happens entirely before it. Origin verification (above) is what actually helps there, not this +mechanism. + +## Unpairing + +`revokePairingGrant()` (surfaced as `sync.websitePairing.unpair()` on the phone) removes the paired +website's ephemeral device from the mirror space's member roster, and `clearPairingGrant()` wipes the +`joinsessions` slot the request and grant share — since the two phases now live at one address, +clearing it clears both together; there's no way to keep one and drop the other. This is **real, +immediate revocation** — the next read the website attempts fails live at the node, because access is +a real space-membership check performed on every request, not a cached decision. The one honest +residual: this cannot erase what the website already fetched and decrypted before the revocation — a +decrypted value, once read into a third party's page, is an ordinary value that page can log or +persist, and there is no way to reach into a website's memory. Any UI built on this must say "revoked" +for what it actually is, but should not claim past reads are somehow undone. + +## Refreshing + +`sync.websitePairing.refresh(id)` re-mints against the current state of the mirror (useful after the +user enables another collection for cloud sync, so an already-paired site's grant picks up the new +coverage without a full unpair/re-approve). A bare `octobot-client` integration does the equivalent by +calling `mintPairingGrant()` again and re-publishing with `publishPairingGrant()`, passing the `hash` +its own *previous* `publishPairingGrant()` call returned as `baseHash` — not the request's original +hash, and not `null`. + +## Transport + +The rendezvous is a single collection, `joinsessions` (`_pairing/session/{code}`) — distinct from the +QR-pairing flow's `_pairing` collection, which is far too small (16 KB) and has no TTL. **One address +serves both phases of the exchange, keyed throughout by the same human-typeable `code`**: the +website's "request" doc, and the phone's "grant" doc that later overwrites it in place. + +The two phases are told apart on the wire by an unsealed top-level `kind` field — `'octobot-pairing-request'` +for the discovery phase, or `'octobot-pairing-grant'` for the delivery phase (which wraps the actual +sealed blob: `{v: 1, kind: 'octobot-pairing-grant', sealed: }`) — so a poller can tell +"still waiting" from "approved" without attempting to unseal anything. `fetchPairingRequestByCode()` +and `fetchPairingGrant()` both handle this internally; you never need to inspect `kind` yourself. + +**Claiming a request is a compare-and-swap, not a blind overwrite.** `fetchPairingRequestByCode()` +returns the pulled document's `hash` alongside the parsed request. `publishPairingGrant()`'s +`baseHash` on first publish must be exactly that hash — the write only succeeds if the slot still +holds precisely the request doc the phone read, so a request that was swapped or already claimed +between the read and the write is detected as a conflict (`OctoBotConflictError`) instead of silently +overwritten. There is no `baseHash: null` "fresh slot" case here the way the retired two-address +design had one for the grant — the slot is never empty by the time a caller reaches +`publishPairingGrant()`, it already holds the request. + +Merging the two phases into one address does not weaken confidentiality: the old +`pairingsnapshots` collection was already public-read regardless of its address entropy, and the old +`pairingrequests` doc carried the session id in plaintext, so guessing the code already yielded the +session address for free. The real confidentiality boundary was always that the grant is sealed to +the website's ephemeral KEM key, which never leaves the browser — an address split provided no +protection the seal didn't already provide, only lifecycle bookkeeping this merge no longer needs. + +The collection is public read/write, reached through a cap-less `StarfishClient` this package builds +internally; nothing here needs a seed or a cap for the request/grant exchange itself. Reading the +mirror once a grant is unsealed is a separate, cap-authenticated connection (see +`ReadMirrorCollectionsOptions`). + +Reaching the rendezvous from a browser page needs the page's origin allowlisted for CORS at the +infrastructure level — this is centrally managed (one allowlist for the shared sync server, not +per-node operator configuration the way direct node access would be), but it is still a real, +named dependency. A `*.drakkar.software` subdomain, `localhost`, or a bare IP-literal origin is +allowlisted by a wildcard already; anything else needs a one-time addition. diff --git a/docs/content/client-sdk/wire-contract.md b/docs/content/client-sdk/wire-contract.md new file mode 100644 index 000000000..44b51a958 --- /dev/null +++ b/docs/content/client-sdk/wire-contract.md @@ -0,0 +1,99 @@ +--- +title: "Wire Contract" +description: "Every literal string the client shares with the node's Python sync implementation — bootstrap challenge, HKDF salt, collection paths, and per-collection encryption info strings." +sidebar_position: 14 +--- + +# Wire contract + +Every literal below is shared with the node's Python implementation in this same repo +(`packages/sync/octobot_sync/`). **A mismatch on either side breaks sync silently** — no error, just +data that never syncs, or that syncs under the wrong identity. `tests/wireContract.test.ts` pins all +of these; if you're changing one on purpose, update both sides in the same change and check the test +still documents what actually shipped. + +| Constant | Value | TS location | Python source | +|---|---|---|---| +| Bootstrap challenge | `'octobot:sync-bootstrap'` | `identity/capProvider.ts::BOOTSTRAP_CHALLENGE` | `constants.py::SYNC_BOOTSTRAP_CHALLENGE` | +| HKDF salt | `'octobot-starfish-identity-v1'` | `crypto/wireConstants.ts::STARFISH_ENCRYPTION_SALT` | `constants.py::HKDF_SALT_STRING` | +| Sync mount path | `'sync'` | `crypto/wireConstants.ts::SYNC_MOUNT_PATH` | the app's sync sub-app mount | +| Sync namespace | `'octobot'` | `crypto/wireConstants.ts::SYNC_NAMESPACE` | the Starfish namespace this node registers under | +| Node REST prefix | `'/api/v1'` | `transport/constants.ts::API_PREFIX` | the node's FastAPI router prefix | +| Default node port | `5001` | `transport/constants.ts::DEFAULT_NODE_PORT` | the node's default listen port | +| Blob envelope keys | `{ iv, data }` | `crypto/secretEncryptor.ts` | `crypto.py::BLOB_IV_KEY` / `BLOB_DATA_KEY` | +| AES-GCM IV length | 12 bytes | `crypto/secretEncryptor.ts::IV_BYTES` | `crypto.py::IV_BYTES` | + +## Collection paths and per-collection HKDF `info` + +Each collection's `encryptionInfo` MUST equal `'octobot-sync-' + ` — +the node derives its per-collection key from this exact string. + +| Collection key | Storage path | `encryptionInfo` | Python `Collections` enum value | +|---|---|---|---| +| `userData` | `users/{identity}/data` | `octobot-sync-user-data` | `user-data` | +| `accounts` | `users/{identity}/accounts` | `octobot-sync-user-accounts` | `user-accounts` | +| `settings` | `users/{identity}/settings` | `octobot-sync-user-settings` | `user-settings` | +| `strategies` | `users/{identity}/strategies` | `octobot-sync-user-strategies` | `user-strategies` | +| `actions` | `users/{identity}/actions` | `octobot-sync-user-actions` | `user-actions` | +| `accountTrading` | `users/{identity}/accounts/{accountId}/trading` | `octobot-sync-user-accounts-trading` | `user-accounts-trading` | + +Source of truth for the Python side: `packages/sync/octobot_sync/enums.py::Collections`. + +## Pairing wire literals + +Separate from the node's own collections above — the device-code website-pairing flow (see +[Website pairing](website-pairing.md)) uses its own rendezvous path and payload markers: + +| Constant | Value | TS location | +|---|---|---| +| Join session path | `_pairing/session/{code}` | `transport/rendezvous.ts` | +| Pairing request payload kind | `'octobot-pairing-request'` | `identity/pairingRequest.ts` | +| Pairing request payload version | `1` | `identity/pairingRequest.ts` | +| Join session grant document kind | `'octobot-pairing-grant'` | `client/pairing/pairingGrantExchange.ts` | +| Join session grant document version | `1` | `client/pairing/pairingGrantExchange.ts` | + +The request payload also carries a required `requesterKind: 'website' | 'device'` field +(`identity/pairingRequest.ts`) — `'website'` for the original case (a third-party site running this +package in a browser), `'device'` for another OctoBot client (e.g. a second phone) pairing as a +read-only viewer of this wallet's cloud mirror. `createPairingRequest`/`startPairingRequest` default it +to `'website'` when not passed, but it is always present on the wire — there is no absent-means-website +fallback in `parsePairingRequest`, which rejects a payload missing it. The approving side branches its +copy on this field: a `'device'` request has no real "origin" to verify (see mobile2's +`website-pairing-approve.tsx`), trust instead comes from the human typing the code themselves. + +**One address serves both phases**, keyed throughout by the same human-typeable `code` — the website's +"request" document, later overwritten in place by the phone's "grant" document. The two are told apart +on the wire by the top-level `kind` field: `'octobot-pairing-request'` for the request phase (fields +unsealed — public keys and `origin`, nothing confidential), or `'octobot-pairing-grant'` for the grant +phase, which wraps the actual encrypted payload: `{v: 1, kind: 'octobot-pairing-grant', sealed: +}`. The `kind`/`v` on this OUTER wrapper are plaintext on the wire deliberately — a poller +needs to distinguish "still just a request" from "a grant has been published" without attempting +`unseal()` on a document that might not even be sealed yet. This replaces the retired two-collection, +two-address design (`pairingrequests` at `_pairing/requests/{code}`, `pairingsnapshots` at +`_pairing/snapshots/{sessionId}`) — merging was safe because the old high-entropy session address +bought no real confidentiality the grant's own sealing didn't already provide (see +[Website pairing](website-pairing.md)'s Transport section for the full argument). + +`tests/wireContract.test.ts`'s `'wire contract: device-code pairing (rendezvous)'` describe block pins +the path, the request kind/version, and the grant document's outer kind/version/`sealed` shape. Note +this is a DIFFERENT payload from the QR read-only pairing flow's own `'octobot-read-only-pairing'` kind +(pinned separately, in that same test file's `'wire contract: QR read-only pairing'` block) — the flows +are distinct mechanisms (see [Website pairing](website-pairing.md)'s intro for why) and do not share +wire literals. + +The path is public read/write, and claiming a request is a compare-and-swap: `publishPairingGrant()`'s +`baseHash` on first publish must be the exact `hash` `fetchPairingRequestByCode()` returned alongside +the request it read, so a request swapped or already claimed between the read and the write is +detected as a conflict rather than silently overwritten (`pushRendezvousDoc()`'s `baseHash` mechanism). +A party that captures the human code during its short live window can still publish a +self-consistent replacement request before the legitimate device reads it — that race is inherent to a +human-typed code and not closeable by this compare-and-swap alone (it only protects a caller's OWN +subsequent writes, not a swap that happens entirely before its first read); see +[Website pairing](website-pairing.md) for the full model, including why origin verification is the +actual defense for that case. + +## Why this page exists + +Every one of these strings is duplicated, by necessity, on both sides of the wire — TypeScript +cannot import Python constants. The single highest-value thing a change to `packages/sync/` or this +package can do is check this table (and re-run `tests/wireContract.test.ts`) before merging. diff --git a/docs/docusaurus.config.ts b/docs/docusaurus.config.ts index 41742d05e..e15824c1b 100644 --- a/docs/docusaurus.config.ts +++ b/docs/docusaurus.config.ts @@ -57,7 +57,30 @@ const config: Config = { }, }, + // The embedded octobot-client demo (src/components/demo/) is styled with + // Tailwind v4, scoped under `.octobot-demo` (see src/css/demo.css) so it + // never touches Infima. Tailwind v4 has no standalone CLI/watcher step — + // it's a PostCSS plugin, so it's wired straight into Docusaurus's own + // PostCSS pipeline here rather than added as a separate build step. + customFields: { + // Overridable default rendezvous server for the website-pairing demo + // section (src/components/demo/sections/WebsitePairingSim.tsx) — set + // these to point the docs build at a local/staging sync server instead + // of production. + rendezvous: { + baseUrl: process.env.DEMO_RENDEZVOUS_BASE_URL, + namespace: process.env.DEMO_RENDEZVOUS_NAMESPACE, + }, + }, + plugins: [ + () => ({ + name: 'tailwind-postcss', + configurePostCss(options) { + options.plugins.push(require('@tailwindcss/postcss')) + return options + }, + }), [require.resolve('docusaurus-lunr-search'), { languages: ['en', 'fr'], }], @@ -184,6 +207,12 @@ const config: Config = { position: 'left', label: 'Developers', }, + { + type: 'docSidebar', + sidebarId: 'client-sdk', + position: 'left', + label: 'Client SDK', + }, { type: 'localeDropdown', position: 'right', @@ -210,6 +239,8 @@ const config: Config = { {label: 'OctoBot Cloud', to: '/investing/introduction'}, {label: 'Blog', to: '/blog'}, {label: 'Developers', to: '/developers/getting-started'}, + {label: 'Client SDK', to: '/client-sdk/getting-started'}, + {label: 'Client SDK demo', to: '/demo'}, ], }, { diff --git a/docs/package-lock.json b/docs/package-lock.json index c617be333..fefc8e05e 100644 --- a/docs/package-lock.json +++ b/docs/package-lock.json @@ -11,22 +11,55 @@ "@docusaurus/core": "3.9.2", "@docusaurus/plugin-client-redirects": "^3.9.2", "@docusaurus/preset-classic": "3.9.2", + "@drakkar.software/octobot-client": "file:../packages/client/octobot_client_ts", + "@drakkar.software/starfish-spaces": "3.0.0-alpha.70", + "@fontsource/dm-mono": "^5.2.8", + "@fontsource/dm-sans": "^5.2.8", "@mdx-js/react": "^3.0.0", + "@tailwindcss/postcss": "^4.2.4", "docusaurus-lunr-search": "^3.6.0", "prism-react-renderer": "^2.3.0", + "qrcode.react": "^4.2.0", "react": "^19.0.0", - "react-dom": "^19.0.0" + "react-dom": "^19.0.0", + "tailwindcss": "^4.2.4" }, "devDependencies": { "@docusaurus/module-type-aliases": "3.9.2", "@docusaurus/tsconfig": "3.9.2", "@docusaurus/types": "3.9.2", - "typescript": "~5.6.2" + "@types/node": "^22.0.0", + "typescript": "~5.9.3", + "vitest": "^3.0.0" }, "engines": { "node": ">=20.0" } }, + "../packages/client/octobot_client_ts": { + "name": "@drakkar.software/octobot-client", + "version": "0.2.0", + "license": "GPL-3.0", + "dependencies": { + "@drakkar.software/octobot-protocol": "^0.6.0", + "@drakkar.software/starfish-client": "3.0.0-alpha.70", + "@drakkar.software/starfish-identities": "3.0.0-alpha.70", + "@drakkar.software/starfish-keyring": "3.0.0-alpha.70", + "@drakkar.software/starfish-protocol": "3.0.0-alpha.70", + "@drakkar.software/starfish-replica": "3.0.0-alpha.70", + "@drakkar.software/starfish-spaces": "^3.0.0-alpha.70", + "@noble/curves": "2.2.0", + "@noble/hashes": "2.2.0" + }, + "devDependencies": { + "@types/node": "^20.11.0", + "typescript": "~5.9.2", + "vitest": "^2.1.9" + }, + "engines": { + "node": ">=22" + } + }, "node_modules/@algolia/abtesting": { "version": "1.16.0", "resolved": "https://registry.npmjs.org/@algolia/abtesting/-/abtesting-1.16.0.tgz", @@ -260,6 +293,18 @@ "node": ">= 14.0.0" } }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@babel/code-frame": { "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", @@ -4087,249 +4132,871 @@ "node": ">=20.0" } }, - "node_modules/@hapi/hoek": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", - "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==", - "license": "BSD-3-Clause" - }, - "node_modules/@hapi/topo": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz", - "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==", - "license": "BSD-3-Clause", - "dependencies": { - "@hapi/hoek": "^9.0.0" - } + "node_modules/@drakkar.software/octobot-client": { + "resolved": "../packages/client/octobot_client_ts", + "link": true }, - "node_modules/@jest/schemas": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", - "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", - "license": "MIT", + "node_modules/@drakkar.software/starfish-client": { + "version": "3.0.0-alpha.70", + "resolved": "https://registry.npmjs.org/@drakkar.software/starfish-client/-/starfish-client-3.0.0-alpha.70.tgz", + "integrity": "sha512-P09/laErYKWcLDSLkjkKoK8TldhNzHcHsoulcG0+6sfwNLNNQjakIsmIMCdi9jBJWcJs6+9ugFNnsRx5GdbQJw==", "dependencies": { - "@sinclair/typebox": "^0.27.8" + "@drakkar.software/starfish-protocol": "3.0.0-alpha.70" }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/types": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", - "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", - "license": "MIT", - "dependencies": { - "@jest/schemas": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^17.0.8", - "chalk": "^4.0.0" + "peerDependencies": { + "@legendapp/state": ">=2.0.0", + "immer": ">=9.0.0", + "react": ">=18.0.0", + "zustand": ">=4.0.0" }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" + "peerDependenciesMeta": { + "@legendapp/state": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "zustand": { + "optional": true + } } }, - "node_modules/@jridgewell/remapping": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", - "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "license": "MIT", + "node_modules/@drakkar.software/starfish-identities": { + "version": "3.0.0-alpha.70", + "resolved": "https://registry.npmjs.org/@drakkar.software/starfish-identities/-/starfish-identities-3.0.0-alpha.70.tgz", + "integrity": "sha512-D0PStGUmIP1N3kodnsE2YUTu7KmBBKexyrdg1T0sNthFHgwfRssJIN83+0YKp944eT6WLIH99SbrqFJPdX8KCg==", "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" + "@drakkar.software/starfish-client": "3.0.0-alpha.70", + "@drakkar.software/starfish-keyring": "3.0.0-alpha.70", + "@drakkar.software/starfish-protocol": "3.0.0-alpha.70", + "@noble/curves": "^2.2.0", + "@noble/hashes": "^2.0.0", + "hash-wasm": "^4.12.0" } }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "node_modules/@drakkar.software/starfish-identities/node_modules/@noble/hashes": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.3.0.tgz", + "integrity": "sha512-oN+QwyX7VSHotibwubG3kpzbwKrfnyR6OOO+3Nk/53ADL7FmgHHz4TgrbaYKvvOw09u6QTx0oiH1cNCIOuN0CQ==", "license": "MIT", "engines": { - "node": ">=6.0.0" + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" } }, - "node_modules/@jridgewell/source-map": { - "version": "0.3.11", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", - "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", - "license": "MIT", + "node_modules/@drakkar.software/starfish-keyring": { + "version": "3.0.0-alpha.70", + "resolved": "https://registry.npmjs.org/@drakkar.software/starfish-keyring/-/starfish-keyring-3.0.0-alpha.70.tgz", + "integrity": "sha512-1QQ3bZBHX2z2mr2qsGCfMp290GlrHyI77FLLD2DWQF6dA7AMuNohMjXIaTUMJQnsZGhVwWWzB1gbvBLB/Tm4IQ==", "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25" + "@drakkar.software/starfish-client": "3.0.0-alpha.70", + "@drakkar.software/starfish-protocol": "3.0.0-alpha.70", + "@noble/curves": "^2.2.0" } }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "license": "MIT", + "node_modules/@drakkar.software/starfish-protocol": { + "version": "3.0.0-alpha.70", + "resolved": "https://registry.npmjs.org/@drakkar.software/starfish-protocol/-/starfish-protocol-3.0.0-alpha.70.tgz", + "integrity": "sha512-ADjWr2pmM6P9Au/FqQ9o6Gx+JYYtlCNs/4fFURqlwCyV3ltK5uSyC/rVlXll+o0ymNaaHwb/2QLdZYopdh03RQ==", "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" + "@noble/curves": "^2.2.0", + "@noble/hashes": "^2.2.0", + "hash-wasm": "^4.12.0" } }, - "node_modules/@jsonjoy.com/base64": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-1.1.2.tgz", - "integrity": "sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA==", - "license": "Apache-2.0", + "node_modules/@drakkar.software/starfish-protocol/node_modules/@noble/hashes": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.3.0.tgz", + "integrity": "sha512-oN+QwyX7VSHotibwubG3kpzbwKrfnyR6OOO+3Nk/53ADL7FmgHHz4TgrbaYKvvOw09u6QTx0oiH1cNCIOuN0CQ==", + "license": "MIT", "engines": { - "node": ">=10.0" + "node": ">= 20.19.0" }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" + "url": "https://paulmillr.com/funding/" } }, - "node_modules/@jsonjoy.com/buffers": { - "version": "17.67.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-17.67.0.tgz", - "integrity": "sha512-tfExRpYxBvi32vPs9ZHaTjSP4fHAfzSmcahOfNxtvGHcyJel+aibkPlGeBB+7AoC6hL7lXIE++8okecBxx7lcw==", - "license": "Apache-2.0", - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" + "node_modules/@drakkar.software/starfish-server": { + "version": "3.0.0-alpha.70", + "resolved": "https://registry.npmjs.org/@drakkar.software/starfish-server/-/starfish-server-3.0.0-alpha.70.tgz", + "integrity": "sha512-At7ITTiTBV1CUwzf5nQigIkdzOKOlzQT2IkdR21OTB8Rjw53nOgiEVlQRG7LNoKSvZcMe+5TJNQKhELAHyBDXg==", + "dependencies": { + "@drakkar.software/starfish-protocol": "3.0.0-alpha.70", + "@noble/curves": "^2.2.0", + "@noble/hashes": "^2.2.0", + "hono": "^4.12.7" }, "peerDependencies": { - "tslib": "2" + "@aws-sdk/client-s3": ">=3.0.0", + "ajv": ">=8.0.0" + }, + "peerDependenciesMeta": { + "@aws-sdk/client-s3": { + "optional": true + }, + "ajv": { + "optional": true + } } }, - "node_modules/@jsonjoy.com/codegen": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/codegen/-/codegen-1.0.0.tgz", - "integrity": "sha512-E8Oy+08cmCf0EK/NMxpaJZmOxPqM+6iSe2S4nlSBrPZOORoDJILxtbSUEDKQyTamm/BVAhIGllOBNU79/dwf0g==", - "license": "Apache-2.0", + "node_modules/@drakkar.software/starfish-server/node_modules/@noble/hashes": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.3.0.tgz", + "integrity": "sha512-oN+QwyX7VSHotibwubG3kpzbwKrfnyR6OOO+3Nk/53ADL7FmgHHz4TgrbaYKvvOw09u6QTx0oiH1cNCIOuN0CQ==", + "license": "MIT", "engines": { - "node": ">=10.0" + "node": ">= 20.19.0" }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" + "url": "https://paulmillr.com/funding/" } }, - "node_modules/@jsonjoy.com/fs-core": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-core/-/fs-core-4.57.1.tgz", - "integrity": "sha512-YrEi/ZPmgc+GfdO0esBF04qv8boK9Dg9WpRQw/+vM8Qt3nnVIJWIa8HwZ/LXVZ0DB11XUROM8El/7yYTJX+WtA==", - "license": "Apache-2.0", + "node_modules/@drakkar.software/starfish-sharing": { + "version": "3.0.0-alpha.70", + "resolved": "https://registry.npmjs.org/@drakkar.software/starfish-sharing/-/starfish-sharing-3.0.0-alpha.70.tgz", + "integrity": "sha512-pu9umLormQagFJC14yINL/pKHTeks2h5w69EcsidPsFPUNubQV8Goa2jrYMHI3jl74P81jXPTWIvMl7p48ZQqQ==", "dependencies": { - "@jsonjoy.com/fs-node-builtins": "4.57.1", - "@jsonjoy.com/fs-node-utils": "4.57.1", - "thingies": "^2.5.0" - }, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" + "@drakkar.software/starfish-client": "3.0.0-alpha.70", + "@drakkar.software/starfish-keyring": "3.0.0-alpha.70", + "@drakkar.software/starfish-protocol": "3.0.0-alpha.70" } }, - "node_modules/@jsonjoy.com/fs-fsa": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-fsa/-/fs-fsa-4.57.1.tgz", - "integrity": "sha512-ooEPvSW/HQDivPDPZMibHGKZf/QS4WRir1czGZmXmp3MsQqLECZEpN0JobrD8iV9BzsuwdIv+PxtWX9WpPLsIA==", - "license": "Apache-2.0", + "node_modules/@drakkar.software/starfish-spaces": { + "version": "3.0.0-alpha.70", + "resolved": "https://registry.npmjs.org/@drakkar.software/starfish-spaces/-/starfish-spaces-3.0.0-alpha.70.tgz", + "integrity": "sha512-QEDMc7Z7XbrkiLlpQrrPmrh+OU52CRrxRm6DySagmpS/hMIfeq4HJJ7vtGR65Pi9OyrCIbAXH5QZUMhqNphetQ==", "dependencies": { - "@jsonjoy.com/fs-core": "4.57.1", - "@jsonjoy.com/fs-node-builtins": "4.57.1", - "@jsonjoy.com/fs-node-utils": "4.57.1", - "thingies": "^2.5.0" - }, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" + "@drakkar.software/starfish-client": "3.0.0-alpha.70", + "@drakkar.software/starfish-identities": "3.0.0-alpha.70", + "@drakkar.software/starfish-keyring": "3.0.0-alpha.70", + "@drakkar.software/starfish-protocol": "3.0.0-alpha.70", + "@drakkar.software/starfish-server": "3.0.0-alpha.70", + "@drakkar.software/starfish-sharing": "3.0.0-alpha.70", + "@noble/curves": "^2.2.0", + "@noble/hashes": "^2.0.0", + "@scure/bip39": "^1.5.4" } }, - "node_modules/@jsonjoy.com/fs-node": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node/-/fs-node-4.57.1.tgz", - "integrity": "sha512-3YaKhP8gXEKN+2O49GLNfNb5l2gbnCFHyAaybbA2JkkbQP3dpdef7WcUaHAulg/c5Dg4VncHsA3NWAUSZMR5KQ==", - "license": "Apache-2.0", - "dependencies": { - "@jsonjoy.com/fs-core": "4.57.1", - "@jsonjoy.com/fs-node-builtins": "4.57.1", - "@jsonjoy.com/fs-node-utils": "4.57.1", - "@jsonjoy.com/fs-print": "4.57.1", - "@jsonjoy.com/fs-snapshot": "4.57.1", - "glob-to-regex.js": "^1.0.0", - "thingies": "^2.5.0" - }, + "node_modules/@drakkar.software/starfish-spaces/node_modules/@noble/hashes": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.3.0.tgz", + "integrity": "sha512-oN+QwyX7VSHotibwubG3kpzbwKrfnyR6OOO+3Nk/53ADL7FmgHHz4TgrbaYKvvOw09u6QTx0oiH1cNCIOuN0CQ==", + "license": "MIT", "engines": { - "node": ">=10.0" + "node": ">= 20.19.0" }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" + "url": "https://paulmillr.com/funding/" } }, - "node_modules/@jsonjoy.com/fs-node-builtins": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-builtins/-/fs-node-builtins-4.57.1.tgz", - "integrity": "sha512-XHkFKQ5GSH3uxm8c3ZYXVrexGdscpWKIcMWKFQpMpMJc8gA3AwOMBJXJlgpdJqmrhPyQXxaY9nbkNeYpacC0Og==", - "license": "Apache-2.0", + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" + "node": ">=18" } }, - "node_modules/@jsonjoy.com/fs-node-to-fsa": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-to-fsa/-/fs-node-to-fsa-4.57.1.tgz", - "integrity": "sha512-pqGHyWWzNck4jRfaGV39hkqpY5QjRUQ/nRbNT7FYbBa0xf4bDG+TE1Gt2KWZrSkrkZZDE3qZUjYMbjwSliX6pg==", - "license": "Apache-2.0", - "dependencies": { - "@jsonjoy.com/fs-fsa": "4.57.1", - "@jsonjoy.com/fs-node-builtins": "4.57.1", - "@jsonjoy.com/fs-node-utils": "4.57.1" - }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], "engines": { - "node": ">=10.0" + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@fontsource/dm-mono": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@fontsource/dm-mono/-/dm-mono-5.3.0.tgz", + "integrity": "sha512-OINjI8C1S/wpchhQxl7njZdMn4+hnDCpQ4YtvvOpKNARo+0J8O1x1IcrChxNjHOhfVv1by8C/FQoy3hXK+C1Ug==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, + "node_modules/@fontsource/dm-sans": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@fontsource/dm-sans/-/dm-sans-5.3.0.tgz", + "integrity": "sha512-lYJtMXXO28q1z+yz+z8XKd0s4hXaa9QdkETzkyD760sidCv5heI86weYA0sx0Nc4pAMAQTUuyf4gO44cYKKS9g==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, + "node_modules/@hapi/hoek": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", + "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/topo": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz", + "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@jsonjoy.com/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/buffers": { + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-17.67.0.tgz", + "integrity": "sha512-tfExRpYxBvi32vPs9ZHaTjSP4fHAfzSmcahOfNxtvGHcyJel+aibkPlGeBB+7AoC6hL7lXIE++8okecBxx7lcw==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/codegen": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/codegen/-/codegen-1.0.0.tgz", + "integrity": "sha512-E8Oy+08cmCf0EK/NMxpaJZmOxPqM+6iSe2S4nlSBrPZOORoDJILxtbSUEDKQyTamm/BVAhIGllOBNU79/dwf0g==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-core": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-core/-/fs-core-4.57.1.tgz", + "integrity": "sha512-YrEi/ZPmgc+GfdO0esBF04qv8boK9Dg9WpRQw/+vM8Qt3nnVIJWIa8HwZ/LXVZ0DB11XUROM8El/7yYTJX+WtA==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/fs-node-builtins": "4.57.1", + "@jsonjoy.com/fs-node-utils": "4.57.1", + "thingies": "^2.5.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-fsa": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-fsa/-/fs-fsa-4.57.1.tgz", + "integrity": "sha512-ooEPvSW/HQDivPDPZMibHGKZf/QS4WRir1czGZmXmp3MsQqLECZEpN0JobrD8iV9BzsuwdIv+PxtWX9WpPLsIA==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/fs-core": "4.57.1", + "@jsonjoy.com/fs-node-builtins": "4.57.1", + "@jsonjoy.com/fs-node-utils": "4.57.1", + "thingies": "^2.5.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-node": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node/-/fs-node-4.57.1.tgz", + "integrity": "sha512-3YaKhP8gXEKN+2O49GLNfNb5l2gbnCFHyAaybbA2JkkbQP3dpdef7WcUaHAulg/c5Dg4VncHsA3NWAUSZMR5KQ==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/fs-core": "4.57.1", + "@jsonjoy.com/fs-node-builtins": "4.57.1", + "@jsonjoy.com/fs-node-utils": "4.57.1", + "@jsonjoy.com/fs-print": "4.57.1", + "@jsonjoy.com/fs-snapshot": "4.57.1", + "glob-to-regex.js": "^1.0.0", + "thingies": "^2.5.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-node-builtins": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-builtins/-/fs-node-builtins-4.57.1.tgz", + "integrity": "sha512-XHkFKQ5GSH3uxm8c3ZYXVrexGdscpWKIcMWKFQpMpMJc8gA3AwOMBJXJlgpdJqmrhPyQXxaY9nbkNeYpacC0Og==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-node-to-fsa": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-to-fsa/-/fs-node-to-fsa-4.57.1.tgz", + "integrity": "sha512-pqGHyWWzNck4jRfaGV39hkqpY5QjRUQ/nRbNT7FYbBa0xf4bDG+TE1Gt2KWZrSkrkZZDE3qZUjYMbjwSliX6pg==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/fs-fsa": "4.57.1", + "@jsonjoy.com/fs-node-builtins": "4.57.1", + "@jsonjoy.com/fs-node-utils": "4.57.1" + }, + "engines": { + "node": ">=10.0" }, "funding": { "type": "github", @@ -4655,6 +5322,50 @@ "react": ">=16" } }, + "node_modules/@napi-rs/lzma-linux-x64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", + "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@noble/curves": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-2.3.0.tgz", + "integrity": "sha512-v7cY+4oWYPQszRj6ZFGzTVL7uP2TaLo1xMhWHzYC5wj0ZhOXQ5x+sBre8rF3hi8cAoi0bh1qXoovoOkdFtvqEg==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "2.3.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/curves/node_modules/@noble/hashes": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.3.0.tgz", + "integrity": "sha512-oN+QwyX7VSHotibwubG3kpzbwKrfnyR6OOO+3Nk/53ADL7FmgHHz4TgrbaYKvvOw09u6QTx0oiH1cNCIOuN0CQ==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@noble/hashes": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", @@ -4798,105 +5509,489 @@ "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.6.0.tgz", "integrity": "sha512-xNLYLBFTBKkCzEZIw842BxytQQATQv+lDTCEMZ8C196iJcJJMBUZxrhSTxLaohMyKK8QlzRNTRkUmanucnDSqg==", "license": "MIT", - "dependencies": { - "asn1js": "^3.0.6", - "pvtsutils": "^1.3.6", - "tslib": "^2.8.1" - } - }, - "node_modules/@peculiar/asn1-x509": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509/-/asn1-x509-2.6.1.tgz", - "integrity": "sha512-O9jT5F1A2+t3r7C4VT7LYGXqkGLK7Kj1xFpz7U0isPrubwU5PbDoyYtx6MiGst29yq7pXN5vZbQFKRCP+lLZlA==", + "dependencies": { + "asn1js": "^3.0.6", + "pvtsutils": "^1.3.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-x509": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509/-/asn1-x509-2.6.1.tgz", + "integrity": "sha512-O9jT5F1A2+t3r7C4VT7LYGXqkGLK7Kj1xFpz7U0isPrubwU5PbDoyYtx6MiGst29yq7pXN5vZbQFKRCP+lLZlA==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.6.0", + "asn1js": "^3.0.6", + "pvtsutils": "^1.3.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-x509-attr": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509-attr/-/asn1-x509-attr-2.6.1.tgz", + "integrity": "sha512-tlW6cxoHwgcQghnJwv3YS+9OO1737zgPogZ+CgWRUK4roEwIPzRH4JEiG770xe5HX2ATfCpmX60gurfWIF9dcQ==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.1", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/x509": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/@peculiar/x509/-/x509-1.14.3.tgz", + "integrity": "sha512-C2Xj8FZ0uHWeCXXqX5B4/gVFQmtSkiuOolzAgutjTfseNOHT3pUjljDZsTSxXFGgio54bCzVFqmEOUrIVk8RDA==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-cms": "^2.6.0", + "@peculiar/asn1-csr": "^2.6.0", + "@peculiar/asn1-ecc": "^2.6.0", + "@peculiar/asn1-pkcs9": "^2.6.0", + "@peculiar/asn1-rsa": "^2.6.0", + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.0", + "pvtsutils": "^1.3.6", + "reflect-metadata": "^0.2.2", + "tslib": "^2.8.1", + "tsyringe": "^4.10.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@pnpm/config.env-replace": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@pnpm/config.env-replace/-/config.env-replace-1.1.0.tgz", + "integrity": "sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w==", + "license": "MIT", + "engines": { + "node": ">=12.22.0" + } + }, + "node_modules/@pnpm/network.ca-file": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@pnpm/network.ca-file/-/network.ca-file-1.0.2.tgz", + "integrity": "sha512-YcPQ8a0jwYU9bTdJDpXjMi7Brhkr1mXsXrUJvjqM2mQDgkRiz8jFaQGOdaLxgjtUfQgZhKy/O3cG/YwmgKaxLA==", + "license": "MIT", + "dependencies": { + "graceful-fs": "4.2.10" + }, + "engines": { + "node": ">=12.22.0" + } + }, + "node_modules/@pnpm/network.ca-file/node_modules/graceful-fs": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", + "license": "ISC" + }, + "node_modules/@pnpm/npm-conf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@pnpm/npm-conf/-/npm-conf-3.0.2.tgz", + "integrity": "sha512-h104Kh26rR8tm+a3Qkc5S4VLYint3FE48as7+/5oCEcKR2idC/pF1G6AhIXKI+eHPJa/3J9i5z0Al47IeGHPkA==", + "license": "MIT", + "dependencies": { + "@pnpm/config.env-replace": "^1.1.0", + "@pnpm/network.ca-file": "^1.0.1", + "config-chain": "^1.1.11" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@polka/url": { + "version": "1.0.0-next.29", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", + "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.4.tgz", + "integrity": "sha512-RrPokAb7dmbxFoeO3TloqHyOjgye8RkBhSqmp4aJMIex4c9r46ZstPnleDQOq1t46VOVjwIuwNogIqbodV1Vvg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.4.tgz", + "integrity": "sha512-JKuJc+pnpks2pjy7L/N3v/cAkZxYlnmuZoD840ldbMI5KDbC4iO9NKwPKYdjYFCMAIIlBzYSFHxIJVYzRo2/8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.4.tgz", + "integrity": "sha512-krw5uS2STmvJ02x0uTXHbqQNuz+9eZ1iw+qXk9dmW2gvV4jV7O2hEoOnuhFrpOPiel1mBFtqbxYZZtC46hXLOw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.4.tgz", + "integrity": "sha512-wsTxtgApb4PrOsNJIm0FZ1h3WvCC+k9uxLJ4ad75hgoS4NiRes2SoJFlDAyMwiUY8IssDqGcHbXuN0sx1tfF1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.4.tgz", + "integrity": "sha512-GUOnQlyZe3yAXhWOtOMsn5Qkrv5E5mZXa0thbARWi5Ei2szlVXJFQhddZ4HbAzh8q92w5twp+CQvs/eFanz9YQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.4.tgz", + "integrity": "sha512-/Y7f3QuxjzPKsjA/rfEDa3+0vXqyjmJ50Ln8dPpCmWkKTrUoWHG1cWhTqaAMLob2m2nESWuC7yGrREz019Ztqg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.4.tgz", + "integrity": "sha512-81wiiX3v7aqy+T+bT61TJ78yJjRquqFFTTbAPt08imfQQzkPIW8t6aJbkTagtCCrXMNc9D66+geqlK7ydLPNqA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.4.tgz", + "integrity": "sha512-9kmDIvNZqdoHOBZgNtpTBeLWYO/LVipM3H/j62P8848/l/VPEQL6N3uxU9pvP1oZAsXyC2MEnFP3ovRjo7WYNQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.4.tgz", + "integrity": "sha512-CcnXHWnXg69g+DX5VWL3FHts3qMRN2uVEHX+BZvGLdd07/gXkn3ePjYtO1LDJvxkGKVHMclKBRa1QUTH+6toYQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.4.tgz", + "integrity": "sha512-iFOibiHnTRuhrWLlRsOQFdZJJIa7S8OwkneJr4ocALP16u5yk6lWLINFwhHaEqBFMsKDUZofLkGos7+CPzGB3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.4.tgz", + "integrity": "sha512-XnWYMI7euHlb5a871xPja+Gm7DRCFU+FGRrtS2sMq9N8FvqtpagUy6gD4YOemC5MRk9xbh8+jYMEJbigFQwsgA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.4.tgz", + "integrity": "sha512-qGDAlO0U8xedCcsdRm9oaoQY8DAx/QT7uIxJWhCdx0ceIWX783UC9QSYkdpzAe29wNiVfp24+bZdQmn49o45SQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.4.tgz", + "integrity": "sha512-ru4H6ezD7ysA5EiEK6qkkaEb4modH8CTej6kUy/gQi20u3kB3G7Zn8snXXkeJSCOFKG/rbPPtM/+9Wgas1961w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.4.tgz", + "integrity": "sha512-2W4MO5WQVJnbJaZdvDb9rhBDuFU1nKIepPFpJUBsTh2k1YY2g+ODViaWuyOAjQ5cOP7NvrvLzt3wvHOoiAvc7w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.4.tgz", + "integrity": "sha512-+fxjfuoAmVMCYV5QyjoIpu0cp5DOiOTeqYFk1AVaxGr+/ravWLX89XfQmptsoWcaVy/TGf2hexzbUOrCQIL1CQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.4.tgz", + "integrity": "sha512-jTn8JfHGL4djjFxPuM06LmNUJDsst2jeVlsd9OmIH6zc5sC9K6rIuO4YajXatLUpBmBKl6b35ro1QZocLi+tcA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.4.tgz", + "integrity": "sha512-oCJCJL4pXsoDcP2QZ+JVlPTIRc6266zsIaeJJsWImmF7HO0W8nb6HuSgZlMWxJwaPf8ehbSw8yo0EUw925hKsA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.4.tgz", + "integrity": "sha512-W69hukhZ3KKNRCaMIEzKvcFye42hh0FE1+YoYaf5+Ikacuftoco6yO/xouz0hc5d5W/s3yBro5jRiuEE/Q5vUw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.4.tgz", + "integrity": "sha512-qiXbGG2jkjXhzXpsFZSR2Xpb8DN/UaxYsbb/STbuR/6fpaDgRmmaq1B/LmtF2wQFOFOSsK2jdE0RZ3a0zHn4QA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.4.tgz", + "integrity": "sha512-nWeM//hxv8mIo6jD7Hu4o48DVmV9pbV6gsKaWU+4NFyqHoPKwrkRiZGLKUhOBk8qNmDmpwFtPKg80Bo/Tn4xiQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.4.tgz", + "integrity": "sha512-s62SQ/vgsRSvMwDkOEfTqfgASF0f26ZNaQuTA6Aok5lrikf89yI2W0gFHvZb2Jpgc6N8JnOKZgCK2iciO3CsxQ==", + "cpu": [ + "arm64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "@peculiar/asn1-schema": "^2.6.0", - "asn1js": "^3.0.6", - "pvtsutils": "^1.3.6", - "tslib": "^2.8.1" - } - }, - "node_modules/@peculiar/asn1-x509-attr": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509-attr/-/asn1-x509-attr-2.6.1.tgz", - "integrity": "sha512-tlW6cxoHwgcQghnJwv3YS+9OO1737zgPogZ+CgWRUK4roEwIPzRH4JEiG770xe5HX2ATfCpmX60gurfWIF9dcQ==", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.4.tgz", + "integrity": "sha512-J6wGf8TVGbXJq+HH+ttTvrcfNKPbuZecV6KT1B8I18BC5IURUh5kl4Yl5OEP5eFIUoI5BWxCsyYMhFsDx8kekw==", + "cpu": [ + "arm64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "@peculiar/asn1-schema": "^2.6.0", - "@peculiar/asn1-x509": "^2.6.1", - "asn1js": "^3.0.6", - "tslib": "^2.8.1" - } - }, - "node_modules/@peculiar/x509": { - "version": "1.14.3", - "resolved": "https://registry.npmjs.org/@peculiar/x509/-/x509-1.14.3.tgz", - "integrity": "sha512-C2Xj8FZ0uHWeCXXqX5B4/gVFQmtSkiuOolzAgutjTfseNOHT3pUjljDZsTSxXFGgio54bCzVFqmEOUrIVk8RDA==", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.4.tgz", + "integrity": "sha512-zmfrQd/0wu6oJs8Vq8KwY/YtsKSsLtKe/HwAP4Wqy8LhWjeT55fHRAkOhYQ12wI3ayS4Tt12d5CDRD7N96SAYQ==", + "cpu": [ + "ia32" + ], + "dev": true, "license": "MIT", - "dependencies": { - "@peculiar/asn1-cms": "^2.6.0", - "@peculiar/asn1-csr": "^2.6.0", - "@peculiar/asn1-ecc": "^2.6.0", - "@peculiar/asn1-pkcs9": "^2.6.0", - "@peculiar/asn1-rsa": "^2.6.0", - "@peculiar/asn1-schema": "^2.6.0", - "@peculiar/asn1-x509": "^2.6.0", - "pvtsutils": "^1.3.6", - "reflect-metadata": "^0.2.2", - "tslib": "^2.8.1", - "tsyringe": "^4.10.0" - }, - "engines": { - "node": ">=20.0.0" - } + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.4.tgz", + "integrity": "sha512-qPzHqdj9rfUD+w79dtE07zi/kFwKyCJqplp5K5ygeLTp7jLpAoc16OAH39HSmRC9UpozaecsleI8uAdEj6v2yw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.4.tgz", + "integrity": "sha512-zD6NdeWEByGE9QF9vCrlJ5YQB4oq9q91kPZS37Jwj5hOkvR1lTBSpsKhKDw4IJtbQ35LsTS1HD9DZYGKIshU1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] }, - "node_modules/@pnpm/config.env-replace": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@pnpm/config.env-replace/-/config.env-replace-1.1.0.tgz", - "integrity": "sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w==", + "node_modules/@scure/base": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.2.6.tgz", + "integrity": "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==", "license": "MIT", - "engines": { - "node": ">=12.22.0" + "funding": { + "url": "https://paulmillr.com/funding/" } }, - "node_modules/@pnpm/network.ca-file": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@pnpm/network.ca-file/-/network.ca-file-1.0.2.tgz", - "integrity": "sha512-YcPQ8a0jwYU9bTdJDpXjMi7Brhkr1mXsXrUJvjqM2mQDgkRiz8jFaQGOdaLxgjtUfQgZhKy/O3cG/YwmgKaxLA==", + "node_modules/@scure/bip39": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.6.0.tgz", + "integrity": "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A==", "license": "MIT", "dependencies": { - "graceful-fs": "4.2.10" + "@noble/hashes": "~1.8.0", + "@scure/base": "~1.2.5" }, - "engines": { - "node": ">=12.22.0" + "funding": { + "url": "https://paulmillr.com/funding/" } }, - "node_modules/@pnpm/network.ca-file/node_modules/graceful-fs": { - "version": "4.2.10", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", - "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", - "license": "ISC" - }, - "node_modules/@pnpm/npm-conf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@pnpm/npm-conf/-/npm-conf-3.0.2.tgz", - "integrity": "sha512-h104Kh26rR8tm+a3Qkc5S4VLYint3FE48as7+/5oCEcKR2idC/pF1G6AhIXKI+eHPJa/3J9i5z0Al47IeGHPkA==", + "node_modules/@scure/bip39/node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", "license": "MIT", - "dependencies": { - "@pnpm/config.env-replace": "^1.1.0", - "@pnpm/network.ca-file": "^1.0.1", - "config-chain": "^1.1.11" - }, "engines": { - "node": ">=12" + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" } }, - "node_modules/@polka/url": { - "version": "1.0.0-next.29", - "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", - "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", - "license": "MIT" - }, "node_modules/@sideway/address": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", @@ -5204,16 +6299,281 @@ "url": "https://github.com/sponsors/gregberge" } }, - "node_modules/@szmarczak/http-timer": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-5.0.1.tgz", - "integrity": "sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==", + "node_modules/@szmarczak/http-timer": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-5.0.1.tgz", + "integrity": "sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==", + "license": "MIT", + "dependencies": { + "defer-to-connect": "^2.0.1" + }, + "engines": { + "node": ">=14.16" + } + }, + "node_modules/@tailwindcss/node": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.3.tgz", + "integrity": "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==", + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.24.1", + "jiti": "^2.7.0", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.3.3" + } + }, + "node_modules/@tailwindcss/node/node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.3.tgz", + "integrity": "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==", + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.3.3", + "@tailwindcss/oxide-darwin-arm64": "4.3.3", + "@tailwindcss/oxide-darwin-x64": "4.3.3", + "@tailwindcss/oxide-freebsd-x64": "4.3.3", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.3", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.3", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.3", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.3", + "@tailwindcss/oxide-linux-x64-musl": "4.3.3", + "@tailwindcss/oxide-wasm32-wasi": "4.3.3", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.3", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.3" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.3.tgz", + "integrity": "sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.3.tgz", + "integrity": "sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.3.tgz", + "integrity": "sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.3.tgz", + "integrity": "sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.3.tgz", + "integrity": "sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.3.tgz", + "integrity": "sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.3.tgz", + "integrity": "sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.3.tgz", + "integrity": "sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.3.tgz", + "integrity": "sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.3.tgz", + "integrity": "sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.11.1", + "@emnapi/runtime": "^1.11.1", + "@emnapi/wasi-threads": "^1.2.2", + "@napi-rs/wasm-runtime": "^1.1.4", + "@tybys/wasm-util": "^0.10.2", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.3.tgz", + "integrity": "sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.3.tgz", + "integrity": "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/postcss": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.3.3.tgz", + "integrity": "sha512-JTSZZGQi1AyKirbLN3azmjVzef92tcX7h+iSqPdaeStyFpGpDlKvvpxeOE8njhbUanbRwr3z8DyzhICWnMtQeg==", "license": "MIT", "dependencies": { - "defer-to-connect": "^2.0.1" - }, - "engines": { - "node": ">=14.16" + "@alloc/quick-lru": "^5.2.0", + "@tailwindcss/node": "4.3.3", + "@tailwindcss/oxide": "4.3.3", + "postcss": "^8.5.16", + "tailwindcss": "4.3.3" } }, "node_modules/@types/body-parser": { @@ -5235,6 +6595,17 @@ "@types/node": "*" } }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, "node_modules/@types/connect": { "version": "3.4.38", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", @@ -5263,6 +6634,13 @@ "@types/ms": "*" } }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/eslint": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", @@ -5284,9 +6662,9 @@ } }, "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", "license": "MIT" }, "node_modules/@types/estree-jsx": { @@ -5428,12 +6806,12 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz", - "integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==", + "version": "22.20.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.1.tgz", + "integrity": "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==", "license": "MIT", "dependencies": { - "undici-types": "~7.18.0" + "undici-types": "~6.21.0" } }, "node_modules/@types/parse5": { @@ -5599,6 +6977,121 @@ "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", "license": "ISC" }, + "node_modules/@vitest/expect": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.7.tgz", + "integrity": "sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.7.tgz", + "integrity": "sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.7", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.7.tgz", + "integrity": "sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.7.tgz", + "integrity": "sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.7", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.7.tgz", + "integrity": "sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.7", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.7.tgz", + "integrity": "sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.7.tgz", + "integrity": "sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.7", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/@webassemblyjs/ast": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", @@ -6105,6 +7598,16 @@ "node": ">=12.0.0" } }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/astring": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/astring/-/astring-1.9.0.tgz", @@ -6476,6 +7979,16 @@ "node": ">=6.0.0" } }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/cacheable-lookup": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz", @@ -6623,6 +8136,23 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -6688,6 +8218,16 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, "node_modules/cheerio": { "version": "1.0.0-rc.12", "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.12.tgz", @@ -7783,6 +9323,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/deep-extend": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", @@ -7909,6 +9459,15 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/detect-node": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", @@ -8284,13 +9843,13 @@ } }, "node_modules/enhanced-resolve": { - "version": "5.20.1", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz", - "integrity": "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==", + "version": "5.24.5", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.5.tgz", + "integrity": "sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A==", "license": "MIT", "dependencies": { "graceful-fs": "^4.2.4", - "tapable": "^2.3.0" + "tapable": "^2.3.3" }, "engines": { "node": ">=10.13.0" @@ -8385,6 +9944,48 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/esbuild": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -8663,6 +10264,16 @@ "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/express": { "version": "4.22.1", "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", @@ -9483,6 +11094,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/hash-wasm": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/hash-wasm/-/hash-wasm-4.12.0.tgz", + "integrity": "sha512-+/2B2rYLb48I/evdOIhP+K/DD2ca2fgBjp6O+GBEnCDk2e4rpeXIK8GvIyRPjTezgmWn9gmKwkQjjx6BtqDHVQ==", + "license": "MIT" + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -9856,6 +11473,15 @@ "react-is": "^16.7.0" } }, + "node_modules/hono": { + "version": "4.13.1", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.13.1.tgz", + "integrity": "sha512-kdJoFVv2xmayw6cY09H7AbMJMt8Jn5jdlEdXsP7AGBdF2DIptVlKlOLKXP41yPip4/a3yQPv9gVcJYI8YY04dw==", + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, "node_modules/hpack.js": { "version": "2.1.6", "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", @@ -10854,13 +12480,262 @@ "shell-quote": "^1.8.3" } }, - "node_modules/leven": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", - "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", - "license": "MIT", + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=6" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, "node_modules/lilconfig": { @@ -10969,6 +12844,13 @@ "loose-envify": "cli.js" } }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, "node_modules/lower-case": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", @@ -11009,6 +12891,15 @@ "resolved": "https://registry.npmjs.org/lunr-languages/-/lunr-languages-1.14.0.tgz", "integrity": "sha512-hWUAb2KqM3L7J5bcrngszzISY4BxrXn/Xhbb9TTCJYEGqlR1nG67/M14sp09+PTIRklobrn57IAxcdcO/ZFyNA==" }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, "node_modules/mark.js": { "version": "8.11.1", "resolved": "https://registry.npmjs.org/mark.js/-/mark.js-8.11.1.tgz", @@ -13460,9 +15351,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "version": "3.3.17", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.17.tgz", + "integrity": "sha512-xQLf0A3HOMlgHq0n247/LRuAOYmB7dXJ/DvAxGvsSBij45XtBSmQycu+F8ODbHwns/XyFZagyL1+J0Offw1E0g==", "funding": [ { "type": "github", @@ -14081,6 +15972,23 @@ "node": ">=8" } }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -14132,9 +16040,9 @@ } }, "node_modules/postcss": { - "version": "8.5.8", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", - "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", "funding": [ { "type": "opencollective", @@ -14151,7 +16059,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.11", + "nanoid": "^3.3.17", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -15745,6 +17653,15 @@ "node": ">=16.0.0" } }, + "node_modules/qrcode.react": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/qrcode.react/-/qrcode.react-4.2.0.tgz", + "integrity": "sha512-QpgqWi8rD9DsS9EP3z7BT+5lY5SFhsqGjpgW5DY/i3mK4M9DTBNz3ErMi8BWYEfI3L0d8GIbGmcdFAS1uIRGjA==", + "license": "ISC", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/qs": { "version": "6.14.2", "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", @@ -16721,6 +18638,52 @@ "node": ">=0.10.0" } }, + "node_modules/rollup": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.4.tgz", + "integrity": "sha512-RXOqwaPsBGjMNMa4sQjDjHieHEZDFoj/Rdr46l2MU5DfEs16wHJPC2RPTPHWhNl+M3aI472LLqFkFKut4SblOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@napi-rs/lzma-linux-x64-gnu": "1.5.1", + "@rollup/rollup-android-arm-eabi": "4.62.4", + "@rollup/rollup-android-arm64": "4.62.4", + "@rollup/rollup-darwin-arm64": "4.62.4", + "@rollup/rollup-darwin-x64": "4.62.4", + "@rollup/rollup-freebsd-arm64": "4.62.4", + "@rollup/rollup-freebsd-x64": "4.62.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.4", + "@rollup/rollup-linux-arm-musleabihf": "4.62.4", + "@rollup/rollup-linux-arm64-gnu": "4.62.4", + "@rollup/rollup-linux-arm64-musl": "4.62.4", + "@rollup/rollup-linux-loong64-gnu": "4.62.4", + "@rollup/rollup-linux-loong64-musl": "4.62.4", + "@rollup/rollup-linux-ppc64-gnu": "4.62.4", + "@rollup/rollup-linux-ppc64-musl": "4.62.4", + "@rollup/rollup-linux-riscv64-gnu": "4.62.4", + "@rollup/rollup-linux-riscv64-musl": "4.62.4", + "@rollup/rollup-linux-s390x-gnu": "4.62.4", + "@rollup/rollup-linux-x64-gnu": "4.62.4", + "@rollup/rollup-linux-x64-musl": "4.62.4", + "@rollup/rollup-openbsd-x64": "4.62.4", + "@rollup/rollup-openharmony-arm64": "4.62.4", + "@rollup/rollup-win32-arm64-msvc": "4.62.4", + "@rollup/rollup-win32-ia32-msvc": "4.62.4", + "@rollup/rollup-win32-x64-gnu": "4.62.4", + "@rollup/rollup-win32-x64-msvc": "4.62.4", + "fsevents": "~2.3.2" + } + }, "node_modules/rtlcss": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/rtlcss/-/rtlcss-4.3.0.tgz", @@ -17237,6 +19200,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, "node_modules/signal-exit": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", @@ -17434,6 +19404,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, "node_modules/statuses": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", @@ -17572,6 +19549,26 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/strip-literal/node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, "node_modules/style-to-js": { "version": "1.1.21", "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz", @@ -17670,10 +19667,16 @@ "node": ">= 10" } }, + "node_modules/tailwindcss": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz", + "integrity": "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==", + "license": "MIT" + }, "node_modules/tapable": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.2.tgz", - "integrity": "sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA==", + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", "license": "MIT", "engines": { "node": ">=6" @@ -17803,6 +19806,68 @@ "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==", "license": "MIT" }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/tinypool": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", @@ -17812,6 +19877,26 @@ "node": "^18.0.0 || >=20.0.0" } }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -18016,9 +20101,9 @@ } }, "node_modules/typescript": { - "version": "5.6.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", - "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", "bin": { @@ -18030,9 +20115,9 @@ } }, "node_modules/undici-types": { - "version": "7.18.2", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", - "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "license": "MIT" }, "node_modules/unicode-canonical-property-names-ecmascript": { @@ -18544,6 +20629,228 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/vite": { + "version": "7.3.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz", + "integrity": "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0 || ^0.28.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite-node/node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/vitest": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.7.tgz", + "integrity": "sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.7", + "@vitest/mocker": "3.2.7", + "@vitest/pretty-format": "^3.2.7", + "@vitest/runner": "3.2.7", + "@vitest/snapshot": "3.2.7", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.7", + "@vitest/ui": "3.2.7", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/watchpack": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz", @@ -18984,6 +21291,23 @@ "node": ">= 8" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/wide-align": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", diff --git a/docs/package.json b/docs/package.json index 5d8eb21e2..4c7d5276f 100644 --- a/docs/package.json +++ b/docs/package.json @@ -7,30 +7,41 @@ "start": "npm run collect && docusaurus start", "prebuild": "npm run collect", "build": "docusaurus build", - "collect": "node scripts/collect-tentacles.mjs && node scripts/sync-root-docs.mjs && node scripts/generate-llms-txt.mjs", + "sdk:build": "npm --prefix ../packages/client/octobot_client_ts install --no-audit --no-fund && npm --prefix ../packages/client/octobot_client_ts exec -- tsc --build --force", + "collect": "npm run sdk:build && node scripts/collect-tentacles.mjs && node scripts/sync-root-docs.mjs && node scripts/generate-llms-txt.mjs", "swizzle": "docusaurus swizzle", "deploy": "docusaurus deploy", "clear": "docusaurus clear", "serve": "docusaurus serve", "write-translations": "docusaurus write-translations", "write-heading-ids": "docusaurus write-heading-ids", - "typecheck": "tsc" + "typecheck": "tsc", + "test": "vitest run" }, "dependencies": { "@docusaurus/core": "3.9.2", "@docusaurus/plugin-client-redirects": "^3.9.2", "@docusaurus/preset-classic": "3.9.2", + "@drakkar.software/octobot-client": "file:../packages/client/octobot_client_ts", + "@drakkar.software/starfish-spaces": "3.0.0-alpha.70", + "@fontsource/dm-mono": "^5.2.8", + "@fontsource/dm-sans": "^5.2.8", "@mdx-js/react": "^3.0.0", + "@tailwindcss/postcss": "^4.2.4", "docusaurus-lunr-search": "^3.6.0", "prism-react-renderer": "^2.3.0", + "qrcode.react": "^4.2.0", "react": "^19.0.0", - "react-dom": "^19.0.0" + "react-dom": "^19.0.0", + "tailwindcss": "^4.2.4" }, "devDependencies": { "@docusaurus/module-type-aliases": "3.9.2", "@docusaurus/tsconfig": "3.9.2", "@docusaurus/types": "3.9.2", - "typescript": "~5.6.2" + "@types/node": "^22.0.0", + "typescript": "~5.9.3", + "vitest": "^3.0.0" }, "browserslist": { "production": [ diff --git a/docs/sidebars.ts b/docs/sidebars.ts index 61b19b9dd..03ce4e7b3 100644 --- a/docs/sidebars.ts +++ b/docs/sidebars.ts @@ -19,6 +19,12 @@ const sidebars: SidebarsConfig = { dirName: 'octobot-script', }, ], + 'client-sdk': [ + { + type: 'autogenerated', + dirName: 'client-sdk', + }, + ], developers: [ 'developers/getting-started', { diff --git a/docs/src/components/demo/Embed.tsx b/docs/src/components/demo/Embed.tsx new file mode 100644 index 000000000..17688f7b6 --- /dev/null +++ b/docs/src/components/demo/Embed.tsx @@ -0,0 +1,79 @@ +import BrowserOnly from "@docusaurus/BrowserOnly" +import "../../css/demo.css" + +export type DemoSectionId = + | "overview" + | "derive" + | "queue" + | "errors" + | "propose" + | "website-pairing" + | "escape-hatches" + +const NEEDS_WALLET_AND_NODE: ReadonlySet = new Set([ + "derive", + "queue", + "propose", +]) + +// One section per docs page, matched to whatever that page is teaching. Each +// embed gets its OWN WalletKeyProvider/NodeUrlProvider instance — unlike +// /demo (pages/demo.tsx), where all sections share one, so trying a key on +// one page doesn't leak into another's local state. That mirrors how a +// reader actually encounters these: as independent examples, not one +// continuous session. +export function DemoEmbed({ section }: { section: DemoSectionId }) { + return ( + }> + {() => { + // `require()`, not a top-level `import` — Docusaurus prerenders + // every page with `react-dom/server`, and the SDK / starfish-spaces + // touch browser-only globals (crypto.subtle, window) in places. This + // callback only ever runs client-side, so requiring here keeps those + // modules out of the server bundle's module graph entirely. This is + // the pattern Docusaurus's own BrowserOnly docs recommend. + const { Overview } = require("./sections/Overview") + const { DerivationHero } = require("./sections/DerivationHero") + const { QueuePanel } = require("./sections/QueuePanel") + const { ErrorTaxonomy } = require("./sections/ErrorTaxonomy") + const { ProposePanel } = require("./sections/ProposePanel") + const { WebsitePairingSim } = require("./sections/WebsitePairingSim") + const { EscapeHatches } = require("./sections/EscapeHatches") + const { WalletKeyProvider } = require("./lib/walletKeyContext") + const { NodeUrlProvider } = require("./lib/nodeUrlContext") + const { + SecureContextWarning, + } = require("./components/SecureContextWarning") + + const Component = { + overview: Overview, + derive: DerivationHero, + queue: QueuePanel, + errors: ErrorTaxonomy, + propose: ProposePanel, + "website-pairing": WebsitePairingSim, + "escape-hatches": EscapeHatches, + }[section] + + const body = NEEDS_WALLET_AND_NODE.has(section) ? ( + + + + + + ) : ( + + ) + + return ( +
+ + {body} +
+ ) + }} +
+ ) +} + +export default DemoEmbed diff --git a/docs/src/components/demo/components/ByteMeter.tsx b/docs/src/components/demo/components/ByteMeter.tsx new file mode 100644 index 000000000..9942cbd62 --- /dev/null +++ b/docs/src/components/demo/components/ByteMeter.tsx @@ -0,0 +1,64 @@ +// QR byte-mode ceilings (version 40, the largest QR symbol), by error- +// correction level — the SDK does not encode against a ceiling, it just +// `JSON.stringify`s (see `protocol/proposal.ts`), so the demo has to be the +// one to say when a payload has left "actually scannable" territory. +const QR_MAX_BYTES_ECC_L = 2953 +// A phone camera at typical screen size gives up well before the +// theoretical max — measured empirically against this package's own +// proposal payloads (see CLAUDE.md for the measured sizes). +const PRACTICAL_SCAN_CEILING = 1200 + +export function byteLength(payload: string): number { + return new TextEncoder().encode(payload).byteLength +} + +export function ByteMeter({ bytes }: { bytes: number }) { + const pct = Math.min(100, (bytes / QR_MAX_BYTES_ECC_L) * 100) + const overPractical = bytes > PRACTICAL_SCAN_CEILING + const overCeiling = bytes > QR_MAX_BYTES_ECC_L + + return ( +
+
+ + {bytes.toLocaleString()} bytes + + + ceiling {QR_MAX_BYTES_ECC_L.toLocaleString()} · practical scan ~ + {PRACTICAL_SCAN_CEILING.toLocaleString()} + +
+
+
+
+ {overCeiling ? ( +

+ over the QR ceiling — this payload cannot be encoded as a scannable + code. Use copy-to-clipboard instead. +

+ ) : overPractical ? ( +

+ past where most phone cameras reliably scan. Dense but technically + valid — copy-to-clipboard is the safer path. +

+ ) : null} +
+ ) +} diff --git a/docs/src/components/demo/components/CodeBlock.tsx b/docs/src/components/demo/components/CodeBlock.tsx new file mode 100644 index 000000000..0aae874d8 --- /dev/null +++ b/docs/src/components/demo/components/CodeBlock.tsx @@ -0,0 +1,57 @@ +import { useState } from "react" + +/** + * The one code-block treatment used everywhere on the page, wire side and + * paper side alike — a page whose entire value is code should never show + * two visually distinct code surfaces (see CLAUDE.md, "one code treatment"). + * + * Always copy-exact: `code` must be the real snippet that produced what is + * on screen, never pseudocode. Set `recorded` when the panel above this + * block ran against a fixture rather than a live call — it renders a + * visible stamp rather than a quiet omission. + */ +export function CodeBlock({ + code, + language = "ts", + recorded, +}: { + code: string + language?: string + recorded?: string +}) { + const [copied, setCopied] = useState(false) + + const onCopy = () => { + void navigator.clipboard.writeText(code).then(() => { + setCopied(true) + setTimeout(() => setCopied(false), 1500) + }) + } + + return ( +
+
+ + {language} + +
+ {recorded ? ( + + {recorded} + + ) : null} + +
+
+
+        {code}
+      
+
+ ) +} diff --git a/docs/src/components/demo/components/NodeBadge.tsx b/docs/src/components/demo/components/NodeBadge.tsx new file mode 100644 index 000000000..6d95a92a3 --- /dev/null +++ b/docs/src/components/demo/components/NodeBadge.tsx @@ -0,0 +1,20 @@ +/** + * Amber has exactly one job on this page: mark a panel that needs a + * reachable OctoBot node. Never used decoratively elsewhere. + */ +export function NodeBadge({ reachable }: { reachable?: boolean }) { + if (reachable === true) { + return ( + + + node reachable + + ) + } + return ( + + + needs a running node + + ) +} diff --git a/docs/src/components/demo/components/Section.tsx b/docs/src/components/demo/components/Section.tsx new file mode 100644 index 000000000..0d0a53853 --- /dev/null +++ b/docs/src/components/demo/components/Section.tsx @@ -0,0 +1,50 @@ +import type { ReactNode } from "react" + +/** + * The only section wrapper on the page. `weight` controls the visual + * emphasis directly — stations are deliberately unequal (the derivation + * hero is full-bleed, the propose panel is narrow) rather than the equal- + * card-row template a 6-item feature list defaults to. + */ +export function Section({ + id, + eyebrow, + title, + weight = "normal", + children, +}: { + id: string + eyebrow: string + title: string + weight?: "hero" | "normal" | "compact" + children: ReactNode +}) { + return ( +
+
+ + {eyebrow} + +
+

+ {title} +

+ {children} +
+ ) +} diff --git a/docs/src/components/demo/components/SecureContextWarning.tsx b/docs/src/components/demo/components/SecureContextWarning.tsx new file mode 100644 index 000000000..3e4ddd70a --- /dev/null +++ b/docs/src/components/demo/components/SecureContextWarning.tsx @@ -0,0 +1,29 @@ +import { useEffect, useState } from "react" +import { isWebCryptoAvailable } from "../lib/secureContext" + +// The docs site is HTTPS, so a normal visit satisfies this. It matters for +// two real cases this component now exists to catch: a reader running the +// site locally over plain `http://192.168.x.x` (LAN, e.g. to test the QR-scan +// flow from a phone), and any future non-HTTPS deployment of the docs build. +// `isWebCryptoAvailable()` reads `window`, so this must only ever evaluate to +// `true` after mount — SSR/prerender has no `window` and must not flag a +// false warning. +export function SecureContextWarning() { + const [unavailable, setUnavailable] = useState(false) + + useEffect(() => { + setUnavailable(!isWebCryptoAvailable()) + }, []) + + if (!unavailable) return null + + return ( +
+

+ This page isn't a secure context (no `crypto.subtle`) — the SDK below + will throw on every derivation. Load it over HTTPS or from + `localhost`; a plain-HTTP LAN address won't work. +

+
+ ) +} diff --git a/docs/src/components/demo/lib/hex.ts b/docs/src/components/demo/lib/hex.ts new file mode 100644 index 000000000..24aa8b66a --- /dev/null +++ b/docs/src/components/demo/lib/hex.ts @@ -0,0 +1,15 @@ +// The SDK's own hex<->bytes helpers live in its internal tier and aren't +// part of the public surface (`identity/`'s exports take/return hex strings +// already for everything a consumer needs). This is the same 3-line +// conversion, kept local rather than reaching into the SDK's internals. +export function hexToBytes(hex: string): Uint8Array { + const clean = hex.startsWith("0x") ? hex.slice(2) : hex + const bytes = new Uint8Array(clean.length / 2) + for (let i = 0; i < bytes.length; i++) + bytes[i] = Number.parseInt(clean.slice(i * 2, i * 2 + 2), 16) + return bytes +} + +export function bytesToHex(bytes: Uint8Array): string { + return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("") +} diff --git a/docs/src/components/demo/lib/nodeUrlContext.tsx b/docs/src/components/demo/lib/nodeUrlContext.tsx new file mode 100644 index 000000000..be78028ee --- /dev/null +++ b/docs/src/components/demo/lib/nodeUrlContext.tsx @@ -0,0 +1,45 @@ +import { createContext, type ReactNode, useContext, useState } from "react" + +const STORAGE_KEY = "octobot-client-demo:node-url" + +type NodeUrlContextValue = { + url: string + setUrl: (url: string) => void +} + +// One node URL, shared by every panel that needs to reach a real node +// (connect/queue, mint a read-only pairing) — entering it once in either +// place fills in the other, same as the shared wallet key. Persisted to +// localStorage: unlike the wallet key, a node's address isn't a secret, +// and retyping a tailnet hostname every reload is real friction. +const NodeUrlContext = createContext(null) + +export function NodeUrlProvider({ children }: { children: ReactNode }) { + const [url, setUrlState] = useState(() => { + try { + return localStorage.getItem(STORAGE_KEY) ?? "" + } catch { + return "" + } + }) + const setUrl = (next: string) => { + setUrlState(next) + try { + localStorage.setItem(STORAGE_KEY, next) + } catch { + // Private browsing / storage disabled — the demo still works, it just + // won't remember the url across a reload. + } + } + return ( + + {children} + + ) +} + +export function useNodeUrl(): NodeUrlContextValue { + const ctx = useContext(NodeUrlContext) + if (!ctx) throw new Error("useNodeUrl must be used within a NodeUrlProvider") + return ctx +} diff --git a/docs/src/components/demo/lib/placeholderAccount.ts b/docs/src/components/demo/lib/placeholderAccount.ts new file mode 100644 index 000000000..8b00aa07e --- /dev/null +++ b/docs/src/components/demo/lib/placeholderAccount.ts @@ -0,0 +1,32 @@ +export const PLACEHOLDER_ACCOUNT_ID = "demo-account" + +/** Whether `automations.create()` genuinely used the placeholder accountId + * because this node really has zero accounts — not because + * `accounts.list()` never ran, or ran and failed. A failed list (e.g. this + * wallet isn't authorized on this node at all) also leaves `accounts` null, + * which used to look identical to "a real node with zero accounts" here — + * the regression this guards against. `accountId` alone already implies + * "no accounts to pick from"; this only adds "and that's because the list + * call actually succeeded", not because it never ran or failed. */ +export function usedGenuinePlaceholder(opts: { + accountId: string + listSucceeded: boolean +}): boolean { + return opts.accountId === PLACEHOLDER_ACCOUNT_ID && opts.listSucceeded +} + +/** Whether the "expected: the node validated the queued action and + * correctly rejected the placeholder" copy is honest to show. Even when the + * placeholder really was used, `automations.create()` can still fail for a + * reason that has nothing to do with the placeholder — most notably an + * `unauthorized` 403 from a key that was never authorized on this node at + * all, which is exactly the bug that shipped this copy under the wrong + * error. Only `action_failed` (`OctoBotActionError` — the node genuinely + * validated the queued action and rejected it) makes the "expected" framing + * true. */ +export function isExpectedPlaceholderRejection(opts: { + usedPlaceholder: boolean + errorCode: string | undefined +}): boolean { + return opts.usedPlaceholder && opts.errorCode === "action_failed" +} diff --git a/docs/src/components/demo/lib/randomKey.ts b/docs/src/components/demo/lib/randomKey.ts new file mode 100644 index 000000000..6de55ccc1 --- /dev/null +++ b/docs/src/components/demo/lib/randomKey.ts @@ -0,0 +1,11 @@ +import { normalizeEvmPrivateKey } from "@drakkar.software/octobot-client/identity" +import { bytesToHex } from "./hex" + +/** A fresh, local-only secp256k1 private key for demo panels — never derived + * from a mnemonic, never sent anywhere. Every panel that needs a throwaway + * wallet generates one of these instead of a BIP39 phrase. */ +export function generateRandomPrivateKey(): string { + const bytes = new Uint8Array(32) + crypto.getRandomValues(bytes) + return normalizeEvmPrivateKey(bytesToHex(bytes)) +} diff --git a/docs/src/components/demo/lib/secureContext.ts b/docs/src/components/demo/lib/secureContext.ts new file mode 100644 index 000000000..a581f3205 --- /dev/null +++ b/docs/src/components/demo/lib/secureContext.ts @@ -0,0 +1,13 @@ +// The SDK needs `crypto.subtle`, which the platform only exposes in a +// secure context — `localhost` counts, an arbitrary LAN IP does not. This +// bites hardest exactly when this demo is most useful: `vite --host` on a +// LAN IP to test the QR-scan flow from a phone. Same check +// node_web_interface uses (`src/lib/secure-context.ts`), for the same +// reason. +export function isWebCryptoAvailable(): boolean { + return ( + typeof window !== "undefined" && + window.isSecureContext === true && + !!window.crypto?.subtle + ) +} diff --git a/docs/src/components/demo/lib/walletKeyContext.tsx b/docs/src/components/demo/lib/walletKeyContext.tsx new file mode 100644 index 000000000..1fd1d5078 --- /dev/null +++ b/docs/src/components/demo/lib/walletKeyContext.tsx @@ -0,0 +1,64 @@ +import { createContext, type ReactNode, useContext, useState } from "react" +import { generateRandomPrivateKey } from "./randomKey" +import { + persistPrivateKey, + resolveInitialPrivateKey, + safeLocalStorage, +} from "./walletKeyStorage" + +type WalletKeyContextValue = { + privateKey: string + setPrivateKey: (key: string) => void + regenerate: () => void +} + +// One private key, shared by every section that represents "your wallet" +// (derive, queue/connect, propose) — so loading your own key in one place is +// reflected everywhere else that touches a real node, instead of each +// section silently holding its own. +// +// Persisted to localStorage, same as the node URL right next to it. This +// used to regenerate on every page load, which silently swapped wallets +// underneath anyone who had authorized a key on their node — the very next +// call after a reload would 403 with no indication the identity had +// changed. This is a throwaway demo key, not a real wallet, so keeping it in +// localStorage is an acceptable trust model here — `regenerate()` is the +// explicit escape hatch for anyone who wants a fresh identity instead. +// +// The load/persist logic lives in `walletKeyStorage.ts`, tested there — kept +// out of this file so the regression it guards against (see +// `resolveInitialPrivateKey`'s doc comment) has a unit test that doesn't +// need a React renderer. +// +// The website-pairing simulation's "phone" wallet is deliberately NOT this +// context — that's a second, independent device by design. +const WalletKeyContext = createContext(null) + +export function WalletKeyProvider({ children }: { children: ReactNode }) { + const [privateKey, setPrivateKeyState] = useState(() => { + const storage = safeLocalStorage() + return storage + ? resolveInitialPrivateKey(storage) + : generateRandomPrivateKey() + }) + const setPrivateKey = (key: string) => { + setPrivateKeyState(key) + const storage = safeLocalStorage() + if (storage) persistPrivateKey(storage, key) + } + const regenerate = () => setPrivateKey(generateRandomPrivateKey()) + return ( + + {children} + + ) +} + +export function useWalletKey(): WalletKeyContextValue { + const ctx = useContext(WalletKeyContext) + if (!ctx) + throw new Error("useWalletKey must be used within a WalletKeyProvider") + return ctx +} diff --git a/docs/src/components/demo/lib/walletKeyStorage.ts b/docs/src/components/demo/lib/walletKeyStorage.ts new file mode 100644 index 000000000..f09d7d4b9 --- /dev/null +++ b/docs/src/components/demo/lib/walletKeyStorage.ts @@ -0,0 +1,54 @@ +import { generateRandomPrivateKey } from "./randomKey" + +export const WALLET_KEY_STORAGE_KEY = "octobot-client-demo:wallet-key" + +/** Storage is narrowed to just the methods used, so a test can pass a plain + * object instead of a real `Storage` / mocked `localStorage`. */ +type Reader = Pick +type Writer = Pick + +/** Reads a previously persisted key, or `null` if there isn't one (never + * written yet, or storage is unavailable/throws — private browsing, quota, + * disabled storage, …). Never throws. */ +export function loadStoredPrivateKey(storage: Reader): string | null { + try { + return storage.getItem(WALLET_KEY_STORAGE_KEY) || null + } catch { + return null + } +} + +/** Best-effort persist — swallows a storage failure rather than crashing the + * demo over an unavailable localStorage. */ +export function persistPrivateKey(storage: Writer, key: string): void { + try { + storage.setItem(WALLET_KEY_STORAGE_KEY, key) + } catch { + // Private browsing / storage disabled — the demo still works, it just + // won't remember the key across a reload. + } +} + +/** The wallet key's actual startup contract: reuse whatever was persisted + * last time, generate fresh only if nothing was. This is the exact + * regression this module exists to prevent — the demo used to always + * generate a brand-new key on every page load + * (`useState(() => generateRandomPrivateKey())`, no read), which silently + * swapped wallets out from under anyone who had authorized their key on a + * real node and then reloaded the page. */ +export function resolveInitialPrivateKey(storage: Reader): string { + return loadStoredPrivateKey(storage) ?? generateRandomPrivateKey() +} + +/** Referencing the `localStorage` global itself — not just calling a method + * on it — can throw in some sandboxed/policy-restricted contexts. Callers + * must go through this rather than passing `localStorage` directly, or that + * throw happens outside `loadStoredPrivateKey`/`persistPrivateKey`'s own + * try/catch. */ +export function safeLocalStorage(): Storage | null { + try { + return localStorage + } catch { + return null + } +} diff --git a/docs/src/components/demo/sections/DerivationHero.tsx b/docs/src/components/demo/sections/DerivationHero.tsx new file mode 100644 index 000000000..439e8a9d3 --- /dev/null +++ b/docs/src/components/demo/sections/DerivationHero.tsx @@ -0,0 +1,440 @@ +import { + deriveBip44PrivateKey, + deriveEvmAddress, + deriveRoot, + isEvmPrivateKey, + normalizeEvmPrivateKey, + registerDerivationScheme, +} from "@drakkar.software/octobot-client/identity" +import { useCallback, useEffect, useRef, useState } from "react" +import { CodeBlock } from "../components/CodeBlock" +import { Section } from "../components/Section" +import { bytesToHex, hexToBytes } from "../lib/hex" +import { useWalletKey } from "../lib/walletKeyContext" + +type Rung = { + key: string + label: string + detail: string + status: "pending" | "running" | "done" + value?: string + ms?: number +} + +const INITIAL_RUNGS: Rung[] = [ + { + key: "privkey", + label: "Private key", + detail: + "0x-prefixed, 64 hex chars, secp256k1 scalar in [1, n-1] — used as-is, nothing to derive", + status: "pending", + }, + { + key: "address", + label: "EIP-55 address", + detail: "secp256k1 pubkey → keccak256 → checksum", + status: "pending", + }, + { + key: "root", + label: "Starfish root identity", + detail: + "EIP-191 sign('octobot:sync-bootstrap') → HKDF-expand → Ed25519 + X25519", + status: "pending", + }, + { + key: "userid", + label: "userId", + detail: "hex(sha256(rootEdPub)).slice(0, 32)", + status: "pending", + }, +] + +// Registered once, locally, purely so the wrong-scheme toggle has something +// real to compare against. This scheme does NOT ship with the SDK — 'bip44' +// is the only built-in. A raw private key has nothing for a scheme to derive +// (bip44 passes it through unchanged), so the only way a second scheme can +// produce a genuinely different identity from the SAME key is to transform +// the key material itself — this hashes it. That keeps it a legitimate +// DerivationScheme, just a hypothetical one, exactly the shape +// `registerDerivationScheme` exists for: a consumer adding support for a +// wallet type the SDK doesn't ship. +let hypotheticalRegistered = false +function ensureHypotheticalScheme() { + if (hypotheticalRegistered) return + try { + registerDerivationScheme({ + id: "demo-hypothetical", + derive: async (privateKeyHex) => { + const normalized = normalizeEvmPrivateKey(privateKeyHex) + const digest = await crypto.subtle.digest( + "SHA-256", + hexToBytes(normalized) as Uint8Array, + ) + return normalizeEvmPrivateKey(bytesToHex(new Uint8Array(digest))) + }, + }) + } catch { + // Vite HMR re-executes this module (resetting the flag above) without + // resetting the SDK's own registry, which lives in a different, + // non-hot-reloaded module — "already registered" then just means a + // prior hot-reload already did this, which is fine. + } + hypotheticalRegistered = true +} + +async function timed( + fn: () => Promise | T, +): Promise<{ value: T; ms: number }> { + const start = performance.now() + const value = await fn() + return { value, ms: performance.now() - start } +} + +export function DerivationHero() { + const { privateKey, setPrivateKey, regenerate } = useWalletKey() + const [customKeyOpen, setCustomKeyOpen] = useState(false) + const [customKeyDraft, setCustomKeyDraft] = useState("") + const [customKeyError, setCustomKeyError] = useState(null) + const [rungs, setRungs] = useState(INITIAL_RUNGS) + const [fullHash, setFullHash] = useState(null) + const [userId, setUserId] = useState(null) + const [requestCount, setRequestCount] = useState(0) + const [compareUserId, setCompareUserId] = useState(null) + const [showCompare, setShowCompare] = useState(false) + const [isRunning, setIsRunning] = useState(false) + const runIdRef = useRef(0) + + // useCallback with an empty dep array, not a plain closure: `run` closes + // over nothing reactive (only stable setState functions and the stable + // runIdRef), so this reference never changes across renders. That makes + // including `run` in the mount effect's deps below both correct AND safe + // — no biome-ignore fighting the exhaustive-deps autofixer, which would + // otherwise keep re-adding `run` to a plain closure's deps and reintroduce + // a re-derive-every-render loop. + const run = useCallback(async (key: string) => { + const runId = ++runIdRef.current + setIsRunning(true) + setRungs(INITIAL_RUNGS.map((r) => ({ ...r }))) + setFullHash(null) + setUserId(null) + setCompareUserId(null) + + // Prove the "no network" claim rather than assert it: count every fetch + // issued while this pipeline runs. It should never move. Two overlapping + // runs (e.g. React StrictMode's double effect-invoke) would otherwise + // each save/restore window.fetch independently and corrupt each other's + // patch — the isRunning guard on the only other caller (the custom-key + // button) prevents a user-triggered overlap, and the `window.fetch === + // patchedFetch` check below means a stale run's cleanup can never stomp + // a newer run's still-active patch or restore a dead closure. + const originalFetch = window.fetch + let count = 0 + const patchedFetch = ((...args: Parameters) => { + count += 1 + setRequestCount(count) + return originalFetch(...args) + }) as typeof fetch + window.fetch = patchedFetch + + try { + const step1 = await timed(() => deriveBip44PrivateKey(key)) + if (runIdRef.current !== runId) return + setRungs((prev) => + prev.map((r) => + r.key === "privkey" + ? { ...r, status: "done", value: step1.value, ms: step1.ms } + : r, + ), + ) + + const step2 = await timed(() => deriveEvmAddress(hexToBytes(step1.value))) + if (runIdRef.current !== runId) return + setRungs((prev) => + prev.map((r) => + r.key === "address" + ? { ...r, status: "done", value: step2.value, ms: step2.ms } + : r, + ), + ) + + const step3 = await timed(() => deriveRoot(key, "bip44")) + if (runIdRef.current !== runId) return + setRungs((prev) => + prev.map((r) => + r.key === "root" + ? { + ...r, + status: "done", + value: step3.value.keys.edPub, + ms: step3.ms, + } + : r, + ), + ) + + const step4 = await timed(async () => { + // TS 5.7+ makes Uint8Array generic over ArrayBufferLike; WebCrypto + // wants the concrete ArrayBuffer variant (same cast the SDK's own + // mnemonic.ts uses for the same reason). + const digest = await crypto.subtle.digest( + "SHA-256", + hexToBytes(step3.value.keys.edPub) as Uint8Array, + ) + return bytesToHex(new Uint8Array(digest)) + }) + if (runIdRef.current !== runId) return + setFullHash(step4.value) + setUserId(step3.value.userId) + setRungs((prev) => + prev.map((r) => + r.key === "userid" + ? { ...r, status: "done", value: step3.value.userId, ms: step4.ms } + : r, + ), + ) + } finally { + // Only restore if nothing else has already changed window.fetch away + // from the patch THIS run installed — a stale run's cleanup must + // never clobber a newer run's active patch or restore a dead closure. + if (window.fetch === patchedFetch) window.fetch = originalFetch + if (runIdRef.current === runId) setIsRunning(false) + } + }, []) + + // Re-derives whenever the shared wallet key changes — including when + // QueuePanel/ProposePanel are reading the SAME key below, this is the one + // place that key can be edited (via "use your own instead"), so this + // effect is what keeps the reduction on screen honest after an edit, not + // just on first mount. + useEffect(() => { + void run(privateKey) + }, [privateKey, run]) + + const onCompare = async () => { + if (!privateKey) return + ensureHypotheticalScheme() + setShowCompare(true) + const root = await deriveRoot(privateKey, "demo-hypothetical") + setCompareUserId(root.userId) + } + + const onUseCustomKey = async () => { + // Guards the only other caller of run() — prevents a user submitting a + // custom key while the initial (or a prior custom) derivation is still + // in flight, which is what would otherwise race two overlapping + // window.fetch patches (see the finally block in run()). + if (isRunning) return + const key = customKeyDraft.trim() + if (!key) return + if (!isEvmPrivateKey(key)) { + setCustomKeyError( + "not a valid private key — expected 0x followed by 64 hex characters", + ) + return + } + const normalized = normalizeEvmPrivateKey(key) + setCustomKeyError(null) + setPrivateKey(normalized) + setCustomKeyOpen(false) + setShowCompare(false) + await run(normalized) + } + + const allDone = rungs.every((r) => r.status === "done") + + return ( +
+

+ Every read this SDK ever makes goes to{" "} + + users/{userId}/… + {" "} + — and userId is not the + wallet address. It's derived, deterministically, from a chain that runs + entirely in this tab. No node involved yet. +

+ +
+ + requests sent while deriving + + + {requestCount} + + + — check devtools' network tab if you don't believe it + +
+ +
+
+ + private key + +
+ + +
+
+

+ {privateKey || "generating…"} +

+

+ a throwaway demo key kept in this browser's localStorage — reload and + it's still the same identity. Never sent anywhere — see the counter + above. Hit "regenerate" for a fresh one. +

+ {customKeyOpen ? ( +
+