Repository navigation
v0.21.0
Authentication through Yielded Auth. foldkit-auth 0.1.0 is new;
foldkit-remote 0.18.0, foldkit-remote-server 0.14.0, foldkit-sync
0.12.0, foldkit-guard 0.3.0, foldkit-agent-mcp 0.7.0, foldkit-ssr 0.7.0,
foldkit-site 0.4.0 and foldkit-plus 0.3.0 for the changes below, and
foldkit-primitives 0.10.2 for a corrected doc comment; every package that
pins one of them republishes with a patch to re-pin.
Five changes can break code written for 0.20:
serveFetch's anddefineDocumentHost'sresolvePrincipalreturns an
Effect, refusing withRemoteUnauthenticated; a resolver that throws is a
500, where it was a 401 carrying its message.foldkit-agent-mcp'sauthenticatereturns an Effect refusing with
AgentMcp.unauthenticated(), where it returned the principal orundefined.Guards.declarerequiresunguarded: 'allow' | 'deny'.SSR.entrythrows for a plan with a server fallback and noorigins.Remote.httpreads every 401 asUnauthenticatedand every 403 as
Forbidden, whatever the body says, so a view that compared other tags for
them sees these instead.
Added
-
foldkit-sync/journal:documentHost, a document's Sync host for any
runtime. It answers an upgrade request before a socket opens: refused
with theResponseto send (401 for a refused principal, 500 for a
resolver, layer or journal that broke), or accepted withservefor the
opened socket andabandonfor one that never opens, withrevalidate,
opens,admitandsettleasdefineDocumentHosthas them, and
layerrequired exactly when the resolver needs a service.defineDocumentHostis now built on it,
and a runtime withoutWebSocketPairis a 500 that resolves no one. -
foldkit-auth: the client repeats whom it was answered as.
AuthServer.answered(config)(the resolver's config, with itskey) is
serveFetch'sheaders, naming each answer's principal in
x-foldkit-subject;believing()fromfoldkit-authis afetchfor
Remote.http/httpWithLivethat repeats it, so once another tab switches
account a read is refused withSUBJECT_CHANGEDrather than answered as
the new account. Any 401 makes it believe no one, and an answer to a
request sent before it (or beforeforget(), for this tab's own sign-in
and sign-out) is not believed. The subject is URI-encoded. -
foldkit-remote-server:serveFetchtakesheaders: principal => …,
added to every answer to that principal, a live stream's included. -
foldkit-auth(early, 0.1.0): a Yielded Auth session as the principal.
AuthServer.resolver({ auth, http, principal })fromfoldkit-auth/server
is aresolvePrincipalforserveFetch: it reads the session cookie Yielded
set (named from the sameHttp.makeoptions), verifies it with the
application'sAuthservice, and builds the principal with the
application'sprincipal. No cookie is a visitor; a cookie on a request
without a trustedOriginand Yielded's CSRF header, a cookie that no
longer verifies, two session cookies, or anx-foldkit-subjectheader
naming someone else is a 401Unauthenticated;httpnaming something
that is not an origin throws at once. Withrequests: 'browser'it serves
what a browser sends on the page's behalf,defineDocumentHost's upgrade
or a posted form, where a trustedOriginalone vouches for the cookie,
since neither carries a CSRF header;SUBJECT_HEADERandsubjectSaid,
fromfoldkit-auth, are what a client sends. Also re-exported as
foldkit-plus/authandfoldkit-plus/auth/server, an optional peer. -
foldkit-sync:defineDocumentHosttakeslayer: env => Layerfor its resolver.
resolvePrincipalmay need a service (a session store), whichlayer
provides; it is required exactly when the resolver needs one, built per
upgrade, kept while the socket is open forrevalidate, and closed at once
for a refused upgrade. -
foldkit-site:access, who may be shown a page. A node'saccess: { allow, otherwise }covers it and the nodes below it;
Site.accessOf(site, route, who)answersAccess.Allowed,Pending
whilewhois unknown, orDenied({ url })withotherwise's address for
what was asked. Presentation for the client and a server's 303; the server
guards the data regardless. Anotherwisewhose target the same rule
covers throws (a sign-in page mounted under it would loop with a growing
returnTo), and a rule in a node's options names its reader's type. -
examples/auth: Yielded Auth end to end. Password sign-in and
sign-out through Yielded's own client and routes, a guarded notes list
(an admin reads every note, a member their own, a visitor none),Mefor
"signed in as" and an admin line, and a change of principal carried to the
other tabs; a notes pageSite.accesssends a visitor away from, server
rendered as the request's reader; a Sync board whose socket is opened
with the session cookie, served on Node bydocumentHost; unit, mutation
and Chromium e2e tests, the e2e across two tabs and a server restart. -
foldkit-remote:RemoteData.denied(error), the denial a failure
was, aDenied(UnauthenticatedorForbidden, with the server's
reason; not a mutation'srefusal), typed,
or none for any other failure: aRemoteError's_tagis open, so a view
had to compare strings to tell "sign in again" from "could not load". -
foldkit-guard:Me, the visitor as an Entity.Guards.me(G, { interactions })declares one row,me, that everyone reads and no one
writes:signedIn,key(the principal's declaredkey) andmayfor the
named interactions;GuardServer.me(Me, Policy)serves it, counts for
validateas applying its rules, and refuses a Policy of another set when
it is made.Guards.anyoneis the rule everyone passes, a visitor
included, for a public surface underunguarded: 'deny': a surface naming
it restricts no one, for Sync documents and shared fields too, and
Guards.allnow takes at least one rule.Policy.keyisdeclare'skey
(G.key), for a seam holding the Policy.AuthServer.resolvertakes
key: Policy.keyto comparex-foldkit-subjectwith it. -
foldkit-ssr: a posted form's own resources.SSR.entrytakes
resources: request => LayerandSSR.handleresources: Layer, built
once for the post and provided to the Commands its fallback runs beside the
config'sresources; where both provide a service, the post's is used, so
a Command reads as who posted.SSR.entry'soriginsnames where a form
may be posted from: any otherOriginis a 403 before the body is read or
flagsandresourcesrun (Origin: nullcounts as the site's own when
Sec-Fetch-Sitesayssame-origin), and a plan with a server fallback
made into an entry withoutoriginsthrows, since a post runsflagsand
everyresourcesas whoever posted. -
serveFetchanddefineDocumentHosttakerevalidate: { every, key? }. While a live stream or a socket is open, the resolver is asked again,
of the request it opened with: a refusal ends the stream with
Unauthenticated(anevent: errorframe the client reads back as it) or
closes the socket with 4401, and a principal whosekey(asliveHub's,
Policy.keyOfwith guards) changed ends the
stream as a live error, or closes the socket with 1012, to be opened again
as the new principal. A revoked session stops being served within one
interval; the clients do not yet tell these closes apart, so a replica
reconnects after a 4401 too, and is refused at the upgrade. -
foldkit-guard:Policy.provide(context), a Policy whose bodies have
the services they need provided, for the read seams
(GuardDrizzle.visible,GuardServer.memory,GuardSync), which take a
Policy needing nothing more. A seam built from it counts as built from the
Policy it came from, forGuardServer.validate. -
foldkit-remote:RemoteUnauthenticated(_tag: 'Unauthenticated',
with an optionalreason), the refusal of a caller who could not be told,
besideRemoteForbidden;RemoteJsonAnswercarries it as
unauthenticated: { reason? }.foldkit-remote-server's
RemoteServer.unauthenticated(reason?)makes one. -
foldkit-remote: every call kind fails withUnauthenticatedon a
401. Reads, queries, mutations and live streams declare it in their RPC
errors andRemoteClient's types;Remote.httpandliveFetchread a 401,
Remote's own (with itsreason) or one from middleware in front of it, as
it, andData.mutation,ReadFailedandQueryFailedcarry
error._tag: 'Unauthenticated'.Data.satisfyandprefetchcan fail with
it, for a server render to answer as it answers a lost session.
foldkit-remote-server'sanswersends a handler's as a 401, and
serveFetchends a live stream with it as anevent: errorframe the
client reads back.
Changed
-
foldkit-agent-mcp:authenticateis an Effect.authenticate: request => Effect<Principal, Unauthenticated>, so it may ask a token store;
where it returnedPrincipal | undefined. Failing with
AgentMcp.unauthenticated(reason?)(or Remote'sRemoteUnauthenticated,
of the same shape) is a 401 carryingreason; a defect or a throw is a 500
that says nothing to the caller and is logged. Wrap
a plain lookup:request => Option.match(lookup(request), { onNone: () => Effect.fail(AgentMcp.unauthenticated()), onSome: Effect.succeed }). -
foldkit-guard:Guards.declarerequiresunguarded. What a guarded
Entity's surface with no rules decides is written down rather than
defaulted to'allow'. Passunguarded: 'allow'to keep what an omitted
one did;'deny'refuses such a surface. Anything else, an omission from
a caller without types included, throws atdeclare. -
serveFetchanddefineDocumentHostresolve the principal with an
Effect.resolvePrincipal: (request, env) => Effect<P, RemoteUnauthenticated, RP>, whereserveFetch'slayerprovidesRPtoo,
so a resolver can read a session store; the layer is built once per request.
A refusal is a 401 (serveFetch's body{ error, unauthenticated: { reason? } }); a resolver that throws or dies is a 500 that says nothing, where a
throw used to be a 401 carrying its message. Wrap a plain value:
resolvePrincipal: request => Effect.succeed(actorOf(request)).
Fixed
-
foldkit-primitives' broadcast doc no longer says a tab never hears
itself.postBroadcastposts on a channel of its own, so the posting
tab'sbroadcastMessageshears it; name the sender indata. -
A Remote Message's error is a
RemoteError.RemoteError.reasonis
string | undefined, as the decodedReadFailed/QueryFailederror is,
soRemoteData.denied(message.error)type-checks under
exactOptionalPropertyTypes. -
serveFetchkeeps a live stream's layer open while it streams. The
request'slayerwas closed as soon as the response was built, so a live
stream's re-reads, and itsrevalidatepasses, ran on released resources:
a layer holding a connection pool failed every one. It now closes when the
answer is sent, or, for a live stream, when the stream ends or the client
leaves. A defect while a stream runs ends it with anInternal error
frame rather than a broken body, andrevalidate.everymust be longer
than zero. -
Remote.httpreads every 401 asUnauthenticatedand every 403 as
Forbidden. A body with anunauthenticatedorforbiddenkey in a
shape other than Remote's (true, a number forreason) was decoded as
an answer and failed as a broken one. Remote's own refusal is taken as it
is; anything else is the status, with no reason. -
defineDocumentHoststops serving a socket before it closes it for
revalidate. A worker's socket fires its own close only when the
replica answers, and exchanges in flight until then were still appended as
the principal just refused. A resolver that throws, rather than fails or
dies, is a 500 too, andrevalidate.everymust be longer than zero.