@pikku/core 0.9.11 → 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 (124) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/errors/errors.d.ts +2 -0
  3. package/dist/errors/errors.js +6 -0
  4. package/dist/function/function-runner.d.ts +15 -9
  5. package/dist/function/function-runner.js +55 -37
  6. package/dist/function/functions.types.d.ts +67 -1
  7. package/dist/function/functions.types.js +43 -1
  8. package/dist/index.d.ts +2 -6
  9. package/dist/index.js +2 -6
  10. package/dist/middleware/auth-apikey.d.ts +19 -11
  11. package/dist/middleware/auth-apikey.js +33 -33
  12. package/dist/middleware/auth-bearer.d.ts +34 -7
  13. package/dist/middleware/auth-bearer.js +53 -36
  14. package/dist/middleware/auth-cookie.d.ts +30 -12
  15. package/dist/middleware/auth-cookie.js +51 -43
  16. package/dist/middleware/timeout.d.ts +6 -2
  17. package/dist/middleware/timeout.js +7 -9
  18. package/dist/middleware-runner.d.ts +46 -43
  19. package/dist/middleware-runner.js +100 -69
  20. package/dist/permissions.d.ts +82 -20
  21. package/dist/permissions.js +176 -62
  22. package/dist/pikku-state.d.ts +46 -1
  23. package/dist/pikku-state.js +16 -1
  24. package/dist/services/content-service.d.ts +2 -3
  25. package/dist/services/index.d.ts +0 -1
  26. package/dist/services/index.js +0 -1
  27. package/dist/services/local-content.d.ts +2 -4
  28. package/dist/types/core.types.d.ts +110 -4
  29. package/dist/types/core.types.js +44 -1
  30. package/dist/utils.d.ts +1 -1
  31. package/dist/wirings/channel/channel-handler.d.ts +2 -2
  32. package/dist/wirings/channel/channel-handler.js +16 -12
  33. package/dist/wirings/channel/channel-runner.d.ts +2 -2
  34. package/dist/wirings/channel/channel-runner.js +6 -24
  35. package/dist/wirings/channel/channel.types.d.ts +15 -14
  36. package/dist/wirings/channel/local/local-channel-runner.js +12 -12
  37. package/dist/wirings/channel/serverless/serverless-channel-runner.js +7 -8
  38. package/dist/wirings/cli/channel/cli-channel-runner.d.ts +12 -0
  39. package/dist/wirings/cli/channel/cli-channel-runner.js +83 -0
  40. package/dist/wirings/cli/channel/index.d.ts +1 -0
  41. package/dist/wirings/cli/channel/index.js +1 -0
  42. package/dist/wirings/cli/cli-runner.d.ts +42 -0
  43. package/dist/wirings/cli/cli-runner.js +352 -0
  44. package/dist/wirings/cli/cli.types.d.ts +204 -0
  45. package/dist/wirings/cli/cli.types.js +1 -0
  46. package/dist/wirings/cli/command-parser.d.ts +19 -0
  47. package/dist/wirings/cli/command-parser.js +403 -0
  48. package/dist/wirings/cli/index.d.ts +5 -0
  49. package/dist/wirings/cli/index.js +5 -0
  50. package/dist/wirings/http/http-runner.d.ts +59 -10
  51. package/dist/wirings/http/http-runner.js +138 -131
  52. package/dist/wirings/http/http.types.d.ts +11 -10
  53. package/dist/wirings/http/index.d.ts +1 -1
  54. package/dist/wirings/http/index.js +1 -1
  55. package/dist/wirings/http/pikku-fetch-http-response.js +3 -1
  56. package/dist/wirings/http/routers/http-router.d.ts +0 -2
  57. package/dist/wirings/http/routers/path-to-regex.d.ts +1 -1
  58. package/dist/wirings/http/routers/path-to-regex.js +5 -34
  59. package/dist/wirings/mcp/mcp-endpoint-registry.d.ts +1 -1
  60. package/dist/wirings/mcp/mcp-runner.d.ts +4 -5
  61. package/dist/wirings/mcp/mcp-runner.js +32 -34
  62. package/dist/wirings/mcp/mcp.types.d.ts +38 -14
  63. package/dist/wirings/queue/queue-runner.d.ts +2 -2
  64. package/dist/wirings/queue/queue-runner.js +25 -27
  65. package/dist/wirings/queue/queue.types.d.ts +6 -5
  66. package/dist/wirings/rpc/index.d.ts +1 -1
  67. package/dist/wirings/rpc/index.js +1 -1
  68. package/dist/wirings/rpc/rpc-runner.d.ts +6 -18
  69. package/dist/wirings/rpc/rpc-runner.js +13 -8
  70. package/dist/wirings/scheduler/scheduler-runner.d.ts +2 -2
  71. package/dist/wirings/scheduler/scheduler-runner.js +38 -35
  72. package/dist/wirings/scheduler/scheduler.types.d.ts +5 -4
  73. package/package.json +9 -7
  74. package/src/errors/errors.ts +6 -0
  75. package/src/factory-functions.test.ts +60 -1
  76. package/src/function/function-runner.test.ts +81 -26
  77. package/src/function/function-runner.ts +89 -57
  78. package/src/function/functions.types.ts +85 -2
  79. package/src/index.ts +6 -19
  80. package/src/middleware/auth-apikey.ts +42 -57
  81. package/src/middleware/auth-bearer.ts +66 -49
  82. package/src/middleware/auth-cookie.ts +63 -82
  83. package/src/middleware/timeout.ts +10 -14
  84. package/src/middleware-runner.test.ts +24 -16
  85. package/src/middleware-runner.ts +115 -87
  86. package/src/permissions.test.ts +26 -27
  87. package/src/permissions.ts +216 -104
  88. package/src/pikku-state.ts +79 -2
  89. package/src/services/content-service.ts +5 -4
  90. package/src/services/index.ts +0 -1
  91. package/src/services/local-content.ts +9 -4
  92. package/src/types/core.types.ts +143 -5
  93. package/src/utils.ts +1 -1
  94. package/src/wirings/channel/channel-handler.ts +18 -21
  95. package/src/wirings/channel/channel-runner.ts +19 -32
  96. package/src/wirings/channel/channel.types.ts +29 -16
  97. package/src/wirings/channel/local/local-channel-runner.ts +25 -15
  98. package/src/wirings/channel/serverless/serverless-channel-runner.ts +15 -17
  99. package/src/wirings/cli/channel/cli-channel-runner.ts +123 -0
  100. package/src/wirings/cli/channel/index.ts +1 -0
  101. package/src/wirings/cli/cli-runner.test.ts +403 -0
  102. package/src/wirings/cli/cli-runner.ts +539 -0
  103. package/src/wirings/cli/cli.types.ts +350 -0
  104. package/src/wirings/cli/command-parser.test.ts +445 -0
  105. package/src/wirings/cli/command-parser.ts +504 -0
  106. package/src/wirings/cli/index.ts +13 -0
  107. package/src/wirings/http/http-runner.test.ts +8 -7
  108. package/src/wirings/http/http-runner.ts +165 -158
  109. package/src/wirings/http/http.types.ts +58 -11
  110. package/src/wirings/http/index.ts +7 -1
  111. package/src/wirings/http/pikku-fetch-http-response.ts +3 -1
  112. package/src/wirings/http/routers/http-router.ts +0 -2
  113. package/src/wirings/http/routers/path-to-regex.test.ts +4 -5
  114. package/src/wirings/http/routers/path-to-regex.ts +6 -43
  115. package/src/wirings/mcp/mcp-endpoint-registry.ts +1 -1
  116. package/src/wirings/mcp/mcp-runner.ts +58 -55
  117. package/src/wirings/mcp/mcp.types.ts +65 -14
  118. package/src/wirings/queue/queue-runner.ts +44 -47
  119. package/src/wirings/queue/queue.types.ts +10 -6
  120. package/src/wirings/rpc/index.ts +1 -1
  121. package/src/wirings/rpc/rpc-runner.ts +27 -9
  122. package/src/wirings/scheduler/scheduler-runner.ts +58 -56
  123. package/src/wirings/scheduler/scheduler.types.ts +10 -3
  124. package/tsconfig.tsbuildinfo +1 -1
