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.
Breaking
Section titled “Breaking”- Express:
GET /atlas/mapis off by default.atlasRouter(runtime, { map })takesboolean | (req) => boolean | Promise<boolean>; a guard that returnsfalseanswers 403forbidden. 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: passmap: trueto keep it public, ormap: (req) => isOperator(req)to gate it;atlas mapon the CLI is the development path. (4e34554) - Express:
Atlas-Awaitis 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 noawaitedfield.awaitTimeoutMsbounds an honored wait (default the runtime’s 2000). Migration: setallowAwait: 'all'if anonymous clients rely onawaited; clients treat a missingawaitedas “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: 500internal-errorfor the unexpected, 400invalid-inputfor an unparseable body (Express’s HTML error page is gone);store-failedmaps to 503. NewonError(error, req)hook receives every swallowed error. Migration: wireonErrorto your logger; clients that matched on Express’s default error body switch tocode. (833121b) - Rejection params never carry exception text.
decide-failedparams are{},query-failedparams are{},upcast-failedparams are{ collection, from }(a record) or{ type, from }(a fact); the tracereasonkeeps the message. Clients may now also seestore-rejected(422),store-failed(503) andinternal-error(500). Migration: stop parsing a message out ofdecide-failed/query-failed/upcast-failedparams and readruntime.trace()or theobserveevents instead; map the three new codes. (833121b, 70bf574) - A forged
SYSTEMactor is refused before access.dispatchandqueryreject an external actor whose id issystem, whose roles includesystem, or whoserolesis not an array:forbiddenwith params{ actor: 'reserved' }, before any access rule is read. The exportedSYSTEMobject itself, by reference, still dispatches (an edge subscriber writing back); a structural copy does not.'internal'commands stay undispatchable for every actor,SYSTEMincluded, as before. Migration: anidentifythat mints{ id: 'system' }or thesystemrole for a real caller must use another id and role; code that built its own{ id: 'system', roles: ['system'] }importsSYSTEMfromatlas/runtimeinstead. (c058d20) CommitResultgained a third outcome.src/runtime/store.tsexportsinterface Poison { code; message }andcommitmay 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 asfailedwith codestore-rejectedand 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 isrejectedwithstore-rejected(params.codeis 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: customStoreadapters add the poison branch tocommit(the contract suite intest/store-contract.tsexercises it) and keep throwing for transient errors. (70bf574)runtime.trace()is bounded.traceLimit(default 1000) keeps the newest events;observestill receives every one. Migration: code that read the whole history fromtrace()moves toobserve, or raisestraceLimit. (8af4585)t.intaccepts only safe integers (Number.isSafeInteger):1e300isinvalid-input, it would not cast tobigintdownstream.t.recordrejects the keys__proto__,constructorandprototype(issuereserved key). Migration: none for well-formed input; a client that sent those values now getsinvalid-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 (IDENTIFIERinsrc/runtime/view.ts);viewSpecthrows at boot andatlas checkATLAS020 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 })andt.pattern(re, message, of?)with a bounded base shape; ATLAS003 checks a consumer’smaxagainst the producer’s; JSON Schema emitsmaxLength/maxItems. (3880c2b)- ATLAS028 (warning): a command or query input validated by
t.patternover a string without a max (ReDoS surface); fix:t.pattern(re, message, t.text(n)). ATLAS020 extended with the identifier rule. (3880c2b, 1b1a309) atlasRouteroptionsmap,allowAwait,awaitTimeoutMs,onError. (4e34554, 833121b)createRuntime({ traceLimit }). (8af4585).github/workflows/check.yml: Node 24 and Postgres 17,npm run checkandnpm run test:pgon push tomain/developand on pull requests. (24f7948)LICENSE(MIT) and thelicensefield inpackage.json.
- A commit the database refused deterministically (
\u0000in a jsonb string, a number beyondbigint) was retried forever and blocked every fact behind it for that consumer; it now settles asstore-rejectedand the cursor advances. (70bf574) - Exception text, including stack traces and quoted data, reached clients through
decide-failed/query-failed/upcast-failedparams 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.recordaccepted__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
systemid or role and pass'internal'access checks. (c058d20)
From CHANGELOG.md. All versions: Releases.