@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.
- package/CHANGELOG.md +15 -0
- package/dist/analytics/analytics.types.d.ts +20 -0
- package/dist/analytics/anonymous-analytics-identity.d.ts +2 -0
- package/dist/analytics/anonymous-analytics-identity.js +2 -0
- package/dist/analytics/compose-analytics-identity.d.ts +2 -0
- package/dist/analytics/compose-analytics-identity.js +2 -0
- package/dist/analytics/cookie-analytics-identity.d.ts +2 -0
- package/dist/analytics/cookie-analytics-identity.js +2 -0
- package/dist/analytics/define-analytics-events.d.ts +3 -0
- package/dist/analytics/define-analytics-events.js +2 -0
- package/dist/analytics/fan-out-analytics.d.ts +2 -0
- package/dist/analytics/fan-out-analytics.js +2 -0
- package/dist/analytics/logger-analytics-service.d.ts +2 -0
- package/dist/analytics/logger-analytics-service.js +2 -0
- package/dist/analytics/mint-cookie.d.ts +8 -1
- package/dist/analytics/mint-cookie.js +8 -4
- package/dist/function/compensation-name.d.ts +4 -0
- package/dist/function/compensation-name.js +6 -0
- package/dist/function/function-meta.types.d.ts +7 -0
- package/dist/function/function-runner.js +17 -0
- package/dist/function/functions.types.d.ts +18 -0
- package/dist/function/functions.types.js +11 -0
- package/dist/middleware/require-origin.d.ts +2 -0
- package/dist/middleware/require-origin.js +2 -0
- package/dist/permissions.d.ts +5 -0
- package/dist/permissions.js +5 -0
- package/dist/testing/service-tests/queued-workflow-harness.d.ts +61 -0
- package/dist/testing/service-tests/queued-workflow-harness.js +151 -0
- package/dist/testing/service-tests/workflow-compensation-queued-tests.d.ts +11 -0
- package/dist/testing/service-tests/workflow-compensation-queued-tests.js +683 -0
- package/dist/testing/service-tests.d.ts +4 -0
- package/dist/testing/service-tests.js +4 -0
- package/dist/utils/hmac.d.ts +0 -33
- package/dist/utils/hmac.js +0 -61
- package/dist/utils.d.ts +7 -0
- package/dist/utils.js +16 -0
- package/dist/wirings/flag/define-feature-flags.d.ts +1 -13
- package/dist/wirings/flag/define-feature-flags.js +1 -13
- package/dist/wirings/http/http-runner.js +1 -3
- package/dist/wirings/rpc/rpc-runner.js +21 -1
- package/dist/wirings/trigger/webhook-source-runner.d.ts +2 -2
- package/dist/wirings/trigger/webhook-source-runner.js +15 -15
- package/dist/wirings/trigger/webhook-source.types.d.ts +9 -12
- package/dist/wirings/workflow/dsl/workflow-dsl.types.d.ts +38 -6
- package/dist/wirings/workflow/graph/graph-node.d.ts +2 -1
- package/dist/wirings/workflow/graph/graph-node.js +2 -1
- package/dist/wirings/workflow/graph/graph-runner.d.ts +1 -1
- package/dist/wirings/workflow/graph/graph-runner.js +104 -67
- package/dist/wirings/workflow/graph/graph-validation.js +2 -2
- package/dist/wirings/workflow/graph/workflow-graph.types.d.ts +20 -1
- package/dist/wirings/workflow/index.d.ts +1 -1
- package/dist/wirings/workflow/pikku-workflow-service.d.ts +13 -0
- package/dist/wirings/workflow/pikku-workflow-service.js +113 -143
- package/dist/wirings/workflow/run-timeline.d.ts +2 -0
- package/dist/wirings/workflow/run-timeline.js +3 -0
- package/dist/wirings/workflow/workflow-child-step.d.ts +17 -0
- package/dist/wirings/workflow/workflow-child-step.js +31 -0
- package/dist/wirings/workflow/workflow-compensation.d.ts +68 -0
- package/dist/wirings/workflow/workflow-compensation.js +282 -0
- package/dist/wirings/workflow/workflow-constants.d.ts +5 -0
- package/dist/wirings/workflow/workflow-constants.js +9 -0
- package/dist/wirings/workflow/workflow-dsl-pass.d.ts +12 -0
- package/dist/wirings/workflow/workflow-dsl-pass.js +75 -0
- package/dist/wirings/workflow/workflow-queue-routing.js +3 -3
- package/dist/wirings/workflow/workflow-run-status.js +13 -1
- package/dist/wirings/workflow/workflow-status-stream.js +2 -0
- package/dist/wirings/workflow/workflow-unwind-plan.d.ts +38 -0
- package/dist/wirings/workflow/workflow-unwind-plan.js +120 -0
- package/dist/wirings/workflow/workflow.types.d.ts +9 -2
- package/knowledge/decisions/internals/workflow-step-compensation-runs-as-its-own-durable-step.md +21 -18
- package/package.json +1 -1
- 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
|
}
|
package/dist/utils/hmac.d.ts
CHANGED
|
@@ -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 {};
|
package/dist/utils/hmac.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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<
|
|
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
|
-
|
|
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 (
|
|
155
|
-
|
|
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
|
|
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
|
|
126
|
-
*
|
|
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
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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;
|
|
@@ -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<{
|