@plitzi/sdk-server 0.35.3 → 0.35.4

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,14 @@
1
1
  # @plitzi/sdk-server
2
2
 
3
+ ## 0.35.4
4
+
5
+ ### Patch Changes
6
+
7
+ - v0.35.4
8
+ - Updated dependencies
9
+ - @plitzi/plitzi-sdk@0.35.4
10
+ - @plitzi/sdk-shared@0.35.4
11
+
3
12
  ## 0.35.3
4
13
 
5
14
  ### Patch Changes
package/dist/actions.d.ts CHANGED
@@ -55,7 +55,8 @@ export { createKvStore } from './modules/actions/runtime/kvStore';
55
55
  * and still wants refusals on the log stream wires one of each; that is the normal shape, not a workaround.
56
56
  */
57
57
  export { createRunLogger, createRejectLogger } from './modules/actions/runtime/runLogger';
58
- export type { ActionCredential, ActionDbDriver, ActionKvAdapter, ActionKvStore, ActionLookups, ActionRejectRecord, ActionRunRecord, ActionRunRequest, ActionRunResult, ActionsConfig, ActionTask, ActionTaskContext, ActionTaskRegistry, RegisteredTask, ResolvedConnector } from './modules/actions/types';
58
+ export type { TaskRegistryOptions } from './modules/actions/tasks/registry';
59
+ export type { ActionCredential, ActionDbDriver, ActionEmailAdapter, ActionEmailMessage, ActionKvAdapter, ActionKvStore, ActionLookups, ActionRejectRecord, ActionRunRecord, ActionRunRequest, ActionRunResult, ActionsConfig, ActionTask, ActionTaskContext, ActionTaskRegistry, RegisteredTask, ResolvedConnector } from './modules/actions/types';
59
60
  export type { ActionsModule } from './modules/actions';
60
61
  export type { ActiveRun, RunGuards } from './modules/actions/runtime/guards';
61
62
  export type { ScheduleResult, ScheduleRunner, ScheduleTick } from './modules/actions/runtime/schedule';
@@ -37,4 +37,5 @@ export { handleActionCancel } from './transport/cancelHandler';
37
37
  export type { ActiveRun, RunGuards } from './runtime/guards';
38
38
  export type { ScheduleResult, ScheduleRunner, ScheduleTick } from './runtime/schedule';
39
39
  export type { ActionTaskDescriptor } from './taskCatalog';
40
- export type { ActionCredential, ActionDbDriver, ActionKvStore, ActionLookups, ActionRejectRecord, ActionRunRecord, ActionRunRequest, ActionRunResult, ActionsConfig, ActionTask, ActionTaskContext, ActionTaskRegistry, RegisteredTask, ResolvedConnector } from './types';
40
+ export type { TaskRegistryOptions } from './tasks/registry';
41
+ export type { ActionCredential, ActionDbDriver, ActionEmailAdapter, ActionEmailMessage, ActionKvStore, ActionLookups, ActionRejectRecord, ActionRunRecord, ActionRunRequest, ActionRunResult, ActionsConfig, ActionTask, ActionTaskContext, ActionTaskRegistry, RegisteredTask, ResolvedConnector } from './types';
@@ -25,7 +25,10 @@ import { handleActionCancel } from "./transport/cancelHandler.js";
25
25
  * the same as having no single-flight at all. Nothing outside this folder needs to know how a run is assembled.
26
26
  */