@@ -1,4 +1,4 @@
1
- import { CoreServices, CoreUserSession, PikkuWiringTypes } from './types/core.types.js';
1
+ import { CoreServices, CoreUserSession, PikkuWiringTypes, PermissionMetadata } from './types/core.types.js';
2
2
  import { CorePermissionGroup, CorePikkuPermission } from './function/functions.types.js';
3
3
  /**
4
4
  * This function validates permissions by iterating over permission groups and executing the corresponding permission functions. If all functions in at least one group return true, the permission is considered valid.
@@ -8,30 +8,65 @@ import { CorePermissionGroup, CorePikkuPermission } from './function/functions.t
8
8
  * @returns A promise that resolves to void.
9
9
  */
10
10
  export declare const verifyPermissions: (permissions: CorePermissionGroup, services: CoreServices, data: any, session?: CoreUserSession) => Promise<boolean>;
11
+ /**
12
+ * Registers a single permission function by name in the global permission store.
13
+ *
14
+ * This function is used by CLI-generated code to register permission functions
15
+ * that can be referenced by name in metadata. It stores permissions in a special
16
+ * namespace to avoid conflicts with tag-based permissions.
17
+ *
18
+ * @param {string} name - The unique name (pikkuFuncName) of the permission function.
19
+ * @param {CorePikkuPermission} permission - The permission function to register.
20
+ *
21
+ * @example
22
+ * ```typescript
23
+ * // Called by CLI-generated pikku-permissions.gen.ts
24
+ * registerPermission('adminPermission_src_permissions_ts_10_5', adminPermission)
25
+ * registerPermission('readPermission_src_permissions_ts_20_10', readPermission)
26
+ * ```
27
+ */
28
+ export declare const registerPermission: (name: string, permission: CorePikkuPermission<any>) => void;
29
+ /**
30
+ * Retrieves a registered permission function by its name.
31
+ *
32
+ * This function looks up permissions that was registered with registerPermission.
33
+ * It's used internally by the framework to resolve permission references in metadata.
34
+ *
35
+ * @param {string} name - The unique name (pikkuFuncName) of the permission function.
36
+ * @returns {CorePikkuPermission | undefined} The permission function, or undefined if not found.
37
+ *
38
+ * @internal
39
+ */
40
+ export declare const getPermissionByName: (name: string) => CorePikkuPermission | undefined;
11
41
  /**
12
42
  * Adds global permissions for a specific tag.
13
43
  *
14
44
  * This function allows you to register permissions that will be applied to
15
45
  * any wiring (HTTP, Channel, Queue, Scheduler, MCP) that includes the matching tag.
16
46
  *
47
+ * For tree-shaking benefits, wrap in a factory function:
48
+ * `export const x = () => addPermission('tag', [...])`
49
+ *
17
50
  * @param {string} tag - The tag that the permissions should apply to.
18
51
  * @param {any[]} permissions - The permissions array to apply for the specified tag.
19
52
  *
20
- * @throws {Error} If permissions for the tag already exist.
53
+ * @returns {CorePermissionGroup | CorePikkuPermission[]} The permissions (for chaining/wrapping).
21
54
  *
22
55
  * @example
23
56
  * ```typescript
24
- * // Add admin permissions for admin endpoints
25
- * addPermission('admin', [adminPermission])
26
- *
27
- * // Add authentication permissions for auth endpoints
28
- * addPermission('auth', [authPermission])
57
+ * // Recommended: tree-shakeable
58
+ * export const adminPermissions = () => addPermission('admin', [
59
+ * adminPermission,
60
+ * rolePermission({ role: 'admin' })
61
+ * ])
29
62
  *
30
- * // Add read permissions for all API endpoints
31
- * addPermission('api', [readPermission])
63
+ * // Also works: no tree-shaking
64
+ * export const apiPermissions = addPermission('api', [
65
+ * readPermission
66
+ * ])
32
67
  * ```
33
68
  */
