@workflow/world-local 4.1.1 → 4.1.3

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.
@@ -2,7 +2,7 @@ import path from 'node:path';
2
2
  import { HookNotFoundError } from '@workflow/errors';
3
3
  import { HookSchema } from '@workflow/world';
4
4
  import { DEFAULT_RESOLVE_DATA_OPTION } from '../config.js';
5
- import { deleteJSON, listJSONFiles, paginatedFileSystemQuery, readJSON, readJSONWithFallback, } from '../fs.js';
5
+ import { assertSafeEntityId, deleteJSON, listJSONFiles, paginatedFileSystemQuery, readJSON, readJSONWithFallback, } from '../fs.js';
6
6
  import { filterHookData } from './filters.js';
7
7
  import { hashToken } from './helpers.js';
8
8
  /**
@@ -24,6 +24,7 @@ export function createHooksStorage(basedir, tag) {
24
24
  return null;
25
25
  }
26
26
  async function get(hookId, params) {
27
+ assertSafeEntityId('hookId', hookId);
27
28
  const hook = await readJSONWithFallback(basedir, 'hooks', hookId, HookSchema, tag);
28
29
  if (!hook) {
29
30
  throw new HookNotFoundError(hookId);
@@ -1 +1 @@
1
- {"version":3,"file":"hooks-storage.js","sourceRoot":"","sources":["../../src/storage/hooks-storage.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAQrD,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7C,OAAO,EAAE,2BAA2B,EAAE,MAAM,cAAc,CAAC;AAC3D,OAAO,EACL,UAAU,EACV,aAAa,EACb,wBAAwB,EACxB,QAAQ,EACR,oBAAoB,GACrB,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAEzC;;;GAGG;AACH,MAAM,UAAU,kBAAkB,CAChC,OAAe,EACf,GAAY;IAEZ,sEAAsE;IACtE,KAAK,UAAU,eAAe,CAAC,KAAa;QAC1C,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QAC7C,MAAM,KAAK,GAAG,MAAM,aAAa,CAAC,QAAQ,CAAC,CAAC;QAE5C,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACzB,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,GAAG,IAAI,OAAO,CAAC,CAAC;YACrD,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,QAAQ,EAAE,UAAU,CAAC,CAAC;YAClD,IAAI,IAAI,IAAI,IAAI,CAAC,KAAK,KAAK,KAAK,EAAE,CAAC;gBACjC,OAAO,EAAE,GAAG,IAAI,EAAE,SAAS,EAAE,IAAI,CAAC,SAAS,IAAI,IAAI,EAAE,CAAC;YACxD,CAAC;QACH,CAAC;QAED,OAAO,IAAI,CAAC;IACd,CAAC;IAED,KAAK,UAAU,GAAG,CAAC,MAAc,EAAE,MAAsB;QACvD,MAAM,IAAI,GAAG,MAAM,oBAAoB,CACrC,OAAO,EACP,OAAO,EACP,MAAM,EACN,UAAU,EACV,GAAG,CACJ,CAAC;QACF,IAAI,CAAC,IAAI,EAAE,CAAC;YACV,MAAM,IAAI,iBAAiB,CAAC,MAAM,CAAC,CAAC;QACtC,CAAC;QACD,MAAM,WAAW,GAAG,MAAM,EAAE,WAAW,IAAI,2BAA2B,CAAC;QACvE,OAAO,cAAc,CACnB,EAAE,GAAG,IAAI,EAAE,SAAS,EAAE,IAAI,CAAC,SAAS,IAAI,IAAI,EAAE,EAC9C,WAAW,CACZ,CAAC;IACJ,CAAC;IAED,KAAK,UAAU,UAAU,CAAC,KAAa;QACrC,MAAM,IAAI,GAAG,MAAM,eAAe,CAAC,KAAK,CAAC,CAAC;QAC1C,IAAI,CAAC,IAAI,EAAE,CAAC;YACV,MAAM,IAAI,iBAAiB,CAAC,KAAK,CAAC,CAAC;QACrC,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC;IAED,KAAK,UAAU,IAAI,CACjB,MAAuB;QAEvB,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QAC7C,MAAM,WAAW,GAAG,MAAM,CAAC,WAAW,IAAI,2BAA2B,CAAC;QAEtE,MAAM,MAAM,GAAG,MAAM,wBAAwB,CAAC;YAC5C,SAAS,EAAE,QAAQ;YACnB,MAAM,EAAE,UAAU;YAClB,SAAS,EAAE,MAAM,CAAC,UAAU,EAAE,SAAS,IAAI,KAAK;YAChD,KAAK,EAAE,MAAM,CAAC,UAAU,EAAE,KAAK;YAC/B,MAAM,EAAE,MAAM,CAAC,UAAU,EAAE,MAAM;YACjC,UAAU,EAAE,SAAS,EAAE,2DAA2D;YAClF,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE;gBACf,8BAA8B;gBAC9B,IAAI,MAAM,CAAC,KAAK,IAAI,IAAI,CAAC,KAAK,KAAK,MAAM,CAAC,KAAK,EAAE,CAAC;oBAChD,OAAO,KAAK,CAAC;gBACf,CAAC;gBACD,OAAO,IAAI,CAAC;YACd,CAAC;YACD,YAAY,EAAE,GAAG,EAAE;gBACjB,oEAAoE;gBACpE,kEAAkE;gBAClE,kEAAkE;gBAClE,OAAO,IAAI,CAAC;YACd,CAAC;YACD,KAAK,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM;SAC7B,CAAC,CAAC;QAEH,sCAAsC;QACtC,OAAO;YACL,GAAG,MAAM;YACT,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,cAAc,CAAC,IAAI,EAAE,WAAW,CAAC,CAAC;SACnE,CAAC;IACJ,CAAC;IAED,OAAO,EAAE,GAAG,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC;AACnC,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,oBAAoB,CACxC,OAAe,EACf,KAAa;IAEb,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;IAC7C,MAAM,KAAK,GAAG,MAAM,aAAa,CAAC,QAAQ,CAAC,CAAC;IAE5C,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,GAAG,IAAI,OAAO,CAAC,CAAC;QACrD,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,QAAQ,EAAE,UAAU,CAAC,CAAC;QAClD,IAAI,IAAI,IAAI,IAAI,CAAC,KAAK,KAAK,KAAK,EAAE,CAAC;YACjC,wDAAwD;YACxD,MAAM,cAAc,GAAG,IAAI,CAAC,IAAI,CAC9B,QAAQ,EACR,QAAQ,EACR,GAAG,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAChC,CAAC;YACF,MAAM,UAAU,CAAC,cAAc,CAAC,CAAC;YACjC,MAAM,UAAU,CAAC,QAAQ,CAAC,CAAC;QAC7B,CAAC;IACH,CAAC;AACH,CAAC","sourcesContent":["import path from 'node:path';\nimport { HookNotFoundError } from '@workflow/errors';\nimport type {\n GetHookParams,\n Hook,\n ListHooksParams,\n PaginatedResponse,\n Storage,\n} from '@workflow/world';\nimport { HookSchema } from '@workflow/world';\nimport { DEFAULT_RESOLVE_DATA_OPTION } from '../config.js';\nimport {\n deleteJSON,\n listJSONFiles,\n paginatedFileSystemQuery,\n readJSON,\n readJSONWithFallback,\n} from '../fs.js';\nimport { filterHookData } from './filters.js';\nimport { hashToken } from './helpers.js';\n\n/**\n * Creates a hooks storage implementation using the filesystem.\n * Implements the Storage['hooks'] interface with hook CRUD operations.\n */\nexport function createHooksStorage(\n basedir: string,\n tag?: string\n): Storage['hooks'] {\n // Helper function to find a hook by token (shared between getByToken)\n async function findHookByToken(token: string): Promise<Hook | null> {\n const hooksDir = path.join(basedir, 'hooks');\n const files = await listJSONFiles(hooksDir);\n\n for (const file of files) {\n const hookPath = path.join(hooksDir, `${file}.json`);\n const hook = await readJSON(hookPath, HookSchema);\n if (hook && hook.token === token) {\n return { ...hook, isWebhook: hook.isWebhook ?? true };\n }\n }\n\n return null;\n }\n\n async function get(hookId: string, params?: GetHookParams): Promise<Hook> {\n const hook = await readJSONWithFallback(\n basedir,\n 'hooks',\n hookId,\n HookSchema,\n tag\n );\n if (!hook) {\n throw new HookNotFoundError(hookId);\n }\n const resolveData = params?.resolveData || DEFAULT_RESOLVE_DATA_OPTION;\n return filterHookData(\n { ...hook, isWebhook: hook.isWebhook ?? true },\n resolveData\n );\n }\n\n async function getByToken(token: string): Promise<Hook> {\n const hook = await findHookByToken(token);\n if (!hook) {\n throw new HookNotFoundError(token);\n }\n return hook;\n }\n\n async function list(\n params: ListHooksParams\n ): Promise<PaginatedResponse<Hook>> {\n const hooksDir = path.join(basedir, 'hooks');\n const resolveData = params.resolveData || DEFAULT_RESOLVE_DATA_OPTION;\n\n const result = await paginatedFileSystemQuery({\n directory: hooksDir,\n schema: HookSchema,\n sortOrder: params.pagination?.sortOrder ?? 'asc',\n limit: params.pagination?.limit,\n cursor: params.pagination?.cursor,\n filePrefix: undefined, // Hooks don't have ULIDs, so we can't optimize by filename\n filter: (hook) => {\n // Filter by runId if provided\n if (params.runId && hook.runId !== params.runId) {\n return false;\n }\n return true;\n },\n getCreatedAt: () => {\n // Hook files don't have ULID timestamps in filename, so return null\n // to skip the filename-based optimization and defer to JSON-based\n // cursor filtering which uses the actual createdAt from the file.\n return null;\n },\n getId: (hook) => hook.hookId,\n });\n\n // Transform the data after pagination\n return {\n ...result,\n data: result.data.map((hook) => filterHookData(hook, resolveData)),\n };\n }\n\n return { get, getByToken, list };\n}\n\n/**\n * Helper function to delete all hooks associated with a workflow run.\n * Called when a run reaches a terminal state.\n */\nexport async function deleteAllHooksForRun(\n basedir: string,\n runId: string\n): Promise<void> {\n const hooksDir = path.join(basedir, 'hooks');\n const files = await listJSONFiles(hooksDir);\n\n for (const file of files) {\n const hookPath = path.join(hooksDir, `${file}.json`);\n const hook = await readJSON(hookPath, HookSchema);\n if (hook && hook.runId === runId) {\n // Delete the token constraint file to free up the token\n const constraintPath = path.join(\n hooksDir,\n 'tokens',\n `${hashToken(hook.token)}.json`\n );\n await deleteJSON(constraintPath);\n await deleteJSON(hookPath);\n }\n }\n}\n"]}
1
+ {"version":3,"file":"hooks-storage.js","sourceRoot":"","sources":["../../src/storage/hooks-storage.ts"],"names":[],"mappings":"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","sourcesContent":["import path from 'node:path';\nimport { HookNotFoundError } from '@workflow/errors';\nimport type {\n GetHookParams,\n Hook,\n ListHooksParams,\n PaginatedResponse,\n Storage,\n} from '@workflow/world';\nimport { HookSchema } from '@workflow/world';\nimport { DEFAULT_RESOLVE_DATA_OPTION } from '../config.js';\nimport {\n assertSafeEntityId,\n deleteJSON,\n listJSONFiles,\n paginatedFileSystemQuery,\n readJSON,\n readJSONWithFallback,\n} from '../fs.js';\nimport { filterHookData } from './filters.js';\nimport { hashToken } from './helpers.js';\n\n/**\n * Creates a hooks storage implementation using the filesystem.\n * Implements the Storage['hooks'] interface with hook CRUD operations.\n */\nexport function createHooksStorage(\n basedir: string,\n tag?: string\n): Storage['hooks'] {\n // Helper function to find a hook by token (shared between getByToken)\n async function findHookByToken(token: string): Promise<Hook | null> {\n const hooksDir = path.join(basedir, 'hooks');\n const files = await listJSONFiles(hooksDir);\n\n for (const file of files) {\n const hookPath = path.join(hooksDir, `${file}.json`);\n const hook = await readJSON(hookPath, HookSchema);\n if (hook && hook.token === token) {\n return { ...hook, isWebhook: hook.isWebhook ?? true };\n }\n }\n\n return null;\n }\n\n async function get(hookId: string, params?: GetHookParams): Promise<Hook> {\n assertSafeEntityId('hookId', hookId);\n const hook = await readJSONWithFallback(\n basedir,\n 'hooks',\n hookId,\n HookSchema,\n tag\n );\n if (!hook) {\n throw new HookNotFoundError(hookId);\n }\n const resolveData = params?.resolveData || DEFAULT_RESOLVE_DATA_OPTION;\n return filterHookData(\n { ...hook, isWebhook: hook.isWebhook ?? true },\n resolveData\n );\n }\n\n async function getByToken(token: string): Promise<Hook> {\n const hook = await findHookByToken(token);\n if (!hook) {\n throw new HookNotFoundError(token);\n }\n return hook;\n }\n\n async function list(\n params: ListHooksParams\n ): Promise<PaginatedResponse<Hook>> {\n const hooksDir = path.join(basedir, 'hooks');\n const resolveData = params.resolveData || DEFAULT_RESOLVE_DATA_OPTION;\n\n const result = await paginatedFileSystemQuery({\n directory: hooksDir,\n schema: HookSchema,\n sortOrder: params.pagination?.sortOrder ?? 'asc',\n limit: params.pagination?.limit,\n cursor: params.pagination?.cursor,\n filePrefix: undefined, // Hooks don't have ULIDs, so we can't optimize by filename\n filter: (hook) => {\n // Filter by runId if provided\n if (params.runId && hook.runId !== params.runId) {\n return false;\n }\n return true;\n },\n getCreatedAt: () => {\n // Hook files don't have ULID timestamps in filename, so return null\n // to skip the filename-based optimization and defer to JSON-based\n // cursor filtering which uses the actual createdAt from the file.\n return null;\n },\n getId: (hook) => hook.hookId,\n });\n\n // Transform the data after pagination\n return {\n ...result,\n data: result.data.map((hook) => filterHookData(hook, resolveData)),\n };\n }\n\n return { get, getByToken, list };\n}\n\n/**\n * Helper function to delete all hooks associated with a workflow run.\n * Called when a run reaches a terminal state.\n */\nexport async function deleteAllHooksForRun(\n basedir: string,\n runId: string\n): Promise<void> {\n const hooksDir = path.join(basedir, 'hooks');\n const files = await listJSONFiles(hooksDir);\n\n for (const file of files) {\n const hookPath = path.join(hooksDir, `${file}.json`);\n const hook = await readJSON(hookPath, HookSchema);\n if (hook && hook.runId === runId) {\n // Delete the token constraint file to free up the token\n const constraintPath = path.join(\n hooksDir,\n 'tokens',\n `${hashToken(hook.token)}.json`\n );\n await deleteJSON(constraintPath);\n await deleteJSON(hookPath);\n }\n }\n}\n"]}
@@ -1,4 +1,13 @@
1
1
  import type { Storage } from '@workflow/world';
2
+ import { type LocalRunsStorage } from './runs-storage.js';
3
+ /**
4
+ * Storage shape used inside world-local: identical to `Storage`, but `runs`
5
+ * exposes the internal `fileIdFilter` option on `list()`. Structurally
6
+ * assignable to `Storage` at public boundaries (e.g., `reenqueueActiveRuns`).
7
+ */
8
+ export type LocalStorage = Omit<Storage, 'runs'> & {
9
+ runs: LocalRunsStorage;
10
+ };
2
11
  /**
3
12
  * Creates a complete storage implementation using the filesystem.
4
13
  * This is the main entry point that composes all storage implementations.
@@ -8,5 +17,5 @@ import type { Storage } from '@workflow/world';
8
17
  * @param basedir - The base directory for storing workflow data
9
18
  * @returns A complete Storage implementation with tracing
10
19
  */
11
- export declare function createStorage(basedir: string, tag?: string): Storage;
20
+ export declare function createStorage(basedir: string, tag?: string): LocalStorage;
12
21
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/storage/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAO/C;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,OAAO,CAiBpE"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/storage/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAI/C,OAAO,EAAqB,KAAK,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAG7E;;;;GAIG;AACH,MAAM,MAAM,YAAY,GAAG,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,GAAG;IAAE,IAAI,EAAE,gBAAgB,CAAA;CAAE,CAAC;AAE9E;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,YAAY,CAezE"}
@@ -14,19 +14,17 @@ import { createStepsStorage } from './steps-storage.js';
14
14
  */
