@plitzi/sdk-shared 0.35.4 → 0.35.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,28 @@
1
1
  # @plitzi/sdk-shared
2
2
 
3
+ ## 0.35.6
4
+
5
+ ### Patch Changes
6
+
7
+ - A failed server action can give back what it already did.
8
+
9
+ - **`flow.onFailure` ("On Failure") marks where the undo begins.** A run that reaches it has succeeded and ends there.
10
+ A run whose step failed jumps to it and runs the steps after it, in order, each still asking its own `when` — the
11
+ failure may have come before the thing to undo was ever done — with `{{ failure.step }}` and `{{ failure.message }}`
12
+ in scope. The run still ends failed, with the failure it had; an undo step that fails too is added to it.
13
+ - **The undo runs on its own clock and budget:** 5 seconds (or the run's own timeout if shorter) and its own outbound
14
+ request budget, because the run's may be exactly what ran out, and a caller closing the connection does not stop it.
15
+ A run that hit its deadline is undone too.
16
+ - **`defineAction` writes it from `onFailure: [...]`**, after the answer. `validateActionDocument` refuses an output
17
+ step or a second handler after one, warns about a handler with nothing to undo or nothing that undoes, and reserves
18
+ `failure` as a step id. `FAILURE_HANDLER_TASK` is exported from `@plitzi/sdk-shared/actions`.
19
+
20
+ ## 0.35.5
21
+
22
+ ### Patch Changes
23
+
24
+ - v0.35.5
25
+
3
26
  ## 0.35.4
4
27
 
5
28
  ### Patch Changes
@@ -0,0 +1,8 @@
1
+ /**
2
+ * The step that marks where a server action's undo begins.
3
+ *
4
+ * Steps after it run only when a step before it failed, so an action can give back what it already did — seats it took,
5
+ * a row it wrote — instead of leaving it behind a failure. Named once because four places read it: the runner that jumps
6
+ * to it, the validator that checks where it sits, the authoring API that writes it and the task catalog that offers it.
7
+ */
8
+ export declare const FAILURE_HANDLER_TASK = "flow.onFailure";
@@ -0,0 +1,4 @@
1
+ //#region src/actions/failureHandler.ts
2
+ var e = "flow.onFailure";
3
+ //#endregion
4
+ export { e as FAILURE_HANDLER_TASK };
@@ -1,6 +1,9 @@
1
1
  export { cronMatches, isKnownTimeZone, parseCron, zonedClock } from './cron';
2
+ export { FAILURE_HANDLER_TASK } from './failureHandler';
2
3
  export { triggerAccess, triggerCacheMs, triggerHasStaleVerify, triggerInput, triggerVerify } from './triggerParams';
3
4
  export { actionName, actionTriggers, isActionEnabled } from './triggers';
4
5
  export { validateActionDocument } from './validateDocument';
6
+ export { isSingleEmailAddress, readSmtpCredential, SMTP_CREDENTIAL_KEYS } from './smtp';
5
7
  export type { CronExpression } from './cron';
8
+ export type { SmtpCredentialProblem, SmtpCredentialReading, SmtpSecurity, SmtpSettings } from './smtp';
6
9
  export type { ActionDocumentIssue, ActionDocumentReport } from './validateDocument';
@@ -1,5 +1,7 @@
1
1
  import { cronMatches as e, isKnownTimeZone as t, parseCron as n, zonedClock as r } from "./cron.mjs";
2
- import { triggerAccess as i, triggerCacheMs as a, triggerHasStaleVerify as o, triggerInput as s, triggerVerify as c } from "./triggerParams.mjs";
3
- import { actionName as l, actionTriggers as u, isActionEnabled as d } from "./triggers.mjs";
4
- import { validateActionDocument as f } from "./validateDocument.mjs";
5
- export { l as actionName, u as actionTriggers, e as cronMatches, d as isActionEnabled, t as isKnownTimeZone, n as parseCron, i as triggerAccess, a as triggerCacheMs, o as triggerHasStaleVerify, s as triggerInput, c as triggerVerify, f as validateActionDocument, r as zonedClock };
2
+ import { FAILURE_HANDLER_TASK as i } from "./failureHandler.mjs";
3
+ import { triggerAccess as a, triggerCacheMs as o, triggerHasStaleVerify as s, triggerInput as c, triggerVerify as l } from "./triggerParams.mjs";
4
+ import { actionName as u, actionTriggers as d, isActionEnabled as f } from "./triggers.mjs";
5
+ import { validateActionDocument as p } from "./validateDocument.mjs";
6
+ import { SMTP_CREDENTIAL_KEYS as m, isSingleEmailAddress as h, readSmtpCredential as g } from "./smtp.mjs";
7
+ export { i as FAILURE_HANDLER_TASK, m as SMTP_CREDENTIAL_KEYS, u as actionName, d as actionTriggers, e as cronMatches, f as isActionEnabled, t as isKnownTimeZone, h as isSingleEmailAddress, n as parseCron, g as readSmtpCredential, a as triggerAccess, o as triggerCacheMs, s as triggerHasStaleVerify, c as triggerInput, l as triggerVerify, p as validateActionDocument, r as zonedClock };
@@ -0,0 +1,34 @@
1
+ /**
2
+ * The SMTP server a space sends its flows' mail through, as its credential stores it — one rule for everybody who
3
+ * reads one: the editor saving it, the API accepting it, the check reporting on it and the `email.send` task using it.
4
+ *
5
+ * A credential is a bag of strings, so every value arrives as text and is judged here: a port that is not a port, a
6
+ * sender that is not one address, a password with no user to go with it. Each problem names the key it is about, so a
7
+ * form can put the sentence under the field.
8
+ */
9
+ export type SmtpSecurity = 'tls' | 'starttls' | 'none';
10
+ export type SmtpSettings = {
11
+ host: string;
12
+ port: number;
13
+ /** `tls` connects encrypted (usually 465); `starttls` upgrades a plain connection (usually 587); `none` never does. */
14
+ security: SmtpSecurity;
15
+ /** Empty with an empty password: a relay that authenticates by where the connection comes from. */
16
+ username: string;
17
+ password: string;
18
+ fromEmail: string;
19
+ fromName: string;
20
+ };
21
+ export type SmtpCredentialProblem = {
22
+ key: keyof SmtpSettings;
23
+ message: string;
24
+ };
25
+ export type SmtpCredentialReading = {
26
+ settings: SmtpSettings;
27
+ problems: [];
28
+ } | {
29
+ problems: SmtpCredentialProblem[];
30
+ };
31
+ /** The keys an SMTP credential holds, in the order an editor asks for them. */
32
+ export declare const SMTP_CREDENTIAL_KEYS: readonly (keyof SmtpSettings)[];
33
+ export declare const isSingleEmailAddress: (value: string) => boolean;
34
+ export declare const readSmtpCredential: (data: Record<string, unknown>) => SmtpCredentialReading;
@@ -0,0 +1,51 @@
1
+ //#region src/actions/smtp.ts
2
+ var e = [
3
+ "host",
4
+ "port",
5
+ "security",
6
+ "username",
7
+ "password",
8
+ "fromEmail",
9
+ "fromName"
10
+ ], t = [
11
+ "tls",
12
+ "starttls",
13
+ "none"
14
+ ], n = /^[A-Za-z0-9](?:[A-Za-z0-9.-]{0,251}[A-Za-z0-9])?$/, r = /^[^\s@,;<>"]+@[^\s@,;<>"]+\.[^\s@,;<>"]+$/, i = (e) => r.test(e), a = (e) => t.some((t) => t === e), o = (e, t) => {
15
+ let n = e[t];
16
+ return typeof n == "number" ? String(n) : typeof n == "string" ? n.trim() : "";
17
+ }, s = (e) => {
18
+ let t = o(e, "host"), r = Number(o(e, "port")), s = o(e, "security"), c = o(e, "username"), l = typeof e.password == "string" ? e.password : "", u = o(e, "fromEmail"), d = o(e, "fromName"), f = [];
19
+ return n.test(t) || f.push({
20
+ key: "host",
21
+ message: "A host name like smtp.example.com, with no scheme and no port"
22
+ }), (!Number.isInteger(r) || r < 1 || r > 65535) && f.push({
23
+ key: "port",
24
+ message: "A port number, usually 587 or 465"
25
+ }), a(s) || f.push({
26
+ key: "security",
27
+ message: "One of tls, starttls or none"
28
+ }), l !== "" && c === "" && f.push({
29
+ key: "username",
30
+ message: "A password needs the user it belongs to"
31
+ }), i(u) || f.push({
32
+ key: "fromEmail",
33
+ message: "The one address this server sends as"
34
+ }), (/[\r\n]/.test(d) || d.length > 100) && f.push({
35
+ key: "fromName",
36
+ message: "One line of at most 100 characters"
37
+ }), f.length > 0 || !a(s) ? { problems: f } : {
38
+ settings: {
39
+ host: t,
40
+ port: r,
41
+ security: s,
42
+ username: c,
43
+ password: l,
44
+ fromEmail: u,
45
+ fromName: d
46
+ },
47
+ problems: []
48
+ };
49
+ };
50
+ //#endregion
51
+ export { e as SMTP_CREDENTIAL_KEYS, i as isSingleEmailAddress, s as readSmtpCredential };
@@ -1,41 +1,43 @@
1
1
  import { isKnownTimeZone as e, parseCron as t } from "./cron.mjs";
2
- import { triggerAccess as n, triggerHasStaleVerify as r, triggerVerify as i } from "./triggerParams.mjs";
2
+ import { FAILURE_HANDLER_TASK as n } from "./failureHandler.mjs";
3
+ import { triggerAccess as r, triggerHasStaleVerify as i, triggerVerify as a } from "./triggerParams.mjs";
3
4
  //#region src/actions/validateDocument.ts
4
- var a = [
5
+ var o = [
5
6
  "text",
6
7
  "number",
7
8
  "boolean",
8
9
  "date",
9
10
  "json",
10
11
  "file"
11
- ], o = [
12
+ ], s = [
12
13
  "public",
13
14
  "session",
14
15
  "role"
15
- ], s = [
16
+ ], c = [
16
17
  "call",
17
18
  "webhook",
18
19
  "schedule",
19
20
  "render",
20
21
  "custom"
21
- ], c = ["sha256", "sha1"], l = /* @__PURE__ */ new Set([
22
+ ], l = ["sha256", "sha1"], u = /* @__PURE__ */ new Set([
22
23
  "input",
23
24
  "user",
24
25
  "spaceId",
25
26
  "environment",
26
27
  "trigger",
27
28
  "runId",
28
- "now"
29
- ]), u = /^[a-z][a-zA-Z0-9]*\.[a-z][a-zA-Z0-9]*$/, d = (e) => typeof e == "object" && !!e && !Array.isArray(e), f = (e) => typeof e == "string" && e.trim() !== "", p = (e) => {
29
+ "now",
30
+ "failure"
31
+ ]), d = /^[a-z][a-zA-Z0-9]*\.[a-z][a-zA-Z0-9]*$/, f = (e) => typeof e == "object" && !!e && !Array.isArray(e), p = (e) => typeof e == "string" && e.trim() !== "", m = (e) => {
30
32
  try {
31
33
  let t = JSON.parse(e);
32
- return d(t) ? t : void 0;
34
+ return f(t) ? t : void 0;
33
35
  } catch {
34
36
  return;
35
37
  }
36
- }, m = (e, t, n) => {
38
+ }, h = (e, t, n) => {
37
39
  if (e !== void 0) {
38
- if (!d(e)) {
40
+ if (!f(e)) {
39
41
  n.push({
40
42
  path: t,
41
43
  message: "must be an object of field definitions"
@@ -43,215 +45,245 @@ var a = [
43
45
  return;
44
46
  }
45
47
  Object.entries(e).forEach(([e, r]) => {
46
- if (!d(r) || !f(r.type)) {
48
+ if (!f(r) || !p(r.type)) {
47
49
  n.push({
48
50
  path: `${t}.${e}`,
49
51
  message: "must declare a type"
50
52
  });
51
53
  return;
52
54
  }
53
- a.includes(r.type) || n.push({
55
+ o.includes(r.type) || n.push({
54
56
  path: `${t}.${e}.type`,
55
57
  message: `unknown field type "${r.type}"`,
56
- hint: `one of ${a.join(", ")}`
58
+ hint: `one of ${o.join(", ")}`
57
59
  });
58
60
  });
59
61
  }
60
- }, h = (a, l, u, h) => {
61
- let g = `nodes.${a}`, _ = l.action;
62
- if (!f(_) || !s.includes(_)) {
62
+ }, g = (n, o, u, d) => {
63
+ let g = `nodes.${n}`, _ = o.action;
64
+ if (!p(_) || !c.includes(_)) {
63
65
  u.push({
64
66
  path: `${g}.action`,
65
67
  message: `"${String(_)}" is not a way an action can be started`,
66
- hint: `one of ${s.join(", ")}`
68
+ hint: `one of ${c.join(", ")}`
67
69
  });
68
70
  return;
69
71
  }
70
- let v = d(l.params) ? l.params : {};
71
- if (f(v.input)) {
72
- let e = p(v.input);
72
+ let v = f(o.params) ? o.params : {};
73
+ if (p(v.input)) {
74
+ let e = m(v.input);
73
75
  e === void 0 ? u.push({
74
76
  path: `${g}.params.input`,
75
77
  message: "is not valid JSON"
76
- }) : m(e, `${g}.params.input`, u);
78
+ }) : h(e, `${g}.params.input`, u);
77
79
  }
78
80
  if (_ !== "schedule") {
79
- if (!f(v.access) || !o.includes(v.access)) u.push({
81
+ if (!p(v.access) || !s.includes(v.access)) u.push({
80
82
  path: `${g}.params.access`,
81
83
  message: "must say who may start a run this way",
82
- hint: `one of ${o.join(", ")}`
84
+ hint: `one of ${s.join(", ")}`
83
85
  });
84
86
  else if (v.access === "role") {
85
- let e = n(v);
87
+ let e = r(v);
86
88
  e?.mode === "role" && e.permissions.length === 0 && u.push({
87
89
  path: `${g}.params.permissions`,
88
90
  message: "role access must name at least one permission"
89
91
  });
90
92
  }
91
93
  }
92
- if (_ === "schedule" && (f(v.cron) ? t(v.cron) || u.push({
94
+ if (_ === "schedule" && (p(v.cron) ? t(v.cron) || u.push({
93
95
  path: `${g}.params.cron`,
94
96
  message: `"${v.cron}" is not an expression this server can read, so it would never fire`,
95
97
  hint: "five fields — minute hour day-of-month month day-of-week — with *, lists, ranges and steps"
96
98
  }) : u.push({
97
99
  path: `${g}.params.cron`,
98
100
  message: "a scheduled trigger needs a cron expression"
99
- }), f(v.timezone) && !e(v.timezone) && u.push({
101
+ }), p(v.timezone) && !e(v.timezone) && u.push({
100
102
  path: `${g}.params.timezone`,
101
103
  message: `"${v.timezone}" is not a time zone this server knows, so the schedule would never fire`,
102
104
  hint: "an IANA name like America/Santiago or Europe/Madrid — leave it empty for UTC"
103
- })), f(v.cacheSeconds)) {
105
+ })), p(v.cacheSeconds)) {
104
106
  let e = Number.parseFloat(v.cacheSeconds);
105
107
  !Number.isFinite(e) || e < 0 ? u.push({
106
108
  path: `${g}.params.cacheSeconds`,
107
109
  message: "must be a number of seconds, or left empty"
108
- }) : _ !== "render" && h.push({
110
+ }) : _ !== "render" && d.push({
109
111
  path: `${g}.params.cacheSeconds`,
110
112
  message: `a "${_}" trigger is never answered from a cache`,
111
113
  hint: "reuse only applies to a render, where the same page is built for one visitor after another"
112
114
  });
113
115
  }
114
- _ === "custom" && !f(v.name) && u.push({
116
+ _ === "custom" && !p(v.name) && u.push({
115
117
  path: `${g}.params.name`,
116
118
  message: "a custom trigger needs a name to be mounted under"
117
- }), _ === "webhook" && (r(v) && u.push({
119
+ }), _ === "webhook" && (i(v) && u.push({
118
120
  path: `${g}.params.signatureCredential`,
119
121
  message: "this signature check is in a format nothing reads any more, so the webhook is unverified",
120
122
  hint: "name the credential holding the signing secret in \"Signing secret\", and its header beside it"
121
- }), i(v) || h.push({
123
+ }), a(v) || d.push({
122
124
  path: `${g}.params.signatureCredential`,
123
125
  message: "this webhook accepts unsigned requests",
124
126
  hint: "name the credential holding the signing secret, and only the sender you expect can start a run"
125
- }), f(v.signatureAlgorithm) && !c.includes(v.signatureAlgorithm) && h.push({
127
+ }), p(v.signatureAlgorithm) && !l.includes(v.signatureAlgorithm) && d.push({
126
128
  path: `${g}.params.signatureAlgorithm`,
127
129
  message: `"${v.signatureAlgorithm}" is not an algorithm this server signs with, so sha256 is used`,
128
- hint: `one of ${c.join(", ")}`
129
- }), f(v.signatureToleranceSeconds) && !f(v.signatureTimestampHeader) && h.push({
130
+ hint: `one of ${l.join(", ")}`
131
+ }), p(v.signatureToleranceSeconds) && !p(v.signatureTimestampHeader) && d.push({
130
132
  path: `${g}.params.signatureToleranceSeconds`,
131
133
  message: "a tolerance with no timestamp header expires nothing",
132
134
  hint: "name the header the sender puts the signing time in, or drop the tolerance"
133
- })), _ === "call" && v.access === "public" && h.push({
135
+ })), _ === "call" && v.access === "public" && d.push({
134
136
  path: `${g}.params.access`,
135
137
  message: "anyone can run this action, including signed-out visitors",
136
138
  hint: "use session access unless the page needs it before anyone signs in"
137
139
  });
138
- }, g = (e, t, n, r) => {
139
- if (!d(e)) {
140
- n.push({
140
+ }, _ = (e, t, r, i) => {
141
+ if (!f(e)) {
142
+ r.push({
141
143
  path: "nodes",
142
144
  message: "must be an object of steps"
143
145
  });
144
146
  return;
145
147
  }
146
- let i = Object.entries(e), a = i.filter(([, e]) => d(e) && e.type === "trigger");
147
- a.length === 0 && n.push({
148
+ let a = Object.entries(e), o = a.filter(([, e]) => f(e) && e.type === "trigger");
149
+ o.length === 0 && r.push({
148
150
  path: "nodes",
149
151
  message: "this action has no way in",
150
152
  hint: "add a trigger step — call, webhook, schedule, render or custom"
151
153
  });
152
- let o = /* @__PURE__ */ new Map();
153
- a.forEach(([e, t]) => {
154
- f(t.action) && o.set(t.action, [...o.get(t.action) ?? [], e]);
155
- }), o.forEach((e, t) => {
156
- e.length > 1 && n.push({
154
+ let s = /* @__PURE__ */ new Map();
155
+ o.forEach(([e, t]) => {
156
+ p(t.action) && s.set(t.action, [...s.get(t.action) ?? [], e]);
157
+ }), s.forEach((e, t) => {
158
+ e.length > 1 && r.push({
157
159
  path: `nodes.${e[1]}`,
158
160
  message: `this action has ${e.length} "${t}" triggers`,
159
161
  hint: "one step per way in — a second flow starts from a different kind of trigger"
160
162
  });
161
- }), i.forEach(([t, i]) => {
163
+ }), a.forEach(([t, n]) => {
162
164
  let a = `nodes.${t}`;
163
- if (!d(i)) {
164
- n.push({
165
+ if (!f(n)) {
166
+ r.push({
165
167
  path: a,
166
168
  message: "must be an object"
167
169
  });
168
170
  return;
169
171
  }
170
- if (i.type === "trigger") {
171
- h(t, i, n, r);
172
+ if (n.type === "trigger") {
173
+ g(t, n, r, i);
172
174
  return;
173
175
  }
174
- if (i.type !== "task") {
175
- n.push({
176
+ if (n.type !== "task") {
177
+ r.push({
176
178
  path: `${a}.type`,
177
- message: `"${String(i.type)}" steps run in the browser and cannot run on the server`,
179
+ message: `"${String(n.type)}" steps run in the browser and cannot run on the server`,
178
180
  hint: "a server action is made of task steps"
179
181
  });
180
182
  return;
181
183
  }
182
- if (!f(i.action) || !u.test(i.action)) {
183
- n.push({
184
+ if (!p(n.action) || !d.test(n.action)) {
185
+ r.push({
184
186
  path: `${a}.action`,
185
187
  message: "must name a task as <namespace>.<action>",
186
188
  hint: "for example flow.output or http.request"
187
189
  });
188
190
  return;
189
191
  }
190
- f(i.afterNode) && !Object.hasOwn(e, i.afterNode) && n.push({
192
+ p(n.afterNode) && !Object.hasOwn(e, n.afterNode) && r.push({
191
193
  path: `${a}.afterNode`,
192
- message: `points at "${i.afterNode}", which is not a step here`
194
+ message: `points at "${n.afterNode}", which is not a step here`
193
195
  });
194
- }), i.forEach(([e]) => {
195
- l.has(e) && n.push({
196
+ }), a.forEach(([e]) => {
197
+ u.has(e) && r.push({
196
198
  path: `nodes.${e}`,
197
199
  message: `"${e}" is what the run itself publishes in the flow scope, so a step may not take the name`,
198
200
  hint: `rename the step — every later step reads {{ ${e} }} expecting the run's own value`
199
201
  });
200
202
  });
201
- let s = /* @__PURE__ */ new Set(), c = /* @__PURE__ */ new Set();
202
- a.forEach(([t]) => {
203
- let n = /* @__PURE__ */ new Set(), r = t;
204
- for (; r;) {
205
- if (n.has(r)) {
206
- c.add(r);
203
+ let c = /* @__PURE__ */ new Set(), l = /* @__PURE__ */ new Set(), m = [];
204
+ o.forEach(([t]) => {
205
+ let n = /* @__PURE__ */ new Set(), r = [], i = t;
206
+ for (; i;) {
207
+ if (n.has(i)) {
208
+ l.add(i);
207
209
  break;
208
210
  }
209
- n.add(r), s.add(r);
210
- let t = e[r];
211
- r = f(t?.afterNode) ? t.afterNode : "";
211
+ n.add(i), r.push(i), c.add(i);
212
+ let t = e[i];
213
+ i = p(t?.afterNode) ? t.afterNode : "";
212
214
  }
213
- }), c.forEach((e) => {
214
- n.push({
215
+ m.push(r);
216
+ }), l.forEach((e) => {
217
+ r.push({
215
218
  path: `nodes.${e}`,
216
219
  message: "this step is reached again by its own chain, so the flow never ends",
217
220
  hint: "break the `afterNode` chain that comes back to it"
218
221
  });
219
- }), i.forEach(([e]) => {
220
- s.has(e) || r.push({
222
+ }), a.forEach(([e]) => {
223
+ c.has(e) || i.push({
221
224
  path: `nodes.${e}`,
222
225
  message: "no trigger reaches this step, so it never runs",
223
226
  hint: "chain it with `afterNode` from a trigger, or delete it"
224
227
  });
225
228
  });
226
- let p = i.filter(([, e]) => d(e) && e.action === "flow.output");
227
- for (let [t, n] of p) f(n.afterNode) && Object.hasOwn(e, n.afterNode) && r.push({
228
- path: `nodes.${t}.afterNode`,
229
- message: "the output step is not the last one, so the steps after it run for nothing",
230
- hint: "move flow.output to the end of the flow"
231
- });
232
- p.length === 0 && d(t) && Object.keys(t).length > 0 && r.push({
229
+ let h = (t) => e[t]?.action, _ = a.filter(([, e]) => f(e) && e.action === "flow.output");
230
+ for (let [t, n] of _) {
231
+ let r = p(n.afterNode) && h(n.afterNode) === "flow.onFailure";
232
+ p(n.afterNode) && Object.hasOwn(e, n.afterNode) && !r && i.push({
233
+ path: `nodes.${t}.afterNode`,
234
+ message: "the output step is not the last one, so the steps after it run for nothing",
235
+ hint: "move flow.output to the end of the flow"
236
+ });
237
+ }
238
+ let v = /* @__PURE__ */ new Set(), y = (e, t) => {
239
+ v.has(`${t.path}|${t.message}`) || (v.add(`${t.path}|${t.message}`), e.push(t));
240
+ };
241
+ m.forEach((e) => {
242
+ let t = e.findIndex((e) => h(e) === n);
243
+ if (t === -1) return;
244
+ let a = e[t];
245
+ t === 1 && y(i, {
246
+ path: `nodes.${a}`,
247
+ message: "nothing runs before this step, so nothing can fail and the steps after it never run",
248
+ hint: "put On Failure after the steps whose work it undoes"
249
+ }), t === e.length - 1 && y(i, {
250
+ path: `nodes.${a}`,
251
+ message: "nothing follows this step, so a failed run undoes nothing",
252
+ hint: "add the steps that give back what the flow already did"
253
+ }), e.slice(t + 1).forEach((e) => {
254
+ h(e) === "flow.onFailure" && y(r, {
255
+ path: `nodes.${e}`,
256
+ message: "a flow has one place its undo begins, and this is a second one",
257
+ hint: "keep one On Failure step and put every undo step after it"
258
+ }), h(e) === "flow.output" && y(r, {
259
+ path: `nodes.${e}`,
260
+ message: "an output step after On Failure answers a run that has already failed",
261
+ hint: "move flow.output before On Failure"
262
+ });
263
+ });
264
+ }), _.length === 0 && f(t) && Object.keys(t).length > 0 && i.push({
233
265
  path: "nodes",
234
266
  message: "this action is expected to answer values but no step names any",
235
267
  hint: "add a flow.output step at the end naming what the caller should get"
236
268
  });
237
- }, _ = /\{\{\s*credential\./, v = (e, t) => {
269
+ }, v = /\{\{\s*credential\./, y = (e, t) => {
238
270
  Object.entries(e).forEach(([e, n]) => {
239
- if (!d(n) || !d(n.params)) return;
240
- let r = f(n.params.credential) ? n.params.credential : "";
271
+ if (!f(n) || !f(n.params)) return;
272
+ let r = p(n.params.credential) ? n.params.credential : "";
241
273
  Object.entries(n.params).forEach(([n, i]) => {
242
- typeof i == "string" && _.test(i) && r === "" && t.push({
274
+ typeof i == "string" && v.test(i) && r === "" && t.push({
243
275
  path: `nodes.${e}.params.${n}`,
244
276
  message: "this step reads a credential it did not ask for, so the token resolves to nothing",
245
277
  hint: "set the step’s `credential` to the identifier it should use"
246
278
  });
247
279
  });
248
280
  });
249
- }, y = (e) => {
281
+ }, b = (e) => {
250
282
  let t = [], n = [];
251
- return d(e) ? (f(e.name) || t.push({
283
+ return f(e) ? (p(e.name) || t.push({
252
284
  path: "name",
253
285
  message: "must have a name"
254
- }), m(e.output, "output", t), g(e.nodes, e.output, t, n), v(d(e.nodes) ? e.nodes : {}, n), {
286
+ }), h(e.output, "output", t), _(e.nodes, e.output, t, n), y(f(e.nodes) ? e.nodes : {}, n), {
255
287
  valid: t.length === 0,
256
288
  errors: t,
257
289
  warnings: n
@@ -265,4 +297,4 @@ var a = [
265
297
  };
266
298
  };
267
299
  //#endregion
268
- export { y as validateActionDocument };
300
+ export { b as validateActionDocument };
@@ -93,19 +93,19 @@ export declare const spaceEventSchemas: {
93
93
  selector: z.ZodString;
94
94
  path: z.ZodOptional<z.ZodCustom<StyleCategory, StyleCategory>>;
95
95
  type: z.ZodEnum<{
96
+ id: "id";
96
97
  class: "class";
97
98
  element: "element";
98
- id: "id";
99
99
  }>;
100
100
  style: z.ZodOptional<z.ZodCustom<import('..').StyleAttributes, import('..').StyleAttributes>>;
101
101
  params: z.ZodObject<{
102
102
  componentType: z.ZodOptional<z.ZodString>;
103
103
  styleSelector: z.ZodOptional<z.ZodString>;
104
104
  styleState: z.ZodOptional<z.ZodEnum<{
105
- disabled: "disabled";
106
105
  hover: "hover";
107
106
  active: "active";
108
107
  focus: "focus";
108
+ disabled: "disabled";
109
109
  checked: "checked";
110
110
  visited: "visited";
111
111
  }>>;
@@ -124,10 +124,10 @@ export declare const spaceEventSchemas: {
124
124
  params: z.ZodObject<{
125
125
  componentType: z.ZodOptional<z.ZodString>;
126
126
  styleState: z.ZodOptional<z.ZodEnum<{
127
- disabled: "disabled";
128
127
  hover: "hover";
129
128
  active: "active";
130
129
  focus: "focus";
130
+ disabled: "disabled";
131
131
  checked: "checked";
132
132
  visited: "visited";
133
133
  }>>;
@@ -281,9 +281,9 @@ export declare const spaceEventSchemas: {
281
281
  }>;
282
282
  selector: z.ZodString;
283
283
  type: z.ZodEnum<{
284
+ id: "id";
284
285
  class: "class";
285
286
  element: "element";
286
- id: "id";
287
287
  }>;
288
288
  path: z.ZodOptional<z.ZodCustom<StyleCategory, StyleCategory>>;
289
289
  style: z.ZodOptional<z.ZodCustom<import('..').StyleAttributes, import('..').StyleAttributes>>;
@@ -291,10 +291,10 @@ export declare const spaceEventSchemas: {
291
291
  componentType: z.ZodOptional<z.ZodString>;
292
292
  styleSelector: z.ZodOptional<z.ZodString>;
293
293
  styleState: z.ZodOptional<z.ZodEnum<{
294
- disabled: "disabled";
295
294
  hover: "hover";
296
295
  active: "active";
297
296
  focus: "focus";
297
+ disabled: "disabled";
298
298
  checked: "checked";
299
299
  visited: "visited";
300
300
  }>>;
@@ -314,10 +314,10 @@ export declare const spaceEventSchemas: {
314
314
  params: z.ZodObject<{
315
315
  componentType: z.ZodOptional<z.ZodString>;
316
316
  styleState: z.ZodOptional<z.ZodEnum<{
317
- disabled: "disabled";
318
317
  hover: "hover";
319
318
  active: "active";
320
319
  focus: "focus";
320
+ disabled: "disabled";
321
321
  checked: "checked";
322
322
  visited: "visited";
323
323
  }>>;