Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
ec543eb
feat: migrate browser authenticator and integrate published WalletKit
Dzejkop Oct 7, 2026
50831f5
Disable automatic Vercel deployments
Dzejkop Oct 7, 2026
0df4aec
turning the spec into a readme
Oct 8, 2026
8028af9
docs: link repository responsibilities and trim setup
Oct 8, 2026
5baf5d0
docs: remove README boilerplate and repetition
Oct 8, 2026
e7f5eb5
Replace embedded RP simulation with staging authenticator flow
Oct 9, 2026
505b5e5
Restore provider PRF byte compatibility and accurate wallet errors
Oct 9, 2026
6224330
Add opt-in developer panel for credential vault inspection
Oct 9, 2026
d013f80
Replace MediaPipe preview guidance with RGBNet ONNX worker
Oct 9, 2026
f20f69f
Move selfie preview above setup instructions
Oct 9, 2026
6d0abb2
Adapt authenticator layout to desktop and mobile browsers
Oct 9, 2026
ae0b178
Checkpoint local authenticator and TEE demo integration
Oct 13, 2026
17314f0
Converge authenticator demo on responsive staging UI
Oct 13, 2026
e0780e8
Isolate synthetic enrollment and organize browser test configuration
Oct 14, 2026
0bb4ad5
Group build tooling and remove obsolete staging helper references
Oct 14, 2026
87d22b0
Restore conventional config locations and clarify system README
Oct 14, 2026
bc70d67
Simplify README around local setup and testing
Oct 14, 2026
7aca3da
Clarify product and experiment boundaries
Oct 14, 2026
1905d14
Simplify authenticator header and wallet copy
Oct 14, 2026
ee9d6aa
Split browser and Bun API into separate workspaces
Oct 14, 2026
db1995a
Package web and API builds in one Bun container
Oct 14, 2026
201d3c2
Shorten README around setup and everyday commands
Oct 14, 2026
3496b81
Add authenticator favicon from the existing emblem
Oct 14, 2026
e8715d9
Trim local setup instructions
Oct 14, 2026
9fba61f
Flatten web and API workspaces at the repository root
Oct 14, 2026
72b0c53
Document published package boundary and refresh UI review captures
Oct 14, 2026
edb697e
Use the World human badge as the authenticator hero
Oct 14, 2026
21f9353
Retry connection resets while the CI container starts
Oct 14, 2026
e44a80d
Redirect verify connector links while preserving their query
Oct 14, 2026
88ff25e
Remove planning documents and experimental demos from the product tree
Oct 14, 2026
2b7f2b5
Organize README sections and link upstream dependencies
Oct 14, 2026
1ef9012
Provide a safe exit when proof delivery expires
Oct 14, 2026
c6c4234
Recover failed registrations and show wallet setup timeouts
Oct 14, 2026
b3126d2
Keep model assets local and simplify setup documentation
Oct 14, 2026
512976b
Remove camera preview from the shipped authenticator UI
Oct 14, 2026
2cf9a98
use bun router to replace the big if statement that was the backedn
Oct 16, 2026
8095f73
Ignore query-only return URLs for authenticator requests
Oct 16, 2026
9efdda8
Align the PRF contract with the wallet's passkey derivation
Oct 17, 2026
84f6a4b
Offer an exit when a request expires before delivery
Oct 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
.git
**/node_modules
**/dist
**/dist-server
**/dist-static
**/.env
**/.env.*
**/*.pem
**/*.tsbuildinfo
**/test
**/tests
.artifacts
.runtime
experiments
playwright-report
test-results
25 changes: 21 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,13 @@ jobs:
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
bun-version: 1.4.2

- name: Install dependencies
run: bun install
run: bun install --frozen-lockfile

- name: Check formatting
run: bun run format:check
- name: Run tests
run: bun run test

- name: Run linting
run: bun run lint
Expand All @@ -37,3 +37,20 @@ jobs:

- name: Build project
run: bun run build

- name: Install test browsers
run: bunx playwright install --with-deps chromium webkit

- name: Browser regression tests
run: bunx playwright test -c tests/e2e/playwright.config.ts

- name: Build and smoke-test container
run: |
docker build -t web-authenticator:test .
docker run --detach --name authenticator-test -p 127.0.0.1:4195:4173 web-authenticator:test
trap 'docker logs authenticator-test; docker rm --force authenticator-test' EXIT
# Docker can reset connections before Bun starts accepting requests.
curl --fail --retry 10 --retry-all-errors --retry-delay 1 --retry-max-time 30 --max-time 5 http://127.0.0.1:4195/ > /dev/null
curl --fail --silent --show-error --max-time 5 http://127.0.0.1:4195/api/health/ready > /dev/null
status=$(curl --silent --show-error --output /dev/null --write-out '%{http_code}' --max-time 5 'http://127.0.0.1:4195/api/staging-rp?id=invalid')
test "$status" = 400
58 changes: 23 additions & 35 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,41 +1,29 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
node_modules/
web/dist/
api/dist/
web/dist-server/
web/dist-static/
.artifacts/
.runtime/
playwright-report/
test-results/
.env
.env.*

# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions

# testing
/coverage

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
*.pem
*.tsbuildinfo
coverage/
.vercel/
.wrangler/

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*

# env files (can opt-in for committing if needed)
.env*

# vercel
.vercel
web/public/models/*
!web/public/models/README.md

# typescript
*.tsbuildinfo
next-env.d.ts
.next/
demo/
dist/
public/models/
**/target/
.codex-visualizations/
7 changes: 6 additions & 1 deletion .prettierignore
Original file line number Diff line number Diff line change
@@ -1 +1,6 @@
relyance-sci.yml
relyance-sci.yml
/demo/
/.codex-visualizations/
/src/server/**/target/
/dist/
/.next/
24 changes: 24 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
FROM oven/bun:1.4.2 AS build
WORKDIR /app

COPY package.json bun.lock ./
COPY web/package.json web/package.json
COPY api/package.json api/package.json
COPY packages/contracts/package.json packages/contracts/package.json
COPY packages/browser-admission/package.json packages/browser-admission/package.json
RUN bun install --frozen-lockfile

COPY tsconfig.json ./
COPY web web
COPY api api
RUN bun run build

FROM oven/bun:1.4.2-slim AS runtime
WORKDIR /app
COPY --from=build --chown=bun:bun /app/web/dist web/dist
COPY --from=build --chown=bun:bun /app/api/dist api/dist
ENV AUTHENTICATOR_HOST=0.0.0.0
ENV PORT=4173
USER bun
EXPOSE 4173
CMD ["bun", "api/dist/main.js"]
55 changes: 53 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,54 @@
# web-authenticator
# World ID Web Authenticator

World ID Web Authenticator.
Unlock your World ID wallet with a passkey and approve requests from apps.
Currently uses **staging** contracts for enrollment.
## Dependencies

