features · ledger

The ledger

Every @feature annotation across the org, in one searchable index. Filter by repo, category, or maturity; each row resolves to the exact source line it was declared on.

⬡ status coverage63%1,968 / 3,109 features annotated1,141 missing status:

worst offenders

  • 11%openalice-embed601 missing
  • 66%openalice-platform144 missing
  • 78%openalice-persona66 missing
  • 92%openalice60 missing
  • 31%openalice-world36 missing
  • 23%openalice-social34 missing
  • 0%openalice-lab25 missing
  • 0%openalice-inspector23 missing
0 matched0 in ledger
  • embed-app.lib.account-analytics
    openalice-embed · embed-app/lib
    Server-only aggregator: merges per-widget ConversationAnalytics from agent-lite into a single account-level summary. Used by the home, analytics, and insights pages so each calls one function instead of fan-out per widget. Errors from agent-lite (AgentLiteNotConfiguredError / AgentLiteApiError) are caught per-widget and treated as zeros so a single misconfigured widget never breaks the whole dashboard.
  • embed-app.lib.action-error
    openalice-embed · embed-app/lib
    Single safe, locale-aware error mapper for all server actions. Keeps internal service names, raw status codes, and backend message bodies OUT of the browser. Rules: - TenantsNotConfiguredError: returns the "notConfigured" i18n key message (safe config-state hint — no backend data in it). - TenantsApiError: LOG the raw detail server-side only; return a status- keyed message from the `errors` i18n namespace. - Anything else: log + return generic. Call sites already call `describeError(e)` — update them to `await describeError(e)` (the function is now async).
  • embed-app.lib.action-locale
    openalice-embed · embed-app/lib
    Locale resolution helpers for server actions. Server actions run outside the Next.js rendering pipeline, so `getTranslations()` cannot use the automatically-resolved request locale — we must read the `NEXT_LOCALE` cookie ourselves and pass it explicitly. `actionLocale`: reads the NEXT_LOCALE cookie, validates it against the 6 supported locales, and defaults to "en". Safe to call from any `"use server"` action. `actionT`: shorthand that calls getTranslations with the caller's locale + the requested namespace. Returns the same strongly-typed translator that `getTranslations` returns from pages/layouts.
  • embed-app.lib.agent-base
    openalice-embed · embed-app/lib
    The public agent-lite base URL used by browser-side calls to the embed API (e.g. the avatar catalog at `${agentBase()}/v1/embed/avatars`). Reads NEXT_PUBLIC_AGENT_BASE when set, else the dev default, with any trailing slash trimmed. Client-safe (no `server-only`) — it only touches a NEXT_PUBLIC_* var and is imported by "use client" components. Previously copy-pasted in AppearanceEditor and the avatar StudioClient (the latter's comment literally read "mirrors AppearanceEditor.agentBase"). This is the single definition.
  • embed-app.lib.agent-lite-client
    openalice-embed · embed-app/lib
    Tests for agentLite.ts — the server-side analytics/conversations client. Strategy: all public functions are async and call `fetch`. We stub `fetch` via vi.stubGlobal and set INTERNAL_SECRET / EMBED_RUNTIME_INTERNAL_URL via vi.stubEnv so the full code path (including the private normalizers) is exercised without a live agent-lite service. Covered: AgentLiteNotConfiguredError — thrown when INTERNAL_SECRET is absent AgentLiteApiError — thrown on non-2xx responses normalize() — defensive field coercion (via getConversationAnalytics) normalizeConversationList() — sentiment/intent/score guard (via listConversations) normalizeTopConversation() — null-on-missing-id / rating-range guard normalizeSentimentInsights()— breakdown defaults-to-zero (via getSentimentInsights) normalizeRevenueInsights() — money helper floor-0 (via getRevenueInsights)
  • embed-app.lib.agent-lite-client
    openalice-embed · embed-app/lib
    Server-side typed client for the openalice-agent-lite /internal analytics surface. Runs server-side only (route handlers / server components) so the shared INTERNAL_SECRET never reaches the browser. The `server-only` import enforces that at build time. Endpoint (openalice-agent-lite): GET /internal/analytics/summary?widget_id=<public_id>&days=<N> → ConversationAnalytics (see shape below) Auth: header `x-internal-secret: <INTERNAL_SECRET>` — the same shared broadcast secret agent-lite + tenants validate (compose wires it from INTERNAL_BROADCAST_SECRET). The dashboard reaches agent-lite server-side over the docker network via EMBED_RUNTIME_INTERNAL_URL (default http://openalice-agent-lite-dev:3050).
  • embed-app.lib.agent-lite-client
    openalice-embed · embed-app/lib
    Tests for the live-takeover additions to agentLite.ts (EMBED_LIVE_TAKEOVER_V1): takeoverConversation() — POST /v1/conversations/{id}/takeover {operator_name} with the tenant bearer. sendOperatorMessage() — POST /v1/conversations/{id}/operator-message {text} with the tenant bearer. releaseConversation() — POST /v1/conversations/{id}/release {} with the tenant bearer. normalizeConversationList() — control_mode/operator_name defensive coercion (via listConversations). normalizeTranscript() — control_mode/operator_name + per-message operator name (top-level or meta.name) coercion (via getConversationTranscript). Strategy mirrors agentLite.test.ts: stub global fetch + env vars so the full request/normalize path runs without a live agent-lite.
  • embed-app.lib.agent-lite-client
    openalice-embed · embed-app/lib
    Tests for the inbox-depth additions to agentLite.ts (#773 wave 2): searchConversations() — GET /internal/conversations with the ?q= + ?tag= filters (visitor/id substring + exact tag). addConversationTag() — POST /v1/conversations/{id}/tags {tag} (bearer). removeConversationTag() — DELETE /v1/conversations/{id}/tags/{tag} (bearer). addConversationNote() — POST /v1/conversations/{id}/notes {text} (bearer). normalizeConversationList() — tags[] defensive coercion (via search/list). normalizeTranscript() — tags[] + notes[] coercion (body|text key, via getConversationTranscript). normalizeConversationTag() (client-safe validator in widgetSchema) is also unit-tested here so the client + agent-lite Rust validator stay aligned. Strategy mirrors agentLiteTakeover.test.ts: stub global fetch + env vars so the full request/normalize path runs without a live agent-lite.
  • embed-app.lib.api-error
    openalice-embed · embed-app/lib
    Single helper for reading a backend error body, regardless of shape. Every OpenAlice Rust service (auth / tenants / agent-lite) now wraps every 4xx/5xx in the uniform, lossless error envelope: { "error": { "code": "<machine_code>", "message": "<human message>", ... } } This helper extracts the human message across that envelope AND the legacy `{"error":"msg"}` / `{"message":"msg"}` shapes, so the lib `request` helpers and proxy routes stay correct across the rollout. Returns `fallback` (usually the raw response text) when no message field is present.
  • embed-app.lib.auth-client
    openalice-embed · embed-app/lib
    Server-side auth client — THE PLATFORM CHAIN (runbook #17, Law 6: no auth proxies). Flows go DIRECTLY to openalice-auth and come back with an ORG-SCOPED platform JWT for the session cookie: GET {auth}/me/orgs → first org (created when none) POST {auth}/orgs/{id}/token → ORG JWT {org{org_id,role,perms}} POST {auth}/orgs → the customer org (creator=owner) POST {auth}/orgs/{id}/token → org JWT POST {tenants}/me/workspace/ensure → idempotent Embed workspace seed The org token is tid-compatible everywhere (kernel rule: tenant_id() = tid → org.org_id → user_id), so tenants' /me/* surface accepts it as-is — proven e2e 2026-07-05 (ensure 201 → idempotent 200 → /me/widgets 200). Exported names and result shapes are UNCHANGED from the proxy era, so the login/signup/reset server actions did not move. Runs server-side only; the customer's password never reaches the browser bundle. Error policy resistance); reset paths never log request bodies.
  • embed-app.lib.balance-formatters
    openalice-embed · embed-app/lib
    Pure formatting helpers for the prepaid balance card. No server-only imports — safe to import in tests and server components alike. Exported helpers: formatEur(cents) — integer cents → "€X.XX" string formatTxAmount(cents, kind) — signed display string for a transaction row balanceStatusKind(cents, overCap) — "over"|"empty"|"low"|"normal" formatTxDate(iso) — ISO timestamp → "Jun 19" short label txLabel(kind, reference) — human kind label (top-up, conversation, voice …)
  • embed-app.lib.balance-formatters.test
    openalice-embed · embed-app/lib
    Unit tests for balanceFormatters.ts — pure formatting helpers used by the prepaid balance card on /settings/billing. formatEur: - 0 cents → "€0.00" - 2000 cents (€20 top-up) → "€20.00" - 3 cents (€0.03 conversation) → "€0.03" - 45 cents (€0.45 low-balance case) → "€0.45" - 1200 cents (€12.00 voice) → "€12.00" formatTxAmount: - top-up credit → "+€20.00" - conversation debit → "−€0.03" (uses U+2212 MINUS SIGN) - voice debit → "−€0.12" - refund credit → "+€5.00" - unknown debit kind → "−€1.00" isCredit: - "top-up" → true - "topup" → true - "refund" → true - "credit" → true - "conversation" → false - "voice" → false - "unknown" → false balanceStatusKind: - overCap=true → "over" regardless of balance - 0 cents, overCap=false → "empty" - negative cents → "empty" - 45 cents (< 100 threshold) → "low" - 99 cents → "low" - 100 cents → "normal" - 10000 cents → "normal" - custom threshold: 500 cents, threshold=1000 → "low" txLabel: - "top-up" / null → "top-up" - "topup" / null → "top-up" - "conversation" / null → "conversation" - "voice" / null → "voice minute" - "refund" / null → "refund" - "credit" / "promo" → "promo" - "credit" / null → "credit" - unknown / "INV-123" → "INV-123" - unknown / null → raw kind formatTxDate: - Valid ISO → short month-day string (e.g. "19 Jun" or "Jun 19" depending on locale) - Invalid string → "" - Empty string → ""
  • embed-app.lib.billing-client
    openalice-embed · embed-app/lib
    Server-side typed client for the openalice-tenants /me/billing surface. Runs server-side only (server components / server actions) so the tenant bearer token never reaches the browser — `server-only` enforces it. Routes (openalice-tenants/src/handlers/me/{billing.rs,topup.rs}): GET /me/billing — BillingState (plan + usage + catalog + billing_enabled feature flag) POST /me/billing/checkout — { plan } → { checkout_url, plan } POST /me/billing/topup — { amount_cents, idempotency_key? } → { checkout_url, amount_cents } (one-off prepaid top-up; handler: me/topup.rs) Billing is FEATURE-FLAGGED on the tenants side (MOLLIE_API_KEY). When unset, GET still returns Free defaults with billing_enabled=false and checkout responds 503 — the dashboard renders "billing coming soon" instead of crashing. Reachability: same as the widgets client — server-side over the docker network via EMBED_ACCOUNTS_INTERNAL_URL (default http://openalice-tenants-dev:3990).
  • embed-app.lib.billing-rates
    openalice-embed · embed-app/lib
    Prepaid PAYG unit rates (euros-per-unit) mirrored from the openalice-tenants billing defaults. Client-safe — pure constants, no `server-only` import — so both the server billing page and any "use client" component render the SAME numbers the backend charges, with no drift. The tenants service (openalice-tenants) remains the authoritative source of truth; these are the documented dashboard defaults. If tenants' defaults change, update them here in lockstep (and the billing.balance.ratesLine ICU string already interpolates these via {convRate}/{voiceRate}, so the copy never hardcodes a number again). CAVEAT (review #5 N1): the backend rates are env-overridable via PREPAID_RATE_CONVERSATION_CENTS / PREPAID_RATE_VOICE_MINUTE_CENTS (tenants billing.rs). If an operator ever sets those, the backend charges the override while this display still shows the defaults — do NOT override in env without updating here, or better: ship the follow-up that exposes the EFFECTIVE rates through the billing API so the UI stops being a second source of truth.
  • embed-app.lib.dev-token
    openalice-embed · embed-app/lib
    Tenant JWT resolution for the embed dashboard data layer. Resolution order (getDashboardToken): 0. REAL session — the HttpOnly `oa_dash_session` cookie set by the login/signup server actions (lib/session.ts). This is the production 1. EMBED_DASHBOARD_DEV_TOKEN — a pre-minted bearer, if supplied (dev). 2. Mint one at runtime from SHARED_JWT_SECRET + EMBED_DEV_TENANT_ID, matching openalice-tenants' SignupClaims shape exactly: { sub, tid, iss:"openalice-auth", role:"user", plan, iat, exp } signed HS256 (dev). Same secret the tenants container uses, so the token validates server-side. The dev sources (1 + 2) are gated TWICE: behind their own env vars AND behind `OPENALICE_ENV === "dev"` (see getDevToken). In prod/staging the dev path is hard-disabled even if a stray dev-token var is present — only the real session cookie authenticates. They exist purely so the dashboard works without a login during local dev.
  • embed-app.lib.experiments-client
    openalice-embed · embed-app/lib
    Server-side typed client for the openalice-tenants /me/experiments CRUD surface (A/B experiment engine, migration 0019_experiments.sql). All calls run server-side (route handlers / server actions / server components) so the tenant bearer token is never shipped to the browser. The `server-only` import enforces this at build time. Routes (openalice-tenants/src/handlers/me/experiments.rs): GET /me/experiments?widget_id=<public_id> — list (ExperimentSummary[]) POST /me/experiments — create (ExperimentFull) GET /me/experiments/{exp_id} — get (ExperimentFull) PATCH /me/experiments/{exp_id} — patch name/status (ExperimentFull) DELETE /me/experiments/{exp_id} — 204 (cascades variants) GET /me/experiments/{exp_id}/variants — list (VariantFull[]) POST /me/experiments/{exp_id}/variants — create (VariantFull) PATCH /me/experiments/{exp_id}/variants/{var_id} — patch (VariantFull) DELETE /me/experiments/{exp_id}/variants/{var_id} — 204 Auth: HS256 bearer JWT (same `getDashboardToken()` as the widgets client). Reachability: server-side over the docker network via EMBED_ACCOUNTS_INTERNAL_URL. The config_overlay of a variant is an allowlist-only JSONB object — the safe fields are exported here (SAFE_OVERLAY_FIELDS) so the New-Experiment form UI can offer exactly the same field set the tenants write-validator and the agent-lite read-guard enforce. NEVER add provider/consent/tools/ domain/retention/escalation/memory keys here — those are security-sensitive and must not be overridable by an experiment.
  • embed-app.lib.experiments-insights-client
    openalice-embed · embed-app/lib
    Server-side typed client for openalice-agent-lite's internal A/B experiment insights surface. Runs server-side only (route handlers / server components) so the shared INTERNAL_SECRET never reaches the browser. The `server-only` import enforces that at build time. Endpoint (openalice-agent-lite): GET /internal/insights/experiments?widget_id=<public_id>&experiment_id=<uuid>&days=<N> → VariantMetrics[] (one row per variant that has accrued data) Auth: header `x-internal-secret: <INTERNAL_SECRET>` — the same shared secret agent-lite + tenants validate (mirrors agentLite.ts exactly). The dashboard reaches agent-lite server-side over the docker network via EMBED_RUNTIME_INTERNAL_URL (default http://openalice-agent-lite-dev:3050). v1 honesty contract: `collecting_data` is true while a variant has fewer than 30 conversations. The dashboard shows a "Collecting data" pill and withholds any winner indicator until every variant clears that threshold AND the experiment is concluded. NO p-values are computed in v1.
  • embed-app.lib.experiments-shared
    openalice-embed · embed-app/lib
    Client-SAFE constants + derived types for the A/B experiment engine. Deliberately has NO `server-only` import, so client components (the ABTestsView New-Experiment builder) can import SAFE_OVERLAY_FIELDS / EXPERIMENT_STATUSES as runtime values WITHOUT dragging the server-only tenants data client (./experiments) into the browser bundle. The server-side data client (./experiments) re-exports both constants so existing server callers keep importing them from "@/lib/experiments" unchanged. Keep SAFE_OVERLAY_FIELDS in sync with the tenants write-validator (src/handlers/me/experiments.rs) and the agent-lite read-guard (src/ab_overlay.rs) — all three must agree (unknown key → 400).
  • embed-app.lib.experiments-shared
    openalice-embed · embed-app/lib
    Tests for experimentsShared.ts — the client-safe A/B experiment constants. Regression-guards the overlay field allowlist (must stay in sync with the Rust validator in openalice-tenants) and the status enum values.
  • embed-app.lib.field-limits
    openalice-embed · embed-app/lib
    Single source of truth for dashboard form field length/range limits, mirroring the openalice-tenants validators (src/handlers/me/{widgets.rs, tenant.rs,account.rs} + src/handlers/widgets.rs). The shared Field primitive (components/Field.tsx) and every editor read these so the client-side counter/validation agrees byte-for-byte with what the backend will accept — no drift, no surprise 400s. Where widgetSchema.ts already exports a canonical constant we re-use it (don't fork the number); the few caps that only live in the Rust validators (name, display_name, email, url) are declared here. Client-safe: pure constants, no server-only imports — both server modules and "use client" components may import this.
  • embed-app.lib.model-catalog
    openalice-embed · embed-app/lib
    Client-SAFE schema + helpers for the per-widget AI-model selector. This module is imported by BOTH the server-only API client (models.ts) and the browser ModelSelector component, so it carries NO `server-only` import and never touches the tenant bearer token. The catalog itself is served by openalice-tenants: GET /me/models → { models: ModelCatalogEntry[], plan: string } The tenants server is the REAL gate: PATCH /me/widgets/{id} re-validates the chosen llm_model against the catalog, the tenant's plan, and the widget's eu_only_mode (rejects 400 if not allowed). These helpers only decide what the owner is OFFERED (region filter) or shown LOCKED (tier gating).
  • embed-app.lib.model-catalog.test
    openalice-embed · embed-app/lib
    Unit tests for the client-safe model-catalog helpers that drive the per-widget AI-model selector: the eu_only_mode REGION filter and the plan TIER gating (free < starter < pro < business).
  • embed-app.lib.models-client
    openalice-embed · embed-app/lib
    Server-side typed client for the openalice-tenants /me/models catalog surface. The catalog lists the AI models this tenant may offer per-widget, tagged by region (eu/us), min_tier, vision, and a blurb — plus the tenant's current plan id. The ModelSelector uses it to OFFER allowed models (region filter) and show higher-tier models LOCKED (tier gating); the tenants server re-validates on PATCH /me/widgets/{id} and is the real gate. Route (openalice-tenants): GET /me/models → { models: ModelCatalogEntry[], plan: string } Auth + reachability mirror tenants.ts / voices.ts exactly: HS256 bearer JWT via getDashboardToken(), EMBED_ACCOUNTS_INTERNAL_URL over the docker network. The `server-only` import keeps the tenant bearer token out of the browser bundle. Client-safe types + helpers live in modelCatalog.ts and are re-exported here for convenience.
  • embed-app.lib.page-meta
    openalice-embed · embed-app/lib
    Server-side helper that builds a per-page browser-tab title of the form `{Page label} · OpenAlice Embed`. Every dashboard page exports `generateMetadata` via {@link pageTitle} so each tab has a distinct, locale-correct title instead of the layout's flat default. The label comes from an EXISTING i18n key (a crumb / nav / eyebrow) wherever one exists; pages without a concise reusable label use the dedicated `meta` namespace. Server-only — `getTranslations` runs in the server module that owns `generateMetadata`.
  • embed-app.lib.platform-admin
    openalice-embed · embed-app/lib
    Platform-admin gate for OPS tools that touch GLOBAL, cross-tenant assets (e.g. the VRM thumbnail studio, which generates previews for the shared avatar catalog every tenant sees). Regular customers must NOT reach these — they only consume the global catalog, never mutate it. Admin identity = the session token's `tid` claim is in the allowlist `EMBED_ADMIN_TENANT_IDS` (comma-separated). When that env is unset we fall back to `EMBED_DEV_TENANT_ID` so the single-tenant dev box (NAO) is admin out of the box — on the DEV BOX ONLY (`OPENALICE_ENV === "dev"`). Real production with the allowlist unset throws (fail closed) instead of falling back. NB: `NODE_ENV` is "production" even on the dev box (the container runs `next build`), so it can't distinguish dev from prod — `OPENALICE_ENV` is the real cluster marker (same gate as middleware.ts's dev-token).
  • embed-app.lib.platform-admin
    openalice-embed · embed-app/lib
    Tests for the platform-admin gate. Key behaviors under test: isPlatformAdmin: - tid claim in EMBED_ADMIN_TENANT_IDS → admin - tid claim NOT in the allowlist → not admin - anonymous session (no token) → not admin - allowlist unset in non-production → EMBED_DEV_TENANT_ID dev fallback - allowlist unset in PRODUCTION → throws (fail closed) instead of silently minting the dev tenant as a cross-tenant platform admin NOTE: admin.ts imports `server-only` (stubbed via the vitest alias) and getDashboardToken from @/lib/devToken, which we mock so no cookie store / Next runtime is needed. Env vars are stubbed per-test via vi.stubEnv.
  • embed-app.lib.qr
    openalice-embed · embed-app/lib
    Tiny, self-contained QR-code encoder (client-safe, zero dependencies). WHY vendored instead of an npm dep: the only thing we ever encode is the TOTP `otpauth://` URI during MFA setup. That URI embeds the shared secret, so it MUST NOT be sent to any third-party QR image service — rendering it in the browser from our own origin keeps the secret on the user's machine. Adding a build-time dependency is also out of scope here (the gate forbids `npm install`), so we ship a minimal byte-mode encoder covering the small payloads MFA produces. Scope / limitations (deliberately minimal — this is NOT a general QR lib): - Byte (8-bit) mode only — otpauth URIs are ASCII. - Error-correction level L (lowest) — maximises capacity; the screen is clean and the payload short, so L is ample. - Versions 1..10 (up to 271 bytes at level L) — an otpauth URI with a 32-char base32 secret + issuer + label is ~80-130 bytes, well inside this. We throw if a payload somehow exceeds version 10 rather than silently truncating. The implementation is a compact, well-tested public-domain-style QR matrix generator (Reed-Solomon over GF(256), the standard QR placement + masking). It returns a boolean matrix (true = dark module) the UI renders as an inline SVG — no <canvas>, no network, no secret leaving the page. Boundary note: pure computation, no `server-only` and no browser globals, so it is safe to import from a "use client" component.
  • embed-app.lib.qr.test
    openalice-embed · embed-app/lib
    Structural tests for the vendored QR encoder (src/lib/qr.ts). We don't re-implement a QR decoder here; instead we assert the invariants that prove the matrix is a well-formed QR symbol: - the result is a square boolean matrix with a valid QR module count (4*version + 17), versions 1..10; - the three finder patterns (top-left, top-right, bottom-left) are the canonical 7x7 dark ring around a 3x3 dark core; - a longer payload escalates to a larger version (more modules); - a realistic otpauth:// URI encodes without throwing. Rationale: the only consumer is the MFA setup QR, whose payload is a short ASCII otpauth URI — these invariants catch placement/version regressions that would render an unscannable code.
  • embed-app.lib.safe-url
    openalice-embed · embed-app/lib
    Static SSRF guard for tenant-supplied OUTBOUND URLs (webhook endpoints). Blocks the obvious internal / private / reserved targets at the dashboard layer as defense-in-depth. NOTE: this is NOT a complete SSRF defense. A public hostname can still resolve to an internal IP (DNS rebinding). The AUTHORITATIVE check — resolve the host at DELIVERY time and reject private resolved IPs — must live in the webhook-delivery service (openalice-tenants). This guard stops the trivially obvious cases (localhost, RFC-1918 literals, internal docker service names, cloud-metadata 169.254.x) that the previous `startsWith("https://")` check let straight through.
  • embed-app.lib.safe-url
    openalice-embed · embed-app/lib
    Static SSRF guard for webhook endpoint URLs. Must accept public https FQDNs / public IPs and reject non-https, localhost, RFC-1918 / loopback / link-local IPv4+IPv6 literals, cloud-metadata (169.254.x), reserved suffixes, and single-label internal docker service names.