@pikku/core 0.12.63 → 0.12.66

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 (225) hide show
  1. package/CHANGELOG.md +368 -0
  2. package/dist/dev/hot-reload.js +13 -55
  3. package/dist/dev/module-runner.d.ts +14 -0
  4. package/dist/dev/module-runner.js +59 -0
  5. package/dist/dev/reload-meta.d.ts +13 -0
  6. package/dist/dev/reload-meta.js +22 -0
  7. package/dist/errors/errors.d.ts +12 -0
  8. package/dist/errors/errors.js +19 -0
  9. package/dist/function/function-runner.d.ts +5 -5
  10. package/dist/function/function-runner.js +39 -111
  11. package/dist/function/functions.types.d.ts +17 -7
  12. package/dist/index.d.ts +5 -1
  13. package/dist/index.js +3 -1
  14. package/dist/middleware/auth-apikey.d.ts +1 -0
  15. package/dist/middleware/auth-bearer.d.ts +1 -0
  16. package/dist/middleware/auth-cookie.d.ts +1 -0
  17. package/dist/middleware/cors.d.ts +1 -0
  18. package/dist/middleware/index.d.ts +1 -1
  19. package/dist/middleware/index.js +1 -1
  20. package/dist/middleware/remote-auth.d.ts +1 -0
  21. package/dist/middleware/telemetry.d.ts +2 -0
  22. package/dist/middleware/timeout.d.ts +1 -0
  23. package/dist/permissions.d.ts +42 -27
  24. package/dist/permissions.js +101 -203
  25. package/dist/pikku-state.js +0 -2
  26. package/dist/scopes.d.ts +17 -0
  27. package/dist/scopes.js +59 -0
  28. package/dist/services/ai-embedding-service.d.ts +31 -0
  29. package/dist/services/ai-embedding-service.js +1 -0
  30. package/dist/services/credential-wire-service.d.ts +3 -1
  31. package/dist/services/credential-wire-service.js +10 -4
  32. package/dist/services/index.d.ts +4 -0
  33. package/dist/services/index.js +2 -0
  34. package/dist/services/meta-service.d.ts +15 -2
  35. package/dist/services/meta-service.js +51 -1
  36. package/dist/services/queue-webhook-service.d.ts +55 -0
  37. package/dist/services/queue-webhook-service.js +136 -0
  38. package/dist/services/scope-service.d.ts +67 -0
  39. package/dist/services/scope-service.js +1 -0
  40. package/dist/services/typed-secret-service.d.ts +8 -0
  41. package/dist/services/typed-secret-service.js +39 -4
  42. package/dist/services/webhook-service.d.ts +140 -0
  43. package/dist/services/webhook-service.js +44 -0
  44. package/dist/services/workflow-service.d.ts +7 -0
  45. package/dist/testing/service-tests.js +37 -0
  46. package/dist/types/core.types.d.ts +34 -6
  47. package/dist/types/state.types.d.ts +20 -2
  48. package/dist/utils/hmac.d.ts +16 -0
  49. package/dist/utils/hmac.js +26 -0
  50. package/dist/utils/safe-fetch.d.ts +51 -0
  51. package/dist/utils/safe-fetch.js +192 -0
  52. package/dist/wirings/ai-agent/ai-agent-agui.d.ts +84 -0
  53. package/dist/wirings/ai-agent/ai-agent-agui.js +305 -0
  54. package/dist/wirings/ai-agent/ai-agent-prepare.d.ts +105 -1
  55. package/dist/wirings/ai-agent/ai-agent-prepare.js +249 -11
  56. package/dist/wirings/ai-agent/ai-agent-runner.js +10 -1
  57. package/dist/wirings/ai-agent/ai-agent-stream.js +88 -16
  58. package/dist/wirings/ai-agent/ai-agent.types.d.ts +56 -2
  59. package/dist/wirings/ai-agent/index.d.ts +2 -1
  60. package/dist/wirings/ai-agent/index.js +2 -1
  61. package/dist/wirings/ai-agent/voice-input.d.ts +1 -0
  62. package/dist/wirings/ai-agent/voice-input.js +5 -41
  63. package/dist/wirings/ai-agent/voice-output.d.ts +1 -0
  64. package/dist/wirings/channel/channel-common.js +0 -1
  65. package/dist/wirings/channel/channel-handler.js +1 -4
  66. package/dist/wirings/channel/channel-middleware-runner.d.ts +12 -0
  67. package/dist/wirings/channel/channel-middleware-runner.js +34 -19
  68. package/dist/wirings/channel/channel.types.d.ts +2 -6
  69. package/dist/wirings/cli/cli-runner.js +3 -2
  70. package/dist/wirings/cli/cli.types.d.ts +1 -2
  71. package/dist/wirings/cli/command-parser.d.ts +2 -0
  72. package/dist/wirings/cli/command-parser.js +59 -2
  73. package/dist/wirings/credential/credential.types.d.ts +14 -0
  74. package/dist/wirings/credential/validate-credential-definitions.js +1 -0
  75. package/dist/wirings/gateway/gateway-runner.js +100 -50
  76. package/dist/wirings/gateway/gateway.types.d.ts +8 -5
  77. package/dist/wirings/http/http-routes.js +0 -3
  78. package/dist/wirings/http/http-runner.d.ts +9 -29
  79. package/dist/wirings/http/http-runner.js +9 -34
  80. package/dist/wirings/http/http.types.d.ts +4 -12
  81. package/dist/wirings/mcp/mcp-runner.js +0 -2
  82. package/dist/wirings/mcp/mcp.types.d.ts +5 -11
  83. package/dist/wirings/oauth2/index.d.ts +0 -3
  84. package/dist/wirings/oauth2/index.js +1 -2
  85. package/dist/wirings/rpc/addon-runner.d.ts +28 -0
  86. package/dist/wirings/rpc/addon-runner.js +173 -0
  87. package/dist/wirings/rpc/index.d.ts +5 -1
  88. package/dist/wirings/rpc/index.js +3 -1
  89. package/dist/wirings/rpc/remote-addon-auth.d.ts +26 -0
  90. package/dist/wirings/rpc/remote-addon-auth.js +43 -0
  91. package/dist/wirings/rpc/rpc-runner.d.ts +19 -0
  92. package/dist/wirings/rpc/rpc-runner.js +109 -3
  93. package/dist/wirings/rpc/rpc-types.d.ts +15 -1
  94. package/dist/wirings/rpc/wire-addon.js +9 -0
  95. package/dist/wirings/rpc/wire-remote-addon.d.ts +47 -0
  96. package/dist/wirings/rpc/wire-remote-addon.js +19 -0
  97. package/dist/wirings/scope/index.d.ts +3 -0
  98. package/dist/wirings/scope/index.js +2 -0
  99. package/dist/wirings/scope/scope.types.d.ts +39 -0
  100. package/dist/wirings/scope/scope.types.js +1 -0
  101. package/dist/wirings/scope/validate-scope-definitions.d.ts +16 -0
  102. package/dist/wirings/scope/validate-scope-definitions.js +76 -0
  103. package/dist/wirings/scope/wire-scope.d.ts +33 -0
  104. package/dist/wirings/scope/wire-scope.js +32 -0
  105. package/dist/wirings/secret/secret.types.d.ts +14 -0
  106. package/dist/wirings/secret/validate-secret-definitions.js +2 -0
  107. package/dist/wirings/variable/validate-variable-definitions.js +2 -0
  108. package/dist/wirings/variable/variable.types.d.ts +14 -0
  109. package/dist/wirings/workflow/dsl/index.d.ts +1 -1
  110. package/dist/wirings/workflow/dsl/workflow-dsl.types.d.ts +89 -7
  111. package/dist/wirings/workflow/graph/graph-node.d.ts +2 -0
  112. package/dist/wirings/workflow/graph/graph-runner.js +3 -0
  113. package/dist/wirings/workflow/graph/wire-workflow-graph.d.ts +6 -0
  114. package/dist/wirings/workflow/graph/wire-workflow-graph.js +1 -0
  115. package/dist/wirings/workflow/graph/workflow-graph.types.d.ts +5 -0
  116. package/dist/wirings/workflow/index.d.ts +2 -2
  117. package/dist/wirings/workflow/index.js +1 -1
  118. package/dist/wirings/workflow/pikku-workflow-service.d.ts +59 -1
  119. package/dist/wirings/workflow/pikku-workflow-service.js +187 -0
  120. package/dist/wirings/workflow/workflow.types.d.ts +1 -3
  121. package/package.json +6 -1
  122. package/run-tests.sh +1 -0
  123. package/src/dev/hot-reload.ts +13 -68
  124. package/src/dev/module-runner.test.ts +169 -0
  125. package/src/dev/module-runner.ts +103 -0
  126. package/src/dev/reload-meta.test.ts +31 -2
  127. package/src/dev/reload-meta.ts +26 -0
  128. package/src/errors/errors.ts +24 -0
  129. package/src/function/function-runner.test.ts +276 -142
  130. package/src/function/function-runner.ts +52 -145
  131. package/src/function/functions.types.ts +28 -38
  132. package/src/index.ts +8 -6
  133. package/src/middleware/index.ts +1 -5
  134. package/src/permissions.test.ts +160 -373
  135. package/src/permissions.ts +137 -279
  136. package/src/pikku-state.ts +0 -2
  137. package/src/scopes.test.ts +167 -0
  138. package/src/scopes.ts +69 -0
  139. package/src/services/ai-embedding-service.ts +31 -0
  140. package/src/services/credential-wire-service.ts +10 -4
  141. package/src/services/index.ts +20 -0
  142. package/src/services/meta-service.ts +68 -3
  143. package/src/services/queue-webhook-service.test.ts +408 -0
  144. package/src/services/queue-webhook-service.ts +182 -0
  145. package/src/services/scope-service.ts +75 -0
  146. package/src/services/typed-secret-service.test.ts +35 -0
  147. package/src/services/typed-secret-service.ts +39 -4
  148. package/src/services/webhook-service.ts +180 -0
  149. package/src/services/workflow-service.ts +7 -0
  150. package/src/testing/service-tests.ts +49 -0
  151. package/src/types/core.types.ts +42 -14
  152. package/src/types/state.types.ts +23 -3
  153. package/src/utils/hmac.ts +27 -0
  154. package/src/utils/safe-fetch.test.ts +373 -0
  155. package/src/utils/safe-fetch.ts +213 -0
  156. package/src/wirings/ai-agent/ai-agent-agui.test.ts +1127 -0
  157. package/src/wirings/ai-agent/ai-agent-agui.ts +386 -0
  158. package/src/wirings/ai-agent/ai-agent-authorization.test.ts +204 -0
  159. package/src/wirings/ai-agent/ai-agent-prepare.test.ts +483 -0
  160. package/src/wirings/ai-agent/ai-agent-prepare.ts +324 -13
  161. package/src/wirings/ai-agent/ai-agent-resume-authorization.test.ts +207 -0
  162. package/src/wirings/ai-agent/ai-agent-runner.test.ts +48 -1
  163. package/src/wirings/ai-agent/ai-agent-runner.ts +28 -0
  164. package/src/wirings/ai-agent/ai-agent-stream.test.ts +301 -10
  165. package/src/wirings/ai-agent/ai-agent-stream.ts +122 -16
  166. package/src/wirings/ai-agent/ai-agent.types.ts +57 -0
  167. package/src/wirings/ai-agent/index.ts +5 -0
  168. package/src/wirings/ai-agent/voice-input.test.ts +90 -0
  169. package/src/wirings/ai-agent/voice-input.ts +9 -40
  170. package/src/wirings/channel/channel-common.ts +0 -1
  171. package/src/wirings/channel/channel-handler.ts +1 -9
  172. package/src/wirings/channel/channel-middleware-runner.test.ts +28 -2
  173. package/src/wirings/channel/channel-middleware-runner.ts +39 -27
  174. package/src/wirings/channel/channel.types.ts +0 -6
  175. package/src/wirings/cli/cli-runner.ts +4 -2
  176. package/src/wirings/cli/cli.types.ts +0 -2
  177. package/src/wirings/cli/command-parser.test.ts +130 -0
  178. package/src/wirings/cli/command-parser.ts +80 -2
  179. package/src/wirings/credential/credential.types.ts +14 -0
  180. package/src/wirings/credential/validate-credential-definitions.ts +1 -0
  181. package/src/wirings/gateway/gateway-authorization.test.ts +444 -0
  182. package/src/wirings/gateway/gateway-runner.ts +114 -68
  183. package/src/wirings/gateway/gateway.types.ts +7 -9
  184. package/src/wirings/http/http-routes.ts +0 -3
  185. package/src/wirings/http/http-runner.test.ts +1 -11
  186. package/src/wirings/http/http-runner.ts +16 -39
  187. package/src/wirings/http/http.types.ts +7 -14
  188. package/src/wirings/mcp/mcp-runner.ts +0 -2
  189. package/src/wirings/mcp/mcp.types.ts +3 -11
  190. package/src/wirings/oauth2/index.ts +0 -3
  191. package/src/wirings/rpc/addon-runner.ts +233 -0
  192. package/src/wirings/rpc/index.ts +17 -1
  193. package/src/wirings/rpc/remote-addon-auth.ts +69 -0
  194. package/src/wirings/rpc/rpc-runner.test.ts +319 -16
  195. package/src/wirings/rpc/rpc-runner.ts +155 -3
  196. package/src/wirings/rpc/rpc-types.ts +16 -5
  197. package/src/wirings/rpc/wire-addon.test.ts +6 -3
  198. package/src/wirings/rpc/wire-addon.ts +9 -0
  199. package/src/wirings/rpc/wire-remote-addon.ts +57 -0
  200. package/src/wirings/scope/index.ts +14 -0
  201. package/src/wirings/scope/scope.test.ts +135 -0
  202. package/src/wirings/scope/scope.types.ts +44 -0
  203. package/src/wirings/scope/validate-scope-definitions.ts +110 -0
  204. package/src/wirings/scope/wire-scope.ts +34 -0
  205. package/src/wirings/secret/secret.types.ts +14 -0
  206. package/src/wirings/secret/validate-secret-definitions.ts +2 -0
  207. package/src/wirings/variable/validate-variable-definitions.ts +2 -0
  208. package/src/wirings/variable/variable.types.ts +14 -0
  209. package/src/wirings/workflow/dsl/index.ts +4 -0
  210. package/src/wirings/workflow/dsl/workflow-dsl.types.ts +97 -6
  211. package/src/wirings/workflow/graph/graph-node.ts +2 -0
  212. package/src/wirings/workflow/graph/graph-runner.test.ts +58 -0
  213. package/src/wirings/workflow/graph/graph-runner.ts +3 -0
  214. package/src/wirings/workflow/graph/wire-workflow-graph.ts +7 -0
  215. package/src/wirings/workflow/graph/workflow-graph.types.ts +5 -0
  216. package/src/wirings/workflow/index.ts +5 -0
  217. package/src/wirings/workflow/pikku-workflow-service.test.ts +341 -0
  218. package/src/wirings/workflow/pikku-workflow-service.ts +272 -0
  219. package/src/wirings/workflow/workflow-on-error.test.ts +154 -0
  220. package/src/wirings/workflow/workflow-step-session.test.ts +21 -5
  221. package/src/wirings/workflow/workflow.types.ts +4 -2
  222. package/tsconfig.tsbuildinfo +1 -1
  223. package/src/wirings/oauth2/oauth2-client.test.ts +0 -930
  224. package/src/wirings/oauth2/oauth2-client.ts +0 -331
  225. package/src/wirings/oauth2/oauth2-routes.ts +0 -234
