@pikku/core 0.12.8 → 0.12.10
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 +31 -0
- package/dist/env.d.ts +1 -0
- package/dist/env.js +1 -0
- package/dist/errors/errors.d.ts +14 -0
- package/dist/errors/errors.js +27 -0
- package/dist/function/function-runner.d.ts +3 -1
- package/dist/function/function-runner.js +5 -1
- package/dist/handle-error.js +2 -1
- package/dist/middleware/auth-bearer.js +10 -1
- package/dist/middleware/auth-cookie.js +34 -25
- package/dist/middleware/timeout.js +11 -5
- package/dist/pikku-state.js +1 -1
- package/dist/services/credential-service.d.ts +40 -0
- package/dist/services/credential-service.js +1 -0
- package/dist/services/credential-wire-service.d.ts +13 -0
- package/dist/services/credential-wire-service.js +31 -0
- package/dist/services/index.d.ts +5 -0
- package/dist/services/index.js +3 -0
- package/dist/services/local-content.d.ts +7 -4
- package/dist/services/local-content.js +42 -12
- package/dist/services/local-credential-service.d.ts +10 -0
- package/dist/services/local-credential-service.js +33 -0
- package/dist/services/local-secrets.js +2 -2
- package/dist/services/typed-credential-service.d.ts +27 -0
- package/dist/services/typed-credential-service.js +40 -0
- package/dist/testing/service-tests.d.ts +8 -0
- package/dist/testing/service-tests.js +107 -1
- package/dist/types/core.types.d.ts +7 -0
- package/dist/wirings/ai-agent/agent-dynamic-workflow.js +173 -53
- package/dist/wirings/ai-agent/ai-agent-prepare.js +2 -1
- package/dist/wirings/ai-agent/ai-agent-runner.js +2 -1
- package/dist/wirings/ai-agent/ai-agent-stream.js +2 -1
- package/dist/wirings/channel/local/local-channel-runner.js +11 -6
- package/dist/wirings/credential/credential.types.d.ts +24 -0
- package/dist/wirings/credential/credential.types.js +1 -0
- package/dist/wirings/credential/index.d.ts +3 -0
- package/dist/wirings/credential/index.js +2 -0
- package/dist/wirings/credential/validate-credential-definitions.d.ts +6 -0
- package/dist/wirings/credential/validate-credential-definitions.js +38 -0
- package/dist/wirings/credential/wire-credential.d.ts +48 -0
- package/dist/wirings/credential/wire-credential.js +47 -0
- package/dist/wirings/http/http-runner.js +8 -2
- package/dist/wirings/http/pikku-fetch-http-request.js +11 -1
- package/dist/wirings/oauth2/index.d.ts +2 -1
- package/dist/wirings/oauth2/index.js +1 -1
- package/dist/wirings/oauth2/oauth2-client.js +4 -8
- package/dist/wirings/oauth2/oauth2-routes.d.ts +35 -0
- package/dist/wirings/oauth2/oauth2-routes.js +146 -0
- package/dist/wirings/oauth2/oauth2.types.d.ts +0 -26
- package/dist/wirings/workflow/graph/graph-runner.d.ts +8 -1
- package/dist/wirings/workflow/graph/graph-runner.js +87 -7
- package/dist/wirings/workflow/pikku-workflow-service.d.ts +5 -2
- package/dist/wirings/workflow/pikku-workflow-service.js +99 -24
- package/dist/wirings/workflow/workflow.types.d.ts +4 -0
- package/package.json +2 -1
- package/src/env.ts +1 -0
- package/src/errors/errors.ts +35 -0
- package/src/function/function-runner.ts +11 -0
- package/src/handle-error.ts +2 -1
- package/src/middleware/auth-bearer.ts +10 -1
- package/src/middleware/auth-cookie.ts +11 -4
- package/src/middleware/timeout.ts +14 -7
- package/src/pikku-state.ts +1 -1
- package/src/services/credential-service.ts +44 -0
- package/src/services/credential-wire-service.ts +44 -0
- package/src/services/index.ts +12 -0
- package/src/services/local-content.ts +61 -13
- package/src/services/local-credential-service.ts +41 -0
- package/src/services/local-secrets.test.ts +2 -2
- package/src/services/local-secrets.ts +2 -2
- package/src/services/typed-credential-service.ts +75 -0
- package/src/testing/service-tests.ts +140 -1
- package/src/types/core.types.ts +7 -0
- package/src/wirings/ai-agent/agent-dynamic-workflow.ts +378 -237
- package/src/wirings/ai-agent/ai-agent-prepare.ts +2 -1
- package/src/wirings/ai-agent/ai-agent-runner.test.ts +34 -0
- package/src/wirings/ai-agent/ai-agent-runner.ts +2 -1
- package/src/wirings/ai-agent/ai-agent-stream.ts +2 -1
- package/src/wirings/channel/local/local-channel-runner.ts +11 -6
- package/src/wirings/credential/credential.types.ts +28 -0
- package/src/wirings/credential/index.ts +8 -0
- package/src/wirings/credential/validate-credential-definitions.ts +64 -0
- package/src/wirings/credential/wire-credential.ts +49 -0
- package/src/wirings/http/http-runner.ts +11 -2
- package/src/wirings/http/pikku-fetch-http-request.ts +11 -1
- package/src/wirings/oauth2/index.ts +2 -1
- package/src/wirings/oauth2/oauth2-client.ts +4 -8
- package/src/wirings/oauth2/oauth2-routes.ts +234 -0
- package/src/wirings/oauth2/oauth2.types.ts +0 -27
- package/src/wirings/workflow/graph/graph-runner.test.ts +42 -0
- package/src/wirings/workflow/graph/graph-runner.ts +117 -8
- package/src/wirings/workflow/pikku-workflow-service.test.ts +109 -0
- package/src/wirings/workflow/pikku-workflow-service.ts +150 -34
- package/src/wirings/workflow/workflow.types.ts +4 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/wirings/oauth2/wire-oauth2-credential.d.ts +0 -20
- package/dist/wirings/oauth2/wire-oauth2-credential.js +0 -21
- package/src/wirings/oauth2/wire-oauth2-credential.ts +0 -23
|
@@ -4,6 +4,7 @@ import { getDurationInMilliseconds } from '../../time-utils.js';
|
|
|
4
4
|
import { continueGraph, executeGraphStep, runWorkflowGraph, runFromMeta, } from './graph/graph-runner.js';
|
|
5
5
|
import { PikkuError, addError } from '../../errors/error-handler.js';
|
|
6
6
|
import { RPCNotFoundError } from '../rpc/rpc-runner.js';
|
|
7
|
+
import { ChildWorkflowStartedException } from './graph/graph-runner.js';
|
|
7
8
|
/**
|
|
8
9
|
* Exception thrown when workflow needs to pause for async step
|
|
9
10
|
*/
|
|
@@ -84,6 +85,9 @@ const WORKFLOW_END_STATES = new Set([
|
|
|
84
85
|
*/
|
|
85
86
|
export class PikkuWorkflowService {
|
|
86
87
|
inlineRuns = new Set();
|
|
88
|
+
get logger() {
|
|
89
|
+
return getSingletonServices()?.logger;
|
|
90
|
+
}
|
|
87
91
|
constructor() { }
|
|
88
92
|
/**
|
|
89
93
|
* Check if a run is executing inline (without queues)
|
|
@@ -124,12 +128,12 @@ export class PikkuWorkflowService {
|
|
|
124
128
|
}
|
|
125
129
|
async queueStepWorker(runId, stepName, rpcName, data) {
|
|
126
130
|
const queueService = this.verifyQueueService();
|
|
127
|
-
await queueService.add(this.getConfig().stepWorkerQueueName, {
|
|
131
|
+
await queueService.add(this.getConfig().stepWorkerQueueName, JSON.parse(JSON.stringify({
|
|
128
132
|
runId,
|
|
129
133
|
stepName,
|
|
130
134
|
rpcName,
|
|
131
135
|
data,
|
|
132
|
-
});
|
|
136
|
+
})));
|
|
133
137
|
}
|
|
134
138
|
/**
|
|
135
139
|
* Execute a workflow sleep step completion
|
|
@@ -163,7 +167,9 @@ export class PikkuWorkflowService {
|
|
|
163
167
|
throw new WorkflowNotFoundError(name);
|
|
164
168
|
}
|
|
165
169
|
if (workflowMeta.source === 'graph' || workflowMeta.source === 'ai-agent') {
|
|
166
|
-
const shouldInline = options?.inline ||
|
|
170
|
+
const shouldInline = options?.inline ||
|
|
171
|
+
workflowMeta.inline ||
|
|
172
|
+
!getSingletonServices()?.queueService;
|
|
167
173
|
return runWorkflowGraph(this, name, input, rpcService, shouldInline, options?.startNode, wire);
|
|
168
174
|
}
|
|
169
175
|
// DSL workflow - check registration exists
|
|
@@ -175,17 +181,25 @@ export class PikkuWorkflowService {
|
|
|
175
181
|
if (!workflowMeta.graphHash) {
|
|
176
182
|
throw new Error(`Missing workflow graphHash for '${name}'`);
|
|
177
183
|
}
|
|
178
|
-
const shouldInline = options?.inline ||
|
|
184
|
+
const shouldInline = options?.inline ||
|
|
185
|
+
workflowMeta.inline ||
|
|
186
|
+
!getSingletonServices()?.queueService;
|
|
179
187
|
const runId = await this.createRun(name, input, shouldInline, workflowMeta.graphHash, wire);
|
|
180
188
|
if (shouldInline) {
|
|
181
189
|
this.inlineRuns.add(runId);
|
|
182
|
-
|
|
183
|
-
.
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
.
|
|
190
|
+
try {
|
|
191
|
+
await this.runWorkflowJob(runId, rpcService);
|
|
192
|
+
}
|
|
193
|
+
catch (error) {
|
|
194
|
+
if (error.name !== 'WorkflowAsyncException' &&
|
|
195
|
+
error.name !== 'WorkflowCancelledException' &&
|
|
196
|
+
error.name !== 'WorkflowSuspendedException') {
|
|
197
|
+
getSingletonServices().logger.error(`Workflow ${name} (run ${runId}) failed:`, error);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
finally {
|
|
187
201
|
this.inlineRuns.delete(runId);
|
|
188
|
-
}
|
|
202
|
+
}
|
|
189
203
|
}
|
|
190
204
|
else {
|
|
191
205
|
await this.resumeWorkflow(runId);
|
|
@@ -227,6 +241,14 @@ export class PikkuWorkflowService {
|
|
|
227
241
|
}
|
|
228
242
|
if (workflowMeta?.source === 'graph') {
|
|
229
243
|
await continueGraph(this, runId, run.workflow);
|
|
244
|
+
const updatedRun = await this.getRun(runId);
|
|
245
|
+
if (updatedRun?.status === 'completed') {
|
|
246
|
+
await this.onChildWorkflowCompleted(updatedRun, updatedRun.output);
|
|
247
|
+
}
|
|
248
|
+
else if (updatedRun?.status === 'failed' ||
|
|
249
|
+
updatedRun?.status === 'cancelled') {
|
|
250
|
+
await this.onChildWorkflowFailed(updatedRun, new Error(updatedRun.error?.message || 'Child workflow failed'));
|
|
251
|
+
}
|
|
230
252
|
return;
|
|
231
253
|
}
|
|
232
254
|
const registrations = pikkuState(null, 'workflows', 'registrations');
|
|
@@ -249,6 +271,7 @@ export class PikkuWorkflowService {
|
|
|
249
271
|
data: () => run.input,
|
|
250
272
|
});
|
|
251
273
|
await this.updateRunStatus(runId, 'completed', result);
|
|
274
|
+
await this.onChildWorkflowCompleted(run, result);
|
|
252
275
|
}
|
|
253
276
|
catch (error) {
|
|
254
277
|
if (error instanceof WorkflowAsyncException) {
|
|
@@ -260,6 +283,7 @@ export class PikkuWorkflowService {
|
|
|
260
283
|
stack: '',
|
|
261
284
|
code: 'WORKFLOW_CANCELLED',
|
|
262
285
|
});
|
|
286
|
+
await this.onChildWorkflowFailed(run, error);
|
|
263
287
|
throw error;
|
|
264
288
|
}
|
|
265
289
|
if (error instanceof WorkflowSuspendedException) {
|
|
@@ -275,10 +299,27 @@ export class PikkuWorkflowService {
|
|
|
275
299
|
stack: error.stack,
|
|
276
300
|
code: error.code,
|
|
277
301
|
});
|
|
302
|
+
await this.onChildWorkflowFailed(run, error);
|
|
278
303
|
throw error;
|
|
279
304
|
}
|
|
280
305
|
});
|
|
281
306
|
}
|
|
307
|
+
async onChildWorkflowCompleted(childRun, result) {
|
|
308
|
+
const { parentRunId, parentStepId } = childRun.wire ?? {};
|
|
309
|
+
if (!parentRunId || !parentStepId)
|
|
310
|
+
return;
|
|
311
|
+
this.logger?.debug(`Child workflow ${childRun.id} completed, updating parent step ${parentStepId}`);
|
|
312
|
+
await this.setStepResult(parentStepId, result);
|
|
313
|
+
await this.resumeWorkflow(parentRunId);
|
|
314
|
+
}
|
|
315
|
+
async onChildWorkflowFailed(childRun, error) {
|
|
316
|
+
const { parentRunId, parentStepId } = childRun.wire ?? {};
|
|
317
|
+
if (!parentRunId || !parentStepId)
|
|
318
|
+
return;
|
|
319
|
+
this.logger?.debug(`Child workflow ${childRun.id} failed, updating parent step ${parentStepId}`);
|
|
320
|
+
await this.setStepError(parentStepId, error);
|
|
321
|
+
await this.resumeWorkflow(parentRunId);
|
|
322
|
+
}
|
|
282
323
|
async runVersionMismatchFallback(run, currentMeta, rpcService) {
|
|
283
324
|
const source = currentMeta.source;
|
|
284
325
|
if (source === 'complex') {
|
|
@@ -309,9 +350,8 @@ export class PikkuWorkflowService {
|
|
|
309
350
|
await this.withStepLock(runId, stepName, async () => {
|
|
310
351
|
// Get step state
|
|
311
352
|
let stepState = await this.getStepState(runId, stepName);
|
|
312
|
-
// Idempotency - if already succeeded,
|
|
353
|
+
// Idempotency - if already succeeded, nothing to do
|
|
313
354
|
if (stepState.status === 'succeeded') {
|
|
314
|
-
await this.resumeWorkflow(runId);
|
|
315
355
|
return;
|
|
316
356
|
}
|
|
317
357
|
// Log warning if already running (race condition)
|
|
@@ -322,8 +362,7 @@ export class PikkuWorkflowService {
|
|
|
322
362
|
if (stepState.status === 'failed') {
|
|
323
363
|
stepState = await this.createRetryAttempt(stepState.stepId, 'running');
|
|
324
364
|
}
|
|
325
|
-
if (stepState.status === 'pending') {
|
|
326
|
-
// Mark pending step as running before execution
|
|
365
|
+
if (stepState.status === 'pending' || stepState.status === 'scheduled') {
|
|
327
366
|
await this.setStepRunning(stepState.stepId);
|
|
328
367
|
}
|
|
329
368
|
try {
|
|
@@ -334,17 +373,49 @@ export class PikkuWorkflowService {
|
|
|
334
373
|
}
|
|
335
374
|
const meta = pikkuState(null, 'workflows', 'meta');
|
|
336
375
|
const workflowMeta = meta[run.workflow];
|
|
337
|
-
|
|
376
|
+
const isGraphWorkflow = workflowMeta?.source === 'graph' ||
|
|
377
|
+
workflowMeta?.source === 'ai-agent';
|
|
378
|
+
if (isGraphWorkflow &&
|
|
379
|
+
workflowMeta?.nodes &&
|
|
380
|
+
stepName in workflowMeta.nodes) {
|
|
338
381
|
result = await executeGraphStep(this, rpcService, runId, stepState.stepId, stepName, rpcName, data, run.workflow);
|
|
339
382
|
}
|
|
340
383
|
else {
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
384
|
+
// Check if rpcName refers to a sub-workflow
|
|
385
|
+
const subWorkflowMeta = meta[rpcName];
|
|
386
|
+
if (subWorkflowMeta) {
|
|
387
|
+
const childWire = {
|
|
388
|
+
type: 'workflow',
|
|
389
|
+
id: rpcName,
|
|
390
|
+
parentRunId: runId,
|
|
391
|
+
parentStepId: stepState.stepId,
|
|
392
|
+
};
|
|
393
|
+
const shouldInline = subWorkflowMeta.inline || !getSingletonServices()?.queueService;
|
|
394
|
+
const { runId: childRunId } = await this.startWorkflow(rpcName, data, childWire, rpcService, { inline: shouldInline });
|
|
395
|
+
await this.setStepChildRunId(stepState.stepId, childRunId);
|
|
396
|
+
if (shouldInline) {
|
|
397
|
+
const childRun = await this.getRun(childRunId);
|
|
398
|
+
if (childRun?.status === 'failed') {
|
|
399
|
+
throw new Error(childRun.error?.message || 'Sub-workflow failed');
|
|
400
|
+
}
|
|
401
|
+
if (childRun?.status === 'cancelled') {
|
|
402
|
+
throw new Error('Sub-workflow was cancelled');
|
|
403
|
+
}
|
|
404
|
+
result = childRun?.output;
|
|
405
|
+
}
|
|
406
|
+
else {
|
|
407
|
+
throw new ChildWorkflowStartedException(runId, stepState.stepId, childRunId);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
else {
|
|
411
|
+
result = await rpcService.rpcWithWire(rpcName, data, {
|
|
412
|
+
workflowStep: {
|
|
413
|
+
runId,
|
|
414
|
+
stepId: stepState.stepId,
|
|
415
|
+
attemptCount: stepState.attemptCount,
|
|
416
|
+
},
|
|
417
|
+
});
|
|
418
|
+
}
|
|
348
419
|
}
|
|
349
420
|
// Store result and mark succeeded
|
|
350
421
|
await this.setStepResult(stepState.stepId, result);
|
|
@@ -352,6 +423,10 @@ export class PikkuWorkflowService {
|
|
|
352
423
|
await this.resumeWorkflow(runId);
|
|
353
424
|
}
|
|
354
425
|
catch (error) {
|
|
426
|
+
if (error instanceof ChildWorkflowStartedException) {
|
|
427
|
+
this.logger?.debug(`Workflow step '${stepName}': child workflow ${error.childRunId} started, waiting for completion`);
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
355
430
|
if (error instanceof RPCNotFoundError) {
|
|
356
431
|
await this.setStepError(stepState.stepId, error);
|
|
357
432
|
await this.updateRunStatus(runId, 'suspended', undefined, {
|
|
@@ -437,12 +512,12 @@ export class PikkuWorkflowService {
|
|
|
437
512
|
// Map step retry options to queue job options
|
|
438
513
|
const retries = stepOptions?.retries ?? 0;
|
|
439
514
|
const retryDelay = stepOptions?.retryDelay;
|
|
440
|
-
await getSingletonServices().queueService.add(this.getConfig().stepWorkerQueueName, {
|
|
515
|
+
await getSingletonServices().queueService.add(this.getConfig().stepWorkerQueueName, JSON.parse(JSON.stringify({
|
|
441
516
|
runId,
|
|
442
517
|
stepName,
|
|
443
518
|
rpcName,
|
|
444
519
|
data,
|
|
445
|
-
}, {
|
|
520
|
+
})), {
|
|
446
521
|
// attempts includes initial attempt, retries doesn't
|
|
447
522
|
attempts: retries + 1,
|
|
448
523
|
// Map retry delay to backoff
|
|
@@ -7,6 +7,7 @@ export interface WorkflowRunWire {
|
|
|
7
7
|
type: string;
|
|
8
8
|
id?: string;
|
|
9
9
|
parentRunId?: string;
|
|
10
|
+
parentStepId?: string;
|
|
10
11
|
}
|
|
11
12
|
export interface WorkflowServiceConfig {
|
|
12
13
|
retries: number;
|
|
@@ -166,6 +167,7 @@ export type WorkflowsMeta = Record<string, CommonWireMeta & {
|
|
|
166
167
|
steps: WorkflowStepMeta[];
|
|
167
168
|
context?: WorkflowContext;
|
|
168
169
|
dsl?: boolean;
|
|
170
|
+
inline?: boolean;
|
|
169
171
|
}>;
|
|
170
172
|
/**
|
|
171
173
|
* Unified workflow runtime meta (used by runtime to execute workflows)
|
|
@@ -183,6 +185,8 @@ export interface WorkflowRuntimeMeta {
|
|
|
183
185
|
description?: string;
|
|
184
186
|
/** Tags for organization */
|
|
185
187
|
tags?: string[];
|
|
188
|
+
/** If true, workflow always executes inline without queues */
|
|
189
|
+
inline?: boolean;
|
|
186
190
|
/** Serialized nodes */
|
|
187
191
|
nodes?: Record<string, any>;
|
|
188
192
|
/** Entry node IDs for graph workflows (computed at build time) */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pikku/core",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.10",
|
|
4
4
|
"author": "yasser.fadl@gmail.com",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"module": "dist/index.js",
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
"./cli": "./dist/wirings/cli/index.js",
|
|
36
36
|
"./cli/channel": "./dist/wirings/cli/channel/index.js",
|
|
37
37
|
"./node": "./dist/wirings/node/index.js",
|
|
38
|
+
"./credential": "./dist/wirings/credential/index.js",
|
|
38
39
|
"./secret": "./dist/wirings/secret/index.js",
|
|
39
40
|
"./variable": "./dist/wirings/variable/index.js",
|
|
40
41
|
"./oauth2": "./dist/wirings/oauth2/index.js",
|
package/src/env.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const isProduction = () => process.env.NODE_ENV === 'production'
|
package/src/errors/errors.ts
CHANGED
|
@@ -372,3 +372,38 @@ addError(MissingSchemaError, {
|
|
|
372
372
|
message:
|
|
373
373
|
'A required schema was not found. Ensure schema generation has been run.',
|
|
374
374
|
})
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* An AI provider has not been configured. Set up an AI provider (e.g. OpenAI, Anthropic) to use agent features.
|
|
378
|
+
* @group Error
|
|
379
|
+
*/
|
|
380
|
+
export class AIProviderNotConfiguredError extends PikkuError {
|
|
381
|
+
constructor() {
|
|
382
|
+
super(
|
|
383
|
+
'No AI provider configured. Please set up an AI provider (e.g. OpenAI, Anthropic) and provide a valid API key to use this agent.'
|
|
384
|
+
)
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
addError(AIProviderNotConfiguredError, {
|
|
388
|
+
status: 503,
|
|
389
|
+
message:
|
|
390
|
+
'No AI provider configured. Please set up an AI provider (e.g. OpenAI, Anthropic) and provide a valid API key to use this agent.',
|
|
391
|
+
})
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* The AI provider API key is missing or invalid.
|
|
395
|
+
* @group Error
|
|
396
|
+
*/
|
|
397
|
+
export class AIProviderAuthError extends PikkuError {
|
|
398
|
+
constructor(message?: string) {
|
|
399
|
+
super(
|
|
400
|
+
message ||
|
|
401
|
+
'AI provider API key is missing or invalid. Please check your API key configuration.'
|
|
402
|
+
)
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
addError(AIProviderAuthError, {
|
|
406
|
+
status: 401,
|
|
407
|
+
message:
|
|
408
|
+
'AI provider API key is missing or invalid. Please check your API key configuration.',
|
|
409
|
+
})
|
|
@@ -28,6 +28,8 @@ import { parseVersionedId } from '../version.js'
|
|
|
28
28
|
import type { SessionService } from '../services/user-session-service.js'
|
|
29
29
|
import { createFunctionSessionWireProps } from '../services/user-session-service.js'
|
|
30
30
|
import { ForbiddenError, ReadonlySessionError } from '../errors/errors.js'
|
|
31
|
+
import type { PikkuCredentialWireService } from '../services/credential-wire-service.js'
|
|
32
|
+
import { createWireServicesCredentialWireProps } from '../services/credential-wire-service.js'
|
|
31
33
|
import { rpcService } from '../wirings/rpc/rpc-runner.js'
|
|
32
34
|
import { closeWireServices } from '../utils.js'
|
|
33
35
|
|
|
@@ -146,6 +148,7 @@ export const runPikkuFunc = async <In = any, Out = any>(
|
|
|
146
148
|
tags = [],
|
|
147
149
|
wire,
|
|
148
150
|
sessionService,
|
|
151
|
+
credentialWireService,
|
|
149
152
|
packageName = null,
|
|
150
153
|
}: {
|
|
151
154
|
singletonServices: CoreSingletonServices
|
|
@@ -162,6 +165,7 @@ export const runPikkuFunc = async <In = any, Out = any>(
|
|
|
162
165
|
tags?: string[]
|
|
163
166
|
wire: PikkuWire
|
|
164
167
|
sessionService?: SessionService<CoreUserSession>
|
|
168
|
+
credentialWireService?: PikkuCredentialWireService
|
|
165
169
|
packageName?: string | null
|
|
166
170
|
}
|
|
167
171
|
): Promise<Out> => {
|
|
@@ -312,6 +316,13 @@ export const runPikkuFunc = async <In = any, Out = any>(
|
|
|
312
316
|
})
|
|
313
317
|
}
|
|
314
318
|
|
|
319
|
+
if (credentialWireService) {
|
|
320
|
+
Object.assign(
|
|
321
|
+
resolvedWire,
|
|
322
|
+
createWireServicesCredentialWireProps(credentialWireService)
|
|
323
|
+
)
|
|
324
|
+
}
|
|
325
|
+
|
|
315
326
|
const wireServices = await resolvedCreateWireServices?.(
|
|
316
327
|
resolvedSingletonServices,
|
|
317
328
|
resolvedWire
|
package/src/handle-error.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isProduction } from './env.js'
|
|
1
2
|
import { getErrorResponse } from './errors/error-handler.js'
|
|
2
3
|
import { NotFoundError } from './errors/errors.js'
|
|
3
4
|
import type { Logger } from './services/logger.js'
|
|
@@ -55,7 +56,7 @@ export const handleHTTPError = (
|
|
|
55
56
|
if (trackerId) {
|
|
56
57
|
logger.warn(`Error id: ${trackerId}`)
|
|
57
58
|
const errorBody: Record<string, unknown> = { errorId: trackerId }
|
|
58
|
-
if (exposeErrors && e instanceof Error) {
|
|
59
|
+
if (exposeErrors && !isProduction() && e instanceof Error) {
|
|
59
60
|
errorBody.message = e.message
|
|
60
61
|
errorBody.stack = e.stack
|
|
61
62
|
}
|
|
@@ -2,6 +2,15 @@ import { InvalidSessionError } from '../errors/errors.js'
|
|
|
2
2
|
import type { CoreUserSession } from '../types/core.types.js'
|
|
3
3
|
import { pikkuMiddleware, pikkuMiddlewareFactory } from '../types/core.types.js'
|
|
4
4
|
|
|
5
|
+
const constantTimeEqual = (a: string, b: string): boolean => {
|
|
6
|
+
if (a.length !== b.length) return false
|
|
7
|
+
let result = 0
|
|
8
|
+
for (let i = 0; i < a.length; i++) {
|
|
9
|
+
result |= a.charCodeAt(i) ^ b.charCodeAt(i)
|
|
10
|
+
}
|
|
11
|
+
return result === 0
|
|
12
|
+
}
|
|
13
|
+
|
|
5
14
|
/**
|
|
6
15
|
* Bearer token middleware that extracts and validates tokens from the Authorization header.
|
|
7
16
|
*
|
|
@@ -55,7 +64,7 @@ export const authBearer = pikkuMiddlewareFactory<{
|
|
|
55
64
|
|
|
56
65
|
let userSession: CoreUserSession | null = null
|
|
57
66
|
|
|
58
|
-
if (token && bearerToken
|
|
67
|
+
if (token && constantTimeEqual(bearerToken, token.value)) {
|
|
59
68
|
userSession = token.userSession
|
|
60
69
|
} else if (jwtService && !token) {
|
|
61
70
|
userSession = await jwtService.decode(bearerToken)
|
|
@@ -35,8 +35,15 @@ export const authCookie = pikkuMiddlewareFactory<{
|
|
|
35
35
|
name: string
|
|
36
36
|
options: SerializeOptions
|
|
37
37
|
expiresIn: RelativeTimeInput
|
|
38
|
-
}>(({ name, options, expiresIn }) =>
|
|
39
|
-
|
|
38
|
+
}>(({ name, options, expiresIn }) => {
|
|
39
|
+
const mergedOptions: SerializeOptions = {
|
|
40
|
+
httpOnly: true,
|
|
41
|
+
secure: true,
|
|
42
|
+
sameSite: 'lax',
|
|
43
|
+
path: '/',
|
|
44
|
+
...options,
|
|
45
|
+
}
|
|
46
|
+
return pikkuMiddleware(
|
|
40
47
|
async (
|
|
41
48
|
{ jwt: jwtService, logger },
|
|
42
49
|
{ http, setSession, getSession, session, hasSessionChanged },
|
|
@@ -67,7 +74,7 @@ export const authCookie = pikkuMiddlewareFactory<{
|
|
|
67
74
|
name,
|
|
68
75
|
await jwtService.encode(expiresIn, currentSession),
|
|
69
76
|
{
|
|
70
|
-
...
|
|
77
|
+
...mergedOptions,
|
|
71
78
|
expires: getRelativeTimeOffsetFromNow(expiresIn),
|
|
72
79
|
}
|
|
73
80
|
)
|
|
@@ -77,4 +84,4 @@ export const authCookie = pikkuMiddlewareFactory<{
|
|
|
77
84
|
}
|
|
78
85
|
}
|
|
79
86
|
)
|
|
80
|
-
)
|
|
87
|
+
})
|
|
@@ -4,12 +4,19 @@ import { pikkuMiddleware, pikkuMiddlewareFactory } from '../types/core.types.js'
|
|
|
4
4
|
export const timeout = () =>
|
|
5
5
|
pikkuMiddlewareFactory<{ timeout: number }>(({ timeout }) =>
|
|
6
6
|
pikkuMiddleware(async (_services, _wire, next) => {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
7
|
+
let timeoutId: ReturnType<typeof setTimeout> | undefined
|
|
8
|
+
await Promise.race([
|
|
9
|
+
next(),
|
|
10
|
+
new Promise<never>((_resolve, reject) => {
|
|
11
|
+
timeoutId = setTimeout(
|
|
12
|
+
() => reject(new RequestTimeoutError()),
|
|
13
|
+
timeout
|
|
14
|
+
)
|
|
15
|
+
}),
|
|
16
|
+
]).finally(() => {
|
|
17
|
+
if (timeoutId !== undefined) {
|
|
18
|
+
clearTimeout(timeoutId)
|
|
19
|
+
}
|
|
20
|
+
})
|
|
14
21
|
})
|
|
15
22
|
)
|
package/src/pikku-state.ts
CHANGED
|
@@ -14,7 +14,7 @@ import type { GatewaysMeta } from './wirings/gateway/gateway.types.js'
|
|
|
14
14
|
import type { ScheduledTasksMeta } from './wirings/scheduler/scheduler.types.js'
|
|
15
15
|
import type { TriggerMeta } from './wirings/trigger/trigger.types.js'
|
|
16
16
|
|
|
17
|
-
const PIKKU_STATE_KEY = Symbol
|
|
17
|
+
const PIKKU_STATE_KEY = Symbol('@pikku/core/state')
|
|
18
18
|
|
|
19
19
|
export const getAllPackageStates = (): Map<string, PikkuPackageState> => {
|
|
20
20
|
if (!(globalThis as any)[PIKKU_STATE_KEY]) {
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interface for managing dynamic/managed credentials.
|
|
3
|
+
* Used for OAuth tokens, per-user API keys, and other credentials
|
|
4
|
+
* that change at runtime (as opposed to SecretService which is for
|
|
5
|
+
* static, developer-configured values).
|
|
6
|
+
*/
|
|
7
|
+
export interface CredentialService {
|
|
8
|
+
/**
|
|
9
|
+
* Retrieves a credential by name and optional user ID.
|
|
10
|
+
* @param name - The credential name (e.g. 'stripe', 'google-sheets').
|
|
11
|
+
* @param userId - Optional user ID for per-user credentials. Omit for platform-level.
|
|
12
|
+
* @returns The credential value, or null if not found.
|
|
13
|
+
*/
|
|
14
|
+
get<T = unknown>(name: string, userId?: string): Promise<T | null>
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Stores a credential.
|
|
18
|
+
* @param name - The credential name.
|
|
19
|
+
* @param value - The credential value to store.
|
|
20
|
+
* @param userId - Optional user ID for per-user credentials. Omit for platform-level.
|
|
21
|
+
*/
|
|
22
|
+
set(name: string, value: unknown, userId?: string): Promise<void>
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Deletes a credential.
|
|
26
|
+
* @param name - The credential name.
|
|
27
|
+
* @param userId - Optional user ID for per-user credentials. Omit for platform-level.
|
|
28
|
+
*/
|
|
29
|
+
delete(name: string, userId?: string): Promise<void>
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Checks if a credential exists.
|
|
33
|
+
* @param name - The credential name.
|
|
34
|
+
* @param userId - Optional user ID for per-user credentials. Omit for platform-level.
|
|
35
|
+
*/
|
|
36
|
+
has(name: string, userId?: string): Promise<boolean>
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Retrieves all credentials for a user.
|
|
40
|
+
* @param userId - The user ID.
|
|
41
|
+
* @returns A record of credential name to value.
|
|
42
|
+
*/
|
|
43
|
+
getAll(userId: string): Promise<Record<string, unknown>>
|
|
44
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export class PikkuCredentialWireService {
|
|
2
|
+
private credentials: Record<string, unknown> = {}
|
|
3
|
+
|
|
4
|
+
set(name: string, value: unknown): void {
|
|
5
|
+
this.credentials[name] = value
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
getAll(): Record<string, unknown> {
|
|
9
|
+
return this.credentials
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
getScoped(allowedNames: string[]): Record<string, unknown> {
|
|
13
|
+
const scoped: Record<string, unknown> = {}
|
|
14
|
+
for (const name of allowedNames) {
|
|
15
|
+
if (name in this.credentials) {
|
|
16
|
+
scoped[name] = this.credentials[name]
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return scoped
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function createMiddlewareCredentialWireProps(
|
|
24
|
+
credentialWire: PikkuCredentialWireService
|
|
25
|
+
) {
|
|
26
|
+
return {
|
|
27
|
+
setCredential: (name: string, value: unknown) =>
|
|
28
|
+
credentialWire.set(name, value),
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function createWireServicesCredentialWireProps(
|
|
33
|
+
credentialWire: PikkuCredentialWireService,
|
|
34
|
+
allowedNames?: string[]
|
|
35
|
+
) {
|
|
36
|
+
return {
|
|
37
|
+
setCredential: (name: string, value: unknown) =>
|
|
38
|
+
credentialWire.set(name, value),
|
|
39
|
+
getCredentials: () =>
|
|
40
|
+
allowedNames
|
|
41
|
+
? credentialWire.getScoped(allowedNames)
|
|
42
|
+
: credentialWire.getAll(),
|
|
43
|
+
}
|
|
44
|
+
}
|
package/src/services/index.ts
CHANGED
|
@@ -10,8 +10,14 @@ export {
|
|
|
10
10
|
createFunctionSessionWireProps,
|
|
11
11
|
} from './user-session-service.js'
|
|
12
12
|
export { TypedSecretService } from './typed-secret-service.js'
|
|
13
|
+
export {
|
|
14
|
+
PikkuCredentialWireService,
|
|
15
|
+
createMiddlewareCredentialWireProps,
|
|
16
|
+
createWireServicesCredentialWireProps,
|
|
17
|
+
} from './credential-wire-service.js'
|
|
13
18
|
export { TypedVariablesService } from './typed-variables-service.js'
|
|
14
19
|
export { LocalSecretService } from './local-secrets.js'
|
|
20
|
+
export { LocalCredentialService } from './local-credential-service.js'
|
|
15
21
|
export { LocalVariablesService } from './local-variables.js'
|
|
16
22
|
export { ConsoleLogger } from './logger-console.js'
|
|
17
23
|
export { InMemoryWorkflowService } from './in-memory-workflow-service.js'
|
|
@@ -53,4 +59,10 @@ export type {
|
|
|
53
59
|
CredentialStatus,
|
|
54
60
|
CredentialMeta,
|
|
55
61
|
} from './typed-secret-service.js'
|
|
62
|
+
export type { CredentialService } from './credential-service.js'
|
|
63
|
+
export { TypedCredentialService } from './typed-credential-service.js'
|
|
64
|
+
export type {
|
|
65
|
+
CredentialStatusInfo,
|
|
66
|
+
CredentialMetaInfo,
|
|
67
|
+
} from './typed-credential-service.js'
|
|
56
68
|
export type { VariableStatus, VariableMeta } from './typed-variables-service.js'
|