winche_core 0.2.0
winche_core: ^0.2.0 copied to clipboard
Core primitives for the Winche Dart stack: the app registry, the session every service is bound to, the service contracts each Winche package implements, and the shared identity and options models.
CHANGELOG #
0.2.0 #
A shared exception root, an identity that accepts whatever a backend issues,
and a storageKey that is a digest rather than a derived name.
Breaking #
-
WincheIdentity.storageKeyis now a 128-bit SHA-256 digest ofid, rendered as 32 lowercase hex characters. It was the id itself when already lowercase, and a case-folded stem plus a short hand-rolled hash otherwise. Every service keying on-disk state bystorageKeywill look in a new location and find nothing. Forwinche_databasethat means cached documents are re-fetched and pending writes that never reached the server are lost. There is no migration: services adopting this must decide for themselves whether to move the old directory or abandon it.The change buys three properties the old scheme could not have at once. A key can no longer be collapsed by a case-insensitive filesystem, because it is all lowercase — the property that lets
User1anduser1stay apart on NTFS and default macOS APFS. Every id yields a usable name, which is what allows the validation relaxed below. And the length is fixed at 32 characters however long the id, keeping composed paths clear of Windows'MAX_PATH.SHA-256 rather than a hand-rolled hash because this value is a directory name: it has to be identical across Dart releases, platforms, and between native and web, or an upgrade silently orphans local state. Adds a dependency on
package:crypto. -
WincheSessionExpirednow extendsWincheExceptionrather than implementingExceptiondirectly. Its message andtoStringoutput are unchanged; only its supertype is new. -
The exported types of
winche_core.dartare now prefixedWinche. Two were not:0.1.0 0.2.0 SessionConsumerWincheSessionConsumerSessionConsumerErrorWincheSessionConsumerErrorA rename only — no behaviour changes with it.
Three names are deliberately left alone.
SessionDispatcher,ContractRecorderandWincheTokenFetcherare not exported from either barrel and appear in no exported signature, so no consumer can reach them.ScriptedAuthService, fromtesting.dart, keeps its name: it is a test double rather than part of the runtime API, and the prefix earns nothing in a file you only import from a test. -
WincheNonAuthServiceis gone.WincheDatabaseServiceandWincheStorageServicenow extendWincheServicedirectly and implementWincheSessionConsumer. Anything extending one of those two — every real service — is unaffected. Anything referring toWincheNonAuthServiceby name, as a variable type or a type argument, needs one of those two orWincheService.The intermediate class existed to carry two method declarations. Those are the interface now, so it was a level of hierarchy that named nothing.
-
WincheSessionConsumer.onTokenChangedhas no default implementation. It was an empty body onWincheNonAuthService, inherited silently. A consumer that never reacts to a token rotation is a real bug — it does not re-dial, or never retries work a stale token paused — and it failed only in the field, once a token actually expired. Every concrete service must now write the method. An empty body is a fine answer; it just has to be one you gave. -
WincheServiceErroris nowWincheSessionConsumerError, and lives withWincheSessionConsumerrather than among the models. Itsservicefield is renamedconsumerand retyped fromWincheServicetoWincheSessionConsumer: only a consumer has hooks that can fail this way, so the old type was never accurate.errorandstackTraceare unchanged, anderroris stillObjectrather than narrowed — a hook can throw anything, so handlers should inspect it rather than the wrapper.It is not an exception, and was briefly made one during this release before being reverted. It is produced by the dispatcher and delivered as a value on
WincheApp.errors; nothing throws it, so typing it as an exception only invitescatchsites that never fire. The thing worth catching iserror, which it carries.WincheApp.errorsis therefore now aStream<WincheSessionConsumerError>.
Added #
-
WincheSessionConsumer, the interface the session dispatcher works through. Exactly three members —isDisposed,onSessionChanged,onTokenChanged— and deliberately not a fourth: the dispatcher decides what each consumer should be bound to and knows nothing about what a consumer is, what it connects to, or which app it belongs to.WincheDatabaseServiceandWincheStorageServiceimplement it, so every production consumer already qualifies; the interface exists so that fact is a coincidence the dispatcher does not rely on, and so it can be tested without the service hierarchy present. -
WincheException, the root of every exception the stack throws. One root across every package, soon WincheExceptioncatches everything from the SDK — including from a Winche package added to the app later, the case a per-package hierarchy always misses. It carries amessageand derivestoStringfrom the runtime type, so a downstream exception cannot forget to name itself in its own output. Downstream packages should extend this instead of implementingException, and group wire errors under one intermediate subclass rather than spreading them directly beneath it. -
WincheUnboundException, moved in fromwinche_database. "No identity is bound to this app" is a condition every service shares, so it belongs where every service can name it: a consumer using two Winche packages writes onecatch, and a third package does not make it three. Deliberately not a wire error — it never crossed the wire, and unlike aPERMISSION_DENIEDit is fixed by signing in rather than by handling it.
Changed #
-
WincheIdentityaccepts almost any id. The[A-Za-z0-9._-]{1,128}rule, along with the.,..and trailing-dot checks, existed solely to keepidusable as a path component;storageKeynow guarantees that regardless. Emails, LDAP distinguished names, provider ids likeauth0|5f3c2b1a, non-ASCII ids and ids far longer than 128 characters are all valid, so a backend issuing them can sign in at all — previously it could not.Two rejections remain. An id that is empty or whitespace-only is never a real principal. An id containing a control character (C0, DEL, or C1) is refused because
idis interpolated verbatim intotoStringand intoWincheSessionExpired's message, where an embedded newline would let a backend-supplied value forge a log entry.Relaxing validation is source-compatible: every id valid in 0.1.0 is still valid.
0.1.0 #
A breaking redesign of the whole package around a session model, replacing the stream-based auth contract and service-lookup app of 0.0.1. Nothing from 0.0.1 is source-compatible with this release.
- Session model. A
WincheSession(identity,isActive,token({forceRefresh})) is now the unit core hands consumers, instead of raw identity/token streams.isActivelets a consumer notice that an operation has outlived the session it started under, andtoken()throws the package's only exception,WincheSessionExpired, once that happens — checked both synchronously up front and again after the fetch resolves, so a call straddling a user switch can never return the next user's token. - Two-hook consumer contract.
WincheNonAuthServiceis now exactlyonSessionChanged(WincheSession?)andonTokenChanged(), replacingonAuthTokenChanged/onActiveIdentityChanged. A session change tears down and rebuilds; a token rotation with the same identity only nudges. - Reconcile-to-latest dispatch. Sessions changing faster than consumers
can keep up no longer queue every intermediate value — a rapid
A -> B -> Cnow ends atCwithBnever dispatched, andA -> null -> Acollapses to "stillA". Core also awaits every consumer's hook before starting the next dispatch, so two session swaps can never interleave inside one consumer. - Failure isolation. A consumer whose hook throws is reported on the
app's new
errorsstream (Stream<WincheServiceError>) and left unbound — retried automatically on the next change — while every other consumer still receives its dispatch. Core does not track service health or latch failures. - Notify-based auth seam.
WincheAuthServiceno longer exposesauthTokenChanges/identityChangesstreams, norsignIn/signOut. It now announces changes through two@protectedmethods,notifyIdentityChanged(WincheIdentity?)andnotifyTokenRotated(), and exposes no sign-in surface of its own: how a backend authenticates — password, OIDC redirect, refresh token, device code — is entirely up to the implementing package, which is free to add whatever methods its backend needs. WincheCredentialsandsignIn/signOutremoved. There is no credentials type and no sign-in method on the base class; see above.WincheIdentityis now a single concrete class, replacing the abstractWincheIdentityplusSimpleIdentity.idis validated against^[A-Za-z0-9._-]{1,128}$and rejects.,.., and a trailing dot — an identity'sidbecomes a path and IndexedDB database-name component in every consuming service, and NTFS strips a trailing dot, soabc.andabcwould otherwise silently collide. The class is notconstbecause that validation is not const-evaluable. Claims equality is shallow.WincheOptionsnarrowed todatabaseEndpoint,storageEndpoint, and a newdirectoryResolver. Core hands out only the directory root; how a service composes a path beneath it, and what it names an IndexedDB database, is that service's business, not core's.WincheAppis no longer a service-lookup surface.service<T>()andmaybeService<T>()are gone from the public API; the service registry,session, andauthServiceare now@internal. The public surface isname,options,errors,isDisposed, anddispose(). Each service package is expected to expose its owninstance/instanceForaccessor rather than routing lookups through the app.Winche.initializeAppis synchronous.- A hook deadline. Dispatch is sequential and teardown awaits it, so one
onSessionChangedthat never returned starved every other service and hung the app forever — and a consuming service's hook opens sockets and stores, the two things that hang.WincheApp.hookTimeout(30s by default, also onWinche.initializeApp) bounds every hook and everydispose. An abandoned hook is treated exactly like one that threw: reported and left unbound. WincheApp.settledis public. Awaiting it is how you know every service has finished reacting to a sign-in change — for gating UI, and so a service package's tests can wait for a dispatch without racing the wall clock.package:winche_core/testing.dart— conformance suites (WincheServiceContract,WincheAuthContract) and aScriptedAuthServicedouble. The two auth rules and the swap-vs-nudge distinction cannot be expressed in types; these turn them into a test any implementation can run. Deliberately free of a test-framework dependency: a suite returns aWincheContractReportrather than callingexpectitself.WincheIdentity.storageKey— the name to use when an identity becomes a directory or IndexedDB database component.idvalidation cannot rule out case collisions, and on NTFS and default macOS APFSUser1anduser1resolve to the same directory, so a backend issuing case-sensitive ids would let one user read another's cached state. An all-lowercase id passes through unchanged; anything else is case-folded with a stable digest appended.WincheService.instanceFor(app, create)— returns the service already attached toapp, or builds one. Service packages should route theirinstance/instanceForaccessors through this rather than keeping a static cache keyed byWincheApp, which has to remember to evict itself on dispose and hands back a disposed instance when it forgets.
0.0.1 #
Initial release.
Wincheapp registry:initializeApp,app,appFor,maybeAppFor,apps,deinitializeApp,deinitializeAll.WincheAppwith a service registry keyed by concrete type, plusservice<T>()/maybeService<T>()lookups.WincheOptionscarrying the backend endpoints —databaseEndpointandstorageEndpoint. Everything else, persistence included, is owned by the service it belongs to.- Sealed service hierarchy:
WincheService, split intoWincheAuthServiceandWincheNonAuthService, withWincheDatabaseServiceandWincheStorageServiceprovided as ready-made categories. - Consuming services are wired to the app's auth service regardless of
registration order, and inherit
getAuthToken(),activeIdentity,onAuthTokenChangedandonActiveIdentityChanged. WincheIdentity,SimpleIdentityandWincheCredentials<TIdentity>models.- Pure Dart — no Flutter dependency.