@pithy-sh/cloudflare 0.1.1 → 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.
Files changed (139) hide show
  1. package/dist/ai/aiManager.d.ts +99 -0
  2. package/dist/ai/aiManager.d.ts.map +1 -0
  3. package/dist/ai/aiManager.js +148 -0
  4. package/dist/ai/vectorizeManager.d.ts +67 -0
  5. package/dist/ai/vectorizeManager.d.ts.map +1 -0
  6. package/dist/ai/vectorizeManager.js +101 -0
  7. package/dist/ai/vectorizeProvisioner.d.ts +123 -0
  8. package/dist/ai/vectorizeProvisioner.d.ts.map +1 -0
  9. package/dist/ai/vectorizeProvisioner.js +195 -0
  10. package/dist/client/accounts.d.ts +48 -0
  11. package/dist/client/accounts.d.ts.map +1 -0
  12. package/dist/client/accounts.js +55 -0
  13. package/dist/client/clients.d.ts +122 -0
  14. package/dist/client/clients.d.ts.map +1 -0
  15. package/dist/client/clients.js +216 -0
  16. package/dist/client/errors.d.ts +73 -0
  17. package/dist/client/errors.d.ts.map +1 -0
  18. package/dist/client/errors.js +98 -0
  19. package/dist/client/manager.d.ts +45 -0
  20. package/dist/client/manager.d.ts.map +1 -0
  21. package/dist/client/manager.js +51 -0
  22. package/dist/d1/d1Manager.d.ts +74 -0
  23. package/dist/d1/d1Manager.d.ts.map +1 -0
  24. package/dist/d1/d1Manager.js +128 -0
  25. package/dist/d1/d1PreparedStatement.d.ts +24 -0
  26. package/dist/d1/d1PreparedStatement.d.ts.map +1 -0
  27. package/dist/d1/d1PreparedStatement.js +91 -0
  28. package/dist/d1/d1Provisioner.d.ts +28 -0
  29. package/dist/d1/d1Provisioner.d.ts.map +1 -0
  30. package/dist/d1/d1Provisioner.js +67 -0
  31. package/dist/email/emailRoutingManager.d.ts +36 -0
  32. package/dist/email/emailRoutingManager.d.ts.map +1 -0
  33. package/dist/email/emailRoutingManager.js +124 -0
  34. package/dist/email/emailSendManager.d.ts +45 -0
  35. package/dist/email/emailSendManager.d.ts.map +1 -0
  36. package/dist/email/emailSendManager.js +52 -0
  37. package/dist/env/devVars.d.ts +65 -0
  38. package/dist/env/devVars.d.ts.map +1 -0
  39. package/dist/env/devVars.js +84 -0
  40. package/dist/hostnames/customHostnamesManager.d.ts +51 -0
  41. package/dist/hostnames/customHostnamesManager.d.ts.map +1 -0
  42. package/dist/hostnames/customHostnamesManager.js +84 -0
  43. package/dist/kv/kvManager.d.ts +76 -0
  44. package/dist/kv/kvManager.d.ts.map +1 -0
  45. package/dist/kv/kvManager.js +135 -0
  46. package/dist/kv/kvProvisioner.d.ts +28 -0
  47. package/dist/kv/kvProvisioner.d.ts.map +1 -0
  48. package/dist/kv/kvProvisioner.js +71 -0
  49. package/dist/media/assetSeeder.d.ts +31 -0
  50. package/dist/media/assetSeeder.d.ts.map +1 -0
  51. package/dist/media/assetSeeder.js +70 -0
  52. package/dist/media/imageManager.d.ts +34 -0
  53. package/dist/media/imageManager.d.ts.map +1 -0
  54. package/dist/media/imageManager.js +103 -0
  55. package/dist/media/ownership.d.ts +40 -0
  56. package/dist/media/ownership.d.ts.map +1 -0
  57. package/dist/media/ownership.js +46 -0
  58. package/dist/media/streamManager.d.ts +83 -0
  59. package/dist/media/streamManager.d.ts.map +1 -0
  60. package/dist/media/streamManager.js +145 -0
  61. package/dist/queue/queueManager.d.ts +84 -0
  62. package/dist/queue/queueManager.d.ts.map +1 -0
  63. package/dist/queue/queueManager.js +114 -0
  64. package/dist/r2/r2Credentials.d.ts +12 -0
  65. package/dist/r2/r2Credentials.d.ts.map +1 -0
  66. package/dist/r2/r2Credentials.js +13 -0
  67. package/dist/r2/r2Manager.d.ts +226 -0
  68. package/dist/r2/r2Manager.d.ts.map +1 -0
  69. package/dist/r2/r2Manager.js +405 -0
  70. package/dist/r2/r2Provisioner.d.ts +39 -0
  71. package/dist/r2/r2Provisioner.d.ts.map +1 -0
  72. package/dist/r2/r2Provisioner.js +86 -0
  73. package/dist/secrets/secretsStoreManager.d.ts +82 -0
  74. package/dist/secrets/secretsStoreManager.d.ts.map +1 -0
  75. package/dist/secrets/secretsStoreManager.js +147 -0
  76. package/dist/secrets/secretsStores.d.ts +47 -0
  77. package/dist/secrets/secretsStores.d.ts.map +1 -0
  78. package/dist/secrets/secretsStores.js +65 -0
  79. package/dist/test-utils/emailRoutingRules.d.ts +39 -0
  80. package/dist/test-utils/emailRoutingRules.d.ts.map +1 -0
  81. package/dist/test-utils/emailRoutingRules.js +89 -0
  82. package/dist/test-utils/fixtureReportSetup.d.ts +25 -0
  83. package/dist/test-utils/fixtureReportSetup.d.ts.map +1 -0
  84. package/dist/test-utils/fixtureReportSetup.js +30 -0
  85. package/dist/test-utils/fixtures.d.ts +224 -0
  86. package/dist/test-utils/fixtures.d.ts.map +1 -0
  87. package/dist/test-utils/fixtures.js +277 -0
  88. package/dist/test-utils/harness.d.ts +192 -0
  89. package/dist/test-utils/harness.d.ts.map +1 -0
  90. package/dist/test-utils/harness.js +351 -0
  91. package/dist/test-utils/inboundRecorder.d.ts +91 -0
  92. package/dist/test-utils/inboundRecorder.d.ts.map +1 -0
  93. package/dist/test-utils/inboundRecorder.js +141 -0
  94. package/dist/test-utils/integrationSetup.d.ts +22 -0
  95. package/dist/test-utils/integrationSetup.d.ts.map +1 -0
  96. package/dist/test-utils/integrationSetup.js +37 -0
  97. package/dist/test-utils/reap.d.ts +112 -0
  98. package/dist/test-utils/reap.d.ts.map +1 -0
  99. package/dist/test-utils/reap.js +185 -0
  100. package/dist/tokens/accountTokensManager.d.ts +157 -0
  101. package/dist/tokens/accountTokensManager.d.ts.map +1 -0
  102. package/dist/tokens/accountTokensManager.js +280 -0
  103. package/dist/tokens/permissions.d.ts +36 -0
  104. package/dist/tokens/permissions.d.ts.map +1 -0
  105. package/dist/tokens/permissions.js +50 -0
  106. package/dist/tokens/profiles.d.ts +102 -0
  107. package/dist/tokens/profiles.d.ts.map +1 -0
  108. package/dist/tokens/profiles.js +137 -0
  109. package/dist/turnstile/turnstileManager.d.ts +75 -0
  110. package/dist/turnstile/turnstileManager.d.ts.map +1 -0
  111. package/dist/turnstile/turnstileManager.js +138 -0
  112. package/dist/user/userManager.d.ts +36 -0
  113. package/dist/user/userManager.d.ts.map +1 -0
  114. package/dist/user/userManager.js +58 -0
  115. package/dist/workers/buildsManager.d.ts +116 -0
  116. package/dist/workers/buildsManager.d.ts.map +1 -0
  117. package/dist/workers/buildsManager.js +237 -0
  118. package/dist/workers/buildsTypes.d.ts +102 -0
  119. package/dist/workers/buildsTypes.d.ts.map +1 -0
  120. package/dist/workers/buildsTypes.js +87 -0
  121. package/dist/workers/workersBuildEvent.d.ts +64 -0
  122. package/dist/workers/workersBuildEvent.d.ts.map +1 -0
  123. package/dist/workers/workersBuildEvent.js +47 -0
  124. package/dist/workers/workersManager.d.ts +136 -0
  125. package/dist/workers/workersManager.d.ts.map +1 -0
  126. package/dist/workers/workersManager.js +312 -0
  127. package/dist/workers/workersProvisioner.d.ts +80 -0
  128. package/dist/workers/workersProvisioner.d.ts.map +1 -0
  129. package/dist/workers/workersProvisioner.js +106 -0
  130. package/dist/workflows/stepFailure.d.ts +180 -0
  131. package/dist/workflows/stepFailure.d.ts.map +1 -0
  132. package/dist/workflows/stepFailure.js +227 -0
  133. package/dist/workflows/workflowsClient.d.ts +87 -0
  134. package/dist/workflows/workflowsClient.d.ts.map +1 -0
  135. package/dist/workflows/workflowsClient.js +156 -0
  136. package/dist/zones/zonesManager.d.ts +50 -0
  137. package/dist/zones/zonesManager.d.ts.map +1 -0
  138. package/dist/zones/zonesManager.js +74 -0
  139. 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 };