@pikku/core 0.12.20 → 0.12.22

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 (141) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/dist/dev/hot-reload.js +20 -6
  3. package/dist/errors/error-handler.d.ts +1 -1
  4. package/dist/errors/error-handler.js +2 -2
  5. package/dist/function/functions.types.d.ts +3 -2
  6. package/dist/handle-error.js +3 -1
  7. package/dist/index.d.ts +2 -2
  8. package/dist/index.js +2 -2
  9. package/dist/middleware/index.d.ts +2 -2
  10. package/dist/middleware/index.js +2 -2
  11. package/dist/middleware/telemetry.d.ts +2 -2
  12. package/dist/middleware/telemetry.js +2 -2
  13. package/dist/middleware-runner.d.ts +15 -27
  14. package/dist/middleware-runner.js +25 -30
  15. package/dist/permissions.d.ts +15 -22
  16. package/dist/permissions.js +30 -25
  17. package/dist/pikku-request.js +1 -1
  18. package/dist/pikku-state.js +2 -3
  19. package/dist/services/ai-agent-runner-service.d.ts +1 -0
  20. package/dist/services/in-memory-queue-service.js +1 -1
  21. package/dist/services/in-memory-workflow-service.d.ts +15 -13
  22. package/dist/services/in-memory-workflow-service.js +31 -13
  23. package/dist/services/local-content.js +8 -1
  24. package/dist/services/user-session-service.d.ts +1 -1
  25. package/dist/testing/service-tests.js +24 -0
  26. package/dist/types/core.types.d.ts +4 -0
  27. package/dist/types/state.types.d.ts +2 -18
  28. package/dist/wirings/ai-agent/ai-agent-memory.d.ts +24 -5
  29. package/dist/wirings/ai-agent/ai-agent-memory.js +128 -23
  30. package/dist/wirings/ai-agent/ai-agent-model-config.d.ts +10 -1
  31. package/dist/wirings/ai-agent/ai-agent-model-config.js +15 -35
  32. package/dist/wirings/ai-agent/ai-agent-prepare.js +4 -1
  33. package/dist/wirings/ai-agent/ai-agent-runner.js +45 -31
  34. package/dist/wirings/ai-agent/ai-agent-stream.js +63 -34
  35. package/dist/wirings/ai-agent/ai-agent.types.d.ts +20 -0
  36. package/dist/wirings/channel/channel-handler.js +1 -1
  37. package/dist/wirings/channel/channel-store.d.ts +13 -0
  38. package/dist/wirings/channel/channel.types.d.ts +3 -0
  39. package/dist/wirings/channel/local/local-channel-runner.js +23 -5
  40. package/dist/wirings/channel/pikku-abstract-channel-handler.js +8 -0
  41. package/dist/wirings/channel/serverless/serverless-channel-runner.js +9 -0
  42. package/dist/wirings/cli/cli-runner.js +24 -5
  43. package/dist/wirings/cli/command-parser.js +19 -4
  44. package/dist/wirings/http/http-runner.js +72 -36
  45. package/dist/wirings/http/http.types.d.ts +1 -0
  46. package/dist/wirings/http/pikku-fetch-http-request.js +3 -1
  47. package/dist/wirings/http/web-request.js +32 -0
  48. package/dist/wirings/rpc/rpc-runner.js +13 -3
  49. package/dist/wirings/workflow/graph/graph-node.d.ts +8 -0
  50. package/dist/wirings/workflow/graph/graph-node.js +4 -0
  51. package/dist/wirings/workflow/graph/graph-runner.js +12 -10
  52. package/dist/wirings/workflow/graph/workflow-graph.types.d.ts +4 -0
  53. package/dist/wirings/workflow/index.d.ts +1 -1
  54. package/dist/wirings/workflow/pikku-workflow-service.d.ts +81 -16
  55. package/dist/wirings/workflow/pikku-workflow-service.js +266 -45
  56. package/dist/wirings/workflow/workflow.types.d.ts +34 -0
  57. package/package.json +1 -1
  58. package/run-tests.sh +4 -1
  59. package/src/dev/hot-reload.test.ts +62 -11
  60. package/src/dev/hot-reload.ts +28 -7
  61. package/src/errors/error-handler.ts +8 -2
  62. package/src/errors/error.test.ts +2 -0
  63. package/src/function/function-runner.test.ts +500 -10
  64. package/src/function/functions.types.ts +3 -2
  65. package/src/handle-error.test.ts +1 -1
  66. package/src/handle-error.ts +4 -1
  67. package/src/index.ts +12 -2
  68. package/src/middleware/index.ts +11 -2
  69. package/src/middleware/telemetry.ts +2 -2
  70. package/src/middleware-runner.test.ts +16 -16
  71. package/src/middleware-runner.ts +42 -30
  72. package/src/permissions.test.ts +27 -24
  73. package/src/permissions.ts +41 -25
  74. package/src/pikku-request.test.ts +35 -0
  75. package/src/pikku-request.ts +1 -1
  76. package/src/pikku-state.ts +2 -3
  77. package/src/run-tests-script.test.ts +18 -0
  78. package/src/services/ai-agent-runner-service.ts +1 -0
  79. package/src/services/in-memory-queue-service.ts +1 -1
  80. package/src/services/in-memory-session-store.ts +1 -2
  81. package/src/services/in-memory-workflow-service.test.ts +33 -11
  82. package/src/services/in-memory-workflow-service.ts +49 -13
  83. package/src/services/local-content.test.ts +54 -0
  84. package/src/services/local-content.ts +12 -1
  85. package/src/services/typed-credential-service.ts +3 -3
  86. package/src/services/typed-secret-service.ts +3 -3
  87. package/src/services/typed-variables-service.ts +3 -3
  88. package/src/services/user-session-service.ts +4 -4
  89. package/src/testing/service-tests.ts +30 -0
  90. package/src/types/core.types.ts +6 -2
  91. package/src/types/state.types.ts +2 -13
  92. package/src/wirings/ai-agent/ai-agent-memory.test.ts +324 -0
  93. package/src/wirings/ai-agent/ai-agent-memory.ts +187 -36
  94. package/src/wirings/ai-agent/ai-agent-model-config.test.ts +12 -90
  95. package/src/wirings/ai-agent/ai-agent-model-config.ts +14 -38
  96. package/src/wirings/ai-agent/ai-agent-prepare.test.ts +292 -0
  97. package/src/wirings/ai-agent/ai-agent-prepare.ts +4 -1
  98. package/src/wirings/ai-agent/ai-agent-registry.test.ts +230 -3
  99. package/src/wirings/ai-agent/ai-agent-runner.test.ts +625 -6
  100. package/src/wirings/ai-agent/ai-agent-runner.ts +65 -50
  101. package/src/wirings/ai-agent/ai-agent-stream.test.ts +544 -5
  102. package/src/wirings/ai-agent/ai-agent-stream.ts +71 -69
  103. package/src/wirings/ai-agent/ai-agent.types.ts +24 -0
  104. package/src/wirings/channel/channel-handler.test.ts +272 -0
  105. package/src/wirings/channel/channel-handler.ts +1 -1
  106. package/src/wirings/channel/channel-middleware-runner.test.ts +163 -0
  107. package/src/wirings/channel/channel-store.ts +19 -0
  108. package/src/wirings/channel/channel.types.ts +4 -0
  109. package/src/wirings/channel/local/local-channel-runner.test.ts +63 -0
  110. package/src/wirings/channel/local/local-channel-runner.ts +41 -5
  111. package/src/wirings/channel/local/local-eventhub-service.ts +3 -3
  112. package/src/wirings/channel/pikku-abstract-channel-handler.ts +9 -2
  113. package/src/wirings/channel/serverless/serverless-channel-runner.ts +23 -5
  114. package/src/wirings/cli/channel/cli-raw-channel-runner.ts +7 -2
  115. package/src/wirings/cli/cli-runner.test.ts +255 -2
  116. package/src/wirings/cli/cli-runner.ts +31 -10
  117. package/src/wirings/cli/cli.types.ts +4 -8
  118. package/src/wirings/cli/command-parser.test.ts +83 -0
  119. package/src/wirings/cli/command-parser.ts +23 -4
  120. package/src/wirings/http/http-runner.test.ts +296 -1
  121. package/src/wirings/http/http-runner.ts +87 -57
  122. package/src/wirings/http/http.types.ts +11 -26
  123. package/src/wirings/http/pikku-fetch-http-request.ts +8 -4
  124. package/src/wirings/http/pikku-fetch-http-response.test.ts +41 -0
  125. package/src/wirings/http/web-request.test.ts +115 -0
  126. package/src/wirings/http/web-request.ts +43 -5
  127. package/src/wirings/mcp/mcp-runner.test.ts +367 -0
  128. package/src/wirings/queue/queue.types.ts +2 -5
  129. package/src/wirings/rpc/rpc-runner.test.ts +511 -0
  130. package/src/wirings/rpc/rpc-runner.ts +15 -3
  131. package/src/wirings/rpc/wire-addon.test.ts +57 -0
  132. package/src/wirings/workflow/graph/graph-node.ts +12 -0
  133. package/src/wirings/workflow/graph/graph-runner.test.ts +98 -0
  134. package/src/wirings/workflow/graph/graph-runner.ts +28 -10
  135. package/src/wirings/workflow/graph/workflow-graph.types.ts +4 -0
  136. package/src/wirings/workflow/index.ts +1 -0
  137. package/src/wirings/workflow/pikku-workflow-service.test.ts +19 -2
  138. package/src/wirings/workflow/pikku-workflow-service.ts +370 -71
  139. package/src/wirings/workflow/workflow-queue-workers.test.ts +131 -0
  140. package/src/wirings/workflow/workflow.types.ts +68 -5
  141. package/tsconfig.tsbuildinfo +1 -1
