From ead63a74c7f5d725c49a0c5ac68ca8a6adb6b794 Mon Sep 17 00:00:00 2001 From: Herklos Date: Fri, 7 Aug 2026 13:40:49 +0200 Subject: [PATCH 1/3] feat(client,sync): add @drakkar.software/octobot-client SDK with read-only + device-code pairing, space-mirror cloud sync MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Squashes this branch's 21 commits into one. A standalone TypeScript SDK extracted for third-party/website integrations: wallet derivation (bip44-only, legacy derivation removed entirely — a derivation-scheme registry replaces the old fixed union so a future wallet type can register its own scheme), read-only node pairing (scan a QR, connect read-only, propose actions back to the owning device for confirmation), and device-code website pairing (a website shows a code, a human types it into the phone, the phone approves and mints a grant) — the last one closed two full rounds of adversarial review (rendezvous trust, replay/freshness, origin validation, clear-and-re-approve recovery), and had two real bugs fixed in manual code review plus two more found writing an end-to-end security/functional test suite (a never-written document's null pull is a JSON string, not a literal; missing null-pull handling entirely). The retired sealed-data-snapshot pairing exchange (superseded by the grant-based space-mirror approach below) has been deleted, not just deprecated. A running Vite demo (`octobot_client_ts_demo`) exercises every piece above against the real shared sync server — no in-memory stand-ins. Iterated several times on honesty/UX: leads with private keys instead of seed phrases, shares one wallet key across derive/connect/propose instead of regenerating it, persists the node URL and auto-connects when one exists, explains the expected placeholder-account failure, and docs were rebuilt around the demo's own "here's what's real, here's what's fixtures" structure. This session added a further section proving the paired website's grant is read-only: `attemptDirectMirrorWrite` makes a raw push against the mirror space using only the cap+device keys a paired site already holds, and the demo shows the server reject it — a paired website cannot unilaterally write, a change has to be requested, approved, and sent by the user's own device. Grant-based pairing writes per-collection mirror nodes into a dedicated Starfish space (replacing the old sealed-snapshot exchange): the mirror engine runs on `starfish-replica`'s pluggable `ReplicaChannel`/space-mirror channel, content lives in the generic `objdoc` collection (a short-lived dedicated `mirrordoc` collection was tried and retired once `objdoc`'s size limit grew to fit), and a three-value visibility ("private"|"shared"| "public") now routes each collection to the right storage tier (private -> E2EE `objdoc`, public -> plaintext world-readable `objpub`) instead of a binary third-party-eligible flag. The Python-side writer (`octobot_sync.mirror.writer`) mirrors this on the node. Moved off a local tarball + pnpm patch for `starfish-replica`/ `starfish-spaces` (workarounds for two alpha.69 bugs — `createNode`'s invalid self-minted cap, `readSpaces`' stale-while-revalidate race) onto the published alpha.70 registry versions once both fixes shipped upstream. This session additionally: fixed `PairingRequestSession` to carry its own `rendezvous` (was re-spread by hand at every call site) as prep for lifting the pairing exchange into `starfish-spaces` as a generic primitive; bumped every `starfish-*` dependency to the published `3.0.0a71`/`3.0.0-alpha.71` (protocol/keyring/identities/sharing/spaces/sdk); and repointed `octobot_sync.mirror.writer` off a vendored, stale, buggy fork of octospaces' Python `dk-spaces-sdk` (predated `starfish-spaces`, byte-identical storage paths, two real crash bugs, never even wired into the build) onto the real `starfish-spaces` package directly, deleting the vendored copy (`packages/dkspaces/`) — see that commit's own detailed rationale for the full investigation. Also adds a required `requesterKind: 'website' | 'device'` field to `PairingRequestPayload` — the device-code exchange was website-shaped but never actually website-specific, and mobile2 now uses it as a second requester role (a phone pairing as a read-only viewer of another phone's cloud mirror). `createPairingRequest`/`startPairingRequest` default it to `'website'`; `parsePairingRequest` rejects a payload that omits it — no absent-means-website fallback. Docs/wire-contract/changelog updated. node_web_interface Settings page: the "Node type" section is hidden (JSX commented out, not deleted — only "standalone" is actually selectable today) and Cloud sync moves from its own dedicated card into a small inline section under Node configuration's "OctoBot interface" block (`CloudSyncCard.tsx` now exports `CloudSyncSection`, unwrapped from `Card`). That section's own copy was further simplified: the enabled-state "X of Y collections" summary box is gone in favor of a bare Configure button (the count added nothing the modal itself doesn't already show), and the intro line now says plainly that cloud sync is required for website pairing to read anything, without the now-redundant "does not affect syncing to your own node-connected clients" clause. client: 684 tests, demo: 16 tests, packages/sync: pytest green. All build clean. Squashes: 7c6687ab1, 746eb27d9, 21d7208dd, 756cf85a2, 26b48c825, dcb55be2c, 5e3efb7a7, 3255fc46b, e9afecc6b, 2a6b6c656, fdf2e098b, 2381c3cb5, ef8db0eb2, 265d3a28d, 878a5c16f, 30af238e8, 624f8f78c, 72feaaf16, 0180fabdf, 01d2e6bf3, 6727a184a. Full pre-squash history preserved at backup/feature-add-sdk-pre-squash-20260807. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Fwj6oBJXJvwSdnEoNqJ3YG --- .github/workflows/main.yml | 18 + .gitignore | 1 + docs/content/client-sdk/_category_.json | 1 + docs/content/client-sdk/accounts.md | 70 + .../content/client-sdk/advanced-primitives.md | 69 + docs/content/client-sdk/automations.md | 72 + .../client-sdk/collections-and-encryption.md | 49 + docs/content/client-sdk/errors.md | 80 + docs/content/client-sdk/escape-hatches.md | 63 + docs/content/client-sdk/getting-started.md | 115 + docs/content/client-sdk/identity.md | 124 + docs/content/client-sdk/node-rest-api.md | 44 + docs/content/client-sdk/read-only-pairing.md | 116 + docs/content/client-sdk/strategies.md | 63 + docs/content/client-sdk/user-actions.md | 88 + docs/content/client-sdk/website-pairing.md | 287 ++ docs/content/client-sdk/wire-contract.md | 99 + docs/docusaurus.config.ts | 31 + docs/package-lock.json | 2966 +++++++++++++++-- docs/package.json | 19 +- docs/sidebars.ts | 6 + docs/src/components/demo/Embed.tsx | 79 + .../components/demo/components/ByteMeter.tsx | 64 + .../components/demo/components/CodeBlock.tsx | 57 + .../components/demo/components/NodeBadge.tsx | 20 + .../components/demo/components/Section.tsx | 50 + .../demo/components/SecureContextWarning.tsx | 29 + docs/src/components/demo/lib/hex.ts | 15 + .../components/demo/lib/nodeUrlContext.tsx | 45 + .../components/demo/lib/placeholderAccount.ts | 32 + docs/src/components/demo/lib/randomKey.ts | 11 + docs/src/components/demo/lib/secureContext.ts | 13 + .../components/demo/lib/walletKeyContext.tsx | 64 + .../components/demo/lib/walletKeyStorage.ts | 54 + .../demo/sections/DerivationHero.tsx | 440 +++ .../demo/sections/ErrorTaxonomy.tsx | 322 ++ .../demo/sections/EscapeHatches.tsx | 155 + .../src/components/demo/sections/Overview.tsx | 111 + .../components/demo/sections/ProposePanel.tsx | 343 ++ .../components/demo/sections/QueuePanel.tsx | 419 +++ .../demo/sections/WebsitePairingSim.tsx | 912 +++++ docs/src/css/demo.css | 112 + docs/src/pages/demo.tsx | 107 + docs/tests/placeholderAccount.test.ts | 90 + docs/tests/walletKeyStorage.test.ts | 100 + packages/client/octobot_client_ts/.gitignore | 3 + .../client/octobot_client_ts/CHANGELOG.md | 59 + packages/client/octobot_client_ts/CLAUDE.md | 36 + packages/client/octobot_client_ts/README.md | 140 + .../octobot_client_ts/package-lock.json | 1705 ++++++++++ .../client/octobot_client_ts/package.json | 52 + .../src/client/adapters/accounts.ts | 280 ++ .../src/client/adapters/actionHandle.ts | 67 + .../src/client/adapters/automations.ts | 108 + .../src/client/adapters/documents.ts | 129 + .../src/client/adapters/nodeApi.ts | 116 + .../src/client/adapters/settings.ts | 62 + .../src/client/adapters/strategies.ts | 95 + .../src/client/connect/connect.ts | 154 + .../src/client/connect/readOnly.ts | 267 ++ .../src/client/core/errors.ts | 176 + .../src/client/core/options.ts | 42 + .../src/client/core/session.ts | 154 + .../src/client/core/views.ts | 84 + .../octobot_client_ts/src/client/index.ts | 66 + .../src/client/mirror/collections.ts | 295 ++ .../src/client/mirror/index.ts | 35 + .../src/client/mirror/writer.ts | 99 + .../src/client/pairing/mirrorGrant.ts | 124 + .../src/client/pairing/mirrorReader.ts | 152 + .../client/pairing/pairingGrantExchange.ts | 270 ++ .../src/client/pairing/pairingRequest.ts | 177 + .../octobot_client_ts/src/client/strategy.ts | 64 + .../src/collections/index.ts | 2 + .../src/collections/nodeCollections.ts | 58 + .../src/collections/paths.ts | 17 + .../src/crypto/collectionKeys.ts | 58 + .../octobot_client_ts/src/crypto/hkdf.ts | 19 + .../octobot_client_ts/src/crypto/index.ts | 10 + .../src/crypto/secretEncryptor.ts | 102 + .../src/crypto/wireConstants.ts | 19 + .../octobot_client_ts/src/identity/address.ts | 19 + .../src/identity/bip39-wordlist.ts | 2050 ++++++++++++ .../src/identity/capProvider.ts | 108 + .../src/identity/derivationSchemes.ts | 49 + .../octobot_client_ts/src/identity/evm.ts | 93 + .../octobot_client_ts/src/identity/index.ts | 29 + .../octobot_client_ts/src/identity/keys.ts | 51 + .../src/identity/mnemonic.ts | 97 + .../octobot_client_ts/src/identity/pairing.ts | 128 + .../src/identity/pairingRequest.ts | 293 ++ .../octobot_client_ts/src/identity/secret.ts | 62 + .../client/octobot_client_ts/src/index.ts | 129 + .../octobot_client_ts/src/internal/bytes.ts | 41 + .../octobot_client_ts/src/node-api/dexPair.ts | 62 + .../octobot_client_ts/src/node-api/dsl.ts | 13 + .../src/node-api/exchanges.ts | 85 + .../octobot_client_ts/src/node-api/index.ts | 53 + .../src/node-api/marketMaking.ts | 84 + .../src/node-api/octobots.ts | 24 + .../src/node-api/orderBookPreview.ts | 17 + .../octobot_client_ts/src/node-api/pairing.ts | 82 + .../octobot_client_ts/src/node-api/pairs.ts | 94 + .../src/node-api/requiredFunds.ts | 17 + .../octobot_client_ts/src/node-api/setup.ts | 29 + .../octobot_client_ts/src/node-api/wallet.ts | 114 + .../octobot_client_ts/src/protocol/actions.ts | 171 + .../src/protocol/documents.ts | 32 + .../octobot_client_ts/src/protocol/index.ts | 80 + .../orchestration/createAutomation.ts | 106 + .../octobot_client_ts/src/protocol/poll.ts | 3 + .../src/protocol/proposal.ts | 64 + .../octobot_client_ts/src/protocol/state.ts | 372 +++ .../src/protocol/strategy/build.ts | 97 + .../src/protocol/strategy/builders.ts | 634 ++++ .../src/protocol/strategy/index.ts | 48 + .../src/protocol/strategy/kinds.ts | 6 + .../src/protocol/strategy/patch.ts | 332 ++ .../octobot_client_ts/src/protocol/symbols.ts | 10 + .../src/transport/constants.ts | 10 + .../src/transport/documents.ts | 61 + .../octobot_client_ts/src/transport/index.ts | 22 + .../octobot_client_ts/src/transport/probe.ts | 152 + .../src/transport/rendezvous.ts | 155 + .../octobot_client_ts/src/transport/rest.ts | 83 + .../src/transport/syncClient.ts | 38 + .../octobot_client_ts/src/transport/urls.ts | 128 + .../octobot_client_ts/tests/accounts.test.ts | 141 + .../tests/appendAndActionHandle.test.ts | 196 ++ .../octobot_client_ts/tests/auth.test.ts | 94 + .../tests/automationStatus.test.ts | 44 + .../tests/automationsUpdate.test.ts | 67 + .../tests/collectionKeys.test.ts | 76 + .../tests/connectReadOnly.test.ts | 45 + .../tests/connectVerify.test.ts | 96 + .../tests/createAutomationSequencing.test.ts | 149 + .../tests/derivationSchemes.test.ts | 31 + .../octobot_client_ts/tests/detect.test.ts | 306 ++ .../octobot_client_ts/tests/dexPair.test.ts | 113 + .../tests/errorTaxonomy.test.ts | 80 + .../octobot_client_ts/tests/evm.test.ts | 84 + .../octobot_client_ts/tests/facade.test.ts | 67 + .../tests/helpers/fakeNode.smoke.test.ts | 86 + .../tests/helpers/fakeNode.ts | 287 ++ .../tests/identityGolden.test.ts | 64 + .../octobot_client_ts/tests/layering.test.ts | 62 + .../tests/mirrorCollections.test.ts | 349 ++ .../tests/mirrorGrantJoinRequest.test.ts | 56 + .../octobot_client_ts/tests/mnemonic.test.ts | 231 ++ .../octobot_client_ts/tests/noCache.test.ts | 59 + .../tests/nodeApiSecurity.test.ts | 172 + .../tests/nodeAuthRequest.test.ts | 109 + .../tests/nodePairing.test.ts | 130 + .../tests/nodeWallet.test.ts | 103 + .../octobot_client_ts/tests/pairing.test.ts | 124 + .../tests/pairingGrantExchange.test.ts | 247 ++ .../tests/pairingPayloadSecrecy.test.ts | 54 + .../tests/pairingRequest.test.ts | 260 ++ .../tests/pairingRequestFlow.test.ts | 153 + .../tests/pairingRequestLookup.test.ts | 178 + .../octobot_client_ts/tests/proposal.test.ts | 45 + .../octobot_client_ts/tests/protocol.test.ts | 1250 +++++++ .../octobot_client_ts/tests/readOnly.test.ts | 187 ++ .../tests/readOnlyPairingPayload.test.ts | 113 + .../tests/readOnlySecurity.test.ts | 88 + .../tests/rendezvous.test.ts | 237 ++ .../tests/scannedCode.test.ts | 140 + .../tests/secretEncryptor.test.ts | 108 + .../octobot_client_ts/tests/session.test.ts | 119 + .../tests/settledContract.test.ts | 72 + .../tests/tradedPairs.test.ts | 64 + .../tests/wireContract.test.ts | 203 ++ .../client/octobot_client_ts/tsconfig.json | 20 + .../services/octobot_services/constants.py | 22 + packages/sync/octobot_sync/mirror/__init__.py | 0 .../sync/octobot_sync/mirror/collections.py | 150 + .../octobot_sync/mirror/node_collections.py | 95 + packages/sync/octobot_sync/mirror/plan.py | 60 + packages/sync/octobot_sync/mirror/writer.py | 344 ++ packages/sync/requirements.txt | 13 +- .../sync/tests/mirror/test_collections.py | 167 + .../tests/mirror/test_node_collections.py | 98 + packages/sync/tests/mirror/test_plan.py | 95 + .../mirror/test_writer_pull_hash_or_none.py | 105 + .../node_api_interface/api/routes/nodes.py | 21 +- .../tests/test_routes_nodes.py | 110 + .../src/components/Settings/CloudSyncCard.tsx | 267 ++ .../Settings/NodeConfigurationCard.tsx | 18 +- .../__tests__/cloud-sync-collections.test.ts | 70 + .../src/lib/cloud-sync-collections.ts | 76 + .../node_api_service/node_api.py | 58 + .../node_api_service/tests/test_cloud_sync.py | 137 + 192 files changed, 28279 insertions(+), 334 deletions(-) create mode 100644 docs/content/client-sdk/_category_.json create mode 100644 docs/content/client-sdk/accounts.md create mode 100644 docs/content/client-sdk/advanced-primitives.md create mode 100644 docs/content/client-sdk/automations.md create mode 100644 docs/content/client-sdk/collections-and-encryption.md create mode 100644 docs/content/client-sdk/errors.md create mode 100644 docs/content/client-sdk/escape-hatches.md create mode 100644 docs/content/client-sdk/getting-started.md create mode 100644 docs/content/client-sdk/identity.md create mode 100644 docs/content/client-sdk/node-rest-api.md create mode 100644 docs/content/client-sdk/read-only-pairing.md create mode 100644 docs/content/client-sdk/strategies.md create mode 100644 docs/content/client-sdk/user-actions.md create mode 100644 docs/content/client-sdk/website-pairing.md create mode 100644 docs/content/client-sdk/wire-contract.md create mode 100644 docs/src/components/demo/Embed.tsx create mode 100644 docs/src/components/demo/components/ByteMeter.tsx create mode 100644 docs/src/components/demo/components/CodeBlock.tsx create mode 100644 docs/src/components/demo/components/NodeBadge.tsx create mode 100644 docs/src/components/demo/components/Section.tsx create mode 100644 docs/src/components/demo/components/SecureContextWarning.tsx create mode 100644 docs/src/components/demo/lib/hex.ts create mode 100644 docs/src/components/demo/lib/nodeUrlContext.tsx create mode 100644 docs/src/components/demo/lib/placeholderAccount.ts create mode 100644 docs/src/components/demo/lib/randomKey.ts create mode 100644 docs/src/components/demo/lib/secureContext.ts create mode 100644 docs/src/components/demo/lib/walletKeyContext.tsx create mode 100644 docs/src/components/demo/lib/walletKeyStorage.ts create mode 100644 docs/src/components/demo/sections/DerivationHero.tsx create mode 100644 docs/src/components/demo/sections/ErrorTaxonomy.tsx create mode 100644 docs/src/components/demo/sections/EscapeHatches.tsx create mode 100644 docs/src/components/demo/sections/Overview.tsx create mode 100644 docs/src/components/demo/sections/ProposePanel.tsx create mode 100644 docs/src/components/demo/sections/QueuePanel.tsx create mode 100644 docs/src/components/demo/sections/WebsitePairingSim.tsx create mode 100644 docs/src/css/demo.css create mode 100644 docs/src/pages/demo.tsx create mode 100644 docs/tests/placeholderAccount.test.ts create mode 100644 docs/tests/walletKeyStorage.test.ts create mode 100644 packages/client/octobot_client_ts/.gitignore create mode 100644 packages/client/octobot_client_ts/CHANGELOG.md create mode 100644 packages/client/octobot_client_ts/CLAUDE.md create mode 100644 packages/client/octobot_client_ts/README.md create mode 100644 packages/client/octobot_client_ts/package-lock.json create mode 100644 packages/client/octobot_client_ts/package.json create mode 100644 packages/client/octobot_client_ts/src/client/adapters/accounts.ts create mode 100644 packages/client/octobot_client_ts/src/client/adapters/actionHandle.ts create mode 100644 packages/client/octobot_client_ts/src/client/adapters/automations.ts create mode 100644 packages/client/octobot_client_ts/src/client/adapters/documents.ts create mode 100644 packages/client/octobot_client_ts/src/client/adapters/nodeApi.ts create mode 100644 packages/client/octobot_client_ts/src/client/adapters/settings.ts create mode 100644 packages/client/octobot_client_ts/src/client/adapters/strategies.ts create mode 100644 packages/client/octobot_client_ts/src/client/connect/connect.ts create mode 100644 packages/client/octobot_client_ts/src/client/connect/readOnly.ts create mode 100644 packages/client/octobot_client_ts/src/client/core/errors.ts create mode 100644 packages/client/octobot_client_ts/src/client/core/options.ts create mode 100644 packages/client/octobot_client_ts/src/client/core/session.ts create mode 100644 packages/client/octobot_client_ts/src/client/core/views.ts create mode 100644 packages/client/octobot_client_ts/src/client/index.ts create mode 100644 packages/client/octobot_client_ts/src/client/mirror/collections.ts create mode 100644 packages/client/octobot_client_ts/src/client/mirror/index.ts create mode 100644 packages/client/octobot_client_ts/src/client/mirror/writer.ts create mode 100644 packages/client/octobot_client_ts/src/client/pairing/mirrorGrant.ts create mode 100644 packages/client/octobot_client_ts/src/client/pairing/mirrorReader.ts create mode 100644 packages/client/octobot_client_ts/src/client/pairing/pairingGrantExchange.ts create mode 100644 packages/client/octobot_client_ts/src/client/pairing/pairingRequest.ts create mode 100644 packages/client/octobot_client_ts/src/client/strategy.ts create mode 100644 packages/client/octobot_client_ts/src/collections/index.ts create mode 100644 packages/client/octobot_client_ts/src/collections/nodeCollections.ts create mode 100644 packages/client/octobot_client_ts/src/collections/paths.ts create mode 100644 packages/client/octobot_client_ts/src/crypto/collectionKeys.ts create mode 100644 packages/client/octobot_client_ts/src/crypto/hkdf.ts create mode 100644 packages/client/octobot_client_ts/src/crypto/index.ts create mode 100644 packages/client/octobot_client_ts/src/crypto/secretEncryptor.ts create mode 100644 packages/client/octobot_client_ts/src/crypto/wireConstants.ts create mode 100644 packages/client/octobot_client_ts/src/identity/address.ts create mode 100644 packages/client/octobot_client_ts/src/identity/bip39-wordlist.ts create mode 100644 packages/client/octobot_client_ts/src/identity/capProvider.ts create mode 100644 packages/client/octobot_client_ts/src/identity/derivationSchemes.ts create mode 100644 packages/client/octobot_client_ts/src/identity/evm.ts create mode 100644 packages/client/octobot_client_ts/src/identity/index.ts create mode 100644 packages/client/octobot_client_ts/src/identity/keys.ts create mode 100644 packages/client/octobot_client_ts/src/identity/mnemonic.ts create mode 100644 packages/client/octobot_client_ts/src/identity/pairing.ts create mode 100644 packages/client/octobot_client_ts/src/identity/pairingRequest.ts create mode 100644 packages/client/octobot_client_ts/src/identity/secret.ts create mode 100644 packages/client/octobot_client_ts/src/index.ts create mode 100644 packages/client/octobot_client_ts/src/internal/bytes.ts create mode 100644 packages/client/octobot_client_ts/src/node-api/dexPair.ts create mode 100644 packages/client/octobot_client_ts/src/node-api/dsl.ts create mode 100644 packages/client/octobot_client_ts/src/node-api/exchanges.ts create mode 100644 packages/client/octobot_client_ts/src/node-api/index.ts create mode 100644 packages/client/octobot_client_ts/src/node-api/marketMaking.ts create mode 100644 packages/client/octobot_client_ts/src/node-api/octobots.ts create mode 100644 packages/client/octobot_client_ts/src/node-api/orderBookPreview.ts create mode 100644 packages/client/octobot_client_ts/src/node-api/pairing.ts create mode 100644 packages/client/octobot_client_ts/src/node-api/pairs.ts create mode 100644 packages/client/octobot_client_ts/src/node-api/requiredFunds.ts create mode 100644 packages/client/octobot_client_ts/src/node-api/setup.ts create mode 100644 packages/client/octobot_client_ts/src/node-api/wallet.ts create mode 100644 packages/client/octobot_client_ts/src/protocol/actions.ts create mode 100644 packages/client/octobot_client_ts/src/protocol/documents.ts create mode 100644 packages/client/octobot_client_ts/src/protocol/index.ts create mode 100644 packages/client/octobot_client_ts/src/protocol/orchestration/createAutomation.ts create mode 100644 packages/client/octobot_client_ts/src/protocol/poll.ts create mode 100644 packages/client/octobot_client_ts/src/protocol/proposal.ts create mode 100644 packages/client/octobot_client_ts/src/protocol/state.ts create mode 100644 packages/client/octobot_client_ts/src/protocol/strategy/build.ts create mode 100644 packages/client/octobot_client_ts/src/protocol/strategy/builders.ts create mode 100644 packages/client/octobot_client_ts/src/protocol/strategy/index.ts create mode 100644 packages/client/octobot_client_ts/src/protocol/strategy/kinds.ts create mode 100644 packages/client/octobot_client_ts/src/protocol/strategy/patch.ts create mode 100644 packages/client/octobot_client_ts/src/protocol/symbols.ts create mode 100644 packages/client/octobot_client_ts/src/transport/constants.ts create mode 100644 packages/client/octobot_client_ts/src/transport/documents.ts create mode 100644 packages/client/octobot_client_ts/src/transport/index.ts create mode 100644 packages/client/octobot_client_ts/src/transport/probe.ts create mode 100644 packages/client/octobot_client_ts/src/transport/rendezvous.ts create mode 100644 packages/client/octobot_client_ts/src/transport/rest.ts create mode 100644 packages/client/octobot_client_ts/src/transport/syncClient.ts create mode 100644 packages/client/octobot_client_ts/src/transport/urls.ts create mode 100644 packages/client/octobot_client_ts/tests/accounts.test.ts create mode 100644 packages/client/octobot_client_ts/tests/appendAndActionHandle.test.ts create mode 100644 packages/client/octobot_client_ts/tests/auth.test.ts create mode 100644 packages/client/octobot_client_ts/tests/automationStatus.test.ts create mode 100644 packages/client/octobot_client_ts/tests/automationsUpdate.test.ts create mode 100644 packages/client/octobot_client_ts/tests/collectionKeys.test.ts create mode 100644 packages/client/octobot_client_ts/tests/connectReadOnly.test.ts create mode 100644 packages/client/octobot_client_ts/tests/connectVerify.test.ts create mode 100644 packages/client/octobot_client_ts/tests/createAutomationSequencing.test.ts create mode 100644 packages/client/octobot_client_ts/tests/derivationSchemes.test.ts create mode 100644 packages/client/octobot_client_ts/tests/detect.test.ts create mode 100644 packages/client/octobot_client_ts/tests/dexPair.test.ts create mode 100644 packages/client/octobot_client_ts/tests/errorTaxonomy.test.ts create mode 100644 packages/client/octobot_client_ts/tests/evm.test.ts create mode 100644 packages/client/octobot_client_ts/tests/facade.test.ts create mode 100644 packages/client/octobot_client_ts/tests/helpers/fakeNode.smoke.test.ts create mode 100644 packages/client/octobot_client_ts/tests/helpers/fakeNode.ts create mode 100644 packages/client/octobot_client_ts/tests/identityGolden.test.ts create mode 100644 packages/client/octobot_client_ts/tests/layering.test.ts create mode 100644 packages/client/octobot_client_ts/tests/mirrorCollections.test.ts create mode 100644 packages/client/octobot_client_ts/tests/mirrorGrantJoinRequest.test.ts create mode 100644 packages/client/octobot_client_ts/tests/mnemonic.test.ts create mode 100644 packages/client/octobot_client_ts/tests/noCache.test.ts create mode 100644 packages/client/octobot_client_ts/tests/nodeApiSecurity.test.ts create mode 100644 packages/client/octobot_client_ts/tests/nodeAuthRequest.test.ts create mode 100644 packages/client/octobot_client_ts/tests/nodePairing.test.ts create mode 100644 packages/client/octobot_client_ts/tests/nodeWallet.test.ts create mode 100644 packages/client/octobot_client_ts/tests/pairing.test.ts create mode 100644 packages/client/octobot_client_ts/tests/pairingGrantExchange.test.ts create mode 100644 packages/client/octobot_client_ts/tests/pairingPayloadSecrecy.test.ts create mode 100644 packages/client/octobot_client_ts/tests/pairingRequest.test.ts create mode 100644 packages/client/octobot_client_ts/tests/pairingRequestFlow.test.ts create mode 100644 packages/client/octobot_client_ts/tests/pairingRequestLookup.test.ts create mode 100644 packages/client/octobot_client_ts/tests/proposal.test.ts create mode 100644 packages/client/octobot_client_ts/tests/protocol.test.ts create mode 100644 packages/client/octobot_client_ts/tests/readOnly.test.ts create mode 100644 packages/client/octobot_client_ts/tests/readOnlyPairingPayload.test.ts create mode 100644 packages/client/octobot_client_ts/tests/readOnlySecurity.test.ts create mode 100644 packages/client/octobot_client_ts/tests/rendezvous.test.ts create mode 100644 packages/client/octobot_client_ts/tests/scannedCode.test.ts create mode 100644 packages/client/octobot_client_ts/tests/secretEncryptor.test.ts create mode 100644 packages/client/octobot_client_ts/tests/session.test.ts create mode 100644 packages/client/octobot_client_ts/tests/settledContract.test.ts create mode 100644 packages/client/octobot_client_ts/tests/tradedPairs.test.ts create mode 100644 packages/client/octobot_client_ts/tests/wireContract.test.ts create mode 100644 packages/client/octobot_client_ts/tsconfig.json create mode 100644 packages/sync/octobot_sync/mirror/__init__.py create mode 100644 packages/sync/octobot_sync/mirror/collections.py create mode 100644 packages/sync/octobot_sync/mirror/node_collections.py create mode 100644 packages/sync/octobot_sync/mirror/plan.py create mode 100644 packages/sync/octobot_sync/mirror/writer.py create mode 100644 packages/sync/tests/mirror/test_collections.py create mode 100644 packages/sync/tests/mirror/test_node_collections.py create mode 100644 packages/sync/tests/mirror/test_plan.py create mode 100644 packages/sync/tests/mirror/test_writer_pull_hash_or_none.py create mode 100644 packages/tentacles/Services/Interfaces/node_web_interface/src/components/Settings/CloudSyncCard.tsx create mode 100644 packages/tentacles/Services/Interfaces/node_web_interface/src/lib/__tests__/cloud-sync-collections.test.ts create mode 100644 packages/tentacles/Services/Interfaces/node_web_interface/src/lib/cloud-sync-collections.ts create mode 100644 packages/tentacles/Services/Services_bases/node_api_service/tests/test_cloud_sync.py 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..807b3f091 --- /dev/null +++ b/docs/content/client-sdk/website-pairing.md @@ -0,0 +1,287 @@ +--- +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 space-member grant, not a data snapshot.** The website never receives a node credential, and +it never receives a one-time export either. Approving a request invites the website's ephemeral device +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 — as a +`space:member`. That membership is what the website actually reads through: every poll is a live pull +against the mirror, decrypted client-side with the space's own keyring, 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 real `space:member` cap on the wallet's **shared** mirror space — never the private one, +and never `user-accounts-auth` (exchange credentials), which the mirror never writes to any space at +all, at any layer. 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. + +**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 ? ( +
+