diff --git a/Cargo.lock b/Cargo.lock index 4c87ae9..cb472f8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -359,13 +359,13 @@ checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" [[package]] name = "async-trait" -version = "0.1.89" +version = "0.1.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 3.0.6", ] [[package]] @@ -872,7 +872,7 @@ version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.48.0", ] [[package]] @@ -1039,7 +1039,7 @@ checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crsl-lib" version = "0.1.0" -source = "git+https://github.com/Monas-project/crsl-lib?rev=e13b86ce6d6a9c27ebd01a9b4fe82d6bc18f8a01#e13b86ce6d6a9c27ebd01a9b4fe82d6bc18f8a01" +source = "git+https://github.com/Monas-project/crsl-lib?rev=0fbabbf#0fbabbf0c054444dc284f7b0f90fc8ae56a52798" dependencies = [ "bincode 2.0.1", "cid", @@ -1160,7 +1160,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d162beedaa69905488a8da94f5ac3edb4dd4788b732fadb7bd120b2625c1976" dependencies = [ "data-encoding", - "syn 2.0.114", + "syn 1.0.109", ] [[package]] @@ -3223,6 +3223,7 @@ dependencies = [ "base64 0.21.7", "base64-url", "bs58", + "cbor4ii", "cid", "clap", "crsl-lib", @@ -3240,6 +3241,7 @@ dependencies = [ "parking_lot 0.12.5", "rand 0.8.5", "serde", + "serde_bytes", "serde_json", "sha2", "sha3", @@ -4735,6 +4737,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "sync_wrapper" version = "1.0.2" diff --git a/docs/design.md b/docs/design.md index dc6ad0f..8b292ff 100644 --- a/docs/design.md +++ b/docs/design.md @@ -260,6 +260,8 @@ flowchart TD 失効を先に行うのは、逆順だと「再暗号化してから失効するまでの窓」で取り消し済みの相手が書き込めてしまうためである。先に失効させておけば、後段が失敗してローカル状態を巻き戻しても、余分な失効が残るだけで害はない。 +ただし失効はstate-nodeの**1メンバーにcommitされた時点で成功**であり、他メンバーへの伝播はベストエフォートのpush + 定期syncである。各メンバーの認可は自分の持つpolicyに対するローカル判断なので、境界がまだ届いていないメンバーは旧Tokenのwriteをその間受理する。取り消しはこれを待たない(書き手が取り消しを妨げられてはならない)。代わりにstate-nodeは届いた/届かなかったメンバーを返し(`InvalidateTokensOutcome`)、SDKは`RevokeShareOutput::token_invalidation_reach`として呼び出し側に見せる。そうして受理されたwriteが失効を巻き戻さないことは、CRDTのフィールド別マージ(§11)が保証する。 + `min_valid_issued_at`は時刻ベースの一括失効なので、**残存する受信者のTokenも巻き添えで失効する**(判定は排他なので、取り消しと同じ秒に発行されたTokenも失効する)。呼び出し側は取り消し後に、残存受信者へ新しいKeyEnvelopeと新しいTokenの両方を配り直す必要がある。SDKは`RevokeShareOutput`で再発行KeyEnvelope(`reissued_envelopes`)と失効時刻(`token_invalidated_at`)の両方を返す。 取り消しはACL・CEK・ローカルciphertext・state node状態にまたがるload-modify-saveであり、そのどれにもversion CASが無い。したがって**同じcontentへの取り消しはcontent単位で直列化する**。並行させると、双方が同じShareを読んで後勝ちでsaveし片方の受信者削除が消える(lost update)、異なるCEKが同じ`key_epoch`として配られる、といった分岐が起こる。SDKのコントローラはgatewayから共有され複数リクエストから同時に呼ばれるため、これは理論上の話ではない。現状の直列化はプロセス内に閉じており、複数gatewayプロセスからの並行取り消しには対応しない — そこまで守るにはShare・CEK・ciphertextを1つのtransactional CASにまとめるか、state node側にCASを置く必要がある。 @@ -456,10 +458,28 @@ crsl-libはMonasのために設計されたCIDネイティブなDAG CRDTライ 他のノードへ同期 │ ▼ -コンフリクト時はLWW(Last-Write-Wins)でマージ +コンフリクト時はフィールド別にマージ(下記) ``` -コンテンツ本体の意味的なマージは現時点で未実装であり、研究課題として位置づけられている。 +並行して進んだ版(複数のhead)は、次のcommitか、headを読む操作の直前に1つのMergeノードへ畳まれる。畳み方はcrsl-libが利用側から受け取るマージポリシーで決まり(`Repo::with_merge_policy`)、state-nodeは版のpayloadを**フィールドごとに別の規則**で畳む: + +| フィールド | 規則 | 理由 | +|---|---|---| +| コンテンツ本体(ciphertext) | `(body_updated_at, data)` の辞書順 max | 明示的な本文更新でのみ順序を進め、policy-only 更新と Merge は本文と順序をそのまま引き継ぐ。head 自体の timestamp や直近の親との差分では選ばない | +| `access_policy.min_valid_issued_at` | 全headの**max** | 失効境界は単調にしか進まない。timestampで選ぶと、境界を知らないノードが受理した並行writeが境界を巻き戻す | +| `access_policy.owner` / `content_id` | 不変(genesisで確定) | — | + +payload全体をtimestampで丸ごと選ぶ(純粋なLWW)と、revokeと並行するwriteの一方が必ず消える — writeがtimestampで勝てばrevokeが消え、revokeが勝てば正当なwriteが消える。どちらも「競合していないフィールドの変更が、競合したフィールドの勝敗に巻き込まれる」のが原因で、フィールド別に畳めば両方残る。マージポリシーはプロセスに焼かれておりデータとともには流れないため、**同じContent Networkの全メンバーが同じ規則を持つ**必要がある。 + +このマージが決めるのは「Mergeノードに何を入れるか」であり、「そのheadを受理してよかったか」ではない。失効境界を知らないメンバーが旧Tokenで受理したwriteは、最新のwriteであれば本体として残る(境界は残るので以後は書けない)。それを弾くにはwriteが自分のTokenを持ち歩き、マージ時に畳んだ境界に対して検証する必要がある — ワイヤ形式の変更を伴うため別issueで追跡する。 + +`body_updated_at` は本文と同じ payload に保存する論理的な更新順序であり、別 DAG ではない。本文更新ではローカルの単調 timestamp と観測済みの順序 + 1 の大きい方を採る。policy-only 更新、再マージ、再起動で順序を失わず、同値時は ciphertext の辞書順で決定する。観測・マージ・payload の生成・commit は同じ repository lock 内で行う。 + +同期 export は operation と DAG ノードを payload・parents・genesis・metadata で対応付け、実ノードの timestamp を送る。履歴の位置対応は使用しない。`since_version` はそのノードと祖先を既知とみなし、兄弟枝を省かず親から順に送る。曖昧な対応は推測せずエラーにする。 + +保存・wire 形式の変更: `body_updated_at` は必須で、旧形式を 0 等へ暗黙補完しない。現行デモは顧客利用前のため、全 state-node を同時更新し、新しいストアから開始してコンテンツを再作成する必要がある。既存ストアを維持する場合の移行は未実装。データ削除やデプロイは本変更では行わない。 + +コンテンツ本体の意味的なマージ(同じフィールド内での両立)は現時点で未実装であり、研究課題として位置づけられている。 ### 将来のCRDT拡張 diff --git a/docs/revoke-write-bypass-investigation.md b/docs/revoke-write-bypass-investigation.md new file mode 100644 index 0000000..468316f --- /dev/null +++ b/docs/revoke-write-bypass-investigation.md @@ -0,0 +1,127 @@ +# Revoke後の書き込みバイパス調査 — 旧delegated tokenの書き込みがstate nodeに受理される + +- 日付: 2026-09-10 +- 発見経緯: example-ui のデモ録画(revoke/削除/権限変更ジャーニー)の自動実行中に検出。2回連続で再現(cutoff伝播待ち10秒を入れても再現) +- 対象: `monas-ui-wt` worktree(branch `feat/example-ui-monas-drive`)+ demoノード node1〜node4.monas-demo.net +- 深刻度: High — revoke の完全性保証(revoke後は書き込めない)がクラスタ全体で成立していない。機密性は CEK ローテーションで維持されている(後述) + +## 症状 + +再現ジャーニー(`.demo-permissions.mjs` Phase D): + +1. Alice (owner, gateway :3000 → node1) がファイルを作成し、Bob (gateway :3001 → node2) に read+write で共有 +2. Alice が Bob を revoke + - UI/SDK 上は成功: 「Invalidate prior tokens: Token cutoff advanced on the state-node first — before rotation, so the revoked recipient cannot write in between」「token cutoff 1789033235」 +3. Bob の旧tokenでの「Edit contents」の **読み込みは拒否される**(Could not load contents) +4. しかし **10秒以上待った後の保存(update)は受理される**: + `PUT bafkrei… accepted: token gP61Fq… grants write` +5. Alice の verified read でネットワーク head が Bob の書き込み("this must not land")に置き換わったことを確認 + - 証跡スクリーンショット: `/tmp/monas-demo2-videos/bug-head-after-revoked-write.png` ほか(bug-write-accepted-{alice,bob}.png) + +## 根本原因 + +**revoke の失効境界(`min_valid_issued_at`)はメンバーノード間でベストエフォート伝播であり、かつ write の relay は認可拒否(403)を受けても次のメンバーへフェイルオーバーし続けるため、「まだ revoke を知らないメンバー」が1台でもあれば旧tokenの書き込みがそこで受理される。** + +### 経路の詳細 + +関連コードはすべて `monas-state-node/src/`。 + +1. **revoke時の失効伝播に保証がない** — `application_service/state_node_service.rs` `invalidate_tokens_inner()` + - genesis を持つノードが CRDT の access_policy に新 `min_valid_issued_at` をコミット + - 他メンバーへは `push_operations` で送るが、失敗しても `tracing::warn!(… will rely on sync)` のみ。呼び出しは成功として返る + - 追いつきは periodic sync(30s間隔、`application_service/node.rs`。前回runの遅延でさらに遅れうる)任せ + +2. **writeのrelayは403でも止まらない** — 同ファイル `relay_with_failover()`(757行付近) + - `auth_verdict_is_authoritative()` は **常に false**(190〜197行) + - コメントにある設計判断: owner-signed membership (issue #63) が入るまで、メンバーであることを証明できない候補の403は「偽403で書き込みを封じる攻撃」でありうるため、拒否を受けても次の候補へ続行し、全滅した場合のみ最後に拒否を返す(availability優先) + - 結果として、revoke済みを知っているメンバーが拒否しても、**cutoff未達のメンバーを探し当てた時点で書き込み成功**になる + +3. **受理側の認可はローカルビュー依存** — `infrastructure/auth/ucan_adapter.rs` `authorize()` / `verify_auth_token()` + - 検証は `content_repo.get_access_policy()`(= 自ノードのCRDT headのaccess_policy)の `min_valid_issued_at` に対する `iat > cutoff`(排他)チェック + - ロジック自体は正しい。**ローカル判定は正しいがビューが古い**、分散整合性の問題 + +4. **受理された書き込みは正当なheadとして伝播する** — `infrastructure/crdt_repository.rs` `update_content()` + - access_policy: None は既存policyを保存し、CRDT headが進む。以後のsyncで全ノードに伝播し、owner の verified read にも「recipient with write access が編集した新しい版」として見える + +### 前提が崩れるポイント + +`relay_with_failover` の「本物のメンバーなら全員同じ判定を返すはず」という前提は、revoke直後のポリシー不一致ウィンドウでは成立しない。このウィンドウ中、フェイルオーバーは「一番古いビューを持つメンバーを探し当てる」動作になる。relay固有の問題でもなく、revoked recipient が悪意クライアントとして各メンバーへ直接試行しても同じ。 + +### 補足: readが拒否されたのはなぜか + +Phase D で Bob の read(Edit contents の読み込み)が拒否されたのは認可ではなく **CEKローテーション** のため(revokeで新CEKに再暗号化済み、旧CEKでは復号不能)。read の認可も同じ弱点を持つはずで、cutoff未達メンバーからは旧tokenで旧版ciphertextを読める可能性がある。つまり: + +- 機密性(新しい版を読めない): CEKローテーションで守られている +- 完全性(revoke後に書けない): **破れている** ← 本バグ + +UI/SDKの表示「Token cutoff advanced on the state-node first — before rotation, so the revoked recipient cannot write in between」は単一ノード内でのみ真で、クラスタ全体では成り立っていない。 + +## 対策案 + +1. **短期** — `invalidate_tokens` の完了条件強化 + - cutoff適用を全メンバー(少なくとも過半数)への同期適用成功で完了とする + - `push_operations` 失敗を warn で飲まず、部分成功を SDK に返し、UI の「cannot write in between」の断定表示をやめる +2. **中期** — write受理時の再検証 + - メンバーがcommit前に quorum read で最新cutoffを確認する、または「revoke操作が自ノードheadに含まれているか」を検証してから受理 +3. **設計** — issue #63(owner-signed membership)の実装 + - メンバーであることを owner 署名で証明できれば `auth_verdict_is_authoritative` を復活でき、attestedメンバーの403で即打ち切りできる(偽403攻撃と両立) + +## 再現手順 + +前提: node1〜node4 が `/node/register` 済み(空なら全createが "No available member nodes found (HTTP 500)" で落ちる。登録は +`curl -X POST https://nodeN.monas-demo.net/node/register -H 'Content-Type: application/json' -d '{"total_capacity":1000000}'`)。 +ローカルスタック: vite :5173/:5174、gateway :3000(node1)/:3001(node2)、account :4002/:4003。 + +``` +cd /Users/soma/monas/monas-ui-wt/example-ui +node .demo-permissions.mjs # Phase D で "BUG: revoked recipient's write was accepted" で停止 +``` + +スクリプト: `example-ui/.demo-permissions.mjs`(untracked、録画付きジャーニー)。 +Phase A〜C(write共有での編集、AlreadyShared確認、revoke→再shareによる権限ダウングレード/アップグレード)は通過し、Phase D の「revoke後の書き込み拒否」検証で停止する。 + +## 関連する既知の設計・issue + +- issue #63: owner-signed membership(`auth_verdict_is_authoritative` 復活の前提) +- issue #61: request署名のリプレイ防御をtimestamp鮮度チェックに一本化(jti単回消費の廃止) +- bug #93: 非メンバーノードのrelay(1-hop制限)— 本バグのwrite relay経路そのもの +- `docs/` の該当設計メモがあれば追記のこと + +## 未確定事項 + +- 受理したメンバーへの `push_operations` が実際に失敗していたのか、それとも periodic sync の遅延だけで説明できるのか(demoノードのログ未確認) +- read側のバイパス(cutoff未達メンバーからの旧版read)の実地再現は未実施 + +## 決定(2026-09-10) + +検討した3案: + +- A. 入場審査を quorum に — revoke は過半数メンバーへの適用成功で完了、delegated write の受理は他メンバーの最新ビューを過半数確認できたときのみ。q+q>k で「成功した revoke 後の旧トークン write は必ずどこかで 403」が成立する。LWW マージ自体は変えない(認可は commit 前の入口チェック)。代償はメンバー過半数に届かないときの delegated write / revoke の可用性。 +- B. 自己証明 op + policy-aware head — update op に token(iat・capability の証明)を埋め、head 導出を「op 集合内の最大 cutoff に対して認可が成立する op だけを LWW で畳む」に変える。cutoff は単調なので収束性は保たれる。crsl-lib の head 計算・node_verification・SDK の verified read まで波及する別 PR 規模。 +- C. warn のみ — 保証は与えず、状態を正直に報告する。 + +**C を採用**(PR #47 内で完結させるため)。B は別 issue として起票する。 + +### 追記: マージ規則の欠陥(C の後に判明) + +C の実装後、A/B/C のどれとも別に、**CRDT のマージ規則そのものが revoke を消す**ことが分かった。access_policy は版ノードの payload に本体と同居しており、crsl-lib の Merge は payload を timestamp で丸ごと選ぶ(純 LWW)。よって revoke と並行する write が timestamp で勝つと、Merge ノードの policy は write 側の古い `min_valid_issued_at` になり、失効境界が巻き戻る — 「窓の中で1回書ける」ではなく「窓の中で1回書ければ以後も書ける」だった。逆(revoke が timestamp で勝つ)では、本体を変えていない revoke ノードが並行する正当な write を消す。 + +これは A/B の代替ではなく前提で、分断や sync 遅延など「並行 head が生じる状況」すべてで起きる。修正は「policy を別 DAG に出す」のではなく、同じ payload のままフィールド別に畳む(本体は本体を変えた head の中で LWW、`min_valid_issued_at` は max)。crsl-lib に利用側からマージポリシーを注入する口(`Repo::with_merge_policy`)と、head を読む前に並行 head を畳む口(`Repo::merge_heads`)を足し、state-node で `MonasMergePolicy` を注入する。詳細は design.md §11。 + +- crsl-lib: PR (feat/injectable-merge-policy) +- monas: PR (feat/policy-aware-merge → feat/example-ui-monas-drive) + +残るのは「窓の中の write が1回本体として残る」だけで、それは B で閉じる。 + +### C で入れたもの + +- `monas-state-node` `invalidate_tokens_inner`: 各メンバーへの `push_operations` を1回リトライし、届いた/届かなかったメンバーを `InvalidateTokensOutcome { new_min_valid_issued_at, notified_members, unreached_members, relayed }` で返す。挙動(revoke は待たない・失敗しない)は変えない。relay 経路では伝播情報は「不明」(`relayed: true`)。 +- HTTP `POST /content/:id/access/invalidate` レスポンスに `notified_members` / `unreached_members` / `relayed` を**常に**含める(旧ノードとの判別のため `skip_serializing_if` を使わない)。 +- `monas-sdk` `RevokeShareOutput.token_invalidation_reach`(旧ノード応答では `None` = 不明。空リストを「全員到達」と誤読しない)。 +- example-ui: revoke の Protocol activity に「Cutoff propagation」ステップを追加し、全員到達 / N 台未到達(+~30 s の窓の説明) / relay で不明 / 旧ノードで不明 を出し分け。未到達・不明のときはトーストでも警告。「cannot write in between」という断定文言は削除。 +- テスト: state-node 単体(未到達メンバーの報告・リトライ回数・全員到達)、SDK 単体(旧/新レスポンスの判別)。 + +### 残課題 + +- B の起票(`docs/` にこのメモをリンク)。 +- demo ノード(node1〜4)は旧バイナリのため、UI 上は「reach unknown」表示になる。新バイナリのデプロイ後に `.demo-permissions.mjs` Phase D を再実行し、node3 等を落とした状態で `unreached_members` が出ることを確認する。 diff --git a/example-ui/.env.example b/example-ui/.env.example new file mode 100644 index 0000000..bdd4df6 --- /dev/null +++ b/example-ui/.env.example @@ -0,0 +1,11 @@ +# Dev-server proxy targets (used by vite.config.ts). +# These point the same-origin /api and /account-api paths at your local +# services so the browser never hits CORS during local development. +# +# Copy to `.env` and edit if your Docker maps different ports. + +# monas-gateway — the main backend the UI calls (embeds monas-sdk). +VITE_GATEWAY_TARGET=http://127.0.0.1:3000 + +# monas-account — only used by "create account" to seed the P-256 signing key. +VITE_ACCOUNT_TARGET=http://127.0.0.1:4002 diff --git a/example-ui/.gitignore b/example-ui/.gitignore new file mode 100644 index 0000000..545d149 --- /dev/null +++ b/example-ui/.gitignore @@ -0,0 +1,28 @@ +node_modules +dist +dist2 +dist-ssr +*.local +.env +.DS_Store +*.tsbuildinfo +vite.config.ts.timestamp-*.mjs + +# Demo-recording harness — not part of the app (kept locally, not committed). +recording/ + +# Playwright run artifacts (traces, screenshots, HTML report). +test-results/ +playwright-report/ +blob-report/ +.playwright-artifacts-*/ + +# Playwright agent definitions — regenerate with +# npx playwright init-agents --loop= +# The choice of loop is per-developer, and these must be regenerated whenever +# Playwright is updated, so they are not committed. +.claude/ +.mcp.json + +# vite dep-optimizer cache (deps_temp_* is left behind by an interrupted `vite`) +.vite diff --git a/example-ui/.gitleaksignore b/example-ui/.gitleaksignore new file mode 100644 index 0000000..a55f902 --- /dev/null +++ b/example-ui/.gitleaksignore @@ -0,0 +1,5 @@ +# Reviewed false positives (gitleaks high-entropy vault-service-token rule +# misfiring on long camelCase identifiers, not real secrets). +# +# App.tsx:301 is the revoke handler comparing two public-key variable names. +src/App.tsx:vault-service-token:301 diff --git a/example-ui/README.md b/example-ui/README.md new file mode 100644 index 0000000..e2c4568 --- /dev/null +++ b/example-ui/README.md @@ -0,0 +1,394 @@ +# Monas Drive — example UI + +A minimal, Google-Drive-like web UI for the Monas protocol, built on the +**monas-sdk** via the **monas-gateway** HTTP API. It lets you **create, open, +edit, share, revoke and delete** files, and surfaces the +encryption + state-node work behind every action in a live **Protocol activity** +panel (CEK → AES-256-GCM → SHA-256 CID → storage → state-node → HPKE). + +The UI talks to a **single backend — the gateway** — which embeds the SDK and +orchestrates everything server-side: + +``` +┌──────────────┐ /api/* (Vite proxy) ┌───────────────┐ embeds monas-sdk +│ this UI │ ────────────────────────▶ │ monas-gateway │ ─┬─▶ encrypt + store (monas-content) +│ (React+Vite) │ single endpoint │ :3000 │ ├─▶ state-node (:8080) +└──────────────┘ └───────────────┘ └─▶ sign (in-process, the SDK's signing account) +``` + +## Run + +```bash +cd example-ui +npm install +npm run dev # http://localhost:5173 +``` + +You also need the gateway running, e.g. via your local Docker. It signs +state-node requests itself (no separate account service). The gateway defaults +to `:3000` and reads: + +``` +MONAS_API_PORT=3000 +MONAS_STATE_NODE_URL=http://127.0.0.1:8080 +MONAS_PERSISTENCE_DIR=... # recommended; otherwise the signing account, CEKs and shares are in-memory +``` + +### Endpoint & CORS + +The browser calls same-origin `/api/*`, and the **Vite dev server proxies it** +to the gateway — so you never hit CORS locally. The target is configurable in +`.env` (copy `.env.example`): + +``` +VITE_GATEWAY_TARGET=http://127.0.0.1:3000 +``` + +You can also repoint the gateway at runtime from the **Settings** dialog (gear +icon) — there are presets for *local (proxied)* and a *public API*. ⚠️ Pointing +at a cross-origin URL directly (not through the proxy) requires that server to +send permissive CORS headers. + +## Tests + +### Isolated UI regressions (no backend required) + +```bash +npm ci --ignore-scripts +npx playwright install chromium # once, if not already installed +npm run test:regression +npm run build +``` + +This suite starts its own Vite on `127.0.0.1:5198` (fails if occupied). It runs +actual App/store/flow/API-adapter paths with HTTP fixtures, intercepts all +backend requests, rejects unknown endpoints and blocks external origins. No +hosted nodes or account keys are needed or modified. Results/traces go to +`/tmp/monas-ui-regression-results`. It covers recipient import → edit → reopen, +owner preview/head checks, revocation reach reporting, and legacy identities. +These are UI regressions, not cryptographic or distributed-protocol tests. + +Legacy identity migration keeps the **last-created signing account**, matching +`POST /account` replacing the gateway's single key. Earlier signing entries +remain available as envelope-decryption keypairs; removing the current account +does not promote them. `activeLabel` from old UI switching cannot change the +backend's key. The account API has no read-current-key endpoint, so a reset or +externally replaced backend key still requires explicit user recovery. + +### Real-stack suites + +```bash +npm test # UI suite (tests/) — ~22s +npm run test:ui # same, in the Playwright UI runner +npm run test:e2e # real-stack journeys (tests-e2e/) — minutes +``` + +Both need a running stack. `npm test` only needs vite + gateway; +`test:e2e` additionally exercises the state-node round trip, so the gateway's +`MONAS_STATE_NODE_URL` must point at a node that is up — a local cluster +(`monas-state-node/scripts/start-local-nodes.sh`) or a hosted node. + +Two suites, deliberately split by what they cost and what they prove: + +| | `tests/` (`npm test`) | `tests-e2e/` (`npm run test:e2e`) | +| --- | --- | --- | +| Proves | the **UI** behaves — every control does what it claims | a fresh user can run every journey against **real nodes** | +| Content mutations | none (seeds `localStorage` directly) | many — create/edit/share/revoke/delete on the network | +| Runtime | ~22s | minutes | + +`tests/` avoids content mutations on purpose: a create is a real crypto + +state-node round trip, so a suite that made one per scenario would take minutes +and mostly re-test the protocol that the journeys already cover. Where a +scenario needs a file to exist, it writes a registry entry into `localStorage` +and reloads. + +`tests-e2e/full-stack.spec.ts` holds three independent journeys: the content +lifecycle (create → preview → verify integrity → verified read → edit → +old-version read → reload → delete), the sharing lifecycle (share to a local +identity with the HPKE round-trip proof, share to a pasted external key, +revoke with envelope reissue), and binary upload + filter views + delete. + +`tests-e2e/cross-device.spec.ts` (J-4) is the two-device share: two browser +contexts, each bound to its **own gateway**, exchange only +what people would paste into a chat — a public key one way, a share package +the other. The recipient unwraps it, reads the shared version back from *his* +state node with the delegated token, is refused after the owner revokes a +third party, reads again with the reissued token, reads the owner's post-share +edit, **writes his own version** with the token (which the owner reads from +*her* node and pulls into her copy), is refused the stale package, and — once +the owner revokes him — has his write refused while her copy keeps his last +authorised version. Point the second gateway at a **different node** so +the reads prove replication too: + +```bash +MONAS_STATE_NODE_URL=https://node2.monas-demo.net ./scripts/second-device.sh # :3001 +``` + +vite proxies `/api2` to it (`VITE_GATEWAY2_TARGET` to override). + +Modal structure is asserted with **ARIA snapshots** (`toMatchAriaSnapshot`) +rather than CSS selectors, so the whole control set of a dialog is checked in +one assertion and the tests survive styling changes. + +### Bugs this suite found (and now guards) + +Writing the suite surfaced two defects, both since fixed. The tests are the +regression guards — reverting either fix makes them fail, which was verified +rather than assumed: + +- **G-34** — with *Paste public key* selected and the field empty, *Wrap CEK & + share* stayed enabled and did nothing: both branches of `submit()` return + early on missing input, with no toast and no validation. The button is now + gated on a `recipientReady` check and the empty field explains why. +- **S-07** — *Test connection* called `saveEndpoints(cfg)` before probing, + because the probe could only read the endpoint back out of storage. Testing + an endpoint therefore committed it, and *Reset to proxy* only resets + component state, so a cancelled edit could not be undone from the dialog. + `probeGateway(base?)` now takes the candidate URL, so probing has no side + effect. + +The plan the suite was generated from lives in `specs/ui-coverage.md` +(49 scenarios); the tests here cover the P0 subset that needs no fixtures. + +### Extending the suite + +The plan and the tests were produced with [Playwright +Agents](https://playwright.dev/docs/test-agents) — a *planner* explores the +running app and writes the plan, a *generator* turns plan entries into specs +while verifying selectors against the live UI, and a *healer* repairs tests +whose locators have drifted. The agent definitions are gitignored (they are +per-developer and must be regenerated when Playwright is updated): + +```bash +npx playwright init-agents --loop=claude # or codex | vscode | opencode +``` + +`tests/seed.spec.ts` is the bootstrap the planner starts from: it clears +`localStorage` and creates the signing account, without which content +operations are refused and most of the UI is unreachable. + +Note that the agents are used at **authoring** time only. What runs in CI is +ordinary, deterministic Playwright code — no model is in the execution loop, +so the suite cannot go non-deterministic on a model update. + +## Gateway / SDK endpoints used + +| Action | Gateway call | SDK model | +| ----------------- | ------------------------------------- | --------------------------------- | +| Create account | `POST /account` | `CreateSigningAccountOutput` | +| New file / Upload | `POST /content` | `CreateContent{Input,Output}` | +| Open / preview | `GET /content/{id}` | `GetContent{Input,Output}` | +| Edit contents | `PUT /content/{id}` | `UpdateContent{Input,Output}` | +| Delete | `DELETE /content/{id}` | `DeleteContent{Input,Output}` | +| Share | `POST /share` | `ShareContent{Input,Output}` | +| Import shared | `POST /share/decrypt` | `DecryptSharedContent{Input,Out}` | +| Revoke | `POST /share/revoke` | `RevokeShare{Input,Output}` | +| Verified read | `POST /state/read` | `ReadContentFromStateNode{In,Out}`| +| (history/version) | `POST /state/history`, `/state/...` | `state` models | + +Notes on the contract: + +- All content/keys are exchanged as **base64url (no padding)** — matching the + SDK models. +- Responses are wrapped in the SDK `ApiResponse` envelope + (`{ success, data, error: { type, message }, trace_id }`); the client unwraps + `data` or throws the typed error. +- `POST /content`, `PUT/DELETE /content/{id}`, `POST /share/revoke` and the + `/state/*` calls require an **`X-Request-Timestamp`** header (the gateway + returns 401 without it). The UI sends the current Unix time; the SDK then + signs the state-node request with its signing account. +- **Two read paths, and they prove different things.** `GET /content/{id}` + reads the gateway's own local store — convenient, but it never touches the + network, so it proves nothing about what the state node holds. + `POST /state/read` fetches the version from the state node (relayed to a + member when the contacted node isn't one) and verifies it: the Node CID is + recomputed, the CEK decrypts it (AES-256-GCM) and the plaintext is + re-addressed to the local id. The preview modal exposes both. + What the verified read does *not* prove is that the version is the newest or + that a legitimate writer produced it — version metadata has no trust anchor + yet (issue #59). +- **Sharing is HPKE Auth mode.** `/share` and `/share/revoke` take the sender's + *private* key (the SDK never stores it) because the wrap mixes it in; + `/share/decrypt` correspondingly takes the sender's **public key**, not a + self-asserted `sender_key_id`. The recipient TOFU-pins that key on the first + envelope for a content and rejects any later envelope that doesn't match. +- **`key_epoch` must be carried through untouched.** Every revoke rotates the + CEK and bumps the epoch, and recipients reject envelopes older than the epoch + they've recorded (rollback replay defence). A revoke therefore also returns + `reissued_envelopes` for the *surviving* recipients — the UI swaps those into + its registry, because a recipient left holding the pre-rotation envelope can + no longer decrypt. + +## Accounts & the signing key + +A device has **one account**. Open the identity chip (top-right) → **Create +account**: the UI sends `POST /account` to the **gateway**, whose SDK generates +and keeps a **P-256** key and returns it. The gateway never creates this key on +its own — until you create the account, content operations are refused. The SDK signs +every state-node request with that key (create / edit / delete, and a +recipient's reads and writes under a delegated token), and it is the key +other people share *to* — a delegated token's audience is the recipient's +signing key, so a share addressed to any other key could open its envelope but +never read or write the state node. + +The gateway holds exactly one key, which is why the dialog does not offer a +second account or a keypair-only identity: creating another would overwrite +the key the gateway signs with and silently orphan the first. To start over, +remove the account and create a new one (content created under the old key can +then no longer be updated or deleted from this device). + +## Is my copy the newest? (sync status) + +Every synced row carries a sync badge, and the preview repeats it as a one-line +status: + +| Badge | Meaning | +| -------------------- | ----------------------------------------------------------------------- | +| `up to date` | the Content Network head is the version this device holds | +| `newer on network` | someone else wrote after this device's last save/import | +| `synced` | on a Content Network, head not compared yet | +| `can't reach network`| the last check failed (node down, token voided, …); hover for the error | + +The comparison is a **verified read** of the head (`POST /state/read` with +`accept_any_version`): the plaintext is re-derived and re-addressed, and the +resulting plain id is compared with the one this device holds — the owner's +local version, or for a recipient the version it last wrote (else the one the +envelope carried). It runs when a file is opened, on *Check now* / *Read from +state-node*, and in a background sweep every 30 s. When behind, the owner's +*Pull & edit* adopts the head into the local copy first (`POST /state/pull`); +a write-share recipient's *Edit contents* already starts from the head. + +*Verify integrity* is related but narrower: it byte-compares the ciphertext +this gateway stored with the head's. "Not the head" there is the same +*newer on network* condition, not a corruption; the reason string from the SDK +is shown under the badge. + +## Sharing with someone on another device + +Monas does not carry key envelopes between people — that is deliberately left +to whatever channel the two of you already have. The UI makes both ends a +copy-paste: + +1. **Recipient**: identity chip → **Copy public key** on the identity you want + to receive with, and send it to the owner. +2. **Owner**: row menu → **Share** → *Paste public key* → **Wrap CEK & share**. + The dialog shows the **share package** for that recipient (also **Copy + package** on the row). Send it over chat, mail, anything. +3. **Recipient**: sidebar **Import shared** → paste → **Unwrap & add to my + Drive**. The gateway unwraps the content key with your identity (HPKE Auth, + so it also proves the package came from that sender and pins their key) + and decrypts. The file appears with a *shared with me* badge; **Open** + unwraps it again from the kept envelope; with a write share, **Edit + contents** writes back to the owner's Content Network (below). + +The package is one JSON document (`kind: "monas-share"`, `v: 1`): the file's +name/type/size, the owner's content id and Content Network id, both public +keys, the recipient KeyId, permissions, the `KeyEnvelope` and the delegated +token. It is not secret — the key inside is wrapped to the recipient only — +but it is a capability, so treat it like a link to the file. + +**Reading from the state node as a recipient.** The package also carries a +delegated token (JWT, one hour, issued for the Content Network id). In the +preview of a received file the state-node panel — latest version, history, +**Read from state-node** — works with that token: the gateway signs the +request with your account key (the token's audience) and presents the token, +and the state node checks both. So the identity you receive with must be your +signing account. The verified read uses the shared version's id only to pick +the CEK and reports the id the plaintext actually addresses to, so it follows +the owner's edits: after they edit, *Read from state-node* shows the new +version and flags it as newer than shared. + +A revoke of *any* recipient voids every token issued before it, yours +included; the owner's dialog then shows a re-wrapped package with a fresh +token — import it and reads work again (the old envelope still opens the +version it carried). A pre-rotation package is refused as stale on import: +the SDK keeps the sender pin (sender key, key epoch, CEK) under the Content +Network id, so the check holds across the owner's edits even though those +change the content id the package names. + +**Writing as a recipient.** With a read+write share the row menu of a received +file offers **Edit contents**. The editor loads the owner's newest version +from the state node (not the one the envelope carried), and *Re-encrypt & +save* goes to `PUT /api/share/content/:networkId` with the delegated token: +the gateway encrypts under the CEK it pinned at import — it has no content +record of its own — signs with your account key and PUTs to the owner's +Content Network. The state node grants the write on the token's `write` +capability and refuses it after a revoke. In the preview, *Read from +state-node* then reports the version as *your edit*. + +The owner sees it the other way round: their local copy is now behind the +head, so their verified read flags it as *newer than your copy*. **Edit +contents** on the owner's side first **pulls** the head into the local +record (`POST /api/state/pull`: a verified read whose ciphertext is adopted as +the newest local version, keeping integrity checks true), so the edit starts +from the recipient's version rather than overwriting it. A revoke pulls the +same way inside the SDK before rotating the key — otherwise re-encrypting the +stale local plaintext would silently roll the file back. If that pull fails +the revoke still goes through (a writer must not be able to block +revocation) and the UI says so. + +## File size limit + +The UI refuses file bodies over **64 KB** (`MAX_FILE_BYTES` in +`src/config.ts`) on every path that sends one — New file, Upload, Edit, and a +recipient's edit — before anything is encrypted or sent to the gateway. + +Why: a state node sends a content's **whole version history** to the other +members in one peer request on create, revoke and delete. A request much past +~256 KiB is dropped by the peer connection (measured; the exact cause inside +libp2p is not pinned down). Every revoke and every edit adds another full copy +of the body to that history. + +Measured on the hosted 4-node demo (create → share → revoke → re-share → +delete from the UI): + +| Body | Create | Revoke 403 / delete 410 seen by the recipient | Immediate push to other members | +|---|---|---|---| +| 32 KB, 48 KB | ok | ok | ok | +| 64 KB | ok | ok | the delete push failed to 2 members | + +When the immediate push fails the operation still succeeds on the node that +took it, and the other members catch up on the next periodic sync (~30 s). +Until then a write sent through one of those members can still be accepted. +The same happens below 64 KB once a file has accumulated enough versions. + +Raising the limit needs the state node to stop sending the whole history in +one request (send only what the peer lacks, or chunk it); the UI limit is the +stop-gap until then. + +## What's real vs. illustrative + +A single gateway call does the whole orchestration server-side, so the Protocol +activity panel pairs **one real call** per action with **illustrative phases** +that narrate the protocol and read ids out of the response: + +- **Real:** the labelled "· gateway call" step in each run (and the share + unwrap+decrypt proof). Errors are shown verbatim with their SDK type. +- **Illustrative:** CEK generation, CID addressing, member selection, token + issuance, etc. — these happen *inside* the SDK call; the panel narrates them + with a short minimum duration for readability. + +## Notes + +- **No folders.** Monas has no folder concept, so the UI lists files flat + rather than invent one that would exist only in this browser. (Registries + saved by older builds with folders load flat; the folder rows are dropped.) +- The gateway has no listing API, so the UI keeps its own file registry in + `localStorage` (`monas.registry.v3`). Identities and the endpoint live there + too. Clearing site data resets the demo. +- **Rename** a file through the name field in `Edit contents`; the new name + reaches the SDK with that update. +- Private keys for demo identities are stored in `localStorage` so the HPKE + round-trip proof can run — fine for a local demo, not for production. + +## Project layout + +``` +src/ + api/ gateway client (account=keypair, content, share, stateNode=state) + base64url helpers + pipeline/ per-action flow definitions + sequential runner → drives the activity panel + store/ localStorage-backed registry + identities (React via useSyncExternalStore) + components/ TopBar, Sidebar, FileBrowser, PipelinePanel, modals, icons, toasts + config.ts single gateway endpoint (proxy default + presets) + App.tsx wiring: actions → pipeline → registry updates +``` diff --git a/example-ui/index.html b/example-ui/index.html new file mode 100644 index 0000000..9b9e297 --- /dev/null +++ b/example-ui/index.html @@ -0,0 +1,13 @@ + + + + + + + Monas Drive — example UI + + +
+ + + diff --git a/example-ui/package-lock.json b/example-ui/package-lock.json new file mode 100644 index 0000000..a08b210 --- /dev/null +++ b/example-ui/package-lock.json @@ -0,0 +1,1812 @@ +{ + "name": "monas-example-ui", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "monas-example-ui", + "version": "0.1.0", + "dependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@playwright/test": "^1.61.1", + "@types/node": "^20.16.0", + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "@vitejs/plugin-react": "^4.3.3", + "playwright": "^1.61.1", + "typescript": "^5.6.3", + "vite": "^5.4.10" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.3.tgz", + "integrity": "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz", + "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@playwright/test": { + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.61.1.tgz", + "integrity": "sha512-8nKv6+0RJSL9FE4jYOEGXnPeM/Hg12qZpmqzZjRh3qM0Y7c3z1mrOTfFLids72RDQYVh9WpLEfR5WdpNX4fkig==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.61.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.4.tgz", + "integrity": "sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.4.tgz", + "integrity": "sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.4.tgz", + "integrity": "sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.4.tgz", + "integrity": "sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.4.tgz", + "integrity": "sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.4.tgz", + "integrity": "sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.4.tgz", + "integrity": "sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.4.tgz", + "integrity": "sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.4.tgz", + "integrity": "sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.4.tgz", + "integrity": "sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.4.tgz", + "integrity": "sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.4.tgz", + "integrity": "sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.4.tgz", + "integrity": "sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.4.tgz", + "integrity": "sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.4.tgz", + "integrity": "sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.4.tgz", + "integrity": "sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.4.tgz", + "integrity": "sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.4.tgz", + "integrity": "sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.4.tgz", + "integrity": "sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.4.tgz", + "integrity": "sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.4.tgz", + "integrity": "sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.4.tgz", + "integrity": "sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.4.tgz", + "integrity": "sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.4.tgz", + "integrity": "sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.4.tgz", + "integrity": "sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.41", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.41.tgz", + "integrity": "sha512-ECymXOukMnOoVkC2bb1Vc/w/836DXncOg5m8Xj1RH7xSHZJWNYY6Zh7EH477vcnD5egKNNfy2RpNOmuChhFPgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.29", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.29.tgz", + "integrity": "sha512-ch0qJdr2JY0r04NXSprbK6TXOgnaJ1Tz23fm5W+z0/CBah6BSBc3n96h7K9GOtwh0HrilNWHIBzE1Ko4Dcw/Wg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.31", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.31.tgz", + "integrity": "sha512-MujYO3eP72uvmSE0i4wltsodRfIpZATP3jvzRNRGGxgzId7aVocVJJV3nf01qnzzKFGxQVC9bpWxl5cjxTr/7Q==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001793", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001793.tgz", + "integrity": "sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.360", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.360.tgz", + "integrity": "sha512-GkcBt6YYAw9SxFWn+xVar4cLVGlXVuswwtRLBozi2zp0GjXs4ZnOrqV4zbXzg35n7w81hCkyJNYicgXlVHAmBA==", + "dev": true, + "license": "ISC" + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.44", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.44.tgz", + "integrity": "sha512-5WUyunoPMsvvEhS8AxHtRzP+oA8UCkJ7YRxatWKjngndhDGLiqEVAQKWjFAiAiuL8zMRGzGSJxFnLetoa43qGQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/playwright": { + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.1.tgz", + "integrity": "sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.61.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.1.tgz", + "integrity": "sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.4.tgz", + "integrity": "sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.4", + "@rollup/rollup-android-arm64": "4.60.4", + "@rollup/rollup-darwin-arm64": "4.60.4", + "@rollup/rollup-darwin-x64": "4.60.4", + "@rollup/rollup-freebsd-arm64": "4.60.4", + "@rollup/rollup-freebsd-x64": "4.60.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.4", + "@rollup/rollup-linux-arm-musleabihf": "4.60.4", + "@rollup/rollup-linux-arm64-gnu": "4.60.4", + "@rollup/rollup-linux-arm64-musl": "4.60.4", + "@rollup/rollup-linux-loong64-gnu": "4.60.4", + "@rollup/rollup-linux-loong64-musl": "4.60.4", + "@rollup/rollup-linux-ppc64-gnu": "4.60.4", + "@rollup/rollup-linux-ppc64-musl": "4.60.4", + "@rollup/rollup-linux-riscv64-gnu": "4.60.4", + "@rollup/rollup-linux-riscv64-musl": "4.60.4", + "@rollup/rollup-linux-s390x-gnu": "4.60.4", + "@rollup/rollup-linux-x64-gnu": "4.60.4", + "@rollup/rollup-linux-x64-musl": "4.60.4", + "@rollup/rollup-openbsd-x64": "4.60.4", + "@rollup/rollup-openharmony-arm64": "4.60.4", + "@rollup/rollup-win32-arm64-msvc": "4.60.4", + "@rollup/rollup-win32-ia32-msvc": "4.60.4", + "@rollup/rollup-win32-x64-gnu": "4.60.4", + "@rollup/rollup-win32-x64-msvc": "4.60.4", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + } + } +} diff --git a/example-ui/package.json b/example-ui/package.json new file mode 100644 index 0000000..f3cd68c --- /dev/null +++ b/example-ui/package.json @@ -0,0 +1,30 @@ +{ + "name": "monas-example-ui", + "private": true, + "version": "0.1.0", + "type": "module", + "description": "Minimal Drive-like example UI for the Monas protocol (encrypted, content-addressed, state-node synced file sharing).", + "scripts": { + "dev": "vite", + "build": "tsc --noEmit && vite build", + "preview": "vite preview", + "test": "playwright test", + "test:ui": "playwright test --ui", + "test:regression": "playwright test -c playwright.regression.config.ts", + "test:e2e": "playwright test -c playwright.e2e.config.ts" + }, + "dependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@playwright/test": "^1.61.1", + "@types/node": "^20.16.0", + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "@vitejs/plugin-react": "^4.3.3", + "playwright": "^1.61.1", + "typescript": "^5.6.3", + "vite": "^5.4.10" + } +} diff --git a/example-ui/playwright.config.ts b/example-ui/playwright.config.ts new file mode 100644 index 0000000..2b0d5d9 --- /dev/null +++ b/example-ui/playwright.config.ts @@ -0,0 +1,24 @@ +import { defineConfig, devices } from "@playwright/test"; + +// The UI is only meaningful against a running stack (vite → monas-gateway → +// a 4-node state-node cluster), so there is no webServer block here: the +// stack is started out of band and these tests attach to it. See README. +export default defineConfig({ + testDir: "./tests", + // The protocol work behind a single click (encrypt → CID → state-node + // round-trip across 4 nodes) is genuinely slow; the defaults are far too + // tight and would report protocol latency as a test failure. + timeout: 180_000, + expect: { timeout: 30_000 }, + // These tests share one gateway and one localStorage-backed registry, so + // they cannot run concurrently against each other. + workers: 1, + fullyParallel: false, + reporter: [["list"]], + use: { + baseURL: process.env.E2E_URL || "http://localhost:5174", + trace: "retain-on-failure", + screenshot: "only-on-failure", + }, + projects: [{ name: "chromium", use: { ...devices["Desktop Chrome"] } }], +}); diff --git a/example-ui/playwright.e2e.config.ts b/example-ui/playwright.e2e.config.ts new file mode 100644 index 0000000..1a1cb92 --- /dev/null +++ b/example-ui/playwright.e2e.config.ts @@ -0,0 +1,24 @@ +import { defineConfig, devices } from "@playwright/test"; + +// Real-stack journeys (tests-e2e/): every test mutates content on the live +// state-node network through the local gateway, so this config is split from +// the cheap UI suite (playwright.config.ts) and run on demand: +// npm run test:e2e +// The stack is started out of band exactly as for `npm test` — the only extra +// requirement is that the gateway's MONAS_STATE_NODE_URL points at a node that +// is actually up. +export default defineConfig({ + testDir: "./tests-e2e", + // A journey chains many protocol round trips; give each test real headroom. + timeout: 600_000, + expect: { timeout: 30_000 }, + workers: 1, + fullyParallel: false, + reporter: [["list"]], + use: { + baseURL: process.env.E2E_URL || "http://localhost:5174", + trace: "retain-on-failure", + screenshot: "only-on-failure", + }, + projects: [{ name: "chromium", use: { ...devices["Desktop Chrome"] } }], +}); diff --git a/example-ui/playwright.regression.config.ts b/example-ui/playwright.regression.config.ts new file mode 100644 index 0000000..5911cb1 --- /dev/null +++ b/example-ui/playwright.regression.config.ts @@ -0,0 +1,18 @@ +import { defineConfig, devices } from "@playwright/test"; + +// No running gateway/account/state nodes required. Never reuse a live UI server. +export default defineConfig({ + testDir: "./tests-regression", + timeout: 30_000, + expect: { timeout: 4_000 }, + workers: 1, + reporter: "list", + outputDir: "/tmp/monas-ui-regression-results", + use: { baseURL: "http://127.0.0.1:5198", serviceWorkers: "block", trace: "retain-on-failure" }, + webServer: { + command: "npx vite --host 127.0.0.1 --port 5198 --strictPort", + url: "http://127.0.0.1:5198", + reuseExistingServer: false, + }, + projects: [{ name: "chromium", use: { ...devices["Desktop Chrome"] } }], +}); diff --git a/example-ui/public/monas.svg b/example-ui/public/monas.svg new file mode 100644 index 0000000..9ddc9aa --- /dev/null +++ b/example-ui/public/monas.svg @@ -0,0 +1,4 @@ + + + + diff --git a/example-ui/scripts/second-device.sh b/example-ui/scripts/second-device.sh new file mode 100755 index 0000000..a869477 --- /dev/null +++ b/example-ui/scripts/second-device.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +# Run a second, independent gateway on :3001 — "another person's device" for +# the cross-device share journey (J-4). +# +# Independence is the point: it has its own persistence dir (signing account, +# CEK store, sender pins, shares), so nothing the first device knows leaks +# into the second. Both talk to the state-node network. +# +# MONAS_STATE_NODE_URL=https://node2.monas-demo.net ./scripts/second-device.sh +# +# Then in the browser context that plays the second device, set the endpoint +# (Settings, or localStorage "monas.endpoints.v2") to {"gateway":"/api2"} — +# vite proxies it to this gateway. Create the account from that context. +# Ctrl-C stops it. +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +STATE_NODE="${MONAS_STATE_NODE_URL:?set MONAS_STATE_NODE_URL to a state node}" +PERSIST="${MONAS_PERSISTENCE_DIR2:-$(mktemp -d "${TMPDIR:-/tmp}/monas-device2.XXXXXX")}" +GW_PORT="${MONAS_API_PORT2:-3001}" + +echo "second device: gateway :$GW_PORT, persistence $PERSIST" + +MONAS_API_PORT="$GW_PORT" \ +MONAS_STATE_NODE_URL="$STATE_NODE" \ +MONAS_PERSISTENCE_DIR="$PERSIST" \ + exec "$ROOT/target/debug/monas-gateway" diff --git a/example-ui/specs/README.md b/example-ui/specs/README.md new file mode 100644 index 0000000..48a788b --- /dev/null +++ b/example-ui/specs/README.md @@ -0,0 +1,3 @@ +# Specs + +This is a directory for test plans. diff --git a/example-ui/specs/ui-coverage.md b/example-ui/specs/ui-coverage.md new file mode 100644 index 0000000..2205376 --- /dev/null +++ b/example-ui/specs/ui-coverage.md @@ -0,0 +1,580 @@ +# Monas Drive example UI — coverage test plan + +Playwright scenarios for the interactive surface that the real-stack journeys +(`tests-e2e/full-stack.spec.ts`) do **not** need to touch. The journeys walk +the protocol paths (create account → file → preview → verify → edit → +share → revoke → delete) against real nodes; the scenarios below cover the +rest of the interactive surface cheaply, without content mutations. + +## Preconditions & house rules + +- The stack must be running: vite `:5174`, gateway `127.0.0.1:3000`; for + `tests-e2e/` the gateway's + `MONAS_STATE_NODE_URL` must point at a live state node. +- `tests/seed.spec.ts` runs first. It clears `localStorage`, waits for the + gateway health dot, and creates the **signing account** `agent-main`. + Without a signing account every content operation is refused (see S-14). +- The Drive registry lives in `localStorage`; assume a blank registry unless a + scenario seeds one. +- **Content mutations are expensive** (~1.5–3s, sometimes far longer: real + crypto + a 4-node state-node round trip). Scenarios are ordered so that the + cheap, pure-UI ones run first and the few that need a real file **share one + fixture file** rather than each creating their own. Use + `timeout: 180_000` / `expect.timeout: 30_000` from `playwright.config.ts`. +- Prefer asserting on **observable UI state** (`.on`, `.active`, `.badge`, + toast text, `localStorage`) over screenshots. + +### Shared fixture + +Scenarios marked **[fixture]** reuse one file created once per file: + +``` +name: probe.txt +contents: "probe content v1" +created: at "/" with a signing account present +``` + +Create it once in a `test.beforeAll` / serial-mode first test and let the later +scenarios in that file operate on it. Do **not** create a file per scenario. + +--- + +## Accessible names observed in the running app + +Recorded from the live accessibility tree — use these for ARIA/role assertions. +Several controls are **icon-only with no accessible name**; those are flagged +and must be located by `title`, CSS class, or fixed with an `aria-label` +(see "Accessibility defects" at the end). + +**TopBar** +| Control | Role / name | +|---|---| +| Settings | `button "Settings · endpoint"` (name comes from `title`) | +| Account chip | `button` — **no accessible name**; children render `"No identity"` / `"click to create"`, or the identity label | +| Gateway health | `generic "Gateway health (monas-gateway → SDK)"` wrapping `generic "gateway"`; **state lives only in the CSS class** `.dot.up` / `.dot.down` / `.dot` | + +**Sidebar** — all `role="button"`, `tabIndex=0` `div.nav-item` +`button "New file"`, `button "Upload"`, `button "Import shared"`, +`button "My Drive"`, `button "Encrypted files"`, `button "On state-node"`, +`button "Shared"`. Active view = `.nav-item.active`. Counts render inside a +`span.mono` (text reads e.g. `"Encrypted files0"`). + +**PipelinePanel** +Expanded: `generic "Protocol activity"`, `button "Collapse"` (name from `title`), +`button "Clear"` (only when `runs.length > 0`). +Collapsed: `aside.pipeline.collapsed` with a single +`button "Show protocol activity"` (name from `title`, **no text/aria-label**). + +**SettingsModal** (`Endpoint`) +`heading "Endpoint"`, `textbox` (value `/api`) labelled +`"monas-gateway base URL"`, +`button "Local (Vite proxy → Docker)"`, `button "Local (direct :3000)"`, +`button "Reset to proxy"`, `button "Test connection"`, `button "Save"`. +Labels are **siblings, not `for`-associated** — `getByLabel` will not work. + +**ShareModal** +`heading "Share “”"`, seg 1 = `button "Pick identity"` / +`button "Paste public key"`, seg 2 (permission) = `button "read"` / +`button "read + write"`, `select` of `"