@sanoma-ai/workflows 0.9.1 → 0.10.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 (190) hide show
  1. package/README.md +167 -50
  2. package/dist/apply-steps.js +5 -2
  3. package/dist/apply-steps.js.map +1 -1
  4. package/dist/apply.d.ts +3 -0
  5. package/dist/apply.js +27 -13
  6. package/dist/apply.js.map +1 -1
  7. package/dist/approvals.d.ts +1 -0
  8. package/dist/approvals.js +6 -1
  9. package/dist/approvals.js.map +1 -1
  10. package/dist/apps.d.ts +7 -24
  11. package/dist/apps.js +51 -67
  12. package/dist/apps.js.map +1 -1
  13. package/dist/ast.d.ts +9 -7
  14. package/dist/ast.js +2 -0
  15. package/dist/ast.js.map +1 -1
  16. package/dist/authentication.d.ts +24 -0
  17. package/dist/authentication.js +68 -0
  18. package/dist/authentication.js.map +1 -0
  19. package/dist/call-workflow.d.ts +5 -0
  20. package/dist/call-workflow.js +4 -3
  21. package/dist/call-workflow.js.map +1 -1
  22. package/dist/call.d.ts +3 -2
  23. package/dist/call.js +34 -7
  24. package/dist/call.js.map +1 -1
  25. package/dist/cedar.js +14 -1
  26. package/dist/cedar.js.map +1 -1
  27. package/dist/change-steps.js +7 -5
  28. package/dist/change-steps.js.map +1 -1
  29. package/dist/change.d.ts +7 -0
  30. package/dist/change.js +7 -0
  31. package/dist/change.js.map +1 -1
  32. package/dist/client.d.ts +10 -6
  33. package/dist/client.js +96 -43
  34. package/dist/client.js.map +1 -1
  35. package/dist/collection.d.ts +7 -0
  36. package/dist/collection.js +5 -2
  37. package/dist/collection.js.map +1 -1
  38. package/dist/config.d.ts +9 -0
  39. package/dist/config.js +45 -9
  40. package/dist/config.js.map +1 -1
  41. package/dist/connection.d.ts +168 -0
  42. package/dist/connection.js +327 -0
  43. package/dist/connection.js.map +1 -0
  44. package/dist/credentials.d.ts +7 -53
  45. package/dist/credentials.js +84 -247
  46. package/dist/credentials.js.map +1 -1
  47. package/dist/db.js +21 -2
  48. package/dist/db.js.map +1 -1
  49. package/dist/define.d.ts +9 -2
  50. package/dist/define.js.map +1 -1
  51. package/dist/definitions.d.ts +20 -0
  52. package/dist/definitions.js +16 -0
  53. package/dist/definitions.js.map +1 -0
  54. package/dist/deployment-key.d.ts +2 -0
  55. package/dist/deployment-key.js +22 -0
  56. package/dist/deployment-key.js.map +1 -0
  57. package/dist/describe.d.ts +9 -177
  58. package/dist/describe.js +71 -34
  59. package/dist/describe.js.map +1 -1
  60. package/dist/description.d.ts +532 -0
  61. package/dist/description.js +381 -0
  62. package/dist/description.js.map +1 -0
  63. package/dist/direct.js +3 -0
  64. package/dist/direct.js.map +1 -1
  65. package/dist/drift.d.ts +1 -1
  66. package/dist/drift.js +1 -1
  67. package/dist/drift.js.map +1 -1
  68. package/dist/env.d.ts +2 -0
  69. package/dist/env.js +2 -2
  70. package/dist/env.js.map +1 -1
  71. package/dist/index.d.ts +10 -2
  72. package/dist/index.js +10 -2
  73. package/dist/index.js.map +1 -1
  74. package/dist/ledger.d.ts +2 -0
  75. package/dist/ledger.js +1 -1
  76. package/dist/ledger.js.map +1 -1
  77. package/dist/lint.d.ts +0 -9
  78. package/dist/lint.js +1 -16
  79. package/dist/lint.js.map +1 -1
  80. package/dist/load.d.ts +74 -0
  81. package/dist/load.js +357 -0
  82. package/dist/load.js.map +1 -0
  83. package/dist/lockfile.d.ts +2 -0
  84. package/dist/lockfile.js +8 -3
  85. package/dist/lockfile.js.map +1 -1
  86. package/dist/op.d.ts +10 -1
  87. package/dist/op.js +1 -1
  88. package/dist/op.js.map +1 -1
  89. package/dist/outline.js +13 -4
  90. package/dist/outline.js.map +1 -1
  91. package/dist/paths.d.ts +18 -0
  92. package/dist/paths.js +21 -3
  93. package/dist/paths.js.map +1 -1
  94. package/dist/plugin.d.ts +0 -11
  95. package/dist/plugin.js +3 -18
  96. package/dist/plugin.js.map +1 -1
  97. package/dist/policy.d.ts +2 -0
  98. package/dist/policy.js.map +1 -1
  99. package/dist/resource.d.ts +10 -4
  100. package/dist/resource.js +11 -4
  101. package/dist/resource.js.map +1 -1
  102. package/dist/resources.d.ts +33 -76
  103. package/dist/resources.js +219 -227
  104. package/dist/resources.js.map +1 -1
  105. package/dist/run.d.ts +2 -0
  106. package/dist/run.js.map +1 -1
  107. package/dist/sandbox.d.ts +1 -1
  108. package/dist/sandbox.js +5 -2
  109. package/dist/sandbox.js.map +1 -1
  110. package/dist/scenarios.js +7 -1
  111. package/dist/scenarios.js.map +1 -1
  112. package/dist/schema.js +8 -0
  113. package/dist/schema.js.map +1 -1
  114. package/dist/secret-store-check.d.ts +11 -0
  115. package/dist/secret-store-check.js +76 -0
  116. package/dist/secret-store-check.js.map +1 -0
  117. package/dist/secret-store.d.ts +106 -0
  118. package/dist/secret-store.js +228 -0
  119. package/dist/secret-store.js.map +1 -0
  120. package/dist/shared.d.ts +17 -11
  121. package/dist/shared.js +34 -7
  122. package/dist/shared.js.map +1 -1
  123. package/dist/sites.d.ts +4 -93
  124. package/dist/sites.js +61 -84
  125. package/dist/sites.js.map +1 -1
  126. package/dist/version.d.ts +2 -1
  127. package/dist/version.js +2 -1
  128. package/dist/version.js.map +1 -1
  129. package/dist/wakeup-driver.js +15 -6
  130. package/dist/wakeup-driver.js.map +1 -1
  131. package/dist/worker.js +12 -8
  132. package/dist/worker.js.map +1 -1
  133. package/dist/writer.d.ts +53 -14
  134. package/dist/writer.js +113 -43
  135. package/dist/writer.js.map +1 -1
  136. package/oxlint.json +0 -5
  137. package/package.json +5 -1
  138. package/src/apply-steps.ts +5 -2
  139. package/src/apply.ts +54 -21
  140. package/src/approvals.ts +7 -1
  141. package/src/apps.ts +54 -85
  142. package/src/ast.ts +6 -6
  143. package/src/authentication.ts +74 -0
  144. package/src/call-workflow.ts +5 -4
  145. package/src/call.ts +53 -9
  146. package/src/cedar.ts +14 -1
  147. package/src/change-steps.ts +7 -4
  148. package/src/change.ts +10 -0
  149. package/src/client.ts +108 -41
  150. package/src/collection.ts +10 -2
  151. package/src/config.ts +62 -8
  152. package/src/connection.ts +446 -0
  153. package/src/credentials.ts +94 -281
  154. package/src/db.ts +21 -2
  155. package/src/define.ts +7 -2
  156. package/src/definitions.ts +31 -0
  157. package/src/deployment-key.ts +23 -0
  158. package/src/describe.ts +102 -230
  159. package/src/description.ts +426 -0
  160. package/src/direct.ts +3 -0
  161. package/src/drift.ts +10 -2
  162. package/src/env.ts +4 -2
  163. package/src/index.ts +42 -1
  164. package/src/ledger.ts +3 -1
  165. package/src/lint.ts +1 -16
  166. package/src/load.ts +437 -0
  167. package/src/lockfile.ts +10 -3
  168. package/src/op.ts +10 -1
  169. package/src/outline.ts +18 -5
  170. package/src/paths.ts +24 -2
  171. package/src/plugin.ts +3 -24
  172. package/src/policy.ts +2 -0
  173. package/src/resource.ts +24 -6
  174. package/src/resources.ts +246 -301
  175. package/src/run.ts +2 -0
  176. package/src/sandbox.ts +6 -2
  177. package/src/scenarios.ts +7 -1
  178. package/src/schema.ts +10 -0
  179. package/src/secret-store-check.ts +93 -0
  180. package/src/secret-store.ts +314 -0
  181. package/src/shared.ts +35 -10
  182. package/src/sites.ts +70 -180
  183. package/src/version.ts +2 -1
  184. package/src/wakeup-driver.ts +22 -6
  185. package/src/worker.ts +12 -8
  186. package/src/writer.ts +145 -56
  187. package/dist/datafile.d.ts +0 -97
  188. package/dist/datafile.js +0 -479
  189. package/dist/datafile.js.map +0 -1
  190. package/src/datafile.ts +0 -601
