@exaudeus/workrail 0.12.0 → 0.13.0

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/dist/application/services/enhanced-loop-validator.js +3 -3
  2. package/dist/application/services/step-output-decoder.d.ts +6 -0
  3. package/dist/application/services/step-output-decoder.js +49 -0
  4. package/dist/application/services/validation-engine.d.ts +9 -0
  5. package/dist/application/services/validation-engine.js +142 -18
  6. package/dist/application/services/workflow-interpreter.d.ts +1 -1
  7. package/dist/application/services/workflow-interpreter.js +147 -81
  8. package/dist/application/services/workflow-service.d.ts +2 -0
  9. package/dist/application/services/workflow-service.js +3 -3
  10. package/dist/application/use-cases/validate-step-output.d.ts +2 -0
  11. package/dist/di/container.js +19 -3
  12. package/dist/di/tokens.d.ts +3 -0
  13. package/dist/di/tokens.js +3 -0
  14. package/dist/domain/execution/state.d.ts +6 -6
  15. package/dist/domain/workflow-id-policy.d.ts +17 -0
  16. package/dist/domain/workflow-id-policy.js +57 -0
  17. package/dist/infrastructure/storage/enhanced-multi-source-workflow-storage.js +33 -6
  18. package/dist/infrastructure/storage/file-workflow-storage.js +3 -1
  19. package/dist/infrastructure/storage/schema-validating-workflow-storage.js +13 -8
  20. package/dist/manifest.json +261 -109
  21. package/dist/mcp/handlers/v2-execution.js +62 -79
  22. package/dist/mcp/handlers/v2-workflow.js +14 -9
  23. package/dist/mcp/server.js +53 -48
  24. package/dist/mcp/tool-descriptions.js +15 -15
  25. package/dist/mcp/tools.js +14 -14
  26. package/dist/mcp/types/tool-description-types.d.ts +1 -1
  27. package/dist/mcp/types/tool-description-types.js +5 -5
  28. package/dist/mcp/types.d.ts +2 -0
  29. package/dist/v2/durable-core/constants.d.ts +15 -0
  30. package/dist/v2/durable-core/constants.js +18 -0
  31. package/dist/v2/durable-core/domain/ack-advance-append-plan.d.ts +32 -0
  32. package/dist/v2/durable-core/domain/ack-advance-append-plan.js +95 -0
  33. package/dist/v2/durable-core/domain/loop-runtime.d.ts +50 -0
  34. package/dist/v2/durable-core/domain/loop-runtime.js +95 -0
  35. package/dist/v2/durable-core/domain/notes-markdown.d.ts +4 -0
  36. package/dist/v2/durable-core/domain/notes-markdown.js +46 -0
  37. package/dist/v2/durable-core/domain/outputs.d.ts +12 -0
  38. package/dist/v2/durable-core/domain/outputs.js +18 -0
  39. package/dist/v2/durable-core/schemas/execution-snapshot/execution-snapshot.v1.d.ts +113 -113
  40. package/dist/v2/durable-core/schemas/execution-snapshot/execution-snapshot.v1.js +11 -10
  41. package/dist/v2/durable-core/schemas/export-bundle/index.d.ts +7129 -0
  42. package/dist/v2/durable-core/schemas/export-bundle/index.js +82 -0
  43. package/dist/v2/durable-core/schemas/lib/decision-trace-ref.d.ts +80 -0
  44. package/dist/v2/durable-core/schemas/lib/decision-trace-ref.js +38 -0
  45. package/dist/v2/durable-core/schemas/lib/dedupe-key.d.ts +8 -0
  46. package/dist/v2/durable-core/schemas/lib/dedupe-key.js +28 -0
  47. package/dist/v2/durable-core/schemas/lib/utf8-bounded-string.d.ts +6 -0
  48. package/dist/v2/durable-core/schemas/lib/utf8-bounded-string.js +12 -0
  49. package/dist/v2/durable-core/schemas/session/events.d.ts +158 -12
  50. package/dist/v2/durable-core/schemas/session/events.js +47 -20
  51. package/dist/v2/durable-core/schemas/session/manifest.d.ts +1 -1
  52. package/dist/v2/durable-core/schemas/session/manifest.js +6 -1
  53. package/dist/v2/durable-core/schemas/session/preferences.d.ts +5 -0
  54. package/dist/v2/durable-core/schemas/session/preferences.js +6 -0
  55. package/dist/v2/durable-core/schemas/session/session-health.d.ts +3 -0
  56. package/dist/v2/durable-core/tokens/index.d.ts +0 -1
  57. package/dist/v2/durable-core/tokens/index.js +1 -4
  58. package/dist/v2/durable-core/tokens/token-codec.d.ts +3 -2
  59. package/dist/v2/durable-core/tokens/token-codec.js +12 -6
  60. package/dist/v2/durable-core/tokens/token-signer.d.ts +3 -2
  61. package/dist/v2/durable-core/tokens/token-signer.js +8 -9
  62. package/dist/v2/infra/local/base64url/index.d.ts +5 -0
  63. package/dist/v2/infra/local/base64url/index.js +48 -0
  64. package/dist/v2/infra/local/fs/index.js +8 -4
  65. package/dist/v2/infra/local/keyring/index.d.ts +5 -1
  66. package/dist/v2/infra/local/keyring/index.js +41 -32
  67. package/dist/v2/infra/local/pinned-workflow-store/index.d.ts +3 -1
  68. package/dist/v2/infra/local/pinned-workflow-store/index.js +50 -62
  69. package/dist/v2/infra/local/random-entropy/index.d.ts +4 -0
  70. package/dist/v2/infra/local/random-entropy/index.js +10 -0
  71. package/dist/v2/infra/local/session-lock/index.d.ts +3 -1
  72. package/dist/v2/infra/local/session-lock/index.js +4 -3
  73. package/dist/v2/infra/local/session-store/index.js +26 -4
  74. package/dist/v2/infra/local/snapshot-store/index.js +20 -25
  75. package/dist/v2/infra/local/time-clock/index.d.ts +5 -0
  76. package/dist/v2/infra/local/time-clock/index.js +12 -0
  77. package/dist/v2/infra/local/utf8/index.d.ts +5 -0
  78. package/dist/v2/infra/local/utf8/index.js +12 -0
  79. package/dist/v2/ports/base64url.port.d.ts +12 -0
  80. package/dist/v2/ports/base64url.port.js +2 -0
  81. package/dist/v2/ports/random-entropy.port.d.ts +3 -0
  82. package/dist/v2/ports/random-entropy.port.js +2 -0
  83. package/dist/v2/ports/time-clock.port.d.ts +4 -0
  84. package/dist/v2/ports/time-clock.port.js +2 -0
  85. package/dist/v2/ports/utf8.port.d.ts +3 -0
  86. package/dist/v2/ports/utf8.port.js +2 -0
  87. package/dist/v2/projections/node-outputs.js +28 -11
  88. package/dist/v2/projections/preferences.d.ts +1 -2
  89. package/dist/v2/projections/preferences.js +11 -4
  90. package/dist/v2/projections/run-dag.js +40 -28
  91. package/dist/v2/projections/run-status-signals.d.ts +1 -2
  92. package/dist/v2/usecases/execution-session-gate.js +33 -34
  93. package/package.json +3 -1
  94. package/spec/workflow.schema.json +2 -2
  95. package/workflows/coding-task-workflow-agentic.json +213 -50
  96. package/workflows/relocation-workflow-us.json +430 -0
  97. package/dist/v2/durable-core/tokens/base64url.d.ts +0 -7
  98. package/dist/v2/durable-core/tokens/base64url.js +0 -16
