Skip to content

Commit 54e43db

Browse files
os-zhuangclaude
andauthored
ADR-0081 (cloud): open org-management basics + retire plugin-org-scoping to enterprise (#2699)
* chore: bump objectui to 195121ade6ec feat(studio): match form designer/preview column density to the runtime form (#2345) objectui@195121ade6ec921e5786030093f0b23fc58d02f7 * feat(auth,platform-objects): open single-org org-management basics (cloud ADR-0081 D1) Single-org deployments had NO way to add a user at all: the invite affordances hid behind 'features.multiOrgEnabled' gates, and nothing ever created an organization (OrgScopingPlugin and its default-org bootstrap are multi-org-gated), so sessions carried no activeOrganizationId and better-auth organization/invite-member had no org to resolve. - plugin-auth: ensureDefaultOrganization moves here (open home; the multi-org seed-ownership step becomes an injectable option that plugin-org-scoping's wrapper keeps providing), wired in single-org mode on kernel:ready + after every sys_user_permission_set insert (autoDefaultOrganization opt-out). - plugin-auth: default session.create.before hook stamps activeOrganizationId from the caller's sys_member row (owner-preferred); host-supplied hooks chain first and win (autoActiveOrganization opt-out). - platform-objects: member/invitation/team actions re-gate from 'features.multiOrgEnabled' to 'features.organization' (the capability is always mounted); sys_organization's own create/leave/delete keep the multi-org gate so the single-org default org stays protected. - setup nav: new nav_organization entry deep-links to the ACTIVE org's record page ({current_org_id}); nav_teams / nav_invitations drop the org-scoping service gate; the Organizations LIST keeps it (multi-org only). Live-verified single-org via browser: first-run wizard -> default org (renamed, CJK name) + owner membership + active-org session; invite -> accept -> change role -> remove, all through better-auth endpoints. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat!: retire plugin-org-scoping — multi-org runtime moves to the enterprise @objectstack/organizations package (cloud ADR-0081 D2) BREAKING: the open @objectstack/plugin-org-scoping package is removed. The multi-org runtime (organization_id auto-stamp, per-org seed replay, multi-org default-org bootstrap) is now the closed-source @objectstack/organizations package, which keeps registering the historical 'org-scoping' service — the plugin-security RLS probe and the requiresService nav gates are unaffected. - cli serve / plugin-dev / verify harness mount it best-effort by name when OS_MULTI_ORG_ENABLED=true; a missing package WARNS loudly (serve/dev) or throws an actionable error (verify's explicit multiTenant opt-in) instead of silently downgrading the RLS posture. - The open member-management basics are unaffected: plugin-auth's single-org default-org bootstrap + better-auth invitations (ADR-0081 D1) keep working without the enterprise package. - dogfood multi-org RLS gate skips (loudly) when the package isn't linked; enterprise/cloud CI runs it. Live-verified: multi-org stack with the enterprise package linked (bootstrap, second-org create, {current_org_id} nav following org switch) and the missing-package single-org downgrade warning. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(verify,changeset): keep the verify harness pure single-tenant; drop deleted package from changeset fixed group - verify harness: pass autoDefaultOrganization: false to AuthPlugin. The single-org default-org bootstrap (ADR-0081 D1) is a product onboarding convenience for objectstack dev/serve; running it inside the harness minted a Default Organization + bound the dev admin as owner, giving every 'single-tenant' fixture an active org — which switched on org-scoped RLS and reparented seeded rows, breaking the pure single-tenant baseline the dogfood proofs assert (ADR-0057 identity create, ADR-0062 federation, ADR-0086 two-doors). The bootstrap stays covered by plugin-auth unit tests + browser E2E. - .changeset/config.json: remove @objectstack/plugin-org-scoping from the fixed group (package deleted in this branch). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 602053c commit 54e43db

47 files changed

Lines changed: 629 additions & 2490 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.changeset/config.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@
4141
"@objectstack/plugin-email",
4242
"@objectstack/plugin-hono-server",
4343
"@objectstack/mcp",
44-
"@objectstack/plugin-org-scoping",
4544
"@objectstack/plugin-reports",
4645
"@objectstack/plugin-security",
4746
"@objectstack/plugin-sharing",

.objectui-sha

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
c27bd3264f394bec5f5f71326118bfb115fbe884
1+
195121ade6ec921e5786030093f0b23fc58d02f7

packages/cli/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,6 @@
5959
"@objectstack/plugin-auth": "workspace:*",
6060
"@objectstack/plugin-email": "workspace:*",
6161
"@objectstack/plugin-hono-server": "workspace:*",
62-
"@objectstack/plugin-org-scoping": "workspace:*",
6362
"@objectstack/plugin-reports": "workspace:*",
6463
"@objectstack/plugin-security": "workspace:*",
6564
"@objectstack/plugin-sharing": "workspace:*",

packages/cli/src/commands/serve.ts

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1279,21 +1279,28 @@ export default class Serve extends Command {
12791279
}
12801280
}
12811281

1282-
// Pair: OrgScopingPlugin (multi-tenant) — optional, must register BEFORE SecurityPlugin
1283-
// OrgScopingPlugin provides `organization_id` auto-stamp, per-org
1284-
// seed-replay, and default-org bootstrap. SecurityPlugin probes
1285-
// the `org-scoping` service at start() time and conditionally
1286-
// strips the wildcard `tenant_isolation` RLS when this plugin
1287-
// is absent — so registration order matters.
1282+
// Pair: OrganizationsPlugin (multi-org, ENTERPRISE) — must register
1283+
// BEFORE SecurityPlugin. The multi-org runtime (`organization_id`
1284+
// auto-stamp, per-org seed replay, multi-org default-org bootstrap)
1285+
// lives in the closed-source `@objectstack/organizations` package
1286+
// (ADR-0081 D2; it registers the historical `org-scoping` service
1287+
// SecurityPlugin probes at start() to keep vs strip the wildcard
1288+
// `tenant_isolation` RLS — so registration order matters). Without
1289+
// it, deployments are single-org: the open member-management
1290+
// basics (plugin-auth's default-org bootstrap + better-auth
1291+
// invitations) still work.
12881292
const multiTenant = resolveMultiOrgEnabled();
12891293
if (multiTenant) {
12901294
try {
1291-
const orgScopingPkg = '@objectstack/plugin-org-scoping';
1292-
const { OrgScopingPlugin } = await import(/* webpackIgnore: true */ orgScopingPkg);
1293-
await kernel.use(new OrgScopingPlugin());
1294-
trackPlugin('OrgScoping');
1295+
const organizationsPkg = '@objectstack/organizations';
1296+
const mod: any = await import(/* webpackIgnore: true */ organizationsPkg);
1297+
await kernel.use(new mod.OrganizationsPlugin());
1298+
trackPlugin('Organizations');
12951299
} catch {
1296-
// optional — multi-tenant mode requested but plugin not installed
1300+
// Requested multi-org without the enterprise package — loud,
1301+
// not silent: RLS tenant policies will be STRIPPED and every
1302+
// org boundary is inert until the package is installed.
1303+
console.warn(chalk.yellow(' ⚠ OS_MULTI_ORG_ENABLED=true but @objectstack/organizations (enterprise) is not installed — running single-org.'));
12971304
}
12981305
}
12991306

packages/cli/src/commands/verify.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ export default class Verify extends Command {
4242
default: false,
4343
}),
4444
'multi-tenant': Flags.boolean({
45-
description: 'Boot org-scoped (register plugin-org-scoping) so tenant-isolation RLS policies apply (also honors $OS_MULTI_ORG_ENABLED)',
45+
description: 'Boot org-scoped (register the enterprise @objectstack/organizations plugin) so tenant-isolation RLS policies apply (also honors $OS_MULTI_ORG_ENABLED)',
4646
default: false,
4747
}),
4848
json: Flags.boolean({ description: 'Emit the structured report as JSON', default: false }),

