sdxc

Type to search, or start from one of these:

Changelog

Every dated release of the collection, newest first, with its notes.

2026.9.17

On GitHub

@sdxc/auth

Republished because @sdxc/crypto changed.

@sdxc/billing

Republished because @sdxc/crypto changed.

@sdxc/crypto

  • feat: join byte runs without a local helper Every protocol framing a record — a header, an info string, a length prefix — concatenates bytes, and each caller writing that loop again is another place the offset arithmetic can be wrong.

    Parts come in as BinaryLike, so a label spelled as text joins the binary around it and the caller keeps no TextEncoder of its own.

@sdxc/distill

  • refactor: rename @sdxc/readability to say what it does Readability is the name of a measurement — how hard a text is to read — and this package does not measure anything. It finds the article in a page and throws the furniture away, which is what distilling is, and what Chrome's own reader mode has been called all along.

    extract and extractFrom become distill and distillFrom, and the error and namespace names follow them. The "extracted" outcome keeps its spelling: it is a value the article cache has already written, and renaming it would expire live entries to no purpose.

  • chore: publish distill It was marked private, so the bootstrap filtered it out before it ever looked at npm. Nothing holds it back: it reaches only public packages, and finding the article in a page is useful well outside the reader that needed it first.

    It gains the license file every published package carries.

  • docs: write the README for npm rather than for the repo A published README is read by somebody who can reach only the one page npm serves them. It gains how to install it, how versions are numbered and what depending on one promises, and the license and author every other published package states.

    The tips become two patterns that run: caching what comes back, and deciding the article was worth fetching. Related Packages goes, having described a shelf the reader cannot see.

@sdxc/feed

  • feat: bound what a fetch will read and follow A feed address comes from whoever pasted it, so every fetch is untrusted network input. Two bounds were missing: a publisher serving a gigabyte could exhaust an isolate's memory before the parser saw a byte of it, and a redirect chain could spend a caller's whole budget without ever answering.

    The body is now read off a stream and refused the moment it passes the cap, and a Content-Length already past it is refused before a single read. Redirects are walked here rather than by the runtime, counted, and each Location resolved against the URL it came from — which also makes the address a response finally came from a fact this package tracked rather than one the caller infers.

    Both bounds belong here rather than in a caller, because every caller wants them and a cap applied by one is a cap the next one forgets. A refusal carries its own error type, so a caller can tell a document it declined to read from an origin it could not reach.

  • feat: expose the links a document and its response declare One list folding an Atom link element, an RSS atom:link, a JSON Feed's hubs and the response's Link header, so a caller asking which hub a feed names asks once. selectHub ranks the header over the document and takes only an https one.

@sdxc/html

  • feat: extract an article from a page Scores a document's blocks to find the one that carries the article, serializes it back to markup, and reads the metadata a page declares about itself. Sanitization moves into @sdxc/html, where the parser it needs already lives.

  • feat: sanitize against an allowlist that names every element's attributes No attribute is global: each element states the ones it may keep and the schemes each URL among them may use. A style attribute is named nowhere, so a publisher's positioning and background images are gone before any policy has to refuse them.

  • refactor: rename @sdxc/readability to say what it does Readability is the name of a measurement — how hard a text is to read — and this package does not measure anything. It finds the article in a page and throws the furniture away, which is what distilling is, and what Chrome's own reader mode has been called all along.

    extract and extractFrom become distill and distillFrom, and the error and namespace names follow them. The "extracted" outcome keeps its spelling: it is a value the article cache has already written, and renaming it would expire live entries to no purpose.

@sdxc/http

Republished because @sdxc/crypto changed.

@sdxc/mcp

  • feat: let a resource live under a scheme of its own The route matcher takes http and https, so a reader:// address could not be expressed at all. It is carried as the host of an https address while matching and restored when the href is built, which leaves the template a client reads unchanged.

@sdxc/opml

  • feat: read and write the folder a feed sits in An outline nested under another carries its nearest enclosing folder on read, and a document written back groups each folder's feeds under one outline with the unfiled ones after them.

  • chore: publish opml It was marked private, which is why the bootstrap answered that there was nothing else to publish: the filter drops a private package before npm is ever asked about it. It reaches only public packages, and a subscription list is a format other people read and write too.

@sdxc/pagination

Republished because @sdxc/crypto changed.

@sdxc/sample

Republished because @sdxc/crypto changed.

@sdxc/spec

Republished because @sdxc/html changed.

@sdxc/webhooks

Republished because @sdxc/crypto changed.

Compare: https://github.com/sergiodxa/monorepo/compare/v2026.9.16...v2026.9.17

2026.9.16

On GitHub

@sdxc/auth

Republished because @sdxc/crypto changed.

@sdxc/billing

Republished because @sdxc/crypto changed.

@sdxc/cloudflare-mocks

  • fix: refuse the transactions the platform refuses A Durable Object rejects BEGIN, COMMIT, ROLLBACK, SAVEPOINT and RELEASE, and offers atomicity through coalescing every write a turn makes instead. The mock ran all of them, being plain SQLite, so code that could not execute passed its tests — which is how a broken transaction reached a running app.

    The mock now throws the runtime's own message, matching on each statement's leading keyword past comments and quoted text so a migration naming a column "begin" still runs. The driver no longer emits SQL the platform rejects: the token-based protocol cannot be bridged to the synchronous transactionSync the platform offers, so it refuses rather than handing out a scope that silently never rolls back, and it stops advertising savepoints and transactional DDL.

    Both READMEs document what a Durable Object actually gives, and where the turn-scoped guarantee ends.

@sdxc/crypto

  • docs: stop calling scrypt a Web Crypto primitive Password hashing reaches for node:crypto, because scrypt has no Web Crypto equivalent — which the README states plainly and the description contradicted. The description and the package table now carry the exception too.

