@pikku/core 0.12.18 → 0.12.20

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 (38) hide show
  1. package/CHANGELOG.md +76 -14
  2. package/dist/function/function-runner.js +53 -3
  3. package/dist/function/functions.types.d.ts +1 -0
  4. package/dist/index.d.ts +1 -1
  5. package/dist/services/content-service.d.ts +66 -37
  6. package/dist/services/in-memory-workflow-service.d.ts +5 -2
  7. package/dist/services/in-memory-workflow-service.js +3 -1
  8. package/dist/services/index.d.ts +1 -1
  9. package/dist/services/local-content.d.ts +10 -12
  10. package/dist/services/local-content.js +46 -37
  11. package/dist/services/workflow-service.d.ts +5 -2
  12. package/dist/wirings/cli/channel/cli-channel-runner.js +4 -2
  13. package/dist/wirings/cli/cli-runner.js +4 -2
  14. package/dist/wirings/rpc/rpc-runner.d.ts +3 -2
  15. package/dist/wirings/rpc/rpc-runner.js +32 -8
  16. package/dist/wirings/workflow/graph/graph-runner.js +4 -1
  17. package/dist/wirings/workflow/index.d.ts +1 -1
  18. package/dist/wirings/workflow/pikku-workflow-service.d.ts +5 -2
  19. package/dist/wirings/workflow/pikku-workflow-service.js +6 -1
  20. package/dist/wirings/workflow/workflow.types.d.ts +14 -0
  21. package/package.json +1 -1
  22. package/src/function/function-runner.ts +68 -3
  23. package/src/function/functions.types.ts +5 -14
  24. package/src/index.ts +10 -1
  25. package/src/services/content-service.ts +79 -51
  26. package/src/services/in-memory-workflow-service.test.ts +21 -0
  27. package/src/services/in-memory-workflow-service.ts +8 -1
  28. package/src/services/index.ts +10 -1
  29. package/src/services/local-content.ts +68 -52
  30. package/src/services/workflow-service.ts +6 -1
  31. package/src/wirings/cli/channel/cli-channel-runner.ts +4 -2
  32. package/src/wirings/cli/cli-runner.ts +4 -2
  33. package/src/wirings/rpc/rpc-runner.ts +45 -18
  34. package/src/wirings/workflow/graph/graph-runner.ts +5 -1
  35. package/src/wirings/workflow/index.ts +1 -0
  36. package/src/wirings/workflow/pikku-workflow-service.ts +13 -2
  37. package/src/wirings/workflow/workflow.types.ts +15 -0
  38. package/tsconfig.tsbuildinfo +1 -1
@@ -1,10 +1,12 @@
1
1
  import { pikkuState } from '../../../pikku-state.js';
2
2
  import { generateCommandHelp, parseCLIArguments } from '../command-parser.js';
3
3
  /**
4
- * Default JSON renderer for CLI output
4
+ * Default JSON renderer for CLI output. Emits single-line NDJSON so
5
+ * the output stays machine-parseable when the CLI is run with
6
+ * `--output json`; pretty-printing here would break NDJSON consumers.
5
7
  */
6
8
  const defaultJSONRenderer = (_services, data) => {
7
- console.log(JSON.stringify(data, null, 2));
9
+ console.log(JSON.stringify(data));
8
10
  };
9
11
  /**
10
12
  * Execute a CLI program via WebSocket channel
@@ -17,10 +17,12 @@ export class CLIError extends Error {
17
17
  }
18
18
  }
19
19
  /**
20
- * Default JSON renderer for CLI output
20
+ * Default JSON renderer for CLI output. Emits single-line NDJSON so
21
+ * the output stays machine-parseable when the CLI is run with
22
+ * `--output json`; pretty-printing here would break NDJSON consumers.
21
23
  */
22
24
  const defaultJSONRenderer = (_services, data) => {
23
- console.log(JSON.stringify(data, null, 2));
25
+ console.log(JSON.stringify(data));
24
26
  };
