@sanoma-ai/workflows 0.0.0-stage → 0.2.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/LICENSE +105 -0
- package/README.md +544 -2
- package/dist/apply-steps.d.ts +6 -0
- package/dist/apply-steps.js +93 -0
- package/dist/apply-steps.js.map +1 -0
- package/dist/apply.d.ts +197 -0
- package/dist/apply.js +470 -0
- package/dist/apply.js.map +1 -0
- package/dist/approvals.d.ts +46 -0
- package/dist/approvals.js +111 -0
- package/dist/approvals.js.map +1 -0
- package/dist/ast.d.ts +57 -0
- package/dist/ast.js +81 -0
- package/dist/ast.js.map +1 -0
- package/dist/call.d.ts +37 -0
- package/dist/call.js +417 -0
- package/dist/call.js.map +1 -0
- package/dist/client.d.ts +203 -0
- package/dist/client.js +457 -0
- package/dist/client.js.map +1 -0
- package/dist/config.d.ts +126 -0
- package/dist/config.js +264 -0
- package/dist/config.js.map +1 -0
- package/dist/credentials.d.ts +55 -0
- package/dist/credentials.js +261 -0
- package/dist/credentials.js.map +1 -0
- package/dist/cucumber.d.ts +64 -0
- package/dist/cucumber.js +158 -0
- package/dist/cucumber.js.map +1 -0
- package/dist/datafile.d.ts +71 -0
- package/dist/datafile.js +380 -0
- package/dist/datafile.js.map +1 -0
- package/dist/db.d.ts +58 -0
- package/dist/db.js +179 -0
- package/dist/db.js.map +1 -0
- package/dist/define.d.ts +205 -0
- package/dist/define.js +103 -0
- package/dist/define.js.map +1 -0
- package/dist/describe.d.ts +141 -0
- package/dist/describe.js +135 -0
- package/dist/describe.js.map +1 -0
- package/dist/drift.d.ts +127 -0
- package/dist/drift.js +167 -0
- package/dist/drift.js.map +1 -0
- package/dist/env.d.ts +49 -0
- package/dist/env.js +83 -0
- package/dist/env.js.map +1 -0
- package/dist/errors.d.ts +88 -0
- package/dist/errors.js +148 -0
- package/dist/errors.js.map +1 -0
- package/dist/expectations.d.ts +26 -0
- package/dist/expectations.js +162 -0
- package/dist/expectations.js.map +1 -0
- package/dist/fake.d.ts +55 -0
- package/dist/fake.js +87 -0
- package/dist/fake.js.map +1 -0
- package/dist/fill.d.ts +9 -0
- package/dist/fill.js +17 -0
- package/dist/fill.js.map +1 -0
- package/dist/host.d.ts +56 -0
- package/dist/host.js +45 -0
- package/dist/host.js.map +1 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +29 -0
- package/dist/index.js.map +1 -0
- package/dist/ledger.d.ts +217 -0
- package/dist/ledger.js +251 -0
- package/dist/ledger.js.map +1 -0
- package/dist/lint.d.ts +32 -0
- package/dist/lint.js +168 -0
- package/dist/lint.js.map +1 -0
- package/dist/lockfile.d.ts +36 -0
- package/dist/lockfile.js +69 -0
- package/dist/lockfile.js.map +1 -0
- package/dist/log.d.ts +24 -0
- package/dist/log.js +83 -0
- package/dist/log.js.map +1 -0
- package/dist/op.d.ts +260 -0
- package/dist/op.js +188 -0
- package/dist/op.js.map +1 -0
- package/dist/outline.d.ts +179 -0
- package/dist/outline.js +506 -0
- package/dist/outline.js.map +1 -0
- package/dist/paths.d.ts +11 -0
- package/dist/paths.js +30 -0
- package/dist/paths.js.map +1 -0
- package/dist/plugin.d.ts +28 -0
- package/dist/plugin.js +25 -0
- package/dist/plugin.js.map +1 -0
- package/dist/policy.d.ts +114 -0
- package/dist/policy.js +60 -0
- package/dist/policy.js.map +1 -0
- package/dist/resource.d.ts +378 -0
- package/dist/resource.js +430 -0
- package/dist/resource.js.map +1 -0
- package/dist/resources.d.ts +77 -0
- package/dist/resources.js +244 -0
- package/dist/resources.js.map +1 -0
- package/dist/run.d.ts +101 -0
- package/dist/run.js +7 -0
- package/dist/run.js.map +1 -0
- package/dist/sandbox.d.ts +33 -0
- package/dist/sandbox.js +127 -0
- package/dist/sandbox.js.map +1 -0
- package/dist/scenario.d.ts +221 -0
- package/dist/scenario.js +1104 -0
- package/dist/scenario.js.map +1 -0
- package/dist/scenarios.d.ts +55 -0
- package/dist/scenarios.js +71 -0
- package/dist/scenarios.js.map +1 -0
- package/dist/secret.d.ts +42 -0
- package/dist/secret.js +58 -0
- package/dist/secret.js.map +1 -0
- package/dist/shared.d.ts +69 -0
- package/dist/shared.js +84 -0
- package/dist/shared.js.map +1 -0
- package/dist/source.d.ts +58 -0
- package/dist/source.js +54 -0
- package/dist/source.js.map +1 -0
- package/dist/steps.d.ts +76 -0
- package/dist/steps.js +23 -0
- package/dist/steps.js.map +1 -0
- package/dist/version.d.ts +29 -0
- package/dist/version.js +76 -0
- package/dist/version.js.map +1 -0
- package/dist/worker.d.ts +37 -0
- package/dist/worker.js +228 -0
- package/dist/worker.js.map +1 -0
- package/dist/writer.d.ts +35 -0
- package/dist/writer.js +201 -0
- package/dist/writer.js.map +1 -0
- package/oxlint.json +128 -0
- package/package.json +96 -3
- package/src/apply-steps.ts +110 -0
- package/src/apply.ts +626 -0
- package/src/approvals.ts +157 -0
- package/src/ast.ts +115 -0
- package/src/call.ts +483 -0
- package/src/client.ts +604 -0
- package/src/config.ts +404 -0
- package/src/credentials.ts +296 -0
- package/src/cucumber.ts +230 -0
- package/src/datafile.ts +481 -0
- package/src/db.ts +197 -0
- package/src/define.ts +285 -0
- package/src/describe.ts +282 -0
- package/src/drift.ts +259 -0
- package/src/env.ts +122 -0
- package/src/errors.ts +202 -0
- package/src/expectations.ts +191 -0
- package/src/fake.ts +156 -0
- package/src/fill.ts +24 -0
- package/src/host.ts +96 -0
- package/src/index.ts +143 -0
- package/src/ledger.ts +419 -0
- package/src/lint.ts +208 -0
- package/src/lockfile.ts +95 -0
- package/src/log.ts +82 -0
- package/src/op.ts +402 -0
- package/src/outline.ts +620 -0
- package/src/paths.ts +32 -0
- package/src/plugin.ts +37 -0
- package/src/policy.ts +139 -0
- package/src/resource.ts +670 -0
- package/src/resources.ts +311 -0
- package/src/run.ts +106 -0
- package/src/sandbox.ts +161 -0
- package/src/scenario.ts +1365 -0
- package/src/scenarios.ts +111 -0
- package/src/secret.ts +92 -0
- package/src/shared.ts +126 -0
- package/src/source.ts +82 -0
- package/src/steps.ts +90 -0
- package/src/version.ts +86 -0
- package/src/worker.ts +264 -0
- package/src/writer.ts +225 -0
package/src/plugin.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { readDataFile } from "./datafile.ts";
|
|
2
|
+
|
|
3
|
+
// An oxlint JS plugin with one rule, `sanoma/data-file`: the data-file subset `readResources`
|
|
4
|
+
// reads, the same check as `lintResources`, on the tree oxlint has already parsed. The
|
|
5
|
+
// package's `oxlint.json` turns it on for `resources/**`. Typed here by the little it uses, so
|
|
6
|
+
// the package needs no oxlint types.
|
|
7
|
+
|
|
8
|
+
interface Context {
|
|
9
|
+
filename: string;
|
|
10
|
+
report(diagnostic: { message: string; node: { range: [number, number] } }): void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const plugin = {
|
|
14
|
+
meta: { name: "sanoma" },
|
|
15
|
+
rules: {
|
|
16
|
+
"data-file": {
|
|
17
|
+
meta: {
|
|
18
|
+
type: "problem",
|
|
19
|
+
docs: {
|
|
20
|
+
description:
|
|
21
|
+
"A data file under resources/ holds only imports, `export const <name> = <vendor>.<type>({ … })` with literal fields, and `export default [ … ]`",
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
create(context: Context) {
|
|
25
|
+
return {
|
|
26
|
+
Program(program: unknown) {
|
|
27
|
+
for (const { start, end, message } of readDataFile(program, context.filename).problems) {
|
|
28
|
+
context.report({ message, node: { range: [start, end] } });
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export default plugin;
|
package/src/policy.ts
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { type ApprovalState, Approver, Covers, type Principal, sameApprover } from "./define.ts";
|
|
3
|
+
import type { Effect, Op } from "./op.ts";
|
|
4
|
+
|
|
5
|
+
/** An operation as a policy sees it: plain data, without its schemas. */
|
|
6
|
+
export type PolicyOp = Pick<Op, "id" | "vendor" | "resource" | "name" | "effect">;
|
|
7
|
+
|
|
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
|
+
*/
|
|
12
|
+
export const policyOpOf = ({ id, vendor, resource, name, effect }: Op): PolicyOp => ({
|
|
13
|
+
id,
|
|
14
|
+
vendor,
|
|
15
|
+
resource,
|
|
16
|
+
name,
|
|
17
|
+
effect,
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
/** What a policy sees for each operation a run calls. Plain data, so a test can build one by hand. */
|
|
21
|
+
export interface PolicyCall {
|
|
22
|
+
op: PolicyOp;
|
|
23
|
+
/** The same as `op.effect`. */
|
|
24
|
+
effect: Effect;
|
|
25
|
+
/** The resource instance the call acts on, from the operation's `target`, when it declares one. */
|
|
26
|
+
target?: string;
|
|
27
|
+
/** The operation's input, already checked against its schema. */
|
|
28
|
+
input: unknown;
|
|
29
|
+
/** Who started the run. */
|
|
30
|
+
actor: Principal;
|
|
31
|
+
/**
|
|
32
|
+
* The run so far: a copy of its approvals, including those the policy asked for earlier.
|
|
33
|
+
* A policy request has `requestedBy: "policy"` and the `op` it held.
|
|
34
|
+
*/
|
|
35
|
+
run: { id: string; workflow: string; approvals: readonly ApprovalState[] };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* A policy's answer. `reasons` say why, for whoever reads the ledger; they change nothing
|
|
40
|
+
* about what happens to the call.
|
|
41
|
+
*/
|
|
42
|
+
export type Decision =
|
|
43
|
+
| { kind: "allow"; reasons?: string[] }
|
|
44
|
+
| { kind: "deny"; reason: string; reasons?: string[] }
|
|
45
|
+
| ApproveDecision;
|
|
46
|
+
|
|
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
|
+
*/
|
|
52
|
+
interface ApproveDecision {
|
|
53
|
+
kind: "approve";
|
|
54
|
+
approver: Approver;
|
|
55
|
+
title?: string;
|
|
56
|
+
covers?: Op[];
|
|
57
|
+
}
|
|
58
|
+
|
|
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 };
|
|
85
|
+
|
|
86
|
+
export const allow = (reasons?: string[]): Decision =>
|
|
87
|
+
reasons === undefined ? { kind: "allow" } : { kind: "allow", reasons };
|
|
88
|
+
export const deny = (reason: string, reasons?: string[]): Decision =>
|
|
89
|
+
reasons === undefined ? { kind: "deny", reason } : { kind: "deny", reason, reasons };
|
|
90
|
+
|
|
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 {
|
|
93
|
+
return {
|
|
94
|
+
kind: "approve",
|
|
95
|
+
approver,
|
|
96
|
+
...(title === undefined ? {} : { title }),
|
|
97
|
+
...(covers === undefined ? {} : { covers }),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
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.
|
|
108
|
+
*/
|
|
109
|
+
export type Policy = ((call: PolicyCall) => Decision | Promise<Decision>) & {
|
|
110
|
+
/** Names this version of the policy, from `definePolicy(fn, { version })`. */
|
|
111
|
+
readonly version?: string;
|
|
112
|
+
};
|
|
113
|
+
|
|
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 }));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Allows every operation call. A config says so explicitly: `policy: allowAll`. */
|
|
128
|
+
export const allowAll: Policy = definePolicy(() => allow());
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* True when an approval in the run was approved, covers the operation, and was addressed to
|
|
132
|
+
* `approver`. Check all three: a workflow can request an approval covering any operation from
|
|
133
|
+
* anyone it names, so an approval's `covers` alone does not say the right person approved.
|
|
134
|
+
*/
|
|
135
|
+
export function approvedFor(approvals: readonly ApprovalState[], opId: string, approver: Approver): boolean {
|
|
136
|
+
return approvals.some(
|
|
137
|
+
(a) => a.status === "approved" && a.covers.includes(opId) && sameApprover(a.approver, approver),
|
|
138
|
+
);
|
|
139
|
+
}
|