@objectstack/plugin-security 6.8.1 → 6.9.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 +33 -64
- package/dist/index.d.ts +33 -64
- package/dist/index.js +45 -139
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +45 -138
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -4
package/dist/index.d.mts
CHANGED
|
@@ -927,6 +927,14 @@ declare const securityObjects: ((Omit<{
|
|
|
927
927
|
} | undefined;
|
|
928
928
|
recordTypes?: string[] | undefined;
|
|
929
929
|
sharingModel?: "read" | "full" | "private" | "read_write" | undefined;
|
|
930
|
+
publicSharing?: {
|
|
931
|
+
enabled: boolean;
|
|
932
|
+
allowedAudiences?: ("email" | "public" | "link_only" | "signed_in")[] | undefined;
|
|
933
|
+
allowedPermissions?: ("edit" | "view" | "comment")[] | undefined;
|
|
934
|
+
maxExpiryDays?: number | undefined;
|
|
935
|
+
redactFields?: string[] | undefined;
|
|
936
|
+
eligibility?: string | undefined;
|
|
937
|
+
} | undefined;
|
|
930
938
|
keyPrefix?: string | undefined;
|
|
931
939
|
detail?: {
|
|
932
940
|
[x: string]: unknown;
|
|
@@ -3366,6 +3374,14 @@ declare const securityObjects: ((Omit<{
|
|
|
3366
3374
|
} | undefined;
|
|
3367
3375
|
recordTypes?: string[] | undefined;
|
|
3368
3376
|
sharingModel?: "read" | "full" | "private" | "read_write" | undefined;
|
|
3377
|
+
publicSharing?: {
|
|
3378
|
+
enabled: boolean;
|
|
3379
|
+
allowedAudiences?: ("email" | "public" | "link_only" | "signed_in")[] | undefined;
|
|
3380
|
+
allowedPermissions?: ("edit" | "view" | "comment")[] | undefined;
|
|
3381
|
+
maxExpiryDays?: number | undefined;
|
|
3382
|
+
redactFields?: string[] | undefined;
|
|
3383
|
+
eligibility?: string | undefined;
|
|
3384
|
+
} | undefined;
|
|
3369
3385
|
keyPrefix?: string | undefined;
|
|
3370
3386
|
detail?: {
|
|
3371
3387
|
[x: string]: unknown;
|
|
@@ -5769,6 +5785,14 @@ declare const securityObjects: ((Omit<{
|
|
|
5769
5785
|
} | undefined;
|
|
5770
5786
|
recordTypes?: string[] | undefined;
|
|
5771
5787
|
sharingModel?: "read" | "full" | "private" | "read_write" | undefined;
|
|
5788
|
+
publicSharing?: {
|
|
5789
|
+
enabled: boolean;
|
|
5790
|
+
allowedAudiences?: ("email" | "public" | "link_only" | "signed_in")[] | undefined;
|
|
5791
|
+
allowedPermissions?: ("edit" | "view" | "comment")[] | undefined;
|
|
5792
|
+
maxExpiryDays?: number | undefined;
|
|
5793
|
+
redactFields?: string[] | undefined;
|
|
5794
|
+
eligibility?: string | undefined;
|
|
5795
|
+
} | undefined;
|
|
5772
5796
|
keyPrefix?: string | undefined;
|
|
5773
5797
|
detail?: {
|
|
5774
5798
|
[x: string]: unknown;
|
|
@@ -7694,6 +7718,14 @@ declare const securityObjects: ((Omit<{
|
|
|
7694
7718
|
} | undefined;
|
|
7695
7719
|
recordTypes?: string[] | undefined;
|
|
7696
7720
|
sharingModel?: "read" | "full" | "private" | "read_write" | undefined;
|
|
7721
|
+
publicSharing?: {
|
|
7722
|
+
enabled: boolean;
|
|
7723
|
+
allowedAudiences?: ("email" | "public" | "link_only" | "signed_in")[] | undefined;
|
|
7724
|
+
allowedPermissions?: ("edit" | "view" | "comment")[] | undefined;
|
|
7725
|
+
maxExpiryDays?: number | undefined;
|
|
7726
|
+
redactFields?: string[] | undefined;
|
|
7727
|
+
eligibility?: string | undefined;
|
|
7728
|
+
} | undefined;
|
|
7697
7729
|
keyPrefix?: string | undefined;
|
|
7698
7730
|
detail?: {
|
|
7699
7731
|
[x: string]: unknown;
|
|
@@ -8730,69 +8762,6 @@ declare const securityPluginManifestHeader: {
|
|
|
8730
8762
|
description: string;
|
|
8731
8763
|
};
|
|
8732
8764
|
|
|
8733
|
-
/**
|
|
8734
|
-
* ensureUserHasOrganization — auto-create a personal org for new users.
|
|
8735
|
-
*
|
|
8736
|
-
* In multi-tenant mode, every record visible through the default
|
|
8737
|
-
* `tenant_isolation` RLS policy must have an `organization_id`, and
|
|
8738
|
-
* every authenticated user must have an `activeOrganizationId` on their
|
|
8739
|
-
* session for that policy to evaluate to anything other than "deny
|
|
8740
|
-
* all". A user with zero `sys_member` rows, however, can sign in
|
|
8741
|
-
* successfully and reach the dashboard — the dashboard's
|
|
8742
|
-
* `RequireOrganization` guard has a single-tenant carve-out that lets
|
|
8743
|
-
* users with empty organization lists through, so they land on a UI
|
|
8744
|
-
* that simply hides every record. The standard remedy ("invite users
|
|
8745
|
-
* via an admin") doesn't apply to self-service signup.
|
|
8746
|
-
*
|
|
8747
|
-
* This helper, run right after a `sys_user` insert, ensures the new
|
|
8748
|
-
* user has at least one organization by creating a personal workspace
|
|
8749
|
-
* (named "<User>'s Workspace", slug `<username>-workspace`) and an
|
|
8750
|
-
* owner-role `sys_member` row. The user's session will pick this up as
|
|
8751
|
-
* their `activeOrganizationId` on the next sign-in / org-list refresh
|
|
8752
|
-
* (better-auth's `setActiveOrganization` runs lazily when the picker
|
|
8753
|
-
* sees exactly one membership).
|
|
8754
|
-
*
|
|
8755
|
-
* Idempotent: bails out if the user already has any `sys_member` row.
|
|
8756
|
-
* Slug collisions retry with a numeric suffix; a cap of 5 attempts
|
|
8757
|
-
* means a pathological username will fail loudly rather than loop.
|
|
8758
|
-
*/
|
|
8759
|
-
interface EnsureOptions {
|
|
8760
|
-
logger?: {
|
|
8761
|
-
info: (message: string, meta?: Record<string, any>) => void;
|
|
8762
|
-
warn: (message: string, meta?: Record<string, any>) => void;
|
|
8763
|
-
};
|
|
8764
|
-
/**
|
|
8765
|
-
* Optional hook called after a personal org is successfully created.
|
|
8766
|
-
* Used by SecurityPlugin to wire in `cloneTenantSeedData` so each
|
|
8767
|
-
* new workspace gets its own copy of demo data. Pulled in via DI
|
|
8768
|
-
* to keep this helper free of a hard import on the cloner (which
|
|
8769
|
-
* keeps the tenant-claim and ensure-org test surfaces narrow).
|
|
8770
|
-
*/
|
|
8771
|
-
cloneSeedData?: (ql: any, targetOrgId: string, opts: {
|
|
8772
|
-
logger?: EnsureOptions['logger'];
|
|
8773
|
-
}) => Promise<{
|
|
8774
|
-
object: string;
|
|
8775
|
-
count: number;
|
|
8776
|
-
}[]>;
|
|
8777
|
-
}
|
|
8778
|
-
/**
|
|
8779
|
-
* Ensure `user` has at least one `sys_member` row. Creates a personal
|
|
8780
|
-
* organization owned by them if not.
|
|
8781
|
-
*
|
|
8782
|
-
* Returns `{ created: true, organizationId }` when a new org was made,
|
|
8783
|
-
* or `{ created: false, reason }` when the user already has memberships
|
|
8784
|
-
* or the operation was skipped.
|
|
8785
|
-
*/
|
|
8786
|
-
declare function ensureUserHasOrganization(ql: any, user: {
|
|
8787
|
-
id: string;
|
|
8788
|
-
name?: string;
|
|
8789
|
-
email?: string;
|
|
8790
|
-
}, options?: EnsureOptions): Promise<{
|
|
8791
|
-
created: boolean;
|
|
8792
|
-
organizationId?: string;
|
|
8793
|
-
reason?: string;
|
|
8794
|
-
}>;
|
|
8795
|
-
|
|
8796
8765
|
interface CloneOptions {
|
|
8797
8766
|
logger?: {
|
|
8798
8767
|
info: (message: string, meta?: Record<string, any>) => void;
|
|
@@ -8804,4 +8773,4 @@ declare function cloneTenantSeedData(ql: any, targetOrgId: string, options?: Clo
|
|
|
8804
8773
|
count: number;
|
|
8805
8774
|
}[]>;
|
|
8806
8775
|
|
|
8807
|
-
export { FieldMasker, PermissionDeniedError, PermissionEvaluator, RLSCompiler, RLS_DENY_FILTER, SECURITY_PLUGIN_ID, SECURITY_PLUGIN_VERSION, SecurityPlugin, cloneTenantSeedData,
|
|
8776
|
+
export { FieldMasker, PermissionDeniedError, PermissionEvaluator, RLSCompiler, RLS_DENY_FILTER, SECURITY_PLUGIN_ID, SECURITY_PLUGIN_VERSION, SecurityPlugin, cloneTenantSeedData, isPermissionDeniedError, securityDefaultPermissionSets, securityObjects, securityPluginManifestHeader };
|
package/dist/index.d.ts
CHANGED
|
@@ -927,6 +927,14 @@ declare const securityObjects: ((Omit<{
|
|
|
927
927
|
} | undefined;
|
|
928
928
|
recordTypes?: string[] | undefined;
|
|
929
929
|
sharingModel?: "read" | "full" | "private" | "read_write" | undefined;
|
|
930
|
+
publicSharing?: {
|
|
931
|
+
enabled: boolean;
|
|
932
|
+
allowedAudiences?: ("email" | "public" | "link_only" | "signed_in")[] | undefined;
|
|
933
|
+
allowedPermissions?: ("edit" | "view" | "comment")[] | undefined;
|
|
934
|
+
maxExpiryDays?: number | undefined;
|
|
935
|
+
redactFields?: string[] | undefined;
|
|
936
|
+
eligibility?: string | undefined;
|
|
937
|
+
} | undefined;
|
|
930
938
|
keyPrefix?: string | undefined;
|
|
931
939
|
detail?: {
|
|
932
940
|
[x: string]: unknown;
|
|
@@ -3366,6 +3374,14 @@ declare const securityObjects: ((Omit<{
|
|
|
3366
3374
|
} | undefined;
|
|
3367
3375
|
recordTypes?: string[] | undefined;
|
|
3368
3376
|
sharingModel?: "read" | "full" | "private" | "read_write" | undefined;
|
|
3377
|
+
publicSharing?: {
|
|
3378
|
+
enabled: boolean;
|
|
3379
|
+
allowedAudiences?: ("email" | "public" | "link_only" | "signed_in")[] | undefined;
|
|
3380
|
+
allowedPermissions?: ("edit" | "view" | "comment")[] | undefined;
|
|
3381
|
+
maxExpiryDays?: number | undefined;
|
|
3382
|
+
redactFields?: string[] | undefined;
|
|
3383
|
+
eligibility?: string | undefined;
|
|
3384
|
+
} | undefined;
|
|
3369
3385
|
keyPrefix?: string | undefined;
|
|
3370
3386
|
detail?: {
|
|
3371
3387
|
[x: string]: unknown;
|
|
@@ -5769,6 +5785,14 @@ declare const securityObjects: ((Omit<{
|
|
|
5769
5785
|
} | undefined;
|
|
5770
5786
|
recordTypes?: string[] | undefined;
|
|
5771
5787
|
sharingModel?: "read" | "full" | "private" | "read_write" | undefined;
|
|
5788
|
+
publicSharing?: {
|
|
5789
|
+
enabled: boolean;
|
|
5790
|
+
allowedAudiences?: ("email" | "public" | "link_only" | "signed_in")[] | undefined;
|
|
5791
|
+
allowedPermissions?: ("edit" | "view" | "comment")[] | undefined;
|
|
5792
|
+
maxExpiryDays?: number | undefined;
|
|
5793
|
+
redactFields?: string[] | undefined;
|
|
5794
|
+
eligibility?: string | undefined;
|
|
5795
|
+
} | undefined;
|
|
5772
5796
|
keyPrefix?: string | undefined;
|
|
5773
5797
|
detail?: {
|
|
5774
5798
|
[x: string]: unknown;
|
|
@@ -7694,6 +7718,14 @@ declare const securityObjects: ((Omit<{
|
|
|
7694
7718
|
} | undefined;
|
|
7695
7719
|
recordTypes?: string[] | undefined;
|
|
7696
7720
|
sharingModel?: "read" | "full" | "private" | "read_write" | undefined;
|
|
7721
|
+
publicSharing?: {
|
|
7722
|
+
enabled: boolean;
|
|
7723
|
+
allowedAudiences?: ("email" | "public" | "link_only" | "signed_in")[] | undefined;
|
|
7724
|
+
allowedPermissions?: ("edit" | "view" | "comment")[] | undefined;
|
|
7725
|
+
maxExpiryDays?: number | undefined;
|
|
7726
|
+
redactFields?: string[] | undefined;
|
|
7727
|
+
eligibility?: string | undefined;
|
|
7728
|
+
} | undefined;
|
|
7697
7729
|
keyPrefix?: string | undefined;
|
|
7698
7730
|
detail?: {
|
|
7699
7731
|
[x: string]: unknown;
|
|
@@ -8730,69 +8762,6 @@ declare const securityPluginManifestHeader: {
|
|
|
8730
8762
|
description: string;
|
|
8731
8763
|
};
|
|
8732
8764
|
|
|
8733
|
-
/**
|
|
8734
|
-
* ensureUserHasOrganization — auto-create a personal org for new users.
|
|
8735
|
-
*
|
|
8736
|
-
* In multi-tenant mode, every record visible through the default
|
|
8737
|
-
* `tenant_isolation` RLS policy must have an `organization_id`, and
|
|
8738
|
-
* every authenticated user must have an `activeOrganizationId` on their
|
|
8739
|
-
* session for that policy to evaluate to anything other than "deny
|
|
8740
|
-
* all". A user with zero `sys_member` rows, however, can sign in
|
|
8741
|
-
* successfully and reach the dashboard — the dashboard's
|
|
8742
|
-
* `RequireOrganization` guard has a single-tenant carve-out that lets
|
|
8743
|
-
* users with empty organization lists through, so they land on a UI
|
|
8744
|
-
* that simply hides every record. The standard remedy ("invite users
|
|
8745
|
-
* via an admin") doesn't apply to self-service signup.
|
|
8746
|
-
*
|
|
8747
|
-
* This helper, run right after a `sys_user` insert, ensures the new
|
|
8748
|
-
* user has at least one organization by creating a personal workspace
|
|
8749
|
-
* (named "<User>'s Workspace", slug `<username>-workspace`) and an
|
|
8750
|
-
* owner-role `sys_member` row. The user's session will pick this up as
|
|
8751
|
-
* their `activeOrganizationId` on the next sign-in / org-list refresh
|
|
8752
|
-
* (better-auth's `setActiveOrganization` runs lazily when the picker
|
|
8753
|
-
* sees exactly one membership).
|
|
8754
|
-
*
|
|
8755
|
-
* Idempotent: bails out if the user already has any `sys_member` row.
|
|
8756
|
-
* Slug collisions retry with a numeric suffix; a cap of 5 attempts
|
|
8757
|
-
* means a pathological username will fail loudly rather than loop.
|
|
8758
|
-
*/
|
|
8759
|
-
interface EnsureOptions {
|
|
8760
|
-
logger?: {
|
|
8761
|
-
info: (message: string, meta?: Record<string, any>) => void;
|
|
8762
|
-
warn: (message: string, meta?: Record<string, any>) => void;
|
|
8763
|
-
};
|
|
8764
|
-
/**
|
|
8765
|
-
* Optional hook called after a personal org is successfully created.
|
|
8766
|
-
* Used by SecurityPlugin to wire in `cloneTenantSeedData` so each
|
|
8767
|
-
* new workspace gets its own copy of demo data. Pulled in via DI
|
|
8768
|
-
* to keep this helper free of a hard import on the cloner (which
|
|
8769
|
-
* keeps the tenant-claim and ensure-org test surfaces narrow).
|
|
8770
|
-
*/
|
|
8771
|
-
cloneSeedData?: (ql: any, targetOrgId: string, opts: {
|
|
8772
|
-
logger?: EnsureOptions['logger'];
|
|
8773
|
-
}) => Promise<{
|
|
8774
|
-
object: string;
|
|
8775
|
-
count: number;
|
|
8776
|
-
}[]>;
|
|
8777
|
-
}
|
|
8778
|
-
/**
|
|
8779
|
-
* Ensure `user` has at least one `sys_member` row. Creates a personal
|
|
8780
|
-
* organization owned by them if not.
|
|
8781
|
-
*
|
|
8782
|
-
* Returns `{ created: true, organizationId }` when a new org was made,
|
|
8783
|
-
* or `{ created: false, reason }` when the user already has memberships
|
|
8784
|
-
* or the operation was skipped.
|
|
8785
|
-
*/
|
|
8786
|
-
declare function ensureUserHasOrganization(ql: any, user: {
|
|
8787
|
-
id: string;
|
|
8788
|
-
name?: string;
|
|
8789
|
-
email?: string;
|
|
8790
|
-
}, options?: EnsureOptions): Promise<{
|
|
8791
|
-
created: boolean;
|
|
8792
|
-
organizationId?: string;
|
|
8793
|
-
reason?: string;
|
|
8794
|
-
}>;
|
|
8795
|
-
|
|
8796
8765
|
interface CloneOptions {
|
|
8797
8766
|
logger?: {
|
|
8798
8767
|
info: (message: string, meta?: Record<string, any>) => void;
|
|
@@ -8804,4 +8773,4 @@ declare function cloneTenantSeedData(ql: any, targetOrgId: string, options?: Clo
|
|
|
8804
8773
|
count: number;
|
|
8805
8774
|
}[]>;
|
|
8806
8775
|
|
|
8807
|
-
export { FieldMasker, PermissionDeniedError, PermissionEvaluator, RLSCompiler, RLS_DENY_FILTER, SECURITY_PLUGIN_ID, SECURITY_PLUGIN_VERSION, SecurityPlugin, cloneTenantSeedData,
|
|
8776
|
+
export { FieldMasker, PermissionDeniedError, PermissionEvaluator, RLSCompiler, RLS_DENY_FILTER, SECURITY_PLUGIN_ID, SECURITY_PLUGIN_VERSION, SecurityPlugin, cloneTenantSeedData, isPermissionDeniedError, securityDefaultPermissionSets, securityObjects, securityPluginManifestHeader };
|
package/dist/index.js
CHANGED
|
@@ -29,7 +29,6 @@ __export(index_exports, {
|
|
|
29
29
|
SECURITY_PLUGIN_VERSION: () => SECURITY_PLUGIN_VERSION,
|
|
30
30
|
SecurityPlugin: () => SecurityPlugin,
|
|
31
31
|
cloneTenantSeedData: () => cloneTenantSeedData,
|
|
32
|
-
ensureUserHasOrganization: () => ensureUserHasOrganization,
|
|
33
32
|
isPermissionDeniedError: () => isPermissionDeniedError,
|
|
34
33
|
securityDefaultPermissionSets: () => securityDefaultPermissionSets,
|
|
35
34
|
securityObjects: () => securityObjects,
|
|
@@ -378,6 +377,7 @@ function genId(prefix) {
|
|
|
378
377
|
}
|
|
379
378
|
async function bootstrapPlatformAdmin(ql, bootstrapPermissionSets, options = {}) {
|
|
380
379
|
const logger = options.logger;
|
|
380
|
+
const multiTenant = options.multiTenant === true;
|
|
381
381
|
if (!ql || typeof ql.find !== "function" || typeof ql.insert !== "function") {
|
|
382
382
|
return { seeded: 0, adminPromoted: false, reason: "objectql_unavailable" };
|
|
383
383
|
}
|
|
@@ -439,7 +439,48 @@ async function bootstrapPlatformAdmin(ql, bootstrapPermissionSets, options = {})
|
|
|
439
439
|
return { seeded: seededCount, adminPromoted: false, reason: "insert_failed" };
|
|
440
440
|
}
|
|
441
441
|
logger?.info?.(`[security] first user promoted to platform admin: ${target.email ?? target.id}`);
|
|
442
|
-
|
|
442
|
+
let defaultOrgCreated = false;
|
|
443
|
+
let defaultOrgId;
|
|
444
|
+
if (multiTenant) {
|
|
445
|
+
const memberships = await tryFind(ql, "sys_member", { user_id: target.id }, 1);
|
|
446
|
+
if (memberships.length === 0) {
|
|
447
|
+
const existingDefault = await tryFind(ql, "sys_organization", { slug: "default" }, 1);
|
|
448
|
+
if (existingDefault.length > 0 && existingDefault[0].id) {
|
|
449
|
+
defaultOrgId = String(existingDefault[0].id);
|
|
450
|
+
} else {
|
|
451
|
+
const newOrgId = genId("org");
|
|
452
|
+
const orgRow = await tryInsert(ql, "sys_organization", {
|
|
453
|
+
id: newOrgId,
|
|
454
|
+
name: "Default Organization",
|
|
455
|
+
slug: "default",
|
|
456
|
+
logo: null,
|
|
457
|
+
metadata: null
|
|
458
|
+
});
|
|
459
|
+
if (orgRow) {
|
|
460
|
+
defaultOrgId = orgRow?.id ?? newOrgId;
|
|
461
|
+
defaultOrgCreated = true;
|
|
462
|
+
} else {
|
|
463
|
+
logger?.warn?.("[security] failed to create default organization for platform admin");
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
if (defaultOrgId) {
|
|
467
|
+
const memRow = await tryInsert(ql, "sys_member", {
|
|
468
|
+
id: genId("mem"),
|
|
469
|
+
organization_id: defaultOrgId,
|
|
470
|
+
user_id: target.id,
|
|
471
|
+
role: "owner"
|
|
472
|
+
});
|
|
473
|
+
if (memRow) {
|
|
474
|
+
logger?.info?.(
|
|
475
|
+
`[security] bound platform admin to default organization (${defaultOrgId}): ${target.email ?? target.id}`
|
|
476
|
+
);
|
|
477
|
+
} else {
|
|
478
|
+
logger?.warn?.("[security] failed to bind platform admin to default organization");
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
return { seeded: seededCount, adminPromoted: true, defaultOrgCreated, defaultOrgId };
|
|
443
484
|
}
|
|
444
485
|
|
|
445
486
|
// src/claim-orphan-tenant-rows.ts
|
|
@@ -688,126 +729,6 @@ async function cloneTenantSeedData(ql, targetOrgId, options = {}) {
|
|
|
688
729
|
return summary;
|
|
689
730
|
}
|
|
690
731
|
|
|
691
|
-
// src/ensure-user-has-organization.ts
|
|
692
|
-
var SYSTEM_CTX4 = { isSystem: true };
|
|
693
|
-
function genId2(prefix) {
|
|
694
|
-
const rand = Math.random().toString(36).slice(2, 10);
|
|
695
|
-
const ts = Date.now().toString(36);
|
|
696
|
-
return `${prefix}_${ts}${rand}`;
|
|
697
|
-
}
|
|
698
|
-
function slugify(input, fallback = "workspace") {
|
|
699
|
-
const cleaned = input.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40);
|
|
700
|
-
return cleaned || fallback;
|
|
701
|
-
}
|
|
702
|
-
function deriveSlugFallback(user) {
|
|
703
|
-
if (user.email) {
|
|
704
|
-
const local = user.email.split("@")[0] ?? "";
|
|
705
|
-
const localSlug = local.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40);
|
|
706
|
-
if (localSlug) return localSlug;
|
|
707
|
-
}
|
|
708
|
-
const idTail = user.id.replace(/[^a-z0-9]/gi, "").slice(-8).toLowerCase();
|
|
709
|
-
return idTail ? `user-${idTail}` : "user";
|
|
710
|
-
}
|
|
711
|
-
function deriveBaseName(user) {
|
|
712
|
-
if (user.name && user.name.trim()) return user.name.trim();
|
|
713
|
-
if (user.email) {
|
|
714
|
-
const local = user.email.split("@")[0];
|
|
715
|
-
if (local) return local;
|
|
716
|
-
}
|
|
717
|
-
return user.id;
|
|
718
|
-
}
|
|
719
|
-
async function tryFind2(ql, object, where, limit = 1) {
|
|
720
|
-
try {
|
|
721
|
-
const rows = await ql.find(object, { where, limit }, { context: SYSTEM_CTX4 });
|
|
722
|
-
return Array.isArray(rows) ? rows : [];
|
|
723
|
-
} catch {
|
|
724
|
-
return [];
|
|
725
|
-
}
|
|
726
|
-
}
|
|
727
|
-
async function ensureUserHasOrganization(ql, user, options = {}) {
|
|
728
|
-
const logger = options.logger;
|
|
729
|
-
const cloneSeedData = options.cloneSeedData;
|
|
730
|
-
if (!ql || typeof ql.find !== "function" || typeof ql.insert !== "function") {
|
|
731
|
-
return { created: false, reason: "objectql_unavailable" };
|
|
732
|
-
}
|
|
733
|
-
if (!user?.id) return { created: false, reason: "invalid_user" };
|
|
734
|
-
const existing = await tryFind2(ql, "sys_member", { user_id: user.id }, 1);
|
|
735
|
-
if (existing.length > 0) {
|
|
736
|
-
return { created: false, reason: "already_member" };
|
|
737
|
-
}
|
|
738
|
-
const base = deriveBaseName(user);
|
|
739
|
-
const orgName = `${base}'s Workspace`;
|
|
740
|
-
const slugFallback = deriveSlugFallback(user);
|
|
741
|
-
const baseSlug = slugify(base, slugFallback);
|
|
742
|
-
let slug = `${baseSlug}-workspace`;
|
|
743
|
-
for (let attempt = 1; attempt <= 5; attempt += 1) {
|
|
744
|
-
const collision = await tryFind2(ql, "sys_organization", { slug }, 1);
|
|
745
|
-
if (collision.length === 0) break;
|
|
746
|
-
slug = `${baseSlug}-workspace-${attempt + 1}`;
|
|
747
|
-
if (attempt === 5) {
|
|
748
|
-
logger?.warn?.(
|
|
749
|
-
`[security] could not find a free slug for personal org of ${user.email ?? user.id}`
|
|
750
|
-
);
|
|
751
|
-
return { created: false, reason: "slug_exhausted" };
|
|
752
|
-
}
|
|
753
|
-
}
|
|
754
|
-
const orgId = genId2("org");
|
|
755
|
-
let orgRow = null;
|
|
756
|
-
try {
|
|
757
|
-
orgRow = await ql.insert(
|
|
758
|
-
"sys_organization",
|
|
759
|
-
{ id: orgId, name: orgName, slug, logo: null, metadata: null },
|
|
760
|
-
{ context: SYSTEM_CTX4 }
|
|
761
|
-
);
|
|
762
|
-
} catch (e) {
|
|
763
|
-
logger?.warn?.(`[security] failed to create personal org for ${user.email ?? user.id}`, {
|
|
764
|
-
error: e.message
|
|
765
|
-
});
|
|
766
|
-
return { created: false, reason: "org_insert_failed" };
|
|
767
|
-
}
|
|
768
|
-
const finalOrgId = orgRow?.id ?? orgId;
|
|
769
|
-
try {
|
|
770
|
-
await ql.insert(
|
|
771
|
-
"sys_member",
|
|
772
|
-
{
|
|
773
|
-
id: genId2("mem"),
|
|
774
|
-
organization_id: finalOrgId,
|
|
775
|
-
user_id: user.id,
|
|
776
|
-
role: "owner"
|
|
777
|
-
},
|
|
778
|
-
{ context: SYSTEM_CTX4 }
|
|
779
|
-
);
|
|
780
|
-
} catch (e) {
|
|
781
|
-
logger?.warn?.(`[security] failed to create owner-member row for ${user.email ?? user.id}`, {
|
|
782
|
-
error: e.message
|
|
783
|
-
});
|
|
784
|
-
return { created: false, reason: "member_insert_failed", organizationId: finalOrgId };
|
|
785
|
-
}
|
|
786
|
-
logger?.info?.(
|
|
787
|
-
`[security] created personal organization "${orgName}" (${finalOrgId}) for ${user.email ?? user.id}`
|
|
788
|
-
);
|
|
789
|
-
if (cloneSeedData) {
|
|
790
|
-
void cloneSeedData(ql, finalOrgId, { logger }).then(
|
|
791
|
-
(summary) => {
|
|
792
|
-
if (summary.length > 0) {
|
|
793
|
-
const total = summary.reduce((s, c) => s + c.count, 0);
|
|
794
|
-
logger?.info?.(
|
|
795
|
-
`[security] cloned ${total} seed row(s) into personal organization ${finalOrgId}`,
|
|
796
|
-
{ breakdown: summary }
|
|
797
|
-
);
|
|
798
|
-
}
|
|
799
|
-
},
|
|
800
|
-
(e) => {
|
|
801
|
-
logger?.warn?.("[security] cloneTenantSeedData failed", {
|
|
802
|
-
organizationId: finalOrgId,
|
|
803
|
-
error: e.message
|
|
804
|
-
});
|
|
805
|
-
}
|
|
806
|
-
);
|
|
807
|
-
}
|
|
808
|
-
return { created: true, organizationId: finalOrgId };
|
|
809
|
-
}
|
|
810
|
-
|
|
811
732
|
// src/manifest.ts
|
|
812
733
|
var import_security = require("@objectstack/platform-objects/security");
|
|
813
734
|
var SECURITY_PLUGIN_ID = "com.objectstack.plugin-security";
|
|
@@ -1042,7 +963,8 @@ var SecurityPlugin = class {
|
|
|
1042
963
|
const runBootstrap = async () => {
|
|
1043
964
|
try {
|
|
1044
965
|
const report = await bootstrapPlatformAdmin(ql, this.bootstrapPermissionSets, {
|
|
1045
|
-
logger: ctx.logger
|
|
966
|
+
logger: ctx.logger,
|
|
967
|
+
multiTenant: this.multiTenant
|
|
1046
968
|
});
|
|
1047
969
|
bootstrapRanOnce = true;
|
|
1048
970
|
ctx.logger.info("[security] platform bootstrap complete", report);
|
|
@@ -1063,21 +985,6 @@ var SecurityPlugin = class {
|
|
|
1063
985
|
if (bootstrapRanOnce) {
|
|
1064
986
|
await runBootstrap();
|
|
1065
987
|
}
|
|
1066
|
-
if (this.multiTenant) {
|
|
1067
|
-
const newUser = opCtx?.result ?? opCtx?.data;
|
|
1068
|
-
if (newUser?.id) {
|
|
1069
|
-
try {
|
|
1070
|
-
await ensureUserHasOrganization(ql, newUser, {
|
|
1071
|
-
logger: ctx.logger,
|
|
1072
|
-
cloneSeedData: cloneTenantSeedData
|
|
1073
|
-
});
|
|
1074
|
-
} catch (e) {
|
|
1075
|
-
ctx.logger.warn("[security] ensure-user-has-organization failed", {
|
|
1076
|
-
error: e.message
|
|
1077
|
-
});
|
|
1078
|
-
}
|
|
1079
|
-
}
|
|
1080
|
-
}
|
|
1081
988
|
}
|
|
1082
989
|
});
|
|
1083
990
|
if (this.multiTenant) {
|
|
@@ -1255,7 +1162,6 @@ var SecurityPlugin = class {
|
|
|
1255
1162
|
SECURITY_PLUGIN_VERSION,
|
|
1256
1163
|
SecurityPlugin,
|
|
1257
1164
|
cloneTenantSeedData,
|
|
1258
|
-
ensureUserHasOrganization,
|
|
1259
1165
|
isPermissionDeniedError,
|
|
1260
1166
|
securityDefaultPermissionSets,
|
|
1261
1167
|
securityObjects,
|