@objectstack/plugin-org-scoping 9.9.1 → 9.10.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/dist/index.d.mts CHANGED
@@ -99,6 +99,26 @@ declare function claimOrphanOrgRows(ql: any, organizationId: string, options?: C
99
99
  count: number;
100
100
  }[]>;
101
101
 
102
+ interface ClaimOwnershipOptions {
103
+ logger?: {
104
+ info: (message: string, meta?: Record<string, any>) => void;
105
+ warn: (message: string, meta?: Record<string, any>) => void;
106
+ };
107
+ }
108
+ /**
109
+ * Assign `owner_id = ownerUserId` to every NULL-owned seed row of `organizationId`.
110
+ *
111
+ * Walks `ql.registry.getAllObjects()`, filters to schemas that
112
+ * (a) are not `managedBy` (skip sys_/auth/platform tables),
113
+ * (b) are not `sys_*`-namespaced,
114
+ * (c) declare BOTH `owner_id` and `organization_id`,
115
+ * and updates the org's unowned rows as `isSystem`. Returns a per-object summary.
116
+ */
117
+ declare function claimOrgSeedOwnership(ql: any, organizationId: string, ownerUserId: string, options?: ClaimOwnershipOptions): Promise<{
118
+ object: string;
119
+ count: number;
120
+ }[]>;
121
+
102
122
  interface CloneOptions {
103
123
  logger?: {
104
124
  info: (message: string, meta?: Record<string, any>) => void;
@@ -110,37 +130,6 @@ declare function cloneOrgSeedData(ql: any, targetOrgId: string, options?: CloneO
110
130
  count: number;
111
131
  }[]>;
112
132
 
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
133
  interface EnsureOptions {
145
134
  logger?: {
146
135
  info: (message: string, meta?: Record<string, any>) => void;
@@ -156,6 +145,8 @@ interface EnsureDefaultOrganizationResult {
156
145
  memberCreated: boolean;
157
146
  /** Human-readable reason when the helper short-circuited. */
158
147
  reason?: 'no_admin' | 'admin_already_in_org' | 'org_insert_failed' | 'member_insert_failed';
148
+ /** Count of the default org's seeded rows re-owned to the platform admin. */
149
+ ownershipClaimed?: number;
159
150
  }
160
151
  /**
161
152
  * Ensure the platform admin has a Default Organization to operate in.
@@ -187,4 +178,4 @@ declare const orgScopingPluginManifestHeader: {
187
178
  description: string;
188
179
  };
189
180
 
190
- export { type EnsureDefaultOrganizationResult, ORG_SCOPING_PLUGIN_ID, ORG_SCOPING_PLUGIN_VERSION, OrgScopingPlugin, type OrgScopingPluginOptions, claimOrphanOrgRows, cloneOrgSeedData, ensureDefaultOrganization, orgScopingObjects, orgScopingPluginManifestHeader };
181
+ export { type EnsureDefaultOrganizationResult, ORG_SCOPING_PLUGIN_ID, ORG_SCOPING_PLUGIN_VERSION, OrgScopingPlugin, type OrgScopingPluginOptions, claimOrgSeedOwnership, claimOrphanOrgRows, cloneOrgSeedData, ensureDefaultOrganization, orgScopingObjects, orgScopingPluginManifestHeader };
package/dist/index.d.ts CHANGED
@@ -99,6 +99,26 @@ declare function claimOrphanOrgRows(ql: any, organizationId: string, options?: C
99
99
  count: number;
100
100
  }[]>;
101
101
 
102
+ interface ClaimOwnershipOptions {
103
+ logger?: {
104
+ info: (message: string, meta?: Record<string, any>) => void;
105
+ warn: (message: string, meta?: Record<string, any>) => void;
106
+ };
107
+ }
108
+ /**
109
+ * Assign `owner_id = ownerUserId` to every NULL-owned seed row of `organizationId`.
110
+ *
111
+ * Walks `ql.registry.getAllObjects()`, filters to schemas that
112
+ * (a) are not `managedBy` (skip sys_/auth/platform tables),
113
+ * (b) are not `sys_*`-namespaced,
114
+ * (c) declare BOTH `owner_id` and `organization_id`,
115
+ * and updates the org's unowned rows as `isSystem`. Returns a per-object summary.
116
+ */
117
+ declare function claimOrgSeedOwnership(ql: any, organizationId: string, ownerUserId: string, options?: ClaimOwnershipOptions): Promise<{
118
+ object: string;
119
+ count: number;
120
+ }[]>;
121
+
102
122
  interface CloneOptions {
103
123
  logger?: {
104
124
  info: (message: string, meta?: Record<string, any>) => void;
@@ -110,37 +130,6 @@ declare function cloneOrgSeedData(ql: any, targetOrgId: string, options?: CloneO
110
130
  count: number;
111
131
  }[]>;
112
132
 
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
133
  interface EnsureOptions {
145
134
  logger?: {
146
135
  info: (message: string, meta?: Record<string, any>) => void;
@@ -156,6 +145,8 @@ interface EnsureDefaultOrganizationResult {
156
145
  memberCreated: boolean;
157
146
  /** Human-readable reason when the helper short-circuited. */
158
147
  reason?: 'no_admin' | 'admin_already_in_org' | 'org_insert_failed' | 'member_insert_failed';
148
+ /** Count of the default org's seeded rows re-owned to the platform admin. */
149
+ ownershipClaimed?: number;
159
150
  }
160
151
  /**
161
152
  * Ensure the platform admin has a Default Organization to operate in.
@@ -187,4 +178,4 @@ declare const orgScopingPluginManifestHeader: {
187
178
  description: string;
188
179
  };
189
180
 
190
- export { type EnsureDefaultOrganizationResult, ORG_SCOPING_PLUGIN_ID, ORG_SCOPING_PLUGIN_VERSION, OrgScopingPlugin, type OrgScopingPluginOptions, claimOrphanOrgRows, cloneOrgSeedData, ensureDefaultOrganization, orgScopingObjects, orgScopingPluginManifestHeader };
181
+ export { type EnsureDefaultOrganizationResult, ORG_SCOPING_PLUGIN_ID, ORG_SCOPING_PLUGIN_VERSION, OrgScopingPlugin, type OrgScopingPluginOptions, claimOrgSeedOwnership, claimOrphanOrgRows, cloneOrgSeedData, ensureDefaultOrganization, orgScopingObjects, orgScopingPluginManifestHeader };
package/dist/index.js CHANGED
@@ -23,6 +23,7 @@ __export(index_exports, {
23
23
  ORG_SCOPING_PLUGIN_ID: () => ORG_SCOPING_PLUGIN_ID,
24
24
  ORG_SCOPING_PLUGIN_VERSION: () => ORG_SCOPING_PLUGIN_VERSION,
25
25
  OrgScopingPlugin: () => OrgScopingPlugin,
26
+ claimOrgSeedOwnership: () => claimOrgSeedOwnership,
26
27
  claimOrphanOrgRows: () => claimOrphanOrgRows,
27
28
  cloneOrgSeedData: () => cloneOrgSeedData,
28
29
  ensureDefaultOrganization: () => ensureDefaultOrganization,
@@ -277,11 +278,71 @@ async function cloneOrgSeedData(ql, targetOrgId, options = {}) {
277
278
  return summary;
278
279
  }
279
280
 
280
- // src/ensure-default-organization.ts
281
+ // src/claim-org-seed-ownership.ts
281
282
  var SYSTEM_CTX3 = { isSystem: true };
283
+ function hasField(schema, field) {
284
+ const fields = schema?.fields;
285
+ if (!fields) return false;
286
+ if (Array.isArray(fields)) return fields.some((f) => f?.name === field);
287
+ return Object.prototype.hasOwnProperty.call(fields, field);
288
+ }
289
+ async function claimOrgSeedOwnership(ql, organizationId, ownerUserId, options = {}) {
290
+ const logger = options.logger;
291
+ if (!organizationId || !ownerUserId) return [];
292
+ if (!ql || typeof ql.update !== "function" || typeof ql.find !== "function") return [];
293
+ const registry = ql.registry;
294
+ if (!registry || typeof registry.getAllObjects !== "function") {
295
+ logger?.warn?.("[org-scoping] claimOrgSeedOwnership: registry unavailable");
296
+ return [];
297
+ }
298
+ const schemas = registry.getAllObjects();
299
+ const results = [];
300
+ for (const schema of schemas) {
301
+ if (!schema?.name) continue;
302
+ if (schema.managedBy) continue;
303
+ if (schema.name.startsWith("sys_")) continue;
304
+ if (!hasField(schema, "owner_id") || !hasField(schema, "organization_id")) continue;
305
+ try {
306
+ const orphans = await ql.find(
307
+ schema.name,
308
+ { where: { organization_id: organizationId, owner_id: null }, limit: 1e4, fields: ["id"] },
309
+ { context: SYSTEM_CTX3 }
310
+ );
311
+ const list = Array.isArray(orphans) ? orphans : Array.isArray(orphans?.records) ? orphans.records : [];
312
+ if (list.length === 0) continue;
313
+ let updated = 0;
314
+ for (const row of list) {
315
+ if (!row?.id) continue;
316
+ try {
317
+ await ql.update(schema.name, { id: row.id, owner_id: ownerUserId }, { context: SYSTEM_CTX3 });
318
+ updated += 1;
319
+ } catch (e) {
320
+ logger?.warn?.(`[org-scoping] claimOrgSeedOwnership failed for ${schema.name}:${row.id}`, {
321
+ error: e.message
322
+ });
323
+ }
324
+ }
325
+ if (updated > 0) results.push({ object: schema.name, count: updated });
326
+ } catch (e) {
327
+ logger?.warn?.(`[org-scoping] claimOrgSeedOwnership scan failed for ${schema.name}`, {
328
+ error: e.message
329
+ });
330
+ }
331
+ }
332
+ if (results.length > 0) {
333
+ const total = results.reduce((s, r) => s + r.count, 0);
334
+ logger?.info?.(`[org-scoping] handed ${total} seeded row(s) of org ${organizationId} to owner ${ownerUserId}`, {
335
+ breakdown: results
336
+ });
337
+ }
338
+ return results;
339
+ }
340
+
341
+ // src/ensure-default-organization.ts
342
+ var SYSTEM_CTX4 = { isSystem: true };
282
343
  async function tryFind(ql, object, where, limit = 100) {
283
344
  try {
284
- const rows = await ql.find(object, { where, limit }, { context: SYSTEM_CTX3 });
345
+ const rows = await ql.find(object, { where, limit }, { context: SYSTEM_CTX4 });
285
346
  return Array.isArray(rows) ? rows : Array.isArray(rows?.records) ? rows.records : [];
286
347
  } catch {
287
348
  return [];
@@ -289,7 +350,7 @@ async function tryFind(ql, object, where, limit = 100) {
289
350
  }
290
351
  async function tryInsert(ql, object, data) {
291
352
  try {
292
- return await ql.insert(object, data, { context: SYSTEM_CTX3 });
353
+ return await ql.insert(object, data, { context: SYSTEM_CTX4 });
293
354
  } catch {
294
355
  return null;
295
356
  }
@@ -375,7 +436,18 @@ async function ensureDefaultOrganization(ql, options = {}) {
375
436
  `[org-scoping] bound platform admin to default organization (${defaultOrgId})`,
376
437
  { userId: adminUserId, defaultOrgId }
377
438
  );
378
- return { defaultOrgCreated, defaultOrgId, memberCreated: true };
439
+ let ownershipClaimed = 0;
440
+ if (defaultOrgId) {
441
+ try {
442
+ const claims = await claimOrgSeedOwnership(ql, defaultOrgId, adminUserId, { logger });
443
+ ownershipClaimed = claims.reduce((s, c) => s + c.count, 0);
444
+ } catch (e) {
445
+ logger?.warn?.("[org-scoping] default-org seed ownership handoff failed", {
446
+ error: e.message
447
+ });
448
+ }
449
+ }
450
+ return { defaultOrgCreated, defaultOrgId, memberCreated: true, ownershipClaimed };
379
451
  }
380
452
 
381
453
  // src/manifest.ts
@@ -617,6 +689,7 @@ var OrgScopingPlugin = class {
617
689
  ORG_SCOPING_PLUGIN_ID,
618
690
  ORG_SCOPING_PLUGIN_VERSION,
619
691
  OrgScopingPlugin,
692
+ claimOrgSeedOwnership,
620
693
  claimOrphanOrgRows,
621
694
  cloneOrgSeedData,
622
695
  ensureDefaultOrganization,
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/claim-orphan-org-rows.ts","../src/clone-org-seed-data.ts","../src/ensure-default-organization.ts","../src/manifest.ts","../src/org-scoping-plugin.ts"],"sourcesContent":["// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * @objectstack/plugin-org-scoping\n *\n * Row-level Organization isolation for ObjectStack:\n * - auto-stamps `organization_id` on insert from\n * `ExecutionContext.tenantId`,\n * - replays seed datasets (or clones from the donor org) on every\n * `sys_organization` insert,\n * - bootstraps a Default Organization for the first platform admin.\n *\n * Pair with `@objectstack/plugin-security` to get full multi-tenant\n * RBAC + RLS + Field-Level Security. Install standalone for\n * single-tenant deployments — plugin-security detects this plugin's\n * presence via `getService('org-scoping')` and adjusts wildcard\n * tenant policy handling accordingly.\n */\n\nexport { OrgScopingPlugin } from './org-scoping-plugin.js';\nexport type { OrgScopingPluginOptions } from './org-scoping-plugin.js';\nexport { claimOrphanOrgRows } from './claim-orphan-org-rows.js';\nexport { cloneOrgSeedData } from './clone-org-seed-data.js';\nexport {\n ensureDefaultOrganization,\n type EnsureDefaultOrganizationResult,\n} from './ensure-default-organization.js';\nexport {\n orgScopingObjects,\n orgScopingPluginManifestHeader,\n ORG_SCOPING_PLUGIN_ID,\n ORG_SCOPING_PLUGIN_VERSION,\n} from './manifest.js';\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * claimOrphanOrgRows — assign seed-loaded records to the first organization.\n *\n * Seeds (`defineSeed`) are inserted by `SeedLoaderService` using\n * `{ context: { isSystem: true } }`, which intentionally bypasses\n * SecurityPlugin's `organization_id` auto-fill. As a result, in\n * multi-tenant mode every seed row lands with `organization_id = NULL`.\n *\n * That's correct for **cross-tenant metadata** — `sys_permission_set`\n * rows, default roles, etc. (objects whose schema has `managedBy` set)\n * — but for **business-domain seeds** (CRM `lead`, `account`, `contact`,\n * …) it means the rows are invisible to anyone bound to an organization\n * (the default `tenant_isolation` RLS policy\n * `organization_id = current_user.organization_id` filters them out).\n *\n * This helper runs **once**, on first-organization creation, and\n * back-fills `organization_id` on every orphaned (`organization_id IS\n * NULL`) seed row of every user-defined object that declares the\n * column. Result: out of the box, the freshly registered owner sees the\n * shipped demo data scoped to their first org — no manual claim step.\n *\n * Idempotent: a no-op once an organization-tagged row exists, and\n * `managedBy` schemas (`sys_*` better-auth/platform tables) are always\n * skipped so cross-tenant defaults stay cross-tenant.\n */\n\nimport type { ServiceObject } from '@objectstack/spec/data';\n\ninterface ClaimOptions {\n logger?: {\n info: (message: string, meta?: Record<string, any>) => void;\n warn: (message: string, meta?: Record<string, any>) => void;\n };\n}\n\nconst SYSTEM_CTX = { isSystem: true };\n\nfunction hasOrganizationField(schema: ServiceObject): boolean {\n const fields: any = (schema as any)?.fields;\n if (!fields) return false;\n if (Array.isArray(fields)) {\n return fields.some((f) => f?.name === 'organization_id');\n }\n return Object.prototype.hasOwnProperty.call(fields, 'organization_id');\n}\n\n/**\n * Assign every orphaned seed row to `organizationId`.\n *\n * Walks `ql.registry.getAllObjects()`, filters to schemas that\n * (a) are not `managedBy` (skip sys_/auth/platform tables),\n * (b) declare an `organization_id` field,\n * and runs an `update(where: { organization_id: null }, patch: {\n * organization_id: organizationId })` against each as `isSystem`.\n *\n * Returns a per-object summary `{ object, count }[]`.\n */\nexport async function claimOrphanOrgRows(\n ql: any,\n organizationId: string,\n options: ClaimOptions = {},\n): Promise<{ object: string; count: number }[]> {\n const logger = options.logger;\n if (!ql || typeof ql.update !== 'function' || typeof ql.find !== 'function') {\n return [];\n }\n const registry = (ql as any).registry;\n if (!registry || typeof registry.getAllObjects !== 'function') {\n logger?.warn?.('[org-scoping] claimOrphanOrgRows: registry unavailable');\n return [];\n }\n\n const schemas: ServiceObject[] = registry.getAllObjects();\n const results: { object: string; count: number }[] = [];\n\n for (const schema of schemas) {\n if (!schema?.name) continue;\n if ((schema as any).managedBy) continue;\n // Defense in depth: any platform-namespaced object (`sys_*`) is\n // off-limits for tenant claim regardless of `managedBy`. Platform\n // tables that should be tenant-scoped are inserted with an explicit\n // `organization_id` by the code that owns them, so they will never\n // be orphans here.\n if (schema.name.startsWith('sys_')) continue;\n if (!hasOrganizationField(schema)) continue;\n\n try {\n const orphans = await ql.find(\n schema.name,\n { where: { organization_id: null }, limit: 10_000, fields: ['id'] },\n { context: SYSTEM_CTX },\n );\n const list: any[] = Array.isArray(orphans)\n ? orphans\n : Array.isArray(orphans?.records)\n ? orphans.records\n : [];\n if (list.length === 0) continue;\n\n let updated = 0;\n for (const row of list) {\n if (!row?.id) continue;\n try {\n await ql.update(\n schema.name,\n { id: row.id, organization_id: organizationId },\n { context: SYSTEM_CTX },\n );\n updated += 1;\n } catch (e) {\n logger?.warn?.(`[org-scoping] claim failed for ${schema.name}:${row.id}`, {\n error: (e as Error).message,\n });\n }\n }\n if (updated > 0) {\n results.push({ object: schema.name, count: updated });\n }\n } catch (e) {\n logger?.warn?.(`[org-scoping] claim scan failed for ${schema.name}`, {\n error: (e as Error).message,\n });\n }\n }\n\n if (results.length > 0) {\n const total = results.reduce((s, r) => s + r.count, 0);\n logger?.info?.(`[org-scoping] claimed ${total} orphan seed row(s) for organization ${organizationId}`, {\n breakdown: results,\n });\n }\n return results;\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * cloneOrgSeedData — give every newly-registered org its own copy of\n * the demo seed data.\n *\n * Multi-tenant deployments treat each `sys_organization` as a hard\n * isolation boundary. The platform-wide `claimOrphanOrgRows` hook\n * (see `claim-orphan-tenant-rows.ts`) only fires for the very first\n * org — every subsequent org (created explicitly by a user via\n * `createOrganization`, or by an admin from the console) starts\n * empty. For demo / trial-org UX (Salesforce-style \"you get a\n * fully populated sandbox on signup\"), we want every freshly minted\n * org to receive a private clone of the platform-first org's\n * user-defined data.\n *\n * Strategy:\n * 1. Pick the donor org — the very first `sys_organization`.\n * 2. Walk `ql.registry.getAllObjects()` once to collect schemas\n * that are user-defined (not `managedBy`, not `sys_*`) AND\n * declare an `organization_id` field.\n * 3. Pass A — for each donor object, find rows where\n * `organization_id = donorOrgId`, generate a new id, insert a\n * shallow copy under `targetOrgId`, recording an\n * `oldId → newId` map keyed by object name. Lookup field values\n * pointing at donor rows are left untouched in this pass; the\n * remap happens in pass B so we don't depend on topological\n * ordering of inserts.\n * 4. Pass B — for each cloned row, walk its lookup-shaped fields\n * and rewrite values that match the donor map for the field's\n * `reference` object.\n *\n * Idempotent: skipped if the target org already has rows in any\n * cloned object, or if no donor org exists, or if the target IS the\n * donor (claim hook handles the donor itself).\n *\n * Best-effort: per-object failures are logged at `warn` and don't\n * abort the rest of the clone. FK fields that reference an object\n * that wasn't cloned (e.g. the lookup target lives in `sys_*`, or\n * the remap key isn't present) are left as-is — broken refs are\n * preferable to losing whole rows.\n */\n\nimport type { ServiceObject } from '@objectstack/spec/data';\n\ninterface CloneOptions {\n logger?: {\n info: (message: string, meta?: Record<string, any>) => void;\n warn: (message: string, meta?: Record<string, any>) => void;\n };\n}\n\ninterface FieldDescriptor {\n name: string;\n type?: string;\n reference?: string;\n multiple?: boolean;\n unique?: boolean;\n}\n\nconst SYSTEM_CTX = { isSystem: true };\n\nconst SKIP_COPY_FIELDS = new Set<string>([\n 'id',\n 'created_at',\n 'updated_at',\n 'organization_id',\n]);\n\n// Computed / virtual / system-managed field types — these have no\n// physical column in the DB, so re-inserting them would fail with\n// \"table X has no column named Y\". `find()` returns them in the\n// projected row (formula evaluation, rollup summary), but they must\n// NEVER be sent back to `insert()`.\n//\n// NOTE: `autonumber` IS a real string column in the SQL driver — it\n// has no auto-generation in this codebase, the value comes from the\n// seed file itself. Cloning it preserves the demo's \"CTR-0001\" /\n// \"QTE-0001\" identifiers so users see meaningful titleFormats and\n// the `externalId` upsert key keeps working on subsequent re-seeds.\nconst SKIP_COPY_TYPES = new Set<string>(['formula', 'summary']);\n\nfunction fieldList(schema: ServiceObject): FieldDescriptor[] {\n const fields: any = (schema as any)?.fields;\n if (!fields) return [];\n if (Array.isArray(fields)) {\n return fields.map((f: any) => ({\n name: f?.name,\n type: f?.type,\n reference: f?.reference,\n multiple: f?.multiple,\n unique: f?.unique,\n }));\n }\n return Object.entries(fields as Record<string, any>).map(([name, f]) => ({\n name,\n type: f?.type,\n reference: f?.reference,\n multiple: f?.multiple,\n unique: f?.unique,\n }));\n}\n\nfunction isLookupField(f: FieldDescriptor): boolean {\n return (f.type === 'lookup' || f.type === 'master_detail' || f.type === 'tree') && !!f.reference;\n}\n\nfunction hasOrgField(schema: ServiceObject): boolean {\n return fieldList(schema).some((f) => f.name === 'organization_id');\n}\n\nfunction shortId(): string {\n // Mirror the format `nanoid(16)` used elsewhere in the codebase\n // without pulling a runtime dep here.\n const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-';\n let out = '';\n for (let i = 0; i < 16; i++) {\n out += alphabet[Math.floor(Math.random() * alphabet.length)];\n }\n return out;\n}\n\nasync function findDonorOrgId(ql: any): Promise<string | null> {\n try {\n const res = await ql.find(\n 'sys_organization',\n { orderBy: { created_at: 'asc' }, limit: 1, fields: ['id'] },\n { context: SYSTEM_CTX },\n );\n const list: any[] = Array.isArray(res) ? res : Array.isArray(res?.records) ? res.records : [];\n return list[0]?.id ?? null;\n } catch {\n return null;\n }\n}\n\nexport async function cloneOrgSeedData(\n ql: any,\n targetOrgId: string,\n options: CloneOptions = {},\n): Promise<{ object: string; count: number }[]> {\n const logger = options.logger;\n if (!ql || typeof ql.find !== 'function' || typeof ql.insert !== 'function') {\n return [];\n }\n const registry = (ql as any).registry;\n if (!registry || typeof registry.getAllObjects !== 'function') {\n logger?.warn?.('[org-scoping] cloneOrgSeedData: registry unavailable');\n return [];\n }\n\n const donorOrgId = await findDonorOrgId(ql);\n if (!donorOrgId) return [];\n if (donorOrgId === targetOrgId) return [];\n\n const schemas: ServiceObject[] = registry.getAllObjects().filter(\n (s: any) => s?.name && !s.managedBy && !s.name.startsWith('sys_') && hasOrgField(s),\n );\n\n // Pass A: clone rows shallowly, build per-object oldId → newId map.\n const remap: Record<string, Record<string, string>> = {};\n const summary: { object: string; count: number }[] = [];\n // Track inserted shadow records so pass B can rewrite their lookups\n // without re-fetching from the DB.\n const inserted: { object: string; newId: string; record: Record<string, unknown>; lookups: FieldDescriptor[] }[] = [];\n\n for (const schema of schemas) {\n const objectName = schema.name as string;\n try {\n // Idempotency: if target org already has any row in this object,\n // assume a previous clone (or manual data) and skip — never\n // double-clone.\n const existing = await ql.find(\n objectName,\n { where: { organization_id: targetOrgId }, limit: 1, fields: ['id'] },\n { context: SYSTEM_CTX },\n );\n const existingList: any[] = Array.isArray(existing)\n ? existing\n : Array.isArray(existing?.records)\n ? existing.records\n : [];\n if (existingList.length > 0) {\n continue;\n }\n\n const donorRows = await ql.find(\n objectName,\n { where: { organization_id: donorOrgId }, limit: 10_000 },\n { context: SYSTEM_CTX },\n );\n const rows: any[] = Array.isArray(donorRows)\n ? donorRows\n : Array.isArray(donorRows?.records)\n ? donorRows.records\n : [];\n if (rows.length === 0) continue;\n\n const fields = fieldList(schema);\n const lookups = fields.filter(isLookupField);\n const uniqueFields = fields.filter((f) => f.unique && !SKIP_COPY_FIELDS.has(f.name));\n const objectRemap: Record<string, string> = (remap[objectName] ??= {});\n let cloned = 0;\n for (const row of rows) {\n const newId = shortId();\n const data: Record<string, unknown> = { id: newId, organization_id: targetOrgId };\n for (const f of fields) {\n if (SKIP_COPY_FIELDS.has(f.name)) continue;\n if (f.type && SKIP_COPY_TYPES.has(f.type)) continue;\n if (row[f.name] === undefined) continue;\n data[f.name] = row[f.name];\n }\n // Disambiguate UNIQUE columns. Many seed schemas declare\n // single-column unique indexes (e.g. `lead.email`) without\n // tenant scoping — cloning the donor row verbatim would\n // collide. Append a per-tenant suffix so each org gets its\n // own copy.\n const suffix = `+${targetOrgId.slice(-6)}`;\n for (const uf of uniqueFields) {\n const v = data[uf.name];\n if (typeof v !== 'string' || !v) continue;\n if (uf.type === 'email' && v.includes('@')) {\n const [local, domain] = v.split('@');\n data[uf.name] = `clone-${targetOrgId.slice(-6)}-${local}@${domain}`;\n } else {\n data[uf.name] = `${v}${suffix}`;\n }\n }\n try {\n await ql.insert(objectName, data, { context: SYSTEM_CTX });\n objectRemap[row.id] = newId;\n inserted.push({ object: objectName, newId, record: data, lookups });\n cloned++;\n } catch (e) {\n logger?.warn?.('[org-scoping] cloneOrgSeedData: insert failed', {\n object: objectName,\n error: (e as Error).message,\n });\n }\n }\n if (cloned > 0) summary.push({ object: objectName, count: cloned });\n } catch (e) {\n logger?.warn?.('[org-scoping] cloneOrgSeedData: object failed', {\n object: objectName,\n error: (e as Error).message,\n });\n }\n }\n\n // Pass B: rewrite lookup field values using the per-object remap so\n // intra-clone relationships stay intact.\n //\n // Cross-tenant FK hygiene: when a donor row's lookup value DOESN'T\n // appear in `remap[reference]` (i.e. the donor itself had a stale\n // FK pointing at another tenant's record, or the referenced object\n // wasn't included in this clone), we NULL the field instead of\n // leaving the orphan string in place. Otherwise every subsequent\n // clone perpetuates the broken FK chain (donor → tenant A → tenant\n // B → ...) and renderers display raw IDs because `find()` for the\n // referenced ID returns no row in the current tenant.\n for (const item of inserted) {\n if (item.lookups.length === 0) continue;\n const patch: Record<string, unknown> = {};\n let dirty = false;\n for (const f of item.lookups) {\n const oldVal = item.record[f.name];\n if (oldVal == null) continue;\n const targetMap = remap[f.reference!];\n if (Array.isArray(oldVal)) {\n // For multi-value lookups: remap when possible, drop entries\n // that have no remap (rather than keep an orphan string).\n const next = oldVal\n .map((v: any) => (typeof v === 'string' && targetMap?.[v]) || null)\n .filter((v: any) => v != null);\n if (next.length !== oldVal.length || next.some((v, i) => v !== oldVal[i])) {\n patch[f.name] = next.length > 0 ? next : null;\n dirty = true;\n }\n } else if (typeof oldVal === 'string') {\n if (targetMap && targetMap[oldVal]) {\n patch[f.name] = targetMap[oldVal];\n dirty = true;\n } else {\n // Unresolvable cross-tenant reference — null it out so the\n // UI shows \"empty\" rather than a dangling ID.\n patch[f.name] = null;\n dirty = true;\n }\n }\n }\n if (!dirty) continue;\n try {\n await ql.update(item.object, { id: item.newId, ...patch }, { context: SYSTEM_CTX });\n } catch (e) {\n logger?.warn?.('[org-scoping] cloneOrgSeedData: lookup remap failed', {\n object: item.object,\n id: item.newId,\n error: (e as Error).message,\n });\n }\n }\n\n return summary;\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * ensureDefaultOrganization — multi-tenant bootstrap helper.\n *\n * In multi-tenant deployments the freshly-promoted platform admin\n * (`admin_full_access` granted with `organization_id IS NULL`) needs\n * at least one `sys_organization` to carry an `activeOrganizationId`\n * on their session. Without it, the default `tenant_isolation` RLS\n * policy filters everything to zero rows and the admin sees an empty\n * console even though they have full access.\n *\n * Strategy (idempotent, run on `kernel:ready` and after every\n * `sys_user_permission_set` insert):\n *\n * 1. Find the platform admin (oldest `sys_user_permission_set` row\n * with `permission_set_id = admin_full_access` and\n * `organization_id IS NULL`). If none, no-op.\n * 2. If that user already has any `sys_member` row, no-op (they\n * either created their own org or were invited into one — we\n * respect that and never auto-create a \"Default Organization\"\n * behind their back).\n * 3. Re-use a pre-existing `slug='default'` org if present;\n * otherwise create one. Stable slug keeps human-readable URLs\n * predictable across cold-boots.\n * 4. Insert a `sys_member { role: 'owner' }` linking the admin to\n * the default org.\n *\n * This is the ONLY framework-side auto-provisioning of an org.\n * Subsequent users must accept an invitation or explicitly create\n * their first organization — `claimOrphanOrgRows` / `cloneOrgSeedData`\n * handle the seed-data side for those flows.\n */\n\ninterface EnsureOptions {\n logger?: {\n info: (message: string, meta?: Record<string, any>) => void;\n warn: (message: string, meta?: Record<string, any>) => void;\n };\n}\n\nconst SYSTEM_CTX = { isSystem: true };\n\nasync function tryFind(ql: any, object: string, where: any, limit = 100): Promise<any[]> {\n try {\n const rows = await ql.find(object, { where, limit }, { context: SYSTEM_CTX });\n return Array.isArray(rows) ? rows : Array.isArray(rows?.records) ? rows.records : [];\n } catch {\n return [];\n }\n}\n\nasync function tryInsert(ql: any, object: string, data: any): Promise<any | null> {\n try {\n return await ql.insert(object, data, { context: SYSTEM_CTX });\n } catch {\n return null;\n }\n}\n\nfunction genId(prefix: string): string {\n const rand = Math.random().toString(36).slice(2, 10);\n const ts = Date.now().toString(36);\n return `${prefix}_${ts}${rand}`;\n}\n\nexport interface EnsureDefaultOrganizationResult {\n /** Whether a brand-new org row was inserted (vs. re-using slug=default). */\n defaultOrgCreated: boolean;\n /** Resolved (or freshly minted) default-org id; undefined when no admin exists yet. */\n defaultOrgId?: string;\n /** Whether a sys_member row was inserted binding the admin to the default org. */\n memberCreated: boolean;\n /** Human-readable reason when the helper short-circuited. */\n reason?: 'no_admin' | 'admin_already_in_org' | 'org_insert_failed' | 'member_insert_failed';\n}\n\n/**\n * Ensure the platform admin has a Default Organization to operate in.\n * Safe to call multiple times — idempotent on stable slug `default`\n * and on the presence of any existing `sys_member` row for the admin.\n */\nexport async function ensureDefaultOrganization(\n ql: any,\n options: EnsureOptions = {},\n): Promise<EnsureDefaultOrganizationResult> {\n const logger = options.logger;\n if (!ql || typeof ql.find !== 'function' || typeof ql.insert !== 'function') {\n return { defaultOrgCreated: false, memberCreated: false, reason: 'no_admin' };\n }\n\n // 1. Find the platform admin permission-set id.\n const adminPs = await tryFind(ql, 'sys_permission_set', { name: 'admin_full_access' }, 1);\n if (adminPs.length === 0 || !adminPs[0].id) {\n return { defaultOrgCreated: false, memberCreated: false, reason: 'no_admin' };\n }\n const adminPsId = adminPs[0].id;\n\n // 2. Find the platform admin user (oldest cross-tenant grant).\n const adminGrants = await tryFind(\n ql,\n 'sys_user_permission_set',\n { permission_set_id: adminPsId, organization_id: null },\n 50,\n );\n if (adminGrants.length === 0) {\n return { defaultOrgCreated: false, memberCreated: false, reason: 'no_admin' };\n }\n const sortedGrants = [...adminGrants].sort((a, b) => {\n const ta = a.created_at ? new Date(a.created_at).getTime() : 0;\n const tb = b.created_at ? new Date(b.created_at).getTime() : 0;\n return ta - tb;\n });\n const adminUserId: string | undefined = sortedGrants[0]?.user_id;\n if (!adminUserId) {\n return { defaultOrgCreated: false, memberCreated: false, reason: 'no_admin' };\n }\n\n // 3. Respect existing membership — never auto-create a default org\n // behind an admin who already belongs somewhere.\n const memberships = await tryFind(ql, 'sys_member', { user_id: adminUserId }, 1);\n if (memberships.length > 0) {\n return {\n defaultOrgCreated: false,\n memberCreated: false,\n reason: 'admin_already_in_org',\n };\n }\n\n // 4. Re-use or create the `default` org.\n let defaultOrgId: string | undefined;\n let defaultOrgCreated = false;\n const existingDefault = await tryFind(ql, 'sys_organization', { slug: 'default' }, 1);\n if (existingDefault.length > 0 && existingDefault[0].id) {\n defaultOrgId = String(existingDefault[0].id);\n } else {\n const newOrgId = genId('org');\n const orgRow = await tryInsert(ql, 'sys_organization', {\n id: newOrgId,\n name: 'Default Organization',\n slug: 'default',\n logo: null,\n metadata: null,\n });\n if (!orgRow) {\n logger?.warn?.('[org-scoping] failed to create default organization for platform admin');\n return { defaultOrgCreated: false, memberCreated: false, reason: 'org_insert_failed' };\n }\n defaultOrgId = orgRow?.id ?? newOrgId;\n defaultOrgCreated = true;\n }\n\n // 5. Bind the admin as owner.\n const memRow = await tryInsert(ql, 'sys_member', {\n id: genId('mem'),\n organization_id: defaultOrgId,\n user_id: adminUserId,\n role: 'owner',\n });\n if (!memRow) {\n logger?.warn?.('[org-scoping] failed to bind platform admin to default organization');\n return {\n defaultOrgCreated,\n defaultOrgId,\n memberCreated: false,\n reason: 'member_insert_failed',\n };\n }\n logger?.info?.(\n `[org-scoping] bound platform admin to default organization (${defaultOrgId})`,\n { userId: adminUserId, defaultOrgId },\n );\n return { defaultOrgCreated, defaultOrgId, memberCreated: true };\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * Canonical plugin-org-scoping manifest source.\n *\n * Imported by `objectstack.config.ts` (compile-time) and\n * `org-scoping-plugin.ts` (runtime `manifest.register`) so the two\n * registration paths cannot drift.\n */\n\nexport const ORG_SCOPING_PLUGIN_ID = 'com.objectstack.plugin-org-scoping';\nexport const ORG_SCOPING_PLUGIN_VERSION = '1.0.0';\n\n/** This plugin owns no `sys_*` objects — Organization itself lives in `@objectstack/platform-objects`. */\nexport const orgScopingObjects = [] as const;\n\n/** Manifest header shared by compile-time config and runtime registration. */\nexport const orgScopingPluginManifestHeader = {\n id: ORG_SCOPING_PLUGIN_ID,\n namespace: 'sys',\n version: ORG_SCOPING_PLUGIN_VERSION,\n type: 'plugin' as const,\n scope: 'system' as const,\n defaultDatasource: 'cloud',\n name: 'Organization Scoping Plugin',\n description:\n 'Row-level Organization isolation: auto-stamps `organization_id` on insert from ' +\n '`ExecutionContext.tenantId`, replays seed datasets per new org, and bootstraps a default ' +\n 'organization for the first platform admin.',\n};\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport { Plugin, PluginContext } from '@objectstack/core';\nimport { claimOrphanOrgRows } from './claim-orphan-org-rows.js';\nimport { cloneOrgSeedData } from './clone-org-seed-data.js';\nimport { ensureDefaultOrganization } from './ensure-default-organization.js';\nimport {\n orgScopingObjects,\n orgScopingPluginManifestHeader,\n} from './manifest.js';\n\nexport interface OrgScopingPluginOptions {\n /**\n * Whether to auto-create a `Default Organization` (slug `default`)\n * and bind the first platform admin as `owner` when they have zero\n * memberships. Set to `false` for deployments that fully self-manage\n * org provisioning via invitation links or a custom onboarding flow.\n *\n * @default true\n */\n ensureDefaultOrganization?: boolean;\n}\n\n/**\n * OrgScopingPlugin\n *\n * Makes `sys_organization` a first-class row-level isolation boundary:\n *\n * 1. **insert auto-stamp** — on every authenticated `insert` whose\n * target object declares `organization_id`, fill the column from\n * `ExecutionContext.tenantId`. Without this, freshly-created\n * rows have `organization_id = NULL` and the default\n * `tenant_isolation` RLS policy hides them from the very user\n * who just created them.\n *\n * 2. **per-org seed replay** — after `sys_organization` insert, copy\n * the artifact's demo seed data into the new org. Three paths\n * (in order of preference):\n * a. replay registered `seed-datasets` via the kernel-level\n * `seed-replayer` callable (set by AppPlugin),\n * b. for the FIRST org, `claimOrphanOrgRows` adopts any\n * NULL-org rows a previous inline-seed may have inserted,\n * c. for SUBSEQUENT orgs, `cloneOrgSeedData` shallow-clones\n * rows from the very first org (donor-pattern).\n *\n * 3. **default-org bootstrap** — on `kernel:ready` and after every\n * `sys_user_permission_set` insert, ensure the platform admin has\n * a Default Organization to operate in (idempotent on slug\n * `default` + admin's existing memberships).\n *\n * Why split from plugin-security:\n * - plugin-security is a single-tenant-aware RBAC + RLS engine; it\n * should not know about Organization-specific seed flows.\n * - This plugin is purely opt-in: not installing it gives a\n * single-tenant deployment (no `organization_id` injection, no\n * per-org seed clone, no default-org bootstrap). plugin-security\n * detects its presence via `getService('org-scoping')` and adjusts\n * RLS policy stripping accordingly.\n *\n * Naming note: \"org-scoping\" deliberately avoids the word \"tenant\"\n * because in ObjectStack \"tenant\" already means *physical isolation*\n * (one Environment = one database, per ADR-0002 and driver-turso's\n * multi-tenant router). This plugin is about LOGICAL row-level\n * scoping inside a single database — orthogonal to physical tenancy.\n *\n * Dependencies:\n * - `objectql` (engine middleware host)\n */\nexport class OrgScopingPlugin implements Plugin {\n name = 'com.objectstack.org-scoping';\n type = 'standard';\n version = '1.0.0';\n dependencies = ['com.objectstack.engine.objectql'];\n\n /** Per-object field-name cache; same shape as SecurityPlugin's. */\n private readonly fieldNamesCache = new Map<string, Set<string> | null>();\n\n private readonly opts: Required<OrgScopingPluginOptions>;\n\n constructor(options: OrgScopingPluginOptions = {}) {\n this.opts = {\n ensureDefaultOrganization: options.ensureDefaultOrganization !== false,\n };\n }\n\n async init(ctx: PluginContext): Promise<void> {\n ctx.logger.info('Initializing Org-Scoping Plugin...');\n // Service registration doubles as plugin-security's\n // \"multi-tenant mode is on\" probe: SecurityPlugin queries\n // `getService('org-scoping')` and keeps wildcard\n // `current_user.organization_id` RLS policies when this returns.\n ctx.registerService('org-scoping', this);\n\n ctx\n .getService<{ register(m: any): void }>('manifest')\n .register({\n ...orgScopingPluginManifestHeader,\n objects: orgScopingObjects,\n });\n ctx.logger.info('Org-Scoping Plugin initialized');\n }\n\n async start(ctx: PluginContext): Promise<void> {\n ctx.logger.info('Starting Org-Scoping Plugin...');\n\n let ql: any;\n let metadata: any;\n try {\n ql = ctx.getService('objectql');\n try {\n metadata = ctx.getService('metadata');\n } catch {\n metadata = undefined;\n }\n } catch {\n ctx.logger.warn(\n 'ObjectQL service not available, org-scoping middleware not registered',\n );\n return;\n }\n if (!ql || typeof ql.registerMiddleware !== 'function') {\n ctx.logger.warn(\n 'ObjectQL engine does not support middleware, org-scoping middleware not registered',\n );\n return;\n }\n\n // ── Middleware A: auto-stamp `organization_id` on insert ──────────\n ql.registerMiddleware(async (opCtx: any, next: () => Promise<void>) => {\n if (opCtx.context?.isSystem) return next();\n if (\n opCtx.operation === 'insert' &&\n opCtx.data &&\n typeof opCtx.data === 'object' &&\n !Array.isArray(opCtx.data) &&\n opCtx.context?.tenantId\n ) {\n const fields = await this.getObjectFieldNames(metadata, opCtx.object, ql);\n if (fields && fields.has('organization_id')) {\n const data = opCtx.data as Record<string, unknown>;\n if (data.organization_id == null || data.organization_id === '') {\n data.organization_id = opCtx.context.tenantId;\n }\n }\n }\n await next();\n });\n\n // ── Middleware B: per-org seed pipeline on sys_organization insert ─\n ql.registerMiddleware(async (opCtx: any, next: () => Promise<void>) => {\n await next();\n if (\n opCtx?.object !== 'sys_organization' ||\n (opCtx?.operation !== 'create' && opCtx?.operation !== 'insert')\n ) {\n return;\n }\n const newOrgId = opCtx?.result?.id ?? opCtx?.data?.id;\n if (!newOrgId) return;\n\n const kernel: any = (ctx as any).kernel ?? ctx;\n let datasets: any[] | undefined;\n try {\n const raw = kernel?.getService?.('seed-datasets');\n if (Array.isArray(raw) && raw.length > 0) datasets = raw;\n } catch {\n /* service not registered */\n }\n\n // Count existing orgs to pick the right fallback path.\n let orgCount = 0;\n try {\n const allOrgs = await ql.find(\n 'sys_organization',\n { limit: 2, fields: ['id'] },\n { context: { isSystem: true } },\n );\n const list: any[] = Array.isArray(allOrgs)\n ? allOrgs\n : Array.isArray(allOrgs?.records)\n ? allOrgs.records\n : [];\n orgCount = list.length;\n } catch (e) {\n ctx.logger.warn('[org-scoping] failed to count organizations', {\n error: (e as Error).message,\n });\n }\n\n // Primary path: SeedLoader replay scoped to newOrgId.\n let replayed = false;\n try {\n const replayer: any = kernel?.getService?.('seed-replayer');\n if (typeof replayer === 'function') {\n const summary = await replayer(newOrgId);\n const total = (summary?.inserted ?? 0) + (summary?.updated ?? 0);\n ctx.logger.info(\n `[org-scoping] per-org seed replay for ${newOrgId}: +${summary?.inserted ?? 0} inserted, ${summary?.updated ?? 0} updated, ${summary?.errors?.length ?? 0} error(s)`,\n {\n organizationId: newOrgId,\n errors: summary?.errors?.slice?.(0, 5),\n },\n );\n if (total > 0) replayed = true;\n } else if (datasets) {\n ctx.logger.warn(\n '[org-scoping] per-org seed: datasets present but no replayer registered',\n { organizationId: newOrgId },\n );\n }\n } catch (e) {\n ctx.logger.warn(\n '[org-scoping] per-org seed replay failed, falling back',\n { organizationId: newOrgId, error: (e as Error).message },\n );\n }\n if (replayed) return;\n\n // Fallback A: legacy claim for first org.\n if (orgCount === 1) {\n try {\n const claims = await claimOrphanOrgRows(ql, newOrgId, { logger: ctx.logger });\n if (claims.length > 0) {\n const total = claims.reduce((s, c) => s + c.count, 0);\n ctx.logger.info(\n `[org-scoping] claimed ${total} orphan seed row(s) for first organization ${newOrgId}`,\n { breakdown: claims },\n );\n return;\n }\n } catch (e) {\n ctx.logger.warn('[org-scoping] claim-orphan-org-rows failed', {\n error: (e as Error).message,\n });\n }\n }\n\n // Fallback B: clone from donor org for subsequent orgs.\n if (orgCount > 1) {\n try {\n const summary = await cloneOrgSeedData(ql, newOrgId, { logger: ctx.logger });\n if (summary.length > 0) {\n const total = summary.reduce((s, c) => s + c.count, 0);\n ctx.logger.info(\n `[org-scoping] cloned ${total} seed row(s) for new organization ${newOrgId}`,\n { breakdown: summary },\n );\n }\n } catch (e) {\n ctx.logger.warn('[org-scoping] clone-org-seed-data failed', {\n organizationId: newOrgId,\n error: (e as Error).message,\n });\n }\n }\n });\n\n // ── Default-org bootstrap on kernel:ready + on admin grant ────────\n if (this.opts.ensureDefaultOrganization) {\n const runEnsure = async () => {\n try {\n const res = await ensureDefaultOrganization(ql, { logger: ctx.logger });\n if (res.defaultOrgCreated) {\n ctx.logger.info(\n `[org-scoping] created Default Organization ${res.defaultOrgId} for platform admin`,\n );\n }\n } catch (e) {\n ctx.logger.warn?.('[org-scoping] ensureDefaultOrganization failed', {\n error: (e as Error).message,\n });\n }\n };\n if (typeof (ctx as any).hook === 'function') {\n (ctx as any).hook('kernel:ready', runEnsure);\n } else {\n void runEnsure();\n }\n // Re-run after every admin grant — handles the \"first sign-up\n // promoted to platform admin\" case where the kernel:ready hook\n // fired before any user existed.\n ql.registerMiddleware(async (opCtx: any, next: () => Promise<void>) => {\n await next();\n if (\n opCtx?.object === 'sys_user_permission_set' &&\n (opCtx?.operation === 'insert' || opCtx?.operation === 'create')\n ) {\n await runEnsure();\n }\n });\n }\n\n ctx.logger.info('Org-Scoping middleware registered on ObjectQL engine');\n }\n\n async destroy(): Promise<void> {\n // No cleanup needed\n }\n\n /**\n * Resolve the column-name set for an object (mirrors SecurityPlugin's\n * loader so the two plugins behave consistently). Returns `null` if\n * the schema can't be loaded — caller skips injection.\n */\n private async getObjectFieldNames(\n metadata: any,\n objectName: string,\n ql?: any,\n ): Promise<Set<string> | null> {\n if (this.fieldNamesCache.has(objectName)) {\n return this.fieldNamesCache.get(objectName) ?? null;\n }\n const result = await this.loadObjectFieldNames(metadata, objectName, ql);\n if (result) this.fieldNamesCache.set(objectName, result);\n return result;\n }\n\n private async loadObjectFieldNames(\n metadata: any,\n objectName: string,\n ql?: any,\n ): Promise<Set<string> | null> {\n try {\n let obj: any =\n typeof ql?.getSchema === 'function' ? ql.getSchema(objectName) : null;\n if (!obj || !obj.fields) {\n obj = await metadata?.get?.('object', objectName);\n }\n if (!obj || !obj.fields) return null;\n const set = new Set<string>(['id']);\n if (Array.isArray(obj.fields)) {\n for (const f of obj.fields) {\n if (f?.name) set.add(String(f.name));\n }\n } else if (typeof obj.fields === 'object') {\n for (const key of Object.keys(obj.fields)) {\n set.add(key);\n const v = (obj.fields as Record<string, any>)[key];\n if (v && typeof v === 'object' && v.name) set.add(String(v.name));\n }\n } else {\n return null;\n }\n return set;\n } catch {\n return null;\n }\n }\n}\n"],"mappings":"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","names":["SYSTEM_CTX","SYSTEM_CTX"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/claim-orphan-org-rows.ts","../src/clone-org-seed-data.ts","../src/claim-org-seed-ownership.ts","../src/ensure-default-organization.ts","../src/manifest.ts","../src/org-scoping-plugin.ts"],"sourcesContent":["// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * @objectstack/plugin-org-scoping\n *\n * Row-level Organization isolation for ObjectStack:\n * - auto-stamps `organization_id` on insert from\n * `ExecutionContext.tenantId`,\n * - replays seed datasets (or clones from the donor org) on every\n * `sys_organization` insert,\n * - bootstraps a Default Organization for the first platform admin.\n *\n * Pair with `@objectstack/plugin-security` to get full multi-tenant\n * RBAC + RLS + Field-Level Security. Install standalone for\n * single-tenant deployments — plugin-security detects this plugin's\n * presence via `getService('org-scoping')` and adjusts wildcard\n * tenant policy handling accordingly.\n */\n\nexport { OrgScopingPlugin } from './org-scoping-plugin.js';\nexport type { OrgScopingPluginOptions } from './org-scoping-plugin.js';\nexport { claimOrphanOrgRows } from './claim-orphan-org-rows.js';\nexport { claimOrgSeedOwnership } from './claim-org-seed-ownership.js';\nexport { cloneOrgSeedData } from './clone-org-seed-data.js';\nexport {\n ensureDefaultOrganization,\n type EnsureDefaultOrganizationResult,\n} from './ensure-default-organization.js';\nexport {\n orgScopingObjects,\n orgScopingPluginManifestHeader,\n ORG_SCOPING_PLUGIN_ID,\n ORG_SCOPING_PLUGIN_VERSION,\n} from './manifest.js';\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * claimOrphanOrgRows — assign seed-loaded records to the first organization.\n *\n * Seeds (`defineSeed`) are inserted by `SeedLoaderService` using\n * `{ context: { isSystem: true } }`, which intentionally bypasses\n * SecurityPlugin's `organization_id` auto-fill. As a result, in\n * multi-tenant mode every seed row lands with `organization_id = NULL`.\n *\n * That's correct for **cross-tenant metadata** — `sys_permission_set`\n * rows, default roles, etc. (objects whose schema has `managedBy` set)\n * — but for **business-domain seeds** (CRM `lead`, `account`, `contact`,\n * …) it means the rows are invisible to anyone bound to an organization\n * (the default `tenant_isolation` RLS policy\n * `organization_id = current_user.organization_id` filters them out).\n *\n * This helper runs **once**, on first-organization creation, and\n * back-fills `organization_id` on every orphaned (`organization_id IS\n * NULL`) seed row of every user-defined object that declares the\n * column. Result: out of the box, the freshly registered owner sees the\n * shipped demo data scoped to their first org — no manual claim step.\n *\n * Idempotent: a no-op once an organization-tagged row exists, and\n * `managedBy` schemas (`sys_*` better-auth/platform tables) are always\n * skipped so cross-tenant defaults stay cross-tenant.\n */\n\nimport type { ServiceObject } from '@objectstack/spec/data';\n\ninterface ClaimOptions {\n logger?: {\n info: (message: string, meta?: Record<string, any>) => void;\n warn: (message: string, meta?: Record<string, any>) => void;\n };\n}\n\nconst SYSTEM_CTX = { isSystem: true };\n\nfunction hasOrganizationField(schema: ServiceObject): boolean {\n const fields: any = (schema as any)?.fields;\n if (!fields) return false;\n if (Array.isArray(fields)) {\n return fields.some((f) => f?.name === 'organization_id');\n }\n return Object.prototype.hasOwnProperty.call(fields, 'organization_id');\n}\n\n/**\n * Assign every orphaned seed row to `organizationId`.\n *\n * Walks `ql.registry.getAllObjects()`, filters to schemas that\n * (a) are not `managedBy` (skip sys_/auth/platform tables),\n * (b) declare an `organization_id` field,\n * and runs an `update(where: { organization_id: null }, patch: {\n * organization_id: organizationId })` against each as `isSystem`.\n *\n * Returns a per-object summary `{ object, count }[]`.\n */\nexport async function claimOrphanOrgRows(\n ql: any,\n organizationId: string,\n options: ClaimOptions = {},\n): Promise<{ object: string; count: number }[]> {\n const logger = options.logger;\n if (!ql || typeof ql.update !== 'function' || typeof ql.find !== 'function') {\n return [];\n }\n const registry = (ql as any).registry;\n if (!registry || typeof registry.getAllObjects !== 'function') {\n logger?.warn?.('[org-scoping] claimOrphanOrgRows: registry unavailable');\n return [];\n }\n\n const schemas: ServiceObject[] = registry.getAllObjects();\n const results: { object: string; count: number }[] = [];\n\n for (const schema of schemas) {\n if (!schema?.name) continue;\n if ((schema as any).managedBy) continue;\n // Defense in depth: any platform-namespaced object (`sys_*`) is\n // off-limits for tenant claim regardless of `managedBy`. Platform\n // tables that should be tenant-scoped are inserted with an explicit\n // `organization_id` by the code that owns them, so they will never\n // be orphans here.\n if (schema.name.startsWith('sys_')) continue;\n if (!hasOrganizationField(schema)) continue;\n\n try {\n const orphans = await ql.find(\n schema.name,\n { where: { organization_id: null }, limit: 10_000, fields: ['id'] },\n { context: SYSTEM_CTX },\n );\n const list: any[] = Array.isArray(orphans)\n ? orphans\n : Array.isArray(orphans?.records)\n ? orphans.records\n : [];\n if (list.length === 0) continue;\n\n let updated = 0;\n for (const row of list) {\n if (!row?.id) continue;\n try {\n await ql.update(\n schema.name,\n { id: row.id, organization_id: organizationId },\n { context: SYSTEM_CTX },\n );\n updated += 1;\n } catch (e) {\n logger?.warn?.(`[org-scoping] claim failed for ${schema.name}:${row.id}`, {\n error: (e as Error).message,\n });\n }\n }\n if (updated > 0) {\n results.push({ object: schema.name, count: updated });\n }\n } catch (e) {\n logger?.warn?.(`[org-scoping] claim scan failed for ${schema.name}`, {\n error: (e as Error).message,\n });\n }\n }\n\n if (results.length > 0) {\n const total = results.reduce((s, r) => s + r.count, 0);\n logger?.info?.(`[org-scoping] claimed ${total} orphan seed row(s) for organization ${organizationId}`, {\n breakdown: results,\n });\n }\n return results;\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * cloneOrgSeedData — give every newly-registered org its own copy of\n * the demo seed data.\n *\n * Multi-tenant deployments treat each `sys_organization` as a hard\n * isolation boundary. The platform-wide `claimOrphanOrgRows` hook\n * (see `claim-orphan-tenant-rows.ts`) only fires for the very first\n * org — every subsequent org (created explicitly by a user via\n * `createOrganization`, or by an admin from the console) starts\n * empty. For demo / trial-org UX (Salesforce-style \"you get a\n * fully populated sandbox on signup\"), we want every freshly minted\n * org to receive a private clone of the platform-first org's\n * user-defined data.\n *\n * Strategy:\n * 1. Pick the donor org — the very first `sys_organization`.\n * 2. Walk `ql.registry.getAllObjects()` once to collect schemas\n * that are user-defined (not `managedBy`, not `sys_*`) AND\n * declare an `organization_id` field.\n * 3. Pass A — for each donor object, find rows where\n * `organization_id = donorOrgId`, generate a new id, insert a\n * shallow copy under `targetOrgId`, recording an\n * `oldId → newId` map keyed by object name. Lookup field values\n * pointing at donor rows are left untouched in this pass; the\n * remap happens in pass B so we don't depend on topological\n * ordering of inserts.\n * 4. Pass B — for each cloned row, walk its lookup-shaped fields\n * and rewrite values that match the donor map for the field's\n * `reference` object.\n *\n * Idempotent: skipped if the target org already has rows in any\n * cloned object, or if no donor org exists, or if the target IS the\n * donor (claim hook handles the donor itself).\n *\n * Best-effort: per-object failures are logged at `warn` and don't\n * abort the rest of the clone. FK fields that reference an object\n * that wasn't cloned (e.g. the lookup target lives in `sys_*`, or\n * the remap key isn't present) are left as-is — broken refs are\n * preferable to losing whole rows.\n */\n\nimport type { ServiceObject } from '@objectstack/spec/data';\n\ninterface CloneOptions {\n logger?: {\n info: (message: string, meta?: Record<string, any>) => void;\n warn: (message: string, meta?: Record<string, any>) => void;\n };\n}\n\ninterface FieldDescriptor {\n name: string;\n type?: string;\n reference?: string;\n multiple?: boolean;\n unique?: boolean;\n}\n\nconst SYSTEM_CTX = { isSystem: true };\n\nconst SKIP_COPY_FIELDS = new Set<string>([\n 'id',\n 'created_at',\n 'updated_at',\n 'organization_id',\n]);\n\n// Computed / virtual / system-managed field types — these have no\n// physical column in the DB, so re-inserting them would fail with\n// \"table X has no column named Y\". `find()` returns them in the\n// projected row (formula evaluation, rollup summary), but they must\n// NEVER be sent back to `insert()`.\n//\n// NOTE: `autonumber` IS a real string column in the SQL driver — it\n// has no auto-generation in this codebase, the value comes from the\n// seed file itself. Cloning it preserves the demo's \"CTR-0001\" /\n// \"QTE-0001\" identifiers so users see meaningful titleFormats and\n// the `externalId` upsert key keeps working on subsequent re-seeds.\nconst SKIP_COPY_TYPES = new Set<string>(['formula', 'summary']);\n\nfunction fieldList(schema: ServiceObject): FieldDescriptor[] {\n const fields: any = (schema as any)?.fields;\n if (!fields) return [];\n if (Array.isArray(fields)) {\n return fields.map((f: any) => ({\n name: f?.name,\n type: f?.type,\n reference: f?.reference,\n multiple: f?.multiple,\n unique: f?.unique,\n }));\n }\n return Object.entries(fields as Record<string, any>).map(([name, f]) => ({\n name,\n type: f?.type,\n reference: f?.reference,\n multiple: f?.multiple,\n unique: f?.unique,\n }));\n}\n\nfunction isLookupField(f: FieldDescriptor): boolean {\n return (f.type === 'lookup' || f.type === 'master_detail' || f.type === 'tree') && !!f.reference;\n}\n\nfunction hasOrgField(schema: ServiceObject): boolean {\n return fieldList(schema).some((f) => f.name === 'organization_id');\n}\n\nfunction shortId(): string {\n // Mirror the format `nanoid(16)` used elsewhere in the codebase\n // without pulling a runtime dep here.\n const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-';\n let out = '';\n for (let i = 0; i < 16; i++) {\n out += alphabet[Math.floor(Math.random() * alphabet.length)];\n }\n return out;\n}\n\nasync function findDonorOrgId(ql: any): Promise<string | null> {\n try {\n const res = await ql.find(\n 'sys_organization',\n { orderBy: { created_at: 'asc' }, limit: 1, fields: ['id'] },\n { context: SYSTEM_CTX },\n );\n const list: any[] = Array.isArray(res) ? res : Array.isArray(res?.records) ? res.records : [];\n return list[0]?.id ?? null;\n } catch {\n return null;\n }\n}\n\nexport async function cloneOrgSeedData(\n ql: any,\n targetOrgId: string,\n options: CloneOptions = {},\n): Promise<{ object: string; count: number }[]> {\n const logger = options.logger;\n if (!ql || typeof ql.find !== 'function' || typeof ql.insert !== 'function') {\n return [];\n }\n const registry = (ql as any).registry;\n if (!registry || typeof registry.getAllObjects !== 'function') {\n logger?.warn?.('[org-scoping] cloneOrgSeedData: registry unavailable');\n return [];\n }\n\n const donorOrgId = await findDonorOrgId(ql);\n if (!donorOrgId) return [];\n if (donorOrgId === targetOrgId) return [];\n\n const schemas: ServiceObject[] = registry.getAllObjects().filter(\n (s: any) => s?.name && !s.managedBy && !s.name.startsWith('sys_') && hasOrgField(s),\n );\n\n // Pass A: clone rows shallowly, build per-object oldId → newId map.\n const remap: Record<string, Record<string, string>> = {};\n const summary: { object: string; count: number }[] = [];\n // Track inserted shadow records so pass B can rewrite their lookups\n // without re-fetching from the DB.\n const inserted: { object: string; newId: string; record: Record<string, unknown>; lookups: FieldDescriptor[] }[] = [];\n\n for (const schema of schemas) {\n const objectName = schema.name as string;\n try {\n // Idempotency: if target org already has any row in this object,\n // assume a previous clone (or manual data) and skip — never\n // double-clone.\n const existing = await ql.find(\n objectName,\n { where: { organization_id: targetOrgId }, limit: 1, fields: ['id'] },\n { context: SYSTEM_CTX },\n );\n const existingList: any[] = Array.isArray(existing)\n ? existing\n : Array.isArray(existing?.records)\n ? existing.records\n : [];\n if (existingList.length > 0) {\n continue;\n }\n\n const donorRows = await ql.find(\n objectName,\n { where: { organization_id: donorOrgId }, limit: 10_000 },\n { context: SYSTEM_CTX },\n );\n const rows: any[] = Array.isArray(donorRows)\n ? donorRows\n : Array.isArray(donorRows?.records)\n ? donorRows.records\n : [];\n if (rows.length === 0) continue;\n\n const fields = fieldList(schema);\n const lookups = fields.filter(isLookupField);\n const uniqueFields = fields.filter((f) => f.unique && !SKIP_COPY_FIELDS.has(f.name));\n const objectRemap: Record<string, string> = (remap[objectName] ??= {});\n let cloned = 0;\n for (const row of rows) {\n const newId = shortId();\n const data: Record<string, unknown> = { id: newId, organization_id: targetOrgId };\n for (const f of fields) {\n if (SKIP_COPY_FIELDS.has(f.name)) continue;\n if (f.type && SKIP_COPY_TYPES.has(f.type)) continue;\n if (row[f.name] === undefined) continue;\n data[f.name] = row[f.name];\n }\n // Disambiguate UNIQUE columns. Many seed schemas declare\n // single-column unique indexes (e.g. `lead.email`) without\n // tenant scoping — cloning the donor row verbatim would\n // collide. Append a per-tenant suffix so each org gets its\n // own copy.\n const suffix = `+${targetOrgId.slice(-6)}`;\n for (const uf of uniqueFields) {\n const v = data[uf.name];\n if (typeof v !== 'string' || !v) continue;\n if (uf.type === 'email' && v.includes('@')) {\n const [local, domain] = v.split('@');\n data[uf.name] = `clone-${targetOrgId.slice(-6)}-${local}@${domain}`;\n } else {\n data[uf.name] = `${v}${suffix}`;\n }\n }\n try {\n await ql.insert(objectName, data, { context: SYSTEM_CTX });\n objectRemap[row.id] = newId;\n inserted.push({ object: objectName, newId, record: data, lookups });\n cloned++;\n } catch (e) {\n logger?.warn?.('[org-scoping] cloneOrgSeedData: insert failed', {\n object: objectName,\n error: (e as Error).message,\n });\n }\n }\n if (cloned > 0) summary.push({ object: objectName, count: cloned });\n } catch (e) {\n logger?.warn?.('[org-scoping] cloneOrgSeedData: object failed', {\n object: objectName,\n error: (e as Error).message,\n });\n }\n }\n\n // Pass B: rewrite lookup field values using the per-object remap so\n // intra-clone relationships stay intact.\n //\n // Cross-tenant FK hygiene: when a donor row's lookup value DOESN'T\n // appear in `remap[reference]` (i.e. the donor itself had a stale\n // FK pointing at another tenant's record, or the referenced object\n // wasn't included in this clone), we NULL the field instead of\n // leaving the orphan string in place. Otherwise every subsequent\n // clone perpetuates the broken FK chain (donor → tenant A → tenant\n // B → ...) and renderers display raw IDs because `find()` for the\n // referenced ID returns no row in the current tenant.\n for (const item of inserted) {\n if (item.lookups.length === 0) continue;\n const patch: Record<string, unknown> = {};\n let dirty = false;\n for (const f of item.lookups) {\n const oldVal = item.record[f.name];\n if (oldVal == null) continue;\n const targetMap = remap[f.reference!];\n if (Array.isArray(oldVal)) {\n // For multi-value lookups: remap when possible, drop entries\n // that have no remap (rather than keep an orphan string).\n const next = oldVal\n .map((v: any) => (typeof v === 'string' && targetMap?.[v]) || null)\n .filter((v: any) => v != null);\n if (next.length !== oldVal.length || next.some((v, i) => v !== oldVal[i])) {\n patch[f.name] = next.length > 0 ? next : null;\n dirty = true;\n }\n } else if (typeof oldVal === 'string') {\n if (targetMap && targetMap[oldVal]) {\n patch[f.name] = targetMap[oldVal];\n dirty = true;\n } else {\n // Unresolvable cross-tenant reference — null it out so the\n // UI shows \"empty\" rather than a dangling ID.\n patch[f.name] = null;\n dirty = true;\n }\n }\n }\n if (!dirty) continue;\n try {\n await ql.update(item.object, { id: item.newId, ...patch }, { context: SYSTEM_CTX });\n } catch (e) {\n logger?.warn?.('[org-scoping] cloneOrgSeedData: lookup remap failed', {\n object: item.object,\n id: item.newId,\n error: (e as Error).message,\n });\n }\n }\n\n return summary;\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * claimOrgSeedOwnership — hand an organization's seeded records to its owner.\n *\n * The multi-tenant twin of plugin-security's `claimSeedOwnership` (single-tenant\n * first-admin handoff). Seeded rows land `owner_id = NULL` (the author leaves it\n * unset and `cel`os.user.id`` resolves to NULL at seed time, since the owning\n * admin does not exist yet). In multi-tenant mode those rows are scoped to an\n * org by `claimOrphanOrgRows` / per-org replay, but their `owner_id` stays NULL\n * — so \"My\" views, owner reports and owner notifications are empty for the org's\n * members until ownership is assigned.\n *\n * This runs when the org's owner is established (e.g. `ensureDefaultOrganization`\n * binds the platform admin as the default org's `owner`) and assigns\n * `owner_id = ownerUserId` to that org's NULL-owned rows — the ownership\n * companion to `claimOrphanOrgRows`'s `organization_id` back-fill.\n *\n * Scoped to a single org (`organization_id = organizationId`) so it never\n * touches another tenant's rows. Idempotent: only NULL-owned rows are updated.\n * `managedBy` and `sys_*` tables are skipped.\n */\n\nimport type { ServiceObject } from '@objectstack/spec/data';\n\ninterface ClaimOwnershipOptions {\n logger?: {\n info: (message: string, meta?: Record<string, any>) => void;\n warn: (message: string, meta?: Record<string, any>) => void;\n };\n}\n\nconst SYSTEM_CTX = { isSystem: true };\n\nfunction hasField(schema: ServiceObject, field: string): boolean {\n const fields: any = (schema as any)?.fields;\n if (!fields) return false;\n if (Array.isArray(fields)) return fields.some((f) => f?.name === field);\n return Object.prototype.hasOwnProperty.call(fields, field);\n}\n\n/**\n * Assign `owner_id = ownerUserId` to every NULL-owned seed row of `organizationId`.\n *\n * Walks `ql.registry.getAllObjects()`, filters to schemas that\n * (a) are not `managedBy` (skip sys_/auth/platform tables),\n * (b) are not `sys_*`-namespaced,\n * (c) declare BOTH `owner_id` and `organization_id`,\n * and updates the org's unowned rows as `isSystem`. Returns a per-object summary.\n */\nexport async function claimOrgSeedOwnership(\n ql: any,\n organizationId: string,\n ownerUserId: string,\n options: ClaimOwnershipOptions = {},\n): Promise<{ object: string; count: number }[]> {\n const logger = options.logger;\n if (!organizationId || !ownerUserId) return [];\n if (!ql || typeof ql.update !== 'function' || typeof ql.find !== 'function') return [];\n const registry = (ql as any).registry;\n if (!registry || typeof registry.getAllObjects !== 'function') {\n logger?.warn?.('[org-scoping] claimOrgSeedOwnership: registry unavailable');\n return [];\n }\n\n const schemas: ServiceObject[] = registry.getAllObjects();\n const results: { object: string; count: number }[] = [];\n\n for (const schema of schemas) {\n if (!schema?.name) continue;\n if ((schema as any).managedBy) continue;\n if (schema.name.startsWith('sys_')) continue;\n // Both columns are required: owner_id to assign, organization_id to scope.\n if (!hasField(schema, 'owner_id') || !hasField(schema, 'organization_id')) continue;\n\n try {\n const orphans = await ql.find(\n schema.name,\n { where: { organization_id: organizationId, owner_id: null }, limit: 10_000, fields: ['id'] },\n { context: SYSTEM_CTX },\n );\n const list: any[] = Array.isArray(orphans)\n ? orphans\n : Array.isArray(orphans?.records)\n ? orphans.records\n : [];\n if (list.length === 0) continue;\n\n let updated = 0;\n for (const row of list) {\n if (!row?.id) continue;\n try {\n await ql.update(schema.name, { id: row.id, owner_id: ownerUserId }, { context: SYSTEM_CTX });\n updated += 1;\n } catch (e) {\n logger?.warn?.(`[org-scoping] claimOrgSeedOwnership failed for ${schema.name}:${row.id}`, {\n error: (e as Error).message,\n });\n }\n }\n if (updated > 0) results.push({ object: schema.name, count: updated });\n } catch (e) {\n logger?.warn?.(`[org-scoping] claimOrgSeedOwnership scan failed for ${schema.name}`, {\n error: (e as Error).message,\n });\n }\n }\n\n if (results.length > 0) {\n const total = results.reduce((s, r) => s + r.count, 0);\n logger?.info?.(`[org-scoping] handed ${total} seeded row(s) of org ${organizationId} to owner ${ownerUserId}`, {\n breakdown: results,\n });\n }\n return results;\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * ensureDefaultOrganization — multi-tenant bootstrap helper.\n *\n * In multi-tenant deployments the freshly-promoted platform admin\n * (`admin_full_access` granted with `organization_id IS NULL`) needs\n * at least one `sys_organization` to carry an `activeOrganizationId`\n * on their session. Without it, the default `tenant_isolation` RLS\n * policy filters everything to zero rows and the admin sees an empty\n * console even though they have full access.\n *\n * Strategy (idempotent, run on `kernel:ready` and after every\n * `sys_user_permission_set` insert):\n *\n * 1. Find the platform admin (oldest `sys_user_permission_set` row\n * with `permission_set_id = admin_full_access` and\n * `organization_id IS NULL`). If none, no-op.\n * 2. If that user already has any `sys_member` row, no-op (they\n * either created their own org or were invited into one — we\n * respect that and never auto-create a \"Default Organization\"\n * behind their back).\n * 3. Re-use a pre-existing `slug='default'` org if present;\n * otherwise create one. Stable slug keeps human-readable URLs\n * predictable across cold-boots.\n * 4. Insert a `sys_member { role: 'owner' }` linking the admin to\n * the default org.\n *\n * This is the ONLY framework-side auto-provisioning of an org.\n * Subsequent users must accept an invitation or explicitly create\n * their first organization — `claimOrphanOrgRows` / `cloneOrgSeedData`\n * handle the seed-data side for those flows.\n */\n\nimport { claimOrgSeedOwnership } from './claim-org-seed-ownership.js';\n\ninterface EnsureOptions {\n logger?: {\n info: (message: string, meta?: Record<string, any>) => void;\n warn: (message: string, meta?: Record<string, any>) => void;\n };\n}\n\nconst SYSTEM_CTX = { isSystem: true };\n\nasync function tryFind(ql: any, object: string, where: any, limit = 100): Promise<any[]> {\n try {\n const rows = await ql.find(object, { where, limit }, { context: SYSTEM_CTX });\n return Array.isArray(rows) ? rows : Array.isArray(rows?.records) ? rows.records : [];\n } catch {\n return [];\n }\n}\n\nasync function tryInsert(ql: any, object: string, data: any): Promise<any | null> {\n try {\n return await ql.insert(object, data, { context: SYSTEM_CTX });\n } catch {\n return null;\n }\n}\n\nfunction genId(prefix: string): string {\n const rand = Math.random().toString(36).slice(2, 10);\n const ts = Date.now().toString(36);\n return `${prefix}_${ts}${rand}`;\n}\n\nexport interface EnsureDefaultOrganizationResult {\n /** Whether a brand-new org row was inserted (vs. re-using slug=default). */\n defaultOrgCreated: boolean;\n /** Resolved (or freshly minted) default-org id; undefined when no admin exists yet. */\n defaultOrgId?: string;\n /** Whether a sys_member row was inserted binding the admin to the default org. */\n memberCreated: boolean;\n /** Human-readable reason when the helper short-circuited. */\n reason?: 'no_admin' | 'admin_already_in_org' | 'org_insert_failed' | 'member_insert_failed';\n /** Count of the default org's seeded rows re-owned to the platform admin. */\n ownershipClaimed?: number;\n}\n\n/**\n * Ensure the platform admin has a Default Organization to operate in.\n * Safe to call multiple times — idempotent on stable slug `default`\n * and on the presence of any existing `sys_member` row for the admin.\n */\nexport async function ensureDefaultOrganization(\n ql: any,\n options: EnsureOptions = {},\n): Promise<EnsureDefaultOrganizationResult> {\n const logger = options.logger;\n if (!ql || typeof ql.find !== 'function' || typeof ql.insert !== 'function') {\n return { defaultOrgCreated: false, memberCreated: false, reason: 'no_admin' };\n }\n\n // 1. Find the platform admin permission-set id.\n const adminPs = await tryFind(ql, 'sys_permission_set', { name: 'admin_full_access' }, 1);\n if (adminPs.length === 0 || !adminPs[0].id) {\n return { defaultOrgCreated: false, memberCreated: false, reason: 'no_admin' };\n }\n const adminPsId = adminPs[0].id;\n\n // 2. Find the platform admin user (oldest cross-tenant grant).\n const adminGrants = await tryFind(\n ql,\n 'sys_user_permission_set',\n { permission_set_id: adminPsId, organization_id: null },\n 50,\n );\n if (adminGrants.length === 0) {\n return { defaultOrgCreated: false, memberCreated: false, reason: 'no_admin' };\n }\n const sortedGrants = [...adminGrants].sort((a, b) => {\n const ta = a.created_at ? new Date(a.created_at).getTime() : 0;\n const tb = b.created_at ? new Date(b.created_at).getTime() : 0;\n return ta - tb;\n });\n const adminUserId: string | undefined = sortedGrants[0]?.user_id;\n if (!adminUserId) {\n return { defaultOrgCreated: false, memberCreated: false, reason: 'no_admin' };\n }\n\n // 3. Respect existing membership — never auto-create a default org\n // behind an admin who already belongs somewhere.\n const memberships = await tryFind(ql, 'sys_member', { user_id: adminUserId }, 1);\n if (memberships.length > 0) {\n return {\n defaultOrgCreated: false,\n memberCreated: false,\n reason: 'admin_already_in_org',\n };\n }\n\n // 4. Re-use or create the `default` org.\n let defaultOrgId: string | undefined;\n let defaultOrgCreated = false;\n const existingDefault = await tryFind(ql, 'sys_organization', { slug: 'default' }, 1);\n if (existingDefault.length > 0 && existingDefault[0].id) {\n defaultOrgId = String(existingDefault[0].id);\n } else {\n const newOrgId = genId('org');\n const orgRow = await tryInsert(ql, 'sys_organization', {\n id: newOrgId,\n name: 'Default Organization',\n slug: 'default',\n logo: null,\n metadata: null,\n });\n if (!orgRow) {\n logger?.warn?.('[org-scoping] failed to create default organization for platform admin');\n return { defaultOrgCreated: false, memberCreated: false, reason: 'org_insert_failed' };\n }\n defaultOrgId = orgRow?.id ?? newOrgId;\n defaultOrgCreated = true;\n }\n\n // 5. Bind the admin as owner.\n const memRow = await tryInsert(ql, 'sys_member', {\n id: genId('mem'),\n organization_id: defaultOrgId,\n user_id: adminUserId,\n role: 'owner',\n });\n if (!memRow) {\n logger?.warn?.('[org-scoping] failed to bind platform admin to default organization');\n return {\n defaultOrgCreated,\n defaultOrgId,\n memberCreated: false,\n reason: 'member_insert_failed',\n };\n }\n logger?.info?.(\n `[org-scoping] bound platform admin to default organization (${defaultOrgId})`,\n { userId: adminUserId, defaultOrgId },\n );\n\n // 6. Hand the default org's seeded rows (owner_id NULL) to the admin so\n // owner-keyed UX works out of the box — the multi-tenant companion to the\n // single-tenant first-admin handoff. Best-effort; never undoes the bind.\n let ownershipClaimed = 0;\n if (defaultOrgId) {\n try {\n const claims = await claimOrgSeedOwnership(ql, defaultOrgId, adminUserId, { logger });\n ownershipClaimed = claims.reduce((s, c) => s + c.count, 0);\n } catch (e) {\n logger?.warn?.('[org-scoping] default-org seed ownership handoff failed', {\n error: (e as Error).message,\n });\n }\n }\n\n return { defaultOrgCreated, defaultOrgId, memberCreated: true, ownershipClaimed };\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * Canonical plugin-org-scoping manifest source.\n *\n * Imported by `objectstack.config.ts` (compile-time) and\n * `org-scoping-plugin.ts` (runtime `manifest.register`) so the two\n * registration paths cannot drift.\n */\n\nexport const ORG_SCOPING_PLUGIN_ID = 'com.objectstack.plugin-org-scoping';\nexport const ORG_SCOPING_PLUGIN_VERSION = '1.0.0';\n\n/** This plugin owns no `sys_*` objects — Organization itself lives in `@objectstack/platform-objects`. */\nexport const orgScopingObjects = [] as const;\n\n/** Manifest header shared by compile-time config and runtime registration. */\nexport const orgScopingPluginManifestHeader = {\n id: ORG_SCOPING_PLUGIN_ID,\n namespace: 'sys',\n version: ORG_SCOPING_PLUGIN_VERSION,\n type: 'plugin' as const,\n scope: 'system' as const,\n defaultDatasource: 'cloud',\n name: 'Organization Scoping Plugin',\n description:\n 'Row-level Organization isolation: auto-stamps `organization_id` on insert from ' +\n '`ExecutionContext.tenantId`, replays seed datasets per new org, and bootstraps a default ' +\n 'organization for the first platform admin.',\n};\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport { Plugin, PluginContext } from '@objectstack/core';\nimport { claimOrphanOrgRows } from './claim-orphan-org-rows.js';\nimport { cloneOrgSeedData } from './clone-org-seed-data.js';\nimport { ensureDefaultOrganization } from './ensure-default-organization.js';\nimport {\n orgScopingObjects,\n orgScopingPluginManifestHeader,\n} from './manifest.js';\n\nexport interface OrgScopingPluginOptions {\n /**\n * Whether to auto-create a `Default Organization` (slug `default`)\n * and bind the first platform admin as `owner` when they have zero\n * memberships. Set to `false` for deployments that fully self-manage\n * org provisioning via invitation links or a custom onboarding flow.\n *\n * @default true\n */\n ensureDefaultOrganization?: boolean;\n}\n\n/**\n * OrgScopingPlugin\n *\n * Makes `sys_organization` a first-class row-level isolation boundary:\n *\n * 1. **insert auto-stamp** — on every authenticated `insert` whose\n * target object declares `organization_id`, fill the column from\n * `ExecutionContext.tenantId`. Without this, freshly-created\n * rows have `organization_id = NULL` and the default\n * `tenant_isolation` RLS policy hides them from the very user\n * who just created them.\n *\n * 2. **per-org seed replay** — after `sys_organization` insert, copy\n * the artifact's demo seed data into the new org. Three paths\n * (in order of preference):\n * a. replay registered `seed-datasets` via the kernel-level\n * `seed-replayer` callable (set by AppPlugin),\n * b. for the FIRST org, `claimOrphanOrgRows` adopts any\n * NULL-org rows a previous inline-seed may have inserted,\n * c. for SUBSEQUENT orgs, `cloneOrgSeedData` shallow-clones\n * rows from the very first org (donor-pattern).\n *\n * 3. **default-org bootstrap** — on `kernel:ready` and after every\n * `sys_user_permission_set` insert, ensure the platform admin has\n * a Default Organization to operate in (idempotent on slug\n * `default` + admin's existing memberships).\n *\n * Why split from plugin-security:\n * - plugin-security is a single-tenant-aware RBAC + RLS engine; it\n * should not know about Organization-specific seed flows.\n * - This plugin is purely opt-in: not installing it gives a\n * single-tenant deployment (no `organization_id` injection, no\n * per-org seed clone, no default-org bootstrap). plugin-security\n * detects its presence via `getService('org-scoping')` and adjusts\n * RLS policy stripping accordingly.\n *\n * Naming note: \"org-scoping\" deliberately avoids the word \"tenant\"\n * because in ObjectStack \"tenant\" already means *physical isolation*\n * (one Environment = one database, per ADR-0002 and driver-turso's\n * multi-tenant router). This plugin is about LOGICAL row-level\n * scoping inside a single database — orthogonal to physical tenancy.\n *\n * Dependencies:\n * - `objectql` (engine middleware host)\n */\nexport class OrgScopingPlugin implements Plugin {\n name = 'com.objectstack.org-scoping';\n type = 'standard';\n version = '1.0.0';\n dependencies = ['com.objectstack.engine.objectql'];\n\n /** Per-object field-name cache; same shape as SecurityPlugin's. */\n private readonly fieldNamesCache = new Map<string, Set<string> | null>();\n\n private readonly opts: Required<OrgScopingPluginOptions>;\n\n constructor(options: OrgScopingPluginOptions = {}) {\n this.opts = {\n ensureDefaultOrganization: options.ensureDefaultOrganization !== false,\n };\n }\n\n async init(ctx: PluginContext): Promise<void> {\n ctx.logger.info('Initializing Org-Scoping Plugin...');\n // Service registration doubles as plugin-security's\n // \"multi-tenant mode is on\" probe: SecurityPlugin queries\n // `getService('org-scoping')` and keeps wildcard\n // `current_user.organization_id` RLS policies when this returns.\n ctx.registerService('org-scoping', this);\n\n ctx\n .getService<{ register(m: any): void }>('manifest')\n .register({\n ...orgScopingPluginManifestHeader,\n objects: orgScopingObjects,\n });\n ctx.logger.info('Org-Scoping Plugin initialized');\n }\n\n async start(ctx: PluginContext): Promise<void> {\n ctx.logger.info('Starting Org-Scoping Plugin...');\n\n let ql: any;\n let metadata: any;\n try {\n ql = ctx.getService('objectql');\n try {\n metadata = ctx.getService('metadata');\n } catch {\n metadata = undefined;\n }\n } catch {\n ctx.logger.warn(\n 'ObjectQL service not available, org-scoping middleware not registered',\n );\n return;\n }\n if (!ql || typeof ql.registerMiddleware !== 'function') {\n ctx.logger.warn(\n 'ObjectQL engine does not support middleware, org-scoping middleware not registered',\n );\n return;\n }\n\n // ── Middleware A: auto-stamp `organization_id` on insert ──────────\n ql.registerMiddleware(async (opCtx: any, next: () => Promise<void>) => {\n if (opCtx.context?.isSystem) return next();\n if (\n opCtx.operation === 'insert' &&\n opCtx.data &&\n typeof opCtx.data === 'object' &&\n !Array.isArray(opCtx.data) &&\n opCtx.context?.tenantId\n ) {\n const fields = await this.getObjectFieldNames(metadata, opCtx.object, ql);\n if (fields && fields.has('organization_id')) {\n const data = opCtx.data as Record<string, unknown>;\n if (data.organization_id == null || data.organization_id === '') {\n data.organization_id = opCtx.context.tenantId;\n }\n }\n }\n await next();\n });\n\n // ── Middleware B: per-org seed pipeline on sys_organization insert ─\n ql.registerMiddleware(async (opCtx: any, next: () => Promise<void>) => {\n await next();\n if (\n opCtx?.object !== 'sys_organization' ||\n (opCtx?.operation !== 'create' && opCtx?.operation !== 'insert')\n ) {\n return;\n }\n const newOrgId = opCtx?.result?.id ?? opCtx?.data?.id;\n if (!newOrgId) return;\n\n const kernel: any = (ctx as any).kernel ?? ctx;\n let datasets: any[] | undefined;\n try {\n const raw = kernel?.getService?.('seed-datasets');\n if (Array.isArray(raw) && raw.length > 0) datasets = raw;\n } catch {\n /* service not registered */\n }\n\n // Count existing orgs to pick the right fallback path.\n let orgCount = 0;\n try {\n const allOrgs = await ql.find(\n 'sys_organization',\n { limit: 2, fields: ['id'] },\n { context: { isSystem: true } },\n );\n const list: any[] = Array.isArray(allOrgs)\n ? allOrgs\n : Array.isArray(allOrgs?.records)\n ? allOrgs.records\n : [];\n orgCount = list.length;\n } catch (e) {\n ctx.logger.warn('[org-scoping] failed to count organizations', {\n error: (e as Error).message,\n });\n }\n\n // Primary path: SeedLoader replay scoped to newOrgId.\n let replayed = false;\n try {\n const replayer: any = kernel?.getService?.('seed-replayer');\n if (typeof replayer === 'function') {\n const summary = await replayer(newOrgId);\n const total = (summary?.inserted ?? 0) + (summary?.updated ?? 0);\n ctx.logger.info(\n `[org-scoping] per-org seed replay for ${newOrgId}: +${summary?.inserted ?? 0} inserted, ${summary?.updated ?? 0} updated, ${summary?.errors?.length ?? 0} error(s)`,\n {\n organizationId: newOrgId,\n errors: summary?.errors?.slice?.(0, 5),\n },\n );\n if (total > 0) replayed = true;\n } else if (datasets) {\n ctx.logger.warn(\n '[org-scoping] per-org seed: datasets present but no replayer registered',\n { organizationId: newOrgId },\n );\n }\n } catch (e) {\n ctx.logger.warn(\n '[org-scoping] per-org seed replay failed, falling back',\n { organizationId: newOrgId, error: (e as Error).message },\n );\n }\n if (replayed) return;\n\n // Fallback A: legacy claim for first org.\n if (orgCount === 1) {\n try {\n const claims = await claimOrphanOrgRows(ql, newOrgId, { logger: ctx.logger });\n if (claims.length > 0) {\n const total = claims.reduce((s, c) => s + c.count, 0);\n ctx.logger.info(\n `[org-scoping] claimed ${total} orphan seed row(s) for first organization ${newOrgId}`,\n { breakdown: claims },\n );\n return;\n }\n } catch (e) {\n ctx.logger.warn('[org-scoping] claim-orphan-org-rows failed', {\n error: (e as Error).message,\n });\n }\n }\n\n // Fallback B: clone from donor org for subsequent orgs.\n if (orgCount > 1) {\n try {\n const summary = await cloneOrgSeedData(ql, newOrgId, { logger: ctx.logger });\n if (summary.length > 0) {\n const total = summary.reduce((s, c) => s + c.count, 0);\n ctx.logger.info(\n `[org-scoping] cloned ${total} seed row(s) for new organization ${newOrgId}`,\n { breakdown: summary },\n );\n }\n } catch (e) {\n ctx.logger.warn('[org-scoping] clone-org-seed-data failed', {\n organizationId: newOrgId,\n error: (e as Error).message,\n });\n }\n }\n });\n\n // ── Default-org bootstrap on kernel:ready + on admin grant ────────\n if (this.opts.ensureDefaultOrganization) {\n const runEnsure = async () => {\n try {\n const res = await ensureDefaultOrganization(ql, { logger: ctx.logger });\n if (res.defaultOrgCreated) {\n ctx.logger.info(\n `[org-scoping] created Default Organization ${res.defaultOrgId} for platform admin`,\n );\n }\n } catch (e) {\n ctx.logger.warn?.('[org-scoping] ensureDefaultOrganization failed', {\n error: (e as Error).message,\n });\n }\n };\n if (typeof (ctx as any).hook === 'function') {\n (ctx as any).hook('kernel:ready', runEnsure);\n } else {\n void runEnsure();\n }\n // Re-run after every admin grant — handles the \"first sign-up\n // promoted to platform admin\" case where the kernel:ready hook\n // fired before any user existed.\n ql.registerMiddleware(async (opCtx: any, next: () => Promise<void>) => {\n await next();\n if (\n opCtx?.object === 'sys_user_permission_set' &&\n (opCtx?.operation === 'insert' || opCtx?.operation === 'create')\n ) {\n await runEnsure();\n }\n });\n }\n\n ctx.logger.info('Org-Scoping middleware registered on ObjectQL engine');\n }\n\n async destroy(): Promise<void> {\n // No cleanup needed\n }\n\n /**\n * Resolve the column-name set for an object (mirrors SecurityPlugin's\n * loader so the two plugins behave consistently). Returns `null` if\n * the schema can't be loaded — caller skips injection.\n */\n private async getObjectFieldNames(\n metadata: any,\n objectName: string,\n ql?: any,\n ): Promise<Set<string> | null> {\n if (this.fieldNamesCache.has(objectName)) {\n return this.fieldNamesCache.get(objectName) ?? null;\n }\n const result = await this.loadObjectFieldNames(metadata, objectName, ql);\n if (result) this.fieldNamesCache.set(objectName, result);\n return result;\n }\n\n private async loadObjectFieldNames(\n metadata: any,\n objectName: string,\n ql?: any,\n ): Promise<Set<string> | null> {\n try {\n let obj: any =\n typeof ql?.getSchema === 'function' ? ql.getSchema(objectName) : null;\n if (!obj || !obj.fields) {\n obj = await metadata?.get?.('object', objectName);\n }\n if (!obj || !obj.fields) return null;\n const set = new Set<string>(['id']);\n if (Array.isArray(obj.fields)) {\n for (const f of obj.fields) {\n if (f?.name) set.add(String(f.name));\n }\n } else if (typeof obj.fields === 'object') {\n for (const key of Object.keys(obj.fields)) {\n set.add(key);\n const v = (obj.fields as Record<string, any>)[key];\n if (v && typeof v === 'object' && v.name) set.add(String(v.name));\n }\n } else {\n return null;\n }\n return set;\n } catch {\n return null;\n }\n }\n}\n"],"mappings":"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","names":["SYSTEM_CTX","SYSTEM_CTX","SYSTEM_CTX"]}
package/dist/index.mjs CHANGED
@@ -244,11 +244,71 @@ async function cloneOrgSeedData(ql, targetOrgId, options = {}) {
244
244
  return summary;
245
245
  }
246
246
 
247
- // src/ensure-default-organization.ts
247
+ // src/claim-org-seed-ownership.ts
248
248
  var SYSTEM_CTX3 = { isSystem: true };
249
+ function hasField(schema, field) {
250
+ const fields = schema?.fields;
251
+ if (!fields) return false;
252
+ if (Array.isArray(fields)) return fields.some((f) => f?.name === field);
253
+ return Object.prototype.hasOwnProperty.call(fields, field);
254
+ }
255
+ async function claimOrgSeedOwnership(ql, organizationId, ownerUserId, options = {}) {
256
+ const logger = options.logger;
257
+ if (!organizationId || !ownerUserId) return [];
258
+ if (!ql || typeof ql.update !== "function" || typeof ql.find !== "function") return [];
259
+ const registry = ql.registry;
260
+ if (!registry || typeof registry.getAllObjects !== "function") {
261
+ logger?.warn?.("[org-scoping] claimOrgSeedOwnership: registry unavailable");
262
+ return [];
263
+ }
264
+ const schemas = registry.getAllObjects();
265
+ const results = [];
266
+ for (const schema of schemas) {
267
+ if (!schema?.name) continue;
268
+ if (schema.managedBy) continue;
269
+ if (schema.name.startsWith("sys_")) continue;
270
+ if (!hasField(schema, "owner_id") || !hasField(schema, "organization_id")) continue;
271
+ try {
272
+ const orphans = await ql.find(
273
+ schema.name,
274
+ { where: { organization_id: organizationId, owner_id: null }, limit: 1e4, fields: ["id"] },
275
+ { context: SYSTEM_CTX3 }
276
+ );
277
+ const list = Array.isArray(orphans) ? orphans : Array.isArray(orphans?.records) ? orphans.records : [];
278
+ if (list.length === 0) continue;
279
+ let updated = 0;
280
+ for (const row of list) {
281
+ if (!row?.id) continue;
282
+ try {
283
+ await ql.update(schema.name, { id: row.id, owner_id: ownerUserId }, { context: SYSTEM_CTX3 });
284
+ updated += 1;
285
+ } catch (e) {
286
+ logger?.warn?.(`[org-scoping] claimOrgSeedOwnership failed for ${schema.name}:${row.id}`, {
287
+ error: e.message
288
+ });
289
+ }
290
+ }
291
+ if (updated > 0) results.push({ object: schema.name, count: updated });
292
+ } catch (e) {
293
+ logger?.warn?.(`[org-scoping] claimOrgSeedOwnership scan failed for ${schema.name}`, {
294
+ error: e.message
295
+ });
296
+ }
297
+ }
298
+ if (results.length > 0) {
299
+ const total = results.reduce((s, r) => s + r.count, 0);
300
+ logger?.info?.(`[org-scoping] handed ${total} seeded row(s) of org ${organizationId} to owner ${ownerUserId}`, {
301
+ breakdown: results
302
+ });
303
+ }
304
+ return results;
305
+ }
306
+
307
+ // src/ensure-default-organization.ts
308
+ var SYSTEM_CTX4 = { isSystem: true };
249
309
  async function tryFind(ql, object, where, limit = 100) {
250
310
  try {
251
- const rows = await ql.find(object, { where, limit }, { context: SYSTEM_CTX3 });
311
+ const rows = await ql.find(object, { where, limit }, { context: SYSTEM_CTX4 });
252
312
  return Array.isArray(rows) ? rows : Array.isArray(rows?.records) ? rows.records : [];
253
313
  } catch {
254
314
  return [];
@@ -256,7 +316,7 @@ async function tryFind(ql, object, where, limit = 100) {
256
316
  }
257
317
  async function tryInsert(ql, object, data) {
258
318
  try {
259
- return await ql.insert(object, data, { context: SYSTEM_CTX3 });
319
+ return await ql.insert(object, data, { context: SYSTEM_CTX4 });
260
320
  } catch {
261
321
  return null;
262
322
  }
@@ -342,7 +402,18 @@ async function ensureDefaultOrganization(ql, options = {}) {
342
402
  `[org-scoping] bound platform admin to default organization (${defaultOrgId})`,
343
403
  { userId: adminUserId, defaultOrgId }
344
404
  );
345
- return { defaultOrgCreated, defaultOrgId, memberCreated: true };
405
+ let ownershipClaimed = 0;
406
+ if (defaultOrgId) {
407
+ try {
408
+ const claims = await claimOrgSeedOwnership(ql, defaultOrgId, adminUserId, { logger });
409
+ ownershipClaimed = claims.reduce((s, c) => s + c.count, 0);
410
+ } catch (e) {
411
+ logger?.warn?.("[org-scoping] default-org seed ownership handoff failed", {
412
+ error: e.message
413
+ });
414
+ }
415
+ }
416
+ return { defaultOrgCreated, defaultOrgId, memberCreated: true, ownershipClaimed };
346
417
  }
347
418
 
348
419
  // src/manifest.ts
@@ -583,6 +654,7 @@ export {
583
654
  ORG_SCOPING_PLUGIN_ID,
584
655
  ORG_SCOPING_PLUGIN_VERSION,
585
656
  OrgScopingPlugin,
657
+ claimOrgSeedOwnership,
586
658
  claimOrphanOrgRows,
587
659
  cloneOrgSeedData,
588
660
  ensureDefaultOrganization,
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/claim-orphan-org-rows.ts","../src/clone-org-seed-data.ts","../src/ensure-default-organization.ts","../src/manifest.ts","../src/org-scoping-plugin.ts"],"sourcesContent":["// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * claimOrphanOrgRows — assign seed-loaded records to the first organization.\n *\n * Seeds (`defineSeed`) are inserted by `SeedLoaderService` using\n * `{ context: { isSystem: true } }`, which intentionally bypasses\n * SecurityPlugin's `organization_id` auto-fill. As a result, in\n * multi-tenant mode every seed row lands with `organization_id = NULL`.\n *\n * That's correct for **cross-tenant metadata** — `sys_permission_set`\n * rows, default roles, etc. (objects whose schema has `managedBy` set)\n * — but for **business-domain seeds** (CRM `lead`, `account`, `contact`,\n * …) it means the rows are invisible to anyone bound to an organization\n * (the default `tenant_isolation` RLS policy\n * `organization_id = current_user.organization_id` filters them out).\n *\n * This helper runs **once**, on first-organization creation, and\n * back-fills `organization_id` on every orphaned (`organization_id IS\n * NULL`) seed row of every user-defined object that declares the\n * column. Result: out of the box, the freshly registered owner sees the\n * shipped demo data scoped to their first org — no manual claim step.\n *\n * Idempotent: a no-op once an organization-tagged row exists, and\n * `managedBy` schemas (`sys_*` better-auth/platform tables) are always\n * skipped so cross-tenant defaults stay cross-tenant.\n */\n\nimport type { ServiceObject } from '@objectstack/spec/data';\n\ninterface ClaimOptions {\n logger?: {\n info: (message: string, meta?: Record<string, any>) => void;\n warn: (message: string, meta?: Record<string, any>) => void;\n };\n}\n\nconst SYSTEM_CTX = { isSystem: true };\n\nfunction hasOrganizationField(schema: ServiceObject): boolean {\n const fields: any = (schema as any)?.fields;\n if (!fields) return false;\n if (Array.isArray(fields)) {\n return fields.some((f) => f?.name === 'organization_id');\n }\n return Object.prototype.hasOwnProperty.call(fields, 'organization_id');\n}\n\n/**\n * Assign every orphaned seed row to `organizationId`.\n *\n * Walks `ql.registry.getAllObjects()`, filters to schemas that\n * (a) are not `managedBy` (skip sys_/auth/platform tables),\n * (b) declare an `organization_id` field,\n * and runs an `update(where: { organization_id: null }, patch: {\n * organization_id: organizationId })` against each as `isSystem`.\n *\n * Returns a per-object summary `{ object, count }[]`.\n */\nexport async function claimOrphanOrgRows(\n ql: any,\n organizationId: string,\n options: ClaimOptions = {},\n): Promise<{ object: string; count: number }[]> {\n const logger = options.logger;\n if (!ql || typeof ql.update !== 'function' || typeof ql.find !== 'function') {\n return [];\n }\n const registry = (ql as any).registry;\n if (!registry || typeof registry.getAllObjects !== 'function') {\n logger?.warn?.('[org-scoping] claimOrphanOrgRows: registry unavailable');\n return [];\n }\n\n const schemas: ServiceObject[] = registry.getAllObjects();\n const results: { object: string; count: number }[] = [];\n\n for (const schema of schemas) {\n if (!schema?.name) continue;\n if ((schema as any).managedBy) continue;\n // Defense in depth: any platform-namespaced object (`sys_*`) is\n // off-limits for tenant claim regardless of `managedBy`. Platform\n // tables that should be tenant-scoped are inserted with an explicit\n // `organization_id` by the code that owns them, so they will never\n // be orphans here.\n if (schema.name.startsWith('sys_')) continue;\n if (!hasOrganizationField(schema)) continue;\n\n try {\n const orphans = await ql.find(\n schema.name,\n { where: { organization_id: null }, limit: 10_000, fields: ['id'] },\n { context: SYSTEM_CTX },\n );\n const list: any[] = Array.isArray(orphans)\n ? orphans\n : Array.isArray(orphans?.records)\n ? orphans.records\n : [];\n if (list.length === 0) continue;\n\n let updated = 0;\n for (const row of list) {\n if (!row?.id) continue;\n try {\n await ql.update(\n schema.name,\n { id: row.id, organization_id: organizationId },\n { context: SYSTEM_CTX },\n );\n updated += 1;\n } catch (e) {\n logger?.warn?.(`[org-scoping] claim failed for ${schema.name}:${row.id}`, {\n error: (e as Error).message,\n });\n }\n }\n if (updated > 0) {\n results.push({ object: schema.name, count: updated });\n }\n } catch (e) {\n logger?.warn?.(`[org-scoping] claim scan failed for ${schema.name}`, {\n error: (e as Error).message,\n });\n }\n }\n\n if (results.length > 0) {\n const total = results.reduce((s, r) => s + r.count, 0);\n logger?.info?.(`[org-scoping] claimed ${total} orphan seed row(s) for organization ${organizationId}`, {\n breakdown: results,\n });\n }\n return results;\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * cloneOrgSeedData — give every newly-registered org its own copy of\n * the demo seed data.\n *\n * Multi-tenant deployments treat each `sys_organization` as a hard\n * isolation boundary. The platform-wide `claimOrphanOrgRows` hook\n * (see `claim-orphan-tenant-rows.ts`) only fires for the very first\n * org — every subsequent org (created explicitly by a user via\n * `createOrganization`, or by an admin from the console) starts\n * empty. For demo / trial-org UX (Salesforce-style \"you get a\n * fully populated sandbox on signup\"), we want every freshly minted\n * org to receive a private clone of the platform-first org's\n * user-defined data.\n *\n * Strategy:\n * 1. Pick the donor org — the very first `sys_organization`.\n * 2. Walk `ql.registry.getAllObjects()` once to collect schemas\n * that are user-defined (not `managedBy`, not `sys_*`) AND\n * declare an `organization_id` field.\n * 3. Pass A — for each donor object, find rows where\n * `organization_id = donorOrgId`, generate a new id, insert a\n * shallow copy under `targetOrgId`, recording an\n * `oldId → newId` map keyed by object name. Lookup field values\n * pointing at donor rows are left untouched in this pass; the\n * remap happens in pass B so we don't depend on topological\n * ordering of inserts.\n * 4. Pass B — for each cloned row, walk its lookup-shaped fields\n * and rewrite values that match the donor map for the field's\n * `reference` object.\n *\n * Idempotent: skipped if the target org already has rows in any\n * cloned object, or if no donor org exists, or if the target IS the\n * donor (claim hook handles the donor itself).\n *\n * Best-effort: per-object failures are logged at `warn` and don't\n * abort the rest of the clone. FK fields that reference an object\n * that wasn't cloned (e.g. the lookup target lives in `sys_*`, or\n * the remap key isn't present) are left as-is — broken refs are\n * preferable to losing whole rows.\n */\n\nimport type { ServiceObject } from '@objectstack/spec/data';\n\ninterface CloneOptions {\n logger?: {\n info: (message: string, meta?: Record<string, any>) => void;\n warn: (message: string, meta?: Record<string, any>) => void;\n };\n}\n\ninterface FieldDescriptor {\n name: string;\n type?: string;\n reference?: string;\n multiple?: boolean;\n unique?: boolean;\n}\n\nconst SYSTEM_CTX = { isSystem: true };\n\nconst SKIP_COPY_FIELDS = new Set<string>([\n 'id',\n 'created_at',\n 'updated_at',\n 'organization_id',\n]);\n\n// Computed / virtual / system-managed field types — these have no\n// physical column in the DB, so re-inserting them would fail with\n// \"table X has no column named Y\". `find()` returns them in the\n// projected row (formula evaluation, rollup summary), but they must\n// NEVER be sent back to `insert()`.\n//\n// NOTE: `autonumber` IS a real string column in the SQL driver — it\n// has no auto-generation in this codebase, the value comes from the\n// seed file itself. Cloning it preserves the demo's \"CTR-0001\" /\n// \"QTE-0001\" identifiers so users see meaningful titleFormats and\n// the `externalId` upsert key keeps working on subsequent re-seeds.\nconst SKIP_COPY_TYPES = new Set<string>(['formula', 'summary']);\n\nfunction fieldList(schema: ServiceObject): FieldDescriptor[] {\n const fields: any = (schema as any)?.fields;\n if (!fields) return [];\n if (Array.isArray(fields)) {\n return fields.map((f: any) => ({\n name: f?.name,\n type: f?.type,\n reference: f?.reference,\n multiple: f?.multiple,\n unique: f?.unique,\n }));\n }\n return Object.entries(fields as Record<string, any>).map(([name, f]) => ({\n name,\n type: f?.type,\n reference: f?.reference,\n multiple: f?.multiple,\n unique: f?.unique,\n }));\n}\n\nfunction isLookupField(f: FieldDescriptor): boolean {\n return (f.type === 'lookup' || f.type === 'master_detail' || f.type === 'tree') && !!f.reference;\n}\n\nfunction hasOrgField(schema: ServiceObject): boolean {\n return fieldList(schema).some((f) => f.name === 'organization_id');\n}\n\nfunction shortId(): string {\n // Mirror the format `nanoid(16)` used elsewhere in the codebase\n // without pulling a runtime dep here.\n const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-';\n let out = '';\n for (let i = 0; i < 16; i++) {\n out += alphabet[Math.floor(Math.random() * alphabet.length)];\n }\n return out;\n}\n\nasync function findDonorOrgId(ql: any): Promise<string | null> {\n try {\n const res = await ql.find(\n 'sys_organization',\n { orderBy: { created_at: 'asc' }, limit: 1, fields: ['id'] },\n { context: SYSTEM_CTX },\n );\n const list: any[] = Array.isArray(res) ? res : Array.isArray(res?.records) ? res.records : [];\n return list[0]?.id ?? null;\n } catch {\n return null;\n }\n}\n\nexport async function cloneOrgSeedData(\n ql: any,\n targetOrgId: string,\n options: CloneOptions = {},\n): Promise<{ object: string; count: number }[]> {\n const logger = options.logger;\n if (!ql || typeof ql.find !== 'function' || typeof ql.insert !== 'function') {\n return [];\n }\n const registry = (ql as any).registry;\n if (!registry || typeof registry.getAllObjects !== 'function') {\n logger?.warn?.('[org-scoping] cloneOrgSeedData: registry unavailable');\n return [];\n }\n\n const donorOrgId = await findDonorOrgId(ql);\n if (!donorOrgId) return [];\n if (donorOrgId === targetOrgId) return [];\n\n const schemas: ServiceObject[] = registry.getAllObjects().filter(\n (s: any) => s?.name && !s.managedBy && !s.name.startsWith('sys_') && hasOrgField(s),\n );\n\n // Pass A: clone rows shallowly, build per-object oldId → newId map.\n const remap: Record<string, Record<string, string>> = {};\n const summary: { object: string; count: number }[] = [];\n // Track inserted shadow records so pass B can rewrite their lookups\n // without re-fetching from the DB.\n const inserted: { object: string; newId: string; record: Record<string, unknown>; lookups: FieldDescriptor[] }[] = [];\n\n for (const schema of schemas) {\n const objectName = schema.name as string;\n try {\n // Idempotency: if target org already has any row in this object,\n // assume a previous clone (or manual data) and skip — never\n // double-clone.\n const existing = await ql.find(\n objectName,\n { where: { organization_id: targetOrgId }, limit: 1, fields: ['id'] },\n { context: SYSTEM_CTX },\n );\n const existingList: any[] = Array.isArray(existing)\n ? existing\n : Array.isArray(existing?.records)\n ? existing.records\n : [];\n if (existingList.length > 0) {\n continue;\n }\n\n const donorRows = await ql.find(\n objectName,\n { where: { organization_id: donorOrgId }, limit: 10_000 },\n { context: SYSTEM_CTX },\n );\n const rows: any[] = Array.isArray(donorRows)\n ? donorRows\n : Array.isArray(donorRows?.records)\n ? donorRows.records\n : [];\n if (rows.length === 0) continue;\n\n const fields = fieldList(schema);\n const lookups = fields.filter(isLookupField);\n const uniqueFields = fields.filter((f) => f.unique && !SKIP_COPY_FIELDS.has(f.name));\n const objectRemap: Record<string, string> = (remap[objectName] ??= {});\n let cloned = 0;\n for (const row of rows) {\n const newId = shortId();\n const data: Record<string, unknown> = { id: newId, organization_id: targetOrgId };\n for (const f of fields) {\n if (SKIP_COPY_FIELDS.has(f.name)) continue;\n if (f.type && SKIP_COPY_TYPES.has(f.type)) continue;\n if (row[f.name] === undefined) continue;\n data[f.name] = row[f.name];\n }\n // Disambiguate UNIQUE columns. Many seed schemas declare\n // single-column unique indexes (e.g. `lead.email`) without\n // tenant scoping — cloning the donor row verbatim would\n // collide. Append a per-tenant suffix so each org gets its\n // own copy.\n const suffix = `+${targetOrgId.slice(-6)}`;\n for (const uf of uniqueFields) {\n const v = data[uf.name];\n if (typeof v !== 'string' || !v) continue;\n if (uf.type === 'email' && v.includes('@')) {\n const [local, domain] = v.split('@');\n data[uf.name] = `clone-${targetOrgId.slice(-6)}-${local}@${domain}`;\n } else {\n data[uf.name] = `${v}${suffix}`;\n }\n }\n try {\n await ql.insert(objectName, data, { context: SYSTEM_CTX });\n objectRemap[row.id] = newId;\n inserted.push({ object: objectName, newId, record: data, lookups });\n cloned++;\n } catch (e) {\n logger?.warn?.('[org-scoping] cloneOrgSeedData: insert failed', {\n object: objectName,\n error: (e as Error).message,\n });\n }\n }\n if (cloned > 0) summary.push({ object: objectName, count: cloned });\n } catch (e) {\n logger?.warn?.('[org-scoping] cloneOrgSeedData: object failed', {\n object: objectName,\n error: (e as Error).message,\n });\n }\n }\n\n // Pass B: rewrite lookup field values using the per-object remap so\n // intra-clone relationships stay intact.\n //\n // Cross-tenant FK hygiene: when a donor row's lookup value DOESN'T\n // appear in `remap[reference]` (i.e. the donor itself had a stale\n // FK pointing at another tenant's record, or the referenced object\n // wasn't included in this clone), we NULL the field instead of\n // leaving the orphan string in place. Otherwise every subsequent\n // clone perpetuates the broken FK chain (donor → tenant A → tenant\n // B → ...) and renderers display raw IDs because `find()` for the\n // referenced ID returns no row in the current tenant.\n for (const item of inserted) {\n if (item.lookups.length === 0) continue;\n const patch: Record<string, unknown> = {};\n let dirty = false;\n for (const f of item.lookups) {\n const oldVal = item.record[f.name];\n if (oldVal == null) continue;\n const targetMap = remap[f.reference!];\n if (Array.isArray(oldVal)) {\n // For multi-value lookups: remap when possible, drop entries\n // that have no remap (rather than keep an orphan string).\n const next = oldVal\n .map((v: any) => (typeof v === 'string' && targetMap?.[v]) || null)\n .filter((v: any) => v != null);\n if (next.length !== oldVal.length || next.some((v, i) => v !== oldVal[i])) {\n patch[f.name] = next.length > 0 ? next : null;\n dirty = true;\n }\n } else if (typeof oldVal === 'string') {\n if (targetMap && targetMap[oldVal]) {\n patch[f.name] = targetMap[oldVal];\n dirty = true;\n } else {\n // Unresolvable cross-tenant reference — null it out so the\n // UI shows \"empty\" rather than a dangling ID.\n patch[f.name] = null;\n dirty = true;\n }\n }\n }\n if (!dirty) continue;\n try {\n await ql.update(item.object, { id: item.newId, ...patch }, { context: SYSTEM_CTX });\n } catch (e) {\n logger?.warn?.('[org-scoping] cloneOrgSeedData: lookup remap failed', {\n object: item.object,\n id: item.newId,\n error: (e as Error).message,\n });\n }\n }\n\n return summary;\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * ensureDefaultOrganization — multi-tenant bootstrap helper.\n *\n * In multi-tenant deployments the freshly-promoted platform admin\n * (`admin_full_access` granted with `organization_id IS NULL`) needs\n * at least one `sys_organization` to carry an `activeOrganizationId`\n * on their session. Without it, the default `tenant_isolation` RLS\n * policy filters everything to zero rows and the admin sees an empty\n * console even though they have full access.\n *\n * Strategy (idempotent, run on `kernel:ready` and after every\n * `sys_user_permission_set` insert):\n *\n * 1. Find the platform admin (oldest `sys_user_permission_set` row\n * with `permission_set_id = admin_full_access` and\n * `organization_id IS NULL`). If none, no-op.\n * 2. If that user already has any `sys_member` row, no-op (they\n * either created their own org or were invited into one — we\n * respect that and never auto-create a \"Default Organization\"\n * behind their back).\n * 3. Re-use a pre-existing `slug='default'` org if present;\n * otherwise create one. Stable slug keeps human-readable URLs\n * predictable across cold-boots.\n * 4. Insert a `sys_member { role: 'owner' }` linking the admin to\n * the default org.\n *\n * This is the ONLY framework-side auto-provisioning of an org.\n * Subsequent users must accept an invitation or explicitly create\n * their first organization — `claimOrphanOrgRows` / `cloneOrgSeedData`\n * handle the seed-data side for those flows.\n */\n\ninterface EnsureOptions {\n logger?: {\n info: (message: string, meta?: Record<string, any>) => void;\n warn: (message: string, meta?: Record<string, any>) => void;\n };\n}\n\nconst SYSTEM_CTX = { isSystem: true };\n\nasync function tryFind(ql: any, object: string, where: any, limit = 100): Promise<any[]> {\n try {\n const rows = await ql.find(object, { where, limit }, { context: SYSTEM_CTX });\n return Array.isArray(rows) ? rows : Array.isArray(rows?.records) ? rows.records : [];\n } catch {\n return [];\n }\n}\n\nasync function tryInsert(ql: any, object: string, data: any): Promise<any | null> {\n try {\n return await ql.insert(object, data, { context: SYSTEM_CTX });\n } catch {\n return null;\n }\n}\n\nfunction genId(prefix: string): string {\n const rand = Math.random().toString(36).slice(2, 10);\n const ts = Date.now().toString(36);\n return `${prefix}_${ts}${rand}`;\n}\n\nexport interface EnsureDefaultOrganizationResult {\n /** Whether a brand-new org row was inserted (vs. re-using slug=default). */\n defaultOrgCreated: boolean;\n /** Resolved (or freshly minted) default-org id; undefined when no admin exists yet. */\n defaultOrgId?: string;\n /** Whether a sys_member row was inserted binding the admin to the default org. */\n memberCreated: boolean;\n /** Human-readable reason when the helper short-circuited. */\n reason?: 'no_admin' | 'admin_already_in_org' | 'org_insert_failed' | 'member_insert_failed';\n}\n\n/**\n * Ensure the platform admin has a Default Organization to operate in.\n * Safe to call multiple times — idempotent on stable slug `default`\n * and on the presence of any existing `sys_member` row for the admin.\n */\nexport async function ensureDefaultOrganization(\n ql: any,\n options: EnsureOptions = {},\n): Promise<EnsureDefaultOrganizationResult> {\n const logger = options.logger;\n if (!ql || typeof ql.find !== 'function' || typeof ql.insert !== 'function') {\n return { defaultOrgCreated: false, memberCreated: false, reason: 'no_admin' };\n }\n\n // 1. Find the platform admin permission-set id.\n const adminPs = await tryFind(ql, 'sys_permission_set', { name: 'admin_full_access' }, 1);\n if (adminPs.length === 0 || !adminPs[0].id) {\n return { defaultOrgCreated: false, memberCreated: false, reason: 'no_admin' };\n }\n const adminPsId = adminPs[0].id;\n\n // 2. Find the platform admin user (oldest cross-tenant grant).\n const adminGrants = await tryFind(\n ql,\n 'sys_user_permission_set',\n { permission_set_id: adminPsId, organization_id: null },\n 50,\n );\n if (adminGrants.length === 0) {\n return { defaultOrgCreated: false, memberCreated: false, reason: 'no_admin' };\n }\n const sortedGrants = [...adminGrants].sort((a, b) => {\n const ta = a.created_at ? new Date(a.created_at).getTime() : 0;\n const tb = b.created_at ? new Date(b.created_at).getTime() : 0;\n return ta - tb;\n });\n const adminUserId: string | undefined = sortedGrants[0]?.user_id;\n if (!adminUserId) {\n return { defaultOrgCreated: false, memberCreated: false, reason: 'no_admin' };\n }\n\n // 3. Respect existing membership — never auto-create a default org\n // behind an admin who already belongs somewhere.\n const memberships = await tryFind(ql, 'sys_member', { user_id: adminUserId }, 1);\n if (memberships.length > 0) {\n return {\n defaultOrgCreated: false,\n memberCreated: false,\n reason: 'admin_already_in_org',\n };\n }\n\n // 4. Re-use or create the `default` org.\n let defaultOrgId: string | undefined;\n let defaultOrgCreated = false;\n const existingDefault = await tryFind(ql, 'sys_organization', { slug: 'default' }, 1);\n if (existingDefault.length > 0 && existingDefault[0].id) {\n defaultOrgId = String(existingDefault[0].id);\n } else {\n const newOrgId = genId('org');\n const orgRow = await tryInsert(ql, 'sys_organization', {\n id: newOrgId,\n name: 'Default Organization',\n slug: 'default',\n logo: null,\n metadata: null,\n });\n if (!orgRow) {\n logger?.warn?.('[org-scoping] failed to create default organization for platform admin');\n return { defaultOrgCreated: false, memberCreated: false, reason: 'org_insert_failed' };\n }\n defaultOrgId = orgRow?.id ?? newOrgId;\n defaultOrgCreated = true;\n }\n\n // 5. Bind the admin as owner.\n const memRow = await tryInsert(ql, 'sys_member', {\n id: genId('mem'),\n organization_id: defaultOrgId,\n user_id: adminUserId,\n role: 'owner',\n });\n if (!memRow) {\n logger?.warn?.('[org-scoping] failed to bind platform admin to default organization');\n return {\n defaultOrgCreated,\n defaultOrgId,\n memberCreated: false,\n reason: 'member_insert_failed',\n };\n }\n logger?.info?.(\n `[org-scoping] bound platform admin to default organization (${defaultOrgId})`,\n { userId: adminUserId, defaultOrgId },\n );\n return { defaultOrgCreated, defaultOrgId, memberCreated: true };\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * Canonical plugin-org-scoping manifest source.\n *\n * Imported by `objectstack.config.ts` (compile-time) and\n * `org-scoping-plugin.ts` (runtime `manifest.register`) so the two\n * registration paths cannot drift.\n */\n\nexport const ORG_SCOPING_PLUGIN_ID = 'com.objectstack.plugin-org-scoping';\nexport const ORG_SCOPING_PLUGIN_VERSION = '1.0.0';\n\n/** This plugin owns no `sys_*` objects — Organization itself lives in `@objectstack/platform-objects`. */\nexport const orgScopingObjects = [] as const;\n\n/** Manifest header shared by compile-time config and runtime registration. */\nexport const orgScopingPluginManifestHeader = {\n id: ORG_SCOPING_PLUGIN_ID,\n namespace: 'sys',\n version: ORG_SCOPING_PLUGIN_VERSION,\n type: 'plugin' as const,\n scope: 'system' as const,\n defaultDatasource: 'cloud',\n name: 'Organization Scoping Plugin',\n description:\n 'Row-level Organization isolation: auto-stamps `organization_id` on insert from ' +\n '`ExecutionContext.tenantId`, replays seed datasets per new org, and bootstraps a default ' +\n 'organization for the first platform admin.',\n};\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport { Plugin, PluginContext } from '@objectstack/core';\nimport { claimOrphanOrgRows } from './claim-orphan-org-rows.js';\nimport { cloneOrgSeedData } from './clone-org-seed-data.js';\nimport { ensureDefaultOrganization } from './ensure-default-organization.js';\nimport {\n orgScopingObjects,\n orgScopingPluginManifestHeader,\n} from './manifest.js';\n\nexport interface OrgScopingPluginOptions {\n /**\n * Whether to auto-create a `Default Organization` (slug `default`)\n * and bind the first platform admin as `owner` when they have zero\n * memberships. Set to `false` for deployments that fully self-manage\n * org provisioning via invitation links or a custom onboarding flow.\n *\n * @default true\n */\n ensureDefaultOrganization?: boolean;\n}\n\n/**\n * OrgScopingPlugin\n *\n * Makes `sys_organization` a first-class row-level isolation boundary:\n *\n * 1. **insert auto-stamp** — on every authenticated `insert` whose\n * target object declares `organization_id`, fill the column from\n * `ExecutionContext.tenantId`. Without this, freshly-created\n * rows have `organization_id = NULL` and the default\n * `tenant_isolation` RLS policy hides them from the very user\n * who just created them.\n *\n * 2. **per-org seed replay** — after `sys_organization` insert, copy\n * the artifact's demo seed data into the new org. Three paths\n * (in order of preference):\n * a. replay registered `seed-datasets` via the kernel-level\n * `seed-replayer` callable (set by AppPlugin),\n * b. for the FIRST org, `claimOrphanOrgRows` adopts any\n * NULL-org rows a previous inline-seed may have inserted,\n * c. for SUBSEQUENT orgs, `cloneOrgSeedData` shallow-clones\n * rows from the very first org (donor-pattern).\n *\n * 3. **default-org bootstrap** — on `kernel:ready` and after every\n * `sys_user_permission_set` insert, ensure the platform admin has\n * a Default Organization to operate in (idempotent on slug\n * `default` + admin's existing memberships).\n *\n * Why split from plugin-security:\n * - plugin-security is a single-tenant-aware RBAC + RLS engine; it\n * should not know about Organization-specific seed flows.\n * - This plugin is purely opt-in: not installing it gives a\n * single-tenant deployment (no `organization_id` injection, no\n * per-org seed clone, no default-org bootstrap). plugin-security\n * detects its presence via `getService('org-scoping')` and adjusts\n * RLS policy stripping accordingly.\n *\n * Naming note: \"org-scoping\" deliberately avoids the word \"tenant\"\n * because in ObjectStack \"tenant\" already means *physical isolation*\n * (one Environment = one database, per ADR-0002 and driver-turso's\n * multi-tenant router). This plugin is about LOGICAL row-level\n * scoping inside a single database — orthogonal to physical tenancy.\n *\n * Dependencies:\n * - `objectql` (engine middleware host)\n */\nexport class OrgScopingPlugin implements Plugin {\n name = 'com.objectstack.org-scoping';\n type = 'standard';\n version = '1.0.0';\n dependencies = ['com.objectstack.engine.objectql'];\n\n /** Per-object field-name cache; same shape as SecurityPlugin's. */\n private readonly fieldNamesCache = new Map<string, Set<string> | null>();\n\n private readonly opts: Required<OrgScopingPluginOptions>;\n\n constructor(options: OrgScopingPluginOptions = {}) {\n this.opts = {\n ensureDefaultOrganization: options.ensureDefaultOrganization !== false,\n };\n }\n\n async init(ctx: PluginContext): Promise<void> {\n ctx.logger.info('Initializing Org-Scoping Plugin...');\n // Service registration doubles as plugin-security's\n // \"multi-tenant mode is on\" probe: SecurityPlugin queries\n // `getService('org-scoping')` and keeps wildcard\n // `current_user.organization_id` RLS policies when this returns.\n ctx.registerService('org-scoping', this);\n\n ctx\n .getService<{ register(m: any): void }>('manifest')\n .register({\n ...orgScopingPluginManifestHeader,\n objects: orgScopingObjects,\n });\n ctx.logger.info('Org-Scoping Plugin initialized');\n }\n\n async start(ctx: PluginContext): Promise<void> {\n ctx.logger.info('Starting Org-Scoping Plugin...');\n\n let ql: any;\n let metadata: any;\n try {\n ql = ctx.getService('objectql');\n try {\n metadata = ctx.getService('metadata');\n } catch {\n metadata = undefined;\n }\n } catch {\n ctx.logger.warn(\n 'ObjectQL service not available, org-scoping middleware not registered',\n );\n return;\n }\n if (!ql || typeof ql.registerMiddleware !== 'function') {\n ctx.logger.warn(\n 'ObjectQL engine does not support middleware, org-scoping middleware not registered',\n );\n return;\n }\n\n // ── Middleware A: auto-stamp `organization_id` on insert ──────────\n ql.registerMiddleware(async (opCtx: any, next: () => Promise<void>) => {\n if (opCtx.context?.isSystem) return next();\n if (\n opCtx.operation === 'insert' &&\n opCtx.data &&\n typeof opCtx.data === 'object' &&\n !Array.isArray(opCtx.data) &&\n opCtx.context?.tenantId\n ) {\n const fields = await this.getObjectFieldNames(metadata, opCtx.object, ql);\n if (fields && fields.has('organization_id')) {\n const data = opCtx.data as Record<string, unknown>;\n if (data.organization_id == null || data.organization_id === '') {\n data.organization_id = opCtx.context.tenantId;\n }\n }\n }\n await next();\n });\n\n // ── Middleware B: per-org seed pipeline on sys_organization insert ─\n ql.registerMiddleware(async (opCtx: any, next: () => Promise<void>) => {\n await next();\n if (\n opCtx?.object !== 'sys_organization' ||\n (opCtx?.operation !== 'create' && opCtx?.operation !== 'insert')\n ) {\n return;\n }\n const newOrgId = opCtx?.result?.id ?? opCtx?.data?.id;\n if (!newOrgId) return;\n\n const kernel: any = (ctx as any).kernel ?? ctx;\n let datasets: any[] | undefined;\n try {\n const raw = kernel?.getService?.('seed-datasets');\n if (Array.isArray(raw) && raw.length > 0) datasets = raw;\n } catch {\n /* service not registered */\n }\n\n // Count existing orgs to pick the right fallback path.\n let orgCount = 0;\n try {\n const allOrgs = await ql.find(\n 'sys_organization',\n { limit: 2, fields: ['id'] },\n { context: { isSystem: true } },\n );\n const list: any[] = Array.isArray(allOrgs)\n ? allOrgs\n : Array.isArray(allOrgs?.records)\n ? allOrgs.records\n : [];\n orgCount = list.length;\n } catch (e) {\n ctx.logger.warn('[org-scoping] failed to count organizations', {\n error: (e as Error).message,\n });\n }\n\n // Primary path: SeedLoader replay scoped to newOrgId.\n let replayed = false;\n try {\n const replayer: any = kernel?.getService?.('seed-replayer');\n if (typeof replayer === 'function') {\n const summary = await replayer(newOrgId);\n const total = (summary?.inserted ?? 0) + (summary?.updated ?? 0);\n ctx.logger.info(\n `[org-scoping] per-org seed replay for ${newOrgId}: +${summary?.inserted ?? 0} inserted, ${summary?.updated ?? 0} updated, ${summary?.errors?.length ?? 0} error(s)`,\n {\n organizationId: newOrgId,\n errors: summary?.errors?.slice?.(0, 5),\n },\n );\n if (total > 0) replayed = true;\n } else if (datasets) {\n ctx.logger.warn(\n '[org-scoping] per-org seed: datasets present but no replayer registered',\n { organizationId: newOrgId },\n );\n }\n } catch (e) {\n ctx.logger.warn(\n '[org-scoping] per-org seed replay failed, falling back',\n { organizationId: newOrgId, error: (e as Error).message },\n );\n }\n if (replayed) return;\n\n // Fallback A: legacy claim for first org.\n if (orgCount === 1) {\n try {\n const claims = await claimOrphanOrgRows(ql, newOrgId, { logger: ctx.logger });\n if (claims.length > 0) {\n const total = claims.reduce((s, c) => s + c.count, 0);\n ctx.logger.info(\n `[org-scoping] claimed ${total} orphan seed row(s) for first organization ${newOrgId}`,\n { breakdown: claims },\n );\n return;\n }\n } catch (e) {\n ctx.logger.warn('[org-scoping] claim-orphan-org-rows failed', {\n error: (e as Error).message,\n });\n }\n }\n\n // Fallback B: clone from donor org for subsequent orgs.\n if (orgCount > 1) {\n try {\n const summary = await cloneOrgSeedData(ql, newOrgId, { logger: ctx.logger });\n if (summary.length > 0) {\n const total = summary.reduce((s, c) => s + c.count, 0);\n ctx.logger.info(\n `[org-scoping] cloned ${total} seed row(s) for new organization ${newOrgId}`,\n { breakdown: summary },\n );\n }\n } catch (e) {\n ctx.logger.warn('[org-scoping] clone-org-seed-data failed', {\n organizationId: newOrgId,\n error: (e as Error).message,\n });\n }\n }\n });\n\n // ── Default-org bootstrap on kernel:ready + on admin grant ────────\n if (this.opts.ensureDefaultOrganization) {\n const runEnsure = async () => {\n try {\n const res = await ensureDefaultOrganization(ql, { logger: ctx.logger });\n if (res.defaultOrgCreated) {\n ctx.logger.info(\n `[org-scoping] created Default Organization ${res.defaultOrgId} for platform admin`,\n );\n }\n } catch (e) {\n ctx.logger.warn?.('[org-scoping] ensureDefaultOrganization failed', {\n error: (e as Error).message,\n });\n }\n };\n if (typeof (ctx as any).hook === 'function') {\n (ctx as any).hook('kernel:ready', runEnsure);\n } else {\n void runEnsure();\n }\n // Re-run after every admin grant — handles the \"first sign-up\n // promoted to platform admin\" case where the kernel:ready hook\n // fired before any user existed.\n ql.registerMiddleware(async (opCtx: any, next: () => Promise<void>) => {\n await next();\n if (\n opCtx?.object === 'sys_user_permission_set' &&\n (opCtx?.operation === 'insert' || opCtx?.operation === 'create')\n ) {\n await runEnsure();\n }\n });\n }\n\n ctx.logger.info('Org-Scoping middleware registered on ObjectQL engine');\n }\n\n async destroy(): Promise<void> {\n // No cleanup needed\n }\n\n /**\n * Resolve the column-name set for an object (mirrors SecurityPlugin's\n * loader so the two plugins behave consistently). Returns `null` if\n * the schema can't be loaded — caller skips injection.\n */\n private async getObjectFieldNames(\n metadata: any,\n objectName: string,\n ql?: any,\n ): Promise<Set<string> | null> {\n if (this.fieldNamesCache.has(objectName)) {\n return this.fieldNamesCache.get(objectName) ?? null;\n }\n const result = await this.loadObjectFieldNames(metadata, objectName, ql);\n if (result) this.fieldNamesCache.set(objectName, result);\n return result;\n }\n\n private async loadObjectFieldNames(\n metadata: any,\n objectName: string,\n ql?: any,\n ): Promise<Set<string> | null> {\n try {\n let obj: any =\n typeof ql?.getSchema === 'function' ? ql.getSchema(objectName) : null;\n if (!obj || !obj.fields) {\n obj = await metadata?.get?.('object', objectName);\n }\n if (!obj || !obj.fields) return null;\n const set = new Set<string>(['id']);\n if (Array.isArray(obj.fields)) {\n for (const f of obj.fields) {\n if (f?.name) set.add(String(f.name));\n }\n } else if (typeof obj.fields === 'object') {\n for (const key of Object.keys(obj.fields)) {\n set.add(key);\n const v = (obj.fields as Record<string, any>)[key];\n if (v && typeof v === 'object' && v.name) set.add(String(v.name));\n }\n } else {\n return null;\n }\n return set;\n } catch {\n return null;\n }\n }\n}\n"],"mappings":"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","names":["SYSTEM_CTX","SYSTEM_CTX"]}
1
+ {"version":3,"sources":["../src/claim-orphan-org-rows.ts","../src/clone-org-seed-data.ts","../src/claim-org-seed-ownership.ts","../src/ensure-default-organization.ts","../src/manifest.ts","../src/org-scoping-plugin.ts"],"sourcesContent":["// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * claimOrphanOrgRows — assign seed-loaded records to the first organization.\n *\n * Seeds (`defineSeed`) are inserted by `SeedLoaderService` using\n * `{ context: { isSystem: true } }`, which intentionally bypasses\n * SecurityPlugin's `organization_id` auto-fill. As a result, in\n * multi-tenant mode every seed row lands with `organization_id = NULL`.\n *\n * That's correct for **cross-tenant metadata** — `sys_permission_set`\n * rows, default roles, etc. (objects whose schema has `managedBy` set)\n * — but for **business-domain seeds** (CRM `lead`, `account`, `contact`,\n * …) it means the rows are invisible to anyone bound to an organization\n * (the default `tenant_isolation` RLS policy\n * `organization_id = current_user.organization_id` filters them out).\n *\n * This helper runs **once**, on first-organization creation, and\n * back-fills `organization_id` on every orphaned (`organization_id IS\n * NULL`) seed row of every user-defined object that declares the\n * column. Result: out of the box, the freshly registered owner sees the\n * shipped demo data scoped to their first org — no manual claim step.\n *\n * Idempotent: a no-op once an organization-tagged row exists, and\n * `managedBy` schemas (`sys_*` better-auth/platform tables) are always\n * skipped so cross-tenant defaults stay cross-tenant.\n */\n\nimport type { ServiceObject } from '@objectstack/spec/data';\n\ninterface ClaimOptions {\n logger?: {\n info: (message: string, meta?: Record<string, any>) => void;\n warn: (message: string, meta?: Record<string, any>) => void;\n };\n}\n\nconst SYSTEM_CTX = { isSystem: true };\n\nfunction hasOrganizationField(schema: ServiceObject): boolean {\n const fields: any = (schema as any)?.fields;\n if (!fields) return false;\n if (Array.isArray(fields)) {\n return fields.some((f) => f?.name === 'organization_id');\n }\n return Object.prototype.hasOwnProperty.call(fields, 'organization_id');\n}\n\n/**\n * Assign every orphaned seed row to `organizationId`.\n *\n * Walks `ql.registry.getAllObjects()`, filters to schemas that\n * (a) are not `managedBy` (skip sys_/auth/platform tables),\n * (b) declare an `organization_id` field,\n * and runs an `update(where: { organization_id: null }, patch: {\n * organization_id: organizationId })` against each as `isSystem`.\n *\n * Returns a per-object summary `{ object, count }[]`.\n */\nexport async function claimOrphanOrgRows(\n ql: any,\n organizationId: string,\n options: ClaimOptions = {},\n): Promise<{ object: string; count: number }[]> {\n const logger = options.logger;\n if (!ql || typeof ql.update !== 'function' || typeof ql.find !== 'function') {\n return [];\n }\n const registry = (ql as any).registry;\n if (!registry || typeof registry.getAllObjects !== 'function') {\n logger?.warn?.('[org-scoping] claimOrphanOrgRows: registry unavailable');\n return [];\n }\n\n const schemas: ServiceObject[] = registry.getAllObjects();\n const results: { object: string; count: number }[] = [];\n\n for (const schema of schemas) {\n if (!schema?.name) continue;\n if ((schema as any).managedBy) continue;\n // Defense in depth: any platform-namespaced object (`sys_*`) is\n // off-limits for tenant claim regardless of `managedBy`. Platform\n // tables that should be tenant-scoped are inserted with an explicit\n // `organization_id` by the code that owns them, so they will never\n // be orphans here.\n if (schema.name.startsWith('sys_')) continue;\n if (!hasOrganizationField(schema)) continue;\n\n try {\n const orphans = await ql.find(\n schema.name,\n { where: { organization_id: null }, limit: 10_000, fields: ['id'] },\n { context: SYSTEM_CTX },\n );\n const list: any[] = Array.isArray(orphans)\n ? orphans\n : Array.isArray(orphans?.records)\n ? orphans.records\n : [];\n if (list.length === 0) continue;\n\n let updated = 0;\n for (const row of list) {\n if (!row?.id) continue;\n try {\n await ql.update(\n schema.name,\n { id: row.id, organization_id: organizationId },\n { context: SYSTEM_CTX },\n );\n updated += 1;\n } catch (e) {\n logger?.warn?.(`[org-scoping] claim failed for ${schema.name}:${row.id}`, {\n error: (e as Error).message,\n });\n }\n }\n if (updated > 0) {\n results.push({ object: schema.name, count: updated });\n }\n } catch (e) {\n logger?.warn?.(`[org-scoping] claim scan failed for ${schema.name}`, {\n error: (e as Error).message,\n });\n }\n }\n\n if (results.length > 0) {\n const total = results.reduce((s, r) => s + r.count, 0);\n logger?.info?.(`[org-scoping] claimed ${total} orphan seed row(s) for organization ${organizationId}`, {\n breakdown: results,\n });\n }\n return results;\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * cloneOrgSeedData — give every newly-registered org its own copy of\n * the demo seed data.\n *\n * Multi-tenant deployments treat each `sys_organization` as a hard\n * isolation boundary. The platform-wide `claimOrphanOrgRows` hook\n * (see `claim-orphan-tenant-rows.ts`) only fires for the very first\n * org — every subsequent org (created explicitly by a user via\n * `createOrganization`, or by an admin from the console) starts\n * empty. For demo / trial-org UX (Salesforce-style \"you get a\n * fully populated sandbox on signup\"), we want every freshly minted\n * org to receive a private clone of the platform-first org's\n * user-defined data.\n *\n * Strategy:\n * 1. Pick the donor org — the very first `sys_organization`.\n * 2. Walk `ql.registry.getAllObjects()` once to collect schemas\n * that are user-defined (not `managedBy`, not `sys_*`) AND\n * declare an `organization_id` field.\n * 3. Pass A — for each donor object, find rows where\n * `organization_id = donorOrgId`, generate a new id, insert a\n * shallow copy under `targetOrgId`, recording an\n * `oldId → newId` map keyed by object name. Lookup field values\n * pointing at donor rows are left untouched in this pass; the\n * remap happens in pass B so we don't depend on topological\n * ordering of inserts.\n * 4. Pass B — for each cloned row, walk its lookup-shaped fields\n * and rewrite values that match the donor map for the field's\n * `reference` object.\n *\n * Idempotent: skipped if the target org already has rows in any\n * cloned object, or if no donor org exists, or if the target IS the\n * donor (claim hook handles the donor itself).\n *\n * Best-effort: per-object failures are logged at `warn` and don't\n * abort the rest of the clone. FK fields that reference an object\n * that wasn't cloned (e.g. the lookup target lives in `sys_*`, or\n * the remap key isn't present) are left as-is — broken refs are\n * preferable to losing whole rows.\n */\n\nimport type { ServiceObject } from '@objectstack/spec/data';\n\ninterface CloneOptions {\n logger?: {\n info: (message: string, meta?: Record<string, any>) => void;\n warn: (message: string, meta?: Record<string, any>) => void;\n };\n}\n\ninterface FieldDescriptor {\n name: string;\n type?: string;\n reference?: string;\n multiple?: boolean;\n unique?: boolean;\n}\n\nconst SYSTEM_CTX = { isSystem: true };\n\nconst SKIP_COPY_FIELDS = new Set<string>([\n 'id',\n 'created_at',\n 'updated_at',\n 'organization_id',\n]);\n\n// Computed / virtual / system-managed field types — these have no\n// physical column in the DB, so re-inserting them would fail with\n// \"table X has no column named Y\". `find()` returns them in the\n// projected row (formula evaluation, rollup summary), but they must\n// NEVER be sent back to `insert()`.\n//\n// NOTE: `autonumber` IS a real string column in the SQL driver — it\n// has no auto-generation in this codebase, the value comes from the\n// seed file itself. Cloning it preserves the demo's \"CTR-0001\" /\n// \"QTE-0001\" identifiers so users see meaningful titleFormats and\n// the `externalId` upsert key keeps working on subsequent re-seeds.\nconst SKIP_COPY_TYPES = new Set<string>(['formula', 'summary']);\n\nfunction fieldList(schema: ServiceObject): FieldDescriptor[] {\n const fields: any = (schema as any)?.fields;\n if (!fields) return [];\n if (Array.isArray(fields)) {\n return fields.map((f: any) => ({\n name: f?.name,\n type: f?.type,\n reference: f?.reference,\n multiple: f?.multiple,\n unique: f?.unique,\n }));\n }\n return Object.entries(fields as Record<string, any>).map(([name, f]) => ({\n name,\n type: f?.type,\n reference: f?.reference,\n multiple: f?.multiple,\n unique: f?.unique,\n }));\n}\n\nfunction isLookupField(f: FieldDescriptor): boolean {\n return (f.type === 'lookup' || f.type === 'master_detail' || f.type === 'tree') && !!f.reference;\n}\n\nfunction hasOrgField(schema: ServiceObject): boolean {\n return fieldList(schema).some((f) => f.name === 'organization_id');\n}\n\nfunction shortId(): string {\n // Mirror the format `nanoid(16)` used elsewhere in the codebase\n // without pulling a runtime dep here.\n const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-';\n let out = '';\n for (let i = 0; i < 16; i++) {\n out += alphabet[Math.floor(Math.random() * alphabet.length)];\n }\n return out;\n}\n\nasync function findDonorOrgId(ql: any): Promise<string | null> {\n try {\n const res = await ql.find(\n 'sys_organization',\n { orderBy: { created_at: 'asc' }, limit: 1, fields: ['id'] },\n { context: SYSTEM_CTX },\n );\n const list: any[] = Array.isArray(res) ? res : Array.isArray(res?.records) ? res.records : [];\n return list[0]?.id ?? null;\n } catch {\n return null;\n }\n}\n\nexport async function cloneOrgSeedData(\n ql: any,\n targetOrgId: string,\n options: CloneOptions = {},\n): Promise<{ object: string; count: number }[]> {\n const logger = options.logger;\n if (!ql || typeof ql.find !== 'function' || typeof ql.insert !== 'function') {\n return [];\n }\n const registry = (ql as any).registry;\n if (!registry || typeof registry.getAllObjects !== 'function') {\n logger?.warn?.('[org-scoping] cloneOrgSeedData: registry unavailable');\n return [];\n }\n\n const donorOrgId = await findDonorOrgId(ql);\n if (!donorOrgId) return [];\n if (donorOrgId === targetOrgId) return [];\n\n const schemas: ServiceObject[] = registry.getAllObjects().filter(\n (s: any) => s?.name && !s.managedBy && !s.name.startsWith('sys_') && hasOrgField(s),\n );\n\n // Pass A: clone rows shallowly, build per-object oldId → newId map.\n const remap: Record<string, Record<string, string>> = {};\n const summary: { object: string; count: number }[] = [];\n // Track inserted shadow records so pass B can rewrite their lookups\n // without re-fetching from the DB.\n const inserted: { object: string; newId: string; record: Record<string, unknown>; lookups: FieldDescriptor[] }[] = [];\n\n for (const schema of schemas) {\n const objectName = schema.name as string;\n try {\n // Idempotency: if target org already has any row in this object,\n // assume a previous clone (or manual data) and skip — never\n // double-clone.\n const existing = await ql.find(\n objectName,\n { where: { organization_id: targetOrgId }, limit: 1, fields: ['id'] },\n { context: SYSTEM_CTX },\n );\n const existingList: any[] = Array.isArray(existing)\n ? existing\n : Array.isArray(existing?.records)\n ? existing.records\n : [];\n if (existingList.length > 0) {\n continue;\n }\n\n const donorRows = await ql.find(\n objectName,\n { where: { organization_id: donorOrgId }, limit: 10_000 },\n { context: SYSTEM_CTX },\n );\n const rows: any[] = Array.isArray(donorRows)\n ? donorRows\n : Array.isArray(donorRows?.records)\n ? donorRows.records\n : [];\n if (rows.length === 0) continue;\n\n const fields = fieldList(schema);\n const lookups = fields.filter(isLookupField);\n const uniqueFields = fields.filter((f) => f.unique && !SKIP_COPY_FIELDS.has(f.name));\n const objectRemap: Record<string, string> = (remap[objectName] ??= {});\n let cloned = 0;\n for (const row of rows) {\n const newId = shortId();\n const data: Record<string, unknown> = { id: newId, organization_id: targetOrgId };\n for (const f of fields) {\n if (SKIP_COPY_FIELDS.has(f.name)) continue;\n if (f.type && SKIP_COPY_TYPES.has(f.type)) continue;\n if (row[f.name] === undefined) continue;\n data[f.name] = row[f.name];\n }\n // Disambiguate UNIQUE columns. Many seed schemas declare\n // single-column unique indexes (e.g. `lead.email`) without\n // tenant scoping — cloning the donor row verbatim would\n // collide. Append a per-tenant suffix so each org gets its\n // own copy.\n const suffix = `+${targetOrgId.slice(-6)}`;\n for (const uf of uniqueFields) {\n const v = data[uf.name];\n if (typeof v !== 'string' || !v) continue;\n if (uf.type === 'email' && v.includes('@')) {\n const [local, domain] = v.split('@');\n data[uf.name] = `clone-${targetOrgId.slice(-6)}-${local}@${domain}`;\n } else {\n data[uf.name] = `${v}${suffix}`;\n }\n }\n try {\n await ql.insert(objectName, data, { context: SYSTEM_CTX });\n objectRemap[row.id] = newId;\n inserted.push({ object: objectName, newId, record: data, lookups });\n cloned++;\n } catch (e) {\n logger?.warn?.('[org-scoping] cloneOrgSeedData: insert failed', {\n object: objectName,\n error: (e as Error).message,\n });\n }\n }\n if (cloned > 0) summary.push({ object: objectName, count: cloned });\n } catch (e) {\n logger?.warn?.('[org-scoping] cloneOrgSeedData: object failed', {\n object: objectName,\n error: (e as Error).message,\n });\n }\n }\n\n // Pass B: rewrite lookup field values using the per-object remap so\n // intra-clone relationships stay intact.\n //\n // Cross-tenant FK hygiene: when a donor row's lookup value DOESN'T\n // appear in `remap[reference]` (i.e. the donor itself had a stale\n // FK pointing at another tenant's record, or the referenced object\n // wasn't included in this clone), we NULL the field instead of\n // leaving the orphan string in place. Otherwise every subsequent\n // clone perpetuates the broken FK chain (donor → tenant A → tenant\n // B → ...) and renderers display raw IDs because `find()` for the\n // referenced ID returns no row in the current tenant.\n for (const item of inserted) {\n if (item.lookups.length === 0) continue;\n const patch: Record<string, unknown> = {};\n let dirty = false;\n for (const f of item.lookups) {\n const oldVal = item.record[f.name];\n if (oldVal == null) continue;\n const targetMap = remap[f.reference!];\n if (Array.isArray(oldVal)) {\n // For multi-value lookups: remap when possible, drop entries\n // that have no remap (rather than keep an orphan string).\n const next = oldVal\n .map((v: any) => (typeof v === 'string' && targetMap?.[v]) || null)\n .filter((v: any) => v != null);\n if (next.length !== oldVal.length || next.some((v, i) => v !== oldVal[i])) {\n patch[f.name] = next.length > 0 ? next : null;\n dirty = true;\n }\n } else if (typeof oldVal === 'string') {\n if (targetMap && targetMap[oldVal]) {\n patch[f.name] = targetMap[oldVal];\n dirty = true;\n } else {\n // Unresolvable cross-tenant reference — null it out so the\n // UI shows \"empty\" rather than a dangling ID.\n patch[f.name] = null;\n dirty = true;\n }\n }\n }\n if (!dirty) continue;\n try {\n await ql.update(item.object, { id: item.newId, ...patch }, { context: SYSTEM_CTX });\n } catch (e) {\n logger?.warn?.('[org-scoping] cloneOrgSeedData: lookup remap failed', {\n object: item.object,\n id: item.newId,\n error: (e as Error).message,\n });\n }\n }\n\n return summary;\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * claimOrgSeedOwnership — hand an organization's seeded records to its owner.\n *\n * The multi-tenant twin of plugin-security's `claimSeedOwnership` (single-tenant\n * first-admin handoff). Seeded rows land `owner_id = NULL` (the author leaves it\n * unset and `cel`os.user.id`` resolves to NULL at seed time, since the owning\n * admin does not exist yet). In multi-tenant mode those rows are scoped to an\n * org by `claimOrphanOrgRows` / per-org replay, but their `owner_id` stays NULL\n * — so \"My\" views, owner reports and owner notifications are empty for the org's\n * members until ownership is assigned.\n *\n * This runs when the org's owner is established (e.g. `ensureDefaultOrganization`\n * binds the platform admin as the default org's `owner`) and assigns\n * `owner_id = ownerUserId` to that org's NULL-owned rows — the ownership\n * companion to `claimOrphanOrgRows`'s `organization_id` back-fill.\n *\n * Scoped to a single org (`organization_id = organizationId`) so it never\n * touches another tenant's rows. Idempotent: only NULL-owned rows are updated.\n * `managedBy` and `sys_*` tables are skipped.\n */\n\nimport type { ServiceObject } from '@objectstack/spec/data';\n\ninterface ClaimOwnershipOptions {\n logger?: {\n info: (message: string, meta?: Record<string, any>) => void;\n warn: (message: string, meta?: Record<string, any>) => void;\n };\n}\n\nconst SYSTEM_CTX = { isSystem: true };\n\nfunction hasField(schema: ServiceObject, field: string): boolean {\n const fields: any = (schema as any)?.fields;\n if (!fields) return false;\n if (Array.isArray(fields)) return fields.some((f) => f?.name === field);\n return Object.prototype.hasOwnProperty.call(fields, field);\n}\n\n/**\n * Assign `owner_id = ownerUserId` to every NULL-owned seed row of `organizationId`.\n *\n * Walks `ql.registry.getAllObjects()`, filters to schemas that\n * (a) are not `managedBy` (skip sys_/auth/platform tables),\n * (b) are not `sys_*`-namespaced,\n * (c) declare BOTH `owner_id` and `organization_id`,\n * and updates the org's unowned rows as `isSystem`. Returns a per-object summary.\n */\nexport async function claimOrgSeedOwnership(\n ql: any,\n organizationId: string,\n ownerUserId: string,\n options: ClaimOwnershipOptions = {},\n): Promise<{ object: string; count: number }[]> {\n const logger = options.logger;\n if (!organizationId || !ownerUserId) return [];\n if (!ql || typeof ql.update !== 'function' || typeof ql.find !== 'function') return [];\n const registry = (ql as any).registry;\n if (!registry || typeof registry.getAllObjects !== 'function') {\n logger?.warn?.('[org-scoping] claimOrgSeedOwnership: registry unavailable');\n return [];\n }\n\n const schemas: ServiceObject[] = registry.getAllObjects();\n const results: { object: string; count: number }[] = [];\n\n for (const schema of schemas) {\n if (!schema?.name) continue;\n if ((schema as any).managedBy) continue;\n if (schema.name.startsWith('sys_')) continue;\n // Both columns are required: owner_id to assign, organization_id to scope.\n if (!hasField(schema, 'owner_id') || !hasField(schema, 'organization_id')) continue;\n\n try {\n const orphans = await ql.find(\n schema.name,\n { where: { organization_id: organizationId, owner_id: null }, limit: 10_000, fields: ['id'] },\n { context: SYSTEM_CTX },\n );\n const list: any[] = Array.isArray(orphans)\n ? orphans\n : Array.isArray(orphans?.records)\n ? orphans.records\n : [];\n if (list.length === 0) continue;\n\n let updated = 0;\n for (const row of list) {\n if (!row?.id) continue;\n try {\n await ql.update(schema.name, { id: row.id, owner_id: ownerUserId }, { context: SYSTEM_CTX });\n updated += 1;\n } catch (e) {\n logger?.warn?.(`[org-scoping] claimOrgSeedOwnership failed for ${schema.name}:${row.id}`, {\n error: (e as Error).message,\n });\n }\n }\n if (updated > 0) results.push({ object: schema.name, count: updated });\n } catch (e) {\n logger?.warn?.(`[org-scoping] claimOrgSeedOwnership scan failed for ${schema.name}`, {\n error: (e as Error).message,\n });\n }\n }\n\n if (results.length > 0) {\n const total = results.reduce((s, r) => s + r.count, 0);\n logger?.info?.(`[org-scoping] handed ${total} seeded row(s) of org ${organizationId} to owner ${ownerUserId}`, {\n breakdown: results,\n });\n }\n return results;\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * ensureDefaultOrganization — multi-tenant bootstrap helper.\n *\n * In multi-tenant deployments the freshly-promoted platform admin\n * (`admin_full_access` granted with `organization_id IS NULL`) needs\n * at least one `sys_organization` to carry an `activeOrganizationId`\n * on their session. Without it, the default `tenant_isolation` RLS\n * policy filters everything to zero rows and the admin sees an empty\n * console even though they have full access.\n *\n * Strategy (idempotent, run on `kernel:ready` and after every\n * `sys_user_permission_set` insert):\n *\n * 1. Find the platform admin (oldest `sys_user_permission_set` row\n * with `permission_set_id = admin_full_access` and\n * `organization_id IS NULL`). If none, no-op.\n * 2. If that user already has any `sys_member` row, no-op (they\n * either created their own org or were invited into one — we\n * respect that and never auto-create a \"Default Organization\"\n * behind their back).\n * 3. Re-use a pre-existing `slug='default'` org if present;\n * otherwise create one. Stable slug keeps human-readable URLs\n * predictable across cold-boots.\n * 4. Insert a `sys_member { role: 'owner' }` linking the admin to\n * the default org.\n *\n * This is the ONLY framework-side auto-provisioning of an org.\n * Subsequent users must accept an invitation or explicitly create\n * their first organization — `claimOrphanOrgRows` / `cloneOrgSeedData`\n * handle the seed-data side for those flows.\n */\n\nimport { claimOrgSeedOwnership } from './claim-org-seed-ownership.js';\n\ninterface EnsureOptions {\n logger?: {\n info: (message: string, meta?: Record<string, any>) => void;\n warn: (message: string, meta?: Record<string, any>) => void;\n };\n}\n\nconst SYSTEM_CTX = { isSystem: true };\n\nasync function tryFind(ql: any, object: string, where: any, limit = 100): Promise<any[]> {\n try {\n const rows = await ql.find(object, { where, limit }, { context: SYSTEM_CTX });\n return Array.isArray(rows) ? rows : Array.isArray(rows?.records) ? rows.records : [];\n } catch {\n return [];\n }\n}\n\nasync function tryInsert(ql: any, object: string, data: any): Promise<any | null> {\n try {\n return await ql.insert(object, data, { context: SYSTEM_CTX });\n } catch {\n return null;\n }\n}\n\nfunction genId(prefix: string): string {\n const rand = Math.random().toString(36).slice(2, 10);\n const ts = Date.now().toString(36);\n return `${prefix}_${ts}${rand}`;\n}\n\nexport interface EnsureDefaultOrganizationResult {\n /** Whether a brand-new org row was inserted (vs. re-using slug=default). */\n defaultOrgCreated: boolean;\n /** Resolved (or freshly minted) default-org id; undefined when no admin exists yet. */\n defaultOrgId?: string;\n /** Whether a sys_member row was inserted binding the admin to the default org. */\n memberCreated: boolean;\n /** Human-readable reason when the helper short-circuited. */\n reason?: 'no_admin' | 'admin_already_in_org' | 'org_insert_failed' | 'member_insert_failed';\n /** Count of the default org's seeded rows re-owned to the platform admin. */\n ownershipClaimed?: number;\n}\n\n/**\n * Ensure the platform admin has a Default Organization to operate in.\n * Safe to call multiple times — idempotent on stable slug `default`\n * and on the presence of any existing `sys_member` row for the admin.\n */\nexport async function ensureDefaultOrganization(\n ql: any,\n options: EnsureOptions = {},\n): Promise<EnsureDefaultOrganizationResult> {\n const logger = options.logger;\n if (!ql || typeof ql.find !== 'function' || typeof ql.insert !== 'function') {\n return { defaultOrgCreated: false, memberCreated: false, reason: 'no_admin' };\n }\n\n // 1. Find the platform admin permission-set id.\n const adminPs = await tryFind(ql, 'sys_permission_set', { name: 'admin_full_access' }, 1);\n if (adminPs.length === 0 || !adminPs[0].id) {\n return { defaultOrgCreated: false, memberCreated: false, reason: 'no_admin' };\n }\n const adminPsId = adminPs[0].id;\n\n // 2. Find the platform admin user (oldest cross-tenant grant).\n const adminGrants = await tryFind(\n ql,\n 'sys_user_permission_set',\n { permission_set_id: adminPsId, organization_id: null },\n 50,\n );\n if (adminGrants.length === 0) {\n return { defaultOrgCreated: false, memberCreated: false, reason: 'no_admin' };\n }\n const sortedGrants = [...adminGrants].sort((a, b) => {\n const ta = a.created_at ? new Date(a.created_at).getTime() : 0;\n const tb = b.created_at ? new Date(b.created_at).getTime() : 0;\n return ta - tb;\n });\n const adminUserId: string | undefined = sortedGrants[0]?.user_id;\n if (!adminUserId) {\n return { defaultOrgCreated: false, memberCreated: false, reason: 'no_admin' };\n }\n\n // 3. Respect existing membership — never auto-create a default org\n // behind an admin who already belongs somewhere.\n const memberships = await tryFind(ql, 'sys_member', { user_id: adminUserId }, 1);\n if (memberships.length > 0) {\n return {\n defaultOrgCreated: false,\n memberCreated: false,\n reason: 'admin_already_in_org',\n };\n }\n\n // 4. Re-use or create the `default` org.\n let defaultOrgId: string | undefined;\n let defaultOrgCreated = false;\n const existingDefault = await tryFind(ql, 'sys_organization', { slug: 'default' }, 1);\n if (existingDefault.length > 0 && existingDefault[0].id) {\n defaultOrgId = String(existingDefault[0].id);\n } else {\n const newOrgId = genId('org');\n const orgRow = await tryInsert(ql, 'sys_organization', {\n id: newOrgId,\n name: 'Default Organization',\n slug: 'default',\n logo: null,\n metadata: null,\n });\n if (!orgRow) {\n logger?.warn?.('[org-scoping] failed to create default organization for platform admin');\n return { defaultOrgCreated: false, memberCreated: false, reason: 'org_insert_failed' };\n }\n defaultOrgId = orgRow?.id ?? newOrgId;\n defaultOrgCreated = true;\n }\n\n // 5. Bind the admin as owner.\n const memRow = await tryInsert(ql, 'sys_member', {\n id: genId('mem'),\n organization_id: defaultOrgId,\n user_id: adminUserId,\n role: 'owner',\n });\n if (!memRow) {\n logger?.warn?.('[org-scoping] failed to bind platform admin to default organization');\n return {\n defaultOrgCreated,\n defaultOrgId,\n memberCreated: false,\n reason: 'member_insert_failed',\n };\n }\n logger?.info?.(\n `[org-scoping] bound platform admin to default organization (${defaultOrgId})`,\n { userId: adminUserId, defaultOrgId },\n );\n\n // 6. Hand the default org's seeded rows (owner_id NULL) to the admin so\n // owner-keyed UX works out of the box — the multi-tenant companion to the\n // single-tenant first-admin handoff. Best-effort; never undoes the bind.\n let ownershipClaimed = 0;\n if (defaultOrgId) {\n try {\n const claims = await claimOrgSeedOwnership(ql, defaultOrgId, adminUserId, { logger });\n ownershipClaimed = claims.reduce((s, c) => s + c.count, 0);\n } catch (e) {\n logger?.warn?.('[org-scoping] default-org seed ownership handoff failed', {\n error: (e as Error).message,\n });\n }\n }\n\n return { defaultOrgCreated, defaultOrgId, memberCreated: true, ownershipClaimed };\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * Canonical plugin-org-scoping manifest source.\n *\n * Imported by `objectstack.config.ts` (compile-time) and\n * `org-scoping-plugin.ts` (runtime `manifest.register`) so the two\n * registration paths cannot drift.\n */\n\nexport const ORG_SCOPING_PLUGIN_ID = 'com.objectstack.plugin-org-scoping';\nexport const ORG_SCOPING_PLUGIN_VERSION = '1.0.0';\n\n/** This plugin owns no `sys_*` objects — Organization itself lives in `@objectstack/platform-objects`. */\nexport const orgScopingObjects = [] as const;\n\n/** Manifest header shared by compile-time config and runtime registration. */\nexport const orgScopingPluginManifestHeader = {\n id: ORG_SCOPING_PLUGIN_ID,\n namespace: 'sys',\n version: ORG_SCOPING_PLUGIN_VERSION,\n type: 'plugin' as const,\n scope: 'system' as const,\n defaultDatasource: 'cloud',\n name: 'Organization Scoping Plugin',\n description:\n 'Row-level Organization isolation: auto-stamps `organization_id` on insert from ' +\n '`ExecutionContext.tenantId`, replays seed datasets per new org, and bootstraps a default ' +\n 'organization for the first platform admin.',\n};\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport { Plugin, PluginContext } from '@objectstack/core';\nimport { claimOrphanOrgRows } from './claim-orphan-org-rows.js';\nimport { cloneOrgSeedData } from './clone-org-seed-data.js';\nimport { ensureDefaultOrganization } from './ensure-default-organization.js';\nimport {\n orgScopingObjects,\n orgScopingPluginManifestHeader,\n} from './manifest.js';\n\nexport interface OrgScopingPluginOptions {\n /**\n * Whether to auto-create a `Default Organization` (slug `default`)\n * and bind the first platform admin as `owner` when they have zero\n * memberships. Set to `false` for deployments that fully self-manage\n * org provisioning via invitation links or a custom onboarding flow.\n *\n * @default true\n */\n ensureDefaultOrganization?: boolean;\n}\n\n/**\n * OrgScopingPlugin\n *\n * Makes `sys_organization` a first-class row-level isolation boundary:\n *\n * 1. **insert auto-stamp** — on every authenticated `insert` whose\n * target object declares `organization_id`, fill the column from\n * `ExecutionContext.tenantId`. Without this, freshly-created\n * rows have `organization_id = NULL` and the default\n * `tenant_isolation` RLS policy hides them from the very user\n * who just created them.\n *\n * 2. **per-org seed replay** — after `sys_organization` insert, copy\n * the artifact's demo seed data into the new org. Three paths\n * (in order of preference):\n * a. replay registered `seed-datasets` via the kernel-level\n * `seed-replayer` callable (set by AppPlugin),\n * b. for the FIRST org, `claimOrphanOrgRows` adopts any\n * NULL-org rows a previous inline-seed may have inserted,\n * c. for SUBSEQUENT orgs, `cloneOrgSeedData` shallow-clones\n * rows from the very first org (donor-pattern).\n *\n * 3. **default-org bootstrap** — on `kernel:ready` and after every\n * `sys_user_permission_set` insert, ensure the platform admin has\n * a Default Organization to operate in (idempotent on slug\n * `default` + admin's existing memberships).\n *\n * Why split from plugin-security:\n * - plugin-security is a single-tenant-aware RBAC + RLS engine; it\n * should not know about Organization-specific seed flows.\n * - This plugin is purely opt-in: not installing it gives a\n * single-tenant deployment (no `organization_id` injection, no\n * per-org seed clone, no default-org bootstrap). plugin-security\n * detects its presence via `getService('org-scoping')` and adjusts\n * RLS policy stripping accordingly.\n *\n * Naming note: \"org-scoping\" deliberately avoids the word \"tenant\"\n * because in ObjectStack \"tenant\" already means *physical isolation*\n * (one Environment = one database, per ADR-0002 and driver-turso's\n * multi-tenant router). This plugin is about LOGICAL row-level\n * scoping inside a single database — orthogonal to physical tenancy.\n *\n * Dependencies:\n * - `objectql` (engine middleware host)\n */\nexport class OrgScopingPlugin implements Plugin {\n name = 'com.objectstack.org-scoping';\n type = 'standard';\n version = '1.0.0';\n dependencies = ['com.objectstack.engine.objectql'];\n\n /** Per-object field-name cache; same shape as SecurityPlugin's. */\n private readonly fieldNamesCache = new Map<string, Set<string> | null>();\n\n private readonly opts: Required<OrgScopingPluginOptions>;\n\n constructor(options: OrgScopingPluginOptions = {}) {\n this.opts = {\n ensureDefaultOrganization: options.ensureDefaultOrganization !== false,\n };\n }\n\n async init(ctx: PluginContext): Promise<void> {\n ctx.logger.info('Initializing Org-Scoping Plugin...');\n // Service registration doubles as plugin-security's\n // \"multi-tenant mode is on\" probe: SecurityPlugin queries\n // `getService('org-scoping')` and keeps wildcard\n // `current_user.organization_id` RLS policies when this returns.\n ctx.registerService('org-scoping', this);\n\n ctx\n .getService<{ register(m: any): void }>('manifest')\n .register({\n ...orgScopingPluginManifestHeader,\n objects: orgScopingObjects,\n });\n ctx.logger.info('Org-Scoping Plugin initialized');\n }\n\n async start(ctx: PluginContext): Promise<void> {\n ctx.logger.info('Starting Org-Scoping Plugin...');\n\n let ql: any;\n let metadata: any;\n try {\n ql = ctx.getService('objectql');\n try {\n metadata = ctx.getService('metadata');\n } catch {\n metadata = undefined;\n }\n } catch {\n ctx.logger.warn(\n 'ObjectQL service not available, org-scoping middleware not registered',\n );\n return;\n }\n if (!ql || typeof ql.registerMiddleware !== 'function') {\n ctx.logger.warn(\n 'ObjectQL engine does not support middleware, org-scoping middleware not registered',\n );\n return;\n }\n\n // ── Middleware A: auto-stamp `organization_id` on insert ──────────\n ql.registerMiddleware(async (opCtx: any, next: () => Promise<void>) => {\n if (opCtx.context?.isSystem) return next();\n if (\n opCtx.operation === 'insert' &&\n opCtx.data &&\n typeof opCtx.data === 'object' &&\n !Array.isArray(opCtx.data) &&\n opCtx.context?.tenantId\n ) {\n const fields = await this.getObjectFieldNames(metadata, opCtx.object, ql);\n if (fields && fields.has('organization_id')) {\n const data = opCtx.data as Record<string, unknown>;\n if (data.organization_id == null || data.organization_id === '') {\n data.organization_id = opCtx.context.tenantId;\n }\n }\n }\n await next();\n });\n\n // ── Middleware B: per-org seed pipeline on sys_organization insert ─\n ql.registerMiddleware(async (opCtx: any, next: () => Promise<void>) => {\n await next();\n if (\n opCtx?.object !== 'sys_organization' ||\n (opCtx?.operation !== 'create' && opCtx?.operation !== 'insert')\n ) {\n return;\n }\n const newOrgId = opCtx?.result?.id ?? opCtx?.data?.id;\n if (!newOrgId) return;\n\n const kernel: any = (ctx as any).kernel ?? ctx;\n let datasets: any[] | undefined;\n try {\n const raw = kernel?.getService?.('seed-datasets');\n if (Array.isArray(raw) && raw.length > 0) datasets = raw;\n } catch {\n /* service not registered */\n }\n\n // Count existing orgs to pick the right fallback path.\n let orgCount = 0;\n try {\n const allOrgs = await ql.find(\n 'sys_organization',\n { limit: 2, fields: ['id'] },\n { context: { isSystem: true } },\n );\n const list: any[] = Array.isArray(allOrgs)\n ? allOrgs\n : Array.isArray(allOrgs?.records)\n ? allOrgs.records\n : [];\n orgCount = list.length;\n } catch (e) {\n ctx.logger.warn('[org-scoping] failed to count organizations', {\n error: (e as Error).message,\n });\n }\n\n // Primary path: SeedLoader replay scoped to newOrgId.\n let replayed = false;\n try {\n const replayer: any = kernel?.getService?.('seed-replayer');\n if (typeof replayer === 'function') {\n const summary = await replayer(newOrgId);\n const total = (summary?.inserted ?? 0) + (summary?.updated ?? 0);\n ctx.logger.info(\n `[org-scoping] per-org seed replay for ${newOrgId}: +${summary?.inserted ?? 0} inserted, ${summary?.updated ?? 0} updated, ${summary?.errors?.length ?? 0} error(s)`,\n {\n organizationId: newOrgId,\n errors: summary?.errors?.slice?.(0, 5),\n },\n );\n if (total > 0) replayed = true;\n } else if (datasets) {\n ctx.logger.warn(\n '[org-scoping] per-org seed: datasets present but no replayer registered',\n { organizationId: newOrgId },\n );\n }\n } catch (e) {\n ctx.logger.warn(\n '[org-scoping] per-org seed replay failed, falling back',\n { organizationId: newOrgId, error: (e as Error).message },\n );\n }\n if (replayed) return;\n\n // Fallback A: legacy claim for first org.\n if (orgCount === 1) {\n try {\n const claims = await claimOrphanOrgRows(ql, newOrgId, { logger: ctx.logger });\n if (claims.length > 0) {\n const total = claims.reduce((s, c) => s + c.count, 0);\n ctx.logger.info(\n `[org-scoping] claimed ${total} orphan seed row(s) for first organization ${newOrgId}`,\n { breakdown: claims },\n );\n return;\n }\n } catch (e) {\n ctx.logger.warn('[org-scoping] claim-orphan-org-rows failed', {\n error: (e as Error).message,\n });\n }\n }\n\n // Fallback B: clone from donor org for subsequent orgs.\n if (orgCount > 1) {\n try {\n const summary = await cloneOrgSeedData(ql, newOrgId, { logger: ctx.logger });\n if (summary.length > 0) {\n const total = summary.reduce((s, c) => s + c.count, 0);\n ctx.logger.info(\n `[org-scoping] cloned ${total} seed row(s) for new organization ${newOrgId}`,\n { breakdown: summary },\n );\n }\n } catch (e) {\n ctx.logger.warn('[org-scoping] clone-org-seed-data failed', {\n organizationId: newOrgId,\n error: (e as Error).message,\n });\n }\n }\n });\n\n // ── Default-org bootstrap on kernel:ready + on admin grant ────────\n if (this.opts.ensureDefaultOrganization) {\n const runEnsure = async () => {\n try {\n const res = await ensureDefaultOrganization(ql, { logger: ctx.logger });\n if (res.defaultOrgCreated) {\n ctx.logger.info(\n `[org-scoping] created Default Organization ${res.defaultOrgId} for platform admin`,\n );\n }\n } catch (e) {\n ctx.logger.warn?.('[org-scoping] ensureDefaultOrganization failed', {\n error: (e as Error).message,\n });\n }\n };\n if (typeof (ctx as any).hook === 'function') {\n (ctx as any).hook('kernel:ready', runEnsure);\n } else {\n void runEnsure();\n }\n // Re-run after every admin grant — handles the \"first sign-up\n // promoted to platform admin\" case where the kernel:ready hook\n // fired before any user existed.\n ql.registerMiddleware(async (opCtx: any, next: () => Promise<void>) => {\n await next();\n if (\n opCtx?.object === 'sys_user_permission_set' &&\n (opCtx?.operation === 'insert' || opCtx?.operation === 'create')\n ) {\n await runEnsure();\n }\n });\n }\n\n ctx.logger.info('Org-Scoping middleware registered on ObjectQL engine');\n }\n\n async destroy(): Promise<void> {\n // No cleanup needed\n }\n\n /**\n * Resolve the column-name set for an object (mirrors SecurityPlugin's\n * loader so the two plugins behave consistently). Returns `null` if\n * the schema can't be loaded — caller skips injection.\n */\n private async getObjectFieldNames(\n metadata: any,\n objectName: string,\n ql?: any,\n ): Promise<Set<string> | null> {\n if (this.fieldNamesCache.has(objectName)) {\n return this.fieldNamesCache.get(objectName) ?? null;\n }\n const result = await this.loadObjectFieldNames(metadata, objectName, ql);\n if (result) this.fieldNamesCache.set(objectName, result);\n return result;\n }\n\n private async loadObjectFieldNames(\n metadata: any,\n objectName: string,\n ql?: any,\n ): Promise<Set<string> | null> {\n try {\n let obj: any =\n typeof ql?.getSchema === 'function' ? ql.getSchema(objectName) : null;\n if (!obj || !obj.fields) {\n obj = await metadata?.get?.('object', objectName);\n }\n if (!obj || !obj.fields) return null;\n const set = new Set<string>(['id']);\n if (Array.isArray(obj.fields)) {\n for (const f of obj.fields) {\n if (f?.name) set.add(String(f.name));\n }\n } else if (typeof obj.fields === 'object') {\n for (const key of Object.keys(obj.fields)) {\n set.add(key);\n const v = (obj.fields as Record<string, any>)[key];\n if (v && typeof v === 'object' && v.name) set.add(String(v.name));\n }\n } else {\n return null;\n }\n return set;\n } catch {\n return null;\n }\n }\n}\n"],"mappings":"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","names":["SYSTEM_CTX","SYSTEM_CTX","SYSTEM_CTX"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@objectstack/plugin-org-scoping",
3
- "version": "9.9.1",
3
+ "version": "9.10.0",
4
4
  "license": "Apache-2.0",
5
5
  "description": "Organization-Scoping Plugin for ObjectStack — row-level Organization isolation, per-org seed replay, default-org bootstrap",
6
6
  "main": "dist/index.js",
@@ -13,9 +13,9 @@
13
13
  }
14
14
  },
15
15
  "dependencies": {
16
- "@objectstack/core": "9.9.1",
17
- "@objectstack/platform-objects": "9.9.1",
18
- "@objectstack/spec": "9.9.1"
16
+ "@objectstack/core": "9.10.0",
17
+ "@objectstack/platform-objects": "9.10.0",
18
+ "@objectstack/spec": "9.10.0"
19
19
  },
20
20
  "devDependencies": {
21
21
  "@types/node": "^25.9.3",