@objectstack/plugin-security 5.1.0 → 5.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.mts CHANGED
@@ -179,6 +179,14 @@ interface RLSUserContext {
179
179
  */
180
180
  organization_id?: string;
181
181
  roles?: string[];
182
+ /**
183
+ * IDs of all users that share the active organization with the
184
+ * current user (incl. self). Pre-resolved by the runtime so RLS can
185
+ * scope identity tables like `sys_user` via
186
+ * `id IN (current_user.org_user_ids)` without needing subquery
187
+ * support in the compiler.
188
+ */
189
+ org_user_ids?: string[];
182
190
  [key: string]: unknown;
183
191
  }
184
192
  /**
@@ -8552,4 +8560,78 @@ declare const securityPluginManifestHeader: {
8552
8560
  description: string;
8553
8561
  };
8554
8562
 
8555
- export { FieldMasker, PermissionDeniedError, PermissionEvaluator, RLSCompiler, RLS_DENY_FILTER, SECURITY_PLUGIN_ID, SECURITY_PLUGIN_VERSION, SecurityPlugin, isPermissionDeniedError, securityDefaultPermissionSets, securityObjects, securityPluginManifestHeader };
8563
+ /**
8564
+ * ensureUserHasOrganization — auto-create a personal org for new users.
8565
+ *
8566
+ * In multi-tenant mode, every record visible through the default
8567
+ * `tenant_isolation` RLS policy must have an `organization_id`, and
8568
+ * every authenticated user must have an `activeOrganizationId` on their
8569
+ * session for that policy to evaluate to anything other than "deny
8570
+ * all". A user with zero `sys_member` rows, however, can sign in
8571
+ * successfully and reach the dashboard — the dashboard's
8572
+ * `RequireOrganization` guard has a single-tenant carve-out that lets
8573
+ * users with empty organization lists through, so they land on a UI
8574
+ * that simply hides every record. The standard remedy ("invite users
8575
+ * via an admin") doesn't apply to self-service signup.
8576
+ *
8577
+ * This helper, run right after a `sys_user` insert, ensures the new
8578
+ * user has at least one organization by creating a personal workspace
8579
+ * (named "<User>'s Workspace", slug `<username>-workspace`) and an
8580
+ * owner-role `sys_member` row. The user's session will pick this up as
8581
+ * their `activeOrganizationId` on the next sign-in / org-list refresh
8582
+ * (better-auth's `setActiveOrganization` runs lazily when the picker
8583
+ * sees exactly one membership).
8584
+ *
8585
+ * Idempotent: bails out if the user already has any `sys_member` row.
8586
+ * Slug collisions retry with a numeric suffix; a cap of 5 attempts
8587
+ * means a pathological username will fail loudly rather than loop.
8588
+ */
8589
+ interface EnsureOptions {
8590
+ logger?: {
8591
+ info: (message: string, meta?: Record<string, any>) => void;
8592
+ warn: (message: string, meta?: Record<string, any>) => void;
8593
+ };
8594
+ /**
8595
+ * Optional hook called after a personal org is successfully created.
8596
+ * Used by SecurityPlugin to wire in `cloneTenantSeedData` so each
8597
+ * new workspace gets its own copy of demo data. Pulled in via DI
8598
+ * to keep this helper free of a hard import on the cloner (which
8599
+ * keeps the tenant-claim and ensure-org test surfaces narrow).
8600
+ */
8601
+ cloneSeedData?: (ql: any, targetOrgId: string, opts: {
8602
+ logger?: EnsureOptions['logger'];
8603
+ }) => Promise<{
8604
+ object: string;
8605
+ count: number;
8606
+ }[]>;
8607
+ }
8608
+ /**
8609
+ * Ensure `user` has at least one `sys_member` row. Creates a personal
8610
+ * organization owned by them if not.
8611
+ *
8612
+ * Returns `{ created: true, organizationId }` when a new org was made,
8613
+ * or `{ created: false, reason }` when the user already has memberships
8614
+ * or the operation was skipped.
8615
+ */
8616
+ declare function ensureUserHasOrganization(ql: any, user: {
8617
+ id: string;
8618
+ name?: string;
8619
+ email?: string;
8620
+ }, options?: EnsureOptions): Promise<{
8621
+ created: boolean;
8622
+ organizationId?: string;
8623
+ reason?: string;
8624
+ }>;
8625
+
8626
+ interface CloneOptions {
8627
+ logger?: {
8628
+ info: (message: string, meta?: Record<string, any>) => void;
8629
+ warn: (message: string, meta?: Record<string, any>) => void;
8630
+ };
8631
+ }
8632
+ declare function cloneTenantSeedData(ql: any, targetOrgId: string, options?: CloneOptions): Promise<{
8633
+ object: string;
8634
+ count: number;
8635
+ }[]>;
8636
+
8637
+ export { FieldMasker, PermissionDeniedError, PermissionEvaluator, RLSCompiler, RLS_DENY_FILTER, SECURITY_PLUGIN_ID, SECURITY_PLUGIN_VERSION, SecurityPlugin, cloneTenantSeedData, ensureUserHasOrganization, isPermissionDeniedError, securityDefaultPermissionSets, securityObjects, securityPluginManifestHeader };
package/dist/index.d.ts CHANGED
@@ -179,6 +179,14 @@ interface RLSUserContext {
179
179
  */
180
180
  organization_id?: string;
181
181
  roles?: string[];
182
+ /**
183
+ * IDs of all users that share the active organization with the
184
+ * current user (incl. self). Pre-resolved by the runtime so RLS can
185
+ * scope identity tables like `sys_user` via
186
+ * `id IN (current_user.org_user_ids)` without needing subquery
187
+ * support in the compiler.
188
+ */
189
+ org_user_ids?: string[];
182
190
  [key: string]: unknown;
183
191
  }