34
- export declare const addPermission: (tag: string, permissions: CorePermissionGroup | CorePikkuPermission[]) => void;
69
+ export declare const addPermission: (tag: string, permissions: CorePermissionGroup | CorePikkuPermission[]) => CorePermissionGroup | CorePikkuPermission[];
35
70
  /**
36
71
  * Retrieves permissions for a given set of tags.
37
72
  *
@@ -49,17 +84,44 @@ export declare const addPermission: (tag: string, permissions: CorePermissionGro
49
84
  */
50
85
  export declare const getPermissionsForTags: (tags?: string[]) => readonly (CorePermissionGroup | CorePikkuPermission)[];
51
86
  /**
52
- * Runs permission checks in the specified order:
53
- * 1) wiring tag permissions - at least one must pass if any exist
54
- * 2) wiring permissions - must pass if defined
55
- * 3) function tag permissions - at least one must pass if any exist
56
- * 4) function permissions - must pass if defined
87
+ * Combines wiring-specific permissions with function-level permissions.
88
+ *
89
+ * This function resolves permission metadata into actual permission functions and combines them.
90
+ * It filters out wire permissions without tags from inheritedPermissions to avoid duplication
91
+ * (those are passed separately as wirePermissions).
92
+ *
93
+ * @param {object} options - Configuration object for combining permissions.
94
+ * @param {PermissionMetadata[] | undefined} options.wireInheritedPermissions - Metadata from wiring (HTTP + tags + wire with tags).
95
+ * @param {CorePermissionGroup | CorePikkuPermission[] | undefined} options.wirePermissions - Inline wire permissions.
96
+ * @param {PermissionMetadata[] | undefined} options.funcInheritedPermissions - Function permissions metadata (only tags).
97
+ * @param {CorePermissionGroup | CorePikkuPermission[] | undefined} options.funcPermissions - Inline function permissions.
98
+ * @returns {(CorePermissionGroup | CorePikkuPermission)[]} Combined array of resolved permissions.
99
+ *
100
+ * @example
101
+ * ```typescript
102
+ * const combined = combinePermissions(wireType, wireId, {
103
+ * wireInheritedPermissions: meta.permissions,
104
+ * wirePermissions: inlinePermissions,
105
+ * funcInheritedPermissions: funcMeta.permissions,
106
+ * funcPermissions: funcConfig.permissions
107
+ * })
108
+ * ```
109
+ */
110
+ export declare const combinePermissions: (wireType: PikkuWiringTypes, uid: string, { wireInheritedPermissions, wirePermissions, funcInheritedPermissions, funcPermissions, }?: {
111
+ wireInheritedPermissions?: PermissionMetadata[];
112
+ wirePermissions?: CorePermissionGroup | CorePikkuPermission[];
113
+ funcInheritedPermissions?: PermissionMetadata[];
114
+ funcPermissions?: CorePermissionGroup | CorePikkuPermission[];
115
+ }) => readonly (CorePermissionGroup | CorePikkuPermission)[];
116
+ /**
117
+ * Runs permission checks using combined permissions from all sources.
118
+ * Combines permissions from wire and function levels, then validates them.
57
119
  */
