@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/resources.ts CHANGED
@@ -1,12 +1,31 @@
1
- import { readdirSync, readFileSync, statSync } from "node:fs";
1
+ import { readFileSync, statSync } from "node:fs";
2
2
  import { dirname, join, relative, resolve, sep } from "node:path";
3
3
  import { parsed } from "./ast.ts";
4
- import { rootOf, type SanomaConfig } from "./config.ts";
5
- import { type DataFile, type DataFileExport, type DataValue, readDataFile, Reference } from "./datafile.ts";
4
+ import {
5
+ type DataFile,
6
+ type DataFileExport,
7
+ type DataValue,
8
+ readDataFile,
9
+ Reference,
10
+ SECRET_ENTRIES,
11
+ } from "./datafile.ts";
12
+ import { type Binding, type Bindings, LOCKFILE, readLockfile } from "./lockfile.ts";
6
13
  import { type Connector, VENDOR } from "./op.ts";
14
+ import { filesUnder } from "./paths.ts";
15
+ import { peopleConnector } from "./person.ts";
7
16
  import type { Span } from "./outline.ts";
8
17
  import { type Declared, type Resource, resourceTypesOf } from "./resource.ts";
9
- import { problemLine } from "./shared.ts";
18
+ import { Principal } from "./define.ts";
19
+ import { NotAdmittedError } from "./errors.ts";
20
+ import {
21
+ AGENTS,
22
+ declaresPeople,
23
+ type PeopleResource,
24
+ personOf,
25
+ principalOf,
26
+ problemLine,
27
+ RUNTIME_AGENTS,
28
+ } from "./shared.ts";
10
29
 
11
30
  /** A resource a data file declares, as read from the file without running it. */
