From 8e41dda728ae0b5a488e98dfdcbc2d9a8954f21b Mon Sep 17 00:00:00 2001 From: Eddie Date: Thu, 4 Jun 2026 22:35:17 -0400 Subject: [PATCH 1/5] chore: README + deploy to EffectStream Cloudflare account MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add README.md covering what the project does, architecture, local-dev workflow, build/deploy commands, KV namespace IDs, manual testing via the embed iframe, and security caveats * Update wrangler.jsonc: - account_id: EffectStream (28ea08e3…) instead of rvcas (864bff2c…) - name: wallet-passkeys (was passkeys) so the workers.dev URL matches the GitHub repo name - drop the rvcas custom_domain route; the workers.dev URL is sufficient - kv_namespaces: new IDs created on this Cloudflare account - CHALLENGES: 11f31ec5555e4f58afc922748bff0438 - CREDENTIALS: ecc0c8045cbe47c5bf542521ec5940d7 Live at https://wallet-passkeys.ac-edward.workers.dev --- README.md | 172 +++++++++++++++++++++++++++++++++++++++++++++++++ wrangler.jsonc | 14 ++-- 2 files changed, 176 insertions(+), 10 deletions(-) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..cddc2ea --- /dev/null +++ b/README.md @@ -0,0 +1,172 @@ +# wallet-passkeys + +A passkey-backed authentication provider for EffectStream applications. Hosted as +a single Cloudflare Worker that serves both a React UI and a `/api/*` backend. +Designed to be embedded as a cross-origin iframe inside any dApp so the dApp can +let a user sign in with a passkey and receive a `did:key` identity plus a +delegated access key that can sign messages on their behalf without re-prompting +biometrics on every interaction. + +**Live deployment**: + +Embed entry point (for dApps): + +This is a fork of [`rvcas/passkeys`](https://github.com/rvcas/passkeys), adapted +to run on the EffectStream Cloudflare account with EffectStream's own KV +namespaces and worker URL. + +## What it does + +The user flow, end to end: + +1. The dApp embeds `wallet-passkeys/embed` as an iframe. +2. The embed asks the OS to create (or use) a platform passkey via the WebAuthn + API. The passkey's public key is stored on the worker; the private key never + leaves the device's secure enclave. +3. The embed generates an in-browser P-256 ECDSA "access key" and asks the + passkey to sign a challenge that *commits to that access key's public key*. + The worker verifies the passkey signature and stores the (root passkey, + access key) authorization tuple. +4. The embed `postMessage`s back to the parent dApp with the user's `did:key` + identity and the access key's public key. +5. Subsequent sign requests from the dApp (`{ type: "sign", payload: { message } }`) + are signed silently by the access key — no biometric prompt. + +The whole thing is a passkey-rooted delegation system. The passkey is the +identity; the access key is a per-session signer authorized by the passkey. + +## Architecture + +``` +┌──────────── dApp at any origin ────────────────┐ +│ + +``` + +## Notes on security + +* The access key's *private* JWK is stored in `localStorage` on the embed + origin. An XSS at this origin can exfiltrate it. This is a deliberate trade + for silent signing. +* The worker sets `Content-Security-Policy: frame-ancestors *` to allow any + origin to embed it. Anti-clickjacking is handled inside the iframe via an + `IntersectionObserver(trackVisibility:true)` check. +* Worker traffic uses `Access-Control-Allow-Origin: ` for the + CORS preflight, so any dApp can call the `/api/*` endpoints from within an + iframe at this origin. + +## License + +Same as the upstream project. See [LICENSE](./LICENSE) once added. diff --git a/wrangler.jsonc b/wrangler.jsonc index dedd22e..9e73ae2 100644 --- a/wrangler.jsonc +++ b/wrangler.jsonc @@ -1,14 +1,8 @@ { "$schema": "node_modules/wrangler/config-schema.json", - "name": "passkeys", + "name": "wallet-passkeys", "main": "./worker/index.ts", - "account_id": "864bff2c217f01d65488f7f5d61c5e55", - "routes": [ - { - "pattern": "passkeys.rvcas.dev", - "custom_domain": true, - }, - ], + "account_id": "28ea08e36bc67a4f136df373255ce175", "compatibility_date": "2025-09-27", "observability": { "enabled": true, @@ -23,11 +17,11 @@ "kv_namespaces": [ { "binding": "CHALLENGES", - "id": "2a9f96d3cdf145ce89f6825665773780", + "id": "11f31ec5555e4f58afc922748bff0438", }, { "binding": "CREDENTIALS", - "id": "e4e1f6d964a74f6db2dee1017310f380", + "id": "ecc0c8045cbe47c5bf542521ec5940d7", }, ], } From 2e1cc76ae55f8853842b226f402ec787137b1820 Mon Sep 17 00:00:00 2001 From: Eddie Date: Thu, 4 Jun 2026 22:44:34 -0400 Subject: [PATCH 2/5] =?UTF-8?q?style:=20terminal/CLI=20aesthetic=20?= =?UTF-8?q?=E2=80=94=20green-on-black,=20JetBrains=20Mono,=20sharp=20corne?= =?UTF-8?q?rs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The iframe consumer (wallet-passkeys-app) is being restyled in parallel with a deliberately opposite visual language. Together they make the iframe boundary unmistakable when the wallet pops up inside the dApp. Theme: * :root + .dark CSS variables redone in OKLCH: near-black background, phosphor-green foreground/accent, neon-green primary * --font-sans / --font-heading / --font-mono all → "JetBrains Mono" * --radius: 0.125rem (sharp 2px corners, no soft rounding) * Subtle scanline gradient on body, soft phosphor-glow on primary buttons Components: * header.tsx — "wallet-passkeys" wordmark with green/dim-green split + "auth provider" eyebrow * embed-auth.tsx — terminal-prompt strip ("$ wallet-passkeys connect") at the top of both the unauth and authenticated states; [OK]/[WARN]/[ERR] prefixes on status lines; uppercase tracked labels; mono inputs * index.html — JetBrains Mono via Google Fonts CDN; title now "wallet-passkeys · auth provider" Builds clean with `pnpm run build`. Already deployed to https://wallet-passkeys.ac-edward.workers.dev --- index.html | 8 +- src/components/embed-auth.tsx | 93 ++++++++------- src/components/header.tsx | 8 +- src/style.css | 205 +++++++++++++++++++++------------- 4 files changed, 195 insertions(+), 119 deletions(-) diff --git a/index.html b/index.html index 289e0bc..ca67bd7 100644 --- a/index.html +++ b/index.html @@ -4,7 +4,13 @@ - passkeys + + + + wallet-passkeys · auth provider
diff --git a/src/components/embed-auth.tsx b/src/components/embed-auth.tsx index 4135f47..3bc3bb9 100644 --- a/src/components/embed-auth.tsx +++ b/src/components/embed-auth.tsx @@ -237,55 +237,64 @@ export function EmbedAuth() { if (done) { return ( -
-

Authenticated

-

- Access key active. Listening for sign requests. -

+
+
wallet-passkeys connect --status
+
+

+ [OK] Authenticated +

+

+ access-key active · listening for sign requests +

+
); } return ( -
- {obscured && ( -

- This iframe appears to be obscured. For security, authentication will - open in a new window. +

+
wallet-passkeys connect
+
+ {obscured && ( +

+ [WARN] iframe obscured · auth will open in a new window +

+ )} +
+ + setUsername(e.target.value)} + className="flex h-8 w-full rounded-none border border-border bg-background px-3 py-1 text-sm font-mono" + /> +
+ {error &&

[ERR] {error}

} +
+ + +
+

+ wallet-passkeys.ac-edward.workers.dev

- )} -
- - setUsername(e.target.value)} - className="flex h-8 w-full rounded-md border border-border bg-background px-3 py-1 text-sm" - /> -
- {error &&

{error}

} -
- -
-

passkeys.rvcas.dev

); } diff --git a/src/components/header.tsx b/src/components/header.tsx index bf697b0..f805ef5 100644 --- a/src/components/header.tsx +++ b/src/components/header.tsx @@ -14,7 +14,13 @@ export function Header() { return (
- Passkeys + + wallet + -passkeys + + · auth provider + + +
); } + // ── Unauthenticated state ───────────────────────────────────────────── return ( -
-
wallet-passkeys connect
-
+
+ {header} +
+
+
Connection request
+
+ A dApp wants you to connect a passkey-backed wallet. Register a new + passkey or sign in with an existing one. No seed phrase, no extension. +
+
+ {obscured && (

[WARN] iframe obscured · auth will open in a new window

)} +
+ {error &&

[ERR] {error}

} -
- - -
-

- wallet-passkeys.ac-edward.workers.dev -

+ +
); } diff --git a/src/style.css b/src/style.css index ede1398..194380d 100644 --- a/src/style.css +++ b/src/style.css @@ -184,3 +184,136 @@ button[data-slot="button"]:not([disabled]).bg-primary, content: "$ "; color: oklch(0.86 0.27 145); } + +/* ───────────────────────────────────────────────────────────────────────── + Wallet-popup layout — used by the /embed route. Renders as a fixed-height + browser-wallet panel: brand header on top, scrollable body, sticky action + footer (OK / Cancel / Register / Sign In) at the bottom. The parent dApp + floats the iframe top-right; this layout fills whatever box it's given. + ───────────────────────────────────────────────────────────────────────── */ +html.embed, html.embed body, html.embed #app { height: 100%; } + +.wallet-shell { + height: 100vh; + display: flex; + flex-direction: column; + background: var(--background); + overflow: hidden; +} + +.wallet-header { + flex: 0 0 auto; + display: flex; + align-items: center; + gap: 10px; + padding: 12px 14px; + border-bottom: 1px solid var(--border); + background: oklch(0.04 0 0); +} +.wallet-header .mark { + width: 30px; + height: 30px; + display: grid; + place-items: center; + border: 1px solid oklch(0.86 0.27 145 / 55%); + color: oklch(0.86 0.27 145); + font-weight: 700; + font-size: 12px; + box-shadow: 0 0 10px oklch(0.86 0.27 145 / 25%); +} +.wallet-header .title { + font-size: 12.5px; + font-weight: 600; + letter-spacing: 0.02em; + color: oklch(0.92 0.18 145); + line-height: 1.15; +} +.wallet-header .subtitle { + font-size: 9.5px; + color: oklch(0.55 0.06 145); + text-transform: uppercase; + letter-spacing: 0.16em; +} +.wallet-status { + margin-left: auto; + display: inline-flex; + align-items: center; + gap: 6px; + font-size: 9.5px; + text-transform: uppercase; + letter-spacing: 0.12em; + color: oklch(0.55 0.06 145); +} +.wallet-status::before { + content: ""; + width: 7px; + height: 7px; + border-radius: 999px; + background: oklch(0.45 0.04 145); +} +.wallet-status.live::before { + background: oklch(0.86 0.27 145); + box-shadow: 0 0 9px oklch(0.86 0.27 145 / 90%); + animation: pulse 1.6s ease-in-out infinite; +} +@keyframes pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.45; } +} + +.wallet-body { + flex: 1 1 auto; + overflow-y: auto; + padding: 16px 14px; + display: flex; + flex-direction: column; + gap: 14px; +} + +.wallet-footer { + flex: 0 0 auto; + display: flex; + gap: 8px; + padding: 12px 14px; + border-top: 1px solid var(--border); + background: oklch(0.04 0 0); +} +.wallet-footer > * { flex: 1; } + +/* Connected-account chip in the body — mimics a wallet's "account" row. */ +.wallet-account { + border: 1px solid var(--border); + background: oklch(0.10 0 0); + padding: 10px 12px; +} +.wallet-account .label { + font-size: 9.5px; + text-transform: uppercase; + letter-spacing: 0.14em; + color: oklch(0.55 0.06 145); +} +.wallet-account .value { + font-family: var(--font-mono); + font-size: 11px; + color: oklch(0.92 0.18 145); + word-break: break-all; + margin-top: 3px; +} + +/* Approve-request panel — the "review and approve" card a wallet shows. */ +.wallet-approve { + border: 1px solid oklch(0.86 0.27 145 / 35%); + background: oklch(0.86 0.27 145 / 6%); + padding: 12px; +} +.wallet-approve .req-title { + font-size: 12px; + font-weight: 600; + color: oklch(0.92 0.18 145); +} +.wallet-approve .req-detail { + font-size: 11px; + color: oklch(0.65 0.08 145); + margin-top: 4px; + line-height: 1.5; +}