Skip to content

Security: developerz-ai/ultimate

Security

SECURITY.md

Security Policy

Supported versions

Framework packages move in lockstep, so "supported version" is one number covering all 31 — the 30 @ultimat3/* packages and the unscoped create-ultimate. Re-derive the list with bun run scripts/release-workflow.ts --json.

Version Supported npm dist-tag
the current major — npm view @ultimat3/core version ✅ every fix latest
the previous major, until 6 months after the current major's ## X.0.0 date in CHANGELOG.md ✅ security fixes, as patches cut from release/<major>.x maint-<major> — npm view @ultimat3/core dist-tags
every older major ❌ upgrade — see Upgrading

The window applies from v30 (Stability); before it, only the latest major got fixes. A fix ships as a patch to every package in one release, on the same lockstep rule as any other release; there are no per-package security branches. A fix that cannot be non-breaking may ship a major inside the 90-day window under its advisory id: bun run scripts/release.ts --bump major --security GHSA-xxxx-xxxx-xxxx. Report against the newest major in the table you run.

Reporting a vulnerability

Report privately via GitHub Security Advisories. Do not open a public issue for a vulnerability.

Include: the affected package and version, a reproduction, the impact, and any suggested fix. GitHub delivers a private advisory to this repository's administrators, the developerz-ai organisation (GOVERNANCE.md). The aim is to acknowledge within 3 business days; the fix then ships as a patch on the rule above.

Security posture of the framework

Design decisions that carry security weight, so you know what to audit:

Area Posture
Authz One policy definition enforced across HTTP, live queries, jobs, and MCP. There is deliberately no second authz system. An action without a policy fails at registration.
MCP tool visibility Three outcomes, never blurred. A tool the caller's role may not invoke is omitted from tools/list and answers ToolNotFound — never Forbidden, so there is no enumeration oracle. A tool the connection's token lacks the scope for is refused explicitly (X_MCP_SCOPE_DENIED), because a well-behaved client can fix that. A tool that ran and whose policy denied the input answers X_FORBIDDEN, identical to the HTTP answer. Visibility is fail-closed and input-independent: visibleTo is either a role allowlist — which admits only the roles it names, so a caller with no matching role is refused — or a predicate over the caller alone, which structurally cannot read call arguments, so existence cannot be probed by varying them. Computed per connection, and every outcome is audited, ToolNotFound at warn.
MCP dev server db.query is read-only in four layers: a SELECT-only Postgres role, BEGIN READ ONLY + SET LOCAL statement_timeout, a single-read parse (batches, statement-level write keywords including data-modifying CTEs, locking clauses, EXPLAIN ANALYZE and whole function families — file access, pg_advisory_* locks, set_config, pg_sleep* — are all refused, by prefix of the called function name — quoted and schema-qualified spellings included — so a spelling nobody listed is refused rather than admitted), and caps — limit defaults to 100 rows and clamps to a hard maximum of 1000, plus a 256 KiB byte cap. The role layer is conditional: a managed Postgres that refuses CREATE ROLE/GRANT leaves it out, and the response's guards array names the layers that actually engaged. db.migrate refuses any database that is not a branch DB. The /_x dashboard refuses to mount in production.
Multi-tenancy A tenant-scoped entity queried without an org predicate throws X_TENANCY_UNSCOPED. It is a runtime guard, not a convention.
Cross-tenant references A references() with both ends tenant-scoped is a composite key in the database: a row naming another tenant's parent is refused, on raw SQL too, with the same code as a dangling id so absence and foreignness cannot be told apart. crossTenant: '<reason>' states an exception on the edge.
Secrets Env vars declared secret are redacted in logs, traces, and error output. .env is gitignored; .env.example documents the shape with no values.
Admin Every admin mutation is appended to an audit log with actor, before/after diff, and request id. Destructive actions require re-confirmation.
Preview deploys Any environment but production serves Disallow: / in robots.txt and no sitemap (isIndexable in @ultimat3/seo), Service-worker caches are namespaced by build ID (cacheNamespace in @ultimat3/pwa), so a preview isolates its cache from production only on a different build ID or a different origin.
CSP Locked defaults. The theme inline script ships with its sha256 hash so no unsafe-inline is needed.
Egress in tests Sealed by default, As of 2026-11-03 — sealNetwork() replaces global fetch under bun test, and any request no mockFetch(match, answer) registered throws X_TEST_NETWORK_SEALED naming the URL and the line that allows it; requestLog() reads what was sent. Opting out is an env var (ULTIMATE_TEST_ALLOW_NET=1), never an API, so no test file can quietly unseal the network for itself. A caller that swallowed the refusal used to defeat the seal, and @ultimat3/scraping's /robots.txt read was one; an unreachable robots.txt now refuses the scrape with X_SCRAPE_ROBOTS_DISALLOWED (packages/scraping/src/robots.ts, verdictOf), so under bun test an unmocked read goes red instead of green. Outside bun test an offline fakeBrowser/fixtureBrowser scrape still sends one robots request per origin (wiki/Known-Gaps.md). In app source, the raw-fetch guard x new ships refuses a server fetch( with no signal (X_RAW_FETCH); a vendor call is outbound() from @ultimat3/core/outbound.

Known gaps

As of 2026-10. None of these is closed by the current release; audit accordingly. The full defect list, security-weighted or not, is Known gaps.

  • No third-party security audit. The auth stack has had no external review. Better Auth binds through AuthAdapter rather than being a dependency, so the seam is narrow and swappable — but neither the built-in adapter nor a Better Auth binding has been audited by anyone outside this repo.

  • Rate limiting holds across replicas only on the Postgres stores. postgresRateLimitStore (@ultimat3/http, table SQL_RATE_LIMIT_TABLE) and postgresAuthLimiter (@ultimat3/auth) count every replica against one bucket; memoryRateLimitStore() and MemoryAuthLimiter count per process, so N replicas on them enforce N × the configured bucket. The store and the app each declare a scope ('process' | 'shared'), httpServer({ rateLimitStore }) reaches the seam through the supported API, and an app declaring scope: 'shared' against a per-process store refuses at boot — X_RATE_LIMIT_NOT_SHARED, and X_AUTH_LIMITER_NOT_SHARED for the credential path.

    The two sides answer a missing scope differently, and the HTTP one refuses the boot. This file said "both default to 'process'" and that is false for HTTP:

    Side No scope declared
    HTTP — resolveRateLimitConfig throws X_RATE_LIMIT_SCOPE_UNSET while the limiter is enabled. DEFAULT_RATE_LIMIT is typed Omit<RateLimitConfig, 'scope'>, so it structurally cannot carry one. Defaulting made "we did not ask" indistinguishable from "the app said one replica", and the chart's replicas: 3 then enforced every number three times over, silently. enabled: false reads as 'process' — a limiter that is switched off has nothing to be wrong about
    Credentials — DEFAULT_AUTH_RATE_LIMIT defaults to 'process'. Per-IP and per-account lockout, one process' worth of state

    A shared proxy can add a fleet-wide limit at ingress, but it does not satisfy the app's own guards: scope: 'shared' still needs the Postgres stores, and an enabled scope: 'process' limit still counts per replica.

  • One master key seals every purpose — accepted, As of 2026-10-07 (owner decision 16, #648). seal() encrypts sealed entity columns, MFA secrets, OAuth handshakes and MCP confirmation arguments under the key x secrets manages (ULTIMATE_SECRETS_KEY, else .secrets.key); only the IV's MAC key is derived from it under a fixed label (packages/core/src/seal-keys.ts). There is no subkey per purpose, so one leaked key opens every sealed value, and rotating it (x secrets rotate, the retired ring ULTIMATE_SECRETS_RETIRED_KEYS) rotates all of them together. Treat the key as the most sensitive secret the app holds.

  • A tenant has its own login lockout — kept, As of 2026-10-07 (owner decision 17, #648). Besides the per-IP and per-account buckets, failed sign-ins count against the org: rateLimit.orgMaxAttempts, default maxAttempts × 20 = 100 per 15-minute window (packages/auth/src/rate-limit.ts, ORG_ATTEMPT_FACTOR). An attacker who knows about 20 of an org's addresses can use up that allowance and lock the whole org out of password sign-in for lockoutMs. It stays because it is what stops a spray spread across one tenant's accounts. Raise orgMaxAttempts for a large tenant; release a held org with await auth.orgLimiter.recordSuccess(orgKey(orgId)) (the fix X_ACCOUNT_LOCKED prints for a held org).

  • The sync protocol has had no adversarial review. Tiers 1–2 ship, and so does tier 3's client half: record types marked persist: true are stored under principal-scoped IndexedDB keys and restored on page boot before the first socket connection, stale until the server confirms them (packages/realtime/src/record-persister.ts); a restore whose principal changed while it read is discarded. Only synced truth is written, never an optimistic overlay — but what a hostile client can send the sync node has not been reviewed by anyone outside this repository.

There aren't any published security advisories