@pikku/core 0.12.64 → 0.12.67
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 +311 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/permissions.d.ts +12 -4
- package/dist/permissions.js +11 -32
- package/dist/scopes.d.ts +14 -0
- package/dist/scopes.js +39 -8
- package/dist/testing/service-tests.js +37 -0
- package/dist/types/core.types.d.ts +22 -0
- package/dist/wirings/ai-agent/ai-agent-prepare.d.ts +64 -0
- package/dist/wirings/ai-agent/ai-agent-prepare.js +103 -5
- package/dist/wirings/ai-agent/ai-agent-runner.js +5 -1
- package/dist/wirings/ai-agent/ai-agent-stream.js +28 -7
- package/dist/wirings/ai-agent/ai-agent.types.d.ts +29 -1
- package/dist/wirings/ai-agent/index.d.ts +1 -1
- package/dist/wirings/ai-agent/index.js +1 -1
- package/dist/wirings/ai-agent/voice-input.js +3 -3
- package/dist/wirings/cli/cli-runner.js +3 -0
- package/dist/wirings/cli/command-parser.d.ts +2 -0
- package/dist/wirings/cli/command-parser.js +59 -2
- package/dist/wirings/credential/credential.types.d.ts +14 -0
- package/dist/wirings/credential/validate-credential-definitions.js +1 -0
- package/dist/wirings/gateway/gateway-runner.js +100 -50
- package/dist/wirings/gateway/gateway.types.d.ts +8 -5
- package/dist/wirings/http/http.types.d.ts +3 -3
- package/dist/wirings/scope/validate-scope-definitions.d.ts +8 -0
- package/dist/wirings/scope/validate-scope-definitions.js +16 -1
- package/dist/wirings/secret/secret.types.d.ts +14 -0
- package/dist/wirings/secret/validate-secret-definitions.js +2 -0
- package/dist/wirings/variable/validate-variable-definitions.js +2 -0
- package/dist/wirings/variable/variable.types.d.ts +14 -0
- package/dist/wirings/workflow/dsl/workflow-dsl.types.d.ts +36 -6
- package/dist/wirings/workflow/pikku-workflow-service.d.ts +8 -0
- package/dist/wirings/workflow/pikku-workflow-service.js +16 -0
- package/dist/wirings/workflow/workflow.types.d.ts +0 -2
- package/package.json +2 -1
- package/src/index.ts +2 -1
- package/src/permissions.test.ts +14 -8
- package/src/permissions.ts +14 -36
- package/src/scopes.test.ts +37 -1
- package/src/scopes.ts +48 -9
- package/src/testing/service-tests.ts +49 -0
- package/src/types/core.types.ts +23 -0
- package/src/wirings/ai-agent/ai-agent-authorization.test.ts +204 -0
- package/src/wirings/ai-agent/ai-agent-prepare.test.ts +175 -0
- package/src/wirings/ai-agent/ai-agent-prepare.ts +132 -5
- package/src/wirings/ai-agent/ai-agent-resume-authorization.test.ts +207 -0
- package/src/wirings/ai-agent/ai-agent-runner.ts +7 -0
- package/src/wirings/ai-agent/ai-agent-stream.test.ts +103 -0
- package/src/wirings/ai-agent/ai-agent-stream.ts +38 -6
- package/src/wirings/ai-agent/ai-agent.types.ts +29 -0
- package/src/wirings/ai-agent/index.ts +4 -0
- package/src/wirings/ai-agent/voice-input.test.ts +90 -0
- package/src/wirings/ai-agent/voice-input.ts +8 -10
- package/src/wirings/cli/cli-runner.ts +4 -0
- package/src/wirings/cli/command-parser.test.ts +130 -0
- package/src/wirings/cli/command-parser.ts +80 -2
- package/src/wirings/credential/credential.types.ts +14 -0
- package/src/wirings/credential/validate-credential-definitions.ts +1 -0
- package/src/wirings/gateway/gateway-authorization.test.ts +444 -0
- package/src/wirings/gateway/gateway-runner.ts +114 -68
- package/src/wirings/gateway/gateway.types.ts +7 -9
- package/src/wirings/http/http.types.ts +6 -4
- package/src/wirings/scope/scope.test.ts +25 -0
- package/src/wirings/scope/validate-scope-definitions.ts +16 -1
- package/src/wirings/secret/secret.types.ts +14 -0
- package/src/wirings/secret/validate-secret-definitions.ts +2 -0
- package/src/wirings/variable/validate-variable-definitions.ts +2 -0
- package/src/wirings/variable/variable.types.ts +14 -0
- package/src/wirings/workflow/dsl/workflow-dsl.types.ts +36 -6
- package/src/wirings/workflow/pikku-workflow-service.ts +36 -0
- package/src/wirings/workflow/workflow-on-error.test.ts +154 -0
- package/src/wirings/workflow/workflow.types.ts +0 -2
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { pikkuState } from '../../pikku-state.js';
|
|
2
2
|
import { NotFoundError, UnauthorizedError } from '../../errors/errors.js';
|
|
3
|
-
import { addFunction } from '../../function/function-runner.js';
|
|
3
|
+
import { addFunction, runPikkuFunc } from '../../function/function-runner.js';
|
|
4
4
|
import { runMiddleware } from '../../middleware-runner.js';
|
|
5
5
|
import { httpRouter } from '../http/routers/http-router.js';
|
|
6
6
|
/**
|
|
@@ -9,6 +9,52 @@ import { httpRouter } from '../http/routers/http-router.js';
|
|
|
9
9
|
* requests share one construction).
|
|
10
10
|
*/
|
|
11
11
|
const resolvedAdapters = new WeakMap();
|
|
12
|
+
/**
|
|
13
|
+
* The generated function id a gateway's handler is registered under.
|
|
14
|
+
*/
|
|
15
|
+
const gatewayHandlerFuncId = (name) => `gateway__${name}__handler`;
|
|
16
|
+
/**
|
|
17
|
+
* Bridges a session established by gateway middleware onto the wire so the
|
|
18
|
+
* handler's gate can see it.
|
|
19
|
+
*
|
|
20
|
+
* Gateway middleware is the only place a webhook can acquire a session (e.g.
|
|
21
|
+
* mapping a verified platform sender to a user). Middleware that assigns
|
|
22
|
+
* `wire.session` needs nothing, but middleware using the idiomatic
|
|
23
|
+
* `wire.setSession()` writes to the enclosing wiring's session service, which
|
|
24
|
+
* the handler's own invocation does not read — without this the session would
|
|
25
|
+
* be silently invisible to `auth` and `scopes`.
|
|
26
|
+
*/
|
|
27
|
+
const bridgeMiddlewareSession = async (wire) => {
|
|
28
|
+
if (wire.session || !wire.getSession)
|
|
29
|
+
return;
|
|
30
|
+
const session = await wire.getSession();
|
|
31
|
+
if (session) {
|
|
32
|
+
wire.session = session;
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* Registers a gateway's handler as a real pikku function so that invoking it
|
|
37
|
+
* goes through the function runner's gate. Without this the handler is called
|
|
38
|
+
* directly and its own `auth`, `scopes` and `permissions` are never evaluated.
|
|
39
|
+
*
|
|
40
|
+
* The handler is registered as sessionless: a gateway's inbound traffic is
|
|
41
|
+
* platform-authenticated (adapter signature verification), not session-bearing,
|
|
42
|
+
* so requiring a session by default would break every webhook. A handler that
|
|
43
|
+
* does need one declares `auth: true`, exactly like `pikkuSessionlessFunc`.
|
|
44
|
+
* `scopes` and `permissions` are always enforced when declared.
|
|
45
|
+
*/
|
|
46
|
+
const registerGatewayHandler = (config) => {
|
|
47
|
+
const funcId = gatewayHandlerFuncId(config.name);
|
|
48
|
+
const funcMeta = pikkuState(null, 'function', 'meta');
|
|
49
|
+
funcMeta[funcId] = {
|
|
50
|
+
pikkuFuncId: funcId,
|
|
51
|
+
inputSchemaName: null,
|
|
52
|
+
outputSchemaName: null,
|
|
53
|
+
sessionless: true,
|
|
54
|
+
};
|
|
55
|
+
addFunction(funcId, config.func);
|
|
56
|
+
return funcId;
|
|
57
|
+
};
|
|
12
58
|
export const resolveGatewayAdapter = (config, services) => {
|
|
13
59
|
let resolved = resolvedAdapters.get(config);
|
|
14
60
|
if (!resolved) {
|
|
@@ -107,12 +153,13 @@ const wireWebhookGateway = (config) => {
|
|
|
107
153
|
* 2. Parse body via adapter → GatewayInboundMessage (or null to ignore)
|
|
108
154
|
* 3. Populate `wire.gateway`
|
|
109
155
|
* 4. Run user middleware (which can read `wire.gateway` for auth)
|
|
110
|
-
* 5.
|
|
156
|
+
* 5. Invoke the handler through the function runner, which enforces its
|
|
157
|
+
* `auth`/`scopes`/`permissions` before running it
|
|
111
158
|
* 6. Auto-send response via adapter if func returns outbound content
|
|
112
159
|
*/
|
|
113
160
|
const createWebhookPostHandler = (config) => {
|
|
114
|
-
const { name,
|
|
115
|
-
const
|
|
161
|
+
const { name, middleware: userMiddleware } = config;
|
|
162
|
+
const handlerFuncId = registerGatewayHandler(config);
|
|
116
163
|
return async (services, data, wire) => {
|
|
117
164
|
const adapter = await resolveGatewayAdapter(config, services);
|
|
118
165
|
// Check for POST-based webhook verification (e.g. Slack url_verification)
|
|
@@ -136,23 +183,28 @@ const createWebhookPostHandler = (config) => {
|
|
|
136
183
|
send: (msg) => adapter.send(parsed.senderId, msg),
|
|
137
184
|
};
|
|
138
185
|
wire.gateway = gateway;
|
|
139
|
-
//
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
186
|
+
// Gateway middleware runs first and outside the gate, so it can establish
|
|
187
|
+
// the session the gate then checks. The handler is invoked through the
|
|
188
|
+
// function runner, which enforces its auth, scopes and permissions and
|
|
189
|
+
// applies the handler's own middleware.
|
|
190
|
+
const invoke = async () => {
|
|
191
|
+
await bridgeMiddlewareSession(wire);
|
|
192
|
+
return await runPikkuFunc('gateway', name, handlerFuncId, {
|
|
193
|
+
singletonServices: services,
|
|
194
|
+
data: () => parsed,
|
|
195
|
+
auth: config.auth,
|
|
196
|
+
wire: wire,
|
|
197
|
+
});
|
|
151
198
|
};
|
|
152
|
-
|
|
153
|
-
|
|
199
|
+
const gatewayMiddleware = userMiddleware;
|
|
200
|
+
const result = gatewayMiddleware?.length
|
|
201
|
+
? await runMiddleware(services, wire, gatewayMiddleware, invoke)
|
|
202
|
+
: await invoke();
|
|
203
|
+
// Auto-send response if the func returns outbound content
|
|
204
|
+
if (result && (result.text || result.richContent || result.attachments)) {
|
|
205
|
+
await adapter.send(parsed.senderId, result);
|
|
154
206
|
}
|
|
155
|
-
return
|
|
207
|
+
return { ok: true };
|
|
156
208
|
};
|
|
157
209
|
};
|
|
158
210
|
/**
|
|
@@ -215,8 +267,8 @@ const wireWebsocketGateway = (config) => {
|
|
|
215
267
|
connect: { pikkuFuncId: connectFuncId },
|
|
216
268
|
message: { pikkuFuncId: messageFuncId },
|
|
217
269
|
};
|
|
218
|
-
const userFuncConfig = config.func;
|
|
219
270
|
const userMiddleware = config.middleware;
|
|
271
|
+
const handlerFuncId = registerGatewayHandler(config);
|
|
220
272
|
// Register onConnect
|
|
221
273
|
addFunction(connectFuncId, {
|
|
222
274
|
auth: false,
|
|
@@ -249,22 +301,21 @@ const wireWebsocketGateway = (config) => {
|
|
|
249
301
|
},
|
|
250
302
|
};
|
|
251
303
|
wire.gateway = gateway;
|
|
252
|
-
const
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
wire.channel?.send(result);
|
|
261
|
-
}
|
|
304
|
+
const invoke = async () => {
|
|
305
|
+
await bridgeMiddlewareSession(wire);
|
|
306
|
+
return await runPikkuFunc('gateway', name, handlerFuncId, {
|
|
307
|
+
singletonServices: services,
|
|
308
|
+
data: () => parsed,
|
|
309
|
+
auth: config.auth,
|
|
310
|
+
wire: wire,
|
|
311
|
+
});
|
|
262
312
|
};
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
313
|
+
const gatewayMiddleware = userMiddleware;
|
|
314
|
+
const result = gatewayMiddleware?.length
|
|
315
|
+
? await runMiddleware(services, wire, gatewayMiddleware, invoke)
|
|
316
|
+
: await invoke();
|
|
317
|
+
if (result && (result.text || result.richContent || result.attachments)) {
|
|
318
|
+
wire.channel?.send(result);
|
|
268
319
|
}
|
|
269
320
|
},
|
|
270
321
|
});
|
|
@@ -300,8 +351,8 @@ const wireListenerGateway = (config) => {
|
|
|
300
351
|
* @param singletonServices - Singleton services to pass to handler/middleware
|
|
301
352
|
*/
|
|
302
353
|
export const createListenerMessageHandler = (name, config, singletonServices) => {
|
|
303
|
-
const userFuncConfig = config.func;
|
|
304
354
|
const userMiddleware = config.middleware;
|
|
355
|
+
const handlerFuncId = registerGatewayHandler(config);
|
|
305
356
|
return async (rawData) => {
|
|
306
357
|
const adapter = await resolveGatewayAdapter(config, singletonServices);
|
|
307
358
|
const parsed = adapter.parse(rawData);
|
|
@@ -315,21 +366,20 @@ export const createListenerMessageHandler = (name, config, singletonServices) =>
|
|
|
315
366
|
send: (msg) => adapter.send(parsed.senderId, msg),
|
|
316
367
|
};
|
|
317
368
|
wire.gateway = gateway;
|
|
318
|
-
const
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
}
|
|
369
|
+
const invoke = async () => {
|
|
370
|
+
await bridgeMiddlewareSession(wire);
|
|
371
|
+
return await runPikkuFunc('gateway', name, handlerFuncId, {
|
|
372
|
+
singletonServices,
|
|
373
|
+
data: () => parsed,
|
|
374
|
+
auth: config.auth,
|
|
375
|
+
wire,
|
|
376
|
+
});
|
|
327
377
|
};
|
|
328
|
-
|
|
329
|
-
await runMiddleware(singletonServices, wire,
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
await
|
|
378
|
+
const result = userMiddleware?.length
|
|
379
|
+
? await runMiddleware(singletonServices, wire, userMiddleware, invoke)
|
|
380
|
+
: await invoke();
|
|
381
|
+
if (result && (result.text || result.richContent || result.attachments)) {
|
|
382
|
+
await adapter.send(parsed.senderId, result);
|
|
333
383
|
}
|
|
334
384
|
};
|
|
335
385
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { CommonWireMeta, CorePikkuMiddleware, CorePikkuMiddlewareGroup, CoreSingletonServices } from '../../types/core.types.js';
|
|
2
|
-
import type { CorePikkuFunctionConfig
|
|
2
|
+
import type { CorePikkuFunctionConfig } from '../../function/functions.types.js';
|
|
3
3
|
import type { PikkuHTTPRequest } from '../http/http.types.js';
|
|
4
4
|
/**
|
|
5
5
|
* Attachment in gateway messages (images, files, etc.)
|
|
@@ -102,7 +102,7 @@ export type GatewayTransportType = 'webhook' | 'websocket' | 'listener';
|
|
|
102
102
|
/**
|
|
103
103
|
* Core gateway configuration for wireGateway()
|
|
104
104
|
*/
|
|
105
|
-
export type CoreGateway<PikkuFunctionConfig = CorePikkuFunctionConfig<any, any>,
|
|
105
|
+
export type CoreGateway<PikkuFunctionConfig = CorePikkuFunctionConfig<any, any>, PikkuMiddleware extends CorePikkuMiddleware = CorePikkuMiddleware> = Partial<Pick<CommonWireMeta, 'title' | 'summary' | 'description' | 'errors'>> & {
|
|
106
106
|
/** Unique name for this gateway */
|
|
107
107
|
name: string;
|
|
108
108
|
/** Transport type */
|
|
@@ -117,11 +117,14 @@ export type CoreGateway<PikkuFunctionConfig = CorePikkuFunctionConfig<any, any>,
|
|
|
117
117
|
func: PikkuFunctionConfig;
|
|
118
118
|
/** Optional middleware chain (e.g., auth) */
|
|
119
119
|
middleware?: CorePikkuMiddlewareGroup<any, any>;
|
|
120
|
-
/** Optional permissions */
|
|
121
|
-
permissions?: CorePermissionGroup | PikkuPermission[];
|
|
122
120
|
/** Optional tags for categorization */
|
|
123
121
|
tags?: string[];
|
|
124
|
-
/**
|
|
122
|
+
/**
|
|
123
|
+
* Whether the handler requires a session. Left unset, the handler's own
|
|
124
|
+
* `auth` governs, and a gateway handler is sessionless by default — inbound
|
|
125
|
+
* gateway traffic is platform-authenticated by the adapter, not
|
|
126
|
+
* session-bearing. Set `true` to require a session for every message.
|
|
127
|
+
*/
|
|
125
128
|
auth?: boolean;
|
|
126
129
|
};
|
|
127
130
|
/**
|
|
@@ -71,7 +71,7 @@ export type PikkuQuery<T = Record<string, string | undefined>> = Record<string,
|
|
|
71
71
|
* @template PikkuFunctionSessionless - The sessionless API function type, defaults to `CorePikkuFunctionSessionless`.
|
|
72
72
|
* @template PikkuPermission - The permission function type, defaults to `CorePikkuPermission`.
|
|
73
73
|
*/
|
|
74
|
-
export type CoreHTTPFunctionWiring<In, Out, R extends string, PikkuFunction extends CorePikkuFunction<In, Out, any, any, any> = CorePikkuFunction<In, Out>, PikkuFunctionSessionless extends CorePikkuFunctionSessionless<In, Out, any, any, any> = CorePikkuFunctionSessionless<In, Out>, PikkuPermission extends CorePikkuPermission<In, any, any> = CorePikkuPermission<In, any, any>, PikkuMiddleware extends CorePikkuMiddleware<any, any> = CorePikkuMiddleware<any>> = (CoreHTTPFunction & {
|
|
74
|
+
export type CoreHTTPFunctionWiring<In, Out, R extends string, PikkuFunction extends CorePikkuFunction<In, Out, any, any, any> = CorePikkuFunction<In, Out>, PikkuFunctionSessionless extends CorePikkuFunctionSessionless<In, Out, any, any, any> = CorePikkuFunctionSessionless<In, Out>, PikkuPermission extends CorePikkuPermission<In, any, any> = CorePikkuPermission<In, any, any>, PikkuMiddleware extends CorePikkuMiddleware<any, any> = CorePikkuMiddleware<any, any>> = (CoreHTTPFunction & {
|
|
75
75
|
route: R;
|
|
76
76
|
method: HTTPMethod;
|
|
77
77
|
func: CorePikkuFunctionConfig<PikkuFunction, PikkuPermission, PikkuMiddleware>;
|
|
@@ -177,7 +177,7 @@ export interface PikkuHTTPResponse<Out = unknown> {
|
|
|
177
177
|
/**
|
|
178
178
|
* Single route configuration - supports all wireHTTP options
|
|
179
179
|
*/
|
|
180
|
-
export type HTTPRouteConfig<PikkuFunction extends CorePikkuFunction<any, any, any, any, any> | CorePikkuFunctionSessionless<any, any, any, any, any> = CorePikkuFunction<any, any, any, any, any> | CorePikkuFunctionSessionless<any, any, any, any, any>, PikkuPermission extends CorePikkuPermission<any, any, any> = CorePikkuPermission<any>, PikkuMiddleware extends CorePikkuMiddleware<any, any> = CorePikkuMiddleware<any, any>> = HTTPRouteBaseConfig & {
|
|
180
|
+
export type HTTPRouteConfig<PikkuFunction extends CorePikkuFunction<any, any, any, any, any> | CorePikkuFunctionSessionless<any, any, any, any, any> = CorePikkuFunction<any, any, any, any, any> | CorePikkuFunctionSessionless<any, any, any, any, any>, PikkuPermission extends CorePikkuPermission<any, any, any> = CorePikkuPermission<any, any, any>, PikkuMiddleware extends CorePikkuMiddleware<any, any> = CorePikkuMiddleware<any, any>> = HTTPRouteBaseConfig & {
|
|
181
181
|
method: HTTPMethod;
|
|
182
182
|
route: string;
|
|
183
183
|
func: CorePikkuFunctionConfig<PikkuFunction, PikkuPermission, PikkuMiddleware>;
|
|
@@ -188,7 +188,7 @@ export type HTTPRouteConfig<PikkuFunction extends CorePikkuFunction<any, any, an
|
|
|
188
188
|
/**
|
|
189
189
|
* Group-level configuration applied to all routes
|
|
190
190
|
*/
|
|
191
|
-
export type HTTPRoutesGroupConfig<PikkuPermission extends CorePikkuPermission<any, any, any> = CorePikkuPermission<any>, PikkuMiddleware extends CorePikkuMiddleware<any, any> = CorePikkuMiddleware<any, any>> = {
|
|
191
|
+
export type HTTPRoutesGroupConfig<PikkuPermission extends CorePikkuPermission<any, any, any> = CorePikkuPermission<any, any, any>, PikkuMiddleware extends CorePikkuMiddleware<any, any> = CorePikkuMiddleware<any, any>> = {
|
|
192
192
|
basePath?: string;
|
|
193
193
|
tags?: string[];
|
|
194
194
|
auth?: boolean;
|
|
@@ -3,6 +3,14 @@ import type { FlatScope, ScopeDefinitions, ScopeDefinitionsMeta } from './scope.
|
|
|
3
3
|
* Flattens declared scope trees into the full list of grantable scope ids,
|
|
4
4
|
* depth-first. Every node is emitted, including intermediate ones.
|
|
5
5
|
*
|
|
6
|
+
* Ids are unique. A root may legitimately be declared more than once — an addon
|
|
7
|
+
* and its host app both contributing the same `admin` tree, say — and
|
|
8
|
+
* {@link validateAndBuildScopeDefinitionsMeta} already guarantees those
|
|
9
|
+
* declarations are identical, so the second one is redundant rather than
|
|
10
|
+
* conflicting. Collapsing it here keeps every consumer honest: codegen emits an
|
|
11
|
+
* object literal keyed by id (duplicates are a TypeScript error), and a
|
|
12
|
+
* ScopeService syncs one row per scope instead of re-writing the same one.
|
|
13
|
+
*
|
|
6
14
|
* Used by codegen to build the `ScopeId` union, and by a ScopeService to sync
|
|
7
15
|
* the declared set into its store.
|
|
8
16
|
*/
|
|
@@ -30,6 +30,14 @@ const flattenNodes = (nodes, prefix, out) => {
|
|
|
30
30
|
* Flattens declared scope trees into the full list of grantable scope ids,
|
|
31
31
|
* depth-first. Every node is emitted, including intermediate ones.
|
|
32
32
|
*
|
|
33
|
+
* Ids are unique. A root may legitimately be declared more than once — an addon
|
|
34
|
+
* and its host app both contributing the same `admin` tree, say — and
|
|
35
|
+
* {@link validateAndBuildScopeDefinitionsMeta} already guarantees those
|
|
36
|
+
* declarations are identical, so the second one is redundant rather than
|
|
37
|
+
* conflicting. Collapsing it here keeps every consumer honest: codegen emits an
|
|
38
|
+
* object literal keyed by id (duplicates are a TypeScript error), and a
|
|
39
|
+
* ScopeService syncs one row per scope instead of re-writing the same one.
|
|
40
|
+
*
|
|
33
41
|
* Used by codegen to build the `ScopeId` union, and by a ScopeService to sync
|
|
34
42
|
* the declared set into its store.
|
|
35
43
|
*/
|
|
@@ -39,7 +47,14 @@ export const flattenScopeDefinitions = (definitions) => {
|
|
|
39
47
|
out.push({ id: def.name, description: def.description });
|
|
40
48
|
flattenNodes(def.scopes, def.name, out);
|
|
41
49
|
}
|
|
42
|
-
|
|
50
|
+
const seen = new Set();
|
|
51
|
+
return out.filter((scope) => {
|
|
52
|
+
if (seen.has(scope.id)) {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
seen.add(scope.id);
|
|
56
|
+
return true;
|
|
57
|
+
});
|
|
43
58
|
};
|
|
44
59
|
/**
|
|
45
60
|
* Validates declared scopes and keys them by name.
|
|
@@ -4,6 +4,13 @@ export type CoreSecret<T = unknown> = {
|
|
|
4
4
|
description?: string;
|
|
5
5
|
secretId: string;
|
|
6
6
|
schema: T;
|
|
7
|
+
/**
|
|
8
|
+
* Link to documentation explaining how to obtain this value — a provider's
|
|
9
|
+
* API-key page, a setup guide, an internal runbook. Surfaced by consoles and
|
|
10
|
+
* deploy UIs so a user facing a missing value has somewhere to go instead of
|
|
11
|
+
* an opaque identifier.
|
|
12
|
+
*/
|
|
13
|
+
docsUrl?: string;
|
|
7
14
|
/**
|
|
8
15
|
* Optional rotation cadence for this secret, e.g. '1d', '30day', '1w'.
|
|
9
16
|
* Stored in the generated secrets metadata so consumers can tell when a
|
|
@@ -25,6 +32,13 @@ export type SecretDefinitionMeta = {
|
|
|
25
32
|
description?: string;
|
|
26
33
|
secretId: string;
|
|
27
34
|
schema?: Record<string, unknown> | string;
|
|
35
|
+
/**
|
|
36
|
+
* Link to documentation explaining how to obtain this value — a provider's
|
|
37
|
+
* API-key page, a setup guide, an internal runbook. Surfaced by consoles and
|
|
38
|
+
* deploy UIs so a user facing a missing value has somewhere to go instead of
|
|
39
|
+
* an opaque identifier.
|
|
40
|
+
*/
|
|
41
|
+
docsUrl?: string;
|
|
28
42
|
oauth2?: OAuth2CredentialConfig;
|
|
29
43
|
rotationPeriod?: string;
|
|
30
44
|
sourceFile?: string;
|
|
@@ -34,6 +34,7 @@ export function validateAndBuildSecretDefinitionsMeta(definitions, schemaLookup)
|
|
|
34
34
|
schema: def.schema,
|
|
35
35
|
oauth2: def.oauth2,
|
|
36
36
|
rotationPeriod: def.rotationPeriod,
|
|
37
|
+
docsUrl: def.docsUrl,
|
|
37
38
|
sourceFile: def.sourceFile,
|
|
38
39
|
};
|
|
39
40
|
}
|
|
@@ -49,6 +50,7 @@ export function validateAndBuildSecretDefinitionsMeta(definitions, schemaLookup)
|
|
|
49
50
|
schema: def.schema,
|
|
50
51
|
oauth2: def.oauth2,
|
|
51
52
|
rotationPeriod: def.rotationPeriod,
|
|
53
|
+
docsUrl: def.docsUrl,
|
|
52
54
|
sourceFile: def.sourceFile,
|
|
53
55
|
};
|
|
54
56
|
}
|
|
@@ -24,6 +24,7 @@ export function validateAndBuildVariableDefinitionsMeta(definitions, schemaLooku
|
|
|
24
24
|
description: def.description,
|
|
25
25
|
variableId: def.variableId,
|
|
26
26
|
schema: def.schema,
|
|
27
|
+
docsUrl: def.docsUrl,
|
|
27
28
|
sourceFile: def.sourceFile,
|
|
28
29
|
};
|
|
29
30
|
}
|
|
@@ -37,6 +38,7 @@ export function validateAndBuildVariableDefinitionsMeta(definitions, schemaLooku
|
|
|
37
38
|
description: def.description,
|
|
38
39
|
variableId: def.variableId,
|
|
39
40
|
schema: def.schema,
|
|
41
|
+
docsUrl: def.docsUrl,
|
|
40
42
|
sourceFile: def.sourceFile,
|
|
41
43
|
};
|
|
42
44
|
}
|
|
@@ -4,6 +4,13 @@ export type CoreVariable<T = unknown> = {
|
|
|
4
4
|
description?: string;
|
|
5
5
|
variableId: string;
|
|
6
6
|
schema: T;
|
|
7
|
+
/**
|
|
8
|
+
* Link to documentation explaining how to obtain this value — a provider's
|
|
9
|
+
* API-key page, a setup guide, an internal runbook. Surfaced by consoles and
|
|
10
|
+
* deploy UIs so a user facing a missing value has somewhere to go instead of
|
|
11
|
+
* an opaque identifier.
|
|
12
|
+
*/
|
|
13
|
+
docsUrl?: string;
|
|
7
14
|
};
|
|
8
15
|
export type VariableDefinitionMeta = {
|
|
9
16
|
name: string;
|
|
@@ -11,6 +18,13 @@ export type VariableDefinitionMeta = {
|
|
|
11
18
|
description?: string;
|
|
12
19
|
variableId: string;
|
|
13
20
|
schema?: Record<string, unknown> | string;
|
|
21
|
+
/**
|
|
22
|
+
* Link to documentation explaining how to obtain this value — a provider's
|
|
23
|
+
* API-key page, a setup guide, an internal runbook. Surfaced by consoles and
|
|
24
|
+
* deploy UIs so a user facing a missing value has somewhere to go instead of
|
|
25
|
+
* an opaque identifier.
|
|
26
|
+
*/
|
|
27
|
+
docsUrl?: string;
|
|
14
28
|
sourceFile?: string;
|
|
15
29
|
};
|
|
16
30
|
export type VariableDefinitionsMeta = Record<string, VariableDefinitionMeta>;
|
|
@@ -15,6 +15,13 @@ export interface WorkflowStepOptions {
|
|
|
15
15
|
retries?: number;
|
|
16
16
|
/** Delay between retry attempts (e.g., '1s', '2s', '2min') */
|
|
17
17
|
retryDelay?: string | number;
|
|
18
|
+
/**
|
|
19
|
+
* RPC to invoke for compensation when this step fails after exhausting its
|
|
20
|
+
* retries. Mirrors a graph node's `onError`: the handler receives
|
|
21
|
+
* `{ error: { message } }` and the original error is still thrown, so the
|
|
22
|
+
* workflow fails — this is compensation, not recovery.
|
|
23
|
+
*/
|
|
24
|
+
onError?: string;
|
|
18
25
|
/**
|
|
19
26
|
* Run this step as an actor (scenarios). The RPC is sent through the
|
|
20
27
|
* actor's authenticated client over the REAL transport — never dispatched
|
|
@@ -214,16 +221,20 @@ export interface ParallelGroupStepMeta {
|
|
|
214
221
|
*/
|
|
215
222
|
export interface FanoutStepMeta {
|
|
216
223
|
type: 'fanout';
|
|
217
|
-
/**
|
|
218
|
-
|
|
224
|
+
/**
|
|
225
|
+
* Step name for this fanout. Optional: a fanout is not itself a cached step,
|
|
226
|
+
* and node ids are step names — borrowing a body step's name would give the
|
|
227
|
+
* loop and that step the same id, collapsing one onto the other.
|
|
228
|
+
*/
|
|
229
|
+
stepName?: string;
|
|
219
230
|
/** Source array variable name */
|
|
220
231
|
sourceVar: string;
|
|
221
232
|
/** Iterator variable name */
|
|
222
233
|
itemVar: string;
|
|
223
234
|
/** Execution mode */
|
|
224
235
|
mode: 'parallel' | 'sequential';
|
|
225
|
-
/**
|
|
226
|
-
|
|
236
|
+
/** Steps to execute inline per iteration, in order */
|
|
237
|
+
body: Array<RpcStepMeta | SleepStepMeta | SuspendStepMeta>;
|
|
227
238
|
/** Time between iterations (sequential mode only) */
|
|
228
239
|
timeBetween?: string;
|
|
229
240
|
}
|
|
@@ -234,6 +245,12 @@ export interface ReturnStepMeta {
|
|
|
234
245
|
type: 'return';
|
|
235
246
|
/** Output bindings */
|
|
236
247
|
outputs: Record<string, OutputBinding>;
|
|
248
|
+
/**
|
|
249
|
+
* Variables spread into the returned object (`return { ...r }`), or the sole
|
|
250
|
+
* returned variable (`return r`). Their fields are not enumerable statically,
|
|
251
|
+
* so they are recorded by name rather than expanded into `outputs`.
|
|
252
|
+
*/
|
|
253
|
+
spread?: string[];
|
|
237
254
|
}
|
|
238
255
|
/**
|
|
239
256
|
* Inline step metadata (legacy support)
|
|
@@ -258,6 +275,13 @@ export interface SleepStepMeta {
|
|
|
258
275
|
stepName: string;
|
|
259
276
|
/** Sleep duration */
|
|
260
277
|
duration: string | number;
|
|
278
|
+
/**
|
|
279
|
+
* Source text of a duration only known at runtime (e.g. a loop variable).
|
|
280
|
+
* The closure evaluates it, so it is legal DSL; it is kept separate from
|
|
281
|
+
* `duration` so regenerated code emits it raw rather than as a string
|
|
282
|
+
* literal, exactly as `expression` does on a set step.
|
|
283
|
+
*/
|
|
284
|
+
expression?: string;
|
|
261
285
|
}
|
|
262
286
|
/**
|
|
263
287
|
* Cancel step metadata
|
|
@@ -276,8 +300,14 @@ export interface SetStepMeta {
|
|
|
276
300
|
type: 'set';
|
|
277
301
|
/** Variable name to set (must be in context) */
|
|
278
302
|
variable: string;
|
|
279
|
-
/**
|
|
280
|
-
value
|
|
303
|
+
/** Literal value to assign. Mutually exclusive with `expression`. */
|
|
304
|
+
value?: unknown;
|
|
305
|
+
/**
|
|
306
|
+
* Source text of a non-literal assignment (e.g. `count + 1`). Kept separate
|
|
307
|
+
* from `value` so regenerated code can emit it raw — a string `value` is a
|
|
308
|
+
* string literal, an `expression` is code.
|
|
309
|
+
*/
|
|
310
|
+
expression?: string;
|
|
281
311
|
}
|
|
282
312
|
/**
|
|
283
313
|
* Switch case metadata
|
|
@@ -443,6 +443,14 @@ export declare abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
443
443
|
* loop exits immediately without recording a step error or retrying.
|
|
444
444
|
*/
|
|
445
445
|
private runInlineRetryLoop;
|
|
446
|
+
/**
|
|
447
|
+
* Run a failed step's compensation handler as a durable step of its own.
|
|
448
|
+
*
|
|
449
|
+
* Durable rather than a bare invoke so a replay does not compensate twice —
|
|
450
|
+
* a handler is typically a refund or a rollback. `onError` is deliberately
|
|
451
|
+
* not forwarded: a compensation handler cannot itself compensate.
|
|
452
|
+
*/
|
|
453
|
+
private runStepCompensation;
|
|
446
454
|
private rpcStep;
|
|
447
455
|
private inlineStep;
|
|
448
456
|
private sleepStep;
|
|
@@ -1156,6 +1156,16 @@ export class PikkuWorkflowService {
|
|
|
1156
1156
|
}
|
|
1157
1157
|
}
|
|
1158
1158
|
}
|
|
1159
|
+
/**
|
|
1160
|
+
* Run a failed step's compensation handler as a durable step of its own.
|
|
1161
|
+
*
|
|
1162
|
+
* Durable rather than a bare invoke so a replay does not compensate twice —
|
|
1163
|
+
* a handler is typically a refund or a rollback. `onError` is deliberately
|
|
1164
|
+
* not forwarded: a compensation handler cannot itself compensate.
|
|
1165
|
+
*/
|
|
1166
|
+
async runStepCompensation(runId, stepName, onErrorRpcName, rpcService, error) {
|
|
1167
|
+
await this.rpcStep(runId, `${stepName}:onError`, onErrorRpcName, { error: { message: error.message } }, rpcService, { retries: 0 });
|
|
1168
|
+
}
|
|
1159
1169
|
async rpcStep(runId, logicalStepName, rpcName, data, rpcService, stepOptions) {
|
|
1160
1170
|
// Capture the predecessor before nextStepKey advances the lineage to us.
|
|
1161
1171
|
const fromStepName = this.lastStepName(runId);
|
|
@@ -1168,6 +1178,7 @@ export class PikkuWorkflowService {
|
|
|
1168
1178
|
retries: stepOptions?.retries ?? DEFAULT_STEP_RETRIES,
|
|
1169
1179
|
retryDelay: stepOptions?.retryDelay,
|
|
1170
1180
|
actor: stepOptions?.actor,
|
|
1181
|
+
onError: stepOptions?.onError,
|
|
1171
1182
|
};
|
|
1172
1183
|
// Check if step already exists
|
|
1173
1184
|
let stepState;
|
|
@@ -1186,6 +1197,11 @@ export class PikkuWorkflowService {
|
|
|
1186
1197
|
// Step failed with retries exhausted - throw error to fail the workflow
|
|
1187
1198
|
const error = new Error(stepState.error?.message ||
|
|
1188
1199
|
`Step '${stepName}' failed after exhausting all retries`);
|
|
1200
|
+
// Compensation, mirroring a graph node's onError: run the handler, then
|
|
1201
|
+
// still throw — the workflow fails either way.
|
|
1202
|
+
if (resolvedStepOptions.onError) {
|
|
1203
|
+
await this.runStepCompensation(runId, stepName, resolvedStepOptions.onError, rpcService, error);
|
|
1204
|
+
}
|
|
1189
1205
|
// Preserve original error properties if available
|
|
1190
1206
|
if (stepState.error) {
|
|
1191
1207
|
Object.assign(error, stepState.error);
|
|
@@ -194,8 +194,6 @@ export type CoreWorkflow<PikkuFunctionConfig extends CorePikkuFunctionConfig<any
|
|
|
194
194
|
func: PikkuFunctionConfig;
|
|
195
195
|
/** Middleware chain for this workflow */
|
|
196
196
|
middleware?: PikkuFunctionConfig['middleware'];
|
|
197
|
-
/** Permission requirements */
|
|
198
|
-
permissions?: PikkuFunctionConfig['permissions'];
|
|
199
197
|
/** Tags for organization and filtering */
|
|
200
198
|
tags?: string[];
|
|
201
199
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pikku/core",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.67",
|
|
4
4
|
"author": "yasser.fadl@gmail.com",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"module": "dist/index.js",
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
"./scheduler": "./dist/wirings/scheduler/index.js",
|
|
34
34
|
"./trigger": "./dist/wirings/trigger/index.js",
|
|
35
35
|
"./rpc": "./dist/wirings/rpc/index.js",
|
|
36
|
+
"./safe-fetch": "./dist/utils/safe-fetch.js",
|
|
36
37
|
"./mcp": "./dist/wirings/mcp/index.js",
|
|
37
38
|
"./ai-agent": "./dist/wirings/ai-agent/index.js",
|
|
38
39
|
"./gateway": "./dist/wirings/gateway/index.js",
|
package/src/index.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* @module @pikku/core
|
|
3
3
|
*/
|
|
4
4
|
export type {
|
|
5
|
+
AuthInstance,
|
|
5
6
|
CommonWireMeta,
|
|
6
7
|
CoreConfig,
|
|
7
8
|
CorePikkuMiddleware,
|
|
@@ -197,7 +198,7 @@ export {
|
|
|
197
198
|
addGlobalMiddleware,
|
|
198
199
|
} from './middleware-runner.js'
|
|
199
200
|
export { addGlobalPermission, checkAuthPermissions } from './permissions.js'
|
|
200
|
-
export { verifyScopes } from './scopes.js'
|
|
201
|
+
export { hasScopes, verifyScopes } from './scopes.js'
|
|
201
202
|
export {
|
|
202
203
|
isSerializable,
|
|
203
204
|
stopSingletonServices,
|
package/src/permissions.test.ts
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
clearPermissionsCache,
|
|
7
7
|
checkAuthPermissions,
|
|
8
8
|
} from './permissions.js'
|
|
9
|
-
import {
|
|
9
|
+
import { resetPikkuState } from './pikku-state.js'
|
|
10
10
|
import { pikkuAuth } from './function/functions.types.js'
|
|
11
11
|
import type { CoreServices, CoreUserSession } from './types/core.types.js'
|
|
12
12
|
import type { CorePermissionGroup } from './function/functions.types.js'
|
|
@@ -190,25 +190,31 @@ describe('checkAuthPermissions', () => {
|
|
|
190
190
|
)
|
|
191
191
|
})
|
|
192
192
|
|
|
193
|
-
test('
|
|
194
|
-
const store = pikkuState(null, 'misc', 'permissions')
|
|
195
|
-
store['isAdmin'] = [pikkuAuth(async () => false)] as any
|
|
193
|
+
test('evaluates a live pikkuAuth predicate from the config group', async () => {
|
|
196
194
|
assert.equal(
|
|
197
195
|
await checkAuthPermissions(
|
|
198
|
-
|
|
196
|
+
{ admin: pikkuAuth(async () => false) as any },
|
|
199
197
|
mockSession,
|
|
200
198
|
mockServices
|
|
201
199
|
),
|
|
202
200
|
false
|
|
203
201
|
)
|
|
202
|
+
assert.equal(
|
|
203
|
+
await checkAuthPermissions(
|
|
204
|
+
{ admin: pikkuAuth(async () => true) as any },
|
|
205
|
+
mockSession,
|
|
206
|
+
mockServices
|
|
207
|
+
),
|
|
208
|
+
true
|
|
209
|
+
)
|
|
204
210
|
})
|
|
205
211
|
|
|
206
212
|
test('ignores data-dependent permissions (no auth marker)', async () => {
|
|
207
|
-
|
|
208
|
-
|
|
213
|
+
// A bare pikkuPermission carries no __pikkuAuth brand, so it cannot be
|
|
214
|
+
// evaluated at filter time and must not gate the tool list.
|
|
209
215
|
assert.equal(
|
|
210
216
|
await checkAuthPermissions(
|
|
211
|
-
|
|
217
|
+
{ ownsRow: (async () => false) as any },
|
|
212
218
|
mockSession,
|
|
213
219
|
mockServices
|
|
214
220
|
),
|