@pikku/core 0.12.1 → 0.12.2

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 (70) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/middleware-runner.js +1 -0
  3. package/dist/permissions.js +1 -0
  4. package/dist/pikku-state.js +4 -0
  5. package/dist/services/gateway-service.d.ts +19 -0
  6. package/dist/services/gateway-service.js +1 -0
  7. package/dist/services/index.d.ts +2 -0
  8. package/dist/services/index.js +1 -0
  9. package/dist/services/local-gateway-service.d.ts +22 -0
  10. package/dist/services/local-gateway-service.js +51 -0
  11. package/dist/types/core.types.d.ts +3 -1
  12. package/dist/types/state.types.d.ts +5 -0
  13. package/dist/utils.js +1 -1
  14. package/dist/wirings/ai-agent/ai-agent-assistant-ui.js +27 -5
  15. package/dist/wirings/ai-agent/ai-agent-memory.js +13 -2
  16. package/dist/wirings/ai-agent/ai-agent-prepare.js +14 -1
  17. package/dist/wirings/ai-agent/ai-agent-stream.js +17 -2
  18. package/dist/wirings/ai-agent/ai-agent.types.d.ts +35 -2
  19. package/dist/wirings/ai-agent/index.d.ts +1 -1
  20. package/dist/wirings/channel/channel-handler.d.ts +5 -2
  21. package/dist/wirings/channel/channel-handler.js +11 -1
  22. package/dist/wirings/channel/channel-middleware-runner.js +1 -0
  23. package/dist/wirings/channel/channel.types.d.ts +6 -0
  24. package/dist/wirings/channel/index.d.ts +1 -1
  25. package/dist/wirings/channel/local/local-channel-handler.d.ts +7 -0
  26. package/dist/wirings/channel/local/local-channel-handler.js +17 -0
  27. package/dist/wirings/channel/local/local-channel-runner.js +14 -1
  28. package/dist/wirings/channel/pikku-abstract-channel-handler.d.ts +2 -1
  29. package/dist/wirings/channel/pikku-abstract-channel-handler.js +1 -0
  30. package/dist/wirings/channel/serverless/serverless-channel-runner.js +1 -1
  31. package/dist/wirings/cli/cli-runner.js +3 -0
  32. package/dist/wirings/gateway/gateway-runner.d.ts +24 -0
  33. package/dist/wirings/gateway/gateway-runner.js +325 -0
  34. package/dist/wirings/gateway/gateway.types.d.ts +127 -0
  35. package/dist/wirings/gateway/gateway.types.js +1 -0
  36. package/dist/wirings/gateway/index.d.ts +2 -0
  37. package/dist/wirings/gateway/index.js +1 -0
  38. package/dist/wirings/http/http-runner.js +3 -0
  39. package/package.json +3 -2
  40. package/src/middleware-runner.ts +1 -0
  41. package/src/permissions.ts +1 -0
  42. package/src/pikku-state.ts +5 -0
  43. package/src/services/gateway-service.ts +20 -0
  44. package/src/services/index.ts +2 -0
  45. package/src/services/local-gateway-service.ts +62 -0
  46. package/src/types/core.types.ts +3 -0
  47. package/src/types/state.types.ts +8 -0
  48. package/src/utils.ts +1 -1
  49. package/src/wirings/ai-agent/ai-agent-assistant-ui.ts +33 -9
  50. package/src/wirings/ai-agent/ai-agent-memory.ts +10 -1
  51. package/src/wirings/ai-agent/ai-agent-prepare.ts +19 -1
  52. package/src/wirings/ai-agent/ai-agent-stream.ts +13 -2
  53. package/src/wirings/ai-agent/ai-agent.types.ts +34 -2
  54. package/src/wirings/ai-agent/index.ts +2 -0
  55. package/src/wirings/channel/channel-handler.ts +30 -2
  56. package/src/wirings/channel/channel-middleware-runner.ts +1 -0
  57. package/src/wirings/channel/channel.types.ts +11 -0
  58. package/src/wirings/channel/index.ts +1 -0
  59. package/src/wirings/channel/local/local-channel-handler.ts +26 -0
  60. package/src/wirings/channel/local/local-channel-runner.ts +15 -1
  61. package/src/wirings/channel/pikku-abstract-channel-handler.test.ts +2 -0
  62. package/src/wirings/channel/pikku-abstract-channel-handler.ts +8 -1
  63. package/src/wirings/channel/serverless/serverless-channel-runner.ts +1 -1
  64. package/src/wirings/cli/cli-runner.ts +3 -0
  65. package/src/wirings/gateway/gateway-runner.test.ts +474 -0
  66. package/src/wirings/gateway/gateway-runner.ts +411 -0
  67. package/src/wirings/gateway/gateway.types.ts +149 -0
  68. package/src/wirings/gateway/index.ts +13 -0
  69. package/src/wirings/http/http-runner.ts +3 -0
  70. package/tsconfig.tsbuildinfo +1 -1
