diff --git a/.changeset/calm-peers-pair.md b/.changeset/calm-peers-pair.md new file mode 100644 index 000000000..6f9fdcdc2 --- /dev/null +++ b/.changeset/calm-peers-pair.md @@ -0,0 +1,6 @@ +--- +"@ckb-ccc/libp2p": patch +--- + +feat(libp2p): add peer pairing and JSON-RPC services and transports for Khie +connections. diff --git a/.changeset/fresh-khie-owners.md b/.changeset/fresh-khie-owners.md new file mode 100644 index 000000000..014a0410e --- /dev/null +++ b/.changeset/fresh-khie-owners.md @@ -0,0 +1,10 @@ +--- +"@ckb-ccc/connector": patch +"@ckb-ccc/connector-react": major +--- + +refactor(connector): emit owned wallet and signer connections, remove deprecated compatibility APIs, require setClient callers to transfer Client ownership, and release connections from the React Provider. + +See the [Connector 2.0 migration guide](https://docs.ckbccc.com/en/docs/migration/connector-v2) +for the required `setClient`, connection event, Client ownership, and removed +property changes. diff --git a/.changeset/gentle-signals-connect.md b/.changeset/gentle-signals-connect.md new file mode 100644 index 000000000..a069f2c50 --- /dev/null +++ b/.changeset/gentle-signals-connect.md @@ -0,0 +1,6 @@ +--- +"@ckb-ccc/core": minor +--- + +feat(core): add `SignerJsonRpc` and `buildSignerJsonRpcHandler` for remote +signers, plus an `AbortSignal.any` compatibility helper. diff --git a/.changeset/small-names-pair.md b/.changeset/small-names-pair.md new file mode 100644 index 000000000..6b59d5912 --- /dev/null +++ b/.changeset/small-names-pair.md @@ -0,0 +1,6 @@ +--- +"@ckb-ccc/connector": minor +--- + +feat(connector): add Khie peer-to-peer wallet connections, supporting pairing +codes from either the wallet or the connector. diff --git a/assets/khie.svg b/assets/khie.svg new file mode 100644 index 000000000..43f3fc137 --- /dev/null +++ b/assets/khie.svg @@ -0,0 +1,64 @@ + + + + + + + + + + + diff --git a/assets/khieMono.svg b/assets/khieMono.svg new file mode 100644 index 000000000..9f29b32a3 --- /dev/null +++ b/assets/khieMono.svg @@ -0,0 +1,46 @@ + + + + + + + + + + diff --git a/packages/app/next.config.mjs b/packages/app/next.config.mjs index b7a4a183a..f96f0876e 100644 --- a/packages/app/next.config.mjs +++ b/packages/app/next.config.mjs @@ -1,6 +1,6 @@ /** @type {import('next').NextConfig} */ const nextConfig = { - allowedDevOrigins: ["192.168.*.*"], + allowedDevOrigins: ["192.168.*.*", "*.trycloudflare.com"], experimental: { // Work around Next.js 16.3's incompatibility with TypeScript 7's dual-version aliases. // https://github.com/vercel/next.js/issues/96589 diff --git a/packages/app/package.json b/packages/app/package.json index 8cca40129..10bf47372 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -10,14 +10,17 @@ "url": "git://github.com/ckb-devrel/ccc.git" }, "scripts": { - "dev": "next dev --webpack", + "dev": "next dev", "build": "next build", "start": "next start", "lint": "eslint ./src", "format": "prettier --write ./src" }, "dependencies": { + "@chainsafe/libp2p-noise": "^17.0.0", + "@chainsafe/libp2p-yamux": "^8.0.1", "@ckb-ccc/connector-react": "workspace:*", + "@ckb-ccc/libp2p": "workspace:*", "@ckb-ccc/lumos-patches": "workspace:*", "@ckb-ccc/ssri": "workspace:*", "@ckb-ccc/type-id": "workspace:*", @@ -25,11 +28,19 @@ "@ckb-lumos/common-scripts": "0.24.0-next.2", "@ckb-lumos/config-manager": "0.24.0-next.2", "@ckb-lumos/helpers": "0.24.0-next.2", + "@libp2p/circuit-relay-v2": "^4.2.11", + "@libp2p/identify": "^4.1.12", + "@libp2p/interface": "^3.2.5", + "@libp2p/webrtc": "^6.0.29", + "@libp2p/websockets": "^10.1.19", + "@multiformats/multiaddr": "^13.0.3", "@scure/bip32": "^2.0.0", "@scure/bip39": "^2.0.0", + "core-js": "^3.50.0", + "libp2p": "^3.3.8", "lucide-react": "^1.21.0", "next": "16.3.4", - "qrcode.react": "^4.2.0", + "qr": "^0.6.0", "react": "19.2.8", "react-dom": "19.2.8" }, diff --git a/packages/app/src/app/module-workspace.tsx b/packages/app/src/app/module-workspace.tsx index 39fa2ec4a..24579e661 100644 --- a/packages/app/src/app/module-workspace.tsx +++ b/packages/app/src/app/module-workspace.tsx @@ -13,13 +13,17 @@ export const ModuleWorkspace = memo(function ModuleWorkspace({ client, log, module, + setClient, signer, + wallet, }: { active: boolean; client: ccc.Client; log: DemoLogger; module?: DemoModule; + setClient: (owner: ccc.Owner) => unknown; signer?: ccc.Signer; + wallet?: ccc.Wallet; }) { const Module = module?.component; const slotRef = useRef(null); @@ -56,7 +60,9 @@ export const ModuleWorkspace = memo(function ModuleWorkspace({ client={client} log={log} module={module} + setClient={setClient} signer={signer} + wallet={wallet} workspaceRef={workspaceRef} /> @@ -69,13 +75,17 @@ function MountedModuleWorkspace({ client, log, module, + setClient, signer, + wallet, workspaceRef, }: { client: ccc.Client; log: DemoLogger; module: DemoModule; + setClient: (owner: ccc.Owner) => unknown; signer?: ccc.Signer; + wallet?: ccc.Wallet; workspaceRef: React.RefObject; }) { const Module = module.component; @@ -140,7 +150,9 @@ function MountedModuleWorkspace({
@@ -179,9 +191,11 @@ function MountedModuleWorkspace({ void; + setClient: (owner: ccc.Owner) => unknown; show: ShowModuleReadout; signer?: ccc.Signer; + wallet?: ccc.Wallet; submitTransaction: SubmitTransaction; }; @@ -116,6 +120,15 @@ function defineModules(definitions: readonly DemoModuleDefinition[]) { } export const demoModules: readonly DemoModule[] = defineModules([ + { + name: "Khie", + description: + "Khie is a peer-to-peer protocol that connects wallets and applications. To connect, select Khie in the application's connector, then either use the connector to scan the wallet's pairing code or use the wallet to scan the connector's pairing code. For wallets without native Khie support, this module lets them connect to remote applications through Khie.\n\nKhie (契, /kʰje/) is Hakka for bond, agreement, or connection.", + group: "Wallet", + icon: KhieIcon, + access: "signer", + component: KhieModule, + }, { name: "Hash", description: diff --git a/packages/app/src/app/modules/khie/khie-client-module.module.css b/packages/app/src/app/modules/khie/khie-client-module.module.css new file mode 100644 index 000000000..ca656c44b --- /dev/null +++ b/packages/app/src/app/modules/khie/khie-client-module.module.css @@ -0,0 +1,1106 @@ +.input-action-control { + display: grid; + min-width: 0; + grid-template-columns: minmax(0, 1fr) auto; + gap: 8px; +} + +.endpoint-control { + overflow: hidden; + border: 1px solid var(--line-dark); + grid-template-columns: minmax(0, 1fr) auto; + gap: 0; + background: rgb(7 10 12 / 72%); + transition: border-color 160ms ease; +} + +.endpoint-control:focus-within { + border-color: var(--signal); +} + +.endpoint-control input { + height: 44px; + border: 0; + background: transparent; +} + +.endpoint-control .inline-action { + height: 44px; +} + +.endpoint-control .inline-action button { + height: 44px; + border: 0; + border-left: 1px solid var(--line-dark); +} + +.endpoint-control .inline-action .connect-button { + width: 44px; + min-width: 44px; + padding: 0; + border: 0; + color: var(--muted); + background: transparent; +} + +.endpoint-control .inline-action .connect-button:not(:disabled), +.endpoint-control .inline-action .connect-button:not(:disabled):hover, +.endpoint-control .inline-action .connect-button:not(:disabled):focus-visible { + color: var(--signal); + background: transparent; +} + +.endpoint-control .inline-action .connect-button:disabled { + color: var(--line-dark); + opacity: 1; +} + +.relay-settings-content { + width: 100%; + min-width: 0; + grid-row: 2; + grid-column: 1 / -1; + justify-self: stretch; + animation: relay-settings-enter 240ms var(--easing-mechanical) both; +} + +.relay-settings-content :global(.module-actions) { + animation: none !important; +} + +.relay-controls { + display: flex; + z-index: 1; + width: 100%; + align-self: end; + align-items: center; + justify-content: space-between; + justify-self: stretch; + grid-row: 1; + grid-column: 3; + gap: 12px; +} + +.relay-settings-toggle { + display: flex; + align-items: center; + gap: 6px; + padding: 0; + border: 0; + background: transparent; + cursor: pointer; + font: + 700 10px/1 "Courier New", + monospace; + letter-spacing: 0.1em; + list-style: none; + text-transform: uppercase; +} + +.relay-settings-toggle { + margin-left: auto; + color: var(--muted); +} + +.settings-chevron { + color: var(--line-dark); + transition: transform 160ms ease; +} + +.relay-settings-toggle[aria-expanded="true"] .settings-chevron { + transform: rotate(180deg); +} + +@keyframes relay-settings-enter { + from { + opacity: 0; + transform: translateY(-6px); + } +} + +.paired-panel { + width: 100%; + gap: 12px; +} + +.request-title { + display: flex; + min-width: 0; + margin: 0; + align-items: center; + gap: 8px; + color: var(--signal); + font: + 700 15px/1.4 "Courier New", + monospace; + overflow-wrap: anywhere; +} + +.request-queue { + flex: 0 0 auto; + margin-left: auto; + color: var(--muted); + font-size: 9px; + letter-spacing: 0.08em; + line-height: 1.2; + text-transform: uppercase; +} + +.session-action:global(.module-actions) { + display: flex; + width: fit-content; + flex: 0 0 auto; + grid-template-columns: none; + justify-self: end; +} + +.session-action button { + min-height: 34px; + padding-inline: 12px; +} + +.request-area { + display: grid; + width: 100%; + min-width: 0; + padding-top: 14px; + border-top: 1px solid var(--line); + gap: 12px; +} + +.request-idle { + margin: 0; + padding: 14px 0; + color: var(--muted); + font: + 11px/1.5 "Courier New", + monospace; + text-align: center; +} + +.request-card { + display: grid; + min-width: 0; + gap: 12px; +} + +.request-description { + max-height: 112px; + overflow: auto; + margin: 0; + color: var(--ink); + font: + 12px/1.55 "Courier New", + monospace; + overflow-wrap: anywhere; + white-space: pre-wrap; +} + +.transaction-details { + display: grid; + max-height: min(52vh, 430px); + min-width: 0; + padding-right: 4px; + overflow: auto; + gap: 14px; + scrollbar-color: var(--line-dark) transparent; + scrollbar-width: thin; +} + +.transaction-summary { + display: grid; + min-width: 0; + align-items: center; + grid-template-columns: minmax(0, 1fr) auto; + gap: 12px; +} + +.transaction-hash { + overflow: hidden; + min-width: 0; + color: inherit; + font: + 10px/1.4 "Courier New", + monospace; + text-overflow: ellipsis; + white-space: nowrap; + transition: color 160ms ease; +} + +.transaction-hash-copy, +.transaction-hash-copy:hover, +.transaction-hash-copy:focus-visible { + display: grid; + overflow: hidden; + min-width: 0; + min-height: 0; + padding: 0; + border: 0; + outline: none; + color: var(--muted); + background: transparent; + grid-template-columns: minmax(0, 1fr) auto; + gap: 6px; + text-align: left; + text-transform: none; + transition: color 160ms ease; +} + +.transaction-hash-copy:hover, +.transaction-hash-copy:focus-visible, +.transaction-hash-copy[data-copied="true"] { + color: var(--signal); +} + +.transaction-hash-copy :global(.copyable-text-icon) { + display: grid; + color: inherit; + place-items: center; + transition: color 160ms ease; +} + +.transaction-fee { + color: var(--ink); + font: + 9px/1.4 "Courier New", + monospace; + text-align: right; + white-space: nowrap; +} + +.transaction-cell-group, +.transaction-cell-list { + display: grid; + min-width: 0; +} + +.transaction-cell-group { + gap: 7px; +} + +.transaction-cell-list { + gap: 6px; +} + +.transaction-cell-heading { + display: flex; + align-items: center; + justify-content: space-between; + color: var(--muted); + font: + 700 10px/1.3 "Courier New", + monospace; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.transaction-cell-heading span { + color: var(--line-dark); +} + +.transaction-cell { + --capacity-share: 0%; + + position: relative; + overflow: hidden; + min-width: 0; + border: 1px solid var(--line); + background: transparent; +} + +.transaction-cell summary::before { + position: absolute; + z-index: 0; + width: var(--capacity-share); + inset: 0 auto 0 0; + background: rgb(91 206 250 / 11%); + content: ""; + pointer-events: none; +} + +.transaction-cell summary { + position: relative; + z-index: 1; + display: grid; + min-width: 0; + padding: 10px 11px; + align-items: center; + cursor: pointer; + grid-template-columns: minmax(0, 1fr) auto; + gap: 10px; + list-style: none; +} + +.transaction-cell summary > * { + position: relative; + z-index: 1; +} + +.transaction-cell summary::-webkit-details-marker { + display: none; +} + +.transaction-cell summary > svg { + color: var(--muted); + transition: transform 160ms ease; +} + +.transaction-cell[open] summary > svg { + transform: rotate(180deg); +} + +.transaction-cell-summary { + display: grid; + min-width: 0; + align-items: center; + grid-template-columns: minmax(0, 1fr) auto; + gap: 12px; +} + +.transaction-cell-identity { + display: grid; + overflow: hidden; + min-width: 0; + gap: 6px; +} + +.transaction-cell-summary small { + overflow: hidden; + min-width: 0; + color: var(--ink); + font: + 700 9px/1.2 "Courier New", + monospace; + letter-spacing: 0.06em; + text-overflow: ellipsis; + text-transform: uppercase; + white-space: nowrap; +} + +.transaction-cell-capacity { + min-width: max-content; + align-self: center; + color: var(--ink); + font: + 700 12px/1.25 "Courier New", + monospace; + justify-self: end; + text-align: right; + white-space: nowrap; +} + +.transaction-cell-identity code { + overflow: hidden; + min-width: 0; + color: var(--ink); + font: + 10px/1.35 "Courier New", + monospace; + text-overflow: ellipsis; + white-space: nowrap; +} + +.transaction-cell-expanded { + position: relative; + z-index: 1; + display: grid; + min-width: 0; + padding: 10px 11px 12px; + border-top: 1px solid var(--line); + gap: 9px; + background: rgb(7 10 12 / 72%); +} + +.transaction-script { + display: grid; + min-width: 0; + padding-left: 9px; + border-left: 1px solid var(--line-dark); + gap: 7px; +} + +.transaction-script-heading > span, +.transaction-cell-field > span { + color: var(--muted); + font: + 700 9px/1.25 "Courier New", + monospace; + letter-spacing: 0.06em; + text-transform: uppercase; +} + +.transaction-script-heading { + display: grid; + min-width: 0; + align-items: center; + grid-template-columns: auto minmax(0, 1fr); + gap: 10px; +} + +.transaction-script-address { + display: grid; + overflow: hidden; + min-width: 0; + padding: 0; + border: 0; + align-items: center; + color: var(--muted); + background: transparent; + grid-template-columns: minmax(0, 1fr) auto; + gap: 5px; + text-align: right; + text-transform: none; + transition: color 160ms ease; +} + +.transaction-script-address code { + overflow: hidden; + min-width: 0; + font: + 9px/1.3 "Courier New", + monospace; + text-overflow: ellipsis; + white-space: nowrap; +} + +.transaction-script-address :global(.copyable-text-icon) { + display: grid; + color: inherit; + place-items: center; +} + +.transaction-script-address:hover, +.transaction-script-address:focus-visible, +.transaction-script-address[data-copied="true"] { + color: var(--signal); +} + +.transaction-cell-field { + display: grid; + min-width: 0; + gap: 4px; +} + +.transaction-cell-field code { + overflow: auto hidden; + min-width: 0; + color: var(--ink); + font: + 10px/1.4 "Courier New", + monospace; + white-space: nowrap; +} + +.transaction-cell-field[data-multiline="true"] code { + overflow: visible; + overflow-wrap: anywhere; + white-space: pre-wrap; +} + +.transaction-cell-copy { + display: grid; + width: 100%; + min-width: 0; + padding: 0; + border: 0; + align-items: center; + color: var(--ink); + background: transparent; + grid-template-columns: minmax(0, 1fr) auto; + gap: 6px; + text-align: left; + text-transform: none; + transition: color 160ms ease; +} + +.transaction-cell-copy code { + overflow: hidden; + text-overflow: ellipsis; +} + +.transaction-cell-copy :global(.copyable-text-icon) { + display: grid; + color: var(--muted); + place-items: center; + transition: color 160ms ease; +} + +.transaction-cell-copy:hover, +.transaction-cell-copy:focus-visible, +.transaction-cell-copy[data-copied="true"], +.transaction-cell-copy:hover :global(.copyable-text-icon), +.transaction-cell-copy:focus-visible :global(.copyable-text-icon), +.transaction-cell-copy[data-copied="true"] :global(.copyable-text-icon) { + color: var(--signal); +} + +.transaction-cell-status, +.transaction-cell-unavailable { + margin: 0; + padding: 10px 11px; + border: 1px solid var(--line); + color: var(--muted); + font: + 10px/1.4 "Courier New", + monospace; +} + +.transaction-cell-unavailable { + display: grid; + min-width: 0; + gap: 4px; +} + +.transaction-cell-unavailable strong { + color: var(--ink); +} + +.approval-actions { + flex-wrap: nowrap; +} + +.approval-actions button, +.approval-actions button:global(.is-primary) { + min-width: 96px; +} + +.peer-loading { + margin: 0; + color: var(--muted); + font: + 11px/1.5 "Courier New", + monospace; +} + +.peer-details { + display: grid; + width: 100%; + min-width: 0; + gap: 10px; +} + +.peer-overview { + display: flex; + width: 100%; + min-width: 0; + align-items: center; + justify-content: space-between; + gap: 16px; +} + +.peer-copy { + display: grid; + flex: 1 1 auto; + min-width: 0; + gap: 6px; +} + +.peer-primary { + display: flex; + min-width: 0; + align-items: center; + gap: 12px; +} + +.peer-agent { + overflow: hidden; + min-width: 0; + color: var(--muted); + font: + 10px/1.35 "Courier New", + monospace; + text-overflow: ellipsis; + white-space: nowrap; +} + +.peer-path { + display: inline-flex; + flex: 0 0 auto; + align-items: center; + gap: 6px; + color: var(--muted); + font: + 700 12px/1 "Courier New", + monospace; +} + +.peer-path::before { + width: 5px; + height: 5px; + border-radius: 50%; + background: var(--muted); + content: ""; +} + +.peer-path[data-direct="true"]::before { + background: var(--signal); +} + +.peer-name { + overflow: hidden; + flex: 1 1 auto; + min-width: 0; + color: var(--ink); + font: + 700 14px/1.35 "Courier New", + monospace; + text-overflow: ellipsis; + white-space: nowrap; +} + +.peer-id { + max-width: 150px; +} + +.peer-time > span { + flex: 0 0 auto; + color: var(--muted); + font: + 700 9px/1.2 "Courier New", + monospace; + letter-spacing: 0.06em; + text-transform: uppercase; +} + +.peer-times { + display: flex; + min-width: 0; + margin-top: 4px; + flex-wrap: wrap; + align-items: baseline; + gap: 6px 14px; +} + +.peer-time { + display: flex; + min-width: 0; + align-items: baseline; + gap: 6px; +} + +.peer-time code, +.peer-time time, +.peer-time strong { + overflow: hidden; + color: var(--ink); + font: + 10px/1.4 "Courier New", + monospace; + text-overflow: ellipsis; + white-space: nowrap; +} + +.pairing-overlay { + position: absolute; + z-index: 4; + display: grid; + inset: 0; + place-items: center; +} + +.pairing-dialog:global(.module-actions) { + display: flex; + width: 100%; + min-height: 104px; + padding: 26px 28px; + align-items: center; + justify-content: center; + flex-direction: column; + gap: 20px; + background: var(--paper); +} + +.connecting-message { + margin: 0; + color: var(--signal); + font: + 700 14px/1.4 "Courier New", + monospace; + text-align: center; +} + +.pairing-dialog .connecting-cancel { + min-height: 30px; + padding: 0 12px; + font-size: 10px; +} + +.inline-action { + display: flex; + height: 46px; + flex-wrap: nowrap; +} + +.inline-action button { + height: 46px; + min-width: 142px; + min-height: 0; + white-space: nowrap; +} + +.pairing-group { + grid-row: 1; + grid-column: 1; + align-items: center; + grid-template-columns: minmax(0, 1fr) auto; + column-gap: 12px; + row-gap: 14px; +} + +.pairing-columns { + display: grid; + min-width: 0; + align-items: center; + grid-template-columns: minmax(0, 1fr) 34px minmax(0, 1fr); + gap: 16px; +} + +.pairing-columns .inline-action button { + min-width: 100px; +} + +.remote-column { + align-self: stretch; + grid-row: 1; + grid-column: 3; + grid-template-rows: auto minmax(0, 1fr); +} + +.remote-actions { + display: grid; + align-content: center; + gap: 7px; +} + +.scan-action { + display: grid; + width: 100%; + grid-template-columns: minmax(0, 1fr); +} + +.scan-action .scan-button { + display: grid; + width: 100%; + height: 64px; + max-width: none; + min-height: 64px; + min-width: 0; + padding: 8px 12px; + border-color: var(--signal); + color: var(--recess); + grid-template-columns: 38px minmax(0, 1fr) 18px; + grid-column: 1 / -1; + gap: 11px; + background: var(--signal); + text-align: left; +} + +.scan-action .scan-button:not(:disabled):hover, +.scan-action .scan-button:not(:disabled):focus-visible { + border-color: var(--signal); + color: var(--recess); + background: var(--signal-hover); + transform: translateX(4px); +} + +.scan-icon { + display: grid; + width: 38px; + height: 38px; + border: 1px solid currentColor; + place-items: center; +} + +.scan-copy { + display: flex; + min-width: 0; + align-items: center; +} + +.scan-copy strong { + font-size: 12px; + line-height: 1.2; +} + +.scan-arrow { + justify-self: end; + transition: transform 180ms ease; +} + +.scan-button:hover .scan-arrow, +.scan-button:focus-visible .scan-arrow { + transform: translateX(3px); +} + +.endpoint-pair { + display: grid; + min-width: 0; + justify-items: center; + gap: 12px; +} + +.divider { + position: relative; + display: grid; + align-self: stretch; + width: 34px; + min-height: 100%; + grid-row: 1; + grid-column: 2; + color: var(--muted); + font: + 700 9px/1 "Courier New", + monospace; + letter-spacing: 0.08em; + place-items: center; + text-transform: uppercase; +} + +.divider::before { + position: absolute; + z-index: 0; + top: 0; + bottom: 0; + left: 50%; + width: 1px; + background: var(--line-dark); + content: ""; +} + +.divider span { + z-index: 1; + padding: 8px 4px; + background: var(--panel); +} + +.scanner { + display: grid; + min-width: 0; + gap: 16px; +} + +.scanner-actions:global(.module-actions) { + display: flex; + width: fit-content; + grid-template-columns: none; + justify-content: center; + justify-self: center; +} + +.scanner-actions:global(.module-actions) button { + padding-inline: 18px; +} + +.scanner-video { + display: block; + width: min(100%, 360px); + aspect-ratio: 1; + justify-self: center; + object-fit: cover; + object-position: center; + background: rgb(0 0 0 / 72%); +} + +.endpoint-list { + display: grid; + grid-column: 1 / -1; + gap: 12px; +} + +.endpoint-qr { + --qr-code-size: min(220px, calc(100vw - 96px)); + + justify-self: center; + max-width: 100%; + height: auto; + aspect-ratio: 1; +} + +.endpoint-copy { + display: flex; + width: 100%; + min-width: 0; + padding: 12px; + border: 0; + align-items: center; + justify-content: flex-start; + gap: 12px; + color: var(--ink); + background: transparent; + cursor: pointer; + font: + 12px/1.55 "Courier New", + monospace; + text-align: left; + transition: color 160ms ease; +} + +.endpoint-pending { + display: flex; + width: 100%; + min-height: 46px; + padding: 12px; + align-items: center; + justify-content: center; + color: var(--muted); + font: + 12px/1.55 "Courier New", + monospace; +} + +.endpoint-retry { + display: flex; + width: 100%; + min-height: 46px; + align-items: center; + justify-content: center; +} + +.relay-retry { + display: flex; + padding: 0; + border: 0; + align-items: center; + gap: 6px; + color: var(--signal); + background: transparent; + cursor: pointer; + font: + 700 10px/1 "Courier New", + monospace; + letter-spacing: 0.1em; + text-transform: uppercase; +} + +.relay-retry:disabled { + color: var(--muted); + cursor: default; +} + +.compatibility-help { + display: grid; + padding: 12px 14px; + border: 1px solid var(--line-dark); + gap: 5px; +} + +.compatibility-help strong { + color: var(--ink); + font-size: 13px; +} + +.compatibility-help span { + color: var(--muted); + font-size: 12px; + line-height: 1.5; +} + +.endpoint-copy > span:not(:global(.copyable-text-icon)) { + flex: 1 1 auto; + overflow: hidden; + min-width: 0; + text-overflow: ellipsis; + white-space: nowrap; +} + +.endpoint-copy :global(.copyable-text-icon) { + display: grid; + flex: 0 0 auto; + color: var(--muted); + place-items: center; + transition: color 160ms ease; +} + +.endpoint-copy:hover, +.endpoint-copy:focus-visible, +.endpoint-copy[data-copied="true"] { + color: var(--signal); + outline: none; +} + +.endpoint-copy:hover :global(.copyable-text-icon), +.endpoint-copy:focus-visible :global(.copyable-text-icon), +.endpoint-copy[data-copied="true"] :global(.copyable-text-icon) { + color: var(--signal); +} + +@media (max-width: 680px) { + .endpoint-qr { + --qr-code-size: min(22rem, calc(100% - 1.5rem)); + } + + .pairing-columns { + grid-template-columns: minmax(0, 1fr); + gap: 18px; + } + + .divider { + height: 17px; + width: 100%; + min-height: 1px; + grid-row: 2; + grid-column: 1; + } + + .divider::before { + top: 50%; + right: 0; + bottom: auto; + left: 0; + width: auto; + height: 1px; + transform: none; + } + + .divider span { + padding: 4px 8px; + } + + .relay-settings-content { + grid-row: 5; + grid-column: 1; + } + + .relay-controls { + grid-row: 4; + grid-column: 1; + } + + .pairing-group { + grid-row: 1; + grid-column: 1; + } + + .remote-column { + grid-row: 3; + grid-column: 1; + } + + .input-action-control { + grid-template-columns: minmax(0, 1fr); + } + + .inline-action, + .inline-action button { + width: 100%; + } + + .pairing-columns .input-action-control { + grid-template-columns: minmax(0, 1fr) auto; + } + + .pairing-columns .inline-action { + display: flex; + width: auto; + } + + .pairing-columns .inline-action button { + width: auto; + min-width: 100px; + } + + .approval-actions button { + width: auto; + flex: 1 1 0; + } +} diff --git a/packages/app/src/app/modules/khie/khie-client-module.tsx b/packages/app/src/app/modules/khie/khie-client-module.tsx new file mode 100644 index 000000000..7aa2d6025 --- /dev/null +++ b/packages/app/src/app/modules/khie/khie-client-module.tsx @@ -0,0 +1,1316 @@ +"use client"; + +import { ccc } from "@ckb-ccc/connector-react"; +import { Libp2p } from "@ckb-ccc/libp2p"; +import { ArrowRight, Check, ChevronDown, ScanLine, X } from "lucide-react"; +import { + type CSSProperties, + useCallback, + useEffect, + useEffectEvent, + useRef, + useState, +} from "react"; +import { CopyableText } from "../../copyable-text"; +import type { ModuleRuntimeProps } from "../../modules"; +import { QrCode } from "../../qr-code"; +import { QrScanner } from "../../qr-scanner"; +import styles from "./khie-client-module.module.css"; +import { + DEFAULT_KHIE_RELAY_ADDRESS, + type KhieRemotePeer, + KhieSignerSession, +} from "./khie-signer-session"; +import { displayPeerName } from "./peer-name"; + +type SignerWaiter = { + abort: () => void; + networkId: string; + reject: (cause: Error) => void; + resolve: (signer: ccc.Signer) => void; + signal: AbortSignal; +}; +type ApprovalPrompt = ccc.SignerJsonRpcConfirmation & { + abort: () => void; + resolve: (approved: boolean) => void; + signal: AbortSignal; +}; +type RelayState = "connected" | "connecting" | "failed" | "idle"; + +const PROVIDER_ENDPOINT_URL = "https://app.ckbccc.com/#khie"; +const JSON_RPC_REQUEST_TIMEOUT_MS = 120_000; +const APPROVAL_ENABLE_DELAY_MS = 1_000; + +export function KhieClientModule({ + client, + log, + setClient, + signer, + signerIcon, + signerName, + show, +}: Pick & { + signer?: ccc.Signer; + signerIcon?: string; + signerName?: string; +}) { + const [session, setSession] = useState(); + const [nodeReady, setNodeReady] = useState(false); + const [pairingEndpoint, setPairingEndpoint] = useState(""); + const [paired, setPaired] = useState(false); + const [pairing, setPairing] = useState(false); + const [relayState, setRelayState] = useState("idle"); + const [relayAddress, setRelayAddress] = useState(DEFAULT_KHIE_RELAY_ADDRESS); + const [khieEndpoint, setKhieEndpoint] = useState(""); + const [scanning, setScanning] = useState(false); + const [advancedSettingsOpen, setAdvancedSettingsOpen] = useState(false); + const [approval, setApproval] = useState(); + const [approvalEnabled, setApprovalEnabled] = useState(false); + const [queuedApprovalCount, setQueuedApprovalCount] = useState(0); + const [remotePeer, setRemotePeer] = useState(); + const [incompatiblePeerError, setIncompatiblePeerError] = useState(); + + const signerRef = useRef(signer); + const signerWaiters = useRef(new Set()); + const approvalRef = useRef(undefined); + const approvalEnabledRef = useRef(false); + const approvalQueue = useRef([]); + + const connectingRelay = relayState === "connecting"; + const relayConnected = relayState === "connected"; + const relayConnectionFailed = relayState === "failed"; + + const showCurrent = useEffectEvent(show); + const logCurrent = useEffectEvent(log); + const reportCurrentError = useEffectEvent((cause: unknown) => { + setIncompatiblePeerError( + isIncompatiblePeerError(cause) ? cause.message : undefined, + ); + reportError(cause, show, log); + }); + const getSignerMetadata = useEffectEvent(() => ({ + name: signerName, + icon: signerIcon, + })); + const connectSigner = useEffectEvent( + async (networkId: string, signal: AbortSignal) => { + signal.throwIfAborted(); + const current = signerRef.current; + if ( + current && + networkIdFromAddressPrefix(current.client.addressPrefix) === networkId + ) { + return current; + } + + const clientOwner = clientOwnerForNetworkId(networkId, client); + if (clientOwner) { + setClient(clientOwner); + } + + return new Promise((resolve, reject) => { + const waiter: SignerWaiter = { + abort: () => { + if (!signerWaiters.current.delete(waiter)) { + return; + } + signal.removeEventListener("abort", waiter.abort); + reject(abortReason(signal)); + }, + networkId, + reject, + resolve, + signal, + }; + signerWaiters.current.add(waiter); + signal.addEventListener("abort", waiter.abort, { once: true }); + if (signal.aborted) { + waiter.abort(); + return; + } + resolveSignerWaiters(signerRef.current, signerWaiters.current); + }); + }, + ); + const settleApproval = useCallback( + (prompt: ApprovalPrompt, approved: boolean) => { + if (approvalRef.current === prompt) { + approvalEnabledRef.current = false; + setApprovalEnabled(false); + const next = approvalQueue.current.shift(); + approvalRef.current = next; + setApproval(next); + } else { + const index = approvalQueue.current.indexOf(prompt); + if (index === -1) { + return false; + } + approvalQueue.current.splice(index, 1); + } + setQueuedApprovalCount(approvalQueue.current.length); + + prompt.signal.removeEventListener("abort", prompt.abort); + prompt.resolve(approved); + return true; + }, + [], + ); + const confirmKhieRequest = useEffectEvent( + (request: ccc.SignerJsonRpcConfirmation, signal: AbortSignal) => + new Promise((resolve) => { + const prompt: ApprovalPrompt = { + ...request, + abort: () => void settleApproval(prompt, false), + resolve, + signal, + }; + signal.addEventListener("abort", prompt.abort, { once: true }); + + if (approvalRef.current) { + approvalQueue.current.push(prompt); + setQueuedApprovalCount(approvalQueue.current.length); + } else { + approvalEnabledRef.current = false; + setApprovalEnabled(false); + approvalRef.current = prompt; + setApproval(prompt); + } + + if (signal.aborted) { + prompt.abort(); + } + }), + ); + const rejectPendingApprovals = useEffectEvent(() => { + const prompts = [approvalRef.current, ...approvalQueue.current].filter( + (prompt): prompt is ApprovalPrompt => prompt !== undefined, + ); + approvalRef.current = undefined; + approvalEnabledRef.current = false; + approvalQueue.current = []; + setApproval(undefined); + setApprovalEnabled(false); + setQueuedApprovalCount(0); + prompts.forEach((prompt) => { + prompt.signal.removeEventListener("abort", prompt.abort); + prompt.resolve(false); + }); + }); + const connectDefaultRelay = useEffectEvent( + async (currentSession: KhieSignerSession) => { + setRelayState("connecting"); + if (!(await currentSession.connectRelay(DEFAULT_KHIE_RELAY_ADDRESS))) { + setRelayState("failed"); + return; + } + + setRelayState("connected"); + logCurrent(`Relay connected: ${DEFAULT_KHIE_RELAY_ADDRESS}`, "success"); + }, + ); + + const resolveApproval = (approved: boolean) => { + if (!approvalEnabledRef.current) { + return; + } + approvalEnabledRef.current = false; + setApprovalEnabled(false); + + const current = approvalRef.current; + if (!current) { + return; + } + + if (!settleApproval(current, approved)) { + return; + } + + if (approved) { + const title = formatApprovalTitle(current); + show({ + label: "REQUEST APPROVED", + tone: "success", + content: {title} request approved, + }); + log(`${title} request approved`, "success"); + } + }; + + useEffect(() => { + signerRef.current = signer; + resolveSignerWaiters(signer, signerWaiters.current); + }, [signer]); + + useEffect(() => { + if (!approval) { + return; + } + + const timeout = window.setTimeout(() => { + if (approvalRef.current !== approval) { + return; + } + approvalEnabledRef.current = true; + setApprovalEnabled(true); + }, APPROVAL_ENABLE_DELAY_MS); + return () => window.clearTimeout(timeout); + }, [approval]); + + useEffect( + () => () => { + rejectSignerWaiters( + signerWaiters.current, + new Error("Signer module stopped"), + ); + rejectPendingApprovals(); + }, + [], + ); + + const pairEndpoint = useCallback( + async (endpoint: string) => { + const address = endpoint.trim(); + if (!session || !address) { + return; + } + + setIncompatiblePeerError(undefined); + setPairing(true); + try { + if (await session.pair(address)) { + setKhieEndpoint(""); + } + } finally { + setPairing(false); + } + }, + [session], + ); + + useEffect(() => { + showCurrent({ + label: "STARTING LIBP2P", + tone: "idle", + content: Loading browser transports…, + }); + logCurrent("Starting signer libp2p node"); + + const owner = KhieSignerSession.open({ + endpointUrl: PROVIDER_ENDPOINT_URL, + handler: async (payload) => { + const controller = new AbortController(); + const { signal } = controller; + const reportTimeout = () => { + if (!isTimeoutError(signal.reason)) { + return; + } + + const title = formatRequestTitle(payload.method); + showCurrent({ + label: "REQUEST TIMED OUT", + tone: "error", + content: {title} request timed out, + }); + logCurrent(`${title} request timed out`, "error"); + }; + signal.addEventListener("abort", reportTimeout, { once: true }); + const timeout = setTimeout( + () => controller.abort(requestTimeoutError()), + JSON_RPC_REQUEST_TIMEOUT_MS, + ); + + try { + const handleRequest = ccc.buildSignerJsonRpcHandler({ + connect: (networkId) => connectSigner(networkId, signal), + confirmRequest: (request) => confirmKhieRequest(request, signal), + getSigner: () => signerRef.current, + getSignerMetadata, + }); + const result = await handleRequest(payload); + signal.throwIfAborted(); + const completed = formatRequestCompletion(payload.method); + if (completed) { + showCurrent({ + label: "REQUEST COMPLETED", + tone: "success", + content: {completed}, + }); + logCurrent(completed, "success"); + } + return result; + } catch (cause) { + signal.throwIfAborted(); + throw cause; + } finally { + clearTimeout(timeout); + signal.removeEventListener("abort", reportTimeout); + } + }, + onEndpointChange: setPairingEndpoint, + onError: reportCurrentError, + onPaired: () => { + setIncompatiblePeerError(undefined); + setPaired(true); + setPairing(false); + setScanning(false); + showCurrent({ + label: "CONNECTED", + tone: "success", + content: Khie peer connected, + }); + logCurrent("Khie peer paired", "success"); + }, + onRemotePeerChange: setRemotePeer, + onReady: (session) => { + setNodeReady(true); + showCurrent({ + label: "LIBP2P NODE READY", + tone: "success", + content: Browser libp2p node is ready, + }); + logCurrent("Signer node is ready", "success"); + void connectDefaultRelay(session); + }, + onUnpaired: () => { + setPaired(false); + setRemotePeer(undefined); + rejectSignerWaiters( + signerWaiters.current, + new Error("Khie peer disconnected"), + ); + rejectPendingApprovals(); + showCurrent({ + label: "UNPAIRED", + tone: "idle", + content: Khie peer is no longer paired, + }); + logCurrent("Khie peer unpaired"); + }, + }); + const nextSession = owner.value; + setSession(nextSession); + + return () => { + void owner.dispose(); + }; + }, []); + + const connectRelay = async () => { + const address = relayAddress.trim(); + if (!session || !address) { + return; + } + + setRelayState("connecting"); + if (!(await session.connectRelay(address))) { + setRelayState("failed"); + return; + } + + setRelayState("connected"); + log(`Relay connected: ${address}`, "success"); + show({ + label: "RELAY CONNECTED", + tone: "success", + content: {address}, + }); + }; + + const unpair = () => session?.unpair(); + const showingPairingOverlay = pairing; + const approvalDescription = approval + ? formatApprovalDescription(approval) + : undefined; + + if (paired) { + return ( +
+ +
+ {approval ? ( + <> +

+ {formatApprovalTitle(approval)} + {queuedApprovalCount > 0 ? ( + + +{queuedApprovalCount} queued + + ) : null} +

+
+ {approval.method === "sign_transaction" ? ( + + ) : approvalDescription ? ( +

+ {approvalDescription} +

+ ) : null} +
+ + +
+
+ + ) : ( +

Ready for requests…

+ )} +
+
+ ); + } + + if (scanning && !showingPairingOverlay) { + return ( +
+
+ { + setScanning(false); + reportError(cause, show, log); + }} + onScan={(value) => { + const endpoint = value.trim(); + setKhieEndpoint(endpoint); + setScanning(false); + log("Peer session endpoint scanned", "success"); + void pairEndpoint(endpoint); + }} + /> +
+ +
+
+
+ ); + } + + return ( +
+
+
+
+ Let a connector scan this +
+ {pairingEndpoint && !relayConnectionFailed ? ( +
+ + reportError(cause, show, log)} + > + {pairingEndpoint} + +
+ ) : ( +
+ + {relayConnectionFailed ? ( +
+ +
+ ) : ( + + {connectingRelay + ? "Connecting to relay…" + : "Preparing pairing code…"} + + )} +
+ )} +
+
+ +
+ Scan a connector +
+
+ +
+
+ + setKhieEndpoint(event.currentTarget.value) + } + /> +
+ +
+
+
+
+
+ +
+ {advancedSettingsOpen ? ( +
+ Relay multiaddr +
+ + setRelayAddress(event.currentTarget.value) + } + /> +
+ +
+
+
+ ) : null} +
+ {incompatiblePeerError ? ( +
+ This is not a compatible Khie pairing code + + Open the app you want to use and click something like + "Connect wallet", then choose Khie in the connector. You + can either scan its pairing code with this module, or use the + connector to scan the pairing code shown here. + +
+ ) : null} +
+ {showingPairingOverlay ? ( +
+
+

Pairing with Khie...

+ +
+
+ ) : null} +
+ ); +} + +function RemotePeerDetails({ + onUnpair, + peer, +}: { + onUnpair: () => void; + peer?: KhieRemotePeer; +}) { + const [now, setNow] = useState(peer?.lastRequestAt ?? 0); + + useEffect(() => { + if (peer?.lastRequestAt === undefined) { + return; + } + + const interval = setInterval(() => setNow(Date.now()), 1000); + return () => clearInterval(interval); + }, [peer?.lastRequestAt]); + + const path = + peer?.direct === undefined + ? "Unknown path" + : peer.direct + ? "Direct" + : "Relayed"; + const connectedAt = peer?.connectedAt + ? new Date(peer.connectedAt) + : undefined; + const name = displayPeerName(peer?.name); + + return ( +
+
+ {peer ? ( +
+
+ + {path} + + + {name} + +
+ + {peer.agentVersion ?? "Unknown agent"} + +
+ ) : ( +

Loading remote peer details…

+ )} +
+ +
+
+ + {peer ? ( +
+
+ Peer ID + + {peer.id} + +
+
+ Connected + {connectedAt ? ( + + ) : ( + Not available + )} +
+
+ Last request + + {peer.lastRequestAt === undefined + ? "No requests yet" + : formatElapsedDuration(peer.lastRequestAt, now)} + +
+
+ ) : null} +
+ ); +} + +type TransactionCellView = { + cellOutput?: ccc.CellOutput; + extraCapacity?: ccc.Num; + key: string; + label: string; + outputData?: string; + reference?: string; +}; + +function TransactionApprovalDetails({ + client, + transaction, +}: { + client: ccc.Client; + transaction: ccc.Transaction; +}) { + const [inputResolution, setInputResolution] = useState<{ + cells: TransactionCellView[]; + client: ccc.Client; + transaction: ccc.Transaction; + }>(); + const [feeResolution, setFeeResolution] = useState<{ + client: ccc.Client; + transaction: ccc.Transaction; + value: ccc.Num | null; + }>(); + const inputs = + inputResolution?.client === client && + inputResolution.transaction === transaction + ? inputResolution.cells + : undefined; + const fee = + feeResolution?.client === client && + feeResolution.transaction === transaction + ? feeResolution.value + : undefined; + + useEffect(() => { + let active = true; + + void Promise.all( + transaction.inputs.map(async (input, index) => { + const reference = `${input.previousOutput.txHash}:${input.previousOutput.index}`; + try { + const cell = + input.cellOutput && input.outputData !== undefined + ? ccc.Cell.from({ + cellOutput: input.cellOutput, + outPoint: input.previousOutput, + outputData: input.outputData, + }) + : await client.getCell(input.previousOutput); + let extraCapacity: ccc.Num | undefined; + if (cell) { + try { + extraCapacity = await cell.getDaoProfit(client); + } catch { + // Keep the cell visible; getFee independently reports availability. + } + } + return { + cellOutput: cell?.cellOutput, + extraCapacity, + key: reference, + label: `${index} Input`, + outputData: cell?.outputData, + reference, + }; + } catch { + return { + key: reference, + label: `${index} Input`, + reference, + }; + } + }), + ).then((resolved) => { + if (active) { + setInputResolution({ cells: resolved, client, transaction }); + } + }); + + void transaction + .getFee(client) + .then((value) => { + if (active) { + setFeeResolution({ client, transaction, value }); + } + }) + .catch(() => { + if (active) { + setFeeResolution({ client, transaction, value: null }); + } + }); + + return () => { + active = false; + }; + }, [client, transaction]); + + const outputs = transaction.outputs.map((cellOutput, index) => ({ + cellOutput, + key: `output-${index}`, + label: `${index} Output`, + outputData: transaction.outputsData[index] ?? "0x", + })); + const transactionHash = transaction.hash(); + + return ( +
+
+ + + {transactionHash} + + + + {fee === undefined + ? "Fee …" + : fee === null + ? "Fee unavailable" + : `Fee ${ccc.fixedPointToString(fee)} CKB · ${transactionFeeRate(transaction, fee)} shannons/KB`} + +
+ + +
+ ); +} + +function TransactionCellGroup({ + cells, + client, + empty, + title, +}: { + cells?: TransactionCellView[]; + client: ccc.Client; + empty: string; + title: string; +}) { + const totalCapacity = + cells?.reduce( + (total, cell) => total + transactionCellCapacity(cell), + ccc.Zero, + ) ?? ccc.Zero; + + return ( +
+
+ {title} + {cells?.length ?? "…"} +
+
+ {cells === undefined ? ( +

Loading cells…

+ ) : cells.length === 0 ? ( +

{empty}

+ ) : ( + cells.map((cell) => ( + + )) + )} +
+
+ ); +} + +function TransactionCellItem({ + cell, + client, + totalCapacity, +}: { + cell: TransactionCellView; + client: ccc.Client; + totalCapacity: bigint; +}) { + const { cellOutput } = cell; + const capacity = transactionCellCapacity(cell); + const capacityShare = + cellOutput && totalCapacity > ccc.Zero + ? Number((capacity * ccc.numFrom(1000)) / totalCapacity) / 10 + : 0; + const style = { + "--capacity-share": `${capacityShare}%`, + } as CSSProperties; + + if (!cellOutput) { + return ( +
+ {cell.label} + Cell details unavailable +
+ ); + } + + const lockAddress = ccc.Address.fromScript( + cellOutput.lock, + client, + ).toString(); + + return ( +
+ + + + {cell.label} + {lockAddress} + + + {cell.extraCapacity && cell.extraCapacity > ccc.Zero + ? `${ccc.fixedPointToString(cellOutput.capacity)} + ${ccc.fixedPointToString(cell.extraCapacity)} CKB` + : `${ccc.fixedPointToString(cellOutput.capacity)} CKB`} + + + +
+ {cell.reference ? ( + + ) : null} + + + +
+
+ ); +} + +function TransactionScriptDetails({ + address, + label, + script, +}: { + address?: string; + label: string; + script?: ccc.Script; +}) { + if (!script) { + return ; + } + + return ( +
+
+ {label} + {address ? ( + + {address} + + ) : null} +
+ + + +
+ ); +} + +function TransactionCellField({ + copyable = false, + label, + multiline = false, + value, +}: { + copyable?: boolean; + label: string; + multiline?: boolean; + value: string; +}) { + return ( +
+ {label} + {copyable ? ( + + {value} + + ) : ( + {value} + )} +
+ ); +} + +function transactionCellCapacity(cell: TransactionCellView) { + return ( + (cell.cellOutput?.capacity ?? ccc.Zero) + (cell.extraCapacity ?? ccc.Zero) + ); +} + +function transactionFeeRate(transaction: ccc.Transaction, fee: ccc.Num) { + return ( + (fee * ccc.numFrom(1000)) / ccc.numFrom(transaction.toBytes().length + 4) + ); +} + +function formatElapsedDuration(timestamp: number, now: number) { + const seconds = Math.max(0, Math.floor((now - timestamp) / 1000)); + if (seconds < 60) { + return seconds === 0 + ? "Just now" + : `${seconds} second${seconds === 1 ? "" : "s"} ago`; + } + + const minutes = Math.floor(seconds / 60); + if (minutes < 60) { + return `${minutes} minute${minutes === 1 ? "" : "s"} ago`; + } + + const hours = Math.floor(minutes / 60); + if (hours < 24) { + return `${hours} hour${hours === 1 ? "" : "s"} ago`; + } + + const days = Math.floor(hours / 24); + return `${days} day${days === 1 ? "" : "s"} ago`; +} + +function clientOwnerForNetworkId(networkId: string, current: ccc.Client) { + if (networkIdFromAddressPrefix(current.addressPrefix) === networkId) { + return; + } + + if (networkId === "ckb-mainnet") { + return ccc.ClientPublicMainnet.open(); + } + if (networkId === "ckb-testnet") { + return ccc.ClientPublicTestnet.open(); + } + + throw new ccc.JsonRpcError({ + code: -32001, + message: `Unsupported network ID: ${networkId}`, + }); +} + +function formatApprovalDescription(approval: ccc.SignerJsonRpcConfirmation) { + switch (approval.method) { + case "connect": + return `Switch to network ${approval.networkId}`; + case "sign_message": + return approval.message.value; + case "sign_transaction": + return `Transaction ${approval.transaction.hash()}`; + } +} + +function formatApprovalTitle(approval: ccc.SignerJsonRpcConfirmation) { + return formatRequestTitle(approval.method); +} + +function formatRequestTitle(method: string) { + switch (method) { + case "connect": + return "Connect"; + case "sign_message": + return "Sign Message"; + case "sign_transaction": + return "Sign Transaction"; + default: + return method; + } +} + +function formatRequestCompletion(method: string) { + switch (method) { + case "connect": + return "Signer connected"; + case "sign_message": + return "Message signed"; + case "sign_transaction": + return "Transaction signed"; + } +} + +// TODO: In the next major version, read the network ID directly from Client +// instead of inferring it from addressPrefix. +function networkIdFromAddressPrefix(addressPrefix: string) { + if (addressPrefix === "ckb") { + return "ckb-mainnet"; + } + if (addressPrefix === "ckt") { + return "ckb-testnet"; + } + + throw new ccc.JsonRpcError({ + code: -32001, + message: `Unsupported address prefix: ${addressPrefix}`, + }); +} + +function resolveSignerWaiters( + signer: ccc.Signer | undefined, + waiters: Set, +) { + if (!signer) { + return; + } + + waiters.forEach((waiter) => { + if ( + waiter.networkId !== + networkIdFromAddressPrefix(signer.client.addressPrefix) + ) { + return; + } + + waiters.delete(waiter); + waiter.signal.removeEventListener("abort", waiter.abort); + waiter.resolve(signer); + }); +} + +function rejectSignerWaiters(waiters: Set, cause: Error) { + waiters.forEach((waiter) => { + waiter.signal.removeEventListener("abort", waiter.abort); + waiter.reject(cause); + }); + waiters.clear(); +} + +function abortReason(signal: AbortSignal) { + if (signal.reason instanceof Error) { + return signal.reason; + } + + const error = new Error("JSON-RPC request canceled"); + error.name = "AbortError"; + return error; +} + +function requestTimeoutError() { + const error = new Error("JSON-RPC request timed out"); + error.name = "TimeoutError"; + return error; +} + +function isTimeoutError(cause: unknown): cause is Error { + return cause instanceof Error && cause.name === "TimeoutError"; +} + +function reportError( + cause: unknown, + show: ModuleRuntimeProps["show"], + log: ModuleRuntimeProps["log"], +) { + const message = + cause instanceof Error ? cause.message : "libp2p operation failed"; + + if (isIncompatiblePeerError(cause)) { + show({ + label: "INCOMPATIBLE PAIRING CODE", + tone: "error", + content: Scan a pairing code from a connector, + }); + log(message, "error"); + return; + } + + show({ + label: "LIBP2P FAULT", + tone: "error", + content: {message}, + }); + log(message, "error"); +} + +function isIncompatiblePeerError(cause: unknown): cause is Error { + return ( + cause instanceof Libp2p.PairingEndpointRoleError || + (cause instanceof Error && cause.name === "UnsupportedProtocolError") + ); +} diff --git a/packages/app/src/app/modules/khie/khie-icon.tsx b/packages/app/src/app/modules/khie/khie-icon.tsx new file mode 100644 index 000000000..e3e27ab6d --- /dev/null +++ b/packages/app/src/app/modules/khie/khie-icon.tsx @@ -0,0 +1,22 @@ +import type { LucideProps } from "lucide-react"; +import { forwardRef } from "react"; + +export const KhieIcon = forwardRef( + ({ color = "currentColor", size = 24, ...props }, ref) => ( + + ), +); + +KhieIcon.displayName = "KhieIcon"; diff --git a/packages/app/src/app/modules/khie/khie-module.tsx b/packages/app/src/app/modules/khie/khie-module.tsx new file mode 100644 index 000000000..10786f039 --- /dev/null +++ b/packages/app/src/app/modules/khie/khie-module.tsx @@ -0,0 +1,38 @@ +"use client"; + +import dynamic from "next/dynamic"; +import type { ModuleRuntimeProps } from "../../modules"; + +const KhieClientModule = dynamic( + () => + import("./khie-client-module").then((module) => module.KhieClientModule), + { + ssr: false, + loading: () => Loading browser libp2p module…, + }, +); + +export function KhieModule({ + client, + log, + setClient, + show, + signer, + wallet, +}: ModuleRuntimeProps) { + if (!signer) { + return null; + } + + return ( + + ); +} diff --git a/packages/app/src/app/modules/khie/khie-signer-session.ts b/packages/app/src/app/modules/khie/khie-signer-session.ts new file mode 100644 index 000000000..03138ac1a --- /dev/null +++ b/packages/app/src/app/modules/khie/khie-signer-session.ts @@ -0,0 +1,472 @@ +import { ccc } from "@ckb-ccc/connector-react"; +import { Libp2p } from "@ckb-ccc/libp2p"; +import type { Identify } from "@libp2p/identify"; +import type { + Connection, + IdentifyResult, + Peer, + PeerId, +} from "@libp2p/interface"; +import { multiaddr } from "@multiformats/multiaddr"; + +type BrowserJsonRpcComponents = Libp2p.JsonRpcServiceComponents & { + pairing: Libp2p.PairingService; +}; +type KhieSignerServices = { + identify: Identify; + jsonRpc: Libp2p.JsonRpcService; + pairing: Libp2p.PairingService; +}; +type KhieSignerNode = Awaited>; +type KhieSignerJsonRpcHandler = (payload: ccc.JsonRpcPayload) => unknown; + +class KhieConnectionAuthorizer { + private readonly connections = new Map< + string, + { status: "connected" } | { status: "connecting" } + >(); + + async handle( + peerId: PeerId, + payload: ccc.JsonRpcPayload, + handler: KhieSignerJsonRpcHandler, + ) { + const peer = peerId.toString(); + // SignerJsonRpc reads metadata before it sends the connect request. + if (payload.method === "get_info") { + return handler(payload); + } + + if (payload.method !== "connect") { + if (this.connections.get(peer)?.status !== "connected") { + throw new ccc.JsonRpcError({ + code: -32001, + message: "Connect must be approved before this request", + }); + } + return handler(payload); + } + + const connecting = { status: "connecting" } as const; + this.connections.set(peer, connecting); + try { + const result = await handler(payload); + // A later connect attempt or unpair invalidates this completion. + if (this.connections.get(peer) === connecting) { + this.connections.set(peer, { status: "connected" }); + } + return result; + } finally { + if (this.connections.get(peer) === connecting) { + this.connections.delete(peer); + } + } + } + + unpair(peerId: PeerId) { + this.connections.delete(peerId.toString()); + } +} + +type KhieSignerSessionResources = { + abortController: AbortController; + lastRequest?: { at: number; peerId: PeerId }; + node?: KhieSignerNode; + nodeSubscriptions: Array<() => void>; + pairingController?: AbortController; + pairedPeer?: PeerId; + pairedPeerName?: string; + relayConnection?: Connection; +}; + +export type KhieSignerSessionConfig = { + endpointUrl: string; + handler: KhieSignerJsonRpcHandler; + onEndpointChange?: (endpoint: string) => void; + onError?: (error: Error) => void; + onPaired?: () => void; + onRemotePeerChange?: (peer: KhieRemotePeer) => void; + onReady?: (session: KhieSignerSession) => void; + onUnpaired?: () => void; + pairedPeerTimeoutMs?: number; +}; + +export type KhieRemotePeer = { + agentVersion?: string; + connectedAt?: number; + direct?: boolean; + id: string; + lastRequestAt?: number; + name?: string; +}; + +export const DEFAULT_KHIE_RELAY_ADDRESS = "/dns4/relay.ckbccc.com/tcp/443/wss"; + +const KHIE_PAIRING_PROTOCOL = "/nervos-ckb/khie/pairing/0.0.1"; +const KHIE_JSON_RPC_PROTOCOL = "/nervos-ckb/khie/json-rpc/0.0.1"; +const DEFAULT_PAIRED_PEER_TIMEOUT_MS = 30 * 60 * 1000; + +export class KhieSignerSession { + private readonly resources: KhieSignerSessionResources = { + abortController: new AbortController(), + nodeSubscriptions: [], + }; + private events?: KhieSignerSessionConfig; + private endpointUpdateId = 0; + + private constructor(private readonly config: KhieSignerSessionConfig) { + this.events = config; + } + + static open(config: KhieSignerSessionConfig) { + const session = new KhieSignerSession(config); + void session.start(); + return new ccc.OwnerUnique(session, (session) => session.close()); + } + + async connectRelay(relayAddress: string) { + const address = relayAddress.trim(); + const node = this.resources.node; + if (!node || !address) { + return false; + } + + const previous = this.resources.relayConnection; + this.resources.relayConnection = undefined; + + let connection: Connection | undefined; + try { + await previous?.close(); + connection = await node.dial(multiaddr(address), { + signal: this.resources.abortController.signal, + }); + this.resources.abortController.signal.throwIfAborted(); + + this.resources.relayConnection = connection; + return true; + } catch (cause) { + await connection?.close(); + this.events?.onError?.(asError(cause)); + return false; + } + } + + async pair(endpoint: string) { + const address = endpoint.trim(); + const resources = this.resources; + const node = resources.node; + if ( + !node || + !address || + resources.pairedPeer || + resources.pairingController + ) { + return false; + } + + const pairingController = new AbortController(); + resources.pairingController = pairingController; + const signal = ccc.abortSignalAny([ + resources.abortController.signal, + pairingController.signal, + ]); + + try { + let target: Libp2p.PairingTarget; + try { + target = await Libp2p.decodePairingEndpoint(address, "connector"); + } catch (cause) { + this.events?.onError?.(asError(cause)); + return false; + } + + try { + await node.services.pairing.pair(target, { signal }); + return true; + } catch { + // PairingService reports its own errors through onError. + return false; + } + } finally { + if (resources.pairingController === pairingController) { + resources.pairingController = undefined; + } + } + } + + cancelPairing() { + const controller = this.resources.pairingController; + if (!controller) { + return; + } + + const error = new Error("Pairing canceled"); + error.name = "AbortError"; + controller.abort(error); + } + + async unpair() { + const node = this.resources.node; + const peerId = this.resources.pairedPeer; + if (!node || !peerId) { + return; + } + + await node.services.pairing.unpair(peerId); + } + + private async start() { + const { abortController } = this.resources; + try { + const node = await createKhieSignerNode( + () => !abortController.signal.aborted && !this.resources.pairedPeer, + (peerId) => this.resources.pairedPeer?.equals(peerId) === true, + this.config.handler, + (peerId) => this.recordRequest(peerId), + this.config.pairedPeerTimeoutMs ?? DEFAULT_PAIRED_PEER_TIMEOUT_MS, + abortController.signal, + ); + this.resources.node = node; + this.observeNode(node); + this.events?.onReady?.(this); + } catch (cause) { + this.events?.onError?.(asError(cause)); + } + } + + private observeNode(node: KhieSignerNode) { + const syncEndpoint = () => { + void this.syncEndpoint(node).catch((cause: unknown) => { + this.events?.onError?.(asError(cause)); + }); + }; + const syncIdentifiedPeer = (event: CustomEvent) => { + const peerId = this.resources.pairedPeer; + if (!peerId?.equals(event.detail.peerId)) { + return; + } + + void this.syncRemotePeer(node, peerId, event.detail); + }; + + node.addEventListener("self:peer:update", syncEndpoint); + node.addEventListener("peer:identify", syncIdentifiedPeer); + this.resources.nodeSubscriptions.push( + () => node.removeEventListener("self:peer:update", syncEndpoint), + () => node.removeEventListener("peer:identify", syncIdentifiedPeer), + node.services.pairing.onSecretChanged(syncEndpoint), + node.services.pairing.onError((error) => { + const signal = this.resources.pairingController?.signal; + if (signal?.aborted && error === signal.reason) { + return; + } + + this.events?.onError?.(error); + }), + node.services.jsonRpc.onError((error) => { + this.events?.onError?.(error); + }), + node.services.pairing.onPaired((peerId, name) => { + if (this.resources.pairedPeer) { + return; + } + + this.resources.pairedPeer = peerId; + this.resources.pairedPeerName = name; + this.events?.onPaired?.(); + void this.syncRemotePeer(node, peerId); + }), + node.services.pairing.onUnpaired((peerId) => { + if (!this.resources.pairedPeer?.equals(peerId)) { + return; + } + + this.resources.pairedPeer = undefined; + this.resources.pairedPeerName = undefined; + this.events?.onUnpaired?.(); + }), + ); + syncEndpoint(); + } + + private recordRequest(peerId: PeerId) { + const pairedPeer = this.resources.pairedPeer; + if (pairedPeer && !pairedPeer.equals(peerId)) { + return; + } + + this.resources.lastRequest = { at: Date.now(), peerId }; + const node = this.resources.node; + if (node && pairedPeer) { + void this.syncRemotePeer(node, peerId); + } + } + + private async syncRemotePeer( + node: KhieSignerNode, + peerId: PeerId, + identified?: IdentifyResult, + ) { + let peer: Peer | undefined; + try { + peer = await node.peerStore.get(peerId); + } catch { + // A connection is still useful before identify has populated the peer store. + } + + if (!this.resources.pairedPeer?.equals(peerId)) { + return; + } + + const connection = + identified?.connection ?? + node.getConnections(peerId).find(({ status }) => status === "open") ?? + node.getConnections(peerId)[0]; + const lastRequest = this.resources.lastRequest; + + const metadataText = (key: string) => { + const value = peer?.metadata.get(key); + return value ? ccc.bytesTo(value, "utf8") : undefined; + }; + + this.events?.onRemotePeerChange?.({ + agentVersion: identified?.agentVersion ?? metadataText("AgentVersion"), + connectedAt: connection?.timeline.open, + direct: connection?.direct, + id: peerId.toString(), + lastRequestAt: + lastRequest?.peerId.equals(peerId) === true + ? lastRequest.at + : undefined, + name: this.resources.pairedPeerName, + }); + } + + private async syncEndpoint(node: KhieSignerNode) { + const updateId = ++this.endpointUpdateId; + const addresses = node.getMultiaddrs(); + const endpoint = + addresses.length === 0 + ? "" + : await Libp2p.encodePairingEndpoint( + this.config.endpointUrl, + addresses, + node.services.pairing.secret, + "provider", + ); + if (updateId !== this.endpointUpdateId) { + return; + } + this.events?.onEndpointChange?.(endpoint); + } + + private close() { + this.events = undefined; + return this.releaseResources(); + } + + private async releaseResources() { + const { abortController, node, nodeSubscriptions, relayConnection } = + this.resources; + abortController.abort(); + nodeSubscriptions.splice(0).forEach((unsubscribe) => unsubscribe()); + + try { + if (node && this.resources.pairedPeer) { + await node.services.pairing.unpair(this.resources.pairedPeer); + } + } finally { + try { + await relayConnection?.close(); + } finally { + await node?.stop(); + } + } + } +} + +async function createKhieSignerNode( + canPair: Libp2p.PairingGuard, + isSelectedPeer: (peerId: PeerId) => boolean, + handler: KhieSignerJsonRpcHandler, + onRequest: (peerId: PeerId) => void, + pairedPeerTimeoutMs: number, + signal: AbortSignal, +) { + signal.throwIfAborted(); + const connectionAuthorizer = new KhieConnectionAuthorizer(); + + const [ + { noise }, + { yamux }, + { circuitRelayTransport }, + { identify }, + { webRTC }, + { webSockets }, + { createLibp2p }, + ] = await Promise.all([ + import("@chainsafe/libp2p-noise"), + import("@chainsafe/libp2p-yamux"), + import("@libp2p/circuit-relay-v2"), + import("@libp2p/identify"), + import("@libp2p/webrtc"), + import("@libp2p/websockets"), + import("libp2p"), + ]); + signal.throwIfAborted(); + + let node: + Awaited>> | undefined; + try { + node = await createLibp2p({ + addresses: { listen: ["/p2p-circuit", "/webrtc"] }, + transports: [webSockets(), webRTC(), circuitRelayTransport()], + connectionEncrypters: [noise()], + streamMuxers: [yamux()], + services: { + identify: identify(), + pairing: Libp2p.pairingService( + { protocol: KHIE_PAIRING_PROTOCOL, pairedPeerTimeoutMs }, + canPair, + ), + jsonRpc: Libp2p.jsonRpcService( + { protocol: KHIE_JSON_RPC_PROTOCOL }, + function (request) { + const { pairing } = this.components; + if ( + !pairing.isPaired(request.peerId) || + !isSelectedPeer(request.peerId) + ) { + throw new ccc.JsonRpcError({ + code: -32000, + message: "Peer is not paired for Khie access", + }); + } + + pairing.refresh(request.peerId); + onRequest(request.peerId); + return connectionAuthorizer.handle( + request.peerId, + request.payload, + handler, + ); + }, + ), + }, + }); + node.services.pairing.onUnpaired((peerId) => { + connectionAuthorizer.unpair(peerId); + }); + signal.throwIfAborted(); + return node; + } catch (cause) { + await node?.stop(); + throw cause; + } +} + +function asError(cause: unknown) { + return cause instanceof Error + ? cause + : new Error("Khie signer session failed"); +} diff --git a/packages/app/src/app/modules/khie/peer-name.ts b/packages/app/src/app/modules/khie/peer-name.ts new file mode 100644 index 000000000..e9c318f83 --- /dev/null +++ b/packages/app/src/app/modules/khie/peer-name.ts @@ -0,0 +1,16 @@ +const DISPLAY_CONTROL = /[\u0000-\u001f\u007f-\u009f\u2028\u2029]/g; +const BIDI_CONTROL = /[\u061c\u200e\u200f\u202a-\u202e\u2066-\u2069]/g; + +export function displayPeerName(value: unknown) { + if (typeof value !== "string") { + return "Unknown"; + } + + return ( + value + .replace(DISPLAY_CONTROL, " ") + .replace(BIDI_CONTROL, "") + .replace(/\s+/g, " ") + .trim() || "Unknown" + ); +} diff --git a/packages/app/src/app/page.tsx b/packages/app/src/app/page.tsx index 1ba51cc7d..511f5af50 100644 --- a/packages/app/src/app/page.tsx +++ b/packages/app/src/app/page.tsx @@ -82,12 +82,15 @@ export default function Home() { const connected = signer !== undefined; const usingPrivateKey = privateKeySigner !== undefined; const needsAccess = selectedModule?.access === "signer"; + const displayedModule = selectedModule ?? stagedModule; const workspaceReady = selectedModule !== undefined && (!needsAccess || connected); useEffect(() => { const selectModuleFromAnchor = () => { - const id = window.location.hash.slice(1); + const fragment = window.location.hash.slice(1); + const separator = fragment.indexOf("?"); + const id = separator === -1 ? fragment : fragment.slice(0, separator); setSelectedModule(demoModules.find((module) => module.id === id)); }; @@ -332,7 +335,9 @@ export default function Home() { client={client} log={log} module={stagedModule} + setClient={setClient} signer={signer} + wallet={usingPrivateKey ? undefined : wallet} />
@@ -363,12 +368,14 @@ export default function Home() { ) : ( )} - {privateKeyMode ? "Private key" : "Module mounted"} + {privateKeyMode ? "Private key" : "Connect to continue"}

- {privateKeyMode ? "Enter your private key" : "Who are you?"} + {privateKeyMode + ? "Enter your private key" + : (displayedModule?.name ?? "Who are you?")}

-

+

{privateKeyMode ? ( <> @@ -378,12 +385,10 @@ export default function Home() { be read by browser extensions. This key stays only on this page and is cleared when you leave or reload. + ) : displayedModule ? ( + displayedModule.description ) : ( - <> - Link required for this module. -
- Choose one of the link options to continue. - + "Choose one of the link options to continue." )}

@@ -524,6 +529,10 @@ export default function Home() { className="connected-wallet-icon" src={wallet.icon} alt={wallet.name} + referrerPolicy="no-referrer" + onError={(event) => { + event.currentTarget.hidden = true; + }} /> ) : ( diff --git a/packages/app/src/app/provider.tsx b/packages/app/src/app/provider.tsx index 0a8790dea..fceb5dc92 100644 --- a/packages/app/src/app/provider.tsx +++ b/packages/app/src/app/provider.tsx @@ -26,7 +26,7 @@ export function AppProvider({ children }: { children: ReactNode }) { return ( { + if (!value) { + return; + } + + const matrix = encodeQR(value, "raw", { border: 2, ecc: "medium" }); + return { + path: matrix + .flatMap((row, y) => + row.map((dark, x) => (dark ? `M${x} ${y}h1v1h-1z` : "")), + ) + .join(""), + size: matrix.length, + }; + }, [value]); const style: QrCodeStyle | undefined = size === undefined ? undefined @@ -32,17 +50,15 @@ export function QrCode({ style={style} aria-live="polite" > - {value ? ( - + viewBox={`0 0 ${qr.size} ${qr.size}`} + role="img" + > + {title} + + ) : (
`; } - onClose(onClosed?: () => void) { + close(onClosed?: () => void) { if (this.mainRef.value) { this.mainRef.value.style.height = "0"; } setTimeout(() => { this.dispatchEvent(new ConnectorCloseEvent()); - this.bodyRef.value?.onClose?.(); + this.backToHome(); onClosed?.(); }, 150); } + private backToHome(): void { + const scene = this.contentRef.value?.firstElementChild as + ConnectorScene | null | undefined; + scene?.close(); + this.pairingKhie = false; + } + updated() { + this.observeContent(); + this.syncHeight(); + } + + private observeContent() { + const content = this.contentRef.value; + if (!content || this.resizeObserver) { + return; + } + + this.resizeObserver = new ResizeObserver(() => this.syncHeight()); + this.resizeObserver.observe(content); + } + + private syncHeight() { if (!this.mainRef.value) { return; } this.mainRef.value.style.height = `${ - this.bodyRef.value?.clientHeight ?? 0 + this.contentRef.value?.clientHeight ?? 0 }px`; } @@ -269,5 +399,9 @@ export class WebComponentConnector extends LitElement { overflow: hidden; transition: height 0.15s ease-out; } + + .content { + display: flow-root; + } `; } diff --git a/packages/connector/src/core-js.d.ts b/packages/connector/src/core-js.d.ts new file mode 100644 index 000000000..215f23af0 --- /dev/null +++ b/packages/connector/src/core-js.d.ts @@ -0,0 +1 @@ +declare module "core-js/es/promise/with-resolvers.js"; diff --git a/packages/connector/src/events/external.ts b/packages/connector/src/events/external.ts index d144a38d8..d9586a3bd 100644 --- a/packages/connector/src/events/external.ts +++ b/packages/connector/src/events/external.ts @@ -1,13 +1,5 @@ import { ccc } from "@ckb-ccc/ccc"; -export class ConnectorWillUpdateEvent extends Event { - static readonly eventName = "willUpdate"; - - constructor() { - super(ConnectorWillUpdateEvent.eventName); - } -} - export class ConnectorCloseEvent extends Event { static readonly eventName = "close"; @@ -24,8 +16,26 @@ export class SelectClientEvent extends Event { } } +export type ConnectorConnection = { + wallet: ccc.Wallet; + signerInfo: ccc.SignerInfo; +}; + +export class ConnectorConnectionEvent extends Event { + static readonly eventName = "connection"; + + constructor( + public readonly connectionOwner?: ccc.Owner, + ) { + super(ConnectorConnectionEvent.eventName, { + bubbles: true, + composed: true, + }); + } +} + export interface ConnectorEventMap { - [ConnectorWillUpdateEvent.eventName]: ConnectorWillUpdateEvent; [ConnectorCloseEvent.eventName]: ConnectorCloseEvent; [SelectClientEvent.eventName]: SelectClientEvent; + [ConnectorConnectionEvent.eventName]: ConnectorConnectionEvent; } diff --git a/packages/connector/src/scenes/connected.ts b/packages/connector/src/scenes/connected.ts index 746b408e6..2e8ae23cf 100644 --- a/packages/connector/src/scenes/connected.ts +++ b/packages/connector/src/scenes/connected.ts @@ -118,6 +118,7 @@ export class ConnectedScene extends LitElement { class="connecting-wallet-icon" src=${wallet.icon} alt=${wallet.name} + referrerpolicy="no-referrer" /> ${formatString(recommendedAddress)} @@ -137,7 +138,7 @@ export class ConnectedScene extends LitElement { ${formatString(internalAddress, 11, 9)} @@ -265,6 +266,18 @@ export class ConnectedScene extends LitElement { color: var(--tip-color-hover, var(--tip-color)); } + .address-copy { + transition: color 0.15s ease-in-out; + } + + .address-copy:hover { + color: var(--btn-color-hover, var(--btn-color, inherit)); + } + + .address-copy.text-tip:hover { + color: var(--tip-color-hover, var(--tip-color)); + } + .fee-rate-value { margin-left: auto; color: var(--tip-color); @@ -385,11 +398,7 @@ export class ConnectedScene extends LitElement { } `; - updated() { - this.dispatchEvent(new Event("updated", { bubbles: true, composed: true })); - } - - public onClose() { + public close() { this.selectingClient = false; this.selectingFeeRate = false; } diff --git a/packages/connector/src/scenes/error.ts b/packages/connector/src/scenes/error.ts new file mode 100644 index 000000000..480aaafc0 --- /dev/null +++ b/packages/connector/src/scenes/error.ts @@ -0,0 +1,46 @@ +export function errorMessage(cause: unknown): string { + if (cause instanceof Error) { + const message = cause.message || cause.name; + return cause.name && cause.name !== "Error" + ? `${cause.name}: ${message}` + : message; + } + + if (typeof cause === "string") { + return cause; + } + + if ( + typeof ErrorEvent !== "undefined" && + cause instanceof ErrorEvent && + cause.error !== undefined + ) { + return errorMessage(cause.error); + } + + if (typeof cause === "object" && cause !== null) { + if ( + "message" in cause && + typeof cause.message === "string" && + cause.message + ) { + const name = + "name" in cause && typeof cause.name === "string" ? cause.name : ""; + return name ? `${name}: ${cause.message}` : cause.message; + } + + try { + const serialized = JSON.stringify(cause); + if (serialized && serialized !== "{}") { + return serialized; + } + } catch { + // Fall through to the browser's string representation. + } + } + + const message = String(cause); + return message && message !== "[object Object]" + ? message + : "Unknown browser error"; +} diff --git a/packages/connector/src/scenes/fee-rate.ts b/packages/connector/src/scenes/fee-rate.ts index 313ab680b..9a2b6bc57 100644 --- a/packages/connector/src/scenes/fee-rate.ts +++ b/packages/connector/src/scenes/fee-rate.ts @@ -183,10 +183,6 @@ export class FeeRateScene extends LitElement { `; } - updated() { - this.dispatchEvent(new Event("updated", { bubbles: true, composed: true })); - } - static styles = css` :host { width: 100%; @@ -244,5 +240,9 @@ export class FeeRateScene extends LitElement { width: 9rem; margin-left: auto; } + + .fee-rate-input::part(input) { + text-align: right; + } `; } diff --git a/packages/connector/src/scenes/index.ts b/packages/connector/src/scenes/index.ts index 6ba3882ec..2f55726fe 100644 --- a/packages/connector/src/scenes/index.ts +++ b/packages/connector/src/scenes/index.ts @@ -1,3 +1,4 @@ import "./connected.js"; import "./fee-rate.js"; +import "./khie/connect.js"; import "./selecting/index.js"; diff --git a/packages/connector/src/scenes/khie/connect.ts b/packages/connector/src/scenes/khie/connect.ts new file mode 100644 index 000000000..850fd99c6 --- /dev/null +++ b/packages/connector/src/scenes/khie/connect.ts @@ -0,0 +1,101 @@ +import { ccc } from "@ckb-ccc/ccc"; +import { css, html, LitElement } from "lit"; +import { customElement, property, query, state } from "lit/decorators.js"; +import { errorMessage } from "../error.js"; +import type { KhiePairing } from "./pairing.js"; +import { KHIE_WALLET_NAME, khieSignerIcon } from "./wallet.js"; + +type KhieLoadStatus = "loading" | "ready" | "error"; + +@customElement("ccc-khie-connect-scene") +export class KhieConnectScene extends LitElement { + @property({ attribute: false }) + public appName?: string; + + @property({ attribute: false }) + public client!: ccc.Client; + + @state() + private status: KhieLoadStatus = "loading"; + @state() + private error?: string; + + @query("ccc-khie-pairing") + private khiePairing?: KhiePairing; + + private loadId = 0; + + connectedCallback() { + super.connectedCallback(); + void this.loadKhie(); + } + + disconnectedCallback() { + super.disconnectedCallback(); + this.close(); + } + + render() { + return html` + ${this.renderBody()} + `; + } + + private renderBody() { + if (this.status === "ready") { + return html``; + } + + return html` this.loadKhie() : undefined} + >`; + } + + public close() { + this.loadId += 1; + this.khiePairing?.close(); + } + + private async loadKhie() { + const loadId = ++this.loadId; + this.status = "loading"; + this.error = undefined; + + try { + await import("./pairing.js"); + if (loadId === this.loadId) { + this.status = "ready"; + } + } catch (cause) { + if (loadId === this.loadId) { + this.error = errorMessage(cause); + this.status = "error"; + } + } + } + + private back = () => { + if (this.khiePairing?.cancelScan()) { + return; + } + + this.dispatchEvent(new Event("back", { bubbles: true, composed: true })); + }; + + static styles = css` + :host { + display: block; + } + `; +} diff --git a/packages/connector/src/scenes/khie/node.ts b/packages/connector/src/scenes/khie/node.ts new file mode 100644 index 000000000..85aee11f2 --- /dev/null +++ b/packages/connector/src/scenes/khie/node.ts @@ -0,0 +1,77 @@ +import { ccc } from "@ckb-ccc/ccc"; +import { Libp2p } from "@ckb-ccc/libp2p"; +import { ensurePromiseWithResolvers } from "./promiseWithResolvers.js"; + +export const JSON_RPC_PROTOCOL = "/nervos-ckb/khie/json-rpc/0.0.1"; +export const DEFAULT_RELAY_ADDRESS = "/dns4/relay.ckbccc.com/tcp/443/wss"; +export const CONNECTOR_ENDPOINT_URL = "https://app.ckbccc.com/#khie"; + +const PAIRING_PROTOCOL = "/nervos-ckb/khie/pairing/0.0.1"; +const PAIRED_PEER_TIMEOUT_MS = 30 * 60 * 1000; + +async function createNode( + canPair: Libp2p.PairingGuard, + signal: AbortSignal, + name?: string, +) { + signal.throwIfAborted(); + await ensurePromiseWithResolvers(); + signal.throwIfAborted(); + + const [ + { noise }, + { yamux }, + { circuitRelayTransport }, + { identify }, + { webRTC }, + { webSockets }, + { createLibp2p }, + ] = await Promise.all([ + import("@chainsafe/libp2p-noise"), + import("@chainsafe/libp2p-yamux"), + import("@libp2p/circuit-relay-v2"), + import("@libp2p/identify"), + import("./webrtc.js"), + import("@libp2p/websockets"), + import("libp2p"), + ]); + + signal.throwIfAborted(); + return createLibp2p({ + addresses: { + listen: ["/p2p-circuit", "/webrtc"], + }, + transports: [webSockets(), webRTC(), circuitRelayTransport()], + connectionEncrypters: [noise()], + streamMuxers: [yamux()], + services: { + identify: identify(), + pairing: Libp2p.pairingService( + { + name, + protocol: PAIRING_PROTOCOL, + pairedPeerTimeoutMs: PAIRED_PEER_TIMEOUT_MS, + }, + canPair, + ), + }, + }); +} + +export type KhieNode = Awaited>; + +export async function createKhieNode( + canPair: Libp2p.PairingGuard, + signal: AbortSignal, + name?: string, +) { + let node: KhieNode | undefined; + try { + node = await createNode(canPair, signal, name); + signal.throwIfAborted(); + return new ccc.OwnerUnique(node, (node) => node.stop()); + } catch (cause) { + await node?.stop(); + throw cause; + } +} diff --git a/packages/connector/src/scenes/khie/pairing.ts b/packages/connector/src/scenes/khie/pairing.ts new file mode 100644 index 000000000..4375d2fe1 --- /dev/null +++ b/packages/connector/src/scenes/khie/pairing.ts @@ -0,0 +1,664 @@ +import { ccc } from "@ckb-ccc/ccc"; +import { css, html, LitElement } from "lit"; +import { customElement, property, state } from "lit/decorators.js"; +import { CHECK_SVG } from "../../assets/check.svg.js"; +import { LEFT_SVG } from "../../assets/left.svg.js"; +import { RETRY_SVG } from "../../assets/retry.svg.js"; +import { SCAN_SVG } from "../../assets/scan.svg.js"; +import type { QrScannedEvent } from "../../components/qr-scanner.js"; +import { ConnectorConnectionEvent } from "../../events/external.js"; +import { CloseRequestEvent } from "../../events/internal.js"; +import { errorMessage } from "../error.js"; +import { DEFAULT_RELAY_ADDRESS } from "./node.js"; +import { KhiePairingSession } from "./session.js"; +import { KHIE_WALLET_NAME, khieSignerIcon } from "./wallet.js"; + +@customElement("ccc-khie-pairing") +export class KhiePairing extends LitElement { + @property({ attribute: false }) + public appName?: string; + + @property({ attribute: false }) + public client!: ccc.Client; + + @state() + private relayAddress = DEFAULT_RELAY_ADDRESS; + @state() + private isAdvancedSettingsOpen = false; + @state() + private khieEndpoint = ""; + @state() + private isScanning = false; + @state() + private localError?: string; + + private session!: KhiePairingSession; + + connectedCallback() { + super.connectedCallback(); + this.localError = undefined; + + this.session = new KhiePairingSession({ + name: this.appName, + client: this.client, + onConnected: (connectionOwner) => { + this.dispatchEvent( + new CloseRequestEvent(() => { + if (!connectionOwner.isValid) { + return; + } + try { + this.dispatchEvent(new ConnectorConnectionEvent(connectionOwner)); + } finally { + void connectionOwner.dispose().catch(() => {}); + } + }), + ); + }, + onStateChange: () => { + if (this.session.state.phase !== "idle") { + this.localError = undefined; + } + this.requestUpdate(); + }, + }); + void this.session.start(this.relayAddress.trim()); + } + + disconnectedCallback() { + super.disconnectedCallback(); + void this.session.close(); + } + + public close() { + this.isScanning = false; + void this.session.close(); + } + + public cancelScan() { + if (!this.isScanning) { + return false; + } + + this.isScanning = false; + return true; + } + + private async pair() { + const endpoint = this.khieEndpoint.trim(); + if (!endpoint) { + return; + } + + this.localError = undefined; + if (await this.session.pair(endpoint)) { + this.khieEndpoint = ""; + } + } + + private startScanner() { + this.isScanning = true; + this.localError = undefined; + } + + private connectRelay() { + void this.session.connectRelay(this.relayAddress.trim()); + } + + private qrScanned = (event: QrScannedEvent) => { + this.khieEndpoint = event.value.trim(); + this.isScanning = false; + void this.pair(); + }; + + private scannerError = (event: ErrorEvent) => { + this.localError = `Unable to scan pairing code: ${errorMessage(event.error)}`; + this.isScanning = false; + }; + + render() { + const { + canPair, + error: sessionError, + errorKind, + ownEndpoint, + phase, + relayState, + signer, + } = this.session.state; + const error = this.localError ?? sessionError; + + if (phase === "pairing" || phase === "connecting") { + return html` { + void this.session.retrySigner(); + } + : undefined + } + >`; + } + + if (this.isScanning) { + return html``; + } + + return html` +
+
+ +
+ { + this.localError = errorMessage(event.error); + }} + > + ${ + relayState === "failed" + ? html` this.connectRelay()}> + ${RETRY_SVG} Try again + ` + : ownEndpoint + ? html` { + this.localError = errorMessage(event.error); + }} + > + ${ownEndpoint} + ` + : html`Connecting relay…` + } +
+
+ + + +
+ +
+ this.startScanner()}> + ${SCAN_SVG} Scan a wallet + +
+ { + this.khieEndpoint = ( + event.currentTarget as HTMLElement & { value: string } + ).value; + }} + @keydown=${(event: KeyboardEvent) => { + if (event.key === "Enter") { + void this.pair(); + } + }} + > + +
+
+ + +
+ + ${ + this.isAdvancedSettingsOpen + ? html`
+ +
+ { + this.relayAddress = ( + event.currentTarget as HTMLElement & { value: string } + ).value; + }} + > + +
+
` + : undefined + } +
+ ${ + error && !this.localError && errorKind === "incompatible-peer" + ? html`` + : undefined + } + ${ + error && (this.localError || errorKind !== "incompatible-peer") + ? html`${error}` + : undefined + } + `; + } + + static styles = css` + :host { + display: flex; + width: min(44rem, calc(100vw - 4rem)); + flex-direction: column; + gap: 1rem; + } + + .pairing-layout { + display: grid; + min-width: 0; + grid-template-columns: minmax(0, 1fr) 2.25rem minmax(0, 1fr); + gap: 1rem; + } + + .field { + display: flex; + min-width: 0; + flex-direction: column; + gap: 0.5rem; + } + + label { + font-size: 0.85rem; + font-weight: 600; + } + + .pairing-side { + min-height: 17.5rem; + } + + .own-side { + grid-row: 1; + grid-column: 1; + } + + .remote-side { + display: grid; + grid-row: 1; + grid-column: 3; + grid-template-rows: auto minmax(0, 1fr) auto; + } + + button { + box-sizing: border-box; + padding: 0.65rem 0.85rem; + border: 1px solid var(--divider); + border-radius: 0.4rem; + color: var(--btn-color, inherit); + background: var(--btn-primary); + font: inherit; + cursor: pointer; + } + + button:hover:not(:disabled) { + color: var(--btn-color-hover, var(--btn-color, inherit)); + background: var(--btn-primary-hover); + } + + button:disabled { + cursor: not-allowed; + opacity: 0.55; + } + + .endpoint-input { + min-width: 0; + height: 2.7rem; + } + + .endpoint-pair { + display: grid; + min-width: 0; + justify-items: center; + gap: 0.75rem; + } + + .qr-code { + width: min(13rem, 100%); + } + + .endpoint-copy, + .endpoint-pending { + box-sizing: border-box; + width: 100%; + min-height: 2.7rem; + } + + .endpoint-copy { + display: grid; + min-width: 0; + padding: 0.65rem 0.85rem; + align-items: center; + color: var(--btn-color, inherit); + cursor: pointer; + grid-template-columns: minmax(0, 1fr) auto; + gap: 0.7rem; + text-align: left; + transition: color 0.15s ease-in-out; + } + + .endpoint-copy:hover { + color: var(--btn-color-hover, var(--btn-color, inherit)); + } + + .endpoint-copy span { + overflow: hidden; + font-family: monospace; + font-size: 0.72rem; + text-overflow: ellipsis; + white-space: nowrap; + } + + .endpoint-pending { + display: flex; + padding: 0.65rem 0.85rem; + align-items: center; + justify-content: center; + color: var(--tip-color); + font-size: 0.8rem; + } + + .divider { + position: relative; + display: grid; + grid-row: 1; + grid-column: 2; + color: var(--tip-color); + font-size: 0.7rem; + place-items: center; + text-transform: uppercase; + } + + .divider::before { + position: absolute; + top: 0; + bottom: 0; + left: 50%; + width: 1px; + background: var(--divider); + content: ""; + } + + .divider span { + z-index: 1; + padding: 0.5rem 0.25rem; + background: var(--background); + } + + .remote-actions { + display: grid; + align-content: center; + gap: 0.5rem; + } + + .endpoint-control, + .relay-control { + display: grid; + min-width: 0; + grid-template-columns: minmax(0, 1fr) auto; + } + + .endpoint-control { + overflow: hidden; + border: 1px solid var(--divider); + border-radius: 0.4rem; + } + + .endpoint-control:focus-within { + border-color: var(--tip-color-hover, var(--tip-color)); + } + + .endpoint-input::part(input) { + height: 100%; + padding: 0.65rem; + border: 0; + border-radius: 0; + } + + .endpoint-submit { + display: grid; + width: 2.7rem; + height: 2.7rem; + padding: 0; + border: 0; + border-radius: 0; + color: var(--btn-color-hover, var(--btn-color, inherit)); + background: transparent; + place-items: center; + } + + .endpoint-submit:hover:not(:disabled) { + background: transparent; + } + + .endpoint-submit:disabled { + color: var(--divider); + opacity: 1; + } + + .endpoint-submit svg { + width: 1.3rem; + height: 1.3rem; + } + + .text-button { + padding: 0; + border: 0; + color: var(--tip-color); + background: transparent; + font-size: 0.72rem; + } + + .text-button:hover:not(:disabled) { + color: var(--tip-color-hover, var(--tip-color)); + background: transparent; + } + + .advanced-toggle { + display: flex; + align-items: center; + justify-self: end; + gap: 0.3rem; + } + + .settings-chevron { + display: grid; + transform: rotate(-90deg); + transition: transform 0.15s ease-in-out; + --left-stroke-width: 2.5; + } + + .settings-chevron svg { + width: 0.65rem; + height: 0.65rem; + } + + .advanced-toggle[aria-expanded="true"] .settings-chevron { + transform: rotate(90deg); + } + + .advanced-settings { + grid-row: 2; + grid-column: 1 / -1; + animation: advanced-settings-enter 0.2s ease-out both; + } + + .relay-control { + gap: 0.5rem; + } + + .relay-input { + min-width: 0; + height: 100%; + } + + .relay-input::part(input) { + height: 100%; + padding: 0.65rem; + border-color: var(--divider); + border-radius: 0.4rem; + } + + .relay-control button { + white-space: nowrap; + } + + @keyframes advanced-settings-enter { + from { + opacity: 0; + transform: translateY(-0.35rem); + } + } + + .error { + display: block; + color: var(--tip-color); + font-size: 0.8rem; + text-align: center; + } + + .compatibility-help { + display: grid; + padding: 0.8rem 0.9rem; + border: 1px solid var(--divider); + border-radius: 0.4rem; + gap: 0.3rem; + } + + .compatibility-help strong { + font-size: 0.85rem; + } + + .compatibility-help span { + color: var(--tip-color); + font-size: 0.78rem; + line-height: 1.45; + } + + .compatibility-help a { + color: inherit; + text-decoration: underline; + text-underline-offset: 0.15em; + } + + @media (max-width: 40rem) { + :host { + width: min(28rem, calc(100vw - 4rem)); + } + + .qr-code { + /* I hate Apple */ + width: min(22rem, calc(100% - 1.5rem)); + height: min(22rem, calc(100vw - 5.5rem)); + } + + .pairing-layout { + grid-template-columns: minmax(0, 1fr); + } + + .divider { + height: 1.25rem; + grid-row: 2; + grid-column: 1; + } + + .divider::before { + top: 50%; + right: 0; + bottom: auto; + left: 0; + width: auto; + height: 1px; + } + + .divider span { + padding: 0.25rem 0.5rem; + } + + .remote-side { + min-height: 11rem; + grid-row: 3; + grid-column: 1; + } + + .advanced-settings { + grid-row: 4; + } + } + `; +} diff --git a/packages/connector/src/scenes/khie/promiseWithResolvers.test.ts b/packages/connector/src/scenes/khie/promiseWithResolvers.test.ts new file mode 100644 index 000000000..bd5778b5a --- /dev/null +++ b/packages/connector/src/scenes/khie/promiseWithResolvers.test.ts @@ -0,0 +1,26 @@ +import { describe, expect, it } from "vitest"; +import { ensurePromiseWithResolvers } from "./promiseWithResolvers.js"; + +describe("ensurePromiseWithResolvers", () => { + it("installs Promise.withResolvers when it is unavailable", async () => { + const original = Object.getOwnPropertyDescriptor(Promise, "withResolvers"); + + try { + Object.defineProperty(Promise, "withResolvers", { + configurable: true, + value: undefined, + writable: true, + }); + + await ensurePromiseWithResolvers(); + + expect(Reflect.get(Promise, "withResolvers")).toBeTypeOf("function"); + } finally { + if (original) { + Object.defineProperty(Promise, "withResolvers", original); + } else { + Reflect.deleteProperty(Promise, "withResolvers"); + } + } + }); +}); diff --git a/packages/connector/src/scenes/khie/promiseWithResolvers.ts b/packages/connector/src/scenes/khie/promiseWithResolvers.ts new file mode 100644 index 000000000..f56f11443 --- /dev/null +++ b/packages/connector/src/scenes/khie/promiseWithResolvers.ts @@ -0,0 +1,7 @@ +export async function ensurePromiseWithResolvers(): Promise { + if (typeof Reflect.get(Promise, "withResolvers") === "function") { + return; + } + + await import("core-js/es/promise/with-resolvers.js"); +} diff --git a/packages/connector/src/scenes/khie/session.ts b/packages/connector/src/scenes/khie/session.ts new file mode 100644 index 000000000..5693d9541 --- /dev/null +++ b/packages/connector/src/scenes/khie/session.ts @@ -0,0 +1,410 @@ +import { ccc } from "@ckb-ccc/ccc"; +import { Libp2p } from "@ckb-ccc/libp2p"; +import type { Connection, PeerId } from "@libp2p/interface"; +import { multiaddr } from "@multiformats/multiaddr"; +import type { ConnectorConnection } from "../../events/external.js"; +import { errorMessage } from "../error.js"; +import { + CONNECTOR_ENDPOINT_URL, + createKhieNode, + JSON_RPC_PROTOCOL, + type KhieNode, +} from "./node.js"; +import { khieWalletFrom } from "./wallet.js"; + +export type KhiePairingPhase = "connecting" | "idle" | "pairing"; +export type KhieRelayState = "connected" | "connecting" | "failed" | "idle"; + +export type KhiePairingSessionState = Readonly<{ + canPair: boolean; + error?: string; + errorKind?: "incompatible-peer"; + ownEndpoint: string; + phase: KhiePairingPhase; + relayState: KhieRelayState; + signer?: ccc.SignerJsonRpc; +}>; + +export const KHIE_PAIRING_SESSION_INITIAL_STATE: KhiePairingSessionState = { + canPair: false, + ownEndpoint: "", + phase: "idle", + relayState: "idle", +}; + +export type KhiePairingSessionConfig = { + client: ccc.Client; + name?: string; + onConnected: (connectionOwner: ccc.Owner) => void; + onStateChange: () => void; +}; + +type KhiePairingSessionResources = { + abortController: AbortController; + nodeOwner?: ccc.Owner; + relayConnection?: Connection; + nodeSubscriptions: Array<() => void>; + selectedPeer?: PeerId; + pendingSigner?: { + cleanup: () => Promise; + signer: ccc.SignerJsonRpc; + }; +}; + +function removeNodeSubscriptions(resources: KhiePairingSessionResources) { + for (const unsubscribe of resources.nodeSubscriptions.splice(0)) { + unsubscribe(); + } +} + +async function releaseResources(resources: KhiePairingSessionResources) { + removeNodeSubscriptions(resources); + + try { + if (resources.pendingSigner) { + await resources.pendingSigner.cleanup(); + } else { + await resources.relayConnection?.close(); + } + } finally { + await resources.nodeOwner?.dispose(); + } +} + +export class KhiePairingSession { + private readonly client: ccc.Client; + private readonly name?: string; + private readonly onConnected: KhiePairingSessionConfig["onConnected"]; + private readonly onStateChange: KhiePairingSessionConfig["onStateChange"]; + + private resources?: KhiePairingSessionResources; + + private currentState = KHIE_PAIRING_SESSION_INITIAL_STATE; + private hasStarted = false; + private closing?: Promise; + + constructor(config: KhiePairingSessionConfig) { + this.client = config.client; + this.name = config.name; + this.onConnected = config.onConnected; + this.onStateChange = config.onStateChange; + } + + get state(): KhiePairingSessionState { + return this.currentState; + } + + private update(patch: Partial) { + if (this.closing) { + return; + } + + this.currentState = { ...this.currentState, ...patch }; + this.onStateChange(); + } + + private beginOperation( + patch: Omit, "error" | "errorKind"> = {}, + ) { + this.update({ ...patch, error: undefined, errorKind: undefined }); + } + + private updateError( + cause: unknown, + patch: Omit, "error" | "errorKind"> = {}, + ) { + this.update({ + ...patch, + error: errorMessage(cause), + errorKind: isIncompatiblePeerError(cause) + ? "incompatible-peer" + : undefined, + }); + } + + async start(relayAddress: string) { + if (this.hasStarted || this.closing) { + return; + } + this.hasStarted = true; + + const resources: KhiePairingSessionResources = { + abortController: new AbortController(), + nodeSubscriptions: [], + }; + this.resources = resources; + const signal = resources.abortController.signal; + + try { + resources.nodeOwner = await createKhieNode( + () => !signal.aborted && !resources.selectedPeer, + signal, + this.name, + ); + signal.throwIfAborted(); + const node = resources.nodeOwner.value; + this.update({ canPair: true }); + this.observeNode(resources, node); + await this.connectRelay(relayAddress); + } catch (cause) { + this.updateError(cause); + await this.close(); + } + } + + close(): Promise { + if (this.closing) { + return this.closing; + } + + const resources = this.resources; + this.resources = undefined; + + resources?.abortController.abort(); + this.closing = resources ? releaseResources(resources) : Promise.resolve(); + return this.closing; + } + + private observeNode(resources: KhiePairingSessionResources, node: KhieNode) { + let endpointUpdateId = 0; + const syncEndpoint = () => { + const updateId = ++endpointUpdateId; + const addresses = node.getMultiaddrs(); + if (addresses.length === 0) { + this.update({ ownEndpoint: "" }); + return; + } + + void Libp2p.encodePairingEndpoint( + CONNECTOR_ENDPOINT_URL, + addresses, + node.services.pairing.secret, + "connector", + ) + .then((ownEndpoint) => { + if (updateId !== endpointUpdateId) { + return; + } + this.update({ ownEndpoint }); + }) + .catch((cause: unknown) => { + if (updateId !== endpointUpdateId) { + return; + } + this.updateError(cause); + }); + }; + + node.addEventListener("self:peer:update", syncEndpoint); + resources.nodeSubscriptions.push( + () => node.removeEventListener("self:peer:update", syncEndpoint), + node.services.pairing.onSecretChanged(syncEndpoint), + node.services.pairing.onError((error) => { + this.updateError(error); + }), + node.services.pairing.onPaired((peerId) => { + void this.acceptPeer(peerId); + }), + ); + syncEndpoint(); + } + + async connectRelay(relayAddress: string) { + const resources = this.resources; + const node = resources?.nodeOwner?.value; + if ( + !node || + !relayAddress || + resources.selectedPeer || + this.currentState.relayState === "connecting" + ) { + return; + } + + this.beginOperation({ relayState: "connecting" }); + const signal = resources.abortController.signal; + + try { + const previous = resources.relayConnection; + resources.relayConnection = undefined; + + await previous?.close(); + const connection = await node.dial(multiaddr(relayAddress), { signal }); + signal.throwIfAborted(); + + resources.relayConnection = connection; + this.update({ relayState: "connected" }); + } catch (cause) { + this.updateError(cause, { relayState: "failed" }); + } + } + + async pair(endpoint: string) { + const resources = this.resources; + const node = resources?.nodeOwner?.value; + if (!node || !endpoint || this.currentState.phase !== "idle") { + return false; + } + + this.beginOperation({ phase: "pairing" }); + const signal = resources.abortController.signal; + + try { + const target = await Libp2p.decodePairingEndpoint(endpoint, "provider"); + await node.services.pairing.pair(target, { signal }); + return true; + } catch (cause) { + this.updateError(cause); + return false; + } finally { + if (!resources.selectedPeer) { + this.update({ phase: "idle" }); + } + } + } + + private async acceptPeer(peerId: PeerId) { + const resources = this.resources; + const node = resources?.nodeOwner?.value; + if (!resources || !node || resources.selectedPeer) { + return; + } + + const signal = resources.abortController.signal; + + resources.selectedPeer = peerId; + this.beginOperation({ phase: "pairing" }); + + const transport = new Libp2p.JsonRpcTransportLibp2p(node, peerId, { + protocol: JSON_RPC_PROTOCOL, + signal, + onResponse: () => node.services.pairing.refresh(peerId), + }); + + let signer: ccc.SignerJsonRpc; + try { + signer = await ccc.SignerJsonRpc.new(this.client, { transport }); + signal.throwIfAborted(); + } catch (cause) { + resources.selectedPeer = undefined; + this.updateError(cause, { phase: "idle" }); + await node.services.pairing.unpair(peerId); + return; + } + + const cleanup = this.createPairingCleanup( + node, + peerId, + signer, + resources.relayConnection, + ); + resources.pendingSigner = { cleanup, signer }; + await this.connectSigner(); + } + + private createPairingCleanup( + node: KhieNode, + peerId: PeerId, + signer: ccc.SignerJsonRpc, + relayConnection?: Connection, + ) { + let unsubscribeUnpaired = () => {}; + + const cleanup = async () => { + unsubscribeUnpaired(); + + try { + await node.services.pairing.unpair(peerId); + } finally { + await relayConnection?.close(); + } + }; + + unsubscribeUnpaired = node.services.pairing.onUnpaired((unpairedPeer) => { + if (!unpairedPeer.equals(peerId)) { + return; + } + + if (this.resources?.pendingSigner?.signer === signer) { + this.updateError( + new Error("The wallet unpaired. Go back and pair again."), + { signer: undefined }, + ); + } + signer.replace(); + void this.close(); + }); + return cleanup; + } + + retrySigner() { + return this.connectSigner(); + } + + private async connectSigner() { + const resources = this.resources; + const pendingSigner = resources?.pendingSigner; + const nodeOwner = resources?.nodeOwner; + if (!resources || !pendingSigner || !nodeOwner) { + return; + } + + const { cleanup, signer } = pendingSigner; + this.beginOperation({ phase: "connecting", signer }); + const signal = resources.abortController.signal; + + try { + await signer.connect(); + if (!(await signer.isConnected())) { + throw new Error("Khie signer did not connect"); + } + signal.throwIfAborted(); + + removeNodeSubscriptions(resources); + const abortController = resources.abortController; + const nodeOwnership = nodeOwner.map((node) => node); + const connectedOwner = new ccc.OwnerUnique( + nodeOwnership.value, + async () => { + abortController.abort(); + try { + await cleanup(); + } finally { + await nodeOwnership.dispose(); + } + }, + ); + + const connectionOwner = new ccc.OwnerUnique(signer, (signer) => + signer.disconnect(), + ).map((signer) => { + const wallet = khieWalletFrom(signer); + return { + wallet, + signerInfo: new ccc.SignerInfo(wallet.name, signer), + }; + }); + signer.onReplaced(() => { + void connectionOwner.dispose().catch(() => {}); + void connectedOwner.dispose().catch(() => {}); + }); + resources.pendingSigner = undefined; + this.resources = undefined; + try { + this.onConnected(connectionOwner); + } catch (cause) { + await connectionOwner.dispose(); + throw cause; + } + } catch (cause) { + this.updateError(cause); + } + } +} + +function isIncompatiblePeerError(cause: unknown): cause is Error { + return ( + cause instanceof Libp2p.PairingEndpointRoleError || + (cause instanceof Error && cause.name === "UnsupportedProtocolError") + ); +} diff --git a/packages/connector/src/scenes/khie/wallet.ts b/packages/connector/src/scenes/khie/wallet.ts new file mode 100644 index 000000000..a2146522e --- /dev/null +++ b/packages/connector/src/scenes/khie/wallet.ts @@ -0,0 +1,31 @@ +import { ccc } from "@ckb-ccc/ccc"; +import { KHIE_SVG } from "../../assets/khie.svg.js"; + +export const KHIE_WALLET_NAME = "Khie"; + +export function khieSignerIcon(icon: string | undefined): string { + if (!icon) { + return KHIE_SVG; + } + + try { + const url = new URL(icon); + if ( + url.protocol === "https:" || + (url.protocol === "data:" && /^data:image\//i.test(icon)) + ) { + return icon; + } + } catch { + // Fall through to the default Khie icon. + } + + return KHIE_SVG; +} + +export function khieWalletFrom(signer: ccc.SignerJsonRpc): ccc.Wallet { + return { + name: signer.name ?? KHIE_WALLET_NAME, + icon: khieSignerIcon(signer.icon), + }; +} diff --git a/packages/connector/src/scenes/khie/webrtc.browser.ts b/packages/connector/src/scenes/khie/webrtc.browser.ts new file mode 100644 index 000000000..980b890c1 --- /dev/null +++ b/packages/connector/src/scenes/khie/webrtc.browser.ts @@ -0,0 +1 @@ +export { webRTC } from "@libp2p/webrtc"; diff --git a/packages/connector/src/scenes/khie/webrtc.ts b/packages/connector/src/scenes/khie/webrtc.ts new file mode 100644 index 000000000..891dd32d1 --- /dev/null +++ b/packages/connector/src/scenes/khie/webrtc.ts @@ -0,0 +1,5 @@ +type WebRTCFactory = typeof import("@libp2p/webrtc").webRTC; + +export const webRTC: WebRTCFactory = () => { + throw new Error("WebRTC is only available in browsers"); +}; diff --git a/packages/connector/src/scenes/selecting/connecting.ts b/packages/connector/src/scenes/selecting/connecting.ts deleted file mode 100644 index 4ed9bddd2..000000000 --- a/packages/connector/src/scenes/selecting/connecting.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { ccc } from "@ckb-ccc/ccc"; -import { html } from "lit"; -import { RETRY_SVG } from "../../assets/retry.svg.js"; - -export function generateConnectingScene( - wallet: ccc.WalletWithSigners, - signer: ccc.SignerInfo, - error: string | undefined, - onSignerSelected: ( - wallet: ccc.WalletWithSigners, - signer: ccc.SignerInfo, - ) => unknown, -) { - return [ - wallet.name, - html` - ${wallet.name} - ${ - error ? `Failed to open ${wallet.name}` : `Opening ${wallet.name}...` - } - ${error ?? "Confirm connection in the wallet"} - onSignerSelected(wallet, signer)} - > - ${RETRY_SVG} Try again - - `, - ]; -} diff --git a/packages/connector/src/scenes/selecting/index.ts b/packages/connector/src/scenes/selecting/index.ts index e719425f8..e4d00cd8e 100644 --- a/packages/connector/src/scenes/selecting/index.ts +++ b/packages/connector/src/scenes/selecting/index.ts @@ -2,7 +2,7 @@ import { ccc } from "@ckb-ccc/ccc"; import { css, html, LitElement } from "lit"; import { customElement, property, state } from "lit/decorators.js"; import { CloseRequestEvent, ConnectedEvent } from "../../events/internal.js"; -import { generateConnectingScene } from "./connecting.js"; +import { errorMessage } from "../error.js"; import { generateSignersScene } from "./signers.js"; import { generateWalletsScene } from "./wallets.js"; @@ -19,30 +19,7 @@ export class SelectingScene extends LitElement { private connectingError?: string; render() { - const [title, body] = (() => { - if (!this.selectedWallet) { - return generateWalletsScene( - this.wallets ?? [], - (wallet) => { - this.selectedWallet = wallet; - }, - this.signerSelectedHandler, - ); - } - if (!this.selectedSigner) { - return generateSignersScene( - this.selectedWallet, - this.signerSelectedHandler, - ); - } - - return generateConnectingScene( - this.selectedWallet, - this.selectedSigner, - this.connectingError, - this.signerSelectedHandler, - ); - })(); + const [title, body] = this.renderContent(); return html``; } - public onClose() { + private renderContent() { + const wallet = this.selectedWallet; + if (!wallet) { + return generateWalletsScene( + this.wallets ?? [], + (selectedWallet) => { + this.selectedWallet = selectedWallet; + }, + this.signerSelectedHandler, + ); + } + + const signer = this.selectedSigner; + if (!signer) { + return generateSignersScene(wallet, this.signerSelectedHandler); + } + + return [ + wallet.name, + html` this.signerSelectedHandler(wallet, signer)} + >`, + ]; + } + + public close() { this.selectedWallet = undefined; this.selectedSigner = undefined; this.connectingError = undefined; @@ -72,93 +78,48 @@ export class SelectingScene extends LitElement { wallet: ccc.WalletWithSigners, signerInfo: ccc.SignerInfo, ) => { + void this.connectSigner(wallet, signerInfo); + }; + + private async connectSigner( + wallet: ccc.WalletWithSigners, + signerInfo: ccc.SignerInfo, + ) { this.connectingError = undefined; this.selectedWallet = wallet; this.selectedSigner = signerInfo; - void (async () => { - const { signer } = signerInfo; - try { - await signer.connect(); - - if (!(await signer.isConnected())) { - this.connectingError = "Unknown connection status"; - return; - } - } catch (error) { - if (typeof error !== "object" || error === null) { - this.connectingError = JSON.stringify(error); - return; - } - if (!("message" in error)) { - this.connectingError = "Unknown error"; - return; - } + const { signer } = signerInfo; + try { + await signer.connect(); - const message = error.message; - if (typeof message === "string") { - this.connectingError = message; - return; - } - - this.connectingError = JSON.stringify(message); + if (!(await signer.isConnected())) { + this.connectingError = "Unknown connection status"; return; } + } catch (cause) { + this.connectingError = errorMessage(cause); + return; + } - this.dispatchEvent( - new CloseRequestEvent(() => { - this.dispatchEvent(new ConnectedEvent(wallet.name, signerInfo.name)); - }), - ); - })(); - }; + this.dispatchEvent( + new CloseRequestEvent(() => { + this.dispatchEvent(new ConnectedEvent(wallet.name, signerInfo.name)); + }), + ); + } static styles = css` - .primary-icon { - color: var(--icon-primary); - } - :host { display: block; } - .text-bold { - font-weight: bold; - } - - .text-tip { - color: var(--tip-color); - transition: color 0.15s ease-in-out; - } - - .text-tip:hover { - color: var(--tip-color-hover, var(--tip-color)); - } - .mb-1 { margin-bottom: 0.7rem; } - .mb-2 { - margin-bottom: 1rem; - } .mt-1 { margin-top: 0.7rem; } - .mt-2 { - margin-top: 1rem; - } - .ml-1 { - margin-left: 0.7rem; - } - .ml-2 { - margin-left: 1em; - } - .mr-1 { - margin-right: 0.7rem; - } - .mr-2 { - margin-right: 1rem; - } .wallet-icon { width: 4rem; @@ -166,19 +127,5 @@ export class SelectingScene extends LitElement { margin-bottom: 0.5rem; border-radius: 0.8rem; } - - .connecting-wallet-icon { - width: 5rem; - height: 5rem; - border-radius: 1rem; - } - - .text-center { - text-align: center; - } `; - - updated() { - this.dispatchEvent(new Event("updated", { bubbles: true, composed: true })); - } } diff --git a/packages/connector/src/scenes/selecting/signers.ts b/packages/connector/src/scenes/selecting/signers.ts index ad334a89b..2166e5812 100644 --- a/packages/connector/src/scenes/selecting/signers.ts +++ b/packages/connector/src/scenes/selecting/signers.ts @@ -30,7 +30,7 @@ export function generateSignersScene( "Select a Chain", html` - ${wallet.name} + ${wallet.name} ${wallet.name} ${repeat( wallet.signers, @@ -38,7 +38,7 @@ export function generateSignersScene( (signer) => html` { + @click=${() => { onSignerSelected(wallet, signer); }} > diff --git a/packages/connector/src/scenes/selecting/wallets.ts b/packages/connector/src/scenes/selecting/wallets.ts index d7b5ee54d..7d385fa82 100644 --- a/packages/connector/src/scenes/selecting/wallets.ts +++ b/packages/connector/src/scenes/selecting/wallets.ts @@ -1,6 +1,7 @@ import { ccc } from "@ckb-ccc/ccc"; import { html } from "lit"; import { repeat } from "lit/directives/repeat.js"; +import { KHIE_SVG } from "../../assets/khie.svg.js"; export function generateWalletsScene( wallets: ccc.WalletWithSigners[], @@ -12,24 +13,36 @@ export function generateWalletsScene( ) { return [ "Connect Wallet", - repeat( - wallets, - (wallet) => wallet.name, - (wallet) => html` - { - if (wallet.signers.length === 1) { - onSignerSelected(wallet, wallet.signers[0]); - } else { - onWalletSelected(wallet); - } - }} - > - ${wallet.name} - ${wallet.name} - - `, - ), + html` + { + event.currentTarget?.dispatchEvent( + new Event("select-khie", { bubbles: true, composed: true }), + ); + }} + > + Khie + Khie + + ${repeat( + wallets, + (wallet) => wallet.name, + (wallet) => html` + { + if (wallet.signers.length === 1) { + onSignerSelected(wallet, wallet.signers[0]); + } else { + onWalletSelected(wallet); + } + }} + > + ${wallet.name} + ${wallet.name} + + `, + )} + `, ]; } diff --git a/packages/connector/src/signers/index.ts b/packages/connector/src/signers/index.ts index 2745c29ac..b0e231683 100644 --- a/packages/connector/src/signers/index.ts +++ b/packages/connector/src/signers/index.ts @@ -9,7 +9,6 @@ export class SignersController { constructor( private readonly host: ReactiveControllerHost & { client: ccc.Client; - preferredNetworks?: ccc.NetworkPreference[]; name?: string; icon?: string; refreshSigner: () => void; diff --git a/packages/connector/vitest.config.mts b/packages/connector/vitest.config.mts new file mode 100644 index 000000000..ca2dc9bfc --- /dev/null +++ b/packages/connector/vitest.config.mts @@ -0,0 +1,11 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + root: import.meta.dirname, + test: { + include: ["src/**/*.test.ts"], + coverage: { + include: ["src/**/*.ts"], + }, + }, +}); diff --git a/packages/core/src/advancedBarrel.ts b/packages/core/src/advancedBarrel.ts index 11afd0c38..83dad9a85 100644 --- a/packages/core/src/advancedBarrel.ts +++ b/packages/core/src/advancedBarrel.ts @@ -4,3 +4,4 @@ export * from "./ckb/advanced.js"; export * from "./client/advanced.js"; export * from "./hasher/advanced.js"; export * from "./jsonRpc/advanced.js"; +export * from "./signer/advanced.js"; diff --git a/packages/core/src/jsonRpc/transports/transport.ts b/packages/core/src/jsonRpc/transports/transport.ts index 2cabb0545..304d68e7e 100644 --- a/packages/core/src/jsonRpc/transports/transport.ts +++ b/packages/core/src/jsonRpc/transports/transport.ts @@ -7,18 +7,30 @@ export type JsonRpcPayload = { params: unknown[] | Record; }; -export type JsonRpcError = { +export type JsonRpcErrorLike = { code: number; message: string; data?: Data; }; -export type JsonRpcResponse = { +export class JsonRpcError extends Error { + readonly code: number; + readonly data?: Data; + + constructor(error: JsonRpcErrorLike) { + super(error.message); + this.name = "JsonRpcError"; + this.code = error.code; + this.data = error.data; + } +} + +export type JsonRpcResponse = { id: JsonRpcId; jsonrpc: "2.0"; } & ( | { result: Result; error?: never } - | { result?: never; error: JsonRpcError } + | { result?: never; error: JsonRpcErrorLike } ); export interface JsonRpcTransport { diff --git a/packages/core/src/signer/advanced.ts b/packages/core/src/signer/advanced.ts new file mode 100644 index 000000000..b2c1ad517 --- /dev/null +++ b/packages/core/src/signer/advanced.ts @@ -0,0 +1 @@ +export * from "./jsonRpc/transformers.js"; diff --git a/packages/core/src/signer/index.ts b/packages/core/src/signer/index.ts index 350b487e8..835b95c06 100644 --- a/packages/core/src/signer/index.ts +++ b/packages/core/src/signer/index.ts @@ -3,5 +3,6 @@ export * from "./ckb/index.js"; export * from "./doge/index.js"; export * from "./dummy/index.js"; export * from "./evm/index.js"; +export * from "./jsonRpc/index.js"; export * from "./nostr/index.js"; export * from "./signer/index.js"; diff --git a/packages/core/src/signer/jsonRpc/handler.test.ts b/packages/core/src/signer/jsonRpc/handler.test.ts new file mode 100644 index 000000000..c0a2a85b4 --- /dev/null +++ b/packages/core/src/signer/jsonRpc/handler.test.ts @@ -0,0 +1,109 @@ +import { describe, expect, it, vi } from "vitest"; +import { ClientPublicTestnet } from "../../client/index.js"; +import { JsonRpcError, type JsonRpcPayload } from "../../jsonRpc/index.js"; +import { SignerSignType, SignerType, type Signer } from "../signer/index.js"; +import { buildSignerJsonRpcHandler } from "./index.js"; +import { SignerJsonRpcTransformers } from "./transformers.js"; + +function payload(method: string, params: unknown[] = []): JsonRpcPayload { + return { id: 0, jsonrpc: "2.0", method, params }; +} + +function mockSigner(overrides: Partial = {}) { + return { + client: new ClientPublicTestnet(), + connect: vi.fn(async () => {}), + isConnected: vi.fn(async () => true), + signType: SignerSignType.CkbSecp256k1, + type: SignerType.CKB, + ...overrides, + } as unknown as Signer; +} + +describe("buildSignerJsonRpcHandler", () => { + it("returns signer information with application metadata", async () => { + const signer = mockSigner(); + const handler = buildSignerJsonRpcHandler({ + connect: async () => signer, + confirmRequest: async () => true, + getSigner: () => signer, + getSignerMetadata: () => ({ name: "Test wallet", icon: "test.svg" }), + }); + + expect(handler(payload("get_info"))).toEqual({ + type: SignerType.CKB, + sign_type: SignerSignType.CkbSecp256k1, + name: "Test wallet", + icon: "test.svg", + }); + }); + + it("confirms and connects the requested network", async () => { + const signerConnect = vi.fn(async () => {}); + const signer = mockSigner({ connect: signerConnect }); + const connect = vi.fn(async () => signer); + const confirmRequest = vi.fn(async () => true); + const handler = buildSignerJsonRpcHandler({ + connect, + confirmRequest, + getSigner: () => signer, + }); + + await expect( + handler(payload("connect", ["ckb-testnet"])), + ).resolves.toBeNull(); + expect(confirmRequest).toHaveBeenCalledWith({ + method: "connect", + networkId: "ckb-testnet", + }); + expect(connect).toHaveBeenCalledWith("ckb-testnet"); + expect(signerConnect).not.toHaveBeenCalled(); + }); + + it("validates methods and parameters with JSON-RPC errors", async () => { + const signer = mockSigner(); + const handler = buildSignerJsonRpcHandler({ + connect: async () => signer, + confirmRequest: async () => true, + getSigner: () => signer, + }); + + expect(() => handler(payload("unknown"))).toThrow( + new JsonRpcError({ + code: -32601, + message: "Unsupported method: unknown", + }), + ); + await expect(handler(payload("connect"))).rejects.toMatchObject({ + code: -32602, + }); + }); + + it("does not sign rejected requests", async () => { + const message = Uint8Array.from([1, 2]); + const signMessageRaw = vi.fn(); + const signer = mockSigner({ signMessageRaw }); + const confirmRequest = vi.fn(async () => false); + const handler = buildSignerJsonRpcHandler({ + connect: async () => signer, + confirmRequest, + getSigner: () => signer, + }); + + const request = handler( + payload("sign_message", [SignerJsonRpcTransformers.messageFrom(message)]), + ); + + await expect(request).rejects.toEqual( + new JsonRpcError({ + code: -32003, + message: "User rejected request", + }), + ); + expect(confirmRequest).toHaveBeenCalledWith({ + method: "sign_message", + message: { type: "bytes", value: "0x0102" }, + }); + expect(signMessageRaw).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/core/src/signer/jsonRpc/handler.ts b/packages/core/src/signer/jsonRpc/handler.ts new file mode 100644 index 000000000..4f7729fe4 --- /dev/null +++ b/packages/core/src/signer/jsonRpc/handler.ts @@ -0,0 +1,217 @@ +import type { Transaction } from "../../ckb/index.js"; +import { JsonRpcTransformers } from "../../client/jsonRpc/advanced.js"; +import { JsonRpcError, type JsonRpcPayload } from "../../jsonRpc/index.js"; +import type { Signer } from "../signer/index.js"; +import { signerJsonRpcNetworkIdFromAddressPrefix } from "./network.js"; +import { + SignerJsonRpcTransformers, + type SignerJsonRpcInfo, + type SignerJsonRpcMessageToSign, +} from "./transformers.js"; + +export type SignerJsonRpcConfirmation = + | { method: "connect"; networkId: string } + | { method: "sign_message"; message: SignerJsonRpcMessageToSign } + | { method: "sign_transaction"; transaction: Transaction }; + +export type SignerJsonRpcHandlerConfig = { + getSigner: () => Signer | undefined; + getSignerMetadata?: () => Pick; + /** Connects the requested network and returns its connected Signer. */ + connect: (networkId: string) => Promise; + confirmRequest: (request: SignerJsonRpcConfirmation) => Promise; +}; + +export type SignerJsonRpcHandler = (payload: JsonRpcPayload) => unknown; + +export function buildSignerJsonRpcHandler({ + connect, + confirmRequest, + getSigner, + getSignerMetadata = () => ({}), +}: SignerJsonRpcHandlerConfig): SignerJsonRpcHandler { + const requireSigner = () => { + const signer = getSigner(); + if (!signer) { + throw new JsonRpcError({ + code: -32000, + message: "No signer is connected", + }); + } + + return signer; + }; + + const handlers = new Map([ + [ + "get_info", + (payload) => { + requireParams(payload, 0); + return buildSignerInfo(requireSigner(), getSignerMetadata()); + }, + ], + [ + "connect", + async (payload) => { + const [networkId] = requireParams(payload, 1); + if (typeof networkId !== "string" || !networkId) { + throw new JsonRpcError({ + code: -32602, + message: "Invalid network ID", + }); + } + + await requireConfirmation(confirmRequest, { + method: "connect", + networkId, + }); + + const signer = await connect(networkId); + const actualNetworkId = signerJsonRpcNetworkIdFromAddressPrefix( + signer.client.addressPrefix, + ); + if (actualNetworkId !== networkId) { + throw new JsonRpcError({ + code: -32002, + message: `Signer uses ${actualNetworkId}, expected ${networkId}`, + }); + } + + return null; + }, + ], + [ + "get_scripts", + async (payload) => { + requireParams(payload, 0); + return (await requireSigner().getAddressObjs()).map(({ script }) => + JsonRpcTransformers.scriptFrom(script), + ); + }, + ], + [ + "get_native_address", + (payload) => { + requireParams(payload, 0); + return requireSigner().getInternalAddress(); + }, + ], + [ + "get_identity", + (payload) => { + requireParams(payload, 0); + return requireSigner().getIdentity(); + }, + ], + [ + "sign_message", + async (payload) => { + const [message] = requireParams(payload, 1); + const parsedMessage = parseMessageParam(message); + await requireConfirmation(confirmRequest, { + method: "sign_message", + message: SignerJsonRpcTransformers.messageFrom(parsedMessage), + }); + return requireSigner().signMessageRaw(parsedMessage); + }, + ], + [ + "prepare_transaction", + async (payload) => { + const [transaction] = requireParams(payload, 1); + return JsonRpcTransformers.transactionFrom( + await requireSigner().prepareTransaction( + parseTransactionParam(transaction), + ), + ); + }, + ], + [ + "sign_transaction", + async (payload) => { + const [transaction] = requireParams(payload, 1); + const parsedTransaction = parseTransactionParam(transaction); + await requireConfirmation(confirmRequest, { + method: "sign_transaction", + transaction: parsedTransaction, + }); + return JsonRpcTransformers.transactionFrom( + await requireSigner().signOnlyTransaction(parsedTransaction), + ); + }, + ], + ]); + + return (payload: JsonRpcPayload) => { + const handler = handlers.get(payload.method); + if (!handler) { + throw new JsonRpcError({ + code: -32601, + message: `Unsupported method: ${payload.method}`, + }); + } + + return handler(payload); + }; +} + +async function requireConfirmation( + confirmRequest: SignerJsonRpcHandlerConfig["confirmRequest"], + request: SignerJsonRpcConfirmation, +) { + if (!(await confirmRequest(request))) { + throw new JsonRpcError({ + code: -32003, + message: "User rejected request", + }); + } +} + +function parseMessageParam(message: unknown) { + try { + return SignerJsonRpcTransformers.messageTo(message); + } catch (cause) { + throw new JsonRpcError({ + code: -32602, + message: + cause instanceof Error ? cause.message : "Invalid signer message", + }); + } +} + +function parseTransactionParam(transaction: unknown) { + try { + return JsonRpcTransformers.transactionTo( + transaction as Parameters[0], + ); + } catch (cause) { + throw new JsonRpcError({ + code: -32602, + message: + cause instanceof Error ? cause.message : "Invalid signer transaction", + }); + } +} + +function requireParams(payload: JsonRpcPayload, count: number) { + if (!Array.isArray(payload.params) || payload.params.length !== count) { + throw new JsonRpcError({ + code: -32602, + message: `${payload.method} expects ${count} parameter${count === 1 ? "" : "s"}`, + }); + } + + return payload.params; +} + +function buildSignerInfo( + signer: Signer, + metadata: Pick, +) { + return SignerJsonRpcTransformers.infoFrom({ + type: signer.type, + signType: signer.signType, + name: metadata.name, + icon: metadata.icon, + }); +} diff --git a/packages/core/src/signer/jsonRpc/index.test.ts b/packages/core/src/signer/jsonRpc/index.test.ts new file mode 100644 index 000000000..06e8a724b --- /dev/null +++ b/packages/core/src/signer/jsonRpc/index.test.ts @@ -0,0 +1,213 @@ +import { describe, expect, it, vi } from "vitest"; +import { ClientPublicTestnet } from "../../client/index.js"; +import { + JsonRpcPayload, + JsonRpcResponse, + JsonRpcTransport, +} from "../../jsonRpc/index.js"; +import { SignerSignType, SignerType } from "../signer/index.js"; +import { SignerJsonRpc } from "./index.js"; +import { SignerJsonRpcTransformers } from "./transformers.js"; + +function response(payload: JsonRpcPayload, result: unknown): JsonRpcResponse { + return { + jsonrpc: "2.0", + id: payload.id, + result, + }; +} + +describe("SignerJsonRpc", () => { + it("serializes signer info with snake-case keys", () => { + expect( + SignerJsonRpcTransformers.infoFrom({ + type: SignerType.CKB, + signType: SignerSignType.CkbSecp256k1, + name: "Test wallet", + }), + ).toEqual({ + type: SignerType.CKB, + sign_type: SignerSignType.CkbSecp256k1, + name: "Test wallet", + }); + }); + + it("loads info before explicitly connecting", async () => { + const client = new ClientPublicTestnet(); + const requests: Array<[string, unknown]> = []; + const transport: JsonRpcTransport = { + async request(payload) { + requests.push([payload.method, payload.params]); + if (payload.method === "connect") { + return response(payload, null); + } + + return response(payload, { + type: SignerType.CKB, + sign_type: SignerSignType.CkbSecp256k1, + name: "Test wallet", + icon: "https://example.com/icon.png", + }); + }, + }; + const signer = await SignerJsonRpc.new(client, { transport }); + + expect(requests).toEqual([["get_info", []]]); + await expect(signer.isConnected()).resolves.toBe(false); + expect(signer.name).toBe("Test wallet"); + expect(signer.icon).toBe("https://example.com/icon.png"); + + await signer.connect(); + + expect(requests).toEqual([ + ["get_info", []], + ["connect", ["ckb-testnet"]], + ]); + await expect(signer.isConnected()).resolves.toBe(true); + }); + + it("shares concurrent connect requests", async () => { + let resolveConnect = () => {}; + const connectPending = new Promise((resolve) => { + resolveConnect = resolve; + }); + let connectRequests = 0; + const transport: JsonRpcTransport = { + async request(payload) { + if (payload.method === "connect") { + connectRequests += 1; + await connectPending; + return response(payload, null); + } + + return response(payload, { + type: SignerType.CKB, + sign_type: SignerSignType.CkbSecp256k1, + }); + }, + }; + const signer = await SignerJsonRpc.new(new ClientPublicTestnet(), { + transport, + }); + + const first = signer.connect(); + const second = signer.connect(); + + expect(second).toBe(first); + await vi.waitFor(() => expect(connectRequests).toBe(1)); + resolveConnect(); + await first; + await signer.connect(); + expect(connectRequests).toBe(1); + }); + + it("loads scripts using the local client address prefix", async () => { + const client = new ClientPublicTestnet(); + const methods: string[] = []; + const transport: JsonRpcTransport = { + async request(payload) { + methods.push(payload.method); + return response( + payload, + payload.method === "get_info" + ? { + type: SignerType.CKB, + sign_type: SignerSignType.CkbSecp256k1, + } + : [ + { + code_hash: `0x${"00".repeat(32)}`, + hash_type: "type", + args: "0x", + }, + ], + ); + }, + }; + const signer = await SignerJsonRpc.new(client, { transport }); + + const [address] = await signer.getAddressObjs(); + + expect(address.prefix).toBe(client.addressPrefix); + expect(address.script.codeHash).toBe(`0x${"00".repeat(32)}`); + expect(methods).toEqual(["get_info", "get_scripts"]); + }); + + it("runs disconnect cleanup before notifying replacement", async () => { + const close = vi.fn(async () => {}); + const order: string[] = []; + const disconnectHandler = vi.fn(async () => { + order.push("disconnect"); + }); + const replaced = vi.fn(() => { + order.push("replaced"); + }); + const transport: JsonRpcTransport & { close(): Promise } = { + close, + async request(payload) { + return response(payload, { + type: SignerType.CKB, + sign_type: SignerSignType.CkbSecp256k1, + }); + }, + }; + const signer = await SignerJsonRpc.new(new ClientPublicTestnet(), { + transport, + disconnectHandler, + }); + await signer.connect(); + signer.onReplaced(replaced); + + await Promise.all([signer.disconnect(), signer.disconnect()]); + + expect(close).not.toHaveBeenCalled(); + expect(disconnectHandler).toHaveBeenCalledOnce(); + expect(replaced).toHaveBeenCalledOnce(); + expect(order).toEqual(["disconnect", "replaced"]); + await expect(signer.isConnected()).resolves.toBe(false); + + await signer.connect(); + signer.onReplaced(replaced); + await signer.disconnect(); + + expect(disconnectHandler).toHaveBeenCalledTimes(2); + expect(replaced).toHaveBeenCalledTimes(2); + expect(order).toEqual(["disconnect", "replaced", "disconnect", "replaced"]); + await expect(signer.isConnected()).resolves.toBe(false); + }); + + it("notifies all replacement listeners exactly once", async () => { + const listener = vi.fn(); + const otherListener = vi.fn(); + const removedListener = vi.fn(); + const nextListener = vi.fn(); + const transport: JsonRpcTransport = { + async request(payload) { + return response(payload, { + type: SignerType.CKB, + sign_type: SignerSignType.CkbSecp256k1, + }); + }, + }; + const signer = await SignerJsonRpc.new(new ClientPublicTestnet(), { + transport, + }); + + await signer.connect(); + signer.onReplaced(listener); + signer.onReplaced(otherListener); + const remove = signer.onReplaced(removedListener); + remove(); + signer.replace(); + + signer.onReplaced(nextListener); + await signer.connect(); + signer.replace(); + + expect(listener).toHaveBeenCalledOnce(); + expect(otherListener).toHaveBeenCalledOnce(); + expect(removedListener).not.toHaveBeenCalled(); + expect(nextListener).toHaveBeenCalledOnce(); + await expect(signer.isConnected()).resolves.toBe(false); + }); +}); diff --git a/packages/core/src/signer/jsonRpc/index.ts b/packages/core/src/signer/jsonRpc/index.ts new file mode 100644 index 000000000..ab960f714 --- /dev/null +++ b/packages/core/src/signer/jsonRpc/index.ts @@ -0,0 +1,2 @@ +export * from "./handler.js"; +export * from "./signer.js"; diff --git a/packages/core/src/signer/jsonRpc/network.test.ts b/packages/core/src/signer/jsonRpc/network.test.ts new file mode 100644 index 000000000..1be263a48 --- /dev/null +++ b/packages/core/src/signer/jsonRpc/network.test.ts @@ -0,0 +1,19 @@ +import { describe, expect, it } from "vitest"; +import { signerJsonRpcNetworkIdFromAddressPrefix } from "./network.js"; + +describe("signerJsonRpcNetworkIdFromAddressPrefix", () => { + it.each([ + ["ckb", "ckb-mainnet"], + ["ckt", "ckb-testnet"], + ])("maps %s to %s", (addressPrefix, networkId) => { + expect(signerJsonRpcNetworkIdFromAddressPrefix(addressPrefix)).toBe( + networkId, + ); + }); + + it("rejects unsupported address prefixes", () => { + expect(() => signerJsonRpcNetworkIdFromAddressPrefix("custom")).toThrow( + "Unsupported address prefix: custom", + ); + }); +}); diff --git a/packages/core/src/signer/jsonRpc/network.ts b/packages/core/src/signer/jsonRpc/network.ts new file mode 100644 index 000000000..2e2438541 --- /dev/null +++ b/packages/core/src/signer/jsonRpc/network.ts @@ -0,0 +1,15 @@ +const CKB_MAINNET_NETWORK_ID = "ckb-mainnet"; +const CKB_TESTNET_NETWORK_ID = "ckb-testnet"; + +// TODO: In the next major version, read the network ID directly from Client +// instead of inferring it from addressPrefix. +export function signerJsonRpcNetworkIdFromAddressPrefix(addressPrefix: string) { + if (addressPrefix === "ckb") { + return CKB_MAINNET_NETWORK_ID; + } + if (addressPrefix === "ckt") { + return CKB_TESTNET_NETWORK_ID; + } + + throw new Error(`Unsupported address prefix: ${addressPrefix}`); +} diff --git a/packages/core/src/signer/jsonRpc/signer.ts b/packages/core/src/signer/jsonRpc/signer.ts new file mode 100644 index 000000000..d2dd30c9a --- /dev/null +++ b/packages/core/src/signer/jsonRpc/signer.ts @@ -0,0 +1,181 @@ +import { Address } from "../../address/index.js"; +import { Script } from "../../ckb/index.js"; +import { Client } from "../../client/index.js"; +import { + JsonRpcScript, + JsonRpcTransformers, +} from "../../client/jsonRpc/advanced.js"; +import { RequestorJsonRpc } from "../../jsonRpc/index.js"; +import { Signer } from "../signer/index.js"; +import { signerJsonRpcNetworkIdFromAddressPrefix } from "./network.js"; +import { + SignerJsonRpcInfo, + SignerJsonRpcTransformers, +} from "./transformers.js"; + +export type SignerJsonRpcConfig = Omit< + Parameters[0], + "onError" +> & { + /** Cleans up integration-owned resources before replacement is announced. */ + disconnectHandler?: () => PromiseLike | void; +}; + +export class SignerJsonRpc extends Signer { + private connecting?: Promise; + private disconnecting?: Promise; + private readonly replacedListeners = new Set<() => void>(); + + private constructor( + client: Client, + readonly requestor: RequestorJsonRpc, + private info: SignerJsonRpcInfo, + private readonly disconnectHandler?: () => PromiseLike | void, + ) { + super(client); + } + + /** Creates a Signer that borrows an existing Transport. */ + static async new(client: Client, config: SignerJsonRpcConfig) { + const { disconnectHandler, ...requestorConfig } = config; + const requestor = RequestorJsonRpc.new(requestorConfig); + const info = (await requestor.request( + "get_info", + [], + [], + SignerJsonRpcTransformers.infoTo, + )) as SignerJsonRpcInfo; + + return new SignerJsonRpc(client, requestor, info, disconnectHandler); + } + + get type() { + return this.info.type; + } + + get signType() { + return this.info.signType; + } + + get url() { + return this.requestor.url; + } + + get name() { + return this.info.name; + } + + get icon() { + return this.info.icon; + } + + connect(): Promise { + if (this.disconnecting) { + return this.disconnecting.then(() => this.connect()); + } + if (this.connecting) { + return this.connecting; + } + + const connecting = this.requestConnect( + signerJsonRpcNetworkIdFromAddressPrefix(this.client.addressPrefix), + ).catch((cause: unknown) => { + if (this.connecting === connecting) { + this.connecting = undefined; + } + throw cause; + }); + this.connecting = connecting; + return connecting; + } + + disconnect(): Promise { + if (this.disconnecting) { + return this.disconnecting; + } + if (!this.connecting) { + return Promise.resolve(); + } + + this.connecting = undefined; + this.disconnecting = Promise.resolve() + .then(() => this.disconnectHandler?.()) + .finally(() => { + this.replace(); + this.disconnecting = undefined; + }); + return this.disconnecting; + } + + onReplaced(listener: () => void) { + this.replacedListeners.add(listener); + return () => this.replacedListeners.delete(listener); + } + + replace() { + this.connecting = undefined; + const listeners = [...this.replacedListeners]; + this.replacedListeners.clear(); + listeners.forEach((listener) => listener()); + } + + async isConnected() { + return ( + this.connecting?.then( + () => true, + () => false, + ) ?? false + ); + } + + getScripts = this.buildSender("get_scripts", [], (scripts: JsonRpcScript[]) => + scripts.map((script) => JsonRpcTransformers.scriptTo(script)), + ) as () => Promise; + + async getAddressObjs() { + return (await this.getScripts()).map((script) => + Address.fromScript(script, this.client), + ); + } + + getInternalAddress = this.buildSender( + "get_native_address", + [], + ) as Signer["getInternalAddress"]; + + getIdentity = this.buildSender("get_identity", []) as Signer["getIdentity"]; + + signMessageRaw = this.buildSender("sign_message", [ + SignerJsonRpcTransformers.messageFrom, + ]) as Signer["signMessageRaw"]; + + prepareTransaction = this.buildSender( + "prepare_transaction", + [JsonRpcTransformers.transactionFrom], + JsonRpcTransformers.transactionTo, + ) as Signer["prepareTransaction"]; + + signOnlyTransaction = this.buildSender( + "sign_transaction", + [JsonRpcTransformers.transactionFrom], + JsonRpcTransformers.transactionTo, + ) as Signer["signOnlyTransaction"]; + + buildSender( + rpcMethod: Parameters[0], + inTransformers?: Parameters[2], + outTransformer?: Parameters[3], + ): (...request: unknown[]) => Promise { + return async (...request: unknown[]) => + this.requestor.request( + rpcMethod, + request, + inTransformers, + outTransformer, + ); + } + + private requestConnect = this.buildSender("connect", []) as ( + networkId: string, + ) => Promise; +} diff --git a/packages/core/src/signer/jsonRpc/transformers.ts b/packages/core/src/signer/jsonRpc/transformers.ts new file mode 100644 index 000000000..75fb1d2cd --- /dev/null +++ b/packages/core/src/signer/jsonRpc/transformers.ts @@ -0,0 +1,67 @@ +import { BytesLike, bytesFrom } from "../../bytes/index.js"; +import { Hex, hexFrom } from "../../hex/index.js"; +import { SignerSignType, SignerType } from "../signer/index.js"; + +export type SignerJsonRpcInfo = { + type: SignerType; + signType: SignerSignType; + name?: string; + icon?: string; +}; + +export type SignerJsonRpcInfoPayload = { + type: SignerType; + sign_type: SignerSignType; + name?: string; + icon?: string; +}; + +export type SignerJsonRpcMessageToSign = + { type: "string"; value: string } | { type: "bytes"; value: Hex }; + +export class SignerJsonRpcTransformers { + static infoFrom(info: SignerJsonRpcInfo): SignerJsonRpcInfoPayload { + return { + type: info.type, + sign_type: info.signType, + name: info.name, + icon: info.icon, + }; + } + + static infoTo(info: SignerJsonRpcInfoPayload): SignerJsonRpcInfo { + return { + type: info.type, + signType: info.sign_type, + name: info.name, + icon: info.icon, + }; + } + + static messageFrom(message: string | BytesLike): SignerJsonRpcMessageToSign { + if (typeof message === "string") { + return { type: "string", value: message }; + } + + return { type: "bytes", value: hexFrom(message) }; + } + + static messageTo(message: unknown): string | Uint8Array { + if (!isRecord(message) || typeof message.value !== "string") { + throw new Error("Invalid signer message"); + } + + if (message.type === "string") { + return message.value; + } + if (message.type === "bytes") { + return bytesFrom(message.value); + } + + throw new Error("Invalid signer message type"); + } +} + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null; +} diff --git a/packages/core/src/utils/abortSignal.test.ts b/packages/core/src/utils/abortSignal.test.ts new file mode 100644 index 000000000..2b97c07e7 --- /dev/null +++ b/packages/core/src/utils/abortSignal.test.ts @@ -0,0 +1,32 @@ +import { describe, expect, it } from "vitest"; +import { abortSignalAny } from "./abortSignal.js"; + +describe("abortSignalAny", () => { + it("combines abort signals when AbortSignal.any is unavailable", () => { + const original = Object.getOwnPropertyDescriptor(AbortSignal, "any"); + + try { + Object.defineProperty(AbortSignal, "any", { + configurable: true, + value: undefined, + writable: true, + }); + + const first = new AbortController(); + const second = new AbortController(); + const combined = abortSignalAny([first.signal, second.signal]); + const reason = new Error("cancelled"); + + second.abort(reason); + + expect(combined.aborted).toBe(true); + expect(combined.reason).toBe(reason); + } finally { + if (original) { + Object.defineProperty(AbortSignal, "any", original); + } else { + Reflect.deleteProperty(AbortSignal, "any"); + } + } + }); +}); diff --git a/packages/core/src/utils/abortSignal.ts b/packages/core/src/utils/abortSignal.ts new file mode 100644 index 000000000..13ced610d --- /dev/null +++ b/packages/core/src/utils/abortSignal.ts @@ -0,0 +1,42 @@ +/** + * Creates a signal that aborts when any of the input signals aborts. + * + * Uses the native `AbortSignal.any` when available and falls back to an + * `AbortController` implementation for older runtimes. + * + * @public + */ +export function abortSignalAny(signals: readonly AbortSignal[]): AbortSignal { + if (typeof AbortSignal.any === "function") { + return AbortSignal.any([...signals]); + } + + const controller = new AbortController(); + const listeners = new Map void>(); + const cleanup = () => { + for (const [signal, listener] of listeners) { + signal.removeEventListener("abort", listener); + } + listeners.clear(); + }; + const abortFrom = (signal: AbortSignal) => { + cleanup(); + controller.abort(signal.reason); + }; + + for (const signal of signals) { + if (signal.aborted) { + abortFrom(signal); + break; + } + if (listeners.has(signal)) { + continue; + } + + const listener = () => abortFrom(signal); + listeners.set(signal, listener); + signal.addEventListener("abort", listener, { once: true }); + } + + return controller.signal; +} diff --git a/packages/core/src/utils/index.ts b/packages/core/src/utils/index.ts index aae014e40..462422f2b 100644 --- a/packages/core/src/utils/index.ts +++ b/packages/core/src/utils/index.ts @@ -1,6 +1,7 @@ import { Zero } from "../fixedPoint/index.js"; import { NumLike, numFrom, numToHex, type Num } from "../num/index.js"; +export * from "./abortSignal.js"; export * from "./constructor.js"; export * from "./owner/index.js"; export * from "./proxy.js"; diff --git a/packages/core/src/utils/owner/owner.test.ts b/packages/core/src/utils/owner/owner.test.ts index a3d8238ae..3d47ad396 100644 --- a/packages/core/src/utils/owner/owner.test.ts +++ b/packages/core/src/utils/owner/owner.test.ts @@ -18,8 +18,11 @@ describe("Owner", () => { const dispose = vi.fn(); const source = new OwnerUnique("value", dispose); + expect(source.isValid).toBe(true); const mapped = source.map((value) => ({ value })); + expect(source.isValid).toBe(false); + expect(mapped.isValid).toBe(true); expect(mapped.value).toEqual({ value: "value" }); expect(() => source.value).toThrow( "Cannot access a moved or disposed Owner", @@ -27,7 +30,9 @@ describe("Owner", () => { await source.dispose(); expect(dispose).not.toHaveBeenCalled(); - await mapped.dispose(); + const disposing = mapped.dispose(); + expect(mapped.isValid).toBe(false); + await disposing; expect(dispose).toHaveBeenCalledOnce(); expect(dispose).toHaveBeenCalledWith("value"); }); diff --git a/packages/core/src/utils/owner/owner.ts b/packages/core/src/utils/owner/owner.ts index 1975d9d96..787417864 100644 --- a/packages/core/src/utils/owner/owner.ts +++ b/packages/core/src/utils/owner/owner.ts @@ -45,6 +45,11 @@ export abstract class Owner { private readonly [ownerId] = true; private disposing?: Promise; + /** Whether this ownership claim has not been moved or disposed. */ + get isValid(): boolean { + return this.disposing === undefined; + } + /** Returns whether a value implements CCC Owner semantics. */ static is(value: unknown): value is Owner { return ( diff --git a/packages/docs/content/docs/guides/connect-wallets.mdx b/packages/docs/content/docs/guides/connect-wallets.mdx index 7e46cded8..a0894b51f 100644 --- a/packages/docs/content/docs/guides/connect-wallets.mdx +++ b/packages/docs/content/docs/guides/connect-wallets.mdx @@ -151,7 +151,7 @@ Returns the full wallet state and control functions. Use it when you need to con | `open` | `() => void` | Open the connector modal | | `close` | `() => void` | Close the connector modal | | `disconnect` | `() => void` | Disconnect the current wallet | -| `setClient` | `(client: ccc.Client) => void` | Switch to a different CKB client (mainnet/testnet/RPC) | +| `setClient` | `(owner: ccc.Owner) => void` | Transfer and switch to a different CKB client | | `client` | `ccc.Client` | Current CKB client (defaults to `ClientPublicTestnet`) | | `wallet` | `ccc.Wallet \| undefined` | Currently connected wallet (`name`, `icon`) | | `signerInfo` | `ccc.SignerInfo \| undefined` | Current signer info (`name`, `signer`) | diff --git a/packages/docs/content/docs/guides/connect-wallets.zh.mdx b/packages/docs/content/docs/guides/connect-wallets.zh.mdx index 0eb324cd4..76d0abf1a 100644 --- a/packages/docs/content/docs/guides/connect-wallets.zh.mdx +++ b/packages/docs/content/docs/guides/connect-wallets.zh.mdx @@ -149,7 +149,7 @@ icon: Wallet | `open` | `() => void` | 打开连接器弹窗 | | `close` | `() => void` | 关闭连接器弹窗 | | `disconnect` | `() => void` | 断开当前钱包 | -| `setClient` | `(client: ccc.Client) => void` | 切换 CKB Client(主网/测试网/自定义 RPC) | +| `setClient` | `(owner: ccc.Owner) => void` | 转移并切换 CKB Client | | `client` | `ccc.Client` | 当前 CKB Client(默认为 `ClientPublicTestnet`) | | `wallet` | `ccc.Wallet \| undefined` | 当前已连接的钱包(含 `name`、`icon`) | | `signerInfo` | `ccc.SignerInfo \| undefined` | 当前 Signer 信息(含 `name`、`signer`) | diff --git a/packages/docs/content/docs/meta.json b/packages/docs/content/docs/meta.json index 7c919c5f2..9f4d0f44c 100644 --- a/packages/docs/content/docs/meta.json +++ b/packages/docs/content/docs/meta.json @@ -19,6 +19,8 @@ "guides/udt-tokens", "guides/spore-protocol", "guides/node-js-backend", + "---Migration---", + "migration/connector-v2", "---Examples---", "code-examples", "---AI Resources---", diff --git a/packages/docs/content/docs/migration/connector-v2.mdx b/packages/docs/content/docs/migration/connector-v2.mdx new file mode 100644 index 000000000..23fe7d7ff --- /dev/null +++ b/packages/docs/content/docs/migration/connector-v2.mdx @@ -0,0 +1,103 @@ +--- +title: Migrate to Connector 2.0 +description: Migrate Connector and Connector React to explicit resource ownership. +icon: RefreshCw +--- + +Follow the sections that match your integration. React applications using only +`ccc.Provider` usually need sections 1 and 5; direct Web Component integrations +also need sections 2–4. + +## 1. Pass an Owner to `setClient` + +Pass an `Owner` instead of a bare Client to every +`useCcc().setClient` call. + +**Why:** a bare Client does not say who must release its resources. A temporary +`setClient(new Client())` could therefore leak them. + +```diff +-setClient(new ccc.ClientPublicMainnet()); ++setClient(ccc.ClientPublicMainnet.open()); +``` + +The Provider consumes this Owner and disposes it on replacement or unmount. Do +not reuse or dispose the Owner after passing it in. If the Client must remain +caller-owned, pass the bare Client through `defaultClient` or `clientOptions` +instead. + +## 2. Own the Web Component Client outside the Connector + +Create the Client Owner in your application and assign its borrowed value to +`connector.client` before attaching the element. + +**Why:** a Web Component can be temporarily detached during a DOM move, so its +lifecycle is not a reliable final disposal boundary. + +```ts +const clientOwner = ccc.ClientPublicTestnet.open(); +connector.client = clientOwner.value; +document.body.append(connector); + +// Final application cleanup: +await clientOwner.dispose(); +``` + +## 3. Apply `select-client` requests + +Listen for `select-client` and decide whether to update the controlled +`connector.client`. + +**Why:** network and fee-rate changes are requests; the application that owns +the Client controls whether they are applied. + +```ts +connector.addEventListener("select-client", (event) => { + connector.client = (event as ccc.SelectClientEvent).client; +}); +``` + +## 4. Replace `willUpdate` with owned connection events + +Remove `ConnectorWillUpdateEvent` and React `onWillUpdate` listeners. For a +direct Web Component integration, move each +`ConnectorConnectionEvent.connectionOwner` into application state. Dispose the +previous Owner on replacement or clear, and dispose the final Owner during +application cleanup. + +**Why:** `willUpdate` only reported a render lifecycle change. It could not +transfer signer ownership or distinguish connect, replace, and clear. + +```ts +let connectionOwner: ccc.Owner | undefined; + +connector.addEventListener("connection", (event) => { + event.stopPropagation(); + const next = (event as ccc.ConnectorConnectionEvent).connectionOwner?.map( + (connection) => connection, + ); + const previous = connectionOwner; + connectionOwner = next; + void previous?.dispose(); +}); +``` + +An absent Owner means clear. `connector.disconnect()` uses this path, so +disposing the previous Owner is what closes the signer. `ccc.Provider` performs +all of this automatically. + +## 5. Remove obsolete props + +Remove `preferredNetworks` from `` and ``. + +**Why:** the compatibility property was already ignored. No replacement is +required. + +## 6. Check Client switching behavior + +Verify connect, replace, disconnect, Client switch, and final unmount. + +**Why:** named wallet selections can be rediscovered by the signers controller. +A direct connection without wallet and signer lookup names, such as Khie, +cannot be refreshed that way and is cleared when the Client changes. Reconnect +it on the new Client. diff --git a/packages/docs/content/docs/migration/connector-v2.zh.mdx b/packages/docs/content/docs/migration/connector-v2.zh.mdx new file mode 100644 index 000000000..3c5443533 --- /dev/null +++ b/packages/docs/content/docs/migration/connector-v2.zh.mdx @@ -0,0 +1,94 @@ +--- +title: 迁移到 Connector 2.0 +description: 将 Connector 和 Connector React 迁移到显式资源所有权语义。 +icon: RefreshCw +--- + +只需阅读与你的集成方式相关的章节。仅使用 `ccc.Provider` 的 React 应用通常 +只需处理第 1、5 节;直接使用 Web Component 的应用还需处理第 2–4 节。 + +## 1. 向 `setClient` 传入 Owner + +将所有传给 `useCcc().setClient` 的裸 Client 改为 `Owner`。 + +**原因:** 裸 Client 无法表达由谁释放资源,`setClient(new Client())` 这类临时 +实例因此可能泄漏。 + +```diff +-setClient(new ccc.ClientPublicMainnet()); ++setClient(ccc.ClientPublicMainnet.open()); +``` + +Provider 会消费该 Owner,并在替换 Client 或卸载时释放它。传入后不要继续复用 +或释放该 Owner。如果 Client 必须继续由调用方持有,请改为通过 `defaultClient` +或 `clientOptions` 传入裸 Client。 + +## 2. 在 Connector 外持有 Web Component Client + +在应用中创建 Client Owner,并在挂载元素前将借用值赋给 +`connector.client`。 + +**原因:** Web Component 可能因 DOM 移动而暂时脱离文档,其生命周期不能作为 +可靠的最终释放边界。 + +```ts +const clientOwner = ccc.ClientPublicTestnet.open(); +connector.client = clientOwner.value; +document.body.append(connector); + +// 应用最终清理时: +await clientOwner.dispose(); +``` + +## 3. 应用 `select-client` 请求 + +监听 `select-client`,并决定是否更新受控的 `connector.client`。 + +**原因:** 网络和费率变化是一项请求;是否应用它,应由持有 Client 的应用决定。 + +```ts +connector.addEventListener("select-client", (event) => { + connector.client = (event as ccc.SelectClientEvent).client; +}); +``` + +## 4. 用带所有权的连接事件替代 `willUpdate` + +删除 `ConnectorWillUpdateEvent` 和 React `onWillUpdate` 监听。直接使用 Web +Component 时,将每个 `ConnectorConnectionEvent.connectionOwner` 移入应用 +状态。连接替换或清空时释放上一个 Owner,并在应用最终清理时释放最后一个 +Owner。 + +**原因:** `willUpdate` 只能表示渲染生命周期发生变化,既不能转移 signer +所有权,也无法区分连接、替换和清空。 + +```ts +let connectionOwner: ccc.Owner | undefined; + +connector.addEventListener("connection", (event) => { + event.stopPropagation(); + const next = (event as ccc.ConnectorConnectionEvent).connectionOwner?.map( + (connection) => connection, + ); + const previous = connectionOwner; + connectionOwner = next; + void previous?.dispose(); +}); +``` + +Owner 为空表示清空连接。`connector.disconnect()` 也使用这条路径,因此释放上一 +个 Owner 才会关闭 signer。`ccc.Provider` 已自动完成以上处理。 + +## 5. 删除无效属性 + +从 `` 和 `` 中删除 `preferredNetworks`。 + +**原因:** 该兼容属性此前已经被忽略,不需要替代配置。 + +## 6. 检查 Client 切换行为 + +验证连接、替换、断开、切换 Client 和最终卸载。 + +**原因:** 带 wallet 与 signer 查找名称的选择可以由 signers controller 重新 +发现。Khie 这类没有查找名称的直接连接无法通过该方式刷新,会在 Client 变化时 +被清空,需要在新 Client 上重新连接。 diff --git a/packages/docs/content/docs/migration/meta.json b/packages/docs/content/docs/migration/meta.json new file mode 100644 index 000000000..65db512d3 --- /dev/null +++ b/packages/docs/content/docs/migration/meta.json @@ -0,0 +1,6 @@ +{ + "title": "Migration", + "icon": "RefreshCw", + "defaultOpen": true, + "pages": ["connector-v2"] +} diff --git a/packages/docs/content/docs/packages/core-packages/connector-react.mdx b/packages/docs/content/docs/packages/core-packages/connector-react.mdx index 8b21e1495..eb8efdf7e 100644 --- a/packages/docs/content/docs/packages/core-packages/connector-react.mdx +++ b/packages/docs/content/docs/packages/core-packages/connector-react.mdx @@ -181,8 +181,7 @@ The hook throws if called outside a `` tree. Clients passed through `defaultClient` or `clientOptions` remain owned by the caller. `setClient(owner)` transfers ownership to the Provider, which disposes -the owner on the next `setClient` call or when the Provider unmounts. Passing a bare `Client` to -`setClient` remains supported for compatibility, but is deprecated. +the owner on the next `setClient` call or when the Provider unmounts. ## `useBorrowedOrOwned()` hook diff --git a/packages/docs/content/docs/packages/core-packages/connector-react.zh.mdx b/packages/docs/content/docs/packages/core-packages/connector-react.zh.mdx index bbdd92329..ca49f22e4 100644 --- a/packages/docs/content/docs/packages/core-packages/connector-react.zh.mdx +++ b/packages/docs/content/docs/packages/core-packages/connector-react.zh.mdx @@ -177,8 +177,7 @@ const { 通过 `defaultClient` 或 `clientOptions` 传入的客户端仍由调用方持有并负责清理。 调用 `setClient(owner)` 会将所有权转移给 Provider;该 owner 会在下次调用 -`setClient` 或 Provider 卸载时清理。为兼容旧代码,`setClient` 仍可接收裸 -`Client`,但该用法已弃用。 +`setClient` 或 Provider 卸载时清理。 ## `useBorrowedOrOwned()` Hook diff --git a/packages/docs/content/docs/packages/core-packages/connector.mdx b/packages/docs/content/docs/packages/core-packages/connector.mdx index 5e49bc934..c36999cf7 100644 --- a/packages/docs/content/docs/packages/core-packages/connector.mdx +++ b/packages/docs/content/docs/packages/core-packages/connector.mdx @@ -82,10 +82,14 @@ only. const clientOwner = ccc.ClientPublicTestnet.open(); connector.client = clientOwner.value; connector.style.cssText = "display: none; z-index: 999;"; + let connectionOwner; window.addEventListener( "pagehide", - () => void clientOwner.dispose(), + () => { + void connectionOwner?.dispose(); + void clientOwner.dispose(); + }, { once: true }, ); @@ -101,9 +105,15 @@ only. connector.client = event.client; }); - connector.addEventListener("willUpdate", () => { - if (connector.signer) { - connector.signer.signer.getRecommendedAddress().then((addr) => { + connector.addEventListener("connection", (event) => { + event.stopPropagation(); + const owner = event.connectionOwner?.map((connection) => connection); + const previous = connectionOwner; + connectionOwner = owner; + void previous?.dispose(); + + if (owner) { + owner.value.signerInfo.signer.getRecommendedAddress().then((addr) => { document.getElementById("address").textContent = addr; }); } @@ -146,15 +156,25 @@ connector.addEventListener("select-client", (event) => { connector.client = (event as ccc.SelectClientEvent).client; }); -connector.addEventListener("willUpdate", () => { - console.log("Connected wallet:", connector.wallet?.name); - console.log("Signer:", connector.signer); +let connectionOwner: ccc.Owner | undefined; +connector.addEventListener("connection", (event) => { + event.stopPropagation(); + const owner = (event as ccc.ConnectorConnectionEvent).connectionOwner?.map( + (connection) => connection, + ); + const previous = connectionOwner; + connectionOwner = owner; + void previous?.dispose(); + + console.log("Connected wallet:", owner?.value.wallet.name); + console.log("Signer:", owner?.value.signerInfo); }); // `client` must be assigned before the element is connected. document.body.append(connector); -// Once the Connector can no longer use this Client, the caller releases it: +// Once these resources are no longer used, the caller releases them: +// await connectionOwner?.dispose(); // await clientOwner.dispose(); ``` diff --git a/packages/docs/content/docs/packages/core-packages/connector.zh.mdx b/packages/docs/content/docs/packages/core-packages/connector.zh.mdx index 27280e652..d72589467 100644 --- a/packages/docs/content/docs/packages/core-packages/connector.zh.mdx +++ b/packages/docs/content/docs/packages/core-packages/connector.zh.mdx @@ -78,10 +78,14 @@ Web Component 要求传入 `client` 且只借用它,不会创建或释放 Clie const clientOwner = ccc.ClientPublicTestnet.open(); connector.client = clientOwner.value; connector.style.cssText = "display: none; z-index: 999;"; + let connectionOwner; window.addEventListener( "pagehide", - () => void clientOwner.dispose(), + () => { + void connectionOwner?.dispose(); + void clientOwner.dispose(); + }, { once: true }, ); @@ -97,9 +101,15 @@ Web Component 要求传入 `client` 且只借用它,不会创建或释放 Clie connector.client = event.client; }); - connector.addEventListener("willUpdate", () => { - if (connector.signer) { - connector.signer.signer.getRecommendedAddress().then((addr) => { + connector.addEventListener("connection", (event) => { + event.stopPropagation(); + const owner = event.connectionOwner?.map((connection) => connection); + const previous = connectionOwner; + connectionOwner = owner; + void previous?.dispose(); + + if (owner) { + owner.value.signerInfo.signer.getRecommendedAddress().then((addr) => { document.getElementById("address").textContent = addr; }); } @@ -142,15 +152,25 @@ connector.addEventListener("select-client", (event) => { connector.client = (event as ccc.SelectClientEvent).client; }); -connector.addEventListener("willUpdate", () => { - console.log("已连接钱包:", connector.wallet?.name); - console.log("Signer:", connector.signer); +let connectionOwner: ccc.Owner | undefined; +connector.addEventListener("connection", (event) => { + event.stopPropagation(); + const owner = (event as ccc.ConnectorConnectionEvent).connectionOwner?.map( + (connection) => connection, + ); + const previous = connectionOwner; + connectionOwner = owner; + void previous?.dispose(); + + console.log("已连接钱包:", owner?.value.wallet.name); + console.log("Signer:", owner?.value.signerInfo); }); // `client` 必须在元素连接到文档前赋值。 document.body.append(connector); -// 确认 Connector 不会再使用该 Client 后,由调用方释放: +// 确认不再使用这些资源后,由调用方释放: +// await connectionOwner?.dispose(); // await clientOwner.dispose(); ``` diff --git a/packages/libp2p/.npmignore b/packages/libp2p/.npmignore new file mode 100644 index 000000000..7a88408aa --- /dev/null +++ b/packages/libp2p/.npmignore @@ -0,0 +1,21 @@ +node_modules/ +misc/ + +*test.js +*test.ts +*test.d.ts +*test.d.ts.map +*spec.js +*spec.ts +*spec.d.ts +*spec.d.ts.map + +tsconfig.json +tsconfig.*.json +eslint.config.mjs +.prettierrc +.prettierignore + +tsconfig.tsbuildinfo +tsconfig.*.tsbuildinfo +.github/ diff --git a/packages/libp2p/.prettierignore b/packages/libp2p/.prettierignore new file mode 100644 index 000000000..aef5d239c --- /dev/null +++ b/packages/libp2p/.prettierignore @@ -0,0 +1,15 @@ +node_modules/ + +dist/ +dist.commonjs/ + +.npmignore +.prettierrc +tsconfig.json +eslint.config.mjs +prettier.config.* + +tsconfig.tsbuildinfo +.github/ + +CHANGELOG.md diff --git a/packages/libp2p/CHANGELOG.md b/packages/libp2p/CHANGELOG.md new file mode 100644 index 000000000..0b309fd7d --- /dev/null +++ b/packages/libp2p/CHANGELOG.md @@ -0,0 +1 @@ +# @ckb-ccc/libp2p diff --git a/packages/libp2p/README.md b/packages/libp2p/README.md new file mode 100644 index 000000000..631c35b63 --- /dev/null +++ b/packages/libp2p/README.md @@ -0,0 +1,3 @@ +# @ckb-ccc/libp2p + +Pairing and JSON-RPC support for using CCC over libp2p. diff --git a/packages/libp2p/eslint.config.mjs b/packages/libp2p/eslint.config.mjs new file mode 100644 index 000000000..b6132c277 --- /dev/null +++ b/packages/libp2p/eslint.config.mjs @@ -0,0 +1,62 @@ +// @ts-check + +import eslint from "@eslint/js"; +import eslintPluginPrettierRecommended from "eslint-plugin-prettier/recommended"; +import tseslint from "typescript-eslint"; + +import { dirname } from "path"; +import { fileURLToPath } from "url"; + +export default [ + ...tseslint.config({ + files: ["**/*.ts"], + extends: [ + eslint.configs.recommended, + ...tseslint.configs.recommendedTypeChecked, + ], + rules: { + "@typescript-eslint/no-unused-vars": [ + "error", + { + args: "all", + argsIgnorePattern: "^_", + caughtErrors: "all", + caughtErrorsIgnorePattern: "^_", + destructuredArrayIgnorePattern: "^_", + varsIgnorePattern: "^_", + ignoreRestSiblings: true, + }, + ], + "@typescript-eslint/unbound-method": ["error", { ignoreStatic: true }], + "@typescript-eslint/no-unsafe-member-access": "off", + "@typescript-eslint/require-await": "off", + "@typescript-eslint/only-throw-error": [ + "error", + { + allowThrowingAny: true, + allowThrowingUnknown: true, + allowRethrowing: true, + }, + ], + "@typescript-eslint/prefer-promise-reject-errors": [ + "error", + { + allowThrowingAny: true, + allowThrowingUnknown: true, + }, + ], + "no-empty": "off", + "prefer-const": [ + "error", + { ignoreReadBeforeAssign: true, destructuring: "all" }, + ], + }, + languageOptions: { + parserOptions: { + project: true, + tsconfigRootDir: dirname(fileURLToPath(import.meta.url)), + }, + }, + }), + eslintPluginPrettierRecommended, +]; diff --git a/packages/libp2p/misc/basedirs/dist.commonjs/package.json b/packages/libp2p/misc/basedirs/dist.commonjs/package.json new file mode 100644 index 000000000..5bbefffba --- /dev/null +++ b/packages/libp2p/misc/basedirs/dist.commonjs/package.json @@ -0,0 +1,3 @@ +{ + "type": "commonjs" +} diff --git a/packages/libp2p/misc/basedirs/dist/package.json b/packages/libp2p/misc/basedirs/dist/package.json new file mode 100644 index 000000000..aead43de3 --- /dev/null +++ b/packages/libp2p/misc/basedirs/dist/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} \ No newline at end of file diff --git a/packages/libp2p/package.json b/packages/libp2p/package.json new file mode 100644 index 000000000..382b59304 --- /dev/null +++ b/packages/libp2p/package.json @@ -0,0 +1,57 @@ +{ + "name": "@ckb-ccc/libp2p", + "version": "0.0.1", + "description": "CCC support for libp2p", + "author": "Hanssen0 <0@hanssen0.com>", + "license": "MIT", + "private": false, + "homepage": "https://github.com/ckb-devrel/ccc", + "repository": { + "type": "git", + "url": "git://github.com/ckb-devrel/ccc.git" + }, + "sideEffects": false, + "main": "./dist.commonjs/index.js", + "module": "./dist/index.mjs", + "exports": { + ".": { + "import": "./dist/index.mjs", + "require": "./dist.commonjs/index.js" + }, + "./barrel": { + "import": "./dist/barrel.mjs", + "require": "./dist.commonjs/barrel.js" + }, + "./package.json": "./package.json" + }, + "scripts": { + "build": "tsdown", + "lint": "tsc --noEmit && eslint ./src", + "format": "prettier --write . && eslint --fix ./src" + }, + "devDependencies": { + "@eslint/js": "^10.0.1", + "eslint": "^10.5.0", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-prettier": "^5.5.6", + "prettier": "^3.8.4", + "prettier-plugin-organize-imports": "^4.3.0", + "tsdown": "^0.22.3", + "@typescript/native": "npm:typescript@^7.0.2", + "typescript": "npm:@typescript/typescript6@^6.0.2", + "typescript-eslint": "^8.61.1" + }, + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@ckb-ccc/core": "workspace:*", + "@libp2p/interface": "^3.3.0", + "@libp2p/interface-internal": "^3.1.12", + "@libp2p/utils": "^7.4.0", + "@multiformats/multiaddr": "^13.0.3", + "it-length-prefixed": "^11.0.1" + }, + "packageManager": "pnpm@11.8.0", + "types": "./dist.commonjs/index.d.ts" +} diff --git a/packages/libp2p/prettier.config.cjs b/packages/libp2p/prettier.config.cjs new file mode 100644 index 000000000..5e1810363 --- /dev/null +++ b/packages/libp2p/prettier.config.cjs @@ -0,0 +1,11 @@ +/** + * @see https://prettier.io/docs/configuration + * @type {import("prettier").Config} + */ +const config = { + singleQuote: false, + trailingComma: "all", + plugins: [require.resolve("prettier-plugin-organize-imports")], +}; + +module.exports = config; diff --git a/packages/libp2p/src/barrel.ts b/packages/libp2p/src/barrel.ts new file mode 100644 index 000000000..3ac3a32ee --- /dev/null +++ b/packages/libp2p/src/barrel.ts @@ -0,0 +1,4 @@ +export * from "./jsonRpcService.js"; +export * from "./jsonRpcTransport.js"; +export * from "./pairingEndpoint.js"; +export * from "./pairingService.js"; diff --git a/packages/libp2p/src/index.ts b/packages/libp2p/src/index.ts new file mode 100644 index 000000000..a34f262b9 --- /dev/null +++ b/packages/libp2p/src/index.ts @@ -0,0 +1 @@ +export * as Libp2p from "./barrel.js"; diff --git a/packages/libp2p/src/jsonRpcService.ts b/packages/libp2p/src/jsonRpcService.ts new file mode 100644 index 000000000..fb4810aa5 --- /dev/null +++ b/packages/libp2p/src/jsonRpcService.ts @@ -0,0 +1,167 @@ +import { ccc, JsonRpcError } from "@ckb-ccc/core"; +import type { Connection, PeerId, Stream } from "@libp2p/interface"; +import type { Registrar } from "@libp2p/interface-internal"; +import { lpStream } from "@libp2p/utils"; + +const DEFAULT_MAX_MESSAGE_LENGTH = 1024 * 1024; + +export type JsonRpcServiceComponents = { + registrar: Registrar; +}; + +export type JsonRpcServiceConfig = { + protocol: string; + maxMessageLength?: number; +}; + +export type JsonRpcRequest = { + peerId: PeerId; + payload: ccc.JsonRpcPayload; +}; + +export type JsonRpcRequestHandler< + Components extends JsonRpcServiceComponents = JsonRpcServiceComponents, +> = (this: JsonRpcService, request: JsonRpcRequest) => unknown; + +export abstract class JsonRpcService< + Components extends JsonRpcServiceComponents = JsonRpcServiceComponents, +> { + private readonly errorListeners = new Set<(error: Error) => void>(); + readonly maxMessageLength: number; + + constructor( + readonly components: Components, + readonly config: JsonRpcServiceConfig, + ) { + const maxMessageLength = + config.maxMessageLength ?? DEFAULT_MAX_MESSAGE_LENGTH; + if (!Number.isSafeInteger(maxMessageLength) || maxMessageLength <= 0) { + throw new Error( + "Maximum JSON-RPC message length must be a positive integer", + ); + } + + this.maxMessageLength = maxMessageLength; + } + + protected abstract handleRequest(request: JsonRpcRequest): unknown; + + onError(listener: (error: Error) => void) { + this.errorListeners.add(listener); + return () => this.errorListeners.delete(listener); + } + + async start() { + await this.components.registrar.handle( + this.config.protocol, + this.handleProtocol.bind(this), + { runOnLimitedConnection: true }, + ); + } + + async stop() { + await this.components.registrar.unhandle(this.config.protocol); + } + + private async handleProtocol(stream: Stream, connection: Connection) { + let requestAborted = false; + + try { + const rpcStream = lpStream(stream, { + maxDataLength: this.maxMessageLength, + }); + const payload = parseJsonRpcRequest( + ccc.bytesTo((await rpcStream.read()).subarray(), "utf8"), + ); + + let response: ccc.JsonRpcResponse; + try { + response = { + jsonrpc: "2.0", + id: payload.id, + result: await this.handleRequest({ + peerId: connection.remotePeer, + payload, + }), + }; + } catch (cause) { + requestAborted = isAbortError(cause); + const error = toJsonRpcError(cause); + response = { + jsonrpc: "2.0", + id: payload.id, + error: { + code: error.code, + message: error.message, + data: error.data, + }, + }; + } + + await rpcStream.write(ccc.bytesFrom(JSON.stringify(response), "utf8")); + await stream.close(); + } catch (cause) { + if (requestAborted) { + return; + } + this.handleError(cause, stream); + } + } + + private handleError(cause: unknown, stream?: Stream) { + const error = asJsonRpcError(cause); + + stream?.abort(error); + this.errorListeners.forEach((listener) => listener(error)); + return error; + } +} + +function toJsonRpcError(cause: unknown) { + if (cause instanceof JsonRpcError) { + return cause; + } + + return new JsonRpcError({ + code: -32603, + message: asJsonRpcError(cause).message, + }); +} + +function parseJsonRpcRequest(data: string) { + const payload = JSON.parse(data) as Partial; + if ( + payload?.jsonrpc !== "2.0" || + (typeof payload.id !== "number" && typeof payload.id !== "string") || + typeof payload.method !== "string" + ) { + throw new Error("Invalid JSON-RPC request"); + } + + return payload as ccc.JsonRpcPayload; +} + +function asJsonRpcError(cause: unknown) { + return cause instanceof Error ? cause : new Error("JSON-RPC request failed"); +} + +function isAbortError(cause: unknown) { + return ( + cause instanceof Error && + (cause.name === "AbortError" || cause.name === "TimeoutError") + ); +} + +export function jsonRpcService< + Components extends JsonRpcServiceComponents = JsonRpcServiceComponents, +>( + config: JsonRpcServiceConfig, + handler: JsonRpcRequestHandler, +): (components: Components) => JsonRpcService { + return (components: Components) => + new (class extends JsonRpcService { + protected handleRequest(request: JsonRpcRequest) { + return handler.call(this, request); + } + })(components, config); +} diff --git a/packages/libp2p/src/jsonRpcTransport.ts b/packages/libp2p/src/jsonRpcTransport.ts new file mode 100644 index 000000000..2d08a0f5b --- /dev/null +++ b/packages/libp2p/src/jsonRpcTransport.ts @@ -0,0 +1,85 @@ +import { ccc } from "@ckb-ccc/core"; +import type { Libp2p, PeerId, Stream } from "@libp2p/interface"; +import { lpStream } from "@libp2p/utils"; + +const DEFAULT_MAX_MESSAGE_LENGTH = 1024 * 1024; +const DEFAULT_TIMEOUT = 120_000; + +export type JsonRpcTransportLibp2pConfig = { + protocol: string; + maxMessageLength?: number; + timeout?: number; + signal?: AbortSignal; + onResponse?: (response: ccc.JsonRpcResponse) => void; +}; + +export class JsonRpcTransportLibp2p implements ccc.JsonRpcTransport { + private readonly maxMessageLength: number; + private readonly timeout: number; + + constructor( + private readonly node: Libp2p, + readonly peerId: PeerId, + private readonly config: JsonRpcTransportLibp2pConfig, + ) { + const maxMessageLength = + config.maxMessageLength ?? DEFAULT_MAX_MESSAGE_LENGTH; + if (!Number.isSafeInteger(maxMessageLength) || maxMessageLength <= 0) { + throw new Error( + "Maximum JSON-RPC message length must be a positive integer", + ); + } + + this.maxMessageLength = maxMessageLength; + + const timeout = config.timeout ?? DEFAULT_TIMEOUT; + if (!Number.isSafeInteger(timeout) || timeout <= 0) { + throw new Error("JSON-RPC request timeout must be a positive integer"); + } + + this.timeout = timeout; + } + + async request(payload: ccc.JsonRpcPayload): Promise { + const timeoutSignal = AbortSignal.timeout(this.timeout); + const signal = this.config.signal + ? ccc.abortSignalAny([this.config.signal, timeoutSignal]) + : timeoutSignal; + + let stream: Stream | undefined; + let response: ccc.JsonRpcResponse; + try { + // dialProtocol handles the signal, so no separate entry check is needed. + stream = await this.node.dialProtocol(this.peerId, this.config.protocol, { + runOnLimitedConnection: true, + signal, + }); + + const rpcStream = lpStream(stream, { + maxDataLength: this.maxMessageLength, + }); + + await rpcStream.write(ccc.bytesFrom(JSON.stringify(payload), "utf8"), { + signal, + }); + await stream.close({ signal }); + + response = JSON.parse( + ccc.bytesTo((await rpcStream.read({ signal })).subarray(), "utf8"), + ) as ccc.JsonRpcResponse; + await stream.closeRead({ signal }); + signal.throwIfAborted(); + } catch (cause) { + const error = asJsonRpcError(cause); + stream?.abort(error); + throw error; + } + + this.config.onResponse?.(response); + return response; + } +} + +function asJsonRpcError(cause: unknown) { + return cause instanceof Error ? cause : new Error("JSON-RPC request failed"); +} diff --git a/packages/libp2p/src/pairingEndpoint.test.ts b/packages/libp2p/src/pairingEndpoint.test.ts new file mode 100644 index 000000000..ae6a0f8e0 --- /dev/null +++ b/packages/libp2p/src/pairingEndpoint.test.ts @@ -0,0 +1,147 @@ +import { ccc } from "@ckb-ccc/core"; +import { multiaddr, type Multiaddr } from "@multiformats/multiaddr"; +import * as lp from "it-length-prefixed"; +import { describe, expect, it } from "vitest"; +import { + decodePairingEndpoint, + encodePairingEndpoint, + PairingEndpointRoleError, +} from "./pairingEndpoint.js"; + +const addresses = [ + multiaddr( + "/dns4/relay.ckbccc.com/tcp/443/wss/p2p/12D3KooWQwLwBK3EaCaJQNL9KBUvPi9Vh3gZqPfLQVi7aZpHkF3S/p2p-circuit/webrtc/p2p/12D3KooWJZQ7ypYJ6LHVYbNcKZX7HxV5pnPHJHvJ7zH2Bf6WmDKm", + ), + multiaddr( + "/dns4/relay.ckbccc.com/tcp/443/wss/p2p/12D3KooWQwLwBK3EaCaJQNL9KBUvPi9Vh3gZqPfLQVi7aZpHkF3S/p2p-circuit/p2p/12D3KooWJZQ7ypYJ6LHVYbNcKZX7HxV5pnPHJHvJ7zH2Bf6WmDKm", + ), +]; + +async function compress(bytes: Uint8Array) { + const stream = new CompressionStream("deflate"); + const output = new Response(stream.readable).arrayBuffer(); + const writer = stream.writable.getWriter(); + await writer.write(Uint8Array.from(bytes)); + await writer.close(); + return new Uint8Array(await output); +} + +function endpointFromCompressedAddresses(bytes: Uint8Array) { + const url = new URL("https://app.ckbccc.com/#signer"); + const params = new URLSearchParams({ + addresses: ccc.bytesTo(bytes, "base64url"), + secret: "pairing-secret", + }); + url.hash = `signer?${params.toString()}`; + return url.toString(); +} + +function fragmentParams(url: URL) { + const separator = url.hash.indexOf("?"); + return { + params: new URLSearchParams(url.hash.slice(separator + 1)), + route: url.hash.slice(1, separator), + }; +} + +describe("pairing endpoint", () => { + it("encodes pairing parameters in the URL fragment", async () => { + const endpoint = await encodePairingEndpoint( + "https://app.ckbccc.com/?theme=dark&secret=old#signer?source=qr", + addresses, + "pairing-secret", + "provider", + ); + const url = new URL(endpoint); + const { params, route } = fragmentParams(url); + + expect(url.searchParams.toString()).toBe("theme=dark&secret=old"); + expect(route).toBe("signer"); + expect(params.get("source")).toBe("qr"); + expect(params.get("role")).toBe("provider"); + expect(params.get("addresses")).toMatch(/^[\w-]+$/); + expect(params.get("secret")).toBe("pairing-secret"); + expect(params.has("addr")).toBe(false); + await expect( + decodePairingEndpoint(endpoint, "provider"), + ).resolves.toMatchObject({ + addresses, + secret: "pairing-secret", + }); + }); + + it("rejects an unexpected or missing endpoint role", async () => { + const endpoint = await encodePairingEndpoint( + "https://app.ckbccc.com/#signer", + addresses, + "pairing-secret", + "provider", + ); + + await expect( + decodePairingEndpoint(endpoint, "connector"), + ).rejects.toBeInstanceOf(PairingEndpointRoleError); + + const url = new URL(endpoint); + const { params, route } = fragmentParams(url); + params.delete("role"); + url.hash = `${route}?${params.toString()}`; + await expect( + decodePairingEndpoint(url.toString(), "provider"), + ).rejects.toMatchObject({ + actualRole: undefined, + expectedRole: "provider", + }); + }); + + it("rejects pairing parameters outside the fragment", async () => { + const url = new URL("https://app.ckbccc.com/#signer"); + url.searchParams.set("addresses", "ignored"); + url.searchParams.set("secret", "pairing-secret"); + + await expect(decodePairingEndpoint(url.toString())).rejects.toThrow( + "Pairing endpoint is incomplete", + ); + }); + + it("rejects invalid compressed addresses", async () => { + await expect( + decodePairingEndpoint( + "https://app.ckbccc.com/#signer?addresses=invalid&secret=pairing-secret", + ), + ).rejects.toThrow("Pairing endpoint contains invalid compressed addresses"); + }); + + it("stops decompression after the output limit", async () => { + const compressed = await compress(new Uint8Array(16 * 1024 + 1)); + + await expect( + decodePairingEndpoint(endpointFromCompressedAddresses(compressed)), + ).rejects.toMatchObject({ + cause: { message: "Decompressed address data is too large" }, + }); + }); + + it("limits the number of addresses", async () => { + const tooManyAddresses = Array.from({ length: 17 }, () => addresses[0]); + await expect( + encodePairingEndpoint( + "https://app.ckbccc.com/#signer", + tooManyAddresses, + "pairing-secret", + ), + ).rejects.toThrow("Pairing endpoint supports at most 16 addresses"); + + const encoded = encodeAddressesForTest(tooManyAddresses); + const compressed = await compress(encoded); + await expect( + decodePairingEndpoint(endpointFromCompressedAddresses(compressed)), + ).rejects.toMatchObject({ cause: { message: "Too many addresses" } }); + }); +}); + +function encodeAddressesForTest(addresses: readonly Multiaddr[]) { + return ccc.bytesConcat( + ...lp.encode(addresses.map((address) => address.bytes)), + ); +} diff --git a/packages/libp2p/src/pairingEndpoint.ts b/packages/libp2p/src/pairingEndpoint.ts new file mode 100644 index 000000000..7bbc771c2 --- /dev/null +++ b/packages/libp2p/src/pairingEndpoint.ts @@ -0,0 +1,181 @@ +import { ccc } from "@ckb-ccc/core"; +import { multiaddr, type Multiaddr } from "@multiformats/multiaddr"; +import * as lp from "it-length-prefixed"; +import type { PairingTarget } from "./pairingService.js"; + +const MAX_DECOMPRESSED_ADDRESSES_LENGTH = 16 * 1024; +const MAX_ADDRESSES = 16; +const PAIRING_PARAMETER_NAMES = ["addresses", "role", "secret"] as const; + +export class PairingEndpointRoleError extends Error { + constructor( + readonly expectedRole: string, + readonly actualRole: string | undefined, + ) { + super( + `Expected a ${expectedRole} pairing endpoint, received ${actualRole ? `role "${actualRole}"` : "an endpoint without a role"}`, + ); + this.name = "PairingEndpointRoleError"; + } +} + +export async function encodePairingEndpoint( + endpointUrl: string, + addresses: readonly Multiaddr[], + secret: string, + role?: string, +) { + if (addresses.length === 0) { + throw new Error("Pairing endpoint requires at least one address"); + } + if (addresses.length > MAX_ADDRESSES) { + throw new Error( + `Pairing endpoint supports at most ${MAX_ADDRESSES} addresses`, + ); + } + + const addressBytes = encodeAddresses(addresses); + if (addressBytes.byteLength > MAX_DECOMPRESSED_ADDRESSES_LENGTH) { + throw new Error("Pairing endpoint address data is too large"); + } + + const compressedAddressBytes = await transformBytes( + addressBytes, + new CompressionStream("deflate"), + ); + + const encodedAddresses = ccc.bytesTo(compressedAddressBytes, "base64url"); + + const url = new URL(endpointUrl); + const { params, route } = fragmentParameters(url); + + PAIRING_PARAMETER_NAMES.forEach((name) => { + params.delete(name); + }); + if (role) { + params.set("role", role); + } + params.set("addresses", encodedAddresses); + params.set("secret", secret); + url.hash = `${route}?${params.toString()}`; + + return url.toString(); +} + +export async function decodePairingEndpoint( + endpoint: string, + expectedRole?: string, +): Promise { + const url = new URL(endpoint.trim()); + const { params } = fragmentParameters(url); + if (expectedRole) { + const actualRole = params.get("role")?.trim() || undefined; + if (actualRole !== expectedRole) { + throw new PairingEndpointRoleError(expectedRole, actualRole); + } + } + const compressedAddresses = params.get("addresses")?.trim(); + const secret = params.get("secret")?.trim(); + + if (!compressedAddresses || !secret) { + throw new Error("Pairing endpoint is incomplete"); + } + + const addresses = await decodeCompressedAddresses(compressedAddresses); + + return { addresses, secret }; +} + +function fragmentParameters(url: URL) { + const fragment = url.hash.slice(1); + const separator = fragment.indexOf("?"); + if (separator === -1) { + return { params: new URLSearchParams(), route: fragment }; + } + + return { + params: new URLSearchParams(fragment.slice(separator + 1)), + route: fragment.slice(0, separator), + }; +} + +function encodeAddresses(addresses: readonly Multiaddr[]): Uint8Array { + return ccc.bytesConcat( + ...lp.encode(addresses.map((address) => address.bytes)), + ); +} + +async function decodeCompressedAddresses(value: string): Promise { + try { + const compressedBytes = ccc.bytesFrom(value, "base64url"); + const bytes = await transformBytes( + compressedBytes, + new DecompressionStream("deflate"), + MAX_DECOMPRESSED_ADDRESSES_LENGTH, + ); + const addresses: Multiaddr[] = []; + for (const address of lp.decode([bytes], { + maxDataLength: MAX_DECOMPRESSED_ADDRESSES_LENGTH, + })) { + if (addresses.length >= MAX_ADDRESSES) { + throw new Error("Too many addresses"); + } + addresses.push(multiaddr(address.subarray())); + } + + if (addresses.length === 0) { + throw new Error("Missing addresses"); + } + return addresses; + } catch (cause) { + throw new Error("Pairing endpoint contains invalid compressed addresses", { + cause, + }); + } +} + +async function transformBytes( + bytes: Uint8Array, + transform: CompressionStream | DecompressionStream, + maxOutputLength?: number, +) { + const readable = + maxOutputLength === undefined + ? transform.readable + : transform.readable.pipeThrough(limitBytes(maxOutputLength)); + const output = new Response(readable).arrayBuffer().then( + (buffer) => ({ buffer, ok: true }) as const, + (error: unknown) => ({ error, ok: false }) as const, + ); + const writer = transform.writable.getWriter(); + try { + await writer.write(Uint8Array.from(bytes)); + await writer.close(); + } catch (cause) { + const result = await output; + if (!result.ok) { + throw result.error; + } + throw cause; + } + + const result = await output; + if (!result.ok) { + throw result.error; + } + return new Uint8Array(result.buffer); +} + +function limitBytes(maxLength: number) { + let length = 0; + + return new TransformStream({ + transform(chunk, controller) { + if (chunk.byteLength > maxLength - length) { + throw new Error("Decompressed address data is too large"); + } + length += chunk.byteLength; + controller.enqueue(chunk); + }, + }); +} diff --git a/packages/libp2p/src/pairingService.test.ts b/packages/libp2p/src/pairingService.test.ts new file mode 100644 index 000000000..584065110 --- /dev/null +++ b/packages/libp2p/src/pairingService.test.ts @@ -0,0 +1,152 @@ +import type { Connection, PeerId, StreamHandler } from "@libp2p/interface"; +import { streamPair } from "@libp2p/utils"; +import { describe, expect, it, vi, type Mock } from "vitest"; +import { + pairingService, + type PairingService, + type PairingServiceComponents, +} from "./pairingService.js"; + +const PROTOCOL = "/pairing/test/1.0.0"; +type OpenStream = PairingServiceComponents["connectionManager"]["openStream"]; + +type TestNode = { + components: PairingServiceComponents; + handler?: StreamHandler; + openStream: Mock; + peerId: PeerId; + service: PairingService; +}; + +function testPeerId(value: string): PeerId { + return { + equals: (other: PeerId) => other.toString() === value, + toString: () => value, + } as PeerId; +} + +function createTestNode(id: string): TestNode { + const node = {} as TestNode; + const openStream = vi.fn(); + const components = { + connectionManager: { + openStream, + }, + registrar: { + handle: vi.fn(async (_protocol: string, handler: StreamHandler) => { + node.handler = handler; + }), + unhandle: vi.fn(async () => {}), + }, + } as unknown as PairingServiceComponents; + + node.components = components; + node.openStream = openStream; + node.peerId = testPeerId(id); + node.service = pairingService( + { pairedPeerTimeoutMs: 60_000, protocol: PROTOCOL }, + () => true, + )(components); + return node; +} + +function connect(source: TestNode, target: TestNode) { + source.openStream.mockImplementation(async (_target, _protocol, options) => { + const [outbound, inbound] = await streamPair({ protocol: PROTOCOL }); + const outgoingConnection = { + remotePeer: target.peerId, + } as Connection; + const incomingConnection = { + remotePeer: source.peerId, + } as Connection; + options?.onProgress?.({ + detail: { connection: outgoingConnection, stream: outbound }, + type: "connection:opened-stream", + }); + void target.handler?.(inbound, incomingConnection); + return outbound; + }); +} + +describe("PairingService secret rotation", () => { + it("rotates both peers after pairing and rejects the consumed secret", async () => { + const initiator = createTestNode("initiator"); + const provider = createTestNode("provider"); + const replay = createTestNode("replay"); + await Promise.all([ + initiator.service.start(), + provider.service.start(), + replay.service.start(), + ]); + connect(initiator, provider); + connect(replay, provider); + + const initiatorSecret = initiator.service.secret; + const providerSecret = provider.service.secret; + const replaySecret = replay.service.secret; + const initiatorSecrets: string[] = []; + const providerSecrets: string[] = []; + initiator.service.onSecretChanged((secret) => + initiatorSecrets.push(secret), + ); + provider.service.onSecretChanged((secret) => providerSecrets.push(secret)); + + try { + await expect( + initiator.service.pair({ addresses: [], secret: providerSecret }), + ).resolves.toBe(provider.peerId); + + expect(initiator.service.secret).not.toBe(initiatorSecret); + expect(provider.service.secret).not.toBe(providerSecret); + expect(initiatorSecrets).toEqual([initiator.service.secret]); + expect(providerSecrets).toEqual([provider.service.secret]); + + await expect( + replay.service.pair({ addresses: [], secret: providerSecret }), + ).rejects.toThrow("Pairing rejected"); + expect(replay.service.secret).toBe(replaySecret); + expect(providerSecrets).toEqual([provider.service.secret]); + } finally { + await Promise.all([ + initiator.service.stop(), + provider.service.stop(), + replay.service.stop(), + ]); + } + }); + + it("accepts only one concurrent request with the same secret", async () => { + const first = createTestNode("first"); + const second = createTestNode("second"); + const provider = createTestNode("provider"); + await Promise.all([ + first.service.start(), + second.service.start(), + provider.service.start(), + ]); + connect(first, provider); + connect(second, provider); + + const secret = provider.service.secret; + try { + const results = await Promise.allSettled([ + first.service.pair({ addresses: [], secret }), + second.service.pair({ addresses: [], secret }), + ]); + + expect( + results.filter(({ status }) => status === "fulfilled"), + ).toHaveLength(1); + expect( + results.filter(({ status }) => status === "rejected"), + ).toHaveLength(1); + expect(provider.service.secret).not.toBe(secret); + } finally { + await Promise.all([ + first.service.stop(), + second.service.stop(), + provider.service.stop(), + ]); + } + }); +}); diff --git a/packages/libp2p/src/pairingService.ts b/packages/libp2p/src/pairingService.ts new file mode 100644 index 000000000..414681f06 --- /dev/null +++ b/packages/libp2p/src/pairingService.ts @@ -0,0 +1,411 @@ +import { ccc } from "@ckb-ccc/core"; +import type { + AbortOptions, + Connection, + PeerId, + Stream, +} from "@libp2p/interface"; +import type { ConnectionManager, Registrar } from "@libp2p/interface-internal"; +import { lpStream } from "@libp2p/utils"; +import type { Multiaddr } from "@multiformats/multiaddr"; + +const MAX_PAIRING_MESSAGE_LENGTH = 1024; +const UNPAIR_TIMEOUT_MS = 10_000; + +export type PairingServiceComponents = { + connectionManager: ConnectionManager; + registrar: Registrar; +}; + +export type PairingServiceConfig = { + /** Optional self-reported label shared with the paired peer. */ + name?: string; + protocol: string; + pairedPeerTimeoutMs: number; +}; + +export type PairingTarget = { + addresses: Multiaddr[]; + secret: string; +}; + +export type PairingGuard< + Components extends PairingServiceComponents = PairingServiceComponents, +> = (this: PairingService, peerId: PeerId) => boolean; + +type PairingRequest = + { type: "pair"; secret: string; name?: string } | { type: "unpair" }; + +type PairingResponse = + { ok: true; name?: string } | { ok: false; error: string }; + +export abstract class PairingService< + Components extends PairingServiceComponents = PairingServiceComponents, +> { + private currentSecret = createSecret(); + private readonly errorListeners = new Set<(error: Error) => void>(); + private readonly pairedListeners = new Set< + (peerId: PeerId, name?: string) => void + >(); + private readonly unpairedListeners = new Set<(peerId: PeerId) => void>(); + private readonly secretChangedListeners = new Set<(secret: string) => void>(); + private readonly pairingTimeouts = new Map< + string, + ReturnType + >(); + + constructor( + readonly components: Components, + readonly config: PairingServiceConfig, + ) { + if (config.pairedPeerTimeoutMs <= 0) { + throw new Error("Paired peer timeout must be greater than zero"); + } + } + + protected abstract canPair(peerId: PeerId): boolean; + + /** The credential to include in the currently issued pairing endpoint. */ + get secret() { + return this.currentSecret; + } + + onError(listener: (error: Error) => void) { + this.errorListeners.add(listener); + return () => this.errorListeners.delete(listener); + } + + onPaired(listener: (peerId: PeerId, name?: string) => void) { + this.pairedListeners.add(listener); + return () => this.pairedListeners.delete(listener); + } + + onUnpaired(listener: (peerId: PeerId) => void) { + this.unpairedListeners.add(listener); + return () => this.unpairedListeners.delete(listener); + } + + onSecretChanged(listener: (secret: string) => void) { + this.secretChangedListeners.add(listener); + return () => this.secretChangedListeners.delete(listener); + } + + async start() { + await this.components.registrar.handle( + this.config.protocol, + this.handleProtocol.bind(this), + { runOnLimitedConnection: true }, + ); + } + + async stop() { + await this.components.registrar.unhandle(this.config.protocol); + + this.pairingTimeouts.forEach((timeout) => clearTimeout(timeout)); + this.pairingTimeouts.clear(); + } + + isPaired(peerId: PeerId) { + return this.pairingTimeouts.has(peerId.toString()); + } + + refresh(peerId: PeerId) { + const key = peerId.toString(); + const timeout = this.pairingTimeouts.get(key); + if (timeout === undefined) { + return; + } + + clearTimeout(timeout); + this.pairingTimeouts.set(key, this.createPeerTimeout(peerId)); + } + + async pair( + target: PairingTarget, + options: AbortOptions = {}, + ): Promise { + let stream: Stream | undefined; + let addedPeer: PeerId | undefined; + + try { + const opened = await this.openStream(target.addresses, options); + stream = opened.stream; + + if (!opened.connection) { + throw new Error("Could not determine remote peer"); + } + + if (this.addPairedPeer(opened.connection.remotePeer)) { + addedPeer = opened.connection.remotePeer; + } + + const response = await exchange( + stream, + { + type: "pair", + secret: target.secret, + name: this.config.name, + }, + options, + ); + options.signal?.throwIfAborted(); + stream = undefined; + + if (!response.ok) { + throw new Error(response.error); + } + + if (addedPeer) { + this.rotateSecret(); + this.notifyPairedPeer(addedPeer, response.name); + } + return opened.connection.remotePeer; + } catch (cause) { + if (addedPeer) { + this.removePairedPeer(addedPeer, false); + } + throw this.handleError(cause, stream); + } + } + + async unpair(peerId: PeerId) { + if (!this.removePairedPeer(peerId)) { + return; + } + + const options = { signal: AbortSignal.timeout(UNPAIR_TIMEOUT_MS) }; + + let stream: Stream | undefined; + try { + const opened = await this.openStream(peerId, options); + stream = opened.stream; + const response = await exchange( + opened.stream, + { type: "unpair" }, + options, + ); + stream = undefined; + if (!response.ok) { + throw new Error(response.error); + } + } catch (cause) { + this.handleError(cause, stream); + } + } + + private async handleProtocol(stream: Stream, connection: Connection) { + let addedPeer: PeerId | undefined; + + try { + const pairingStream = lpStream(stream, { + maxDataLength: MAX_PAIRING_MESSAGE_LENGTH, + }); + const request = parsePairingRequest( + (await pairingStream.read()).subarray(), + ); + + if (request.type === "unpair") { + this.removePairedPeer(connection.remotePeer); + await writeResponse(pairingStream, stream, { ok: true }); + return; + } + + if ( + request.secret !== this.secret || + !this.canPair(connection.remotePeer) + ) { + await writeResponse(pairingStream, stream, { + ok: false, + error: "Pairing rejected", + }); + return; + } + + // Consume the credential before the next asynchronous boundary so two + // concurrent requests cannot both pair with the same endpoint. + this.rotateSecret(); + + if (this.addPairedPeer(connection.remotePeer)) { + addedPeer = connection.remotePeer; + } + await writeResponse(pairingStream, stream, { + ok: true, + name: this.config.name, + }); + if (addedPeer) { + this.notifyPairedPeer(addedPeer, request.name); + } + } catch (cause) { + if (addedPeer) { + this.removePairedPeer(addedPeer, false); + } + this.handleError(cause, stream); + } + } + + private addPairedPeer(peerId: PeerId) { + const key = peerId.toString(); + const timeout = this.pairingTimeouts.get(key); + if (timeout !== undefined) { + clearTimeout(timeout); + } + + this.pairingTimeouts.set(key, this.createPeerTimeout(peerId)); + return timeout === undefined; + } + + private notifyPairedPeer(peerId: PeerId, name?: string) { + this.pairedListeners.forEach((listener) => listener(peerId, name)); + } + + private rotateSecret() { + this.currentSecret = createSecret(); + this.secretChangedListeners.forEach((listener) => + listener(this.currentSecret), + ); + } + + private removePairedPeer(peerId: PeerId, notify = true) { + const key = peerId.toString(); + const timeout = this.pairingTimeouts.get(key); + if (timeout === undefined) { + return false; + } + + clearTimeout(timeout); + this.pairingTimeouts.delete(key); + + if (notify) { + this.unpairedListeners.forEach((listener) => listener(peerId)); + } + return true; + } + + private createPeerTimeout(peerId: PeerId) { + return setTimeout(() => { + this.removePairedPeer(peerId); + }, this.config.pairedPeerTimeoutMs); + } + + private async openStream( + target: PeerId | Multiaddr | Multiaddr[], + options: AbortOptions, + ) { + let connection: Connection | undefined; + const stream = await this.components.connectionManager.openStream( + target, + this.config.protocol, + { + runOnLimitedConnection: true, + signal: options.signal, + onProgress(event) { + if (event.type === "connection:opened-stream") { + connection = event.detail.connection; + } + }, + }, + ); + + return { stream, connection }; + } + + private handleError(cause: unknown, stream?: Stream) { + const error = asError(cause, "Pairing operation failed"); + + stream?.abort(error); + this.errorListeners.forEach((listener) => listener(error)); + return error; + } +} + +export function pairingService< + Components extends PairingServiceComponents = PairingServiceComponents, +>( + config: PairingServiceConfig, + canPair: PairingGuard, +): (components: Components) => PairingService { + return (components: Components) => + new (class extends PairingService { + protected canPair(peerId: PeerId) { + return canPair.call(this, peerId); + } + })(components, config); +} + +async function exchange( + stream: Stream, + request: PairingRequest, + options: AbortOptions, +) { + const pairingStream = lpStream(stream, { + maxDataLength: MAX_PAIRING_MESSAGE_LENGTH, + }); + + await pairingStream.write(ccc.bytesFrom(JSON.stringify(request), "utf8"), { + signal: options.signal, + }); + await stream.close(options); + + const response = parsePairingResponse( + (await pairingStream.read(options)).subarray(), + ); + await stream.closeRead(options); + return response; +} + +async function writeResponse( + pairingStream: ReturnType, + stream: Stream, + response: PairingResponse, +) { + await pairingStream.write(ccc.bytesFrom(JSON.stringify(response), "utf8")); + await stream.close(); +} + +function parsePairingRequest(data: Uint8Array): PairingRequest { + const request = JSON.parse( + ccc.bytesTo(data, "utf8"), + ) as Partial; + if ( + request.type === "pair" && + typeof request.secret === "string" && + request.secret + ) { + return { + type: "pair", + secret: request.secret, + name: typeof request.name === "string" ? request.name : undefined, + }; + } + + if (request.type === "unpair") { + return { type: "unpair" }; + } + + throw new Error("Invalid pairing request"); +} + +function parsePairingResponse(data: Uint8Array): PairingResponse { + const response = JSON.parse( + ccc.bytesTo(data, "utf8"), + ) as Partial; + if (response.ok === true) { + return { + ok: true, + name: typeof response.name === "string" ? response.name : undefined, + }; + } + if (response.ok === false && typeof response.error === "string") { + return { ok: false, error: response.error }; + } + + throw new Error("Invalid pairing response"); +} + +function createSecret() { + return ccc.bytesTo(crypto.getRandomValues(new Uint8Array(16)), "base64url"); +} + +function asError(cause: unknown, fallback: string) { + return cause instanceof Error ? cause : new Error(fallback); +} diff --git a/packages/libp2p/tsconfig.json b/packages/libp2p/tsconfig.json new file mode 100644 index 000000000..3399c6f67 --- /dev/null +++ b/packages/libp2p/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "es2022", + "incremental": true, + "allowJs": true, + "importHelpers": false, + "declaration": true, + "declarationMap": true, + "experimentalDecorators": true, + "useDefineForClassFields": false, + "esModuleInterop": true, + "strict": true, + "noImplicitAny": true, + "strictBindCallApply": true, + "strictNullChecks": true, + "alwaysStrict": true, + "noFallthroughCasesInSwitch": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "Bundler", + "rootDir": "./src" + }, + "include": ["src/**/*"] +} diff --git a/packages/libp2p/tsdown.config.mts b/packages/libp2p/tsdown.config.mts new file mode 100644 index 000000000..9453d8b6d --- /dev/null +++ b/packages/libp2p/tsdown.config.mts @@ -0,0 +1,41 @@ +import { defineConfig } from "tsdown"; + +const common = { + minify: true, + dts: true, + platform: "neutral" as const, + sourcemap: true, + exports: true, +}; + +const entry = { + index: "src/index.ts", + barrel: "src/barrel.ts", +} as const; + +const bundleDeps: string[] = []; + +export default defineConfig( + ( + [ + { + entry, + deps: { + onlyBundle: [] as string[], + }, + format: "esm", + copy: "./misc/basedirs/dist/*", + }, + { + entry, + deps: { + alwaysBundle: bundleDeps, + onlyBundle: bundleDeps, + }, + format: "cjs", + outDir: "dist.commonjs", + copy: "./misc/basedirs/dist.commonjs/*", + }, + ] as const + ).map((c) => ({ ...c, ...common })), +); diff --git a/packages/libp2p/typedoc.json b/packages/libp2p/typedoc.json new file mode 100644 index 000000000..a9badeedd --- /dev/null +++ b/packages/libp2p/typedoc.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://typedoc.org/schema.json", + "entryPoints": ["./src/index.ts"], + "extends": ["../../typedoc.base.json"], + "name": "@ckb-ccc/libp2p" +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 95cf736ef..d46afcaa8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -50,9 +50,18 @@ importers: packages/app: dependencies: + '@chainsafe/libp2p-noise': + specifier: ^17.0.0 + version: 17.0.0 + '@chainsafe/libp2p-yamux': + specifier: ^8.0.1 + version: 8.0.1 '@ckb-ccc/connector-react': specifier: workspace:* version: link:../connector-react + '@ckb-ccc/libp2p': + specifier: workspace:* + version: link:../libp2p '@ckb-ccc/lumos-patches': specifier: workspace:* version: link:../lumos-patches @@ -74,21 +83,45 @@ importers: '@ckb-lumos/helpers': specifier: 0.24.0-next.2 version: 0.24.0-next.2 + '@libp2p/circuit-relay-v2': + specifier: ^4.2.11 + version: 4.2.13 + '@libp2p/identify': + specifier: ^4.1.12 + version: 4.1.14 + '@libp2p/interface': + specifier: ^3.2.5 + version: 3.3.0 + '@libp2p/webrtc': + specifier: ^6.0.29 + version: 6.0.32(react-native@0.87.1(@babel/core@7.29.7)(@types/react@19.2.18)(react@19.2.8)) + '@libp2p/websockets': + specifier: ^10.1.19 + version: 10.1.21 + '@multiformats/multiaddr': + specifier: ^13.0.3 + version: 13.0.3 '@scure/bip32': specifier: ^2.0.0 version: 2.4.0 '@scure/bip39': specifier: ^2.0.0 version: 2.4.0 + core-js: + specifier: ^3.50.0 + version: 3.50.0 + libp2p: + specifier: ^3.3.8 + version: 3.3.11 lucide-react: specifier: ^1.21.0 version: 1.39.0(react@19.2.8) next: specifier: 16.3.4 version: 16.3.4(@babel/core@7.29.7)(@types/node@26.4.1)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - qrcode.react: - specifier: ^4.2.0 - version: 4.2.0(react@19.2.8) + qr: + specifier: ^0.6.0 + version: 0.6.0 react: specifier: 19.2.8 version: 19.2.8 @@ -245,12 +278,48 @@ importers: packages/connector: dependencies: + '@chainsafe/libp2p-noise': + specifier: ^17.0.0 + version: 17.0.0 + '@chainsafe/libp2p-yamux': + specifier: ^8.0.1 + version: 8.0.1 '@ckb-ccc/ccc': specifier: workspace:* version: link:../ccc + '@ckb-ccc/libp2p': + specifier: workspace:* + version: link:../libp2p + '@libp2p/circuit-relay-v2': + specifier: ^4.2.12 + version: 4.2.13 + '@libp2p/identify': + specifier: ^4.1.13 + version: 4.1.14 + '@libp2p/interface': + specifier: ^3.3.0 + version: 3.3.0 + '@libp2p/webrtc': + specifier: ^6.0.30 + version: 6.0.32(react-native@0.87.1(@babel/core@7.29.7)(@types/react@19.2.18)(react@19.2.8)) + '@libp2p/websockets': + specifier: ^10.1.20 + version: 10.1.21 + '@multiformats/multiaddr': + specifier: ^13.0.3 + version: 13.0.3 + core-js: + specifier: ^3.50.0 + version: 3.50.0 + libp2p: + specifier: ^3.3.9 + version: 3.3.11 lit: specifier: ^3.3.3 version: 3.3.3 + qr: + specifier: ^0.6.0 + version: 0.6.0 devDependencies: '@eslint/js': specifier: ^10.0.1 @@ -865,6 +934,58 @@ importers: specifier: ^8.61.1 version: 8.70.0(@typescript/typescript6@6.0.2)(eslint@10.10.0(jiti@2.7.0)) + packages/libp2p: + dependencies: + '@ckb-ccc/core': + specifier: workspace:* + version: link:../core + '@libp2p/interface': + specifier: ^3.3.0 + version: 3.3.0 + '@libp2p/interface-internal': + specifier: ^3.1.12 + version: 3.1.13 + '@libp2p/utils': + specifier: ^7.4.0 + version: 7.4.1 + '@multiformats/multiaddr': + specifier: ^13.0.3 + version: 13.0.3 + it-length-prefixed: + specifier: ^11.0.1 + version: 11.0.1 + devDependencies: + '@eslint/js': + specifier: ^10.0.1 + version: 10.0.1(eslint@10.10.0(jiti@2.7.0)) + '@typescript/native': + specifier: npm:typescript@^7.0.2 + version: typescript@7.0.2 + eslint: + specifier: ^10.5.0 + version: 10.10.0(jiti@2.7.0) + eslint-config-prettier: + specifier: ^10.1.8 + version: 10.1.8(eslint@10.10.0(jiti@2.7.0)) + eslint-plugin-prettier: + specifier: ^5.5.6 + version: 5.5.6(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@10.10.0(jiti@2.7.0)))(eslint@10.10.0(jiti@2.7.0))(prettier@3.9.6) + prettier: + specifier: ^3.8.4 + version: 3.9.6 + prettier-plugin-organize-imports: + specifier: ^4.3.0 + version: 4.3.0(@typescript/typescript6@6.0.2)(prettier@3.9.6) + tsdown: + specifier: ^0.22.3 + version: 0.22.14(@typescript/typescript6@6.0.2)(tsx@4.23.13)(unrun@0.2.22(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.3)(synckit@0.11.13)) + typescript: + specifier: npm:@typescript/typescript6@^6.0.2 + version: '@typescript/typescript6@6.0.2' + typescript-eslint: + specifier: ^8.61.1 + version: 8.70.0(@typescript/typescript6@6.0.2)(eslint@10.10.0(jiti@2.7.0)) + packages/lumos-patches: dependencies: '@ckb-ccc/core': @@ -1830,6 +1951,24 @@ packages: '@cacheable/utils@2.5.0': resolution: {integrity: sha512-buipgOVDkkPXNR5+xBpDw7Zk2n1EvU7qBJCNUcL7rhQ//kfpOXPAvQ511Os0vpLYJ1pZnvudNytkQt2hst3wqA==} + '@chainsafe/as-chacha20poly1305@0.1.0': + resolution: {integrity: sha512-BpNcL8/lji/GM3+vZ/bgRWqJ1q5kwvTFmGPk7pxm/QQZDbaMI98waOHjEymTjq2JmdD/INdNBFOVSyJofXg7ew==} + + '@chainsafe/as-sha256@1.2.5': + resolution: {integrity: sha512-8LzmWxBC/2O5BgbP+aRom/FaDJMrM04VZMlVIivYTc6yNQKYrONmIv27m3q/1VBgxrfrNNqmibitb2hE2tQzIw==} + + '@chainsafe/is-ip@2.1.0': + resolution: {integrity: sha512-KIjt+6IfysQ4GCv66xihEitBjvhU/bixbbbFxdJ1sqCp4uJ0wuZiYBPhksZoy4lfaF0k9cwNzY5upEW/VWdw3w==} + + '@chainsafe/libp2p-noise@17.0.0': + resolution: {integrity: sha512-vwrmY2Y+L1xYhIDiEpl61KHxwrLCZoXzTpwhyk34u+3+6zCAZPL3GxH3i2cs+u5IYNoyLptORdH17RKFXy7upA==} + + '@chainsafe/libp2p-yamux@8.0.1': + resolution: {integrity: sha512-pJsqmUg1cZRJZn/luAtQaq0uLcVfExo51Rg7iRtAEceNYtsKUi/exfegnvTBzTnF1CGmTzVEV3MCLsRhqiNyoA==} + + '@chainsafe/netmask@2.0.0': + resolution: {integrity: sha512-I3Z+6SWUoaljh3TBzCnCxjlUyN8tA+NAk5L6m9IxvCf1BENQTePzPMis97CoN/iMW1St3WN+AWCCRp+TTBRiDg==} + '@changesets/apply-release-plan@8.1.0': resolution: {integrity: sha512-M93HOGyX3ssg6He3b5NotaHtQVu6uajHS6UIQ28xKt2RzskCy73ayX4I914qBKjTJmrE4YFlELQjfcqxbmGLJw==} engines: {node: ^22.11 || ^24 || >=26} @@ -1961,6 +2100,10 @@ packages: resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} + '@dnsquery/dns-packet@6.1.1': + resolution: {integrity: sha512-WXTuFvL3G+74SchFAtz3FgIYVOe196ycvGsMgvSH/8Goptb1qpIQtIuM4SOK9G9lhMWYpHxnXyy544ZhluFOew==} + engines: {node: '>=6'} + '@emnapi/core@1.10.0': resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} @@ -2779,6 +2922,10 @@ packages: resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} + '@isaacs/ttlcache@1.4.1': + resolution: {integrity: sha512-RQgQ4uQ+pLbqXfOmieB91ejmLwvSgv9nLx6sT6sD83s7umBypgg+OIBOBbEUiJXrfpnp9j0mRhYYdzp9uqq3lA==} + engines: {node: '>=12'} + '@istanbuljs/load-nyc-config@1.1.0': resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==} engines: {node: '>=8'} @@ -2857,6 +3004,10 @@ packages: node-notifier: optional: true + '@jest/schemas@29.6.3': + resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@jest/schemas@30.0.5': resolution: {integrity: sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} @@ -2885,6 +3036,10 @@ packages: resolution: {integrity: sha512-EDnDhn0jleU9ZhpVoA4gvqw+Ev0iw/r5upNT2b79RiwiaTiYAMMhvNJP3lmocjIe5J2ZkoNr0B3K/J6O5GIm4Q==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@jest/types@29.6.3': + resolution: {integrity: sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@jest/types@30.0.5': resolution: {integrity: sha512-aREYa3aku9SSnea4aX6bhKn4bgv3AXkgijoQgbYV3yvbiGt6z+MQ85+6mIhx9DsKW2BuB/cLR/A+tcMThx+KLQ==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} @@ -2933,6 +3088,57 @@ packages: '@keyv/serialize@1.1.1': resolution: {integrity: sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==} + '@leichtgewicht/ip-codec@2.0.5': + resolution: {integrity: sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==} + + '@libp2p/circuit-relay-v2@4.2.13': + resolution: {integrity: sha512-AGEzj72W7Ae5eizaM+HeDzJBR28zK4yYp4BYvUOHQViT62W97FIITo/9Gc5rlkKti+gJJAurBm3cHV4A8Wcoxw==} + + '@libp2p/crypto@5.1.23': + resolution: {integrity: sha512-u6XVMD1YpUJgjS5MAayrxlzi+hQcj3FHY0wS6/M/T93ntyCW13BmmRzFb2ESamk65PEuSCAChmQeSzRV2sh2rQ==} + + '@libp2p/identify@4.1.14': + resolution: {integrity: sha512-c7oU0DSKzkMNsanruOQc4Zy6mSf3fq8/W+4E/I936HiHZc/LIbUuCWaxbgmQexdfKqECH1qH4q9IwaCUWmjPMQ==} + + '@libp2p/interface-internal@3.1.13': + resolution: {integrity: sha512-+vjQzpQl+0uoWCyNwV5NDnv1RAK9LDeWGeMbx/Fl2ZWrndtuFUPa9LxwqHXrTly6YOhgUatSzVqZBWnrQ1PxGg==} + + '@libp2p/interface@3.3.0': + resolution: {integrity: sha512-SXahM/4IgpiFKTtocbXYSTA1wEUVjktmT1yBCzBbc2Vgsu1VBCg6SvsFmXo2Hbeyev3D8EU+y3uaCKDB43C/Vg==} + + '@libp2p/keychain@6.1.7': + resolution: {integrity: sha512-kemRM/37UXfkqmZsFFIsICX8iMX3TUShYGaRL7xXqNq3oIebm12e54DW1dtDuUnpNLctdF6zWPPelUHQm50Szg==} + + '@libp2p/logger@6.2.13': + resolution: {integrity: sha512-+tkZuRPoiFdh+frG//3hQ3goV1uMzRblpZ+mwtezgLXkRNqB5YKBLl+c/rtrurwAUqjCuI/yoEPJBiCizg2z6w==} + + '@libp2p/multistream-select@7.0.28': + resolution: {integrity: sha512-X6rmxeAlFeTDHDc4IPujz+y9TdA8cSMFfWyGJfxdIbQpDc1PwZI/Ttt3YWp0LAT9sbSYVFaWA6cGTN9EnMWtGQ==} + + '@libp2p/noise@17.0.2': + resolution: {integrity: sha512-FY83yQf4/aqqiJHY6XB5aO7vtst6mMoy0r/vGsnTaosaYQ6g9xwkNbTq/juzTXjRbz9pBVkp62zXKJrXmm9G3w==} + + '@libp2p/peer-collections@7.0.28': + resolution: {integrity: sha512-+evafZ5oSnGRfB7z+ycf09ehzNhCKoOZaiHgi6TpnDitpRQM5KQeN0H9Ep2uNJtzyW944HOG41BqcSaHbEHv7Q==} + + '@libp2p/peer-id@6.0.15': + resolution: {integrity: sha512-aIbYJcldMNOIzmm9J57aESjZInYFdrQ9AY0ij+qn0Oam2vBxDfEIJZxJu+JzVkB2rVvT+FHDQXKWT2nAqNgB8w==} + + '@libp2p/peer-record@9.0.16': + resolution: {integrity: sha512-J4QoPr0F1v6xqjHjAJ9KPbg4jdDlGutDk2K3Km6+o0nWzZCgCGeSEIDOO2UvPa2bFoZTL62cD73fn8tm5K06cg==} + + '@libp2p/peer-store@12.0.28': + resolution: {integrity: sha512-dj4qRhN01kml/It5/4P4DVcjfEZXJ2uZVVItCrs2YBGG2HLEYfWQZYROVbTCXDOCWwZK46aJ8eSLt4orsefq7Q==} + + '@libp2p/utils@7.4.1': + resolution: {integrity: sha512-e9cZeop+awI0rFczG4DvAwVeHqBhuCZfTyskvgONfKQrsT7L0BSlTlTTzZoFF8Dnb+QrzHsoIpnl0PrCr0h+Hg==} + + '@libp2p/webrtc@6.0.32': + resolution: {integrity: sha512-XJTKNlsVh6v0LqbvBUFQqlwxHEQRA8KW8W2aLEZrjf8NtIww6BYozsaYVe+0W6ujjZWHjTkVjUwPgVqbIPzNFA==} + + '@libp2p/websockets@10.1.21': + resolution: {integrity: sha512-2iItZGEfTlGFUEcFCghtlsIiQ+fBowopysBB/8ya0MvZkRic/6o9oSklWT5pWDmXtmqmbp1Dko/iLcBOhDOgFw==} + '@lit-labs/ssr-dom-shim@1.4.0': resolution: {integrity: sha512-ficsEARKnmmW5njugNYKipTm4SFnbik7CXtoencDZzmzo/dQ+2Q0bgkzJuoJP20Aj0F+izzJjOqsnkd6F/o1bw==} @@ -2979,6 +3185,18 @@ packages: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + '@multiformats/dns@1.0.15': + resolution: {integrity: sha512-W0zAMABtAn+3chgFcPGvllKND7M6GblMAAFcJQTy+iMmGiyFErZYPzAh2b50Y3SFf340iFV7+ckVgZkGfUyxzA==} + + '@multiformats/multiaddr-matcher@3.0.2': + resolution: {integrity: sha512-iphGQJliZxe2yKu57bdRDgeS+3znc5uXtMybDO1Wau3rIjas4zjrjlyxmFz3wqyUL9f3VDQwas/ZqA7N4QeSfw==} + + '@multiformats/multiaddr-to-uri@12.0.0': + resolution: {integrity: sha512-3uIEBCiy8tfzxYYBl81x1tISiNBQ7mHU4pGjippbJRoQYHzy/ZdZM/7JvTldr8pc/dzpkaNJxnsuxxlhsPOJsA==} + + '@multiformats/multiaddr@13.0.3': + resolution: {integrity: sha512-mEqqJ4r3a/uuFMTpRkU316wGNIDQNhuVWpm+ebKTQeYsfv9jXbPONWM6VVnj3KGUrwfsX7GZOyp4TFqEA2SPCw==} + '@napi-rs/lzma-linux-x64-gnu@1.5.1': resolution: {integrity: sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==} engines: {node: ^22.20 || ^24.12 || >=25} @@ -3191,6 +3409,55 @@ packages: resolution: {integrity: sha512-X5XaVWZIBCT7HHZGm5I7ZQXDwLG+bGXuSrMQAW+7Zvl87h1kmc1ZB1VSRJcpUfoUrGQp4Fkoxm5kZ+Ms+aW+eA==} engines: {node: '>= 20.19.0'} + '@node-datachannel/android-arm64@0.33.2': + resolution: {integrity: sha512-I71a0jICUNYgh4ZI5AcINEMuRtQSVyrpp4FV3kJZMcx31xS0vKKtf06tpsUAtW5uS+uRXbZR+vlMEqS1h710wQ==} + cpu: [arm64] + os: [android] + + '@node-datachannel/darwin-arm64@0.33.2': + resolution: {integrity: sha512-EwBGiaMh3MX6zjYoZHuK02Q6hmQQIvR39335Kb/ZnP3fXZU0/VWx6ksHLM6K0XFFYRfP+yJeVgU79g++xoeQpw==} + cpu: [arm64] + os: [darwin] + + '@node-datachannel/darwin-x64@0.33.2': + resolution: {integrity: sha512-a1nvL6MiskSjV3TWTNXdVpI/2kHNIOP2AbuBZjR5eC7VSrH/6hCFfHIBW4SFKXGFm8LOGliY9Df10UnJUutjwA==} + cpu: [x64] + os: [darwin] + + '@node-datachannel/linux-arm64-gnu@0.33.2': + resolution: {integrity: sha512-YWAu3EiMl2HRi/fwWlfFTW30wFTkRVYJWRHQcI1uDSeZYG/tfyBqBy6CRy9Sgrb0xjOYZ6XhLDj8cdX0foPlpg==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@node-datachannel/linux-arm64-musl@0.33.2': + resolution: {integrity: sha512-16o3Ny5hbxTTjDmhsZog/t7p2tRP6s/Oa/V9kKzZfiGlCqvxxCE4ACOWw/MWHOhN0xI9/63IIRfrrnQnPxjK8g==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@node-datachannel/linux-x64-gnu@0.33.2': + resolution: {integrity: sha512-grLRAbZgSIX8nmObciblyCmnevDCst0snhV+z1fvDmUlzXIVXeDmv6lEzAaWs3KRleagqwSsFg5d1td1bwMT9w==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@node-datachannel/linux-x64-musl@0.33.2': + resolution: {integrity: sha512-PlpgvYjo+gmBhIkRFaZWIhCF335FMTJmCpl+FVa4XliEgyOqD+cvjQPWcbnlrA2Jd+QnLiZt+NxT7nv4QXpoiQ==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@node-datachannel/win32-arm64-msvc@0.33.2': + resolution: {integrity: sha512-8NpHkm7R8EY0jHGHOXdK7K0wjWY34PJ+PJY++2VDHTAtXGazNm28zcnVE/4IU0N26YGdCDSSWat3OO3W6nwoRw==} + cpu: [arm64] + os: [win32] + + '@node-datachannel/win32-x64-msvc@0.33.2': + resolution: {integrity: sha512-aLk0O/M1JNVpzxx4RVBXMJTMlpswE04Cz5baayKwuEN+qk9hO6DSJgYMR6Wq9a5zGYAmvLFlOg+LsePyXkWliA==} + cpu: [x64] + os: [win32] + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -3306,6 +3573,69 @@ packages: resolution: {integrity: sha512-7FNeNl8NCE7aINx7WXiKQrPYZWC/hvrTsmk6zmxbI7LTXE7hVek/n8AfVgpe2y82zl3w0HvCHN0bVKMBoJcC0w==} engines: {node: '>= 10.0.0'} + '@peculiar/asn1-asym-key@2.9.4': + resolution: {integrity: sha512-s7SJfjcXlR3MYMngDTeMLCy3VjGaIxeEy905CQ0j09pDbeYhNBvBGA7r7cAvCZYkVFo9kVg9RAki0K2iUz7SGQ==} + engines: {node: '>=14'} + + '@peculiar/asn1-cms@2.9.4': + resolution: {integrity: sha512-cben7oxmQsUGZqotus7yt0srYdncOT6RNWcTQ77T2RFOXejYVYkXadrfePdRcrVpO9K95IRLKKglG2k38jKXuw==} + engines: {node: '>=14'} + + '@peculiar/asn1-csr@2.9.4': + resolution: {integrity: sha512-xd4YN4vpRjkDAQWVfZZkeu12IEND7DOpkqaHSIHxZl1uggUNa9Ju0QxY2jHvDAS9pP0zhRBytg8ifsnGo3V0jw==} + engines: {node: '>=14'} + + '@peculiar/asn1-ecc@2.9.4': + resolution: {integrity: sha512-JJXefFshRAuVAjWQo/39bkg1ywc1VaiO44S8RRC+Ykvf/u2KDmYffoDb0ZBPCR5uJy4AGKQhl8mX+Q8ShcWaXQ==} + engines: {node: '>=14'} + + '@peculiar/asn1-pfx@2.9.4': + resolution: {integrity: sha512-khuGzHTzNzk4GDlIBEILyIs6Lce0yn0ZBdoI9v93kmNncfZRhD+AQ5ODFqdhvoE8cMJF/JMTQ8yA+t1D14kqCw==} + engines: {node: '>=14'} + + '@peculiar/asn1-pkcs8@2.9.4': + resolution: {integrity: sha512-duRdotlUx9eDZe6QrQpQKl61RbWykCHBCkKayP8V8XdEFwlKHZ8qGGDMyS6Pye7OX7nLFttTTpRkJeet78ckwQ==} + engines: {node: '>=14'} + + '@peculiar/asn1-pkcs9@2.9.4': + resolution: {integrity: sha512-kaL4cNxBpdQE2dKlyZBqz4ygCrwffO+8wfoxTEqM1Z8RadvCeELBRzcv0dzM8aY9azHMwODO5nxU65zXmhToOQ==} + engines: {node: '>=14'} + + '@peculiar/asn1-rsa@2.9.4': + resolution: {integrity: sha512-pZ96eD1PptovcWQ/GSmuNFXd/7EQJNlKfDaNCyE2rx3W0v6QFelkzquVqRSRyyDXXCYD69ZXJDzZ8GhIiQzKoA==} + engines: {node: '>=14'} + + '@peculiar/asn1-schema@2.9.4': + resolution: {integrity: sha512-GjzePcT9Iw8NzeOPf73iNS9xM+TBhd/FilAfP+RQGkTMQJTVWtytN3JHJACCjf/ABNau5S7mS3g+DcuxmRgYEg==} + engines: {node: '>=14'} + + '@peculiar/asn1-x509-attr@2.9.4': + resolution: {integrity: sha512-ehQXbpQaQYycgu8OrvigwSPTFfVRcu0ECNYCWw+yzBp02Lw5paRqzzhUpfOgO2K38+WfFZuEz/0RPtam5g0OMg==} + engines: {node: '>=14'} + + '@peculiar/asn1-x509-post-quantum@2.9.4': + resolution: {integrity: sha512-GD0k7BY3dsEIeai7C7bVRPddj/PzZu+sTawRPRxdHKbdy1f9b58WQyk0eFJdr28GShyLVTU8poyi4+bTAZfEtQ==} + engines: {node: '>=14'} + + '@peculiar/asn1-x509@2.9.4': + resolution: {integrity: sha512-CxhBo/RdEbMMob7T31ZdQjGuoyRFLVwrDzTn25bihzBasRg9kRm/0IxIPvhgQtcK/9dNcO1XQL2fuPugwELL0Q==} + engines: {node: '>=14'} + + '@peculiar/json-schema@1.1.12': + resolution: {integrity: sha512-coUfuoMeIB7B8/NMekxaDzLhaYmp0HZNPEjYRm9goRou8UZIC3z21s0sL9AWoCw4EG876QyO3kYrc61WNF9B/w==} + engines: {node: '>=8.0.0'} + + '@peculiar/utils@2.0.3': + resolution: {integrity: sha512-+oL3HPFRIZ1St2K50lWCXiioIgSoxzz7R1J3uF6neO2yl1sgmpgY6XXJH4BdpoDkMWznQTeYF6oWNDZLCdQ4eQ==} + + '@peculiar/webcrypto@1.7.1': + resolution: {integrity: sha512-ODOov0sGMJMf3jPonOkgGqPknTsu+DdQ7kD++gz8aI+aFMOMHFbWAA2taqXXVTdP+OTOQR/znGvSpmkeI0WTYQ==} + engines: {node: '>=14.18.0'} + + '@peculiar/x509@2.1.0': + resolution: {integrity: sha512-IYbg1R03CSQGWwl24kGyqrdVtixNSbRaDvBg1r5wyYjTP+VwPQkka1BzTgU5+vxiuwqg04OxdvdJ1xYYFIdUSA==} + engines: {node: '>=20.0.0'} + '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} @@ -3689,6 +4019,58 @@ packages: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + '@react-native/asset-utils@0.87.1': + resolution: {integrity: sha512-FeFnbn9ENPs7IVBzZt1bBWfiRGvT4q+CsWwXGFyKVW/1ol3ylBRuTtXBGHIAmcNN4fUR60nSXsCN19pzNHkPgA==} + engines: {node: ^22.13.0 || ^24.3.0 || >= 26.0.0} + + '@react-native/codegen@0.87.1': + resolution: {integrity: sha512-qbaqEdlUfj2vRgvWTpMoNgHnEqAhAYJLUrpGkb0WC9n0kdtqUvgigpz4bDktZwolM9BemXwgGFgyiAnbs3t0xw==} + engines: {node: ^22.13.0 || ^24.3.0 || >= 26.0.0} + peerDependencies: + '@babel/core': '*' + + '@react-native/community-cli-plugin@0.87.1': + resolution: {integrity: sha512-aGBae6v+ngy8fIpU1EOaVxn/8DOgmJNphEdn5Eb0wTchUCxr8bDjpTJYNdrptyn3qI/elk8r9agQ2OBaFvrRlw==} + engines: {node: ^22.13.0 || ^24.3.0 || >= 26.0.0} + peerDependencies: + '@react-native-community/cli': '*' + '@react-native/metro-config': 0.87.1 + peerDependenciesMeta: + '@react-native-community/cli': + optional: true + '@react-native/metro-config': + optional: true + + '@react-native/debugger-frontend@0.87.1': + resolution: {integrity: sha512-RNm7soJB+8YSauLnsCCylA1eVfT6JWaDTPUEF01uu9YwDyZ7remKlZbXtmVbtscbNGcp+hbI4iZUdVOpQWsHuA==} + engines: {node: ^22.13.0 || ^24.3.0 || >= 26.0.0} + + '@react-native/debugger-shell@0.87.1': + resolution: {integrity: sha512-eNbKjcnseJIjy5XCDwyhKOT1ngOg3Dv1fVxTDtnVFqdqjqsbfY4v9JuJG1RZJ7+mFoRRe05HE8GSQU8B7n5xwQ==} + engines: {node: ^22.13.0 || ^24.3.0 || >= 26.0.0} + + '@react-native/dev-middleware@0.87.1': + resolution: {integrity: sha512-KgvAGUaVl6/XrWFAPK8e0ojDRbsdBjr4bnwyn6PC3CwxPQc5iv+i7hMoUwYS8ORSkHKfjt+cV86/mBQ4lG8yfA==} + engines: {node: ^22.13.0 || ^24.3.0 || >= 26.0.0} + + '@react-native/gradle-plugin@0.87.1': + resolution: {integrity: sha512-bZ5X2BNxaSlfp2KlDtUM910QqW9G1yTIeZXghuv4ag8SAQLzm5Mf9j5GjJfT6ax2Qo2aRT15Vi3Tro/yLGJstA==} + engines: {node: ^22.13.0 || ^24.3.0 || >= 26.0.0} + + '@react-native/normalize-colors@0.87.1': + resolution: {integrity: sha512-8+AutemzX+a7cuKgTUyb7JUk3sFYgLyrSnlTLrL6LuW/LKDE+FYE8lJGWYhJPJcE51Ge1D8yRzxgQEdEFZtuIw==} + + '@react-native/virtualized-lists@0.87.1': + resolution: {integrity: sha512-qSZjeX3UJrDvyfjf7yc3E68rp1XnzE+5nu8ImklhkVC0+p/XiaHPb/KGkRqDdnQyWHN55BRYSsCMEwgVI6WRNQ==} + engines: {node: ^22.13.0 || ^24.3.0 || >= 26.0.0} + peerDependencies: + '@types/react': ^19.2.0 + react: '*' + react-native: 0.87.1 + peerDependenciesMeta: + '@types/react': + optional: true + '@react-types/shared@3.36.0': resolution: {integrity: sha512-DkP/H0C2YjjS7gZWKNqOmU8a16qHPjQNdzMwmTq9SzplM6Iw0kVMTZ0OIoe6FOgGqa+FwMsE2QbPjh/n3g/jXQ==} peerDependencies: @@ -4079,12 +4461,19 @@ packages: engines: {node: '>= 8.0.0'} hasBin: true + '@sinclair/typebox@0.27.12': + resolution: {integrity: sha512-hhyNJ+nbR6ZR7pToHvllEFun9TL0sbL+tk/ON75lo+Xas054uez98qRbsuNt7MBCyZKK4+8Yli/OAGZhmfBZ/g==} + '@sinclair/typebox@0.34.40': resolution: {integrity: sha512-gwBNIP8ZAYev/ORDWW0QvxdwPXwxBtLsdsJgSc7eDIRt8ubP+rxUBzPsrwnu16fgEF8Bx4lh/+mvQvJzcTM6Kw==} '@sinclair/typebox@0.34.52': resolution: {integrity: sha512-XiMQh7qqVlxZzcVD+kkGMNGMzcTrDMLWI7S4x7z1MkCkbDPrekpZXEUK0eZqZFMuHQg2a2DZOcDIh9o5v3Gonw==} + '@sindresorhus/fnv1a@3.1.0': + resolution: {integrity: sha512-KV321z5m/0nuAg83W1dPLy85HpHDk7Sdi4fJbwvacWsEhAh+rZUW4ZfGcXmUIvjZg4ss2bcwNlRhJ7GBEUG08w==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + '@sindresorhus/is@4.6.0': resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==} engines: {node: '>=10'} @@ -5081,138 +5470,273 @@ packages: cpu: [x64] os: [win32] + '@yuku-codegen/binding-android-arm64@0.8.7': + resolution: {integrity: sha512-C/0zV5IhgVdYhGJTwrY0v8dknxlhiKwtVJkMUaexu9/QvRmzlV4vfU3hZlUSgqc2BxQHntL1mCVbDq8j0FRFDw==} + cpu: [arm64] + os: [android] + '@yuku-codegen/binding-android-arm64@0.9.5': resolution: {integrity: sha512-jrOY5WM+AaAqkv51fHP1x28ifto4WgcZVzodLUyMU1jMWn5Sq+VciRdk/n8E0Ey5w4p4cWWE+m5GfXWOYh7Kzw==} cpu: [arm64] os: [android] + '@yuku-codegen/binding-darwin-arm64@0.8.7': + resolution: {integrity: sha512-/u+REDMI4a0/lsJXTM4c53/w31OGZLOReZIyg62uhgLs0kc8NHsj/nOcxTdlQjq5gi0zhdkccD9LaLTXcdzPvw==} + cpu: [arm64] + os: [darwin] + '@yuku-codegen/binding-darwin-arm64@0.9.5': resolution: {integrity: sha512-4O4lkCQIzPZGjiNB1GORSI6hBECbiiSBS/APZXPvNKYH+nhY4uuqv03LNXA+SET3hoBjvr95P5rIhY8KQaQUBA==} cpu: [arm64] os: [darwin] + '@yuku-codegen/binding-darwin-x64@0.8.7': + resolution: {integrity: sha512-sMMzFOwCo4WXR+/6zIBThOocSC50iIIZZdfiIDbaLvj0Ax/rWt/iavyfEAqajyvzydLyCqR/ZItdLWSRlu1umw==} + cpu: [x64] + os: [darwin] + '@yuku-codegen/binding-darwin-x64@0.9.5': resolution: {integrity: sha512-9EvoUO0SEhD6/d8VHdWuPerepPMSR1y84+UEgz8Un1Ope14Oe7xMvePpEQLTLovoIFZ8Zg3iZ8z8E11pZMqC3g==} cpu: [x64] os: [darwin] + '@yuku-codegen/binding-freebsd-x64@0.8.7': + resolution: {integrity: sha512-MpdpKXix9P+Y1rKgjvcNeNtGjXeL1CmttNhYINrWls8kRpm4xM/oBGTmn6w7to8lAlwj5jm8q03dQPl5mRv4Qw==} + cpu: [x64] + os: [freebsd] + '@yuku-codegen/binding-freebsd-x64@0.9.5': resolution: {integrity: sha512-HqT78WwgHTmp8lwujoUa9CrIortX4DdpuiVC18ZPSGvuuJf4ylpIEI6QrQEM78Zwz3muhAWAOXZ5irdiYY+AyA==} cpu: [x64] os: [freebsd] + '@yuku-codegen/binding-linux-arm-gnu@0.8.7': + resolution: {integrity: sha512-rr1srFLlPAmC1vtxfc9C1YLDe3iH09YjfSeeIidBqKhzx1MATjOAq4mjlRUOnhr/L27MotWIYOFKwVsd5JZFOg==} + cpu: [arm] + os: [linux] + libc: [glibc] + '@yuku-codegen/binding-linux-arm-gnu@0.9.5': resolution: {integrity: sha512-QJXwIW6Ms3QIawbcBIedmrmXnfdpuAOjZJ/eAABq5XTzWvSxZ7lutu9W5yIHahlaTaFnBo7ikrVMD1szLTpPUw==} cpu: [arm] os: [linux] libc: [glibc] + '@yuku-codegen/binding-linux-arm-musl@0.8.7': + resolution: {integrity: sha512-eAufXh8qBRpiSO6ueaMDL+yyoXIGLhpUce72YbcACtZU2qhExwBIJyEtQf5kH2Ki0X2aqkSjSfog4OPtKXan3Q==} + cpu: [arm] + os: [linux] + libc: [musl] + '@yuku-codegen/binding-linux-arm-musl@0.9.5': resolution: {integrity: sha512-eHbFy3IHGb+IYarrYwAo0yWSEQV3eAmEn6VrsKA6I1Wy1YPJxWqSJlV69JhqsHtJuJlljUIF3ex0y46yaKIu9w==} cpu: [arm] os: [linux] libc: [musl] + '@yuku-codegen/binding-linux-arm64-gnu@0.8.7': + resolution: {integrity: sha512-gw4w6wPoHObBrdIC4duVWLmJOvpdE25j5D7yrM5mACNlK4klRz/lv8hK+ssQk9EJHBgZjSaqZIJVFgqNYbfv7A==} + cpu: [arm64] + os: [linux] + libc: [glibc] + '@yuku-codegen/binding-linux-arm64-gnu@0.9.5': resolution: {integrity: sha512-ByoJMbySTaDhjAXSu8q6Lh7HKg3YoesXpcT72aYk0Aiw4PCznmY4ybpLTq0RCvp0RIPhFm/6yECFiGBwyCC1nw==} cpu: [arm64] os: [linux] libc: [glibc] + '@yuku-codegen/binding-linux-arm64-musl@0.8.7': + resolution: {integrity: sha512-L68N6Y4XkqcIaKo3Ra88JEvBEH4AHff44A4INcrxeVWZ8CZtu2tCpfVxe3hR8qQQMcvBSQlDn24KpkCKEhVvfA==} + cpu: [arm64] + os: [linux] + libc: [musl] + '@yuku-codegen/binding-linux-arm64-musl@0.9.5': resolution: {integrity: sha512-gD9vfXIoBw1toSxhO9rgmSu/FEfy3PMznJAxVjIuH8DrWEiDKXmJO0pJKfj1Ltbe/TmtWVZR+TjISqeSIGQzMg==} cpu: [arm64] os: [linux] libc: [musl] + '@yuku-codegen/binding-linux-x64-gnu@0.8.7': + resolution: {integrity: sha512-dzyAbltJmf3Cqlb8HcFuYIf5Yn0fl1vTr3XJ9HiVNNvOlhqPSArOqtw9vI1p6/VTXTjrMLXlU+s+/kNHiIy/Cw==} + cpu: [x64] + os: [linux] + libc: [glibc] + '@yuku-codegen/binding-linux-x64-gnu@0.9.5': resolution: {integrity: sha512-BARvdnvqMGjOr5Iel2JH+9H5vAIE0R6H0Z2fsT02xrvmI/1ZXNB8lkuX+ZGevPhZb3zJ9WeC/R8JZstrsUOK5g==} cpu: [x64] os: [linux] libc: [glibc] + '@yuku-codegen/binding-linux-x64-musl@0.8.7': + resolution: {integrity: sha512-Otw4MH3404q0Bbvl+YTdW9aoUV5vXmUw8260bWvt1XlaoIX/ceSgI4ygheRDMfBPoHt6FDayQaO9OLVUZAgkFA==} + cpu: [x64] + os: [linux] + libc: [musl] + '@yuku-codegen/binding-linux-x64-musl@0.9.5': resolution: {integrity: sha512-x8flcevS1fbb7ESrmpOY/pON4eInSaE/7Ktjqx2udOE2W33BNSZmJuwpyUgo54AoHNqrcaM7szqydyJn5fNvew==} cpu: [x64] os: [linux] libc: [musl] + '@yuku-codegen/binding-win32-arm64@0.8.7': + resolution: {integrity: sha512-qo/jyrzryiBuKEsFiuWaBCBe3tRMynQ0qFWFgOEjcCMQeZfBm+wKiVEUEFXXLc7bh8YezguAWp0Mtnhq4ARNyA==} + cpu: [arm64] + os: [win32] + '@yuku-codegen/binding-win32-arm64@0.9.5': resolution: {integrity: sha512-KOL/rBatWqH4ZpCNoF8ZtSNdOJbAxBJLmk/VeRciepGROPTbPum1A9t67GpFgOU7qrkUWlPJdJ+KHKbvbmOt+w==} cpu: [arm64] os: [win32] + '@yuku-codegen/binding-win32-x64@0.8.7': + resolution: {integrity: sha512-D5lDsVDx6m00E6bWySlWdH72Ca4TPSaphDqB6QjU6MpuNLIJqoGoatYyq2rOmBE8Zv/kunot/o58KGL03P3eiA==} + cpu: [x64] + os: [win32] + '@yuku-codegen/binding-win32-x64@0.9.5': resolution: {integrity: sha512-FxENahEjWSan59Syh/us/Kf4wNq8qrJLFJ3R2N4Oiwtb6yNdz/rWLNTIoNSssnsp7IoWJdUP6o/Z8ppg7lXMcg==} cpu: [x64] os: [win32] + '@yuku-parser/binding-android-arm64@0.8.7': + resolution: {integrity: sha512-eGKYiUDX7Y0V7tDTmg+JTVnXnjMqfXXsorZ+EDf5kxwchQ3Or1HS14MzI2fw+jFhHR85fCWt+mtX33Yao73hIQ==} + cpu: [arm64] + os: [android] + '@yuku-parser/binding-android-arm64@0.9.5': resolution: {integrity: sha512-A2JCFCSHfnficqYEw4Iujpx7XrkMM3UfFcgJFmYpZSo9zM4nyhmdIIE0FogSduuU60lhM0/UcfuUBXIVNBMlGQ==} cpu: [arm64] os: [android] + '@yuku-parser/binding-darwin-arm64@0.8.7': + resolution: {integrity: sha512-Re0RHelKLnjEURulY2/KxW+Ngb8zuNA4BRZuMwgGQNzVumT6u4U2N2hc01oeYVNVof0i7GrXE4UCNBgbpRRnjQ==} + cpu: [arm64] + os: [darwin] + '@yuku-parser/binding-darwin-arm64@0.9.5': resolution: {integrity: sha512-3PiyU+Eare4YuKaQ22N98/yAiROPY5o/NQJHraICzDvk4pgS+m+bgLKOvkGBRn33OnV95Vdv1Mn38b+MQHpULQ==} cpu: [arm64] os: [darwin] + '@yuku-parser/binding-darwin-x64@0.8.7': + resolution: {integrity: sha512-Hn8DROtQkjlA1ACbPgj4a7eP9IuVOI504oiTwpkWPbpaDWD9KdmnVYCqW+1LfenNK/g7O9NhWGpXEdaCNX7lIA==} + cpu: [x64] + os: [darwin] + '@yuku-parser/binding-darwin-x64@0.9.5': resolution: {integrity: sha512-blFMAFI7AInI83XaiOF8cIeiRM46Nz9EfpZtZPRLbKxSA9aAr5v8aHYpmfN6NoyXxAv/10pwS4gsAuc1W6fy5Q==} cpu: [x64] os: [darwin] + '@yuku-parser/binding-freebsd-x64@0.8.7': + resolution: {integrity: sha512-bAP2OV8wRuzplX/jYxv9+vvqQT8JxyNphI8fLfXGL054Xs+4/J5u33cIm3y4rxY8rdoLmmdiJs2Tq7r7lrDRfA==} + cpu: [x64] + os: [freebsd] + '@yuku-parser/binding-freebsd-x64@0.9.5': resolution: {integrity: sha512-TsNuL4qsZdO0tHp5GW43Y5fHeLPpPHA675wdcPNTcdq2ZO5AvsQWFFoiDg8CH4oBktfsQ9tdvKhjLnLYwKvp4g==} cpu: [x64] os: [freebsd] + '@yuku-parser/binding-linux-arm-gnu@0.8.7': + resolution: {integrity: sha512-kTYwJQQgmZeAWdDIWabiReIZMpmfLueIj1tCmjStUtFGhR1Z0qwxonKVfUC4N7h/VhGGzLZ//7O1kgt1QKqgCg==} + cpu: [arm] + os: [linux] + libc: [glibc] + '@yuku-parser/binding-linux-arm-gnu@0.9.5': resolution: {integrity: sha512-b0afYK5gHeV8RdmOcqAlgM8ONsye4cax4DMnIBaoJyPMd1UTGvTbpkqQcPVdhmHlcfcGVGMV1laeVovlr/dscw==} cpu: [arm] os: [linux] libc: [glibc] + '@yuku-parser/binding-linux-arm-musl@0.8.7': + resolution: {integrity: sha512-uL4jE8HPT2BLlxAXyD10LqgPuXa9eDa0BKpCdSANmzIJghq/2eZo3/gQNtaxPZMupWoxjYzSad9IXrwu7aYPXQ==} + cpu: [arm] + os: [linux] + libc: [musl] + '@yuku-parser/binding-linux-arm-musl@0.9.5': resolution: {integrity: sha512-fD3lKzl+r6j6n8DwiMY53qnh5DLqD8KJjCp+NbVud54Nnh3Q9Wprqss3YzyMHP3NSJk663Wlg1E1X5qOuFVFig==} cpu: [arm] os: [linux] libc: [musl] + '@yuku-parser/binding-linux-arm64-gnu@0.8.7': + resolution: {integrity: sha512-3gVN4pWSKZmXiNX7cU164dR9MPvesCHnlH6nPfpK+yQsCuYphjKKplcb4SnZBrhiqmXbgb2HR0c2TS0IZUPhgA==} + cpu: [arm64] + os: [linux] + libc: [glibc] + '@yuku-parser/binding-linux-arm64-gnu@0.9.5': resolution: {integrity: sha512-aRU/aCphV1MWCl/lvI6NX8LgucHQ68Fx+vz7NBb/5MEr2EuzCxiPOQqeFcMdWh+2AED/p0AvAREch0c+cSnlhA==} cpu: [arm64] os: [linux] libc: [glibc] + '@yuku-parser/binding-linux-arm64-musl@0.8.7': + resolution: {integrity: sha512-S0mwfEjoLpxzXeZw802Wa4RaELsQiPtWqG6INcy8j4GtvNFtl4LCX3eGO1XLn9pyLAISLzTRyU3zUCBUPin8lg==} + cpu: [arm64] + os: [linux] + libc: [musl] + '@yuku-parser/binding-linux-arm64-musl@0.9.5': resolution: {integrity: sha512-5+Guro0l8H473YXlEjVNBRLN/IbPbJdnQh1zo0OLt49xxnON+XMJRRaEyTOTfkn9QSRg0+BhEKGR4W9Gu2uZJQ==} cpu: [arm64] os: [linux] libc: [musl] + '@yuku-parser/binding-linux-x64-gnu@0.8.7': + resolution: {integrity: sha512-lnbWdPmerE5D1uH1G4IEZKnPzCrWCStRGrtgpSIe1RibAo5bZIjDbbbPYXmMHCEh4F+x/JaJpElh26a3r+BPbg==} + cpu: [x64] + os: [linux] + libc: [glibc] + '@yuku-parser/binding-linux-x64-gnu@0.9.5': resolution: {integrity: sha512-pwwSyV9q+GlvzSXlsMZBMlgk22L5bud714/vqZCdeUTivzeUVltQzUaf3IQXOGXdpc59JYTeuMCtbGquaAUoCA==} cpu: [x64] os: [linux] libc: [glibc] + '@yuku-parser/binding-linux-x64-musl@0.8.7': + resolution: {integrity: sha512-769uwndMvMzUvATWbAcEvyLHKA+DzhHSCl/obBUrRdYfRo26yxui6S8y3z7uJ+Naup7UKrDxrpK7OnQkxkl9KQ==} + cpu: [x64] + os: [linux] + libc: [musl] + '@yuku-parser/binding-linux-x64-musl@0.9.5': resolution: {integrity: sha512-z18j6JN3lBHH8vzN7gG1M8fI0nlgItSfnC9PfbmUbt97iHViKfw2iA2G9fGwRMe5LyPRZBrejIlapbygPbpdaw==} cpu: [x64] os: [linux] libc: [musl] + '@yuku-parser/binding-win32-arm64@0.8.7': + resolution: {integrity: sha512-mEB/9PlaAkisJ6KWGz0zvywXoU6+80dTlR2LwS7s/jcXXoU6fm2+sitBZXtqu3+Q4DcDgPxM45uWMCzPs0TSRw==} + cpu: [arm64] + os: [win32] + '@yuku-parser/binding-win32-arm64@0.9.5': resolution: {integrity: sha512-s6Gwttb1dQvtPX6Bgkw+UPC9IO3UBDXc6Zezog8MMgvu3UfJBBB9TQqKBX/2quu0Eyh+lLSAyNlIyyecaagUPg==} cpu: [arm64] os: [win32] + '@yuku-parser/binding-win32-x64@0.8.7': + resolution: {integrity: sha512-8vNB2DP0ou61nGb8tc/qfi41gfyDXz1MHr2zqL3nR+cJ6CEbiuWV/l/a/vv151gCgiZLLAyGkQGENpozdg716w==} + cpu: [x64] + os: [win32] + '@yuku-parser/binding-win32-x64@0.9.5': resolution: {integrity: sha512-FrERt9YWatY3bJfSUdi4YWY+6iQcyo3MzCC821BxlWM5TFZEHijnpz/bknR79VHlXY/9CvXz8ZlaAGPsTtN3nw==} cpu: [x64] os: [win32] + '@yuku-toolchain/types@0.8.7': + resolution: {integrity: sha512-2Z53dNxAJL6UvFoIrDZvYf3zlO8s4VJK4O2hhaB4mXVwwpX/7ajtss3cmfqKvamlNLWyt9FSWs4eoYdlbxpnHA==} + '@yuku-toolchain/types@0.9.3': resolution: {integrity: sha512-rFE+5P4g2wxko5C85MugJOlVjBHEQq87dIkhLkniLXLp63PEtgaFjD954i5HXlfnyzLxPcZHsSOVDVgmo1HToA==} @@ -5232,6 +5756,9 @@ packages: resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} engines: {node: '>=6.5'} + abort-error@1.0.2: + resolution: {integrity: sha512-lVgvB2NyPLqbXXhVmXcYFTC1x5K7CiVdPgdY7LGgFQWC8506oN01sPN3i9cl9ynuwF4iJ0TS9exnR7cZ9FuX4w==} + accepts@2.0.0: resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} engines: {node: '>= 0.6'} @@ -5263,6 +5790,10 @@ packages: resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} engines: {node: '>= 6.0.0'} + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + ajv-formats@2.1.1: resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} peerDependencies: @@ -5299,6 +5830,9 @@ packages: resolution: {integrity: sha512-7ML6fa2K93FIfifG3GMWhDEwT5qQzPTmoHKCTvhzGEwdbQ4n0yYUWZlLYT75WllTGJCJtNUI0C1ybN4BCegqvg==} engines: {node: '>= 14.0.0'} + anser@1.4.10: + resolution: {integrity: sha512-hCv9AqTQ8ycjpSd3upOJd7vFwW1JaoYQ7tpham03GJ1ca8/65rqn0RpaWpItOAd6ylW9wAw6luXYPJIyPFVOww==} + ansi-escapes@4.3.2: resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} engines: {node: '>=8'} @@ -5331,6 +5865,10 @@ packages: resolution: {integrity: sha512-BJ8/l4R5LRE7hW9WdSuGYrLSHi2ynxeFpDFbH0K/CgNeY/tyhk+vO6TYxXC5r5CpUhNVX310xzPsN/H9lCdfOA==} engines: {node: '>=14'} + any-signal@4.2.0: + resolution: {integrity: sha512-LndMvYuAPf4rC195lk7oSFuHOYFpOszIYrNYv0gHAvz+aEhE9qPZLhmrIz5pXP2BSsPOXvsuHDXEGaiQhIh9wA==} + engines: {node: '>=16.0.0', npm: '>=7.0.0'} + anymatch@3.1.3: resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} engines: {node: '>= 8'} @@ -5397,6 +5935,10 @@ packages: asap@2.0.6: resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} + asn1js@3.0.10: + resolution: {integrity: sha512-S2s3aOytiKdFRdulw2qPE51MzjzVOisppcVv7jVFR+Kw0kxwvFrDcYA0h7Ndqbmj0HkMIXYWaoj7fli8kgx1eg==} + engines: {node: '>=12.0.0'} + assertion-error@2.0.1: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} @@ -5450,6 +5992,9 @@ packages: resolution: {integrity: sha512-gtGo1B+u14jrZQv6TdSWIWkTqclboo7Qn+dFAGUOIuXLFuJKAdg3U5MIWzZnW4vfUb4dX9skmAMiby86e/SF4A==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + babel-plugin-syntax-hermes-parser@0.36.1: + resolution: {integrity: sha512-ycduwJbvdvIMmVvlAZqGggS+pm5Eu4Bk9pcV9Sm2Z4PJNRVsKkv0g7vHj+LeuC1gHTeF67sJXFOq61IlqCa2hA==} + babel-preset-current-node-syntax@1.2.0: resolution: {integrity: sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==} peerDependencies: @@ -5481,6 +6026,9 @@ packages: resolution: {integrity: sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==} engines: {node: '>= 0.4'} + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + baseline-browser-mapping@2.11.20: resolution: {integrity: sha512-H0ulySigv6icDJ1F7SjtdCD6PrhTpdYCmP0CactWy1+ekh0AFd0o1Wn5T8b+hnTmdBx19u9yhL6wvCylXMY7zw==} engines: {node: '>=6.0.0'} @@ -5665,10 +6213,21 @@ packages: resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} engines: {node: '>= 20.19.0'} + chrome-launcher@0.15.2: + resolution: {integrity: sha512-zdLEwNo3aUVzIhKhTtXfxhdvZhUghrnmkvcAq2NoDd+LeOHKf03H5jwZ8T/STsAlzyALkBVK552iaG1fGf1xVQ==} + engines: {node: '>=12.13.0'} + hasBin: true + chrome-trace-event@1.0.4: resolution: {integrity: sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==} engines: {node: '>=6.0'} + chromium-edge-launcher@0.3.0: + resolution: {integrity: sha512-p03azHlGjtyRvFEee3cyvtsRYdniSkwjkzmM/KmVnqT5d7QkkwpJBhis/zCLMYdQMVJ5tt140TBNqqrZPaWeFA==} + + ci-info@2.0.0: + resolution: {integrity: sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==} + ci-info@3.9.0: resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} engines: {node: '>=8'} @@ -5757,6 +6316,10 @@ packages: comma-separated-tokens@2.0.3: resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + commander@12.1.0: + resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} + engines: {node: '>=18'} + commander@15.0.0: resolution: {integrity: sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg==} engines: {node: '>=22.12.0'} @@ -5789,6 +6352,10 @@ packages: resolution: {integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==} engines: {'0': node >= 6.0} + connect@3.7.0: + resolution: {integrity: sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==} + engines: {node: '>= 0.10.0'} + content-disposition@1.1.0: resolution: {integrity: sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==} engines: {node: '>=18'} @@ -5819,6 +6386,9 @@ packages: resolution: {integrity: sha512-fereAvAvxDrQDOXybk3Qu3dPbOoKoysFMWtkY3mv5BsL8//OSZVL5DCLYqgRfY5cWirgRzlC+WSrxp6Bo3eNZg==} hasBin: true + core-js@3.50.0: + resolution: {integrity: sha512-BRWgOLKkFeCgRudR6zrs8p9XJZcE14grzKMMssoYrk6krtuEZ7MTKPIY5RzOnqsEKIR9kst7wNzphttraT+Yqw==} + core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} @@ -6044,9 +6614,20 @@ packages: dataloader@2.2.3: resolution: {integrity: sha512-y2krtASINtPFS1rSDjacrFgn1dcUuoREVabwlOGOe4SdxenREqwjwjElAdwvbGM7kgZz9a3KVicWR7vcz8rnzA==} + datastore-core@12.0.1: + resolution: {integrity: sha512-pfgIE5LG0Z5oyc6TxiLaF/0j4Gh1Yrs2+Ck/qssf9Oxw4s4YKC3zpqifuzThnKakAFWONr/vRv+66Koc0yJvyA==} + dayjs@1.11.23: resolution: {integrity: sha512-QDTCU0M0MxR3hQfnlDJfwekQiaanm1ubOD231u73WBckQ/fsamwRLiE2GBz6D3a/xF1NgfiDLJjXBa1hYOYTtQ==} + debug@2.6.9: + resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + debug@3.2.7: resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} peerDependencies: @@ -6055,6 +6636,15 @@ packages: supports-color: optional: true + debug@4.3.4: + resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} @@ -6103,6 +6693,10 @@ packages: delaunator@5.1.0: resolution: {integrity: sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==} + delay@7.0.0: + resolution: {integrity: sha512-C3vaGs818qzZjCvVJ98GQUMVyWeg7dr5w2Nwwb2t5K8G98jOyyVO2ti2bKYk5yoYElqH3F2yA53ykuEnwD6MCg==} + engines: {node: '>=20'} + delayed-stream@1.0.0: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} @@ -6115,6 +6709,10 @@ packages: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} + destroy@1.2.0: + resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + detect-libc@2.1.2: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} @@ -6211,6 +6809,10 @@ packages: resolution: {integrity: sha512-YGRs8knHhKHVShLkFET/rWAU8kmHbOV5LwN938RHI0pljAJ1Gf6SzXsSmRaEzcXTtOOmVqJ5+WtQPL5uigY50Q==} engines: {node: '>=14'} + encodeurl@1.0.2: + resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} + engines: {node: '>= 0.8'} + encodeurl@2.0.0: resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} engines: {node: '>= 0.8'} @@ -6234,6 +6836,9 @@ packages: error-ex@1.3.4: resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} + error-stack-parser@2.1.4: + resolution: {integrity: sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==} + es-abstract-get@1.0.0: resolution: {integrity: sha512-6PMWXpdhshVvFp+FoWYs1EvG1Nj0tvk0dZM+XcK0xMEM1czRVcP6ohqPWHy6qPagSpC8j4+p89WXlT+xXJs/fg==} engines: {node: '>= 0.4'} @@ -6557,6 +7162,9 @@ packages: eventemitter3@2.0.3: resolution: {integrity: sha512-jLN68Dx5kyFHaePoXWPsCGW5qdyZQtLYHkxkg02/Mz6g0kYpDx4FyP6XfArhQdlOC4b8Mv+EMxPo/8La7Tzghg==} + eventemitter3@5.0.4: + resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} + events@3.3.0: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} @@ -6581,6 +7189,9 @@ packages: resolution: {integrity: sha512-m8YrYgvKe9+9gEnWEuKz+qCGfHqkrff7PPfyDnOFkjsfYRKqiYyOxDFMFieCGAuhtk/VNm63tvNgKZVzVy+Hvg==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + exponential-backoff@3.1.3: + resolution: {integrity: sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==} + express@5.2.1: resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} engines: {node: '>= 18'} @@ -6629,6 +7240,11 @@ packages: fastq@1.20.1: resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} + fb-dotslash@0.5.8: + resolution: {integrity: sha512-XHYLKk9J4BupDxi9bSEhkfss0m+Vr9ChTrjhf9l2iw3jB5C7BnY4GVPoMcqbrTutsKJso6yj2nAB6BI/F2oZaA==} + engines: {node: '>=20'} + hasBin: true + fb-watchman@2.0.2: resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} @@ -6659,6 +7275,10 @@ packages: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} + finalhandler@1.1.2: + resolution: {integrity: sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==} + engines: {node: '>= 0.8'} + finalhandler@2.1.1: resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} engines: {node: '>= 18.0.0'} @@ -6681,6 +7301,9 @@ packages: flatted@3.4.4: resolution: {integrity: sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==} + flow-enums-runtime@0.0.6: + resolution: {integrity: sha512-3PYnM29RFXwvAN6Pc/scUfkI7RwhQ/xqyLUyPNlXUp9S40zI8nup9tUSrTLSVnWGBN38FNiGWbwZOB6uR4OGdw==} + follow-redirects@1.16.0: resolution: {integrity: sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==} engines: {node: '>=4.0'} @@ -6728,6 +7351,10 @@ packages: react-dom: optional: true + fresh@0.5.2: + resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} + engines: {node: '>= 0.6'} + fresh@2.0.0: resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} engines: {node: '>= 0.8'} @@ -6901,6 +7528,10 @@ packages: resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} engines: {node: '>=8.0.0'} + get-port@7.2.0: + resolution: {integrity: sha512-afP4W205ONCuMoPBqcR6PSXnzX35KTcJygfJfcp+QY+uwm3p20p1YczWXhlICIzGMCxYBQcySEcOgsJcrkyobg==} + engines: {node: '>=16'} + get-proto@1.0.1: resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} engines: {node: '>= 0.4'} @@ -6916,6 +7547,10 @@ packages: get-tsconfig@4.14.3: resolution: {integrity: sha512-++QEw4DIY7WGoukz+/+A/8dGYPT9l9yIadnmSgZ8Rjr3YVSVDipQSO9CdnJo9ePqFqUUqh+wk9uIaoiAwsiPkA==} + get-tsconfig@5.0.0-beta.5: + resolution: {integrity: sha512-/6gFNr0N04nob252sTQxyFLi3eKFRqIg1I87YcqAMT1i6SQrSF6KujUEQrtrjMV0H/eejTCltLdDSTEMzHbnsQ==} + engines: {node: '>=20.20.0'} + get-tsconfig@5.0.0-beta.6: resolution: {integrity: sha512-X6fBC0pmImC70gvX2zm56go9hx0MyoGVdG0tUCkg/D+Xnh5TJsOZ7iDbOdI3PvmtrDxnu1YdDufpK2QJX1Meqw==} engines: {node: '>=20.20.0'} @@ -7023,6 +7658,9 @@ packages: resolution: {integrity: sha512-iZyKG96/JwPz1N55vj2Ie2vXbhu440zfUfJvSwEqEbeLluk7NnapfGqa7LH0mOsnDxTF85Mx8/dyR6HfqcbmbQ==} engines: {node: '>=20'} + hashlru@2.3.0: + resolution: {integrity: sha512-0cMsjjIC8I+D3M44pOQdsy0OHXGLVz6Z0beRuufhKa0KfaD2wGwAev6jILzXsd3/vpnNQJmWyZtIILqM1N+n5A==} + hasown@2.0.4: resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} engines: {node: '>= 0.4'} @@ -7054,12 +7692,21 @@ packages: hastscript@9.0.1: resolution: {integrity: sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==} + hermes-compiler@250829098.0.17: + resolution: {integrity: sha512-qG1PXzTEtriF6oQLZF3vyHhSMxOdW5h2TqqLri0rdpstPustd2fSvRZQMVAPdlhgFwBfYnj3OUZtiO6LjYsEFw==} + hermes-estree@0.25.1: resolution: {integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==} + hermes-estree@0.36.1: + resolution: {integrity: sha512-guv1nQ6IJ7S83NRFPWc3SA7IBZrdNC9kapwOq6uXvF4wP+sDCgjzQbKPCoyYmoyZRzztF/n/c36l/rccCZSiCw==} + hermes-parser@0.25.1: resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==} + hermes-parser@0.36.1: + resolution: {integrity: sha512-GApNk4zLHi2UWoWZZkx7LNCOSzLSc5lB55pZ/PhK7ycFeg7u5LcF88p/WbpIi1XUDtE0MpHE3uRR3u3KB7TjSQ==} + hex-rgb@4.3.0: resolution: {integrity: sha512-Ox1pJVrDCyGHMG9CFg1tmrRUMRPRsAWYc/PinY0XzJU4K7y7vjNoLKIQ7BR5UJMCxNN8EM1MNDmHWA/B3aZUuw==} engines: {node: '>=6'} @@ -7093,6 +7740,10 @@ packages: resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} engines: {node: '>= 6'} + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + human-id@4.2.1: resolution: {integrity: sha512-zPGsiS+dWoTZtZ4AtpA9Y+BdSFSNWvnouNlWNoUFyAM6xHOHmdCvqO3k8AIbdamCOv4gUFUVNPf6rJFfc4UiJw==} hasBin: true @@ -7120,6 +7771,11 @@ packages: resolution: {integrity: sha512-brTTsvFRt5C1gGHtPst/281UjPD5t9fBqbgoMPlVWy11ZLTPfu7HxK4ZYqO9H7o/yC9rSTCI85EaQ4OoY12qYw==} engines: {node: '>= 4'} + image-size@1.2.1: + resolution: {integrity: sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==} + engines: {node: '>=16.x'} + hasBin: true + immutable@4.3.7: resolution: {integrity: sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw==} @@ -7157,6 +7813,12 @@ packages: inline-style-parser@0.2.7: resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==} + interface-datastore@10.0.1: + resolution: {integrity: sha512-DYMj/Og5Cz1Qwkx6/x5KRvR8SYEX7rVAv3KKCm2NzTwWSfpNAC4PahjcYbHyoZBP6zPWrhQv5n5wE+vaDdgSAg==} + + interface-store@8.0.0: + resolution: {integrity: sha512-e2+s3EEROzM+Wlas4hU3zveTUscvVMf1BOvdsJfpzFm19SoEXLVadpACjWOnM491HqGpvtfFnevyiaN8W+I6Eg==} + internal-slot@1.1.0: resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} engines: {node: '>= 0.4'} @@ -7168,6 +7830,9 @@ packages: resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} engines: {node: '>=12'} + invariant@2.2.4: + resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==} + ipaddr.js@1.9.1: resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} engines: {node: '>= 0.10'} @@ -7226,10 +7891,18 @@ packages: is-decimal@2.0.1: resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==} + is-docker@2.2.1: + resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} + engines: {node: '>=8'} + hasBin: true + is-document.all@1.0.0: resolution: {integrity: sha512-+XSoyS05OdBbhFuELhgTCpFNHkpBOJqtsZfUFFpe5QTw+9Sjbh8zitxhQkYAo6wV7e1Vb8cAPvpCk9jGam/82g==} engines: {node: '>= 0.4'} + is-electron@2.2.2: + resolution: {integrity: sha512-FO/Rhvz5tuw4MCWkpMzHFKWD2LsfHzIb7i6MdPYZ/KW7AlxawyLkqdy+jPZP1WubqEADE3O4FUENlJHDfQASRg==} + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -7261,6 +7934,9 @@ packages: resolution: {integrity: sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==} engines: {node: '>=12'} + is-loopback-addr@2.1.0: + resolution: {integrity: sha512-77bB59Coq+vGTibZlnuYVU1bUuiboGZ62ZJ7TqmgYKb+hko9dSlqS5OfqvmPZhwJys48At4y4D1UJP3GuvoXtQ==} + is-map@2.0.3: resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} engines: {node: '>= 0.4'} @@ -7269,6 +7945,10 @@ packages: resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} engines: {node: '>= 0.4'} + is-network-error@1.3.2: + resolution: {integrity: sha512-PhBY86zaxNZUuWP6h13Vu5oFe0XY6/UlKzQnYFELzGVHygP3MxmvTfYSG7GN3aIab/iWudSMgjSnG9Dq+nHrgA==} + engines: {node: '>=16'} + is-number-object@1.1.1: resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} engines: {node: '>= 0.4'} @@ -7332,6 +8012,10 @@ packages: resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==} engines: {node: '>= 0.4'} + is-wsl@2.2.0: + resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} + engines: {node: '>=8'} + isarray@0.0.1: resolution: {integrity: sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==} @@ -7369,6 +8053,64 @@ packages: resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} engines: {node: '>=8'} + it-all@3.0.11: + resolution: {integrity: sha512-Gvqj6MO4GMLnFdtE68HZRpGBskNC+9+GQ+JevTGNYLyhjUuPhjDLU3jN1LpBemXJDW1bRSkczqA/qGyKlPKrcQ==} + + it-byte-stream@3.0.0: + resolution: {integrity: sha512-dpTm5CaRFuz7Wp8VRo8TLkhVNx4KCNT7fAtVorAz/CxrwBJwyz6a6Au9inPxqDN6kN7VVN3WUweJCziG39T48w==} + + it-drain@3.0.12: + resolution: {integrity: sha512-RaFA9X1PF2Pf1Jlqhgf5PlXLgf6CaZt7tSzhia+EkEVcAJRKa0Uhr8UnjVv0GmOA3Air9jDJfIX2KIvz5hZ1Ag==} + + it-filter@3.1.6: + resolution: {integrity: sha512-yXiGPAvJn/exXjVFSCMQc3+J/7RLpOMwKoY2DH1yMhF4lYkdRoAdOwU0vnDACAlRAexf7AZvESZIc9mzhEoi/A==} + + it-length-prefixed-stream@3.0.1: + resolution: {integrity: sha512-XffBM6F8exghmr9jLQa2TdnwYm/Y5a1zC/v9NFFIiGrDb4lgJfo3YBlXHwjtCqbOJBQ4idO5agY8uUM9VUEb8Q==} + + it-length-prefixed@11.0.1: + resolution: {integrity: sha512-0Cy4RHFiL2CH060Lkigq0N37VaPg7oSylG6YjXErq+ccJFYcNEWNzxNjbqceio/sY1C+q84vZXOCE6/C0flYfQ==} + + it-map@3.1.6: + resolution: {integrity: sha512-wCix0FXImtIPIxhCnbz35RqWs00e/CReSZX9nZq1j46JcAzBBp57ob9/2l1WnDYEaUURIR8xCyg2NsWbOwBJFQ==} + + it-merge@3.0.15: + resolution: {integrity: sha512-rUskoyECUdJsUwpfBtiLXddm56J+vukMztq+PpfTss1mRl/aHv1YJs+6Q2XnpvDThFC2y4Xm636X0xh6IecPGg==} + + it-parallel@3.0.16: + resolution: {integrity: sha512-qr3nTgj4fraSfr6Ix9JkHsy/Yb/4vmS3QIAu3fsX52r2SOTLbohXoixKp6AIhTJcorpKwjP0VFcpHr5V54jivA==} + + it-peekable@3.0.10: + resolution: {integrity: sha512-2E6+p1pelZOhzp69aaiiBuEybWzAl10uYbIdCR3Pxy8bFNnS/kgpbLtGbNbIZ6RVdU7yHHkmATYwjy52GfFEKA==} + + it-pipe@3.0.1: + resolution: {integrity: sha512-sIoNrQl1qSRg2seYSBH/3QxWhJFn9PKYvOf/bHdtCBF0bnghey44VyASsWzn5dAx0DCDDABq1hZIuzKmtBZmKA==} + engines: {node: '>=16.0.0', npm: '>=7.0.0'} + + it-protobuf-stream@3.0.1: + resolution: {integrity: sha512-lKjTCPwL+aKUDovZUt4CyUAsYlOv9z09zyNnnGVWaQmbO6f0n6vV12F4hg0YwWyk9Er6UVWdu1EM7d6P62JnHw==} + + it-pushable@3.2.4: + resolution: {integrity: sha512-WSD7Ss4oCRfDZJT4ldLWr0Bom/muY90xxoJ5PQnU3uSKf0kxCOeehqZtiJX1ARqn+ymXGh1bxpDW9bDNHp2ivQ==} + + it-queue@1.1.4: + resolution: {integrity: sha512-cGH0+YfMPmwQ+lV+MhJcOXqOgE04h0zQCXvDUmvGGTyW0VqxJZbi7V+PQFEuPVN/5dHQg5RY9aVjMCgmxXaE5Q==} + + it-queueless-pushable@2.0.5: + resolution: {integrity: sha512-BaKqGLL1AQMR1AEaxiM09vzJQVXHHhfhh9UV0qPqORw/8Rm8igDQqT1qHregfHIb1NIW9jxQ/aXBibHJyuivuQ==} + + it-reader@7.0.0: + resolution: {integrity: sha512-bTWQPHH1if8N1K9XeidDjhTDm51ALbOksMa9uCZVVsQkoIPXP0XVKM88/Ynqr7HS18La/P1Kvu7C73j4rRoKWQ==} + + it-sort@3.0.11: + resolution: {integrity: sha512-eZ22LAoNLx4i4gVV44tJPoUYf/o+mHKa6+OigdVH/hmsdA2qoJN6MNPvKZyZKBf6+S/8PBE44zyvkzdYGkRhbA==} + + it-stream-types@2.0.4: + resolution: {integrity: sha512-tsX+klvMQ53J4Jm2B52vCIs7WD609ck+VS9X2TKMEv7VPY9VwaYKmSWyHek5QS0wHBtP0bWj9KMqCtAHgVKiXw==} + + it-take@3.0.11: + resolution: {integrity: sha512-zvoeEjLViGFyhYT5KNCgmcIH90Si8lCve4aTMvgej/ZQRfB9YzrcJW3UHIJjbQ9TiAnsT4vsWDImEFQNk5xmnA==} + iterare@1.2.1: resolution: {integrity: sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q==} engines: {node: '>=6'} @@ -7433,6 +8175,10 @@ packages: resolution: {integrity: sha512-LrPj3sPMjsQoOB3jrb8p/sa+XkSFNKo60TTsyc+EB2kxQJHgbwElpXnx1yX25fdFn5958FIObRtcLSHyV8VIAw==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-get-type@29.6.3: + resolution: {integrity: sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-haste-map@30.5.1: resolution: {integrity: sha512-VIFgt67jW480YDxKfEv9IYQKrFpYt7bOCMn3VsnjK7AK9qo7p6acpnA1DHwsVOULE3dTaYew/6JaTD/d0VDHoQ==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} @@ -7493,6 +8239,10 @@ packages: resolution: {integrity: sha512-cNWFdSb5xuDGl8hKkAZJ3YtI/PzHpAPFV+HUXWIOG8rMhpDTLVbvAc2d2wRicoYw2wGsJGLaurJT6BLC97bLXQ==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-util@29.7.0: + resolution: {integrity: sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-util@30.0.5: resolution: {integrity: sha512-pvyPWssDZR0FlfMxCBoc0tvM8iUEskaRFALUtGQYzVEAqisAztmy+R8LnU14KT4XA0H/a5HMVTXat1jLne010g==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} @@ -7501,6 +8251,10 @@ packages: resolution: {integrity: sha512-yKuxmNy2rSbTXw+3SIPanJo+nV4/BS1p26v44IYBFMsswSQySfMMcPHErnOncda7i9HEz0q605rIhSTBVgrZTg==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-validate@29.7.0: + resolution: {integrity: sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-validate@30.5.1: resolution: {integrity: sha512-i/buJ56wTpxihE93hQYNMfdOThS87+HGvLZzZJmU4xggPcdTYQq051iwALLCHp3q+SkCGH+EFejQZz5EbBSkkg==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} @@ -7513,6 +8267,10 @@ packages: resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} engines: {node: '>= 10.13.0'} + jest-worker@29.7.0: + resolution: {integrity: sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-worker@30.5.1: resolution: {integrity: sha512-Cbxh5v7AoLuFRmFJSM4/aHdQ68rjXvUWr716EE0Dh3I7T+T/3FgFKhOERGXHcU2Meftq9+9zxPM3TSNyI9D+HA==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} @@ -7562,6 +8320,9 @@ packages: jsbi@4.3.2: resolution: {integrity: sha512-9fqMSQbhJykSeii05nxKl4m6Eqn2P6rOlYiS+C5Dr/HPIU/7yZxu5qzbs40tgaFORiw2Amd0mirjxatXYMkIew==} + jsc-safe-url@0.2.4: + resolution: {integrity: sha512-0wM3YBWtYePOjfyXQH5MWQ8H7sdk5EXSwZvmSLKk2RboVQ2Bu239jycHDz5J/8Blf3K0Qnoy2b6xD+z10MFB+Q==} + jsesc@0.5.0: resolution: {integrity: sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==} hasBin: true @@ -7646,9 +8407,15 @@ packages: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} + libp2p@3.3.11: + resolution: {integrity: sha512-+J3Dw1RnGdcpQaPN1HD+Z6xVS3qY0vnsqE2Tu//pfoiJxabaZVkc2FBNnNQ1Sj+Th2qoloeJ11jvxuNXSa1gtA==} + libphonenumber-js@1.13.7: resolution: {integrity: sha512-rvr3HIMdOgzhz1RFGjftji+wjoAFlzhqCNqJOU/MKTZQ8d9NZxAR/tI+0weDicyoucqVR0U1GCniqHJ0f8aM2A==} + lighthouse-logger@1.4.2: + resolution: {integrity: sha512-gPWxznF6TKmUHrOQjlVo2UbaL2EJ71mb2CCeRs/2qBpi4L/g4LUVc9+3lKQ6DTUZwJswfM7ainGrLO1+fOqa2g==} + lightningcss-android-arm64@1.32.0: resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} engines: {node: '>= 12.0.0'} @@ -7770,6 +8537,9 @@ packages: lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + lodash.throttle@4.1.1: + resolution: {integrity: sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==} + log-symbols@7.0.1: resolution: {integrity: sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==} engines: {node: '>=18'} @@ -7811,6 +8581,9 @@ packages: magicast@0.5.4: resolution: {integrity: sha512-llBEhWm1SacoRwgHUoQJYtwp4PBLF4faQi5TCpIGyGs9n4y5+juI0tDgyKIfpqxckRHaHzouUEph3THklWh03w==} + main-event@1.0.5: + resolution: {integrity: sha512-4l9z8r7Q446mhhVTwdHmfPksOYBwN2xa7ewEW2yxPVNQapIaAf57ORFLMB91SpMEMa4wXowect7fq3uMLjDnGA==} + make-dir@4.0.0: resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} engines: {node: '>=10'} @@ -7818,6 +8591,9 @@ packages: make-error@1.3.6: resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} + makeerror@1.0.12: + resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==} + markdown-extensions@2.0.0: resolution: {integrity: sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q==} engines: {node: '>=16'} @@ -7839,6 +8615,9 @@ packages: engines: {node: '>= 20'} hasBin: true + marky@1.3.0: + resolution: {integrity: sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ==} + math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} @@ -7909,6 +8688,9 @@ packages: resolution: {integrity: sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==} engines: {node: '>= 4.0.0'} + memoize-one@5.2.1: + resolution: {integrity: sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==} + merge-descriptors@2.0.0: resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} engines: {node: '>=18'} @@ -7927,6 +8709,64 @@ packages: resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} engines: {node: '>= 0.6'} + metro-babel-transformer@0.87.0: + resolution: {integrity: sha512-IEn1K1FyY4J1sA5y6zqDjf2OkfmpTEqhZOeP6MJX8HepSW0cuHGw1m8bYOdv2adkG3XUE9dtM0csUs0gP/Xa5w==} + engines: {node: ^22.13.0 || ^24.3.0 || >= 26.0.0} + + metro-cache-key@0.87.0: + resolution: {integrity: sha512-Q+MPt6jl0zQogr4Q02WaJK6HY+GtE5A0nzj8kIV1Owgrx6OMNvm6scPTr1SM/R4LpCE8EH/Y5qfbXQ84GHTr0Q==} + engines: {node: ^22.13.0 || ^24.3.0 || >= 26.0.0} + + metro-cache@0.87.0: + resolution: {integrity: sha512-146vS1BMSKcp99jddOhFBfHwzUEWN35NrsnSJDF2sQQ0ZT5OsBcOjd574PM233TWEZISRJ5DOK+vokD+1ubx+w==} + engines: {node: ^22.13.0 || ^24.3.0 || >= 26.0.0} + + metro-config@0.87.0: + resolution: {integrity: sha512-yZ9QAIzWH9MxwrzwRlX/CBGRWOT14l7klSDYg8hdtSdnoUs5A7MQRdHE2KB9iHVzGQW5wgWM5aXJswNeWbSQPA==} + engines: {node: ^22.13.0 || ^24.3.0 || >= 26.0.0} + + metro-core@0.87.0: + resolution: {integrity: sha512-yW57+pCOHRC/CJZ99GA2PTd+30dORwDAjUPRCokj91IWW5In9Jwtt2FB5wACrGO8P0GHTyVHdTwDNyZsNndUbA==} + engines: {node: ^22.13.0 || ^24.3.0 || >= 26.0.0} + + metro-file-map@0.87.0: + resolution: {integrity: sha512-Dc57t8jsINwA90bbVlqaeDlxf1rVGgj5SmOEnOMbaHNUM/HCYYTJxPV8SRdOBwh7qTz/biO9vaQvBTjRBgbbsg==} + engines: {node: ^22.13.0 || ^24.3.0 || >= 26.0.0} + + metro-minify-terser@0.87.0: + resolution: {integrity: sha512-tPa0O983PDutFu3LXbArRH5NduogcKrvW6fs9VHhksTKUA1iqDyoD1ZSj/Me52xJ6T9/9pOwIVyj9ulKc/zMkg==} + engines: {node: ^22.13.0 || ^24.3.0 || >= 26.0.0} + + metro-resolver@0.87.0: + resolution: {integrity: sha512-Xl3M9R3KToaHJvXlI2lSOxtYHitzxite+195DSi00HL9PcS7Xik5+3xlRjfkKb3FA86SZxYPj+WJwlcfgaZoxg==} + engines: {node: ^22.13.0 || ^24.3.0 || >= 26.0.0} + + metro-runtime@0.87.0: + resolution: {integrity: sha512-XsXZkgEwI0ZMYSBfvOMAbenzwa60XlObXJ27g6/Khgrz9ESbiBbAsd7hR62G2jRBYOhGAzG61Gk22dpRJj/mdw==} + engines: {node: ^22.13.0 || ^24.3.0 || >= 26.0.0} + + metro-source-map@0.87.0: + resolution: {integrity: sha512-31BrYqu1c2co93rF1LN9Pw+7g+BrfDyxJkNQWrYm+pfA/+eVYVumF7tFMHbXLePLmHfhvSgVvbK6su1Oyiw1ng==} + engines: {node: ^22.13.0 || ^24.3.0 || >= 26.0.0} + + metro-symbolicate@0.87.0: + resolution: {integrity: sha512-uOpTxAXu74N+RSujUZ78L6gjI6bDdnz6XuW+AIUNuubZDEQPIpaX0StzIb0GMeQWP6zfHOHwFrWPP5Iquu1GXw==} + engines: {node: ^22.13.0 || ^24.3.0 || >= 26.0.0} + hasBin: true + + metro-transform-plugins@0.87.0: + resolution: {integrity: sha512-i8keUe9+BaSwMuQM26DGheElCpTtflAKIrSwJAm8ZsgDb50RAUQus+e6zt2suaJXJ1OxZa7vqgtqoZxdniM6Fw==} + engines: {node: ^22.13.0 || ^24.3.0 || >= 26.0.0} + + metro-transform-worker@0.87.0: + resolution: {integrity: sha512-YftLzNJxCTYxEN5k4AzR8KYwiENTEuz30L+4QeoMrtDd+U8mDThZg/ArR3JVRd8LaikwPOjVAS5SP3xPJN0AaA==} + engines: {node: ^22.13.0 || ^24.3.0 || >= 26.0.0} + + metro@0.87.0: + resolution: {integrity: sha512-fRqFhSzQhLNQSCvJFeuRzBRXAOOKXf1O8d2cvmMtG6yFR0jCllQ7vBsXoLP18yuqtf+N1XwWXTPF11eWy9q6dQ==} + engines: {node: ^22.13.0 || ^24.3.0 || >= 26.0.0} + hasBin: true + micromark-core-commonmark@2.0.3: resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} @@ -8052,6 +8892,11 @@ packages: resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} engines: {node: '>=18'} + mime@1.6.0: + resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} + engines: {node: '>=4'} + hasBin: true + mime@2.6.0: resolution: {integrity: sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==} engines: {node: '>=4.0.0'} @@ -8112,6 +8957,9 @@ packages: monaco-editor@0.56.0: resolution: {integrity: sha512-sXboRm3BeBeLm938eaiyLMe0OxzfXIlZvbv4ir/jVgQy1zDhWjgmny0WoN45fuDKhCCQsYMbBJrv/A6jd8aCUg==} + mortice@3.3.1: + resolution: {integrity: sha512-t3oESfijIPGsmsdLEKjF+grHfrbnKSXflJtgb1wY14cjxZpS6GnhHRXTxxzCAoCCnq1YYfpEPwY3gjiCPhOufQ==} + motion-dom@13.1.1: resolution: {integrity: sha512-XSf8VYWSB6G/0IY3rWVbyLcxWXtAVHkN1PQE2agTaCv3u8RGvbwu56TyyR/MNzBqqNavEBTZzErcxI1TxBrjcA==} @@ -8129,13 +8977,26 @@ packages: react-dom: optional: true + ms@2.0.0: + resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} + + ms@2.1.2: + resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + ms@4.0.0-nightly.202508271359: + resolution: {integrity: sha512-WC/Eo7NzFrOV/RRrTaI0fxKVbNCzEy76j2VqNV8SxDf9D69gSE2Lh0QwYvDlhiYmheBYExAvEAxVf5NoN0cj2A==} + engines: {node: '>=20'} + multer@2.2.0: resolution: {integrity: sha512-6rdyFg2kLrMh9Jee7/BMPuV9lEAd7lLW2YUpF9/YxR7njyoUwwQ0ZPh3TaIY50Sw6vlyD2HW3wGOkTS4P79xrQ==} engines: {node: '>= 10.16.0'} + multiformats@13.4.2: + resolution: {integrity: sha512-eh6eHCrRi1+POZ3dA+Dq1C6jhP1GNtr9CRINMb67OKzqW9I5DUuZM/3jLPlzhgpGeiNUlEGEbkCYChXMCc/8DQ==} + multiformats@14.0.5: resolution: {integrity: sha512-vbIm83F2yZ1pWJGS0yl0ysracIvv56LtbrIyiIQHoLdYDJOMoLfVFsXhh9DUH4SFdkdkFhucyWniihsNzVEjkQ==} @@ -8151,6 +9012,11 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + nanoid@6.0.1: + resolution: {integrity: sha512-3wVS3i51pE2pi1k5FFL/95BGfVS0kSsvDVuGXHOtxox/TywUmtgq+3qiTOTbs9J7KfHaXPiN171k/A6dBnaXFw==} + engines: {node: ^22 || ^24 || >=26} + hasBin: true + napi-postinstall@0.3.4: resolution: {integrity: sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} @@ -8166,6 +9032,10 @@ packages: neo-async@2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + netmask@2.1.1: + resolution: {integrity: sha512-eonl3sLUha+S1GzTPxychyhnUzKyeQkZ7jLjKrBagJgPla13F+uQ71HgpFefyHgqrjEbCPkDArxYsjY8/+gLKA==} + engines: {node: '>= 0.4.0'} + next-themes@0.4.6: resolution: {integrity: sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==} peerDependencies: @@ -8199,6 +9069,10 @@ packages: node-addon-api@7.1.1: resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} + node-datachannel@0.33.3: + resolution: {integrity: sha512-Tf7bbOjUXh7gPiWKTFpMDZXpippya6pdsEz3tgxsIJiW+BMLWSaX7O6F9kDjHu0GCan4vSmXlfA8l21O4D+K9Q==} + engines: {node: '>=18.20.0'} + node-emoji@2.2.0: resolution: {integrity: sha512-Z3lTE9pLaJF47NyMhd4ww1yFTAP8YhYI8SleJiHzM46Fgpm5cnNzSl9XfzFNqbaz+VlJrIj3fXQ4DeN1Rjm6cw==} engines: {node: '>=18'} @@ -8241,6 +9115,13 @@ packages: resolution: {integrity: sha512-K1HmQeZT2HrjpsR6KgqbN2FAXL2NrJJmNUSD9ck7HGTVu1JKXox8n9SB+tjbU8m8JGLF4OscrroPepew/L7/Xw==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + nullthrows@1.1.1: + resolution: {integrity: sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw==} + + ob1@0.87.0: + resolution: {integrity: sha512-8Q8sKCiUwsxgSmjDtVWyRgmxsgeJXXam3oQH6Id8ADfNaJMV6GZKyeAl8+pGVVdgwAZabfk5+aExle7AP/nZiA==} + engines: {node: ^22.13.0 || ^24.3.0 || >= 26.0.0} + object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -8277,6 +9158,10 @@ packages: resolution: {integrity: sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==} engines: {node: '>=12.20.0'} + on-finished@2.3.0: + resolution: {integrity: sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==} + engines: {node: '>= 0.8'} + on-finished@2.4.1: resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} engines: {node: '>= 0.8'} @@ -8298,6 +9183,10 @@ packages: oniguruma-to-es@4.3.6: resolution: {integrity: sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA==} + open@7.4.2: + resolution: {integrity: sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==} + engines: {node: '>=8'} + optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -8310,6 +9199,14 @@ packages: resolution: {integrity: sha512-19YVAg7T+WTrxggPukVq7DjTv6+PJ867TmhCvBsYwmbFCsZd344rq2Ld1p0wo8f8Qrrhgp82c6FJRqdXWtSEhg==} engines: {node: '>= 0.4'} + p-defer@4.0.1: + resolution: {integrity: sha512-Mr5KC5efvAK5VUptYEIopP1bakB85k2IWXaRC0rsh1uwn1L6M0LVml8OIQ4Gudg4oyZakf7FmeRLkMMtZW1i5A==} + engines: {node: '>=12'} + + p-event@7.1.0: + resolution: {integrity: sha512-/lkPs5W1aC3cp6vqZefpdosOn65J571sWodyfOQiF0+tmDCpU+H8Atwpu0vQROCVUlZuToDN5eyTLsMLLc54mg==} + engines: {node: '>=20'} + p-limit@2.3.0: resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} engines: {node: '>=6'} @@ -8326,10 +9223,26 @@ packages: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} + p-queue@9.3.3: + resolution: {integrity: sha512-NXAOdnEe5FsZJfT4oK84lE1Y5cFFdWlRuOo5tww8DyNMxyRXwn39fIkUtNLKppcPC+UYU/bXujNCUGDv01y7CA==} + engines: {node: '>=20'} + + p-retry@8.0.1: + resolution: {integrity: sha512-NAigyu8hfvJe7MsS18mnc4is0MZtau3QMdBklaJKK23oBQb6occO3UPM3vHmTckW8KhfyjMZaUOqdTFEYliHgg==} + engines: {node: '>=22'} + + p-timeout@7.0.1: + resolution: {integrity: sha512-AxTM2wDGORHGEkPCt8yqxOTMgpfbEHqF51f/5fJCmwFC3C/zNcGT63SymH2ttOAaiIws2zVg4+izQCjrakcwHg==} + engines: {node: '>=20'} + p-try@2.2.0: resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} engines: {node: '>=6'} + p-wait-for@6.0.0: + resolution: {integrity: sha512-2kKzMtjS8TVcpCOU/gr3vZ4K/WIyS1AsEFXFWapM/0lERCdyTbB6ZeuCIp+cL1aeLZfQoMdZFCBTHiK4I9UtOw==} + engines: {node: '>=20'} + package-json-from-dist@1.0.1: resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} @@ -8526,6 +9439,10 @@ packages: engines: {node: '>=14'} hasBin: true + pretty-format@29.7.0: + resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + pretty-format@30.0.5: resolution: {integrity: sha512-D1tKtYvByrBkFLe2wHJl2bwMJIiT8rW+XA+TiataH79/FszLQMrpGEvzUVkzPau7OCO0Qnrhpe87PqtOAIB8Yw==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} @@ -8537,12 +9454,24 @@ packages: process-nextick-args@2.0.1: resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + progress-events@1.1.0: + resolution: {integrity: sha512-82DVc5tI36neVB3IjdXR11ztwGuoBc98em9ijzubeZKxI47OlV2Znq6mlPqE5xPDzO2Uw98GHiQSjj2favBCRQ==} + + promise@8.3.0: + resolution: {integrity: sha512-rZPNPKTOYVNEEKFaq1HqTgOwZD+4/YHS5ukLzQCypkj+OkYx7iv0mA91lJlpPPZ8vMau3IIGj5Qlwrx+8iiSmg==} + prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} property-information@7.2.0: resolution: {integrity: sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==} + protons-runtime@5.6.0: + resolution: {integrity: sha512-/Kde+sB9DsMFrddJT/UZWe6XqvL7SL5dbag/DBCElFKhkwDj7XKt53S+mzLyaDP5OqS0wXjV5SA572uWDaT0Hg==} + + protons-runtime@7.0.0: + resolution: {integrity: sha512-r/e72006xoVND4Uvf1aIs4OQOkJaASBU3ip4DzOWAktoKAkTiOYqKoS3TYMBm8HnnYU8+h0uEzr5gIRwk/pmTg==} + proxy-addr@2.0.7: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} @@ -8562,14 +9491,20 @@ packages: pure-rand@7.0.1: resolution: {integrity: sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==} + pvtsutils@1.3.6: + resolution: {integrity: sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==} + + pvutils@1.2.0: + resolution: {integrity: sha512-BbubeCEyTuQjVMakvJQ/Sxbc93F2pwmbsxONT/ZRrwU7Ua38d8unYTwXpTVLAKJ4BDuH9IGztCjQcd/N/39Dvg==} + engines: {node: '>=16.0.0'} + qified@0.10.1: resolution: {integrity: sha512-+Owyggi9IxT1ePKGafcI87ubSmxol6smwJ+RAHDQlx9+9cPwFWDiKFFCPuWhr9ignlGpZ9vDQLw67N4dcTVFEA==} engines: {node: '>=20'} - qrcode.react@4.2.0: - resolution: {integrity: sha512-QpgqWi8rD9DsS9EP3z7BT+5lY5SFhsqGjpgW5DY/i3mK4M9DTBNz3ErMi8BWYEfI3L0d8GIbGmcdFAS1uIRGjA==} - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + qr@0.6.0: + resolution: {integrity: sha512-P23VoX7SipHALdiIYG+D+LT/6n22dNKwV92FAb3d+Nlki/5WisSsfLt0UDFz2XEBtuwrECTznvu+chKKFCSYhA==} + engines: {node: '>= 20.19.0'} qs@6.15.3: resolution: {integrity: sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==} @@ -8585,9 +9520,26 @@ packages: queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + queue@6.0.2: + resolution: {integrity: sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==} + + race-event@1.6.1: + resolution: {integrity: sha512-vi7WH5g5KoTFpu2mme/HqZiWH14XSOtg5rfp6raBskBHl7wnmy3F/biAIyY5MsK+BHWhoPhxtZ1Y2R7OHHaWyQ==} + + race-signal@2.0.2: + resolution: {integrity: sha512-IuNNNY5NLriMFEGl3BVei2xq9NIcpkuAklhPWamhu5tRJrJz0w9sloUMz73YXpmUluE2EZocmJm9rpQqXmCPiA==} + rambda@7.5.0: resolution: {integrity: sha512-y/M9weqWAH4iopRd7EHDEQQvpFPHj1AA3oHozE9tfITHUtTR7Z9PSlIRRG2l1GuW7sefC1cXFfIcF+cgnShdBA==} + random-int@3.1.0: + resolution: {integrity: sha512-h8CRz8cpvzj0hC/iH/1Gapgcl2TQ6xtnCpyOI5WvWfXf/yrDx2DOU+tD9rX23j36IF11xg1KqB9W11Z18JPMdw==} + engines: {node: '>=12'} + + range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + range-parser@1.3.0: resolution: {integrity: sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==} engines: {node: '>= 0.6'} @@ -8608,6 +9560,9 @@ packages: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-devtools-core@6.1.5: + resolution: {integrity: sha512-ePrwPfxAnB+7hgnEr8vpKxL9cmnp7F322t8oqcPshbIQQhDKgFDW4tjhF2wjVbdXF9O/nyuy3sQWd9JGpiLPvA==} + react-dom@19.2.8: resolution: {integrity: sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==} peerDependencies: @@ -8622,6 +9577,26 @@ packages: react-is@19.2.8: resolution: {integrity: sha512-s5un28nYxKJw5gvUHyW5PCC28CvBqLu9r3cWgzHT4Vo/5fqqkFcdRYsGcKf50WMPpjjFZS5d76fn3YCo2njKwQ==} + react-native-webrtc@124.0.8: + resolution: {integrity: sha512-uuQxvmk+mvnk5U0tr+1N42sKZqgm41fJrBA+fmCvML9J9P4roSh2So82t5RHAlu/vE9vxu5AKgivAiH61clCBg==} + peerDependencies: + react-native: '>=0.60.0' + + react-native@0.87.1: + resolution: {integrity: sha512-DJKG6ANoD7BtrE4z9DewiSD7/RxCX73lK5Pu49aUr85P3333Dm2roTiP0rRjQNDZdVizHSOmstWfwF/o9EjCRA==} + engines: {node: ^22.13.0 || ^24.3.0 || >= 26.0.0} + hasBin: true + peerDependencies: + '@types/react': ^19.1.1 + react: ^19.2.3 + peerDependenciesMeta: + '@types/react': + optional: true + + react-refresh@0.14.2: + resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==} + engines: {node: '>=0.10.0'} + react-remove-scroll-bar@2.3.8: resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==} engines: {node: '>=10'} @@ -8712,6 +9687,9 @@ packages: resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} engines: {node: '>= 0.4'} + regenerator-runtime@0.13.11: + resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==} + regex-recursion@6.0.2: resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==} @@ -8798,6 +9776,9 @@ packages: resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} engines: {node: '>=18'} + retimeable-signal@1.0.1: + resolution: {integrity: sha512-Cy26CYfbWnYu8HMoJeDhaMpW/EYFIbne3vMf6G9RSrOyWYXbPehja/BEdzpqmM84uy2bfBD7NPZhoQ4GZEtgvg==} + reusify@1.1.0: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} @@ -8810,6 +9791,25 @@ packages: robust-predicates@3.0.3: resolution: {integrity: sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==} + rolldown-plugin-dts@0.27.14: + resolution: {integrity: sha512-ZvuDDwoIpRK9RPxDXratCpklFO9QZZWndf/sd0VBFb4LEj0jj07UcHK9OCh7V4XiFz2Z89ziyBC2K6tJiDjrbw==} + engines: {node: ^22.18.0 || >=24.11.0} + peerDependencies: + '@typescript/native-preview': '*' + '@volar/typescript': ~2.4.0 + rolldown: ^1.0.0 + typescript: ^5.0.0 || ^6.0.0 || ~7.0.0 + vue-tsc: ~3.2.0 || ~3.3.0 + peerDependenciesMeta: + '@typescript/native-preview': + optional: true + '@volar/typescript': + optional: true + typescript: + optional: true + vue-tsc: + optional: true + rolldown-plugin-dts@0.28.5: resolution: {integrity: sha512-yYd3C9CeJwqjOc9X23m0Tyxcqic491uLZlfg51szT287S8zCCqLR2uoySoElgqy2CLn7PdXcEo1dlkBs4n1WHg==} engines: {node: ^22.18.0 || ^24.11.0 || >=26.0.0} @@ -8881,6 +9881,9 @@ packages: safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + sanitize-filename@1.6.4: + resolution: {integrity: sha512-9ZyI08PsvdQl2r/bBIGubpVdR3RR9sY6RDiWFPreA21C/EFlQhmgo20UZlNjZMMZNubusLhAQozkA0Od5J21Eg==} + satori@0.10.14: resolution: {integrity: sha512-abovcqmwl97WKioxpkfuMeZmndB1TuDFY/R+FymrZyiGP+pMYomvgSzVPnbNMWHHESOPosVHGL352oFbdAnJcA==} engines: {node: '>=16'} @@ -8912,10 +9915,22 @@ packages: engines: {node: '>=10'} hasBin: true + send@0.19.2: + resolution: {integrity: sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==} + engines: {node: '>= 0.8.0'} + send@1.2.1: resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} engines: {node: '>= 18'} + serialize-error@2.1.0: + resolution: {integrity: sha512-ghgmKt5o4Tly5yEG/UJp8qTd0AN7Xalw4XBtDEKP655B699qMEtra1WlXeE6WIvdEG481JvRxULKsInq/iNysw==} + engines: {node: '>=0.10.0'} + + serve-static@1.16.3: + resolution: {integrity: sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==} + engines: {node: '>= 0.8.0'} + serve-static@2.2.1: resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} engines: {node: '>= 18'} @@ -9004,6 +10019,10 @@ packages: source-map-support@0.5.21: resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + source-map@0.5.7: + resolution: {integrity: sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==} + engines: {node: '>=0.10.0'} + source-map@0.6.1: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} @@ -9040,9 +10059,20 @@ packages: stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + stackframe@1.3.4: + resolution: {integrity: sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==} + + stacktrace-parser@0.1.11: + resolution: {integrity: sha512-WjlahMgHmCJpqzU8bIBy4qtsZdU9lRlcZE3Lvyej6t4tuOuv1vk57OW3MBrj6hXBFx/nNoC9MPMTcr5YA7NQbg==} + engines: {node: '>=6'} + state-local@1.0.7: resolution: {integrity: sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==} + statuses@1.5.0: + resolution: {integrity: sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==} + engines: {node: '>= 0.6'} + statuses@2.0.2: resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} engines: {node: '>= 0.8'} @@ -9181,6 +10211,10 @@ packages: resolution: {integrity: sha512-oK8WG9diS3DlhdUkcFn4tkNIiIbBx9lI2ClF8K+b2/m8Eyv47LSawxUzZQSNKUrVb2KsqeTDCcjAAVPYaSLVTA==} engines: {node: '>=14.18.0'} + supports-color@10.2.2: + resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==} + engines: {node: '>=18'} + supports-color@5.5.0: resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} engines: {node: '>=4'} @@ -9275,6 +10309,9 @@ packages: third-party-capital@1.0.20: resolution: {integrity: sha512-oB7yIimd8SuGptespDAZnNkzIz+NWaJCu2RMsbs4Wmp9zSDUM8Nhi3s2OOcqYuv3mN4hitXc8DVx+LyUmbUDiA==} + throat@5.0.0: + resolution: {integrity: sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==} + through2@2.0.5: resolution: {integrity: sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==} @@ -9300,6 +10337,9 @@ packages: resolution: {integrity: sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==} engines: {node: '>=14.0.0'} + tmpl@1.0.5: + resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} + to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -9325,6 +10365,9 @@ packages: trough@2.2.0: resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} + truncate-utf8-bytes@1.0.2: + resolution: {integrity: sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==} + ts-api-utils@1.4.3: resolution: {integrity: sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==} engines: {node: '>=16'} @@ -9404,6 +10447,40 @@ packages: resolution: {integrity: sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==} engines: {node: '>=6'} + tsdown@0.22.14: + resolution: {integrity: sha512-ule7Y+fsAN2iZbLDoo7C4KYljFJNJJ+fLshyn+9gozeTspVersWHxwdGB+Dm2hzA38s6muFnUTl0jK3vJm9ifQ==} + engines: {node: ^22.18.0 || >=24.11.0} + hasBin: true + peerDependencies: + '@arethetypeswrong/core': ^0.18.1 + '@tsdown/css': 0.22.14 + '@tsdown/exe': 0.22.14 + '@vitejs/devtools': '*' + publint: ^0.3.8 + tsx: '*' + typescript: ^5.0.0 || ^6.0.0 || ^7.0.0 + unplugin-unused: ^0.5.0 + unrun: '*' + peerDependenciesMeta: + '@arethetypeswrong/core': + optional: true + '@tsdown/css': + optional: true + '@tsdown/exe': + optional: true + '@vitejs/devtools': + optional: true + publint: + optional: true + tsx: + optional: true + typescript: + optional: true + unplugin-unused: + optional: true + unrun: + optional: true + tsdown@0.23.0: resolution: {integrity: sha512-BaT+ep1xnj5hdyICLd5r5SYYE+TXnI1ATePLykv9vcKpHpFTWUWRH+x1AF/E2qcu3YB6+vI8IdyxIIIffEqw5Q==} engines: {node: ^22.18.0 || ^24.11.0 || >=26.0.0} @@ -9438,6 +10515,9 @@ packages: unrun: optional: true + tslib@1.14.1: + resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} + tslib@2.3.1: resolution: {integrity: sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==} @@ -9452,6 +10532,10 @@ packages: engines: {node: '>=18.0.0'} hasBin: true + tsyringe@4.10.0: + resolution: {integrity: sha512-axr3IdNuVIxnaK5XGEUFTu3YmAQ6lllgrvqfEoR16g/HGnYY/6We4oWENtAnzK6/LpJ2ur9PAb80RBt7/U4ugw==} + engines: {node: '>= 6.0.0'} + type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} @@ -9472,6 +10556,10 @@ packages: resolution: {integrity: sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==} engines: {node: '>=8'} + type-fest@0.7.1: + resolution: {integrity: sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg==} + engines: {node: '>=8'} + type-fest@0.8.1: resolution: {integrity: sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==} engines: {node: '>=8'} @@ -9575,6 +10663,12 @@ packages: resolution: {integrity: sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g==} engines: {node: '>=8'} + uint8-varint@2.0.5: + resolution: {integrity: sha512-jeFLbL/x30wBRnWjKE1qVBXeumG46r7XmYkpis955lTQ+blccGKFrOsSMHlxePwYB1pI7L8YPHz1t4jLxEs3nA==} + + uint8-varint@3.0.0: + resolution: {integrity: sha512-S4DdpXBaLwKcFo7f0bWzWfHjbZ/i3QhM842qn+ZvHjxqFCfUcEB9SQNcmI69S+zMlcmIcKxsk9Iyw77S2Kxv6Q==} + uint8array-extras@1.5.0: resolution: {integrity: sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==} engines: {node: '>=18'} @@ -9587,6 +10681,18 @@ packages: resolution: {integrity: sha512-9vqDWmoSXOoi+K14zNaf6LBV51Q8MayF0/IiQs3GlygIKUYtog603e6virExkjjFosfJUBI4LhbQK1iq8IG11A==} engines: {node: '>=14.0.0'} + uint8arraylist@2.4.9: + resolution: {integrity: sha512-KxWjyEFzchzik3aoQlK66oaoxIReoMo5bQRm1fcjBUZvE8xv/tyR3CTKhjh6K/faV8VaF6hd5pjr45CzbwuwkA==} + + uint8arraylist@3.0.2: + resolution: {integrity: sha512-LDVoq9BQaGJzGDUovEnoX6rpKCvnY/Jbtws4ikwnBzjRbq5qBAFpBZevUEbSmMM87aO0Sp+wOZy2ZXf5yODmXQ==} + + uint8arrays@5.1.1: + resolution: {integrity: sha512-9muQwa4wZG4dKi9gMAIBtnk2Pw87SRpvWTH6lOGm19V2Uqxr4uomUf2PGqPnWc+qs06sN8owUU4jfcoWOcfwVQ==} + + uint8arrays@6.1.1: + resolution: {integrity: sha512-iz7JN0XCSZYA111lhFG2Ui9EhFvTNekqSRHw3lvMHq+dzwWy1OQftxFQREEh4rffU0oSoXdQHsk2TiHKVm4fsA==} + unbox-primitive@1.1.0: resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} engines: {node: '>= 0.4'} @@ -9641,6 +10747,10 @@ packages: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} engines: {node: '>= 10.0.0'} + unlimited-timeout@0.1.0: + resolution: {integrity: sha512-D4g+mxFeQGQHzCfnvij+R35ukJ0658Zzudw7j16p4tBBbNasKkKM4SocYxqhwT5xA7a9JYWDzKkEFyMlRi5sng==} + engines: {node: '>=20'} + unpipe@1.0.0: resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} engines: {node: '>= 0.8'} @@ -9696,9 +10806,19 @@ packages: peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + utf8-byte-length@1.0.5: + resolution: {integrity: sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==} + + utf8-codec@1.0.0: + resolution: {integrity: sha512-S/QSLezp3qvG4ld5PUfXiH7mCFxLKjSVZRFkB3DOjgwHuJPFDkInAXc/anf7BAbHt/D38ozDzL+QMZ6/7gsI6w==} + util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + utils-merge@1.0.1: + resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} + engines: {node: '>= 0.4.0'} + uuid@14.0.2: resolution: {integrity: sha512-xZe/16rV4aa+HGSOCiY2YeLT1OybRLrrkL/Rqaq7p7GMVXjFh+6wN4oMYgjFmnSnhY8t6Xpdl2l9qmnHYuMHwQ==} hasBin: true @@ -9732,6 +10852,10 @@ packages: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} + verkit@0.3.2: + resolution: {integrity: sha512-zj/ob3UsvJGN0whEAKFp53REA5X66hvffVqoCtVQAakJKnKlH+/PcOfMoFwIG/o4rElqLv/ycAFlx8ZlXUorCg==} + engines: {node: '>=18.12.0'} + verkit@0.4.0: resolution: {integrity: sha512-sXMwN6DMHeouPfCxkxWkKAmxphWKEenHYY5H1nIBzU3PmDsmJp6kBXJdshjVdpMZuWCmL9SH7KFRx29AylpP6g==} engines: {node: '>=18.12.0'} @@ -9826,13 +10950,25 @@ packages: jsdom: optional: true + vlq@1.0.1: + resolution: {integrity: sha512-gQpnTgkubC6hQgdIcRdYGDSDc+SaujOdyesZQMv6JlfQee/9Mp0Qhnys6WxDWvQnL5WZdT7o2Ul187aSt0Rq+w==} + + walker@1.0.8: + resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} + watchpack@2.5.2: resolution: {integrity: sha512-6i/00NBjP4yGPs+caKSyRfpTF/8Torsu0MOW3mMzIbhgISFder8i7xbqgHlLMwJrdiN8ndBV3UA1/AfzPSr+jg==} engines: {node: '>=10.13.0'} + weald@1.1.3: + resolution: {integrity: sha512-vMWtNbYuPb58NeG2+0sKA0Een4VMDwzf+3oHqh68buWRSOMUBlUeRb11LhV28czV+DUpJHRykifijZDuS9bInA==} + web-namespaces@2.0.1: resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==} + webcrypto-core@1.9.2: + resolution: {integrity: sha512-gsXecm82UQNlTBURJGuqOWy1Ww08S3kZUcr3aOJS02Pk0xLtkfeUAVC0u0xhgdonFme80edSJUIJyuvL/7250Q==} + webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} @@ -9854,9 +10990,16 @@ packages: webpack-cli: optional: true + whatwg-fetch@3.6.20: + resolution: {integrity: sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==} + whatwg-url@5.0.0: resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + wherearewe@2.0.1: + resolution: {integrity: sha512-XUguZbDxCA2wBn2LoFtcEhXL6AXo+hVjGonwhSTTTU9SzbWG8Xu3onNIpzf9j/mYUcJQ0f+m37SzG77G851uFw==} + engines: {node: '>=16.0.0', npm: '>=7.0.0'} + which-boxed-primitive@1.1.1: resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} engines: {node: '>= 0.4'} @@ -9905,6 +11048,18 @@ packages: resolution: {integrity: sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + ws@7.5.13: + resolution: {integrity: sha512-rsKI6xDBFVf4r/x8XyChGK04QR/XHroxs/jUcoWvtEZM8TPU/X/uIY9B1CsSzYws9ZJb/6bbBu7dPhFW00CAoA==} + engines: {node: '>=8.3.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ^5.0.2 + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + ws@8.21.0: resolution: {integrity: sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==} engines: {node: '>=10.0.0'} @@ -9985,15 +11140,24 @@ packages: yuku-analyzer@0.9.3: resolution: {integrity: sha512-2xSREErroEF8boH7XyKfVO5hbjP6PCIYYnLR2Abg6PGJaFVbETLucmbzIebBOHeyY4GO6VXzloTuhUldR/zyew==} + yuku-ast@0.8.7: + resolution: {integrity: sha512-h6+4bDfyootiMB9vckk5uKo5r5j0GHrkr17FQTDNfEsFT3DWlN9uu1HJwQwc64pgmLCI945fWM3lbTIqxjT3GQ==} + yuku-ast@0.9.3: resolution: {integrity: sha512-Kt0PXlPCXdKF1fWFTl7N3DaxsVk6DHF8VAXHJMkwPtJnWYgbx20pYgGSweuC/86mIM7k1NUITQ4JffgOLUWTCw==} yuku-ast@0.9.5: resolution: {integrity: sha512-Q8qW8WwQnN5Cm0ZZivdRIfv0sRLTjUq0YumXJkw8CYN1aCdICH9rk4C47/4n6kA5EqDtlj+F608twoTSV2MwdQ==} + yuku-codegen@0.8.7: + resolution: {integrity: sha512-adwDZSh8oVDzhE6Du9PwVWxcOxeV0e2EVhUuMKWfhSY4wkrDq9eqixlxFF3l/XGUV1E7UFzhpz9393MUumkyNw==} + yuku-codegen@0.9.5: resolution: {integrity: sha512-zGUVyDpSK4b6c9B0yyxi2P0wujn1XZTbG9dCb7d6gyAoP63EMgyLzUwPUvwxPG5jgYqhlI7w+S3oCyapqGr4PA==} + yuku-parser@0.8.7: + resolution: {integrity: sha512-vRD9nwt4L3aYpxNqeSC4WqLv58xrXef0Ong1Mc45CTXTIpvLafx7JO05sczmQZwdLEZvywrLOGdNC5+Rp5N1BQ==} + yuku-parser@0.9.5: resolution: {integrity: sha512-IBnAdNVMswWbJcM7woPluOudectJzFjJ1N8jVRxP9Uq4CIv5hZdBIcdmtRbFDYF/Ph2j2gVup4YJ4N9mh0jYFA==} @@ -10376,6 +11540,39 @@ snapshots: hashery: 1.5.1 keyv: 5.6.0 + '@chainsafe/as-chacha20poly1305@0.1.0': {} + + '@chainsafe/as-sha256@1.2.5': {} + + '@chainsafe/is-ip@2.1.0': {} + + '@chainsafe/libp2p-noise@17.0.0': + dependencies: + '@chainsafe/as-chacha20poly1305': 0.1.0 + '@chainsafe/as-sha256': 1.2.5 + '@libp2p/crypto': 5.1.23 + '@libp2p/interface': 3.3.0 + '@libp2p/peer-id': 6.0.15 + '@libp2p/utils': 7.4.1 + '@noble/ciphers': 2.4.0 + '@noble/curves': 2.4.0 + '@noble/hashes': 2.4.0 + protons-runtime: 5.6.0 + uint8arraylist: 2.4.9 + uint8arrays: 5.1.1 + wherearewe: 2.0.1 + + '@chainsafe/libp2p-yamux@8.0.1': + dependencies: + '@libp2p/interface': 3.3.0 + '@libp2p/utils': 7.4.1 + race-signal: 2.0.2 + uint8arraylist: 2.4.9 + + '@chainsafe/netmask@2.0.0': + dependencies: + '@chainsafe/is-ip': 2.1.0 + '@changesets/apply-release-plan@8.1.0': dependencies: '@changesets/config': 4.0.0 @@ -10599,6 +11796,11 @@ snapshots: dependencies: '@jridgewell/trace-mapping': 0.3.9 + '@dnsquery/dns-packet@6.1.1': + dependencies: + '@leichtgewicht/ip-codec': 2.0.5 + utf8-codec: 1.0.0 + '@emnapi/core@1.10.0': dependencies: '@emnapi/wasi-threads': 1.2.1 @@ -11255,6 +12457,8 @@ snapshots: wrap-ansi: 8.1.0 wrap-ansi-cjs: wrap-ansi@7.0.0 + '@isaacs/ttlcache@1.4.1': {} + '@istanbuljs/load-nyc-config@1.1.0': dependencies: camelcase: 5.3.1 @@ -11396,6 +12600,10 @@ snapshots: transitivePeerDependencies: - supports-color + '@jest/schemas@29.6.3': + dependencies: + '@sinclair/typebox': 0.27.12 + '@jest/schemas@30.0.5': dependencies: '@sinclair/typebox': 0.34.40 @@ -11451,6 +12659,15 @@ snapshots: transitivePeerDependencies: - supports-color + '@jest/types@29.6.3': + dependencies: + '@jest/schemas': 29.6.3 + '@types/istanbul-lib-coverage': 2.0.6 + '@types/istanbul-reports': 3.0.4 + '@types/node': 26.5.0 + '@types/yargs': 17.0.35 + chalk: 4.1.2 + '@jest/types@30.0.5': dependencies: '@jest/pattern': 30.0.1 @@ -11529,6 +12746,242 @@ snapshots: '@keyv/serialize@1.1.1': {} + '@leichtgewicht/ip-codec@2.0.5': {} + + '@libp2p/circuit-relay-v2@4.2.13': + dependencies: + '@libp2p/crypto': 5.1.23 + '@libp2p/interface': 3.3.0 + '@libp2p/interface-internal': 3.1.13 + '@libp2p/peer-collections': 7.0.28 + '@libp2p/peer-id': 6.0.15 + '@libp2p/peer-record': 9.0.16 + '@libp2p/utils': 7.4.1 + '@multiformats/multiaddr': 13.0.3 + '@multiformats/multiaddr-matcher': 3.0.2 + any-signal: 4.2.0 + main-event: 1.0.5 + multiformats: 14.0.5 + nanoid: 6.0.1 + progress-events: 1.1.0 + protons-runtime: 7.0.0 + retimeable-signal: 1.0.1 + uint8arraylist: 3.0.2 + uint8arrays: 6.1.1 + + '@libp2p/crypto@5.1.23': + dependencies: + '@libp2p/interface': 3.3.0 + '@noble/curves': 2.4.0 + '@noble/hashes': 2.4.0 + multiformats: 14.0.5 + protons-runtime: 7.0.0 + uint8arraylist: 3.0.2 + uint8arrays: 6.1.1 + + '@libp2p/identify@4.1.14': + dependencies: + '@libp2p/crypto': 5.1.23 + '@libp2p/interface': 3.3.0 + '@libp2p/interface-internal': 3.1.13 + '@libp2p/peer-id': 6.0.15 + '@libp2p/peer-record': 9.0.16 + '@libp2p/utils': 7.4.1 + '@multiformats/multiaddr': 13.0.3 + '@multiformats/multiaddr-matcher': 3.0.2 + it-drain: 3.0.12 + it-parallel: 3.0.16 + main-event: 1.0.5 + protons-runtime: 7.0.0 + uint8arraylist: 3.0.2 + uint8arrays: 6.1.1 + + '@libp2p/interface-internal@3.1.13': + dependencies: + '@libp2p/interface': 3.3.0 + '@libp2p/peer-collections': 7.0.28 + '@multiformats/multiaddr': 13.0.3 + progress-events: 1.1.0 + + '@libp2p/interface@3.3.0': + dependencies: + '@multiformats/dns': 1.0.15 + '@multiformats/multiaddr': 13.0.3 + main-event: 1.0.5 + multiformats: 14.0.5 + progress-events: 1.1.0 + uint8arraylist: 3.0.2 + + '@libp2p/keychain@6.1.7': + dependencies: + '@libp2p/crypto': 5.1.23 + '@libp2p/interface': 3.3.0 + '@noble/hashes': 2.4.0 + asn1js: 3.0.10 + interface-datastore: 10.0.1 + multiformats: 14.0.5 + sanitize-filename: 1.6.4 + uint8arrays: 6.1.1 + + '@libp2p/logger@6.2.13': + dependencies: + '@libp2p/interface': 3.3.0 + '@multiformats/multiaddr': 13.0.3 + interface-datastore: 10.0.1 + multiformats: 14.0.5 + weald: 1.1.3 + + '@libp2p/multistream-select@7.0.28': + dependencies: + '@libp2p/interface': 3.3.0 + '@libp2p/utils': 7.4.1 + it-length-prefixed: 11.0.1 + uint8arraylist: 3.0.2 + uint8arrays: 6.1.1 + + '@libp2p/noise@17.0.2': + dependencies: + '@chainsafe/as-chacha20poly1305': 0.1.0 + '@chainsafe/as-sha256': 1.2.5 + '@libp2p/crypto': 5.1.23 + '@libp2p/interface': 3.3.0 + '@libp2p/peer-id': 6.0.15 + '@libp2p/utils': 7.4.1 + '@noble/ciphers': 2.4.0 + '@noble/curves': 2.4.0 + '@noble/hashes': 2.4.0 + protons-runtime: 7.0.0 + uint8arraylist: 3.0.2 + uint8arrays: 6.1.1 + wherearewe: 2.0.1 + + '@libp2p/peer-collections@7.0.28': + dependencies: + '@libp2p/interface': 3.3.0 + '@libp2p/peer-id': 6.0.15 + '@libp2p/utils': 7.4.1 + multiformats: 14.0.5 + + '@libp2p/peer-id@6.0.15': + dependencies: + '@libp2p/crypto': 5.1.23 + '@libp2p/interface': 3.3.0 + multiformats: 14.0.5 + uint8arrays: 6.1.1 + + '@libp2p/peer-record@9.0.16': + dependencies: + '@libp2p/crypto': 5.1.23 + '@libp2p/interface': 3.3.0 + '@libp2p/peer-id': 6.0.15 + '@multiformats/multiaddr': 13.0.3 + multiformats: 14.0.5 + protons-runtime: 7.0.0 + uint8-varint: 3.0.0 + uint8arraylist: 3.0.2 + uint8arrays: 6.1.1 + + '@libp2p/peer-store@12.0.28': + dependencies: + '@libp2p/crypto': 5.1.23 + '@libp2p/interface': 3.3.0 + '@libp2p/peer-collections': 7.0.28 + '@libp2p/peer-id': 6.0.15 + '@libp2p/peer-record': 9.0.16 + '@multiformats/multiaddr': 13.0.3 + interface-datastore: 10.0.1 + it-all: 3.0.11 + main-event: 1.0.5 + mortice: 3.3.1 + multiformats: 14.0.5 + protons-runtime: 7.0.0 + uint8arraylist: 3.0.2 + uint8arrays: 6.1.1 + + '@libp2p/utils@7.4.1': + dependencies: + '@chainsafe/is-ip': 2.1.0 + '@chainsafe/netmask': 2.0.0 + '@libp2p/interface': 3.3.0 + '@libp2p/logger': 6.2.13 + '@multiformats/multiaddr': 13.0.3 + '@multiformats/multiaddr-matcher': 3.0.2 + '@sindresorhus/fnv1a': 3.1.0 + any-signal: 4.2.0 + cborg: 6.1.1 + delay: 7.0.0 + is-loopback-addr: 2.1.0 + it-length-prefixed: 11.0.1 + it-pipe: 3.0.1 + it-pushable: 3.2.4 + it-stream-types: 2.0.4 + main-event: 1.0.5 + netmask: 2.1.1 + p-defer: 4.0.1 + p-event: 7.1.0 + progress-events: 1.1.0 + protons-runtime: 7.0.0 + race-signal: 2.0.2 + uint8-varint: 3.0.0 + uint8arraylist: 3.0.2 + uint8arrays: 6.1.1 + + '@libp2p/webrtc@6.0.32(react-native@0.87.1(@babel/core@7.29.7)(@types/react@19.2.18)(react@19.2.8))': + dependencies: + '@chainsafe/is-ip': 2.1.0 + '@libp2p/crypto': 5.1.23 + '@libp2p/interface': 3.3.0 + '@libp2p/interface-internal': 3.1.13 + '@libp2p/keychain': 6.1.7 + '@libp2p/noise': 17.0.2 + '@libp2p/peer-id': 6.0.15 + '@libp2p/utils': 7.4.1 + '@multiformats/multiaddr': 13.0.3 + '@multiformats/multiaddr-matcher': 3.0.2 + '@peculiar/webcrypto': 1.7.1 + '@peculiar/x509': 2.1.0 + get-port: 7.2.0 + interface-datastore: 10.0.1 + it-length-prefixed: 11.0.1 + it-protobuf-stream: 3.0.1 + it-pushable: 3.2.4 + it-stream-types: 2.0.4 + main-event: 1.0.5 + multiformats: 14.0.5 + node-datachannel: 0.33.3 + p-defer: 4.0.1 + p-event: 7.1.0 + p-timeout: 7.0.1 + p-wait-for: 6.0.0 + progress-events: 1.1.0 + protons-runtime: 7.0.0 + race-signal: 2.0.2 + react-native-webrtc: 124.0.8(react-native@0.87.1(@babel/core@7.29.7)(@types/react@19.2.18)(react@19.2.8)) + reflect-metadata: 0.2.2 + uint8-varint: 3.0.0 + uint8arraylist: 3.0.2 + uint8arrays: 6.1.1 + transitivePeerDependencies: + - react-native + - supports-color + + '@libp2p/websockets@10.1.21': + dependencies: + '@libp2p/interface': 3.3.0 + '@libp2p/utils': 7.4.1 + '@multiformats/multiaddr': 13.0.3 + '@multiformats/multiaddr-matcher': 3.0.2 + '@multiformats/multiaddr-to-uri': 12.0.0 + main-event: 1.0.5 + p-event: 7.1.0 + progress-events: 1.1.0 + uint8arraylist: 3.0.2 + uint8arrays: 6.1.1 + ws: 8.21.3 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + '@lit-labs/ssr-dom-shim@1.4.0': {} '@lit/react@1.0.8(@types/react@19.2.18)': @@ -11603,6 +13056,30 @@ snapshots: react: 19.2.8 react-dom: 19.2.8(react@19.2.8) + '@multiformats/dns@1.0.15': + dependencies: + '@dnsquery/dns-packet': 6.1.1 + '@libp2p/interface': 3.3.0 + hashlru: 2.3.0 + p-queue: 9.3.3 + progress-events: 1.1.0 + uint8arrays: 6.1.1 + + '@multiformats/multiaddr-matcher@3.0.2': + dependencies: + '@multiformats/multiaddr': 13.0.3 + + '@multiformats/multiaddr-to-uri@12.0.0': + dependencies: + '@multiformats/multiaddr': 13.0.3 + + '@multiformats/multiaddr@13.0.3': + dependencies: + '@chainsafe/is-ip': 2.1.0 + multiformats: 14.0.5 + uint8-varint: 3.0.0 + uint8arrays: 6.1.1 + '@napi-rs/lzma-linux-x64-gnu@1.5.1': optional: true @@ -11803,6 +13280,33 @@ snapshots: '@noble/hashes@2.4.0': {} + '@node-datachannel/android-arm64@0.33.2': + optional: true + + '@node-datachannel/darwin-arm64@0.33.2': + optional: true + + '@node-datachannel/darwin-x64@0.33.2': + optional: true + + '@node-datachannel/linux-arm64-gnu@0.33.2': + optional: true + + '@node-datachannel/linux-arm64-musl@0.33.2': + optional: true + + '@node-datachannel/linux-x64-gnu@0.33.2': + optional: true + + '@node-datachannel/linux-x64-musl@0.33.2': + optional: true + + '@node-datachannel/win32-arm64-msvc@0.33.2': + optional: true + + '@node-datachannel/win32-x64-msvc@0.33.2': + optional: true + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -11888,6 +13392,128 @@ snapshots: '@parcel/watcher-win32-arm64': 2.6.0 '@parcel/watcher-win32-x64': 2.6.0 + '@peculiar/asn1-asym-key@2.9.4': + dependencies: + '@peculiar/asn1-pkcs8': 2.9.4 + '@peculiar/asn1-schema': 2.9.4 + '@peculiar/asn1-x509': 2.9.4 + asn1js: 3.0.10 + tslib: 2.8.1 + + '@peculiar/asn1-cms@2.9.4': + dependencies: + '@peculiar/asn1-schema': 2.9.4 + '@peculiar/asn1-x509': 2.9.4 + '@peculiar/asn1-x509-attr': 2.9.4 + asn1js: 3.0.10 + tslib: 2.8.1 + + '@peculiar/asn1-csr@2.9.4': + dependencies: + '@peculiar/asn1-schema': 2.9.4 + '@peculiar/asn1-x509': 2.9.4 + asn1js: 3.0.10 + tslib: 2.8.1 + + '@peculiar/asn1-ecc@2.9.4': + dependencies: + '@peculiar/asn1-schema': 2.9.4 + '@peculiar/asn1-x509': 2.9.4 + asn1js: 3.0.10 + tslib: 2.8.1 + + '@peculiar/asn1-pfx@2.9.4': + dependencies: + '@peculiar/asn1-cms': 2.9.4 + '@peculiar/asn1-pkcs8': 2.9.4 + '@peculiar/asn1-rsa': 2.9.4 + '@peculiar/asn1-schema': 2.9.4 + asn1js: 3.0.10 + tslib: 2.8.1 + + '@peculiar/asn1-pkcs8@2.9.4': + dependencies: + '@peculiar/asn1-schema': 2.9.4 + '@peculiar/asn1-x509': 2.9.4 + asn1js: 3.0.10 + tslib: 2.8.1 + + '@peculiar/asn1-pkcs9@2.9.4': + dependencies: + '@peculiar/asn1-cms': 2.9.4 + '@peculiar/asn1-pfx': 2.9.4 + '@peculiar/asn1-pkcs8': 2.9.4 + '@peculiar/asn1-schema': 2.9.4 + '@peculiar/asn1-x509': 2.9.4 + '@peculiar/asn1-x509-attr': 2.9.4 + asn1js: 3.0.10 + tslib: 2.8.1 + + '@peculiar/asn1-rsa@2.9.4': + dependencies: + '@peculiar/asn1-schema': 2.9.4 + '@peculiar/asn1-x509': 2.9.4 + asn1js: 3.0.10 + tslib: 2.8.1 + + '@peculiar/asn1-schema@2.9.4': + dependencies: + '@peculiar/utils': 2.0.3 + asn1js: 3.0.10 + tslib: 2.8.1 + + '@peculiar/asn1-x509-attr@2.9.4': + dependencies: + '@peculiar/asn1-schema': 2.9.4 + '@peculiar/asn1-x509': 2.9.4 + asn1js: 3.0.10 + tslib: 2.8.1 + + '@peculiar/asn1-x509-post-quantum@2.9.4': + dependencies: + '@peculiar/asn1-asym-key': 2.9.4 + '@peculiar/asn1-schema': 2.9.4 + '@peculiar/asn1-x509': 2.9.4 + asn1js: 3.0.10 + tslib: 2.8.1 + + '@peculiar/asn1-x509@2.9.4': + dependencies: + '@peculiar/asn1-schema': 2.9.4 + '@peculiar/utils': 2.0.3 + asn1js: 3.0.10 + tslib: 2.8.1 + + '@peculiar/json-schema@1.1.12': + dependencies: + tslib: 2.8.1 + + '@peculiar/utils@2.0.3': + dependencies: + tslib: 2.8.1 + + '@peculiar/webcrypto@1.7.1': + dependencies: + '@peculiar/asn1-schema': 2.9.4 + '@peculiar/json-schema': 1.1.12 + '@peculiar/utils': 2.0.3 + tslib: 2.8.1 + webcrypto-core: 1.9.2 + + '@peculiar/x509@2.1.0': + dependencies: + '@peculiar/asn1-cms': 2.9.4 + '@peculiar/asn1-csr': 2.9.4 + '@peculiar/asn1-ecc': 2.9.4 + '@peculiar/asn1-pkcs9': 2.9.4 + '@peculiar/asn1-rsa': 2.9.4 + '@peculiar/asn1-schema': 2.9.4 + '@peculiar/asn1-x509': 2.9.4 + '@peculiar/asn1-x509-post-quantum': 2.9.4 + pvtsutils: 1.3.6 + tslib: 2.8.1 + tsyringe: 4.10.0 + '@pkgjs/parseargs@0.11.0': optional: true @@ -12265,6 +13891,73 @@ snapshots: react-aria: 3.50.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8) react-dom: 19.2.8(react@19.2.8) + '@react-native/asset-utils@0.87.1': {} + + '@react-native/codegen@0.87.1(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/parser': 7.29.8 + hermes-parser: 0.36.1 + invariant: 2.2.4 + nullthrows: 1.1.1 + tinyglobby: 0.2.17 + yargs: 17.7.3 + + '@react-native/community-cli-plugin@0.87.1': + dependencies: + '@react-native/asset-utils': 0.87.1 + '@react-native/dev-middleware': 0.87.1 + debug: 4.4.3 + invariant: 2.2.4 + metro: 0.87.0 + semver: 7.8.5 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + '@react-native/debugger-frontend@0.87.1': {} + + '@react-native/debugger-shell@0.87.1': + dependencies: + cross-spawn: 7.0.6 + debug: 4.4.3 + fb-dotslash: 0.5.8 + transitivePeerDependencies: + - supports-color + + '@react-native/dev-middleware@0.87.1': + dependencies: + '@isaacs/ttlcache': 1.4.1 + '@react-native/debugger-frontend': 0.87.1 + '@react-native/debugger-shell': 0.87.1 + chrome-launcher: 0.15.2 + chromium-edge-launcher: 0.3.0 + connect: 3.7.0 + debug: 4.4.3 + invariant: 2.2.4 + nullthrows: 1.1.1 + open: 7.4.2 + serve-static: 1.16.3 + ws: 7.5.13 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + '@react-native/gradle-plugin@0.87.1': {} + + '@react-native/normalize-colors@0.87.1': {} + + '@react-native/virtualized-lists@0.87.1(@types/react@19.2.18)(react-native@0.87.1(@babel/core@7.29.7)(@types/react@19.2.18)(react@19.2.8))(react@19.2.8)': + dependencies: + invariant: 2.2.4 + nullthrows: 1.1.1 + react: 19.2.8 + react-native: 0.87.1(@babel/core@7.29.7)(@types/react@19.2.18)(react@19.2.8) + optionalDependencies: + '@types/react': 19.2.18 + '@react-types/shared@3.36.0(react@19.2.8)': dependencies: react: 19.2.8 @@ -12521,10 +14214,14 @@ snapshots: fflate: 0.7.5 string.prototype.codepointat: 0.2.1 + '@sinclair/typebox@0.27.12': {} + '@sinclair/typebox@0.34.40': {} '@sinclair/typebox@0.34.52': {} + '@sindresorhus/fnv1a@3.1.0': {} + '@sindresorhus/is@4.6.0': {} '@sinonjs/commons@3.0.1': @@ -13547,78 +15244,152 @@ snapshots: '@yuku-analyzer/binding-win32-x64@0.9.3': optional: true + '@yuku-codegen/binding-android-arm64@0.8.7': + optional: true + '@yuku-codegen/binding-android-arm64@0.9.5': optional: true + '@yuku-codegen/binding-darwin-arm64@0.8.7': + optional: true + '@yuku-codegen/binding-darwin-arm64@0.9.5': optional: true + '@yuku-codegen/binding-darwin-x64@0.8.7': + optional: true + '@yuku-codegen/binding-darwin-x64@0.9.5': optional: true + '@yuku-codegen/binding-freebsd-x64@0.8.7': + optional: true + '@yuku-codegen/binding-freebsd-x64@0.9.5': optional: true + '@yuku-codegen/binding-linux-arm-gnu@0.8.7': + optional: true + '@yuku-codegen/binding-linux-arm-gnu@0.9.5': optional: true + '@yuku-codegen/binding-linux-arm-musl@0.8.7': + optional: true + '@yuku-codegen/binding-linux-arm-musl@0.9.5': optional: true + '@yuku-codegen/binding-linux-arm64-gnu@0.8.7': + optional: true + '@yuku-codegen/binding-linux-arm64-gnu@0.9.5': optional: true + '@yuku-codegen/binding-linux-arm64-musl@0.8.7': + optional: true + '@yuku-codegen/binding-linux-arm64-musl@0.9.5': optional: true + '@yuku-codegen/binding-linux-x64-gnu@0.8.7': + optional: true + '@yuku-codegen/binding-linux-x64-gnu@0.9.5': optional: true + '@yuku-codegen/binding-linux-x64-musl@0.8.7': + optional: true + '@yuku-codegen/binding-linux-x64-musl@0.9.5': optional: true + '@yuku-codegen/binding-win32-arm64@0.8.7': + optional: true + '@yuku-codegen/binding-win32-arm64@0.9.5': optional: true + '@yuku-codegen/binding-win32-x64@0.8.7': + optional: true + '@yuku-codegen/binding-win32-x64@0.9.5': optional: true + '@yuku-parser/binding-android-arm64@0.8.7': + optional: true + '@yuku-parser/binding-android-arm64@0.9.5': optional: true + '@yuku-parser/binding-darwin-arm64@0.8.7': + optional: true + '@yuku-parser/binding-darwin-arm64@0.9.5': optional: true + '@yuku-parser/binding-darwin-x64@0.8.7': + optional: true + '@yuku-parser/binding-darwin-x64@0.9.5': optional: true + '@yuku-parser/binding-freebsd-x64@0.8.7': + optional: true + '@yuku-parser/binding-freebsd-x64@0.9.5': optional: true + '@yuku-parser/binding-linux-arm-gnu@0.8.7': + optional: true + '@yuku-parser/binding-linux-arm-gnu@0.9.5': optional: true + '@yuku-parser/binding-linux-arm-musl@0.8.7': + optional: true + '@yuku-parser/binding-linux-arm-musl@0.9.5': optional: true + '@yuku-parser/binding-linux-arm64-gnu@0.8.7': + optional: true + '@yuku-parser/binding-linux-arm64-gnu@0.9.5': optional: true + '@yuku-parser/binding-linux-arm64-musl@0.8.7': + optional: true + '@yuku-parser/binding-linux-arm64-musl@0.9.5': optional: true + '@yuku-parser/binding-linux-x64-gnu@0.8.7': + optional: true + '@yuku-parser/binding-linux-x64-gnu@0.9.5': optional: true + '@yuku-parser/binding-linux-x64-musl@0.8.7': + optional: true + '@yuku-parser/binding-linux-x64-musl@0.9.5': optional: true + '@yuku-parser/binding-win32-arm64@0.8.7': + optional: true + '@yuku-parser/binding-win32-arm64@0.9.5': optional: true + '@yuku-parser/binding-win32-x64@0.8.7': + optional: true + '@yuku-parser/binding-win32-x64@0.9.5': optional: true + '@yuku-toolchain/types@0.8.7': {} + '@yuku-toolchain/types@0.9.3': {} '@yuku-toolchain/types@0.9.5': {} @@ -13631,6 +15402,8 @@ snapshots: dependencies: event-target-shim: 5.0.1 + abort-error@1.0.2: {} + accepts@2.0.0: dependencies: mime-types: 3.0.2 @@ -13658,6 +15431,8 @@ snapshots: transitivePeerDependencies: - supports-color + agent-base@7.1.4: {} + ajv-formats@2.1.1(ajv@8.20.0): optionalDependencies: ajv: 8.20.0 @@ -13707,6 +15482,8 @@ snapshots: '@algolia/requester-node-http': 5.46.0 optional: true + anser@1.4.10: {} + ansi-escapes@4.3.2: dependencies: type-fest: 0.21.3 @@ -13729,6 +15506,8 @@ snapshots: ansis@4.3.1: {} + any-signal@4.2.0: {} + anymatch@3.1.3: dependencies: normalize-path: 3.0.0 @@ -13823,6 +15602,12 @@ snapshots: asap@2.0.6: {} + asn1js@3.0.10: + dependencies: + pvtsutils: 1.3.6 + pvutils: 1.2.0 + tslib: 2.8.1 + assertion-error@2.0.1: {} ast-types-flow@0.0.8: {} @@ -13886,6 +15671,10 @@ snapshots: dependencies: '@types/babel__core': 7.20.5 + babel-plugin-syntax-hermes-parser@0.36.1: + dependencies: + hermes-parser: 0.36.1 + babel-preset-current-node-syntax@1.2.0(@babel/core@7.29.7): dependencies: '@babel/core': 7.29.7 @@ -13923,6 +15712,8 @@ snapshots: base64-js@0.0.8: {} + base64-js@1.5.1: {} + baseline-browser-mapping@2.11.20: {} baseline-browser-mapping@2.11.21: {} @@ -14118,8 +15909,29 @@ snapshots: dependencies: readdirp: 5.1.1 + chrome-launcher@0.15.2: + dependencies: + '@types/node': 26.5.0 + escape-string-regexp: 4.0.0 + is-wsl: 2.2.0 + lighthouse-logger: 1.4.2 + transitivePeerDependencies: + - supports-color + chrome-trace-event@1.0.4: {} + chromium-edge-launcher@0.3.0: + dependencies: + '@types/node': 26.5.0 + escape-string-regexp: 4.0.0 + is-wsl: 2.2.0 + lighthouse-logger: 1.4.2 + mkdirp: 1.0.4 + transitivePeerDependencies: + - supports-color + + ci-info@2.0.0: {} + ci-info@3.9.0: {} ci-info@4.4.0: {} @@ -14198,6 +16010,8 @@ snapshots: comma-separated-tokens@2.0.3: {} + commander@12.1.0: {} + commander@15.0.0: {} commander@2.20.3: {} @@ -14224,6 +16038,15 @@ snapshots: readable-stream: 3.6.2 typedarray: 0.0.6 + connect@3.7.0: + dependencies: + debug: 2.6.9 + finalhandler: 1.1.2 + parseurl: 1.3.3 + utils-merge: 1.0.1 + transitivePeerDependencies: + - supports-color + content-disposition@1.1.0: {} content-type@1.0.5: {} @@ -14248,6 +16071,8 @@ snapshots: untildify: 4.0.0 yargs: 16.2.0 + core-js@3.50.0: {} + core-util-is@1.0.3: {} cors@2.8.6: @@ -14513,12 +16338,34 @@ snapshots: dataloader@2.2.3: {} + datastore-core@12.0.1: + dependencies: + '@libp2p/logger': 6.2.13 + abort-error: 1.0.2 + interface-datastore: 10.0.1 + interface-store: 8.0.0 + it-drain: 3.0.12 + it-filter: 3.1.6 + it-map: 3.1.6 + it-merge: 3.0.15 + it-pipe: 3.0.1 + it-sort: 3.0.11 + it-take: 3.0.11 + dayjs@1.11.23: {} + debug@2.6.9: + dependencies: + ms: 2.0.0 + debug@3.2.7: dependencies: ms: 2.1.3 + debug@4.3.4: + dependencies: + ms: 2.1.2 + debug@4.4.3: dependencies: ms: 2.1.3 @@ -14558,12 +16405,19 @@ snapshots: dependencies: robust-predicates: 3.0.3 + delay@7.0.0: + dependencies: + random-int: 3.1.0 + unlimited-timeout: 0.1.0 + delayed-stream@1.0.0: {} depd@2.0.0: {} dequal@2.0.3: {} + destroy@1.2.0: {} + detect-libc@2.1.2: {} detect-newline@3.1.0: {} @@ -14645,6 +16499,8 @@ snapshots: empathic@2.0.1: {} + encodeurl@1.0.2: {} + encodeurl@2.0.0: {} enhanced-resolve@5.24.3: @@ -14665,6 +16521,10 @@ snapshots: dependencies: is-arrayish: 0.2.1 + error-stack-parser@2.1.4: + dependencies: + stackframe: 1.3.4 + es-abstract-get@1.0.0: dependencies: es-errors: 1.3.0 @@ -15332,6 +17192,8 @@ snapshots: eventemitter3@2.0.3: {} + eventemitter3@5.0.4: {} + events@3.3.0: {} execa@5.1.1: @@ -15368,6 +17230,8 @@ snapshots: jest-mock: 30.5.1 jest-util: 30.5.1 + exponential-backoff@3.1.3: {} + express@5.2.1: dependencies: accepts: 2.0.0 @@ -15449,6 +17313,8 @@ snapshots: dependencies: reusify: 1.1.0 + fb-dotslash@0.5.8: {} + fb-watchman@2.0.2: dependencies: bser: 2.1.1 @@ -15480,6 +17346,18 @@ snapshots: dependencies: to-regex-range: 5.0.1 + finalhandler@1.1.2: + dependencies: + debug: 2.6.9 + encodeurl: 1.0.2 + escape-html: 1.0.3 + on-finished: 2.3.0 + parseurl: 1.3.3 + statuses: 1.5.0 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + finalhandler@2.1.1: dependencies: debug: 4.4.3 @@ -15515,6 +17393,8 @@ snapshots: flatted@3.4.4: {} + flow-enums-runtime@0.0.6: {} + follow-redirects@1.16.0: {} for-each@0.3.5: @@ -15569,6 +17449,8 @@ snapshots: react: 19.2.8 react-dom: 19.2.8(react@19.2.8) + fresh@0.5.2: {} + fresh@2.0.0: {} fs-extra@10.1.0: @@ -15732,6 +17614,8 @@ snapshots: get-package-type@0.1.0: {} + get-port@7.2.0: {} + get-proto@1.0.1: dependencies: dunder-proto: 1.0.1 @@ -15749,6 +17633,10 @@ snapshots: dependencies: resolve-pkg-maps: 1.0.0 + get-tsconfig@5.0.0-beta.5: + dependencies: + resolve-pkg-maps: 1.0.0 + get-tsconfig@5.0.0-beta.6: dependencies: resolve-pkg-maps: 1.0.0 @@ -15859,6 +17747,8 @@ snapshots: dependencies: hookified: 1.15.1 + hashlru@2.3.0: {} + hasown@2.0.4: dependencies: function-bind: 1.1.2 @@ -15971,12 +17861,20 @@ snapshots: property-information: 7.2.0 space-separated-tokens: 2.0.2 + hermes-compiler@250829098.0.17: {} + hermes-estree@0.25.1: {} + hermes-estree@0.36.1: {} + hermes-parser@0.25.1: dependencies: hermes-estree: 0.25.1 + hermes-parser@0.36.1: + dependencies: + hermes-estree: 0.36.1 + hex-rgb@4.3.0: {} hmac-drbg@1.0.1: @@ -16012,6 +17910,13 @@ snapshots: transitivePeerDependencies: - supports-color + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + human-id@4.2.1: {} human-signals@2.1.0: {} @@ -16030,6 +17935,10 @@ snapshots: ignore@7.0.9: {} + image-size@1.2.1: + dependencies: + queue: 6.0.2 + immutable@4.3.7: {} import-fresh@3.3.1: @@ -16059,6 +17968,16 @@ snapshots: inline-style-parser@0.2.7: {} + interface-datastore@10.0.1: + dependencies: + abort-error: 1.0.2 + interface-store: 8.0.0 + uint8arrays: 6.1.1 + + interface-store@8.0.0: + dependencies: + abort-error: 1.0.2 + internal-slot@1.1.0: dependencies: es-errors: 1.3.0 @@ -16069,6 +17988,10 @@ snapshots: internmap@2.0.3: {} + invariant@2.2.4: + dependencies: + loose-envify: 1.4.0 + ipaddr.js@1.9.1: {} is-alphabetical@2.0.1: {} @@ -16132,10 +18055,14 @@ snapshots: is-decimal@2.0.1: {} + is-docker@2.2.1: {} + is-document.all@1.0.0: dependencies: call-bound: 1.0.4 + is-electron@2.2.2: {} + is-extglob@2.1.1: {} is-finalizationregistry@1.1.1: @@ -16162,10 +18089,14 @@ snapshots: is-interactive@2.0.0: {} + is-loopback-addr@2.1.0: {} + is-map@2.0.3: {} is-negative-zero@2.0.3: {} + is-network-error@1.3.2: {} + is-number-object@1.1.1: dependencies: call-bound: 1.0.4 @@ -16222,6 +18153,10 @@ snapshots: call-bound: 1.0.4 get-intrinsic: 1.3.0 + is-wsl@2.2.0: + dependencies: + is-docker: 2.2.1 + isarray@0.0.1: {} isarray@1.0.0: {} @@ -16265,6 +18200,96 @@ snapshots: html-escaper: 2.0.2 istanbul-lib-report: 3.0.1 + it-all@3.0.11: {} + + it-byte-stream@3.0.0: + dependencies: + abort-error: 1.0.2 + it-queueless-pushable: 2.0.5 + it-stream-types: 2.0.4 + race-signal: 2.0.2 + uint8arraylist: 3.0.2 + + it-drain@3.0.12: {} + + it-filter@3.1.6: + dependencies: + it-peekable: 3.0.10 + + it-length-prefixed-stream@3.0.1: + dependencies: + abort-error: 1.0.2 + it-byte-stream: 3.0.0 + it-stream-types: 2.0.4 + uint8-varint: 3.0.0 + uint8arraylist: 3.0.2 + + it-length-prefixed@11.0.1: + dependencies: + it-reader: 7.0.0 + it-stream-types: 2.0.4 + uint8-varint: 3.0.0 + uint8arraylist: 3.0.2 + uint8arrays: 6.1.1 + + it-map@3.1.6: + dependencies: + it-peekable: 3.0.10 + + it-merge@3.0.15: + dependencies: + it-queueless-pushable: 2.0.5 + + it-parallel@3.0.16: + dependencies: + p-defer: 4.0.1 + + it-peekable@3.0.10: {} + + it-pipe@3.0.1: + dependencies: + it-merge: 3.0.15 + it-pushable: 3.2.4 + it-stream-types: 2.0.4 + + it-protobuf-stream@3.0.1: + dependencies: + abort-error: 1.0.2 + it-length-prefixed-stream: 3.0.1 + it-stream-types: 2.0.4 + uint8arraylist: 3.0.2 + + it-pushable@3.2.4: + dependencies: + p-defer: 4.0.1 + + it-queue@1.1.4: + dependencies: + abort-error: 1.0.2 + it-pushable: 3.2.4 + main-event: 1.0.5 + race-event: 1.6.1 + race-signal: 2.0.2 + + it-queueless-pushable@2.0.5: + dependencies: + abort-error: 1.0.2 + p-defer: 4.0.1 + race-signal: 2.0.2 + + it-reader@7.0.0: + dependencies: + it-stream-types: 2.0.4 + uint8arraylist: 3.0.2 + + it-sort@3.0.11: + dependencies: + it-all: 3.0.11 + + it-stream-types@2.0.4: {} + + it-take@3.0.11: {} + iterare@1.2.1: {} iterator.prototype@1.1.5: @@ -16401,6 +18426,8 @@ snapshots: jest-util: 30.5.1 jest-validate: 30.5.1 + jest-get-type@29.6.3: {} + jest-haste-map@30.5.1: dependencies: '@jest/types': 30.5.1 @@ -16574,6 +18601,15 @@ snapshots: transitivePeerDependencies: - supports-color + jest-util@29.7.0: + dependencies: + '@jest/types': 29.6.3 + '@types/node': 26.5.0 + chalk: 4.1.2 + ci-info: 3.9.0 + graceful-fs: 4.2.11 + picomatch: 2.3.2 + jest-util@30.0.5: dependencies: '@jest/types': 30.0.5 @@ -16592,6 +18628,15 @@ snapshots: graceful-fs: 4.2.11 picomatch: 4.0.7 + jest-validate@29.7.0: + dependencies: + '@jest/types': 29.6.3 + camelcase: 6.3.0 + chalk: 4.1.2 + jest-get-type: 29.6.3 + leven: 3.1.0 + pretty-format: 29.7.0 + jest-validate@30.5.1: dependencies: '@jest/get-type': 30.5.0 @@ -16618,6 +18663,13 @@ snapshots: merge-stream: 2.0.0 supports-color: 8.1.1 + jest-worker@29.7.0: + dependencies: + '@types/node': 26.5.0 + jest-util: 29.7.0 + merge-stream: 2.0.0 + supports-color: 8.1.1 + jest-worker@30.5.1: dependencies: '@types/node': 26.5.0 @@ -16666,6 +18718,8 @@ snapshots: jsbi@4.3.2: {} + jsc-safe-url@0.2.4: {} + jsesc@0.5.0: {} jsesc@3.1.0: {} @@ -16742,8 +18796,45 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 + libp2p@3.3.11: + dependencies: + '@chainsafe/is-ip': 2.1.0 + '@chainsafe/netmask': 2.0.0 + '@libp2p/crypto': 5.1.23 + '@libp2p/interface': 3.3.0 + '@libp2p/interface-internal': 3.1.13 + '@libp2p/logger': 6.2.13 + '@libp2p/multistream-select': 7.0.28 + '@libp2p/peer-collections': 7.0.28 + '@libp2p/peer-id': 6.0.15 + '@libp2p/peer-store': 12.0.28 + '@libp2p/utils': 7.4.1 + '@multiformats/dns': 1.0.15 + '@multiformats/multiaddr': 13.0.3 + '@multiformats/multiaddr-matcher': 3.0.2 + any-signal: 4.2.0 + datastore-core: 12.0.1 + interface-datastore: 10.0.1 + it-merge: 3.0.15 + it-parallel: 3.0.16 + main-event: 1.0.5 + multiformats: 14.0.5 + p-defer: 4.0.1 + p-event: 7.1.0 + p-retry: 8.0.1 + progress-events: 1.1.0 + race-signal: 2.0.2 + uint8arrays: 6.1.1 + libphonenumber-js@1.13.7: {} + lighthouse-logger@1.4.2: + dependencies: + debug: 2.6.9 + marky: 1.3.0 + transitivePeerDependencies: + - supports-color + lightningcss-android-arm64@1.32.0: optional: true @@ -16844,6 +18935,8 @@ snapshots: lodash.merge@4.6.2: {} + lodash.throttle@4.1.1: {} + log-symbols@7.0.1: dependencies: is-unicode-supported: 2.1.0 @@ -16887,12 +18980,18 @@ snapshots: '@babel/types': 7.29.8 source-map-js: 1.2.1 + main-event@1.0.5: {} + make-dir@4.0.0: dependencies: semver: 7.8.5 make-error@1.3.6: {} + makeerror@1.0.12: + dependencies: + tmpl: 1.0.5 + markdown-extensions@2.0.0: {} markdown-it@14.3.0: @@ -16910,6 +19009,8 @@ snapshots: marked@16.4.2: {} + marky@1.3.0: {} + math-intrinsics@1.1.0: {} mdast-util-directive@3.1.0: @@ -17101,6 +19202,8 @@ snapshots: fs-monkey: 1.1.0 optional: true + memoize-one@5.2.1: {} + merge-descriptors@2.0.0: {} merge-stream@2.0.0: {} @@ -17134,6 +19237,179 @@ snapshots: methods@1.1.2: {} + metro-babel-transformer@0.87.0: + dependencies: + '@babel/core': 7.29.7 + flow-enums-runtime: 0.0.6 + hermes-parser: 0.36.1 + metro-cache-key: 0.87.0 + nullthrows: 1.1.1 + transitivePeerDependencies: + - supports-color + + metro-cache-key@0.87.0: + dependencies: + flow-enums-runtime: 0.0.6 + + metro-cache@0.87.0: + dependencies: + exponential-backoff: 3.1.3 + flow-enums-runtime: 0.0.6 + https-proxy-agent: 7.0.6 + metro-core: 0.87.0 + transitivePeerDependencies: + - supports-color + + metro-config@0.87.0: + dependencies: + connect: 3.7.0 + flow-enums-runtime: 0.0.6 + jest-validate: 29.7.0 + metro: 0.87.0 + metro-cache: 0.87.0 + metro-core: 0.87.0 + metro-runtime: 0.87.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + metro-core@0.87.0: + dependencies: + flow-enums-runtime: 0.0.6 + lodash.throttle: 4.1.1 + metro-resolver: 0.87.0 + + metro-file-map@0.87.0: + dependencies: + debug: 4.4.3 + fb-watchman: 2.0.2 + flow-enums-runtime: 0.0.6 + graceful-fs: 4.2.11 + invariant: 2.2.4 + jest-worker: 29.7.0 + micromatch: 4.0.8 + nullthrows: 1.1.1 + walker: 1.0.8 + transitivePeerDependencies: + - supports-color + + metro-minify-terser@0.87.0: + dependencies: + flow-enums-runtime: 0.0.6 + terser: 5.51.2 + + metro-resolver@0.87.0: + dependencies: + flow-enums-runtime: 0.0.6 + + metro-runtime@0.87.0: + dependencies: + '@babel/runtime': 7.29.7 + flow-enums-runtime: 0.0.6 + + metro-source-map@0.87.0: + dependencies: + '@babel/traverse': 7.29.8 + '@babel/types': 7.29.8 + flow-enums-runtime: 0.0.6 + invariant: 2.2.4 + metro-symbolicate: 0.87.0 + nullthrows: 1.1.1 + ob1: 0.87.0 + source-map: 0.5.7 + vlq: 1.0.1 + transitivePeerDependencies: + - supports-color + + metro-symbolicate@0.87.0: + dependencies: + flow-enums-runtime: 0.0.6 + invariant: 2.2.4 + metro-source-map: 0.87.0 + nullthrows: 1.1.1 + source-map: 0.5.7 + vlq: 1.0.1 + transitivePeerDependencies: + - supports-color + + metro-transform-plugins@0.87.0: + dependencies: + '@babel/core': 7.29.7 + '@babel/generator': 7.29.8 + '@babel/template': 7.29.7 + '@babel/traverse': 7.29.8 + flow-enums-runtime: 0.0.6 + nullthrows: 1.1.1 + transitivePeerDependencies: + - supports-color + + metro-transform-worker@0.87.0: + dependencies: + '@babel/core': 7.29.7 + '@babel/generator': 7.29.8 + '@babel/parser': 7.29.8 + '@babel/types': 7.29.8 + flow-enums-runtime: 0.0.6 + metro: 0.87.0 + metro-babel-transformer: 0.87.0 + metro-cache: 0.87.0 + metro-cache-key: 0.87.0 + metro-minify-terser: 0.87.0 + metro-source-map: 0.87.0 + metro-transform-plugins: 0.87.0 + nullthrows: 1.1.1 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + metro@0.87.0: + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/core': 7.29.7 + '@babel/generator': 7.29.8 + '@babel/parser': 7.29.8 + '@babel/template': 7.29.7 + '@babel/traverse': 7.29.8 + '@babel/types': 7.29.8 + accepts: 2.0.0 + ci-info: 2.0.0 + connect: 3.7.0 + debug: 4.4.3 + error-stack-parser: 2.1.4 + flow-enums-runtime: 0.0.6 + graceful-fs: 4.2.11 + hermes-parser: 0.36.1 + image-size: 1.2.1 + invariant: 2.2.4 + jest-worker: 29.7.0 + jsc-safe-url: 0.2.4 + lodash.throttle: 4.1.1 + metro-babel-transformer: 0.87.0 + metro-cache: 0.87.0 + metro-cache-key: 0.87.0 + metro-config: 0.87.0 + metro-core: 0.87.0 + metro-file-map: 0.87.0 + metro-resolver: 0.87.0 + metro-runtime: 0.87.0 + metro-source-map: 0.87.0 + metro-symbolicate: 0.87.0 + metro-transform-plugins: 0.87.0 + metro-transform-worker: 0.87.0 + mime-types: 3.0.2 + nullthrows: 1.1.1 + serialize-error: 2.1.0 + source-map: 0.5.7 + throat: 5.0.0 + ws: 7.5.13 + yargs: 17.7.3 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + micromark-core-commonmark@2.0.3: dependencies: decode-named-character-reference: 1.3.0 @@ -17415,6 +19691,8 @@ snapshots: dependencies: mime-db: 1.54.0 + mime@1.6.0: {} + mime@2.6.0: {} mimic-fn@2.1.0: {} @@ -17462,6 +19740,12 @@ snapshots: dompurify: 3.4.8 marked: 14.0.0 + mortice@3.3.1: + dependencies: + abort-error: 1.0.2 + it-queue: 1.1.4 + main-event: 1.0.5 + motion-dom@13.1.1: dependencies: motion-utils: 13.0.0 @@ -17476,8 +19760,14 @@ snapshots: react: 19.2.8 react-dom: 19.2.8(react@19.2.8) + ms@2.0.0: {} + + ms@2.1.2: {} + ms@2.1.3: {} + ms@4.0.0-nightly.202508271359: {} + multer@2.2.0: dependencies: append-field: 1.0.0 @@ -17485,6 +19775,8 @@ snapshots: concat-stream: 2.0.0 type-is: 1.6.18 + multiformats@13.4.2: {} + multiformats@14.0.5: {} mute-stream@3.0.0: {} @@ -17493,6 +19785,8 @@ snapshots: nanoid@3.3.18: {} + nanoid@6.0.1: {} + napi-postinstall@0.3.4: {} natural-compare@1.4.0: {} @@ -17503,6 +19797,8 @@ snapshots: neo-async@2.6.2: {} + netmask@2.1.1: {} + next-themes@0.4.6(react-dom@19.2.8(react@19.2.8))(react@19.2.8): dependencies: react: 19.2.8 @@ -17538,6 +19834,20 @@ snapshots: node-addon-api@7.1.1: {} + node-datachannel@0.33.3: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + '@node-datachannel/android-arm64': 0.33.2 + '@node-datachannel/darwin-arm64': 0.33.2 + '@node-datachannel/darwin-x64': 0.33.2 + '@node-datachannel/linux-arm64-gnu': 0.33.2 + '@node-datachannel/linux-arm64-musl': 0.33.2 + '@node-datachannel/linux-x64-gnu': 0.33.2 + '@node-datachannel/linux-x64-musl': 0.33.2 + '@node-datachannel/win32-arm64-msvc': 0.33.2 + '@node-datachannel/win32-x64-msvc': 0.33.2 + node-emoji@2.2.0: dependencies: '@sindresorhus/is': 4.6.0 @@ -17580,6 +19890,12 @@ snapshots: npm-to-yarn@3.2.0: {} + nullthrows@1.1.1: {} + + ob1@0.87.0: + dependencies: + flow-enums-runtime: 0.0.6 + object-assign@4.1.1: {} object-inspect@1.13.4: {} @@ -17624,6 +19940,10 @@ snapshots: obug@2.1.4: {} + on-finished@2.3.0: + dependencies: + ee-first: 1.1.1 + on-finished@2.4.1: dependencies: ee-first: 1.1.1 @@ -17648,6 +19968,11 @@ snapshots: regex: 6.1.0 regex-recursion: 6.0.2 + open@7.4.2: + dependencies: + is-docker: 2.2.1 + is-wsl: 2.2.0 + optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -17675,6 +20000,12 @@ snapshots: object-keys: 1.1.1 safe-push-apply: 1.0.0 + p-defer@4.0.1: {} + + p-event@7.1.0: + dependencies: + p-timeout: 7.0.1 + p-limit@2.3.0: dependencies: p-try: 2.2.0 @@ -17691,8 +20022,21 @@ snapshots: dependencies: p-limit: 3.1.0 + p-queue@9.3.3: + dependencies: + eventemitter3: 5.0.4 + p-timeout: 7.0.1 + + p-retry@8.0.1: + dependencies: + is-network-error: 1.3.2 + + p-timeout@7.0.1: {} + p-try@2.2.0: {} + p-wait-for@6.0.0: {} + package-json-from-dist@1.0.1: {} package-manager-detector@1.8.0: {} @@ -17817,6 +20161,12 @@ snapshots: prettier@3.9.6: {} + pretty-format@29.7.0: + dependencies: + '@jest/schemas': 29.6.3 + ansi-styles: 5.2.0 + react-is: 18.3.1 + pretty-format@30.0.5: dependencies: '@jest/schemas': 30.0.5 @@ -17832,6 +20182,12 @@ snapshots: process-nextick-args@2.0.1: {} + progress-events@1.1.0: {} + + promise@8.3.0: + dependencies: + asap: 2.0.6 + prop-types@15.8.1: dependencies: loose-envify: 1.4.0 @@ -17840,6 +20196,18 @@ snapshots: property-information@7.2.0: {} + protons-runtime@5.6.0: + dependencies: + uint8-varint: 2.0.5 + uint8arraylist: 2.4.9 + uint8arrays: 5.1.1 + + protons-runtime@7.0.0: + dependencies: + uint8-varint: 3.0.0 + uint8arraylist: 3.0.2 + uint8arrays: 6.1.1 + proxy-addr@2.0.7: dependencies: forwarded: 0.2.0 @@ -17853,13 +20221,17 @@ snapshots: pure-rand@7.0.1: {} + pvtsutils@1.3.6: + dependencies: + tslib: 2.8.1 + + pvutils@1.2.0: {} + qified@0.10.1: dependencies: hookified: 2.2.0 - qrcode.react@4.2.0(react@19.2.8): - dependencies: - react: 19.2.8 + qr@0.6.0: {} qs@6.15.3: dependencies: @@ -17875,8 +20247,22 @@ snapshots: queue-microtask@1.2.3: {} + queue@6.0.2: + dependencies: + inherits: 2.0.4 + + race-event@1.6.1: + dependencies: + abort-error: 1.0.2 + + race-signal@2.0.2: {} + rambda@7.5.0: {} + random-int@3.1.0: {} + + range-parser@1.2.1: {} + range-parser@1.3.0: {} raw-body@3.0.2: @@ -17906,6 +20292,14 @@ snapshots: react-stately: 3.48.0(react@19.2.8) use-sync-external-store: 1.6.0(react@19.2.8) + react-devtools-core@6.1.5: + dependencies: + shell-quote: 1.10.0 + ws: 7.5.13 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + react-dom@19.2.8(react@19.2.8): dependencies: react: 19.2.8 @@ -17917,6 +20311,59 @@ snapshots: react-is@19.2.8: {} + react-native-webrtc@124.0.8(react-native@0.87.1(@babel/core@7.29.7)(@types/react@19.2.18)(react@19.2.8)): + dependencies: + base64-js: 1.5.1 + debug: 4.3.4 + react-native: 0.87.1(@babel/core@7.29.7)(@types/react@19.2.18)(react@19.2.8) + transitivePeerDependencies: + - supports-color + + react-native@0.87.1(@babel/core@7.29.7)(@types/react@19.2.18)(react@19.2.8): + dependencies: + '@react-native/asset-utils': 0.87.1 + '@react-native/codegen': 0.87.1(@babel/core@7.29.7) + '@react-native/community-cli-plugin': 0.87.1 + '@react-native/gradle-plugin': 0.87.1 + '@react-native/normalize-colors': 0.87.1 + '@react-native/virtualized-lists': 0.87.1(@types/react@19.2.18)(react-native@0.87.1(@babel/core@7.29.7)(@types/react@19.2.18)(react@19.2.8))(react@19.2.8) + anser: 1.4.10 + ansi-regex: 5.0.1 + babel-plugin-syntax-hermes-parser: 0.36.1 + base64-js: 1.5.1 + commander: 12.1.0 + flow-enums-runtime: 0.0.6 + hermes-compiler: 250829098.0.17 + invariant: 2.2.4 + memoize-one: 5.2.1 + metro-runtime: 0.87.0 + metro-source-map: 0.87.0 + nullthrows: 1.1.1 + pretty-format: 29.7.0 + promise: 8.3.0 + react: 19.2.8 + react-devtools-core: 6.1.5 + react-refresh: 0.14.2 + regenerator-runtime: 0.13.11 + scheduler: 0.27.0 + semver: 7.8.5 + stacktrace-parser: 0.1.11 + tinyglobby: 0.2.17 + whatwg-fetch: 3.6.20 + ws: 7.5.13 + yargs: 17.7.3 + optionalDependencies: + '@types/react': 19.2.18 + transitivePeerDependencies: + - '@babel/core' + - '@react-native-community/cli' + - '@react-native/metro-config' + - bufferutil + - supports-color + - utf-8-validate + + react-refresh@0.14.2: {} + react-remove-scroll-bar@2.3.8(@types/react@19.2.18)(react@19.2.8): dependencies: react: 19.2.8 @@ -18041,6 +20488,8 @@ snapshots: get-proto: 1.0.1 which-builtin-type: 1.2.1 + regenerator-runtime@0.13.11: {} + regex-recursion@6.0.2: dependencies: regex-utilities: 2.3.0 @@ -18167,6 +20616,8 @@ snapshots: onetime: 7.0.0 signal-exit: 4.1.0 + retimeable-signal@1.0.1: {} + reusify@1.1.0: {} rimraf@3.0.2: @@ -18175,6 +20626,20 @@ snapshots: robust-predicates@3.0.3: {} + rolldown-plugin-dts@0.27.14(@typescript/typescript6@6.0.2)(rolldown@1.2.7): + dependencies: + dts-resolver: 3.0.0 + get-tsconfig: 5.0.0-beta.5 + obug: 2.1.4 + rolldown: 1.2.7 + yuku-ast: 0.8.7 + yuku-codegen: 0.8.7 + yuku-parser: 0.8.7 + optionalDependencies: + typescript: '@typescript/typescript6@6.0.2' + transitivePeerDependencies: + - oxc-resolver + rolldown-plugin-dts@0.28.5(@typescript/typescript6@6.0.2)(rolldown@1.2.7): dependencies: dts-resolver: 3.0.0 @@ -18317,6 +20782,10 @@ snapshots: safer-buffer@2.1.2: {} + sanitize-filename@1.6.4: + dependencies: + truncate-utf8-bytes: 1.0.2 + satori@0.10.14: dependencies: '@shuding/opentype.js': 1.4.0-beta.0 @@ -18355,6 +20824,24 @@ snapshots: semver@7.8.5: {} + send@0.19.2: + dependencies: + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 0.5.2 + http-errors: 2.0.1 + mime: 1.6.0 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + send@1.2.1: dependencies: debug: 4.4.3 @@ -18371,6 +20858,17 @@ snapshots: transitivePeerDependencies: - supports-color + serialize-error@2.1.0: {} + + serve-static@1.16.3: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 0.19.2 + transitivePeerDependencies: + - supports-color + serve-static@2.2.1: dependencies: encodeurl: 2.0.0 @@ -18506,6 +21004,8 @@ snapshots: buffer-from: 1.1.2 source-map: 0.6.1 + source-map@0.5.7: {} + source-map@0.6.1: {} source-map@0.7.6: {} @@ -18536,8 +21036,16 @@ snapshots: stackback@0.0.2: {} + stackframe@1.3.4: {} + + stacktrace-parser@0.1.11: + dependencies: + type-fest: 0.7.1 + state-local@1.0.7: {} + statuses@1.5.0: {} + statuses@2.0.2: {} std-env@4.2.0: {} @@ -18706,6 +21214,8 @@ snapshots: transitivePeerDependencies: - supports-color + supports-color@10.2.2: {} + supports-color@5.5.0: dependencies: has-flag: 3.0.0 @@ -18775,6 +21285,8 @@ snapshots: third-party-capital@1.0.20: {} + throat@5.0.0: {} + through2@2.0.5: dependencies: readable-stream: 2.3.8 @@ -18795,6 +21307,8 @@ snapshots: tinyrainbow@3.1.1: {} + tmpl@1.0.5: {} + to-regex-range@5.0.1: dependencies: is-number: 7.0.0 @@ -18815,6 +21329,10 @@ snapshots: trough@2.2.0: {} + truncate-utf8-bytes@1.0.2: + dependencies: + utf8-byte-length: 1.0.5 + ts-api-utils@1.4.3(@typescript/typescript6@6.0.2): dependencies: typescript: '@typescript/typescript6@6.0.2' @@ -18894,6 +21412,33 @@ snapshots: minimist: 1.2.8 strip-bom: 3.0.0 + tsdown@0.22.14(@typescript/typescript6@6.0.2)(tsx@4.23.13)(unrun@0.2.22(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.3)(synckit@0.11.13)): + dependencies: + ansis: 4.3.1 + cac: 7.0.0 + defu: 6.1.7 + empathic: 2.0.1 + hookable: 6.1.1 + import-without-cache: 0.4.0 + obug: 2.1.4 + picomatch: 4.0.7 + rolldown: 1.2.7 + rolldown-plugin-dts: 0.27.14(@typescript/typescript6@6.0.2)(rolldown@1.2.7) + tinyexec: 1.3.1 + tinyglobby: 0.2.17 + tree-kill: 1.2.2 + unconfig-core: 7.5.0 + verkit: 0.3.2 + optionalDependencies: + tsx: 4.23.13 + typescript: '@typescript/typescript6@6.0.2' + unrun: 0.2.22(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.3)(synckit@0.11.13) + transitivePeerDependencies: + - '@typescript/native-preview' + - '@volar/typescript' + - oxc-resolver + - vue-tsc + tsdown@0.23.0(@typescript/typescript6@6.0.2)(tsx@4.23.13)(unrun@0.2.22(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.3)(synckit@0.11.13)): dependencies: cac: 7.0.0 @@ -18920,6 +21465,8 @@ snapshots: - oxc-resolver - vue-tsc + tslib@1.14.1: {} + tslib@2.3.1: {} tslib@2.7.0: {} @@ -18932,6 +21479,10 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + tsyringe@4.10.0: + dependencies: + tslib: 1.14.1 + type-check@0.4.0: dependencies: prelude-ls: 1.2.1 @@ -18944,6 +21495,8 @@ snapshots: type-fest@0.6.0: {} + type-fest@0.7.1: {} + type-fest@0.8.1: {} type-fest@4.41.0: {} @@ -19081,12 +21634,38 @@ snapshots: dependencies: '@lukeed/csprng': 1.1.0 + uint8-varint@2.0.5: + dependencies: + uint8arraylist: 2.4.9 + uint8arrays: 5.1.1 + + uint8-varint@3.0.0: + dependencies: + uint8arraylist: 3.0.2 + uint8arrays: 6.1.1 + uint8array-extras@1.5.0: {} uint8array-tools@0.0.8: {} uint8array-tools@0.0.9: {} + uint8arraylist@2.4.9: + dependencies: + uint8arrays: 5.1.1 + + uint8arraylist@3.0.2: + dependencies: + uint8arrays: 6.1.1 + + uint8arrays@5.1.1: + dependencies: + multiformats: 13.4.2 + + uint8arrays@6.1.1: + dependencies: + multiformats: 14.0.5 + unbox-primitive@1.1.0: dependencies: call-bound: 1.0.4 @@ -19159,6 +21738,8 @@ snapshots: universalify@2.0.1: optional: true + unlimited-timeout@0.1.0: {} + unpipe@1.0.0: {} unrs-resolver@1.12.2: @@ -19229,8 +21810,14 @@ snapshots: dependencies: react: 19.2.8 + utf8-byte-length@1.0.5: {} + + utf8-codec@1.0.0: {} + util-deprecate@1.0.2: {} + utils-merge@1.0.1: {} + uuid@14.0.2: {} v8-compile-cache-lib@3.0.1: {} @@ -19258,6 +21845,8 @@ snapshots: vary@1.1.2: {} + verkit@0.3.2: {} + verkit@0.4.0: {} vfile-location@5.0.3: @@ -19365,12 +21954,31 @@ snapshots: transitivePeerDependencies: - msw + vlq@1.0.1: {} + + walker@1.0.8: + dependencies: + makeerror: 1.0.12 + watchpack@2.5.2: dependencies: graceful-fs: 4.2.11 + weald@1.1.3: + dependencies: + ms: 4.0.0-nightly.202508271359 + supports-color: 10.2.2 + web-namespaces@2.0.1: {} + webcrypto-core@1.9.2: + dependencies: + '@peculiar/asn1-schema': 2.9.4 + '@peculiar/json-schema': 1.1.12 + '@peculiar/utils': 2.0.3 + asn1js: 3.0.10 + tslib: 2.8.1 + webidl-conversions@3.0.1: {} webpack-node-externals@3.0.0: @@ -19458,11 +22066,17 @@ snapshots: - postcss - uglify-js + whatwg-fetch@3.6.20: {} + whatwg-url@5.0.0: dependencies: tr46: 0.0.3 webidl-conversions: 3.0.1 + wherearewe@2.0.1: + dependencies: + is-electron: 2.2.2 + which-boxed-primitive@1.1.1: dependencies: is-bigint: 1.1.0 @@ -19536,6 +22150,8 @@ snapshots: imurmurhash: 0.1.4 signal-exit: 4.1.0 + ws@7.5.13: {} + ws@8.21.0: {} ws@8.21.3: {} @@ -19607,6 +22223,10 @@ snapshots: '@yuku-analyzer/binding-win32-arm64': 0.9.3 '@yuku-analyzer/binding-win32-x64': 0.9.3 + yuku-ast@0.8.7: + dependencies: + '@yuku-toolchain/types': 0.8.7 + yuku-ast@0.9.3: dependencies: '@yuku-toolchain/types': 0.9.3 @@ -19615,6 +22235,23 @@ snapshots: dependencies: '@yuku-toolchain/types': 0.9.5 + yuku-codegen@0.8.7: + dependencies: + '@yuku-toolchain/types': 0.8.7 + optionalDependencies: + '@yuku-codegen/binding-android-arm64': 0.8.7 + '@yuku-codegen/binding-darwin-arm64': 0.8.7 + '@yuku-codegen/binding-darwin-x64': 0.8.7 + '@yuku-codegen/binding-freebsd-x64': 0.8.7 + '@yuku-codegen/binding-linux-arm-gnu': 0.8.7 + '@yuku-codegen/binding-linux-arm-musl': 0.8.7 + '@yuku-codegen/binding-linux-arm64-gnu': 0.8.7 + '@yuku-codegen/binding-linux-arm64-musl': 0.8.7 + '@yuku-codegen/binding-linux-x64-gnu': 0.8.7 + '@yuku-codegen/binding-linux-x64-musl': 0.8.7 + '@yuku-codegen/binding-win32-arm64': 0.8.7 + '@yuku-codegen/binding-win32-x64': 0.8.7 + yuku-codegen@0.9.5: dependencies: '@yuku-toolchain/types': 0.9.5 @@ -19632,6 +22269,24 @@ snapshots: '@yuku-codegen/binding-win32-arm64': 0.9.5 '@yuku-codegen/binding-win32-x64': 0.9.5 + yuku-parser@0.8.7: + dependencies: + '@yuku-toolchain/types': 0.8.7 + yuku-ast: 0.8.7 + optionalDependencies: + '@yuku-parser/binding-android-arm64': 0.8.7 + '@yuku-parser/binding-darwin-arm64': 0.8.7 + '@yuku-parser/binding-darwin-x64': 0.8.7 + '@yuku-parser/binding-freebsd-x64': 0.8.7 + '@yuku-parser/binding-linux-arm-gnu': 0.8.7 + '@yuku-parser/binding-linux-arm-musl': 0.8.7 + '@yuku-parser/binding-linux-arm64-gnu': 0.8.7 + '@yuku-parser/binding-linux-arm64-musl': 0.8.7 + '@yuku-parser/binding-linux-x64-gnu': 0.8.7 + '@yuku-parser/binding-linux-x64-musl': 0.8.7 + '@yuku-parser/binding-win32-arm64': 0.8.7 + '@yuku-parser/binding-win32-x64': 0.8.7 + yuku-parser@0.9.5: dependencies: '@yuku-toolchain/types': 0.9.5 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 090df4745..fba67cac3 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -8,6 +8,7 @@ allowBuilds: core-js: true core-js-pure: true esbuild: true + node-datachannel: true secp256k1: true sharp: true unrs-resolver: true diff --git a/vitest.config.mts b/vitest.config.mts index 8bd4f4c26..21d8f7e24 100644 --- a/vitest.config.mts +++ b/vitest.config.mts @@ -1,8 +1,10 @@ import { defineConfig, coverageConfigDefaults } from "vitest/config"; const packages = [ + "packages/connector", "packages/core", "packages/did-ckb", + "packages/libp2p", "packages/nip07", "packages/ssri", "packages/type-id",