Changelog
Every dated release of the collection, newest first, with its notes.
2026.9.17
@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
infostring, 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 noTextEncoderof 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.
extractandextractFrombecomedistillanddistillFrom, 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.
selectHubranks 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.
extractandextractFrombecomedistillanddistillFrom, 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
@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()andshutdown()both answer with aResult, butready()is declaredPromise<void>— as theFlagsinterface printed a few lines above it already showed.setProviderinitializes the provider it registers and hands its caller that outcome, soready()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.fetchis 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
keyhas 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.tsexports seven plugin factories and its own header comment lists all seven; the README named four. ThecreateHttpPluginexample also imported from/workerswhile 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.
inputalso takes any JSON value, not only a plain object, and a schema that validates the rawFormData/URLSearchParamssource rather than a flattened object —remix/data-schema/form-data'sobject()— 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
@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 rewrotedependenciesalone, so aworkspace:range inpeerDependenciesreached 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/flagsshipped the API an application evaluates through and theProvidercontract 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
reasonalone 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.EngineProvideris 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
deadLetterQueueandonInvalidwere 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 answersdead-letterfor a message it refuses, and still records a dead-lettered batch as a job log that endeddead_letter. It is now told that a batch is dead-lettered instead of deciding it from a name, andapplymay 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 leadingva git tag or a user agent carries, and reporting anything else as aResultfailure 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, so1.2.4-rc.1satisfies^ 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,queryandfragmentparsed their assertion insideobserveValue, after theagent-browsercall, 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
@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/markdownexportshighlight, aMarkdown.walkvisitor holding onecodehandler, 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
tokensfield belongs here rather than to the format: the entry declares it onMarkdown.Codethrough module augmentation, so@sdxc/markdownhas 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/markdocis 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'sMarkdowncomponent takes adocumentrather 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
switchonnode.typethe 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/markdownreads and writes markdown itself, over a first-party AST. Every node is a plain JSON-serializable object with atypediscriminator and aposition, 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 asunknown.Markdown.parsereads the frontmatter block and the body in one traversal, andMarkdown.frontmatterstops after the block so an index over a hundred posts reads a hundred titles without parsing a hundred bodies.Markdown.stringifywrites a document back, normalized, so a round trip is idempotent and the output is a fixed point of the repository's formatter.Markdown.walkis 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;
/plaingives text,/htmlgives static HTML carryingmd-classes to style, and/remixgivesremix/uinodes.@sdxc/markdown/serverand@sdxc/markdown/clientare gone, and so isMarkdownView— a view callstoRemixand 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
@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 inlinedisplay/visibilitydeclaration hide an element, while a stylesheet stays unread.feat: fetch a page, and let every match scope its own lookups
HTML.fetchasks fortext/html, parses the body when that is what arrived, and reports a rejected request, an error status or another content type as anHTMLFetchErrornaming 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,strandspecjoin 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/htmldescribes 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 anElementof 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.parsetakes a parsed XML document andSitemap.fetchretrieves one, both answering with the same instanceappendbuilds, 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
200fails 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 akindof"index", andtoString()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
fixtureleaves 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.formatgives the language string composition as a tool;spec.run_id,spec.attemptandspec.noncegive a suite the one value that must not reproduce across runs, while everysampledraw stays deterministic.htmlreads a served page, and shares one addressing vocabulary withbrowserso a document is addressed the same way whether or not a browser is needed.Named bases resolve relative targets,
dbtakes its own grant and named connections, and the runner gainssetup/teardown,skip, retries with aflakycount, 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 fillmoves no range input and fires nochange, sobrowser.filldrives 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
baseskey,on "name", and a composition with ADR-007 that keeps configuration from ever implying authority. Both sides now say so.ADR-007 gains the
dbfamily 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'sDATABASE_URLgating 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 successfulsetup, and--artifactsgain 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-callis 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,strandspecjoin 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/htmldescribes 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 anElementof 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.
createHttpPluginnow 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 runsets 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
@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 aLICENSE.mdand 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/loggerand@sdxc/rate-limit, over the already-published@sdxc/crypto,@sdxc/duration,@sdxc/jwtand@sdxc/result.RelyingParty.Options.rateLimitis now aRateLimit, pairing the adapter with a requiredkey(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.ServiceClientkeeps a bare adapter, since it counts against its own client id.BREAKING CHANGE:
rateLimit: adapterbecomesrateLimit: { 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
Responseinto the request's response is published to npm as@sdxc/catch-response-middleware. Its only dependency isremixitself, 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/durationand@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 callingack()andretry()on a Cloudflare message, the dispatcher was entered through aMessageBatch, and cron existed only because the platform firedscheduledwith an expression to match. ADR-054 records the design; this is it.The lifecycle now decides an ending and answers with a
Settlementfor someone else to apply, sodeliver()anddeliverBatch()name no platform and every backend becomes a translation outside that seam.deliverBatchtakes anapplycallback 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.JobQueueis the port both adapters implement:@sdxc/jobs/cloudflarefor Queues,@sdxc/jobs/memoryfor tests and for anything running without a platform, and@sdxc/jobs/conformanceis 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 astype. A backend that wants to index, count or route by job can, andinputstops 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 asjob.hook_failedrather than sinking work that succeeded. That replaces aninstanceofbranch deciding on the app's behalf which reporting failures were worth a redelivery. The ping itself is@sdxc/jobs/uptime, a client answering with aResult, wired to nothing.monitorIdbecomesmeta, unconstrained and inferred as written, andctx.of(job)reads a delivery as one job — its parsed input and its meta, ornullfor another job's delivery — which is what gives a dispatcher-level hook the types a handler already has.BREAKING CHANGE:
createJobDispatchertakesqueuerather thansend;dispatcher.queueanddispatcher.scheduledbecomedeliverBatchandtick, reached throughcloudflare.worker(dispatcher);job()takesmetarather thanmonitorId; theuptimeoption andctx.monitorIdare 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
Locationclass 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
CurrentLogcontext key now states its type.remix/routerre-exportscreateContextKeywithout theContextKeytype it returns, so the inferred type could not be named in a declaration file and the publish build failed with TS2883. Neitherbun run typechecknor the tests catch that, because neither emits declarations.feat: declare remix as an optional peer
remixmoves from a dependency to an optional peer, matching what@sdxc/authsettled on. Only the./middlewareexport reachesremix/routerat 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
keyis 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 a429carrying no quota fields, since an outage made no decision to report.Both changes drop a dependency: the package now needs only
@sdxc/duration,@sdxc/loggerand@sdxc/result. It is published to npm as@sdxc/rate-limit.BREAKING CHANGE:
rateLimit({ ... })requireskey; 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/resultand@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
@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 readsactions. 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
handlerof 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
@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.hashnow derives with scrypt throughnode:cryptoat 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:verifyreturnsUnsupportedAlgorithmErrorfor them andneedsRehashreports 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:cryptoscrypt 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: trueand 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
@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.