tom_core_server 1.3.0
tom_core_server: ^1.3.0 copied to clipboard
Server library for the TOM framework.
1.3.0 #
This release is built from the repository at the state the entries below describe (an Unreleased section included); its own section is in the full history linked at the end.
Unreleased #
Added #
-
OpenTelemetry tracing (
tracingmodule, new;server,configuration,datasources,transactions,messaging,metricsmodules; kernelTomTracingseam).TomOtelTracer({processors, resource, samplePercent, exportClient})implements the kernel'sTomTraceroverpackage:opentelemetry(^0.18.11, imported by this module only), sampling parent-based with new traces atsamplePercentdecided from the trace id;TomOtelTracer.otlp({endpoint, headers, resource, samplePercent, exportInterval, client})exports OTLP/HTTP protobuf to<endpoint>/v1/traces, batched every 5 s;fromConfiguration(configuration, {serviceName});flush,shutdownandclosewait for the exports in flight.TomUntracedClient(idle;closelets calls in flight finish) andTomInMemorySpanExporter(spans,named,childrenOf,attributeOf).TomTracingException, error keystracing.config.no_endpointandtracing.config.invalid_sample_percent. Configurationtracing(TRACING, defaultfalse) andtracingSamplePercent(TRACING_SAMPLE_PERCENT, default100); spans go tootlpEndpointwithotlpHeaders.TomServer.configureTracing(), called bystart()right afterconfigureHttpClients(), installs the tracer asTomServer.tracerand registers it with the shutdown cleanup. Spans: the request's server span intomRequestContextMiddleware, named<METHOD> <route>, continuing the caller'straceparentand answered as the response's; each SQL statement a client span named after its first keyword withdb.system.name(TomSqlDatasource.dbSystemName:mariadb,postgresql),db.operation.name,db.query.text(placeholders, never bound values) andtom.datasource,withLinktakingsqlText;TomTransaction.span(transaction, with id, read-only flag, outcome and participant count) — statements and transactions only inside traced work (kernelTomTracing.start(childOnly:)) — andrunInSpan, used byrunInTransactionand transactional endpoints;TomMessageOutbox.senda producer spanoutbox sendwritingtraceparent/tracestateintoTomMessage.headers, continued by the consumer spanoutbox deliver;TomWebhookTransportno longer posts the message's trace headers. The metrics push runs untraced.doc/tracing.md,doc/api/api_summary_tracing.md,example/tracing/tracing_example.dart; tests intest/tracing_test.dart,test/tracing_db_test.dart(db) andtest/tracing_otel_test.dart(otel, run bytool/run_otel_tests.sh, whose collector configuration gains a traces pipeline). -
Export charts, the email channel, and the export register with quota and retention (
export,db_migrationmodules).TomTabularChart({type, categoryColumn, valueColumns, title})withTomChartType(bar,line,pie) names columns of its section, every row a point;TomTabularSectionand itsofRows,ofMapsandofEntitiestakecharts, checked byvalidateAgainst(error keysexport.chart.unknown_column,export.chart.no_series,export.chart.pie_series,export.chart.not_numeric).TomPdfRendererdraws each chart below its section's table withpackage:pdf's chart widgets (constructor argumentchartHeight, default 180), skippingnullcells and pie slices of zero or less;TomXlsxRendererwrites nativebarChart/lineChart/pieChartparts underxl/charts/with one drawing per charted sheet, anchored right of the table, the series bound to the sheet's cells by absolute references; CSV leaves charts out.TomExportEmailDelivery({store, outbox, linkFor, from, maxAttachmentBytes})(defaultMaxAttachmentBytes, 10 MiB) sends a stored export withsend(export, {to, subject, text, fileName, from, mode})as a link or an attachment read back from the store (TomExportEmailMode:link,attach,auto), queued throughTomMessageOutbox; error keysexport.email.no_recipient,export.email.too_large,export.email.no_link.TomExportRegister(record,find,ownedBy,usageOf,storedBefore,remove) withTomExportRecordandTomExportUsage, in memory (TomMemoryExportRegister) or in the tabletom_export(TomSqlExportRegister,exportRegisterTable,exportRegisterTableDdl,defaultExportRegisterTable, rowsTomExportRecordRowandTomExportUsageRow), listed byframeworkOwnedTablesunder theexportRegisterargument.TomExportService({formats, store, register, quota})records whatstoreAs(result, format, {key, owner})stores and enforcesTomExportQuota({maxCount, maxBytes})before rendering and after storing (error keysexport.quota.count_exceeded,export.quota.bytes_exceeded; a quota without a register is anArgumentError);deleteStored(key)removes the export and its record.TomExportRetention({store, register, maxAge, batchSize})deletes exports older thanmaxAgewithpurge({now}), or as the jobjobDefinition({name = 'tom.export.purge', schedule})run by aTomInlineJobDispatcher. Reference migrationexample/export/migrations/main/tomcore/[1]-create_tom_export.sqland its PostgreSQL form;doc/export.md§ Charts, § The Email Channel, § The Export Register, § Quota, § Retention and § Error Keys;example/export/export_email.dart,example/export/export_retention.dartand steps 7 and 8 ofexample/export/export_example.dart; tests intest/export_test.dart,test/export_register_test.dart,test/export_register_db_test.dart,test/framework_schema_test.dartandtest/reference_migrations_test.dart. -
Preference defaults and the bulk preference write (
preferencesmodule).TomPreferenceDefault(tabletom_preference_default,createTableSql),TomPreferenceDefaultScope(deployment,role),TomPreferenceDefaultRepositoryand the administrative serviceTomPreferenceDefaults(resolve,maxRoleLength,setForDeployment,setForRole(role, key, value, {precedence}),removeForDeployment,removeForRole,all,effectiveFor(roles),effectiveValue(key, roles); error keypreferences.default.invalid_role; no endpoint). A principal reads its own value, else the default of a role it holds (highest precedence, then role name in code-unit order), else the deployment's:TomUserPreferences( repository, {defaults})— falling back to theTomPreferenceDefaultsbean — merges them intoall()(ordered by key, the page window cut in memory,TomPageRequest.sortnot applied) andread(), withTomUserPreferenceDto.originnaming the source.setAllEncoded(writes)writes a batch all or none in one transaction, the ceiling counting the batch whole; the endpointwritePreferences(POST /preferences) serves it, andTomUserPreferencesErrorHandleranswerspreferences.bulk.malformedwith a400. Every preference write goes throughsaveByKeyon(principalId, preferenceKey). Reference migrationexample/preferences/migrations/main/tomcore/[2]-create_tom_preference_default.sqland its PostgreSQL form;doc/preferences.md§ Defaults Beneath a Principal's Own Values, § Writing Several Preferences at Once; step 13 ofexample/preferences/preferences_example.dart; tests intest/preferences_defaults_db_test.dart,test/preferences_endpoints_test.dartandtest/reference_migrations_test.dart. -
Single-statement upsert (
object_persistence,datasourcesmodules).TomSqlDatasourceRepository.upsert(entity, {conflictFields})inserts or overwrites the row holding the same unique-key values in one statement, with no read before it, and returns the entity carrying the written row's key;upsertBlockernames why an entity cannot be upserted (column access rights, file-reference columns, encrypted columns, a version column, soft delete, entity or change listeners or history, an audit policy), refused ascrud_repository.upsert.unsupported, and a conflict key omitting the@TomDbScopeor tenant field is refused ascrud_repository.upsert.unscoped_conflict_key.saveByKey(entity, {conflictFields})upserts where allowed and otherwise finds the row by the key and saves it. The dialect fragmentTomStandardSqlDialect.upsert(table, values, {conflictKey, updateColumns, keyColumn})renders MariaDB'sINSERT … ON DUPLICATE KEY UPDATE c = VALUES(c), key = LAST_INSERT_ID(key);PostgresSqlDialect.upsertrendersINSERT … ON CONFLICT (k) DO UPDATE SET c = EXCLUDED.c RETURNING key.doc/object_persistence.md§ Upserting by a unique key,doc/datasources.md§ Upserting in one statement; tests intest/postgres_dialect_test.dartandtest/preferences_defaults_db_test.dart. -
Job priority, pauses, checkpoints and the run archive (
scheduling,db_migrationmodules).TomSqlJobStoreimplements the kernel's priority, pause and checkpoint contract:tom_job_runcarriespriority INT NOT NULL DEFAULT 0andcheckpoint LONGTEXT NULL, the claim scan readsORDER BY priority DESC, seqand leaves out paused jobs, andsaveCheckpointis anUPDATEfenced on the claim. Paused jobs are rows oftom_job_pause(jobPauseTable,jobPauseTableDdl,defaultJobPauseTable,TomJobPauseRow; constructor argumentpauseTable);pauseJobkeeps the first pause's instant.purgeFinished(before, {now})moves finished runs intotom_job_run_archive(jobRunArchiveTable,jobRunArchiveTableDdl,defaultJobRunArchiveTable; constructor argumentarchiveTable,nullfor a purge that deletes) in one transaction — anINSERT … SELECT, then aDELETEof exactly the archived runs;archivedRuns({jobName, limit})reads it newest first asTomArchivedJobRuns andpurgeArchive(before)trims it.deleteAllempties all three tables,TomJobRunRowcarriespriorityandcheckpoint,TomJobRunArchiveRowmaps an archived run, andframeworkOwnedTableslists both tables (argumentsjobPause,jobRunArchive). Reference migrationsexample/scheduling/migrations/main/tomcore/[1]-create_tom_job_run.sql(the two columns),[4]-create_tom_job_pause.sql,[5]-create_tom_job_run_archive.sqland their PostgreSQL forms;doc/scheduling.md§ Priority, Pausing and Checkpoints and § The Archive; step 11 ofexample/scheduling/scheduling_example.dart; tests intest/scheduling_db_test.dart,test/scheduling_schema_test.dartandtest/framework_schema_test.dart. -
Recovery of prepared two-phase branches (
transactions,datasources,servermodules). EveryTomTransactionhas anid(tx_<process tag>_<counter>). Between the phases the coordinator records the prepared branches of itsTomPreparedBranchHolderparticipants under that id inTomTransactionManager.decisionLog(aTomTransactionDecisionLog), outside the transaction's scope; a record that cannot be written rolls the transaction back, and the record is forgotten once every branch committed and kept when one broke its promise.TomSqlDecisionLog(datasourceName, {table, createSchema})keeps decisions intom_transaction_decision(decisionTable,decisionTableDdl,defaultDecisionTable,TomDecisionRow) of a one-phase data source,TomMemoryDecisionLogin memory.TomTransactionRecovery.recover({sources, log, now})lists the prepared branches of everyTomTwoPhaseRecoverabledata source, passes over the branches it does not own and those younger thanTomTransactionRecovery.minimumAge(one minute), commits those a decision names, rolls back the rest, forgets the decisions none of whose branches is left, and returns aTomRecoveryReport(committed,rolledBack,tooYoung,failed,forgotten); a branch that fails to resolve is reported and the rest are still resolved.MariaDbXADatasource(XA RECOVER/XA COMMIT/XA ROLLBACK) andPostgresXADatasource(pg_prepared_xactsof the current database /COMMIT PREPARED/ROLLBACK PREPARED) mix inTomSqlTwoPhaseRecovery, whose four statements a driver supplies;tomSqlOwnsBranchandtomSqlBranchOpenedAtread a branch id's data source and opening time, and an id of other characters is refused witherror.datasource.invalid_branch_id. Thetransactionsconfiguration key (decisionLog,decisionTable,createSchema,recoverOnStart,recoveryMinimumAgeSeconds) is applied byTomServer.recoverTransactions, whichstart()calls afterconfigurePrivacy; a malformed section istransactions.recovery.config.invalid, an unregistered log data sourcetransactions.recovery.no_datasource(TomTransactionRecoveryException). Reference migrationexample/transactions/migrations/main/tomcore/[2]-create_tom_transaction_decision.sqland its PostgreSQL form;example/transactions/transaction_recovery_example.dart. -
Down migrations and rollback (
db_migrationmodule). A migration may have a down script beside it, named after its basename plus.down(TomDbMigrations.downSuffix):[3]-add_column.down.sqlundoes[3]-add_column.sql,[3]-x@dev.down.sqlundoes[3]-x@dev.sql.TomDbMigrations.isDownMigrationrecognises one,collectDownMigrationFilescollects them keyed by the migration each undoes (two for one migration aredb_migration.migration.duplicate_migration_basename), andcollectMigrationFilesskips them, so neither the forward run nor the convergence check sees them.TomDbMigrations.rollback(directory, {datasource, schema, toVersion})androllbackDataSource(migrator, dsInfo, dataSourceDirectory, schema, {toVersion})undo the migrations applied abovetoVersion(-1: all) newest first by installation order, under the migration lock, and answer the versions undone; a target below-1isdb_migration.rollback.invalid_target.TomDbMigrator.rollback(dsi, schema, toVersion, scripts)takes the tree's scripts asTomMigrationScript( description, up, [down]). Before anything runs, every migration to undo must have a down script (<apply>.rollback_migration.no_down_script) and be recorded with the description and up-script checksum the tree holds (<apply>.rollback_migration.script_changed); a recorded failure still refuses the schema. Each down script runs, then its record is deleted, so a later forward run applies the migration again; environment tags do not filter a rollback.TomSqlMigrationAdaptor.revertScript(protected, per database) andforgetMigrationdo the work: PostgreSQL runs the down script and the deletion in one transaction, MariaDB statement by statement, leaving the migration recorded when a down script fails part-way; either failure is<apply>.rollback_migration.failed.TomDbMigrations.rollbackFromArgs(args, directory)reads--rollback-migrations=<datasource>/<schema>/<version>(rollbackMigrationsOption) and refuses a malformed request withdb_migration.rollback.invalid_arguments; the fourtom_core_samplesservers run it beside the repair and exit.example/db_migration/db_migration_repair_example.dartshows the connection-free half;test/db_migration_rollback_db_test.dartcovers the rollback against MariaDB and PostgreSQL. -
A migration adaptor serves several data-source types (
db_migrationmodule).@TomDbMigrationAdaptor(type, {alsoServes})lists further types; one instance per adaptor class is registered under each, and a type claimed twice is stilldb_migration.initialization.found_more_than_one_migration_adapter.MariadbMigrationAdaptorservesmariadbandmariadb_xa,PostgresMigrationAdaptorpostgresqlandpostgresql_xa. -
Query sentences filter on associations; subqueries in the builder (
object_persistenceanddatasourcesmodules,subquery.dart). A predicate term may name a field of a@TomReferenceassociation —find by tags.name = :n— or test it,tags is empty/tags is not empty. The term compiles to a correlatedEXISTS (SELECT 1 FROM <join>, <target> WHERE …)over the association'sleftSideColumnsandrightSideColumns, a semi-join that answers an owning row once however many associated rows match; it works infind,countanddeletesentences and under a leadingnot.tags.name <> :nmeans "has a tag whose name is not n"; "has no tag named n" isnot tags.name = :n. Refusals areTomQuerySyntaxExceptionkeyedquery_sentence_compiler.association.no_field,.unknown_field,.context_columnsand.self_reference.TomSubquery(types, {where, projection})rendersSELECT <projection or 1> FROM <tables> WHERE …, adding the live-row test of every@TomSoftDeletetype unlessTomSoftDelete.includingDeletedis on;TomExistsandTomInSubquerytest it, built withTomQueryBuilder.exists,notExists,isInSubqueryandisNotInSubquery. A subquery is opaque to the walks over the enclosing tree and contributes its parameters in place.TomStandardSqlDialectgainsexists,inSubqueryandsubquery.test/query_associations_test.dartandtest/query_associations_db_test.dartcover them without and with a server. -
Soft delete, history tables, lifecycle hooks and change events (
object_persistenceanddatasourcesmodules,entity_lifecycle.dart).@TomSoftDelete({field = 'deletedAt'})makes every repository delete —delete,deleteById,deleteAllById,deleteByQuerySentence,deleteByQuery— anUPDATEsetting a nullableDateTimecolumn, and ANDs<deleted> IS NULLinto the repository's scope, so every read, count, existence check and write guard leaves deleted rows out;TomSoftDelete.includingDeleted(body)shows them,restoreById(id)clears the deletion time andpurgeById(id)deletes for good. A versioneddelete(entity)is version-checked and moves the version (TomSqlDatasourceRepository.deletedAtParameter,nextVersionParameter); a delete cut to a window is resolved to the window's keys first.@TomHistory(table:)copies the row as it was — read raw, encrypted columns as ciphertext — into a history table withhistory_id,history_action,history_atandhistory_bybefore every update, delete and restore;historyOf(id)answersTomHistoryEntryversions, oldest first, read through the reflectedTomHistoryMeta.TomEntityLifecycle.register<T>(listener)registers aTomEntityListenerwhosebeforeSavemay change or refuse the entity and whoseafterSave/beforeDelete/afterDeletesee the row as stored;TomEntityLifecycle.onChange(listener, {entity})is told of everyTomEntityChange(inserted,updated,deleted,restored) with the rows before and after, their values with encrypted fields redacted, the principal and the tenant, andtoJson(). The write of an entity with a listener, a change listener or a history runs inside a transaction — the caller's or its own — so the history, the listeners' writes and the change commit or roll back together, and a change listener inserting an outbox row publishes exactly the committed changes.TomUpdateis the generatedUPDATEa soft delete and a restore render as. Refusals areTomEntityLifecycleExceptionkeyedcrud_repository.soft_delete.invalid_field,crud_repository.soft_delete.not_soft_deleteandcrud_repository.history.not_declared.example/object_persistence/entity_lifecycle_example.dartwalks the connection-free half;test/entity_lifecycle_test.dartandtest/entity_lifecycle_db_test.dartcover it without and with a server. -
Migration lock and repair (
db_migrationanddatasourcesmodules).TomDbMigrations.migrateDataSourceruns every schema of a data source under that database's migration lock,TomDbMigrator.withMigrationLock(dsi, body): one migration run per database at a time across every process, so replicas starting together apply each script once — a second process waits, then finds the migrations applied. The SQL adaptors take it through the newTomSqlDatasource.withSessionLock(name, body, {wait, poll}), a database-wide session lock held on a connection of its own beside the pool: released whenbodyreturns or throws and by the database when the holding process dies, re-entrant within the zone per database and name, and refused afterwaitwitherror.datasource.session_lock_timeout. Drivers supply the protectedtryLockOn/unlockOn—GET_LOCK/RELEASE_LOCKon MariaDB,pg_try_advisory_lock(hashtextextended(name, 0))/pg_advisory_unlockon PostgreSQL.TomSqlMigrationAdaptor.migrationLockName(databaseName)(tom_db_migration:<db>, hashed past MariaDB's 64 characters) andTomSqlMigrationAdaptor.lockWait(10 minutes) name and bound it; a data source that is not aTomSqlDatasourceis refused with<initialize>.lock.not_a_sql_datasource.TomDbMigrations.withMigrationLock(datasourceName, body)holds it around a wider run, such as a whole per-tenantforEachTenantwalk. A migration recorded as failed is resolved withTomDbMigrator.repair/TomDbMigrations.repair(datasourceName, schemaName, version, action)andTomMigrationRepair.remove(the next run applies it again) orTomMigrationRepair.markResolved(the next run skips it); repair runs under the lock, never runs a script and touches only records withsuccess <> 1, refusing a version with none (<initialize>.repair.no_failed_record) and reporting a failed read or write as<initialize>.repair.failed.TomDbMigrations.repairFromArgs(args)reads--repair-migration=<datasource>/<schema>/<version>and--repair-action=remove|resolve(repairMigrationOption,repairActionOption), answers whether it ran, and refuses a malformed request withdb_migration.repair.invalid_arguments. The failed-history refusal names the repair. The fourtom_core_samplesservers run it before migrating and exit.example/db_migration/db_migration_repair_example.dartshows the connection-free half;test/db_migration_lock_db_test.dartcovers the lock and the repair against a live server. -
Read replicas (
datasourcesmodule,read_replicas.dart). A SQL data source takesreplicas— one settings map per replica, each overriding the primary's settings ([{host: db-replica-1}]) — withreplicaStickinessMs(default5000) andreplicaRetryMs(default30000). A statement runs on a replica only when it is read-only work (a read-only transaction, aGETorHEADrequest, or aTomReplicaRouting.readOnlyblock), a repository read of an entity not marked@TomPrimaryReads, a read (TomReplicaRouting.isReadStatement: aSELECTorWITHthat locks, writes and calls no session function), and the principal (per tenant) has not written withinreplicaStickinessMson this node. Replicas are used round-robin, a read-only transaction stays on one, and a replica that fails before serving is left out forreplicaRetryMswith a warning while the primary serves; one unreachable at connect is marked down, not fatal.TomReplicaRouting.primaryforces the primary;routeSafeRequests(defaulttrue) decides whether safe requests are read-only work. Every read method ofTomSqlDatasourceRepositoryruns insideTomReplicaRouting.repositoryRead; the framework's own stores read the primary.TomSqlDatasourcegainsreplicas,replicaStickinessMs,replicaRetryMs,replicaSourcesand the driver hooknewReplica()(a plain one-phase data source even behind an XA primary); replicas register as<name>.replica<n>. A malformedreplicassetting is refused witherror.initialize.invalid_replicas.TomResourceGrant,TomPrincipalResourceGrant,TomPrincipalDelegation,TomUserTwoFactorEnrolment,TomExternalIdentityLinkandTomExternalIdentityTokenare@TomPrimaryReads.example/datasources/read_replicas_example.dartshows the rules without a server;test/read_replicas_test.dartandtest/read_replicas_db_test.dartcover them. -
Privacy: GDPR erasure, subject-access export, retention and crypto-shredding (new
privacymodule,lib/src/tomserver/privacy/). Entities declare their personal data:@TomDataSubject(onErasure:)on theint/Stringcolumn naming a person, withTomErasure.delete,anonymizeorunlink;@TomPersonal(subject:, replacement:)on a column holding their data;@TomRetention(field:, days:, action:)on the class.TomPersonalDataModel.ofresolves and checks them (TomSubjectField,TomPersonalField), refusing an unusable declaration withprivacy.model.subject_type,…subject_unknown,…subject_ambiguous,…structural_personal,…retention_fieldor…retention_anonymize.TomPrivacy.register(repository)enrols an entity (registration order is visiting order);TomPrivacy.erase(subject, keepAuditKeys:, entities:)deletes, anonymizes or unlinks every row naming the subject through the repositories — audited, files cascaded, unscoped by row scopes — and answers aTomErasureReportofTomErasureCounts;subjectAccess/exportSubjectcollect those rows, once each, into aTomSubjectAccessrendered as XLSX, CSV or PDF;applyRetention/retentionJob(privacy.retention,tomRetentionJobName) applyTomRetentionPolicys — by default the declared ones,TomRetentionPolicy.declared()— aretentionBatchSizeat a time and answerTomRetentionCounts. Each operation is audited (erasureTarget,subjectAccessTarget,retentionTarget). Crypto-shredding:TomSubjectKeysseals a value under a 32-byte key per subject (tomsubj:v1:…, AES-256-GCM, subject and field authenticated), wrapped under the field-encryption keyring's key-encryption keys and kept by aTomSubjectKeyStore(TomSqlSubjectKeyStoreovertom_subject_key—subjectKeyTable,subjectKeyTableDdl,defaultSubjectKeyTable, read back asTomSubjectKeyRow— orTomMemorySubjectKeyStore) asTomSubjectKeyRecords;shreddeletes the key,revealopens a record's sealed values or shows[erased].TomPrivacy.installSubjectKeys/configureinstall them;TomServer.startapplies theprivacyconfiguration through the newTomServer.configurePrivacy, afterconfigureFieldEncryption. Failures areTomPrivacyExceptions (privacy.not_registered,privacy.no_subject_keys,privacy.seal.invalid,privacy.no_datasource,privacy.config.invalid). Reference migrationexample/privacy/migrations/main/tomcore/[1]-create_tom_subject_key.sqlwith its PostgreSQL form;frameworkOwnedTableslists the table under the newsubjectKeyparameter.TomUserPreference,TomUserTwoFactorEnrolment,TomPrincipalResourceGrantandTomPrincipalDelegationdeclare their principal columns data subjects (and the preference value and enrolment hint personal).doc/privacy.md,doc/api/api_summary_privacy.md, the module readme andexample/privacy/privacy_example.dart; covered bytest/privacy_test.dartand thedb-taggedtest/privacy_db_test.dart. -
The audit trail seals personal values (
auditmodule).TomAuditTrail.recordMutationtakesbeforeSubjects/afterSubjects— a field mapped to the data subject its value belongs to — and seals those values through the new staticTomAuditTrail.personalValueSealerbefore the record is written;null, the default, records them as they are.TomSqlDatasourceRepositorypasses the subjects of an entity's@TomPersonalfields from the stored rows, so with subject keys installed a repository's records hold personal values sealed per person. -
TomAesGcmis public (object_persistencemodule): the AES-256-GCM cipher of field encryption and data-key wrapping, now shared with the privacy module's sealing (seal,open). -
Field-level encryption with key rotation (
object_persistencemodule). AStringfield annotated@TomEncryptedis stored as AES-256-GCM ciphertext —tomenc:v1:<data key id>:<base64url>, randomised per write, with<table>.<column>as additional authenticated data — and read as plaintext:TomSqlDatasourceRepository.saveencrypts it, the data source's entity row mapping loads the data keys a result names (TomFieldEncryption.prepareRead) and decrypts it, andTomFieldEncryption.rawreads the stored values as they are without loading references.@TomBlindIndex(field, normalization:)(TomBlindIndexNormalization:exact,trim,trimLowercase) makes a companion column an HMAC-SHA-256 of the normalised plaintext, filled on every save;TomFieldEncryption.blindIndexConditionbuilds the lookup (<index> is in ( … )over every index key,blindIndexCandidates),blindIndexOfandblindIndexthe single value. Data keys are kept intom_encryption_key(encryptionKeyTable,encryptionKeyTableDdl,defaultEncryptionKeyTable, read back asTomDataKeyRow; listed byframeworkOwnedTablesunder the newencryptionKeyparameter; reference migrationexample/object_persistence/migrations/main/tomcore/[1]-create_tom_encryption_key.sqlwith its PostgreSQL form), wrapped under aTomKeyEncryptionKey—TomAesKeyEncryptionKey, an application-held AES-256 key — by aTomDataKeyStore(TomSqlDataKeyStore, resolved by name per call so a routed tenant keeps its keys in its own schema or database, or.overa data source;TomMemoryDataKeyStore) asTomDataKeyRecords (TomDataKeyPurpose,TomDataKeyState).TomFieldKeyringunwraps and caches them, reloads everyrefreshIntervalor for a key it has not seen, and rotates:rotateDataKey,rotateIndexKey(index-2,index-3, …) andrewrapfor a key-encryption key.TomFieldReencryption—run()in every tenant, orjob(schedule:)asobject_persistence.reencrypt(tomReencryptionJobName) on an inline dispatcher — rewrites values under a previous data key, plaintext (read whileacceptPlaintextis on) and stale blind indexes, each row under a guard on the values it read, reporting aTomReencryptionReport.TomServer.startinstalls the keyring from thefieldEncryptionconfiguration (TomServer.configureFieldEncryption,TomFieldEncryption.configure) afteridempotency. Repository audit images redact encrypted fields and blind indexes (TomFieldEncryption.redact,[encrypted]).TomAnnotationAnalyzer.encryptedColumns/blindIndexColumns, andTomColumnInformation.encrypted/blindIndex.TomFieldEncryptionExceptioncarriesfield_encryption.not_configured,…unknown_key,…decrypt_failed,…plaintext_refused,…unknown_kek,…invalid_kek,…config.invalidand…no_datasource; the analyzer refuses a misdeclared field (annotation_analyzer.encryption.not_string,…both,…structural_column,…index_without_field) and the query compiler a condition, group key or aggregate on an encrypted column (query_sentence_compiler.encrypted_column). The cipher ispointycastle's AES-GCM, the blind indexcrypto's HMAC.doc/object_persistence.md§ Field-Level Encryption,doc/configuration.md§ Field-Level Encryption, notes indoc/server.md,doc/tenancy.md,doc/db_migration.md,doc/audit.mdanddoc/datasources.md;example/object_persistence/field_encryption_example.dart, on the package tour;test/field_encryption_test.dart,test/field_encryption_db_test.dart. -
Sagas (
transactionsmodule).TomSagaOrchestratorruns work that spans services as aTomSagaofTomSagaSteps, each anactionand an optionalcompensation(TomSagaWork, given aTomSagaContextwith the attempt, the reason it compensates and the saga's JSON data, which a step may change and which is saved with it). Every step and every compensation is a job run of an inlineTomSchedulerthe orchestrator owns — jobssaga.<name>andsaga.<name>.compensation(tomSagaJobPrefix,stepJobName,compensationJobName) — run inTomTransactionManager.runInTransactiontogether with the update that moves the saga on, written only over the version the run was queued for: a step that throws leaves no trace and is retried byTomSaga.retryPolicy, a step that committed never runs again. A step that fails for good, or throws aTomPermanentFailure, turns the saga tocompensating, and the completed steps are compensated last first bycompensationRetryPolicy(ten attempts by default); a compensation that fails for good ends the sagafailedand raises the scheduler's alert (TomSagaStatus:running,compensating,completed,compensated,failed).register,begin(name, data:, id:)— for the tenant in context, inside the caller's transaction when there is one —find,start,tick,close, andresume, which re-queues every unfinished saga whose run is gone, at start and everysweepInterval(five minutes), in every tenant of an installedTomTenancy. States (TomSagaState) are kept in aTomSagaStore:TomMemorySagaStore, orTomSqlSagaStore— a data source resolved by name per call, so under tenancy a tenant's sagas live with its data, or bound with.over(datasource)— intom_saga, declared once bysagaTable(sagaTableDdl,defaultSagaTable, read back asTomSagaRow), listed byframeworkOwnedTablesunder the newsagaparameter, and shipped as the reference migrationexample/transactions/migrations/main/tomcore/[1]-create_tom_saga.sqlwith its PostgreSQL form.TomSagaExceptioncarriestransactions.saga.invalid_definition,…already_registered,…unknown,…invalid_id,…id_taken,…data_not_json,…closedand…no_datasource.doc/transactions.md§ Sagas, notes indoc/tenancy.mdanddoc/db_migration.md;example/transactions/saga_example.dart, on the package tour;test/saga_test.dart,test/saga_db_test.dart. -
TomEventInstants(schedulingmodule): strictly increasing window instants for the runs a component enqueues itself rather than through a schedule —next(now)answersnowin UTC, or one microsecond past the last instant it gave out. Used by the outbox and the saga orchestrator. -
Idempotency keys (
endpointsmodule).@TomIdempotent({retention})on an endpoint lets a request carry anIdempotency-Keyheader (1 to 191 characters): the first request runs and its response is stored; a retry with the same key gets the stored response withIdempotent-Replayed: truewithout running again; the same key for a different request (method, path, query and body, compared as a SHA-256) answers422endpoint.idempotency.key_reused, a key whose request still runs409endpoint.idempotency.in_progresswithRetry-After: 1, an invalid key400endpoint.idempotency.key_invalid. A key is scoped to the tenant, the authenticated principal and the operation ("<METHOD> <uri>"); a request without a principal or without the header runs as before. A5xx, a thrown handler and the statuses saying the request did not run (TomIdempotency.notStoredStatuses:401,403,408,409,425,429) release the key;set-cookieand the hop-by-hop headers are not replayed. A claim left unfinished pastinProgressTimeout(five minutes) is taken over; records expire after the retention (24 hours by default) and are removed byTomIdempotencyStore.purgeExpired.TomIdempotencyholds the settings andhandle, whichTomEndpointHandler.handleMethodCallroutes every request of an annotated endpoint through (notOPTIONS);TomMethodMetadata.idempotentcarries the annotation, and a second one on a method stops the router build withendpoint_pipeline.get_class_method_metadata.multiple_idempotent_tags. Stores:TomMemoryIdempotencyStore(the default, one process) andTomSqlIdempotencyStore— a data source resolved by name per call, so under tenancy a tenant's keys live in its own schema or database, or bound with.over(datasource)— keeping the records intom_idempotency_key, declared once byidempotencyTable(idempotencyTableDdl,defaultIdempotencyTable, read back asTomIdempotencyRow), listed byframeworkOwnedTablesunder the newidempotencyparameter, and shipped as the reference migrationexample/endpoints/migrations/main/tomcore/[1]-create_tom_idempotency_key.sqlwith its PostgreSQL form.TomIdempotencyScope,TomIdempotencyRecordandTomIdempotencyClaimmake up the store contract;TomIdempotencyExceptioncarries the keys above plusendpoint.idempotency.no_datasourceandendpoint.idempotency.config.invalid.TomServer.idempotencyConfigurationKey('idempotency') andTomServer.configureIdempotency([provider]), called bystart()right afterconfigureHttpClients, apply thedatasource,table,createSchema,retentionMinutesandinProgressTimeoutSecondskeys.doc/endpoints.md§ Idempotency Keys,doc/configuration.md§ Idempotency Keys, notes indoc/tenancy.md,doc/db_migration.mdanddoc/server.md;example/endpoints/idempotency_example.dart, on the package tour;test/idempotency_test.dart,test/idempotency_db_test.dart. -
Idempotent receiver (inbox) for consumers of a message broker (
messagingmodule).TomInbox.process(consumer, messageId, work, {acknowledge})records the(consumer, message id)pair and runsworkin one database transaction, skipsworkfor a pair already recorded (TomInboxOutcome.processed/duplicate), and callsacknowledgeafter the commit — for a duplicate too; a failingworkrolls back with its record, is not acknowledged and is rethrown.recordAndRun,hasProcessedandpurgeProcessedBeforecomplete the contract.TomSqlInbox(datasource,table,createSchema,clock;ensureSchema,deleteAll) keeps the records intom_message_inboxof the data source the consumer's work writes to, so both commit together and simultaneous deliveries are serialised by the table's key;TomMemoryInboxkeeps them in memory for tests. The table is declared once byinboxTable(inboxTableDdl,defaultInboxTable, read back asTomInboxRow), listed byframeworkOwnedTablesunder the newinboxparameter, and shipped as the reference migrationexample/messaging/migrations/main/tomcore/[2]-create_tom_message_inbox.sqlwith its PostgreSQL form.TomInboxExceptionrefuses an empty or over-long consumer name or message id (messaging.inbox.invalid_consumer,messaging.inbox.invalid_message_id). Broker-neutral: there is no message bus or broker adapter intom_core, and any consumer loop callsprocesswith its own acknowledgement.doc/messaging.md§ Idempotent Receiver (Inbox);example/messaging/inbox_example.dart, on the package tour;test/inbox_test.dart,test/inbox_db_test.dart. -
Resilient outgoing HTTP calls, configured by
httpClient.TomServer.httpClientConfigurationKey('httpClient') names the configuration key andTomServer.configureHttpClients([provider])applies it to the kernel'sTomHttpResilience.configure—timeoutMs,streamedTimeoutMs(uploads with a streamed body; off unless set),retry,circuitBreaker,bulkheadand per-destinationdestinations.start()calls it after the self-login and before building the router; a value of the wrong shape stops the start withhttp.client.config.invalid, and without the key the kernel's defaults stand (a 30-second timeout among them). The server's own HTTP clients run under these policies:TomHttpOAuthTokenTransportsends through one sharedTomResilientClient(aGET— discovery, SAML metadata, user info — is retried, the tokenPOSTis not, as a code is single-use), andTomS3BlobStoreandTomOtlpHttpTransportdefault their client to one; the webhook, SMS and push transports, the FCM token exchange and the server self-login get it throughTomServerChannel's default client factory.doc/configuration.md§ Outgoing HTTP Calls, with short notes indoc/server.md,doc/authentication.md,doc/file_storage.md,doc/metrics.mdanddoc/messaging.md;test/http_client_resilience_test.dart. -
Graceful shutdown on SIGTERM.
TomGracefulShutdown(servermodule) runs, in order:GET /readyanswers503 shutting down; requests are still accepted for the readiness delay; the listener closes and the shutdown gate refuses a new request with503,Connection: closeandRetry-After: 1while the probe paths (tomProbePaths) pass; every started scheduler — the outbox's included — is stopped and the requests and runs in flight get the grace period to finish; once the responses are written, the started outboxes and schedulers are closed, releasing their leases, and connections still open at the deadline are closed; the data sources close their pools.TomServer.gracefulShutdownis the server's instance andTomServer.shutdown()runs it without a signal (idempotent, never exits).TomServer.startinstalls the gate as the outermost pipeline layer and registers the shutdown withtomShutdownCleanupfor SIGTERM (not on Windows) and SIGINT, raising its phase timeout to cover the delay and the grace period.doc/server.md§ Graceful Shutdown, with the Kubernetes settings (terminationGracePeriodSeconds,preStopor readiness delay, probes). -
Configuration keys
shutdownGracePeriodSeconds(default20),shutdownReadinessDelaySeconds(default0) andexitOnShutdownSignal(defaulttrue; environmentSHUTDOWN_GRACE_PERIOD_SECONDS,SHUTDOWN_READINESS_DELAY_SECONDS,EXIT_ON_SHUTDOWN_SIGNAL): the grace period, the time requests are still taken after readiness turned to503, and whether a signal ends the process with exit code0once the shutdown and every cleanup phase have run.doc/configuration.md§ Graceful Shutdown. -
TomHealthServer.markShuttingDown()andTomHealthServer.isShuttingDown: once marked,GET /readyanswers503 shutting downwithout asking the readiness analyzer, and liveness is unchanged;resetHealthChecks()clears the mark.doc/healthcheck.md§ While the server shuts down. -
example/server/graceful_shutdown_example.dart— a graceful shutdown step by step, around a handler and a scheduler, on the package tour. -
Request correlation ids and structured logs.
tomRequestContextMiddlewaremints every request'sTomRequestIdand continues the caller's W3Ctraceparentwith a span of the server's own (the kernel'sTomTraceContext), or starts a trace; it runs the rest of the pipeline in a zone carrying both and answers with theX-Request-Id(tomRequestIdHeader) andtraceparentresponse headers.TomServer.startinstalls it after the metrics endpoint and before the request metrics, so the access log, both error layers and authentication log under the same ids. Building it registers therequest_idfield of structured log lines (TomLogContext); a JSON line carriestrace_id,span_id,tenantandrequest_id, and no personal data in a fixed field.doc/server.md§ Correlation Ids and Structured Logs. -
Configuration key
logFormat(text— the default — orjson; environmentLOG_FORMAT) andTomServer.applyLogFormat:jsonreplaces the default console output with the kernel'sTomJsonLogOutput, keeping an output the application installed; an unknown value is logged as an error and changes nothing.TomServer.startapplies it.doc/configuration.md§ Log Format and Readiness. -
Framework readiness checks —
TomFrameworkReadinessAnalyzerandTomReadinessCheckin thehealthcheckmodule: every registered data source operational and answeringSELECT 1withindataSourceTimeout, no database migration running or failed in this process, every started message outbox delivering, and — only whenmaxOutboxWaitis set — no message waiting longer. The body isready/not readyand one line per check.TomServer.startinstalls it onGET /readywhen the new configuration keyframeworkReadiness(defaulttrue, environmentFRAMEWORK_READINESS) is on and the application installed no readiness analyzer of its own;/healthis not affected.doc/healthcheck.md§ The Framework Readiness Checks, with the Kubernetes probe configuration. -
TomDbMigrations.lastRun, aTomMigrationRunwith itsTomMigrationRunState(notRun,running,succeeded,failed), recorded by everymigrateDatabasescall, andTomDbMigrations.resetLastRun().doc/db_migration.md§ Where the last run stands. -
TomMessageOutbox.started— the started, unclosed outboxes of the isolate — andTomMessageOutbox.oldestWaitingSince().doc/messaging.md§ The outbox and readiness. -
example/healthcheck/readiness_logging_example.dart— the framework readiness checks and the JSON log line of one request, on the package tour. -
Metrics, exposed for Prometheus and over OTLP — the new
metricsmodule. The server records into the kernel's process registry (TomMetrics.registry, the kernel's newmetricsmodule) beside the scheduler's job series:tom_http_server_requests{method,route,status} andtom_http_server_request_duration_seconds{method,route}, labelled by the endpoint's route pattern (tomUnmatchedRoute,unmatched, when no endpoint matched) throughtomRequestMetricsMiddleware; a Prometheus scrape answered bytomMetricsEndpointMiddlewareat the outer edge of the pipeline, before authentication, with an optional bearer token compared in constant time; and an OTLP/HTTP JSON export —TomOtlpHttpTransport(signal-neutral, posts to<endpoint>/v1/<signal>,parseHeadersin theOTEL_EXPORTER_OTLP_HEADERSform,urlOf),TomOtlpMetricsExporter(cumulative, every interval, a final push onclose),tomOtlpMetricsJsonandTomOtlpExportException(otlp.export.failed,otlp.config.invalid_headers).doc/metrics.md,example/metrics/metrics_example.dart. -
Configuration keys
metricsEndpoint,metricsPath,metricsToken,otlpEndpoint,otlpHeadersandotlpMetricsIntervalSeconds(environmentMETRICS_ENDPOINT,METRICS_PATH,METRICS_TOKEN,OTLP_ENDPOINT,OTLP_HEADERS,OTLP_METRICS_INTERVAL_SECONDS);TomServer.startinstalls the scrape whenmetricsEndpointis on and createsTomServer.metricsExporter— registered with the shutdown cleanup — whenotlpEndpointis set.doc/configuration.md§ Publishing Metrics. -
TomRequestLogInfo.route— the pattern of the endpoint that served the request, set by the endpoint pipeline. -
Data-source series, recorded by
TomSqlDatasourcefor both drivers:tom_db_statement_duration_secondsandtom_db_statement_errors{datasource,operation},tom_db_pool_connections{datasource,state:idle,in_use,waiting,max},tom_db_pool_acquire_duration_secondsandtom_db_pool_acquire_failures{datasource} — names exported astomDbStatementDurationMetric,tomDbStatementErrorsMetric,tomDbPoolConnectionsMetric,tomDbPoolAcquireDurationMetricandtomDbPoolAcquireFailuresMetric. The label is the newTomSqlDatasource.registeredName, recorded byregisterAs, whichTomDataSourceInfo.initializecalls —<base>@<tenant>for a tenant's own pool. -
tom_cache_lookups{cache,result:hit,miss,expired} (tomCacheLookupsMetric) andtom_rate_limit_refusals{rule,tenant} (tomRateLimitRefusalsMetric, withtomNoTenantLabel=noneoutside any tenant). -
TomSqlJobStore.backlogand its row typeTomJobBacklogRow— the due, unclaimed runs per job and the earliest among them, in one groupedSELECT, read by the kernel scheduler'stom_job_backlogandtom_job_backlog_age_secondsgauges. The messaging outbox runs as the jobmessaging.outbox, so its lag is the kernel's job series. -
PostgreSQL (15 and later) as a second database beside MariaDB.
PostgresDatasource(type: postgresql) overpackage:postgres, with the settingsschema(the session'ssearch_path) andsslMode(disable,require— the default — orverifyFull; anything else iserror.initialize.invalid_ssl_mode), port5432by default and thepostgresdatabase whendatabaseNameis empty. Sessions run in UTC;:nameplaceholders become$1,$2, …; every bind is sent untyped so PostgreSQL infers it from its use; result columns match without regard to case; a single-integerINSERT … RETURNINGis reported as the generated id; a statement refused withcached plan must not change result typeis prepared again once.PostgresXADatasource(type: postgresql_xa) takes part in two-phase commits withBEGIN/PREPARE TRANSACTION/COMMIT PREPARED/ROLLBACK [PREPARED], honours read-only, and needsmax_prepared_transactions > 0. Failures arePostgresDatasourceException/PostgresConnectionLostException;classifySqlFailurereads a PostgreSQLServerExceptionasdefinite.doc/datasources.md§ The SQL Drivers. -
TomSqlDatasource, the driver-neutral SQL base both drivers extend: the pool, a connection per transaction (TomSqlBranch), the statement paths, the one-statement rule, the retry after a lost connection, the row mapping and the settings binding, withTomSqlPreparedStatement,TomSqlRow/TomSqlResult/TomSqlResultValues(every column read as text in one convention),TomSqlLink,TomSqlDatasourceException,TomSqlConnectionLostException,tomSqlBindValue/tomSqlBindParamsandtomSqlBranchId. -
PostgresSqlDialect, and the canonical DDL in each database's words.TomStandardSqlDialectgainscolumnType,columnDefinition,declaresIndexesInline,keyDefinition,createIndex,insertInto(withignoreConflict),replaceInto,incrementCounter,returningKey,translateCanonical,translateCanonicalScript,createSchema,schemaSetting,currentSchema,tableColumnsQuery,rejectingTrigger,truthPredicateandquoteIdentifier; the defaults render MariaDB's spelling unchanged.PostgresSqlDialectrenders the framework's canonical types (DATETIME(n)→TIMESTAMP(n),TINYINT(1)→SMALLINT,LONGTEXT→TEXT,CHAR(n)→VARCHAR(n), blobs →BYTEA,AUTO_INCREMENT→ identity …), indexes as separate statements,ON CONFLICT,RETURNING, and MariaDB'sREPLACEas aDELETEof every superseded row followed by an upsert.doc/datasources.md§ One declaration, every database. -
TomFrameworkTablerenders for the connected database:createStatements/create,insertRow(withignoreConflict),replaceStatements/replaceRow(one transaction when a database needs two statements), andTomFrameworkColumn.ddlIn.createTableDdlstays the canonical rendering the convergence guard reads. -
PostgresMigrationAdaptor(type: postgresql) runs a script and its version row in one transaction, so a failed script is rolled back and nothing is recorded; its keys are prefixedpostgres_migration.andpostgres_migration_adapter.(PostgresMigrationException). The shared part isTomSqlMigrationAdaptor(withTomSqlMigrationException), which holds the history, the rebinding, the refusal of a recorded failure and the staticchecksumOf/isWellFormedChecksum;TomDbVersionlives beside it.doc/db_migration.md§ The SQL Adaptors. -
PostgreSQL reference migrations. Every
example/<module>/migrations/main/tomcore/*.sqlhas a generated PostgreSQL form inexample/<module>/migrations_postgresql/main/tomcore/, written bydart run tool/generate_postgres_migrations.dartand held to it bytest/postgres_migrations_test.dart. -
TruthPredicateandTomQueryBuilder.truthPredicate— a constant predicate, rendered through the dialect'struthPredicate; a data scope that admits nothing renders it. -
auditTrailGuardStatements(table, dialect, event:)— the audit trail's write-once guard in a given dialect's database (a trigger function and its trigger on PostgreSQL). -
SqlPlaceholderScan.toPositional(slot:, backslashEscapes:)— the slot renderer and PostgreSQL's literal rule; a::cast is never read as a placeholder. -
The DB suites run against both databases.
tool/run_db_tests_postgres.shstarts PostgreSQL 15 on port 5439 (docker-compose.test.postgres.yaml, its own compose project,max_prepared_transactions=20) and runs thedb-tagged suites withTOM_CORE_SERVER_TEST_DB_ENGINE=postgresql; a test of a MariaDB-client-only behaviour skips there with its reason stated.test/postgres_dialect_test.dartcovers the dialect and the driver's rewrite and settings without a server. -
A local copy of item authorizations per signed-in user.
TomItemAuthorizationClient(copiedTypes: …)answers checks and accessible-item lists of those types from a copy — every item the user reaches with its level — loaded byloadCopyat sign-in (one accessible-item query per level) or on first use, held in memory on each node inTomItemAuthorizationCache's new local-copy cache (copyCacheName,lookupCopy,storeCopy;install(copyTimeToLive: …, maximumCopies: …), twelve hours and 1,000 copies by default), never persisted. A copy drops with every change that names its user — or no user — on an item of its type, and with*for the user, the report of a group membership or delegation change; one overtaken by a change while loading is not kept, and one pastmaximumScopeItemsis refused (error.item_authority.scope_too_large). The row scope reads it unchanged, still as anINlist.doc/item_authorization.md§ The Local Copy,doc/cache.md; tests intest/item_authorization_client_test.dartandtest/item_authorization_invalidation_test.dart. -
The item_authorization module: item authorizations kept by an external authority.
TomItemAuthorityis the transport-neutral port to it — Tom UAM's checks (at most 100 items a call), accessible-item query (paged) and change feed with the application's credentials, and share, change, revoke and shared-with-me with the user's token.TomItemAuthorizationClientanswerslevelOn/levelsOn/allowsfromTomItemAuthorizationCacheand asks the rest in chunks, gathersaccessibleItemIdspage by page and caches them, refuses a list overmaximumScopeItems(error.item_authority.scope_too_large) rather than cutting it short, drops the answers each sharing act affects on every node, and builds the feed follower; an answer overtaken by an invalidation, or given while acting for a delegator, is not cached.TomItemAuthorityScopeSourcefeeds theresourcerow scope from it.TomItemAuthorizationCachegains accessible-item lists (lookupAccessible,storeAccessible), dropped with the changes of their subject and type, andgeneration.doc/item_authorization.md,doc/authorization.md§ When an external authority keeps the grants; tests intest/item_authorization_client_test.dart. -
Item-authorization answers, dropped by item and subject from outside.
TomItemAuthorizationCachekeeps the level a remote authority answered for a subject on an item (TomCachedItemLevel). The new invalidation kindTomInvalidationKind.item, raised byTomCacheInvalidator.invalidateItemAuthorization(item, subject: …), drops the answers on an item, a pattern<type>:<prefix>*or*— for one subject or every subject — on every node, through the installed channel.TomItemAuthorizationWebhookverifies anitemAuthorization.changeddelivery (HMAC-SHA256 over event id, timestamp and body; two secrets during a rotation; five minutes' tolerance) and raises one per change;TomItemAuthorizationFeedFollowerreads the authority's change feed on a timer as the catch-up for a lost delivery, taking the current position and dropping every answer when it has none or its cursor expired (TomItemAuthorizationFeedExpiredException).TomCache.removeWheredrops the entries a test selects.doc/cache.md§ Item-Authorization Answers; tests intest/item_authorization_invalidation_test.dartandtest/cache_invalidation_db_test.dart. -
Item permission levels in the grant layer. On the key of an item type the kernel's
TomItemTypeRegistrydeclares,TomPrincipalResourceRoleResolver.foldRoleskeeps the stronger level by the type's order andTomResourceDataScopegrades the level through the type (TomResourceRoleRegistry.strongerOn,stateOn):viewandcommentadmit a row to queries,editandmanageto writes.doc/authorization.md§ Grants, Grades and Delegations, § The grades are the design; tests intest/resource_data_scope_test.dart. -
The sign-in organization.
TomSignInOrganizationDirectory, a bean the deployment installs, lists the organizations a principal may sign in for (TomSignInOrganizations: the organizations, the default, whether a choice is offered, and withTomOrganizationFallbackwhat a non-member organization at sign-in does) and signs a principal in for one of them (signInFor).TomAuthenticationServersettles every login's organization against the list before the second factor is decided — the named organization for a member, the default for none, refusal or the default for a non-member (login.error.organization.not-member,login.error.organization.none) — andswitchOrganizationissues new tokens for another organization within a session without a credential check, the second factor carried over, audited assession.organizationSwitched;signInOrganizationsanswers the list. Without a directory nothing changes.doc/authentication.md§ The sign-in organization; tests intest/sign_in_organization_test.dart. -
Tenant and organization from an external identity provider's token.
TomExternalTenancyMapping(tenancyMappingon both provider configs, read fromtenantClaim,organizationClaimandorganizationParameter) reads the tenant and the sign-in organization from an OIDC token or a SAML assertion — for an application signing in through a Tom identity provider such as Tom UAM.TomOAuthAuthenticationServicerefuses a claimed tenant other than the request's (login.error.tenant-mismatch) before any account is linked or provisioned, places the principal in the claimed tenant, and makes the claimed organization the login's and the principal's. A configured claim is required (login.error.oauth.tenant-missing,login.error.oauth.organization-missing,login.error.oauth.tenant-invalid). WithorganizationParameter, an OIDC authorization request carries the organization the login asked for, so a new authorization naming another organization is how a user switches.doc/authentication.md§ Claim mapping: tenant and organization; tests intest/oauth_test.dartandtest/saml_test.dart. -
Identity federation per tenant. SCIM, the pending state of an external sign-in and the provider configuration follow the tenant in context.
TomSqlScimStore.namedresolves its data source through tenancy's routing on every call, andtom_scim_resourcegains atenantcolumn every statement filters on, so two tenants' identity providers see only their own users and groups in every isolation model (auserNameis unique per tenant);TomMemoryScimStorekeeps a partition per tenant.TomScimProvisioningtakes secrets per tenant (tenantBearerTokens,scim.tenants.<id>.bearerTokens), and inside a tenant only its own admit.TomOAuthAuthorizationState.tenantrecords the tenant a flow began in, and both state stores answer and spend records of the tenant in context only, so a callback in another tenant fails asoauth.flow.unknown_state;tom_oauth_stategains atenantcolumn.TomOAuthProviders.tenantsandTomSamlProviders.tenants(TomTenantProviderSets) hold provider sets per tenant, read byloadTenants()fromoauth.tenants/saml.tenantsor installed at run time;resolve()answers the set of the tenant in context. A deployment that applied the earlier reference migrations runsALTER TABLE tom_oauth_state ADD COLUMN tenant VARCHAR(32) NOT NULL DEFAULT ''andALTER TABLE tom_scim_resource ADD COLUMN tenant VARCHAR(32) NOT NULL DEFAULT '', then re-createsuq_tom_scim_resource_nameandix_tom_scim_resource_externalwithtenantfirst.doc/tenancy.md§ Identity Federation per Tenant; tests intest/tenant_bound_stores_test.dart,test/tenant_bound_stores_db_test.dart(all three isolation models) andtest/oauth_test.dart. -
Removing a tenant from a running deployment —
TomTenancy.removeTenant, the counterpart ofaddTenant. The tenant leavestenants, so its requests — by token claim, host, path or header — and its work in context are refused astenancy.tenant.unknown; its routes are dropped and the data sources the tenancy opened for it are unregistered and closed, with their connection pools. A data source it only named by reference stays open; its schema or database is left for the caller to drop, with the returnedTomTenant.doc/tenancy.md§ Removing a Tenant; tests intest/tenancy_test.dartand, under all three isolation models,test/tenancy_db_test.dart. -
Tenant resolution by a path segment —
TomTenantPathResolverintenancy/tenant_resolution.dart, beside the host and header resolvers. It names the tenant by the path segment afterprefix(tby default:/t/acme/…), mapped throughlookupwhen the URLs name tenants by a slug other than the tenant id. A segment the lookup does not know is refused byTomTenancy.resolveRequestastenancy.tenant.unknown(404) instead of passing as a request without a tenant; a token for another tenant is refused astenancy.resolution.conflict(403), as for host and header. Configured asresolution: [path]withpathPrefixandpathSegments(segment → tenant id).doc/tenancy.md§ Resolving a Request's Tenant; tests intest/tenancy_test.dart. -
Secret references in configuration —
configuration/secrets.dart. A value in any configuration source may be writtensecret:<provider>:<path>[#<field>].TomSecretsholds the registered providers and the resolved values:register,providers,references,load(every reference in a tree, each provider path fetched once, fatal on the first failure),resolve,current,substitute,watch,refresh(fail-soft: a reference that cannot be fetched keeps its last value and is reported),startRefresh,stopRefreshandreset.TomSecretsProvider(name,fetch,close) is the store interface,TomMapSecretsProvidera map-backed one for tests and local development;TomSecretReference(parse,isReference,prefix),TomSecretPayload(data,version,valueOf— a field of a map or of a JSON object string),TomSecretSubscriptionandTomSecretsRefresh(changed,failed).TomSecretsExceptionraisessecrets.reference.malformed,secrets.reference.unresolved,secrets.provider.unknown,secrets.field.required,secrets.field.missing,secrets.fetch.not_foundandsecrets.fetch.failed. -
TomBaseServerConfiguration.collectSources: the four configuration sources merged in precedence order with secret references unresolved — what a boot hands toTomSecrets.load.getDefaultValuesiscollectSourceswith every reference substituted in place, so a secret takes the precedence of the source it was written in; a reference that was not loaded is refused (secrets.reference.unresolved). -
example/configuration/secrets_example.dart(nine steps, in the tour) andtest/secrets_test.dart. -
Service accounts and API keys —
authentication/api_keys.dartandauthentication/sql_api_key_store.dart.TomServiceAccount(id,name,roles,tenant,organization,disabled;withDisabled,withRoles),TomApiKey(the SHA-256 of the secret, never the secret;isLiveAt,copyWith),TomIssuedApiKey(the one-timetomk.<keyId>.<secret>value),TomApiKeyIdentity(the key a request authenticated with, in the request zone),TomApiKeyStore,TomMemoryApiKeyStore, andTomApiKeyswithinstall,reset,current,saveAccount,issue,rotate(overlap zero by default — the old key ends at once; an overlap never extends a key past its own expiry),revoke,keysOf,authenticateandhashSecret. A key's roles are intersected with its account's; the principal isservice:<accountId>with the account's tenant and organization. Key records are cached per node intom.apiKeysfor at mostlookupCacheSeconds(60), andrevoke/rotatedrop the entry throughTomCacheInvalidator;lastUsedAtis written at most once pertouchInterval(one minute).TomSqlApiKeyStorekeeps accounts and keys intom_service_accountandtom_api_key(serviceAccountTable,apiKeyTable,defaultServiceAccountTable,defaultApiKeyTable,TomServiceAccountRow,TomApiKeyRow).TomApiKeyExceptionraisesapi_key.issue.unknown_account,api_key.issue.role_not_held,api_key.rotate.unknown_keyandapi_key.revoke.unknown_key. -
Requests authenticated by an API key.
tomDefaultAuthenticationMiddlewarereadsX-API-Keywhen a request carries noAuthorizationheader, puts the service account's principal and itsTomApiKeyIdentityin the zone. A key never widens a request that presented a bearer token; with noTomApiKeysinstalled the header is ignored with a warning. -
Rate limiting — the new
ratelimitmodule (ratelimit/rate_limit.dart,ratelimit/sql_rate_limiter.dart).TomRateLimitScope(principal,apiKey,address;parse),TomRateLimitRule(name,scope,limit,window,pathPrefixes,anonymousOnly),TomRateLimitDecisionwith the sliding-window counter inslidingWindowandretryAfterSeconds,TomRateLimiter,TomMemoryRateLimiter(per node),TomSqlRateLimiter(thetom_rate_limittable —rateLimitTable,defaultRateLimitTable,TomRateLimitRow— counted with one atomicINSERT … ON DUPLICATE KEY UPDATEper request),TomClientAddress(X-Forwarded-Forread right to left, only from trusted proxies),TomRateLimits(configurationKeyrateLimits,fromConfiguration,install,reset,current,bucketOf,check,headersOf,tooManyRequests) andtomRateLimitMiddleware. A refused request is answered429withRetry-AfterandRateLimit-Limit/-Remaining/-Reset; an admitted one carries theRateLimit-*headers of its tightest rule.TomRateLimitExceptionraisesrate_limit.exceededandrate_limit.config.not_a_map,invalid_rule,duplicate_rule,unknown_scope,unknown_limiterandmissing_datasource. Nothing is limited by default. -
TomServerstackstomRateLimitMiddlewareinside the authentication middleware, andstartinstalls the limits therateLimitsconfiguration key describes when none were installed. -
example/ratelimit/rate_limit_example.dartwalks a service account's key through seven steps;test/api_keys_test.dartandtest/ratelimit_test.dartcover both features without a database andtest/ratelimit_api_keys_db_test.dart(taggeddb) runs the shared store and limiter across two nodes. -
doc/ratelimit.md,doc/api/api_summary_ratelimit.md, the module readme, and the service-account section ofdoc/authentication.md. -
Multi-tenancy — the new
tenancymodule (tenancy/tenancy.dart,tenancy/tenant_resolution.dart).TomTenancyholds the deployment'sTomTenants, read from thetenancyconfiguration key byfromConfiguration, and is installed withTomTenancy.install, which provisions every tenant and routes data-source lookups;reset,current,addTenant(onboarding without a restart),isolationOf,route,runFor,forEachTenantandresolveRequest.TomTenantIsolation—shared,schema(a schema of its own on the shared server, created withcreateSchemas) ordatabase(a database of its own, configured per tenant) — is chosen per tenant.TomTenantResolver,TomTenantHostResolver(subdomain or own host),TomTenantHeaderResolver(X-Tenant-Idby default) andTomTenantResolution.TomTenancyExceptionraisestenancy.config.not_a_map,tenancy.config.unknown_isolation,tenancy.config.unknown_resolver,tenancy.config.schema_unsupported,tenancy.config.invalid_schema,tenancy.config.missing_datasource,tenancy.config.unknown_datasourceandtenancy.tenant.unknown. A tenant is an isolation boundary, distinct from the organization, a business unit inside it. -
Requests run in their tenant. With a tenancy installed,
tomDefaultAuthenticationMiddlewareresolves the request's tenant — the verified token'stenantclaim, authoritative; otherwise host or header — and runs the request insideTomTenantContext. A request naming another tenant than its token is answered403 tenancy.resolution.conflict, an unknown tenant404 tenancy.tenant.unknown, a malformed one400 tenancy.id.invalid, each with a JSON body. -
Tokens are bound to their tenant.
TomAuthenticationServersets the principal'stenantto the tenant the login runs in, so the token carries thetenantclaim, and refuses a principal placed in another tenant withlogin.error.tenant-mismatch. -
@TomTenantOwned(object_persistence/persistence_annotations.dart,fielddefaulttenantId) andTomAnnotationAnalyzer.findTenantOwnership. The repository ANDs<field> = <tenant>with the@TomDbScopepredicate on every read (parameters prefixedTomSqlDatasourceRepository.tenantParameterPrefix,__tomtenant_), fills the tenant on insert, and refuses a write with no tenant (data_scope.write.no_tenant) or naming another (data_scope.write.foreign_tenant). -
Data-source routing.
TomDataSourceInfo.router,routeandgetBaseDataSourceInfo;getDataSourceInfoanswers the data source as the current tenant reaches it. -
TomSchemaProvisioning(datasources/framework_table.dart): a store's tables are created at most once, however many first calls arrive together. -
Tenants in caches, revocations, jobs and messages.
TomCache's static API qualifies keys by the tenant in context;TomCacheInvalidator.revokePrincipalandinvalidateCacheEntryqualify by it;TomAuthorizationCache.isRevokedreads the token'stenantclaim.tom_job_rungainstenant VARCHAR(32) NOT NULL DEFAULT ''and the window key(job_name, tenant, scheduled_for);TomSqlJobStore.findWindowtakestenant:. The outbox queues a message for the tenant in context and delivers it inside it;TomDeliveryReceipt.tenant,TomDeliveryReceipt.forTenantandTomDeliveryCorrelation.tenant, stored intom_delivery_report.tenant VARCHAR(32) NULL. -
example/tenancy/tenancy_example.dartwalks two tenants through eight steps;test/tenancy_test.dartcovers the module without a database andtest/tenancy_db_test.dart(taggeddb) under all three isolation models. -
doc/tenancy.md,doc/api/api_summary_tenancy.mdand the module readme. -
User provisioning over SCIM 2.0 (RFC 7643 / RFC 7644), for Microsoft Entra ID, Okta and any other SCIM client.
TomScimService(authentication/scim_service.dart) creates, reads, queries, replaces, patches and deletes users and groups over aTomScimStore—TomMemoryScimStore, orTomSqlScimStore(authentication/sql_scim_store.dart) over the new framework tabletom_scim_resource(scimResourceTable,scimResourceTableDdl,defaultScimResourceTableinauthentication/scim_sql.dart; reference migrationexample/authentication/migrations/main/tomcore/[5]-create_tom_scim_resource.sql). Filtering (eqnecoswewgtgeltlepr,and/or/not, value filters), paging,attributes/excludedAttributes, and PATCH in the forms Entra ID and Okta send (TomScimFilter,TomScimPath,scimValues/scimLookup/scimKeyOfinauthentication/scim_filter.dart). Bulk, sorting, ETags,/Me,.searchand password changes are not supported andServiceProviderConfigsays so.TomScimResource,TomScimResourceType,TomScimSchemasandTomScimException(scim.not_found,scim.uniqueness,scim.invalid_filter,scim.invalid_path,scim.invalid_value,scim.invalid_syntax,scim.no_target,scim.mutability,scim.unauthorized,scim.not_enabled;clientThrow: true) inauthentication/scim_resource.dart. -
The SCIM endpoints (
TomScimApi/TomScimServer,authentication/scim_endpoints.dart): seventeen routes under/scim/v2—ServiceProviderConfig,ResourceTypes,Schemas, andUsers/Groupswith GET, POST, PUT, PATCH and DELETE — inapplication/scim+json, enabled by installing aTomScimProvisioningat start-up, read fromscim.bearerTokensandscim.baseUrlbyfromConfiguration. The identity provider's bearer secrets are held as SHA-256 digests and compared in constant time. -
Deprovisioning revokes access. Deactivating, deleting or renaming a user, and changing, renaming or deleting a group, calls
TomScimService.revoke—TomCacheInvalidator.revokePrincipalby default — for every affected user, so the sessions they hold are refused on every node. -
TomScimIdentityDirectory(authentication/scim_directory.dart): single sign-on resolves to the provisioned account underprovisioning: linkExisting, with roles mapped from the account's groups, and refuses a deactivated account withlogin.error.scim.inactive. -
TomEndpointCredentials(server/server.dart): paths whoseAuthorizationheader belongs to the endpoint.extractAuthenticationleaves such a header unread and reports the newTomCredentialOutcome.endpointCredential.TomScimProvisioning.installclaims/scim/v2/. -
example/authentication/scim_provisioning_example.dartruns provisioning, single sign-on and deprovisioning end to end through the server's router. -
Asymmetric token signing, key rotation and a JSON Web Key Set. Tokens are signed RS256 by default, ES256 or HS256 by
jwtAlgorithm, with the private key read from the PEM file atjwtSigningKeyFile(PKCS#8; EC keys on the named curve P-256). Every token carries akidheader: the RFC 7638 thumbprint of the key's public JWK, so every server derives the same id from the same key without configuring it.jwtVerificationKeyFileslists public keys (SPKI) that still verify — the keys an issuer rotated away from, or the issuer's keys on a server that only verifies.jwtIssuer(defaulttom) andjwtAudience(default none) are written into every token and required of every token verified. New inauthentication/jwt_keys.dart:TomJwtSigning(current/install/reset,keys,signingKeyId,canSign,issuer,audience,keyFor,jwks,requireSigning),TomJwtAlgorithm,TomJwtKey,TomJwtSigningKey,readPemFileandTomJwtKeyException(jwt.keys.unknown_algorithm,jwt.keys.unreadable_key_file,jwt.keys.verify_only;clientThrow: false). -
GET /.well-known/jwks.json(TomJwksService/TomJwksServer,authentication/jwks_endpoint.dart), mounted like every component endpoint: the public half of every verification key, signing key first, asapplication/jwk-set+jsonwithcache-control: public, max-age=300;@TomPublicAccess;404under HS256. Any JWT library or gateway verifies this deployment's tokens through it. -
A verify-only server. Verification key files and no signing key: tokens are verified, and issuing one is refused with
jwt.keys.verify_only. -
doc/authentication.mddocuments the rotation procedure. -
Enterprise single sign-on over SAML 2.0. SAML identity providers are configured under
saml.providers(TomSamlProviders,TomSamlProviderConfig,authentication/saml_provider.dart), from the identity provider's metadata (metadataUrl, fetched byTomSamlProviders.load) or directly (issuer,ssoUrl,idpCertificates).TomSamlFlow(authentication/saml_flow.dart) runs service-provider-initiated Web Browser SSO in three legs: theAuthnRequestover the HTTP-Redirect binding (signed whenspSigningKeyFileis set), the response toPOST /saml/acs, answered with a303to the application'sredirectUricarrying a one-time handoff code, and the completion through the existingPOST /oauth/complete.GET /saml/metadata/<providerId>publishes this server's metadata (TomSamlService/TomSamlServer,authentication/saml_endpoints.dart).TomSamlResponseValidatorchecks destination,InResponseTo, issuer, status, a single unencrypted assertion, signatures, bearer confirmation, validity window and audience, each with its ownsaml.*key.TomXmlSignatureVerifierandTomExclusiveCanonicalizer(authentication/saml_xml_signature.dart) verify RSA-SHA256/512 enveloped signatures under exclusive canonicalization, refuse SHA-1, ignoreKeyInfo, and defeat signature wrapping. NewTomSamlException,TomSamlNames,TomXmlDsig,TomSamlCertificates,TomSamlIdpMetadata. New dependencyxml. -
OpenID Connect discovery. A provider with
discovery: true(and an optionaldiscoveryUrl) takesauthorizationEndpoint,tokenEndpoint,userInfoEndpointandrevocationEndpointfrom the issuer's discovery document, read at start-up byTomOAuthProviders.load/discover; explicit values win, and the document's issuer must match exactly (oauth.config.discovery_issuer_mismatch,oauth.config.discovery_failed,oauth.config.discovery_not_loaded).TomOAuthProviderConfig.isDiscovered. -
Group-to-role mapping.
TomExternalRoleMapping(authentication/external_provider.dart) readsgroupsClaim(dotted paths supported),roleMappinganddefaultRolesper provider, for both protocols (oauth.config.invalid_role_mapping). The mapped roles are applied to the session's principal on every login byapplyExternalRoles, beside the account's own.TomExternalIdentitygainedgroups,roles,toJsonandfromJson;TomOAuthProviderConfiggainedroleMapping. -
TomExternalProvider, the interfaceTomOAuthProviderConfigandTomSamlProviderConfigimplement. -
TomOAuthProvisioning.linkExisting: link a pre-provisioned account by the asserted username, provision none. -
The OpenID Connect nonce. Every flow whose scopes include
openidsends a nonce, stored onTomOAuthAuthorizationState.nonce, and refuses anid_tokenthat does not return it (oauth.flow.nonce_mismatch). -
TomOAuthFlow.samlandTomOAuthFlow.descriptors: the flow dispatches SAML provider ids toTomSamlFlow, andGET /oauth/providerslists OAuth and SAML providers together. Provider ids are unique across both protocols (oauth.config.duplicate_provider_id). -
TomOAuthAuthorizationState.assertedIdentityholds a verified SAML identity between the assertion consumer service and the completion. -
Optimistic locking:
@TomVersion(object_persistence/persistence_annotations.dart) on anintfield, read asTomColumnInformation.versionand found byTomAnnotationAnalyzer.findVersionColumn. An insert writes version1; an update writesversion + 1onlyWHERE … AND version = :__tomversion_expected(TomSqlDatasourceRepository.versionParameter;buildUpdateStatementtakes the column asversion:), anddelete(entity)checks the version the same way. A write that changes no row raisesTomOptimisticLockException(extendsTomRepositoryException,clientThrow: true,heldVersion,storedVersion—nullwhen the row is gone) withcrud_repository.save.version_conflict/crud_repository.delete.version_conflict, writing nothing, discarding no blob and auditing nothing. A versioned save or delete with anullversion is refused withcrud_repository.save.version_missing/crud_repository.delete.version_missing. The analyzer refuses a second version field (annotation_analyzer.version.more_than_one) and one that is not anint, is the entity id, an association or read-only (annotation_analyzer.version.invalid_field).deleteByIdand the query deletes are not version-checked. Example:example/object_persistence/optimistic_locking_example.dart. -
TomPreparedStatement.runandTomStatementOutcome<T>(datasources/datasource_initialization.dart):runreturns the rows, the affected-row count (affectedRows) and the generated key (generatedId) of that one execution.MariadbPreparedStatement.runreads all three from the execution's own result set, retry included;executereturns its rows. -
TomSchemaDivergenceKind.nullableVersionColumn(error): the schema-convergence check reports a@TomVersioncolumn the migration chain leaves nullable, since aNULLversion matches no versioned save.TomEntitySchemaReaderreads a version column asnullable: false. A table gains the column withALTER TABLE t ADD COLUMN version BIGINT NOT NULL DEFAULT 0;, and its existing rows load at version0. -
The OpenAPI document endpoint (
endpoints/openapi_document_endpoint.dart).TomOpenApiServicedeclaresGET /openapi.json(@TomPublicAccess(),logRequests: false) andTomOpenApiServerserves the fileTomOpenApiServer.globalSettingDocumentPathnames, read on every request. The document is generated at build time bytom_core_openapi; the server only serves it. With no path configured the route answers404with the error envelope keyedopenapi.document.not_served, and with a path naming no file404keyedopenapi.document.missing. -
TomBaseServerConfiguration.openApiDocument(environment nameOPEN_API_DOCUMENT, default empty): the documentGET /openapi.jsonserves.TomServer.startsetsTomOpenApiServer.globalSettingDocumentPathfrom it. -
doc/endpoints.md§ The OpenAPI Document anddoc/configuration.md§ Publishing the OpenAPI Document. -
Per-instance health analyzers (
healthcheckmodule).TomHealthServer({liveness, readiness, timeout})carries its own analyzers and deadline, read through the gettersliveness,readinessandtimeout; each one left out is the process-wide setting (livenessAnalyzer,readinessAnalyzer,healthTimeout), read when the probe arrives. A non-positivetimeoutis refused with anArgumentError. The mounted bean is built with no arguments and answers with the process-wide settings.TomHealthServer.verdictOftakes the deadline per call (timeout, defaulthealthTimeout).doc/healthcheck.md§ Per-Instance Analyzers. -
Cooperative cancellation of a timed-out health analysis (
healthcheckmodule).TomHealthCheck(probe,deadline,isCancelled,cancelled,throwIfCancelled,cancel) is handed to every analysis; when the deadline passesverdictOfanswersTIMEOUTand cancels it, andthrowIfCancelledthrowsTomHealthCheckCancelled. Probes share one analysis per analyzer, and the first deadline to pass cancels it.TomFrameworkReadinessAnalyzer.checks([check])stops between its stages once the check is cancelled.doc/healthcheck.md§ Cancelling a Timed-Out Analysis;example/healthcheck/healthcheck_example.dartsteps 7 and 8.
Changed #
-
Breaking:
TomHealthAnalyzer.analyzeHealthtakes the check. The contract isFuture<TomHealthStatus> analyzeHealth(TomHealthCheck check); an analyzer overridinganalyzeHealth()adds the parameter, and one composingTomFrameworkReadinessAnalyzerpasses its check through. A synchronous throw fromanalyzeHealthis answered asFAILED, like an asynchronous one. -
The request id is minted before authentication.
tomDefaultAuthenticationMiddlewarecarries theTomRequestIdit finds in the zone — the onetomRequestContextMiddlewareminted — into the request's zone values, and mints one only when it runs without that middleware outside it. -
TomMessageOutbox.startafterclosethrowsmessaging.outbox.closed, assenddoes. -
The server records metrics.
doc/server.mdno longer states that the module has no metrics facility and that a metrics route is an ordinary endpoint: the pipeline now opens with the scrape endpoint (when configured) and the request series, outside logging, both error layers and authentication.doc/server.md§ The Pipeline and § Metrics. -
Breaking: the MariaDB types extend the SQL base.
MariaDbDatasource/MariaDbXADatasourceextendTomSqlDatasource;MariadbDatasourceExceptionextendsTomSqlDatasourceException,MariadbPreparedStatementExceptionextendsTomSqlConnectionLostException,MariadbPreparedStatementextendsTomSqlPreparedStatement, andMariadbMigrationAdaptor/MariadbMigrationExceptionextendTomSqlMigrationAdaptor/TomSqlMigrationException.MariaDbDatasource.integerSettingsisTomSqlDatasource.integerSettings;mapGroupedRowis an instance method of the base taking aTomSqlRow;mapResultSetmaps throughmapSqlResult;checksumOf/isWellFormedChecksumareTomSqlMigrationAdaptor's. -
Breaking:
TomFrameworkTable.insert(verb, values)is gone — useinsertRow(dialect, values, ignoreConflict: …)orreplaceRow/replaceStatements. -
The framework's stores write in their data source's dialect: every store creates its table with
TomFrameworkTable.create, inserts withinsertRowand replaces withreplaceRow; the rate limiter counts withincrementCounter; repository inserts of a single surrogate key go throughreturningKey; schema isolation creates and points at a schema throughcreateSchema/schemaSetting(a schema inside the one database on PostgreSQL); the framework-schemaverifyLiveclassifies with the connected dialect and readstableColumnsQuery; the audit trigger probe reads the dialect'scurrentSchema. -
Per-principal rows are keyed by the tenant-qualified principal id (
TomPrincipal.qualifiedId,tenant:<tenant>/<id>; the bare id without tenancy). A principal id is unique within its tenant only: another tenant'su1is a different person.Tom2FAEnrolmentStore.principalKeyOfandTom2FAGraceStore.principalKeyOf, theownerdata scope,TomUserPreferencesandTomResourceDataScope's default key all use it, so undersharedisolation two tenants' rows stay apart in one table, and underschemaanddatabasethe stored key is the same.TomUserPreference.principalIdis aStringandtom_user_preference.principal_idaVARCHAR(255); a deployment that applied the earlier reference migration runsALTER TABLE tom_user_preference MODIFY principal_id VARCHAR(255) NOT NULL— its rows, written without tenancy, keep their keys.TomAuditActor.userIdis aString?. Scope values bind in the column's type and compare by their text (TomDataScope.inList,TomDataScope.permits): anintowner column of a deployment without tenancy keeps matching the bare id, and a tenant-qualified id matches no row there.doc/tenancy.md§ Principals per Tenant; tests intest/principal_per_tenant_db_test.dart(all three isolation models) andtest/tenancy_test.dart. -
tom_job_run.job_nameisVARCHAR(150), so the window key(job_name, tenant, scheduled_for)stays inside the 767-byte index limit of older InnoDB row formats; the reference migrationexample/scheduling/migrations/main/tomcore/[1]-create_tom_job_run.sqldeclares the new shape. -
TomSqlDatasourceRepository.datasourceInfois a getter resolved throughTomDataSourceInfo.routeon every access, and prepared statements are cached per data source, so one repository serves every tenant. -
Every framework store's
ensureSchema—TomSqlJobStore,TomSqlLeaseLock,TomSqlSchedulerNodes,TomSqlDeliveryReportStore,TomSqlInvalidationChannel,TomSqlOAuthStateStore,TomSqlScimStore,TomDatabaseBlobStore— runs throughTomSchemaProvisioning. Concurrent first calls on a pooled data source no longer race aCREATE TABLEstill in progress on another connection. -
frameworkOwnedTableslistsscimResourceTableand takes ascimResourceparameter, so the framework schema check coverstom_scim_resource. -
verifyBearerTokenchecks whose token it is: the algorithm is the configured one and never the token'salgheader (no algorithm confusion), thekidmust name a held key, andissandaudmust match the configuration. -
installJwtSigningdefaults to RS256. Unconfigured, it signs with the bundledtom_cryptoRSA pair, in a development or test environment only, with a warning; ES256 has no sample key and is refused unconfigured everywhere.jwtSharedSecretKeyis the HS256 secret and is read under no other algorithm.jwtRsaPrivKey/jwtRsaPubKeyremain the pair that encrypts the token's access-control payload, under every algorithm. -
Breaking:
TomAuthenticationServer.globalSettingJwtOrganizationis gone. Issued tokens carryTomJwtSigning.current's issuer and audience — the configuredjwtIssuerandjwtAudience. -
Requires
tom_crypto1.3.0 (TomJwtConfiguration.keyId, written as thekidheader, andTomServerJwtToken(audience:)). -
Breaking: external logins provision just in time by default.
TomOAuthProvisioning.autoProvisionis the default ofTomOAuthProviderConfigandTomSamlProviderConfig. A deployment offering a public provider setsprovisioning: refusefor it. -
Breaking:
TomOAuthExchange.provideris aTomExternalProvider, so it carries a SAML provider as well; code reading OAuth-only fields from it checks forTomOAuthProviderConfig. -
Breaking:
tom_oauth_statehas two more columns,nonce VARCHAR(191) NOT NULLandasserted_identity TEXT NULL(example/authentication/migrations/main/tomcore/[2]-create_tom_oauth_state.sql).CREATE TABLE IF NOT EXISTSleaves an existing table as it is, so a deployment holding one adds the two columns itself, or drops the table — it holds only ten-minute pending records.TomOAuthStateRowmaps both. -
TomOAuthFlow's constructor is no longerconst: it checks that no provider id is configured under both protocols. -
An existing account is not linked on an unverified address. When the provider states
email_verified: false, linking by the asserted username is refused withlogin.error.oauth.unverified-email. -
randomUrlToken, shared by the OAuth and SAML flows, is hidden from the barrel. -
Breaking: a prepared statement keeps no per-execution state. A repository runs one statement handle from every request at once, so a count or key left on the handle belonged to whichever execution finished last; two overlapping saves could read each other's. The repository's insert key,
deleteByIdcount, query-delete counts and versioned writes read theTomStatementOutcomeof their ownrun. A driver implementsrun.
Removed #
- Breaking:
TomPreparedStatement.lastGeneratedIdandTomPreparedStatement.lastAffectedRows(and theirMariadbPreparedStatementoverrides). ReadTomStatementOutcome.generatedIdandTomStatementOutcome.affectedRowsfromruninstead.
Tests #
test/graceful_shutdown_test.dartsends a real SIGTERM to a server process (test/fixtures/graceful_shutdown_server.dart) during a slow request: the request completes, no new request is accepted, the scheduler's file lease is released and the process exits with0. In process it covers the drain and the refusals with the probes passed, a scheduler drained before its lease is released, a run past the grace period abandoned with its lease released, and a second call handed the first shutdown.test/structured_logs_readiness_test.dartcovers the request context (trace continued or started, the request id kept by authentication and named on the response, the fields of a JSON line),logFormat, and the framework readiness checks without a database — including a started server answering/ready503after a failed migration while/healthstays200;test/structured_logs_readiness_db_test.dart(tagdb) the data source check against MariaDB or PostgreSQL.test/metrics_test.dartcovers the request series by route pattern, an unmatched path, an escaping error counted as500, the scrape (not counted, token refusals,HEAD), one scrape carrying request, rate-limit, cache and job series, an expired cache read, and the OTLP exporter against an in-process receiver;test/metrics_db_test.dart(tagdb) the statement and pool series on MariaDB or PostgreSQL;test/metrics_otel_test.dart(new tagotel, run bytool/run_otel_tests.sh) has promtool lint the scrape and an OpenTelemetry Collector (tool/otel/collector.yaml) receive an export.test/optimistic_locking_test.dartcovers which fields may be the version, the versionedUPDATE, the values a versioned save and delete bind, the refusals against a scripted data source, and the migration shapes the convergence check accepts and reports;test/optimistic_locking_db_test.dart(tagdb) refuses the second of two saves of one row against MariaDB, lets exactly one of six simultaneous saves win, and turns versioning on for a table that already holds rows.test/datasources_test.dart's groupMariadbPreparedStatement.runasserts the count of an execution, of its retry, and of two overlapping executions of one handle.test/scim_test.dartcovers the filter and path language, a conformance subset over HTTP in the request shapes Entra ID and Okta send — discovery, the bearer secret, users, groups, every PATCH form — and that each deprovisioning change refuses the session it should;test/scim_store_db_test.dart(tagdb) runs the relational store and the service over it.framework_schema_test.dartconverges and auditstom_scim_resource;reference_migrations_test.dartrecords its migration.test/jwt_keys_test.dartcovers key ids, RS256 and ES256 key files, rotation, what a verifier refuses, a verify-only server and the published set — verified bypackage:jose(a dev dependency), which shares no code with the framework.endpoints_pipeline_test.dartasserts the mounted/.well-known/jwks.jsonroute.test/saml_test.dartcovers exclusive canonicalization, every refusal of the SAML response validator (signature wrapping, SHA-1, another key, another request, audience, issuer, recipient, validity window, status, DOCTYPE, encryption), the three legs of the flow, identity-provider metadata and the two endpoints against a scripted identity provider (test/fixtures/saml_test_idp.dart, keys intest/fixtures/saml/).test/sso_keycloak_test.dart(tagsso) logs in over OpenID Connect and over SAML 2.0 against Keycloak 26, started bytool/run_sso_tests.shfromdocker-compose.sso.yamlwith the realm intest/fixtures/keycloak/tom-realm.json; it skips when no Keycloak is listening.test/oauth_test.dart's groupsingle sign-on over OpenID Connectcovers the nonce, discovery, role mapping and the provisioning modes;test/oauth_state_store_db_test.dartround-trips the nonce and a verified SAML identity throughtom_oauth_state.
Fixed #
- A two-phase data source can be migrated. A data source of type
mariadb_xaorpostgresql_xahad no migrator, andmigrateDatabasesrefused it withdb_migration.migration.no_migrator_found_for_datasource_type; the MariaDB and PostgreSQL adaptors serve those types too. - A write binding no parameter no longer hangs on MariaDB. For a
statement with neither parameters nor result columns —
delete by label is null— the server answersPREPAREwith thePREPARE_OKpacket alone, andmysql_client0.0.27 waits for anEOFthat never comes.MariaDbDatasourceruns a statement that binds nothing and answers no rows (one not starting withSELECT,WITH,SHOWor a bracket) over the text protocol and skips its prepare, so its errors surface when it runs; a parameterless read keeps the prepared, binary path. The protected driver hook isprepareOn(link, command, parameterCount): a driver implementingTomSqlDatasourceadds the parameter count to its override. - Two outbox messages queued in the same microsecond are both delivered.
TomMessageOutbox.sendgave a message's run the clock's reading as its window instant, andTomSqlJobStorekeeps one run per job, tenant and window — so a second message queued in the same microsecond replaced the first intom_job_run. Each run now takes its instant fromTomEventInstants.next. - CORS reaches the response, not only the preflight. Every response an
endpoint returns — an error included — now carries the endpoint's
Access-Control-Allow-Origin(corsAllowedOrigins, elseglobalSettingCorsAllowedOriginsDefault), the same value its preflight announces; before, a browser passed the preflight and was then refused every response body. NewTomEndpointHandler.withAllowedOrigin. - The request body no longer reaches the trace log. The binder's trace line
named the raw JSON body, so a login's password was written down in clear; it
now names only the bound object, whose
toStringmasks its secrets. - Every request gets its own
TomRequestLogInfo.TomServer.startseeded the record throughcontextSetupMiddleware, which hands every request the same object; the first request marked it logged, and the access log fell silent for every request after it. The pipeline now seeds a new record per request. - Concurrent transactions on one data source no longer wait for each other.
MariaDbDatasourceheld a single connection, so a transaction held the whole data source from its first statement to its conclusion and every other transaction — and every statement outside one — waited behind it. Each transaction now runs on a pooled connection of its own, and a statement outside a transaction borrows one for itself. - A lost connection inside a transaction is no longer retried elsewhere. A
prepared statement whose connection is found gone is retried once on another
connection only outside a transaction; inside one the transaction lived on the
lost connection, so the failure is reported as
error.failed-to-execute.transaction_connection_lostinstead of running the statement outside the transaction it belongs to. MariaDbDatasource.closeno longer throws when a connection fails to close. The failure is logged and the connection dropped; idle connections are closed at once and lent ones when they are handed back.
Added #
-
A connection pool for data sources:
TomConnectionPool<C>(datasources/connection_pool.dart), withTomConnectionPoolSettings(maxSize10,minIdle0,acquireTimeout30 s,idleTimeout10 min,maxLifetime30 min,validateAfterIdle30 s;validate()), the driver interfaceTomConnectionFactory<C>(open,close,isUsable,validate) andTomConnectionPoolException(clientThrow: false) with the keyserror.datasource.pool.closed,error.datasource.pool.acquire_timeoutanderror.datasource.pool.invalid_settings. Waiters are served first come, first served, a released connection is handed straight to the longest waiter, and idle and old connections are retired when the pool is next used rather than by a timer. -
Pool settings on
MariaDbDatasource:poolMaxSize,poolMinIdle,poolAcquireTimeoutMs,poolIdleTimeoutMs,poolMaxLifetimeMsandpoolValidateAfterIdleMs, bound from the configuration keys of the same names, with thepoolSettingsgetter;openConnection()(overridable; opens oneMySQLConnection),openConnectionCountandlentConnectionCount.toStringreadsopen (N lent, M idle)while the data source is open. -
MariaDbDatasource.withDriverConnection(body)runsbodywith theMySQLConnectiona statement would use — the running transaction's, or one lent for the call — for work that needs the driver's own API. -
error.initialize.invalid_integer: an integer setting other thanportgiven as a string that is no integer, naming thesettingand thevalue. -
tomRequestLogInfoMiddleware(), the middleware that seeds a newTomRequestLogInfounderTomRequestLogInfo.contextNameinto every request's context.TomServer.startinstalls it outermost; a deployment that composes its own pipeline withtomLoggingMiddlewareinstalls it the same way. -
A send pace for delivered second-factor codes:
Tom2FASendLimiter,Tom2FASendand the per-process defaultTom2FAMemorySendLimiter(authentication/two_factor_send_limit.dart).TomDeliveredCode2FAServiceclaims every challenge and enrolment code from it: the first code to an address goes out at once, each later one in a run waits twice as long (sendInterval, 30 s, doubling up tomaxSendInterval, 30 days), and a correct code ends the run. A send too soon fails withlogin.error.2fa.send-limit(TomLoginFailure.rateLimited,retryAfterSeconds). -
Passkeys:
TomWebAuthn2FAService. The fourth shipped second factor, registered as@Tom2FAAdaptor('webauthn')and offered onceglobalSettingRelyingPartyIdis set. It issues WebAuthn creation and request options (the kernel's W3C JSON wire types), verifies registrations and assertions withTomWebAuthnVerifier— client data type, challenge and origin, RP id hash, user-present and user-verified flags, an ES256 or RS256 signature, a rising signature counter, the user handle — and keeps one enrolment per credential, writing the counter back after every login. Attestation is requested asnoneand not verified. New keylogin.error.2fa.webauthn-registration-invalid. -
TomWebAuthnSoftwareAuthenticator, a passkey authenticator in software (real P-256 keys, CBOR and signatures in memory) for tests and bring-up. -
Cross-node cache invalidation (
cache/cache_invalidation.dart):TomCacheInvalidator—install(channel),revokePrincipal(username),invalidateCacheEntry(cacheName, key),invalidateCache(cacheName),raise,apply,reset,channel,nodeId— applies an invalidation on this node and publishes it through the installedTomInvalidationChannel(stalenessBound,start,publish,stop); a publish failure propagates after the local apply.TomCacheInvalidation(principal,cacheEntry,cache,fromPartsfactories),TomInvalidationKind, andTomCacheInvalidateMessage, which carries a worker's call to the main isolate.TomLocalInvalidationChannelis the default: no other node, a bound of zero. -
TomSqlInvalidationChannel(cache/sql_invalidation_channel.dart), a channel over a table every node polls:publishinserts a row, each node reads the rows above the last id it saw everypollInterval(2 s; also the staleness bound) and skips its own,startfirst applies the principal revocations raised withinretention, and rows older thanretentionare pruned.TomSqlInvalidationChannel.named(datasourceName)resolves a registered data source and throwscache.invalidation.unknown_datasourceotherwise.TomCacheInvalidationRowis the reflected row. -
The invalidation log table,
tom_cache_invalidation:invalidationTable,invalidationTableDdlanddefaultInvalidationTable(cache/invalidation_sql.dart), listed byframeworkOwnedTables(new parameterinvalidation), with the reference migrationexample/cache/migrations/main/tomcore/[1]-create_tom_cache_invalidation.sql. -
Principal revocation in
TomAuthorizationCache:revokeLocally(username, at),revokedAt(username),isRevoked(token)andrevocationRetention(24 h).getPrincipalrefuses a token issued at or before its user's last revocation — compared oniat, elsenbf, in whole seconds — withauthorization.cache.revoked_principal, whether or not a principal is cached for it; the server's authentication middleware serves such a request unauthenticated, as it does an expired entry. -
TomCache.clear(), which empties the map and the most-recently-used chain together; the clearance timer and a cache invalidation both use it.
Changed #
TomAuthorizationCache.disposeforgets every revocation as well as dropping the cached principals, and the eviction sweep forgets revocations older thanrevocationRetention.- Breaking: every participant hook is handed the transaction.
TomTransactionParticipant.prepare,commit,rollbackandendTransactiontake(TomTransaction transaction), likestartTransaction, and the coordinator passes the transaction it is concluding. A participant serving several transactions at once — a pooled data source — needs it to know which one a call concludes. - Breaking:
MariadbPreparedStatementholds noPreparedStmt. The constructor isMariadbPreparedStatement(parameterList, datasource, resultType, command, {groupedProjection})and thepreparedStmtfield is gone. Each pooled connection prepares the statement on first use and keeps it;deallocate()forgets it on every connection, each deallocating it when it is next handed back. - Breaking:
MariaDbDatasource.normalizePortis replaced bynormalizeIntegerSettings, which folds every setting inintegerSettings—port,timeOutand the six pool settings — from a string to anint. - Breaking:
MariaDbDatasource.connectionis gone. The data source holds a pool:connect()opens it and its first connection,connectedanswers whether it is open with at least one established connection, andisOperational()whether it can reach its database. A transaction gets its own connection, pinned from its first statement toendTransaction; a second start of the same transaction is refused witherror.transaction.already_active, another transaction is not. XA transactions get per-transaction branch ids, so concurrent ones are separate branches.
Tests #
test/connection_pool_test.dartcoversTomConnectionPoolover a fake factory and an injectable clock — lending, waiting in order, the typed timeout, retirement and closing.datasources_test.dartruns concurrent transactions over stand-in connections, anddatasources_db_test.dartcommits one transaction while another sleeps on a live server.- Every error key the package raises is named by a test. New
*_error_keys_test.dartfiles for object persistence, data sources, database migration, audit, endpoints, export and messaging pin over 150 keys, each asserting the exact key; none needs a database. test/ai_citation_test.dartfails the suite when an artefact of the package cites a document in the AI-state mount. It runstom_ai/core/tool/check_doc_ai_citations.pyover the package throughtom_ai/core/tool/package_gate.dart; the rule and its one exemption (the runtime_ai/trail/directory) are stated in the checker alone.test/formatting_test.dartdelegates to the shared guard. The walk, the generated-file exclusion, the coverage floor and the choice of formatter moved totom_ai/core/tool/package_format_guard.dart, which every package undertom_ai/coreshares; the file keeps only this package's floor, landmarks and generated-output flag.
Added #
-
Push notifications:
TomPushTransportwith FCM, APNs and a described protocol.TomMessageChannel.pushnow has a transport. ATomPushProtocolmaps a notification to a service's request and its answer back:TomFcmPushProtocol(Firebase Cloud Messaging, HTTP v1),TomApnsPushProtocol(Apple's push service, over HTTP/2) andTomFieldMappedPushProtocolfor any other JSON push API, described by field paths asTomFieldMappedSmsProtocoldescribes an SMS gateway. Credentials are minted and refreshed byTomFcmServiceAccountCredential(an RS256 service-account assertion exchanged for an access token) andTomApnsProviderToken(an ES256 provider token); a credential the service rejects is refreshed and the token tried once more. The wire is aTomPushExchange, HTTP/2 over the newhttp2dependency for APNs. Both vendor protocols are tested against scripted answers in the vendors' documented formats, not against the live services. -
TomMessage.push,TomPushRequestandTomPushService. The factory puts the title insubjectand the data map inmetadata; the service is the thirdTomTemplatedMessageService, with its own requiredtitleandbodysub-keys. -
A server warns about its configuration at startup. One warning lists every key in the configuration resources that names no field of the configuration class — where a misspelled setting would otherwise be dropped in silence — and one lists every
@TomRecommendedFieldsetting no source supplied. Only the resources are checked for surplus keys; the environment,.envand command line keep their tolerance.settingWarningsis the check on its own. -
TomPageRequest.stableUnder(key)returns the request with [key] appended as its last ascending sort term when it cuts a window and does not already sort on the key. It is the rule the repository applies to every paged read, moved onto the page request so it can be tested without a database. -
A server that boots with resource-key grants unenforced says so.
tom.protection.resourceKeysstays off by default — enforcing it refuses every endpoint that declares no access in an application without grants — sostart()now warns throughTomServer.warnOnUnenforcedResourceKeyswhen it is off outside a development or test environment, naming the setting. Nothing refuses to start. -
isDevelopmentOrTestRuntime(), the check JWT signing already made privately, is now a public function inconfiguration/runtime_environment.dartshared by both.
Changed #
-
A health analysis is never started twice. A timed-out analysis cannot be cancelled, so
TomHealthServer.verdictOfkeeps one analysis in flight per analyzer: a probe arriving while it is pending waits on it under its own deadline. An analyzer that hangs on every probe now leaves one pending future behind instead of one per probe. -
A typed
TomConfigreads the text of its type. A YAML${VAR:default}expansion is written back into the tree as text, soport: ${DB_PORT:3306}threw forTomIntConfigwhileport: 3306read. After a failed cast, aStringvalue is now read by theTomArgs.convertToObjectMaptable the environment,.envand command-line sources already use:'3306'resolves for anint,'2.5'for adouble,'true'for abool. The map keeps the text, and aTomStringConfigreads it unchanged; text of the wrong type is stillconfiguration.tom_config.property_type_mismatch. -
TomArgstrims a key and a value asTomEnvFiledoes, sok = visk→vfrom either source, quoted whitespace is kept and a whitespace-only key is skipped. -
TomDbColumntakes no type parameters, and one resolver answers what a column is stored as. An annotation reaches the persistence layer through reflection, which reconstructs it without type arguments, soTomDbColumn<DART_TYPE, COL_TYPE>'s parameters were never seen at run time —declaredDartType()anddeclaredColumnType()always fell back to the fields. Both methods and both parameters are gone;columnType:anddartType:are the declarations, and the converters are typedObject? Function(TomDbType)/TomDbType Function(dynamic). Annotations whose box said more than the field type (an enum orDurationstored as text) now saycolumnType: TomDbString.TomAnnotationAnalyzer.familyOfis the single resolver — one table for boxes, Dart types and observableTom…wrappers, explicit type first, an explicit type with no familyunknownrather than the field's type — and the query compiler, the schema-convergence check, the export projection and the MariaDB datasource read it (newTomAnnotationAnalyzer.dartTypeOf).columnType: boolon anintfield was boolean to the compiler and integer to the convergence check; it is boolean to both. -
The API summaries moved from
doc/api_reference/todoc/api/, where every other package and the workspace guideline already kept them. The folder held only the markdown summaries and their index; the nameapi_referencenow means only the built PDF,doc/{package}_api_reference.pdf. -
TomLoginFailuremoved totom_core_kernel, besideTomAuthenticationResult.TomAuthenticationServernow sets the result'sloginFailureReasonwherever it setserror: from the caughtTomLoginException's reason,secondFactorInvalidfor a refused code it offers again, andproviderErrorfor a masked unexpected error. -
TomErrorHandler.handleUnknownResultTypeErroris called, and spelt right. Renamed fromhandleUnknowResultTypeError. The pipeline now calls it when rendering a result is refused withreflector.type.not_annotated(an unannotated result type, or a type nested in one); every other rendering failure still goes tohandleConversionError. The log line names the hook, and a failing hook still raisesconversion_error_handler_failed. -
TomDataSource.isOperationalis abstract. It defaulted totrue, so a driver that never implemented it reported usable forever throughTomDataSourceInfo.isOperational. Every instance member of the data-source contract is now abstract, asTomPreparedStatement's already were, and a source-reading test holds both contracts to it. -
Unreachable raises removed, with their documented entries:
annotation_analyzer.analyse_persistent_type.columns_not_found,query_sentence_compiler.analyse_order_by.failed,query_sentence_compiler.build_expression.negation_of_non_bool,query_sentence_compiler.create_is_in.failed(now aStateErrornaming the broken invariant),endpoint_pipeline.get_class_method_metadata.multiple_api_endpoint_implementation_tags,error.failed-to-connect.connect_failed,error.failed-to-connect.remained_unconnectedandtom_datasource_info.initialize.no_datasource_obtained_for_type.TomDataSource.createDatasourceOfreturns a non-nullableTomDataSource. -
TomEndpointRouting.scanClassestakes an optional class set, defaulting to every annotated class, so the partition can be checked against classes no reflection root could carry. -
Registrations use
TomBean.withType<X>(bean), following the kernel's move of the key into a type argument. The endpoint pipeline's default component, whose type is read off a mirror, registers throughTomBean.withRuntimeType. Reflection artifacts regenerated. -
TomBaseServerConfiguration.listenToAnyIPis renamedlistenToAnyIp. It was the one field whose name did not round-trip through theSCREAMING_SNAKE_CASEenvironment convention, soLISTEN_TO_ANY_IPbound nothing. A configuration file that sets the keylistenToAnyIPverbatim must writelistenToAnyIp; the old key is now unknown and is traced, not applied. -
The mutation catalogue covers all twenty modules.
tool/mutations.yamlnow recordsconfiguration,authentication,object_persistenceand the rest ofdatasourcesandendpoints. Every entry is killed by the tests it names. -
tool/mutation_sweep.pyreports a mutant that does not compile as INVALID, not as killed. Such a mutant fails every test that imports it and so proves nothing; it counts toward the exit code like a survivor, since its entry has to be rewritten before its behaviour counts as pinned. A run that hangs is stopped after ten minutes, with the test VMs it spawned, and counted as killed.
Fixed #
-
Reading
TomDataSourceInfo.datasourcebeforeinitializeraises its key. The field waslateas well as nullable, so the read threw aLateInitializationErrorbefore the null check andisOperationalthrew too; it now raisestom_datasource_info.get_datasource.datasource_is_nullandisOperationalanswersfalse. -
A repository read an instant up to 0.9 s late whenever its fraction was under a tenth of a second. Rows from a prepared statement — every repository read, and
save's re-read — arrive over MySQL's binary protocol, wheremysql_clientrenders aDATETIME's microseconds as a bare integer: 91 371 µs came back as.91371, read as 0.91371 s. One stored instant in ten was affected, silently; it surfaced as an intermittent failure of the preferences "a replace moves the timestamp forward" test. The data source now marks binary-protocol rows and restores the six-digit fraction before the temporal readers parse them; text-protocol rows, whose fraction the server formats at the column's own precision, are left alone. -
A
@TomTransactionalendpoint's transaction now holds its writes. Nothing in the framework ever calledregisterParticipant, so the transaction the pipeline opened, committed and rolled back was empty: every statement autocommitted, and an endpoint that threw half way had its rollback called with nothing to undo.MariaDbDatasourcenow enlists itself at the point of use — the first statement it runs inside a running transaction joins it — and a data source that cannot start its transaction fails that statement. Because a transaction belongs to the one connection, the transaction holds the data source until the coordinator ends it: a statement from elsewhere waits for the release instead of running inside it, so two concurrent transactions on one data source take turns rather than the second being refused. The transaction's own verbs bypass that admission.test/transactional_endpoint_db_test.dartdrives it through the router, a repository and MariaDB. -
A commit that does not commit fails the request. The pipeline ignored
commitTransaction()'s result, so a vetoed or partial commit answered 200 with the method's result. It now raisesTomTransactionException(endpoint_pipeline.handle_method_call.commit_failed), and the error path rolls back only a transaction that is still running. -
A stored date or time the driver cannot read is refused by column. A
DATEcolumn holding a timestamp, aTIMEholding an interval or an unreadableDATETIMEescaped row mapping as a bareFormatException— no key, noclientThrow, no column. Every row shape now reads them through_readTemporal, which raiseserror.result_processing.not_a_date,.not_a_timeor.not_an_instantnaming the column and the value. -
TomDeliveryReportEndpoint.installraises aTomMessagingExceptionfor an empty secret, not the kernel'sTomException, so it is in the server family like every other server raise. -
A raise site throws the family, or says why not.
doc/little_things.mdstates the rule and its three exempt categories (programmer error,parse contract,kernel protocol); the thirteen exempt sites carry a// throws-outside-family:marker, andserver_exception_family_test.dartenforces it package-wide.little_things.mdalso counted the directTomServerExceptionraises as five; there are six. -
MariaDbXADatasourcedesynchronised its own connection on every prepare.preparesentXA ENDandXA PREPAREas one command, andmysql_clientcompletes a call on the first reply it reads without checking whether more follow — so the PREPARE's reply stayed on the wire, and every later call on that connection was answered by its predecessor's reply.XA COMMITreturned before the server had committed (a reader on another connection saw the commit only when it lost the race, which is why the DB-backed run failed about one time in four), a failedXA PREPAREwas never reported, and a query on the XA connection came back empty.prepareandrollbacknow issue one statement per call;rollbacktracks whether the branch has been ended, so a prepare refused after itsXA ENDrolls back without a second, refusedXA END. -
executeQueryandexecuteCommandrefuse a multi-statement command witherror.datasource.multiple_statements, before the connection is touched, so the shape above cannot recur elsewhere. The check is quote- and comment-aware (TomSqlLexer.splitStatements). -
The XA branch id is bounded to the 64-byte
gtridlimit. It was built from the database name, host, port and a timestamp, and fitted only for short coordinates — the test database on127.0.0.1came to 62 bytes — so a real hostname or a longer database name made every XA transaction fail to start. It is now<database>@<host>:<port>_<process>_<time>_<counter>, with the unique tail kept whole and the readable head truncated, and a random per-process tag so two processes on one database cannot mint the same id.MariaDbXADatasource.maxBranchIdBytesnames the limit. -
The DB-backed suites no longer share one schema. The test fixture gives every suite file a database of its own, created when it registers its data source and dropped when the group ends, so no suite — and no second run against the same server — can truncate or drop another's tables.
-
dart_test.yamlstates the real reason the suite runs serially. It said the DB suites shared one schema; the reason that remains is thatconfiguration_testandruntime_environment_testchange the process-wide working directory, which every concurrently running suite file would inherit. -
A broken markdown reference link in
doc/authentication.md. The page links type names to in-document anchors throughout;TomAuthenticationServicecarried the brackets with no target and has no anchor to point at, so the brackets are gone.
Earlier entries #
The full history is in the repository's CHANGELOG.md; this file holds the newest entries that fit pub.dev's size limit.