Releases: sergiodxa/monorepo
Release list
2026.9.23
@sdxc/atom
Republished because @sdxc/xml changed.
@sdxc/auth
-
feat: support a non-JWT access token in RelyingParty
OAuth2 states no shape for an access token at all — RFC 9068 is this
package's own issuer's choice, not a rule every issuer follows — so a
third-party provider's access token is commonly an opaque string, not a
JWT. RelyingParty.callback and AuthSession.accessToken both assumed
otherwise, unconditionally and uncaught: decoding a real Google, Microsoft,
GitLab, LinkedIn or Apple access token this way throws JWTInvalid, since
none of them issue one that decodes as a JWT for the scopes an ordinary
sign-in requests.AccessToken.tryDecode(raw) answers the decoded token or null instead of
throwing, for a caller that doesn't already know it's holding a JWT.
decode and verify stay strict, since a caller naming either already knows
what it has (this package's own resource-server and service-client roles,
reading a token this package minted for itself).Grant.accessToken and AuthSession#accessToken are now AccessToken | null.
Grant gains accessTokenRaw (the token exactly as the issuer answered it,
whatever its shape) and expiresAt (from the token response's own
expires_in, seconds since the epoch) — both answer the same way whether or
not the access token happens to be a JWT, so a caller that only needs to
hold the token and know when it expires never has to touch the decoded
form. AuthSession's own memoization was fixed alongside this: null now
means "decoded and absent," not "not yet decoded," so an opaque access
token doesn't re-attempt decoding on every read.Updates auth-saas's connection-sign-in.ts (ADR-027) to use the new,
non-throwing path: the try/catch that previously turned a JWTInvalid crash
into a clean refusal is gone, since there's nothing to catch anymore, and
the sealed connection identity's token expiry now reads grant.expiresAt
rather than reaching for a JWT claim that may not exist. Real sign-in
against every catalog provider — Google, Apple, Microsoft, GitLab and
LinkedIn's real, opaque access tokens included — no longer depends on the
provider happening to issue a JWT. -
feat: tenant-scoped management API surface on ManagementClient
Extends ManagementClient with a parallel, tenant-scoped surface for a
multi-tenant provider's own management API — auth-saas's, whose design names
this exact class, configured with a baseUrl and resources for the API's own
origin, as its administrative role. The existing single-tenant
fetchSubjectById/Subject/SUBJECT_SCHEMA is untouched.Every new method calls one of three shared helpers instead of repeating
fetch/parse boilerplate: #send (token, request, non-2xx classification),
#decode (JSON body against a schema), #call and #page (the whole-response
and keyset-list wrappers over both). A non-2xx response declaring
application/problem+json decodes into a new ManagementProblem carrying every
RFC 9457 field (type, title, status, detail, instance, errors); anything
else falls back to the existing flat ManagementError. Options gains
apiVersion, sent as X-API-Version on every call, and apiVersionReceived
echoes back what the response carried.Covers subjects (create, read, update, block, unblock, delete, add/verify/
remove identifier), credentials and sessions (list, revoke one or all,
revoke a passkey, force a password reset, reset a second factor), clients
(list, register, update, rotate/revoke secret, disable, delete), grants
(list, revoke), audit events (a filtered page), and tenants/members/domains
(read, list/invite/change-role/remove a member, list/attach a domain, read
its verification, update the MFA policy) — every wire shape cross-checked
against the real operation it fronts rather than guessed, catching a few
real mismatches along the way: verifying an identifier takes no subject id
(the ticket alone resolves it), grants and their revocation are
subject-scoped rather than tenant-wide, and membership/domain listing
answers a plain array since neither is paginated server-side.Deliberately not built, because nothing on the other side exists to call:
listing subjects, defining a scope or assigning a role, API keys, webhook
endpoints, and import/export runs.Exports parseLinkHeader/serializeLinkHeader/LinkValue from
@sdxc/pagination's public entry (previously internal to link.ts) so this
package can read a keyset list's Link header without a second RFC 8288
implementation; @sdxc/auth takes it as a new dependency.
@sdxc/billing
Republished because @sdxc/crypto changed.
@sdxc/crypto
-
feat: TOTP second factor and recovery codes, mechanism (ADR-026 pass 1)
The tenant-object side of a second factor, independent of the mailbox that
already recovers a password: enrolment, activation, recovery codes, removal,
an administrator reset, and trusted devices. Nothing yet demands this factor
during sign-in or at the authorization endpoint — that's a second pass, once
this lands.database/totp.ts holds five tables (totp_enrolments, totp_factors,
totp_claims, recovery_codes, trusted_devices) and the operations over them.
beginTotpEnrolment mints and seals a secret, answering the otpauth:// URI and
setup key once — nothing afterwards ever returns the secret again.
activateTotpFactor spends the enrolment unconditionally (a wrong code is a
failed setup, not a retryable one) and only on a verified code writes the
factor and mints ten recovery codes, replacing any prior factor and clearing
every trusted device, since a second authenticator is a second scan of the
same QR code rather than a second credential. Recovery codes are single-use,
found by digest the way a session token already is; regenerating replaces
the whole set rather than leaving a half-old one nobody can reason about.
removeTotpFactor requires proof first and refuses outright under a tenant
policy ofrequired. resetSecondFactor is the administrator path: strips
everything, revokes every session, marks the subject as owing a fresh
enrolment, and answers the address to notify — mailing it is left to a
Worker-side caller, since a tenant object has no mail client of its own.The secret is sealed at rest under an AES-GCM key imported once per isolate
and cached, the same lazy-import pattern the DAU cap's own enforcement read
already uses. Adds a Base32 codec to @sdxc/crypto (mirroring its existing
Base64/Base64Url/Hex classes) since recovery codes need the same alphabet
TOTP secrets already use and nothing exported it.The replay guard (totp_claims) and the two flows that actually demand this
factor mid-sign-in — completeSecondFactor and the authorization endpoint's
step-up outcome — are the next pass's job; this one only builds what they'll
call.
@sdxc/feed
Republished because @sdxc/atom changed.
@sdxc/http
Republished because @sdxc/crypto changed.
@sdxc/opml
Republished because @sdxc/xml changed.
@sdxc/pagination
-
feat: tenant-scoped management API surface on ManagementClient
Extends ManagementClient with a parallel, tenant-scoped surface for a
multi-tenant provider's own management API — auth-saas's, whose design names
this exact class, configured with a baseUrl and resources for the API's own
origin, as its administrative role. The existing single-tenant
fetchSubjectById/Subject/SUBJECT_SCHEMA is untouched.Every new method calls one of three shared helpers instead of repeating
fetch/parse boilerplate: #send (token, request, non-2xx classification),
#decode (JSON body against a schema), #call and #page (the whole-response
and keyset-list wrappers over both). A non-2xx response declaring
application/problem+json decodes into a new ManagementProblem carrying every
RFC 9457 field (type, title, status, detail, instance, errors); anything
else falls back to the existing flat ManagementError. Options gains
apiVersion, sent as X-API-Version on every call, and apiVersionReceived
echoes back what the response carried.Covers subjects (create, read, update, block, unblock, delete, add/verify/
remove identifier), credentials and sessions (list, revoke one or all,
revoke a passkey, force a password reset, reset a second factor), clients
(list, register, update, rotate/revoke secret, disable, delete), grants
(list, revoke), audit events (a filtered page), and tenants/members/domains
(read, list/invite/change-role/remove a member, list/attach a domain, read
its verification, update the MFA policy) — every wire shape cross-checked
against the real operation it fronts rather than guessed, catching a few
real mismatches along the way: verifying an identifier takes no subject id
(the ticket alone resolves it), grants and their revocation are
subject-scoped rather than tenant-wide, and membership/domain listing
answers a plain array since neither is paginated server-side.Deliberately not built, because nothing on the other side exists to call:
listing subjects, defining a scope or assigning a role, API keys, webhook
endpoints, and import/export runs.Exports parseLinkHeader/serializeLinkHeader/LinkValue from
@sdxc/pagination's public entry (previously internal to link.ts) so this
package can read a keyset list's Link header without a second RFC 8288
implementation; @sdxc/auth takes it as a new dependency.
@sdxc/passkey
-
feat: add @sdxc/passkey, WebAuthn for the browser and the server
Two entry points over one implementation of WebAuthn Level 3.@sdxc/passkey/clientreduces a ceremony to one call:Passkey.register,
.authenticateand.autofilltake the server's JSON options verbatim and
return the response as JSON, reporting every outcome as aResultso a
dismissed prompt is a value to branch on rather than an exception to catch.
Only one ceremony is ever open, so an autofill prompt and a button no longer
cancel each other by accident.@sdxc/passkey/serveris a statelessRelyingParty:register()and
`authentica...
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, aninfostring, 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: 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 fornode: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 claimedready()andshutdown()both answer with aResult, but
ready()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, andcreateController()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: 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 rewrotedependencies
alone, 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
butexists.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 readingreasonalone 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 leading
va git tag or a user agent carries, and reporting anything else as a
Resultfailure 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,queryandfragment
parsed their assertion insideobserveValue, after theagent-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: 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 on
Markdown.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 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 asunknown.Markdown.parsereads the frontmatter block and the body in one traversal, and
Markdown.frontmatterstops 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.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 calls
toRemixand 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: 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
Removesprivate, 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 a200fails 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", 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
Removesprivate, 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, sofixtureleaves 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.html
reads 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 a
flakycount, 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 thebaseskey,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_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 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: 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 a
LICENSE.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: adapterbecomes
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 thrownResponseinto 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 insrc/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 as
type. 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.queue
anddispatcher.scheduledbecomedeliverBatchandtick, reached through
cloudflare.worker(dispatcher);job()takesmetarather thanmonitorId; theuptime
option 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
TheLocationclass 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-exports
createContextKeywithout theContextKeytype it returns, so the inferred type could
not be named in a declaration file and the publish build failed with TS2883. Neither
bun 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 ...
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 readshandler, 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
Dropsprivate: 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: 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:verify
returnsUnsupportedAlgorithmErrorfor 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 implementnode: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 dropsprivate: 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: 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.