@sanoma-ai/workflows 0.1.0 → 0.4.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.
Files changed (184) hide show
  1. package/README.md +186 -76
  2. package/dist/apply-steps.d.ts +6 -0
  3. package/dist/apply-steps.js +93 -0
  4. package/dist/apply-steps.js.map +1 -0
  5. package/dist/apply.d.ts +197 -0
  6. package/dist/apply.js +470 -0
  7. package/dist/apply.js.map +1 -0
  8. package/dist/approvals.d.ts +11 -6
  9. package/dist/approvals.js +38 -10
  10. package/dist/approvals.js.map +1 -1
  11. package/dist/call-workflow.d.ts +16 -0
  12. package/dist/call-workflow.js +35 -0
  13. package/dist/call-workflow.js.map +1 -0
  14. package/dist/call.d.ts +35 -2
  15. package/dist/call.js +123 -43
  16. package/dist/call.js.map +1 -1
  17. package/dist/cedar.d.ts +87 -0
  18. package/dist/cedar.js +365 -0
  19. package/dist/cedar.js.map +1 -0
  20. package/dist/client.d.ts +123 -9
  21. package/dist/client.js +355 -34
  22. package/dist/client.js.map +1 -1
  23. package/dist/collection.d.ts +107 -0
  24. package/dist/collection.js +306 -0
  25. package/dist/collection.js.map +1 -0
  26. package/dist/config.d.ts +42 -14
  27. package/dist/config.js +109 -28
  28. package/dist/config.js.map +1 -1
  29. package/dist/constructors.d.ts +2 -0
  30. package/dist/constructors.js +5 -0
  31. package/dist/constructors.js.map +1 -0
  32. package/dist/credentials.d.ts +25 -0
  33. package/dist/credentials.js +73 -5
  34. package/dist/credentials.js.map +1 -1
  35. package/dist/data.d.ts +2 -0
  36. package/dist/data.js +3 -0
  37. package/dist/data.js.map +1 -0
  38. package/dist/datafile.d.ts +38 -3
  39. package/dist/datafile.js +166 -13
  40. package/dist/datafile.js.map +1 -1
  41. package/dist/db.d.ts +5 -3
  42. package/dist/db.js +88 -21
  43. package/dist/db.js.map +1 -1
  44. package/dist/define.d.ts +10 -4
  45. package/dist/define.js +1 -5
  46. package/dist/define.js.map +1 -1
  47. package/dist/describe.d.ts +54 -8
  48. package/dist/describe.js +84 -11
  49. package/dist/describe.js.map +1 -1
  50. package/dist/direct.d.ts +9 -0
  51. package/dist/direct.js +11 -0
  52. package/dist/direct.js.map +1 -0
  53. package/dist/drift.d.ts +64 -12
  54. package/dist/drift.js +118 -41
  55. package/dist/drift.js.map +1 -1
  56. package/dist/errors.d.ts +16 -1
  57. package/dist/errors.js +30 -1
  58. package/dist/errors.js.map +1 -1
  59. package/dist/expectations.d.ts +4 -4
  60. package/dist/expectations.js +43 -9
  61. package/dist/expectations.js.map +1 -1
  62. package/dist/host.d.ts +56 -0
  63. package/dist/host.js +45 -0
  64. package/dist/host.js.map +1 -0
  65. package/dist/index.d.ts +13 -5
  66. package/dist/index.js +15 -5
  67. package/dist/index.js.map +1 -1
  68. package/dist/ledger.d.ts +12 -1
  69. package/dist/ledger.js +18 -11
  70. package/dist/ledger.js.map +1 -1
  71. package/dist/lint.d.ts +5 -4
  72. package/dist/lint.js +24 -7
  73. package/dist/lint.js.map +1 -1
  74. package/dist/lockfile.d.ts +36 -0
  75. package/dist/lockfile.js +69 -0
  76. package/dist/lockfile.js.map +1 -0
  77. package/dist/op.d.ts +9 -0
  78. package/dist/op.js.map +1 -1
  79. package/dist/outline.d.ts +2 -0
  80. package/dist/outline.js +1 -1
  81. package/dist/outline.js.map +1 -1
  82. package/dist/paths.d.ts +4 -0
  83. package/dist/paths.js +17 -3
  84. package/dist/paths.js.map +1 -1
  85. package/dist/person.d.ts +61 -0
  86. package/dist/person.js +69 -0
  87. package/dist/person.js.map +1 -0
  88. package/dist/plugin.js +1 -1
  89. package/dist/plugin.js.map +1 -1
  90. package/dist/policy.d.ts +51 -55
  91. package/dist/policy.js +14 -36
  92. package/dist/policy.js.map +1 -1
  93. package/dist/resource.d.ts +207 -25
  94. package/dist/resource.js +184 -30
  95. package/dist/resource.js.map +1 -1
  96. package/dist/resources.d.ts +76 -9
  97. package/dist/resources.js +170 -30
  98. package/dist/resources.js.map +1 -1
  99. package/dist/run.d.ts +31 -1
  100. package/dist/run.js +6 -1
  101. package/dist/run.js.map +1 -1
  102. package/dist/sandbox.js +5 -1
  103. package/dist/sandbox.js.map +1 -1
  104. package/dist/scenario.d.ts +25 -9
  105. package/dist/scenario.js +125 -27
  106. package/dist/scenario.js.map +1 -1
  107. package/dist/scenarios.d.ts +10 -6
  108. package/dist/scenarios.js +8 -4
  109. package/dist/scenarios.js.map +1 -1
  110. package/dist/schema.d.ts +32 -0
  111. package/dist/schema.js +225 -0
  112. package/dist/schema.js.map +1 -0
  113. package/dist/sdk.d.ts +34 -0
  114. package/dist/sdk.js +168 -0
  115. package/dist/sdk.js.map +1 -0
  116. package/dist/secret.d.ts +42 -0
  117. package/dist/secret.js +58 -0
  118. package/dist/secret.js.map +1 -0
  119. package/dist/shared.d.ts +188 -4
  120. package/dist/shared.js +165 -5
  121. package/dist/shared.js.map +1 -1
  122. package/dist/sites.d.ts +52 -0
  123. package/dist/sites.js +174 -0
  124. package/dist/sites.js.map +1 -0
  125. package/dist/source.d.ts +58 -0
  126. package/dist/source.js +54 -0
  127. package/dist/source.js.map +1 -0
  128. package/dist/version.d.ts +3 -2
  129. package/dist/version.js +3 -2
  130. package/dist/version.js.map +1 -1
  131. package/dist/worker.d.ts +6 -2
  132. package/dist/worker.js +37 -8
  133. package/dist/worker.js.map +1 -1
  134. package/dist/writer.d.ts +35 -0
  135. package/dist/writer.js +201 -0
  136. package/dist/writer.js.map +1 -0
  137. package/oxlint.json +5 -16
  138. package/package.json +11 -1
  139. package/src/apply-steps.ts +110 -0
  140. package/src/apply.ts +626 -0
  141. package/src/approvals.ts +49 -13
  142. package/src/call-workflow.ts +44 -0
  143. package/src/call.ts +173 -46
  144. package/src/cedar.ts +493 -0
  145. package/src/client.ts +450 -49
  146. package/src/collection.ts +431 -0
  147. package/src/config.ts +168 -48
  148. package/src/constructors.ts +4 -0
  149. package/src/credentials.ts +90 -5
  150. package/src/data.ts +10 -0
  151. package/src/datafile.ts +207 -15
  152. package/src/db.ts +90 -22
  153. package/src/define.ts +11 -9
  154. package/src/describe.ts +146 -23
  155. package/src/direct.ts +15 -0
  156. package/src/drift.ts +155 -45
  157. package/src/errors.ts +36 -1
  158. package/src/expectations.ts +46 -11
  159. package/src/host.ts +96 -0
  160. package/src/index.ts +56 -2
  161. package/src/ledger.ts +35 -17
  162. package/src/lint.ts +29 -12
  163. package/src/lockfile.ts +95 -0
  164. package/src/op.ts +9 -0
  165. package/src/outline.ts +1 -1
  166. package/src/paths.ts +18 -3
  167. package/src/person.ts +75 -0
  168. package/src/plugin.ts +1 -1
  169. package/src/policy.ts +59 -63
  170. package/src/resource.ts +302 -52
  171. package/src/resources.ts +226 -30
  172. package/src/run.ts +33 -1
  173. package/src/sandbox.ts +5 -1
  174. package/src/scenario.ts +175 -43
  175. package/src/scenarios.ts +15 -8
  176. package/src/schema.ts +261 -0
  177. package/src/sdk.ts +211 -0
  178. package/src/secret.ts +92 -0
  179. package/src/shared.ts +279 -6
  180. package/src/sites.ts +220 -0
  181. package/src/source.ts +82 -0
  182. package/src/version.ts +3 -2
  183. package/src/worker.ts +45 -11
  184. package/src/writer.ts +225 -0
