@litusarchieve18/agricore-utils 1.0.7 → 1.0.9

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
@@ -90,6 +90,18 @@ const recordToSave = {
90
90
  ...infrastructure, // Injects canonicalId, companyId, legalEntityId, etc.
91
91
  };
92
92
 
93
+ To completely eliminate database bottlenecks and solve serverless rate-limiting, AgriCore has migrated to a state-of-the-art, "Zero-DB" authorization architecture. By decoupling our security rules from the database SDK, our backend API endpoints now resolve complex multi-tenant boundaries and granular role-based access controls in roughly 1 millisecond.
94
+
95
+ Key Features of the New Security Engine:
96
+
97
+ One-Time Session Generation: On login, the backend dynamically aggregates a user's full hierarchical profile—including role assignments, availableScopes (Company > Legal Entity > Facility), and granular resourceOverrides—into a single, cryptographically signed JSON Web Token (JWT).
98
+
99
+ Zero-DB Library Validations: The @litusarchieve18/agricore-utils library has been upgraded to utilize pure, synchronous functions. Deep hierarchical access checks (findSpecificOverride) and tenant isolation (assertTenantBoundary) are now resolved directly against the JWT payload in-memory, completely bypassing the database for routine requests.
100
+
101
+ Smart Frontend Interceptor: A centralized React hook (useApiClient) acts as an automated API wrapper. It seamlessly injects the user's customJWT and the globally selected activeScopeId into every outbound request. This keeps UI forms completely decoupled from authentication logic.
102
+
103
+ Instant Context Switching: Because the JWT securely holds the user's entire "VIP List" of permitted scopes, users can instantly switch between farms, legal entities, and facilities in the frontend global dropdown without triggering a new authentication handshake or database query.
104
+
93
105
  🛠️ Development & Publishing Guide
94
106
  Zero Dependencies
95
107
  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.
@@ -117,7 +129,11 @@ Run the publish command:
117
129
  Bash
118
130
  npm publish --access public
119
131
  📝 Version History
120
- v1.0.5add RESOURCE_MODULE_MAP
132
+ V1.0.9added jwt function
133
+
134
+ v1.0.8 — added temporary RESOURCE_REQUIREMENTS
135
+
136
+ v1.0.7 — add RESOURCE_MODULE_MAP
121
137
 
122
138
  v1.0.6 — adding more logic
123
139
 
package/dist/index.d.mts CHANGED
@@ -490,24 +490,33 @@ declare const RESOURCE_PATHS: {
490
490
  declare const RESOURCE_MODULE_MAP: Record<string, string[]>;
491
491
  declare const ACCESS_RANK: Record<AccessLevelType, number>;
492
492
  declare const ROLE_SECTIONS_BY_KEY: Record<string, UserRoleType[]>;
493
+ interface ResourceRequirement {
494
+ allowedRoles?: UserRoleType[];
495
+ requiredPrivileges?: string[];
496
+ }
497
+ declare const EVERYONE: ("owner" | "manager" | "supervisor" | "leadingHand" | "worker")[];
498
+ declare const RESOURCE_REQUIREMENTS: Record<string, ResourceRequirement>;
493
499
  declare function evaluateBaseAccess(resourcePath: string, session: {
494
500
  role?: UserRoleType;
495
501
  privileges?: string[];
496
502
  enabledModules?: string[];
497
- }, requirements?: {
498
- allowedRoles?: UserRoleType[];
499
- requiredPrivileges?: string[];
500
- }): AccessLevelType;
503
+ }, requirements?: ResourceRequirement): AccessLevelType;
504
+ /**
505
+ * The Judge: Overrides win over base access (Specificity principle).
506
+ */
507
+ declare function resolveEffectiveAccess(base: AccessLevelType, override?: AccessLevelType | null): AccessLevelType;
508
+ declare function findSpecificOverride(session: AgriCoreSession, activeScopeId: string, resourcePath: string): AccessLevelType | null;
501
509
  /**
502
510
  * Tenant Boundary Guard (Backend Only)
503
511
  * Note: db is typed as 'any' here because Base44 SDK types vary,
504
512
  * but you can refine this with your internal SDK types.
505
513
  */
