@naylence/advanced-security 0.3.15 → 0.4.1
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/LICENSE +1 -1
- package/dist/browser/index.cjs +2673 -3
- package/dist/browser/index.mjs +2684 -14
- package/dist/cjs/advanced-security-isomorphic.js +4 -0
- package/dist/cjs/advanced-security-isomorphic.js.map +1 -1
- package/dist/cjs/naylence/fame/expr/ast.js +135 -0
- package/dist/cjs/naylence/fame/expr/ast.js.map +1 -0
- package/dist/cjs/naylence/fame/expr/builtins.js +477 -0
- package/dist/cjs/naylence/fame/expr/builtins.js.map +1 -0
- package/dist/cjs/naylence/fame/expr/errors.js +88 -0
- package/dist/cjs/naylence/fame/expr/errors.js.map +1 -0
- package/dist/cjs/naylence/fame/expr/evaluator.js +385 -0
- package/dist/cjs/naylence/fame/expr/evaluator.js.map +1 -0
- package/dist/cjs/naylence/fame/expr/index.js +21 -0
- package/dist/cjs/naylence/fame/expr/index.js.map +1 -0
- package/dist/cjs/naylence/fame/expr/limits.js +80 -0
- package/dist/cjs/naylence/fame/expr/limits.js.map +1 -0
- package/dist/cjs/naylence/fame/expr/parser.js +429 -0
- package/dist/cjs/naylence/fame/expr/parser.js.map +1 -0
- package/dist/cjs/naylence/fame/expr/tokenizer.js +336 -0
- package/dist/cjs/naylence/fame/expr/tokenizer.js.map +1 -0
- package/dist/cjs/naylence/fame/factory-manifest.js +2 -0
- package/dist/cjs/naylence/fame/factory-manifest.js.map +1 -1
- package/dist/cjs/naylence/fame/security/auth/index.js +7 -0
- package/dist/cjs/naylence/fame/security/auth/index.js.map +1 -0
- package/dist/cjs/naylence/fame/security/auth/policy/advanced-authorization-policy-factory.js +70 -0
- package/dist/cjs/naylence/fame/security/auth/policy/advanced-authorization-policy-factory.js.map +1 -0
- package/dist/cjs/naylence/fame/security/auth/policy/advanced-authorization-policy.js +562 -0
- package/dist/cjs/naylence/fame/security/auth/policy/advanced-authorization-policy.js.map +1 -0
- package/dist/cjs/naylence/fame/security/auth/policy/expr-builtins.js +129 -0
- package/dist/cjs/naylence/fame/security/auth/policy/expr-builtins.js.map +1 -0
- package/dist/cjs/naylence/fame/security/auth/policy/index.js +15 -0
- package/dist/cjs/naylence/fame/security/auth/policy/index.js.map +1 -0
- package/dist/cjs/naylence/fame/security/index.js +2 -0
- package/dist/cjs/naylence/fame/security/index.js.map +1 -1
- package/dist/cjs/naylence/fame/security/register-advanced-security-factories.js +2 -0
- package/dist/cjs/naylence/fame/security/register-advanced-security-factories.js.map +1 -1
- package/dist/cjs/naylence/fame/security/strict-overlay-security-profile.js +64 -0
- package/dist/cjs/naylence/fame/security/strict-overlay-security-profile.js.map +1 -0
- package/dist/cjs/package.json +3 -0
- package/dist/cjs/plugin.js +2 -0
- package/dist/cjs/plugin.js.map +1 -1
- package/dist/cjs/version.js +2 -2
- package/dist/cjs/version.js.map +1 -1
- package/dist/esm/advanced-security-isomorphic.js +4 -0
- package/dist/esm/advanced-security-isomorphic.js.map +1 -1
- package/dist/esm/naylence/fame/expr/ast.js +135 -0
- package/dist/esm/naylence/fame/expr/ast.js.map +1 -0
- package/dist/esm/naylence/fame/expr/builtins.js +477 -0
- package/dist/esm/naylence/fame/expr/builtins.js.map +1 -0
- package/dist/esm/naylence/fame/expr/errors.js +88 -0
- package/dist/esm/naylence/fame/expr/errors.js.map +1 -0
- package/dist/esm/naylence/fame/expr/evaluator.js +385 -0
- package/dist/esm/naylence/fame/expr/evaluator.js.map +1 -0
- package/dist/esm/naylence/fame/expr/index.js +21 -0
- package/dist/esm/naylence/fame/expr/index.js.map +1 -0
- package/dist/esm/naylence/fame/expr/limits.js +80 -0
- package/dist/esm/naylence/fame/expr/limits.js.map +1 -0
- package/dist/esm/naylence/fame/expr/parser.js +429 -0
- package/dist/esm/naylence/fame/expr/parser.js.map +1 -0
- package/dist/esm/naylence/fame/expr/tokenizer.js +336 -0
- package/dist/esm/naylence/fame/expr/tokenizer.js.map +1 -0
- package/dist/esm/naylence/fame/factory-manifest.js +2 -0
- package/dist/esm/naylence/fame/factory-manifest.js.map +1 -1
- package/dist/esm/naylence/fame/security/auth/index.js +7 -0
- package/dist/esm/naylence/fame/security/auth/index.js.map +1 -0
- package/dist/esm/naylence/fame/security/auth/policy/advanced-authorization-policy-factory.js +70 -0
- package/dist/esm/naylence/fame/security/auth/policy/advanced-authorization-policy-factory.js.map +1 -0
- package/dist/esm/naylence/fame/security/auth/policy/advanced-authorization-policy.js +562 -0
- package/dist/esm/naylence/fame/security/auth/policy/advanced-authorization-policy.js.map +1 -0
- package/dist/esm/naylence/fame/security/auth/policy/expr-builtins.js +129 -0
- package/dist/esm/naylence/fame/security/auth/policy/expr-builtins.js.map +1 -0
- package/dist/esm/naylence/fame/security/auth/policy/index.js +15 -0
- package/dist/esm/naylence/fame/security/auth/policy/index.js.map +1 -0
- package/dist/esm/naylence/fame/security/index.js +2 -0
- package/dist/esm/naylence/fame/security/index.js.map +1 -1
- package/dist/esm/naylence/fame/security/register-advanced-security-factories.js +2 -0
- package/dist/esm/naylence/fame/security/register-advanced-security-factories.js.map +1 -1
- package/dist/esm/naylence/fame/security/strict-overlay-security-profile.js +64 -0
- package/dist/esm/naylence/fame/security/strict-overlay-security-profile.js.map +1 -0
- package/dist/esm/package.json +3 -0
- package/dist/esm/plugin.js +2 -0
- package/dist/esm/plugin.js.map +1 -1
- package/dist/esm/version.js +2 -2
- package/dist/esm/version.js.map +1 -1
- package/dist/node/index.cjs +2795 -6
- package/dist/node/index.mjs +2770 -15
- package/dist/node/node.cjs +2819 -3
- package/dist/node/node.mjs +2796 -15
- package/dist/types/advanced-security-isomorphic.d.ts +2 -0
- package/dist/types/advanced-security-isomorphic.d.ts.map +1 -1
- package/dist/types/naylence/fame/expr/ast.d.ts +85 -0
- package/dist/types/naylence/fame/expr/ast.d.ts.map +1 -0
- package/dist/types/naylence/fame/expr/builtins.d.ts +79 -0
- package/dist/types/naylence/fame/expr/builtins.d.ts.map +1 -0
- package/dist/types/naylence/fame/expr/errors.d.ts +61 -0
- package/dist/types/naylence/fame/expr/errors.d.ts.map +1 -0
- package/dist/types/naylence/fame/expr/evaluator.d.ts +90 -0
- package/dist/types/naylence/fame/expr/evaluator.d.ts.map +1 -0
- package/dist/types/naylence/fame/expr/index.d.ts +16 -0
- package/dist/types/naylence/fame/expr/index.d.ts.map +1 -0
- package/dist/types/naylence/fame/expr/limits.d.ts +65 -0
- package/dist/types/naylence/fame/expr/limits.d.ts.map +1 -0
- package/dist/types/naylence/fame/expr/parser.d.ts +102 -0
- package/dist/types/naylence/fame/expr/parser.d.ts.map +1 -0
- package/dist/types/naylence/fame/expr/tokenizer.d.ts +51 -0
- package/dist/types/naylence/fame/expr/tokenizer.d.ts.map +1 -0
- package/dist/types/naylence/fame/factory-manifest.d.ts +1 -1
- package/dist/types/naylence/fame/factory-manifest.d.ts.map +1 -1
- package/dist/types/naylence/fame/security/auth/index.d.ts +7 -0
- package/dist/types/naylence/fame/security/auth/index.d.ts.map +1 -0
- package/dist/types/naylence/fame/security/auth/policy/advanced-authorization-policy-factory.d.ts +47 -0
- package/dist/types/naylence/fame/security/auth/policy/advanced-authorization-policy-factory.d.ts.map +1 -0
- package/dist/types/naylence/fame/security/auth/policy/advanced-authorization-policy.d.ts +73 -0
- package/dist/types/naylence/fame/security/auth/policy/advanced-authorization-policy.d.ts.map +1 -0
- package/dist/types/naylence/fame/security/auth/policy/expr-builtins.d.ts +14 -0
- package/dist/types/naylence/fame/security/auth/policy/expr-builtins.d.ts.map +1 -0
- package/dist/types/naylence/fame/security/auth/policy/index.d.ts +12 -0
- package/dist/types/naylence/fame/security/auth/policy/index.d.ts.map +1 -0
- package/dist/types/naylence/fame/security/index.d.ts +2 -0
- package/dist/types/naylence/fame/security/index.d.ts.map +1 -1
- package/dist/types/naylence/fame/security/register-advanced-security-factories.d.ts +1 -0
- package/dist/types/naylence/fame/security/register-advanced-security-factories.d.ts.map +1 -1
- package/dist/types/naylence/fame/security/strict-overlay-security-profile.d.ts +11 -0
- package/dist/types/naylence/fame/security/strict-overlay-security-profile.d.ts.map +1 -0
- package/dist/types/plugin.d.ts.map +1 -1
- package/dist/types/version.d.ts +1 -1
- package/dist/types/version.d.ts.map +1 -1
- package/package.json +5 -4
package/dist/browser/index.cjs
CHANGED
|
@@ -2,12 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
var factory = require('@naylence/factory');
|
|
4
4
|
var runtime = require('@naylence/runtime');
|
|
5
|
+
var sha256 = require('@noble/hashes/sha256');
|
|
6
|
+
var core = require('@naylence/core');
|
|
5
7
|
var asn1Schema = require('@peculiar/asn1-schema');
|
|
6
8
|
var asn1Csr = require('@peculiar/asn1-csr');
|
|
7
9
|
var asn1X509 = require('@peculiar/asn1-x509');
|
|
8
10
|
var ed25519 = require('@noble/ed25519');
|
|
9
11
|
var sha2_js = require('@noble/hashes/sha2.js');
|
|
10
|
-
var core = require('@naylence/core');
|
|
11
12
|
var chacha_js = require('@noble/ciphers/chacha.js');
|
|
12
13
|
var ed25519_js = require('@noble/curves/ed25519.js');
|
|
13
14
|
var hkdf_js = require('@noble/hashes/hkdf.js');
|
|
@@ -23,6 +24,7 @@ var x509 = require('@peculiar/x509');
|
|
|
23
24
|
* Provides the list of advanced security factory modules for registration.
|
|
24
25
|
*/
|
|
25
26
|
const MODULES = [
|
|
27
|
+
"./security/auth/policy/advanced-authorization-policy-factory.js",
|
|
26
28
|
"./security/cert/default-ca-service-factory.js",
|
|
27
29
|
"./security/cert/default-certificate-manager-factory.js",
|
|
28
30
|
"./security/cert/trust-store/browser-trust-store-provider-factory.js",
|
|
@@ -39,6 +41,7 @@ const MODULES = [
|
|
|
39
41
|
"./welcome/advanced-welcome-service-factory.js"
|
|
40
42
|
];
|
|
41
43
|
const MODULE_LOADERS = {
|
|
44
|
+
"./security/auth/policy/advanced-authorization-policy-factory.js": () => Promise.resolve().then(function () { return advancedAuthorizationPolicyFactory; }),
|
|
42
45
|
"./security/cert/default-ca-service-factory.js": () => Promise.resolve().then(function () { return defaultCaServiceFactory; }),
|
|
43
46
|
"./security/cert/default-certificate-manager-factory.js": () => Promise.resolve().then(function () { return defaultCertificateManagerFactory; }),
|
|
44
47
|
"./security/cert/trust-store/browser-trust-store-provider-factory.js": () => Promise.resolve().then(function () { return browserTrustStoreProviderFactory; }),
|
|
@@ -216,6 +219,75 @@ function getEncryptionManagerFactoryRegistry() {
|
|
|
216
219
|
return globalRegistry;
|
|
217
220
|
}
|
|
218
221
|
|
|
222
|
+
/**
|
|
223
|
+
* Strict Overlay Security Profile
|
|
224
|
+
*
|
|
225
|
+
* Provides the strict-overlay security profile for advanced security scenarios.
|
|
226
|
+
* This profile requires X.509 certificate-based signing and supports both
|
|
227
|
+
* channel and sealed encryption modes.
|
|
228
|
+
*/
|
|
229
|
+
const ENV_VAR_DEFAULT_ENCRYPTION_LEVEL = "FAME_DEFAULT_ENCRYPTION_LEVEL";
|
|
230
|
+
const ENV_VAR_AUTHORIZATION_PROFILE = "FAME_AUTHORIZATION_PROFILE";
|
|
231
|
+
const PROFILE_NAME_STRICT_OVERLAY = "strict-overlay";
|
|
232
|
+
const STRICT_OVERLAY_PROFILE = {
|
|
233
|
+
type: "DefaultSecurityManager",
|
|
234
|
+
security_policy: {
|
|
235
|
+
type: "DefaultSecurityPolicy",
|
|
236
|
+
signing: {
|
|
237
|
+
signing_material: "x509-chain",
|
|
238
|
+
require_cert_sid_match: true,
|
|
239
|
+
inbound: {
|
|
240
|
+
signature_policy: "required",
|
|
241
|
+
unsigned_violation_action: "nack",
|
|
242
|
+
invalid_signature_action: "nack",
|
|
243
|
+
},
|
|
244
|
+
response: {
|
|
245
|
+
mirror_request_signing: true,
|
|
246
|
+
always_sign_responses: false,
|
|
247
|
+
sign_error_responses: true,
|
|
248
|
+
},
|
|
249
|
+
outbound: {
|
|
250
|
+
default_signing: true,
|
|
251
|
+
sign_sensitive_operations: true,
|
|
252
|
+
sign_if_recipient_expects: true,
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
encryption: {
|
|
256
|
+
inbound: {
|
|
257
|
+
allow_plaintext: true,
|
|
258
|
+
allow_channel: true,
|
|
259
|
+
allow_sealed: true,
|
|
260
|
+
plaintext_violation_action: "nack",
|
|
261
|
+
channel_violation_action: "nack",
|
|
262
|
+
sealed_violation_action: "nack",
|
|
263
|
+
},
|
|
264
|
+
response: {
|
|
265
|
+
mirror_request_level: true,
|
|
266
|
+
minimum_response_level: "plaintext",
|
|
267
|
+
escalate_sealed_responses: false,
|
|
268
|
+
},
|
|
269
|
+
outbound: {
|
|
270
|
+
default_level: factory.Expressions.env(ENV_VAR_DEFAULT_ENCRYPTION_LEVEL, "channel"),
|
|
271
|
+
escalate_if_peer_supports: false,
|
|
272
|
+
prefer_sealed_for_sensitive: false,
|
|
273
|
+
},
|
|
274
|
+
},
|
|
275
|
+
},
|
|
276
|
+
authorizer: {
|
|
277
|
+
type: "AuthorizationProfile",
|
|
278
|
+
profile: factory.Expressions.env(ENV_VAR_AUTHORIZATION_PROFILE, "jwt"),
|
|
279
|
+
},
|
|
280
|
+
};
|
|
281
|
+
// Register the strict-overlay profile
|
|
282
|
+
runtime.registerProfile(runtime.SECURITY_MANAGER_FACTORY_BASE_TYPE, PROFILE_NAME_STRICT_OVERLAY, STRICT_OVERLAY_PROFILE, { source: "advanced-security:strict-overlay-security-profile", allowOverride: true });
|
|
283
|
+
|
|
284
|
+
var strictOverlaySecurityProfile = /*#__PURE__*/Object.freeze({
|
|
285
|
+
__proto__: null,
|
|
286
|
+
ENV_VAR_AUTHORIZATION_PROFILE: ENV_VAR_AUTHORIZATION_PROFILE,
|
|
287
|
+
ENV_VAR_DEFAULT_ENCRYPTION_LEVEL: ENV_VAR_DEFAULT_ENCRYPTION_LEVEL,
|
|
288
|
+
PROFILE_NAME_STRICT_OVERLAY: PROFILE_NAME_STRICT_OVERLAY
|
|
289
|
+
});
|
|
290
|
+
|
|
219
291
|
const SECURITY_PREFIX = "./security/";
|
|
220
292
|
const SECURITY_MODULES = MODULES.filter((spec) => spec.startsWith(SECURITY_PREFIX));
|
|
221
293
|
const EXTRA_MODULES = MODULES.filter((spec) => !spec.startsWith(SECURITY_PREFIX));
|
|
@@ -500,12 +572,12 @@ async function registerAdvancedSecurityFactories(registrar = factory.Registry, o
|
|
|
500
572
|
}
|
|
501
573
|
|
|
502
574
|
// This file is auto-generated during build - do not edit manually
|
|
503
|
-
// Generated from package.json version: 0.
|
|
575
|
+
// Generated from package.json version: 0.4.1
|
|
504
576
|
/**
|
|
505
577
|
* The package version, injected at build time.
|
|
506
578
|
* @internal
|
|
507
579
|
*/
|
|
508
|
-
const VERSION = '0.
|
|
580
|
+
const VERSION = '0.4.1';
|
|
509
581
|
|
|
510
582
|
async function registerAdvancedSecurityPluginFactories(registrar = factory.Registry) {
|
|
511
583
|
await registerAdvancedSecurityFactories(registrar);
|
|
@@ -530,6 +602,8 @@ const advancedSecurityPlugin = {
|
|
|
530
602
|
try {
|
|
531
603
|
// console.log('[naylence:advanced-security] registering advanced security factories...');
|
|
532
604
|
await registerAdvancedSecurityPluginFactories();
|
|
605
|
+
// Import modules with side-effect registrations (not in manifest)
|
|
606
|
+
await Promise.resolve().then(function () { return strictOverlaySecurityProfile; });
|
|
533
607
|
// console.log('[naylence:advanced-security] advanced security factories registered');
|
|
534
608
|
initialized = true;
|
|
535
609
|
}
|
|
@@ -549,6 +623,2602 @@ var plugin = /*#__PURE__*/Object.freeze({
|
|
|
549
623
|
registerAdvancedSecurityPluginFactories: registerAdvancedSecurityPluginFactories
|
|
550
624
|
});
|
|
551
625
|
|
|
626
|
+
/**
|
|
627
|
+
* Abstract Syntax Tree (AST) node types for the expression language.
|
|
628
|
+
*
|
|
629
|
+
* The AST is produced by the parser and consumed by the evaluator.
|
|
630
|
+
*/
|
|
631
|
+
// ============================================================
|
|
632
|
+
// AST Utilities
|
|
633
|
+
// ============================================================
|
|
634
|
+
/**
|
|
635
|
+
* Counts the total number of nodes in an AST.
|
|
636
|
+
*/
|
|
637
|
+
function countAstNodes(node) {
|
|
638
|
+
let count = 1;
|
|
639
|
+
switch (node.type) {
|
|
640
|
+
case "StringLiteral":
|
|
641
|
+
case "NumberLiteral":
|
|
642
|
+
case "BooleanLiteral":
|
|
643
|
+
case "NullLiteral":
|
|
644
|
+
case "Identifier":
|
|
645
|
+
return count;
|
|
646
|
+
case "ArrayLiteral":
|
|
647
|
+
for (const element of node.elements) {
|
|
648
|
+
count += countAstNodes(element);
|
|
649
|
+
}
|
|
650
|
+
return count;
|
|
651
|
+
case "MemberAccess":
|
|
652
|
+
return count + countAstNodes(node.object);
|
|
653
|
+
case "IndexAccess":
|
|
654
|
+
return count + countAstNodes(node.object) + countAstNodes(node.index);
|
|
655
|
+
case "FunctionCall":
|
|
656
|
+
for (const arg of node.args) {
|
|
657
|
+
count += countAstNodes(arg);
|
|
658
|
+
}
|
|
659
|
+
return count;
|
|
660
|
+
case "UnaryOp":
|
|
661
|
+
return count + countAstNodes(node.operand);
|
|
662
|
+
case "BinaryOp":
|
|
663
|
+
return count + countAstNodes(node.left) + countAstNodes(node.right);
|
|
664
|
+
case "TernaryOp":
|
|
665
|
+
return (count +
|
|
666
|
+
countAstNodes(node.condition) +
|
|
667
|
+
countAstNodes(node.consequent) +
|
|
668
|
+
countAstNodes(node.alternate));
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
/**
|
|
672
|
+
* Calculates the maximum depth of an AST.
|
|
673
|
+
*/
|
|
674
|
+
function calculateAstDepth(node) {
|
|
675
|
+
switch (node.type) {
|
|
676
|
+
case "StringLiteral":
|
|
677
|
+
case "NumberLiteral":
|
|
678
|
+
case "BooleanLiteral":
|
|
679
|
+
case "NullLiteral":
|
|
680
|
+
case "Identifier":
|
|
681
|
+
return 1;
|
|
682
|
+
case "ArrayLiteral": {
|
|
683
|
+
let maxChildDepth = 0;
|
|
684
|
+
for (const element of node.elements) {
|
|
685
|
+
maxChildDepth = Math.max(maxChildDepth, calculateAstDepth(element));
|
|
686
|
+
}
|
|
687
|
+
return 1 + maxChildDepth;
|
|
688
|
+
}
|
|
689
|
+
case "MemberAccess":
|
|
690
|
+
return 1 + calculateAstDepth(node.object);
|
|
691
|
+
case "IndexAccess":
|
|
692
|
+
return (1 +
|
|
693
|
+
Math.max(calculateAstDepth(node.object), calculateAstDepth(node.index)));
|
|
694
|
+
case "FunctionCall": {
|
|
695
|
+
let maxArgDepth = 0;
|
|
696
|
+
for (const arg of node.args) {
|
|
697
|
+
maxArgDepth = Math.max(maxArgDepth, calculateAstDepth(arg));
|
|
698
|
+
}
|
|
699
|
+
return 1 + maxArgDepth;
|
|
700
|
+
}
|
|
701
|
+
case "UnaryOp":
|
|
702
|
+
return 1 + calculateAstDepth(node.operand);
|
|
703
|
+
case "BinaryOp":
|
|
704
|
+
return (1 +
|
|
705
|
+
Math.max(calculateAstDepth(node.left), calculateAstDepth(node.right)));
|
|
706
|
+
case "TernaryOp":
|
|
707
|
+
return (1 +
|
|
708
|
+
Math.max(calculateAstDepth(node.condition), calculateAstDepth(node.consequent), calculateAstDepth(node.alternate)));
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
/**
|
|
713
|
+
* Error types for the expression evaluation engine.
|
|
714
|
+
*
|
|
715
|
+
* All expression errors extend ExpressionError for consistent handling.
|
|
716
|
+
*/
|
|
717
|
+
/**
|
|
718
|
+
* Base error class for all expression-related errors.
|
|
719
|
+
*/
|
|
720
|
+
class ExpressionError extends Error {
|
|
721
|
+
constructor(message, position, expression) {
|
|
722
|
+
super(message);
|
|
723
|
+
this.name = "ExpressionError";
|
|
724
|
+
this.position = position;
|
|
725
|
+
this.expression = expression;
|
|
726
|
+
}
|
|
727
|
+
/**
|
|
728
|
+
* Returns a formatted error message with position context.
|
|
729
|
+
*/
|
|
730
|
+
formatWithContext() {
|
|
731
|
+
if (this.expression === undefined || this.position === undefined) {
|
|
732
|
+
return this.message;
|
|
733
|
+
}
|
|
734
|
+
const pointer = " ".repeat(this.position) + "^";
|
|
735
|
+
return `${this.message}\n ${this.expression}\n ${pointer}`;
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
/**
|
|
739
|
+
* Error thrown during tokenization (lexical analysis).
|
|
740
|
+
*/
|
|
741
|
+
class TokenizerError extends ExpressionError {
|
|
742
|
+
constructor(message, position, expression) {
|
|
743
|
+
super(message, position, expression);
|
|
744
|
+
this.name = "TokenizerError";
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
/**
|
|
748
|
+
* Error thrown during parsing (syntax analysis).
|
|
749
|
+
*/
|
|
750
|
+
class ParseError extends ExpressionError {
|
|
751
|
+
constructor(message, position, expression) {
|
|
752
|
+
super(message, position, expression);
|
|
753
|
+
this.name = "ParseError";
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
/**
|
|
757
|
+
* Error thrown during evaluation (runtime error).
|
|
758
|
+
*/
|
|
759
|
+
class EvaluationError extends ExpressionError {
|
|
760
|
+
constructor(message, position, expression, path) {
|
|
761
|
+
super(message, position, expression);
|
|
762
|
+
this.name = "EvaluationError";
|
|
763
|
+
this.path = path;
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
/**
|
|
767
|
+
* Error thrown for type mismatches during evaluation.
|
|
768
|
+
*/
|
|
769
|
+
class TypeError extends EvaluationError {
|
|
770
|
+
constructor(expected, actual, position, expression) {
|
|
771
|
+
super(`Type error: expected ${expected}, got ${actual}`, position, expression);
|
|
772
|
+
this.name = "TypeError";
|
|
773
|
+
this.expected = expected;
|
|
774
|
+
this.actual = actual;
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
/**
|
|
778
|
+
* Error thrown when a built-in function encounters an error.
|
|
779
|
+
*/
|
|
780
|
+
class BuiltinError extends EvaluationError {
|
|
781
|
+
constructor(functionName, message, position, expression) {
|
|
782
|
+
super(`${functionName}: ${message}`, position, expression);
|
|
783
|
+
this.name = "BuiltinError";
|
|
784
|
+
this.functionName = functionName;
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
/**
|
|
789
|
+
* Resource limits for expression parsing and evaluation.
|
|
790
|
+
*
|
|
791
|
+
* These limits protect against resource exhaustion attacks and
|
|
792
|
+
* overly complex expressions.
|
|
793
|
+
*/
|
|
794
|
+
/**
|
|
795
|
+
* Default expression limits.
|
|
796
|
+
*
|
|
797
|
+
* These values are chosen to allow reasonable expressions while
|
|
798
|
+
* preventing resource exhaustion.
|
|
799
|
+
*/
|
|
800
|
+
const DEFAULT_EXPRESSION_LIMITS = {
|
|
801
|
+
maxExpressionLength: 4096,
|
|
802
|
+
maxAstDepth: 32,
|
|
803
|
+
maxAstNodes: 256,
|
|
804
|
+
maxRegexPatternLength: 256,
|
|
805
|
+
maxGlobPatternLength: 256,
|
|
806
|
+
maxStringLength: 1024,
|
|
807
|
+
maxArrayLength: 64,
|
|
808
|
+
maxFunctionArgs: 16,
|
|
809
|
+
maxMemberAccessDepth: 16,
|
|
810
|
+
};
|
|
811
|
+
/**
|
|
812
|
+
* Validates that expression length is within limits.
|
|
813
|
+
*/
|
|
814
|
+
function checkExpressionLength(expression, limits = DEFAULT_EXPRESSION_LIMITS) {
|
|
815
|
+
if (expression.length > limits.maxExpressionLength) {
|
|
816
|
+
throw new Error(`Expression length ${expression.length} exceeds limit of ${limits.maxExpressionLength}`);
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
/**
|
|
820
|
+
* Validates AST depth during parsing.
|
|
821
|
+
*/
|
|
822
|
+
function checkAstDepth(depth, limits = DEFAULT_EXPRESSION_LIMITS) {
|
|
823
|
+
if (depth > limits.maxAstDepth) {
|
|
824
|
+
throw new Error(`AST depth ${depth} exceeds limit of ${limits.maxAstDepth}`);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
/**
|
|
828
|
+
* Validates AST node count during parsing.
|
|
829
|
+
*/
|
|
830
|
+
function checkAstNodeCount(count, limits = DEFAULT_EXPRESSION_LIMITS) {
|
|
831
|
+
if (count > limits.maxAstNodes) {
|
|
832
|
+
throw new Error(`AST node count ${count} exceeds limit of ${limits.maxAstNodes}`);
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
/**
|
|
836
|
+
* Validates regex pattern length before compilation.
|
|
837
|
+
*/
|
|
838
|
+
function checkRegexPatternLength(pattern, limits = DEFAULT_EXPRESSION_LIMITS) {
|
|
839
|
+
if (pattern.length > limits.maxRegexPatternLength) {
|
|
840
|
+
throw new Error(`Regex pattern length ${pattern.length} exceeds limit of ${limits.maxRegexPatternLength}`);
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
/**
|
|
844
|
+
* Validates glob pattern length before compilation.
|
|
845
|
+
*/
|
|
846
|
+
function checkGlobPatternLength(pattern, limits = DEFAULT_EXPRESSION_LIMITS) {
|
|
847
|
+
if (pattern.length > limits.maxGlobPatternLength) {
|
|
848
|
+
throw new Error(`Glob pattern length ${pattern.length} exceeds limit of ${limits.maxGlobPatternLength}`);
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
/**
|
|
852
|
+
* Validates array length during evaluation.
|
|
853
|
+
*/
|
|
854
|
+
function checkArrayLength(length, limits = DEFAULT_EXPRESSION_LIMITS) {
|
|
855
|
+
if (length > limits.maxArrayLength) {
|
|
856
|
+
throw new Error(`Array length ${length} exceeds limit of ${limits.maxArrayLength}`);
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
/**
|
|
860
|
+
* Validates function argument count.
|
|
861
|
+
*/
|
|
862
|
+
function checkFunctionArgCount(count, limits = DEFAULT_EXPRESSION_LIMITS) {
|
|
863
|
+
if (count > limits.maxFunctionArgs) {
|
|
864
|
+
throw new Error(`Function argument count ${count} exceeds limit of ${limits.maxFunctionArgs}`);
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
/**
|
|
869
|
+
* Tokenizer (lexer) for the expression language.
|
|
870
|
+
*
|
|
871
|
+
* Converts expression strings into a stream of tokens for the parser.
|
|
872
|
+
*/
|
|
873
|
+
/**
|
|
874
|
+
* Keywords recognized by the tokenizer.
|
|
875
|
+
*/
|
|
876
|
+
const KEYWORDS = new Map([
|
|
877
|
+
["true", "TRUE"],
|
|
878
|
+
["false", "FALSE"],
|
|
879
|
+
["null", "NULL"],
|
|
880
|
+
["in", "IN"],
|
|
881
|
+
["not", "NOT"],
|
|
882
|
+
]);
|
|
883
|
+
/**
|
|
884
|
+
* Checks if a character is a digit.
|
|
885
|
+
*/
|
|
886
|
+
function isDigit(ch) {
|
|
887
|
+
return ch >= "0" && ch <= "9";
|
|
888
|
+
}
|
|
889
|
+
/**
|
|
890
|
+
* Checks if a character can start an identifier.
|
|
891
|
+
*/
|
|
892
|
+
function isIdentifierStart(ch) {
|
|
893
|
+
return ((ch >= "a" && ch <= "z") ||
|
|
894
|
+
(ch >= "A" && ch <= "Z") ||
|
|
895
|
+
ch === "_");
|
|
896
|
+
}
|
|
897
|
+
/**
|
|
898
|
+
* Checks if a character can continue an identifier.
|
|
899
|
+
*/
|
|
900
|
+
function isIdentifierPart(ch) {
|
|
901
|
+
return isIdentifierStart(ch) || isDigit(ch);
|
|
902
|
+
}
|
|
903
|
+
/**
|
|
904
|
+
* Checks if a character is whitespace.
|
|
905
|
+
*/
|
|
906
|
+
function isWhitespace(ch) {
|
|
907
|
+
return ch === " " || ch === "\t" || ch === "\n" || ch === "\r";
|
|
908
|
+
}
|
|
909
|
+
/**
|
|
910
|
+
* Tokenizer for expression strings.
|
|
911
|
+
*/
|
|
912
|
+
class Tokenizer {
|
|
913
|
+
constructor(source, limits) {
|
|
914
|
+
this.position = 0;
|
|
915
|
+
this.tokens = [];
|
|
916
|
+
this.source = source;
|
|
917
|
+
this.limits = limits;
|
|
918
|
+
}
|
|
919
|
+
/**
|
|
920
|
+
* Tokenizes the source expression and returns all tokens.
|
|
921
|
+
*/
|
|
922
|
+
tokenize() {
|
|
923
|
+
checkExpressionLength(this.source, this.limits);
|
|
924
|
+
while (!this.isAtEnd()) {
|
|
925
|
+
this.scanToken();
|
|
926
|
+
}
|
|
927
|
+
this.tokens.push({
|
|
928
|
+
type: "EOF",
|
|
929
|
+
value: "",
|
|
930
|
+
position: this.position,
|
|
931
|
+
});
|
|
932
|
+
return this.tokens;
|
|
933
|
+
}
|
|
934
|
+
isAtEnd() {
|
|
935
|
+
return this.position >= this.source.length;
|
|
936
|
+
}
|
|
937
|
+
peek() {
|
|
938
|
+
if (this.isAtEnd())
|
|
939
|
+
return "\0";
|
|
940
|
+
return this.source[this.position];
|
|
941
|
+
}
|
|
942
|
+
peekNext() {
|
|
943
|
+
if (this.position + 1 >= this.source.length)
|
|
944
|
+
return "\0";
|
|
945
|
+
return this.source[this.position + 1];
|
|
946
|
+
}
|
|
947
|
+
advance() {
|
|
948
|
+
return this.source[this.position++];
|
|
949
|
+
}
|
|
950
|
+
addToken(type, value, position) {
|
|
951
|
+
this.tokens.push({ type, value, position });
|
|
952
|
+
}
|
|
953
|
+
scanToken() {
|
|
954
|
+
const ch = this.advance();
|
|
955
|
+
const startPosition = this.position - 1;
|
|
956
|
+
// Skip whitespace
|
|
957
|
+
if (isWhitespace(ch)) {
|
|
958
|
+
return;
|
|
959
|
+
}
|
|
960
|
+
// Single-character tokens
|
|
961
|
+
switch (ch) {
|
|
962
|
+
case "(":
|
|
963
|
+
this.addToken("LPAREN", "(", startPosition);
|
|
964
|
+
return;
|
|
965
|
+
case ")":
|
|
966
|
+
this.addToken("RPAREN", ")", startPosition);
|
|
967
|
+
return;
|
|
968
|
+
case "[":
|
|
969
|
+
this.addToken("LBRACKET", "[", startPosition);
|
|
970
|
+
return;
|
|
971
|
+
case "]":
|
|
972
|
+
this.addToken("RBRACKET", "]", startPosition);
|
|
973
|
+
return;
|
|
974
|
+
case ".":
|
|
975
|
+
this.addToken("DOT", ".", startPosition);
|
|
976
|
+
return;
|
|
977
|
+
case ",":
|
|
978
|
+
this.addToken("COMMA", ",", startPosition);
|
|
979
|
+
return;
|
|
980
|
+
case "+":
|
|
981
|
+
this.addToken("PLUS", "+", startPosition);
|
|
982
|
+
return;
|
|
983
|
+
case "-":
|
|
984
|
+
this.addToken("MINUS", "-", startPosition);
|
|
985
|
+
return;
|
|
986
|
+
case "*":
|
|
987
|
+
this.addToken("STAR", "*", startPosition);
|
|
988
|
+
return;
|
|
989
|
+
case "/":
|
|
990
|
+
this.addToken("SLASH", "/", startPosition);
|
|
991
|
+
return;
|
|
992
|
+
case "%":
|
|
993
|
+
this.addToken("PERCENT", "%", startPosition);
|
|
994
|
+
return;
|
|
995
|
+
case "?":
|
|
996
|
+
this.addToken("QUESTION", "?", startPosition);
|
|
997
|
+
return;
|
|
998
|
+
case ":":
|
|
999
|
+
this.addToken("COLON", ":", startPosition);
|
|
1000
|
+
return;
|
|
1001
|
+
}
|
|
1002
|
+
// Two-character operators
|
|
1003
|
+
if (ch === "<") {
|
|
1004
|
+
if (this.peek() === "=") {
|
|
1005
|
+
this.advance();
|
|
1006
|
+
this.addToken("LE", "<=", startPosition);
|
|
1007
|
+
}
|
|
1008
|
+
else {
|
|
1009
|
+
this.addToken("LT", "<", startPosition);
|
|
1010
|
+
}
|
|
1011
|
+
return;
|
|
1012
|
+
}
|
|
1013
|
+
if (ch === ">") {
|
|
1014
|
+
if (this.peek() === "=") {
|
|
1015
|
+
this.advance();
|
|
1016
|
+
this.addToken("GE", ">=", startPosition);
|
|
1017
|
+
}
|
|
1018
|
+
else {
|
|
1019
|
+
this.addToken("GT", ">", startPosition);
|
|
1020
|
+
}
|
|
1021
|
+
return;
|
|
1022
|
+
}
|
|
1023
|
+
if (ch === "=") {
|
|
1024
|
+
if (this.peek() === "=") {
|
|
1025
|
+
this.advance();
|
|
1026
|
+
this.addToken("EQ", "==", startPosition);
|
|
1027
|
+
return;
|
|
1028
|
+
}
|
|
1029
|
+
throw new TokenizerError("Unexpected '='. Did you mean '=='?", startPosition, this.source);
|
|
1030
|
+
}
|
|
1031
|
+
if (ch === "!") {
|
|
1032
|
+
if (this.peek() === "=") {
|
|
1033
|
+
this.advance();
|
|
1034
|
+
this.addToken("NE", "!=", startPosition);
|
|
1035
|
+
}
|
|
1036
|
+
else {
|
|
1037
|
+
this.addToken("NOT", "!", startPosition);
|
|
1038
|
+
}
|
|
1039
|
+
return;
|
|
1040
|
+
}
|
|
1041
|
+
if (ch === "&") {
|
|
1042
|
+
if (this.peek() === "&") {
|
|
1043
|
+
this.advance();
|
|
1044
|
+
this.addToken("AND", "&&", startPosition);
|
|
1045
|
+
return;
|
|
1046
|
+
}
|
|
1047
|
+
throw new TokenizerError("Unexpected '&'. Did you mean '&&'?", startPosition, this.source);
|
|
1048
|
+
}
|
|
1049
|
+
if (ch === "|") {
|
|
1050
|
+
if (this.peek() === "|") {
|
|
1051
|
+
this.advance();
|
|
1052
|
+
this.addToken("OR", "||", startPosition);
|
|
1053
|
+
return;
|
|
1054
|
+
}
|
|
1055
|
+
throw new TokenizerError("Unexpected '|'. Did you mean '||'?", startPosition, this.source);
|
|
1056
|
+
}
|
|
1057
|
+
// String literals
|
|
1058
|
+
if (ch === '"' || ch === "'") {
|
|
1059
|
+
this.scanString(ch, startPosition);
|
|
1060
|
+
return;
|
|
1061
|
+
}
|
|
1062
|
+
// Number literals
|
|
1063
|
+
if (isDigit(ch)) {
|
|
1064
|
+
this.scanNumber(startPosition);
|
|
1065
|
+
return;
|
|
1066
|
+
}
|
|
1067
|
+
// Identifiers and keywords
|
|
1068
|
+
if (isIdentifierStart(ch)) {
|
|
1069
|
+
this.scanIdentifier(startPosition);
|
|
1070
|
+
return;
|
|
1071
|
+
}
|
|
1072
|
+
throw new TokenizerError(`Unexpected character: '${ch}'`, startPosition, this.source);
|
|
1073
|
+
}
|
|
1074
|
+
scanString(quote, startPosition) {
|
|
1075
|
+
let value = "";
|
|
1076
|
+
while (!this.isAtEnd() && this.peek() !== quote) {
|
|
1077
|
+
const ch = this.advance();
|
|
1078
|
+
if (ch === "\\") {
|
|
1079
|
+
// Escape sequence
|
|
1080
|
+
if (this.isAtEnd()) {
|
|
1081
|
+
throw new TokenizerError("Unterminated string", startPosition, this.source);
|
|
1082
|
+
}
|
|
1083
|
+
const escaped = this.advance();
|
|
1084
|
+
switch (escaped) {
|
|
1085
|
+
case "n":
|
|
1086
|
+
value += "\n";
|
|
1087
|
+
break;
|
|
1088
|
+
case "r":
|
|
1089
|
+
value += "\r";
|
|
1090
|
+
break;
|
|
1091
|
+
case "t":
|
|
1092
|
+
value += "\t";
|
|
1093
|
+
break;
|
|
1094
|
+
case "\\":
|
|
1095
|
+
value += "\\";
|
|
1096
|
+
break;
|
|
1097
|
+
case '"':
|
|
1098
|
+
value += '"';
|
|
1099
|
+
break;
|
|
1100
|
+
case "'":
|
|
1101
|
+
value += "'";
|
|
1102
|
+
break;
|
|
1103
|
+
default:
|
|
1104
|
+
throw new TokenizerError(`Invalid escape sequence: \\${escaped}`, this.position - 2, this.source);
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
else if (ch === "\n" || ch === "\r") {
|
|
1108
|
+
throw new TokenizerError("Unterminated string (newline in string literal)", startPosition, this.source);
|
|
1109
|
+
}
|
|
1110
|
+
else {
|
|
1111
|
+
value += ch;
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
if (this.isAtEnd()) {
|
|
1115
|
+
throw new TokenizerError("Unterminated string", startPosition, this.source);
|
|
1116
|
+
}
|
|
1117
|
+
// Consume closing quote
|
|
1118
|
+
this.advance();
|
|
1119
|
+
this.addToken("STRING", value, startPosition);
|
|
1120
|
+
}
|
|
1121
|
+
scanNumber(startPosition) {
|
|
1122
|
+
// Back up to include the first digit
|
|
1123
|
+
this.position--;
|
|
1124
|
+
let value = "";
|
|
1125
|
+
// Integer part
|
|
1126
|
+
while (isDigit(this.peek())) {
|
|
1127
|
+
value += this.advance();
|
|
1128
|
+
}
|
|
1129
|
+
// Fractional part
|
|
1130
|
+
if (this.peek() === "." && isDigit(this.peekNext())) {
|
|
1131
|
+
value += this.advance(); // consume '.'
|
|
1132
|
+
while (isDigit(this.peek())) {
|
|
1133
|
+
value += this.advance();
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
// Exponent part
|
|
1137
|
+
if (this.peek() === "e" || this.peek() === "E") {
|
|
1138
|
+
value += this.advance();
|
|
1139
|
+
if (this.peek() === "+" || this.peek() === "-") {
|
|
1140
|
+
value += this.advance();
|
|
1141
|
+
}
|
|
1142
|
+
if (!isDigit(this.peek())) {
|
|
1143
|
+
throw new TokenizerError("Invalid number: expected exponent digits", startPosition, this.source);
|
|
1144
|
+
}
|
|
1145
|
+
while (isDigit(this.peek())) {
|
|
1146
|
+
value += this.advance();
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
this.addToken("NUMBER", value, startPosition);
|
|
1150
|
+
}
|
|
1151
|
+
scanIdentifier(startPosition) {
|
|
1152
|
+
// Back up to include the first character
|
|
1153
|
+
this.position--;
|
|
1154
|
+
let value = "";
|
|
1155
|
+
while (isIdentifierPart(this.peek())) {
|
|
1156
|
+
value += this.advance();
|
|
1157
|
+
}
|
|
1158
|
+
// Check for "not in" compound keyword
|
|
1159
|
+
const valueLower = value.toLowerCase();
|
|
1160
|
+
if (valueLower === "not") {
|
|
1161
|
+
// Check if followed by whitespace and "in"
|
|
1162
|
+
const savedPosition = this.position;
|
|
1163
|
+
// Skip whitespace
|
|
1164
|
+
while (isWhitespace(this.peek())) {
|
|
1165
|
+
this.advance();
|
|
1166
|
+
}
|
|
1167
|
+
// Check for "in"
|
|
1168
|
+
if (this.peek() === "i" &&
|
|
1169
|
+
this.peekNext() === "n" &&
|
|
1170
|
+
!isIdentifierPart(this.source[this.position + 2] ?? "\0")) {
|
|
1171
|
+
this.advance(); // consume 'i'
|
|
1172
|
+
this.advance(); // consume 'n'
|
|
1173
|
+
this.addToken("NOT_IN", "not in", startPosition);
|
|
1174
|
+
return;
|
|
1175
|
+
}
|
|
1176
|
+
// Not "not in", restore position
|
|
1177
|
+
this.position = savedPosition;
|
|
1178
|
+
}
|
|
1179
|
+
// Check for keyword
|
|
1180
|
+
const keywordType = KEYWORDS.get(valueLower);
|
|
1181
|
+
if (keywordType) {
|
|
1182
|
+
this.addToken(keywordType, value, startPosition);
|
|
1183
|
+
}
|
|
1184
|
+
else {
|
|
1185
|
+
this.addToken("IDENTIFIER", value, startPosition);
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
/**
|
|
1190
|
+
* Tokenizes an expression string into tokens.
|
|
1191
|
+
*
|
|
1192
|
+
* @param source - The expression string to tokenize
|
|
1193
|
+
* @param limits - Optional expression limits
|
|
1194
|
+
* @returns Array of tokens
|
|
1195
|
+
* @throws TokenizerError if the expression contains invalid tokens
|
|
1196
|
+
*/
|
|
1197
|
+
function tokenize(source, limits) {
|
|
1198
|
+
const tokenizer = new Tokenizer(source, limits);
|
|
1199
|
+
return tokenizer.tokenize();
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
/**
|
|
1203
|
+
* Parser for the expression language.
|
|
1204
|
+
*
|
|
1205
|
+
* Parses a stream of tokens into an Abstract Syntax Tree (AST).
|
|
1206
|
+
* Uses recursive descent parsing with operator precedence.
|
|
1207
|
+
*
|
|
1208
|
+
* Precedence (lowest to highest):
|
|
1209
|
+
* 1. Ternary: ? :
|
|
1210
|
+
* 2. Logical OR: ||
|
|
1211
|
+
* 3. Logical AND: &&
|
|
1212
|
+
* 4. Membership: in, not in
|
|
1213
|
+
* 5. Equality: ==, !=
|
|
1214
|
+
* 6. Comparison: <, <=, >, >=
|
|
1215
|
+
* 7. Additive: +, -
|
|
1216
|
+
* 8. Multiplicative: *, /, %
|
|
1217
|
+
* 9. Unary: !, -
|
|
1218
|
+
* 10. Postfix: . [] ()
|
|
1219
|
+
* 11. Primary: literals, identifiers, parentheses
|
|
1220
|
+
*/
|
|
1221
|
+
/**
|
|
1222
|
+
* Parser for expression strings.
|
|
1223
|
+
*/
|
|
1224
|
+
class Parser {
|
|
1225
|
+
constructor(tokens, source, limits = DEFAULT_EXPRESSION_LIMITS) {
|
|
1226
|
+
this.current = 0;
|
|
1227
|
+
this.tokens = tokens;
|
|
1228
|
+
this.source = source;
|
|
1229
|
+
this.limits = limits;
|
|
1230
|
+
}
|
|
1231
|
+
/**
|
|
1232
|
+
* Parses the token stream into an AST.
|
|
1233
|
+
*/
|
|
1234
|
+
parse() {
|
|
1235
|
+
const ast = this.parseTernary();
|
|
1236
|
+
if (!this.isAtEnd()) {
|
|
1237
|
+
const token = this.peek();
|
|
1238
|
+
throw new ParseError(`Unexpected token: ${token.value || token.type}`, token.position, this.source);
|
|
1239
|
+
}
|
|
1240
|
+
// Validate AST limits
|
|
1241
|
+
const nodeCount = countAstNodes(ast);
|
|
1242
|
+
checkAstNodeCount(nodeCount, this.limits);
|
|
1243
|
+
const depth = calculateAstDepth(ast);
|
|
1244
|
+
checkAstDepth(depth, this.limits);
|
|
1245
|
+
return ast;
|
|
1246
|
+
}
|
|
1247
|
+
// ============================================================
|
|
1248
|
+
// Token Helpers
|
|
1249
|
+
// ============================================================
|
|
1250
|
+
isAtEnd() {
|
|
1251
|
+
return this.peek().type === "EOF";
|
|
1252
|
+
}
|
|
1253
|
+
peek() {
|
|
1254
|
+
return this.tokens[this.current];
|
|
1255
|
+
}
|
|
1256
|
+
previous() {
|
|
1257
|
+
return this.tokens[this.current - 1];
|
|
1258
|
+
}
|
|
1259
|
+
advance() {
|
|
1260
|
+
if (!this.isAtEnd()) {
|
|
1261
|
+
this.current++;
|
|
1262
|
+
}
|
|
1263
|
+
return this.previous();
|
|
1264
|
+
}
|
|
1265
|
+
check(type) {
|
|
1266
|
+
if (this.isAtEnd())
|
|
1267
|
+
return false;
|
|
1268
|
+
return this.peek().type === type;
|
|
1269
|
+
}
|
|
1270
|
+
match(...types) {
|
|
1271
|
+
for (const type of types) {
|
|
1272
|
+
if (this.check(type)) {
|
|
1273
|
+
this.advance();
|
|
1274
|
+
return true;
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
return false;
|
|
1278
|
+
}
|
|
1279
|
+
consume(type, message) {
|
|
1280
|
+
if (this.check(type)) {
|
|
1281
|
+
return this.advance();
|
|
1282
|
+
}
|
|
1283
|
+
const token = this.peek();
|
|
1284
|
+
throw new ParseError(message, token.position, this.source);
|
|
1285
|
+
}
|
|
1286
|
+
// ============================================================
|
|
1287
|
+
// Expression Parsing (by precedence, lowest to highest)
|
|
1288
|
+
// ============================================================
|
|
1289
|
+
/**
|
|
1290
|
+
* Parses ternary expressions: condition ? consequent : alternate
|
|
1291
|
+
*/
|
|
1292
|
+
parseTernary() {
|
|
1293
|
+
const position = this.peek().position;
|
|
1294
|
+
let node = this.parseOr();
|
|
1295
|
+
if (this.match("QUESTION")) {
|
|
1296
|
+
const consequent = this.parseTernary();
|
|
1297
|
+
this.consume("COLON", "Expected ':' in ternary expression");
|
|
1298
|
+
const alternate = this.parseTernary();
|
|
1299
|
+
node = {
|
|
1300
|
+
type: "TernaryOp",
|
|
1301
|
+
position,
|
|
1302
|
+
condition: node,
|
|
1303
|
+
consequent,
|
|
1304
|
+
alternate,
|
|
1305
|
+
};
|
|
1306
|
+
}
|
|
1307
|
+
return node;
|
|
1308
|
+
}
|
|
1309
|
+
/**
|
|
1310
|
+
* Parses logical OR: ||
|
|
1311
|
+
*/
|
|
1312
|
+
parseOr() {
|
|
1313
|
+
let node = this.parseAnd();
|
|
1314
|
+
while (this.match("OR")) {
|
|
1315
|
+
const position = this.previous().position;
|
|
1316
|
+
const right = this.parseAnd();
|
|
1317
|
+
node = {
|
|
1318
|
+
type: "BinaryOp",
|
|
1319
|
+
position,
|
|
1320
|
+
operator: "||",
|
|
1321
|
+
left: node,
|
|
1322
|
+
right,
|
|
1323
|
+
};
|
|
1324
|
+
}
|
|
1325
|
+
return node;
|
|
1326
|
+
}
|
|
1327
|
+
/**
|
|
1328
|
+
* Parses logical AND: &&
|
|
1329
|
+
*/
|
|
1330
|
+
parseAnd() {
|
|
1331
|
+
let node = this.parseEquality();
|
|
1332
|
+
while (this.match("AND")) {
|
|
1333
|
+
const position = this.previous().position;
|
|
1334
|
+
const right = this.parseEquality();
|
|
1335
|
+
node = {
|
|
1336
|
+
type: "BinaryOp",
|
|
1337
|
+
position,
|
|
1338
|
+
operator: "&&",
|
|
1339
|
+
left: node,
|
|
1340
|
+
right,
|
|
1341
|
+
};
|
|
1342
|
+
}
|
|
1343
|
+
return node;
|
|
1344
|
+
}
|
|
1345
|
+
/**
|
|
1346
|
+
* Parses equality: ==, !=
|
|
1347
|
+
*/
|
|
1348
|
+
parseEquality() {
|
|
1349
|
+
let node = this.parseMembership();
|
|
1350
|
+
while (this.match("EQ", "NE")) {
|
|
1351
|
+
const operator = this.previous().type === "EQ" ? "==" : "!=";
|
|
1352
|
+
const position = this.previous().position;
|
|
1353
|
+
const right = this.parseMembership();
|
|
1354
|
+
node = {
|
|
1355
|
+
type: "BinaryOp",
|
|
1356
|
+
position,
|
|
1357
|
+
operator: operator,
|
|
1358
|
+
left: node,
|
|
1359
|
+
right,
|
|
1360
|
+
};
|
|
1361
|
+
}
|
|
1362
|
+
return node;
|
|
1363
|
+
}
|
|
1364
|
+
/**
|
|
1365
|
+
* Parses membership: in, not in
|
|
1366
|
+
*/
|
|
1367
|
+
parseMembership() {
|
|
1368
|
+
let node = this.parseComparison();
|
|
1369
|
+
while (this.match("IN", "NOT_IN")) {
|
|
1370
|
+
const operator = this.previous().type === "IN" ? "in" : "not in";
|
|
1371
|
+
const position = this.previous().position;
|
|
1372
|
+
const right = this.parseComparison();
|
|
1373
|
+
node = {
|
|
1374
|
+
type: "BinaryOp",
|
|
1375
|
+
position,
|
|
1376
|
+
operator: operator,
|
|
1377
|
+
left: node,
|
|
1378
|
+
right,
|
|
1379
|
+
};
|
|
1380
|
+
}
|
|
1381
|
+
return node;
|
|
1382
|
+
}
|
|
1383
|
+
/**
|
|
1384
|
+
* Parses comparison: <, <=, >, >=
|
|
1385
|
+
*/
|
|
1386
|
+
parseComparison() {
|
|
1387
|
+
let node = this.parseAdditive();
|
|
1388
|
+
while (this.match("LT", "LE", "GT", "GE")) {
|
|
1389
|
+
const token = this.previous();
|
|
1390
|
+
let operator;
|
|
1391
|
+
switch (token.type) {
|
|
1392
|
+
case "LT":
|
|
1393
|
+
operator = "<";
|
|
1394
|
+
break;
|
|
1395
|
+
case "LE":
|
|
1396
|
+
operator = "<=";
|
|
1397
|
+
break;
|
|
1398
|
+
case "GT":
|
|
1399
|
+
operator = ">";
|
|
1400
|
+
break;
|
|
1401
|
+
case "GE":
|
|
1402
|
+
operator = ">=";
|
|
1403
|
+
break;
|
|
1404
|
+
default:
|
|
1405
|
+
throw new ParseError(`Unexpected token: ${token.type}`, token.position, this.source);
|
|
1406
|
+
}
|
|
1407
|
+
const position = token.position;
|
|
1408
|
+
const right = this.parseAdditive();
|
|
1409
|
+
node = {
|
|
1410
|
+
type: "BinaryOp",
|
|
1411
|
+
position,
|
|
1412
|
+
operator,
|
|
1413
|
+
left: node,
|
|
1414
|
+
right,
|
|
1415
|
+
};
|
|
1416
|
+
}
|
|
1417
|
+
return node;
|
|
1418
|
+
}
|
|
1419
|
+
/**
|
|
1420
|
+
* Parses additive: +, -
|
|
1421
|
+
*/
|
|
1422
|
+
parseAdditive() {
|
|
1423
|
+
let node = this.parseMultiplicative();
|
|
1424
|
+
while (this.match("PLUS", "MINUS")) {
|
|
1425
|
+
const operator = this.previous().type === "PLUS" ? "+" : "-";
|
|
1426
|
+
const position = this.previous().position;
|
|
1427
|
+
const right = this.parseMultiplicative();
|
|
1428
|
+
node = {
|
|
1429
|
+
type: "BinaryOp",
|
|
1430
|
+
position,
|
|
1431
|
+
operator: operator,
|
|
1432
|
+
left: node,
|
|
1433
|
+
right,
|
|
1434
|
+
};
|
|
1435
|
+
}
|
|
1436
|
+
return node;
|
|
1437
|
+
}
|
|
1438
|
+
/**
|
|
1439
|
+
* Parses multiplicative: *, /, %
|
|
1440
|
+
*/
|
|
1441
|
+
parseMultiplicative() {
|
|
1442
|
+
let node = this.parseUnary();
|
|
1443
|
+
while (this.match("STAR", "SLASH", "PERCENT")) {
|
|
1444
|
+
const token = this.previous();
|
|
1445
|
+
let operator;
|
|
1446
|
+
switch (token.type) {
|
|
1447
|
+
case "STAR":
|
|
1448
|
+
operator = "*";
|
|
1449
|
+
break;
|
|
1450
|
+
case "SLASH":
|
|
1451
|
+
operator = "/";
|
|
1452
|
+
break;
|
|
1453
|
+
case "PERCENT":
|
|
1454
|
+
operator = "%";
|
|
1455
|
+
break;
|
|
1456
|
+
default:
|
|
1457
|
+
throw new ParseError(`Unexpected token: ${token.type}`, token.position, this.source);
|
|
1458
|
+
}
|
|
1459
|
+
const position = token.position;
|
|
1460
|
+
const right = this.parseUnary();
|
|
1461
|
+
node = {
|
|
1462
|
+
type: "BinaryOp",
|
|
1463
|
+
position,
|
|
1464
|
+
operator,
|
|
1465
|
+
left: node,
|
|
1466
|
+
right,
|
|
1467
|
+
};
|
|
1468
|
+
}
|
|
1469
|
+
return node;
|
|
1470
|
+
}
|
|
1471
|
+
/**
|
|
1472
|
+
* Parses unary: !, -
|
|
1473
|
+
*/
|
|
1474
|
+
parseUnary() {
|
|
1475
|
+
if (this.match("NOT", "MINUS")) {
|
|
1476
|
+
const token = this.previous();
|
|
1477
|
+
const operator = token.type === "NOT" ? "!" : "-";
|
|
1478
|
+
const position = token.position;
|
|
1479
|
+
const operand = this.parseUnary();
|
|
1480
|
+
return {
|
|
1481
|
+
type: "UnaryOp",
|
|
1482
|
+
position,
|
|
1483
|
+
operator,
|
|
1484
|
+
operand,
|
|
1485
|
+
};
|
|
1486
|
+
}
|
|
1487
|
+
return this.parsePostfix();
|
|
1488
|
+
}
|
|
1489
|
+
/**
|
|
1490
|
+
* Parses postfix: . [] ()
|
|
1491
|
+
*/
|
|
1492
|
+
parsePostfix() {
|
|
1493
|
+
let node = this.parsePrimary();
|
|
1494
|
+
while (true) {
|
|
1495
|
+
if (this.match("DOT")) {
|
|
1496
|
+
const position = this.previous().position;
|
|
1497
|
+
const propToken = this.consume("IDENTIFIER", "Expected property name after '.'");
|
|
1498
|
+
node = {
|
|
1499
|
+
type: "MemberAccess",
|
|
1500
|
+
position,
|
|
1501
|
+
object: node,
|
|
1502
|
+
property: propToken.value,
|
|
1503
|
+
};
|
|
1504
|
+
}
|
|
1505
|
+
else if (this.match("LBRACKET")) {
|
|
1506
|
+
const position = this.previous().position;
|
|
1507
|
+
const index = this.parseTernary();
|
|
1508
|
+
this.consume("RBRACKET", "Expected ']' after index");
|
|
1509
|
+
node = {
|
|
1510
|
+
type: "IndexAccess",
|
|
1511
|
+
position,
|
|
1512
|
+
object: node,
|
|
1513
|
+
index,
|
|
1514
|
+
};
|
|
1515
|
+
}
|
|
1516
|
+
else if (this.match("LPAREN")) {
|
|
1517
|
+
// Function call - node must be an identifier
|
|
1518
|
+
if (node.type !== "Identifier") {
|
|
1519
|
+
throw new ParseError("Only named functions can be called", node.position, this.source);
|
|
1520
|
+
}
|
|
1521
|
+
const position = this.previous().position;
|
|
1522
|
+
const args = this.parseArgumentList();
|
|
1523
|
+
checkFunctionArgCount(args.length, this.limits);
|
|
1524
|
+
node = {
|
|
1525
|
+
type: "FunctionCall",
|
|
1526
|
+
position,
|
|
1527
|
+
name: node.name,
|
|
1528
|
+
args,
|
|
1529
|
+
};
|
|
1530
|
+
}
|
|
1531
|
+
else {
|
|
1532
|
+
break;
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
return node;
|
|
1536
|
+
}
|
|
1537
|
+
/**
|
|
1538
|
+
* Parses function argument list (already consumed opening paren).
|
|
1539
|
+
*/
|
|
1540
|
+
parseArgumentList() {
|
|
1541
|
+
const args = [];
|
|
1542
|
+
if (!this.check("RPAREN")) {
|
|
1543
|
+
do {
|
|
1544
|
+
args.push(this.parseTernary());
|
|
1545
|
+
} while (this.match("COMMA"));
|
|
1546
|
+
}
|
|
1547
|
+
this.consume("RPAREN", "Expected ')' after function arguments");
|
|
1548
|
+
return args;
|
|
1549
|
+
}
|
|
1550
|
+
/**
|
|
1551
|
+
* Parses primary expressions: literals, identifiers, parentheses, arrays.
|
|
1552
|
+
*/
|
|
1553
|
+
parsePrimary() {
|
|
1554
|
+
const token = this.peek();
|
|
1555
|
+
const position = token.position;
|
|
1556
|
+
// Boolean literals
|
|
1557
|
+
if (this.match("TRUE")) {
|
|
1558
|
+
return { type: "BooleanLiteral", position, value: true };
|
|
1559
|
+
}
|
|
1560
|
+
if (this.match("FALSE")) {
|
|
1561
|
+
return { type: "BooleanLiteral", position, value: false };
|
|
1562
|
+
}
|
|
1563
|
+
// Null literal
|
|
1564
|
+
if (this.match("NULL")) {
|
|
1565
|
+
return { type: "NullLiteral", position };
|
|
1566
|
+
}
|
|
1567
|
+
// String literal
|
|
1568
|
+
if (this.match("STRING")) {
|
|
1569
|
+
return {
|
|
1570
|
+
type: "StringLiteral",
|
|
1571
|
+
position,
|
|
1572
|
+
value: this.previous().value,
|
|
1573
|
+
};
|
|
1574
|
+
}
|
|
1575
|
+
// Number literal
|
|
1576
|
+
if (this.match("NUMBER")) {
|
|
1577
|
+
const value = parseFloat(this.previous().value);
|
|
1578
|
+
if (!Number.isFinite(value)) {
|
|
1579
|
+
throw new ParseError("Invalid number", position, this.source);
|
|
1580
|
+
}
|
|
1581
|
+
return { type: "NumberLiteral", position, value };
|
|
1582
|
+
}
|
|
1583
|
+
// Identifier
|
|
1584
|
+
if (this.match("IDENTIFIER")) {
|
|
1585
|
+
return {
|
|
1586
|
+
type: "Identifier",
|
|
1587
|
+
position,
|
|
1588
|
+
name: this.previous().value,
|
|
1589
|
+
};
|
|
1590
|
+
}
|
|
1591
|
+
// Parenthesized expression
|
|
1592
|
+
if (this.match("LPAREN")) {
|
|
1593
|
+
const expr = this.parseTernary();
|
|
1594
|
+
this.consume("RPAREN", "Expected ')' after expression");
|
|
1595
|
+
return expr;
|
|
1596
|
+
}
|
|
1597
|
+
// Array literal
|
|
1598
|
+
if (this.match("LBRACKET")) {
|
|
1599
|
+
const elements = [];
|
|
1600
|
+
if (!this.check("RBRACKET")) {
|
|
1601
|
+
do {
|
|
1602
|
+
elements.push(this.parseTernary());
|
|
1603
|
+
} while (this.match("COMMA"));
|
|
1604
|
+
}
|
|
1605
|
+
this.consume("RBRACKET", "Expected ']' after array elements");
|
|
1606
|
+
checkArrayLength(elements.length, this.limits);
|
|
1607
|
+
return { type: "ArrayLiteral", position, elements };
|
|
1608
|
+
}
|
|
1609
|
+
throw new ParseError(`Unexpected token: ${token.value || token.type}`, position, this.source);
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
/**
|
|
1613
|
+
* Parses an expression string into an AST.
|
|
1614
|
+
*
|
|
1615
|
+
* @param source - The expression string to parse
|
|
1616
|
+
* @param limits - Optional expression limits
|
|
1617
|
+
* @returns The parsed AST
|
|
1618
|
+
* @throws TokenizerError if tokenization fails
|
|
1619
|
+
* @throws ParseError if parsing fails
|
|
1620
|
+
*/
|
|
1621
|
+
function parse(source, limits = DEFAULT_EXPRESSION_LIMITS) {
|
|
1622
|
+
const tokens = tokenize(source, limits);
|
|
1623
|
+
const parser = new Parser(tokens, source, limits);
|
|
1624
|
+
return parser.parse();
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
/**
|
|
1628
|
+
* Built-in functions for the expression language.
|
|
1629
|
+
*
|
|
1630
|
+
* All built-in functions are pure and deterministic.
|
|
1631
|
+
*
|
|
1632
|
+
* Null handling semantics:
|
|
1633
|
+
* - `undefined` is normalized to `null` throughout the expression value model.
|
|
1634
|
+
* - Predicate-style builtins (starts_with, ends_with, contains, glob_match,
|
|
1635
|
+
* regex_match, etc.) return `false` when passed `null` for required args
|
|
1636
|
+
* instead of throwing an error.
|
|
1637
|
+
* - Wrong non-null types still raise BuiltinError to surface real bugs.
|
|
1638
|
+
* - Non-predicate operations (arithmetic, comparisons) remain strict.
|
|
1639
|
+
*/
|
|
1640
|
+
/**
|
|
1641
|
+
* Normalizes a JavaScript value to an ExprValue.
|
|
1642
|
+
*
|
|
1643
|
+
* Rules:
|
|
1644
|
+
* - `undefined` -> `null`
|
|
1645
|
+
* - `null` -> `null`
|
|
1646
|
+
* - boolean/number/string -> returned as-is
|
|
1647
|
+
* - array -> elements are recursively normalized
|
|
1648
|
+
* - object -> returned as-is (reads will normalize on access)
|
|
1649
|
+
* - other types (function, symbol, etc.) -> `null`
|
|
1650
|
+
*
|
|
1651
|
+
* This ensures `undefined` never leaks into the expression value model.
|
|
1652
|
+
*/
|
|
1653
|
+
function normalizeJsValue(value) {
|
|
1654
|
+
if (value === undefined || value === null) {
|
|
1655
|
+
return null;
|
|
1656
|
+
}
|
|
1657
|
+
if (typeof value === "boolean" || typeof value === "number") {
|
|
1658
|
+
return value;
|
|
1659
|
+
}
|
|
1660
|
+
if (typeof value === "string") {
|
|
1661
|
+
return value;
|
|
1662
|
+
}
|
|
1663
|
+
if (Array.isArray(value)) {
|
|
1664
|
+
return value.map((element) => normalizeJsValue(element));
|
|
1665
|
+
}
|
|
1666
|
+
if (typeof value === "object") {
|
|
1667
|
+
// Return the object as-is; reads will normalize on access
|
|
1668
|
+
return value;
|
|
1669
|
+
}
|
|
1670
|
+
// Function, symbol, bigint, etc. -> null
|
|
1671
|
+
return null;
|
|
1672
|
+
}
|
|
1673
|
+
/**
|
|
1674
|
+
* Gets the type name of a value for error messages.
|
|
1675
|
+
*/
|
|
1676
|
+
function getTypeName(value) {
|
|
1677
|
+
if (value === null)
|
|
1678
|
+
return "null";
|
|
1679
|
+
if (Array.isArray(value))
|
|
1680
|
+
return "array";
|
|
1681
|
+
return typeof value;
|
|
1682
|
+
}
|
|
1683
|
+
/**
|
|
1684
|
+
* Asserts that a value is a string.
|
|
1685
|
+
*/
|
|
1686
|
+
function assertString$1(value, argName, functionName) {
|
|
1687
|
+
if (typeof value !== "string") {
|
|
1688
|
+
throw new BuiltinError(functionName, `${argName} must be a string, got ${getTypeName(value)}`);
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
/**
|
|
1692
|
+
* Checks if a value is null (for null-tolerant predicates).
|
|
1693
|
+
*/
|
|
1694
|
+
function isNull$1(value) {
|
|
1695
|
+
return value === null;
|
|
1696
|
+
}
|
|
1697
|
+
/**
|
|
1698
|
+
* Asserts that a non-null value is a string (for null-tolerant predicates).
|
|
1699
|
+
* Returns false if the value is null (indicating predicate should return false).
|
|
1700
|
+
* Throws BuiltinError if the value is non-null but not a string.
|
|
1701
|
+
*/
|
|
1702
|
+
function assertStringOrNull$1(value, argName, functionName) {
|
|
1703
|
+
if (isNull$1(value)) {
|
|
1704
|
+
return false;
|
|
1705
|
+
}
|
|
1706
|
+
if (typeof value !== "string") {
|
|
1707
|
+
throw new BuiltinError(functionName, `${argName} must be a string, got ${getTypeName(value)}`);
|
|
1708
|
+
}
|
|
1709
|
+
return true;
|
|
1710
|
+
}
|
|
1711
|
+
/**
|
|
1712
|
+
* Gets an argument by index, throwing if not present.
|
|
1713
|
+
*/
|
|
1714
|
+
function getArg$1(args, index, functionName) {
|
|
1715
|
+
const value = args[index];
|
|
1716
|
+
if (value === undefined) {
|
|
1717
|
+
throw new BuiltinError(functionName, `missing argument at index ${index}`);
|
|
1718
|
+
}
|
|
1719
|
+
return value;
|
|
1720
|
+
}
|
|
1721
|
+
/**
|
|
1722
|
+
* Asserts argument count.
|
|
1723
|
+
*/
|
|
1724
|
+
function assertArgCount$1(args, expected, functionName) {
|
|
1725
|
+
if (args.length !== expected) {
|
|
1726
|
+
throw new BuiltinError(functionName, `expected ${expected} argument(s), got ${args.length}`);
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1729
|
+
/**
|
|
1730
|
+
* Asserts argument count range.
|
|
1731
|
+
*/
|
|
1732
|
+
function assertArgCountRange(args, min, max, functionName) {
|
|
1733
|
+
if (args.length < min || args.length > max) {
|
|
1734
|
+
throw new BuiltinError(functionName, `expected ${min}-${max} argument(s), got ${args.length}`);
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
// ============================================================
|
|
1738
|
+
// String Helpers
|
|
1739
|
+
// ============================================================
|
|
1740
|
+
/**
|
|
1741
|
+
* lower(s: string) -> string
|
|
1742
|
+
*
|
|
1743
|
+
* Returns the lowercase version of the string.
|
|
1744
|
+
*/
|
|
1745
|
+
const lower = (args) => {
|
|
1746
|
+
assertArgCount$1(args, 1, "lower");
|
|
1747
|
+
const s = getArg$1(args, 0, "lower");
|
|
1748
|
+
assertString$1(s, "s", "lower");
|
|
1749
|
+
return s.toLowerCase();
|
|
1750
|
+
};
|
|
1751
|
+
/**
|
|
1752
|
+
* upper(s: string) -> string
|
|
1753
|
+
*
|
|
1754
|
+
* Returns the uppercase version of the string.
|
|
1755
|
+
*/
|
|
1756
|
+
const upper = (args) => {
|
|
1757
|
+
assertArgCount$1(args, 1, "upper");
|
|
1758
|
+
const s = getArg$1(args, 0, "upper");
|
|
1759
|
+
assertString$1(s, "s", "upper");
|
|
1760
|
+
return s.toUpperCase();
|
|
1761
|
+
};
|
|
1762
|
+
/**
|
|
1763
|
+
* starts_with(s: string, prefix: string) -> bool
|
|
1764
|
+
*
|
|
1765
|
+
* Returns true if the string starts with the prefix.
|
|
1766
|
+
* Null-tolerant: returns false if either argument is null.
|
|
1767
|
+
*/
|
|
1768
|
+
const starts_with = (args) => {
|
|
1769
|
+
assertArgCount$1(args, 2, "starts_with");
|
|
1770
|
+
const s = getArg$1(args, 0, "starts_with");
|
|
1771
|
+
const prefix = getArg$1(args, 1, "starts_with");
|
|
1772
|
+
// Null-tolerant: return false if either arg is null
|
|
1773
|
+
if (!assertStringOrNull$1(s, "s", "starts_with"))
|
|
1774
|
+
return false;
|
|
1775
|
+
if (!assertStringOrNull$1(prefix, "prefix", "starts_with"))
|
|
1776
|
+
return false;
|
|
1777
|
+
return s.startsWith(prefix);
|
|
1778
|
+
};
|
|
1779
|
+
/**
|
|
1780
|
+
* ends_with(s: string, suffix: string) -> bool
|
|
1781
|
+
*
|
|
1782
|
+
* Returns true if the string ends with the suffix.
|
|
1783
|
+
* Null-tolerant: returns false if either argument is null.
|
|
1784
|
+
*/
|
|
1785
|
+
const ends_with = (args) => {
|
|
1786
|
+
assertArgCount$1(args, 2, "ends_with");
|
|
1787
|
+
const s = getArg$1(args, 0, "ends_with");
|
|
1788
|
+
const suffix = getArg$1(args, 1, "ends_with");
|
|
1789
|
+
// Null-tolerant: return false if either arg is null
|
|
1790
|
+
if (!assertStringOrNull$1(s, "s", "ends_with"))
|
|
1791
|
+
return false;
|
|
1792
|
+
if (!assertStringOrNull$1(suffix, "suffix", "ends_with"))
|
|
1793
|
+
return false;
|
|
1794
|
+
return s.endsWith(suffix);
|
|
1795
|
+
};
|
|
1796
|
+
/**
|
|
1797
|
+
* contains(s: string, substring: string) -> bool
|
|
1798
|
+
*
|
|
1799
|
+
* Returns true if the string contains the substring.
|
|
1800
|
+
* Null-tolerant: returns false if either argument is null.
|
|
1801
|
+
*/
|
|
1802
|
+
const contains = (args) => {
|
|
1803
|
+
assertArgCount$1(args, 2, "contains");
|
|
1804
|
+
const s = getArg$1(args, 0, "contains");
|
|
1805
|
+
const substring = getArg$1(args, 1, "contains");
|
|
1806
|
+
// Null-tolerant: return false if either arg is null
|
|
1807
|
+
if (!assertStringOrNull$1(s, "s", "contains"))
|
|
1808
|
+
return false;
|
|
1809
|
+
if (!assertStringOrNull$1(substring, "substring", "contains"))
|
|
1810
|
+
return false;
|
|
1811
|
+
return s.includes(substring);
|
|
1812
|
+
};
|
|
1813
|
+
/**
|
|
1814
|
+
* split(s: string, separator: string) -> string[]
|
|
1815
|
+
*
|
|
1816
|
+
* Splits the string by the separator.
|
|
1817
|
+
*/
|
|
1818
|
+
const split = (args) => {
|
|
1819
|
+
assertArgCountRange(args, 1, 2, "split");
|
|
1820
|
+
const s = getArg$1(args, 0, "split");
|
|
1821
|
+
assertString$1(s, "s", "split");
|
|
1822
|
+
const separator = args.length >= 2 ? getArg$1(args, 1, "split") : " ";
|
|
1823
|
+
assertString$1(separator, "separator", "split");
|
|
1824
|
+
return s.split(separator);
|
|
1825
|
+
};
|
|
1826
|
+
// ============================================================
|
|
1827
|
+
// Collection Helpers
|
|
1828
|
+
// ============================================================
|
|
1829
|
+
/**
|
|
1830
|
+
* len(x: string | array) -> number
|
|
1831
|
+
*
|
|
1832
|
+
* Returns the length of a string or array.
|
|
1833
|
+
*/
|
|
1834
|
+
const len = (args) => {
|
|
1835
|
+
assertArgCount$1(args, 1, "len");
|
|
1836
|
+
const x = getArg$1(args, 0, "len");
|
|
1837
|
+
if (typeof x === "string") {
|
|
1838
|
+
return x.length;
|
|
1839
|
+
}
|
|
1840
|
+
if (Array.isArray(x)) {
|
|
1841
|
+
return x.length;
|
|
1842
|
+
}
|
|
1843
|
+
throw new BuiltinError("len", `expected string or array, got ${getTypeName(x)}`);
|
|
1844
|
+
};
|
|
1845
|
+
// ============================================================
|
|
1846
|
+
// Generic Helpers
|
|
1847
|
+
// ============================================================
|
|
1848
|
+
/**
|
|
1849
|
+
* exists(x: any) -> bool
|
|
1850
|
+
*
|
|
1851
|
+
* Returns true if the value is not null.
|
|
1852
|
+
* Missing bindings and missing properties evaluate to null, so this
|
|
1853
|
+
* can be used to check for presence.
|
|
1854
|
+
*/
|
|
1855
|
+
const exists = (args) => {
|
|
1856
|
+
assertArgCount$1(args, 1, "exists");
|
|
1857
|
+
const x = getArg$1(args, 0, "exists");
|
|
1858
|
+
return x !== null;
|
|
1859
|
+
};
|
|
1860
|
+
/**
|
|
1861
|
+
* coalesce(a: any, b: any) -> any
|
|
1862
|
+
*
|
|
1863
|
+
* Returns `a` if it is not null, otherwise returns `b`.
|
|
1864
|
+
* This is useful for providing default values.
|
|
1865
|
+
*/
|
|
1866
|
+
const coalesce = (args) => {
|
|
1867
|
+
assertArgCount$1(args, 2, "coalesce");
|
|
1868
|
+
const a = getArg$1(args, 0, "coalesce");
|
|
1869
|
+
const b = getArg$1(args, 1, "coalesce");
|
|
1870
|
+
return a !== null ? a : b;
|
|
1871
|
+
};
|
|
1872
|
+
/**
|
|
1873
|
+
* trim(s: string) -> string
|
|
1874
|
+
*
|
|
1875
|
+
* Trims whitespace from both ends of a string.
|
|
1876
|
+
* Returns an empty string if `s` is null (for convenient composition).
|
|
1877
|
+
* Throws BuiltinError if `s` is non-null but not a string.
|
|
1878
|
+
*/
|
|
1879
|
+
const trim = (args) => {
|
|
1880
|
+
assertArgCount$1(args, 1, "trim");
|
|
1881
|
+
const s = getArg$1(args, 0, "trim");
|
|
1882
|
+
// Null-friendly: return empty string for null
|
|
1883
|
+
if (s === null) {
|
|
1884
|
+
return "";
|
|
1885
|
+
}
|
|
1886
|
+
// Strict type check for non-null values
|
|
1887
|
+
if (typeof s !== "string") {
|
|
1888
|
+
throw new BuiltinError("trim", `s must be a string, got ${getTypeName(s)}`);
|
|
1889
|
+
}
|
|
1890
|
+
return s.trim();
|
|
1891
|
+
};
|
|
1892
|
+
/**
|
|
1893
|
+
* secure_hash(input_str: string, length: number) -> string
|
|
1894
|
+
*
|
|
1895
|
+
* Generates a deterministic secure hash/fingerprint of the input string.
|
|
1896
|
+
* Uses SHA-256 hashing to create a stable identifier of the specified length.
|
|
1897
|
+
* Returns base62-encoded string (alphanumeric, case-sensitive).
|
|
1898
|
+
* Automatically rehashes if result contains blacklisted words.
|
|
1899
|
+
* Returns empty string if input_str is null (for convenient composition).
|
|
1900
|
+
* Throws BuiltinError if input_str is non-null but not a string, or if length is invalid.
|
|
1901
|
+
*/
|
|
1902
|
+
const secure_hash = (args) => {
|
|
1903
|
+
assertArgCount$1(args, 2, "secure_hash");
|
|
1904
|
+
const input_str = getArg$1(args, 0, "secure_hash");
|
|
1905
|
+
const length = getArg$1(args, 1, "secure_hash");
|
|
1906
|
+
// Null-friendly: return empty string for null input
|
|
1907
|
+
if (input_str === null) {
|
|
1908
|
+
return "";
|
|
1909
|
+
}
|
|
1910
|
+
// Strict type check for input_str
|
|
1911
|
+
if (typeof input_str !== "string") {
|
|
1912
|
+
throw new BuiltinError("secure_hash", `input_str must be a string, got ${getTypeName(input_str)}`);
|
|
1913
|
+
}
|
|
1914
|
+
// Strict type check for length
|
|
1915
|
+
if (typeof length !== "number") {
|
|
1916
|
+
throw new BuiltinError("secure_hash", `length must be a number, got ${getTypeName(length)}`);
|
|
1917
|
+
}
|
|
1918
|
+
// Validate length is a positive integer
|
|
1919
|
+
if (!Number.isInteger(length) || length <= 0) {
|
|
1920
|
+
throw new BuiltinError("secure_hash", `length must be a positive integer, got ${length}`);
|
|
1921
|
+
}
|
|
1922
|
+
// Use generateFingerprintSync from @naylence/core
|
|
1923
|
+
// This provides SHA-256 hashing, base62 encoding, and profanity filtering
|
|
1924
|
+
return core.generateFingerprintSync(input_str, length, sha256.sha256);
|
|
1925
|
+
};
|
|
1926
|
+
// ============================================================
|
|
1927
|
+
// Pattern Helpers (BSL-only)
|
|
1928
|
+
// ============================================================
|
|
1929
|
+
/**
|
|
1930
|
+
* Escapes special regex characters in a string.
|
|
1931
|
+
*/
|
|
1932
|
+
function escapeRegex(str) {
|
|
1933
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1934
|
+
}
|
|
1935
|
+
/**
|
|
1936
|
+
* Converts a glob pattern to a regex pattern.
|
|
1937
|
+
*/
|
|
1938
|
+
function globToRegex(glob) {
|
|
1939
|
+
const parts = [];
|
|
1940
|
+
let i = 0;
|
|
1941
|
+
while (i < glob.length) {
|
|
1942
|
+
const ch = glob[i];
|
|
1943
|
+
if (ch === "*") {
|
|
1944
|
+
if (glob[i + 1] === "*") {
|
|
1945
|
+
// `**` matches any characters
|
|
1946
|
+
parts.push(".*");
|
|
1947
|
+
i += 2;
|
|
1948
|
+
}
|
|
1949
|
+
else {
|
|
1950
|
+
// `*` matches any characters except dots
|
|
1951
|
+
parts.push("[^.]*");
|
|
1952
|
+
i += 1;
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1955
|
+
else if (ch === "?") {
|
|
1956
|
+
// `?` matches a single character
|
|
1957
|
+
parts.push("[^.]");
|
|
1958
|
+
i += 1;
|
|
1959
|
+
}
|
|
1960
|
+
else {
|
|
1961
|
+
parts.push(escapeRegex(ch));
|
|
1962
|
+
i += 1;
|
|
1963
|
+
}
|
|
1964
|
+
}
|
|
1965
|
+
return parts.join("");
|
|
1966
|
+
}
|
|
1967
|
+
/**
|
|
1968
|
+
* glob_match(value: string, pattern: string) -> bool
|
|
1969
|
+
*
|
|
1970
|
+
* Returns true if the value matches the glob pattern.
|
|
1971
|
+
* Glob syntax: * (single segment), ** (any depth), ? (single char)
|
|
1972
|
+
* Null-tolerant: returns false if either argument is null.
|
|
1973
|
+
*/
|
|
1974
|
+
const glob_match = (args, context) => {
|
|
1975
|
+
assertArgCount$1(args, 2, "glob_match");
|
|
1976
|
+
const value = getArg$1(args, 0, "glob_match");
|
|
1977
|
+
const pattern = getArg$1(args, 1, "glob_match");
|
|
1978
|
+
// Null-tolerant: return false if either arg is null
|
|
1979
|
+
if (!assertStringOrNull$1(value, "value", "glob_match"))
|
|
1980
|
+
return false;
|
|
1981
|
+
if (!assertStringOrNull$1(pattern, "pattern", "glob_match"))
|
|
1982
|
+
return false;
|
|
1983
|
+
// Validate pattern length
|
|
1984
|
+
checkGlobPatternLength(pattern, context.limits);
|
|
1985
|
+
// Convert glob to regex
|
|
1986
|
+
const regexPattern = `^${globToRegex(pattern)}$`;
|
|
1987
|
+
try {
|
|
1988
|
+
const regex = new RegExp(regexPattern);
|
|
1989
|
+
return regex.test(value);
|
|
1990
|
+
}
|
|
1991
|
+
catch {
|
|
1992
|
+
throw new BuiltinError("glob_match", `invalid glob pattern: ${pattern}`);
|
|
1993
|
+
}
|
|
1994
|
+
};
|
|
1995
|
+
/**
|
|
1996
|
+
* Detects potentially catastrophic regex patterns.
|
|
1997
|
+
*
|
|
1998
|
+
* This is a best-effort heuristic check for common ReDoS patterns.
|
|
1999
|
+
*/
|
|
2000
|
+
function isSafeRegex(pattern) {
|
|
2001
|
+
// Check for obvious catastrophic patterns:
|
|
2002
|
+
// - Nested quantifiers: (a+)+, (a*)*
|
|
2003
|
+
// - Overlapping alternation with quantifiers: (a|a)+
|
|
2004
|
+
// Simple heuristic: reject patterns with nested quantifiers
|
|
2005
|
+
const nestedQuantifiers = /([+*?]|\{\d+,?\d*\})\s*\)\s*([+*?]|\{\d+,?\d*\})/;
|
|
2006
|
+
if (nestedQuantifiers.test(pattern)) {
|
|
2007
|
+
return false;
|
|
2008
|
+
}
|
|
2009
|
+
// Reject patterns with excessive backtracking potential
|
|
2010
|
+
const excessiveBacktracking = /(\.\*){3,}|(\.\+){3,}/;
|
|
2011
|
+
if (excessiveBacktracking.test(pattern)) {
|
|
2012
|
+
return false;
|
|
2013
|
+
}
|
|
2014
|
+
return true;
|
|
2015
|
+
}
|
|
2016
|
+
/**
|
|
2017
|
+
* regex_match(value: string, pattern: string) -> bool
|
|
2018
|
+
*
|
|
2019
|
+
* Returns true if the value matches the regex pattern.
|
|
2020
|
+
* The pattern is anchored (full match).
|
|
2021
|
+
* Null-tolerant: returns false if either argument is null.
|
|
2022
|
+
*/
|
|
2023
|
+
const regex_match = (args, context) => {
|
|
2024
|
+
assertArgCount$1(args, 2, "regex_match");
|
|
2025
|
+
const value = getArg$1(args, 0, "regex_match");
|
|
2026
|
+
const pattern = getArg$1(args, 1, "regex_match");
|
|
2027
|
+
// Null-tolerant: return false if either arg is null
|
|
2028
|
+
if (!assertStringOrNull$1(value, "value", "regex_match"))
|
|
2029
|
+
return false;
|
|
2030
|
+
if (!assertStringOrNull$1(pattern, "pattern", "regex_match"))
|
|
2031
|
+
return false;
|
|
2032
|
+
// Validate pattern length
|
|
2033
|
+
checkRegexPatternLength(pattern, context.limits);
|
|
2034
|
+
// Check for potentially unsafe patterns
|
|
2035
|
+
if (!isSafeRegex(pattern)) {
|
|
2036
|
+
throw new BuiltinError("regex_match", `pattern may cause excessive backtracking: ${pattern}`);
|
|
2037
|
+
}
|
|
2038
|
+
// Anchor the pattern for full match
|
|
2039
|
+
const anchoredPattern = pattern.startsWith("^")
|
|
2040
|
+
? pattern
|
|
2041
|
+
: pattern.endsWith("$")
|
|
2042
|
+
? pattern
|
|
2043
|
+
: `^(?:${pattern})$`;
|
|
2044
|
+
try {
|
|
2045
|
+
const regex = new RegExp(anchoredPattern);
|
|
2046
|
+
return regex.test(value);
|
|
2047
|
+
}
|
|
2048
|
+
catch (error) {
|
|
2049
|
+
throw new BuiltinError("regex_match", `invalid regex pattern: ${pattern} - ${error instanceof Error ? error.message : String(error)}`);
|
|
2050
|
+
}
|
|
2051
|
+
};
|
|
2052
|
+
// ============================================================
|
|
2053
|
+
// Registry
|
|
2054
|
+
// ============================================================
|
|
2055
|
+
/**
|
|
2056
|
+
* Registry of all built-in functions.
|
|
2057
|
+
*/
|
|
2058
|
+
const BUILTIN_FUNCTIONS = new Map([
|
|
2059
|
+
// String helpers
|
|
2060
|
+
["lower", lower],
|
|
2061
|
+
["upper", upper],
|
|
2062
|
+
["starts_with", starts_with],
|
|
2063
|
+
["ends_with", ends_with],
|
|
2064
|
+
["contains", contains],
|
|
2065
|
+
["split", split],
|
|
2066
|
+
["trim", trim],
|
|
2067
|
+
// Collection helpers
|
|
2068
|
+
["len", len],
|
|
2069
|
+
// Generic helpers
|
|
2070
|
+
["exists", exists],
|
|
2071
|
+
["coalesce", coalesce],
|
|
2072
|
+
["secure_hash", secure_hash],
|
|
2073
|
+
// Pattern helpers
|
|
2074
|
+
["glob_match", glob_match],
|
|
2075
|
+
["regex_match", regex_match],
|
|
2076
|
+
]);
|
|
2077
|
+
/**
|
|
2078
|
+
* Calls a built-in function by name.
|
|
2079
|
+
*
|
|
2080
|
+
* @param name - The function name
|
|
2081
|
+
* @param args - The function arguments
|
|
2082
|
+
* @param context - The evaluation context
|
|
2083
|
+
* @returns The function result
|
|
2084
|
+
* @throws BuiltinError if the function doesn't exist or fails
|
|
2085
|
+
*/
|
|
2086
|
+
function callBuiltin(name, args, context, functions = BUILTIN_FUNCTIONS) {
|
|
2087
|
+
const fn = functions.get(name);
|
|
2088
|
+
if (!fn) {
|
|
2089
|
+
throw new EvaluationError(`Unknown function: ${name}`, context.position, context.source);
|
|
2090
|
+
}
|
|
2091
|
+
return fn(args, context);
|
|
2092
|
+
}
|
|
2093
|
+
|
|
2094
|
+
/**
|
|
2095
|
+
* Expression evaluator.
|
|
2096
|
+
*
|
|
2097
|
+
* Evaluates an AST against a set of variable bindings and returns a value.
|
|
2098
|
+
*
|
|
2099
|
+
* Null handling semantics:
|
|
2100
|
+
* - `undefined` values are normalized to `null` throughout evaluation.
|
|
2101
|
+
* - Missing identifiers evaluate to `null`.
|
|
2102
|
+
* - Member access on `null` or non-object returns `null`.
|
|
2103
|
+
* - Missing properties return `null` (including properties set to `undefined`).
|
|
2104
|
+
*/
|
|
2105
|
+
/**
|
|
2106
|
+
* Evaluates an AST node and returns the result.
|
|
2107
|
+
*/
|
|
2108
|
+
class Evaluator {
|
|
2109
|
+
constructor(context) {
|
|
2110
|
+
this.memberAccessDepth = 0;
|
|
2111
|
+
this.context = context;
|
|
2112
|
+
this.limits = context.limits ?? DEFAULT_EXPRESSION_LIMITS;
|
|
2113
|
+
this.source = context.source ?? "";
|
|
2114
|
+
this.functions = context.functions ?? BUILTIN_FUNCTIONS;
|
|
2115
|
+
}
|
|
2116
|
+
/**
|
|
2117
|
+
* Evaluates an AST node and returns the value.
|
|
2118
|
+
*/
|
|
2119
|
+
evaluate(node) {
|
|
2120
|
+
switch (node.type) {
|
|
2121
|
+
case "StringLiteral":
|
|
2122
|
+
return node.value;
|
|
2123
|
+
case "NumberLiteral":
|
|
2124
|
+
return node.value;
|
|
2125
|
+
case "BooleanLiteral":
|
|
2126
|
+
return node.value;
|
|
2127
|
+
case "NullLiteral":
|
|
2128
|
+
return null;
|
|
2129
|
+
case "ArrayLiteral":
|
|
2130
|
+
return node.elements.map((e) => this.evaluate(e));
|
|
2131
|
+
case "Identifier":
|
|
2132
|
+
return this.evaluateIdentifier(node.name, node.position);
|
|
2133
|
+
case "MemberAccess":
|
|
2134
|
+
return this.evaluateMemberAccess(node);
|
|
2135
|
+
case "IndexAccess":
|
|
2136
|
+
return this.evaluateIndexAccess(node);
|
|
2137
|
+
case "FunctionCall":
|
|
2138
|
+
return this.evaluateFunctionCall(node);
|
|
2139
|
+
case "UnaryOp":
|
|
2140
|
+
return this.evaluateUnaryOp(node.operator, node.operand, node.position);
|
|
2141
|
+
case "BinaryOp":
|
|
2142
|
+
return this.evaluateBinaryOp(node.operator, node.left, node.right, node.position);
|
|
2143
|
+
case "TernaryOp":
|
|
2144
|
+
return this.evaluateTernaryOp(node.condition, node.consequent, node.alternate, node.position);
|
|
2145
|
+
}
|
|
2146
|
+
}
|
|
2147
|
+
/**
|
|
2148
|
+
* Evaluates as boolean with strict type checking.
|
|
2149
|
+
*/
|
|
2150
|
+
evaluateAsBoolean(node) {
|
|
2151
|
+
const value = this.evaluate(node);
|
|
2152
|
+
if (typeof value !== "boolean") {
|
|
2153
|
+
throw new TypeError("boolean", getTypeName(value), node.position, this.source);
|
|
2154
|
+
}
|
|
2155
|
+
return value;
|
|
2156
|
+
}
|
|
2157
|
+
evaluateIdentifier(name, _position) {
|
|
2158
|
+
// Check if it's a top-level binding
|
|
2159
|
+
if (name in this.context.bindings) {
|
|
2160
|
+
// Normalize the value to ensure undefined becomes null
|
|
2161
|
+
return normalizeJsValue(this.context.bindings[name]);
|
|
2162
|
+
}
|
|
2163
|
+
// Unknown identifier evaluates to null (missing field)
|
|
2164
|
+
return null;
|
|
2165
|
+
}
|
|
2166
|
+
evaluateMemberAccess(node) {
|
|
2167
|
+
// Check member access depth
|
|
2168
|
+
this.memberAccessDepth++;
|
|
2169
|
+
if (this.memberAccessDepth > this.limits.maxMemberAccessDepth) {
|
|
2170
|
+
throw new EvaluationError(`Member access depth ${this.memberAccessDepth} exceeds limit of ${this.limits.maxMemberAccessDepth}`, node.position, this.source);
|
|
2171
|
+
}
|
|
2172
|
+
try {
|
|
2173
|
+
const obj = this.evaluate(node.object);
|
|
2174
|
+
// Null-safe member access: null.foo -> null
|
|
2175
|
+
if (obj === null) {
|
|
2176
|
+
return null;
|
|
2177
|
+
}
|
|
2178
|
+
// Must be an object (not primitive, not array)
|
|
2179
|
+
if (typeof obj !== "object" || Array.isArray(obj)) {
|
|
2180
|
+
// Type mismatch during access returns null (not error)
|
|
2181
|
+
return null;
|
|
2182
|
+
}
|
|
2183
|
+
const record = obj;
|
|
2184
|
+
if (node.property in record) {
|
|
2185
|
+
// Normalize the value to ensure undefined becomes null
|
|
2186
|
+
return normalizeJsValue(record[node.property]);
|
|
2187
|
+
}
|
|
2188
|
+
// Missing property evaluates to null
|
|
2189
|
+
return null;
|
|
2190
|
+
}
|
|
2191
|
+
finally {
|
|
2192
|
+
this.memberAccessDepth--;
|
|
2193
|
+
}
|
|
2194
|
+
}
|
|
2195
|
+
evaluateIndexAccess(node) {
|
|
2196
|
+
const obj = this.evaluate(node.object);
|
|
2197
|
+
const index = this.evaluate(node.index);
|
|
2198
|
+
// Null-safe index access: null[0] -> null
|
|
2199
|
+
if (obj === null) {
|
|
2200
|
+
return null;
|
|
2201
|
+
}
|
|
2202
|
+
// Array access with numeric index
|
|
2203
|
+
if (Array.isArray(obj)) {
|
|
2204
|
+
if (typeof index !== "number") {
|
|
2205
|
+
throw new TypeError("number", getTypeName(index), node.position, this.source);
|
|
2206
|
+
}
|
|
2207
|
+
const intIndex = Math.floor(index);
|
|
2208
|
+
if (intIndex < 0 || intIndex >= obj.length) {
|
|
2209
|
+
// Out of bounds evaluates to null
|
|
2210
|
+
return null;
|
|
2211
|
+
}
|
|
2212
|
+
// Normalize array element to ensure undefined becomes null
|
|
2213
|
+
return normalizeJsValue(obj[intIndex]);
|
|
2214
|
+
}
|
|
2215
|
+
// Object access with string key
|
|
2216
|
+
if (typeof obj === "object") {
|
|
2217
|
+
if (typeof index !== "string") {
|
|
2218
|
+
throw new TypeError("string", getTypeName(index), node.position, this.source);
|
|
2219
|
+
}
|
|
2220
|
+
const record = obj;
|
|
2221
|
+
if (index in record) {
|
|
2222
|
+
// Normalize the value to ensure undefined becomes null
|
|
2223
|
+
return normalizeJsValue(record[index]);
|
|
2224
|
+
}
|
|
2225
|
+
// Missing key evaluates to null
|
|
2226
|
+
return null;
|
|
2227
|
+
}
|
|
2228
|
+
// Type mismatch during access returns null
|
|
2229
|
+
return null;
|
|
2230
|
+
}
|
|
2231
|
+
evaluateFunctionCall(node) {
|
|
2232
|
+
// Evaluate arguments
|
|
2233
|
+
const args = node.args.map((arg) => this.evaluate(arg));
|
|
2234
|
+
const builtinContext = {
|
|
2235
|
+
limits: this.limits,
|
|
2236
|
+
position: node.position,
|
|
2237
|
+
source: this.source,
|
|
2238
|
+
};
|
|
2239
|
+
return callBuiltin(node.name, args, builtinContext, this.functions);
|
|
2240
|
+
}
|
|
2241
|
+
evaluateUnaryOp(operator, operand, position) {
|
|
2242
|
+
const value = this.evaluate(operand);
|
|
2243
|
+
switch (operator) {
|
|
2244
|
+
case "!":
|
|
2245
|
+
if (typeof value !== "boolean") {
|
|
2246
|
+
throw new TypeError("boolean", getTypeName(value), position, this.source);
|
|
2247
|
+
}
|
|
2248
|
+
return !value;
|
|
2249
|
+
case "-":
|
|
2250
|
+
if (typeof value !== "number") {
|
|
2251
|
+
throw new TypeError("number", getTypeName(value), position, this.source);
|
|
2252
|
+
}
|
|
2253
|
+
return -value;
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
2256
|
+
evaluateBinaryOp(operator, left, right, position) {
|
|
2257
|
+
// Short-circuit evaluation for logical operators
|
|
2258
|
+
if (operator === "&&") {
|
|
2259
|
+
const leftValue = this.evaluate(left);
|
|
2260
|
+
if (typeof leftValue !== "boolean") {
|
|
2261
|
+
throw new TypeError("boolean", getTypeName(leftValue), left.position, this.source);
|
|
2262
|
+
}
|
|
2263
|
+
if (!leftValue)
|
|
2264
|
+
return false;
|
|
2265
|
+
const rightValue = this.evaluate(right);
|
|
2266
|
+
if (typeof rightValue !== "boolean") {
|
|
2267
|
+
throw new TypeError("boolean", getTypeName(rightValue), right.position, this.source);
|
|
2268
|
+
}
|
|
2269
|
+
return rightValue;
|
|
2270
|
+
}
|
|
2271
|
+
if (operator === "||") {
|
|
2272
|
+
const leftValue = this.evaluate(left);
|
|
2273
|
+
if (typeof leftValue !== "boolean") {
|
|
2274
|
+
throw new TypeError("boolean", getTypeName(leftValue), left.position, this.source);
|
|
2275
|
+
}
|
|
2276
|
+
if (leftValue)
|
|
2277
|
+
return true;
|
|
2278
|
+
const rightValue = this.evaluate(right);
|
|
2279
|
+
if (typeof rightValue !== "boolean") {
|
|
2280
|
+
throw new TypeError("boolean", getTypeName(rightValue), right.position, this.source);
|
|
2281
|
+
}
|
|
2282
|
+
return rightValue;
|
|
2283
|
+
}
|
|
2284
|
+
// Eager evaluation for other operators
|
|
2285
|
+
const leftValue = this.evaluate(left);
|
|
2286
|
+
const rightValue = this.evaluate(right);
|
|
2287
|
+
switch (operator) {
|
|
2288
|
+
// Arithmetic
|
|
2289
|
+
case "+":
|
|
2290
|
+
if (typeof leftValue === "string" && typeof rightValue === "string") {
|
|
2291
|
+
return leftValue + rightValue;
|
|
2292
|
+
}
|
|
2293
|
+
if (typeof leftValue === "number" && typeof rightValue === "number") {
|
|
2294
|
+
return leftValue + rightValue;
|
|
2295
|
+
}
|
|
2296
|
+
throw new EvaluationError(`Cannot add ${getTypeName(leftValue)} and ${getTypeName(rightValue)}`, position, this.source);
|
|
2297
|
+
case "-":
|
|
2298
|
+
if (typeof leftValue !== "number" || typeof rightValue !== "number") {
|
|
2299
|
+
throw new EvaluationError(`Cannot subtract ${getTypeName(leftValue)} and ${getTypeName(rightValue)}`, position, this.source);
|
|
2300
|
+
}
|
|
2301
|
+
return leftValue - rightValue;
|
|
2302
|
+
case "*":
|
|
2303
|
+
if (typeof leftValue !== "number" || typeof rightValue !== "number") {
|
|
2304
|
+
throw new EvaluationError(`Cannot multiply ${getTypeName(leftValue)} and ${getTypeName(rightValue)}`, position, this.source);
|
|
2305
|
+
}
|
|
2306
|
+
return leftValue * rightValue;
|
|
2307
|
+
case "/":
|
|
2308
|
+
if (typeof leftValue !== "number" || typeof rightValue !== "number") {
|
|
2309
|
+
throw new EvaluationError(`Cannot divide ${getTypeName(leftValue)} and ${getTypeName(rightValue)}`, position, this.source);
|
|
2310
|
+
}
|
|
2311
|
+
if (rightValue === 0) {
|
|
2312
|
+
throw new EvaluationError("Division by zero", position, this.source);
|
|
2313
|
+
}
|
|
2314
|
+
return leftValue / rightValue;
|
|
2315
|
+
case "%":
|
|
2316
|
+
if (typeof leftValue !== "number" || typeof rightValue !== "number") {
|
|
2317
|
+
throw new EvaluationError(`Cannot compute modulo of ${getTypeName(leftValue)} and ${getTypeName(rightValue)}`, position, this.source);
|
|
2318
|
+
}
|
|
2319
|
+
if (rightValue === 0) {
|
|
2320
|
+
throw new EvaluationError("Modulo by zero", position, this.source);
|
|
2321
|
+
}
|
|
2322
|
+
return leftValue % rightValue;
|
|
2323
|
+
// Comparison
|
|
2324
|
+
case "<":
|
|
2325
|
+
case "<=":
|
|
2326
|
+
case ">":
|
|
2327
|
+
case ">=":
|
|
2328
|
+
return this.evaluateComparison(operator, leftValue, rightValue, position);
|
|
2329
|
+
// Equality
|
|
2330
|
+
case "==":
|
|
2331
|
+
return this.valuesEqual(leftValue, rightValue);
|
|
2332
|
+
case "!=":
|
|
2333
|
+
return !this.valuesEqual(leftValue, rightValue);
|
|
2334
|
+
// Membership
|
|
2335
|
+
case "in":
|
|
2336
|
+
return this.evaluateIn(leftValue, rightValue, position);
|
|
2337
|
+
case "not in":
|
|
2338
|
+
return !this.evaluateIn(leftValue, rightValue, position);
|
|
2339
|
+
}
|
|
2340
|
+
}
|
|
2341
|
+
evaluateComparison(operator, left, right, position) {
|
|
2342
|
+
// Numbers
|
|
2343
|
+
if (typeof left === "number" && typeof right === "number") {
|
|
2344
|
+
switch (operator) {
|
|
2345
|
+
case "<":
|
|
2346
|
+
return left < right;
|
|
2347
|
+
case "<=":
|
|
2348
|
+
return left <= right;
|
|
2349
|
+
case ">":
|
|
2350
|
+
return left > right;
|
|
2351
|
+
case ">=":
|
|
2352
|
+
return left >= right;
|
|
2353
|
+
}
|
|
2354
|
+
}
|
|
2355
|
+
// Strings
|
|
2356
|
+
if (typeof left === "string" && typeof right === "string") {
|
|
2357
|
+
switch (operator) {
|
|
2358
|
+
case "<":
|
|
2359
|
+
return left < right;
|
|
2360
|
+
case "<=":
|
|
2361
|
+
return left <= right;
|
|
2362
|
+
case ">":
|
|
2363
|
+
return left > right;
|
|
2364
|
+
case ">=":
|
|
2365
|
+
return left >= right;
|
|
2366
|
+
}
|
|
2367
|
+
}
|
|
2368
|
+
throw new EvaluationError(`Cannot compare ${getTypeName(left)} and ${getTypeName(right)} with ${operator}`, position, this.source);
|
|
2369
|
+
}
|
|
2370
|
+
evaluateIn(left, right, position) {
|
|
2371
|
+
// String in string (substring check)
|
|
2372
|
+
if (typeof left === "string" && typeof right === "string") {
|
|
2373
|
+
return right.includes(left);
|
|
2374
|
+
}
|
|
2375
|
+
// Value in array
|
|
2376
|
+
if (Array.isArray(right)) {
|
|
2377
|
+
return right.some((item) => this.valuesEqual(left, item));
|
|
2378
|
+
}
|
|
2379
|
+
// Key in object
|
|
2380
|
+
if (typeof right === "object" && right !== null && !Array.isArray(right)) {
|
|
2381
|
+
if (typeof left !== "string") {
|
|
2382
|
+
throw new EvaluationError(`Cannot check if ${getTypeName(left)} is a key in object (expected string)`, position, this.source);
|
|
2383
|
+
}
|
|
2384
|
+
return left in right;
|
|
2385
|
+
}
|
|
2386
|
+
throw new EvaluationError(`Cannot check membership: ${getTypeName(left)} in ${getTypeName(right)}`, position, this.source);
|
|
2387
|
+
}
|
|
2388
|
+
evaluateTernaryOp(condition, consequent, alternate, _position) {
|
|
2389
|
+
const condValue = this.evaluate(condition);
|
|
2390
|
+
if (typeof condValue !== "boolean") {
|
|
2391
|
+
throw new TypeError("boolean", getTypeName(condValue), condition.position, this.source);
|
|
2392
|
+
}
|
|
2393
|
+
return condValue ? this.evaluate(consequent) : this.evaluate(alternate);
|
|
2394
|
+
}
|
|
2395
|
+
/**
|
|
2396
|
+
* Deep equality check for expression values.
|
|
2397
|
+
*/
|
|
2398
|
+
valuesEqual(a, b) {
|
|
2399
|
+
// Identical primitives or same reference
|
|
2400
|
+
if (a === b)
|
|
2401
|
+
return true;
|
|
2402
|
+
// Type mismatch
|
|
2403
|
+
if (typeof a !== typeof b)
|
|
2404
|
+
return false;
|
|
2405
|
+
// null check (both must be null if one is)
|
|
2406
|
+
if (a === null || b === null)
|
|
2407
|
+
return false;
|
|
2408
|
+
// Arrays
|
|
2409
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
2410
|
+
if (a.length !== b.length)
|
|
2411
|
+
return false;
|
|
2412
|
+
for (let i = 0; i < a.length; i++) {
|
|
2413
|
+
if (!this.valuesEqual(a[i], b[i])) {
|
|
2414
|
+
return false;
|
|
2415
|
+
}
|
|
2416
|
+
}
|
|
2417
|
+
return true;
|
|
2418
|
+
}
|
|
2419
|
+
// Objects
|
|
2420
|
+
if (typeof a === "object" && typeof b === "object") {
|
|
2421
|
+
const aKeys = Object.keys(a);
|
|
2422
|
+
const bKeys = Object.keys(b);
|
|
2423
|
+
if (aKeys.length !== bKeys.length)
|
|
2424
|
+
return false;
|
|
2425
|
+
for (const key of aKeys) {
|
|
2426
|
+
if (!Object.prototype.hasOwnProperty.call(b, key))
|
|
2427
|
+
return false;
|
|
2428
|
+
const aRecord = a;
|
|
2429
|
+
const bRecord = b;
|
|
2430
|
+
if (!this.valuesEqual(aRecord[key], bRecord[key])) {
|
|
2431
|
+
return false;
|
|
2432
|
+
}
|
|
2433
|
+
}
|
|
2434
|
+
return true;
|
|
2435
|
+
}
|
|
2436
|
+
return false;
|
|
2437
|
+
}
|
|
2438
|
+
}
|
|
2439
|
+
/**
|
|
2440
|
+
* Evaluates an AST as a boolean condition.
|
|
2441
|
+
*
|
|
2442
|
+
* @param ast - The AST to evaluate
|
|
2443
|
+
* @param context - The evaluation context with bindings
|
|
2444
|
+
* @returns true if the condition is met, false otherwise (including errors)
|
|
2445
|
+
*/
|
|
2446
|
+
function evaluateAsBoolean(ast, context) {
|
|
2447
|
+
try {
|
|
2448
|
+
const evaluator = new Evaluator(context);
|
|
2449
|
+
const value = evaluator.evaluateAsBoolean(ast);
|
|
2450
|
+
return { value };
|
|
2451
|
+
}
|
|
2452
|
+
catch (error) {
|
|
2453
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2454
|
+
return { value: false, error: message };
|
|
2455
|
+
}
|
|
2456
|
+
}
|
|
2457
|
+
|
|
2458
|
+
/**
|
|
2459
|
+
* Authorization-specific expression built-ins.
|
|
2460
|
+
*
|
|
2461
|
+
* Null handling semantics:
|
|
2462
|
+
* - Scope predicate builtins (has_scope, has_any_scope, has_all_scopes)
|
|
2463
|
+
* return `false` when passed `null` for required args.
|
|
2464
|
+
* - Wrong non-null types still raise BuiltinError to surface real bugs.
|
|
2465
|
+
*/
|
|
2466
|
+
/**
|
|
2467
|
+
* Checks if a value is null.
|
|
2468
|
+
*/
|
|
2469
|
+
function isNull(value) {
|
|
2470
|
+
return value === null;
|
|
2471
|
+
}
|
|
2472
|
+
/**
|
|
2473
|
+
* Creates a function registry with auth helpers installed.
|
|
2474
|
+
*/
|
|
2475
|
+
function createAuthFunctionRegistry(grantedScopes = []) {
|
|
2476
|
+
const scopes = grantedScopes ?? [];
|
|
2477
|
+
/**
|
|
2478
|
+
* Checks if any granted scope matches a pattern (using glob syntax).
|
|
2479
|
+
*/
|
|
2480
|
+
const matchesScope = (scope) => {
|
|
2481
|
+
// Exact match for now; safe and deterministic.
|
|
2482
|
+
return scopes.includes(scope);
|
|
2483
|
+
};
|
|
2484
|
+
/**
|
|
2485
|
+
* has_scope(scope: string) -> bool
|
|
2486
|
+
*
|
|
2487
|
+
* Returns true if the scope is in the granted scopes.
|
|
2488
|
+
* Null-tolerant: returns false if scope is null.
|
|
2489
|
+
*/
|
|
2490
|
+
const has_scope = (args) => {
|
|
2491
|
+
assertArgCount(args, 1, "has_scope");
|
|
2492
|
+
const scope = getArg(args, 0, "has_scope");
|
|
2493
|
+
// Null-tolerant: return false if scope is null
|
|
2494
|
+
if (!assertStringOrNull(scope, "scope", "has_scope"))
|
|
2495
|
+
return false;
|
|
2496
|
+
return matchesScope(scope);
|
|
2497
|
+
};
|
|
2498
|
+
/**
|
|
2499
|
+
* has_any_scope(scopes: string[]) -> bool
|
|
2500
|
+
*
|
|
2501
|
+
* Returns true if any scope in the array is in the granted scopes.
|
|
2502
|
+
* Null-tolerant: returns false if scopes is null.
|
|
2503
|
+
*/
|
|
2504
|
+
const has_any_scope = (args) => {
|
|
2505
|
+
assertArgCount(args, 1, "has_any_scope");
|
|
2506
|
+
const values = getArg(args, 0, "has_any_scope");
|
|
2507
|
+
// Null-tolerant: return false if scopes is null
|
|
2508
|
+
if (!assertStringArrayOrNull(values, "scopes", "has_any_scope"))
|
|
2509
|
+
return false;
|
|
2510
|
+
if (values.length === 0) {
|
|
2511
|
+
return false;
|
|
2512
|
+
}
|
|
2513
|
+
return values.some((scope) => matchesScope(scope));
|
|
2514
|
+
};
|
|
2515
|
+
/**
|
|
2516
|
+
* has_all_scopes(scopes: string[]) -> bool
|
|
2517
|
+
*
|
|
2518
|
+
* Returns true if all scopes in the array are in the granted scopes.
|
|
2519
|
+
* Null-tolerant: returns false if scopes is null.
|
|
2520
|
+
*/
|
|
2521
|
+
const has_all_scopes = (args) => {
|
|
2522
|
+
assertArgCount(args, 1, "has_all_scopes");
|
|
2523
|
+
const values = getArg(args, 0, "has_all_scopes");
|
|
2524
|
+
// Null-tolerant: return false if scopes is null
|
|
2525
|
+
if (!assertStringArrayOrNull(values, "scopes", "has_all_scopes"))
|
|
2526
|
+
return false;
|
|
2527
|
+
if (values.length === 0) {
|
|
2528
|
+
return true;
|
|
2529
|
+
}
|
|
2530
|
+
return values.every((scope) => matchesScope(scope));
|
|
2531
|
+
};
|
|
2532
|
+
return new Map([
|
|
2533
|
+
...BUILTIN_FUNCTIONS,
|
|
2534
|
+
["has_scope", has_scope],
|
|
2535
|
+
["has_any_scope", has_any_scope],
|
|
2536
|
+
["has_all_scopes", has_all_scopes],
|
|
2537
|
+
]);
|
|
2538
|
+
}
|
|
2539
|
+
/**
|
|
2540
|
+
* Asserts that a non-null value is a string (for null-tolerant predicates).
|
|
2541
|
+
* Returns false if the value is null (indicating predicate should return false).
|
|
2542
|
+
* Throws BuiltinError if the value is non-null but not a string.
|
|
2543
|
+
*/
|
|
2544
|
+
function assertStringOrNull(value, argName, functionName) {
|
|
2545
|
+
if (isNull(value)) {
|
|
2546
|
+
return false;
|
|
2547
|
+
}
|
|
2548
|
+
if (typeof value !== "string") {
|
|
2549
|
+
throw new BuiltinError(functionName, `${argName} must be a string, got ${getTypeName(value)}`);
|
|
2550
|
+
}
|
|
2551
|
+
return true;
|
|
2552
|
+
}
|
|
2553
|
+
/**
|
|
2554
|
+
* Asserts that a non-null value is an array of strings (for null-tolerant predicates).
|
|
2555
|
+
* Returns false if the value is null (indicating predicate should return false).
|
|
2556
|
+
* Throws BuiltinError if the value is non-null but not a string array.
|
|
2557
|
+
*/
|
|
2558
|
+
function assertStringArrayOrNull(value, argName, functionName) {
|
|
2559
|
+
if (isNull(value)) {
|
|
2560
|
+
return false;
|
|
2561
|
+
}
|
|
2562
|
+
if (!Array.isArray(value)) {
|
|
2563
|
+
throw new BuiltinError(functionName, `${argName} must be an array of strings, got ${getTypeName(value)}`);
|
|
2564
|
+
}
|
|
2565
|
+
for (let i = 0; i < value.length; i++) {
|
|
2566
|
+
if (typeof value[i] !== "string") {
|
|
2567
|
+
throw new BuiltinError(functionName, `${argName}[${i}] must be a string, got ${getTypeName(value[i])}`);
|
|
2568
|
+
}
|
|
2569
|
+
}
|
|
2570
|
+
return true;
|
|
2571
|
+
}
|
|
2572
|
+
function getArg(args, index, functionName) {
|
|
2573
|
+
const value = args[index];
|
|
2574
|
+
if (value === undefined) {
|
|
2575
|
+
throw new BuiltinError(functionName, `missing argument at index ${index}`);
|
|
2576
|
+
}
|
|
2577
|
+
return value;
|
|
2578
|
+
}
|
|
2579
|
+
function assertArgCount(args, expected, functionName) {
|
|
2580
|
+
if (args.length !== expected) {
|
|
2581
|
+
throw new BuiltinError(functionName, `expected ${expected} argument(s), got ${args.length}`);
|
|
2582
|
+
}
|
|
2583
|
+
}
|
|
2584
|
+
|
|
2585
|
+
/**
|
|
2586
|
+
* Expression-based authorization policy implementation.
|
|
2587
|
+
*
|
|
2588
|
+
* Extends the basic policy with support for `when` expression evaluation.
|
|
2589
|
+
* This is part of the BSL-licensed Advanced Security package.
|
|
2590
|
+
*/
|
|
2591
|
+
/**
|
|
2592
|
+
* Simple console logger implementation.
|
|
2593
|
+
*/
|
|
2594
|
+
const defaultLogger = {
|
|
2595
|
+
debug: () => { },
|
|
2596
|
+
warning: (event, data) => {
|
|
2597
|
+
console.warn(`[naylence.security.auth.policy.expression] ${event}`, data);
|
|
2598
|
+
},
|
|
2599
|
+
};
|
|
2600
|
+
/**
|
|
2601
|
+
* Extracts the target address string from the envelope.
|
|
2602
|
+
*/
|
|
2603
|
+
function extractAddress(envelope) {
|
|
2604
|
+
const to = envelope.to;
|
|
2605
|
+
if (!to) {
|
|
2606
|
+
return undefined;
|
|
2607
|
+
}
|
|
2608
|
+
if (typeof to === "string") {
|
|
2609
|
+
return to;
|
|
2610
|
+
}
|
|
2611
|
+
if (typeof to === "object" && "toString" in to) {
|
|
2612
|
+
return to.toString();
|
|
2613
|
+
}
|
|
2614
|
+
return undefined;
|
|
2615
|
+
}
|
|
2616
|
+
/**
|
|
2617
|
+
* Extracts granted scopes from the authorization context.
|
|
2618
|
+
*/
|
|
2619
|
+
function extractGrantedScopes(context) {
|
|
2620
|
+
const authContext = context?.security?.authorization;
|
|
2621
|
+
if (!authContext) {
|
|
2622
|
+
return [];
|
|
2623
|
+
}
|
|
2624
|
+
if (Array.isArray(authContext.grantedScopes)) {
|
|
2625
|
+
return authContext.grantedScopes;
|
|
2626
|
+
}
|
|
2627
|
+
const claims = authContext.claims;
|
|
2628
|
+
if (claims) {
|
|
2629
|
+
const scopeClaim = claims.scope ?? claims.scopes ?? claims.scp;
|
|
2630
|
+
if (typeof scopeClaim === "string") {
|
|
2631
|
+
return scopeClaim.split(/\s+/).filter((s) => s.length > 0);
|
|
2632
|
+
}
|
|
2633
|
+
if (Array.isArray(scopeClaim)) {
|
|
2634
|
+
return scopeClaim.filter((s) => typeof s === "string");
|
|
2635
|
+
}
|
|
2636
|
+
}
|
|
2637
|
+
return [];
|
|
2638
|
+
}
|
|
2639
|
+
/**
|
|
2640
|
+
* Extracts claims from the authorization context.
|
|
2641
|
+
*/
|
|
2642
|
+
function extractClaims(context) {
|
|
2643
|
+
const authContext = context?.security?.authorization;
|
|
2644
|
+
if (!authContext?.claims) {
|
|
2645
|
+
return {};
|
|
2646
|
+
}
|
|
2647
|
+
return authContext.claims;
|
|
2648
|
+
}
|
|
2649
|
+
/**
|
|
2650
|
+
* Creates a safe envelope subset for expression bindings.
|
|
2651
|
+
*/
|
|
2652
|
+
function createEnvelopeBindings(envelope) {
|
|
2653
|
+
const frame = envelope.frame;
|
|
2654
|
+
const envelopeRecord = envelope;
|
|
2655
|
+
return {
|
|
2656
|
+
id: envelope.id ?? null,
|
|
2657
|
+
traceId: envelopeRecord.traceId ?? null,
|
|
2658
|
+
corrId: envelopeRecord.corrId ?? null,
|
|
2659
|
+
flowId: envelopeRecord.flowId ?? null,
|
|
2660
|
+
to: extractAddress(envelope) ?? null,
|
|
2661
|
+
frame: frame
|
|
2662
|
+
? { type: frame.type ?? null }
|
|
2663
|
+
: { type: null },
|
|
2664
|
+
};
|
|
2665
|
+
}
|
|
2666
|
+
/**
|
|
2667
|
+
* Creates delivery context bindings for expression evaluation.
|
|
2668
|
+
*/
|
|
2669
|
+
function createDeliveryBindings(context, action) {
|
|
2670
|
+
return {
|
|
2671
|
+
origin_type: context?.originType ?? null,
|
|
2672
|
+
routing_action: action,
|
|
2673
|
+
};
|
|
2674
|
+
}
|
|
2675
|
+
/**
|
|
2676
|
+
* Expression-based authorization policy that evaluates rules with `when` expressions.
|
|
2677
|
+
*
|
|
2678
|
+
* Features:
|
|
2679
|
+
* - All features of BasicAuthorizationPolicy
|
|
2680
|
+
* - Expression evaluation for `when` clauses
|
|
2681
|
+
* - Deterministic, side-effect-free evaluation
|
|
2682
|
+
* - Missing fields evaluate to null (not error)
|
|
2683
|
+
* - Parse/evaluation errors cause rule to not match
|
|
2684
|
+
*/
|
|
2685
|
+
class AdvancedAuthorizationPolicy {
|
|
2686
|
+
constructor(options) {
|
|
2687
|
+
const { policyDefinition, warnOnUnknownFields = true, expressionLimits = DEFAULT_EXPRESSION_LIMITS, logger = defaultLogger, } = options;
|
|
2688
|
+
this.expressionLimits = expressionLimits;
|
|
2689
|
+
this.logger = logger;
|
|
2690
|
+
// Validate and extract default effect
|
|
2691
|
+
this.defaultEffect = this.validateDefaultEffect(policyDefinition.default_effect);
|
|
2692
|
+
// Warn about unknown policy fields
|
|
2693
|
+
if (warnOnUnknownFields) {
|
|
2694
|
+
this.warnUnknownPolicyFields(policyDefinition);
|
|
2695
|
+
}
|
|
2696
|
+
// Compile rules for efficient evaluation
|
|
2697
|
+
this.compiledRules = this.compileRules(policyDefinition.rules, warnOnUnknownFields);
|
|
2698
|
+
this.logger.debug("expression_policy_compiled", {
|
|
2699
|
+
defaultEffect: this.defaultEffect,
|
|
2700
|
+
ruleCount: this.compiledRules.length,
|
|
2701
|
+
rulesWithWhen: this.compiledRules.filter((r) => r.whenAst).length,
|
|
2702
|
+
});
|
|
2703
|
+
}
|
|
2704
|
+
/**
|
|
2705
|
+
* Evaluates the policy against a request.
|
|
2706
|
+
*/
|
|
2707
|
+
async evaluateRequest(_node, envelope, context, action) {
|
|
2708
|
+
const resolvedAction = action ?? "*";
|
|
2709
|
+
const resolvedActionNormalized = this.normalizeActionToken(resolvedAction) ?? resolvedAction;
|
|
2710
|
+
const address = extractAddress(envelope);
|
|
2711
|
+
const grantedScopes = extractGrantedScopes(context);
|
|
2712
|
+
const rawFrameType = envelope.frame
|
|
2713
|
+
?.type;
|
|
2714
|
+
const frameTypeNormalized = typeof rawFrameType === "string" && rawFrameType.trim().length > 0
|
|
2715
|
+
? rawFrameType.trim().toLowerCase()
|
|
2716
|
+
: "";
|
|
2717
|
+
const rawOriginType = context?.originType;
|
|
2718
|
+
const originTypeNormalized = typeof rawOriginType === "string"
|
|
2719
|
+
? this.normalizeOriginTypeToken(rawOriginType) ?? undefined
|
|
2720
|
+
: undefined;
|
|
2721
|
+
// Prepare expression bindings (lazy)
|
|
2722
|
+
let expressionBindings = null;
|
|
2723
|
+
let functionRegistry = null;
|
|
2724
|
+
const evaluationTrace = [];
|
|
2725
|
+
// Evaluate rules in order (first match wins)
|
|
2726
|
+
for (const rule of this.compiledRules) {
|
|
2727
|
+
const step = {
|
|
2728
|
+
ruleId: rule.id,
|
|
2729
|
+
result: false,
|
|
2730
|
+
};
|
|
2731
|
+
// Check frame type match
|
|
2732
|
+
if (rule.frameTypes) {
|
|
2733
|
+
if (!frameTypeNormalized) {
|
|
2734
|
+
step.expression = "frame_type: missing";
|
|
2735
|
+
step.result = false;
|
|
2736
|
+
evaluationTrace.push(step);
|
|
2737
|
+
continue;
|
|
2738
|
+
}
|
|
2739
|
+
if (!rule.frameTypes.has(frameTypeNormalized)) {
|
|
2740
|
+
step.expression = `frame_type: ${rawFrameType ?? "unknown"} not in rule set`;
|
|
2741
|
+
step.result = false;
|
|
2742
|
+
evaluationTrace.push(step);
|
|
2743
|
+
continue;
|
|
2744
|
+
}
|
|
2745
|
+
}
|
|
2746
|
+
// Check origin type match
|
|
2747
|
+
if (rule.originTypes) {
|
|
2748
|
+
if (originTypeNormalized === undefined) {
|
|
2749
|
+
step.expression = "origin_type: missing (rule requires origin)";
|
|
2750
|
+
step.result = false;
|
|
2751
|
+
evaluationTrace.push(step);
|
|
2752
|
+
continue;
|
|
2753
|
+
}
|
|
2754
|
+
if (!rule.originTypes.has(originTypeNormalized)) {
|
|
2755
|
+
step.expression = `origin_type: ${rawOriginType ?? "unknown"} not in [${Array.from(rule.originTypes).join(", ")}]`;
|
|
2756
|
+
step.result = false;
|
|
2757
|
+
evaluationTrace.push(step);
|
|
2758
|
+
continue;
|
|
2759
|
+
}
|
|
2760
|
+
}
|
|
2761
|
+
// Check action match
|
|
2762
|
+
if (!rule.actions.has("*") && !rule.actions.has(resolvedActionNormalized)) {
|
|
2763
|
+
step.expression = `action: ${resolvedActionNormalized} not in [${Array.from(rule.actions).join(", ")}]`;
|
|
2764
|
+
step.result = false;
|
|
2765
|
+
evaluationTrace.push(step);
|
|
2766
|
+
continue;
|
|
2767
|
+
}
|
|
2768
|
+
// Check address match
|
|
2769
|
+
if (rule.addressPatterns) {
|
|
2770
|
+
if (!address) {
|
|
2771
|
+
step.expression = "address: pattern requires address, but none provided";
|
|
2772
|
+
step.result = false;
|
|
2773
|
+
evaluationTrace.push(step);
|
|
2774
|
+
continue;
|
|
2775
|
+
}
|
|
2776
|
+
const matched = rule.addressPatterns.some((p) => p.match(address));
|
|
2777
|
+
if (!matched) {
|
|
2778
|
+
const patterns = rule.addressPatterns.map((p) => p.source).join(", ");
|
|
2779
|
+
step.expression = `address: none of [${patterns}] matched ${address}`;
|
|
2780
|
+
step.result = false;
|
|
2781
|
+
evaluationTrace.push(step);
|
|
2782
|
+
continue;
|
|
2783
|
+
}
|
|
2784
|
+
}
|
|
2785
|
+
// Check scope match
|
|
2786
|
+
if (rule.scopeMatcher) {
|
|
2787
|
+
if (!rule.scopeMatcher(grantedScopes)) {
|
|
2788
|
+
step.expression = "scope: requirement not satisfied";
|
|
2789
|
+
step.boundValues = { grantedScopes: [...grantedScopes] };
|
|
2790
|
+
step.result = false;
|
|
2791
|
+
evaluationTrace.push(step);
|
|
2792
|
+
continue;
|
|
2793
|
+
}
|
|
2794
|
+
}
|
|
2795
|
+
// Check when expression
|
|
2796
|
+
if (rule.whenParseError) {
|
|
2797
|
+
// Parse error - rule does not match
|
|
2798
|
+
step.expression = `when: parse error - ${rule.whenParseError}`;
|
|
2799
|
+
step.result = false;
|
|
2800
|
+
evaluationTrace.push(step);
|
|
2801
|
+
continue;
|
|
2802
|
+
}
|
|
2803
|
+
if (rule.whenAst) {
|
|
2804
|
+
// Lazy initialization of expression bindings
|
|
2805
|
+
if (!expressionBindings) {
|
|
2806
|
+
expressionBindings = {
|
|
2807
|
+
claims: extractClaims(context),
|
|
2808
|
+
envelope: createEnvelopeBindings(envelope),
|
|
2809
|
+
delivery: createDeliveryBindings(context, resolvedAction),
|
|
2810
|
+
time: {
|
|
2811
|
+
now_ms: Date.now(),
|
|
2812
|
+
now_iso: new Date().toISOString(),
|
|
2813
|
+
},
|
|
2814
|
+
};
|
|
2815
|
+
}
|
|
2816
|
+
const functions = functionRegistry ?? createAuthFunctionRegistry(grantedScopes);
|
|
2817
|
+
functionRegistry = functions;
|
|
2818
|
+
const evalContext = {
|
|
2819
|
+
bindings: expressionBindings,
|
|
2820
|
+
limits: this.expressionLimits,
|
|
2821
|
+
source: rule.whenSource,
|
|
2822
|
+
functions,
|
|
2823
|
+
};
|
|
2824
|
+
const whenResult = evaluateAsBoolean(rule.whenAst, evalContext);
|
|
2825
|
+
if (whenResult.error) {
|
|
2826
|
+
// Evaluation error - rule does not match
|
|
2827
|
+
step.expression = `when: evaluation error - ${whenResult.error}`;
|
|
2828
|
+
step.result = false;
|
|
2829
|
+
evaluationTrace.push(step);
|
|
2830
|
+
continue;
|
|
2831
|
+
}
|
|
2832
|
+
if (!whenResult.value) {
|
|
2833
|
+
// Expression evaluated to false
|
|
2834
|
+
step.expression = `when: expression evaluated to false`;
|
|
2835
|
+
step.boundValues = {
|
|
2836
|
+
whenExpression: rule.whenSource,
|
|
2837
|
+
};
|
|
2838
|
+
step.result = false;
|
|
2839
|
+
evaluationTrace.push(step);
|
|
2840
|
+
continue;
|
|
2841
|
+
}
|
|
2842
|
+
// Expression evaluated to true
|
|
2843
|
+
step.expression = `when: expression evaluated to true`;
|
|
2844
|
+
}
|
|
2845
|
+
// Rule matched
|
|
2846
|
+
step.result = true;
|
|
2847
|
+
if (!step.expression) {
|
|
2848
|
+
step.expression = "all conditions matched";
|
|
2849
|
+
}
|
|
2850
|
+
step.boundValues = {
|
|
2851
|
+
action: resolvedAction,
|
|
2852
|
+
address,
|
|
2853
|
+
grantedScopes: [...grantedScopes],
|
|
2854
|
+
...(rule.whenSource ? { whenExpression: rule.whenSource } : {}),
|
|
2855
|
+
};
|
|
2856
|
+
evaluationTrace.push(step);
|
|
2857
|
+
this.logger.debug("rule_matched", {
|
|
2858
|
+
ruleId: rule.id,
|
|
2859
|
+
effect: rule.effect,
|
|
2860
|
+
action: resolvedAction,
|
|
2861
|
+
address,
|
|
2862
|
+
hadWhenClause: Boolean(rule.whenAst),
|
|
2863
|
+
});
|
|
2864
|
+
return {
|
|
2865
|
+
effect: rule.effect,
|
|
2866
|
+
reason: rule.description ?? `Matched rule: ${rule.id}`,
|
|
2867
|
+
matchedRule: rule.id,
|
|
2868
|
+
evaluationTrace,
|
|
2869
|
+
};
|
|
2870
|
+
}
|
|
2871
|
+
// No rule matched, apply default effect
|
|
2872
|
+
this.logger.debug("no_rule_matched", {
|
|
2873
|
+
defaultEffect: this.defaultEffect,
|
|
2874
|
+
action: resolvedAction,
|
|
2875
|
+
address,
|
|
2876
|
+
});
|
|
2877
|
+
return {
|
|
2878
|
+
effect: this.defaultEffect,
|
|
2879
|
+
reason: `No rule matched, applying default effect: ${this.defaultEffect}`,
|
|
2880
|
+
evaluationTrace,
|
|
2881
|
+
};
|
|
2882
|
+
}
|
|
2883
|
+
validateDefaultEffect(effect) {
|
|
2884
|
+
if (effect === undefined || effect === null) {
|
|
2885
|
+
return "deny";
|
|
2886
|
+
}
|
|
2887
|
+
if (effect !== "allow" && effect !== "deny") {
|
|
2888
|
+
throw new Error(`Invalid default_effect: "${String(effect)}". Must be "allow" or "deny"`);
|
|
2889
|
+
}
|
|
2890
|
+
return effect;
|
|
2891
|
+
}
|
|
2892
|
+
warnUnknownPolicyFields(definition) {
|
|
2893
|
+
for (const key of Object.keys(definition)) {
|
|
2894
|
+
if (!runtime.KNOWN_POLICY_FIELDS.has(key)) {
|
|
2895
|
+
this.logger.warning("unknown_policy_field", { field: key });
|
|
2896
|
+
}
|
|
2897
|
+
}
|
|
2898
|
+
}
|
|
2899
|
+
compileRules(rules, warnOnUnknown) {
|
|
2900
|
+
return rules.map((rule, index) => this.compileRule(rule, index, warnOnUnknown));
|
|
2901
|
+
}
|
|
2902
|
+
compileRule(rule, index, warnOnUnknown) {
|
|
2903
|
+
const id = rule.id ?? `rule_${index}`;
|
|
2904
|
+
// Validate effect
|
|
2905
|
+
if (!runtime.VALID_EFFECTS.includes(rule.effect)) {
|
|
2906
|
+
throw new Error(`Invalid effect in rule "${id}": "${String(rule.effect)}". Must be "allow" or "deny"`);
|
|
2907
|
+
}
|
|
2908
|
+
// Compile action(s)
|
|
2909
|
+
const actions = this.compileActions(rule.action, id);
|
|
2910
|
+
// Compile address patterns
|
|
2911
|
+
const addressPatterns = this.compileAddress(rule.address, id);
|
|
2912
|
+
// Compile frame type gating
|
|
2913
|
+
const frameTypes = this.compileFrameTypes(rule.frame_type, id);
|
|
2914
|
+
// Compile origin type gating
|
|
2915
|
+
const originTypes = this.compileOriginTypes(rule.origin_type, id);
|
|
2916
|
+
// Compile scope matcher
|
|
2917
|
+
let scopeMatcher;
|
|
2918
|
+
if (rule.scope !== undefined) {
|
|
2919
|
+
try {
|
|
2920
|
+
const compiled = runtime.compileGlobOnlyScopeRequirement(rule.scope, id);
|
|
2921
|
+
scopeMatcher = (scopes) => compiled.evaluate(scopes);
|
|
2922
|
+
}
|
|
2923
|
+
catch (error) {
|
|
2924
|
+
throw new Error(`Invalid scope requirement in rule "${id}": ${error instanceof Error ? error.message : String(error)}`);
|
|
2925
|
+
}
|
|
2926
|
+
}
|
|
2927
|
+
// Compile when expression
|
|
2928
|
+
let whenAst;
|
|
2929
|
+
let whenSource;
|
|
2930
|
+
let whenParseError;
|
|
2931
|
+
if (typeof rule.when === "string" && rule.when.trim().length > 0) {
|
|
2932
|
+
whenSource = rule.when.trim();
|
|
2933
|
+
try {
|
|
2934
|
+
whenAst = parse(whenSource, this.expressionLimits);
|
|
2935
|
+
}
|
|
2936
|
+
catch (error) {
|
|
2937
|
+
// Parse error - store for evaluation time
|
|
2938
|
+
whenParseError =
|
|
2939
|
+
error instanceof Error ? error.message : String(error);
|
|
2940
|
+
this.logger.warning("when_parse_error", {
|
|
2941
|
+
ruleId: id,
|
|
2942
|
+
expression: whenSource,
|
|
2943
|
+
error: whenParseError,
|
|
2944
|
+
});
|
|
2945
|
+
}
|
|
2946
|
+
}
|
|
2947
|
+
// Warn about unknown fields
|
|
2948
|
+
if (warnOnUnknown) {
|
|
2949
|
+
for (const key of Object.keys(rule)) {
|
|
2950
|
+
if (!runtime.KNOWN_RULE_FIELDS.has(key)) {
|
|
2951
|
+
this.logger.warning("unknown_rule_field", { ruleId: id, field: key });
|
|
2952
|
+
}
|
|
2953
|
+
}
|
|
2954
|
+
}
|
|
2955
|
+
return {
|
|
2956
|
+
id,
|
|
2957
|
+
description: rule.description,
|
|
2958
|
+
effect: rule.effect,
|
|
2959
|
+
actions,
|
|
2960
|
+
frameTypes,
|
|
2961
|
+
originTypes,
|
|
2962
|
+
addressPatterns,
|
|
2963
|
+
scopeMatcher,
|
|
2964
|
+
whenAst,
|
|
2965
|
+
whenSource,
|
|
2966
|
+
whenParseError,
|
|
2967
|
+
};
|
|
2968
|
+
}
|
|
2969
|
+
compileActions(action, ruleId) {
|
|
2970
|
+
if (action === undefined) {
|
|
2971
|
+
return new Set(["*"]);
|
|
2972
|
+
}
|
|
2973
|
+
if (typeof action === "string") {
|
|
2974
|
+
const normalized = this.normalizeActionToken(action);
|
|
2975
|
+
if (!normalized) {
|
|
2976
|
+
throw new Error(`Invalid action in rule "${ruleId}": "${action}". Must be one of: ${runtime.VALID_ACTIONS.join(", ")}`);
|
|
2977
|
+
}
|
|
2978
|
+
return new Set([normalized]);
|
|
2979
|
+
}
|
|
2980
|
+
if (!Array.isArray(action)) {
|
|
2981
|
+
throw new Error(`Invalid action in rule "${ruleId}": must be a string or array of strings`);
|
|
2982
|
+
}
|
|
2983
|
+
if (action.length === 0) {
|
|
2984
|
+
throw new Error(`Invalid action in rule "${ruleId}": array must not be empty`);
|
|
2985
|
+
}
|
|
2986
|
+
const actions = new Set();
|
|
2987
|
+
for (const a of action) {
|
|
2988
|
+
if (typeof a !== "string") {
|
|
2989
|
+
throw new Error(`Invalid action in rule "${ruleId}": all values must be strings`);
|
|
2990
|
+
}
|
|
2991
|
+
const normalized = this.normalizeActionToken(a);
|
|
2992
|
+
if (!normalized) {
|
|
2993
|
+
throw new Error(`Invalid action in rule "${ruleId}": "${a}". Must be one of: ${runtime.VALID_ACTIONS.join(", ")}`);
|
|
2994
|
+
}
|
|
2995
|
+
actions.add(normalized);
|
|
2996
|
+
}
|
|
2997
|
+
return actions;
|
|
2998
|
+
}
|
|
2999
|
+
compileAddress(address, ruleId) {
|
|
3000
|
+
if (address === undefined) {
|
|
3001
|
+
return undefined;
|
|
3002
|
+
}
|
|
3003
|
+
const context = `address in rule "${ruleId}"`;
|
|
3004
|
+
if (typeof address === "string") {
|
|
3005
|
+
const trimmed = address.trim();
|
|
3006
|
+
if (!trimmed) {
|
|
3007
|
+
throw new Error(`Invalid address in rule "${ruleId}": value must not be empty`);
|
|
3008
|
+
}
|
|
3009
|
+
try {
|
|
3010
|
+
return [runtime.compileGlobPattern(trimmed, context)];
|
|
3011
|
+
}
|
|
3012
|
+
catch (error) {
|
|
3013
|
+
throw new Error(`Invalid address in rule "${ruleId}": ${error instanceof Error ? error.message : String(error)}`);
|
|
3014
|
+
}
|
|
3015
|
+
}
|
|
3016
|
+
if (!Array.isArray(address)) {
|
|
3017
|
+
throw new Error(`Invalid address in rule "${ruleId}": must be a string or array of strings`);
|
|
3018
|
+
}
|
|
3019
|
+
if (address.length === 0) {
|
|
3020
|
+
throw new Error(`Invalid address in rule "${ruleId}": array must not be empty`);
|
|
3021
|
+
}
|
|
3022
|
+
const patterns = [];
|
|
3023
|
+
for (const addr of address) {
|
|
3024
|
+
if (typeof addr !== "string") {
|
|
3025
|
+
throw new Error(`Invalid address in rule "${ruleId}": all values must be strings`);
|
|
3026
|
+
}
|
|
3027
|
+
const trimmed = addr.trim();
|
|
3028
|
+
if (!trimmed) {
|
|
3029
|
+
throw new Error(`Invalid address in rule "${ruleId}": values must not be empty`);
|
|
3030
|
+
}
|
|
3031
|
+
try {
|
|
3032
|
+
patterns.push(runtime.compileGlobPattern(trimmed, context));
|
|
3033
|
+
}
|
|
3034
|
+
catch (error) {
|
|
3035
|
+
throw new Error(`Invalid address in rule "${ruleId}": ${error instanceof Error ? error.message : String(error)}`);
|
|
3036
|
+
}
|
|
3037
|
+
}
|
|
3038
|
+
return patterns;
|
|
3039
|
+
}
|
|
3040
|
+
compileFrameTypes(frameType, ruleId) {
|
|
3041
|
+
if (frameType === undefined) {
|
|
3042
|
+
return undefined;
|
|
3043
|
+
}
|
|
3044
|
+
if (typeof frameType === "string") {
|
|
3045
|
+
const normalized = frameType.trim().toLowerCase();
|
|
3046
|
+
if (!normalized) {
|
|
3047
|
+
throw new Error(`Invalid frame_type in rule "${ruleId}": value must not be empty`);
|
|
3048
|
+
}
|
|
3049
|
+
return new Set([normalized]);
|
|
3050
|
+
}
|
|
3051
|
+
if (!Array.isArray(frameType)) {
|
|
3052
|
+
throw new Error(`Invalid frame_type in rule "${ruleId}": must be a string or array of strings`);
|
|
3053
|
+
}
|
|
3054
|
+
if (frameType.length === 0) {
|
|
3055
|
+
throw new Error(`Invalid frame_type in rule "${ruleId}": array must not be empty`);
|
|
3056
|
+
}
|
|
3057
|
+
const frameTypes = new Set();
|
|
3058
|
+
for (const ft of frameType) {
|
|
3059
|
+
if (typeof ft !== "string") {
|
|
3060
|
+
throw new Error(`Invalid frame_type in rule "${ruleId}": all values must be strings`);
|
|
3061
|
+
}
|
|
3062
|
+
const normalized = ft.trim().toLowerCase();
|
|
3063
|
+
if (!normalized) {
|
|
3064
|
+
throw new Error(`Invalid frame_type in rule "${ruleId}": values must not be empty`);
|
|
3065
|
+
}
|
|
3066
|
+
frameTypes.add(normalized);
|
|
3067
|
+
}
|
|
3068
|
+
return frameTypes;
|
|
3069
|
+
}
|
|
3070
|
+
compileOriginTypes(originType, ruleId) {
|
|
3071
|
+
if (originType === undefined) {
|
|
3072
|
+
return undefined;
|
|
3073
|
+
}
|
|
3074
|
+
if (typeof originType === "string") {
|
|
3075
|
+
const trimmed = originType.trim();
|
|
3076
|
+
if (!trimmed) {
|
|
3077
|
+
throw new Error(`Invalid origin_type in rule "${ruleId}": value must not be empty`);
|
|
3078
|
+
}
|
|
3079
|
+
const normalized = this.normalizeOriginTypeToken(trimmed);
|
|
3080
|
+
if (!normalized) {
|
|
3081
|
+
throw new Error(`Invalid origin_type in rule "${ruleId}": "${originType}". Must be one of: ${runtime.VALID_ORIGIN_TYPES.join(", ")}`);
|
|
3082
|
+
}
|
|
3083
|
+
return new Set([normalized]);
|
|
3084
|
+
}
|
|
3085
|
+
if (!Array.isArray(originType)) {
|
|
3086
|
+
throw new Error(`Invalid origin_type in rule "${ruleId}": must be a string or array of strings`);
|
|
3087
|
+
}
|
|
3088
|
+
if (originType.length === 0) {
|
|
3089
|
+
throw new Error(`Invalid origin_type in rule "${ruleId}": array must not be empty`);
|
|
3090
|
+
}
|
|
3091
|
+
const originTypes = new Set();
|
|
3092
|
+
for (const ot of originType) {
|
|
3093
|
+
if (typeof ot !== "string") {
|
|
3094
|
+
throw new Error(`Invalid origin_type in rule "${ruleId}": all values must be strings`);
|
|
3095
|
+
}
|
|
3096
|
+
const trimmed = ot.trim();
|
|
3097
|
+
if (!trimmed) {
|
|
3098
|
+
throw new Error(`Invalid origin_type in rule "${ruleId}": values must not be empty`);
|
|
3099
|
+
}
|
|
3100
|
+
const normalized = this.normalizeOriginTypeToken(trimmed);
|
|
3101
|
+
if (!normalized) {
|
|
3102
|
+
throw new Error(`Invalid origin_type in rule "${ruleId}": "${ot}". Must be one of: ${runtime.VALID_ORIGIN_TYPES.join(", ")}`);
|
|
3103
|
+
}
|
|
3104
|
+
originTypes.add(normalized);
|
|
3105
|
+
}
|
|
3106
|
+
return originTypes;
|
|
3107
|
+
}
|
|
3108
|
+
normalizeActionToken(value) {
|
|
3109
|
+
const trimmed = value.trim();
|
|
3110
|
+
if (!trimmed) {
|
|
3111
|
+
return null;
|
|
3112
|
+
}
|
|
3113
|
+
if (trimmed === "*") {
|
|
3114
|
+
return "*";
|
|
3115
|
+
}
|
|
3116
|
+
const normalized = trimmed.replace(/[\s_-]+/g, "").toLowerCase();
|
|
3117
|
+
const map = {
|
|
3118
|
+
connect: "Connect",
|
|
3119
|
+
forwardupstream: "ForwardUpstream",
|
|
3120
|
+
forwarddownstream: "ForwardDownstream",
|
|
3121
|
+
forwardpeer: "ForwardPeer",
|
|
3122
|
+
deliverlocal: "DeliverLocal",
|
|
3123
|
+
};
|
|
3124
|
+
return map[normalized] ?? null;
|
|
3125
|
+
}
|
|
3126
|
+
normalizeOriginTypeToken(value) {
|
|
3127
|
+
const trimmed = value.trim();
|
|
3128
|
+
if (!trimmed) {
|
|
3129
|
+
return null;
|
|
3130
|
+
}
|
|
3131
|
+
const normalized = trimmed.replace(/[\s_-]+/g, "").toLowerCase();
|
|
3132
|
+
const map = {
|
|
3133
|
+
downstream: "downstream",
|
|
3134
|
+
upstream: "upstream",
|
|
3135
|
+
peer: "peer",
|
|
3136
|
+
local: "local",
|
|
3137
|
+
};
|
|
3138
|
+
return map[normalized] ?? null;
|
|
3139
|
+
}
|
|
3140
|
+
}
|
|
3141
|
+
|
|
3142
|
+
var advancedAuthorizationPolicy = /*#__PURE__*/Object.freeze({
|
|
3143
|
+
__proto__: null,
|
|
3144
|
+
AdvancedAuthorizationPolicy: AdvancedAuthorizationPolicy
|
|
3145
|
+
});
|
|
3146
|
+
|
|
3147
|
+
/**
|
|
3148
|
+
* Factory for creating AdvancedAuthorizationPolicy instances.
|
|
3149
|
+
*/
|
|
3150
|
+
let modulePromise = null;
|
|
3151
|
+
function getModule() {
|
|
3152
|
+
if (!modulePromise) {
|
|
3153
|
+
modulePromise = Promise.resolve().then(function () { return advancedAuthorizationPolicy; });
|
|
3154
|
+
}
|
|
3155
|
+
return modulePromise;
|
|
3156
|
+
}
|
|
3157
|
+
function normalizeConfig$5(config) {
|
|
3158
|
+
if (!config) {
|
|
3159
|
+
throw new Error("AdvancedAuthorizationPolicyFactory requires a configuration with a policyDefinition");
|
|
3160
|
+
}
|
|
3161
|
+
const candidate = config;
|
|
3162
|
+
// Support both camelCase and snake_case for policyDefinition
|
|
3163
|
+
const policyDefinition = (candidate.policyDefinition ??
|
|
3164
|
+
candidate.policy_definition);
|
|
3165
|
+
if (!policyDefinition || typeof policyDefinition !== "object") {
|
|
3166
|
+
throw new Error("AdvancedAuthorizationPolicyConfig requires a policyDefinition object");
|
|
3167
|
+
}
|
|
3168
|
+
// Support both camelCase and snake_case for warnOnUnknownFields
|
|
3169
|
+
const warnOnUnknownFields = candidate.warnOnUnknownFields ?? candidate.warn_on_unknown_fields;
|
|
3170
|
+
if (warnOnUnknownFields !== undefined &&
|
|
3171
|
+
typeof warnOnUnknownFields !== "boolean") {
|
|
3172
|
+
throw new Error("warnOnUnknownFields must be a boolean");
|
|
3173
|
+
}
|
|
3174
|
+
// Support both camelCase and snake_case for expressionLimits
|
|
3175
|
+
const expressionLimits = (candidate.expressionLimits ??
|
|
3176
|
+
candidate.expression_limits);
|
|
3177
|
+
return {
|
|
3178
|
+
policyDefinition,
|
|
3179
|
+
warnOnUnknownFields: warnOnUnknownFields ?? true,
|
|
3180
|
+
expressionLimits,
|
|
3181
|
+
};
|
|
3182
|
+
}
|
|
3183
|
+
/**
|
|
3184
|
+
* Factory metadata for registration.
|
|
3185
|
+
*/
|
|
3186
|
+
const FACTORY_META$e = {
|
|
3187
|
+
base: runtime.AUTHORIZATION_POLICY_FACTORY_BASE_TYPE,
|
|
3188
|
+
key: "AdvancedAuthorizationPolicy",
|
|
3189
|
+
};
|
|
3190
|
+
/**
|
|
3191
|
+
* Factory for creating AdvancedAuthorizationPolicy instances.
|
|
3192
|
+
*/
|
|
3193
|
+
class AdvancedAuthorizationPolicyFactory extends runtime.AuthorizationPolicyFactory {
|
|
3194
|
+
constructor() {
|
|
3195
|
+
super(...arguments);
|
|
3196
|
+
this.type = "AdvancedAuthorizationPolicy";
|
|
3197
|
+
}
|
|
3198
|
+
/**
|
|
3199
|
+
* Creates an AdvancedAuthorizationPolicy from the given configuration.
|
|
3200
|
+
*
|
|
3201
|
+
* @param config - Configuration with policyDefinition
|
|
3202
|
+
* @returns The created authorization policy
|
|
3203
|
+
*/
|
|
3204
|
+
async create(config) {
|
|
3205
|
+
const normalized = normalizeConfig$5(config);
|
|
3206
|
+
const { AdvancedAuthorizationPolicy } = await getModule();
|
|
3207
|
+
return new AdvancedAuthorizationPolicy({
|
|
3208
|
+
policyDefinition: normalized.policyDefinition,
|
|
3209
|
+
warnOnUnknownFields: normalized.warnOnUnknownFields,
|
|
3210
|
+
expressionLimits: normalized.expressionLimits,
|
|
3211
|
+
});
|
|
3212
|
+
}
|
|
3213
|
+
}
|
|
3214
|
+
|
|
3215
|
+
var advancedAuthorizationPolicyFactory = /*#__PURE__*/Object.freeze({
|
|
3216
|
+
__proto__: null,
|
|
3217
|
+
AdvancedAuthorizationPolicyFactory: AdvancedAuthorizationPolicyFactory,
|
|
3218
|
+
FACTORY_META: FACTORY_META$e,
|
|
3219
|
+
default: AdvancedAuthorizationPolicyFactory
|
|
3220
|
+
});
|
|
3221
|
+
|
|
552
3222
|
const logger$g = runtime.getLogger("naylence.fame.security.cert.util");
|
|
553
3223
|
const CACHE_LIMIT = 512;
|
|
554
3224
|
const OID_ED25519 = "1.3.101.112";
|