@pikku/core 0.12.57 → 0.12.60

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 CHANGED
@@ -1,3 +1,32 @@
1
+ ## 0.12.60
2
+
3
+ ### Patch Changes
4
+
5
+ - a3a49f2: fix(workflow): carry `pikkuUserId` onto queued workflow step wires so authed steps rehydrate their session
6
+
7
+ A workflow step invoked on the queued (pg-boss) executor received the bare job wire (payload is just `{ runId }`), so `pikkuUserId` was never on the step wire and an authed step (`pikkuFunc`) threw `Authentication required` — even though the run wire persisted the acting user's id and the inline executor worked. `invokeStepRpc` now reads `pikkuUserId` from the persisted run wire and merges it into the step wire override, so authed steps rehydrate their session via the `SessionStore` on both the inline and queued paths.
8
+
9
+ ## 0.12.59
10
+
11
+ ### Patch Changes
12
+
13
+ - 1f3f510: Warn when a Pikku function body performs a runtime dynamic `import(...)`.
14
+
15
+ The inspector now flags any `pikkuFunc`/`pikkuSessionlessFunc` (and friends) whose handler body contains a dynamic `import(...)` call — including nested callbacks — with the new `PKU498` diagnostic. Function bodies run on every invocation, so a dynamic import there adds per-call latency and defeats bundling/tree-shaking; the import belongs at the top of the module or in your services/`wireServices` setup instead.
16
+
17
+ Type-only positions like `import('x').Foo` are not flagged. The rule defaults to `warn` — a printed yellow warning that does not fail the build — and is configurable via `lint.functionDynamicImport` in `pikku.config.json` (`'off'` to silence, `'error'` to make it a hard build failure), matching the existing `servicesNotDestructured`/`wiresNotDestructured` lints.
18
+
19
+ ## 0.12.58
20
+
21
+ ### Patch Changes
22
+
23
+ - 7b17b14: Allow a workflow-graph node's `func` to reference a registered AI agent by name, dispatched as an agent run — exactly like sub-workflows. `executeGraphStep`/`executeGraphNodeInline` now check the agent registry and dispatch matching nodes via the agent-run path (`rpc.agent.run`), so the node's result is the agent's declared output and downstream nodes can `ref()` it. The generated `pikkuWorkflowGraph` wrapper widens its node-func union to also accept `keyof FlattenedWorkflowMap` and `keyof FlattenedAgentMap`, and `ref()` resolves an agent node's output keys.
24
+ - daec082: Drop Node 22 support — the minimum supported runtime is now Node 24 (LTS).
25
+
26
+ Node 22 deadlocks `pikku dev` at `loadUserBootstrap` (tsx `register()` + `require(esm)` cycle handling on node 22.12+), and Node 20 is already below our floor. The `engines.node` requirement is raised to `>=24` across all packages, matching `.nvmrc` and the CI test matrix. Closes #751.
27
+
28
+ - e0fd352: wireGateway: allow `adapter` to be a factory `(services) => GatewayAdapter | Promise<GatewayAdapter>`, resolved lazily on first inbound request (webhook/websocket) or gateway start (listener) and cached. Real platform adapters (WhatsApp Cloud API, Slack) need secrets that only exist after boot, while wireGateway runs at module load — a factory bridges that. Factory adapters register the GET verify route unconditionally since verifyWebhook can't be probed before first resolve.
29
+
1
30
  ## 0.12.57
2
31
 
3
32
  ### Patch Changes
@@ -1,5 +1,5 @@
1
1
  import { pikkuState, getSingletonServices } from '../pikku-state.js';