package/src/sites.ts CHANGED
@@ -1,11 +1,13 @@
1
1
  import { existsSync, readdirSync } from "node:fs";
2
- import { basename, join, relative, sep } from "node:path";
3
- import { childrenOf, type LintProblem, type Node, readSource, type SourceFile } from "./ast.ts";
2
+ import { basename, join } from "node:path";
3
+ import { z } from "zod";
4
+ import { childrenOf, keyName, type LintProblem, type Node, readSource, type SourceFile } from "./ast.ts";
4
5
  import { type CedarPolicySource, visitorPermits } from "./cedar.ts";
5
- import { keyName } from "./datafile.ts";
6
- import type { ConfigDescription } from "./describe.ts";
6
+ import type { ConfigDescription, SiteCall, SiteEntry, SitePage } from "./description.ts";
7
7
  import { stringValue, unwrap } from "./outline.ts";
8
- import { APPS_DIR, appFolders, filesUnder, isDirectory } from "./paths.ts";
8
+ import { exportsOf, loadModules } from "./load.ts";
9
+ import { APPS_DIR, appFolders, filesUnder, isDirectory, relPath, SITE_ENTRIES } from "./paths.ts";
10
+ import type { ResourceProblem } from "./resources.ts";
9
11
  import { START } from "./schema.ts";