184
192
  /**
@@ -8552,4 +8560,78 @@ declare const securityPluginManifestHeader: {
8552
8560
  description: string;
8553
8561
  };
8554
8562
 
8555
- export { FieldMasker, PermissionDeniedError, PermissionEvaluator, RLSCompiler, RLS_DENY_FILTER, SECURITY_PLUGIN_ID, SECURITY_PLUGIN_VERSION, SecurityPlugin, isPermissionDeniedError, securityDefaultPermissionSets, securityObjects, securityPluginManifestHeader };
8563
+ /**
8564
+ * ensureUserHasOrganization — auto-create a personal org for new users.
8565
+ *
8566
+ * In multi-tenant mode, every record visible through the default
8567
+ * `tenant_isolation` RLS policy must have an `organization_id`, and
8568
+ * every authenticated user must have an `activeOrganizationId` on their
8569
+ * session for that policy to evaluate to anything other than "deny
8570
+ * all". A user with zero `sys_member` rows, however, can sign in
8571
+ * successfully and reach the dashboard — the dashboard's
8572
+ * `RequireOrganization` guard has a single-tenant carve-out that lets
8573
+ * users with empty organization lists through, so they land on a UI
8574
+ * that simply hides every record. The standard remedy ("invite users
8575
+ * via an admin") doesn't apply to self-service signup.
8576
+ *
8577
+ * This helper, run right after a `sys_user` insert, ensures the new
8578
+ * user has at least one organization by creating a personal workspace
8579
+ * (named "<User>'s Workspace", slug `<username>-workspace`) and an
8580
+ * owner-role `sys_member` row. The user's session will pick this up as
8581
+ * their `activeOrganizationId` on the next sign-in / org-list refresh
8582
+ * (better-auth's `setActiveOrganization` runs lazily when the picker
8583
+ * sees exactly one membership).
8584
+ *
8585
+ * Idempotent: bails out if the user already has any `sys_member` row.
8586
+ * Slug collisions retry with a numeric suffix; a cap of 5 attempts
8587
+ * means a pathological username will fail loudly rather than loop.
8588
+ */
8589
+ interface EnsureOptions {
8590
+ logger?: {
8591
+ info: (message: string, meta?: Record<string, any>) => void;
8592
+ warn: (message: string, meta?: Record<string, any>) => void;
8593
+ };
8594
+ /**
8595
+ * Optional hook called after a personal org is successfully created.
8596
+ * Used by SecurityPlugin to wire in `cloneTenantSeedData` so each
8597
+ * new workspace gets its own copy of demo data. Pulled in via DI
8598
+ * to keep this helper free of a hard import on the cloner (which
8599
+ * keeps the tenant-claim and ensure-org test surfaces narrow).
8600
+ */
8601
+ cloneSeedData?: (ql: any, targetOrgId: string, opts: {
8602
+ logger?: EnsureOptions['logger'];
8603
+ }) => Promise<{
8604
+ object: string;
8605
+ count: number;
8606
+ }[]>;
8607
+ }
8608
+ /**
8609
+ * Ensure `user` has at least one `sys_member` row. Creates a personal
8610
+ * organization owned by them if not.
8611
+ *
8612
+ * Returns `{ created: true, organizationId }` when a new org was made,
8613
+ * or `{ created: false, reason }` when the user already has memberships
8614
+ * or the operation was skipped.
8615
+ */
8616
+ declare function ensureUserHasOrganization(ql: any, user: {
8617
+ id: string;
8618
+ name?: string;
8619
+ email?: string;
8620
+ }, options?: EnsureOptions): Promise<{
8621
+ created: boolean;
8622
+ organizationId?: string;
8623
+ reason?: string;
8624
+ }>;
8625
+
8626
+ interface CloneOptions {
8627
+ logger?: {
8628
+ info: (message: string, meta?: Record<string, any>) => void;
8629
+ warn: (message: string, meta?: Record<string, any>) => void;
8630
+ };
8631
+ }
8632
+ declare function cloneTenantSeedData(ql: any, targetOrgId: string, options?: CloneOptions): Promise<{
8633
+ object: string;
8634
+ count: number;
8635
+ }[]>;
8636
+
8637
+ export { FieldMasker, PermissionDeniedError, PermissionEvaluator, RLSCompiler, RLS_DENY_FILTER, SECURITY_PLUGIN_ID, SECURITY_PLUGIN_VERSION, SecurityPlugin, cloneTenantSeedData, ensureUserHasOrganization, isPermissionDeniedError, securityDefaultPermissionSets, securityObjects, securityPluginManifestHeader };
package/dist/index.js CHANGED
@@ -28,6 +28,8 @@ __export(index_exports, {
28
28
  SECURITY_PLUGIN_ID: () => SECURITY_PLUGIN_ID,
29
29
  SECURITY_PLUGIN_VERSION: () => SECURITY_PLUGIN_VERSION,
30
30
  SecurityPlugin: () => SecurityPlugin,
31
+ cloneTenantSeedData: () => cloneTenantSeedData,
32
+ ensureUserHasOrganization: () => ensureUserHasOrganization,
31
33
  isPermissionDeniedError: () => isPermissionDeniedError,
32
34
  securityDefaultPermissionSets: () => securityDefaultPermissionSets,
33
35
  securityObjects: () => securityObjects,
@@ -179,7 +181,8 @@ var RLSCompiler = class {
179
181
  const userCtx = {
180
182
  id: executionContext?.userId,
181
183
  organization_id: executionContext?.tenantId,
182
- roles: executionContext?.roles
184
+ roles: executionContext?.roles,
185
+ org_user_ids: executionContext?.org_user_ids
183
186
  };
184
187
  const filters = [];
185
188
  for (const policy of policies) {
@@ -1248,6 +1251,8 @@ var SecurityPlugin = class {
1248
1251
  SECURITY_PLUGIN_ID,
1249
1252
  SECURITY_PLUGIN_VERSION,
1250
1253
  SecurityPlugin,
1254
+ cloneTenantSeedData,
1255
+ ensureUserHasOrganization,
1251
1256
  isPermissionDeniedError,
1252
1257
  securityDefaultPermissionSets,
1253
1258
  securityObjects,