packages/dogfood/test/authz-conformance.matrix.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ export const AUTHZ_CONFORMANCE: AuthzPrimitive[] = [
4141
{ id: 'controlled-by-parent', summary: 'master-detail controlled_by_parent', state: 'enforced',
4242
enforcement: 'plugin-security/security-plugin.ts computeControlledByParentFilter + assertControlledByParentWrite', proof: 'controlled-by-parent.dogfood.test.ts' },
4343
{ id: 'multi-tenant', summary: 'organization isolation', state: 'enforced',
44-
enforcement: 'plugin-org-scoping + wildcard tenant RLS', proof: 'rls-multitenant.dogfood.test.ts' },
44+
enforcement: '@objectstack/organizations (enterprise) + wildcard tenant RLS', proof: 'rls-multitenant.dogfood.test.ts' },
4545
{ id: 'anonymous-deny', summary: 'secure-by-default anonymous posture (capability)', state: 'enforced',
4646
enforcement: 'rest/rest-server.ts enforceAuth (requireAuth)', proof: 'showcase-anonymous-deny.dogfood.test.ts' },
4747
{ id: 'default-profile', summary: 'app-declared default profile (isDefault)', state: 'enforced',

packages/dogfood/test/rls-multitenant.dogfood.test.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,19 @@ import crmStack from '@objectstack/example-crm';
3030
import { bootStack, type VerifyStack } from '@objectstack/verify';
3131
import { runRlsProofs, formatRlsReport, type RlsReport } from '@objectstack/verify';
3232

33-
describe('objectstack verify RLS: CRM multi-tenant (#1994 org-scoped)', () => {
33+
// The multi-org runtime moved to the ENTERPRISE `@objectstack/organizations`
34+
// package (ADR-0081 D2) — not part of this open workspace. Skip (loudly) when
35+
// it isn't linked in; enterprise/cloud CI, which ships the package, runs this.
36+
const organizationsPkg = '@objectstack/organizations';
37+
const organizationsAvailable = await import(/* webpackIgnore: true */ organizationsPkg)
38+
.then(() => true)
39+
.catch(() => false);
40+
if (!organizationsAvailable) {
41+
// eslint-disable-next-line no-console
42+
console.warn('[dogfood] @objectstack/organizations (enterprise) not installed — skipping the multi-org RLS gate');
43+
}
44+
45+
describe.skipIf(!organizationsAvailable)('objectstack verify RLS: CRM multi-tenant (#1994 org-scoped)', () => {
3446
let stack: VerifyStack;
3547
let report: RlsReport;
3648

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

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,22 @@ export const SETUP_NAV_CONTRIBUTIONS: NavigationContribution[] = [
5757
priority: BASE_PRIORITY,
5858
items: [
5959
{ id: 'nav_users', type: 'object', label: 'Users', objectName: 'sys_user', icon: 'user' },
60+
// The ACTIVE organization's record page (Members / Invitations / Teams
61+
// tabs with the better-auth row actions), rendered inside the app shell
62+
// (ADR-0081). `{current_org_id}` resolves from the session's active
63+
// organization; unresolved (e.g. org-less admin before bootstrap) it
64+
// falls back to the sys_organization list — one row in single-org.
65+
{ id: 'nav_organization', type: 'object', label: 'Organization', objectName: 'sys_organization', recordId: '{current_org_id}', icon: 'building-2' },
6066
{ id: 'nav_business_units', type: 'object', label: 'Business Units', objectName: 'sys_business_unit', icon: 'building', requiresObject: 'sys_business_unit' },
61-
{ id: 'nav_teams', type: 'object', label: 'Teams', objectName: 'sys_team', icon: 'users-round', requiresService: 'org-scoping' },
67+
// Teams / Invitations no longer gate on `org-scoping` (ADR-0081 D1):
68+
// the better-auth organization capability is always mounted, and
69+
// plugin-auth's single-org default-org bootstrap guarantees an org to
70+
// invite into — these are the OPEN member-management basics. Only the
71+
// org LIST below keeps the gate: browsing organizations is meaningful
72+
// only when more than one can exist (enterprise multi-org).
73+
{ id: 'nav_teams', type: 'object', label: 'Teams', objectName: 'sys_team', icon: 'users-round' },
6274
{ id: 'nav_organizations', type: 'object', label: 'Organizations', objectName: 'sys_organization', icon: 'building-2', requiresService: 'org-scoping' },
63-
{ id: 'nav_invitations', type: 'object', label: 'Invitations', objectName: 'sys_invitation', icon: 'mail', requiresService: 'org-scoping' },
75+
{ id: 'nav_invitations', type: 'object', label: 'Invitations', objectName: 'sys_invitation', icon: 'mail' },
6476
],
6577
},
6678
{

packages/platform-objects/src/apps/translations/en.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ export const en: TranslationData = {
6363

6464
// People & Organization
6565
nav_users: { label: 'Users' },
66+
nav_organization: { label: 'Organization' },
6667
nav_business_units: { label: 'Business Units' },
6768
nav_teams: { label: 'Teams' },
6869
nav_organizations: { label: 'Organizations' },

packages/platform-objects/src/apps/translations/es-ES.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ export const esES: TranslationData = {
4545
nav_system_overview: { label: 'Resumen del Sistema' },
4646

4747
nav_users: { label: 'Usuarios' },
48+
nav_organization: { label: 'Organización' },
4849
nav_business_units: { label: 'Unidades de negocio' },
4950
nav_teams: { label: 'Equipos' },
5051
nav_organizations: { label: 'Organizaciones' },

0 commit comments

Comments
 (0)