Skip to content

feat(example): perch-authz-console — local-key Nido login + policy visualizer/builder - #190

Closed
willemneal wants to merge 4 commits into
feat/testkitfrom
feat/perch-authz-console
Closed

feat(example): perch-authz-console — local-key Nido login + policy visualizer/builder#190
willemneal wants to merge 4 commits into
feat/testkitfrom
feat/perch-authz-console

Conversation

@willemneal

Copy link
Copy Markdown
Contributor

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

  • 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 — one signer each for secp256r1 (the real 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.
  • Real derived C-address + real perch doc_hash that updates live as the policy changes.
  • Simulate __check_auth — pick a call + signers → Kleene verdict + a readable 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 (browser snapshots)

A Playwright e2e drives connect → visualize → simulate → build → attenuate and captures artifacts/*.png at each state. All four look right; the flow caught a real render-ordering bug (verdict wiped by a re-render). vite build + tsc clean.

Preview on GitHub Pages

.github/workflows/perch-authz-preview.yml deploys a per-PR preview to the gh-pages branch under pr-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 the gh-pages branch — it's mutually exclusive with the Actions-based pages.yml that serves status-message-dapp, so choose one or give this example its own Pages target.

Roadmap

Same simulateCheckAuth call, swapped backend: real soroban-env in the browser (wasmi) + rs-soroban-sdk#1657's local-storage cache → lazy testnet pulls, otherwise offline.

🤖 Generated with Claude Code

…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>
@github-actions

Copy link
Copy Markdown
PR Preview Action v1.8.1

QR code for preview link

🚀 View preview at
https://nidohq.github.io/nido/pr-preview/pr-190/

Built to branch gh-pages at 2026-08-15 17:48 UTC.
Preview will be ready when the GitHub Pages deployment is complete.

working-directory: examples/perch-authz-console

- name: Deploy / update / tear down the PR preview
uses: rossjrw/pr-preview-action@v1

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium Severity severity

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);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Low Severity severity

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).

Suggested change
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));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium Severity severity

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.

@github-actions

Copy link
Copy Markdown

Preview deployed!

https://190.nido.fyi

Account URLs use numeric preview suffixes, for example <contract-address>--190.nido.fyi.

@github-actions

github-actions Bot commented Aug 15, 2026

Copy link
Copy Markdown

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.

willemneal and others added 3 commits August 15, 2026 17:19
…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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Low Severity severity

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.

Suggested change
? { 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.

willemneal added a commit that referenced this pull request Aug 17, 2026
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>
@willemneal

Copy link
Copy Markdown
Contributor Author

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.)

@willemneal willemneal closed this Aug 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant