- TypeScript 96%
- CSS 2.5%
- HTML 0.9%
- Dockerfile 0.4%
- JavaScript 0.2%
|
All checks were successful
Build and Push Docker Image / build (push) Successful in 53s
Two compounding failures on the first CI run: 1. `bun install --frozen-lockfile` in the build stage OOMed inside the homelab runner: same `oven/bun:1.3.10` JIT memory regression that killed the signer at deploy time. Pin to 1.3.4 (stock upstream `igloo-server:1.1.0` also uses 1.3.4, which is the known-good version on memory-constrained hosts). 2. `cache-to: type=registry,ref=...:buildcache` failed with "error writing layer blob: unknown" on push. The Forgejo registry either doesn't accept buildcache's special manifest format or our default REGISTRY_TOKEN scope doesn't cover writing a second image ref. Either way the cache export sank the whole job even though the actual image push would have succeeded. Drop the cache for now and re-introduce once the token / registry config is sorted. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|---|---|---|
| .forgejo/workflows | ||
| e2e | ||
| messages | ||
| project.inlang | ||
| public | ||
| src | ||
| test | ||
| .dockerignore | ||
| .gitignore | ||
| bun.lock | ||
| Caddyfile | ||
| CLAUDE.md | ||
| compose.yml | ||
| Dockerfile | ||
| index.html | ||
| package.json | ||
| postcss.config.js | ||
| README.md | ||
| tailwind.config.js | ||
| tsconfig.json | ||
| vite.config.ts | ||
| vitest.config.ts | ||
Igloo Keysmith
Browser-side FROSTR keyset bootstrapper. Generate a fresh shared key or split an existing Nostr nsec into k-of-n FROSTR shares. Everything runs inside the user's tab; the underlying secret and the resulting shares never cross the network.
Pairs with igloo-server (always-on remote signer), igloo-desktop (offline custody), and any other FROSTR signing client.
How it differs from the other Igloo apps
| Generate? | Split nsec? | Runs in browser? | Native install? | |
|---|---|---|---|---|
| igloo-keysmith | ✅ | ✅ | ✅ | optional (PWA) |
| igloo-desktop | ✅ | ✅ | ❌ (Electron) | ✅ |
| igloo-pwa | ✅ | ✅ | ✅ | ✅ |
| igloo-web | ❌ (signer only) | ❌ | ✅ | ❌ |
| igloo-server | ❌ (signer only) | ❌ | – | ❌ (Bun server) |
Keysmith is the small, V1-compatible web app for the create-or-split flow.
It pairs cleanly with igloo-server because both depend on the same
@frostr/igloo-core
primitives.
Quick start
bun install
bun run dev # http://localhost:5173
bun run test # vitest
bun run build # production bundle in dist/
bun run preview # smoke-test the production bundle
The crypto primitives come from @frostr/igloo-core:
generateNostrKeyPair()for the random-mode entropy.nsecToHex()+validateNsec()for the import-mode path.generateKeysetWithSecret(threshold, total, hexSecret)for the FROST split.
The wrapper around them — including a normalised KeysetBundle shape — lives
in src/lib/keysmith.ts and is unit-tested in
src/lib/keysmith.spec.ts.
Security model
- No backend. Static Vite build; deploy to any object storage / CDN. The shipped service worker pre-caches the whole app so users can reload offline once installed as a PWA.
- No telemetry, no analytics, no external requests at runtime. Verify with the network panel — generating a keyset never hits the wire.
- The plaintext nsec (in Split mode) and the freshly-generated nsec (in Generate mode) only exist inside the React component tree. After the keyset is rendered the source variables are dropped from state.
- The resulting shares are shown but not stored. Use the provided Copy / Download buttons to take them out of the page; nothing persists across reloads.
- Use the Hide / Show toggle on each card to keep credentials masked when you're not the only person looking at the screen.
Trust model in practice
Hosting Keysmith on a domain you control still asks users to trust that the first page load is the unmodified app. Mitigations:
- Install as a PWA. Once installed, the service worker serves the cached bundle and the hoster can't push a malicious update without the user noticing the re-install.
- Subresource Integrity (SRI) on the script tags — not yet automated; contributions welcome.
- Reproducible builds. Pin the lockfile, document the build command, and publish hashes alongside the deploy.
For the strongest model, use the native
igloo-desktop app — it never
crosses a network.
After generation
The output is one bfgroup1… (group credential) plus N bfshare1… shares.
Hand each share to a separate operator / device. Common patterns:
- One share on
igloo-server(always-on signer), the rest on devices usingigloo-desktop/igloo-android/frost2x. - Strict offline custody: keep N-k shares encrypted on cold media, distribute the rest to active signers.
Never put two threshold-meeting shares on the same machine. Doing so defeats the threshold property — the host alone could sign without quorum.
Deployment
Keysmith is a pure static bundle — bun run build writes everything to
dist/ and there is no runtime backend. Two paths:
Static hosting. Drop dist/ on any CDN, object store, GitHub Pages,
Caddy/nginx, etc. Make sure the host serves index.html as the fallback
for unknown routes so the PWA shell loads on a hard refresh.
Docker + reverse proxy (homelab). The repo ships a Dockerfile and
compose.yml that produce a caddy:2-alpine image with the bundle baked
in. The Caddyfile disables admin and auto-HTTPS (TLS terminates at your
reverse proxy), sets Cache-Control: immutable on hashed /assets/*,
no-cache on sw.js and manifest.webmanifest so PWA updates land, and
falls back to index.html for unknown paths.
docker compose build
docker compose up -d
curl -I http://localhost:4173/ # 200 from Caddy
The bundled compose.yml includes commented Traefik labels. Uncomment
them, set the Host(...) rule and entrypoints to match your Traefik
installation, and put the container on the shared Docker network Traefik
watches. The public keysmith origin you pick must be added to the paired
signer's ALLOWED_ORIGINS — see Pairing with igloo-server
below. For the signer side (Traefik labels, TLS, env vars), see
igloo-server's docs/DEPLOY.md.
Pairing with igloo-server ("Send to signer")
After generating, configure your signer's URL in the Signer integration
panel and each share card grows a Send share N to signer button. The
request is a single POST {base}/api/user/credentials with the new share
credential, the group credential, and the optional label/relays. It rides on
your existing signer session (no API key needed), so:
- Your signer must be reachable at that URL (the page does an HTTPS POST from your browser, not from any edufeed server).
- You must be signed in to the signer in another tab.
- Your signer must list this page's origin in
ALLOWED_ORIGINS. Fromigloo-serverv1.x onward, that automatically also setsAccess-Control-Allow-Credentials: truefor the whitelisted origins.
Example: keysmith hosted at https://keys.edufeed.org, signer at
https://igloo.edufeed.org:
# On the igloo-server side
ALLOWED_ORIGINS=https://keys.edufeed.org,https://igloo.edufeed.org
NODE_ENV=production
AUTH_ENABLED=true
Failure modes the UI surfaces inline:
| Symptom | What to fix |
|---|---|
| "Could not reach the signer." | Server offline, URL wrong, or this origin isn't listed in ALLOWED_ORIGINS. |
| "Not signed in to that server." | Open the signer in another tab, log in, then retry. |
| Server-supplied 4xx message | Per-server validation. The message is the verbatim server response. |
License
See LICENSE (TBD).