@litusarchieve18/agricore-utils 1.0.8 → 1.0.10
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 +19 -1
- package/dist/index.d.mts +5 -2
- package/dist/index.d.ts +5 -2
- package/dist/index.js +48 -23
- package/dist/index.mjs +48 -23
- package/package.json +13 -3
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,13 @@ Run the publish command:
|
|
|
117
129
|
Bash
|
|
118
130
|
npm publish --access public
|
|
119
131
|
📝 Version History
|
|
120
|
-
|
|
132
|
+
V1.0.10 — add an explicit exports map
|
|
133
|
+
|
|
134
|
+
V1.0.9 — added jwt function
|
|
135
|
+
|
|
136
|
+
v1.0.8 — added temporary RESOURCE_REQUIREMENTS
|
|
137
|
+
|
|
138
|
+
v1.0.7 — add RESOURCE_MODULE_MAP
|
|
121
139
|
|
|
122
140
|
v1.0.6 — adding more logic
|
|
123
141
|
|
package/dist/index.d.mts
CHANGED
|
@@ -505,15 +505,18 @@ declare function evaluateBaseAccess(resourcePath: string, session: {
|
|
|
505
505
|
* The Judge: Overrides win over base access (Specificity principle).
|
|
506
506
|
*/
|
|
507
507
|
declare function resolveEffectiveAccess(base: AccessLevelType, override?: AccessLevelType | null): AccessLevelType;
|
|
508
|
+
declare function findSpecificOverride(session: AgriCoreSession, activeScopeId: string, resourcePath: string): AccessLevelType | null;
|
|
508
509
|
/**
|
|
509
510
|
* Tenant Boundary Guard (Backend Only)
|
|
510
511
|
* Note: db is typed as 'any' here because Base44 SDK types vary,
|
|
511
512
|
* but you can refine this with your internal SDK types.
|
|
512
513
|
*/
|
|
513
|
-
declare function assertTenantBoundary(db: any,
|
|
514
|
+
declare function assertTenantBoundary(db: any, session: AgriCoreSession, // Pass the JWT session instead of just requesterCompanyId
|
|
515
|
+
targets: {
|
|
514
516
|
targetUserId?: string;
|
|
515
517
|
authScopeId?: string;
|
|
516
518
|
expectedScopeType?: 'company' | 'legalEntity' | 'facility';
|
|
517
519
|
}): Promise<boolean>;
|
|
520
|
+
declare function verifyAndExtractSession(token: string, secret: string): Promise<AgriCoreSession>;
|
|
518
521
|
|
|
519
|
-
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, 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
|
@@ -505,15 +505,18 @@ declare function evaluateBaseAccess(resourcePath: string, session: {
|
|
|
505
505
|
* The Judge: Overrides win over base access (Specificity principle).
|
|
506
506
|
*/
|
|
507
507
|
declare function resolveEffectiveAccess(base: AccessLevelType, override?: AccessLevelType | null): AccessLevelType;
|
|
508
|
+
declare function findSpecificOverride(session: AgriCoreSession, activeScopeId: string, resourcePath: string): AccessLevelType | null;
|
|
508
509
|
/**
|
|
509
510
|
* Tenant Boundary Guard (Backend Only)
|
|
510
511
|
* Note: db is typed as 'any' here because Base44 SDK types vary,
|
|
511
512
|
* but you can refine this with your internal SDK types.
|
|
512
513
|
*/
|
|
513
|
-
declare function assertTenantBoundary(db: any,
|
|
514
|
+
declare function assertTenantBoundary(db: any, session: AgriCoreSession, // Pass the JWT session instead of just requesterCompanyId
|
|
515
|
+
targets: {
|
|
514
516
|
targetUserId?: string;
|
|
515
517
|
authScopeId?: string;
|
|
516
518
|
expectedScopeType?: 'company' | 'legalEntity' | 'facility';
|
|
517
519
|
}): Promise<boolean>;
|
|
520
|
+
declare function verifyAndExtractSession(token: string, secret: string): Promise<AgriCoreSession>;
|
|
518
521
|
|
|
519
|
-
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, 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
|
@@ -76,6 +76,7 @@ __export(index_exports, {
|
|
|
76
76
|
canWrite: () => canWrite,
|
|
77
77
|
evaluateBaseAccess: () => evaluateBaseAccess,
|
|
78
78
|
extractAppCode: () => extractAppCode,
|
|
79
|
+
findSpecificOverride: () => findSpecificOverride,
|
|
79
80
|
generateCanonicalId: () => generateCanonicalId,
|
|
80
81
|
generateInfrastructureFields: () => generateInfrastructureFields,
|
|
81
82
|
getAcceptedMimeTypes: () => getAcceptedMimeTypes,
|
|
@@ -91,9 +92,11 @@ __export(index_exports, {
|
|
|
91
92
|
resolveEffectiveAccess: () => resolveEffectiveAccess,
|
|
92
93
|
resolveError: () => resolveError,
|
|
93
94
|
resolveMenuVisibility: () => resolveMenuVisibility,
|
|
95
|
+
verifyAndExtractSession: () => verifyAndExtractSession,
|
|
94
96
|
withAgriLogging: () => withAgriLogging
|
|
95
97
|
});
|
|
96
98
|
module.exports = __toCommonJS(index_exports);
|
|
99
|
+
var import_jose = require("jose");
|
|
97
100
|
|
|
98
101
|
// src/logger.ts
|
|
99
102
|
var SENSITIVE_KEYS = ["password", "token", "secret", "apiKey", "creditCard", "authorization"];
|
|
@@ -432,7 +435,7 @@ function generateCanonicalId() {
|
|
|
432
435
|
}
|
|
433
436
|
function generateInfrastructureFields(activeScope, availableScopes) {
|
|
434
437
|
if (!activeScope || !availableScopes?.length) {
|
|
435
|
-
throw
|
|
438
|
+
throw ErrorFactory.unauthorized("[generateInfrastructureFields] Missing scope context.");
|
|
436
439
|
}
|
|
437
440
|
let companyId = null;
|
|
438
441
|
let legalEntityId = null;
|
|
@@ -446,7 +449,7 @@ function generateInfrastructureFields(activeScope, availableScopes) {
|
|
|
446
449
|
pointer = parentId ? availableScopes.find((s) => s.base44Id === parentId) || null : null;
|
|
447
450
|
}
|
|
448
451
|
if (!companyId) {
|
|
449
|
-
throw
|
|
452
|
+
throw ErrorFactory.unauthorized("[generateInfrastructureFields] Could not resolve companyId from scope tree.");
|
|
450
453
|
}
|
|
451
454
|
return {
|
|
452
455
|
canonicalId: crypto.randomUUID(),
|
|
@@ -1000,41 +1003,61 @@ function evaluateBaseAccess(resourcePath, session, requirements = {}) {
|
|
|
1000
1003
|
function resolveEffectiveAccess(base, override) {
|
|
1001
1004
|
return override === void 0 || override === null ? base : override;
|
|
1002
1005
|
}
|
|
1003
|
-
|
|
1004
|
-
|
|
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.");
|
|
1006
1023
|
}
|
|
1007
1024
|
if (targets.targetUserId) {
|
|
1008
1025
|
const targetUserAssignments = await db.UserRoleAssignment.filter({
|
|
1009
1026
|
userId: targets.targetUserId,
|
|
1010
|
-
companyId:
|
|
1027
|
+
companyId: session.companyId
|
|
1011
1028
|
});
|
|
1012
1029
|
if (targetUserAssignments.length === 0) {
|
|
1013
|
-
console.error(`[SECURITY BREACH
|
|
1014
|
-
throw ErrorFactory.forbidden(MOD.ADMIN, ACT.EDIT, "Invalid target user
|
|
1030
|
+
console.error(`[SECURITY BREACH] User tried to modify targetUserId ${targets.targetUserId}`);
|
|
1031
|
+
throw ErrorFactory.forbidden(MOD.ADMIN, ACT.EDIT, "Invalid target user.");
|
|
1015
1032
|
}
|
|
1016
1033
|
}
|
|
1017
1034
|
if (targets.authScopeId) {
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
const le = await db.LegalEntity.filter({ canonicalId: targets.authScopeId, companyId: requesterCompanyId });
|
|
1025
|
-
if (le.length > 0) matchFound = true;
|
|
1026
|
-
}
|
|
1027
|
-
if (!matchFound && (targets.expectedScopeType === "facility" || !targets.expectedScopeType)) {
|
|
1028
|
-
const fac = await db.Facility.filter({ canonicalId: targets.authScopeId, companyId: requesterCompanyId });
|
|
1029
|
-
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.");
|
|
1030
1041
|
}
|
|
1031
|
-
if (
|
|
1032
|
-
console.error(`[
|
|
1033
|
-
throw ErrorFactory.forbidden(MOD.ADMIN, ACT.EDIT, "Invalid authorization scope level
|
|
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.");
|
|
1034
1045
|
}
|
|
1035
1046
|
}
|
|
1036
1047
|
return true;
|
|
1037
1048
|
}
|
|
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
|
+
}
|
|
1060
|
+
}
|
|
1038
1061
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1039
1062
|
0 && (module.exports = {
|
|
1040
1063
|
ACCESS_RANK,
|
|
@@ -1093,6 +1116,7 @@ async function assertTenantBoundary(db, requesterCompanyId, targets) {
|
|
|
1093
1116
|
canWrite,
|
|
1094
1117
|
evaluateBaseAccess,
|
|
1095
1118
|
extractAppCode,
|
|
1119
|
+
findSpecificOverride,
|
|
1096
1120
|
generateCanonicalId,
|
|
1097
1121
|
generateInfrastructureFields,
|
|
1098
1122
|
getAcceptedMimeTypes,
|
|
@@ -1108,5 +1132,6 @@ async function assertTenantBoundary(db, requesterCompanyId, targets) {
|
|
|
1108
1132
|
resolveEffectiveAccess,
|
|
1109
1133
|
resolveError,
|
|
1110
1134
|
resolveMenuVisibility,
|
|
1135
|
+
verifyAndExtractSession,
|
|
1111
1136
|
withAgriLogging
|
|
1112
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
|
|
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
|
|
355
|
+
throw ErrorFactory.unauthorized("[generateInfrastructureFields] Could not resolve companyId from scope tree.");
|
|
353
356
|
}
|
|
354
357
|
return {
|
|
355
358
|
canonicalId: crypto.randomUUID(),
|
|
@@ -903,41 +906,61 @@ function evaluateBaseAccess(resourcePath, session, requirements = {}) {
|
|
|
903
906
|
function resolveEffectiveAccess(base, override) {
|
|
904
907
|
return override === void 0 || override === null ? base : override;
|
|
905
908
|
}
|
|
906
|
-
|
|
907
|
-
|
|
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.");
|
|
909
926
|
}
|
|
910
927
|
if (targets.targetUserId) {
|
|
911
928
|
const targetUserAssignments = await db.UserRoleAssignment.filter({
|
|
912
929
|
userId: targets.targetUserId,
|
|
913
|
-
companyId:
|
|
930
|
+
companyId: session.companyId
|
|
914
931
|
});
|
|
915
932
|
if (targetUserAssignments.length === 0) {
|
|
916
|
-
console.error(`[SECURITY BREACH
|
|
917
|
-
throw ErrorFactory.forbidden(MOD.ADMIN, ACT.EDIT, "Invalid target user
|
|
933
|
+
console.error(`[SECURITY BREACH] User tried to modify targetUserId ${targets.targetUserId}`);
|
|
934
|
+
throw ErrorFactory.forbidden(MOD.ADMIN, ACT.EDIT, "Invalid target user.");
|
|
918
935
|
}
|
|
919
936
|
}
|
|
920
937
|
if (targets.authScopeId) {
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
const le = await db.LegalEntity.filter({ canonicalId: targets.authScopeId, companyId: requesterCompanyId });
|
|
928
|
-
if (le.length > 0) matchFound = true;
|
|
929
|
-
}
|
|
930
|
-
if (!matchFound && (targets.expectedScopeType === "facility" || !targets.expectedScopeType)) {
|
|
931
|
-
const fac = await db.Facility.filter({ canonicalId: targets.authScopeId, companyId: requesterCompanyId });
|
|
932
|
-
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.");
|
|
933
944
|
}
|
|
934
|
-
if (
|
|
935
|
-
console.error(`[
|
|
936
|
-
throw ErrorFactory.forbidden(MOD.ADMIN, ACT.EDIT, "Invalid authorization scope level
|
|
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.");
|
|
937
948
|
}
|
|
938
949
|
}
|
|
939
950
|
return true;
|
|
940
951
|
}
|
|
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
|
+
}
|
|
963
|
+
}
|
|
941
964
|
export {
|
|
942
965
|
ACCESS_RANK,
|
|
943
966
|
ACT,
|
|
@@ -995,6 +1018,7 @@ export {
|
|
|
995
1018
|
canWrite,
|
|
996
1019
|
evaluateBaseAccess,
|
|
997
1020
|
extractAppCode,
|
|
1021
|
+
findSpecificOverride,
|
|
998
1022
|
generateCanonicalId,
|
|
999
1023
|
generateInfrastructureFields,
|
|
1000
1024
|
getAcceptedMimeTypes,
|
|
@@ -1010,5 +1034,6 @@ export {
|
|
|
1010
1034
|
resolveEffectiveAccess,
|
|
1011
1035
|
resolveError,
|
|
1012
1036
|
resolveMenuVisibility,
|
|
1037
|
+
verifyAndExtractSession,
|
|
1013
1038
|
withAgriLogging
|
|
1014
1039
|
};
|
package/package.json
CHANGED
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@litusarchieve18/agricore-utils",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.10",
|
|
4
4
|
"description": "Canonical Backend Utility Library for AgriCore",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
7
7
|
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.mjs",
|
|
12
|
+
"require": "./dist/index.js",
|
|
13
|
+
"default": "./dist/index.mjs"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
8
16
|
"files": [
|
|
9
17
|
"dist"
|
|
10
18
|
],
|
|
@@ -23,5 +31,7 @@
|
|
|
23
31
|
"tsup": "^8.0.0",
|
|
24
32
|
"typescript": "^5.0.0"
|
|
25
33
|
},
|
|
26
|
-
"dependencies": {
|
|
27
|
-
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"jose": "^6.2.2"
|
|
36
|
+
}
|
|
37
|
+
}
|