@naylence/runtime 0.3.21 → 0.4.0

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 (85) hide show
  1. package/dist/browser/index.cjs +3144 -1307
  2. package/dist/browser/index.mjs +3116 -1301
  3. package/dist/cjs/naylence/fame/factory-manifest.js +6 -0
  4. package/dist/cjs/naylence/fame/node/node-event-listener.js +4 -0
  5. package/dist/cjs/naylence/fame/security/auth/default-policy-authorizer-factory.js +147 -0
  6. package/dist/cjs/naylence/fame/security/auth/default-policy-authorizer.js +291 -0
  7. package/dist/cjs/naylence/fame/security/auth/oauth2-authorizer-factory.js +7 -0
  8. package/dist/cjs/naylence/fame/security/auth/oauth2-authorizer.js +19 -4
  9. package/dist/cjs/naylence/fame/security/auth/policy/authorization-policy-definition.js +60 -0
  10. package/dist/cjs/naylence/fame/security/auth/policy/authorization-policy-factory.js +35 -0
  11. package/dist/cjs/naylence/fame/security/auth/policy/authorization-policy-source-factory.js +35 -0
  12. package/dist/cjs/naylence/fame/security/auth/policy/authorization-policy-source.js +2 -0
  13. package/dist/cjs/naylence/fame/security/auth/policy/authorization-policy.js +2 -0
  14. package/dist/cjs/naylence/fame/security/auth/policy/basic-authorization-policy-factory.js +99 -0
  15. package/dist/cjs/naylence/fame/security/auth/policy/basic-authorization-policy.js +449 -0
  16. package/dist/cjs/naylence/fame/security/auth/policy/index.js +40 -0
  17. package/dist/cjs/naylence/fame/security/auth/policy/local-file-authorization-policy-source-factory.js +101 -0
  18. package/dist/cjs/naylence/fame/security/auth/policy/local-file-authorization-policy-source.js +164 -0
  19. package/dist/cjs/naylence/fame/security/auth/policy/pattern-matcher.js +195 -0
  20. package/dist/cjs/naylence/fame/security/auth/policy/scope-matcher.js +169 -0
  21. package/dist/cjs/naylence/fame/security/auth/policy-authorizer.js +2 -0
  22. package/dist/cjs/naylence/fame/security/default-security-manager.js +94 -0
  23. package/dist/cjs/naylence/fame/security/index.js +3 -0
  24. package/dist/cjs/naylence/fame/security/node-security-profile-factory.js +3 -1
  25. package/dist/cjs/naylence/fame/sentinel/router.js +67 -1
  26. package/dist/cjs/naylence/fame/sentinel/sentinel.js +46 -2
  27. package/dist/cjs/naylence/fame/util/register-runtime-factories.js +2 -0
  28. package/dist/cjs/version.js +2 -2
  29. package/dist/esm/naylence/fame/factory-manifest.js +6 -0
  30. package/dist/esm/naylence/fame/node/node-event-listener.js +4 -0
  31. package/dist/esm/naylence/fame/security/auth/default-policy-authorizer-factory.js +110 -0
  32. package/dist/esm/naylence/fame/security/auth/default-policy-authorizer.js +287 -0
  33. package/dist/esm/naylence/fame/security/auth/oauth2-authorizer-factory.js +7 -0
  34. package/dist/esm/naylence/fame/security/auth/oauth2-authorizer.js +19 -4
  35. package/dist/esm/naylence/fame/security/auth/policy/authorization-policy-definition.js +57 -0
  36. package/dist/esm/naylence/fame/security/auth/policy/authorization-policy-factory.js +31 -0
  37. package/dist/esm/naylence/fame/security/auth/policy/authorization-policy-source-factory.js +31 -0
  38. package/dist/esm/naylence/fame/security/auth/policy/authorization-policy-source.js +1 -0
  39. package/dist/esm/naylence/fame/security/auth/policy/authorization-policy.js +1 -0
  40. package/dist/esm/naylence/fame/security/auth/policy/basic-authorization-policy-factory.js +62 -0
  41. package/dist/esm/naylence/fame/security/auth/policy/basic-authorization-policy.js +445 -0
  42. package/dist/esm/naylence/fame/security/auth/policy/index.js +20 -0
  43. package/dist/esm/naylence/fame/security/auth/policy/local-file-authorization-policy-source-factory.js +64 -0
  44. package/dist/esm/naylence/fame/security/auth/policy/local-file-authorization-policy-source.js +127 -0
  45. package/dist/esm/naylence/fame/security/auth/policy/pattern-matcher.js +185 -0
  46. package/dist/esm/naylence/fame/security/auth/policy/scope-matcher.js +162 -0
  47. package/dist/esm/naylence/fame/security/auth/policy-authorizer.js +1 -0
  48. package/dist/esm/naylence/fame/security/default-security-manager.js +94 -0
  49. package/dist/esm/naylence/fame/security/index.js +3 -0
  50. package/dist/esm/naylence/fame/security/node-security-profile-factory.js +2 -0
  51. package/dist/esm/naylence/fame/sentinel/router.js +64 -0
  52. package/dist/esm/naylence/fame/sentinel/sentinel.js +47 -3
  53. package/dist/esm/naylence/fame/util/register-runtime-factories.js +2 -0
  54. package/dist/esm/version.js +2 -2
  55. package/dist/node/index.cjs +3140 -1303
  56. package/dist/node/index.mjs +3116 -1301
  57. package/dist/node/node.cjs +3191 -1338
  58. package/dist/node/node.mjs +3167 -1336
  59. package/dist/types/naylence/fame/factory-manifest.d.ts +1 -1
  60. package/dist/types/naylence/fame/node/node-event-listener.d.ts +31 -0
  61. package/dist/types/naylence/fame/security/auth/authorizer.d.ts +37 -0
  62. package/dist/types/naylence/fame/security/auth/default-policy-authorizer-factory.d.ts +55 -0
  63. package/dist/types/naylence/fame/security/auth/default-policy-authorizer.d.ts +99 -0
  64. package/dist/types/naylence/fame/security/auth/oauth2-authorizer-factory.d.ts +2 -0
  65. package/dist/types/naylence/fame/security/auth/oauth2-authorizer.d.ts +2 -0
  66. package/dist/types/naylence/fame/security/auth/policy/authorization-policy-definition.d.ts +166 -0
  67. package/dist/types/naylence/fame/security/auth/policy/authorization-policy-factory.d.ts +38 -0
  68. package/dist/types/naylence/fame/security/auth/policy/authorization-policy-source-factory.d.ts +38 -0
  69. package/dist/types/naylence/fame/security/auth/policy/authorization-policy-source.d.ts +20 -0
  70. package/dist/types/naylence/fame/security/auth/policy/authorization-policy.d.ts +55 -0
  71. package/dist/types/naylence/fame/security/auth/policy/basic-authorization-policy-factory.d.ts +42 -0
  72. package/dist/types/naylence/fame/security/auth/policy/basic-authorization-policy.d.ts +78 -0
  73. package/dist/types/naylence/fame/security/auth/policy/index.d.ts +19 -0
  74. package/dist/types/naylence/fame/security/auth/policy/local-file-authorization-policy-source-factory.d.ts +51 -0
  75. package/dist/types/naylence/fame/security/auth/policy/local-file-authorization-policy-source.d.ts +67 -0
  76. package/dist/types/naylence/fame/security/auth/policy/pattern-matcher.d.ts +84 -0
  77. package/dist/types/naylence/fame/security/auth/policy/scope-matcher.d.ts +61 -0
  78. package/dist/types/naylence/fame/security/auth/policy-authorizer.d.ts +12 -0
  79. package/dist/types/naylence/fame/security/default-security-manager.d.ts +22 -0
  80. package/dist/types/naylence/fame/security/index.d.ts +2 -0
  81. package/dist/types/naylence/fame/security/node-security-profile-factory.d.ts +1 -0
  82. package/dist/types/naylence/fame/sentinel/router.d.ts +68 -0
  83. package/dist/types/naylence/fame/sentinel/sentinel.d.ts +16 -0
  84. package/dist/types/version.d.ts +1 -1
  85. package/package.json +1 -1
