@pikku/core 0.12.134 → 0.12.136

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 (72) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/analytics/analytics.types.d.ts +20 -0
  3. package/dist/analytics/anonymous-analytics-identity.d.ts +2 -0
  4. package/dist/analytics/anonymous-analytics-identity.js +2 -0
  5. package/dist/analytics/compose-analytics-identity.d.ts +2 -0
  6. package/dist/analytics/compose-analytics-identity.js +2 -0
  7. package/dist/analytics/cookie-analytics-identity.d.ts +2 -0
  8. package/dist/analytics/cookie-analytics-identity.js +2 -0
  9. package/dist/analytics/define-analytics-events.d.ts +3 -0
  10. package/dist/analytics/define-analytics-events.js +2 -0
  11. package/dist/analytics/fan-out-analytics.d.ts +2 -0
  12. package/dist/analytics/fan-out-analytics.js +2 -0
  13. package/dist/analytics/logger-analytics-service.d.ts +2 -0
  14. package/dist/analytics/logger-analytics-service.js +2 -0
  15. package/dist/analytics/mint-cookie.d.ts +8 -1
  16. package/dist/analytics/mint-cookie.js +8 -4
  17. package/dist/function/compensation-name.d.ts +4 -0
  18. package/dist/function/compensation-name.js +6 -0
  19. package/dist/function/function-meta.types.d.ts +7 -0
  20. package/dist/function/function-runner.js +17 -0
  21. package/dist/function/functions.types.d.ts +18 -0
  22. package/dist/function/functions.types.js +11 -0
  23. package/dist/middleware/require-origin.d.ts +2 -0
  24. package/dist/middleware/require-origin.js +2 -0
  25. package/dist/permissions.d.ts +5 -0
  26. package/dist/permissions.js +5 -0
  27. package/dist/testing/service-tests/queued-workflow-harness.d.ts +61 -0
  28. package/dist/testing/service-tests/queued-workflow-harness.js +151 -0
  29. package/dist/testing/service-tests/workflow-compensation-queued-tests.d.ts +11 -0
  30. package/dist/testing/service-tests/workflow-compensation-queued-tests.js +683 -0
  31. package/dist/testing/service-tests.d.ts +4 -0
  32. package/dist/testing/service-tests.js +4 -0
  33. package/dist/utils/hmac.d.ts +0 -33
  34. package/dist/utils/hmac.js +0 -61
  35. package/dist/utils.d.ts +7 -0
  36. package/dist/utils.js +16 -0
  37. package/dist/wirings/flag/define-feature-flags.d.ts +1 -13
  38. package/dist/wirings/flag/define-feature-flags.js +1 -13
  39. package/dist/wirings/http/http-runner.js +1 -3
  40. package/dist/wirings/rpc/rpc-runner.js +21 -1
  41. package/dist/wirings/trigger/webhook-source-runner.d.ts +2 -2
  42. package/dist/wirings/trigger/webhook-source-runner.js +15 -15
  43. package/dist/wirings/trigger/webhook-source.types.d.ts +9 -12
  44. package/dist/wirings/workflow/dsl/workflow-dsl.types.d.ts +38 -6
  45. package/dist/wirings/workflow/graph/graph-node.d.ts +2 -1
  46. package/dist/wirings/workflow/graph/graph-node.js +2 -1
  47. package/dist/wirings/workflow/graph/graph-runner.d.ts +1 -1
  48. package/dist/wirings/workflow/graph/graph-runner.js +104 -67
  49. package/dist/wirings/workflow/graph/graph-validation.js +2 -2
  50. package/dist/wirings/workflow/graph/workflow-graph.types.d.ts +20 -1
  51. package/dist/wirings/workflow/index.d.ts +1 -1
  52. package/dist/wirings/workflow/pikku-workflow-service.d.ts +13 -0
  53. package/dist/wirings/workflow/pikku-workflow-service.js +113 -143
  54. package/dist/wirings/workflow/run-timeline.d.ts +2 -0
  55. package/dist/wirings/workflow/run-timeline.js +3 -0
  56. package/dist/wirings/workflow/workflow-child-step.d.ts +17 -0
  57. package/dist/wirings/workflow/workflow-child-step.js +31 -0
  58. package/dist/wirings/workflow/workflow-compensation.d.ts +68 -0
  59. package/dist/wirings/workflow/workflow-compensation.js +282 -0
  60. package/dist/wirings/workflow/workflow-constants.d.ts +5 -0
  61. package/dist/wirings/workflow/workflow-constants.js +9 -0
  62. package/dist/wirings/workflow/workflow-dsl-pass.d.ts +12 -0
  63. package/dist/wirings/workflow/workflow-dsl-pass.js +75 -0
  64. package/dist/wirings/workflow/workflow-queue-routing.js +3 -3
  65. package/dist/wirings/workflow/workflow-run-status.js +13 -1
  66. package/dist/wirings/workflow/workflow-status-stream.js +2 -0
  67. package/dist/wirings/workflow/workflow-unwind-plan.d.ts +38 -0
  68. package/dist/wirings/workflow/workflow-unwind-plan.js +120 -0
  69. package/dist/wirings/workflow/workflow.types.d.ts +9 -2
  70. package/knowledge/decisions/internals/workflow-step-compensation-runs-as-its-own-durable-step.md +21 -18
  71. package/package.json +1 -1
  72. package/src/public-surface.json +1 -1
