@litusarchieve18/agricore-utils 1.0.3 → 1.0.4

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
@@ -62,6 +62,38 @@ 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
+ To document these powerful new security and architectural features in your README.md, you should add sections that explain why these tools exist. They move your library from being a "Type definition" to a "Security Framework."
66
+
67
+ Here are the suggested paragraphs to add to your README.md:
68
+
69
+ 🔐 Security & Access Control
70
+ The library now includes a centralized logic layer to enforce granular permissions across all AgriCore services.
71
+
72
+ 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.
73
+
74
+ 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.
75
+
76
+ 🛡️ Multi-Tenant Boundary Protection (Backend Only)
77
+ To ensure absolute data isolation in our multi-tenant environment, the library provides a "Boundary Guard."
78
+
79
+ 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.
80
+
81
+ 🌳 Infrastructure & Hierarchy Resolution
82
+ 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.
83
+
84
+ 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.
85
+
86
+ // Example: Using the new utilities in a Base44 Save Hook
87
+ const infrastructure = generateInfrastructureFields(activeScope, session.availableScopes);
88
+
89
+ // Ensure the user isn't trying to save data into a company they don't belong to
90
+ await assertTenantBoundary(db, session.companyId, { authScopeId: infrastructure.authScopeId });
91
+
92
+ const recordToSave = {
93
+ ...userInput,
94
+ ...infrastructure, // Injects canonicalId, companyId, legalEntityId, etc.
95
+ };
96
+
65
97
  🛠️ Development & Publishing Guide
66
98
  Zero Dependencies
67
99
  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 +121,9 @@ Run the publish command:
89
121
  Bash
90
122
  npm publish --access public
91
123
  📝 Version History
124
+ v1.0.4 — add generateInfrastructureFields, security access control and multi tenant boundary
92
125
  v1.0.3 — add the base44Id
126
+
93
127
  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
128
 
95
129
  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,62 @@ 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) throw new Error("Missing requester company context");
877
+ if (targets.targetUserId) {
878
+ const assignments = await db.UserRoleAssignment.filter({
879
+ userId: targets.targetUserId,
880
+ companyId: requesterCompanyId
881
+ });
882
+ if (assignments.length === 0) throw new Error("Tenant boundary violation: User");
883
+ }
884
+ if (targets.authScopeId) {
885
+ const [c, le, fac] = await Promise.all([
886
+ db.Company.filter({ canonicalId: targets.authScopeId, id: requesterCompanyId }),
887
+ db.LegalEntity.filter({ canonicalId: targets.authScopeId, companyId: requesterCompanyId }),
888
+ db.Facility.filter({ canonicalId: targets.authScopeId, companyId: requesterCompanyId })
889
+ ]);
890
+ if (!c.length && !le.length && !fac.length) throw new Error("Tenant boundary violation: Scope");
891
+ }
892
+ return true;
893
+ }
894
+ function resolveEffectiveAccess(base, override) {
895
+ if (!override) return base;
896
+ return ACCESS_RANK[override] >= ACCESS_RANK[base] ? override : base;
897
+ }
826
898
  // Annotate the CommonJS export names for ESM import in node:
827
899
  0 && (module.exports = {
900
+ ACCESS_RANK,
828
901
  ACT,
829
902
  AccessLevel,
830
903
  AgriLogger,
@@ -854,6 +927,7 @@ var RESOURCE_PATHS = {
854
927
  PhysicalState,
855
928
  PlantingMethod,
856
929
  RESOURCE_PATHS,
930
+ ROLE_SECTIONS_BY_KEY,
857
931
  RelationshipType,
858
932
  RowOrientation,
859
933
  SENTINEL_UUID,
@@ -870,11 +944,14 @@ var RESOURCE_PATHS = {
870
944
  VigorRating,
871
945
  WaterSource,
872
946
  assertCanWrite,
947
+ assertTenantBoundary,
873
948
  buildRlsFilter,
874
949
  canRead,
875
950
  canWrite,
951
+ evaluateBaseAccess,
876
952
  extractAppCode,
877
953
  generateCanonicalId,
954
+ generateInfrastructureFields,
878
955
  getAcceptedMimeTypes,
879
956
  getApproveAction,
880
957
  getFormMeta,
@@ -885,9 +962,8 @@ var RESOURCE_PATHS = {
885
962
  isTabWritable,
886
963
  resolveAccess,
887
964
  resolveAppError,
888
- resolveAuthScopeId,
965
+ resolveEffectiveAccess,
889
966
  resolveError,
890
967
  resolveMenuVisibility,
891
- withAgriLogging,
892
- withSaveMiddleware
968
+ withAgriLogging
893
969
  });
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,61 @@ 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) throw new Error("Missing requester company context");
783
+ if (targets.targetUserId) {
784
+ const assignments = await db.UserRoleAssignment.filter({
785
+ userId: targets.targetUserId,
786
+ companyId: requesterCompanyId
787
+ });
788
+ if (assignments.length === 0) throw new Error("Tenant boundary violation: User");
789
+ }
790
+ if (targets.authScopeId) {
791
+ const [c, le, fac] = await Promise.all([
792
+ db.Company.filter({ canonicalId: targets.authScopeId, id: requesterCompanyId }),
793
+ db.LegalEntity.filter({ canonicalId: targets.authScopeId, companyId: requesterCompanyId }),
794
+ db.Facility.filter({ canonicalId: targets.authScopeId, companyId: requesterCompanyId })
795
+ ]);
796
+ if (!c.length && !le.length && !fac.length) throw new Error("Tenant boundary violation: Scope");
797
+ }
798
+ return true;
799
+ }
800
+ function resolveEffectiveAccess(base, override) {
801
+ if (!override) return base;
802
+ return ACCESS_RANK[override] >= ACCESS_RANK[base] ? override : base;
803
+ }
736
804
  export {
805
+ ACCESS_RANK,
737
806
  ACT,
738
807
  AccessLevel,
739
808
  AgriLogger,
@@ -763,6 +832,7 @@ export {
763
832
  PhysicalState,
764
833
  PlantingMethod,
765
834
  RESOURCE_PATHS,
835
+ ROLE_SECTIONS_BY_KEY,
766
836
  RelationshipType,
767
837
  RowOrientation,
768
838
  SENTINEL_UUID,
@@ -779,11 +849,14 @@ export {
779
849
  VigorRating,
780
850
  WaterSource,
781
851
  assertCanWrite,
852
+ assertTenantBoundary,
782
853
  buildRlsFilter,
783
854
  canRead,
784
855
  canWrite,
856
+ evaluateBaseAccess,
785
857
  extractAppCode,
786
858
  generateCanonicalId,
859
+ generateInfrastructureFields,
787
860
  getAcceptedMimeTypes,
788
861
  getApproveAction,
789
862
  getFormMeta,
@@ -794,9 +867,8 @@ export {
794
867
  isTabWritable,
795
868
  resolveAccess,
796
869
  resolveAppError,
797
- resolveAuthScopeId,
870
+ resolveEffectiveAccess,
798
871
  resolveError,
799
872
  resolveMenuVisibility,
800
- withAgriLogging,
801
- withSaveMiddleware
873
+ withAgriLogging
802
874
  };
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.4",
4
4
  "description": "Canonical Backend Utility Library for AgriCore",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",