No description
  • TypeScript 96%
  • CSS 2.5%
  • HTML 0.9%
  • Dockerfile 0.4%
  • JavaScript 0.2%
Find a file
Steffen Rörtgen 47ce9f4e96
All checks were successful
Build and Push Docker Image / build (push) Successful in 53s
fix(ci+docker): pin bun 1.3.4 + drop registry buildcache
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>
2026-06-09 15:32:12 +02:00
.forgejo/workflows fix(ci+docker): pin bun 1.3.4 + drop registry buildcache 2026-06-09 15:32:12 +02:00
e2e feat(import): re-open existing keysets + fix npub in splitExistingNsec 2026-06-08 16:33:13 +02:00
messages feat(import): re-open existing keysets + fix npub in splitExistingNsec 2026-06-08 16:33:13 +02:00
project.inlang feat: warm-vault redesign, German i18n, brand icons, UI↔signer e2e 2026-06-07 12:38:27 +02:00
public feat: warm-vault redesign, German i18n, brand icons, UI↔signer e2e 2026-06-07 12:38:27 +02:00
src feat(import): re-open existing keysets + fix npub in splitExistingNsec 2026-06-08 16:33:13 +02:00
test feat: initial igloo-keysmith — browser-side FROSTR keyset bootstrapper 2026-06-04 18:26:19 +02:00
.dockerignore feat(deploy): Docker + Caddy image, Traefik-friendly compose 2026-06-07 13:01:58 +02:00
.gitignore feat: warm-vault redesign, German i18n, brand icons, UI↔signer e2e 2026-06-07 12:38:27 +02:00
bun.lock chore(deps): pin @inlang/paraglide-js as direct devDep 2026-06-07 13:01:49 +02:00
Caddyfile feat(deploy): Docker + Caddy image, Traefik-friendly compose 2026-06-07 13:01:58 +02:00
CLAUDE.md feat: warm-vault redesign, German i18n, brand icons, UI↔signer e2e 2026-06-07 12:38:27 +02:00
compose.yml feat(deploy): Docker + Caddy image, Traefik-friendly compose 2026-06-07 13:01:58 +02:00
Dockerfile fix(ci+docker): pin bun 1.3.4 + drop registry buildcache 2026-06-09 15:32:12 +02:00
index.html feat: warm-vault redesign, German i18n, brand icons, UI↔signer e2e 2026-06-07 12:38:27 +02:00
package.json chore(deps): pin @inlang/paraglide-js as direct devDep 2026-06-07 13:01:49 +02:00
postcss.config.js feat: initial igloo-keysmith — browser-side FROSTR keyset bootstrapper 2026-06-04 18:26:19 +02:00
README.md feat(deploy): Docker + Caddy image, Traefik-friendly compose 2026-06-07 13:01:58 +02:00
tailwind.config.js feat: initial igloo-keysmith — browser-side FROSTR keyset bootstrapper 2026-06-04 18:26:19 +02:00
tsconfig.json feat: warm-vault redesign, German i18n, brand icons, UI↔signer e2e 2026-06-07 12:38:27 +02:00
vite.config.ts feat: warm-vault redesign, German i18n, brand icons, UI↔signer e2e 2026-06-07 12:38:27 +02:00
vitest.config.ts feat: warm-vault redesign, German i18n, brand icons, UI↔signer e2e 2026-06-07 12:38:27 +02:00

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:

  1. 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.
  2. Subresource Integrity (SRI) on the script tags — not yet automated; contributions welcome.
  3. 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 using igloo-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:

  1. Your signer must be reachable at that URL (the page does an HTTPS POST from your browser, not from any edufeed server).
  2. You must be signed in to the signer in another tab.
  3. Your signer must list this page's origin in ALLOWED_ORIGINS. From igloo-server v1.x onward, that automatically also sets Access-Control-Allow-Credentials: true for 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).