@sdxc/data-table-sqlstorage

  • fix: refuse the transactions the platform refuses A Durable Object rejects BEGIN, COMMIT, ROLLBACK, SAVEPOINT and RELEASE, and offers atomicity through coalescing every write a turn makes instead. The mock ran all of them, being plain SQLite, so code that could not execute passed its tests — which is how a broken transaction reached a running app.

    The mock now throws the runtime's own message, matching on each statement's leading keyword past comments and quoted text so a migration naming a column "begin" still runs. The driver no longer emits SQL the platform rejects: the token-based protocol cannot be bridged to the synchronous transactionSync the platform offers, so it refuses rather than handing out a scope that silently never rolls back, and it stops advertising savepoints and transactional DDL.

    Both READMEs document what a Durable Object actually gives, and where the turn-scoped guarantee ends.

  • fix: split a script on terminators, not on every semicolon executeScript cut on every ;, so one inside a string literal, a quoted identifier, a line comment or a block comment corrupted the script. A migration adding a trigger half-applied, since a trigger body's own semicolons ended the statement early — at deploy time, against a real database, with an error that named neither the migration nor the cause.

    The scanner tracks quoting, both comment forms and trigger bodies, where CASE nests and END unnests, so a body's semicolons stay inert. A trigger's BEGIN is therefore never read as the transaction control this driver refuses.

    An unterminated literal, identifier, comment or trigger body now throws before anything runs, naming the problem and the line it opened on, rather than applying whichever prefix happened to parse.

@sdxc/feed

  • feat: read JSON Feed alongside RSS and Atom Text that opens a JSON object is parsed as JSON Feed and normalized into the same shape, so a reader handed a feed.json URL needs to know nothing new. Items gain contentText, which holds a plain-text body as plain text, and discovery follows application/feed+json links, preferring them over application/json.

@sdxc/flags

  • docs: correct what ready() returns The README claimed ready() and shutdown() both answer with a Result, but ready() is declared Promise<void> — as the Flags interface printed a few lines above it already showed. setProvider initializes the provider it registers and hands its caller that outcome, so ready() has nothing left to report and awaits the same memoized answer.

@sdxc/flags-engine

Republished because @sdxc/flags changed.

@sdxc/html

  • docs: name fetch in the package description HTML.fetch is a first-class export that performs the HTTP call itself, but the description covered parsing alone. The README heading already said "fetch or parse"; the description and the package table now agree with it.

@sdxc/http

Republished because @sdxc/crypto changed.

@sdxc/jobs

Republished because @sdxc/validate changed.

@sdxc/json-feed

  • feat: read and write JSON Feed 1.1 documents A builder and parser for the JSON syndication format, named as the format names its own fields. Extension objects round-trip untouched, a parsed 1.0 document writes back out as 1.0, and reading is lenient the way the format asks: a field typed the wrong way is skipped, and only an item without an id is discarded.

@sdxc/lazy-route

  • test: cover a stand-in inside a controller A route map whose actions live in different modules can name a loader per action, and createController() takes those as readily as a plain object does, keeping the route map's typing on each one. The chain still runs in order: the controller's own middleware answers before any action's module is imported.

@sdxc/pagination

Republished because @sdxc/crypto changed.

@sdxc/rate-limit

  • docs: give every README example the key it requires key has no default, and the README says so, but the login and fail-closed examples both omitted it and would not have typechecked. Both surfaces are anonymous, so they key on the connecting address the way the earlier example in the same README does.

@sdxc/sample

Republished because @sdxc/crypto changed.

@sdxc/spec

  • docs: correct the workers entry point's capability list src/workers.ts exports seven plugin factories and its own header comment lists all seven; the README named four. The createHttpPlugin example also imported from /workers while the prose around it described the root entry, which both entries export.

@sdxc/validate

  • docs: rewrite the README for the package it documents Two behaviors were missing. input also takes any JSON value, not only a plain object, and a schema that validates the raw FormData/URLSearchParams source rather than a flattened object — remix/data-schema/form-data's object() — has its rejection retried against that source, so it passes through the same call. A reader working from the README alone would not know either worked.

    It was also the last README still written in the internal style, against a guide that asks a published package for the npm reader's version: no route module paths or other vocabulary that means nothing outside a checkout, exports described in a sentence each instead of Parameters/Returns scaffolding, and the Versioning, License and Author trailer every sibling carries.

@sdxc/webhooks

Republished because @sdxc/crypto changed.

Compare: https://github.com/sergiodxa/monorepo/compare/v2026.9.15...v2026.9.16

2026.9.15

On GitHub

@sdxc/atom

Republished because @sdxc/result changed.

@sdxc/auth

Republished because @sdxc/crypto changed.

@sdxc/billing

Republished because @sdxc/crypto changed.

@sdxc/cache

Republished because @sdxc/duration changed.

@sdxc/cron

Republished because @sdxc/duration changed.

@sdxc/crypto

Republished because @sdxc/result changed.

@sdxc/dates

Republished because @sdxc/duration changed.

@sdxc/duration

Republished because @sdxc/result changed.

@sdxc/feed

Republished because @sdxc/atom changed.

@sdxc/flags

  • feat: implement the OpenFeature specification (ADR-059) Add @sdxc/flags, an implementation of the OpenFeature specification v0.9.0 for the dynamic-context paradigm, so a behavior can change without a deploy and the flag system stays a constructor argument.

    The evaluation API is createFlags() rather than a global singleton, which is the specification's own Requirement 1.8 and keeps a Worker isolate from sharing flag state across concurrent requests. Nothing on the evaluation path throws: a failed evaluation answers with the default value it was handed and carries the reason and error code on its detailed form, so a provider outage is distinguishable from a flag being off. Object flags take a schema and are validated rather than cast.

    Two providers ship. NoopProvider answers defaults, and InMemoryProvider is a real provider with lifecycle and events that the specification's own Gherkin suites evaluate against. A conformance suite exports from /conformance so a provider written anywhere runs the same assertions as the two in the box.

    Compliance is a ledger rather than a claim: a test over the vendored specification.json asserts every MUST-class requirement is either covered by a test named for it or declined with the condition that excuses it, so a specification bump fails with the list of new requirements.

    Both middleware subpaths publish one client to a shared context key, so a job handler reads ctx.flags exactly as a route handler does.

    No app adopts the package yet; that waits on a provider against a real flag system.

  • fix: declare the jobs peer as a workspace range The peer was pinned to * because the release pipeline rewrote dependencies alone, so a workspace: range in peerDependencies reached the manifest check and failed the publish. Peers pin the same as dependencies now, so the range is written the way every other workspace range in the repo is and resolves to the dated version at publish time.

