@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.
Files changed (98) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/dist/env.d.ts +1 -0
  3. package/dist/env.js +1 -0
  4. package/dist/errors/errors.d.ts +14 -0
  5. package/dist/errors/errors.js +27 -0
  6. package/dist/function/function-runner.d.ts +3 -1
  7. package/dist/function/function-runner.js +5 -1
  8. package/dist/handle-error.js +2 -1
  9. package/dist/middleware/auth-bearer.js +10 -1
  10. package/dist/middleware/auth-cookie.js +34 -25
  11. package/dist/middleware/timeout.js +11 -5
  12. package/dist/pikku-state.js +1 -1
  13. package/dist/services/credential-service.d.ts +40 -0
  14. package/dist/services/credential-service.js +1 -0
  15. package/dist/services/credential-wire-service.d.ts +13 -0
  16. package/dist/services/credential-wire-service.js +31 -0
  17. package/dist/services/index.d.ts +5 -0
  18. package/dist/services/index.js +3 -0
  19. package/dist/services/local-content.d.ts +7 -4
  20. package/dist/services/local-content.js +42 -12
  21. package/dist/services/local-credential-service.d.ts +10 -0
  22. package/dist/services/local-credential-service.js +33 -0
  23. package/dist/services/local-secrets.js +2 -2
  24. package/dist/services/typed-credential-service.d.ts +27 -0
  25. package/dist/services/typed-credential-service.js +40 -0
  26. package/dist/testing/service-tests.d.ts +8 -0
  27. package/dist/testing/service-tests.js +107 -1
  28. package/dist/types/core.types.d.ts +7 -0
  29. package/dist/wirings/ai-agent/agent-dynamic-workflow.js +173 -53
  30. package/dist/wirings/ai-agent/ai-agent-prepare.js +2 -1
  31. package/dist/wirings/ai-agent/ai-agent-runner.js +2 -1
  32. package/dist/wirings/ai-agent/ai-agent-stream.js +2 -1
  33. package/dist/wirings/channel/local/local-channel-runner.js +11 -6
  34. package/dist/wirings/credential/credential.types.d.ts +24 -0
  35. package/dist/wirings/credential/credential.types.js +1 -0
  36. package/dist/wirings/credential/index.d.ts +3 -0
  37. package/dist/wirings/credential/index.js +2 -0
  38. package/dist/wirings/credential/validate-credential-definitions.d.ts +6 -0
  39. package/dist/wirings/credential/validate-credential-definitions.js +38 -0
  40. package/dist/wirings/credential/wire-credential.d.ts +48 -0
  41. package/dist/wirings/credential/wire-credential.js +47 -0
  42. package/dist/wirings/http/http-runner.js +8 -2
  43. package/dist/wirings/http/pikku-fetch-http-request.js +11 -1
  44. package/dist/wirings/oauth2/index.d.ts +2 -1
  45. package/dist/wirings/oauth2/index.js +1 -1
  46. package/dist/wirings/oauth2/oauth2-client.js +4 -8
  47. package/dist/wirings/oauth2/oauth2-routes.d.ts +35 -0
  48. package/dist/wirings/oauth2/oauth2-routes.js +146 -0
  49. package/dist/wirings/oauth2/oauth2.types.d.ts +0 -26
  50. package/dist/wirings/workflow/graph/graph-runner.d.ts +8 -1
  51. package/dist/wirings/workflow/graph/graph-runner.js +87 -7
  52. package/dist/wirings/workflow/pikku-workflow-service.d.ts +5 -2
  53. package/dist/wirings/workflow/pikku-workflow-service.js +99 -24
  54. package/dist/wirings/workflow/workflow.types.d.ts +4 -0
  55. package/package.json +2 -1
  56. package/src/env.ts +1 -0
  57. package/src/errors/errors.ts +35 -0
  58. package/src/function/function-runner.ts +11 -0
  59. package/src/handle-error.ts +2 -1
  60. package/src/middleware/auth-bearer.ts +10 -1
  61. package/src/middleware/auth-cookie.ts +11 -4
  62. package/src/middleware/timeout.ts +14 -7
  63. package/src/pikku-state.ts +1 -1
  64. package/src/services/credential-service.ts +44 -0
  65. package/src/services/credential-wire-service.ts +44 -0
  66. package/src/services/index.ts +12 -0
  67. package/src/services/local-content.ts +61 -13
  68. package/src/services/local-credential-service.ts +41 -0
  69. package/src/services/local-secrets.test.ts +2 -2
  70. package/src/services/local-secrets.ts +2 -2
  71. package/src/services/typed-credential-service.ts +75 -0
  72. package/src/testing/service-tests.ts +140 -1
  73. package/src/types/core.types.ts +7 -0
  74. package/src/wirings/ai-agent/agent-dynamic-workflow.ts +378 -237
  75. package/src/wirings/ai-agent/ai-agent-prepare.ts +2 -1
  76. package/src/wirings/ai-agent/ai-agent-runner.test.ts +34 -0
  77. package/src/wirings/ai-agent/ai-agent-runner.ts +2 -1
  78. package/src/wirings/ai-agent/ai-agent-stream.ts +2 -1
  79. package/src/wirings/channel/local/local-channel-runner.ts +11 -6
  80. package/src/wirings/credential/credential.types.ts +28 -0
  81. package/src/wirings/credential/index.ts +8 -0
  82. package/src/wirings/credential/validate-credential-definitions.ts +64 -0
  83. package/src/wirings/credential/wire-credential.ts +49 -0
  84. package/src/wirings/http/http-runner.ts +11 -2
  85. package/src/wirings/http/pikku-fetch-http-request.ts +11 -1
  86. package/src/wirings/oauth2/index.ts +2 -1
  87. package/src/wirings/oauth2/oauth2-client.ts +4 -8
  88. package/src/wirings/oauth2/oauth2-routes.ts +234 -0
  89. package/src/wirings/oauth2/oauth2.types.ts +0 -27
  90. package/src/wirings/workflow/graph/graph-runner.test.ts +42 -0
  91. package/src/wirings/workflow/graph/graph-runner.ts +117 -8
  92. package/src/wirings/workflow/pikku-workflow-service.test.ts +109 -0
  93. package/src/wirings/workflow/pikku-workflow-service.ts +150 -34
  94. package/src/wirings/workflow/workflow.types.ts +4 -0
  95. package/tsconfig.tsbuildinfo +1 -1
  96. package/dist/wirings/oauth2/wire-oauth2-credential.d.ts +0 -20
  97. package/dist/wirings/oauth2/wire-oauth2-credential.js +0 -21
  98. 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 || !getSingletonServices()?.queueService;
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 || !getSingletonServices()?.queueService;
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
- this.runWorkflowJob(runId, rpcService)
183
- .catch((err) => {
184
- getSingletonServices().logger.error(`Workflow ${name} (run ${runId}) failed:`, err);
185
- })
186
- .finally(() => {
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, resume orchestrator and return
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
- if (workflowMeta?.nodes && stepName in workflowMeta.nodes) {
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
- result = await rpcService.rpcWithWire(rpcName, data, {
342
- workflowStep: {
343
- runId,
344
- stepId: stepState.stepId,
345
- attemptCount: stepState.attemptCount,
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.8",
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'
@@ -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
@@ -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 === token.value) {
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
- pikkuMiddleware(
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
- ...options,
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
- const timeoutId = setTimeout(() => {
8
- throw new RequestTimeoutError()
9
- }, timeout)
10
-
11
- await next()
12
-
13
- clearTimeout(timeoutId)
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
  )
@@ -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.for('@pikku/core/state')
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
+ }
@@ -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'