@pikku/core 0.12.1 → 0.12.3

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 (165) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/dist/crypto-utils.d.ts +10 -0
  3. package/dist/crypto-utils.js +62 -0
  4. package/dist/function/function-runner.js +4 -4
  5. package/dist/function/functions.types.d.ts +26 -1
  6. package/dist/function/functions.types.js +16 -0
  7. package/dist/handle-error.d.ts +1 -1
  8. package/dist/handle-error.js +7 -3
  9. package/dist/index.d.ts +14 -2
  10. package/dist/index.js +1 -1
  11. package/dist/middleware-runner.js +1 -0
  12. package/dist/permissions.js +1 -0
  13. package/dist/pikku-state.js +7 -1
  14. package/dist/schema.js +1 -1
  15. package/dist/services/gateway-service.d.ts +19 -0
  16. package/dist/services/gateway-service.js +1 -0
  17. package/dist/services/in-memory-workflow-service.d.ts +8 -2
  18. package/dist/services/in-memory-workflow-service.js +28 -1
  19. package/dist/services/index.d.ts +2 -0
  20. package/dist/services/index.js +1 -0
  21. package/dist/services/local-gateway-service.d.ts +22 -0
  22. package/dist/services/local-gateway-service.js +51 -0
  23. package/dist/services/workflow-service.d.ts +8 -2
  24. package/dist/testing/index.d.ts +2 -0
  25. package/dist/testing/index.js +1 -0
  26. package/dist/testing/service-tests.d.ts +31 -0
  27. package/dist/testing/service-tests.js +598 -0
  28. package/dist/types/core.types.d.ts +12 -2
  29. package/dist/types/state.types.d.ts +9 -2
  30. package/dist/utils/hash.d.ts +2 -0
  31. package/dist/utils/hash.js +23 -0
  32. package/dist/utils.js +1 -1
  33. package/dist/wirings/ai-agent/agent-dynamic-workflow.d.ts +6 -0
  34. package/dist/wirings/ai-agent/agent-dynamic-workflow.js +348 -0
  35. package/dist/wirings/ai-agent/ai-agent-memory.d.ts +1 -1
  36. package/dist/wirings/ai-agent/ai-agent-memory.js +19 -7
  37. package/dist/wirings/ai-agent/ai-agent-prepare.d.ts +10 -7
  38. package/dist/wirings/ai-agent/ai-agent-prepare.js +141 -48
  39. package/dist/wirings/ai-agent/ai-agent-runner.d.ts +4 -0
  40. package/dist/wirings/ai-agent/ai-agent-runner.js +381 -27
  41. package/dist/wirings/ai-agent/ai-agent-stream.d.ts +7 -2
  42. package/dist/wirings/ai-agent/ai-agent-stream.js +126 -48
  43. package/dist/wirings/ai-agent/ai-agent.types.d.ts +57 -3
  44. package/dist/wirings/ai-agent/index.d.ts +2 -3
  45. package/dist/wirings/ai-agent/index.js +1 -2
  46. package/dist/wirings/channel/channel-handler.d.ts +5 -2
  47. package/dist/wirings/channel/channel-handler.js +16 -2
  48. package/dist/wirings/channel/channel-middleware-runner.js +1 -0
  49. package/dist/wirings/channel/channel.types.d.ts +7 -7
  50. package/dist/wirings/channel/index.d.ts +1 -1
  51. package/dist/wirings/channel/local/local-channel-handler.d.ts +8 -1
  52. package/dist/wirings/channel/local/local-channel-handler.js +22 -3
  53. package/dist/wirings/channel/local/local-channel-runner.js +28 -5
  54. package/dist/wirings/channel/pikku-abstract-channel-handler.d.ts +2 -1
  55. package/dist/wirings/channel/pikku-abstract-channel-handler.js +1 -0
  56. package/dist/wirings/channel/serverless/serverless-channel-runner.js +5 -2
  57. package/dist/wirings/cli/cli-runner.js +13 -2
  58. package/dist/wirings/cli/cli.types.d.ts +1 -0
  59. package/dist/wirings/gateway/gateway-runner.d.ts +24 -0
  60. package/dist/wirings/gateway/gateway-runner.js +325 -0
  61. package/dist/wirings/gateway/gateway.types.d.ts +127 -0
  62. package/dist/wirings/gateway/gateway.types.js +1 -0
  63. package/dist/wirings/gateway/index.d.ts +2 -0
  64. package/dist/wirings/gateway/index.js +1 -0
  65. package/dist/wirings/http/http-routes.js +0 -1
  66. package/dist/wirings/http/http-runner.d.ts +1 -1
  67. package/dist/wirings/http/http-runner.js +28 -3
  68. package/dist/wirings/http/http.types.d.ts +3 -11
  69. package/dist/wirings/http/index.d.ts +2 -1
  70. package/dist/wirings/http/index.js +1 -0
  71. package/dist/wirings/http/pikku-fetch-http-request.d.ts +1 -0
  72. package/dist/wirings/http/pikku-fetch-http-request.js +3 -0
  73. package/dist/wirings/http/pikku-fetch-http-response.d.ts +1 -0
  74. package/dist/wirings/http/pikku-fetch-http-response.js +4 -4
  75. package/dist/wirings/http/web-request.d.ts +12 -0
  76. package/dist/wirings/http/web-request.js +85 -0
  77. package/dist/wirings/mcp/mcp-endpoint-registry.js +1 -1
  78. package/dist/wirings/mcp/mcp-runner.js +13 -1
  79. package/dist/wirings/rpc/rpc-runner.d.ts +30 -6
  80. package/dist/wirings/rpc/rpc-runner.js +36 -14
  81. package/dist/wirings/rpc/rpc-types.d.ts +6 -1
  82. package/dist/wirings/rpc/wire-addon.d.ts +1 -0
  83. package/dist/wirings/rpc/wire-addon.js +1 -1
  84. package/dist/wirings/scheduler/scheduler-runner.js +1 -0
  85. package/dist/wirings/workflow/graph/graph-validation.d.ts +3 -0
  86. package/dist/wirings/workflow/graph/graph-validation.js +175 -0
  87. package/dist/wirings/workflow/graph/index.d.ts +1 -0
  88. package/dist/wirings/workflow/graph/index.js +1 -0
  89. package/dist/wirings/workflow/index.d.ts +1 -1
  90. package/dist/wirings/workflow/pikku-workflow-service.d.ts +8 -2
  91. package/dist/wirings/workflow/pikku-workflow-service.js +1 -2
  92. package/dist/wirings/workflow/workflow.types.d.ts +10 -1
  93. package/package.json +6 -3
  94. package/src/crypto-utils.test.ts +99 -1
  95. package/src/crypto-utils.ts +114 -0
  96. package/src/function/function-runner.ts +5 -5
  97. package/src/function/functions.types.ts +38 -1
  98. package/src/handle-error.test.ts +65 -2
  99. package/src/handle-error.ts +8 -4
  100. package/src/index.ts +14 -0
  101. package/src/middleware-runner.ts +1 -0
  102. package/src/permissions.ts +1 -0
  103. package/src/pikku-state.ts +8 -1
  104. package/src/schema.ts +1 -1
  105. package/src/services/gateway-service.ts +20 -0
  106. package/src/services/in-memory-workflow-service.ts +45 -2
  107. package/src/services/index.ts +2 -0
  108. package/src/services/local-gateway-service.ts +62 -0
  109. package/src/services/workflow-service.ts +12 -1
  110. package/src/testing/index.ts +2 -0
  111. package/src/testing/service-tests.ts +873 -0
  112. package/src/types/core.types.ts +12 -1
  113. package/src/types/state.types.ts +12 -2
  114. package/src/utils/hash.test.ts +68 -0
  115. package/src/utils/hash.ts +26 -0
  116. package/src/utils.ts +1 -1
  117. package/src/wirings/ai-agent/agent-dynamic-workflow.ts +469 -0
  118. package/src/wirings/ai-agent/ai-agent-memory.ts +17 -7
  119. package/src/wirings/ai-agent/ai-agent-prepare.ts +194 -62
  120. package/src/wirings/ai-agent/ai-agent-runner.test.ts +3 -1
  121. package/src/wirings/ai-agent/ai-agent-runner.ts +553 -31
  122. package/src/wirings/ai-agent/ai-agent-stream.test.ts +209 -2
  123. package/src/wirings/ai-agent/ai-agent-stream.ts +195 -91
  124. package/src/wirings/ai-agent/ai-agent.types.ts +57 -3
  125. package/src/wirings/ai-agent/index.ts +3 -5
  126. package/src/wirings/channel/channel-handler.ts +41 -4
  127. package/src/wirings/channel/channel-middleware-runner.ts +1 -0
  128. package/src/wirings/channel/channel.types.ts +12 -7
  129. package/src/wirings/channel/index.ts +1 -0
  130. package/src/wirings/channel/local/local-channel-handler.ts +31 -3
  131. package/src/wirings/channel/local/local-channel-runner.ts +32 -8
  132. package/src/wirings/channel/pikku-abstract-channel-handler.test.ts +2 -0
  133. package/src/wirings/channel/pikku-abstract-channel-handler.ts +8 -1
  134. package/src/wirings/channel/serverless/serverless-channel-runner.ts +5 -2
  135. package/src/wirings/cli/cli-runner.ts +15 -2
  136. package/src/wirings/cli/cli.types.ts +1 -0
  137. package/src/wirings/gateway/gateway-runner.test.ts +474 -0
  138. package/src/wirings/gateway/gateway-runner.ts +411 -0
  139. package/src/wirings/gateway/gateway.types.ts +149 -0
  140. package/src/wirings/gateway/index.ts +13 -0
  141. package/src/wirings/http/http-routes.ts +0 -1
  142. package/src/wirings/http/http-runner.ts +38 -10
  143. package/src/wirings/http/http.types.ts +3 -12
  144. package/src/wirings/http/index.ts +3 -0
  145. package/src/wirings/http/pikku-fetch-http-request.ts +4 -0
  146. package/src/wirings/http/pikku-fetch-http-response.ts +4 -4
  147. package/src/wirings/http/web-request.test.ts +236 -0
  148. package/src/wirings/http/web-request.ts +104 -0
  149. package/src/wirings/mcp/mcp-endpoint-registry.ts +1 -1
  150. package/src/wirings/mcp/mcp-runner.ts +30 -12
  151. package/src/wirings/rpc/rpc-runner.ts +45 -24
  152. package/src/wirings/rpc/rpc-types.ts +6 -1
  153. package/src/wirings/rpc/wire-addon.ts +2 -1
  154. package/src/wirings/scheduler/scheduler-runner.ts +1 -0
  155. package/src/wirings/workflow/graph/graph-validation.test.ts +170 -0
  156. package/src/wirings/workflow/graph/graph-validation.ts +237 -0
  157. package/src/wirings/workflow/graph/index.ts +5 -0
  158. package/src/wirings/workflow/index.ts +1 -0
  159. package/src/wirings/workflow/pikku-workflow-service.ts +14 -3
  160. package/src/wirings/workflow/workflow.types.ts +9 -1
  161. package/tsconfig.tsbuildinfo +1 -1
  162. package/dist/wirings/ai-agent/ai-agent-assistant-ui.d.ts +0 -5
  163. package/dist/wirings/ai-agent/ai-agent-assistant-ui.js +0 -146
  164. package/src/wirings/ai-agent/ai-agent-assistant-ui.test.ts +0 -363
  165. package/src/wirings/ai-agent/ai-agent-assistant-ui.ts +0 -214