@sdxc/flags-engine

  • feat: add the flag evaluation engine (ADR-060) Decides what a flag is worth. @sdxc/flags shipped the API an application evaluates through and the Provider contract a flag system implements, but nothing that resolves a real flag; this is the half that holds the rules, reads the context a request arrives with, and works out which variant a subject gets.

    A flag is variants, an optional default variant, and an ordered rule list where the first match wins. Conditions are a typed union rather than an expression language, so an editor completes the operators and a write can be validated against the same schema the engine parses with. Every operator compares within one type, and a field the caller did not send matches nothing but exists.

    A split buckets on MurmurHash3 of the subject, scaled onto the weights' own sum, so a percentage means the same thing here as in the reference engine: the same subject lands in the same arm on every request and in every isolate, and two flags at one percentage cover different subjects unless they share a seed.

    Evaluation is pure and synchronous, which is what lets a provider, an HTTP endpoint and an admin preview call one function. It never throws and never logs. Every reason the specification names has exactly one cause here, so a consumer reading reason alone tells a flag that is off from a flag system that is broken.

    Definitions arrive through FlagStore, one method answering with the whole set. An in-memory store and a Cloudflare KV store ship, and an application wanting a row per flag writes that store against its own schema and runs the same conformance suite the two shipped ones do. Parsing is per flag, so a mistyped rule fails the flag someone just edited and leaves the rest resolving.

    EngineProvider is the adapter onto @sdxc/flags, and it is the only module here that knows OpenFeature.

@sdxc/highlight

Republished because @sdxc/markdown changed.

@sdxc/html

Republished because @sdxc/result changed.

@sdxc/http

Republished because @sdxc/crypto changed.

@sdxc/jobs

  • refactor!: let the Cloudflare worker own its dead-letter queue deadLetterQueue and onInvalid were dispatcher options, but neither meant anything to a backend that is pulled rather than pushed: one is matched against the queue name a batch arrived on, which only a push backend reports, and the other exists because this platform reaches a dead-letter queue by exhausting retries rather than by being asked, so a refused body has to be written there.

    Both move to cloudflare.worker(dispatcher, options), the consume side that already knows which queue a batch came from. The dispatcher keeps what is its own: it still answers dead-letter for a message it refuses, and still records a dead-lettered batch as a job log that ended dead_letter. It is now told that a batch is dead-lettered instead of deciding it from a name, and apply may return a promise, since forwarding a body is a write that must land before the message is acked.

    The { invalid: … } envelope becomes the contract it always was, at @sdxc/jobs/queue: the adapter writes it, the dispatcher reads it back.

@sdxc/jwt

Republished because @sdxc/duration changed.

@sdxc/mail

Republished because @sdxc/highlight changed.

@sdxc/markdown

Republished because @sdxc/result changed.

@sdxc/mcp

Republished because @sdxc/result changed.

@sdxc/pagination

Republished because @sdxc/crypto changed.

@sdxc/rate-limit

Republished because @sdxc/duration changed.

@sdxc/result

Republished because @sdxc/types changed.

@sdxc/rss

Republished because @sdxc/result changed.

@sdxc/sample

Republished because @sdxc/crypto changed.

@sdxc/semver

  • feat: add SemVer 2.0.0 parsing, ordering and comparisons Answers the version questions this repository kept asking in two private copies: whether one version stands in a named relation to another, and which of a list of versions is the newest.

    parse() is the single grammar gate, accepting SemVer 2.0.0 plus the leading v a git tag or a user agent carries, and reporting anything else as a Result failure naming the text.

    compare() is total, so a list whose entries come straight from a registry sorts in one call: text that is not a version ranks below every version and ties with other such text, collecting those entries at the front.

    satisfies() covers the eight comparisons =, !=, <, <=, >, >=, ~ and ^ without the range grammar. A prerelease takes part by precedence alone, so 1.2.4-rc.1 satisfies ^ 1.2.3; a release channel is expressed by comparing against the prerelease itself.

@sdxc/session-storage-kv

Republished because @sdxc/duration changed.

@sdxc/sitemap

Republished because @sdxc/result changed.

@sdxc/spec

  • fix: read a browser assertion's grammar before spawning the CLI browser.cookie, text, url, path, title, query and fragment parsed their assertion inside observeValue, after the agent-browser call, so a malformed call reported a missing binary instead of the grammar mistake wherever the CLI is not installed.

@sdxc/types

  • feat: add JSONPrimitive, the scalar leaf of JSONValue Names the half of a JSON boundary that holds no other value, so an API that compares, indexes or keys by what it is handed can say so in its signature instead of taking JSONValue and rejecting structures at runtime.

    JSONValue is now written in terms of it, which keeps the two in step: a primitive is a value by construction rather than by a union that repeats the four scalars in both places.

@sdxc/validate

Republished because @sdxc/result changed.

@sdxc/webhooks

Republished because @sdxc/crypto changed.

@sdxc/workers-cache

Republished because @sdxc/result changed.

@sdxc/xml

Republished because @sdxc/result changed.

@sdxc/yaml

Republished because @sdxc/result changed.

Compare: https://github.com/sergiodxa/monorepo/compare/v2026.9.14...v2026.9.15

2026.9.14

On GitHub

@sdxc/api-client

  • chore: make the package public, with a README written for npm

@sdxc/billing

  • chore: make the package public, with a README written for npm

  • fix: write out the exported context key type The declaration emit cannot name ContextKey, which remix/router returns from createContextKey but does not re-export, so the publish build failed with TS2883. ContextKey is structural, so the annotation writes its shape out.

@sdxc/cache

  • chore: make the package public, with a README written for npm

  • fix: declare the result dependency its sources import The index, both adapters and the conformance entry import @sdxc/result, which the manifest omitted. An installed consumer would have failed to resolve it.

@sdxc/cloudflare-mocks

  • chore: make the package public, with a README written for npm

@sdxc/data-table-d1

  • chore: make the package public, with a README written for npm

@sdxc/data-table-sqlstorage

  • chore: make the package public, with a README written for npm Corrects the documented default for savepoints, which the driver enables.

