@sanoma-ai/workflows 0.10.0 → 0.10.2
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/CLOUD-IDENTITY.md +140 -0
- package/KMS-SECRETS.md +99 -0
- package/README.md +39 -7
- package/SECRET-SUPPORT.md +33 -0
- package/dist/apps.d.ts +6 -0
- package/dist/apps.js +18 -1
- package/dist/apps.js.map +1 -1
- package/dist/authentication.d.ts +35 -0
- package/dist/authentication.js +71 -1
- package/dist/authentication.js.map +1 -1
- package/dist/call.js +7 -5
- package/dist/call.js.map +1 -1
- package/dist/client.d.ts +54 -6
- package/dist/client.js +177 -5
- package/dist/client.js.map +1 -1
- package/dist/cloud-identity.d.ts +52 -0
- package/dist/cloud-identity.js +222 -0
- package/dist/cloud-identity.js.map +1 -0
- package/dist/config.d.ts +7 -4
- package/dist/config.js +44 -12
- package/dist/config.js.map +1 -1
- package/dist/connection.d.ts +223 -2
- package/dist/connection.js +340 -4
- package/dist/connection.js.map +1 -1
- package/dist/db.js +15 -2
- package/dist/db.js.map +1 -1
- package/dist/describe.d.ts +8 -0
- package/dist/describe.js +51 -1
- package/dist/describe.js.map +1 -1
- package/dist/direct.js +4 -0
- package/dist/direct.js.map +1 -1
- package/dist/fake.d.ts +13 -1
- package/dist/fake.js +70 -0
- package/dist/fake.js.map +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/kms-secrets.d.ts +24 -0
- package/dist/kms-secrets.js +209 -0
- package/dist/kms-secrets.js.map +1 -0
- package/dist/lint.js +1 -1
- package/dist/lint.js.map +1 -1
- package/dist/load.d.ts +11 -0
- package/dist/load.js +42 -8
- package/dist/load.js.map +1 -1
- package/dist/op.d.ts +14 -6
- package/dist/op.js +20 -2
- package/dist/op.js.map +1 -1
- package/dist/outline.d.ts +5 -0
- package/dist/outline.js +33 -1
- package/dist/outline.js.map +1 -1
- package/dist/paths.d.ts +2 -4
- package/dist/paths.js +2 -8
- package/dist/paths.js.map +1 -1
- package/dist/run.d.ts +3 -1
- package/dist/run.js.map +1 -1
- package/dist/sandbox.d.ts +4 -1
- package/dist/sandbox.js +51 -5
- package/dist/sandbox.js.map +1 -1
- package/dist/scenario.d.ts +5 -0
- package/dist/scenario.js +3 -2
- package/dist/scenario.js.map +1 -1
- package/dist/secret.d.ts +2 -0
- package/dist/secret.js +32 -0
- package/dist/secret.js.map +1 -1
- package/dist/shared.d.ts +4 -0
- package/dist/shared.js +7 -0
- package/dist/shared.js.map +1 -1
- package/dist/sites.js +18 -3
- package/dist/sites.js.map +1 -1
- package/dist/steps.d.ts +3 -0
- package/dist/steps.js.map +1 -1
- package/oxlint.json +3 -0
- package/package.json +16 -2
- package/src/apps.ts +17 -1
- package/src/authentication.ts +126 -2
- package/src/call.ts +4 -5
- package/src/client.ts +242 -6
- package/src/cloud-identity.ts +266 -0
- package/src/config.ts +45 -11
- package/src/connection.ts +478 -6
- package/src/db.ts +15 -2
- package/src/describe.ts +52 -1
- package/src/direct.ts +4 -0
- package/src/fake.ts +71 -0
- package/src/index.ts +13 -1
- package/src/kms-secrets.ts +271 -0
- package/src/lint.ts +1 -1
- package/src/load.ts +36 -7
- package/src/op.ts +33 -8
- package/src/outline.ts +35 -2
- package/src/paths.ts +2 -9
- package/src/run.ts +3 -1
- package/src/sandbox.ts +55 -5
- package/src/scenario.ts +24 -6
- package/src/secret.ts +29 -0
- package/src/shared.ts +9 -0
- package/src/sites.ts +19 -3
- package/src/steps.ts +3 -2
package/src/paths.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
2
2
|
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
3
|
-
import { APP_NAME } from "./shared.ts";
|
|
3
|
+
import { APP_NAME, APPS_DIR, appOfPath } from "./shared.ts";
|
|
4
4
|
|
|
5
5
|
// Where an import may reach, as the lints and the data-file reader all decide it, the one walk up
|
|
6
6
|
// the directories and the one walk down (`filesUnder`). Node's `path` and `fs` only, no parser:
|
|
@@ -65,8 +65,7 @@ export function filesUnder(dir: string, keep: (name: string) => boolean): string
|
|
|
65
65
|
.toSorted();
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
|
|
69
|
-
export const APPS_DIR = "apps";
|
|
68
|
+
export { APPS_DIR, appOfPath };
|
|
70
69
|
|
|
71
70
|
/** Each directory's company root (the nearest one with a `sanoma.config.ts`, short of a `node_modules`), once found. */
|
|
72
71
|
const roots = new Map<string, string>();
|
|
@@ -98,12 +97,6 @@ export function appOfFile(file: string | undefined): string | undefined {
|
|
|
98
97
|
return root === null ? undefined : appOfPath(relPath(root, file));
|
|
99
98
|
}
|
|
100
99
|
|
|
101
|
-
/** The app a path relative to a company's root is in: `<app>` under `apps/<app>/`; undefined for any other. */
|
|
102
|
-
export function appOfPath(rel: string): string | undefined {
|
|
103
|
-
const [top, app, ...rest] = rel.split("/");
|
|
104
|
-
return top === APPS_DIR && app !== undefined && rest.length && APP_NAME.test(app) ? app : undefined;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
100
|
/** The apps' folders under `root` whose names are an `APP_NAME`, sorted: `apps/<app>/`, absolute. */
|
|
108
101
|
export function appFolders(root: string): string[] {
|
|
109
102
|
const dir = join(root, APPS_DIR);
|
package/src/run.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ConnectionRegistry } from "./connection.ts";
|
|
1
|
+
import type { ConnectionRef, ConnectionRegistry, ConnectionSnapshot } from "./connection.ts";
|
|
2
2
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
3
3
|
import type { Collection } from "./collection.ts";
|
|
4
4
|
import type { DeadlineRegistry } from "./deadlines.ts";
|
|
@@ -98,6 +98,8 @@ export interface Run {
|
|
|
98
98
|
fakeDrivers?: Map<string, DriverFn>;
|
|
99
99
|
/** A sandbox run's own secrets, in memory, from `seedSandbox`; unset for a live run. */
|
|
100
100
|
fakeSecrets?: Secrets;
|
|
101
|
+
/** Synthetic, run-local identities; this resolver never reads production connection records. */
|
|
102
|
+
fakeConnection?: (vendor: string, ref?: ConnectionRef) => ConnectionSnapshot | undefined;
|
|
101
103
|
approvals: ApprovalState[];
|
|
102
104
|
/** The next ledger `seq`. Advanced only outside steps, so a replay counts the same way. */
|
|
103
105
|
seq: number;
|
package/src/sandbox.ts
CHANGED
|
@@ -3,7 +3,8 @@ import { indexDrivers, sandboxDrivers } from "./config.ts";
|
|
|
3
3
|
import { CALL_WORKFLOW, CallInput } from "./call-workflow.ts";
|
|
4
4
|
import { opIdsOf } from "./define.ts";
|
|
5
5
|
import { SanomaError } from "./errors.ts";
|
|
6
|
-
import type
|
|
6
|
+
import { connectionFakes, type Fake, type ResourceFake } from "./fake.ts";
|
|
7
|
+
import type { ConnectionCatalog, ConnectionRef, ConnectionSnapshot } from "./connection.ts";
|
|
7
8
|
import { entry, type LedgerRecord, write } from "./ledger.ts";
|
|
8
9
|
import type { DriverFn } from "./op.ts";
|
|
9
10
|
import type { Run } from "./run.ts";
|
|
@@ -20,6 +21,30 @@ import { errorMessage } from "./shared.ts";
|
|
|
20
21
|
const holdsResources = (fake: Fake<unknown>): fake is Fake<unknown> & ResourceFake =>
|
|
21
22
|
"override" in fake && "put" in fake && "remove" in fake;
|
|
22
23
|
|
|
24
|
+
/** Declared accounts get synthetic identities in each run; no production metadata or secrets are read. */
|
|
25
|
+
export function sandboxConnection(catalog: ConnectionCatalog, runId: string) {
|
|
26
|
+
return (vendor: string, ref?: ConnectionRef): ConnectionSnapshot | undefined => {
|
|
27
|
+
const definitions = [...catalog.definitions.values()].filter((d) => d.vendor === vendor);
|
|
28
|
+
const selected = ref ?? definitions.find((d) => d.default);
|
|
29
|
+
if (!selected && !Object.keys(catalog.methods.get(vendor) ?? {}).length) return undefined;
|
|
30
|
+
const definition = selected && catalog.definitions.get(selected.$connection);
|
|
31
|
+
if (!definition || selected?.vendor !== vendor || definition.vendor !== vendor)
|
|
32
|
+
throw new SanomaError("invalid_input", "Select an explicitly declared sandbox connection");
|
|
33
|
+
const revision = `sandbox:${runId}:${definition.$connection}`;
|
|
34
|
+
if (selected.revision !== undefined && selected.revision !== revision)
|
|
35
|
+
throw new SanomaError("invalid_input", "A sandbox cannot use a production connection revision");
|
|
36
|
+
return {
|
|
37
|
+
id: definition.$connection,
|
|
38
|
+
vendor,
|
|
39
|
+
method: definition.method,
|
|
40
|
+
account: definition.account ?? `fake:${definition.$connection}`,
|
|
41
|
+
scopes: [...definition.scopes],
|
|
42
|
+
revision,
|
|
43
|
+
definition: revision,
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
23
48
|
/**
|
|
24
49
|
* The error a sandbox run of the scenario fails with when it seeds a resource of a vendor whose
|
|
25
50
|
* fake (one of `fakes`, by vendor) holds none, or whose connector attaches no fake; undefined
|
|
@@ -110,10 +135,18 @@ export async function seedSandbox(run: Run, name: string, input: unknown): Promi
|
|
|
110
135
|
});
|
|
111
136
|
}
|
|
112
137
|
const fakes = new Map([...state.fakes].map(([vendor, make]) => [vendor, make()]));
|
|
138
|
+
const named = new Map(
|
|
139
|
+
[...state.fakes]
|
|
140
|
+
.filter(([vendor]) => Object.keys(state.connections!.catalog.methods.get(vendor) ?? {}).length)
|
|
141
|
+
.map(([vendor, make]) => [vendor, connectionFakes(make)]),
|
|
142
|
+
);
|
|
143
|
+
const select = sandboxConnection(state.connections!.catalog, run.id);
|
|
144
|
+
run.fakeConnection = select;
|
|
113
145
|
// The built-ins first, so a connector's fake that implements one of their operations wins.
|
|
114
146
|
const drivers = indexDrivers(
|
|
115
|
-
[...sandboxDrivers(state.ops), ...Array.from(fakes
|
|
147
|
+
[...sandboxDrivers(state.ops), ...Array.from(fakes, ([vendor, f]) => named.get(vendor)?.driver ?? f.driver)],
|
|
116
148
|
state.ops,
|
|
149
|
+
{ sandbox: true },
|
|
117
150
|
);
|
|
118
151
|
// The built-in call workflow selects exactly one operation. Other direct operations
|
|
119
152
|
// (including live connection mutations) need no fake unless this scenario calls them.
|
|
@@ -134,16 +167,33 @@ export async function seedSandbox(run: Run, name: string, input: unknown): Promi
|
|
|
134
167
|
for (const [i, given] of scenario.given.entries()) {
|
|
135
168
|
if ("fault" in given) continue;
|
|
136
169
|
if ("op" in given) {
|
|
137
|
-
const
|
|
170
|
+
const connection = select(state.ops.get(given.op)!.vendor, given.connection);
|
|
171
|
+
const call = {
|
|
172
|
+
idempotencyKey: `${run.id}:seed:${i}`,
|
|
173
|
+
runId: run.id,
|
|
174
|
+
opId: given.op,
|
|
175
|
+
attempt: 1,
|
|
176
|
+
secrets,
|
|
177
|
+
connection,
|
|
178
|
+
};
|
|
138
179
|
const output = await drivers.get(given.op)!(given.input, call);
|
|
139
180
|
seeded.push({ op: given.op, input: given.input, output });
|
|
140
181
|
continue;
|
|
141
182
|
}
|
|
142
183
|
// Checked above: the vendor's fake holds resources.
|
|
143
|
-
|
|
184
|
+
const selected = select(given.vendor, given.connection);
|
|
185
|
+
const fake = selected
|
|
186
|
+
? named.get(given.vendor)!.account({ vendor: given.vendor, $connection: selected.id })
|
|
187
|
+
: fakes.get(given.vendor)!;
|
|
188
|
+
seeded.push(seedResource(name, fake as Fake<unknown> & ResourceFake, given));
|
|
144
189
|
}
|
|
145
190
|
for (const given of scenario.given) {
|
|
146
|
-
if ("fault" in given)
|
|
191
|
+
if ("fault" in given) {
|
|
192
|
+
const vendor = state.ops.get(given.op)!.vendor;
|
|
193
|
+
const selected = select(vendor, given.connection);
|
|
194
|
+
const fake = selected ? named.get(vendor)!.account({ vendor, $connection: selected.id }) : fakes.get(vendor)!;
|
|
195
|
+
fake[given.fault](given.op);
|
|
196
|
+
}
|
|
147
197
|
}
|
|
148
198
|
copies = drivers;
|
|
149
199
|
return { seeds: seeded };
|
package/src/scenario.ts
CHANGED
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
import { errorCode, errorInfo, SanomaError } from "./errors.ts";
|
|
30
30
|
import { type CallRecord, contains, type CoreExpectation, detailOf, isCall } from "./expectations.ts";
|
|
31
31
|
import type { Fake } from "./fake.ts";
|
|
32
|
+
import type { ConnectionRef } from "./connection.ts";
|
|
32
33
|
import { fill, isBug, objectUnder, restOf, seedFrom, shapeOf } from "./fill.ts";
|
|
33
34
|
import type { LedgerRecord } from "./ledger.ts";
|
|
34
35
|
import type { Driver, Op } from "./op.ts";
|
|
@@ -92,10 +93,26 @@ export interface ScenarioStep {
|
|
|
92
93
|
* its import id, with `fields` to set on it (put, when the vendor has none) or `gone` to remove it.
|
|
93
94
|
*/
|
|
94
95
|
export type Given =
|
|
95
|
-
| { step: string; op: string; input: unknown; as?: string }
|
|
96
|
-
| { step: string; op: string; fault: Fault }
|
|
97
|
-
| {
|
|
98
|
-
|
|
96
|
+
| { step: string; op: string; input: unknown; as?: string; connection?: ConnectionRef }
|
|
97
|
+
| { step: string; op: string; fault: Fault; connection?: ConnectionRef }
|
|
98
|
+
| {
|
|
99
|
+
step: string;
|
|
100
|
+
resource: string;
|
|
101
|
+
vendor: string;
|
|
102
|
+
type: string;
|
|
103
|
+
name: string;
|
|
104
|
+
fields: Record<string, unknown>;
|
|
105
|
+
connection?: ConnectionRef;
|
|
106
|
+
}
|
|
107
|
+
| {
|
|
108
|
+
step: string;
|
|
109
|
+
resource: string;
|
|
110
|
+
vendor: string;
|
|
111
|
+
type: string;
|
|
112
|
+
name: string;
|
|
113
|
+
gone: true;
|
|
114
|
+
connection?: ConnectionRef;
|
|
115
|
+
};
|
|
99
116
|
|
|
100
117
|
/** How one approval is decided; `approval` is its title or its id. */
|
|
101
118
|
export interface Decision {
|
|
@@ -671,7 +688,8 @@ function rulesOf(scope: Scope): { rules: Rule[]; parameterTypes: ParameterTypeSp
|
|
|
671
688
|
read: (values) => {
|
|
672
689
|
const seed = def.fn(...values);
|
|
673
690
|
const op = opNamed(seed.op);
|
|
674
|
-
|
|
691
|
+
const connection = seed.connection && { connection: seed.connection };
|
|
692
|
+
if ("fault" in seed) return [{ op, fault: seed.fault, ...connection }];
|
|
675
693
|
let input: unknown;
|
|
676
694
|
try {
|
|
677
695
|
input = fill(opOf(op).input, seed.input);
|
|
@@ -681,7 +699,7 @@ function rulesOf(scope: Scope): { rules: Rule[]; parameterTypes: ParameterTypeSp
|
|
|
681
699
|
cause: err,
|
|
682
700
|
});
|
|
683
701
|
}
|
|
684
|
-
return [{ op, input, ...(seed.as === undefined ? {} : { as: seed.as }) }];
|
|
702
|
+
return [{ op, input, ...(seed.as === undefined ? {} : { as: seed.as }), ...connection }];
|
|
685
703
|
},
|
|
686
704
|
});
|
|
687
705
|
continue;
|
package/src/secret.ts
CHANGED
|
@@ -46,6 +46,35 @@ export interface Secrets {
|
|
|
46
46
|
ensure(name: string, make: () => string): Promise<string>;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
/** A named call can resolve only the auxiliary secret markers present in its authorized input. */
|
|
50
|
+
export function secretsForInput(input: unknown, secrets: Secrets, creates: readonly string[] = []): Secrets {
|
|
51
|
+
const allowed = new Map<string, boolean>();
|
|
52
|
+
for (const name of creates) {
|
|
53
|
+
if (!SECRET_NAME.test(name)) throw new DriverError("Invalid operation secret destination", { retryable: false });
|
|
54
|
+
allowed.set(name, true);
|
|
55
|
+
}
|
|
56
|
+
const visit = (value: unknown) => {
|
|
57
|
+
if (isSecret(value)) allowed.set(value.$secret, allowed.get(value.$secret) === true || value.generate === true);
|
|
58
|
+
else if (Array.isArray(value)) value.forEach(visit);
|
|
59
|
+
else if (value && typeof value === "object") Object.values(value).forEach(visit);
|
|
60
|
+
};
|
|
61
|
+
visit(input);
|
|
62
|
+
const check = (name: string, generate: boolean) => {
|
|
63
|
+
if (!allowed.has(name) || (generate && !allowed.get(name)))
|
|
64
|
+
throw new DriverError("The call does not authorize this secret access", { retryable: false });
|
|
65
|
+
};
|
|
66
|
+
return {
|
|
67
|
+
async get(name) {
|
|
68
|
+
check(name, false);
|
|
69
|
+
return secrets.get(name);
|
|
70
|
+
},
|
|
71
|
+
async ensure(name, make) {
|
|
72
|
+
check(name, true);
|
|
73
|
+
return secrets.ensure(name, make);
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
49
78
|
/** A value for a secret with `generate`: 32 random bytes, base64url. */
|
|
50
79
|
export const generateSecret = (): string => randomBytes(32).toString("base64url");
|
|
51
80
|
|
package/src/shared.ts
CHANGED
|
@@ -89,6 +89,15 @@ export const SITE_NAME = /^[a-z][a-z0-9-]*$/;
|
|
|
89
89
|
*/
|
|
90
90
|
export const APP_NAME = /^[a-z][a-z0-9]*$/;
|
|
91
91
|
|
|
92
|
+
/** Where a company's apps are, under its root: `apps/<app>/`. */
|
|
93
|
+
export const APPS_DIR = "apps";
|
|
94
|
+
|
|
95
|
+
/** The app a path relative to a company's root is in: `<app>` under `apps/<app>/`; undefined for any other. */
|
|
96
|
+
export function appOfPath(rel: string): string | undefined {
|
|
97
|
+
const [top, app, ...rest] = rel.split("/");
|
|
98
|
+
return top === APPS_DIR && app !== undefined && rest.length && APP_NAME.test(app) ? app : undefined;
|
|
99
|
+
}
|
|
100
|
+
|
|
92
101
|
/** A name declared under `apps/<app>/` as the runtime has it, `<app>.<name>`; one declared at the root as it is. */
|
|
93
102
|
export const qualified = (app: string | undefined, name: string): string =>
|
|
94
103
|
app === undefined ? name : `${app}.${name}`;
|
package/src/sites.ts
CHANGED
|
@@ -7,6 +7,7 @@ import type { ConfigDescription, SiteCall, SiteEntry, SitePage } from "./descrip
|
|
|
7
7
|
import { stringValue, unwrap } from "./outline.ts";
|
|
8
8
|
import { exportsOf, loadModules } from "./load.ts";
|
|
9
9
|
import { APPS_DIR, appFolders, filesUnder, isDirectory, relPath, SITE_ENTRIES } from "./paths.ts";
|
|
10
|
+
import { isObject } from "./resource.ts";
|
|
10
11
|
import type { ResourceProblem } from "./resources.ts";
|
|
11
12
|
import { START } from "./schema.ts";
|
|
12
13
|
import {
|
|
@@ -428,6 +429,9 @@ const SiteDefinitionSchema = z.strictObject({
|
|
|
428
429
|
locales: z.array(z.string()).optional(),
|
|
429
430
|
});
|
|
430
431
|
|
|
432
|
+
/** What `site.ts` says of who may see a site: read together or not at all. */
|
|
433
|
+
const ACCESS = ["public", "login", "locales"];
|
|
434
|
+
|
|
431
435
|
/**
|
|
432
436
|
* What the site in `dir` (relative to `root`) says of itself in its `site.ts`, loaded
|
|
433
437
|
* (`loadModules`, `defineSite` served from `sanoma/site`), and what is wrong there: its default
|
|
@@ -452,13 +456,25 @@ function siteDefinitionOf(
|
|
|
452
456
|
const value = loaded.exports.find(([name]) => name === "default")?.[1];
|
|
453
457
|
if (value === undefined) return { problems: [] };
|
|
454
458
|
const problem = (message: string) => ({ file, export: "default", message });
|
|
455
|
-
const
|
|
459
|
+
const problems: (ResourceProblem & { file: string })[] = [];
|
|
460
|
+
let parsed = SiteDefinitionSchema.safeParse(value);
|
|
456
461
|
if (!parsed.success) {
|
|
457
462
|
const why = z.prettifyError(parsed.error).replaceAll(/\s*\n\s*/g, " ");
|
|
458
|
-
|
|
463
|
+
problems.push(problem(`its default export is \`defineSite({ … })\` from "sanoma/site": ${why}`));
|
|
464
|
+
// The fields it names wrongly (and the keys it does not know) left out, the rest still read:
|
|
465
|
+
// a bad icon keeps a site's `public`, `login` and `locales`. Those three say who may see it,
|
|
466
|
+
// so a wrong one leaves out all three: never a site read as public without its login.
|
|
467
|
+
const wrong = new Set(
|
|
468
|
+
parsed.error.issues.flatMap((i) => (i.code === "unrecognized_keys" ? i.keys : i.path.slice(0, 1))),
|
|
469
|
+
);
|
|
470
|
+
if (ACCESS.some((key) => wrong.has(key))) for (const key of ACCESS) wrong.add(key);
|
|
471
|
+
if (!isObject(value) || !wrong.size) return { problems };
|
|
472
|
+
parsed = SiteDefinitionSchema.safeParse(
|
|
473
|
+
Object.fromEntries(Object.entries(value).filter(([key]) => !wrong.has(key))),
|
|
474
|
+
);
|
|
475
|
+
if (!parsed.success) return { problems };
|
|
459
476
|
}
|
|
460
477
|
const { title, icon, public: isPublic, login, locales } = parsed.data;
|
|
461
|
-
const problems: (ResourceProblem & { file: string })[] = [];
|
|
462
478
|
const loginProblem = (): string | undefined => {
|
|
463
479
|
if (!isPublic) return "a login is for a public site: add `public: true`, or remove `login`";
|
|
464
480
|
if (!connectors.vendors.has(login!)) return `login names "${login}", which is not a connector in sanoma.config.ts`;
|
package/src/steps.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { LedgerRecord } from "./ledger.ts";
|
|
2
2
|
import type { Op } from "./op.ts";
|
|
3
|
+
import type { ConnectionRef } from "./connection.ts";
|
|
3
4
|
|
|
4
5
|
/*
|
|
5
6
|
* Steps a connector (or a company) ships for its Gherkin tests, as plain data: expressions and
|
|
@@ -19,8 +20,8 @@ type AnyOp<I = any, O = any> = Op<string, string, string, I, O>;
|
|
|
19
20
|
* seed's name (else the type's) as the export.
|
|
20
21
|
*/
|
|
21
22
|
export type Seed =
|
|
22
|
-
| { op: AnyOp; input: Record<string, unknown>; as?: string }
|
|
23
|
-
| { op: AnyOp; fault: "failNext" | "rateLimit" | "loseReply" };
|
|
23
|
+
| { op: AnyOp; input: Record<string, unknown>; as?: string; connection?: ConnectionRef }
|
|
24
|
+
| { op: AnyOp; fault: "failNext" | "rateLimit" | "loseReply"; connection?: ConnectionRef };
|
|
24
25
|
|
|
25
26
|
/** A call through an operation: what it was given, and what it returned. */
|
|
26
27
|
export interface Call<I, O> {
|