@@ -0,0 +1,185 @@
1
+ /**
2
+ * Pattern matching utilities for authorization policies.
3
+ *
4
+ * Supports:
5
+ * - Glob patterns: `*` (single segment), `**` (any depth), `?` (single char)
6
+ * - Regex patterns: patterns starting with `^` (for advanced/BSL use only)
7
+ *
8
+ * The OSS/basic policy uses glob-only matching via `compileGlobPattern()`.
9
+ * The advanced/BSL policy may use `compilePattern()` which interprets `^` as regex.
10
+ */
11
+ /**
12
+ * Checks if a pattern string is a regex pattern.
13
+ * Regex patterns start with `^`.
14
+ */
15
+ export function isRegexPattern(pattern) {
16
+ return pattern.startsWith('^');
17
+ }
18
+ /**
19
+ * Asserts that a pattern is not a regex pattern.
20
+ * Throws an error if the pattern starts with `^`.
21
+ *
22
+ * Use this in OSS/basic policy to reject regex patterns.
23
+ *
24
+ * @param pattern - The pattern to check
25
+ * @param context - Optional context for the error message (e.g., "address", "scope")
26
+ * @throws Error if the pattern is a regex pattern
27
+ */
28
+ export function assertNotRegexPattern(pattern, context) {
29
+ if (pattern.startsWith('^')) {
30
+ const contextStr = context ? ` in ${context}` : '';
31
+ throw new Error(`Regex patterns are not supported${contextStr} in OSS/basic policy. ` +
32
+ `Pattern "${pattern}" starts with '^'. Use glob patterns instead.`);
33
+ }
34
+ }
35
+ /**
36
+ * Escapes special regex characters in a string.
37
+ */
38
+ function escapeRegex(str) {
39
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
40
+ }
41
+ /**
42
+ * Converts a glob pattern to a regex pattern.
43
+ *
44
+ * Glob syntax:
45
+ * - `*` matches a single segment (no dots)
46
+ * - `**` matches any number of segments (including zero)
47
+ * - Other characters are matched literally
48
+ *
49
+ * @param glob - The glob pattern to convert
50
+ * @returns A regex pattern string (without anchors)
51
+ */
52
+ function globToRegex(glob) {
53
+ const parts = [];
54
+ let i = 0;
55
+ while (i < glob.length) {
56
+ if (glob[i] === '*') {
57
+ if (glob[i + 1] === '*') {
58
+ // `**` matches any characters (including dots)
59
+ parts.push('.*');
60
+ i += 2;
61
+ }
62
+ else {
63
+ // `*` matches any characters except dots (single segment)
64
+ parts.push('[^.]*');
65
+ i += 1;
66
+ }
67
+ }
68
+ else if (glob[i] === '?') {
69
+ // `?` matches a single character (not a dot)
70
+ parts.push('[^.]');
71
+ i += 1;
72
+ }
73
+ else {
74
+ // Escape and add literal character
75
+ parts.push(escapeRegex(glob[i]));
76
+ i += 1;
77
+ }
78
+ }
79
+ return parts.join('');
80
+ }
81
+ /**
82
+ * Compiles a pattern string into an efficient matcher.
83
+ *
84
+ * @param pattern - Glob pattern or regex (starting with `^`)
85
+ * @returns A compiled pattern object
86
+ * @throws Error if the regex pattern is invalid
87
+ */
88
+ export function compilePattern(pattern) {
89
+ if (isRegexPattern(pattern)) {
90
+ // Regex pattern - compile directly
91
+ const regex = new RegExp(pattern);
92
+ return {
93
+ source: pattern,
94
+ isRegex: true,
95
+ match: (value) => regex.test(value),
96
+ };
97
+ }
98
+ // Glob pattern - convert to regex with anchors
99
+ const regexStr = `^${globToRegex(pattern)}$`;
100
+ const regex = new RegExp(regexStr);
101
+ return {
102
+ source: pattern,
103
+ isRegex: false,
104
+ match: (value) => regex.test(value),
105
+ };
106
+ }
107
+ /**
108
+ * Compiles a pattern string as a glob pattern only (no regex interpretation).
109
+ *
110
+ * This is the preferred method for OSS/basic policy evaluation.
111
+ * Patterns starting with `^` are rejected with an error.
112
+ *
113
+ * @param pattern - Glob pattern (regex patterns rejected)
114
+ * @param context - Optional context for error messages
115
+ * @returns A compiled pattern object
116
+ * @throws Error if pattern starts with `^` (regex attempt)
117
+ */
118
+ export function compileGlobPattern(pattern, context) {
119
+ // Reject regex patterns in OSS/basic policy
120
+ assertNotRegexPattern(pattern, context);
121
+ // Convert glob to regex with anchors
122
+ const regexStr = `^${globToRegex(pattern)}$`;
123
+ const regex = new RegExp(regexStr);
124
+ return {
125
+ source: pattern,
126
+ isRegex: false,
127
+ match: (value) => regex.test(value),
128
+ };
129
+ }
130
+ /**
131
+ * Cache for compiled patterns to avoid recompilation.
132
+ */
133
+ const patternCache = new Map();
134
+ /**
135
+ * Cache for glob-only compiled patterns.
136
+ */
137
+ const globPatternCache = new Map();
138
+ /**
139
+ * Gets or compiles a pattern, with caching.
140
+ *
141
+ * @param pattern - Glob pattern or regex
142
+ * @returns A compiled pattern object
143
+ */
144
+ export function getCompiledPattern(pattern) {
145
+ let compiled = patternCache.get(pattern);
146
+ if (!compiled) {
147
+ compiled = compilePattern(pattern);
148
+ patternCache.set(pattern, compiled);
149
+ }
150
+ return compiled;
151
+ }
152
+ /**
153
+ * Gets or compiles a glob-only pattern, with caching.
154
+ *
155
+ * This is the preferred method for OSS/basic policy evaluation.
156
+ * Patterns are always treated as globs, never regex.
157
+ *
158
+ * @param pattern - Glob pattern (never interpreted as regex)
159
+ * @returns A compiled pattern object
160
+ */
161
+ export function getCompiledGlobPattern(pattern) {
162
+ let compiled = globPatternCache.get(pattern);
163
+ if (!compiled) {
164
+ compiled = compileGlobPattern(pattern);
165
+ globPatternCache.set(pattern, compiled);
166
+ }
167
+ return compiled;
168
+ }
169
+ /**
170
+ * Matches a value against a pattern string.
171
+ *
172
+ * @param pattern - Glob pattern or regex (starting with `^`)
173
+ * @param value - The value to match
174
+ * @returns True if the value matches the pattern
175
+ */
176
+ export function matchPattern(pattern, value) {
177
+ return getCompiledPattern(pattern).match(value);
178
+ }
179
+ /**
180
+ * Clears the pattern cache.
181
+ * Useful for testing or when memory is a concern.
182
+ */
183
+ export function clearPatternCache() {
184
+ patternCache.clear();
185
+ }
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Scope matching utilities for authorization policies.
3
+ *
4
+ * Supports:
5
+ * - Simple string patterns (glob only in OSS/basic policy)
6
+ * - Logical operators: any_of, all_of, none_of
7
+ * - Recursive nesting with depth limits
8
+ */
9
+ import { MAX_SCOPE_NESTING_DEPTH } from './authorization-policy-definition.js';
10
+ import { matchPattern, compileGlobPattern, } from './pattern-matcher.js';
11
+ /**
12
+ * Checks if any of the granted scopes match the given pattern.
13
+ */
14
+ function anyScopeMatchesPattern(grantedScopes, pattern) {
15
+ return grantedScopes.some((scope) => matchPattern(pattern, scope));
16
+ }
17
+ /**
18
+ * Normalizes a scope requirement into a typed structure.
19
+ *
20
+ * @param requirement - The scope requirement to normalize
21
+ * @param depth - Current nesting depth (for recursion limit)
22
+ * @returns Normalized scope requirement
23
+ * @throws Error if nesting exceeds maximum depth
24
+ */
25
+ export function normalizeScopeRequirement(requirement, depth = 0) {
26
+ if (depth > MAX_SCOPE_NESTING_DEPTH) {
27
+ throw new Error(`Scope requirement nesting exceeds maximum depth of ${MAX_SCOPE_NESTING_DEPTH}`);
28
+ }
29
+ // Simple string pattern
30
+ if (typeof requirement === 'string') {
31
+ return { type: 'pattern', pattern: requirement };
32
+ }
33
+ // Object with logical operator
34
+ if (typeof requirement !== 'object' || requirement === null) {
35
+ throw new Error(`Invalid scope requirement: ${String(requirement)}`);
36
+ }
37
+ const keys = Object.keys(requirement);
38
+ if (keys.length !== 1) {
39
+ throw new Error(`Scope requirement object must have exactly one key (any_of, all_of, or none_of), got: ${keys.join(', ')}`);
40
+ }
41
+ const key = keys[0];
42
+ const value = requirement[key];
43
+ if (!Array.isArray(value)) {
44
+ throw new Error(`Scope requirement "${key}" must have an array value, got: ${typeof value}`);
45
+ }
46
+ const nested = value.map((item) => normalizeScopeRequirement(item, depth + 1));
47
+ switch (key) {
48
+ case 'any_of':
49
+ return { type: 'any_of', requirements: nested };
50
+ case 'all_of':
51
+ return { type: 'all_of', requirements: nested };
52
+ case 'none_of':
53
+ return { type: 'none_of', requirements: nested };
54
+ default:
55
+ throw new Error(`Unknown scope requirement operator: "${key}". Expected any_of, all_of, or none_of`);
56
+ }
57
+ }
58
+ /**
59
+ * Evaluates a normalized scope requirement against granted scopes.
60
+ *
61
+ * @param requirement - The normalized scope requirement
62
+ * @param grantedScopes - The scopes granted to the principal
63
+ * @returns True if the requirement is satisfied
64
+ */
65
+ export function evaluateNormalizedScopeRequirement(requirement, grantedScopes) {
66
+ switch (requirement.type) {
67
+ case 'pattern':
68
+ return anyScopeMatchesPattern(grantedScopes, requirement.pattern);
69
+ case 'any_of':
70
+ return requirement.requirements.some((req) => evaluateNormalizedScopeRequirement(req, grantedScopes));
71
+ case 'all_of':
72
+ return requirement.requirements.every((req) => evaluateNormalizedScopeRequirement(req, grantedScopes));
73
+ case 'none_of':
74
+ return !requirement.requirements.some((req) => evaluateNormalizedScopeRequirement(req, grantedScopes));
75
+ default:
76
+ // Exhaustive check
77
+ throw new Error(`Unknown scope requirement type: ${requirement.type}`);
78
+ }
79
+ }
80
+ /**
81
+ * Evaluates a scope requirement against granted scopes.
82
+ *
83
+ * This is the main entry point for scope matching.
84
+ *
85
+ * @param requirement - The scope requirement (string or object)
86
+ * @param grantedScopes - The scopes granted to the principal
87
+ * @returns True if the requirement is satisfied
88
+ */
89
+ export function evaluateScopeRequirement(requirement, grantedScopes) {
90
+ const normalized = normalizeScopeRequirement(requirement);
91
+ return evaluateNormalizedScopeRequirement(normalized, grantedScopes);
92
+ }
93
+ /**
94
+ * Pre-compiles a scope requirement for efficient repeated evaluation.
95
+ *
96
+ * @param requirement - The scope requirement to compile
97
+ * @returns A function that evaluates the requirement against granted scopes
98
+ */
99
+ export function compileScopeRequirement(requirement) {
100
+ const normalized = normalizeScopeRequirement(requirement);
101
+ return (grantedScopes) => evaluateNormalizedScopeRequirement(normalized, grantedScopes);
102
+ }
103
+ /**
104
+ * Pre-compiles a scope requirement for OSS/basic policy (glob-only, no regex).
105
+ *
106
+ * This version rejects patterns starting with `^` at compile time.
107
+ *
108
+ * @param requirement - The scope requirement to compile
109
+ * @param ruleId - Rule ID for error messages
110
+ * @returns A compiled scope requirement
111
+ * @throws Error if any pattern starts with `^` (regex attempt)
112
+ */
113
+ export function compileGlobOnlyScopeRequirement(requirement, ruleId) {
114
+ const context = `scope in rule "${ruleId}"`;
115
+ // Compile the requirement, pre-compiling all patterns as globs
116
+ const compiled = compileGlobOnlyNormalized(normalizeScopeRequirement(requirement), context);
117
+ return {
118
+ evaluate: (grantedScopes) => evaluateCompiledScope(compiled, grantedScopes),
119
+ };
120
+ }
121
+ /**
122
+ * Compiles a normalized scope requirement into efficient matchers (glob-only).
123
+ */
124
+ function compileGlobOnlyNormalized(requirement, context) {
125
+ switch (requirement.type) {
126
+ case 'pattern':
127
+ return {
128
+ type: 'pattern',
129
+ matcher: compileGlobPattern(requirement.pattern, context),
130
+ };
131
+ case 'any_of':
132
+ return {
133
+ type: 'any_of',
134
+ requirements: requirement.requirements.map((r) => compileGlobOnlyNormalized(r, context)),
135
+ };
136
+ case 'all_of':
137
+ return {
138
+ type: 'all_of',
139
+ requirements: requirement.requirements.map((r) => compileGlobOnlyNormalized(r, context)),
140
+ };
141
+ case 'none_of':
142
+ return {
143
+ type: 'none_of',
144
+ requirements: requirement.requirements.map((r) => compileGlobOnlyNormalized(r, context)),
145
+ };
146
+ }
147
+ }
148
+ /**
149
+ * Evaluates a compiled scope node against granted scopes.
150
+ */
151
+ function evaluateCompiledScope(node, grantedScopes) {
152
+ switch (node.type) {
153
+ case 'pattern':
154
+ return grantedScopes.some((scope) => node.matcher.match(scope));
155
+ case 'any_of':
156
+ return node.requirements.some((r) => evaluateCompiledScope(r, grantedScopes));
157
+ case 'all_of':
158
+ return node.requirements.every((r) => evaluateCompiledScope(r, grantedScopes));
159
+ case 'none_of':
160
+ return !node.requirements.some((r) => evaluateCompiledScope(r, grantedScopes));
161
+ }
162
+ }
@@ -4,6 +4,7 @@ import { SecurityAction, } from './policy/security-policy.js';
4
4
  import { EnvelopeSecurityHandler } from '../node/envelope-security-handler.js';