506
- declare function assertTenantBoundary(db: any, requesterCompanyId: string, targets: {
514
+ declare function assertTenantBoundary(db: any, session: AgriCoreSession, // Pass the JWT session instead of just requesterCompanyId
515
+ targets: {
507
516
  targetUserId?: string;
508
517
  authScopeId?: string;
509
518
  expectedScopeType?: 'company' | 'legalEntity' | 'facility';
510
519
  }): Promise<boolean>;
511
- declare function resolveEffectiveAccess(base: AccessLevelType, override?: AccessLevelType | null): AccessLevelType;
520
+ declare function verifyAndExtractSession(token: string, secret: string): Promise<AgriCoreSession>;
512
521
 
513
- 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_MODULE_MAP, 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 };
522
+ 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, EVERYONE, 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_MODULE_MAP, RESOURCE_PATHS, RESOURCE_REQUIREMENTS, ROLE_SECTIONS_BY_KEY, RelationshipType, type ResolvedError, type ResourceOverrides, type ResourceRequirement, 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, findSpecificOverride, generateCanonicalId, generateInfrastructureFields, getAcceptedMimeTypes, getApproveAction, getFormMeta, getFormsGroupedByModule, getTaskTypeOptions, handleAppErrorResponse, isTabVisible, isTabWritable, resolveAccess, resolveAppError, resolveEffectiveAccess, resolveError, resolveMenuVisibility, verifyAndExtractSession, withAgriLogging };
package/dist/index.d.ts CHANGED
@@ -490,24 +490,33 @@ declare const RESOURCE_PATHS: {
490
490
  declare const RESOURCE_MODULE_MAP: Record<string, string[]>;
491
491
  declare const ACCESS_RANK: Record<AccessLevelType, number>;
492
492
  declare const ROLE_SECTIONS_BY_KEY: Record<string, UserRoleType[]>;
493
+ interface ResourceRequirement {
494
+ allowedRoles?: UserRoleType[];
495
+ requiredPrivileges?: string[];
496
+ }
497
+ declare const EVERYONE: ("owner" | "manager" | "supervisor" | "leadingHand" | "worker")[];
498
+ declare const RESOURCE_REQUIREMENTS: Record<string, ResourceRequirement>;
493
499
  declare function evaluateBaseAccess(resourcePath: string, session: {
494
500
  role?: UserRoleType;
495
501
  privileges?: string[];
496
502
  enabledModules?: string[];
497
- }, requirements?: {
498
- allowedRoles?: UserRoleType[];
499
- requiredPrivileges?: string[];
500
- }): AccessLevelType;
503
+ }, requirements?: ResourceRequirement): AccessLevelType;
504
+ /**
505
+ * The Judge: Overrides win over base access (Specificity principle).
506
+ */
507
+ declare function resolveEffectiveAccess(base: AccessLevelType, override?: AccessLevelType | null): AccessLevelType;
508
+ declare function findSpecificOverride(session: AgriCoreSession, activeScopeId: string, resourcePath: string): AccessLevelType | null;
501
509
  /**
502
510
  * Tenant Boundary Guard (Backend Only)
503
511
  * Note: db is typed as 'any' here because Base44 SDK types vary,
504
512
  * but you can refine this with your internal SDK types.
505
513
  */
