@litusarchieve18/agricore-utils 1.0.3 → 1.0.5

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 CHANGED
@@ -14,7 +14,7 @@ npm install @litusarchieve18/agricore-utils
14
14
 
15
15
  For Base44 / Deno Environments
16
16
  You do not need a .npmrc or GITHUB_TOKEN to use this library in Base44. Import it directly using the npm: protocol:
17
- import { AgriLogger, AppError, MOD } from 'npm:@litusarchieve18/agricore-utils@^1.0.2';
17
+ import { AgriLogger, AppError, MOD } from 'npm:@litusarchieve18/agricore-utils@^1.0.4';
18
18
 
19
19
  🏗️ Core Architecture & Features
20
20
  This library is designed with the "Adapter Pattern" in mind. It defines strict contracts (AgriCoreSession) so that your business logic remains completely isolated from the underlying Base44 database infrastructure.
@@ -23,7 +23,7 @@ This library is designed with the "Adapter Pattern" in mind. It defines strict c
23
23
  The universal contract for authenticated users. The v2 upgrade includes injected UI-ready navigation data (availableScopes) and tenant configurations to prevent unnecessary frontend API calls.
24
24
 
25
25
  TypeScript
26
- import { AgriCoreSession } from 'npm:@litusarchieve18/agricore-utils@^1.0.2';
26
+ import { AgriCoreSession } from 'npm:@litusarchieve18/agricore-utils@^1.0.4';
27
27
 
28
28
  // Example of the v2 structure:
