@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.
- package/CHANGELOG.md +36 -0
- package/dist/errors/errors.d.ts +4 -0
- package/dist/errors/errors.js +12 -0
- package/dist/function/function-runner.js +2 -1
- package/dist/function/functions.types.js +1 -0
- package/dist/function/index.d.ts +2 -1
- package/dist/function/index.js +1 -0
- package/dist/handle-error.d.ts +2 -2
- package/dist/handle-error.js +8 -8
- package/dist/index.d.ts +2 -3
- package/dist/index.js +1 -2
- package/dist/middleware/index.d.ts +3 -0
- package/dist/middleware/index.js +3 -0
- package/dist/middleware/remote-auth.js +5 -2
- package/dist/middleware/telemetry.d.ts +47 -0
- package/dist/middleware/telemetry.js +106 -0
- package/dist/middleware-runner.js +31 -4
- package/dist/permissions.d.ts +13 -1
- package/dist/permissions.js +51 -0
- package/dist/pikku-state.d.ts +0 -1
- package/dist/pikku-state.js +1 -4
- package/dist/remote.d.ts +10 -0
- package/dist/remote.js +32 -0
- package/dist/schema.js +2 -0
- package/dist/services/deployment-service.d.ts +12 -1
- package/dist/services/in-memory-queue-service.d.ts +7 -0
- package/dist/services/in-memory-queue-service.js +28 -0
- package/dist/services/index.d.ts +4 -1
- package/dist/services/index.js +2 -1
- package/dist/services/logger-console.d.ts +26 -40
- package/dist/services/logger-console.js +102 -46
- package/dist/services/logger.d.ts +5 -0
- package/dist/services/meta-service.d.ts +175 -0
- package/dist/services/meta-service.js +310 -0
- package/dist/services/workflow-service.d.ts +6 -1
- package/dist/testing/service-tests.js +0 -8
- package/dist/types/core.types.d.ts +21 -0
- package/dist/types/core.types.js +7 -1
- package/dist/types/state.types.d.ts +2 -2
- package/dist/wirings/ai-agent/ai-agent-prepare.js +42 -21
- package/dist/wirings/ai-agent/ai-agent-registry.js +2 -2
- package/dist/wirings/ai-agent/ai-agent-runner.js +18 -1
- package/dist/wirings/ai-agent/ai-agent-stream.js +1 -0
- package/dist/wirings/ai-agent/ai-agent.types.d.ts +5 -3
- package/dist/wirings/channel/channel-runner.js +3 -3
- package/dist/wirings/cli/cli-runner.js +5 -3
- package/dist/wirings/cli/command-parser.js +7 -3
- package/dist/wirings/http/http-runner.d.ts +1 -1
- package/dist/wirings/http/http-runner.js +23 -11
- package/dist/wirings/http/http.types.d.ts +3 -0
- package/dist/wirings/http/pikku-fetch-http-response.d.ts +1 -0
- package/dist/wirings/http/pikku-fetch-http-response.js +3 -0
- package/dist/wirings/mcp/mcp-runner.js +5 -3
- package/dist/wirings/mcp/mcp.types.d.ts +1 -1
- package/dist/wirings/queue/queue-runner.d.ts +3 -1
- package/dist/wirings/queue/queue-runner.js +7 -3
- package/dist/wirings/rpc/rpc-runner.js +56 -90
- package/dist/wirings/scheduler/scheduler-runner.d.ts +3 -1
- package/dist/wirings/scheduler/scheduler-runner.js +9 -5
- package/dist/wirings/trigger/trigger-runner.js +4 -2
- package/dist/wirings/workflow/dsl/workflow-runner.d.ts +1 -1
- package/dist/wirings/workflow/dsl/workflow-runner.js +6 -9
- package/dist/wirings/workflow/graph/graph-runner.d.ts +1 -1
- package/dist/wirings/workflow/graph/graph-runner.js +18 -4
- package/dist/wirings/workflow/index.d.ts +4 -2
- package/dist/wirings/workflow/index.js +4 -2
- package/dist/wirings/workflow/pikku-workflow-service.d.ts +16 -4
- package/dist/wirings/workflow/pikku-workflow-service.js +216 -24
- package/dist/wirings/workflow/workflow-queue-workers.d.ts +40 -0
- package/dist/wirings/workflow/workflow-queue-workers.js +41 -0
- package/dist/wirings/workflow/workflow.types.d.ts +17 -1
- package/package.json +4 -1
- package/src/errors/errors.ts +12 -0
- package/src/function/function-runner.ts +5 -4
- package/src/function/functions.types.ts +1 -0
- package/src/function/index.ts +10 -0
- package/src/handle-error.test.ts +2 -2
- package/src/handle-error.ts +8 -8
- package/src/index.ts +2 -2
- package/src/middleware/index.ts +3 -0
- package/src/middleware/remote-auth.test.ts +4 -4
- package/src/middleware/remote-auth.ts +7 -2
- package/src/middleware/telemetry.ts +110 -0
- package/src/middleware-runner.test.ts +149 -1
- package/src/middleware-runner.ts +48 -4
- package/src/permissions.ts +70 -0
- package/src/pikku-state.test.ts +0 -26
- package/src/pikku-state.ts +1 -5
- package/src/remote.ts +46 -0
- package/src/schema.ts +1 -0
- package/src/services/deployment-service.ts +17 -1
- package/src/services/in-memory-queue-service.ts +46 -0
- package/src/services/index.ts +21 -1
- package/src/services/logger-console.ts +127 -47
- package/src/services/logger.ts +6 -0
- package/src/services/meta-service.ts +523 -0
- package/src/services/workflow-service.ts +8 -0
- package/src/testing/service-tests.ts +0 -10
- package/src/types/core.types.ts +36 -1
- package/src/types/state.types.ts +2 -2
- package/src/wirings/ai-agent/ai-agent-prepare.ts +51 -40
- package/src/wirings/ai-agent/ai-agent-registry.ts +3 -3
- package/src/wirings/ai-agent/ai-agent-runner.ts +16 -1
- package/src/wirings/ai-agent/ai-agent-stream.ts +8 -0
- package/src/wirings/ai-agent/ai-agent.types.ts +5 -3
- package/src/wirings/channel/channel-runner.ts +4 -5
- package/src/wirings/channel/local/local-channel-runner.test.ts +5 -1
- package/src/wirings/cli/cli-runner.test.ts +8 -7
- package/src/wirings/cli/cli-runner.ts +6 -4
- package/src/wirings/cli/command-parser.ts +8 -3
- package/src/wirings/http/http-runner.ts +27 -11
- package/src/wirings/http/http.types.ts +3 -0
- package/src/wirings/http/pikku-fetch-http-response.ts +4 -0
- package/src/wirings/mcp/mcp-runner.ts +7 -9
- package/src/wirings/mcp/mcp.types.ts +1 -1
- package/src/wirings/queue/queue-runner.test.ts +5 -11
- package/src/wirings/queue/queue-runner.ts +11 -3
- package/src/wirings/rpc/rpc-runner.ts +82 -115
- package/src/wirings/scheduler/scheduler-runner.test.ts +5 -11
- package/src/wirings/scheduler/scheduler-runner.ts +13 -5
- package/src/wirings/trigger/trigger-runner.test.ts +10 -11
- package/src/wirings/trigger/trigger-runner.ts +6 -4
- package/src/wirings/workflow/dsl/workflow-runner.ts +11 -10
- package/src/wirings/workflow/graph/graph-runner.ts +19 -4
- package/src/wirings/workflow/index.ts +17 -6
- package/src/wirings/workflow/pikku-workflow-service.ts +284 -24
- package/src/wirings/workflow/workflow-queue-workers.ts +85 -0
- package/src/wirings/workflow/workflow.types.ts +16 -1
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/wirings/ai-agent/agent-dynamic-workflow.d.ts +0 -6
- package/dist/wirings/ai-agent/agent-dynamic-workflow.js +0 -468
- package/dist/wirings/workflow/workflow-helpers.d.ts +0 -36
- package/dist/wirings/workflow/workflow-helpers.js +0 -38
- package/src/wirings/ai-agent/agent-dynamic-workflow.ts +0 -610
- package/src/wirings/workflow/workflow-helpers.test.ts +0 -129
- package/src/wirings/workflow/workflow-helpers.ts +0 -79
package/src/function/index.ts
CHANGED
|
@@ -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'
|
package/src/handle-error.test.ts
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
115
|
+
assert.strictEqual(http._state.jsonBody.errorId, 'tracker-2')
|
|
116
116
|
})
|
|
117
117
|
|
|
118
118
|
test('should set status 403 for ForbiddenError', () => {
|
package/src/handle-error.ts
CHANGED
|
@@ -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}
|
|
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
|
-
|
|
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
|
-
|
|
41
|
+
errorId: traceId,
|
|
42
42
|
})
|
|
43
43
|
|
|
44
44
|
// Log certain status codes as warnings
|
|
45
45
|
if (logWarningsForStatusCodes.includes(errorResponse.status)) {
|
|
46
|
-
if (
|
|
47
|
-
logger.warn(`Warning id: ${
|
|
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 (
|
|
57
|
-
logger.warn(`Error id: ${
|
|
58
|
-
const errorBody: Record<string, unknown> = { errorId:
|
|
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 {
|
package/src/middleware/index.ts
CHANGED
|
@@ -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(
|
|
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 {
|
|
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
|
})
|
package/src/middleware-runner.ts
CHANGED
|
@@ -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
|
-
//
|
|
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 (
|
|
39
|
-
return await
|
|
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
|
-
//
|
|
277
|
+
// Sort by priority, deduplicate, and freeze
|
|
278
|
+
sortByPriority(resolved)
|
|
235
279
|
middlewareCache[wireType][uid] = freezeDedupe(
|
|
236
280
|
resolved
|
|
237
281
|
) as readonly CorePikkuMiddleware[]
|
package/src/permissions.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/pikku-state.test.ts
CHANGED
|
@@ -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 = {
|
package/src/pikku-state.ts
CHANGED
|
@@ -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')
|