15
15
  export function createStorage(basedir, tag) {
16
16
  // Create raw storage implementations
17
- const storage = {
18
- runs: createRunsStorage(basedir, tag),
19
- steps: createStepsStorage(basedir, tag),
20
- events: createEventsStorage(basedir, tag),
21
- hooks: createHooksStorage(basedir, tag),
22
- };
17
+ const runs = createRunsStorage(basedir, tag);
18
+ const steps = createStepsStorage(basedir, tag);
19
+ const events = createEventsStorage(basedir, tag);
20
+ const hooks = createHooksStorage(basedir, tag);
23
21
  // Instrument all storage methods with tracing
24
22
  // NOTE: Span names are lowercase per OTEL semantic conventions
25
23
  return {
26
- runs: instrumentObject('world.runs', storage.runs),
27
- steps: instrumentObject('world.steps', storage.steps),
28
- events: instrumentObject('world.events', storage.events),
29
- hooks: instrumentObject('world.hooks', storage.hooks),
24
+ runs: instrumentObject('world.runs', runs),
25
+ steps: instrumentObject('world.steps', steps),
26
+ events: instrumentObject('world.events', events),
27
+ hooks: instrumentObject('world.hooks', hooks),
30
28
  };
31
29
  }
32
30
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/storage/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAC1D,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AACxD,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AACtD,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAExD;;;;;;;;GAQG;AACH,MAAM,UAAU,aAAa,CAAC,OAAe,EAAE,GAAY;IACzD,qCAAqC;IACrC,MAAM,OAAO,GAAY;QACvB,IAAI,EAAE,iBAAiB,CAAC,OAAO,EAAE,GAAG,CAAC;QACrC,KAAK,EAAE,kBAAkB,CAAC,OAAO,EAAE,GAAG,CAAC;QACvC,MAAM,EAAE,mBAAmB,CAAC,OAAO,EAAE,GAAG,CAAC;QACzC,KAAK,EAAE,kBAAkB,CAAC,OAAO,EAAE,GAAG,CAAC;KACxC,CAAC;IAEF,8CAA8C;IAC9C,+DAA+D;IAC/D,OAAO;QACL,IAAI,EAAE,gBAAgB,CAAC,YAAY,EAAE,OAAO,CAAC,IAAI,CAAC;QAClD,KAAK,EAAE,gBAAgB,CAAC,aAAa,EAAE,OAAO,CAAC,KAAK,CAAC;QACrD,MAAM,EAAE,gBAAgB,CAAC,cAAc,EAAE,OAAO,CAAC,MAAM,CAAC;QACxD,KAAK,EAAE,gBAAgB,CAAC,aAAa,EAAE,OAAO,CAAC,KAAK,CAAC;KACtD,CAAC;AACJ,CAAC","sourcesContent":["import type { Storage } from '@workflow/world';\nimport { instrumentObject } from '../instrumentObject.js';\nimport { createEventsStorage } from './events-storage.js';\nimport { createHooksStorage } from './hooks-storage.js';\nimport { createRunsStorage } from './runs-storage.js';\nimport { createStepsStorage } from './steps-storage.js';\n\n/**\n * Creates a complete storage implementation using the filesystem.\n * This is the main entry point that composes all storage implementations.\n *\n * All storage methods are instrumented with tracing spans for observability.\n *\n * @param basedir - The base directory for storing workflow data\n * @returns A complete Storage implementation with tracing\n */\nexport function createStorage(basedir: string, tag?: string): Storage {\n // Create raw storage implementations\n const storage: Storage = {\n runs: createRunsStorage(basedir, tag),\n steps: createStepsStorage(basedir, tag),\n events: createEventsStorage(basedir, tag),\n hooks: createHooksStorage(basedir, tag),\n };\n\n // Instrument all storage methods with tracing\n // NOTE: Span names are lowercase per OTEL semantic conventions\n return {\n runs: instrumentObject('world.runs', storage.runs),\n steps: instrumentObject('world.steps', storage.steps),\n events: instrumentObject('world.events', storage.events),\n hooks: instrumentObject('world.hooks', storage.hooks),\n };\n}\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/storage/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAC1D,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AACxD,OAAO,EAAE,iBAAiB,EAAyB,MAAM,mBAAmB,CAAC;AAC7E,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AASxD;;;;;;;;GAQG;AACH,MAAM,UAAU,aAAa,CAAC,OAAe,EAAE,GAAY;IACzD,qCAAqC;IACrC,MAAM,IAAI,GAAG,iBAAiB,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;IAC7C,MAAM,KAAK,GAAG,kBAAkB,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;IAC/C,MAAM,MAAM,GAAG,mBAAmB,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;IACjD,MAAM,KAAK,GAAG,kBAAkB,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;IAE/C,8CAA8C;IAC9C,+DAA+D;IAC/D,OAAO;QACL,IAAI,EAAE,gBAAgB,CAAC,YAAY,EAAE,IAAI,CAAC;QAC1C,KAAK,EAAE,gBAAgB,CAAC,aAAa,EAAE,KAAK,CAAC;QAC7C,MAAM,EAAE,gBAAgB,CAAC,cAAc,EAAE,MAAM,CAAC;QAChD,KAAK,EAAE,gBAAgB,CAAC,aAAa,EAAE,KAAK,CAAC;KAC9C,CAAC;AACJ,CAAC","sourcesContent":["import type { Storage } from '@workflow/world';\nimport { instrumentObject } from '../instrumentObject.js';\nimport { createEventsStorage } from './events-storage.js';\nimport { createHooksStorage } from './hooks-storage.js';\nimport { createRunsStorage, type LocalRunsStorage } from './runs-storage.js';\nimport { createStepsStorage } from './steps-storage.js';\n\n/**\n * Storage shape used inside world-local: identical to `Storage`, but `runs`\n * exposes the internal `fileIdFilter` option on `list()`. Structurally\n * assignable to `Storage` at public boundaries (e.g., `reenqueueActiveRuns`).\n */\nexport type LocalStorage = Omit<Storage, 'runs'> & { runs: LocalRunsStorage };\n\n/**\n * Creates a complete storage implementation using the filesystem.\n * This is the main entry point that composes all storage implementations.\n *\n * All storage methods are instrumented with tracing spans for observability.\n *\n * @param basedir - The base directory for storing workflow data\n * @returns A complete Storage implementation with tracing\n */\nexport function createStorage(basedir: string, tag?: string): LocalStorage {\n // Create raw storage implementations\n const runs = createRunsStorage(basedir, tag);\n const steps = createStepsStorage(basedir, tag);\n const events = createEventsStorage(basedir, tag);\n const hooks = createHooksStorage(basedir, tag);\n\n // Instrument all storage methods with tracing\n // NOTE: Span names are lowercase per OTEL semantic conventions\n return {\n runs: instrumentObject('world.runs', runs),\n steps: instrumentObject('world.steps', steps),\n events: instrumentObject('world.events', events),\n hooks: instrumentObject('world.hooks', hooks),\n };\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"legacy.d.ts","sourceRoot":"","sources":["../../src/storage/legacy.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAS,WAAW,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAQvE;;;;;;;GAOG;AACH,wBAAsB,iBAAiB,CACrC,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,GAAG,EACT,UAAU,EAAE,WAAW,EACvB,MAAM,CAAC,EAAE;IAAE,WAAW,CAAC,EAAE,MAAM,GAAG,KAAK,CAAA;CAAE,GACxC,OAAO,CAAC,WAAW,CAAC,CA6DtB"}
1
+ {"version":3,"file":"legacy.d.ts","sourceRoot":"","sources":["../../src/storage/legacy.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAS,WAAW,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAQvE;;;;;;;GAOG;AACH,wBAAsB,iBAAiB,CACrC,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,GAAG,EACT,UAAU,EAAE,WAAW,EACvB,MAAM,CAAC,EAAE;IAAE,WAAW,CAAC,EAAE,MAAM,GAAG,KAAK,CAAA;CAAE,GACxC,OAAO,CAAC,WAAW,CAAC,CAuEtB"}
@@ -1,7 +1,6 @@
1
- import path from 'node:path';
2
1
  import { SPEC_VERSION_CURRENT } from '@workflow/world';
3
2
  import { DEFAULT_RESOLVE_DATA_OPTION } from '../config.js';
4
- import { writeJSON } from '../fs.js';
3
+ import { assertSafeEntityId, resolveWithinBase, writeJSON } from '../fs.js';
5
4
  import { filterRunData, stripEventDataRefs } from './filters.js';
6
5
  import { monotonicUlid } from './helpers.js';
7
6
  import { deleteAllHooksForRun } from './hooks-storage.js';
@@ -14,6 +13,11 @@ import { deleteAllHooksForRun } from './hooks-storage.js';
14
13
  * - Other events: Throw error (not supported for legacy runs)
15
14
  */
16
15
  export async function handleLegacyEvent(basedir, runId, data, currentRun, params) {
16
+ // Defense in depth: events.create already validates runId before routing
17
+ // here, but handleLegacyEvent is exported and its signature doesn't
18
+ // document that invariant. Validating locally keeps the guarantee in this
19
+ // file.
20
+ assertSafeEntityId('runId', runId);
17
21
  const resolveData = params?.resolveData ?? DEFAULT_RESOLVE_DATA_OPTION;
18
22
  switch (data.eventType) {
19
23
  case 'run_cancelled': {
@@ -35,7 +39,7 @@ export async function handleLegacyEvent(basedir, runId, data, currentRun, params
35
39
  completedAt: now,
36
40
  updatedAt: now,
37
41
  };
38
- const runPath = path.join(basedir, 'runs', `${runId}.json`);
42
+ const runPath = resolveWithinBase(basedir, 'runs', `${runId}.json`);
39
43
  await writeJSON(runPath, run, { overwrite: true });
40
44
  await deleteAllHooksForRun(basedir, runId);
41
45
  // Return without event (legacy behavior skips event storage)
@@ -60,7 +64,7 @@ export async function handleLegacyEvent(basedir, runId, data, currentRun, params
60
64
  specVersion: SPEC_VERSION_CURRENT,
61
65
  };
62
66
  const compositeKey = `${runId}-${eventId}`;
63
- const eventPath = path.join(basedir, 'events', `${compositeKey}.json`);
67
+ const eventPath = resolveWithinBase(basedir, 'events', `${compositeKey}.json`);
64
68
  await writeJSON(eventPath, event);
65
69
  return { event: stripEventDataRefs(event, resolveData) };
66
70
  }
@@ -1 +1 @@
1
- {"version":3,"file":"legacy.js","sourceRoot":"","sources":["../../src/storage/legacy.ts"],"names":[],"mappings":"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","sourcesContent":["import path from 'node:path';\nimport type { Event, EventResult, WorkflowRun } from '@workflow/world';\nimport { SPEC_VERSION_CURRENT } from '@workflow/world';\nimport { DEFAULT_RESOLVE_DATA_OPTION } from '../config.js';\nimport { writeJSON } from '../fs.js';\nimport { filterRunData, stripEventDataRefs } from './filters.js';\nimport { monotonicUlid } from './helpers.js';\nimport { deleteAllHooksForRun } from './hooks-storage.js';\n\n/**\n * Handle events for legacy runs (pre-event-sourcing, specVersion < 2).\n * Legacy runs use different behavior:\n * - run_cancelled: Skip event storage, directly update run\n * - wait_completed: Store event only (no entity mutation)\n * - hook_received: Store event only (hooks exist via old system, no entity mutation)\n * - Other events: Throw error (not supported for legacy runs)\n */\nexport async function handleLegacyEvent(\n basedir: string,\n runId: string,\n data: any,\n currentRun: WorkflowRun,\n params?: { resolveData?: 'none' | 'all' }\n): Promise<EventResult> {\n const resolveData = params?.resolveData ?? DEFAULT_RESOLVE_DATA_OPTION;\n\n switch (data.eventType) {\n case 'run_cancelled': {\n // Legacy: Skip event storage, directly update run to cancelled\n const now = new Date();\n const run: WorkflowRun = {\n runId: currentRun.runId,\n deploymentId: currentRun.deploymentId,\n workflowName: currentRun.workflowName,\n specVersion: currentRun.specVersion,\n executionContext: currentRun.executionContext,\n input: currentRun.input,\n createdAt: currentRun.createdAt,\n expiredAt: currentRun.expiredAt,\n startedAt: currentRun.startedAt,\n status: 'cancelled',\n output: undefined,\n error: undefined,\n completedAt: now,\n updatedAt: now,\n };\n const runPath = path.join(basedir, 'runs', `${runId}.json`);\n await writeJSON(runPath, run, { overwrite: true });\n await deleteAllHooksForRun(basedir, runId);\n // Return without event (legacy behavior skips event storage)\n // Type assertion: EventResult expects WorkflowRun, filterRunData may return WorkflowRunWithoutData\n return {\n event: undefined,\n run: filterRunData(run, resolveData) as WorkflowRun,\n };\n }\n\n case 'wait_completed':\n case 'hook_received': {\n // Legacy: Store event only (no entity mutation)\n // - wait_completed: for replay purposes\n // - hook_received: hooks exist via old system, just record the event\n const eventId = `evnt_${monotonicUlid()}`;\n const now = new Date();\n const event: Event = {\n ...data,\n runId,\n eventId,\n createdAt: now,\n specVersion: SPEC_VERSION_CURRENT,\n };\n const compositeKey = `${runId}-${eventId}`;\n const eventPath = path.join(basedir, 'events', `${compositeKey}.json`);\n await writeJSON(eventPath, event);\n return { event: stripEventDataRefs(event, resolveData) };\n }\n\n default:\n throw new Error(\n `Event type '${data.eventType}' not supported for legacy runs ` +\n `(specVersion: ${currentRun.specVersion || 'undefined'}). ` +\n `Please upgrade 'workflow' package.`\n );\n }\n}\n"]}
1
+ {"version":3,"file":"legacy.js","sourceRoot":"","sources":["../../src/storage/legacy.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Event, EventResult, WorkflowRun } from '@workflow/world';\nimport { SPEC_VERSION_CURRENT } from '@workflow/world';\nimport { DEFAULT_RESOLVE_DATA_OPTION } from '../config.js';\nimport { assertSafeEntityId, resolveWithinBase, writeJSON } from '../fs.js';\nimport { filterRunData, stripEventDataRefs } from './filters.js';\nimport { monotonicUlid } from './helpers.js';\nimport { deleteAllHooksForRun } from './hooks-storage.js';\n\n/**\n * Handle events for legacy runs (pre-event-sourcing, specVersion < 2).\n * Legacy runs use different behavior:\n * - run_cancelled: Skip event storage, directly update run\n * - wait_completed: Store event only (no entity mutation)\n * - hook_received: Store event only (hooks exist via old system, no entity mutation)\n * - Other events: Throw error (not supported for legacy runs)\n */\nexport async function handleLegacyEvent(\n basedir: string,\n runId: string,\n data: any,\n currentRun: WorkflowRun,\n params?: { resolveData?: 'none' | 'all' }\n): Promise<EventResult> {\n // Defense in depth: events.create already validates runId before routing\n // here, but handleLegacyEvent is exported and its signature doesn't\n // document that invariant. Validating locally keeps the guarantee in this\n // file.\n assertSafeEntityId('runId', runId);\n\n const resolveData = params?.resolveData ?? DEFAULT_RESOLVE_DATA_OPTION;\n\n switch (data.eventType) {\n case 'run_cancelled': {\n // Legacy: Skip event storage, directly update run to cancelled\n const now = new Date();\n const run: WorkflowRun = {\n runId: currentRun.runId,\n deploymentId: currentRun.deploymentId,\n workflowName: currentRun.workflowName,\n specVersion: currentRun.specVersion,\n executionContext: currentRun.executionContext,\n input: currentRun.input,\n createdAt: currentRun.createdAt,\n expiredAt: currentRun.expiredAt,\n startedAt: currentRun.startedAt,\n status: 'cancelled',\n output: undefined,\n error: undefined,\n completedAt: now,\n updatedAt: now,\n };\n const runPath = resolveWithinBase(basedir, 'runs', `${runId}.json`);\n await writeJSON(runPath, run, { overwrite: true });\n await deleteAllHooksForRun(basedir, runId);\n // Return without event (legacy behavior skips event storage)\n // Type assertion: EventResult expects WorkflowRun, filterRunData may return WorkflowRunWithoutData\n return {\n event: undefined,\n run: filterRunData(run, resolveData) as WorkflowRun,\n };\n }\n\n case 'wait_completed':\n case 'hook_received': {\n // Legacy: Store event only (no entity mutation)\n // - wait_completed: for replay purposes\n // - hook_received: hooks exist via old system, just record the event\n const eventId = `evnt_${monotonicUlid()}`;\n const now = new Date();\n const event: Event = {\n ...data,\n runId,\n eventId,\n createdAt: now,\n specVersion: SPEC_VERSION_CURRENT,\n };\n const compositeKey = `${runId}-${eventId}`;\n const eventPath = resolveWithinBase(\n basedir,\n 'events',\n `${compositeKey}.json`\n );\n await writeJSON(eventPath, event);\n return { event: stripEventDataRefs(event, resolveData) };\n }\n\n default:\n throw new Error(\n `Event type '${data.eventType}' not supported for legacy runs ` +\n `(specVersion: ${currentRun.specVersion || 'undefined'}). ` +\n `Please upgrade 'workflow' package.`\n );\n }\n}\n"]}
@@ -1,7 +1,29 @@
1
- import type { Storage } from '@workflow/world';
1
+ import type { ListWorkflowRunsParams, PaginatedResponse, Storage, WorkflowRun, WorkflowRunWithoutData } from '@workflow/world';
2
+ /**
3
+ * Internal extension of `ListWorkflowRunsParams` that adds a `fileIdFilter`
4
+ * for scoping queries by raw filename (e.g., by tag suffix). Kept out of the
5
+ * public `Storage['runs']['list']` surface — consumers of `@workflow/world`
6
+ * must not see this option.
7
+ */
8
+ export interface LocalListWorkflowRunsParams extends ListWorkflowRunsParams {
9
+ fileIdFilter?: (fileId: string) => boolean;
10
+ }
11
+ export interface LocalRunsStorage {
12
+ get: Storage['runs']['get'];
13
+ list: {
14
+ (params: LocalListWorkflowRunsParams & {
15
+ resolveData: 'none';
16
+ }): Promise<PaginatedResponse<WorkflowRunWithoutData>>;
17
+ (params?: LocalListWorkflowRunsParams & {
18
+ resolveData?: 'all';
19
+ }): Promise<PaginatedResponse<WorkflowRun>>;
20
+ (params?: LocalListWorkflowRunsParams): Promise<PaginatedResponse<WorkflowRun | WorkflowRunWithoutData>>;
21
+ };
22
+ }
2
23
  /**
3
24
  * Creates the runs storage implementation using the filesystem.
4
- * Implements the Storage['runs'] interface with get and list operations.
25
+ * Implements the Storage['runs'] interface with get and list operations,
26
+ * plus an internal `fileIdFilter` on `list` for tag-scoped recovery queries.
5
27
  */
6
- export declare function createRunsStorage(basedir: string, tag?: string): Storage['runs'];
28
+ export declare function createRunsStorage(basedir: string, tag?: string): LocalRunsStorage;
7
29
  //# sourceMappingURL=runs-storage.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"runs-storage.d.ts","sourceRoot":"","sources":["../../src/storage/runs-storage.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAA0B,MAAM,iBAAiB,CAAC;AAOvE;;;GAGG;AACH,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,MAAM,EACf,GAAG,CAAC,EAAE,MAAM,GACX,OAAO,CAAC,MAAM,CAAC,CAwDjB"}
1
+ {"version":3,"file":"runs-storage.d.ts","sourceRoot":"","sources":["../../src/storage/runs-storage.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,sBAAsB,EACtB,iBAAiB,EACjB,OAAO,EACP,WAAW,EACX,sBAAsB,EACvB,MAAM,iBAAiB,CAAC;AAWzB;;;;;GAKG;AACH,MAAM,WAAW,2BAA4B,SAAQ,sBAAsB;IACzE,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC;CAC5C;AAED,MAAM,WAAW,gBAAgB;IAC/B,GAAG,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC,KAAK,CAAC,CAAC;IAC5B,IAAI,EAAE;QACJ,CACE,MAAM,EAAE,2BAA2B,GAAG;YAAE,WAAW,EAAE,MAAM,CAAA;SAAE,GAC5D,OAAO,CAAC,iBAAiB,CAAC,sBAAsB,CAAC,CAAC,CAAC;QACtD,CACE,MAAM,CAAC,EAAE,2BAA2B,GAAG;YAAE,WAAW,CAAC,EAAE,KAAK,CAAA;SAAE,GAC7D,OAAO,CAAC,iBAAiB,CAAC,WAAW,CAAC,CAAC,CAAC;QAC3C,CACE,MAAM,CAAC,EAAE,2BAA2B,GACnC,OAAO,CAAC,iBAAiB,CAAC,WAAW,GAAG,sBAAsB,CAAC,CAAC,CAAC;KACrE,CAAC;CACH;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,MAAM,EACf,GAAG,CAAC,EAAE,MAAM,GACX,gBAAgB,CA0DlB"}
@@ -2,16 +2,18 @@ import path from 'node:path';
2
2
  import { WorkflowRunNotFoundError } from '@workflow/errors';
3
3
  import { WorkflowRunSchema } from '@workflow/world';
4
4
  import { DEFAULT_RESOLVE_DATA_OPTION } from '../config.js';
5
- import { paginatedFileSystemQuery, readJSONWithFallback } from '../fs.js';
5
+ import { assertSafeEntityId, paginatedFileSystemQuery, readJSONWithFallback, } from '../fs.js';
6
6
  import { filterRunData } from './filters.js';
7
7
  import { getObjectCreatedAt } from './helpers.js';
8
8
  /**
9
9
  * Creates the runs storage implementation using the filesystem.
10
- * Implements the Storage['runs'] interface with get and list operations.
10
+ * Implements the Storage['runs'] interface with get and list operations,
11
+ * plus an internal `fileIdFilter` on `list` for tag-scoped recovery queries.
11
12
  */
12
13
  export function createRunsStorage(basedir, tag) {
13
14
  return {
14
15
  get: (async (id, params) => {
16
+ assertSafeEntityId('runId', id);
15
17
  const run = await readJSONWithFallback(basedir, 'runs', id, WorkflowRunSchema, tag);
16
18
  if (!run) {
17
19
  throw new WorkflowRunNotFoundError(id);
@@ -24,6 +26,7 @@ export function createRunsStorage(basedir, tag) {
24
26
  const result = await paginatedFileSystemQuery({
25
27
  directory: path.join(basedir, 'runs'),
26
28
  schema: WorkflowRunSchema,
29
+ fileIdFilter: params?.fileIdFilter,
27
30
  filter: (run) => {
28
31
  if (params?.workflowName &&
29
32
  run.workflowName !== params.workflowName) {
@@ -1 +1 @@
1
- {"version":3,"file":"runs-storage.js","sourceRoot":"","sources":["../../src/storage/runs-storage.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,wBAAwB,EAAE,MAAM,kBAAkB,CAAC;AAE5D,OAAO,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACpD,OAAO,EAAE,2BAA2B,EAAE,MAAM,cAAc,CAAC;AAC3D,OAAO,EAAE,wBAAwB,EAAE,oBAAoB,EAAE,MAAM,UAAU,CAAC;AAC1E,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAC7C,OAAO,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAElD;;;GAGG;AACH,MAAM,UAAU,iBAAiB,CAC/B,OAAe,EACf,GAAY;IAEZ,OAAO;QACL,GAAG,EAAE,CAAC,KAAK,EAAE,EAAU,EAAE,MAAY,EAAE,EAAE;YACvC,MAAM,GAAG,GAAG,MAAM,oBAAoB,CACpC,OAAO,EACP,MAAM,EACN,EAAE,EACF,iBAAiB,EACjB,GAAG,CACJ,CAAC;YACF,IAAI,CAAC,GAAG,EAAE,CAAC;gBACT,MAAM,IAAI,wBAAwB,CAAC,EAAE,CAAC,CAAC;YACzC,CAAC;YACD,MAAM,WAAW,GAAG,MAAM,EAAE,WAAW,IAAI,2BAA2B,CAAC;YACvE,OAAO,aAAa,CAAC,GAAG,EAAE,WAAW,CAAC,CAAC;QACzC,CAAC,CAA2B;QAE5B,IAAI,EAAE,CAAC,KAAK,EAAE,MAAY,EAAE,EAAE;YAC5B,MAAM,WAAW,GAAG,MAAM,EAAE,WAAW,IAAI,2BAA2B,CAAC;YACvE,MAAM,MAAM,GAAG,MAAM,wBAAwB,CAAC;gBAC5C,SAAS,EAAE,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC;gBACrC,MAAM,EAAE,iBAAiB;gBACzB,MAAM,EAAE,CAAC,GAAG,EAAE,EAAE;oBACd,IACE,MAAM,EAAE,YAAY;wBACpB,GAAG,CAAC,YAAY,KAAK,MAAM,CAAC,YAAY,EACxC,CAAC;wBACD,OAAO,KAAK,CAAC;oBACf,CAAC;oBACD,IAAI,MAAM,EAAE,MAAM,IAAI,GAAG,CAAC,MAAM,KAAK,MAAM,CAAC,MAAM,EAAE,CAAC;wBACnD,OAAO,KAAK,CAAC;oBACf,CAAC;oBACD,OAAO,IAAI,CAAC;gBACd,CAAC;gBACD,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,SAAS,IAAI,MAAM;gBAClD,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,KAAK;gBAChC,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM;gBAClC,YAAY,EAAE,kBAAkB,CAAC,MAAM,CAAC;gBACxC,KAAK,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,KAAK;aAC1B,CAAC,CAAC;YAEH,gEAAgE;YAChE,IAAI,WAAW,KAAK,MAAM,EAAE,CAAC;gBAC3B,OAAO;oBACL,GAAG,MAAM;oBACT,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;wBAC9B,GAAG,GAAG;wBACN,KAAK,EAAE,SAAS;wBAChB,MAAM,EAAE,SAAS;qBAClB,CAAC,CAA6B;iBAChC,CAAC;YACJ,CAAC;YAED,OAAO,MAAM,CAAC;QAChB,CAAC,CAA4B;KAC9B,CAAC;AACJ,CAAC","sourcesContent":["import path from 'node:path';\nimport { WorkflowRunNotFoundError } from '@workflow/errors';\nimport type { Storage, WorkflowRunWithoutData } from '@workflow/world';\nimport { WorkflowRunSchema } from '@workflow/world';\nimport { DEFAULT_RESOLVE_DATA_OPTION } from '../config.js';\nimport { paginatedFileSystemQuery, readJSONWithFallback } from '../fs.js';\nimport { filterRunData } from './filters.js';\nimport { getObjectCreatedAt } from './helpers.js';\n\n/**\n * Creates the runs storage implementation using the filesystem.\n * Implements the Storage['runs'] interface with get and list operations.\n */\nexport function createRunsStorage(\n basedir: string,\n tag?: string\n): Storage['runs'] {\n return {\n get: (async (id: string, params?: any) => {\n const run = await readJSONWithFallback(\n basedir,\n 'runs',\n id,\n WorkflowRunSchema,\n tag\n );\n if (!run) {\n throw new WorkflowRunNotFoundError(id);\n }\n const resolveData = params?.resolveData ?? DEFAULT_RESOLVE_DATA_OPTION;\n return filterRunData(run, resolveData);\n }) as Storage['runs']['get'],\n\n list: (async (params?: any) => {\n const resolveData = params?.resolveData ?? DEFAULT_RESOLVE_DATA_OPTION;\n const result = await paginatedFileSystemQuery({\n directory: path.join(basedir, 'runs'),\n schema: WorkflowRunSchema,\n filter: (run) => {\n if (\n params?.workflowName &&\n run.workflowName !== params.workflowName\n ) {\n return false;\n }\n if (params?.status && run.status !== params.status) {\n return false;\n }\n return true;\n },\n sortOrder: params?.pagination?.sortOrder ?? 'desc',\n limit: params?.pagination?.limit,\n cursor: params?.pagination?.cursor,\n getCreatedAt: getObjectCreatedAt('wrun'),\n getId: (run) => run.runId,\n });\n\n // If resolveData is \"none\", replace input/output with undefined\n if (resolveData === 'none') {\n return {\n ...result,\n data: result.data.map((run) => ({\n ...run,\n input: undefined,\n output: undefined,\n })) as WorkflowRunWithoutData[],\n };\n }\n\n return result;\n }) as Storage['runs']['list'],\n };\n}\n"]}
1
+ {"version":3,"file":"runs-storage.js","sourceRoot":"","sources":["../../src/storage/runs-storage.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,wBAAwB,EAAE,MAAM,kBAAkB,CAAC;AAQ5D,OAAO,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACpD,OAAO,EAAE,2BAA2B,EAAE,MAAM,cAAc,CAAC;AAC3D,OAAO,EACL,kBAAkB,EAClB,wBAAwB,EACxB,oBAAoB,GACrB,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAC7C,OAAO,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AA2BlD;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAC/B,OAAe,EACf,GAAY;IAEZ,OAAO;QACL,GAAG,EAAE,CAAC,KAAK,EAAE,EAAU,EAAE,MAAY,EAAE,EAAE;YACvC,kBAAkB,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;YAChC,MAAM,GAAG,GAAG,MAAM,oBAAoB,CACpC,OAAO,EACP,MAAM,EACN,EAAE,EACF,iBAAiB,EACjB,GAAG,CACJ,CAAC;YACF,IAAI,CAAC,GAAG,EAAE,CAAC;gBACT,MAAM,IAAI,wBAAwB,CAAC,EAAE,CAAC,CAAC;YACzC,CAAC;YACD,MAAM,WAAW,GAAG,MAAM,EAAE,WAAW,IAAI,2BAA2B,CAAC;YACvE,OAAO,aAAa,CAAC,GAAG,EAAE,WAAW,CAAC,CAAC;QACzC,CAAC,CAA2B;QAE5B,IAAI,EAAE,CAAC,KAAK,EAAE,MAAoC,EAAE,EAAE;YACpD,MAAM,WAAW,GAAG,MAAM,EAAE,WAAW,IAAI,2BAA2B,CAAC;YACvE,MAAM,MAAM,GAAG,MAAM,wBAAwB,CAAC;gBAC5C,SAAS,EAAE,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC;gBACrC,MAAM,EAAE,iBAAiB;gBACzB,YAAY,EAAE,MAAM,EAAE,YAAY;gBAClC,MAAM,EAAE,CAAC,GAAG,EAAE,EAAE;oBACd,IACE,MAAM,EAAE,YAAY;wBACpB,GAAG,CAAC,YAAY,KAAK,MAAM,CAAC,YAAY,EACxC,CAAC;wBACD,OAAO,KAAK,CAAC;oBACf,CAAC;oBACD,IAAI,MAAM,EAAE,MAAM,IAAI,GAAG,CAAC,MAAM,KAAK,MAAM,CAAC,MAAM,EAAE,CAAC;wBACnD,OAAO,KAAK,CAAC;oBACf,CAAC;oBACD,OAAO,IAAI,CAAC;gBACd,CAAC;gBACD,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,SAAS,IAAI,MAAM;gBAClD,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,KAAK;gBAChC,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM;gBAClC,YAAY,EAAE,kBAAkB,CAAC,MAAM,CAAC;gBACxC,KAAK,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,KAAK;aAC1B,CAAC,CAAC;YAEH,gEAAgE;YAChE,IAAI,WAAW,KAAK,MAAM,EAAE,CAAC;gBAC3B,OAAO;oBACL,GAAG,MAAM;oBACT,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;wBAC9B,GAAG,GAAG;wBACN,KAAK,EAAE,SAAS;wBAChB,MAAM,EAAE,SAAS;qBAClB,CAAC,CAA6B;iBAChC,CAAC;YACJ,CAAC;YAED,OAAO,MAAM,CAAC;QAChB,CAAC,CAA6B;KAC/B,CAAC;AACJ,CAAC","sourcesContent":["import path from 'node:path';\nimport { WorkflowRunNotFoundError } from '@workflow/errors';\nimport type {\n ListWorkflowRunsParams,\n PaginatedResponse,\n Storage,\n WorkflowRun,\n WorkflowRunWithoutData,\n} from '@workflow/world';\nimport { WorkflowRunSchema } from '@workflow/world';\nimport { DEFAULT_RESOLVE_DATA_OPTION } from '../config.js';\nimport {\n assertSafeEntityId,\n paginatedFileSystemQuery,\n readJSONWithFallback,\n} from '../fs.js';\nimport { filterRunData } from './filters.js';\nimport { getObjectCreatedAt } from './helpers.js';\n\n/**\n * Internal extension of `ListWorkflowRunsParams` that adds a `fileIdFilter`\n * for scoping queries by raw filename (e.g., by tag suffix). Kept out of the\n * public `Storage['runs']['list']` surface — consumers of `@workflow/world`\n * must not see this option.\n */\nexport interface LocalListWorkflowRunsParams extends ListWorkflowRunsParams {\n fileIdFilter?: (fileId: string) => boolean;\n}\n\nexport interface LocalRunsStorage {\n get: Storage['runs']['get'];\n list: {\n (\n params: LocalListWorkflowRunsParams & { resolveData: 'none' }\n ): Promise<PaginatedResponse<WorkflowRunWithoutData>>;\n (\n params?: LocalListWorkflowRunsParams & { resolveData?: 'all' }\n ): Promise<PaginatedResponse<WorkflowRun>>;\n (\n params?: LocalListWorkflowRunsParams\n ): Promise<PaginatedResponse<WorkflowRun | WorkflowRunWithoutData>>;\n };\n}\n\n/**\n * Creates the runs storage implementation using the filesystem.\n * Implements the Storage['runs'] interface with get and list operations,\n * plus an internal `fileIdFilter` on `list` for tag-scoped recovery queries.\n */\nexport function createRunsStorage(\n basedir: string,\n tag?: string\n): LocalRunsStorage {\n return {\n get: (async (id: string, params?: any) => {\n assertSafeEntityId('runId', id);\n const run = await readJSONWithFallback(\n basedir,\n 'runs',\n id,\n WorkflowRunSchema,\n tag\n );\n if (!run) {\n throw new WorkflowRunNotFoundError(id);\n }\n const resolveData = params?.resolveData ?? DEFAULT_RESOLVE_DATA_OPTION;\n return filterRunData(run, resolveData);\n }) as Storage['runs']['get'],\n\n list: (async (params?: LocalListWorkflowRunsParams) => {\n const resolveData = params?.resolveData ?? DEFAULT_RESOLVE_DATA_OPTION;\n const result = await paginatedFileSystemQuery({\n directory: path.join(basedir, 'runs'),\n schema: WorkflowRunSchema,\n fileIdFilter: params?.fileIdFilter,\n filter: (run) => {\n if (\n params?.workflowName &&\n run.workflowName !== params.workflowName\n ) {\n return false;\n }\n if (params?.status && run.status !== params.status) {\n return false;\n }\n return true;\n },\n sortOrder: params?.pagination?.sortOrder ?? 'desc',\n limit: params?.pagination?.limit,\n cursor: params?.pagination?.cursor,\n getCreatedAt: getObjectCreatedAt('wrun'),\n getId: (run) => run.runId,\n });\n\n // If resolveData is \"none\", replace input/output with undefined\n if (resolveData === 'none') {\n return {\n ...result,\n data: result.data.map((run) => ({\n ...run,\n input: undefined,\n output: undefined,\n })) as WorkflowRunWithoutData[],\n };\n }\n\n return result;\n }) as LocalRunsStorage['list'],\n };\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"steps-storage.d.ts","sourceRoot":"","sources":["../../src/storage/steps-storage.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAmB,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAYhE;;;GAGG;AACH,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,MAAM,EACf,GAAG,CAAC,EAAE,MAAM,GACX,OAAO,CAAC,OAAO,CAAC,CAwDlB"}
1
+ {"version":3,"file":"steps-storage.d.ts","sourceRoot":"","sources":["../../src/storage/steps-storage.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAmB,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAahE;;;GAGG;AACH,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,MAAM,EACf,GAAG,CAAC,EAAE,MAAM,GACX,OAAO,CAAC,OAAO,CAAC,CA4DlB"}
@@ -1,7 +1,7 @@
1
1
  import path from 'node:path';
2
2
  import { StepSchema } from '@workflow/world';
3
3
  import { DEFAULT_RESOLVE_DATA_OPTION } from '../config.js';
4
- import { listJSONFiles, paginatedFileSystemQuery, readJSONWithFallback, stripTag, } from '../fs.js';
4
+ import { assertSafeEntityId, listJSONFiles, paginatedFileSystemQuery, readJSONWithFallback, stripTag, } from '../fs.js';
5
5
  import { filterStepData } from './filters.js';
6
6
  import { getObjectCreatedAt } from './helpers.js';
7
7
  /**
@@ -11,6 +11,7 @@ import { getObjectCreatedAt } from './helpers.js';
11
11
  export function createStepsStorage(basedir, tag) {
12
12
  return {
13
13
  get: (async (runId, stepId, params) => {
14
+ assertSafeEntityId('stepId', stepId);
14
15
  if (!runId) {
15
16
  const fileIds = await listJSONFiles(path.join(basedir, 'steps'));
16
17
  const fileId = fileIds.find((fid) => stripTag(fid).endsWith(`-${stepId}`));
@@ -19,6 +20,9 @@ export function createStepsStorage(basedir, tag) {
19
20
  }
20
21
  runId = stripTag(fileId).split('-')[0];
21
22
  }
23
+ else {
24
+ assertSafeEntityId('runId', runId);
25
+ }
22
26
  const compositeKey = `${runId}-${stepId}`;
23
27
  const step = await readJSONWithFallback(basedir, 'steps', compositeKey, StepSchema, tag);
24
28
  if (!step) {
@@ -28,6 +32,7 @@ export function createStepsStorage(basedir, tag) {
28
32
  return filterStepData(step, resolveData);
29
33
  }),
30
34
  list: (async (params) => {
35
+ assertSafeEntityId('runId', params.runId);
31
36
  const resolveData = params.resolveData ?? DEFAULT_RESOLVE_DATA_OPTION;
32
37
  const result = await paginatedFileSystemQuery({
33
38
  directory: path.join(basedir, 'steps'),
@@ -1 +1 @@
1
- {"version":3,"file":"steps-storage.js","sourceRoot":"","sources":["../../src/storage/steps-storage.ts"],"names":[],"mappings":"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","sourcesContent":["import path from 'node:path';\nimport type { StepWithoutData, Storage } from '@workflow/world';\nimport { StepSchema } from '@workflow/world';\nimport { DEFAULT_RESOLVE_DATA_OPTION } from '../config.js';\nimport {\n listJSONFiles,\n paginatedFileSystemQuery,\n readJSONWithFallback,\n stripTag,\n} from '../fs.js';\nimport { filterStepData } from './filters.js';\nimport { getObjectCreatedAt } from './helpers.js';\n\n/**\n * Creates the steps storage implementation using the filesystem.\n * Implements the Storage['steps'] interface with get and list operations.\n */\nexport function createStepsStorage(\n basedir: string,\n tag?: string\n): Storage['steps'] {\n return {\n get: (async (runId: string | undefined, stepId: string, params?: any) => {\n if (!runId) {\n const fileIds = await listJSONFiles(path.join(basedir, 'steps'));\n const fileId = fileIds.find((fid) =>\n stripTag(fid).endsWith(`-${stepId}`)\n );\n if (!fileId) {\n throw new Error(`Step ${stepId} not found`);\n }\n runId = stripTag(fileId).split('-')[0];\n }\n const compositeKey = `${runId}-${stepId}`;\n const step = await readJSONWithFallback(\n basedir,\n 'steps',\n compositeKey,\n StepSchema,\n tag\n );\n if (!step) {\n throw new Error(`Step ${stepId} in run ${runId} not found`);\n }\n const resolveData = params?.resolveData ?? DEFAULT_RESOLVE_DATA_OPTION;\n return filterStepData(step, resolveData);\n }) as Storage['steps']['get'],\n\n list: (async (params: any) => {\n const resolveData = params.resolveData ?? DEFAULT_RESOLVE_DATA_OPTION;\n const result = await paginatedFileSystemQuery({\n directory: path.join(basedir, 'steps'),\n schema: StepSchema,\n filePrefix: `${params.runId}-`,\n sortOrder: params.pagination?.sortOrder ?? 'desc',\n limit: params.pagination?.limit,\n cursor: params.pagination?.cursor,\n getCreatedAt: getObjectCreatedAt('step'),\n getId: (step) => step.stepId,\n });\n\n // If resolveData is \"none\", replace input/output with undefined\n if (resolveData === 'none') {\n return {\n ...result,\n data: result.data.map((step) => ({\n ...step,\n input: undefined,\n output: undefined,\n })) as StepWithoutData[],\n };\n }\n\n return result;\n }) as Storage['steps']['list'],\n };\n}\n"]}
1
+ {"version":3,"file":"steps-storage.js","sourceRoot":"","sources":["../../src/storage/steps-storage.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7C,OAAO,EAAE,2BAA2B,EAAE,MAAM,cAAc,CAAC;AAC3D,OAAO,EACL,kBAAkB,EAClB,aAAa,EACb,wBAAwB,EACxB,oBAAoB,EACpB,QAAQ,GACT,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAElD;;;GAGG;AACH,MAAM,UAAU,kBAAkB,CAChC,OAAe,EACf,GAAY;IAEZ,OAAO;QACL,GAAG,EAAE,CAAC,KAAK,EAAE,KAAyB,EAAE,MAAc,EAAE,MAAY,EAAE,EAAE;YACtE,kBAAkB,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;YACrC,IAAI,CAAC,KAAK,EAAE,CAAC;gBACX,MAAM,OAAO,GAAG,MAAM,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC;gBACjE,MAAM,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAClC,QAAQ,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAC,IAAI,MAAM,EAAE,CAAC,CACrC,CAAC;gBACF,IAAI,CAAC,MAAM,EAAE,CAAC;oBACZ,MAAM,IAAI,KAAK,CAAC,QAAQ,MAAM,YAAY,CAAC,CAAC;gBAC9C,CAAC;gBACD,KAAK,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;YACzC,CAAC;iBAAM,CAAC;gBACN,kBAAkB,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;YACrC,CAAC;YACD,MAAM,YAAY,GAAG,GAAG,KAAK,IAAI,MAAM,EAAE,CAAC;YAC1C,MAAM,IAAI,GAAG,MAAM,oBAAoB,CACrC,OAAO,EACP,OAAO,EACP,YAAY,EACZ,UAAU,EACV,GAAG,CACJ,CAAC;YACF,IAAI,CAAC,IAAI,EAAE,CAAC;gBACV,MAAM,IAAI,KAAK,CAAC,QAAQ,MAAM,WAAW,KAAK,YAAY,CAAC,CAAC;YAC9D,CAAC;YACD,MAAM,WAAW,GAAG,MAAM,EAAE,WAAW,IAAI,2BAA2B,CAAC;YACvE,OAAO,cAAc,CAAC,IAAI,EAAE,WAAW,CAAC,CAAC;QAC3C,CAAC,CAA4B;QAE7B,IAAI,EAAE,CAAC,KAAK,EAAE,MAAW,EAAE,EAAE;YAC3B,kBAAkB,CAAC,OAAO,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC;YAC1C,MAAM,WAAW,GAAG,MAAM,CAAC,WAAW,IAAI,2BAA2B,CAAC;YACtE,MAAM,MAAM,GAAG,MAAM,wBAAwB,CAAC;gBAC5C,SAAS,EAAE,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,OAAO,CAAC;gBACtC,MAAM,EAAE,UAAU;gBAClB,UAAU,EAAE,GAAG,MAAM,CAAC,KAAK,GAAG;gBAC9B,SAAS,EAAE,MAAM,CAAC,UAAU,EAAE,SAAS,IAAI,MAAM;gBACjD,KAAK,EAAE,MAAM,CAAC,UAAU,EAAE,KAAK;gBAC/B,MAAM,EAAE,MAAM,CAAC,UAAU,EAAE,MAAM;gBACjC,YAAY,EAAE,kBAAkB,CAAC,MAAM,CAAC;gBACxC,KAAK,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM;aAC7B,CAAC,CAAC;YAEH,gEAAgE;YAChE,IAAI,WAAW,KAAK,MAAM,EAAE,CAAC;gBAC3B,OAAO;oBACL,GAAG,MAAM;oBACT,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;wBAC/B,GAAG,IAAI;wBACP,KAAK,EAAE,SAAS;wBAChB,MAAM,EAAE,SAAS;qBAClB,CAAC,CAAsB;iBACzB,CAAC;YACJ,CAAC;YAED,OAAO,MAAM,CAAC;QAChB,CAAC,CAA6B;KAC/B,CAAC;AACJ,CAAC","sourcesContent":["import path from 'node:path';\nimport type { StepWithoutData, Storage } from '@workflow/world';\nimport { StepSchema } from '@workflow/world';\nimport { DEFAULT_RESOLVE_DATA_OPTION } from '../config.js';\nimport {\n assertSafeEntityId,\n listJSONFiles,\n paginatedFileSystemQuery,\n readJSONWithFallback,\n stripTag,\n} from '../fs.js';\nimport { filterStepData } from './filters.js';\nimport { getObjectCreatedAt } from './helpers.js';\n\n/**\n * Creates the steps storage implementation using the filesystem.\n * Implements the Storage['steps'] interface with get and list operations.\n */\nexport function createStepsStorage(\n basedir: string,\n tag?: string\n): Storage['steps'] {\n return {\n get: (async (runId: string | undefined, stepId: string, params?: any) => {\n assertSafeEntityId('stepId', stepId);\n if (!runId) {\n const fileIds = await listJSONFiles(path.join(basedir, 'steps'));\n const fileId = fileIds.find((fid) =>\n stripTag(fid).endsWith(`-${stepId}`)\n );\n if (!fileId) {\n throw new Error(`Step ${stepId} not found`);\n }\n runId = stripTag(fileId).split('-')[0];\n } else {\n assertSafeEntityId('runId', runId);\n }\n const compositeKey = `${runId}-${stepId}`;\n const step = await readJSONWithFallback(\n basedir,\n 'steps',\n compositeKey,\n StepSchema,\n tag\n );\n if (!step) {\n throw new Error(`Step ${stepId} in run ${runId} not found`);\n }\n const resolveData = params?.resolveData ?? DEFAULT_RESOLVE_DATA_OPTION;\n return filterStepData(step, resolveData);\n }) as Storage['steps']['get'],\n\n list: (async (params: any) => {\n assertSafeEntityId('runId', params.runId);\n const resolveData = params.resolveData ?? DEFAULT_RESOLVE_DATA_OPTION;\n const result = await paginatedFileSystemQuery({\n directory: path.join(basedir, 'steps'),\n schema: StepSchema,\n filePrefix: `${params.runId}-`,\n sortOrder: params.pagination?.sortOrder ?? 'desc',\n limit: params.pagination?.limit,\n cursor: params.pagination?.cursor,\n getCreatedAt: getObjectCreatedAt('step'),\n getId: (step) => step.stepId,\n });\n\n // If resolveData is \"none\", replace input/output with undefined\n if (resolveData === 'none') {\n return {\n ...result,\n data: result.data.map((step) => ({\n ...step,\n input: undefined,\n output: undefined,\n })) as StepWithoutData[],\n };\n }\n\n return result;\n }) as Storage['steps']['list'],\n };\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"streamer.d.ts","sourceRoot":"","sources":["../src/streamer.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAGV,QAAQ,EAET,MAAM,iBAAiB,CAAC;AAqBzB;;;;;;GAMG;AACH,MAAM,WAAW,KAAK;IACpB,GAAG,EAAE,OAAO,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;CACf;AAED,wBAAgB,cAAc,CAAC,KAAK,EAAE,KAAK,uBAG1C;AAED,kEAAkE;AAClE,wBAAgB,UAAU,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAEtD;AAED,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,MAAM;;;EAKlD;AAuCD,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,QAAQ,CAojBtE"}
1
+ {"version":3,"file":"streamer.d.ts","sourceRoot":"","sources":["../src/streamer.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAGV,QAAQ,EAET,MAAM,iBAAiB,CAAC;AAsBzB;;;;;;GAMG;AACH,MAAM,WAAW,KAAK;IACpB,GAAG,EAAE,OAAO,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;CACf;AAQD,wBAAgB,cAAc,CAAC,KAAK,EAAE,KAAK,uBAG1C;AAED,kEAAkE;AAClE,wBAAgB,UAAU,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAEtD;AAED,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,MAAM;;;EAKlD;AA4DD,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,QAAQ,CAujBtE"}
package/dist/streamer.js CHANGED
@@ -1,8 +1,9 @@
1
1
  import { EventEmitter } from 'node:events';
2
+ import fs from 'node:fs/promises';
2
3
  import path from 'node:path';
3
4
  import { monotonicFactory } from 'ulid';
4
5
  import { z } from 'zod';
5
- import { listFilesByExtension, readBuffer, readJSONWithFallback, taggedPath, write, writeJSON, } from './fs.js';
6
+ import { assertSafeEntityId, readBuffer, readFirstByte, readJSONWithFallback, taggedPath, write, writeJSON, } from './fs.js';
6
7
  // Create a monotonic ULID factory that ensures ULIDs are always increasing
7
8
  // even when generated within the same millisecond
8
9
  const monotonicUlid = monotonicFactory(() => Math.random());
@@ -10,46 +11,59 @@ const monotonicUlid = monotonicFactory(() => Math.random());
10
11
  const RunStreamsSchema = z.object({
11
12
  streams: z.array(z.string()),
12
13
  });
14
+ const EOF_MARKER = 1;
15
+ function isEofByte(byte) {
16
+ return byte === EOF_MARKER;
17
+ }
13
18
  export function serializeChunk(chunk) {
14
- const eofByte = Buffer.from([chunk.eof ? 1 : 0]);
19
+ const eofByte = Buffer.from([chunk.eof ? EOF_MARKER : 0]);
15
20
  return Buffer.concat([eofByte, chunk.chunk]);
16
21
  }
17
22
  /** Check only the EOF flag byte without copying chunk payload. */
18
23
  export function isEofChunk(serialized) {
19
- return serialized[0] === 1;
24
+ return isEofByte(serialized[0]);
20
25
  }
21
26
  export function deserializeChunk(serialized) {
22
- const eof = serialized[0] === 1;
27
+ const eof = isEofChunk(serialized);
23
28
  // Create a copy instead of a view to prevent ArrayBuffer detachment
24
29
  const chunk = Buffer.from(serialized.subarray(1));
25
30
  return { eof, chunk };
26
31
  }
32
+ async function listChunkEntries(chunksDir) {
33
+ try {
34
+ return await fs.readdir(chunksDir);
35
+ }
36
+ catch (error) {
37
+ if (error.code === 'ENOENT')
38
+ return [];
39
+ throw error;
40
+ }
41
+ }
42
+ function addChunkFilesByExtension(extMap, entries, sourceExtension, fileExtension = sourceExtension, include = () => true) {
43
+ for (const entry of entries) {
44
+ if (!entry.endsWith(sourceExtension))
45
+ continue;
46
+ const file = entry.slice(0, -sourceExtension.length);
47
+ if (include(file))
48
+ extMap.set(file, fileExtension);
49
+ }
50
+ }
27
51
  /**
28
52
  * List chunk files for a stream, sorted chronologically (ULID order).
29
53
  * Returns both the sorted file names and a map of file → extension for
30
54
  * resolving the full path. Handles tagged and legacy (.json) formats.
31
55
  */
32
56
  async function listChunkFilesForStream(chunksDir, name, tag) {
33
- const listPromises = [
34
- listFilesByExtension(chunksDir, '.bin'),
35
- listFilesByExtension(chunksDir, '.json'),
36
- ];
37
- if (tag) {
38
- listPromises.push(listFilesByExtension(chunksDir, `.${tag}.bin`));
39
- }
40
- const [binFiles, jsonFiles, ...taggedResults] = await Promise.all(listPromises);
41
- const taggedBinFiles = taggedResults[0] ?? [];
57
+ // Name is used as a filename prefix below; validate it can't escape chunksDir.
58
+ assertSafeEntityId('streamName', name);
59
+ const entries = await listChunkEntries(chunksDir);
42
60
  const extMap = new Map();
43
- for (const f of jsonFiles)
44
- extMap.set(f, '.json');
45
- const tagSfx = tag ? `.${tag}` : '';
46
- for (const f of binFiles) {
47
- if (tag && f.endsWith(tagSfx))
48
- continue;
49
- extMap.set(f, '.bin');
61
+ addChunkFilesByExtension(extMap, entries, '.json');
62
+ addChunkFilesByExtension(extMap, entries, '.bin', '.bin', tag ? (file) => !file.endsWith(`.${tag}`) : undefined);
63
+ if (tag) {
64
+ const taggedExtension = `.${tag}.bin`;
65
+ addChunkFilesByExtension(extMap, entries, taggedExtension);
50
66
  }
51
- for (const f of taggedBinFiles)
52
- extMap.set(f, `.${tag}.bin`);
53
67
  const files = [...extMap.keys()]
54
68
  .filter((file) => file.startsWith(`${name}-`))
55
69
  .sort();
@@ -62,6 +76,8 @@ export function createStreamer(basedir, tag) {
62
76
  const registeredStreams = new Set();
63
77
  // Helper to record the runId <> streamId association
64
78
  async function registerStreamForRun(runId, streamName) {
79
+ assertSafeEntityId('runId', runId);
80
+ assertSafeEntityId('streamName', streamName);
65
81
  const cacheKey = `${runId}:${streamName}`;
66
82
  if (registeredStreams.has(cacheKey)) {
67
83
  return; // Already registered in this session
@@ -165,6 +181,7 @@ export function createStreamer(basedir, tag) {
165
181
  streamEmitter.emit(`close:${name}`, { streamName: name });
166
182
  },
167
183
  async listStreamsByRunId(runId) {
184
+ assertSafeEntityId('runId', runId);
168
185
  const data = await readJSONWithFallback(basedir, 'streams/runs', runId, RunStreamsSchema, tag);
169
186
  return data?.streams ?? [];
170
187
  },
@@ -193,7 +210,7 @@ export function createStreamer(basedir, tag) {
193
210
  const filePath = path.join(chunksDir, `${file}${ext}`);
194
211
  // Before the cursor: only need to check EOF (1 byte), skip content
195
212
  if (dataIndex < startIndex) {
196
- if (isEofChunk(await readBuffer(filePath))) {
213
+ if (isEofByte(await readFirstByte(filePath))) {
197
214
  streamDone = true;
198
215
  break;
199
216
  }
@@ -202,7 +219,7 @@ export function createStreamer(basedir, tag) {
202
219
  }
203
220
  // Collected enough data chunks — peek at the next file for EOF/hasMore
204
221
  if (resultChunks.length >= limit) {
205
- if (isEofChunk(await readBuffer(filePath))) {
222
+ if (isEofByte(await readFirstByte(filePath))) {
206
223
  streamDone = true;
207
224
  }
208
225
  else {
@@ -239,13 +256,12 @@ export function createStreamer(basedir, tag) {
239
256
  async getStreamInfo(name, _runId) {
240
257
  const chunksDir = path.join(basedir, 'streams', 'chunks');
241
258
  const { files: chunkFiles, extMap: fileExtMap } = await listChunkFilesForStream(chunksDir, name, tag);
242
- // Only read the first byte of each file to check EOF — no full
243
- // deserialization needed since we just need a count.
259
+ // Read only the EOF marker byte because metadata never needs payloads.
244
260
  let streamDone = false;
245
261
  let dataCount = 0;
246
262
  for (const file of chunkFiles) {
247
263
  const ext = fileExtMap.get(file) ?? '.bin';
248
- if (isEofChunk(await readBuffer(path.join(chunksDir, `${file}${ext}`)))) {
264
+ if (isEofByte(await readFirstByte(path.join(chunksDir, `${file}${ext}`)))) {
249
265
  streamDone = true;
250
266
  break;
251
267
  }
@@ -328,10 +344,7 @@ export function createStreamer(basedir, tag) {
328
344
  chunkFiles.length > 0) {
329
345
  const lastFile = chunkFiles[chunkFiles.length - 1];
330
346
  const lastExt = fileExtMap.get(lastFile) ?? '.bin';
331
- // Note: this incurs an extra disk read to check the EOF marker.
332
- // Acceptable since negative startIndex is not a hot path.
333
- const lastChunk = deserializeChunk(await readBuffer(path.join(chunksDir, `${lastFile}${lastExt}`)));
334
- if (lastChunk?.eof === true) {
347
+ if (isEofByte(await readFirstByte(path.join(chunksDir, `${lastFile}${lastExt}`)))) {
335
348
  dataChunkCount--;
336
349
  }
337
350
  }