@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.
- package/dist/browser/index.cjs +3144 -1307
- package/dist/browser/index.mjs +3116 -1301
- package/dist/cjs/naylence/fame/factory-manifest.js +6 -0
- package/dist/cjs/naylence/fame/node/node-event-listener.js +4 -0
- package/dist/cjs/naylence/fame/security/auth/default-policy-authorizer-factory.js +147 -0
- package/dist/cjs/naylence/fame/security/auth/default-policy-authorizer.js +291 -0
- package/dist/cjs/naylence/fame/security/auth/oauth2-authorizer-factory.js +7 -0
- package/dist/cjs/naylence/fame/security/auth/oauth2-authorizer.js +19 -4
- package/dist/cjs/naylence/fame/security/auth/policy/authorization-policy-definition.js +60 -0
- package/dist/cjs/naylence/fame/security/auth/policy/authorization-policy-factory.js +35 -0
- package/dist/cjs/naylence/fame/security/auth/policy/authorization-policy-source-factory.js +35 -0
- package/dist/cjs/naylence/fame/security/auth/policy/authorization-policy-source.js +2 -0
- package/dist/cjs/naylence/fame/security/auth/policy/authorization-policy.js +2 -0
- package/dist/cjs/naylence/fame/security/auth/policy/basic-authorization-policy-factory.js +99 -0
- package/dist/cjs/naylence/fame/security/auth/policy/basic-authorization-policy.js +449 -0
- package/dist/cjs/naylence/fame/security/auth/policy/index.js +40 -0
- package/dist/cjs/naylence/fame/security/auth/policy/local-file-authorization-policy-source-factory.js +101 -0
- package/dist/cjs/naylence/fame/security/auth/policy/local-file-authorization-policy-source.js +164 -0
- package/dist/cjs/naylence/fame/security/auth/policy/pattern-matcher.js +195 -0
- package/dist/cjs/naylence/fame/security/auth/policy/scope-matcher.js +169 -0
- package/dist/cjs/naylence/fame/security/auth/policy-authorizer.js +2 -0
- package/dist/cjs/naylence/fame/security/default-security-manager.js +94 -0
- package/dist/cjs/naylence/fame/security/index.js +3 -0
- package/dist/cjs/naylence/fame/security/node-security-profile-factory.js +3 -1
- package/dist/cjs/naylence/fame/sentinel/router.js +67 -1
- package/dist/cjs/naylence/fame/sentinel/sentinel.js +46 -2
- package/dist/cjs/naylence/fame/util/register-runtime-factories.js +2 -0
- package/dist/cjs/version.js +2 -2
- package/dist/esm/naylence/fame/factory-manifest.js +6 -0
- package/dist/esm/naylence/fame/node/node-event-listener.js +4 -0
- package/dist/esm/naylence/fame/security/auth/default-policy-authorizer-factory.js +110 -0
- package/dist/esm/naylence/fame/security/auth/default-policy-authorizer.js +287 -0
- package/dist/esm/naylence/fame/security/auth/oauth2-authorizer-factory.js +7 -0
- package/dist/esm/naylence/fame/security/auth/oauth2-authorizer.js +19 -4
- package/dist/esm/naylence/fame/security/auth/policy/authorization-policy-definition.js +57 -0
- package/dist/esm/naylence/fame/security/auth/policy/authorization-policy-factory.js +31 -0
- package/dist/esm/naylence/fame/security/auth/policy/authorization-policy-source-factory.js +31 -0
- package/dist/esm/naylence/fame/security/auth/policy/authorization-policy-source.js +1 -0
- package/dist/esm/naylence/fame/security/auth/policy/authorization-policy.js +1 -0
- package/dist/esm/naylence/fame/security/auth/policy/basic-authorization-policy-factory.js +62 -0
- package/dist/esm/naylence/fame/security/auth/policy/basic-authorization-policy.js +445 -0
- package/dist/esm/naylence/fame/security/auth/policy/index.js +20 -0
- package/dist/esm/naylence/fame/security/auth/policy/local-file-authorization-policy-source-factory.js +64 -0
- package/dist/esm/naylence/fame/security/auth/policy/local-file-authorization-policy-source.js +127 -0
- package/dist/esm/naylence/fame/security/auth/policy/pattern-matcher.js +185 -0
- package/dist/esm/naylence/fame/security/auth/policy/scope-matcher.js +162 -0
- package/dist/esm/naylence/fame/security/auth/policy-authorizer.js +1 -0
- package/dist/esm/naylence/fame/security/default-security-manager.js +94 -0
- package/dist/esm/naylence/fame/security/index.js +3 -0
- package/dist/esm/naylence/fame/security/node-security-profile-factory.js +2 -0
- package/dist/esm/naylence/fame/sentinel/router.js +64 -0
- package/dist/esm/naylence/fame/sentinel/sentinel.js +47 -3
- package/dist/esm/naylence/fame/util/register-runtime-factories.js +2 -0
- package/dist/esm/version.js +2 -2
- package/dist/node/index.cjs +3140 -1303
- package/dist/node/index.mjs +3116 -1301
- package/dist/node/node.cjs +3191 -1338
- package/dist/node/node.mjs +3167 -1336
- package/dist/types/naylence/fame/factory-manifest.d.ts +1 -1
- package/dist/types/naylence/fame/node/node-event-listener.d.ts +31 -0
- package/dist/types/naylence/fame/security/auth/authorizer.d.ts +37 -0
- package/dist/types/naylence/fame/security/auth/default-policy-authorizer-factory.d.ts +55 -0
- package/dist/types/naylence/fame/security/auth/default-policy-authorizer.d.ts +99 -0
- package/dist/types/naylence/fame/security/auth/oauth2-authorizer-factory.d.ts +2 -0
- package/dist/types/naylence/fame/security/auth/oauth2-authorizer.d.ts +2 -0
- package/dist/types/naylence/fame/security/auth/policy/authorization-policy-definition.d.ts +166 -0
- package/dist/types/naylence/fame/security/auth/policy/authorization-policy-factory.d.ts +38 -0
- package/dist/types/naylence/fame/security/auth/policy/authorization-policy-source-factory.d.ts +38 -0
- package/dist/types/naylence/fame/security/auth/policy/authorization-policy-source.d.ts +20 -0
- package/dist/types/naylence/fame/security/auth/policy/authorization-policy.d.ts +55 -0
- package/dist/types/naylence/fame/security/auth/policy/basic-authorization-policy-factory.d.ts +42 -0
- package/dist/types/naylence/fame/security/auth/policy/basic-authorization-policy.d.ts +78 -0
- package/dist/types/naylence/fame/security/auth/policy/index.d.ts +19 -0
- package/dist/types/naylence/fame/security/auth/policy/local-file-authorization-policy-source-factory.d.ts +51 -0
- package/dist/types/naylence/fame/security/auth/policy/local-file-authorization-policy-source.d.ts +67 -0
- package/dist/types/naylence/fame/security/auth/policy/pattern-matcher.d.ts +84 -0
- package/dist/types/naylence/fame/security/auth/policy/scope-matcher.d.ts +61 -0
- package/dist/types/naylence/fame/security/auth/policy-authorizer.d.ts +12 -0
- package/dist/types/naylence/fame/security/default-security-manager.d.ts +22 -0
- package/dist/types/naylence/fame/security/index.d.ts +2 -0
- package/dist/types/naylence/fame/security/node-security-profile-factory.d.ts +1 -0
- package/dist/types/naylence/fame/sentinel/router.d.ts +68 -0
- package/dist/types/naylence/fame/sentinel/sentinel.d.ts +16 -0
- package/dist/types/version.d.ts +1 -1
- 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
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -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
|
-
|
|
284
|
-
|
|
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',
|
package/dist/esm/version.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// This file is auto-generated during build - do not edit manually
|
|
2
|
-
// Generated from package.json version: 0.
|
|
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.
|
|
7
|
+
export const VERSION = '0.4.0';
|