@@ -41,8 +41,12 @@ export interface ServiceTestConfig {
41
41
  leaseService?: () => Promise<LeaseService>;
42
42
  /** A workflow service that fences each step write to the claim that made it. */
43
43
  workflowFencing?: () => Promise<WorkflowFencingHarness>;
44
+ /** A workflow service to run saga compensation and graph `recover` against, driven through a queue. */
45
+ workflowCompensationQueued?: () => Promise<PikkuWorkflowService>;
46
+ workflowCompensationQueuedOptions?: QueuedCompensationOptions;
44
47
  };
45
48
  }
49
+ import { type QueuedCompensationOptions } from './service-tests/workflow-compensation-queued-tests.js';
46
50
  /**
47
51
  * The shared conformance suite every storage backend runs.
48
52
  *
@@ -10,6 +10,7 @@ import { defineAgentRunServiceTests } from './service-tests/agent-run-service-te
10
10
  import { defineSessionStoreTests } from './service-tests/session-store-tests.js';
11
11
  import { defineLeaseServiceTests } from './service-tests/lease-service-tests.js';
12
12
  import { defineWorkflowFencingTests } from './service-tests/workflow-fencing-tests.js';
13
+ import { defineWorkflowCompensationQueuedTests, } from './service-tests/workflow-compensation-queued-tests.js';
13
14
  /**
14
15
  * The shared conformance suite every storage backend runs.
15
16
  *
@@ -27,6 +28,9 @@ export function defineServiceTests(config) {
27
28
  if (services.workflowService) {
28
29
  defineWorkflowServiceTests(name, services.workflowService);
29
30
  }
31
+ if (services.workflowCompensationQueued) {
32
+ defineWorkflowCompensationQueuedTests(name, services.workflowCompensationQueued, services.workflowCompensationQueuedOptions);
33
+ }
30
34
  if (services.workflowRunService) {
31
35
  defineWorkflowRunServiceTests(name, services.workflowRunService);
32
36
  }
@@ -1,4 +1,3 @@
1
- import type { CredentialService } from '../services/credential-service.js';
2
1
  /**
3
2
  * HMAC-SHA256 of a payload, hex-encoded. Senders wrap this in their own scheme
4
3
  * prefix (`sha256=`, `v0=`, …).
@@ -12,7 +11,6 @@ export declare function timingSafeStringEqual(a: string, b: string): boolean;
12
11
  export type WebhookPayload = string | Uint8Array;
13
12
  export type HmacAlgorithm = 'sha1' | 'sha256' | 'sha512';
14
13
  export type SecretEncoding = 'utf8' | 'hex' | 'base64';
15
- type SecretSource = string | null | (() => Promise<string | null>);
16
14
  /** The HMAC of `payload` under `secret`, for handshakes that answer with a digest. */
17
15
  export declare function hmacDigest(secret: string, algorithm: HmacAlgorithm, payload: WebhookPayload, encoding: 'hex' | 'base64', secretEncoding?: SecretEncoding): string;
18
16
  /** Whether `signature` is the HMAC of `payload` under `secret`, compared in constant time. */