- [WalletKit](https://github.com/worldcoin/walletkit) — `@worldcoin/walletkit-web@0.27.1` for wallet storage, registration and proofs.
- [Biometrics TEE](https://github.com/worldcoin/biometrics-tee) — intended for face processing; not integrated yet.

## Local Development

Install [Bun](https://bun.sh) **1.4.2**, then:

```sh
bun install --frozen-lockfile
bun run dev
```

Open [localhost:4173](http://localhost:4173) in a browser with WebAuthn PRF support.

## Repository Structure

```text
web/ React UI, browser logic and public assets
api/ Bun HTTP server and API routes
packages/ Shared contracts and browser admission policy
tests/ Browser tests and integration fixtures
```

`web/` and `api/` own their dependencies. Bun serves both from one origin.

## Running Tests

```sh
bun run test
bun x playwright install chromium webkit # First run only
bun run test:browser
```

Static checks:

```sh
bun run lint
bun run typecheck
```

## Build and Run

```sh
bun run build
bun run start
```

[Container instructions](api/README.md#container) · [App request format](web/README.md#app-requests)
59 changes: 59 additions & 0 deletions api/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Authenticator API

Bun serves the web build and `/api/staging-rp` from the same origin. The RP lookup
reads the staging registry; passkeys, [WalletKit](https://github.com/worldcoin/walletkit) and proof generation run in the
browser. Promon sessions and Redis are not implemented yet.

Run `bun run dev` from the repo root to watch both apps. For the compiled app,
run `bun run build` followed by `bun run start`.

## Layout

```
main.ts Bun.serve listener settings
server.ts wires the route table, static fallback and error handler
routes/index.ts every server route, using Bun's built-in router
routes/*.ts one file per endpoint, exporting GET/POST like Next.js route.ts
lib/ config, security headers, static serving, registry client
test/ runs against a real server on an ephemeral port
```

To add an endpoint, create `routes/<name>.ts` and register it in
[`routes/index.ts`](routes/index.ts). Security headers are applied to every route
centrally in [`lib/http.ts`](lib/http.ts). Unmatched `/api/*` paths and methods
return JSON 404; everything else falls through to the web build.

## Health

- `GET /api/health/live` returns 200 whenever the process can serve requests. It
never checks dependencies.
- `GET /api/health/ready` returns 503 when the web build is missing. The staging
RP registry is not part of readiness: when it fails, only `/api/staging-rp`
degrades (502).

## Configuration

[`lib/config.ts`](lib/config.ts) reads the listener settings and defines response
security headers. `AUTHENTICATOR_HOST` defaults to `127.0.0.1`; `PORT` defaults to
`4173` (`AUTHENTICATOR_PORT` is also accepted). Invalid ports fail startup.

The CSP allows the official World ID bridges and WalletKit's staging endpoints.
Review it when changing SDK versions or network destinations. Server configuration
and credentials must never be imported by `web`.

## Container

From the repo root:

```sh
docker build -t web-authenticator .
docker run --rm -p 127.0.0.1:4173:4173 web-authenticator
```

The image runs as a non-root user and contains the compiled API and web assets.
It listens on `0.0.0.0:4173` inside the container. Hosting beyond localhost requires
HTTPS for camera and passkey access. Install the [RGBNet model](../web/README.md#camera-guidance)
before building if camera guidance is needed.

This packages the app; AWS hosting, Redis and secrets belong in deployment
configuration and are not provisioned by this Dockerfile.
44 changes: 44 additions & 0 deletions api/lib/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
export const AUTHENTICATOR_APP_SECURITY_HEADERS_V0 = Object.freeze({
"Cache-Control": "no-store",
"Content-Security-Policy": [
"default-src 'self'",
"base-uri 'none'",
"connect-src 'self' https://bridge.worldcoin.org https://staging-bridge.worldcoin.org https://gateway.id-infra.worldcoin.dev https://indexer.us.id-infra.worldcoin.dev https://node0.us.staging.world.oprf.taceo.network wss://node0.us.staging.world.oprf.taceo.network https://node1.us.staging.world.oprf.taceo.network wss://node1.us.staging.world.oprf.taceo.network https://node2.us.staging.world.oprf.taceo.network wss://node2.us.staging.world.oprf.taceo.network https://node3.us.staging.world.oprf.taceo.network wss://node3.us.staging.world.oprf.taceo.network https://node4.us.staging.world.oprf.taceo.network wss://node4.us.staging.world.oprf.taceo.network",
"font-src 'self'",
"form-action 'none'",
"frame-ancestors 'none'",
"img-src 'self' blob:",
"media-src 'self' blob:",
"object-src 'none'",
"script-src 'self' 'wasm-unsafe-eval'",
"style-src 'self'",
].join("; "),
"Cross-Origin-Opener-Policy": "same-origin",
"Cross-Origin-Resource-Policy": "same-origin",
"Permissions-Policy": "camera=(self), microphone=(), geolocation=()",
"Referrer-Policy": "no-referrer",
"X-Content-Type-Options": "nosniff",
});

const ENV: Readonly<Record<string, string | undefined>> =
process.env;

function envValue(name: string): string | undefined {
const value = ENV[name]?.trim();
return value === undefined || value === "" ? undefined : value;
}

function envPort(): number {
const raw = envValue("PORT") ?? envValue("AUTHENTICATOR_PORT");
if (raw === undefined) return 4173;
const port = Number(raw);
if (!Number.isInteger(port) || port < 1 || port > 65535) {
throw new TypeError(`authenticator_port_invalid: ${raw}`);
}
return port;
}

export const AUTHENTICATOR_APP_CONFIG = Object.freeze({
host: envValue("AUTHENTICATOR_HOST") ?? "127.0.0.1",
port: envPort(),
});
38 changes: 38 additions & 0 deletions api/lib/http.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { AUTHENTICATOR_APP_SECURITY_HEADERS_V0 } from "./config";

export type Handler = (request: Request) => Response | Promise<Response>;
type Method = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
export type RouteTable = Record<string, Handler | Partial<Record<Method, Handler>>>;

function applySecurityHeaders(response: Response): Response {
try {
for (const [name, value] of Object.entries(AUTHENTICATOR_APP_SECURITY_HEADERS_V0)) response.headers.set(name, value);
return response;
} catch {
// Response.redirect() and Response.error() have immutable headers.
const headers = new Headers(response.headers);
for (const [name, value] of Object.entries(AUTHENTICATOR_APP_SECURITY_HEADERS_V0)) headers.set(name, value);
return new Response(response.body, { status: response.status, statusText: response.statusText, headers });
}
}

export function withSecurityHeaders(handler: Handler): Handler {
return async request => applySecurityHeaders(await handler(request));
}

// Applied once here so a new route cannot ship without the security headers.
export function secureRoutes(routes: RouteTable): RouteTable {
return Object.fromEntries(Object.entries(routes).map(([path, route]) => [
path,
typeof route === "function"
? withSecurityHeaders(route)
: Object.fromEntries(Object.entries(route).flatMap(([method, handler]) =>
handler === undefined ? [] : [[method, withSecurityHeaders(handler)]])),
]));
}

export function internalError(error: Error): Response {
// eslint-disable-next-line no-console -- Unhandled request failure; message only, no request material.
console.error("request_unhandled_error", { name: error.name, message: error.message });
return applySecurityHeaders(Response.json({ error: "Internal server error" }, { status: 500 }));
}
14 changes: 14 additions & 0 deletions api/lib/rp-registry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { createPublicClient, http, parseAbi } from 'viem'

// World ID protocol staging deployment on World Chain (chain 480).
const registry = '0x37d2462fE7B4a07987263AAd062C6593C4f567b9'
const client = createPublicClient({
transport: http('https://worldchain-mainnet.g.alchemy.com/public', { timeout: 10000, retryCount: 0 }),
})
const abi = parseAbi([
'function getRp(uint64 rpId) view returns ((bool initialized, bool active, address manager, address signer, uint160 oprfKeyId, string unverifiedWellKnownDomain))',
])

export function readStagingRp(rpId: bigint) {
return client.readContract({ address: registry, abi, functionName: 'getRp', args: [rpId] })
}
Loading
Loading