@pithy-sh/cloudflare 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ai/aiManager.d.ts +99 -0
- package/dist/ai/aiManager.d.ts.map +1 -0
- package/dist/ai/aiManager.js +148 -0
- package/dist/ai/vectorizeManager.d.ts +67 -0
- package/dist/ai/vectorizeManager.d.ts.map +1 -0
- package/dist/ai/vectorizeManager.js +101 -0
- package/dist/ai/vectorizeProvisioner.d.ts +123 -0
- package/dist/ai/vectorizeProvisioner.d.ts.map +1 -0
- package/dist/ai/vectorizeProvisioner.js +195 -0
- package/dist/client/accounts.d.ts +48 -0
- package/dist/client/accounts.d.ts.map +1 -0
- package/dist/client/accounts.js +55 -0
- package/dist/client/clients.d.ts +122 -0
- package/dist/client/clients.d.ts.map +1 -0
- package/dist/client/clients.js +216 -0
- package/dist/client/errors.d.ts +73 -0
- package/dist/client/errors.d.ts.map +1 -0
- package/dist/client/errors.js +98 -0
- package/dist/client/manager.d.ts +45 -0
- package/dist/client/manager.d.ts.map +1 -0
- package/dist/client/manager.js +51 -0
- package/dist/d1/d1Manager.d.ts +74 -0
- package/dist/d1/d1Manager.d.ts.map +1 -0
- package/dist/d1/d1Manager.js +128 -0
- package/dist/d1/d1PreparedStatement.d.ts +24 -0
- package/dist/d1/d1PreparedStatement.d.ts.map +1 -0
- package/dist/d1/d1PreparedStatement.js +91 -0
- package/dist/d1/d1Provisioner.d.ts +28 -0
- package/dist/d1/d1Provisioner.d.ts.map +1 -0
- package/dist/d1/d1Provisioner.js +67 -0
- package/dist/email/emailRoutingManager.d.ts +36 -0
- package/dist/email/emailRoutingManager.d.ts.map +1 -0
- package/dist/email/emailRoutingManager.js +124 -0
- package/dist/email/emailSendManager.d.ts +45 -0
- package/dist/email/emailSendManager.d.ts.map +1 -0
- package/dist/email/emailSendManager.js +52 -0
- package/dist/env/devVars.d.ts +65 -0
- package/dist/env/devVars.d.ts.map +1 -0
- package/dist/env/devVars.js +84 -0
- package/dist/hostnames/customHostnamesManager.d.ts +51 -0
- package/dist/hostnames/customHostnamesManager.d.ts.map +1 -0
- package/dist/hostnames/customHostnamesManager.js +84 -0
- package/dist/kv/kvManager.d.ts +76 -0
- package/dist/kv/kvManager.d.ts.map +1 -0
- package/dist/kv/kvManager.js +135 -0
- package/dist/kv/kvProvisioner.d.ts +28 -0
- package/dist/kv/kvProvisioner.d.ts.map +1 -0
- package/dist/kv/kvProvisioner.js +71 -0
- package/dist/media/assetSeeder.d.ts +31 -0
- package/dist/media/assetSeeder.d.ts.map +1 -0
- package/dist/media/assetSeeder.js +70 -0
- package/dist/media/imageManager.d.ts +34 -0
- package/dist/media/imageManager.d.ts.map +1 -0
- package/dist/media/imageManager.js +103 -0
- package/dist/media/ownership.d.ts +40 -0
- package/dist/media/ownership.d.ts.map +1 -0
- package/dist/media/ownership.js +46 -0
- package/dist/media/streamManager.d.ts +83 -0
- package/dist/media/streamManager.d.ts.map +1 -0
- package/dist/media/streamManager.js +145 -0
- package/dist/queue/queueManager.d.ts +84 -0
- package/dist/queue/queueManager.d.ts.map +1 -0
- package/dist/queue/queueManager.js +114 -0
- package/dist/r2/r2Credentials.d.ts +12 -0
- package/dist/r2/r2Credentials.d.ts.map +1 -0
- package/dist/r2/r2Credentials.js +13 -0
- package/dist/r2/r2Manager.d.ts +226 -0
- package/dist/r2/r2Manager.d.ts.map +1 -0
- package/dist/r2/r2Manager.js +405 -0
- package/dist/r2/r2Provisioner.d.ts +39 -0
- package/dist/r2/r2Provisioner.d.ts.map +1 -0
- package/dist/r2/r2Provisioner.js +86 -0
- package/dist/secrets/secretsStoreManager.d.ts +82 -0
- package/dist/secrets/secretsStoreManager.d.ts.map +1 -0
- package/dist/secrets/secretsStoreManager.js +147 -0
- package/dist/secrets/secretsStores.d.ts +47 -0
- package/dist/secrets/secretsStores.d.ts.map +1 -0
- package/dist/secrets/secretsStores.js +65 -0
- package/dist/test-utils/emailRoutingRules.d.ts +39 -0
- package/dist/test-utils/emailRoutingRules.d.ts.map +1 -0
- package/dist/test-utils/emailRoutingRules.js +89 -0
- package/dist/test-utils/fixtureReportSetup.d.ts +25 -0
- package/dist/test-utils/fixtureReportSetup.d.ts.map +1 -0
- package/dist/test-utils/fixtureReportSetup.js +30 -0
- package/dist/test-utils/fixtures.d.ts +224 -0
- package/dist/test-utils/fixtures.d.ts.map +1 -0
- package/dist/test-utils/fixtures.js +277 -0
- package/dist/test-utils/harness.d.ts +192 -0
- package/dist/test-utils/harness.d.ts.map +1 -0
- package/dist/test-utils/harness.js +351 -0
- package/dist/test-utils/inboundRecorder.d.ts +91 -0
- package/dist/test-utils/inboundRecorder.d.ts.map +1 -0
- package/dist/test-utils/inboundRecorder.js +141 -0
- package/dist/test-utils/integrationSetup.d.ts +22 -0
- package/dist/test-utils/integrationSetup.d.ts.map +1 -0
- package/dist/test-utils/integrationSetup.js +37 -0
- package/dist/test-utils/reap.d.ts +112 -0
- package/dist/test-utils/reap.d.ts.map +1 -0
- package/dist/test-utils/reap.js +185 -0
- package/dist/tokens/accountTokensManager.d.ts +157 -0
- package/dist/tokens/accountTokensManager.d.ts.map +1 -0
- package/dist/tokens/accountTokensManager.js +280 -0
- package/dist/tokens/permissions.d.ts +36 -0
- package/dist/tokens/permissions.d.ts.map +1 -0
- package/dist/tokens/permissions.js +50 -0
- package/dist/tokens/profiles.d.ts +102 -0
- package/dist/tokens/profiles.d.ts.map +1 -0
- package/dist/tokens/profiles.js +137 -0
- package/dist/turnstile/turnstileManager.d.ts +75 -0
- package/dist/turnstile/turnstileManager.d.ts.map +1 -0
- package/dist/turnstile/turnstileManager.js +138 -0
- package/dist/user/userManager.d.ts +36 -0
- package/dist/user/userManager.d.ts.map +1 -0
- package/dist/user/userManager.js +58 -0
- package/dist/workers/buildsManager.d.ts +116 -0
- package/dist/workers/buildsManager.d.ts.map +1 -0
- package/dist/workers/buildsManager.js +237 -0
- package/dist/workers/buildsTypes.d.ts +102 -0
- package/dist/workers/buildsTypes.d.ts.map +1 -0
- package/dist/workers/buildsTypes.js +87 -0
- package/dist/workers/workersBuildEvent.d.ts +64 -0
- package/dist/workers/workersBuildEvent.d.ts.map +1 -0
- package/dist/workers/workersBuildEvent.js +47 -0
- package/dist/workers/workersManager.d.ts +136 -0
- package/dist/workers/workersManager.d.ts.map +1 -0
- package/dist/workers/workersManager.js +312 -0
- package/dist/workers/workersProvisioner.d.ts +80 -0
- package/dist/workers/workersProvisioner.d.ts.map +1 -0
- package/dist/workers/workersProvisioner.js +106 -0
- package/dist/workflows/stepFailure.d.ts +180 -0
- package/dist/workflows/stepFailure.d.ts.map +1 -0
- package/dist/workflows/stepFailure.js +227 -0
- package/dist/workflows/workflowsClient.d.ts +87 -0
- package/dist/workflows/workflowsClient.d.ts.map +1 -0
- package/dist/workflows/workflowsClient.js +156 -0
- package/dist/zones/zonesManager.d.ts +50 -0
- package/dist/zones/zonesManager.d.ts.map +1 -0
- package/dist/zones/zonesManager.js +74 -0
- package/package.json +18 -9
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { PithyError } from "@pithy-sh/core/src/error/pithyError";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
/**
|
|
4
|
+
* **The step's sentence, recovered from an instance the engine has already written its own over.**
|
|
5
|
+
*
|
|
6
|
+
* A Workflow instance carries two failure texts. The instance-level `error` is the *platform's*: when a
|
|
7
|
+
* step raises `NonRetryableError`, the engine replaces whatever was thrown with
|
|
8
|
+
* `"The execution of the Workflow instance was terminated, as a step threw an NonRetryableError…"`.
|
|
9
|
+
* The step's own entry in `steps[]` still holds the text the kit raised. Reporting the first is how
|
|
10
|
+
* `pithy secrets create` on a name that already exists started answering with a sentence about durable
|
|
11
|
+
* execution instead of one about the secret (pithy-sh/pithy#349), the day #338 made that refusal terminal.
|
|
12
|
+
*
|
|
13
|
+
* So this reads the step, and it is read *here* rather than at a call site: every dispatch that polls a
|
|
14
|
+
* Workflow — secrets write and probe, payments reconcile, vector reprocess — goes through one
|
|
15
|
+
* `dispatchAndPoll`, and a rule that lives in the primitive cannot be missed by the next caller.
|
|
16
|
+
*
|
|
17
|
+
* ## What may cross into an operator's `message`, and what may not
|
|
18
|
+
*
|
|
19
|
+
* A `PithyError`'s `message` is public and its `detail` is stripped at the HTTP boundary — that is the
|
|
20
|
+
* security boundary, and this function is on the wrong side of it: the text arrives from a Worker whose
|
|
21
|
+
* code we did not write, over an API, and it could say anything. So a step's text is promoted into the
|
|
22
|
+
* operator's `message` **only when it is demonstrably a sentence the kit itself authored as public**:
|
|
23
|
+
*
|
|
24
|
+
* - `PithyError: <text>` — the throw's own name is the proof. `PithyError.message` *is*
|
|
25
|
+
* `payload.message`, the field already written to be safe for a client.
|
|
26
|
+
* - The encoding core's `classifiedSteps` writes, inside the platform's terminal envelope. **This file
|
|
27
|
+
* does not restate that encoding**; it calls `decodeWorkflowStepMessage`, which is the same module
|
|
28
|
+
* the writer calls, for the reason in its own doc comment: two packages agreeing about a string by
|
|
29
|
+
* each writing it down is a coincidence, not a contract (pithy-sh/pithy#353).
|
|
30
|
+
*
|
|
31
|
+
* Anything else — a foreign throw, a bare string, the engine's own prose — stays in `detail`, where it
|
|
32
|
+
* always was. Nothing that was not already public becomes public here.
|
|
33
|
+
*
|
|
34
|
+
* `action` rides that encoding and is promoted with the sentence, because it is already the operator's
|
|
35
|
+
* field: the CLI prints it under the problem line and `operatorError` includes it. `detail` does not
|
|
36
|
+
* cross, has no encoding, and is not read here.
|
|
37
|
+
*
|
|
38
|
+
* ## The literals are captured, not guessed
|
|
39
|
+
*
|
|
40
|
+
* Every platform string below was read off a real Workflows engine — the shipped Workflow class driven
|
|
41
|
+
* under `wrangler dev` (wrangler 4.123.0, 2026-08-14), instance detail fetched from the local dev
|
|
42
|
+
* session's own instance endpoint. They are frozen so the gate in `stepFailure.test.ts` polices a fixed
|
|
43
|
+
* set rather than whatever this file happens to believe today.
|
|
44
|
+
*/
|
|
45
|
+
/** A failure as the Workflows API records one: a class name and its text. Instance-level and per-step alike. */
|
|
46
|
+
export declare const WorkflowFailureDetail: z.ZodObject<{
|
|
47
|
+
name: z.ZodOptional<z.ZodString>;
|
|
48
|
+
message: z.ZodOptional<z.ZodString>;
|
|
49
|
+
}, z.core.$strip>;
|
|
50
|
+
export type WorkflowFailureDetail = z.output<typeof WorkflowFailureDetail>;
|
|
51
|
+
/** One attempt at a step. The engine appends one per retry, so the last failed one is the answer. */
|
|
52
|
+
export declare const WorkflowStepAttempt: z.ZodObject<{
|
|
53
|
+
success: z.ZodOptional<z.ZodNullable<z.ZodBoolean>>;
|
|
54
|
+
error: z.ZodOptional<z.ZodNullable<z.ZodObject<{
|
|
55
|
+
name: z.ZodOptional<z.ZodString>;
|
|
56
|
+
message: z.ZodOptional<z.ZodString>;
|
|
57
|
+
}, z.core.$strip>>>;
|
|
58
|
+
}, z.core.$strip>;
|
|
59
|
+
export type WorkflowStepAttempt = z.output<typeof WorkflowStepAttempt>;
|
|
60
|
+
/**
|
|
61
|
+
* One entry in an instance's `steps[]`.
|
|
62
|
+
*
|
|
63
|
+
* The API's four step shapes (`step`, `sleep`, `waitForEvent`, `termination`) differ in where the
|
|
64
|
+
* failure lives — a `step` keeps one per attempt, the others keep one on the entry — so both are
|
|
65
|
+
* optional here and the reader takes whichever is present. Unknown fields (`config`, `output`,
|
|
66
|
+
* timestamps) are stripped rather than refused: this is a diagnostic read, and a step shape nobody
|
|
67
|
+
* anticipated must not turn a failed Workflow into a parse error about the failure report.
|
|
68
|
+
*/
|
|
69
|
+
export declare const WorkflowStepRecord: z.ZodObject<{
|
|
70
|
+
name: z.ZodOptional<z.ZodString>;
|
|
71
|
+
type: z.ZodOptional<z.ZodString>;
|
|
72
|
+
success: z.ZodOptional<z.ZodNullable<z.ZodBoolean>>;
|
|
73
|
+
error: z.ZodOptional<z.ZodNullable<z.ZodObject<{
|
|
74
|
+
name: z.ZodOptional<z.ZodString>;
|
|
75
|
+
message: z.ZodOptional<z.ZodString>;
|
|
76
|
+
}, z.core.$strip>>>;
|
|
77
|
+
attempts: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
78
|
+
success: z.ZodOptional<z.ZodNullable<z.ZodBoolean>>;
|
|
79
|
+
error: z.ZodOptional<z.ZodNullable<z.ZodObject<{
|
|
80
|
+
name: z.ZodOptional<z.ZodString>;
|
|
81
|
+
message: z.ZodOptional<z.ZodString>;
|
|
82
|
+
}, z.core.$strip>>>;
|
|
83
|
+
}, z.core.$strip>>>;
|
|
84
|
+
}, z.core.$strip>;
|
|
85
|
+
export type WorkflowStepRecord = z.output<typeof WorkflowStepRecord>;
|
|
86
|
+
/** What a failed instance's steps say about why, once the platform's own prose is set aside. */
|
|
87
|
+
export interface WorkflowStepFailure {
|
|
88
|
+
/** The failed step's name, when the engine reported one. */
|
|
89
|
+
step?: string;
|
|
90
|
+
/** The step's recorded text, verbatim, platform envelope and all. For `detail` — never for `message`. */
|
|
91
|
+
raw: string;
|
|
92
|
+
/** The `PithyError` code the step raised, when its text carries one. */
|
|
93
|
+
code?: string;
|
|
94
|
+
/** The step's own public sentence — present only when the text is one the kit authored. */
|
|
95
|
+
sentence?: string;
|
|
96
|
+
/** The remedy the raising error stated, when it stated one. The CLI's action line. */
|
|
97
|
+
action?: string;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* The kit-authored public sentence inside a step's recorded text, and the remedy riding with it —
|
|
101
|
+
* `null` when there is none.
|
|
102
|
+
*
|
|
103
|
+
* Exported for the gate, which drives captured engine output through it: a platform sentence reaching an
|
|
104
|
+
* operator is a test failure, and the test has to be able to ask this question directly.
|
|
105
|
+
*/
|
|
106
|
+
export declare function kitSentence(raw: string): {
|
|
107
|
+
code?: string;
|
|
108
|
+
message: string;
|
|
109
|
+
action?: string;
|
|
110
|
+
} | null;
|
|
111
|
+
/**
|
|
112
|
+
* The last step that failed, and what it said. `null` when no step reports a failure — an instance the
|
|
113
|
+
* platform failed on its own (a timeout, a terminate) has steps that all succeeded, or none at all.
|
|
114
|
+
*
|
|
115
|
+
* Read from the end: the failing step is the last one the instance reached, and an earlier step that
|
|
116
|
+
* failed and was retried into success has nothing to say about why the instance ended.
|
|
117
|
+
*
|
|
118
|
+
* Each entry is validated on its way in and an entry that will not parse is skipped rather than thrown
|
|
119
|
+
* on. The caller is already reporting a failure; refusing to describe it because one step of the report
|
|
120
|
+
* had an unfamiliar shape trades a useful sentence for a useless one.
|
|
121
|
+
*/
|
|
122
|
+
export declare function stepFailure(steps: readonly unknown[] | undefined): WorkflowStepFailure | null;
|
|
123
|
+
/**
|
|
124
|
+
* The code a terminal Workflow failure falls back to when the fault that ended the run cannot be
|
|
125
|
+
* attributed. Stated here because two things must agree on it: the thrower below, and the gate that
|
|
126
|
+
* proves this code is not one a transport failure can arrive under.
|
|
127
|
+
*/
|
|
128
|
+
export declare const WORKFLOW_FAILED_CODE: "core/workflow_failed";
|
|
129
|
+
/** The status pinned to {@link WORKFLOW_FAILED_CODE}, restated so the throw cannot drift off it. */
|
|
130
|
+
export declare const WORKFLOW_FAILED_STATUS: 500;
|
|
131
|
+
/**
|
|
132
|
+
* **The error an operator is handed for a Workflow that ran and did not complete** (pithy-sh/pithy#365).
|
|
133
|
+
*
|
|
134
|
+
* `#353` fixed the sentence and the remedy. The `code` and the `status` were still the *transport's*:
|
|
135
|
+
* `dispatchAndPoll` threw `CloudflareRequestError`, which fixes `cloudflare/request_failed` and 502 by
|
|
136
|
+
* construction, so a step that raised `secrets/already_exists` with 409 reached the CLI as a 502 —
|
|
137
|
+
* "the far side is broken, try later" for a request that was delivered, ran, and was permanently
|
|
138
|
+
* refused. Anything branching on the pair rather than reading the prose was told the opposite of what
|
|
139
|
+
* happened.
|
|
140
|
+
*
|
|
141
|
+
* ## The distinction that has to survive, and where it lives
|
|
142
|
+
*
|
|
143
|
+
* A Workflow whose *step raised* is not the same event as a dispatch that could not be delivered, and
|
|
144
|
+
* the difference is now in the two machine-readable fields rather than in the message:
|
|
145
|
+
*
|
|
146
|
+
* - **The step raised, and the kit pins a status for its code** → that code, that status. Terminal by
|
|
147
|
+
* the code's own definition; `secrets/already_exists` is a 409 wherever it is raised.
|
|
148
|
+
* - **The run ended and nothing is attributable** — a foreign throw, the platform's own prose, an
|
|
149
|
+
* `unclassified` fault, an adopter code the kit pins no status for → {@link WORKFLOW_FAILED_CODE},
|
|
150
|
+
* 500. Terminal, and deliberately not 502.
|
|
151
|
+
* - **The dispatch itself failed** — the REST call was refused, timed out, or answered with a shape
|
|
152
|
+
* nobody expected → `cloudflare/request_failed` / `cloudflare/invalid_response`, 502, thrown by
|
|
153
|
+
* `cloudflareRequest` exactly as before. This function is never reached for one.
|
|
154
|
+
*
|
|
155
|
+
* So the reader tells them apart on `code` alone, and the three sets are disjoint.
|
|
156
|
+
*
|
|
157
|
+
* ## Why the status is recovered from the code rather than carried on the wire
|
|
158
|
+
*
|
|
159
|
+
* Nothing but `code`, `message` and `action` crosses a durable step boundary — the engine records the
|
|
160
|
+
* throw's text and discards the throw. A fourth field could have been encoded, at the cost of
|
|
161
|
+
* reopening a format `#353` froze against a measurement. It is not needed: **every kit member pins
|
|
162
|
+
* `status` to one literal**, so the code *is* the status (`kitErrorStatus`). A code the kit does not
|
|
163
|
+
* define has no pinned status, and rather than invent one this says so with its own code.
|
|
164
|
+
*
|
|
165
|
+
* ## `detail` still does not cross
|
|
166
|
+
*
|
|
167
|
+
* `detail` is composed here from the platform's raw text and the instance id — the operator's side of
|
|
168
|
+
* the boundary — and nothing derived from the step's own `detail` is in it, because the step's
|
|
169
|
+
* `detail` never left the step. The only fields promoted from the far side are `message` and `action`,
|
|
170
|
+
* both already public, both already proved kit-authored by `kitSentence`.
|
|
171
|
+
*/
|
|
172
|
+
export declare function terminalWorkflowError(args: {
|
|
173
|
+
/** What the instance's steps said, or `null` when no step reported a failure. */
|
|
174
|
+
failure: WorkflowStepFailure | null;
|
|
175
|
+
/** The sentence to use when the step authored none — the caller's own, about the instance. */
|
|
176
|
+
fallbackMessage: string;
|
|
177
|
+
/** The operator's context line. Raw platform text and the instance id; never the dispatched params. */
|
|
178
|
+
detail: string;
|
|
179
|
+
}): PithyError;
|
|
180
|
+
//# sourceMappingURL=stepFailure.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stepFailure.d.ts","sourceRoot":"","sources":["../../src/workflows/stepFailure.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,UAAU,EAAE,MAAM,qCAAqC,CAAC;AAMjE,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AAEH,gHAAgH;AAChH,eAAO,MAAM,qBAAqB;;;iBAQwD,CAAC;AAC3F,MAAM,MAAM,qBAAqB,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAE3E,qGAAqG;AACrG,eAAO,MAAM,mBAAmB;;;;;;iBAK0D,CAAC;AAC3F,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAEvE;;;;;;;;GAQG;AACH,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;iBAQ+D,CAAC;AAC/F,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAErE,gGAAgG;AAChG,MAAM,WAAW,mBAAmB;IAClC,4DAA4D;IAC5D,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,yGAAyG;IACzG,GAAG,EAAE,MAAM,CAAC;IACZ,wEAAwE;IACxE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,2FAA2F;IAC3F,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,sFAAsF;IACtF,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAmBD;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAqBnG;AAcD;;;;;;;;;;GAUG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,SAAS,OAAO,EAAE,GAAG,SAAS,GAAG,mBAAmB,GAAG,IAAI,CAoB7F;AAED;;;;GAIG;AACH,eAAO,MAAM,oBAAoB,EAAG,sBAA+B,CAAC;AAEpE,oGAAoG;AACpG,eAAO,MAAM,sBAAsB,EAAG,GAAY,CAAC;AAEnD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE;IAC1C,iFAAiF;IACjF,OAAO,EAAE,mBAAmB,GAAG,IAAI,CAAC;IACpC,8FAA8F;IAC9F,eAAe,EAAE,MAAM,CAAC;IACxB,uGAAuG;IACvG,MAAM,EAAE,MAAM,CAAC;CAChB,GAAG,UAAU,CAmBb"}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { PithyError } from "@pithy-sh/core/src/error/pithyError";
|
|
3
|
+
import { ErrorPayload, kitErrorStatus } from "@pithy-sh/core/src/error/payload";
|
|
4
|
+
import { MAX_WORKFLOW_STEP_TEXT, decodeWorkflowStepMessage, splitWorkflowStepCode } from "@pithy-sh/core/src/workflow/stepMessage";
|
|
5
|
+
//#region src/workflows/stepFailure.ts
|
|
6
|
+
/**
|
|
7
|
+
* **The step's sentence, recovered from an instance the engine has already written its own over.**
|
|
8
|
+
*
|
|
9
|
+
* A Workflow instance carries two failure texts. The instance-level `error` is the *platform's*: when a
|
|
10
|
+
* step raises `NonRetryableError`, the engine replaces whatever was thrown with
|
|
11
|
+
* `"The execution of the Workflow instance was terminated, as a step threw an NonRetryableError…"`.
|
|
12
|
+
* The step's own entry in `steps[]` still holds the text the kit raised. Reporting the first is how
|
|
13
|
+
* `pithy secrets create` on a name that already exists started answering with a sentence about durable
|
|
14
|
+
* execution instead of one about the secret (pithy-sh/pithy#349), the day #338 made that refusal terminal.
|
|
15
|
+
*
|
|
16
|
+
* So this reads the step, and it is read *here* rather than at a call site: every dispatch that polls a
|
|
17
|
+
* Workflow — secrets write and probe, payments reconcile, vector reprocess — goes through one
|
|
18
|
+
* `dispatchAndPoll`, and a rule that lives in the primitive cannot be missed by the next caller.
|
|
19
|
+
*
|
|
20
|
+
* ## What may cross into an operator's `message`, and what may not
|
|
21
|
+
*
|
|
22
|
+
* A `PithyError`'s `message` is public and its `detail` is stripped at the HTTP boundary — that is the
|
|
23
|
+
* security boundary, and this function is on the wrong side of it: the text arrives from a Worker whose
|
|
24
|
+
* code we did not write, over an API, and it could say anything. So a step's text is promoted into the
|
|
25
|
+
* operator's `message` **only when it is demonstrably a sentence the kit itself authored as public**:
|
|
26
|
+
*
|
|
27
|
+
* - `PithyError: <text>` — the throw's own name is the proof. `PithyError.message` *is*
|
|
28
|
+
* `payload.message`, the field already written to be safe for a client.
|
|
29
|
+
* - The encoding core's `classifiedSteps` writes, inside the platform's terminal envelope. **This file
|
|
30
|
+
* does not restate that encoding**; it calls `decodeWorkflowStepMessage`, which is the same module
|
|
31
|
+
* the writer calls, for the reason in its own doc comment: two packages agreeing about a string by
|
|
32
|
+
* each writing it down is a coincidence, not a contract (pithy-sh/pithy#353).
|
|
33
|
+
*
|
|
34
|
+
* Anything else — a foreign throw, a bare string, the engine's own prose — stays in `detail`, where it
|
|
35
|
+
* always was. Nothing that was not already public becomes public here.
|
|
36
|
+
*
|
|
37
|
+
* `action` rides that encoding and is promoted with the sentence, because it is already the operator's
|
|
38
|
+
* field: the CLI prints it under the problem line and `operatorError` includes it. `detail` does not
|
|
39
|
+
* cross, has no encoding, and is not read here.
|
|
40
|
+
*
|
|
41
|
+
* ## The literals are captured, not guessed
|
|
42
|
+
*
|
|
43
|
+
* Every platform string below was read off a real Workflows engine — the shipped Workflow class driven
|
|
44
|
+
* under `wrangler dev` (wrangler 4.123.0, 2026-08-14), instance detail fetched from the local dev
|
|
45
|
+
* session's own instance endpoint. They are frozen so the gate in `stepFailure.test.ts` polices a fixed
|
|
46
|
+
* set rather than whatever this file happens to believe today.
|
|
47
|
+
*/
|
|
48
|
+
/** A failure as the Workflows API records one: a class name and its text. Instance-level and per-step alike. */
|
|
49
|
+
const WorkflowFailureDetail = z.object({
|
|
50
|
+
name: z.string().optional().describe("The thrown value's class name, as the engine recorded it."),
|
|
51
|
+
message: z.string().optional().describe("The failure text — the engine's own for a terminal step, else the throw's.")
|
|
52
|
+
}).describe("One failure the Workflows API reports, on an instance or on a step attempt.");
|
|
53
|
+
/** One attempt at a step. The engine appends one per retry, so the last failed one is the answer. */
|
|
54
|
+
const WorkflowStepAttempt = z.object({
|
|
55
|
+
success: z.boolean().nullish().describe("Whether this attempt returned; `false` once it threw."),
|
|
56
|
+
error: WorkflowFailureDetail.nullish().describe("What this attempt threw, absent when it returned.")
|
|
57
|
+
}).describe("One attempt at a Workflow step, as the instance-detail endpoint reports it.");
|
|
58
|
+
/**
|
|
59
|
+
* One entry in an instance's `steps[]`.
|
|
60
|
+
*
|
|
61
|
+
* The API's four step shapes (`step`, `sleep`, `waitForEvent`, `termination`) differ in where the
|
|
62
|
+
* failure lives — a `step` keeps one per attempt, the others keep one on the entry — so both are
|
|
63
|
+
* optional here and the reader takes whichever is present. Unknown fields (`config`, `output`,
|
|
64
|
+
* timestamps) are stripped rather than refused: this is a diagnostic read, and a step shape nobody
|
|
65
|
+
* anticipated must not turn a failed Workflow into a parse error about the failure report.
|
|
66
|
+
*/
|
|
67
|
+
const WorkflowStepRecord = z.object({
|
|
68
|
+
name: z.string().optional().describe("The step's name, as `step.do` was called with it."),
|
|
69
|
+
type: z.string().optional().describe("Which of the API's step shapes this is — `step`, `sleep`, `waitForEvent`."),
|
|
70
|
+
success: z.boolean().nullish().describe("Whether the step completed; `false` once every attempt failed."),
|
|
71
|
+
error: WorkflowFailureDetail.nullish().describe("The failure, for the shapes that record one on the entry."),
|
|
72
|
+
attempts: z.array(WorkflowStepAttempt).optional().describe("Each attempt at the step, oldest first.")
|
|
73
|
+
}).describe("One step of a Workflow instance, narrowed to the fields a failure report reads.");
|
|
74
|
+
/**
|
|
75
|
+
* The envelope the engine wraps a terminal step throw in, captured verbatim. The inner text is *not*
|
|
76
|
+
* JSON-quoted — a message containing a `"` is embedded raw — so this is a prefix/suffix strip and never
|
|
77
|
+
* a `JSON.parse`.
|
|
78
|
+
*/
|
|
79
|
+
const TERMINAL_STEP_ENVELOPE = Object.freeze({
|
|
80
|
+
prefix: "Step threw a NonRetryableError with message \"",
|
|
81
|
+
suffix: "\""
|
|
82
|
+
});
|
|
83
|
+
/**
|
|
84
|
+
* The throw names a kit step error can arrive under. `PithyError` is core's one throw vehicle;
|
|
85
|
+
* `NonRetryableError` is what `classifiedSteps` re-throws a terminal fault as. A name outside this set
|
|
86
|
+
* is somebody else's error, and its text stays in `detail`.
|
|
87
|
+
*/
|
|
88
|
+
const KIT_THROW_NAMES = Object.freeze(["PithyError", "NonRetryableError"]);
|
|
89
|
+
/**
|
|
90
|
+
* The kit-authored public sentence inside a step's recorded text, and the remedy riding with it —
|
|
91
|
+
* `null` when there is none.
|
|
92
|
+
*
|
|
93
|
+
* Exported for the gate, which drives captured engine output through it: a platform sentence reaching an
|
|
94
|
+
* operator is a test failure, and the test has to be able to ask this question directly.
|
|
95
|
+
*/
|
|
96
|
+
function kitSentence(raw) {
|
|
97
|
+
let text = raw.trim();
|
|
98
|
+
if (text.startsWith(TERMINAL_STEP_ENVELOPE.prefix) && text.endsWith(TERMINAL_STEP_ENVELOPE.suffix)) text = text.slice(TERMINAL_STEP_ENVELOPE.prefix.length, -TERMINAL_STEP_ENVELOPE.suffix.length);
|
|
99
|
+
const thrown = KIT_THROW_NAMES.map((name) => `${name}: `).find((prefix) => text.startsWith(prefix));
|
|
100
|
+
if (thrown === void 0) return null;
|
|
101
|
+
text = text.slice(thrown.length);
|
|
102
|
+
if (thrown === "NonRetryableError: ") return decodeWorkflowStepMessage(text);
|
|
103
|
+
const { code, rest } = splitWorkflowStepCode(text);
|
|
104
|
+
const message = rest.trim();
|
|
105
|
+
if (message === "" || message.length > MAX_WORKFLOW_STEP_TEXT) return null;
|
|
106
|
+
if (/[\n\r]/.test(message)) return null;
|
|
107
|
+
return code === void 0 ? { message } : {
|
|
108
|
+
code,
|
|
109
|
+
message
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
/** The recorded failure text of one step entry, from its last failed attempt or its own error. */
|
|
113
|
+
function failureTextOf(step) {
|
|
114
|
+
for (let index = (step.attempts?.length ?? 0) - 1; index >= 0; index -= 1) {
|
|
115
|
+
const attempt = step.attempts?.[index];
|
|
116
|
+
if (attempt?.success === true) continue;
|
|
117
|
+
const message = attempt?.error?.message;
|
|
118
|
+
if (message !== void 0 && message !== "") return message;
|
|
119
|
+
}
|
|
120
|
+
const own = step.error?.message;
|
|
121
|
+
return own === void 0 || own === "" ? void 0 : own;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* The last step that failed, and what it said. `null` when no step reports a failure — an instance the
|
|
125
|
+
* platform failed on its own (a timeout, a terminate) has steps that all succeeded, or none at all.
|
|
126
|
+
*
|
|
127
|
+
* Read from the end: the failing step is the last one the instance reached, and an earlier step that
|
|
128
|
+
* failed and was retried into success has nothing to say about why the instance ended.
|
|
129
|
+
*
|
|
130
|
+
* Each entry is validated on its way in and an entry that will not parse is skipped rather than thrown
|
|
131
|
+
* on. The caller is already reporting a failure; refusing to describe it because one step of the report
|
|
132
|
+
* had an unfamiliar shape trades a useful sentence for a useless one.
|
|
133
|
+
*/
|
|
134
|
+
function stepFailure(steps) {
|
|
135
|
+
if (steps === void 0) return null;
|
|
136
|
+
for (let index = steps.length - 1; index >= 0; index -= 1) {
|
|
137
|
+
const parsed = WorkflowStepRecord.safeParse(steps[index]);
|
|
138
|
+
if (!parsed.success) continue;
|
|
139
|
+
if (parsed.data.success === true) continue;
|
|
140
|
+
const raw = failureTextOf(parsed.data);
|
|
141
|
+
if (raw === void 0) continue;
|
|
142
|
+
const kit = kitSentence(raw);
|
|
143
|
+
return {
|
|
144
|
+
...parsed.data.name === void 0 ? {} : { step: parsed.data.name },
|
|
145
|
+
raw,
|
|
146
|
+
...kit?.code === void 0 ? {} : { code: kit.code },
|
|
147
|
+
...kit === null ? {} : { sentence: kit.message },
|
|
148
|
+
...kit?.action === void 0 ? {} : { action: kit.action }
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* The code a terminal Workflow failure falls back to when the fault that ended the run cannot be
|
|
155
|
+
* attributed. Stated here because two things must agree on it: the thrower below, and the gate that
|
|
156
|
+
* proves this code is not one a transport failure can arrive under.
|
|
157
|
+
*/
|
|
158
|
+
const WORKFLOW_FAILED_CODE = "core/workflow_failed";
|
|
159
|
+
/** The status pinned to {@link WORKFLOW_FAILED_CODE}, restated so the throw cannot drift off it. */
|
|
160
|
+
const WORKFLOW_FAILED_STATUS = 500;
|
|
161
|
+
/**
|
|
162
|
+
* **The error an operator is handed for a Workflow that ran and did not complete** (pithy-sh/pithy#365).
|
|
163
|
+
*
|
|
164
|
+
* `#353` fixed the sentence and the remedy. The `code` and the `status` were still the *transport's*:
|
|
165
|
+
* `dispatchAndPoll` threw `CloudflareRequestError`, which fixes `cloudflare/request_failed` and 502 by
|
|
166
|
+
* construction, so a step that raised `secrets/already_exists` with 409 reached the CLI as a 502 —
|
|
167
|
+
* "the far side is broken, try later" for a request that was delivered, ran, and was permanently
|
|
168
|
+
* refused. Anything branching on the pair rather than reading the prose was told the opposite of what
|
|
169
|
+
* happened.
|
|
170
|
+
*
|
|
171
|
+
* ## The distinction that has to survive, and where it lives
|
|
172
|
+
*
|
|
173
|
+
* A Workflow whose *step raised* is not the same event as a dispatch that could not be delivered, and
|
|
174
|
+
* the difference is now in the two machine-readable fields rather than in the message:
|
|
175
|
+
*
|
|
176
|
+
* - **The step raised, and the kit pins a status for its code** → that code, that status. Terminal by
|
|
177
|
+
* the code's own definition; `secrets/already_exists` is a 409 wherever it is raised.
|
|
178
|
+
* - **The run ended and nothing is attributable** — a foreign throw, the platform's own prose, an
|
|
179
|
+
* `unclassified` fault, an adopter code the kit pins no status for → {@link WORKFLOW_FAILED_CODE},
|
|
180
|
+
* 500. Terminal, and deliberately not 502.
|
|
181
|
+
* - **The dispatch itself failed** — the REST call was refused, timed out, or answered with a shape
|
|
182
|
+
* nobody expected → `cloudflare/request_failed` / `cloudflare/invalid_response`, 502, thrown by
|
|
183
|
+
* `cloudflareRequest` exactly as before. This function is never reached for one.
|
|
184
|
+
*
|
|
185
|
+
* So the reader tells them apart on `code` alone, and the three sets are disjoint.
|
|
186
|
+
*
|
|
187
|
+
* ## Why the status is recovered from the code rather than carried on the wire
|
|
188
|
+
*
|
|
189
|
+
* Nothing but `code`, `message` and `action` crosses a durable step boundary — the engine records the
|
|
190
|
+
* throw's text and discards the throw. A fourth field could have been encoded, at the cost of
|
|
191
|
+
* reopening a format `#353` froze against a measurement. It is not needed: **every kit member pins
|
|
192
|
+
* `status` to one literal**, so the code *is* the status (`kitErrorStatus`). A code the kit does not
|
|
193
|
+
* define has no pinned status, and rather than invent one this says so with its own code.
|
|
194
|
+
*
|
|
195
|
+
* ## `detail` still does not cross
|
|
196
|
+
*
|
|
197
|
+
* `detail` is composed here from the platform's raw text and the instance id — the operator's side of
|
|
198
|
+
* the boundary — and nothing derived from the step's own `detail` is in it, because the step's
|
|
199
|
+
* `detail` never left the step. The only fields promoted from the far side are `message` and `action`,
|
|
200
|
+
* both already public, both already proved kit-authored by `kitSentence`.
|
|
201
|
+
*/
|
|
202
|
+
function terminalWorkflowError(args) {
|
|
203
|
+
const { failure, fallbackMessage, detail } = args;
|
|
204
|
+
const message = failure?.sentence ?? fallbackMessage;
|
|
205
|
+
const action = failure?.sentence === void 0 ? void 0 : failure.action;
|
|
206
|
+
const status = failure?.code === void 0 ? void 0 : kitErrorStatus(failure.code);
|
|
207
|
+
if (failure?.sentence !== void 0 && failure.code !== void 0 && status !== void 0) {
|
|
208
|
+
const candidate = {
|
|
209
|
+
code: failure.code,
|
|
210
|
+
status,
|
|
211
|
+
message,
|
|
212
|
+
action,
|
|
213
|
+
detail
|
|
214
|
+
};
|
|
215
|
+
const parsed = ErrorPayload.safeParse(candidate);
|
|
216
|
+
if (parsed.success) return new PithyError(parsed.data);
|
|
217
|
+
}
|
|
218
|
+
return new PithyError({
|
|
219
|
+
code: WORKFLOW_FAILED_CODE,
|
|
220
|
+
status: 500,
|
|
221
|
+
message,
|
|
222
|
+
action,
|
|
223
|
+
detail
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
//#endregion
|
|
227
|
+
export { WORKFLOW_FAILED_CODE, WORKFLOW_FAILED_STATUS, WorkflowFailureDetail, WorkflowStepAttempt, WorkflowStepRecord, kitSentence, stepFailure, terminalWorkflowError };
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { CloudflareManager, type CloudflareManagerConfig } from "../client/manager";
|
|
3
|
+
/** Pause `ms` milliseconds between status polls. Injectable so tests run with no real delay. */
|
|
4
|
+
export type Sleeper = (ms: number) => Promise<void>;
|
|
5
|
+
export interface CloudflareWorkflowsClientConfig extends CloudflareManagerConfig {
|
|
6
|
+
/** Override the poll delay (tests). Defaults to a real `setTimeout`. */
|
|
7
|
+
sleeper?: Sleeper;
|
|
8
|
+
}
|
|
9
|
+
export declare const WorkflowInstanceStatus: z.ZodEnum<{
|
|
10
|
+
complete: "complete";
|
|
11
|
+
errored: "errored";
|
|
12
|
+
paused: "paused";
|
|
13
|
+
queued: "queued";
|
|
14
|
+
rollingBack: "rollingBack";
|
|
15
|
+
running: "running";
|
|
16
|
+
terminated: "terminated";
|
|
17
|
+
unknown: "unknown";
|
|
18
|
+
waiting: "waiting";
|
|
19
|
+
waitingForPause: "waitingForPause";
|
|
20
|
+
}>;
|
|
21
|
+
export type WorkflowInstanceStatus = z.output<typeof WorkflowInstanceStatus>;
|
|
22
|
+
export declare const WorkflowInstance: z.ZodObject<{
|
|
23
|
+
status: z.ZodEnum<{
|
|
24
|
+
complete: "complete";
|
|
25
|
+
errored: "errored";
|
|
26
|
+
paused: "paused";
|
|
27
|
+
queued: "queued";
|
|
28
|
+
rollingBack: "rollingBack";
|
|
29
|
+
running: "running";
|
|
30
|
+
terminated: "terminated";
|
|
31
|
+
unknown: "unknown";
|
|
32
|
+
waiting: "waiting";
|
|
33
|
+
waitingForPause: "waitingForPause";
|
|
34
|
+
}>;
|
|
35
|
+
output: z.ZodOptional<z.ZodUnknown>;
|
|
36
|
+
error: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
|
|
37
|
+
message: z.ZodOptional<z.ZodString>;
|
|
38
|
+
}, z.core.$strip>, z.ZodNull]>>;
|
|
39
|
+
steps: z.ZodOptional<z.ZodArray<z.ZodUnknown>>;
|
|
40
|
+
}, z.core.$strip>;
|
|
41
|
+
export type WorkflowInstance = z.output<typeof WorkflowInstance>;
|
|
42
|
+
/**
|
|
43
|
+
* A client over the Cloudflare Workflows API — the only sanctioned way to reach CF Workflows from
|
|
44
|
+
* outside a Worker (CLAUDE.md: no hand-rolled CF `fetch` outside `@pithy-sh/cloudflare`). The
|
|
45
|
+
* `pithy secrets` CLI uses it to trigger a per-env manager's Workflow and poll to completion: the
|
|
46
|
+
* CLI never runs secret logic locally, since the master key is worker-only.
|
|
47
|
+
*
|
|
48
|
+
* The SDK owns transport and envelope handling. Instance status is still re-validated through
|
|
49
|
+
* {@link WorkflowInstance} rather than taken from the SDK's types: the SDK narrows `output` to
|
|
50
|
+
* `string | number`, but a Workflow's return value is arbitrary JSON, and the status enum is a wire
|
|
51
|
+
* value we decode at the boundary like every other external input.
|
|
52
|
+
*/
|
|
53
|
+
export declare class CloudflareWorkflowsClient extends CloudflareManager {
|
|
54
|
+
#private;
|
|
55
|
+
constructor(config: CloudflareWorkflowsClientConfig);
|
|
56
|
+
getServiceType(): string;
|
|
57
|
+
/** Prove reach by listing the account's Workflows; never throws. */
|
|
58
|
+
validateServiceAccess(): Promise<boolean>;
|
|
59
|
+
/** Create (trigger) a Workflow instance and return its id. */
|
|
60
|
+
createInstance(workflowName: string, params: unknown, instanceId?: string): Promise<string>;
|
|
61
|
+
/** Fetch an instance's status. Returns `null` on a 404 (a just-created instance can briefly lag). */
|
|
62
|
+
getInstanceStatus(workflowName: string, instanceId: string): Promise<WorkflowInstance | null>;
|
|
63
|
+
/**
|
|
64
|
+
* Trigger a Workflow and poll until it reaches a terminal state. Resolves with the instance
|
|
65
|
+
* `output` on `complete`. The error never carries the dispatched params (which may be secret).
|
|
66
|
+
*
|
|
67
|
+
* **Three failures, three codes, and a caller can tell them apart without reading a word of prose**
|
|
68
|
+
* (pithy-sh/pithy#365):
|
|
69
|
+
*
|
|
70
|
+
* | What happened | Code | Status |
|
|
71
|
+
* |---|---|---|
|
|
72
|
+
* | A step raised, and the kit pins a status for its code | that code | that status |
|
|
73
|
+
* | The run ended terminally, nothing attributable | `core/workflow_failed` | 500 |
|
|
74
|
+
* | The dispatch or a poll could not be delivered | `cloudflare/request_failed` | 502 |
|
|
75
|
+
* | We stopped waiting; the instance may still finish | `core/upstream_timeout` | 504 |
|
|
76
|
+
*
|
|
77
|
+
* The first two are terminal — the run is over and re-driving it reaches the same end. The last two
|
|
78
|
+
* are not. Reporting all of them as 502 told an operator to wait for a recovery that would never
|
|
79
|
+
* come; see `terminalWorkflowError` for the argument in full.
|
|
80
|
+
*/
|
|
81
|
+
dispatchAndPoll(workflowName: string, params: unknown, options?: {
|
|
82
|
+
pollIntervalMs?: number;
|
|
83
|
+
maxPolls?: number;
|
|
84
|
+
instanceId?: string;
|
|
85
|
+
}): Promise<unknown>;
|
|
86
|
+
}
|
|
87
|
+
//# sourceMappingURL=workflowsClient.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workflowsClient.d.ts","sourceRoot":"","sources":["../../src/workflows/workflowsClient.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAE,iBAAiB,EAAE,KAAK,uBAAuB,EAAE,MAAM,mBAAmB,CAAC;AAGpF,gGAAgG;AAChG,MAAM,MAAM,OAAO,GAAG,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;AAEpD,MAAM,WAAW,+BAAgC,SAAQ,uBAAuB;IAC9E,wEAAwE;IACxE,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,eAAO,MAAM,sBAAsB;;;;;;;;;;;EAawD,CAAC;AAC5F,MAAM,MAAM,sBAAsB,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAE7E,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;iBAqBuE,CAAC;AACrG,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAOjE;;;;;;;;;;GAUG;AACH,qBAAa,yBAA0B,SAAQ,iBAAiB;;IAG9D,YAAY,MAAM,EAAE,+BAA+B,EAGlD;IAED,cAAc,IAAI,MAAM,CAEvB;IAED,oEAAoE;IAC9D,qBAAqB,IAAI,OAAO,CAAC,OAAO,CAAC,CAO9C;IAED,8DAA8D;IACxD,cAAc,CAAC,YAAY,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAYhG;IAED,qGAAqG;IAC/F,iBAAiB,CAAC,YAAY,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAoBlG;IAED;;;;;;;;;;;;;;;;;OAiBG;IACG,eAAe,CACnB,YAAY,EAAE,MAAM,EACpB,MAAM,EAAE,OAAO,EACf,OAAO,GAAE;QAAE,cAAc,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAA;KAAO,GAChF,OAAO,CAAC,OAAO,CAAC,CAmClB;CACF"}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { CloudflareInvalidResponseError, cloudflareRequest, isNotFoundError } from "../client/errors.js";
|
|
2
|
+
import { CloudflareManager } from "../client/manager.js";
|
|
3
|
+
import { stepFailure, terminalWorkflowError } from "./stepFailure.js";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { UpstreamTimeoutError } from "@pithy-sh/core/src/error/pithyError";
|
|
6
|
+
//#region src/workflows/workflowsClient.ts
|
|
7
|
+
const WorkflowInstanceStatus = z.enum([
|
|
8
|
+
"queued",
|
|
9
|
+
"running",
|
|
10
|
+
"paused",
|
|
11
|
+
"errored",
|
|
12
|
+
"terminated",
|
|
13
|
+
"complete",
|
|
14
|
+
"waitingForPause",
|
|
15
|
+
"waiting",
|
|
16
|
+
"rollingBack",
|
|
17
|
+
"unknown"
|
|
18
|
+
]).describe("Lifecycle state of a Cloudflare Workflow instance, from the status endpoint.");
|
|
19
|
+
const WorkflowInstance = z.object({
|
|
20
|
+
status: WorkflowInstanceStatus.describe("The instance's current lifecycle state."),
|
|
21
|
+
output: z.unknown().optional().describe("The Workflow's return value, present once `status` is `complete`."),
|
|
22
|
+
error: z.union([
|
|
23
|
+
z.string(),
|
|
24
|
+
z.object({ message: z.string().optional().describe("Failure message.") }).describe("The structured form of the failure, when the API sends an object rather than a string."),
|
|
25
|
+
z.null()
|
|
26
|
+
]).optional().describe("The instance-level failure, present when `status` is `errored` or `terminated`."),
|
|
27
|
+
steps: z.array(z.unknown()).optional().describe("The instance's steps, each validated where it is read (`stepFailure`). Held as `unknown` here on purpose: the API has four step shapes, this client reads one field of one of them, and a shape nobody anticipated must not turn a failed Workflow into a parse error about the failure report.")
|
|
28
|
+
}).describe("A Cloudflare Workflow instance's status, as returned by the instance-detail endpoint.");
|
|
29
|
+
/** Terminal states a poll stops on. */
|
|
30
|
+
const TERMINAL = /* @__PURE__ */ new Set([
|
|
31
|
+
"complete",
|
|
32
|
+
"errored",
|
|
33
|
+
"terminated"
|
|
34
|
+
]);
|
|
35
|
+
const defaultSleeper = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
36
|
+
/**
|
|
37
|
+
* A client over the Cloudflare Workflows API — the only sanctioned way to reach CF Workflows from
|
|
38
|
+
* outside a Worker (CLAUDE.md: no hand-rolled CF `fetch` outside `@pithy-sh/cloudflare`). The
|
|
39
|
+
* `pithy secrets` CLI uses it to trigger a per-env manager's Workflow and poll to completion: the
|
|
40
|
+
* CLI never runs secret logic locally, since the master key is worker-only.
|
|
41
|
+
*
|
|
42
|
+
* The SDK owns transport and envelope handling. Instance status is still re-validated through
|
|
43
|
+
* {@link WorkflowInstance} rather than taken from the SDK's types: the SDK narrows `output` to
|
|
44
|
+
* `string | number`, but a Workflow's return value is arbitrary JSON, and the status enum is a wire
|
|
45
|
+
* value we decode at the boundary like every other external input.
|
|
46
|
+
*/
|
|
47
|
+
var CloudflareWorkflowsClient = class extends CloudflareManager {
|
|
48
|
+
#sleeper;
|
|
49
|
+
constructor(config) {
|
|
50
|
+
super(config);
|
|
51
|
+
this.#sleeper = config.sleeper ?? defaultSleeper;
|
|
52
|
+
}
|
|
53
|
+
getServiceType() {
|
|
54
|
+
return "Workflows";
|
|
55
|
+
}
|
|
56
|
+
/** Prove reach by listing the account's Workflows; never throws. */
|
|
57
|
+
async validateServiceAccess() {
|
|
58
|
+
try {
|
|
59
|
+
for await (const _workflow of this.getClient().workflows.list({ account_id: this.accountId })) break;
|
|
60
|
+
return true;
|
|
61
|
+
} catch {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/** Create (trigger) a Workflow instance and return its id. */
|
|
66
|
+
async createInstance(workflowName, params, instanceId) {
|
|
67
|
+
return cloudflareRequest(`create instance of ${workflowName}`, async () => {
|
|
68
|
+
const created = await this.getClient().workflows.instances.create(workflowName, {
|
|
69
|
+
account_id: this.accountId,
|
|
70
|
+
params,
|
|
71
|
+
...instanceId ? { instance_id: instanceId } : {}
|
|
72
|
+
});
|
|
73
|
+
if (!created.id) throw new CloudflareInvalidResponseError({ detail: `create instance of ${workflowName}: missing instance id` });
|
|
74
|
+
return created.id;
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
/** Fetch an instance's status. Returns `null` on a 404 (a just-created instance can briefly lag). */
|
|
78
|
+
async getInstanceStatus(workflowName, instanceId) {
|
|
79
|
+
const operation = `get status of ${workflowName}/${instanceId}`;
|
|
80
|
+
return cloudflareRequest(operation, async () => {
|
|
81
|
+
let response;
|
|
82
|
+
try {
|
|
83
|
+
response = await this.getClient().workflows.instances.get(instanceId, {
|
|
84
|
+
account_id: this.accountId,
|
|
85
|
+
workflow_name: workflowName
|
|
86
|
+
});
|
|
87
|
+
} catch (error) {
|
|
88
|
+
if (isNotFoundError(error)) return null;
|
|
89
|
+
throw error;
|
|
90
|
+
}
|
|
91
|
+
const parsed = WorkflowInstance.safeParse(response);
|
|
92
|
+
if (!parsed.success) throw new CloudflareInvalidResponseError({ detail: `${operation}: unexpected shape` });
|
|
93
|
+
return parsed.data;
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Trigger a Workflow and poll until it reaches a terminal state. Resolves with the instance
|
|
98
|
+
* `output` on `complete`. The error never carries the dispatched params (which may be secret).
|
|
99
|
+
*
|
|
100
|
+
* **Three failures, three codes, and a caller can tell them apart without reading a word of prose**
|
|
101
|
+
* (pithy-sh/pithy#365):
|
|
102
|
+
*
|
|
103
|
+
* | What happened | Code | Status |
|
|
104
|
+
* |---|---|---|
|
|
105
|
+
* | A step raised, and the kit pins a status for its code | that code | that status |
|
|
106
|
+
* | The run ended terminally, nothing attributable | `core/workflow_failed` | 500 |
|
|
107
|
+
* | The dispatch or a poll could not be delivered | `cloudflare/request_failed` | 502 |
|
|
108
|
+
* | We stopped waiting; the instance may still finish | `core/upstream_timeout` | 504 |
|
|
109
|
+
*
|
|
110
|
+
* The first two are terminal — the run is over and re-driving it reaches the same end. The last two
|
|
111
|
+
* are not. Reporting all of them as 502 told an operator to wait for a recovery that would never
|
|
112
|
+
* come; see `terminalWorkflowError` for the argument in full.
|
|
113
|
+
*/
|
|
114
|
+
async dispatchAndPoll(workflowName, params, options = {}) {
|
|
115
|
+
const pollIntervalMs = options.pollIntervalMs ?? 1e3;
|
|
116
|
+
const maxPolls = options.maxPolls ?? 120;
|
|
117
|
+
const id = await this.createInstance(workflowName, params, options.instanceId);
|
|
118
|
+
for (let poll = 0; poll < maxPolls; poll++) {
|
|
119
|
+
await this.#sleeper(pollIntervalMs);
|
|
120
|
+
const instance = await this.getInstanceStatus(workflowName, id);
|
|
121
|
+
if (!instance) continue;
|
|
122
|
+
if (!TERMINAL.has(instance.status)) continue;
|
|
123
|
+
if (instance.status === "complete") return instance.output;
|
|
124
|
+
const failure = stepFailure(instance.steps);
|
|
125
|
+
throw terminalWorkflowError({
|
|
126
|
+
failure,
|
|
127
|
+
fallbackMessage: `Workflow ${workflowName} did not complete (${instance.status}).`,
|
|
128
|
+
detail: `instance ${id} ended ${instance.status}: ${describeFailure(failure, instance.error)}`
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
throw new UpstreamTimeoutError({
|
|
132
|
+
message: `Workflow ${workflowName} did not finish in time.`,
|
|
133
|
+
action: `Check the instance in the Cloudflare dashboard; it may still complete.`,
|
|
134
|
+
detail: `instance ${id} still running after ${maxPolls} polls`
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
/** The most specific instance-error text, for a failure `detail`. Never the dispatched params. */
|
|
139
|
+
function describeError(error) {
|
|
140
|
+
if (!error) return "no error detail";
|
|
141
|
+
if (typeof error === "string") return error;
|
|
142
|
+
return error.message ?? "no error detail";
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Everything known about why, for the `detail` line: the failed step's name and its recorded text
|
|
146
|
+
* verbatim, then the instance's own. Both, because they differ — that difference is the bug this
|
|
147
|
+
* function exists because of — and `detail` is the operator's side of the boundary, where the raw
|
|
148
|
+
* platform text belongs. Never the dispatched params.
|
|
149
|
+
*/
|
|
150
|
+
function describeFailure(failure, error) {
|
|
151
|
+
const instance = describeError(error);
|
|
152
|
+
if (failure === null) return instance;
|
|
153
|
+
return `${failure.step === void 0 ? "step" : `step '${failure.step}'`} raised${failure.code === void 0 ? "" : ` [${failure.code}]`} ${failure.raw} — instance error: ${instance}`;
|
|
154
|
+
}
|
|
155
|
+
//#endregion
|
|
156
|
+
export { CloudflareWorkflowsClient, WorkflowInstance, WorkflowInstanceStatus };
|