@pikku/core 0.12.64 → 0.12.67

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 (74) hide show
  1. package/CHANGELOG.md +311 -0
  2. package/dist/index.d.ts +2 -2
  3. package/dist/index.js +1 -1
  4. package/dist/permissions.d.ts +12 -4
  5. package/dist/permissions.js +11 -32
  6. package/dist/scopes.d.ts +14 -0
  7. package/dist/scopes.js +39 -8
  8. package/dist/testing/service-tests.js +37 -0
  9. package/dist/types/core.types.d.ts +22 -0
  10. package/dist/wirings/ai-agent/ai-agent-prepare.d.ts +64 -0
  11. package/dist/wirings/ai-agent/ai-agent-prepare.js +103 -5
  12. package/dist/wirings/ai-agent/ai-agent-runner.js +5 -1
  13. package/dist/wirings/ai-agent/ai-agent-stream.js +28 -7
  14. package/dist/wirings/ai-agent/ai-agent.types.d.ts +29 -1
  15. package/dist/wirings/ai-agent/index.d.ts +1 -1
  16. package/dist/wirings/ai-agent/index.js +1 -1
  17. package/dist/wirings/ai-agent/voice-input.js +3 -3
  18. package/dist/wirings/cli/cli-runner.js +3 -0
  19. package/dist/wirings/cli/command-parser.d.ts +2 -0
  20. package/dist/wirings/cli/command-parser.js +59 -2
  21. package/dist/wirings/credential/credential.types.d.ts +14 -0
  22. package/dist/wirings/credential/validate-credential-definitions.js +1 -0
  23. package/dist/wirings/gateway/gateway-runner.js +100 -50
  24. package/dist/wirings/gateway/gateway.types.d.ts +8 -5
  25. package/dist/wirings/http/http.types.d.ts +3 -3
  26. package/dist/wirings/scope/validate-scope-definitions.d.ts +8 -0
  27. package/dist/wirings/scope/validate-scope-definitions.js +16 -1
  28. package/dist/wirings/secret/secret.types.d.ts +14 -0
  29. package/dist/wirings/secret/validate-secret-definitions.js +2 -0
  30. package/dist/wirings/variable/validate-variable-definitions.js +2 -0
  31. package/dist/wirings/variable/variable.types.d.ts +14 -0
  32. package/dist/wirings/workflow/dsl/workflow-dsl.types.d.ts +36 -6
  33. package/dist/wirings/workflow/pikku-workflow-service.d.ts +8 -0
  34. package/dist/wirings/workflow/pikku-workflow-service.js +16 -0
  35. package/dist/wirings/workflow/workflow.types.d.ts +0 -2
  36. package/package.json +2 -1
  37. package/src/index.ts +2 -1
  38. package/src/permissions.test.ts +14 -8
  39. package/src/permissions.ts +14 -36
  40. package/src/scopes.test.ts +37 -1
  41. package/src/scopes.ts +48 -9
  42. package/src/testing/service-tests.ts +49 -0
  43. package/src/types/core.types.ts +23 -0
  44. package/src/wirings/ai-agent/ai-agent-authorization.test.ts +204 -0
  45. package/src/wirings/ai-agent/ai-agent-prepare.test.ts +175 -0
  46. package/src/wirings/ai-agent/ai-agent-prepare.ts +132 -5
  47. package/src/wirings/ai-agent/ai-agent-resume-authorization.test.ts +207 -0
  48. package/src/wirings/ai-agent/ai-agent-runner.ts +7 -0
  49. package/src/wirings/ai-agent/ai-agent-stream.test.ts +103 -0
  50. package/src/wirings/ai-agent/ai-agent-stream.ts +38 -6
  51. package/src/wirings/ai-agent/ai-agent.types.ts +29 -0
  52. package/src/wirings/ai-agent/index.ts +4 -0
  53. package/src/wirings/ai-agent/voice-input.test.ts +90 -0
  54. package/src/wirings/ai-agent/voice-input.ts +8 -10
  55. package/src/wirings/cli/cli-runner.ts +4 -0
  56. package/src/wirings/cli/command-parser.test.ts +130 -0
  57. package/src/wirings/cli/command-parser.ts +80 -2
  58. package/src/wirings/credential/credential.types.ts +14 -0
  59. package/src/wirings/credential/validate-credential-definitions.ts +1 -0
  60. package/src/wirings/gateway/gateway-authorization.test.ts +444 -0
  61. package/src/wirings/gateway/gateway-runner.ts +114 -68
  62. package/src/wirings/gateway/gateway.types.ts +7 -9
  63. package/src/wirings/http/http.types.ts +6 -4
  64. package/src/wirings/scope/scope.test.ts +25 -0
  65. package/src/wirings/scope/validate-scope-definitions.ts +16 -1
  66. package/src/wirings/secret/secret.types.ts +14 -0
  67. package/src/wirings/secret/validate-secret-definitions.ts +2 -0
  68. package/src/wirings/variable/validate-variable-definitions.ts +2 -0
  69. package/src/wirings/variable/variable.types.ts +14 -0
  70. package/src/wirings/workflow/dsl/workflow-dsl.types.ts +36 -6
  71. package/src/wirings/workflow/pikku-workflow-service.ts +36 -0
  72. package/src/wirings/workflow/workflow-on-error.test.ts +154 -0
  73. package/src/wirings/workflow/workflow.types.ts +0 -2
  74. package/tsconfig.tsbuildinfo +1 -1
