openalice-platform
**Level 2 of the 3-level model:** company (OpenAliceLabs) → **platform (this repo)** → products (openalice-embed, …). Dependencies point ONLY downward: products consume the platform; the platform knows nothing about products.
Architecture rating
ARCHITECTURE.md, CONTRIBUTING.md, QUICKSTART.md, ROADMAP.md, WHITEPAPER.md, DEPRECATED-REPOS.md, docs/design/ — unusually thorough doc surface for a platform repo; manifest valid but scored 30/100 (missing exposes/tables declarations)
JWT auth with alg-none/issuer/role rejection tests, MFA, SCIM, PoW challenge, internal-secret gating — but 55 env vars (incl. JWT private key, HMAC secrets, API keys, OAuth creds) are used-but-undeclared in manifest, creating drift/leak risk; CI failing so security tests may be broken
~30+ identity HTTP tests (auth rejection, MFA, admin, device-role), vitest configs across 6 TS packages, Rust test dirs in 6 crates, 420 features tracked — but CI is failing so suite is currently red; coverage breadth exists, green status does not
Three DATABASE_URLs (main, migration, RAG) and FGA crate suggest multi-store design, but manifest declares zero owned tables, no migration directories visible in structure scan, no RLS/tenant-isolation evidence, GDPR posture unknown
CI conclusion=failure as of 2026-07-15, no evidence of graceful shutdown, timeouts, retries, or idempotency in provided signals; error-path test coverage exists (400/401 cases) but build is broken so guarantees are unverifiable
Drift score 0/100 (weakest Atlas axis), 55 undeclared env vars, manifest quality 30/100, 240 unreferenced definitions (though ~40 are test fns, rest are genuine dead code); zero circular deps is the sole bright spot
Health probe returns 'no https endpoint in manifest exposes', up=false, no evidence of structured logging, metrics export, or distributed tracing in any crate; effectively unobservable in production
source · auto-grader-2026-07-18
Ecosystem manifest
.atlas-deps.ymlAtlas grepped the repo's source for env reads (rust env::var · ts process.env · py os.environ · shell $VAR) and diffed against the manifest's consumes_env. Two lists below: stale declarations and undeclared reads.
- ACCESS_COOKIE_TTL_SECS
- AUTH_PORT
- AUTH_PUBLIC_URL
- AUTO_IDLE_AFTER_SECS
- CORS_ORIGINS
- DASHBOARD_BASE_URL
- DATABASE_URL
- DOMAIN
- E2E_BASE_URL
- ELEVENLABS_API_KEY
- EMAIL_FROM
- EMAIL_FROM_NAME
- EMAIL_RESET_PATH
- EMAIL_VERIFY_BASE_URL
- EMAIL_VERIFY_PATH
- EMBED_SIGNUP_FULL
- FISH_AUDIO_API_KEY
- IDENTITY_API_BASE
- INTERNAL_BROADCAST_SECRET
- INTERNAL_SECRET
- MIGRATION_DATABASE_URL
- MISTRAL_API_KEY
- MOLLIE_API_KEY
- NEXT_DIST_DIR
- NEXT_PUBLIC_API_URL
- NEXT_PUBLIC_AUTH_URL
- NEXT_PUBLIC_EMBED_API_BASE
- NOTIFICATION_COOLDOWN_SECS
- OA_JWT_ALG
- OA_JWT_KID
- OA_JWT_PRIVATE_KEY_PEM
- OA_JWT_PRIVATE_KEY_PEM_FILE
- OA_NEXT_DIST_DIR
- OPENAI_API_KEY
- OPENALICE_ENV
- OPENALICE_PROJECTS_DIR
- OPENROUTER_API_KEY
- POW_CHALLENGE_DISABLED
- POW_CHALLENGE_HMAC_SECRET
- RAG_DATABASE_URL
- REFRESH_COOKIE_TTL_SECS
- SHARED_JWT_SECRET
- SMTP_URL
- SSO_COOKIE_DOMAIN
- SSO_PUBLIC_BASE_URL
- STREAM_KEY_ENCRYPTION_SECRET
- TENANTS_INTERNAL_URL
- TURBOPACK
- TWITCH_OAUTH_CLIENT_ID
- TWITCH_OAUTH_CLIENT_SECRET
- WAITLIST_EXPORT_TOKEN
- YOUTUBE_OAUTH_CLIENT_ID
- YOUTUBE_OAUTH_CLIENT_SECRET
Code composition
tokei · loc- Rust39.5%
- TSX32.0%
- TypeScript10.9%
- JSON6.5%
- CSS5.5%
- Other5.6%
- Rust39.5%
- TSX32.0%
- TypeScript10.9%
- JSON6.5%
- CSS5.5%
- Other5.6%
- Rust39.5%· 35,177
- TSX32.0%· 28,533
- TypeScript10.9%· 9,691
- JSON6.5%· 5,754
- CSS5.5%· 4,940
- JavaScript3.1%· 2,753
- Python1.3%· 1,150
- SQL0.5%· 467
- +6 more0.7%· 629 · 33 files
| Language | % code | code | comments | blanks | files |
|---|---|---|---|---|---|
| Rust | 39.5% | 35,177 | 3,966 | 4,670 | 162 |
| TSX | 32.0% | 28,533 | 6,995 | 2,321 | 244 |
| TypeScript | 10.9% | 9,691 | 3,386 | 1,372 | 117 |
| JSON | 6.5% | 5,754 | 0 | 0 | 41 |
| CSS | 5.5% | 4,940 | 1,832 | 461 | 20 |
| JavaScript | 3.1% | 2,753 | 868 | 262 | 20 |
| Python | 1.3% | 1,150 | 80 | 127 | 5 |
| SQL | 0.5% | 467 | 513 | 65 | 25 |
| TOML | 0.5% | 459 | 284 | 80 | 17 |
| Shell | 0.1% | 74 | 24 | 9 | 3 |
| Dockerfile | 0.1% | 49 | 32 | 14 | 3 |
| YAML | 0.0% | 24 | 1 | 0 | 1 |
| HTML | 0.0% | 12 | 0 | 0 | 1 |
| SVG | 0.0% | 11 | 4 | 0 | 8 |
| Markdown | 0.0% | 0 | 3,651 | 1,063 | 59 |
| Plain Text | 0.0% | 0 | 166 | 46 | 3 |
Telemetry ribbon · last 90 days
122 eventsagent-lite/tools
1 feature- agent-lite.tools.ssrf-guardcrates/oa-framework/src/ssrf.rs:3Deny-by-default SSRF guard for the `perform_action` tool's outbound HTTP. The `perform_action` tool dispatches HTTP requests to URLs that originate from a tenant's client-action registry. Even though those URLs are https-validated at registration time in openalice-tenants, agent-lite re-validates at the CALL boundary and additionally resolves the host so a URL can never be used to reach the operator's own internal network (metadata service, docker hostnames, RFC-1918 ranges, loopback, …). SECURITY MODEL (deny-by-default): 1. scheme MUST be exactly `https`. 2. the host MUST be a fully-qualified domain name — NOT `localhost`, NOT a bare single-label hostname (e.g. an internal docker service name like `openalice-tenants-dev`), and NOT a bare IP literal. 3. EVERY address the host resolves to MUST be a public, routable unicast address. If ANY resolved address falls in a private / loopback / link-local / reserved / unspecified / metadata range, the whole URL is rejected (so a "public" name that resolves to an internal IP — DNS rebinding — is also blocked). Blocked IPv4 ranges: 0.0.0.0/8, 10.0.0.0/8, 100.64.0.0/10 (CGNAT), 127.0.0.0/8, 169.254.0.0/16 (link-local, incl. 169.254.169.254 metadata), 172.16.0.0/12, 192.0.0.0/24, 192.0.2.0/24, 192.168.0.0/16, 198.18.0.0/15, 198.51.100.0/24, 203.0.113.0/24, 224.0.0.0/4 (multicast), 240.0.0.0/4 (reserved), 255.255.255.255 (broadcast). Blocked IPv6: ::, ::1 (loopback), fc00::/7 (ULA), fe80::/10 (link-local), multicast, and IPv4-mapped/compatible addresses (re-checked as IPv4). This module is pure + unit-tested: the range checks take an `IpAddr` so they can be exercised exhaustively without any DNS, and the host-shape checks are tested directly. The one impure step (DNS resolution) is isolated in `check_url_safe`, which is deny-by-default on resolve failure.
agent-lite/webhooks
1 feature- agent-lite.webhooks.dispatchcrates/oa-framework/src/webhook.rs:3Fire-and-forget outbound webhook delivery for conversation lifecycle events (conversation.started / conversation.ended / escalation.triggered / lead.captured). SAFETY (load-bearing): delivery is STRICTLY fire-and-forget. `fire()` is never awaited on the conversation turn path in a way that blocks it — it fetches the widget's active webhooks and spawns one detached `tokio::task` per matching delivery, then returns. A webhook error (fetch failure, DNS, timeout, non-2xx) NEVER affects the conversation outcome, latency, or response. When a widget has no webhooks configured the dispatcher fetches an empty list and immediately no-ops, so behaviour is byte-identical to a build without webhooks. Two impls, mirroring the SecretFetcher seam (src/tools/secrets.rs): - `NullWebhookDispatch` — default; `fire()` is an immediate no-op. Used by the no-tenants demo + tests. - `TenantsWebhookDispatch` — production; calls openalice-tenants `GET /internal/widgets/{id}/webhooks/active` over the shared `X-Internal-Secret` channel and, for each webhook subscribed to the event, fires a signed POST with `X-OpenAlice-Signature: t=<unix>,v1=<hmac_hex>` where hmac = HMAC-SHA256(secret, "{t}.{body}").
auth/agent
2 features- auth.agent_config.getcrates/identity/src/handlers/config.rs:76Read the user's unified agent config — shared identity (avatar + VRM), voice settings (TTS+STT provider/voice/language/emotion/pace), and per-product enable+system-prompt for live/social/persona/world. Returns a built-in default when no row exists yet.api:GET /auth/agent-configsince 0.1.0
- auth.agent_config.putcrates/identity/src/handlers/config.rs:114Upsert the user's agent config. Validates tts/stt providers against the known-good allowlist and per-product JSON shape. Partial updates supported — fields the request omits keep their current values.api:PUT /auth/agent-configsince 0.1.0
auth/broadcast
2 features- auth.broadcast_config.getcrates/identity/src/handlers/config.rs:250Read the user's current studio broadcast config (preset_id + per-stream overrides). Defaults to {preset_id="qa-host", overrides={}} when no row has been saved yet — first-time streamers don't have to click anything before going live.api:GET /auth/broadcast-configsince 0.1.0
- auth.broadcast_config.putcrates/identity/src/handlers/config.rs:288Upsert the user's studio broadcast config. Validates preset_id against the @openalicelabs/presets allowlist; rejects unknown ids with 400. Returns the persisted row so the client can mirror updated_at.api:PUT /auth/broadcast-configsince 0.1.0
auth/crypto
1 feature- auth.password.hashcrates/identity/src/auth.rs:602Hash a password with Argon2id using a per-call random salt.since 0.1.0
auth/devices
4 features- auth.devices.listcrates/identity/src/devices.rs:181List the user's paired devices — Hub /account/devices renders this.api:GET /auth/devicessince 0.6.0
- auth.devices.pair_claimcrates/identity/src/devices.rs:111Exchange a pairing code for a 90-day device-scoped JWT (audience = live + persona). Unauthenticated by design — the code itself is the credential.api:POST /auth/devices/pair/claimsince 0.6.0
- auth.devices.pair_startcrates/identity/src/devices.rs:75Generate a short-lived pairing code (8 chars, 10 min TTL) the user types into openalice-stream-host. Code is single-use; claiming it issues a 90-day device-scoped JWT.api:POST /auth/devices/pair/startsince 0.6.0
- auth.devices.revokecrates/identity/src/devices.rs:214Revoke a paired device — sets revoked_at, the issued JWT's jti goes into the revocation set so subsequent calls from that token are rejected.api:DELETE /auth/devices/{id}since 0.6.0
auth/email
2 features- auth.email.verifycrates/identity/src/handlers/email.rs:172Consume an email-verification token (looked up by SHA-256 hash). Marks the user verified and the token used. Idempotent — already-verified users get 200 with the same payload.api:POST /auth/verifysince 0.1.0
- auth.email.verify_sendcrates/identity/src/handlers/email.rs:48Issue a new email-verification token for the authenticated user, hash it, persist with 24h expiry, and "send" the verification link via the email stub.api:POST /auth/verify-email/sendsince 0.1.0
auth/internal
8 features- auth.internal.api_keys.createcrates/identity/src/handlers/account.rs:42Internal X-Internal-Secret-gated sibling for the embed dashboard's "create API key" flow (openalice-tenants POST /me/account/api-keys). Mints a fresh key, persists ONLY its SHA-256 hash + non-secret prefix + scopes (never the plaintext), and returns the PLAINTEXT key exactly once so the UI can show "copy now — it won't be shown again". Owner is the path {user_id}; IDOR-safe.api:POST /internal/users/{user_id}/api-keyssince 0.6.0
- auth.internal.api_keys.listcrates/identity/src/handlers/account.rs:124Internal X-Internal-Secret-gated sibling for the embed dashboard's API-keys list (openalice-tenants GET /me/account/api-keys). Returns the user's keys with NO key material — only id, non-secret prefix, name, scopes, created_at, last_used_at. Owner-scoped by the path {user_id}.api:GET /internal/users/{user_id}/api-keyssince 0.6.0
- auth.internal.api_keys.revokecrates/identity/src/handlers/account.rs:154Internal X-Internal-Secret-gated sibling for the embed dashboard's "revoke API key" flow (openalice-tenants DELETE /me/account/api-keys/{key_id}). Deletes the key — scoped to the path {user_id} so one user can never revoke another's key even if the key_id is guessed. 404 if the key isn't this user's.api:DELETE /internal/users/{user_id}/api-keys/{key_id}since 0.6.0
- auth.internal.delete_usercrates/identity/src/handlers/admin.rs:141Service-to-service compensating delete for a freshly-registered user. Used by openalice-tenants' signup saga — when /auth/register succeeds but the subsequent tenant write fails, tenants calls this to roll the orphaned auth user back (otherwise the email is permanently "taken" with no account). X-Internal-Secret-gated. All user FK children are ON DELETE CASCADE, so this fully removes the identity. Idempotent: deleting an absent id returns 204.api:DELETE /internal/users/{id}since 0.2.0
- auth.internal.mfa.activatecrates/identity/src/handlers/account.rs:260Internal X-Internal-Secret-gated sibling for the embed dashboard's MFA activate (openalice-tenants POST /me/account/mfa/activate). Verifies a TOTP code (±1 step skew, constant-time) against the pending encrypted secret, then flips mfa_enabled=true so subsequent logins require a second factor. Replay-aware: persists the matched TOTP step to mfa_last_step so the activate code can't be replayed at the login verify challenge. 400 if no setup is pending.api:POST /internal/users/{user_id}/mfa/activatesince 0.6.0
- auth.internal.mfa.disablecrates/identity/src/handlers/account.rs:324Internal X-Internal-Secret-gated sibling for the embed dashboard's MFA disable (openalice-tenants POST /me/account/mfa/disable). Re-confirms identity with EITHER a current TOTP code OR the account password (constant-time), then wipes the secret, backup codes, and the enabled/pending flags. Defends against a session-stealing attacker silently turning MFA off.api:POST /internal/users/{user_id}/mfa/disablesince 0.6.0
- auth.internal.mfa.setupcrates/identity/src/handlers/account.rs:188Internal X-Internal-Secret-gated sibling for the embed dashboard's MFA setup (openalice-tenants POST /me/account/mfa/setup). Mints a fresh TOTP secret stored ENCRYPTED (AES-256-GCM), plus backup codes stored HASHED, in the `pending` state (NOT enabled). Returns secret + otpauth_uri (for QR) + plaintext backup codes ONCE. 409 if MFA is already enabled — disable first.api:POST /internal/users/{user_id}/mfa/setupsince 0.6.0
- auth.internal.mfa.verifycrates/identity/src/handlers/account.rs:411Internal X-Internal-Secret-gated MFA challenge verifier for the LOGIN flow (openalice-tenants proxies the login challenge here). Accepts a current TOTP code OR a one-time backup code (a used backup code is CONSUMED so it works exactly once). Constant-time TOTP check with ±1 step skew, REPLAY-PROTECTED: a code whose step <= the stored mfa_last_step is rejected and every accepted step is persisted, so the same valid code can't be replayed here or at any sibling verify path. Returns 204 on success, 401 otherwise.api:POST /internal/users/{user_id}/mfa/verifysince 0.6.0
auth/mfa
3 features- auth.mfa.disablecrates/identity/src/handlers/mfa.rs:177Disable MFA after re-confirming the user's password (defense against a session-stealing attacker silently turning MFA off). Clears mfa_secret + mfa_enabled.api:POST /auth/mfa/disablesince 0.1.0
- auth.mfa.setupcrates/identity/src/handlers/mfa.rs:21Begin MFA enrollment — generates a fresh TOTP secret, stores it ENCRYPTED at rest (AES-256-GCM, mfa_secret_encrypted; the legacy plaintext mfa_secret column is no longer written and is NULLed), NOT yet enabled, and returns the secret + otpauth URL so the client can render a QR. Re-running before /auth/mfa/verify rotates the secret. Idempotent against an already-enabled user — returns 409 so the client tells the user to disable first.api:POST /auth/mfa/setupsince 0.1.0
- auth.mfa.verifycrates/identity/src/handlers/mfa.rs:90Confirm enrollment by submitting a code derived from the secret stored at /auth/mfa/setup. On success, mfa_enabled flips to true and subsequent /auth/login flows require a second factor.api:POST /auth/mfa/verifysince 0.1.0
auth/oauth
1 feature- auth.oauth.youtube_startcrates/identity/src/handlers/oauth.rs:113Begin a YouTube (Google) OAuth flow — generates a state nonce, persists it server-side, returns the Google authorize URL the client should redirect to. The callback will create a `chat_sources` row with the user's refresh + access tokens.api:POST /auth/connectors/youtube/startsince 0.1.0
auth/orgs
1 feature- auth.orgscrates/identity/src/handlers/orgs.rs:3Create + list the caller's organizations. The ORGANIZATION"POST /orgs""GET /me/orgs"
auth/password
2 features- auth.password.forgotcrates/identity/src/handlers/email.rs:305Issue a password-reset token for the given email and send the reset link via the email stub. Always returns 204 — never reveals whether the email is registered (timing attacks aside, this stops trivial account-enumeration).api:POST /auth/password/forgotsince 0.1.0
- auth.password.resetcrates/identity/src/handlers/email.rs:374Consume a password-reset token (looked up by SHA-256 hash) and rewrite the user's password. Marks the token used and revokes ALL outstanding refresh tokens for the user (forces re-login on every device).api:POST /auth/password/resetsince 0.1.0
auth/portal
1 feature- auth.portalcrates/identity/src/handlers/portal.rs:5The dashboard's data API, carved onto openalice-auth so the
auth/presence
1 feature- auth.presence.onlinecrates/identity/src/handlers/profile.rs:68List users with last_seen ≤ 2 minutes ago who haven't set their status to invisible. Powers the Hub "who's online" widget and any cross-product presence indicator.api:GET /auth/users/onlinesince 0.1.0
auth/realtime
4 features- auth.realtime.broadcastcrates/identity/src/handlers/admin.rs:40Service-to-service entry point — POST a notification (optionally targeted at a specific user) and the auth WS hub fans it out to every connected client. Authenticated by a shared secret in the `X-Internal-Secret` header (env `INTERNAL_BROADCAST_SECRET`). Producers: social-api on DM, persona on session events, billing on plan changes.api:POST /internal/broadcastsince 0.1.0
- auth.realtime.chat_eventcrates/identity/src/handlers/admin.rs:91Service-to-service entry — POST a normalized chat message from any platform connector (openalice-chat-bridge for YouTube/Twitch/Kick/TikTok) and the broadcast owner's connected agent receives it as a `chat_message` event over /auth/ws. Bypasses the notification cooldown (chat is a stream). Authenticated by X-Internal-Secret.api:POST /internal/chatsince 0.1.0
- auth.realtime.chat_feedcrates/identity/src/handlers/admin.rs:121Service-to-service entry — POST a mechanical chat aggregation tick (n-gram topics + mood + highlight quotes over a fixed window) from the chat-bridge feed-aggregator. The broadcast owner's connected agent receives it as a `chat_feed` event over /auth/ws — a cheap "what is the room doing?" heartbeat that costs zero LLM tokens to produce. Authenticated by X-Internal-Secret.api:POST /internal/chat-feedsince 0.5.0
- auth.realtime.wscrates/identity/src/handlers/ws.rs:35WebSocket fan-out — clients open with ?token=ACCESS_JWT and receive cross-product real-time events (presence_changed today, notifications + DM events later). Server-push only; messages from the client are currently ignored.api:GET /auth/wssince 0.1.0
auth/security
1 feature- auth.login.throttlecrates/identity/src/throttle.rs:78Laravel-style failed-attempt login throttle — per-account (email+IP) and per-IP lockout windows + MFA code attempt limiting. Counts only FAILURES, clears on success, returns Retry-After seconds while locked.since 0.2.0
auth/seed
1 feature- auth.seed.per-envcrates/identity/src/seed.rs:5Env-gated, idempotent demo seeder. `OPENALICE_ENV=dev|staging` inserts the two demo `users` rows (smoke-test@blal.pro + hello@openalicelabs.com) with a KNOWN dev-only password so the dashboard login proxy resolves a real auth user → tenant. `prod` is a hard NO-OP — it refuses to seed (no DB access) even if invoked. Every insert is `ON CONFLICT DO NOTHING` so re-running is safe. Mirrors `openalice-tenants/src/seed.rs` exactly. Runs AFTER migrations. gated-by: OPENALICE_ENV prod-safety: hard no-op (Seeded::None) — never writes a row in prod ## DEV-ONLY login credentials Both seeded accounts share the SAME dev-only password so a developer can log in to the embed dashboard immediately on a fresh dev DB: ```text smoke-test@blal.pro password: embed-dev-1234 hello@openalicelabs.com password: embed-dev-1234 ``` This password is dev/staging ONLY — it is NEVER seeded in prod (the `OPENALICE_ENV` guard short-circuits before any DB access). The plaintext is documented here on purpose: it is a throwaway demo credential, not a secret. ## Why a seeder (not a migration) Seed is *data*, migration is *schema*. The two never mix (a migration runs once per DB and is checksum-tracked; a seed is demo data we want to be able to re-apply by hand). This module runs as the `seed` subcommand on the auth binary AFTER `sqlx::migrate!`, against the same pool. ## UUID linkage to the tenants demo accounts (the load-bearing part) The dashboard login proxy (openalice-tenants `auth_proxy/login.rs`) looks the tenant up by the AUTH user id: ```sql SELECT id, plan FROM tenants WHERE id = $1 -- $1 = auth users.id ``` i.e. in v1 `auth.users.id == tenants.id` (the owner of the auth user IS the owner of the workspace). So for a real working login the auth `users.id` MUST equal the tenant id the tenants seeder pins. We therefore reuse the EXACT same fixed UUIDs the tenants seeder uses for its tenants: ```text smoke-test@blal.pro → 019de6d1-5fe3-7191-a90c-197384c96c97 (TENANT_SMOKE_ID) hello@openalicelabs.com → 98f50107-0851-4ab1-8ea5-0df90bb4a2db (TENANT_LABS_ID) ``` With matching ids the login flow is end-to-end working: auth verifies the password → returns user.id → tenants resolves `tenants WHERE id = user.id` → mints a `tid` JWT. No follow-up is required for the linkage to work.
auth/sessions
2 features- auth.sessions.listcrates/identity/src/handlers/session.rs:188List the authenticated user's active refresh-token sessions (one per browser/device login). Each row carries user_agent, created_at, last_used_at, and a `current` flag marking the device this request came from.api:GET /auth/sessionssince 0.1.0
- auth.sessions.revokecrates/identity/src/handlers/session.rs:230Revoke a specific refresh-token session by id. Marks the row revoked and blacklists its current jti so the next /auth/refresh from that device fails. Existing access tokens on that device survive until natural expiry (≤1h).api:DELETE /auth/sessions/:idsince 0.1.0
auth/tokens
1 feature- auth.token.refreshcrates/identity/src/handlers/session.rs:29Exchange a refresh token for a fresh access+refresh JWT pair (rotation).api:POST /auth/refreshsince 0.1.0
auth/users
6 features- auth.account.deletecrates/identity/src/handlers/profile.rs:243Permanently delete the authenticated user's account and revoke all tokens.api:DELETE /auth/accountsince 0.1.0
- auth.profile.updatecrates/identity/src/handlers/profile.rs:110Update authenticated user's profile fields (display name, avatar, bio).api:PUT /auth/profilesince 0.1.0
- auth.user.logincrates/identity/src/handlers/login.rs:74Verify email+password and issue access+refresh JWT pair.api:POST /auth/loginsince 0.1.0
- auth.user.login_mfacrates/identity/src/handlers/login.rs:322Exchange a pre-MFA token + TOTP code for a full session. Issued only when /auth/login responds with mfa_required=true. Pre-MFA token is audience-restricted so it can't be used against any downstream service if leaked.api:POST /auth/login/mfasince 0.1.0
- auth.user.mecrates/identity/src/handlers/profile.rs:22Return current authenticated user's profile (id, email, username, status). Side effect — bumps users.last_seen so the user surfaces in the online-users feed.api:GET /auth/mesince 0.1.0
- auth.user.registercrates/identity/src/handlers/register.rs:24Register a new user with email + username + password (Argon2id), returns access+refresh JWT pair.api:POST /auth/registersince 0.1.0
axum-common/audit
1 feature- axum-common.audit.clientcrates/oa-framework/src/audit.rs:70Fire-and-forget cluster-wide audit-log writer. Spawns a tokio task per event so callers never wait on the POST. Tasks are tracked in a JoinSet (behind a Mutex) so a graceful shutdown can drain in-flight POSTs via flush(). Disabled automatically when TENANTS_INTERNAL_URL is unset (dev boxes without the tenants container). Failures are warn-logged, never surfaced to the caller — audit gaps are less bad than mutations rolling back.
axum-common/auth
2 features- axum-common.auth.internal-secretcrates/oa-framework/src/auth.rs:603Constant-time X-Internal-Secret header verifier. Guards every /internal/* endpoint across the cluster. Returns 503 when the secret is not provisioned (operator misconfiguration) and 403 on mismatch, so call sites can ?-propagate the error tuple unchanged.
- axum-common.auth.jwt-verifiercrates/oa-framework/src/auth.rs:77Generic JWT validator trait for Axum services. Each service implements JwtVerifier on its own AppState, adapting its native Claims struct into JwtClaims<Sub>. The AuthUser extractor is generic over S: JwtVerifier, so the same extractor works across every service without forcing a single concrete claims shape.
axum-common/circuit-breaker
1 feature- axum-common.circuit-breaker.breakercrates/oa-framework/src/circuit_breaker.rs:82Three-state (Closed → Open → HalfOpen) per-target circuit breaker. Process-local DashMap registry — same target name from any caller returns the same shared state. Configurable trip threshold and cool-down via OA_CB_<TARGET>_THRESHOLD / OA_CB_<TARGET>_COOLDOWN_MS env vars. Prevents cascading failure when downstream services (voice, anim, scheduler) go hard-down; callers fast-fail instead of queuing full-timeout round-trips.
axum-common/cors
1 feature- axum-common.cors.env-or-defaultcrates/oa-framework/src/cors.rs:72CORS layer builder with runtime override support. Reads CORS_ORIGINS (comma-separated) for per-deployment configuration; falls back to the baked-in *.blal.pro + localhost dev origin list when unset or empty. Invalid entries are warn-logged and skipped; if all entries are invalid the default list is used so a typo can't silently disable CORS. Always sets allow_credentials(true) for SSO cookie transport.
axum-common/crate
1 feature- axum-common.crate.shared-http-plumbingcrates/oa-framework/src/lib.rs:1Shared Axum HTTP plumbing crate consumed by every OpenAlice backend service. Centralises JWT auth, SSO cookies, CORS, health, Prometheus metrics, per-tenant rate limiting, circuit breaking, and cluster-wide audit logging so security fixes and ecosystem-wide changes land in one place. Consumers: openalice-auth, social-api, voice, persona, atlas-api, rt, a2a.
axum-common/db
1 feature- axum-common.db.rlscrates/oa-framework/src/db.rs:4`init_rls_pool` seeds every connection with the
axum-common/health
1 feature- axum-common.health.routecrates/oa-framework/src/health.rs:20Shared unauthenticated GET /health → "ok" route. No auth, no rate limit — safe for Traefik health checks, Prometheus probes, and Docker HEALTHCHECK. Exported both as a bare handler (health_handler) for attachment to an existing router and as a standalone Router<S> (health_route) for merging with a different state.
axum-common/http
1 feature- axum-common.http.internal-clientcrates/oa-framework/src/http.rs:27Standard reqwest::Client for service-to-service /internal/* calls. Preconfigured with 5 s timeout, Rustls (no OpenSSL), and the openalice-internal/<version> user-agent. Centralises client construction so timeout, TLS, and agent string can't drift across services.
axum-common/http-metrics
1 feature- axum-common.http-metricscrates/oa-framework/src/http_metrics.rs:1Shared Prometheus HTTP-request latency middleware + recorder installer. Used by every OpenAlice backend service as the foundation for cross-service observability. Emits two metric families: - http_requests_total{method, path, status} counter - http_request_duration_seconds{method, path, status} histogram The `path` label is the MATCHED ROUTE TEMPLATE (e.g. `/v1/embed/conversations/{id}/message`), not the concrete request URI. This bounds label cardinality — without it a unique visitor id per path would produce unbounded label sets and explode Prometheus storage.
axum-common/metrics
1 feature- axum-common.metrics.buildercrates/oa-framework/src/metrics.rs:14Lightweight Prometheus exposition-format string builder. Appends HELP + TYPE + value lines via a fluent .gauge()/.counter() API. For services that emit only a handful of metrics and don't need a full metrics-exporter-prometheus recorder. metrics_response() wraps the output in the correct text/plain; version=0.0.4 Content-Type for Prometheus.
axum-common/rate-limit
2 features- axum-common.rate-limit.ipcrates/oa-framework/src/rate_limit.rs:347Per-IP fixed-window limiter for the UNAUTHENTICATED sensitive endpoints that have no `tid` claim to key on — signup (account-creation spam) and sso/resolve (email/domain enumeration oracle). [`TenantRateLimit`] deliberately PASSES THROUGH unauthenticated requests (so health + login are never 429'd), which leaves those public routes relying only on Traefik's generous per-IP edge cap. This limiter is the application-layer second line: a tighter, per-route per-IP bucket. Same fixed-window token-bucket as [`TenantRateLimit`], keyed on the spoof-safe client IP.
- axum-common.rate-limit.tenantcrates/oa-framework/src/rate_limit.rs:82Per-tenant token-bucket rate limiter for Axum services. Fixed-window (default 600 req/min, 60 s window) keyed on the JWT tid claim. tenant_rate_limit_layer() wires it as an Axum middleware: passes requests through when under quota, returns 429 + Retry-After when exhausted. Unauthenticated requests pass through so health checks and login routes are never accidentally blocked.
axum-common/security-headers
1 feature- axum-common.security-headerscrates/oa-framework/src/security_headers.rs:1Shared API security-response-headers middleware applied by every OpenAlice backend service so a consistent, hardened header set lands on every JSON response in one place. These are tuned for JSON APIs sitting behind Traefik TLS — NOT HTML pages: - NO Content-Security-Policy (no markup/script context to constrain). - NO Strict-Transport-Security (HSTS is terminated by Traefik at the TLS edge; emitting it from the app would be redundant and risk a weaker max-age clobbering the edge policy). The audit found NO security headers on any API response; this closes that gap fleet-wide. CORS is deliberately out of scope here — it is configured separately via the `cors` module and must not be touched by this layer.
axum-common/tracing
1 feature- axum-common.tracing.otlp-exportcrates/oa-framework/src/tracing.rs:1Shared tracing initialisation + W3C trace-context propagation for every OpenAlice backend service. Wraps the per-service `tracing_subscriber::fmt` boilerplate in one place and, WHEN an OTLP endpoint env var is set, adds a batch OpenTelemetry span exporter so traces land in the shared Tempo instance (cross-service distributed tracing). When the env var is unset the exporter is a hard no-op and behaviour is byte-for-byte the old fmt-only setup — a service that hasn't opted in cannot regress.
composites
1 feature- ui.composites.emojipackages/ui/src/composites/Emoji.tsx:6Curated inline-SVG emoji registry (Fluent flat, MIT) — renderssince 0.18.4
dashboard/admin
3 features- dashboard.admin.overviewapps/dashboard/src/app/(shell)/admin/page.tsx:2Admin overview — platform stats (total users, active today, active 7d, admins, banned) via client.adminGetStats() + recent-users preview via client.adminGetUsers({per_page:10}). Non-admins receive a 403 and are redirected to /. Links to /admin/users for the full list.
- dashboard.admin.user-detailapps/dashboard/src/app/(shell)/admin/users/[id]/page.tsx:2Admin user detail — full profile view for a single user (username, email, display name, active status, joined, last seen, UUID). Action panel: grant/revoke admin via client.adminSetAdmin(id, bool); ban/unban via client.adminBanUser / adminUnbanUser. All mutations confirm before executing. Redirects non-admins (403) to /.
- dashboard.admin.user-listapps/dashboard/src/app/(shell)/admin/users/page.tsx:2Admin user list — paginated (20/page) table with 400ms debounced search by username or email via client.adminGetUsers(). Per-row ban/unban actions (client.adminBanUser / adminUnbanUser). Admin-flagged users show ShieldCheck icon; ban/unban actions are suppressed for admins. Redirects non-admins (403) back to /.
dashboard/auth
1 feature- dashboard.auth.loginapps/dashboard/src/app/login/page.tsx:2Login page — email + password form using useLoginAction from @openalicelabs/ui. Auto-redirects authenticated users to /. Displays inline Banner on auth failure. Entry point for the entire dashboard; all protected pages redirect here on 401.
dashboard/billing
1 feature- dashboard.billing.plan-managementapps/dashboard/src/app/(shell)/billing/page.tsx:2Billing page — shows current plan card (monthly cost, usage, remaining calls, renewal date) sourced from useBilling. Renders a plan comparison grid (Free / Starter / Pro / Enterprise) with EUR pricing; Pro is highlighted as "Most Popular". Upgrade/downgrade via changePlan() with immediate effect. Includes FAQ section. Prices are net; VAT at checkout.
dashboard/components
2 features- ui.composites.lily-page-blockspackages/ui/src/composites/LilyPageBlocks.tsx:38S2 P0.5 «Полки» — the shelves block (pressable item cards /since 0.23.1
- ui.composites.tier-cardpackages/ui/src/composites/TierCard.tsx:9Presentational pricing tier card (name/price/blurb/quota + footer slot), pure props.since 0.4.0
dashboard/keys
1 feature- dashboard.keys.manageapps/dashboard/src/app/(shell)/keys/page.tsx:2API key management — lists all active keys (name, created, last-used) sourced from useKeys. Create-key dialog with name input; on success shows one-time plaintext reveal with CopyButton (key is never shown again). Supports per-key deletion with optimistic loading state.
dashboard/lib
2 features- dashboard.lib.sdk-clientapps/dashboard/src/lib/sdk-client.ts:2Thin re-export of OpenAliceClient, ApiRequestError, and RateLimitError from @openalicelabs/sdk. Pages that need direct client access (admin pages) import from here; useAuth().client is the live singleton pre-configured with the JWT. Local sdk-client is now a thin re-export of @openalicelabs/sdk. Was a verbatim copy of OpenAliceClient — same source of truth.
- dashboard.lib.sdk-typesapps/dashboard/src/lib/sdk-types.ts:2Thin re-export of all @openalicelabs/sdk TypeScript types used in the dashboard — AdminStats, AdminUser, ApiKey, BillingStatus, UsageResponse, etc. Single source of truth for platform API shapes; no local type duplication. Local sdk-types is now a thin re-export of @openalicelabs/sdk types. Identical content was duplicated; SDK is the single source of truth.
dashboard/overview
1 feature- dashboard.overview.homeapps/dashboard/src/app/(shell)/page.tsx:2Landing overview — shows four stat cards (API calls used, current plan, limit, plan status) sourced from useBilling, plus three quick-action cards linking to /keys, external docs, and /billing. Guards auth via useAuthGuard; renders Skeleton placeholders while loading.
dashboard/pages
3 features- dashboard.page.errorapps/dashboard/src/app/error.tsx:16Root error boundary — EmptyState panel (dashboard design tokens) with a retry (reset()) and a link back to the overview.page:/errorsince 0.1.0
- dashboard.page.global-errorapps/dashboard/src/app/global-error.tsx:13Root-layout error boundary — own html/body, no external deps beyond globals.css, plain Tailwind utility classes (no component imports that could themselves be the failure). Reload via reset().page:/global-errorsince 0.1.0
- dashboard.page.not-foundapps/dashboard/src/app/not-found.tsx:25Root 404 — EmptyState panel (dashboard design tokens) with a link back to the overview. Internal tool, no i18n.page:/404since 0.1.0
dashboard/shell
4 features- dashboard.shell.root-layoutapps/dashboard/src/app/layout.tsx:2Root layout — mounts AuthProvider (JWT lifecycle) + ToastProvider around the entire app. Binds the SDK client to NEXT_PUBLIC_API_URL (default api.blal.pro). Sets Open Graph metadata and forces `robots: noindex` (internal tool).
- dashboard.shell.route-group-layoutapps/dashboard/src/app/(shell)/layout.tsx:2Route-group layout — hosts DashboardShell (sidebar nav + EcosystemShell rail) for every authenticated page. All routes except /login live under this group, so DashboardShell wraps them ONCE here instead of once per page.tsx. Route groups (parenthesized folder) are invisible to the URL: (shell)/keys/page.tsx still serves /keys.
- dashboard.shell.sidebar-layoutapps/dashboard/src/test/DashboardShell.test.tsx:2Component tests for DashboardShell — the persistent sidebar layout shell. Verified behaviours: - Base nav (Overview / API Keys / Usage / Billing) always renders, each link pointing at the right href. - The Admin nav item renders ONLY when the authenticated user is an admin (user.is_admin === true); it is absent for non-admins and when user is null. - Active-state styling: the nav item matching the current pathname carries the active class (bg-surface-3); others carry the inactive class. - Prefix-match: a nested route (e.g. /admin/users) marks the /admin item active, while "/" only matches exactly (never as a prefix). - Sign Out wires through useLogoutAction() and then routes to /login. - Children render inside the <main> content region. - The shell wraps EcosystemShell with currentProduct="dashboard". @openalicelabs/ui is mocked to lightweight passthrough components so the test exercises DashboardShell's own logic (nav set, active class, logout wiring) without the real design-system runtime (CSS, ecosystem context, auth WS). next/navigation is aliased to src/test/__mocks__/next-navigation.ts via vitest.config.ts; next/link is stubbed inline to a plain <a>.
- dashboard.shell.sidebar-layoutapps/dashboard/src/components/DashboardShell.tsx:2Persistent layout shell — responsive sidebar (desktop fixed / mobile slide-in) with nav links: Overview, API Keys, Usage, Billing, and Admin (admin users only). Wraps EcosystemShell to inject the cross-product rail (Hub/Social/World/Persona/ Live/Dashboard). Reads `user.is_admin` from useAuth to conditionally show the Admin nav item.
dashboard/team
1 feature- dashboard.team.manageapps/dashboard/src/app/(shell)/team/page.tsx:2Team management for the ACTIVE organization (the Google-console model: teams/roles/invites live in THE global dashboard, never in a product app). Roster with role changes + removal (last-owner protected server-side), invite-by-email with a copyable token (email delivery deferred), pending invites list. All authorization is enforced by the platform API through the shared RBAC catalog — this page only renders what the caller may do.
dashboard/test
4 features- dashboard.test.configapps/dashboard/vitest.config.ts:2Vitest configuration for the openalice-dashboard Next.js app. Mirrors the sibling openalice-embed-app harness (jsdom + @vitejs/plugin-react + jest-dom). Environment: jsdom (React component rendering via @testing-library/react). React plugin: @vitejs/plugin-react (JSX transform; fast-refresh in watch mode only — no effect on `vitest run`). Path alias: @/ → src/ mirrors the tsconfig `paths` entry so test imports resolve the same way as production imports. next/navigation stub: DashboardShell imports { usePathname, useRouter } at module load. The real module reads Next's app-router context and throws outside a Next render, so it is aliased to a lightweight stub. (This app is a pure client dashboard with no API routes, so — unlike embed-app — it needs no server-only / next/headers / next/server stubs.)
- dashboard.test.error-pagesapps/dashboard/src/test/ErrorPages.test.tsx:2Root not-found / error / global-error — the internal-tool trio (brand canon sweep, 2026-07-15). Minimal, English-only, dashboard design tokens (@openalicelabs/ui EmptyState/Button). @openalicelabs/ui is mocked to lightweight passthroughs (same recipe as DashboardShell.test) so these tests exercise the pages' own logic (reset wiring, links) without the real design-system runtime.
- dashboard.test.mocksapps/dashboard/src/test/__mocks__/next-navigation.ts:2Minimal stub for `next/navigation`, aliased in vitest.config.ts. The real module reads Next's app-router context via AsyncLocalStorage and throws when imported outside a Next render, so we provide lightweight vi.fn()s that client components (DashboardShell, the page components) can import without a Next runtime. Tests drive behaviour by overriding the mocked return values, e.g. (usePathname as Mock).mockReturnValue("/keys"); and assert navigation via the shared `routerMock.push` / `routerMock.replace` spies. Call `resetNavigationMocks()` in beforeEach to clear state between tests.
- dashboard.test.setupapps/dashboard/src/test/setup.ts:2Global test setup file — runs once per test suite before any tests. Registers @testing-library/jest-dom custom matchers (toBeInTheDocument, toHaveTextContent, toHaveAttribute, etc.) on the vitest `expect` object so every test file can use them without a local import.
dashboard/usage
2 features- dashboard.usage.analyticsapps/dashboard/src/test/UsagePage.test.tsx:2Component tests for the Usage analytics page. The page reads useBilling() and useUsage(30) and renders: a loading state, an error banner, the monthly usage card (percentage + threshold colouring), a per-endpoint breakdown, and the headline stat cards (total / endpoints / remaining). Verified behaviours: - Loading state: while either hook is loading, a shaped Skeleton placeholder shows (sr-only "Loading usage data…" label for a11y) and the real usage card does not. - Error state: a billing OR usage error surfaces in the danger banner. - Auth guard: the page delegates session verification + the 401 redirect entirely to the shared useAuthGuard() hook (no more hand-rolled useEffect + router.replace — see feedback-... useAuthGuard adoption, 2026-07 componentization wave). This test only asserts the page calls the hook; useAuthGuard's own redirect behaviour is covered by its own unit tests in @openalicelabs/ui. - Usage card: renders the percentage from billing.usage.percentage and the used/limit call counts. - Over-80% warning banner appears; under-80% it does not. - Endpoint breakdown: each endpoint name + rounded avg latency renders, and the "Endpoints Used" stat equals endpoints.length. @openalicelabs/ui is fully mocked: the data hooks (useAuthGuard/useBilling/ useUsage) are spies, and every visual primitive is a passthrough that renders its children / title so text assertions hit real DOM. UsagePage no longer wraps DashboardShell itself (moved to app/(shell)/layout.tsx, 2026-07 shell refactor) — this test renders the page in isolation, so no shell/nav mocks are needed. next/navigation is the global vitest stub (src/test/__mocks__/next-navigation.ts).
- dashboard.usage.analyticsapps/dashboard/src/app/(shell)/usage/page.tsx:2Usage analytics — rolling 30-day window via useUsage(30) + current billing period from useBilling. Monthly progress bar with warning/danger colour thresholds (>80% warning, >95% danger). Endpoint breakdown: call counts + avg latency per endpoint, headline stat cards (total, endpoints used, remaining).
embed-app/component
2 features- embed-app.component.fieldpackages/ui/src/composites/Field.tsx:4Shared, accessible form-field primitive for the dashboard. Wraps a single control (`<input>`/`<textarea>`/`<select>`) with a label (+ required marker), an optional hint, a LIVE character counter (when `maxLength` is set), and an inline error — all wired to the control via `aria-invalid` + `aria-describedby` so screen readers announce state correctly. Works in BOTH forms: • Controlled — pass `value` (drives the counter directly). • Uncontrolled — pass a `defaultValue` on the child control; the counter is driven by the bubbling `input` event (server-action forms submit the raw DOM value, so no `value`/`onChange` round-trip is required). The child control is given the wiring props in one of two ways: • children-as-element → props are merged onto it via `cloneElement` (the common case: `<Field ...><input className="oa-input" /></Field>`). • children-as-function (render-prop) → the props object is handed to the caller to spread, for cases that need full control over the element. Copy is localized via the shared `fieldKit` i18n namespace (charsLeft / required / optional). Tokenized + reduced-motion + high-contrast safe in Field.module.css.
- embed-app.component.oapackages/ui/src/styles/field.css:2Self-contained styles for the shared <Field> primitive. Every value is tokenized against the global custom properties declared in globals.css (--sakura* / --cream* / --ink-soft / --mute / --line* / --r-* / --shadow-* / --ease / --font-mono / --font-display) so Field inherits the dashboard theme automatically and stays consistent with the oa-field primitives.
embed-app/ui
1 feature- embed-app.ui.confirm-inlinepackages/ui/src/composites/ConfirmInline.tsx:4Inline danger banner that replaces browser `window.confirm()` for destructive actions. Renders at the action site as a slide-in panel (~200 ms) so the user sees consequence-clear copy without a modal interruption. Usage (controlled): const [confirmingId, setConfirmingId] = useState<string | null>(null); ... <ConfirmInline open={confirmingId === item.id} heading="Delete this webhook? We'll stop sending events to it." confirmLabel="Delete" onConfirm={() => { setConfirmingId(null); doDelete(item.id); }} onCancel={() => setConfirmingId(null)} /> For high-stakes actions pass `requireTyping="DELETE"` — the confirm button stays disabled until the user types the exact value.
identity/scim
2 features- identity.scim.directory-synccrates/identity/src/handlers/scim/mod.rs:4Enterprise SCIM 2.0 endpoints under `/scim/v2/*` so an IdP (Okta / Azure AD / Entra) auto-provisions + deprovisions org members. v1 = Users + discovery: POST/GET/PUT/PATCH/DELETE Users (userName = email; `active` toggles membership), plus ServiceProviderConfig / ResourceTypes / Schemas for IdP validation. Auth is a per-org SHA-256-hashed bearer token (the bearer IS the grant — no session JWT), resolved directly against org_scim_token (identity's SCIM tables carry no RLS). Provisioning routes through the shared `sso_support::provision_member` / `sso_support::deactivate_member` seam the OIDC/SAML JIT path also uses. ## SCIM error format (RFC 7644 §3.12) SCIM has its OWN error envelope, distinct from the `/me/*` JSON `{error}` "status":"<code>","detail":"<msg>"}` with content-type `application/scim+json`. [`scim_err`] builds it. ## Security posture (carried over VERBATIM) * Fail-CLOSED bearer auth (see [`auth::ScimAuth`]); token stored as a SHA-256 hash only, never logged. * Within an authenticated SCIM token the org is FIXED (the token's org), so a SCIM 404 reveals nothing cross-org.
- identity.scim.token-configcrates/identity/src/handlers/me_scim.rs:4Owner/admin-only management of an org's SCIM bearer token (org_scim_token). POST generates a fresh 256-bit token, stores its SHA-256 hash + an 8-char display prefix, and returns the plaintext ONCE (never again). GET reports whether SCIM is configured + the prefix + last_used_at (NEVER the token). DELETE revokes the token. Gated by the owner/admin SSO-management check (`sso_support::can_manage_sso`) — SCIM is the same enterprise-directory tier as OIDC/SAML SSO. Integration seams changed from tenants: org_id-keyed plain SQL (no RLS), the caller's role resolves from `organization_members`, the audit write is a structured tracing event. The token SECURITY posture (256-bit CSPRNG, SHA-256 hash stored, plaintext returned once, never logged) is UNCHANGED.
identity/sso
2 features- identity.sso.config-crudcrates/identity/src/handlers/me_sso.rs:4Owner/admin-only CRUD for an org's SSO provider config (org_sso). GET returns the config WITHOUT the client_secret (only a `has_client_secret` flag). PUT upserts issuer/client_id/secret/domain (OIDC) or IdP entityID/SSO-URL/signing-cert (SAML) + the auto_provision + enabled switches; the OIDC client_secret is AES-256-GCM encrypted at rest before it touches the DB and is NEVER returned. DELETE removes the config. Gated by the owner/admin SSO-management check (`sso_support::can_manage_sso`). Integration seams changed from tenants: org_id-keyed plain SQL (no RLS), the caller's role resolves from `organization_members`, the client_secret is encrypted with identity `crypto::encrypt_secret`, and the audit write is a structured tracing event. Every INPUT VALIDATION + SECURITY check (https-only issuer, domain normalisation, X.509 cert parse/normalise, secret never echoed) is carried over VERBATIM.
- identity.sso.public-flowcrates/identity/src/handlers/sso.rs:7Authorization-Code-with-PKCE OIDC login + SP-initiated SAML 2.0 login for an org's dashboard users. /start performs discovery, mints PKCE(S256) + state + nonce (server-side, single-use, 10-min TTL) and 302s to the IdP. /callback (OIDC) validates state, exchanges the code (with the PKCE verifier), cryptographically verifies the id_token (JWKS-by-kid signature, iss/aud/exp/nonce), enforces the org's allowed_email_domain on a verified email, resolves the org member (existing organization_members, or JIT-provisions a 'member' when auto_provision is on), mints an org-scoped session JWT and 302s to the dashboard with the HttpOnly oa_dash_session cookie. /acs (SAML) runs the full XSW + profile checklist then reuses the SAME post-validation seam. SECURITY POSTURE — carried over from the tenants original VERBATIM: * Fails CLOSED on every validation failure (state, code exchange, id_token signature/claims, SAML assertion, domain, provisioning). * Never logs the id_token, access_token, code, client_secret, or any verifier/nonce. Error pages carry an opaque generic message. * state/nonce are 256-bit CSPRNG; PKCE is S256; the login-state row is single-use (consumed atomically on first redeem) and short-TTL. * IdP HTTP uses the dedicated 30s client with redirects disabled. Only the INTEGRATION SEAMS differ from the tenants original: org_id-keyed plain SQL (no RLS / no SECURITY DEFINER carve-outs), the client_secret is decrypted with identity `crypto::decrypt_secret`, the auth user is resolved DIRECTLY via `handlers::admin::ensure_user_by_email` (no HTTP hop), member provisioning goes through `sso_support`, and the session JWT is minted with identity's `auth::issue_org_token` (same `oa_dash_session` cookie).
landing/api
4 features- landing.api.waitlistapps/landing/src/test/waitlist-route.test.ts:2Tests for POST /api/waitlist — the real lead-capture endpoint. Contract under test: 1. A valid {email, product} appends one JSONL line and returns { ok:true, status:"added" } (email lowercased + trimmed in the record). 2. A re-submit of the same (email, product) returns status:"already" without a second append. 3. Invalid email → 400 { error:{ code:"invalid_email" } }. 4. Unknown product → 400 { error:{ code:"invalid_product" } }. 5. Malformed JSON → 400 { error:{ code:"bad_json" } }. 6. Over the per-IP rate limit → 429 { error:{ code:"rate_limited" } }. 7. De-dupe survives a "restart": a fresh module load whose readFile returns a persisted lead reads that lead back as "already" with no second append (the boot-replay hydration of the seen-set, W1.2). fs is mocked so the test never touches disk; we assert the appended payload.
- landing.api.waitlistapps/landing/src/app/api/waitlist/route.ts:22POST /api/waitlist {email, product, ts} — real pre-launch lead capture replacing the decorative #reserve/#verify/#create anchors + /forge mailto. Validates email (RFC-lite), in-memory per-IP sliding-window rate-limit (5/min), and appends one JSONL line per lead to data/waitlist.jsonl on the bind-mounted repo path (process.cwd()=/app) so signups survive container rebuilds. De-dupes per (email, product) in-process → status "already"; the de-dupe Set is lazily hydrated ONCE from the persisted JSONL on the first POST, so a lead recorded before a redeploy is still recognized as "already" (W1.2 — survives container restarts) instead of writing a duplicate line. Errors use the {error:{code,message}} envelope; success {ok:true,status}. Email-forwarding/CRM (Brevo) is LAUNCH-GATED (needs keys); this only durably records leads.route:/api/waitlistsince 0.4.0
- landing.api.waitlist.exportapps/landing/src/test/waitlist-export-route.test.ts:2Tests for GET /api/waitlist/export — the authed lead-download endpoint. Contract under test: 1. No WAITLIST_EXPORT_TOKEN configured → 503 export_not_configured (fail-closed; leads are never served unguarded). 2. Token configured but request has no/blank token → 401 unauthorized. 3. Token configured but a WRONG token → 401 unauthorized. 4. Correct token via Bearer header → 200 text/csv with a header row + one row per persisted lead (RFC-4180 quoting of an awkward field). 5. Correct token via ?token= query param → 200 (both auth carriers work). 6. ?format=json with a correct token → 200 application/json array of the parsed records. 7. No persisted file (ENOENT) → 200 with just the CSV header (empty export), not an error. 8. Malformed lines in the JSONL are skipped, not fatal. fs is mocked so the test never touches disk; the env token is set/cleared per test.
- landing.api.waitlist.exportapps/landing/src/app/api/waitlist/export/route.ts:27GET /api/waitlist/export?format=csv|json — authed download of the captured pre-launch waitlist leads (the data/waitlist.jsonl written by POST /api/waitlist, #723). Auth is a single shared WAITLIST_EXPORT_TOKEN env accepted as `Authorization: Bearer <token>` OR `?token=<token>`; constant-time compared; missing env → 503 fail-closed (never serves leads unguarded), wrong/absent token → 401. Reads the JSONL once, tolerantly parses each line (skips blanks + malformed), and returns either text/csv with a header row + RFC-4180 quoting (default) or a JSON array (?format=json), both as a Content-Disposition: attachment download. ENOENT (no leads yet) returns an empty CSV header / empty JSON array, not an error. Node runtime + force-dynamic (reads the filesystem per request, never cached).route:/api/waitlist/exportsince 0.4.0
landing/components
2 features- landing.component.legal-page-headapps/landing/src/components/site/LegalPageHead.tsx:17Shared page-head (eyebrow kicker + h1 title + mono meta line, optionally carrying a language-switch link) for the 6 legal routes and their /de/* mirrors. Pure chrome — no legal text lives here.component:LegalPageHeadsince 0.8.0
- landing.waitlist-formapps/landing/src/app/(landing)/vault/WaitlistForm.tsx:20Reusable client lead-capture form for the pre-launch product waitlists (Soul Vault #reserve, Identity Passport #verify, Cloning Lab #create, Forge early-access). Single email input + submit + honest one-line GDPR/consent note, on the scoped .oa-embed-lp sakura tokens (.signup-email row). POSTs {email,product,ts} to /api/waitlist (real persistent capture), renders idle/submitting/added/already/error states with the honest "You're on the list" success — never a fake "we'll email you" (email/CRM is launch-gated). No-JS friendly: native required email input. Test seam: injectable `submit`.component:WaitlistFormsince 0.4.0
landing/design
1 feature- landing.embed.classesapps/landing/src/app/(landing)/embed/embed.classes.ts:12Utility recipes for the landing base system (embed.css converting bottom-up).since 0.9.0
landing/embed
31 features- embed-lp.countupapps/landing/src/app/(landing)/embed/CountUp.tsx:6Animated numeral for the /embed landing — counts from `from`component:CountUpsince 0.3.0
- landing.embed.ascii-emergenceapps/landing/src/test/AsciiEmergence.test.tsx:2Tests for AsciiEmergence — the WebGL glyph-emergence overlay on the Meet-Lily showpiece portrait. Contract under test (ASCII-first reveal, founder 2026-07-10 — the raw photo must never paint un-ASCII'd; the figure's one-way data-ascii state drives everything): 1. Under prefers-reduced-motion (or -data) the effect NEVER initializes and the figure settles to data-ascii="img" IMMEDIATELY — the static original photo, zero wait, sibling <img> untouched. 2. Without IntersectionObserver / ResizeObserver the effect can never run → data-ascii="img" immediately (never a veiled hole). 3. With IO/RO present but the cross-origin portrait failing to load, it settles to data-ascii="img" as soon as the error fires — no canvas, img inline-opacity untouched. 4. The atlas canvas readback is gesture-gated ONLY on WebKit (needsGestureGate): iOS never enhances before the first gesture (and settles to the photo at the 5s deadline, after which a late gesture no longer boots); Chromium goes straight to idle init. 5. Unmount during any pending stage is safe (no throw, no leak). 6. orderByInk (the glyph-atlas density sort) orders ascending by ink, is stable on ties, and tolerates a short ink array. The WebGL happy path is intentionally NOT simulated in jsdom (no real GL context exists); it is covered by the e2e screenshot loop. The fallback ladder above is the part that must be bulletproof — the img must look perfect with the canvas absent.
- landing.embed.ascii-emergenceapps/landing/src/app/(landing)/embed/AsciiEmergence.tsx:70ASCII-emergence living visual on the /embed Meet-Lily showpiece — Lily's portrait renders as live sakura-tinted ASCII/glyphs (petals, katakana, terminal chars) on a hand-rolled zero-dependency WebGL1 fragment shader that scrambles at ~12 Hz (interleaved-gradient-noise phase) and resolves into the real photo as the section scrolls in (scroll-mapped uProgress, both directions), sweeping face-out as a noisy settle-front, with cursor-heat glyph densification near the pointer. ASCII-first reveal (founder 2026-07-10): under JS the img starts hidden behind the .ow-veil shimmer in its exact box and the figure's one-way data-ascii state resolves pending→live (stamped right after the FIRST GL frame draws — she is born as glyphs, the raw photo never paints un-ASCII'd) or pending→img (graceful photo fallback: reduced motion/data immediately; no IO/RO/WebGL or CORS texture failure immediately; 5s no-gesture deadline + 4s post-gesture boot grace; pure-CSS 8s rescue; no-JS never hides). The atlas canvas readback is gesture-gated ONLY on WebKit (needsGestureGate — iOS/Safari Advanced Fingerprinting Protection); other engines init on requestIdleCallback after load. Glyph atlas is canvas-2D rasterized + ink-density sorted at runtime. Canvas overlays the img inside the same .ow-figure (inherits peek clip-path/transform); rAF only while intersecting + animating; DPR≤1.5.component:AsciiEmergencesince 0.2.0
- landing.embed.ascii-fieldapps/landing/src/test/AsciiField.test.tsx:2Tests for AsciiField — the ambient Mandelbrot-luminance glyph substrate behind the /embed hero. Contract under test (progressive enhancement — the hero must be perfect with the canvas blank): 1. Under prefers-reduced-motion the field NEVER initializes (no data-live, no rAF, no context request). 2. Without a canvas-2D context (headless/jsdom) it aborts silently. 3. Happy path (mocked 2D context): initializes, marks data-live, schedules rAF and paints glyphs from the canon alphabets. 4. Unmount during the pending-init window and after init are both safe (teardown cancels rAF, clears data-live). 5. The math core (hash2 / vnoise / mandelLum) is deterministic, in range, and X-axis symmetric (the logo motif's symmetry).
- landing.embed.ascii-fieldapps/landing/src/app/(landing)/embed/AsciiField.tsx:42Ambient ASCII substrate behind the /embed hero — a very faint sakura-tinted 36×18 glyph grid (terminal chars + occasional petals/katakana) on a cheap canvas-2D loop whose luminance is Mandelbrot escape-time centred on the classic full-set logo frame (X-axis symmetric, ±12% breathing zoom, per NAO's Mandelbrot steering note) modulated by drifting value-noise (0.8/0.2). Glyphs mutate on ~1.7s epochs; paints throttle to ~10fps; rAF only while the hero intersects; init on requestIdleCallback after load; radial-fade mask + 0.11 opacity in CSS (2026-06-12 audit: raised from 0.07/40×20 — invisible at 2K). Never inits under prefers-reduced-motion or without a 2D context — aria-hidden, pointer-events none, zero layout impact.component:AsciiFieldsince 0.3.0
- landing.embed.card-tiltapps/landing/src/app/(landing)/embed/EmbedTilt.tsx:15Subtle mouse-following 3D hover-tilt for the /embed content cards (.card / .inbox / .price) and the hero browser frame (.embed-hero-tilt, gentle large-perspective tilt that composes with its float animation) — a client island sets per-element --rx/--ry from the cursor position which the hover transform consumes. Mouse-only (hover:hover + pointer:fine), disabled under prefers-reduced-motion.component:EmbedTiltsince 0.2.0
- landing.embed.code-snippetapps/landing/src/test/CodeSnippet.test.tsx:2Tests for the CodeSnippet component — the self-typing terminal card on /embed. Contract under test: 1. Renders a <pre> element (the code block). 2. SSR initial state: the full snippet text is present in the DOM (CodeSnippet initialises n=FULL_LEN on the server, so no-JS/crawlers see the real install code). 3. The snippet contains the canonical embed.js URL. 4. The snippet contains the `data-widget` attribute string. 5. The traffic-light dots render (three <i> elements inside .ct). 6. Under prefers-reduced-motion the component leaves the full snippet visible and does NOT add a blinking caret. 7. In a motion-allowed environment the type-out effect resets n to 0 (starts typing), so the visible snippet is initially empty (n=0 means zero chars rendered in the pre). The ghost text still carries the full snippet.
- landing.embed.code-snippetapps/landing/src/app/(landing)/embed/CodeSnippet.tsx:22"One snippet" install block for /embed — a terminal card whose one-line install <script> self-types char-by-char on load (syntax highlighting preserved: tag/attr/str/comment) then holds a blinking caret. Full snippet is SSR'd for no-JS/crawlers; type-out is client-only, paused while the tab is backgrounded, static under prefers-reduced-motion.component:CodeSnippetsince 0.2.0
- landing.embed.corner-widgetapps/landing/src/app/(landing)/embed/EmbedCornerWidget.tsx:36The single real interactive Lily on /embed — the deployed embed.blal.pro launcher (tenant openalice, agent aria, widget wgt_embed_landing, 2D portrait, voice default) mounted bottom-right so visitors actually try the product and accept consent on their own click. Hero/showpiece are static visuals; this is the live one.component:EmbedCornerWidgetsince 0.2.0
- landing.embed.eu-flagapps/landing/src/app/(landing)/embed/EmbedEuFlag.tsx:16/embed's tight, ring-styled EU flag glyph — dedupes the trust-row + footer copies that reimplemented the same raw SVG.component:EmbedEuFlagsince 0.4.0
- landing.embed.eu-sovereignapps/landing/src/app/(landing)/embed/components/EuSovereignSection.tsx:27/embed's "Built in the EU. All of it." checkable-facts block — 4 verifiable EU-sovereignty facts (model, hosting, captcha, paperwork) as a scannable checklist, linking to /embed/security. Sits directly under TrustBeatSection, deepening its EU-flag + Mistral tiles into checkable detail. Text/RAG/hosting/captcha scope only — voice is deliberately unmentioned (behind its own consent-gate).component:EuSovereignSectionsince 0.5.0
- landing.embed.footerapps/landing/src/app/(landing)/embed/EmbedFooter.tsx:28Shared footer for the /embed product group — brand, product/company/legal columns, EU flag + locale switcher. Extracted from embed/page.tsx and promoted to the group layout so all three pages (embed, signup, security) share it.component:EmbedFootersince 0.4.0
- landing.embed.hero-stageapps/landing/src/app/(landing)/embed/EmbedStage.tsx:32Hero in-situ browser-frame for /embed — a floating mock customer site (Lumière) with a STATIC visual of the Lily voice widget docked in-corner (role=img illustration, no live mic/consent), a self-playing typewriter caption that flows continuously (a rolling two-line window — the finished line rolls up dimmed while the next types beneath; never a visible reset; reduced-motion-static + tab-visibility aware) and a "1 line of code" callout. The real interactive widget is the page corner launcher.component:EmbedStagesince 0.2.0
- landing.embed.hero-stageapps/landing/src/test/EmbedStage.test.tsx:2EmbedStage render tests — the hero browser mock with the ghost-cursor mini-scenario (hero living pass, 2026-06-12). Contract under test: 1. The ghost cursor renders inside the mock .site area, aria-hidden and as a non-interactive <span> (pure CSS animation, no JS driver). 2. It sits alongside the docked widget (the scenario's target) and the mock renders its role=img illustration + first caption statically under reduced motion. 3. INFINITE FLOW (NAO, 2026-06-12): with motion allowed the caption loop never visibly resets — when a line finishes its hold, it rolls up into the dimmed .ow-cap__prev slot while the next line types beneath; the typing line is never blanked. Under reduced motion no previous slot ever appears (static single line).
- landing.embed.layoutapps/landing/src/app/(landing)/embed/layout.tsx:2Group layout for the /embed product pages (embed, signup, security) — mounts the shared EmbedNav + EmbedFooter chrome ONCE (2026-07-13; signup/security previously hand-rolled a bare logo link with no nav/footer) plus the live Lily corner widget ONCE for the whole group (one source of truth; the (main) products-site layout does the same for its group), so every page carries the live demo and the conversation persists across the site.layout:(landing)/embedsince 0.2.0
- landing.embed.leads-inboxapps/landing/src/app/(landing)/embed/LeadsInbox.tsx:49"What you wake up to" living-hero leads-inbox demo for /embed — a never-empty rolling feed of example overnight leads inside a fixed-height list viewport (the box never resizes mid-loop, so no page jump). RESTING STATE IS THE DEFAULT: rows are server-rendered fully visible with no animation classes, so static captures and no-JS always see a populated inbox. The paper-ball toss (arced translateX×translateY flight with a ~300° tumble, squash-and-settle bounce, one-shot sakura ring-pulse + 1px hairline row flash at landing, unfold + shimmer + intent type-on) is an IntersectionObserver-gated enhancement applied only to client-inserted rows while the inbox is in view — it stops off-screen and re-runs on re-entry. The mono "N captured tonight" counter starts from the REAL captured-lead total when the optional `capturedCount` prop is passed (the /embed page reads it server-side from data/waitlist.jsonl via lib/waitlistCount, #723) and otherwise from the example row count (VISIBLE); it ticks ON each landing (impact beat) and honestly resets to that same resting baseline + 1 when the example pool loops — so the demo animation builds on the honest number instead of overwriting it. A 0/absent count keeps the original example-only behavior. The example rows themselves stay example (the header + the line below say so). Each lead wears a self-hosted CC0 DiceBear Lorelei avatar. Pure-CSS choreography; static fully-populated under prefers-reduced-motion (showing the real total when provided); paused while the tab is backgrounded.component:LeadsInboxsince 0.2.0
- landing.embed.leads-inboxapps/landing/src/test/LeadsInbox.test.tsx:2Tests for LeadsInbox — the "what you wake up to" living-hero rolling feed of example overnight leads on /embed. Contract under test: 1. SSR-shape render: three lead rows are ALWAYS present (never empty), each with a name, intent, tag and a decorative avatar (alt="") served from the self-hosted /embed-assets/leads/ directory — no runtime third-party (DiceBear API) calls. 2. The honesty label "Leads · example" stays in the inbox header. 3. The mono counter reads "N captured tonight" and ticks ON each LANDING (the toss impact beat, IMPACT_MS after the row is inserted), then HONESTLY resets when the example pool loops. 4. Under prefers-reduced-motion the feed is static: rows never roll, the count is the fixed full-night value (7), no timers loop. 8. RESTING STATE IS THE DEFAULT (2026-06-12 audit): the SSR trio render WITHOUT `.is-stream` (fully visible, zero animation classes — static captures never see empty/mid-flight rows); only client-inserted rows carry `.is-stream`. With IntersectionObserver present the roll loop is gated on the inbox being in view (starts on entry, stops on exit, re-runs on re-entry); without it (jsdom) it simply runs. 5. Paper-ball structure (NAO feedback 2026-06-12): rows live inside a FIXED-HEIGHT `.inbox__list` viewport (height: calc(var(--lead-h)*3) + overflow hidden in embed.css — the box never resizes mid-loop, no page jump) and each avatar is nested `.lead__slot > .lead__avatar` for the arced toss-in. The old left-edge `.inbox__flow` stream line is GONE — from the DOM and from embed.css. 6. Unmount during the pending roll window is safe (timers cleared). 7. Asset guard: every POOL avatar exists on disk under /public and carries the embedded CC0 1.0 license metadata (DiceBear Lorelei, creator Lisa Wischofsky) — keeps committed SVGs and POOL in sync. 9. Real captured-count seed: the optional `capturedCount` prop (the real lead total the /embed page reads server-side from data/waitlist.jsonl, #723) seeds the resting + reduced-motion count and the loop's wrap reset (baseline + 1) with the real number; a 0 / absent count keeps the original example-only behavior, so the example rows stay example.
- landing.embed.leads-inbox-countapps/landing/src/app/(landing)/embed/LeadsInboxCount.tsx:27Suspense boundary around the real captured-lead count read — decouples /embed's TTFB from the data/waitlist.jsonl disk read; fallback matches LeadsInbox's own empty-state rendering.component:LeadsInboxCountsince 0.4.0
- landing.embed.live-widgetapps/landing/src/app/(landing)/embed/EmbedLiveWidget.tsx:22Client-only mount of the deployed embed.blal.pro widget into a specific in-page container (not the corner launcher) via window.OpenAlice.mount, used twice on /embed (hero voice + showpiece chat). Loads the type=module bundle once, polls for the global, and calls widget.destroy() on unmount.component:EmbedLiveWidgetsince 0.2.0
- landing.embed.live-widgetapps/landing/src/test/EmbedLiveWidget.test.tsx:2Tests for EmbedLiveWidget — the client component that polls for window.OpenAlice and mounts the deployed embed bundle into a DOM container. Contract under test: 1. Renders a div[role=group] with the supplied aria-label. 2. Applies the className prop to the container div. 3. When window.OpenAlice is already present, calls .mount() with the expected fixed args (tenant, agent, inline=true, autoStartVoice=false). 4. mount() receives the defaultMode prop value. 5. Optional props (avatarImageUrl, brandName, tenantName) are forwarded when provided, and omitted when absent. 6. Calls widget.destroy() when the component unmounts. 7. When window.OpenAlice is absent, the component does NOT throw and keeps its container rendered while the poll is pending. 8. Unmounting while the poll is still pending (window.OpenAlice never appeared) does NOT throw and cleans up without leaving the container. POLL-CLEANUP (FIXED, commit 151466f "cancel the embed-bundle poll on unmount (AbortSignal)"): loadEmbedBundle() now takes an AbortSignal. The recursive window.setTimeout tick checks signal.aborted and bails, and the effect's cleanup calls controller.abort() — which clearTimeout()s the pending tick and rejects the in-flight promise with an AbortError that the .catch() swallows silently. So a widget unmounted before window.OpenAlice lands no longer leaks recursive ticks until the 15 s deadline nor logs a spurious console.warn on teardown. See loadEmbedBundle() in EmbedLiveWidget.tsx.
- landing.embed.navapps/landing/src/app/(landing)/embed/EmbedNav.tsx:25/embed group top navigation (logo + Meet Lily/Install/Pricing/FAQ + "Add to your site" CTA). Chrome/behavior lives in the shared Nav component; mounted once by the group layout for /embed, /embed/signup and /embed/security.component:EmbedNavsince 0.2.0
- landing.embed.navapps/landing/src/test/EmbedNav.test.tsx:2Tests for EmbedNav — the /embed top nav with mobile hamburger menu. Contract under test: 1. Renders all expected navigation links (Meet Lily, Install, Pricing, FAQ). 2. Renders the "Add to your site" CTA linking to /embed/signup. 3. Mobile menu toggle button is present with correct aria-label and aria-expanded=false initially. 4. Clicking the toggle opens the mobile menu (aria-expanded=true, .is-open on the navmenu). 5. Clicking the toggle a second time closes the menu. 6. Pressing Escape closes the open menu. 7. Clicking a nav link inside the open menu closes it.
- landing.embed.revealapps/landing/src/app/(landing)/embed/EmbedReveal.tsx:28Client-only scroll-reveal island for /embed — ports the approved design's IntersectionObserver that adds `.in` to `.reveal` elements as they enter the viewport (threshold 0.12, unobserve once shown). Behaviour-only, renders nothing, SSR-safe.component:EmbedRevealsince 0.2.0
- landing.embed.showpieceapps/landing/src/test/EmbedShowpiece.test.tsx:2EmbedShowpiece tests — the "Meet Lily" self-playing chat pedestal. Contract under test: 1. SSR shape: the first scenario's FULL close (question → reply → one-tap card) renders statically — no-JS and reduced-motion both get a real exchange, never a blank chat. 2. Exit beat (2026-06-12 audit): between scenarios the finished exchange fades out via .ow-msgs--leaving (messages stay in the DOM during the beat — they drift up + fade in CSS) and only THEN does the next scenario's question land; the chat never hard-resets. 3. Under prefers-reduced-motion no timers run; the static close stays.
- landing.embed.showpieceapps/landing/src/app/(landing)/embed/EmbedShowpiece.tsx:29"Meet Lily" showpiece pedestal for /embed — a self-playing chat-mode visual of Lily (role=img illustration) on a radial sakura glow that CYCLES through several distinct conversion closes (SSO→demo, volume→quote, migration, pricing): each plays question → typing → reply → one-tap outcome card, holds, then the finished exchange drifts up and fades (exit beat — no hard reset frame) before the next scenario's question lands (never blank). Full static first close under prefers-reduced-motion; paused while the tab is backgrounded. The real interactive widget is the page corner launcher.component:EmbedShowpiecesince 0.2.0
- landing.embed.signup-email-formapps/landing/src/app/(landing)/embed/signup/SignupEmailForm.tsx:16On-page email capture for /embed/signup — single email input + primary submit that redirects (pure client navigation, no backend, nothing stored) to the real dashboard signup at https://embed.blal.pro/login?email=<encoded>&mode=signup so the email arrives prefilled; the native GET form action doubles as the no-JS fallback producing the identical URL.component:SignupEmailFormpage:/embed/signupsince 0.2.0
- landing.embed.signup-email-formapps/landing/src/test/SignupEmailForm.test.tsx:2Tests for SignupEmailForm — the /embed/signup on-page email capture. Contract under test: 1. Renders a required email input and a primary (.btn-p) submit button. 2. Native no-JS fallback: form action=https://embed.blal.pro/login, method=get, with a hidden mode=signup input after the email input. 3. Submitting with an email navigates (pure client navigation) to 4. The email is trimmed and encodeURIComponent-encoded. 5. Submitting empty / whitespace-only does not navigate. 6. The /embed/signup page renders the form as primary CTA and keeps the founders@ mailto links as secondary options.
- landing.embed.signup.full-formapps/landing/src/app/(landing)/embed/signup/FullSignupForm.tsx:35Flag-hidden full registration form on /embed/signup (EMBED_SIGNUP_FULL=1 or ?full=1 preview) — username/email/password with live strength meter, honeypot, GDPR consent gate, inline translated errors (stable codes), a11y wiring (aria-invalid/describedby, autocomplete new-password). Success → identity session cookies already set (same-origin proxy forwards Domain=.blal.pro Set-Cookie) → "account created" beat → redirect to the dashboard SIGNED IN.component:FullSignupFormsince 0.4.0
- landing.embed.signup.register-proxyapps/landing/src/app/(landing)/embed/signup/api/route.ts:23Same-origin registration proxy POST /embed/signup/api → identity /auth/register. Forwards the Domain=.blal.pro HttpOnly session cookies so the browser adopts them and the new user reaches the dashboard ALREADY signed in (no second password entry). Honeypot (silent fake 200), per-IP token bucket 5/10min, shape validation before upstream, stable error codes (taken/invalid/weak/unavailable) for i18n. Part of the flag-hidden full-registration flow (EMBED_SIGNUP_FULL=1 or ?full=1 preview).route:POST /embed/signup/apisince 0.4.0
- landing.embed.signup.talk-to-lilyapps/landing/src/components/TalkToLilyButton.tsx:19Opens the mounted corner widget via window.OpenAlice.open() so demos + founding requests are taken by Lily herself (lead-capture into our own inbox). Ready-gated — renders only once embed.js registered the host API (500ms poll ×20), so it never dead-clicks; hidden from no-JS. variant="ghost" for secondary placements (the /embed Meet section), default is the primary .btn-p.component:TalkToLilyButtonsince 0.2.0
- landing.embed.skip-linkapps/landing/src/app/(landing)/embed/EmbedSkipLink.tsx:22Shared keyboard skip-to-content link for the /embed product group — mounted first (before EmbedNav) by the group layout so Tab reaches it before any nav control, on all three pages (embed, signup, security). Extracted from embed/page.tsx (which had it AFTER the nav in render order — wrong tab order) and promoted to the group layout, matching EmbedFooter's client-component pattern.component:EmbedSkipLinksince 0.4.0
landing/lab
1 feature- landing.lab.showpieceapps/landing/src/app/(landing)/lab/LabShowpiece.tsx:23"Media → soul" showpiece for /lab — a self-playing role=img illustration of the creation flow on the /embed sakura pedestal: raw inputs (photos/voice/writing/docs) stream into a distillation core, the four pipeline steps tick (upload→distill→review→seal), and a signed .soul with its consent passport drops out. Honest labels baked in ("stylized, not photoreal", consent-gated, watermarked, you own the output). Static under prefers-reduced-motion; paused while the tab is backgrounded.component:LabShowpiecesince 0.3.0
landing/layout
1 feature- landing.layout.mainapps/landing/src/app/(main)/layout.tsx:22Shared chrome for the openalice PRODUCTS SITE (home, about, trust, docs, contact) — sakura-on-near-black SiteNav + SiteFooter on the scoped .oa-embed-lp design system with .oa-site additions, plus the EmbedReveal scroll-reveal island. Replaced the old violet Navbar/Footer chrome 2026-06-12. The Atelier composite landings (persona/live/world/social) and /embed render their own chrome in the sibling (landing) group.layout:(main)since 0.3.0
landing/lib
3 features- landing.lib.siteCopyapps/landing/src/lib/siteCopy.ts:2ONE canonical module for the marketing site's static English metadata copy (page `<title>`/description, openGraph/twitter variants, shared og:image alt text). Next.js static `export const metadata` cannot read the NEXT_LOCALE cookie, so metadata stays English on every route by design (SEO baseline) — but before this module the same title/description text was duplicated by hand across ~20 page.tsx files and structuredData.ts's organizationJsonLd(), so a copy edit meant hunting every file and risked drift (caught 2026-07-15 copy-hygiene wave: the root layout title and /contact description still carried em-dashes the rest of the site had already dropped; /page.tsx and /embed/page.tsx's OG descriptions still said "EU-sovereign" and "auditable", both banned jargon; /page.tsx's description still said "at your door" after the site-wide rename to "at your place"). Every page.tsx and layout.tsx that sets `metadata` now imports its strings from here; organizationJsonLd() imports `about.description` so the JSON-LD description and the /about meta description can never drift apart again (a test asserts the two stay byte-identical).layout:rootpage:/page:/aboutpage:/contactpage:/docspage:/qrpage:/trustpage:/privacypage:/termspage:/cookiespage:/dpapage:/impressumpage:/ai-disclosurepage:/embedpage:/embed/securitypage:/embed/signuppage:/404lib:structuredDatasince 0.7.0
- landing.lib.themeapps/landing/src/lib/theme.ts:17One named constant for the #0a0a0a carrier background — dedupes the literal across layout.tsx, (main)/layout.tsx and (landing)/embed/page.tsx's viewport/metadata exports (CSS contexts use the --bg-primary custom property instead).const:OA_THEME_BGsince 0.4.0
- landing.lib.waitlist-countapps/landing/src/lib/waitlistCount.ts:20Server-only best-effort count of captured pre-launch leads, read lazily from data/waitlist.jsonl (the store POST /api/waitlist writes, #723), mirroring the routes' tolerant per-line parse. A real lead = one JSONL line carrying a string `email`; blank/malformed lines are skipped and a missing/unreadable file (ENOENT on a fresh deploy) yields 0. Never throws — a page render can't be broken by the store being absent. Used by the /embed page to seed the LeadsInbox demo's resting "captured" count with the real total when leads exist (falls back to the example count otherwise).function:waitlistCountsince 0.4.0
landing/lily
5 features- landing.lily.classesapps/landing/src/app/(landing)/lily/lily.classes.ts:10Structural utility recipes for /lily (lily.css retired; system overrides live with their base in embed.css).since 0.8.0
- landing.lily.conversationapps/landing/src/app/(landing)/lily/LilyConversation.tsx:58The full-screen inline mount of the REAL deployed embed.js widget on /lily — same wgt_embed_landing demo id/backend/rate-caps as the site-wide EmbedCornerWidget, mounted via window.OpenAlice.mount({inline:true}) (reusing EmbedLiveWidget.tsx's bundle loader). 4 starter chips call window.OpenAlice.sendMessage(text) — a real send into the live conversation, disabled until the widget is actually ready. Honest-failure fallback (a real link to /embed) if the bundle never loads.component:LilyConversationsince 0.8.0
- landing.lily.conversationapps/landing/src/test/LilyConversation.test.tsx:2Tests for LilyConversation — the REAL, live Lily mounted full-stage on /lily. Same contract-testing idiom as EmbedLiveWidget.test.tsx (the component this one reuses the bundle loader from): mount() call args, chip → real sendMessage(), disabled-until-ready honesty, cleanup, and the honest-failure fallback (never a fake/frozen chat UI).
- landing.lily.layoutapps/landing/src/app/(landing)/lily/layout.tsx:39Minimal chrome for /lily — a slim fixed OPENALICE·Embed lockup nav (BrandMark + logo__word/dot/product, no link row, no CTA, no mobile menu) and a quiet footer ("Get Lily for your business →" + Impressum/Privacy + LocaleSwitcher, frontend-consistency audit F1), wrapped in the SAME .oa-embed-lp/.oa-theme-embed design-system scope as /embed. Deliberately does NOT mount EmbedCornerWidget — /lily's stage IS the one real Lily on this page (LilyConversation, page.tsx).layout:(landing)/lilysince 0.8.0
- landing.lily.layoutapps/landing/src/test/LilyLayout.test.tsx:2Chrome tests for /lily's OWN minimal group layout (§15 canon): a slim OPENALICE·Embed lockup nav (crawl-gate's `nav .logo__word` marker, honestly — the real §13 recipe, not a stand-in), a quiet 2-link footer (NOT SiteFooter/EmbedFooter's multi-column chrome), the skip link, and — critically — NO EmbedCornerWidget mount ("one Lily per page": the page IS the widget, mounted full-stage by LilyConversation instead).
landing/lint-theme
1 feature- landing.lint-themeapps/landing/src/test/LintTheme.test.ts:2Tests for scripts/lint-theme.mjs — the ONE-theme hard gate (2026-07-15). Verifies the gate itself actually catches what it claims to, using fixture TEXT (no filesystem/child-process involved — findingsForText is a pure function): a raw hex literal fails; the same literal on a line carrying the `theme-lint-allow` marker passes; an SVG fill/stroke JSX attribute is structurally exempt; a hardcoded font-family fails while a var()-routed one and an @font-face resource definition both pass; and isTokenFile() correctly recognizes tokens.css / globals.css by path suffix. This is the fixture-with-a-raw-hex-fails spec required by the ONE-theme wave gate list. The "lint-theme: public/ scan" block below is the ROOT-CAUSE proof: the gate used to walk src/** only, which let a raw-hex, file://-path HTML fixture (public/og/template.html) sit unseen for weeks. These tests exercise the REAL filesystem scanPublic() (a temp public/ dir, not just findingsForText) to prove a planted hex literal in public/**\/*.html or *.css actually fails the scanner now, and that the allowlist/inline marker escape hatches still work there too.
landing/live
1 feature- landing.live.hero-stageapps/landing/src/app/(landing)/live/LiveStage.tsx:15/live hero stage — an abstract browser window (traffic dots + live.openalice.eu address bar + viewer count) with the Lily avatar centred and large inside on a clean sakura-lit backdrop, plus one small LIVE pill. The "agent lives on the web" metaphor; minimal, lots of negative space, Server Component, pure-CSS pulse, sakura palette.component:LiveStagesince 0.4.0
landing/observability
2 features- landing.error-beaconapps/landing/src/lib/errorBeacon.ts:8Reports uncaught window errors + unhandled rejections to the agent-lite /v1/embed/client-error collector (observability).since 0.1.0
- landing.error-beacon-mountapps/landing/src/components/ErrorBeacon.tsx:10Root client component that installs the landing error beacon on mount.since 0.1.0
landing/page
3 features- landing.page.embedapps/landing/src/test/EmbedGlance.test.tsx:2"At a glance" fact-sheet tests (Alex's SEO/AEO copy pack 2026-07-11) — renders the full /embed page (jsdom) and asserts the citable block's contract: 1. #glance is a real, machine-readable <dl>: one h2 (the canon title) + exactly 8 dt/dd rows in the pack's order (what / install / pricing / annual / hosting / languages / trial / control). 2. The facts are the pack's canon lines verbatim: public prices only (€49/€249/€799 excl. VAT — NEVER the €149 founding price, which is NAO's personal offer), annual 12-for-10, the language canon (Lily answers ANY language, the UI ships in six), no-free-tier trial, EU hosting, Art. 50 disclosure. 3. The literal "<script>" renders as TEXT via the ICU tag convention (same as faq.installA), never as real markup. 4. Placement: between #pricing and #faq (the machine-readable zone) — the page-visible annual anchor for the structured-data offers.
- landing.page.embedapps/landing/src/test/EmbedPricing.test.tsx:2Pricing v2 copy/structure tests — renders the full /embed page (server component tree + client islands, jsdom) and asserts the pricing section's contract: 1. Exactly 3 plan cards: Starter €49 / Pro €249 (featured) / Business €799 — the old "Enterprise · Custom" card is GONE. 2. The numeric comparison axis (conversations + voice minutes) is emphasised via .num wrappers. 3. Business says the exact phrase "+ Premium frontier models, EU-hosted (opt-in)" and NEVER the word "sovereign". 4. No em-dashes in card copy (the "—" in the compare table cells is the spec'd absence marker, not prose). 5. Under the grid: a Talk-to-sales line (→ /embed/signup) + the mono no-surprise-bills footnote (D-022: no free tier/trial). 6. A collapsed compare-plans <details> table + 5 pricing FAQ items. 7. The honest-arithmetic pricing headline (730 hours/mo, €249, ~€0.34/hr) stays intact — no €5,000 anchor, no strike-through.
- landing.page.lilyapps/landing/src/test/LilyPage.test.tsx:2/lily content tests (§15 canon): the compact top-strip intro (H1 + sub + a visible, persistent AI disclosure — distinct from the widget's own one-time consent-gate disclosure), the 4 real starter chips, and the ZERO-landing-blocks rule — no pricing table, no signup wall (Lily ANSWERS about prices; that IS the demo, per the locked frame).
landing/pages
25 features- landing.page.aboutapps/landing/src/app/(main)/about/page.tsx:30/about — institute-sober profile of OpenAlice Laboratories: what the lab is (EU R&D institute building embodied AI employees), the O.P.E.N. / A.L.I.C.E. two-layer architecture block (acronym expansions sourced from the org-profile README + labs-site), and the honest operating paragraph (one human + N agents, human-in-the-loop approvals for sensitive actions, kill-switch discipline). No lore — the sober register of the products site. Etalon parity wave 2026-06-12: each architecture card carries a thin living MandelBand (the etalon's Mandelbrot math as a 56px ink-on-dark cross-section), the operating paragraph is drawn as the OrgPulse diagram (1 human + 6 agents, phosphor pulses on the connections), and the blocks reveal with staggers — all reduced-motion safe. Composition-polish wave 2026-07-15 (foreman finding: hero dead-right, org-pulse diagram floating in a void): the hero carries the shared inner-page ascii/glow substrate, and the org-pulse diagram now sits inside the standard dark frame + hairline (site.css `.operate-grid__fig`), sized to balance the wider prose column beside it. The lab-portal card was already right; untouched.page:/aboutsince 0.3.0
- landing.page.ai-disclosureapps/landing/src/app/(main)/ai-disclosure/page.tsx:25/ai-disclosure — EU AI Act Art. 50 transparency notice. Renders English by default; switches to the verbatim German translation when NEXT_LOCALE=de (falls back to EN for fr/es/it/pt). /de/ai-disclosure is a separate, always-German route (mirrors the retired public/de/ai-disclosure.html). Converted from static HTML into a real (main)-group route (2026-07-15). Legal text verbatim, see content.en.tsx / content.de.tsx.page:/ai-disclosuresince 0.8.0
- landing.page.ai-disclosure-deapps/landing/src/app/(main)/de/ai-disclosure/page.tsx:24/de/ai-disclosure — forced-German mirror of /ai-disclosure: renders content.de.tsx unconditionally, ignoring the NEXT_LOCALE cookie (same job the retired public/de/ai-disclosure.html did). Shares the verbatim content module with /ai-disclosure's own DE branch.page:/de/ai-disclosuresince 0.8.0
- landing.page.contactapps/landing/src/app/(main)/contact/page.tsx:36/contact — two quiet doors: talk to Lily (the mounted corner widget takes sales/pilot/procurement questions and captures the lead — no mailto: openalicelabs.eu had no MX and D-022 proves sales run through Embed) and see the live demo on /embed. Institute-sober, no form backend. Etalon parity wave 2026-06-12: the two routes get the glass treatment (translucent gradient + backdrop-blur over 4 ambient etalon petals drifting behind them), the CTA buttons are magnetic (≤6px toward the cursor, spring back — MagneticCta, mouse-only), and the cards stagger-reveal. All reduced-motion safe. Composition-polish wave 2026-07-15 (foreman finding: two cards left, dead right half): the hero carries the shared inner-page ascii/glow substrate, and the two doors gain a third, quiet column — a framed portrait of Lily (the page's own story: "she's in the corner") with a subtle pointer toward the real, mounted corner widget.page:/contactsince 0.3.0
- landing.page.cookiesapps/landing/src/app/(main)/cookies/page.tsx:25/cookies — cookie/ePrivacy notice. Renders English by default; switches to the verbatim German translation when NEXT_LOCALE=de (falls back to EN for fr/es/it/pt). /de/cookies is a separate, always-German route (mirrors the retired public/de/cookies.html). Converted from static HTML into a real (main)-group route (2026-07-15). Legal text verbatim, see content.en.tsx / content.de.tsx.page:/cookiessince 0.8.0
- landing.page.cookies-deapps/landing/src/app/(main)/de/cookies/page.tsx:24/de/cookies — forced-German mirror of /cookies: renders content.de.tsx unconditionally, ignoring the NEXT_LOCALE cookie (same job the retired public/de/cookies.html did). Shares the verbatim content module with /cookies's own DE branch.page:/de/cookiessince 0.8.0
- landing.page.docsapps/landing/src/app/(main)/docs/page.tsx:27/docs — honest, short install & setup for Embed: the real one-line embed snippet (DocsSnippet — the etalon CodeSnippet adapted to TYPE ITSELF on scroll-into-view, SSR-full for no-JS/crawlers), five numbered setup steps pointing at the real dashboard surfaces (knowledge upload, widget settings, proactive triggers, takeover inbox) at embed.blal.pro with a border-glow pulse on reveal, a "2 minutes" CountUp in the lede (the REAL number), and a pointer to /embed#faq. No invented features. Etalon parity wave 2026-06-12; composition-polish wave 2026-07-15 (foreman finding: the page read as a left column next to 3 viewports of dead black) — the hero carries the shared faint AsciiField/glow substrate every inner-page hero now shares, the snippet sits 2-col beside its heading at ≥1100px, and the steps gain a sticky right rail: a REAL screenshot of the live product widget (captured off this very site, framed) — not a fabricated dashboard mock, per the "never fabricate UI" rule.page:/docssince 0.3.0
- landing.page.dpaapps/landing/src/app/(main)/dpa/page.tsx:25/dpa — Data Processing Agreement (Art. 28 GDPR) for OpenAlice Embed. Renders English by default; switches to the verbatim German translation when NEXT_LOCALE=de (falls back to EN for fr/es/it/pt). /de/dpa is a separate, always-German route (mirrors the retired public/de/dpa.html). Converted from static HTML into a real (main)-group route (2026-07-15). Legal text verbatim, see content.en.tsx / content.de.tsx.page:/dpasince 0.8.0
- landing.page.dpa-deapps/landing/src/app/(main)/de/dpa/page.tsx:24/de/dpa — forced-German mirror of /dpa: renders content.de.tsx unconditionally, ignoring the NEXT_LOCALE cookie (same job the retired public/de/dpa.html did). Shares the verbatim content module with /dpa's own DE branch.page:/de/dpasince 0.8.0
- landing.page.embedapps/landing/src/app/(landing)/embed/page.tsx:78OpenAlice Embed product landing — "she lives on your site". A FAITHFUL 1:1 reproduction of the approved Sakura design (landing-ideal-2026-06-07.html) — its CSS is ported verbatim into a co-located embed.css, every selector scoped under `.oa-embed-lp` so the design's generic classes don't leak. Colors (#08080f black, #fa6aa6/#ff5594 sakura), fonts (Fraunces / Hanken Grotesk / JetBrains Mono / Noto Serif JP), layout and ALL animations (pulse, floatb, rise, reveal, the widget's own ring/breathe) are preserved, incl. prefers-reduced-motion. The hero in-situ dock + showpiece pedestal are STATIC, non-interactive visuals of the Lily widget (role=img illustrations, no mic/consent so a first-time visitor is never startled). The ONE real, interactive Lily is the bottom-right corner launcher (EmbedCornerWidget) — the deployed bundle, where visitors try the product and accept consent on their own click. AEO fact sheet (Alex copy pack 2026-07-11): the #glance "at a glance" section between pricing and FAQ — a real <dl> of 8 one-line citable facts (what/install/pricing/annual 12-for-10/hosting/languages/trial/control), i18n ×6, the page-visible anchor for the structured-data annual offers. Decomposed into per-section components 2026-07-13 (was a single 999-line file); the nav + footer chrome moved up to the group layout (shared with /embed/signup + /embed/security). EuSovereignSection (2026-07-14, brand-strategist copy pack) sits directly under TrustBeatSection — "Built in the EU. All of it.", 4 checkable facts (model/hosting/captcha/paperwork) deepening the trust strip's EU-flag + Mistral tiles, linking to /embed/security. Text/RAG/hosting/captcha scope only — voice is deliberately unmentioned.page:/embedsince 0.2.0
- landing.page.embed-securityapps/landing/src/app/(landing)/embed/security/page.tsx:8/embed/security — the trust page a compliance-minded EU buyer checks. Honest data-flow + sub-processor + GDPR carve-out; voice is EU by default (Mistral-native) with US providers strictly per-tenant opt-in. Linked from the footer Legal column + the FAQ. Reuses the scoped .oa-embed-lp theme.page:/embed/securitysince 0.2.0
- landing.page.embed-signupapps/landing/src/app/(landing)/embed/signup/page.tsx:12/embed/signup — founding-access page that the landing's primary "Add to your site" CTA points to (was a 404). Honest early-access flow: on-page email capture (pure client redirect to the real dashboard signup at embed.blal.pro with the email prefilled), book a call + email as secondary, locked-in founding pricing, founding terms by agreement. Reuses the scoped .oa-embed-lp Sakura theme. Swap for the real Mollie checkout at launch.page:/embed/signupsince 0.2.0
- landing.page.errorapps/landing/src/app/error.tsx:92Root route-segment error boundary — reuses the 404 terminal-card pattern (same .oa-404/.term chrome) with a calm "something broke" message, a reset() retry, and a home link. Locale read from the NEXT_LOCALE cookie directly (next-intl context not assumed reachable on a broken tree); copy inlined per-locale in this file rather than messages/*.json.page:/errorsince 0.9.0
- landing.page.global-errorapps/landing/src/app/global-error.tsx:88Root-LAYOUT error boundary — own html/body per Next's contract, reuses the 404 terminal-card chrome, zero shared imports besides the design-system CSS (a build-time bundle concern, not a runtime tree dependency). Locale via direct NEXT_LOCALE cookie read; copy inlined per-locale (duplicated from error.tsx on purpose — independently self-contained).page:/global-errorsince 0.9.0
- landing.page.homeapps/landing/src/app/(main)/page.tsx:40openalice products-site home — TWO-DOOR rebuild (2026-07-15, xAI-restraint architecture pass; polish pass + Door B content redirect same day; Door B poster-face + balance pass same day), exactly FOUR blocks + footer. (1) HERO — centered, xAI-style: one ambient effect (petals + the shared faint AsciiField texture, no side portrait), huge Fraunces H1 ("Your business just hired its first employee") with a VIVID sakura accent (var(--sakura-vivid), not the raw --sk), a staccato sub naming both doors, the price beat, two pill CTAs. (2) TWO DOORS — the signature: a 2-tile grid, "One employee. Two doors." Door A (On your website → /embed) carries Lily's living ASCII portrait + an "online now" badge INSIDE the tile; Door B (At your place → the lily-root QR micro-landing) carries a VENUE-poster mockup — the printed artifact the BUYER puts on their own wall/counter: a "YOURBRAND" lockup placeholder, Lily's own framed portrait as the card's visual focal (a plain photo, not the ASCII treatment — that living language stays Door A's alone), the poster's own pitch to its visitor ("Need help?"), the QR + "Scan me" call, and a small "Powered by OpenAlice" credit — on a tilted cream card with a tape strip and a sakura keyline frame, widened to occupy a footprint comparable to Door A's edge-to-edge portrait. All eyebrow-role/CTA/accent TEXT on the tiles uses the vivid sakura token, never the dulled --sks. (3) PROOF STRIP — one thin line, not a tile grid: "2-min install · from €49/mo · EU-hosted · cancel anytime". (4) FINAL — two start cards (xAI's "Choose how to get started" pattern, reusing /embed's own `.price` card recipe): "Start self-serve" (€49/mo, live today, a 4-line benefit checklist, primary CTA → /embed/signup) and "Founding: white-glove" (setup done for you, locked price, a direct line → /contact, its "Founding" label now the vivid accent instead of unstyled grey), closed by a short honest note. Replaces the 2026-07-15-morning five-block home (hero+portrait / single Embed product card+lab line / 6-tile trust strip / CTA-pair final) — the products card, the lab line, the trust-tile grid and the "embed" glyph-strip easter egg are CUT (their information now lives in the door tiles / proof strip / /about); nothing here is decoration without a job. Body copy carries no em-dashes (short declarative sentences instead, xAI register). Reduced-motion safe throughout.page:/since 0.6.0
- landing.page.impressumapps/landing/src/app/(main)/impressum/page.tsx:26/impressum — TMG/DDG § 5 provider notice. German-required content served in German at BOTH /impressum and /de/impressum, always — a single canonical German document (never gated by the NEXT_LOCALE cookie), matching SiteFooter's own "Impressum has no /de/ prefix" convention and the 2026-07-14 security-audit P1-4 fix that added /de/impressum. Converted from the standalone public/impressum.html static page (served via a next.config.ts rewrite, its own hand-forked public/legal.css) into a real (main)-group route (2026-07-15) — now shares SiteNav/SiteFooter/design tokens instead of a forked palette. Legal text verbatim, see content.de.tsx; /de/impressum reuses this same component.page:/impressumpage:/de/impressumsince 0.8.0
- landing.page.impressum-deapps/landing/src/app/(main)/de/impressum/page.tsx:24/de/impressum — the explicit /de/ entry point for the same single German Impressum document /impressum already serves (security-audit P1-4, 2026-07-14: a bare-cookie /impressum wasn't discoverable enough for TMG/DDG purposes without a /de/ URL to match the site's other legal mirrors). Reuses /impressum's page component verbatim rather than duplicating the body — one source of German legal text, two entry URLs. Canonical points at /impressum (the primary URL) to avoid duplicate-content signals.page:/de/impressumsince 0.8.0
- landing.page.lilyapps/landing/src/app/(landing)/lily/page.tsx:41"Ask Lily anything" (§15 canon, marketing-lead locked frame) — the shop-stall where Lily sells herself. Full-screen CONVERSATION as the ONLY content, no landing blocks (no pricing table, no signup wall — Lily ANSWERS about prices, that IS the demo). A compact top-strip intro (H1 "Ask Lily anything." + one-line who-she-is + a visible, persistent AI disclosure — distinct from the widget's OWN consent-gate disclosure, which only shows once) sits above the stage; LilyConversation mounts the REAL deployed embed.js widget inline (same wgt_embed_landing demo id + D-022 Demo A rate caps as the site-wide corner widget), with 4 real starter chips that send into the actual conversation via window.OpenAlice.sendMessage. Both doors (site → /embed, no-site → /qr) live INSIDE the conversation — Lily routes herself; this page carries no separate door UI.page:/lilysince 0.8.0
- landing.page.not-foundapps/landing/src/app/not-found.tsx:22Site-wide 404 — a terminal-styled card (JetBrains Mono, traffic-light bar, blinking caret; caret static under prefers-reduced-motion) carrying the products site's ONE permitted lore line ("You have wandered off the path. Follow the white rabbit home →", localized) linking back to /. Everything else on the site stays institute-sober. Etalon parity wave 2026-06-12: a slow scanline sweeps the terminal card (7s loop, gone under reduced motion) and the headline settles from a one-time glyph scramble (GlyphSettle, ~95ms/char, SSR-final). i18n wave (2026-07-15): the copy now lives in the `notFound` messages namespace (all 6 locales) via getTranslations — next-intl works fine in the root not-found.tsx (RSC, same request-config path as every other page); SiteNav is mounted for a real way back (it has no (main)-group dependency — same .oa-embed-lp/.oa-theme-embed/.oa-site scope classes this page already wraps itself in).page:/404since 0.3.0
- landing.page.privacyapps/landing/src/app/(main)/privacy/page.tsx:25/privacy — GDPR privacy policy. Renders English by default; switches to the verbatim German translation when NEXT_LOCALE=de (falls back to EN for fr/es/it/pt — no translation exists for those yet). /de/privacy is a separate, always-German route for readers who land on that URL directly (mirrors the retired public/de/privacy.html). Converted from static HTML (public/privacy.html + public/de/privacy.html, served via next.config.ts rewrites + a hand-forked public/legal.css) into a real (main)-group route (2026-07-15) — now shares SiteNav/SiteFooter/design tokens. Legal text verbatim, see content.en.tsx / content.de.tsx.page:/privacysince 0.8.0
- landing.page.privacy-deapps/landing/src/app/(main)/de/privacy/page.tsx:24/de/privacy — forced-German mirror of /privacy: renders content.de.tsx unconditionally, ignoring the NEXT_LOCALE cookie, for readers who land on this URL directly (same job the retired public/de/privacy.html did). Shares the verbatim content module with /privacy's own DE branch — one source of German legal text, two entry URLs.page:/de/privacysince 0.8.0
- landing.page.qrapps/landing/src/app/(main)/qr/page.tsx:37/qr — Door B's full deep-sell page, on THIS site. IA unification (2026-07-15, TWO-DOOR-COPY-2026-07-15 §9, founder-approved verdict "Да делаем унификацию"): the buyer journey lives on ONE domain (openalice.eu); this page replaces lily.blal.pro's bare-root micro-landing as door B's marketing surface — lily.blal.pro becomes a pure product host (see embed-dashboard's thinned lily-landing page). Three tight blocks + footer, same (main)-group chrome as home/about/docs/trust: (1) hero — the canon-locked "No website? No problem." H1 (TWO-DOOR-COPY §3, kept verbatim), the printed-QR-poster subline, the primary CTA → /embed/signup (one Magnetic, matching home/embed's signature-CTA discipline), the niche line naming concrete verticals, and the mirror door-ribbon → /embed ("Have a website? Meet the widget", the door-B counterpart of /embed hero's own ribbon → here); (2) how it works — a 3-node `.flow` (print → scan → answer, the same recipe /trust's data-flow map uses); (3) the poster showcase — PosterMockup (extracted 2026-07-15 from the home page's Door B tile so both mounts share one recipe) beside the four proof bullets ported from the retired lily-landing micro-landing, closed by a pricing pointer (from €49 → /embed#pricing) and a final CTA + honest note (talk to Lily in the corner — EmbedCornerWidget is mounted sitewide by the (main) layout, so this is a real, present demo, not a claim). Ad-register throughout (founder canon): short lines, zero em-dashes, no vendor/country comparisons, GDPR/Hetzner kept as trust words. All copy via the "qr" i18n namespace — EN/DE authored, fr/es/it/pt translated (6/6 i18n-matrix gate). Composition-polish wave 2026-07-15: the hero carries the shared inner-page ascii/glow substrate; the flow + poster showcase sections were already judged balanced and stay untouched.page:/qrsince 0.7.0
- landing.page.termsapps/landing/src/app/(main)/terms/page.tsx:25/terms — Terms of Service for OpenAlice Embed. Renders English by default; switches to the verbatim German translation when NEXT_LOCALE=de (falls back to EN for fr/es/it/pt). /de/terms is a separate, always-German route for readers who land on that URL directly (mirrors the retired public/de/terms.html). Converted from static HTML into a real (main)-group route (2026-07-15). Legal text verbatim, see content.en.tsx / content.de.tsx.page:/termssince 0.8.0
- landing.page.terms-deapps/landing/src/app/(main)/de/terms/page.tsx:24/de/terms — forced-German mirror of /terms: renders content.de.tsx unconditionally, ignoring the NEXT_LOCALE cookie (same job the retired public/de/terms.html did). Shares the verbatim content module with /terms's own DE branch.page:/de/termssince 0.8.0
- landing.page.trustapps/landing/src/app/(main)/trust/page.tsx:28/trust — the B2B closer. Data-flow map (visitor chat → Hetzner Germany → Mistral EU inference, voice synthesized EU-side, data at rest EU), GDPR stance linking the existing /dpa + /ai-disclosure (no new legal claims), sober EU AI Act position (limited-risk conversational AI, transparency via disclosure + consent), VERIFIED-only subprocessor table (Hetzner active, Mistral active, Mollie contracted, Brevo pending activation — Bunny omitted, not found in code), the exact EU-voice statement (Mistral-native EU default, US providers strictly per-tenant opt-in), and the off/session/persistent retention summary. Etalon parity wave 2026-06-12: the data-flow map animates (3 dots travel the EU path on a 6s loop, behind the node cards; static arrows under reduced motion / mobile), subprocessor rows stagger-reveal opacity-only, and the retention modes carry a tiny visual state strip (hollow / pulsing / steady-ringed dots). Composition-polish wave 2026-07-15: the hero carries the shared inner-page ascii/glow substrate (foreman finding: header block read dead-right, same as every other inner page); the flow/table/prose sections below were already judged fine and stay untouched.page:/trustsince 0.3.0
landing/passport
1 feature- landing.passport.showpieceapps/landing/src/app/(landing)/passport/PassportShowpiece.tsx:23"Verify a passport" showpiece for /passport — a self-playing role=img illustration of an identity-asset W3C Verifiable Credential being checked: the four attestations (provenance/ownership/authenticity/consent) tick green one by one and a VERIFIED stamp lands, on the /embed radial sakura pedestal + theme tokens. Cycles across asset kinds (voice clip, likeness, soul) and includes one honest REVOKED scenario where a withdrawn consent mandate fails the check. ed25519 + C2PA labels baked in; no blockchain/token language. Static under prefers-reduced-motion; paused while the tab is backgrounded.component:PassportShowpiecesince 0.3.0
landing/persona
2 features- landing.persona.hero-stageapps/landing/src/app/(landing)/persona/PersonaStage.tsx:19Atelier-themed hero stage preview panel for /persona — framed avatar portrait, live status, chat/voice/video mode chips, and a "talk to her" affordance that wakes the deployed embed launcher.component:PersonaStagesince 0.2.0
- landing.persona.live-demo-widgetapps/landing/src/app/(landing)/persona/PersonaDemo.tsx:22Client-only injection of the deployed embed.blal.pro avatar widget (aria.vrm, tenant openalice, agent aria) so the /persona landing ships a real, talkable 3D avatar demo rather than a mockup.component:PersonaDemoWidgetsince 0.2.0
landing/seo
1 feature- landing.seo.structured-dataapps/landing/src/lib/structuredData.ts:2Schema.org JSON-LD builders for the marketing site (AEO tech pack). Site-wide Organization + WebSite graph in the root layout (no sameAs — no social profiles exist; no SearchAction — no site search exists); SoftwareApplication (BusinessApplication, AggregateOffer €49-€799/mo EUR with valueAddedTaxIncluded:false per the "excl. VAT" fine print; each tier Offer carries a TWO-entry priceSpecification — monthly per-1-MON plus the annual 12-for-10 option, price=monthly×10 per 12 MON, made page-visible by the at-a-glance fact block per Alex's copy-pack verdict 2026-07-11) + FAQPage (all 13 on-page Q&A verbatim: 4 pricing + 9 objection-handling) on /embed. Every string is sourced from the live next-intl catalog (messages/{locale}.json) or the page's own metadata — zero invented facts. Because localization is cookie-based (single URL, no locale routes), building from t() makes the JSON-LD match the rendered locale automatically; crawlers (no cookie) see the en default. Serialization escapes <,>,& as \uXXXX so copy can never terminate the <script> element.layout:rootpage:/embedsince 0.4.0
landing/site
21 features- landing.site.ascii-portraitapps/landing/src/test/AsciiPortrait.test.tsx:2Tests for AsciiPortrait — the home hero's living sakura-phosphor ASCII portrait of Lily (canvas-2D, image→glyph sampler). Contract under test (ASCII-first reveal, founder 2026-07-10 — the raw photo must never paint un-ASCII'd; the figure's one-way data-ascii state drives everything): 1. Renders an aria-hidden .hero-ascii-portrait canvas + the .ascii-veil placeholder and nothing else. 2. data-ascii="live" (+ canvas data-live) is stamped ONLY on the FIRST PAINTED FRAME — never at init start — and the img is never inline-dimmed (the depth ghost is CSS-driven). 3. The canvas readback is gesture-gated ONLY on WebKit (needsGestureGate): iOS/Safari never enhance before the first gesture; Chromium/Firefox go straight to idle init. 4. Fallback ladder → data-ascii="img" (the graceful photo): reduced-motion / reduced-data immediately (no gate, no probe); probe error immediately; a never-loading portrait / a never- gesturing WebKit visitor at the 5s reveal deadline — after which a late gesture can no longer boot (no glyph-pop over the photo). 5. Unmount at any pending stage is safe (no throw, listeners removed). 6. The sampler (samplePortrait) returns a cols×rows luminance/alpha buffer with Rec.601 luminance and alpha both in range. 7. The painter (paintPortrait) is deterministic for a fixed time, skips sub-floor-alpha cells, paints only canon glyphs, and tints strictly on the sakura→cream phosphor ramp. The full visual happy-path (real 2D raster) is still covered by the e2e screenshot loop; here the boot path runs against a recording 2D-context fake, which is enough to prove the REVEAL ORDER (boot ≠ reveal; first painted frame = reveal). The fallback ladder + pure cores below are the parts that must be bulletproof — the <img> must look perfect with the canvas absent.
- landing.site.ascii-portraitapps/landing/src/components/site/AsciiPortrait.tsx:63Home hero focal — Lily sampled into a living sakura-phosphor ASCII glyph field on a cheap canvas-2D loop (image→ASCII / CRT aesthetic per NAO). ASCII-first reveal (founder 2026-07-10): under JS the SSR <img> starts hidden behind a shimmer veil in its exact box (zero CLS) and the figure's data-ascii attribute drives a one-way pending→live|img state — "live" is stamped on the FIRST PAINTED FRAME (never at init start), so she is born as glyphs and the raw photo never paints un-ASCII'd; "img" is the graceful photo fallback (reduced motion/data immediately, canvas/image failure immediately, 5s no-gesture deadline + 4s post-gesture boot grace, pure-CSS 8s rescue, no-JS never hides). Canvas readback (getImageData sampling) is gesture-gated ONLY on WebKit (needsGestureGate UA scope — iOS/Safari Advanced Fingerprinting Protection); other browsers init on requestIdleCallback after load. Per-cell luminance+alpha sampled from the same-origin portrait (transparent bg → empty cells → exact silhouette); luminance→density-ramp + sakura→cream tint, alpha-gated. Living: ~7s CRT scan-band, ±1 epoch glyph jitter (~1.2s), ±5% breath (~6s), rare petal/katakana accents; ~10fps, rAF only while intersecting + foreground. Grid derives from the box (~6×10 CSS px/cell, clamped) so glyphs never stretch. ≤880px → still boots but calmer (slower idle cadence, no cursor-heat). Canvas-2D (not the /embed WebGL showpiece) because the GPU-less dev box can only render 2D headless — self-verifiable + lighter. aria-hidden, pointer-events none.component:AsciiPortraitsince 0.3.0
- landing.site.brand-markapps/landing/src/components/site/BrandMark.tsx:19The two-circle openalice sigil (cream + sakura) — dedupes the 8 copy-pasted raw SVGs (nav logos, footers, corner-widget "powered by" tags) into one parametrized component, pixel-identical to each prior inline instance.component:BrandMarksince 0.4.0
- landing.site.cursor-dotapps/landing/src/components/site/CursorDot.tsx:20Sakura cursor-companion dot (6px, lerp trail, grows ×2.6 over links/buttons) shared by both landings via the root layout. Accompanies — never replaces — the native cursor; fine-pointer only; reduced-motion never mounts; rAF loop parks itself when the trail catches up (zero idle CPU).component:CursorDotsince 0.4.0
- landing.site.docs-snippetapps/landing/src/components/site/DocsSnippet.tsx:27/docs install snippet — the etalon CodeSnippet adapted to type itself on scroll-into-view (IntersectionObserver, once) instead of on load. Same token stream (byte-identical install <script> line), same syntax highlighting, same SSR-full/no-CLS ghost-spacer/backgrounded-tab-pause/reduced-motion-static contract; reuses embed.css .code/.code-caret/.code-ghost styling.component:DocsSnippetsince 0.3.0
- landing.site.footerapps/landing/src/components/site/SiteFooter.tsx:15Products-site footer — Products (Embed + in-the-lab Live/World), Site (Docs/Pricing/About/Trust/Contact) and the full 6-link Legal column (impressum/privacy/terms/dpa/ai-disclosure/cookies, all serving the existing static legal set), with "Made in the EU" + SVG flag and the LocaleSwitcher in the bottom row (frontend-consistency audit F1, 2026-07-15 — the switcher previously only reached visitors on /embed pages).component:SiteFootersince 0.3.0
- landing.site.glyph-settleapps/landing/src/components/site/GlyphSettle.tsx:24One-time glyph-scramble settle for the 404 terminal headline — characters flicker through the etalon AsciiField's ASCII glyph ramp and resolve left→right at ~34ms/char, once. SSR/no-JS/reduced-motion show the final text (resting state default); the animating span is aria-hidden with an SR-only static copy; mono-safe alphabet → zero CLS.component:GlyphSettlesince 0.3.0
- landing.site.glyph-settleapps/landing/src/test/GlyphSettle.test.tsx:2Tests for GlyphSettle — the 404 headline's one-time glyph-scramble. Contract under test (the etalon's resting-state discipline): 1. Reduced motion → the final text renders and never scrambles. 2. Motion path → scramble starts (same length, mono-safe etalon glyphs, spaces preserved) and SETTLES into the exact final text. 3. The animating span is aria-hidden; a static SR copy always holds the real line.
- landing.site.magnetic-ctaapps/landing/src/components/site/Magnetic.tsx:23Magnetic primary-CTA micro-interaction — the button leans up to ~9px toward a fine pointer inside a padded halo and springs home on leave (rAF-eased, transform-only). Coarse pointers + prefers-reduced-motion render children untouched. Reserved for the ONE primary CTA per view (hero + finale) on the home + /embed landings, and /qr's hero (2026-07-15, IA unification — Door B's own deep-sell page joins the same signature-CTA family).component:Magneticsince 0.4.0
- landing.site.magnetic-ctaapps/landing/src/components/site/MagneticCta.tsx:15Magnetic hover island for the /contact CTA buttons — pointermove pulls each .contact-grid .btn ≤6px toward the cursor; pointerleave clears the transform with a spring-back transition (.is-spring overshoot bezier in site.css). Mouse-only (hover:hover + pointer:fine), disabled under prefers-reduced-motion, renders nothing (the etalon EmbedTilt pattern).component:MagneticCtasince 0.3.0
- landing.site.mandel-bandapps/landing/src/components/site/MandelBand.tsx:36Thin living Mandelbrot cross-section band for the /about architecture cards — a 100%×56px canvas of ink-on-dark terminal glyphs (4 rows) sampling the classic full-set frame compressed into a band, breathing ±10% zoom, ~8fps, value-noise modulated. Imports the etalon AsciiField's exported math core (mandelLum/vnoise/hash2/FIELD_GLYPHS); same engineering discipline: idle-init after load, IntersectionObserver + visibility gating, never inits under prefers-reduced-motion, silent abort without a 2D context, fixed height → zero CLS.component:MandelBandsince 0.3.0
- landing.site.mandel-bandapps/landing/src/test/MandelBand.test.tsx:2Tests for MandelBand — the thin living Mandelbrot cross-section band on the /about architecture cards (the etalon AsciiField's math, reframed). Contract under test (same progressive-enhancement shape as AsciiField): 1. Under prefers-reduced-motion the band NEVER initializes. 2. Without a canvas-2D context (jsdom) it aborts silently. 3. Happy path (mocked 2D context): initializes, marks data-live, schedules rAF and paints glyphs from the etalon's FIELD_GLYPHS ramp. 4. paintBand is deterministic, ink-only (no sakura fillStyle), and X-symmetric across the band's horizontal midline (the logo motif).
- landing.site.navapps/landing/src/components/site/Nav.tsx:17Shared parametric top nav — fixed bar + fullscreen-overlay mobile menu (burger→X morph, focus-trap, scroll-lock, resize auto-close, ?menu=open QA hook) — used by both SiteNav and EmbedNav (unified 2026-07-13; was two ~90%-identical files).component:Navsince 0.4.0
- landing.site.navapps/landing/src/components/site/SiteNav.tsx:16Products-site top nav — wordmark, Embed (active) + Live/World (dimmed "lab" links) + Docs/Pricing/About, "Start now →" CTA to /embed/signup (sale-max: purchase path in the header; Lily handles sales questions). Chrome/behavior lives in the shared Nav component.component:SiteNavsince 0.3.0
- landing.site.org-pulseapps/landing/src/components/site/OrgPulse.tsx:18Animated "1 human + N agents" org diagram for /about — pure SVG/CSS server component: ink human center node with a breathing sakura ring, six sage agent nodes, and phosphor pulses traveling the radial connections center→agent on a dignified 7s staggered loop (CSS custom-property translate keyframes, no JS). Fixed 220×220 → zero CLS; pulses rest at opacity 0 and the whole motion layer dies under prefers-reduced-motion, leaving a static org chart.component:OrgPulsesince 0.3.0
- landing.site.petalsapps/landing/src/components/site/Petals.tsx:10Ambient drifting-petal decoration — dedupes the 5 hand-typed `.petals` blocks (embed hero/final, signup, contact, home hero) into one `count`-parametrized component.component:Petalssince 0.4.0
- landing.site.poster-mockupapps/landing/src/components/site/PosterMockup.tsx:22Shared venue-poster visual (brand lockup + Lily portrait + headline + QrGlyph/scan label + powered-by credit) — used by the home page's Door B tile AND /qr's poster showcase. Decorative (aria-hidden); styled entirely by the existing `.poster` rules in site.css.component:PosterMockupsince 0.7.0
- landing.site.qr-glyphapps/landing/src/components/site/QrGlyph.tsx:19Decorative QR-sticker motif — three finder-pattern corners + a deterministic data scatter (fixed sin-hash, SSR-stable, same recipe family as AsciiPortrait's hash2) on a cream sticker card with a soft shadow. Used as the centerpiece code under the "Scan me" label on the home page's Door B venue-poster mockup (site.css `.poster .door-tile__qr` sizes it level, ~84px, no independent tilt — 2026-07-15 redirect from an earlier Lily-photo poster concept). aria-hidden, decorative only.component:QrGlyphsince 0.5.0
- landing.site.sticky-ctaapps/landing/src/components/site/StickyCta.tsx:21Phone-only sticky bottom CTA bar (≤640px) shared by the home + /embed landings — appears after the hero scrolls out (IntersectionObserver, no scroll listeners), retires when the finale CTA enters, safe-area-inset aware, reduced-motion keeps it but drops the slide. Mobile-conversion catch from the 2026-07-06 cross-device audit.component:StickyCtasince 0.4.0
- landing.site.trust-stripapps/landing/src/components/site/TrustStrip.tsx:11The `.trust .titem` tile row shared by /embed and the products-site home — dedupes the two hand-rolled copies (same classes + CountUp + 70ms stagger) behind a `tiles` prop.component:TrustStripsince 0.4.0
- landing.site.webmcp-toolsapps/landing/src/components/site/WebMcpTools.tsx:37Registers 2-3 WebMCP tools (open_chat_with_lily, ask_lily, get_pricing_summary) on document.modelContext / navigator.modelContext so an agentic browser (or Lighthouse's agentic-browsing category) can discover and drive the live Lily widget directly — the landing's answer to the LH 13 webmcp-registered-tools / webmcp-schema-validity audits. Best-effort + fully guarded: a no-op on any browser without the (still-experimental, Chrome-origin-trial) API. Mounted once per layout group (main + /embed), same pattern as EmbedCornerWidget.component:WebMcpToolssince 0.4.0
landing/social
1 feature- landing.social.hero-stageapps/landing/src/app/(landing)/social/SocialStage.tsx:14Atelier-themed hero stage for /social — a Discord-like "shared room" panel with a server rail, channel list (text + live voice channel), and a presence stack of mixed human + AI-agent members where one row is explicitly tagged as an agent. Pure CSS/Tailwind, blue accent, subtle online-dot pulse.component:SocialStagesince 0.2.0
landing/soul
1 feature- landing.soul.navapps/landing/src/app/(landing)/vault/SoulNav.tsx:17Shared top navigation for the Soul Vault product pages (/vault, /passport, /lab) — a parametric port of /embed's EmbedNav (logo + section links + a single primary CTA over a full-page scrim, mobile hamburger, Escape/scrim/resize close, 860px breakpoint) reusing the scoped .oa-embed-lp nav classes. Brand, links and CTA label/href are props so all three pages share one nav.component:SoulNavsince 0.3.0
landing/test
14 features- landing.test.about-trustapps/landing/src/test/AboutTrust.test.tsx:2/about + /trust content rails — the O.P.E.N./A.L.I.C.E. architecture block, the honest one-human-N-agents paragraph, the verified-only subprocessor table (no Bunny — not found in code), the exact EU-voice statement, retention controls, and the hard content rails (no crypto, "business-critical" never "critical infrastructure", no fake metrics).
- landing.test.configapps/landing/vitest.config.ts:2Vitest configuration for the openalice-landing Next.js marketing site. Environment: jsdom (React component rendering via @testing-library/react). React plugin: @vitejs/plugin-react (JSX transform). Path alias: @/ → src/ mirrors the tsconfig `paths` entry so test imports resolve the same way as production imports. Server-only stubs: Next.js modules that throw at import time in non-Next environments are aliased to lightweight stub files. The landing uses next/link, next/script, and next/navigation — all stubbed.
- landing.test.crawl-gateapps/landing/src/test/CrawlGate.test.ts:2Unit tests for scripts/crawl-gate.mjs's PURE functions (no browser, no network — same idiom as LintTheme.test.ts: exercise the parsing/ comparison logic directly with fixture data so the live browser run isn't the only thing standing between a regression and CI green). Covers: the tokens.css Graphite-ramp parser (against the REAL file, so a renamed section fails this test immediately instead of only being discovered on the next live crawl), hex/rgb color matching, href classification, the locale-diff exemption rule + Jaccard similarity, the title-uniqueness canonical-exemption rule, and the placeholder-text scanner (including the impressum's allowlisted markers).
- landing.test.docs-contact-404apps/landing/src/test/DocsContact404.test.tsx:2/docs (real install snippet + dashboard steps), /contact (mailto-only — no Calendly exists in the repo) and the 404 terminal (the site's one lore line, linking home).
- landing.test.error-pagesapps/landing/src/test/ErrorPages.test.tsx:2error.tsx (route-segment error boundary) + global-error.tsx (root-layout error boundary) — system-pages wave, 2026-07-15. Both reuse the 404 terminal-card chrome and carry a small INLINE 6-locale copy map (no next-intl dependency — see each file's header for why), reading the locale off document.cookie directly. These tests exercise: the calm "something broke" message renders, reset() is wired and callable, the digest surfaces on global-error, and the cookie-driven locale switch actually changes the rendered copy (jsdom provides a real document.cookie, so no next-intl mock is needed here at all).
- landing.test.etalon-parity-motionapps/landing/src/test/EtalonParityMotion.test.ts:2Reduced-motion kill-switch audit for the etalon parity wave: every NEW animation hook introduced on / /about /trust /docs /contact and the 404 must die inside a `@media (prefers-reduced-motion: reduce)` block — grep-level assertions on the stylesheets (the brief's contract), because jsdom doesn't evaluate media queries. Pseudo-element animations need explicit kills: embed.css's global `.oa-embed-lp * { animation: none }` only reaches elements.
- landing.test.full-signup-formapps/landing/src/test/FullSignupForm.test.tsx:2The flag-hidden REAL registration form: field rendering + a11y wiring, the honeypot staying invisible/unfocusable, the consent gate holding the submit disabled, the strength meter reacting, and the submit path posting to the same-origin proxy + rendering the success beat (cookies are the proxy's job — the form only needs ok:true).
- landing.test.legal-pagesapps/landing/src/test/LegalPages.test.tsx:2The 6 legal pages (impressum/privacy/terms/dpa/ai-disclosure/cookies), converted from hand-forked static HTML in public/ into real (main)-group routes (2026-07-15): (1) each route renders inside the unified (main) chrome (SiteNav present — the whole point of the migration, replacing the standalone lp-top/lp-foot header/footer); (2) a verbatim spot-check per page (one distinctive legal sentence lifted from the retired public/*.html, EN + DE where a translation exists) so a future edit can't silently drop legal content; (3) the /de/* mirrors force German content regardless of the NEXT_LOCALE cookie, and /impressum itself never switches to English (TMG/DDG requires German, always).
- landing.test.not-found-i18napps/landing/src/test/NotFoundI18n.test.tsx:2/404 localization (system-pages wave, 2026-07-15): the `notFound` messages namespace renders EN + DE via getTranslations, keeping the white-rabbit personality line in both languages. Companion to the English-focused assertions in DocsContact404.test.tsx (terminal chrome, GlyphSettle, SiteNav) — this file's whole point is proving the SAME page actually SWITCHES copy when the locale flips, using the real per-locale catalogs (not stubs), same dynamic-locale mock recipe as LegalPages.test.tsx (vi.hoisted localeState + a getTranslations factory that loads the matching messages/{locale}.json).
- landing.test.qr-pageapps/landing/src/test/QrPage.test.tsx:2/qr — Door B's full deep-sell page (IA unification, 2026-07-15, TWO-DOOR-COPY §9). Route renders, the poster mockup is present (the SAME recipe as the home page's Door B tile), links point where the founder's verdict says they should (CTA → /embed/signup, mirror ribbon → /embed, pricing pointer → /embed#pricing — never back at lily.blal.pro), and the ad-register hard rules hold (booking-word-free, zero em-dashes).
- landing.test.route-residencyapps/landing/src/test/RouteResidency.test.ts:2Repo-level route-residency gate — the CI-hard, filesystem-only half of the crawl-gate campaign (scripts/crawl-gate.mjs is the live, runtime half; this test needs no browser/network and runs on every `npm test`). Walks every src/app/**\/page.tsx and asserts it lives under the (main)/ or (landing)/ Next.js route groups — the two groups that mount SiteNav/EmbedNav + SiteFooter/EmbedFooter (see (main)/layout.tsx and (landing)/embed/layout.tsx). A page.tsx dropped anywhere ELSE in src/app/ still becomes a live route (Next's filesystem router doesn't care about groups), but renders with NO shared chrome at all — exactly the "lives its own life" drift the founder's crawler-gate directive exists to catch, except this version catches it at commit time instead of at crawl time. Root-level error.tsx/not-found.tsx/global-error.tsx and src/app/api/** route.ts handlers are FILES, not page.tsx, and are correctly excluded by the `page.tsx`-only filename match below — no special-casing needed. Also asserts public/ carries no *.html or *.css (both retired as of the 2026-07-15 legal-route migration — see next.config.ts's own comment on why the old public/*.html + public/legal.css rewrites were removed): any file must be a real route or a real token file now, not a static mockup with its own forked palette.
- landing.test.setupapps/landing/src/test/setup.ts:2Global test setup file — runs once per test suite before any tests. Registers @testing-library/jest-dom custom matchers (toBeInTheDocument, toHaveTextContent, etc.) on the vitest `expect` object so every test file can use them without a local import. jsdom globals: jsdom does not implement window.matchMedia, so we install a writable stub here. Tests that need to control the return value use vi.spyOn(window, "matchMedia").mockReturnValue({...}); the stub makes that spyOn call legal (vitest 4 requires the property to be an existing function). vi.restoreAllMocks() in afterEach reverts each spy back to this stub.
- landing.test.site-chromeapps/landing/src/test/SiteChrome.test.tsx:2Products-site chrome + home — SiteNav (links, lab-dimmed products, CTA, mobile toggle), SiteFooter (six legal links, EU SVG flag, no flag emoji) and the two-door home's four-block content rails (2026-07-15 rebuild): centered hero, the two door tiles (Door A → /embed, Door B → /qr, the IA-unification target — 2026-07-15, TWO-DOOR-COPY §9), the proof strip, and the final two start cards. The old single Embed product card, 6-tile trust grid and "embed" glyph-strip easter egg are cut — no coverage for them here anymore.
- landing.test.smokeapps/landing/src/test/smoke.test.ts:2Harness smoke — verifies vitest resolves + jest-dom is registered.
landing/ui
1 feature- landing.ui.locale-switcherapps/landing/src/components/site/LocaleSwitcher.tsx:4Language switcher = the canonical library Select (full ARIA combobox + listbox, keyboard nav, type-ahead, click-outside) — the LAST hand-rolled dropdown in the ecosystem retired onto the ONE implementation (landing conveyor L0, 2026-07-17; the dashboard switcher made the same move 2026-07-04/05). placement="up" because the switcher lives at the BOTTOM of its pages (signup card footer, /embed footer) — a downward panel fell below the fold and the option focus() scroll-jumped the page (the «всё смещается» bug); the library Select grew the placement prop for exactly this. The landing's premium glass trigger skin rides the `.lp-locale` wrapper modifier in globals.css (cascade law — the package's unlayered oa-sel skin owns those props). Selecting writes the NEXT_LOCALE cookie via the `setLocale` server action then `router.refresh()` — server components re-render in the new locale with no full reload (same mechanism as always).
landing/vault
1 feature- landing.vault.showpieceapps/landing/src/app/(landing)/vault/VaultShowpiece.tsx:24"What's inside a soul" showpiece for /vault — a self-playing role=img illustration of the encrypted, versioned .soul bundle on a radial sakura glow pedestal (reuses the /embed theme tokens + pedestal). A sealing sweep crosses the lock, the five genome facets (personality/voice/likeness/memory/consent) reveal in a stagger, and the version chip ticks v1→v2→v3 along a hash-chained history strip. Honest by construction — "encrypted per-identity", "custodian, not owner", "stylized likeness" labels are baked in. Static under prefers-reduced-motion; paused while the tab is backgrounded.component:VaultShowpiecesince 0.3.0
landing/waitlist-form
1 feature- landing.waitlist-formapps/landing/src/test/WaitlistForm.test.tsx:2Tests for WaitlistForm — the real pre-launch lead-capture form. Contract under test: 1. Renders a required email input, a primary (.btn-p) submit, and an honest GDPR/consent note (no "we'll email you" promise). 2. Submitting with an email calls the injected submit(email, product) with the trimmed email and the configured product. 3. On "added" it shows the honest "You're on the list." success. 4. On "already" it shows the "already on the list" variant. 5. On a thrown error it surfaces the message in an alert and stays a form. 6. Empty / whitespace-only submit does not call submit.
landing/world
1 feature- landing.world.hero-stageapps/landing/src/app/(landing)/world/WorldStage.tsx:17Pure-CSS isometric "living world" hero stage for /world — a tilted glowing floor grid, a portal arch, and human + agent presence-dots with proximity rings, layered for depth in emerald accent. Animated via Tailwind (animate-pulse), no scripts.component:WorldStagesince 0.2.0
llm-router/adapters
4 features- llm-router.adapters.mistralcrates/llm-router/src/adapters/mistral.rs:1Mistral La Plateforme adapter. Sovereignty: EU 🇫🇷, no consent gate. Default model: mistral-large-latest (Large 2). The single EU-sovereign provider with broad model coverage; chosen as the default per reference-sovereignty-stack-2026-05-26.
- llm-router.adapters.mockcrates/llm-router/src/adapters/mock.rs:1In-memory adapter that returns a scripted response. Powers the unit tests in this crate and the router tests in consumers that don't want to hit a real provider. Not for production.
- llm-router.adapters.modulecrates/llm-router/src/adapters/mod.rs:1Adapter module — one file per provider. Each adapter is a thin `LLMProvider` impl that translates the provider-neutral `ChatRequest` to the upstream's wire format, calls the upstream over HTTPS, and translates the response back into `ChatResponse`. Costing is computed from `ProviderConfig::pricing` × actual token counts so the router doesn't need per-model price tables.
- llm-router.adapters.openroutercrates/llm-router/src/adapters/openrouter.rs:1OpenRouter aggregator adapter. Single API key gets us access to every major model (Gemini 3.5 Flash, Mistral Large, Claude Sonnet, GPT-5, …). Sovereignty: aggregator (US infra; surfaced upstream per resolved model). The MVP primary for openalice-agent-lite — one wire, many models, with per-call model selection via `ChatRequest::model` (use the OpenRouter model id, e.g. `"google/gemini-3.5-flash"`).
llm-router/config
1 feature- llm-router.config.provider-policycrates/llm-router/src/config.rs:1Per-provider config + per-tenant policy. Encodes sovereignty (EU/US/Aggregator), GDPR Art 49 consent gate, regulated-sector US block, allowed-model allow-list. Router enforces these before dispatching to an adapter.
llm-router/cost
1 feature- llm-router.cost.tracker-traitcrates/llm-router/src/cost.rs:1CostTracker trait + default impl. Every successful call emits a CostEvent that consumers route into their own accounting (atlas /api/v1/usage/providers, openalice-tenants quota, billing exports). Default impl just logs at INFO so smoke tests / dev work need no wiring.
llm-router/crate
1 feature- llm-router.crate.model-agnostic-routercrates/llm-router/src/lib.rs:1Model-agnostic LLM provider router for OpenAlice. One LLMProvider trait, multiple adapters (Mistral / Claude / GPT / Gemini / IONOS Llama / OpenRouter), per-tenant policy (sovereignty + GDPR Art 49 consent + regulated-sector US block + allow-list), and a cost tracker that attributes token usage per provider × per tenant. Consumers: openalice-agent-lite (Embed), future kiosk, mobile, enterprise demo, lounge.
llm-router/error
1 feature- llm-router.error.llm-errorcrates/llm-router/src/error.rs:1Typed error enum for every fallible operation in the router. Adapters surface upstream HTTP / decode failures here; the router surfaces policy + config violations (unknown provider, consent-gate refusal, regulated-sector US-block).
llm-router/provider
1 feature- llm-router.provider.traitcrates/llm-router/src/provider.rs:1Single async trait every LLM adapter implements: chat completion + streaming chat completion. Wire-shape types (Message, ChatRequest, ChatResponse, Usage) are provider-neutral; adapters translate to/from their upstream's specific format.
llm-router/router
1 feature- llm-router.router.dispatchcrates/llm-router/src/router.rs:1Router struct that holds registered providers, enforces tenant policy (sovereignty + consent + sector), dispatches calls to the selected adapter, and fans out CostEvents to the configured CostTracker. The sole entry point for consumers.
mollie/sdk
15 features- mollie.sdk.clientcrates/mollie/src/client.rs:13MollieClient — bearer-auth REST client over reqwest. `new(&api_key)` or `from_env()` (reads MOLLIE_API_KEY). Wraps GET / POST against api.mollie.com/v2.type:MollieClientsince 0.1.0
- mollie.sdk.cratecrates/mollie/src/lib.rs:7Async Rust SDK for the Mollie payments REST API — Customer / Subscription / Payment / webhook payloads. Library crate; not yet wired into platform-api (billing milestone pending).crate:openalice-mollietype:MollieClienttype:MollieErrorsince 0.1.0
- mollie.sdk.customers.createcrates/mollie/src/client.rs:132POST /customers — create a Mollie customer (name + email, optional locale + metadata) used to anchor recurring subscriptions.since 0.1.0
- mollie.sdk.customers.getcrates/mollie/src/client.rs:145GET /customers/{id} — fetch a single customer by Mollie id.since 0.1.0
- mollie.sdk.errorcrates/mollie/src/error.rs:1Typed error enum — distinguishes MissingKey / Api{status, detail} / Http so callers can map to HTTP status + retry policy without parsing strings.type:MollieErrorsince 0.1.0
- mollie.sdk.payments.createcrates/mollie/src/client.rs:276POST /payments — one-off payment with amount, description, redirect_url, and optional webhook_url; returns the hosted Mollie checkout link in `_links.checkout`.since 0.1.0
- mollie.sdk.payments.create_idempotentcrates/mollie/src/client.rs:318Idempotent POST /payments — sends Idempotency-Key header to prevent double-charge on retry.since 0.1.1
- mollie.sdk.payments.getcrates/mollie/src/client.rs:333GET /payments/{id} — fetch a single payment's status (open, paid, canceled, failed, expired); used by webhook handlers to verify post-redirect state.since 0.1.0
- mollie.sdk.payments.listcrates/mollie/src/client.rs:343GET /payments — list all payments on the account; unwraps `_embedded.payments` for caller convenience.since 0.1.0
- mollie.sdk.subscriptions.cancelcrates/mollie/src/client.rs:241DELETE /customers/{cid}/subscriptions/{sid} — cancel a subscription; Mollie accepts both 200 and 204 responses, both are mapped to Ok(()).since 0.1.0
- mollie.sdk.subscriptions.createcrates/mollie/src/client.rs:157POST /customers/{id}/subscriptions — start a recurring subscription with amount, interval (e.g. "1 month"), description, and optional webhook_url.since 0.1.0
- mollie.sdk.subscriptions.create_idempotentcrates/mollie/src/client.rs:204Idempotent POST /customers/{id}/subscriptions — sends Idempotency-Key header to prevent double-charge on retry.since 0.1.1
- mollie.sdk.subscriptions.getcrates/mollie/src/client.rs:224GET /customers/{cid}/subscriptions/{sid} — fetch a single subscription state (active, suspended, canceled).since 0.1.0
- mollie.sdk.subscriptions.listcrates/mollie/src/client.rs:258GET /customers/{id}/subscriptions — list all subscriptions for a customer; unwraps Mollie's `_embedded.subscriptions` envelope so callers see Vec<Subscription>.since 0.1.0
- mollie.sdk.typescrates/mollie/src/types.rs:3Typed Rust structs for Mollie's Customer, Subscription, Payment, Amount, and webhook payloads — callers never hand-roll Mollie JSON. Mirrors v2 API field shapes with serde rename rules.type:Customertype:CreateCustomerRequesttype:Amounttype:Subscriptiontype:Paymentsince 0.1.0
mollie/webhook
5 features- mollie.webhook.dedupcrates/mollie/src/webhook.rs:258Plug-in trait for atomic webhook replay deduplication — callers supply a seen-id store; returns New/AlreadySeen per payment ID.since 0.1.1
- mollie.webhook.signature-verifycrates/mollie/src/webhook.rs:45HMAC-SHA256 signature verification for Mollie webhook callbacks. Header X-Mollie-Signature carries `sha256=<hex>`; verification uses constant-time comparison via `subtle::ConstantTimeEq` to prevent timing attacks.fn:verify_webhookfn:verify_from_headerstype:WebhookErrorsince 0.1.1
- mollie.webhook.signature-verify.errorcrates/mollie/src/webhook.rs:61Typed error enum for webhook verification — distinguishes missing header, malformed header, unconfigured secret, and signature mismatch.since 0.1.1
- mollie.webhook.signature-verify.verify_from_headerscrates/mollie/src/webhook.rs:174HeaderMap convenience wrapper — extracts X-Mollie-Signature (handles dual-header rotation) and calls verify_webhook.since 0.1.1
- mollie.webhook.signature-verify.verify_webhookcrates/mollie/src/webhook.rs:109Core HMAC-SHA256 verifier. Parses `sha256=<hex>` prefix, decodes hex, computes HMAC over raw body, compares with constant-time equality.since 0.1.1
openalice-rag/chunker
4 features- openalice-rag.chunker.htmlcrates/rag/src/chunker/html.rs:1HTML chunker — extracts main content from <main>, <article>, or <body>, strips tags via a simple regex (no DOM parser at scaffold phase), then delegates window-chunking to PlainTextChunker internally. Good enough for blog posts, docs pages, and marketing copy; a real DOM parser (scraper / lol-html) can replace the regex step later without changing the interface.
- openalice-rag.chunker.markdowncrates/rag/src/chunker/markdown.rs:1Markdown chunker — splits on H1/H2/H3 boundaries and enforces a soft token-size ceiling (~512 tokens ≈ ~2048 chars). Documents with no headers are treated as a single section. Each chunk carries the nearest heading text as metadata so the vector store and ranker can surface the section context alongside the retrieved text.
- openalice-rag.chunker.plain-textcrates/rag/src/chunker/plain_text.rs:1Fixed-window plain-text chunker with overlap. Splits the document body on character boundaries: each chunk is `window` chars wide, advancing by `window - overlap` chars on each step. Overlap ensures that retrieval doesn't miss content that spans a window boundary.
- openalice-rag.chunker.traitcrates/rag/src/chunker/mod.rs:1Chunker trait + concrete adapter re-exports. A chunker splits an input Document into retrieval-sized Chunks with positional metadata. Three adapters: MarkdownChunker (header-aware), PlainTextChunker (token-window with overlap), HtmlChunker (extracts main content, strips tags via regex).
openalice-rag/config
1 feature- openalice-rag.config.policycrates/rag/src/config.rs:1Per-embedder config + per-tenant policy. Encodes sovereignty (EU/US/Aggregator), GDPR Art 49 consent gate, regulated-sector US block, allowed-embedder allow-list. Router enforces these before dispatching to an adapter. Mirrors the shape of openalice-llm-router config exactly — same policy model, different noun (embedder).
openalice-rag/cost
1 feature- openalice-rag.cost.tracker-traitcrates/rag/src/cost.rs:1CostTracker trait + default impl. Every successful embed call emits a CostEvent that consumers route into their own accounting (atlas /api/v1/usage/providers, openalice-tenants quota, billing exports). Default impl just logs at INFO so smoke tests / dev work need no wiring.
openalice-rag/crate
1 feature- openalice-rag.crate.rag-pipelinecrates/rag/src/lib.rs:1Model-agnostic RAG ingestion + retrieval for OpenAlice. Three orthogonal abstractions: Chunker (document → chunks), Embedder (chunks → vectors), VectorStore (persist + retrieve by similarity). One RagRouter composes all three: index(documents) runs chunk → embed → store; retrieve(query, k) runs embed → search. Per-tenant policy mirrors openalice-llm-router (sovereignty + GDPR Art 49 consent + regulated-sector block). Cost tracking emits CostEvent per embed call. Consumers: openalice-agent-lite BrandKnowledge; dashboard search; future Alice memory injection.
openalice-rag/embedder
5 features- openalice-rag.embedder.mistralcrates/rag/src/embedder/mistral.rs:1Mistral embedder adapter — mistral-embed (1024-dim, EU-sovereign, no GDPR consent gate needed). Good default for EU-regulated-sector tenants who cannot use US providers.
- openalice-rag.embedder.mockcrates/rag/src/embedder/mock.rs:1Deterministic in-memory embedder. Returns synthetic vectors derived from a stable hash of each input string — same text always produces the same vector, different texts produce orthogonal-ish vectors. No network calls, zero setup. Primary embedder for unit + integration tests in this crate and in consumers that don't want a live API.
- openalice-rag.embedder.openaicrates/rag/src/embedder/openai.rs:1OpenAI embedder adapter — text-embedding-3-small (1536-dim, US, GDPR Art 49 consent required for EU tenants). Higher quality than BGE-M3 on English-only content; sovereignty gate enforced by the router policy before this adapter is reached.
- openalice-rag.embedder.openroutercrates/rag/src/embedder/openrouter.rs:1OpenRouter embedder adapter. BGE-M3 via OpenRouter's OpenAI-compatible /api/v1/embeddings endpoint. Sovereignty = aggregator (US infra; consent gate inherited from router policy). MVP primary embedder — one key, many embedding models, consistent wire format.
- openalice-rag.embedder.traitcrates/rag/src/embedder/mod.rs:1Embedder trait + concrete adapter re-exports. An embedder maps a slice of text strings into a fixed-size vector (embedding). MVP primary is OpenRouterEmbedder (BGE-M3). Additional adapters: OpenAI (text-embedding-3-small), Mistral (mistral-embed). MockEmbedder for
openalice-rag/error
1 feature- openalice-rag.error.rag-errorcrates/rag/src/error.rs:1Typed error enum for every fallible operation in the RAG pipeline. Adapters (chunkers, embedders, stores) surface upstream HTTP / decode / I/O failures here; the router surfaces config + policy violations (unknown embedder, unknown store, consent-gate refusal, regulated-sector US-block for US embedding providers).
openalice-rag/ingest
1 feature- openalice-rag.ingest.urlcrates/rag/src/ingest.rs:1Tenant-scoped URL ingestion pipeline for brand knowledge. Given a (tenant_id, https-url) it fetches the page (HTTPS-only, with a timeout and a response size cap to bound memory + block SSRF-style abuse), strips it to text, chunks it, embeds each chunk via the router's policy-selected Embedder, and upserts the chunks scoped to the tenant. This is the production path behind `brand_kb_url`. The embedder is the one selected by the tenant policy + router config — in production that MUST be the EU-sovereign MistralEmbedder; MockEmbedder is tests only.
openalice-rag/router
1 feature- openalice-rag.router.dispatchcrates/rag/src/router.rs:1RagRouter — the single entry point for consumers. Composes a Chunker, an Embedder, and a VectorStore into two operations: index(documents) → chunk → embed → upsert retrieve(query, k) → embed query → similarity-search Per-tenant policy is enforced on every embed call (sovereignty + consent + sector). Cost events are emitted after every successful embed via the configured CostTracker.
openalice-rag/stats
1 feature- openalice-rag.stats.kb-statscrates/rag/src/stats.rs:1Tenant-scoped knowledge-base statistics queried directly from the `rag_chunks` table. These are the ONLY honest metrics the schema can produce without additional instrumentation: - `total_chunks` — COUNT(*) for the tenant (or 0 when unindexed). - `distinct_docs` — COUNT(DISTINCT document_id) for the tenant; each distinct document_id corresponds to one ingested file or URL (the router assigns a stable document_id per source before chunking). - `last_indexed_at` — MAX(created_at) for the tenant; null when zero rows. NOTE: The schema has NO `widget_id` column in `rag_chunks` — chunks are scoped by `tenant_id` only. All stats here are therefore ACCOUNT-LEVEL (tenant-level), not per-widget. The caller is responsible for labelling the response correctly so the dashboard never presents these as "per-widget" numbers. Coverage % and retrieval latency are deliberately absent — neither is genuinely measurable from the chunks table alone, and inventing them would violate the honest-data mandate.
openalice-rag/store
3 features- openalice-rag.store.inmemorycrates/rag/src/store/inmemory.rs:1In-memory vector store backed by a HashMap and a linear cosine- similarity scan. Not suitable for production at scale (O(n) retrieval) but zero external dependencies, fully deterministic, and fast enough for unit tests, dev, and tenants with small corpora (< 10k chunks).
- openalice-rag.store.pgvectorcrates/rag/src/store/pgvector.rs:1Production vector store backed by Postgres + the pgvector extension. Uses sqlx for async query execution. Gated behind the `pgvector` feature flag so the default build doesn't compile sqlx and stays fast. The schema lives in `migrations/0001_rag_chunks.sql` (base table) + `migrations/0002_rag_chunks_rls.sql` (RLS tenant-isolation backstop) — apply both before first use. Column contract (must stay in sync with the migration): id TEXT PRIMARY KEY -- Chunk::id document_id TEXT NOT NULL -- Chunk::document_id tenant_id TEXT -- scoping key (stamped by the router) source_url TEXT -- Chunk::source_url chunk_index INTEGER -- Chunk::position content TEXT NOT NULL -- Chunk::text embedding vector(N) NOT NULL -- ChunkWithVector::vector (N = embedder dims) metadata JSONB -- Chunk::metadata created_at TIMESTAMPTZ Tenant isolation is enforced TWICE (audit finding B1, 2026-07 — the original app-level-only WHERE clause had zero DB backstop): 1. App level — every query still emits an explicit `WHERE tenant_id = $n` clause (defense-in-depth; keeps query plans tenant-pruned). 2. DB level — every operation runs inside a transaction that binds `app.tenant_id` via `SELECT set_config('app.tenant_id', $1, true)` (`with_tenant_context`, below) BEFORE touching the table. The `rag_chunks_tenant_isolation` policy (migration 0002) is FORCE- enforced, so even a hypothetical future call that forgets the WHERE clause — or a raw/injected query against this table — sees ZERO rows when `app.tenant_id` isn't bound, and the policy's `WITH CHECK` rejects any INSERT/UPDATE writing a mismatched tenant_id. The runtime role (`rag_app`) is NOSUPERUSER NOBYPASSRLS so the FORCE actually bites it — see the migration header for the role-creation prerequisite.
- openalice-rag.store.traitcrates/rag/src/store/mod.rs:1VectorStore trait + concrete adapter re-exports. A vector store persists ChunkWithVector records and answers approximate-nearest- neighbour queries by embedding similarity. Two adapters in this PgvectorStore (sqlx + pgvector — production, behind feature flag).
openalice-rag/tests
3 features- openalice-rag.tests.pg-rls-integrationcrates/rag/tests/pg_rls_test.rs:1Postgres-integration proof of the rag_chunks tenant-isolation mechanism (audit finding B1) — the regression net the adversarial review demanded: the FORCE-RLS + `app.tenant_id` machinery had ZERO automated coverage (every proof was a manual psql session), so a future migration edit or policy drop would ship silently. Proves, against a REAL Postgres: 1. the concatenated migration (`RAG_CHUNKS_MIGRATION_SQL` = 0001+0002) applies twice cleanly (the boot-time replay contract); 2. a no-context read under a non-BYPASSRLS role sees ZERO rows even with a bare SELECT (FORCE RLS safe-deny); 3. a bound `app.tenant_id` sees exactly that tenant's rows — and a foreign/unknown tenant context sees none of them; 4. `with_tenant_context`'s SET LOCAL binding does NOT leak across a reused pooled connection (pool pinned to max_connections=1 so the same physical connection is provably reused); 5. the policy's WITH CHECK rejects a cross-tenant INSERT. GATING: needs `TEST_DATABASE_URL` pointing at a Postgres whose role can CREATE SCHEMA + CREATE ROLE (dev: the `rag` superuser on the rag DB, or any scratch DB on a cluster with the pgvector extension available). Unset ⇒ the test SKIPS (prints + returns) so plain `cargo test` stays green everywhere, mirroring embed-accounts' TEST_DATABASE_URL suite. ISOLATION: everything runs in a throwaway `rag_rls_test_<hex>` schema (search_path pinned per-connection) + a throwaway NOLOGIN probe role; both are dropped at the end. The cluster-wide `rag_app` role is created only if missing (migration 0002 needs it as table owner) and is never altered — safe to run against a shared dev cluster.
- openalice-rag.tests.router-integrationcrates/rag/tests/router_test.rs:1Integration tests for RagRouter covering: index + retrieve roundtrip, chunker output shapes, deterministic mock embedder, tenant scoping, empty query behaviour, and document deletion. All tests use MockEmbedder + InMemoryStore — no network calls, no external deps.
- openalice-rag.tests.unitcrates/rag/src/tests.rs:1Comprehensive unit tests for openalice-rag. Covers: - SSRF guard edge-cases (ssrf::is_blocked_ip + vet_ingest_url) - PlainTextChunker (empty / short / multi-chunk / overlap / unbroken) - MarkdownChunker (empty / no-headers / multi-headers / soft-flush) - HtmlChunker (extraction priority / tag-stripping / whitespace / error) - InMemoryStore (retrieve scoping / upsert idempotency / delete_document / cosine ordering / k-limit / purge_source cross-tenant) - config::Pricing::cost_cents (boundary values) - error::RagError::is_retryable (HTTP status matrix) - RagRouter policy enforcement (no-embedder / consent / regulated / allowed_embedders allow-list) - ingest::mime_for additional MIME variants Skipped (external deps required): - PgvectorStore — needs live Postgres + pgvector - OpenAiEmbedder / MistralEmbedder / OpenRouterEmbedder — need live APIs - ingest_url network fetch — needs an actual HTTPS server - stats_for_tenant — needs live Postgres These are integration / smoke tests, not unit tests.
openalice-rag/types
1 feature- openalice-rag.types.document-chunkcrates/rag/src/types.rs:1Core data types shared across the RAG pipeline: Document (ingestion input), Chunk (chunker output), ChunkWithVector (embedder output), RetrievedChunk (store output). All are Clone + Serialize + Deserialize so they can cross async task / actor boundaries without lifetime gymnastics.
platform/lint-reuse
1 feature- platform.lint-reuseapps/landing/src/test/LintReuse.test.ts:2Tests for scripts/lint-reuse.mjs — the REUSE gate (2026-07-16). The inverse of lint-theme.mjs: instead of catching a raw value that drifted OUTSIDE the token system, this catches code that reinvents something a shared component or the class+token system already provides. Verifies, using fixture TEXT (no filesystem/child-process — findStyleLiteralFindings/findRawElementFindings are pure functions of their input), that: a hardcoded inline-style design literal fails; the same value routed through var() or driven by a dynamic expression passes; a CSS custom-property setter always passes; a raw dashboard element standing in for a live @openalicelabs/ui component fails; the same element as a real `<Component>` usage passes; the `reuse-lint-allow` marker suppresses either; and the component registry loader correctly resolves `as`-aliased exports and skips type-only ones. Imports the script from the repo root (this tool is intentionally NOT landing-scoped — it also rules on apps/dashboard).
presets/lib
1 feature- presets.lib.packageincoming/presets/src/index.ts:16Stream-engagement preset library — six personality + producer config blobs an agent adopts at broadcast start (system prompt + rate budget + scoring weights bundled together).npm:@openalicelabs/presetssince 0.1.0
presets/loader
2 features- presets.loader.defaultincoming/presets/src/index.ts:159DEFAULT_PRESET_ID — the "just go live" fallback used by Studio when the broadcaster hasn't picked a preset. Points at the qa-host preset (Auto mode + Q&A rate budget).const:DEFAULT_PRESET_IDsince 0.1.0
- presets.loader.registryincoming/presets/src/index.ts:144PRESETS array + getPreset(id) lookup — the public surface consumers (Studio picker, chat-bridge Director, agents) use to enumerate and load presets by stable id.const:PRESETSfn:getPresetsince 0.1.0
presets/scene
6 features- presets.scene.battle-chatincoming/presets/src/index.ts:107High-velocity battle/roast preset — selective mode at 4 responses/min, one-liner wit, low question weight, VIP bonus. For competitive gaming, debates, and fast banter formats.preset:battle-chatsince 0.1.0
- presets.scene.coding-streamincoming/presets/src/index.ts:121Live-coding preset — selective at ~1 response/min, high topic-relevance + question weights, tool_calls budget cranked to 120 so the agent can multitask code+chat without blowing the rate gate.preset:coding-streamsince 0.1.0
- presets.scene.cozy-hangoutincoming/presets/src/index.ts:114Low-pressure echo-mode preset — answers nearly everything at 6 responses/min, strong first-time-chatter bonus, low score floor. Best for small audiences and chill streams.preset:cozy-hangoutsince 0.1.0
- presets.scene.lecture-moderatorincoming/presets/src/index.ts:100Long-form lecture / tutorial preset — highlight mode with 5-minute chat-break windows surfacing the top 3-5 questions; heavy topic-relevance weighting so chat can't derail the talk.preset:lecture-moderatorsince 0.1.0
- presets.scene.qa-hostincoming/presets/src/index.ts:93Default Q&A host preset — selective mode at ~2 responses/min, paced conversational answers, first-time-chatter bonus. The "I just want to go live" starting point.preset:qa-hostsince 0.1.0
- presets.scene.talk-show-panelincoming/presets/src/index.ts:128Multi-AI co-host preset — Director emits {score, route} so each message goes to the persona whose topic+temperament best matches. For 2-4 distinct personas (Alice + Persona + custom).preset:talk-show-panelsince 0.1.0
presets/schema
1 feature- presets.schema.presetincoming/presets/src/index.ts:30Typed shape of a preset (id, label, mode, score_threshold, system+director prompts, rate limits, per-signal weights) — source of truth shared by Studio picker, Director scoring, and consumers.type:Presettype:PresetLimitstype:PresetWeightssince 0.1.0
realtime/manager
4 features- realtime.manager.classincoming/realtime/src/manager.ts:21Framework-agnostic WebSocket lifecycle — lazy URL factory, jittered exponential backoff with stability gate, tracked subscriptions, status events.npm:@openalicelabs/realtimesince 0.1.0
- realtime.manager.connectincoming/realtime/src/manager.ts:106Idempotent connect — resolves the lazy url() factory, opens a WebSocket, wires onopen/onmessage/onclose/onerror handlers, and bails cleanly on null URL or disposal.since 0.1.0
- realtime.manager.disconnectincoming/realtime/src/manager.ts:136Tear down the socket, clear reconnect + stability timers, detach handlers, and flip status to closed — terminal (sets disposed so connect() becomes a no-op).since 0.1.0
- realtime.manager.sendincoming/realtime/src/manager.ts:212Fire-and-forget JSON.stringify + WebSocket.send; no buffering, no retry — callers must gate on onStatus("open") for delivery guarantees.since 0.1.0
realtime/package
1 feature- realtime.packageincoming/realtime/src/index.ts:7npm package entry — re-exports the framework-agnostic RealtimeManager and the React useRealtime hook for OpenAlice front-ends.npm:@openalicelabs/realtimesince 0.1.0
realtime/react
1 feature- realtime.use_realtime_hookincoming/realtime/src/useRealtime.ts:44React hook that wraps RealtimeManager — manages mount/unmount lifecycle, exposes status for UI badges, returns the manager for subscribe/send/etc.hook:useRealtimesince 0.1.0
realtime/reconnect
1 feature- realtime.manager.backoffincoming/realtime/src/manager.ts:271Jittered exponential backoff on close — doubles per attempt, capped at maxBackoffMs, ±20% jitter, resets only after stableAfterMs of continuous open to avoid masquerading fast-disconnect loops.since 0.1.0
realtime/subscriptions
2 features- realtime.manager.subscribeincoming/realtime/src/manager.ts:168Add channel ids to the tracked Set and emit a single subscribe frame for the freshly-added subset; reconnect handler replays the whole set on open.since 0.1.0
- realtime.manager.unsubscribeincoming/realtime/src/manager.ts:187Drop ids from the tracked Set and emit a single unsubscribe frame — guarantees a later reconnect won't re-add removed channels.since 0.1.0
remotion/primitives
5 features- remotion.cursor-spritepackages/remotion/src/CursorSprite.tsx:2CursorSprite — a reusable animated pointer sprite (arrow + click ripple) for fixture-driven montage scenes. Pure presentation, colors/scale parameterized — a product supplies x/y/clickProgress (via interpolate/spring) and its own brand colors.package:@openalicelabs/remotionsince 0.1.0
- remotion.deterministic-iframepackages/remotion/src/DeterministicIframe.tsx:2DeterministicIframe — mounts a same-origin <iframe> and drives it to the exact DOM state for the current Remotion frame before capture, via a caller-named window-global seek function, gated behind delayRender/continueRender plus a paint-settle double-rAF. Generalizes the "screenshot a live embedded app, frame by frame" pattern with zero product coupling.package:@openalicelabs/remotionsince 0.1.0
- remotion.sequence-cyclepackages/remotion/src/SequenceCycle.tsx:2SequenceCycle — cycles a fixed list of items through equal-length back-to-back <Sequence> windows, one fresh mount per item. Extracted from the "montage that cycles N fixture scenarios through one composition" pattern.package:@openalicelabs/remotionsince 0.1.0
- remotion.stagger-inpackages/remotion/src/stagger.ts:2staggerIn — a pure fade+slide-up entrance computation (spring-driven, delay-offset) extracted from the "panel N of a montage enters at its own delayed beat" pattern. Call it once per item with an increasing delay for a staggered reveal.package:@openalicelabs/remotionsince 0.1.0
- remotion.toolkitpackages/remotion/src/index.ts:2Abstract Remotion toolkit (@openalicelabs/remotion) — the deterministic iframe driver, cursor sprite, and montage/stagger helpers products use to build promo videos, showcases, and onboarding renders. NO product imports live in this package; embed-remotion is its first consumer, not a dependency.package:@openalicelabs/remotionsince 0.1.0
remotion/tooling
1 feature- remotion.sync-assetspackages/remotion/src/sync-assets.ts:2syncAssets — reusable "copy a Remotion project's runtime asset dependencies (a built SDK bundle, fixture/playbook JSON, avatar layer-PNGs, ...) into its OWN public/ folder" helper, so Remotion's dev server / renderer can serve them same-origin. Node-only; a separate subpath export kept out of the browser-facing composition bundle.package:@openalicelabs/remotionsince 0.1.0
rt-client/client
3 features- rt-client.client.crateincoming/rt-client/src/index.ts:154Typed WebSocket client for openalice-rt — owns Subscribe/Mutate envelope shapes, subscription replay on reconnect, and outbox draining; delegates connection lifecycle to RealtimeManager.class:RtClientsince 0.1.0
- rt-client.client.mutateincoming/rt-client/src/index.ts:259Send a typed create/update/delete mutation envelope to openalice-rt; frames are queued in the outbox and flushed on reconnect if the socket is temporarily down.method:RtClient.mutatesince 0.1.0
- rt-client.client.subscribeincoming/rt-client/src/index.ts:222Subscribe to room entity-delta streams; returns an RtSubscription handle that is automatically replayed after reconnect and can be dropped via unsubscribe().method:RtClient.subscribeinterface:RtSubscriptionsince 0.1.0
rt-client/protocol
1 feature- rt-client.protocol.wire-typesincoming/rt-client/src/index.ts:26Typed envelopes mirroring openalice-rt/src/protocol.rs — EntityType, Action, SubQuery, DeltaEntry, DeltaSet, ClientMessage, and ServerMessage keep consumers in sync with the Rust server protocol without per-product re-implementation.type:EntityTypetype:Actioninterface:SubQueryinterface:DeltaEntryinterface:DeltaSettype:ClientMessagetype:ServerMessagesince 0.1.0
sdk/auth
1 feature- sdk.auth.mfapackages/sdk/src/index.ts:22Full MFA (TOTP) lifecycle — setup, verify, disable, and login flow with pre_mfa_token exchange — enabling two-factor enforcement across all OpenAlice frontends.package:@openalicelabs/sdksince 0.3.0
sdk/client
2 features- sdk.client.corepackages/sdk/src/index.ts:12OpenAliceClient — single fetch-based class wrapping every REST endpoint across openalice-auth and openalice-social-api with typed responses, Bearer auth, cookie-credential forwarding, and automatic rate-limit error surfacing.package:@openalicelabs/sdksince 0.1.0
- sdk.client.cratepackages/sdk/src/index.ts:2Zero-dependency TypeScript REST client for the full OpenAlice platform surface — auth, social, live, billing, and ML — published as @openalicelabs/sdk.package:@openalicelabs/sdksince 0.1.0
sdk/errors
1 feature- sdk.errors.rate-limitpackages/sdk/src/index.ts:42Typed RateLimitError class extending ApiRequestError — parses Retry-After headers and retry_after body fields on HTTP 429 responses so callers can back-off deterministically.package:@openalicelabs/sdksince 0.1.0
sdk/live
1 feature- sdk.live.agent-configpackages/sdk/src/index.ts:32Agent config + broadcast config endpoints — read and upsert per-user AI agent identity, voice settings, per-product enablement, scene allowlists, renderer mode, and studio preset persistence used by the Live control plane.package:@openalicelabs/sdksince 0.5.0
security/pow_captcha
1 feature- security.pow_captcha.challengecrates/oa-framework/src/pow_captcha.rs:2HMAC-signed, stateless-to-issue proof-of-work challenge that gates a sensitive unauthenticated surface (currently: openalice-identity's login-abuse escalation, see `openalice_identity::throttle`) behind a small, invisible client-side CPU cost — WITHOUT any third-party service, API key, or network call. DESIGN NOTE — this is ALTCHA-EQUIVALENT, not an ALTCHA PORT: the founder asked for "a self-hosted proof-of-work captcha — use ALTCHA". ALTCHA's current (v3.x, altcha.org) wire protocol turned out to be a KDF/prefix-matching scheme with a canonical-JSON-serialized HMAC signature (`HMAC(JSON.stringify(sortKeys(parameters)))`) — replicating that byte-for-byte in Rust, on the AUTH TRUST ROOT, under review-gate time pressure, without a live JS runtime to cross-check vectors against, was judged too risky (a canonicalization mismatch either breaks every legitimate login attempt, or worse, silently accepts a forged one). This module instead implements a first-party protocol with the SAME properties ALTCHA provides — self-hosted, HMAC-signed by our own secret, invisible client-side proof-of-work, zero third party, activates with no external key — using primitives already proven in this exact codebase (`webhook.rs::hmac_sign`, `embed_guard.rs::sign_attachment_url` — bind a tuple, HMAC it, recompute + constant-time-compare to verify). It is small enough to read start to finish in one sitting, which is the property that actually matters for something gating authentication. PROTOCOL (classic Hashcash-style, textbook + easy to audit): 1. `issue()` mints a random `challenge_id` (UUIDv4), picks `difficulty` (required leading ZERO BITS) and `expires_at` (unix seconds, short TTL), and signs the tuple: signature = hex(HMAC-SHA256(secret, "{id}.{difficulty}.{expires_at}")) Stateless — no write happens on issue, so it's safe to call on every gated response. 2. The client brute-forces a `nonce` such that SHA256("{challenge_id}:{nonce}") has at least `difficulty` leading zero BITS, then echoes back the full challenge (id/difficulty/expires_at/signature) plus the found `nonce`. 3. `verify()` checks, in order: (a) the HMAC signature over the SUBMITTED (id, difficulty, expires_at) — this is what makes `difficulty`/`expires_at` tamper-evident: an attacker who lowers the difficulty or extends the expiry invalidates the signature, so those fields don't need separate integrity checks; (b) not expired; (c) not already consumed (replay guard — a solved challenge is single-use, else an attacker could solve one and replay it across unlimited login attempts, defeating the whole point of the CPU cost); (d) the proof-of-work itself. Only a full `Ok` marks the challenge consumed.
stt/adapters
5 features- stt.adapters.elevenlabs-scribecrates/stt/src/adapters/elevenlabs.rs:1ElevenLabs Scribe adapter — MVP primary STT. Sovereignty: US, GDPR Art 49 consent required. Same vendor as our TTS (single key), best-in-class accuracy + speaker diarisation. API contract: POST https://api.elevenlabs.io/v1/speech-to-text multipart/form-data with `file` (audio blob), `model_id` (`scribe_v1` default), optional `language_code`, optional `tag_audio_events`. Returns JSON with `text` + `language_code` + `language_probability` + `words[]`.
- stt.adapters.mistral-nativecrates/stt/src/adapters/mistral_native.rs:1Mistral Voxtral STT via the DIRECT api.mistral.ai endpoint. Sovereignty: EU (api.mistral.ai is EU-hosted, no US routing). Uses MISTRAL_API_KEY — the same key the TTS MistralNativeAdapter uses — NOT the shared OpenRouter key. This is the fix for the embed widget 502: OpenRouter does NOT support audio transcription (it is a chat-completions gateway), so voxtral-openrouter calls fail. This adapter calls the real Mistral audio endpoint directly. API contract: POST https://api.mistral.ai/v1/audio/transcriptions multipart/form-data: file — the raw audio blob (Content-Type: <mime>; filename: audio.webm) model — e.g. "voxtral-mini-latest" (default) language — ISO-639-1 (optional, auto-detect when absent) Authorization: Bearer <MISTRAL_API_KEY> Response JSON: { text: "...", ... } (Same shape as OpenAI Whisper — the `text` field is the transcript.)
- stt.adapters.mockcrates/stt/src/adapters/mock.rs:1In-memory adapter that returns a scripted transcript. Used by the integration tests in this crate and by consumer tests that don't want to hit a real provider.
- stt.adapters.modulecrates/stt/src/adapters/mod.rs:1Adapter module — one file per STT provider.
- stt.adapters.voxtral-openroutercrates/stt/src/adapters/voxtral_openrouter.rs:1Voxtral (Mistral) STT via OpenRouter — secondary STT option. Sovereignty: Aggregator (OpenRouter resolves the upstream; Voxtral is Mistral, EU-origin, but routed through OpenRouter's US-hosted gateway, so we treat it as Aggregator for policy purposes). Shares the single OPENROUTER_API_KEY with the LLM router — NOT a new env var. API contract: POST https://openrouter.ai/api/v1/audio/transcriptions Headers: Authorization: Bearer <key>, Content-Type: application/json Body JSON: { "model": "mistralai/voxtral-mini-transcribe", "input_audio": { "data": "<base64 raw bytes>", "format": "webm|mp3|wav|ogg|m4a|flac|aac" }, "language": "<iso-639-1, optional>" } Response JSON: { "text": "...", "usage": { "seconds": 9.2, "total_tokens": 113, "input_tokens": 83, "output_tokens": 30, "cost": 0.000508 } } // cost in DOLLARS
stt/config
1 feature- stt.config.provider-policycrates/stt/src/config.rs:1Mirror of openalice-llm-router::config — same Sovereignty + Sector + TenantPolicy + ProviderConfig shapes so consumers can treat both routers uniformly. Pricing here is per-audio-second (vs per-million- tokens for LLMs).
stt/cost
1 feature- stt.cost.tracker-traitcrates/stt/src/cost.rs:1Mirror of openalice-llm-router::cost. CostEvent shape differs only in the metric (audio_seconds vs tokens) so consumers can route both into the same atlas / openalice-tenants accounting pipeline by discriminating on `kind`.
stt/crate
1 feature- stt.crate.model-agnostic-routercrates/stt/src/lib.rs:1Model-agnostic STT provider router for OpenAlice. One STTProvider trait, multiple adapters (ElevenLabs Scribe today; Whisper API, Deepgram, Whisper.cpp planned). Per-tenant policy (sovereignty, GDPR consent, regulated-sector block, allow-list), cost tracker attributing audio-second usage per provider × per tenant. Sister crate to openalice-llm-router + openalice-tts.
stt/error
1 feature- stt.error.stt-errorcrates/stt/src/error.rs:1Typed error enum for every fallible STT operation. Same shape as openalice-llm-router::LLMError so consumers can normalise.
stt/provider
1 feature- stt.provider.traitcrates/stt/src/provider.rs:1Single async trait every STT adapter implements: transcribe_blob (single-shot) + transcribe_stream (live, partial + final chunks). Wire-shape types (AudioFormat, Transcript, TranscriptChunk) are provider-neutral; adapters translate to/from upstream wire formats.
stt/router
1 feature- stt.router.dispatchcrates/stt/src/router.rs:1STT router — mirrors openalice-llm-router::Router. Same policy providers (consent_us_stt rather than consent_us_llms — separate gate so customers can opt-in voice ↔ LLM independently), regulated-sector US block, allowed_providers allow-list.
tenants/sso
2 features- tenants.sso.oidc-corecrates/identity/src/oidc/mod.rs:3Standards-correct OIDC Authorization-Code-with-PKCE primitives for per-org enterprise SSO: discovery (.well-known) + JWKS fetch/cache, PKCE (S256) + CSRF state + replay nonce generation, and id_token verification (asymmetric-alg allow-list, JWKS-by-kid signature check, iss/aud/exp/nbf + nonce). Built on the vetted `jsonwebtoken` crate (RS/ES/PS via DecodingKey::from_jwk) — no hand-rolled crypto. All IdP HTTP runs on a dedicated 30s-timeout client. The HTTP flow handlers that drive these primitives live in `handlers::sso`. The owner/admin config CRUD lives in `handlers::me::sso`.
- tenants.sso.saml-corecrates/identity/src/saml/mod.rs:3Hand-written, pure-Rust SAML 2.0 Service-Provider validation for per-org enterprise SSO (SP-initiated, HTTP-POST ACS). The security-critical assertion-validation logic — the XML-Signature-Wrapping (XSW) defenses — is owned in auditable code (signature.rs): reject >1 Assertion/Response; resolve each ds:Reference to a SINGLE element by ID (no descendant axis); assert signed-element ⊇ consumed-element; an algorithm allow-list (rsa-sha256 / sha256 / exc-c14n only — SHA-1/HMAC/none rejected) BEFORE any crypto; per-Reference exc-c14n → sha256 constant-time digest compare; SignedInfo exc-c14n → RSA-SHA256 verify against the PINNED configured IdP cert (never the response's embedded KeyInfo). Plus DTD/XXE reject, Extensions schema-harden, and fail-closed void-canonicalization. Exclusive C14N is borrowed from the vetted pure-Rust `bergshamra-c14n`; the signature verify uses `ring` (RSA_PKCS1_2048_8192_SHA256), digest `sha2`, cert parse `x509-parser`, XML `uppsala`, Redirect-binding DEFLATE `miniz_oxide`. Profile checks (verify.rs): Conditions NotBefore/NotOnOrAfter (±30s leeway, reusing the OIDC constant), AudienceRestriction == SP entityID, bearer SubjectConfirmation (Recipient == ACS, NotOnOrAfter valid, NotBefore absent, InResponseTo == the stored AuthnRequest ID — single-use via sso_login_states, the SAML mirror of OIDC state/nonce), Issuer == configured IdP entityID. Claims (NameID + email) read ONLY from the signature-bound subtree. AuthnRequest, auto-metadata-ingest, multi-IdP per org. The HTTP flow handlers (`saml_start`/`saml_acs`/`saml_metadata`) live in `handlers::sso`; the owner/admin config CRUD lives in `handlers::me::sso`. The complete security spec is `docs/SAML_SP_SECURITY_DESIGN.md`.
tts/adapters
6 features- tts.adapters.elevenlabs-v3crates/tts/src/adapters/elevenlabs.rs:1ElevenLabs V3 adapter — MVP primary TTS. Sovereignty: US, GDPR Art 49 consent required. Same vendor as our STT (single key). Supports inline audio tags via the EmotionHint enum — translate_emotion() maps high-level hints to [bracketed] V3 tags prepended to the text. Raw passthrough is automatic: user-supplied [tags] in the text are never stripped. Blob endpoint: POST https://api.elevenlabs.io/v1/text-to-speech/{voice_id} Stream endpoint: POST https://api.elevenlabs.io/v1/text-to-speech/{voice_id}/stream Headers: xi-api-key: <key> (NOT bearer) Content-Type: application/json Accept: audio/mpeg (or audio/wav per output_format) Request body (eleven_v3 shape): { "text", "model_id", "language_code", "voice_settings", "output_format", "seed" }
- tts.adapters.fish-audiocrates/tts/src/adapters/fish_audio.rs:1Fish Audio TTS adapter — the CHEAP US standard voice (~$0.015 / 1k bytes, ~3–7× cheaper than ElevenLabs). Sovereignty: US, GDPR Art 49 consent required (same gating as ElevenLabs). Endpoint: POST https://api.fish.audio/v1/tts (verified live 2026-06-26) Headers: Authorization: Bearer <key> Content-Type: application/json Body: { "text", "format" (mp3|wav|opus), "reference_id"? (voice), "mp3_bitrate"? } reference_id omitted ⇒ Fish's default voice. Response: raw audio bytes (audio/mpeg for mp3). BILLING NOTE: Fish bills per UTF-8 BYTE, not per character — so accented (é/ü/ñ) and CJK text costs ~1.5–4× more than the same length of ASCII. We therefore compute `cost_cents` from BYTES while still reporting the human `characters` count in `Usage` (parity with how the dashboard shows length).
- tts.adapters.mistral-nativecrates/tts/src/adapters/mistral_native.rs:1Mistral-native TTS adapter + Voices (cloning) API client. EU-sovereign: calls api.mistral.ai DIRECTLY with MISTRAL_API_KEY — NOT via OpenRouter (the OpenRouter path is `voxtral_openrouter` and is sovereignty=Aggregator; it does NOT expose the Voices/cloning API). Mistral SAS is a French company processing in EU infra, so `MistralNativeAdapter::config()` reports `Sovereignty::Eu` and the GDPR Art 49(1)(a) US-consent gate is bypassed by the router. Two structs share one `reqwest::Client` + `api_key`: 1. `MistralNativeAdapter` (impl `TTSProvider`) — synthesis. Blob endpoint: POST https://api.mistral.ai/v1/audio/speech Headers: Authorization: Bearer <MISTRAL_API_KEY>, Content-Type: application/json Body JSON: { "model": "<model>", "input": "<text>", "voice": "<voice id|name>", "response_format": "mp3" } (No "speed": Mistral 422s on unknown fields — extra_forbidden.) Response: JSON envelope `{"audio_data":"<base64 mp3>"}` (Content-Type application/json — even with `Accept: audio/mpeg`; verified 2026-06-11). synth_blob unwraps + base64-decodes it; raw bodies pass through. Mirrors `VoxtralOpenRouterAdapter::synth_blob` exactly — same request shape — only the base_url, auth header, sovereignty and provider id differ. 2. `MistralVoicesClient` — the voice-cloning library API. Hits api.mistral.ai/v1/voices. Used server-side by openalice-tenants (/me/voices CRUD) to create/list/get/update/delete cloned voices and fetch sample audio. The created voice id is then passed back as the `voice` parameter in a normal `synth_blob` call. Create: POST /v1/voices List: GET /v1/voices Get: GET /v1/voices/{id} Update: PATCH /v1/voices/{id} Delete: DELETE /v1/voices/{id} Sample: GET /v1/voices/{id}/sample (raw audio bytes) SECURITY: MISTRAL_API_KEY must NEVER reach the browser — every Voices call is server-side only. Voice ids from Mistral are treated as opaque strings; callers validate ownership (id, tenant_id) before any mutation.
- tts.adapters.mockcrates/tts/src/adapters/mock.rs:1In-memory adapter that returns a scripted audio blob (empty bytes by default). Used by the integration tests in this crate and by consumer tests that don't want to hit a real provider.
- tts.adapters.modulecrates/tts/src/adapters/mod.rs:1Adapter module — one file per TTS provider.
- tts.adapters.voxtral-openroutercrates/tts/src/adapters/voxtral_openrouter.rs:1Voxtral (Mistral) TTS via OpenRouter — secondary TTS option. Sovereignty: Aggregator (OpenRouter resolves the upstream). Shares the single OPENROUTER_API_KEY with the LLM router — NOT a new env var. API contract: POST https://openrouter.ai/api/v1/audio/speech Headers: Authorization: Bearer <key>, Content-Type: application/json Body JSON: { "model": "mistralai/voxtral-mini-tts-2603", "input": "<text>", "voice": "<voice name>", "response_format": "mp3", "speed": 1.0 } Response: RAW audio bytes (Content-Type audio/mpeg for mp3), NOT JSON. There may be an X-Generation-Id header.
tts/config
1 feature- tts.config.provider-policycrates/tts/src/config.rs:1Mirror of openalice-llm-router::config + openalice-stt::config — same Sovereignty + Sector + TenantPolicy + ProviderConfig shapes so consumers can treat all three routers uniformly. Pricing here is per-thousand-characters (vs per-million-tokens for LLMs or per-audio- second for STT). Consent gate: consent_us_tts — separate from both consent_us_llms and consent_us_stt so customers can opt-in independently per modality.
tts/cost
1 feature- tts.cost.tracker-traitcrates/tts/src/cost.rs:1Mirror of openalice-llm-router::cost + openalice-stt::cost. CostEvent shape differs in the metric (characters + audio_seconds_generated vs tokens or audio_seconds) so consumers can route all three into the same atlas / openalice-tenants accounting pipeline by discriminating on `kind`. Default impl logs at INFO; no wiring needed for smoke tests.
tts/crate
1 feature- tts.crate.model-agnostic-routercrates/tts/src/lib.rs:1Model-agnostic TTS provider router for OpenAlice. One TTSProvider trait, multiple adapters (ElevenLabs V3 today; additional providers planned). Per-tenant policy (sovereignty, GDPR Art 49 consent, regulated-sector block, allow-list), and a cost tracker that attributes character usage per provider × per tenant. ElevenLabs V3 audio tags (EmotionHint enum → [bracketed] tags) supported at the adapter layer. Sister crate to openalice-llm-router + openalice-stt.
tts/error
1 feature- tts.error.tts-errorcrates/tts/src/error.rs:1Typed error enum for every fallible TTS operation. Same shape as openalice-llm-router::LLMError and openalice-stt::STTError so consumers can normalise across all three routers.
tts/provider
1 feature- tts.provider.traitcrates/tts/src/provider.rs:1Single async trait every TTS adapter implements: synth_blob (single-shot) + synth_stream (streaming chunks). Wire-shape types (AudioFormat, EmotionHint, SynthRequest, SynthResult, SynthChunk) are provider-neutral; adapters translate to/from upstream wire formats. EmotionHint is translated by ElevenLabs V3 adapter into inline [bracketed] audio tags.
tts/router
1 feature- tts.router.dispatchcrates/tts/src/router.rs:1TTS router — mirrors openalice-llm-router::Router and openalice-stt::Router. Same policy gates: sovereignty (EU/US/ Aggregator), GDPR Art 49 consent for US providers (consent_us_tts — separate gate from consent_us_llms / consent_us_stt so customers can opt-in per modality independently), regulated-sector US block, allowed_providers allow-list.
ui
1 feature- ui.flagpackages/ui/src/Flag.tsx:2Inline-SVG mini flags for the locale switchers (en/de/fr/es/it/pt). Emoji flags render as tofu on Linux/Windows — these are crisp 20x14 rounded-rect SVGs, identical across landing and dashboard (this file is the ONE canonical source; consumers vendor it).since 0.1.0
ui/api-hooks
11 features- ui.api-hooks.errorspackages/ui/src/api-hooks/errors.ts:2Shared error message helpers for API hooks — extracts user-facing message from ApiRequestError, generic Error, or unknown, and strips bearer tokens / auth credentials before display.function:errorMessagefunction:sanitizeErrorsince 0.5.0
- ui.api-hooks.useAsyncStatepackages/ui/src/api-hooks/useAsyncState.ts:19Tiny generic helper that owns {data, loading, error} for an async fetch — collapses three useStates into one.hook:useAsyncStatesince 0.5.0
- ui.api-hooks.useAuthGuardpackages/ui/src/api-hooks/useAuthGuard.ts:44Verify the current session against /auth/me on mount. On 401 or missing token, clear auth + redirect to /login. Returns the authenticated user once verified.hook:useAuthGuardsince 0.5.0
- ui.api-hooks.useBillingpackages/ui/src/api-hooks/hooks.ts:162Fetch billing status + plans, expose changePlan against the active OpenAliceClient.hook:useBillingsince 0.5.0
- ui.api-hooks.useKeyspackages/ui/src/api-hooks/hooks.ts:90List/create/delete API keys against the active OpenAliceClient. Exposes newKey + clearNewKey for one-shot reveal.hook:useKeyssince 0.5.0
- ui.api-hooks.useLoginActionpackages/ui/src/api-hooks/actions.ts:33Returns an async (email, password) → AuthActionResult that drives AuthProvider.login + sets the SSO sessionStorage flag. Translates RateLimitError into a UI-friendly message with retryAfter seconds.hook:useLoginActionsince 0.5.0
- ui.api-hooks.useLogoutActionpackages/ui/src/api-hooks/actions.ts:107Returns an async () → void that drives AuthProvider.logout, then clears the SSO sessionStorage flag. Errors during logout are swallowed — local state is cleared regardless.hook:useLogoutActionsince 0.5.0
- ui.api-hooks.useMepackages/ui/src/api-hooks/hooks.ts:55Fetch the currently authenticated PublicUser from the active OpenAliceClient and expose loading/error/refetch.hook:useMesince 0.5.0
- ui.api-hooks.useRegisterActionpackages/ui/src/api-hooks/actions.ts:67Returns an async (name, email, password, _confirm?) → AuthActionResult that drives AuthProvider.register + sets the SSO sessionStorage flag. Translates RateLimitError into a UI-friendly message.hook:useRegisterActionsince 0.5.0
- ui.api-hooks.useUsagepackages/ui/src/api-hooks/hooks.ts:232Fetch usage rollup for the last N days from the active OpenAliceClient.hook:useUsagesince 0.5.0
- ui.api-hooks.useUserpackages/ui/src/api-hooks/hooks.ts:20Legacy adapter — fetch the current User (legacy shape) from the active OpenAliceClient. Prefer useMe() for new code.hook:useUsersince 0.5.0
ui/auth
3 features- ui.auth.gatepackages/ui/src/auth/AuthGate.tsx:25Client-side route guard — renders skeleton until auth resolves, redirects to /login if unauthenticated.component:AuthGatesince 0.4.0
- ui.auth.providerpackages/ui/src/auth/AuthProvider.tsx:79Shared React context for cross-domain SSO — JWT in localStorage, HttpOnly cookie probe, cross-tab sync.component:AuthProviderhook:useAuthcomponent:AuthGatesince 0.4.0
- ui.auth.realtime-wspackages/ui/src/auth/useAuthWs.ts:7Hook that opens a WebSocket to /auth/ws while the user is signed in, dispatches each incoming event envelope to a handler, and reconnects with exponential backoff on disconnect.hook:useAuthWssince 0.5.0
ui/composite
27 features- ui.composite.action-cardpackages/ui/src/composites/ActionCard.tsx:28Icon-tile action/navigation card — tone-lit tile, hover-nudge arrow, accent-warming hairline; dedups the hand-rolled overview patterns.component:ActionCardsince 0.24.0
- ui.composite.auth-shellpackages/ui/src/composites/AuthShell.tsx:28Centered auth-page layout — atmospheric grain + halo, brand mark, card, footer.component:AuthShellsince 0.6.0
- ui.composite.auth-splitpackages/ui/src/composites/AuthSplit.tsx:32The unified split-screen auth layout (embed-login etalon) — brand pane + form pane, one look for every product.component:AuthSplitsince 0.24.0
- ui.composite.brand-markpackages/ui/src/composites/BrandMark.tsx:17OpenAlice wordmark / monogram — SVG, crisp at any size.component:BrandMarksince 0.6.0
- ui.composite.closing-ctapackages/ui/src/composites/ClosingCta.tsx:30Centered dark final CTA — sigil, em-italic display heading, sub-copy, an xl primary pill plus quiet link, and a mono risk-pill row.component:ClosingCtasince 0.7.0
- ui.composite.dashboard-gridpackages/ui/src/composites/DashboardGrid.tsx:21Responsive auto-fit grid for stat / card lists.component:DashboardGridsince 0.6.0
- ui.composite.editor-headpackages/ui/src/composites/EditorHead.tsx:13Unified dashboard page heading (eyebrow+JP glyph, rich headline, sub line, actions slot); markup-only, consumer-styled.since 0.12.0
- ui.composite.faq-sectionpackages/ui/src/composites/FaqSection.tsx:40Light editorial FAQ teaser — 2-col heading + native details accordion with rotate-on-open icon.component:FaqSectionsince 0.7.0
- ui.composite.feature-cardspackages/ui/src/composites/FeatureCards.tsx:33Light 2-col head + 3-col left-accent feature card grid with arrow feet.component:FeatureCardssince 0.7.0
- ui.composite.landing-footerpackages/ui/src/composites/LandingFooter.tsx:35Editorial 4-column landing footer — brand + tagline, three link columns, bottom meta bar.component:LandingFootersince 0.7.0
- ui.composite.landing-heropackages/ui/src/composites/LandingHero.tsx:44Editorial 2-column product-landing hero — eyebrow, em-italic display headline, CTA pair, risk pills, a free-form visual stage slot, plus a stats bar and meta sign-off strip.component:LandingHerosince 0.7.0
- ui.composite.landing-navpackages/ui/src/composites/LandingNav.tsx:32Sticky frosted topbar with breadcrumb, inline nav links, primary CTA, and mobile hamburger panel.component:LandingNavsince 0.7.0
- ui.composite.landing-railpackages/ui/src/composites/LandingRail.tsx:37Fixed left product-switcher rail with brand sigil + initial links.component:LandingRailsince 0.7.0
- ui.composite.live-indicatorpackages/ui/src/composites/LiveIndicator.tsx:16Pulsing chip for live/recording state. Plasma cyan or warm red tone.component:LiveIndicatorsince 0.6.0
- ui.composite.page-headerpackages/ui/src/composites/PageHeader.tsx:22Standard page title + description + actions row.component:PageHeadersince 0.6.0
- ui.composite.pricing-sectionpackages/ui/src/composites/PricingSection.tsx:52Editorial pricing block — tier grid, featured tier, billing toggle, lifetime scarcity card.component:PricingSectionsince 0.7.0
- ui.composite.problem-sectionpackages/ui/src/composites/ProblemSection.tsx:34Dark editorial 2-col problem block — numbered pains + pull-quote + visual slot.component:ProblemSectionsince 0.7.0
- ui.composite.radial-gaugepackages/ui/src/composites/RadialGauge.tsx:24SVG arc gauge (value vs max) — gradient stroke, threshold tones (80/95 auto ladder), display-number center; zero chart deps.component:RadialGaugesince 0.24.0
- ui.composite.revealpackages/ui/src/composites/Reveal.tsx:14Viewport-triggered fade+rise entrance with sibling stagger; reduced-motion-safe.since 0.10.0
- ui.composite.section-headerpackages/ui/src/composites/SectionHeader.tsx:13Sub-section title row (h2-weight) inside a page.component:SectionHeadersince 0.6.0
- ui.composite.settings-shellpackages/ui/src/composites/SettingsShell.tsx:29Left-nav + main-content layout for settings/account pages.component:SettingsShellsince 0.6.0
- ui.composite.statpackages/ui/src/composites/Stat.tsx:30Metric/stat tile — unifies the 4 drifted stat-card shapes (split/inline layout, glass/solid/plain surface, tone-colored icon chip) behind one component.component:Statsince 0.24.0
- ui.composite.theme-providerpackages/ui/src/composites/ThemeProvider.tsx:22Sets data-theme on document root, persists to localStorage, optional system mode.component:ThemeProvidersince 0.6.0
- ui.composite.toggle-switchpackages/ui/src/composites/ToggleSwitch.tsx:5Accessible on/off switch (a styled checkbox) shared by the form editors. The markup + the `.switch`/`.switchTrack` CSS were duplicated verbatim in persona.module.css and mode.module.css; this is the single definition. NOTE: distinct from the global `oa-toggle` (a SEGMENTED control). This is a binary on/off switch. Pure presentational (no hooks) — renders inside the already-client editors.
- ui.composite.user-tablepackages/ui/src/composites/UserTable.tsx:32Admin user-list table (User/Status/Last Seen/Joined[/Actions]) — ends the flush-vs-padded, Link-vs-onClick drift between the Recent Users preview and the full User Management table.component:UserTablecomponent:UserRowsince 0.24.0
- ui.composite.value-prop-gridpackages/ui/src/composites/ValuePropGrid.tsx:38Dark editorial value-props section — 2-col head over a 3-col grid of numbered accent cards.component:ValuePropGridsince 0.7.0
- ui.composite.widget-cardpackages/ui/src/composites/WidgetCard.tsx:12Unified widget summary card (grid/row), pure props, glass skin via widget-card.css, pluggable link component.since 0.12.0
ui/hook
1 feature- ui.hook.use-spotlightpackages/ui/src/hooks/useSpotlight.ts:11Cursor-tracking CSS vars for the .oa-spotlight lit-edge utility.since 0.8.0
ui/primitive
31 features- ui.primitive.avatarpackages/ui/src/primitives/Avatar.tsx:14Circular avatar with image fallback to initials and optional ring.component:Avatarsince 0.1.0
- ui.primitive.badgepackages/ui/src/primitives/Badge.tsx:21Chip / tag — 2px radius, hairline ring, semantic + brand variants.component:Badgesince 0.1.0
- ui.primitive.bannerpackages/ui/src/primitives/Banner.tsx:28Full-width persistent message with left-edge accent and optional dismiss.component:Bannersince 0.6.0
- ui.primitive.breadcrumbspackages/ui/src/primitives/Breadcrumbs.tsx:14Chevron-separated path navigation. Last item is current page.component:Breadcrumbssince 0.6.0
- ui.primitive.buttonpackages/ui/src/primitives/Button.tsx:28Tactile button primitive — five variants, three sizes, loading state, spring press.component:Buttonsince 0.1.0
- ui.primitive.cardpackages/ui/src/primitives/Card.tsx:17Surface card with hairline border. Optional hover-lift via `interactive`.component:Cardsince 0.1.0
- ui.primitive.checkboxpackages/ui/src/primitives/Checkbox.tsx:17Square checkbox with smooth check animation, native semantics, validators.component:Checkboxsince 0.6.0
- ui.primitive.comboboxpackages/ui/src/primitives/Combobox.tsx:18Searchable typeahead select. Custom filter, keyboard nav, validators.component:Comboboxsince 0.6.0
- ui.primitive.copy-buttonpackages/ui/src/primitives/CopyButton.tsx:17Clipboard copy with success-state animation.component:CopyButtonsince 0.6.0
- ui.primitive.dialogpackages/ui/src/primitives/Dialog.tsx:31Compositional dialog API on top of Modal — header, title, description, footer slots.component:Dialogsince 0.4.0
- ui.primitive.dropdownpackages/ui/src/primitives/Dropdown.tsx:26Compound dropdown menu — trigger / content / item / separator / label, click-outside + ESC close.component:Dropdownsince 0.1.0
- ui.primitive.empty-statepackages/ui/src/primitives/EmptyState.tsx:21Centered empty-state panel — icon/illustration slot, title, body, optional CTA.component:EmptyStatesince 0.6.0
- ui.primitive.form-fieldpackages/ui/src/primitives/FormField.tsx:32Composes Label + control + helper/error + optional validators. Universal form row.component:FormFieldsince 0.6.0
- ui.primitive.info-tilepackages/ui/src/primitives/InfoTile.tsx:26Bordered surface-2 tile — generic settings/detail row container, ends the p-4-vs-px-3-py-2 padding drift.component:InfoTilesince 0.24.0
- ui.primitive.inputpackages/ui/src/primitives/Input.tsx:20Hairline text input — supports validators, helper text, icon slots, monospace mode.component:Inputsince 0.1.0
- ui.primitive.kbd-keypackages/ui/src/primitives/KbdKey.tsx:21Keyboard-shortcut chip. Multi-key combos via `combo` (string) or `keys` (array).component:KbdKeysince 0.6.0
- ui.primitive.labelpackages/ui/src/primitives/Label.tsx:12Semantic form label with required-star slot.component:Labelsince 0.6.0
- ui.primitive.modalpackages/ui/src/primitives/Modal.tsx:20Centered dialog primitive — focus trap, ESC, scroll-lock, glass backdrop with grain.component:Modalsince 0.1.0
- ui.primitive.paginationpackages/ui/src/primitives/Pagination.tsx:14Page-number navigator with prev/next + ellipsis.component:Paginationsince 0.6.0
- ui.primitive.progresspackages/ui/src/primitives/Progress.tsx:13Bar progress — determinate (0-100) or indeterminate sliding bar.component:Progresssince 0.6.0
- ui.primitive.radio-grouppackages/ui/src/primitives/RadioGroup.tsx:20Radio button group with native semantics + Atelier styling + validators.component:RadioGroupsince 0.6.0
- ui.primitive.scroll-areapackages/ui/src/primitives/ScrollArea.tsx:13Scrollable container with Atelier scrollbar styling.component:ScrollAreasince 0.1.0
- ui.primitive.selectpackages/ui/src/primitives/Select.tsx:18Headless select with keyboard nav, click-outside close, and validators.component:Selectsince 0.6.0
- ui.primitive.separatorpackages/ui/src/primitives/Separator.tsx:13Hairline horizontal/vertical divider with optional decorative gradient variant.component:Separatorsince 0.1.0
- ui.primitive.skeletonpackages/ui/src/primitives/Skeleton.tsx:14Loading placeholder with warm shimmer.component:Skeletonsince 0.1.0
- ui.primitive.sliderpackages/ui/src/primitives/Slider.tsx:14Range slider — native input semantics, brand-500 fill, focus ring.component:Slidersince 0.6.0
- ui.primitive.spinnerpackages/ui/src/primitives/Spinner.tsx:14Indeterminate circular loader. Matches button sizes for inline use.component:Spinnersince 0.6.0
- ui.primitive.switchpackages/ui/src/primitives/Switch.tsx:16Pill toggle. Solar amber active state, hairline ring, native checkbox semantics.component:Switchsince 0.1.0
- ui.primitive.tabspackages/ui/src/primitives/Tabs.tsx:27Hairline-underline tabs with keyboard arrow nav.component:Tabssince 0.6.0
- ui.primitive.textareapackages/ui/src/primitives/Textarea.tsx:13Multi-line text input with auto-resize, char count, and validators.component:Textareasince 0.1.0
- ui.primitive.tooltippackages/ui/src/primitives/Tooltip.tsx:14Hover hint with 4-position support, configurable delay, hairline border.component:Tooltipsince 0.1.0
ui/shell
2 features- ui.command-palettepackages/ui/src/composites/CommandPalette.tsx:7Cmd+K / Ctrl+K command palette — modal overlay with fuzzy filter over a flat list of commands. Mounted by EcosystemShell so every product gets the same shortcut surface.component:CommandPalettesince 0.5.0
- ui.ecosystem.shellpackages/ui/src/composites/EcosystemShell.tsx:13Persistent cross-product chrome — left sidebar with product switcher, top bar with account chip. Wraps each product's layout so the 5 products feel like one app.component:EcosystemShellsince 0.4.0
voice-client/codec
2 features- voice-client.opus-bitrate-capincoming/voice-client/src/codec.ts:54Caps the Opus encoder at a configurable maxBitrate (default 32 kbps) via setParameters so voice rooms stay under the SFU's per-stream bandwidth budget.since 0.1.0
- voice-client.opus-codec-pinincoming/voice-client/src/codec.ts:16Calls setCodecPreferences on every audio transceiver to force Opus over G.711 fallbacks on older Safari and Android WebRTC stacks; no-ops cleanly where the API is missing.since 0.1.0
voice-client/config
1 feature- voice-client.defaultsincoming/voice-client/src/defaults.ts:7Compile-time defaults for the VoiceClient — SFU WebSocket URL and audio capture constraints. Kept in one place so call sites and tests don't drift.since 0.1.0
voice-client/controls
3 features- voice-client.deafen-controlincoming/voice-client/src/react.ts:349Drops every remote audio element's volume to zero, force-mutes the local mic, and restores the prior mute state on undeafen so muting myself before deafen survives the round-trip.since 0.1.0
- voice-client.mute-toggleincoming/voice-client/src/client.ts:260Flips the local audio track's enabled bit and echoes a "mute" signal to the SFU so every other participant's UI sees the state change.since 0.1.0
- voice-client.push-to-talkincoming/voice-client/src/react.ts:382Unmutes the mic while the configured PTT key (default V) is held with pointer locked, then re-mutes on key-up or lock-exit so World's spatial-audio rooms can run mic-always-off-except-while-holding.since 0.1.0
voice-client/lifecycle
2 features- voice-client.connect-lifecycleincoming/voice-client/src/client.ts:137Acquires the microphone (full-duplex), opens the SFU WebSocket with optional ?token=, and drives the state machine through idle → connecting → live, surfacing NotAllowedError as a clean "mic denied" callback.since 0.1.0
- voice-client.disconnect-teardownincoming/voice-client/src/client.ts:206Sends a "leave" frame, closes the WebSocket + RTCPeerConnection, stops every local track (mic, screen, webcam), and resets participant state so the instance is reusable for a fresh connect().since 0.1.0
voice-client/media
2 features- voice-client.screen-shareincoming/voice-client/src/client.ts:279Calls getDisplayMedia(), adds the resulting video track to the peer connection, signals "screen_share_start", and auto-removes the track when the user clicks the browser's stop-sharing chrome.since 0.1.0
- voice-client.webcam-publishincoming/voice-client/src/client.ts:339Requests a low-bandwidth 320x320@15fps camera track via getUserMedia, attaches it to the peer connection, and surfaces NotAllowedError as a clean "camera denied" callback.since 0.1.0
voice-client/networking
1 feature- voice-client.ice-server-defaultsincoming/voice-client/src/ice.ts:16Builds an RTCIceServer[] from Google STUN plus NEXT_PUBLIC_TURN_* env vars, warning once when TURN is absent so callers know mobile/corporate NAT will break.since 0.1.0
voice/client
3 features- voice-client.coreincoming/voice-client/src/client.ts:13Shared protocol-level WebRTC client for the openalice-voice SFU. Powers Live (recvonly), World (full duplex), and future Social/Persona consumers.class:VoiceClientsince 0.1.0
- voice-client.packageincoming/voice-client/src/index.ts:14Shared WebRTC voice client package — kills duplication between Live's listener and World's full-duplex voice chat. Future home of Social DM calls and Persona voice embed.package:@openalicelabs/voice-clientsince 0.1.0
- voice-client.reactincoming/voice-client/src/react.ts:17useVoiceClient — React hook wrapping VoiceClient with audio elements, VAD, deafen, PTT.hook:useVoiceClientsince 0.1.0