@naylence/advanced-security 0.4.4 → 0.4.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/dist/browser/index.cjs +201 -18
  2. package/dist/browser/index.mjs +200 -17
  3. package/dist/cjs/naylence/fame/expr/builtins.js +1 -1
  4. package/dist/cjs/naylence/fame/expr/builtins.js.map +1 -1
  5. package/dist/cjs/naylence/fame/security/auth/policy/advanced-authorization-policy.js +32 -13
  6. package/dist/cjs/naylence/fame/security/auth/policy/advanced-authorization-policy.js.map +1 -1
  7. package/dist/cjs/naylence/fame/security/auth/policy/expr-builtins.js +166 -2
  8. package/dist/cjs/naylence/fame/security/auth/policy/expr-builtins.js.map +1 -1
  9. package/dist/cjs/naylence/fame/security/auth/policy/index.js +1 -1
  10. package/dist/cjs/naylence/fame/security/auth/policy/index.js.map +1 -1
  11. package/dist/cjs/version.js +2 -2
  12. package/dist/esm/naylence/fame/expr/builtins.js +1 -1
  13. package/dist/esm/naylence/fame/expr/builtins.js.map +1 -1
  14. package/dist/esm/naylence/fame/security/auth/policy/advanced-authorization-policy.js +32 -13
  15. package/dist/esm/naylence/fame/security/auth/policy/advanced-authorization-policy.js.map +1 -1
  16. package/dist/esm/naylence/fame/security/auth/policy/expr-builtins.js +166 -2
  17. package/dist/esm/naylence/fame/security/auth/policy/expr-builtins.js.map +1 -1
  18. package/dist/esm/naylence/fame/security/auth/policy/index.js +1 -1
  19. package/dist/esm/naylence/fame/security/auth/policy/index.js.map +1 -1
  20. package/dist/esm/version.js +2 -2
  21. package/dist/node/index.cjs +203 -18
  22. package/dist/node/index.mjs +201 -18
  23. package/dist/node/node.cjs +203 -18
  24. package/dist/node/node.mjs +201 -18
  25. package/dist/types/naylence/fame/security/auth/policy/advanced-authorization-policy.d.ts.map +1 -1
  26. package/dist/types/naylence/fame/security/auth/policy/expr-builtins.d.ts +71 -1
  27. package/dist/types/naylence/fame/security/auth/policy/expr-builtins.d.ts.map +1 -1
  28. package/dist/types/naylence/fame/security/auth/policy/index.d.ts +1 -1
  29. package/dist/types/naylence/fame/security/auth/policy/index.d.ts.map +1 -1
  30. package/dist/types/version.d.ts +1 -1
  31. package/package.json +1 -1
