Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .changeset/25495-org-default-currency.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
'@objectstack/plugin-auth': patch
---

Clause-②: no
Metadata-protocol: none

A new organization now gets its default currency from its creator's language or region. When an organization is created through `POST /api/v1/auth/organization/create`, the server reads the creator's stored language (`sys_user.locale`), or else the creating request's `Accept-Language`. It writes that region's ISO 4217 currency as the organization's `localization.currency`: `zh-CN` gives `CNY`, `ja` gives `JPY`, `en-GB` gives `GBP`. The value is an ordinary tenant setting, so Setup → Localization shows it and can change it. Amounts in `currencyMode: 'dynamic'` fields then show that currency instead of a plain number.

When there is nothing to derive from (no stored language, no `Accept-Language`, or a tag with no country such as `es-419`), nothing is written and the setting stays unset, as before. There is no fallback to the `default_country` or `locale` platform defaults. A value already set on any rung (the organization's own, a global row, or an `OS_LOCALIZATION_CURRENCY` override) is never overwritten. A host's own `onOrganizationCreated` callback still runs, before this step.

The `os dev` seed does the same for its admin's workspace from the host's POSIX locale (`LC_ALL`, then `LC_MONETARY`, then `LANG`), beside its time-zone seeding. `C`, `POSIX`, and a host with no locale set write nothing.

The region-to-currency table is a checked-in snapshot of CLDR 48 `currencyData`.
6 changes: 5 additions & 1 deletion packages/plugins/plugin-auth/src/auth-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1320,8 +1320,12 @@ export function authEmailLocaleFromRequest(source: unknown): string | undefined
* context, or anything carrying either. Never throws: a vendor changing the
* shape it hands a callback must degrade to the deployment default, never fail
* the send.
*
* Exported for `organization-default-currency.ts`, which reads the creating
* request's language off the endpoint context the same way (#25495): one
* reader, not two.
*/
function acceptLanguageHeader(source: unknown): string | undefined {
export function acceptLanguageHeader(source: unknown): string | undefined {
if (!source || typeof source !== 'object') return undefined;
const nested = (source as { request?: unknown }).request;
const bags = [
Expand Down
36 changes: 36 additions & 0 deletions packages/plugins/plugin-auth/src/auth-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,11 @@ import { runRegisterSsoProviderFromForm, runRegisterSamlProviderFromForm, runReq
import { runResendVerificationEmail } from './send-verification-email.js';
import { runSysSessionRevoke, sysSessionRevokeDepsFor } from './sys-session-revoke.js';
import { readJsonRequestBody } from './request-json-body.js';
import {
seedDevWorkspaceCurrency,
withOrganizationDefaultCurrency,
type CurrencySettingsDoor,
} from './organization-default-currency.js';
import type { CounterStore } from './rate-limit-storage.js';
import { authIdentityManifest } from './manifest.js';
import { scheduleLegacySsoSecretMigration } from './sso-client-secret.js';
Expand Down Expand Up @@ -680,6 +685,26 @@ export class AuthPlugin implements Plugin {
}
};

// [#25495] A new organization takes its creator's default currency
// (`localization.currency`), derived from their stored language or the
// creating request's `Accept-Language`; nothing derivable ⇒ nothing
// written, a value already set ⇒ left alone. Wrapped AFTER
// `auth:configure` so a host's own `onOrganizationCreated`, from the
// plugin options or from a configure handler, still runs, and runs first.
// The settings service is resolved when an organization is created, not
// here: it starts after this plugin.
authConfig.onOrganizationCreated = withOrganizationDefaultCurrency(authConfig.onOrganizationCreated, {
dataEngine,
getSettings: () => {
try {
return ctx.getService<CurrencySettingsDoor>('settings');
} catch {
return undefined;
}
},
logger: ctx.logger,
});

// Initialize auth manager with data engine
this.authManager = new AuthManager(authConfig);
// ADR-0092 D6 — remember the storage better-auth will actually use so the
Expand Down Expand Up @@ -2487,6 +2512,17 @@ export class AuthPlugin implements Plugin {
// creator's — see the method. Only on the seeding boot: a later boot
// takes the database as it finds it.
if (seededId) await this.seedDevWorkspaceTimezone(ctx, ql, seededId);
// [#25495] And with the host's currency, by the rule a new
// organization's creator gets (`organization-default-currency.ts`).
if (seededId) {
let settings: CurrencySettingsDoor | undefined;
try {
settings = ctx.getService<CurrencySettingsDoor>('settings');
} catch {
settings = undefined;
}
await seedDevWorkspaceCurrency({ settings, ql, userId: seededId, logger: ctx.logger });
}
ctx.logger.info(`🔑 Dev admin seeded: ${email} / ${password}`);
// Surface the credentials in the `serve` startup banner. The
// ctx.logger line above is swallowed by serve's boot-quiet window
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,12 @@ const ENV_KEYS = [
'OS_MULTI_ORG_ENABLED',
'OS_PLATFORM_OWNER_EMAIL',
'OS_LOCALIZATION_TIMEZONE',
// [#25495] The same seed writes `localization.currency` from the host's
// POSIX locale; cleared so the "no localization write" cases below cannot
// pick up a currency write from the machine running them.
'LC_ALL',
'LC_MONETARY',
'LANG',
] as const;

/** Publishes an in-memory SQLite driver the way a datasource plugin does. */
Expand Down
Loading
Loading