27
27
  var createActionsModule = (config) => {
28
- const registry = createTaskRegistry(config.tasks, (config.dbDrivers?.length ?? 0) > 0);
28
+ const registry = createTaskRegistry(config.tasks, {
29
+ db: (config.dbDrivers?.length ?? 0) > 0,
30
+ email: config.email !== void 0
31
+ });
29
32
  const { runAction } = createActionRunner(config, registry, config.fetchImpl);
30
33
  /**
31
34
  * Single-flight over the store the deployment already gave the `kv` tasks — its own Redis, table or whatever it
@@ -26,6 +26,7 @@ var actionsModuleFor = (config) => {
26
26
  rateLimit: config.action?.rateLimit,
27
27
  idempotency: config.action?.idempotency,
28
28
  dbDrivers: config.action?.dbDrivers,
29
+ email: config.action?.email,
29
30
  onRun: config.action?.onRun
30
31
  });
31
32
  modules.set(config, module);
@@ -260,6 +260,7 @@ var createActionRunner = (config, registry, baseFetch = fetch) => {
260
260
  fetch: runFetch,
261
261
  kv: scopedKv,
262
262
  dbDrivers: config.dbDrivers ?? [],
263
+ email: config.email,
263
264
  emit: (chunk) => request.emit?.(redact(chunk))
264
265
  });
265
266
  const trace = [];
@@ -280,7 +281,13 @@ var createActionRunner = (config, registry, baseFetch = fetch) => {
280
281
  spaceId: request.spaceId,
281
282
  environment: request.environment,
282
283
  trigger: request.trigger,
283
- runId
284
+ runId,
285
+ /**
286
+ * The moment the run started, as an ISO string — and the same moment for every step. A check that asks the
287
+ * time and the write it guards cannot straddle midnight, and a template reads it in the zone it cares about
288
+ * with `{{ now|date('Y-m-d', 'Europe/Madrid') }}`.
289
+ */
290
+ now: new Date(startedAt).toISOString()
284
291
  };
285
292
  let status = "completed";
286
293
  let returned;
@@ -2,7 +2,8 @@ import { ActionTask } from '../types';
2
2
  /**
3
3
  * The tasks `sdk-server` itself ships: the ones that are mechanism rather than policy.
4
4
  *
5
- * Anything needing an account somewhere — email, AI, object storage — is the DEPLOYMENT's, registered through
6
- * `actions.tasks`. A server with no mail transport must not offer a step whose only possible outcome is failure.
5
+ * Anything needing an account somewhere — AI, object storage — is the DEPLOYMENT's, registered through
6
+ * `actions.tasks`. `db.query` and `email.send` sit in between: the task is shipped because its rules are the same
7
+ * everywhere, the driver or transport is the deployment's, and the registry offers the task only when one was given.
7
8
  */
8
9
  export declare const builtinTasks: ActionTask<Record<string, unknown>>[];
@@ -8,8 +8,9 @@ import { transformTasks } from "./transform.js";
8
8
  /**
9
9
  * The tasks `sdk-server` itself ships: the ones that are mechanism rather than policy.
10
10
  *
11
- * Anything needing an account somewhere — email, AI, object storage — is the DEPLOYMENT's, registered through
12
- * `actions.tasks`. A server with no mail transport must not offer a step whose only possible outcome is failure.
11
+ * Anything needing an account somewhere — AI, object storage — is the DEPLOYMENT's, registered through
12
+ * `actions.tasks`. `db.query` and `email.send` sit in between: the task is shipped because its rules are the same
13
+ * everywhere, the driver or transport is the deployment's, and the registry offers the task only when one was given.
13
14
  */
