@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,445 @@
1
+ /**
2
+ * Basic authorization policy implementation.
3
+ *
4
+ * Evaluates authorization rules defined in YAML/JSON policy files.
5
+ * Uses first-match-wins semantics with glob/regex pattern matching.
6
+ */
7
+ import { getLogger } from '../../../util/logging.js';
8
+ import { KNOWN_POLICY_FIELDS, KNOWN_RULE_FIELDS, VALID_ACTIONS, VALID_EFFECTS, VALID_ORIGIN_TYPES, } from './authorization-policy-definition.js';
9
+ import { compileGlobPattern } from './pattern-matcher.js';
10
+ import { compileGlobOnlyScopeRequirement } from './scope-matcher.js';
11
+ const logger = getLogger('naylence.fame.security.auth.policy.basic_authorization_policy');
12
+ /**
13
+ * Extracts the target address string from the envelope.
14
+ */
15
+ function extractAddress(envelope) {
16
+ const to = envelope.to;
17
+ if (!to) {
18
+ return undefined;
19
+ }
20
+ // FameAddress can be a string or object with toString()
21
+ if (typeof to === 'string') {
22
+ return to;
23
+ }
24
+ if (typeof to === 'object' && 'toString' in to) {
25
+ return to.toString();
26
+ }
27
+ return undefined;
28
+ }
29
+ /**
30
+ * Extracts granted scopes from the authorization context.
31
+ */
32
+ function extractGrantedScopes(context) {
33
+ const authContext = context?.security?.authorization;
34
+ if (!authContext) {
35
+ return [];
36
+ }
37
+ // Check grantedScopes first
38
+ if (Array.isArray(authContext.grantedScopes)) {
39
+ return authContext.grantedScopes;
40
+ }
41
+ // Fall back to claims.scope if available
42
+ const claims = authContext.claims;
43
+ if (claims) {
44
+ const scopeClaim = claims.scope ?? claims.scopes ?? claims.scp;
45
+ if (typeof scopeClaim === 'string') {
46
+ // Space-separated scopes (OAuth2 convention)
47
+ return scopeClaim.split(/\s+/).filter((s) => s.length > 0);
48
+ }
49
+ if (Array.isArray(scopeClaim)) {
50
+ return scopeClaim.filter((s) => typeof s === 'string');
51
+ }
52
+ }
53
+ return [];
54
+ }
55
+ /**
56
+ * Basic authorization policy that evaluates rules from a policy definition.
57
+ *
58
+ * Features:
59
+ * - First-match-wins rule evaluation
60
+ * - Glob and regex pattern matching for addresses
61
+ * - Scope matching with any_of/all_of/none_of operators
62
+ * - Action-based filtering (connect, send, receive)
63
+ */
64
+ export class BasicAuthorizationPolicy {
65
+ constructor(options) {
66
+ const { policyDefinition, warnOnUnknownFields = true } = options;
67
+ // Validate and extract default effect
68
+ this.defaultEffect = this.validateDefaultEffect(policyDefinition.default_effect);
69
+ // Warn about unknown policy fields
70
+ if (warnOnUnknownFields) {
71
+ this.warnUnknownPolicyFields(policyDefinition);
72
+ }
73
+ // Compile rules for efficient evaluation
74
+ this.compiledRules = this.compileRules(policyDefinition.rules, warnOnUnknownFields);
75
+ logger.debug('policy_compiled', {
76
+ defaultEffect: this.defaultEffect,
77
+ ruleCount: this.compiledRules.length,
78
+ });
79
+ }
80
+ /**
81
+ * Evaluates the policy against a request with an explicitly provided action.
82
+ *
83
+ * @param _node - The node handling the request (unused in basic policy)
84
+ * @param envelope - The FAME envelope being authorized
85
+ * @param context - Optional delivery context with authorization info
86
+ * @param action - The authorization action token (required, no inference)
87
+ * @returns Authorization decision indicating allow/deny
88
+ */
89
+ async evaluateRequest(_node, envelope, context, action) {
90
+ // Action must be explicitly provided; default to wildcard if omitted
91
+ // for backward compatibility during transition
92
+ const resolvedAction = action ?? '*';
93
+ const address = extractAddress(envelope);
94
+ const grantedScopes = extractGrantedScopes(context);
95
+ const rawFrameType = envelope.frame
96
+ ?.type;
97
+ const frameTypeNormalized = typeof rawFrameType === 'string' && rawFrameType.trim().length > 0
98
+ ? rawFrameType.trim().toLowerCase()
99
+ : '';
100
+ // Extract and normalize origin type for rule matching
101
+ const rawOriginType = context?.originType;
102
+ const originTypeNormalized = typeof rawOriginType === 'string' && rawOriginType.trim().length > 0
103
+ ? rawOriginType.trim().toLowerCase()
104
+ : undefined;
105
+ const evaluationTrace = [];
106
+ // Evaluate rules in order (first match wins)
107
+ for (const rule of this.compiledRules) {
108
+ const step = {
109
+ ruleId: rule.id,
110
+ result: false,
111
+ };
112
+ // Skip rules with 'when' clause (handled by advanced policy)
113
+ if (rule.hasWhenClause) {
114
+ step.expression = 'when clause (skipped by basic policy)';
115
+ step.result = false;
116
+ evaluationTrace.push(step);
117
+ continue;
118
+ }
119
+ // Check frame type match
120
+ if (rule.frameTypes) {
121
+ if (!frameTypeNormalized) {
122
+ step.expression = 'frame_type: missing';
123
+ step.result = false;
124
+ evaluationTrace.push(step);
125
+ continue;
126
+ }
127
+ if (!rule.frameTypes.has(frameTypeNormalized)) {
128
+ step.expression = `frame_type: ${rawFrameType ?? 'unknown'} not in rule set`;
129
+ step.result = false;
130
+ evaluationTrace.push(step);
131
+ continue;
132
+ }
133
+ }
134
+ // Check origin type match (early gate for efficiency)
135
+ if (rule.originTypes) {
136
+ if (originTypeNormalized === undefined) {
137
+ step.expression = 'origin_type: missing (rule requires origin)';
138
+ step.result = false;
139
+ evaluationTrace.push(step);
140
+ continue;
141
+ }
142
+ if (!rule.originTypes.has(originTypeNormalized)) {
143
+ step.expression = `origin_type: ${rawOriginType ?? 'unknown'} not in [${Array.from(rule.originTypes).join(', ')}]`;
144
+ step.result = false;
145
+ evaluationTrace.push(step);
146
+ continue;
147
+ }
148
+ }
149
+ // Check action match
150
+ if (!rule.actions.has('*') && !rule.actions.has(resolvedAction)) {
151
+ step.expression = `action: ${resolvedAction} not in [${Array.from(rule.actions).join(', ')}]`;
152
+ step.result = false;
153
+ evaluationTrace.push(step);
154
+ continue;
155
+ }
156
+ // Check address match (any pattern in the list matches)
157
+ if (rule.addressPatterns) {
158
+ if (!address) {
159
+ step.expression = `address: pattern requires address, but none provided`;
160
+ step.result = false;
161
+ evaluationTrace.push(step);
162
+ continue;
163
+ }
164
+ const matched = rule.addressPatterns.some((p) => p.match(address));
165
+ if (!matched) {
166
+ const patterns = rule.addressPatterns.map((p) => p.source).join(', ');
167
+ step.expression = `address: none of [${patterns}] matched ${address}`;
168
+ step.result = false;
169
+ evaluationTrace.push(step);
170
+ continue;
171
+ }
172
+ }
173
+ // Check scope match
174
+ if (rule.scopeMatcher) {
175
+ if (!rule.scopeMatcher(grantedScopes)) {
176
+ step.expression = `scope: requirement not satisfied`;
177
+ step.boundValues = { grantedScopes: [...grantedScopes] };
178
+ step.result = false;
179
+ evaluationTrace.push(step);
180
+ continue;
181
+ }
182
+ }
183
+ // Rule matched
184
+ step.result = true;
185
+ step.expression = 'all conditions matched';
186
+ step.boundValues = {
187
+ action: resolvedAction,
188
+ address,
189
+ grantedScopes: [...grantedScopes],
190
+ };
191
+ evaluationTrace.push(step);
192
+ logger.debug('rule_matched', {
193
+ ruleId: rule.id,
194
+ effect: rule.effect,
195
+ action: resolvedAction,
196
+ address,
197
+ });
198
+ return {
199
+ effect: rule.effect,
200
+ reason: rule.description ?? `Matched rule: ${rule.id}`,
201
+ matchedRule: rule.id,
202
+ evaluationTrace,
203
+ };
204
+ }
205
+ // No rule matched, apply default effect
206
+ logger.debug('no_rule_matched', {
207
+ defaultEffect: this.defaultEffect,
208
+ action: resolvedAction,
209
+ address,
210
+ });
211
+ return {
212
+ effect: this.defaultEffect,
213
+ reason: `No rule matched, applying default effect: ${this.defaultEffect}`,
214
+ evaluationTrace,
215
+ };
216
+ }
217
+ validateDefaultEffect(effect) {
218
+ if (effect !== 'allow' && effect !== 'deny') {
219
+ throw new Error(`Invalid default_effect: "${String(effect)}". Must be "allow" or "deny"`);
220
+ }
221
+ return effect;
222
+ }
223
+ warnUnknownPolicyFields(definition) {
224
+ for (const key of Object.keys(definition)) {
225
+ if (!KNOWN_POLICY_FIELDS.has(key)) {
226
+ logger.warning('unknown_policy_field', { field: key });
227
+ }
228
+ }
229
+ }
230
+ compileRules(rules, warnOnUnknown) {
231
+ return rules.map((rule, index) => this.compileRule(rule, index, warnOnUnknown));
232
+ }
233
+ compileRule(rule, index, warnOnUnknown) {
234
+ // Generate ID if not provided
235
+ const id = rule.id ?? `rule_${index}`;
236
+ // Validate effect
237
+ if (!VALID_EFFECTS.includes(rule.effect)) {
238
+ throw new Error(`Invalid effect in rule "${id}": "${String(rule.effect)}". Must be "allow" or "deny"`);
239
+ }
240
+ // Validate and compile action(s)
241
+ const actions = this.compileActions(rule.action, id);
242
+ // Compile address patterns (glob-only, no regex)
243
+ const addressPatterns = this.compileAddress(rule.address, id);
244
+ // Compile frame type gating
245
+ const frameTypes = this.compileFrameTypes(rule.frame_type, id);
246
+ // Compile origin type gating
247
+ const originTypes = this.compileOriginTypes(rule.origin_type, id);
248
+ // Compile scope matcher (glob-only, no regex)
249
+ let scopeMatcher;
250
+ if (rule.scope !== undefined) {
251
+ try {
252
+ const compiled = compileGlobOnlyScopeRequirement(rule.scope, id);
253
+ scopeMatcher = (scopes) => compiled.evaluate(scopes);
254
+ }
255
+ catch (error) {
256
+ throw new Error(`Invalid scope requirement in rule "${id}": ${error instanceof Error ? error.message : String(error)}`);
257
+ }
258
+ }
259
+ // Warn about unknown fields
260
+ if (warnOnUnknown) {
261
+ for (const key of Object.keys(rule)) {
262
+ if (!KNOWN_RULE_FIELDS.has(key)) {
263
+ logger.warning('unknown_rule_field', { ruleId: id, field: key });
264
+ }
265
+ }
266
+ }
267
+ return {
268
+ id,
269
+ description: rule.description,
270
+ effect: rule.effect,
271
+ actions,
272
+ frameTypes,
273
+ originTypes,
274
+ addressPatterns,
275
+ scopeMatcher,
276
+ hasWhenClause: typeof rule.when === 'string' && rule.when.length > 0,
277
+ };
278
+ }
279
+ /**
280
+ * Compiles action field into a Set of valid actions.
281
+ * Supports single RuleAction or array of RuleAction (implicit any-of).
282
+ */
283
+ compileActions(action, ruleId) {
284
+ // Default to wildcard if not specified
285
+ if (action === undefined) {
286
+ return new Set(['*']);
287
+ }
288
+ // Handle single action
289
+ if (typeof action === 'string') {
290
+ if (!VALID_ACTIONS.includes(action)) {
291
+ throw new Error(`Invalid action in rule "${ruleId}": "${action}". Must be one of: ${VALID_ACTIONS.join(', ')}`);
292
+ }
293
+ return new Set([action]);
294
+ }
295
+ // Handle array of actions
296
+ if (!Array.isArray(action)) {
297
+ throw new Error(`Invalid action in rule "${ruleId}": must be a string or array of strings`);
298
+ }
299
+ if (action.length === 0) {
300
+ throw new Error(`Invalid action in rule "${ruleId}": array must not be empty`);
301
+ }
302
+ const actions = new Set();
303
+ for (const a of action) {
304
+ if (typeof a !== 'string') {
305
+ throw new Error(`Invalid action in rule "${ruleId}": all values must be strings`);
306
+ }
307
+ if (!VALID_ACTIONS.includes(a)) {
308
+ throw new Error(`Invalid action in rule "${ruleId}": "${a}". Must be one of: ${VALID_ACTIONS.join(', ')}`);
309
+ }
310
+ actions.add(a);
311
+ }
312
+ return actions;
313
+ }
314
+ /**
315
+ * Compiles address field into an array of glob matchers.
316
+ * Supports single string or array of strings (implicit any-of).
317
+ * Returns undefined if not specified (no address gating).
318
+ *
319
+ * All patterns are treated as globs - `^` prefix is rejected as an error.
320
+ */
321
+ compileAddress(address, ruleId) {
322
+ if (address === undefined) {
323
+ return undefined;
324
+ }
325
+ const context = `address in rule "${ruleId}"`;
326
+ // Handle single address pattern
327
+ if (typeof address === 'string') {
328
+ const trimmed = address.trim();
329
+ if (!trimmed) {
330
+ throw new Error(`Invalid address in rule "${ruleId}": value must not be empty`);
331
+ }
332
+ try {
333
+ return [compileGlobPattern(trimmed, context)];
334
+ }
335
+ catch (error) {
336
+ throw new Error(`Invalid address in rule "${ruleId}": ${error instanceof Error ? error.message : String(error)}`);
337
+ }
338
+ }
339
+ // Handle array of address patterns
340
+ if (!Array.isArray(address)) {
341
+ throw new Error(`Invalid address in rule "${ruleId}": must be a string or array of strings`);
342
+ }
343
+ if (address.length === 0) {
344
+ throw new Error(`Invalid address in rule "${ruleId}": array must not be empty`);
345
+ }
346
+ const patterns = [];
347
+ for (const addr of address) {
348
+ if (typeof addr !== 'string') {
349
+ throw new Error(`Invalid address in rule "${ruleId}": all values must be strings`);
350
+ }
351
+ const trimmed = addr.trim();
352
+ if (!trimmed) {
353
+ throw new Error(`Invalid address in rule "${ruleId}": values must not be empty`);
354
+ }
355
+ try {
356
+ patterns.push(compileGlobPattern(trimmed, context));
357
+ }
358
+ catch (error) {
359
+ throw new Error(`Invalid address in rule "${ruleId}": ${error instanceof Error ? error.message : String(error)}`);
360
+ }
361
+ }
362
+ return patterns;
363
+ }
364
+ /**
365
+ * Compiles frame_type field into a Set of normalized frame types.
366
+ * Supports single string or array of strings (implicit any-of).
367
+ * Returns undefined if not specified (no frame type gating).
368
+ */
369
+ compileFrameTypes(frameType, ruleId) {
370
+ if (frameType === undefined) {
371
+ return undefined;
372
+ }
373
+ // Handle single frame type
374
+ if (typeof frameType === 'string') {
375
+ const normalized = frameType.trim().toLowerCase();
376
+ if (!normalized) {
377
+ throw new Error(`Invalid frame_type in rule "${ruleId}": value must not be empty`);
378
+ }
379
+ return new Set([normalized]);
380
+ }
381
+ // Handle array of frame types
382
+ if (!Array.isArray(frameType)) {
383
+ throw new Error(`Invalid frame_type in rule "${ruleId}": must be a string or array of strings`);
384
+ }
385
+ if (frameType.length === 0) {
386
+ throw new Error(`Invalid frame_type in rule "${ruleId}": array must not be empty`);
387
+ }
388
+ const frameTypes = new Set();
389
+ for (const ft of frameType) {
390
+ if (typeof ft !== 'string') {
391
+ throw new Error(`Invalid frame_type in rule "${ruleId}": all values must be strings`);
392
+ }
393
+ const normalized = ft.trim().toLowerCase();
394
+ if (!normalized) {
395
+ throw new Error(`Invalid frame_type in rule "${ruleId}": values must not be empty`);
396
+ }
397
+ frameTypes.add(normalized);
398
+ }
399
+ return frameTypes;
400
+ }
401
+ /**
402
+ * Compiles origin_type field into a Set of normalized origin types.
403
+ * Supports single string or array of strings (implicit any-of).
404
+ * Returns undefined if not specified (no origin type gating).
405
+ * Valid values: 'downstream', 'upstream', 'peer', 'local' (case-insensitive).
406
+ */
407
+ compileOriginTypes(originType, ruleId) {
408
+ if (originType === undefined) {
409
+ return undefined;
410
+ }
411
+ // Handle single origin type
412
+ if (typeof originType === 'string') {
413
+ const normalized = originType.trim().toLowerCase();
414
+ if (!normalized) {
415
+ throw new Error(`Invalid origin_type in rule "${ruleId}": value must not be empty`);
416
+ }
417
+ if (!VALID_ORIGIN_TYPES.includes(normalized)) {
418
+ throw new Error(`Invalid origin_type in rule "${ruleId}": "${originType}". Must be one of: ${VALID_ORIGIN_TYPES.join(', ')}`);
419
+ }
420
+ return new Set([normalized]);
421
+ }
422
+ // Handle array of origin types
423
+ if (!Array.isArray(originType)) {
424
+ throw new Error(`Invalid origin_type in rule "${ruleId}": must be a string or array of strings`);
425
+ }
426
+ if (originType.length === 0) {
427
+ throw new Error(`Invalid origin_type in rule "${ruleId}": array must not be empty`);
428
+ }
429
+ const originTypes = new Set();
430
+ for (const ot of originType) {
431
+ if (typeof ot !== 'string') {
432
+ throw new Error(`Invalid origin_type in rule "${ruleId}": all values must be strings`);
433
+ }
434
+ const normalized = ot.trim().toLowerCase();
435
+ if (!normalized) {
436
+ throw new Error(`Invalid origin_type in rule "${ruleId}": values must not be empty`);
437
+ }
438
+ if (!VALID_ORIGIN_TYPES.includes(normalized)) {
439
+ throw new Error(`Invalid origin_type in rule "${ruleId}": "${ot}". Must be one of: ${VALID_ORIGIN_TYPES.join(', ')}`);
440
+ }
441
+ originTypes.add(normalized);
442
+ }
443
+ return originTypes;
444
+ }
445
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Authorization policy module exports.
3
+ *
4
+ * This module provides interfaces and factories for pluggable authorization policies.
5
+ */
6
+ // Core interfaces and types
7
+ export * from './authorization-policy.js';
8
+ export * from './authorization-policy-source.js';
9
+ export * from './authorization-policy-definition.js';
10
+ // Pattern and scope matchers
11
+ export { compilePattern, compileGlobPattern, getCompiledGlobPattern, matchPattern, isRegexPattern, assertNotRegexPattern, } from './pattern-matcher.js';
12
+ export { evaluateScopeRequirement, compileScopeRequirement, normalizeScopeRequirement, compileGlobOnlyScopeRequirement, } from './scope-matcher.js';
13
+ // Factory base classes
14
+ export { AUTHORIZATION_POLICY_FACTORY_BASE_TYPE, AuthorizationPolicyFactory, } from './authorization-policy-factory.js';
15
+ export { AUTHORIZATION_POLICY_SOURCE_FACTORY_BASE_TYPE, AuthorizationPolicySourceFactory, } from './authorization-policy-source-factory.js';
16
+ // Basic authorization policy (browser and node)
17
+ export { BasicAuthorizationPolicy } from './basic-authorization-policy.js';
18
+ export { BasicAuthorizationPolicyFactory } from './basic-authorization-policy-factory.js';
19
+ // Note: LocalFileAuthorizationPolicySource and its factory are node-only
20
+ // and are registered via the factory manifest, not exported here directly.
@@ -0,0 +1,64 @@
1
+ import { safeImport } from '../../../util/lazy-import.js';
2
+ import { AUTHORIZATION_POLICY_SOURCE_FACTORY_BASE_TYPE, AuthorizationPolicySourceFactory, } from './authorization-policy-source-factory.js';
3
+ let localFileModulePromise = null;
4
+ async function getLocalFileModule() {
5
+ if (!localFileModulePromise) {
6
+ localFileModulePromise = safeImport(() => import('./local-file-authorization-policy-source.js'), 'local-file-authorization-policy-source');
7
+ }
8
+ return localFileModulePromise;
9
+ }
10
+ function normalizeConfig(config) {
11
+ if (!config) {
12
+ throw new Error('LocalFileAuthorizationPolicySourceFactory requires a configuration with a path');
13
+ }
14
+ const candidate = config;
15
+ const path = candidate.path;
16
+ if (typeof path !== 'string' || path.trim().length === 0) {
17
+ throw new Error('LocalFileAuthorizationPolicySourceConfig requires a non-empty path');
18
+ }
19
+ const format = candidate.format;
20
+ if (format !== undefined && !['yaml', 'json', 'auto'].includes(format)) {
21
+ throw new Error(`Invalid format "${String(format)}". Must be "yaml", "json", or "auto"`);
22
+ }
23
+ const policyFactory = candidate.policyFactory;
24
+ return {
25
+ path: path.trim(),
26
+ format: format ?? 'auto',
27
+ policyFactory,
28
+ };
29
+ }
30
+ /**
31
+ * Factory metadata for registration.
32
+ */
33
+ export const FACTORY_META = {
34
+ base: AUTHORIZATION_POLICY_SOURCE_FACTORY_BASE_TYPE,
35
+ key: 'LocalFileAuthorizationPolicySource',
36
+ };
37
+ /**
38
+ * Factory for creating LocalFileAuthorizationPolicySource instances.
39
+ *
40
+ * This factory uses lazy loading to avoid pulling in Node.js-specific
41
+ * code (filesystem operations) in browser environments.
42
+ */
43
+ export class LocalFileAuthorizationPolicySourceFactory extends AuthorizationPolicySourceFactory {
44
+ constructor() {
45
+ super(...arguments);
46
+ this.type = 'LocalFileAuthorizationPolicySource';
47
+ }
48
+ /**
49
+ * Creates a LocalFileAuthorizationPolicySource from the given configuration.
50
+ *
51
+ * @param config - Configuration specifying the policy file path and options
52
+ * @returns The created policy source
53
+ */
54
+ async create(config) {
55
+ const normalized = normalizeConfig(config);
56
+ const { LocalFileAuthorizationPolicySource } = await getLocalFileModule();
57
+ return new LocalFileAuthorizationPolicySource({
58
+ path: normalized.path,
59
+ format: normalized.format,
60
+ policyFactory: normalized.policyFactory,
61
+ });
62
+ }
63
+ }
64
+ export default LocalFileAuthorizationPolicySourceFactory;
@@ -0,0 +1,127 @@
1
+ import { parse as parseYaml } from 'yaml';
2
+ import { getLogger } from '../../../util/logging.js';
3
+ import { AuthorizationPolicyFactory, } from './authorization-policy-factory.js';
4
+ const logger = getLogger('naylence.fame.security.auth.policy.local_file_authorization_policy_source');
5
+ function isPlainObject(value) {
6
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
7
+ }
8
+ function parseJson(content) {
9
+ const parsed = JSON.parse(content);
10
+ if (!isPlainObject(parsed)) {
11
+ throw new Error('Parsed JSON policy must be an object');
12
+ }
13
+ return parsed;
14
+ }
15
+ function parseYamlContent(content) {
16
+ const parsed = parseYaml(content ?? '');
17
+ if (parsed == null) {
18
+ return {};
19
+ }
20
+ if (!isPlainObject(parsed)) {
21
+ throw new Error('Parsed YAML policy must be an object');
22
+ }
23
+ return parsed;
24
+ }
25
+ function detectFormat(filePath) {
26
+ const lower = filePath.toLowerCase();
27
+ if (lower.endsWith('.yaml') || lower.endsWith('.yml')) {
28
+ return 'yaml';
29
+ }
30
+ if (lower.endsWith('.json')) {
31
+ return 'json';
32
+ }
33
+ // Default to YAML for unknown extensions
34
+ return 'yaml';
35
+ }
36
+ /**
37
+ * An authorization policy source that loads policy definitions from a local file.
38
+ *
39
+ * Supports YAML and JSON formats. The file must contain a valid policy
40
+ * configuration object that can be used to create an AuthorizationPolicy
41
+ * via the factory system.
42
+ *
43
+ * This is a Node.js-only implementation that uses the filesystem.
44
+ */
45
+ export class LocalFileAuthorizationPolicySource {
46
+ constructor(options) {
47
+ this.cachedPolicy = null;
48
+ this.path = options.path;
49
+ this.format = options.format ?? 'auto';
50
+ this.policyFactoryConfig = options.policyFactory;
51
+ }
52
+ /**
53
+ * Loads the authorization policy from the configured file.
54
+ *
55
+ * The file is read and parsed according to the configured format.
56
+ * The parsed content is then used to create an AuthorizationPolicy
57
+ * via the factory system.
58
+ *
59
+ * @returns The loaded authorization policy
60
+ */
61
+ async loadPolicy() {
62
+ // Return cached policy if available
63
+ if (this.cachedPolicy) {
64
+ return this.cachedPolicy;
65
+ }
66
+ logger.debug('loading_policy_from_file', { path: this.path });
67
+ // Dynamic import of fs for Node.js
68
+ const fs = await import('node:fs/promises');
69
+ // Read the file
70
+ const content = await fs.readFile(this.path, 'utf-8');
71
+ // Determine format
72
+ const effectiveFormat = this.format === 'auto' ? detectFormat(this.path) : this.format;
73
+ // Parse the content
74
+ let policyDefinition;
75
+ if (effectiveFormat === 'json') {
76
+ policyDefinition = parseJson(content);
77
+ }
78
+ else {
79
+ policyDefinition = parseYamlContent(content);
80
+ }
81
+ logger.debug('parsed_policy_definition', {
82
+ path: this.path,
83
+ format: effectiveFormat,
84
+ hasType: 'type' in policyDefinition,
85
+ });
86
+ // Determine the factory configuration to use
87
+ const factoryConfig = this.policyFactoryConfig ?? policyDefinition;
88
+ // Ensure we have a type field for the factory
89
+ if (!('type' in factoryConfig) || typeof factoryConfig.type !== 'string') {
90
+ throw new Error(`Policy definition at ${this.path} must have a 'type' field, ` +
91
+ `or policyFactory config must be provided`);
92
+ }
93
+ // Build the factory config with the policy definition
94
+ // The file content IS the policy definition, so we extract the type
95
+ // and wrap the remaining content as the policyDefinition
96
+ const { type, ...restOfFile } = policyDefinition;
97
+ const mergedConfig = this.policyFactoryConfig != null
98
+ ? { ...this.policyFactoryConfig, policyDefinition }
99
+ : { type: factoryConfig.type, policyDefinition: restOfFile };
100
+ // Create the policy using the factory system
101
+ const policy = await AuthorizationPolicyFactory.createAuthorizationPolicy(mergedConfig);
102
+ if (!policy) {
103
+ throw new Error(`Failed to create authorization policy from ${this.path}`);
104
+ }
105
+ this.cachedPolicy = policy;
106
+ logger.info('loaded_policy_from_file', {
107
+ path: this.path,
108
+ policyType: factoryConfig.type,
109
+ });
110
+ return policy;
111
+ }
112
+ /**
113
+ * Clears the cached policy, forcing a reload on the next loadPolicy() call.
114
+ */
115
+ clearCache() {
116
+ this.cachedPolicy = null;
117
+ }
118
+ /**
119
+ * Reloads the policy from the file, clearing any cached version.
120
+ *
121
+ * @returns The reloaded authorization policy
122
+ */
123
+ async reloadPolicy() {
124
+ this.clearCache();
125
+ return this.loadPolicy();
126
+ }
127
+ }