@@ -1,4 +1,4 @@
1
- import type { PikkuChannel, PikkuChannelHandler } from './channel.types.js';
1
+ import type { BinaryData, PikkuChannel, PikkuChannelHandler } from './channel.types.js';
2
2
  export declare abstract class PikkuAbstractChannelHandler<OpeningData = unknown, Out = unknown> implements PikkuChannelHandler<OpeningData, Out> {
3
3
  channelId: string;
4
4
  channelName: string;
@@ -6,6 +6,7 @@ export declare abstract class PikkuAbstractChannelHandler<OpeningData = unknown,
6
6
  protected channel?: PikkuChannel<OpeningData, Out>;
7
7
  constructor(channelId: string, channelName: string, openingData: OpeningData);
8
8
  abstract send(message: Out, isBinary?: boolean): Promise<void> | void;
9
+ abstract sendBinary(data: BinaryData): Promise<void> | void;
9
10
  getChannel(): PikkuChannel<OpeningData, Out>;
10
11
  open(): void;
11
12
  close(): Promise<void> | void;
@@ -14,6 +14,7 @@ export class PikkuAbstractChannelHandler {
14
14
  channelId: this.channelId,
15
15
  openingData: this.openingData,
16
16
  send: this.send.bind(this),
17
+ sendBinary: this.sendBinary.bind(this),
17
18
  close: this.close.bind(this),
18
19
  state: 'initial',
19
20
  };
@@ -171,7 +171,7 @@ export const runChannelMessage = async ({ channelId, channelStore, channelHandle
171
171
  };
172
172
  let response;
173
173
  try {
174
- const onMessage = processMessageHandlers(services, channelConfig, channelHandler, userSession);
174
+ const { onMessage } = processMessageHandlers(services, channelConfig, channelHandler, userSession);
175
175
  response = await onMessage(data);
176
176
  }
177
177
  catch (e) {
@@ -207,6 +207,9 @@ export async function runCLICommand({ program, commandPath, data, singletonServi
207
207
  await Promise.resolve(renderer(singletonServices, data, undefined));
208
208
  }
209
209
  },
210
+ sendBinary: () => {
211
+ throw new Error('Binary data is not supported on CLI channels');
212
+ },
210
213
  close: () => {
211
214
  if (channel) {
212
215
  channel.state = 'closed';
@@ -0,0 +1,24 @@
1
+ import type { CoreGateway } from './gateway.types.js';
2
+ import type { CoreSingletonServices } from '../../types/core.types.js';
3
+ /**
4
+ * Register a messaging gateway.
5
+ *
6
+ * `wireGateway` is a meta-wiring that composes existing primitives
7
+ * (`wireHTTP`, `wireChannel`) under the hood. It does not replace them;
8
+ * it layers on top.
9
+ *
10
+ * @param config - Gateway configuration (name, type, adapter, func, middleware, etc.)
11
+ */
12
+ export declare const wireGateway: (config: CoreGateway) => void;
13
+ /**
14
+ * Create the message handler callback for a listener gateway.
15
+ *
16
+ * Returns a function `(rawData: unknown) => Promise<void>` that can be
17
+ * passed to `adapter.init()`. Used by `LocalGatewayService` (and any
18
+ * other GatewayService implementation) to wire up listener gateways.
19
+ *
20
+ * @param name - Gateway name (for wire.gateway metadata)
21
+ * @param config - The gateway configuration (adapter, func, middleware)
22
+ * @param singletonServices - Singleton services to pass to handler/middleware
23
+ */
24
+ export declare const createListenerMessageHandler: (name: string, config: CoreGateway, singletonServices: CoreSingletonServices) => ((rawData: unknown) => Promise<void>);
@@ -0,0 +1,325 @@
1
+ import { pikkuState } from '../../pikku-state.js';
2
+ import { addFunction } from '../../function/function-runner.js';
3
+ import { runMiddleware } from '../../middleware-runner.js';
4
+ import { httpRouter } from '../http/routers/http-router.js';
5
+ /**
6
+ * Register a messaging gateway.
7
+ *
8
+ * `wireGateway` is a meta-wiring that composes existing primitives
9
+ * (`wireHTTP`, `wireChannel`) under the hood. It does not replace them;
10
+ * it layers on top.
11
+ *
12
+ * @param config - Gateway configuration (name, type, adapter, func, middleware, etc.)
13
+ */
14
+ export const wireGateway = (config) => {
15
+ // Store gateway config
16
+ pikkuState(null, 'gateway', 'gateways').set(config.name, config);
17
+ switch (config.type) {
18
+ case 'webhook':
19
+ wireWebhookGateway(config);
20
+ break;
21
+ case 'websocket':
22
+ wireWebsocketGateway(config);
23
+ break;
24
+ case 'listener':
25
+ wireListenerGateway(config);
26
+ break;
27
+ default:
28
+ throw new Error(`Unknown gateway type '${config.type}' for gateway '${config.name}'`);
29
+ }
30
+ };
31
+ // ---------------------------------------------------------------------------
32
+ // Webhook gateway — platform POSTs to us
33
+ // ---------------------------------------------------------------------------
34
+ const wireWebhookGateway = (config) => {
35
+ const { name, route, adapter } = config;
36
+ if (!route) {
37
+ throw new Error(`Webhook gateway '${name}' requires a route`);
38
+ }
39
+ const postFuncId = `gateway__${name}__post`;
40
+ const httpMeta = pikkuState(null, 'http', 'meta');
41
+ const funcMeta = pikkuState(null, 'function', 'meta');
42
+ const routes = pikkuState(null, 'http', 'routes');
43
+ // --- POST handler (main message receiver) --------------------------------
44
+ funcMeta[postFuncId] = {
45
+ pikkuFuncId: postFuncId,
46
+ inputSchemaName: null,
47
+ outputSchemaName: null,
48
+ sessionless: true,
49
+ };
50
+ httpMeta['post'][route] = {
51
+ pikkuFuncId: postFuncId,
52
+ route,
53
+ method: 'post',
54
+ };
55
+ const postHandler = {
56
+ auth: false,
57
+ func: createWebhookPostHandler(config),
58
+ };
59
+ addFunction(postFuncId, postHandler);
60
+ if (!routes.has('post')) {
61
+ routes.set('post', new Map());
62
+ }
63
+ routes.get('post').set(route, {
64
+ method: 'post',
65
+ route,
66
+ func: postHandler,
67
+ auth: false,
68
+ });
69
+ // --- GET handler (webhook verification, e.g. WhatsApp challenge) ---------
70
+ if (adapter.verifyWebhook) {
71
+ const verifyFuncId = `gateway__${name}__verify`;
72
+ funcMeta[verifyFuncId] = {
73
+ pikkuFuncId: verifyFuncId,
74
+ inputSchemaName: null,
75
+ outputSchemaName: null,
76
+ sessionless: true,
77
+ };
78
+ httpMeta['get'][route] = {
79
+ pikkuFuncId: verifyFuncId,
80
+ route,
81
+ method: 'get',
82
+ };
83
+ const verifyHandler = {
84
+ auth: false,
85
+ func: createWebhookVerifyHandler(adapter),
86
+ };
87
+ addFunction(verifyFuncId, verifyHandler);
88
+ if (!routes.has('get')) {
89
+ routes.set('get', new Map());
90
+ }
91
+ routes.get('get').set(route, {
92
+ method: 'get',
93
+ route,
94
+ func: verifyHandler,
95
+ auth: false,
96
+ });
97
+ }
98
+ // Force router to re-initialize on next match
99
+ httpRouter.reset();
100
+ };
101
+ /**
102
+ * Creates the POST handler function for a webhook gateway.
103
+ *
104
+ * Flow:
105
+ * 1. Check for POST-based verification (e.g. Slack `url_verification`)
106
+ * 2. Parse body via adapter → GatewayInboundMessage (or null to ignore)
107
+ * 3. Populate `wire.gateway`
108
+ * 4. Run user middleware (which can read `wire.gateway` for auth)
109
+ * 5. Call user func with parsed message
110
+ * 6. Auto-send response via adapter if func returns outbound content
111
+ */
112
+ const createWebhookPostHandler = (config) => {
113
+ const { name, adapter, func: userFunc, middleware: userMiddleware } = config;
114
+ const userFuncConfig = userFunc;
115
+ return async (services, data, wire) => {
116
+ // Check for POST-based webhook verification (e.g. Slack url_verification)
117
+ if (adapter.verifyWebhook) {
118
+ const verifyResult = await adapter.verifyWebhook(data, wire.http?.request);
119
+ if (verifyResult.verified) {
120
+ return verifyResult.response;
121
+ }
122
+ }
123
+ // Parse the platform-specific payload
124
+ const parsed = adapter.parse(data);
125
+ if (!parsed) {
126
+ // Ignored event (delivery receipt, typing indicator, etc.)
127
+ return { ok: true };
128
+ }
129
+ // Populate wire.gateway
130
+ const gateway = {
131
+ gatewayName: name,
132
+ senderId: parsed.senderId,
133
+ platform: adapter.name,
134
+ send: (msg) => adapter.send(parsed.senderId, msg),
135
+ };
136
+ wire.gateway = gateway;
137
+ // Build combined middleware chain: gateway-level + func-level
138
+ const allMiddleware = [
139
+ ...(userMiddleware || []),
140
+ ...(userFuncConfig.middleware || []),
141
+ ];
142
+ const exec = async () => {
143
+ const result = await userFuncConfig.func(services, parsed, wire);
144
+ // Auto-send response if the func returns outbound content
145
+ if (result && (result.text || result.richContent || result.attachments)) {
146
+ await adapter.send(parsed.senderId, result);
147
+ }
148
+ return { ok: true };
149
+ };
150
+ if (allMiddleware.length > 0) {
151
+ return await runMiddleware(services, wire, allMiddleware, exec);
152
+ }
153
+ return await exec();
154
+ };
155
+ };
156
+ /**
157
+ * Creates the GET handler for webhook verification challenges.
158
+ * Passes query parameters to the adapter's verifyWebhook method.
159
+ */
160
+ const createWebhookVerifyHandler = (adapter) => {
161
+ return async (_services, _data, wire) => {
162
+ if (!adapter.verifyWebhook) {
163
+ return { error: 'Verification not supported' };
164
+ }
165
+ const query = wire.http?.request?.query();
166
+ const result = await adapter.verifyWebhook(query, wire.http?.request);
167
+ if (result.verified) {
168
+ return result.response;
169
+ }
170
+ return { error: 'Verification failed' };
171
+ };
172
+ };
173
+ // ---------------------------------------------------------------------------
174
+ // WebSocket gateway — client connects via WebSocket
175
+ // ---------------------------------------------------------------------------
176
+ const wireWebsocketGateway = (config) => {
177
+ const { name, route, adapter } = config;
178
+ if (!route) {
179
+ throw new Error(`WebSocket gateway '${name}' requires a route`);
180
+ }
181
+ // Store gateway config — the channel runner will reference this
182
+ pikkuState(null, 'gateway', 'gateways').set(config.name, config);
183
+ const channelsMeta = pikkuState(null, 'channel', 'meta');
184
+ const channels = pikkuState(null, 'channel', 'channels');
185
+ const messageFuncId = `gateway__${name}__message`;
186
+ const connectFuncId = `gateway__${name}__connect`;
187
+ // Add function metadata
188
+ const funcMeta = pikkuState(null, 'function', 'meta');
189
+ funcMeta[messageFuncId] = {
190
+ pikkuFuncId: messageFuncId,
191
+ inputSchemaName: null,
192
+ outputSchemaName: null,
193
+ sessionless: true,
194
+ };
195
+ funcMeta[connectFuncId] = {
196
+ pikkuFuncId: connectFuncId,
197
+ inputSchemaName: null,
198
+ outputSchemaName: null,
199
+ sessionless: true,
200
+ };
201
+ // Add channel metadata
202
+ channelsMeta[name] = {
203
+ name,
204
+ route,
205
+ gateway: true,
206
+ connect: { pikkuFuncId: connectFuncId },
207
+ message: { pikkuFuncId: messageFuncId },
208
+ };
209
+ const userFuncConfig = config.func;
210
+ const userMiddleware = config.middleware;
211
+ // Register onConnect
212
+ addFunction(connectFuncId, {
213
+ auth: false,
214
+ func: async (_services, _data, wire) => {
215
+ ;
216
+ wire.gateway = {
217
+ gatewayName: name,
218
+ senderId: '',
219
+ platform: adapter.name,
220
+ send: async (msg) => {
221
+ wire.channel?.send(msg);
222
+ },
223
+ };
224
+ },
225
+ });
226
+ // Register onMessage
227
+ addFunction(messageFuncId, {
228
+ auth: false,
229
+ func: async (services, data, wire) => {
230
+ const parsed = adapter.parse(data);
231
+ if (!parsed)
232
+ return;
233
+ const gateway = {
234
+ gatewayName: name,
235
+ senderId: parsed.senderId,
236
+ platform: adapter.name,
237
+ send: async (msg) => {
238
+ wire.channel?.send(msg);
239
+ },
240
+ };
241
+ wire.gateway = gateway;
242
+ const allMiddleware = [
243
+ ...(userMiddleware || []),
244
+ ...(userFuncConfig.middleware || []),
245
+ ];
246
+ const exec = async () => {
247
+ const result = await userFuncConfig.func(services, parsed, wire);
248
+ if (result &&
249
+ (result.text || result.richContent || result.attachments)) {
250
+ wire.channel?.send(result);
251
+ }
252
+ };
253
+ if (allMiddleware.length > 0) {
254
+ await runMiddleware(services, wire, allMiddleware, exec);
255
+ }
256
+ else {
257
+ await exec();
258
+ }
259
+ },
260
+ });
261
+ // Store channel config
262
+ channels.set(name, {
263
+ name,
264
+ route,
265
+ auth: false,
266
+ onConnect: { func: async () => { } },
267
+ onMessage: { func: async () => { } },
268
+ });
269
+ // Force router to re-initialize
270
+ httpRouter.reset();
271
+ };
272
+ // ---------------------------------------------------------------------------
273
+ // Listener gateway — standalone event loop, no route
274
+ // ---------------------------------------------------------------------------
275
+ const wireListenerGateway = (config) => {
276
+ // Listener gateways don't register routes.
277
+ // They are started manually and call into the gateway's func directly.
278
+ // The gateway config is already stored in pikkuState.
279
+ pikkuState(null, 'gateway', 'gateways').set(config.name, config);
280
+ };
281
+ /**
282
+ * Create the message handler callback for a listener gateway.
283
+ *
284
+ * Returns a function `(rawData: unknown) => Promise<void>` that can be
285
+ * passed to `adapter.init()`. Used by `LocalGatewayService` (and any
286
+ * other GatewayService implementation) to wire up listener gateways.
287
+ *
288
+ * @param name - Gateway name (for wire.gateway metadata)
289
+ * @param config - The gateway configuration (adapter, func, middleware)
290
+ * @param singletonServices - Singleton services to pass to handler/middleware
291
+ */
292
+ export const createListenerMessageHandler = (name, config, singletonServices) => {
293
+ const { adapter } = config;
294
+ const userFuncConfig = config.func;
295
+ const userMiddleware = config.middleware;
296
+ return async (rawData) => {
297
+ const parsed = adapter.parse(rawData);
298
+ if (!parsed)
299
+ return;
300
+ const wire = {};
301
+ const gateway = {
302
+ gatewayName: name,
303
+ senderId: parsed.senderId,
304
+ platform: adapter.name,
305
+ send: (msg) => adapter.send(parsed.senderId, msg),
306
+ };
307
+ wire.gateway = gateway;
308
+ const allMiddleware = [
309
+ ...(userMiddleware || []),
310
+ ...(userFuncConfig.middleware || []),
311
+ ];
312
+ const exec = async () => {
313
+ const result = await userFuncConfig.func(singletonServices, parsed, wire);
314
+ if (result && (result.text || result.richContent || result.attachments)) {
315
+ await adapter.send(parsed.senderId, result);
316
+ }
317
+ };
318
+ if (allMiddleware.length > 0) {
319
+ await runMiddleware(singletonServices, wire, allMiddleware, exec);
320
+ }
321
+ else {
322
+ await exec();
323
+ }
324
+ };
325
+ };
@@ -0,0 +1,127 @@
1
+ import type { CommonWireMeta, CorePikkuMiddleware, CorePikkuMiddlewareGroup } from '../../types/core.types.js';
2
+ import type { CorePikkuFunctionConfig, CorePermissionGroup, CorePikkuPermission } from '../../function/functions.types.js';
3
+ import type { PikkuHTTPRequest } from '../http/http.types.js';
4
+ /**
5
+ * Attachment in gateway messages (images, files, etc.)
6
+ */
7
+ export interface GatewayAttachment {
8
+ type: string;
9
+ url?: string;
10
+ data?: ArrayBuffer | Uint8Array;
11
+ mimeType?: string;
12
+ filename?: string;
13
+ }
14
+ /**
15
+ * Normalized inbound message from any platform
16
+ */
17
+ export interface GatewayInboundMessage {
18
+ /** Platform-specific sender identifier (phone number, Slack user ID, etc.) */
19
+ senderId: string;
20
+ /** Message text content */
21
+ text: string;
22
+ /** Original platform-specific payload */
23
+ raw: unknown;
24
+ /** Optional file/media attachments */
25
+ attachments?: GatewayAttachment[];
26
+ /** Optional platform-specific metadata */
27
+ metadata?: Record<string, unknown>;
28
+ }
29
+ /**
30
+ * Outbound message to send back via the platform
31
+ */
32
+ export interface GatewayOutboundMessage {
33
+ /** Plain text response */
34
+ text?: string;
35
+ /** Platform-specific rich content (Slack blocks, WhatsApp templates, etc.) */
36
+ richContent?: Record<string, unknown>;
37
+ /** Optional file/media attachments */
38
+ attachments?: GatewayAttachment[];
39
+ }
40
+ /**
41
+ * Result of webhook verification (e.g., WhatsApp GET challenge, Slack url_verification)
42
+ */
43
+ export type WebhookVerificationResult = {
44
+ verified: true;
45
+ response: unknown;
46
+ } | {
47
+ verified: false;
48
+ };
49
+ /**
50
+ * Gateway adapter interface — implemented by platform-specific addon packages.
51
+ *
52
+ * Two responsibilities:
53
+ * 1. Parse incoming platform-specific format into normalized GatewayInboundMessage
54
+ * 2. Send outgoing GatewayOutboundMessage via the platform's API
55
+ */
56
+ export interface GatewayAdapter {
57
+ /** Platform name (e.g., 'whatsapp', 'slack', 'telegram') */
58
+ name: string;
59
+ /** Parse platform-specific payload into normalized message. Return null to ignore (e.g., delivery receipts). */
60
+ parse(data: unknown): GatewayInboundMessage | null;
61
+ /** Send a message to a specific sender via the platform's API */
62
+ send(senderId: string, message: GatewayOutboundMessage): Promise<void>;
63
+ /** Initialize the adapter (connect to platform, start listening).
64
+ * Called by GatewayService.start() — the adapter should call onMessage for each incoming event. */
65
+ init(onMessage: (data: unknown) => Promise<void>): Promise<void>;
66
+ /** Tear down the adapter (disconnect, release resources). */
67
+ close(): Promise<void>;
68
+ /** Handle webhook verification challenges (only for webhook type).
69
+ * Receives the data (body or query params) and the Pikku HTTP request for additional inspection. */
70
+ verifyWebhook?(data: unknown, request?: PikkuHTTPRequest): WebhookVerificationResult | Promise<WebhookVerificationResult>;
71
+ }
72
+ /**
73
+ * The gateway wire object available on wire.gateway inside handler functions and middleware
74
+ */
75
+ export interface PikkuGateway {
76
+ /** Name of this gateway instance */
77
+ gatewayName: string;
78
+ /** Platform-specific sender identifier */
79
+ senderId: string;
80
+ /** Platform name from adapter.name */
81
+ platform: string;
82
+ /** Send a proactive message to the sender */
83
+ send(msg: GatewayOutboundMessage): Promise<void>;
84
+ }
85
+ /**
86
+ * Gateway transport type:
87
+ * - 'webhook': Platform POSTs to us (WhatsApp Cloud API, Slack Events API, Telegram webhooks)
88
+ * - 'websocket': Client connects via WebSocket (WebChat, browser widget)
89
+ * - 'listener': Standalone event loop, no route (Baileys, Signal CLI, Matrix sync)
90
+ */
91
+ export type GatewayTransportType = 'webhook' | 'websocket' | 'listener';
92
+ /**
93
+ * Core gateway configuration for wireGateway()
94
+ */
95
+ export type CoreGateway<PikkuFunctionConfig = CorePikkuFunctionConfig<any, any>, PikkuPermission extends CorePikkuPermission = CorePikkuPermission, PikkuMiddleware extends CorePikkuMiddleware = CorePikkuMiddleware> = {
96
+ /** Unique name for this gateway */
97
+ name: string;
98
+ /** Transport type */
99
+ type: GatewayTransportType;
100
+ /** HTTP route for webhook/websocket types */
101
+ route?: string;
102
+ /** The gateway adapter (parse inbound, send outbound) */
103
+ adapter: GatewayAdapter;
104
+ /** The handler function that processes parsed messages */
105
+ func: PikkuFunctionConfig;
106
+ /** Optional middleware chain (e.g., auth) */
107
+ middleware?: CorePikkuMiddlewareGroup<any, any>;
108
+ /** Optional permissions */
109
+ permissions?: CorePermissionGroup | PikkuPermission[];
110
+ /** Optional tags for categorization */
111
+ tags?: string[];
112
+ /** Whether authentication is required (default: true) */
113
+ auth?: boolean;
114
+ };
115
+ /**
116
+ * Metadata for a registered gateway, stored in state
117
+ */
118
+ export type GatewayMeta = CommonWireMeta & {
119
+ name: string;
120
+ type: GatewayTransportType;
121
+ route?: string;
122
+ gateway: true;
123
+ };
124
+ /**
125
+ * All gateway metadata keyed by name
126
+ */
127
+ export type GatewaysMeta = Record<string, GatewayMeta>;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ export { wireGateway, createListenerMessageHandler } from './gateway-runner.js';
2
+ export type { GatewayAdapter, GatewayAttachment, GatewayInboundMessage, GatewayOutboundMessage, GatewayMeta, GatewaysMeta, GatewayTransportType, CoreGateway, PikkuGateway, WebhookVerificationResult, } from './gateway.types.js';
@@ -0,0 +1 @@
1
+ export { wireGateway, createListenerMessageHandler } from './gateway-runner.js';
@@ -240,6 +240,9 @@ const executeRoute = async (services, matchedRoute, http, options) => {
240
240
  send: (data) => {
241
241
  response.arrayBuffer(isSerializable(data) ? JSON.stringify(data) : data);
242
242
  },
243
+ sendBinary: (data) => {
244
+ response.arrayBuffer(data);
245
+ },
243
246
  close: () => {
244
247
  channel.state = 'closed';
245
248
  response.close?.();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pikku/core",
3
- "version": "0.12.1",
3
+ "version": "0.12.2",
4
4
  "author": "yasser.fadl@gmail.com",
5
5
  "license": "MIT",
6
6
  "module": "dist/index.js",
@@ -30,6 +30,7 @@
30
30
  "./rpc": "./dist/wirings/rpc/index.js",
31
31
  "./mcp": "./dist/wirings/mcp/index.js",
32
32
  "./ai-agent": "./dist/wirings/ai-agent/index.js",
33
+ "./gateway": "./dist/wirings/gateway/index.js",
33
34
  "./cli": "./dist/wirings/cli/index.js",
34
35
  "./cli/channel": "./dist/wirings/cli/channel/index.js",
35
36
  "./node": "./dist/wirings/node/index.js",
@@ -50,7 +51,7 @@
50
51
  "picoquery": "^2.5.0"
51
52
  },
52
53
  "devDependencies": {
53
- "@types/node": "^24.10.12",
54
+ "@types/node": "^24.11.0",
54
55
  "tsx": "^4.21.0",
55
56
  "typescript": "^5.9"
56
57
  },
@@ -121,6 +121,7 @@ const middlewareCache: Record<
121
121
  agent: {},
122
122
  cli: {},
123
123
  workflow: {},
124
+ gateway: {},
124
125
  }
125
126
 
126
127
  /**
@@ -130,6 +130,7 @@ const combinedPermissionsCache: Record<
130
130
  agent: {},
131
131
  cli: {},
132
132
  workflow: {},
133
+ gateway: {},
133
134
  }
134
135
 
135
136
  /**
@@ -10,6 +10,7 @@ import type {
10
10
  MCPPromptMeta,
11
11
  } from './wirings/mcp/mcp.types.js'
12
12
  import type { AIAgentMeta } from './wirings/ai-agent/ai-agent.types.js'
13
+ import type { GatewaysMeta } from './wirings/gateway/gateway.types.js'
13
14
  import type { ScheduledTasksMeta } from './wirings/scheduler/scheduler.types.js'
14
15
  import type { TriggerMeta } from './wirings/trigger/trigger.types.js'
15
16
 
@@ -121,6 +122,10 @@ const createEmptyPackageState = (): PikkuPackageState => ({
121
122
  agents: new Map(),
122
123
  agentsMeta: {} as AIAgentMeta,
123
124
  },
125
+ gateway: {
126
+ gateways: new Map(),
127
+ meta: {} as GatewaysMeta,
128
+ },
124
129
  cli: {
125
130
  meta: { programs: {}, renderers: {} },
126
131
  programs: {},
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Abstract GatewayService interface.
3
+ *
4
+ * Implementations manage listener gateway lifecycle — initializing adapters
5
+ * and delivering messages to handler functions.
6
+ *
7
+ * A `LocalGatewayService` starts all listeners unconditionally;
8
+ * a distributed implementation could check leader election first.
9
+ */
10
+ export interface GatewayService {
11
+ /**
12
+ * Start all listener gateways
13
+ */
14
+ start(): Promise<void>
15
+
16
+ /**
17
+ * Stop all listener gateways
18
+ */
19
+ stop(): Promise<void>
20
+ }
@@ -17,6 +17,7 @@ export { ConsoleLogger } from './logger-console.js'
17
17
  export { InMemoryWorkflowService } from './in-memory-workflow-service.js'
18
18
  export { InMemoryTriggerService } from './in-memory-trigger-service.js'
19
19
  export { InMemoryAIRunStateService } from './in-memory-ai-run-state-service.js'
20
+ export { LocalGatewayService } from './local-gateway-service.js'
20
21
  export type { ContentService } from './content-service.js'
21
22
  export type { JWTService } from './jwt-service.js'
22
23
  export type { Logger } from './logger.js'
@@ -30,6 +31,7 @@ export type {
30
31
  SchedulerService,
31
32
  } from './scheduler-service.js'
32
33
  export type { TriggerService } from './trigger-service.js'
34
+ export type { GatewayService } from './gateway-service.js'
33
35
  export type {
34
36
  DeploymentService,
35
37
  DeploymentConfig,