Skip to content

Commit dcd73a2

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-15999-authz-outage-status-relay
2 parents 673ead7 + d1c86a7 commit dcd73a2

36 files changed

Lines changed: 2245 additions & 120 deletions
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
"@objectstack/spec": patch
3+
---
4+
5+
`api-surface/` now records the value half of a name declared as both a const and a type, so deleting it is a breaking change the gate reports.
6+
7+
TypeScript merges an `export const X` and an `export type X` into ONE symbol whose flags carry both. `build-api-surface.ts` mapped that symbol through a first-match-wins lookup that tested `TypeAlias` before `Variable`, so the shard recorded `X (type)` alone and the value half was never enumerated. Ablated: deleting `export const RestApiRouteRegistration` while keeping its type alias left all 17 shards byte-identical, the export total unmoved, and `check:api-surface` printing "public API surface + factory signatures unchanged" at exit 0 — on a removed public value export, which is the exact removal the ADR-0059 breadth gate exists to make loud. On the fixed generator the same deletion reports `- RestApiRouteRegistration (const)` as 1 breaking change and exits 1.
8+
9+
The generator now emits one row per DECLARED kind. The shipped `api-surface/` shards gain **134 rows across 10 of 17 entry points** — every one of them the previously-missing `(const)` half of a name that also declares a type — as a pure insertion: zero rows removed, zero modified, no reordering.
10+
11+
**Why `patch` and not `minor`, measured against what a consumer can observe.** No export was added, removed or renamed: `dist/` is byte-identical across this change, and the row grammar `Name (kind)` is untouched, so anything that parsed the artifact before parses it now. The 134 new rows describe exports that already existed — the record got more complete, no capability arrived. What changes is the accuracy of a shipped record and the strictness of this repo's own gate, which is a fix.
12+
13+
One consequence for the release seat, stated because it is not visible from the diff: `build-spec-changes.ts --previous-surface` is a release-time join, so a release crossing this change will list those 134 rows as `added` surface entries. They are not new API — they are the same exports, newly recorded.

.github/workflows/shard-timings-refresh.yml

Lines changed: 604 additions & 0 deletions
Large diffs are not rendered by default.

content/docs/permissions/sharing-rules.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ principal: **system** contexts, and **platform operators** — a holder of
176176
`manage_platform_settings` or of the built-in `platform_admin` position
177177
administers rules across the deployment whether or not an organization is
178178
selected, which is also what a single-tenant deployment looks like before its
179-
default organization is bootstrapped (ADR-0081 D1).
179+
default organization is bootstrapped (cloud ADR-0081 D1).
180180

181181
### Switching a rule off withdraws the access it granted
182182

