@sanoma-ai/workflows 0.10.11 → 0.11.0

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/src/login-code.ts CHANGED
@@ -1,14 +1,15 @@
1
1
  import { z } from "zod";
2
2
  import { defineWorkflow, type WorkflowDefinition } from "./define.ts";
3
+ import type { Login } from "./config.ts";
3
4
  import type { Op } from "./op.ts";
4
5
  import { openLoginCode } from "./seal.ts";
5
6
  import type { LOGIN_CODE_WORKFLOW } from "./shared.ts";
6
7
 
7
8
  // The built-in `login-code` workflow: one mail with a sign-in code, through the config's
8
- // `login.sender`. The app starts it as the runtime's agent `login` (`LOGIN_AGENT`) with the code
9
- // sealed (`sealLoginCode`), so neither the run's input nor the ledger shows it: the call is
10
- // recorded with its body as `<text>` (`recorded` in `call.ts`). It is a workflow like any other:
11
- // the call goes through `ctx`, under the policy, in the ledger.
9
+ // `login.sender`, from its `login.from`. The app starts it as the runtime's agent `login`
10
+ // (`LOGIN_AGENT`) with the code sealed (`sealLoginCode`), so neither the run's input nor the
11
+ // ledger shows it: the call is recorded with its body as `<text>` (`recorded` in `call.ts`). It
12
+ // is a workflow like any other: the call goes through `ctx`, under the policy, in the ledger.
12
13
 
13
14
  /** An operation on `ctx`. */
14
15
  type OpCall = (input: unknown) => Promise<unknown>;
@@ -16,17 +17,26 @@ type OpCall = (input: unknown) => Promise<unknown>;
16
17
  /** The workflow's input: to whom, and the code, sealed. */
17
18
  const LoginCodeInput = z.object({ to: z.string().min(1), code: z.string().min(1) });
18
19
 
19
- /** What a sign-in code is sent as, for checks before any is: a sender must take it, and the policy is asked about it. */
20
- export const CODE_MAIL = { to: "person@example.com", subject: "Your sign-in code", text: "<text>" };
20
+ /**
21
+ * The mail a sign-in code is sent as, from `from`, saying it is `company`'s: the workflow's, and,
22
+ * with a stand-in `to` and `text`, what is checked before any is sent (a sender must take it, and
23
+ * the policy is asked about it).
24
+ */
25
+ export const codeMail = (from: string, company: string, to = "person@example.com", text = "<text>") => ({
26
+ from,
27
+ to,
28
+ subject: `Your ${company} sign-in code`,
29
+ text,
30
+ });
21
31
 
22
32
  /** The mail's text: the code, what it is for, and how long it lasts; nothing else, and no link. */
23
33
  const textOf = (company: string, code: string) =>
24
34
  `Your sign-in code for ${company} is ${code}.\n\n` +
25
35
  "It works once, for five minutes. If you did not ask for it, ignore this mail.\n";
26
36
 
27
- /** The login-code workflow for a config: it sends through `sender`, saying it is `company`'s. */
37
+ /** The login-code workflow for a config: it sends through `sender`, from `from`, saying it is `company`'s. */
28
38
  export function loginCodeWorkflow(
29
- sender: Op,
39
+ { sender, from }: Login,
30
40
  company: string,
31
41
  ): WorkflowDefinition<readonly Op[], typeof LoginCodeInput> {
32
42
  return defineWorkflow({
@@ -38,11 +48,7 @@ export function loginCodeWorkflow(
38
48
  uses: [sender],
39
49
  run: async (ctx, input) => {
40
50
  const { vendor, resource, name } = sender;
41
- const mail = {
42
- to: input.to,
43
- subject: `Your ${company} sign-in code`,
44
- text: textOf(company, openLoginCode(input.code, input.to)),
45
- };
51
+ const mail = codeMail(from, company, input.to, textOf(company, openLoginCode(input.code, input.to)));
46
52
  // `ctx` called directly, so the outline places the call.
47
53
  return (ctx as unknown as Record<string, Record<string, Record<string, OpCall>>>)[vendor]![resource]![name]!(
48
54
  mail,
package/src/op.ts CHANGED
@@ -530,6 +530,21 @@ export function isOp(x: unknown): x is Op {
530
530
  return typeof x === "object" && x !== null && (x as Op).kind === "op";
531
531
  }
532
532
 
533
+ /**
534
+ * True when `op`'s input takes `sample` and keeps each of its fields: one that strips a field
535
+ * takes none of it. A wakeup's `register` and the config's `login.sender` are checked with it.
536
+ */
537
+ export function keepsInput(op: Op, sample: Record<string, unknown>): boolean {
538
+ try {
539
+ const parsed = op.input.safeParse(sample);
540
+ const data = parsed.data as Record<string, unknown> | undefined;
541
+ return parsed.success && Object.keys(sample).every((key) => data?.[key] !== undefined);
542
+ } catch {
543
+ // An async refinement, which a sync parse cannot run: no input the runtime can check.
544
+ return false;
545
+ }
546
+ }
547
+
533
548
  /**
534
549
  * What a driver for a connector must implement: every operation, by resource and name. Each
535
550
  * function gets the input as the operation's schema parses it and returns what its output
package/src/shared.ts CHANGED
@@ -613,6 +613,6 @@ export type OpFn<I, O> = (input: I) => Promise<O>;
613
613
  export interface CollectionApi<R> {
614
614
  get: OpFn<{ key: string }, Saved<R> | null>;
615
615
  list(query?: ListQuery<R>): Promise<Saved<R>[]>;
616
- save: OpFn<{ record: R; ifAbsent?: boolean }, Saved<R>>;
616
+ save: OpFn<{ record: R; ifAbsent?: boolean; ifRecord?: Record<string, unknown> }, Saved<R>>;
617
617
  delete: OpFn<{ key: string }, { deleted: boolean }>;
618
618
  }
package/src/wakeup.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import type { z } from "zod";
2
2
  import { hasPath } from "./host.ts";
3
- import type { Op } from "./op.ts";
3
+ import { keepsInput, type Op } from "./op.ts";
4
4
  import type { References, Resource } from "./resource.ts";
5
5
  import type { DeclaredResource } from "./resources.ts";
6
6
 
@@ -110,21 +110,6 @@ export function checkWakeup(
110
110
  /** A registration every register operation must take: the input `checkWakeupOps` tries. */
111
111
  const SAMPLE: WakeupRegistration = { at: new Date(0).toISOString() };
112
112
 
113
- /**
114
- * True when `op`'s input takes `sample` and keeps each of its fields: one that strips a field
115
- * takes none of it. A wakeup's `register` is checked with it.
116
- */
117
- function keepsInput(op: Op, sample: Record<string, unknown>): boolean {
118
- try {
119
- const parsed = op.input.safeParse(sample);
120
- const data = parsed.data as Record<string, unknown> | undefined;
121
- return parsed.success && Object.keys(sample).every((key) => data?.[key] !== undefined);
122
- } catch {
123
- // An async refinement, which a sync parse cannot run: no input the runtime can check.
124
- return false;
125
- }
126
- }
127
-
128
113
  /** The host types a wakeup type may name: its `inputs.host` reference's. */
129
114
  export const wakeupHostTypes = (type: Pick<Resource, "fields" | "wakeup">): string[] =>
130
115
  type.wakeup ? [type.fields.references?.[type.wakeup.inputs.host]?.type ?? []].flat() : [];