@pikku/core 0.12.14 → 0.12.16

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 (136) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/dist/errors/errors.d.ts +4 -0
  3. package/dist/errors/errors.js +12 -0
  4. package/dist/function/function-runner.js +2 -1
  5. package/dist/function/functions.types.js +1 -0
  6. package/dist/function/index.d.ts +2 -1
  7. package/dist/function/index.js +1 -0
  8. package/dist/handle-error.d.ts +2 -2
  9. package/dist/handle-error.js +8 -8
  10. package/dist/index.d.ts +2 -3
  11. package/dist/index.js +1 -2
  12. package/dist/middleware/index.d.ts +3 -0
  13. package/dist/middleware/index.js +3 -0
  14. package/dist/middleware/remote-auth.js +5 -2
  15. package/dist/middleware/telemetry.d.ts +47 -0
  16. package/dist/middleware/telemetry.js +106 -0
  17. package/dist/middleware-runner.js +31 -4
  18. package/dist/permissions.d.ts +13 -1
  19. package/dist/permissions.js +51 -0
  20. package/dist/pikku-state.d.ts +0 -1
  21. package/dist/pikku-state.js +1 -4
  22. package/dist/remote.d.ts +10 -0
  23. package/dist/remote.js +32 -0
  24. package/dist/schema.js +2 -0
  25. package/dist/services/deployment-service.d.ts +12 -1
  26. package/dist/services/in-memory-queue-service.d.ts +7 -0
  27. package/dist/services/in-memory-queue-service.js +28 -0
  28. package/dist/services/index.d.ts +4 -1
  29. package/dist/services/index.js +2 -1
  30. package/dist/services/logger-console.d.ts +26 -40
  31. package/dist/services/logger-console.js +102 -46
  32. package/dist/services/logger.d.ts +5 -0
  33. package/dist/services/meta-service.d.ts +175 -0
  34. package/dist/services/meta-service.js +310 -0
  35. package/dist/services/workflow-service.d.ts +6 -1
  36. package/dist/testing/service-tests.js +0 -8
  37. package/dist/types/core.types.d.ts +21 -0
  38. package/dist/types/core.types.js +7 -1
  39. package/dist/types/state.types.d.ts +2 -2
  40. package/dist/wirings/ai-agent/ai-agent-prepare.js +42 -21
  41. package/dist/wirings/ai-agent/ai-agent-registry.js +2 -2
  42. package/dist/wirings/ai-agent/ai-agent-runner.js +18 -1
  43. package/dist/wirings/ai-agent/ai-agent-stream.js +1 -0
  44. package/dist/wirings/ai-agent/ai-agent.types.d.ts +5 -3
  45. package/dist/wirings/channel/channel-runner.js +3 -3
  46. package/dist/wirings/cli/cli-runner.js +5 -3
  47. package/dist/wirings/cli/command-parser.js +7 -3
  48. package/dist/wirings/http/http-runner.d.ts +1 -1
  49. package/dist/wirings/http/http-runner.js +23 -11
  50. package/dist/wirings/http/http.types.d.ts +3 -0
  51. package/dist/wirings/http/pikku-fetch-http-response.d.ts +1 -0
  52. package/dist/wirings/http/pikku-fetch-http-response.js +3 -0
  53. package/dist/wirings/mcp/mcp-runner.js +5 -3
  54. package/dist/wirings/mcp/mcp.types.d.ts +1 -1
  55. package/dist/wirings/queue/queue-runner.d.ts +3 -1
  56. package/dist/wirings/queue/queue-runner.js +7 -3
  57. package/dist/wirings/rpc/rpc-runner.js +56 -90
  58. package/dist/wirings/scheduler/scheduler-runner.d.ts +3 -1
  59. package/dist/wirings/scheduler/scheduler-runner.js +9 -5
  60. package/dist/wirings/trigger/trigger-runner.js +4 -2
  61. package/dist/wirings/workflow/dsl/workflow-runner.d.ts +1 -1
  62. package/dist/wirings/workflow/dsl/workflow-runner.js +6 -9
  63. package/dist/wirings/workflow/graph/graph-runner.d.ts +1 -1
  64. package/dist/wirings/workflow/graph/graph-runner.js +18 -4
  65. package/dist/wirings/workflow/index.d.ts +4 -2
  66. package/dist/wirings/workflow/index.js +4 -2
  67. package/dist/wirings/workflow/pikku-workflow-service.d.ts +16 -4
  68. package/dist/wirings/workflow/pikku-workflow-service.js +216 -24
  69. package/dist/wirings/workflow/workflow-queue-workers.d.ts +40 -0
  70. package/dist/wirings/workflow/workflow-queue-workers.js +41 -0
  71. package/dist/wirings/workflow/workflow.types.d.ts +17 -1
  72. package/package.json +4 -1
  73. package/src/errors/errors.ts +12 -0
  74. package/src/function/function-runner.ts +5 -4
  75. package/src/function/functions.types.ts +1 -0
  76. package/src/function/index.ts +10 -0
  77. package/src/handle-error.test.ts +2 -2
  78. package/src/handle-error.ts +8 -8
  79. package/src/index.ts +2 -2
  80. package/src/middleware/index.ts +3 -0
  81. package/src/middleware/remote-auth.test.ts +4 -4
  82. package/src/middleware/remote-auth.ts +7 -2
  83. package/src/middleware/telemetry.ts +110 -0
  84. package/src/middleware-runner.test.ts +149 -1
  85. package/src/middleware-runner.ts +48 -4
  86. package/src/permissions.ts +70 -0
  87. package/src/pikku-state.test.ts +0 -26
  88. package/src/pikku-state.ts +1 -5
  89. package/src/remote.ts +46 -0
  90. package/src/schema.ts +1 -0
  91. package/src/services/deployment-service.ts +17 -1
  92. package/src/services/in-memory-queue-service.ts +46 -0
  93. package/src/services/index.ts +21 -1
  94. package/src/services/logger-console.ts +127 -47
  95. package/src/services/logger.ts +6 -0
  96. package/src/services/meta-service.ts +523 -0
  97. package/src/services/workflow-service.ts +8 -0
  98. package/src/testing/service-tests.ts +0 -10
  99. package/src/types/core.types.ts +36 -1
  100. package/src/types/state.types.ts +2 -2
  101. package/src/wirings/ai-agent/ai-agent-prepare.ts +51 -40
  102. package/src/wirings/ai-agent/ai-agent-registry.ts +3 -3
  103. package/src/wirings/ai-agent/ai-agent-runner.ts +16 -1
  104. package/src/wirings/ai-agent/ai-agent-stream.ts +8 -0
  105. package/src/wirings/ai-agent/ai-agent.types.ts +5 -3
  106. package/src/wirings/channel/channel-runner.ts +4 -5
  107. package/src/wirings/channel/local/local-channel-runner.test.ts +5 -1
  108. package/src/wirings/cli/cli-runner.test.ts +8 -7
  109. package/src/wirings/cli/cli-runner.ts +6 -4
  110. package/src/wirings/cli/command-parser.ts +8 -3
  111. package/src/wirings/http/http-runner.ts +27 -11
  112. package/src/wirings/http/http.types.ts +3 -0
  113. package/src/wirings/http/pikku-fetch-http-response.ts +4 -0
  114. package/src/wirings/mcp/mcp-runner.ts +7 -9
  115. package/src/wirings/mcp/mcp.types.ts +1 -1
  116. package/src/wirings/queue/queue-runner.test.ts +5 -11
  117. package/src/wirings/queue/queue-runner.ts +11 -3
  118. package/src/wirings/rpc/rpc-runner.ts +82 -115
  119. package/src/wirings/scheduler/scheduler-runner.test.ts +5 -11
  120. package/src/wirings/scheduler/scheduler-runner.ts +13 -5
  121. package/src/wirings/trigger/trigger-runner.test.ts +10 -11
  122. package/src/wirings/trigger/trigger-runner.ts +6 -4
  123. package/src/wirings/workflow/dsl/workflow-runner.ts +11 -10
  124. package/src/wirings/workflow/graph/graph-runner.ts +19 -4
  125. package/src/wirings/workflow/index.ts +17 -6
  126. package/src/wirings/workflow/pikku-workflow-service.ts +284 -24
  127. package/src/wirings/workflow/workflow-queue-workers.ts +85 -0
  128. package/src/wirings/workflow/workflow.types.ts +16 -1
  129. package/tsconfig.tsbuildinfo +1 -1
  130. package/dist/wirings/ai-agent/agent-dynamic-workflow.d.ts +0 -6
  131. package/dist/wirings/ai-agent/agent-dynamic-workflow.js +0 -468
  132. package/dist/wirings/workflow/workflow-helpers.d.ts +0 -36
  133. package/dist/wirings/workflow/workflow-helpers.js +0 -38
  134. package/src/wirings/ai-agent/agent-dynamic-workflow.ts +0 -610
  135. package/src/wirings/workflow/workflow-helpers.test.ts +0 -129
  136. package/src/wirings/workflow/workflow-helpers.ts +0 -79