docs/adr/0093-tenancy-mode-and-membership-lifecycle.md

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
- **Date:** 2026-07-13
55
- **Deciders:** ObjectStack Protocol Architects
66
- **Implementation:** #2882 (Phase 0 — tactical create-user bind, merged) → this PR (Phases 1–3 — `tenancy` service, fail-fast boot guard, membership reconciler, consumer migration, backfill, docs). One revision from the original plan, ratified in D2: the endpoint-level create-user bind **delegates to the shared reconciler** (one implementation, two call sites) instead of being deleted. Runtime verification confirmed the hook fires for `admin.createUser`, but better-auth *defers* `user.create.after` post-commit (#1881), so the endpoint keeps its delegated call to report `organizationId` / `membershipCreated` deterministically in its response. Cloud-host semantics (personal-org hook precedence, multi-org non-binding, D5 blast radius) verified against `objectstack-ai/cloud` — see D2/D3/D5.
7-
- **Relates to:** [ADR-0049](./0049-no-unenforced-security-properties.md) (no unenforced security properties), [ADR-0057](./0057-erp-authorization-core-business-units-and-scope-depth.md) (org-scoped identity optionality), [ADR-0068](./0068-unified-user-context-and-built-in-identity-roles.md) (platform-admin gate), [ADR-0092](./0092-sys-user-profile-field-delegation.md) (identity write guard), the default-org bootstrap (`plugin-auth/src/ensure-default-organization.ts`, referenced in code as "ADR-0081 D1" — that decision record predates this repo's ADR series), #2766 (admin user management), PR #2882 (single-org create-user membership bind — the tactical fix this ADR generalizes)
7+
- **Relates to:** [ADR-0049](./0049-no-unenforced-security-properties.md) (no unenforced security properties), [ADR-0057](./0057-erp-authorization-core-business-units-and-scope-depth.md) (org-scoped identity optionality), [ADR-0068](./0068-unified-user-context-and-built-in-identity-roles.md) (platform-admin gate), [ADR-0092](./0092-sys-user-profile-field-delegation.md) (identity write guard), the default-org bootstrap (`plugin-auth/src/ensure-default-organization.ts`, referenced in code as "cloud ADR-0081 D1" — identified 2026-09-04: that record is **cloud ADR-0081**, *Organization Management — Open Basics, Enterprise `@objectstack/organizations`*, Accepted 2026-07-09 in the `objectstack-ai/cloud` repo's own ADR series, where its **D1** is this bootstrap; it predates *this* repo's series because it lives in that one, where it is current), #2766 (admin user management), PR #2882 (single-org create-user membership bind — the tactical fix this ADR generalizes)
88

99
## TL;DR
1010

@@ -65,8 +65,9 @@ Decision:
6565
resolves a session's `activeOrganizationId` from the caller's `sys_member`
6666
row (owner-preferred, else oldest), only when the draft lacks one,
6767
best-effort, opt-out via `autoActiveOrganization: false`. Shipped behaviour,
68-
previously cited in code as a pre-repo "ADR-0081 D1" whose number now
69-
collides with this repo's ADR-0081; anchored here because it reads exactly
68+
previously cited in code as an unqualified "ADR-0081 D1" — the record is
69+
cloud ADR-0081 (identified 2026-09-04), and written without that qualifier
70+
the number collides with this repo's ADR-0081; anchored here because it reads exactly
7071
the invariant D1 states and D2 owns.
7172

7273
## Context
@@ -384,7 +385,9 @@ admin surfaces.
384385
different, larger decision — refused here to keep this ADR mechanical.
385386
2. **The dual frontend flags keep their meaning.** `features.organization`
386387
(member management available) vs `features.multiOrgEnabled` (org management
387-
available) is a deliberate ADR-0081-D1 distinction; only their *backing
388+
available) is a deliberate cloud ADR-0081 D1/D2 distinction — D1 keeps
389+
basic member management OPEN, D2 closes multi-org into the enterprise
390+
`@objectstack/organizations` package; only their *backing
388391
fact* moves to the tenancy service.
389392
3. **better-auth keeps owning `sys_member` CRUD.** The reconciler writes
390393
through the system context exactly as `ensureDefaultOrganization` does; no
@@ -398,12 +401,15 @@ admin surfaces.
398401
> **This decision is a RECORDING, not a new ruling.** The behaviour below has
399402
> shipped since before this ADR, and nothing about it changes. What changes is
400403
> that it now has an anchor. The code carried it as **"ADR-0081 D1"**, a label
401-
> inherited from a decision record that predates this repo's ADR series (the
402-
> same pre-repo record the *Relates to* line names for the default-org
403-
> bootstrap). That number now collides with this repo's
404+
> inherited from **cloud ADR-0081***Organization Management — Open Basics,
405+
> Enterprise `@objectstack/organizations`*, Accepted 2026-07-09 in the
406+
> `objectstack-ai/cloud` repo's ADR series (identified 2026-09-04; the same
407+
> record the *Relates to* line names for the default-org bootstrap). Written
408+
> without that qualifier the number collides with this repo's
404409
> [ADR-0081](./0081-trusted-react-page-tier.md) — the trusted `kind:'react'`
405410
> page tier — so a reader following the citation landed in a document about
406-
> React pages with no signal they were in the wrong record. The decision is
411+
> React pages with no signal they were in the wrong record; the surviving
412+
> citations elsewhere in this repo now carry the `cloud ` qualifier. The decision is
407413
> restated here because this is the record that owns the fact it depends on:
408414
> `sys_member` and the membership lifecycle (D1/D2).
409415

