@sanoma-ai/workflows 0.9.1 → 0.10.1

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 (207) hide show
  1. package/README.md +193 -50
  2. package/SECRET-SUPPORT.md +33 -0
  3. package/dist/apply-steps.js +5 -2
  4. package/dist/apply-steps.js.map +1 -1
  5. package/dist/apply.d.ts +3 -0
  6. package/dist/apply.js +27 -13
  7. package/dist/apply.js.map +1 -1
  8. package/dist/approvals.d.ts +1 -0
  9. package/dist/approvals.js +6 -1
  10. package/dist/approvals.js.map +1 -1
  11. package/dist/apps.d.ts +13 -24
  12. package/dist/apps.js +69 -68
  13. package/dist/apps.js.map +1 -1
  14. package/dist/ast.d.ts +9 -7
  15. package/dist/ast.js +2 -0
  16. package/dist/ast.js.map +1 -1
  17. package/dist/authentication.d.ts +54 -0
  18. package/dist/authentication.js +138 -0
  19. package/dist/authentication.js.map +1 -0
  20. package/dist/call-workflow.d.ts +5 -0
  21. package/dist/call-workflow.js +4 -3
  22. package/dist/call-workflow.js.map +1 -1
  23. package/dist/call.d.ts +3 -2
  24. package/dist/call.js +36 -7
  25. package/dist/call.js.map +1 -1
  26. package/dist/cedar.js +14 -1
  27. package/dist/cedar.js.map +1 -1
  28. package/dist/change-steps.js +7 -5
  29. package/dist/change-steps.js.map +1 -1
  30. package/dist/change.d.ts +7 -0
  31. package/dist/change.js +7 -0
  32. package/dist/change.js.map +1 -1
  33. package/dist/client.d.ts +62 -10
  34. package/dist/client.js +272 -47
  35. package/dist/client.js.map +1 -1
  36. package/dist/collection.d.ts +7 -0
  37. package/dist/collection.js +5 -2
  38. package/dist/collection.js.map +1 -1
  39. package/dist/config.d.ts +16 -4
  40. package/dist/config.js +89 -21
  41. package/dist/config.js.map +1 -1
  42. package/dist/connection.d.ts +389 -0
  43. package/dist/connection.js +657 -0
  44. package/dist/connection.js.map +1 -0
  45. package/dist/credentials.d.ts +7 -53
  46. package/dist/credentials.js +84 -247
  47. package/dist/credentials.js.map +1 -1
  48. package/dist/db.js +34 -2
  49. package/dist/db.js.map +1 -1
  50. package/dist/define.d.ts +9 -2
  51. package/dist/define.js.map +1 -1
  52. package/dist/definitions.d.ts +20 -0
  53. package/dist/definitions.js +16 -0
  54. package/dist/definitions.js.map +1 -0
  55. package/dist/deployment-key.d.ts +2 -0
  56. package/dist/deployment-key.js +22 -0
  57. package/dist/deployment-key.js.map +1 -0
  58. package/dist/describe.d.ts +15 -175
  59. package/dist/describe.js +122 -35
  60. package/dist/describe.js.map +1 -1
  61. package/dist/description.d.ts +532 -0
  62. package/dist/description.js +381 -0
  63. package/dist/description.js.map +1 -0
  64. package/dist/direct.js +7 -0
  65. package/dist/direct.js.map +1 -1
  66. package/dist/drift.d.ts +1 -1
  67. package/dist/drift.js +1 -1
  68. package/dist/drift.js.map +1 -1
  69. package/dist/env.d.ts +2 -0
  70. package/dist/env.js +2 -2
  71. package/dist/env.js.map +1 -1
  72. package/dist/fake.d.ts +13 -1
  73. package/dist/fake.js +70 -0
  74. package/dist/fake.js.map +1 -1
  75. package/dist/index.d.ts +11 -3
  76. package/dist/index.js +10 -2
  77. package/dist/index.js.map +1 -1
  78. package/dist/ledger.d.ts +2 -0
  79. package/dist/ledger.js +1 -1
  80. package/dist/ledger.js.map +1 -1
  81. package/dist/lint.d.ts +0 -9
  82. package/dist/lint.js +2 -17
  83. package/dist/lint.js.map +1 -1
  84. package/dist/load.d.ts +85 -0
  85. package/dist/load.js +391 -0
  86. package/dist/load.js.map +1 -0
  87. package/dist/lockfile.d.ts +2 -0
  88. package/dist/lockfile.js +8 -3
  89. package/dist/lockfile.js.map +1 -1
  90. package/dist/op.d.ts +22 -5
  91. package/dist/op.js +20 -2
  92. package/dist/op.js.map +1 -1
  93. package/dist/outline.d.ts +5 -0
  94. package/dist/outline.js +46 -5
  95. package/dist/outline.js.map +1 -1
  96. package/dist/paths.d.ts +18 -2
  97. package/dist/paths.js +19 -7
  98. package/dist/paths.js.map +1 -1
  99. package/dist/plugin.d.ts +0 -11
  100. package/dist/plugin.js +3 -18
  101. package/dist/plugin.js.map +1 -1
  102. package/dist/policy.d.ts +2 -0
  103. package/dist/policy.js.map +1 -1
  104. package/dist/resource.d.ts +10 -4
  105. package/dist/resource.js +11 -4
  106. package/dist/resource.js.map +1 -1
  107. package/dist/resources.d.ts +33 -76
  108. package/dist/resources.js +219 -227
  109. package/dist/resources.js.map +1 -1
  110. package/dist/run.d.ts +4 -0
  111. package/dist/run.js.map +1 -1
  112. package/dist/sandbox.d.ts +5 -2
  113. package/dist/sandbox.js +56 -7
  114. package/dist/sandbox.js.map +1 -1
  115. package/dist/scenario.d.ts +5 -0
  116. package/dist/scenario.js +3 -2
  117. package/dist/scenario.js.map +1 -1
  118. package/dist/scenarios.js +7 -1
  119. package/dist/scenarios.js.map +1 -1
  120. package/dist/schema.js +8 -0
  121. package/dist/schema.js.map +1 -1
  122. package/dist/secret-store-check.d.ts +11 -0
  123. package/dist/secret-store-check.js +76 -0
  124. package/dist/secret-store-check.js.map +1 -0
  125. package/dist/secret-store.d.ts +106 -0
  126. package/dist/secret-store.js +228 -0
  127. package/dist/secret-store.js.map +1 -0
  128. package/dist/secret.d.ts +2 -0
  129. package/dist/secret.js +32 -0
  130. package/dist/secret.js.map +1 -1
  131. package/dist/shared.d.ts +21 -11
  132. package/dist/shared.js +41 -7
  133. package/dist/shared.js.map +1 -1
  134. package/dist/sites.d.ts +4 -93
  135. package/dist/sites.js +76 -84
  136. package/dist/sites.js.map +1 -1
  137. package/dist/steps.d.ts +3 -0
  138. package/dist/steps.js.map +1 -1
  139. package/dist/version.d.ts +2 -1
  140. package/dist/version.js +2 -1
  141. package/dist/version.js.map +1 -1
  142. package/dist/wakeup-driver.js +15 -6
  143. package/dist/wakeup-driver.js.map +1 -1
  144. package/dist/worker.js +12 -8
  145. package/dist/worker.js.map +1 -1
  146. package/dist/writer.d.ts +53 -14
  147. package/dist/writer.js +113 -43
  148. package/dist/writer.js.map +1 -1
  149. package/oxlint.json +1 -5
  150. package/package.json +6 -1
  151. package/src/apply-steps.ts +5 -2
  152. package/src/apply.ts +54 -21
  153. package/src/approvals.ts +7 -1
  154. package/src/apps.ts +71 -86
  155. package/src/ast.ts +6 -6
  156. package/src/authentication.ts +191 -0
  157. package/src/call-workflow.ts +5 -4
  158. package/src/call.ts +52 -9
  159. package/src/cedar.ts +14 -1
  160. package/src/change-steps.ts +7 -4
  161. package/src/change.ts +10 -0
  162. package/src/client.ts +346 -45
  163. package/src/collection.ts +10 -2
  164. package/src/config.ts +107 -19
  165. package/src/connection.ts +912 -0
  166. package/src/credentials.ts +94 -281
  167. package/src/db.ts +34 -2
  168. package/src/define.ts +7 -2
  169. package/src/definitions.ts +31 -0
  170. package/src/deployment-key.ts +23 -0
  171. package/src/describe.ts +150 -227
  172. package/src/description.ts +426 -0
  173. package/src/direct.ts +7 -0
  174. package/src/drift.ts +10 -2
  175. package/src/env.ts +4 -2
  176. package/src/fake.ts +71 -0
  177. package/src/index.ts +53 -1
  178. package/src/ledger.ts +3 -1
  179. package/src/lint.ts +2 -17
  180. package/src/load.ts +466 -0
  181. package/src/lockfile.ts +10 -3
  182. package/src/op.ts +41 -7
  183. package/src/outline.ts +53 -7
  184. package/src/paths.ts +21 -6
  185. package/src/plugin.ts +3 -24
  186. package/src/policy.ts +2 -0
  187. package/src/resource.ts +24 -6
  188. package/src/resources.ts +246 -301
  189. package/src/run.ts +4 -0
  190. package/src/sandbox.ts +61 -7
  191. package/src/scenario.ts +24 -6
  192. package/src/scenarios.ts +7 -1
  193. package/src/schema.ts +10 -0
  194. package/src/secret-store-check.ts +93 -0
  195. package/src/secret-store.ts +314 -0
  196. package/src/secret.ts +29 -0
  197. package/src/shared.ts +44 -10
  198. package/src/sites.ts +85 -179
  199. package/src/steps.ts +3 -2
  200. package/src/version.ts +2 -1
  201. package/src/wakeup-driver.ts +22 -6
  202. package/src/worker.ts +12 -8
  203. package/src/writer.ts +145 -56
  204. package/dist/datafile.d.ts +0 -97
  205. package/dist/datafile.js +0 -479
  206. package/dist/datafile.js.map +0 -1
  207. package/src/datafile.ts +0 -601
