qdadm 0.31.0 → 0.32.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.
@@ -0,0 +1,212 @@
1
+ /**
2
+ * CompositeAuthAdapter - Routes to sub-adapters based on entity patterns
3
+ *
4
+ * Enables multi-source authentication where different entities may use
5
+ * different auth backends (internal JWT, external API keys, OAuth, etc.)
6
+ *
7
+ * Pattern matching:
8
+ * - Exact match: 'products' matches only 'products'
9
+ * - Prefix glob: 'external-*' matches 'external-products', 'external-orders'
10
+ * - Suffix glob: '*-readonly' matches 'products-readonly'
11
+ *
12
+ * @example
13
+ * ```js
14
+ * const composite = new CompositeAuthAdapter({
15
+ * default: internalAuthAdapter, // JWT for most entities
16
+ * mapping: {
17
+ * 'products': apiKeyAdapter, // API key for products
18
+ * 'external-*': externalAuth, // OAuth for external-* entities
19
+ * 'readonly-*': readonlyAuth // Read-only adapter
20
+ * }
21
+ * })
22
+ *
23
+ * composite.canPerform('books', 'read') // → uses default (internal)
24
+ * composite.canPerform('products', 'read') // → uses apiKeyAdapter
25
+ * composite.canPerform('external-orders', 'read') // → uses externalAuth
26
+ * ```
27
+ */
28
+
29
+ import { AuthAdapter } from './AuthAdapter.js'
30
+ import { authFactory } from './factory.js'
31
+
32
+ export class CompositeAuthAdapter extends AuthAdapter {
33
+ /**
34
+ * @param {object} config - Composite auth configuration
35
+ * @param {AuthAdapter|string|object} config.default - Default adapter (required)
36
+ * @param {Object<string, AuthAdapter|string|object>} [config.mapping={}] - Entity pattern to adapter mapping
37
+ * @param {object} [context={}] - Factory context with authTypes
38
+ */
39
+ constructor(config, context = {}) {
40
+ super()
41
+
42
+ const { default: defaultConfig, mapping = {} } = config
43
+
44
+ if (!defaultConfig) {
45
+ throw new Error('[CompositeAuthAdapter] "default" adapter is required')
46
+ }
47
+
48
+ // Resolve default adapter via factory
49
+ this._default = authFactory(defaultConfig, context)
50
+
51
+ // Resolve mapped adapters
52
+ this._exactMatches = new Map()
53
+ this._patterns = []
54
+
55
+ for (const [pattern, adapterConfig] of Object.entries(mapping)) {
56
+ const adapter = authFactory(adapterConfig, context)
57
+
58
+ if (pattern.includes('*')) {
59
+ // Glob pattern: convert to regex
60
+ const regexPattern = pattern
61
+ .replace(/[.+?^${}()|[\]\\]/g, '\\$&') // Escape regex special chars
62
+ .replace(/\*/g, '.*') // * → .*
63
+ const regex = new RegExp(`^${regexPattern}$`)
64
+ this._patterns.push({ pattern, regex, adapter })
65
+ } else {
66
+ // Exact match
67
+ this._exactMatches.set(pattern, adapter)
68
+ }
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Get the adapter for a specific entity
74
+ *
75
+ * Resolution order:
76
+ * 1. Exact match in mapping
77
+ * 2. First matching glob pattern
78
+ * 3. Default adapter
79
+ *
80
+ * @param {string} entity - Entity name
81
+ * @returns {AuthAdapter}
82
+ */
83
+ _getAdapter(entity) {
84
+ // 1. Exact match
85
+ if (this._exactMatches.has(entity)) {
86
+ return this._exactMatches.get(entity)
87
+ }
88
+
89
+ // 2. Pattern match (first wins)
90
+ for (const { regex, adapter } of this._patterns) {
91
+ if (regex.test(entity)) {
92
+ return adapter
93
+ }
94
+ }
95
+
96
+ // 3. Default
97
+ return this._default
98
+ }
99
+
100
+ /**
101
+ * Check if user can perform action on entity type (scope check)
102
+ * Delegates to the appropriate adapter based on entity name
103
+ *
104
+ * @param {string} entity - Entity name
105
+ * @param {string} action - Action: read, create, update, delete, list
106
+ * @returns {boolean}
107
+ */
108
+ canPerform(entity, action) {
109
+ return this._getAdapter(entity).canPerform(entity, action)
110
+ }
111
+
112
+ /**
113
+ * Check if user can access specific record (silo check)
114
+ * Delegates to the appropriate adapter based on entity name
115
+ *
116
+ * @param {string} entity - Entity name
117
+ * @param {object} record - The record to check
118
+ * @returns {boolean}
119
+ */
120
+ canAccessRecord(entity, record) {
121
+ return this._getAdapter(entity).canAccessRecord(entity, record)
122
+ }
123
+
124
+ /**
125
+ * Get current user from the default adapter
126
+ * The "user" concept comes from the primary auth source
127
+ *
128
+ * @returns {object|null}
129
+ */
130
+ getCurrentUser() {
131
+ return this._default.getCurrentUser()
132
+ }
133
+
134
+ /**
135
+ * Check if user is granted an attribute (role or permission)
136
+ * Delegates to default adapter for global permissions
137
+ *
138
+ * @param {string} attribute - Role or permission to check
139
+ * @param {object} [subject] - Optional subject for context
140
+ * @returns {boolean}
141
+ */
142
+ isGranted(attribute, subject = null) {
143
+ // For entity-specific permissions (entity:action), route to appropriate adapter
144
+ if (attribute.includes(':') && !attribute.startsWith('ROLE_')) {
145
+ const [entity] = attribute.split(':')
146
+ const adapter = this._getAdapter(entity)
147
+ if (adapter.isGranted) {
148
+ return adapter.isGranted(attribute, subject)
149
+ }
150
+ }
151
+
152
+ // Global permissions use default adapter
153
+ if (this._default.isGranted) {
154
+ return this._default.isGranted(attribute, subject)
155
+ }
156
+
157
+ // Fallback for adapters without isGranted
158
+ return true
159
+ }
160
+
161
+ /**
162
+ * Get the default adapter
163
+ * @returns {AuthAdapter}
164
+ */
165
+ get defaultAdapter() {
166
+ return this._default
167
+ }
168
+
169
+ /**
170
+ * Get adapter info for debugging
171
+ * @returns {object}
172
+ */
173
+ getAdapterInfo() {
174
+ const info = {
175
+ default: this._getAdapterName(this._default),
176
+ exactMatches: {},
177
+ patterns: []
178
+ }
179
+
180
+ for (const [entity, adapter] of this._exactMatches) {
181
+ info.exactMatches[entity] = this._getAdapterName(adapter)
182
+ }
183
+
184
+ for (const { pattern, adapter } of this._patterns) {
185
+ info.patterns.push({
186
+ pattern,
187
+ adapter: this._getAdapterName(adapter)
188
+ })
189
+ }
190
+
191
+ return info
192
+ }
193
+
194
+ /**
195
+ * Get adapter name for debugging
196
+ * @private
197
+ */
198
+ _getAdapterName(adapter) {
199
+ return adapter.constructor?.name || 'Unknown'
200
+ }
201
+ }
202
+
203
+ /**
204
+ * Factory function to create CompositeAuthAdapter
205
+ *
206
+ * @param {object} config - { default, mapping }
207
+ * @param {object} [context] - Factory context
208
+ * @returns {CompositeAuthAdapter}
209
+ */
210
+ export function createCompositeAuthAdapter(config, context = {}) {
211
+ return new CompositeAuthAdapter(config, context)
212
+ }
@@ -0,0 +1,207 @@
1
+ /**
2
+ * Auth Factory/Resolver Pattern
3
+ *
4
+ * Enables declarative auth adapter configuration with auto-resolution.
5
+ * Follows the same pattern as storage/factory.js for consistency.
6
+ *
7
+ * IMPORTANT: Backward compatible - passing an AuthAdapter instance
8
+ * works exactly as before (simple passthrough).
9
+ *
10
+ * Usage:
11
+ * ```js
12
+ * // Instance passthrough (current behavior, unchanged)
13
+ * authFactory(myAdapter) // → myAdapter
14
+ *
15
+ * // String pattern → factory resolves from registry
16
+ * authFactory('permissive') // → PermissiveAuthAdapter
17
+ * authFactory('jwt:internal') // → JwtAuthAdapter (if registered)
18
+ *
19
+ * // Config object → factory resolves
20
+ * authFactory({ type: 'jwt', tokenKey: 'auth_token' })
21
+ *
22
+ * // Composite config → creates CompositeAuthAdapter
23
+ * authFactory({
24
+ * default: myAdapter,
25
+ * mapping: { 'external-*': externalAdapter }
26
+ * })
27
+ * ```
28
+ */
29
+
30
+ import { AuthAdapter } from './AuthAdapter.js'
31
+ import { PermissiveAuthAdapter } from './PermissiveAdapter.js'
32
+
33
+ /**
34
+ * Built-in auth adapter types
35
+ * Extended via context.authTypes for custom adapters
36
+ * @type {Record<string, typeof AuthAdapter>}
37
+ */
38
+ export const authTypes = {
39
+ permissive: PermissiveAuthAdapter
40
+ }
41
+
42
+ /**
43
+ * Parse auth pattern 'type' or 'type:scope'
44
+ *
45
+ * @param {string} pattern - Auth pattern (e.g., 'jwt', 'apikey:external')
46
+ * @returns {{type: string, scope?: string} | null} Parsed config or null
47
+ *
48
+ * @example
49
+ * parseAuthPattern('jwt') // → { type: 'jwt' }
50
+ * parseAuthPattern('apikey:external') // → { type: 'apikey', scope: 'external' }
51
+ */
52
+ export function parseAuthPattern(pattern) {
53
+ if (typeof pattern !== 'string') return null
54
+
55
+ const match = pattern.match(/^(\w+)(?::(.+))?$/)
56
+ if (match) {
57
+ const [, type, scope] = match
58
+ return scope ? { type, scope } : { type }
59
+ }
60
+ return null
61
+ }
62
+
63
+ /**
64
+ * Default auth resolver - creates adapter instance from config
65
+ *
66
+ * @param {object} config - Normalized auth config with `type` property
67
+ * @param {object} context - Context with authTypes registry
68
+ * @returns {AuthAdapter} Adapter instance
69
+ */
70
+ export function defaultAuthResolver(config, context = {}) {
71
+ const { type, ...rest } = config
72
+
73
+ // Merge built-in types with custom types from context
74
+ const allTypes = { ...authTypes, ...context.authTypes }
75
+ const AdapterClass = allTypes[type]
76
+
77
+ if (!AdapterClass) {
78
+ throw new Error(
79
+ `[authFactory] Unknown auth type: "${type}". ` +
80
+ `Available: ${Object.keys(allTypes).join(', ')}`
81
+ )
82
+ }
83
+
84
+ return new AdapterClass(rest)
85
+ }
86
+
87
+ /**
88
+ * Check if config is a composite auth config
89
+ * Composite config has 'default' + optional 'mapping'
90
+ *
91
+ * @param {any} config
92
+ * @returns {boolean}
93
+ */
94
+ function isCompositeConfig(config) {
95
+ return (
96
+ config &&
97
+ typeof config === 'object' &&
98
+ 'default' in config &&
99
+ !('type' in config)
100
+ )
101
+ }
102
+
103
+ /**
104
+ * Auth factory - normalizes input and resolves to adapter
105
+ *
106
+ * Handles:
107
+ * - AuthAdapter instance → return directly (backward compatible)
108
+ * - String pattern 'type' → parse and resolve
109
+ * - Config object with 'type' → resolve via registry
110
+ * - Config object with 'default' → create CompositeAuthAdapter
111
+ *
112
+ * @param {AuthAdapter | string | object} config - Auth config
113
+ * @param {object} [context={}] - Context with authTypes, authResolver
114
+ * @returns {AuthAdapter} Adapter instance
115
+ *
116
+ * @example
117
+ * // Instance passthrough (most common, backward compatible)
118
+ * authFactory(myAdapter) // → myAdapter
119
+ *
120
+ * // String patterns
121
+ * authFactory('permissive') // → PermissiveAuthAdapter
122
+ * authFactory('jwt') // → JwtAuthAdapter (if registered)
123
+ *
124
+ * // Config objects
125
+ * authFactory({ type: 'jwt', tokenKey: 'token' })
126
+ *
127
+ * // Composite (multi-source)
128
+ * authFactory({
129
+ * default: myAdapter,
130
+ * mapping: { 'products': apiKeyAdapter }
131
+ * })
132
+ */
133
+ export function authFactory(config, context = {}) {
134
+ const { authResolver = defaultAuthResolver } = context
135
+
136
+ // Null/undefined → permissive (safe default)
137
+ if (config == null) {
138
+ return new PermissiveAuthAdapter()
139
+ }
140
+
141
+ // Already an AuthAdapter instance → return directly (backward compatible)
142
+ if (config instanceof AuthAdapter) {
143
+ return config
144
+ }
145
+
146
+ // Duck-typed adapter (has canPerform method)
147
+ if (typeof config.canPerform === 'function') {
148
+ return config
149
+ }
150
+
151
+ // String pattern → parse and resolve
152
+ if (typeof config === 'string') {
153
+ const parsed = parseAuthPattern(config)
154
+ if (!parsed) {
155
+ throw new Error(`[authFactory] Invalid auth pattern: "${config}"`)
156
+ }
157
+ return authResolver(parsed, context)
158
+ }
159
+
160
+ // Config object
161
+ if (typeof config === 'object') {
162
+ // Composite config: { default: ..., mapping: { ... } }
163
+ // Handled separately to avoid circular dependency
164
+ // The CompositeAuthAdapter is resolved via authTypes registry
165
+ if (isCompositeConfig(config)) {
166
+ const CompositeClass = context.authTypes?.composite || context.CompositeAuthAdapter
167
+ if (!CompositeClass) {
168
+ throw new Error(
169
+ '[authFactory] Composite config requires CompositeAuthAdapter. ' +
170
+ 'Pass it via context.CompositeAuthAdapter or register as context.authTypes.composite'
171
+ )
172
+ }
173
+ return new CompositeClass(config, context)
174
+ }
175
+
176
+ // Simple config with type: { type: 'jwt', ... }
177
+ if (config.type) {
178
+ return authResolver(config, context)
179
+ }
180
+
181
+ throw new Error(
182
+ '[authFactory] Config object requires either "type" or "default" property'
183
+ )
184
+ }
185
+
186
+ throw new Error(
187
+ `[authFactory] Invalid auth config: ${typeof config}. ` +
188
+ 'Expected AuthAdapter instance, string, or config object.'
189
+ )
190
+ }
191
+
192
+ /**
193
+ * Create a custom auth factory with bound context
194
+ *
195
+ * @param {object} context - Context with authTypes, authResolver
196
+ * @returns {function} Auth factory with bound context
197
+ *
198
+ * @example
199
+ * const myAuthFactory = createAuthFactory({
200
+ * authTypes: { jwt: JwtAuthAdapter, apikey: ApiKeyAuthAdapter }
201
+ * })
202
+ *
203
+ * const adapter = myAuthFactory('jwt')
204
+ */
205
+ export function createAuthFactory(context) {
206
+ return (config) => authFactory(config, context)
207
+ }
@@ -0,0 +1,257 @@
1
+ /**
2
+ * Tests for auth factory and CompositeAuthAdapter
3
+ */
4
+ import { describe, it, expect } from 'vitest'
5
+ import { AuthAdapter } from './AuthAdapter.js'
6
+ import { PermissiveAuthAdapter } from './PermissiveAdapter.js'
7
+ import { CompositeAuthAdapter } from './CompositeAuthAdapter.js'
8
+ import { authFactory, parseAuthPattern, authTypes } from './factory.js'
9
+
10
+ // Test adapter for custom types
11
+ class TestAuthAdapter extends AuthAdapter {
12
+ constructor(options = {}) {
13
+ super()
14
+ this.options = options
15
+ }
16
+ canPerform() { return true }
17
+ canAccessRecord() { return true }
18
+ getCurrentUser() { return { id: 'test' } }
19
+ }
20
+
21
+ // Adapter that tracks which entity was checked
22
+ class TrackingAdapter extends AuthAdapter {
23
+ constructor(name) {
24
+ super()
25
+ this.name = name
26
+ this.checkedEntities = []
27
+ }
28
+ canPerform(entity, action) {
29
+ this.checkedEntities.push({ entity, action })
30
+ return true
31
+ }
32
+ canAccessRecord() { return true }
33
+ getCurrentUser() { return { name: this.name } }
34
+ }
35
+
36
+ describe('parseAuthPattern', () => {
37
+ it('parses simple type', () => {
38
+ expect(parseAuthPattern('jwt')).toEqual({ type: 'jwt' })
39
+ expect(parseAuthPattern('permissive')).toEqual({ type: 'permissive' })
40
+ })
41
+
42
+ it('parses type with scope', () => {
43
+ expect(parseAuthPattern('jwt:internal')).toEqual({ type: 'jwt', scope: 'internal' })
44
+ expect(parseAuthPattern('apikey:external')).toEqual({ type: 'apikey', scope: 'external' })
45
+ })
46
+
47
+ it('returns null for invalid input', () => {
48
+ expect(parseAuthPattern(null)).toBeNull()
49
+ expect(parseAuthPattern(123)).toBeNull()
50
+ expect(parseAuthPattern('')).toBeNull()
51
+ })
52
+ })
53
+
54
+ describe('authFactory', () => {
55
+ describe('instance passthrough', () => {
56
+ it('returns AuthAdapter instance as-is', () => {
57
+ const adapter = new PermissiveAuthAdapter()
58
+ const result = authFactory(adapter)
59
+ expect(result).toBe(adapter)
60
+ })
61
+
62
+ it('returns duck-typed adapter as-is', () => {
63
+ const duckAdapter = {
64
+ canPerform: () => true,
65
+ canAccessRecord: () => true,
66
+ getCurrentUser: () => null
67
+ }
68
+ const result = authFactory(duckAdapter)
69
+ expect(result).toBe(duckAdapter)
70
+ })
71
+ })
72
+
73
+ describe('null/undefined', () => {
74
+ it('returns PermissiveAuthAdapter for null', () => {
75
+ const result = authFactory(null)
76
+ expect(result).toBeInstanceOf(PermissiveAuthAdapter)
77
+ })
78
+
79
+ it('returns PermissiveAuthAdapter for undefined', () => {
80
+ const result = authFactory(undefined)
81
+ expect(result).toBeInstanceOf(PermissiveAuthAdapter)
82
+ })
83
+ })
84
+
85
+ describe('string patterns', () => {
86
+ it('resolves built-in types', () => {
87
+ const result = authFactory('permissive')
88
+ expect(result).toBeInstanceOf(PermissiveAuthAdapter)
89
+ })
90
+
91
+ it('resolves custom types from context', () => {
92
+ const result = authFactory('test', {
93
+ authTypes: { test: TestAuthAdapter }
94
+ })
95
+ expect(result).toBeInstanceOf(TestAuthAdapter)
96
+ })
97
+
98
+ it('throws for unknown type', () => {
99
+ expect(() => authFactory('unknown')).toThrow(/Unknown auth type/)
100
+ })
101
+ })
102
+
103
+ describe('config objects', () => {
104
+ it('resolves config with type', () => {
105
+ const result = authFactory(
106
+ { type: 'test', foo: 'bar' },
107
+ { authTypes: { test: TestAuthAdapter } }
108
+ )
109
+ expect(result).toBeInstanceOf(TestAuthAdapter)
110
+ expect(result.options.foo).toBe('bar')
111
+ })
112
+
113
+ it('throws for config without type or default', () => {
114
+ expect(() => authFactory({ foo: 'bar' })).toThrow(/requires either "type" or "default"/)
115
+ })
116
+ })
117
+
118
+ describe('composite config', () => {
119
+ it('creates CompositeAuthAdapter from config', () => {
120
+ const defaultAdapter = new PermissiveAuthAdapter()
121
+ const result = authFactory(
122
+ { default: defaultAdapter },
123
+ { CompositeAuthAdapter }
124
+ )
125
+ expect(result).toBeInstanceOf(CompositeAuthAdapter)
126
+ })
127
+
128
+ it('throws without CompositeAuthAdapter in context', () => {
129
+ const defaultAdapter = new PermissiveAuthAdapter()
130
+ expect(() => authFactory({ default: defaultAdapter })).toThrow(/CompositeAuthAdapter/)
131
+ })
132
+ })
133
+ })
134
+
135
+ describe('CompositeAuthAdapter', () => {
136
+ describe('routing', () => {
137
+ it('uses default adapter for unmatched entities', () => {
138
+ const defaultAdapter = new TrackingAdapter('default')
139
+ const composite = new CompositeAuthAdapter({ default: defaultAdapter })
140
+
141
+ composite.canPerform('books', 'read')
142
+ composite.canPerform('users', 'create')
143
+
144
+ expect(defaultAdapter.checkedEntities).toEqual([
145
+ { entity: 'books', action: 'read' },
146
+ { entity: 'users', action: 'create' }
147
+ ])
148
+ })
149
+
150
+ it('routes exact matches to mapped adapter', () => {
151
+ const defaultAdapter = new TrackingAdapter('default')
152
+ const productsAdapter = new TrackingAdapter('products')
153
+
154
+ const composite = new CompositeAuthAdapter({
155
+ default: defaultAdapter,
156
+ mapping: { products: productsAdapter }
157
+ })
158
+
159
+ composite.canPerform('books', 'read')
160
+ composite.canPerform('products', 'read')
161
+
162
+ expect(defaultAdapter.checkedEntities).toHaveLength(1)
163
+ expect(defaultAdapter.checkedEntities[0].entity).toBe('books')
164
+
165
+ expect(productsAdapter.checkedEntities).toHaveLength(1)
166
+ expect(productsAdapter.checkedEntities[0].entity).toBe('products')
167
+ })
168
+
169
+ it('routes glob patterns to mapped adapter', () => {
170
+ const defaultAdapter = new TrackingAdapter('default')
171
+ const externalAdapter = new TrackingAdapter('external')
172
+
173
+ const composite = new CompositeAuthAdapter({
174
+ default: defaultAdapter,
175
+ mapping: { 'external-*': externalAdapter }
176
+ })
177
+
178
+ composite.canPerform('books', 'read')
179
+ composite.canPerform('external-products', 'read')
180
+ composite.canPerform('external-orders', 'list')
181
+
182
+ expect(defaultAdapter.checkedEntities).toHaveLength(1)
183
+ expect(externalAdapter.checkedEntities).toHaveLength(2)
184
+ })
185
+
186
+ it('supports suffix glob patterns', () => {
187
+ const defaultAdapter = new TrackingAdapter('default')
188
+ const readonlyAdapter = new TrackingAdapter('readonly')
189
+
190
+ const composite = new CompositeAuthAdapter({
191
+ default: defaultAdapter,
192
+ mapping: { '*-readonly': readonlyAdapter }
193
+ })
194
+
195
+ composite.canPerform('products-readonly', 'read')
196
+ composite.canPerform('products', 'read')
197
+
198
+ expect(readonlyAdapter.checkedEntities).toHaveLength(1)
199
+ expect(defaultAdapter.checkedEntities).toHaveLength(1)
200
+ })
201
+ })
202
+
203
+ describe('getCurrentUser', () => {
204
+ it('returns user from default adapter', () => {
205
+ const defaultAdapter = new TrackingAdapter('admin')
206
+ const externalAdapter = new TrackingAdapter('service')
207
+
208
+ const composite = new CompositeAuthAdapter({
209
+ default: defaultAdapter,
210
+ mapping: { 'external-*': externalAdapter }
211
+ })
212
+
213
+ expect(composite.getCurrentUser()).toEqual({ name: 'admin' })
214
+ })
215
+ })
216
+
217
+ describe('factory integration', () => {
218
+ it('resolves adapters in mapping via factory', () => {
219
+ const defaultAdapter = new PermissiveAuthAdapter()
220
+
221
+ const composite = new CompositeAuthAdapter(
222
+ {
223
+ default: defaultAdapter,
224
+ mapping: {
225
+ products: { type: 'test' }
226
+ }
227
+ },
228
+ { authTypes: { test: TestAuthAdapter } }
229
+ )
230
+
231
+ // Products should use TestAuthAdapter
232
+ expect(composite._getAdapter('products')).toBeInstanceOf(TestAuthAdapter)
233
+ // Others use default
234
+ expect(composite._getAdapter('books')).toBe(defaultAdapter)
235
+ })
236
+ })
237
+
238
+ describe('getAdapterInfo', () => {
239
+ it('returns debug info about adapters', () => {
240
+ const composite = new CompositeAuthAdapter({
241
+ default: new PermissiveAuthAdapter(),
242
+ mapping: {
243
+ products: new TestAuthAdapter(),
244
+ 'external-*': new TrackingAdapter('ext')
245
+ }
246
+ })
247
+
248
+ const info = composite.getAdapterInfo()
249
+
250
+ expect(info.default).toBe('PermissiveAuthAdapter')
251
+ expect(info.exactMatches.products).toBe('TestAuthAdapter')
252
+ expect(info.patterns).toHaveLength(1)
253
+ expect(info.patterns[0].pattern).toBe('external-*')
254
+ expect(info.patterns[0].adapter).toBe('TrackingAdapter')
255
+ })
256
+ })
257
+ })
@@ -3,6 +3,10 @@
3
3
  *
4
4
  * AuthAdapter interface and implementations for scope/silo permission checks.
5
5
  * Includes Symfony-inspired role hierarchy and security checker.
6
+ *
7
+ * Multi-source auth:
8
+ * - CompositeAuthAdapter routes to different adapters based on entity patterns
9
+ * - authFactory normalizes config (instance, string, or object)
6
10
  */
7
11
 
8
12
  // Interface
@@ -16,3 +20,13 @@ export { SecurityChecker, createSecurityChecker } from './SecurityChecker.js'
16
20
 
17
21
  // Implementations
18
22
  export { PermissiveAuthAdapter, createPermissiveAdapter } from './PermissiveAdapter.js'
23
+ export { CompositeAuthAdapter, createCompositeAuthAdapter } from './CompositeAuthAdapter.js'
24
+
25
+ // Factory/Resolver pattern (like storage/factory.js)
26
+ export {
27
+ authFactory,
28
+ createAuthFactory,
29
+ authTypes,
30
+ parseAuthPattern,
31
+ defaultAuthResolver
32
+ } from './factory.js'