2
- import { createListenerMessageHandler } from '../wirings/gateway/gateway-runner.js';
2
+ import { createListenerMessageHandler, resolveGatewayAdapter, } from '../wirings/gateway/gateway-runner.js';
3
3
  /**
4
4
  * Local GatewayService implementation.
5
5
  *
@@ -27,8 +27,9 @@ export class LocalGatewayService {
27
27
  if (this.activeAdapters.has(name))
28
28
  continue;
29
29
  const handleMessage = createListenerMessageHandler(name, config, singletonServices);
30
- await config.adapter.init(handleMessage);
31
- this.activeAdapters.set(name, config.adapter);
30
+ const adapter = await resolveGatewayAdapter(config, singletonServices);
31
+ await adapter.init(handleMessage);
32
+ this.activeAdapters.set(name, adapter);
32
33
  singletonServices.logger.info(`Started listener gateway: ${name}`);
33
34
  }
34
35
  }
@@ -1,4 +1,5 @@
1
- import type { CoreGateway } from './gateway.types.js';
1
+ import type { CoreGateway, GatewayAdapter } from './gateway.types.js';
2
+ export declare const resolveGatewayAdapter: (config: CoreGateway, services: CoreSingletonServices) => Promise<GatewayAdapter>;
2
3
  import type { CoreSingletonServices } from '../../types/core.types.js';
3
4
  /**
4
5
  * Register a messaging gateway.
@@ -2,6 +2,23 @@ import { pikkuState } from '../../pikku-state.js';
2
2
  import { addFunction } from '../../function/function-runner.js';
3
3
  import { runMiddleware } from '../../middleware-runner.js';
4
4
  import { httpRouter } from '../http/routers/http-router.js';
5
+ /**
6
+ * Lazily resolve a gateway's adapter. Factories are invoked once with the
7
+ * singleton services and cached (promise-cached, so concurrent first
8
+ * requests share one construction).
9
+ */
10
+ const resolvedAdapters = new WeakMap();
11
+ export const resolveGatewayAdapter = (config, services) => {
12
+ let resolved = resolvedAdapters.get(config);
13
+ if (!resolved) {
14
+ resolved =
15
+ typeof config.adapter === 'function'
16
+ ? Promise.resolve(config.adapter(services))
17
+ : Promise.resolve(config.adapter);
18
+ resolvedAdapters.set(config, resolved);
19
+ }
20
+ return resolved;
21
+ };
5
22
  /**
6
23
  * Register a messaging gateway.
7
24
  *
@@ -67,7 +84,9 @@ const wireWebhookGateway = (config) => {
67
84
  auth: false,
68
85
  });
69
86
  // --- GET handler (webhook verification, e.g. WhatsApp challenge) ---------
70
- if (adapter.verifyWebhook) {
87
+ // Factory adapters can't be probed for verifyWebhook until first resolve,
88
+ // so register the GET route unconditionally for them.
89
+ if (typeof adapter === 'function' || adapter.verifyWebhook) {
71
90
  const verifyFuncId = `gateway__${name}__verify`;
72
91
  funcMeta[verifyFuncId] = {
73
92
  pikkuFuncId: verifyFuncId,
@@ -82,7 +101,7 @@ const wireWebhookGateway = (config) => {
82
101
  };
83
102
  const verifyHandler = {
84
103
  auth: false,
85
- func: createWebhookVerifyHandler(adapter),
104
+ func: createWebhookVerifyHandler(config),
86
105
  };
87
106
  addFunction(verifyFuncId, verifyHandler);
88
107
  if (!routes.has('get')) {
@@ -110,9 +129,10 @@ const wireWebhookGateway = (config) => {
110
129
  * 6. Auto-send response via adapter if func returns outbound content
111
130
  */