docs/adr/0105-group-tenancy-posture-and-first-class-org-scope.md

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -368,11 +368,15 @@ seed/config replay, org analytics, and the D13 promotion tooling.
368368
> #16137 wires `serve` to the open registrar. ⛔ ADR-0132 does not itself
369369
> deliver that; it ships the registrar.
370370
371-
> **Citation note (2026-08-16) — hygiene, not a decision.** Code and tests
372-
> carried this entitlement as **"ADR-0081 D2"**, a label inherited from a
373-
> decision record that predates this repo's ADR series — the same pre-repo
374-
> record whose "ADR-0081 D1" label [ADR-0093](./0093-tenancy-mode-and-membership-lifecycle.md)
375-
> D9 names. That number now collides with this repo's
371+
> **Citation note (2026-08-16; record identified 2026-09-04) — hygiene, not a
372+
> decision.** Code and tests carried this entitlement as **"ADR-0081 D2"**, a
373+
> label inherited from **cloud ADR-0081***Organization Management — Open
374+
> Basics, Enterprise `@objectstack/organizations`*, Accepted 2026-07-09 in the
375+
> `objectstack-ai/cloud` repo's ADR series, which is the record the *Builds on*
376+
> line above already cites by that name, and the same one whose "ADR-0081 D1"
377+
> label [ADR-0093](./0093-tenancy-mode-and-membership-lifecycle.md)
378+
> D9 names; its **D2** is the multi-org entitlement decision. Written without
379+
> that qualifier the number collides with this repo's
376380
> [ADR-0081](./0081-trusted-react-page-tier.md), the trusted `kind:'react'`
377381
> page tier, whose Decision section is numbered 1–4 and has no D-numbered
378382
> decisions at all — so a reader following the citation landed in a document

