Repository navigation
v0.19.0
@kanzo-tech/ai
A field whose model fails says so, and tells you. A refused call or a broken stream used to end
as if the model had nothing to offer. Now the field shows the failure under itself — the error's
own message, or the new failed translation when it has none — and AssistProvider's new
onFailure(error: unknown) receives what was thrown, whole. A model that sends nothing for 30 s —
before its first word or between two — is cut off and arrives as an AiError (exported) whose
code is "ai/silent" and whose data.after is 30000. Branch on error.code in your failure
view; set translations.failed if you translate the field.
@kanzo-tech/auth
AuthErrorCode is spelled area/kind. Every code is renamed, in error.code, in the error
field of authRoutes' JSON answers and in the new ?code= of its redirects:
claims.no-subject→claims/no-subjectsession.absent→session/absentorganization.not-a-member→organization/not-a-memberorganization.invalid→organization/invalidcallback.state-mismatch→callback/state-mismatchcallback.nonce-mismatch→callback/nonce-mismatchtoken.exchange-failed→token/exchange-failed
Replace the dot with a slash wherever you compare against one. AuthError also takes an optional
data and ErrorOptions — new AuthError(code, message, data?, { cause }?) — and its cause is
set through ErrorOptions.
@kanzo-tech/auth
Every wait on the IdP or the BFF ends in 30 s, and says who did not answer. Three codes are new:
idp/silent (the IdP did not answer within data.after = 30000 ms), session/silent (/session or
/refresh did not, likewise) and idp/unreachable (the IdP could not be reached, or answered a
5xx or a page that is not OAuth). bffAuth's requests to /session and /refresh are cut at 30 s;
browserAuth sets oidc-client-ts's requestTimeoutInSeconds to 30, which also moves its silent
renewal from 10 s to 30 s.
browserAuth no longer reads every failed silent renewal or callback as signed out: it resolves
null only for the IdP's login_required, interaction_required, consent_required,
account_selection_required or invalid_grant, and for a callback URL whose state this tab does
not hold. Anything else rejects getSession() — idp/silent, idp/unreachable, or
token/exchange-failed for a refusal — with the library's error as cause. On the server,
relyingParty codes a discovery failure and an unanswered token request the same way where it used
to throw an uncoded error from begin and end, or token/exchange-failed from complete and
refresh; authRoutes answers them 502 or 504 on /refresh.
The timeout option on relyingParty and issuer is gone: the figure is fixed at 30 s. Delete it
from your config if you passed one.
@kanzo-tech/auth
A failed sign-in, callback or sign-out redirects to your problem page instead of answering JSON.
authRoutes' /signin, /callback and /signout answer an AuthError with a 302 to
problemPage — a new option, default /auth/problem — carrying the code:
/auth/problem?code=callback%2Fstate-mismatch. They used to answer a 400 JSON body (and /signout
a bare 500), which the person read in the address bar. Add a page at that path that renders the
code query parameter. authMiddleware keeps the same path public by default and takes the same
problemPage option: if you move the page, pass the new path to both, or a failed sign-in loops
back into signing in. A failure that is not an AuthError is still thrown to Next.
@kanzo-tech/auth
A session that cannot be read is "failed", not "anonymous". When getSession rejects — the
IdP or the session store is down, the network dropped — AuthProvider now settles on
status: "failed", and useSession() carries what was thrown, untouched, on a new error: unknown
field (undefined otherwise). It used to settle on "anonymous", which sent the person to sign in
over an outage. AuthStatus gains "failed": a switch over it that was exhaustive no longer is.
Render your problem view for "failed", keyed on error's code when it is an AuthError. Gate
draws neither its children nor its fallback while the status is "failed".
@kanzo-tech/auth
A session endpoint that fails is a failure, not "nobody is signed in". bffAuth().getSession()
used to resolve null for anything but a session; now only a 401 means no session. A 5xx, a 200
whose body is not a session, or a request that never reached the server rejects with an AuthError
coded session/unavailable — data.status holds the status when there was one, and cause what the
network threw. A failed read is not kept: the next getSession() asks again. A 5xx from /refresh
rejects the request auth.fetch was making with the same code, instead of ending the session.
On the server, a SessionStore that throws now fails as session/unavailable with the driver's
error as cause; authRoutes' /session answers it 503 with { error, message } instead of a
bare 500, and authProxy answers an outage 503, 502 or 504 instead of a 401.
@kanzo-tech/ui
A chart whose query fails says so, and tells you. A failed query used to leave a chart blank or
on its last render, a ChartStat on its skeleton and a ChartFilter on "Loading…" for good. Now
ChartRoot reads This chart could not be drawn. in its own frame, ChartStat shows a dash, and
ChartFilter says its values could not be read. MosaicProvider takes onFailure(error: unknown),
called with the thrown value from every chart, stat and input beneath it — pass it to your failure
view and branch on error.code. useChartQuery and useMosaicInput return it as error.
@kanzo-tech/mosaic
engine() always settles, and a failed boot is retried. A DuckDB worker that would not load
(a 404, a CSP refusal) used to leave engine() — and every chart and graph waiting on it — loading
forever. It now rejects with an EngineError whose code is "engine/unavailable"; a boot that
takes longer than 60 s rejects the same way, with data.after set to 60000. The failure is not
remembered: the next engine() boots again, so a dropped download no longer needs a reload.
engine({ signal }) ends your own wait when the signal aborts, without stopping the boot other
callers share. Key your failure screen on error.code; EngineError is exported.
@kanzo-tech/mosaic
engine().query(sql, { signal }) requires the signal. It is the shape fossil's Engine now
requires, so the engine you chart through stays the one a corpus opens into. Where you called
query(sql) with no options, pass one: query(sql, { signal: controller.signal }) — an abort
interrupts the running statement and rejects with signal.reason.
@kanzo-tech/graph
onFailure receives the value that was thrown, not its message. On GraphRoot and useGraph,
onFailure is now (error: unknown) => void. A corpus that will not open or a read that fails
arrives as fossil threw it, with its code and data; the graph's own failures are a GraphError,
now exported, whose code is "graph/no-webgl", "graph/context-lost", "graph/nothing-to-draw"
(no vertex type with a position) or "graph/untranslatable-filter" (a crossfilter
clause the corpus cannot be asked). Where you wrote
onFailure={setMessage} with a string state, write onFailure={setFailure} with unknown state, or
onFailure={(error) => …}, and branch on error.code; render error.message only as the fallback.
A GPU device that never comes up now fails. When the browser offers WebGL but cosmos.gl cannot
make a device, the canvas used to say "Loading the graph…" for good. After 10 s the root is now
failed and onFailure receives a GraphError coded "graph/no-webgl" with data.after of
10000. A lost WebGL context also sets status to "failed".
GraphInspector now tells a vertex the corpus does not have ("This vertex is not in the corpus.")
apart from a read that failed ("This vertex could not be read."); only the failure reaches
onFailure. When one of the graph's parallel reads fails, the others are cancelled.
A corpus promise must settle: the graph shows "Opening the corpus…" until it does and sets no
deadline on it.
@fossil-lang/corpus must be ^0.3.0-alpha.19. It is the first release whose open and reads
raise coded failures and whose Engine.query requires a signal. Bump it beside this release. A
corpus fossil cannot read is refused by open with fossil's own code (corpus/unsupported-format,
…), which onFailure receives as is.
@kanzo-tech/navigation
A guard that fails no longer traps the person on the page. When shouldBlockFn threw or its
promise rejected, the navigation was cancelled and never replayed: the click did nothing, and the
failure surfaced only as an unhandled rejection. Now the navigation goes ahead and useBlocker's new
onFailure(error: unknown) receives what was thrown, whole — pass it to your failure view and branch
on error.code.