@@ -6,7 +6,7 @@ import { Certificate, SubjectAlternativeName, NameConstraints, id_ce_subjectAltN
6
6
  import { verify, etc } from '@noble/ed25519';
7
7
  import { sha256 as sha256$1, sha512 } from '@noble/hashes/sha2.js';
8
8
  import { generateFingerprintSync, localDeliveryContext, createFameEnvelope, generateId, formatAddress, FameAddress, SigningMaterial, DeliveryOriginType as DeliveryOriginType$1 } from '@naylence/core';
9
- import { sha256 } from '@noble/hashes/sha256';
9
+ import { sha256 } from '@noble/hashes/sha2';
10
10
  import { chacha20poly1305 } from '@noble/ciphers/chacha.js';
11
11
  import { x25519 } from '@noble/curves/ed25519.js';
12
12
  import { hkdf } from '@noble/hashes/hkdf.js';
@@ -16,12 +16,12 @@ import { sha256 as sha256$2 } from '@noble/hashes/sha256.js';
16
16
  import { X509Certificate } from '@peculiar/x509';
17
17
 
18
18
  // This file is auto-generated during build - do not edit manually
19
- // Generated from package.json version: 0.4.4
19
+ // Generated from package.json version: 0.4.5
20
20
  /**
21
21
  * The package version, injected at build time.
22
22
  * @internal
23
23
  */
24
- const VERSION = '0.4.4';
24
+ const VERSION = '0.4.5';
25
25
 
26
26
  /**
27
27
  * Abstract Syntax Tree (AST) node types for the expression language.
@@ -1946,8 +1946,86 @@ function evaluateAsBoolean(ast, context) {
1946
1946
  * Null handling semantics:
1947
1947
  * - Scope predicate builtins (has_scope, has_any_scope, has_all_scopes)
1948
1948
  * return `false` when passed `null` for required args.
1949
+ * - Security predicate builtins (is_signed, is_encrypted, is_encrypted_at_least)
1950
+ * return `false` when the envelope lacks the required security posture.
1949
1951
  * - Wrong non-null types still raise BuiltinError to surface real bugs.
1950
1952
  */
1953
+ /**
1954
+ * Valid encryption levels for is_encrypted_at_least comparisons.
1955
+ */
1956
+ const VALID_ENCRYPTION_LEVELS = [
1957
+ "plaintext",
1958
+ "channel",
1959
+ "sealed",
1960
+ ];
1961
+ /**
1962
+ * Encryption level ordering for comparison.
1963
+ * Higher number = stronger encryption.
1964
+ */
1965
+ const ENCRYPTION_LEVEL_ORDER = {
1966
+ plaintext: 0,
1967
+ channel: 1,
1968
+ sealed: 2,
1969
+ };
1970
+ /**
1971
+ * Normalizes an encryption algorithm string to an EncryptionLevel.
1972
+ *
1973
+ * Mapping rules:
1974
+ * - null/undefined => "plaintext" (no encryption present)
1975
+ * - alg contains "-channel" => "channel" (e.g., "chacha20-poly1305-channel")
1976
+ * - alg contains "-sealed" => "sealed" (explicit sealed marker)
1977
+ * - alg matches ECDH-ES pattern with AEAD cipher => "sealed" (e.g., "ECDH-ES+A256GCM")
1978
+ * - otherwise => "unknown"
1979
+ *
1980
+ * Currently supported algorithms:
1981
+ * - Channel: "chacha20-poly1305-channel"
1982
+ * - Sealed: "ECDH-ES+A256GCM"
1983
+ *
1984
+ * This helper is centralized to ensure consistent mapping across TS and Python.
1985
+ */
1986
+ function normalizeEncryptionLevelFromAlg(alg) {
1987
+ if (alg === null || alg === undefined) {
1988
+ return "plaintext";
1989
+ }
1990
+ const algLower = alg.toLowerCase();
1991
+ // Check for channel encryption (e.g., "chacha20-poly1305-channel")
1992
+ // Must check before other patterns since channel suffix is explicit
1993
+ if (algLower.includes("-channel")) {
1994
+ return "channel";
1995
+ }
1996
+ // Check for explicit sealed marker
1997
+ if (algLower.includes("-sealed")) {
1998
+ return "sealed";
1999
+ }
2000
+ // ECDH-ES key agreement with AEAD cipher => sealed encryption
2001
+ // Pattern: "ECDH-ES+A256GCM", "ECDH-ES+A128GCM", etc.
2002
+ if (algLower.startsWith("ecdh-es") && algLower.includes("+a")) {
2003
+ return "sealed";
2004
+ }
2005
+ return "unknown";
2006
+ }
2007
+ /**
2008
+ * Creates security bindings from an envelope's sec header.
2009
+ * Exposes only metadata, never raw values like sig.val or enc.val.
2010
+ */
2011
+ function createSecurityBindings(sec) {
2012
+ const sigPresent = sec?.sig !== undefined;
2013
+ const encPresent = sec?.enc !== undefined;
2014
+ return {
2015
+ sig: {
2016
+ present: sigPresent,
2017
+ kid: sec?.sig?.kid ?? null,
2018
+ },
2019
+ enc: {
2020
+ present: encPresent,
2021
+ alg: sec?.enc?.alg ?? null,
2022
+ kid: sec?.enc?.kid ?? null,
2023
+ level: encPresent
2024
+ ? normalizeEncryptionLevelFromAlg(sec?.enc?.alg ?? null)
2025
+ : "plaintext",
2026
+ },
2027
+ };
2028
+ }
1951
2029
  /**
1952
2030
  * Checks if a value is null.
1953
2031
  */
@@ -1956,9 +2034,21 @@ function isNull(value) {
1956
2034
  }
1957
2035
  /**
1958
2036
  * Creates a function registry with auth helpers installed.
2037
+ *
2038
+ * This registry extends the base builtins with:
2039
+ * - Scope builtins: has_scope, has_any_scope, has_all_scopes
2040
+ * - Security builtins: is_signed, encryption_level, is_encrypted, is_encrypted_at_least
1959
2041
  */
1960
- function createAuthFunctionRegistry(grantedScopes = []) {
1961
- const scopes = grantedScopes ?? [];
2042
+ function createAuthFunctionRegistry(grantedScopesOrOptions = []) {
2043
+ // Handle both old signature (array) and new signature (options object)
2044
+ const options = Array.isArray(grantedScopesOrOptions)
2045
+ ? { grantedScopes: grantedScopesOrOptions }
2046
+ : grantedScopesOrOptions;
2047
+ const scopes = options.grantedScopes ?? [];
2048
+ const secBindings = options.securityBindings ?? {
2049
+ sig: { present: false},
2050
+ enc: { level: "plaintext" },
2051
+ };
1962
2052
  /**
1963
2053
  * Checks if any granted scope matches a pattern (using glob syntax).
1964
2054
  */
@@ -2014,11 +2104,85 @@ function createAuthFunctionRegistry(grantedScopes = []) {
2014
2104
  }
2015
2105
  return values.every((scope) => matchesScope(scope));
2016
2106
  };
2107
+ // ============================================================
2108
+ // Security posture builtins
2109
+ // ============================================================
2110
+ /**
2111
+ * is_signed() -> bool
2112
+ *
2113
+ * Returns true if the envelope has a signature present.
2114
+ * No arguments required.
2115
+ */
2116
+ const is_signed = (args) => {
2117
+ assertArgCount(args, 0, "is_signed");
2118
+ return secBindings.sig.present;
2119
+ };
2120
+ /**
2121
+ * encryption_level() -> string
2122
+ *
2123
+ * Returns the normalized encryption level: "plaintext" | "channel" | "sealed" | "unknown"
2124
+ * No arguments required.
2125
+ */
2126
+ const encryption_level = (args) => {
2127
+ assertArgCount(args, 0, "encryption_level");
2128
+ return secBindings.enc.level;
2129
+ };
2130
+ /**
2131
+ * is_encrypted() -> bool
2132
+ *
2133
+ * Returns true if the encryption level is not "plaintext".
2134
+ * This means the envelope has some form of encryption (channel, sealed, or unknown).
2135
+ * No arguments required.
2136
+ */
2137
+ const is_encrypted = (args) => {
2138
+ assertArgCount(args, 0, "is_encrypted");
2139
+ return secBindings.enc.level !== "plaintext";
2140
+ };
2141
+ /**
2142
+ * is_encrypted_at_least(level: string) -> bool
2143
+ *
2144
+ * Returns true if the envelope's encryption level meets or exceeds the required level.
2145
+ *
2146
+ * Level ordering: plaintext < channel < sealed
2147
+ *
2148
+ * Special handling:
2149
+ * - "unknown" encryption level does NOT satisfy "channel" or "sealed" (conservative)
2150
+ * - "plaintext" is always satisfied (any envelope meets at least plaintext)
2151
+ * - null argument => false (predicate-style)
2152
+ * - invalid level string => BuiltinError
2153
+ */
2154
+ const is_encrypted_at_least = (args) => {
2155
+ assertArgCount(args, 1, "is_encrypted_at_least");
2156
+ const requiredLevel = getArg(args, 0, "is_encrypted_at_least");
2157
+ // Null-tolerant: return false if level is null
2158
+ if (!assertStringOrNull(requiredLevel, "level", "is_encrypted_at_least")) {
2159
+ return false;
2160
+ }
2161
+ // Validate required level
2162
+ if (!VALID_ENCRYPTION_LEVELS.includes(requiredLevel)) {
2163
+ throw new BuiltinError("is_encrypted_at_least", `level must be one of: ${VALID_ENCRYPTION_LEVELS.join(", ")}; got "${requiredLevel}"`);
2164
+ }
2165
+ const currentLevel = secBindings.enc.level;
2166
+ const requiredOrder = ENCRYPTION_LEVEL_ORDER[requiredLevel] ?? 0;
2167
+ const currentOrder = ENCRYPTION_LEVEL_ORDER[currentLevel];
2168
+ // If current level is "unknown", it only satisfies "plaintext"
2169
+ if (currentOrder === undefined) {
2170
+ // "unknown" is treated as NOT meeting channel/sealed requirements
2171
+ return requiredOrder === 0; // Only plaintext is satisfied by unknown
2172
+ }
2173
+ return currentOrder >= requiredOrder;
2174
+ };
2017
2175
  return new Map([
2018
2176
  ...BUILTIN_FUNCTIONS,
2177
+ // Scope builtins
2019
2178
  ["has_scope", has_scope],
2020
2179
  ["has_any_scope", has_any_scope],
2021
2180
  ["has_all_scopes", has_all_scopes],
2181
+ // Security posture builtins
2182
+ ["is_signed", is_signed],
2183
+ ["encryption_level", encryption_level],
2184
+ ["is_encrypted", is_encrypted],
2185
+ ["is_encrypted_at_least", is_encrypted_at_least],
2022
2186
  ]);
2023
2187
  }
2024
2188
  /**
@@ -2161,19 +2325,33 @@ function extractClaims(context) {
2161
2325
  }
2162
2326
  /**
2163
2327
  * Creates a safe envelope subset for expression bindings.
2328
+ *
2329
+ * Exposes:
2330
+ * - id, sid, traceId, corrId, flowId, to
2331
+ * - frame: { type }
2332
+ * - sec: { sig: { present, kid }, enc: { present, alg, kid, level } }
2333
+ *
2334
+ * IMPORTANT: Does NOT expose raw security values (sig.val, enc.val).
2164
2335
  */
2165
2336
  function createEnvelopeBindings(envelope) {
2166
2337
  const frame = envelope.frame;
2167
2338
  const envelopeRecord = envelope;
2339
+ const sec = envelopeRecord.sec;
2340
+ const securityBindings = createSecurityBindings(sec);
2168
2341
  return {
2169
- id: envelope.id ?? null,
2170
- traceId: envelopeRecord.traceId ?? null,
2171
- corrId: envelopeRecord.corrId ?? null,
2172
- flowId: envelopeRecord.flowId ?? null,
2173
- to: extractAddress(envelope) ?? null,
2174
- frame: frame
2175
- ? { type: frame.type ?? null }
2176
- : { type: null },
2342
+ bindings: {
2343
+ id: envelope.id ?? null,
2344
+ sid: envelopeRecord.sid ?? null,
2345
+ traceId: envelopeRecord.traceId ?? null,
2346
+ corrId: envelopeRecord.corrId ?? null,
2347
+ flowId: envelopeRecord.flowId ?? null,
2348
+ to: extractAddress(envelope) ?? null,
2349
+ frame: frame
2350
+ ? { type: frame.type ?? null }
2351
+ : { type: null },
2352
+ sec: securityBindings,
2353
+ },
2354
+ securityBindings,
2177
2355
  };
2178
2356
  }
2179
2357
  /**
@@ -2327,11 +2505,12 @@ class AdvancedAuthorizationPolicy {
2327
2505
  continue;
2328
2506
  }
2329
2507
  if (rule.whenAst) {
2330
- // Lazy initialization of expression bindings
2508
+ // Lazy initialization of expression bindings and security context
2331
2509
  if (!expressionBindings) {
2510
+ const envelopeResult = createEnvelopeBindings(envelope);
2332
2511
  expressionBindings = {
2333
2512
  claims: extractClaims(context),
2334
- envelope: createEnvelopeBindings(envelope),
2513
+ envelope: envelopeResult.bindings,
2335
2514
  delivery: createDeliveryBindings(context, resolvedAction),
2336
2515
  node: createNodeBindings(node),
2337
2516
  time: {
@@ -2339,9 +2518,13 @@ class AdvancedAuthorizationPolicy {
2339
2518
  now_iso: new Date().toISOString(),
2340
2519
  },
2341
2520
  };
2521
+ // Create function registry with security bindings for security builtins
2522
+ functionRegistry = createAuthFunctionRegistry({
2523
+ grantedScopes,
2524
+ securityBindings: envelopeResult.securityBindings,
2525
+ });
2342
2526
  }
2343
- const functions = functionRegistry ?? createAuthFunctionRegistry(grantedScopes);
2344
- functionRegistry = functions;
2527
+ const functions = functionRegistry;
2345
2528
  const evalContext = {
2346
2529
  bindings: expressionBindings,
2347
2530
  limits: this.expressionLimits,
@@ -12662,4 +12845,4 @@ var plugin = /*#__PURE__*/Object.freeze({
12662
12845
  registerAdvancedSecurityPluginFactories: registerAdvancedSecurityPluginFactories
12663
12846
  });
12664
12847
 
12665
- export { FACTORY_META$e as ADVANCED_AUTHORIZATION_POLICY_FACTORY_META, FACTORY_META$9 as ADVANCED_EDDSA_ENVELOPE_SIGNER_FACTORY_META, FACTORY_META$8 as ADVANCED_EDDSA_ENVELOPE_VERIFIER_FACTORY_META, FACTORY_META$4 as ADVANCED_WELCOME_FACTORY_META, AFTHelper, AFTLoadBalancerStickinessManager, AFTLoadBalancerStickinessManagerFactory, AFTReplicaStickinessManager, AFTReplicaStickinessManagerFactory, FACTORY_META$6 as AFT_LOAD_BALANCER_FACTORY_META, FACTORY_META$5 as AFT_REPLICA_FACTORY_META, AdvancedAuthorizationPolicy, AdvancedAuthorizationPolicyFactory, AdvancedEdDSAEnvelopeSignerFactory, AdvancedEdDSAEnvelopeVerifierFactory, AdvancedWelcomeService, AdvancedWelcomeServiceFactory, BUILTIN_FUNCTIONS, BuiltinError, CAServiceClient, CompositeEncryptionManager, CompositeEncryptionManagerFactory, DEFAULT_EXPRESSION_LIMITS, FACTORY_META$b as DEFAULT_SECURE_CHANNEL_MANAGER_FACTORY_META, DEFAULT_STICKINESS_SECURITY_LEVEL, DefaultSecureChannelManager, DefaultSecureChannelManagerFactory, ENV_VAR_FAME_CA_SERVICE_URL, EdDSAEnvelopeVerifier, EvaluationError, Evaluator, ExpressionError, GRANT_PURPOSE_CA_SIGN, LimitExceededError, NoAFTSigner, ParseError, Parser, SidOnlyAFTVerifier, SignedAFTSigner, SignedOptionalAFTVerifier, StickinessMode, StrictAFTVerifier, Tokenizer, TokenizerError, TypeError, UnsignedAFTSigner, VERSION, X5CKeyManager, X5CKeyManagerFactory, FACTORY_META$7 as X5C_KEY_MANAGER_FACTORY_META, __advancedSecurityPluginLoader, astToString, base64UrlDecode, base64UrlEncode, calculateAstDepth, callBuiltin, index as channelEncryption, checkArrayLength, checkAstDepth, checkAstNodeCount, checkExpressionLength, checkFunctionArgCount, checkGlobPatternLength, checkRegexPatternLength, countAstNodes, createAftHelper, createAftPayload, createAftReplicaStickinessManager, createAftSigner, createAftVerifier, createAuthFunctionRegistry, createEd25519Csr, evaluate, evaluateAsBoolean, extractCertificateInfo, formatCertificateInfo, getTypeName, isBuiltinFunction, normalizeJsValue, normalizeStickinessMode, parse, publicKeyFromX5c, registerAdvancedSecurityFactories, index$1 as sealedEncryption, serializeAftClaims, serializeAftHeader, tokenize, utf8Decode, validateJwkX5cCertificate };
12848
+ export { FACTORY_META$e as ADVANCED_AUTHORIZATION_POLICY_FACTORY_META, FACTORY_META$9 as ADVANCED_EDDSA_ENVELOPE_SIGNER_FACTORY_META, FACTORY_META$8 as ADVANCED_EDDSA_ENVELOPE_VERIFIER_FACTORY_META, FACTORY_META$4 as ADVANCED_WELCOME_FACTORY_META, AFTHelper, AFTLoadBalancerStickinessManager, AFTLoadBalancerStickinessManagerFactory, AFTReplicaStickinessManager, AFTReplicaStickinessManagerFactory, FACTORY_META$6 as AFT_LOAD_BALANCER_FACTORY_META, FACTORY_META$5 as AFT_REPLICA_FACTORY_META, AdvancedAuthorizationPolicy, AdvancedAuthorizationPolicyFactory, AdvancedEdDSAEnvelopeSignerFactory, AdvancedEdDSAEnvelopeVerifierFactory, AdvancedWelcomeService, AdvancedWelcomeServiceFactory, BUILTIN_FUNCTIONS, BuiltinError, CAServiceClient, CompositeEncryptionManager, CompositeEncryptionManagerFactory, DEFAULT_EXPRESSION_LIMITS, FACTORY_META$b as DEFAULT_SECURE_CHANNEL_MANAGER_FACTORY_META, DEFAULT_STICKINESS_SECURITY_LEVEL, DefaultSecureChannelManager, DefaultSecureChannelManagerFactory, ENV_VAR_FAME_CA_SERVICE_URL, EdDSAEnvelopeVerifier, EvaluationError, Evaluator, ExpressionError, GRANT_PURPOSE_CA_SIGN, LimitExceededError, NoAFTSigner, ParseError, Parser, SidOnlyAFTVerifier, SignedAFTSigner, SignedOptionalAFTVerifier, StickinessMode, StrictAFTVerifier, Tokenizer, TokenizerError, TypeError, UnsignedAFTSigner, VERSION, X5CKeyManager, X5CKeyManagerFactory, FACTORY_META$7 as X5C_KEY_MANAGER_FACTORY_META, __advancedSecurityPluginLoader, astToString, base64UrlDecode, base64UrlEncode, calculateAstDepth, callBuiltin, index as channelEncryption, checkArrayLength, checkAstDepth, checkAstNodeCount, checkExpressionLength, checkFunctionArgCount, checkGlobPatternLength, checkRegexPatternLength, countAstNodes, createAftHelper, createAftPayload, createAftReplicaStickinessManager, createAftSigner, createAftVerifier, createAuthFunctionRegistry, createEd25519Csr, createSecurityBindings, evaluate, evaluateAsBoolean, extractCertificateInfo, formatCertificateInfo, getTypeName, isBuiltinFunction, normalizeEncryptionLevelFromAlg, normalizeJsValue, normalizeStickinessMode, parse, publicKeyFromX5c, registerAdvancedSecurityFactories, index$1 as sealedEncryption, serializeAftClaims, serializeAftHeader, tokenize, utf8Decode, validateJwkX5cCertificate };
@@ -2,7 +2,7 @@
2
2
 
3
3
  var factory = require('@naylence/factory');
4
4
  var runtime = require('@naylence/runtime');
5
- var sha256 = require('@noble/hashes/sha256');
5
+ var sha2 = require('@noble/hashes/sha2');
6
6
  var core = require('@naylence/core');
7
7
  var asn1Schema = require('@peculiar/asn1-schema');
8
8
  var asn1X509 = require('@peculiar/asn1-x509');
@@ -575,12 +575,12 @@ async function registerAdvancedSecurityFactories(registrar = factory.Registry, o
575
575
  }
576
576
 
577
577
  // This file is auto-generated during build - do not edit manually
578
- // Generated from package.json version: 0.4.4
578
+ // Generated from package.json version: 0.4.5
579
579
  /**
580
580
  * The package version, injected at build time.
581
581
  * @internal
582
582
  */
583
- const VERSION = '0.4.4';
583
+ const VERSION = '0.4.5';
584
584
 
585
585
  async function registerAdvancedSecurityPluginFactories(registrar = factory.Registry) {
586
586
  await registerAdvancedSecurityFactories(registrar, { includeExtras: true });
@@ -1985,7 +1985,7 @@ const secure_hash = (args) => {
1985
1985
  }
1986
1986
  // Use generateFingerprintSync from @naylence/core
1987
1987
  // This provides SHA-256 hashing, base62 encoding, and profanity filtering
1988
- return core.generateFingerprintSync(input_str, length, sha256.sha256);
1988
+ return core.generateFingerprintSync(input_str, length, sha2.sha256);
1989
1989
  };
1990
1990
  // ============================================================
1991
1991
  // Pattern Helpers (BSL-only)
@@ -2559,8 +2559,86 @@ function evaluateAsBoolean(ast, context) {
2559
2559
  * Null handling semantics:
2560
2560
  * - Scope predicate builtins (has_scope, has_any_scope, has_all_scopes)
2561
2561
  * return `false` when passed `null` for required args.
2562
+ * - Security predicate builtins (is_signed, is_encrypted, is_encrypted_at_least)
2563
+ * return `false` when the envelope lacks the required security posture.
2562
2564
  * - Wrong non-null types still raise BuiltinError to surface real bugs.
2563
2565
  */
2566
+ /**
2567
+ * Valid encryption levels for is_encrypted_at_least comparisons.
2568
+ */
2569
+ const VALID_ENCRYPTION_LEVELS = [
2570
+ "plaintext",
2571
+ "channel",
2572
+ "sealed",
2573
+ ];
2574
+ /**
2575
+ * Encryption level ordering for comparison.
2576
+ * Higher number = stronger encryption.
2577
+ */
2578
+ const ENCRYPTION_LEVEL_ORDER = {
2579
+ plaintext: 0,
2580
+ channel: 1,
2581
+ sealed: 2,
2582
+ };
2583
+ /**
2584
+ * Normalizes an encryption algorithm string to an EncryptionLevel.
2585
+ *
2586
+ * Mapping rules:
2587
+ * - null/undefined => "plaintext" (no encryption present)
2588
+ * - alg contains "-channel" => "channel" (e.g., "chacha20-poly1305-channel")
2589
+ * - alg contains "-sealed" => "sealed" (explicit sealed marker)
2590
+ * - alg matches ECDH-ES pattern with AEAD cipher => "sealed" (e.g., "ECDH-ES+A256GCM")
2591
+ * - otherwise => "unknown"
2592
+ *
2593
+ * Currently supported algorithms:
2594
+ * - Channel: "chacha20-poly1305-channel"
2595
+ * - Sealed: "ECDH-ES+A256GCM"
2596
+ *
2597
+ * This helper is centralized to ensure consistent mapping across TS and Python.
2598
+ */
2599
+ function normalizeEncryptionLevelFromAlg(alg) {
2600
+ if (alg === null || alg === undefined) {
2601
+ return "plaintext";
2602
+ }
2603
+ const algLower = alg.toLowerCase();
2604
+ // Check for channel encryption (e.g., "chacha20-poly1305-channel")
2605
+ // Must check before other patterns since channel suffix is explicit
2606
+ if (algLower.includes("-channel")) {
2607
+ return "channel";
2608
+ }
2609
+ // Check for explicit sealed marker
2610
+ if (algLower.includes("-sealed")) {
2611
+ return "sealed";
2612
+ }
2613
+ // ECDH-ES key agreement with AEAD cipher => sealed encryption
2614
+ // Pattern: "ECDH-ES+A256GCM", "ECDH-ES+A128GCM", etc.
2615
+ if (algLower.startsWith("ecdh-es") && algLower.includes("+a")) {
2616
+ return "sealed";
2617
+ }
2618
+ return "unknown";
2619
+ }
2620
+ /**
2621
+ * Creates security bindings from an envelope's sec header.
2622
+ * Exposes only metadata, never raw values like sig.val or enc.val.
2623
+ */
2624
+ function createSecurityBindings(sec) {
2625
+ const sigPresent = sec?.sig !== undefined;
2626
+ const encPresent = sec?.enc !== undefined;
2627
+ return {
2628
+ sig: {
2629
+ present: sigPresent,
2630
+ kid: sec?.sig?.kid ?? null,
2631
+ },
2632
+ enc: {
2633
+ present: encPresent,
2634
+ alg: sec?.enc?.alg ?? null,
2635
+ kid: sec?.enc?.kid ?? null,
2636
+ level: encPresent
2637
+ ? normalizeEncryptionLevelFromAlg(sec?.enc?.alg ?? null)
2638
+ : "plaintext",
2639
+ },
2640
+ };
2641
+ }
2564
2642
  /**
2565
2643
  * Checks if a value is null.
2566
2644
  */
@@ -2569,9 +2647,21 @@ function isNull(value) {
2569
2647
  }
2570
2648
  /**
2571
2649
  * Creates a function registry with auth helpers installed.
2650
+ *
2651
+ * This registry extends the base builtins with:
2652
+ * - Scope builtins: has_scope, has_any_scope, has_all_scopes
2653
+ * - Security builtins: is_signed, encryption_level, is_encrypted, is_encrypted_at_least
2572
2654
  */
2573
- function createAuthFunctionRegistry(grantedScopes = []) {
2574
- const scopes = grantedScopes ?? [];
2655
+ function createAuthFunctionRegistry(grantedScopesOrOptions = []) {
2656
+ // Handle both old signature (array) and new signature (options object)
2657
+ const options = Array.isArray(grantedScopesOrOptions)
2658
+ ? { grantedScopes: grantedScopesOrOptions }
2659
+ : grantedScopesOrOptions;
2660
+ const scopes = options.grantedScopes ?? [];
2661
+ const secBindings = options.securityBindings ?? {
2662
+ sig: { present: false, kid: null },
2663
+ enc: { present: false, alg: null, kid: null, level: "plaintext" },
2664
+ };
2575
2665
  /**
2576
2666
  * Checks if any granted scope matches a pattern (using glob syntax).
2577
2667
  */
@@ -2627,11 +2717,85 @@ function createAuthFunctionRegistry(grantedScopes = []) {
2627
2717
  }
2628
2718
  return values.every((scope) => matchesScope(scope));
2629
2719
  };
2720
+ // ============================================================
2721
+ // Security posture builtins
2722
+ // ============================================================
2723
+ /**
2724
+ * is_signed() -> bool
2725
+ *
2726
+ * Returns true if the envelope has a signature present.
2727
+ * No arguments required.
2728
+ */
2729
+ const is_signed = (args) => {
2730
+ assertArgCount(args, 0, "is_signed");
2731
+ return secBindings.sig.present;
2732
+ };
2733
+ /**
2734
+ * encryption_level() -> string
2735
+ *
2736
+ * Returns the normalized encryption level: "plaintext" | "channel" | "sealed" | "unknown"
2737
+ * No arguments required.
2738
+ */
2739
+ const encryption_level = (args) => {
2740
+ assertArgCount(args, 0, "encryption_level");
2741
+ return secBindings.enc.level;
2742
+ };
2743
+ /**
2744
+ * is_encrypted() -> bool
2745
+ *
2746
+ * Returns true if the encryption level is not "plaintext".
2747
+ * This means the envelope has some form of encryption (channel, sealed, or unknown).
2748
+ * No arguments required.
2749
+ */
2750
+ const is_encrypted = (args) => {
2751
+ assertArgCount(args, 0, "is_encrypted");
2752
+ return secBindings.enc.level !== "plaintext";
2753
+ };
2754
+ /**
2755
+ * is_encrypted_at_least(level: string) -> bool
2756
+ *
2757
+ * Returns true if the envelope's encryption level meets or exceeds the required level.
2758
+ *
2759
+ * Level ordering: plaintext < channel < sealed
2760
+ *
2761
+ * Special handling:
2762
+ * - "unknown" encryption level does NOT satisfy "channel" or "sealed" (conservative)
2763
+ * - "plaintext" is always satisfied (any envelope meets at least plaintext)
2764
+ * - null argument => false (predicate-style)
2765
+ * - invalid level string => BuiltinError
2766
+ */
2767
+ const is_encrypted_at_least = (args) => {
2768
+ assertArgCount(args, 1, "is_encrypted_at_least");
2769
+ const requiredLevel = getArg(args, 0, "is_encrypted_at_least");
2770
+ // Null-tolerant: return false if level is null
2771
+ if (!assertStringOrNull(requiredLevel, "level", "is_encrypted_at_least")) {
2772
+ return false;
2773
+ }
2774
+ // Validate required level
2775
+ if (!VALID_ENCRYPTION_LEVELS.includes(requiredLevel)) {
2776
+ throw new BuiltinError("is_encrypted_at_least", `level must be one of: ${VALID_ENCRYPTION_LEVELS.join(", ")}; got "${requiredLevel}"`);
2777
+ }
2778
+ const currentLevel = secBindings.enc.level;
2779
+ const requiredOrder = ENCRYPTION_LEVEL_ORDER[requiredLevel] ?? 0;
2780
+ const currentOrder = ENCRYPTION_LEVEL_ORDER[currentLevel];
2781
+ // If current level is "unknown", it only satisfies "plaintext"
2782
+ if (currentOrder === undefined) {
2783
+ // "unknown" is treated as NOT meeting channel/sealed requirements
2784
+ return requiredOrder === 0; // Only plaintext is satisfied by unknown
2785
+ }
2786
+ return currentOrder >= requiredOrder;
2787
+ };
2630
2788
  return new Map([
2631
2789
  ...BUILTIN_FUNCTIONS,
2790
+ // Scope builtins
2632
2791
  ["has_scope", has_scope],
2633
2792
  ["has_any_scope", has_any_scope],
2634
2793
  ["has_all_scopes", has_all_scopes],
2794
+ // Security posture builtins
2795
+ ["is_signed", is_signed],
2796
+ ["encryption_level", encryption_level],
2797
+ ["is_encrypted", is_encrypted],
2798
+ ["is_encrypted_at_least", is_encrypted_at_least],
2635
2799
  ]);
2636
2800
  }
2637
2801
  /**
@@ -2774,19 +2938,33 @@ function extractClaims(context) {
2774
2938
  }
2775
2939
  /**
2776
2940
  * Creates a safe envelope subset for expression bindings.
2941
+ *
2942
+ * Exposes:
2943
+ * - id, sid, traceId, corrId, flowId, to
2944
+ * - frame: { type }
2945
+ * - sec: { sig: { present, kid }, enc: { present, alg, kid, level } }
2946
+ *
2947
+ * IMPORTANT: Does NOT expose raw security values (sig.val, enc.val).
2777
2948
  */
2778
2949
  function createEnvelopeBindings(envelope) {
2779
2950
  const frame = envelope.frame;
2780
2951
  const envelopeRecord = envelope;
2952
+ const sec = envelopeRecord.sec;
2953
+ const securityBindings = createSecurityBindings(sec);
2781
2954
  return {
2782
- id: envelope.id ?? null,
2783
- traceId: envelopeRecord.traceId ?? null,
2784
- corrId: envelopeRecord.corrId ?? null,
2785
- flowId: envelopeRecord.flowId ?? null,
2786
- to: extractAddress(envelope) ?? null,
2787
- frame: frame
2788
- ? { type: frame.type ?? null }
2789
- : { type: null },
2955
+ bindings: {
2956
+ id: envelope.id ?? null,
2957
+ sid: envelopeRecord.sid ?? null,
2958
+ traceId: envelopeRecord.traceId ?? null,
2959
+ corrId: envelopeRecord.corrId ?? null,
2960
+ flowId: envelopeRecord.flowId ?? null,
2961
+ to: extractAddress(envelope) ?? null,
2962
+ frame: frame
2963
+ ? { type: frame.type ?? null }
2964
+ : { type: null },
2965
+ sec: securityBindings,
2966
+ },
2967
+ securityBindings,
2790
2968
  };
2791
2969
  }
2792
2970
  /**
@@ -2940,11 +3118,12 @@ class AdvancedAuthorizationPolicy {
2940
3118
  continue;
2941
3119
  }
2942
3120
  if (rule.whenAst) {
2943
- // Lazy initialization of expression bindings
3121
+ // Lazy initialization of expression bindings and security context
2944
3122
  if (!expressionBindings) {
3123
+ const envelopeResult = createEnvelopeBindings(envelope);
2945
3124
  expressionBindings = {
2946
3125
  claims: extractClaims(context),
2947
- envelope: createEnvelopeBindings(envelope),
3126
+ envelope: envelopeResult.bindings,
2948
3127
  delivery: createDeliveryBindings(context, resolvedAction),
2949
3128
  node: createNodeBindings(node),
2950
3129
  time: {
@@ -2952,9 +3131,13 @@ class AdvancedAuthorizationPolicy {
2952
3131
  now_iso: new Date().toISOString(),
2953
3132
  },
2954
3133
  };
3134
+ // Create function registry with security bindings for security builtins
3135
+ functionRegistry = createAuthFunctionRegistry({
3136
+ grantedScopes,
3137
+ securityBindings: envelopeResult.securityBindings,
3138
+ });
2955
3139
  }
2956
- const functions = functionRegistry ?? createAuthFunctionRegistry(grantedScopes);
2957
- functionRegistry = functions;
3140
+ const functions = functionRegistry;
2958
3141
  const evalContext = {
2959
3142
  bindings: expressionBindings,
2960
3143
  limits: this.expressionLimits,
@@ -13104,6 +13287,7 @@ exports.createAftVerifier = createAftVerifier;
13104
13287
  exports.createAuthFunctionRegistry = createAuthFunctionRegistry;
13105
13288
  exports.createEd25519Csr = createEd25519Csr;
13106
13289
  exports.createEd25519CsrFromPem = createEd25519CsrFromPem;
13290
+ exports.createSecurityBindings = createSecurityBindings;
13107
13291
  exports.createTestCA = createTestCA;
13108
13292
  exports.evaluate = evaluate;
13109
13293
  exports.evaluateAsBoolean = evaluateAsBoolean;
@@ -13116,6 +13300,7 @@ exports.extractSpiffeIdFromCert = extractSpiffeIdFromCert;
13116
13300
  exports.formatCertificateInfo = formatCertificateInfo;
13117
13301
  exports.getTypeName = getTypeName;
13118
13302
  exports.isBuiltinFunction = isBuiltinFunction;
13303
+ exports.normalizeEncryptionLevelFromAlg = normalizeEncryptionLevelFromAlg;
13119
13304
  exports.normalizeJsValue = normalizeJsValue;
13120
13305
  exports.normalizeStickinessMode = normalizeStickinessMode;
13121
13306
  exports.parse = parse;