@plitzi/sdk-server 0.35.5 → 0.35.7
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 +38 -0
- package/dist/modules/actions/runtime/check.js +1 -1
- package/dist/modules/actions/runtime/email.js +3 -3
- package/dist/modules/actions/runtime/runAction.js +102 -3
- package/dist/modules/actions/tasks/email.js +11 -6
- package/dist/modules/actions/tasks/flow.js +20 -1
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,43 @@
|
|
|
1
1
|
# @plitzi/sdk-server
|
|
2
2
|
|
|
3
|
+
## 0.35.7
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- An SMTP failure says which credential it was, and a credential's values can be replaced from the builder.
|
|
8
|
+
|
|
9
|
+
- **`email.send` names the credential it could not send through:** `Could not send through the SMTP credential
|
|
10
|
+
"ceniza-smtp": its SMTP host is on a private network…`. The host used to be in the sentence, and a run's trace
|
|
11
|
+
redacts every value of a credential it resolved, so the message read `The SMTP host "«redacted»"…`.
|
|
12
|
+
- **Credentials can be edited in the builder.** A pencil on each row opens the form with the name and the provider
|
|
13
|
+
fixed and nothing it holds shown: every value is entered again, and saving replaces all of them. SMTP credentials
|
|
14
|
+
are also labelled in the list.
|
|
15
|
+
|
|
16
|
+
- Updated dependencies
|
|
17
|
+
- @plitzi/plitzi-sdk@0.35.7
|
|
18
|
+
- @plitzi/sdk-shared@0.35.7
|
|
19
|
+
|
|
20
|
+
## 0.35.6
|
|
21
|
+
|
|
22
|
+
### Patch Changes
|
|
23
|
+
|
|
24
|
+
- A failed server action can give back what it already did.
|
|
25
|
+
|
|
26
|
+
- **`flow.onFailure` ("On Failure") marks where the undo begins.** A run that reaches it has succeeded and ends there.
|
|
27
|
+
A run whose step failed jumps to it and runs the steps after it, in order, each still asking its own `when` — the
|
|
28
|
+
failure may have come before the thing to undo was ever done — with `{{ failure.step }}` and `{{ failure.message }}`
|
|
29
|
+
in scope. The run still ends failed, with the failure it had; an undo step that fails too is added to it.
|
|
30
|
+
- **The undo runs on its own clock and budget:** 5 seconds (or the run's own timeout if shorter) and its own outbound
|
|
31
|
+
request budget, because the run's may be exactly what ran out, and a caller closing the connection does not stop it.
|
|
32
|
+
A run that hit its deadline is undone too.
|
|
33
|
+
- **`defineAction` writes it from `onFailure: [...]`**, after the answer. `validateActionDocument` refuses an output
|
|
34
|
+
step or a second handler after one, warns about a handler with nothing to undo or nothing that undoes, and reserves
|
|
35
|
+
`failure` as a step id. `FAILURE_HANDLER_TASK` is exported from `@plitzi/sdk-shared/actions`.
|
|
36
|
+
|
|
37
|
+
- Updated dependencies
|
|
38
|
+
- @plitzi/plitzi-sdk@0.35.6
|
|
39
|
+
- @plitzi/sdk-shared@0.35.6
|
|
40
|
+
|
|
3
41
|
## 0.35.5
|
|
4
42
|
|
|
5
43
|
### Patch Changes
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { triggerParams } from "./triggers.js";
|
|
2
|
-
import { hasValidToken } from "@plitzi/sdk-shared/helpers/twigWrapper";
|
|
3
2
|
import { parseCron, readSmtpCredential, triggerHasStaleVerify, triggerVerify } from "@plitzi/sdk-shared/actions";
|
|
3
|
+
import { hasValidToken } from "@plitzi/sdk-shared/helpers/twigWrapper";
|
|
4
4
|
//#region src/modules/actions/runtime/check.ts
|
|
5
5
|
/** A value the check can reason about: a literal the author typed, not a template resolved at run time. */
|
|
6
6
|
var literal = (value) => {
|
|
@@ -63,11 +63,11 @@ var createEmailSender = ({ kv, dailyLimitPerSpace = 200, allowPrivateHosts = fal
|
|
|
63
63
|
try {
|
|
64
64
|
addresses = await lookup(smtp.host);
|
|
65
65
|
} catch {
|
|
66
|
-
throw new Error(
|
|
66
|
+
throw new Error("its SMTP host does not resolve");
|
|
67
67
|
}
|
|
68
68
|
const [first] = addresses;
|
|
69
|
-
if (addresses.length === 0) throw new Error(
|
|
70
|
-
if (!allowPrivateHosts && (isBlockedHost(smtp.host) || addresses.some(({ address }) => isBlockedHost(address)))) throw new Error(
|
|
69
|
+
if (addresses.length === 0) throw new Error("its SMTP host does not resolve");
|
|
70
|
+
if (!allowPrivateHosts && (isBlockedHost(smtp.host) || addresses.some(({ address }) => isBlockedHost(address)))) throw new Error("its SMTP host is on a private network, which this server does not connect to");
|
|
71
71
|
return (delivery) => deliver({
|
|
72
72
|
address: first.address,
|
|
73
73
|
smtp: delivery.smtp,
|
|
@@ -8,6 +8,7 @@ import { createEmailSender } from "./email.js";
|
|
|
8
8
|
import { createRedactor, projectUser } from "./scope.js";
|
|
9
9
|
import { precheckRun } from "./precheck.js";
|
|
10
10
|
import onAbort from "../../../helpers/onAbort.js";
|
|
11
|
+
import { FAILURE_HANDLER_TASK } from "@plitzi/sdk-shared/actions";
|
|
11
12
|
import { evaluateRuleGroup } from "@plitzi/sdk-shared/helpers/ruleEvaluator";
|
|
12
13
|
import { hasValidToken, processTwig } from "@plitzi/sdk-shared/helpers/twigWrapper";
|
|
13
14
|
//#region src/modules/actions/runtime/runAction.ts
|
|
@@ -16,6 +17,14 @@ var MAX_TWIG_RESOLUTION_PASSES = 5;
|
|
|
16
17
|
* boundaries a flow actually has, and it keeps a long run to sixty reads a minute. */
|
|
17
18
|
var CANCEL_POLL_MS = 1e3;
|
|
18
19
|
/**
|
|
20
|
+
* The most a failed run's undo may take, on a clock of its own.
|
|
21
|
+
*
|
|
22
|
+
* Its own because the run's clock may be exactly what ran out: a run that hit its deadline has seats to give back
|
|
23
|
+
* too. Short because the caller is still waiting for the failure, and giving back what a flow took is a handful of
|
|
24
|
+
* writes, not another run. Never longer than the run itself was allowed.
|
|
25
|
+
*/
|
|
26
|
+
var UNDO_TIMEOUT_MS = 5e3;
|
|
27
|
+
/**
|
|
19
28
|
* The run's deadline, as a promise that loses patience.
|
|
20
29
|
*
|
|
21
30
|
* Aborting the controller is the polite half: a task that watches its signal stops, and the loop refuses to start
|
|
@@ -167,6 +176,21 @@ var errorNode = (action) => ({
|
|
|
167
176
|
flowId: "",
|
|
168
177
|
enabled: true
|
|
169
178
|
});
|
|
179
|
+
/**
|
|
180
|
+
* Where a chain's undo begins, if it has one: the first `flow.onFailure` its steps reach.
|
|
181
|
+
*
|
|
182
|
+
* Per trigger, because each way in walks its own chain. Walked with a visited set: the validator refuses a chain that
|
|
183
|
+
* comes back on itself, but a stored document is still customer input, and a lookup must not be what spins.
|
|
184
|
+
*/
|
|
185
|
+
var failureHandlerOf = (nodes, trigger) => {
|
|
186
|
+
const visited = /* @__PURE__ */ new Set();
|
|
187
|
+
let next = nodes[trigger.afterNode];
|
|
188
|
+
while (next && !visited.has(next.id)) {
|
|
189
|
+
if (next.action === FAILURE_HANDLER_TASK) return next;
|
|
190
|
+
visited.add(next.id);
|
|
191
|
+
next = nodes[next.afterNode];
|
|
192
|
+
}
|
|
193
|
+
};
|
|
170
194
|
var withDefaults = (task, params) => Object.entries(task.params).reduce((acum, [key, param]) => {
|
|
171
195
|
if (acum[key] === void 0 || acum[key] === "") acum[key] = param.defaultValue ?? "";
|
|
172
196
|
return acum;
|
|
@@ -229,15 +253,21 @@ var createActionRunner = (config, registry, baseFetch = fetch) => {
|
|
|
229
253
|
const deadline = createDeadline(request.emit ? limits.streamTimeoutMs : limits.timeoutMs, controller);
|
|
230
254
|
const releaseOuter = onAbort(request.signal, () => controller.abort());
|
|
231
255
|
const scopedKv = namespaceKv(kv, request.spaceId);
|
|
232
|
-
const
|
|
233
|
-
|
|
256
|
+
const lineage = [...request.lineage ?? [], entry.id];
|
|
257
|
+
/**
|
|
258
|
+
* What a step runs with, for one abort signal and one outbound budget.
|
|
259
|
+
*
|
|
260
|
+
* Two of them per run at most: the flow's, and — only when it failed — the undo's, which must still be able to
|
|
261
|
+
* reach the outside world after the flow's signal was aborted or its request budget spent.
|
|
262
|
+
*/
|
|
263
|
+
const contextFor = (signal, runFetch) => (scope) => ({
|
|
234
264
|
runId,
|
|
235
265
|
spaceId: request.spaceId,
|
|
236
266
|
environment: request.environment,
|
|
237
267
|
trigger: request.trigger,
|
|
238
268
|
user: request.user,
|
|
239
269
|
callerId: request.callerId,
|
|
240
|
-
signal
|
|
270
|
+
signal,
|
|
241
271
|
scope,
|
|
242
272
|
/**
|
|
243
273
|
* The secret a STEP asked for, resolved inside that step and never in the flow scope.
|
|
@@ -268,6 +298,7 @@ var createActionRunner = (config, registry, baseFetch = fetch) => {
|
|
|
268
298
|
email: emailSender,
|
|
269
299
|
emit: (chunk) => request.emit?.(redact(chunk))
|
|
270
300
|
});
|
|
301
|
+
const buildContext = contextFor(controller.signal, createRunFetch(baseFetch, controller.signal, limits, lineage));
|
|
271
302
|
const trace = [];
|
|
272
303
|
const startedAt = Date.now();
|
|
273
304
|
let failure;
|
|
@@ -304,11 +335,15 @@ var createActionRunner = (config, registry, baseFetch = fetch) => {
|
|
|
304
335
|
* hits its deadline answers, releases its slot and closes its connection while whatever step ignored the abort
|
|
305
336
|
* carries on in the background — which is the difference between a stuck STEP and a stuck server.
|
|
306
337
|
*/
|
|
338
|
+
const failureHandler = failureHandlerOf(document.nodes, triggerNode);
|
|
339
|
+
/** The step the flow was running when it stopped — what `{{ failure.step }}` names to the undo. */
|
|
340
|
+
let stepInFlight = "";
|
|
307
341
|
const runFlow = async () => {
|
|
308
342
|
let current = triggerNode;
|
|
309
343
|
let executed = 0;
|
|
310
344
|
let next = document.nodes[current.afterNode];
|
|
311
345
|
while (next) {
|
|
346
|
+
if (next.action === FAILURE_HANDLER_TASK) break;
|
|
312
347
|
if (controller.signal.aborted) {
|
|
313
348
|
status = "aborted";
|
|
314
349
|
break;
|
|
@@ -321,6 +356,7 @@ var createActionRunner = (config, registry, baseFetch = fetch) => {
|
|
|
321
356
|
executed += 1;
|
|
322
357
|
if (executed > limits.maxNodes) throw new ActionRunError("over_capacity", `Action exceeded its ${limits.maxNodes} step budget`);
|
|
323
358
|
const startTime = Date.now();
|
|
359
|
+
stepInFlight = next.id;
|
|
324
360
|
const outcome = await runNode(next, scope, registry, buildContext);
|
|
325
361
|
trace.push({
|
|
326
362
|
node: next,
|
|
@@ -337,6 +373,68 @@ var createActionRunner = (config, registry, baseFetch = fetch) => {
|
|
|
337
373
|
next = document.nodes[current.afterNode];
|
|
338
374
|
}
|
|
339
375
|
};
|
|
376
|
+
/**
|
|
377
|
+
* Gives back what a run that did not finish already did: the steps after `flow.onFailure`.
|
|
378
|
+
*
|
|
379
|
+
* On a clock and an outbound budget of its own, because the run's may be exactly what ran out, and deliberately
|
|
380
|
+
* NOT tied to the caller's connection — somebody closing the tab is the last reason to leave seats taken. Each step
|
|
381
|
+
* still asks its own `when`: the failure may have come before the thing to undo was ever done. Nothing it does
|
|
382
|
+
* changes the run's status or its answer; an undo that fails too is added to the failure, never put in its place.
|
|
383
|
+
*/
|
|
384
|
+
const undo = async (handler) => {
|
|
385
|
+
const undoController = new AbortController();
|
|
386
|
+
const undoDeadline = createDeadline(Math.min(limits.timeoutMs, UNDO_TIMEOUT_MS), undoController);
|
|
387
|
+
const buildUndoContext = contextFor(undoController.signal, createRunFetch(baseFetch, undoController.signal, limits, lineage));
|
|
388
|
+
scope.failure = {
|
|
389
|
+
step: stepInFlight,
|
|
390
|
+
message: failure ?? "",
|
|
391
|
+
status
|
|
392
|
+
};
|
|
393
|
+
trace.push({
|
|
394
|
+
node: handler,
|
|
395
|
+
status: "success",
|
|
396
|
+
result: redact(scope.failure),
|
|
397
|
+
postCallbacks: [],
|
|
398
|
+
startTime: Date.now(),
|
|
399
|
+
endTime: Date.now()
|
|
400
|
+
});
|
|
401
|
+
const runUndo = async () => {
|
|
402
|
+
let executed = 0;
|
|
403
|
+
let next = document.nodes[handler.afterNode];
|
|
404
|
+
while (next) {
|
|
405
|
+
executed += 1;
|
|
406
|
+
if (executed > limits.maxNodes) throw new ActionRunError("over_capacity", `Undoing the action exceeded its ${limits.maxNodes} step budget`);
|
|
407
|
+
const startTime = Date.now();
|
|
408
|
+
const outcome = await runNode(next, scope, registry, buildUndoContext);
|
|
409
|
+
trace.push({
|
|
410
|
+
node: next,
|
|
411
|
+
status: outcome.status,
|
|
412
|
+
result: redact(outcome.result),
|
|
413
|
+
postCallbacks: [],
|
|
414
|
+
startTime,
|
|
415
|
+
endTime: Date.now()
|
|
416
|
+
});
|
|
417
|
+
scope[next.id] = outcome.result;
|
|
418
|
+
next = document.nodes[next.afterNode];
|
|
419
|
+
}
|
|
420
|
+
};
|
|
421
|
+
try {
|
|
422
|
+
await Promise.race([runUndo(), undoDeadline.promise]);
|
|
423
|
+
} catch (error) {
|
|
424
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
425
|
+
trace.push({
|
|
426
|
+
node: errorNode("undo"),
|
|
427
|
+
status: "failed",
|
|
428
|
+
result: redact({ error: message }),
|
|
429
|
+
postCallbacks: [],
|
|
430
|
+
startTime: Date.now(),
|
|
431
|
+
endTime: Date.now()
|
|
432
|
+
});
|
|
433
|
+
failure = `${failure ?? "Action failed"} — and undoing it failed too: ${message}`;
|
|
434
|
+
} finally {
|
|
435
|
+
undoDeadline.clear();
|
|
436
|
+
}
|
|
437
|
+
};
|
|
340
438
|
/** Set when the run ends in a way the CALLER has to hear as a status code rather than as a result. */
|
|
341
439
|
let fatal;
|
|
342
440
|
try {
|
|
@@ -380,6 +478,7 @@ var createActionRunner = (config, registry, baseFetch = fetch) => {
|
|
|
380
478
|
deadline.clear();
|
|
381
479
|
releaseOuter();
|
|
382
480
|
}
|
|
481
|
+
if (status !== "completed" && failureHandler?.enabled) await undo(failureHandler);
|
|
383
482
|
const output = returned ?? {};
|
|
384
483
|
await record({
|
|
385
484
|
runId,
|
|
@@ -54,12 +54,17 @@ var emailTasks = [{
|
|
|
54
54
|
if (!stored) throw new Error(`This space has no SMTP credential called "${identifier}" — add it in Credentials`);
|
|
55
55
|
const reading = readSmtpCredential(stored);
|
|
56
56
|
if (!("settings" in reading)) throw new Error(`Credential "${identifier}" is not a usable SMTP server: ${reading.problems.map((problem) => `${problem.key} — ${problem.message}`).join("; ")}`);
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
57
|
+
try {
|
|
58
|
+
await ctx.email.send(ctx.spaceId, reading.settings, {
|
|
59
|
+
to: recipient,
|
|
60
|
+
subject: title,
|
|
61
|
+
text,
|
|
62
|
+
...answerTo === "" ? {} : { replyTo: answerTo }
|
|
63
|
+
}, ctx.signal);
|
|
64
|
+
} catch (error) {
|
|
65
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
66
|
+
throw new Error(`Could not send through the SMTP credential "${identifier}": ${reason}`, { cause: error });
|
|
67
|
+
}
|
|
63
68
|
return { sent: true };
|
|
64
69
|
}
|
|
65
70
|
}];
|
|
@@ -46,6 +46,24 @@ var fail = {
|
|
|
46
46
|
}
|
|
47
47
|
};
|
|
48
48
|
/**
|
|
49
|
+
* Where a flow's undo begins — read by the runner, never run as a step.
|
|
50
|
+
*
|
|
51
|
+
* A run that reaches it has succeeded and ends there. A run whose step failed before it jumps here instead and runs
|
|
52
|
+
* the steps after it, each still asking its own `when` — the failure may have come before the thing to undo was ever
|
|
53
|
+
* done — and still ends as failed, with the failure it had. What failed is in their scope as `{{ failure.step }}` and
|
|
54
|
+
* `{{ failure.message }}`.
|
|
55
|
+
*/
|
|
56
|
+
var onFailure = {
|
|
57
|
+
namespace: "flow",
|
|
58
|
+
action: "onFailure",
|
|
59
|
+
title: "On Failure",
|
|
60
|
+
description: "The steps after this one run only when a step before it fails, to undo what the flow already did",
|
|
61
|
+
params: {},
|
|
62
|
+
run: () => {
|
|
63
|
+
throw new Error("On Failure marks where a failed run’s undo begins; the runner never runs it as a step");
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
/**
|
|
49
67
|
* Declares what the caller gets back — and IS the declaration.
|
|
50
68
|
*
|
|
51
69
|
* The flow scope is not the answer: it holds every node's raw result, including whatever a fetch happened to
|
|
@@ -102,7 +120,8 @@ var emit = {
|
|
|
102
120
|
var flowTasks = [
|
|
103
121
|
delay,
|
|
104
122
|
fail,
|
|
105
|
-
output
|
|
123
|
+
output,
|
|
124
|
+
onFailure
|
|
106
125
|
];
|
|
107
126
|
var streamTasks = [emit];
|
|
108
127
|
//#endregion
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@plitzi/sdk-server",
|
|
3
|
-
"version": "0.35.
|
|
3
|
+
"version": "0.35.7",
|
|
4
4
|
"license": "AGPL-3.0",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist",
|
|
@@ -58,8 +58,8 @@
|
|
|
58
58
|
"type": "module",
|
|
59
59
|
"sideEffects": false,
|
|
60
60
|
"dependencies": {
|
|
61
|
-
"@plitzi/plitzi-sdk": "0.35.
|
|
62
|
-
"@plitzi/sdk-shared": "0.35.
|
|
61
|
+
"@plitzi/plitzi-sdk": "0.35.7",
|
|
62
|
+
"@plitzi/sdk-shared": "0.35.7",
|
|
63
63
|
"ejs": "^6.0.1",
|
|
64
64
|
"esbuild": "^0.28.2",
|
|
65
65
|
"jsonwebtoken": "^9.0.3",
|