@sdxc/get-client-ip

  • chore: make the package public, with a README written for npm

@sdxc/highlight

  • feat: paint a parsed markdown document through a walk visitor @sdxc/highlight/markdown exports highlight, a Markdown.walk visitor holding one code handler, so a document paints in the pass that walks it. The handler resolves the language the block names, tokenizes its body, and returns a copy of the node carrying both; a block naming no language, as an indented one never does, is painted as plain.

    The tokens field belongs here rather than to the format: the entry declares it on Markdown.Code through module augmentation, so @sdxc/markdown has no dependency on this package and no field of its own that knows fences can be painted.

    Visitors are values and merge by spread, so one walk can paint and rewrite at once. Every handler is synchronous, which keeps a painting pass inside a render path free of an await.

    @sdxc/highlight/markdoc is removed with the parser it adapted to, and the Markdoc dependency goes with it.

  • chore: make the package public, with a README written for npm

  • chore: drop the unused remix dependency Nothing under src imports remix, so the dependency only added install weight for a consumer.

@sdxc/hostname

  • chore: make the package public, with a README written for npm

  • refactor: build requests through @sdxc/api-client The client held its own header, URL-joining and request helpers. It now composes an APIClient that carries the zone token in its before hook. The public API is unchanged.

@sdxc/http

  • chore: make the package public, with a README written for npm The previous description named Request factories the package does not export, and the status-code and content-type references omitted roughly thirty real exports.

@sdxc/icons

  • docs: point the related-packages row at the markdown renderer entry

  • chore: make the package public, with a README written for npm

  • fix: scope the package tsconfig to src The release build compiles a package with src as the rootDir, so the codegen script the include pattern reached failed the build with TS6059. The script keeps its coverage under its own tsconfig in the directory it lives in.

@sdxc/mail

  • refactor: render a markdown document the caller already parsed @sdxc/mail/markdown's Markdown component takes a document rather than a source string, so a caller holding a parsed document pays for no parser at all and the same document can render as a page and as an email.

    Conversion is a switch on node.type the compiler proves exhaustive, which replaces the shape-sniffing the untyped tree needed. That reach extends the coverage an inbox gets: strikethrough, alerts, tables, footnotes, thematic breaks and inline code all render now, and a code block arrives already painted when the caller ran the highlighter.

    Raw HTML renders as escaped text, so markup an author did not vet never reaches an inbox. The package no longer depends on a markdown parser of any kind.

  • chore: make the package public, with a README written for npm

@sdxc/markdown

  • feat: parse and write GitHub Flavored Markdown over a typed AST @sdxc/markdown reads and writes markdown itself, over a first-party AST. Every node is a plain JSON-serializable object with a type discriminator and a position, so a parsed document caches in KV, travels in a payload, diffs in a test, and narrows in the compiler where content used to arrive as unknown.

    Markdown.parse reads the frontmatter block and the body in one traversal, and Markdown.frontmatter stops after the block so an index over a hundred posts reads a hundred titles without parsing a hundred bodies. Markdown.stringify writes a document back, normalized, so a round trip is idempotent and the output is a fixed point of the repository's formatter. Markdown.walk is the one transform mechanism: a visitor keyed by node type, which can be asynchronous, and which turns a handler's throw into a failure carrying the node's position.

    The dialect is everything in CommonMark plus GFM's tables, task lists, strikethrough and literal autolinks, plus GitHub's alerts and footnotes. Two additions sit on top: {% key="value" %} annotations that decorate a block, and registered elements whose children parse as markdown rather than as text.

    Four entry points, each named for what it produces. The root is the format; /plain gives text, /html gives static HTML carrying md- classes to style, and /remix gives remix/ui nodes. @sdxc/markdown/server and @sdxc/markdown/client are gone, and so is MarkdownView — a view calls toRemix and owns the markup around it. The class is a namespace now: its constructor is private and every operation is static, so per-app configuration is an options object the app hoists rather than an instance it builds.

    Conformance is asserted as a floor that can only rise: 648 of 652 CommonMark examples and 658 of 672 GFM ones, the remainder being the divergences ADR-058 records. The format entry weighs 66.2 KB minified against the 181.1 KB of the entry it replaces.

  • chore: make the package public, with a README written for npm Corrects a schema example that called a checks helper the schema package does not export.

@sdxc/mcp

  • chore: make the package public, with a README written for npm

  • fix: write out the exported context key types The declaration emit cannot name ContextKey, which remix/router returns from createContextKey but does not re-export, so the publish build failed with TS2883. ContextKey is structural, so the annotations write its shape out.

@sdxc/pagination

  • chore: make the package public, with a README written for npm

@sdxc/response

  • chore: make the package public, with a README written for npm

@sdxc/seo

  • chore: make the package public, with a README written for npm

@sdxc/server-timing

  • chore: make the package public, with a README written for npm

@sdxc/session-storage-kv

  • chore: make the package public, with a README written for npm Corrects the session middleware example, which named a signature the middleware does not take.

@sdxc/strings

  • chore: make the package public, with a README written for npm

@sdxc/typeid

  • chore: make the package public, with a README written for npm

  • fix: correct the UUID and suffix in the documented examples The encode, decode and fromUUID examples paired a UUID with a suffix that is not its encoding, in both directions. They now use the TypeID specification's own vector.

@sdxc/u

  • chore: make the package public, with a README written for npm

  • fix: name the mixin type the exported functions return The declaration emit wrote the return type of the two functions that return a raw() mixin as a relative path into the u package source, which the release build rejects as reaching outside dist/. The type ships from the u entrypoint now, and the functions that return one write it out.

@sdxc/ui

  • chore: make the package public, with a README written for npm The previous README documented three exports that do not exist, the wrong theme variable contract and an invented radius scale; all three are corrected against the source.

  • fix: name the mixin type the exported functions return The declaration emit wrote the return type of the two functions that return a raw() mixin as a relative path into the u package source, which the release build rejects as reaching outside dist/. The type ships from the u entrypoint now, and the functions that return one write it out.

@sdxc/uuid

  • chore: make the package public, with a README written for npm

@sdxc/webhooks

  • chore: make the package public, with a README written for npm

