diff --git a/documentation/invite-join-drift-analysis.md b/documentation/invite-join-drift-analysis.md new file mode 100644 index 00000000..55286cd2 --- /dev/null +++ b/documentation/invite-join-drift-analysis.md @@ -0,0 +1,110 @@ +# Invite Join Failure Analysis: Ratchet Tree Consistency Drift + +## Summary + +During dogfooding in mixed-client environments, some invitees failed to join groups after receiving valid welcome events. + +Observed user-facing error: + +```text +Failed to join group with any matching key package. Last error: non-blank intermediate node must list leaf node in its unmerged_leaves +``` + +Root cause is most consistent with **ratchet tree consistency drift** (fork/divergence) across implementations, not invitation transport failure. + +## Context + +- Marmot protocol flow: key package published -> admin invites -> commit produced -> welcome delivered -> invitee joins. +- In noisy environments with mixed implementations and aggressive self-updates, state can diverge between participants. +- Divergence later surfaces at join-time as strict ratchet tree validation failure. + +## Failure Surface + +At `marmot-ts` level: + +- `joinGroupFromWelcome()` retries matching key packages and rethrows the last error. +- The reported invariant violation is thrown inside `ts-mls` ratchet tree validation. + +Interpretation: + +- Join/decryption path advanced far enough to parse and validate GroupInfo/tree. +- Failure indicates structural inconsistency in the welcome ratchet tree view (especially `unmerged_leaves` coherence along direct paths). + +## Why Mixed Implementations Trigger This + +Likely contributing factors: + +1. Aggressive self-update timing around invite/join windows. +2. Concurrent membership-changing commits from multiple admins. +3. Out-of-order or delayed ingestion causing stale base state when producing commits/welcomes. +4. Different implementation behavior around proposal bundling and commit sequencing. +5. Divergent handling of race windows after add/commit/welcome publication. + +These conditions can produce a welcome that appears valid in transport terms but references a tree state that fails strict invariants on the joiner. + +## Recommendations (Actionable, Future Iteration) + +### 1) Add Structured Interop Error Reporting + +- Introduce typed join errors (e.g., `WELCOME_TREE_INCONSISTENT`) with metadata: + - groupId (if available) + - epoch + - signer index + - candidate key package ref + - underlying validation code/message +- Preserve strict fail behavior (no bypass of MLS validation). + +### 2) Add Pre-Join / Join Diagnostics Logs + +Capture for each join attempt: + +- welcome id, ciphersuite, number of secrets +- attempted key package refs and whether each matched a secret slot +- exact inner error + stack for each failed attempt +- first detected invariant mismatch context if available + +### 3) Add Commit-Side Guardrails + +- Optional interop-safe mode: + - serialize membership-changing commits + - throttle self-updates near add/join windows + - avoid parallel admin add bursts + +### 4) Add Drift/Fork Detection Before New Commits + +- Compare local `(groupId, epoch, treeHash)` against recent network-observed state before sending another commit. +- If divergent, block commit and require reconciliation path. + +### 5) Add Welcome Quality Gates + +- Before sending welcome gift-wrap: + - run local consistency diagnostics + - verify tree extension presence/shape expectations +- On suspicious state, fail closed and request resync/retry. + +### 6) Add Quarantine Policy for Known-Bad Peers + +- If repeated invalid-tree failures are associated with a client fingerprint/version, quarantine invites/commits from that source until manual recovery. + +### 7) Expand Interop Stress Tests + +Add deterministic tests for: + +- aggressive self-update after join +- concurrent admin commits with delayed ingestion +- mixed-version/mixed-behavior clients +- repeated add/remove churn with welcomes + +Goal: reproduce drift signatures before release. + +## Non-Goal + +Do **not** weaken MLS tree validation to “accept” inconsistent state. Robustness should come from earlier detection, better policies, and clearer recovery paths. + +## Proposed Follow-Up Work Items + +1. Implement typed join error taxonomy. +2. Add debug-level structured logs in join and invite commit paths. +3. Add interop-safe commit policy mode. +4. Add drift preflight checks before commit creation. +5. Add new interop race tests based on this incident. diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 92c34e54..299c8f67 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,7 +9,7 @@ importers: dependencies: "@hpke/core": specifier: ^1.8.0 - version: 1.8.0 + version: 1.9.0 "@noble/ciphers": specifier: ^2.1.1 version: 2.1.1 @@ -24,10 +24,10 @@ importers: version: 2.0.0 applesauce-common: specifier: ^5.1.0 - version: 5.1.0(typescript@5.9.3) + version: 5.2.0(typescript@5.9.3) applesauce-core: specifier: ^5.1.0 - version: 5.1.0(typescript@5.9.3) + version: 5.2.0(typescript@5.9.3) debug: specifier: ^4.4.3 version: 4.4.3 @@ -43,16 +43,16 @@ importers: devDependencies: "@changesets/cli": specifier: ^2.29.8 - version: 2.29.8(@types/node@24.11.0) + version: 2.30.0(@types/node@24.12.0) "@types/debug": specifier: ^4.1.12 - version: 4.1.12 + version: 4.1.13 "@types/node": specifier: ^24.11.0 - version: 24.11.0 + version: 24.12.0 applesauce-accounts: specifier: ^5.1.0 - version: 5.1.0(typescript@5.9.3) + version: 5.2.0(@capacitor/core@7.6.0)(typescript@5.9.3) husky: specifier: ^9.1.7 version: 9.1.7 @@ -67,7 +67,7 @@ importers: version: 5.9.3 vitest: specifier: ^3.2.4 - version: 3.2.4(@types/debug@4.1.12)(@types/node@24.11.0)(jiti@2.6.1)(lightningcss@1.30.2) + version: 3.2.4(@types/debug@4.1.13)(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.32.0) examples: dependencies: @@ -79,31 +79,31 @@ importers: version: 2.0.1 "@tailwindcss/vite": specifier: ^4.1.18 - version: 4.1.18(vite@7.3.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.30.2)) + version: 4.2.2(vite@7.3.1(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.32.0)) applesauce-accounts: specifier: ^5.1.0 - version: 5.1.0(typescript@5.9.3) + version: 5.2.0(@capacitor/core@7.6.0)(typescript@5.9.3) applesauce-common: specifier: ^5.1.0 - version: 5.1.0(typescript@5.9.3) + version: 5.2.0(typescript@5.9.3) applesauce-core: specifier: ^5.1.0 - version: 5.1.0(typescript@5.9.3) + version: 5.2.0(typescript@5.9.3) applesauce-extra: specifier: ^5.0.0 - version: 5.0.0(typescript@5.9.3) + version: 5.0.0(@capacitor/core@7.6.0)(typescript@5.9.3) applesauce-loaders: specifier: ^5.1.0 version: 5.1.0(typescript@5.9.3) applesauce-relay: specifier: ^5.1.0 - version: 5.1.0(typescript@5.9.3) + version: 5.2.0(typescript@5.9.3) applesauce-signers: specifier: ^5.1.0 - version: 5.1.0(typescript@5.9.3) + version: 5.2.0(@capacitor/core@7.6.0)(typescript@5.9.3) daisyui: specifier: ^5.5.14 - version: 5.5.18 + version: 5.5.19 localforage: specifier: ^1.10.0 version: 1.10.0 @@ -121,14 +121,14 @@ importers: version: 7.8.2 tailwindcss: specifier: ^4.1.18 - version: 4.1.18 + version: 4.2.2 unique-names-generator: specifier: ^4.7.1 version: 4.7.1 devDependencies: "@types/node": specifier: ^24.10.4 - version: 24.10.13 + version: 24.12.0 "@types/react": specifier: ^19.2.7 version: 19.2.14 @@ -137,13 +137,13 @@ importers: version: 19.2.3(@types/react@19.2.14) "@vitejs/plugin-react": specifier: ^4.7.0 - version: 4.7.0(vite@7.3.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.30.2)) + version: 4.7.0(vite@7.3.1(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.32.0)) typescript: specifier: ~5.9.3 version: 5.9.3 vite: specifier: ^7.3.0 - version: 7.3.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.30.2) + version: 7.3.1(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.32.0) packages: "@babel/code-frame@7.29.0": @@ -232,17 +232,17 @@ packages: } engines: { node: ">=6.9.0" } - "@babel/helpers@7.28.6": + "@babel/helpers@7.29.2": resolution: { - integrity: sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==, + integrity: sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==, } engines: { node: ">=6.9.0" } - "@babel/parser@7.29.0": + "@babel/parser@7.29.2": resolution: { - integrity: sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==, + integrity: sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==, } engines: { node: ">=6.0.0" } hasBin: true @@ -265,10 +265,10 @@ packages: peerDependencies: "@babel/core": ^7.0.0-0 - "@babel/runtime@7.28.6": + "@babel/runtime@7.29.2": resolution: { - integrity: sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==, + integrity: sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==, } engines: { node: ">=6.9.0" } @@ -293,10 +293,16 @@ packages: } engines: { node: ">=6.9.0" } - "@changesets/apply-release-plan@7.0.14": + "@capacitor/core@7.6.0": resolution: { - integrity: sha512-ddBvf9PHdy2YY0OUiEl3TV78mH9sckndJR14QAt87KLEbIov81XO0q0QAmvooBxXlqRRP8I9B7XOzZwQG7JkWA==, + integrity: sha512-K6LEvgxW6Nd6uNWHj3U512hUtB4kln71TvVgdvwJRFMLySpUrWwb/A8Q7c3QGogvPvgOjN1U65onkyRTihPGwA==, + } + + "@changesets/apply-release-plan@7.1.0": + resolution: + { + integrity: sha512-yq8ML3YS7koKQ/9bk1PqO0HMzApIFNwjlwCnwFEXMzNe8NpzeeYYKCmnhWJGkN8g7E51MnWaSbqRcTcdIxUgnQ==, } "@changesets/assemble-release-plan@6.0.9": @@ -311,17 +317,17 @@ packages: integrity: sha512-x/xEleCFLH28c3bQeQIyeZf8lFXyDFVn1SgcBiR2Tw/r4IAWlk1fzxCEZ6NxQAjF2Nwtczoen3OA2qR+UawQ8Q==, } - "@changesets/cli@2.29.8": + "@changesets/cli@2.30.0": resolution: { - integrity: sha512-1weuGZpP63YWUYjay/E84qqwcnt5yJMM0tep10Up7Q5cS/DGe2IZ0Uj3HNMxGhCINZuR7aO9WBMdKnPit5ZDPA==, + integrity: sha512-5D3Nk2JPqMI1wK25pEymeWRSlSMdo5QOGlyfrKg0AOufrUcjEE3RQgaCpHoBiM31CSNrtSgdJ0U6zL1rLDDfBA==, } hasBin: true - "@changesets/config@3.1.2": + "@changesets/config@3.1.3": resolution: { - integrity: sha512-CYiRhA4bWKemdYi/uwImjPxqWNpqGPNbEBdX1BdONALFIDK7MCUj6FPkzD+z9gJcvDFUQJn9aDVf4UG7OT6Kog==, + integrity: sha512-vnXjcey8YgBn2L1OPWd3ORs0bGC4LoYcK/ubpgvzNVr53JXV5GiTVj7fWdMRsoKUH7hhhMAQnsJUqLr21EncNw==, } "@changesets/errors@0.2.0": @@ -336,10 +342,10 @@ packages: integrity: sha512-gphr+v0mv2I3Oxt19VdWRRUxq3sseyUpX9DaHpTUmLj92Y10AGy+XOtV+kbM6L/fDcpx7/ISDFK6T8A/P3lOdQ==, } - "@changesets/get-release-plan@4.0.14": + "@changesets/get-release-plan@4.0.15": resolution: { - integrity: sha512-yjZMHpUHgl4Xl5gRlolVuxDkm4HgSJqT93Ri1Uz8kGrQb+5iJ8dkXJ20M2j/Y4iV5QzS2c5SeTxVSKX+2eMI0g==, + integrity: sha512-Q04ZaRPuEVZtA+auOYgFaVQQSA98dXiVe/yFaZfY7hoSmQICHGvP0TF4u3EDNHWmmCS4ekA/XSpKlSM2PyTS2g==, } "@changesets/get-version-range-type@0.4.0": @@ -360,10 +366,10 @@ packages: integrity: sha512-OQtR36ZlnuTxKqoW4Sv6x5YIhOmClRd5pWsjZsddYxpWs517R0HkyiefQPIytCVh4ZcC5x9XaG8KTdd5iRQUfg==, } - "@changesets/parse@0.4.2": + "@changesets/parse@0.4.3": resolution: { - integrity: sha512-Uo5MC5mfg4OM0jU3up66fmSn6/NE9INK+8/Vn/7sMVcdWg46zfbvvUSjD9EMonVqPi9fbrJH9SXHn48Tr1f2yA==, + integrity: sha512-ZDmNc53+dXdWEv7fqIUSgRQOLYoUom5Z40gmLgmATmYR9NbL6FJJHwakcCpzaeCy+1D0m0n7mT4jj2B/MQPl7A==, } "@changesets/pre@2.0.2": @@ -372,10 +378,10 @@ packages: integrity: sha512-HaL/gEyFVvkf9KFg6484wR9s0qjAXlZ8qWPDkTyKF6+zqjBe/I2mygg3MbpZ++hdi0ToqNUF8cjj7fBy0dg8Ug==, } - "@changesets/read@0.6.6": + "@changesets/read@0.6.7": resolution: { - integrity: sha512-P5QaN9hJSQQKJShzzpBT13FzOSPyHbqdoIBUd2DJdgvnECCyO6LmAOWSV+O8se2TaZJVwSXjL+v9yhb+a9JeJg==, + integrity: sha512-D1G4AUYGrBEk8vj8MGwf75k9GpN6XL3wg8i42P2jZZwFLXnlr2Pn7r9yuQNbaMCarP7ZQWNJbV6XLeysAIMhTA==, } "@changesets/should-skip-package@0.1.2": @@ -402,251 +408,244 @@ packages: integrity: sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q==, } - "@esbuild/aix-ppc64@0.27.3": + "@esbuild/aix-ppc64@0.27.4": resolution: { - integrity: sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==, + integrity: sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==, } engines: { node: ">=18" } cpu: [ppc64] os: [aix] - "@esbuild/android-arm64@0.27.3": + "@esbuild/android-arm64@0.27.4": resolution: { - integrity: sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==, + integrity: sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==, } engines: { node: ">=18" } cpu: [arm64] os: [android] - "@esbuild/android-arm@0.27.3": + "@esbuild/android-arm@0.27.4": resolution: { - integrity: sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==, + integrity: sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==, } engines: { node: ">=18" } cpu: [arm] os: [android] - "@esbuild/android-x64@0.27.3": + "@esbuild/android-x64@0.27.4": resolution: { - integrity: sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==, + integrity: sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==, } engines: { node: ">=18" } cpu: [x64] os: [android] - "@esbuild/darwin-arm64@0.27.3": + "@esbuild/darwin-arm64@0.27.4": resolution: { - integrity: sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==, + integrity: sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==, } engines: { node: ">=18" } cpu: [arm64] os: [darwin] - "@esbuild/darwin-x64@0.27.3": + "@esbuild/darwin-x64@0.27.4": resolution: { - integrity: sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==, + integrity: sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==, } engines: { node: ">=18" } cpu: [x64] os: [darwin] - "@esbuild/freebsd-arm64@0.27.3": + "@esbuild/freebsd-arm64@0.27.4": resolution: { - integrity: sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==, + integrity: sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==, } engines: { node: ">=18" } cpu: [arm64] os: [freebsd] - "@esbuild/freebsd-x64@0.27.3": + "@esbuild/freebsd-x64@0.27.4": resolution: { - integrity: sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==, + integrity: sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==, } engines: { node: ">=18" } cpu: [x64] os: [freebsd] - "@esbuild/linux-arm64@0.27.3": + "@esbuild/linux-arm64@0.27.4": resolution: { - integrity: sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==, + integrity: sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==, } engines: { node: ">=18" } cpu: [arm64] os: [linux] - "@esbuild/linux-arm@0.27.3": + "@esbuild/linux-arm@0.27.4": resolution: { - integrity: sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==, + integrity: sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==, } engines: { node: ">=18" } cpu: [arm] os: [linux] - "@esbuild/linux-ia32@0.27.3": + "@esbuild/linux-ia32@0.27.4": resolution: { - integrity: sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==, + integrity: sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==, } engines: { node: ">=18" } cpu: [ia32] os: [linux] - "@esbuild/linux-loong64@0.27.3": + "@esbuild/linux-loong64@0.27.4": resolution: { - integrity: sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==, + integrity: sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==, } engines: { node: ">=18" } cpu: [loong64] os: [linux] - "@esbuild/linux-mips64el@0.27.3": + "@esbuild/linux-mips64el@0.27.4": resolution: { - integrity: sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==, + integrity: sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==, } engines: { node: ">=18" } cpu: [mips64el] os: [linux] - "@esbuild/linux-ppc64@0.27.3": + "@esbuild/linux-ppc64@0.27.4": resolution: { - integrity: sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==, + integrity: sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==, } engines: { node: ">=18" } cpu: [ppc64] os: [linux] - "@esbuild/linux-riscv64@0.27.3": + "@esbuild/linux-riscv64@0.27.4": resolution: { - integrity: sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==, + integrity: sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==, } engines: { node: ">=18" } cpu: [riscv64] os: [linux] - "@esbuild/linux-s390x@0.27.3": + "@esbuild/linux-s390x@0.27.4": resolution: { - integrity: sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==, + integrity: sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==, } engines: { node: ">=18" } cpu: [s390x] os: [linux] - "@esbuild/linux-x64@0.27.3": + "@esbuild/linux-x64@0.27.4": resolution: { - integrity: sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==, + integrity: sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==, } engines: { node: ">=18" } cpu: [x64] os: [linux] - "@esbuild/netbsd-arm64@0.27.3": + "@esbuild/netbsd-arm64@0.27.4": resolution: { - integrity: sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==, + integrity: sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==, } engines: { node: ">=18" } cpu: [arm64] os: [netbsd] - "@esbuild/netbsd-x64@0.27.3": + "@esbuild/netbsd-x64@0.27.4": resolution: { - integrity: sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==, + integrity: sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==, } engines: { node: ">=18" } cpu: [x64] os: [netbsd] - "@esbuild/openbsd-arm64@0.27.3": + "@esbuild/openbsd-arm64@0.27.4": resolution: { - integrity: sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==, + integrity: sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==, } engines: { node: ">=18" } cpu: [arm64] os: [openbsd] - "@esbuild/openbsd-x64@0.27.3": + "@esbuild/openbsd-x64@0.27.4": resolution: { - integrity: sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==, + integrity: sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==, } engines: { node: ">=18" } cpu: [x64] os: [openbsd] - "@esbuild/openharmony-arm64@0.27.3": + "@esbuild/openharmony-arm64@0.27.4": resolution: { - integrity: sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==, + integrity: sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==, } engines: { node: ">=18" } cpu: [arm64] os: [openharmony] - "@esbuild/sunos-x64@0.27.3": + "@esbuild/sunos-x64@0.27.4": resolution: { - integrity: sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==, + integrity: sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==, } engines: { node: ">=18" } cpu: [x64] os: [sunos] - "@esbuild/win32-arm64@0.27.3": + "@esbuild/win32-arm64@0.27.4": resolution: { - integrity: sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==, + integrity: sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==, } engines: { node: ">=18" } cpu: [arm64] os: [win32] - "@esbuild/win32-ia32@0.27.3": + "@esbuild/win32-ia32@0.27.4": resolution: { - integrity: sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==, + integrity: sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==, } engines: { node: ">=18" } cpu: [ia32] os: [win32] - "@esbuild/win32-x64@0.27.3": + "@esbuild/win32-x64@0.27.4": resolution: { - integrity: sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==, + integrity: sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==, } engines: { node: ">=18" } cpu: [x64] os: [win32] - "@hpke/common@1.8.1": + "@hpke/common@1.10.1": resolution: { - integrity: sha512-PSI4QSxH8XDli0TqAsWycVfrLLCM/bBe+hVlJwtuJJiKIvCaFS3CXX/WtRfJceLJye9NHc2J7GvHVCY9B1BEbA==, - } - engines: { node: ">=16.0.0" } - - "@hpke/common@1.9.0": - resolution: - { - integrity: sha512-Sdxj4KqtmBt8FiwRkNLxXF+peqLR3FwVxtnsemiiEzClgslckRpFv3yK8mchoCwvyRwLOMS3Y2Z9ND2bq3sRVg==, + integrity: sha512-moJwhmtLtuxiUzzNp1jpfBfx8yefKoO9D/RCR9dmwrnc7qjJqId1rEtQz+lSlU5cabX8daToMSx/7HayXOiaFw==, } engines: { node: ">=16.0.0" } @@ -657,10 +656,10 @@ packages: } engines: { node: ">=16.0.0" } - "@hpke/core@1.8.0": + "@hpke/core@1.9.0": resolution: { - integrity: sha512-yHuo+2q4HSPUFuxcg87Kiy7QZRk4IeR+cwBB0qW8fHnr71bnRCArM39Cq1bWHBt75gTyeERGD/v1H14yPB2wyw==, + integrity: sha512-pFxWl1nNJeQCSUFs7+GAblHvXBCjn9EPN65vdKlYQil2aURaRxfGMO6vBKGqm1YHTKwiAxJQNEI70PbSowMP9Q==, } engines: { node: ">=16.0.0" } @@ -819,215 +818,215 @@ packages: integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==, } - "@rollup/rollup-android-arm-eabi@4.57.1": + "@rollup/rollup-android-arm-eabi@4.60.0": resolution: { - integrity: sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==, + integrity: sha512-WOhNW9K8bR3kf4zLxbfg6Pxu2ybOUbB2AjMDHSQx86LIF4rH4Ft7vmMwNt0loO0eonglSNy4cpD3MKXXKQu0/A==, } cpu: [arm] os: [android] - "@rollup/rollup-android-arm64@4.57.1": + "@rollup/rollup-android-arm64@4.60.0": resolution: { - integrity: sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==, + integrity: sha512-u6JHLll5QKRvjciE78bQXDmqRqNs5M/3GVqZeMwvmjaNODJih/WIrJlFVEihvV0MiYFmd+ZyPr9wxOVbPAG2Iw==, } cpu: [arm64] os: [android] - "@rollup/rollup-darwin-arm64@4.57.1": + "@rollup/rollup-darwin-arm64@4.60.0": resolution: { - integrity: sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==, + integrity: sha512-qEF7CsKKzSRc20Ciu2Zw1wRrBz4g56F7r/vRwY430UPp/nt1x21Q/fpJ9N5l47WWvJlkNCPJz3QRVw008fi7yA==, } cpu: [arm64] os: [darwin] - "@rollup/rollup-darwin-x64@4.57.1": + "@rollup/rollup-darwin-x64@4.60.0": resolution: { - integrity: sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==, + integrity: sha512-WADYozJ4QCnXCH4wPB+3FuGmDPoFseVCUrANmA5LWwGmC6FL14BWC7pcq+FstOZv3baGX65tZ378uT6WG8ynTw==, } cpu: [x64] os: [darwin] - "@rollup/rollup-freebsd-arm64@4.57.1": + "@rollup/rollup-freebsd-arm64@4.60.0": resolution: { - integrity: sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==, + integrity: sha512-6b8wGHJlDrGeSE3aH5mGNHBjA0TTkxdoNHik5EkvPHCt351XnigA4pS7Wsj/Eo9Y8RBU6f35cjN9SYmCFBtzxw==, } cpu: [arm64] os: [freebsd] - "@rollup/rollup-freebsd-x64@4.57.1": + "@rollup/rollup-freebsd-x64@4.60.0": resolution: { - integrity: sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==, + integrity: sha512-h25Ga0t4jaylMB8M/JKAyrvvfxGRjnPQIR8lnCayyzEjEOx2EJIlIiMbhpWxDRKGKF8jbNH01NnN663dH638mA==, } cpu: [x64] os: [freebsd] - "@rollup/rollup-linux-arm-gnueabihf@4.57.1": + "@rollup/rollup-linux-arm-gnueabihf@4.60.0": resolution: { - integrity: sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==, + integrity: sha512-RzeBwv0B3qtVBWtcuABtSuCzToo2IEAIQrcyB/b2zMvBWVbjo8bZDjACUpnaafaxhTw2W+imQbP2BD1usasK4g==, } cpu: [arm] os: [linux] libc: [glibc] - "@rollup/rollup-linux-arm-musleabihf@4.57.1": + "@rollup/rollup-linux-arm-musleabihf@4.60.0": resolution: { - integrity: sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==, + integrity: sha512-Sf7zusNI2CIU1HLzuu9Tc5YGAHEZs5Lu7N1ssJG4Tkw6e0MEsN7NdjUDDfGNHy2IU+ENyWT+L2obgWiguWibWQ==, } cpu: [arm] os: [linux] libc: [musl] - "@rollup/rollup-linux-arm64-gnu@4.57.1": + "@rollup/rollup-linux-arm64-gnu@4.60.0": resolution: { - integrity: sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==, + integrity: sha512-DX2x7CMcrJzsE91q7/O02IJQ5/aLkVtYFryqCjduJhUfGKG6yJV8hxaw8pZa93lLEpPTP/ohdN4wFz7yp/ry9A==, } cpu: [arm64] os: [linux] libc: [glibc] - "@rollup/rollup-linux-arm64-musl@4.57.1": + "@rollup/rollup-linux-arm64-musl@4.60.0": resolution: { - integrity: sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==, + integrity: sha512-09EL+yFVbJZlhcQfShpswwRZ0Rg+z/CsSELFCnPt3iK+iqwGsI4zht3secj5vLEs957QvFFXnzAT0FFPIxSrkQ==, } cpu: [arm64] os: [linux] libc: [musl] - "@rollup/rollup-linux-loong64-gnu@4.57.1": + "@rollup/rollup-linux-loong64-gnu@4.60.0": resolution: { - integrity: sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==, + integrity: sha512-i9IcCMPr3EXm8EQg5jnja0Zyc1iFxJjZWlb4wr7U2Wx/GrddOuEafxRdMPRYVaXjgbhvqalp6np07hN1w9kAKw==, } cpu: [loong64] os: [linux] libc: [glibc] - "@rollup/rollup-linux-loong64-musl@4.57.1": + "@rollup/rollup-linux-loong64-musl@4.60.0": resolution: { - integrity: sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==, + integrity: sha512-DGzdJK9kyJ+B78MCkWeGnpXJ91tK/iKA6HwHxF4TAlPIY7GXEvMe8hBFRgdrR9Ly4qebR/7gfUs9y2IoaVEyog==, } cpu: [loong64] os: [linux] libc: [musl] - "@rollup/rollup-linux-ppc64-gnu@4.57.1": + "@rollup/rollup-linux-ppc64-gnu@4.60.0": resolution: { - integrity: sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==, + integrity: sha512-RwpnLsqC8qbS8z1H1AxBA1H6qknR4YpPR9w2XX0vo2Sz10miu57PkNcnHVaZkbqyw/kUWfKMI73jhmfi9BRMUQ==, } cpu: [ppc64] os: [linux] libc: [glibc] - "@rollup/rollup-linux-ppc64-musl@4.57.1": + "@rollup/rollup-linux-ppc64-musl@4.60.0": resolution: { - integrity: sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==, + integrity: sha512-Z8pPf54Ly3aqtdWC3G4rFigZgNvd+qJlOE52fmko3KST9SoGfAdSRCwyoyG05q1HrrAblLbk1/PSIV+80/pxLg==, } cpu: [ppc64] os: [linux] libc: [musl] - "@rollup/rollup-linux-riscv64-gnu@4.57.1": + "@rollup/rollup-linux-riscv64-gnu@4.60.0": resolution: { - integrity: sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==, + integrity: sha512-3a3qQustp3COCGvnP4SvrMHnPQ9d1vzCakQVRTliaz8cIp/wULGjiGpbcqrkv0WrHTEp8bQD/B3HBjzujVWLOA==, } cpu: [riscv64] os: [linux] libc: [glibc] - "@rollup/rollup-linux-riscv64-musl@4.57.1": + "@rollup/rollup-linux-riscv64-musl@4.60.0": resolution: { - integrity: sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==, + integrity: sha512-pjZDsVH/1VsghMJ2/kAaxt6dL0psT6ZexQVrijczOf+PeP2BUqTHYejk3l6TlPRydggINOeNRhvpLa0AYpCWSQ==, } cpu: [riscv64] os: [linux] libc: [musl] - "@rollup/rollup-linux-s390x-gnu@4.57.1": + "@rollup/rollup-linux-s390x-gnu@4.60.0": resolution: { - integrity: sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==, + integrity: sha512-3ObQs0BhvPgiUVZrN7gqCSvmFuMWvWvsjG5ayJ3Lraqv+2KhOsp+pUbigqbeWqueGIsnn+09HBw27rJ+gYK4VQ==, } cpu: [s390x] os: [linux] libc: [glibc] - "@rollup/rollup-linux-x64-gnu@4.57.1": + "@rollup/rollup-linux-x64-gnu@4.60.0": resolution: { - integrity: sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==, + integrity: sha512-EtylprDtQPdS5rXvAayrNDYoJhIz1/vzN2fEubo3yLE7tfAw+948dO0g4M0vkTVFhKojnF+n6C8bDNe+gDRdTg==, } cpu: [x64] os: [linux] libc: [glibc] - "@rollup/rollup-linux-x64-musl@4.57.1": + "@rollup/rollup-linux-x64-musl@4.60.0": resolution: { - integrity: sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==, + integrity: sha512-k09oiRCi/bHU9UVFqD17r3eJR9bn03TyKraCrlz5ULFJGdJGi7VOmm9jl44vOJvRJ6P7WuBi/s2A97LxxHGIdw==, } cpu: [x64] os: [linux] libc: [musl] - "@rollup/rollup-openbsd-x64@4.57.1": + "@rollup/rollup-openbsd-x64@4.60.0": resolution: { - integrity: sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==, + integrity: sha512-1o/0/pIhozoSaDJoDcec+IVLbnRtQmHwPV730+AOD29lHEEo4F5BEUB24H0OBdhbBBDwIOSuf7vgg0Ywxdfiiw==, } cpu: [x64] os: [openbsd] - "@rollup/rollup-openharmony-arm64@4.57.1": + "@rollup/rollup-openharmony-arm64@4.60.0": resolution: { - integrity: sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==, + integrity: sha512-pESDkos/PDzYwtyzB5p/UoNU/8fJo68vcXM9ZW2V0kjYayj1KaaUfi1NmTUTUpMn4UhU4gTuK8gIaFO4UGuMbA==, } cpu: [arm64] os: [openharmony] - "@rollup/rollup-win32-arm64-msvc@4.57.1": + "@rollup/rollup-win32-arm64-msvc@4.60.0": resolution: { - integrity: sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==, + integrity: sha512-hj1wFStD7B1YBeYmvY+lWXZ7ey73YGPcViMShYikqKT1GtstIKQAtfUI6yrzPjAy/O7pO0VLXGmUVWXQMaYgTQ==, } cpu: [arm64] os: [win32] - "@rollup/rollup-win32-ia32-msvc@4.57.1": + "@rollup/rollup-win32-ia32-msvc@4.60.0": resolution: { - integrity: sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==, + integrity: sha512-SyaIPFoxmUPlNDq5EHkTbiKzmSEmq/gOYFI/3HHJ8iS/v1mbugVa7dXUzcJGQfoytp9DJFLhHH4U3/eTy2Bq4w==, } cpu: [ia32] os: [win32] - "@rollup/rollup-win32-x64-gnu@4.57.1": + "@rollup/rollup-win32-x64-gnu@4.60.0": resolution: { - integrity: sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==, + integrity: sha512-RdcryEfzZr+lAr5kRm2ucN9aVlCCa2QNq4hXelZxb8GG0NJSazq44Z3PCCc8wISRuCVnGs0lQJVX5Vp6fKA+IA==, } cpu: [x64] os: [win32] - "@rollup/rollup-win32-x64-msvc@4.57.1": + "@rollup/rollup-win32-x64-msvc@4.60.0": resolution: { - integrity: sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==, + integrity: sha512-PrsWNQ8BuE00O3Xsx3ALh2Df8fAj9+cvvX9AIA6o4KpATR98c9mud4XtDWVvsEuyia5U4tVSTKygawyJkjm60w==, } cpu: [x64] os: [win32] @@ -1074,101 +1073,101 @@ packages: integrity: sha512-PsxdFj/d2AcJcZDX1FXN3dDgitDDTmwf78rKZq1a6c1P1Nan1X/Sxc7667zU3U+AN60g7SxxP0YCVw2H/hBycg==, } - "@tailwindcss/node@4.1.18": + "@tailwindcss/node@4.2.2": resolution: { - integrity: sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==, + integrity: sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA==, } - "@tailwindcss/oxide-android-arm64@4.1.18": + "@tailwindcss/oxide-android-arm64@4.2.2": resolution: { - integrity: sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==, + integrity: sha512-dXGR1n+P3B6748jZO/SvHZq7qBOqqzQ+yFrXpoOWWALWndF9MoSKAT3Q0fYgAzYzGhxNYOoysRvYlpixRBBoDg==, } - engines: { node: ">= 10" } + engines: { node: ">= 20" } cpu: [arm64] os: [android] - "@tailwindcss/oxide-darwin-arm64@4.1.18": + "@tailwindcss/oxide-darwin-arm64@4.2.2": resolution: { - integrity: sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==, + integrity: sha512-iq9Qjr6knfMpZHj55/37ouZeykwbDqF21gPFtfnhCCKGDcPI/21FKC9XdMO/XyBM7qKORx6UIhGgg6jLl7BZlg==, } - engines: { node: ">= 10" } + engines: { node: ">= 20" } cpu: [arm64] os: [darwin] - "@tailwindcss/oxide-darwin-x64@4.1.18": + "@tailwindcss/oxide-darwin-x64@4.2.2": resolution: { - integrity: sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw==, + integrity: sha512-BlR+2c3nzc8f2G639LpL89YY4bdcIdUmiOOkv2GQv4/4M0vJlpXEa0JXNHhCHU7VWOKWT/CjqHdTP8aUuDJkuw==, } - engines: { node: ">= 10" } + engines: { node: ">= 20" } cpu: [x64] os: [darwin] - "@tailwindcss/oxide-freebsd-x64@4.1.18": + "@tailwindcss/oxide-freebsd-x64@4.2.2": resolution: { - integrity: sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==, + integrity: sha512-YUqUgrGMSu2CDO82hzlQ5qSb5xmx3RUrke/QgnoEx7KvmRJHQuZHZmZTLSuuHwFf0DJPybFMXMYf+WJdxHy/nQ==, } - engines: { node: ">= 10" } + engines: { node: ">= 20" } cpu: [x64] os: [freebsd] - "@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18": + "@tailwindcss/oxide-linux-arm-gnueabihf@4.2.2": resolution: { - integrity: sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==, + integrity: sha512-FPdhvsW6g06T9BWT0qTwiVZYE2WIFo2dY5aCSpjG/S/u1tby+wXoslXS0kl3/KXnULlLr1E3NPRRw0g7t2kgaQ==, } - engines: { node: ">= 10" } + engines: { node: ">= 20" } cpu: [arm] os: [linux] - "@tailwindcss/oxide-linux-arm64-gnu@4.1.18": + "@tailwindcss/oxide-linux-arm64-gnu@4.2.2": resolution: { - integrity: sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==, + integrity: sha512-4og1V+ftEPXGttOO7eCmW7VICmzzJWgMx+QXAJRAhjrSjumCwWqMfkDrNu1LXEQzNAwz28NCUpucgQPrR4S2yw==, } - engines: { node: ">= 10" } + engines: { node: ">= 20" } cpu: [arm64] os: [linux] libc: [glibc] - "@tailwindcss/oxide-linux-arm64-musl@4.1.18": + "@tailwindcss/oxide-linux-arm64-musl@4.2.2": resolution: { - integrity: sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==, + integrity: sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag==, } - engines: { node: ">= 10" } + engines: { node: ">= 20" } cpu: [arm64] os: [linux] libc: [musl] - "@tailwindcss/oxide-linux-x64-gnu@4.1.18": + "@tailwindcss/oxide-linux-x64-gnu@4.2.2": resolution: { - integrity: sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==, + integrity: sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg==, } - engines: { node: ">= 10" } + engines: { node: ">= 20" } cpu: [x64] os: [linux] libc: [glibc] - "@tailwindcss/oxide-linux-x64-musl@4.1.18": + "@tailwindcss/oxide-linux-x64-musl@4.2.2": resolution: { - integrity: sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==, + integrity: sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ==, } - engines: { node: ">= 10" } + engines: { node: ">= 20" } cpu: [x64] os: [linux] libc: [musl] - "@tailwindcss/oxide-wasm32-wasi@4.1.18": + "@tailwindcss/oxide-wasm32-wasi@4.2.2": resolution: { - integrity: sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==, + integrity: sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q==, } engines: { node: ">=14.0.0" } cpu: [wasm32] @@ -1180,38 +1179,38 @@ packages: - "@emnapi/wasi-threads" - tslib - "@tailwindcss/oxide-win32-arm64-msvc@4.1.18": + "@tailwindcss/oxide-win32-arm64-msvc@4.2.2": resolution: { - integrity: sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==, + integrity: sha512-qPmaQM4iKu5mxpsrWZMOZRgZv1tOZpUm+zdhhQP0VhJfyGGO3aUKdbh3gDZc/dPLQwW4eSqWGrrcWNBZWUWaXQ==, } - engines: { node: ">= 10" } + engines: { node: ">= 20" } cpu: [arm64] os: [win32] - "@tailwindcss/oxide-win32-x64-msvc@4.1.18": + "@tailwindcss/oxide-win32-x64-msvc@4.2.2": resolution: { - integrity: sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==, + integrity: sha512-1T/37VvI7WyH66b+vqHj/cLwnCxt7Qt3WFu5Q8hk65aOvlwAhs7rAp1VkulBJw/N4tMirXjVnylTR72uI0HGcA==, } - engines: { node: ">= 10" } + engines: { node: ">= 20" } cpu: [x64] os: [win32] - "@tailwindcss/oxide@4.1.18": + "@tailwindcss/oxide@4.2.2": resolution: { - integrity: sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==, + integrity: sha512-qEUA07+E5kehxYp9BVMpq9E8vnJuBHfJEC0vPC5e7iL/hw7HR61aDKoVoKzrG+QKp56vhNZe4qwkRmMC0zDLvg==, } - engines: { node: ">= 10" } + engines: { node: ">= 20" } - "@tailwindcss/vite@4.1.18": + "@tailwindcss/vite@4.2.2": resolution: { - integrity: sha512-jVA+/UpKL1vRLg6Hkao5jldawNmRo7mQYrZtNHMIVpLfLhDml5nMRUo/8MwoX2vNXvnaXNNMedrMfMugAVX1nA==, + integrity: sha512-mEiF5HO1QqCLXoNEfXVA1Tzo+cYsrqV7w9Juj2wdUFyW07JRenqMG225MvPwr3ZD9N1bFQj46X7r33iHxLUW0w==, } peerDependencies: - vite: ^5.2.0 || ^6 || ^7 + vite: ^5.2.0 || ^6 || ^7 || ^8 "@types/babel__core@7.20.5": resolution: @@ -1243,10 +1242,10 @@ packages: integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==, } - "@types/debug@4.1.12": + "@types/debug@4.1.13": resolution: { - integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==, + integrity: sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==, } "@types/deep-eql@4.0.2": @@ -1273,16 +1272,10 @@ packages: integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==, } - "@types/node@24.10.13": - resolution: - { - integrity: sha512-oH72nZRfDv9lADUBSo104Aq7gPHpQZc4BTx38r9xf9pg5LfP6EzSyH2n7qFmmxRQXh7YlUXODcYsg6PuTDSxGg==, - } - - "@types/node@24.11.0": + "@types/node@24.12.0": resolution: { - integrity: sha512-fPxQqz4VTgPI/IQ+lj9r0h+fDR66bzoeMGHp8ASee+32OSGIkeASsoZuJixsQoVef1QJbeubcPBxKk22QVoWdw==, + integrity: sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ==, } "@types/react-dom@19.2.3": @@ -1372,22 +1365,22 @@ packages: } engines: { node: ">=8" } - applesauce-accounts@5.1.0: + applesauce-accounts@5.2.0: resolution: { - integrity: sha512-xFNqofcQx+GjLbJKqkhEGWPMKPA0Y523ZiLuPwiLxwxiQRN053Rsq+uOTV0qmg/Hspylg3EGR49wwCIE8WnNGg==, + integrity: sha512-iobgIHrlkyWwsOA80t8HDhpNjRVBanby5HqDk5NfxzdDfMQPcJSMehnwTOcNnzeOOARkj+8dlNXbdCWQ7Zyefg==, } - applesauce-common@5.1.0: + applesauce-common@5.2.0: resolution: { - integrity: sha512-qUEJibEtawkCgrxoyj0u+ECk3C/PKwGSmOq9kCupGjHVG21BU6Nh29BxU4vcmmBXH2ByiA6VuZgyJZb3t2JJuA==, + integrity: sha512-6Natb0szkj65OBKK5LBHIszAmVd8ha9GkCZcavJnZbNeWBgoDTO0hfkgI4pk2L6L/OWNceo2XCajvDAx0AjlgQ==, } - applesauce-core@5.1.0: + applesauce-core@5.2.0: resolution: { - integrity: sha512-kk4nHndK4zjS8Sa6mC8LGtQ0LDSP4hlCGPJ9lpyIln7MkZaNFWD9eFd+fsEhfE9kyrne9IyYuVfJNp+EqY1b9w==, + integrity: sha512-aSuM6q6/Gs2FGUqytlHDjKZpSst2xKaT0vMXUQFWUctECNIxvwy6/hTDDInukMuI9mrQdjnO781ZJJgghI7RNw==, } applesauce-extra@5.0.0: @@ -1402,16 +1395,16 @@ packages: integrity: sha512-xllWYl7KxG0oaJqKVdZNzxN8OZQoDMaMmaTAO9Ao1Son+mmJyR9Q4UVicSwSlzzHarf59WCfvJeSdrSHIitkHg==, } - applesauce-relay@5.1.0: + applesauce-relay@5.2.0: resolution: { - integrity: sha512-d0LTJmQmr5gsYFm9A6efPEo2Bx/ewoL7LNsIdieMx34QohZBpPb137RvU9KQ1lFIXTm0tudd8VYfAPncqti2OQ==, + integrity: sha512-ty8PzHenocGdTr3x3It8Ql0rMD9rxB6VGCzGRfL5QF6epdstv2YHKuTyr8QdPBvf7yxfc7oZcMi6djSwNxXqkQ==, } - applesauce-signers@5.1.0: + applesauce-signers@5.2.0: resolution: { - integrity: sha512-sdQe6J1txYV1GVX8/zSGZDyyXuuZomePHSfUDozZmNAnXhCXE0wqVfhLK0yegVMnomSgoeDUCsGmJiTE2BHqoQ==, + integrity: sha512-7tN7lNK2XERdrRchG5z4rdpMqOacFdv7rRhiS+DLTdlbqeSf0wD6Kj8M3vSqq5f2pVS2cl5Z4E/m5RpWC4PSxg==, } argparse@1.0.10: @@ -1440,18 +1433,19 @@ packages: } engines: { node: ">=12" } - balanced-match@4.0.2: + balanced-match@4.0.4: resolution: { - integrity: sha512-x0K50QvKQ97fdEz2kPehIerj+YTeptKF9hyYkKf6egnwmMWAkADiO0QCzSp0R5xN8FTZgYaBfSaue46Ej62nMg==, + integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==, } - engines: { node: 20 || >=22 } + engines: { node: 18 || 20 || >=22 } - baseline-browser-mapping@2.9.19: + baseline-browser-mapping@2.10.10: resolution: { - integrity: sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==, + integrity: sha512-sUoJ3IMxx4AyRqO4MLeHlnGDkyXRoUG0/AI9fjK+vS72ekpV0yWVY7O0BVjmBcRtkNcsAO2QDZ4tdKKGoI6YaQ==, } + engines: { node: ">=6.0.0" } hasBin: true better-path-resolve@1.0.0: @@ -1461,12 +1455,12 @@ packages: } engines: { node: ">=4" } - brace-expansion@5.0.2: + brace-expansion@5.0.4: resolution: { - integrity: sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw==, + integrity: sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==, } - engines: { node: 20 || >=22 } + engines: { node: 18 || 20 || >=22 } braces@3.0.3: resolution: @@ -1490,10 +1484,10 @@ packages: } engines: { node: ">=8" } - caniuse-lite@1.0.30001769: + caniuse-lite@1.0.30001781: resolution: { - integrity: sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg==, + integrity: sha512-RdwNCyMsNBftLjW6w01z8bKEvT6e/5tpPVEgtn22TiLGlstHOVecsX2KHFkD5e/vRnIE4EGzpuIODb3mtswtkw==, } chai@5.3.3: @@ -1516,13 +1510,6 @@ packages: } engines: { node: ">= 16" } - ci-info@3.9.0: - resolution: - { - integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==, - } - engines: { node: ">=8" } - convert-source-map@2.0.0: resolution: { @@ -1542,10 +1529,10 @@ packages: integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==, } - daisyui@5.5.18: + daisyui@5.5.19: resolution: { - integrity: sha512-VVzjpOitMGB6DWIBeRSapbjdOevFqyzpk9u5Um6a4tyId3JFrU5pbtF0vgjXDth76mJZbueN/j9Ok03SPrh/og==, + integrity: sha512-pbFAkl1VCEh/MPCeclKL61I/MqRIFFhNU7yiXoDDRapXN4/qNCoMxeCCswyxEEhqL5eiTTfwHvucFtOE71C9sA==, } debug@4.4.3: @@ -1588,16 +1575,16 @@ packages: } engines: { node: ">=8" } - electron-to-chromium@1.5.286: + electron-to-chromium@1.5.321: resolution: { - integrity: sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==, + integrity: sha512-L2C7Q279W2D/J4PLZLk7sebOILDSWos7bMsMNN06rK482umHUrh/3lM8G7IlHFOYip2oAg5nha1rCMxr/rs6ZQ==, } - enhanced-resolve@5.19.0: + enhanced-resolve@5.20.1: resolution: { - integrity: sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==, + integrity: sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==, } engines: { node: ">=10.13.0" } @@ -1614,10 +1601,10 @@ packages: integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==, } - esbuild@0.27.3: + esbuild@0.27.4: resolution: { - integrity: sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==, + integrity: sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==, } engines: { node: ">=18" } hasBin: true @@ -1928,113 +1915,113 @@ packages: integrity: sha512-3QEofgiBOP4Ehs9BI+RkZdXZNtSys0nsJ6fyGeSiAGCBsMwHGUDS/JQlY/sTnWs91A2Nh0S9XXfA8Sy9g6QpuQ==, } - lightningcss-android-arm64@1.30.2: + lightningcss-android-arm64@1.32.0: resolution: { - integrity: sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==, + integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==, } engines: { node: ">= 12.0.0" } cpu: [arm64] os: [android] - lightningcss-darwin-arm64@1.30.2: + lightningcss-darwin-arm64@1.32.0: resolution: { - integrity: sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==, + integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==, } engines: { node: ">= 12.0.0" } cpu: [arm64] os: [darwin] - lightningcss-darwin-x64@1.30.2: + lightningcss-darwin-x64@1.32.0: resolution: { - integrity: sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==, + integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==, } engines: { node: ">= 12.0.0" } cpu: [x64] os: [darwin] - lightningcss-freebsd-x64@1.30.2: + lightningcss-freebsd-x64@1.32.0: resolution: { - integrity: sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==, + integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==, } engines: { node: ">= 12.0.0" } cpu: [x64] os: [freebsd] - lightningcss-linux-arm-gnueabihf@1.30.2: + lightningcss-linux-arm-gnueabihf@1.32.0: resolution: { - integrity: sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==, + integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==, } engines: { node: ">= 12.0.0" } cpu: [arm] os: [linux] - lightningcss-linux-arm64-gnu@1.30.2: + lightningcss-linux-arm64-gnu@1.32.0: resolution: { - integrity: sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==, + integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==, } engines: { node: ">= 12.0.0" } cpu: [arm64] os: [linux] libc: [glibc] - lightningcss-linux-arm64-musl@1.30.2: + lightningcss-linux-arm64-musl@1.32.0: resolution: { - integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==, + integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==, } engines: { node: ">= 12.0.0" } cpu: [arm64] os: [linux] libc: [musl] - lightningcss-linux-x64-gnu@1.30.2: + lightningcss-linux-x64-gnu@1.32.0: resolution: { - integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==, + integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==, } engines: { node: ">= 12.0.0" } cpu: [x64] os: [linux] libc: [glibc] - lightningcss-linux-x64-musl@1.30.2: + lightningcss-linux-x64-musl@1.32.0: resolution: { - integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==, + integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==, } engines: { node: ">= 12.0.0" } cpu: [x64] os: [linux] libc: [musl] - lightningcss-win32-arm64-msvc@1.30.2: + lightningcss-win32-arm64-msvc@1.32.0: resolution: { - integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==, + integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==, } engines: { node: ">= 12.0.0" } cpu: [arm64] os: [win32] - lightningcss-win32-x64-msvc@1.30.2: + lightningcss-win32-x64-msvc@1.32.0: resolution: { - integrity: sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==, + integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==, } engines: { node: ">= 12.0.0" } cpu: [x64] os: [win32] - lightningcss@1.30.2: + lightningcss@1.32.0: resolution: { - integrity: sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==, + integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==, } engines: { node: ">= 12.0.0" } @@ -2063,10 +2050,10 @@ packages: integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==, } - lru-cache@11.2.6: + lru-cache@11.2.7: resolution: { - integrity: sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==, + integrity: sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==, } engines: { node: 20 || >=22 } @@ -2096,17 +2083,17 @@ packages: } engines: { node: ">=8.6" } - minimatch@10.2.0: + minimatch@10.2.4: resolution: { - integrity: sha512-ugkC31VaVg9cF0DFVoADH12k6061zNZkZON+aX8AWsR9GhPcErkcMBceb6znR8wLERM2AkkOxy2nWRLpT9Jq5w==, + integrity: sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==, } - engines: { node: 20 || >=22 } + engines: { node: 18 || 20 || >=22 } - minipass@7.1.2: + minipass@7.1.3: resolution: { - integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==, + integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==, } engines: { node: ">=16 || 14 >=14.17" } @@ -2131,19 +2118,27 @@ packages: engines: { node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1 } hasBin: true - nanoid@5.1.6: + nanoid@5.1.7: resolution: { - integrity: sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg==, + integrity: sha512-ua3NDgISf6jdwezAheMOk4mbE1LXjm1DfMUDMuJf4AqxLFK3ccGpgWizwa5YV7Yz9EpXwEaWoRXSb/BnV0t5dQ==, } engines: { node: ^18 || >=20 } hasBin: true - node-releases@2.0.27: + node-releases@2.0.36: + resolution: + { + integrity: sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==, + } + + nostr-signer-capacitor-plugin@0.0.5: resolution: { - integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==, + integrity: sha512-/EvqWz71HZ5cWmzvfXWTm48AWZtbeZDbOg3vLwXyXPjnIp1DR7Wurww/Mo41ORNu1DNPlqH20l7kIXKO6vR5og==, } + peerDependencies: + "@capacitor/core": ^7.0.0 nostr-tools@2.19.4: resolution: @@ -2240,12 +2235,12 @@ packages: } engines: { node: ">=8" } - path-scurry@2.0.1: + path-scurry@2.0.2: resolution: { - integrity: sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==, + integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==, } - engines: { node: 20 || >=22 } + engines: { node: 18 || 20 || >=22 } path-type@4.0.0: resolution: @@ -2294,10 +2289,10 @@ packages: } engines: { node: ">=6" } - postcss@8.5.6: + postcss@8.5.8: resolution: { - integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==, + integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==, } engines: { node: ^10 || ^12 || >=14 } @@ -2388,10 +2383,10 @@ packages: engines: { node: 20 || >=22 } hasBin: true - rollup@4.57.1: + rollup@4.60.0: resolution: { - integrity: sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==, + integrity: sha512-yqjxruMGBQJ2gG4HtjZtAfXArHomazDHoFwFFmZZl0r7Pdo7qCIXKqKHZc8yeoMgzJJ+pO6pEEHa+V7uzWlrAQ==, } engines: { node: ">=18.0.0", npm: ">=8.0.0" } hasBin: true @@ -2520,10 +2515,10 @@ packages: integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==, } - tailwindcss@4.1.18: + tailwindcss@4.2.2: resolution: { - integrity: sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==, + integrity: sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q==, } tapable@2.3.0: @@ -2776,8 +2771,8 @@ snapshots: "@babel/generator": 7.29.1 "@babel/helper-compilation-targets": 7.28.6 "@babel/helper-module-transforms": 7.28.6(@babel/core@7.29.0) - "@babel/helpers": 7.28.6 - "@babel/parser": 7.29.0 + "@babel/helpers": 7.29.2 + "@babel/parser": 7.29.2 "@babel/template": 7.28.6 "@babel/traverse": 7.29.0 "@babel/types": 7.29.0 @@ -2792,7 +2787,7 @@ snapshots: "@babel/generator@7.29.1": dependencies: - "@babel/parser": 7.29.0 + "@babel/parser": 7.29.2 "@babel/types": 7.29.0 "@jridgewell/gen-mapping": 0.3.13 "@jridgewell/trace-mapping": 0.3.31 @@ -2832,12 +2827,12 @@ snapshots: "@babel/helper-validator-option@7.27.1": {} - "@babel/helpers@7.28.6": + "@babel/helpers@7.29.2": dependencies: "@babel/template": 7.28.6 "@babel/types": 7.29.0 - "@babel/parser@7.29.0": + "@babel/parser@7.29.2": dependencies: "@babel/types": 7.29.0 @@ -2851,12 +2846,12 @@ snapshots: "@babel/core": 7.29.0 "@babel/helper-plugin-utils": 7.28.6 - "@babel/runtime@7.28.6": {} + "@babel/runtime@7.29.2": {} "@babel/template@7.28.6": dependencies: "@babel/code-frame": 7.29.0 - "@babel/parser": 7.29.0 + "@babel/parser": 7.29.2 "@babel/types": 7.29.0 "@babel/traverse@7.29.0": @@ -2864,7 +2859,7 @@ snapshots: "@babel/code-frame": 7.29.0 "@babel/generator": 7.29.1 "@babel/helper-globals": 7.28.0 - "@babel/parser": 7.29.0 + "@babel/parser": 7.29.2 "@babel/template": 7.28.6 "@babel/types": 7.29.0 debug: 4.4.3 @@ -2876,9 +2871,14 @@ snapshots: "@babel/helper-string-parser": 7.27.1 "@babel/helper-validator-identifier": 7.28.5 - "@changesets/apply-release-plan@7.0.14": + "@capacitor/core@7.6.0": + dependencies: + tslib: 2.8.1 + optional: true + + "@changesets/apply-release-plan@7.1.0": dependencies: - "@changesets/config": 3.1.2 + "@changesets/config": 3.1.3 "@changesets/get-version-range-type": 0.4.0 "@changesets/git": 3.0.4 "@changesets/should-skip-package": 0.1.2 @@ -2905,30 +2905,28 @@ snapshots: dependencies: "@changesets/types": 6.1.0 - "@changesets/cli@2.29.8(@types/node@24.11.0)": + "@changesets/cli@2.30.0(@types/node@24.12.0)": dependencies: - "@changesets/apply-release-plan": 7.0.14 + "@changesets/apply-release-plan": 7.1.0 "@changesets/assemble-release-plan": 6.0.9 "@changesets/changelog-git": 0.2.1 - "@changesets/config": 3.1.2 + "@changesets/config": 3.1.3 "@changesets/errors": 0.2.0 "@changesets/get-dependents-graph": 2.1.3 - "@changesets/get-release-plan": 4.0.14 + "@changesets/get-release-plan": 4.0.15 "@changesets/git": 3.0.4 "@changesets/logger": 0.1.1 "@changesets/pre": 2.0.2 - "@changesets/read": 0.6.6 + "@changesets/read": 0.6.7 "@changesets/should-skip-package": 0.1.2 "@changesets/types": 6.1.0 "@changesets/write": 0.4.0 - "@inquirer/external-editor": 1.0.3(@types/node@24.11.0) + "@inquirer/external-editor": 1.0.3(@types/node@24.12.0) "@manypkg/get-packages": 1.1.3 ansi-colors: 4.1.3 - ci-info: 3.9.0 enquirer: 2.4.1 fs-extra: 7.0.1 mri: 1.2.0 - p-limit: 2.3.0 package-manager-detector: 0.2.11 picocolors: 1.1.1 resolve-from: 5.0.0 @@ -2938,11 +2936,12 @@ snapshots: transitivePeerDependencies: - "@types/node" - "@changesets/config@3.1.2": + "@changesets/config@3.1.3": dependencies: "@changesets/errors": 0.2.0 "@changesets/get-dependents-graph": 2.1.3 "@changesets/logger": 0.1.1 + "@changesets/should-skip-package": 0.1.2 "@changesets/types": 6.1.0 "@manypkg/get-packages": 1.1.3 fs-extra: 7.0.1 @@ -2959,12 +2958,12 @@ snapshots: picocolors: 1.1.1 semver: 7.7.4 - "@changesets/get-release-plan@4.0.14": + "@changesets/get-release-plan@4.0.15": dependencies: "@changesets/assemble-release-plan": 6.0.9 - "@changesets/config": 3.1.2 + "@changesets/config": 3.1.3 "@changesets/pre": 2.0.2 - "@changesets/read": 0.6.6 + "@changesets/read": 0.6.7 "@changesets/types": 6.1.0 "@manypkg/get-packages": 1.1.3 @@ -2982,7 +2981,7 @@ snapshots: dependencies: picocolors: 1.1.1 - "@changesets/parse@0.4.2": + "@changesets/parse@0.4.3": dependencies: "@changesets/types": 6.1.0 js-yaml: 4.1.1 @@ -2994,11 +2993,11 @@ snapshots: "@manypkg/get-packages": 1.1.3 fs-extra: 7.0.1 - "@changesets/read@0.6.6": + "@changesets/read@0.6.7": dependencies: "@changesets/git": 3.0.4 "@changesets/logger": 0.1.1 - "@changesets/parse": 0.4.2 + "@changesets/parse": 0.4.3 "@changesets/types": 6.1.0 fs-extra: 7.0.1 p-filter: 2.1.0 @@ -3020,102 +3019,100 @@ snapshots: human-id: 4.1.3 prettier: 2.8.8 - "@esbuild/aix-ppc64@0.27.3": + "@esbuild/aix-ppc64@0.27.4": optional: true - "@esbuild/android-arm64@0.27.3": + "@esbuild/android-arm64@0.27.4": optional: true - "@esbuild/android-arm@0.27.3": + "@esbuild/android-arm@0.27.4": optional: true - "@esbuild/android-x64@0.27.3": + "@esbuild/android-x64@0.27.4": optional: true - "@esbuild/darwin-arm64@0.27.3": + "@esbuild/darwin-arm64@0.27.4": optional: true - "@esbuild/darwin-x64@0.27.3": + "@esbuild/darwin-x64@0.27.4": optional: true - "@esbuild/freebsd-arm64@0.27.3": + "@esbuild/freebsd-arm64@0.27.4": optional: true - "@esbuild/freebsd-x64@0.27.3": + "@esbuild/freebsd-x64@0.27.4": optional: true - "@esbuild/linux-arm64@0.27.3": + "@esbuild/linux-arm64@0.27.4": optional: true - "@esbuild/linux-arm@0.27.3": + "@esbuild/linux-arm@0.27.4": optional: true - "@esbuild/linux-ia32@0.27.3": + "@esbuild/linux-ia32@0.27.4": optional: true - "@esbuild/linux-loong64@0.27.3": + "@esbuild/linux-loong64@0.27.4": optional: true - "@esbuild/linux-mips64el@0.27.3": + "@esbuild/linux-mips64el@0.27.4": optional: true - "@esbuild/linux-ppc64@0.27.3": + "@esbuild/linux-ppc64@0.27.4": optional: true - "@esbuild/linux-riscv64@0.27.3": + "@esbuild/linux-riscv64@0.27.4": optional: true - "@esbuild/linux-s390x@0.27.3": + "@esbuild/linux-s390x@0.27.4": optional: true - "@esbuild/linux-x64@0.27.3": + "@esbuild/linux-x64@0.27.4": optional: true - "@esbuild/netbsd-arm64@0.27.3": + "@esbuild/netbsd-arm64@0.27.4": optional: true - "@esbuild/netbsd-x64@0.27.3": + "@esbuild/netbsd-x64@0.27.4": optional: true - "@esbuild/openbsd-arm64@0.27.3": + "@esbuild/openbsd-arm64@0.27.4": optional: true - "@esbuild/openbsd-x64@0.27.3": + "@esbuild/openbsd-x64@0.27.4": optional: true - "@esbuild/openharmony-arm64@0.27.3": + "@esbuild/openharmony-arm64@0.27.4": optional: true - "@esbuild/sunos-x64@0.27.3": + "@esbuild/sunos-x64@0.27.4": optional: true - "@esbuild/win32-arm64@0.27.3": + "@esbuild/win32-arm64@0.27.4": optional: true - "@esbuild/win32-ia32@0.27.3": + "@esbuild/win32-ia32@0.27.4": optional: true - "@esbuild/win32-x64@0.27.3": + "@esbuild/win32-x64@0.27.4": optional: true - "@hpke/common@1.8.1": {} - - "@hpke/common@1.9.0": {} + "@hpke/common@1.10.1": {} "@hpke/core@1.7.5": dependencies: - "@hpke/common": 1.8.1 + "@hpke/common": 1.10.1 - "@hpke/core@1.8.0": + "@hpke/core@1.9.0": dependencies: - "@hpke/common": 1.9.0 + "@hpke/common": 1.10.1 - "@inquirer/external-editor@1.0.3(@types/node@24.11.0)": + "@inquirer/external-editor@1.0.3(@types/node@24.12.0)": dependencies: chardet: 2.1.1 iconv-lite: 0.7.2 optionalDependencies: - "@types/node": 24.11.0 + "@types/node": 24.12.0 "@isaacs/cliui@9.0.0": {} @@ -3140,14 +3137,14 @@ snapshots: "@manypkg/find-root@1.1.0": dependencies: - "@babel/runtime": 7.28.6 + "@babel/runtime": 7.29.2 "@types/node": 12.20.55 find-up: 4.1.0 fs-extra: 8.1.0 "@manypkg/get-packages@1.1.3": dependencies: - "@babel/runtime": 7.28.6 + "@babel/runtime": 7.29.2 "@changesets/types": 4.1.0 "@manypkg/find-root": 1.1.0 fs-extra: 8.1.0 @@ -3194,79 +3191,79 @@ snapshots: "@rolldown/pluginutils@1.0.0-beta.27": {} - "@rollup/rollup-android-arm-eabi@4.57.1": + "@rollup/rollup-android-arm-eabi@4.60.0": optional: true - "@rollup/rollup-android-arm64@4.57.1": + "@rollup/rollup-android-arm64@4.60.0": optional: true - "@rollup/rollup-darwin-arm64@4.57.1": + "@rollup/rollup-darwin-arm64@4.60.0": optional: true - "@rollup/rollup-darwin-x64@4.57.1": + "@rollup/rollup-darwin-x64@4.60.0": optional: true - "@rollup/rollup-freebsd-arm64@4.57.1": + "@rollup/rollup-freebsd-arm64@4.60.0": optional: true - "@rollup/rollup-freebsd-x64@4.57.1": + "@rollup/rollup-freebsd-x64@4.60.0": optional: true - "@rollup/rollup-linux-arm-gnueabihf@4.57.1": + "@rollup/rollup-linux-arm-gnueabihf@4.60.0": optional: true - "@rollup/rollup-linux-arm-musleabihf@4.57.1": + "@rollup/rollup-linux-arm-musleabihf@4.60.0": optional: true - "@rollup/rollup-linux-arm64-gnu@4.57.1": + "@rollup/rollup-linux-arm64-gnu@4.60.0": optional: true - "@rollup/rollup-linux-arm64-musl@4.57.1": + "@rollup/rollup-linux-arm64-musl@4.60.0": optional: true - "@rollup/rollup-linux-loong64-gnu@4.57.1": + "@rollup/rollup-linux-loong64-gnu@4.60.0": optional: true - "@rollup/rollup-linux-loong64-musl@4.57.1": + "@rollup/rollup-linux-loong64-musl@4.60.0": optional: true - "@rollup/rollup-linux-ppc64-gnu@4.57.1": + "@rollup/rollup-linux-ppc64-gnu@4.60.0": optional: true - "@rollup/rollup-linux-ppc64-musl@4.57.1": + "@rollup/rollup-linux-ppc64-musl@4.60.0": optional: true - "@rollup/rollup-linux-riscv64-gnu@4.57.1": + "@rollup/rollup-linux-riscv64-gnu@4.60.0": optional: true - "@rollup/rollup-linux-riscv64-musl@4.57.1": + "@rollup/rollup-linux-riscv64-musl@4.60.0": optional: true - "@rollup/rollup-linux-s390x-gnu@4.57.1": + "@rollup/rollup-linux-s390x-gnu@4.60.0": optional: true - "@rollup/rollup-linux-x64-gnu@4.57.1": + "@rollup/rollup-linux-x64-gnu@4.60.0": optional: true - "@rollup/rollup-linux-x64-musl@4.57.1": + "@rollup/rollup-linux-x64-musl@4.60.0": optional: true - "@rollup/rollup-openbsd-x64@4.57.1": + "@rollup/rollup-openbsd-x64@4.60.0": optional: true - "@rollup/rollup-openharmony-arm64@4.57.1": + "@rollup/rollup-openharmony-arm64@4.60.0": optional: true - "@rollup/rollup-win32-arm64-msvc@4.57.1": + "@rollup/rollup-win32-arm64-msvc@4.60.0": optional: true - "@rollup/rollup-win32-ia32-msvc@4.57.1": + "@rollup/rollup-win32-ia32-msvc@4.60.0": optional: true - "@rollup/rollup-win32-x64-gnu@4.57.1": + "@rollup/rollup-win32-x64-gnu@4.60.0": optional: true - "@rollup/rollup-win32-x64-msvc@4.57.1": + "@rollup/rollup-win32-x64-msvc@4.60.0": optional: true "@scure/base@1.1.1": {} @@ -3297,77 +3294,77 @@ snapshots: "@noble/hashes": 2.0.1 "@scure/base": 2.0.0 - "@tailwindcss/node@4.1.18": + "@tailwindcss/node@4.2.2": dependencies: "@jridgewell/remapping": 2.3.5 - enhanced-resolve: 5.19.0 + enhanced-resolve: 5.20.1 jiti: 2.6.1 - lightningcss: 1.30.2 + lightningcss: 1.32.0 magic-string: 0.30.21 source-map-js: 1.2.1 - tailwindcss: 4.1.18 + tailwindcss: 4.2.2 - "@tailwindcss/oxide-android-arm64@4.1.18": + "@tailwindcss/oxide-android-arm64@4.2.2": optional: true - "@tailwindcss/oxide-darwin-arm64@4.1.18": + "@tailwindcss/oxide-darwin-arm64@4.2.2": optional: true - "@tailwindcss/oxide-darwin-x64@4.1.18": + "@tailwindcss/oxide-darwin-x64@4.2.2": optional: true - "@tailwindcss/oxide-freebsd-x64@4.1.18": + "@tailwindcss/oxide-freebsd-x64@4.2.2": optional: true - "@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18": + "@tailwindcss/oxide-linux-arm-gnueabihf@4.2.2": optional: true - "@tailwindcss/oxide-linux-arm64-gnu@4.1.18": + "@tailwindcss/oxide-linux-arm64-gnu@4.2.2": optional: true - "@tailwindcss/oxide-linux-arm64-musl@4.1.18": + "@tailwindcss/oxide-linux-arm64-musl@4.2.2": optional: true - "@tailwindcss/oxide-linux-x64-gnu@4.1.18": + "@tailwindcss/oxide-linux-x64-gnu@4.2.2": optional: true - "@tailwindcss/oxide-linux-x64-musl@4.1.18": + "@tailwindcss/oxide-linux-x64-musl@4.2.2": optional: true - "@tailwindcss/oxide-wasm32-wasi@4.1.18": + "@tailwindcss/oxide-wasm32-wasi@4.2.2": optional: true - "@tailwindcss/oxide-win32-arm64-msvc@4.1.18": + "@tailwindcss/oxide-win32-arm64-msvc@4.2.2": optional: true - "@tailwindcss/oxide-win32-x64-msvc@4.1.18": + "@tailwindcss/oxide-win32-x64-msvc@4.2.2": optional: true - "@tailwindcss/oxide@4.1.18": + "@tailwindcss/oxide@4.2.2": optionalDependencies: - "@tailwindcss/oxide-android-arm64": 4.1.18 - "@tailwindcss/oxide-darwin-arm64": 4.1.18 - "@tailwindcss/oxide-darwin-x64": 4.1.18 - "@tailwindcss/oxide-freebsd-x64": 4.1.18 - "@tailwindcss/oxide-linux-arm-gnueabihf": 4.1.18 - "@tailwindcss/oxide-linux-arm64-gnu": 4.1.18 - "@tailwindcss/oxide-linux-arm64-musl": 4.1.18 - "@tailwindcss/oxide-linux-x64-gnu": 4.1.18 - "@tailwindcss/oxide-linux-x64-musl": 4.1.18 - "@tailwindcss/oxide-wasm32-wasi": 4.1.18 - "@tailwindcss/oxide-win32-arm64-msvc": 4.1.18 - "@tailwindcss/oxide-win32-x64-msvc": 4.1.18 - - "@tailwindcss/vite@4.1.18(vite@7.3.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.30.2))": - dependencies: - "@tailwindcss/node": 4.1.18 - "@tailwindcss/oxide": 4.1.18 - tailwindcss: 4.1.18 - vite: 7.3.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.30.2) + "@tailwindcss/oxide-android-arm64": 4.2.2 + "@tailwindcss/oxide-darwin-arm64": 4.2.2 + "@tailwindcss/oxide-darwin-x64": 4.2.2 + "@tailwindcss/oxide-freebsd-x64": 4.2.2 + "@tailwindcss/oxide-linux-arm-gnueabihf": 4.2.2 + "@tailwindcss/oxide-linux-arm64-gnu": 4.2.2 + "@tailwindcss/oxide-linux-arm64-musl": 4.2.2 + "@tailwindcss/oxide-linux-x64-gnu": 4.2.2 + "@tailwindcss/oxide-linux-x64-musl": 4.2.2 + "@tailwindcss/oxide-wasm32-wasi": 4.2.2 + "@tailwindcss/oxide-win32-arm64-msvc": 4.2.2 + "@tailwindcss/oxide-win32-x64-msvc": 4.2.2 + + "@tailwindcss/vite@4.2.2(vite@7.3.1(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.32.0))": + dependencies: + "@tailwindcss/node": 4.2.2 + "@tailwindcss/oxide": 4.2.2 + tailwindcss: 4.2.2 + vite: 7.3.1(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.32.0) "@types/babel__core@7.20.5": dependencies: - "@babel/parser": 7.29.0 + "@babel/parser": 7.29.2 "@babel/types": 7.29.0 "@types/babel__generator": 7.27.0 "@types/babel__template": 7.4.4 @@ -3379,7 +3376,7 @@ snapshots: "@types/babel__template@7.4.4": dependencies: - "@babel/parser": 7.29.0 + "@babel/parser": 7.29.2 "@babel/types": 7.29.0 "@types/babel__traverse@7.28.0": @@ -3391,7 +3388,7 @@ snapshots: "@types/deep-eql": 4.0.2 assertion-error: 2.0.1 - "@types/debug@4.1.12": + "@types/debug@4.1.13": dependencies: "@types/ms": 2.1.0 @@ -3403,11 +3400,7 @@ snapshots: "@types/node@12.20.55": {} - "@types/node@24.10.13": - dependencies: - undici-types: 7.16.0 - - "@types/node@24.11.0": + "@types/node@24.12.0": dependencies: undici-types: 7.16.0 @@ -3419,7 +3412,7 @@ snapshots: dependencies: csstype: 3.2.3 - "@vitejs/plugin-react@4.7.0(vite@7.3.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.30.2))": + "@vitejs/plugin-react@4.7.0(vite@7.3.1(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.32.0))": dependencies: "@babel/core": 7.29.0 "@babel/plugin-transform-react-jsx-self": 7.27.1(@babel/core@7.29.0) @@ -3427,7 +3420,7 @@ snapshots: "@rolldown/pluginutils": 1.0.0-beta.27 "@types/babel__core": 7.20.5 react-refresh: 0.17.0 - vite: 7.3.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.30.2) + vite: 7.3.1(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.32.0) transitivePeerDependencies: - supports-color @@ -3439,13 +3432,13 @@ snapshots: chai: 5.3.3 tinyrainbow: 2.0.0 - "@vitest/mocker@3.2.4(vite@7.3.1(@types/node@24.11.0)(jiti@2.6.1)(lightningcss@1.30.2))": + "@vitest/mocker@3.2.4(vite@7.3.1(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.32.0))": dependencies: "@vitest/spy": 3.2.4 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 7.3.1(@types/node@24.11.0)(jiti@2.6.1)(lightningcss@1.30.2) + vite: 7.3.1(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.32.0) "@vitest/pretty-format@3.2.4": dependencies: @@ -3477,20 +3470,23 @@ snapshots: ansi-regex@5.0.1: {} - applesauce-accounts@5.1.0(typescript@5.9.3): + applesauce-accounts@5.2.0(@capacitor/core@7.6.0)(typescript@5.9.3): dependencies: - applesauce-core: 5.1.0(typescript@5.9.3) - applesauce-signers: 5.1.0(typescript@5.9.3) - nanoid: 5.1.6 + applesauce-core: 5.2.0(typescript@5.9.3) + applesauce-signers: 5.2.0(@capacitor/core@7.6.0)(typescript@5.9.3) + nanoid: 5.1.7 rxjs: 7.8.2 + optionalDependencies: + nostr-signer-capacitor-plugin: 0.0.5(@capacitor/core@7.6.0) transitivePeerDependencies: + - "@capacitor/core" - supports-color - typescript - applesauce-common@5.1.0(typescript@5.9.3): + applesauce-common@5.2.0(typescript@5.9.3): dependencies: "@scure/base": 1.2.6 - applesauce-core: 5.1.0(typescript@5.9.3) + applesauce-core: 5.2.0(typescript@5.9.3) hash-sum: 2.0.0 light-bolt11-decoder: 3.2.0 rxjs: 7.8.2 @@ -3498,57 +3494,61 @@ snapshots: - supports-color - typescript - applesauce-core@5.1.0(typescript@5.9.3): + applesauce-core@5.2.0(typescript@5.9.3): dependencies: debug: 4.4.3 fast-deep-equal: 3.1.3 hash-sum: 2.0.0 - nanoid: 5.1.6 + nanoid: 5.1.7 nostr-tools: 2.19.4(typescript@5.9.3) rxjs: 7.8.2 transitivePeerDependencies: - supports-color - typescript - applesauce-extra@5.0.0(typescript@5.9.3): + applesauce-extra@5.0.0(@capacitor/core@7.6.0)(typescript@5.9.3): dependencies: - applesauce-core: 5.1.0(typescript@5.9.3) + applesauce-core: 5.2.0(typescript@5.9.3) rxjs: 7.8.2 optionalDependencies: - applesauce-relay: 5.1.0(typescript@5.9.3) - applesauce-signers: 5.1.0(typescript@5.9.3) + applesauce-relay: 5.2.0(typescript@5.9.3) + applesauce-signers: 5.2.0(@capacitor/core@7.6.0)(typescript@5.9.3) transitivePeerDependencies: + - "@capacitor/core" - supports-color - typescript applesauce-loaders@5.1.0(typescript@5.9.3): dependencies: - applesauce-core: 5.1.0(typescript@5.9.3) - nanoid: 5.1.6 + applesauce-core: 5.2.0(typescript@5.9.3) + nanoid: 5.1.7 rxjs: 7.8.2 transitivePeerDependencies: - supports-color - typescript - applesauce-relay@5.1.0(typescript@5.9.3): + applesauce-relay@5.2.0(typescript@5.9.3): dependencies: "@noble/hashes": 1.8.0 - applesauce-core: 5.1.0(typescript@5.9.3) - nanoid: 5.1.6 + applesauce-core: 5.2.0(typescript@5.9.3) + nanoid: 5.1.7 nostr-tools: 2.19.4(typescript@5.9.3) rxjs: 7.8.2 transitivePeerDependencies: - supports-color - typescript - applesauce-signers@5.1.0(typescript@5.9.3): + applesauce-signers@5.2.0(@capacitor/core@7.6.0)(typescript@5.9.3): dependencies: "@noble/secp256k1": 1.7.2 - applesauce-core: 5.1.0(typescript@5.9.3) + applesauce-core: 5.2.0(typescript@5.9.3) debug: 4.4.3 - nanoid: 5.1.6 + nanoid: 5.1.7 rxjs: 7.8.2 + optionalDependencies: + nostr-signer-capacitor-plugin: 0.0.5(@capacitor/core@7.6.0) transitivePeerDependencies: + - "@capacitor/core" - supports-color - typescript @@ -3562,19 +3562,17 @@ snapshots: assertion-error@2.0.1: {} - balanced-match@4.0.2: - dependencies: - jackspeak: 4.2.3 + balanced-match@4.0.4: {} - baseline-browser-mapping@2.9.19: {} + baseline-browser-mapping@2.10.10: {} better-path-resolve@1.0.0: dependencies: is-windows: 1.0.2 - brace-expansion@5.0.2: + brace-expansion@5.0.4: dependencies: - balanced-match: 4.0.2 + balanced-match: 4.0.4 braces@3.0.3: dependencies: @@ -3582,15 +3580,15 @@ snapshots: browserslist@4.28.1: dependencies: - baseline-browser-mapping: 2.9.19 - caniuse-lite: 1.0.30001769 - electron-to-chromium: 1.5.286 - node-releases: 2.0.27 + baseline-browser-mapping: 2.10.10 + caniuse-lite: 1.0.30001781 + electron-to-chromium: 1.5.321 + node-releases: 2.0.36 update-browserslist-db: 1.2.3(browserslist@4.28.1) cac@6.7.14: {} - caniuse-lite@1.0.30001769: {} + caniuse-lite@1.0.30001781: {} chai@5.3.3: dependencies: @@ -3604,8 +3602,6 @@ snapshots: check-error@2.1.3: {} - ci-info@3.9.0: {} - convert-source-map@2.0.0: {} cross-spawn@7.0.6: @@ -3616,7 +3612,7 @@ snapshots: csstype@3.2.3: {} - daisyui@5.5.18: {} + daisyui@5.5.19: {} debug@4.4.3: dependencies: @@ -3632,9 +3628,9 @@ snapshots: dependencies: path-type: 4.0.0 - electron-to-chromium@1.5.286: {} + electron-to-chromium@1.5.321: {} - enhanced-resolve@5.19.0: + enhanced-resolve@5.20.1: dependencies: graceful-fs: 4.2.11 tapable: 2.3.0 @@ -3646,34 +3642,34 @@ snapshots: es-module-lexer@1.7.0: {} - esbuild@0.27.3: + esbuild@0.27.4: optionalDependencies: - "@esbuild/aix-ppc64": 0.27.3 - "@esbuild/android-arm": 0.27.3 - "@esbuild/android-arm64": 0.27.3 - "@esbuild/android-x64": 0.27.3 - "@esbuild/darwin-arm64": 0.27.3 - "@esbuild/darwin-x64": 0.27.3 - "@esbuild/freebsd-arm64": 0.27.3 - "@esbuild/freebsd-x64": 0.27.3 - "@esbuild/linux-arm": 0.27.3 - "@esbuild/linux-arm64": 0.27.3 - "@esbuild/linux-ia32": 0.27.3 - "@esbuild/linux-loong64": 0.27.3 - "@esbuild/linux-mips64el": 0.27.3 - "@esbuild/linux-ppc64": 0.27.3 - "@esbuild/linux-riscv64": 0.27.3 - "@esbuild/linux-s390x": 0.27.3 - "@esbuild/linux-x64": 0.27.3 - "@esbuild/netbsd-arm64": 0.27.3 - "@esbuild/netbsd-x64": 0.27.3 - "@esbuild/openbsd-arm64": 0.27.3 - "@esbuild/openbsd-x64": 0.27.3 - "@esbuild/openharmony-arm64": 0.27.3 - "@esbuild/sunos-x64": 0.27.3 - "@esbuild/win32-arm64": 0.27.3 - "@esbuild/win32-ia32": 0.27.3 - "@esbuild/win32-x64": 0.27.3 + "@esbuild/aix-ppc64": 0.27.4 + "@esbuild/android-arm": 0.27.4 + "@esbuild/android-arm64": 0.27.4 + "@esbuild/android-x64": 0.27.4 + "@esbuild/darwin-arm64": 0.27.4 + "@esbuild/darwin-x64": 0.27.4 + "@esbuild/freebsd-arm64": 0.27.4 + "@esbuild/freebsd-x64": 0.27.4 + "@esbuild/linux-arm": 0.27.4 + "@esbuild/linux-arm64": 0.27.4 + "@esbuild/linux-ia32": 0.27.4 + "@esbuild/linux-loong64": 0.27.4 + "@esbuild/linux-mips64el": 0.27.4 + "@esbuild/linux-ppc64": 0.27.4 + "@esbuild/linux-riscv64": 0.27.4 + "@esbuild/linux-s390x": 0.27.4 + "@esbuild/linux-x64": 0.27.4 + "@esbuild/netbsd-arm64": 0.27.4 + "@esbuild/netbsd-x64": 0.27.4 + "@esbuild/openbsd-arm64": 0.27.4 + "@esbuild/openbsd-x64": 0.27.4 + "@esbuild/openharmony-arm64": 0.27.4 + "@esbuild/sunos-x64": 0.27.4 + "@esbuild/win32-arm64": 0.27.4 + "@esbuild/win32-ia32": 0.27.4 + "@esbuild/win32-x64": 0.27.4 escalade@3.2.0: {} @@ -3746,10 +3742,10 @@ snapshots: dependencies: foreground-child: 3.3.1 jackspeak: 4.2.3 - minimatch: 10.2.0 - minipass: 7.1.2 + minimatch: 10.2.4 + minipass: 7.1.3 package-json-from-dist: 1.0.1 - path-scurry: 2.0.1 + path-scurry: 2.0.2 globby@11.1.0: dependencies: @@ -3827,54 +3823,54 @@ snapshots: dependencies: "@scure/base": 1.1.1 - lightningcss-android-arm64@1.30.2: + lightningcss-android-arm64@1.32.0: optional: true - lightningcss-darwin-arm64@1.30.2: + lightningcss-darwin-arm64@1.32.0: optional: true - lightningcss-darwin-x64@1.30.2: + lightningcss-darwin-x64@1.32.0: optional: true - lightningcss-freebsd-x64@1.30.2: + lightningcss-freebsd-x64@1.32.0: optional: true - lightningcss-linux-arm-gnueabihf@1.30.2: + lightningcss-linux-arm-gnueabihf@1.32.0: optional: true - lightningcss-linux-arm64-gnu@1.30.2: + lightningcss-linux-arm64-gnu@1.32.0: optional: true - lightningcss-linux-arm64-musl@1.30.2: + lightningcss-linux-arm64-musl@1.32.0: optional: true - lightningcss-linux-x64-gnu@1.30.2: + lightningcss-linux-x64-gnu@1.32.0: optional: true - lightningcss-linux-x64-musl@1.30.2: + lightningcss-linux-x64-musl@1.32.0: optional: true - lightningcss-win32-arm64-msvc@1.30.2: + lightningcss-win32-arm64-msvc@1.32.0: optional: true - lightningcss-win32-x64-msvc@1.30.2: + lightningcss-win32-x64-msvc@1.32.0: optional: true - lightningcss@1.30.2: + lightningcss@1.32.0: dependencies: detect-libc: 2.1.2 optionalDependencies: - lightningcss-android-arm64: 1.30.2 - lightningcss-darwin-arm64: 1.30.2 - lightningcss-darwin-x64: 1.30.2 - lightningcss-freebsd-x64: 1.30.2 - lightningcss-linux-arm-gnueabihf: 1.30.2 - lightningcss-linux-arm64-gnu: 1.30.2 - lightningcss-linux-arm64-musl: 1.30.2 - lightningcss-linux-x64-gnu: 1.30.2 - lightningcss-linux-x64-musl: 1.30.2 - lightningcss-win32-arm64-msvc: 1.30.2 - lightningcss-win32-x64-msvc: 1.30.2 + lightningcss-android-arm64: 1.32.0 + lightningcss-darwin-arm64: 1.32.0 + lightningcss-darwin-x64: 1.32.0 + lightningcss-freebsd-x64: 1.32.0 + lightningcss-linux-arm-gnueabihf: 1.32.0 + lightningcss-linux-arm64-gnu: 1.32.0 + lightningcss-linux-arm64-musl: 1.32.0 + lightningcss-linux-x64-gnu: 1.32.0 + lightningcss-linux-x64-musl: 1.32.0 + lightningcss-win32-arm64-msvc: 1.32.0 + lightningcss-win32-x64-msvc: 1.32.0 localforage@1.10.0: dependencies: @@ -3888,7 +3884,7 @@ snapshots: loupe@3.2.1: {} - lru-cache@11.2.6: {} + lru-cache@11.2.7: {} lru-cache@5.1.1: dependencies: @@ -3905,11 +3901,11 @@ snapshots: braces: 3.0.3 picomatch: 2.3.1 - minimatch@10.2.0: + minimatch@10.2.4: dependencies: - brace-expansion: 5.0.2 + brace-expansion: 5.0.4 - minipass@7.1.2: {} + minipass@7.1.3: {} mri@1.2.0: {} @@ -3917,9 +3913,14 @@ snapshots: nanoid@3.3.11: {} - nanoid@5.1.6: {} + nanoid@5.1.7: {} - node-releases@2.0.27: {} + node-releases@2.0.36: {} + + nostr-signer-capacitor-plugin@0.0.5(@capacitor/core@7.6.0): + dependencies: + "@capacitor/core": 7.6.0 + optional: true nostr-tools@2.19.4(typescript@5.9.3): dependencies: @@ -3975,10 +3976,10 @@ snapshots: path-key@3.1.1: {} - path-scurry@2.0.1: + path-scurry@2.0.2: dependencies: - lru-cache: 11.2.6 - minipass: 7.1.2 + lru-cache: 11.2.7 + minipass: 7.1.3 path-type@4.0.0: {} @@ -3994,7 +3995,7 @@ snapshots: pify@4.0.1: {} - postcss@8.5.6: + postcss@8.5.8: dependencies: nanoid: 3.3.11 picocolors: 1.1.1 @@ -4015,7 +4016,7 @@ snapshots: react-error-boundary@5.0.0(react@19.2.4): dependencies: - "@babel/runtime": 7.28.6 + "@babel/runtime": 7.29.2 react: 19.2.4 react-refresh@0.17.0: {} @@ -4038,35 +4039,35 @@ snapshots: glob: 11.1.0 package-json-from-dist: 1.0.1 - rollup@4.57.1: + rollup@4.60.0: dependencies: "@types/estree": 1.0.8 optionalDependencies: - "@rollup/rollup-android-arm-eabi": 4.57.1 - "@rollup/rollup-android-arm64": 4.57.1 - "@rollup/rollup-darwin-arm64": 4.57.1 - "@rollup/rollup-darwin-x64": 4.57.1 - "@rollup/rollup-freebsd-arm64": 4.57.1 - "@rollup/rollup-freebsd-x64": 4.57.1 - "@rollup/rollup-linux-arm-gnueabihf": 4.57.1 - "@rollup/rollup-linux-arm-musleabihf": 4.57.1 - "@rollup/rollup-linux-arm64-gnu": 4.57.1 - "@rollup/rollup-linux-arm64-musl": 4.57.1 - "@rollup/rollup-linux-loong64-gnu": 4.57.1 - "@rollup/rollup-linux-loong64-musl": 4.57.1 - "@rollup/rollup-linux-ppc64-gnu": 4.57.1 - "@rollup/rollup-linux-ppc64-musl": 4.57.1 - "@rollup/rollup-linux-riscv64-gnu": 4.57.1 - "@rollup/rollup-linux-riscv64-musl": 4.57.1 - "@rollup/rollup-linux-s390x-gnu": 4.57.1 - "@rollup/rollup-linux-x64-gnu": 4.57.1 - "@rollup/rollup-linux-x64-musl": 4.57.1 - "@rollup/rollup-openbsd-x64": 4.57.1 - "@rollup/rollup-openharmony-arm64": 4.57.1 - "@rollup/rollup-win32-arm64-msvc": 4.57.1 - "@rollup/rollup-win32-ia32-msvc": 4.57.1 - "@rollup/rollup-win32-x64-gnu": 4.57.1 - "@rollup/rollup-win32-x64-msvc": 4.57.1 + "@rollup/rollup-android-arm-eabi": 4.60.0 + "@rollup/rollup-android-arm64": 4.60.0 + "@rollup/rollup-darwin-arm64": 4.60.0 + "@rollup/rollup-darwin-x64": 4.60.0 + "@rollup/rollup-freebsd-arm64": 4.60.0 + "@rollup/rollup-freebsd-x64": 4.60.0 + "@rollup/rollup-linux-arm-gnueabihf": 4.60.0 + "@rollup/rollup-linux-arm-musleabihf": 4.60.0 + "@rollup/rollup-linux-arm64-gnu": 4.60.0 + "@rollup/rollup-linux-arm64-musl": 4.60.0 + "@rollup/rollup-linux-loong64-gnu": 4.60.0 + "@rollup/rollup-linux-loong64-musl": 4.60.0 + "@rollup/rollup-linux-ppc64-gnu": 4.60.0 + "@rollup/rollup-linux-ppc64-musl": 4.60.0 + "@rollup/rollup-linux-riscv64-gnu": 4.60.0 + "@rollup/rollup-linux-riscv64-musl": 4.60.0 + "@rollup/rollup-linux-s390x-gnu": 4.60.0 + "@rollup/rollup-linux-x64-gnu": 4.60.0 + "@rollup/rollup-linux-x64-musl": 4.60.0 + "@rollup/rollup-openbsd-x64": 4.60.0 + "@rollup/rollup-openharmony-arm64": 4.60.0 + "@rollup/rollup-win32-arm64-msvc": 4.60.0 + "@rollup/rollup-win32-ia32-msvc": 4.60.0 + "@rollup/rollup-win32-x64-gnu": 4.60.0 + "@rollup/rollup-win32-x64-msvc": 4.60.0 fsevents: 2.3.3 run-parallel@1.2.0: @@ -4120,7 +4121,7 @@ snapshots: dependencies: js-tokens: 9.0.1 - tailwindcss@4.1.18: {} + tailwindcss@4.2.2: {} tapable@2.3.0: {} @@ -4168,13 +4169,13 @@ snapshots: escalade: 3.2.0 picocolors: 1.1.1 - vite-node@3.2.4(@types/node@24.11.0)(jiti@2.6.1)(lightningcss@1.30.2): + vite-node@3.2.4(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.32.0): dependencies: cac: 6.7.14 debug: 4.4.3 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 7.3.1(@types/node@24.11.0)(jiti@2.6.1)(lightningcss@1.30.2) + vite: 7.3.1(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.32.0) transitivePeerDependencies: - "@types/node" - jiti @@ -4189,39 +4190,25 @@ snapshots: - tsx - yaml - vite@7.3.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.30.2): - dependencies: - esbuild: 0.27.3 - fdir: 6.5.0(picomatch@4.0.3) - picomatch: 4.0.3 - postcss: 8.5.6 - rollup: 4.57.1 - tinyglobby: 0.2.15 - optionalDependencies: - "@types/node": 24.10.13 - fsevents: 2.3.3 - jiti: 2.6.1 - lightningcss: 1.30.2 - - vite@7.3.1(@types/node@24.11.0)(jiti@2.6.1)(lightningcss@1.30.2): + vite@7.3.1(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.32.0): dependencies: - esbuild: 0.27.3 + esbuild: 0.27.4 fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 - postcss: 8.5.6 - rollup: 4.57.1 + postcss: 8.5.8 + rollup: 4.60.0 tinyglobby: 0.2.15 optionalDependencies: - "@types/node": 24.11.0 + "@types/node": 24.12.0 fsevents: 2.3.3 jiti: 2.6.1 - lightningcss: 1.30.2 + lightningcss: 1.32.0 - vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.11.0)(jiti@2.6.1)(lightningcss@1.30.2): + vitest@3.2.4(@types/debug@4.1.13)(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.32.0): dependencies: "@types/chai": 5.2.3 "@vitest/expect": 3.2.4 - "@vitest/mocker": 3.2.4(vite@7.3.1(@types/node@24.11.0)(jiti@2.6.1)(lightningcss@1.30.2)) + "@vitest/mocker": 3.2.4(vite@7.3.1(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.32.0)) "@vitest/pretty-format": 3.2.4 "@vitest/runner": 3.2.4 "@vitest/snapshot": 3.2.4 @@ -4239,12 +4226,12 @@ snapshots: tinyglobby: 0.2.15 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 7.3.1(@types/node@24.11.0)(jiti@2.6.1)(lightningcss@1.30.2) - vite-node: 3.2.4(@types/node@24.11.0)(jiti@2.6.1)(lightningcss@1.30.2) + vite: 7.3.1(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.32.0) + vite-node: 3.2.4(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.32.0) why-is-node-running: 2.3.0 optionalDependencies: - "@types/debug": 4.1.12 - "@types/node": 24.11.0 + "@types/debug": 4.1.13 + "@types/node": 24.12.0 transitivePeerDependencies: - jiti - less diff --git a/references/ts-mls b/references/ts-mls index d4190e6d..f783b16e 160000 --- a/references/ts-mls +++ b/references/ts-mls @@ -1 +1 @@ -Subproject commit d4190e6d200087cd6becc68c956ea932493d3a0b +Subproject commit f783b16e941c80d0a7b2c33fa9e85de1160a5a65 diff --git a/src/__tests__/epoch-authenticator-drift.test.ts b/src/__tests__/epoch-authenticator-drift.test.ts new file mode 100644 index 00000000..818afcac --- /dev/null +++ b/src/__tests__/epoch-authenticator-drift.test.ts @@ -0,0 +1,551 @@ +import { PrivateKeyAccount } from "applesauce-accounts/accounts"; +import { + unlockGiftWrap, + type Rumor, +} from "applesauce-common/helpers/gift-wrap"; +import { bytesToHex } from "@noble/hashes/utils.js"; +import { nodeTypes } from "ts-mls/nodeType.js"; +import { + directPath, + leafToNodeIndex, + leafWidth, + toLeafIndex, +} from "ts-mls/treemath.js"; +import { describe, expect, it } from "vitest"; + +import { MarmotClient } from "../client/marmot-client.js"; +import { + KEY_PACKAGE_KIND, + WELCOME_EPOCH_AUTHENTICATOR_TAG, +} from "../core/protocol.js"; +import { getWelcomeEpochAuthenticator } from "../core/welcome.js"; +import { KeyPackageStore } from "../store/key-package-store.js"; +import { KeyValueGroupStateBackend } from "../store/adapters/key-value-group-state-backend.js"; +import { GROUP_EVENT_KIND } from "../core/protocol.js"; +import { MemoryBackend } from "./ingest-commit-race.test.js"; +import { ScriptedMockNetwork } from "./helpers/scripted-mock-network.js"; + +async function createClient( + account: PrivateKeyAccount, + network: ScriptedMockNetwork, +): Promise { + return new MarmotClient({ + groupStateBackend: new KeyValueGroupStateBackend(new MemoryBackend()), + keyPackageStore: new KeyPackageStore(new MemoryBackend()), + signer: account.signer, + network, + }); +} + +async function getGiftWrapFor( + network: ScriptedMockNetwork, + recipientPubkey: string, +): Promise { + const giftwraps = await network.request(["wss://mock-inbox.test"], { + kinds: [1059], + "#p": [recipientPubkey], + }); + return giftwraps[giftwraps.length - 1]; +} + +async function setupTwoAdminGroup(): Promise<{ + network: ScriptedMockNetwork; + adminA: PrivateKeyAccount; + adminB: PrivateKeyAccount; + inviteeC: PrivateKeyAccount; + clientA: MarmotClient; + clientB: MarmotClient; + clientC: MarmotClient; + groupA: Awaited>; + groupB: Awaited>; + inviteeCPubkey: string; +}> { + const network = new ScriptedMockNetwork(); + + const adminA = PrivateKeyAccount.generateNew(); + const adminB = PrivateKeyAccount.generateNew(); + const inviteeC = PrivateKeyAccount.generateNew(); + + const clientA = await createClient(adminA, network); + const clientB = await createClient(adminB, network); + const clientC = await createClient(inviteeC, network); + + const adminBPubkey = await adminB.signer.getPublicKey(); + const inviteeCPubkey = await inviteeC.signer.getPublicKey(); + + await clientB.keyPackages.create({ relays: ["wss://mock-relay.test"] }); + network.stepAll(); + + const groupA = await clientA.createGroup("epoch-authenticator-drift", { + adminPubkeys: [adminBPubkey], + relays: ["wss://mock-relay.test"], + }); + + const keyPkgB = network.events.find( + (event) => event.kind === KEY_PACKAGE_KIND && event.pubkey === adminBPubkey, + ); + if (!keyPkgB) { + throw new Error( + "Epoch authenticator setup failed: missing key package for admin B", + ); + } + + await groupA.inviteByKeyPackageEvent(keyPkgB); + network.stepAll(); + + const giftwrapB = await getGiftWrapFor(network, adminBPubkey); + if (!giftwrapB) { + throw new Error( + "Epoch authenticator setup failed: missing welcome for admin B", + ); + } + + const rumorB = (await unlockGiftWrap(giftwrapB, adminB.signer)) as Rumor; + const { group: groupB } = await clientB.joinGroupFromWelcome({ + welcomeRumor: rumorB, + }); + + return { + network, + adminA, + adminB, + inviteeC, + clientA, + clientB, + clientC, + groupA, + groupB, + inviteeCPubkey, + }; +} + +function getEpochAuthenticatorHex(group: { + state: { keySchedule: { epochAuthenticator: Uint8Array } }; +}): string { + return bytesToHex(group.state.keySchedule.epochAuthenticator); +} + +function withEpochAuthenticatorTag( + rumor: Rumor, + epochAuthenticatorHex: string, +): Rumor { + const tags = rumor.tags.filter( + (tag) => tag[0] !== WELCOME_EPOCH_AUTHENTICATOR_TAG, + ); + tags.push([WELCOME_EPOCH_AUTHENTICATOR_TAG, epochAuthenticatorHex]); + + return { + ...rumor, + tags, + }; +} + +function forceDirectPathUnmergedLeafDivergence(tree: any[]): boolean { + let mutated = false; + for (let nodeIndex = 0; nodeIndex < tree.length && !mutated; nodeIndex++) { + const node = tree[nodeIndex]; + if (node?.nodeType !== nodeTypes.leaf) continue; + + const leafIndex = toLeafIndex(Math.floor(nodeIndex / 2)); + const path = directPath(leafToNodeIndex(leafIndex), leafWidth(tree.length)); + if (path.length < 2) continue; + + const firstParentIndex = path[0]!; + const secondParentIndex = path[1]!; + + if (tree[firstParentIndex]?.nodeType !== nodeTypes.parent) { + tree[firstParentIndex] = { + nodeType: nodeTypes.parent, + parent: { + hpkePublicKey: new Uint8Array(32).fill((firstParentIndex % 251) + 1), + parentHash: new Uint8Array(), + unmergedLeaves: [], + }, + }; + } + if (tree[secondParentIndex]?.nodeType !== nodeTypes.parent) { + tree[secondParentIndex] = { + nodeType: nodeTypes.parent, + parent: { + hpkePublicKey: new Uint8Array(32).fill((secondParentIndex % 251) + 2), + parentHash: new Uint8Array(), + unmergedLeaves: [], + }, + }; + } + + const firstParent = tree[firstParentIndex]; + const secondParent = tree[secondParentIndex]; + if (firstParent?.nodeType !== nodeTypes.parent) continue; + if (secondParent?.nodeType !== nodeTypes.parent) continue; + + firstParent.parent.unmergedLeaves = [leafIndex]; + secondParent.parent.unmergedLeaves = []; + mutated = true; + } + + return mutated; +} + +describe("epoch authenticator drift diagnostics", () => { + it("matches the welcome tag and inviter state after a successful join", async () => { + const { network, clientC, groupA, inviteeC, inviteeCPubkey } = + await setupTwoAdminGroup(); + + await clientC.keyPackages.create({ relays: ["wss://mock-relay.test"] }); + network.stepAll(); + + const keyPkgC = network.events.find( + (event) => + event.kind === KEY_PACKAGE_KIND && event.pubkey === inviteeCPubkey, + ); + if (!keyPkgC) { + throw new Error( + "Baseline join failed: missing key package for invitee C", + ); + } + + await groupA.inviteByKeyPackageEvent(keyPkgC); + network.stepAll(); + + const giftwrapC = await getGiftWrapFor(network, inviteeCPubkey); + if (!giftwrapC) { + throw new Error("Baseline join failed: missing welcome for invitee C"); + } + + const rumorC = (await unlockGiftWrap(giftwrapC, inviteeC.signer)) as Rumor; + const taggedEpochAuthenticator = getWelcomeEpochAuthenticator(rumorC); + + expect(taggedEpochAuthenticator).toBeDefined(); + expect(bytesToHex(taggedEpochAuthenticator!)).toBe( + getEpochAuthenticatorHex(groupA), + ); + + const { group: joinedGroup } = await clientC.joinGroupFromWelcome({ + welcomeRumor: rumorC, + }); + + expect(joinedGroup.state.groupContext.epoch).toBe( + groupA.state.groupContext.epoch, + ); + expect(getEpochAuthenticatorHex(joinedGroup)).toBe( + getEpochAuthenticatorHex(groupA), + ); + expect(bytesToHex(joinedGroup.state.groupContext.treeHash)).toBe( + bytesToHex(groupA.state.groupContext.treeHash), + ); + }); + + it("tracks the welcome-producing branch under competing same-window commits", async () => { + const { network, clientC, groupA, groupB, inviteeC, inviteeCPubkey } = + await setupTwoAdminGroup(); + + await clientC.keyPackages.create({ relays: ["wss://mock-relay.test"] }); + network.stepAll(); + + const keyPkgC = network.events.find( + (event) => + event.kind === KEY_PACKAGE_KIND && event.pubkey === inviteeCPubkey, + ); + if (!keyPkgC) { + throw new Error("Branch race failed: missing key package for invitee C"); + } + + const publishCut = network.events.length; + await groupA.inviteByKeyPackageEvent(keyPkgC); + const branchAuthenticator = getEpochAuthenticatorHex(groupA); + await groupB.selfUpdate(); + const competingAuthenticator = getEpochAuthenticatorHex(groupB); + + const raceEvents = network.events.slice(publishCut); + const welcomeC = raceEvents.find( + (event) => + event.kind === 1059 && + event.tags.some((tag) => tag[0] === "p" && tag[1] === inviteeCPubkey), + ); + const commits = raceEvents.filter( + (event) => event.kind === GROUP_EVENT_KIND, + ); + + if (!welcomeC || commits.length < 2) { + throw new Error("Branch race failed: missing welcome/commit race events"); + } + + network.stepWhere((event) => event.id === commits[1]!.id); + network.stepWhere((event) => event.id === welcomeC.id); + network.stepWhere((event) => event.id === commits[0]!.id); + + const giftwrapC = await getGiftWrapFor(network, inviteeCPubkey); + if (!giftwrapC) { + throw new Error( + "Branch race failed: missing delivered welcome for invitee C", + ); + } + + const rumorC = (await unlockGiftWrap(giftwrapC, inviteeC.signer)) as Rumor; + const taggedEpochAuthenticator = getWelcomeEpochAuthenticator(rumorC); + + expect(bytesToHex(taggedEpochAuthenticator!)).toBe(branchAuthenticator); + expect(branchAuthenticator).not.toBe(competingAuthenticator); + + const { group: joinedGroup } = await clientC.joinGroupFromWelcome({ + welcomeRumor: rumorC, + }); + + expect(getEpochAuthenticatorHex(joinedGroup)).toBe(branchAuthenticator); + expect(getEpochAuthenticatorHex(joinedGroup)).not.toBe( + competingAuthenticator, + ); + }); + + it("detects a forged epoch authenticator claim after an otherwise valid join", async () => { + const { network, clientC, groupA, inviteeC, inviteeCPubkey } = + await setupTwoAdminGroup(); + + await clientC.keyPackages.create({ relays: ["wss://mock-relay.test"] }); + network.stepAll(); + + const keyPkgC = network.events.find( + (event) => + event.kind === KEY_PACKAGE_KIND && event.pubkey === inviteeCPubkey, + ); + if (!keyPkgC) { + throw new Error( + "Forged-ea test failed: missing key package for invitee C", + ); + } + + await groupA.inviteByKeyPackageEvent(keyPkgC); + network.stepAll(); + + const giftwrapC = await getGiftWrapFor(network, inviteeCPubkey); + if (!giftwrapC) { + throw new Error("Forged-ea test failed: missing welcome for invitee C"); + } + + const originalRumor = (await unlockGiftWrap( + giftwrapC, + inviteeC.signer, + )) as Rumor; + const honestEpochAuthenticator = + getWelcomeEpochAuthenticator(originalRumor); + if (!honestEpochAuthenticator) { + throw new Error( + "Forged-ea test failed: welcome is missing honest ea tag", + ); + } + + const forgedHex = `${"ab".repeat(31)}cd`; + expect(forgedHex).not.toBe(bytesToHex(honestEpochAuthenticator)); + + const forgedRumor = withEpochAuthenticatorTag(originalRumor, forgedHex); + + expect(bytesToHex(getWelcomeEpochAuthenticator(forgedRumor)!)).toBe( + forgedHex, + ); + + const { group: joinedGroup } = await clientC.joinGroupFromWelcome({ + welcomeRumor: forgedRumor, + }); + + expect(getEpochAuthenticatorHex(joinedGroup)).toBe( + getEpochAuthenticatorHex(groupA), + ); + expect(getEpochAuthenticatorHex(joinedGroup)).not.toBe(forgedHex); + expect(getEpochAuthenticatorHex(joinedGroup)).toBe( + bytesToHex(honestEpochAuthenticator), + ); + }); + + it("detects when a welcome carries the competing branch epoch authenticator", async () => { + const { network, clientC, groupA, groupB, inviteeC, inviteeCPubkey } = + await setupTwoAdminGroup(); + + await clientC.keyPackages.create({ relays: ["wss://mock-relay.test"] }); + network.stepAll(); + + const keyPkgC = network.events.find( + (event) => + event.kind === KEY_PACKAGE_KIND && event.pubkey === inviteeCPubkey, + ); + if (!keyPkgC) { + throw new Error( + "Cross-branch oracle test failed: missing key package for invitee C", + ); + } + + const publishCut = network.events.length; + await groupA.inviteByKeyPackageEvent(keyPkgC); + const welcomeBranchAuthenticator = getEpochAuthenticatorHex(groupA); + await groupB.selfUpdate(); + const competingBranchAuthenticator = getEpochAuthenticatorHex(groupB); + + expect(welcomeBranchAuthenticator).not.toBe(competingBranchAuthenticator); + + const raceEvents = network.events.slice(publishCut); + const welcomeC = raceEvents.find( + (event) => + event.kind === 1059 && + event.tags.some((tag) => tag[0] === "p" && tag[1] === inviteeCPubkey), + ); + const commits = raceEvents.filter( + (event) => event.kind === GROUP_EVENT_KIND, + ); + + if (!welcomeC || commits.length < 2) { + throw new Error( + "Cross-branch oracle test failed: missing welcome/commit race events", + ); + } + + network.stepWhere((event) => event.id === commits[1]!.id); + network.stepWhere((event) => event.id === welcomeC.id); + network.stepWhere((event) => event.id === commits[0]!.id); + + const giftwrapC = await getGiftWrapFor(network, inviteeCPubkey); + if (!giftwrapC) { + throw new Error( + "Cross-branch oracle test failed: missing delivered welcome for invitee C", + ); + } + + const originalRumor = (await unlockGiftWrap( + giftwrapC, + inviteeC.signer, + )) as Rumor; + const forgedRumor = withEpochAuthenticatorTag( + originalRumor, + competingBranchAuthenticator, + ); + + expect(bytesToHex(getWelcomeEpochAuthenticator(forgedRumor)!)).toBe( + competingBranchAuthenticator, + ); + + const { group: joinedGroup } = await clientC.joinGroupFromWelcome({ + welcomeRumor: forgedRumor, + }); + + expect(getEpochAuthenticatorHex(joinedGroup)).toBe( + welcomeBranchAuthenticator, + ); + expect(getEpochAuthenticatorHex(joinedGroup)).not.toBe( + competingBranchAuthenticator, + ); + }); + + it("does not treat the welcome tag as sufficient when join fails before valid state construction", async () => { + const signature = + "non-blank intermediate node must list leaf node in its unmerged_leaves"; + + const network = new ScriptedMockNetwork(); + + const adminA = PrivateKeyAccount.generateNew(); + const adminB = PrivateKeyAccount.generateNew(); + const buggyC = PrivateKeyAccount.generateNew(); + const inviteeD = PrivateKeyAccount.generateNew(); + + const clientA = await createClient(adminA, network); + const clientB = await createClient(adminB, network); + const clientC = await createClient(buggyC, network); + const clientD = await createClient(inviteeD, network); + + const adminBPubkey = await adminB.signer.getPublicKey(); + const buggyCPubkey = await buggyC.signer.getPublicKey(); + const inviteeDPubkey = await inviteeD.signer.getPublicKey(); + + await clientB.keyPackages.create({ relays: ["wss://mock-relay.test"] }); + await clientC.keyPackages.create({ relays: ["wss://mock-relay.test"] }); + network.stepAll(); + + const groupA = await clientA.createGroup("epoch-authenticator-r5", { + adminPubkeys: [adminBPubkey, buggyCPubkey], + relays: ["wss://mock-relay.test"], + }); + + const keyPkgB = network.events.find( + (event) => + event.kind === KEY_PACKAGE_KIND && event.pubkey === adminBPubkey, + ); + const keyPkgC = network.events.find( + (event) => + event.kind === KEY_PACKAGE_KIND && event.pubkey === buggyCPubkey, + ); + if (!keyPkgB || !keyPkgC) { + throw new Error( + "Failure-boundary setup failed: missing admin key package events", + ); + } + + await groupA.inviteByKeyPackageEvent(keyPkgB); + await groupA.inviteByKeyPackageEvent(keyPkgC); + network.stepAll(); + + const giftwrapB = await getGiftWrapFor(network, adminBPubkey); + const giftwrapC = await getGiftWrapFor(network, buggyCPubkey); + if (!giftwrapB || !giftwrapC) { + throw new Error( + "Failure-boundary setup failed: missing admin welcome giftwraps", + ); + } + + const rumorB = (await unlockGiftWrap(giftwrapB, adminB.signer)) as Rumor; + const rumorC = (await unlockGiftWrap(giftwrapC, buggyC.signer)) as Rumor; + + const { group: groupB } = await clientB.joinGroupFromWelcome({ + welcomeRumor: rumorB, + }); + const { group: groupC } = await clientC.joinGroupFromWelcome({ + welcomeRumor: rumorC, + }); + + await groupC.selfUpdate(); + await groupA.selfUpdate(); + await groupB.selfUpdate(); + + const mutated = forceDirectPathUnmergedLeafDivergence( + groupC.state.ratchetTree, + ); + + if (!mutated) { + throw new Error( + "Failure-boundary setup failed: could not mutate buggy tree", + ); + } + + await clientD.keyPackages.create({ relays: ["wss://mock-relay.test"] }); + network.stepAll(); + + const keyPkgD = network.events.find( + (event) => + event.kind === KEY_PACKAGE_KIND && event.pubkey === inviteeDPubkey, + ); + if (!keyPkgD) { + throw new Error( + "Failure-boundary setup failed: missing invitee D key package", + ); + } + + await groupC.inviteByKeyPackageEvent(keyPkgD); + network.stepAll(); + + const giftwrapD = await getGiftWrapFor(network, inviteeDPubkey); + if (!giftwrapD) { + throw new Error( + "Failure-boundary setup failed: missing invitee D welcome", + ); + } + + const rumorD = (await unlockGiftWrap(giftwrapD, inviteeD.signer)) as Rumor; + const taggedEpochAuthenticator = getWelcomeEpochAuthenticator(rumorD); + + expect(taggedEpochAuthenticator).toBeDefined(); + expect(bytesToHex(taggedEpochAuthenticator!)).toBe( + getEpochAuthenticatorHex(groupC), + ); + + await expect( + clientD.joinGroupFromWelcome({ welcomeRumor: rumorD }), + ).rejects.toThrow(signature); + }); +}); diff --git a/src/__tests__/helpers/scripted-mock-network.ts b/src/__tests__/helpers/scripted-mock-network.ts new file mode 100644 index 00000000..c48017d4 --- /dev/null +++ b/src/__tests__/helpers/scripted-mock-network.ts @@ -0,0 +1,142 @@ +import type { NostrEvent } from "applesauce-core/helpers/event"; +import type { Filter } from "applesauce-core/helpers/filter"; + +import type { + PublishResponse, + Subscribable, + Unsubscribable, +} from "../../client/nostr-interface.js"; +import { MockNetwork } from "./mock-network.js"; + +type StoredEvent = { + event: NostrEvent; + relays: string[]; + visible: boolean; + seq: number; +}; + +/** + * Deterministic network test harness with explicit visibility control. + * + * Publish always succeeds immediately, but events stay hidden until released via + * step/stepWhere/stepAll. This lets tests reproduce race windows deterministically. + */ +export class ScriptedMockNetwork extends MockNetwork { + private seq = 0; + private readonly stored: StoredEvent[] = []; + + override async publish( + relays: string[], + event: NostrEvent, + ): Promise> { + this.stored.push({ + event, + relays: [...relays], + visible: false, + seq: this.seq++, + }); + this.events.push(event); + + const result: Record = {}; + for (const relay of relays) { + result[relay] = { from: relay, ok: true }; + } + return result; + } + + step(count = 1): number { + let released = 0; + for (const item of this.stored) { + if (released >= count) break; + if (item.visible) continue; + item.visible = true; + released++; + } + return released; + } + + stepWhere(predicate: (event: NostrEvent) => boolean, count = 1): number { + let released = 0; + for (const item of this.stored) { + if (released >= count) break; + if (item.visible) continue; + if (!predicate(item.event)) continue; + item.visible = true; + released++; + } + return released; + } + + stepAll(): number { + return this.step(Number.MAX_SAFE_INTEGER); + } + + override async request( + relays: string[], + filters: Filter | Filter[], + ): Promise { + const filterArray = Array.isArray(filters) ? filters : [filters]; + const relaySet = new Set(relays); + + return this.stored + .filter((item) => item.visible) + .filter((item) => item.relays.some((r) => relaySet.has(r))) + .filter((item) => + filterArray.some((filter) => this.matchesFilter(item.event, filter)), + ) + .sort((a, b) => a.seq - b.seq) + .map((item) => item.event); + } + + override subscription( + relays: string[], + filters: Filter | Filter[], + ): Subscribable { + const relaySet = new Set(relays); + const filterArray = Array.isArray(filters) ? filters : [filters]; + const matchingEvents = this.stored + .filter((item) => item.visible) + .filter((item) => item.relays.some((r) => relaySet.has(r))) + .filter((item) => + filterArray.some((filter) => this.matchesFilter(item.event, filter)), + ) + .map((item) => item.event); + + return { + subscribe: (observer: any): Unsubscribable => { + if (observer.next && matchingEvents.length > 0) { + observer.next(matchingEvents); + } + return { unsubscribe: () => undefined }; + }, + }; + } + + override clear(): void { + super.clear(); + this.stored.length = 0; + this.seq = 0; + } + + private matchesFilter(event: NostrEvent, filter: Filter): boolean { + if (filter.kinds && !filter.kinds.includes(event.kind)) return false; + if (filter.authors && !filter.authors.includes(event.pubkey)) return false; + + if (!this.matchesTagFilter(event, "#h", filter["#h"])) return false; + if (!this.matchesTagFilter(event, "#e", filter["#e"])) return false; + if (!this.matchesTagFilter(event, "#p", filter["#p"])) return false; + + return true; + } + + private matchesTagFilter( + event: NostrEvent, + filterKey: "#h" | "#e" | "#p", + values: string[] | string | undefined, + ): boolean { + if (!values) return true; + const wanted = Array.isArray(values) ? values : [values]; + const tagName = filterKey[1]; + return event.tags.some((t) => t[0] === tagName && wanted.includes(t[1])); + } +} diff --git a/src/__tests__/invite-join-drift-repro.test.ts b/src/__tests__/invite-join-drift-repro.test.ts new file mode 100644 index 00000000..936e8e49 --- /dev/null +++ b/src/__tests__/invite-join-drift-repro.test.ts @@ -0,0 +1,1304 @@ +import { PrivateKeyAccount } from "applesauce-accounts/accounts"; +import { + unlockGiftWrap, + type Rumor, +} from "applesauce-common/helpers/gift-wrap"; +import { + defaultCryptoProvider, + defaultProposalTypes, + getCiphersuiteImpl, + unsafeTestingAuthenticationService, +} from "ts-mls"; +import { validateRatchetTree } from "ts-mls/clientState.js"; +import { defaultLifetimeConfig } from "ts-mls/lifetimeConfig.js"; +import { nodeTypes } from "ts-mls/nodeType.js"; +import { + leafWidth, + directPath, + leafToNodeIndex, + toLeafIndex, +} from "ts-mls/treemath.js"; +import { describe, expect, it } from "vitest"; + +import { MarmotClient } from "../client/marmot-client.js"; +import { createCredential } from "../core/credential.js"; +import { createSimpleGroup } from "../core/group.js"; +import { generateKeyPackage } from "../core/key-package.js"; +import { GROUP_EVENT_KIND, KEY_PACKAGE_KIND } from "../core/protocol.js"; +import { readWelcomeGroupInfo } from "../core/welcome.js"; +import { KeyPackageStore } from "../store/key-package-store.js"; +import { KeyValueGroupStateBackend } from "../store/adapters/key-value-group-state-backend.js"; +import { MemoryBackend } from "./ingest-commit-race.test.js"; +import { ScriptedMockNetwork } from "./helpers/scripted-mock-network.js"; + +type Variant = "V1" | "V2" | "V3"; + +type RunResult = { ok: boolean; error?: string }; + +async function createClient( + account: PrivateKeyAccount, + network: ScriptedMockNetwork, +) { + return new MarmotClient({ + groupStateBackend: new KeyValueGroupStateBackend(new MemoryBackend()), + keyPackageStore: new KeyPackageStore(new MemoryBackend()), + signer: account.signer, + network, + }); +} + +async function getGiftWrapFor( + network: ScriptedMockNetwork, + recipientPubkey: string, +) { + const giftwraps = await network.request(["wss://mock-inbox.test"], { + kinds: [1059], + "#p": [recipientPubkey], + }); + return giftwraps[giftwraps.length - 1]; +} + +async function drainGroupIngestFromNetwork( + group: { + ingest: (events: any[]) => AsyncGenerator; + groupData: { nostrGroupId: Uint8Array }; + }, + network: ScriptedMockNetwork, +) { + const groupEvents = await network.request(["wss://mock-relay.test"], { + kinds: [GROUP_EVENT_KIND], + "#h": [Buffer.from(group.groupData.nostrGroupId).toString("hex")], + }); + for await (const _ of group.ingest(groupEvents)) { + // drain iterator + } +} + +async function setupTwoAdminGroup() { + const network = new ScriptedMockNetwork(); + + const adminA = PrivateKeyAccount.generateNew(); + const adminB = PrivateKeyAccount.generateNew(); + const inviteeC = PrivateKeyAccount.generateNew(); + + const clientA = await createClient(adminA, network); + const clientB = await createClient(adminB, network); + const clientC = await createClient(inviteeC, network); + + const adminBPubkey = await adminB.signer.getPublicKey(); + const inviteeCPubkey = await inviteeC.signer.getPublicKey(); + + await clientB.keyPackages.create({ relays: ["wss://mock-relay.test"] }); + network.stepAll(); + + const groupA = await clientA.createGroup("drift-r1", { + adminPubkeys: [adminBPubkey], + relays: ["wss://mock-relay.test"], + }); + + const keyPkgB = network.events.find( + (e) => e.kind === KEY_PACKAGE_KIND && e.pubkey === adminBPubkey, + ); + if (!keyPkgB) + throw new Error("R1 setup failed: missing key package event for admin B"); + + await groupA.inviteByKeyPackageEvent(keyPkgB); + network.stepAll(); + + const giftwrapB = await getGiftWrapFor(network, adminBPubkey); + if (!giftwrapB) + throw new Error("R1 setup failed: missing welcome giftwrap for admin B"); + + const rumorB = (await unlockGiftWrap(giftwrapB, adminB.signer)) as Rumor; + const { group: groupB } = await clientB.joinGroupFromWelcome({ + welcomeRumor: rumorB, + }); + + return { + network, + adminA, + adminB, + inviteeC, + clientA, + clientB, + clientC, + groupA, + groupB, + inviteeCPubkey, + }; +} + +function treeFingerprint(state: { + ratchetTree: any[]; + groupContext: { epoch: bigint }; +}): string { + const parents = state.ratchetTree + .map((node, i) => { + if (!node || node.nodeType !== nodeTypes.parent) return null; + const leaves = node.parent.unmergedLeaves ?? []; + return `${i}[${leaves.map((x: number) => Number(x)).join(",")}]`; + }) + .filter(Boolean) + .join("|"); + return `epoch=${state.groupContext.epoch};parents=${parents}`; +} + +async function inspectWelcomeTree(options: { + client: MarmotClient; + welcomeRumor: Rumor; +}): Promise { + const ciphersuiteImpl = await getCiphersuiteImpl( + "MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519", + defaultCryptoProvider, + ); + const keyPackages = await options.client.keyPackages.list(); + for (const pkg of keyPackages) { + const full = await options.client.keyPackages.get(pkg.keyPackageRef); + if (!full?.privatePackage) continue; + try { + const groupInfo = await readWelcomeGroupInfo({ + welcome: options.welcomeRumor, + keyPackage: { + publicPackage: full.publicPackage, + privatePackage: full.privatePackage, + }, + ciphersuiteImpl, + }); + return `welcome-epoch=${groupInfo.groupContext.epoch};extensions=${groupInfo.groupContext.extensions.length}`; + } catch { + // try next key package + } + } + return "welcome-inspection=unavailable"; +} + +async function runR1(variant: Variant): Promise { + const { + network, + clientA, + clientB, + clientC, + groupA, + groupB, + inviteeC, + inviteeCPubkey, + } = await setupTwoAdminGroup(); + + await clientC.keyPackages.create({ relays: ["wss://mock-relay.test"] }); + network.stepAll(); + + const keyPkgC = network.events.find( + (e) => e.kind === KEY_PACKAGE_KIND && e.pubkey === inviteeCPubkey, + ); + if (!keyPkgC) + throw new Error("R1 setup failed: missing key package event for invitee C"); + + const publishCut = network.events.length; + + await groupA.inviteByKeyPackageEvent(keyPkgC); + await groupB.selfUpdate(); + + const raceEvents = network.events.slice(publishCut); + const raceCommits = raceEvents.filter((e) => e.kind === GROUP_EVENT_KIND); + const commitA = raceCommits[0]; + const commitB = raceCommits[1]; + const welcomeC = raceEvents.find( + (e) => + e.kind === 1059 && + e.tags.some((t) => t[0] === "p" && t[1] === inviteeCPubkey), + ); + + if (!commitA || !commitB || !welcomeC) { + throw new Error( + `R1 setup failed: race window events not found. commits=${raceCommits.length} adminACommit=${Boolean( + commitA, + )} secondCommit=${Boolean(commitB)} welcomeC=${Boolean(welcomeC)} events=${JSON.stringify( + raceEvents.map((e) => ({ + id: e.id, + kind: e.kind, + pubkey: e.pubkey.slice(0, 16), + p: e.tags.filter((t) => t[0] === "p").map((t) => t[1].slice(0, 16)), + })), + )}`, + ); + } + + if (variant === "V1") { + network.stepWhere((e) => e.id === commitA.id); + network.stepWhere((e) => e.id === welcomeC.id); + network.stepWhere((e) => e.id === commitB.id); + } else if (variant === "V2") { + network.stepWhere((e) => e.id === commitB.id); + network.stepWhere((e) => e.id === welcomeC.id); + network.stepWhere((e) => e.id === commitA.id); + } else { + network.stepWhere((e) => e.id === welcomeC.id); + network.stepWhere((e) => e.id === commitB.id); + network.stepWhere((e) => e.id === commitA.id); + } + + const giftwrapC = await getGiftWrapFor(network, inviteeCPubkey); + if (!giftwrapC) { + throw new Error(`R1 ${variant}: welcome was not visible to invitee C`); + } + + const rumorC = (await unlockGiftWrap(giftwrapC, inviteeC.signer)) as Rumor; + const fpA = treeFingerprint(groupA.state as any); + const fpB = treeFingerprint(groupB.state as any); + const welcomeFp = await inspectWelcomeTree({ + client: clientC as any, + welcomeRumor: rumorC, + }); + + try { + await clientC.joinGroupFromWelcome({ welcomeRumor: rumorC }); + return { ok: true }; + } catch (error) { + return { + ok: false, + error: `${error instanceof Error ? error.message : String(error)} | ${fpA} | ${fpB} | ${welcomeFp}`, + }; + } +} + +async function runR2(staleDepth: 1 | 2): Promise { + const { + network, + clientA, + clientB, + clientC, + groupA, + groupB, + inviteeC, + inviteeCPubkey, + } = await setupTwoAdminGroup(); + + // B advances the group while A remains stale. + for (let i = 0; i < staleDepth; i++) { + await groupB.selfUpdate(); + } + + await clientC.keyPackages.create({ relays: ["wss://mock-relay.test"] }); + network.stepAll(); + + const keyPkgC = network.events.find( + (e) => e.kind === KEY_PACKAGE_KIND && e.pubkey === inviteeCPubkey, + ); + if (!keyPkgC) + throw new Error("R2 setup failed: missing key package event for invitee C"); + + // A produces welcome from stale state view. + await groupA.inviteByKeyPackageEvent(keyPkgC); + network.stepAll(); + + const giftwrapC = await getGiftWrapFor(network, inviteeCPubkey); + if (!giftwrapC) + throw new Error("R2 setup failed: missing welcome giftwrap for invitee C"); + + const rumorC = (await unlockGiftWrap(giftwrapC, inviteeC.signer)) as Rumor; + try { + await clientC.joinGroupFromWelcome({ welcomeRumor: rumorC }); + return { ok: true }; + } catch (error) { + return { + ok: false, + error: error instanceof Error ? error.message : String(error), + }; + } +} + +async function runR3(variant: Variant): Promise { + const network = new ScriptedMockNetwork(); + + const adminA = PrivateKeyAccount.generateNew(); + const adminB = PrivateKeyAccount.generateNew(); + const inviteeC = PrivateKeyAccount.generateNew(); + const inviteeD = PrivateKeyAccount.generateNew(); + + const clientA = await createClient(adminA, network); + const clientB = await createClient(adminB, network); + const clientC = await createClient(inviteeC, network); + const clientD = await createClient(inviteeD, network); + + const adminBPubkey = await adminB.signer.getPublicKey(); + const inviteeCPubkey = await inviteeC.signer.getPublicKey(); + const inviteeDPubkey = await inviteeD.signer.getPublicKey(); + + await clientB.keyPackages.create({ relays: ["wss://mock-relay.test"] }); + network.stepAll(); + + const groupA = await clientA.createGroup("drift-r3", { + adminPubkeys: [adminBPubkey], + relays: ["wss://mock-relay.test"], + }); + + const keyPkgB = network.events.find( + (e) => e.kind === KEY_PACKAGE_KIND && e.pubkey === adminBPubkey, + ); + if (!keyPkgB) + throw new Error("R3 setup failed: missing key package event for admin B"); + + await groupA.inviteByKeyPackageEvent(keyPkgB); + network.stepAll(); + + const giftwrapB = await getGiftWrapFor(network, adminBPubkey); + if (!giftwrapB) + throw new Error("R3 setup failed: missing welcome for admin B"); + const rumorB = (await unlockGiftWrap(giftwrapB, adminB.signer)) as Rumor; + const { group: groupB } = await clientB.joinGroupFromWelcome({ + welcomeRumor: rumorB, + }); + + await clientC.keyPackages.create({ relays: ["wss://mock-relay.test"] }); + await clientD.keyPackages.create({ relays: ["wss://mock-relay.test"] }); + network.stepAll(); + + const keyPkgC = network.events.find( + (e) => e.kind === KEY_PACKAGE_KIND && e.pubkey === inviteeCPubkey, + ); + const keyPkgD = network.events.find( + (e) => e.kind === KEY_PACKAGE_KIND && e.pubkey === inviteeDPubkey, + ); + if (!keyPkgC || !keyPkgD) + throw new Error("R3 setup failed: missing invitee key package(s)"); + + const publishCut = network.events.length; + await groupA.inviteByKeyPackageEvent(keyPkgC); + await groupB.inviteByKeyPackageEvent(keyPkgD); + + const raceEvents = network.events.slice(publishCut); + const cWelcome = raceEvents.find( + (e) => + e.kind === 1059 && + e.tags.some((t) => t[0] === "p" && t[1] === inviteeCPubkey), + ); + const dWelcome = raceEvents.find( + (e) => + e.kind === 1059 && + e.tags.some((t) => t[0] === "p" && t[1] === inviteeDPubkey), + ); + const commits = raceEvents.filter((e) => e.kind === GROUP_EVENT_KIND); + if (!cWelcome || !dWelcome || commits.length < 2) { + throw new Error("R3 setup failed: overlapping race events missing"); + } + + if (variant === "V1") { + network.stepWhere((e) => e.id === commits[0]!.id); + network.stepWhere((e) => e.id === cWelcome.id); + network.stepWhere((e) => e.id === commits[1]!.id); + network.stepWhere((e) => e.id === dWelcome.id); + } else if (variant === "V2") { + network.stepWhere((e) => e.id === commits[1]!.id); + network.stepWhere((e) => e.id === dWelcome.id); + network.stepWhere((e) => e.id === commits[0]!.id); + network.stepWhere((e) => e.id === cWelcome.id); + } else { + network.stepWhere((e) => e.id === cWelcome.id); + network.stepWhere((e) => e.id === dWelcome.id); + network.stepWhere((e) => e.id === commits[0]!.id); + network.stepWhere((e) => e.id === commits[1]!.id); + } + + const giftwrapC = await getGiftWrapFor(network, inviteeCPubkey); + if (!giftwrapC) throw new Error("R3 setup failed: welcome for C not visible"); + + const rumorC = (await unlockGiftWrap(giftwrapC, inviteeC.signer)) as Rumor; + try { + await clientC.joinGroupFromWelcome({ welcomeRumor: rumorC }); + return { ok: true }; + } catch (error) { + return { + ok: false, + error: error instanceof Error ? error.message : String(error), + }; + } +} + +async function runR1BranchDriftSweep(params: { + aSelfUpdates: number; + bSelfUpdates: number; + releaseOrder: + | "welcome-first" + | "a-then-welcome-then-b" + | "b-then-welcome-then-a"; +}): Promise { + const { network, clientC, groupA, groupB, inviteeC, inviteeCPubkey } = + await setupTwoAdminGroup(); + + // Build divergent local branches before invite by committing without releasing + // events to the other admin. + for (let i = 0; i < params.aSelfUpdates; i++) { + await groupA.selfUpdate(); + } + for (let i = 0; i < params.bSelfUpdates; i++) { + await groupB.selfUpdate(); + } + + await clientC.keyPackages.create({ relays: ["wss://mock-relay.test"] }); + network.stepAll(); + const keyPkgC = network.events.find( + (e) => e.kind === KEY_PACKAGE_KIND && e.pubkey === inviteeCPubkey, + ); + if (!keyPkgC) { + throw new Error( + "R1 branch drift setup failed: missing key package event for invitee C", + ); + } + + const publishCut = network.events.length; + await groupA.inviteByKeyPackageEvent(keyPkgC); + await groupB.selfUpdate(); + + const raceEvents = network.events.slice(publishCut); + const commits = raceEvents.filter((e) => e.kind === GROUP_EVENT_KIND); + const welcome = raceEvents.find( + (e) => + e.kind === 1059 && + e.tags.some((t) => t[0] === "p" && t[1] === inviteeCPubkey), + ); + if (!welcome || commits.length < 2) { + throw new Error("R1 branch drift setup failed: missing race events"); + } + + if (params.releaseOrder === "welcome-first") { + network.stepWhere((e) => e.id === welcome.id); + network.stepWhere((e) => e.id === commits[0]!.id); + network.stepWhere((e) => e.id === commits[1]!.id); + } else if (params.releaseOrder === "a-then-welcome-then-b") { + network.stepWhere((e) => e.id === commits[0]!.id); + network.stepWhere((e) => e.id === welcome.id); + network.stepWhere((e) => e.id === commits[1]!.id); + } else { + network.stepWhere((e) => e.id === commits[1]!.id); + network.stepWhere((e) => e.id === welcome.id); + network.stepWhere((e) => e.id === commits[0]!.id); + } + + const giftwrapC = await getGiftWrapFor(network, inviteeCPubkey); + if (!giftwrapC) { + throw new Error( + "R1 branch drift setup failed: welcome not visible for invitee C", + ); + } + const rumorC = (await unlockGiftWrap(giftwrapC, inviteeC.signer)) as Rumor; + + try { + await clientC.joinGroupFromWelcome({ welcomeRumor: rumorC }); + return { ok: true }; + } catch (error) { + return { + ok: false, + error: error instanceof Error ? error.message : String(error), + }; + } +} + +async function runR5RealWorldSequence(): Promise { + const signature = + "non-blank intermediate node must list leaf node in its unmerged_leaves"; + + const network = new ScriptedMockNetwork(); + + const adminA = PrivateKeyAccount.generateNew(); + const adminB = PrivateKeyAccount.generateNew(); + const buggyC = PrivateKeyAccount.generateNew(); + const inviteeD = PrivateKeyAccount.generateNew(); + + const clientA = await createClient(adminA, network); + const clientB = await createClient(adminB, network); + const clientC = await createClient(buggyC, network); + const clientD = await createClient(inviteeD, network); + + const adminBPubkey = await adminB.signer.getPublicKey(); + const buggyCPubkey = await buggyC.signer.getPublicKey(); + const inviteeDPubkey = await inviteeD.signer.getPublicKey(); + + // 1) A creates group and invites B admin. + await clientB.keyPackages.create({ relays: ["wss://mock-relay.test"] }); + await clientC.keyPackages.create({ relays: ["wss://mock-relay.test"] }); + network.stepAll(); + const groupA = await clientA.createGroup("drift-r5", { + adminPubkeys: [adminBPubkey, buggyCPubkey], + relays: ["wss://mock-relay.test"], + }); + const keyPkgB = network.events.find( + (e) => e.kind === KEY_PACKAGE_KIND && e.pubkey === adminBPubkey, + ); + if (!keyPkgB) + throw new Error("R5 setup failed: missing key package for admin B"); + await groupA.inviteByKeyPackageEvent(keyPkgB); + const keyPkgCAdmin = network.events.find( + (e) => e.kind === KEY_PACKAGE_KIND && e.pubkey === buggyCPubkey, + ); + if (!keyPkgCAdmin) + throw new Error("R5 setup failed: missing key package for admin C"); + await groupA.inviteByKeyPackageEvent(keyPkgCAdmin); + network.stepAll(); + const giftwrapB = await getGiftWrapFor(network, adminBPubkey); + if (!giftwrapB) + throw new Error("R5 setup failed: missing welcome for admin B"); + const rumorB = (await unlockGiftWrap(giftwrapB, adminB.signer)) as Rumor; + const { group: groupB } = await clientB.joinGroupFromWelcome({ + welcomeRumor: rumorB, + }); + + // 2) Buggy C joins as admin and sends traffic. + const giftwrapC = await getGiftWrapFor(network, buggyCPubkey); + if (!giftwrapC) + throw new Error("R5 setup failed: missing welcome for buggy C"); + const rumorC = (await unlockGiftWrap(giftwrapC, buggyC.signer)) as Rumor; + const { group: groupC } = await clientC.joinGroupFromWelcome({ + welcomeRumor: rumorC, + }); + + await groupC.selfUpdate(); + await groupA.selfUpdate(); + await groupB.selfUpdate(); + + // 3) Simulate buggy aggressive self-update fork by corrupting C local direct-path metadata. + const tree = groupC.state.ratchetTree; + let mutated = false; + for (let nodeIndex = 0; nodeIndex < tree.length && !mutated; nodeIndex++) { + const node = tree[nodeIndex]; + if (node?.nodeType !== nodeTypes.leaf) continue; + const leafIndex = toLeafIndex(Math.floor(nodeIndex / 2)); + const dp = directPath(leafToNodeIndex(leafIndex), leafWidth(tree.length)); + if (dp.length < 2) continue; + + const firstIdx = dp[0]!; + const secondIdx = dp[1]!; + + if (tree[firstIdx]?.nodeType !== nodeTypes.parent) { + tree[firstIdx] = { + nodeType: nodeTypes.parent, + parent: { + hpkePublicKey: new Uint8Array(32).fill((firstIdx % 251) + 1), + parentHash: new Uint8Array(), + unmergedLeaves: [], + }, + }; + } + if (tree[secondIdx]?.nodeType !== nodeTypes.parent) { + tree[secondIdx] = { + nodeType: nodeTypes.parent, + parent: { + hpkePublicKey: new Uint8Array(32).fill((secondIdx % 251) + 2), + parentHash: new Uint8Array(), + unmergedLeaves: [], + }, + }; + } + + const firstParent = tree[firstIdx]; + const secondParent = tree[secondIdx]; + if (firstParent?.nodeType !== nodeTypes.parent) continue; + if (secondParent?.nodeType !== nodeTypes.parent) continue; + + firstParent.parent.unmergedLeaves = [leafIndex]; + secondParent.parent.unmergedLeaves = []; + mutated = true; + } + + if (!mutated) + throw new Error("R5 setup failed: could not mutate buggy C tree"); + + // 4) Buggy C now invites D from divergent state; D should naturally fail on join. + await clientD.keyPackages.create({ relays: ["wss://mock-relay.test"] }); + network.stepAll(); + const keyPkgD = network.events.find( + (e) => e.kind === KEY_PACKAGE_KIND && e.pubkey === inviteeDPubkey, + ); + if (!keyPkgD) + throw new Error("R5 setup failed: missing key package for invitee D"); + + await groupC.inviteByKeyPackageEvent(keyPkgD); + network.stepAll(); + + const giftwrapD = await getGiftWrapFor(network, inviteeDPubkey); + if (!giftwrapD) + throw new Error("R5 setup failed: missing welcome for invitee D"); + const rumorD = (await unlockGiftWrap(giftwrapD, inviteeD.signer)) as Rumor; + + try { + await clientD.joinGroupFromWelcome({ welcomeRumor: rumorD }); + return { ok: true }; + } catch (error) { + const msg = error instanceof Error ? error.message : String(error); + return { ok: false, error: msg.includes(signature) ? msg : msg }; + } +} + +function forceDirectPathUnmergedLeafDivergence(tree: any[]): boolean { + let mutated = false; + for (let nodeIndex = 0; nodeIndex < tree.length && !mutated; nodeIndex++) { + const node = tree[nodeIndex]; + if (node?.nodeType !== nodeTypes.leaf) continue; + const leafIndex = toLeafIndex(Math.floor(nodeIndex / 2)); + const dp = directPath(leafToNodeIndex(leafIndex), leafWidth(tree.length)); + if (dp.length < 2) continue; + + const firstIdx = dp[0]!; + const secondIdx = dp[1]!; + + if (tree[firstIdx]?.nodeType !== nodeTypes.parent) { + tree[firstIdx] = { + nodeType: nodeTypes.parent, + parent: { + hpkePublicKey: new Uint8Array(32).fill((firstIdx % 251) + 1), + parentHash: new Uint8Array(), + unmergedLeaves: [], + }, + }; + } + if (tree[secondIdx]?.nodeType !== nodeTypes.parent) { + tree[secondIdx] = { + nodeType: nodeTypes.parent, + parent: { + hpkePublicKey: new Uint8Array(32).fill((secondIdx % 251) + 2), + parentHash: new Uint8Array(), + unmergedLeaves: [], + }, + }; + } + + const firstParent = tree[firstIdx]; + const secondParent = tree[secondIdx]; + if (firstParent?.nodeType !== nodeTypes.parent) continue; + if (secondParent?.nodeType !== nodeTypes.parent) continue; + + firstParent.parent.unmergedLeaves = [leafIndex]; + secondParent.parent.unmergedLeaves = []; + mutated = true; + } + + return mutated; +} + +describe("Invite-join drift deterministic reproduction matrix", () => { + it.each(["V1", "V2", "V3"] as const)( + "R1 variant %s is deterministic across repeated runs", + async (variant) => { + const runA = await runR1(variant); + const runB = await runR1(variant); + + expect(runA.ok).toBe(runB.ok); + expect(runA.error ?? null).toBe(runB.error ?? null); + }, + ); + + it.each([1, 2] as const)( + "R2 stale producer depth %i is deterministic across repeated runs", + async (depth) => { + const runA = await runR2(depth); + const runB = await runR2(depth); + + expect(runA.ok).toBe(runB.ok); + expect(runA.error ?? null).toBe(runB.error ?? null); + }, + ); + + it.each(["V1", "V2", "V3"] as const)( + "R3 variant %s is deterministic across repeated runs", + async (variant) => { + const runA = await runR3(variant); + const runB = await runR3(variant); + + expect(runA.ok).toBe(runB.ok); + expect(runA.error ?? null).toBe(runB.error ?? null); + }, + ); + + it("scan matrix for invariant failure signature (diagnostic sweep)", async () => { + const signature = + "non-blank intermediate node must list leaf node in its unmerged_leaves"; + + const outcomes: string[] = []; + + for (let round = 0; round < 3; round++) { + for (const variant of ["V1", "V2", "V3"] as const) { + const r1 = await runR1(variant); + outcomes.push( + `round=${round};R1-${variant}:${r1.ok ? "ok" : r1.error}`, + ); + } + + for (const depth of [1, 2] as const) { + const r2 = await runR2(depth); + outcomes.push(`round=${round};R2-${depth}:${r2.ok ? "ok" : r2.error}`); + } + + for (const variant of ["V1", "V2", "V3"] as const) { + const r3 = await runR3(variant); + outcomes.push( + `round=${round};R3-${variant}:${r3.ok ? "ok" : r3.error}`, + ); + } + } + + const hits = outcomes.filter((o) => o.includes(signature)); + + // Deterministic sweep currently serves as a probe and should execute without + // harness flakiness. We intentionally don't require hits yet. + expect(Array.isArray(hits)).toBe(true); + }, 20000); + + it("stress branch-drift schedules and probe invariant signature", async () => { + const signature = + "non-blank intermediate node must list leaf node in its unmerged_leaves"; + const outcomes: string[] = []; + + for (const aDepth of [0, 1, 2] as const) { + for (const bDepth of [0, 1, 2] as const) { + for (const releaseOrder of [ + "welcome-first", + "a-then-welcome-then-b", + "b-then-welcome-then-a", + ] as const) { + const res = await runR1BranchDriftSweep({ + aSelfUpdates: aDepth, + bSelfUpdates: bDepth, + releaseOrder, + }); + outcomes.push( + `a=${aDepth};b=${bDepth};order=${releaseOrder};result=${res.ok ? "ok" : res.error}`, + ); + } + } + } + + const hits = outcomes.filter((x) => x.includes(signature)); + expect(Array.isArray(hits)).toBe(true); + }, 30000); + + it("R4 simulated buggy self-update client shows fork symptoms and invariant-class failure", async () => { + const signature = + "non-blank intermediate node must list leaf node in its unmerged_leaves"; + + const { + network, + clientA, + clientB, + clientC, + groupA, + groupB, + inviteeC, + inviteeCPubkey, + } = await setupTwoAdminGroup(); + + await clientC.keyPackages.create({ relays: ["wss://mock-relay.test"] }); + network.stepAll(); + + const keyPkgC = network.events.find( + (e) => e.kind === KEY_PACKAGE_KIND && e.pubkey === inviteeCPubkey, + ); + if (!keyPkgC) + throw new Error("R4 setup failed: missing key package for invitee C"); + + await groupA.inviteByKeyPackageEvent(keyPkgC); + network.stepAll(); + + const giftwrapC = await getGiftWrapFor(network, inviteeCPubkey); + if (!giftwrapC) + throw new Error("R4 setup failed: missing welcome for invitee C"); + + const rumorC = (await unlockGiftWrap(giftwrapC, inviteeC.signer)) as Rumor; + const { group: groupC } = await clientC.joinGroupFromWelcome({ + welcomeRumor: rumorC, + }); + + // Simulate the known buggy implementation behavior by forcing direct-path + // unmergedLeaves divergence on C's local state after join. + const tree = groupC.state.ratchetTree; + let mutated = false; + for (let nodeIndex = 0; nodeIndex < tree.length && !mutated; nodeIndex++) { + const node = tree[nodeIndex]; + if (node?.nodeType !== nodeTypes.leaf) continue; + const leafIndex = toLeafIndex(Math.floor(nodeIndex / 2)); + const dp = directPath(leafToNodeIndex(leafIndex), leafWidth(tree.length)); + if (dp.length < 2) continue; + const firstIdx = dp[0]!; + const secondIdx = dp[1]!; + + if (tree[firstIdx]?.nodeType !== nodeTypes.parent) { + tree[firstIdx] = { + nodeType: nodeTypes.parent, + parent: { + hpkePublicKey: new Uint8Array(32).fill((firstIdx % 251) + 1), + parentHash: new Uint8Array(), + unmergedLeaves: [], + }, + }; + } + if (tree[secondIdx]?.nodeType !== nodeTypes.parent) { + tree[secondIdx] = { + nodeType: nodeTypes.parent, + parent: { + hpkePublicKey: new Uint8Array(32).fill((secondIdx % 251) + 2), + parentHash: new Uint8Array(), + unmergedLeaves: [], + }, + }; + } + + const firstParent = tree[firstIdx]; + const secondParent = tree[secondIdx]; + if (firstParent?.nodeType !== nodeTypes.parent) continue; + if (secondParent?.nodeType !== nodeTypes.parent) continue; + firstParent.parent.unmergedLeaves = [leafIndex]; + secondParent.parent.unmergedLeaves = []; + mutated = true; + } + + expect(mutated).toBe(true); + + // Validate the exact invariant-class failure on the mutated (buggy) client state. + const ciphersuiteImpl = await getCiphersuiteImpl( + "MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519", + defaultCryptoProvider, + ); + const treeErr = await validateRatchetTree( + groupC.state.ratchetTree, + groupC.state.groupContext, + defaultLifetimeConfig, + unsafeTestingAuthenticationService, + groupC.state.groupContext.treeHash, + ciphersuiteImpl, + ); + expect(treeErr?.message ?? "").toContain(signature); + + // A buggy client may still emit events while effectively forked from peers. + // We don't assert selfUpdate failure here because validation timing differs by flow. + await groupC.selfUpdate(); + + // Ensure healthy peers are still operating, emphasizing mixed-implementation drift. + await expect(groupA.selfUpdate()).resolves.toBeDefined(); + await expect(groupB.selfUpdate()).resolves.toBeDefined(); + }); + + it("minimal ts-mls unmerged_leaves invariant trigger (causality probe)", async () => { + const impl = await getCiphersuiteImpl( + "MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519", + defaultCryptoProvider, + ); + + const aliceCredential = createCredential("a".repeat(64)); + const aliceKeyPackage = await generateKeyPackage({ + credential: aliceCredential, + ciphersuiteImpl: impl, + }); + + const { clientState: createdState } = await createSimpleGroup( + aliceKeyPackage, + impl, + "invariant-probe", + { adminPubkeys: ["a".repeat(64)], relays: [] }, + ); + + const bobCredential = createCredential("b".repeat(64)); + const bobKeyPackage = await generateKeyPackage({ + credential: bobCredential, + ciphersuiteImpl: impl, + }); + const charlieCredential = createCredential("c".repeat(64)); + const charlieKeyPackage = await generateKeyPackage({ + credential: charlieCredential, + ciphersuiteImpl: impl, + }); + const daveCredential = createCredential("d".repeat(64)); + const daveKeyPackage = await generateKeyPackage({ + credential: daveCredential, + ciphersuiteImpl: impl, + }); + const erinCredential = createCredential("e".repeat(64)); + const erinKeyPackage = await generateKeyPackage({ + credential: erinCredential, + ciphersuiteImpl: impl, + }); + + const { createCommit } = await import("ts-mls"); + const addBob = await createCommit({ + context: { + cipherSuite: impl, + authService: unsafeTestingAuthenticationService, + }, + state: createdState, + wireAsPublicMessage: false, + ratchetTreeExtension: true, + extraProposals: [ + { + proposalType: defaultProposalTypes.add, + add: { keyPackage: bobKeyPackage.publicPackage }, + }, + ], + }); + + const addCharlie = await createCommit({ + context: { + cipherSuite: impl, + authService: unsafeTestingAuthenticationService, + }, + state: addBob.newState, + wireAsPublicMessage: false, + ratchetTreeExtension: true, + extraProposals: [ + { + proposalType: defaultProposalTypes.add, + add: { keyPackage: charlieKeyPackage.publicPackage }, + }, + ], + }); + + const addDave = await createCommit({ + context: { + cipherSuite: impl, + authService: unsafeTestingAuthenticationService, + }, + state: addCharlie.newState, + wireAsPublicMessage: false, + ratchetTreeExtension: true, + extraProposals: [ + { + proposalType: defaultProposalTypes.add, + add: { keyPackage: daveKeyPackage.publicPackage }, + }, + ], + }); + + const addErin = await createCommit({ + context: { + cipherSuite: impl, + authService: unsafeTestingAuthenticationService, + }, + state: addDave.newState, + wireAsPublicMessage: false, + ratchetTreeExtension: true, + extraProposals: [ + { + proposalType: defaultProposalTypes.add, + add: { keyPackage: erinKeyPackage.publicPackage }, + }, + ], + }); + + const tree = addErin.newState.ratchetTree; + let mutated = false; + + for (let nodeIndex = 0; nodeIndex < tree.length && !mutated; nodeIndex++) { + const node = tree[nodeIndex]; + if (node?.nodeType !== nodeTypes.leaf) continue; + + const leafIndex = toLeafIndex(Math.floor(nodeIndex / 2)); + const dp = directPath(leafToNodeIndex(leafIndex), leafWidth(tree.length)); + if (dp.length < 2) continue; + + const firstIdx = dp[0]!; + const secondIdx = dp[1]!; + + if (tree[firstIdx]?.nodeType !== nodeTypes.parent) { + tree[firstIdx] = { + nodeType: nodeTypes.parent, + parent: { + hpkePublicKey: new Uint8Array(32).fill((firstIdx % 251) + 1), + parentHash: new Uint8Array(), + unmergedLeaves: [], + }, + }; + } + if (tree[secondIdx]?.nodeType !== nodeTypes.parent) { + tree[secondIdx] = { + nodeType: nodeTypes.parent, + parent: { + hpkePublicKey: new Uint8Array(32).fill((secondIdx % 251) + 2), + parentHash: new Uint8Array(), + unmergedLeaves: [], + }, + }; + } + + const firstParent = tree[firstIdx]; + const secondParent = tree[secondIdx]; + if (firstParent?.nodeType !== nodeTypes.parent) continue; + if (secondParent?.nodeType !== nodeTypes.parent) continue; + + // Force inconsistency: one direct-path parent lists the leaf as unmerged, + // another direct-path parent does not. + firstParent.parent.unmergedLeaves = [leafIndex]; + secondParent.parent.unmergedLeaves = []; + mutated = true; + } + + expect(mutated).toBe(true); + + const err = await validateRatchetTree( + tree, + addErin.newState.groupContext, + defaultLifetimeConfig, + unsafeTestingAuthenticationService, + addErin.newState.groupContext.treeHash, + impl, + ); + + expect(err?.message).toBe( + "non-blank intermediate node must list leaf node in its unmerged_leaves", + ); + }); + + it("R5 real-world sequence: buggy forked inviter emits welcome that fails natural join", async () => { + const signature = + "non-blank intermediate node must list leaf node in its unmerged_leaves"; + const res = await runR5RealWorldSequence(); + expect(res.ok).toBe(false); + expect(res.error ?? "").toContain(signature); + }); + + it("R6 non-admin buggy self-update ingested by admin before invite is observable and join remains deterministic", async () => { + const network = new ScriptedMockNetwork(); + + const adminA = PrivateKeyAccount.generateNew(); + const adminB = PrivateKeyAccount.generateNew(); + const memberC = PrivateKeyAccount.generateNew(); + const inviteeD = PrivateKeyAccount.generateNew(); + + const clientA = await createClient(adminA, network); + const clientB = await createClient(adminB, network); + const clientC = await createClient(memberC, network); + const clientD = await createClient(inviteeD, network); + + const adminBPubkey = await adminB.signer.getPublicKey(); + const memberCPubkey = await memberC.signer.getPublicKey(); + const inviteeDPubkey = await inviteeD.signer.getPublicKey(); + + await clientB.keyPackages.create({ relays: ["wss://mock-relay.test"] }); + await clientC.keyPackages.create({ relays: ["wss://mock-relay.test"] }); + network.stepAll(); + + const groupA = await clientA.createGroup("drift-r6", { + adminPubkeys: [adminBPubkey], + relays: ["wss://mock-relay.test"], + }); + + const keyPkgB = network.events.find( + (e) => e.kind === KEY_PACKAGE_KIND && e.pubkey === adminBPubkey, + ); + if (!keyPkgB) + throw new Error("R6 setup failed: missing key package for admin B"); + await groupA.inviteByKeyPackageEvent(keyPkgB); + network.stepAll(); + + const giftwrapB = await getGiftWrapFor(network, adminBPubkey); + if (!giftwrapB) + throw new Error("R6 setup failed: missing welcome for admin B"); + const rumorB = (await unlockGiftWrap(giftwrapB, adminB.signer)) as Rumor; + await clientB.joinGroupFromWelcome({ welcomeRumor: rumorB }); + + const keyPkgC = network.events.find( + (e) => e.kind === KEY_PACKAGE_KIND && e.pubkey === memberCPubkey, + ); + if (!keyPkgC) + throw new Error("R6 setup failed: missing key package for member C"); + await groupA.inviteByKeyPackageEvent(keyPkgC); + network.stepAll(); + + const giftwrapC = await getGiftWrapFor(network, memberCPubkey); + if (!giftwrapC) + throw new Error("R6 setup failed: missing welcome for member C"); + const rumorC = (await unlockGiftWrap(giftwrapC, memberC.signer)) as Rumor; + const { group: groupC } = await clientC.joinGroupFromWelcome({ + welcomeRumor: rumorC, + }); + + const mutated = forceDirectPathUnmergedLeafDivergence( + groupC.state.ratchetTree as any[], + ); + expect(mutated).toBe(true); + + await groupC.selfUpdate(); + network.stepAll(); + await drainGroupIngestFromNetwork(groupA as any, network); + + await clientD.keyPackages.create({ relays: ["wss://mock-relay.test"] }); + network.stepAll(); + const keyPkgD = network.events.find( + (e) => e.kind === KEY_PACKAGE_KIND && e.pubkey === inviteeDPubkey, + ); + if (!keyPkgD) + throw new Error("R6 setup failed: missing key package for invitee D"); + + await groupA.inviteByKeyPackageEvent(keyPkgD); + network.stepAll(); + + const giftwrapD = await getGiftWrapFor(network, inviteeDPubkey); + if (!giftwrapD) + throw new Error("R6 setup failed: missing welcome for invitee D"); + const rumorD = (await unlockGiftWrap(giftwrapD, inviteeD.signer)) as Rumor; + + await expect( + clientD.joinGroupFromWelcome({ welcomeRumor: rumorD }), + ).resolves.toBeDefined(); + }); + + it("R7 sequential invitees from buggy inviter: first deterministic failure implies second deterministic failure", async () => { + const signature = + "non-blank intermediate node must list leaf node in its unmerged_leaves"; + + const network = new ScriptedMockNetwork(); + const adminA = PrivateKeyAccount.generateNew(); + const adminB = PrivateKeyAccount.generateNew(); + const buggyC = PrivateKeyAccount.generateNew(); + const inviteeD = PrivateKeyAccount.generateNew(); + const inviteeE = PrivateKeyAccount.generateNew(); + + const clientA = await createClient(adminA, network); + const clientB = await createClient(adminB, network); + const clientC = await createClient(buggyC, network); + const clientD = await createClient(inviteeD, network); + const clientE = await createClient(inviteeE, network); + + const adminBPubkey = await adminB.signer.getPublicKey(); + const buggyCPubkey = await buggyC.signer.getPublicKey(); + const inviteeDPubkey = await inviteeD.signer.getPublicKey(); + const inviteeEPubkey = await inviteeE.signer.getPublicKey(); + + await clientB.keyPackages.create({ relays: ["wss://mock-relay.test"] }); + await clientC.keyPackages.create({ relays: ["wss://mock-relay.test"] }); + network.stepAll(); + + const groupA = await clientA.createGroup("drift-r7", { + adminPubkeys: [adminBPubkey, buggyCPubkey], + relays: ["wss://mock-relay.test"], + }); + + const keyPkgB = network.events.find( + (e) => e.kind === KEY_PACKAGE_KIND && e.pubkey === adminBPubkey, + ); + const keyPkgC = network.events.find( + (e) => e.kind === KEY_PACKAGE_KIND && e.pubkey === buggyCPubkey, + ); + if (!keyPkgB || !keyPkgC) + throw new Error("R7 setup failed: missing admin key package(s)"); + + await groupA.inviteByKeyPackageEvent(keyPkgB); + await groupA.inviteByKeyPackageEvent(keyPkgC); + network.stepAll(); + + const giftwrapC = await getGiftWrapFor(network, buggyCPubkey); + if (!giftwrapC) + throw new Error("R7 setup failed: missing welcome for buggy C"); + const rumorC = (await unlockGiftWrap(giftwrapC, buggyC.signer)) as Rumor; + const { group: groupC } = await clientC.joinGroupFromWelcome({ + welcomeRumor: rumorC, + }); + + expect( + forceDirectPathUnmergedLeafDivergence(groupC.state.ratchetTree as any[]), + ).toBe(true); + + await clientD.keyPackages.create({ relays: ["wss://mock-relay.test"] }); + await clientE.keyPackages.create({ relays: ["wss://mock-relay.test"] }); + network.stepAll(); + + const keyPkgD = network.events.find( + (e) => e.kind === KEY_PACKAGE_KIND && e.pubkey === inviteeDPubkey, + ); + const keyPkgE = network.events.find( + (e) => e.kind === KEY_PACKAGE_KIND && e.pubkey === inviteeEPubkey, + ); + if (!keyPkgD || !keyPkgE) + throw new Error("R7 setup failed: missing invitee key package(s)"); + + await groupC.inviteByKeyPackageEvent(keyPkgD); + network.stepAll(); + const giftwrapD = await getGiftWrapFor(network, inviteeDPubkey); + if (!giftwrapD) + throw new Error("R7 setup failed: missing welcome for invitee D"); + const rumorD = (await unlockGiftWrap(giftwrapD, inviteeD.signer)) as Rumor; + + await groupC.inviteByKeyPackageEvent(keyPkgE); + network.stepAll(); + const giftwrapE = await getGiftWrapFor(network, inviteeEPubkey); + if (!giftwrapE) + throw new Error("R7 setup failed: missing welcome for invitee E"); + const rumorE = (await unlockGiftWrap(giftwrapE, inviteeE.signer)) as Rumor; + + await expect( + clientD.joinGroupFromWelcome({ welcomeRumor: rumorD }), + ).rejects.toThrow(signature); + await expect( + clientE.joinGroupFromWelcome({ welcomeRumor: rumorE }), + ).rejects.toThrow(signature); + }); + + it("R8 regression: healthy non-admin self-update does not break future joins", async () => { + const network = new ScriptedMockNetwork(); + + const adminA = PrivateKeyAccount.generateNew(); + const adminB = PrivateKeyAccount.generateNew(); + const memberC = PrivateKeyAccount.generateNew(); + const inviteeD = PrivateKeyAccount.generateNew(); + + const clientA = await createClient(adminA, network); + const clientB = await createClient(adminB, network); + const clientC = await createClient(memberC, network); + const clientD = await createClient(inviteeD, network); + + const adminBPubkey = await adminB.signer.getPublicKey(); + const memberCPubkey = await memberC.signer.getPublicKey(); + const inviteeDPubkey = await inviteeD.signer.getPublicKey(); + + await clientB.keyPackages.create({ relays: ["wss://mock-relay.test"] }); + await clientC.keyPackages.create({ relays: ["wss://mock-relay.test"] }); + network.stepAll(); + + const groupA = await clientA.createGroup("drift-r8", { + adminPubkeys: [adminBPubkey], + relays: ["wss://mock-relay.test"], + }); + + const keyPkgB = network.events.find( + (e) => e.kind === KEY_PACKAGE_KIND && e.pubkey === adminBPubkey, + ); + if (!keyPkgB) + throw new Error("R8 setup failed: missing key package for admin B"); + await groupA.inviteByKeyPackageEvent(keyPkgB); + network.stepAll(); + + const keyPkgC = network.events.find( + (e) => e.kind === KEY_PACKAGE_KIND && e.pubkey === memberCPubkey, + ); + if (!keyPkgC) + throw new Error("R8 setup failed: missing key package for member C"); + await groupA.inviteByKeyPackageEvent(keyPkgC); + network.stepAll(); + + const giftwrapC = await getGiftWrapFor(network, memberCPubkey); + if (!giftwrapC) + throw new Error("R8 setup failed: missing welcome for member C"); + const rumorC = (await unlockGiftWrap(giftwrapC, memberC.signer)) as Rumor; + const { group: groupC } = await clientC.joinGroupFromWelcome({ + welcomeRumor: rumorC, + }); + + await expect(groupC.selfUpdate()).resolves.toBeDefined(); + network.stepAll(); + await drainGroupIngestFromNetwork(groupA as any, network); + + await clientD.keyPackages.create({ relays: ["wss://mock-relay.test"] }); + network.stepAll(); + const keyPkgD = network.events.find( + (e) => e.kind === KEY_PACKAGE_KIND && e.pubkey === inviteeDPubkey, + ); + if (!keyPkgD) + throw new Error("R8 setup failed: missing key package for invitee D"); + await groupA.inviteByKeyPackageEvent(keyPkgD); + network.stepAll(); + + const giftwrapD = await getGiftWrapFor(network, inviteeDPubkey); + if (!giftwrapD) + throw new Error("R8 setup failed: missing welcome for invitee D"); + const rumorD = (await unlockGiftWrap(giftwrapD, inviteeD.signer)) as Rumor; + + await expect( + clientD.joinGroupFromWelcome({ welcomeRumor: rumorD }), + ).resolves.toBeDefined(); + }); +}); diff --git a/src/__tests__/welcome.test.ts b/src/__tests__/welcome.test.ts index 0ba9adbc..78c4efd0 100644 --- a/src/__tests__/welcome.test.ts +++ b/src/__tests__/welcome.test.ts @@ -1,8 +1,16 @@ import { describe, expect, it } from "vitest"; +import { bytesToHex } from "@noble/hashes/utils.js"; import type { NostrEvent } from "nostr-tools"; -import { getWelcome } from "../core/welcome"; -import { WELCOME_EVENT_KIND } from "../core/protocol"; +import { + createWelcomeRumor, + getWelcome, + getWelcomeEpochAuthenticator, +} from "../core/welcome"; +import { + WELCOME_EPOCH_AUTHENTICATOR_TAG, + WELCOME_EVENT_KIND, +} from "../core/protocol"; describe("spec compliance (MIP-02)", () => { it("should reject decoding kind 444 events that are missing an encoding=base64 tag", () => { @@ -32,4 +40,46 @@ describe("spec compliance (MIP-02)", () => { expect(() => getWelcome(event)).toThrow(/encoding=base64 tag/i); }); + + it("should include and decode the optional epoch authenticator tag", () => { + const epochAuthenticator = new Uint8Array([1, 2, 3, 4]); + + const event = createWelcomeRumor({ + welcome: { + cipherSuite: 1, + secrets: [], + encryptedGroupInfo: new Uint8Array(), + }, + author: "0".repeat(64), + groupRelays: ["wss://relay.example"], + epochAuthenticator, + }); + + expect( + event.tags.find((tag) => tag[0] === WELCOME_EPOCH_AUTHENTICATOR_TAG), + ).toEqual([ + WELCOME_EPOCH_AUTHENTICATOR_TAG, + bytesToHex(epochAuthenticator), + ]); + expect(getWelcomeEpochAuthenticator(event)).toEqual(epochAuthenticator); + }); + + it("should reject malformed epoch authenticator tags", () => { + const event: NostrEvent = { + kind: WELCOME_EVENT_KIND, + pubkey: "0".repeat(64), + created_at: 1, + content: "00", + tags: [ + ["encoding", "base64"], + [WELCOME_EPOCH_AUTHENTICATOR_TAG, "not-hex"], + ], + id: "0".repeat(64), + sig: "0".repeat(128), + }; + + expect(() => getWelcomeEpochAuthenticator(event)).toThrow( + /Invalid ea tag/i, + ); + }); }); diff --git a/src/client/group/marmot-group.ts b/src/client/group/marmot-group.ts index bc153939..a0bdf112 100644 --- a/src/client/group/marmot-group.ts +++ b/src/client/group/marmot-group.ts @@ -396,6 +396,12 @@ export class MarmotGroup< extraProposals: [], }); + this.log( + "self-update diagnostics: epoch=%d -> %d", + this.state.groupContext.epoch, + newState.groupContext.epoch, + ); + const commitEvent = await createGroupEvent({ message: commit, state: this.state, @@ -686,6 +692,13 @@ export class MarmotGroup< ...commitOptions, }); + this.log( + "commit diagnostics: local epoch=%d next epoch=%d proposals=%d", + this.state.groupContext.epoch, + newState.groupContext.epoch, + allProposals.length, + ); + // Wrap the commit in a group event // Use this.state (not newState) to get the exporter_secret for the current epoch // This ensures all members at the current epoch can decrypt the commit @@ -743,6 +756,7 @@ export class MarmotGroup< groupRelays: groupData.relays, keyPackageEventId: recipient.keyPackageEventId, keyPackageEvent: recipient.keyPackageEvent, + epochAuthenticator: this.state.keySchedule.epochAuthenticator, }); // Gift wrap the welcome event to the newly added user @@ -1137,6 +1151,16 @@ export class MarmotGroup< // STEP 4: Sort commits to handle race conditions (MIP-03) // ============================================================================ commits = sortGroupCommits(commits); + log( + "sorted commits: %O", + commits.map((x) => ({ + id: x.event.id, + created_at: x.event.created_at, + epoch: isPrivateMessage(x.message) + ? String(x.message.privateMessage.epoch) + : "n/a", + })), + ); // ============================================================================ // STEP 5: Process commits sequentially diff --git a/src/client/marmot-client.ts b/src/client/marmot-client.ts index 4aa5a53f..18f267cb 100644 --- a/src/client/marmot-client.ts +++ b/src/client/marmot-client.ts @@ -415,6 +415,12 @@ export class MarmotClient< const ciphersuiteImpl = await this.cryptoProvider.getCiphersuiteImpl( welcome.cipherSuite, ); + log( + "join diagnostics: welcome secrets=%d ciphersuite=%d rumor=%s", + welcome.secrets.length, + welcome.cipherSuite, + welcomeRumor.id, + ); // Find all local KeyPackage candidates with matching cipherSuite const allKeyPackages = await this.keyPackages.list(); @@ -461,6 +467,12 @@ export class MarmotClient< "No matching KeyPackage found in local store. Make sure you have published a KeyPackage event.", ); + log( + "join diagnostics: %d candidate key packages (%d with matching secret)", + candidatePackages.length, + candidatePackages.filter((p) => p.hasMatchingSecret).length, + ); + // Prioritize packages that have matching secrets in the welcome // This ensures we try the most likely candidates first (RFC 9420 compliance) const prioritizedKeyPackages = [ @@ -475,6 +487,11 @@ export class MarmotClient< for (const keyPackage of prioritizedKeyPackages) { try { + log( + "join attempt: keyPackageRef=%s hasMatchingSecret=%s", + bytesToHex(keyPackage.keyPackageRef), + keyPackage.hasMatchingSecret, + ); // Attempt to join the group with this key package // In v2, joinGroup takes a single params object with context clientState = await joinGroup({ @@ -488,11 +505,22 @@ export class MarmotClient< privateKeys: keyPackage.privatePackage, }); consumedKeyPackageRef = keyPackage.keyPackageRef; + log( + "join success: keyPackageRef=%s groupId=%s epoch=%s", + bytesToHex(keyPackage.keyPackageRef), + bytesToHex(clientState.groupContext.groupId), + clientState.groupContext.epoch.toString(), + ); // If successful, break out of the loop break; } catch (error) { // Store the error and continue to the next candidate lastError = error instanceof Error ? error : new Error(String(error)); + log( + "join attempt failed: keyPackageRef=%s error=%s", + bytesToHex(keyPackage.keyPackageRef), + lastError.message, + ); // Continue trying with the next key package } } diff --git a/src/core/protocol.ts b/src/core/protocol.ts index 71b0bde6..bc97d7b0 100644 --- a/src/core/protocol.ts +++ b/src/core/protocol.ts @@ -85,3 +85,6 @@ export const GROUP_EVENT_KIND = 445; /** Event kind for welcome events */ export const WELCOME_EVENT_KIND = 444; + +/** The name of the optional tag that contains the welcome epoch authenticator */ +export const WELCOME_EPOCH_AUTHENTICATOR_TAG = "ea"; diff --git a/src/core/welcome.ts b/src/core/welcome.ts index 6e6d47ac..b3e7784b 100644 --- a/src/core/welcome.ts +++ b/src/core/welcome.ts @@ -1,5 +1,6 @@ import { Rumor } from "applesauce-common/helpers/gift-wrap"; import { getTagValue, NostrEvent } from "applesauce-core/helpers/event"; +import { bytesToHex, hexToBytes } from "@noble/hashes/utils.js"; import { getEventHash } from "nostr-tools"; import { decode, @@ -36,7 +37,11 @@ import { isMarmotGroupDataExtension, } from "./marmot-group-data.js"; import { isRumorLike } from "./nostr.js"; -import { type MarmotGroupData, WELCOME_EVENT_KIND } from "./protocol.js"; +import { + type MarmotGroupData, + WELCOME_EPOCH_AUTHENTICATOR_TAG, + WELCOME_EVENT_KIND, +} from "./protocol.js"; /** * Creates a welcome rumor (kind 444) for a welcome message. @@ -52,12 +57,14 @@ export function createWelcomeRumor({ author, groupRelays, keyPackageEventId, + epochAuthenticator, }: { welcome: Welcome; author: string; keyPackageEventId?: string; keyPackageEvent?: NostrEvent; groupRelays: string[]; + epochAuthenticator?: Uint8Array; }): Rumor { // Serialize the welcome message as a full MLSMessage (RFC 9420) const mlsMessage: MlsWelcomeMessage = { @@ -82,6 +89,12 @@ export function createWelcomeRumor({ // Add the key package event ID if known if (keyPackageEventId) draft.tags.push(["e", keyPackageEventId]); + if (epochAuthenticator) + draft.tags.push([ + WELCOME_EPOCH_AUTHENTICATOR_TAG, + bytesToHex(epochAuthenticator), + ]); + // Calculate the event ID for the rumor const id = getEventHash(draft); @@ -96,6 +109,22 @@ export function getWelcomeKeyPackageEventId(event: Rumor): string | undefined { return getTagValue(event, "e"); } +/** Returns the optional epoch authenticator from a welcome rumor */ +export function getWelcomeEpochAuthenticator( + event: Rumor, +): Uint8Array | undefined { + const value = getTagValue(event, WELCOME_EPOCH_AUTHENTICATOR_TAG); + if (!value) return undefined; + + try { + return hexToBytes(value); + } catch (error) { + throw new Error( + `Invalid ${WELCOME_EPOCH_AUTHENTICATOR_TAG} tag: ${error instanceof Error ? error.message : String(error)}`, + ); + } +} + /** Returns the group relays from a welcome rumor */ export function getWelcomeGroupRelays(event: Rumor): string[] { // NOTE: The "relays" tag is a normal Nostr tag vector: ["relays", ...urls]