25
27
  /**
26
28
  * Registers a CLI command tree and all its functions
@@ -18,10 +18,11 @@ export declare class ContextAwareRPCService {
18
18
  private services;
19
19
  private wire;
20
20
  private options;
21
+ private packageName;
21
22
  constructor(services: CoreServices, wire: PikkuWire, options: {
22
23
  requiresAuth?: boolean;
23
24
  sessionService?: SessionService<CoreUserSession>;
24
- });
25
+ }, packageName?: string | null);
25
26
  rpcExposed(funcName: string, data: any): Promise<any>;
26
27
  rpc<In = any, Out = any>(funcName: string, data: In): Promise<Out>;
27
28
  /**
@@ -96,7 +97,7 @@ export declare class PikkuRPCService<Services extends CoreServices, TypedRPC = P
96
97
  getContextRPCService(services: Services, wire: PikkuWire, requiresAuthOrOptions?: boolean | {
97
98
  requiresAuth?: boolean;
98
99
  sessionService?: SessionService<CoreUserSession>;
99
- } | undefined, depth?: number): TypedRPC;
100
+ } | undefined, depth?: number, packageName?: string | null): TypedRPC;
100
101
  }
101
102
  export declare const rpcService: PikkuRPCService<import("../../types/core.types.js").CoreSingletonServices<{
102
103
  logLevel?: import("../../services/logger.js").LogLevel;
@@ -39,7 +39,22 @@ export const resolveNamespace = (namespacedFunction) => {
39
39
  addonConfig: pkgConfig,
40
40
  };
41
41
  };
42
- const getPikkuFunctionName = (rpcName) => {
42
+ /**
43
+ * Resolve a bare (non-namespaced) RPC name to its pikkuFuncId, preferring
44
+ * the caller's addon package when provided. Returns the function name plus
45
+ * the package scope that resolved it (null = root), so callers can thread
46
+ * the scope into runPikkuFunc without a second lookup.
47
+ */
48
+ const resolvePikkuFunction = (rpcName, packageName = null) => {
49
+ // Addon-scoped calls: try the caller's package function meta first.
50
+ // (RPC meta only lives in root; addon functions are registered under their package.)
51
+ if (packageName) {
52
+ const pkgFunctions = pikkuState(packageName, 'function', 'meta');
53
+ const pkgMeta = pkgFunctions?.[rpcName];
54
+ if (pkgMeta) {
55
+ return { pikkuFuncId: pkgMeta.pikkuFuncId || rpcName, packageName };
56
+ }
57
+ }
43
58
  const rpc = pikkuState(null, 'rpc', 'meta');
44
59
  let rpcMeta = rpc[rpcName];
45
60
  if (!rpcMeta) {
@@ -51,17 +66,19 @@ const getPikkuFunctionName = (rpcName) => {
51
66
  if (!rpcMeta) {
52
67
  throw new RPCNotFoundError(rpcName);
53
68
  }
54
- return rpcMeta;
69
+ return { pikkuFuncId: rpcMeta, packageName: null };
55
70
  };
56
71
  // Context-aware RPC client for use within services
57
72
  export class ContextAwareRPCService {
58
73
  services;
59
74
  wire;
60
75
  options;
61
- constructor(services, wire, options) {
76
+ packageName;
77
+ constructor(services, wire, options, packageName = null) {
62
78
  this.services = services;
63
79
  this.wire = wire;
64
80
  this.options = options;
81
+ this.packageName = packageName;
65
82
  }
66
83
  async rpcExposed(funcName, data) {
67
84
  let functionMeta;
@@ -105,13 +122,18 @@ export class ContextAwareRPCService {
105
122
  // Not an addon — fall through to local lookup
106
123
  }
107
124
  }
108
- // Try local function, then fall back to deployment service (remote)
125
+ // Bare name: resolve via caller's package scope first (if any), then root.
126
+ // Note: intra-addon bare calls do NOT re-apply the addon's external
127
+ // addonConfig.auth/tags — those gates are only applied on the external
128
+ // 'namespace:func' boundary via invokeAddonFunction.
109
129
  try {
110
- return await runPikkuFunc('rpc', funcName, getPikkuFunctionName(funcName), {
130
+ const resolved = resolvePikkuFunction(funcName, this.packageName);
131
+ return await runPikkuFunc('rpc', funcName, resolved.pikkuFuncId, {
111
132
  auth: this.options.requiresAuth,
112
133
  singletonServices: this.services,
113
134
  data: () => data,
114
135
  wire: updatedWire,
136
+ packageName: resolved.packageName,
115
137
  });
116
138
  }
117
139
  catch (e) {
@@ -181,11 +203,13 @@ export class ContextAwareRPCService {
181
203
  return this.invokeAddonFunction(rpcName, data, mergedWire);
182
204
  }
183
205
  try {
184
- return await runPikkuFunc('rpc', rpcName, getPikkuFunctionName(rpcName), {
206
+ const resolved = resolvePikkuFunction(rpcName, this.packageName);
207
+ return await runPikkuFunc('rpc', rpcName, resolved.pikkuFuncId, {
185
208
  auth: this.options.requiresAuth,
186
209
  singletonServices: this.services,
187
210
  data: () => data,
188
211
  wire: mergedWire,
212
+ packageName: resolved.packageName,
189
213
  });
190
214
  }
191
215
  catch (e) {
@@ -269,12 +293,12 @@ export class ContextAwareRPCService {
269
293
  // RPC Service class for the global interface
270
294
  export class PikkuRPCService {
271
295
  // Convenience function for initializing
272
- getContextRPCService(services, wire, requiresAuthOrOptions, depth = 0) {
296
+ getContextRPCService(services, wire, requiresAuthOrOptions, depth = 0, packageName = null) {
273
297
  const options = typeof requiresAuthOrOptions === 'object' &&
274
298
  requiresAuthOrOptions !== null
275
299
  ? requiresAuthOrOptions
276
300
  : { requiresAuth: requiresAuthOrOptions };
277
- const serviceRPC = new ContextAwareRPCService(services, wire, options);
301
+ const serviceRPC = new ContextAwareRPCService(services, wire, options, packageName);
278
302
  return {
279
303
  depth,
280
304
  global: false,
@@ -580,7 +580,10 @@ export async function runWorkflowGraph(workflowService, graphName, triggerInput,
580
580
  if (!meta.graphHash) {
581
581
  throw new Error(`Workflow graph '${graphName}': missing graphHash in meta`);
582
582
  }
583
- const runId = await workflowService.createRun(graphName, triggerInput, inline ?? false, meta.graphHash, wire ?? { type: 'unknown' });
583
+ const runId = await workflowService.createRun(graphName, triggerInput, inline ?? false, meta.graphHash, wire ?? { type: 'unknown' }, {
584
+ deterministic: meta.deterministic,
585
+ plannedSteps: meta.plannedSteps,
586
+ });
584
587
  if (inline) {
585
588
  workflowService.registerInlineRun(runId);
586
589
  }
@@ -8,5 +8,5 @@ export { pikkuWorkflowGraph, type PikkuWorkflowGraphConfig, type PikkuWorkflowGr
8
8
  export { validateWorkflowWiring, computeEntryNodeIds, } from './graph/graph-validation.js';
9
9
  export { pikkuWorkflowWorkerFunc, pikkuWorkflowOrchestratorFunc, pikkuWorkflowSleeperFunc, } from './workflow-queue-workers.js';
10
10
  export type { WorkflowStepInput as WorkflowStepQueueInput, PikkuWorkflowOrchestratorInput, PikkuWorkflowSleeperInput, } from './workflow-queue-workers.js';
11
- export type { WorkflowService, WorkflowServiceConfig, WorkflowRunWire, WorkflowStatus, WorkflowVersionStatus, StepStatus, WorkflowRun, WorkflowRunStatus, StepState, WorkflowRunService, CoreWorkflow, PikkuWorkflow, ContextVariable, WorkflowContext, WorkflowsMeta, WorkflowRuntimeMeta, WorkflowsRuntimeMeta, WorkflowStepInput, WorkflowOrchestratorInput, WorkflowSleeperInput, } from './workflow.types.js';
11
+ export type { WorkflowService, WorkflowServiceConfig, WorkflowPlannedStep, WorkflowRunWire, WorkflowStatus, WorkflowVersionStatus, StepStatus, WorkflowRun, WorkflowRunStatus, StepState, WorkflowRunService, CoreWorkflow, PikkuWorkflow, ContextVariable, WorkflowContext, WorkflowsMeta, WorkflowRuntimeMeta, WorkflowsRuntimeMeta, WorkflowStepInput, WorkflowOrchestratorInput, WorkflowSleeperInput, } from './workflow.types.js';
12
12
  export type { WorkflowStepOptions, WorkflowWireDoRPC, WorkflowWireDoInline, WorkflowWireSleep, WorkflowWireSuspend, InputSource, OutputBinding, RpcStepMeta, SimpleCondition, Condition, BranchCase, BranchStepMeta, ParallelGroupStepMeta, FanoutStepMeta, ReturnStepMeta, InlineStepMeta, SleepStepMeta, CancelStepMeta, SetStepMeta, SwitchCaseMeta, SwitchStepMeta, FilterStepMeta, ArrayPredicateStepMeta, WorkflowStepMeta, WorkflowStepWire, PikkuWorkflowWire, } from './workflow.types.js';
@@ -1,5 +1,5 @@
1
1
  import type { SerializedError } from '../../types/core.types.js';
2
- import type { PikkuWorkflowWire, StepState, WorkflowRun, WorkflowRunStatus, WorkflowRunWire, WorkflowStatus, WorkflowVersionStatus, WorkflowStepOptions } from './workflow.types.js';
2
+ import type { PikkuWorkflowWire, StepState, WorkflowPlannedStep, WorkflowRun, WorkflowRunStatus, WorkflowRunWire, WorkflowStatus, WorkflowVersionStatus, WorkflowStepOptions } from './workflow.types.js';
3
3
  import type { WorkflowService } from '../../services/workflow-service.js';
4
4
  import { PikkuError } from '../../errors/error-handler.js';
5
5
  /**
@@ -62,7 +62,10 @@ export declare abstract class PikkuWorkflowService implements WorkflowService {
62
62
  */
63
63
  unregisterInlineRun(runId: string): void;
64
64
  registerWorkflowVersions(): Promise<void>;
65
- abstract createRun(workflowName: string, input: any, inline: boolean, graphHash: string, wire: WorkflowRunWire): Promise<string>;
65
+ abstract createRun(workflowName: string, input: any, inline: boolean, graphHash: string, wire: WorkflowRunWire, options?: {
66
+ deterministic?: boolean;
67
+ plannedSteps?: WorkflowPlannedStep[];
68
+ }): Promise<string>;
66
69
  /**
67
70
  * Get a workflow run by ID
68
71
  * @param id - Run ID
@@ -231,6 +231,8 @@ export class PikkuWorkflowService {
231
231
  status: run.status,
232
232
  startedAt: run.createdAt,
233
233
  completedAt: terminalStatuses.has(run.status) ? run.updatedAt : undefined,
234
+ deterministic: run.deterministic,
235
+ plannedSteps: run.plannedSteps,
234
236
  steps,
235
237
  output: run.status === 'completed' ? run.output : undefined,
236
238
  error: run.error
@@ -326,7 +328,10 @@ export class PikkuWorkflowService {
326
328
  const shouldInline = options?.inline ||
327
329
  workflowMeta.inline ||
328
330
  !getSingletonServices()?.queueService;
329
- const runId = await this.createRun(name, input, shouldInline, workflowMeta.graphHash, wire);
331
+ const runId = await this.createRun(name, input, shouldInline, workflowMeta.graphHash, wire, {
332
+ deterministic: workflowMeta.deterministic,
333
+ plannedSteps: workflowMeta.plannedSteps,
334
+ });
330
335
  if (shouldInline) {
331
336
  this.inlineRuns.add(runId);
332
337
  try {
@@ -18,6 +18,10 @@ export interface WorkflowServiceConfig {
18
18
  stepWorkerQueueName: string;
19
19
  sleeperRPCName: string;
20
20
  }
21
+ export interface WorkflowPlannedStep {
22
+ /** Human-readable step label for UI timeline */
23
+ stepName: string;
24
+ }
21
25
  /**
22
26
  * Workflow run status
23
27
  */
@@ -50,6 +54,10 @@ export interface WorkflowRun {
50
54
  inline?: boolean;
51
55
  /** Graph hash of the workflow definition at run creation time */
52
56
  graphHash?: string;
57
+ /** True when the workflow has a static, pre-computable step timeline */
58
+ deterministic?: boolean;
59
+ /** Static planned steps snapshot captured at run start */
60
+ plannedSteps?: WorkflowPlannedStep[];
53
61
  /** Wire origin info (how this run was started) */
54
62
  wire: WorkflowRunWire;
55
63
  /** Creation timestamp */
@@ -95,6 +103,8 @@ export interface WorkflowRunStatus {
95
103
  status: WorkflowStatus;
96
104
  startedAt: Date;
97
105
  completedAt?: Date;
106
+ deterministic?: boolean;
107
+ plannedSteps?: WorkflowPlannedStep[];
98
108
  steps: Array<{
99
109
  name: string;
100
110
  status: StepStatus;
@@ -211,6 +221,10 @@ export interface WorkflowRuntimeMeta {
211
221
  entryNodeIds?: string[];
212
222
  /** Hash of graph topology (nodes, edges, input mappings) */
213
223
  graphHash?: string;
224
+ /** True when the workflow has a static, pre-computable step timeline */
225
+ deterministic?: boolean;
226
+ /** Static planned steps metadata for deterministic workflows */
227
+ plannedSteps?: WorkflowPlannedStep[];
214
228
  }
215
229
  /**
216
230
  * Unified workflow runtime metadata map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pikku/core",
3
- "version": "0.12.18",
3
+ "version": "0.12.20",
4
4
  "author": "yasser.fadl@gmail.com",
5
5
  "license": "MIT",
6
6
  "module": "dist/index.js",
@@ -72,6 +72,54 @@ async function resolveSession(
72
72
  * @param parentServices - The parent/caller's singleton services (used as base)
73
73
  * @returns The package's singleton services
74
74
  */
75
+ /**
76
+ * Find the consumer-defined namespace (from wireAddon) for a given addon package.
77
+ * Returns null if the package isn't registered as an addon.
78
+ */
79
+ const findAddonNamespaceForPackage = (packageName: string): string | null => {
80
+ const addons = pikkuState(null, 'addons', 'packages')
81
+ if (!addons) return null
82
+ for (const [namespace, cfg] of addons.entries()) {
83
+ if (cfg?.package === packageName) return namespace
84
+ }
85
+ return null
86
+ }
87
+
88
+ /**
89
+ * Wrap a workflow service so that bare workflow names passed from inside an
90
+ * addon function are auto-prefixed with the addon's consumer-facing namespace.
91
+ * Without this, `runToCompletion('myWorkflow')` from inside an addon misses
92
+ * the workflow registered under the addon's package scope and throws
93
+ * WorkflowNotFoundError — forcing addons to hardcode their consumer-defined
94
+ * namespace, which couples the addon to its caller.
95
+ *
96
+ * Explicit `'ns:name'` and bare names that already exist in root meta are
97
+ * unaffected; only bare names that would otherwise miss resolution get
98
+ * prefixed.
99
+ */
100
+ const wrapWorkflowServiceForPackage = <T extends object>(
101
+ service: T,
102
+ packageName: string
103
+ ): T => {
104
+ return new Proxy(service, {
105
+ get(target, prop, receiver) {
106
+ if (prop === 'startWorkflow' || prop === 'runToCompletion') {
107
+ const original = Reflect.get(target, prop, receiver) as Function
108
+ return function (this: any, name: string, ...rest: any[]) {
109
+ if (typeof name === 'string' && !name.includes(':')) {
110
+ const namespace = findAddonNamespaceForPackage(packageName)
111
+ if (namespace) {
112
+ name = `${namespace}:${name}`
113
+ }
114
+ }
115
+ return original.call(this, name, ...rest)
116
+ }
117
+ }
118
+ return Reflect.get(target, prop, receiver)
119
+ },
120
+ })
121
+ }
122
+
75
123
  const getOrCreatePackageSingletonServices = async (
76
124
  packageName: string,
77
125
  parentServices: CoreSingletonServices
@@ -101,6 +149,18 @@ const getOrCreatePackageSingletonServices = async (
101
149
  parentServices
102
150
  )
103
151
 
152
+ // Wrap workflowService so that bare names used inside the addon's functions
153
+ // resolve to workflows registered under the addon's package scope.
154
+ if (
155
+ packageServices.workflowService &&
156
+ typeof packageServices.workflowService === 'object'
157
+ ) {
158
+ packageServices.workflowService = wrapWorkflowServiceForPackage(
159
+ packageServices.workflowService as object,
160
+ packageName
161
+ ) as typeof packageServices.workflowService
162
+ }
163
+
104
164
  // Cache the services
105
165
  pikkuState(packageName, 'package', 'singletonServices', packageServices)
106
166
 
@@ -378,11 +438,16 @@ export const runPikkuFunc = async <In = any, Out = any>(
378
438
  wireServices && Object.keys(wireServices).length > 0
379
439
  ? { ...resolvedSingletonServices, ...wireServices }
380
440
  : resolvedSingletonServices
441
+ const callerPackageName = packageName
381
442
  Object.defineProperty(resolvedWire, 'rpc', {
382
443
  get() {
383
- const rpc = rpcService.getContextRPCService(services, resolvedWire, {
384
- sessionService,
385
- })
444
+ const rpc = rpcService.getContextRPCService(
445
+ services,
446
+ resolvedWire,
447
+ { sessionService },
448
+ 0,
449
+ callerPackageName
450
+ )
386
451
  Object.defineProperty(resolvedWire, 'rpc', {
387
452
  value: rpc,
388
453
  writable: true,
@@ -74,15 +74,8 @@ export type CorePikkuFunctionSessionless<
74
74
  export type CorePikkuPermission<
75
75
  In = any,
76
76
  Services extends CoreSingletonServices = CoreServices,
77
- Wire extends PikkuWire<
78
- In,
79
- never,
80
- false,
81
- CoreUserSession,
82
- any,
83
- never,
84
- never
85
- > = PikkuWire<In, never, false, CoreUserSession, never, never, never>,
77
+ Wire extends PikkuWire<In, never, false, CoreUserSession, any, never, never> =
78
+ PikkuWire<In, never, false, CoreUserSession, never, never, never>,
86
79
  > = (services: Services, data: In, wire: Wire) => Promise<boolean>
87
80
 
88
81
  /**
@@ -269,11 +262,8 @@ export type CorePikkuFunctionConfig<
269
262
  PikkuFunction extends
270
263
  | CorePikkuFunction<any, any, any, any, any>
271
264
  | CorePikkuFunctionSessionless<any, any, any, any, any>,
272
- PikkuPermission extends CorePikkuPermission<
273
- any,
274
- any,
275
- any
276
- > = CorePikkuPermission<any>,
265
+ PikkuPermission extends CorePikkuPermission<any, any, any> =
266
+ CorePikkuPermission<any>,
277
267
  PikkuMiddleware extends CorePikkuMiddleware<any, any> = CorePikkuMiddleware<
278
268
  any,
279
269
  any
@@ -292,6 +282,7 @@ export type CorePikkuFunctionConfig<
292
282
  remote?: boolean
293
283
  mcp?: boolean
294
284
  readonly?: boolean
285
+ deploy?: 'serverless' | 'server' | 'auto'
295
286
  approvalRequired?: boolean
296
287
  approvalDescription?: any
297
288
  func: PikkuFunction
package/src/index.ts CHANGED
@@ -98,7 +98,16 @@ export type { QueueService } from './wirings/queue/queue.types.js'
98
98
  export type { JWTService } from './services/jwt-service.js'
99
99
  export type { SecretService } from './services/secret-service.js'
100
100
  export type { VariablesService } from './services/variables-service.js'
101
- export type { ContentService } from './services/content-service.js'
101
+ export type {
102
+ ContentService,
103
+ SignContentKeyArgs,
104
+ SignURLArgs,
105
+ GetUploadURLArgs,
106
+ UploadURLResult,
107
+ BucketKeyArgs,
108
+ WriteFileArgs,
109
+ CopyFileArgs,
110
+ } from './services/content-service.js'
102
111
  export type { DeploymentService } from './services/deployment-service.js'
103
112
  export type { WorkflowService } from './services/workflow-service.js'
104
113
  export type { GatewayService } from './services/gateway-service.js'
@@ -1,81 +1,109 @@
1
- export interface ContentService {
1
+ /**
2
+ * Arguments for signing a content key into a time-limited URL.
3
+ */
4
+ export interface SignContentKeyArgs<TBucket extends string = string> {
5
+ bucket: TBucket
6
+ contentKey: string
7
+ dateLessThan: Date
8
+ dateGreaterThan?: Date
9
+ }
10
+
11
+ /**
12
+ * Arguments for signing an arbitrary URL.
13
+ */
14
+ export interface SignURLArgs {
15
+ url: string
16
+ dateLessThan: Date
17
+ dateGreaterThan?: Date
18
+ }
19
+
20
+ /**
21
+ * Arguments for minting a presigned upload URL.
22
+ */
23
+ export interface GetUploadURLArgs<TBucket extends string = string> {
24
+ bucket: TBucket
25
+ fileKey: string
26
+ contentType: string
27
+ size?: number
28
+ }
29
+
30
+ /**
31
+ * Result of minting a presigned upload URL.
32
+ */
33
+ export interface UploadURLResult {
34
+ uploadUrl: string
35
+ assetKey: string
36
+ uploadHeaders?: Record<string, string>
37
+ uploadMethod?: 'PUT' | 'POST'
38
+ }
39
+
40
+ /**
41
+ * Arguments for an operation that targets a single object by key.
42
+ */
43
+ export interface BucketKeyArgs<TBucket extends string = string> {
44
+ bucket: TBucket
45
+ key: string
46
+ }
47
+
48
+ /**
49
+ * Arguments for writing a stream to storage.
50
+ */
51
+ export interface WriteFileArgs<
52
+ TBucket extends string = string,
53
+ > extends BucketKeyArgs<TBucket> {
54
+ stream: ReadableStream | NodeJS.ReadableStream
55
+ }
56
+
57
+ /**
58
+ * Arguments for copying a local file into storage.
59
+ */
60
+ export interface CopyFileArgs<
61
+ TBucket extends string = string,
62
+ > extends BucketKeyArgs<TBucket> {
63
+ fromAbsolutePath: string
64
+ }
65
+
66
+ export interface ContentService<TBucket extends string = string> {
2
67
  /**
3
68
  * Signs a content key to generate a secure, time-limited access URL.
4
- * @param contentKey - The key representing the content object.
5
- * @param dateLessThan - The expiration time for the signed URL.
6
- * @param dateGreaterThan - (Optional) Start time before which access is denied.
7
69
  */
8
- signContentKey(
9
- contentKey: string,
10
- dateLessThan: Date,
11
- dateGreaterThan?: Date
12
- ): Promise<string>
70
+ signContentKey(args: SignContentKeyArgs<TBucket>): Promise<string>
13
71
 
14
72
  /**
15
73
  * Signs an arbitrary URL to generate a secure, time-limited access URL.
16
- * @param url - The full URL that needs signing.
17
- * @param dateLessThan - The expiration time for the signed URL.
18
- * @param dateGreaterThan - (Optional) Start time before which access is denied.
19
74
  */
20
- signURL(
21
- url: string,
22
- dateLessThan: Date,
23
- dateGreaterThan?: Date
24
- ): Promise<string>
75
+ signURL(args: SignURLArgs): Promise<string>
25
76
 
26
77
  /**
27
78
  * Generates a signed URL for uploading a file directly to storage.
28
- * @param fileKey - The desired key/location of the uploaded file.
29
- * @param contentType - The MIME type of the file.
30
- * @returns A signed upload URL and the finalized asset key.
79
+ * Bucket policy (size limits, MIME allowlist) is enforced by the implementation.
31
80
  */
32
- getUploadURL(
33
- fileKey: string,
34
- contentType: string
35
- ): Promise<{
36
- uploadUrl: string
37
- assetKey: string
38
- uploadHeaders?: Record<string, string>
39
- uploadMethod?: 'PUT' | 'POST'
40
- }>
81
+ getUploadURL(args: GetUploadURLArgs<TBucket>): Promise<UploadURLResult>
41
82
 
42
83
  /**
43
84
  * Deletes a file from the storage backend.
44
- * @param fileName - The name or key of the file to delete.
45
- * @returns A boolean indicating success.
46
85
  */
47
- deleteFile(fileName: string): Promise<boolean>
86
+ deleteFile(args: BucketKeyArgs<TBucket>): Promise<boolean>
48
87
 
49
88
  /**
50
- * Uploads a file stream to storage under a specified asset key.
51
- * @param assetKey - The key where the file will be saved.
52
- * @param stream - A readable stream of the file contents.
53
- * @returns A boolean indicating success.
89
+ * Uploads a file stream to storage under the specified bucket + key.
54
90
  */
55
- writeFile(
56
- assetKey: string,
57
- stream: ReadableStream | NodeJS.ReadableStream
58
- ): Promise<boolean>
91
+ writeFile(args: WriteFileArgs<TBucket>): Promise<boolean>
59
92
 
60
93
  /**
61
- * Copies a file from a local absolute path into storage under a new asset key.
62
- * @param assetKey - The destination key.
63
- * @param fromAbsolutePath - The local absolute file path.
64
- * @returns A boolean indicating success.
94
+ * Copies a file from a local absolute path into storage.
65
95
  */
66
- copyFile(assetKey: string, fromAbsolutePath: string): Promise<boolean>
96
+ copyFile(args: CopyFileArgs<TBucket>): Promise<boolean>
67
97
 
68
98
  /**
69
99
  * Reads a file from storage as a readable stream.
70
- * @param assetKey - The key of the file to read.
71
- * @returns A readable file stream.
72
100
  */
73
- readFile(assetKey: string): Promise<ReadableStream | NodeJS.ReadableStream>
101
+ readFile(
102
+ args: BucketKeyArgs<TBucket>
103
+ ): Promise<ReadableStream | NodeJS.ReadableStream>
74
104
 
75
105
  /**
76
106
  * Reads an entire file from storage into a Buffer.
77
- * @param assetKey - The key of the file to read.
78
- * @returns The file contents as a Buffer.
79
107
  */
80
- readFileAsBuffer(assetKey: string): Promise<Buffer>
108
+ readFileAsBuffer(args: BucketKeyArgs<TBucket>): Promise<Buffer>
81
109
  }
@@ -46,6 +46,27 @@ describe('InMemoryWorkflowService', () => {
46
46
  const state = await service.getRunState(runId)
47
47
  assert.deepStrictEqual(state, {})
48
48
  })
49
+
50
+ test('should persist deterministic planned steps metadata', async () => {
51
+ const runId = await service.createRun(
52
+ 'wf',
53
+ {},
54
+ true,
55
+ 'h',
56
+ {} as any,
57
+ {
58
+ deterministic: true,
59
+ plannedSteps: [{ stepName: 'Step 1' }, { stepName: 'Step 2' }],
60
+ }
61
+ )
62
+
63
+ const run = await service.getRun(runId)
64
+ assert.strictEqual(run?.deterministic, true)
65
+ assert.deepStrictEqual(run?.plannedSteps, [
66
+ { stepName: 'Step 1' },
67
+ { stepName: 'Step 2' },
68
+ ])
69
+ })
49
70
  })
50
71
 
51
72
  describe('getRun', () => {
@@ -2,6 +2,7 @@ import { randomUUID } from 'crypto'
2
2
  import { PikkuWorkflowService } from '../wirings/workflow/pikku-workflow-service.js'
3
3
  import type { SerializedError } from '../types/core.types.js'
4
4
  import type {
5
+ WorkflowPlannedStep,
5
6
  WorkflowRun,
6
7
  WorkflowRunService,
7
8
  WorkflowRunWire,
@@ -55,7 +56,11 @@ export class InMemoryWorkflowService
55
56
  input: any,
56
57
  inline: boolean,
57
58
  graphHash: string,
58
- wire: WorkflowRunWire
59
+ wire: WorkflowRunWire,
60
+ options?: {
61
+ deterministic?: boolean
62
+ plannedSteps?: WorkflowPlannedStep[]
63
+ }
59
64
  ): Promise<string> {
60
65
  const runId = randomUUID()
61
66
  const now = new Date()
@@ -67,6 +72,8 @@ export class InMemoryWorkflowService
67
72
  input,
68
73
  inline,
69
74
  graphHash,
75
+ deterministic: options?.deterministic,
76
+ plannedSteps: options?.plannedSteps,
70
77
  wire,
71
78
  createdAt: now,
72
79
  updatedAt: now,
@@ -25,7 +25,16 @@ export { InMemoryQueueService } from './in-memory-queue-service.js'
25
25
  export { InMemoryTriggerService } from './in-memory-trigger-service.js'
26
26
  export { InMemoryAIRunStateService } from './in-memory-ai-run-state-service.js'
27
27
  export { LocalGatewayService } from './local-gateway-service.js'
28
- export type { ContentService } from './content-service.js'
28
+ export type {
29
+ ContentService,
30
+ SignContentKeyArgs,
31
+ SignURLArgs,
32
+ GetUploadURLArgs,
33
+ UploadURLResult,
34
+ BucketKeyArgs,
35
+ WriteFileArgs,
36
+ CopyFileArgs,
37
+ } from './content-service.js'
29
38
  export type { JWTService } from './jwt-service.js'
30
39
  export type { Logger } from './logger.js'
31
40
  export type { SecretService } from './secret-service.js'