Repository navigation
fix(plugin-auth): a fresh os dev database opens on the shell — the dev seed gives the admin's workspace the host's time zone (#23175) - #23237
Conversation
… time zone WIP: implementation; tests follow. Claude-Session: https://claude.ai/code/session_01FzQy26KznSa5j2gnf3AzsE Co-authored-by: Claude <noreply@anthropic.com>
… dialog's own condition Claude-Session: https://claude.ai/code/session_01FzQy26KznSa5j2gnf3AzsE Co-authored-by: Claude <noreply@anthropic.com>
… zone Claude-Session: https://claude.ai/code/session_01FzQy26KznSa5j2gnf3AzsE Co-authored-by: Claude <noreply@anthropic.com>
…ons type it Claude-Session: https://claude.ai/code/session_01FzQy26KznSa5j2gnf3AzsE Co-authored-by: Claude <noreply@anthropic.com>
…v-seed-workspace-timezone
… it was measured under Claude-Session: https://claude.ai/code/session_01FzQy26KznSa5j2gnf3AzsE Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 8 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 4 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 15 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 0a2d326dc4e10a94575f474e9144da676d768ce8 && git checkout 0a2d326dc4e10a94575f474e9144da676d768ce8
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 01a3421f42b15801dd6289f9a5fdf22fa469b8f2 68696c376b842b7f58d065eb9f75ae0ba4d9623f && git checkout -B drift-repro 01a3421f42b15801dd6289f9a5fdf22fa469b8f2 && git merge --no-ff 68696c376b842b7f58d065eb9f75ae0ba4d9623f
node scripts/docs-audit/affected-docs.mjs --json 01a3421f42b15801dd6289f9a5fdf22fa469b8f2
|
Fixes #23175
Clause-②: no — the dev seed completes the workspace it creates with the host's zone; no published schema, option, flag or export moves, and the prompt's own rule is unchanged.
What changes
On the boot that seeds the dev admin (
NODE_ENV=development,OS_SEED_ADMINon by default),plugin-auth's seed now writes the host's resolved zone tolocalization.timezonefor each organization the seeded admin is a member of. A freshos devdatabase then opens on the console shell, not on the "Set the workspace timezone" dialog.Intl.DateTimeFormat().resolvedOptions().timeZone, soTZchooses it. No new CLI flag, and no new env var.set('localization', 'timezone', zone, { tenantId: orgId }). This is the same per-organizationtenantrow that the dialog's confirm writes throughPUT /api/settings/localization. The key'siana_time_zonedomain is judged there, and the write is audited there. It is not a raw row write.source: 'default'. A value on the organization, global or env rung is left untouched.File surface: the seed path in
packages/plugins/plugin-auth/src/auth-plugin.ts, one new test file, and one changeset. The import-deps region (about :2984),identity-objects-plugin.tsanddefault-organization-invariant.tsare untouched.Mechanism hypotheses, measured
auth-plugin.tsanddev.tsonly passes env vars, as stated. But under the defaultsingleposture the seed does not create the workspace.AuthPlugin.start()creates the Default Organization at boot (ADR-0131 D3). The seed'ssignUpEmailthen triggers the default-organization bootstrap, which binds the new admin as its owner inside the same call chain. So the admin'ssys_memberrow exists by the time the seed's next step runs. Pinned by case 1 below and seen live.localization.timezone, scopetenant, onesys_settingrow per organization. It is not thesys_organization.timezonecolumn atsys-organization.object.tsabout :284. That column's own comment says it is distinct from the settings door's key and is not read by it. The seed writes only the settings key..objectui-sha1fb1ced362.findTimezoneOfferinworkspaceTimezonePromptLogic.tsreadsGET /api/settings/localizationand offers the dialog only whenvalues.timezone.source === 'default', the key is not locked, and the session holds the manifest'swritePermission(setup.write). The request goes to the session's active organization (the dialog needsactiveOrganization.id). Every pin below assertssourcefor exactly that organization.UTCtoo.UTCis the host's resolved zone, and the console's creation path (console: send the creator's browser timezone when a workspace is created, so a new workspace never shows the timezone prompt (objectui half of cloud#2676) objectui#11908) sends whatever the browser reports,UTCincluded. It leaves the key out only when the browser reports nothing. Foros dev, the browser opening the console usually runs on the same host, so the dialog would pre-fill that same zone. "No zone set" is the case whereIntlreports none, or reports one the door refuses (ICU'sEtc/Unknown). Nothing is written then, and the dialog asks, as the ruling requires.os devsets neitherOS_TENANCY_POSTUREnorOS_MULTI_ORG_ENABLED, so it bootssingle(the boot banner readsTenancy: single). The write needs the organization id. A walled posture refuses an organization-lesstenantwrite, and the dialog reads the active organization's row. The seed reads the admin's memberships rather than guessing an organization. Walled postures were not booted here: NOT MEASURED. An admin with no membership gets nothing written.Pins:
src/dev-admin-seed-workspace-timezone.test.tsThese run on a real
ObjectKernel, ObjectQL on in-memory SQLite, the realAuthPlugin,SecurityPluginandSettingsServicePlugin, andNODE_ENV=development. The host zone is set withTZ, and each case first asserts that the process really resolves it.Asia/Shanghai: the seeded admin is in exactly the Default Organization, which resolves{ value: 'Asia/Shanghai', source: 'tenant' }.SettingsService.setwas called with('localization', 'timezone', 'Asia/Shanghai', { tenantId }).OS_SEED_ADMIN=0resolves{ value: 'UTC', source: 'default' }.TZ, whereIntlanswersundefined): the workspace stayssource: 'default', with no localization write.Etc/Unknown): the workspace stayssource: 'default'.tenantrow stored before the seed (Europe/Paris) is kept, with no localization write.Negative-pin ablation. The fix was committed first. The call
if (seededId) await this.seedDevWorkspaceTimezone(ctx, ql, seededId);was removed withscripts/ablation-replace.mjs(anchor x1 to x0, blob11ebf1845458to2568e9af9cfc), with atraprestore armed. Case 1 went red and the four controls stayed green (Tests 1 failed | 4 passed (5)):Restore: blob after restore
11ebf1845458equals the HEAD blob, andgit diff HEADis empty. The subject is imported from source, so this ablation needed no build.Live boot:
os dev --freshonexamples/app-crmThese are sign-in readings as
admin@objectos.ai, read off the same route the dialog reads:activeOrganizationIdGET /api/settings/localization→timezoneTZ=Asia/Shanghaiorg_mv6hnzlwfvausmzc{ value: 'Asia/Shanghai', source: 'tenant' }, so no dialogUTC)org_mv6hologvdf199rd{ value: 'UTC', source: 'tenant' }, so no dialogTZ=Asia/Shanghaiorg_mv6hpy140467la3x{ value: 'UTC', source: 'default' }, so the dialog opensHow the dist ablation was checked:
ablation-dist-preflight --absentproved the call was gone from all 10 built files.os devruns was emitted, and the preflight read that JS.check-dts-emitted2/2). The preflight then found the call back indist/index.jsanddist/index.mjs, and found the working tree clean.Tests and gates (head
68696c376b)pnpm --filter @objectstack/plugin-auth typecheck: exit 0. It coverstscfor src and examples, pluscheck:test-typecheckovertsconfig.test.json. The new test file adds no debt.pnpm --filter @objectstack/plugin-auth exec vitest run --maxWorkers=2:Test Files 137 passed (137),Tests 2785 passed | 10 skipped. That run was at9db639634d. The only later commit is the comment inseedDevWorkspaceTimezone's doc block. On68696c376bthe three seed files were re-run:Tests 22 passed (22).node scripts/pm/dispatch-gates.mjs --commandswas derived after the diff with no paths: 68 families. Reconciled with--ran, each line carrying its exit code: 68 run, 68 exit 0, 0 NOT MEASURED, at68696c376b.9db639634d,check:dual-build-cjs-loadsexited 3 (PREREQUISITE NOT MET: 9 packages had nodist/).eslint --no-inline-config --format jsonon the two changed.tsfiles reports 2 files, 0 errors and 0 warnings. Neither file is ignored.eslint.config.mjsnever enables type-aware linting (noparserOptions.project, about :327). So this diff cannot move any other file's verdict. The fullpnpm lintis CI's.Acceptance notes
@objectstack/verify's harness forcesNODE_ENV=developmentto get the seeded admin. AbootStack(crm)underTZ=Asia/Shanghainow reads{ value: 'Asia/Shanghai', source: 'tenant' }for that admin.UTChost (CI), the resolved value is unchanged.UTChost, harness boots now compute business dates in the host's zone, where they usedUTCbefore.TZ=Asia/Shanghai,analytics-rls.dogfoodandanalytics-timezone.dogfoodstayed green (8 tests).temporal-storage-e2e.dogfoodis NOT MEASURED: its showcase boot could not resolve the unbuilt@objectstack/connector-slackhere.TZ=UTC, or aPUT /api/settings/localization). Whether the harness should pin its own zone is raised with the PM, not decided here.os devhost, the host's zone wins and the dialog does not ask. Setup → Localization, orTZbeforeos dev, changes it.Generated by Claude Code