@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.
Files changed (84) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/add/add-channel.d.ts +5 -1
  3. package/dist/add/add-channel.js +51 -32
  4. package/dist/add/add-cli.d.ts +4 -0
  5. package/dist/add/add-cli.js +128 -23
  6. package/dist/add/add-file-extends-core-type.js +3 -2
  7. package/dist/add/add-file-with-factory.d.ts +2 -2
  8. package/dist/add/add-file-with-factory.js +34 -1
  9. package/dist/add/add-functions.js +52 -5
  10. package/dist/add/add-http-route.js +19 -12
  11. package/dist/add/add-mcp-prompt.js +20 -13
  12. package/dist/add/add-mcp-resource.js +24 -14
  13. package/dist/add/add-mcp-tool.js +23 -13
  14. package/dist/add/add-middleware.js +51 -12
  15. package/dist/add/add-permission.d.ts +1 -2
  16. package/dist/add/add-permission.js +275 -19
  17. package/dist/add/add-queue-worker.js +10 -12
  18. package/dist/add/add-schedule.js +9 -10
  19. package/dist/error-codes.d.ts +35 -0
  20. package/dist/error-codes.js +40 -0
  21. package/dist/index.d.ts +4 -0
  22. package/dist/index.js +3 -0
  23. package/dist/inspector.js +20 -1
  24. package/dist/types.d.ts +31 -3
  25. package/dist/utils/ensure-function-metadata.d.ts +6 -0
  26. package/dist/utils/ensure-function-metadata.js +18 -0
  27. package/dist/utils/extract-function-name.d.ts +2 -2
  28. package/dist/utils/extract-function-name.js +13 -8
  29. package/dist/utils/filter-inspector-state.d.ts +6 -0
  30. package/dist/utils/filter-inspector-state.js +382 -0
  31. package/dist/utils/filter-utils.d.ts +10 -0
  32. package/dist/utils/filter-utils.js +66 -2
  33. package/dist/utils/find-root-dir.d.ts +23 -0
  34. package/dist/utils/find-root-dir.js +55 -0
  35. package/dist/utils/get-files-and-methods.d.ts +2 -1
  36. package/dist/utils/get-files-and-methods.js +2 -1
  37. package/dist/utils/get-property-value.d.ts +9 -0
  38. package/dist/utils/get-property-value.js +20 -0
  39. package/dist/utils/middleware.d.ts +1 -1
  40. package/dist/utils/middleware.js +7 -7
  41. package/dist/utils/permissions.d.ts +43 -0
  42. package/dist/utils/permissions.js +178 -0
  43. package/dist/utils/post-process.d.ts +16 -0
  44. package/dist/utils/post-process.js +132 -0
  45. package/dist/utils/serialize-inspector-state.d.ts +179 -0
  46. package/dist/utils/serialize-inspector-state.js +170 -0
  47. package/dist/visit.js +3 -2
  48. package/package.json +4 -4
  49. package/src/add/add-channel.ts +92 -40
  50. package/src/add/add-cli.ts +188 -29
  51. package/src/add/add-file-extends-core-type.ts +5 -2
  52. package/src/add/add-file-with-factory.ts +45 -2
  53. package/src/add/add-functions.ts +60 -5
  54. package/src/add/add-http-route.ts +46 -21
  55. package/src/add/add-mcp-prompt.ts +42 -21
  56. package/src/add/add-mcp-prompt.ts.tmp +0 -0
  57. package/src/add/add-mcp-resource.ts +50 -24
  58. package/src/add/add-mcp-resource.ts.tmp +0 -0
  59. package/src/add/add-mcp-tool.ts +48 -21
  60. package/src/add/add-middleware.ts +74 -15
  61. package/src/add/add-permission.ts +364 -22
  62. package/src/add/add-queue-worker.ts +22 -25
  63. package/src/add/add-schedule.ts +19 -20
  64. package/src/error-codes.ts +43 -0
  65. package/src/index.ts +7 -0
  66. package/src/inspector.ts +22 -1
  67. package/src/types.ts +38 -3
  68. package/src/utils/ensure-function-metadata.ts +24 -0
  69. package/src/utils/extract-function-name.ts +20 -8
  70. package/src/utils/filter-inspector-state.test.ts +1433 -0
  71. package/src/utils/filter-inspector-state.ts +526 -0
  72. package/src/utils/filter-utils.test.ts +350 -1
  73. package/src/utils/filter-utils.ts +82 -2
  74. package/src/utils/find-root-dir.ts +68 -0
  75. package/src/utils/get-files-and-methods.ts +8 -0
  76. package/src/utils/get-property-value.ts +27 -0
  77. package/src/utils/middleware.ts +14 -7
  78. package/src/utils/permissions.test.ts +327 -0
  79. package/src/utils/permissions.ts +262 -0
  80. package/src/utils/post-process.ts +178 -0
  81. package/src/utils/serialize-inspector-state.ts +375 -0
  82. package/src/utils/test-data/inspector-state.json +1680 -0
  83. package/src/visit.ts +4 -2
  84. 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
+ }