14
15
  var builtinTasks = [
15
16
  ...flowTasks,
@@ -0,0 +1,2 @@
1
+ import { ActionTask } from '../types';
2
+ export declare const emailTasks: ActionTask<Record<string, unknown>>[];
@@ -0,0 +1,62 @@
1
+ //#region src/modules/actions/tasks/email.ts
2
+ /**
3
+ * One address, and nothing a mail header could be split on.
4
+ *
5
+ * Stricter than the standard on purpose: a comma or a semicolon is how one recipient becomes a hundred, and a line
6
+ * break is how a subject becomes a second header. A flow renders what a visitor typed, so this is where that stops.
7
+ */
8
+ var ADDRESS = /^[^\s@,;<>"]+@[^\s@,;<>"]+\.[^\s@,;<>"]+$/;
9
+ var MAX_SUBJECT = 200;
10
+ var MAX_TEXT = 2e4;
11
+ var emailTasks = [{
12
+ namespace: "email",
13
+ action: "send",
14
+ title: "Send Email",
15
+ description: "Sends a plain-text message through the transport this deployment configured",
16
+ params: {
17
+ to: {
18
+ type: "text",
19
+ canBind: true,
20
+ defaultValue: "",
21
+ label: "To"
22
+ },
23
+ subject: {
24
+ type: "text",
25
+ canBind: true,
26
+ defaultValue: "",
27
+ label: "Subject"
28
+ },
29
+ text: {
30
+ type: "codemirror-text",
31
+ canBind: true,
32
+ defaultValue: "",
33
+ label: "Text"
34
+ },
35
+ replyTo: {
36
+ type: "text",
37
+ canBind: true,
38
+ defaultValue: "",
39
+ label: "Reply to"
40
+ }
41
+ },
42
+ run: async ({ to, subject, text, replyTo }, ctx) => {
43
+ if (!ctx.email) throw new Error("This server has no email transport");
44
+ const recipient = to.trim();
45
+ if (!ADDRESS.test(recipient)) throw new Error("The recipient is not one email address");
46
+ const answerTo = replyTo.trim();
47
+ if (answerTo !== "" && !ADDRESS.test(answerTo)) throw new Error("The reply-to is not one email address");
48
+ const title = subject.trim();
49
+ if (title === "" || /[\r\n]/.test(title) || title.length > MAX_SUBJECT) throw new Error(`The subject must be one line of 1 to ${MAX_SUBJECT} characters`);
50
+ if (text.trim() === "" || text.length > MAX_TEXT) throw new Error(`The text must have 1 to ${MAX_TEXT} characters`);
51
+ await ctx.email.send({
52
+ spaceId: ctx.spaceId,
53
+ to: recipient,
54
+ subject: title,
55
+ text,
56
+ ...answerTo === "" ? {} : { replyTo: answerTo }
57
+ });
58
+ return { sent: true };
59
+ }
60
+ }];
61
+ //#endregion
62
+ export { emailTasks };
@@ -0,0 +1 @@
1
+ export {};
@@ -1,5 +1,12 @@
1
1
  import { ActionTask, ActionTaskRegistry } from '../types';
2
2
  export declare const taskName: (task: Pick<ActionTask, "namespace" | "action">) => string;
3
+ /** The shipped tasks that are only real when the deployment supplied what they run through. */
4
+ export type TaskRegistryOptions = {
5
+ /** At least one database driver is registered, so `db.query` has an engine to run against. */
6
+ db?: boolean;
7
+ /** An email transport is configured, so `email.send` has somewhere to hand a message. */
8
+ email?: boolean;
9
+ };
3
10
  /**
4
11
  * Builds the set of tasks this server can run.
5
12
  *
@@ -7,4 +14,4 @@ export declare const taskName: (task: Pick<ActionTask, "namespace" | "action">)
7
14
  * name is unreachable from any document, and one shadowing a built-in silently changes what every existing action
8
15
  * in that deployment does. Both are invisible until a run misbehaves in production.
9
16
  */
10
- export declare const createTaskRegistry: (custom?: ActionTask<never>[], hasDbDrivers?: boolean) => ActionTaskRegistry;
17
+ export declare const createTaskRegistry: (custom?: ActionTask<never>[], { db, email }?: TaskRegistryOptions) => ActionTaskRegistry;
@@ -1,9 +1,14 @@
1
1
  import { builtinTasks } from "./builtins.js";
2
2
  import { dbTasks } from "./db.js";
3
+ import { emailTasks } from "./email.js";
3
4
  //#region src/modules/actions/tasks/registry.ts
4
5
  var NAME_PATTERN = /^[a-z][a-zA-Z0-9]*$/;
5
6
  var taskName = (task) => `${task.namespace}.${task.action}`;
6
- var reservedNamespaces = new Set([...builtinTasks, ...dbTasks].map((task) => task.namespace));
7
+ var reservedNamespaces = new Set([
8
+ ...builtinTasks,
9
+ ...dbTasks,
10
+ ...emailTasks
11
+ ].map((task) => task.namespace));
7
12
  /**
8
13
  * Builds the set of tasks this server can run.
9
14
  *
@@ -11,9 +16,13 @@ var reservedNamespaces = new Set([...builtinTasks, ...dbTasks].map((task) => tas
11
16
  * name is unreachable from any document, and one shadowing a built-in silently changes what every existing action
12
17
  * in that deployment does. Both are invisible until a run misbehaves in production.
13
18
  */
14
- var createTaskRegistry = (custom = [], hasDbDrivers = false) => {
19
+ var createTaskRegistry = (custom = [], { db = false, email = false } = {}) => {
15
20
  const tasks = /* @__PURE__ */ new Map();
16
- (hasDbDrivers ? [...builtinTasks, ...dbTasks] : builtinTasks).forEach((task) => tasks.set(taskName(task), {
21
+ [
22
+ ...builtinTasks,
23
+ ...db ? dbTasks : [],
24
+ ...email ? emailTasks : []
25
+ ].forEach((task) => tasks.set(taskName(task), {
17
26
  ...task,
18
27
  name: taskName(task)
19
28
  }));
@@ -1,4 +1,4 @@
1
- import { ActionEntry, ActionLimits, ActionRejectRecord, ActionRunRecord, ActionRunStatus, ActionTriggerType, ConnectorManifest, Environment, InteractionCallbackParam, InteractionNode, InteractionNodeStatus, SpaceRevision, SSRUser } from '@plitzi/sdk-shared';
1
+ import { ActionEmailAdapter, ActionEntry, ActionLimits, ActionRejectRecord, ActionRunRecord, ActionRunStatus, ActionTriggerType, ConnectorManifest, Environment, InteractionCallbackParam, InteractionNode, InteractionNodeStatus, SpaceRevision, SSRUser } from '@plitzi/sdk-shared';
2
2
  /** Resolved secret material, as the connector engine already models it. */
3
3
  export type ActionCredential = Record<string, string>;
4
4
  /**
@@ -109,6 +109,8 @@ export type ActionTaskContext = {
109
109
  kv: ActionKvStore;
110
110
  /** The database engines this deployment registered, for the `db.query` task. */
111
111
  dbDrivers: ActionDbDriver[];
112
+ /** The transport this deployment gave the `email.send` task. Absent → that task is not offered at all. */
113
+ email?: ActionEmailAdapter;
112
114
  /** Pushes a `data` frame to a streaming caller. A no-op when nobody negotiated a stream. */
113
115
  emit: (chunk: unknown) => void;
114
116
  };
@@ -162,6 +164,8 @@ export type ActionsConfig = {
162
164
  };
163
165
  /** Database engines this deployment lets a flow reach. Empty → the `db.query` task is not offered at all. */
164
166
  dbDrivers?: ActionDbDriver[];
167
+ /** How a flow's mail leaves this server. Absent → the `email.send` task is not offered at all. */
168
+ email?: ActionEmailAdapter;
165
169
  /**
166
170
  * Called once per run that started, for a deployment that keeps a record.
167
171
  *
@@ -197,7 +201,7 @@ export type ActionsConfig = {
197
201
  };
198
202
  /** Re-exported so the module's own files import one place, and a deployment writing an `onRun` or an `onReject`
199
203
  * sees the same shapes the module emits. */
200
- export type { ActionRejectRecord, ActionRunRecord } from '@plitzi/sdk-shared';
204
+ export type { ActionEmailAdapter, ActionEmailMessage, ActionRejectRecord, ActionRunRecord } from '@plitzi/sdk-shared';
201
205
  export type ResolvedActionLimits = Required<ActionLimits>;
202
206
  export type ActionRunRequest = {
203
207
  entry: ActionEntry;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plitzi/sdk-server",
3
- "version": "0.35.3",
3
+ "version": "0.35.4",
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.3",
62
- "@plitzi/sdk-shared": "0.35.3",
61
+ "@plitzi/plitzi-sdk": "0.35.4",
62
+ "@plitzi/sdk-shared": "0.35.4",
63
63
  "ejs": "^6.0.1",
64
64
  "esbuild": "^0.28.2",
65
65
  "jsonwebtoken": "^9.0.3"