Repository navigation
feat(plugin-auth): a new organization takes its creator's default currency (#25495) - #25621
Conversation
…rency Claude-Session: https://claude.ai/code/session_01JVTd3PprStGa26aez5omLB Co-authored-by: Claude <noreply@anthropic.com>
…the real create route and the dev seed Claude-Session: https://claude.ai/code/session_01JVTd3PprStGa26aez5omLB Co-authored-by: Claude <noreply@anthropic.com>
…currency Claude-Session: https://claude.ai/code/session_01JVTd3PprStGa26aez5omLB Co-authored-by: Claude <noreply@anthropic.com>
…g-default-currency
📓 Docs Drift CheckThis PR changes 2 package(s): 39 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 13 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 25 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 1cd4f3a860f106a6e6b8d6943bf12dd43147c079 && git checkout 1cd4f3a860f106a6e6b8d6943bf12dd43147c079
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 7265b33e779e1d63afe50fb86e411beda7582838 40b9af0e491b4618d7c6b30f1ebe187781d69846 && git checkout -B drift-repro 7265b33e779e1d63afe50fb86e411beda7582838 && git merge --no-ff 40b9af0e491b4618d7c6b30f1ebe187781d69846
node scripts/docs-audit/affected-docs.mjs --json 7265b33e779e1d63afe50fb86e411beda7582838
|
…rough auth-manager's one reader Claude-Session: https://claude.ai/code/session_01JVTd3PprStGa26aez5omLB Co-authored-by: Claude <noreply@anthropic.com>
席位核对ACCEPT · Checklist conclusions (read on GitHub, not from the report):
Deviations accepted: the PR body left for the seat to edit (os-dev.md assigns later body edits to the seat; done in this act). Open, not blocking this PR:
Landing: Generated by Claude Code |
Fixes #25495
Clause-②: no
Metadata-protocol: none
What this does
A new organization now gets
localization.currencyfrom its creator's language or region. The maintainer's ruling, as the card quotes it: 「#8345:环境的默认币种,应该在objectstack 仓库吧」「同意乙」.sys_user.locale. If that is not set, the creating request'sAccept-Language, read with the platform's one parser (preferredLocaleFromHeader). This is the same orderGET /auth/me/localizationuses for the user's language, minus its third rung (the deployment default).Intl.Locale#maximize()gives it (zhmaps to CN,jato JP). That region's ISO 4217 currency comes from a checked-in CLDR 48currencyDatasnapshot (REGION_CURRENCY). The provenance block names the package, the file's sha256 and the selection rule.Intlhas no region-to-currency lookup and no installed dependency carries the table, so nothing was added to the dependency tree.es-419), or a malformed tag. There is no fallback todefault_country: 'US'orlocale: 'en-US'.source: 'default'). It is the settings service's ownset, onetenantrow, the same rowPUT /api/settings/localizationwrites. Setup → Localization shows it and can change it.LC_ALL, thenLC_MONETARY, thenLANG). It does not useIntl's default locale, because that answersen-USon a host with no locale set. This container is one:LANGunset, andIntl.DateTimeFormat().resolvedOptions().localeanswersen-US.currencykey now says where a new organization's value comes from.The seam, measured (dispatch assumptions A1–A3)
A1, holds.
AuthManagerOptions.onOrganizationCreatedexists, andafterCreateOrganizationcalls it with{ organizationId, userId, name, slug }. No production code supplied it. AuthPlugin now wraps it afterauth:configure, so a host's own callback still runs, and runs first. Which of the two sources the seam can see: both.userId, read fromsys_useras a system read.Accept-Language: better-auth runsafterCreateOrganizationinside its endpoint-contextAsyncLocalStorage, sotryGetCurrentAuthEndpointContext()carries the headers. The same accessorauth-manager-database-secret.tsalready uses.Case 1 below proves it end to end: no stored locale, only the header, and
CNYlands.A2, holds. It is an ordinary tenant-scoped
SettingsService.set. The tests readgetNamespace('localization', { tenantId }), which is what the Setup → Localization route returns. They see{ value: 'CNY', source: 'tenant' }.A3, NOT MEASURED.
objectstack-ai/cloudis not reachable from this session:add_reporefused it with "you don't have access". In-repo evidence:onOrganizationCreated's docblock says the cloud control plane supplies it to provision an organization's born-with environment. So cloud's organization creation goes through better-authcreateOrganization, and the host callback is preserved by the wrapping. Whether a cloud environment runtime's first organization is created through this seam, and not by provisioning, is still unknown. Triage owns that read.The
organizationplugin factory was not edited. PR #25594 landed onmainduring this run. This branch merged it, andauth-manager.tsandauth-manager-organization-plugin.tsare untouched by this diff.Tests
All results are at head
8bce0422cd; the reader consolidation40b9af0e49re-ran the package suite (3667 passed | 10 skipped) and the four targeted files (115/115). They follow the build closurepnpm turbo run build --filter='@objectstack/plugin-auth...'(29/29 tasks).organization-created-default-currency.test.ts, 9 cases. It boots a realObjectKernelwith ObjectQL on SQLite and the realAuthPlugin,SecurityPluginandSettingsServicePlugin. Organizations are created through the realPOST /organization/createunder thegroupposture, with a fixtureorg-scopingprovider.CNY(source: 'tenant').ja-JPoutranks a zh-CN header and givesJPY.OS_LOCALIZATION_CURRENCY=EURis kept, with no write.GBPis kept.LANG=zh_CN.UTF-8givesCNY.LANG=C.UTF-8stores nothing.organization-default-currency.test.ts, 32 cases. The table is all in theiso_4217_currencydomain. It also covers the mapping, the POSIX env reading, the never-overwrite rule, creator-locale precedence and wrapping order.dev-admin-seed-workspace-timezone.test.ts:LC_ALL/LC_MONETARY/LANGadded to its cleared env, so its "no localization write" cases cannot pick up the new currency write from the machine running them.pnpm --filter @objectstack/plugin-auth test: Test Files 193 passed (193), Tests 3667 passed and 10 skipped (3677).pnpm --filter @objectstack/plugin-auth typecheck: exit 0 (check:test-typecheck: OK).scripts/ablation-replace.mjswith a trap restore. The tests importAuthPluginfrom source, so no rebuild is involved.Accept-Languageis never read (anchor x1 to x0, blob2033a70ato56a7862a). Case 1 goes red: 1 failed, 8 passed.source !== 'default'guard is removed (blob to6fd0c02e). Cases 5, 6 and 9 go red: 3 failed, 6 passed.2033a70a, andgit diff HEADis empty after each leg.Gates
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackat40b9af0e49(and earlier at8bce0422cd) derived 74 commands. All 74 ran with exit 0. The--ranreconciliation answers: "74 derived famil(ies) accounted for — 74 run, 0 NOT-MEASURED (a DERIVED zero — all 74 recorded an exit code and none of them is 3)".eslint --no-inline-configon the six touched files: exit 0.Console check
Measured (the dev's addendum on #25495, comment 6383481471; recorded here by the seat). Full workspace build (
pnpm build, 76/76), and the console at the pinned objectui SHA2958f17e9c6a(pnpm objectui:build). A fresh showcase stack:OS_TENANCY_POSTURE=group OS_PLATFORM_OWNER_EMAIL=admin@objectos.ai OS_AUTH_MEMBERSHIP_POLICY=invite-only pnpm dev -- --fresh -p 38425, with the hostLANGunset. Playwright Chromium, localezh-CN, signed in as the dev admin:POST /api/v1/auth/organization/createwithAccept-Language: zh-CN,zh;q=0.9created 上海分公司.GET /api/settings/localizationthere answerscurrency {value: CNY, source: tenant}; the Default Organization (no creator) answers{value: null, source: default}./_console/apps/com.example.showcase/showcase_product: a new product'sunit_price(dynamic mode, no code) renders¥1,234.50, and every seeded price renders with¥. The same list in the Default Organization renders plain29.99,99.00…./_console/apps/setup/system/settings/localization) shows 默认货币 = CNY with the 租户 (tenant) source badge.Acceptance notes
singleposture's Default Organization gets no currency. It is created at boot byensureDefaultOrganizationExists, which has no creator. The first admin is bound to it later and never passes throughorganization/create. So on the most common self-host path (os serve, first sign-up), the organization still has no currency. Only the dev seed fills it. That is outside the seam this card names. Carrier: none. Raised for triage as a product question: should the Default Organization take its first owner's currency when the owner is bound?service-settingschanges only a code comment, so it has no changeset entry.plugin-authtakespatch.BGtoEUR(from 2026-01-01). The snapshot records that as CLDR has it.sys_user.localeofengivesUSD, even when the browser sendszh-CN.Generated by Claude Code