@@ -23,11 +23,10 @@ import type {
23
23
  } from '../../services/ai-agent-runner-service.js'
24
24
 
25
25
  import {
26
- parseWorkingMemory,
27
- deepMergeWorkingMemory,
28
26
  resolveMemoryServices,
29
27
  loadContextMessages,
30
28
  trimMessages,
29
+ getWorkingMemoryMiddleware,
31
30
  } from './ai-agent-memory.js'
32
31
  import {
33
32
  prepareAgentRun,
@@ -58,6 +57,7 @@ function createPersistingChannel(
58
57
  ): PersistingChannel {
59
58
  let fullText = ''
60
59
  let stepText = ''
60
+ let stepGenerativeUI: unknown | null = null
61
61
  let stepToolCalls: AIToolCall[] = []
62
62
  let stepToolResults: AIToolResult[] = []
63
63
  const totalUsage: {
@@ -72,16 +72,24 @@ function createPersistingChannel(
72
72
  const flushStep = async () => {
73
73
  if (!storage) return
74
74
  const text = stepText
75
+ const generativeUI = stepGenerativeUI
75
76
  const calls = stepToolCalls
76
77
  const results = stepToolResults
77
78
  stepText = ''
79
+ stepGenerativeUI = null
78
80
  stepToolCalls = []
79
81
  stepToolResults = []
80
- if (text || calls.length > 0) {
82
+ if (text || generativeUI != null || calls.length > 0) {
81
83
  const assistantMsg: AIMessage = {
82
84
  id: randomUUID(),
83
85
  role: 'assistant',
84
- content: text || undefined,
86
+ content:
87
+ generativeUI != null
88
+ ? [
89
+ ...(text ? [{ type: 'text' as const, text }] : []),
90
+ { type: 'generative-ui' as const, spec: generativeUI },
91
+ ]
92
+ : text || undefined,
85
93
  toolCalls: calls.length > 0 ? calls : undefined,
86
94
  createdAt: new Date(),
87
95
  }
@@ -137,6 +145,9 @@ function createPersistingChannel(
137
145
  : JSON.stringify(event.result),
138
146
  })
139
147
  break
148
+ case 'generative-ui':
149
+ stepGenerativeUI = event.spec
150
+ break
140
151
  case 'usage':
141
152
  totalUsage.inputTokens += event.tokens.input
142
153
  totalUsage.outputTokens += event.tokens.output
@@ -150,6 +161,9 @@ function createPersistingChannel(
150
161
  }
151
162
  parent.send(event)
152
163
  },
164
+ setState: (s) => parent.setState(s),
165
+ getState: () => parent.getState(),
166
+ clearState: () => parent.clearState(),
153
167
  }
154
168
  return channel
155
169
  }
@@ -158,10 +172,6 @@ async function postStreamCleanup(
158
172
  persistingChannel: PersistingChannel,
159
173
  aiMiddlewares: PikkuAIMiddlewareHooks[],
160
174
  singletonServices: any,
161
- storage: AIStorageService | undefined,
162
- memoryConfig: any,
163
- threadId: string,
164
- workingMemorySchemaName: string | null,
165
175
  messages: AIMessage[],
166
176
  aiRunState: AIRunStateService,
167
177
  runId: string
@@ -185,30 +195,6 @@ async function postStreamCleanup(
185
195
  }
186
196
  }
187
197
 
188
- if (storage && memoryConfig?.workingMemory && outputText) {
189
- const parsed = parseWorkingMemory(outputText)
190
- if (parsed) {
191
- const existing =
192
- (await storage.getWorkingMemory(threadId, 'thread')) ?? {}
193
- const merged = deepMergeWorkingMemory(existing, parsed)
194
-
195
- if (singletonServices.schema && workingMemorySchemaName) {
196
- try {
197
- await singletonServices.schema.validateSchema(
198
- workingMemorySchemaName,
199
- merged
200
- )
201
- } catch (err) {
202
- singletonServices.logger.warn(
203
- `Working memory validation failed: ${err instanceof Error ? err.message : String(err)}`
204
- )
205
- }
206
- }
207
-
208
- await storage.saveWorkingMemory(threadId, 'thread', merged)
209
- }
210
- }
211
-
212
198
  await aiRunState.updateRun(runId, {
213
199
  status: 'completed',
214
200
  ...(usage.model
@@ -228,6 +214,7 @@ type StepLoopParams = {
228
214
  runnerParams: AIAgentRunnerParams
229
215
  maxSteps: number
230
216
  agentRunner: AIAgentRunnerService
217
+ streamChannel: AIStreamChannel
231
218
  persistingChannel: PersistingChannel
232
219
  channel: AIStreamChannel
233
220
  runId: string
@@ -247,7 +234,7 @@ async function runStreamStepLoop(
247
234
  runnerParams,
248
235
  maxSteps,
249
236
  agentRunner,
250
- persistingChannel,
237
+ streamChannel,
251
238
  channel,
252
239
  runId,
253
240
  aiMiddlewares,
@@ -273,7 +260,7 @@ async function runStreamStepLoop(
273
260
 
274
261
  channel.send({ type: 'step-start', stepNumber: step })
275
262
 
276
- const stepResult = await agentRunner.stream(runnerParams, persistingChannel)
263
+ const stepResult = await agentRunner.stream(runnerParams, streamChannel)
277
264
 
278
265
  for (const mw of aiMiddlewares) {
279
266
  if (mw.afterStep) {
@@ -433,10 +420,24 @@ export function appendStepMessages(
433
420
  runnerParams: AIAgentRunnerParams,
434
421
  stepResult: AIAgentStepResult
435
422
  ): void {
423
+ const structuredOutput =
424
+ stepResult.object && typeof stepResult.object === 'object'
425
+ ? (stepResult.object as Record<string, unknown>)
426
+ : null
427
+ const assistantContent =
428
+ structuredOutput?.ui != null
429
+ ? [
430
+ ...(stepResult.text
431
+ ? [{ type: 'text' as const, text: stepResult.text }]
432
+ : []),
433
+ { type: 'generative-ui' as const, spec: structuredOutput.ui },
434
+ ]
435
+ : stepResult.text || undefined
436
+
436
437
  const assistantMsg: AIMessage = {
437
438
  id: randomUUID(),
438
439
  role: 'assistant',
439
- content: stepResult.text || undefined,
440
+ content: assistantContent,
440
441
  toolCalls:
441
442
  stepResult.toolCalls.length > 0
442
443
  ? stepResult.toolCalls.map((tc) => ({
@@ -445,6 +446,9 @@ export function appendStepMessages(
445
446
  args: tc.args as Record<string, unknown>,
446
447
  }))
447
448
  : undefined,
449
+ ...(stepResult.reasoningContent
450
+ ? { reasoningContent: stepResult.reasoningContent }
451
+ : {}),
448
452
  createdAt: new Date(),
449
453
  }
450
454
  runnerParams.messages.push(assistantMsg)
@@ -614,7 +618,15 @@ export async function streamAIAgent(
614
618
  return ''
615
619
  }
616
620
 
617
- const aiMiddlewares: PikkuAIMiddlewareHooks[] = agent.aiMiddleware ?? []
621
+ const aiMiddlewares: PikkuAIMiddlewareHooks[] = [
622
+ ...getWorkingMemoryMiddleware(memoryConfig, storage, {
623
+ threadId,
624
+ workingMemorySchemaName,
625
+ logger: singletonServices.logger,
626
+ schemaService: singletonServices.schema,
627
+ }),
628
+ ...(agent.aiMiddleware ?? []),
629
+ ]
618
630
 
619
631
  let modifiedMessages = runnerParams.messages
620
632
  let modifiedInstructions = runnerParams.instructions
@@ -680,14 +692,16 @@ export async function streamAIAgent(
680
692
  }
681
693
  )
682
694
 
695
+ const persistingChannel = createPersistingChannel(channel, storage, threadId)
696
+
683
697
  const wrappedChannel =
684
698
  allChannelMiddleware.length > 0
685
699
  ? (wrapChannelWithMiddleware(
686
- { channel },
700
+ { channel: persistingChannel },
687
701
  singletonServices,
688
702
  allChannelMiddleware
689
703
  ).channel as AIStreamChannel)
690
- : channel
704
+ : persistingChannel
691
705
 
692
706
  // In delegate mode (default), suppress parent's text from reaching the client
693
707
  // AFTER a sub-agent has been called. If the parent responds directly (no delegation),
@@ -713,18 +727,13 @@ export async function streamAIAgent(
713
727
  }
714
728
  : wrappedChannel
715
729
 
716
- const persistingChannel = createPersistingChannel(
717
- outputChannel,
718
- storage,
719
- threadId
720
- )
721
-
722
730
  try {
723
731
  const loopResult = await runStreamStepLoop({
724
732
  agent,
725
733
  runnerParams,
726
734
  maxSteps,
727
735
  agentRunner,
736
+ streamChannel: outputChannel,
728
737
  persistingChannel,
729
738
  channel,
730
739
  runId,
@@ -757,10 +766,6 @@ export async function streamAIAgent(
757
766
  persistingChannel,
758
767
  aiMiddlewares,
759
768
  singletonServices,
760
- storage,
761
- memoryConfig,
762
- threadId,
763
- workingMemorySchemaName,
764
769
  runnerParams.messages,
765
770
  aiRunState,
766
771
  runId
@@ -1090,7 +1095,15 @@ async function continueAfterToolResult(
1090
1095
 
1091
1096
  const instructions = await buildInstructions(resolvedName, packageName)
1092
1097
 
1093
- const aiMiddlewares: PikkuAIMiddlewareHooks[] = agent.aiMiddleware ?? []
1098
+ const aiMiddlewares: PikkuAIMiddlewareHooks[] = [
1099
+ ...getWorkingMemoryMiddleware(memoryConfig, storage, {
1100
+ threadId: run.threadId,
1101
+ workingMemorySchemaName,
1102
+ logger: singletonServices.logger,
1103
+ schemaService: singletonServices.schema,
1104
+ }),
1105
+ ...(agent.aiMiddleware ?? []),
1106
+ ]
1094
1107
  let modifiedMessages = trimmedMessages
1095
1108
  let modifiedInstructions = instructions
1096
1109
  for (const mw of aiMiddlewares) {
@@ -1137,20 +1150,20 @@ async function continueAfterToolResult(
1137
1150
  }
1138
1151
  )
1139
1152
 
1153
+ const persistingChannel = createPersistingChannel(
1154
+ channel,
1155
+ storage,
1156
+ run.threadId
1157
+ )
1158
+
1140
1159
  const wrappedChannel =
1141
1160
  allChannelMiddleware.length > 0
1142
1161
  ? (wrapChannelWithMiddleware(
1143
- { channel },
1162
+ { channel: persistingChannel },
1144
1163
  singletonServices,
1145
1164
  allChannelMiddleware
1146
1165
  ).channel as AIStreamChannel)
1147
- : channel
1148
-
1149
- const persistingChannel = createPersistingChannel(
1150
- wrappedChannel,
1151
- storage,
1152
- run.threadId
1153
- )
1166
+ : persistingChannel
1154
1167
 
1155
1168
  const streamContext: StreamContext = { channel, options }
1156
1169
  const resumeTools = (
@@ -1166,14 +1179,6 @@ async function continueAfterToolResult(
1166
1179
  ).tools
1167
1180
 
1168
1181
  const resolved = resolveModelConfig(resolvedName, agent)
1169
- console.log(
1170
- '[DEBUG resume] resolvedName:',
1171
- resolvedName,
1172
- 'agent.model:',
1173
- agent.model,
1174
- 'resolved.model:',
1175
- resolved.model
1176
- )
1177
1182
  const maxSteps = resolved.maxSteps ?? 10
1178
1183
 
1179
1184
  const runnerParams: AIAgentRunnerParams = {
@@ -1195,6 +1200,7 @@ async function continueAfterToolResult(
1195
1200
  runnerParams,
1196
1201
  maxSteps,
1197
1202
  agentRunner,
1203
+ streamChannel: wrappedChannel,
1198
1204
  persistingChannel,
1199
1205
  channel,
1200
1206
  runId: run.runId,
@@ -1227,10 +1233,6 @@ async function continueAfterToolResult(
1227
1233
  persistingChannel,
1228
1234
  aiMiddlewares,
1229
1235
  singletonServices,
1230
- storage,
1231
- memoryConfig,
1232
- run.threadId,
1233
- workingMemorySchemaName,
1234
1236
  runnerParams.messages,
1235
1237
  aiRunState,
1236
1238
  run.runId
@@ -29,6 +29,15 @@ export type AIContentPart =
29
29
  mediaType: string
30
30
  filename?: string
31
31
  }
32
+ | {
33
+ type: 'data'
34
+ name: string
35
+ data: unknown
36
+ }
37
+ | {
38
+ type: 'generative-ui'
39
+ spec: unknown
40
+ }
32
41
 
33
42
  export interface AIToolCall {
34
43
  id: string
@@ -48,6 +57,7 @@ export interface AIMessage {
48
57
  content?: string | AIContentPart[]
49
58
  toolCalls?: AIToolCall[]
50
59
  toolResults?: AIToolResult[]
60
+ reasoningContent?: string
51
61
  createdAt: Date
52
62
  }
53
63
 
@@ -299,6 +309,19 @@ export type AIStreamEvent =
299
309
  agent?: string
300
310
  session?: string
301
311
  }
312
+ | {
313
+ type: 'data'
314
+ name: string
315
+ data: unknown
316
+ agent?: string
317
+ session?: string
318
+ }
319
+ | {
320
+ type: 'generative-ui'
321
+ spec: unknown
322
+ agent?: string
323
+ session?: string
324
+ }
302
325
  | {
303
326
  type: 'suspended'
304
327
  reason: 'rpc-missing'
@@ -367,6 +390,7 @@ export interface AgentRunRow {
367
390
  export interface AgentRunService {
368
391
  listThreads(options?: {
369
392
  agentName?: string
393
+ resourceId?: string
370
394
  limit?: number
371
395
  offset?: number
372
396
  }): Promise<AIThread[]>
@@ -0,0 +1,272 @@
1
+ import { beforeEach, describe, test } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+
4
+ import { addFunction } from '../../function/function-runner.js'
5
+ import { pikkuState, resetPikkuState } from '../../pikku-state.js'
6
+ import { processMessageHandlers } from './channel-handler.js'
7
+
8
+ const createLogger = () => {
9
+ const errors: string[] = []
10
+ return {
11
+ errors,
12
+ logger: {
13
+ debug: () => {},
14
+ info: () => {},
15
+ warn: () => {},
16
+ error: (...args: unknown[]) => {
17
+ errors.push(args.map((arg) => String(arg)).join(' '))
18
+ },
19
+ },
20
+ }
21
+ }
22
+
23
+ const createChannelHandler = () => {
24
+ const sent: unknown[] = []
25
+ const channel = {
26
+ channelId: 'channel-1',
27
+ openingData: {},
28
+ state: 'open' as const,
29
+ send: (message: unknown) => {
30
+ sent.push(message)
31
+ },
32
+ sendBinary: () => {},
33
+ close: () => {},
34
+ setState: () => {},
35
+ getState: () => undefined,
36
+ clearState: () => {},
37
+ }
38
+
39
+ return {
40
+ sent,
41
+ handler: {
42
+ send: (message: unknown) => channel.send(message),
43
+ sendBinary: () => {},
44
+ getChannel: () => channel,
45
+ },
46
+ }
47
+ }
48
+
49
+ const registerFunction = (
50
+ funcName: string,
51
+ func: (services: any, data: any, wire: any) => Promise<unknown> | unknown
52
+ ) => {
53
+ addFunction(funcName, { func } as never)
54
+ pikkuState(null, 'function', 'meta')[funcName] = {
55
+ name: funcName,
56
+ sessionless: true,
57
+ permissions: [],
58
+ } as never
59
+ }
60
+
61
+ beforeEach(() => {
62
+ resetPikkuState()
63
+ })
64
+
65
+ describe('processMessageHandlers', () => {
66
+ test('routes json messages using onMessageWiring and appends the routing key to the result', async () => {
67
+ const { logger } = createLogger()
68
+ const { handler } = createChannelHandler()
69
+
70
+ pikkuState(null, 'channel', 'meta').chat = {
71
+ name: 'chat',
72
+ route: '/chat',
73
+ input: null,
74
+ connect: null,
75
+ disconnect: null,
76
+ message: null,
77
+ messageWirings: {
78
+ kind: {
79
+ ping: {
80
+ pikkuFuncId: 'pingFunc',
81
+ packageName: null,
82
+ },
83
+ },
84
+ },
85
+ } as never
86
+ registerFunction('pingFunc', async (_services, data) => ({
87
+ reply: `pong:${data.value}`,
88
+ }))
89
+
90
+ const { onMessage } = processMessageHandlers(
91
+ { logger } as never,
92
+ {
93
+ name: 'chat',
94
+ route: '/chat',
95
+ auth: false,
96
+ onMessageWiring: {
97
+ kind: {
98
+ ping: {
99
+ func: { func: async () => ({ ok: true }) } as never,
100
+ },
101
+ },
102
+ },
103
+ } as never,
104
+ handler as never
105
+ )
106
+
107
+ const result = await onMessage('{"kind":"ping","value":"hello"}')
108
+
109
+ assert.deepEqual(result, {
110
+ reply: 'pong:hello',
111
+ kind: 'ping',
112
+ })
113
+ })
114
+
115
+ test('falls back to the default handler for parsed json with no matching route', async () => {
116
+ const { logger } = createLogger()
117
+ const { handler } = createChannelHandler()
118
+
119
+ pikkuState(null, 'channel', 'meta').chat = {
120
+ name: 'chat',
121
+ route: '/chat',
122
+ input: null,
123
+ connect: null,
124
+ disconnect: null,
125
+ message: {
126
+ pikkuFuncId: 'defaultFunc',
127
+ packageName: null,
128
+ },
129
+ messageWirings: {
130
+ kind: {
131
+ ping: {
132
+ pikkuFuncId: 'pingFunc',
133
+ packageName: null,
134
+ },
135
+ },
136
+ },
137
+ } as never
138
+ registerFunction('defaultFunc', async (_services, data) => ({
139
+ seen: data,
140
+ }))
141
+
142
+ const { onMessage } = processMessageHandlers(
143
+ { logger } as never,
144
+ {
145
+ name: 'chat',
146
+ route: '/chat',
147
+ auth: false,
148
+ onMessage: { func: async () => ({ ok: true }) } as never,
149
+ onMessageWiring: {
150
+ kind: {
151
+ ping: {
152
+ func: { func: async () => ({ ok: true }) } as never,
153
+ },
154
+ },
155
+ },
156
+ } as never,
157
+ handler as never
158
+ )
159
+
160
+ const result = await onMessage('{"kind":"unknown","value":"hello"}')
161
+
162
+ assert.deepEqual(result, {
163
+ seen: { kind: 'unknown', value: 'hello' },
164
+ })
165
+ })
166
+
167
+ test('uses wrapper auth for default messages and sends unauthorized errors without a session', async () => {
168
+ const { errors, logger } = createLogger()
169
+ const { sent, handler } = createChannelHandler()
170
+
171
+ pikkuState(null, 'channel', 'meta').chat = {
172
+ name: 'chat',
173
+ route: '/chat',
174
+ input: null,
175
+ connect: null,
176
+ disconnect: null,
177
+ message: {
178
+ pikkuFuncId: 'defaultFunc',
179
+ packageName: null,
180
+ },
181
+ messageWirings: {},
182
+ } as never
183
+ registerFunction('defaultFunc', async () => ({ ok: true }))
184
+
185
+ const { onMessage } = processMessageHandlers(
186
+ { logger } as never,
187
+ {
188
+ name: 'chat',
189
+ route: '/chat',
190
+ auth: false,
191
+ onMessage: {
192
+ auth: true,
193
+ func: { func: async () => ({ ok: true }) } as never,
194
+ },
195
+ } as never,
196
+ handler as never
197
+ )
198
+
199
+ const result = await onMessage('hello')
200
+
201
+ assert.equal(result, undefined)
202
+ assert.deepEqual(sent, ['Unauthorized for the default message route'])
203
+ assert.match(
204
+ errors[0] || '',
205
+ /requires a session for the default message route/
206
+ )
207
+ })
208
+
209
+ test('logs when no message handler can process the payload', async () => {
210
+ const { errors, logger } = createLogger()
211
+ const { handler } = createChannelHandler()
212
+
213
+ pikkuState(null, 'channel', 'meta').chat = {
214
+ name: 'chat',
215
+ route: '/chat',
216
+ input: null,
217
+ connect: null,
218
+ disconnect: null,
219
+ message: null,
220
+ messageWirings: {},
221
+ } as never
222
+
223
+ const { onMessage } = processMessageHandlers(
224
+ { logger } as never,
225
+ {
226
+ name: 'chat',
227
+ route: '/chat',
228
+ auth: false,
229
+ } as never,
230
+ handler as never
231
+ )
232
+
233
+ const result = await onMessage({ raw: true })
234
+
235
+ assert.equal(result, undefined)
236
+ assert.match(
237
+ errors[0] || '',
238
+ /No handler found for message in channel chat/
239
+ )
240
+ assert.match(errors[1] || '', /Channel config name: chat/)
241
+ })
242
+
243
+ test('blocks binary messages when a session is required and missing', async () => {
244
+ const { errors, logger } = createLogger()
245
+ const { handler } = createChannelHandler()
246
+
247
+ pikkuState(null, 'channel', 'meta').chat = {
248
+ name: 'chat',
249
+ route: '/chat',
250
+ input: null,
251
+ connect: null,
252
+ disconnect: null,
253
+ message: null,
254
+ messageWirings: {},
255
+ } as never
256
+
257
+ const { onBinaryMessage } = processMessageHandlers(
258
+ { logger } as never,
259
+ {
260
+ name: 'chat',
261
+ route: '/chat',
262
+ onBinaryMessage: async () => new Uint8Array([1, 2, 3]),
263
+ } as never,
264
+ handler as never
265
+ )
266
+
267
+ const result = await onBinaryMessage?.(new Uint8Array([1, 2, 3]))
268
+
269
+ assert.equal(result, undefined)
270
+ assert.match(errors[0] || '', /requires a session for binary message/)
271
+ })
272
+ })
@@ -90,7 +90,7 @@ export const processMessageHandlers = (
90
90
  ? `route '${routingProperty}:${routerValue}'`
91
91
  : 'the default message route'
92
92
  logger.error(
93
- `Channel ${channelConfig.name} with id ${channelHandler.getChannel().channelId} requires a session for ${routeMessage}`
93
+ `Channel ${channelConfig.name} with id ${channelHandler.getChannel().channelId} requires a session for ${routeMessage}. No session is attached to this websocket connection. Ensure auth middleware establishes a session during websocket upgrade, and configure sessionStore when the runtime needs persisted sessions.`
94
94
  )
95
95
  // TODO: Send error message back breaks typescript, but should be implemented somehow
96
96
  channelHandler.getChannel().send(`Unauthorized for ${routeMessage}`)