5
5
  import { SecureChannelFrameHandler } from '../node/secure-channel-frame-handler.js';
6
6
  import { KeyFrameHandler } from '../sentinel/key-frame-handler.js';
7
+ import { Deny, mapRoutingActionToAuthorizationAction, } from '../sentinel/router.js';
7
8
  import { getLogger } from '../util/logging.js';
8
9
  import { secureDigest } from '../util/util.js';
9
10
  import { canonicalJson } from '../security/signing/eddsa-signer-verifier.js';
@@ -789,6 +790,99 @@ export class DefaultSecurityManager {
789
790
  });
790
791
  return envelope;
791
792
  }
793
+ /**
794
+ * Route authorization hook - invoked after routing policy selects an action.
795
+ *
796
+ * This method provides centralized route authorization by:
797
+ * 1. Mapping the RoutingAction to an authorization action token
798
+ * 2. Calling authorizer.authorizeRoute() if available
799
+ * 3. Returning a Deny action on authorization failure (opaque on wire)
800
+ *
801
+ * @param node - The node performing the routing
802
+ * @param envelope - The envelope being routed
803
+ * @param selected - The RoutingAction selected by routing policy
804
+ * @param state - The current router state
805
+ * @param context - Optional delivery context
806
+ * @returns The action to execute (selected if authorized, Deny if denied)
807
+ */
808
+ async onRoutingActionSelected(node, envelope, selected, _state, context) {
809
+ // If no authorizer or authorizer doesn't implement authorizeRoute, allow
810
+ if (!this._authorizer) {
811
+ return selected;
812
+ }
813
+ if (typeof this._authorizer.authorizeRoute !== 'function') {
814
+ return selected;
815
+ }
816
+ // Map RoutingAction to authorization action token
817
+ const actionToken = mapRoutingActionToAuthorizationAction(selected);
818
+ // Terminal actions (Drop, Deny) don't need authorization
819
+ if (actionToken === null) {
820
+ return selected;
821
+ }
822
+ try {
823
+ const authResult = await this._authorizer.authorizeRoute(node, envelope, actionToken, context ?? undefined);
824
+ // undefined means allow (authorizer has no opinion)
825
+ if (authResult === undefined) {
826
+ return selected;
827
+ }
828
+ // Check authorization result
829
+ if (authResult.authorized) {
830
+ logger.debug('route_authorization_allowed', {
831
+ envp_id: envelope.id,
832
+ action: actionToken,
833
+ frame_type: envelope.frame?.type ?? null,
834
+ matched_rule: authResult.matchedRule ?? null,
835
+ });
836
+ return selected;
837
+ }
838
+ // Authorization denied - return Deny action with opaque NACK
839
+ logger.warning('route_authorization_denied_by_policy', {
840
+ envp_id: envelope.id,
841
+ action: actionToken,
842
+ frame_type: envelope.frame?.type ?? null,
843
+ origin_type: context?.originType ?? null,
844
+ to: envelope.to?.toString() ?? null,
845
+ denial_reason: authResult.denialReason ?? 'policy_denied',
846
+ matched_rule: authResult.matchedRule ?? null,
847
+ });
848
+ // Determine disclosure mode from configuration
849
+ const disclosure = this.getNackDisclosureMode();
850
+ return new Deny({
851
+ internalReason: authResult.denialReason ?? 'unauthorized_route',
852
+ deniedAction: actionToken,
853
+ matchedRule: authResult.matchedRule,
854
+ disclosure,
855
+ context: {
856
+ frame_type: envelope.frame?.type ?? null,
857
+ origin_type: context?.originType ?? null,
858
+ },
859
+ });
860
+ }
861
+ catch (error) {
862
+ logger.error('route_authorization_error', {
863
+ envp_id: envelope.id,
864
+ action: actionToken,
865
+ error: error instanceof Error ? error.message : String(error),
866
+ });
867
+ // On error, deny by default (fail-safe)
868
+ return new Deny({
869
+ internalReason: 'authorization_error',
870
+ deniedAction: actionToken,
871
+ disclosure: 'opaque',
872
+ context: {
873
+ error: error instanceof Error ? error.message : String(error),
874
+ },
875
+ });
876
+ }
877
+ }
878
+ /**
879
+ * Gets the NACK disclosure mode from configuration.
880
+ * Default is 'opaque' to avoid leaking route existence.
881
+ */
882
+ getNackDisclosureMode() {
883
+ // Future: Could be made configurable via _policy or constructor options
884
+ return 'opaque';
885
+ }
792
886
  async onForwardToRoute(node, nextSegment, envelope, context) {
793
887
  logger.debug('on_forward_to_route_start', {
794
888
  envp_id: envelope.id,
@@ -1,7 +1,10 @@
1
1
  export * from './auth/authorizer.js';
2
2
  export * from './auth/auth-identity.js';
3
+ export * from './auth/policy-authorizer.js';
3
4
  export { AUTHORIZER_FACTORY_BASE_TYPE, AuthorizerFactory, } from './auth/authorizer-factory.js';
4
5
  export * from './auth/auth-injection-strategy.js';
6
+ // Authorization policy exports
7
+ export * from './auth/policy/index.js';
5
8
  export { AUTH_INJECTION_STRATEGY_FACTORY_BASE_TYPE, AuthInjectionStrategyFactory, } from './auth/auth-injection-strategy-factory.js';
6
9
  export * from './auth/token-issuer.js';
7
10
  export { TOKEN_ISSUER_FACTORY_BASE_TYPE, TokenIssuerFactory, } from './auth/token-issuer-factory.js';
@@ -11,6 +11,7 @@ export const ENV_VAR_HMAC_SECRET = 'FAME_HMAC_SECRET';
11
11
  export const ENV_VAR_JWT_REVERSE_AUTH_TRUSTED_ISSUER = 'FAME_JWT_REVERSE_AUTH_TRUSTED_ISSUER';
12
12
  export const ENV_VAR_JWT_REVERSE_AUTH_AUDIENCE = 'FAME_JWT_REVERSE_AUTH_AUDIENCE';
13
13
  export const ENV_VAR_ENFORCE_TOKEN_SUBJECT_NODE_IDENTITY = 'FAME_ENFORCE_TOKEN_SUBJECT_NODE_IDENTITY';
14
+ export const ENV_VAR_TRUSTED_CLIENT_SCOPE = 'FAME_TRUSTED_CLIENT_SCOPE';
14
15
  export const PROFILE_NAME_STRICT_OVERLAY = 'strict-overlay';
15
16
  export const PROFILE_NAME_OVERLAY = 'overlay';
16
17
  export const PROFILE_NAME_OVERLAY_CALLBACK = 'overlay-callback';
@@ -247,6 +248,7 @@ const GATED_PROFILE = {
247
248
  algorithm: Expressions.env(ENV_VAR_JWT_ALGORITHM, 'RS256'),
248
249
  audience: Expressions.env(ENV_VAR_JWT_AUDIENCE),
249
250
  enforce_token_subject_node_identity: Expressions.env(ENV_VAR_ENFORCE_TOKEN_SUBJECT_NODE_IDENTITY, 'false'),
251
+ trusted_client_scope: Expressions.env(ENV_VAR_TRUSTED_CLIENT_SCOPE, 'node.trusted'),
250
252
  },
251
253
  };
252
254
  const GATED_CALLBACK_PROFILE = {
@@ -90,6 +90,70 @@ export class ForwardPeer {
90
90
  }
91
91
  }
92
92
  }
93
+ /**
94
+ * RoutingAction that denies an envelope due to authorization failure.
95
+ *
96
+ * Emits an opaque NO_ROUTE NACK on wire (by default) to avoid leaking
97
+ * route existence, while logging the true denial reason internally.
98
+ */
99
+ export class Deny {
100
+ constructor(options) {
101
+ this.options = options;
102
+ }
103
+ async execute(envelope, router, state, context) {
104
+ const { internalReason, deniedAction, matchedRule, context: extraContext, disclosure = 'opaque', } = this.options;
105
+ // Log detailed denial internally
106
+ logger.warning('route_authorization_denied', {
107
+ envp_id: envelope.id,
108
+ frame_type: envelope.frame?.type ?? null,
109
+ to: envelope.to?.toString() ?? null,
110
+ internal_reason: internalReason,
111
+ denied_action: deniedAction ?? null,
112
+ matched_rule: matchedRule ?? null,
113
+ origin_type: context?.originType ?? null,
114
+ ...extraContext,
115
+ });
116
+ // Emit opaque NACK on wire (or verbose if configured)
117
+ const wireCode = disclosure === 'verbose' ? 'UNAUTHORIZED_ROUTE' : 'NO_ROUTE';
118
+ await emitDeliveryNack(envelope, router, state, wireCode, context ?? undefined);
119
+ }
120
+ }
121
+ /**
122
+ * Maps a RoutingAction instance to an authorization action token.
123
+ *
124
+ * This function uses instanceof checks to determine the action type,
125
+ * avoiding the need to expose action objects to the authorizer.
126
+ *
127
+ * For unknown/custom RoutingAction types, returns null. Callers should
128
+ * treat null as "deny by default" for security (unknown actions are not
129
+ * authorized).
130
+ *
131
+ * @param action - The RoutingAction instance to map
132
+ * @returns The authorization action token, or null for terminal/unknown actions
133
+ */
134
+ export function mapRoutingActionToAuthorizationAction(action) {
135
+ if (action instanceof ForwardUp) {
136
+ return 'ForwardUpstream';
137
+ }
138
+ if (action instanceof ForwardChild) {
139
+ return 'ForwardDownstream';
140
+ }
141
+ if (action instanceof ForwardPeer) {
142
+ return 'ForwardPeer';
143
+ }
144
+ if (action instanceof DeliverLocal) {
145
+ return 'DeliverLocal';
146
+ }
147
+ // Drop and Deny are terminal actions that don't need authorization
148
+ if (action instanceof Drop || action instanceof Deny) {
149
+ return null;
150
+ }
151
+ // Unknown RoutingAction: return null, caller should deny by default
152
+ logger.warning('unknown_routing_action_for_authorization', {
153
+ action_type: action?.constructor?.name ?? 'unknown',
154
+ });
155
+ return null;
156
+ }
93
157
  export class RouterState {
94
158
  constructor(options) {
95
159
  const normalized = normalizeRouterStateOptions(options);
@@ -20,7 +20,7 @@ import { AsyncEvent } from '../util/async-event.js';
20
20
  import { AsyncLock } from '../util/lock.js';
21
21
  import { createResource } from '../connector/connector-factory.js';
22
22
  import { UpstreamSessionManager } from '../node/upstream-session-manager.js';
23
- import { emitDeliveryNack, RouterState } from './router.js';
23
+ import { Drop, emitDeliveryNack, RouterState, } from './router.js';
24
24
  const logger = getLogger('naylence.fame.sentinel.sentinel');
25
25
  const ALLOWED_BEFORE_ATTACH = new Set(['NodeAttach']);
26
26
  const SYSTEM_INBOX = '__sys__';
@@ -280,8 +280,11 @@ export class Sentinel extends FameNode {
280
280
  }
281
281
  }
282
282
  const state = this.buildRouterState();
283
- const action = await this.routingPolicy.decide(processedEnvelope, state, context);
284
- await action.execute(processedEnvelope, this, state, context);
283
+ let action = await this.routingPolicy.decide(processedEnvelope, state, context);
284
+ // Dispatch onRoutingActionSelected hook to allow authorization/replacement
285
+ // The hook must return the action to execute; null/undefined/throw => Drop
286
+ const actionToExecute = await this.dispatchRoutingActionSelected(processedEnvelope, action, state, context);
287
+ await actionToExecute.execute(processedEnvelope, this, state, context);
285
288
  }
286
289
  async forwardToRoute(nextSegment, envelope, context) {
287
290
  if (this.originMatches(context, nextSegment, DeliveryOriginType.DOWNSTREAM)) {
@@ -827,6 +830,47 @@ export class Sentinel extends FameNode {
827
830
  });
828
831
  }
829
832
  }