docs/adr/0131-total-organization-ownership-no-null-organization-id.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -825,9 +825,12 @@ stamp-and-backfill repair (#12929, #13180, #13527, #13572, #13565, #14726) and #
825825
- The issue body of #13564 cites the arm by a bare line number (7320) that had drifted by ~4,600 lines
826826
before the first census read it; on `origin/main` `2514d49f3` the
827827
arms are inside `applyTenantScope` (~12016–12066) and there are **two**. This record cites symbols.
828-
- `ensure-default-organization.ts` cites "ADR-0081 D1" for the Default Organization; in this
829-
repository `docs/adr/0081` is the trusted React page tier — the "0081" is cloud's numbering. The
830-
framework record is [ADR-0093](./0093-tenancy-mode-and-membership-lifecycle.md).
828+
- `ensure-default-organization.ts` cites "cloud ADR-0081 D1" for the Default Organization; in this
829+
repository `docs/adr/0081` is the trusted React page tier — the "0081" is cloud's numbering, and the
830+
record is **cloud ADR-0081** (*Organization Management — Open Basics, Enterprise
831+
`@objectstack/organizations`*, Accepted 2026-07-09), identified 2026-09-04, so the citations now carry
832+
the qualifier rather than resolving here. The framework record is
833+
[ADR-0093](./0093-tenancy-mode-and-membership-lifecycle.md).
831834
- `resolve-authz-context.ts` already resolves positions **by name** (`{ name: { $in: grants.positions } }`
832835
against `sys_position`); D4 changes where that lookup goes first, not what it is keyed on.
833836
- `template-loader.ts` resolves by `(name, locale)`; a grep for `organization` / `tenant` in it returns

docs/qa/platform-checklist/areas/identity-auth.json

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -754,12 +754,12 @@
754754
"fixtures": {
755755
"app": "showcase",
756756
"requires": [
757-
"the organization capability mounted (always mounted per ADR-0081 D1; plugin-auth's default-org bootstrap guarantees an org to manage) and a session with an active organization so {current_org_id} resolves",
757+
"the organization capability mounted (always mounted per cloud ADR-0081 D1; plugin-auth's default-org bootstrap guarantees an org to manage) and a session with an active organization so {current_org_id} resolves",
758758
"at least two members in the org so role changes and removals have a target that is not the admin"
759759
]
760760
},
761761
"steps": [
762-
"sign in as the org owner/admin and open Setup → People & Org → Organization (nav_organization: type object, objectName sys_organization, recordId {current_org_id}, ADR-0081); screenshot the org record page and confirm {current_org_id} resolved to the session's active org (not the list fallback)",
762+
"sign in as the org owner/admin and open Setup → People & Org → Organization (nav_organization: type object, objectName sys_organization, recordId {current_org_id}, cloud ADR-0081 D3); screenshot the org record page and confirm {current_org_id} resolved to the session's active org (not the list fallback)",
763763
"confirm the record page exposes the Members / Invitations / Teams tabs with the better-auth row actions (GET list-members, list-invitations, list-teams feed them)",
764764
"change a member's role: POST /api/v1/auth/organization/update-member-role (client organizations.updateMemberRole) to one of the ADR-0108 closed 4-name vocabulary {owner, admin, delegated_admin, member}; read the membership back and confirm the new role",
765765
"prove the vocabulary is CLOSED, not merely conventional: attempt the same call with role 'guest' (and with any stack-declared position/permission-set name, e.g. showcase's 'contributor') — better-auth's role check refuses it (400 ROLE_NOT_FOUND) before any insert, and no membership/invitation row is left behind",
@@ -771,7 +771,7 @@
771771
],
772772
"acceptance": [
773773
{
774-
"clause": "{current_org_id} resolves to the session's active organization: the Organization nav opens that org's record page (Members/Invitations/Teams tabs), not the raw sys_organization list — the ADR-0081 active-org token is wired",
774+
"clause": "{current_org_id} resolves to the session's active organization: the Organization nav opens that org's record page (Members/Invitations/Teams tabs), not the raw sys_organization list — the cloud ADR-0081 D3 active-org token is wired",
775775
"oracle": "screenshot",
776776
"verify": "the org record page renders for the active org id (cross-checked against GET /api/v1/auth/organization/get-active-member / get-full-organization); the three management tabs are present",
777777
"evidence": "the org-page screenshot + the get-active/get-full response"
@@ -817,11 +817,11 @@
817817
"an org management surface where the affordance is hidden but the forged endpoint succeeds for a non-admin is a FAIL — the server is the authority (ADR-0124 D1)",
818818
"remove-member that drops the roster row but leaves the ex-member's org-scoped access intact is a FAIL — removal must change authorization",
819819
"a role written outside the {owner, admin, delegated_admin, member} vocabulary is a FAIL — including a stored 'guest': the closed list is the write-side guardrail that makes an ungoverned capability grant unrepresentable (ADR-0108), so a 2xx that persists 'guest' is a regression of the closure, not a vocabulary difference. A role change that does not flip any gate is equally a FAIL",
820-
"the Organization nav landing on the raw sys_organization list because {current_org_id} did not resolve (when an active org exists) is a FAIL of the ADR-0081 wiring"
820+
"the Organization nav landing on the raw sys_organization list because {current_org_id} did not resolve (when an active org exists) is a FAIL of the cloud ADR-0081 D3 wiring"
821821
],
822822
"traps": ["wrong-persona", "dispatcher-vs-hono-route", "hydration-race"],
823823
"source": [
824-
"packages/platform-objects/src/apps/setup-nav.contributions.ts#nav_organization (nav_organization recordId {current_org_id}, ADR-0081; Teams/Invitations always mounted per ADR-0081 D1)",
824+
"packages/platform-objects/src/apps/setup-nav.contributions.ts#nav_organization (nav_organization recordId {current_org_id}, cloud ADR-0081 D3; Teams/Invitations always mounted per cloud ADR-0081 D1)",
825825
"packages/plugins/plugin-auth/src/auth-route-ledger.ts (organization family: update-member-role, remove-member, update, create-team, add-team-member, list-members/teams/invitations, get-active-member, get-full-organization)",
826826
"packages/spec/src/identity/membership-role.ts#BUILTIN_MEMBERSHIP_ROLES (BUILTIN_MEMBERSHIP_ROLES / BUILTIN_MEMBERSHIP_ROLE_OPTIONS — THE role vocabulary: owner/admin/delegated_admin/member, ADR-0108; 'nothing widens these at boot any more')",
827827
"docs/adr/0108-membership-grade-is-not-a-capability-channel.md (why the list is closed: a grade decides what you can REACH, never a bundle of what you may do)",

packages/platform-objects/src/apps/setup-nav.contributions.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,12 +71,12 @@ export const SETUP_NAV_CONTRIBUTIONS: NavigationContribution[] = [
7171
{ id: 'nav_users', type: 'object', label: 'Users', objectName: 'sys_user', icon: 'user' },
7272
// The ACTIVE organization's record page (Members / Invitations / Teams
7373
// tabs with the better-auth row actions), rendered inside the app shell
74-
// (ADR-0081). `{current_org_id}` resolves from the session's active
74+
// (cloud ADR-0081 D3). `{current_org_id}` resolves from the session's active
7575
// organization; unresolved (e.g. org-less admin before bootstrap) it
7676
// falls back to the sys_organization list — one row in single-org.
7777
{ id: 'nav_organization', type: 'object', label: 'Organization', objectName: 'sys_organization', recordId: '{current_org_id}', icon: 'building-2' },
7878
{ id: 'nav_business_units', type: 'object', label: 'Business Units', objectName: 'sys_business_unit', icon: 'building', requiresObject: 'sys_business_unit' },
79-
// Teams / Invitations no longer gate on `org-scoping` (ADR-0081 D1):
79+
// Teams / Invitations no longer gate on `org-scoping` (cloud ADR-0081 D1):
8080
// the better-auth organization capability is always mounted, and
8181
// plugin-auth's single-org default-org bootstrap guarantees an org to
8282
// invite into — these are the OPEN member-management basics. Only the

packages/platform-objects/src/identity/invite-entry-toolbar.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
22
//
33
// #11544 — the email-invite entry was UNREACHABLE from where admins actually
4-
// look. The org record page (ADR-0081) opens on tab-0 **Members**
4+
// look. The org record page (cloud ADR-0081 D3) opens on tab-0 **Members**
55
// (`sys_member`), whose toolbar carried exactly one action — `add_member`,
66
// which attaches an ALREADY-REGISTERED user by id. `invite_user` lived only on
77
// tab-1 Invitations. The maintainer, looking to "invite a teammate by email",

packages/platform-objects/src/identity/sys-member.object.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export const SysMember = ObjectSchema.create({
4444
{
4545
// THIRD mirror of `invite_user` (sys_user, sys_invitation are the other
4646
// two — keep all three consistent). It is here because the org record
47-
// page (ADR-0081) opens on tab-0 **Members**, and the email-invite entry
47+
// page (cloud ADR-0081 D3) opens on tab-0 **Members**, and the email-invite entry
4848
// used to live only on tab-1 Invitations: an admin looking to "invite a
4949
// teammate by email" landed on Members, saw only "Add Member" (attach an
5050
// existing user by id), and concluded the product had no invite entry.

0 commit comments

Comments
 (0)