Repository navigation
Commit 95b91cc
fix(driver-sql): write an undeclared builtin audit timestamp in its column datetime form (MySQL: sys_jwks never stored, /auth/jwks 500) (#21272)
Fixes #21259
Clause-②: no
## What was wrong
`initObjects` creates `created_at` and `updated_at` on every managed
table (`createAuditTimestampColumn`, `DATETIME(3)` on MySQL). The
engine's `sys_stamp_audit_insert` hook stamps both on every insert as
`new Date().toISOString()` text. `formatInput` rewrote that text into
the MySQL literal only for a column the object DECLARED as
`Field.datetime`. The registry declares both columns on most objects,
but injects nothing for `managedBy: 'better-auth'` or `systemFields:
false`. On those objects an undeclared audit column reached MySQL as
`2026-10-01T23:35:47.598Z`, and MySQL refused it
(`ER_TRUNCATED_WRONG_VALUE`).
## Producer, measured
- **Bound values.** The MySQL general log at `8dea55d3` shows every
`sys_jwks` INSERT bound `created_at` as `2026-10-01 23:35:47.598` and
`updated_at` as `2026-10-01T23:35:47.598Z`: one instant in two
spellings. Only the non-key columns were extracted; key material was not
read.
- **Who writes `updated_at`.** I ran a scratch vitest (never committed):
`ObjectQLPlugin`, a capturing driver, and the real `SysJwks` from
`platform-objects`.
- The caller payload (the better-auth jwt plugin shape) has the keys
`id, public_key, private_key, alg, crv, created_at`.
- `driver.create` received the same keys **plus `updated_at`**, a
`string` in `toISOString()` form, equal to the `created_at` the hook
re-stamped.
- The registered object's fields are `id, public_key, private_key, alg,
crv, created_at, expires_at`.
- **Where that leaves the producer.** better-auth's jwks schema has no
`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 in `driver-sql`, and
`plugin-auth` is untouched.
## The class, counted
The objects that boot registers: `GET /api/v1/meta/object` on the CRM
boot at `8dea55d3`, MySQL, 81 objects.
| declares | objects |
|:--|--:|
| `created_at` and `updated_at` | 70 |
| `created_at` only | 9: `sys_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_member` |
| `updated_at` only | 0 |
| neither | 2: `sys_oauth_client_assertion`,
`sys_scim_identity_tombstone` |
Every declared audit column is `datetime`. In the baseline boot, two of
the eleven objects failed at a door: `sys_jwks` 5 times and `sys_member`
2 times. The seeded admin had no organization membership on MySQL.
Declaring `updated_at` on `sys_jwks` alone would have closed 1 of the
11.
## The fix
The fix is in `packages/drivers/driver-sql/src/sql-driver.ts`:
- `registerManagedObjectMetadata` records 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.
- `formatInput` writes those columns through `storageDatetimeValue`, the
same rule a declared `Field.datetime` takes. Every write door goes
through it: `create`, `bulkCreate`, `upsert`, `update` and `updateMany`.
- On SQLite and PostgreSQL the hook's text is already canonical and is
bound unchanged, byte for byte.
- One input shape changes on SQLite: a JS `Date` written to an
undeclared audit column is now stored as canonical ISO text, the same as
for a declared `Field.datetime`. Before, it was stored as an epoch
INTEGER and read back as a number. A column the object declares keeps
its declared type.
- `nowColumnDefault` and `migrateMysqlDatetimeColumns` are not touched.
## Pins
`packages/drivers/driver-sql/src/sql-driver-21259-undeclared-audit-timestamp-write.test.ts`
declares one cell per dialect through `declareDialectCell`: SQLite
always, live PostgreSQL and MySQL where provisioned. CI's `Temporal
Conformance (live PG + MySQL)` runs it. Each cell:
- covers the two shapes the census found (`created_at` only, and
neither);
- writes the hook's exact value through `create`, `bulkCreate`,
`upsert`, `update` and `updateMany` (the last two under `preserveAudit`)
and reads back the same instant;
- asserts the value the driver BOUND: the MySQL literal on MySQL, and
the hook's text unchanged on SQLite and PostgreSQL (the control);
- includes a `Date` case.
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 -- --fresh` and the seeded admin's session.
The MySQL server was 8.0.46 with zone `+08:00`; the PostgreSQL server
was 16.14 with zone `Asia/Shanghai`.
| door | MySQL, before (`8dea55d3`) | MySQL, after (`d18427f4`,
driver-sql rebuilt) | PostgreSQL, after | SQLite, after |
|:--|:--|:--|:--|:--|
| `GET /api/v1/auth/jwks` | 500 | 200: one public key (OKP, Ed25519,
EdDSA), no private member | 200 | 200 |
| `GET /api/v1/auth/token` | 500 | 200: a three-segment JWT, header alg
EdDSA with a kid | 200 | 200 |
| `GET /api/v1/auth/get-session` | 200, **no** `set-auth-jwt` header |
200, `set-auth-jwt` present | 200, present | 200, present |
| `sys_jwks` / `sys_member` rows | 0 / 0 | 1 / 1 (`created_at` =
`updated_at`, UTC wall clock) | 1 / 1 | not read |
| `Insert operation failed` WARN lines naming `Incorrect datetime value`
| 7 (5 `sys_jwks`, 2 `sys_member`) | 0 | 0 failed inserts | 0 failed
inserts |
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
- **Before the fix.** The pin ran against the unfixed driver with 21
cases, before the `updateMany` case was added. 6 failed: 5 on live
MySQL, each `Incorrect datetime value: '2026-10-01T21:49:27.479Z'`, and
the SQLite `Date` case, which received `1790891367479`. Live PostgreSQL
had 0 failures.
- **Ablation at `e7586a892e`.** `node scripts/ablation-replace.mjs`
replaced the arm's registry read with `undefined`. The anchor hit went 1
to 0 and the blob changed `d08cb7e95a4a` to `5e6e99e8b352`. Result: 7
failed and 17 passed. The failures were 6 on live MySQL (create ×2,
bulkCreate, upsert, update, updateMany) and 1 on SQLite (the `Date`
case). Live PostgreSQL had 0, as predicted.
- **Restore.** The restore was proven by blob identity with HEAD, an
empty `git diff HEAD` and a clean `git status`. The pin imports the
driver from source (`./index.js`), so no `dist/` leg applies.
## Local verification (HEAD `e7586a892e`)
- **The pin.** 24/24 at `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 at
`d18427f4`. Since then `e7586a892e` adds only a test case and the
changeset, and `sql-driver.ts` is unchanged. Result: 224 files, 5390
passed, 1 skipped. The reporter printed "all 3 dialects were exercised".
- **Inheritors.** `@objectstack/driver-sqlite-wasm`: 36 files, 675
passed. `@objectstack/driver-turso`: 87 files, 2349 passed, 33 skipped.
- **Typecheck.** `typecheck` exit 0 for `driver-sql`,
`driver-sqlite-wasm` and `driver-turso`. `driver-sql`'s program includes
the new test file (`tsc --listFiles`).
- **Gates.** `pnpm check:driver-conformance` exited 0 before the first
edit and after the last commit. The gate list is `node
scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack
--commands`, run at `e7586a892e`; per-gate results are in the dev report
on #21259.
## Acceptance notes
- **Filters.** A filter comparand on an undeclared audit column was
measured on live MySQL, with `$gt` and equality against `…Z` text. Both
answered the right rows, so the read side needs no change.
- **An existing fixture.**
`sql-driver-11176-bulk-and-merge-updated-at.test.ts` §2 routes around
this class in its fixture: it sends `toISOString()` on SQLite and a
`Date` elsewhere. Left untouched.
- **Clause-②.** No authorable or API accept set moves, and the audit
columns are readonly to clients. The write door stops producing a
literal MySQL refuses, for a value the declared-`datetime` path already
took on every dialect.
- **Out of scope.** The `sys_activity` table is missing on MySQL in this
boot; #21241 is not addressed here.
- **A log finding, reported for the seat to file.** On MySQL, the
`[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](https://claude.ai/code/session_017xfMoEjKUuSh2xYB8sCozp)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 9dae475 commit 95b91cc
3 files changed
Lines changed: 262 additions & 0 deletions
File tree
- .changeset
- packages/drivers/driver-sql/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
Lines changed: 209 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
5495 | 5495 | | |
5496 | 5496 | | |
5497 | 5497 | | |
| 5498 | + | |
| 5499 | + | |
| 5500 | + | |
| 5501 | + | |
| 5502 | + | |
| 5503 | + | |
| 5504 | + | |
| 5505 | + | |
| 5506 | + | |
| 5507 | + | |
| 5508 | + | |
| 5509 | + | |
| 5510 | + | |
| 5511 | + | |
| 5512 | + | |
5498 | 5513 | | |
5499 | 5514 | | |
5500 | 5515 | | |
| |||
12050 | 12065 | | |
12051 | 12066 | | |
12052 | 12067 | | |
| 12068 | + | |
| 12069 | + | |
| 12070 | + | |
| 12071 | + | |
| 12072 | + | |
12053 | 12073 | | |
12054 | 12074 | | |
12055 | 12075 | | |
| |||
20024 | 20044 | | |
20025 | 20045 | | |
20026 | 20046 | | |
| 20047 | + | |
| 20048 | + | |
| 20049 | + | |
| 20050 | + | |
| 20051 | + | |
| 20052 | + | |
| 20053 | + | |
| 20054 | + | |
| 20055 | + | |
| 20056 | + | |
| 20057 | + | |
| 20058 | + | |
| 20059 | + | |
| 20060 | + | |
| 20061 | + | |
| 20062 | + | |
| 20063 | + | |
| 20064 | + | |
| 20065 | + | |
| 20066 | + | |
| 20067 | + | |
| 20068 | + | |
20027 | 20069 | | |
20028 | 20070 | | |
20029 | 20071 | | |
| |||
0 commit comments