@@ -22,34 +20,3 @@ export declare function verifyPublicKeySignature(publicKey: string, signature: s
22
20
  algorithm?: string;
23
21
  dsaEncoding?: 'der' | 'ieee-p1363';
24
22
  }): boolean;
25
- /**
26
- * @deprecated Declare `credential` and `verify` on `wireTriggerWebhookSource`,
27
- * which checks every request before `receive` runs, or use the functions above.
28
- */
29
- export declare class WebhookSigningSecret {
30
- private readonly provider;
31
- private readonly secret;
32
- constructor(provider: string, secret: SecretSource);
33
- /**
34
- * The secret a `setup` step or a handshake stored in the credential store,
35
- * so a new one takes effect without a deploy.
36
- */
37
- static fromCredential(provider: string, credentials: CredentialService | undefined, name: string): WebhookSigningSecret;
38
- get configured(): boolean;
39
- /** The secret as it is now, to check one delivery against. */
40
- load(): Promise<WebhookSigningSecret>;
41
- /** For handshakes that answer with a digest, such as Zoom's URL validation. */
42
- hmac(algorithm: HmacAlgorithm, payload: WebhookPayload, encoding: 'hex' | 'base64', secretEncoding?: SecretEncoding): string;
43
- /** Throws unless `signature` is the HMAC of `payload` under the secret. */
44
- verifyHmac(signature: string | undefined, algorithm: HmacAlgorithm, payload: WebhookPayload, encoding: 'hex' | 'base64', secretEncoding?: SecretEncoding): void;
45
- /** For providers that send the shared secret itself rather than a signature. */
46
- verifyToken(token: string | undefined): void;
47
- /** For providers that sign with a private key: the secret is their public key, as PEM. */
48
- verifyPublicKey(signature: string | undefined, payload: WebhookPayload, options?: {
49
- algorithm?: string;
50
- dsaEncoding?: 'der' | 'ieee-p1363';
51
- }): void;
52
- private require;
53
- private rejected;
54
- }
55
- export {};
@@ -1,5 +1,4 @@
1
1
  import { createHmac, createVerify, timingSafeEqual } from 'node:crypto';