@@ -56,6 +56,7 @@ export function validateAndBuildCredentialDefinitionsMeta(
56
56
  type: def.type,
57
57
  schema: def.schema,
58
58
  oauth2: def.oauth2,
59
+ docsUrl: def.docsUrl,
59
60
  sourceFile: def.sourceFile,
60
61
  }
61
62
  }
@@ -0,0 +1,444 @@
1
+ import { test, describe, beforeEach } from 'node:test'
2
+ import * as assert from 'assert'
3
+ import { wireGateway, createListenerMessageHandler } from './gateway-runner.js'
4
+ import { pikkuState, resetPikkuState } from '../../pikku-state.js'
5
+ import { httpRouter } from '../http/routers/http-router.js'
6
+ import { fetch } from '../http/http-runner.js'
7
+ import {
8
+ addGlobalPermission,
9
+ clearPermissionsCache,
10
+ } from '../../permissions.js'
11
+ import type {
12
+ GatewayAdapter,
13
+ GatewayInboundMessage,
14
+ GatewayOutboundMessage,
15
+ } from './gateway.types.js'
16
+
17
+ const createMockAdapter = (): GatewayAdapter & {
18
+ sentMessages: Array<{ senderId: string; message: GatewayOutboundMessage }>
19
+ } => {
20
+ const sentMessages: Array<{
21
+ senderId: string
22
+ message: GatewayOutboundMessage
23
+ }> = []
24
+
25
+ return {
26
+ name: 'mock',
27
+ sentMessages,
28
+ parse(data: unknown): GatewayInboundMessage | null {
29
+ const d = data as Record<string, any>
30
+ if (!d?.text) return null
31
+ return { senderId: d.senderId ?? 'sender-1', text: d.text, raw: data }
32
+ },
33
+ send: async (senderId, message) => {
34
+ sentMessages.push({ senderId, message })
35
+ },
36
+ init: async () => {},
37
+ close: async () => {},
38
+ }
39
+ }
40
+
41
+ const singletonServices = {
42
+ config: {},
43
+ logger: {
44
+ info: () => {},
45
+ warn: () => {},
46
+ error: () => {},
47
+ debug: () => {},
48
+ },
49
+ variables: {},
50
+ secrets: {},
51
+ }
52
+
53
+ const setupState = () => {
54
+ resetPikkuState()
55
+ httpRouter.reset()
56
+ clearPermissionsCache()
57
+ pikkuState(null, 'package', 'singletonServices', singletonServices as any)
58
+ pikkuState(null, 'package', 'factories', {
59
+ createWireServices: async () => ({}),
60
+ } as any)
61
+ }
62
+
63
+ const seedCompiledMeta = () => {
64
+ const httpMeta = pikkuState(null, 'http', 'meta') as any
65
+ const funcMeta = pikkuState(null, 'function', 'meta') as any
66
+ for (const config of pikkuState(null, 'gateway', 'gateways').values()) {
67
+ if (config.type !== 'webhook' || !config.route) continue
68
+ for (const [method, funcId] of [
69
+ ['post', `gateway__${config.name}__post`],
70
+ ['get', `gateway__${config.name}__verify`],
71
+ ] as const) {
72
+ httpMeta[method][config.route] = {
73
+ pikkuFuncId: funcId,
74
+ route: config.route,
75
+ method,
76
+ }
77
+ funcMeta[funcId] = {
78
+ pikkuFuncId: funcId,
79
+ inputSchemaName: null,
80
+ outputSchemaName: null,
81
+ sessionless: true,
82
+ }
83
+ }
84
+ }
85
+ }
86
+
87
+ const postMessage = async (route: string) => {
88
+ const request = new Request(`http://localhost${route}`, {
89
+ method: 'POST',
90
+ headers: { 'Content-Type': 'application/json' },
91
+ body: JSON.stringify({ senderId: 'user-1', text: 'hello' }),
92
+ })
93
+ return await fetch(request)
94
+ }
95
+
96
+ describe('gateway handler authorization', () => {
97
+ beforeEach(() => {
98
+ setupState()
99
+ })
100
+
101
+ describe('webhook gateway', () => {
102
+ test('a denying permission on the handler blocks it', async () => {
103
+ const calls: string[] = []
104
+
105
+ wireGateway({
106
+ name: 'perm-deny',
107
+ type: 'webhook',
108
+ route: '/webhooks/perm-deny',
109
+ adapter: createMockAdapter(),
110
+ func: {
111
+ permissions: { denied: async () => false },
112
+ func: async () => {
113
+ calls.push('ran')
114
+ return { text: 'reply' }
115
+ },
116
+ } as any,
117
+ })
118
+
119
+ seedCompiledMeta()
120
+ httpRouter.initialize()
121
+
122
+ const response = await postMessage('/webhooks/perm-deny')
123
+
124
+ assert.equal(response.status, 403)
125
+ assert.deepEqual(calls, [], 'handler must not run when permissions deny')
126
+ })
127
+
128
+ test('a passing permission on the handler allows it', async () => {
129
+ const calls: string[] = []
130
+
131
+ wireGateway({
132
+ name: 'perm-allow',
133
+ type: 'webhook',
134
+ route: '/webhooks/perm-allow',
135
+ adapter: createMockAdapter(),
136
+ func: {
137
+ permissions: { allowed: async () => true },
138
+ func: async () => {
139
+ calls.push('ran')
140
+ return { text: 'reply' }
141
+ },
142
+ } as any,
143
+ })
144
+
145
+ seedCompiledMeta()
146
+ httpRouter.initialize()
147
+
148
+ const response = await postMessage('/webhooks/perm-allow')
149
+
150
+ assert.equal(response.status, 200)
151
+ assert.deepEqual(calls, ['ran'])
152
+ })
153
+
154
+ test('handler scopes are enforced and fail closed without a session', async () => {
155
+ const calls: string[] = []
156
+
157
+ wireGateway({
158
+ name: 'scope-gate',
159
+ type: 'webhook',
160
+ route: '/webhooks/scope-gate',
161
+ adapter: createMockAdapter(),
162
+ func: {
163
+ scopes: ['admin:webhooks'],
164
+ func: async () => {
165
+ calls.push('ran')
166
+ return { text: 'reply' }
167
+ },
168
+ } as any,
169
+ })
170
+
171
+ seedCompiledMeta()
172
+ httpRouter.initialize()
173
+
174
+ const response = await postMessage('/webhooks/scope-gate')
175
+
176
+ assert.equal(response.status, 403)
177
+ assert.deepEqual(calls, [], 'handler must not run without the scope')
178
+ })
179
+
180
+ test('global permissions apply to gateway handlers', async () => {
181
+ const calls: string[] = []
182
+
183
+ addGlobalPermission([async () => false])
184
+
185
+ wireGateway({
186
+ name: 'global-deny',
187
+ type: 'webhook',
188
+ route: '/webhooks/global-deny',
189
+ adapter: createMockAdapter(),
190
+ func: {
191
+ func: async () => {
192
+ calls.push('ran')
193
+ return { text: 'reply' }
194
+ },
195
+ } as any,
196
+ })
197
+
198
+ seedCompiledMeta()
199
+ httpRouter.initialize()
200
+
201
+ const response = await postMessage('/webhooks/global-deny')
202
+
203
+ assert.equal(response.status, 403)
204
+ assert.deepEqual(calls, [], 'a denying global must block the handler')
205
+ })
206
+
207
+ test('a handler with no authorization declared still runs', async () => {
208
+ const calls: string[] = []
209
+
210
+ wireGateway({
211
+ name: 'open',
212
+ type: 'webhook',
213
+ route: '/webhooks/open',
214
+ adapter: createMockAdapter(),
215
+ func: {
216
+ func: async () => {
217
+ calls.push('ran')
218
+ return { text: 'reply' }
219
+ },
220
+ } as any,
221
+ })
222
+
223
+ seedCompiledMeta()
224
+ httpRouter.initialize()
225
+
226
+ const response = await postMessage('/webhooks/open')
227
+
228
+ assert.equal(response.status, 200)
229
+ assert.deepEqual(calls, ['ran'])
230
+ })
231
+
232
+ test('gateway-level auth: true requires a session', async () => {
233
+ const calls: string[] = []
234
+
235
+ wireGateway({
236
+ name: 'gw-auth',
237
+ type: 'webhook',
238
+ route: '/webhooks/gw-auth',
239
+ adapter: createMockAdapter(),
240
+ auth: true,
241
+ func: {
242
+ func: async () => {
243
+ calls.push('ran')
244
+ return { text: 'reply' }
245
+ },
246
+ } as any,
247
+ })
248
+
249
+ seedCompiledMeta()
250
+ httpRouter.initialize()
251
+
252
+ const response = await postMessage('/webhooks/gw-auth')
253
+
254
+ assert.equal(response.status, 403)
255
+ assert.deepEqual(calls, [], 'auth: true must require a session')
256
+ })
257
+
258
+ test('handler-level auth: true requires a session', async () => {
259
+ const calls: string[] = []
260
+
261
+ wireGateway({
262
+ name: 'handler-auth',
263
+ type: 'webhook',
264
+ route: '/webhooks/handler-auth',
265
+ adapter: createMockAdapter(),
266
+ func: {
267
+ auth: true,
268
+ func: async () => {
269
+ calls.push('ran')
270
+ return { text: 'reply' }
271
+ },
272
+ } as any,
273
+ })
274
+
275
+ seedCompiledMeta()
276
+ httpRouter.initialize()
277
+
278
+ const response = await postMessage('/webhooks/handler-auth')
279
+
280
+ assert.equal(response.status, 403)
281
+ assert.deepEqual(calls, [], 'auth: true must require a session')
282
+ })
283
+
284
+ test('the adapter still auto-sends the handler reply', async () => {
285
+ const adapter = createMockAdapter()
286
+
287
+ wireGateway({
288
+ name: 'reply',
289
+ type: 'webhook',
290
+ route: '/webhooks/reply',
291
+ adapter,
292
+ func: {
293
+ permissions: { allowed: async () => true },
294
+ func: async () => ({ text: 'pong' }),
295
+ } as any,
296
+ })
297
+
298
+ seedCompiledMeta()
299
+ httpRouter.initialize()
300
+
301
+ await postMessage('/webhooks/reply')
302
+
303
+ assert.equal(adapter.sentMessages.length, 1)
304
+ assert.equal(adapter.sentMessages[0].message.text, 'pong')
305
+ })
306
+
307
+ test('a session set by gateway middleware satisfies the handler gate', async () => {
308
+ const seen: any[] = []
309
+
310
+ wireGateway({
311
+ name: 'mw-session',
312
+ type: 'webhook',
313
+ route: '/webhooks/mw-session',
314
+ adapter: createMockAdapter(),
315
+ auth: true,
316
+ middleware: [
317
+ async (_s: any, wire: any, next: any) => {
318
+ await wire.setSession({
319
+ userId: 'mapped-user',
320
+ scopes: ['gateway:inbound'],
321
+ })
322
+ await next()
323
+ },
324
+ ] as any,
325
+ func: {
326
+ scopes: ['gateway:inbound'],
327
+ func: async (_s: any, _d: any, wire: any) => {
328
+ seen.push(wire.session)
329
+ return { text: 'ok' }
330
+ },
331
+ } as any,
332
+ })
333
+
334
+ seedCompiledMeta()
335
+ httpRouter.initialize()
336
+
337
+ const response = await postMessage('/webhooks/mw-session')
338
+
339
+ assert.equal(response.status, 200)
340
+ assert.equal(seen[0]?.userId, 'mapped-user')
341
+ })
342
+
343
+ test('handler middleware still runs, and only once', async () => {
344
+ const order: string[] = []
345
+
346
+ wireGateway({
347
+ name: 'mw',
348
+ type: 'webhook',
349
+ route: '/webhooks/mw',
350
+ adapter: createMockAdapter(),
351
+ func: {
352
+ middleware: [
353
+ async (_s: any, _w: any, next: any) => {
354
+ order.push('func-mw')
355
+ await next()
356
+ },
357
+ ],
358
+ func: async () => {
359
+ order.push('handler')
360
+ return { text: 'ok' }
361
+ },
362
+ } as any,
363
+ middleware: [
364
+ async (_s: any, _w: any, next: any) => {
365
+ order.push('gateway-mw')
366
+ await next()
367
+ },
368
+ ] as any,
369
+ })
370
+
371
+ seedCompiledMeta()
372
+ httpRouter.initialize()
373
+
374
+ await postMessage('/webhooks/mw')
375
+
376
+ assert.deepEqual(order, ['gateway-mw', 'func-mw', 'handler'])
377
+ })
378
+ })
379
+
380
+ describe('listener gateway', () => {
381
+ test('a denying permission on the handler blocks it', async () => {
382
+ const calls: string[] = []
383
+ const adapter = createMockAdapter()
384
+
385
+ const config = {
386
+ name: 'listen-deny',
387
+ type: 'listener' as const,
388
+ adapter,
389
+ func: {
390
+ permissions: { denied: async () => false },
391
+ func: async () => {
392
+ calls.push('ran')
393
+ return { text: 'reply' }
394
+ },
395
+ },
396
+ }
397
+
398
+ wireGateway(config as any)
399
+
400
+ const handler = createListenerMessageHandler(
401
+ config.name,
402
+ config as any,
403
+ singletonServices as any
404
+ )
405
+
406
+ await assert.rejects(
407
+ () => handler({ senderId: 'user-1', text: 'hello' }),
408
+ /Permission denied/
409
+ )
410
+ assert.deepEqual(calls, [], 'handler must not run when permissions deny')
411
+ })
412
+
413
+ test('a passing permission on the handler allows it', async () => {
414
+ const calls: string[] = []
415
+ const adapter = createMockAdapter()
416
+
417
+ const config = {
418
+ name: 'listen-allow',
419
+ type: 'listener' as const,
420
+ adapter,
421
+ func: {
422
+ permissions: { allowed: async () => true },
423
+ func: async () => {
424
+ calls.push('ran')
425
+ return { text: 'reply' }
426
+ },
427
+ },
428
+ }
429
+
430
+ wireGateway(config as any)
431
+
432
+ const handler = createListenerMessageHandler(
433
+ config.name,
434
+ config as any,
435
+ singletonServices as any
436
+ )
437
+
438
+ await handler({ senderId: 'user-1', text: 'hello' })
439
+
440
+ assert.deepEqual(calls, ['ran'])
441
+ assert.equal(adapter.sentMessages.length, 1)
442
+ })
443
+ })
444
+ })
@@ -1,6 +1,6 @@
1
1
  import { pikkuState } from '../../pikku-state.js'
