@objectstack/plugin-security 4.1.0 → 4.1.1
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 +12 -8
- package/dist/index.d.ts +12 -8
- package/dist/index.js +14 -3
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +14 -3
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -4
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/permission-evaluator.ts","../src/rls-compiler.ts","../src/field-masker.ts","../src/errors.ts","../src/bootstrap-platform-admin.ts","../src/claim-orphan-tenant-rows.ts","../src/clone-tenant-seed-data.ts","../src/ensure-user-has-organization.ts","../src/manifest.ts","../src/security-plugin.ts"],"sourcesContent":["// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * @objectstack/plugin-security\n * \n * Security Plugin for ObjectStack\n * Provides RBAC, Row-Level Security (RLS), and Field-Level Security runtime.\n */\n\nexport { SecurityPlugin } from './security-plugin.js';\nexport { PermissionEvaluator } from './permission-evaluator.js';\nexport { RLSCompiler, RLS_DENY_FILTER } from './rls-compiler.js';\nexport { FieldMasker } from './field-masker.js';\nexport { PermissionDeniedError, isPermissionDeniedError } from './errors.js';\nexport {\n securityObjects,\n securityDefaultPermissionSets,\n securityPluginManifestHeader,\n SECURITY_PLUGIN_ID,\n SECURITY_PLUGIN_VERSION,\n} from './manifest.js';\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { PermissionSet, ObjectPermission, FieldPermission } from '@objectstack/spec/security';\n\n/**\n * Operation type mapping to permission checks\n */\nconst OPERATION_TO_PERMISSION: Record<string, keyof ObjectPermission> = {\n find: 'allowRead',\n findOne: 'allowRead',\n count: 'allowRead',\n aggregate: 'allowRead',\n insert: 'allowCreate',\n update: 'allowEdit',\n delete: 'allowDelete',\n};\n\n/**\n * PermissionEvaluator\n * \n * Runtime evaluator for PermissionSet definitions.\n * Resolves aggregated permissions from roles to concrete allow/deny decisions.\n */\nexport class PermissionEvaluator {\n /**\n * Check if an operation is allowed on an object for the given permission sets.\n * Uses \"most permissive\" merging: if ANY permission set allows, it's allowed.\n */\n checkObjectPermission(\n operation: string,\n objectName: string,\n permissionSets: PermissionSet[]\n ): boolean {\n const permKey = OPERATION_TO_PERMISSION[operation];\n if (!permKey) return true; // Unknown operations are allowed by default\n\n for (const ps of permissionSets) {\n // Honour the `'*'` wildcard sentinel — admin permission sets typically\n // grant blanket access via a single `objects: { '*': … }` entry rather\n // than enumerating every system object.\n const objPerm = ps.objects?.[objectName] ?? ps.objects?.['*'];\n if (objPerm) {\n // Check if modifyAllRecords is set (super-user bypass for write ops)\n if (['allowEdit', 'allowDelete'].includes(permKey) && objPerm.modifyAllRecords) {\n return true;\n }\n // Check if viewAllRecords is set (super-user bypass for read ops)\n if (permKey === 'allowRead' && (objPerm.viewAllRecords || objPerm.modifyAllRecords)) {\n return true;\n }\n // Check the specific permission\n if (objPerm[permKey]) {\n return true;\n }\n }\n }\n\n return false;\n }\n\n /**\n * Get the merged field permissions for an object.\n * Returns a map of field names to their effective permissions.\n * Uses \"most permissive\" merging.\n */\n getFieldPermissions(\n objectName: string,\n permissionSets: PermissionSet[]\n ): Record<string, FieldPermission> {\n const result: Record<string, FieldPermission> = {};\n\n for (const ps of permissionSets) {\n if (!ps.fields) continue;\n\n for (const [key, perm] of Object.entries(ps.fields)) {\n // Field keys are in format: \"object_name.field_name\"\n if (!key.startsWith(`${objectName}.`)) continue;\n const fieldName = key.substring(objectName.length + 1);\n\n if (!result[fieldName]) {\n result[fieldName] = { readable: false, editable: false };\n }\n\n // Most permissive merge\n if (perm.readable) result[fieldName].readable = true;\n if (perm.editable) result[fieldName].editable = true;\n }\n }\n\n return result;\n }\n\n /**\n * Resolve permission sets for a list of identifier names from metadata.\n *\n * Identifiers are matched to `PermissionSet.name`. The names may be\n * either role names (when `sys_role.name` is reused as a permission set\n * name — common for default admin/member/viewer roles) or explicit\n * permission set names supplied through `ExecutionContext.permissions[]`\n * (resolved by `resolveExecutionContext` from `sys_user_permission_set`\n * and `sys_role_permission_set`).\n *\n * Async because the underlying metadata service exposes `list()` as a\n * Promise — synchronous iteration would silently yield zero results\n * (the historical SecurityPlugin behaviour, masking all enforcement).\n *\n * `bootstrapPermissionSets` is a fallback list of plugin-owned permission\n * sets (typically the platform defaults: admin_full_access /\n * member_default / viewer_readonly) that are registered via\n * `manifest.register({ permissions })` but do not currently propagate\n * into the metadata service's `list()` index. Without this fallback,\n * SecurityPlugin would never resolve the defaults and all enforcement\n * would be silently disabled for authenticated requests.\n */\n async resolvePermissionSets(\n identifiers: string[],\n metadataService: any,\n bootstrapPermissionSets: PermissionSet[] = [],\n /**\n * Optional async loader for permission set names that aren't found in\n * metadata or bootstrap. Lets callers query user-defined permission\n * sets persisted in `sys_permission_set`. Failures are swallowed.\n */\n dbLoader?: (unresolved: string[]) => Promise<PermissionSet[]>\n ): Promise<PermissionSet[]> {\n if (identifiers.length === 0) return [];\n\n const result: PermissionSet[] = [];\n const seen = new Set<string>();\n\n // Get all permission sets from metadata. Support both async (Manager) and\n // sync (test stub) implementations of `list`.\n let allPermSets: any = [];\n try {\n const listed = metadataService?.list?.('permission')\n ?? metadataService?.list?.('permissions')\n ?? [];\n allPermSets = typeof (listed as any)?.then === 'function' ? await listed : listed;\n } catch {\n allPermSets = [];\n }\n if (!Array.isArray(allPermSets)) allPermSets = [];\n\n const wanted = new Set(identifiers);\n for (const ps of allPermSets) {\n if (wanted.has(ps.name) && !seen.has(ps.name)) {\n seen.add(ps.name);\n result.push(ps);\n }\n }\n\n // Fallback: any wanted name not yet matched is sourced from the\n // bootstrap list (plugin-owned defaults). Avoids silent failure when\n // permission sets are registered via `manifest.register` but the\n // metadata service hasn't indexed them.\n for (const ps of bootstrapPermissionSets) {\n if (wanted.has(ps.name) && !seen.has(ps.name)) {\n seen.add(ps.name);\n result.push(ps);\n }\n }\n\n // Last-resort: query user-defined permission sets from the database.\n // Without this, custom permission sets (created via the admin UI as\n // `sys_permission_set` rows) would be silently ignored both for CRUD\n // enforcement and for field-level masking.\n if (dbLoader) {\n const unresolved = identifiers.filter((n) => !seen.has(n));\n if (unresolved.length > 0) {\n try {\n const dbRows = await dbLoader(unresolved);\n for (const ps of dbRows ?? []) {\n if (ps?.name && !seen.has(ps.name)) {\n seen.add(ps.name);\n result.push(ps);\n }\n }\n } catch {\n // Swallow — the request shouldn't fail just because the DB\n // lookup is unavailable.\n }\n }\n }\n\n return result;\n }\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { RowLevelSecurityPolicy } from '@objectstack/spec/security';\nimport type { ExecutionContext } from '@objectstack/spec/kernel';\n\n/**\n * RLS User Context\n * Variables available for RLS expression evaluation.\n */\ninterface RLSUserContext {\n id?: string;\n /**\n * Active organization id for the request. RLS expressions reference\n * this as `current_user.organization_id`. Sourced from\n * `ExecutionContext.tenantId` (the runtime keeps the abstract\n * \"tenant\" name, but at the data/RLS layer the canonical column is\n * `organization_id` — see better-auth's organization plugin).\n */\n organization_id?: string;\n roles?: string[];\n [key: string]: unknown;\n}\n\n/**\n * Sentinel filter used when applicable RLS policies exist but none can\n * be compiled against the current execution context (typically because a\n * required `current_user.*` variable is missing — e.g. the user has no\n * active organization). The filter compares `id` against a non-printable\n * UUID-shaped string that no real record will ever carry, so the upstream\n * SQL layer naturally returns zero rows without raising an error. This\n * gives us **fail-closed** semantics for select/update/delete on tables\n * that the user is not entitled to see, without forcing every caller to\n * handle a thrown `PermissionDeniedError` for what is conceptually an\n * empty result set.\n *\n * Exposed for the SecurityPlugin's optional short-circuit path and for\n * tests; see {@link RLSCompiler.compileFilter}.\n */\nexport const RLS_DENY_FILTER: Record<string, unknown> = Object.freeze({\n id: '__rls_deny__:00000000-0000-0000-0000-000000000000',\n});\n\n/**\n * RLSCompiler\n * \n * Compiles Row-Level Security policy expressions into query filters.\n * Converts `using` / `check` expressions into ObjectQL-compatible filter conditions.\n */\nexport class RLSCompiler {\n /**\n * Compile RLS policies into a query filter for the given user context.\n * Multiple policies for the same object/operation are OR-combined (any match allows access).\n *\n * Return-value semantics:\n * - `null` → no policies applicable → caller applies no RLS filter.\n * - non-null → caller AND's it onto the existing where clause.\n * - {@link RLS_DENY_FILTER} → policies were defined but none could be\n * compiled (e.g. wildcard `tenant_isolation` against a user with no\n * active organization). The caller must treat this as \"deny by\n * default\" — its `id` comparison naturally yields zero rows on\n * select/update/delete, which is the safe fail-closed answer.\n */\n compileFilter(\n policies: RowLevelSecurityPolicy[],\n executionContext?: ExecutionContext\n ): Record<string, unknown> | null {\n if (policies.length === 0) return null;\n\n const userCtx: RLSUserContext = {\n id: executionContext?.userId,\n organization_id: executionContext?.tenantId,\n roles: executionContext?.roles,\n };\n\n const filters: Record<string, unknown>[] = [];\n\n for (const policy of policies) {\n if (!policy.using) continue;\n const filter = this.compileExpression(policy.using, userCtx);\n if (filter) {\n filters.push(filter);\n }\n }\n\n if (filters.length === 0) {\n // Policies *were* applicable but every one of them depended on a\n // `current_user.*` variable that wasn't populated (or used an\n // expression we couldn't compile). Fail closed — return a sentinel\n // filter that matches no rows. This prevents the \"user without an\n // active org sees every tenant's data\" class of bug.\n return RLS_DENY_FILTER;\n }\n if (filters.length === 1) return filters[0];\n\n // Multiple policies: OR-combine (any policy allows access)\n return { $or: filters };\n }\n\n /**\n * Compile a single RLS expression into a query filter.\n * \n * Supports simple expressions like:\n * - \"field_name = current_user.property\"\n * - \"field_name IN (current_user.array_property)\"\n * - \"field_name = 'literal_value'\"\n */\n compileExpression(\n expression: string,\n userCtx: RLSUserContext\n ): Record<string, unknown> | null {\n if (!expression) return null;\n\n // Handle simple equality: \"field = current_user.property\"\n const eqMatch = expression.match(/^\\s*(\\w+)\\s*=\\s*current_user\\.(\\w+)\\s*$/);\n if (eqMatch) {\n const [, field, prop] = eqMatch;\n const value = userCtx[prop];\n // Skip when the user-context value is missing (undefined or null).\n // A `null` `organization_id` means \"no active organization\" — applying\n // the filter as `organization_id IS NULL` would silently expose every\n // un-tenanted row across tenants and break system tables that lack the\n // column entirely. Treating null as \"skip this policy\" makes the\n // tenant_isolation rule safely opt-out for users without an active org\n // while still applying when one is set.\n if (value === undefined || value === null) return null;\n return { [field]: value };\n }\n\n // Handle literal equality: \"field = 'value'\"\n const litMatch = expression.match(/^\\s*(\\w+)\\s*=\\s*'([^']*)'\\s*$/);\n if (litMatch) {\n const [, field, value] = litMatch;\n return { [field]: value };\n }\n\n // Handle IN: \"field IN (current_user.array_property)\"\n const inMatch = expression.match(/^\\s*(\\w+)\\s+IN\\s+\\(\\s*current_user\\.(\\w+)\\s*\\)\\s*$/i);\n if (inMatch) {\n const [, field, prop] = inMatch;\n const value = userCtx[prop];\n if (!Array.isArray(value) || value.length === 0) return null;\n return { [field]: { $in: value } };\n }\n\n // Unsupported expression: return null (no additional RLS filter applied).\n // Note: callers should treat absence of RLS policies as \"allow all\" only when\n // no policies are defined. If policies exist but cannot be compiled, the caller\n // may want to deny access as a safety measure.\n return null;\n }\n\n /**\n * Get applicable RLS policies for a given object and operation.\n */\n getApplicablePolicies(\n objectName: string,\n operation: string,\n allPolicies: RowLevelSecurityPolicy[]\n ): RowLevelSecurityPolicy[] {\n // Map engine operation to RLS operation type\n const rlsOp = this.mapOperationToRLS(operation);\n\n return allPolicies.filter(policy => {\n // Check object match\n if (policy.object !== objectName && policy.object !== '*') return false;\n\n // Check operation match\n if (policy.operation === 'all') return true;\n if (policy.operation === rlsOp) return true;\n\n return false;\n });\n }\n\n private mapOperationToRLS(operation: string): string {\n switch (operation) {\n case 'find':\n case 'findOne':\n case 'count':\n case 'aggregate':\n return 'select';\n case 'insert':\n return 'insert';\n case 'update':\n return 'update';\n case 'delete':\n return 'delete';\n default:\n return 'select';\n }\n }\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { FieldPermission } from '@objectstack/spec/security';\n\n/**\n * FieldMasker\n * \n * Applies field-level security by stripping restricted fields from query results.\n */\nexport class FieldMasker {\n /**\n * Mask fields in query results based on field permissions.\n * Removes fields that the user does not have read access to.\n */\n maskResults(\n results: any | any[],\n fieldPermissions: Record<string, FieldPermission>,\n _objectName: string\n ): any | any[] {\n // If no field permissions defined, return results as-is\n if (Object.keys(fieldPermissions).length === 0) return results;\n\n // Get list of non-readable fields\n const hiddenFields = Object.entries(fieldPermissions)\n .filter(([, perm]) => !perm.readable)\n .map(([field]) => field);\n\n if (hiddenFields.length === 0) return results;\n\n if (Array.isArray(results)) {\n return results.map(record => this.maskRecord(record, hiddenFields));\n }\n\n return this.maskRecord(results, hiddenFields);\n }\n\n /**\n * Get non-editable fields for use in write operations.\n * Returns a list of field names that should be stripped from incoming data.\n */\n getNonEditableFields(\n fieldPermissions: Record<string, FieldPermission>\n ): string[] {\n return Object.entries(fieldPermissions)\n .filter(([, perm]) => !perm.editable)\n .map(([field]) => field);\n }\n\n /**\n * Strip non-editable fields from write data.\n */\n stripNonEditableFields(\n data: Record<string, any>,\n fieldPermissions: Record<string, FieldPermission>\n ): Record<string, any> {\n const nonEditable = this.getNonEditableFields(fieldPermissions);\n if (nonEditable.length === 0) return data;\n\n const result = { ...data };\n for (const field of nonEditable) {\n delete result[field];\n }\n return result;\n }\n\n /**\n * Detect which fields in the caller's write payload would touch a\n * field they are not allowed to edit. Returns the set of offending\n * field names (no duplicates, sorted for stable error messages).\n *\n * Used by the security middleware on insert/update to fail-closed\n * with an explicit 403 rather than silently dropping fields — a\n * silent drop hides the security boundary from honest clients\n * (their update partially \"doesn't save\") and gives an attacker no\n * negative signal that the field exists. Throwing makes the\n * boundary observable in both directions.\n *\n * `data` may be a single record or an array of records (bulk insert);\n * either way the returned list is the union across all rows.\n *\n * Fields without a permission entry pass through — permission sets\n * are an allow-list at the field level only for fields they\n * explicitly enumerate. Most objects do not declare per-field rules\n * and remain fully editable.\n */\n detectForbiddenWrites(\n data: Record<string, any> | Record<string, any>[],\n fieldPermissions: Record<string, FieldPermission>\n ): string[] {\n if (Object.keys(fieldPermissions).length === 0) return [];\n const nonEditable = new Set(this.getNonEditableFields(fieldPermissions));\n if (nonEditable.size === 0) return [];\n\n const offenders = new Set<string>();\n const rows = Array.isArray(data) ? data : [data];\n for (const row of rows) {\n if (!row || typeof row !== 'object') continue;\n for (const field of Object.keys(row)) {\n if (nonEditable.has(field)) offenders.add(field);\n }\n }\n return Array.from(offenders).sort();\n }\n\n private maskRecord(record: any, hiddenFields: string[]): any {\n if (!record || typeof record !== 'object') return record;\n\n const result = { ...record };\n for (const field of hiddenFields) {\n delete result[field];\n }\n return result;\n }\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * Typed sentinel error thrown by `SecurityPlugin` when an operation is\n * denied. Caught by `@objectstack/runtime`'s HTTP dispatcher and translated\n * to HTTP 403.\n */\nexport class PermissionDeniedError extends Error {\n readonly code = 'PERMISSION_DENIED';\n readonly statusCode = 403;\n readonly details?: Record<string, unknown>;\n constructor(message: string, details?: Record<string, unknown>) {\n super(message);\n this.name = 'PermissionDeniedError';\n this.details = details;\n }\n}\n\nexport function isPermissionDeniedError(e: unknown): e is PermissionDeniedError {\n if (!e || typeof e !== 'object') return false;\n const anyE = e as any;\n return (\n anyE.name === 'PermissionDeniedError' ||\n anyE.code === 'PERMISSION_DENIED' ||\n (typeof anyE.message === 'string' && anyE.message.startsWith('[Security] Access denied'))\n );\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * bootstrapPlatformAdmin — first-boot platform admin promotion.\n *\n * Two responsibilities, both idempotent and run on `kernel:ready`:\n *\n * 1. **Seed `sys_permission_set` rows** for each `defaultPermissionSets`\n * entry (admin_full_access / member_default / viewer_readonly). The\n * dashboard's CRUD on `sys_permission_set` needs persisted rows to\n * exist so admins can grant them to users by id; the in-memory\n * bootstrap list alone is invisible to the standard CRUD UI.\n *\n * 2. **Promote the first registered user to platform admin** by\n * inserting a `sys_user_permission_set` row that points at\n * `admin_full_access` with `organization_id = NULL` (= cross-tenant).\n * If a platform admin already exists, this is a no-op forever.\n *\n * Zero configuration: `pnpm dev:crm` → sign up → \"I'm admin\".\n *\n * The DB column shape (`object_permissions` JSON text) does not match\n * the spec shape (`objects` record). For now we only need stable rows\n * with the right `name` so `resolveExecutionContext` can translate the\n * link-table id back to the bootstrap permission set name; the actual\n * `objects`/`rowLevelSecurity` definitions are still served from the\n * in-memory `bootstrapPermissionSets` list inside SecurityPlugin.\n */\n\nimport type { PermissionSet } from '@objectstack/spec/security';\n\ninterface BootstrapOptions {\n /** Logger from PluginContext. */\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 : [];\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\n/**\n * Persist seed permission sets and promote the first registered user to\n * platform admin. Safe to call multiple times.\n */\nexport async function bootstrapPlatformAdmin(\n ql: any,\n bootstrapPermissionSets: PermissionSet[],\n options: BootstrapOptions = {},\n): Promise<{ seeded: number; adminPromoted: boolean; reason?: string }> {\n const logger = options.logger;\n if (!ql || typeof ql.find !== 'function' || typeof ql.insert !== 'function') {\n return { seeded: 0, adminPromoted: false, reason: 'objectql_unavailable' };\n }\n\n // 1. Seed permission set rows (one row per name, idempotent).\n const seeded: Record<string, string> = {}; // name -> id\n for (const ps of bootstrapPermissionSets) {\n if (!ps.name) continue;\n const existing = await tryFind(ql, 'sys_permission_set', { name: ps.name }, 1);\n if (existing.length > 0 && existing[0].id) {\n seeded[ps.name] = existing[0].id;\n continue;\n }\n const id = genId('ps');\n const created = await tryInsert(ql, 'sys_permission_set', {\n id,\n name: ps.name,\n label: ps.label ?? ps.name,\n description: ps.description ?? null,\n object_permissions: JSON.stringify(ps.objects ?? {}),\n field_permissions: JSON.stringify(ps.fields ?? {}),\n active: true,\n });\n if (created?.id) seeded[ps.name] = created.id;\n else if (created) seeded[ps.name] = id;\n }\n\n const seededCount = Object.keys(seeded).length;\n\n // 2. First-user platform admin promotion.\n const adminPsId = seeded['admin_full_access'];\n if (!adminPsId) {\n return { seeded: seededCount, adminPromoted: false, reason: 'admin_permission_set_missing' };\n }\n\n // If a platform admin already exists, we're done.\n const existingAdminLinks = await tryFind(\n ql,\n 'sys_user_permission_set',\n { permission_set_id: adminPsId },\n 5,\n );\n if (existingAdminLinks.some((r) => !r.organization_id)) {\n return { seeded: seededCount, adminPromoted: false, reason: 'already_have_admin' };\n }\n\n // Promote the oldest user (= first registrant). If no users yet, the\n // sys_user post-create middleware will rerun this on first sign-up.\n const allUsers = await tryFind(ql, 'sys_user', {}, 50);\n if (allUsers.length === 0) {\n logger?.info?.('[security] no users yet — first sign-up will be promoted to platform admin');\n return { seeded: seededCount, adminPromoted: false, reason: 'no_users' };\n }\n const sorted = [...allUsers].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 target = sorted[0];\n\n const inserted = await tryInsert(ql, 'sys_user_permission_set', {\n id: genId('ups'),\n user_id: target.id,\n permission_set_id: adminPsId,\n organization_id: null,\n granted_by: null,\n });\n if (!inserted) {\n logger?.warn?.(`[security] failed to grant admin_full_access to first user ${target.email ?? target.id}`);\n return { seeded: seededCount, adminPromoted: false, reason: 'insert_failed' };\n }\n logger?.info?.(`[security] first user promoted to platform admin: ${target.email ?? target.id}`);\n return { seeded: seededCount, adminPromoted: true };\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * claimOrphanTenantRows — assign seed-loaded records to the first organization.\n *\n * Seeds (`defineDataset`) 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 claimOrphanTenantRows(\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?.('[security] claimOrphanTenantRows: 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?.(`[security] 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?.(`[security] 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?.(`[security] 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 * cloneTenantSeedData — 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 `claimOrphanTenantRows` hook\n * (see `claim-orphan-tenant-rows.ts`) only fires for the very first\n * org — every subsequent user that auto-creates a personal Workspace\n * via `ensureUserHasOrganization` ends up looking at an empty\n * dashboard. 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 cloneTenantSeedData(\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?.('[security] cloneTenantSeedData: 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?.('[security] cloneTenantSeedData: 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?.('[security] cloneTenantSeedData: 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?.('[security] cloneTenantSeedData: 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 * ensureUserHasOrganization — auto-create a personal org for new users.\n *\n * In multi-tenant mode, every record visible through the default\n * `tenant_isolation` RLS policy must have an `organization_id`, and\n * every authenticated user must have an `activeOrganizationId` on their\n * session for that policy to evaluate to anything other than \"deny\n * all\". A user with zero `sys_member` rows, however, can sign in\n * successfully and reach the dashboard — the dashboard's\n * `RequireOrganization` guard has a single-tenant carve-out that lets\n * users with empty organization lists through, so they land on a UI\n * that simply hides every record. The standard remedy (\"invite users\n * via an admin\") doesn't apply to self-service signup.\n *\n * This helper, run right after a `sys_user` insert, ensures the new\n * user has at least one organization by creating a personal workspace\n * (named \"<User>'s Workspace\", slug `<username>-workspace`) and an\n * owner-role `sys_member` row. The user's session will pick this up as\n * their `activeOrganizationId` on the next sign-in / org-list refresh\n * (better-auth's `setActiveOrganization` runs lazily when the picker\n * sees exactly one membership).\n *\n * Idempotent: bails out if the user already has any `sys_member` row.\n * Slug collisions retry with a numeric suffix; a cap of 5 attempts\n * means a pathological username will fail loudly rather than loop.\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 * Optional hook called after a personal org is successfully created.\n * Used by SecurityPlugin to wire in `cloneTenantSeedData` so each\n * new workspace gets its own copy of demo data. Pulled in via DI\n * to keep this helper free of a hard import on the cloner (which\n * keeps the tenant-claim and ensure-org test surfaces narrow).\n */\n cloneSeedData?: (\n ql: any,\n targetOrgId: string,\n opts: { logger?: EnsureOptions['logger'] },\n ) => Promise<{ object: string; count: number }[]>;\n}\n\nconst SYSTEM_CTX = { isSystem: true };\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\nfunction slugify(input: string): string {\n return input\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-+|-+$/g, '')\n .slice(0, 40) || 'workspace';\n}\n\nfunction deriveBaseName(user: { name?: string; email?: string; id: string }): string {\n if (user.name && user.name.trim()) return user.name.trim();\n if (user.email) {\n const local = user.email.split('@')[0];\n if (local) return local;\n }\n return user.id;\n}\n\nasync function tryFind(ql: any, object: string, where: any, limit = 1): Promise<any[]> {\n try {\n const rows = await ql.find(object, { where, limit }, { context: SYSTEM_CTX });\n return Array.isArray(rows) ? rows : [];\n } catch {\n return [];\n }\n}\n\n/**\n * Ensure `user` has at least one `sys_member` row. Creates a personal\n * organization owned by them if not.\n *\n * Returns `{ created: true, organizationId }` when a new org was made,\n * or `{ created: false, reason }` when the user already has memberships\n * or the operation was skipped.\n */\nexport async function ensureUserHasOrganization(\n ql: any,\n user: { id: string; name?: string; email?: string },\n options: EnsureOptions = {},\n): Promise<{ created: boolean; organizationId?: string; reason?: string }> {\n const logger = options.logger;\n const cloneSeedData = options.cloneSeedData;\n if (!ql || typeof ql.find !== 'function' || typeof ql.insert !== 'function') {\n return { created: false, reason: 'objectql_unavailable' };\n }\n if (!user?.id) return { created: false, reason: 'invalid_user' };\n\n // Idempotency gate: any existing membership means we're done.\n const existing = await tryFind(ql, 'sys_member', { user_id: user.id }, 1);\n if (existing.length > 0) {\n return { created: false, reason: 'already_member' };\n }\n\n const base = deriveBaseName(user);\n const orgName = `${base}'s Workspace`;\n const baseSlug = slugify(base);\n\n // Find a free slug. better-auth allows duplicates technically, but\n // the dashboard renders the slug as a stable identifier so we keep\n // them unique-per-platform for human-readable URLs.\n let slug = `${baseSlug}-workspace`;\n for (let attempt = 1; attempt <= 5; attempt += 1) {\n const collision = await tryFind(ql, 'sys_organization', { slug }, 1);\n if (collision.length === 0) break;\n slug = `${baseSlug}-workspace-${attempt + 1}`;\n if (attempt === 5) {\n logger?.warn?.(\n `[security] could not find a free slug for personal org of ${user.email ?? user.id}`,\n );\n return { created: false, reason: 'slug_exhausted' };\n }\n }\n\n const orgId = genId('org');\n let orgRow: any = null;\n try {\n orgRow = await ql.insert(\n 'sys_organization',\n { id: orgId, name: orgName, slug, logo: null, metadata: null },\n { context: SYSTEM_CTX },\n );\n } catch (e) {\n logger?.warn?.(`[security] failed to create personal org for ${user.email ?? user.id}`, {\n error: (e as Error).message,\n });\n return { created: false, reason: 'org_insert_failed' };\n }\n\n const finalOrgId = orgRow?.id ?? orgId;\n\n try {\n await ql.insert(\n 'sys_member',\n {\n id: genId('mem'),\n organization_id: finalOrgId,\n user_id: user.id,\n role: 'owner',\n },\n { context: SYSTEM_CTX },\n );\n } catch (e) {\n logger?.warn?.(`[security] failed to create owner-member row for ${user.email ?? user.id}`, {\n error: (e as Error).message,\n });\n return { created: false, reason: 'member_insert_failed', organizationId: finalOrgId };\n }\n\n logger?.info?.(\n `[security] created personal organization \"${orgName}\" (${finalOrgId}) for ${user.email ?? user.id}`,\n );\n\n // Best-effort: clone the platform-first org's user-defined data into\n // the new personal workspace so demo apps (CRM, etc.) stay populated\n // for every signup. No-op when this IS the first org, when the donor\n // has no data, or when this op fails — never blocks signup.\n if (cloneSeedData) {\n try {\n const summary = await cloneSeedData(ql, finalOrgId, { logger });\n if (summary.length > 0) {\n const total = summary.reduce((s, c) => s + c.count, 0);\n logger?.info?.(\n `[security] cloned ${total} seed row(s) into personal organization ${finalOrgId}`,\n { breakdown: summary },\n );\n }\n } catch (e) {\n logger?.warn?.('[security] cloneTenantSeedData failed', {\n error: (e as Error).message,\n });\n }\n }\n\n return { created: true, organizationId: finalOrgId };\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * Canonical plugin-security manifest source.\n *\n * Both `objectstack.config.ts` (compile-time) and `security-plugin.ts`\n * (runtime `manifest.register`) import from this file so the two\n * registration paths cannot drift (D7).\n */\n\nimport {\n SysPermissionSet,\n SysRole,\n SysUserPermissionSet,\n SysRolePermissionSet,\n defaultPermissionSets,\n} from '@objectstack/platform-objects/security';\n\nexport const SECURITY_PLUGIN_ID = 'com.objectstack.plugin-security';\nexport const SECURITY_PLUGIN_VERSION = '1.0.0';\n\n/** Security objects owned by plugin-security. */\nexport const securityObjects = [\n SysRole,\n SysPermissionSet,\n SysUserPermissionSet,\n SysRolePermissionSet,\n];\n\n/** Default platform permission sets (admin / member / viewer). */\nexport const securityDefaultPermissionSets = defaultPermissionSets;\n\n/** Manifest header shared by compile-time config and runtime registration. */\nexport const securityPluginManifestHeader = {\n id: SECURITY_PLUGIN_ID,\n namespace: 'sys',\n version: SECURITY_PLUGIN_VERSION,\n type: 'plugin' as const,\n scope: 'system' as const,\n defaultDatasource: 'cloud',\n name: 'Security Plugin',\n description: 'RBAC roles and permission sets for ObjectStack (Role, PermissionSet)',\n};\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport { Plugin, PluginContext } from '@objectstack/core';\nimport type { PermissionSet, RowLevelSecurityPolicy } from '@objectstack/spec/security';\nimport { PermissionEvaluator } from './permission-evaluator.js';\nimport { RLSCompiler, RLS_DENY_FILTER } from './rls-compiler.js';\nimport { FieldMasker } from './field-masker.js';\nimport { PermissionDeniedError } from './errors.js';\nimport { bootstrapPlatformAdmin } from './bootstrap-platform-admin.js';\nimport { claimOrphanTenantRows } from './claim-orphan-tenant-rows.js';\nimport { cloneTenantSeedData } from './clone-tenant-seed-data.js';\nimport { ensureUserHasOrganization } from './ensure-user-has-organization.js';\nimport {\n securityObjects,\n securityDefaultPermissionSets,\n securityPluginManifestHeader,\n} from './manifest.js';\n\nexport interface SecurityPluginOptions {\n /**\n * Additional permission sets to register with the metadata service on\n * plugin start. Defaults to {@link securityDefaultPermissionSets}\n * (admin_full_access / member_default / viewer_readonly).\n */\n defaultPermissionSets?: PermissionSet[];\n /**\n * Permission set name applied as an implicit baseline whenever an\n * authenticated request has no resolved permission sets (and no roles\n * that map to one). This guarantees baseline tenant/owner RLS for\n * every logged-in user even before an admin assigns explicit\n * profiles. Set to `null` to disable.\n *\n * @default 'member_default'\n */\n fallbackPermissionSet?: string | null;\n /**\n * Whether this deployment is multi-tenant.\n *\n * When `true` (default), SecurityPlugin:\n * - Auto-injects `organization_id = ctx.tenantId` on insert when\n * the target object declares an `organization_id` field.\n * - Honours the wildcard `tenant_isolation` RLS policy\n * (`organization_id = current_user.organization_id`) shipped with\n * the default `member_default` / `viewer_readonly` permission\n * sets.\n *\n * When `false`, SecurityPlugin:\n * - Skips the `organization_id` auto-injection block (saves a\n * metadata lookup per insert; `owner_id` injection still runs).\n * - Strips any RLS policy whose USING expression references\n * `current_user.organization_id` from the per-request policy\n * set, so single-tenant deployments don't pay the\n * field-existence safety-net cost on every find.\n *\n * Field-Level Security, owner-based RLS, and per-object permission\n * checks (allowRead/allowCreate/…) all operate identically regardless\n * of this flag. Set this to `false` for single-tenant or\n * single-organization deployments where `organization_id` carries no\n * meaning.\n *\n * @default true\n */\n multiTenant?: boolean;\n}\n\n/**\n * SecurityPlugin\n *\n * Provides RBAC, Row-Level Security, and Field-Level Security runtime.\n * Registers as an engine middleware on the ObjectQL engine.\n *\n * This plugin is fully optional — without it, the system operates\n * without permission checks (same as current behavior).\n *\n * Dependencies:\n * - objectql service (ObjectQL engine with middleware support)\n * - metadata service (MetadataFacade for reading permission sets and RLS policies)\n */\nexport class SecurityPlugin implements Plugin {\n name = 'com.objectstack.security';\n type = 'standard';\n version = '1.0.0';\n dependencies = ['com.objectstack.engine.objectql'];\n\n private permissionEvaluator = new PermissionEvaluator();\n private rlsCompiler = new RLSCompiler();\n private fieldMasker = new FieldMasker();\n private readonly bootstrapPermissionSets: PermissionSet[];\n private readonly fallbackPermissionSet: string | null;\n private readonly multiTenant: boolean;\n /**\n * Per-object field-name cache. Populated lazily from the metadata\n * service / ObjectQL registry on first access per object. Schemas are\n * effectively immutable for the lifetime of the kernel today (hot\n * reload tears the kernel down), so we don't bother with\n * invalidation — a kernel restart drops the cache.\n */\n private readonly fieldNamesCache = new Map<string, Set<string> | null>();\n /**\n * Per-object cache of tenancy opt-out. `true` means the schema\n * explicitly disabled multi-tenancy (`tenancy.enabled === false` or\n * `systemFields.tenant === false`). Wildcard policies that target\n * the conventional tenant column (`organization_id`) are treated as\n * *not applicable* on these tables instead of triggering the\n * field-missing deny sentinel — without this, every read of a\n * cross-org catalog (e.g. `sys_package`, the Marketplace) returns\n * zero rows.\n */\n private readonly tenancyDisabledCache = new Map<string, boolean>();\n\n constructor(options: SecurityPluginOptions = {}) {\n this.bootstrapPermissionSets =\n options.defaultPermissionSets ?? securityDefaultPermissionSets;\n this.fallbackPermissionSet =\n options.fallbackPermissionSet === undefined\n ? 'member_default'\n : options.fallbackPermissionSet;\n this.multiTenant = options.multiTenant !== false;\n }\n\n async init(ctx: PluginContext): Promise<void> {\n ctx.logger.info('Initializing Security Plugin...');\n\n // Register security services\n ctx.registerService('security.permissions', this.permissionEvaluator);\n ctx.registerService('security.rls', this.rlsCompiler);\n ctx.registerService('security.fieldMasker', this.fieldMasker);\n // Bootstrap permission sets (admin_full_access, member_default,\n // viewer_readonly by default) — exposed as a service so other\n // plugins (e.g. plugin-hono-server's /me/permissions endpoint)\n // can pass them as the fallback list to\n // `PermissionEvaluator.resolvePermissionSets` without re-importing\n // the platform-objects package directly.\n ctx.registerService('security.bootstrapPermissionSets', this.bootstrapPermissionSets);\n ctx.registerService('security.fallbackPermissionSet', this.fallbackPermissionSet);\n\n ctx.getService<{ register(m: any): void }>('manifest').register({\n ...securityPluginManifestHeader,\n objects: securityObjects,\n // Permission sets ride along on the manifest so the metadata service\n // can resolve them by name when SecurityPlugin middleware queries\n // `metadata.list('permissions')`.\n permissions: this.bootstrapPermissionSets,\n });\n\n ctx.logger.info('Security Plugin initialized', {\n defaultPermissionSets: this.bootstrapPermissionSets.map((p) => p.name),\n });\n }\n\n async start(ctx: PluginContext): Promise<void> {\n ctx.logger.info('Starting Security Plugin...');\n\n // Get required services\n let ql: any;\n let metadata: any;\n\n try {\n ql = ctx.getService('objectql');\n metadata = ctx.getService('metadata');\n } catch (e) {\n ctx.logger.warn('ObjectQL or metadata service not available, security middleware not registered');\n return;\n }\n\n if (!ql || typeof ql.registerMiddleware !== 'function') {\n ctx.logger.warn('ObjectQL engine does not support middleware, security middleware not registered');\n return;\n }\n\n // Construct a dbLoader once that lets resolvePermissionSets\n // surface user-defined permission sets from `sys_permission_set`\n // (created via the admin UI) in addition to plugin-registered\n // ones. Uses `isSystem` to bypass tenant RLS.\n const dbLoader = ql\n ? async (names: string[]) => {\n let rows: any;\n try {\n rows = await ql.find(\n 'sys_permission_set',\n { where: { name: { $in: names } }, limit: names.length },\n { context: { isSystem: true } },\n );\n } catch {\n rows = [];\n }\n const list = Array.isArray(rows) ? rows : rows?.records ?? [];\n return list.map((r: any) => ({\n name: r.name,\n label: r.label,\n objects: typeof r.object_permissions === 'string'\n ? JSON.parse(r.object_permissions || '{}')\n : r.object_permissions ?? {},\n fields: typeof r.field_permissions === 'string'\n ? JSON.parse(r.field_permissions || '{}')\n : r.field_permissions ?? {},\n }));\n }\n : undefined;\n\n // Register security middleware\n ql.registerMiddleware(async (opCtx: any, next: () => Promise<void>) => {\n // System operations bypass security\n if (opCtx.context?.isSystem) {\n return next();\n }\n\n const roles = opCtx.context?.roles ?? [];\n const explicitPermissionSets = opCtx.context?.permissions ?? [];\n\n // Skip security checks if no roles AND no explicit permission sets\n // AND no userId (anonymous/unauthenticated). The auth middleware\n // should handle authentication separately.\n if (\n roles.length === 0 &&\n explicitPermissionSets.length === 0 &&\n !opCtx.context?.userId\n ) {\n return next();\n }\n\n // 1. Resolve permission sets from BOTH role names and explicit\n // permission set names attached to the execution context.\n let permissionSets: PermissionSet[] = [];\n try {\n const requested = [...roles, ...explicitPermissionSets];\n // Implicit baseline: when an authenticated request resolved zero\n // permission sets, fall back to the configured baseline (default\n // `member_default`). This guarantees tenant + owner RLS even\n // before an admin has assigned a profile/permission set.\n if (\n requested.length === 0 &&\n opCtx.context?.userId &&\n this.fallbackPermissionSet\n ) {\n requested.push(this.fallbackPermissionSet);\n }\n permissionSets = await this.permissionEvaluator.resolvePermissionSets(\n requested,\n metadata,\n this.bootstrapPermissionSets,\n dbLoader,\n );\n // **Post-resolution fallback** — closes the fail-open hole that\n // appears when a user's `roles` array is populated (e.g. a\n // better-auth `sys_member.role` like `owner`/`admin`/`member`)\n // but no `sys_role`→`sys_permission_set` binding exists yet, so\n // resolution returns an empty array. Without this, both the\n // CRUD check (`permissionSets.length > 0`) and the RLS injection\n // (`allRlsPolicies.length > 0`) below get skipped → the user\n // sees every tenant's data. Authenticated users with no\n // resolved permission sets always inherit the configured\n // baseline (default `member_default`, which carries\n // `tenant_isolation` + `owner_only_writes`).\n if (\n permissionSets.length === 0 &&\n opCtx.context?.userId &&\n this.fallbackPermissionSet\n ) {\n const fallback = await this.permissionEvaluator.resolvePermissionSets(\n [this.fallbackPermissionSet],\n metadata,\n this.bootstrapPermissionSets,\n dbLoader,\n );\n permissionSets = fallback;\n }\n } catch (e) {\n // If metadata service is misconfigured, log and continue without permission checks\n // rather than blocking all operations\n return next();\n }\n\n // 2. CRUD permission check\n if (permissionSets.length > 0) {\n const allowed = this.permissionEvaluator.checkObjectPermission(\n opCtx.operation,\n opCtx.object,\n permissionSets\n );\n\n if (!allowed) {\n throw new PermissionDeniedError(\n `[Security] Access denied: operation '${opCtx.operation}' on object '${opCtx.object}' ` +\n `is not permitted for roles [${roles.join(', ')}]`,\n { operation: opCtx.operation, object: opCtx.object, roles, permissionSets: explicitPermissionSets },\n );\n }\n }\n\n // 2.5. Field-Level Security write enforcement.\n //\n // The client-side masker (ObjectForm / inline grid) already hides\n // non-editable fields from the UI, but that is a UX layer only —\n // a hand-crafted POST / direct ObjectQL call can still target a\n // forbidden field. We fail-closed here with an explicit 403 and\n // the offending field names, so:\n //\n // - honest clients get an actionable error (vs. silent drop,\n // which manifests as a confusing partial-save), and\n // - probing clients see that the boundary is enforced (vs.\n // getting a 200 with the field silently ignored, which\n // reveals nothing).\n //\n // Runs BEFORE the tenant/owner auto-injection (step 3.5) so the\n // system-set fields are not subject to the user's edit\n // permissions — they are populated from the execution context,\n // not from the caller's payload.\n if (\n (opCtx.operation === 'insert' || opCtx.operation === 'update') &&\n opCtx.data &&\n permissionSets.length > 0\n ) {\n const fieldPerms = this.permissionEvaluator.getFieldPermissions(\n opCtx.object,\n permissionSets,\n );\n if (Object.keys(fieldPerms).length > 0) {\n const forbidden = this.fieldMasker.detectForbiddenWrites(\n opCtx.data,\n fieldPerms,\n );\n if (forbidden.length > 0) {\n throw new PermissionDeniedError(\n `[Security] Field write denied: not permitted to edit ` +\n `[${forbidden.join(', ')}] on '${opCtx.object}'`,\n {\n operation: opCtx.operation,\n object: opCtx.object,\n roles,\n permissionSets: explicitPermissionSets,\n forbiddenFields: forbidden,\n },\n );\n }\n }\n }\n\n // 3.5. Auto-inject tenancy/ownership fields on insert.\n //\n // When an authenticated user inserts a record, the canonical\n // tenant column (`organization_id`) and ownership column\n // (`owner_id`) should be auto-populated from\n // `ExecutionContext.tenantId` / `userId` so the row is visible\n // to the same RLS policies that gate reads. Without this, the\n // user creates a row that has `organization_id = NULL`, which\n // the very next `find` will filter out as a wrong-tenant row —\n // a confusing \"I just created it but I can't see it\" footgun.\n //\n // Only fills fields that:\n // - the object actually declares (so unrelated tables are\n // untouched)\n // - aren't already set in the payload (caller wins)\n // - have a corresponding value on the execution context.\n //\n // The `organization_id` half is gated on `multiTenant`; in\n // single-tenant deployments it's pure overhead.\n if (\n opCtx.operation === 'insert' &&\n opCtx.data &&\n typeof opCtx.data === 'object' &&\n !Array.isArray(opCtx.data)\n ) {\n const needsTenant =\n this.multiTenant && !!opCtx.context?.tenantId;\n const needsOwner = !!opCtx.context?.userId;\n if (needsTenant || needsOwner) {\n const fields = await this.getObjectFieldNames(metadata, opCtx.object, ql);\n if (fields) {\n const data = opCtx.data as Record<string, unknown>;\n if (\n needsTenant &&\n fields.has('organization_id') &&\n (data.organization_id == null || data.organization_id === '')\n ) {\n data.organization_id = opCtx.context!.tenantId;\n }\n if (\n needsOwner &&\n fields.has('owner_id') &&\n (data.owner_id == null || data.owner_id === '')\n ) {\n data.owner_id = opCtx.context!.userId;\n }\n }\n }\n }\n\n // 3. RLS filter injection\n const allRlsPolicies = this.collectRLSPolicies(permissionSets, opCtx.object, opCtx.operation);\n if (allRlsPolicies.length > 0 && opCtx.ast) {\n // Field-existence safety: wildcard policies (`object: '*'`) target\n // fields like `organization_id` that may not exist on every object\n // (e.g. system tables, CRM apps that haven't yet adopted multi-tenancy).\n //\n // We treat such policies as a *deny* contribution rather than dropping\n // them, so they fail-closed when no per-object policy provides an\n // alternate match. Any per-object policy that DOES compile against\n // the object will OR-combine and grant access (e.g. `sys_user_self`).\n // When the schema lookup itself fails we keep all policies (drivers\n // will surface column errors clearly during compilation).\n const objectFields = await this.getObjectFieldNames(metadata, opCtx.object, ql);\n const tenancyDisabled = this.tenancyDisabledCache.get(opCtx.object) === true;\n let dropped = 0;\n const compilable = objectFields\n ? allRlsPolicies.filter((p) => {\n const targetField = this.extractTargetField(p.using);\n if (!targetField) return true;\n if (objectFields.has(targetField)) return true;\n // Schema-level opt-out: when the object explicitly\n // disabled tenancy (`tenancy.enabled === false`), the\n // wildcard `tenant_isolation` policy targeting\n // `organization_id` was never meant to apply. Treat as\n // \"not applicable\" — skip silently without contributing\n // to the deny sentinel, mirroring how the registry skips\n // injecting the column itself for these tables.\n if (tenancyDisabled && targetField === 'organization_id') {\n return false;\n }\n dropped++;\n return false;\n })\n : allRlsPolicies;\n let rlsFilter = this.rlsCompiler.compileFilter(compilable, opCtx.context);\n // If every applicable policy was dropped because of missing fields,\n // contribute the deny sentinel (zero rows) — matches the rls-compiler\n // semantics for \"policies were applicable but none compiled\".\n if (rlsFilter == null && dropped > 0) {\n rlsFilter = { ...RLS_DENY_FILTER };\n }\n if (rlsFilter) {\n if (opCtx.ast.where) {\n opCtx.ast.where = { $and: [opCtx.ast.where, rlsFilter] };\n } else {\n opCtx.ast.where = rlsFilter;\n }\n }\n }\n\n await next();\n\n // 4. Field-level security: mask restricted fields in read results\n if (opCtx.result && ['find', 'findOne'].includes(opCtx.operation)) {\n const fieldPerms = this.permissionEvaluator.getFieldPermissions(opCtx.object, permissionSets);\n if (Object.keys(fieldPerms).length > 0) {\n opCtx.result = this.fieldMasker.maskResults(opCtx.result, fieldPerms, opCtx.object);\n }\n }\n });\n\n ctx.logger.info('Security middleware registered on ObjectQL engine');\n\n // Defer platform admin bootstrap until all plugins finish starting —\n // sys_user / sys_permission_set objects must be registered (by\n // plugin-auth and platform-objects respectively) before we can\n // insert seed rows. Falls back to immediate execution when the\n // kernel does not expose `hook` (test stubs).\n let bootstrapRanOnce = false;\n const runBootstrap = async () => {\n try {\n const report = await bootstrapPlatformAdmin(ql, this.bootstrapPermissionSets, {\n logger: ctx.logger,\n });\n bootstrapRanOnce = true;\n ctx.logger.info('[security] platform bootstrap complete', report);\n return report;\n } catch (e) {\n ctx.logger.warn('[security] platform bootstrap failed', { error: (e as Error).message });\n return undefined;\n }\n };\n if (typeof (ctx as any).hook === 'function') {\n (ctx as any).hook('kernel:ready', runBootstrap);\n } else {\n void runBootstrap();\n }\n\n // Re-run bootstrap after a sys_user insert so the FIRST user that\n // signs up after boot is auto-promoted to platform admin without\n // requiring a server restart. The function itself is idempotent\n // and bails out as soon as any platform admin exists.\n //\n // Also, in multi-tenant mode, ensure every newly registered user\n // has at least one organization — otherwise the default\n // tenant_isolation RLS policy hides every record from them and\n // the dashboard's RequireOrganization guard (which has a\n // single-tenant carve-out for org-less users) lets them through\n // to a UI showing \"No data\" everywhere.\n ql.registerMiddleware(async (opCtx: any, next: () => Promise<void>) => {\n await next();\n if (\n opCtx?.object === 'sys_user' &&\n (opCtx?.operation === 'create' || opCtx?.operation === 'insert')\n ) {\n if (bootstrapRanOnce) {\n await runBootstrap();\n }\n if (this.multiTenant) {\n const newUser = opCtx?.result ?? opCtx?.data;\n if (newUser?.id) {\n try {\n await ensureUserHasOrganization(ql, newUser, {\n logger: ctx.logger,\n cloneSeedData: cloneTenantSeedData,\n });\n } catch (e) {\n ctx.logger.warn('[security] ensure-user-has-organization failed', {\n error: (e as Error).message,\n });\n }\n }\n }\n }\n });\n\n // After a sys_organization insert, give the new org its own private\n // copy of the artifact's demo data (Salesforce-sandbox style):\n //\n // 1. PRIMARY PATH — replay the seed datasets registered on the\n // kernel's `seed-datasets` service (populated by AppPlugin at\n // start) with `organizationId: <newOrgId>`. SeedLoader scopes\n // both existing-record lookups and reference resolution to\n // that org, so upsert mode produces an independent copy per\n // tenant. This works for the FIRST org and EVERY subsequent\n // org, whether the org was auto-created by signup\n // (`ensureUserHasOrganization`) or manually via the\n // better-auth `createOrganization` API.\n //\n // 2. FALLBACK A — when no `seed-datasets` service is registered\n // (e.g. a plugin-shaped deployment with no AppPlugin), and\n // this is the FIRST org, fall back to the legacy\n // `claimOrphanTenantRows` path that adopts any NULL-org rows\n // a previous AppPlugin inline-seed may have inserted.\n //\n // 3. FALLBACK B — when no `seed-datasets` service is registered\n // and this is NOT the first org, fall back to\n // `cloneTenantSeedData` (donor-based row copy from the very\n // first org). Useful for upgrade paths where the new\n // service-based flow hasn't been wired yet.\n if (this.multiTenant) {\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 // Locate the kernel via ctx — most kernel impls expose either\n // `getService` on PluginContext directly or attach the kernel\n // ref. Anything we can't resolve becomes `undefined` and we\n // gracefully fall back.\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 { /* service not registered */ }\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('[security] failed to count organizations', {\n error: (e as Error).message,\n });\n }\n\n // ── Primary path: SeedLoader replay scoped to newOrgId ─────\n // Uses the `seed-replayer` callable that AppPlugin registers\n // on the kernel (keeps plugin-security free of @objectstack/runtime\n // import — runtime already depends on us, so the reverse would\n // be circular).\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 `[security] 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('[security] per-org seed: datasets present but no replayer registered', {\n organizationId: newOrgId,\n });\n }\n } catch (e) {\n ctx.logger.warn('[security] per-org seed replay failed, falling back', {\n organizationId: newOrgId,\n 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 claimOrphanTenantRows(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 `[security] claimed ${total} orphan seed row(s) for first organization ${newOrgId}`,\n { breakdown: claims },\n );\n return;\n }\n } catch (e) {\n ctx.logger.warn('[security] claim-orphan-tenant-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 cloneTenantSeedData(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 `[security] cloned ${total} seed row(s) for new organization ${newOrgId}`,\n { breakdown: summary },\n );\n }\n } catch (e) {\n ctx.logger.warn('[security] clone-tenant-seed-data failed', {\n organizationId: newOrgId,\n error: (e as Error).message,\n });\n }\n }\n });\n }\n }\n\n async destroy(): Promise<void> {\n // No cleanup needed\n }\n\n /**\n * Collect all RLS policies from permission sets applicable to the given object/operation.\n */\n private collectRLSPolicies(\n permissionSets: PermissionSet[],\n objectName: string,\n operation: string\n ): RowLevelSecurityPolicy[] {\n const allPolicies: RowLevelSecurityPolicy[] = [];\n\n for (const ps of permissionSets) {\n if (ps.rowLevelSecurity) {\n for (const policy of ps.rowLevelSecurity) {\n // In single-tenant mode, strip any policy that filters on\n // `current_user.organization_id` — there is no meaningful\n // tenant to compare against, so the policy would either drop\n // every row (when the field exists on the object) or be\n // dropped by the field-existence safety net. Either way it's\n // pure overhead. Substring match is sufficient: every\n // wildcard tenant policy in the default permission sets uses\n // exactly this token, and authors who want a different\n // multi-tenant story should turn `multiTenant: false` off.\n if (\n !this.multiTenant &&\n policy.using &&\n policy.using.includes('current_user.organization_id')\n ) {\n continue;\n }\n allPolicies.push(policy);\n }\n }\n }\n\n return this.rlsCompiler.getApplicablePolicies(objectName, operation, allPolicies);\n }\n\n /**\n * Resolve the column-name set for an object (lowercased). Returns\n * `null` if the schema can't be loaded — caller should fail-closed.\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 // Only cache positive resolutions — a `null` may simply mean the\n // schema isn't registered yet at boot, and we want subsequent calls\n // to retry rather than be permanently denied.\n if (result) {\n this.fieldNamesCache.set(objectName, result);\n }\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 // Prefer ObjectQL's per-engine SchemaRegistry as the source of truth\n // for the live field set: it reflects registry-time augmentations\n // (system-field auto-injection like `organization_id`) that the\n // standalone metadata artifact loaded at boot may not include.\n // Fall back to the metadata service for objects ObjectQL doesn't\n // know about (system tables registered through other paths).\n let obj: any = 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 // Populate the tenancy opt-out cache alongside the field set so\n // the RLS filter pass can decide whether a wildcard\n // `organization_id` policy is genuinely \"applicable but\n // uncompilable\" (deny) versus \"not applicable on this object\"\n // (skip without contributing to the deny sentinel).\n const tenancyDisabled =\n (obj as any)?.tenancy?.enabled === false ||\n (obj as any)?.systemFields?.tenant === false;\n this.tenancyDisabledCache.set(objectName, !!tenancyDisabled);\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 /**\n * Extract the left-hand field name from a simple RLS expression like\n * `field = current_user.x` or `field IN (current_user.y)`. Returns\n * `null` for unsupported shapes (in which case we keep the policy).\n */\n private extractTargetField(using?: string): string | null {\n if (!using) return null;\n // Match `field =` or `field IN`/`in`. Note: `\\b` is omitted after `=`\n // because `=` is non-word and the next char (space) is non-word too —\n // a word boundary cannot exist between two non-word chars, so `=\\b`\n // would never match. We instead require the alternation token to be\n // followed by whitespace or `(`.\n const m = using.match(/^\\s*([a-z_][a-z0-9_]*)\\s*(?:=|IN|in)(?=\\s|\\()/);\n return m ? m[1] : null;\n }\n}\n"],"mappings":"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","names":["SYSTEM_CTX","SYSTEM_CTX","SYSTEM_CTX","genId","tryFind"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/permission-evaluator.ts","../src/rls-compiler.ts","../src/field-masker.ts","../src/errors.ts","../src/bootstrap-platform-admin.ts","../src/claim-orphan-tenant-rows.ts","../src/clone-tenant-seed-data.ts","../src/ensure-user-has-organization.ts","../src/manifest.ts","../src/security-plugin.ts"],"sourcesContent":["// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * @objectstack/plugin-security\n * \n * Security Plugin for ObjectStack\n * Provides RBAC, Row-Level Security (RLS), and Field-Level Security runtime.\n */\n\nexport { SecurityPlugin } from './security-plugin.js';\nexport { PermissionEvaluator } from './permission-evaluator.js';\nexport { RLSCompiler, RLS_DENY_FILTER } from './rls-compiler.js';\nexport { FieldMasker } from './field-masker.js';\nexport { PermissionDeniedError, isPermissionDeniedError } from './errors.js';\nexport {\n securityObjects,\n securityDefaultPermissionSets,\n securityPluginManifestHeader,\n SECURITY_PLUGIN_ID,\n SECURITY_PLUGIN_VERSION,\n} from './manifest.js';\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { PermissionSet, ObjectPermission, FieldPermission } from '@objectstack/spec/security';\n\n/**\n * Operation type mapping to permission checks\n */\nconst OPERATION_TO_PERMISSION: Record<string, keyof ObjectPermission> = {\n find: 'allowRead',\n findOne: 'allowRead',\n count: 'allowRead',\n aggregate: 'allowRead',\n insert: 'allowCreate',\n update: 'allowEdit',\n delete: 'allowDelete',\n};\n\n/**\n * PermissionEvaluator\n * \n * Runtime evaluator for PermissionSet definitions.\n * Resolves aggregated permissions from roles to concrete allow/deny decisions.\n */\nexport class PermissionEvaluator {\n /**\n * Check if an operation is allowed on an object for the given permission sets.\n * Uses \"most permissive\" merging: if ANY permission set allows, it's allowed.\n */\n checkObjectPermission(\n operation: string,\n objectName: string,\n permissionSets: PermissionSet[]\n ): boolean {\n const permKey = OPERATION_TO_PERMISSION[operation];\n if (!permKey) return true; // Unknown operations are allowed by default\n\n for (const ps of permissionSets) {\n // Honour the `'*'` wildcard sentinel — admin permission sets typically\n // grant blanket access via a single `objects: { '*': … }` entry rather\n // than enumerating every system object.\n const objPerm = ps.objects?.[objectName] ?? ps.objects?.['*'];\n if (objPerm) {\n // Check if modifyAllRecords is set (super-user bypass for write ops)\n if (['allowEdit', 'allowDelete'].includes(permKey) && objPerm.modifyAllRecords) {\n return true;\n }\n // Check if viewAllRecords is set (super-user bypass for read ops)\n if (permKey === 'allowRead' && (objPerm.viewAllRecords || objPerm.modifyAllRecords)) {\n return true;\n }\n // Check the specific permission\n if (objPerm[permKey]) {\n return true;\n }\n }\n }\n\n return false;\n }\n\n /**\n * Get the merged field permissions for an object.\n * Returns a map of field names to their effective permissions.\n * Uses \"most permissive\" merging.\n */\n getFieldPermissions(\n objectName: string,\n permissionSets: PermissionSet[]\n ): Record<string, FieldPermission> {\n const result: Record<string, FieldPermission> = {};\n\n for (const ps of permissionSets) {\n if (!ps.fields) continue;\n\n for (const [key, perm] of Object.entries(ps.fields)) {\n // Field keys are in format: \"object_name.field_name\"\n if (!key.startsWith(`${objectName}.`)) continue;\n const fieldName = key.substring(objectName.length + 1);\n\n if (!result[fieldName]) {\n result[fieldName] = { readable: false, editable: false };\n }\n\n // Most permissive merge\n if (perm.readable) result[fieldName].readable = true;\n if (perm.editable) result[fieldName].editable = true;\n }\n }\n\n return result;\n }\n\n /**\n * Resolve permission sets for a list of identifier names from metadata.\n *\n * Identifiers are matched to `PermissionSet.name`. The names may be\n * either role names (when `sys_role.name` is reused as a permission set\n * name — common for default admin/member/viewer roles) or explicit\n * permission set names supplied through `ExecutionContext.permissions[]`\n * (resolved by `resolveExecutionContext` from `sys_user_permission_set`\n * and `sys_role_permission_set`).\n *\n * Async because the underlying metadata service exposes `list()` as a\n * Promise — synchronous iteration would silently yield zero results\n * (the historical SecurityPlugin behaviour, masking all enforcement).\n *\n * `bootstrapPermissionSets` is a fallback list of plugin-owned permission\n * sets (typically the platform defaults: admin_full_access /\n * member_default / viewer_readonly) that are registered via\n * `manifest.register({ permissions })` but do not currently propagate\n * into the metadata service's `list()` index. Without this fallback,\n * SecurityPlugin would never resolve the defaults and all enforcement\n * would be silently disabled for authenticated requests.\n */\n async resolvePermissionSets(\n identifiers: string[],\n metadataService: any,\n bootstrapPermissionSets: PermissionSet[] = [],\n /**\n * Optional async loader for permission set names that aren't found in\n * metadata or bootstrap. Lets callers query user-defined permission\n * sets persisted in `sys_permission_set`. Failures are swallowed.\n */\n dbLoader?: (unresolved: string[]) => Promise<PermissionSet[]>\n ): Promise<PermissionSet[]> {\n if (identifiers.length === 0) return [];\n\n const result: PermissionSet[] = [];\n const seen = new Set<string>();\n\n // Get all permission sets from metadata. Support both async (Manager) and\n // sync (test stub) implementations of `list`.\n let allPermSets: any = [];\n try {\n const listed = metadataService?.list?.('permission')\n ?? metadataService?.list?.('permissions')\n ?? [];\n allPermSets = typeof (listed as any)?.then === 'function' ? await listed : listed;\n } catch {\n allPermSets = [];\n }\n if (!Array.isArray(allPermSets)) allPermSets = [];\n\n const wanted = new Set(identifiers);\n for (const ps of allPermSets) {\n if (wanted.has(ps.name) && !seen.has(ps.name)) {\n seen.add(ps.name);\n result.push(ps);\n }\n }\n\n // Fallback: any wanted name not yet matched is sourced from the\n // bootstrap list (plugin-owned defaults). Avoids silent failure when\n // permission sets are registered via `manifest.register` but the\n // metadata service hasn't indexed them.\n for (const ps of bootstrapPermissionSets) {\n if (wanted.has(ps.name) && !seen.has(ps.name)) {\n seen.add(ps.name);\n result.push(ps);\n }\n }\n\n // Last-resort: query user-defined permission sets from the database.\n // Without this, custom permission sets (created via the admin UI as\n // `sys_permission_set` rows) would be silently ignored both for CRUD\n // enforcement and for field-level masking.\n if (dbLoader) {\n const unresolved = identifiers.filter((n) => !seen.has(n));\n if (unresolved.length > 0) {\n try {\n const dbRows = await dbLoader(unresolved);\n for (const ps of dbRows ?? []) {\n if (ps?.name && !seen.has(ps.name)) {\n seen.add(ps.name);\n result.push(ps);\n }\n }\n } catch {\n // Swallow — the request shouldn't fail just because the DB\n // lookup is unavailable.\n }\n }\n }\n\n return result;\n }\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { RowLevelSecurityPolicy } from '@objectstack/spec/security';\nimport type { ExecutionContext } from '@objectstack/spec/kernel';\n\n/**\n * RLS User Context\n * Variables available for RLS expression evaluation.\n */\ninterface RLSUserContext {\n id?: string;\n /**\n * Active organization id for the request. RLS expressions reference\n * this as `current_user.organization_id`. Sourced from\n * `ExecutionContext.tenantId` (the runtime keeps the abstract\n * \"tenant\" name, but at the data/RLS layer the canonical column is\n * `organization_id` — see better-auth's organization plugin).\n */\n organization_id?: string;\n roles?: string[];\n [key: string]: unknown;\n}\n\n/**\n * Sentinel filter used when applicable RLS policies exist but none can\n * be compiled against the current execution context (typically because a\n * required `current_user.*` variable is missing — e.g. the user has no\n * active organization). The filter compares `id` against a non-printable\n * UUID-shaped string that no real record will ever carry, so the upstream\n * SQL layer naturally returns zero rows without raising an error. This\n * gives us **fail-closed** semantics for select/update/delete on tables\n * that the user is not entitled to see, without forcing every caller to\n * handle a thrown `PermissionDeniedError` for what is conceptually an\n * empty result set.\n *\n * Exposed for the SecurityPlugin's optional short-circuit path and for\n * tests; see {@link RLSCompiler.compileFilter}.\n */\nexport const RLS_DENY_FILTER: Record<string, unknown> = Object.freeze({\n id: '__rls_deny__:00000000-0000-0000-0000-000000000000',\n});\n\n/**\n * RLSCompiler\n * \n * Compiles Row-Level Security policy expressions into query filters.\n * Converts `using` / `check` expressions into ObjectQL-compatible filter conditions.\n */\nexport class RLSCompiler {\n /**\n * Compile RLS policies into a query filter for the given user context.\n * Multiple policies for the same object/operation are OR-combined (any match allows access).\n *\n * Return-value semantics:\n * - `null` → no policies applicable → caller applies no RLS filter.\n * - non-null → caller AND's it onto the existing where clause.\n * - {@link RLS_DENY_FILTER} → policies were defined but none could be\n * compiled (e.g. wildcard `tenant_isolation` against a user with no\n * active organization). The caller must treat this as \"deny by\n * default\" — its `id` comparison naturally yields zero rows on\n * select/update/delete, which is the safe fail-closed answer.\n */\n compileFilter(\n policies: RowLevelSecurityPolicy[],\n executionContext?: ExecutionContext\n ): Record<string, unknown> | null {\n if (policies.length === 0) return null;\n\n const userCtx: RLSUserContext = {\n id: executionContext?.userId,\n organization_id: executionContext?.tenantId,\n roles: executionContext?.roles,\n };\n\n const filters: Record<string, unknown>[] = [];\n\n for (const policy of policies) {\n if (!policy.using) continue;\n const filter = this.compileExpression(policy.using, userCtx);\n if (filter) {\n filters.push(filter);\n }\n }\n\n if (filters.length === 0) {\n // Policies *were* applicable but every one of them depended on a\n // `current_user.*` variable that wasn't populated (or used an\n // expression we couldn't compile). Fail closed — return a sentinel\n // filter that matches no rows. This prevents the \"user without an\n // active org sees every tenant's data\" class of bug.\n return RLS_DENY_FILTER;\n }\n if (filters.length === 1) return filters[0];\n\n // Multiple policies: OR-combine (any policy allows access)\n return { $or: filters };\n }\n\n /**\n * Compile a single RLS expression into a query filter.\n * \n * Supports simple expressions like:\n * - \"field_name = current_user.property\"\n * - \"field_name IN (current_user.array_property)\"\n * - \"field_name = 'literal_value'\"\n */\n compileExpression(\n expression: string,\n userCtx: RLSUserContext\n ): Record<string, unknown> | null {\n if (!expression) return null;\n\n // Handle simple equality: \"field = current_user.property\"\n const eqMatch = expression.match(/^\\s*(\\w+)\\s*=\\s*current_user\\.(\\w+)\\s*$/);\n if (eqMatch) {\n const [, field, prop] = eqMatch;\n const value = userCtx[prop];\n // Skip when the user-context value is missing (undefined or null).\n // A `null` `organization_id` means \"no active organization\" — applying\n // the filter as `organization_id IS NULL` would silently expose every\n // un-tenanted row across tenants and break system tables that lack the\n // column entirely. Treating null as \"skip this policy\" makes the\n // tenant_isolation rule safely opt-out for users without an active org\n // while still applying when one is set.\n if (value === undefined || value === null) return null;\n return { [field]: value };\n }\n\n // Handle literal equality: \"field = 'value'\"\n const litMatch = expression.match(/^\\s*(\\w+)\\s*=\\s*'([^']*)'\\s*$/);\n if (litMatch) {\n const [, field, value] = litMatch;\n return { [field]: value };\n }\n\n // Handle IN: \"field IN (current_user.array_property)\"\n const inMatch = expression.match(/^\\s*(\\w+)\\s+IN\\s+\\(\\s*current_user\\.(\\w+)\\s*\\)\\s*$/i);\n if (inMatch) {\n const [, field, prop] = inMatch;\n const value = userCtx[prop];\n if (!Array.isArray(value) || value.length === 0) return null;\n return { [field]: { $in: value } };\n }\n\n // Unsupported expression: return null (no additional RLS filter applied).\n // Note: callers should treat absence of RLS policies as \"allow all\" only when\n // no policies are defined. If policies exist but cannot be compiled, the caller\n // may want to deny access as a safety measure.\n return null;\n }\n\n /**\n * Get applicable RLS policies for a given object and operation.\n */\n getApplicablePolicies(\n objectName: string,\n operation: string,\n allPolicies: RowLevelSecurityPolicy[]\n ): RowLevelSecurityPolicy[] {\n // Map engine operation to RLS operation type\n const rlsOp = this.mapOperationToRLS(operation);\n\n return allPolicies.filter(policy => {\n // Check object match\n if (policy.object !== objectName && policy.object !== '*') return false;\n\n // Check operation match\n if (policy.operation === 'all') return true;\n if (policy.operation === rlsOp) return true;\n\n return false;\n });\n }\n\n private mapOperationToRLS(operation: string): string {\n switch (operation) {\n case 'find':\n case 'findOne':\n case 'count':\n case 'aggregate':\n return 'select';\n case 'insert':\n return 'insert';\n case 'update':\n return 'update';\n case 'delete':\n return 'delete';\n default:\n return 'select';\n }\n }\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { FieldPermission } from '@objectstack/spec/security';\n\n/**\n * FieldMasker\n * \n * Applies field-level security by stripping restricted fields from query results.\n */\nexport class FieldMasker {\n /**\n * Mask fields in query results based on field permissions.\n * Removes fields that the user does not have read access to.\n */\n maskResults(\n results: any | any[],\n fieldPermissions: Record<string, FieldPermission>,\n _objectName: string\n ): any | any[] {\n // If no field permissions defined, return results as-is\n if (Object.keys(fieldPermissions).length === 0) return results;\n\n // Get list of non-readable fields\n const hiddenFields = Object.entries(fieldPermissions)\n .filter(([, perm]) => !perm.readable)\n .map(([field]) => field);\n\n if (hiddenFields.length === 0) return results;\n\n if (Array.isArray(results)) {\n return results.map(record => this.maskRecord(record, hiddenFields));\n }\n\n return this.maskRecord(results, hiddenFields);\n }\n\n /**\n * Get non-editable fields for use in write operations.\n * Returns a list of field names that should be stripped from incoming data.\n */\n getNonEditableFields(\n fieldPermissions: Record<string, FieldPermission>\n ): string[] {\n return Object.entries(fieldPermissions)\n .filter(([, perm]) => !perm.editable)\n .map(([field]) => field);\n }\n\n /**\n * Strip non-editable fields from write data.\n */\n stripNonEditableFields(\n data: Record<string, any>,\n fieldPermissions: Record<string, FieldPermission>\n ): Record<string, any> {\n const nonEditable = this.getNonEditableFields(fieldPermissions);\n if (nonEditable.length === 0) return data;\n\n const result = { ...data };\n for (const field of nonEditable) {\n delete result[field];\n }\n return result;\n }\n\n /**\n * Detect which fields in the caller's write payload would touch a\n * field they are not allowed to edit. Returns the set of offending\n * field names (no duplicates, sorted for stable error messages).\n *\n * Used by the security middleware on insert/update to fail-closed\n * with an explicit 403 rather than silently dropping fields — a\n * silent drop hides the security boundary from honest clients\n * (their update partially \"doesn't save\") and gives an attacker no\n * negative signal that the field exists. Throwing makes the\n * boundary observable in both directions.\n *\n * `data` may be a single record or an array of records (bulk insert);\n * either way the returned list is the union across all rows.\n *\n * Fields without a permission entry pass through — permission sets\n * are an allow-list at the field level only for fields they\n * explicitly enumerate. Most objects do not declare per-field rules\n * and remain fully editable.\n */\n detectForbiddenWrites(\n data: Record<string, any> | Record<string, any>[],\n fieldPermissions: Record<string, FieldPermission>\n ): string[] {\n if (Object.keys(fieldPermissions).length === 0) return [];\n const nonEditable = new Set(this.getNonEditableFields(fieldPermissions));\n if (nonEditable.size === 0) return [];\n\n const offenders = new Set<string>();\n const rows = Array.isArray(data) ? data : [data];\n for (const row of rows) {\n if (!row || typeof row !== 'object') continue;\n for (const field of Object.keys(row)) {\n if (nonEditable.has(field)) offenders.add(field);\n }\n }\n return Array.from(offenders).sort();\n }\n\n private maskRecord(record: any, hiddenFields: string[]): any {\n if (!record || typeof record !== 'object') return record;\n\n const result = { ...record };\n for (const field of hiddenFields) {\n delete result[field];\n }\n return result;\n }\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * Typed sentinel error thrown by `SecurityPlugin` when an operation is\n * denied. Caught by `@objectstack/runtime`'s HTTP dispatcher and translated\n * to HTTP 403.\n */\nexport class PermissionDeniedError extends Error {\n readonly code = 'PERMISSION_DENIED';\n readonly statusCode = 403;\n readonly details?: Record<string, unknown>;\n constructor(message: string, details?: Record<string, unknown>) {\n super(message);\n this.name = 'PermissionDeniedError';\n this.details = details;\n }\n}\n\nexport function isPermissionDeniedError(e: unknown): e is PermissionDeniedError {\n if (!e || typeof e !== 'object') return false;\n const anyE = e as any;\n return (\n anyE.name === 'PermissionDeniedError' ||\n anyE.code === 'PERMISSION_DENIED' ||\n (typeof anyE.message === 'string' && anyE.message.startsWith('[Security] Access denied'))\n );\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * bootstrapPlatformAdmin — first-boot platform admin promotion.\n *\n * Two responsibilities, both idempotent and run on `kernel:ready`:\n *\n * 1. **Seed `sys_permission_set` rows** for each `defaultPermissionSets`\n * entry (admin_full_access / member_default / viewer_readonly). The\n * dashboard's CRUD on `sys_permission_set` needs persisted rows to\n * exist so admins can grant them to users by id; the in-memory\n * bootstrap list alone is invisible to the standard CRUD UI.\n *\n * 2. **Promote the first registered user to platform admin** by\n * inserting a `sys_user_permission_set` row that points at\n * `admin_full_access` with `organization_id = NULL` (= cross-tenant).\n * If a platform admin already exists, this is a no-op forever.\n *\n * Zero configuration: `pnpm dev:crm` → sign up → \"I'm admin\".\n *\n * The DB column shape (`object_permissions` JSON text) does not match\n * the spec shape (`objects` record). For now we only need stable rows\n * with the right `name` so `resolveExecutionContext` can translate the\n * link-table id back to the bootstrap permission set name; the actual\n * `objects`/`rowLevelSecurity` definitions are still served from the\n * in-memory `bootstrapPermissionSets` list inside SecurityPlugin.\n */\n\nimport type { PermissionSet } from '@objectstack/spec/security';\n\ninterface BootstrapOptions {\n /** Logger from PluginContext. */\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 : [];\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\n/**\n * Persist seed permission sets and promote the first registered user to\n * platform admin. Safe to call multiple times.\n */\nexport async function bootstrapPlatformAdmin(\n ql: any,\n bootstrapPermissionSets: PermissionSet[],\n options: BootstrapOptions = {},\n): Promise<{ seeded: number; adminPromoted: boolean; reason?: string }> {\n const logger = options.logger;\n if (!ql || typeof ql.find !== 'function' || typeof ql.insert !== 'function') {\n return { seeded: 0, adminPromoted: false, reason: 'objectql_unavailable' };\n }\n\n // 1. Seed permission set rows (one row per name, idempotent).\n const seeded: Record<string, string> = {}; // name -> id\n for (const ps of bootstrapPermissionSets) {\n if (!ps.name) continue;\n const existing = await tryFind(ql, 'sys_permission_set', { name: ps.name }, 1);\n if (existing.length > 0 && existing[0].id) {\n seeded[ps.name] = existing[0].id;\n continue;\n }\n const id = genId('ps');\n const created = await tryInsert(ql, 'sys_permission_set', {\n id,\n name: ps.name,\n label: ps.label ?? ps.name,\n description: ps.description ?? null,\n object_permissions: JSON.stringify(ps.objects ?? {}),\n field_permissions: JSON.stringify(ps.fields ?? {}),\n active: true,\n });\n if (created?.id) seeded[ps.name] = created.id;\n else if (created) seeded[ps.name] = id;\n }\n\n const seededCount = Object.keys(seeded).length;\n\n // 2. First-user platform admin promotion.\n const adminPsId = seeded['admin_full_access'];\n if (!adminPsId) {\n return { seeded: seededCount, adminPromoted: false, reason: 'admin_permission_set_missing' };\n }\n\n // If a platform admin already exists, we're done.\n const existingAdminLinks = await tryFind(\n ql,\n 'sys_user_permission_set',\n { permission_set_id: adminPsId },\n 5,\n );\n if (existingAdminLinks.some((r) => !r.organization_id)) {\n return { seeded: seededCount, adminPromoted: false, reason: 'already_have_admin' };\n }\n\n // Promote the oldest user (= first registrant). If no users yet, the\n // sys_user post-create middleware will rerun this on first sign-up.\n const allUsers = await tryFind(ql, 'sys_user', {}, 50);\n if (allUsers.length === 0) {\n logger?.info?.('[security] no users yet — first sign-up will be promoted to platform admin');\n return { seeded: seededCount, adminPromoted: false, reason: 'no_users' };\n }\n const sorted = [...allUsers].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 target = sorted[0];\n\n const inserted = await tryInsert(ql, 'sys_user_permission_set', {\n id: genId('ups'),\n user_id: target.id,\n permission_set_id: adminPsId,\n organization_id: null,\n granted_by: null,\n });\n if (!inserted) {\n logger?.warn?.(`[security] failed to grant admin_full_access to first user ${target.email ?? target.id}`);\n return { seeded: seededCount, adminPromoted: false, reason: 'insert_failed' };\n }\n logger?.info?.(`[security] first user promoted to platform admin: ${target.email ?? target.id}`);\n return { seeded: seededCount, adminPromoted: true };\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * claimOrphanTenantRows — assign seed-loaded records to the first organization.\n *\n * Seeds (`defineDataset`) 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 claimOrphanTenantRows(\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?.('[security] claimOrphanTenantRows: 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?.(`[security] 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?.(`[security] 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?.(`[security] 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 * cloneTenantSeedData — 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 `claimOrphanTenantRows` hook\n * (see `claim-orphan-tenant-rows.ts`) only fires for the very first\n * org — every subsequent user that auto-creates a personal Workspace\n * via `ensureUserHasOrganization` ends up looking at an empty\n * dashboard. 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 cloneTenantSeedData(\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?.('[security] cloneTenantSeedData: 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?.('[security] cloneTenantSeedData: 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?.('[security] cloneTenantSeedData: 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?.('[security] cloneTenantSeedData: 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 * ensureUserHasOrganization — auto-create a personal org for new users.\n *\n * In multi-tenant mode, every record visible through the default\n * `tenant_isolation` RLS policy must have an `organization_id`, and\n * every authenticated user must have an `activeOrganizationId` on their\n * session for that policy to evaluate to anything other than \"deny\n * all\". A user with zero `sys_member` rows, however, can sign in\n * successfully and reach the dashboard — the dashboard's\n * `RequireOrganization` guard has a single-tenant carve-out that lets\n * users with empty organization lists through, so they land on a UI\n * that simply hides every record. The standard remedy (\"invite users\n * via an admin\") doesn't apply to self-service signup.\n *\n * This helper, run right after a `sys_user` insert, ensures the new\n * user has at least one organization by creating a personal workspace\n * (named \"<User>'s Workspace\", slug `<username>-workspace`) and an\n * owner-role `sys_member` row. The user's session will pick this up as\n * their `activeOrganizationId` on the next sign-in / org-list refresh\n * (better-auth's `setActiveOrganization` runs lazily when the picker\n * sees exactly one membership).\n *\n * Idempotent: bails out if the user already has any `sys_member` row.\n * Slug collisions retry with a numeric suffix; a cap of 5 attempts\n * means a pathological username will fail loudly rather than loop.\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 * Optional hook called after a personal org is successfully created.\n * Used by SecurityPlugin to wire in `cloneTenantSeedData` so each\n * new workspace gets its own copy of demo data. Pulled in via DI\n * to keep this helper free of a hard import on the cloner (which\n * keeps the tenant-claim and ensure-org test surfaces narrow).\n */\n cloneSeedData?: (\n ql: any,\n targetOrgId: string,\n opts: { logger?: EnsureOptions['logger'] },\n ) => Promise<{ object: string; count: number }[]>;\n}\n\nconst SYSTEM_CTX = { isSystem: true };\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\nfunction slugify(input: string, fallback = 'workspace'): string {\n const cleaned = input\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-+|-+$/g, '')\n .slice(0, 40);\n return cleaned || fallback;\n}\n\n/**\n * Derive an ASCII-safe slug seed for a user whose display name doesn't\n * survive sanitisation (e.g. Chinese / Japanese / emoji-only names).\n * Prefers the email local-part, then a short id suffix — never the bare\n * literal \"workspace\" (which would produce ugly `workspace-workspace`\n * org slugs and matching `workspace-workspace-<env>.localhost` hostnames).\n */\nfunction deriveSlugFallback(user: { id: string; email?: string }): string {\n if (user.email) {\n const local = user.email.split('@')[0] ?? '';\n const localSlug = local\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-+|-+$/g, '')\n .slice(0, 40);\n if (localSlug) return localSlug;\n }\n const idTail = user.id.replace(/[^a-z0-9]/gi, '').slice(-8).toLowerCase();\n return idTail ? `user-${idTail}` : 'user';\n}\n\nfunction deriveBaseName(user: { name?: string; email?: string; id: string }): string {\n if (user.name && user.name.trim()) return user.name.trim();\n if (user.email) {\n const local = user.email.split('@')[0];\n if (local) return local;\n }\n return user.id;\n}\n\nasync function tryFind(ql: any, object: string, where: any, limit = 1): Promise<any[]> {\n try {\n const rows = await ql.find(object, { where, limit }, { context: SYSTEM_CTX });\n return Array.isArray(rows) ? rows : [];\n } catch {\n return [];\n }\n}\n\n/**\n * Ensure `user` has at least one `sys_member` row. Creates a personal\n * organization owned by them if not.\n *\n * Returns `{ created: true, organizationId }` when a new org was made,\n * or `{ created: false, reason }` when the user already has memberships\n * or the operation was skipped.\n */\nexport async function ensureUserHasOrganization(\n ql: any,\n user: { id: string; name?: string; email?: string },\n options: EnsureOptions = {},\n): Promise<{ created: boolean; organizationId?: string; reason?: string }> {\n const logger = options.logger;\n const cloneSeedData = options.cloneSeedData;\n if (!ql || typeof ql.find !== 'function' || typeof ql.insert !== 'function') {\n return { created: false, reason: 'objectql_unavailable' };\n }\n if (!user?.id) return { created: false, reason: 'invalid_user' };\n\n // Idempotency gate: any existing membership means we're done.\n const existing = await tryFind(ql, 'sys_member', { user_id: user.id }, 1);\n if (existing.length > 0) {\n return { created: false, reason: 'already_member' };\n }\n\n const base = deriveBaseName(user);\n const orgName = `${base}'s Workspace`;\n const slugFallback = deriveSlugFallback(user);\n const baseSlug = slugify(base, slugFallback);\n\n // Find a free slug. better-auth allows duplicates technically, but\n // the dashboard renders the slug as a stable identifier so we keep\n // them unique-per-platform for human-readable URLs.\n let slug = `${baseSlug}-workspace`;\n for (let attempt = 1; attempt <= 5; attempt += 1) {\n const collision = await tryFind(ql, 'sys_organization', { slug }, 1);\n if (collision.length === 0) break;\n slug = `${baseSlug}-workspace-${attempt + 1}`;\n if (attempt === 5) {\n logger?.warn?.(\n `[security] could not find a free slug for personal org of ${user.email ?? user.id}`,\n );\n return { created: false, reason: 'slug_exhausted' };\n }\n }\n\n const orgId = genId('org');\n let orgRow: any = null;\n try {\n orgRow = await ql.insert(\n 'sys_organization',\n { id: orgId, name: orgName, slug, logo: null, metadata: null },\n { context: SYSTEM_CTX },\n );\n } catch (e) {\n logger?.warn?.(`[security] failed to create personal org for ${user.email ?? user.id}`, {\n error: (e as Error).message,\n });\n return { created: false, reason: 'org_insert_failed' };\n }\n\n const finalOrgId = orgRow?.id ?? orgId;\n\n try {\n await ql.insert(\n 'sys_member',\n {\n id: genId('mem'),\n organization_id: finalOrgId,\n user_id: user.id,\n role: 'owner',\n },\n { context: SYSTEM_CTX },\n );\n } catch (e) {\n logger?.warn?.(`[security] failed to create owner-member row for ${user.email ?? user.id}`, {\n error: (e as Error).message,\n });\n return { created: false, reason: 'member_insert_failed', organizationId: finalOrgId };\n }\n\n logger?.info?.(\n `[security] created personal organization \"${orgName}\" (${finalOrgId}) for ${user.email ?? user.id}`,\n );\n\n // Best-effort: clone the platform-first org's user-defined data into\n // the new personal workspace so demo apps (CRM, etc.) stay populated\n // for every signup. No-op when this IS the first org, when the donor\n // has no data, or when this op fails — never blocks signup.\n if (cloneSeedData) {\n try {\n const summary = await cloneSeedData(ql, finalOrgId, { logger });\n if (summary.length > 0) {\n const total = summary.reduce((s, c) => s + c.count, 0);\n logger?.info?.(\n `[security] cloned ${total} seed row(s) into personal organization ${finalOrgId}`,\n { breakdown: summary },\n );\n }\n } catch (e) {\n logger?.warn?.('[security] cloneTenantSeedData failed', {\n error: (e as Error).message,\n });\n }\n }\n\n return { created: true, organizationId: finalOrgId };\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * Canonical plugin-security manifest source.\n *\n * Both `objectstack.config.ts` (compile-time) and `security-plugin.ts`\n * (runtime `manifest.register`) import from this file so the two\n * registration paths cannot drift (D7).\n */\n\nimport {\n SysPermissionSet,\n SysRole,\n SysUserPermissionSet,\n SysRolePermissionSet,\n defaultPermissionSets,\n} from '@objectstack/platform-objects/security';\n\nexport const SECURITY_PLUGIN_ID = 'com.objectstack.plugin-security';\nexport const SECURITY_PLUGIN_VERSION = '1.0.0';\n\n/** Security objects owned by plugin-security. */\nexport const securityObjects = [\n SysRole,\n SysPermissionSet,\n SysUserPermissionSet,\n SysRolePermissionSet,\n];\n\n/** Default platform permission sets (admin / member / viewer). */\nexport const securityDefaultPermissionSets = defaultPermissionSets;\n\n/** Manifest header shared by compile-time config and runtime registration. */\nexport const securityPluginManifestHeader = {\n id: SECURITY_PLUGIN_ID,\n namespace: 'sys',\n version: SECURITY_PLUGIN_VERSION,\n type: 'plugin' as const,\n scope: 'system' as const,\n defaultDatasource: 'cloud',\n name: 'Security Plugin',\n description: 'RBAC roles and permission sets for ObjectStack (Role, PermissionSet)',\n};\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport { Plugin, PluginContext } from '@objectstack/core';\nimport type { PermissionSet, RowLevelSecurityPolicy } from '@objectstack/spec/security';\nimport { PermissionEvaluator } from './permission-evaluator.js';\nimport { RLSCompiler, RLS_DENY_FILTER } from './rls-compiler.js';\nimport { FieldMasker } from './field-masker.js';\nimport { PermissionDeniedError } from './errors.js';\nimport { bootstrapPlatformAdmin } from './bootstrap-platform-admin.js';\nimport { claimOrphanTenantRows } from './claim-orphan-tenant-rows.js';\nimport { cloneTenantSeedData } from './clone-tenant-seed-data.js';\nimport { ensureUserHasOrganization } from './ensure-user-has-organization.js';\nimport {\n securityObjects,\n securityDefaultPermissionSets,\n securityPluginManifestHeader,\n} from './manifest.js';\n\nexport interface SecurityPluginOptions {\n /**\n * Additional permission sets to register with the metadata service on\n * plugin start. Defaults to {@link securityDefaultPermissionSets}\n * (admin_full_access / member_default / viewer_readonly).\n */\n defaultPermissionSets?: PermissionSet[];\n /**\n * Permission set name applied as an implicit baseline whenever an\n * authenticated request has no resolved permission sets (and no roles\n * that map to one). This guarantees baseline tenant/owner RLS for\n * every logged-in user even before an admin assigns explicit\n * profiles. Set to `null` to disable.\n *\n * @default 'member_default'\n */\n fallbackPermissionSet?: string | null;\n /**\n * Whether this deployment is multi-tenant.\n *\n * When `true` (default), SecurityPlugin:\n * - Auto-injects `organization_id = ctx.tenantId` on insert when\n * the target object declares an `organization_id` field.\n * - Honours the wildcard `tenant_isolation` RLS policy\n * (`organization_id = current_user.organization_id`) shipped with\n * the default `member_default` / `viewer_readonly` permission\n * sets.\n *\n * When `false`, SecurityPlugin:\n * - Skips the `organization_id` auto-injection block (saves a\n * metadata lookup per insert; `owner_id` injection still runs).\n * - Strips any RLS policy whose USING expression references\n * `current_user.organization_id` from the per-request policy\n * set, so single-tenant deployments don't pay the\n * field-existence safety-net cost on every find.\n *\n * Field-Level Security, owner-based RLS, and per-object permission\n * checks (allowRead/allowCreate/…) all operate identically regardless\n * of this flag. Set this to `false` for single-tenant or\n * single-organization deployments where `organization_id` carries no\n * meaning.\n *\n * @default true\n */\n multiTenant?: boolean;\n}\n\n/**\n * SecurityPlugin\n *\n * Provides RBAC, Row-Level Security, and Field-Level Security runtime.\n * Registers as an engine middleware on the ObjectQL engine.\n *\n * This plugin is fully optional — without it, the system operates\n * without permission checks (same as current behavior).\n *\n * Dependencies:\n * - objectql service (ObjectQL engine with middleware support)\n * - metadata service (MetadataFacade for reading permission sets and RLS policies)\n */\nexport class SecurityPlugin implements Plugin {\n name = 'com.objectstack.security';\n type = 'standard';\n version = '1.0.0';\n dependencies = ['com.objectstack.engine.objectql'];\n\n private permissionEvaluator = new PermissionEvaluator();\n private rlsCompiler = new RLSCompiler();\n private fieldMasker = new FieldMasker();\n private readonly bootstrapPermissionSets: PermissionSet[];\n private readonly fallbackPermissionSet: string | null;\n private readonly multiTenant: boolean;\n /**\n * Per-object field-name cache. Populated lazily from the metadata\n * service / ObjectQL registry on first access per object. Schemas are\n * effectively immutable for the lifetime of the kernel today (hot\n * reload tears the kernel down), so we don't bother with\n * invalidation — a kernel restart drops the cache.\n */\n private readonly fieldNamesCache = new Map<string, Set<string> | null>();\n /**\n * Per-object cache of tenancy opt-out. `true` means the schema\n * explicitly disabled multi-tenancy (`tenancy.enabled === false` or\n * `systemFields.tenant === false`). Wildcard policies that target\n * the conventional tenant column (`organization_id`) are treated as\n * *not applicable* on these tables instead of triggering the\n * field-missing deny sentinel — without this, every read of a\n * cross-org catalog (e.g. `sys_package`, the Marketplace) returns\n * zero rows.\n */\n private readonly tenancyDisabledCache = new Map<string, boolean>();\n\n constructor(options: SecurityPluginOptions = {}) {\n this.bootstrapPermissionSets =\n options.defaultPermissionSets ?? securityDefaultPermissionSets;\n this.fallbackPermissionSet =\n options.fallbackPermissionSet === undefined\n ? 'member_default'\n : options.fallbackPermissionSet;\n this.multiTenant = options.multiTenant !== false;\n }\n\n async init(ctx: PluginContext): Promise<void> {\n ctx.logger.info('Initializing Security Plugin...');\n\n // Register security services\n ctx.registerService('security.permissions', this.permissionEvaluator);\n ctx.registerService('security.rls', this.rlsCompiler);\n ctx.registerService('security.fieldMasker', this.fieldMasker);\n // Bootstrap permission sets (admin_full_access, member_default,\n // viewer_readonly by default) — exposed as a service so other\n // plugins (e.g. plugin-hono-server's /me/permissions endpoint)\n // can pass them as the fallback list to\n // `PermissionEvaluator.resolvePermissionSets` without re-importing\n // the platform-objects package directly.\n ctx.registerService('security.bootstrapPermissionSets', this.bootstrapPermissionSets);\n ctx.registerService('security.fallbackPermissionSet', this.fallbackPermissionSet);\n\n ctx.getService<{ register(m: any): void }>('manifest').register({\n ...securityPluginManifestHeader,\n objects: securityObjects,\n // Permission sets ride along on the manifest so the metadata service\n // can resolve them by name when SecurityPlugin middleware queries\n // `metadata.list('permissions')`.\n permissions: this.bootstrapPermissionSets,\n });\n\n ctx.logger.info('Security Plugin initialized', {\n defaultPermissionSets: this.bootstrapPermissionSets.map((p) => p.name),\n });\n }\n\n async start(ctx: PluginContext): Promise<void> {\n ctx.logger.info('Starting Security Plugin...');\n\n // Get required services\n let ql: any;\n let metadata: any;\n\n try {\n ql = ctx.getService('objectql');\n metadata = ctx.getService('metadata');\n } catch (e) {\n ctx.logger.warn('ObjectQL or metadata service not available, security middleware not registered');\n return;\n }\n\n if (!ql || typeof ql.registerMiddleware !== 'function') {\n ctx.logger.warn('ObjectQL engine does not support middleware, security middleware not registered');\n return;\n }\n\n // Construct a dbLoader once that lets resolvePermissionSets\n // surface user-defined permission sets from `sys_permission_set`\n // (created via the admin UI) in addition to plugin-registered\n // ones. Uses `isSystem` to bypass tenant RLS.\n const dbLoader = ql\n ? async (names: string[]) => {\n let rows: any;\n try {\n rows = await ql.find(\n 'sys_permission_set',\n { where: { name: { $in: names } }, limit: names.length },\n { context: { isSystem: true } },\n );\n } catch {\n rows = [];\n }\n const list = Array.isArray(rows) ? rows : rows?.records ?? [];\n return list.map((r: any) => ({\n name: r.name,\n label: r.label,\n objects: typeof r.object_permissions === 'string'\n ? JSON.parse(r.object_permissions || '{}')\n : r.object_permissions ?? {},\n fields: typeof r.field_permissions === 'string'\n ? JSON.parse(r.field_permissions || '{}')\n : r.field_permissions ?? {},\n }));\n }\n : undefined;\n\n // Register security middleware\n ql.registerMiddleware(async (opCtx: any, next: () => Promise<void>) => {\n // System operations bypass security\n if (opCtx.context?.isSystem) {\n return next();\n }\n\n const roles = opCtx.context?.roles ?? [];\n const explicitPermissionSets = opCtx.context?.permissions ?? [];\n\n // Skip security checks if no roles AND no explicit permission sets\n // AND no userId (anonymous/unauthenticated). The auth middleware\n // should handle authentication separately.\n if (\n roles.length === 0 &&\n explicitPermissionSets.length === 0 &&\n !opCtx.context?.userId\n ) {\n return next();\n }\n\n // 1. Resolve permission sets from BOTH role names and explicit\n // permission set names attached to the execution context.\n let permissionSets: PermissionSet[] = [];\n try {\n const requested = [...roles, ...explicitPermissionSets];\n // Implicit baseline: when an authenticated request resolved zero\n // permission sets, fall back to the configured baseline (default\n // `member_default`). This guarantees tenant + owner RLS even\n // before an admin has assigned a profile/permission set.\n if (\n requested.length === 0 &&\n opCtx.context?.userId &&\n this.fallbackPermissionSet\n ) {\n requested.push(this.fallbackPermissionSet);\n }\n permissionSets = await this.permissionEvaluator.resolvePermissionSets(\n requested,\n metadata,\n this.bootstrapPermissionSets,\n dbLoader,\n );\n // **Post-resolution fallback** — closes the fail-open hole that\n // appears when a user's `roles` array is populated (e.g. a\n // better-auth `sys_member.role` like `owner`/`admin`/`member`)\n // but no `sys_role`→`sys_permission_set` binding exists yet, so\n // resolution returns an empty array. Without this, both the\n // CRUD check (`permissionSets.length > 0`) and the RLS injection\n // (`allRlsPolicies.length > 0`) below get skipped → the user\n // sees every tenant's data. Authenticated users with no\n // resolved permission sets always inherit the configured\n // baseline (default `member_default`, which carries\n // `tenant_isolation` + `owner_only_writes`).\n if (\n permissionSets.length === 0 &&\n opCtx.context?.userId &&\n this.fallbackPermissionSet\n ) {\n const fallback = await this.permissionEvaluator.resolvePermissionSets(\n [this.fallbackPermissionSet],\n metadata,\n this.bootstrapPermissionSets,\n dbLoader,\n );\n permissionSets = fallback;\n }\n } catch (e) {\n // If metadata service is misconfigured, log and continue without permission checks\n // rather than blocking all operations\n return next();\n }\n\n // 2. CRUD permission check\n if (permissionSets.length > 0) {\n const allowed = this.permissionEvaluator.checkObjectPermission(\n opCtx.operation,\n opCtx.object,\n permissionSets\n );\n\n if (!allowed) {\n throw new PermissionDeniedError(\n `[Security] Access denied: operation '${opCtx.operation}' on object '${opCtx.object}' ` +\n `is not permitted for roles [${roles.join(', ')}]`,\n { operation: opCtx.operation, object: opCtx.object, roles, permissionSets: explicitPermissionSets },\n );\n }\n }\n\n // 2.5. Field-Level Security write enforcement.\n //\n // The client-side masker (ObjectForm / inline grid) already hides\n // non-editable fields from the UI, but that is a UX layer only —\n // a hand-crafted POST / direct ObjectQL call can still target a\n // forbidden field. We fail-closed here with an explicit 403 and\n // the offending field names, so:\n //\n // - honest clients get an actionable error (vs. silent drop,\n // which manifests as a confusing partial-save), and\n // - probing clients see that the boundary is enforced (vs.\n // getting a 200 with the field silently ignored, which\n // reveals nothing).\n //\n // Runs BEFORE the tenant/owner auto-injection (step 3.5) so the\n // system-set fields are not subject to the user's edit\n // permissions — they are populated from the execution context,\n // not from the caller's payload.\n if (\n (opCtx.operation === 'insert' || opCtx.operation === 'update') &&\n opCtx.data &&\n permissionSets.length > 0\n ) {\n const fieldPerms = this.permissionEvaluator.getFieldPermissions(\n opCtx.object,\n permissionSets,\n );\n if (Object.keys(fieldPerms).length > 0) {\n const forbidden = this.fieldMasker.detectForbiddenWrites(\n opCtx.data,\n fieldPerms,\n );\n if (forbidden.length > 0) {\n throw new PermissionDeniedError(\n `[Security] Field write denied: not permitted to edit ` +\n `[${forbidden.join(', ')}] on '${opCtx.object}'`,\n {\n operation: opCtx.operation,\n object: opCtx.object,\n roles,\n permissionSets: explicitPermissionSets,\n forbiddenFields: forbidden,\n },\n );\n }\n }\n }\n\n // 3.5. Auto-inject tenancy/ownership fields on insert.\n //\n // When an authenticated user inserts a record, the canonical\n // tenant column (`organization_id`) and ownership column\n // (`owner_id`) should be auto-populated from\n // `ExecutionContext.tenantId` / `userId` so the row is visible\n // to the same RLS policies that gate reads. Without this, the\n // user creates a row that has `organization_id = NULL`, which\n // the very next `find` will filter out as a wrong-tenant row —\n // a confusing \"I just created it but I can't see it\" footgun.\n //\n // Only fills fields that:\n // - the object actually declares (so unrelated tables are\n // untouched)\n // - aren't already set in the payload (caller wins)\n // - have a corresponding value on the execution context.\n //\n // The `organization_id` half is gated on `multiTenant`; in\n // single-tenant deployments it's pure overhead.\n if (\n opCtx.operation === 'insert' &&\n opCtx.data &&\n typeof opCtx.data === 'object' &&\n !Array.isArray(opCtx.data)\n ) {\n const needsTenant =\n this.multiTenant && !!opCtx.context?.tenantId;\n const needsOwner = !!opCtx.context?.userId;\n if (needsTenant || needsOwner) {\n const fields = await this.getObjectFieldNames(metadata, opCtx.object, ql);\n if (fields) {\n const data = opCtx.data as Record<string, unknown>;\n if (\n needsTenant &&\n fields.has('organization_id') &&\n (data.organization_id == null || data.organization_id === '')\n ) {\n data.organization_id = opCtx.context!.tenantId;\n }\n if (\n needsOwner &&\n fields.has('owner_id') &&\n (data.owner_id == null || data.owner_id === '')\n ) {\n data.owner_id = opCtx.context!.userId;\n }\n }\n }\n }\n\n // 3. RLS filter injection\n const allRlsPolicies = this.collectRLSPolicies(permissionSets, opCtx.object, opCtx.operation);\n if (allRlsPolicies.length > 0 && opCtx.ast) {\n // Field-existence safety: wildcard policies (`object: '*'`) target\n // fields like `organization_id` that may not exist on every object\n // (e.g. system tables, CRM apps that haven't yet adopted multi-tenancy).\n //\n // We treat such policies as a *deny* contribution rather than dropping\n // them, so they fail-closed when no per-object policy provides an\n // alternate match. Any per-object policy that DOES compile against\n // the object will OR-combine and grant access (e.g. `sys_user_self`).\n // When the schema lookup itself fails we keep all policies (drivers\n // will surface column errors clearly during compilation).\n const objectFields = await this.getObjectFieldNames(metadata, opCtx.object, ql);\n const tenancyDisabled = this.tenancyDisabledCache.get(opCtx.object) === true;\n let dropped = 0;\n const compilable = objectFields\n ? allRlsPolicies.filter((p) => {\n const targetField = this.extractTargetField(p.using);\n if (!targetField) return true;\n if (objectFields.has(targetField)) return true;\n // Schema-level opt-out: when the object explicitly\n // disabled tenancy (`tenancy.enabled === false`), the\n // wildcard `tenant_isolation` policy targeting\n // `organization_id` was never meant to apply. Treat as\n // \"not applicable\" — skip silently without contributing\n // to the deny sentinel, mirroring how the registry skips\n // injecting the column itself for these tables.\n if (tenancyDisabled && targetField === 'organization_id') {\n return false;\n }\n dropped++;\n return false;\n })\n : allRlsPolicies;\n let rlsFilter = this.rlsCompiler.compileFilter(compilable, opCtx.context);\n // If every applicable policy was dropped because of missing fields,\n // contribute the deny sentinel (zero rows) — matches the rls-compiler\n // semantics for \"policies were applicable but none compiled\".\n if (rlsFilter == null && dropped > 0) {\n rlsFilter = { ...RLS_DENY_FILTER };\n }\n if (rlsFilter) {\n if (opCtx.ast.where) {\n opCtx.ast.where = { $and: [opCtx.ast.where, rlsFilter] };\n } else {\n opCtx.ast.where = rlsFilter;\n }\n }\n }\n\n await next();\n\n // 4. Field-level security: mask restricted fields in read results\n if (opCtx.result && ['find', 'findOne'].includes(opCtx.operation)) {\n const fieldPerms = this.permissionEvaluator.getFieldPermissions(opCtx.object, permissionSets);\n if (Object.keys(fieldPerms).length > 0) {\n opCtx.result = this.fieldMasker.maskResults(opCtx.result, fieldPerms, opCtx.object);\n }\n }\n });\n\n ctx.logger.info('Security middleware registered on ObjectQL engine');\n\n // Defer platform admin bootstrap until all plugins finish starting —\n // sys_user / sys_permission_set objects must be registered (by\n // plugin-auth and platform-objects respectively) before we can\n // insert seed rows. Falls back to immediate execution when the\n // kernel does not expose `hook` (test stubs).\n let bootstrapRanOnce = false;\n const runBootstrap = async () => {\n try {\n const report = await bootstrapPlatformAdmin(ql, this.bootstrapPermissionSets, {\n logger: ctx.logger,\n });\n bootstrapRanOnce = true;\n ctx.logger.info('[security] platform bootstrap complete', report);\n return report;\n } catch (e) {\n ctx.logger.warn('[security] platform bootstrap failed', { error: (e as Error).message });\n return undefined;\n }\n };\n if (typeof (ctx as any).hook === 'function') {\n (ctx as any).hook('kernel:ready', runBootstrap);\n } else {\n void runBootstrap();\n }\n\n // Re-run bootstrap after a sys_user insert so the FIRST user that\n // signs up after boot is auto-promoted to platform admin without\n // requiring a server restart. The function itself is idempotent\n // and bails out as soon as any platform admin exists.\n //\n // Also, in multi-tenant mode, ensure every newly registered user\n // has at least one organization — otherwise the default\n // tenant_isolation RLS policy hides every record from them and\n // the dashboard's RequireOrganization guard (which has a\n // single-tenant carve-out for org-less users) lets them through\n // to a UI showing \"No data\" everywhere.\n ql.registerMiddleware(async (opCtx: any, next: () => Promise<void>) => {\n await next();\n if (\n opCtx?.object === 'sys_user' &&\n (opCtx?.operation === 'create' || opCtx?.operation === 'insert')\n ) {\n if (bootstrapRanOnce) {\n await runBootstrap();\n }\n if (this.multiTenant) {\n const newUser = opCtx?.result ?? opCtx?.data;\n if (newUser?.id) {\n try {\n await ensureUserHasOrganization(ql, newUser, {\n logger: ctx.logger,\n cloneSeedData: cloneTenantSeedData,\n });\n } catch (e) {\n ctx.logger.warn('[security] ensure-user-has-organization failed', {\n error: (e as Error).message,\n });\n }\n }\n }\n }\n });\n\n // After a sys_organization insert, give the new org its own private\n // copy of the artifact's demo data (Salesforce-sandbox style):\n //\n // 1. PRIMARY PATH — replay the seed datasets registered on the\n // kernel's `seed-datasets` service (populated by AppPlugin at\n // start) with `organizationId: <newOrgId>`. SeedLoader scopes\n // both existing-record lookups and reference resolution to\n // that org, so upsert mode produces an independent copy per\n // tenant. This works for the FIRST org and EVERY subsequent\n // org, whether the org was auto-created by signup\n // (`ensureUserHasOrganization`) or manually via the\n // better-auth `createOrganization` API.\n //\n // 2. FALLBACK A — when no `seed-datasets` service is registered\n // (e.g. a plugin-shaped deployment with no AppPlugin), and\n // this is the FIRST org, fall back to the legacy\n // `claimOrphanTenantRows` path that adopts any NULL-org rows\n // a previous AppPlugin inline-seed may have inserted.\n //\n // 3. FALLBACK B — when no `seed-datasets` service is registered\n // and this is NOT the first org, fall back to\n // `cloneTenantSeedData` (donor-based row copy from the very\n // first org). Useful for upgrade paths where the new\n // service-based flow hasn't been wired yet.\n if (this.multiTenant) {\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 // Locate the kernel via ctx — most kernel impls expose either\n // `getService` on PluginContext directly or attach the kernel\n // ref. Anything we can't resolve becomes `undefined` and we\n // gracefully fall back.\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 { /* service not registered */ }\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('[security] failed to count organizations', {\n error: (e as Error).message,\n });\n }\n\n // ── Primary path: SeedLoader replay scoped to newOrgId ─────\n // Uses the `seed-replayer` callable that AppPlugin registers\n // on the kernel (keeps plugin-security free of @objectstack/runtime\n // import — runtime already depends on us, so the reverse would\n // be circular).\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 `[security] 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('[security] per-org seed: datasets present but no replayer registered', {\n organizationId: newOrgId,\n });\n }\n } catch (e) {\n ctx.logger.warn('[security] per-org seed replay failed, falling back', {\n organizationId: newOrgId,\n 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 claimOrphanTenantRows(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 `[security] claimed ${total} orphan seed row(s) for first organization ${newOrgId}`,\n { breakdown: claims },\n );\n return;\n }\n } catch (e) {\n ctx.logger.warn('[security] claim-orphan-tenant-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 cloneTenantSeedData(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 `[security] cloned ${total} seed row(s) for new organization ${newOrgId}`,\n { breakdown: summary },\n );\n }\n } catch (e) {\n ctx.logger.warn('[security] clone-tenant-seed-data failed', {\n organizationId: newOrgId,\n error: (e as Error).message,\n });\n }\n }\n });\n }\n }\n\n async destroy(): Promise<void> {\n // No cleanup needed\n }\n\n /**\n * Collect all RLS policies from permission sets applicable to the given object/operation.\n */\n private collectRLSPolicies(\n permissionSets: PermissionSet[],\n objectName: string,\n operation: string\n ): RowLevelSecurityPolicy[] {\n const allPolicies: RowLevelSecurityPolicy[] = [];\n\n for (const ps of permissionSets) {\n if (ps.rowLevelSecurity) {\n for (const policy of ps.rowLevelSecurity) {\n // In single-tenant mode, strip any policy that filters on\n // `current_user.organization_id` — there is no meaningful\n // tenant to compare against, so the policy would either drop\n // every row (when the field exists on the object) or be\n // dropped by the field-existence safety net. Either way it's\n // pure overhead. Substring match is sufficient: every\n // wildcard tenant policy in the default permission sets uses\n // exactly this token, and authors who want a different\n // multi-tenant story should turn `multiTenant: false` off.\n if (\n !this.multiTenant &&\n policy.using &&\n policy.using.includes('current_user.organization_id')\n ) {\n continue;\n }\n allPolicies.push(policy);\n }\n }\n }\n\n return this.rlsCompiler.getApplicablePolicies(objectName, operation, allPolicies);\n }\n\n /**\n * Resolve the column-name set for an object (lowercased). Returns\n * `null` if the schema can't be loaded — caller should fail-closed.\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 // Only cache positive resolutions — a `null` may simply mean the\n // schema isn't registered yet at boot, and we want subsequent calls\n // to retry rather than be permanently denied.\n if (result) {\n this.fieldNamesCache.set(objectName, result);\n }\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 // Prefer ObjectQL's per-engine SchemaRegistry as the source of truth\n // for the live field set: it reflects registry-time augmentations\n // (system-field auto-injection like `organization_id`) that the\n // standalone metadata artifact loaded at boot may not include.\n // Fall back to the metadata service for objects ObjectQL doesn't\n // know about (system tables registered through other paths).\n let obj: any = 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 // Populate the tenancy opt-out cache alongside the field set so\n // the RLS filter pass can decide whether a wildcard\n // `organization_id` policy is genuinely \"applicable but\n // uncompilable\" (deny) versus \"not applicable on this object\"\n // (skip without contributing to the deny sentinel).\n const tenancyDisabled =\n (obj as any)?.tenancy?.enabled === false ||\n (obj as any)?.systemFields?.tenant === false;\n this.tenancyDisabledCache.set(objectName, !!tenancyDisabled);\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 /**\n * Extract the left-hand field name from a simple RLS expression like\n * `field = current_user.x` or `field IN (current_user.y)`. Returns\n * `null` for unsupported shapes (in which case we keep the policy).\n */\n private extractTargetField(using?: string): string | null {\n if (!using) return null;\n // Match `field =` or `field IN`/`in`. Note: `\\b` is omitted after `=`\n // because `=` is non-word and the next char (space) is non-word too —\n // a word boundary cannot exist between two non-word chars, so `=\\b`\n // would never match. We instead require the alternation token to be\n // followed by whitespace or `(`.\n const m = using.match(/^\\s*([a-z_][a-z0-9_]*)\\s*(?:=|IN|in)(?=\\s|\\()/);\n return m ? m[1] : null;\n }\n}\n"],"mappings":"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","names":["SYSTEM_CTX","SYSTEM_CTX","SYSTEM_CTX","genId","tryFind"]}
|
package/dist/index.mjs
CHANGED
|
@@ -655,8 +655,18 @@ function genId2(prefix) {
|
|
|
655
655
|
const ts = Date.now().toString(36);
|
|
656
656
|
return `${prefix}_${ts}${rand}`;
|
|
657
657
|
}
|
|
658
|
-
function slugify(input) {
|
|
659
|
-
|
|
658
|
+
function slugify(input, fallback = "workspace") {
|
|
659
|
+
const cleaned = input.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40);
|
|
660
|
+
return cleaned || fallback;
|
|
661
|
+
}
|
|
662
|
+
function deriveSlugFallback(user) {
|
|
663
|
+
if (user.email) {
|
|
664
|
+
const local = user.email.split("@")[0] ?? "";
|
|
665
|
+
const localSlug = local.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40);
|
|
666
|
+
if (localSlug) return localSlug;
|
|
667
|
+
}
|
|
668
|
+
const idTail = user.id.replace(/[^a-z0-9]/gi, "").slice(-8).toLowerCase();
|
|
669
|
+
return idTail ? `user-${idTail}` : "user";
|
|
660
670
|
}
|
|
661
671
|
function deriveBaseName(user) {
|
|
662
672
|
if (user.name && user.name.trim()) return user.name.trim();
|
|
@@ -687,7 +697,8 @@ async function ensureUserHasOrganization(ql, user, options = {}) {
|
|
|
687
697
|
}
|
|
688
698
|
const base = deriveBaseName(user);
|
|
689
699
|
const orgName = `${base}'s Workspace`;
|
|
690
|
-
const
|
|
700
|
+
const slugFallback = deriveSlugFallback(user);
|
|
701
|
+
const baseSlug = slugify(base, slugFallback);
|
|
691
702
|
let slug = `${baseSlug}-workspace`;
|
|
692
703
|
for (let attempt = 1; attempt <= 5; attempt += 1) {
|
|
693
704
|
const collision = await tryFind2(ql, "sys_organization", { slug }, 1);
|