package/src/describe.ts CHANGED
@@ -1,53 +1,58 @@
1
- import { join } from "node:path";
1
+ import { resolve } from "node:path";
2
2
  import type { z } from "zod";
3
3
  import { type AppEntry, appsAt } from "./apps.ts";
4
- import { type LintProblem, readSource, type SourceFile } from "./ast.ts";
5
- import { type Field, recordSchemaOf } from "./collection.ts";
4
+ import { readSource, type SourceFile } from "./ast.ts";
5
+ import { readPolicyFiles } from "./cedar.ts";
6
+ import { recordSchemaOf } from "./collection.ts";
6
7
  import { connectorsOf, resolveConfig, rootOf, type SanomaConfig } from "./config.ts";
7
- import { type Builtin, jsonSchemaOf, opIdsOf, type Trigger, type Use } from "./define.ts";
8
+ import { type Builtin, jsonSchemaOf, opIdsOf, type Use } from "./define.ts";
9
+ import {
10
+ CAPABILITIES,
11
+ type CollectionEntry,
12
+ type ConfigDescription,
13
+ type OpEntry,
14
+ type ResourceTypeEntry,
15
+ type VendorEntry,
16
+ type WorkflowEntry,
17
+ } from "./description.ts";
8
18
  import { isDirect } from "./direct.ts";