833
+ /**
834
+ * Dispatches the onRoutingActionSelected event to all event listeners.
835
+ *
836
+ * This allows listeners (like DefaultSecurityManager) to authorize
837
+ * routing actions and optionally replace them with Deny actions.
838
+ *
839
+ * The hook must return the RoutingAction to execute. If a listener returns
840
+ * null, undefined, or throws, the router will execute a Drop action.
841
+ *
842
+ * @param envelope - The envelope being routed
843
+ * @param selected - The RoutingAction selected by the routing policy
844
+ * @param state - The current router state
845
+ * @param context - Optional delivery context
846
+ * @returns The RoutingAction to execute (never null/undefined)
847
+ */
848
+ async dispatchRoutingActionSelected(envelope, selected, state, context) {
849
+ let currentAction = selected;
850
+ for (const listener of this.eventListeners) {
851
+ if (typeof listener.onRoutingActionSelected !== 'function') {
852
+ continue;
853
+ }
854
+ try {
855
+ const result = await listener.onRoutingActionSelected(this, envelope, currentAction, state, context);
856
+ // null/undefined => treat as denial, execute Drop
857
+ if (result == null) {
858
+ return new Drop();
859
+ }
860
+ // Update current action for next listener in chain
861
+ currentAction = result;
862
+ }
863
+ catch (error) {
864
+ // Hook threw => treat as denial, execute Drop
865
+ logger.warning('routing_action_hook_error', {
866
+ envp_id: envelope.id,
867
+ error: error instanceof Error ? error.message : String(error),
868
+ });
869
+ return new Drop();
870
+ }
871
+ }
872
+ return currentAction;
873
+ }
830
874
  static async aserve(options = {}) {
831
875
  const { logLevel, rootConfig, config, node = null, fabric: providedFabric = null, signals = ['SIGINT', 'SIGTERM'], signal, ...fabricOptions } = options;
832
876
  const resolvedLevel = normalizeServeLogLevel(logLevel) ?? LogLevel.INFO;
@@ -7,6 +7,8 @@ const NODE_ONLY_FACTORY_MODULES = new Set([
7
7
  './connector/websocket-listener-factory.js',
8
8
  './telemetry/open-telemetry-trace-emitter-factory.js',
9
9
  './security/credential/prompt-credential-provider-factory.js',
10
+ './security/auth/default-policy-authorizer-factory.js',
11
+ './security/auth/policy/local-file-authorization-policy-source-factory.js',
10
12
  ]);
11
13
  const BROWSER_ONLY_FACTORY_MODULES = new Set([
12
14
  './security/auth/oauth2-pkce-token-provider-factory.js',
@@ -1,7 +1,7 @@
1
1
  // This file is auto-generated during build - do not edit manually
2
- // Generated from package.json version: 0.3.21
2
+ // Generated from package.json version: 0.4.0
3
3
  /**
4
4
  * The package version, injected at build time.
5
5
  * @internal
6
6
  */
7
- export const VERSION = '0.3.21';
7
+ export const VERSION = '0.4.0';