Skip to content

Atlas 0.2.0

Released 2026-10-12 · v0.2.0 on GitHub · install with "atlas": "github:AndreyDAraya/atlas#v0.2.0"

Production hardening: stricter defaults at the HTTP boundary, no exception text past the runtime, bounded shapes and identifiers, and store refusals that settle instead of retrying forever.

  • Express: GET /atlas/map is off by default. atlasRouter(runtime, { map }) takes boolean | (req) => boolean | Promise<boolean>; a guard that returns false answers 403 forbidden. The map is the system’s blueprint (every command with its input shape and access, internal ones included), so it is no longer served to anyone who finds the URL. Migration: pass map: true to keep it public, or map: (req) => isOperator(req) to gate it; atlas map on the CLI is the development path. (4e34554)
  • Express: Atlas-Await is honored for identified callers only. allowAwait: 'authenticated' | 'all' | 'none' (default 'authenticated'): an await holds a connection up to the timeout per request, so an anonymous header is ignored, the command still runs and the answer has no awaited field. awaitTimeoutMs bounds an honored wait (default the runtime’s 2000). Migration: set allowAwait: 'all' if anonymous clients rely on awaited; clients treat a missing awaited as “not waited”. (4e34554)
  • Express: every route is an error boundary. Whatever throws in a route (the runtime, identify, a body the JSON parser refuses) answers a rejection with no message: 500 internal-error for the unexpected, 400 invalid-input for an unparseable body (Express’s HTML error page is gone); store-failed maps to 503. New onError(error, req) hook receives every swallowed error. Migration: wire onError to your logger; clients that matched on Express’s default error body switch to code. (833121b)
  • Rejection params never carry exception text. decide-failed params are {}, query-failed params are {}, upcast-failed params are { collection, from } (a record) or { type, from } (a fact); the trace reason keeps the message. Clients may now also see store-rejected (422), store-failed (503) and internal-error (500). Migration: stop parsing a message out of decide-failed / query-failed / upcast-failed params and read runtime.trace() or the observe events instead; map the three new codes. (833121b, 70bf574)
  • A forged SYSTEM actor is refused before access. dispatch and query reject an external actor whose id is system, whose roles include system, or whose roles is not an array: forbidden with params { actor: 'reserved' }, before any access rule is read. The exported SYSTEM object itself, by reference, still dispatches (an edge subscriber writing back); a structural copy does not. 'internal' commands stay undispatchable for every actor, SYSTEM included, as before. Migration: an identify that mints { id: 'system' } or the system role for a real caller must use another id and role; code that built its own { id: 'system', roles: ['system'] } imports SYSTEM from atlas/runtime instead. (c058d20)
  • CommitResult gained a third outcome. src/runtime/store.ts exports interface Poison { code; message } and commit may return { ok: false; poison: Poison } for a refusal the database would repeat for the same bytes; it throws only for transient errors. The runtime settles a poisoned delivery as failed with code store-rejected and advances the cursor instead of retrying forever, committing its clean projected rows with the advance (only rows the store refuses are dropped, so the live view matches a rebuild); a poisoned dispatch is rejected with store-rejected (params.code is the SQLSTATE); a view rebuild retries a poisoned batch row by row and skips the refused row. The Postgres adapter returns poison only for refusals the bytes cause: SQLSTATE class 22 (data exception) and the integrity codes 23503, 23514 and 23P01; 23502 (every NOT NULL column is the adapter’s own schema), class 42 (undefined table or column, insufficient privilege) and everything else throw and are retried, since a migration or a GRANT fixes them. Migration: custom Store adapters add the poison branch to commit (the contract suite in test/store-contract.ts exercises it) and keep throwing for transient errors. (70bf574)
  • runtime.trace() is bounded. traceLimit (default 1000) keeps the newest events; observe still receives every one. Migration: code that read the whole history from trace() moves to observe, or raises traceLimit. (8af4585)
  • t.int accepts only safe integers (Number.isSafeInteger): 1e300 is invalid-input, it would not cast to bigint downstream. t.record rejects the keys __proto__, constructor and prototype (issue reserved key). Migration: none for well-formed input; a client that sent those values now gets invalid-input. (3880c2b)
  • View and row field names are bounded identifiers. A view name matches ^[A-Za-z][A-Za-z0-9_-]*$ with at most 40 chars, a row field ^[A-Za-z][A-Za-z0-9_]*$ with at most 63 (IDENTIFIER in src/runtime/view.ts); viewSpec throws at boot and atlas check ATLAS020 reports the rename. Postgres truncates identifiers to 63 bytes, so a long view name collided its live and shadow tables. Migration: rename a view or field ATLAS020 names; a renamed view rebuilds on the next boot. (1b1a309)
  • t.text(max), t.array(of, { max }) and t.pattern(re, message, of?) with a bounded base shape; ATLAS003 checks a consumer’s max against the producer’s; JSON Schema emits maxLength / maxItems. (3880c2b)
  • ATLAS028 (warning): a command or query input validated by t.pattern over a string without a max (ReDoS surface); fix: t.pattern(re, message, t.text(n)). ATLAS020 extended with the identifier rule. (3880c2b, 1b1a309)
  • atlasRouter options map, allowAwait, awaitTimeoutMs, onError. (4e34554, 833121b)
  • createRuntime({ traceLimit }). (8af4585)
  • .github/workflows/check.yml: Node 24 and Postgres 17, npm run check and npm run test:pg on push to main / develop and on pull requests. (24f7948)
  • LICENSE (MIT) and the license field in package.json.
  • A commit the database refused deterministically (\u0000 in a jsonb string, a number beyond bigint) was retried forever and blocked every fact behind it for that consumer; it now settles as store-rejected and the cursor advances. (70bf574)
  • Exception text, including stack traces and quoted data, reached clients through decide-failed / query-failed / upcast-failed params and through Express’s default error page. (833121b)
  • The system map was served unauthenticated at GET /atlas/map. (4e34554)
  • Anonymous callers could hold a connection per request with Atlas-Await. (4e34554)
  • t.record accepted __proto__ and other prototype keys (prototype pollution of parsed values). (3880c2b)
  • A view name longer than Postgres’s 63-byte identifier limit silently collided its live and shadow tables. (1b1a309)
  • An external actor could claim the system id or role and pass 'internal' access checks. (c058d20)

From CHANGELOG.md. All versions: Releases.