dartvel_core 0.9.4
dartvel_core: ^0.9.4 copied to clipboard
The pure-Dart runtime shared by Dartvel apps, servers and the CLI — records with history, auth, jobs, cache, notifications, privacy and webhooks.
0.9.4 #
- Studio's sign-in page was blank in 0.9.3: the rule that keeps deferred code
parts behind the Studio grant also caught the Flutter engine
(
canvaskit.wasm), which the sign-in screen needs. Only a deferred library's*.part.*files need the grant now.
0.9.3 #
- Security: Studio never renders for a signed-out visitor. The admin
mount served
<mount>/index.htmlas a file, so a signed-out request for it got the Studio app, which drew Studio's screens (its data stayed 404). Every document path under the mount now answers 302 to the Studio sign-in without the Studio grant, and Studio's deferred code chunks need the grant too. Upgrade and redeploy if Studio is enabled. - Data models designed in Studio, and model data API: Studio data models
are stored beside records and served at
/_dartvel/data/<Model>governed by their access policies.@DVModel.validate(...),@DVModel.uniqueField()and@DVModel(indexes:, access:)declare the same rules in code.
0.9.2 #
- DV-BROWNFIELD-001 to 003 are registered with the other diagnostics, as the specification's new Brownfield section lists them.
0.9.1 #
-
Studio signs people in itself: a signed-out visit to a Studio page is sent to
<mount>/login, the mount answers the application's sign-in and second factor at<mount>/api/auth/*and<mount>/api/access, and serves the Studio app's shell and code to anybody whilegraph.jsonand the API stay behind the grant. -
dvRenderRoutePageandDVRoutePage: one renderer for a route's page, shared bydartvel build weband the web-server binary. The minifier, the static page head and the structured data move here from dartvel_cli. -
DV-MODULE-021 is registered with the other diagnostics, as the specification lists it.
0.9.0 #
DVModuleRpcandDVModuleRpcRefused: a module operation carried to the backend over generated RPC -- the client half a generated module calls, the transport the generated client installs, and bytes across JSON.DVModuleOutcome.parsereads{compat: backend}.
0.8.0 #
-
dvHeadingSluganddvHeadingIds: the id a link to a heading names, a heading's words as a slug, numbered from 2 when a heading repeats. -
DVModuleEnvironment,DVModuleOutcomeandDVModuleUnavailable: the environment a generated module was compiled for, the four outcomes it declares, and the failure an unavailable operation throws (DV-MODULE-013).
0.7.1 #
- Released with dartvel_flutter 0.7.1, which fixes Esc on a web page; no changes in this package.
0.7.0 #
-
dartvel.webhooksis read.DVWebhooksConfig.readtakesformat(dartvel|cloudevents),mode(structured|binary),source,signature(dartvel|standard),allowPrivateAddresses,retention,disableAfterandmaxAttempts, and refuses anything else withDV-WEBHOOK-009. The defaults send exactly what was sent before. -
DVCloudEvent: CloudEvents 1.0.2 over HTTP.toHttpwrites structured or binary mode;fromHttpreads either and refuses what is not a CloudEvent withDV-WEBHOOK-008. -
DVStandardWebhookSignature: Standard Webhookssign,headerandverify, with the libraries' five-minute window. -
DV-WEBHOOK-008to010are in the diagnostics registry. The specification listed them andDVDiagnosticsdid not, so a lookup of any of the three found nothing. -
Fixed: a schedule on a UTC clock fired early east of Greenwich.
DVCronSchedule.nextAfteranswered a UTC instant in the host's zone, so 02:59:30Z came back as 03:00 local, already past; each tick ran an occurrence that was not due, and two processes sharing a schedule lease claimed different instants for one run. It now answers in the zone of the instant it is given. -
Fixed: a capture session that fails while finishing an error is recorded.
_runreturned the finishing future from inside itstrywithout awaiting it, so a failure there escaped thecatch. -
Breaking:
@DVModel(generatePublicPages:)defaults totrue. AddgeneratePublicPages: falseto a model that must have no public pages. -
DVPolicyAction.viewSensitive: the policy a generated model page asks before it shows a record's protected fields. Nothing registered refuses. -
DVModelPageSpeccarriesprotectedFields,personalandviewPolicy.dvModelPageDatanever reads a protected field, and a personal or unpublished row answers exactly as a missing one (dvModelPageNotFound).dvModelPageResolverasks the model'sviewpolicy as nobody and answers a refusal as a missing record.DVModelPageAccess, inpackage:dartvel_core/framework.dart, asks both questions. -
The browser's own find searches the page-text block.
dvApplyPageHtmlanddvApplyPageTextwrite each paragraph inside<section hidden="until-found" data-dv-anchor="N">, via the newdvFindableHtml. On screen the block is now a clipped 1px box instead ofdisplay:none, which the browser's find skips. Print and the<noscript>override still show the whole block, until-found sections included.dvFallbackStyleis now built fromdvFallbackCssanddvFallbackNoscriptCss. -
@DVPage(findable: false)opts a page out of the browser's find. The default is true, next toselectable. With it set, the web runtime writes none of the page's text into the document, and the sections the build wrote for crawlers and printers are no longer searched. -
dvFindMatch,dvFindMirrorBlocks,dvFindMissingare the plain-Dart half of find in page: which rendered paragraph a browser match means, and what the runtime mirror is written from. -
DV.Cachelives here, and is four calls:get,set,hasanddelete.DVCachemoved from dartvel_flutter. Everything else is a named option on the four:set(key, value, {ttl, tags});get<T>(key, {compute, ttl, tags, staleFor}), wherecompute:reads through with one shared compute per key andstaleFor:serves a stale value while one refresh runs behind it; anddelete(target), whose one positional target is aStringkey, aDVCacheTag(a const value naming a tag) orDVCache.all(a const sentinel), and which throws anArgumentErrornaming the type of anything else.DVCacheTagand the sealedDVCacheTargetare exported withDVCache.ttl:,tags:orstaleFor:on agetwithoutcompute:, andstaleFor:withoutttl:, are anArgumentErrorrather than ignored. Backend code reaches it asDV.Cachefrompackage:dartvel_core/dv.dart. -
DV.Cache.withAdapter(adapter)switches store in code.dartvel.cachesets the storeDV.Cacheuses;withAdapterreturns aDVCacheViewwith the same four calls on another adapter. Every call goes to that adapter, and tags and the shared compute are kept per adapter.DVRedisCacheAdapter.connect(url)opens Redis from aredis://url, withAUTHandSELECT, and is what the configured store uses too. -
Breaking: the rest of
DVCacheis gone or internal. Rewriteremember(key, compute, ...)andstaleWhileRevalidate(...)asget(key, compute: compute, ...),tag(key, tags)asset(key, value, tags: tags),revalidateTag(tag)asdelete(DVCacheTag(tag)), andclear()asdelete(DVCache.all);delete(key)is unchanged.lock,purgeExpired,keysForTag,tags,configure,adapterand the global cache (configureGlobal,globalGet,globalSet,globalDelete,globalTag,globalRevalidateTag) moved toDVCacheRuntime, exported fromframework.dartfor the framework and its tests.dartvel migrate-codedoes not rewrite these: its rules swap one name for another, and these calls change shape. -
DVCacheConfigreadsdartvel.cachefor the build and installs the store at startup: host, port,AUTHandSELECTfrom aredis://url held in an environment variable, a database table on the shared database, or Memcached.DV-CACHE-001to005are registered. -
DVCacheAdapter.removeisdelete. Adapters implementdelete;removestays as a deprecated extension for callers. -
A list or map read back from a JSON store is returned as the type asked for, so a read-through
gethits rather than recomputing on every call. -
Breaking: the change capture machinery left the application barrel.
DVCapture,DVCaptureConsumer,DVCaptureSink,DVWarehouseSink,DVCapturePrivacyAdapter,DVCaptureDeliveryJob,DVCaptureBackfillJob,DVCapturedChangeandDVCaptureBatchare exported fromframework.dartonly. Destinations are declared indartvel.capture.DVCaptureWriteErroris still exported fromdartvel.dart. -
DVCaptureRuntimeruns change capture fromdartvel.capture. The generated server configures it: the log with the declared retention, a destination per declared destination (its connection read by secret name;DV-CDC-009and skipped when unset), delivery and backfill on each destination's own queue with retries and a backoff, a backfill for a destination or data model never copied and for one left behind retention,dv_capture_lag_seconds/dv_capture_lag_changesgauges (scraped at/metricsasdartvel_dv_capture_lag_*) withDV-CDC-003, hourly pruning, and the erasure adapter installed intoDV.Privacy. -
DVCaptureConfigparsesdartvel.capture, refusing an unknown key, a destination type no adapter implements or a malformed duration (DV-CDC-006), a connection written out instead of named (DV-CDC-007), and a destination naming a data model that is not captured (DV-CDC-008). -
The capture log and its destination are storage-neutral. The log, its checkpoints and backfill state, and
DVWarehouseSinkare written through theDVRecordAdapteroperations rather than SQL, so they run on a store that speaks none.DVWarehouseSink(fieldType:)replacescolumnType:and takes aDVFieldType; without it a field is typed by its first value. A field the source contracts is emptied at the destination rather than dropped. The log's flag and version columns are BIGINT; a PostgreSQL or MySQL log from an earlier release is widened on start, or refused with its plan when it has rows. -
A captured data model records to its database's log wherever it is saved.
DVCapture.configuredfalls back to a log over the configured database, so a worker, a script or a desktop app sharing the database is captured too. -
Studio edits and replayed offline writes to a captured model are captured.
DVStudioModelSpec.capturesays a model is captured, through the manifest as well. -
A backfill no longer skips another model's later changes. A consumer with a position is delivered up to the head before a copy moves its checkpoint.
-
DVSqlRecordAdapter.ensureadds a field a shape gained since it was last ensured in the same process. -
Breaking: the offline machinery leaves
dartvel.dart.DVOfflineStore,DVOffline,DVOfflineRemote,DVMutation,DVReplayResult,DVRemoteOutcome,DVOfflineClockandDVOfflineStorePrivacyAdapterare exported fromframework.dartonly. An offline data model's ownsave(),destroy(), reads andsyncStatereplace them.DVSyncStateandDVOfflineQueueFullErrorstay public. -
DVOfflineSyncruns every offline data model's queue (framework-only): replay at start, after a write, on reconnect and on a doubling backoff, one mutation per request, the server's clock taken from each answer, andsignedOut()sending what it can before emptying every store. -
A device store per platform.
dvLocalOfflineDatabaseopens a SQLite file in the application's data directory, IndexedDB in a browser (DVSnapshotDatabaseAdapteroverDVIndexedDbSnapshots), or memory underflutter test.MemoryDVDatabaseAdaptergainsexportTableandimportTable. -
Fixed: the replay route prepared none of its tables, so the first replay against a real database was a server error a device retried for ever.
DVRecordTableRemotenow ensures its schema on first use, and the route's answer carriesserverTime. -
Fixed: a replayed write was put to the policy as a map, which a policy written against the model refused.
DVOfflineReplay.forSpecs(resources:)builds the policy's own class; one that cannot be built refuses. -
DVOfflineStore(onAdopted:)is called when replay replaces the device's copy with the server's. -
A
@DVHomeWidgetclass with a primary constructor is named correctly.class const NextShiftWidget({super.key}) extends StatelessWidgetwas read as a widget namedconst. -
Breaking: the SDK floor is Dart 3.13.0 and Flutter 3.47.0. Dart 3.13 is the first release with primary constructors (
class Point(final int x, final int y);), which thedartvel createscaffold, the samples and generated data models are written with; on Dart 3.12 they are a compile error. Flutter 3.47.0 is the first stable release that ships Dart 3.13.0. Raiseenvironment: sdk:to">=3.13.0 <4.0.0"and upgrade Flutter before taking this release.
0.6.2 #
- Documentation only: install the CLI with
dart pub global activate dartvel_clifirst, then Homebrew, the release binaries, dev_dependencies and npm.
0.6.1 #
- Declares the platforms it supports in pubspec.yaml, so pub.dev lists them instead of inferring them from platform-specific imports. No code changes.
0.6.0 #
-
DVStudioDevGrantopens Studio on a development server to the person running it. A random token, printed as a link: opening it sets an HttpOnly, SameSite=Strict cookie scoped to the mount and redirects the token out of the address, andDVAdminServer(devGrant:)serves the mount only to a browser holding that cookie.DVStudioModelSpec.toManifestandfromManifestcarry a model's spec to a server without the generated code. -
DVPublishedPagesserves Studio's published pages. It answersGET /_dartvel/pages(dvPublishedPagesPath) with every document indartvel_pages, revalidated on each load, and an empty list where nothing was published. -
Studio edits enums, lists, maps and references.
DVStudioFieldSpectakesoptions(an enum's values) andrelation(the model a field holds the key of). The API refuses a value outside the options, a list or map of the wrong shape, and a key no record of the related model has; a list, set or map is kept as JSON and read back as one. -
Studio lists a mounted module's models.
DVStudioModelSpectakesmoduleanddata(the module'sDVModuleData); such a model is addressed as<module>.<Model>and read from the table and database its mount gives it. -
Studio's API shows queues and cache tags.
GET <mount>/api/queueslists every queue the build's graph names, anddefault, with its pending jobs and dead letters (a broker that cannot list says so);POST <mount>/api/queues/jobs/<id>/retryand.../discardact on a dead letter and answer 404 for a job that is not one.GET <mount>/api/cache/tagslists each tag with its keys andPOST <mount>/api/cache/tags/<tag>/revalidatedrops them, saying which. -
Studio grants and revokes access itself.
POST <mount>/api/grantsgrants an account named by address (through the newDVAccountLookup, whichDVDatabaseAuthProviderimplements) or by id, and refuses an address nobody signed up with.DELETE <mount>/api/grants?userId=revokes, and answers 409 (confirm_self,confirm_last) for the caller's own grant or the last one on a tenant untilconfirm=true. Both sit behind the Studio grant check and need the CSRF header. The list names each account's address and marks the caller. -
DVDatabaseAuthProviderkeeps accounts in the application's database.LocalAuthProviderholds them in memory, so a server using it forgot every account on restart. The new provider keeps an id, the address, a name and a PBKDF2 hash indv_accountsover anyDVDatabaseAdapter, answers a wrong password and an unknown address with the same refusal, and lets the table's unique key decide between two sign-ups for one address at once. It implementsDVAccountProviderandDVPasswordProvider, so the account endpoints can change an address or a password and delete an account. -
A Dartvel Cloud build spec carries
formatandcodesign.aabis accepted for android andipafor ios, and anything else is refused when the spec is read. -
Studio opens only for a person allowed
Studio.access.DVAdminServerserved the dashboard on a release mount to any live session, and every customer who signs up to an application has one. It now also asksDV.Auth.authorizationwhether that person mayStudio.access(dvStudioAccessAction), which nobody may by default.DVStudioGrantskeeps the grants in the application's database (dv_studio_grants, per user and tenant) andinstall()makes them the answer; an application that already knows its operators registers its own withregisterAction(dvStudioAccessAction, ...), which is asked instead.dvAdminSessionAuthenticatedis nowdvAdminAuthorized. -
DVAuthAuthorization.registerActionandregisterDeclaredActionregister a policy by itsResource.actionname, for a resource with no Dart type.DVAuthAuthorization.reset()forgets every policy, for a test. -
An IP-address host names no tenant.
DVTenants.resolveread the first label of every host with three or more dots as a subdomain, so a request to127.0.0.1ran on a tenant called127, one to10.0.0.5on10, and so on. IPv4 and IPv6 hosts now resolve to no tenant, and so to the default one. Data written through an IP-address host before this fix is under that numeric tenant (127,0,10, ...), notdefault, and so are sessions issued there, which now fail to authenticate. Move or copy those rows to the default tenant if they should be kept. -
DVShorebirdPatchSourceanswersRequest/Response, and publishes.respond()serves the updater's check, downloads and events in the shape a Dartvel server answers in, builds download URLs from the host the device used, and adds publish and rollback endpoints that answer only a source given apublishTokenand a request bearing it. -
Dev-client TLS.
DVDevClientSigner.certificate()is a self-signed certificate for the pairing key,dvCertificateP256PublicKeyreads a certificate's key by structure, anddvDevClientHttpClientanddvDevClientSecureConnecttrust only a server presenting the key a pairing names. A pairing link must name an https server. -
package:dartvel_core/cloud.dartis the Dartvel Cloud protocol. The build spec, build, artifact, event stream and refusal types the CLI and the hosted Cloud service share, with nodart:io. A spec for a target no worker builds, a profile or store that does not exist, or a project name that is a path is refused when it is read. Artifact names that climb out of their directory are refused. A 402, or the codeplan_required, is a refusal that names the plans page: every cloud build needs a paid plan. -
DVAdminServerserves the admin dashboard from the backend. The mount (DVAdminMount), the decision (dvAdminFor) and the file resolution (dvAdminAsset: the mount root isindex.html, a path that climbs out of the admin root is refused however its dots are encoded, and each file goes out with its content type andcache-control: no-store) moved here from dartvel_cli, so the generated backend can use them.respondanswers null for a caller who may not see the admin, and the application answers that request as it answers any route it does not serve. A mount that requires a sign-in is opened only by a live session of the application's own, read byDVSessionAuthenticationon the request's tenant. -
dartvel infraunits give the web-server binary a writable data directory. They setDARTVEL_DATA_DIR=/var/lib/<app>, the state directory systemd makes writable, becauseProtectSystem=strictmakes/opt/<app>read-only and the binary's SQLite file could not be created beside it. -
DVShorebirdPatchSource: a patch source for Shorebird's updater that Dartvel serves. The updater in Shorebird's engine asks<base_url>/api/v1/patches/check, withbase_urlread fromshorebird.yaml. This answers that check, serves the patch download (resumable) and accepts the install events, from a directory of patches per app, release, platform and architecture, with channels and rollback. It is the open-source updater's wire format; Shorebird itself offers no self-hosting. -
dvApplyGeneratedSchemamakes the generated models' tables and adds the columns a table made by an earlier release is missing, on SQLite by asking the table what it has. It is whatdartvel db migrateruns, moved here so a web-server binary can run the same thing when it starts.dvTenantColumn,dvAddColumnForanddvAddColumnSqlmoved with it. -
Dev-client tunnel primitives.
dvDevClientTunnelPath,dvDevClientTunnelProtocol,dvDevClientTunnelProofanddvDevClientTunnelProofValid, the challenge a dev server signs so a development build sends nothing to a server without its paired key; andDVDevServerHost, the dev machine a paired build reaches its backend on. -
dvDevBackendUrl. A development web build opened from a phone on the LAN called the backend atlocalhost, which on the phone has nothing on it. The generated runtime now reaches a loopback backend at the host the page was served from, in debug and profile web builds only. -
DVProcessStores.install(fallback:)names the store to use whenDATABASE_URLis not set. A web-server binary passes its SQLite file, which opening creates.DATABASE_URLstill wins. -
Files can travel inside a compiled executable.
DVBinaryPayload(package:dartvel_core/binary_payload.dart) splices named sections into adart compile exebinary and reads them back from the running one. Bytes appended after the binary stop it starting, because the runtime finds its snapshot through a trailer at the end of its own file, so the sections go between the runtime and the snapshot and the trailer is rewritten.dvPackFilesanddvUnpackFilescarry a directory as one section, anddvExtractFileswrites it once per build into a directory named by its content. -
DVRecordTable.prunelogsDV-HISTORY-004with the table, how many entries it removed and the declared retention, when it removed any. -
An erasure on a tenant-scoped table stays on the row's tenant. The walk reads such a table whole, and a key is unique only on its tenant, but it deleted or anonymized by key and version alone and purged history by key alone. Erasing a subject's order
o1on one tenant deleted another tenant's ordero1at the same version, belonging to somebody else, and removed that record's history. The write, the reread after a conflict and the history purge now match the row'sdv_tenantas well. -
DVModel.versionandDVModel.softDelete.@DVModel(version: false, softDelete: true), read bydartvel routes. -
A job runs as the tenant it was dispatched under.
DVJobEnvelope.tenantrecords the tenant current whenDV.Jobs.dispatchran, or null for none. Every adapter stores it and reads it back: in-memory, database (a nullabletenantcolumn, added to a table an earlier release made), Redis, SQS, RabbitMQ, Pub/Sub and Kafka.DVQueues.work, and so the worker, runs each handler insidewithTenantfor it, soDV.Database, tenant-scoped models and the per-tenant schema or database resolve what the request resolved. A job with no tenant runs as the default tenant, never as the worker's process tenant or the previous job's.DVQueueAdapter.enqueuetakestenant:; an adapter written outside this package has to accept and store it.dvEnsureFrameworkColumnsadds a column a later release declared to a framework table that predates it, through the schema planner. -
registerDVModelReadCarrierandcarryDVModelRead. How an edit of a model rebuilt from JSON inherits the version the model it edits was read at, so its save is checked against that read rather than refused as unread. -
A browser client sends its credentials to an API on another origin, and only to origins named exactly.
DVCredentialedOrigins.allownames one origin; the browser transport sends a request to it withcredentials: 'include'and every other request with the same-origin default. A wildcard,null, a path, user information or plainhttpoff the loopback is refused when named.allowBackendnames the origin of a backend URL and says why when it cannot. -
Privacy jobs cross a durable queue, and open erasures are tracked to their deadline.
DVPrivacyErasureRequestandDVPrivacyRetentionRequestcarry codecs, registered byregisterJobs, so a request dispatched onto a database queue no longer throws for want of one; the subject keeps its type.requestErasurerecords the request indv_privacy_open_erasuresbefore queueing it, and a completed erasure of the subject closes it.DVPrivacy.checkErasureDeadlinesruns every open request older than a day, reportingDV-PRIVACY-004for one past its deadline; one an adapter missed stays open for the next check.DVPrivacyRuntime.startcreates the walk's tables and registers its jobs against the currentDV.Privacy, andDVPrivacyRuntime.schedules/startSchedulessweep retention daily and check deadlines hourly, each occurrence claimed through a schedule lease. -
DVGraphQL.executeRequestandsubscribeRequest. They run a decoded GraphQL-over-HTTP body, including the persisted-query hash atextensions.persistedQuery.sha256Hash, which the generated route never passed. A hash sent alone runs the document the manifest holds for it. A hash sent beside a document has to match it underpreferandrequire. A body that is not an object counts as an empty request.DVPersistedQueries.withModechanges the mode and keeps the manifest. -
DVHttp.coveringHostnames the declared host whose base URL a URL is under, by the rule a request is sent or refused by, so the build'sDV-HTTP-001check applies the same one. -
DVGraphQL.authorizeModelandDVGraphQLForbidden. A resolver asks the policy forResource.actionon the record it would touch, throughcanAction, and is refused withDVGraphQLForbiddenunless it allows. The caller is the request's API key, OAuth token or session user when a server authenticated one, and theuserit is handed otherwise. An action nothing registered, a caller or resource the policy was not written for, and a policy that throws all refuse. The executor reports the refusal asNot authorized (Resource.action)with theFORBIDDENcode a declared field policy already used. -
DV.Httprefuses an absolute URL no declared host covers (DV-HTTP-001). Only a name was checked:DV.Http.host('paystak')was refused whileDV.Http.get('https://api.paystak.co/...')went out on the default policy with nobody's decision about it. A URL is now sent as the declared host whose base URL it is under -- same scheme, host and port, a path at or below the base path, the longest base URL winning -- with that host's credential, timeout, retries, breaker and pool, and anything else fails before it is sent, faked or not.send(allowUndeclaredHost: true)is for a destination that is data rather than configuration: it sends on the default policy and never as a declared host, so a URL somebody else supplied cannot borrow the application's credential for that host. Webhook deliveries use it, and stay pinned to the address their own check approved.DVRangeQueryBreachedPasswords.overHttpneeds its endpoint declared. -
The
dartvel.httpblock is read strictly. Every key and value the reader does not understand throws naming it, where it used to fall back to the default (retry:forretries:,attempts: many,backoff: linear,jitter: "yes").DVHttp.readConfigreads a whole block without declaring anything. -
Webhook subscriptions, deliveries and payloads are kept in
DV.Database. They were maps in process memory, so a restart lost every subscription, the record of every delivery, and every retry still owed. They are three framework tables now (dv_webhook_subscriptions,dv_webhook_deliveries,dv_webhook_payloads), and the job on the queue only says which endpoint to work, so a delivery that failed before a restart is retried after it as the same delivery, with its earlier attempts counted and still ahead of the next event. A queue lost with the process is refilled:drainqueues one job for each pending delivery that has none, andresume()does it for every endpoint at startup. The drain job also has a payload codec now. Before,DVDatabaseQueueAdapterrefused to store it, so webhooks could not run on the durable queue at all. Retention deletes the payload row and keeps the record.subscription,subscriptions(),delivery,deliveriesandpurgeExpiredPayloadsreturn futures, and ids are random rather than a counter that restarted at 1. -
A webhook connects to the address its check approved. The endpoint check resolved a customer's host and refused private addresses, and then the HTTP client resolved the host again to connect. A name with a short TTL could answer a public address to the first lookup and 127.0.0.1 to the second.
DVHttpRequest.connectAddress(andDV.Http.send(connectAddress:)) now holds a request to one IP: the connection goes there, TLS still sends the host name as SNI and verifies the certificate against it, and the Host header still names it. Every webhook attempt and every redirect hop is pinned to the address its own check passed. On the real wire this also stopspackage:httpfrom following a redirect by itself. Before, the per-hop check that refuses a redirect onto 169.254.169.254 only ran against a test transport, because the default client had already followed the redirect. A pinned request is refused where there is nodart:io. -
A meter amount keeps its precision on PostgreSQL.
dv_meter_recordsdeclaredamount REAL, which PostgreSQL stores in four bytes: a gauge reading of 16777217.25 came back as 16777218, with no error. It isDOUBLE PRECISIONnow, eight bytes on every server and the same REAL affinity on SQLite. An exactNUMERICwould add nothing, since amounts are Dartnums and are aggregated as doubles once read. -
A framework table a server would refuse is refused everywhere.
dvEnsureFrameworkTablenow checks its statement before running it, on every adapter, and throwsDVFrameworkTableErrorfor a column with no type, aTEXTcolumn in a primary key or unique constraint (MySQL cannot index one without a prefix length), or aREAL/FLOATcolumn (four bytes on PostgreSQL or MySQL). The in-memory and SQLite suites now catch what only a server used to. A test scans every package'slibfor such aCREATE TABLE. It found the database queue'sid, purchase grants and notification claims, promotion redemptions and counters, and the meter table's keys, which areVARCHARnow. The second-factor, crash report and 3D scene stores make their tables through it too. A column declaredDOUBLE PRECISIONis widened fromrealorfloatthe way aBIGINTis widened frominteger. Unlike an integer, a table of 4-byte floats that already holds rows was accepting every write, so it is logged as a warning and left as it is rather than stopped. -
Every framework table can be created on PostgreSQL and MySQL. Change capture, record history's log, the analytics outbox, identity and event tables, consent, agreements, privacy tombstones and erasure requests, the offline store, schema backfills and expand/contract state, organizations, API keys, the OAuth provider and content versions declared columns with no type. SQLite accepts that; PostgreSQL and MySQL refuse it at
CREATE TABLEwith a syntax error, so none of those surfaces could start against a server. Each column now has the type of what is written to it --TEXTfor ids, JSON and ISO-8601 times,BIGINTfor sequences, microsecond clocks and the capture write order,INTEGERfor versions, counts and flags -- which SQLite reads with the same affinity the values already had.DVRecordTabletakestypes:, checked against its columns, and makes its bookkeeping columns asdartvel db migratedoes; its history table is always typed. A table with notypes:is still untyped and still SQLite-only.DVWarehouseSinktakescolumnType:, since a capture carries column names and not types. A backfill's chunk boundaries are stored as JSON, so a numeric key reads back as a number rather than as text that compares greater than every number.dvEnsureFrameworkTablereads a schema-qualified table name. Thepostgresjob runs every one of these stores against the server. -
The framework's tables hold 64-bit timestamps on PostgreSQL and MySQL. Sessions, account deletions, the database queue, purchase grants and notification claims, meter records and reports, and promotion redemptions declared their millisecond and microsecond timestamps -- and the queue's
backoff_msand a meter report'squantity-- asINTEGER, which is 64 bits on SQLite and 32 on PostgreSQL and MySQL. Every write to them failed on a server withvalue out of range for type integer, so signing in could not work against PostgreSQL. They areBIGINTnow. The adapters are unchanged: they pass DDL through, and rewritingINTEGERin them would change applications' own DDL and break SQLite'sINTEGER PRIMARY KEY. A table an earlier release already created keeps its types underCREATE TABLE IF NOT EXISTS, so each store now makes its table throughdvEnsureFrameworkTable, which asks the catalogue for columns left 32 bits wide, classifies widening them through the schema planner, and runs it on an empty table -- which every such table is, since no write to it could succeed. A table with rows is not rewritten: the store throws aStateErrorcarrying the plan and theALTER TABLEto schedule. A newpostgresjob in the Tests workflow runs the stores against a PostgreSQL service. -
Account deletion waits out a grace period the project configures. With
DVAuthEndpoints.useDeletionGracePeriodset, a confirmed deletion answers 202 witherasesAt, ends every session of the person at once, and records the request indv_account_deletionsbeside the erasure's own database; the account and its data stay until the window closes. Signing in within it -- completely, with the second factor where there is one -- cancels the deletion and saysdeletionCancelled; signing in after it is 403account_deleted.DVAuthEndpoints.eraseDueDeletionsqueues aDVAccountErasureJobonaccountErasureQueuefor each deletion whose window closed and works them, anderaseScheduledAccountruns one: it claims the deletion by compare-and-set, so a person who cancelled -- even after the job was queued -- has nothing erased; runs the erasure with the deadline measured from the request; and on an adapter it could not reach puts the deletion back, keeps the account, and throws. A claim older thanstaleErasureClaimis taken again. With no window, deletion erases at once as before. -
DVAuthEndpoints.changePassword,POST /auth/account/password. TakescurrentPasswordandnewPassword, checked through the credential guard so a wrong current password counts as one at sign-in does. On an account with an authenticator it also takes acodeorrecoveryCode, unless a factor was presented on the session withinstepUpWindow; otherwise it is the step-up refusal. The new password is breach-checked, the same one is 400password_unchanged, and the provider's own rules apply. Then the session rotates -- recording the factor when one was presented -- and every other session of the person is revoked on every tenant, since the password is the account's; the answer carries how many.DVPasswordProvideris what a provider implements to have its password set, separately fromDVAccountProviderso an existing implementation keeps compiling;LocalAuthProviderimplements it. -
An address change and an account deletion work without application wiring, and refuse when they cannot.
DVAuthEndpointsused to send a verification code only through asendEmailVerificationthe application passed toinstall, and to delete an account with or without aDVPrivacy-- removing the row a person signs in with, erasing nothing, and answering- Now, with nothing passed, a code goes through
DV.Notifications.mailfromDV.Notifications.useMailSender, built byinstall(emailVerificationMail:), else the template the generated server installs withuseGeneratedEmailVerificationMail, elsedefaultEmailVerificationMail; and deletion runs theDV.Privacythe generated server configures. Without a mail provider or a sender the change is 503mail_not_configured, naming what to configure, before a code is minted. A send that throws is 503 and leaves no pending address. Without an erasure, deletion is 503erasure_not_configurednamingDARTVEL_PRIVACY_KEY, checked before the password is; an erasure that could not reach an adapter (DV-PRIVACY-009) is 503erasure_incompletewith the account and its sessions intact, so asking again finishes the walk.DVEmailVerificationis what a template gets: the sender, the new address, the code and how long it works.DVNotificationMail.isConfiguredandDVNotificationsService.mailSendersay whether a mail could leave.
- Now, with nothing passed, a code goes through
-
A route declares the largest body it reads.
Router.get,post,put,delete,headandanytakemaxBodyBytes, andRouter.bodyLimitslists every route in dispatch order with the limit it declared, or null for the server's. That list is what dartvel_shelf'sserve(routeBodyLimits:)takes. The native server reads a body before any Dart runs, so a limit checked in Dart comes after the body is already in memory; the server has to learn a route's limit to enforce it. Every route is listed, not only limited ones, because the first route that matches decides, and a route with no limit of its own that comes first must keep the server's limit. A limit that is not positive is refused at registration.DVRouteBodyLimitis exported fromhttp.dart, anddvDefaultMaxBodyBytes(1 MiB, the same asdvDefaultBodyLimitBytes) from the main barrel. -
Crash ingest limits each client source as well as each install.
DVCrashIngest's per-install limit keys on the install id a report names, which the client writes, so a client writing a new id per report was never limited and could filldv_crash_reports.accept(body, source:)now also counts stored reports against the client's source: its IPv4 address or IPv6 /64, asDVClientAddress.sourceOfresolves it. PastperSourcePerHoura report is refused with the new outcomesourceLimited, answered 429, andDV-CRASH-004is logged once an hour per source without the address. The default is ten installs at their full per-install budget (300 for 30), so an office or carrier-grade NAT is not refused at the second device. 429 rather than 202 meansDVCrashSink.dartvelkeeps what it could not deliver and sends it again later. The install limit is checked first, so one install in a crash loop still gets the final 202. Only a stored report spends either budget. A caller that passes no source counts in one unknown source rather than none.dartvel.crashes.ingest.perSourcePerHoursets the bound;DVCrashConfigrefuses it below 1 or belowperInstallPerHour, naming the key. -
An IPv6 client is counted by its /64.
DVClientAddress.sourceOf, and with it the sign-in and sign-up velocity limits (DVAuthEndpoints.sourceOf) andCommonMiddleware.rateLimit's default identifier, count an IPv6 client's/64network, written as a range such as2001:db8:1:2::/64. Until now each /128 was counted, so a client given a /64, which is what subscribers routinely get, was a new source on every request from a new address and escaped every per-source limit. IPv4 is still counted per address, and an IPv4-mapped address is IPv4, not a member of::ffff:0:0/64.DVClientAddress(ipv6SourcePrefix:),.parseand.fromConfigurationtake the prefix, from 32 to 128.checkIpv6SourcePrefixrefuses anything else, and so doesinstall.DVClientAddress.sourcegives the key for a request andDVCidr.containinggives the network an address is in. -
Changing an account's address, and deleting the account, as endpoints.
DVAuthEndpoints.accountdescribes the signed-in person's account.requestEmailChangesends a code to the new address throughsendEmailVerificationand changes nothing; its answer is the same whether or not another account has the address.verifyEmailChangetakes the code, moves the account, and rotates the session; an address still waiting ispendingEmail.deleteAccountneedsconfirm: true, the password -- checked through the credential guard, so a wrong one counts as one at sign-in does -- and the second factor when the account has one, then runs the project'sDVPrivacyerasure where installed, removes the factors, the account and every session, and clears the cookie; a failed erasure answers 503 with the account intact.DVAccountProvideris what a provider implements to be changed and deleted, andLocalAuthProviderdoes. -
A provisioned host's backend believes the client Caddy reports. With
proxy: { adapter: caddy }, every backend unitdartvel infrarenders setsDARTVEL_TRUSTED_PROXIESto the address Caddy dials it on, the same constant the Caddyfile's upstreams are written from. Without it every request reached the backend from Caddy's address, and every per-source limit counted the whole internet as one client. A host with no proxy trusts none. Units already provisioned are reported as drifted bydartvel infra checkuntil the next apply writes the new line. -
One client address, and a header believed only from a trusted proxy.
DVClientAddressresolves who a request came from: the connection's peer address, unless the peer is in the configured trusted proxies, in which caseX-Forwarded-For(orForwarded, when that is the configured header) is walked from the right and the first hop that is not a trusted proxy is the client. A hop that does not parse stops the walk and nothing left of it is read; a request with no peer address has no client address, never a header's.DVCidrranges refuse what is not exactly a range, including10.0.0.1/8.DVClientAddress.fromConfigurationreads the pubspec's list plusDARTVEL_TRUSTED_PROXIES, andDVClientAddress.installsets the process's resolver, which trusts no proxy until one is installed. -
Per-source limits count the client, not what it wrote. The sign-in and sign-up endpoints'
sourceOfandCommonMiddleware.rateLimit's default identifier took the firstX-Forwarded-Forentry -- the rate limit alsoCF-Connecting-IP,X-Real-IP,Fastly-Client-IP,True-Client-IPandForwarded-- so a client wrote a new one per request and was never counted twice, and every request without one shared a bucket. Both useDVClientAddressnow; a request with no peer address counts asunknown. The rate limit no longer readsremoteAddressoripfrom a map-shaped request;peerAddressis the key.DVWaf.countryHeaderbelieves its header only from a trusted proxy, and the country is otherwise unknown. -
Request.peerAddress, and addresses as values. A request carries the address at the other end of its connection when the server that built it had one (dartvel_shelfsets it from the socket); null otherwise, and never a header.DVIpAddresscompares by meaning rather than spelling -- an IPv4-mapped IPv6 address is its IPv4 address, and IPv6 prints in RFC 5952 form -- and refuses what is not strictly an address, including IPv4 with a leading zero.DVPeerAddressis an address with an optional port, read from1.2.3.4:80,[2001:db8::1]:80or a bare address. -
@DVBackendFunction(mfa:)and@DVPage(mfa:). Both annotations take aDVMfa:DVMfa.requiredfor a second factor at some point in the session,DVMfa.recent(Duration(...))for one within the window.DVAuthEndpoints.requireMfa(policy)is the gate a generated route runs: nobody signed in is a plain 401, a caller on an API key or OAuth token is 403 (it has no factor to present), and a session without a recent enough factor isstepUpRequired-- 401 withinsufficient_user_authenticationandmax_age.DVStepUp.sendis how a generated call answers that: it presentsDVStepUp.challengeand sends the call once more. Only anmfa_requiredanswer is a step-up, calls refused together share one challenge, and a dismissed or failed challenge returns the refusal without sending again. -
Second factors, as endpoints.
DVAuthEndpointsnow also handles the signed-in person's factors:factors(whether an authenticator is active and how many recovery codes are left, never a code),beginTotp(the secret and itsotpauth://URI),confirmTotp,recoveryCodesandremoveFactor. Beginning activates nothing -- sign-in asks for no code until one from the app confirms it. Recovery codes are answered once and kept only as salted HMACs; a new set replaces every earlier code, and generating one needs a second factor withinstepUpWindow(ten minutes by default), because a stolen session printing itself recovery codes would keep the account after the session was revoked. Removing the authenticator takes a code or a recovery code in the same request, however recent the session's factor, and removes every recovery code with it. Each change rotates the session.DVAuthEndpoints.stepUpRequiredis the refusal for a missing or stale factor: 401 with RFC 9470'sinsufficient_user_authenticationandmax_age.DVAccountDirectory(implemented byLocalAuthProvider) lets the authenticator list the account under its address. -
The application's own sign-in, as endpoints.
DVAuthEndpointshandles sign-up, sign-in, a second factor, sign-out, the current session, the signed-in person's sessions, revoking one and revoking the others; the generated backend serves them.DVAuthEndpoints.install(credentials: DVCredentialGuard(provider: ...), secondFactors: ...)names the provider, and until then signing in is a 503 saying so. Credentials go through the guard, so an unknown account and a wrong password get one answer after the same floor of time, velocity limits apply per account and per source, and a breached password is refused at sign-up. A session is issued withDVSessionson the request's tenant and replaces one the request already carried. A browser -- a request withOriginorSec-Fetch-*, which a script cannot remove -- gets it only in the__Host-dv_sessioncookie (HttpOnly,Secure,SameSite=Lax,Path=/); a native client that asks withx-dartvel-session-delivery: tokengets thedvs_token only in the body. An account with a second factor gets a session carryingDVSession.mfaPendingClaim, which the authentication stage refuses on every route withBearer error="insufficient_user_authentication"; a TOTP code or a recovery code at/auth/second-factor, counted against the account's velocity limit, completes it throughDVSessions.completeMfa, and only the rotated token carries the person's privilege. Sign-out revokes on the server and clears the cookie. The sessions endpoints answer for the signed-in person's own sessions on the request's tenant: another person's session id is 404, and revoking the others keeps this one. Nothing presented is logged or echoed, and every answer isno-store.DVSession.toJsonandDVSession.fromJsondescribe a session without its token. -
A route refused for want of a caller says so.
DVBackendPolicy.checkActionanswersDVPolicyDecision.allowed,unauthenticatedorforbidden, andallowsActionis it answering allowed.unauthenticatedis the one case where signing in would change the answer: nobody authenticated,decideis not set, and the registered policy's user parameter is not nullable (DV.Auth.authorization.requiresCaller). Every other refusal -- outside a key's scopes, an action nothing registered,decidesaying no, a caller the policy cannot take, the policy saying no -- staysforbidden. -
A route's policy is asked about the signed-in person.
DVBackendPolicy.allowsActionasked a registered policy with the API key principal or nobody, so a policy written against the application's user refused everybody signed in with a session.DVBackendPolicy.callerFornow picks the caller: the key or OAuth principal when the platform API authenticated the request; otherwise the application's user when the policy can take it -- a policy takingObject?can, souser is Accountanswers for a signed-in account -- thenDVSessionPrincipalfor a policy written against that. The choice is made from the types the registry holds for the policy (DV.Auth.authorization.acceptsCaller) rather than from a name the generator assembled. A GraphQL subscription keeps the session caller as it keeps a key's, since its stream starts outside the request. -
The application's own session is a caller.
DVSessionAuthenticationis the authentication stage for a session: aBearer dvs_...token or the__Host-dv_sessioncookie (dv_sessionin development) becomesDVSessionPrincipal.current-- the session, its user id and tenant, the application's user fromresolveUser, and the membership in the organization on the request's tenant -- and an injectedDVContextcarries it ascontext.sessionandcontext.user, besidecontext.apiPrincipal. A presented session that does not authenticate -- unknown, rotated away, revoked, expired, issued on another tenant, or whose userresolveUserno longer finds -- is one 401 with no reason in it, and one carried by the cookie clears the cookie. The user and the membership are read on every request rather than kept from sign-in, so a role changed mid-session applies to the next request. Any other bearer token, an API key and an OAuth token pass through untouched, and a session presented to a process that installed no stage is a 503 rather than ignored.DVSessionstokens now start withdvs_, a session records the tenant it was issued on (atenantcolumn inDVDatabaseSessionStore), andcheck(token, tenant:)refuses one from another tenant without recording its use. -
A GraphQL field runs under the policy of what it resolves through.
DVGraphQLField(policy: 'Order.create')is asked the way a backend function's route asks it -- scopes, then the registry, thendecide-- before the resolver runs, on queries, mutations, nested fields and subscriptions; a refused field is null with aFORBIDDENerror and its resolver never runs. A root field declaring no policy answers no API key or OAuth token, as a route declaring no policy action already does. A subscription captures the caller and the tenant whenDVGraphQL.subscribeis called, because its stream is started wherever somebody listens. -
@DVPolicyregistrations are a layer the application's own wins over, and a route'sResource.actionis answered by the registry.DV.Auth.authorization.registerDeclaredis what the generated client and server register policy classes with;registeris the application's, and for the same action and resource it is asked instead, whichever ran first.declaredPoliciesandoverriddenPoliciessay which is which.Order?andOrderkey the same resource.canAction(user, 'Order.view', resource:)asks by name, refusing a caller or resource the policy cannot take -- a policy takingOrderasked without an order -- and saying why once, rather than throwing a cast error.DVBackendPolicy.allowsActionis the gate a generated route with a quotedResource.actioncalls: a principal outside its scopes is refused, then an action nothing registered is refused even whendecidesays yes, thendecideanswers if the application set one, and otherwise the registered policy does, with the principal and no resource.DVBackendPolicy.verifyRegisteredrefuses to start a server whose routes name an action nothing registered.DVBackendPolicy.allowsis unchanged for a reference such asDVPolicies.refund. -
DVPlatformApiAuthmanages the current organization's API keys and OAuth clients throughDV.Auth.authorization.apiKeys.issue,list,rotateandrevoke, andoauthClients.register,listandrevoke, act on the organization on the current tenant and ask the policy registered forDVApiKeyResourceorDVOAuthClientResource(create,viewAny,update,delete), which sees the organization, the key or client, and the scopes asked for. With no policy, or nobody signed in, the answer is no and nothing is written. Another organization's key or client named by id is answered as not found. A rate plan the declaration does not have is refused at issue. A third-party principal is refused by its scopes first. -
@DVModel(history: DVHistory(keep: ...)). The annotation field the Record History section designs, read bydartvel routes: a generated model declaring it writes its change log with every change and reads it back withmodel.history(). -
DVOAuthEndpointsserves the OAuth provider over HTTP. Authorization sends a valid request to/oauth/consentwith its parameters, shows an unknown client or unregistered redirect URI without redirecting, and sends any other error back to the client with its state; the consent answer needsDVOAuthEndpoints.resolveUserto name a signed-in person and answers the redirect as JSON. The token endpoint servesauthorization_codewith PKCE,refresh_tokenandclient_credentials. Token, introspection and revocation take only a form POST, refused before anything in a GET or a JSON body is looked at, so a refused exchange spends no code; a repeated parameter or a client authenticating two ways isinvalid_request. Introspection answers only a confidential client or a key or token whose scopes coverDVOAuthToken.introspect, and only for tokens on the request's tenant. Every token response and error isno-storewithPragma: no-cache; the token and revocation endpoints and the RFC 8414 metadata document allow any origin, the authorization endpoint none.dartvel.platformApi.oauth.issuerfixes the metadata's issuer, which is otherwise the request's origin and then not cacheable.DVOAuthProvider.authenticateClientis public. Errors are fixed text and only an error's type is logged. -
DVRecordTablecan hold a tenant's rows in a shared table, and a delete holds to the version it read.scope: DVRecordScope('dv_tenant', tenant)matches every read, update, delete, restore check and history lookup on the column, fills it on a write, and refuses a write naming another value, so two tenants can each keep ano1without either reading, updating or deleting the other's; history and captured changes are recorded under the scope's tenant. A schema-qualified table name (acme.orders, whichschemaPerTenantresolves to) is accepted, and nothing else that is not an identifier.delete(id, base:)is refused withDVConflictErrorwhen the row has moved sincebasewas read, and a hard delete that matches no row because another writer moved it in between now throws instead of returning, having logged and captured a deletion that did not happen.DVWriteResult.insertedsays whether a write created the record, whichversion == 1cannot, since an update that changed nothing stays at one. -
DVPlatformApiis the authentication stage of a generated backend.authenticateRequestresolves advk_key ordvat_access token in anAuthorization: Bearerheader on the current tenant, applies the key's declared rate plan, and answersDVApiAuthentication.nonefor any other header. Every credential refusal is the same 401; a store that fails is a 503 that logs the error type only; a platform credential reaching a process with no platform API installed is a 503 rather than ignored. It buildsDVOrganizations,DVApiKeysand, when OAuth is on,DVOAuthProviderover the application's database on first use.DVApiPrincipal.currentandDVApiPrincipal.actingAscarry the caller in a zone.DVBackendPolicy.allowsrefuses a policy outside the current principal's scopes withDV-APIKEY-002beforedecideis asked. -
DVPlatformApiConfigparsesdartvel.platformApi. Scopes as a list ofResource.actionor{actions, description}, rate plans as{maxRequests, window}with the window written as15m,1hor7d,requireExpiry, andoauthastrueor a map of code, access-token and refresh-token lifetimes. Anything else throwsDVPlatformApiConfigErrornaming the key, including a misspelt key, an empty scope and a plan with no window, which is never defaulted. -
A crash reporter no longer stops a web application from starting, and a crash has one fingerprint on every platform.
dvCrashReportIddrew its random part below1 << 32, which is 0 on the web, where shifts are 32-bit, sonextInt(0)threw. Installation starts a session with a report id, so every generated web application threw before its first frame, and the site build reported it only as "Captured 0 of N routes". The id now draws two 16-bit halves. The fingerprint hash multiplied past 2^53, where web integers are doubles and round, so one bug reported from a browser and from a phone fell into two groups. Each 32-bit multiply is now done in 16-bit halves, exact on both; the value the VM computes is unchanged, so existing groups keep their names.crash_web_numbers_testcompiles a probe with dart2js and runs it under node, since the VM every other test runs on shows neither. -
A server process records its unhandled errors, with its role.
DVServerCrashes.installputs the crash runtime in a web, worker or cron process, and every report it writes carriesDVCrashContext.roleand the device classserver.DVServerCrashes.recordrecords an error as unhandled -- never sampled, written before it returns, never throwing and never re-entering itself -- and sends soon after, because a server does not restart to send; what an earlier process left is sent at install.DVSchedulertakesonFailure, told about each task that throws as it happens, where a failure used to be appended to a list a served process never read.DVQueues.onJobDeadLetteredis told about the last failed attempt of a job only, so one poison job is one report rather thanmaxAttempts.DVCrashSink.repositorykeeps a backend's own reports in its crash table without a request to itself, anddvServerCrashDirectoryForputs records inDARTVEL_CRASH_DIR, else.dartvel/crashesbeside the application, and never at/. -
@DVModeldeclares a subject path and retention.subject:takesDVSubject.self,#field,DVSubject.field('column')orDVSubject.through('column', parent: 'Model');retain:takesDVRetention.days(n)orDVRetention.indefinite.@DVModel.retain(years:, because:)marks the field whose row a law requires to keep, and@DVModel.sensitiveField(onErase: DVErase.anonymize)says an erasure replaces the field and keeps the row.DVRetention.daystakesfromoptionally, withDVRetention.deleteandDVRetention.anonymizeforthen:, andDVPrivacyModelrefuses a dated retention with nofromcolumn: no sweep could ever find such a row expired. -
Crash reports can go to the deployment's own backend.
DVCrashSink.dartvel(endpoint:)posts a report and returns when the backend has it or has refused it for good (400, 413, 422), so a report the backend will never accept is not sent again on every launch; a 5xx, a 404 or no answer throws and leaves the record for the next launch. On the backend,DVCrashIngestaccepts a body, refuses one overmaxBytes(413) or one that is not a whole report with a usable id, install id and release (400), treats a resend as delivered (200) without storing it twice or spending the install's budget, counts rather than stores pastperInstallPerHour(202, withDV-CRASH-004once an hour), and answers 503 when the store fails, without the failed attempt spending the budget. Nothing a report carries is logged or answered on any of those paths, and a store's error is logged by its type alone, because a database error quotes the values it could not insert.DVDatabaseCrashReportRepositorykeeps reports indv_crash_reports, and.application()asks the application'sDV.Databaseon each call.dartvel.crashesacceptssink: dartvelandingest: {perInstallPerHour, maxBytes}, read as strictly as the rest. -
dartvel.crashesis read strictly.DVCrashConfig.parsereadsenabled,disabledIn(debug, profile, release),sink,nonFatalSampleRate(0 to 1),breadcrumbs,fullReportsPerReleaseandidentity.consent, and refuses anything it cannot honour with anArgumentErrornaming the key: a string where a boolean belongs, a sample rate of 25, a misspelt or unknown key, a sink this build does not have. Every setting has a default, which is why none is defaulted when it is wrong -- a replaced setting looks exactly like an honoured one.toDeclarationwrites back whatparsereads, so a generated runtime parses the rules its build checked. -
A crash report's user id is bound to consent, and its flags are the ones in force.
DVCrashIdentityholds the account an install is signed in as and hands a report the id only throughDVConsent.boundIdentity, read when the report is written: no grant, no id, and the report still arrives; a withdrawal takes effect on the next crash. A consent policy that does not declare the category is no identity rather than an exception inside the crash handler.DVCrashContext.userIdcarries it, and is absent from the JSON when null.dvCrashFlagsSnapshot()is every declared flag's answer when the report is written, through the newDVFlags.peek: no exposure is recorded (a crash must not count somebody into an experiment), no diagnostic reported and nothing pinned, and a flag settled on next launch answers with the value this process kept, which is what the crashing code was running on. A context that cannot be read is an empty snapshot, not a lost report. -
Crash records have somewhere to go in a browser.
DVFileCrashStorethrows on the web by design, so a web build had no store a crash handler could write to before the page went away.DVKeyValueCrashStorekeeps records, sent notes and the per-release count in any synchronousDVCrashKeyValue--localStoragein dartvel_flutter -- under a prefix, so it shares the page's storage without reading other keys as records, and the count survives a reload loop the way the file store's survives a restart. -
DV.AnalyticsandDV.Privacyhave a runtime to be.DVAnalyticsRuntime.startbuilds consent and the pipeline over one database, keeps an install id in it across launches, reads the stored consent, connects Feature Flags' exposure whenDVAnalyticsSettings.flagExposureCategorynames a category, and installs the analytics privacy adapters -- and an event tracked before all that has finished waits for it. Judged at the call instead, an event tracked in the first milliseconds of a launch was checked against the declared default, so a category defaulting to granted recorded somebody who had withdrawn it. A pipeline that cannot start drops events with the reason.DVPrivacyRuntimeholds the configuredDVPrivacy, fromDARTVEL_PRIVACY_KEY(hex or base64, at least 32 bytes, no default) orconfigure, and adds every installed adapter to it whichever is configured first.DVAnalyticsSettings.fromConfigreadsdartvel.analytics(store,consent,flags.category,sessionCap) andDVConsentPolicy.fromConfignow refuses what it does not understand: an unknown key, a category body that is not a map, and arequiredortrackingthat is not a boolean, each of which used to be read as absent or false.dvLocalAnalyticsDatabaseopens a device's own SQLite file for consent in the per-user data directory each platform keeps (dvAnalyticsDirectoryFor,DARTVEL_ANALYTICS_DIRwins), and holds it in memory on the web, underflutter test, and where there is no such directory. -
Sign-ups that hit a taken address count against the source. A sign-up that signs somebody in cannot hide that an address was free, so
DVCredentialGuard.signUpmakes probing for accounts through it expensive instead: eachaccountExistsrefusal is recorded against the source that sent it, and a source over itsperSourcebudget is refused withDV-EDGE-005before the challenge, the breach check or the provider runs. Nothing is counted against the address itself -- a sign-in lockout that only taken addresses could trip would be the same oracle again -- and a sign-up that creates an account counts for nothing.DVVelocityLimitergainscheckSourceandrecordSourceFailurefor limits of that shape. -
An LDAP username the directory does not hold costs a bind.
DVLdapAuthenticator.authenticatereturned as soon as the user search came back empty, skipping the password bind, so a miss answered a whole round trip to the directory sooner than a wrong password and the difference listed the usernames that exist. A miss now binds the supplied password to a random DN under no entry, and discards the answer: RFC 4513 asks for invalidCredentials there, and a directory answering noSuchObject has still refused a sign-in rather than failed. The stand-in DN is never the typed username, because a failed bind against a real entry counts toward its lockout. -
A sign-in no longer says whether the account exists.
LocalAuthProvider.signInthrewAuthFailure.unknownAccountwith "No account exists for that e-mail address." for a missing account andAuthFailure.invalidPasswordwith "That password is incorrect." for a wrong password, which let anyone test which e-mail addresses had accounts; only an application that wrapped its provider inDVCredentialGuardwas spared. Both are nowAuthException.invalidCredentials(AuthFailure.invalidCredentials, "That e-mail address and password do not match an account."), the same refusal the guard gives. The work is the same too: a miss verifies the password against a dummy hash made once, by the provider's own hasher, when the provider is constructed. It used to hash the password and then verify it, which is twice the work of a wrong password and answered a miss measurably slower.signUphashes the password before looking the address up, so a taken address no longer answers before the hash.AuthFailure.unknownAccountandAuthFailure.invalidPasswordare deprecated rather than removed, so existing switches compile; no Dartvel provider throws them, andDVCredentialGuardstill collapses a provider that does, behind its refusal floor, which remains the defence for a provider whose lookup itself takes time. -
The processes of a deployment share a store, from
DATABASE_URL.DVProcessStores.installputsDVQueueson a database withDVDatabaseQueueAdapter, unless an adapter was already configured. Outside a preview that database isDATABASE_URLalone, read throughDV.Secrets-- environment, then systemd credentials, then.env-- on a connection of its own:DV.Databaseis left for the application to configure, andDARTVEL_DATABASE, a preview's variable, is ignored, so a production deploy copied from a preview's settings does not put its jobs on the preview's database. In a preview it is the adapterDVPreviewServer.startalready configured with the preview's own database. Without a store it installs nothing; aDATABASE_URLit cannot read throwsDVProcessConfigurationErrorwithout repeating the URL.scheduleLeaseFor(process)is aDVDatabaseScheduleLeaseon that database for any process that ticks the schedules, and null for one that ticks none or was toldDARTVEL_SCHEDULE_LEASE=none. A declaredcronprocess with no shared store throws instead of starting: nothing in the process can tell whether it is the only cron process, and a second one fires every schedule again rather than failing. -
DVDatabaseScheduleLeaseclaims an occurrence in the application's database. A row keyed to the task and the occurrence's instant in UTC indartvel_schedule_leases, so the database's uniqueness decides between two processes on Postgres, MySQL or a SQLite file; an insert that fails is a lost claim only when the row is there afterwards, and otherwise rethrows, so the scheduler records it and runs nothing. Held forhold(two days) and never released. -
DVProcessHealth.serveis the health endpoint of a worker or cron process.GETandHEAD /healthzanswer200 {"status": "ok", "role": ...}; every other path is 404 and every other method 405, so the port serves nothing of the application. -
DVProcessConfigurationreadsDARTVEL_HEALTH_PORT,DARTVEL_SCHEDULE_LEASEand--max-jobs.healthPortis validated likeDARTVEL_PORTand refused for a web process, which answers on its own port.scheduleLeaseWaivedtakes onlynone, and is refused for a process that ticks nothing.maxJobsis a worker's--max-jobs, at least 1, and is refused for any other role. -
DVQueueWorker.runreturns how many jobs completed and takesmaxJobs, returning once that many completed or a pass completed none. -
DVDatabase.configuredAdapteris the adapterconfigurewas given, or null, without resolving a tenant's database or throwing.DVTestHarness.unconfigureQueuesreturnsDVQueuesto a process that configured nothing. -
Two workers on one database queue no longer run a job twice.
DVDatabaseQueueAdapter.reserveread the next queued row and then marked it by id, so two workers polling one table both read the row before either marked it and both ran the job. It now claims the row with anUPDATEthat only matches while the row is still queued and counts the claim only when it changed a row; a worker that loses a row reads the next one rather than reporting an empty queue. -
SqliteDVDatabaseAdapter.filewaits for another connection's write lock. It setsPRAGMA busy_timeout(busyTimeoutMilliseconds, 5000 by default) before anything else, so a web process, a worker and a cron process sharing one file wait the milliseconds another write takes instead of failing at once with "database is locked". -
dartvel.infraprovisions several backend instances, workers and a cron unit.DVInfraBackendCapabilitiesnow defaults every flag to true, because the generated backend readsDARTVEL_PORTandDARTVEL_ROLE; a flag set false still refuses the declaration, for a backend generated before. Backend units binddartvel.server.port,+1,+2... throughDARTVEL_PORT. Withcron: { enabled: true }every backend unit isDARTVEL_ROLE=weband<app>-cron.serviceisDARTVEL_ROLE=cron; withenabled: falseevery backend unit isweb, no unit ticks the schedules and a note says so (this was refused); with cron unstated the first backend unit keeps no role and ticks and the rest areweb. Worker units areDARTVEL_ROLE=workerwithDARTVEL_QUEUE. A unit that used to sayDARTVEL_ROLE=backend, which the backend refuses as unknown, no longer does. The Caddyfile balances every instance in onereverse_proxywithlb_policy round_robin,lb_try_duration 5sandfail_duration 30s, so a stopped instance leaves the rotation.logs.shipand the container and onprem adapters are still refused. -
A backend process is told its role and port, and refuses what it cannot honour.
DVProcessConfiguration.resolvereadsDARTVEL_ROLE(or--role) asweb,workerorcron,DARTVEL_PORT, and a worker'sDARTVEL_QUEUE(a comma-separated list,defaultwhen unset). A port that is not a whole number from 1 to 65535 -- empty, signed, padded, hex, out of range -- throwsDVProcessConfigurationErrorinstead of falling back to the generated port, and so does an unknown role, a--rolethat disagrees with the variable, or a queue named for a process that works none.ticksSchedulesis true forcronand for a process given no role, which is the whole deployment; a process declaredwebleaves them to the cron process. -
DVQueueWorkeris the loop a worker process runs. It works each named queue in turns ofbatchjobs, waitsidlewhen a pass found nothing, and stops whenuntilcompletes. It refuses to start on the process-local default queue, which no other process can dispatch to, and with no job handler registered, which would dead-letter everything it reserved.DVQueues.adapterConfiguredandDVQueues.hasHandlersare new. -
A schedule can fire once across several processes.
DVSchedulertakes alease, claimed for each occurrence before it runs;DVCacheScheduleLeaseclaims it withwriteIfAbsenton anyDVAtomicCacheAdapter(Redis, Memcached), keyed to the occurrence's instant in UTC so timers that land seconds apart claim one key. An occurrence another process claimed is skipped; a store that cannot be reached runs nothing and records the failure rather than running unguarded. -
What an alert writes reads its value the way Studio shows it. A firing rule's summary -- the first line of its incident, the notification body, the pager summary and
DV-ALERT-001-- printed the raw reading, so an incident read "errorBudgetBurn catalog-search is 6.703703703703697, above 6".DVSignalRef.format(value)is new and writes a burn rate as6.7×, a latency in milliseconds to a tenth, a crash rate or fleet health as a percentage, and a count or metric with at most two decimals, never rounding a real value to0. The summary uses it for the reading and the threshold alike, so a latency threshold keeps its fraction of a millisecond. Only the text changed; readings keep their numbers. -
A service level whose window saw no requests has no budget reading, not a full one.
DVServiceLevels.statusreportedbudgetConsumed: 0when samples were read and no request arrived between them, so a service that stopped answering showed its whole budget left. It is now null, likeerrorRateandburnRatealready were, andhasDatais false.DVServiceLevelStatus.requestsis new: the requests the window saw, 0 for this case and null with fewer than two samples.DVErrorBudgetDecisiongainsunmeasured, the levels whose budget could not be read;holdis unchanged and still holds only on an exhausted budget.DVErrorBudgetReleaseGatealready held a rollout on an unread budget and now holds on this one too, saying the level saw no requests rather than that it has no samples (evidence['noTraffic']). -
An incident an alert opened is never published under the alert's name. An alert titles its incident after its rule (
Alert catalog-search-burn) and a crash spike after its release, andDVStatusSnapshot.buildpublished that title the moment anyone posted a public update without renaming the incident first.DVIncident.titleSource(alert,crashorhuman, kept through storage) records who wrote the title, andDVIncident.publicTitleis what the snapshot andDVStatusSubscribers.announcenow use: the title as given when a person wrote it, otherwiseIssue affecting <components>, orService issuewhen none are named. The public update is still published; holding it back until a rename would leave the page reading as operational during a declared incident.openIncidentattributes the title to itssource, andupdate(title:)to the update's. An incident stored before this change is read as titled by whatever opened it, so an alert incident a person renamed before upgrading shows the neutral title until it is renamed again. -
On Android and iOS the application key is in the keyring or it is refused; it is never kept in a file.
DVAppKeyStores.chooseused to hand both platformsDVFileAppKeyStoreunder the home directory, which no platform key store protects. iOS now gets the Keychain, and Android gets the Android Keystore store passed as the newandroidKeystore:factory (dvAppKeyStoreForin dartvel_flutter supplies it). Without one, the store isDVUnavailableAppKeyStore, and every call on it refuses. Both are wrapped inDVMigratingAppKeyStore, which moves a key an earlier version left at the old file path into the keyring the first time it reads. The file is removed only after the keyring copy reads back equal. A keyring that refuses, drops or alters the write leaves the file in place and throws, and writes go to the keyring alone. A keyring that cannot answer throws the new typedDVAppKeyStoreUnavailable(store, reason, platform status) rather than falling back.DVAppKeyStores.platformtakesplatform:andandroidKeystore:, and asks for a Secret Service only on Linux.DVAppKeyStores.legacyFilePathnames the old path.DVDescribedAppKeyStorelets a store defined elsewhere say where it keeps the key. -
The Keychain store works on iOS, keeps its item on this device, and refuses with a reason.
DVKeychainAppKeyStoreis available on iOS as well as macOS. Its item is writtenkSecAttrAccessibleAfterFirstUnlockThisDeviceOnlyand not synchronizable, so it never syncs to iCloud Keychain or restores onto another device. Replacing a key updates the item in place: the old delete-then-add could lose the key when the add was refused. A failingOSStatusis aDVAppKeyStoreUnavailablefromdvKeychainRefusal, which names a device not unlocked since boot (errSecInteractionNotAllowed), a missing entitlement, and an absent keychain.debugItemAttributes()reads back the item's accessibility class and sync flag. -
An alert's state says who it reached, who it missed and whether it is resolving; an incident entry says who wrote it.
DVAlertStategainsdeliveredTo,missed(each unreached user, pager or unresolvable team with the reason from the latest attempt),resolvingSinceandlastNotifiedAt, all read-only;DVAlerting.pendingResolves(rule)names the pagers still owed a resolve.DVIncidentEntry.actoris kept through storage and set byupdate(actor:)andresolve(actor:); the public snapshot never carries it. -
Consent to keep world anchors comes from the application's consent records.
DVConsentSpatialConsent(consent, category:, anchors:)answersDVSpatialConsentfromDVConsent: keeping a world anchor is agreed to while the declared category is granted under the policy version in force, so a grant recorded under an older version does not count, and sharing an anchor is never agreed to here. A category that is required or granted by default is refused at construction, since either is a grant nobody gave. Itsanchorsstore is the one to hand the runtime: a write runs only while the agreement holds (DVSpatialAnchorNotStoredotherwise), and a token read after the agreement lapsed is deleted rather than returned. A withdrawal -- including oneDVConsentcould not record -- deletes every kept token, not only the ones an open scene names;enforce(), called afterload(), deletes tokens kept under an agreement that lapsed while nothing was listening.privacyAdapter()is the Data Compliance adapterxr:anchors: an erasure of the install, or of a user its consent records name, deletes the tokens, including when Product Analytics' consent adapter has already replaced those ids with the pseudonym; an export lists the anchor ids and never a token. -
A world anchor token that was not stored is not marked stored. A session wrote the token the OS handed back and marked the anchor persisted before the write had succeeded, and the session's queue swallowed the failure:
isPersistedsaid true, nothing was on disk, and the mark stopped every later attempt. A failed write now leaves the anchor unpersisted and logs an error naming the anchor and, for aDVSpatialAnchorNotStored, the store's reason -- never the token. Consent is asked again once the OS hands the token over, so a withdrawal made while the OS was persisting stops that token too.DVSpatialAnchorStoregainsids(), so a withdrawal or an erasure can reach tokens no open scene names; an implementation outside Dartvel has to add it. -
XR: the platform-independent runtime for presenting a scene in space.
DVSpatialCapability(withheadset()andglasses()) reads a capability report strictly: a field not reported is false, and a member this version does not know is dropped.DVSpatialConvention.poseToWorldconverts a device pose into the scene's world by change of basis through the same matrix a document in that convention gets, and refuses a non-unit orientation. Scene nodes carry a typedDVAnchor(plane, image, hand, world) that round-trips in the document; the graph places an anchored node at a rigid frame, hides it, or leaves it at the origin, and refuses a mirrored frame.DVXRDeviceis the adapter contract, one method per binding the specification names, withDVXRFakeDeviceas a strict headless device.DVXRRuntime.presentopens a volume or immersive space or says why not (DV-WINDOW-014/015,DV-XR-006,DV-WINDOW-004), keeps immersive spaces exclusive, and aDVSpatialSessionasks for the camera before passthrough, falls back to a full space lit by the studio withDV-XR-001, closes the space and the session when the application leaves the screen, the camera permission is revoked, the event stream breaks or the session ends, persists a world anchor only with consent and only as the OS token (DV-XR-003when it does not re-localize), delivers hand and controller rays in world space and gaze only as a selected node, applies the comfort policy (DV-XR-005) and reports a sustained frame-rate drop once (DV-XR-007). No pose, anchor position or light probe reaches a report. Presented flat, a scene with anchors or the passthrough environment still renders and says so once (DV-XR-002,DV-XR-001). No native binding exists yet on any target. -
A flag resolution names the rule that decided it, and a debug override can reach the whole process.
DVFlagResolution.ruleis the index of the rule that served the value or held the default (DV-FLAGS-005/006), null for a default or an override.DVFlags.setDebugOverride(flag, value)andclearDebugOverrideput an override in force for every read in a debug build, under anywithOverrideszone, notifyDVFlags.changes, refuse a value the flag cannot read, and do nothing in a release build.DVFlags.overridesInForceandoverridesAllowedare whatresolvehandsevaluate, so a tool evaluating another context answers as the app does. -
A preview process starts as a preview or not at all.
DVPreviewServer.start(environment)returns null and installs nothing outsideDARTVEL_ENVIRONMENT=preview. In a preview it switches capture on first, then refuses to start (DVPreviewStartupException) when the preview settings cannot be read, whenDARTVEL_QUEUE_NAMESPACEis notpreview-<name>, whenDARTVEL_DATABASEis missing, does not carry the preview's digest or equalsDARTVEL_PRODUCTION_DATABASE, whenDATABASE_URLcannot be read, or when a members preview has no membership check. Only after every check passes are the queues namespaced andDV.Databaseconfigured fromDATABASE_URLaimed at the preview's own database;DV.Database.configurethen refuses a server adapter naming any other database.wrap(handler)puts a handler behind the preview's access gate. Capture no longer waits for a startup call either: a process whose environment says preview captures mail and notifications, and runs no undeclared schedule, from its first send, including a job worker that never starts a server. Preview deployments now also writeDARTVEL_PRODUCTION_DATABASE, on create and on redeploy. -
DVDatabaseConnectionresolves a database from the environment.DATABASE_URL(postgres, mysql, sqlite, withsslmode) gives the server and credentials andDARTVEL_DATABASEnames the database on it, so one preview secret with no database in it serves every branch.DVDatabaseConnection.parserefuses a server URL that names no database,open()returns the matching adapter, and printing a connection never prints its password. -
Queues live under
DARTVEL_QUEUE_NAMESPACE.DVQueuesdispatches, works, lists and flushes<namespace>.<queue>whenuseNamespacesets a namespace or, in a process whose environment says preview,DARTVEL_QUEUE_NAMESPACEnames one; outside a preview the variable is ignored. A preview therefore never reserves a production job and production never reserves a preview's. A process with no namespace cannot name a queue under apreview-namespace, and a process in a preview with no namespace uses no queue at all. Applied inDVQueuesrather than in each adapter, so every adapter gets it.retryanddiscardstill take a job id as given. -
3D Scenes: the platform-independent runtime.
DV3DSceneDocumentstates its units, up axis and handedness, keeps node ids, child order and keys a newer version wrote, encodes canonically, and refuses a duplicate id, an undeclared asset or a non-finite or zero-scale transform at decode.DVSceneGraphconverts that basis into right-handed Y-up metres and picks against each node's true local shape.DVSceneAssetPolicyrefuses a stored key under another tenant, an unlisted host or an undigested asset before any request;DVSceneAssetLoaderverifies the digest and reads models withDVGltf, which counts triangles and bounds through the node hierarchy without decoding geometry.DVSceneRuntimedrives aDVSceneRendereradapter -- one upload per asset, every resource released once, the previous scene kept on screen while an update loads, andDV-3D-001reported once per boot per cause -- andDV.Test.fake3D()substitutes the headlessDVSceneRecordingRenderer. No renderer is configured by default: until a Flutter GPU adapter exists, every scene presents its poster. -
A media player and capture runtime that report what the device did.
DVMediaControlleris the platform-independent player behindDVBox.video/DVBox.audio:state,position,duration,bufferedanderrorare read-onlyDVMediaSignals moved only by backend reports. Play is notplayinguntil the backend says so; a backend that says playing while the position stops advancing is reportedstalled; a position measured before a seek completed never moves the scrubber back. Protected content with noDVDrmAdapterfor its scheme is refused before the backend sees it (DV-MEDIA-102); an adaptive stream on a backend without streaming falls back to the source's progressive rendition (DV-MEDIA-101) or is refused. Background audio requested without the capability declared warns (DV-MEDIA-103) and pauses with the application. Audio focus (DVAudioFocus) is taken on play and given back on pause, completion, failure, platform loss and dispose.DVMediaCapturerecords audio and video into aDVFile: the capability report is checked before permission is asked, a refusal isDVCapturePermissionRefused(DV-MEDIA-104),capturingfollows the device's confirmations, and backgrounding, a revoked permission or a lost device end the session asDVCaptureInterruptedwith the partial file. Recordings are created 0600 in a 0700 directory before the device writes. Backends, focus, DRM, permissions and timers are interfaces with fakes. -
@DVModel.model3dField(poster:, maxSizeMb:, maxTriangles:). A model field that holds aDVSceneAsset.DVModel3DFieldPolicy.validaterefuses an upload that is over the size (before parsing it), is not a glTF model, or draws more triangles than the budget, with every reason;DVModel3DFieldPolicy.acceptturns a valid upload into the field value, pinned to its bytes by SHA-256 and under the tenant's storage key. -
Scene documents are content.
DV3DSceneContentruns scene documents through the content workflow as thescenekind, and only a published version reachesDV3DSceneStore; a withdrawal removes it. ADV3DSceneBundlecarries published scenes and their approvals to installed applications, decodes every scene before anything is applied, and refuses a scene shipped twice or shipped and removed at once;DV3DSceneBundleInstallerapplies a version once and rolls back by forgetting and re-shipping the previous bundle. A document's keys from a newer version now encode in sorted order, so a scene read back from the workflow encodes to the same bytes. -
Schema Evolution's tracker and Backend Release Management agree on where a migration stands and whether it may contract. Verification is now its own recorded state:
DVSchemaEvolution.verifyrecords that every chunk agrees for the whole window, a discrepancy recorded afterwards takes it away, and the tracker reportsDVReleaseMigrationPhase.verifiedwhile it stands. Reads then move withswitchReads, in a release of their own, while both shapes are still written (readSwitched). The contract, which stops writing the old shape, now reportscontractedrather thanreadSwitched. Before, a rollback planned against the tracker could restore a release that reads a column nobody writes any more. The contract and the later drop are decided byDVContractDecision, the same decisionDVContractStepGatemakes, so a contract the gate holds because the release being replaced still reads the old shape is refused by the tracker too. Before, the tracker contracted straight from verify. A saved state that has no verification recorded reads as not verified. -
A Platform Memory arena can be lent to a worker.
DVPlatformMemoryis aDVWorkerLendable, soDV.Workers.run(task, input: slice.addresses, lend: [arena])hands a worker an arena's addresses and the worker writes in place, which the caller reads through the slice without a copy. While lent,reset(),dispose()and lending it again throw. An address is not a Dart view, and the native backing frees a segment when its last view is collected, so an arena disposed under a worker was a use-after-free, and one reset under it handed the worker's bytes out again. The pool keeps the arena reachable until it gives it back: with the worker's answer, or, for a cancelled or timed-out run, only once the isolate has exited. A disposed arena cannot be lent. -
Expand/contract runs as phases with gates, and its backfill is a verified, throttled job.
DVSchemaEvolutionwalks expand, dual-write, backfill, verify and contract, and refuses a release that already ran a phase, because each phase is its own deploy. The read switch needs every chunk backfilled, every chunk verified, and no dual-write discrepancy inside the whole verification window (DV-SCHEMA-004,DV-SCHEMA-007); the contract, and the later drop of the old column, are refused while a client on a protocol older than the expand's still calls (DV-SCHEMA-005), and a missing client histogram is refused rather than read as none.DVSchemaEvolutionStorekeeps the state in the database, and itsphaseSourceanswers Backend Release Management'sDVMigrationPhaseSourcethroughDVSchemaEvolution.releasePhase.DVBackfillcopies a column in chunks by key and records each chunk after writing it, so a restart resumes after the last one and a pause holds across restarts;verifyhashes each recorded chunk on both shapes -- a swapped pair of values is caught where a count agrees -- names a mismatched chunk (DV-SCHEMA-004) and reports a chunk that verified and then diverged as a dual-write discrepancy (DV-SCHEMA-007).DVBackfillThrottlestarts at thedartvel.database.tierrate (500, 2,000 or 10,000 rows/s), halves when replica lag or write latency crossesdartvel.database.backfill's budget, steps back up by a tenth of the starting rate while under it, and reports staying below its floor past its patience once (DV-SCHEMA-003).DVSchemaBackfillsruns a backfill as slices onDVQueues, each queueing the next, and a paused one stops queueing itself. -
Platform memory: a budget reserved once and handed out arena-style.
DVPlatformMemoryreserves its budget up front in power-of-two segments (native heap through FFI on native targets, typed data on web) and hands outDVInt/DVDouble/DVBoolscalars andMemorySlicelists over int8..int64, float32 and float64 storage. A list larger than a segment is chunked with shift-and-mask indexing and never split when it fits one.reset()makes the arena reusable and invalidates every handle given out before it, including atransformAsyncthat was waiting to resume;dispose()releases it.securedBytesreports what was granted, and less than asked is recorded asDV-MEMORY-001; an exhausted arena throwsDV-MEMORY-002without consuming anything,int64on web-js throwsDV-MEMORY-003, andtouchPageson a mobile or embedded target is refused asDV-MEMORY-004.DVMemory.allocateappliesdartvel.memorydefaults, per-target ceilings and device-profile overrides, and registers each arena for aggregate usage. The four codes are registered, and the registry check now reads codes the specification lists in a text block as well as tables. -
A sweep or erasure no longer removes a row rewritten after it was read.
DVPrivacy.sweepRetentionread the expired rows, then deleted or anonymized each by key alone, so a session renewed between the read and the write was removed anyway, and its history and captured changes with it. An erasure had the same gap: a row moved to another customer after the walk was deleted as the subject's, and a kept row anonymized from a stale read took the version the rewrite already held, so the writer that raced it could save the erased value back without a conflict. Each write now applies only at the version read (a sweep also requires the retention timestamp it read), and history purge and capture follow only a write that applied. A sweep leaves a contended row for the next run, reports it in the newDVRetentionSweep.skipped, and counts it inremainingwhile it is still expired. An erasure,replayErasuresandDVOfflineStorePrivacyAdapterread the row again and erase it at its new version while it still belongs to the subject; a row that no longer does is left alone and kept out of what record adapters are handed. -
DV.Workersruns on web workers in a browser. A web build now picks a web worker per task wherever the page hasWorker, and runs inline only where it does not. The page sends the task's registered name and its input to a worker script --DVWorkerTasks.script, compiled from an entry that registers the same tasks and callsdvWebWorkerMainfrompackage:dartvel_core/web_worker.dart-- and a task that is not registered or an input that cannot be cloned fails by name before a worker is started. Cancelling or timing out terminates the worker, and a script that fails to load, or an error nothing in the worker caught, is a crash rather than a page waiting forever. The pool is sized fromnavigator.hardwareConcurrency. Exercised end to end by compiling a page and a worker with dart2js and running them under Node with aWorkerbuilt onworker_threads; that proves the runner through dart2js's real interop and says nothing about any one browser. It found that reading a task through the pool's erased types was a covariance TypeError in dart2js on the first run. -
What a web worker and its page say to each other. A web worker is a separately loaded script that cannot be handed a Dart function, so a task crosses by a name registered on both sides with
DVWorkerTasks.register, and one name for two functions is refused.dvWorkerUnportablenames the first value structured clone would refuse and where it sits -- a class instance, a function, a map keyed by something other than a string -- where a browser throws aDataCloneErrornaming nothing.dvWorkerHandleis the worker's side of a request: progress, then a value or a failure, always a final answer, including for a task the worker does not have, a result that cannot be cloned back, and a request it cannot read.dvWebWorkerCapabilityreports a web worker as supported with limitations and shared memory only on a cross-origin-isolated page, andDV-WORKER-004is logged once when bytes were copied because it is not. -
Native memory lent to a worker by address.
DVWorkerBuffer.allocategives zero-filled memory outside the Dart heap, andDV.Workers.run(task, input: buffer.lease, lend: [buffer])hands a worker its address: the worker, or native code it calls, reads and writes it in place, and the caller sees the writes without a byte copied either way. A buffer is passed, not shared -- while it is lent the caller cannot read it, free it or lend it to a second run, which throws in the caller's frame. It comes back with the worker's answer, but a run that was cancelled or timed out keeps it until its isolate has actually exited, because that isolate may still be writing and memory freed under it would crash somewhere else, later. A blocking native call made on a worker leaves the caller its event loop. Web builds get the same names, which refuse, andcapability.zeroCopyNativeis false there. -
DV.Workers: declared offloadable work on a bounded pool.DVWorkers.run(task, input: ..., onProgress:, cancellation:, timeout:)runs a top-level or staticDVWorkerTaskon its own isolate on the VM, and its future always completes exactly once -- with the value, with what the task threw, cancelled, timed out, or as a crash when the worker ended without answering, including a worker that exited, died of an uncaught asynchronous error, or awaited something nothing could complete. Input, result or captured state that cannot cross the boundary fails by name (DV-WORKER-002for a capture) and frees its slot. Cancelling or timing out kills the isolate rather than forgetting it, and the slot is held until the isolate has actually exited, so the bound counts threads that are really running. The pool is sized from aDVWorkerProfile-- one core left for the UI, never more than eight -- rather than from a number the application sets, and saturation is logged once per episode asDV-WORKER-005.DVCancellation.until(signal, ended)binds a run to a lifecycle so no progress or result reaches an owner that has gone. Where there are no threads the pool runs work inline and says so once throughDV.logasDV-WORKER-001;DVWorkers.capabilityreports which mechanism carries the work and whether memory is shared or copied. -
A retention sweep's deletes reach the capture log and the warehouse.
DVPrivacy.sweepRetentionremoved and anonymized expired rows with SQL of its own besideDVRecordTable, so no change was captured: a warehouse fed byDVCaptureConsumerkept exactly the rows the source removed for retention, an anonymizing sweep left the personal values standing there, and the capture log kept every earlier value of both. The same was true of an erasure with noDVCapturePrivacyAdapterregistered, ofreplayErasuresafter a restore, and ofDVOfflineStorePrivacyAdapterover a captured table. Each removal now purges the log's values for the row and captures an erased delete, or the anonymized row, through the newDVCapture.recordErasure-- once per row, since the walk leaves a record to the capture adapter when one is registered -- while sweeps stay batched and resumable andplanRetentionstill captures nothing. Separately, an erasure captured the delete of a row already gone at version 0, whichDVWarehouseSinkrightly ignores as older than the row it holds, so an erasure only reached the sinks the adapter erased directly; the delete is now one version past the newest the log or the removed row knows of. -
A transaction belongs to the flow that opened it. The active transaction was a static field, so two requests served by one isolate found each other's: a
DV.transactionin one joined the other's open transaction, one request's failure ran the other's compensations, andafterCommitwork fired on the wrong commit or not at all. The transaction now lives in the zone its body runs in, so nesting within one flow still joins -- through every await, timer and microtask the body schedules -- and an unrelated flow never does.DVTransactionRunner.activeContextis null once the transaction has committed or rolled back, including to work the body left unawaited, andafterCommitcallbacks and compensations run outside the transaction: aDV.transactionopened from one is a new transaction rather than a join onto one that is over, whose callbacks were silently dropped.isolated: trueis unchanged. -
Schema changes are classified by the adapter, and planned. A schema change is described (
DVAddColumn,DVAddIndex,DVAddNotNull,DVChangeColumnType,DVRenameColumn,DVDropColumn,DVCreateTable,DVRawSchemaChange) andDVDatabaseAdapter.classify(change)answersinstant,onlineorblockingfor the server the adapter is connected to, version included: PostgreSQL (a default is a rewrite before 11,NOT NULLis online from 12), MySQL 8 (with the point releases that broughtINSTANT), and SQLite/Turso from the library actually linked. A MariaDB or pre-8 MySQL server, the in-memory adapter and hand-written SQL classify nothing.DVSchemaPlannertreats an unclassifiable change as blocking (DV-SCHEMA-006), refuses a blocking type change as written and plans its expand/contract instead -- only when the expand itself does not block, and saying what the contract step will cost (DV-SCHEMA-001).DVSchemaDeployGaterefuses a blocking change against production without an override that carries a reason, and returns the record to log (DV-SCHEMA-002).DVSchemaSnapshotcarries a production server's provider, version, columns and row counts, so a plan rehearsed against it classifies as production would.classifyis an extension over the adapter contract rather than a new abstract member, so adapters written elsewhere keep compiling; one that implementsDVSchemaClassifierteaches the planner. -
Alerting, service levels and status pages, as a runtime. A
DVServiceLevelsamples cumulative counts from a source that already exists and reports burn rate over a trailing window and budget consumed scaled by how much of the window was observed, so ten minutes of history is ten minutes of the month; a restart in either count restarts both, andDV-ALERT-003is raised once per exhaustion.DVErrorBudgetGatereads exhausted levels before a deploy.DVSignalRefresolves metrics (missing when unregistered, no data when quiet), nearest-rank trace percentiles, crash rate from release health, a level's two-window burn, and application-registered queue, kiosk and quota readers.DVAlertingmoves aDVAlertRulethrough pending and firing only after a positiveforDuration, resolves after the signal has stayed clear (five minutes at least, so a hovering signal pages once), delivers throughDV.Notificationsto users and teams and throughDVAlertPageradapters (DVPagerDutyPager, Events API v2) under one dedup key per episode, retries only the targets that missed a firing and keeps a refused resolve queued until the pager takes it (DV-ALERT-001,002,006), opens aDVIncident, and reports rules with no target or that fire on most days with no action taken (DV-ALERT-005).DVStatusSnapshotpublishes component health without check detail and only public incident updates;DVStatusPageClientserves the last snapshot marked stale with the time it was true when the application is unreachable, errors or hangs (DV-ALERT-004);DVStatusSubscribersannounces public updates. -
Erasure, subject-access export and retention, as a runtime (
DVPrivacy). Each model declares how its rows reach the person they belong to —DVSubject.self,.field(column), or.through(column, parent:)for a row that belongs to somebody through another row — and the walk over those declarations is resolved before anything is changed, so a row reached through a parent the same erasure deletes is still found. Personal data no subject path reaches is refused at construction (DV-PRIVACY-001); personal data with no retention is a warning (DV-PRIVACY-002).eraseremoves a row outright even from a soft-delete table, because a row marked deleted still holds everything it held; deletes the change log of every erased or anonymized row, because a log entry holds earlier values and a revert would put them back; and removes a sensitive field's ciphertext rather than trusting a key it cannot destroy. A row kept underDVRetain(years:, because:)is anonymized, names the subject by pseudonym, bumps its version so a writer holding the old row conflicts instead of re-saving it, and is reported with itsbecause:(DV-PRIVACY-003). An adapter the erasure could not reach makes it incomplete (DV-PRIVACY-009), never a quiet success. The signed receipt names the subject by pseudonym and stops verifying the moment a field of it is edited. A tombstone log letsreplayErasureserase a subject again when a backup is restored (DV-PRIVACY-005).exportwalks the same graph and leaves out another subject's identifier (DV-PRIVACY-006). Retention sweeps run in resumable batches, preview withplanRetention, and let a longer retention hold what a shorter one would delete (DV-PRIVACY-007,DV-PRIVACY-008). Erasures and sweeps run onDVQueues; every export and erasure is recorded through Record History by pseudonym.DVOfflineStorePrivacyAdaptererases a device's copy, queued writes included. -
Semantic search, as a runtime (
DVSemanticIndex). Search over a model's records by meaning, built on what already exists: writes enqueue an embedding job onDVQueuesand embed nothing inline, keyword and hybrid modes use the model'sDVSearchProvider, andDVAIEmbedderwraps anyDVAIAdapter. Keyword stays the default mode. There is no default embedder (DV-SEMANTIC-001): vectors from two models are not comparable, so an index is a generation named after its embedder, dimensions and chunking. A new embedder builds its generation alongside the old one (DV-SEMANTIC-003); writes land in both, queries keep answering from the old one, embedded with the old model, untilbackfill(complete: true)switches over, and a backfill that fails part-way resumes rather than re-embedding what it already did. Vectors of another length are refused. -
Scope goes into the vector query. A k-nearest query filtered afterwards shows one tenant another's rows or none of their own, so a tenant- or policy-scoped model on an adapter that cannot filter is refused (
DV-SEMANTIC-002), and expressible predicates are pushed down. Every loaded row is still checked against the caller's tenant and policy, whatever the adapter was asked to do. What only a policy can decide is post-filtered with refill up to a bound, and a page cut short by that bound says so (bounded,DV-SEMANTIC-005) rather than looking complete. Long fields are chunked, a record is returned once with the chunk that matched, andretrievehands an AI feature rows without sensitive fields; a sensitive field cannot be embedded at all. A record whose embedding job dead-lettered is reported byabsentRecords()(DV-SEMANTIC-004), since nobody reports a result they never saw. Embedding costs are recorded on aDVMeterscounter, and a tenant past a blocking limit is refused (DV-SEMANTIC-007) before the query is embedded, not quietly answered by keyword search.DVInMemoryVectorAdapteris the reference adapter for development and tests; pgvector and the search services' vector APIs are not built yet. -
Organizations, memberships and invitations, as a runtime (
DVOrganizations). A tenant stays the data boundary and an organization is the group of people on it: one organization per tenant, refused a second (DVTenantAlreadyOrganized), and a tenant with none is a personal tenant, on which resolving a membership isDV-ORG-006rather than a quiet "no role". Renaming, transferring and closing never touch the tenant, so a personal tenant becomes an organization by creating one on it. Roles are typed (DVOrgRole), ordered by declaration, and refuse to compare across two declarations; an undeclared name isDV-ORG-001.DVMembership.grantschecks the organization as well as the rank, so an owner of one organization is nobody in another, and it is what anauthorizationpolicy calls. Invitations areDVAuthTokensmagic links or passcodes: single use, expiring by the organization's clock as well as the token store's, never stored, bound to the invited address (a forwarded link is refused and spent, and the invitation stays pending to resend), refused for an existing member (DV-ORG-002) and for a role above the inviter's own. Seats are a query over memberships under a declared rule (every member, at roles, active within a period) and aDVLevelLimitforDV.Meter; a refused acceptance isDV-METER-004. Counting and joining run under one lock per organization, so five acceptances racing for two seats admit two. The last owner cannot leave or be demoted, including by two owners demoting each other at once (DV-ORG-003). Transfer promotes the successor before demoting the owner and rolls back with an enclosingDV.transaction; closing is restorable within a grace period and refuses work meanwhile (DV-ORG-004). Verified SSO domains join at their declared role, exact domain only (DV-ORG-005). Every membership change carries its actor and transaction in Record History. -
Crash reporting and release health, as a runtime (
DVCrashReporting). A crash is written to disk by the handler, synchronously and with nothing awaited, and sent by the next launch: a handler runs in a process that is already going down, and a write that waits for a round trip finishes after the process does. Recovery marks a report sent before deleting it, so a process that dies between the two does not send it twice, and a record cut short by the crash that wrote it is dropped and named (DV-CRASH-005) rather than sent half-read. Breadcrumbs are a ring buffer redacted on the way in — the logger's key list as substrings, fields declared sensitive as exact names, and resolved secret values in every string — so a card number in a backend call's payload never reaches the stored bytes. Reports group by error type and the top application frames with framework frames trimmed, never by message or line, so an id in a message does not split one bug and the same message from two places does not merge two; an override fingerprint wins where the default is wrong. Past a per-release limit a device's crashes are counted but not written (DV-CRASH-004), and the count is kept in the store because a crash loop is a sequence of restarts. Non-fatal errors are sampled (DV-CRASH-008); crashes never are.DVHangWatchdogfiles one hang per freeze (DV-CRASH-007).DVReleaseHealthcounts crash-free sessions and users from sessions that started — a device offered a release that never opened it is in its cohort and not its denominator — andDVReleaseHealthGateholds a rollout when any cohort falls below threshold, even inside a healthy release average (DV-CRASH-010). Reports stored as files ondart:iotargets and in memory elsewhere. -
Outbound webhooks (
DVWebhooks,DV.Webhooks). Events are declared (DVWebhookEvent) and emitting a name that is not declared is refused (DV-WEBHOOK-006), so the catalog a customer reads cannot drift from what the code sends. A payload is serialized once at emit: a model throughtoPublicJson, so its sensitive fields are absent by construction, with the event's declared sensitive fields removed wherever they appear in a map. Each delivery is signed with HMAC-SHA256 overtimestamp.body, the key read fromDVSecretsat every attempt, and a rotation sends both signatures only until its overlap ends (DV-WEBHOOK-007) — a retry after that is not signed with the rotated-out key. Delivery rides the job layer and goes out throughDV.Http, one queue per subscription, and a job always attempts the oldest undelivered delivery for its endpoint: the queue re-queues a failed job at the back, so a job that named its own delivery would have let event 2 overtake a failing event 1. Subscriptions drain concurrently, so an endpoint that takes thirty seconds to answer delays its own deliveries and nobody else's. A delivery that exhausts its attempts is dead-lettered (DV-WEBHOOK-004) and the next proceeds; an endpoint that keeps failing is disabled, its owner told throughonDisabled, and its queue flushed (DV-WEBHOOK-003). The endpoint address is untrusted input: it must be HTTPS, and it is refused when it resolves to a private, loopback, link-local, carrier-grade NAT, reserved or cloud-metadata address — IPv4 mapped or embedded in IPv6 is judged as the IPv4 address it is — both at subscribe time and again at every attempt, since DNS can change in between, and every redirect hop is checked the same way (DV-WEBHOOK-002). Every delivery is recorded; the payload is dropped afterretentionwhile the record is kept, and a replay past the window is refused (DV-WEBHOOK-005) rather than sent with an empty body. -
Sessions, as a runtime (
DVSessions). A session is a record of a signed-in device, and its token is a bearer credential, so the store holds only the token's hash: a dump of the session table is not a list of working sessions. The token is reissued on every privilege boundary —rotate,elevate(claims) andcompleteMfa(second factor) — and the old one stops authenticating in the same write, because a stable identifier across a boundary is session fixation. Revocation is checked on the next request, sorevoke(id)andrevokeOthers(token)sign a device out at once rather than at expiry (DV-SESSION-002). Idle and absolute timeouts both apply, and rotation keeps the original sign-in time, so a session kept alive by rotating still ends.list(userId, currentToken:)shows every live device, newest first, by a listed id that is never the token. Memory andDVDatabaseSessionStoredrivers; the database one issues only the SQL the in-memory adapter runs, so it works with no database configured and on SQLite alike. -
DVMfais a policy, not a sign-in method.DVMfa.requiredneeds a second factor at some point in the session andDVMfa.recent(window)needs one within the window — step-up, which is what a payout wants.requireMfa(token, policy)answers with the session,DVMfaRequired(DV-SESSION-001) when the factor is missing or stale, orDVSessionInvalidwhen there is no live session to ask about. -
Second factors: TOTP and recovery codes (
DVSecondFactors). TOTP per RFC 6238, checked against the RFC's own test vectors, with a provisioning URI for a QR code. Every account records the last time step it accepted and advancing it is a compare-and-set in the store, so the same six digits cannot sign in twice — not inside their window, and not from two requests racing. The secret is sealed withDVFieldCipherand never stored as the base32 an app is shown; enrollment stays pending until a code proves the app has it, and that code cannot then sign in. Recovery codes are kept only as salted HMACs, shown once, spent on use, and replaced wholesale on regeneration; aDVRecoveryCodesprinted into a log prints a count. -
DVSessionCookiesets the session cookie with attributes an application cannot weaken:HttpOnly,SameSite=LaxorStrict, andSecurewith a__Host-name outside development.SameSite=Noneis refused as a configuration error naming the setting (DV-SESSION-003) rather than set as a quietly cross-site cookie. -
Offline-first stores, as a runtime (
DVOfflineStore). A model's writes go to a localDVRecordTableat once — so the same read and write calls work in a tunnel as on Wi-Fi — and to an ordered mutation log thatreplaysends to the server. The failures it exists for are the silent ones, and each has a test that fails when the guard is removed: two replays started by one reconnect share one run instead of sending the log twice; a mutation whose acknowledgement was lost is resent with the same id and the server side (DVRecordTableRemote) applies it once; a transient failure stops replay rather than sending later writes ahead of an earlier one; a write at the log's bound — by count or by the age of the oldest queued write — is refused withDVOfflineQueueFullError(DV-OFFLINE-002) and nothing queued is dropped; a permanent refusal moves to the dead letters (DV-OFFLINE-003) and replay continues. Conflicts use theDVConflictvocabulary record history already has, andDVConflict.askis refused for an offline model (DV-HISTORY-002), since offline there is nobody to ask.lastWriteWinscompares the declared clock rather than arrival order, andDVOfflineClockstamps writes with the offset the server last reported, so a device with the wrong date does not win every conflict (DV-OFFLINE-004, once). Each record has aDVSyncState— pending, syncing, synced, conflicted or rejected — readable and watchable. A memory-backed store says it will not survive the application closing (DV-OFFLINE-001). Not yet generated from@DVModel(offline:): see the specification's status for what is absent. -
Feature flags, as a runtime:
DVFeatureFlagandDVFlags. A flag answers from one pure function of a rule set and an evaluation context — identity, tenant, device, organization role, app version, platform, locale and declared attributes — so a phone and a backend function given the same two reach the same answer. A read resolves a debug-only override, then the syncedDVFlagRules, then the default compiled into the build; with nothing synced it answers the default and says so once (DV-FLAGS-001). A percentage rollout buckets on the first eight bytes ofSHA-256("key:subject")mod 10,000, so a person keeps their answer across devices and reinstalls, two 10% flags pick different tenths, and raising a percentage only adds people; a rollout with no subject holds the default rather than rolling a die (DV-FLAGS-005). A rule value of the wrong type holds the default instead of being coerced (DV-FLAGS-006) —"true"is not a bool and2.5is not an int. Stale rules stay in force and report pastmaxAge(DV-FLAGS-009), because a kill switch that expires back to on is worse than one a day old.onNextLaunchpins a flag for the process,withOverridesscopes overrides to a zone so a test's flags never leak, and an exposure is recorded once per flag per context per session, or reported as withheld when consent is (DV-FLAGS-007).@DVFlags()and@DVFlagare the declarations the generator reads. -
DV.Http: outbound HTTP with the failure paths built in. Declared hosts (DV.Http.declare, or thedartvel.http.hostsblock throughdeclareFromConfig) carry a base URL, a bearer credential named by secret and resolved through Secrets when the request is sent, a timeout, a retry policy, a circuit breaker and a concurrency limit. Requests and responses are the same WinterCGResponseandHeadersthe inbound side uses. Retries are idempotency-aware:GET,HEAD,PUTandDELETEretry on 429, 5xx and transport failures; aPOSTretries only with an idempotency key, sending the same key each time, and asking to retry one without a key sends it once and logsDV-HTTP-003. An open breaker fails fast with a typed error naming the host and when it will try again (DV-HTTP-002), and lets exactly one probe through after its cooldown. A timed-out request keeps its pool slot until it really finishes, and its later failure is observed rather than escaping as an uncaught error. A call inside a span sends atraceparentfor a child span.DV.Test.fakeHttpanswers by host name, refuses any request no stub answers (DV-HTTP-004), and replays fixtures recorded from real responses. -
Usage metering, as a runtime (
DVMeters). ADVMeterDefinitionrecords against the current tenant — never the process — so each customer's total is theirs, and two instances on one database see one number. Every recording carries an idempotency key, from the call or from aDVMeters.withIdempotencyKeyscope (the request or job id), and a key seen before is discarded (DV-METER-002) — including a late retry that would otherwise land in the next period; a recording with no key is refused rather than given a generated one that differs on the retry. A limit needs a declared behaviour (DV-METER-005):blockrefuses and does not count,throttlecounts and admits,allowAndBillcounts and reports the overage, each raisingDV-METER-004. Enforcement is serialised per tenant and meter, so ten simultaneous calls against a limit of three admit three.notifyAtthresholds are announced once, as they are crossed (DV-METER-003). Usage lands in the tenant's billing period, falling back to the calendar month and saying so (DV-METER-010); periods are half-open, a resolver answering a period that does not contain the instant is refused, and a late record is accepted into its closed period within the grace (DV-METER-007) or counted in the open one after it (DV-METER-008). Gauges bill on their average or peak.DVLevelLimitchecks a level such as seats by querying it and writes nothing.DVMemoryMeterStoreandDVDatabaseMeterStore, tested on the in-memory adapter and SQLite. -
Metered usage reaches the billing provider (
DVMeterReporter). A closed period's usage is sent throughDVBillingProvider.recordUsageunder a key made of the tenant, the meter and the period, so a report sent twice — or retried after a response was lost — is billed once. A period is refused while it can still receive usage, before its end plus the grace, because a report sent then leaves out whatever arrives late. A gauge is billed as a whole number rounded up. A period with no usage sends nothing; a meter with no price on the tenant's plan is counted and not billed (DV-METER-009). A report that fails, or a tenant with no billing customer, is queued rather than dropped (DV-METER-006), andretryPendingsends the queue again;DVDatabaseMeterReportQueuekeeps it across restarts and instances.reconcilelists where Dartvel's figure and the provider's differ, per tenant per meter, and changes nothing. -
Versioned writes, record history and soft delete, as a runtime (
DVRecordTable). A write carries the version it read and lands through a conditionalUPDATE ... WHERE _dv_version = ?, so a write against a row that moved is refused withDVConflictError(DV-HISTORY-001) holding what this session wrote, what the row holds and what was read — instead of silently replacing the change that moved it, which is the lost update both writers would have seen as success. A write to an existing row with no version read is refused for the same reason.DVConflictresolves a conflict when the caller says how:ask(the default, and not a legal offline strategy),serverWins,lastWriteWins,fieldMergeand a typedresolver.history: DVHistory(keep: ...)records each change's actor, tenant, transaction and changed fields, withsensitivefields recorded as changed and never as values — the history table is checked for the plaintext, not just the returned object. A change whose entry cannot be written is undone (DV-HISTORY-005).revert(to:)adds a change rather than rewinding, keeps every entry in between, reports the sensitive fields it could not put back (DV-HISTORY-003) and takes the same version check as any write.softDeletemarks rather than removes;restoreis refused when a live record holds a unique field (DV-HISTORY-006). InsideDV.transactionevery write registers its own inverse, so a later failure takes the write and its entry with it. Tested on the in-memory adapter and SQLite; the statements are the subset Postgres and MySQL already run. -
A transaction has an identifier:
DVContext.transactionId. The same for every context in one unit of work — a nested call joins the outer one — and different between two, so a history entry can name the transaction that wrote it. -
A Postgres server that declines TLS no longer breaks the connection that asked.
sslMode: preferis the default and its whole purpose is to ask for TLS and carry on without it — the ordinary shape of a local or containerised Postgres that was never given a certificate. Asking means listening to the socket for the single byte the server answers with, and aSocketis a single-subscription stream. When the answer isSthe socket is replaced by aSecureSocket, which is a new stream, so the adapter could listen to it; when the answer isNthe code carried on with the same socket and the adapter's first act —connection.input.listen(...)— threwBad state: Stream has already been listened to. The mode whose job is falling back to plaintext could not produce a usable connection at all, and the state error masked whatever the server had really said, so every failure in that path reported the same Dart-level symptom instead of its cause. The subscription that read the answer now stays and feeds the stream the adapter reads, which is what the MySQL adapter had already been doing for the same reason. Bytes the server sends in the same segment as its refusal are relayed rather than dropped. -
MemoryDVDatabaseAdapterruns the SQL Dartvel itself issues, so the framework can be run and tested without a database. It understood four statement shapes --select 1,select * from t, an unscoped insert and an unscoped delete -- and threwArgumentErroron everything else, which meant the development adapter could not back the framework's own surfaces:DVPageStore,DVDatabaseCacheAdapterandDVDatabaseQueueAdapterall failed on their first statement. Studio rendered "Could not read pages" where the pages should have been, andDVTest.fakeDatabase()was a fake no Dartvel code could use. It now interpretsCREATE TABLE/DROP TABLE,INSERT(including an explicitrowid),UPDATE ... SET ... WHERE,DELETE ... WHERE, andSELECTwithDISTINCT, a column subset,COUNT(*) AS alias,WHEREconditions joined byANDover= != <> < <= > >= IS NULL IS NOT NULL, multi-keyORDER BYwithrowidandASC/DESC,LIMITandOFFSET-- binding?in the order the statement writes it, and following SQL's rule that a comparison against null never matches. The cache and queue adapters are now held to the same shared contracts on it that they are on SQLite.Anything outside that subset -- a join, a subquery,
OR, SQLite's FTS5MATCH,ALTER TABLE-- still throws, and the message now names the statement it refused. An in-memory adapter that guessed at a query it had not parsed would be worse than one that cannot run it: wrong rows look exactly like right ones. -
A kiosk says when it blocks a route.
routes.allowwas parsed, scanned by doctor and givenDV-KIOSK-006for a route being blocked, and the redirect that did the blocking reported nothing -- so a kiosk sending/adminback to its home page looked exactly like a link that was wrong.DVKioskDegradation.routeBlockedis the member that names it, and it was assigned nowhere until now. The block is logged atdebug, the level the registry gives that code, with the route and where it went. -
An exit result carries the degradation rather than a code spelled out beside it.
DVKioskExitResult.degradationislockedOutaftermaxAttempts,noPolicyin a build with no kiosk policy, andnoneotherwise;codederives from it. The codes were string literals written next to the enum members that mean the same thing -- two places to change and one of them silently stale -- andDVKioskDegradation.lockedOutwas assigned nowhere at all, so a caller could only learn what had happened by matching the text of a code. -
Diagnostic codes for the sections the third pass added:
DV-STORE(store publishing and privacy declarations),DV-RELEASE(backend release management),DV-HISTORY(record history and optimistic concurrency),DV-ORG(organizations and invitations),DV-ANALYTICS(product analytics and consent) andDV-APIKEY(platform API keys, scopes and the OAuth provider). Specified, not built;dartvel explaincan answer them. -
The diagnostic registry carries the codes the specification added for protocol versioning, offline-first models, schema evolution, client schedules, 3D scenes and PDF export.
dartvel explainanswered "unknown code" for twenty codes the document publishes, and the test that exists to catch exactly that disagreement was failing. Its row pattern also skipped any family with a digit in it, so the wholeDV-3Dfamily was outside the check in both directions; it is inside it now. -
dvKioskLocksWindowsreports whether a running kiosk policy holds the surface to one window -- true indevicescope, which the specification defines as one application with no windows. The windowing capability reads it, soopen()under a device kiosk names the kiosk (DV-WINDOW-002) rather than reporting the target as incapable of windows.
0.5.0 #
- The
DV.Databasedocs say whose code generation thebuild_runnerstep they describe is: drift's. Dartvel's own generation isdart run dartvel_cli:dartvel routes, and the build_runner path throughdartvel_generatoris retired, so a step that just said "run code generation" now reads as the retired one. DVImageVariants: the widths images are resized to (Next.js's defaults unlessdartvel.images.widthssays otherwise), the one address a variant is asked for by, and the checks a server applies to a request for one -- a width outside the set, a path out of the site, a host not indartvel.images.remoteHosts, credentials in an address. The widget, the link prefetch and the server all call it, so all three name the same file: a prefetch of a slightly different address is a second download, not a cache hit.dartvel.web.server.streaming: shell--DVPageStreaming.shell, alongsidehead(true) andoff(false), which read and write exactly as before.DVWebServerSettings.streamingstays, now true for either kind of streaming;streamingModesays which.dvHeadPartssplits a page's head into what no render of its shell changes and what page data writes -- the SEO block, a stray title or description, the icon, the structured data. The first part is identical in every render of one shell, which is what makes it safe to send before the data exists.DVRoutePreloadsreads the prefetch manifest a web-server build writes and gives a served route its own<link rel="preload">list, by the pattern the request matched: its deferred parts as scripts, its images as they are fetched, nothing the shell already names and no image drawn through a variant, since which file that is depends on the screen.dvShellFirstChunksis shell-first streaming as one function, the route's preloads in its first write, and both servers stream through it -- the deployed one anddartvel preview, which had only the older head-after-data split, so the page previewed was not the page served.
0.4.0 #
Breaking: DVFieldCipher takes its randomness through a named constructor.
DVFieldCipher.secure(keyring) is what an application wants; the unnamed
constructor is @visibleForTesting and exists so a test can supply a
deterministic source. A cipher that silently accepted a seeded Random in
production was a key generator with no entropy.
- The Android permission table gained the names the capture bridge resolves at run time, so a request can never name a permission the manifest lacks -- Android refuses an undeclared permission instantly, with no dialog, and the answer is indistinguishable from a person tapping Deny.
- Billing gained usage meters, trials, invoices and a webhook receiver, with
every grant keyed by a real customer identity rather than
toString(). The defaulttoString()of an ordinary object is the same constant for every instance, so passing a logged-in user filed every user under one key. - Webhooks are applied newest-wins per subscription. Providers do not
guarantee order, and a stale
activearriving behind a cancellation handed a cancelled customer their entitlement back with a valid signature.
0.3.2 #
- Corrected the constraints on sibling Dartvel packages, which named the
previous release rather than the one published alongside them. A caret on a
0.x version stops at the next minor, so
dartvel_core: ^0.2.1excluded the 0.3.1 published beside it -- a user installing the 0.3.1 set resolved 0.2.x for every sibling and got none of what that release contained.dart pub publish --dry-runcould not see it, because it resolves against pubspec_overrides.yaml and every sibling points at a local path.
0.3.1 #
- Kept web-compatible: the distributed cache hashes in 32-bit arithmetic, and
the LDAP client is behind a conditional export. Both broke
flutter build weboutright. - The main barrel exports Request, Response and Headers again; the full wire
type set moved to
package:dartvel_core/http.dart.
0.3.0 #
Four queue brokers, both network databases reachable over TLS, static generation that produces pages, and a page that can have a body.
Queues, on real brokers #
Seven adapters now: in-memory, database, Redis, SQS, RabbitMQ, Pub/Sub and Kafka. The four that talk to a network service are verified in CI against the real thing -- ElasticMQ, RabbitMQ's own image, Google's emulator and Apache Kafka -- rather than against a fake that agrees with whatever the adapter does.
That distinction found nine bugs which every unit test had passed: a backoff sent as an initial delay, an AMQP channel limit above the server's, delivery-mode written to the wrong bit, publishes returning before the broker had them, a payloadType that would have stopped every handler matching, a Fetch reply parsed with three fewer fields than it has, offset commits sent to a broker that was not the group's coordinator, and a first coordinator lookup that is always refused and always retriable.
Each adapter is written around what its service actually offers. SQS and
Pub/Sub refuse pending rather than returning an empty list, because an empty
list reads as "there is nothing" when the truth is "I cannot see". Kafka is a
log, so it has no dead letters, no priority and no out-of-order retry, and
lag gives the honest version of a backlog: a distance, not a list.
Databases #
PostgreSQL and MySQL both negotiate TLS, which is what a managed endpoint
requires -- Aurora, Neon, Supabase, PlanetScale and Cloud SQL all demand it and
most refuse plaintext, so before this the adapters reached localhost and
nothing else. sslMode takes libpq's names, so a connection string copied from
a provider's console pastes in unchanged.
A refusal is fatal at require and above. Falling back would put the password
on the wire in the clear while the caller believed the connection was
encrypted.
Pages can have bodies #
A private @DVPage input had to be a single expression, so every page needing
a local, a loop or a condition was written as a one-line wrapper around a
public helper. Block bodies are lowered into the generated widget now.
@DVFunctionalWidget and @DVBackendFunction still require expression bodies.
Static generation #
dartvel build web writes a page per route and expands parameterised routes
through the application's own resolvers. @DVModel(generatePublicPages: true)
now generates the route as well as the paths -- it previously produced a list
of addresses that all resolved to the application's own not-found page.
A page no route serves is refused rather than written.
The web output #
Crawler-visible HTML is built from the page's semantics tree rather than from
string literals in the source, so it carries real headings, anchors and
landmarks instead of one paragraph per source line. Pages gained structured
data, a stylesheet for sitemap.xml, and an .htaccess that path URLs need
and that nothing was writing.
In-app links push the route instead of tearing the document down and rebuilding the whole application, which is what a real anchor in the semantics tree does by default.
0.2.1 #
- First published release.
Dartvel's packages are published under the dartvel_dev name on pub.dev.
dartvel was taken on 2026-08-06 by an unrelated package, so the published
identifier carries a suffix while the command stays dartvel.