feat(example): perch-authz-console — local-key Nido login + policy visualizer/builder - #190
feat(example): perch-authz-console — local-key Nido login + policy visualizer/builder#190willemneal wants to merge 4 commits into
Conversation
…sualizer/builder A dApp built on @nidohq/testkit (#188) that logs in a local-key Nido account, visualizes its perch policy, simulates authorization locally, and lets you build more complex policies — including a post-quantum ML-DSA signer. No passkey, no network. Stacked on the testkit branch (feat/testkit / #189). - Wallet login including Nido: a NidoLocalModule implementing the @creit.tech/stellar-wallets-kit ModuleInterface — Nido as a kit wallet, connected with a local key instead of a passkey. - Every verifier: secp256r1 (the real webauthn-verifier, local P-256 key), ed25519, and ML-DSA-65 (post-quantum) — the last two + perch-on-chain simulated ahead of their contracts (ML-DSA groundwork #143), badged as such. - Real derived C-address + real perch doc_hash that updates live as the policy changes. - Simulate __check_auth: pick a call + signers → Kleene verdict + trace. - Build a policy: add rules (scope, functions, arg predicates, spend cap). - Attenuate: narrow a rule with the fail-closed reachable(child) ⊆ reachable(parent) check; widening is refused. Verification: Playwright e2e drives connect → visualize → simulate → build → attenuate and captures browser snapshots (all four states look right; the flow caught a real render-ordering bug). vite build + tsc clean. Preview: .github/workflows/perch-authz-preview.yml deploys a per-PR GitHub Pages preview (gh-pages branch, pr-preview/pr-<N>/, relative base). The one-time Pages source setting is documented in the workflow header (it's mutually exclusive with the Actions-based pages.yml used by status-message). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
| working-directory: examples/perch-authz-console | ||
|
|
||
| - name: Deploy / update / tear down the PR preview | ||
| uses: rossjrw/pr-preview-action@v1 |
There was a problem hiding this comment.
Resource Management: Unpinned third-party GitHub Action in deploy job
The workflow uses rossjrw/pr-preview-action@v1, a mutable tag. If the upstream action is compromised or the tag is retargeted, the workflow’s granted contents: write permission could be abused to push malicious content to gh-pages (and potentially modify repository content within the workflow’s scope).
Pin GitHub Actions to an immutable commit SHA (and optionally use GitHub’s dependency review / allowlist). Minimize permissions (e.g., use fine-grained permissions or separate deploy token) where feasible.
Actions
- Reply
/almanax ask <question>to ask a follow-up question. - Reply
/almanax dismiss [<reason>]and it won't appear again in future scans. - Reply
/almanax resolve [<reason>]to mark the finding as resolved. - Reply
/almanax severity <level> [<reason>]to override the severity.
| li.append(el('span', 'mono', `#${String(seq).padStart(2, '0')}`)); | ||
| const vv = el('span', `v ${v}`, v === 'ok' ? '✓' : v === 'bad' ? '✕' : '·'); | ||
| li.append(vv); | ||
| const m = el('span'); m.innerHTML = msg; li.append(m); |
There was a problem hiding this comment.
Input and Parameter Validation: DOM XSS via log rendering in logLine
logLine() assigns attacker-controlled strings to innerHTML, and some log messages embed unsanitized user inputs (e.g., rule name, check.reason). In a browser context this enables DOM XSS (script execution) by entering HTML/JS payloads into the policy builder fields.
Avoid innerHTML for untrusted content. Render text via textContent or sanitize with a strict allowlist (or build DOM nodes explicitly for the small amount of intended markup).
| const m = el('span'); m.innerHTML = msg; li.append(m); | |
| const m = el('span'); m.textContent = msg; li.append(m); |
Actions
- Reply
/almanax ask <question>to ask a follow-up question. - Reply
/almanax dismiss [<reason>]and it won't appear again in future scans. - Reply
/almanax resolve [<reason>]to mark the finding as resolved. - Reply
/almanax severity <level> [<reason>]to override the severity.
| // Local signing with the primary (admin) signer over the message bytes. | ||
| const bytes = new TextEncoder().encode(message); | ||
| const digest = new Uint8Array(32); | ||
| digest.set(bytes.slice(0, 32)); |
There was a problem hiding this comment.
Signature & Authentication: Message-signing truncation collisions in signMessage
signMessage() derives the signed digest by zero-padding and copying only the first 32 bytes of the message (digest.set(bytes.slice(0, 32))). Different messages sharing the same first 32 bytes will produce identical signatures, which can mislead dApps that rely on signMessage for intent-binding or challenges.
Hash the full message into 32 bytes (e.g., SHA-256) before signing, or sign the full message with a domain-separated scheme (include context prefix like NidoLocalModule signMessage:). Clearly label this as non-production if it must remain a demo.
Actions
- Reply
/almanax ask <question>to ask a follow-up question. - Reply
/almanax dismiss [<reason>]and it won't appear again in future scans. - Reply
/almanax resolve [<reason>]to mark the finding as resolved. - Reply
/almanax severity <level> [<reason>]to override the severity.
|
Preview deployed! Account URLs use numeric preview suffixes, for example |
|
Example dApp preview deployed!
status-message runs on testnet, wallet = THIS PR's preview (https://190.nido.fyi); perch-authz-console runs fully local. The live home is https://nidohq.github.io/nido/ once merged. |
…e shared previews Instead of a bespoke workflow per example, generalize the existing status-message preview to serve both examples, nested by subpath: /<repo>/ → status-message-dapp (home, unchanged) /<repo>/perch-authz-console/ → the perch console (relative base, --base=./) - pages.yml (GitHub Pages home): also build @nidohq/testkit + the perch example and nest it under the site; trigger on the perch example + packages changes. - example-preview.yml (per-PR Cloudflare): same nesting, and comment both preview URLs; trigger on the perch example too. - drop the standalone perch-authz-preview.yml; update the example README. status-message stays at the root (its 404-as-SPA-shell routing is untouched); the perch console has no client-side routing, so it works fine from a subpath. Adding a future example = nesting its build the same way in these two workflows. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…st the first A call can fall under more than one rule of the same scope (e.g. two self-admin rules signed by different keys). The old first-match logic denied a call the first rule couldn't satisfy even when a later rule could — so a self-admin op signed only by the ML-DSA key was wrongly denied because admin-root (needing the admin key) matched first. Now it tries all scope-matching rules and authorizes if any does (matching OZ, where the caller nominates a rule via context_rule_ids), else returns the first deny. +2 tests: a self-admin call authorized by a non-first rule (the ML-DSA signer), and denied when no matching rule is satisfied. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…vs self-admin) The simulate panel always targeted the registry contract, so a self-admin op like set_admin() could only ever hit the ci-publish rule and be denied. Add a target selector: choose "self-admin (this account)" to simulate against the account's own admin rules — set_admin signed by admin (or, via the updated multi-rule simulator, by the ML-DSA pq key) → authorized. e2e asserts the new allow path + snapshot. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
| const args: SimArg[] = [{ type: 'u32', value: 0 }, { type: 'address', value: authorAddr }]; | ||
| const ctx = | ||
| target === 'self' | ||
| ? { contract: a.address, fn, ledger } // self-admin scope |
There was a problem hiding this comment.
Signature & Authentication: auth digest not bound to args in runSimulation
When target === 'self', the simulator builds a SimContext without args, so stableContext(ctx) (and therefore the authDigest) is identical across different argument values. This can over-approve self-admin simulations compared to real Soroban __check_auth, where the digest is bound to the full invocation (including args), potentially misleading users about what a signature actually authorizes.
Include the intended invocation args in the self-admin simulation context (or add explicit UI for self-admin args) so the simulated digest matches on-chain binding semantics and cannot be reused across different parameters.
| ? { contract: a.address, fn, ledger } // self-admin scope | |
| ? { contract: a.address, fn, args, ledger } // self-admin scope |
Actions
- Reply
/almanax ask <question>to ask a follow-up question. - Reply
/almanax dismiss [<reason>]and it won't appear again in future scans. - Reply
/almanax resolve [<reason>]to mark the finding as resolved. - Reply
/almanax severity <level> [<reason>]to override the severity.
The guided tour (perch-status-onchain) now absorbs the console's connect / visualize / attenuate, so the standalone is retired. Drops it from the root workspaces and both shared example workflows (Pages home + per-PR preview). Supersedes #190. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Superseded by #191 — the guided tour absorbs this console's connect / visualize / attenuate into one example. Retiring to keep a single example. (Branch left intact; nothing lost.) |


A dApp built on
@nidohq/testkit(#188 / #189) that logs in a local-key Nido account, visualizes its perch policy, simulates authorization locally, and lets you build more complex policies — including a post-quantum ML-DSA signer. No passkey, no network.Stacked on #189 (feat/testkit) — merge that first; this consumes the testkit.
What it demonstrates
NidoLocalModuleimplementing the@creit.tech/stellar-wallets-kitModuleInterface: Nido as a kit wallet, connected with a local key instead of a passkey.webauthn-verifier, driven by a local P-256 key), ed25519, and ML-DSA-65 (post-quantum). The ed25519/ML-DSA verifiers + perch-on-chain are simulated ahead of their contracts (ML-DSA groundwork Guest-wasm ML-DSA-65 verifier contract (pre-CAP-0087 groundwork) #143) and badged as such.doc_hashthat updates live as the policy changes.__check_auth— pick a call + signers → Kleene verdict + a readable trace.reachable(child) ⊆ reachable(parent)check; widening is refused.Verification (browser snapshots)
A Playwright e2e drives connect → visualize → simulate → build → attenuate and captures
artifacts/*.pngat each state. All four look right; the flow caught a real render-ordering bug (verdict wiped by a re-render).vite build+tscclean.Preview on GitHub Pages
.github/workflows/perch-authz-preview.ymldeploys a per-PR preview to thegh-pagesbranch underpr-preview/pr-<N>/(relative base, so it works at any subpath). One-time maintainer step (documented in the workflow header): set the repo Pages source to thegh-pagesbranch — it's mutually exclusive with the Actions-basedpages.ymlthat servesstatus-message-dapp, so choose one or give this example its own Pages target.Roadmap
Same
simulateCheckAuthcall, swapped backend: realsoroban-envin the browser (wasmi) + rs-soroban-sdk#1657's local-storage cache → lazy testnet pulls, otherwise offline.🤖 Generated with Claude Code