@sdxc/workers-cache

  • chore: make the package public, with a README written for npm Corrects the documented purge return type, selector field names and policy value.

@sdxc/yaml

  • chore: make the package public, with a README written for npm

Compare: https://github.com/sergiodxa/monorepo/compare/v2026.9.12...v2026.9.14

2026.9.12

On GitHub

@sdxc/html

  • feat: query a served page by role and accessible name Parse a response body into a document, then address it by role and accessible name, by field name, by table position or by definition term — no browser, and the answers describe the page as served rather than the page after hydration.

    Names match exactly on the whitespace-normalized accessible name, several matches is a failure carrying every candidate with its position, and visibility is markup-level: hidden, aria-hidden, a <template> and an inline display/visibility declaration hide an element, while a stylesheet stays unread.

  • feat: fetch a page, and let every match scope its own lookups HTML.fetch asks for text/html, parses the body when that is what arrived, and reports a rejected request, an error status or another content type as an HTMLFetchError naming what came back — so a login redirect or a JSON error page is reported rather than parsed.

    Every match now carries the same five lookups over its own subtree, so a caller narrows to a form or a panel and reads inside it, where a name only has to be unique within that section. A scoped miss names what the scope holds.

  • chore: make the package public Removes private, adds the description and the license, and marks the row in the root package table, so the daily release publishes @sdxc/html.

  • feat: let a flow read the page it just fetched html, str and spec join the namespaces a flow may use, so a check can assert on the markup a server returned, compose a value into a request, and give each run an identity no earlier run produced. All three compute from their arguments or from the run's own identity, so none reaches the network and none needs a grant or a place in the request budget.

    @sdxc/html describes the nodes it handles in a vocabulary of its own rather than through the ambient DOM globals. The package ships TypeScript, so a consumer compiles it under the consumer's global scope, and a Worker's generated types declare an Element of their own for HTMLRewriter that merges with the DOM's. A declaration file compiles the package under exactly those hostile globals, so the next consumer to differ finds out here.

@sdxc/sitemap

  • feat: read the protocol the package already writes Sitemap.parse takes a parsed XML document and Sitemap.fetch retrieves one, both answering with the same instance append builds, so a consumer that wants the list of pages a site publishes reads it through one import instead of rediscovering which root elements count, that <loc> is absolute, that <lastmod> is W3C Datetime and that <priority> is a closed range.

    The root element decides whether a sitemap arrived, which is also the content check: a not-found template served under a 200 fails naming the root it found, rather than arriving as an empty entry set. A row that carries no usable <loc> is skipped and the document is kept, and a <lastmod>, <changefreq> or <priority> the protocol refuses leaves that field undefined, so a bad field costs a caller the field rather than the other 49,999 rows.

    A <sitemapindex> reads into the same class under a kind of "index", and toString() writes back the document the instance carries, so an index that is read, filtered and re-serialized stays an index.

  • chore: make the package public Removes private, adds the description, and marks the row in the root package table, so the daily release publishes @sdxc/sitemap. Both packages it depends on are already public.

    Rewrites the README for the npm landing page it becomes: a stranger reaching only what npm serves gets the installation line, the two directions as focused examples, the parsing rules the package owns, and every export. The Remix controller walkthrough, the repository links and the tips are gone, and the patterns that stayed are written to stand on their own.

@sdxc/spec

  • feat: implement ADR-018, what a real browser E2E suite needs Commands absorb fixtures, so fixture leaves the language and the keyword stays reserved to name its replacement. A bare identifier handed to a tool now resolves against the tool's descriptor rather than always becoming a word, and the zero-argument rule holds in every expression position, so an identity value composes where a spec actually writes it.

    str.format gives the language string composition as a tool; spec.run_id, spec.attempt and spec.nonce give a suite the one value that must not reproduce across runs, while every sample draw stays deterministic. html reads a served page, and shares one addressing vocabulary with browser so a document is addressed the same way whether or not a browser is needed.

    Named bases resolve relative targets, db takes its own grant and named connections, and the runner gains setup/teardown, skip, retries with a flaky count, and an artifacts directory.

    Three of the ADR's capability questions are answered against the real tools, and two of them came back needing compensation: agent-browser fill moves no range input and fires no change, so browser.fill drives one itself, and a closed <dialog> is hidden by a stylesheet markup cannot see, so a browser lookup reads visibility from the rendered page.

  • docs: record what ADR-018 settled across the spec ADR suite ADR-018 §7 answers the question ADR-008 left open — how execution environments are defined and selected — with the bases key, on "name", and a composition with ADR-007 that keeps configuration from ever implying authority. Both sides now say so.

    ADR-007 gains the db family and the host-fs grant a tool now demands, ADR-013 gains the two config keys that resolve without a grant, ADR-010's absolute-URL and snapshot-ref decisions are marked superseded, ADR-012's DATABASE_URL gating likewise, and ADR-017 records that the zero-argument reading now holds in every expression position. Every original decision stands as the record.

    teardown, a successful setup, and --artifacts gain the acceptance coverage they lacked, and the addressing vocabulary is exported so a third-party plugin can spread the parameter fragments the plugin guide tells it to.

  • fix: follow the spec language through ADR-018 The flow checker walks a spec's AST to derive the hosts a run may reach, so it tracked two language changes. fixture-call is gone with the construct, and a string literal inside an array literal is reachable now, which a URL sitting in one needs or the run is denied at a host the spec plainly names.

  • feat: let a flow read the page it just fetched html, str and spec join the namespaces a flow may use, so a check can assert on the markup a server returned, compose a value into a request, and give each run an identity no earlier run produced. All three compute from their arguments or from the run's own identity, so none reaches the network and none needs a grant or a place in the request budget.

    @sdxc/html describes the nodes it handles in a vocabulary of its own rather than through the ambient DOM globals. The package ships TypeScript, so a consumer compiles it under the consumer's global scope, and a Worker's generated types declare an Element of their own for HTMLRewriter that merges with the DOM's. A declaration file compiles the package under exactly those hostile globals, so the next consumer to differ finds out here.

  • perf: read one document once however many times a test asks A test asserts many times over one response, and parsing is the expensive part of answering, so re-reading the markup per assertion made a page's size cost what the assertions multiplied it to. The plugin keeps the few documents it most recently parsed, keyed by the source that produced them; a lookup never mutates a document, so callers share one safely.

    This matters most where flows are other people's: an uptime check spends one request from its budget and could spend the CPU of twenty parses.

  • perf: hold one parsed document, not several A parsed document runs about twenty-five times the size of its source, so holding four of them could raise a run's peak memory above what a 128 MB isolate has — while holding one never does: the alternative parses the same markup again, which allocates the same document anyway. One entry keeps the whole win for the case that matters, a test asserting many times over one response, and costs nothing in the worst case.

  • feat: let a host cap how much of a response body http reads A parsed document runs about twenty-five times the size of its source, so one oversized body can exhaust a 128 MB isolate. createHttpPlugin now takes the most bytes it will read: a declared content-length past the cap is refused before a byte arrives, and anything else is counted as it streams and cancelled the moment it passes. Measuring a body already read would come too late, since the memory is spent by then.

    The cap is set where the plugin is constructed, so it is the host's policy and no spec can raise it. spec run sets none. An uptime flow reads at most a mebibyte, which covers every page a server renders and leaves the isolate room to parse it.

    The refusal carries its own error type, so a host recognising it reads a field rather than the wording of a message it does not own.

