Skip to content

Releases: sergiodxa/monorepo

2026.9.23

Choose a tag to compare

@github-actions github-actions released this 23 Sep 02:43
00c5691

@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 of required. 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/client reduces a ceremony to one call: Passkey.register,
    .authenticate and .autofill take the server's JSON options verbatim and
    return the response as JSON, reporting every outcome as a Result so 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/server is a stateless RelyingParty: register() and
    `authentica...

Read more

2026.9.17

Choose a tag to compare

@github-actions github-actions released this 17 Sep 02:46
d1e2d84

@sdxc/auth

Republished because @sdxc/crypto changed.

@sdxc/billing

Republished because @sdxc/crypto changed.

@sdxc/crypto

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

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

@sdxc/distill

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

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

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

    It gains the license file every published package carries.

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

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

@sdxc/feed

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

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

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

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

@sdxc/html

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

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

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

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

@sdxc/http

Republished because @sdxc/crypto changed.

@sdxc/mcp

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

@sdxc/opml

  • feat: read and write the folder a feed sits in
    An outline nested under another carries its nearest enclosing folder on
    read, and a document written back groups each folder's feeds under one
    outline with the unfiled ones after them.
  • chore: publish opml
    It was marked private, which is why the bootstrap answered that there was
    nothing else to publish: the filter drops a private package before npm is
    ever asked about it. It reaches only public packages, and a subscription
    list is a format other people read and write too.

@sdxc/pagination

Republished because @sdxc/crypto changed.

@sdxc/sample

Republished because @sdxc/crypto changed.

@sdxc/spec

Republished because @sdxc/html changed.

@sdxc/webhooks

Republished because @sdxc/crypto changed.

Compare: v2026.9.16...v2026.9.17

2026.9.16

Choose a tag to compare

@github-actions github-actions released this 16 Sep 02:42
704fb5a

@sdxc/auth

Republished because @sdxc/crypto changed.

@sdxc/billing

Republished because @sdxc/crypto changed.

@sdxc/cloudflare-mocks

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

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

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

@sdxc/crypto

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

@sdxc/data-table-sqlstorage

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

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

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

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

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

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

@sdxc/feed

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

@sdxc/flags

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

@sdxc/flags-engine

Republished because @sdxc/flags changed.

@sdxc/html

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

@sdxc/http

Republished because @sdxc/crypto changed.

@sdxc/jobs

Republished because @sdxc/validate changed.

@sdxc/json-feed

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

@sdxc/lazy-route

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

@sdxc/pagination

Republished because @sdxc/crypto changed.

@sdxc/rate-limit

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

@sdxc/sample

Republished because @sdxc/crypto changed.

@sdxc/spec

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

@sdxc/validate

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

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

@sdxc/webhooks

Republished because @sdxc/crypto changed.

Compare: v2026.9.15...v2026.9.16

2026.9.15

Choose a tag to compare

@github-actions github-actions released this 15 Sep 02:49
d56f75a

@sdxc/atom

Republished because @sdxc/result changed.

@sdxc/auth

Republished because @sdxc/crypto changed.

@sdxc/billing

Republished because @sdxc/crypto changed.

@sdxc/cache

Republished because @sdxc/duration changed.

@sdxc/cron

Republished because @sdxc/duration changed.

@sdxc/crypto

Republished because @sdxc/result changed.

@sdxc/dates

Republished because @sdxc/duration changed.

@sdxc/duration

Republished because @sdxc/result changed.

@sdxc/feed

Republished because @sdxc/atom changed.

@sdxc/flags

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

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

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

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

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

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

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

@sdxc/flags-engine

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

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

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

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

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

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

@sdxc/highlight

Republished because @sdxc/markdown changed.

@sdxc/html

Republished because @sdxc/result changed.

@sdxc/http

Republished because @sdxc/crypto changed.

@sdxc/jobs

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

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

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

@sdxc/jwt

Republished because @sdxc/duration changed.

@sdxc/mail

Republished because @sdxc/highlight changed.

@sdxc/markdown

Republished because @sdxc/result changed.

@sdxc/mcp

Republished because @sdxc/result changed.

@sdxc/pagination

Republished because @sdxc/crypto changed.

@sdxc/rate-limit

Republished because @sdxc/duration changed.

@sdxc/result

Republished because @sdxc/types changed.

@sdxc/rss

Republished because @sdxc/result changed.

@sdxc/sample

Republished because @sdxc/crypto changed.

@sdxc/semver

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

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

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

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

@sdxc/session-storage-kv

Republished because @sdxc/duration changed.

@sdxc/sitemap

Republished because @sdxc/result changed.

@sdxc/spec

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

@sdxc/types

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

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

@sdxc/validate

Republished because @sdxc/result changed.

@sdxc/webhooks

Republished because @sdxc/crypto changed.

@sdxc/workers-cache

Republished because @sdxc/result changed.

@sdxc/xml

Republished because @sdxc/result changed.

@sdxc/yaml

Republished because @sdxc/result changed.

Compare: v2026.9.14...v2026.9.15

2026.9.14

Choose a tag to compare

@github-actions github-actions released this 14 Sep 02:45
069de2c

@sdxc/api-client

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

@sdxc/billing

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

@sdxc/cache

  • chore: make the package public, with a README written for npm
  • fix: declare the result dependency its sources import
    The index, both adapters and the conformance entry import @sdxc/result, which the
    manifest omitted. An installed consumer would have failed to resolve it.

@sdxc/cloudflare-mocks

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

@sdxc/data-table-d1

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

@sdxc/data-table-sqlstorage

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

@sdxc/get-client-ip

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

@sdxc/highlight

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

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

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

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

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

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

@sdxc/hostname

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

@sdxc/http

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

@sdxc/icons

  • docs: point the related-packages row at the markdown renderer entry
  • chore: make the package public, with a README written for npm
  • fix: scope the package tsconfig to src
    The release build compiles a package with src as the rootDir, so the codegen
    script the include pattern reached failed the build with TS6059. The script
    keeps its coverage under its own tsconfig in the directory it lives in.

@sdxc/mail

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

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

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

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

@sdxc/markdown

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

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

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

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

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

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

@sdxc/mcp

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

@sdxc/pagination

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

@sdxc/response

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

@sdxc/seo

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

@sdxc/server-timing

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

@sdxc/session-storage-kv

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

@sdxc/strings

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

@sdxc/typeid

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

@sdxc/u

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

@sdxc/ui

  • chore: make the package public, with a README written for npm
    The previous README documented three exports that do not exist, the wrong theme
    variable contract and an invented radius scale; all three are corrected against the
    source.
  • fix: name the mixin type the exported functions return
    The declaration emit wrote the return type of the two functions that return a
    raw() mixin as a relative path into the u package source, which the release
    build rejects as reaching outside dist/. The type ships from the u entrypoint
    now, and the functions that return one write it out.

@sdxc/uuid

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

@sdxc/webhooks

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

@sdxc/workers-cache

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

@sdxc/yaml

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

Compare: v2026.9.12...v2026.9.14

2026.9.12

Choose a tag to compare

@github-actions github-actions released this 12 Sep 02:27
5db15aa

@sdxc/html

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

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

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

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

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

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

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

@sdxc/sitemap

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

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

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

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

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

@sdxc/spec

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Compare: v2026.9.11...v2026.9.12

2026.9.11

Choose a tag to compare

@github-actions github-actions released this 11 Sep 02:22
3aedbf2

@sdxc/atom

  • feat: open the package for publishing

@sdxc/auth

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

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

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

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

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

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

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

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

@sdxc/catch-response-middleware

  • docs: state the throw redirect affordance directly
  • feat: open the package for publishing
    The middleware that turns a thrown Response into the request's response is published
    to npm as @sdxc/catch-response-middleware. Its only dependency is remix itself, and
    its README is rewritten for a reader who can reach only the npm page.
  • chore: upgrade Remix to 3.0.0-rc.2
    The router now answers a method mismatch with 405 and an Allow header
    instead of falling through to the default handler, so the HEAD probes
    against POST-only routes assert 405 and the cross-origin POST to
    /api/subjects/:subjectId reads as a method refusal.

@sdxc/cron

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

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

@sdxc/crypto

Republished because @sdxc/result changed.

@sdxc/dates

Republished because @sdxc/duration changed.

@sdxc/duration

Republished because @sdxc/result changed.

@sdxc/feed

  • feat: open the package for publishing

@sdxc/i18n

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

@sdxc/jobs

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

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

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

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

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

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

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

@sdxc/jwt

Republished because @sdxc/duration changed.

@sdxc/lazy-route

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

@sdxc/location

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

@sdxc/logger

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

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

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

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

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

@sdxc/rate-limit

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

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

    Both changes drop a dependency: the ...

Read more

2026.9.7

Choose a tag to compare

@github-actions github-actions released this 07 Sep 02:10
b34ad16

@sdxc/lazy-route

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

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

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

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

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

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

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

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

Compare: v2026.9.5...v2026.9.7

2026.9.5

Choose a tag to compare

@github-actions github-actions released this 05 Sep 02:18
d03dcaf

@sdxc/crypto

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

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

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

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

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

@sdxc/dates

  • docs: rewrite the README for npm readers
    Adds installation and license sections and expands the formatters into the
    Intl calls they wrap. Corrects a stale endOfDay output and the range the
    day-bounded query pattern describes, which is closed rather than half-open.
  • docs: explain the dated release versioning
    States that a version is its publish date, that any release may change an
    export, and that a dependent should pin one exact date.

@sdxc/duration

  • docs: rewrite the README for npm readers
    Adds installation and license sections, shows the millisecond arithmetic
    toMs and toSeconds replace, and documents that an amount may be zero or
    negative.
  • docs: explain the dated release versioning
    States that a version is its publish date, that any release may change an
    export, and that a dependent should pin one exact date.

@sdxc/i18n

  • feat: publish the package to npm
    The package drops private: true and gains the description npm renders on
    its page, so a release run builds it and ships it as @sdxc/i18n.
  • docs: rewrite the README for npm readers
    Adds installation and license sections, names which export comes from which
    of the three entry points, and gives every pattern its own imports so each
    one stands alone.
  • docs: explain the dated release versioning
    States that a version is its publish date, that any release may change an
    export, and that a dependent should pin one exact date.

@sdxc/jwt

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

@sdxc/result

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

@sdxc/sample

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

@sdxc/spec

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

@sdxc/types

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

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

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

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

Compare: v2026.9.4...v2026.9.5

2026.9.4

Choose a tag to compare

@github-actions github-actions released this 04 Sep 05:24
Immutable release. Only release title and notes can be modified.
d56b596

@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.