Repository navigation
fix(driver-sql): write an undeclared builtin audit timestamp in its column datetime form (MySQL: sys_jwks never stored, /auth/jwks 500) - #21272
Conversation
…olumn's datetime form The engine's audit hook stamps created_at/updated_at as toISOString() text on every insert, and initObjects provisions both columns on every managed table, but formatInput only rewrote the value into the column's storage form for a DECLARED Field.datetime. An object the registry injects no audit fields into (managedBy better-auth, systemFields false) bound the raw ISO text, which MySQL refuses: sys_jwks never stored a signing key and sys_member never stored a membership. Claude-Session: https://claude.ai/code/session_017xfMoEjKUuSh2xYB8sCozp Co-authored-by: Claude <noreply@anthropic.com>
…d changeset Claude-Session: https://claude.ai/code/session_017xfMoEjKUuSh2xYB8sCozp Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 9 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 2 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 11 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin cc9a4e42a429f56062a7e53673112b431eef72f2 && git checkout cc9a4e42a429f56062a7e53673112b431eef72f2
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin f148852752a2544bb25674cbc3385e279896e6c2 e7586a892e7b02bef5d0635f616d15070c45e8a7 && git checkout -B drift-repro f148852752a2544bb25674cbc3385e279896e6c2 && git merge --no-ff e7586a892e7b02bef5d0635f616d15070c45e8a7
node scripts/docs-audit/affected-docs.mjs --json f148852752a2544bb25674cbc3385e279896e6c2
|
Contract reviewServed-tier: Inputs: card #21259 (body, claim 5942590152, os-dev-report 5943202786), PR #21272 (body, 3-file list, three-dot diff against merge base ① Derived judgments
② Semver level
③ Boundary flags
Record written at 2026-10-02T00:34Z. Implemented-by: VERDICT: PASS Generated by Claude Code |
Fixes #21259
Clause-②: no
What was wrong
initObjectscreatescreated_atandupdated_aton every managed table (createAuditTimestampColumn,DATETIME(3)on MySQL). The engine'ssys_stamp_audit_inserthook stamps both on every insert asnew Date().toISOString()text.formatInputrewrote that text into the MySQL literal only for a column the object DECLARED asField.datetime. The registry declares both columns on most objects, but injects nothing formanagedBy: 'better-auth'orsystemFields: false. On those objects an undeclared audit column reached MySQL as2026-10-01T23:35:47.598Z, and MySQL refused it (ER_TRUNCATED_WRONG_VALUE).Producer, measured
8dea55d3shows everysys_jwksINSERT boundcreated_atas2026-10-01 23:35:47.598andupdated_atas2026-10-01T23:35:47.598Z: one instant in two spellings. Only the non-key columns were extracted; key material was not read.updated_at. I ran a scratch vitest (never committed):ObjectQLPlugin, a capturing driver, and the realSysJwksfromplatform-objects.id, public_key, private_key, alg, crv, created_at.driver.createreceived the same keys plusupdated_at, astringintoISOString()form, equal to thecreated_atthe hook re-stamped.id, public_key, private_key, alg, crv, created_at, expires_at.updatedAt(better-auth/dist/plugins/jwt/schema.mjs). The producer is the engine's audit hook (packages/objectql/src/plugin.ts,applyToRecord), not the auth adapter. The value is a correct instant in the platform's canonical form. What was missing is the driver formatting the column it provisioned. So the fix is indriver-sql, andplugin-authis untouched.The class, counted
The objects that boot registers:
GET /api/v1/meta/objecton the CRM boot at8dea55d3, MySQL, 81 objects.created_atandupdated_atcreated_atonlysys_member,sys_invitation,sys_team_member,sys_oauth_access_token,sys_oauth_refresh_token,sys_oauth_client_resource,sys_jwks,sys_scim_connection_binding,sys_scim_group_memberupdated_atonlysys_oauth_client_assertion,sys_scim_identity_tombstoneEvery declared audit column is
datetime. In the baseline boot, two of the eleven objects failed at a door:sys_jwks5 times andsys_member2 times. The seeded admin had no organization membership on MySQL. Declaringupdated_atonsys_jwksalone would have closed 1 of the 11.The fix
The fix is in
packages/drivers/driver-sql/src/sql-driver.ts:registerManagedObjectMetadatarecords the builtin audit columns a managed object does not declare (undeclaredAuditTimestampFields). This runs at every registration, so a later declaration clears the entry. External objects are excluded: their remote columns are not this driver's.formatInputwrites those columns throughstorageDatetimeValue, the same rule a declaredField.datetimetakes. Every write door goes through it:create,bulkCreate,upsert,updateandupdateMany.Datewritten to an undeclared audit column is now stored as canonical ISO text, the same as for a declaredField.datetime. Before, it was stored as an epoch INTEGER and read back as a number. A column the object declares keeps its declared type.nowColumnDefaultandmigrateMysqlDatetimeColumnsare not touched.Pins
packages/drivers/driver-sql/src/sql-driver-21259-undeclared-audit-timestamp-write.test.tsdeclares one cell per dialect throughdeclareDialectCell: SQLite always, live PostgreSQL and MySQL where provisioned. CI'sTemporal Conformance (live PG + MySQL)runs it. Each cell:created_atonly, and neither);create,bulkCreate,upsert,updateandupdateMany(the last two underpreserveAudit) and reads back the same instant;Datecase.The door pin follows the precedent of the seat's answer on #21227 (5940180378, option B): a driver pin of record plus the one-off door measurement below. No live-dialect harness boots
plugin-auth.Door measurement
All runs used
pnpm dev:crm -- --freshand the seeded admin's session. The MySQL server was 8.0.46 with zone+08:00; the PostgreSQL server was 16.14 with zoneAsia/Shanghai.8dea55d3)d18427f4, driver-sql rebuilt)GET /api/v1/auth/jwksGET /api/v1/auth/tokenGET /api/v1/auth/get-sessionset-auth-jwtheaderset-auth-jwtpresentsys_jwks/sys_memberrowscreated_at=updated_at, UTC wall clock)Insert operation failedWARN lines namingIncorrect datetime valuesys_jwks, 2sys_member)The PostgreSQL and SQLite doors were not measured before the fix (
NOT MEASURED: door-before on pg/sqlite). Their control of record is the driver pin's PostgreSQL and SQLite cells, which were green before the fix as well as after it.Reverse verification
updateManycase was added. 6 failed: 5 on live MySQL, eachIncorrect datetime value: '2026-10-01T21:49:27.479Z', and the SQLiteDatecase, which received1790891367479. Live PostgreSQL had 0 failures.e7586a892e.node scripts/ablation-replace.mjsreplaced the arm's registry read withundefined. The anchor hit went 1 to 0 and the blob changedd08cb7e95a4ato5e6e99e8b352. Result: 7 failed and 17 passed. The failures were 6 on live MySQL (create ×2, bulkCreate, upsert, update, updateMany) and 1 on SQLite (theDatecase). Live PostgreSQL had 0, as predicted.git diff HEADand a cleangit status. The pin imports the driver from source (./index.js), so nodist/leg applies.Local verification (HEAD
e7586a892e)e7586a892e: 8 cases × 3 cells, live PostgreSQL 16.14 and MySQL 8.0.46,TZ=America/New_York,OS_EXPECT_LIVE_DIALECT_MATRIX=1.@objectstack/driver-sql, whole suite, same environment. Run atd18427f4. Since thene7586a892eadds only a test case and the changeset, andsql-driver.tsis unchanged. Result: 224 files, 5390 passed, 1 skipped. The reporter printed "all 3 dialects were exercised".@objectstack/driver-sqlite-wasm: 36 files, 675 passed.@objectstack/driver-turso: 87 files, 2349 passed, 33 skipped.typecheckexit 0 fordriver-sql,driver-sqlite-wasmanddriver-turso.driver-sql's program includes the new test file (tsc --listFiles).pnpm check:driver-conformanceexited 0 before the first edit and after the last commit. The gate list isnode scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands, run ate7586a892e; per-gate results are in the dev report on On MySQL, the JWT signing-key row is never written (Incorrect datetime value for sys_jwks.updated_at), so /auth/jwks and /auth/token answer 500 and no OIDC/MCP token can be issued #21259.Acceptance notes
$gtand equality against…Ztext. Both answered the right rows, so the read side needs no change.sql-driver-11176-bulk-and-merge-updated-at.test.ts§2 routes around this class in its fixture: it sendstoISOString()on SQLite and aDateelsewhere. Left untouched.datetimepath already took on every dialect.sys_activitytable is missing on MySQL in this boot; driver-sql on MySQL: a declared datetime field defaulting to NOW() gets a precision-less CURRENT_TIMESTAMP default on its datetime(3) column, so MySQL 8.0 refuses the table: sys_activity is never created and its data door answers 500 #21241 is not addressed here.[Better Auth]ERROR log line printed the failing INSERT with its bound values, while the engine's WARN line for the same failure printed[statement and bound values redacted]. Details are in the dev report.Generated by Claude Code