You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 5cf1b9d
Browse filesBrowse the repository at this point in the historyBrowse files
feat(plugin-auth): the single posture's Default Organization takes its first owner's default currency (#25706)
Fixes#25651
Clause-②: no
Metadata-protocol: none
## What
The single posture's Default Organization now takes its first owner's
default currency (`localization.currency`, one `tenant` row), by the
same rule and the same write a new organization's creator gets (the
#25495 helper, `organization-default-currency.ts`, reused, no second
copy of the rule): the owner's stored `sys_user.locale`, else the
binding request's `Accept-Language`, through the CLDR region table.
Nothing derivable means nothing is written; a value already set on any
rung is kept. No new setting, key or door.
- `ensure-default-organization.ts`: `EnsureDefaultOrganizationResult`
gains an optional `ownerUserId`, set exactly when the call bound
(`memberCreated`) or promoted (`ownerPromoted`) the owner.
- `auth-plugin.ts`: the single-posture `runEnsure` (the once-gated
default-organization bootstrap) calls `seedCreatedOrganizationCurrency`
for the Default Organization and that owner when, and only when, the
gate reports the owner first bound. Outside the `organization` plugin
factory region of `auth-manager.ts`.
## Where the owner is first bound, and what is visible there (measured)
- `ensureDefaultOrganizationExists` creates the Default Organization at
boot with no creator, so it never reaches `onOrganizationCreated`.
- On the first sign-up, the owner bind runs inside that sign-up's
request: `POST /sign-up/email` inserts the `sys_user`; plugin-security's
first-user promotion writes the `sys_user_permission_set` grant from its
write middleware; plugin-auth's bootstrap middleware fires on that grant
insert and binds the owner. A temporary probe on the composed kernel
(case 10 below) printed the gate's answers in order: `no_admin`,
`no_admin`, `admin_already_in_org` (the re-entrant call while the
decision is in flight), then `memberCreated: true` with `ownerUserId`
set. The probe was mutation-and-restore through
`scripts/ablation-replace.mjs` (blob restored to HEAD `8c554309`), not
committed.
- The request is visible: better-auth's endpoint context
(AsyncLocalStorage) is still current there, so `Accept-Language` is
read. Case 10 asserts CNY with no stored locale, which is only possible
from the request.
- An email sign-up stores no `sys_user.locale` (it stays unset; only an
IdP `locale` claim writes it, `idp-locale-claim.ts`). So on a first
email sign-up the sign-up request's `Accept-Language` is the input. A
bind at `kernel:ready` (no request) reads the stored locale only.
## Tests
`organization-created-default-currency.test.ts`, a new describe block on
the real composition (ObjectKernel, ObjectQL on in-memory SQLite,
AuthPlugin, SecurityPlugin, SettingsServicePlugin) under the default
`single` posture, the first sign-up through `AuthManager.handleRequest`:
- 10: a zh-CN first sign-up leaves the Default Organization with
`localization.currency` `{ value: 'CNY', source: 'tenant' }`, exactly
one settings write.
- 11: no stored language and no `Accept-Language`: `source: 'default'`,
no value, zero writes.
- 12: a currency already stored on the Default Organization (USD) is
kept, zero writes.
Each case first asserts the sign-up was bound as the owner, so none
passes vacuously.
Ablation (one leg): the new call's guard replaced by `if (false as
boolean)` via `scripts/ablation-replace.mjs` ⇒ case 10 red (`expected
... to match object { value: 'CNY', source: 'tenant' }`), 11 and 12
green; restored, blob == HEAD `8c554309` and `git diff HEAD` empty.
AuthPlugin is imported from source, so no build was in the loop.
Runs at head 1ce6154:
- `pnpm --filter @objectstack/plugin-auth exec vitest run
--maxWorkers=2`: Test Files 193 passed (193), Tests 3670 passed, 10
skipped.
- `pnpm --filter @objectstack/plugin-auth typecheck`: exit 0 (tsc,
examples, `check:test-typecheck` OK).
- eslint `--no-inline-config` on the three touched source files: exit 0.
- `dispatch-gates --commands`: 74 derived, 73 run exit 0, 1 NOT
MEASURED: `check:dual-build-cjs-loads` exit 3, PREREQUISITE NOT MET (it
needs a whole-workspace build; CI runs it).
## Acceptance notes
- The log lines `seedCreatedOrganizationCurrency` prints name the
"creator" (for example "its creator's language"); for the Default
Organization the first owner stands in for the creator. Wording only,
left as is to keep one copy of the rule.
- The walled posture's default-organization bootstrap
(`@objectstack/organizations`) is not wired here; the card and ruling
cover the single posture.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01JVTd3PprStGa26aez5omLB)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
The single posture's Default Organization now takes its first owner's default currency. That organization is created at boot with no creator, so it never passed through organization creation, and a new self-hosted install showed code-less amounts as plain numbers until an administrator set Setup → Localization → Default currency by hand. When the default-organization bootstrap first binds (or promotes) its owner, the organization gets `localization.currency` by the same rule a new organization's creator gets: the owner's stored `sys_user.locale`, else the binding request's `Accept-Language` (on a first sign-up, the sign-up request), mapped through the CLDR region table. Nothing derivable means nothing is written, and a value already set on any rung is never overwritten. `EnsureDefaultOrganizationResult` gains an optional `ownerUserId`, set when the call bound or promoted the owner.
0 commit comments