Compare: https://github.com/sergiodxa/monorepo/compare/v2026.9.11...v2026.9.12

2026.9.11

On GitHub

@sdxc/atom

  • feat: open the package for publishing

@sdxc/auth

  • refactor: cache through @sdxc/cache The three issuers construct WorkerKVCache in place of Cache.KVStore. The blog's MCP cache holds one instance for both its helpers, and its JSON.stringify/JSON.parse bracketing goes with the unchecked casts it existed to make: cached() hands its loader straight to fetch, and the tool middleware reads a CallToolResult as one.

    packages/auth asserts Issuer.CacheStore against the Cache interface itself rather than a concrete store, which is the claim that actually matters.

  • feat!: Issuer.CacheStore answers with a Result The cache tier a shared Issuer and ServiceClient read through now returns Result<_, Error> from all three methods, following @sdxc/cache. The error is Error rather than a store's own type, so a store answering with a narrower one still satisfies it and this package depends on no cache.

    No observable behavior changes: a store that fails costs a read of the provider, and a document that cannot be fetched still throws the AuthError it always did, rethrown from the failure's cause.

  • feat!: open the package for publishing, with a keyed login limit The OAuth 2.0 and OpenID Connect client is published to npm as @sdxc/auth, with a LICENSE.md and a README written for a reader who can reach only the npm page. Every package it depends on is published alongside it: @sdxc/catch-response-middleware, @sdxc/location, @sdxc/logger and @sdxc/rate-limit, over the already-published @sdxc/crypto, @sdxc/duration, @sdxc/jwt and @sdxc/result.

    RelyingParty.Options.rateLimit is now a RateLimit, pairing the adapter with a required key(request). It read a Cloudflare header before, which is the one place the client stopped being runtime-neutral: on any other runtime every attempt collapsed into a single shared budget. Only the app knows what a login budget belongs to — the connecting address where the platform reports one, a tenant, a submitted username — so it now says. ServiceClient keeps a bare adapter, since it counts against its own client id.

    BREAKING CHANGE: rateLimit: adapter becomes rateLimit: { adapter, key: (request) => string | Promise<string> }.

  • chore: upgrade Remix to 3.0.0-rc.2 The router now answers a method mismatch with 405 and an Allow header instead of falling through to the default handler, so the HEAD probes against POST-only routes assert 405 and the cross-origin POST to /api/subjects/:subjectId reads as a method refusal.

@sdxc/catch-response-middleware

  • docs: state the throw redirect affordance directly

  • feat: open the package for publishing The middleware that turns a thrown Response into the request's response is published to npm as @sdxc/catch-response-middleware. Its only dependency is remix itself, and its README is rewritten for a reader who can reach only the npm page.

  • chore: upgrade Remix to 3.0.0-rc.2 The router now answers a method mismatch with 405 and an Allow header instead of falling through to the default handler, so the HEAD probes against POST-only routes assert 405 and the cross-origin POST to /api/subjects/:subjectId reads as a method refusal.

@sdxc/cron

  • feat: open the package for publishing Cron schedules, their zone-aware occurrences and their descriptors are published to npm as @sdxc/cron. It reaches only @sdxc/duration and @sdxc/result, both already in the release set, so the set grows by one.

    Its README gains an installation section, and its cross-package links point at npm rather than at repository paths, which resolve to nothing for a reader who arrived at the package page.

@sdxc/crypto

Republished because @sdxc/result changed.

@sdxc/dates

Republished because @sdxc/duration changed.

@sdxc/duration

Republished because @sdxc/result changed.

@sdxc/feed

  • feat: open the package for publishing

@sdxc/i18n

  • chore: upgrade Remix to 3.0.0-rc.2 The router now answers a method mismatch with 405 and an Allow header instead of falling through to the default handler, so the HEAD probes against POST-only routes assert 405 and the cross-origin POST to /api/subjects/:subjectId reads as a method refusal.