2
2
  import { NotFoundError, UnauthorizedError } from '../../errors/errors.js'
3
- import { addFunction } from '../../function/function-runner.js'
3
+ import { addFunction, runPikkuFunc } from '../../function/function-runner.js'
4
4
  import { runMiddleware } from '../../middleware-runner.js'
5
5
  import { httpRouter } from '../http/routers/http-router.js'
6
6
  import type {
@@ -17,6 +17,54 @@ import type {
17
17
  */
18
18
  const resolvedAdapters = new WeakMap<CoreGateway, Promise<GatewayAdapter>>()
19
19
 
20
+ /**
21
+ * The generated function id a gateway's handler is registered under.
22
+ */
23
+ const gatewayHandlerFuncId = (name: string) => `gateway__${name}__handler`
24
+
25
+ /**
26
+ * Bridges a session established by gateway middleware onto the wire so the
27
+ * handler's gate can see it.
28
+ *
29
+ * Gateway middleware is the only place a webhook can acquire a session (e.g.
30
+ * mapping a verified platform sender to a user). Middleware that assigns
31
+ * `wire.session` needs nothing, but middleware using the idiomatic
32
+ * `wire.setSession()` writes to the enclosing wiring's session service, which
33
+ * the handler's own invocation does not read — without this the session would
34
+ * be silently invisible to `auth` and `scopes`.
35
+ */
36
+ const bridgeMiddlewareSession = async (wire: PikkuRawWire): Promise<void> => {
37
+ if (wire.session || !wire.getSession) return
38
+ const session = await wire.getSession()
39
+ if (session) {
40
+ wire.session = session
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Registers a gateway's handler as a real pikku function so that invoking it
46
+ * goes through the function runner's gate. Without this the handler is called
47
+ * directly and its own `auth`, `scopes` and `permissions` are never evaluated.
48
+ *
49
+ * The handler is registered as sessionless: a gateway's inbound traffic is
50
+ * platform-authenticated (adapter signature verification), not session-bearing,
51
+ * so requiring a session by default would break every webhook. A handler that
52
+ * does need one declares `auth: true`, exactly like `pikkuSessionlessFunc`.
53
+ * `scopes` and `permissions` are always enforced when declared.
54
+ */
55
+ const registerGatewayHandler = (config: CoreGateway): string => {
56
+ const funcId = gatewayHandlerFuncId(config.name)
57
+ const funcMeta = pikkuState(null, 'function', 'meta')
58
+ funcMeta[funcId] = {
59
+ pikkuFuncId: funcId,
60
+ inputSchemaName: null,
61
+ outputSchemaName: null,
62
+ sessionless: true,
63
+ }
64
+ addFunction(funcId, config.func as any)
65
+ return funcId
66
+ }
67
+
20
68
  export const resolveGatewayAdapter = (
21
69
  config: CoreGateway,
22
70
  services: CoreSingletonServices
@@ -142,15 +190,13 @@ const wireWebhookGateway = (config: CoreGateway): void => {
142
190
  * 2. Parse body via adapter → GatewayInboundMessage (or null to ignore)
143
191
  * 3. Populate `wire.gateway`
144
192
  * 4. Run user middleware (which can read `wire.gateway` for auth)
145
- * 5. Call user func with parsed message
193
+ * 5. Invoke the handler through the function runner, which enforces its
194
+ * `auth`/`scopes`/`permissions` before running it
146
195
  * 6. Auto-send response via adapter if func returns outbound content
147
196
  */
148
197
  const createWebhookPostHandler = (config: CoreGateway) => {
149
- const { name, func: userFunc, middleware: userMiddleware } = config
150
- const userFuncConfig = userFunc as {
151
- func: Function
152
- middleware?: CorePikkuMiddleware[]
153
- }
198
+ const { name, middleware: userMiddleware } = config
199
+ const handlerFuncId = registerGatewayHandler(config)
154
200
 
155
201
  return async (
156
202
  services: CoreSingletonServices,
@@ -183,26 +229,32 @@ const createWebhookPostHandler = (config: CoreGateway) => {
183
229
  }
184
230
  ;(wire as any).gateway = gateway
185
231
 
186
- // Build combined middleware chain: gateway-level + func-level
187
- const allMiddleware: CorePikkuMiddleware[] = [
188
- ...((userMiddleware as CorePikkuMiddleware[] | undefined) || []),
189
- ...(userFuncConfig.middleware || []),
190
- ]
191
-
192
- const exec = async () => {
193
- const result = await userFuncConfig.func(services, parsed, wire)
194
- // Auto-send response if the func returns outbound content
195
- if (result && (result.text || result.richContent || result.attachments)) {
196
- await adapter.send(parsed.senderId, result as GatewayOutboundMessage)
197
- }
198
- return { ok: true }
232
+ // Gateway middleware runs first and outside the gate, so it can establish
233
+ // the session the gate then checks. The handler is invoked through the
234
+ // function runner, which enforces its auth, scopes and permissions and
235
+ // applies the handler's own middleware.
236
+ const invoke = async () => {
237
+ await bridgeMiddlewareSession(wire as any)
238
+ return await runPikkuFunc('gateway', name, handlerFuncId, {
239
+ singletonServices: services,
240
+ data: () => parsed,
241
+ auth: config.auth,
242
+ wire: wire as any,
243
+ })
199
244
  }
200
245
 
201
- if (allMiddleware.length > 0) {
202
- return await runMiddleware(services, wire, allMiddleware, exec)
203
- }
246
+ const gatewayMiddleware = userMiddleware as
247
+ | CorePikkuMiddleware[]
248
+ | undefined
249
+ const result: any = gatewayMiddleware?.length
250
+ ? await runMiddleware(services, wire, gatewayMiddleware, invoke)
251
+ : await invoke()
204
252
 
205
- return await exec()
253
+ // Auto-send response if the func returns outbound content
254
+ if (result && (result.text || result.richContent || result.attachments)) {
255
+ await adapter.send(parsed.senderId, result as GatewayOutboundMessage)
256
+ }
257
+ return { ok: true }
206
258
  }
207
259
  }
208
260
 
@@ -281,11 +333,8 @@ const wireWebsocketGateway = (config: CoreGateway): void => {
281
333
  message: { pikkuFuncId: messageFuncId },
282
334
  } as any
283
335
 
284
- const userFuncConfig = config.func as {
285
- func: Function
286
- middleware?: CorePikkuMiddleware[]
287
- }
288
336
  const userMiddleware = config.middleware as CorePikkuMiddleware[] | undefined
337
+ const handlerFuncId = registerGatewayHandler(config)
289
338
 
290
339
  // Register onConnect
291
340
  addFunction(connectFuncId, {
@@ -321,25 +370,25 @@ const wireWebsocketGateway = (config: CoreGateway): void => {
321
370
  }
322
371
  ;(wire as any).gateway = gateway
323
372
 
324
- const allMiddleware: CorePikkuMiddleware[] = [
325
- ...(userMiddleware || []),
326
- ...(userFuncConfig.middleware || []),
327
- ]
328
-
329
- const exec = async () => {
330
- const result = await userFuncConfig.func(services, parsed, wire)
331
- if (
332
- result &&
333
- (result.text || result.richContent || result.attachments)
334
- ) {
335
- wire.channel?.send(result)
336
- }
373
+ const invoke = async () => {
374
+ await bridgeMiddlewareSession(wire as any)
375
+ return await runPikkuFunc('gateway', name, handlerFuncId, {
376
+ singletonServices: services,
377
+ data: () => parsed,
378
+ auth: config.auth,
379
+ wire: wire as any,
380
+ })
337
381
  }
338
382
 
339
- if (allMiddleware.length > 0) {
340
- await runMiddleware(services, wire, allMiddleware, exec)
341
- } else {
342
- await exec()
383
+ const gatewayMiddleware = userMiddleware as
384
+ | CorePikkuMiddleware[]
385
+ | undefined
386
+ const result: any = gatewayMiddleware?.length
387
+ ? await runMiddleware(services, wire, gatewayMiddleware, invoke)
388
+ : await invoke()
389
+
390
+ if (result && (result.text || result.richContent || result.attachments)) {
391
+ wire.channel?.send(result)
343
392
  }
344
393
  },
345
394
  } as any)
@@ -384,11 +433,8 @@ export const createListenerMessageHandler = (
384
433
  config: CoreGateway,
385
434
  singletonServices: CoreSingletonServices
386
435
  ): ((rawData: unknown) => Promise<void>) => {
387
- const userFuncConfig = config.func as {
388
- func: Function
389
- middleware?: CorePikkuMiddleware[]
390
- }
391
436
  const userMiddleware = config.middleware as CorePikkuMiddleware[] | undefined
437
+ const handlerFuncId = registerGatewayHandler(config)
392
438
 
393
439
  return async (rawData: unknown): Promise<void> => {
394
440
  const adapter = await resolveGatewayAdapter(config, singletonServices)
@@ -404,27 +450,27 @@ export const createListenerMessageHandler = (
404
450
  }
405
451
  ;(wire as any).gateway = gateway
406
452
 
407
- const allMiddleware: CorePikkuMiddleware[] = [
408
- ...(userMiddleware || []),
409
- ...(userFuncConfig.middleware || []),
410
- ]
411
-
412
- const exec = async () => {
413
- const result = await userFuncConfig.func(singletonServices, parsed, wire)
414
- if (result && (result.text || result.richContent || result.attachments)) {
415
- await adapter.send(parsed.senderId, result as GatewayOutboundMessage)
416
- }
453
+ const invoke = async () => {
454
+ await bridgeMiddlewareSession(wire)
455
+ return await runPikkuFunc('gateway', name, handlerFuncId, {
456
+ singletonServices,
457
+ data: () => parsed,
458
+ auth: config.auth,
459
+ wire,
460
+ })
417
461
  }
418
462
 
419
- if (allMiddleware.length > 0) {
420
- await runMiddleware(
421
- singletonServices,
422
- wire as unknown as PikkuWire,
423
- allMiddleware,
424
- exec
425
- )
426
- } else {
427
- await exec()
463
+ const result: any = userMiddleware?.length
464
+ ? await runMiddleware(
465
+ singletonServices,
466
+ wire as any,
467
+ userMiddleware,
468
+ invoke
469
+ )
470
+ : await invoke()
471
+
472
+ if (result && (result.text || result.richContent || result.attachments)) {
473
+ await adapter.send(parsed.senderId, result as GatewayOutboundMessage)
428
474
  }
429
475
  }
430
476
  }