@pikku/core 0.12.19 → 0.12.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +77 -0
- package/dist/dev/hot-reload.js +20 -6
- package/dist/errors/error-handler.d.ts +1 -1
- package/dist/errors/error-handler.js +2 -2
- package/dist/function/function-runner.js +53 -3
- package/dist/function/functions.types.d.ts +3 -2
- package/dist/index.d.ts +3 -3
- package/dist/index.js +2 -2
- package/dist/middleware/index.d.ts +2 -2
- package/dist/middleware/index.js +2 -2
- package/dist/middleware/telemetry.d.ts +2 -2
- package/dist/middleware/telemetry.js +2 -2
- package/dist/middleware-runner.d.ts +15 -27
- package/dist/middleware-runner.js +25 -30
- package/dist/permissions.d.ts +15 -22
- package/dist/permissions.js +30 -25
- package/dist/pikku-request.js +1 -1
- package/dist/pikku-state.js +2 -3
- package/dist/services/ai-agent-runner-service.d.ts +1 -0
- package/dist/services/content-service.d.ts +66 -37
- package/dist/services/in-memory-queue-service.js +1 -1
- package/dist/services/in-memory-workflow-service.d.ts +15 -13
- package/dist/services/in-memory-workflow-service.js +31 -13
- package/dist/services/index.d.ts +1 -1
- package/dist/services/local-content.d.ts +10 -12
- package/dist/services/local-content.js +54 -38
- package/dist/services/user-session-service.d.ts +1 -1
- package/dist/testing/service-tests.js +24 -0
- package/dist/types/core.types.d.ts +4 -0
- package/dist/types/state.types.d.ts +2 -18
- package/dist/wirings/ai-agent/ai-agent-memory.d.ts +24 -5
- package/dist/wirings/ai-agent/ai-agent-memory.js +128 -23
- package/dist/wirings/ai-agent/ai-agent-model-config.d.ts +10 -1
- package/dist/wirings/ai-agent/ai-agent-model-config.js +15 -35
- package/dist/wirings/ai-agent/ai-agent-prepare.js +4 -1
- package/dist/wirings/ai-agent/ai-agent-runner.js +45 -31
- package/dist/wirings/ai-agent/ai-agent-stream.js +63 -34
- package/dist/wirings/ai-agent/ai-agent.types.d.ts +20 -0
- package/dist/wirings/channel/channel-handler.js +1 -1
- package/dist/wirings/channel/channel-store.d.ts +13 -0
- package/dist/wirings/channel/channel.types.d.ts +3 -0
- package/dist/wirings/channel/local/local-channel-runner.js +23 -5
- package/dist/wirings/channel/pikku-abstract-channel-handler.js +8 -0
- package/dist/wirings/channel/serverless/serverless-channel-runner.js +9 -0
- package/dist/wirings/cli/cli-runner.js +24 -5
- package/dist/wirings/cli/command-parser.js +19 -4
- package/dist/wirings/http/http-runner.js +72 -36
- package/dist/wirings/http/http.types.d.ts +1 -0
- package/dist/wirings/http/pikku-fetch-http-request.js +3 -1
- package/dist/wirings/http/web-request.js +32 -0
- package/dist/wirings/rpc/rpc-runner.d.ts +3 -2
- package/dist/wirings/rpc/rpc-runner.js +45 -11
- package/dist/wirings/workflow/graph/graph-node.d.ts +8 -0
- package/dist/wirings/workflow/graph/graph-node.js +4 -0
- package/dist/wirings/workflow/graph/graph-runner.js +12 -10
- package/dist/wirings/workflow/graph/workflow-graph.types.d.ts +4 -0
- package/dist/wirings/workflow/index.d.ts +1 -1
- package/dist/wirings/workflow/pikku-workflow-service.d.ts +81 -16
- package/dist/wirings/workflow/pikku-workflow-service.js +266 -45
- package/dist/wirings/workflow/workflow.types.d.ts +34 -0
- package/package.json +1 -1
- package/run-tests.sh +4 -1
- package/src/dev/hot-reload.test.ts +62 -11
- package/src/dev/hot-reload.ts +28 -7
- package/src/errors/error-handler.ts +8 -2
- package/src/errors/error.test.ts +2 -0
- package/src/function/function-runner.test.ts +500 -10
- package/src/function/function-runner.ts +68 -3
- package/src/function/functions.types.ts +3 -2
- package/src/index.ts +22 -3
- package/src/middleware/index.ts +11 -2
- package/src/middleware/telemetry.ts +2 -2
- package/src/middleware-runner.test.ts +16 -16
- package/src/middleware-runner.ts +42 -30
- package/src/permissions.test.ts +27 -24
- package/src/permissions.ts +41 -25
- package/src/pikku-request.test.ts +35 -0
- package/src/pikku-request.ts +1 -1
- package/src/pikku-state.ts +2 -3
- package/src/run-tests-script.test.ts +18 -0
- package/src/services/ai-agent-runner-service.ts +1 -0
- package/src/services/content-service.ts +79 -51
- package/src/services/in-memory-queue-service.ts +1 -1
- package/src/services/in-memory-session-store.ts +1 -2
- package/src/services/in-memory-workflow-service.test.ts +33 -11
- package/src/services/in-memory-workflow-service.ts +49 -13
- package/src/services/index.ts +10 -1
- package/src/services/local-content.test.ts +54 -0
- package/src/services/local-content.ts +80 -53
- package/src/services/typed-credential-service.ts +3 -3
- package/src/services/typed-secret-service.ts +3 -3
- package/src/services/typed-variables-service.ts +3 -3
- package/src/services/user-session-service.ts +4 -4
- package/src/testing/service-tests.ts +30 -0
- package/src/types/core.types.ts +6 -2
- package/src/types/state.types.ts +2 -13
- package/src/wirings/ai-agent/ai-agent-memory.test.ts +324 -0
- package/src/wirings/ai-agent/ai-agent-memory.ts +187 -36
- package/src/wirings/ai-agent/ai-agent-model-config.test.ts +12 -90
- package/src/wirings/ai-agent/ai-agent-model-config.ts +14 -38
- package/src/wirings/ai-agent/ai-agent-prepare.test.ts +292 -0
- package/src/wirings/ai-agent/ai-agent-prepare.ts +4 -1
- package/src/wirings/ai-agent/ai-agent-registry.test.ts +230 -3
- package/src/wirings/ai-agent/ai-agent-runner.test.ts +625 -6
- package/src/wirings/ai-agent/ai-agent-runner.ts +65 -50
- package/src/wirings/ai-agent/ai-agent-stream.test.ts +544 -5
- package/src/wirings/ai-agent/ai-agent-stream.ts +71 -69
- package/src/wirings/ai-agent/ai-agent.types.ts +24 -0
- package/src/wirings/channel/channel-handler.test.ts +272 -0
- package/src/wirings/channel/channel-handler.ts +1 -1
- package/src/wirings/channel/channel-middleware-runner.test.ts +163 -0
- package/src/wirings/channel/channel-store.ts +19 -0
- package/src/wirings/channel/channel.types.ts +4 -0
- package/src/wirings/channel/local/local-channel-runner.test.ts +63 -0
- package/src/wirings/channel/local/local-channel-runner.ts +41 -5
- package/src/wirings/channel/local/local-eventhub-service.ts +3 -3
- package/src/wirings/channel/pikku-abstract-channel-handler.ts +9 -2
- package/src/wirings/channel/serverless/serverless-channel-runner.ts +23 -5
- package/src/wirings/cli/channel/cli-raw-channel-runner.ts +7 -2
- package/src/wirings/cli/cli-runner.test.ts +255 -2
- package/src/wirings/cli/cli-runner.ts +31 -10
- package/src/wirings/cli/cli.types.ts +4 -8
- package/src/wirings/cli/command-parser.test.ts +83 -0
- package/src/wirings/cli/command-parser.ts +23 -4
- package/src/wirings/http/http-runner.test.ts +296 -1
- package/src/wirings/http/http-runner.ts +87 -57
- package/src/wirings/http/http.types.ts +11 -26
- package/src/wirings/http/pikku-fetch-http-request.ts +8 -4
- package/src/wirings/http/pikku-fetch-http-response.test.ts +41 -0
- package/src/wirings/http/web-request.test.ts +115 -0
- package/src/wirings/http/web-request.ts +43 -5
- package/src/wirings/mcp/mcp-runner.test.ts +367 -0
- package/src/wirings/queue/queue.types.ts +2 -5
- package/src/wirings/rpc/rpc-runner.test.ts +511 -0
- package/src/wirings/rpc/rpc-runner.ts +60 -21
- package/src/wirings/rpc/wire-addon.test.ts +57 -0
- package/src/wirings/workflow/graph/graph-node.ts +12 -0
- package/src/wirings/workflow/graph/graph-runner.test.ts +98 -0
- package/src/wirings/workflow/graph/graph-runner.ts +28 -10
- package/src/wirings/workflow/graph/workflow-graph.types.ts +4 -0
- package/src/wirings/workflow/index.ts +1 -0
- package/src/wirings/workflow/pikku-workflow-service.test.ts +19 -2
- package/src/wirings/workflow/pikku-workflow-service.ts +370 -71
- package/src/wirings/workflow/workflow-queue-workers.test.ts +131 -0
- package/src/wirings/workflow/workflow.types.ts +68 -5
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,511 @@
|
|
|
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 {
|
|
7
|
+
ContextAwareRPCService,
|
|
8
|
+
RPCNotFoundError,
|
|
9
|
+
resolveNamespace,
|
|
10
|
+
rpcService,
|
|
11
|
+
} from './rpc-runner.js'
|
|
12
|
+
|
|
13
|
+
const createLogger = () => ({
|
|
14
|
+
debug: () => {},
|
|
15
|
+
info: () => {},
|
|
16
|
+
warn: () => {},
|
|
17
|
+
error: () => {},
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
const registerFunction = (
|
|
21
|
+
funcName: string,
|
|
22
|
+
func: (services: any, data: any, wire: any) => Promise<unknown> | unknown,
|
|
23
|
+
{
|
|
24
|
+
packageName = null,
|
|
25
|
+
expose,
|
|
26
|
+
pikkuFuncId,
|
|
27
|
+
tags,
|
|
28
|
+
}: {
|
|
29
|
+
packageName?: string | null
|
|
30
|
+
expose?: boolean
|
|
31
|
+
pikkuFuncId?: string
|
|
32
|
+
tags?: string[]
|
|
33
|
+
} = {}
|
|
34
|
+
) => {
|
|
35
|
+
addFunction(funcName, { func } as never, packageName)
|
|
36
|
+
pikkuState(packageName, 'function', 'meta')[funcName] = {
|
|
37
|
+
name: funcName,
|
|
38
|
+
pikkuFuncId,
|
|
39
|
+
sessionless: true,
|
|
40
|
+
permissions: [],
|
|
41
|
+
expose,
|
|
42
|
+
tags,
|
|
43
|
+
} as never
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const createServices = (overrides: Record<string, unknown> = {}) =>
|
|
47
|
+
({
|
|
48
|
+
logger: createLogger(),
|
|
49
|
+
...overrides,
|
|
50
|
+
}) as never
|
|
51
|
+
|
|
52
|
+
beforeEach(() => {
|
|
53
|
+
resetPikkuState()
|
|
54
|
+
pikkuState(null, 'package', 'singletonServices', createServices())
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
describe('resolveNamespace', () => {
|
|
58
|
+
test('returns null for non-namespaced functions', () => {
|
|
59
|
+
assert.equal(resolveNamespace('plainFunction'), null)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
test('returns null when namespace is not registered', () => {
|
|
63
|
+
assert.equal(resolveNamespace('missing:func'), null)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
test('resolves addon namespace to package and function', () => {
|
|
67
|
+
pikkuState(null, 'addons', 'packages').set('stripe', {
|
|
68
|
+
package: '@addon/stripe',
|
|
69
|
+
auth: true,
|
|
70
|
+
tags: ['addon-tag'],
|
|
71
|
+
} as never)
|
|
72
|
+
|
|
73
|
+
assert.deepEqual(resolveNamespace('stripe:createCharge'), {
|
|
74
|
+
package: '@addon/stripe',
|
|
75
|
+
function: 'createCharge',
|
|
76
|
+
addonConfig: {
|
|
77
|
+
package: '@addon/stripe',
|
|
78
|
+
auth: true,
|
|
79
|
+
tags: ['addon-tag'],
|
|
80
|
+
},
|
|
81
|
+
})
|
|
82
|
+
})
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
describe('ContextAwareRPCService.rpc', () => {
|
|
86
|
+
test('runs bare rpc functions through root rpc metadata', async () => {
|
|
87
|
+
pikkuState(null, 'rpc', 'meta').echo = 'echoFunc'
|
|
88
|
+
registerFunction('echoFunc', async (_services, data, wire) => ({
|
|
89
|
+
data,
|
|
90
|
+
traceId: wire.traceId,
|
|
91
|
+
}))
|
|
92
|
+
|
|
93
|
+
const service = new ContextAwareRPCService(
|
|
94
|
+
createServices(),
|
|
95
|
+
{ traceId: 'trace-1' } as never,
|
|
96
|
+
{ requiresAuth: false }
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
const result = await service.rpc('echo', { hello: 'world' })
|
|
100
|
+
|
|
101
|
+
assert.deepEqual(result, {
|
|
102
|
+
data: { hello: 'world' },
|
|
103
|
+
traceId: 'trace-1',
|
|
104
|
+
})
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
test('uses package-scoped function meta before root rpc metadata', async () => {
|
|
108
|
+
pikkuState(null, 'rpc', 'meta').echo = 'rootEchoFunc'
|
|
109
|
+
registerFunction('rootEchoFunc', async () => ({ source: 'root' }))
|
|
110
|
+
registerFunction(
|
|
111
|
+
'packageEchoFunc',
|
|
112
|
+
async () => ({ source: '@addon/pkg' }),
|
|
113
|
+
{
|
|
114
|
+
packageName: '@addon/pkg',
|
|
115
|
+
}
|
|
116
|
+
)
|
|
117
|
+
pikkuState('@addon/pkg', 'function', 'meta').echo = {
|
|
118
|
+
name: 'echo',
|
|
119
|
+
pikkuFuncId: 'packageEchoFunc',
|
|
120
|
+
sessionless: true,
|
|
121
|
+
permissions: [],
|
|
122
|
+
} as never
|
|
123
|
+
|
|
124
|
+
const service = new ContextAwareRPCService(
|
|
125
|
+
createServices(),
|
|
126
|
+
{} as never,
|
|
127
|
+
{},
|
|
128
|
+
'@addon/pkg'
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
const result = await service.rpc('echo', {})
|
|
132
|
+
|
|
133
|
+
assert.deepEqual(result, { source: '@addon/pkg' })
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
test('falls back from versioned rpc names to the base metadata entry', async () => {
|
|
137
|
+
pikkuState(null, 'rpc', 'meta').echo = 'echoFunc'
|
|
138
|
+
registerFunction('echoFunc', async (_services, data) => data)
|
|
139
|
+
|
|
140
|
+
const service = new ContextAwareRPCService(
|
|
141
|
+
createServices(),
|
|
142
|
+
{} as never,
|
|
143
|
+
{}
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
const result = await service.rpc('echo@v2', { ok: true })
|
|
147
|
+
|
|
148
|
+
assert.deepEqual(result, { ok: true })
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
test('resolves internal root functions that are registered outside rpc metadata', async () => {
|
|
152
|
+
registerFunction('pikkuWorkflowSleeper', async (_services, data) => ({
|
|
153
|
+
sleeper: true,
|
|
154
|
+
data,
|
|
155
|
+
}))
|
|
156
|
+
|
|
157
|
+
const service = new ContextAwareRPCService(
|
|
158
|
+
createServices(),
|
|
159
|
+
{} as never,
|
|
160
|
+
{}
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
const result = await service.rpc('pikkuWorkflowSleeper', {
|
|
164
|
+
runId: 'run-1',
|
|
165
|
+
stepId: 'step-1',
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
assert.deepEqual(result, {
|
|
169
|
+
sleeper: true,
|
|
170
|
+
data: {
|
|
171
|
+
runId: 'run-1',
|
|
172
|
+
stepId: 'step-1',
|
|
173
|
+
},
|
|
174
|
+
})
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
test('invokes addon functions with addon auth and tags', async () => {
|
|
178
|
+
pikkuState(null, 'addons', 'packages').set('stripe', {
|
|
179
|
+
package: '@addon/stripe',
|
|
180
|
+
auth: false,
|
|
181
|
+
tags: ['addon-tag'],
|
|
182
|
+
} as never)
|
|
183
|
+
registerFunction(
|
|
184
|
+
'createCharge',
|
|
185
|
+
async (_services, data, wire) => ({
|
|
186
|
+
data,
|
|
187
|
+
auth: wire.wireType,
|
|
188
|
+
}),
|
|
189
|
+
{
|
|
190
|
+
packageName: '@addon/stripe',
|
|
191
|
+
tags: ['function-tag'],
|
|
192
|
+
}
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
pikkuState('@addon/stripe', 'function', 'meta').createCharge = {
|
|
196
|
+
name: 'createCharge',
|
|
197
|
+
sessionless: true,
|
|
198
|
+
permissions: [],
|
|
199
|
+
tags: ['function-tag'],
|
|
200
|
+
} as never
|
|
201
|
+
|
|
202
|
+
const service = new ContextAwareRPCService(
|
|
203
|
+
createServices({
|
|
204
|
+
logger: createLogger(),
|
|
205
|
+
}),
|
|
206
|
+
{ wireType: 'http', wireId: 'wire-1' } as never,
|
|
207
|
+
{ requiresAuth: true }
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
const seenTags: string[][] = []
|
|
211
|
+
const addonTagGroup = pikkuState('@addon/stripe', 'permissions', 'tagGroup')
|
|
212
|
+
addonTagGroup['addon-tag'] = [
|
|
213
|
+
async (_services: any, _data: any, wire: any) => {
|
|
214
|
+
seenTags.push(['addon-tag', wire.wireType])
|
|
215
|
+
return true
|
|
216
|
+
},
|
|
217
|
+
] as never
|
|
218
|
+
addonTagGroup['function-tag'] = [
|
|
219
|
+
async (_services: any, _data: any, wire: any) => {
|
|
220
|
+
seenTags.push(['function-tag', wire.wireType])
|
|
221
|
+
return true
|
|
222
|
+
},
|
|
223
|
+
] as never
|
|
224
|
+
|
|
225
|
+
const result = await service.rpc('stripe:createCharge', { amount: 10 })
|
|
226
|
+
|
|
227
|
+
assert.deepEqual(result, {
|
|
228
|
+
data: { amount: 10 },
|
|
229
|
+
auth: 'http',
|
|
230
|
+
})
|
|
231
|
+
assert.deepEqual(seenTags, [['addon-tag', 'http']])
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
test('falls back to deploymentService when rpc meta is missing', async () => {
|
|
235
|
+
const calls: unknown[][] = []
|
|
236
|
+
const service = new ContextAwareRPCService(
|
|
237
|
+
createServices({
|
|
238
|
+
deploymentService: {
|
|
239
|
+
invoke: async (...args: unknown[]) => {
|
|
240
|
+
calls.push(args)
|
|
241
|
+
return { remote: true }
|
|
242
|
+
},
|
|
243
|
+
},
|
|
244
|
+
}),
|
|
245
|
+
{
|
|
246
|
+
traceId: 'trace-2',
|
|
247
|
+
getSession: async () => ({ userId: 'user-1' }),
|
|
248
|
+
} as never,
|
|
249
|
+
{}
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
const result = await service.rpc('missingRpc', { value: 1 })
|
|
253
|
+
|
|
254
|
+
assert.deepEqual(result, { remote: true })
|
|
255
|
+
assert.deepEqual(calls, [
|
|
256
|
+
['missingRpc', { value: 1 }, { userId: 'user-1' }, 'trace-2'],
|
|
257
|
+
])
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
test('throws RPCNotFoundError when rpc is missing and no deploymentService exists', async () => {
|
|
261
|
+
const service = new ContextAwareRPCService(
|
|
262
|
+
createServices(),
|
|
263
|
+
{} as never,
|
|
264
|
+
{}
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
await assert.rejects(() => service.rpc('missingRpc', {}), RPCNotFoundError)
|
|
268
|
+
})
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
describe('ContextAwareRPCService.rpcExposed', () => {
|
|
272
|
+
test('resolves unversioned exposed rpc names through latest rpc metadata', async () => {
|
|
273
|
+
pikkuState(null, 'rpc', 'meta').listCards = 'listCards@v2'
|
|
274
|
+
registerFunction(
|
|
275
|
+
'listCards@v2',
|
|
276
|
+
async (_services, data) => ({ data, version: 2 }),
|
|
277
|
+
{
|
|
278
|
+
expose: true,
|
|
279
|
+
}
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
const service = new ContextAwareRPCService(
|
|
283
|
+
createServices(),
|
|
284
|
+
{} as never,
|
|
285
|
+
{}
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
const result = await service.rpcExposed('listCards', { ok: true })
|
|
289
|
+
|
|
290
|
+
assert.deepEqual(result, {
|
|
291
|
+
data: { ok: true },
|
|
292
|
+
version: 2,
|
|
293
|
+
})
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
test('throws when the function does not exist', async () => {
|
|
297
|
+
const service = new ContextAwareRPCService(
|
|
298
|
+
createServices(),
|
|
299
|
+
{} as never,
|
|
300
|
+
{}
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
await assert.rejects(
|
|
304
|
+
() => service.rpcExposed('missingRpc', {}),
|
|
305
|
+
RPCNotFoundError
|
|
306
|
+
)
|
|
307
|
+
})
|
|
308
|
+
|
|
309
|
+
test('throws RPCNotFoundError when the function is not exposed', async () => {
|
|
310
|
+
pikkuState(null, 'rpc', 'meta').hiddenFunc = 'hiddenFunc'
|
|
311
|
+
registerFunction('hiddenFunc', async () => ({ ok: true }), {
|
|
312
|
+
expose: false,
|
|
313
|
+
})
|
|
314
|
+
|
|
315
|
+
const service = new ContextAwareRPCService(
|
|
316
|
+
createServices(),
|
|
317
|
+
{} as never,
|
|
318
|
+
{}
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
await assert.rejects(
|
|
322
|
+
() => service.rpcExposed('hiddenFunc', {}),
|
|
323
|
+
RPCNotFoundError
|
|
324
|
+
)
|
|
325
|
+
})
|
|
326
|
+
|
|
327
|
+
test('runs exposed addon functions', async () => {
|
|
328
|
+
pikkuState(null, 'addons', 'packages').set('addon', {
|
|
329
|
+
package: '@addon/pkg',
|
|
330
|
+
} as never)
|
|
331
|
+
registerFunction(
|
|
332
|
+
'visibleFunc',
|
|
333
|
+
async (_services, data) => ({ data, source: 'addon' }),
|
|
334
|
+
{
|
|
335
|
+
packageName: '@addon/pkg',
|
|
336
|
+
expose: true,
|
|
337
|
+
}
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
const service = new ContextAwareRPCService(
|
|
341
|
+
createServices(),
|
|
342
|
+
{} as never,
|
|
343
|
+
{}
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
const result = await service.rpcExposed('addon:visibleFunc', {
|
|
347
|
+
ok: true,
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
assert.deepEqual(result, { data: { ok: true }, source: 'addon' })
|
|
351
|
+
})
|
|
352
|
+
})
|
|
353
|
+
|
|
354
|
+
describe('ContextAwareRPCService.rpcWithWire', () => {
|
|
355
|
+
test('merges wire data for local rpc calls', async () => {
|
|
356
|
+
pikkuState(null, 'rpc', 'meta').echo = 'echoFunc'
|
|
357
|
+
registerFunction('echoFunc', async (_services, _data, wire) => ({
|
|
358
|
+
traceId: wire.traceId,
|
|
359
|
+
custom: wire.custom,
|
|
360
|
+
}))
|
|
361
|
+
|
|
362
|
+
const service = new ContextAwareRPCService(
|
|
363
|
+
createServices(),
|
|
364
|
+
{ traceId: 'base-trace' } as never,
|
|
365
|
+
{}
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
const result = await service.rpcWithWire('echo', {}, {
|
|
369
|
+
custom: 'merged',
|
|
370
|
+
} as never)
|
|
371
|
+
|
|
372
|
+
assert.deepEqual(result, {
|
|
373
|
+
traceId: 'base-trace',
|
|
374
|
+
custom: 'merged',
|
|
375
|
+
})
|
|
376
|
+
})
|
|
377
|
+
|
|
378
|
+
test('falls back to deploymentService for rpcWithWire when local rpc is missing', async () => {
|
|
379
|
+
const calls: unknown[][] = []
|
|
380
|
+
const service = new ContextAwareRPCService(
|
|
381
|
+
createServices({
|
|
382
|
+
deploymentService: {
|
|
383
|
+
invoke: async (...args: unknown[]) => {
|
|
384
|
+
calls.push(args)
|
|
385
|
+
return { remote: true }
|
|
386
|
+
},
|
|
387
|
+
},
|
|
388
|
+
}),
|
|
389
|
+
{ traceId: 'trace-3', session: { userId: 'user-2' } } as never,
|
|
390
|
+
{}
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
const result = await service.rpcWithWire('missingRpc', { value: 2 }, {
|
|
394
|
+
custom: 'wire',
|
|
395
|
+
} as never)
|
|
396
|
+
|
|
397
|
+
assert.deepEqual(result, { remote: true })
|
|
398
|
+
assert.deepEqual(calls, [
|
|
399
|
+
['missingRpc', { value: 2 }, { userId: 'user-2' }, 'trace-3'],
|
|
400
|
+
])
|
|
401
|
+
})
|
|
402
|
+
})
|
|
403
|
+
|
|
404
|
+
describe('ContextAwareRPCService.startWorkflow', () => {
|
|
405
|
+
test('throws when workflowService is unavailable', async () => {
|
|
406
|
+
const service = new ContextAwareRPCService(
|
|
407
|
+
createServices(),
|
|
408
|
+
{} as never,
|
|
409
|
+
{}
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
await assert.rejects(
|
|
413
|
+
() => service.startWorkflow('workflowA', {}),
|
|
414
|
+
/WorkflowService service not available/
|
|
415
|
+
)
|
|
416
|
+
})
|
|
417
|
+
|
|
418
|
+
test('passes derived wire metadata into workflowService.startWorkflow', async () => {
|
|
419
|
+
const calls: unknown[][] = []
|
|
420
|
+
const service = new ContextAwareRPCService(
|
|
421
|
+
createServices({
|
|
422
|
+
workflowService: {
|
|
423
|
+
startWorkflow: async (...args: unknown[]) => {
|
|
424
|
+
calls.push(args)
|
|
425
|
+
return { runId: 'run-1' }
|
|
426
|
+
},
|
|
427
|
+
},
|
|
428
|
+
}),
|
|
429
|
+
{
|
|
430
|
+
wireType: 'rpc',
|
|
431
|
+
wireId: 'rpc-1',
|
|
432
|
+
pikkuUserId: 'user-3',
|
|
433
|
+
workflowStep: { runId: 'parent-run-1' },
|
|
434
|
+
} as never,
|
|
435
|
+
{}
|
|
436
|
+
)
|
|
437
|
+
|
|
438
|
+
const result = await service.startWorkflow('workflowA', { ok: true })
|
|
439
|
+
|
|
440
|
+
assert.deepEqual(result, { runId: 'run-1' })
|
|
441
|
+
assert.deepEqual(calls[0]?.slice(0, 3), [
|
|
442
|
+
'workflowA',
|
|
443
|
+
{ ok: true },
|
|
444
|
+
{
|
|
445
|
+
type: 'rpc',
|
|
446
|
+
id: 'rpc-1',
|
|
447
|
+
parentRunId: 'parent-run-1',
|
|
448
|
+
pikkuUserId: 'user-3',
|
|
449
|
+
},
|
|
450
|
+
])
|
|
451
|
+
})
|
|
452
|
+
})
|
|
453
|
+
|
|
454
|
+
describe('ContextAwareRPCService.remote', () => {
|
|
455
|
+
test('throws when deploymentService is unavailable', async () => {
|
|
456
|
+
const service = new ContextAwareRPCService(
|
|
457
|
+
createServices(),
|
|
458
|
+
{} as never,
|
|
459
|
+
{}
|
|
460
|
+
)
|
|
461
|
+
|
|
462
|
+
await assert.rejects(
|
|
463
|
+
() => service.remote('remoteFunc', {}),
|
|
464
|
+
/No DeploymentService configured for remote RPC: remoteFunc/
|
|
465
|
+
)
|
|
466
|
+
})
|
|
467
|
+
|
|
468
|
+
test('uses deploymentService with session and trace id', async () => {
|
|
469
|
+
const calls: unknown[][] = []
|
|
470
|
+
const service = new ContextAwareRPCService(
|
|
471
|
+
createServices({
|
|
472
|
+
deploymentService: {
|
|
473
|
+
invoke: async (...args: unknown[]) => {
|
|
474
|
+
calls.push(args)
|
|
475
|
+
return { ok: true }
|
|
476
|
+
},
|
|
477
|
+
},
|
|
478
|
+
}),
|
|
479
|
+
{
|
|
480
|
+
traceId: 'trace-4',
|
|
481
|
+
getSession: async () => ({ userId: 'user-4' }),
|
|
482
|
+
} as never,
|
|
483
|
+
{}
|
|
484
|
+
)
|
|
485
|
+
|
|
486
|
+
const result = await service.remote('remoteFunc', { hi: true })
|
|
487
|
+
|
|
488
|
+
assert.deepEqual(result, { ok: true })
|
|
489
|
+
assert.deepEqual(calls, [
|
|
490
|
+
['remoteFunc', { hi: true }, { userId: 'user-4' }, 'trace-4'],
|
|
491
|
+
])
|
|
492
|
+
})
|
|
493
|
+
})
|
|
494
|
+
|
|
495
|
+
describe('rpcService.getContextRPCService', () => {
|
|
496
|
+
test('builds a typed rpc facade with depth and bound methods', async () => {
|
|
497
|
+
pikkuState(null, 'rpc', 'meta').echo = 'echoFunc'
|
|
498
|
+
registerFunction('echoFunc', async (_services, data) => data)
|
|
499
|
+
|
|
500
|
+
const rpc = rpcService.getContextRPCService(
|
|
501
|
+
createServices(),
|
|
502
|
+
{} as never,
|
|
503
|
+
false,
|
|
504
|
+
7
|
|
505
|
+
)
|
|
506
|
+
|
|
507
|
+
assert.equal(rpc.depth, 7)
|
|
508
|
+
assert.equal(rpc.global, false)
|
|
509
|
+
assert.deepEqual(await rpc.invoke('echo', { ok: true }), { ok: true })
|
|
510
|
+
})
|
|
511
|
+
})
|
|
@@ -3,7 +3,6 @@ import type { SessionService } from '../../services/user-session-service.js'
|
|
|
3
3
|
import type { CoreUserSession } from '../../types/core.types.js'
|
|
4
4
|
import { runPikkuFunc } from '../../function/function-runner.js'
|
|
5
5
|
import { pikkuState } from '../../pikku-state.js'
|
|
6
|
-
import { ForbiddenError } from '../../errors/errors.js'
|
|
7
6
|
import { PikkuError, addError } from '../../errors/error-handler.js'
|
|
8
7
|
import type { PikkuRPC, ResolvedFunction } from './rpc-types.js'
|
|
9
8
|
import { parseVersionedId } from '../../version.js'
|
|
@@ -54,7 +53,25 @@ export const resolveNamespace = (
|
|
|
54
53
|
}
|
|
55
54
|
}
|
|
56
55
|
|
|
57
|
-
|
|
56
|
+
/**
|
|
57
|
+
* Resolve a bare (non-namespaced) RPC name to its pikkuFuncId, preferring
|
|
58
|
+
* the caller's addon package when provided. Returns the function name plus
|
|
59
|
+
* the package scope that resolved it (null = root), so callers can thread
|
|
60
|
+
* the scope into runPikkuFunc without a second lookup.
|
|
61
|
+
*/
|
|
62
|
+
const resolvePikkuFunction = (
|
|
63
|
+
rpcName: string,
|
|
64
|
+
packageName: string | null = null
|
|
65
|
+
): { pikkuFuncId: string; packageName: string | null } => {
|
|
66
|
+
// Addon-scoped calls: try the caller's package function meta first.
|
|
67
|
+
// (RPC meta only lives in root; addon functions are registered under their package.)
|
|
68
|
+
if (packageName) {
|
|
69
|
+
const pkgFunctions = pikkuState(packageName, 'function', 'meta')
|
|
70
|
+
const pkgMeta = pkgFunctions?.[rpcName]
|
|
71
|
+
if (pkgMeta) {
|
|
72
|
+
return { pikkuFuncId: pkgMeta.pikkuFuncId || rpcName, packageName }
|
|
73
|
+
}
|
|
74
|
+
}
|
|
58
75
|
const rpc = pikkuState(null, 'rpc', 'meta')
|
|
59
76
|
let rpcMeta = rpc[rpcName]
|
|
60
77
|
if (!rpcMeta) {
|
|
@@ -63,10 +80,20 @@ const getPikkuFunctionName = (rpcName: string): string => {
|
|
|
63
80
|
rpcMeta = rpc[baseName]
|
|
64
81
|
}
|
|
65
82
|
}
|
|
83
|
+
if (!rpcMeta) {
|
|
84
|
+
const rootFunctions = pikkuState(null, 'function', 'meta')
|
|
85
|
+
const rootFunctionMeta = rootFunctions?.[rpcName]
|
|
86
|
+
if (rootFunctionMeta) {
|
|
87
|
+
return {
|
|
88
|
+
pikkuFuncId: rootFunctionMeta.pikkuFuncId || rpcName,
|
|
89
|
+
packageName: null,
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
66
93
|
if (!rpcMeta) {
|
|
67
94
|
throw new RPCNotFoundError(rpcName)
|
|
68
95
|
}
|
|
69
|
-
return rpcMeta
|
|
96
|
+
return { pikkuFuncId: rpcMeta, packageName: null }
|
|
70
97
|
}
|
|
71
98
|
|
|
72
99
|
// Context-aware RPC client for use within services
|
|
@@ -77,7 +104,8 @@ export class ContextAwareRPCService {
|
|
|
77
104
|
private options: {
|
|
78
105
|
requiresAuth?: boolean
|
|
79
106
|
sessionService?: SessionService<CoreUserSession>
|
|
80
|
-
}
|
|
107
|
+
},
|
|
108
|
+
private packageName: string | null = null
|
|
81
109
|
) {}
|
|
82
110
|
|
|
83
111
|
public async rpcExposed(funcName: string, data: any): Promise<any> {
|
|
@@ -90,13 +118,16 @@ export class ContextAwareRPCService {
|
|
|
90
118
|
]
|
|
91
119
|
}
|
|
92
120
|
} else {
|
|
93
|
-
|
|
121
|
+
const resolved = resolvePikkuFunction(funcName, this.packageName)
|
|
122
|
+
functionMeta = pikkuState(resolved.packageName, 'function', 'meta')[
|
|
123
|
+
resolved.pikkuFuncId
|
|
124
|
+
]
|
|
94
125
|
}
|
|
95
126
|
if (!functionMeta) {
|
|
96
127
|
throw new RPCNotFoundError(funcName)
|
|
97
128
|
}
|
|
98
129
|
if (!functionMeta.expose) {
|
|
99
|
-
throw new
|
|
130
|
+
throw new RPCNotFoundError(funcName)
|
|
100
131
|
}
|
|
101
132
|
return await this.rpc(funcName, data)
|
|
102
133
|
}
|
|
@@ -131,17 +162,22 @@ export class ContextAwareRPCService {
|
|
|
131
162
|
}
|
|
132
163
|
}
|
|
133
164
|
|
|
134
|
-
//
|
|
165
|
+
// Bare name: resolve via caller's package scope first (if any), then root.
|
|
166
|
+
// Note: intra-addon bare calls do NOT re-apply the addon's external
|
|
167
|
+
// addonConfig.auth/tags — those gates are only applied on the external
|
|
168
|
+
// 'namespace:func' boundary via invokeAddonFunction.
|
|
135
169
|
try {
|
|
170
|
+
const resolved = resolvePikkuFunction(funcName, this.packageName)
|
|
136
171
|
return await runPikkuFunc<In, Out>(
|
|
137
172
|
'rpc',
|
|
138
173
|
funcName,
|
|
139
|
-
|
|
174
|
+
resolved.pikkuFuncId,
|
|
140
175
|
{
|
|
141
176
|
auth: this.options.requiresAuth,
|
|
142
177
|
singletonServices: this.services,
|
|
143
178
|
data: () => data,
|
|
144
179
|
wire: updatedWire,
|
|
180
|
+
packageName: resolved.packageName,
|
|
145
181
|
}
|
|
146
182
|
)
|
|
147
183
|
} catch (e) {
|
|
@@ -232,17 +268,14 @@ export class ContextAwareRPCService {
|
|
|
232
268
|
}
|
|
233
269
|
|
|
234
270
|
try {
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
wire: mergedWire,
|
|
244
|
-
}
|
|
245
|
-
)
|
|
271
|
+
const resolved = resolvePikkuFunction(rpcName, this.packageName)
|
|
272
|
+
return await runPikkuFunc<In, Out>('rpc', rpcName, resolved.pikkuFuncId, {
|
|
273
|
+
auth: this.options.requiresAuth,
|
|
274
|
+
singletonServices: this.services,
|
|
275
|
+
data: () => data,
|
|
276
|
+
wire: mergedWire,
|
|
277
|
+
packageName: resolved.packageName,
|
|
278
|
+
})
|
|
246
279
|
} catch (e) {
|
|
247
280
|
if (e instanceof RPCNotFoundError && this.services.deploymentService) {
|
|
248
281
|
const session =
|
|
@@ -406,14 +439,20 @@ export class PikkuRPCService<
|
|
|
406
439
|
sessionService?: SessionService<CoreUserSession>
|
|
407
440
|
}
|
|
408
441
|
| undefined,
|
|
409
|
-
depth: number = 0
|
|
442
|
+
depth: number = 0,
|
|
443
|
+
packageName: string | null = null
|
|
410
444
|
): TypedRPC {
|
|
411
445
|
const options =
|
|
412
446
|
typeof requiresAuthOrOptions === 'object' &&
|
|
413
447
|
requiresAuthOrOptions !== null
|
|
414
448
|
? requiresAuthOrOptions
|
|
415
449
|
: { requiresAuth: requiresAuthOrOptions }
|
|
416
|
-
const serviceRPC = new ContextAwareRPCService(
|
|
450
|
+
const serviceRPC = new ContextAwareRPCService(
|
|
451
|
+
services,
|
|
452
|
+
wire,
|
|
453
|
+
options,
|
|
454
|
+
packageName
|
|
455
|
+
)
|
|
417
456
|
return {
|
|
418
457
|
depth,
|
|
419
458
|
global: false,
|