2
- import { UnauthorizedError } from '../errors/errors.js';
3
2
  /**
4
3
  * HMAC-SHA256 of a payload, hex-encoded. Senders wrap this in their own scheme
5
4
  * prefix (`sha256=`, `v0=`, …).
@@ -43,63 +42,3 @@ export function verifyPublicKeySignature(publicKey, signature, payload, options
43
42
  return false;
44
43
  }
45
44
  }
46
- /**
47
- * @deprecated Declare `credential` and `verify` on `wireTriggerWebhookSource`,
48
- * which checks every request before `receive` runs, or use the functions above.
49
- */
50
- export class WebhookSigningSecret {
51
- provider;
52
- secret;
53
- constructor(provider, secret) {
54
- this.provider = provider;
55
- this.secret = secret;
56
- }
57
- /**
58
- * The secret a `setup` step or a handshake stored in the credential store,
59
- * so a new one takes effect without a deploy.
60
- */
61
- static fromCredential(provider, credentials, name) {
62
- return new WebhookSigningSecret(provider, async () => credentials ? credentials.get(name) : null);
63
- }
64
- get configured() {
65
- return typeof this.secret === 'string';
66
- }
67
- /** The secret as it is now, to check one delivery against. */
68
- async load() {
69
- if (typeof this.secret !== 'function') {
70
- return this;
71
- }
72
- return new WebhookSigningSecret(this.provider, await this.secret());
73
- }
74
- /** For handshakes that answer with a digest, such as Zoom's URL validation. */
75
- hmac(algorithm, payload, encoding, secretEncoding = 'utf8') {
76
- return hmacDigest(this.require(), algorithm, payload, encoding, secretEncoding);
77
- }
78
- /** Throws unless `signature` is the HMAC of `payload` under the secret. */
79
- verifyHmac(signature, algorithm, payload, encoding, secretEncoding = 'utf8') {
80
- if (!verifyHmacSignature(this.require(), signature, algorithm, payload, encoding, secretEncoding)) {
81
- throw this.rejected();
82
- }
83
- }
84
- /** For providers that send the shared secret itself rather than a signature. */
85
- verifyToken(token) {
86
- if (!token || !timingSafeStringEqual(token, this.require())) {
87
- throw this.rejected();
88
- }
89
- }
90
- /** For providers that sign with a private key: the secret is their public key, as PEM. */
91
- verifyPublicKey(signature, payload, options = {}) {
92
- if (!verifyPublicKeySignature(this.require(), signature, payload, options)) {
93
- throw this.rejected();
94
- }
95
- }
96
- require() {
97
- if (typeof this.secret !== 'string') {
98
- throw new UnauthorizedError(`The ${this.provider} webhook receiver has no signing secret`);
99
- }
100
- return this.secret;
101
- }
102
- rejected() {
103
- return new UnauthorizedError(`Invalid ${this.provider} webhook signature`);
104
- }
105
- }
package/dist/utils.d.ts CHANGED
@@ -4,6 +4,13 @@ export declare const closeWireServices: (logger: Logger, wireServices: WireServi
4
4
  export declare const createWeakUID: () => string;
5
5
  export declare const isSerializable: (data: any) => boolean;
6
6
  export declare const getTagGroups: <T>(tagGroups: Record<string, T>, tag: string) => T[];
7
+ /**
8
+ * `JSON.parse` of text or UTF-8 bytes, refusing anything that is not JSON with
9
+ * a 400 rather than a 500.
10
+ *
11
+ * @example snippet: pikkuWebhookReceive
12
+ */
13
+ export declare const parseJson: <T = any>(input: string | Uint8Array) => T;
7
14
  export declare const freezeDedupe: <T>(arr?: readonly T[] | T[] | undefined) => readonly T[];
8
15
  /** Stops addon package services first, then the parent singleton services. */
9
16
  export declare const stopSingletonServices: () => Promise<void>;
package/dist/utils.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { getSingletonServices, getAllPackageStates } from './pikku-state.js';
2
+ import { BadRequestError } from './errors/errors.js';
2
3
  export const closeWireServices = async (logger, wireServices) => {
3
4
  await Promise.all(Object.values(wireServices).map(async (service) => {
4
5
  if (service?.close) {
@@ -44,6 +45,21 @@ export const getTagGroups = (tagGroups, tag) => {
44
45
  }
45
46
  return results;
46
47
  };
48
+ /**
49
+ * `JSON.parse` of text or UTF-8 bytes, refusing anything that is not JSON with
50
+ * a 400 rather than a 500.
51
+ *
52
+ * @example snippet: pikkuWebhookReceive
53
+ */
54
+ export const parseJson = (input) => {
55
+ const text = typeof input === 'string' ? input : new TextDecoder().decode(input);
56
+ try {
57
+ return JSON.parse(text);
58
+ }
59
+ catch {
60
+ throw new BadRequestError('Body is not valid JSON');
61
+ }
62
+ };
47
63
  const EMPTY_ARRAY = Object.freeze([]);
48
64
  export const freezeDedupe = (arr) => {
49
65
  if (!arr || arr.length === 0)
@@ -16,18 +16,6 @@ import type { CoreFeatureFlags } from './flag.types.js';
16
16
  * the same reason: a mid-deploy revocation is not something a code edit should
17
17
  * be able to cause.
18
18
  *
19
- * @example
20
- * ```typescript
21
- * defineFeatureFlags({
22
- * takeInBike: {
23
- * description: 'Book a bike in at the counter',
24
- * anyOf: ['bikes:intake'],
25
- * },
26
- * aiAssistant: {
27
- * description: 'The assistant panel',
28
- * // no anyOf — the switch is the whole answer
29
- * },
30
- * })
31
- * ```
19
+ * @example snippet: defineFeatureFlags
32
20
  */
33
21
  export declare const defineFeatureFlags: (_config: CoreFeatureFlags) => void;
@@ -15,18 +15,6 @@
15
15
  * the same reason: a mid-deploy revocation is not something a code edit should
16
16
  * be able to cause.
17
17
  *
18
- * @example
19
- * ```typescript
20
- * defineFeatureFlags({
21
- * takeInBike: {
22
- * description: 'Book a bike in at the counter',
23
- * anyOf: ['bikes:intake'],
24
- * },
25
- * aiAssistant: {
26
- * description: 'The assistant panel',
27
- * // no anyOf — the switch is the whole answer
28
- * },
29
- * })
30
- * ```
18
+ * @example snippet: defineFeatureFlags
31
19
  */
32
20
  export const defineFeatureFlags = (_config) => { };
@@ -215,9 +215,7 @@ const executeRoute = async (services, matchedRoute, http, options) => {
215
215
  await applyWebResponse(http.response, result);
216
216
  }
217
217
  else if (result === undefined || result === null) {
218
- if (!statusSetByRoute) {
219
- http?.response?.status(204);
220
- }
218
+ // Nothing returned: the response keeps the status the function left on it.
221
219
  }
222
220
  else if (route.returnsJSON === false) {
223
221
  http?.response?.arrayBuffer(result);
@@ -1,3 +1,4 @@
1
+ import { forwardStepName, isCompensationStepName, } from '../../function/compensation-name.js';
1
2
  import { runPikkuFunc } from '../../function/function-runner.js';
2
3
  import { addonInstanceForNamespace } from '../addon/addon-runner.js';
3
4
  import { isAddonFunctionExposed } from '../addon/wire-addon.js';
@@ -86,6 +87,15 @@ const resolvePikkuFunction = (rpcName, packageName = null) => {
86
87
  };
87
88
  }
88
89
  }
90
+ if (!rpcMeta && isCompensationStepName(rpcName)) {
91
+ const forward = forwardStepName(rpcName);
92
+ const funcs = pikkuState(null, 'function', 'functions');
93
+ const forwardId = rpc[forward] ?? (funcs.has(forward) ? forward : undefined);
94
+ const forwardFunc = forwardId ? funcs.get(forwardId) : undefined;
95
+ if (forwardId && forwardFunc?.compensate) {
96
+ return { pikkuFuncId: `${forwardId}:compensate`, packageName: null };
97
+ }
98
+ }
89
99
  if (!rpcMeta) {
90
100
  throw new RPCNotFoundError(rpcName);
91
101
  }
@@ -203,7 +213,17 @@ export class ContextAwareRPCService {
203
213
  };
204
214
  }
205
215
  const addonFunctionMeta = pikkuState(resolved.package, 'function', 'meta');
206
- const funcMeta = addonFunctionMeta[resolved.function];
216
+ let funcMeta = addonFunctionMeta[resolved.function];
217
+ if (!funcMeta && isCompensationStepName(resolved.function)) {
218
+ const forwardName = forwardStepName(resolved.function);
219
+ const forwardMeta = addonFunctionMeta[forwardName];
220
+ if (forwardMeta?.compensate) {
221
+ funcMeta = {
222
+ ...forwardMeta,
223
+ pikkuFuncId: `${forwardMeta.pikkuFuncId || forwardName}:compensate`,
224
+ };
225
+ }
226
+ }
207
227
  if (!funcMeta) {
208
228
  return NOT_RESOLVED;
209
229
  }
@@ -14,9 +14,9 @@ export declare const subscribedWebhookEvents: (source: string) => string[];
14
14
  */
15
15
  export declare const receiveWebhookSourceRequest: (sourceName: string, wire: {
16
16
  http?: PikkuHTTP;
17
- }) => Promise<Response | {
17
+ }) => Promise<{
18
18
  received: number;
19
- }>;
19
+ } | void>;
20
20
  /** The `pikku-incoming-webhooks` worker: runs the trigger an event was queued for. Throws so the queue retries. */
21
21
  export declare const dispatchWebhookSourceJob: (job: WebhookSourceJob) => Promise<void>;
22
22
  export type WebhookSourceOutcome = {
@@ -1,5 +1,6 @@
1
1
  import { getSingletonServices, pikkuState } from '../../pikku-state.js';
2
2
  import { addFunction, runPikkuFunc } from '../../function/function-runner.js';
3
+ import { parseJson } from '../../utils.js';
3
4
  import { PikkuMissingMetaError, UnauthorizedError, } from '../../errors/errors.js';
4
5
  import { timingSafeStringEqual, verifyHmacSignature, verifyPublicKeySignature, } from '../../utils/hmac.js';
5
6
  import { webhookSecretCredentialName } from './webhook-source.types.js';
@@ -37,11 +38,11 @@ const getSourceMeta = (source) => {
37
38
  }
38
39
  return meta;
39
40
  };
40
- const runSourceStep = (singletonServices, source, funcId, data) => runPikkuFunc('trigger', source, funcId, {
41
+ const runSourceStep = (singletonServices, source, funcId, data, wire = {}) => runPikkuFunc('trigger', source, funcId, {
41
42
  singletonServices,
42
43
  auth: false,
43
44
  data: () => data,
44
- wire: {},
45
+ wire,
45
46
  });
46
47
  const readRequest = async (http) => {
47
48
  const request = http?.request;
@@ -134,31 +135,30 @@ export const receiveWebhookSourceRequest = async (sourceName, wire) => {
134
135
  const meta = getSourceMeta(sourceName);
135
136
  const store = singletonServices.triggerSourceStore;
136
137
  if (store && !(await store.getTriggerSource(sourceName))?.enabled) {
137
- return new Response(null, { status: 404 });
138
+ wire.http?.response?.status(404);
139
+ return;
138
140
  }
139
141
  const source = pikkuState(null, 'trigger', 'webhookSources').get(sourceName);
140
142
  const request = await readRequest(wire.http);
143
+ // A HEAD is a provider checking the URL is live. It carries no events, so
144
+ // it is answered here rather than in every source's `receive`.
145
+ if (request.method.toLowerCase() === 'head') {
146
+ wire.http?.response?.status(200);
147
+ return;
148
+ }
141
149
  const verified = await verifyRequest(source, request, singletonServices);
142
150
  const result = meta.receive
143
- ? await runSourceStep(singletonServices, sourceName, meta.receive, request)
151
+ ? await runSourceStep(singletonServices, sourceName, meta.receive, request, wire)
144
152
  : {
145
153
  events: [
146
154
  {
147
155
  name: '',
148
- data: request.body.length
149
- ? JSON.parse(new TextDecoder().decode(request.body))
150
- : undefined,
156
+ data: request.body.length ? parseJson(request.body) : undefined,
151
157
  },
152
158
  ],
153
159
  };
154
- if ('respond' in result) {
155
- const { status, body, headers } = result.respond;
156
- const text = body === undefined
157
- ? null
158
- : typeof body === 'string'
159
- ? body
160
- : JSON.stringify(body);
161
- return new Response(text, { status, headers });
160
+ if (!result) {
161
+ return;
162
162
  }
163
163
  if (!verified && result.events.length > 0) {
164
164
  throw new UnauthorizedError(`The ${sourceName} webhook source received events in an unsigned request`);
@@ -20,16 +20,12 @@ export type WebhookRequest = {
20
20
  url: string;
21
21
  query: Record<string, string>;
22
22
  };
23
+ /**
24
+ * The events a request carries. A handshake returns nothing and answers
25
+ * through `http.response` instead, which is sent only then.
26
+ */
23
27
  export type WebhookReceiveResult = {
24
28
  events: TriggerEvent[];
25
- }
26
- /** A handshake, such as Slack's `url_verification`: answered directly, nothing is dispatched. */
27
- | {
28
- respond: {
29
- status: number;
30
- body?: unknown;
31
- headers?: Record<string, string>;
32
- };
33
29
  };
34
30
  export type WebhookSourceMethod = 'post' | 'put' | 'get' | 'head';
35
31
  /** Whatever `setup` wants back on the next deploy, for providers whose endpoints cannot be found by label. */
@@ -120,14 +116,15 @@ export type CoreTriggerWebhookSource<Events extends Record<string, StandardSchem
120
116
  /** What the secret is and where to find it, shown to whoever has to set it. */
121
117
  credentialDescription?: string;
122
118
  /**
123
- * Checked before `receive` on every request with a body. A request is
119
+ * Checked before `receive` on every request. A request with a body is
124
120
  * refused when the credential is not set or the signature does not match.
125
- * A request without a body — a HEAD probe, a validation token in the query —
126
- * reaches `receive` unchecked, and may be answered but dispatches nothing.
121
+ * A request without a body that fails the check — a HEAD probe, a
122
+ * validation token in the query — still reaches `receive`, which may answer
123
+ * it, but any events it returns are refused.
127
124
  */
128
125
  verify?: WebhookVerify;
129
126
  /** Omitted: the JSON body is one event dispatched to the trigger named `<name>`. */
130
- receive?: SourceFunction<WebhookRequest, WebhookReceiveResult>;
127
+ receive?: SourceFunction<WebhookRequest, WebhookReceiveResult | void>;
131
128
  check?: SourceFunction<WebhookLifecycleInput, WebhookCheckResult>;
132
129
  setup?: SourceFunction<WebhookLifecycleInput, WebhookSetupResult>;
133
130
  teardown?: SourceFunction<WebhookTeardownInput, WebhookTeardownResult>;
@@ -3,6 +3,7 @@
3
3
  * These types define the step-based workflow format extracted by the inspector
4
4
  */
5
5
  import type { StandardSchemaV1 } from '@standard-schema/spec';
6
+ import type { SerializedError } from '../../../errors/serialized-error.js';
6
7
  import type { WorkflowRun } from '../workflow.types.js';
7
8
  import type { ScenarioPersona } from '../../../services/personas-service.js';
8
9
  import type { ScenarioStepOptions, ScenarioStepPhase, ScenarioSurface } from '../scenario-step.types.js';
@@ -17,12 +18,11 @@ export interface WorkflowStepOptions {
17
18
  /** Delay between retry attempts (e.g., '1s', '2s', '2min') */
18
19
  retryDelay?: string | number;
19
20
  /**
20
- * RPC to invoke for compensation when this step fails after exhausting its
21
- * retries. Mirrors a graph node's `onError`: the handler receives
22
- * `{ error: { message } }` and the original error is still thrown, so the
23
- * workflow fails — this is compensation, not recovery.
21
+ * Set to `false` to leave this step out of an unwind even though its
22
+ * function declares a `compensate`. It cannot substitute a different
23
+ * compensation.
24
24
  */
25
- onError?: string;
25
+ compensate?: false;
26
26
  /**
27
27
  * Run this step as an actor (scenarios). The RPC is sent through the
28
28
  * actor's authenticated client over the REAL transport — never dispatched
@@ -93,6 +93,10 @@ export type WorkflowWireSleep = (stepName: string, duration: string) => Promise<
93
93
  * loops, like dynamic `do()` step names.
94
94
  */
95
95
  export type WorkflowWireSuspend = (reason: string) => Promise<void>;
96
+ /**
97
+ * Type signature for workflow.milestone() - used by inspector.
98
+ */
99
+ export type WorkflowWireMilestone = (name: string) => Promise<void>;
96
100
  /**
97
101
  * Who is allowed to answer an approval gate, relative to the user who started
98
102
  * the run.
@@ -436,6 +440,14 @@ export interface SuspendStepMeta {
436
440
  /** Reason string passed to workflow.suspend() — becomes the durable step key */
437
441
  reason: string;
438
442
  }
443
+ /**
444
+ * Milestone step metadata (workflow.milestone())
445
+ */
446
+ export interface MilestoneStepMeta {
447
+ type: 'milestone';
448
+ /** Name passed to workflow.milestone() — where a compensating run rests */
449
+ name: string;
450
+ }
439
451
  /**
440
452
  * Approval step metadata (workflow.approval())
441
453
  */
@@ -485,7 +497,7 @@ export interface ArrayPredicateStepMeta {
485
497
  /**
486
498
  * Workflow step metadata (extracted by inspector)
487
499
  */
488
- export type WorkflowStepMeta = RpcStepMeta | ScenarioStepMeta | BranchStepMeta | ParallelGroupStepMeta | FanoutStepMeta | ReturnStepMeta | InlineStepMeta | SleepStepMeta | CancelStepMeta | SuspendStepMeta | ApprovalStepMeta | SwitchStepMeta | FilterStepMeta | ArrayPredicateStepMeta | SetStepMeta;
500
+ export type WorkflowStepMeta = RpcStepMeta | ScenarioStepMeta | BranchStepMeta | ParallelGroupStepMeta | FanoutStepMeta | ReturnStepMeta | InlineStepMeta | SleepStepMeta | CancelStepMeta | SuspendStepMeta | MilestoneStepMeta | ApprovalStepMeta | SwitchStepMeta | FilterStepMeta | ArrayPredicateStepMeta | SetStepMeta;
489
501
  /**
490
502
  * Workflow step wire context for RPC functions
491
503
  * Provides step-level metadata including retry attempt tracking
@@ -520,7 +532,22 @@ export interface WorkflowStepWire {
520
532
  * Workflow wire object for DSL workflows
521
533
  * Provides workflow-specific capabilities to function execution
522
534
  */
535
+ export type CompensatingFor<Out = unknown> = {
536
+ ok: true;
537
+ output: Out;
538
+ stepName?: string;
539
+ } | {
540
+ ok: false;
541
+ output: null;
542
+ error: SerializedError;
543
+ stepName?: string;
544
+ };
523
545
  export interface PikkuWorkflowWire {
546
+ /**
547
+ * Set only while a function's `compensate` is running: the outcome of the
548
+ * forward call being undone. `undefined` on a forward run.
549
+ */
550
+ compensatingFor?: CompensatingFor;
524
551
  /** The workflow name */
525
552
  name: string;
526
553
  /** The current run ID */
@@ -535,6 +562,11 @@ export interface PikkuWorkflowWire {
535
562
  sleep: WorkflowWireSleep;
536
563
  /** Suspend workflow until explicitly resumed */
537
564
  suspend: WorkflowWireSuspend;
565
+ /**
566
+ * A named checkpoint. On failure the unwind stops at the last milestone
567
+ * reached and the run rests there. The name must be a string literal.
568
+ */
569
+ milestone: WorkflowWireMilestone;
538
570
  /** Suspend workflow until a human records a decision against this gate */
539
571
  approval: WorkflowWireApproval;
540
572
  }
@@ -27,7 +27,8 @@ type GraphNodeConfigMap<FuncMap extends Record<string, string>, RPCMap extends R
27
27
  /** How the per-item instances run. Defaults to 'parallel'. */
28
28
  mode?: ForEachMode;
29
29
  input?: (ref: <N extends Extract<keyof FuncMap, string>, P extends keyof ComputeNodeOutputs<FuncMap, RPCMap>[N] & string>(nodeId: N, path: P) => TypedRef<ComputeNodeOutputs<FuncMap, RPCMap>[N][P]>, template: TemplateFn, $item: ItemFn) => InputWithRefs<ComputeNodeInputs<FuncMap, RPCMap>[K]>;
30
- onError?: Extract<keyof FuncMap, string> | Extract<keyof FuncMap, string>[];
30
+ recover?: Extract<keyof FuncMap, string> | Extract<keyof FuncMap, string>[] | 'ignore';
31
+ compensate?: false;
31
32
  retries?: number;
32
33
  retryDelay?: string | number;
33
34
  notes?: string;
@@ -20,7 +20,8 @@ export function createGraph() {
20
20
  mode: def?.mode,
21
21
  input: def?.input,
22
22
  next: def?.next,
23
- onError: def?.onError,
23
+ recover: def?.recover,
24
+ compensate: def?.compensate,
24
25
  retries: def?.retries,
25
26
  retryDelay: def?.retryDelay,
26
27
  };
@@ -8,7 +8,7 @@ export declare class ChildWorkflowStartedException extends Error {
8
8
  constructor(parentRunId: string, stepId: string, childRunId: string);
9
9
  }
10
10
  export declare function stripInstanceOrdinal(name: string): string;
11
- export declare function continueGraph(workflowService: PikkuWorkflowService, runId: string, graphName: string, overrideMeta?: WorkflowRuntimeMeta): Promise<void>;
11
+ export declare function continueGraph(workflowService: PikkuWorkflowService, runId: string, graphName: string, overrideMeta?: WorkflowRuntimeMeta, rpcService?: any): Promise<void>;
12
12
  export declare function executeGraphStep(workflowService: PikkuWorkflowService, rpcService: any, runId: string, stepId: string, nodeId: string, rpcName: string, data: any, graphName: string): Promise<any>;
13
13
  export declare function runFromMeta(workflowService: PikkuWorkflowService, runId: string, meta: WorkflowRuntimeMeta, _rpcService: any): Promise<void>;
14
14
  export declare function runWorkflowGraph(workflowService: PikkuWorkflowService, graphName: string, triggerInput: any, rpcService?: any, inline?: boolean, startNode?: string, wire?: WorkflowRunWire, overrideMeta?: WorkflowRuntimeMeta): Promise<{