@@ -0,0 +1,6 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RiskPolicyV2Schema = exports.AutonomyV2Schema = void 0;
4
+ const zod_1 = require("zod");
5
+ exports.AutonomyV2Schema = zod_1.z.enum(['guided', 'full_auto_stop_on_user_deps', 'full_auto_never_stop']);
6
+ exports.RiskPolicyV2Schema = zod_1.z.enum(['conservative', 'balanced', 'aggressive']);
@@ -10,6 +10,9 @@ export type CorruptionReasonV2 = {
10
10
  } | {
11
11
  readonly code: 'unknown_schema_version';
12
12
  readonly message: string;
13
+ } | {
14
+ readonly code: 'schema_validation_failed';
15
+ readonly message: string;
13
16
  };
14
17
  export type SessionHealthV2 = {
15
18
  readonly kind: 'healthy';
@@ -1,4 +1,3 @@
1
- export { encodeBase64Url, decodeBase64Url } from './base64url.js';
2
1
  export { TokenPayloadV1Schema, StateTokenPayloadV1Schema, AckTokenPayloadV1Schema, CheckpointTokenPayloadV1Schema, expectedPrefixForTokenKind, } from './payloads.js';
3
2
  export type { TokenPayloadV1, StateTokenPayloadV1, AckTokenPayloadV1, CheckpointTokenPayloadV1 } from './payloads.js';
4
3
  export { encodeTokenPayloadV1, encodeUnsignedTokenV1, parseTokenV1 } from './token-codec.js';
@@ -1,9 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.asOutputId = exports.asAttemptId = exports.assertTokenScopeMatchesState = exports.verifyTokenSignatureV1 = exports.signTokenV1 = exports.parseTokenV1 = exports.encodeUnsignedTokenV1 = exports.encodeTokenPayloadV1 = exports.expectedPrefixForTokenKind = exports.CheckpointTokenPayloadV1Schema = exports.AckTokenPayloadV1Schema = exports.StateTokenPayloadV1Schema = exports.TokenPayloadV1Schema = exports.decodeBase64Url = exports.encodeBase64Url = void 0;
4
- var base64url_js_1 = require("./base64url.js");
5
- Object.defineProperty(exports, "encodeBase64Url", { enumerable: true, get: function () { return base64url_js_1.encodeBase64Url; } });
6
- Object.defineProperty(exports, "decodeBase64Url", { enumerable: true, get: function () { return base64url_js_1.decodeBase64Url; } });
3
+ exports.asOutputId = exports.asAttemptId = exports.assertTokenScopeMatchesState = exports.verifyTokenSignatureV1 = exports.signTokenV1 = exports.parseTokenV1 = exports.encodeUnsignedTokenV1 = exports.encodeTokenPayloadV1 = exports.expectedPrefixForTokenKind = exports.CheckpointTokenPayloadV1Schema = exports.AckTokenPayloadV1Schema = exports.StateTokenPayloadV1Schema = exports.TokenPayloadV1Schema = void 0;
7
4
  var payloads_js_1 = require("./payloads.js");
8
5
  Object.defineProperty(exports, "TokenPayloadV1Schema", { enumerable: true, get: function () { return payloads_js_1.TokenPayloadV1Schema; } });
9
6
  Object.defineProperty(exports, "StateTokenPayloadV1Schema", { enumerable: true, get: function () { return payloads_js_1.StateTokenPayloadV1Schema; } });
@@ -1,6 +1,7 @@
1
1
  import type { Result } from 'neverthrow';
2
2
  import type { CanonicalBytes } from '../ids/index.js';
3
3
  import type { TokenStringV1 } from '../ids/index.js';
4
+ import type { Base64UrlPortV2 } from '../../ports/base64url.port.js';
4
5
  import { type TokenPayloadV1, type TokenPrefixV1 } from './payloads.js';
5
6
  export type TokenDecodeErrorV2 = {
6
7
  readonly code: 'TOKEN_INVALID_FORMAT';
@@ -24,8 +25,8 @@ export interface ParsedTokenV1 {
24
25
  readonly payload: TokenPayloadV1;
25
26
  }
26
27
  export declare function encodeTokenPayloadV1(payload: TokenPayloadV1): Result<CanonicalBytes, TokenDecodeErrorV2>;
27
- export declare function encodeUnsignedTokenV1(payload: TokenPayloadV1): Result<{
28
+ export declare function encodeUnsignedTokenV1(payload: TokenPayloadV1, base64url: Base64UrlPortV2): Result<{
28
29
  readonly token: TokenStringV1;
29
30
  readonly payloadBytes: CanonicalBytes;
30
31
  }, TokenDecodeErrorV2>;
31
- export declare function parseTokenV1(token: string): Result<ParsedTokenV1, TokenDecodeErrorV2>;
32
+ export declare function parseTokenV1(token: string, base64url: Base64UrlPortV2): Result<ParsedTokenV1, TokenDecodeErrorV2>;
@@ -6,7 +6,6 @@ exports.parseTokenV1 = parseTokenV1;
6
6
  const neverthrow_1 = require("neverthrow");
7
7
  const jcs_js_1 = require("../canonical/jcs.js");
8
8
  const index_js_1 = require("../ids/index.js");
9
- const base64url_js_1 = require("./base64url.js");
10
9
  const payloads_js_1 = require("./payloads.js");
11
10
  function encodeTokenPayloadV1(payload) {
12
11
  return (0, jcs_js_1.toCanonicalBytes)(payload).mapErr((e) => ({
@@ -14,15 +13,15 @@ function encodeTokenPayloadV1(payload) {
14
13
  message: e.message,
15
14
  }));
16
15
  }
17
- function encodeUnsignedTokenV1(payload) {
16
+ function encodeUnsignedTokenV1(payload, base64url) {
18
17
  const bytes = encodeTokenPayloadV1(payload);
19
18
  if (bytes.isErr())
20
19
  return (0, neverthrow_1.err)(bytes.error);
21
20
  const prefix = (0, payloads_js_1.expectedPrefixForTokenKind)(payload.tokenKind);
22
- const token = `${prefix}.v1.${(0, base64url_js_1.encodeBase64Url)(bytes.value)}.`;
21
+ const token = `${prefix}.v1.${base64url.encodeBase64Url(bytes.value)}.`;
23
22
  return (0, neverthrow_1.ok)({ token: (0, index_js_1.asTokenStringV1)(token), payloadBytes: bytes.value });
24
23
  }
25
- function parseTokenV1(token) {
24
+ function parseTokenV1(token, base64url) {
26
25
  const parts = token.split('.');
27
26
  if (parts.length !== 4)
28
27
  return (0, neverthrow_1.err)({ code: 'TOKEN_INVALID_FORMAT', message: 'Expected 4 dot-separated segments' });
@@ -35,13 +34,20 @@ function parseTokenV1(token) {
35
34
  if (payloadB64.trim() === '' || sigB64.trim() === '') {
36
35
  return (0, neverthrow_1.err)({ code: 'TOKEN_INVALID_FORMAT', message: 'Missing payload or signature segment' });
37
36
  }
38
- const decoded = (0, base64url_js_1.decodeBase64Url)(payloadB64);
37
+ const decoded = base64url.decodeBase64Url(payloadB64);
39
38
  if (decoded.isErr())
40
39
  return (0, neverthrow_1.err)({ code: 'TOKEN_INVALID_FORMAT', message: decoded.error.message });
41
40
  const payloadBytes = (0, index_js_1.asCanonicalBytes)(decoded.value);
41
+ let payloadText;
42
+ try {
43
+ payloadText = new TextDecoder('utf-8', { fatal: true }).decode(payloadBytes);
44
+ }
45
+ catch {
46
+ return (0, neverthrow_1.err)({ code: 'TOKEN_INVALID_FORMAT', message: 'Payload is not valid UTF-8' });
47
+ }
42
48
  let payloadJson;
43
49
  try {
44
- payloadJson = JSON.parse(new TextDecoder().decode(payloadBytes));
50
+ payloadJson = JSON.parse(payloadText);
45
51
  }
46
52
  catch {
47
53
  return (0, neverthrow_1.err)({ code: 'TOKEN_INVALID_FORMAT', message: 'Payload is not valid JSON' });
@@ -1,6 +1,7 @@
1
1
  import type { Result } from 'neverthrow';
2
2
  import type { HmacSha256PortV2 } from '../../ports/hmac-sha256.port.js';
3
3
  import type { KeyringV1 } from '../../ports/keyring.port.js';
4
+ import type { Base64UrlPortV2 } from '../../ports/base64url.port.js';
4
5
  import type { CanonicalBytes, TokenStringV1 } from '../ids/index.js';
5
6
  import type { ParsedTokenV1, TokenDecodeErrorV2 } from './token-codec.js';
6
7
  export type TokenVerifyErrorV2 = {
@@ -10,6 +11,6 @@ export type TokenVerifyErrorV2 = {
10
11
  readonly code: 'TOKEN_INVALID_FORMAT';
11
12
  readonly message: string;
12
13
  };
13
- export declare function signTokenV1(unsignedTokenPrefix: 'st.v1.' | 'ack.v1.' | 'chk.v1.', payloadBytes: CanonicalBytes, keyring: KeyringV1, hmac: HmacSha256PortV2): Result<TokenStringV1, TokenVerifyErrorV2>;
14
- export declare function verifyTokenSignatureV1(parsed: ParsedTokenV1, keyring: KeyringV1, hmac: HmacSha256PortV2): Result<void, TokenVerifyErrorV2>;
14
+ export declare function signTokenV1(unsignedTokenPrefix: 'st.v1.' | 'ack.v1.' | 'chk.v1.', payloadBytes: CanonicalBytes, keyring: KeyringV1, hmac: HmacSha256PortV2, base64url: Base64UrlPortV2): Result<TokenStringV1, TokenVerifyErrorV2>;
15
+ export declare function verifyTokenSignatureV1(parsed: ParsedTokenV1, keyring: KeyringV1, hmac: HmacSha256PortV2, base64url: Base64UrlPortV2): Result<void, TokenVerifyErrorV2>;
15
16
  export declare function assertTokenScopeMatchesState(state: ParsedTokenV1, other: ParsedTokenV1): Result<void, TokenDecodeErrorV2>;
@@ -4,26 +4,25 @@ exports.signTokenV1 = signTokenV1;
4
4
  exports.verifyTokenSignatureV1 = verifyTokenSignatureV1;
5
5
  exports.assertTokenScopeMatchesState = assertTokenScopeMatchesState;
6
6
  const neverthrow_1 = require("neverthrow");
7
- const base64url_js_1 = require("./base64url.js");
8
7
  const index_js_1 = require("../ids/index.js");
9
- function decodeKeyBytes(keyBase64Url) {
10
- const decoded = (0, base64url_js_1.decodeBase64Url)(keyBase64Url);
8
+ function decodeKeyBytes(keyBase64Url, base64url) {
9
+ const decoded = base64url.decodeBase64Url(keyBase64Url);
11
10
  if (decoded.isErr())
12
11
  return (0, neverthrow_1.err)({ code: 'TOKEN_INVALID_FORMAT', message: 'Invalid key encoding' });
13
12
  if (decoded.value.length !== 32)
14
13
  return (0, neverthrow_1.err)({ code: 'TOKEN_INVALID_FORMAT', message: 'Invalid key length' });
15
14
  return (0, neverthrow_1.ok)(decoded.value);
16
15
  }
17
- function signTokenV1(unsignedTokenPrefix, payloadBytes, keyring, hmac) {
18
- const key = decodeKeyBytes(keyring.current.keyBase64Url);
16
+ function signTokenV1(unsignedTokenPrefix, payloadBytes, keyring, hmac, base64url) {
17
+ const key = decodeKeyBytes(keyring.current.keyBase64Url, base64url);
19
18
  if (key.isErr())
20
19
  return (0, neverthrow_1.err)(key.error);
21
20
  const sig = hmac.hmacSha256(key.value, payloadBytes);
22
- const token = `${unsignedTokenPrefix}${(0, base64url_js_1.encodeBase64Url)(payloadBytes)}.${(0, base64url_js_1.encodeBase64Url)(sig)}`;
21
+ const token = `${unsignedTokenPrefix}${base64url.encodeBase64Url(payloadBytes)}.${base64url.encodeBase64Url(sig)}`;
23
22
  return (0, neverthrow_1.ok)((0, index_js_1.asTokenStringV1)(token));
24
23
  }
25
- function verifyTokenSignatureV1(parsed, keyring, hmac) {
26
- const sigBytes = (0, base64url_js_1.decodeBase64Url)(parsed.sigBase64Url);
24
+ function verifyTokenSignatureV1(parsed, keyring, hmac, base64url) {
25
+ const sigBytes = base64url.decodeBase64Url(parsed.sigBase64Url);
27
26
  if (sigBytes.isErr())
28
27
  return (0, neverthrow_1.err)({ code: 'TOKEN_INVALID_FORMAT', message: 'Invalid signature encoding' });
29
28
  if (sigBytes.value.length !== 32)
@@ -32,7 +31,7 @@ function verifyTokenSignatureV1(parsed, keyring, hmac) {
32
31
  if (keyring.previous)
33
32
  keys.push(keyring.previous.keyBase64Url);
34
33
  for (const k of keys) {
35
- const key = decodeKeyBytes(k);
34
+ const key = decodeKeyBytes(k, base64url);
36
35
  if (key.isErr())
37
36
  continue;
38
37
  const expected = hmac.hmacSha256(key.value, parsed.payloadBytes);
@@ -0,0 +1,5 @@
1
+ import type { Base64UrlPortV2 } from '../../../ports/base64url.port.js';
2
+ export declare class NodeBase64UrlV2 implements Base64UrlPortV2 {
3
+ encodeBase64Url(bytes: Uint8Array): string;
4
+ decodeBase64Url(input: string): ReturnType<Base64UrlPortV2['decodeBase64Url']>;
5
+ }
@@ -0,0 +1,48 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.NodeBase64UrlV2 = void 0;
4
+ const neverthrow_1 = require("neverthrow");
5
+ const node_buffer_1 = require("node:buffer");
6
+ class NodeBase64UrlV2 {
7
+ encodeBase64Url(bytes) {
8
+ return node_buffer_1.Buffer.from(bytes).toString('base64url');
9
+ }
10
+ decodeBase64Url(input) {
11
+ if (input.trim() === '') {
12
+ return (0, neverthrow_1.err)({
13
+ code: 'INVALID_BASE64URL_CHARACTERS',
14
+ message: 'Invalid base64url: empty input',
15
+ });
16
+ }
17
+ if (input.includes('=')) {
18
+ return (0, neverthrow_1.err)({
19
+ code: 'INVALID_BASE64URL_PADDING',
20
+ message: 'Invalid base64url: padding is not allowed',
21
+ });
22
+ }
23
+ const base64UrlRe = /^[A-Za-z0-9_-]+$/;
24
+ if (!base64UrlRe.test(input)) {
25
+ return (0, neverthrow_1.err)({
26
+ code: 'INVALID_BASE64URL_CHARACTERS',
27
+ message: 'Invalid base64url: invalid characters',
28
+ });
29
+ }
30
+ try {
31
+ const decoded = node_buffer_1.Buffer.from(input, 'base64url');
32
+ if (decoded.toString('base64url') !== input) {
33
+ return (0, neverthrow_1.err)({
34
+ code: 'INVALID_BASE64URL_CHARACTERS',
35
+ message: 'Invalid base64url: non-canonical encoding',
36
+ });
37
+ }
38
+ return (0, neverthrow_1.ok)(new Uint8Array(decoded));
39
+ }
40
+ catch (e) {
41
+ return (0, neverthrow_1.err)({
42
+ code: 'INVALID_BASE64URL_CHARACTERS',
43
+ message: `Invalid base64url: ${e instanceof Error ? e.message : String(e)}`,
44
+ });
45
+ }
46
+ }
47
+ }
48
+ exports.NodeBase64UrlV2 = NodeBase64UrlV2;
@@ -38,9 +38,14 @@ const fs = __importStar(require("fs/promises"));
38
38
  const fsCb = __importStar(require("fs"));
39
39
  const fs_1 = require("fs");
40
40
  const neverthrow_1 = require("neverthrow");
41
+ function nodeErrorCode(e) {
42
+ if (typeof e !== 'object' || e === null)
43
+ return undefined;
44
+ const code = e.code;
45
+ return typeof code === 'string' ? code : undefined;
46
+ }
41
47
  function mapFsError(e, filePath) {
42
- const any = e;
43
- const code = any?.code;
48
+ const code = nodeErrorCode(e);
44
49
  if (code === 'ENOENT')
45
50
  return { code: 'FS_NOT_FOUND', message: `Not found: ${filePath}` };
46
51
  if (code === 'EEXIST')
@@ -130,8 +135,7 @@ class NodeFileSystemV2 {
130
135
  await dirHandle.close();
131
136
  }
132
137
  })(), (e) => {
133
- const any = e;
134
- const code = any?.code;
138
+ const code = nodeErrorCode(e);
135
139
  if (code === 'EINVAL' || code === 'ENOTSUP') {
136
140
  return { code: 'FS_UNSUPPORTED', message: `Directory fsync unsupported for: ${dirPath}` };
137
141
  }
@@ -2,10 +2,14 @@ import type { ResultAsync } from 'neverthrow';
2
2
  import type { DataDirPortV2 } from '../../../ports/data-dir.port.js';
3
3
  import type { FileSystemPortV2 } from '../../../ports/fs.port.js';
4
4
  import type { KeyringError, KeyringPortV2, KeyringV1 } from '../../../ports/keyring.port.js';
5
+ import type { Base64UrlPortV2 } from '../../../ports/base64url.port.js';
6
+ import type { RandomEntropyPortV2 } from '../../../ports/random-entropy.port.js';
5
7
  export declare class LocalKeyringV2 implements KeyringPortV2 {
6
8
  private readonly dataDir;
7
9
  private readonly fs;
8
- constructor(dataDir: DataDirPortV2, fs: FileSystemPortV2);
10
+ private readonly base64url;
11
+ private readonly entropy;
12
+ constructor(dataDir: DataDirPortV2, fs: FileSystemPortV2, base64url: Base64UrlPortV2, entropy: RandomEntropyPortV2);
9
13
  loadOrCreate(): ResultAsync<KeyringV1, KeyringError>;
10
14
  rotate(): ResultAsync<KeyringV1, KeyringError>;
11
15
  private createAndPersistFresh;
@@ -1,7 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.LocalKeyringV2 = void 0;
4
- const crypto_1 = require("crypto");
5
4
  const zod_1 = require("zod");
6
5
  const neverthrow_1 = require("neverthrow");
7
6
  const jcs_js_1 = require("../../../durable-core/canonical/jcs.js");
@@ -14,32 +13,32 @@ const KeyringFileV1Schema = zod_1.z.object({
14
13
  current: KeyRecordSchema,
15
14
  previous: KeyRecordSchema.nullable(),
16
15
  });
17
- function encodeBase64Url(bytes) {
18
- return Buffer.from(bytes).toString('base64url');
19
- }
20
- function decodeBase64Url(s) {
21
- try {
22
- return new Uint8Array(Buffer.from(s, 'base64url'));
16
+ function validateKeyMaterialWithPort(base64url, keyBase64Url) {
17
+ const decoded = base64url.decodeBase64Url(keyBase64Url);
18
+ if (decoded.isErr()) {
19
+ return (0, neverthrow_1.err)({
20
+ code: 'KEYRING_CORRUPTION_DETECTED',
21
+ message: `Invalid base64url in key material: ${decoded.error.code}`,
22
+ });
23
23
  }
24
- catch {
25
- return null;
24
+ if (decoded.value.length !== 32) {
25
+ return (0, neverthrow_1.err)({
26
+ code: 'KEYRING_CORRUPTION_DETECTED',
27
+ message: `Key material must be exactly 32 bytes, got ${decoded.value.length}`,
28
+ });
26
29
  }
30
+ return (0, neverthrow_1.ok)(undefined);
27
31
  }
28
- function validateKeyMaterialOrThrow(keyBase64Url) {
29
- const decoded = decodeBase64Url(keyBase64Url);
30
- if (!decoded)
31
- throw new Error('invalid_base64url');
32
- if (decoded.length !== 32)
33
- throw new Error('invalid_key_length');
34
- }
35
- function createFreshKeyRecord() {
36
- const bytes = (0, crypto_1.randomBytes)(32);
37
- return { alg: 'hmac_sha256', keyBase64Url: encodeBase64Url(bytes) };
32
+ function createFreshKeyRecord(base64url, entropy) {
33
+ const bytes = entropy.generateBytes(32);
34
+ return { alg: 'hmac_sha256', keyBase64Url: base64url.encodeBase64Url(bytes) };
38
35
  }
39
36
  class LocalKeyringV2 {
40
- constructor(dataDir, fs) {
37
+ constructor(dataDir, fs, base64url, entropy) {
41
38
  this.dataDir = dataDir;
42
39
  this.fs = fs;
40
+ this.base64url = base64url;
41
+ this.entropy = entropy;
43
42
  }
44
43
  loadOrCreate() {
45
44
  const path = this.dataDir.keyringPath();
@@ -56,27 +55,37 @@ class LocalKeyringV2 {
56
55
  return this.loadOrCreate().andThen((kr) => {
57
56
  const next = {
58
57
  v: 1,
59
- current: createFreshKeyRecord(),
58
+ current: createFreshKeyRecord(this.base64url, this.entropy),
60
59
  previous: kr.current,
61
60
  };
62
61
  return this.persist(next).map(() => next);
63
62
  });
64
63
  }
65
64
  createAndPersistFresh() {
66
- const fresh = { v: 1, current: createFreshKeyRecord(), previous: null };
65
+ const fresh = { v: 1, current: createFreshKeyRecord(this.base64url, this.entropy), previous: null };
67
66
  return this.persist(fresh).map(() => fresh);
68
67
  }
69
68
  parseAndValidate(raw, filePath) {
70
- return neverthrow_1.ResultAsync.fromPromise((async () => {
71
- const parsed = JSON.parse(raw);
72
- const validated = KeyringFileV1Schema.safeParse(parsed);
73
- if (!validated.success)
74
- throw new Error('invalid_shape');
75
- validateKeyMaterialOrThrow(validated.data.current.keyBase64Url);
76
- if (validated.data.previous)
77
- validateKeyMaterialOrThrow(validated.data.previous.keyBase64Url);
78
- return validated.data;
79
- })(), () => ({ code: 'KEYRING_CORRUPTION_DETECTED', message: `Invalid keyring file: ${filePath}` }));
69
+ let parsed;
70
+ try {
71
+ parsed = JSON.parse(raw);
72
+ }
73
+ catch {
74
+ return (0, neverthrow_1.errAsync)({ code: 'KEYRING_CORRUPTION_DETECTED', message: `Invalid keyring file: ${filePath}` });
75
+ }
76
+ const validated = KeyringFileV1Schema.safeParse(parsed);
77
+ if (!validated.success) {
78
+ return (0, neverthrow_1.errAsync)({ code: 'KEYRING_CORRUPTION_DETECTED', message: `Invalid keyring file: ${filePath}` });
79
+ }
80
+ const currentValidation = validateKeyMaterialWithPort(this.base64url, validated.data.current.keyBase64Url);
81
+ if (currentValidation.isErr())
82
+ return (0, neverthrow_1.errAsync)(currentValidation.error);
83
+ if (validated.data.previous) {
84
+ const prevValidation = validateKeyMaterialWithPort(this.base64url, validated.data.previous.keyBase64Url);
85
+ if (prevValidation.isErr())
86
+ return (0, neverthrow_1.errAsync)(prevValidation.error);
87
+ }
88
+ return (0, neverthrow_1.okAsync)(validated.data);
80
89
  }
81
90
  persist(keyring) {
82
91
  const dir = this.dataDir.keysDir();
@@ -2,10 +2,12 @@ import type { ResultAsync } from 'neverthrow';
2
2
  import type { PinnedWorkflowStorePortV2, PinnedWorkflowStoreError } from '../../../ports/pinned-workflow-store.port.js';
3
3
  import type { WorkflowHash } from '../../../durable-core/ids/index.js';
4
4
  import type { DataDirPortV2 } from '../../../ports/data-dir.port.js';
5
+ import type { FileSystemPortV2 } from '../../../ports/fs.port.js';
5
6
  import { type CompiledWorkflowSnapshot } from '../../../durable-core/schemas/compiled-workflow/index.js';
6
7
  export declare class LocalPinnedWorkflowStoreV2 implements PinnedWorkflowStorePortV2 {
7
8
  private readonly dataDir;
8
- constructor(dataDir: DataDirPortV2);
9
+ private readonly fs;
10
+ constructor(dataDir: DataDirPortV2, fs: FileSystemPortV2);
9
11
  get(workflowHash: WorkflowHash): ResultAsync<CompiledWorkflowSnapshot | null, PinnedWorkflowStoreError>;
10
12
  put(workflowHash: WorkflowHash, compiled: CompiledWorkflowSnapshot): ResultAsync<void, PinnedWorkflowStoreError>;
11
13
  }
@@ -1,84 +1,72 @@
1
1
  "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
- var __importStar = (this && this.__importStar) || (function () {
19
- var ownKeys = function(o) {
20
- ownKeys = Object.getOwnPropertyNames || function (o) {
21
- var ar = [];
22
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
- return ar;
24
- };
25
- return ownKeys(o);
26
- };
27
- return function (mod) {
28
- if (mod && mod.__esModule) return mod;
29
- var result = {};
30
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
- __setModuleDefault(result, mod);
32
- return result;
33
- };
34
- })();
35
2
  Object.defineProperty(exports, "__esModule", { value: true });
36
3
  exports.LocalPinnedWorkflowStoreV2 = void 0;
37
- const fs = __importStar(require("fs/promises"));
38
4
  const neverthrow_1 = require("neverthrow");
39
5
  const index_js_1 = require("../../../durable-core/schemas/compiled-workflow/index.js");
40
6
  const jcs_js_1 = require("../../../durable-core/canonical/jcs.js");
7
+ function mapFsToStoreError(e) {
8
+ return { code: 'PINNED_WORKFLOW_IO_ERROR', message: e.message };
9
+ }
41
10
  class LocalPinnedWorkflowStoreV2 {
42
- constructor(dataDir) {
11
+ constructor(dataDir, fs) {
43
12
  this.dataDir = dataDir;
13
+ this.fs = fs;
44
14
  }
45
15
  get(workflowHash) {
46
16
  const filePath = this.dataDir.pinnedWorkflowPath(workflowHash);
47
- return neverthrow_1.ResultAsync.fromPromise((async () => {
17
+ return this.fs.readFileUtf8(filePath)
18
+ .orElse((e) => {
19
+ if (e.code === 'FS_NOT_FOUND')
20
+ return (0, neverthrow_1.okAsync)(null);
21
+ return (0, neverthrow_1.errAsync)(mapFsToStoreError(e));
22
+ })
23
+ .andThen((raw) => {
24
+ if (raw === null)
25
+ return (0, neverthrow_1.okAsync)(null);
26
+ let parsed;
48
27
  try {
49
- const raw = await fs.readFile(filePath, 'utf8');
50
- const parsed = JSON.parse(raw);
51
- const validated = index_js_1.CompiledWorkflowSnapshotSchema.safeParse(parsed);
52
- if (!validated.success) {
53
- throw new Error(`Pinned workflow snapshot is invalid: ${filePath}`);
54
- }
55
- return validated.data;
28
+ parsed = JSON.parse(raw);
56
29
  }
57
- catch (e) {
58
- if (e?.code === 'ENOENT')
59
- return null;
60
- throw e;
30
+ catch {
31
+ return (0, neverthrow_1.errAsync)({
32
+ code: 'PINNED_WORKFLOW_IO_ERROR',
33
+ message: `Invalid JSON in pinned workflow snapshot: ${filePath}`,
34
+ });
61
35
  }
62
- })(), (e) => ({
63
- code: 'PINNED_WORKFLOW_IO_ERROR',
64
- message: `Failed to read pinned workflow snapshot: ${filePath} (${e instanceof Error ? e.message : String(e)})`,
65
- }));
36
+ const validated = index_js_1.CompiledWorkflowSnapshotSchema.safeParse(parsed);
37
+ if (!validated.success) {
38
+ return (0, neverthrow_1.errAsync)({
39
+ code: 'PINNED_WORKFLOW_IO_ERROR',
40
+ message: `Pinned workflow snapshot is invalid: ${filePath}`,
41
+ });
42
+ }
43
+ return (0, neverthrow_1.okAsync)(validated.data);
44
+ });
66
45
  }
67
46
  put(workflowHash, compiled) {
68
47
  const dir = this.dataDir.pinnedWorkflowsDir();
69
48
  const filePath = this.dataDir.pinnedWorkflowPath(workflowHash);
70
- return neverthrow_1.ResultAsync.fromPromise((async () => {
71
- await fs.mkdir(dir, { recursive: true });
72
- const canonical = (0, jcs_js_1.toCanonicalBytes)(compiled);
73
- if (canonical.isErr()) {
74
- throw new Error(`Failed to canonicalize compiled snapshot for storage: ${canonical.error.message}`);
75
- }
76
- const tmp = `${filePath}.tmp`;
77
- await fs.writeFile(tmp, Buffer.from(canonical.value));
78
- await fs.rename(tmp, filePath);
79
- })(), (e) => ({
49
+ const tmpPath = `${filePath}.tmp`;
50
+ const canonical = (0, jcs_js_1.toCanonicalBytes)(compiled).mapErr((e) => ({
80
51
  code: 'PINNED_WORKFLOW_IO_ERROR',
81
- message: `Failed to write pinned workflow snapshot: ${filePath} (${e instanceof Error ? e.message : String(e)})`,
52
+ message: `Failed to canonicalize compiled snapshot for storage: ${e.message}`,
53
+ }));
54
+ if (canonical.isErr())
55
+ return (0, neverthrow_1.errAsync)(canonical.error);
56
+ const bytes = canonical.value;
57
+ return this.fs.mkdirp(dir)
58
+ .mapErr(mapFsToStoreError)
59
+ .andThen(() => this.fs.openWriteTruncate(tmpPath).mapErr(mapFsToStoreError))
60
+ .andThen(({ fd }) => this.fs.writeAll(fd, bytes)
61
+ .mapErr(mapFsToStoreError)
62
+ .andThen(() => this.fs.fsyncFile(fd).mapErr(mapFsToStoreError))
63
+ .andThen(() => this.fs.closeFile(fd).mapErr(mapFsToStoreError))
64
+ .orElse((e) => this.fs.closeFile(fd)
65
+ .mapErr(() => e)
66
+ .andThen(() => (0, neverthrow_1.errAsync)(e))))
67
+ .andThen(() => this.fs.rename(tmpPath, filePath).mapErr(mapFsToStoreError))
68
+ .andThen(() => this.fs.fsyncDir(dir).mapErr((e) => {
69
+ return mapFsToStoreError(e);
82
70
  }));
83
71
  }
84
72
  }
@@ -0,0 +1,4 @@
1
+ import type { RandomEntropyPortV2 } from '../../../ports/random-entropy.port.js';
2
+ export declare class NodeRandomEntropyV2 implements RandomEntropyPortV2 {
3
+ generateBytes(count: number): Uint8Array;
4
+ }
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.NodeRandomEntropyV2 = void 0;
4
+ const node_crypto_1 = require("node:crypto");
5
+ class NodeRandomEntropyV2 {
6
+ generateBytes(count) {
7
+ return new Uint8Array((0, node_crypto_1.randomBytes)(count));
8
+ }
9
+ }
10
+ exports.NodeRandomEntropyV2 = NodeRandomEntropyV2;
@@ -1,12 +1,14 @@
1
1
  import type { ResultAsync } from 'neverthrow';
2
2
  import type { DataDirPortV2 } from '../../../ports/data-dir.port.js';
3
3
  import type { FileSystemPortV2 } from '../../../ports/fs.port.js';
4
+ import type { TimeClockPortV2 } from '../../../ports/time-clock.port.js';
4
5
  import type { SessionId } from '../../../durable-core/ids/index.js';
5
6
  import type { SessionLockHandleV2, SessionLockError, SessionLockPortV2 } from '../../../ports/session-lock.port.js';
6
7
  export declare class LocalSessionLockV2 implements SessionLockPortV2 {
7
8
  private readonly dataDir;
8
9
  private readonly fs;
9
- constructor(dataDir: DataDirPortV2, fs: FileSystemPortV2);
10
+ private readonly clock;
11
+ constructor(dataDir: DataDirPortV2, fs: FileSystemPortV2, clock: TimeClockPortV2);
10
12
  acquire(sessionId: SessionId): ResultAsync<SessionLockHandleV2, SessionLockError>;
11
13
  release(handle: SessionLockHandleV2): ResultAsync<void, SessionLockError>;
12
14
  }
@@ -2,9 +2,10 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.LocalSessionLockV2 = void 0;
4
4
  class LocalSessionLockV2 {
5
- constructor(dataDir, fs) {
5
+ constructor(dataDir, fs, clock) {
6
6
  this.dataDir = dataDir;
7
7
  this.fs = fs;
8
+ this.clock = clock;
8
9
  }
9
10
  acquire(sessionId) {
10
11
  const sessionDir = this.dataDir.sessionDir(sessionId);
@@ -25,8 +26,8 @@ class LocalSessionLockV2 {
25
26
  .andThen(() => this.fs.openExclusive(lockPath, new TextEncoder().encode(JSON.stringify({
26
27
  v: 1,
27
28
  sessionId,
28
- pid: process.pid,
29
- startedAtMs: Date.now(),
29
+ pid: this.clock.getPid(),
30
+ startedAtMs: this.clock.nowMs(),
30
31
  }))))
31
32
  .andThen(({ fd }) => this.fs.fsyncFile(fd).andThen(() => this.fs.closeFile(fd)))
32
33
  .mapErr(mapFs)
@@ -233,7 +233,15 @@ class LocalSessionEventLogStoreV2 {
233
233
  const validated = index_js_1.ManifestRecordV1Schema.safeParse(parsed);
234
234
  if (!validated.success) {
235
235
  isComplete = false;
236
- tailReason ?? (tailReason = { code: 'unknown_schema_version', message: 'Unknown manifest schema version (corrupt tail)' });
236
+ const rawVersion = (typeof parsed === 'object' && parsed !== null && 'v' in parsed)
237
+ ? parsed.v
238
+ : undefined;
239
+ if (rawVersion !== 1) {
240
+ tailReason ?? (tailReason = { code: 'unknown_schema_version', message: `Expected v=1, got v=${rawVersion}` });
241
+ }
242
+ else {
243
+ tailReason ?? (tailReason = { code: 'schema_validation_failed', message: 'Manifest record schema validation failed (corrupt tail)' });
244
+ }
237
245
  break;
238
246
  }
239
247
  if (validated.data.manifestIndex !== i) {
@@ -366,7 +374,10 @@ class LocalSessionEventLogStoreV2 {
366
374
  const bytes = bytesRes.value;
367
375
  return this.fs.writeAll(handle.fd, bytes).mapErr(mapFsToStoreError)
368
376
  .andThen(() => this.fs.fsyncFile(handle.fd).mapErr(mapFsToStoreError))
369
- .andThen(() => this.fs.closeFile(handle.fd).mapErr(mapFsToStoreError));
377
+ .andThen(() => this.fs.closeFile(handle.fd).mapErr(mapFsToStoreError))
378
+ .orElse((err) => this.fs.closeFile(handle.fd)
379
+ .mapErr(() => err)
380
+ .andThen(() => (0, neverthrow_1.errAsync)(err)));
370
381
  });
371
382
  }
372
383
  }
@@ -515,11 +526,22 @@ function parseJsonlText(text, schema) {
515
526
  }
516
527
  const validated = schema.safeParse(parsed);
517
528
  if (!validated.success) {
529
+ const rawVersion = (typeof parsed === 'object' && parsed !== null && 'v' in parsed)
530
+ ? parsed.v
531
+ : undefined;
532
+ if (rawVersion !== 1) {
533
+ return (0, neverthrow_1.err)({
534
+ code: 'SESSION_STORE_CORRUPTION_DETECTED',
535
+ location: i === 0 ? 'head' : 'tail',
536
+ reason: { code: 'unknown_schema_version', message: `Expected v=1, got v=${rawVersion}` },
537
+ message: `Unknown schema version at line ${i}: expected v=1, got v=${rawVersion}`,
538
+ });
539
+ }
518
540
  return (0, neverthrow_1.err)({
519
541
  code: 'SESSION_STORE_CORRUPTION_DETECTED',
520
542
  location: i === 0 ? 'head' : 'tail',
521
- reason: { code: 'unknown_schema_version', message: `Invalid record at line ${i}` },
522
- message: `Invalid record at line ${i}`,
543
+ reason: { code: 'schema_validation_failed', message: `Schema validation failed at line ${i}` },
544
+ message: `Schema validation failed at line ${i}`,
523
545
  });
524
546
  }
525
547
  out.push(validated.data);