@pikku/inspector 0.9.6-next.0 → 0.10.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/CHANGELOG.md +6 -0
- package/dist/add/add-channel.d.ts +5 -1
- package/dist/add/add-channel.js +51 -32
- package/dist/add/add-cli.d.ts +4 -0
- package/dist/add/add-cli.js +128 -23
- package/dist/add/add-file-extends-core-type.js +3 -2
- package/dist/add/add-file-with-factory.d.ts +2 -2
- package/dist/add/add-file-with-factory.js +34 -1
- package/dist/add/add-functions.js +52 -5
- package/dist/add/add-http-route.js +19 -12
- package/dist/add/add-mcp-prompt.js +20 -13
- package/dist/add/add-mcp-resource.js +24 -14
- package/dist/add/add-mcp-tool.js +23 -13
- package/dist/add/add-middleware.js +51 -12
- package/dist/add/add-permission.d.ts +1 -2
- package/dist/add/add-permission.js +275 -19
- package/dist/add/add-queue-worker.js +10 -12
- package/dist/add/add-schedule.js +9 -10
- package/dist/error-codes.d.ts +35 -0
- package/dist/error-codes.js +40 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +3 -0
- package/dist/inspector.js +20 -1
- package/dist/types.d.ts +31 -3
- package/dist/utils/ensure-function-metadata.d.ts +6 -0
- package/dist/utils/ensure-function-metadata.js +18 -0
- package/dist/utils/extract-function-name.d.ts +2 -2
- package/dist/utils/extract-function-name.js +13 -8
- package/dist/utils/filter-inspector-state.d.ts +6 -0
- package/dist/utils/filter-inspector-state.js +382 -0
- package/dist/utils/filter-utils.d.ts +10 -0
- package/dist/utils/filter-utils.js +66 -2
- package/dist/utils/find-root-dir.d.ts +23 -0
- package/dist/utils/find-root-dir.js +55 -0
- package/dist/utils/get-files-and-methods.d.ts +2 -1
- package/dist/utils/get-files-and-methods.js +2 -1
- package/dist/utils/get-property-value.d.ts +9 -0
- package/dist/utils/get-property-value.js +20 -0
- package/dist/utils/middleware.d.ts +1 -1
- package/dist/utils/middleware.js +7 -7
- package/dist/utils/permissions.d.ts +43 -0
- package/dist/utils/permissions.js +178 -0
- package/dist/utils/post-process.d.ts +16 -0
- package/dist/utils/post-process.js +132 -0
- package/dist/utils/serialize-inspector-state.d.ts +179 -0
- package/dist/utils/serialize-inspector-state.js +170 -0
- package/dist/visit.js +3 -2
- package/package.json +4 -4
- package/src/add/add-channel.ts +92 -40
- package/src/add/add-cli.ts +188 -29
- package/src/add/add-file-extends-core-type.ts +5 -2
- package/src/add/add-file-with-factory.ts +45 -2
- package/src/add/add-functions.ts +60 -5
- package/src/add/add-http-route.ts +46 -21
- package/src/add/add-mcp-prompt.ts +42 -21
- package/src/add/add-mcp-prompt.ts.tmp +0 -0
- package/src/add/add-mcp-resource.ts +50 -24
- package/src/add/add-mcp-resource.ts.tmp +0 -0
- package/src/add/add-mcp-tool.ts +48 -21
- package/src/add/add-middleware.ts +74 -15
- package/src/add/add-permission.ts +364 -22
- package/src/add/add-queue-worker.ts +22 -25
- package/src/add/add-schedule.ts +19 -20
- package/src/error-codes.ts +43 -0
- package/src/index.ts +7 -0
- package/src/inspector.ts +22 -1
- package/src/types.ts +38 -3
- package/src/utils/ensure-function-metadata.ts +24 -0
- package/src/utils/extract-function-name.ts +20 -8
- package/src/utils/filter-inspector-state.test.ts +1433 -0
- package/src/utils/filter-inspector-state.ts +526 -0
- package/src/utils/filter-utils.test.ts +350 -1
- package/src/utils/filter-utils.ts +82 -2
- package/src/utils/find-root-dir.ts +68 -0
- package/src/utils/get-files-and-methods.ts +8 -0
- package/src/utils/get-property-value.ts +27 -0
- package/src/utils/middleware.ts +14 -7
- package/src/utils/permissions.test.ts +327 -0
- package/src/utils/permissions.ts +262 -0
- package/src/utils/post-process.ts +178 -0
- package/src/utils/serialize-inspector-state.ts +375 -0
- package/src/utils/test-data/inspector-state.json +1680 -0
- package/src/visit.ts +4 -2
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
import { describe, test } from 'node:test'
|
|
2
|
+
import assert from 'node:assert'
|
|
3
|
+
import {
|
|
4
|
+
routeMatchesPattern,
|
|
5
|
+
resolveHTTPPermissions,
|
|
6
|
+
resolvePermissions,
|
|
7
|
+
} from './permissions.js'
|
|
8
|
+
import type { InspectorState, PermissionGroupMeta } from '../types.js'
|
|
9
|
+
|
|
10
|
+
describe('routeMatchesPattern', () => {
|
|
11
|
+
test('should match exact routes', () => {
|
|
12
|
+
assert.strictEqual(routeMatchesPattern('/api/users', '/api/users'), true)
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
test('should not match different routes', () => {
|
|
16
|
+
assert.strictEqual(routeMatchesPattern('/api/users', '/api/posts'), false)
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
test('should match wildcard patterns', () => {
|
|
20
|
+
assert.strictEqual(routeMatchesPattern('/api/users', '/api/*'), true)
|
|
21
|
+
assert.strictEqual(routeMatchesPattern('/api/users/123', '/api/*'), true)
|
|
22
|
+
assert.strictEqual(routeMatchesPattern('/api/posts', '/api/*'), true)
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
test('should match global wildcard', () => {
|
|
26
|
+
assert.strictEqual(routeMatchesPattern('/api/users', '*'), true)
|
|
27
|
+
assert.strictEqual(routeMatchesPattern('/any/path', '*'), true)
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
test('should not match if pattern is more specific', () => {
|
|
31
|
+
assert.strictEqual(routeMatchesPattern('/users', '/api/*'), false)
|
|
32
|
+
})
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
describe('resolveHTTPPermissions', () => {
|
|
36
|
+
test('should return undefined when no permissions exist', () => {
|
|
37
|
+
const state: InspectorState = createMockState()
|
|
38
|
+
|
|
39
|
+
const result = resolveHTTPPermissions(
|
|
40
|
+
state,
|
|
41
|
+
'/api/test',
|
|
42
|
+
undefined,
|
|
43
|
+
undefined,
|
|
44
|
+
{} as any
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
assert.strictEqual(result, undefined)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
test('should resolve global HTTP permissions', () => {
|
|
51
|
+
const state: InspectorState = createMockState()
|
|
52
|
+
const globalPermission: PermissionGroupMeta = {
|
|
53
|
+
exportName: 'globalPermissions',
|
|
54
|
+
sourceFile: '/test.ts',
|
|
55
|
+
position: 0,
|
|
56
|
+
services: { services: [], singletons: [] },
|
|
57
|
+
permissionCount: 1,
|
|
58
|
+
isFactory: true,
|
|
59
|
+
}
|
|
60
|
+
state.http.routePermissions.set('*', globalPermission)
|
|
61
|
+
|
|
62
|
+
const result = resolveHTTPPermissions(
|
|
63
|
+
state,
|
|
64
|
+
'/api/test',
|
|
65
|
+
undefined,
|
|
66
|
+
undefined,
|
|
67
|
+
{} as any
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
assert.ok(result)
|
|
71
|
+
assert.strictEqual(result.length, 1)
|
|
72
|
+
assert.deepEqual(result[0], { type: 'http', route: '*' })
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
test('should resolve route pattern permissions', () => {
|
|
76
|
+
const state: InspectorState = createMockState()
|
|
77
|
+
const apiPermission: PermissionGroupMeta = {
|
|
78
|
+
exportName: 'apiPermissions',
|
|
79
|
+
sourceFile: '/test.ts',
|
|
80
|
+
position: 0,
|
|
81
|
+
services: { services: [], singletons: [] },
|
|
82
|
+
permissionCount: 1,
|
|
83
|
+
isFactory: true,
|
|
84
|
+
}
|
|
85
|
+
state.http.routePermissions.set('/api/*', apiPermission)
|
|
86
|
+
|
|
87
|
+
const result = resolveHTTPPermissions(
|
|
88
|
+
state,
|
|
89
|
+
'/api/users',
|
|
90
|
+
undefined,
|
|
91
|
+
undefined,
|
|
92
|
+
{} as any
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
assert.ok(result)
|
|
96
|
+
assert.strictEqual(result.length, 1)
|
|
97
|
+
assert.deepEqual(result[0], { type: 'http', route: '/api/*' })
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
test('should resolve tag-based permissions', () => {
|
|
101
|
+
const state: InspectorState = createMockState()
|
|
102
|
+
const adminPermission: PermissionGroupMeta = {
|
|
103
|
+
exportName: 'adminPermissions',
|
|
104
|
+
sourceFile: '/test.ts',
|
|
105
|
+
position: 0,
|
|
106
|
+
services: { services: [], singletons: [] },
|
|
107
|
+
permissionCount: 1,
|
|
108
|
+
isFactory: true,
|
|
109
|
+
}
|
|
110
|
+
state.permissions.tagPermissions.set('admin', adminPermission)
|
|
111
|
+
|
|
112
|
+
const result = resolveHTTPPermissions(
|
|
113
|
+
state,
|
|
114
|
+
'/api/admin',
|
|
115
|
+
['admin'],
|
|
116
|
+
undefined,
|
|
117
|
+
{} as any
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
assert.ok(result)
|
|
121
|
+
assert.strictEqual(result.length, 1)
|
|
122
|
+
assert.deepEqual(result[0], { type: 'tag', tag: 'admin' })
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
test('should combine multiple permission sources in correct order', () => {
|
|
126
|
+
const state: InspectorState = createMockState()
|
|
127
|
+
|
|
128
|
+
// Setup global HTTP permission
|
|
129
|
+
state.http.routePermissions.set('*', {
|
|
130
|
+
exportName: 'global',
|
|
131
|
+
sourceFile: '/test.ts',
|
|
132
|
+
position: 0,
|
|
133
|
+
services: { services: [], singletons: [] },
|
|
134
|
+
permissionCount: 1,
|
|
135
|
+
isFactory: true,
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
// Setup route pattern permission
|
|
139
|
+
state.http.routePermissions.set('/api/*', {
|
|
140
|
+
exportName: 'apiRoute',
|
|
141
|
+
sourceFile: '/test.ts',
|
|
142
|
+
position: 0,
|
|
143
|
+
services: { services: [], singletons: [] },
|
|
144
|
+
permissionCount: 1,
|
|
145
|
+
isFactory: true,
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
// Setup tag permission
|
|
149
|
+
state.permissions.tagPermissions.set('admin', {
|
|
150
|
+
exportName: 'adminTag',
|
|
151
|
+
sourceFile: '/test.ts',
|
|
152
|
+
position: 0,
|
|
153
|
+
services: { services: [], singletons: [] },
|
|
154
|
+
permissionCount: 1,
|
|
155
|
+
isFactory: true,
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
// Setup explicit permission
|
|
159
|
+
state.permissions.meta['testPermission'] = {
|
|
160
|
+
services: { services: [], singletons: [] },
|
|
161
|
+
sourceFile: '/test.ts',
|
|
162
|
+
position: 0,
|
|
163
|
+
exportedName: 'testPerm',
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const result = resolveHTTPPermissions(
|
|
167
|
+
state,
|
|
168
|
+
'/api/admin',
|
|
169
|
+
['admin'],
|
|
170
|
+
undefined, // We'd need a real TS node for this
|
|
171
|
+
{} as any
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
assert.ok(result)
|
|
175
|
+
// Should have: global HTTP, route pattern, tag
|
|
176
|
+
assert.strictEqual(result.length, 3)
|
|
177
|
+
assert.deepEqual(result[0], { type: 'http', route: '*' })
|
|
178
|
+
assert.deepEqual(result[1], { type: 'http', route: '/api/*' })
|
|
179
|
+
assert.deepEqual(result[2], { type: 'tag', tag: 'admin' })
|
|
180
|
+
})
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
describe('resolvePermissions', () => {
|
|
184
|
+
test('should return undefined when no permissions exist', () => {
|
|
185
|
+
const state: InspectorState = createMockState()
|
|
186
|
+
const mockObj = createMockObjectLiteral()
|
|
187
|
+
|
|
188
|
+
const result = resolvePermissions(state, mockObj, undefined, {} as any)
|
|
189
|
+
|
|
190
|
+
assert.strictEqual(result, undefined)
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
test('should resolve tag-based permissions', () => {
|
|
194
|
+
const state: InspectorState = createMockState()
|
|
195
|
+
const mcpPermission: PermissionGroupMeta = {
|
|
196
|
+
exportName: 'mcpPermissions',
|
|
197
|
+
sourceFile: '/test.ts',
|
|
198
|
+
position: 0,
|
|
199
|
+
services: { services: [], singletons: [] },
|
|
200
|
+
permissionCount: 1,
|
|
201
|
+
isFactory: true,
|
|
202
|
+
}
|
|
203
|
+
state.permissions.tagPermissions.set('mcp', mcpPermission)
|
|
204
|
+
const mockObj = createMockObjectLiteral()
|
|
205
|
+
|
|
206
|
+
const result = resolvePermissions(state, mockObj, ['mcp'], {} as any)
|
|
207
|
+
|
|
208
|
+
assert.ok(result)
|
|
209
|
+
assert.strictEqual(result.length, 1)
|
|
210
|
+
assert.deepEqual(result[0], { type: 'tag', tag: 'mcp' })
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
test('should handle multiple tags', () => {
|
|
214
|
+
const state: InspectorState = createMockState()
|
|
215
|
+
state.permissions.tagPermissions.set('mcp', {
|
|
216
|
+
exportName: 'mcpPerms',
|
|
217
|
+
sourceFile: '/test.ts',
|
|
218
|
+
position: 0,
|
|
219
|
+
services: { services: [], singletons: [] },
|
|
220
|
+
permissionCount: 1,
|
|
221
|
+
isFactory: true,
|
|
222
|
+
})
|
|
223
|
+
state.permissions.tagPermissions.set('admin', {
|
|
224
|
+
exportName: 'adminPerms',
|
|
225
|
+
sourceFile: '/test.ts',
|
|
226
|
+
position: 0,
|
|
227
|
+
services: { services: [], singletons: [] },
|
|
228
|
+
permissionCount: 1,
|
|
229
|
+
isFactory: true,
|
|
230
|
+
})
|
|
231
|
+
const mockObj = createMockObjectLiteral()
|
|
232
|
+
|
|
233
|
+
const result = resolvePermissions(
|
|
234
|
+
state,
|
|
235
|
+
mockObj,
|
|
236
|
+
['mcp', 'admin'],
|
|
237
|
+
{} as any
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
assert.ok(result)
|
|
241
|
+
assert.strictEqual(result.length, 2)
|
|
242
|
+
assert.deepEqual(result[0], { type: 'tag', tag: 'mcp' })
|
|
243
|
+
assert.deepEqual(result[1], { type: 'tag', tag: 'admin' })
|
|
244
|
+
})
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
// Helper to create a mock TypeScript object literal expression
|
|
248
|
+
function createMockObjectLiteral(): any {
|
|
249
|
+
return {
|
|
250
|
+
properties: [],
|
|
251
|
+
kind: 206, // ObjectLiteralExpression
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Helper function to create a mock InspectorState
|
|
256
|
+
function createMockState(): InspectorState {
|
|
257
|
+
return {
|
|
258
|
+
singletonServicesTypeImportMap: new Map(),
|
|
259
|
+
sessionServicesTypeImportMap: new Map(),
|
|
260
|
+
userSessionTypeImportMap: new Map(),
|
|
261
|
+
configTypeImportMap: new Map(),
|
|
262
|
+
singletonServicesFactories: new Map(),
|
|
263
|
+
sessionServicesFactories: new Map(),
|
|
264
|
+
sessionServicesMeta: new Map(),
|
|
265
|
+
configFactories: new Map(),
|
|
266
|
+
filesAndMethods: {},
|
|
267
|
+
filesAndMethodsErrors: new Map(),
|
|
268
|
+
typesLookup: new Map(),
|
|
269
|
+
http: {
|
|
270
|
+
metaInputTypes: new Map(),
|
|
271
|
+
meta: {
|
|
272
|
+
get: {},
|
|
273
|
+
post: {},
|
|
274
|
+
put: {},
|
|
275
|
+
delete: {},
|
|
276
|
+
head: {},
|
|
277
|
+
patch: {},
|
|
278
|
+
options: {},
|
|
279
|
+
},
|
|
280
|
+
files: new Set(),
|
|
281
|
+
routeMiddleware: new Map(),
|
|
282
|
+
routePermissions: new Map(),
|
|
283
|
+
},
|
|
284
|
+
functions: {
|
|
285
|
+
typesMap: {} as any,
|
|
286
|
+
meta: {},
|
|
287
|
+
files: new Map(),
|
|
288
|
+
},
|
|
289
|
+
channels: {
|
|
290
|
+
meta: {},
|
|
291
|
+
files: new Set(),
|
|
292
|
+
},
|
|
293
|
+
scheduledTasks: {
|
|
294
|
+
meta: {},
|
|
295
|
+
files: new Set(),
|
|
296
|
+
},
|
|
297
|
+
queueWorkers: {
|
|
298
|
+
meta: {},
|
|
299
|
+
files: new Set(),
|
|
300
|
+
},
|
|
301
|
+
rpc: {
|
|
302
|
+
internalMeta: {},
|
|
303
|
+
internalFiles: new Map(),
|
|
304
|
+
exposedMeta: {},
|
|
305
|
+
exposedFiles: new Map(),
|
|
306
|
+
invokedFunctions: new Set(),
|
|
307
|
+
},
|
|
308
|
+
mcpEndpoints: {
|
|
309
|
+
resourcesMeta: {},
|
|
310
|
+
toolsMeta: {},
|
|
311
|
+
promptsMeta: {},
|
|
312
|
+
files: new Set(),
|
|
313
|
+
},
|
|
314
|
+
cli: {
|
|
315
|
+
meta: {},
|
|
316
|
+
files: new Set(),
|
|
317
|
+
},
|
|
318
|
+
middleware: {
|
|
319
|
+
meta: {},
|
|
320
|
+
tagMiddleware: new Map(),
|
|
321
|
+
},
|
|
322
|
+
permissions: {
|
|
323
|
+
meta: {},
|
|
324
|
+
tagPermissions: new Map(),
|
|
325
|
+
},
|
|
326
|
+
}
|
|
327
|
+
}
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import * as ts from 'typescript'
|
|
2
|
+
import { PermissionMetadata } from '@pikku/core'
|
|
3
|
+
import { extractFunctionName } from './extract-function-name.js'
|
|
4
|
+
import { InspectorState } from '../types.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Extract permission pikkuFuncNames from an expression (array or object literal)
|
|
8
|
+
* Resolves each identifier to its pikkuFuncName using extractFunctionName
|
|
9
|
+
* Also handles call expressions (like rolePermission({...}))
|
|
10
|
+
*
|
|
11
|
+
* Supports both formats:
|
|
12
|
+
* - Array: [permission1, permission2]
|
|
13
|
+
* - Record: { groupName: permission1, anotherGroup: [permission2, permission3] }
|
|
14
|
+
*/
|
|
15
|
+
export function extractPermissionPikkuNames(
|
|
16
|
+
node: ts.Expression,
|
|
17
|
+
checker: ts.TypeChecker,
|
|
18
|
+
rootDir: string
|
|
19
|
+
): string[] {
|
|
20
|
+
const names: string[] = []
|
|
21
|
+
|
|
22
|
+
// Helper to extract from a single element
|
|
23
|
+
const extractFromElement = (element: ts.Expression) => {
|
|
24
|
+
if (ts.isIdentifier(element)) {
|
|
25
|
+
const { pikkuFuncName } = extractFunctionName(element, checker, rootDir)
|
|
26
|
+
names.push(pikkuFuncName)
|
|
27
|
+
} else if (ts.isCallExpression(element)) {
|
|
28
|
+
// Handle call expressions like hasEmailQuota(100) or rolePermission({...})
|
|
29
|
+
// Extract the function being called (e.g., 'hasEmailQuota' from 'hasEmailQuota(100)')
|
|
30
|
+
const { pikkuFuncName } = extractFunctionName(
|
|
31
|
+
element.expression,
|
|
32
|
+
checker,
|
|
33
|
+
rootDir
|
|
34
|
+
)
|
|
35
|
+
names.push(pikkuFuncName)
|
|
36
|
+
} else if (ts.isArrayLiteralExpression(element)) {
|
|
37
|
+
// Nested array (for Record values that are arrays)
|
|
38
|
+
for (const nestedElement of element.elements) {
|
|
39
|
+
extractFromElement(nestedElement)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (ts.isArrayLiteralExpression(node)) {
|
|
45
|
+
// Array format: [permission1, permission2]
|
|
46
|
+
for (const element of node.elements) {
|
|
47
|
+
extractFromElement(element)
|
|
48
|
+
}
|
|
49
|
+
} else if (ts.isObjectLiteralExpression(node)) {
|
|
50
|
+
// Record format: { groupName: permission1, anotherGroup: [permission2] }
|
|
51
|
+
for (const prop of node.properties) {
|
|
52
|
+
if (ts.isPropertyAssignment(prop)) {
|
|
53
|
+
extractFromElement(prop.initializer)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return names
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Get permissions array from an object literal expression property
|
|
63
|
+
* Returns the initializer node for the 'permissions' property if it exists
|
|
64
|
+
*/
|
|
65
|
+
export function getPermissionsNode(
|
|
66
|
+
obj: ts.ObjectLiteralExpression
|
|
67
|
+
): ts.Expression | undefined {
|
|
68
|
+
const permissionsProp = obj.properties.find(
|
|
69
|
+
(p) =>
|
|
70
|
+
ts.isPropertyAssignment(p) &&
|
|
71
|
+
ts.isIdentifier(p.name) &&
|
|
72
|
+
p.name.text === 'permissions'
|
|
73
|
+
) as ts.PropertyAssignment | undefined
|
|
74
|
+
|
|
75
|
+
return permissionsProp?.initializer
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Check if a route matches a pattern with wildcards
|
|
80
|
+
* Pattern can be exact match or use * as wildcard
|
|
81
|
+
* e.g., '/api/*' matches '/api/users', '/api/posts/123', etc.
|
|
82
|
+
*/
|
|
83
|
+
export function routeMatchesPattern(route: string, pattern: string): boolean {
|
|
84
|
+
if (route === pattern) return true
|
|
85
|
+
|
|
86
|
+
// Convert pattern to regex: replace * with .*
|
|
87
|
+
const regexPattern = pattern
|
|
88
|
+
.replace(/[.+?^${}()|[\]\\]/g, '\\$&') // Escape regex special chars except *
|
|
89
|
+
.replace(/\*/g, '.*') // Replace * with .*
|
|
90
|
+
|
|
91
|
+
const regex = new RegExp(`^${regexPattern}$`)
|
|
92
|
+
return regex.test(route)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Resolve permissions for an HTTP wiring based on:
|
|
97
|
+
* 1. Global HTTP permissions (addHTTPPermission('*', [...]))
|
|
98
|
+
* 2. Route-specific HTTP permissions (addHTTPPermission('/pattern', [...]))
|
|
99
|
+
* 3. Tag-based permissions (addPermission('tag', [...]))
|
|
100
|
+
* 4. Explicit wiring permissions (wireHTTP({ permissions: [...] }))
|
|
101
|
+
* Returns undefined if no permissions are found, otherwise returns array with at least one item
|
|
102
|
+
*/
|
|
103
|
+
export function resolveHTTPPermissions(
|
|
104
|
+
state: InspectorState,
|
|
105
|
+
route: string,
|
|
106
|
+
tags: string[] | undefined,
|
|
107
|
+
explicitPermissionsNode: ts.Expression | undefined,
|
|
108
|
+
checker: ts.TypeChecker
|
|
109
|
+
): PermissionMetadata[] | undefined {
|
|
110
|
+
const resolved: PermissionMetadata[] = []
|
|
111
|
+
|
|
112
|
+
// 1. HTTP route permission groups (includes '*' for global)
|
|
113
|
+
for (const [pattern, _groupMeta] of state.http.routePermissions.entries()) {
|
|
114
|
+
if (routeMatchesPattern(route, pattern)) {
|
|
115
|
+
// Just reference the group by route pattern
|
|
116
|
+
resolved.push({
|
|
117
|
+
type: 'http',
|
|
118
|
+
route: pattern,
|
|
119
|
+
})
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// 2. Tag-based permission groups
|
|
124
|
+
if (tags && tags.length > 0) {
|
|
125
|
+
for (const tag of tags) {
|
|
126
|
+
if (state.permissions.tagPermissions.has(tag)) {
|
|
127
|
+
// Just reference the group by tag
|
|
128
|
+
resolved.push({
|
|
129
|
+
type: 'tag',
|
|
130
|
+
tag,
|
|
131
|
+
})
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// 3. Explicit wire permissions (inline is OK here)
|
|
137
|
+
if (explicitPermissionsNode) {
|
|
138
|
+
const permissionNames = extractPermissionPikkuNames(
|
|
139
|
+
explicitPermissionsNode,
|
|
140
|
+
checker,
|
|
141
|
+
state.rootDir
|
|
142
|
+
)
|
|
143
|
+
for (const name of permissionNames) {
|
|
144
|
+
const meta = state.permissions.meta[name]
|
|
145
|
+
resolved.push({
|
|
146
|
+
type: 'wire',
|
|
147
|
+
name,
|
|
148
|
+
inline: meta?.exportedName === null,
|
|
149
|
+
})
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return resolved.length > 0 ? resolved : undefined
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Resolve tag-based and explicit permissions (common logic for wires and functions)
|
|
158
|
+
* 1. Tag-based permissions (addPermission('tag', [...]))
|
|
159
|
+
* 2. Explicit permissions (wireHTTP/pikkuFunc({ permissions: [...] }))
|
|
160
|
+
*/
|
|
161
|
+
function resolveTagAndExplicitPermissions(
|
|
162
|
+
state: InspectorState,
|
|
163
|
+
tags: string[] | undefined,
|
|
164
|
+
explicitPermissionsNode: ts.Expression | undefined,
|
|
165
|
+
checker: ts.TypeChecker
|
|
166
|
+
): PermissionMetadata[] {
|
|
167
|
+
const resolved: PermissionMetadata[] = []
|
|
168
|
+
|
|
169
|
+
// 1. Tag-based permission groups
|
|
170
|
+
if (tags && tags.length > 0) {
|
|
171
|
+
for (const tag of tags) {
|
|
172
|
+
if (state.permissions.tagPermissions.has(tag)) {
|
|
173
|
+
// Just reference the group by tag
|
|
174
|
+
resolved.push({
|
|
175
|
+
type: 'tag',
|
|
176
|
+
tag,
|
|
177
|
+
})
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// 2. Explicit permissions (inline is OK here - used directly in wire/function)
|
|
183
|
+
if (explicitPermissionsNode) {
|
|
184
|
+
const permissionNames = extractPermissionPikkuNames(
|
|
185
|
+
explicitPermissionsNode,
|
|
186
|
+
checker,
|
|
187
|
+
state.rootDir
|
|
188
|
+
)
|
|
189
|
+
for (const name of permissionNames) {
|
|
190
|
+
const meta = state.permissions.meta[name]
|
|
191
|
+
resolved.push({
|
|
192
|
+
type: 'wire',
|
|
193
|
+
name,
|
|
194
|
+
inline: meta?.exportedName === null,
|
|
195
|
+
})
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return resolved
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Resolve permissions for a function based on:
|
|
204
|
+
* 1. Tag-based permissions (addPermission('tag', [...]))
|
|
205
|
+
* 2. Explicit function permissions (pikkuFunc({ permissions: [...] }))
|
|
206
|
+
* Returns undefined if no permissions are found, otherwise returns array with at least one item
|
|
207
|
+
*/
|
|
208
|
+
function resolveFunctionPermissionsInternal(
|
|
209
|
+
state: InspectorState,
|
|
210
|
+
tags: string[] | undefined,
|
|
211
|
+
explicitPermissionsNode: ts.Expression | undefined,
|
|
212
|
+
checker: ts.TypeChecker
|
|
213
|
+
): PermissionMetadata[] | undefined {
|
|
214
|
+
const resolved = resolveTagAndExplicitPermissions(
|
|
215
|
+
state,
|
|
216
|
+
tags,
|
|
217
|
+
explicitPermissionsNode,
|
|
218
|
+
checker
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
return resolved.length > 0 ? resolved : undefined
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Convenience wrapper: Extract permissions node from object and resolve
|
|
226
|
+
* Use this in add-* files for cleaner code
|
|
227
|
+
*/
|
|
228
|
+
export function resolvePermissions(
|
|
229
|
+
state: InspectorState,
|
|
230
|
+
obj: ts.ObjectLiteralExpression,
|
|
231
|
+
tags: string[] | undefined,
|
|
232
|
+
checker: ts.TypeChecker
|
|
233
|
+
): PermissionMetadata[] | undefined {
|
|
234
|
+
const explicitPermissionsNode = getPermissionsNode(obj)
|
|
235
|
+
return resolveFunctionPermissionsInternal(
|
|
236
|
+
state,
|
|
237
|
+
tags,
|
|
238
|
+
explicitPermissionsNode,
|
|
239
|
+
checker
|
|
240
|
+
)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Convenience wrapper for HTTP: Extract permissions and resolve with HTTP-specific logic
|
|
245
|
+
* Use this in add-http-route.ts for cleaner code
|
|
246
|
+
*/
|
|
247
|
+
export function resolveHTTPPermissionsFromObject(
|
|
248
|
+
state: InspectorState,
|
|
249
|
+
route: string,
|
|
250
|
+
obj: ts.ObjectLiteralExpression,
|
|
251
|
+
tags: string[] | undefined,
|
|
252
|
+
checker: ts.TypeChecker
|
|
253
|
+
): PermissionMetadata[] | undefined {
|
|
254
|
+
const explicitPermissionsNode = getPermissionsNode(obj)
|
|
255
|
+
return resolveHTTPPermissions(
|
|
256
|
+
state,
|
|
257
|
+
route,
|
|
258
|
+
tags,
|
|
259
|
+
explicitPermissionsNode,
|
|
260
|
+
checker
|
|
261
|
+
)
|
|
262
|
+
}
|