@@ -17,6 +17,15 @@ export type CredentialMeta = {
17
17
  export class TypedSecretService<
18
18
  TMap = Record<string, unknown>,
19
19
  > implements SecretService {
20
+ /**
21
+ * In-process cache of resolved secrets, so callers can read naively without
22
+ * re-hitting the underlying service on every call. Only successful reads are
23
+ * cached (a miss throws and isn't stored); `setSecret`/`deleteSecret`
24
+ * invalidate the key. No TTL — a secret rotated out-of-band won't refresh
25
+ * until restart (see pikkujs/pikku#964).
26
+ */
27
+ private cache = new Map<string, unknown>()
28
+
20
29
  constructor(
21
30
  private secrets: SecretService,
22
31
  private credentialsMeta: Record<string, CredentialMeta>
@@ -25,10 +34,18 @@ export class TypedSecretService<
25
34
  async getSecret<K extends keyof TMap & string>(key: K): Promise<TMap[K]>
26
35
  async getSecret<T = string>(key: string): Promise<T>
27
36
  async getSecret(key: string): Promise<unknown> {
28
- return this.secrets.getSecret(key)
37
+ if (this.cache.has(key)) {
38
+ return this.cache.get(key)
39
+ }
40
+ const value = await this.secrets.getSecret(key)
41
+ this.cache.set(key, value)
42
+ return value
29
43
  }
30
44
 
31
45
  async hasSecret(key: string): Promise<boolean> {
46
+ if (this.cache.has(key)) {
47
+ return true
48
+ }
32
49
  return this.secrets.hasSecret(key)
33
50
  }
34
51
 
@@ -36,17 +53,35 @@ export class TypedSecretService<
36
53
  key: K,
37
54
  value: K extends keyof TMap ? TMap[K] : unknown
38
55
  ): Promise<void> {
39
- return this.secrets.setSecret(key, value)
56
+ await this.secrets.setSecret(key, value)
57
+ this.cache.delete(key)
40
58
  }
41
59
 
42
60
  async deleteSecret(key: string): Promise<void> {
43
- return this.secrets.deleteSecret(key)
61
+ await this.secrets.deleteSecret(key)
62
+ this.cache.delete(key)
44
63
  }
45
64
 
46
65
  async getSecrets<T extends Record<string, unknown> = Record<string, unknown>>(
47
66
  keys: (keyof T & string)[]
48
67
  ): Promise<Partial<T>> {
49
- return this.secrets.getSecrets<T>(keys)
68
+ const result: Partial<T> = {}
69
+ const missing: (keyof T & string)[] = []
70
+ for (const key of keys) {
71
+ if (this.cache.has(key)) {
72
+ result[key] = this.cache.get(key) as T[keyof T & string]
73
+ } else {
74
+ missing.push(key)
75
+ }
76
+ }
77
+ if (missing.length > 0) {
78
+ const fetched = await this.secrets.getSecrets<T>(missing)
79
+ for (const [key, value] of Object.entries(fetched)) {
80
+ this.cache.set(key, value)
81
+ result[key as keyof T & string] = value as T[keyof T & string]
82
+ }
83
+ }
84
+ return result
50
85
  }
51
86
 
52
87
  async getAllStatus(): Promise<CredentialStatus[]> {
@@ -0,0 +1,180 @@
1
+ import { NotImplementedError } from '../errors/errors.js'
2
+ import { hmacSha256Hex, timingSafeStringEqual } from '../utils/hmac.js'
3
+
4
+ export interface SendWebhookInput {
5
+ /** The endpoint the webhook is delivered to via HTTP POST. */
6
+ url: string
7
+ /** Optional event name, sent as the `X-Pikku-Event` header. */
8
+ event?: string
9
+ /** The payload, JSON-serialized into the request body. */
10
+ data: unknown
11
+ /** Extra headers merged over the defaults. */
12
+ headers?: Record<string, string>
13
+ /**
14
+ * Raw HMAC signing key for this delivery. Overrides the config-level
15
+ * secret. When present, the body is signed into the signature header.
16
+ */
17
+ secret?: string
18
+ /** Retry count for failed deliveries. Overrides `config.webhook.retries`. */
19
+ retries?: number
20
+ /**
21
+ * Delay between retries (`30000` or `'30s'`). A concrete value selects a
22
+ * fixed backoff; when omitted, backoff is exponential.
23
+ */
24
+ retryDelay?: string | number
25
+ /**
26
+ * Organization this delivery belongs to. Persisted by a store-backed
27
+ * implementation (e.g. `KyselyWebhookService`) so deliveries can be
28
+ * queried per org; ignored by the queue-only default.
29
+ */
30
+ organizationId?: string
31
+ }
32
+
33
+ export interface SendWebhookResult {
34
+ jobId: string
35
+ }
36
+
37
+ export interface WebhookServiceConfig {
38
+ /** Default retry count for failed deliveries. */
39
+ retries?: number
40
+ /** Default delay between retries; omitted means exponential backoff. */
41
+ retryDelay?: string | number
42
+ /**
43
+ * Name of a secret (resolved through the secret service) used as the
44
+ * default HMAC signing key for all outgoing webhooks.
45
+ */
46
+ secret?: string
47
+ /**
48
+ * Header the body signature is sent in. Defaults to
49
+ * {@link DEFAULT_WEBHOOK_SIGNATURE_HEADER}.
50
+ */
51
+ signatureHeader?: string
52
+ /**
53
+ * SSRF allowlist of hostnames outgoing webhooks may be delivered to. When set,
54
+ * a delivery target must be on this list. When omitted, obvious private and
55
+ * internal hosts (loopback, private ranges, cloud metadata) are blocked and
56
+ * all other public hosts are permitted.
57
+ */
58
+ allowedHosts?: string[]
59
+ }
60
+
61
+ export interface WebhookJobData {
62
+ url: string
63
+ event?: string
64
+ body: string
65
+ headers: Record<string, string>
66
+ /**
67
+ * Delivery-record id, present when a store-backed implementation enqueued
68
+ * the job. The worker keys its per-attempt records off this (it doubles as
69
+ * the queue `jobId`), so it's stable across retries.
70
+ */
71
+ deliveryId?: string
72
+ }
73
+
74
+ /** Outcome of one delivery attempt, recorded against a delivery. */
75
+ export interface WebhookAttemptResult {
76
+ /** HTTP status of the response, absent if the request never completed. */
77
+ statusCode?: number
78
+ /** Response body (truncated) — captured on failure for debugging. */
79
+ responseBody?: string
80
+ /** Network/timeout error message, when the request threw. */
81
+ error?: string
82
+ /** Whether this attempt was a 2xx success. */
83
+ delivered: boolean
84
+ }
85
+
86
+ /** A persisted webhook delivery, as surfaced to the console. */
87
+ export interface WebhookDeliveryRecord {
88
+ deliveryId: string
89
+ organizationId: string | null
90
+ url: string
91
+ event: string | null
92
+ status: 'pending' | 'delivered' | 'failed'
93
+ attempts: number
94
+ createdAt: Date
95
+ updatedAt: Date
96
+ deliveredAt: Date | null
97
+ }
98
+
99
+ /** A single persisted delivery attempt. */
100
+ export interface WebhookAttemptRecord {
101
+ attemptId: string
102
+ deliveryId: string
103
+ attemptNumber: number
104
+ statusCode: number | null
105
+ responseBody: string | null
106
+ error: string | null
107
+ createdAt: Date
108
+ }
109
+
110
+ /** A delivery with its full attempt history. */
111
+ export interface WebhookDeliveryWithAttempts {
112
+ delivery: WebhookDeliveryRecord
113
+ attempts: WebhookAttemptRecord[]
114
+ }
115
+
116
+ export const PIKKU_OUTGOING_WEBHOOK_QUEUE_NAME = 'pikku-outgoing-webhooks'
117
+
118
+ export const DEFAULT_WEBHOOK_SIGNATURE_HEADER = 'X-Pikku-Signature'
119
+
120
+ /** Fallback when neither the per-call nor `config.webhook.retries` is set. */
121
+ export const DEFAULT_WEBHOOK_RETRIES = 3
122
+
123
+ /**
124
+ * Outgoing webhook delivery. {@link QueueWebhookService} is the default
125
+ * implementation; apps can extend this to deliver directly, or through a
126
+ * provider such as Svix.
127
+ */
128
+ export abstract class WebhookService {
129
+ abstract send(input: SendWebhookInput): Promise<SendWebhookResult>
130
+
131
+ /**
132
+ * Sign a body with HMAC-SHA256, producing the signature header value
133
+ * (e.g. `sha256=abc123...`).
134
+ */
135
+ protected sign(secret: string, body: string): string {
136
+ return `sha256=${hmacSha256Hex(secret, body)}`
137
+ }
138
+
139
+ /**
140
+ * Verify a signature produced by {@link WebhookService.sign} — for
141
+ * receivers, which share the signing scheme.
142
+ */
143
+ public verify(secret: string, signature: string, body: string): boolean {
144
+ return timingSafeStringEqual(this.sign(secret, body), signature)
145
+ }
146
+
147
+ /**
148
+ * Record a delivery attempt and roll the delivery's status/attempt-count
149
+ * forward, keyed by the delivery id carried in {@link WebhookJobData}. The
150
+ * default queue-only service keeps no history, so this throws — a store-backed
151
+ * implementation (e.g. `KyselyWebhookService` in `@pikku/kysely`) overrides it.
152
+ */
153
+ public recordAttempt(
154
+ _deliveryId: string,
155
+ _result: WebhookAttemptResult
156
+ ): Promise<void> {
157
+ throw new NotImplementedError(
158
+ 'webhook delivery persistence is not configured'
159
+ )
160
+ }
161
+
162
+ /** List deliveries, most recent first, optionally scoped to an org. */
163
+ public listDeliveries(_opts?: {
164
+ organizationId?: string
165
+ limit?: number
166
+ }): Promise<WebhookDeliveryRecord[]> {
167
+ throw new NotImplementedError(
168
+ 'webhook delivery persistence is not configured'
169
+ )
170
+ }
171
+
172
+ /** A single delivery with its attempt history, or null if unknown. */
173
+ public getDelivery(
174
+ _deliveryId: string
175
+ ): Promise<WebhookDeliveryWithAttempts | null> {
176
+ throw new NotImplementedError(
177
+ 'webhook delivery persistence is not configured'
178
+ )
179
+ }
180
+ }
@@ -50,6 +50,13 @@ export interface WorkflowService {
50
50
 
51
51
  // Orchestration operations
52
52
  resumeWorkflow(runId: string): Promise<void>
53
+ /**
54
+ * Record a human decision against a `workflow.approval()` gate and wake the
55
+ * run. The payload is stored as given and validated on replay, inside the
56
+ * workflow body — so an invalid one re-closes the gate rather than failing the
57
+ * run. `reason` addresses the gate's first reach.
58
+ */
59
+ approveStep(runId: string, reason: string, decision: unknown): Promise<void>
53
60
  startWorkflow<I>(
54
61
  name: string,
55
62
  input: I,
@@ -1086,6 +1086,55 @@ export function defineServiceTests(config: ServiceTestConfig): void {
1086
1086
  assert.ok(Array.isArray(threads))
1087
1087
  })
1088
1088
 
1089
+ if (services.aiStorageService) {
1090
+ const storageFactory = services.aiStorageService
1091
+
1092
+ // The `owners` constraint is what keeps the generated thread-management
1093
+ // functions from leaking across tenants: a caller may only list threads
1094
+ // owned by one of their session principals, matching the
1095
+ // `principal:sub-partition` composition resolveOwnerResourceId writes.
1096
+ test('listThreads scopes to the given owners, including sub-partitions', async () => {
1097
+ const storage = await storageFactory()
1098
+ await storage.createThread('owner-alice')
1099
+ await storage.createThread('owner-alice:project-1')
1100
+ await storage.createThread('owner-bob:secret')
1101
+
1102
+ const threads = await agentService.listThreads({
1103
+ owners: ['owner-alice'],
1104
+ })
1105
+ const ids = threads.map((t) => t.resourceId)
1106
+
1107
+ assert.ok(ids.includes('owner-alice'))
1108
+ assert.ok(ids.includes('owner-alice:project-1'))
1109
+ assert.ok(
1110
+ !ids.some((id) => id.startsWith('owner-bob')),
1111
+ "another owner's threads must not be listed"
1112
+ )
1113
+ })
1114
+
1115
+ test('listThreads with an owner does not match a lookalike prefix', async () => {
1116
+ const storage = await storageFactory()
1117
+ await storage.createThread('owner-al')
1118
+ await storage.createThread('owner-alice-evil:p')
1119
+
1120
+ const threads = await agentService.listThreads({
1121
+ owners: ['owner-al'],
1122
+ })
1123
+ const ids = threads.map((t) => t.resourceId)
1124
+
1125
+ assert.ok(ids.includes('owner-al'))
1126
+ assert.ok(!ids.includes('owner-alice-evil:p'))
1127
+ })
1128
+
1129
+ test('listThreads with an empty owners list returns nothing', async () => {
1130
+ const storage = await storageFactory()
1131
+ await storage.createThread('owner-empty-check')
1132
+
1133
+ const threads = await agentService.listThreads({ owners: [] })
1134
+ assert.deepEqual(threads, [])
1135
+ })
1136
+ }
1137
+
1089
1138
  test('getThread returns null for missing', async () => {
1090
1139
  const thread = await agentService.getThread('missing-thread')
1091
1140
  assert.equal(thread, null)
@@ -32,15 +32,21 @@ import type { AIStorageService } from '../services/ai-storage-service.js'
32
32
  import type { ContentService } from '../services/content-service.js'
33
33
  import type { ScenarioActors } from '../services/scenario-actors-service.js'
34
34
  import type { AIAgentRunnerService } from '../services/ai-agent-runner-service.js'
35
+ import type { AIEmbeddingService } from '../services/ai-embedding-service.js'
35
36
  import type { AIRunStateService } from '../services/ai-run-state-service.js'
36
37
  import type { AgentRunService } from '../wirings/ai-agent/ai-agent.types.js'
37
38
  import type { PikkuAIMiddlewareHooks } from '../wirings/ai-agent/ai-agent.types.js'
38
39
  import type { WorkflowRunService } from '../wirings/workflow/workflow.types.js'
39
40
  import type { CredentialService } from '../services/credential-service.js'
40
41
  import type { EmailService } from '../services/email-service.js'
42
+ import type {
43
+ WebhookService,
44
+ WebhookServiceConfig,
45
+ } from '../services/webhook-service.js'
41
46
  import type { MetaService } from '../services/meta-service.js'
42
47
  import type { CoverageService } from '../services/v8-coverage-service.js'
43
48
  import type { SessionStore } from '../services/session-store.js'
49
+ import type { ScopeService } from '../services/scope-service.js'
44
50
  import type {
45
51
  AuditDurability,
46
52
  AuditLog,
@@ -97,25 +103,24 @@ export type MiddlewareMetadata =
97
103
  * - type: 'tag' = Tag-based permission group (references tagGroup in pikkuState)
98
104
  * - type: 'wire' = Wire-level individual permission
99
105
  */
100
- export type PermissionMetadata =
101
- | {
102
- type: 'http'
103
- route: string // Route pattern (e.g., '*' for all, '/api/*' for specific)
104
- }
105
- | {
106
- type: 'tag'
107
- tag: string // Tag name
108
- }
109
- | {
110
- type: 'wire'
111
- name: string
112
- inline?: boolean // true if inline permission
113
- }
106
+ /**
107
+ * A reference to a permission function a function declares, resolved by name.
108
+ * Used at filter time (e.g. listing agent tools) to run the session-only
109
+ * (pikkuAuth) predicates without request data. Permissions are function-scoped
110
+ * only — there are no wire- or tag-level permission references.
111
+ */
112
+ export type PermissionMetadata = {
113
+ type: 'wire'
114
+ name: string
115
+ inline?: boolean // true if inline permission
116
+ }
114
117
 
115
118
  export type FunctionRuntimeMeta = {
116
119
  pikkuFuncId: string
117
120
  inputSchemaName: string | null
118
121
  outputSchemaName: string | null
122
+ /** Scopes the session must hold to run this function. All are required (AND). */
123
+ scopes?: string[]
119
124
  expose?: boolean
120
125
  remote?: boolean
121
126
  mcp?: boolean
@@ -242,6 +247,8 @@ export type CoreConfig<Config extends Record<string, unknown> = {}> = {
242
247
  secrets?: {}
243
248
 
244
249
  workflow?: WorkflowServiceConfig
250
+ /** Default retry and signing settings for outgoing webhooks. */
251
+ webhook?: WebhookServiceConfig
245
252
  /** Runtime Postgres adapter options (pool sizing). */
246
253
  postgres?: PostgresConfig
247
254
  } & Config
@@ -254,6 +261,12 @@ export interface CoreUserSession {
254
261
  orgId?: string
255
262
  /** True when the session belongs to a synthetic scenario actor — lets audits/analytics address synthetic traffic */
256
263
  actor?: boolean
264
+ /**
265
+ * Scopes granted to this session, checked against a function's `scopes`.
266
+ * Populated by whoever builds the session (e.g. better-auth's `mapSession`
267
+ * resolving them via a ScopeService) — core reads them and never fetches.
268
+ */
269
+ scopes?: string[]
257
270
  }
258
271
 
259
272
  /**
@@ -289,6 +302,8 @@ export interface CoreSingletonServices<Config extends CoreConfig = CoreConfig> {
289
302
  content?: ContentService
290
303
  /** AI agent runner service (model calls + tool loop) */
291
304
  aiAgentRunner?: AIAgentRunnerService
305
+ /** Dedicated embedding service (vector stores use it at index & query time) */
306
+ aiEmbedding?: AIEmbeddingService
292
307
  /** AI run state service (run lifecycle + approval persistence) */
293
308
  aiRunState?: AIRunStateService
294
309
  /** Agent run service (listing threads, runs, steps) */
@@ -299,6 +314,12 @@ export interface CoreSingletonServices<Config extends CoreConfig = CoreConfig> {
299
314
  credentialService?: CredentialService
300
315
  /** Email service for outbound messages and template-backed delivery */
301
316
  emailService?: EmailService
317
+ /**
318
+ * Webhook service for outgoing webhook delivery via a queue. A store-backed
319
+ * implementation (e.g. `KyselyWebhookService`) additionally records delivery
320
+ * history; the queue-only default throws on the delivery-read methods.
321
+ */
322
+ webhookService?: WebhookService
302
323
  /** Meta service for reading .pikku metadata files (filesystem on Node, R2/KV on CF) */
303
324
  metaService?: MetaService
304
325
  /** V8 precise-coverage collector (`pikku dev --coverage` only) */
@@ -315,6 +336,11 @@ export interface CoreSingletonServices<Config extends CoreConfig = CoreConfig> {
315
336
  auditLog?: AuditLog
316
337
  /** Session store for persisting user sessions keyed by pikkuUserId */
317
338
  sessionStore?: SessionStore
339
+ /**
340
+ * Resolves and administers user scopes. Called when building a session (e.g.
341
+ * better-auth's `mapSession`), never by the function runner.
342
+ */
343
+ scopeService?: ScopeService
318
344
  }
319
345
 
320
346
  /**
@@ -341,6 +367,8 @@ export type PikkuWire<
341
367
  traceId: string
342
368
  /** Function id for the current invocation */
343
369
  functionId: string
370
+ /** The addon instance namespace (wireAddon name) currently executing, if any */
371
+ addonNamespace: string
344
372
  http: PikkuHTTP<In>
345
373
  mcp: PikkuMCP<MCPTools>
346
374
  channel: [IsChannel] extends [null]
@@ -84,7 +84,29 @@ export interface PikkuPackageState {
84
84
  addons: {
85
85
  packages: Map<
86
86
  string,
87
- { package: string; rpcEndpoint?: string; auth?: boolean; tags?: string[] }
87
+ {
88
+ package: string
89
+ rpcEndpoint?: string
90
+ auth?: boolean
91
+ tags?: string[]
92
+ /** Per-instance name-aliases: logical name the addon reads -> actual project secret name */
93
+ secretOverrides?: Record<string, string>
94
+ /** Per-instance name-aliases: logical name the addon reads -> actual project variable name */
95
+ variableOverrides?: Record<string, string>
96
+ /** Per-instance name-aliases: logical name the addon reads -> actual project credential name */
97
+ credentialOverrides?: Record<string, string>
98
+ /** Set by `wireRemoteAddon`: this namespace is consumed remotely over HTTP, not bundled */
99
+ remote?: boolean
100
+ /** Remote host base URL (wireRemoteAddon) — string or resolver over singleton services */
101
+ serverUrl?: string | ((services: any) => string | Promise<string>)
102
+ /** Consumer-side auth binding for the hosted addon (wireRemoteAddon) */
103
+ remoteAuth?:
104
+ | { credentialId: string }
105
+ | { secretId: string }
106
+ | { resolve: (services: any, wire: any) => string | Promise<string> }
107
+ /** Map consumer-facing fn name → remote fn name (wireRemoteAddon) */
108
+ remoteName?: (fn: string) => string
109
+ }
88
110
  >
89
111
  }
90
112
  http: {
@@ -144,8 +166,6 @@ export interface PikkuPackageState {
144
166
  tagGroup: Record<string, CorePikkuChannelMiddleware[]>
145
167
  }
146
168
  permissions: {
147
- tagGroup: Record<string, CorePermissionGroup | CorePikkuPermission[]>
148
- httpGroup: Record<string, CorePermissionGroup | CorePikkuPermission[]>
149
169
  global: (CorePermissionGroup | CorePikkuPermission)[]
150
170
  }
151
171
  misc: {
@@ -0,0 +1,27 @@
1
+ import { createHmac, timingSafeEqual } from 'node:crypto'
2
+
3
+ /**
4
+ * HMAC-SHA256 a payload, hex-encoded.
5
+ *
6
+ * Senders wrap this in their own scheme prefix (`sha256=`, `v0=`, …).
7
+ *
8
+ * @param secret - The signing key shared with the other side
9
+ * @param payload - The exact bytes that were signed
10
+ */
11
+ export function hmacSha256Hex(secret: string, payload: string): string {
12
+ return createHmac('sha256', secret).update(payload).digest('hex')
13
+ }
14
+
15
+ /**
16
+ * Compare two signatures without leaking their contents through timing.
17
+ *
18
+ * Returns false rather than throwing on a length mismatch, which is what
19
+ * `timingSafeEqual` does when the buffers differ in size.
20
+ */
21
+ export function timingSafeStringEqual(a: string, b: string): boolean {
22
+ try {
23
+ return timingSafeEqual(Buffer.from(a), Buffer.from(b))
24
+ } catch {
25
+ return false
26
+ }
27
+ }