58
- export declare const runPermissions: (wireType: PikkuWiringTypes, uid: string, { wiringTags, wiringPermissions, funcTags, funcPermissions, allServices, data, session, }: {
59
- wiringTags?: string[];
60
- wiringPermissions?: CorePermissionGroup;
61
- funcTags?: string[];
62
- funcPermissions?: CorePermissionGroup;
120
+ export declare const runPermissions: (wireType: PikkuWiringTypes, uid: string, { wireInheritedPermissions, wirePermissions, funcInheritedPermissions, funcPermissions, allServices, data, session, }: {
121
+ wireInheritedPermissions?: PermissionMetadata[];
122
+ wirePermissions?: CorePermissionGroup | CorePikkuPermission[];
123
+ funcInheritedPermissions?: PermissionMetadata[];
124
+ funcPermissions?: CorePermissionGroup | CorePikkuPermission[];
63
125
  allServices: CoreServices;
64
126
  data: any;
65
127
  session?: CoreUserSession;
@@ -1,6 +1,7 @@
1
1
  import { PikkuWiringTypes, } from './types/core.types.js';
2
2
  import { pikkuState } from './pikku-state.js';
3
3
  import { ForbiddenError } from './errors/errors.js';
4
+ import { freezeDedupe } from './utils.js';
4
5
  /**
5
6
  * This function validates permissions by iterating over permission groups and executing the corresponding permission functions. If all functions in at least one group return true, the permission is considered valid.
6
7
  * @param services - The core services required for permission validation.
@@ -33,36 +34,81 @@ export const verifyPermissions = async (permissions, services, data, session) =>
33
34
  }
34
35
  return false;
35
36
  };
37
+ /**
38
+ * Registers a single permission function by name in the global permission store.
39
+ *
40
+ * This function is used by CLI-generated code to register permission functions
41
+ * that can be referenced by name in metadata. It stores permissions in a special
42
+ * namespace to avoid conflicts with tag-based permissions.
43
+ *
44
+ * @param {string} name - The unique name (pikkuFuncName) of the permission function.
45
+ * @param {CorePikkuPermission} permission - The permission function to register.
46
+ *
47
+ * @example
48
+ * ```typescript
49
+ * // Called by CLI-generated pikku-permissions.gen.ts
50
+ * registerPermission('adminPermission_src_permissions_ts_10_5', adminPermission)
51
+ * registerPermission('readPermission_src_permissions_ts_20_10', readPermission)
52
+ * ```
53
+ */
54
+ export const registerPermission = (name, permission) => {
55
+ const permissionStore = pikkuState('misc', 'permissions');
56
+ permissionStore[name] = [permission];
57
+ };
58
+ /**
59
+ * Retrieves a registered permission function by its name.
60
+ *
61
+ * This function looks up permissions that was registered with registerPermission.
62
+ * It's used internally by the framework to resolve permission references in metadata.
63
+ *
64
+ * @param {string} name - The unique name (pikkuFuncName) of the permission function.
65
+ * @returns {CorePikkuPermission | undefined} The permission function, or undefined if not found.
66
+ *
67
+ * @internal
68
+ */
69
+ export const getPermissionByName = (name) => {
70
+ const permissionStore = pikkuState('misc', 'permissions');
71
+ const permission = permissionStore[name];
72
+ if (Array.isArray(permission) && permission.length === 1) {
73
+ return permission[0];
74
+ }
75
+ return undefined;
76
+ };
36
77
  /**
37
78
  * Adds global permissions for a specific tag.
38
79
  *
39
80
  * This function allows you to register permissions that will be applied to
40
81
  * any wiring (HTTP, Channel, Queue, Scheduler, MCP) that includes the matching tag.
41
82
  *
83
+ * For tree-shaking benefits, wrap in a factory function:
84
+ * `export const x = () => addPermission('tag', [...])`
85
+ *
42
86
  * @param {string} tag - The tag that the permissions should apply to.
43
87
  * @param {any[]} permissions - The permissions array to apply for the specified tag.
44
88
  *
45
- * @throws {Error} If permissions for the tag already exist.
89
+ * @returns {CorePermissionGroup | CorePikkuPermission[]} The permissions (for chaining/wrapping).
46
90
  *
47
91
  * @example
48
92
  * ```typescript
49
- * // Add admin permissions for admin endpoints
50
- * addPermission('admin', [adminPermission])
51
- *
52
- * // Add authentication permissions for auth endpoints
53
- * addPermission('auth', [authPermission])
93
+ * // Recommended: tree-shakeable
94
+ * export const adminPermissions = () => addPermission('admin', [
95
+ * adminPermission,
96
+ * rolePermission({ role: 'admin' })
97
+ * ])
54
98
  *
55
- * // Add read permissions for all API endpoints
56
- * addPermission('api', [readPermission])
99
+ * // Also works: no tree-shaking
100
+ * export const apiPermissions = addPermission('api', [
101
+ * readPermission
102
+ * ])
57
103
  * ```
58
104
  */
59
105
  export const addPermission = (tag, permissions) => {
60
- const permissionsStore = pikkuState('misc', 'permissions');
61
- // Check if tag already exists
62
- if (permissionsStore[tag]) {
106
+ const tagGroups = pikkuState('permissions', 'tagGroup');
107
+ if (tagGroups[tag]) {
63
108
  throw new Error(`Permissions for tag '${tag}' already exist. Use a different tag or remove the existing permissions first.`);
64
109
  }
65
- permissionsStore[tag] = permissions;
110
+ tagGroups[tag] = permissions;
111
+ return permissions;
66
112
  };
67
113
  const EMPTY = [];
68
114
  /**
@@ -84,7 +130,7 @@ export const getPermissionsForTags = (tags) => {
84
130
  if (!tags || tags.length === 0) {
85
131
  return EMPTY;
86
132
  }
87
- const permissionsStore = pikkuState('misc', 'permissions');
133
+ const permissionsStore = pikkuState('permissions', 'tagGroup');
88
134
  const applicablePermissions = [];
89
135
  // Collect permissions for all matching tags
90
136
  for (const tag of new Set(tags)) {
@@ -100,76 +146,144 @@ export const getPermissionsForTags = (tags) => {
100
146
  }
101
147
  return applicablePermissions;
102
148
  };
103
- const permissionCache = {
149
+ const combinedPermissionsCache = {
104
150
  [PikkuWiringTypes.http]: {},
105
151
  [PikkuWiringTypes.rpc]: {},
106
152
  [PikkuWiringTypes.channel]: {},
107
153
  [PikkuWiringTypes.queue]: {},
108
154
  [PikkuWiringTypes.scheduler]: {},
109
155
  [PikkuWiringTypes.mcp]: {},
156
+ [PikkuWiringTypes.cli]: {},
110
157
  };
111
158
  /**
112
- * Runs permission checks in the specified order:
113
- * 1) wiring tag permissions - at least one must pass if any exist
114
- * 2) wiring permissions - must pass if defined
115
- * 3) function tag permissions - at least one must pass if any exist
116
- * 4) function permissions - must pass if defined
159
+ * Combines wiring-specific permissions with function-level permissions.
160
+ *
161
+ * This function resolves permission metadata into actual permission functions and combines them.
162
+ * It filters out wire permissions without tags from inheritedPermissions to avoid duplication
163
+ * (those are passed separately as wirePermissions).
164
+ *
165
+ * @param {object} options - Configuration object for combining permissions.
166
+ * @param {PermissionMetadata[] | undefined} options.wireInheritedPermissions - Metadata from wiring (HTTP + tags + wire with tags).
167
+ * @param {CorePermissionGroup | CorePikkuPermission[] | undefined} options.wirePermissions - Inline wire permissions.
168
+ * @param {PermissionMetadata[] | undefined} options.funcInheritedPermissions - Function permissions metadata (only tags).
169
+ * @param {CorePermissionGroup | CorePikkuPermission[] | undefined} options.funcPermissions - Inline function permissions.
170
+ * @returns {(CorePermissionGroup | CorePikkuPermission)[]} Combined array of resolved permissions.
171
+ *
172
+ * @example
173
+ * ```typescript
174
+ * const combined = combinePermissions(wireType, wireId, {
175
+ * wireInheritedPermissions: meta.permissions,
176
+ * wirePermissions: inlinePermissions,
177
+ * funcInheritedPermissions: funcMeta.permissions,
178
+ * funcPermissions: funcConfig.permissions
179
+ * })
180
+ * ```
117
181
  */
118
- export const runPermissions = async (wireType, uid, { wiringTags, wiringPermissions, funcTags, funcPermissions, allServices, data, session, }) => {
119
- let cachedPermission = permissionCache[wireType][uid];
120
- if (!cachedPermission) {
121
- cachedPermission = {
122
- wiringTags: getPermissionsForTags(wiringTags),
123
- funcTags: getPermissionsForTags(funcTags),
124
- };
125
- permissionCache[wireType][uid] = cachedPermission;
182
+ export const combinePermissions = (wireType, uid, { wireInheritedPermissions, wirePermissions, funcInheritedPermissions, funcPermissions, } = {}) => {
183
+ if (combinedPermissionsCache[wireType][uid]) {
184
+ return combinedPermissionsCache[wireType][uid];
126
185
  }
127
- let permissioned = true;
128
- // 1. Wiring tag permissions - at least one must pass if any exist
129
- const wiringTaggedPermissions = cachedPermission.wiringTags;
130
- if (wiringTaggedPermissions.length > 0) {
131
- permissioned = false; // Start false, need at least one to pass
132
- for (const permissions of wiringTaggedPermissions) {
133
- const result = await verifyPermissions(typeof permissions === 'function' ? { permissions } : permissions, allServices, data, session);
134
- if (result) {
135
- permissioned = true;
136
- break; // At least one passed, we're good at this level
186
+ const resolved = [];
187
+ // 1. Resolve wire inherited permissions (HTTP + tag groups + individual wire permissions)
188
+ if (wireInheritedPermissions) {
189
+ for (const meta of wireInheritedPermissions) {
190
+ if (meta.type === 'http') {
191
+ // Look up HTTP permission group from pikkuState
192
+ const group = pikkuState('permissions', 'httpGroup')[meta.route];
193
+ if (group) {
194
+ if (Array.isArray(group)) {
195
+ resolved.push(...group);
196
+ }
197
+ else {
198
+ resolved.push(group);
199
+ }
200
+ }
201
+ }
202
+ else if (meta.type === 'tag') {
203
+ // Look up tag permission group from pikkuState
204
+ const group = pikkuState('permissions', 'tagGroup')[meta.tag];
205
+ if (group) {
206
+ if (Array.isArray(group)) {
207
+ resolved.push(...group);
208
+ }
209
+ else {
210
+ resolved.push(group);
211
+ }
212
+ }
213
+ }
214
+ else if (meta.type === 'wire') {
215
+ // Individual wire permission (exported, not inline)
216
+ const permission = getPermissionByName(meta.name);
217
+ if (permission) {
218
+ resolved.push(permission);
219
+ }
137
220
  }
138
221
  }
139
- if (!permissioned) {
140
- allServices.logger.debug('Permission denied - wiring tag permissions');
141
- throw new ForbiddenError('Permission denied');
222
+ }
223
+ // 2. Add inline wire permissions
224
+ if (wirePermissions) {
225
+ if (Array.isArray(wirePermissions)) {
226
+ resolved.push(...wirePermissions);
227
+ }
228
+ else {
229
+ resolved.push(wirePermissions);
142
230
  }
143
231
  }
144
- // 2. Wiring permissions - must pass if defined
145
- if (wiringPermissions) {
146
- permissioned = await verifyPermissions(wiringPermissions, allServices, data, session);
147
- if (!permissioned) {
148
- allServices.logger.debug('Permission denied - wiring permissions');
149
- throw new ForbiddenError('Permission denied');
232
+ // 3. Resolve function inherited permissions (only tags, wire permissions already handled)
233
+ if (funcInheritedPermissions) {
234
+ for (const meta of funcInheritedPermissions) {
235
+ if (meta.type === 'tag') {
236
+ // Look up tag permission group from pikkuState
237
+ const group = pikkuState('permissions', 'tagGroup')[meta.tag];
238
+ if (group) {
239
+ if (Array.isArray(group)) {
240
+ resolved.push(...group);
241
+ }
242
+ else {
243
+ resolved.push(group);
244
+ }
245
+ }
246
+ }
247
+ // Note: wire permissions are already handled in wireInheritedPermissions
248
+ }
249
+ }
250
+ // 4. Add inline function permissions
251
+ if (funcPermissions) {
252
+ if (Array.isArray(funcPermissions)) {
253
+ resolved.push(...funcPermissions);
254
+ }
255
+ else {
256
+ resolved.push(funcPermissions);
150
257
  }
151
258
  }
152
- // 3. Function tag permissions - at least one must pass if any exist
153
- const funcTaggedPermissions = cachedPermission.funcTags;
154
- if (funcTaggedPermissions.length > 0) {
155
- permissioned = false; // Start false, need at least one to pass
156
- for (const permissions of funcTaggedPermissions) {
157
- const result = await verifyPermissions(typeof permissions === 'function' ? { permissions } : permissions, allServices, data, session);
259
+ // Deduplicate and freeze
260
+ combinedPermissionsCache[wireType][uid] = freezeDedupe(resolved);
261
+ return combinedPermissionsCache[wireType][uid];
262
+ };
263
+ /**
264
+ * Runs permission checks using combined permissions from all sources.
265
+ * Combines permissions from wire and function levels, then validates them.
266
+ */
267
+ export const runPermissions = async (wireType, uid, { wireInheritedPermissions, wirePermissions, funcInheritedPermissions, funcPermissions, allServices, data, session, }) => {
268
+ // Combine all permissions: wireInheritedPermissions → wirePermissions → funcInheritedPermissions → funcPermissions
269
+ const allPermissions = combinePermissions(wireType, uid, {
270
+ wireInheritedPermissions,
271
+ wirePermissions,
272
+ funcInheritedPermissions,
273
+ funcPermissions,
274
+ });
275
+ // Check all combined permissions - at least one must pass if any exist
276
+ if (allPermissions.length > 0) {
277
+ let permissioned = false;
278
+ for (const permission of allPermissions) {
279
+ const result = await verifyPermissions(typeof permission === 'function' ? { permission } : permission, allServices, data, session);
158
280
  if (result) {
159
281
  permissioned = true;
160
- break; // At least one passed, we're good at this level
282
+ // Continue executing all permissions (don't break early)
161
283
  }
162
284
  }
163
285
  if (!permissioned) {
164
- allServices.logger.debug('Permission denied - function tag permissions');
165
- throw new ForbiddenError('Permission denied');
166
- }
167
- }
168
- // 4. Function permissions - must pass if defined
169
- if (funcPermissions) {
170
- permissioned = await verifyPermissions(funcPermissions, allServices, data, session);
171
- if (!permissioned) {
172
- allServices.logger.debug('Permission denied - function permissions');
286
+ allServices.logger.debug('Permission denied - combined permissions');
173
287
  throw new ForbiddenError('Permission denied');
174
288
  }
175
289
  }
@@ -1,11 +1,12 @@
1
1
  import { ChannelsMeta, CoreChannel } from './wirings/channel/channel.types.js';
2
2
  import { CoreHTTPFunctionWiring, HTTPMethod, HTTPWiringsMeta } from './wirings/http/http.types.js';
3
- import { FunctionsMeta, CorePikkuMiddleware } from './types/core.types.js';
3
+ import { FunctionsMeta, CorePikkuMiddleware, CorePikkuMiddlewareGroup, FunctionServicesMeta } from './types/core.types.js';
4
4
  import { CoreScheduledTask, ScheduledTasksMeta } from './wirings/scheduler/scheduler.types.js';
5
5
  import { ErrorDetails, PikkuError } from './errors/error-handler.js';
6
6
  import { CorePermissionGroup, CorePikkuFunctionConfig, CorePikkuPermission } from './function/functions.types.js';
7
7
  import { queueWorkersMeta, CoreQueueWorker } from './wirings/queue/queue.types.js';
8
8
  import { CoreMCPResource, CoreMCPTool, CoreMCPPrompt, MCPResourceMeta, MCPToolMeta, MCPPromptMeta } from './wirings/mcp/mcp.types.js';
9
+ import { CLIMeta, CLIProgramState } from './wirings/cli/cli.types.js';
9
10
  interface PikkuState {
10
11
  function: {
11
12
  meta: FunctionsMeta;
@@ -44,6 +45,50 @@ interface PikkuState {
44
45
  prompts: Map<string, CoreMCPPrompt>;
45
46
  promptsMeta: MCPPromptMeta;
46
47
  };
48
+ cli: {
49
+ meta: CLIMeta | Record<string, any>;
50
+ programs: Record<string, CLIProgramState>;
51
+ };
52
+ middleware: {
53
+ tagGroup: Record<string, CorePikkuMiddlewareGroup>;
54
+ httpGroup: Record<string, CorePikkuMiddlewareGroup>;
55
+ tagGroupMeta: Record<string, {
56
+ exportName: string | null;
57
+ sourceFile: string;
58
+ position: number;
59
+ services: FunctionServicesMeta;
60
+ middlewareCount: number;
61
+ isFactory: boolean;
62
+ }>;
63
+ httpGroupMeta: Record<string, {
64
+ exportName: string | null;
65
+ sourceFile: string;
66
+ position: number;
67
+ services: FunctionServicesMeta;
68
+ middlewareCount: number;
69
+ isFactory: boolean;
70
+ }>;
71
+ };
72
+ permissions: {
73
+ tagGroup: Record<string, CorePermissionGroup | CorePikkuPermission[]>;
74
+ httpGroup: Record<string, CorePermissionGroup | CorePikkuPermission[]>;
75
+ tagGroupMeta: Record<string, {
76
+ exportName: string | null;
77
+ sourceFile: string;
78
+ position: number;
79
+ services: FunctionServicesMeta;
80
+ permissionCount: number;
81
+ isFactory: boolean;
82
+ }>;
83
+ httpGroupMeta: Record<string, {
84
+ exportName: string | null;
85
+ sourceFile: string;
86
+ position: number;
87
+ services: FunctionServicesMeta;
88
+ permissionCount: number;
89
+ isFactory: boolean;
90
+ }>;
91
+ };
47
92
  misc: {
48
93
  errors: Map<PikkuError, ErrorDetails>;
49
94
  schemas: Map<string, any>;
@@ -10,7 +10,6 @@ export const resetPikkuState = () => {
10
10
  },
11
11
  http: {
12
12
  permissions: new Map(),
13
- middleware: new Map(),
14
13
  routes: new Map(),
15
14
  meta: {},
16
15
  },
@@ -34,6 +33,22 @@ export const resetPikkuState = () => {
34
33
  prompts: new Map(),
35
34
  promptsMeta: {},
36
35
  },
36
+ cli: {
37
+ meta: { programs: {}, renderers: {} },
38
+ programs: {},
39
+ },
40
+ middleware: {
41
+ tagGroup: {},
42
+ httpGroup: {},
43
+ tagGroupMeta: {},
44
+ httpGroupMeta: {},
45
+ },
46
+ permissions: {
47
+ tagGroup: {},
48
+ httpGroup: {},
49
+ tagGroupMeta: {},
50
+ httpGroupMeta: {},
51
+ },
37
52
  misc: {
38
53
  errors: globalThis.pikkuState?.misc?.errors || new Map(),
39
54
  schemas: new Map(),
@@ -1,4 +1,3 @@
1
- import { ReadStream } from 'fs';
2
1
  export interface ContentService {
3
2
  /**
4
3
  * Signs a content key to generate a secure, time-limited access URL.
@@ -36,7 +35,7 @@ export interface ContentService {
36
35
  * @param stream - A readable stream of the file contents.
37
36
  * @returns A boolean indicating success.
38
37
  */
39
- writeFile(assetKey: string, stream: ReadStream): Promise<boolean>;
38
+ writeFile(assetKey: string, stream: ReadableStream | NodeJS.ReadableStream): Promise<boolean>;
40
39
  /**
41
40
  * Copies a file from a local absolute path into storage under a new asset key.
42
41
  * @param assetKey - The destination key.
@@ -49,5 +48,5 @@ export interface ContentService {
49
48
  * @param assetKey - The key of the file to read.
50
49
  * @returns A readable file stream.
51
50
  */
52
- readFile(assetKey: string): Promise<ReadStream>;
51
+ readFile(assetKey: string): Promise<ReadableStream | NodeJS.ReadableStream>;
53
52
  }
@@ -11,4 +11,3 @@ export * from './user-session-service.js';
11
11
  export * from './local-secrets.js';
12
12
  export * from './local-variables.js';
13
13
  export * from './logger-console.js';
14
- export * from './local-content.js';
@@ -13,4 +13,3 @@ export * from './user-session-service.js';
13
13
  export * from './local-secrets.js';
14
14
  export * from './local-variables.js';
15
15
  export * from './logger-console.js';
16
- export * from './local-content.js';
@@ -1,6 +1,4 @@
1
- import { ReadStream } from 'fs';
2
1
  import { ContentService, Logger } from '@pikku/core/services';
3
- import { Readable } from 'stream';
4
2
  export interface LocalContentConfig {
5
3
  localFileUploadPath: string;
6
4
  uploadUrlPrefix: string;
@@ -19,9 +17,9 @@ export declare class LocalContent implements ContentService {
19
17
  uploadUrl: string;
20
18
  assetKey: string;
21
19
  }>;
22
- writeFile(assetKey: string, stream: Readable): Promise<boolean>;
20
+ writeFile(assetKey: string, stream: ReadableStream | NodeJS.ReadableStream): Promise<boolean>;
23
21
  copyFile(assetKey: string, fromAbsolutePath: string): Promise<boolean>;
24
- readFile(assetKey: string): Promise<ReadStream>;
22
+ readFile(assetKey: string): Promise<ReadableStream | NodeJS.ReadableStream>;
25
23
  deleteFile(assetKey: string): Promise<boolean>;
26
24
  private createDirectoryForFile;
27
25
  }