19
+ import { type CredentialStatus, credentialsOf } from "./env.ts";
9
20
  import { lintWorkflow } from "./lint.ts";
10
- import { type Effect, VENDOR, type VendorInfo } from "./op.ts";
11
- import { type Outline, outlineWithSource } from "./outline.ts";
21
+ import { fresh, stampOf, stampsOf } from "./load.ts";
22
+ import { LOCKFILE } from "./lockfile.ts";
23
+ import { VENDOR, type VendorInfo } from "./op.ts";
24
+ import { outlineWithSource } from "./outline.ts";
25
+ import { APPS_DIR, kindDirs } from "./paths.ts";
12
26
  import { allowAll, policyOpOf } from "./policy.ts";
13
- import { type Resource, type ResourceFields, resourceTypesOf, valueAt } from "./resource.ts";
14
- import {
15
- type DataFiles,
16
- type DeclaredCollection,
17
- type DeclaredResource,
18
- inAppPeople,
19
- readDataFiles,
20
- type ResourceProblem,
21
- } from "./resources.ts";
27
+ import { type Resource, resourceTypesOf, valueAt } from "./resource.ts";
28
+ import { type DeclaredResource, inAppPeople, loadDataFiles, type ResourceProblem } from "./resources.ts";
22
29
  import { apiNameProblem } from "./sdk.ts";
23
- import { apiNameOf, BUILT_IN_WORKFLOWS, DATA_VENDOR, problemLine } from "./shared.ts";
30
+ import { apiNameOf, BUILT_IN_WORKFLOWS, DATA_VENDOR, errorMessage, problemLine } from "./shared.ts";
24
31
  import { readSites, type SiteEntry } from "./sites.ts";