29
29
  const session: AgriCoreSession = {
@@ -62,6 +62,34 @@ ErrorFactory
62
62
  4. Domain Enums
63
63
  The universal vocabulary of the AgriCore platform, ensuring type-safety across all microservices (e.g., UserRole, UserPrivilege, AccessLevel, FacilityType, NotificationMode, TaskStatus, RESOURCE_PATHS).
64
64
 
65
+ 🔐 Security & Access Control
66
+ The library now includes a centralized logic layer to enforce granular permissions across all AgriCore services.
67
+
68
+ evaluateBaseAccess: Automatically determines a user's base permission level (NONE, READ, or WRITE) for a specific resource path (e.g., records.financials). It respects the OWNER role "God Mode" bypass and uses ROLE_SECTIONS_BY_KEY to provide sensible defaults for different app sections.
69
+
70
+ Access Ranking (resolveEffectiveAccess): Implements an additive permission model. By ranking levels (NONE < READ < WRITE), it allows the system to compare base permissions against specific user overrides and resolve the highest authorized access level.
71
+
72
+ 🛡️ Multi-Tenant Boundary Protection (Backend Only)
73
+ To ensure absolute data isolation in our multi-tenant environment, the library provides a "Boundary Guard."
74
+
75
+ assertTenantBoundary: A critical security function that prevents "Cross-Tenant" attacks. It validates that any target userId or authScopeId (Facility/Legal Entity) involved in a request belongs specifically to the requester's companyId. This serves as a final, mandatory check before any database write operations.
76
+
77
+ 🌳 Infrastructure & Hierarchy Resolution
78
+ AgriCore data records require a full "Infrastructure Path" (Company ID, Legal Entity ID, and Facility ID). Instead of manual lookups, the library automates tree-walking.
79
+
80
+ generateInfrastructureFields: This utility takes a user’s activeScope and their availableScopes tree. It recursively walks up the hierarchy—from a specific Facility to the Legal Entity and finally to the Root Company. It resolves all relevant infrastructure IDs and generates a new canonicalId, ensuring that every data record is correctly anchored in the organizational hierarchy.
81
+
82
+ // Example: Using the new utilities in a Base44 Save Hook
83
+ const infrastructure = generateInfrastructureFields(activeScope, session.availableScopes);
84
+
85
+ // Ensure the user isn't trying to save data into a company they don't belong to
86
+ await assertTenantBoundary(db, session.companyId, { authScopeId: infrastructure.authScopeId });
87
+
88
+ const recordToSave = {
89
+ ...userInput,
90
+ ...infrastructure, // Injects canonicalId, companyId, legalEntityId, etc.
91
+ };
92
+
65
93
  🛠️ Development & Publishing Guide
66
94
  Zero Dependencies
67
95
  To keep the package ultra-lightweight (~30KB), this library has zero runtime dependencies. All build tools (tsup, typescript) are strictly kept in devDependencies, and the package.json uses the "files": ["dist"] array to ensure only compiled code is uploaded.
@@ -89,7 +117,12 @@ Run the publish command:
89
117
  Bash
90
118
  npm publish --access public
91
119
  📝 Version History
120
+ v1.0.4 — refactor the code and edit readme
121
+
122
+ v1.0.4 — add generateInfrastructureFields, security access control and multi tenant boundary
123
+
92
124
  v1.0.3 — add the base44Id
125
+
93
126
  v1.0.2 — Breaking Change: Upgraded AgriCoreSession interface. Added companyConfig and UI-ready availableScopes arrays to support the frontend hierarchy dropdown without secondary API calls.
94
127
 
95
128
  v1.0.1 — Migrated package from private GitHub Registry to public NPM Registry for Base44/Deno compatibility. Removed baseUrl from tsconfig.json for NodeNext compatibility. Cleaned build artifacts from dependencies.
package/dist/index.d.mts CHANGED
@@ -67,6 +67,13 @@ interface TaskTypeConfig {
67
67
  icon: string;
68
68
  approveAction: string;
69
69
  }
70
+ interface InfrastructureFields {
71
+ canonicalId: string;
72
+ companyId: string;
73
+ legalEntityId: string | null;
74
+ facilityId: string | null;
75
+ authScopeId: string;
76
+ }
70
77
 
71
78
  declare const AgriLogger: {
72
79
  log(level: "INFO" | "WARN" | "ERROR", message: string, meta: any): void;
@@ -201,11 +208,11 @@ declare function generateCanonicalId(): string;
201
208
  * This matches the documented rule:
202
209
  * "Set to facilityId if available, else legalEntityId, else companyId."
203
210
  */
204
- declare function resolveAuthScopeId(fields?: AuthScopeFields): string;
205
- declare function withSaveMiddleware<T extends Record<string, any>>(data: T): T & {
206
- canonicalId: any;
207
- authScopeId: any;
208
- };
211
+ /**
212
+ * generateInfrastructureFields
213
+ * Standardizes the creation of new entities by walking up the scope tree.
214
+ */
215
+ declare function generateInfrastructureFields(activeScope: AvailableScope, availableScopes: AvailableScope[]): InfrastructureFields;
209
216
  declare function buildRlsFilter(session: AgriCoreSession, extra?: Record<string, any>): {
210
217
  authScopeId: {
211
218
  $in: string[];
@@ -233,6 +240,7 @@ declare const UserRole: {
233
240
  readonly LEADING_HAND: "leadingHand";
234
241
  readonly WORKER: "worker";
235
242
  };
243
+ type UserRoleType = typeof UserRole[keyof typeof UserRole];
236
244
  declare const UserPrivilege: {
237
245
  readonly FINANCE: "finance";
238
246
  readonly HR: "hr";
@@ -245,6 +253,7 @@ declare const AccessLevel: {
245
253
  readonly READ: "READ";
246
254
  readonly WRITE: "WRITE";
247
255
  };
256
+ type AccessLevelType = keyof typeof AccessLevel;
248
257
  declare const EnabledModule: {
249
258
  readonly FARM_MANAGEMENT: "farm_management";
250
259
  readonly PACKHOUSE: "packhouse";
@@ -478,5 +487,26 @@ declare const RESOURCE_PATHS: {
478
487
  teamStaff: string;
479
488
  teamTraining: string;
480
489
  };
490
+ declare const ACCESS_RANK: Record<AccessLevelType, number>;
491
+ declare const ROLE_SECTIONS_BY_KEY: Record<string, UserRoleType[]>;
492
+ declare function evaluateBaseAccess(resourcePath: string, session: {
493
+ role?: UserRoleType;
494
+ privileges?: string[];
495
+ enabledModules?: string[];
496
+ }, requirements?: {
497
+ module?: string;
498
+ allowedRoles?: UserRoleType[];
499
+ requiredPrivileges?: string[];
500
+ }): AccessLevelType;
501
+ /**
502
+ * Tenant Boundary Guard (Backend Only)
503
+ * Note: db is typed as 'any' here because Base44 SDK types vary,
504
+ * but you can refine this with your internal SDK types.
505
+ */
506
+ declare function assertTenantBoundary(db: any, requesterCompanyId: string, targets: {
507
+ targetUserId?: string;
508
+ authScopeId?: string;
509
+ }): Promise<boolean>;
510
+ declare function resolveEffectiveAccess(base: AccessLevelType, override?: AccessLevelType): AccessLevelType;
481
511
 
482
- export { ACT, AccessLevel, type AgriCoreSession, AgriLogger, AppError, ApprovalStatus, ApprovalType, AuditStatus, AuditType, type AuthScopeFields, type AvailableScope, CAT, type CompanyConfig, Converter, CropCycleStatus, ERROR_DICTIONARY, EmitterType, EnabledModule, ErrorFactory, FORM_REGISTRY, FacilityType, type FormMeta, HarvestMethod, LinkedStatus, MOD, MODULE_LABELS, MicroclimateZone, MimeType, type ModCode, NettingType, NotificationMode, OrderStatus, type PermissionLevel, PhysicalState, PlantingMethod, RESOURCE_PATHS, RelationshipType, type ResolvedError, type ResourceOverrides, RowOrientation, SENTINEL_UUID, type ScopeType, SoilTexture, TASK_TYPE_REGISTRY, type TabConfig, TargetEntityType, TaskStatus, TaskType, type TaskTypeConfig, TransactionType, TrellisType, UserPrivilege, UserRole, Validator, VigorRating, WaterSource, assertCanWrite, buildRlsFilter, canRead, canWrite, extractAppCode, generateCanonicalId, getAcceptedMimeTypes, getApproveAction, getFormMeta, getFormsGroupedByModule, getTaskTypeOptions, handleAppErrorResponse, isTabVisible, isTabWritable, resolveAccess, resolveAppError, resolveAuthScopeId, resolveError, resolveMenuVisibility, withAgriLogging, withSaveMiddleware };
512
+ export { ACCESS_RANK, ACT, AccessLevel, type AccessLevelType, type AgriCoreSession, AgriLogger, AppError, ApprovalStatus, ApprovalType, AuditStatus, AuditType, type AuthScopeFields, type AvailableScope, CAT, type CompanyConfig, Converter, CropCycleStatus, ERROR_DICTIONARY, EmitterType, EnabledModule, ErrorFactory, FORM_REGISTRY, FacilityType, type FormMeta, HarvestMethod, type InfrastructureFields, LinkedStatus, MOD, MODULE_LABELS, MicroclimateZone, MimeType, type ModCode, NettingType, NotificationMode, OrderStatus, type PermissionLevel, PhysicalState, PlantingMethod, RESOURCE_PATHS, ROLE_SECTIONS_BY_KEY, RelationshipType, type ResolvedError, type ResourceOverrides, RowOrientation, SENTINEL_UUID, type ScopeType, SoilTexture, TASK_TYPE_REGISTRY, type TabConfig, TargetEntityType, TaskStatus, TaskType, type TaskTypeConfig, TransactionType, TrellisType, UserPrivilege, UserRole, type UserRoleType, Validator, VigorRating, WaterSource, assertCanWrite, assertTenantBoundary, buildRlsFilter, canRead, canWrite, evaluateBaseAccess, extractAppCode, generateCanonicalId, generateInfrastructureFields, getAcceptedMimeTypes, getApproveAction, getFormMeta, getFormsGroupedByModule, getTaskTypeOptions, handleAppErrorResponse, isTabVisible, isTabWritable, resolveAccess, resolveAppError, resolveEffectiveAccess, resolveError, resolveMenuVisibility, withAgriLogging };
package/dist/index.d.ts CHANGED
@@ -67,6 +67,13 @@ interface TaskTypeConfig {
67
67
  icon: string;
68
68
  approveAction: string;
69
69
  }
70
+ interface InfrastructureFields {
71
+ canonicalId: string;
72
+ companyId: string;
73
+ legalEntityId: string | null;
74
+ facilityId: string | null;
75
+ authScopeId: string;
76
+ }
70
77
 
71
78
  declare const AgriLogger: {
72
79
  log(level: "INFO" | "WARN" | "ERROR", message: string, meta: any): void;
@@ -201,11 +208,11 @@ declare function generateCanonicalId(): string;
201
208
  * This matches the documented rule:
202
209
  * "Set to facilityId if available, else legalEntityId, else companyId."
203
210
  */
204
- declare function resolveAuthScopeId(fields?: AuthScopeFields): string;
205
- declare function withSaveMiddleware<T extends Record<string, any>>(data: T): T & {
206
- canonicalId: any;
207
- authScopeId: any;
208
- };
211
+ /**
212
+ * generateInfrastructureFields
213
+ * Standardizes the creation of new entities by walking up the scope tree.
214
+ */
215
+ declare function generateInfrastructureFields(activeScope: AvailableScope, availableScopes: AvailableScope[]): InfrastructureFields;
209
216
  declare function buildRlsFilter(session: AgriCoreSession, extra?: Record<string, any>): {
210
217
  authScopeId: {
211
218
  $in: string[];
@@ -233,6 +240,7 @@ declare const UserRole: {
233
240
  readonly LEADING_HAND: "leadingHand";
234
241
  readonly WORKER: "worker";
235
242
  };
243
+ type UserRoleType = typeof UserRole[keyof typeof UserRole];
236
244
  declare const UserPrivilege: {
237
245
  readonly FINANCE: "finance";
238
246
  readonly HR: "hr";
@@ -245,6 +253,7 @@ declare const AccessLevel: {
245
253
  readonly READ: "READ";
246
254
  readonly WRITE: "WRITE";
247
255
  };
256
+ type AccessLevelType = keyof typeof AccessLevel;
248
257
  declare const EnabledModule: {
249
258
  readonly FARM_MANAGEMENT: "farm_management";
250
259
  readonly PACKHOUSE: "packhouse";
@@ -478,5 +487,26 @@ declare const RESOURCE_PATHS: {
478
487
  teamStaff: string;
479
488
  teamTraining: string;
480
489
  };
490
+ declare const ACCESS_RANK: Record<AccessLevelType, number>;
491
+ declare const ROLE_SECTIONS_BY_KEY: Record<string, UserRoleType[]>;
492
+ declare function evaluateBaseAccess(resourcePath: string, session: {
493
+ role?: UserRoleType;
494
+ privileges?: string[];
495
+ enabledModules?: string[];
496
+ }, requirements?: {
497
+ module?: string;
498
+ allowedRoles?: UserRoleType[];
499
+ requiredPrivileges?: string[];
500
+ }): AccessLevelType;
501
+ /**
502
+ * Tenant Boundary Guard (Backend Only)
503
+ * Note: db is typed as 'any' here because Base44 SDK types vary,
504
+ * but you can refine this with your internal SDK types.
505
+ */
506
+ declare function assertTenantBoundary(db: any, requesterCompanyId: string, targets: {
507
+ targetUserId?: string;
508
+ authScopeId?: string;
509
+ }): Promise<boolean>;
510
+ declare function resolveEffectiveAccess(base: AccessLevelType, override?: AccessLevelType): AccessLevelType;
481
511
 
482
- export { ACT, AccessLevel, type AgriCoreSession, AgriLogger, AppError, ApprovalStatus, ApprovalType, AuditStatus, AuditType, type AuthScopeFields, type AvailableScope, CAT, type CompanyConfig, Converter, CropCycleStatus, ERROR_DICTIONARY, EmitterType, EnabledModule, ErrorFactory, FORM_REGISTRY, FacilityType, type FormMeta, HarvestMethod, LinkedStatus, MOD, MODULE_LABELS, MicroclimateZone, MimeType, type ModCode, NettingType, NotificationMode, OrderStatus, type PermissionLevel, PhysicalState, PlantingMethod, RESOURCE_PATHS, RelationshipType, type ResolvedError, type ResourceOverrides, RowOrientation, SENTINEL_UUID, type ScopeType, SoilTexture, TASK_TYPE_REGISTRY, type TabConfig, TargetEntityType, TaskStatus, TaskType, type TaskTypeConfig, TransactionType, TrellisType, UserPrivilege, UserRole, Validator, VigorRating, WaterSource, assertCanWrite, buildRlsFilter, canRead, canWrite, extractAppCode, generateCanonicalId, getAcceptedMimeTypes, getApproveAction, getFormMeta, getFormsGroupedByModule, getTaskTypeOptions, handleAppErrorResponse, isTabVisible, isTabWritable, resolveAccess, resolveAppError, resolveAuthScopeId, resolveError, resolveMenuVisibility, withAgriLogging, withSaveMiddleware };
512
+ export { ACCESS_RANK, ACT, AccessLevel, type AccessLevelType, type AgriCoreSession, AgriLogger, AppError, ApprovalStatus, ApprovalType, AuditStatus, AuditType, type AuthScopeFields, type AvailableScope, CAT, type CompanyConfig, Converter, CropCycleStatus, ERROR_DICTIONARY, EmitterType, EnabledModule, ErrorFactory, FORM_REGISTRY, FacilityType, type FormMeta, HarvestMethod, type InfrastructureFields, LinkedStatus, MOD, MODULE_LABELS, MicroclimateZone, MimeType, type ModCode, NettingType, NotificationMode, OrderStatus, type PermissionLevel, PhysicalState, PlantingMethod, RESOURCE_PATHS, ROLE_SECTIONS_BY_KEY, RelationshipType, type ResolvedError, type ResourceOverrides, RowOrientation, SENTINEL_UUID, type ScopeType, SoilTexture, TASK_TYPE_REGISTRY, type TabConfig, TargetEntityType, TaskStatus, TaskType, type TaskTypeConfig, TransactionType, TrellisType, UserPrivilege, UserRole, type UserRoleType, Validator, VigorRating, WaterSource, assertCanWrite, assertTenantBoundary, buildRlsFilter, canRead, canWrite, evaluateBaseAccess, extractAppCode, generateCanonicalId, generateInfrastructureFields, getAcceptedMimeTypes, getApproveAction, getFormMeta, getFormsGroupedByModule, getTaskTypeOptions, handleAppErrorResponse, isTabVisible, isTabWritable, resolveAccess, resolveAppError, resolveEffectiveAccess, resolveError, resolveMenuVisibility, withAgriLogging };
package/dist/index.js CHANGED
@@ -20,6 +20,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
+ ACCESS_RANK: () => ACCESS_RANK,
23
24
  ACT: () => ACT,
24
25
  AccessLevel: () => AccessLevel,
25
26
  AgriLogger: () => AgriLogger,
@@ -49,6 +50,7 @@ __export(index_exports, {
49
50
  PhysicalState: () => PhysicalState,
50
51
  PlantingMethod: () => PlantingMethod,
51
52
  RESOURCE_PATHS: () => RESOURCE_PATHS,
53
+ ROLE_SECTIONS_BY_KEY: () => ROLE_SECTIONS_BY_KEY,
52
54
  RelationshipType: () => RelationshipType,
53
55
  RowOrientation: () => RowOrientation,
54
56
  SENTINEL_UUID: () => SENTINEL_UUID,
@@ -65,11 +67,14 @@ __export(index_exports, {
65
67
  VigorRating: () => VigorRating,
66
68
  WaterSource: () => WaterSource,
67
69
  assertCanWrite: () => assertCanWrite,
70
+ assertTenantBoundary: () => assertTenantBoundary,
68
71
  buildRlsFilter: () => buildRlsFilter,
69
72
  canRead: () => canRead,
70
73
  canWrite: () => canWrite,
74
+ evaluateBaseAccess: () => evaluateBaseAccess,
71
75
  extractAppCode: () => extractAppCode,
72
76
  generateCanonicalId: () => generateCanonicalId,
77
+ generateInfrastructureFields: () => generateInfrastructureFields,
73
78
  getAcceptedMimeTypes: () => getAcceptedMimeTypes,
74
79
  getApproveAction: () => getApproveAction,
75
80
  getFormMeta: () => getFormMeta,
@@ -80,11 +85,10 @@ __export(index_exports, {
80
85
  isTabWritable: () => isTabWritable,
81
86
  resolveAccess: () => resolveAccess,
82
87
  resolveAppError: () => resolveAppError,
83
- resolveAuthScopeId: () => resolveAuthScopeId,
88
+ resolveEffectiveAccess: () => resolveEffectiveAccess,
84
89
  resolveError: () => resolveError,
85
90
  resolveMenuVisibility: () => resolveMenuVisibility,
86
- withAgriLogging: () => withAgriLogging,
87
- withSaveMiddleware: () => withSaveMiddleware
91
+ withAgriLogging: () => withAgriLogging
88
92
  });
89
93
  module.exports = __toCommonJS(index_exports);
90
94
 
@@ -423,15 +427,30 @@ function generateCanonicalId() {
423
427
  return (c === "x" ? r : r & 3 | 8).toString(16);
424
428
  });
425
429
  }
426
- function resolveAuthScopeId(fields = {}) {
427
- return fields.facilityId || fields.legalEntityId || fields.companyId || SENTINEL_UUID;
428
- }
429
- function withSaveMiddleware(data) {
430
- const canonicalId = data.canonicalId || generateCanonicalId();
430
+ function generateInfrastructureFields(activeScope, availableScopes) {
431
+ if (!activeScope || !availableScopes?.length) {
432
+ throw new Error("[generateInfrastructureFields] Missing scope context.");
433
+ }
434
+ let companyId = null;
435
+ let legalEntityId = null;
436
+ let facilityId = null;
437
+ let pointer = activeScope;
438
+ while (pointer) {
439
+ if (pointer.type === "company") companyId = pointer.base44Id;
440
+ if (pointer.type === "legalEntity") legalEntityId = pointer.base44Id;
441
+ if (pointer.type === "facility") facilityId = pointer.base44Id;
442
+ const parentId = pointer.parentId ?? null;
443
+ pointer = parentId ? availableScopes.find((s) => s.base44Id === parentId) || null : null;
444
+ }
445
+ if (!companyId) {
446
+ throw new Error("[generateInfrastructureFields] Could not resolve companyId from scope tree.");
447
+ }
431
448
  return {
432
- ...data,
433
- canonicalId,
434
- authScopeId: data.authScopeId || resolveAuthScopeId(data)
449
+ canonicalId: crypto.randomUUID(),
450
+ companyId,
451
+ legalEntityId,
452
+ facilityId,
453
+ authScopeId: activeScope.authScopeId
435
454
  };
436
455
  }
437
456
  function buildRlsFilter(session, extra = {}) {
@@ -823,8 +842,72 @@ var RESOURCE_PATHS = {
823
842
  teamStaff: "team.staff",
824
843
  teamTraining: "team.training"
825
844
  };
845
+ var ACCESS_RANK = {
846
+ NONE: 0,
847
+ READ: 1,
848
+ WRITE: 2
849
+ };
850
+ var ROLE_SECTIONS_BY_KEY = {
851
+ dashboard: [UserRole.OWNER, UserRole.MANAGER, UserRole.SUPERVISOR, UserRole.LEADING_HAND, UserRole.WORKER],
852
+ operations: [UserRole.OWNER, UserRole.MANAGER, UserRole.SUPERVISOR, UserRole.LEADING_HAND, UserRole.WORKER],
853
+ equipment: [UserRole.OWNER, UserRole.MANAGER, UserRole.SUPERVISOR, UserRole.LEADING_HAND, UserRole.WORKER],
854
+ safety: [UserRole.OWNER, UserRole.MANAGER, UserRole.SUPERVISOR],
855
+ team: [UserRole.OWNER, UserRole.MANAGER],
856
+ records: [UserRole.OWNER, UserRole.MANAGER],
857
+ adminSettings: []
858
+ };
859
+ function evaluateBaseAccess(resourcePath, session, requirements = {}) {
860
+ if (!session?.role) return "NONE";
861
+ if (session.role === UserRole.OWNER) return "WRITE";
862
+ const topLevel = resourcePath.split(".")[0];
863
+ const defaultRoles = ROLE_SECTIONS_BY_KEY[topLevel] || [];
864
+ if (requirements.module && !session.enabledModules?.includes(requirements.module)) return "NONE";
865
+ const allowedRoles = requirements.allowedRoles?.length ? requirements.allowedRoles : defaultRoles;
866
+ if (!allowedRoles.includes(session.role)) return "NONE";
867
+ if (requirements.requiredPrivileges?.length) {
868
+ const hasPrivilege = requirements.requiredPrivileges.some((p) => session.privileges?.includes(p));
869
+ if (!hasPrivilege) return "NONE";
870
+ }
871
+ const highRoles = [UserRole.OWNER, UserRole.MANAGER];
872
+ const isHighRole = highRoles.includes(session.role);
873
+ return isHighRole ? "WRITE" : "READ";
874
+ }
875
+ async function assertTenantBoundary(db, requesterCompanyId, targets) {
876
+ if (!requesterCompanyId) {
877
+ throw ErrorFactory.internal(MOD.ADMIN, "Missing requester company context for boundary check.");
878
+ }
879
+ if (targets.targetUserId) {
880
+ const targetUserAssignments = await db.UserRoleAssignment.filter({
881
+ userId: targets.targetUserId,
882
+ companyId: requesterCompanyId
883
+ });
884
+ if (targetUserAssignments.length === 0) {
885
+ console.error(`[SECURITY BREACH ATTEMPT] User tried to modify targetUserId ${targets.targetUserId} outside their company ${requesterCompanyId}`);
886
+ throw ErrorFactory.forbidden(MOD.ADMIN, ACT.EDIT, "Invalid target user or tenant boundary violation.");
887
+ }
888
+ }
889
+ if (targets.authScopeId) {
890
+ const [companyMatch, leMatch, facMatch] = await Promise.all([
891
+ db.Company.filter({ canonicalId: targets.authScopeId, id: requesterCompanyId }),
892
+ // Base44 id must match requester
893
+ db.LegalEntity.filter({ canonicalId: targets.authScopeId, companyId: requesterCompanyId }),
894
+ db.Facility.filter({ canonicalId: targets.authScopeId, companyId: requesterCompanyId })
895
+ ]);
896
+ const isValidScope = companyMatch.length > 0 || leMatch.length > 0 || facMatch.length > 0;
897
+ if (!isValidScope) {
898
+ console.error(`[SECURITY BREACH ATTEMPT] User tried to assign scope ${targets.authScopeId} outside company ${requesterCompanyId}`);
899
+ throw ErrorFactory.forbidden(MOD.ADMIN, ACT.EDIT, "Invalid authorization scope or tenant boundary violation.");
900
+ }
901
+ }
902
+ return true;
903
+ }
904
+ function resolveEffectiveAccess(base, override) {
905
+ if (!override) return base;
906
+ return ACCESS_RANK[override] >= ACCESS_RANK[base] ? override : base;
907
+ }
826
908
  // Annotate the CommonJS export names for ESM import in node:
827
909
  0 && (module.exports = {
910
+ ACCESS_RANK,
828
911
  ACT,
829
912
  AccessLevel,
830
913
  AgriLogger,
@@ -854,6 +937,7 @@ var RESOURCE_PATHS = {
854
937
  PhysicalState,
855
938
  PlantingMethod,
856
939
  RESOURCE_PATHS,
940
+ ROLE_SECTIONS_BY_KEY,
857
941
  RelationshipType,
858
942
  RowOrientation,
859
943
  SENTINEL_UUID,
@@ -870,11 +954,14 @@ var RESOURCE_PATHS = {
870
954
  VigorRating,
871
955
  WaterSource,
872
956
  assertCanWrite,
957
+ assertTenantBoundary,
873
958
  buildRlsFilter,
874
959
  canRead,
875
960
  canWrite,
961
+ evaluateBaseAccess,
876
962
  extractAppCode,
877
963
  generateCanonicalId,
964
+ generateInfrastructureFields,
878
965
  getAcceptedMimeTypes,
879
966
  getApproveAction,
880
967
  getFormMeta,
@@ -885,9 +972,8 @@ var RESOURCE_PATHS = {
885
972
  isTabWritable,
886
973
  resolveAccess,
887
974
  resolveAppError,
888
- resolveAuthScopeId,
975
+ resolveEffectiveAccess,
889
976
  resolveError,
890
977
  resolveMenuVisibility,
891
- withAgriLogging,
892
- withSaveMiddleware
978
+ withAgriLogging
893
979
  });
package/dist/index.mjs CHANGED
@@ -333,15 +333,30 @@ function generateCanonicalId() {
333
333
  return (c === "x" ? r : r & 3 | 8).toString(16);
334
334
  });
335
335
  }
336
- function resolveAuthScopeId(fields = {}) {
337
- return fields.facilityId || fields.legalEntityId || fields.companyId || SENTINEL_UUID;
338
- }
339
- function withSaveMiddleware(data) {
340
- const canonicalId = data.canonicalId || generateCanonicalId();
336
+ function generateInfrastructureFields(activeScope, availableScopes) {
337
+ if (!activeScope || !availableScopes?.length) {
338
+ throw new Error("[generateInfrastructureFields] Missing scope context.");
339
+ }
340
+ let companyId = null;
341
+ let legalEntityId = null;
342
+ let facilityId = null;
343
+ let pointer = activeScope;
344
+ while (pointer) {
345
+ if (pointer.type === "company") companyId = pointer.base44Id;
346
+ if (pointer.type === "legalEntity") legalEntityId = pointer.base44Id;
347
+ if (pointer.type === "facility") facilityId = pointer.base44Id;
348
+ const parentId = pointer.parentId ?? null;
349
+ pointer = parentId ? availableScopes.find((s) => s.base44Id === parentId) || null : null;
350
+ }
351
+ if (!companyId) {
352
+ throw new Error("[generateInfrastructureFields] Could not resolve companyId from scope tree.");
353
+ }
341
354
  return {
342
- ...data,
343
- canonicalId,
344
- authScopeId: data.authScopeId || resolveAuthScopeId(data)
355
+ canonicalId: crypto.randomUUID(),
356
+ companyId,
357
+ legalEntityId,
358
+ facilityId,
359
+ authScopeId: activeScope.authScopeId
345
360
  };
346
361
  }
347
362
  function buildRlsFilter(session, extra = {}) {
@@ -733,7 +748,71 @@ var RESOURCE_PATHS = {
733
748
  teamStaff: "team.staff",
734
749
  teamTraining: "team.training"
735
750
  };
751
+ var ACCESS_RANK = {
752
+ NONE: 0,
753
+ READ: 1,
754
+ WRITE: 2
755
+ };
756
+ var ROLE_SECTIONS_BY_KEY = {
757
+ dashboard: [UserRole.OWNER, UserRole.MANAGER, UserRole.SUPERVISOR, UserRole.LEADING_HAND, UserRole.WORKER],
758
+ operations: [UserRole.OWNER, UserRole.MANAGER, UserRole.SUPERVISOR, UserRole.LEADING_HAND, UserRole.WORKER],
759
+ equipment: [UserRole.OWNER, UserRole.MANAGER, UserRole.SUPERVISOR, UserRole.LEADING_HAND, UserRole.WORKER],
760
+ safety: [UserRole.OWNER, UserRole.MANAGER, UserRole.SUPERVISOR],
761
+ team: [UserRole.OWNER, UserRole.MANAGER],
762
+ records: [UserRole.OWNER, UserRole.MANAGER],
763
+ adminSettings: []
764
+ };
765
+ function evaluateBaseAccess(resourcePath, session, requirements = {}) {
766
+ if (!session?.role) return "NONE";
767
+ if (session.role === UserRole.OWNER) return "WRITE";
768
+ const topLevel = resourcePath.split(".")[0];
769
+ const defaultRoles = ROLE_SECTIONS_BY_KEY[topLevel] || [];
770
+ if (requirements.module && !session.enabledModules?.includes(requirements.module)) return "NONE";
771
+ const allowedRoles = requirements.allowedRoles?.length ? requirements.allowedRoles : defaultRoles;
772
+ if (!allowedRoles.includes(session.role)) return "NONE";
773
+ if (requirements.requiredPrivileges?.length) {
774
+ const hasPrivilege = requirements.requiredPrivileges.some((p) => session.privileges?.includes(p));
775
+ if (!hasPrivilege) return "NONE";
776
+ }
777
+ const highRoles = [UserRole.OWNER, UserRole.MANAGER];
778
+ const isHighRole = highRoles.includes(session.role);
779
+ return isHighRole ? "WRITE" : "READ";
780
+ }
781
+ async function assertTenantBoundary(db, requesterCompanyId, targets) {
782
+ if (!requesterCompanyId) {
783
+ throw ErrorFactory.internal(MOD.ADMIN, "Missing requester company context for boundary check.");
784
+ }
785
+ if (targets.targetUserId) {
786
+ const targetUserAssignments = await db.UserRoleAssignment.filter({
787
+ userId: targets.targetUserId,
788
+ companyId: requesterCompanyId
789
+ });
790
+ if (targetUserAssignments.length === 0) {
791
+ console.error(`[SECURITY BREACH ATTEMPT] User tried to modify targetUserId ${targets.targetUserId} outside their company ${requesterCompanyId}`);
792
+ throw ErrorFactory.forbidden(MOD.ADMIN, ACT.EDIT, "Invalid target user or tenant boundary violation.");
793
+ }
794
+ }
795
+ if (targets.authScopeId) {
796
+ const [companyMatch, leMatch, facMatch] = await Promise.all([
797
+ db.Company.filter({ canonicalId: targets.authScopeId, id: requesterCompanyId }),
798
+ // Base44 id must match requester
799
+ db.LegalEntity.filter({ canonicalId: targets.authScopeId, companyId: requesterCompanyId }),
800
+ db.Facility.filter({ canonicalId: targets.authScopeId, companyId: requesterCompanyId })
801
+ ]);
802
+ const isValidScope = companyMatch.length > 0 || leMatch.length > 0 || facMatch.length > 0;
803
+ if (!isValidScope) {
804
+ console.error(`[SECURITY BREACH ATTEMPT] User tried to assign scope ${targets.authScopeId} outside company ${requesterCompanyId}`);
805
+ throw ErrorFactory.forbidden(MOD.ADMIN, ACT.EDIT, "Invalid authorization scope or tenant boundary violation.");
806
+ }
807
+ }
808
+ return true;
809
+ }
810
+ function resolveEffectiveAccess(base, override) {
811
+ if (!override) return base;
812
+ return ACCESS_RANK[override] >= ACCESS_RANK[base] ? override : base;
813
+ }
736
814
  export {
815
+ ACCESS_RANK,
737
816
  ACT,
738
817
  AccessLevel,
739
818
  AgriLogger,
@@ -763,6 +842,7 @@ export {
763
842
  PhysicalState,
764
843
  PlantingMethod,
765
844
  RESOURCE_PATHS,
845
+ ROLE_SECTIONS_BY_KEY,
766
846
  RelationshipType,
767
847
  RowOrientation,
768
848
  SENTINEL_UUID,
@@ -779,11 +859,14 @@ export {
779
859
  VigorRating,
780
860
  WaterSource,
781
861
  assertCanWrite,
862
+ assertTenantBoundary,
782
863
  buildRlsFilter,
783
864
  canRead,
784
865
  canWrite,
866
+ evaluateBaseAccess,
785
867
  extractAppCode,
786
868
  generateCanonicalId,
869
+ generateInfrastructureFields,
787
870
  getAcceptedMimeTypes,
788
871
  getApproveAction,
789
872
  getFormMeta,
@@ -794,9 +877,8 @@ export {
794
877
  isTabWritable,
795
878
  resolveAccess,
796
879
  resolveAppError,
797
- resolveAuthScopeId,
880
+ resolveEffectiveAccess,
798
881
  resolveError,
799
882
  resolveMenuVisibility,
800
- withAgriLogging,
801
- withSaveMiddleware
883
+ withAgriLogging
802
884
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@litusarchieve18/agricore-utils",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "description": "Canonical Backend Utility Library for AgriCore",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",