@@ -257,6 +257,7 @@ export const pikkuAuth = <
257
257
  if (!session) return false
258
258
  return fn(services, session as Session)
259
259
  }
260
+ ;(wrapper as any).__pikkuAuth = true
260
261
  return wrapper as any
261
262
  }
262
263
 
@@ -1,5 +1,15 @@
1
1
  export { addFunction, getAllFunctionNames } from './function-runner.js'
2
+ export {
3
+ pikkuAuth,
4
+ pikkuPermission,
5
+ pikkuPermissionFactory,
6
+ pikkuApprovalDescription,
7
+ } from './functions.types.js'
2
8
  export type {
3
9
  CorePikkuFunction,
4
10
  CorePikkuFunctionSessionless,
11
+ CorePikkuFunctionConfig,
12
+ CorePikkuAuth,
13
+ CorePikkuAuthConfig,
14
+ CorePikkuPermission,
5
15
  } from './functions.types.js'
@@ -91,7 +91,7 @@ describe('handleHTTPError', () => {
91
91
  assert.deepStrictEqual(http._state.jsonBody, {
92
92
  message: 'The server cannot find the requested resource.',
93
93
  payload: undefined,
94
- traceId: 'tracker-1',
94
+ errorId: 'tracker-1',
95
95
  })
96
96
  })