25
32
 
26
33
  // `@sanoma-ai/workflows/describe`: what a UI renders from, apart from the main entry.
34
+ // What `describeConfig` answers, as zod schemas and the types they infer (`./description`).
35
+ export * from "./description.ts";
27
36
  export { outlineWorkflow, type Outline, type OutlineNode, type Span } from "./outline.ts";
28
- // The reader parses data files with oxc-parser too.
29
- export type { LintProblem } from "./ast.ts";
30
- export {
31
- admit,
32
- readDataFiles,
33
- readPeople,
34
- type DataFiles,
35
- type DeclaredCollection,
36
- type DeclaredPeople,
37
- type DeclaredResource,
38
- type ResourceProblem,
39
- } from "./resources.ts";
37
+ export { admit, loadDataFiles, readPeople, type DataFiles, type DeclaredPeople } from "./resources.ts";
40
38
  // The writer: setting a resource's fields in its data file, and rendering a new one.
41
- export { formatOptions, renderDataFile, setFields, type DataFileContent } from "./writer.ts";
42
- export { Reference, type DataValue } from "./datafile.ts";
39
+ export {
40
+ type DataFileContent,
41
+ type DataValue,
42
+ formatOptions,
43
+ NotLiteral,
44
+ Reference,
45
+ renderDataFile,
46
+ setFields,
47
+ } from "./writer.ts";
43
48
 
44
49
  /**
45
- * The resources the config's data files declare, read without importing or running them; throws
46
- * one error listing every problem, each at `file:line:column`, for a test or CI to fail on.
50
+ * The resources the config's data files declare, as loading them makes them (`loadDataFiles`);
51
+ * throws one error listing every problem, one per line, for a test or CI to fail on.
47
52
  * `describeConfig` lists the same, with the problems beside them instead.
48
53
  */