@sdxc/jobs

  • feat!: run over a queue backend of your choosing The package read as platform-neutral — nothing in src/ imported a binding — but it was not: the lifecycle ended a delivery by calling ack() and retry() on a Cloudflare message, the dispatcher was entered through a MessageBatch, and cron existed only because the platform fired scheduled with an expression to match. ADR-054 records the design; this is it.

    The lifecycle now decides an ending and answers with a Settlement for someone else to apply, so deliver() and deliverBatch() name no platform and every backend becomes a translation outside that seam. deliverBatch takes an apply callback rather than answering with an array, because a batch settles each delivery as that one finishes and one job crashing must still leave its batch mates acked. JobQueue is the port both adapters implement: @sdxc/jobs/cloudflare for Queues, @sdxc/jobs/memory for tests and for anything running without a platform, and @sdxc/jobs/conformance is the suite that says what a queue is, so a third adapter can prove itself.

    A message now carries { job, body } instead of the job's name mixed into the payload as type. A backend that wants to index, count or route by job can, and input stops being restricted to object schemas. Deliveries are read in both shapes for one deploy, so messages enqueued by the one before it still run; the fallback comes out next.

    Reporting leaves the lifecycle. onEnd(ctx, status) runs once an ending is decided and before the delivery is settled — the barrier a report that must reach a service needs — and whatever it throws is recorded as job.hook_failed rather than sinking work that succeeded. That replaces an instanceof branch deciding on the app's behalf which reporting failures were worth a redelivery. The ping itself is @sdxc/jobs/uptime, a client answering with a Result, wired to nothing.

    monitorId becomes meta, unconstrained and inferred as written, and ctx.of(job) reads a delivery as one job — its parsed input and its meta, or null for another job's delivery — which is what gives a dispatcher-level hook the types a handler already has.

    BREAKING CHANGE: createJobDispatcher takes queue rather than send; dispatcher.queue and dispatcher.scheduled become deliverBatch and tick, reached through cloudflare.worker(dispatcher); job() takes meta rather than monitorId; the uptime option and ctx.monitorId are gone; and the wire format carries an envelope.

  • chore: upgrade Remix to 3.0.0-rc.2 The router now answers a method mismatch with 405 and an Allow header instead of falling through to the default handler, so the HEAD probes against POST-only routes assert 405 and the cross-origin POST to /api/subjects/:subjectId reads as a method refusal.

@sdxc/jwt

Republished because @sdxc/duration changed.

@sdxc/lazy-route

  • chore: upgrade Remix to 3.0.0-rc.2 The router now answers a method mismatch with 405 and an Allow header instead of falling through to the default handler, so the HEAD probes against POST-only routes assert 405 and the cross-origin POST to /api/subjects/:subjectId reads as a method refusal.

@sdxc/location

  • docs: build URLs with typed route helpers in a Remix v3 action

  • feat: open the package for publishing The Location class and its safe-redirect helpers are published to npm as @sdxc/location. It has no dependencies of its own, and its README is rewritten for a reader who can reach only the npm page.

@sdxc/logger

  • feat: open the package for publishing The wide-event logger and its router middleware are published to npm as @sdxc/logger. Its README is rewritten for a reader who can reach only the npm page.

    The exported CurrentLog context key now states its type. remix/router re-exports createContextKey without the ContextKey type it returns, so the inferred type could not be named in a declaration file and the publish build failed with TS2883. Neither bun run typecheck nor the tests catch that, because neither emits declarations.

  • feat: declare remix as an optional peer remix moves from a dependency to an optional peer, matching what @sdxc/auth settled on. Only the ./middleware export reaches remix/router at runtime, so a consumer of the logger alone no longer installs a Remix release candidate to get it, and one that does use the middleware states the version it is on.

    It stays a dev dependency, so the package's own tests and typecheck resolve it exactly as before.

  • chore: upgrade Remix to 3.0.0-rc.2 The router now answers a method mismatch with 405 and an Allow header instead of falling through to the default handler, so the HEAD probes against POST-only routes assert 405 and the cross-origin POST to /api/subjects/:subjectId reads as a method refusal.

@sdxc/rate-limit

  • feat!: require a key, and answer denials from this package key is now required on every registration. There is no default, because what a budget belongs to is the policy: too broad a key lets one caller spend another's budget, and a key the caller controls lets it mint fresh ones. The old default read a Cloudflare header, which quietly collapsed every caller into one bucket on any other runtime.

    tooManyRequests(decision, window, body?, init?) replaces the borrowed JSON helper and is exported. It fixes the status and writes the quota fields the decision supports, and adds no media type of its own, so a limited page answers HTML where a limited API answers JSON. A closed failure policy still refuses with a 429 carrying no quota fields, since an outage made no decision to report.

    Both changes drop a dependency: the package now needs only @sdxc/duration, @sdxc/logger and @sdxc/result. It is published to npm as @sdxc/rate-limit.

    BREAKING CHANGE: rateLimit({ ... }) requires key; pass the derivation the endpoint should count against, returning one shared bucket for a caller it cannot identify.

  • chore: upgrade Remix to 3.0.0-rc.2 The router now answers a method mismatch with 405 and an Allow header instead of falling through to the default handler, so the HEAD probes against POST-only routes assert 405 and the cross-origin POST to /api/subjects/:subjectId reads as a method refusal.

@sdxc/result

Republished because @sdxc/types changed.

@sdxc/rss

  • feat: open the package for publishing

@sdxc/sample

Republished because @sdxc/crypto changed.

@sdxc/spec

Republished because @sdxc/duration changed.

@sdxc/types

  • feat: add JSONSerialized, the shape a value reads back as Types the read side of a JSON boundary in terms of what was written, instead of widening to JSONValue and casting back: it applies toJSON, drops a property JSON cannot write, writes an unwritable array element as null, and keeps a tuple's length.

    It describes the shape rather than the value, so a cycle, NaN, and an inherited property remain the caller's business.

  • feat: add a cache contract with memory and Worker KV adapters Four methods over string keys, with the value's type taken per call, so one instance serves a namespace whatever mix of types goes into it. Values are constrained to JSONSerializable and read back as JSONSerialized<T>, so a hit and a miss answer with the same type and a Date is typed as the string it serializes to.

    MemoryCache holds serialized text against an injectable clock, so a test caches without a KV namespace and expires an entry without waiting for one. WorkerKVCache defers a put to waitUntil when given one, answering from the instance until it lands, so a write is readable as soon as it resolves and two writes to one key land in order.

    Both run the same conformance suite from @sdxc/cache/conformance, the KV one twice -- awaiting each write and deferring it -- so the two modes are held to answering identically. A store that cannot answer reads as a miss and warns on the invocation's log; what a loader throws and a value JSON cannot write reach the caller.

@sdxc/validate

  • docs: validate inside Remix v3 actions and controllers

  • feat: open the package for publishing Standard Schema validation is published to npm as @sdxc/validate. It reaches only @sdxc/result and @sdxc/types, both already in the release set.

    Its installation section now names the package itself before the schema library installed alongside it, and its cross-package links point at npm rather than at repository paths.

  • chore: upgrade Remix to 3.0.0-rc.2 The router now answers a method mismatch with 405 and an Allow header instead of falling through to the default handler, so the HEAD probes against POST-only routes assert 405 and the cross-origin POST to /api/subjects/:subjectId reads as a method refusal.