package/src/person.ts ADDED
@@ -0,0 +1,75 @@
1
+ import { z } from "zod";
2
+ import { defineConnector } from "./op.ts";
3
+ import { defineResource } from "./resource.ts";
4
+
5
+ // Who exists in a company: people, agents and groups, declared in data files like any resource,
6
+ // as the runtime's own vendor's types (`sanoma.person`, `sanoma.agent`, `sanoma.group`, imported
7
+ // from "sanoma/resources"). They are `vendorless`: the repo is all there is of them, so they have
8
+ // no driver and no drift. A membership is written on a group only, its members references to the
9
+ // declared people and agents; every agent is in the group `agents` besides. Who a principal is,
10
+ // read from them, is `principalOf` (`shared.ts`).
11
+
12
+ /** An id or a group's name: one word, as a scenario names an approver. */
13
+ const WORD = z.string().regex(/^\S+$/, 'is one word, such as "ada"');
14
+ const NONE = { immutable: [], vendorOwned: [], writeOnly: [] };
15
+
16
+ const person = defineResource({
17
+ vendor: "sanoma",
18
+ type: "person",
19
+ title: "Person",
20
+ identity: "id",
21
+ schema: z.object({
22
+ id: WORD,
23
+ name: z.string().optional(),
24
+ /** Matched first by a sign-in that vouches for an email (GitHub's, its verified public one). */
25
+ email: z.email().optional(),
26
+ /** Their GitHub login, which GitHub sign-in matches, ignoring case, when no email does. */
27
+ github: z
28
+ .string()
29
+ .regex(/^[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?$/, "is a GitHub login, such as ada-ortiz")
30
+ .optional(),
31
+ }),
32
+ fields: NONE,
33
+ find: ({ id }) => id,
34
+ vendorless: true,
35
+ });
36
+
37
+ const agent = defineResource({
38
+ vendor: "sanoma",
39
+ type: "agent",
40
+ title: "Agent",
41
+ identity: "id",
42
+ schema: z.object({ id: WORD, name: z.string().optional(), onBehalfOf: WORD.optional() }),
43
+ fields: { ...NONE, references: { onBehalfOf: { type: "sanoma.person" } } },
44
+ find: ({ id }) => id,
45
+ vendorless: true,
46
+ });
47
+
48
+ const group = defineResource({
49
+ vendor: "sanoma",
50
+ type: "group",
51
+ title: "Group",
52
+ identity: "name",
53
+ schema: z.object({ name: WORD, members: z.array(WORD) }),
54
+ fields: {
55
+ ...NONE,
56
+ unordered: ["members"],
57
+ references: { members: { type: ["sanoma.person", "sanoma.agent"] } },
58
+ },
59
+ find: ({ name }) => name,
60
+ vendorless: true,
61
+ });
62
+
63
+ /**
64
+ * What a data file declares the company's people with, imported from "sanoma/resources":
65
+ * `export const ada = sanoma.person({ id: "ada", name: "Ada Ortiz", github: "ada-ortiz" })`,
66
+ * `sanoma.agent({ id: "refund-bot", onBehalfOf: bo })` and
67
+ * `sanoma.group({ name: "finance", members: [ada, refundBot] })`.
68
+ */
69
+ export const sanoma = { person, agent, group };
70
+
71
+ /**
72
+ * The runtime's own connector of the people types: every config's, so its data files may declare
73
+ * them. Its package is `sanoma`, whose `/resources` entry exports `sanoma`.
74
+ */
75
+ export const peopleConnector = defineConnector("sanoma", sanoma, { title: "Sanoma", package: "sanoma" });
package/src/plugin.ts CHANGED
@@ -18,7 +18,7 @@ const plugin = {
18
18
  type: "problem",
19
19
  docs: {
20
20
  description:
21
- "A data file under resources/ holds only imports, `export const <name> = <vendor>.<type>({ … })` with literal fields, and `export default [ … ]`",
21
+ "A data file under resources/ holds only imports, `export const <name> = <vendor>.<type>({ … })` with literal fields, and `export default [ … ]`; resources/data.ts holds only collections, `collection({ … })` from sanoma/data",
22
22
  },
23
23
  },
24
24
  create(context: Context) {
package/src/policy.ts CHANGED
@@ -1,14 +1,10 @@
1
- import { z } from "zod";
2
- import { type ApprovalState, Approver, Covers, type Principal, sameApprover } from "./define.ts";
1
+ import { type ApprovalState, type Approver, type Principal, sameApprover } from "./define.ts";
3
2
  import type { Effect, Op } from "./op.ts";
4
3
 
5
4
  /** An operation as a policy sees it: plain data, without its schemas. */
6
5
  export type PolicyOp = Pick<Op, "id" | "vendor" | "resource" | "name" | "effect">;
7
6
 
8
- /**
9
- * An operation as a policy sees it, for a `PolicyCall`: what the runtime passes, so a policy
10
- * test can build a call from the connector, `{ op: policyOpOf(shop.order.refund), ... }`.
11
- */
7
+ /** An operation as a policy sees it, for a `PolicyCall`: what the runtime passes. */
12
8
  export const policyOpOf = ({ id, vendor, resource, name, effect }: Op): PolicyOp => ({
13
9
  id,
14
10
  vendor,
@@ -17,7 +13,7 @@ export const policyOpOf = ({ id, vendor, resource, name, effect }: Op): PolicyOp
17
13
  effect,
18
14
  });
19
15
 
20
- /** What a policy sees for each operation a run calls. Plain data, so a test can build one by hand. */
16
+ /** What a policy sees for each operation a run calls: plain data. */
21
17
  export interface PolicyCall {
22
18
  op: PolicyOp;
23
19
  /** The same as `op.effect`. */
@@ -33,6 +29,8 @@ export interface PolicyCall {
33
29
  * A policy request has `requestedBy: "policy"` and the `op` it held.
34
30
  */
35
31
  run: { id: string; workflow: string; approvals: readonly ApprovalState[] };
32
+ /** For a collection's `save` or `delete`: the record as it was, when there was one. */
33
+ before?: unknown;
36
34
  }
37
35
 
38
36
  /**
@@ -44,88 +42,86 @@ export type Decision =
44
42
  | { kind: "deny"; reason: string; reasons?: string[] }
45
43
  | ApproveDecision;
46
44
 
47
- /**
48
- * Hold the call until the approver decides: a person's id, or `{ group }` for anyone in it.
49
- * The approval covers the held operation, and `covers` adds others, so a policy can let their
50
- * later calls through once it is approved.
51
- */
45
+ /** Hold the call until the approver decides: a person's id, or `{ group }` for anyone in it. */
52
46
  interface ApproveDecision {
53
47
  kind: "approve";
54
48
  approver: Approver;
55
49
  title?: string;
56
- covers?: Op[];
50
+ reasons?: string[];
57
51
  }
58
52
 
59
- const ReasonList = z.array(z.string(), { error: "reasons must be a list of strings" }).optional();
60
-
61
- /**
62
- * Checks a policy's answer. Parsing also copies it, so nothing else the policy returned is kept,
63
- * and turns `covers` into op ids.
64
- */
65
- export const DecisionSchema = z.discriminatedUnion(
66
- "kind",
67
- [
68
- z.object({ kind: z.literal("allow"), reasons: ReasonList }),
69
- z.object({ kind: z.literal("deny"), reason: z.string({ error: "deny needs a reason" }), reasons: ReasonList }),
70
- z.object({
71
- kind: z.literal("approve"),
72
- approver: Approver,
73
- title: z.string({ error: "the title must be a string" }).optional(),
74
- covers: Covers,
75
- }),
76
- ],
77
- { error: "not a decision" },
78
- );
79
-
80
- /**
81
- * A decision as checked and as the ledger records it: `covers` by op id, and the `version` of
82
- * the policy that made it, when it has one.
83
- */
84
- export type RecordedDecision = z.output<typeof DecisionSchema> & { policyVersion?: string };
53
+ /** A decision as the ledger records it, with the `version` of the policy that made it, when it has one. */
54
+ export type RecordedDecision = Decision & { policyVersion?: string };
85
55
 
86
56
  export const allow = (reasons?: string[]): Decision =>
87
57
  reasons === undefined ? { kind: "allow" } : { kind: "allow", reasons };
88
58
  export const deny = (reason: string, reasons?: string[]): Decision =>
89
59
  reasons === undefined ? { kind: "deny", reason } : { kind: "deny", reason, reasons };
90
60
 
91
- /** Holds the call for `approver`. `covers` lists other operations the approval also stands for. */
92
- export function approve(approver: Approver, { title, covers }: { title?: string; covers?: Op[] } = {}): Decision {
61
+ /** Holds the call for `approver`; `reasons`, the rules that held it, as `allow` and `deny` take them. */
62
+ export function approve(approver: Approver, { title, reasons }: { title?: string; reasons?: string[] } = {}): Decision {
93
63
  return {
94
64
  kind: "approve",
95
65
  approver,
96
66
  ...(title === undefined ? {} : { title }),
97
- ...(covers === undefined ? {} : { covers }),
67
+ ...(reasons === undefined ? {} : { reasons }),
98
68
  };
99
69
  }
100
70
 
101
71
  /**
102
- * Decides whether a run may make an operation call: allow it, deny it (the run fails),
103
- * or hold it until someone approves. Runs are replayed after a restart, so a policy must
104
- * decide the same way every time from the call alone: no clock, randomness or network.
105
- * `lintWorkflow` checks a policy file for these too. Each decision is recorded once made,
106
- * and a replay reuses it, but a call interrupted before that is decided again. A policy
107
- * must not have side effects.
72
+ * Decides whether a run may make an operation call: allow it, deny it (the run fails), or hold it
73
+ * until someone approves. A company's is made from its Cedar files (`cedarPolicy`); a repo with
74
+ * none has `allowAll`. Each decision is recorded once made, and a replay reuses it, but a call
75
+ * interrupted before that is decided again.
108
76
  */
109
- export type Policy = ((call: PolicyCall) => Decision | Promise<Decision>) & {
110
- /** Names this version of the policy, from `definePolicy(fn, { version })`. */
77
+ export type Policy = ((call: PolicyCall) => Decision) & {
78
+ /** Names this version of the policy: a hash of the Cedar policies' ids and text. */
111
79
  readonly version?: string;
80
+ /** What the policy would answer for each request ahead of the call, its input unknown. */
81
+ readonly preview: (requests: readonly PreviewRequest[]) => PreviewAnswer[];
82
+ /**
83
+ * May `actor` start a run of `workflow` with `input` (the action `workflow:<name>`), given the
84
+ * run's `approvals` so far (none before it starts)?
85
+ */
86
+ readonly start: (
87
+ actor: Principal,
88
+ workflow: string,
89
+ input: unknown,
90
+ approvals?: readonly ApprovalState[],
91
+ ) => Decision;
112
92
  };
113
93
 
114
- /** A policy, optionally named with a version so a reader can tell which rules decided a call. */
115
- export function definePolicy(
116
- policy: (call: PolicyCall) => Decision | Promise<Decision>,
117
- options: { version?: string } = {},
118
- ): Policy {
119
- if (typeof policy !== "function") throw new Error("definePolicy expects a function of the call");
120
- if (options.version !== undefined && (typeof options.version !== "string" || !options.version.trim())) {
121
- throw new Error("definePolicy: `version` must be a non-empty string");
122
- }
123
- const defined = (call: PolicyCall) => policy(call);
124
- return Object.freeze(options.version === undefined ? defined : Object.assign(defined, { version: options.version }));
94
+ /** A question asked ahead of a call: may `principal` take `action` (an operation, or `workflow:<name>` to start one) on `resource` (an operation's target)? */
95
+ export interface PreviewRequest {
96
+ principal: Principal;
97
+ action: { id: string; effect?: Effect };
98
+ resource?: string;
99
+ }
100
+
101
+ /**
102
+ * A preview's answer: `depends` when the unknown input decides it. `rule` is the Cedar policy
103
+ * behind a hold, a deny or a dependence; `approver`, a hold's.
104
+ */
105
+ export interface PreviewAnswer {
106
+ answer: "allow" | "hold" | "deny" | "depends";
107
+ rule?: { id: string; file: string; title?: string; reason?: string };
108
+ approver?: Approver;
125
109
  }
126
110
 
127
- /** Allows every operation call. A config says so explicitly: `policy: allowAll`. */
128
- export const allowAll: Policy = definePolicy(() => allow());
111
+ /** A policy that decides every call and start as `decide` does, and previews each as `answer`. */
112
+ const decidingAll = (decide: () => Decision, answer: "allow" | "deny"): Policy =>
113
+ Object.freeze(
114
+ Object.assign(decide, {
115
+ start: decide,
116
+ preview: (requests: readonly PreviewRequest[]): PreviewAnswer[] => requests.map(() => ({ answer })),
117
+ }),
118
+ );
119
+
120
+ /** Allows every operation call and run start: the policy of a repo with no `policies/**\/*.cedar`. */
121
+ export const allowAll: Policy = decidingAll(() => allow(), "allow");
122
+
123
+ /** Refuses every operation call and run start, and previews each as a deny, saying `reason`. */
124
+ export const denyAll = (reason: string): Policy => decidingAll(() => deny(reason), "deny");
129
125
 
130
126
  /**
131
127
  * True when an approval in the run was approved, covers the operation, and was addressed to