10
12
  import {
11
13
  ANONYMOUS,
@@ -19,100 +21,14 @@ import {
19
21
  WORKFLOW_ACTION,
20
22
  } from "./shared.ts";
21
23
 
22
- // What a company's sites call, read from their source with oxc and never run: the reverse index
23
- // a workflow's page shows as Used by, and the names `sanoma check` holds to the config.
24
+ // What a company's sites call, read from their source with oxc and never run (their pages are
25
+ // browser code, the site's outline): the reverse index a workflow's page shows as Used by, and
26
+ // the names `sanoma check` holds to the config. A site's `site.ts` is loaded.
24
27
 
25
- /** A call a site makes, as written in its source. */
26
- export interface SiteCall {
27
- /** What it calls: a workflow, a collection (`api.data.<name>.<op>`) or an operation (`api.<vendor>.<resource>.<op>`). */
28
- kind: "workflow" | "collection" | "op";
29
- /**
30
- * The workflow's name (`RunForm`'s `workflow`, or the workflow whose `apiNameOf` is the name
31
- * `api` gives; that name itself when none is), the collection's name, or the operation's id.
32
- */
33
- target: string;
34
- /**
35
- * What it calls of the workflow (`start`, or a read of its runs: `result`, `latest`, …) or the
36
- * collection (`list`, `save`, …), as written; absent for a workflow called as itself or by
37
- * `RunForm`, which starts it, and for an operation.
38
- */
39
- method?: string;
40
- /** `<RunForm workflow="…" />` or a call on `api`, from `sanoma/site`. */
41
- via: "RunForm" | "api";
42
- /** The file, relative to the config's root, with `/`: `sites/sales/routes/index.tsx`. */
43
- file: string;
44
- /** From 1. */
45
- line: number;
46
- /** From 1, in UTF-16 code units. */
47
- column: number;
48
- /**
49
- * The path of the page the file is, from its `createFileRoute` (or `createLazyFileRoute`) id: groups and pathless
50
- * segments left out, no trailing slash (`/sites/sales`). Absent for a file that is no page,
51
- * such as a component's.
52
- */
53
- page?: string;
54
- }
55
-
56
- /** A page of a site: a route file's path, and for a page per content entry the kind it reads. */
57
- export interface SitePage {
58
- /** As `SiteCall.page` has it: `/sites/www`, `/sites/www/blog/$slug`. */
59
- path: string;
60
- /**
61
- * For a path with one parameter (`$slug`), the content kind whose entries are its pages: the one
62
- * kind its file reads by that parameter, `content.<kind>.get(params.slug)` (or `slug`,
63
- * destructured) with `content` from `sanoma/site`.
64
- */
65
- content?: string;
66
- /**
67
- * `false` when its route says `staticData: { sitemap: false }`, written out: the sitemap leaves it
68
- * out. Its index route's `sitemap` (`true` or `false`) decides over a layout's at its path; a
69
- * pathless or group layout's says nothing.
70
- */
71
- sitemap?: false;
72
- }
73
-
74
- /** A site, `sites/<name>/` or an app's `apps/<app>/sites/<name>/`, and what it calls. */
75
- export interface SiteEntry {
76
- /** Its folder's name, `<app>/<name>` for an app's (`siteKey`): where it is mounted, `/sites/<name>`. */
77
- name: string;
78
- /** The app whose folder holds it (`apps/<app>/sites/`); absent for one at the root. */
79
- app?: string;
80
- /** `defineSite({ title })` in its `site.ts`, when that is a string literal. */
81
- title?: string;
82
- /** `defineSite({ icon })` in its `site.ts`: a Lucide icon's name, as written. */
83
- icon?: string;
84
- /** `defineSite({ public: true })`: served to visitors, not people. */
85
- public?: true;
86
- /** `defineSite({ login })`, the connector whose login half signs visitors in, as written: one that names none is among `problems`. */
87
- login?: string;
88
- /**
89
- * `defineSite({ locales })`, a public site's languages, the first its default: kept only when
90
- * every one is a `LOCALE`, written once, on a public site with no login; else a problem.
91
- */
92
- locales?: string[];
93
- /** The host a public site answers on: set by `startApp` from its `domains`, never by `readSites`. */
94
- domain?: string;
95
- /** Its pages, by path: one for each path its `routes/` files give (a layout and its index are one). */
96
- pages: SitePage[];
97
- /** Its calls, by file and then in the file's order. */
98
- calls: SiteCall[];
99
- /**
100
- * Each call to a workflow, a collection or an operation the config does not have, or to an
101
- * operation a page may not call directly, or, on a public site, that no policy lets a visitor
102
- * make, at its file; then what is wrong in its `site.ts`.
103
- */
104
- problems: (LintProblem & { file: string })[];
105
- /**
106
- * On a public site, each page with one parameter whose file reads content the sitemap cannot
107
- * tie to it (`SitePage.content`), so the sitemap lists none of its pages: at the first read.
108
- */
109
- warnings: (LintProblem & { file: string })[];
110
- }
28
+ export type { SiteCall, SiteEntry, SitePage };
111
29
 
112
30
  /** Where a site's code is: its pages and its components. */
113
31
  const SITE_DIRS = ["routes", "components"];
114
- /** Where `api`, `RunForm` and `defineSite` come from: `sanoma/site`, which re-exports the app's. */
115
- const SITE_SOURCES: ReadonlySet<string> = new Set(["sanoma/site", "@sanoma-ai/app/site"]);
116
32
  const KIND_NAMES = { workflow: "workflow", collection: "collection", op: "operation" } as const;
117
33
  /** What a route's loader and head name its path's parameters by. */
118
34
  const PARAMS: ReadonlySet<string> = new Set(["params"]);
@@ -212,15 +128,14 @@ export function readSites(
212
128
  const read = readSiteFiles(root, dir);
213
129
  const resolved = read.flatMap(({ uses }) => uses.filter((use) => !isAppCollection(use)).map(resolve));
214
130
  const pages = pagesOf(read);
215
- const siteFile = `${dir}/site.ts`;
216
- const { problems: optionProblems, ...options } = siteOptionsOf(join(root, siteFile), connectors);
131
+ const { problems: optionProblems, ...options } = siteDefinitionOf(root, dir, connectors);
217
132
  const calls = resolved.map((r) => r.call);
218
133
  const problems = [
219
134
  ...resolved.flatMap(({ call, problem }) => {
220
135
  const message = problem ?? (options.public ? refused(call, options.login) : undefined);
221
136
  return message === undefined ? [] : [{ file: call.file, line: call.line, column: call.column, message }];
222
137
  }),
223
- ...optionProblems.map((p) => ({ file: siteFile, ...p })),
138
+ ...optionProblems,
224
139
  ];
225
140
  const sorted = [...pages.keys()].toSorted().map((path) => pages.get(path)!);
226
141
  // A page's file it warns of, unless another of its files (a lazy one, its layout) gives its kind.
@@ -261,7 +176,7 @@ const readSiteFiles = (root: string, dir: string) =>
261
176
  .toSorted()
262
177
  .flatMap((path) => {
263
178
  const file = readSource(path);
264
- return typeof file === "string" ? [] : [readSiteFile(file, relative(root, path).split(sep).join("/"))];
179
+ return typeof file === "string" ? [] : [readSiteFile(file, relPath(root, path))];
265
180
  });
266
181
 
267
182
  /**
@@ -282,13 +197,13 @@ export function siteContentPages(root: string): Record<string, string> {
282
197
  /**
283
198
  * Each site's languages (`defineSite({ locales })`, the first its default) under `root`, by name,
284
199
  * for the sites whose `site.ts` gives them without a problem there: what the app's build gives
285
- * its router (`virtual:sanoma/locales`). Read as `readSites` reads them, never run.
200
+ * its router (`virtual:sanoma/locales`). Loaded as `readSites` loads them.
286
201
  */
287
202
  export function siteLocales(root: string): Record<string, string[]> {
288
203
  const none = new Set<string>();
289
204
  return Object.fromEntries(
290
205
  sitesUnder(root).flatMap(({ name, dir }) => {
291
- const { locales } = siteOptionsOf(join(root, dir, "site.ts"), { vendors: none, logins: none });
206
+ const { locales } = siteDefinitionOf(root, dir, { vendors: none, logins: none });
292
207
  return locales ? [[name, locales]] : [];
293
208
  }),
294
209
  );
@@ -297,13 +212,13 @@ export function siteLocales(root: string): Record<string, string[]> {
297
212
  /**
298
213
  * Each site's icon (`defineSite({ icon })`, a Lucide icon's name as written) under `root`, by name,
299
214
  * for the sites whose `site.ts` gives one: the icons the app's build bundles (`virtual:sanoma/icons`).
300
- * Read as `readSites` reads it, never run.
215
+ * Loaded as `readSites` loads it.
301
216
  */
302
217
  export function siteIcons(root: string): Record<string, string> {
303
218
  const none = new Set<string>();
304
219
  return Object.fromEntries(
305
220
  sitesUnder(root).flatMap(({ name, dir }) => {
306
- const { icon } = siteOptionsOf(join(root, dir, "site.ts"), { vendors: none, logins: none });
221
+ const { icon } = siteDefinitionOf(root, dir, { vendors: none, logins: none });
307
222
  return icon === undefined ? [] : [[name, icon]];
308
223
  }),
309
224
  );
@@ -347,9 +262,9 @@ function readSiteFile(
347
262
  { program, at }: SourceFile,
348
263
  file: string,
349
264
  ): { uses: Written[]; route?: FileRoute; warning?: LintProblem & { file: string } } {
350
- const api = importedAs(program, SITE_SOURCES, "api");
351
- const runForm = importedAs(program, SITE_SOURCES, "RunForm");
352
- const content = importedAs(program, SITE_SOURCES, "content");
265
+ const api = importedAs(program, SITE_ENTRIES, "api");
266
+ const runForm = importedAs(program, SITE_ENTRIES, "RunForm");
267
+ const content = importedAs(program, SITE_ENTRIES, "content");
353
268
  if (!api.size && !runForm.size && !file.includes("/routes/")) return { uses: [] };
354
269
  const found: Omit<Written, "page">[] = [];
355
270
  /** Each `content.<kind>.get(arg)`: its kind, its argument and where it is. */
@@ -504,99 +419,74 @@ type SiteConnectors = { vendors: ReadonlySet<string>; logins: ReadonlySet<string
504
419
  /** A property of an object literal that is written out: not a spread, its name not computed. */
505
420
  const isWritten = (p: Node) => p.type === "Property" && !p.computed;
506
421
 
507
- /** What `site.ts` that `sanoma` cannot read without running it is told. */
508
- const unread = (what: string, as: string) => `${what} is read without running site.ts: write it as ${as}`;
422
+ /** What `site.ts`'s default export, `defineSite({ … })`, may say. */
423
+ const SiteDefinitionSchema = z.strictObject({
424
+ title: z.string().optional(),
425
+ icon: z.string().optional(),
426
+ public: z.boolean().optional(),
427
+ login: z.string().optional(),
428
+ locales: z.array(z.string()).optional(),
429
+ });
509
430
 
510
431
  /**
511
- * What `site.ts` says of its site, read without running it, and what is wrong there: its default
512
- * export must be `defineSite({ … })` written out, so it can be read at all; then `title` when it is
513
- * a string, `icon`, `public`, `login` and `locales`, which must be literals. `login` is kept as written; one on a
514
- * site that is not public, or naming no connector of the config with a login half, is a problem.
515
- * `locales` is kept only when it has no problem.
432
+ * What the site in `dir` (relative to `root`) says of itself in its `site.ts`, loaded
433
+ * (`loadModules`, `defineSite` served from `sanoma/site`), and what is wrong there: its default
434
+ * export, `defineSite({ … })`, held to its schema; then a `login` on a site that is not public, or
435
+ * naming no connector of the config with a login half, and `locales` that are not a public site's
436
+ * (with no login) language tags, each once. `locales` is kept only when it has no problem. Nothing
437
+ * for no file, or no default export.
516
438
  */
517
- function siteOptionsOf(
518
- path: string,
439
+ function siteDefinitionOf(
440
+ root: string,
441
+ dir: string,
519
442
  connectors: SiteConnectors,
520
- ): Pick<SiteEntry, "title" | "icon" | "public" | "login" | "locales"> & { problems: LintProblem[] } {
521
- const file = readSource(path);
522
- if (typeof file === "string") return { problems: [] };
523
- const problems: LintProblem[] = [...file.problems];
524
- const problem = (at: Node, message: string) => void problems.push({ ...file.at(at.start), message });
525
- const exported = file.program.body.find((s: Node) => s.type === "ExportDefaultDeclaration");
526
- if (!exported) return { problems };
527
- const call = unwrap(exported.declaration);
528
- const callee = unwrap(call?.callee);
529
- const options = unwrap(call?.arguments?.[0]);
530
- const defineSite = importedAs(file.program, SITE_SOURCES, "defineSite");
531
- // Anything not written out (a variable, a spread, a computed key) could make the site public unseen.
532
- const written =
533
- call?.type === "CallExpression" &&
534
- callee?.type === "Identifier" &&
535
- defineSite.has(callee.name) &&
536
- (options === undefined || options.type === "ObjectExpression");
537
- if (!written) {
538
- problem(
539
- exported,
540
- unread("its default export", '`defineSite({ … })` from "sanoma/site", its options an object literal'),
541
- );
542
- return { problems };
543
- }
544
- for (const p of options?.properties ?? []) {
545
- if (!isWritten(p)) problem(p, unread("each of `defineSite`'s options", "a property with its name"));
443
+ ): Pick<SiteEntry, "at" | "title" | "icon" | "public" | "login" | "locales"> & {
444
+ problems: (ResourceProblem & { file: string })[];
445
+ } {
446
+ const file = `${dir}/site.ts`;
447
+ if (!existsSync(join(root, file))) return { problems: [] };
448
+ // A helper it imports threw: said at the helper.
449
+ const loaded = exportsOf(loadModules(root, [join(root, file)], []), file);
450
+ if (loaded === undefined) return { problems: [] };
451
+ if ("problem" in loaded) return { problems: [loaded.problem] };
452
+ const value = loaded.exports.find(([name]) => name === "default")?.[1];
453
+ if (value === undefined) return { problems: [] };
454
+ const problem = (message: string) => ({ file, export: "default", message });
455
+ const parsed = SiteDefinitionSchema.safeParse(value);
456
+ if (!parsed.success) {
457
+ const why = z.prettifyError(parsed.error).replaceAll(/\s*\n\s*/g, " ");
458
+ return { problems: [problem(`its default export is \`defineSite({ … })\` from "sanoma/site": ${why}`)] };
546
459
  }
547
- // The last of a name, as the object it makes has.
548
- const property = (name: string): Node =>
549
- options?.properties.findLast((p: Node) => isWritten(p) && keyName(p.key) === name);
550
-
551
- const title = stringValue(property("title")?.value);
552
- // Cosmetic: one that is not a string (tsc refuses it) is shown as none, never a problem that stops a site.
553
- const icon = stringValue(property("icon")?.value);
554
- const publicAt = property("public");
555
- const flag = unwrap(publicAt?.value);
556
- const isPublic = flag?.type === "Literal" && flag.value === true;
557
- const publicUnread = publicAt !== undefined && !(flag?.type === "Literal" && typeof flag.value === "boolean");
558
- if (publicUnread) problem(publicAt, unread("`public`", "`true` or `false`"));
559
-
560
- const loginAt = property("login");
561
- const login = stringValue(loginAt?.value);
460
+ const { title, icon, public: isPublic, login, locales } = parsed.data;
461
+ const problems: (ResourceProblem & { file: string })[] = [];
562
462
  const loginProblem = (): string | undefined => {
563
- if (login === undefined) return unread("`login`", "a string");
564
- // When `public` is what is wrong, it is said there.
565
- if (!isPublic && !publicUnread) return "a login is for a public site: add `public: true`, or remove `login`";
566
- if (!connectors.vendors.has(login)) return `login names "${login}", which is not a connector in sanoma.config.ts`;
567
- if (!connectors.logins.has(login)) return `login names "${login}", whose connector has no login half`;
463
+ if (!isPublic) return "a login is for a public site: add `public: true`, or remove `login`";
464
+ if (!connectors.vendors.has(login!)) return `login names "${login}", which is not a connector in sanoma.config.ts`;
465
+ if (!connectors.logins.has(login!)) return `login names "${login}", whose connector has no login half`;
568
466
  return undefined;
569
467
  };
570
- const message = loginAt && loginProblem();
571
- if (message) problem(loginAt, message);
572
-
573
- const localesAt = property("locales");
574
- const list = unwrap(localesAt?.value);
575
- // A hole or a spread is no string.
576
- const tags: (string | undefined)[] | undefined =
577
- list?.type === "ArrayExpression" ? list.elements.map((e: Node) => stringValue(e)) : undefined;
578
- const locales = tags?.every((tag): tag is string => tag !== undefined) ? tags : undefined;
579
- const localesProblem = (): string | undefined => {
580
- if (locales === undefined) return unread("`locales`", "an array of strings");
581
- if (!locales.length) return "`locales` names a site's languages, its default first: give one or more, or remove it";
582
- const bad = locales.find((locale) => !LOCALE.test(locale));
468
+ const loginMessage = login !== undefined ? loginProblem() : undefined;
469
+ if (loginMessage) problems.push(problem(loginMessage));
470
+ const localesProblem = (list: string[]): string | undefined => {
471
+ if (!list.length) return "`locales` names a site's languages, its default first: give one or more, or remove it";
472
+ const bad = list.find((locale) => !LOCALE.test(locale));
583
473
  if (bad !== undefined) return `"${bad}" is not a language tag such as "en", "fr" or "pt-BR"`;
584
- const twice = locales.find((locale, i) => locales.indexOf(locale) !== i);
474
+ const twice = list.find((locale, i) => list.indexOf(locale) !== i);
585
475
  if (twice !== undefined) return `"${twice}" is in \`locales\` twice`;
586
- // When `public` is what is wrong, it is said there.
587
- if ((!isPublic && !publicUnread) || loginAt) {
476
+ if (!isPublic || login !== undefined) {
588
477
  return "`locales` are for a public site with no login: add `public: true`, or remove `locales`";
589
478
  }
590
479
  return undefined;
591
480
  };
592
- const localesMessage = localesAt && localesProblem();
593
- if (localesMessage) problem(localesAt, localesMessage);
481
+ const localesMessage = locales && localesProblem(locales);
482
+ if (localesMessage) problems.push(problem(localesMessage));
594
483
  return {
484
+ at: { file, export: "default" },
595
485
  ...(title !== undefined && { title }),
596
486
  ...(icon !== undefined && { icon }),
597
487
  ...(isPublic && { public: true as const }),
598
488
  ...(login !== undefined && { login }),
599
- ...(localesAt && !localesMessage && isPublic && !loginAt && { locales }),
489
+ ...(locales && !localesMessage && { locales }),
600
490
  problems,
601
491
  };
602
492
  }
package/src/version.ts CHANGED
@@ -18,8 +18,9 @@ export const RUNTIME_VERSION = (
18
18
  * 3: a start the policy holds waits on its approval before the body (2026-10-21).
19
19
  * 4: a sleep for a duration reads DBOS's clock for its start (2026-10-25).
20
20
  * 5: a scheduled run admits its starter and asks the policy in two steps before the body (2026-10-25).
21
+ * 6: pin a named connection before policy and recheck its binding before use (2026-10-29).
21
22
  */
22
- export const STEP_LAYOUT = 5;
23
+ export const STEP_LAYOUT = 6;
23
24
 
24
25
  let dbosVersion: string | undefined;
25
26
 
@@ -154,8 +154,11 @@ const env = z.object({
154
154
  });
155
155
 
156
156
  /** The variable `name`, read when an operation is called, or the driver's refusal. */
157
- function required(name: "SANOMA_WAKEUP_URL" | "SANOMA_WAKEUP_ACCOUNT_TOKEN"): string {
158
- const value = process.env[name];
157
+ function required(
158
+ values: Record<string, string | undefined>,
159
+ name: "SANOMA_WAKEUP_URL" | "SANOMA_WAKEUP_ACCOUNT_TOKEN",
160
+ ): string {
161
+ const value = values[name];
159
162
  if (!value) throw refused(`${name} is not set (${env.shape[name].description})`);
160
163
  return value;
161
164
  }
@@ -167,10 +170,10 @@ const Problem = z.object({ error: z.string() });
167
170
  const instance = (id: string) => `/wakeup/instances/${encodeURIComponent(id)}`;
168
171
 
169
172
  /** The service at `SANOMA_WAKEUP_URL`, read when an operation is called: each request's failure a `DriverError`. */
170
- function serviceApi(timeoutMs: number): WakeupApi {
171
- const base = required("SANOMA_WAKEUP_URL").replace(/\/+$/, "");
173
+ function serviceApi(timeoutMs: number, values: Record<string, string | undefined>): WakeupApi {
174
+ const base = required(values, "SANOMA_WAKEUP_URL").replace(/\/+$/, "");
172
175
  if (!URL.canParse(base)) throw refused("SANOMA_WAKEUP_URL is not a URL");
173
- const account = () => required("SANOMA_WAKEUP_ACCOUNT_TOKEN");
176
+ const account = () => required(values, "SANOMA_WAKEUP_ACCOUNT_TOKEN");
174
177
  /**
175
178
  * One request: its reply parsed by `reply` (none: not read), a 404 `undefined` where `absent`
176
179
  * says it means no such instance, and any other answer but a 2xx a `DriverError`.
@@ -257,7 +260,20 @@ function serviceApi(timeoutMs: number): WakeupApi {
257
260
  * `register` send). `timeoutMs` is how long one request may take: 10 s by default.
258
261
  */
259
262
  export function sanomaWakeupDriver({ timeoutMs = 10_000 }: { timeoutMs?: number } = {}) {
260
- return defineDriver(wakeupOps, { wakeup: wakeupCalls(() => serviceApi(timeoutMs)) }, { env });
263
+ const call = async (name: keyof ReturnType<typeof wakeupCalls>, input: unknown, context: CallContext) => {
264
+ const values = Object.fromEntries(
265
+ await Promise.all(Object.keys(env.shape).map(async (key) => [key, await context.secrets.get(key)])),
266
+ );
267
+ const methods = wakeupCalls(() => serviceApi(timeoutMs, values));
268
+ return (methods[name] as (input: unknown, context: CallContext) => Promise<unknown>)(input, context);
269
+ };
270
+ const methods = Object.fromEntries(
271
+ ["import", "read", "plan", "create", "update", "delete", "register"].map((name) => [
272
+ name,
273
+ (input: unknown, context: CallContext) => call(name as keyof ReturnType<typeof wakeupCalls>, input, context),
274
+ ]),
275
+ );
276
+ return defineDriver(wakeupOps, { wakeup: methods } as never, { env });
261
277
  }
262
278
 
263
279
  /** The wakeup's step for Gherkin tests: an instance at the service before the run, named `wakeup` (`$wakeup.id`). */
package/src/worker.ts CHANGED
@@ -3,7 +3,7 @@ import { awaitApproval } from "./approvals.ts";
3
3
  import { buildCtx, decideStart, isInfrastructureError, startHoldOf } from "./call.ts";
4
4
  import { dbosStatusesOf } from "./client.ts";
5
5
  import { declaredWakeup, type ResolvedConfig, resolveConfig, type SanomaConfig } from "./config.ts";
6
- import { storedSecrets, watchCredentials } from "./credentials.ts";
6
+ import { configuredSecrets } from "./credentials.ts";
7
7
  import { type Db, DBOS_DATABASE_OPTIONS, lazyPool, prepareDatabase, stampVersion } from "./db.ts";
8
8
  import { deadlineRegistry, type Deadlines, idle, onOwnQueue, type SelfStop, stopWhenIdle } from "./deadlines.ts";
9
9
  import type { WorkflowDefinition } from "./define.ts";
@@ -123,6 +123,7 @@ export async function startWorker(config: SanomaConfig, options: WorkerOptions =
123
123
  const horizonMs = options.idleHorizonMs ?? 60_000;
124
124
  const state: WorkerState = {
125
125
  app: resolved.appName,
126
+ connections: resolved.connections,
126
127
  ops: resolved.ops,
127
128
  drivers: resolved.drivers,
128
129
  fakes: resolved.fakes,
@@ -134,7 +135,7 @@ export async function startWorker(config: SanomaConfig, options: WorkerOptions =
134
135
  policy: resolved.policy,
135
136
  ...(resolved.root !== undefined && { root: resolved.root }),
136
137
  ledger: ledger.store,
137
- secrets: storedSecrets(() => secrets.db(), "sanoma"),
138
+ secrets: configuredSecrets(resolved.secrets, () => secrets.db()),
138
139
  workflows: resolved.workflows,
139
140
  outlines: new Map([...resolved.workflows].map(([name, wf]) => [name, outlineOf(wf)])),
140
141
  // `registered` is filled below, before any run starts: every name of this config's is registered then.
@@ -146,7 +147,6 @@ export async function startWorker(config: SanomaConfig, options: WorkerOptions =
146
147
  stopped: false,
147
148
  deadlines: deadlineRegistry(),
148
149
  };
149
- const credentials = await watchCredentials(resolved.databaseUrl, resolved.driversByVendor);
150
150
  const worker: Worker = {
151
151
  unconfigured: [...credentialsOf(resolved.driversByVendor, (name) => process.env[name])]
152
152
  .filter(([vendor, statuses]) => unconfigured(vendor, statuses))
@@ -164,8 +164,8 @@ export async function startWorker(config: SanomaConfig, options: WorkerOptions =
164
164
  let failed: { error: unknown } | undefined;
165
165
  for (const close of [
166
166
  () => DBOS.shutdown(),
167
- () => credentials.close(),
168
167
  () => secrets.close(),
168
+ () => resolved.connectionPool.close(),
169
169
  () => ledger.close(),
170
170
  async () => resolved.dataStore?.close(),
171
171
  ]) {
@@ -184,6 +184,7 @@ export async function startWorker(config: SanomaConfig, options: WorkerOptions =
184
184
  const previous = current;
185
185
  let launched = false;
186
186
  try {
187
+ await configuredSecrets(resolved.secrets, () => secrets.db()).validate();
187
188
  for (const name of resolved.workflows.keys()) {
188
189
  if (!registered.has(name)) registered.set(name, register(name));
189
190
  }
@@ -252,9 +253,12 @@ export async function startWorker(config: SanomaConfig, options: WorkerOptions =
252
253
  let stopFailed: { error: unknown } | undefined;
253
254
  if (launched) await worker.stop().catch((error: unknown) => (stopFailed = { error }));
254
255
  else {
255
- await credentials
256
- .close()
257
- .catch((e: unknown) => warn(`closing the credentials listener failed: ${errorMessage(e)}`));
256
+ await Promise.all([
257
+ secrets.close(),
258
+ ledger.close(),
259
+ resolved.dataStore?.close(),
260
+ resolved.connectionPool.close(),
261
+ ]);
258
262
  }
259
263
  current = previous;
260
264
  if (stopFailed) {
@@ -438,7 +442,7 @@ async function runOn(state: WorkerState, name: string, start: RunArgs | Date): P
438
442
  if (sandbox !== undefined) {
439
443
  // Imported here, so a live worker never loads the Gherkin parser or faker.
440
444
  const { seedSandbox } = await import("./sandbox.ts");
441
- await seedSandbox(run, sandbox);
445
+ await seedSandbox(run, sandbox, parsed);
442
446
  }
443
447
  output = await currentRun.run(run, () => wf.run(buildCtx(wf, run), parsed));
444
448
  } finally {