506
- declare function assertTenantBoundary(db: any, requesterCompanyId: string, targets: {
514
+ declare function assertTenantBoundary(db: any, session: AgriCoreSession, // Pass the JWT session instead of just requesterCompanyId
515
+ targets: {
507
516
  targetUserId?: string;
508
517
  authScopeId?: string;
509
518
  expectedScopeType?: 'company' | 'legalEntity' | 'facility';
510
519
  }): Promise<boolean>;
511
- declare function resolveEffectiveAccess(base: AccessLevelType, override?: AccessLevelType | null): AccessLevelType;
520
+ declare function verifyAndExtractSession(token: string, secret: string): Promise<AgriCoreSession>;
512
521
 
513
- 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_MODULE_MAP, 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 };
522
+ 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, EVERYONE, 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_MODULE_MAP, RESOURCE_PATHS, RESOURCE_REQUIREMENTS, ROLE_SECTIONS_BY_KEY, RelationshipType, type ResolvedError, type ResourceOverrides, type ResourceRequirement, 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, findSpecificOverride, generateCanonicalId, generateInfrastructureFields, getAcceptedMimeTypes, getApproveAction, getFormMeta, getFormsGroupedByModule, getTaskTypeOptions, handleAppErrorResponse, isTabVisible, isTabWritable, resolveAccess, resolveAppError, resolveEffectiveAccess, resolveError, resolveMenuVisibility, verifyAndExtractSession, withAgriLogging };
package/dist/index.js CHANGED
@@ -33,6 +33,7 @@ __export(index_exports, {
33
33
  Converter: () => Converter,
34
34
  CropCycleStatus: () => CropCycleStatus,
35
35
  ERROR_DICTIONARY: () => ERROR_DICTIONARY,
36
+ EVERYONE: () => EVERYONE,
36
37
  EmitterType: () => EmitterType,
37
38
  EnabledModule: () => EnabledModule,
38
39
  ErrorFactory: () => ErrorFactory,
@@ -51,6 +52,7 @@ __export(index_exports, {
51
52
  PlantingMethod: () => PlantingMethod,
52
53
  RESOURCE_MODULE_MAP: () => RESOURCE_MODULE_MAP,
53
54
  RESOURCE_PATHS: () => RESOURCE_PATHS,
55
+ RESOURCE_REQUIREMENTS: () => RESOURCE_REQUIREMENTS,
54
56
  ROLE_SECTIONS_BY_KEY: () => ROLE_SECTIONS_BY_KEY,
55
57
  RelationshipType: () => RelationshipType,
56
58
  RowOrientation: () => RowOrientation,
@@ -74,6 +76,7 @@ __export(index_exports, {
74
76
  canWrite: () => canWrite,
75
77
  evaluateBaseAccess: () => evaluateBaseAccess,
76
78
  extractAppCode: () => extractAppCode,
79
+ findSpecificOverride: () => findSpecificOverride,
77
80
  generateCanonicalId: () => generateCanonicalId,
78
81
  generateInfrastructureFields: () => generateInfrastructureFields,
79
82
  getAcceptedMimeTypes: () => getAcceptedMimeTypes,
@@ -89,9 +92,11 @@ __export(index_exports, {
89
92
  resolveEffectiveAccess: () => resolveEffectiveAccess,
90
93
  resolveError: () => resolveError,
91
94
  resolveMenuVisibility: () => resolveMenuVisibility,
95
+ verifyAndExtractSession: () => verifyAndExtractSession,
92
96
  withAgriLogging: () => withAgriLogging
93
97
  });
94
98
  module.exports = __toCommonJS(index_exports);
99
+ var import_jose = require("jose");
95
100
 
96
101
  // src/logger.ts
97
102
  var SENSITIVE_KEYS = ["password", "token", "secret", "apiKey", "creditCard", "authorization"];
@@ -430,7 +435,7 @@ function generateCanonicalId() {
430
435
  }
431
436
  function generateInfrastructureFields(activeScope, availableScopes) {
432
437
  if (!activeScope || !availableScopes?.length) {
433
- throw new Error("[generateInfrastructureFields] Missing scope context.");
438
+ throw ErrorFactory.unauthorized("[generateInfrastructureFields] Missing scope context.");
434
439
  }
435
440
  let companyId = null;
436
441
  let legalEntityId = null;
@@ -444,7 +449,7 @@ function generateInfrastructureFields(activeScope, availableScopes) {
444
449
  pointer = parentId ? availableScopes.find((s) => s.base44Id === parentId) || null : null;
445
450
  }
446
451
  if (!companyId) {
447
- throw new Error("[generateInfrastructureFields] Could not resolve companyId from scope tree.");
452
+ throw ErrorFactory.unauthorized("[generateInfrastructureFields] Could not resolve companyId from scope tree.");
448
453
  }
449
454
  return {
450
455
  canonicalId: crypto.randomUUID(),
@@ -907,6 +912,76 @@ var ROLE_SECTIONS_BY_KEY = {
907
912
  records: [UserRole.OWNER, UserRole.MANAGER],
908
913
  adminSettings: []
909
914
  };
915
+ var EVERYONE = Object.values(UserRole);
916
+ var RESOURCE_REQUIREMENTS = {
917
+ [RESOURCE_PATHS.dashboard]: {
918
+ allowedRoles: EVERYONE
919
+ },
920
+ [RESOURCE_PATHS.operations]: {
921
+ allowedRoles: EVERYONE
922
+ },
923
+ // [RESOURCE_PATHS.equipment]: EVERYONE,
924
+ // [RESOURCE_PATHS.safety]: ALL_OPERATIONS,
925
+ // [RESOURCE_PATHS.team]: ALL_OPERATIONS,
926
+ // [RESOURCE_PATHS.records]: ALL_OPERATIONS,
927
+ // [RESOURCE_PATHS.adminSettings]: ALL_OPERATIONS,
928
+ // ── Records & Reports ──────────────────────────────────────────────────────
929
+ [RESOURCE_PATHS.documentHub]: {
930
+ allowedRoles: [UserRole.OWNER, UserRole.MANAGER, UserRole.SUPERVISOR],
931
+ requiredPrivileges: [UserPrivilege.ADMIN, UserPrivilege.AUDIT_COMPLIANCE, UserPrivilege.FINANCE, UserPrivilege.HR]
932
+ },
933
+ [RESOURCE_PATHS.documentHubTemplates]: {
934
+ allowedRoles: [UserRole.OWNER, UserRole.MANAGER, UserRole.SUPERVISOR],
935
+ requiredPrivileges: [UserPrivilege.ADMIN, UserPrivilege.AUDIT_COMPLIANCE, UserPrivilege.FINANCE, UserPrivilege.HR]
936
+ },
937
+ [RESOURCE_PATHS.documentHubFolder]: {
938
+ allowedRoles: [UserRole.OWNER, UserRole.MANAGER, UserRole.SUPERVISOR],
939
+ requiredPrivileges: [UserPrivilege.ADMIN, UserPrivilege.AUDIT_COMPLIANCE, UserPrivilege.FINANCE, UserPrivilege.HR]
940
+ },
941
+ [RESOURCE_PATHS.documentHubExtraction]: {
942
+ allowedRoles: [UserRole.OWNER, UserRole.MANAGER, UserRole.SUPERVISOR],
943
+ requiredPrivileges: [UserPrivilege.ADMIN, UserPrivilege.AUDIT_COMPLIANCE, UserPrivilege.FINANCE, UserPrivilege.HR]
944
+ },
945
+ [RESOURCE_PATHS.documentHubQueue]: {
946
+ allowedRoles: [UserRole.OWNER, UserRole.MANAGER, UserRole.SUPERVISOR],
947
+ requiredPrivileges: [UserPrivilege.ADMIN, UserPrivilege.AUDIT_COMPLIANCE, UserPrivilege.FINANCE, UserPrivilege.HR]
948
+ },
949
+ // [RESOURCE_PATHS.analytics]: ALL_OPERATIONS,
950
+ // ── Admin Settings ─────────────────────────────────────────────────────────
951
+ [RESOURCE_PATHS.adminUserRoster]: {
952
+ allowedRoles: [UserRole.OWNER],
953
+ requiredPrivileges: [UserPrivilege.ADMIN]
954
+ },
955
+ [RESOURCE_PATHS.adminBlueprintsSites]: {
956
+ allowedRoles: [UserRole.OWNER],
957
+ requiredPrivileges: [UserPrivilege.ADMIN]
958
+ },
959
+ [RESOURCE_PATHS.adminRolesPermissions]: {
960
+ allowedRoles: [UserRole.OWNER],
961
+ requiredPrivileges: [UserPrivilege.ADMIN]
962
+ },
963
+ [RESOURCE_PATHS.adminCompanySettings]: {
964
+ allowedRoles: [UserRole.OWNER]
965
+ }
966
+ // // ── Operations ────────────────────────────────────────────────────────────
967
+ // [RESOURCE_PATHS.farmManagement]: [EnabledModule.FARM_MANAGEMENT],
968
+ // [RESOURCE_PATHS.packhouse]: [EnabledModule.PACKHOUSE],
969
+ // [RESOURCE_PATHS.accommodation]: [EnabledModule.ACCOMMODATION],
970
+ // // ── Equipment & Fleet ──────────────────────────────────────────────────────
971
+ // [RESOURCE_PATHS.equipPreStart]: ALL_OPERATIONS,
972
+ // [RESOURCE_PATHS.equipMaintenance]: ALL_OPERATIONS,
973
+ // [RESOURCE_PATHS.equipFuelRegister]: ALL_OPERATIONS,
974
+ // [RESOURCE_PATHS.equipCalibration]: AGRI_ONLY,
975
+ // [RESOURCE_PATHS.equipFleet]: ALL_OPERATIONS,
976
+ // // ── Safety, Quality & Compliance ──────────────────────────────────────────
977
+ // [RESOURCE_PATHS.safetyAuditing]: ALL_OPERATIONS,
978
+ // [RESOURCE_PATHS.safetyFood]: AGRI_ONLY,
979
+ // [RESOURCE_PATHS.safetyWhs]: ALL_OPERATIONS,
980
+ // [RESOURCE_PATHS.safetyCleaning]: ALL_OPERATIONS,
981
+ // // ── Team & HR ──────────────────────────────────────────────────────────────
982
+ // [RESOURCE_PATHS.teamStaff]: ALL_OPERATIONS,
983
+ // [RESOURCE_PATHS.teamTraining]: ALL_OPERATIONS,
984
+ };
910
985
  function evaluateBaseAccess(resourcePath, session, requirements = {}) {
911
986
  if (!session?.role) return "NONE";
912
987
  if (session.role === UserRole.OWNER) return "WRITE";
@@ -925,44 +1000,63 @@ function evaluateBaseAccess(resourcePath, session, requirements = {}) {
925
1000
  }
926
1001
  return session.role === UserRole.MANAGER ? "WRITE" : "READ";
927
1002
  }
928
- async function assertTenantBoundary(db, requesterCompanyId, targets) {
929
- if (!requesterCompanyId) {
930
- throw ErrorFactory.internal(MOD.ADMIN, "Missing requester company context for boundary check.");
1003
+ function resolveEffectiveAccess(base, override) {
1004
+ return override === void 0 || override === null ? base : override;
1005
+ }
1006
+ function findSpecificOverride(session, activeScopeId, resourcePath) {
1007
+ const scopeOverrides = session.resourceOverrides?.[activeScopeId];
1008
+ if (!scopeOverrides) return null;
1009
+ const parts = resourcePath.split(".");
1010
+ let currentPath = "";
1011
+ let bestOverride = null;
1012
+ for (const part of parts) {
1013
+ currentPath = currentPath ? `${currentPath}.${part}` : part;
1014
+ if (scopeOverrides[currentPath] !== void 0) {
1015
+ bestOverride = scopeOverrides[currentPath];
1016
+ }
1017
+ }
1018
+ return bestOverride;
1019
+ }
1020
+ async function assertTenantBoundary(db, session, targets) {
1021
+ if (!session.companyId) {
1022
+ throw ErrorFactory.internal(MOD.ADMIN, "Missing requester company context.");
931
1023
  }
932
1024
  if (targets.targetUserId) {
933
1025
  const targetUserAssignments = await db.UserRoleAssignment.filter({
934
1026
  userId: targets.targetUserId,
935
- companyId: requesterCompanyId
1027
+ companyId: session.companyId
936
1028
  });
937
1029
  if (targetUserAssignments.length === 0) {
938
- console.error(`[SECURITY BREACH ATTEMPT] User tried to modify targetUserId ${targets.targetUserId} outside their company ${requesterCompanyId}`);
939
- throw ErrorFactory.forbidden(MOD.ADMIN, ACT.EDIT, "Invalid target user or tenant boundary violation.");
1030
+ console.error(`[SECURITY BREACH] User tried to modify targetUserId ${targets.targetUserId}`);
1031
+ throw ErrorFactory.forbidden(MOD.ADMIN, ACT.EDIT, "Invalid target user.");
940
1032
  }
941
1033
  }
942
1034
  if (targets.authScopeId) {
943
- let matchFound = false;
944
- if (targets.expectedScopeType === "company" || !targets.expectedScopeType) {
945
- const c = await db.Company.filter({ canonicalId: targets.authScopeId, id: requesterCompanyId });
946
- if (c.length > 0) matchFound = true;
947
- }
948
- if (!matchFound && (targets.expectedScopeType === "legalEntity" || !targets.expectedScopeType)) {
949
- const le = await db.LegalEntity.filter({ canonicalId: targets.authScopeId, companyId: requesterCompanyId });
950
- if (le.length > 0) matchFound = true;
951
- }
952
- if (!matchFound && (targets.expectedScopeType === "facility" || !targets.expectedScopeType)) {
953
- const fac = await db.Facility.filter({ canonicalId: targets.authScopeId, companyId: requesterCompanyId });
954
- if (fac.length > 0) matchFound = true;
1035
+ const matchedScope = session.availableScopes?.find(
1036
+ (scope) => scope.authScopeId === targets.authScopeId
1037
+ );
1038
+ if (!matchedScope) {
1039
+ console.error(`[SECURITY BREACH] Scope ${targets.authScopeId} not found in user's permitted list.`);
1040
+ throw ErrorFactory.forbidden(MOD.ADMIN, ACT.EDIT, "Tenant boundary violation.");
955
1041
  }
956
- if (!matchFound) {
957
- console.error(`[SECURITY BREACH/VALIDATION ERROR] Scope ${targets.authScopeId} not found for type ${targets.expectedScopeType} in company ${requesterCompanyId}`);
958
- throw ErrorFactory.forbidden(MOD.ADMIN, ACT.EDIT, "Invalid authorization scope level or tenant boundary violation.");
1042
+ if (targets.expectedScopeType && matchedScope.type !== targets.expectedScopeType) {
1043
+ console.error(`[VALIDATION ERROR] Expected type ${targets.expectedScopeType} but got ${matchedScope.type}`);
1044
+ throw ErrorFactory.forbidden(MOD.ADMIN, ACT.EDIT, "Invalid authorization scope level.");
959
1045
  }
960
1046
  }
961
1047
  return true;
962
1048
  }
963
- function resolveEffectiveAccess(base, override) {
964
- if (override === void 0 || override === null) return base;
965
- return override;
1049
+ async function verifyAndExtractSession(token, secret) {
1050
+ if (!token) {
1051
+ throw ErrorFactory.unauthorized(MOD.SESSION, "UNAUTHORIZED: No secure session token provided.");
1052
+ }
1053
+ try {
1054
+ const secretKey = new TextEncoder().encode(secret);
1055
+ const { payload } = await (0, import_jose.jwtVerify)(token, secretKey);
1056
+ return payload;
1057
+ } catch (error) {
1058
+ throw ErrorFactory.unauthorized(MOD.SESSION, "UNAUTHORIZED: No secure session token provided.");
1059
+ }
966
1060
  }
967
1061
  // Annotate the CommonJS export names for ESM import in node:
968
1062
  0 && (module.exports = {
@@ -979,6 +1073,7 @@ function resolveEffectiveAccess(base, override) {
979
1073
  Converter,
980
1074
  CropCycleStatus,
981
1075
  ERROR_DICTIONARY,
1076
+ EVERYONE,
982
1077
  EmitterType,
983
1078
  EnabledModule,
984
1079
  ErrorFactory,
@@ -997,6 +1092,7 @@ function resolveEffectiveAccess(base, override) {
997
1092
  PlantingMethod,
998
1093
  RESOURCE_MODULE_MAP,
999
1094
  RESOURCE_PATHS,
1095
+ RESOURCE_REQUIREMENTS,
1000
1096
  ROLE_SECTIONS_BY_KEY,
1001
1097
  RelationshipType,
1002
1098
  RowOrientation,
@@ -1020,6 +1116,7 @@ function resolveEffectiveAccess(base, override) {
1020
1116
  canWrite,
1021
1117
  evaluateBaseAccess,
1022
1118
  extractAppCode,
1119
+ findSpecificOverride,
1023
1120
  generateCanonicalId,
1024
1121
  generateInfrastructureFields,
1025
1122
  getAcceptedMimeTypes,
@@ -1035,5 +1132,6 @@ function resolveEffectiveAccess(base, override) {
1035
1132
  resolveEffectiveAccess,
1036
1133
  resolveError,
1037
1134
  resolveMenuVisibility,
1135
+ verifyAndExtractSession,
1038
1136
  withAgriLogging
1039
1137
  });
package/dist/index.mjs CHANGED
@@ -1,3 +1,6 @@
1
+ // src/index.ts
2
+ import { jwtVerify } from "jose";
3
+
1
4
  // src/logger.ts
2
5
  var SENSITIVE_KEYS = ["password", "token", "secret", "apiKey", "creditCard", "authorization"];
3
6
  function redact(obj) {
@@ -335,7 +338,7 @@ function generateCanonicalId() {
335
338
  }
336
339
  function generateInfrastructureFields(activeScope, availableScopes) {
337
340
  if (!activeScope || !availableScopes?.length) {
338
- throw new Error("[generateInfrastructureFields] Missing scope context.");
341
+ throw ErrorFactory.unauthorized("[generateInfrastructureFields] Missing scope context.");
339
342
  }
340
343
  let companyId = null;
341
344
  let legalEntityId = null;
@@ -349,7 +352,7 @@ function generateInfrastructureFields(activeScope, availableScopes) {
349
352
  pointer = parentId ? availableScopes.find((s) => s.base44Id === parentId) || null : null;
350
353
  }
351
354
  if (!companyId) {
352
- throw new Error("[generateInfrastructureFields] Could not resolve companyId from scope tree.");
355
+ throw ErrorFactory.unauthorized("[generateInfrastructureFields] Could not resolve companyId from scope tree.");
353
356
  }
354
357
  return {
355
358
  canonicalId: crypto.randomUUID(),
@@ -812,6 +815,76 @@ var ROLE_SECTIONS_BY_KEY = {
812
815
  records: [UserRole.OWNER, UserRole.MANAGER],
813
816
  adminSettings: []
814
817
  };
818
+ var EVERYONE = Object.values(UserRole);
819
+ var RESOURCE_REQUIREMENTS = {
820
+ [RESOURCE_PATHS.dashboard]: {
821
+ allowedRoles: EVERYONE
822
+ },
823
+ [RESOURCE_PATHS.operations]: {
824
+ allowedRoles: EVERYONE
825
+ },
826
+ // [RESOURCE_PATHS.equipment]: EVERYONE,
827
+ // [RESOURCE_PATHS.safety]: ALL_OPERATIONS,
828
+ // [RESOURCE_PATHS.team]: ALL_OPERATIONS,
829
+ // [RESOURCE_PATHS.records]: ALL_OPERATIONS,
830
+ // [RESOURCE_PATHS.adminSettings]: ALL_OPERATIONS,
831
+ // ── Records & Reports ──────────────────────────────────────────────────────
832
+ [RESOURCE_PATHS.documentHub]: {
833
+ allowedRoles: [UserRole.OWNER, UserRole.MANAGER, UserRole.SUPERVISOR],
834
+ requiredPrivileges: [UserPrivilege.ADMIN, UserPrivilege.AUDIT_COMPLIANCE, UserPrivilege.FINANCE, UserPrivilege.HR]
835
+ },
836
+ [RESOURCE_PATHS.documentHubTemplates]: {
837
+ allowedRoles: [UserRole.OWNER, UserRole.MANAGER, UserRole.SUPERVISOR],
838
+ requiredPrivileges: [UserPrivilege.ADMIN, UserPrivilege.AUDIT_COMPLIANCE, UserPrivilege.FINANCE, UserPrivilege.HR]
839
+ },
840
+ [RESOURCE_PATHS.documentHubFolder]: {
841
+ allowedRoles: [UserRole.OWNER, UserRole.MANAGER, UserRole.SUPERVISOR],
842
+ requiredPrivileges: [UserPrivilege.ADMIN, UserPrivilege.AUDIT_COMPLIANCE, UserPrivilege.FINANCE, UserPrivilege.HR]
843
+ },
844
+ [RESOURCE_PATHS.documentHubExtraction]: {
845
+ allowedRoles: [UserRole.OWNER, UserRole.MANAGER, UserRole.SUPERVISOR],
846
+ requiredPrivileges: [UserPrivilege.ADMIN, UserPrivilege.AUDIT_COMPLIANCE, UserPrivilege.FINANCE, UserPrivilege.HR]
847
+ },
848
+ [RESOURCE_PATHS.documentHubQueue]: {
849
+ allowedRoles: [UserRole.OWNER, UserRole.MANAGER, UserRole.SUPERVISOR],
850
+ requiredPrivileges: [UserPrivilege.ADMIN, UserPrivilege.AUDIT_COMPLIANCE, UserPrivilege.FINANCE, UserPrivilege.HR]
851
+ },
852
+ // [RESOURCE_PATHS.analytics]: ALL_OPERATIONS,
853
+ // ── Admin Settings ─────────────────────────────────────────────────────────
854
+ [RESOURCE_PATHS.adminUserRoster]: {
855
+ allowedRoles: [UserRole.OWNER],
856
+ requiredPrivileges: [UserPrivilege.ADMIN]
857
+ },
858
+ [RESOURCE_PATHS.adminBlueprintsSites]: {
859
+ allowedRoles: [UserRole.OWNER],
860
+ requiredPrivileges: [UserPrivilege.ADMIN]
861
+ },
862
+ [RESOURCE_PATHS.adminRolesPermissions]: {
863
+ allowedRoles: [UserRole.OWNER],
864
+ requiredPrivileges: [UserPrivilege.ADMIN]
865
+ },
866
+ [RESOURCE_PATHS.adminCompanySettings]: {
867
+ allowedRoles: [UserRole.OWNER]
868
+ }
869
+ // // ── Operations ────────────────────────────────────────────────────────────
870
+ // [RESOURCE_PATHS.farmManagement]: [EnabledModule.FARM_MANAGEMENT],
871
+ // [RESOURCE_PATHS.packhouse]: [EnabledModule.PACKHOUSE],
872
+ // [RESOURCE_PATHS.accommodation]: [EnabledModule.ACCOMMODATION],
873
+ // // ── Equipment & Fleet ──────────────────────────────────────────────────────
874
+ // [RESOURCE_PATHS.equipPreStart]: ALL_OPERATIONS,
875
+ // [RESOURCE_PATHS.equipMaintenance]: ALL_OPERATIONS,
876
+ // [RESOURCE_PATHS.equipFuelRegister]: ALL_OPERATIONS,
877
+ // [RESOURCE_PATHS.equipCalibration]: AGRI_ONLY,
878
+ // [RESOURCE_PATHS.equipFleet]: ALL_OPERATIONS,
879
+ // // ── Safety, Quality & Compliance ──────────────────────────────────────────
880
+ // [RESOURCE_PATHS.safetyAuditing]: ALL_OPERATIONS,
881
+ // [RESOURCE_PATHS.safetyFood]: AGRI_ONLY,
882
+ // [RESOURCE_PATHS.safetyWhs]: ALL_OPERATIONS,
883
+ // [RESOURCE_PATHS.safetyCleaning]: ALL_OPERATIONS,
884
+ // // ── Team & HR ──────────────────────────────────────────────────────────────
885
+ // [RESOURCE_PATHS.teamStaff]: ALL_OPERATIONS,
886
+ // [RESOURCE_PATHS.teamTraining]: ALL_OPERATIONS,
887
+ };
815
888
  function evaluateBaseAccess(resourcePath, session, requirements = {}) {
816
889
  if (!session?.role) return "NONE";
817
890
  if (session.role === UserRole.OWNER) return "WRITE";
@@ -830,44 +903,63 @@ function evaluateBaseAccess(resourcePath, session, requirements = {}) {
830
903
  }
831
904
  return session.role === UserRole.MANAGER ? "WRITE" : "READ";
832
905
  }
833
- async function assertTenantBoundary(db, requesterCompanyId, targets) {
834
- if (!requesterCompanyId) {
835
- throw ErrorFactory.internal(MOD.ADMIN, "Missing requester company context for boundary check.");
906
+ function resolveEffectiveAccess(base, override) {
907
+ return override === void 0 || override === null ? base : override;
908
+ }
909
+ function findSpecificOverride(session, activeScopeId, resourcePath) {
910
+ const scopeOverrides = session.resourceOverrides?.[activeScopeId];
911
+ if (!scopeOverrides) return null;
912
+ const parts = resourcePath.split(".");
913
+ let currentPath = "";
914
+ let bestOverride = null;
915
+ for (const part of parts) {
916
+ currentPath = currentPath ? `${currentPath}.${part}` : part;
917
+ if (scopeOverrides[currentPath] !== void 0) {
918
+ bestOverride = scopeOverrides[currentPath];
919
+ }
920
+ }
921
+ return bestOverride;
922
+ }
923
+ async function assertTenantBoundary(db, session, targets) {
924
+ if (!session.companyId) {
925
+ throw ErrorFactory.internal(MOD.ADMIN, "Missing requester company context.");
836
926
  }
837
927
  if (targets.targetUserId) {
838
928
  const targetUserAssignments = await db.UserRoleAssignment.filter({
839
929
  userId: targets.targetUserId,
840
- companyId: requesterCompanyId
930
+ companyId: session.companyId
841
931
  });
842
932
  if (targetUserAssignments.length === 0) {
843
- console.error(`[SECURITY BREACH ATTEMPT] User tried to modify targetUserId ${targets.targetUserId} outside their company ${requesterCompanyId}`);
844
- throw ErrorFactory.forbidden(MOD.ADMIN, ACT.EDIT, "Invalid target user or tenant boundary violation.");
933
+ console.error(`[SECURITY BREACH] User tried to modify targetUserId ${targets.targetUserId}`);
934
+ throw ErrorFactory.forbidden(MOD.ADMIN, ACT.EDIT, "Invalid target user.");
845
935
  }
846
936
  }
847
937
  if (targets.authScopeId) {
848
- let matchFound = false;
849
- if (targets.expectedScopeType === "company" || !targets.expectedScopeType) {
850
- const c = await db.Company.filter({ canonicalId: targets.authScopeId, id: requesterCompanyId });
851
- if (c.length > 0) matchFound = true;
852
- }
853
- if (!matchFound && (targets.expectedScopeType === "legalEntity" || !targets.expectedScopeType)) {
854
- const le = await db.LegalEntity.filter({ canonicalId: targets.authScopeId, companyId: requesterCompanyId });
855
- if (le.length > 0) matchFound = true;
856
- }
857
- if (!matchFound && (targets.expectedScopeType === "facility" || !targets.expectedScopeType)) {
858
- const fac = await db.Facility.filter({ canonicalId: targets.authScopeId, companyId: requesterCompanyId });
859
- if (fac.length > 0) matchFound = true;
938
+ const matchedScope = session.availableScopes?.find(
939
+ (scope) => scope.authScopeId === targets.authScopeId
940
+ );
941
+ if (!matchedScope) {
942
+ console.error(`[SECURITY BREACH] Scope ${targets.authScopeId} not found in user's permitted list.`);
943
+ throw ErrorFactory.forbidden(MOD.ADMIN, ACT.EDIT, "Tenant boundary violation.");
860
944
  }
861
- if (!matchFound) {
862
- console.error(`[SECURITY BREACH/VALIDATION ERROR] Scope ${targets.authScopeId} not found for type ${targets.expectedScopeType} in company ${requesterCompanyId}`);
863
- throw ErrorFactory.forbidden(MOD.ADMIN, ACT.EDIT, "Invalid authorization scope level or tenant boundary violation.");
945
+ if (targets.expectedScopeType && matchedScope.type !== targets.expectedScopeType) {
946
+ console.error(`[VALIDATION ERROR] Expected type ${targets.expectedScopeType} but got ${matchedScope.type}`);
947
+ throw ErrorFactory.forbidden(MOD.ADMIN, ACT.EDIT, "Invalid authorization scope level.");
864
948
  }
865
949
  }
866
950
  return true;
867
951
  }
868
- function resolveEffectiveAccess(base, override) {
869
- if (override === void 0 || override === null) return base;
870
- return override;
952
+ async function verifyAndExtractSession(token, secret) {
953
+ if (!token) {
954
+ throw ErrorFactory.unauthorized(MOD.SESSION, "UNAUTHORIZED: No secure session token provided.");
955
+ }
956
+ try {
957
+ const secretKey = new TextEncoder().encode(secret);
958
+ const { payload } = await jwtVerify(token, secretKey);
959
+ return payload;
960
+ } catch (error) {
961
+ throw ErrorFactory.unauthorized(MOD.SESSION, "UNAUTHORIZED: No secure session token provided.");
962
+ }
871
963
  }
872
964
  export {
873
965
  ACCESS_RANK,
@@ -883,6 +975,7 @@ export {
883
975
  Converter,
884
976
  CropCycleStatus,
885
977
  ERROR_DICTIONARY,
978
+ EVERYONE,
886
979
  EmitterType,
887
980
  EnabledModule,
888
981
  ErrorFactory,
@@ -901,6 +994,7 @@ export {
901
994
  PlantingMethod,
902
995
  RESOURCE_MODULE_MAP,
903
996
  RESOURCE_PATHS,
997
+ RESOURCE_REQUIREMENTS,
904
998
  ROLE_SECTIONS_BY_KEY,
905
999
  RelationshipType,
906
1000
  RowOrientation,
@@ -924,6 +1018,7 @@ export {
924
1018
  canWrite,
925
1019
  evaluateBaseAccess,
926
1020
  extractAppCode,
1021
+ findSpecificOverride,
927
1022
  generateCanonicalId,
928
1023
  generateInfrastructureFields,
929
1024
  getAcceptedMimeTypes,
@@ -939,5 +1034,6 @@ export {
939
1034
  resolveEffectiveAccess,
940
1035
  resolveError,
941
1036
  resolveMenuVisibility,
1037
+ verifyAndExtractSession,
942
1038
  withAgriLogging
943
1039
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@litusarchieve18/agricore-utils",
3
- "version": "1.0.7",
3
+ "version": "1.0.9",
4
4
  "description": "Canonical Backend Utility Library for AgriCore",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -23,5 +23,7 @@
23
23
  "tsup": "^8.0.0",
24
24
  "typescript": "^5.0.0"
25
25
  },
26
- "dependencies": {}
27
- }
26
+ "dependencies": {
27
+ "jose": "^6.2.2"
28
+ }
29
+ }