Repository navigation
Commit e8de8a7
Fixes #25495
Clause-②: no
Metadata-protocol: none
## What this does
A new organization now gets `localization.currency` from its creator's
language or region. The maintainer's ruling, as the card quotes it:
「#8345:环境的默认币种,应该在objectstack 仓库吧」「同意乙」.
- **Where the value comes from.** First the creator's stored
`sys_user.locale`. If that is not set, the creating request's
`Accept-Language`, read with the platform's one parser
(`preferredLocaleFromHeader`). This is the same order `GET
/auth/me/localization` uses for the user's language, minus its third
rung (the deployment default).
- **The mapping.** The tag's region. A bare language uses the region
`Intl.Locale#maximize()` gives it (`zh` maps to CN, `ja` to JP). That
region's ISO 4217 currency comes from a checked-in CLDR 48
`currencyData` snapshot (`REGION_CURRENCY`). The provenance block names
the package, the file's sha256 and the selection rule. `Intl` has no
region-to-currency lookup and no installed dependency carries the table,
so nothing was added to the dependency tree.
- **Nothing to derive from means nothing is written.** That covers no
stored language, no header, a tag with no country (`es-419`), or a
malformed tag. There is no fallback to `default_country: 'US'` or
`locale: 'en-US'`.
- **Never over a set value.** The write happens only while the key still
resolves from the default (`source: 'default'`). It is the settings
service's own `set`, one `tenant` row, the same row `PUT
/api/settings/localization` writes. Setup → Localization shows it and
can change it.
- **The dev seed** does the same for its admin's workspace, beside the
existing time-zone seeding. It reads the host's POSIX locale (`LC_ALL`,
then `LC_MONETARY`, then `LANG`). It does not use `Intl`'s default
locale, because that answers `en-US` on a host with no locale set. This
container is one: `LANG` unset, and
`Intl.DateTimeFormat().resolvedOptions().locale` answers `en-US`.
- **The manifest comment** on the `currency` key now says where a new
organization's value comes from.
## The seam, measured (dispatch assumptions A1–A3)
- **A1, holds.** `AuthManagerOptions.onOrganizationCreated` exists, and
`afterCreateOrganization` calls it with `{ organizationId, userId, name,
slug }`. No production code supplied it. AuthPlugin now wraps it after
`auth:configure`, so a host's own callback still runs, and runs first.
Which of the two sources the seam can see: **both**.
- The stored locale: through `userId`, read from `sys_user` as a system
read.
- The request's `Accept-Language`: better-auth runs
`afterCreateOrganization` inside its endpoint-context
`AsyncLocalStorage`, so `tryGetCurrentAuthEndpointContext()` carries the
headers. The same accessor `auth-manager-database-secret.ts` already
uses.
Case 1 below proves it end to end: no stored locale, only the header,
and `CNY` lands.
- **A2, holds.** It is an ordinary tenant-scoped `SettingsService.set`.
The tests read `getNamespace('localization', { tenantId })`, which is
what the Setup → Localization route returns. They see `{ value: 'CNY',
source: 'tenant' }`.
- **A3, NOT MEASURED.** `objectstack-ai/cloud` is not reachable from
this session: `add_repo` refused 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-auth
`createOrganization`, 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 `organization` plugin factory was not edited. PR #25594 landed on
`main` during this run. This branch merged it, and `auth-manager.ts` and
`auth-manager-organization-plugin.ts` are untouched by this diff.
## Tests
All results are at head `8bce0422cd`; the reader consolidation
`40b9af0e49` re-ran the package suite (3667 passed | 10 skipped) and the
four targeted files (115/115). They follow the build closure `pnpm turbo
run build --filter='@objectstack/plugin-auth...'` (29/29 tasks).
- `organization-created-default-currency.test.ts`, 9 cases. It boots a
real `ObjectKernel` with ObjectQL on SQLite and the real `AuthPlugin`,
`SecurityPlugin` and `SettingsServicePlugin`. Organizations are created
through the real `POST /organization/create` under the `group` posture,
with a fixture `org-scoping` provider.
- 1: a zh-CN request gives `CNY` (`source: 'tenant'`).
- 2 and 3: no derivation gives nothing stored and zero currency writes.
- 4: stored `ja-JP` outranks a zh-CN header and gives `JPY`.
- 5: `OS_LOCALIZATION_CURRENCY=EUR` is kept, with no write.
- 6: a host callback runs first and its `GBP` is kept.
- 7: the dev seed with `LANG=zh_CN.UTF-8` gives `CNY`.
- 8: the dev seed with `LANG=C.UTF-8` stores nothing.
- 9: the dev seed leaves an existing tenant value alone.
- `organization-default-currency.test.ts`, 32 cases. The table is all in
the `iso_4217_currency` domain. 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`
/ `LANG` added 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`).
- **Ablation**, one run per leg, through `scripts/ablation-replace.mjs`
with a trap restore. The tests import `AuthPlugin` from source, so no
rebuild is involved.
- Leg A: the request's `Accept-Language` is never read (anchor x1 to x0,
blob `2033a70a` to `56a7862a`). Case 1 goes red: 1 failed, 8 passed.
- Leg B: the `source !== 'default'` guard is removed (blob to
`6fd0c02e`). Cases 5, 6 and 9 go red: 3 failed, 6 passed.
- Restore: blob equals HEAD `2033a70a`, and `git diff HEAD` is empty
after each leg.
## Gates
- `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` at `40b9af0e49` (and earlier at
`8bce0422cd`) derived 74 commands. All 74 ran with exit 0. The `--ran`
reconciliation 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-config` on the six touched files: exit 0.
- The repo-wide farm runs in CI on this PR.
## 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 SHA `2958f17e9c6a` (`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 host `LANG` unset. Playwright Chromium, locale `zh-CN`, signed
in as the dev admin:
- `POST /api/v1/auth/organization/create` with `Accept-Language:
zh-CN,zh;q=0.9` created 上海分公司. `GET /api/settings/localization` there
answers `currency {value: CNY, source: tenant}`; the Default
Organization (no creator) answers `{value: null, source: default}`.
- In 上海分公司, `/_console/apps/com.example.showcase/showcase_product`: a
new product's `unit_price` (dynamic mode, no code) renders `¥1,234.50`,
and every seeded price renders with `¥`. The same list in the Default
Organization renders plain `29.99`, `99.00` ….
- Setup → Localization
(`/_console/apps/setup/system/settings/localization`) shows 默认货币 = CNY
with the 租户 (tenant) source badge.
## Acceptance notes
- **The `single` posture's Default Organization gets no currency.** It
is created at boot by `ensureDefaultOrganizationExists`, which has no
creator. The first admin is bound to it later and never passes through
`organization/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-settings` changes only a code comment, so it has no changeset
entry. `plugin-auth` takes `patch`.
- CLDR 48 maps `BG` to `EUR` (from 2026-01-01). The snapshot records
that as CLDR has it.
- Precedence follows the card: a stored `sys_user.locale` of `en` gives
`USD`, even when the browser sends `zh-CN`.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01JVTd3PprStGa26aez5omLB)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent 42d9fe8 commit e8de8a7
8 files changed
Lines changed: 983 additions & 1 deletion
File tree
- .changeset
- packages
- plugins/plugin-auth/src
- services/service-settings/src/manifests
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1320 | 1320 | | |
1321 | 1321 | | |
1322 | 1322 | | |
| 1323 | + | |
| 1324 | + | |
| 1325 | + | |
| 1326 | + | |
1323 | 1327 | | |
1324 | | - | |
| 1328 | + | |
1325 | 1329 | | |
1326 | 1330 | | |
1327 | 1331 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
75 | 75 | | |
76 | 76 | | |
77 | 77 | | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
78 | 83 | | |
79 | 84 | | |
80 | 85 | | |
| |||
680 | 685 | | |
681 | 686 | | |
682 | 687 | | |
| 688 | + | |
| 689 | + | |
| 690 | + | |
| 691 | + | |
| 692 | + | |
| 693 | + | |
| 694 | + | |
| 695 | + | |
| 696 | + | |
| 697 | + | |
| 698 | + | |
| 699 | + | |
| 700 | + | |
| 701 | + | |
| 702 | + | |
| 703 | + | |
| 704 | + | |
| 705 | + | |
| 706 | + | |
| 707 | + | |
683 | 708 | | |
684 | 709 | | |
685 | 710 | | |
| |||
2487 | 2512 | | |
2488 | 2513 | | |
2489 | 2514 | | |
| 2515 | + | |
| 2516 | + | |
| 2517 | + | |
| 2518 | + | |
| 2519 | + | |
| 2520 | + | |
| 2521 | + | |
| 2522 | + | |
| 2523 | + | |
| 2524 | + | |
| 2525 | + | |
2490 | 2526 | | |
2491 | 2527 | | |
2492 | 2528 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
69 | 69 | | |
70 | 70 | | |
71 | 71 | | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
72 | 78 | | |
73 | 79 | | |
74 | 80 | | |
| |||
0 commit comments