97
97
 
@@ -112,7 +112,7 @@ describe('handleHTTPError', () => {
112
112
 
113
113
  assert.strictEqual(http._state.statusCode, 400)
114
114
  assert.ok(http._state.jsonBody.message.includes('client error'))
115
- assert.strictEqual(http._state.jsonBody.traceId, 'tracker-2')
115
+ assert.strictEqual(http._state.jsonBody.errorId, 'tracker-2')
116
116
  })
117
117
 
118
118
  test('should set status 403 for ForbiddenError', () => {
@@ -9,7 +9,7 @@ import type { PikkuHTTP } from './wirings/http/http.types.js'
9
9
  *
10
10
  * @param {any} e - The error that occurred
11
11
  * @param {PikkuHTTP | undefined} http - HTTP wire object
12
- * @param {string} trackerId - Unique ID for tracking this error
12
+ * @param {string} traceId - Unique ID for tracking this error
13
13
  * @param {Logger} logger - Logger service
14
14
  * @param {number[]} logWarningsForStatusCodes - HTTP status codes to log as warnings
15
15
  * @param {boolean} respondWith404 - Whether to respond with 404 for NotFoundError
@@ -18,7 +18,7 @@ import type { PikkuHTTP } from './wirings/http/http.types.js'
18
18
  export const handleHTTPError = (
19
19
  e: any,
20
20
  http: PikkuHTTP | undefined,
21
- trackerId: string | undefined,
21
+ traceId: string | undefined,
22
22
  logger: Logger,
23
23
  logWarningsForStatusCodes: number[],
24
24
  respondWith404: boolean,
@@ -38,13 +38,13 @@ export const handleHTTPError = (
38
38
  http?.response?.json({
39
39
  message: errorResponse.message,
40
40
  payload: (e as any).payload,
41
- traceId: trackerId,
41
+ errorId: traceId,
42
42
  })
43
43
 
44
44
  // Log certain status codes as warnings
45
45
  if (logWarningsForStatusCodes.includes(errorResponse.status)) {
46
- if (trackerId) {
47
- logger.warn(`Warning id: ${trackerId}`)
46
+ if (traceId) {
47
+ logger.warn(`Warning id: ${traceId}`)
48
48
  }
49
49
  logger.warn(e instanceof Error ? e.message : e)
50
50
  }
@@ -53,9 +53,9 @@ export const handleHTTPError = (
53
53
  logger.error(e instanceof Error ? e.message : e)
54
54
  http?.response?.status(500)
55
55
 
56
- if (trackerId) {
57
- logger.warn(`Error id: ${trackerId}`)
58
- const errorBody: Record<string, unknown> = { errorId: trackerId }
56
+ if (traceId) {
57
+ logger.warn(`Error id: ${traceId}`)
58
+ const errorBody: Record<string, unknown> = { errorId: traceId }
59
59
  if (exposeErrors && !isProduction() && e instanceof Error) {
60
60
  errorBody.message = e.message
61
61
  errorBody.stack = e.stack
package/src/index.ts CHANGED
@@ -22,6 +22,7 @@ export type {
22
22
  JSONValue,
23
23
  MakeRequired,
24
24
  MiddlewareMetadata,
25
+ MiddlewarePriority,
25
26
  PermissionMetadata,
26
27
  PickOptional,
27
28
  PickRequired,
@@ -103,12 +104,11 @@ export type { AIStorageService } from './services/ai-storage-service.js'
103
104
  export type { HTTPMethod } from './wirings/http/http.types.js'
104
105
  export type { GraphNodeConfig } from './wirings/workflow/graph/workflow-graph.types.js'
105
106
  export { createGraph } from './wirings/workflow/graph/graph-node.js'
106
- export { workflow as wireWorkflow } from './wirings/workflow/workflow-helpers.js'
107
107
  export { wireAddon } from './wirings/rpc/wire-addon.js'
108
108
  export type { WireAddonConfig } from './wirings/rpc/wire-addon.js'
109
109
  export type { PikkuPackageState } from './types/state.types.js'
110
110
  export { runMiddleware, addMiddleware } from './middleware-runner.js'
111
- export { addPermission } from './permissions.js'
111
+ export { addPermission, checkAuthPermissions } from './permissions.js'
112
112
  export { isSerializable, stopSingletonServices } from './utils.js'
113
113
  export { getSingletonServices, getCreateWireServices } from './pikku-state.js'
114
114
  export {
@@ -3,3 +3,6 @@ export { authCookie } from './auth-cookie.js'
3
3
  export { authBearer } from './auth-bearer.js'
4
4
  export { pikkuRemoteAuthMiddleware } from './remote-auth.js'
5
5
  export { cors } from './cors.js'
6
+ export { telemetryOuter, telemetryInner } from './telemetry.js'
7
+ export { addMiddleware, runMiddleware } from '../middleware-runner.js'
8
+ export { addPermission } from '../permissions.js'
@@ -252,7 +252,7 @@ describe('pikkuRemoteAuthMiddleware', () => {
252
252
  http: {
253
253
  request: createMockRequest(
254
254
  { authorization: 'Bearer valid-token' },
255
- '/rpc/otherFunc'
255
+ '/remote/rpc/otherFunc'
256
256
  ),
257
257
  },
258
258
  } as any,
@@ -273,7 +273,7 @@ describe('pikkuRemoteAuthMiddleware', () => {
273
273
  http: {
274
274
  request: createMockRequest(
275
275
  { authorization: 'Bearer valid-token' },
276
- '/rpc/myFunc'
276
+ '/remote/rpc/myFunc'
277
277
  ),
278
278
  },
279
279
  } as any,
@@ -295,7 +295,7 @@ describe('pikkuRemoteAuthMiddleware', () => {
295
295
  http: {
296
296
  request: createMockRequest(
297
297
  { authorization: 'Bearer valid-token' },
298
- '/rpc/anyFunc'
298
+ '/remote/rpc/anyFunc'
299
299
  ),
300
300
  },
301
301
  } as any,
@@ -339,7 +339,7 @@ describe('pikkuRemoteAuthMiddleware', () => {
339
339
  http: {
340
340
  request: createMockRequest(
341
341
  { authorization: 'Bearer valid-token' },
342
- '/rpc/my%20func'
342
+ '/remote/rpc/my%20func'
343
343
  ),
344
344
  },
345
345
  } as any,
@@ -12,6 +12,9 @@ export const pikkuRemoteAuthMiddleware = pikkuMiddleware(
12
12
  try {
13
13
  secret = await secrets.getSecret('PIKKU_REMOTE_SECRET')
14
14
  } catch {
15
+ if (http.request.path().startsWith('/remote/rpc/')) {
16
+ throw new UnauthorizedError()
17
+ }
15
18
  return next()
16
19
  }
17
20
  if (!jwt) {
@@ -42,8 +45,10 @@ export const pikkuRemoteAuthMiddleware = pikkuMiddleware(
42
45
  throw new UnauthorizedError()
43
46
  }
44
47
 
45
- if (payload?.fn && http.request.path().startsWith('/rpc/')) {
46
- const fn = decodeURIComponent(http.request.path().slice('/rpc/'.length))
48
+ if (payload?.fn && http.request.path().startsWith('/remote/rpc/')) {
49
+ const fn = decodeURIComponent(
50
+ http.request.path().slice('/remote/rpc/'.length)
51
+ )
47
52
  if (fn && payload.fn !== fn) {
48
53
  throw new UnauthorizedError()
49
54
  }
@@ -0,0 +1,110 @@
1
+ import { pikkuMiddleware, pikkuMiddlewareFactory } from '../types/core.types.js'
2
+
3
+ /**
4
+ * Outer telemetry middleware that captures total request duration and outcome.
5
+ * Runs at `highest` priority (outermost in the middleware chain).
6
+ *
7
+ * Emits a structured JSON log entry with `__pikku_telemetry: 'end'` containing:
8
+ * - Total duration (including all middleware)
9
+ * - Outcome (ok/error)
10
+ * - Wire metadata (wireType, wireId, traceId)
11
+ * - HTTP details (method, path, status) if applicable
12
+ *
13
+ * @example
14
+ * ```typescript
15
+ * import { telemetryOuter } from '@pikku/core/middleware'
16
+ * addMiddleware('myTag', [telemetryOuter()])
17
+ * ```
18
+ */
19
+ export const telemetryOuter = pikkuMiddlewareFactory<{
20
+ environmentId?: string
21
+ orgId?: string
22
+ } | void>(({ environmentId, orgId } = {}) => {
23
+ return pikkuMiddleware({
24
+ name: 'telemetry-outer',
25
+ priority: 'highest',
26
+ func: async (services, wire, next) => {
27
+ const start = performance.now()
28
+ let outcome = 'ok'
29
+ let errorMessage: string | undefined
30
+ try {
31
+ await next()
32
+ } catch (e) {
33
+ outcome = 'error'
34
+ errorMessage = e instanceof Error ? e.message : String(e)
35
+ throw e
36
+ } finally {
37
+ services.logger.info({
38
+ __pikku_telemetry: 'end',
39
+ __pikku_layer: 'outer',
40
+ traceId: wire.traceId,
41
+ wireType: wire.wireType,
42
+ wireId: wire.wireId,
43
+ totalDuration: Math.round(performance.now() - start),
44
+ outcome,
45
+ ...(errorMessage ? { errorMessage } : {}),
46
+ ...(wire.http
47
+ ? {
48
+ httpStatus: wire.http.response?.statusCode,
49
+ httpMethod: wire.http.request?.method(),
50
+ httpPath: wire.http.request?.path(),
51
+ }
52
+ : {}),
53
+ ...(environmentId ? { environmentId } : {}),
54
+ ...(orgId ? { orgId } : {}),
55
+ })
56
+ }
57
+ },
58
+ })
59
+ })
60
+
61
+ /**
62
+ * Inner telemetry middleware that captures function execution duration and user context.
63
+ * Runs at `lowest` priority (innermost, closest to the function).
64
+ *
65
+ * Emits a structured JSON log entry with `__pikku_telemetry: 'end'` containing:
66
+ * - Function-only duration (excluding outer middleware like auth)
67
+ * - User identity (pikkuUserId) if authenticated
68
+ * - Wire metadata (wireType, wireId, traceId)
69
+ *
70
+ * @example
71
+ * ```typescript
72
+ * import { telemetryInner } from '@pikku/core/middleware'
73
+ * addMiddleware('myTag', [telemetryInner()])
74
+ * ```
75
+ */
76
+ export const telemetryInner = pikkuMiddlewareFactory<{
77
+ environmentId?: string
78
+ orgId?: string
79
+ } | void>(({ environmentId, orgId } = {}) => {
80
+ return pikkuMiddleware({
81
+ name: 'telemetry-inner',
82
+ priority: 'lowest',
83
+ func: async (services, wire, next) => {
84
+ const start = performance.now()
85
+ let outcome = 'ok'
86
+ let errorMessage: string | undefined
87
+ try {
88
+ await next()
89
+ } catch (e) {
90
+ outcome = 'error'
91
+ errorMessage = e instanceof Error ? e.message : String(e)
92
+ throw e
93
+ } finally {
94
+ services.logger.info({
95
+ __pikku_telemetry: 'end',
96
+ __pikku_layer: 'inner',
97
+ traceId: wire.traceId,
98
+ wireType: wire.wireType,
99
+ wireId: wire.wireId,
100
+ functionDuration: Math.round(performance.now() - start),
101
+ outcome,
102
+ pikkuUserId: wire.pikkuUserId,
103
+ ...(errorMessage ? { errorMessage } : {}),
104
+ ...(environmentId ? { environmentId } : {}),
105
+ ...(orgId ? { orgId } : {}),
106
+ })
107
+ }
108
+ },
109
+ })
110
+ })
@@ -6,7 +6,27 @@ import {
6
6
  runMiddleware,
7
7
  } from './middleware-runner.js'
8
8
  import { resetPikkuState } from './pikku-state.js'
9
- import type { CorePikkuMiddleware } from './types/core.types.js'
9
+ import type {
10
+ CorePikkuMiddleware,
11
+ MiddlewarePriority,
12
+ } from './types/core.types.js'
13
+ import { pikkuMiddleware } from './types/core.types.js'
14
+
15
+ const withPriority = (
16
+ name: string,
17
+ priority: MiddlewarePriority,
18
+ log: string[]
19
+ ): CorePikkuMiddleware => {
20
+ return pikkuMiddleware({
21
+ name,
22
+ priority,
23
+ func: async (_services, _wire, next) => {
24
+ log.push(`start:${name}`)
25
+ await next()
26
+ log.push(`end:${name}`)
27
+ },
28
+ })
29
+ }
10
30
 
11
31
  beforeEach(() => {
12
32
  resetPikkuState()
@@ -329,6 +349,56 @@ describe('combineMiddleware', () => {
329
349
  assert.equal(result.length, 1)
330
350
  assert.equal(result[0], middleware)
331
351
  })
352
+
353
+ test('should sort middleware by priority', () => {
354
+ const log: string[] = []
355
+ const low = withPriority('low', 'low', log)
356
+ const highest = withPriority('highest', 'highest', log)
357
+ const medium = withPriority('medium', 'medium', log)
358
+
359
+ const result = combineMiddleware('http', Math.random().toString(), {
360
+ wireMiddleware: [low, highest, medium],
361
+ })
362
+
363
+ assert.equal(result.length, 3)
364
+ assert.equal(result[0], highest)
365
+ assert.equal(result[1], medium)
366
+ assert.equal(result[2], low)
367
+ })
368
+
369
+ test('should default unprioritized middleware to medium', () => {
370
+ const log: string[] = []
371
+ const highest = withPriority('highest', 'highest', log)
372
+ const lowest = withPriority('lowest', 'lowest', log)
373
+ const noPriority: CorePikkuMiddleware = async (_services, _wire, next) => {
374
+ await next()
375
+ }
376
+
377
+ const result = combineMiddleware('http', Math.random().toString(), {
378
+ wireMiddleware: [lowest, noPriority, highest],
379
+ })
380
+
381
+ assert.equal(result.length, 3)
382
+ assert.equal(result[0], highest)
383
+ assert.equal(result[1], noPriority) // defaults to medium
384
+ assert.equal(result[2], lowest)
385
+ })
386
+
387
+ test('should preserve registration order within same priority', () => {
388
+ const log: string[] = []
389
+ const first = withPriority('first', 'medium', log)
390
+ const second = withPriority('second', 'medium', log)
391
+ const third = withPriority('third', 'medium', log)
392
+
393
+ const result = combineMiddleware('http', Math.random().toString(), {
394
+ wireMiddleware: [first, second, third],
395
+ })
396
+
397
+ assert.equal(result.length, 3)
398
+ assert.equal(result[0], first)
399
+ assert.equal(result[1], second)
400
+ assert.equal(result[2], third)
401
+ })
332
402
  })
333
403
 
334
404
  describe('runMiddleware', () => {
@@ -415,4 +485,82 @@ describe('runMiddleware', () => {
415
485
 
416
486
  assert.deepEqual(executionOrder, ['middleware'])
417
487
  })
488
+
489
+ test('should execute middleware in priority order (highest first, lowest last)', async () => {
490
+ const log: string[] = []
491
+ const low = withPriority('low', 'low', log)
492
+ const highest = withPriority('highest', 'highest', log)
493
+ const medium = withPriority('medium', 'medium', log)
494
+
495
+ await runMiddleware(
496
+ {} as any,
497
+ {} as any,
498
+ [low, highest, medium],
499
+ async () => {
500
+ log.push('main')
501
+ }
502
+ )
503
+
504
+ assert.deepEqual(log, [
505
+ 'start:highest',
506
+ 'start:medium',
507
+ 'start:low',
508
+ 'main',
509
+ 'end:low',
510
+ 'end:medium',
511
+ 'end:highest',
512
+ ])
513
+ })
514
+
515
+ test('should sort unsorted middleware passed directly to runMiddleware', async () => {
516
+ const log: string[] = []
517
+ const lowest = withPriority('lowest', 'lowest', log)
518
+ const highest = withPriority('highest', 'highest', log)
519
+
520
+ await runMiddleware({} as any, {} as any, [lowest, highest], async () => {
521
+ log.push('main')
522
+ })
523
+
524
+ // highest should run first (outermost), lowest last (innermost)
525
+ assert.deepEqual(log, [
526
+ 'start:highest',
527
+ 'start:lowest',
528
+ 'main',
529
+ 'end:lowest',
530
+ 'end:highest',
531
+ ])
532
+ })
533
+
534
+ test('should handle all five priority levels in correct order', async () => {
535
+ const log: string[] = []
536
+ const lowest = withPriority('lowest', 'lowest', log)
537
+ const low = withPriority('low', 'low', log)
538
+ const medium = withPriority('medium', 'medium', log)
539
+ const high = withPriority('high', 'high', log)
540
+ const highest = withPriority('highest', 'highest', log)
541
+
542
+ // Pass in reverse order to verify sorting
543
+ await runMiddleware(
544
+ {} as any,
545
+ {} as any,
546
+ [lowest, low, medium, high, highest],
547
+ async () => {
548
+ log.push('main')
549
+ }
550
+ )
551
+
552
+ assert.deepEqual(log, [
553
+ 'start:highest',
554
+ 'start:high',
555
+ 'start:medium',
556
+ 'start:low',
557
+ 'start:lowest',
558
+ 'main',
559
+ 'end:lowest',
560
+ 'end:low',
561
+ 'end:medium',
562
+ 'end:high',
563
+ 'end:highest',
564
+ ])
565
+ })
418
566
  })
@@ -5,10 +5,34 @@ import type {
5
5
  CorePikkuMiddlewareGroup,
6
6
  PikkuWiringTypes,
7
7
  MiddlewareMetadata,
8
+ MiddlewarePriority,
8
9
  } from './types/core.types.js'
9
10
  import { pikkuState } from './pikku-state.js'
10
11
  import { freezeDedupe, getTagGroups } from './utils.js'
11
12
 
13
+ const PRIORITY_ORDER: Record<MiddlewarePriority, number> = {
14
+ highest: 0,
15
+ high: 1,
16
+ medium: 2,
17
+ low: 3,
18
+ lowest: 4,
19
+ }
20
+
21
+ const getMiddlewarePriority = (fn: CorePikkuMiddleware): number => {
22
+ const priority = (
23
+ fn as CorePikkuMiddleware & { __priority?: MiddlewarePriority }
24
+ ).__priority
25
+ return PRIORITY_ORDER[priority ?? 'medium']
26
+ }
27
+
28
+ const sortByPriority = (
29
+ middlewares: CorePikkuMiddleware[]
30
+ ): CorePikkuMiddleware[] => {
31
+ return middlewares.sort(
32
+ (a, b) => getMiddlewarePriority(a) - getMiddlewarePriority(b)
33
+ )
34
+ }
35
+
12
36
  /**
13
37
  * Runs a chain of middleware functions in sequence before executing the main function.
14
38
  *
@@ -32,11 +56,16 @@ export const runMiddleware = async <Middleware extends CorePikkuMiddleware>(
32
56
  middlewares: readonly Middleware[],
33
57
  main?: () => Promise<unknown>
34
58
  ): Promise<unknown> => {
35
- // Deduplicate middleware using Set to avoid running the same middleware multiple times
59
+ // Sort by priority if not already sorted (cached middleware is pre-sorted)
60
+ const sorted = isSortedByPriority(middlewares)
61
+ ? middlewares
62
+ : ([...middlewares].sort(
63
+ (a, b) => getMiddlewarePriority(a) - getMiddlewarePriority(b)
64
+ ) as readonly Middleware[])
36
65
  let result: any
37
66
  const dispatch = async (index: number): Promise<any> => {
38
- if (middlewares && index < middlewares.length) {
39
- return await middlewares[index]!(services as any, wire, () =>
67
+ if (sorted && index < sorted.length) {
68
+ return await sorted[index]!(services as any, wire, () =>
40
69
  dispatch(index + 1)
41
70
  )
42
71
  } else if (main) {
@@ -47,6 +76,20 @@ export const runMiddleware = async <Middleware extends CorePikkuMiddleware>(
47
76
  return result
48
77
  }
49
78
 
79
+ const isSortedByPriority = (
80
+ middlewares: readonly CorePikkuMiddleware[]
81
+ ): boolean => {
82
+ for (let i = 1; i < middlewares.length; i++) {
83
+ if (
84
+ getMiddlewarePriority(middlewares[i]) <
85
+ getMiddlewarePriority(middlewares[i - 1])
86
+ ) {
87
+ return false
88
+ }
89
+ }
90
+ return true
91
+ }
92
+
50
93
  /**
51
94
  * Registers global middleware for a specific tag.
52
95
  *
@@ -231,7 +274,8 @@ export const combineMiddleware = (
231
274
  resolved.push(...funcMiddleware)
232
275
  }
233
276
 
234
- // Deduplicate and freeze
277
+ // Sort by priority, deduplicate, and freeze
278
+ sortByPriority(resolved)
235
279
  middlewareCache[wireType][uid] = freezeDedupe(
236
280
  resolved
237
281
  ) as readonly CorePikkuMiddleware[]
@@ -1,5 +1,6 @@
1
1
  import type {
2
2
  CoreServices,
3
+ CoreUserSession,
3
4
  PikkuWiringTypes,
4
5
  PermissionMetadata,
5
6
  PikkuWire,
@@ -325,3 +326,72 @@ export const runPermissions = async (
325
326
  throw new ForbiddenError('Permission denied')
326
327
  }
327
328
  }
329
+
330
+ /**
331
+ * Checks whether a session passes the auth checks (pikkuAuth only) for a
332
+ * given function/agent. Skips pikkuPermission checks since those require
333
+ * request data which isn't available at filter time.
334
+ *
335
+ * @param funcPermissions - The PermissionMetadata[] from function or agent metadata
336
+ * @param session - The user's session
337
+ * @param services - Singleton services
338
+ * @param packageName - Optional package namespace
339
+ * @returns true if the session passes all auth checks (or no auth checks exist)
340
+ */
341
+ export const checkAuthPermissions = async (
342
+ funcPermissions: PermissionMetadata[] | undefined,
343
+ session: CoreUserSession,
344
+ services: CoreServices,
345
+ packageName: string | null = null
346
+ ): Promise<boolean> => {
347
+ if (!funcPermissions?.length) return true
348
+
349
+ const allPermissions = combinePermissions(
350
+ 'agent',
351
+ `authcheck:${Math.random()}`,
352
+ {
353
+ funcInheritedPermissions: funcPermissions,
354
+ packageName,
355
+ }
356
+ )
357
+
358
+ if (allPermissions.length === 0) return true
359
+
360
+ const wire = { session } as unknown as PikkuWire<
361
+ any,
362
+ never,
363
+ any,
364
+ never,
365
+ never,
366
+ never
367
+ >
368
+
369
+ // Extract only pikkuAuth permissions (marked with __pikkuAuth)
370
+ const authPerms: CorePikkuPermission[] = []
371
+ for (const permission of allPermissions) {
372
+ if (typeof permission === 'function') {
373
+ if ((permission as any).__pikkuAuth) {
374
+ authPerms.push(permission)
375
+ }
376
+ } else if (permission && typeof permission === 'object') {
377
+ for (const funcs of Object.values(permission)) {
378
+ const arr = Array.isArray(funcs) ? funcs : [funcs]
379
+ for (const fn of arr) {
380
+ if (typeof fn === 'function' && (fn as any).__pikkuAuth) {
381
+ authPerms.push(fn)
382
+ }
383
+ }
384
+ }
385
+ }
386
+ }
387
+
388
+ // No auth permissions = allowed (only data-dependent permissions exist)
389
+ if (authPerms.length === 0) return true
390
+
391
+ // All auth permissions must pass
392
+ for (const perm of authPerms) {
393
+ const result = await perm(services, null, wire)
394
+ if (result) return true
395
+ }
396
+ return false
397
+ }
@@ -7,7 +7,6 @@ import {
7
7
  getAllPackageStates,
8
8
  getSingletonServices,
9
9
  getCreateWireServices,
10
- getPikkuMetaDir,
11
10
  addPackageServiceFactories,
12
11
  } from './pikku-state.js'
13
12
 
@@ -176,31 +175,6 @@ describe('getCreateWireServices', () => {
176
175
  })
177
176
  })
178
177
 
179
- describe('getPikkuMetaDir', () => {
180
- test('should return null by default', () => {
181
- const result = getPikkuMetaDir()
182
- assert.strictEqual(result, null)
183
- })
184
-
185
- test('should return metaDir when set', () => {
186
- pikkuState(null, 'package', 'metaDir', '/some/path' as any)
187
- const result = getPikkuMetaDir()
188
- assert.strictEqual(result, '/some/path')
189
- })
190
-
191
- test('should return metaDir for specific package', () => {
192
- pikkuState('@test/pkg', 'package', 'metaDir', '/addon/path' as any)
193
- const result = getPikkuMetaDir('@test/pkg')
194
- assert.strictEqual(result, '/addon/path')
195
- })
196
-
197
- test('should treat null packageName as main', () => {
198
- pikkuState(null, 'package', 'metaDir', '/main/path' as any)
199
- const result = getPikkuMetaDir(null)
200
- assert.strictEqual(result, '/main/path')
201
- })
202
- })
203
-
204
178
  describe('addPackageServiceFactories', () => {
205
179
  test('should register factories for addon package', () => {
206
180
  const mockFactories = {
@@ -156,8 +156,8 @@ const createEmptyPackageState = (): PikkuPackageState => ({
156
156
  package: {
157
157
  factories: null,
158
158
  singletonServices: null,
159
- metaDir: null,
160
159
  credentialsMeta: null,
160
+ requiredParentServices: null,
161
161
  },
162
162
  })
163
163
 
@@ -188,10 +188,6 @@ if (!getAllPackageStates().has('__main__')) {
188
188
  resetPikkuState()
189
189
  }
190
190
 
191
- export const getPikkuMetaDir = (packageName?: string | null): string | null => {
192
- return pikkuState(packageName ?? null, 'package', 'metaDir')
193
- }
194
-
195
191
  export const getSingletonServices = (): CoreSingletonServices => {
196
192
  const services = pikkuState(null, 'package', 'singletonServices')
197
193
  if (!services) throw new Error('Singleton services not initialized')