12
31
  export interface DeclaredResource {
@@ -16,8 +35,12 @@ export interface DeclaredResource {
16
35
  file: string;
17
36
  vendor: string;
18
37
  type: string;
19
- /** Its import id: what the type's `find` makes of its fields. */
38
+ /** What people call it: its identity, else its export's name. */
20
39
  name: string;
40
+ /** The import id that finds it unbound: what its type's `find` makes of its fields; absent when the type has none. */
41
+ identity?: string;
42
+ /** The vendor's id of the object the lockfile binds it to (`LOCKFILE`); absent when unbound. */
43
+ binding?: string;
21
44
  /** Its `export const` statement, as UTF-16 offsets into the file's text. */
22
45
  span: Span;
23
46
  /** Its fields, as its type checked them: each resource it names as that resource's `name`. */
@@ -29,6 +52,22 @@ export interface DeclaredResource {
29
52
  refs: Record<string, string>;
30
53
  }
31
54
 
55
+ /**
56
+ * A collection `resources/data.ts` declares, as read without running it: where it is, for the
57
+ * Resources page. Its fields are the collection's own (`describeConfig`'s `collections`), from
58
+ * the same file, which the runtime imports.
59
+ */
60
+ export interface DeclaredCollection {
61
+ /** `<file>#<export>`, as a resource's: `resources/data.ts#outbound`. */
62
+ id: string;
63
+ /** `resources/data.ts`. */
64
+ file: string;
65
+ /** The collection's name, its operations' middle segment (`data.<name>.get`). */
66
+ name: string;
67
+ /** Its `export const` statement, as UTF-16 offsets into the file's text. */
68
+ span: Span;
69
+ }
70
+
32
71
  /** Something wrong in the data files, or in finding them. */
33
72
  export interface ResourceProblem {
34
73
  /**
@@ -43,26 +82,118 @@ export interface ResourceProblem {
43
82
  message: string;
44
83
  }
45
84
 
46
- /** What the data files declare: each resource read without problems, and the problems. */
85
+ /** What the data files declare: each resource read without problems, the lockfile's bindings, and the problems. */
47
86
  export interface DataFiles {
48
87
  /** By file and then in file order, less any with a problem, and any that names one. */
49
88
  resources: DeclaredResource[];
89
+ /** The lockfile's bindings, by key (`<file>#<export>`), as read: none when it is missing or has a problem. */
90
+ bindings: Bindings;
91
+ /** Each binding whose key no data file declares, by key. */
92
+ orphans: (Binding & { key: string })[];
93
+ /** The collections `resources/data.ts` declares, in file order. */
94
+ collections: DeclaredCollection[];
50
95
  /** By file, line and column. */
51
96
  problems: ResourceProblem[];
52
97
  }
53
98
 
99
+ /** What the data files declare of the people: the resources `principalOf` takes, and what is wrong in their files. */
100
+ export interface DeclaredPeople {
101
+ people: DeclaredResource[];
102
+ /**
103
+ * The problems in each file that declares (or tries to declare) a person, agent or group, and
104
+ * an id two of them share or one of the runtime's own agents (`RUNTIME_AGENTS`) takes. While
105
+ * there are any, nobody is admitted: a broken file never admits more than it says.
106
+ */
107
+ problems: ResourceProblem[];
108
+ }
109
+
110
+ /** A file that imports the runtime's constructors, `sanoma` (by any local name): the files `readPeople` reads. */
111
+ const DECLARES_PEOPLE = /\{[^}]*\bsanoma\b[^}]*\}\s*from\s*["'](?:sanoma|@sanoma-ai\/workflows)\/resources["']/;
112
+ /** The last read of the people under each root, by what its data files were then (`filesStamp`). */
113
+ const peopleRead = new Map<string, { stamp: string; read: DeclaredPeople }>();
114
+
115
+ /** Each data file under `root` with its size and modification time: unchanged while the files are. */
116
+ const filesStamp = (root: string) =>
117
+ dataFilesUnder(join(root, "resources"))
118
+ .map((path) => {
119
+ // Gone since it was listed (an editor's save by rename): stamped as such, and read next time.
120
+ const stat = statSync(path, { bigint: true, throwIfNoEntry: false });
121
+ return `${path} ${stat?.size} ${stat?.mtimeNs}`;
122
+ })
123
+ .join("\n");
124
+
54
125
  /**
55
- * The resources the config's data files declare, read without importing or running them; throws
56
- * one error listing every problem, each at `file:line:column`, for a test or CI to fail on.
57
- * `describeConfig` lists the same, with the problems beside them instead.
126
+ * The people, agents and groups the data files under `root` declare (`sanoma.person`, `.agent`,
127
+ * `.group`), read without running them, as `principalOf` takes them, with what is wrong in their
128
+ * files; none without a root. Read again only when a data file changed (by its size and time),
129
+ * so a new colleague is admitted once their file is saved, and a request costs a `stat` per file.
58
130
  */
59
- export function readResources(config: SanomaConfig): DeclaredResource[] {
60
- const { resources, problems } = readDataFiles(rootOf(config), config.connectors);
131
+ export function readPeople(root: string | undefined): DeclaredPeople {
132
+ if (root === undefined) return { people: [], problems: [] };
133
+ const stamp = filesStamp(root);
134
+ const cached = peopleRead.get(root);
135
+ if (cached?.stamp === stamp) return cached.read;
136
+ const read = readDataFiles(root, [peopleConnector], undefined, (source) => DECLARES_PEOPLE.test(source));
137
+ const people = read.resources;
138
+ // The people are never bound, so the lockfile's problems are not theirs.
139
+ const own = read.problems.filter((p) => p.file !== LOCKFILE);
140
+ const ids = new Map<string, DeclaredResource>();
141
+ for (const r of people) {
142
+ if (r.type === "group") {
143
+ if (r.name === AGENTS) {
144
+ own.push({
145
+ file: r.file,
146
+ message: `${r.id} is named "${AGENTS}", the group every agent is in: give it another name`,
147
+ });
148
+ }
149
+ continue;
150
+ }
151
+ const id = r.name;
152
+ const other = ids.get(id);
153
+ if (other || RUNTIME_AGENTS.includes(id)) {
154
+ own.push({
155
+ file: r.file,
156
+ message: other
157
+ ? `${r.id} has the id "${id}", which ${other.id} has: give each person and agent their own`
158
+ : `${r.id} has the id "${id}", one of the runtime's own agents': give it another`,
159
+ });
160
+ }
161
+ ids.set(id, r);
162
+ }
163
+ const declared = { people, problems: own };
164
+ peopleRead.set(root, { stamp, read: declared });
165
+ return declared;
166
+ }
167
+
168
+ /**
169
+ * Who `asking` is admitted as, by the people the data files declare (`readPeople`): the declared
170
+ * principal (`principalOf`) of the id it names, a person's or an agent's; for a sign-in
171
+ * (`signIn`), of the person it matches (`personOf`: email, GitHub login, then id), never an agent.
172
+ * While no person is declared, whoever names an id is taken as given. Throws `NotAdmittedError`
173
+ * for anyone else, and for everyone while the people's files have problems.
174
+ */
175
+ export function admit(
176
+ asking: { id?: string; email?: string; github?: string; groups?: string[]; onBehalfOf?: string },
177
+ { people, problems }: { people: readonly PeopleResource[]; problems: readonly ResourceProblem[] },
178
+ signIn = false,
179
+ ): Principal {
180
+ const who = asking.github ?? asking.id ?? asking.email ?? "This sign-in";
61
181
  if (problems.length) {
62
- const lines = problems.map((p) => ` ${problemLine(p)}`);
63
- throw new Error(`The resources' data files have problems:\n${lines.join("\n")}`);
182
+ // The runtime's own agents are declared by no file, so a broken one does not stop them (`check`'s scenarios).
183
+ if (!signIn && asking.id !== undefined && RUNTIME_AGENTS.includes(asking.id)) {
184
+ return principalOf(asking.id, people)!;
185
+ }
186
+ throw new NotAdmittedError(who, problems.map(problemLine));
64
187
  }
65
- return resources;
188
+ if (!declaresPeople(people)) {
189
+ const given = Principal.safeParse(asking).data;
190
+ if (given) return given;
191
+ } else {
192
+ const id = signIn ? personOf(asking, people) : asking.id;
193
+ const admitted = id === undefined ? undefined : principalOf(id, people);
194
+ if (admitted) return admitted;
195
+ }
196
+ throw new NotAdmittedError(who);
66
197
  }
67
198
 
68
199
  interface FileEntry {
@@ -82,12 +213,23 @@ interface FileEntry {
82
213
  *
83
214
  * Problems: anything outside the subset, a constructor or data file the config does not know, a
84
215
  * type the connector does not declare, a value the type refuses (a reference where its type
85
- * takes none among them), a reference cycle, and two resources of one type with one name.
216
+ * takes none among them), a reference cycle, and two resources of one type with one identity.
217
+ *
218
+ * The collections `resources/data.ts` declares are listed as they are read (`collections`),
219
+ * never matched to a connector, a resource type or the lockfile: the runtime imports that file.
220
+ *
221
+ * Each declaration is joined to its binding in the lockfile (`readLockfile`) by its key,
222
+ * `<file>#<export>`; a binding no export declares is an orphan. Problems: the lockfile's, a
223
+ * binding of another type than its declaration's, and two declarations bound to one object.
224
+ *
225
+ * `partial` reads part of the files: only those whose source it passes, and in them, an import of
226
+ * a connector not in `connectors`, or of a file left out, is left unread rather than reported.
86
227
  */
87
228
  export function readDataFiles(
88
229
  root: string | undefined,
89
230
  connectors: readonly Connector<any, any>[],
90
231
  types: ReadonlyMap<string, Resource> = resourceTypesOf(connectors),
232
+ partial?: (source: string) => boolean,
91
233
  ): DataFiles {
92
234
  const problems: ResourceProblem[] = [];
93
235
  if (root === undefined) {
@@ -95,13 +237,20 @@ export function readDataFiles(
95
237
  message:
96
238
  "The config has no root, so its data files cannot be found: make it with defineConfig, which records its file, or set its `root`",
97
239
  });
98
- return { resources: [], problems };
240
+ return { resources: [], bindings: {}, orphans: [], collections: [], problems };
99
241
  }
242
+ const lock = readLockfile(root);
243
+ problems.push(...lock.problems);
100
244
 
101
245
  const files = new Map<string, FileEntry>();
246
+ const left = new Set<string>();
102
247
  for (const path of dataFilesUnder(join(root, "resources"))) {
103
248
  const rel = relative(root, path).split(sep).join("/");
104
249
  const source = readFileSync(path, "utf8");
250
+ if (partial && !partial(source)) {
251
+ left.add(path);
252
+ continue;
253
+ }
105
254
  const { program, problems: syntax, at } = parsed(source, path);
106
255
  const data = readDataFile(program, path, join(root, "resources"));
107
256
  const problem = (offset: number, message: string) => problems.push({ file: rel, ...at(offset), message });
@@ -116,6 +265,8 @@ export function readDataFiles(
116
265
  const { id, info } = connector[VENDOR];
117
266
  if (info?.package) entries.set(`${info.package}/resources`, id);
118
267
  }
268
+ // The runtime's own (`peopleConnector`, package `sanoma`) is imported from either name of its entry.
269
+ if (entries.get("sanoma/resources") === "sanoma") for (const entry of SECRET_ENTRIES) entries.set(entry, "sanoma");
119
270
 
120
271
  // What each file's names stand for, and every resource read by its id.
121
272
  const vendors = new Map<FileEntry, Map<string, string>>();
@@ -127,6 +278,7 @@ export function readDataFiles(
127
278
  for (const imp of file.data.imports) {
128
279
  if (imp.kind === "constructors") {
129
280
  const vendor = entries.get(imp.source);
281
+ if (vendor === undefined && partial) continue;
130
282
  if (vendor === undefined) {
131
283
  file.problem(
132
284
  imp.start,
@@ -142,7 +294,9 @@ export function readDataFiles(
142
294
  }
143
295
  continue;
144
296
  }
145
- const other = files.get(resolve(dirname(path), imp.source));
297
+ const target = resolve(dirname(path), imp.source);
298
+ const other = files.get(target);
299
+ if (!other && left.has(target)) continue;
146
300
  if (!other) {
147
301
  file.problem(imp.start, `"${imp.source}" is not a data file under resources/`);
148
302
  } else if (!other.data.declared.includes(imp.imported)) {
@@ -175,7 +329,7 @@ export function readDataFiles(
175
329
  visiting.push(id);
176
330
  const result = build(file, exp);
177
331
  visiting.pop();
178
- if (result) {
332
+ if (result?.declared.name !== undefined) {
179
333
  const { vendor, type, name } = result.declared;
180
334
  const key = `${vendor}.${type} ${JSON.stringify(name)}`;
181
335
  const first = byName.get(key);
@@ -231,26 +385,68 @@ export function readDataFiles(
231
385
  }
232
386
  }
233
387
 
388
+ // Each binding against its declaration: of its type, and the only one bound to its object.
389
+ const boundBy = new Map<string, string>();
390
+ for (const [id, { file, exp }] of declarations) {
391
+ const binding = lock.bindings[id];
392
+ // An unknown constructors import was reported with the import.
393
+ const vendor = vendors.get(file)!.get(exp.vendor);
394
+ if (binding === undefined || vendor === undefined) continue;
395
+ const type = `${vendor}.${exp.type}`;
396
+ const object = `${binding.type} ${binding.id}`;
397
+ const first = boundBy.get(object);
398
+ const why =
399
+ binding.type !== type
400
+ ? `${id} is bound to a ${binding.type} in ${LOCKFILE}, but declares a ${type}`
401
+ : first !== undefined
402
+ ? `${id} is bound to the same ${object} as ${first} in ${LOCKFILE}: bind each object once`
403
+ : undefined;
404
+ if (why === undefined) boundBy.set(object, id);
405
+ else {
406
+ // Left out, as any declaration with a problem, and so is any that names it.
407
+ file.problem(exp.start, why);
408
+ done.set(id, undefined);
409
+ }
410
+ }
411
+ const keys = new Set([...files.values()].flatMap((f) => f.data.declared.map((name) => `${f.rel}#${name}`)));
412
+ const orphans = Object.entries(lock.bindings)
413
+ .filter(([key]) => !keys.has(key))
414
+ .map(([key, binding]) => ({ key, ...binding }));
415
+
234
416
  const resources: DeclaredResource[] = [];
235
417
  for (const [id, { file, exp }] of declarations) {
236
418
  const result = declare(id);
237
419
  if (!result) continue;
238
- const { vendor, type, name, desired } = result.declared;
239
- resources.push({ id, file: file.rel, vendor, type, name, span: [exp.start, exp.end], desired, refs: result.refs });
420
+ const { vendor, type, name: identity, desired } = result.declared;
421
+ const binding = lock.bindings[id]?.id;
422
+ resources.push({
423
+ id,
424
+ file: file.rel,
425
+ vendor,
426
+ type,
427
+ name: identity ?? exp.name,
428
+ ...(identity !== undefined && { identity }),
429
+ ...(binding !== undefined && { binding }),
430
+ span: [exp.start, exp.end],
431
+ desired,
432
+ refs: result.refs,
433
+ });
240
434
  }
435
+ const collections = [...files.values()].flatMap(({ rel, data }) =>
436
+ data.collections.map((c) => ({
437
+ id: `${rel}#${c.export}`,
438
+ file: rel,
439
+ name: c.name,
440
+ span: [c.start, c.end] as Span,
441
+ })),
442
+ );
241
443
  problems.sort(
242
444
  (a, b) =>
243
445
  (a.file ?? "").localeCompare(b.file ?? "") || (a.line ?? 0) - (b.line ?? 0) || (a.column ?? 0) - (b.column ?? 0),
244
446
  );
245
- return { resources, problems };
447
+ return { resources, bindings: lock.bindings, orphans, collections, problems };
246
448
  }
247
449
 
248
- /** The data files under `dir`, sorted: regular `.ts` files, less tests, declarations and `node_modules`; none when `dir` is not a directory. */
249
- function dataFilesUnder(dir: string): string[] {
250
- if (!statSync(dir, { throwIfNoEntry: false })?.isDirectory()) return [];
251
- return readdirSync(dir, { recursive: true, withFileTypes: true })
252
- .filter((d) => d.isFile() && d.name.endsWith(".ts") && !d.name.endsWith(".test.ts") && !d.name.endsWith(".d.ts"))
253
- .map((d) => join(d.parentPath, d.name))
254
- .filter((path) => !relative(dir, path).split(sep).includes("node_modules"))
255
- .toSorted();
256
- }
450
+ /** The data files under `dir`, sorted: regular `.ts` files, less tests, declarations and `node_modules`. */
451
+ const dataFilesUnder = (dir: string): string[] =>
452
+ filesUnder(dir, (name) => name.endsWith(".ts") && !name.endsWith(".test.ts") && !name.endsWith(".d.ts"));
package/src/run.ts CHANGED
@@ -1,10 +1,14 @@
1
- import type { ApprovalState, Principal, WorkflowDefinition } from "./define.ts";
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ import type { Collection } from "./collection.ts";
3
+ import type { ApprovalState, Approver, Principal, WorkflowDefinition } from "./define.ts";
2
4
  import type { Fake } from "./fake.ts";
3
5
  import type { LedgerStore } from "./ledger.ts";
4
6
  import type { DriverFn, Op } from "./op.ts";
5
7
  import type { CallNode } from "./outline.ts";
6
8
  import type { Policy } from "./policy.ts";
7
9
  import type { Resource } from "./resource.ts";
10
+ import type { Secrets } from "./secret.ts";
11
+ import type { Source } from "./source.ts";
8
12
  import type { Steps } from "./steps.ts";
9
13
 
10
14
  /** What the DBOS workflow receives: the workflow's input, who started the run, and for a sandbox run its scenario. */
@@ -13,6 +17,18 @@ export interface RunArgs {
13
17
  startedBy: Principal;
14
18
  /** The scenario a sandbox run is seeded from; the run calls the fakes instead of the drivers. */
15
19
  sandbox?: string;
20
+ /**
21
+ * The policy's hold on the start (`workflow:<name>`), as `SanomaClient.start` was answered: the
22
+ * run waits for this approval before its body runs.
23
+ */
24
+ hold?: StartHold;
25
+ }
26
+
27
+ /** A start the policy held: whom it asks, the approval's title, and the rules that held it. */
28
+ export interface StartHold {
29
+ approver: Approver;
30
+ title: string;
31
+ reasons?: string[];
16
32
  }
17
33
 
18
34
  /**
@@ -31,11 +47,19 @@ export interface WorkerState {
31
47
  fakes: Map<string, () => Fake<any, any>>;
32
48
  /** Where sandbox runs' scenarios are, the resource types their steps may name, and the steps the connectors ship. */
33
49
  scenarios?: URL;
50
+ /** The worker's `source`, when it was given one. */
51
+ source?: Source;
34
52
  types: ReadonlyMap<string, Resource>;
53
+ /** The collections, by name, which scenarios' steps may name (`data.<name>`). */
54
+ collections: ReadonlyMap<string, Collection>;
35
55
  steps: readonly Steps[];
36
56
  /** Its `version`, when it has one, is recorded with each of its decisions. */
37
57
  policy: Policy;
58
+ /** The config's directory, whose data files declare the people an approval's decider is checked against (`readPeople`). */
59
+ root?: string;
38
60
  ledger: LedgerStore;
61
+ /** The stored secrets, for a live run's drivers (`CallContext.secrets`). */
62
+ secrets: Secrets;
39
63
  /**
40
64
  * The config's workflows by name, the built-in `drift` among them. A run takes its definition
41
65
  * from here, by the name it was registered under: a process registers each name with DBOS once,
@@ -62,6 +86,8 @@ export interface Run {
62
86
  sandbox?: string;
63
87
  /** A sandbox run's own fakes' functions, by operation id, from `seedSandbox`; unset for a live run. */
64
88
  fakeDrivers?: Map<string, DriverFn>;
89
+ /** A sandbox run's own secrets, in memory, from `seedSandbox`; unset for a live run. */
90
+ fakeSecrets?: Secrets;
65
91
  approvals: ApprovalState[];
66
92
  /** The next ledger `seq`. Advanced only outside steps, so a replay counts the same way. */
67
93
  seq: number;
@@ -89,3 +115,9 @@ export interface WorkflowOutline {
89
115
  lineStarts: readonly number[];
90
116
  calls: readonly CallNode[];
91
117
  }
118
+
119
+ /**
120
+ * The run whose workflow body is running: what a built-in's own steps read of it (who started
121
+ * it, whether it is a sandbox run, the worker's source and policy), as no `ctx` member gives them.
122
+ */
123
+ export const currentRun = new AsyncLocalStorage<Run>();
package/src/sandbox.ts CHANGED
@@ -6,6 +6,7 @@ import type { Fake, ResourceFake } from "./fake.ts";
6
6
  import { entry, type LedgerRecord, write } from "./ledger.ts";
7
7
  import type { DriverFn } from "./op.ts";
8
8
  import type { Run } from "./run.ts";
9
+ import { memorySecrets } from "./secret.ts";
9
10
  import { type Given, loadScenarios, type Scenario } from "./scenario.ts";
10
11
  import { errorMessage } from "./shared.ts";
11
12
 
@@ -90,6 +91,8 @@ export function seedResource(
90
91
  export async function seedSandbox(run: Run, name: string): Promise<void> {
91
92
  const { state } = run;
92
93
  let copies: Map<string, DriverFn> | undefined;
94
+ // The run's secrets, in memory as its fakes are: seeding and its calls share them.
95
+ const secrets = memorySecrets();
93
96
  const { seeds } = await DBOS.runStep(
94
97
  async () => {
95
98
  // Read in the step, so a recovered run never reads a feature file edited or renamed since.
@@ -126,7 +129,7 @@ export async function seedSandbox(run: Run, name: string): Promise<void> {
126
129
  for (const [i, given] of scenario.given.entries()) {
127
130
  if ("fault" in given) continue;
128
131
  if ("op" in given) {
129
- const call = { idempotencyKey: `${run.id}:seed:${i}`, runId: run.id, opId: given.op, attempt: 1 };
132
+ const call = { idempotencyKey: `${run.id}:seed:${i}`, runId: run.id, opId: given.op, attempt: 1, secrets };
130
133
  const output = await drivers.get(given.op)!(given.input, call);
131
134
  seeded.push({ op: given.op, input: given.input, output });
132
135
  continue;
@@ -153,5 +156,6 @@ export async function seedSandbox(run: Run, name: string): Promise<void> {
153
156
  );
154
157
  }
155
158
  run.fakeDrivers = copies;
159
+ run.fakeSecrets = secrets;
156
160
  await write(run, entry(run, { type: "scenario.seeded", scenario: name, seeds }));
157
161
  }