@objectstack/plugin-security 6.8.0 → 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/README.md +2 -3
- 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 +5 -5
package/dist/index.mjs
CHANGED
|
@@ -339,6 +339,7 @@ function genId(prefix) {
|
|
|
339
339
|
}
|
|
340
340
|
async function bootstrapPlatformAdmin(ql, bootstrapPermissionSets, options = {}) {
|
|
341
341
|
const logger = options.logger;
|
|
342
|
+
const multiTenant = options.multiTenant === true;
|
|
342
343
|
if (!ql || typeof ql.find !== "function" || typeof ql.insert !== "function") {
|
|
343
344
|
return { seeded: 0, adminPromoted: false, reason: "objectql_unavailable" };
|
|
344
345
|
}
|
|
@@ -400,7 +401,48 @@ async function bootstrapPlatformAdmin(ql, bootstrapPermissionSets, options = {})
|
|
|
400
401
|
return { seeded: seededCount, adminPromoted: false, reason: "insert_failed" };
|
|
401
402
|
}
|
|
402
403
|
logger?.info?.(`[security] first user promoted to platform admin: ${target.email ?? target.id}`);
|
|
403
|
-
|
|
404
|
+
let defaultOrgCreated = false;
|
|
405
|
+
let defaultOrgId;
|
|
406
|
+
if (multiTenant) {
|
|
407
|
+
const memberships = await tryFind(ql, "sys_member", { user_id: target.id }, 1);
|
|
408
|
+
if (memberships.length === 0) {
|
|
409
|
+
const existingDefault = await tryFind(ql, "sys_organization", { slug: "default" }, 1);
|
|
410
|
+
if (existingDefault.length > 0 && existingDefault[0].id) {
|
|
411
|
+
defaultOrgId = String(existingDefault[0].id);
|
|
412
|
+
} else {
|
|
413
|
+
const newOrgId = genId("org");
|
|
414
|
+
const orgRow = await tryInsert(ql, "sys_organization", {
|
|
415
|
+
id: newOrgId,
|
|
416
|
+
name: "Default Organization",
|
|
417
|
+
slug: "default",
|
|
418
|
+
logo: null,
|
|
419
|
+
metadata: null
|
|
420
|
+
});
|
|
421
|
+
if (orgRow) {
|
|
422
|
+
defaultOrgId = orgRow?.id ?? newOrgId;
|
|
423
|
+
defaultOrgCreated = true;
|
|
424
|
+
} else {
|
|
425
|
+
logger?.warn?.("[security] failed to create default organization for platform admin");
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
if (defaultOrgId) {
|
|
429
|
+
const memRow = await tryInsert(ql, "sys_member", {
|
|
430
|
+
id: genId("mem"),
|
|
431
|
+
organization_id: defaultOrgId,
|
|
432
|
+
user_id: target.id,
|
|
433
|
+
role: "owner"
|
|
434
|
+
});
|
|
435
|
+
if (memRow) {
|
|
436
|
+
logger?.info?.(
|
|
437
|
+
`[security] bound platform admin to default organization (${defaultOrgId}): ${target.email ?? target.id}`
|
|
438
|
+
);
|
|
439
|
+
} else {
|
|
440
|
+
logger?.warn?.("[security] failed to bind platform admin to default organization");
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
return { seeded: seededCount, adminPromoted: true, defaultOrgCreated, defaultOrgId };
|
|
404
446
|
}
|
|
405
447
|
|
|
406
448
|
// src/claim-orphan-tenant-rows.ts
|
|
@@ -649,126 +691,6 @@ async function cloneTenantSeedData(ql, targetOrgId, options = {}) {
|
|
|
649
691
|
return summary;
|
|
650
692
|
}
|
|
651
693
|
|
|
652
|
-
// src/ensure-user-has-organization.ts
|
|
653
|
-
var SYSTEM_CTX4 = { isSystem: true };
|
|
654
|
-
function genId2(prefix) {
|
|
655
|
-
const rand = Math.random().toString(36).slice(2, 10);
|
|
656
|
-
const ts = Date.now().toString(36);
|
|
657
|
-
return `${prefix}_${ts}${rand}`;
|
|
658
|
-
}
|
|
659
|
-
function slugify(input, fallback = "workspace") {
|
|
660
|
-
const cleaned = input.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40);
|
|
661
|
-
return cleaned || fallback;
|
|
662
|
-
}
|
|
663
|
-
function deriveSlugFallback(user) {
|
|
664
|
-
if (user.email) {
|
|
665
|
-
const local = user.email.split("@")[0] ?? "";
|
|
666
|
-
const localSlug = local.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40);
|
|
667
|
-
if (localSlug) return localSlug;
|
|
668
|
-
}
|
|
669
|
-
const idTail = user.id.replace(/[^a-z0-9]/gi, "").slice(-8).toLowerCase();
|
|
670
|
-
return idTail ? `user-${idTail}` : "user";
|
|
671
|
-
}
|
|
672
|
-
function deriveBaseName(user) {
|
|
673
|
-
if (user.name && user.name.trim()) return user.name.trim();
|
|
674
|
-
if (user.email) {
|
|
675
|
-
const local = user.email.split("@")[0];
|
|
676
|
-
if (local) return local;
|
|
677
|
-
}
|
|
678
|
-
return user.id;
|
|
679
|
-
}
|
|
680
|
-
async function tryFind2(ql, object, where, limit = 1) {
|
|
681
|
-
try {
|
|
682
|
-
const rows = await ql.find(object, { where, limit }, { context: SYSTEM_CTX4 });
|
|
683
|
-
return Array.isArray(rows) ? rows : [];
|
|
684
|
-
} catch {
|
|
685
|
-
return [];
|
|
686
|
-
}
|
|
687
|
-
}
|
|
688
|
-
async function ensureUserHasOrganization(ql, user, options = {}) {
|
|
689
|
-
const logger = options.logger;
|
|
690
|
-
const cloneSeedData = options.cloneSeedData;
|
|
691
|
-
if (!ql || typeof ql.find !== "function" || typeof ql.insert !== "function") {
|
|
692
|
-
return { created: false, reason: "objectql_unavailable" };
|
|
693
|
-
}
|
|
694
|
-
if (!user?.id) return { created: false, reason: "invalid_user" };
|
|
695
|
-
const existing = await tryFind2(ql, "sys_member", { user_id: user.id }, 1);
|
|
696
|
-
if (existing.length > 0) {
|
|
697
|
-
return { created: false, reason: "already_member" };
|
|
698
|
-
}
|
|
699
|
-
const base = deriveBaseName(user);
|
|
700
|
-
const orgName = `${base}'s Workspace`;
|
|
701
|
-
const slugFallback = deriveSlugFallback(user);
|
|
702
|
-
const baseSlug = slugify(base, slugFallback);
|
|
703
|
-
let slug = `${baseSlug}-workspace`;
|
|
704
|
-
for (let attempt = 1; attempt <= 5; attempt += 1) {
|
|
705
|
-
const collision = await tryFind2(ql, "sys_organization", { slug }, 1);
|
|
706
|
-
if (collision.length === 0) break;
|
|
707
|
-
slug = `${baseSlug}-workspace-${attempt + 1}`;
|
|
708
|
-
if (attempt === 5) {
|
|
709
|
-
logger?.warn?.(
|
|
710
|
-
`[security] could not find a free slug for personal org of ${user.email ?? user.id}`
|
|
711
|
-
);
|
|
712
|
-
return { created: false, reason: "slug_exhausted" };
|
|
713
|
-
}
|
|
714
|
-
}
|
|
715
|
-
const orgId = genId2("org");
|
|
716
|
-
let orgRow = null;
|
|
717
|
-
try {
|
|
718
|
-
orgRow = await ql.insert(
|
|
719
|
-
"sys_organization",
|
|
720
|
-
{ id: orgId, name: orgName, slug, logo: null, metadata: null },
|
|
721
|
-
{ context: SYSTEM_CTX4 }
|
|
722
|
-
);
|
|
723
|
-
} catch (e) {
|
|
724
|
-
logger?.warn?.(`[security] failed to create personal org for ${user.email ?? user.id}`, {
|
|
725
|
-
error: e.message
|
|
726
|
-
});
|
|
727
|
-
return { created: false, reason: "org_insert_failed" };
|
|
728
|
-
}
|
|
729
|
-
const finalOrgId = orgRow?.id ?? orgId;
|
|
730
|
-
try {
|
|
731
|
-
await ql.insert(
|
|
732
|
-
"sys_member",
|
|
733
|
-
{
|
|
734
|
-
id: genId2("mem"),
|
|
735
|
-
organization_id: finalOrgId,
|
|
736
|
-
user_id: user.id,
|
|
737
|
-
role: "owner"
|
|
738
|
-
},
|
|
739
|
-
{ context: SYSTEM_CTX4 }
|
|
740
|
-
);
|
|
741
|
-
} catch (e) {
|
|
742
|
-
logger?.warn?.(`[security] failed to create owner-member row for ${user.email ?? user.id}`, {
|
|
743
|
-
error: e.message
|
|
744
|
-
});
|
|
745
|
-
return { created: false, reason: "member_insert_failed", organizationId: finalOrgId };
|
|
746
|
-
}
|
|
747
|
-
logger?.info?.(
|
|
748
|
-
`[security] created personal organization "${orgName}" (${finalOrgId}) for ${user.email ?? user.id}`
|
|
749
|
-
);
|
|
750
|
-
if (cloneSeedData) {
|
|
751
|
-
void cloneSeedData(ql, finalOrgId, { logger }).then(
|
|
752
|
-
(summary) => {
|
|
753
|
-
if (summary.length > 0) {
|
|
754
|
-
const total = summary.reduce((s, c) => s + c.count, 0);
|
|
755
|
-
logger?.info?.(
|
|
756
|
-
`[security] cloned ${total} seed row(s) into personal organization ${finalOrgId}`,
|
|
757
|
-
{ breakdown: summary }
|
|
758
|
-
);
|
|
759
|
-
}
|
|
760
|
-
},
|
|
761
|
-
(e) => {
|
|
762
|
-
logger?.warn?.("[security] cloneTenantSeedData failed", {
|
|
763
|
-
organizationId: finalOrgId,
|
|
764
|
-
error: e.message
|
|
765
|
-
});
|
|
766
|
-
}
|
|
767
|
-
);
|
|
768
|
-
}
|
|
769
|
-
return { created: true, organizationId: finalOrgId };
|
|
770
|
-
}
|
|
771
|
-
|
|
772
694
|
// src/manifest.ts
|
|
773
695
|
import {
|
|
774
696
|
SysPermissionSet,
|
|
@@ -1009,7 +931,8 @@ var SecurityPlugin = class {
|
|
|
1009
931
|
const runBootstrap = async () => {
|
|
1010
932
|
try {
|
|
1011
933
|
const report = await bootstrapPlatformAdmin(ql, this.bootstrapPermissionSets, {
|
|
1012
|
-
logger: ctx.logger
|
|
934
|
+
logger: ctx.logger,
|
|
935
|
+
multiTenant: this.multiTenant
|
|
1013
936
|
});
|
|
1014
937
|
bootstrapRanOnce = true;
|
|
1015
938
|
ctx.logger.info("[security] platform bootstrap complete", report);
|
|
@@ -1030,21 +953,6 @@ var SecurityPlugin = class {
|
|
|
1030
953
|
if (bootstrapRanOnce) {
|
|
1031
954
|
await runBootstrap();
|
|
1032
955
|
}
|
|
1033
|
-
if (this.multiTenant) {
|
|
1034
|
-
const newUser = opCtx?.result ?? opCtx?.data;
|
|
1035
|
-
if (newUser?.id) {
|
|
1036
|
-
try {
|
|
1037
|
-
await ensureUserHasOrganization(ql, newUser, {
|
|
1038
|
-
logger: ctx.logger,
|
|
1039
|
-
cloneSeedData: cloneTenantSeedData
|
|
1040
|
-
});
|
|
1041
|
-
} catch (e) {
|
|
1042
|
-
ctx.logger.warn("[security] ensure-user-has-organization failed", {
|
|
1043
|
-
error: e.message
|
|
1044
|
-
});
|
|
1045
|
-
}
|
|
1046
|
-
}
|
|
1047
|
-
}
|
|
1048
956
|
}
|
|
1049
957
|
});
|
|
1050
958
|
if (this.multiTenant) {
|
|
@@ -1221,7 +1129,6 @@ export {
|
|
|
1221
1129
|
SECURITY_PLUGIN_VERSION,
|
|
1222
1130
|
SecurityPlugin,
|
|
1223
1131
|
cloneTenantSeedData,
|
|
1224
|
-
ensureUserHasOrganization,
|
|
1225
1132
|
isPermissionDeniedError,
|
|
1226
1133
|
securityDefaultPermissionSets,
|
|
1227
1134
|
securityObjects,
|