Skip to content

feat(plugin-auth): the single posture's Default Organization takes its first owner's default currency - #25706

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-25651-default-org-owner-currency
Oct 25, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-25651-default-org-owner-currency

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

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

…s first owner's default currency

Claude-Session: https://claude.ai/code/session_01JVTd3PprStGa26aez5omLB
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Oct 24, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/plugin-auth, touching 4 documentable anchor(s).

⛔ 1 release-owned page(s) name something this change touched. These are read-only:

  • content/docs/releases/v17/17-7.mdx (via ensureDefaultOrganization (symbol, a top-level function))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 21 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json b1790841bb0bf2c7c696e858de51e8105704471a → packageMentionDocs.

Which tree this was computed on

This run read content/docs from c7e9ab0b27c110e489d6111940734154eb24cf43 — the merge of head 1ce6154c182a70cf6d12dec7748b0a58bf331e88 into base b1790841bb0bf2c7c696e858de51e8105704471a, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin c7e9ab0b27c110e489d6111940734154eb24cf43 && git checkout c7e9ab0b27c110e489d6111940734154eb24cf43
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin b1790841bb0bf2c7c696e858de51e8105704471a 1ce6154c182a70cf6d12dec7748b0a58bf331e88 && git checkout -B drift-repro b1790841bb0bf2c7c696e858de51e8105704471a && git merge --no-ff 1ce6154c182a70cf6d12dec7748b0a58bf331e88

node scripts/docs-audit/affected-docs.mjs --json b1790841bb0bf2c7c696e858de51e8105704471a

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs b1790841bb0bf2c7c696e858de51e8105704471a → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对

ACCEPT · domain:server seat 2 · sunhaolin · session_01JVTd3PprStGa26aez5omLB · 2026-10-25T03:28Z · head 1ce6154c18 · claim 6387976517 · report 6388514087

  • Shape: draft, base main, author objectstack-fleet[bot], assignee sunhaolin, Fixes #25651, Clause-②: no, Metadata-protocol: none, one patch changeset on @objectstack/plugin-auth. 4 files; not governed; ruled work (「25619 C 25495 B」, the new-tenant default the ruling covers).
  • Read on the diff: when the single posture's default-organization bootstrap reports the owner first bound (memberCreated, or ownerPromoted), the Default Organization gets the owner's default currency through localization: a new organization gets its default currency from its creator's language or region (zh-CN → CNY), written as a visible Setup → Localization value; with nothing to derive from, it stays unset (maintainer ruling 乙, 2026-10-23) #25495's existing seedCreatedOrganizationCurrency: the same rule and the same tenant write, no second copy. EnsureDefaultOrganizationResult gains an optional ownerUserId. A value already set is never overwritten (the helper's own rule). No new setting, key or door; auth-manager.ts untouched.
  • Proof, as measured by the dev: three cases on the real kernel through a real sign-up: zh-CN gives { value: 'CNY', source: 'tenant' } with one write; nothing derivable gives zero writes; a stored USD is kept. Ablation of the new guard reddens the CNY case only, restored by hash.
  • CI on head 1ce6154c18: 34 success, 7 skipped, 0 red; every skip on main's roster.
  • Writes: mcp_calls 0; relay pr_create and the report comment.

Landing: pr_ready + automerge_enable through the relay in this act.


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 25, 2026 03:29
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 25, 2026 03:29
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 25, 2026
Merged via the queue into main with commit 5cf1b9d Oct 25, 2026
42 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-25651-default-org-owner-currency branch October 25, 2026 04:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

2 participants