@objectstack/plugin-org-scoping 7.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +93 -0
- package/README.md +55 -0
- package/dist/index.d.mts +190 -0
- package/dist/index.d.ts +190 -0
- package/dist/index.js +626 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +592 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +55 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
License text copyright (c) 2020 MariaDB Corporation Ab, All Rights Reserved.
|
|
2
|
+
"Business Source License" is a trademark of MariaDB Corporation Ab.
|
|
3
|
+
|
|
4
|
+
Parameters
|
|
5
|
+
|
|
6
|
+
Licensor: ObjectStack AI LLC
|
|
7
|
+
Licensed Work: ObjectStack Runtime: the BSL-licensed packages
|
|
8
|
+
of the ObjectStack monorepo as listed in LICENSING.md.
|
|
9
|
+
Copyright (c) 2026 ObjectStack AI LLC.
|
|
10
|
+
Additional Use Grant: You may make production use of the Licensed Work, provided
|
|
11
|
+
Your use does not include offering the Licensed Work to third
|
|
12
|
+
parties on a hosted or embedded basis in order to compete with
|
|
13
|
+
ObjectStack AI LLC's paid version(s) of the Licensed Work. For purposes
|
|
14
|
+
of this license:
|
|
15
|
+
|
|
16
|
+
A "competitive offering" is a Product that is offered to third
|
|
17
|
+
parties on a paid basis, including through paid support
|
|
18
|
+
arrangements, that significantly overlaps with the capabilities
|
|
19
|
+
of ObjectStack AI LLC's paid version(s) of the Licensed Work. If Your
|
|
20
|
+
Product is not a competitive offering when You first make it
|
|
21
|
+
generally available, it will not become a competitive offering
|
|
22
|
+
later due to ObjectStack AI LLC releasing a new version of the Licensed
|
|
23
|
+
Work with additional capabilities. In addition, Products that
|
|
24
|
+
are not provided on a paid basis are not competitive.
|
|
25
|
+
|
|
26
|
+
"Product" means software that is offered to end users to manage
|
|
27
|
+
in their own environments or offered as a service on a hosted
|
|
28
|
+
basis.
|
|
29
|
+
|
|
30
|
+
"Embedded" means including the source code or executable code
|
|
31
|
+
from the Licensed Work in a competitive offering. "Embedded"
|
|
32
|
+
also means packaging the competitive offering in such a way
|
|
33
|
+
that the Licensed Work must be accessed or downloaded for the
|
|
34
|
+
competitive offering to operate.
|
|
35
|
+
|
|
36
|
+
Hosting or using the Licensed Work(s) for internal purposes
|
|
37
|
+
within an organization is not considered a competitive
|
|
38
|
+
offering. ObjectStack AI LLC considers your organization to include all
|
|
39
|
+
of your affiliates under common control.
|
|
40
|
+
|
|
41
|
+
For binding interpretive guidance on using ObjectStack AI LLC products
|
|
42
|
+
under the Business Source License, please visit our FAQ.
|
|
43
|
+
(see LICENSING.md in this repository)
|
|
44
|
+
Change Date: Four years from the date the Licensed Work is published.
|
|
45
|
+
Change License: Apache License, Version 2.0
|
|
46
|
+
|
|
47
|
+
For information about alternative licensing arrangements for the Licensed Work,
|
|
48
|
+
please contact licensing@objectstack.dev.
|
|
49
|
+
|
|
50
|
+
Notice
|
|
51
|
+
|
|
52
|
+
Business Source License 1.1
|
|
53
|
+
|
|
54
|
+
Terms
|
|
55
|
+
|
|
56
|
+
The Licensor hereby grants you the right to copy, modify, create derivative
|
|
57
|
+
works, redistribute, and make non-production use of the Licensed Work. The
|
|
58
|
+
Licensor may make an Additional Use Grant, above, permitting limited production use.
|
|
59
|
+
|
|
60
|
+
Effective on the Change Date, or the fourth anniversary of the first publicly
|
|
61
|
+
available distribution of a specific version of the Licensed Work under this
|
|
62
|
+
License, whichever comes first, the Licensor hereby grants you rights under
|
|
63
|
+
the terms of the Change License, and the rights granted in the paragraph
|
|
64
|
+
above terminate.
|
|
65
|
+
|
|
66
|
+
If your use of the Licensed Work does not comply with the requirements
|
|
67
|
+
currently in effect as described in this License, you must purchase a
|
|
68
|
+
commercial license from the Licensor, its affiliated entities, or authorized
|
|
69
|
+
resellers, or you must refrain from using the Licensed Work.
|
|
70
|
+
|
|
71
|
+
All copies of the original and modified Licensed Work, and derivative works
|
|
72
|
+
of the Licensed Work, are subject to this License. This License applies
|
|
73
|
+
separately for each version of the Licensed Work and the Change Date may vary
|
|
74
|
+
for each version of the Licensed Work released by Licensor.
|
|
75
|
+
|
|
76
|
+
You must conspicuously display this License on each original or modified copy
|
|
77
|
+
of the Licensed Work. If you receive the Licensed Work in original or
|
|
78
|
+
modified form from a third party, the terms and conditions set forth in this
|
|
79
|
+
License apply to your use of that work.
|
|
80
|
+
|
|
81
|
+
Any use of the Licensed Work in violation of this License will automatically
|
|
82
|
+
terminate your rights under this License for the current and all other
|
|
83
|
+
versions of the Licensed Work.
|
|
84
|
+
|
|
85
|
+
This License does not grant you any right in any trademark or logo of
|
|
86
|
+
Licensor or its affiliates (provided that you may use a trademark or logo of
|
|
87
|
+
Licensor as expressly required by this License).
|
|
88
|
+
|
|
89
|
+
TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON
|
|
90
|
+
AN "AS IS" BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS,
|
|
91
|
+
EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF
|
|
92
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND
|
|
93
|
+
TITLE.
|
package/README.md
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# @objectstack/plugin-org-scoping
|
|
2
|
+
|
|
3
|
+
> Row-level **Organization** isolation for ObjectStack — the LOGICAL multi-tenant building block.
|
|
4
|
+
|
|
5
|
+
`@objectstack/plugin-org-scoping` makes `sys_organization` a first-class row-level scope:
|
|
6
|
+
|
|
7
|
+
- **Insert auto-stamp** — fills `organization_id` from `ExecutionContext.tenantId` on every authenticated insert (when the target object declares the column).
|
|
8
|
+
- **Per-org seed replay** — every `sys_organization` insert triggers a copy of the app's demo dataset into the new org (via `seed-replayer`, or fallback `claimOrphanOrgRows` / `cloneOrgSeedData`).
|
|
9
|
+
- **Default-org bootstrap** — the first platform admin gets a `Default Organization` (slug `default`) bound as `owner` on `kernel:ready`, so the dashboard isn't empty after first sign-up.
|
|
10
|
+
|
|
11
|
+
Pair with [`@objectstack/plugin-security`](../plugin-security/README.md) for full multi-tenant RBAC + RLS + Field-Level Security. Standalone install gives a single-tenant deployment.
|
|
12
|
+
|
|
13
|
+
## Naming
|
|
14
|
+
|
|
15
|
+
The word "tenant" in ObjectStack means **physical** isolation (one Environment = one database, per ADR-0002 and `@objectstack/driver-turso`'s multi-tenant router). This plugin is about **logical** row-level scoping inside a single database — orthogonal to physical tenancy. Hence "org-scoping", not "multi-tenant".
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pnpm add @objectstack/plugin-org-scoping @objectstack/plugin-security
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Usage
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
import { OrgScopingPlugin } from '@objectstack/plugin-org-scoping';
|
|
27
|
+
import { SecurityPlugin } from '@objectstack/plugin-security';
|
|
28
|
+
|
|
29
|
+
// OrgScopingPlugin MUST be registered BEFORE SecurityPlugin — the
|
|
30
|
+
// latter probes `getService('org-scoping')` at start time to decide
|
|
31
|
+
// whether to keep wildcard `current_user.organization_id` RLS policies.
|
|
32
|
+
await kernel.use(new OrgScopingPlugin());
|
|
33
|
+
await kernel.use(new SecurityPlugin());
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Or via the `OS_MULTI_TENANT` env switch when using `@objectstack/runtime` / `@objectstack/plugin-dev`:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
OS_MULTI_TENANT=true objectstack serve
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Options
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
new OrgScopingPlugin({
|
|
46
|
+
ensureDefaultOrganization: true, // default — auto-create slug="default" for first admin
|
|
47
|
+
});
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Set `ensureDefaultOrganization: false` to fully self-manage onboarding via invitations / a custom UI.
|
|
51
|
+
|
|
52
|
+
## See also
|
|
53
|
+
|
|
54
|
+
- ADR-0002 — Physical multi-tenancy & driver-turso router
|
|
55
|
+
- `@objectstack/plugin-security` — RBAC, RLS, Field-Level Security
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { Plugin, PluginContext } from '@objectstack/core';
|
|
2
|
+
|
|
3
|
+
interface OrgScopingPluginOptions {
|
|
4
|
+
/**
|
|
5
|
+
* Whether to auto-create a `Default Organization` (slug `default`)
|
|
6
|
+
* and bind the first platform admin as `owner` when they have zero
|
|
7
|
+
* memberships. Set to `false` for deployments that fully self-manage
|
|
8
|
+
* org provisioning via invitation links or a custom onboarding flow.
|
|
9
|
+
*
|
|
10
|
+
* @default true
|
|
11
|
+
*/
|
|
12
|
+
ensureDefaultOrganization?: boolean;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* OrgScopingPlugin
|
|
16
|
+
*
|
|
17
|
+
* Makes `sys_organization` a first-class row-level isolation boundary:
|
|
18
|
+
*
|
|
19
|
+
* 1. **insert auto-stamp** — on every authenticated `insert` whose
|
|
20
|
+
* target object declares `organization_id`, fill the column from
|
|
21
|
+
* `ExecutionContext.tenantId`. Without this, freshly-created
|
|
22
|
+
* rows have `organization_id = NULL` and the default
|
|
23
|
+
* `tenant_isolation` RLS policy hides them from the very user
|
|
24
|
+
* who just created them.
|
|
25
|
+
*
|
|
26
|
+
* 2. **per-org seed replay** — after `sys_organization` insert, copy
|
|
27
|
+
* the artifact's demo seed data into the new org. Three paths
|
|
28
|
+
* (in order of preference):
|
|
29
|
+
* a. replay registered `seed-datasets` via the kernel-level
|
|
30
|
+
* `seed-replayer` callable (set by AppPlugin),
|
|
31
|
+
* b. for the FIRST org, `claimOrphanOrgRows` adopts any
|
|
32
|
+
* NULL-org rows a previous inline-seed may have inserted,
|
|
33
|
+
* c. for SUBSEQUENT orgs, `cloneOrgSeedData` shallow-clones
|
|
34
|
+
* rows from the very first org (donor-pattern).
|
|
35
|
+
*
|
|
36
|
+
* 3. **default-org bootstrap** — on `kernel:ready` and after every
|
|
37
|
+
* `sys_user_permission_set` insert, ensure the platform admin has
|
|
38
|
+
* a Default Organization to operate in (idempotent on slug
|
|
39
|
+
* `default` + admin's existing memberships).
|
|
40
|
+
*
|
|
41
|
+
* Why split from plugin-security:
|
|
42
|
+
* - plugin-security is a single-tenant-aware RBAC + RLS engine; it
|
|
43
|
+
* should not know about Organization-specific seed flows.
|
|
44
|
+
* - This plugin is purely opt-in: not installing it gives a
|
|
45
|
+
* single-tenant deployment (no `organization_id` injection, no
|
|
46
|
+
* per-org seed clone, no default-org bootstrap). plugin-security
|
|
47
|
+
* detects its presence via `getService('org-scoping')` and adjusts
|
|
48
|
+
* RLS policy stripping accordingly.
|
|
49
|
+
*
|
|
50
|
+
* Naming note: "org-scoping" deliberately avoids the word "tenant"
|
|
51
|
+
* because in ObjectStack "tenant" already means *physical isolation*
|
|
52
|
+
* (one Environment = one database, per ADR-0002 and driver-turso's
|
|
53
|
+
* multi-tenant router). This plugin is about LOGICAL row-level
|
|
54
|
+
* scoping inside a single database — orthogonal to physical tenancy.
|
|
55
|
+
*
|
|
56
|
+
* Dependencies:
|
|
57
|
+
* - `objectql` (engine middleware host)
|
|
58
|
+
*/
|
|
59
|
+
declare class OrgScopingPlugin implements Plugin {
|
|
60
|
+
name: string;
|
|
61
|
+
type: string;
|
|
62
|
+
version: string;
|
|
63
|
+
dependencies: string[];
|
|
64
|
+
/** Per-object field-name cache; same shape as SecurityPlugin's. */
|
|
65
|
+
private readonly fieldNamesCache;
|
|
66
|
+
private readonly opts;
|
|
67
|
+
constructor(options?: OrgScopingPluginOptions);
|
|
68
|
+
init(ctx: PluginContext): Promise<void>;
|
|
69
|
+
start(ctx: PluginContext): Promise<void>;
|
|
70
|
+
destroy(): Promise<void>;
|
|
71
|
+
/**
|
|
72
|
+
* Resolve the column-name set for an object (mirrors SecurityPlugin's
|
|
73
|
+
* loader so the two plugins behave consistently). Returns `null` if
|
|
74
|
+
* the schema can't be loaded — caller skips injection.
|
|
75
|
+
*/
|
|
76
|
+
private getObjectFieldNames;
|
|
77
|
+
private loadObjectFieldNames;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
interface ClaimOptions {
|
|
81
|
+
logger?: {
|
|
82
|
+
info: (message: string, meta?: Record<string, any>) => void;
|
|
83
|
+
warn: (message: string, meta?: Record<string, any>) => void;
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Assign every orphaned seed row to `organizationId`.
|
|
88
|
+
*
|
|
89
|
+
* Walks `ql.registry.getAllObjects()`, filters to schemas that
|
|
90
|
+
* (a) are not `managedBy` (skip sys_/auth/platform tables),
|
|
91
|
+
* (b) declare an `organization_id` field,
|
|
92
|
+
* and runs an `update(where: { organization_id: null }, patch: {
|
|
93
|
+
* organization_id: organizationId })` against each as `isSystem`.
|
|
94
|
+
*
|
|
95
|
+
* Returns a per-object summary `{ object, count }[]`.
|
|
96
|
+
*/
|
|
97
|
+
declare function claimOrphanOrgRows(ql: any, organizationId: string, options?: ClaimOptions): Promise<{
|
|
98
|
+
object: string;
|
|
99
|
+
count: number;
|
|
100
|
+
}[]>;
|
|
101
|
+
|
|
102
|
+
interface CloneOptions {
|
|
103
|
+
logger?: {
|
|
104
|
+
info: (message: string, meta?: Record<string, any>) => void;
|
|
105
|
+
warn: (message: string, meta?: Record<string, any>) => void;
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
declare function cloneOrgSeedData(ql: any, targetOrgId: string, options?: CloneOptions): Promise<{
|
|
109
|
+
object: string;
|
|
110
|
+
count: number;
|
|
111
|
+
}[]>;
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* ensureDefaultOrganization — multi-tenant bootstrap helper.
|
|
115
|
+
*
|
|
116
|
+
* In multi-tenant deployments the freshly-promoted platform admin
|
|
117
|
+
* (`admin_full_access` granted with `organization_id IS NULL`) needs
|
|
118
|
+
* at least one `sys_organization` to carry an `activeOrganizationId`
|
|
119
|
+
* on their session. Without it, the default `tenant_isolation` RLS
|
|
120
|
+
* policy filters everything to zero rows and the admin sees an empty
|
|
121
|
+
* console even though they have full access.
|
|
122
|
+
*
|
|
123
|
+
* Strategy (idempotent, run on `kernel:ready` and after every
|
|
124
|
+
* `sys_user_permission_set` insert):
|
|
125
|
+
*
|
|
126
|
+
* 1. Find the platform admin (oldest `sys_user_permission_set` row
|
|
127
|
+
* with `permission_set_id = admin_full_access` and
|
|
128
|
+
* `organization_id IS NULL`). If none, no-op.
|
|
129
|
+
* 2. If that user already has any `sys_member` row, no-op (they
|
|
130
|
+
* either created their own org or were invited into one — we
|
|
131
|
+
* respect that and never auto-create a "Default Organization"
|
|
132
|
+
* behind their back).
|
|
133
|
+
* 3. Re-use a pre-existing `slug='default'` org if present;
|
|
134
|
+
* otherwise create one. Stable slug keeps human-readable URLs
|
|
135
|
+
* predictable across cold-boots.
|
|
136
|
+
* 4. Insert a `sys_member { role: 'owner' }` linking the admin to
|
|
137
|
+
* the default org.
|
|
138
|
+
*
|
|
139
|
+
* This is the ONLY framework-side auto-provisioning of an org.
|
|
140
|
+
* Subsequent users must accept an invitation or explicitly create
|
|
141
|
+
* their first organization — `claimOrphanOrgRows` / `cloneOrgSeedData`
|
|
142
|
+
* handle the seed-data side for those flows.
|
|
143
|
+
*/
|
|
144
|
+
interface EnsureOptions {
|
|
145
|
+
logger?: {
|
|
146
|
+
info: (message: string, meta?: Record<string, any>) => void;
|
|
147
|
+
warn: (message: string, meta?: Record<string, any>) => void;
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
interface EnsureDefaultOrganizationResult {
|
|
151
|
+
/** Whether a brand-new org row was inserted (vs. re-using slug=default). */
|
|
152
|
+
defaultOrgCreated: boolean;
|
|
153
|
+
/** Resolved (or freshly minted) default-org id; undefined when no admin exists yet. */
|
|
154
|
+
defaultOrgId?: string;
|
|
155
|
+
/** Whether a sys_member row was inserted binding the admin to the default org. */
|
|
156
|
+
memberCreated: boolean;
|
|
157
|
+
/** Human-readable reason when the helper short-circuited. */
|
|
158
|
+
reason?: 'no_admin' | 'admin_already_in_org' | 'org_insert_failed' | 'member_insert_failed';
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Ensure the platform admin has a Default Organization to operate in.
|
|
162
|
+
* Safe to call multiple times — idempotent on stable slug `default`
|
|
163
|
+
* and on the presence of any existing `sys_member` row for the admin.
|
|
164
|
+
*/
|
|
165
|
+
declare function ensureDefaultOrganization(ql: any, options?: EnsureOptions): Promise<EnsureDefaultOrganizationResult>;
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Canonical plugin-org-scoping manifest source.
|
|
169
|
+
*
|
|
170
|
+
* Imported by `objectstack.config.ts` (compile-time) and
|
|
171
|
+
* `org-scoping-plugin.ts` (runtime `manifest.register`) so the two
|
|
172
|
+
* registration paths cannot drift.
|
|
173
|
+
*/
|
|
174
|
+
declare const ORG_SCOPING_PLUGIN_ID = "com.objectstack.plugin-org-scoping";
|
|
175
|
+
declare const ORG_SCOPING_PLUGIN_VERSION = "1.0.0";
|
|
176
|
+
/** This plugin owns no `sys_*` objects — Organization itself lives in `@objectstack/platform-objects`. */
|
|
177
|
+
declare const orgScopingObjects: readonly [];
|
|
178
|
+
/** Manifest header shared by compile-time config and runtime registration. */
|
|
179
|
+
declare const orgScopingPluginManifestHeader: {
|
|
180
|
+
id: string;
|
|
181
|
+
namespace: string;
|
|
182
|
+
version: string;
|
|
183
|
+
type: "plugin";
|
|
184
|
+
scope: "system";
|
|
185
|
+
defaultDatasource: string;
|
|
186
|
+
name: string;
|
|
187
|
+
description: string;
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
export { type EnsureDefaultOrganizationResult, ORG_SCOPING_PLUGIN_ID, ORG_SCOPING_PLUGIN_VERSION, OrgScopingPlugin, type OrgScopingPluginOptions, claimOrphanOrgRows, cloneOrgSeedData, ensureDefaultOrganization, orgScopingObjects, orgScopingPluginManifestHeader };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { Plugin, PluginContext } from '@objectstack/core';
|
|
2
|
+
|
|
3
|
+
interface OrgScopingPluginOptions {
|
|
4
|
+
/**
|
|
5
|
+
* Whether to auto-create a `Default Organization` (slug `default`)
|
|
6
|
+
* and bind the first platform admin as `owner` when they have zero
|
|
7
|
+
* memberships. Set to `false` for deployments that fully self-manage
|
|
8
|
+
* org provisioning via invitation links or a custom onboarding flow.
|
|
9
|
+
*
|
|
10
|
+
* @default true
|
|
11
|
+
*/
|
|
12
|
+
ensureDefaultOrganization?: boolean;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* OrgScopingPlugin
|
|
16
|
+
*
|
|
17
|
+
* Makes `sys_organization` a first-class row-level isolation boundary:
|
|
18
|
+
*
|
|
19
|
+
* 1. **insert auto-stamp** — on every authenticated `insert` whose
|
|
20
|
+
* target object declares `organization_id`, fill the column from
|
|
21
|
+
* `ExecutionContext.tenantId`. Without this, freshly-created
|
|
22
|
+
* rows have `organization_id = NULL` and the default
|
|
23
|
+
* `tenant_isolation` RLS policy hides them from the very user
|
|
24
|
+
* who just created them.
|
|
25
|
+
*
|
|
26
|
+
* 2. **per-org seed replay** — after `sys_organization` insert, copy
|
|
27
|
+
* the artifact's demo seed data into the new org. Three paths
|
|
28
|
+
* (in order of preference):
|
|
29
|
+
* a. replay registered `seed-datasets` via the kernel-level
|
|
30
|
+
* `seed-replayer` callable (set by AppPlugin),
|
|
31
|
+
* b. for the FIRST org, `claimOrphanOrgRows` adopts any
|
|
32
|
+
* NULL-org rows a previous inline-seed may have inserted,
|
|
33
|
+
* c. for SUBSEQUENT orgs, `cloneOrgSeedData` shallow-clones
|
|
34
|
+
* rows from the very first org (donor-pattern).
|
|
35
|
+
*
|
|
36
|
+
* 3. **default-org bootstrap** — on `kernel:ready` and after every
|
|
37
|
+
* `sys_user_permission_set` insert, ensure the platform admin has
|
|
38
|
+
* a Default Organization to operate in (idempotent on slug
|
|
39
|
+
* `default` + admin's existing memberships).
|
|
40
|
+
*
|
|
41
|
+
* Why split from plugin-security:
|
|
42
|
+
* - plugin-security is a single-tenant-aware RBAC + RLS engine; it
|
|
43
|
+
* should not know about Organization-specific seed flows.
|
|
44
|
+
* - This plugin is purely opt-in: not installing it gives a
|
|
45
|
+
* single-tenant deployment (no `organization_id` injection, no
|
|
46
|
+
* per-org seed clone, no default-org bootstrap). plugin-security
|
|
47
|
+
* detects its presence via `getService('org-scoping')` and adjusts
|
|
48
|
+
* RLS policy stripping accordingly.
|
|
49
|
+
*
|
|
50
|
+
* Naming note: "org-scoping" deliberately avoids the word "tenant"
|
|
51
|
+
* because in ObjectStack "tenant" already means *physical isolation*
|
|
52
|
+
* (one Environment = one database, per ADR-0002 and driver-turso's
|
|
53
|
+
* multi-tenant router). This plugin is about LOGICAL row-level
|
|
54
|
+
* scoping inside a single database — orthogonal to physical tenancy.
|
|
55
|
+
*
|
|
56
|
+
* Dependencies:
|
|
57
|
+
* - `objectql` (engine middleware host)
|
|
58
|
+
*/
|
|
59
|
+
declare class OrgScopingPlugin implements Plugin {
|
|
60
|
+
name: string;
|
|
61
|
+
type: string;
|
|
62
|
+
version: string;
|
|
63
|
+
dependencies: string[];
|
|
64
|
+
/** Per-object field-name cache; same shape as SecurityPlugin's. */
|
|
65
|
+
private readonly fieldNamesCache;
|
|
66
|
+
private readonly opts;
|
|
67
|
+
constructor(options?: OrgScopingPluginOptions);
|
|
68
|
+
init(ctx: PluginContext): Promise<void>;
|
|
69
|
+
start(ctx: PluginContext): Promise<void>;
|
|
70
|
+
destroy(): Promise<void>;
|
|
71
|
+
/**
|
|
72
|
+
* Resolve the column-name set for an object (mirrors SecurityPlugin's
|
|
73
|
+
* loader so the two plugins behave consistently). Returns `null` if
|
|
74
|
+
* the schema can't be loaded — caller skips injection.
|
|
75
|
+
*/
|
|
76
|
+
private getObjectFieldNames;
|
|
77
|
+
private loadObjectFieldNames;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
interface ClaimOptions {
|
|
81
|
+
logger?: {
|
|
82
|
+
info: (message: string, meta?: Record<string, any>) => void;
|
|
83
|
+
warn: (message: string, meta?: Record<string, any>) => void;
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Assign every orphaned seed row to `organizationId`.
|
|
88
|
+
*
|
|
89
|
+
* Walks `ql.registry.getAllObjects()`, filters to schemas that
|
|
90
|
+
* (a) are not `managedBy` (skip sys_/auth/platform tables),
|
|
91
|
+
* (b) declare an `organization_id` field,
|
|
92
|
+
* and runs an `update(where: { organization_id: null }, patch: {
|
|
93
|
+
* organization_id: organizationId })` against each as `isSystem`.
|
|
94
|
+
*
|
|
95
|
+
* Returns a per-object summary `{ object, count }[]`.
|
|
96
|
+
*/
|
|
97
|
+
declare function claimOrphanOrgRows(ql: any, organizationId: string, options?: ClaimOptions): Promise<{
|
|
98
|
+
object: string;
|
|
99
|
+
count: number;
|
|
100
|
+
}[]>;
|
|
101
|
+
|
|
102
|
+
interface CloneOptions {
|
|
103
|
+
logger?: {
|
|
104
|
+
info: (message: string, meta?: Record<string, any>) => void;
|
|
105
|
+
warn: (message: string, meta?: Record<string, any>) => void;
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
declare function cloneOrgSeedData(ql: any, targetOrgId: string, options?: CloneOptions): Promise<{
|
|
109
|
+
object: string;
|
|
110
|
+
count: number;
|
|
111
|
+
}[]>;
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* ensureDefaultOrganization — multi-tenant bootstrap helper.
|
|
115
|
+
*
|
|
116
|
+
* In multi-tenant deployments the freshly-promoted platform admin
|
|
117
|
+
* (`admin_full_access` granted with `organization_id IS NULL`) needs
|
|
118
|
+
* at least one `sys_organization` to carry an `activeOrganizationId`
|
|
119
|
+
* on their session. Without it, the default `tenant_isolation` RLS
|
|
120
|
+
* policy filters everything to zero rows and the admin sees an empty
|
|
121
|
+
* console even though they have full access.
|
|
122
|
+
*
|
|
123
|
+
* Strategy (idempotent, run on `kernel:ready` and after every
|
|
124
|
+
* `sys_user_permission_set` insert):
|
|
125
|
+
*
|
|
126
|
+
* 1. Find the platform admin (oldest `sys_user_permission_set` row
|
|
127
|
+
* with `permission_set_id = admin_full_access` and
|
|
128
|
+
* `organization_id IS NULL`). If none, no-op.
|
|
129
|
+
* 2. If that user already has any `sys_member` row, no-op (they
|
|
130
|
+
* either created their own org or were invited into one — we
|
|
131
|
+
* respect that and never auto-create a "Default Organization"
|
|
132
|
+
* behind their back).
|
|
133
|
+
* 3. Re-use a pre-existing `slug='default'` org if present;
|
|
134
|
+
* otherwise create one. Stable slug keeps human-readable URLs
|
|
135
|
+
* predictable across cold-boots.
|
|
136
|
+
* 4. Insert a `sys_member { role: 'owner' }` linking the admin to
|
|
137
|
+
* the default org.
|
|
138
|
+
*
|
|
139
|
+
* This is the ONLY framework-side auto-provisioning of an org.
|
|
140
|
+
* Subsequent users must accept an invitation or explicitly create
|
|
141
|
+
* their first organization — `claimOrphanOrgRows` / `cloneOrgSeedData`
|
|
142
|
+
* handle the seed-data side for those flows.
|
|
143
|
+
*/
|
|
144
|
+
interface EnsureOptions {
|
|
145
|
+
logger?: {
|
|
146
|
+
info: (message: string, meta?: Record<string, any>) => void;
|
|
147
|
+
warn: (message: string, meta?: Record<string, any>) => void;
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
interface EnsureDefaultOrganizationResult {
|
|
151
|
+
/** Whether a brand-new org row was inserted (vs. re-using slug=default). */
|
|
152
|
+
defaultOrgCreated: boolean;
|
|
153
|
+
/** Resolved (or freshly minted) default-org id; undefined when no admin exists yet. */
|
|
154
|
+
defaultOrgId?: string;
|
|
155
|
+
/** Whether a sys_member row was inserted binding the admin to the default org. */
|
|
156
|
+
memberCreated: boolean;
|
|
157
|
+
/** Human-readable reason when the helper short-circuited. */
|
|
158
|
+
reason?: 'no_admin' | 'admin_already_in_org' | 'org_insert_failed' | 'member_insert_failed';
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Ensure the platform admin has a Default Organization to operate in.
|
|
162
|
+
* Safe to call multiple times — idempotent on stable slug `default`
|
|
163
|
+
* and on the presence of any existing `sys_member` row for the admin.
|
|
164
|
+
*/
|
|
165
|
+
declare function ensureDefaultOrganization(ql: any, options?: EnsureOptions): Promise<EnsureDefaultOrganizationResult>;
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Canonical plugin-org-scoping manifest source.
|
|
169
|
+
*
|
|
170
|
+
* Imported by `objectstack.config.ts` (compile-time) and
|
|
171
|
+
* `org-scoping-plugin.ts` (runtime `manifest.register`) so the two
|
|
172
|
+
* registration paths cannot drift.
|
|
173
|
+
*/
|
|
174
|
+
declare const ORG_SCOPING_PLUGIN_ID = "com.objectstack.plugin-org-scoping";
|
|
175
|
+
declare const ORG_SCOPING_PLUGIN_VERSION = "1.0.0";
|
|
176
|
+
/** This plugin owns no `sys_*` objects — Organization itself lives in `@objectstack/platform-objects`. */
|
|
177
|
+
declare const orgScopingObjects: readonly [];
|
|
178
|
+
/** Manifest header shared by compile-time config and runtime registration. */
|
|
179
|
+
declare const orgScopingPluginManifestHeader: {
|
|
180
|
+
id: string;
|
|
181
|
+
namespace: string;
|
|
182
|
+
version: string;
|
|
183
|
+
type: "plugin";
|
|
184
|
+
scope: "system";
|
|
185
|
+
defaultDatasource: string;
|
|
186
|
+
name: string;
|
|
187
|
+
description: string;
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
export { type EnsureDefaultOrganizationResult, ORG_SCOPING_PLUGIN_ID, ORG_SCOPING_PLUGIN_VERSION, OrgScopingPlugin, type OrgScopingPluginOptions, claimOrphanOrgRows, cloneOrgSeedData, ensureDefaultOrganization, orgScopingObjects, orgScopingPluginManifestHeader };
|