49
54
  export function readResources(config: SanomaConfig): DeclaredResource[] {
50
- const { resources, problems } = readDataFiles(rootOf(config), connectorsOf(config));
55
+ const { resources, problems } = loadDataFiles(rootOf(config), connectorsOf(config));
51
56
  if (problems.length) {
52
57
  const lines = problems.map((p) => ` ${problemLine(p)}`);
53
58
  throw new Error(`The resources' data files have problems:\n${lines.join("\n")}`);
@@ -64,19 +69,10 @@ export {
64
69
  type Bindings,
65
70
  type Rebind,
66
71
  } from "./lockfile.ts";
67
- // The apps under `apps/`, read without running their `app.ts`.
68
- export { crossAppImports, readApps, type AppEntry } from "./apps.ts";
72
+ // The apps under `apps/`, with what their `app.ts` declares.
73
+ export { crossAppImports, readApps } from "./apps.ts";
69
74
  // What the sites call, read from their source: a workflow's Used by.
70
- export {
71
- readSites,
72
- type SiteCall,
73
- type SiteEntry,
74
- type SitePage,
75
- siteContentPages,
76
- siteIcons,
77
- siteLocales,
78
- sitesUnder,
79
- } from "./sites.ts";
75
+ export { readSites, siteContentPages, siteIcons, siteLocales, sitesUnder } from "./sites.ts";
80
76
  // The generated SDK, `.sanoma/api.ts`: a site's `api` typed from this description.
81
77
  export { API_FILE, generateApi, writeApi, writeGenerated } from "./sdk.ts";
82
78
  // The Cedar schema, `.sanoma/schema.cedarschema`: what a company's policies are validated and decided against.
@@ -87,160 +83,51 @@ export { previewRequests } from "./shared.ts";
87
83
  // The apply workflow's plan and report, which the app renders.
88
84
  export type { ApplyInput, ApplyPlan, ApplyReport, ApplyResult, PlanAnswer, PlanStep } from "./apply.ts";
89
85
 
90
- /** What a workflow is, read from its definition: enough to draw a start form and show what it may call. */
91
- export interface WorkflowEntry {
92
- /** Its name: `<app>.<name>` for one an app's folder defines. */
93
- name: string;
94
- /** The app whose folder defines it (`apps/<app>/workflows/`); absent for one at the root. */
95
- app?: string;
96
- title?: string;
97
- /** `manual`, or the schedule it starts on (`defineWorkflow`'s `trigger`). */
98
- trigger: Trigger;
99
- /**
100
- * Set for a workflow the runtime adds itself, `drift`, `apply` or `call`, rather than the config: a label. It
101
- * starts like any other.
102
- */
103
- builtin?: true;
104
- /** The input, as JSON Schema (draft 2020-12), from the zod schema. */
105
- input: Record<string, unknown>;
106
- /** Operation ids the workflow may call, in the order declared. */
107
- ops: string[];
108
- /** Built-ins the workflow may call. */
109
- builtins: Builtin[];
110
- /** The file that defined it, absolute. */
111
- file: string;
112
- /** What its `run` calls, in order, read from its file (`outlineWorkflow`), which the worker holds its runs to. */
113
- outline: Outline;
114
- /**
115
- * What is wrong in its file, by position: what `lintWorkflow` finds (syntax, an import a
116
- * workflow may not make, `instanceof` against the runtime's error classes, what the outline
117
- * cannot draw in each `run`), read the one way the outline is, so a check sees what the
118
- * worker refuses; and, at 1:1, an outline error that is the file's (it cannot be read, or holds
119
- * no workflow of this name). A file defining several workflows gives each its problems. Empty
120
- * for the built-in `drift`, which the package's tests hold to the lint.
121
- */
122
- problems: LintProblem[];
123
- /**
124
- * The text the outline's spans index into: the workflow's file, with `\n` line endings.
125
- * Absent only when the outline is `{ error }`. Whole files: a server may keep it from clients.
126
- */
127
- source?: string;
128
- }
129
-
130
- /** What an operation is, read from its connector: enough to badge it and show its contract. */
131
- export interface OpEntry {
132
- id: string;
133
- vendor: string;
134
- resource: string;
135
- name: string;
136
- effect: Effect;
137
- idempotent: boolean;
138
- description?: string;
139
- /** How a scenario's steps name it: Cucumber expressions over its input's fields. */
140
- phrases?: { given?: string; expect?: string };
141
- /** Whether it may be called directly, outside a run (`isDirect`): the one rule, read here, never derived again. */
142
- direct: boolean;
143
- /** What a call sends, as JSON Schema (`io: "input"`: fields with defaults are optional). */
144
- input: Record<string, unknown>;
145
- /** What a call returns once its schema has parsed the vendor's reply (`io: "output"`). */
146
- output: Record<string, unknown>;
147
- }
148
-
149
- /** Who a vendor is, from its connector's `VendorInfo`: enough to name it, show its logo and link to its connector. */
150
- export interface VendorEntry {
151
- /** The connector's `title`, else the vendor's id. */
152
- title: string;
153
- /** The logo as `data:image/svg+xml` URLs, for an `<img>`: `dark` is for dark backgrounds. */
154
- logo?: { src: string; dark?: string };
155
- /** The connector's npm package name. */
156
- package?: string;
157
- /** The connector package's `homepage` from its package.json: where its code and README are, as an https URL. */
158
- homepage?: string;
159
- /** Whether the vendor's connector attaches a fake (`withSetup`), for sandbox runs. */
160
- fake: boolean;
161
- }
162
-
163
- /** A resource type a connector declares with `defineResource`. */
164
- export interface ResourceTypeEntry {
165
- /** `<vendor>.<type>`, such as `github.repository`. */
166
- id: string;
167
- vendor: string;
168
- type: string;
169
- title: string;
170
- /** How a declared resource's identity is made, such as `name` or `repository_id:pattern`; absent when it is found only by its binding. */
171
- identity?: string;
172
- /** Its flagged fields, by dotted path. */
173
- fields: ResourceFields;
174
- /**
175
- * Its state, as a read returns it, as JSON Schema (`io: "output"`), with `$id`
176
- * `sanoma:resource-type/<id>`: the operations' contracts `$ref` it instead of repeating it.
177
- */
178
- schema: Record<string, unknown>;
179
- /** Its operations' ids, sorted: `<id>.import` and `<id>.read`, and for a writable type its change operations. */
180
- ops: string[];
181
- }
182
-
183
- /** A collection the config declares (`collection()`). */
184
- export interface CollectionEntry {
185
- /** Its name: `<app>.<name>` for one an app's `resources/data.ts` declares. */
186
- name: string;
187
- /** The app whose folder declares it (`apps/<app>/resources/data.ts`); absent for one at the root. */
188
- app?: string;
189
- title?: string;
190
- /** The field each record is found by. */
191
- key: string;
192
- /** Its fields as declared. */
193
- fields: Record<string, Field>;
194
- /** What `save` accepts, as JSON Schema, with `$id` `sanoma:collection/<name>`. */
195
- record: Record<string, unknown>;
196
- /** Its operations' ids: `data.<name>.get`, `.list`, `.save` and `.delete`. */
197
- ops: string[];
198
- /** Where `resources/data.ts` declares it, read as the data files are; absent for one declared elsewhere. */
199
- declared?: DeclaredCollection;
200
- }
201
-
202
86
  /**
203
- * A plain-JSON description of what a config can do: its workflows, the operations they call,
204
- * and whether a policy gates them. The app server builds it from the config and sends it to the
205
- * browser, so the UI shows exactly what the worker enforces. Never written by hand.
87
+ * `describeConfig(config)`, kept and made again only once the files it loaded have changed: each
88
+ * company file a load read and its folder (`stampsOf`), the lockfile, and the folders a new data
89
+ * file, app or site would appear in, one `stat` each per call. What the config imports (its
90
+ * workflows, connectors and policies) is read once, as the worker reads it. When describing again
91
+ * throws, the last description stands until the files change again.
206
92
  */
207
- export interface ConfigDescription {
208
- appName: string;
209
- /** The application version runs started now are stamped with. */
210
- version: string;
211
- workflows: WorkflowEntry[];
212
- ops: OpEntry[];
213
- /** Every vendor the operations are from, by its id (an `OpEntry`'s `vendor`). */
214
- vendors: Record<string, VendorEntry>;
215
- /** The resource types the connectors declare, by `id`. */
216
- resourceTypes: ResourceTypeEntry[];
217
- /** The config's collections, by name. */
218
- collections: CollectionEntry[];
219
- /**
220
- * The resources the data files under `resources/` declare, by file and then in file order,
221
- * less any with a problem and any that names one.
222
- */
223
- resources: DeclaredResource[];
224
- /** Each binding in the lockfile (`LOCKFILE`) whose key no data file declares, as `readDataFiles` reports it. */
225
- orphans: DataFiles["orphans"];
226
- /**
227
- * What is wrong in the data files (`readDataFiles`) and the apps (a folder of `apps/` that is no
228
- * app, an `app.ts` that cannot be read, a collection or site at the root named as an app is),
229
- * each at its file, relative to the root, line and column. A workflow's are on its entry.
230
- */
231
- problems: ResourceProblem[];
232
- /** The apps under `apps/` (`readApps`), by name. Each workflow, collection and site an app declares names it (`app`). */
233
- apps: AppEntry[];
234
- /**
235
- * `defined` when the root has `policies/**\/*.cedar` (else the policy is `allowAll`), with their
236
- * `version`, or while they have `problems` (`policyProblems`), none: then every call and start is refused.
237
- */
238
- policy: { defined: boolean; version?: string; problems?: string[] };
239
- /**
240
- * The sites under `sites/`, by name, with what each calls (`readSites`), and its calls to what
241
- * the config does not have as its problems; none when the root has no `sites/`.
242
- */
243
- sites: SiteEntry[];
93
+ export function liveDescription(config: SanomaConfig): () => ConfigDescription {
94
+ const root = rootOf(config);
95
+ if (root === undefined) {
96
+ const description = describeConfig(config);
97
+ return () => description;
98
+ }
99
+ /** The description and what it read; `last`, kept (and said) when describing again throws. */
100
+ const described = (last?: ConfigDescription) => {
101
+ // The folders and the lockfile as they are before describing, so a change while it reads is seen after.
102
+ const extra = [
103
+ resolve(root, LOCKFILE),
104
+ resolve(root, APPS_DIR),
105
+ ...kindDirs(root, "sites"),
106
+ ...kindDirs(root, "resources"),
107
+ ].map((path) => [path, stampOf(path)] as const);
108
+ let description: ConfigDescription;
109
+ try {
110
+ description = describeConfig(config);
111
+ } catch (err) {
112
+ if (last === undefined) throw err;
113
+ console.warn(
114
+ `sanoma: the files could not be described again, so the last description stands: ${errorMessage(err)}`,
115
+ );
116
+ description = last;
117
+ }
118
+ return { description, stamps: new Map([...extra, ...stampsOf(root)]) };
119
+ };
120
+ let now = described();
121
+ // The workflows as the config imported them, which the worker runs: their outlines and sources
122
+ // from the first description, never a file edited since.
123
+ const { workflows } = now.description;
124
+ return () => {
125
+ if (!fresh(now.stamps)) {
126
+ now = described(now.description);
127
+ now.description.workflows = workflows;
128
+ }
129
+ return now.description;
130
+ };
244
131
  }
245
132
 
246
133
  /**
@@ -254,31 +141,45 @@ export interface ConfigDescription {
254
141
  export function describeConfig(config: SanomaConfig): ConfigDescription {
255
142
  const resolved = resolveConfig(config);
256
143
  const vendors: Record<string, VendorEntry> = {};
144
+ // The variables each driver declares, without a value: their names and manuals alone.
145
+ const env = credentialsOf(resolved.driversByVendor, () => undefined);
257
146
  // A vendor whose operations are split over several connectors is named by the first; its
258
147
  // resource types are every connector's.
259
148
  for (const connector of config.connectors) {
260
149
  const { id, info } = connector[VENDOR];
261
- vendors[id] ??= vendorEntry(id, info, resolved.fakes.has(id));
150
+ vendors[id] ??= vendorEntry(id, info, resolved.fakes.has(id), env.get(id));
262
151
  }
263
152
  const resources = resourceTypesOf(connectorsOf(config));
153
+ // The types a host's `source` names: the repositories a host clones.
154
+ const sources = new Set(
155
+ [...resources.values()].flatMap(({ host, fields }) => {
156
+ const named = host && fields.references?.[host.inputs.source]?.type;
157
+ return named === undefined ? [] : [named].flat();
158
+ }),
159
+ );
264
160
  // Each resource type's state is described once, on its entry, and referenced from its operations.
265
161
  const refs = new Map<z.ZodType, string>([...resources].map(([id, r]) => [r.schema, resourceTypeUri(id)]));
266
162
  const resourceTypes: ResourceTypeEntry[] = [...resources]
267
- .map(([id, { vendor, type, title, identity, fields, schema, ops }]) => ({
268
- id,
269
- vendor,
270
- type,
271
- title,
272
- identity,
273
- fields,
274
- schema: { $id: resourceTypeUri(id), ...toJsonSchema(schema, `${id} state`, "output") },
275
- ops: Object.keys(ops)
276
- .map((name) => `${id}.${name}`)
277
- .toSorted(),
278
- }))
163
+ .map(([id, spec]) => {
164
+ const { vendor, type, title, identity, fields, schema, ops } = spec;
165
+ const capabilities = CAPABILITIES.filter((c) => (c === "source" ? sources.has(id) : spec[c] !== undefined));
166
+ return {
167
+ id,
168
+ vendor,
169
+ type,
170
+ title,
171
+ identity,
172
+ fields,
173
+ schema: { $id: resourceTypeUri(id), ...toJsonSchema(schema, `${id} state`, "output") },
174
+ ops: Object.keys(ops)
175
+ .map((name) => `${id}.${name}`)
176
+ .toSorted(),
177
+ ...(capabilities.length ? { capabilities } : {}),
178
+ };
179
+ })
279
180
  .toSorted((a, b) => a.id.localeCompare(b.id));
280
181
 
281
- const dataFiles = readDataFiles(resolved.root, connectorsOf(config), resources);
182
+ const dataFiles = loadDataFiles(resolved.root, connectorsOf(config), resources);
282
183
  const declared = new Map(dataFiles.collections.map((d) => [d.name, d]));
283
184
  const collections: CollectionEntry[] = [...resolved.collections.values()]
284
185
  .map((c) => ({
@@ -344,6 +245,28 @@ export function describeConfig(config: SanomaConfig): ConfigDescription {
344
245
  );
345
246
 
346
247
  return {
248
+ secrets: {
249
+ store: resolved.secrets.store?.id ?? "postgres",
250
+ namespace: resolved.secrets.namespace ?? "default",
251
+ bindings: Object.fromEntries(
252
+ Object.entries(resolved.secrets.bindings ?? {}).map(([name, b]) => [
253
+ name,
254
+ {
255
+ path: b.bootstrap ?? b.env ?? b.path!,
256
+ management: b.management ?? (b.env || b.bootstrap ? "external" : "sanoma"),
257
+ store: b.bootstrap ? "bootstrap" : b.env ? "environment" : (resolved.secrets.store?.id ?? "postgres"),
258
+ },
259
+ ]),
260
+ ),
261
+ },
262
+ connections: [...resolved.connections.catalog.definitions.values()].map((d) => ({
263
+ id: d.$connection,
264
+ vendor: d.vendor,
265
+ method: d.method,
266
+ scopes: d.scopes,
267
+ ...(d.account && { account: d.account }),
268
+ default: d.default,
269
+ })),
347
270
  appName: resolved.appName,
348
271
  version: resolved.version,
349
272
  workflows,
@@ -358,13 +281,14 @@ export function describeConfig(config: SanomaConfig): ConfigDescription {
358
281
  ...dataFiles.problems,
359
282
  ...appProblems,
360
283
  ...appClashes(apps, collections, sites),
361
- ...domainProblems(resolved.root, dataFiles.resources, resources, sites, read),
284
+ ...domainProblems(dataFiles.resources, resources, sites),
362
285
  ],
363
286
  apps,
364
287
  policy: {
365
288
  defined: resolved.policy !== allowAll,
366
289
  ...(resolved.policy.version === undefined ? {} : { version: resolved.policy.version }),
367
290
  ...(resolved.policyProblems.length ? { problems: resolved.policyProblems } : {}),
291
+ files: resolved.root === undefined ? [] : readPolicyFiles(resolved.root),
368
292
  },
369
293
  sites,
370
294
  };
@@ -404,11 +328,9 @@ function appClashes(
404
328
  * only a public site at a domain, and one site at each.
405
329
  */
406
330
  function domainProblems(
407
- root: string | undefined,
408
331
  declared: readonly DeclaredResource[],
409
332
  types: ReadonlyMap<string, Resource>,
410
333
  sites: readonly SiteEntry[],
411
- read: (path: string) => SourceFile | string,
412
334
  ): ResourceProblem[] {
413
335
  const isPublic = new Set(sites.filter((site) => site.public).map((site) => site.name));
414
336
  const problems: ResourceProblem[] = [];
@@ -426,22 +348,11 @@ function domainProblems(
426
348
  const others = `${names.length === 2 ? "both " : ""}${names.slice(0, -1).join(", ")}`;
427
349
  messages.push(`${domain} is the domain of ${others} and ${names.at(-1)}`);
428
350
  }
429
- if (root !== undefined) problems.push(...messages.map((message) => problemAt(root, resource, read, message)));
351
+ problems.push(...messages.map((message) => ({ ...resource.at, message })));
430
352
  }
431
353
  return problems;
432
354
  }
433
355
 
434
- /** `message` at a declaration's `export const`. */
435
- function problemAt(
436
- root: string,
437
- resource: DeclaredResource,
438
- read: (path: string) => SourceFile | string,
439
- message: string,
440
- ): ResourceProblem {
441
- const file = read(join(root, resource.file));
442
- return { file: resource.file, ...(typeof file === "string" ? {} : file.at(resource.span[0])), message };
443
- }
444
-
445
356
  /**
446
357
  * A site's `api` names each workflow `apiNameOf(name)` beside the vendors' operations and the
447
358
  * collections (`api.data`), so a workflow whose name comes out as another's, a vendor's id or
@@ -477,7 +388,12 @@ function memoised<A, R>(fn: (arg: A) => R): (arg: A) => R {
477
388
  /** A logo's SVG markup as a `data:` URL, for an `<img>`. */
478
389
  export const svgDataUrl = (svg: string) => `data:image/svg+xml,${encodeURIComponent(svg.trim())}`;
479
390
 
480
- const vendorEntry = (id: string, info: VendorInfo | undefined, fake: boolean): VendorEntry => ({
391
+ const vendorEntry = (
392
+ id: string,
393
+ info: VendorInfo | undefined,
394
+ fake: boolean,
395
+ env: readonly CredentialStatus[] | undefined,
396
+ ): VendorEntry => ({
481
397
  title: info?.title ?? id,
482
398
  ...(info?.logo && {
483
399
  logo: { src: svgDataUrl(info.logo.svg), ...(info.logo.dark && { dark: svgDataUrl(info.logo.dark) }) },
@@ -485,6 +401,13 @@ const vendorEntry = (id: string, info: VendorInfo | undefined, fake: boolean): V
485
401
  ...(info?.package && { package: info.package }),
486
402
  ...(info?.homepage && { homepage: info.homepage }),
487
403
  fake,
404
+ ...(env && {
405
+ credentials: env.map(({ name, description, optional }) => ({
406
+ name,
407
+ ...(description !== undefined && { description }),
408
+ ...(optional && { optional }),
409
+ })),
410
+ }),
488
411
  });
489
412
 
490
413
  /** Where a resource type's state schema is, for a `$ref`. */