112
131
  const createWebhookPostHandler = (config) => {
113
- const { name, adapter, func: userFunc, middleware: userMiddleware } = config;
132
+ const { name, func: userFunc, middleware: userMiddleware } = config;
114
133
  const userFuncConfig = userFunc;
115
134
  return async (services, data, wire) => {
135
+ const adapter = await resolveGatewayAdapter(config, services);
116
136
  // Check for POST-based webhook verification (e.g. Slack url_verification)
117
137
  if (adapter.verifyWebhook) {
118
138
  const verifyResult = await adapter.verifyWebhook(data, wire.http?.request);
@@ -157,8 +177,9 @@ const createWebhookPostHandler = (config) => {
157
177
  * Creates the GET handler for webhook verification challenges.
158
178
  * Passes query parameters to the adapter's verifyWebhook method.
159
179
  */
160
- const createWebhookVerifyHandler = (adapter) => {
161
- return async (_services, _data, wire) => {
180
+ const createWebhookVerifyHandler = (config) => {
181
+ return async (services, _data, wire) => {
182
+ const adapter = await resolveGatewayAdapter(config, services);
162
183
  if (!adapter.verifyWebhook) {
163
184
  return { error: 'Verification not supported' };
164
185
  }
@@ -174,7 +195,7 @@ const createWebhookVerifyHandler = (adapter) => {
174
195
  // WebSocket gateway — client connects via WebSocket
175
196
  // ---------------------------------------------------------------------------
176
197
  const wireWebsocketGateway = (config) => {
177
- const { name, route, adapter } = config;
198
+ const { name, route } = config;
178
199
  if (!route) {
179
200
  throw new Error(`WebSocket gateway '${name}' requires a route`);
180
201
  }
@@ -211,8 +232,8 @@ const wireWebsocketGateway = (config) => {
211
232
  // Register onConnect
212
233
  addFunction(connectFuncId, {
213
234
  auth: false,
214
- func: async (_services, _data, wire) => {
215
- ;
235
+ func: async (services, _data, wire) => {
236
+ const adapter = await resolveGatewayAdapter(config, services);
216
237
  wire.gateway = {
217
238
  gatewayName: name,
218
239
  senderId: '',
@@ -227,6 +248,7 @@ const wireWebsocketGateway = (config) => {
227
248
  addFunction(messageFuncId, {
228
249
  auth: false,
229
250
  func: async (services, data, wire) => {
251
+ const adapter = await resolveGatewayAdapter(config, services);
230
252
  const parsed = adapter.parse(data);
231
253
  if (!parsed)
232
254
  return;
@@ -290,10 +312,10 @@ const wireListenerGateway = (config) => {
290
312
  * @param singletonServices - Singleton services to pass to handler/middleware
291
313
  */
292
314
  export const createListenerMessageHandler = (name, config, singletonServices) => {
293
- const { adapter } = config;
294
315
  const userFuncConfig = config.func;
295
316
  const userMiddleware = config.middleware;
296
317
  return async (rawData) => {
318
+ const adapter = await resolveGatewayAdapter(config, singletonServices);
297
319
  const parsed = adapter.parse(rawData);
298
320
  if (!parsed)
299
321
  return;
@@ -1,4 +1,4 @@
1
- import type { CommonWireMeta, CorePikkuMiddleware, CorePikkuMiddlewareGroup } from '../../types/core.types.js';
1
+ import type { CommonWireMeta, CorePikkuMiddleware, CorePikkuMiddlewareGroup, CoreSingletonServices } from '../../types/core.types.js';
2
2
  import type { CorePikkuFunctionConfig, CorePermissionGroup, CorePikkuPermission } from '../../function/functions.types.js';
3
3
  import type { PikkuHTTPRequest } from '../http/http.types.js';
4
4
  /**
@@ -69,6 +69,16 @@ export interface GatewayAdapter {
69
69
  * Receives the data (body or query params) and the Pikku HTTP request for additional inspection. */
70
70
  verifyWebhook?(data: unknown, request?: PikkuHTTPRequest): WebhookVerificationResult | Promise<WebhookVerificationResult>;
71
71
  }
72
+ /**
73
+ * Factory that builds a GatewayAdapter from singleton services.
74
+ *
75
+ * Real platform adapters (WhatsApp Cloud API, Slack, …) need secrets or
76
+ * services that only exist after boot, while `wireGateway` runs at module
77
+ * load. Pass a factory instead of an instance and it is resolved lazily on
78
+ * the first inbound request (webhook/websocket) or on gateway start
79
+ * (listener), then cached for the lifetime of the gateway.
80
+ */
81
+ export type GatewayAdapterFactory = (services: CoreSingletonServices) => GatewayAdapter | Promise<GatewayAdapter>;
72
82
  /**
73
83
  * The gateway wire object available on wire.gateway inside handler functions and middleware
74
84
  */
@@ -100,8 +110,9 @@ export type CoreGateway<PikkuFunctionConfig = CorePikkuFunctionConfig<any, any>,
100
110
  /** HTTP route for webhook/websocket types */
101
111
  route?: string;
102
112
  platform?: string;
103
- /** The gateway adapter (parse inbound, send outbound) */
104
- adapter: GatewayAdapter;
113
+ /** The gateway adapter (parse inbound, send outbound), or a factory
114
+ * resolved lazily from singleton services (for adapters needing secrets) */
115
+ adapter: GatewayAdapter | GatewayAdapterFactory;
105
116
  /** The handler function that processes parsed messages */
106
117
  func: PikkuFunctionConfig;
107
118
  /** Optional middleware chain (e.g., auth) */
@@ -1,2 +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';
1
+ export { wireGateway, createListenerMessageHandler, resolveGatewayAdapter, } from './gateway-runner.js';
2
+ export type { GatewayAdapter, GatewayAdapterFactory, GatewayAttachment, GatewayInboundMessage, GatewayOutboundMessage, GatewayMeta, GatewaysMeta, GatewayTransportType, CoreGateway, PikkuGateway, WebhookVerificationResult, } from './gateway.types.js';
@@ -1 +1 @@
1
- export { wireGateway, createListenerMessageHandler } from './gateway-runner.js';
1
+ export { wireGateway, createListenerMessageHandler, resolveGatewayAdapter, } from './gateway-runner.js';
@@ -441,6 +441,9 @@ export async function executeGraphStep(workflowService, rpcService, runId, stepI
441
441
  try {
442
442
  let result;
443
443
  const subWorkflowMeta = pikkuState(null, 'workflows', 'meta')[rpcName];
444
+ const agentMeta = subWorkflowMeta
445
+ ? undefined
446
+ : pikkuState(null, 'agent', 'agentsMeta')[rpcName];
444
447
  if (subWorkflowMeta) {
445
448
  const childWire = {
446
449
  type: 'workflow',
@@ -465,6 +468,10 @@ export async function executeGraphStep(workflowService, rpcService, runId, stepI
465
468
  throw new ChildWorkflowStartedException(runId, stepId, childRunId);
466
469
  }
467
470
  }
471
+ else if (agentMeta) {
472
+ const agentRun = await rpcService.agent.run(rpcName, data);
473
+ result = agentRun.result;
474
+ }
468
475
  else {
469
476
  result = await invokeGraphNodeRpc(workflowService, rpcService, runId, stepId, nodeId, rpcName, data, graphName);
470
477
  }
@@ -523,6 +530,9 @@ async function executeGraphNodeInline(workflowService, rpcService, runId, graphN
523
530
  try {
524
531
  let result;
525
532
  const subWorkflowMeta = pikkuState(null, 'workflows', 'meta')[rpcName];
533
+ const agentMeta = subWorkflowMeta
534
+ ? undefined
535
+ : pikkuState(null, 'agent', 'agentsMeta')[rpcName];
526
536
  if (subWorkflowMeta) {
527
537
  const childWire = {
528
538
  type: 'workflow',
@@ -541,6 +551,10 @@ async function executeGraphNodeInline(workflowService, rpcService, runId, graphN
541
551
  }
542
552
  result = childRun?.output;
543
553
  }
554
+ else if (agentMeta) {
555
+ const agentRun = await rpcService.agent.run(rpcName, input);
556
+ result = agentRun.result;
557
+ }
544
558
  else {
545
559
  result = await invokeGraphNodeRpc(workflowService, rpcService, runId, stepState.stepId, nodeId, rpcName, input, graphName);
546
560
  }
@@ -1082,7 +1082,11 @@ export class PikkuWorkflowService {
1082
1082
  * that differs between transports is who calls it, not the call itself.
1083
1083
  */
1084
1084
  async invokeStepRpc(runId, stepName, stepState, rpcName, data, rpcService) {
1085
+ // Carry the run's pikkuUserId onto the step wire so authed steps rehydrate their
1086
+ // session on the queued path too (the bare job wire lacks it; inline already has it).
1087
+ const run = await this.getRun(runId);
1085
1088
  return rpcService.rpcWithWire(rpcName, data, {
1089
+ ...(run?.wire?.pikkuUserId ? { pikkuUserId: run.wire.pikkuUserId } : {}),
1086
1090
  workflowStep: {
1087
1091
  runId,
1088
1092
  stepId: stepState.stepId,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pikku/core",
3
- "version": "0.12.57",
3
+ "version": "0.12.60",
4
4
  "author": "yasser.fadl@gmail.com",
5
5
  "license": "MIT",
6
6
  "module": "dist/index.js",
@@ -72,6 +72,6 @@
72
72
  "typescript": "^6.0.3"
73
73
  },
74
74
  "engines": {
75
- "node": ">=22"
75
+ "node": ">=24"
76
76
  }
77
77
  }
@@ -1,7 +1,10 @@
1
1
  import type { GatewayService } from './gateway-service.js'
2
2
  import type { GatewayAdapter } from '../wirings/gateway/gateway.types.js'
3
3
  import { pikkuState, getSingletonServices } from '../pikku-state.js'
4
- import { createListenerMessageHandler } from '../wirings/gateway/gateway-runner.js'
4
+ import {
5
+ createListenerMessageHandler,
6
+ resolveGatewayAdapter,
7
+ } from '../wirings/gateway/gateway-runner.js'
5
8
 
6
9
  /**
7
10
  * Local GatewayService implementation.
@@ -36,8 +39,9 @@ export class LocalGatewayService implements GatewayService {
36
39
  singletonServices
37
40
  )
38
41
 
39
- await config.adapter.init(handleMessage)
40
- this.activeAdapters.set(name, config.adapter)
42
+ const adapter = await resolveGatewayAdapter(config, singletonServices)
43
+ await adapter.init(handleMessage)
44
+ this.activeAdapters.set(name, adapter)
41
45
  singletonServices.logger.info(`Started listener gateway: ${name}`)
42
46
  }
43
47
  }
@@ -139,6 +139,76 @@ describe('wireGateway', () => {
139
139
  assert.equal(adapter.sentMessages[0].message.text, 'reply')
140
140
  })
141
141
 
142
+ test('adapter factory resolves lazily from services and is cached', async () => {
143
+ const adapter = createMockAdapter({ name: 'factory-mock' })
144
+ const factoryCalls: any[] = []
145
+
146
+ wireGateway({
147
+ name: 'test-factory',
148
+ type: 'webhook',
149
+ route: '/webhooks/factory',
150
+ adapter: (services: any) => {
151
+ factoryCalls.push(services)
152
+ return adapter
153
+ },
154
+ func: {
155
+ func: async () => ({ text: 'pong' }),
156
+ },
157
+ })
158
+
159
+ httpRouter.initialize()
160
+
161
+ // Factory must NOT run at wiring time
162
+ assert.equal(factoryCalls.length, 0)
163
+
164
+ const makeRequest = () =>
165
+ fetch(
166
+ new Request('http://localhost/webhooks/factory', {
167
+ method: 'POST',
168
+ headers: { 'Content-Type': 'application/json' },
169
+ body: JSON.stringify({ senderId: 'user-9', text: 'ping' }),
170
+ })
171
+ )
172
+
173
+ const first = await makeRequest()
174
+ assert.equal(first.status, 200)
175
+ const second = await makeRequest()
176
+ assert.equal(second.status, 200)
177
+
178
+ // Factory ran exactly once, with the singleton services
179
+ assert.equal(factoryCalls.length, 1)
180
+ assert.equal(typeof factoryCalls[0].logger.info, 'function')
181
+
182
+ // Messages flowed through the resolved adapter (auto-send reply)
183
+ assert.equal(adapter.sentMessages.length, 2)
184
+ assert.equal(adapter.sentMessages[0].message.text, 'pong')
185
+ })
186
+
187
+ test('factory adapter registers GET verify route unconditionally', async () => {
188
+ const adapter = createMockAdapter({
189
+ verifyResult: { verified: true, response: { challenge: 'abc' } },
190
+ })
191
+
192
+ wireGateway({
193
+ name: 'test-factory-verify',
194
+ type: 'webhook',
195
+ route: '/webhooks/factory-verify',
196
+ adapter: async () => adapter,
197
+ func: { func: async () => {} },
198
+ })
199
+
200
+ httpRouter.initialize()
201
+
202
+ const response = await fetch(
203
+ new Request('http://localhost/webhooks/factory-verify?token=x', {
204
+ method: 'GET',
205
+ })
206
+ )
207
+ assert.equal(response.status, 200)
208
+ const body = await response.json()
209
+ assert.deepEqual(body, { challenge: 'abc' })
210
+ })
211
+
142
212
  test('returns 200 OK for ignored events (adapter returns null)', async () => {
143
213
  const adapter = createMockAdapter({ parseResult: null })
144
214
  const funcCalls: any[] = []
@@ -8,6 +8,28 @@ import type {
8
8
  GatewayOutboundMessage,
9
9
  PikkuGateway,
10
10
  } from './gateway.types.js'
11
+
12
+ /**
13
+ * Lazily resolve a gateway's adapter. Factories are invoked once with the
14
+ * singleton services and cached (promise-cached, so concurrent first
15
+ * requests share one construction).
16
+ */
17
+ const resolvedAdapters = new WeakMap<CoreGateway, Promise<GatewayAdapter>>()
18
+
19
+ export const resolveGatewayAdapter = (
20
+ config: CoreGateway,
21
+ services: CoreSingletonServices
22
+ ): Promise<GatewayAdapter> => {
23
+ let resolved = resolvedAdapters.get(config)
24
+ if (!resolved) {
25
+ resolved =
26
+ typeof config.adapter === 'function'
27
+ ? Promise.resolve(config.adapter(services))
28
+ : Promise.resolve(config.adapter)
29
+ resolvedAdapters.set(config, resolved)
30
+ }
31
+ return resolved
32
+ }
11
33
  import type {
12
34
  PikkuWire,
13
35
  PikkuRawWire,
@@ -94,8 +116,10 @@ const wireWebhookGateway = (config: CoreGateway): void => {
94
116
  } as any)
95
117
 
96
118
  // --- GET handler (webhook verification, e.g. WhatsApp challenge) ---------
119
+ // Factory adapters can't be probed for verifyWebhook until first resolve,
120
+ // so register the GET route unconditionally for them.
97
121
 
98
- if (adapter.verifyWebhook) {
122
+ if (typeof adapter === 'function' || adapter.verifyWebhook) {
99
123
  const verifyFuncId = `gateway__${name}__verify`
100
124
 
101
125
  funcMeta[verifyFuncId] = {
@@ -113,7 +137,7 @@ const wireWebhookGateway = (config: CoreGateway): void => {
113
137
 
114
138
  const verifyHandler = {
115
139
  auth: false,
116
- func: createWebhookVerifyHandler(adapter),
140
+ func: createWebhookVerifyHandler(config),
117
141
  }
118
142
 
119
143
  addFunction(verifyFuncId, verifyHandler as any)
@@ -145,7 +169,7 @@ const wireWebhookGateway = (config: CoreGateway): void => {
145
169
  * 6. Auto-send response via adapter if func returns outbound content
146
170
  */
147
171
  const createWebhookPostHandler = (config: CoreGateway) => {
148
- const { name, adapter, func: userFunc, middleware: userMiddleware } = config
172
+ const { name, func: userFunc, middleware: userMiddleware } = config
149
173
  const userFuncConfig = userFunc as {
150
174
  func: Function
151
175
  middleware?: CorePikkuMiddleware[]
@@ -156,6 +180,8 @@ const createWebhookPostHandler = (config: CoreGateway) => {
156
180
  data: unknown,
157
181
  wire: PikkuWire
158
182
  ) => {
183
+ const adapter = await resolveGatewayAdapter(config, services)
184
+
159
185
  // Check for POST-based webhook verification (e.g. Slack url_verification)
160
186
  if (adapter.verifyWebhook) {
161
187
  const verifyResult = await adapter.verifyWebhook(data, wire.http?.request)
@@ -207,12 +233,13 @@ const createWebhookPostHandler = (config: CoreGateway) => {
207
233
  * Creates the GET handler for webhook verification challenges.
208
234
  * Passes query parameters to the adapter's verifyWebhook method.
209
235
  */
210
- const createWebhookVerifyHandler = (adapter: GatewayAdapter) => {
236
+ const createWebhookVerifyHandler = (config: CoreGateway) => {
211
237
  return async (
212
- _services: CoreSingletonServices,
238
+ services: CoreSingletonServices,
213
239
  _data: unknown,
214
240
  wire: PikkuWire
215
241
  ) => {
242
+ const adapter = await resolveGatewayAdapter(config, services)
216
243
  if (!adapter.verifyWebhook) {
217
244
  return { error: 'Verification not supported' }
218
245
  }
@@ -232,7 +259,7 @@ const createWebhookVerifyHandler = (adapter: GatewayAdapter) => {
232
259
  // ---------------------------------------------------------------------------
233
260
 
234
261
  const wireWebsocketGateway = (config: CoreGateway): void => {
235
- const { name, route, adapter } = config
262
+ const { name, route } = config
236
263
  if (!route) {
237
264
  throw new Error(`WebSocket gateway '${name}' requires a route`)
238
265
  }
@@ -279,7 +306,8 @@ const wireWebsocketGateway = (config: CoreGateway): void => {
279
306
  // Register onConnect
280
307
  addFunction(connectFuncId, {
281
308
  auth: false,
282
- func: async (_services: any, _data: unknown, wire: PikkuWire) => {
309
+ func: async (services: any, _data: unknown, wire: PikkuWire) => {
310
+ const adapter = await resolveGatewayAdapter(config, services)
283
311
  ;(wire as any).gateway = {
284
312
  gatewayName: name,
285
313
  senderId: '',
@@ -295,6 +323,7 @@ const wireWebsocketGateway = (config: CoreGateway): void => {
295
323
  addFunction(messageFuncId, {
296
324
  auth: false,
297
325
  func: async (services: any, data: unknown, wire: PikkuWire) => {
326
+ const adapter = await resolveGatewayAdapter(config, services)
298
327
  const parsed = adapter.parse(data)
299
328
  if (!parsed) return
300
329
 
@@ -371,7 +400,6 @@ export const createListenerMessageHandler = (
371
400
  config: CoreGateway,
372
401
  singletonServices: CoreSingletonServices
373
402
  ): ((rawData: unknown) => Promise<void>) => {
374
- const { adapter } = config
375
403
  const userFuncConfig = config.func as {
376
404
  func: Function
377
405
  middleware?: CorePikkuMiddleware[]
@@ -379,6 +407,7 @@ export const createListenerMessageHandler = (
379
407
  const userMiddleware = config.middleware as CorePikkuMiddleware[] | undefined
380
408
 
381
409
  return async (rawData: unknown): Promise<void> => {
410
+ const adapter = await resolveGatewayAdapter(config, singletonServices)
382
411
  const parsed = adapter.parse(rawData)
383
412
  if (!parsed) return
384
413
 
@@ -2,6 +2,7 @@ import type {
2
2
  CommonWireMeta,
3
3
  CorePikkuMiddleware,
4
4
  CorePikkuMiddlewareGroup,
5
+ CoreSingletonServices,
5
6
  } from '../../types/core.types.js'
6
7
  import type {
7
8
  CorePikkuFunctionConfig,
@@ -83,6 +84,19 @@ export interface GatewayAdapter {
83
84
  ): WebhookVerificationResult | Promise<WebhookVerificationResult>
84
85
  }
85
86
 
87
+ /**
88
+ * Factory that builds a GatewayAdapter from singleton services.
89
+ *
90
+ * Real platform adapters (WhatsApp Cloud API, Slack, …) need secrets or
91
+ * services that only exist after boot, while `wireGateway` runs at module
92
+ * load. Pass a factory instead of an instance and it is resolved lazily on
93
+ * the first inbound request (webhook/websocket) or on gateway start
94
+ * (listener), then cached for the lifetime of the gateway.
95
+ */
96
+ export type GatewayAdapterFactory = (
97
+ services: CoreSingletonServices
98
+ ) => GatewayAdapter | Promise<GatewayAdapter>
99
+
86
100
  /**
87
101
  * The gateway wire object available on wire.gateway inside handler functions and middleware
88
102
  */
@@ -122,8 +136,9 @@ export type CoreGateway<
122
136
  /** HTTP route for webhook/websocket types */
123
137
  route?: string
124
138
  platform?: string
125
- /** The gateway adapter (parse inbound, send outbound) */
126
- adapter: GatewayAdapter
139
+ /** The gateway adapter (parse inbound, send outbound), or a factory
140
+ * resolved lazily from singleton services (for adapters needing secrets) */
141
+ adapter: GatewayAdapter | GatewayAdapterFactory
127
142
  /** The handler function that processes parsed messages */
128
143
  func: PikkuFunctionConfig
129
144
  /** Optional middleware chain (e.g., auth) */
@@ -1,6 +1,11 @@
1
- export { wireGateway, createListenerMessageHandler } from './gateway-runner.js'
1
+ export {
2
+ wireGateway,
3
+ createListenerMessageHandler,
4
+ resolveGatewayAdapter,
5
+ } from './gateway-runner.js'
2
6
  export type {
3
7
  GatewayAdapter,
8
+ GatewayAdapterFactory,
4
9
  GatewayAttachment,
5
10
  GatewayInboundMessage,
6
11
  GatewayOutboundMessage,