@@ -97,3 +97,117 @@ export const decryptJSON = async <T>(
97
97
  )
98
98
  return JSON.parse(decoder.decode(new Uint8Array(decrypted))) as T
99
99
  }
100
+
101
+ /**
102
+ * Envelope encryption utilities.
103
+ *
104
+ * Each secret gets its own random DEK (data encryption key).
105
+ * The DEK is wrapped (encrypted) by a KEK (key encryption key) — typically an env var.
106
+ * The actual secret is encrypted with the DEK.
107
+ *
108
+ * KEK rotation only re-wraps the DEK, never touches the ciphertext.
109
+ */
110
+
111
+ const importRawKey = async (rawBytes: Uint8Array): Promise<CryptoKey> => {
112
+ const subtle = getSubtle()
113
+ return subtle.importKey(
114
+ 'raw',
115
+ rawBytes.buffer as ArrayBuffer,
116
+ { name: 'AES-GCM' },
117
+ true,
118
+ ['encrypt', 'decrypt']
119
+ )
120
+ }
121
+
122
+ export const generateDEK = async (): Promise<string> => {
123
+ const raw = globalThis.crypto.getRandomValues(new Uint8Array(32))
124
+ return toBase64Url(raw)
125
+ }
126
+
127
+ export const wrapDEK = async (
128
+ kek: string,
129
+ plaintextDEK: string
130
+ ): Promise<string> => {
131
+ return encryptJSON(kek, plaintextDEK)
132
+ }
133
+
134
+ export const unwrapDEK = async (
135
+ kek: string,
136
+ wrappedDEK: string
137
+ ): Promise<string> => {
138
+ return decryptJSON<string>(kek, wrappedDEK)
139
+ }
140
+
141
+ const encryptWithDEK = async (
142
+ dekBase64: string,
143
+ value: unknown
144
+ ): Promise<string> => {
145
+ const crypto = globalThis.crypto
146
+ const subtle = getSubtle()
147
+ const iv = crypto.getRandomValues(new Uint8Array(12))
148
+ const key = await importRawKey(fromBase64Url(dekBase64))
149
+ const plaintext = encoder.encode(JSON.stringify(value))
150
+ const encrypted = await subtle.encrypt(
151
+ { name: 'AES-GCM', iv },
152
+ key,
153
+ plaintext
154
+ )
155
+ const cipherBytes = new Uint8Array(encrypted)
156
+ const out = new Uint8Array(iv.length + cipherBytes.length)
157
+ out.set(iv, 0)
158
+ out.set(cipherBytes, iv.length)
159
+ return toBase64Url(out)
160
+ }
161
+
162
+ const decryptWithDEK = async <T>(
163
+ dekBase64: string,
164
+ token: string
165
+ ): Promise<T> => {
166
+ const subtle = getSubtle()
167
+ const data = fromBase64Url(token)
168
+ if (data.length < 13) {
169
+ throw new Error('Invalid encrypted payload')
170
+ }
171
+ const iv = data.slice(0, 12)
172
+ const ciphertext = data.slice(12)
173
+ const key = await importRawKey(fromBase64Url(dekBase64))
174
+ const decrypted = await subtle.decrypt(
175
+ { name: 'AES-GCM', iv },
176
+ key,
177
+ ciphertext
178
+ )
179
+ return JSON.parse(decoder.decode(new Uint8Array(decrypted))) as T
180
+ }
181
+
182
+ export interface EnvelopeEncryptResult {
183
+ ciphertext: string
184
+ wrappedDEK: string
185
+ }
186
+
187
+ export const envelopeEncrypt = async (
188
+ kek: string,
189
+ value: unknown
190
+ ): Promise<EnvelopeEncryptResult> => {
191
+ const dek = await generateDEK()
192
+ const ciphertext = await encryptWithDEK(dek, value)
193
+ const wrappedDEK = await wrapDEK(kek, dek)
194
+ return { ciphertext, wrappedDEK }
195
+ }
196
+
197
+ export const envelopeDecrypt = async <T>(
198
+ kek: string,
199
+ ciphertext: string,
200
+ wrappedDEK: string
201
+ ): Promise<T> => {
202
+ const dek = await unwrapDEK(kek, wrappedDEK)
203
+ return decryptWithDEK<T>(dek, ciphertext)
204
+ }
205
+
206
+ export const envelopeRewrap = async (
207
+ oldKEK: string,
208
+ newKEK: string,
209
+ wrappedDEK: string
210
+ ): Promise<string> => {
211
+ const dek = await unwrapDEK(oldKEK, wrappedDEK)
212
+ return wrapDEK(newKEK, dek)
213
+ }
@@ -95,7 +95,7 @@ export const getAllFunctionNames = (): string[] => {
95
95
  const mainFunctionsMeta = pikkuState(null, 'function', 'meta')
96
96
  functions.push(...Object.keys(mainFunctionsMeta))
97
97
 
98
- const addons = pikkuState(null, 'rpc', 'addons')
98
+ const addons = pikkuState(null, 'addons', 'packages')
99
99
  for (const [namespace, config] of addons) {
100
100
  const packageFunctionsMeta = pikkuState(config.package, 'function', 'meta')
101
101
  for (const funcName of Object.keys(packageFunctionsMeta)) {
@@ -280,6 +280,10 @@ export const runPikkuFunc = async <In = any, Out = any>(
280
280
  // Validate and coerce data if schema is defined
281
281
  const inputSchemaName = funcMeta.inputSchemaName
282
282
  if (inputSchemaName) {
283
+ // Coerce (top level) data types before validation (e.g. string→array, string→date)
284
+ if (coerceDataFromSchema) {
285
+ coerceTopLevelDataFromSchema(inputSchemaName, actualData, packageName)
286
+ }
283
287
  // Validate request data against the defined schema, if any
284
288
  await validateSchema(
285
289
  resolvedSingletonServices.logger,
@@ -288,10 +292,6 @@ export const runPikkuFunc = async <In = any, Out = any>(
288
292
  actualData,
289
293
  packageName
290
294
  )
291
- // Coerce (top level) query string parameters or date objects if specified by the schema
292
- if (coerceDataFromSchema) {
293
- coerceTopLevelDataFromSchema(inputSchemaName, actualData, packageName)
294
- }
295
295
  }
296
296
 
297
297
  if (
@@ -7,6 +7,7 @@ import type {
7
7
  PickRequired,
8
8
  } from '../types/core.types.js'
9
9
  import type { StandardSchemaV1 } from '@standard-schema/spec'
10
+ import type { PikkuError } from '../errors/error-handler.js'
10
11
  import type { CoreNodeConfig } from '../wirings/node/node.types.js'
11
12
 
12
13
  /**
@@ -194,6 +195,40 @@ export const pikkuPermissionFactory = <In = any>(
194
195
  return factory
195
196
  }
196
197
 
198
+ /**
199
+ * A function that generates a human-readable description of a pending approval action.
200
+ * Used by AI agents to show meaningful approval prompts instead of raw tool arguments.
201
+ *
202
+ * @template In - The input type (same as the function it describes).
203
+ * @template Services - The services type, defaults to `CoreSingletonServices`.
204
+ */
205
+ export type CorePikkuApprovalDescription<
206
+ In = any,
207
+ Services extends CoreSingletonServices = CoreSingletonServices,
208
+ > = (services: Services, data: In) => Promise<string>
209
+
210
+ /**
211
+ * Factory function for creating approval description functions with tree-shaking support.
212
+ *
213
+ * @example
214
+ * ```typescript
215
+ * export const deleteTodoApproval = pikkuApprovalDescription(
216
+ * async ({ todoStore }, { id }) => {
217
+ * const todo = await todoStore.get(id)
218
+ * return `Delete todo: "${todo.title}"`
219
+ * }
220
+ * )
221
+ * ```
222
+ */
223
+ export const pikkuApprovalDescription = <
224
+ In = any,
225
+ Services extends CoreSingletonServices = CoreSingletonServices,
226
+ >(
227
+ fn: CorePikkuApprovalDescription<In, Services>
228
+ ): CorePikkuApprovalDescription<In, Services> => {
229
+ return fn
230
+ }
231
+
197
232
  export type CorePikkuAuth<
198
233
  Services extends CoreSingletonServices = CoreServices,
199
234
  Session extends CoreUserSession = CoreUserSession,
@@ -256,7 +291,8 @@ export type CorePikkuFunctionConfig<
256
291
  remote?: boolean
257
292
  mcp?: boolean
258
293
  readonly?: boolean
259
- requiresApproval?: boolean
294
+ approvalRequired?: boolean
295
+ approvalDescription?: any
260
296
  func: PikkuFunction
261
297
  auth?: boolean
262
298
  permissions?: CorePermissionGroup<PikkuPermission>
@@ -264,4 +300,5 @@ export type CorePikkuFunctionConfig<
264
300
  input?: InputSchema
265
301
  output?: OutputSchema
266
302
  node?: CoreNodeConfig
303
+ errors?: Array<typeof PikkuError>
267
304
  }
@@ -267,7 +267,7 @@ describe('handleHTTPError', () => {
267
267
  // Should not throw
268
268
  })
269
269
 
270
- test('should call response.close when available', () => {
270
+ test('should not call response.close automatically', () => {
271
271
  const logger = createMockLogger()
272
272
  const http = createMockHTTP()
273
273
  const error = new BadRequestError('bad')
@@ -282,7 +282,7 @@ describe('handleHTTPError', () => {
282
282
  false
283
283
  )
284
284
 
285
- assert.strictEqual(http._state.closed, true)
285
+ assert.strictEqual(http._state.closed, false)
286
286
  })
287
287
 
288
288
  test('should handle undefined http gracefully', () => {
@@ -335,6 +335,69 @@ describe('handleHTTPError', () => {
335
335
  assert.ok(errors.some((e) => e.args[0] === 'just a string error'))
336
336
  })
337
337
 
338
+ test('should include message and stack when exposeErrors is true', () => {
339
+ const logger = createMockLogger()
340
+ const http = createMockHTTP()
341
+ const error = new Error('something unexpected')
342
+
343
+ handleHTTPError(
344
+ error,
345
+ http as any,
346
+ 'tracker-expose',
347
+ logger as any,
348
+ [],
349
+ true,
350
+ false,
351
+ true
352
+ )
353
+
354
+ assert.strictEqual(http._state.statusCode, 500)
355
+ assert.strictEqual(http._state.jsonBody.errorId, 'tracker-expose')
356
+ assert.strictEqual(http._state.jsonBody.message, 'something unexpected')
357
+ assert.ok(typeof http._state.jsonBody.stack === 'string')
358
+ })
359
+
360
+ test('should not include message and stack when exposeErrors is false', () => {
361
+ const logger = createMockLogger()
362
+ const http = createMockHTTP()
363
+ const error = new Error('something unexpected')
364
+
365
+ handleHTTPError(
366
+ error,
367
+ http as any,
368
+ 'tracker-no-expose',
369
+ logger as any,
370
+ [],
371
+ true,
372
+ false,
373
+ false
374
+ )
375
+
376
+ assert.strictEqual(http._state.statusCode, 500)
377
+ assert.deepStrictEqual(http._state.jsonBody, {
378
+ errorId: 'tracker-no-expose',
379
+ })
380
+ })
381
+
382
+ test('should not expose details for non-Error objects even when exposeErrors is true', () => {
383
+ const logger = createMockLogger()
384
+ const http = createMockHTTP()
385
+
386
+ handleHTTPError(
387
+ 'string error',
388
+ http as any,
389
+ 'tracker-str',
390
+ logger as any,
391
+ [],
392
+ true,
393
+ false,
394
+ true
395
+ )
396
+
397
+ assert.strictEqual(http._state.statusCode, 500)
398
+ assert.deepStrictEqual(http._state.jsonBody, { errorId: 'tracker-str' })
399
+ })
400
+
338
401
  test('should handle warning log without trackerId', () => {
339
402
  const logger = createMockLogger()
340
403
  const http = createMockHTTP()
@@ -21,7 +21,8 @@ export const handleHTTPError = (
21
21
  logger: Logger,
22
22
  logWarningsForStatusCodes: number[],
23
23
  respondWith404: boolean,
24
- bubbleError: boolean
24
+ bubbleError: boolean,
25
+ exposeErrors: boolean = false
25
26
  ) => {
26
27
  // Skip 404 handling if configured to do so
27
28
  if (e instanceof NotFoundError && !respondWith404) {
@@ -53,13 +54,16 @@ export const handleHTTPError = (
53
54
 
54
55
  if (trackerId) {
55
56
  logger.warn(`Error id: ${trackerId}`)
56
- http?.response?.json({ errorId: trackerId })
57
+ const errorBody: Record<string, unknown> = { errorId: trackerId }
58
+ if (exposeErrors && e instanceof Error) {
59
+ errorBody.message = e.message
60
+ errorBody.stack = e.stack
61
+ }
62
+ http?.response?.json(errorBody)
57
63
  }
58
64
  }
59
65
 
60
66
  if (bubbleError) {
61
67
  throw e
62
68
  }
63
-
64
- http?.response?.close?.()
65
69
  }
package/src/index.ts CHANGED
@@ -47,6 +47,7 @@ export type {
47
47
  CorePikkuPermission,
48
48
  CorePikkuPermissionConfig,
49
49
  CorePikkuPermissionFactory,
50
+ CorePikkuApprovalDescription,
50
51
  CorePermissionGroup,
51
52
  ZodLike,
52
53
  } from './function/functions.types.js'
@@ -54,6 +55,7 @@ export {
54
55
  pikkuAuth,
55
56
  pikkuPermission,
56
57
  pikkuPermissionFactory,
58
+ pikkuApprovalDescription,
57
59
  } from './function/functions.types.js'
58
60
  export { addFunction, getAllFunctionNames } from './function/index.js'
59
61
  export { PikkuRequest } from './pikku-request.js'
@@ -86,6 +88,18 @@ export { NotFoundError } from './errors/errors.js'
86
88
  export type { EventHubService } from './wirings/channel/eventhub-service.js'
87
89
  export type { QueueService } from './wirings/queue/queue.types.js'
88
90
  export type { JWTService } from './services/jwt-service.js'
91
+ export type { SecretService } from './services/secret-service.js'
92
+ export type { VariablesService } from './services/variables-service.js'
93
+ export type { ContentService } from './services/content-service.js'
94
+ export type { DeploymentService } from './services/deployment-service.js'
95
+ export type { WorkflowService } from './services/workflow-service.js'
96
+ export type { GatewayService } from './services/gateway-service.js'
97
+ export type { TriggerService } from './services/trigger-service.js'
98
+ export type { SchemaService } from './services/schema-service.js'
99
+ export type { SessionService } from './services/user-session-service.js'
100
+ export type { AIAgentRunnerService } from './services/ai-agent-runner-service.js'
101
+ export type { AIRunStateService } from './services/ai-run-state-service.js'
102
+ export type { AIStorageService } from './services/ai-storage-service.js'
89
103
  export type { HTTPMethod } from './wirings/http/http.types.js'
90
104
  export type { GraphNodeConfig } from './wirings/workflow/graph/workflow-graph.types.js'
91
105
  export { createGraph } from './wirings/workflow/graph/graph-node.js'
@@ -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
 
@@ -71,7 +72,9 @@ const createEmptyPackageState = (): PikkuPackageState => ({
71
72
  rpc: {
72
73
  meta: {},
73
74
  files: new Map(),
74
- addons: new Map(),
75
+ },
76
+ addons: {
77
+ packages: new Map(),
75
78
  },
76
79
  http: {
77
80
  middleware: new Map(),
@@ -121,6 +124,10 @@ const createEmptyPackageState = (): PikkuPackageState => ({
121
124
  agents: new Map(),
122
125
  agentsMeta: {} as AIAgentMeta,
123
126
  },
127
+ gateway: {
128
+ gateways: new Map(),
129
+ meta: {} as GatewaysMeta,
130
+ },
124
131
  cli: {
125
132
  meta: { programs: {}, renderers: {} },
126
133
  programs: {},
package/src/schema.ts CHANGED
@@ -146,6 +146,6 @@ export const validateSchema = async (
146
146
  )
147
147
  }
148
148
  await schemaService.compileSchema(key, schema)
149
- await schemaService.validateSchema(key, data)
149
+ await schemaService.validateSchema(key, data ?? {})
150
150
  }
151
151
  }
@@ -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
+ }
@@ -6,6 +6,7 @@ import type {
6
6
  WorkflowRunWire,
7
7
  StepState,
8
8
  WorkflowStatus,
9
+ WorkflowVersionStatus,
9
10
  WorkflowStepOptions,
10
11
  } from '../wirings/workflow/workflow.types.js'
11
12
 
@@ -40,7 +41,10 @@ export class InMemoryWorkflowService extends PikkuWorkflowService {
40
41
  >() // keyed by runId
41
42
  private runState = new Map<string, Record<string, unknown>>() // keyed by runId
42
43
  private branchKeys = new Map<string, string>() // keyed by stepId
43
- private workflowVersions = new Map<string, { graph: any; source: string }>() // keyed by `${name}:${graphHash}`
44
+ private workflowVersions = new Map<
45
+ string,
46
+ { graph: any; source: string; status: WorkflowVersionStatus }
47
+ >() // keyed by `${name}:${graphHash}`
44
48
 
45
49
  async createRun(
46
50
  workflowName: string,
@@ -376,14 +380,28 @@ export class InMemoryWorkflowService extends PikkuWorkflowService {
376
380
  name: string,
377
381
  graphHash: string,
378
382
  graph: any,
379
- source: string
383
+ source: string,
384
+ status?: WorkflowVersionStatus
380
385
  ): Promise<void> {
381
386
  this.workflowVersions.set(`${name}:${graphHash}`, {
382
387
  graph,
383
388
  source,
389
+ status: status ?? 'active',
384
390
  })
385
391
  }
386
392
 
393
+ async updateWorkflowVersionStatus(
394
+ name: string,
395
+ graphHash: string,
396
+ status: WorkflowVersionStatus
397
+ ): Promise<void> {
398
+ const key = `${name}:${graphHash}`
399
+ const version = this.workflowVersions.get(key)
400
+ if (version) {
401
+ version.status = status
402
+ }
403
+ }
404
+
387
405
  async getWorkflowVersion(
388
406
  name: string,
389
407
  graphHash: string
@@ -392,4 +410,29 @@ export class InMemoryWorkflowService extends PikkuWorkflowService {
392
410
  if (!version) return null
393
411
  return { graph: version.graph, source: version.source }
394
412
  }
413
+
414
+ async getAIGeneratedWorkflows(
415
+ agentName?: string
416
+ ): Promise<Array<{ workflowName: string; graphHash: string; graph: any }>> {
417
+ const results: Array<{
418
+ workflowName: string
419
+ graphHash: string
420
+ graph: any
421
+ }> = []
422
+ const prefix = agentName ? `ai:${agentName}:` : 'ai:'
423
+ for (const [key, value] of this.workflowVersions) {
424
+ if (value.source !== 'ai-agent' || value.status !== 'active') continue
425
+ const separatorIdx = key.lastIndexOf(':')
426
+ const wfName = key.substring(0, separatorIdx)
427
+ const hash = key.substring(separatorIdx + 1)
428
+ if (wfName.startsWith(prefix)) {
429
+ results.push({
430
+ workflowName: wfName,
431
+ graphHash: hash,
432
+ graph: value.graph,
433
+ })
434
+ }
435
+ }
436
+ return results
437
+ }
395
438
  }
@@ -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,
@@ -0,0 +1,62 @@
1
+ import type { GatewayService } from './gateway-service.js'
2
+ import type { GatewayAdapter } from '../wirings/gateway/gateway.types.js'
3
+ import { pikkuState, getSingletonServices } from '../pikku-state.js'
4
+ import { createListenerMessageHandler } from '../wirings/gateway/gateway-runner.js'
5
+
6
+ /**
7
+ * Local GatewayService implementation.
8
+ *
9
+ * Starts all registered listener gateways unconditionally (single-process).
10
+ * For distributed deployments, implement `GatewayService` with leader
11
+ * election or similar coordination.
12
+ *
13
+ * @example
14
+ * ```typescript
15
+ * const gatewayService = new LocalGatewayService()
16
+ * await gatewayService.start()
17
+ *
18
+ * // On shutdown
19
+ * await gatewayService.stop()
20
+ * ```
21
+ */
22
+ export class LocalGatewayService implements GatewayService {
23
+ private activeAdapters = new Map<string, GatewayAdapter>()
24
+
25
+ async start(): Promise<void> {
26
+ const singletonServices = getSingletonServices()
27
+ const gateways = pikkuState(null, 'gateway', 'gateways')
28
+
29
+ for (const [name, config] of gateways) {
30
+ if (config.type !== 'listener') continue
31
+ if (this.activeAdapters.has(name)) continue
32
+
33
+ const handleMessage = createListenerMessageHandler(
34
+ name,
35
+ config,
36
+ singletonServices
37
+ )
38
+
39
+ await config.adapter.init(handleMessage)
40
+ this.activeAdapters.set(name, config.adapter)
41
+ singletonServices.logger.info(`Started listener gateway: ${name}`)
42
+ }
43
+ }
44
+
45
+ async stop(): Promise<void> {
46
+ for (const [name, adapter] of this.activeAdapters) {
47
+ try {
48
+ await adapter.close()
49
+ } catch (e: any) {
50
+ try {
51
+ getSingletonServices().logger.error(
52
+ `Error closing listener gateway '${name}':`,
53
+ e
54
+ )
55
+ } catch {
56
+ // logger may not be available during shutdown
57
+ }
58
+ }
59
+ }
60
+ this.activeAdapters.clear()
61
+ }
62
+ }
@@ -4,6 +4,7 @@ import type {
4
4
  WorkflowRunWire,
5
5
  StepState,
6
6
  WorkflowStatus,
7
+ WorkflowVersionStatus,
7
8
  } from '../wirings/workflow/workflow.types.js'
8
9
 
9
10
  /**
@@ -75,10 +76,20 @@ export interface WorkflowService {
75
76
  name: string,
76
77
  graphHash: string,
77
78
  graph: any,
78
- source: string
79
+ source: string,
80
+ status?: WorkflowVersionStatus
81
+ ): Promise<void>
82
+ updateWorkflowVersionStatus(
83
+ name: string,
84
+ graphHash: string,
85
+ status: WorkflowVersionStatus
79
86
  ): Promise<void>
80
87
  getWorkflowVersion(
81
88
  name: string,
82
89
  graphHash: string
83
90
  ): Promise<{ graph: any; source: string } | null>
91
+
92
+ getAIGeneratedWorkflows(
93
+ agentName?: string
94
+ ): Promise<Array<{ workflowName: string; graphHash: string; graph: any }>>
84
95
  }
@@ -0,0 +1,2 @@
1
+ export { defineServiceTests } from './service-tests.js'
2
+ export type { ServiceTestConfig } from './service-tests.js'