@sdxc/xml

  • feat: open the package for publishing

  • fix: serialize a whole document through stringify

Compare: https://github.com/sergiodxa/monorepo/compare/v2026.9.7...v2026.9.11

2026.9.7

On GitHub

@sdxc/lazy-route

  • feat: defer a route's module until a request reaches it lazy(() => import("./bookmarks")) maps a route without importing its controller, so a cold start loads only the modules the routes it actually serves need. The stand-in keeps the module's own type, so params, request context and route/module mismatches are still checked at the map call.

    Works for both handler shapes, since the router picks between them from the map target rather than the module: a single route reads handler, a route map reads actions. The middleware an action or controller declares is assembled after the module loads and runs ahead of the handler, in the order the router would have run it.

    Validating a controller's actions against its route map moves from startup to the first request to that route, since the actions are not there any earlier. See ADR-049.

  • feat: take guards to run ahead of the module's own A stand-in is an object rather than a function, so it cannot be the handler of an outer action object. A composition root that declares a route group's guards at the map call had no way to keep them there.

    lazy(load, middleware) runs those guards before whatever the module declares. They are typed as middleware with no context transform, so a middleware that publishes a context value is rejected: the loaded handler's type is the module's own and cannot grow to know a value declared at the map call.

  • feat: publish to npm Drops private: true, which is what makes a package public, and adds the metadata the guard requires of one: a description, a LICENSE.md, and the ✅ row in the root README package table.

    The README is rewritten to the public-package structure. It had been written for a reader inside the monorepo, with app-relative controller paths and a Tips section; an npm reader can open none of that, so the examples now use generic subjects and the sections follow Installation / Usage / API / Patterns / Versioning.

    No internal dependencies, so nothing else has to open to ship it.

Compare: https://github.com/sergiodxa/monorepo/compare/v2026.9.5...v2026.9.7

2026.9.5

On GitHub

@sdxc/crypto

  • docs: rewrite the README for npm readers Adds installation and license sections, expands each wrapper into the raw WebCrypto call it stands in for, and drops the repository links and the unpublished package the reference pointed at.

  • docs: explain the dated release versioning States that a version is its publish date, that any release may change an export, and that a dependent should pin one exact date.

  • feat: hash passwords with scrypt password.hash now derives with scrypt through node:crypto at ln=15, r=8, p=3 — 32 MiB of scratch memory — and writes $scrypt$ln=15,r=8,p=3$<salt>$<key>. scrypt is memory-hard, so an attacker pays for memory as well as time, which an iteration count alone never buys.

    Hashes in the previous $pbkdf2-sha256$ format no longer verify: verify returns UnsupportedAlgorithmError for them and needsRehash reports true, so a stored value from an earlier release has to be reset.

    Password hashing is the one part of this package that reaches past Web Crypto, which has no memory-hard derivation on any runtime. Node, Bun and Cloudflare Workers each implement node:crypto scrypt natively and produce identical bytes for the same parameters.

@sdxc/dates

  • docs: rewrite the README for npm readers Adds installation and license sections and expands the formatters into the Intl calls they wrap. Corrects a stale endOfDay output and the range the day-bounded query pattern describes, which is closed rather than half-open.

  • docs: explain the dated release versioning States that a version is its publish date, that any release may change an export, and that a dependent should pin one exact date.

@sdxc/duration

  • docs: rewrite the README for npm readers Adds installation and license sections, shows the millisecond arithmetic toMs and toSeconds replace, and documents that an amount may be zero or negative.

  • docs: explain the dated release versioning States that a version is its publish date, that any release may change an export, and that a dependent should pin one exact date.

@sdxc/i18n

  • feat: publish the package to npm The package drops private: true and gains the description npm renders on its page, so a release run builds it and ships it as @sdxc/i18n.

  • docs: rewrite the README for npm readers Adds installation and license sections, names which export comes from which of the three entry points, and gives every pattern its own imports so each one stands alone.

  • docs: explain the dated release versioning States that a version is its publish date, that any release may change an export, and that a dependent should pin one exact date.

@sdxc/jwt

  • docs: rewrite the README for npm readers Adds installation, versioning, license and author sections, documents the KeyStorage contract so a stranger can implement it, and drops the repository links and the unpublished package the reference pointed at.

@sdxc/result

  • docs: rewrite the README for npm readers Adds installation, versioning, license and author sections and expands each helper into the code it replaces. Corrects RetryError, which is returned inside a Failure rather than thrown, and drops the section that imported an unpublished package.

@sdxc/sample

  • docs: rewrite the README for npm readers Adds installation, versioning, license and author sections and compresses the fifteen namespaces to a description and a method list each. Corrects the PersonRecord field list and two generated example outputs against the source.

@sdxc/spec

  • docs: rewrite the README for npm readers Replaces the checkout-local CLI instructions with what an installed consumer runs, and adds installation, versioning, license and author sections. Keeps the language, capability and permission references, and states that the command runs on Bun.

@sdxc/types

  • docs: rewrite the README for npm readers Adds installation, versioning, license and author sections, replaces the internal examples with generic ones, and shows each type beside the longhand it stands in for. Documents JSONValue as a generic bound, which keeps the caller's shape while rejecting what JSON cannot carry.

  • feat: add JSONSerializable for the write side of a JSON boundary JSONValue names what JSON.parse hands back, so it rejects a Date — the round trip returns a string. That left no type for the other direction, where an object standing in for itself through toJSON is exactly what stringify accepts.

    JSONSerializable adds that branch and nothing else, so an API takes it where a value is written and keeps JSONValue where one is read back.

    Splits the package into one module per type, each with its own header and a type-level test beside it. Those tests assert through expectTypeOf, so the typecheck is what enforces them.

Compare: https://github.com/sergiodxa/monorepo/compare/v2026.9.4...v2026.9.5

2026.9.4

On GitHub

@sdxc/crypto

First release.

@sdxc/dates

First release.

@sdxc/duration

First release.

@sdxc/jwt

First release.

@sdxc/result

First release.

@sdxc/sample

First release.

@sdxc/spec

First release.

@sdxc/types

First release.