@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/config.ts CHANGED
@@ -1,3 +1,16 @@
1
+ import { configuredSecrets } from "./credentials.ts";
2
+ import { secretsForInput } from "./secret.ts";
3
+ import { isRunControlError } from "./errors.ts";
4
+ import { currentRun } from "./run.ts";
5
+ import {
6
+ connectionCatalog,
7
+ ConnectionRegistry,
8
+ connectionDriver,
9
+ connectionOps,
10
+ type ConnectionDefinition,
11
+ type ConnectionRule,
12
+ } from "./connection.ts";
13
+ import { checkSecretConfiguration, type SecretConfiguration } from "./secret-store.ts";
1
14
  import { existsSync, readFileSync } from "node:fs";
2
15
  import { dirname, join, resolve } from "node:path";
3
16
  import { pathToFileURL } from "node:url";
@@ -81,6 +94,10 @@ export interface SanomaConfig {
81
94
  * only, for tests.
82
95
  */
83
96
  ledger?: LedgerStore;
97
+ /** Credential storage and explicit bindings. Omitted: encrypted Postgres, no environment fallback. */
98
+ secrets?: SecretConfiguration;
99
+ connections?: readonly ConnectionDefinition[];
100
+ connectionRules?: readonly ConnectionRule[];
84
101
  /** Postgres for the runtime. Defaults to `SANOMA_DATABASE_URL`, then `DEFAULT_DATABASE_URL`. */
85
102
  databaseUrl?: string;
86
103
  /** The file that defined the config, absolute, when known. Set by `defineConfig` from its call site. */
@@ -118,6 +135,9 @@ export interface SanomaConfig {
118
135
 
119
136
  /** A config, checked, with everything a worker, client or app derives from it. */
120
137
  export interface ResolvedConfig {
138
+ secrets: SecretConfiguration;
139
+ connections: ConnectionRegistry;
140
+ connectionPool: LazyPool;
121
141
  /**
122
142
  * Scopes workflows, queues and versions in the system database: the `name` of
123
143
  * `<root>/package.json` when it names one, else "sanoma".
@@ -252,6 +272,7 @@ export function rootOf(config: Pick<SanomaConfig, "root" | "file">): string | un
252
272
  * `startApp`, the `sanoma` bin when it loads the config.
253
273
  */
254
274
  export function resolveConfig(config: SanomaConfig): ResolvedConfig {
275
+ checkSecretConfiguration(config.secrets ?? {});
255
276
  if (!Array.isArray(config.connectors)) {
256
277
  throw new Error("The config needs `connectors`: the connectors its workflows call, as their packages export them");
257
278
  }
@@ -280,6 +301,12 @@ export function resolveConfig(config: SanomaConfig): ResolvedConfig {
280
301
  const hostTyped = [...types.values()].some((type) => type.host);
281
302
  // Built-in: the collections' operations, `data.<name>.<op>`, on the config's database.
282
303
  const databaseUrl = resolveDatabaseUrl(config);
304
+ const connectionPool = lazyPool(databaseUrl, "connections");
305
+ const connections = new ConnectionRegistry(
306
+ connectionCatalog(config.connectors, config.connections, config.connectionRules),
307
+ () => connectionPool.db(),
308
+ configuredSecrets(config.secrets ?? {}, () => connectionPool.db()),
309
+ );
283
310
  const declared = [...collections.values()];
284
311
  const dataStore = declared.length ? lazyPool(databaseUrl, "data") : undefined;
285
312
  // Built-in, in every config: a change's operations, which the template's policy names
@@ -287,6 +314,7 @@ export function resolveConfig(config: SanomaConfig): ResolvedConfig {
287
314
  const ops = indexConnectors([
288
315
  ...running,
289
316
  changeOps,
317
+ connectionOps,
290
318
  appsListOps,
291
319
  ...(hostTyped ? [sourceOps] : []),
292
320
  ...(dataStore ? [collectionsConnector(declared)] : []),
@@ -299,11 +327,13 @@ export function resolveConfig(config: SanomaConfig): ResolvedConfig {
299
327
  [
300
328
  ...driversByVendor.values(),
301
329
  changeDriver(remotes),
330
+ connectionDriver(connections),
302
331
  appsListDriver,
303
332
  ...(hostTyped ? [sourceDriver] : []),
304
333
  ...(dataStore ? [collectionsDriver(declared, dataStore)] : []),
305
334
  ],
306
335
  ops,
336
+ { connections },
307
337
  );
308
338
  const fakes = makersOf(running, "fake");
309
339
  if (dataStore) fakes.set(DATA_VENDOR, () => collectionsFake(declared));
@@ -326,7 +356,7 @@ export function resolveConfig(config: SanomaConfig): ResolvedConfig {
326
356
  const connectors = connectorsOf(config);
327
357
  // The types the data files may declare: the vendors' and the people's, whose resources are left out.
328
358
  const declarable = resourceTypesOf(connectors);
329
- const read = () => readDeclared(root, connectors, declarable, types);
359
+ const read = () => readDeclared(root, connectors, declarable, types, connections);
330
360
  if (types.size) {
331
361
  names.set(
332
362
  DRIFT_WORKFLOW,
@@ -343,6 +373,9 @@ export function resolveConfig(config: SanomaConfig): ResolvedConfig {
343
373
  if (call.uses.length) names.set(CALL_WORKFLOW, call);
344
374
  const { policy, policies, problems: policyProblems } = policyAt(root, { ops, workflows: names, collections });
345
375
  return {
376
+ secrets: config.secrets ?? {},
377
+ connections,
378
+ connectionPool,
346
379
  appName,
347
380
  databaseUrl,
348
381
  // The built-in's code and operations are part of the app's version, like the config's own.
@@ -495,9 +528,9 @@ export async function declaredWakeup(config: SanomaConfig): Promise<DeclaredWake
495
528
  const connectors = connectorsOf(config);
496
529
  const types = resourceTypesOf(connectors);
497
530
  if (![...types.values()].some((type) => type.wakeup)) return undefined;
498
- // Loaded when it is needed, not with the worker: the reader parses with oxc-parser.
499
- const { readDataFiles } = await import("./resources.ts");
500
- const { resources } = readDataFiles(rootOf(config), connectors, types);
531
+ // Loaded when it is needed, not with the worker.
532
+ const { loadDataFiles } = await import("./resources.ts");
533
+ const { resources } = loadDataFiles(rootOf(config), connectors, types);
501
534
  const typeOf = (r: DeclaredResource) => types.get(`${r.vendor}.${r.type}`)!;
502
535
  const declared = resources.find((r) => typeOf(r).wakeup);
503
536
  if (!declared) return undefined;
@@ -523,15 +556,38 @@ async function readDeclared(
523
556
  connectors: readonly Connector<any, any>[],
524
557
  declarable: ReadonlyMap<string, Resource>,
525
558
  types: ReadonlyMap<string, Resource>,
559
+ connections: ConnectionRegistry,
526
560
  ): Promise<Omit<ApplyDeclared, "rebinds">> {
527
- // Loaded when a run reads the files, not with the worker: the reader parses with oxc-parser.
528
- const { readDataFiles } = await import("./resources.ts");
529
- const { resources, bindings, orphans, problems } = readDataFiles(root, connectors, declarable);
561
+ // Loaded when a run reads the files, not with the worker.
562
+ const { loadDataFiles } = await import("./resources.ts");
563
+ const { resources, bindings, orphans, problems } = loadDataFiles(root, connectors, declarable);
564
+ // Read inside the data-file durable step: every operation of the plan uses one pinned account.
565
+ const run = currentRun.getStore();
566
+ if (run && !run.sandbox) {
567
+ for (const resource of resources.filter((r) => types.has(`${r.vendor}.${r.type}`))) {
568
+ try {
569
+ const selected = await connections.select(resource.vendor, resource.connection, run.actor.id);
570
+ if (selected) {
571
+ const pinned = { vendor: selected.vendor, $connection: selected.id, revision: selected.revision };
572
+ const locked = bindings[resource.id]?.connection;
573
+ if (locked && (locked.$connection !== pinned.$connection || locked.revision !== pinned.revision))
574
+ throw new Error("The bound resource's account changed; explicitly unbind before using a new account");
575
+ resource.connection = pinned;
576
+ }
577
+ } catch {
578
+ problems.push({
579
+ file: resource.file,
580
+ message: `${resource.id}: its connection is unavailable or changed; reconnect and explicitly rebind`,
581
+ });
582
+ resources.splice(resources.indexOf(resource), 1);
583
+ }
584
+ }
585
+ }
530
586
  return {
531
587
  // Only `types`' resources have a vendor object to check or apply: not the people's (vendorless).
532
588
  resources: resources
533
589
  .filter((r) => types.has(`${r.vendor}.${r.type}`))
534
- .map(({ id, vendor, type, name, identity, binding, desired, refs }) => ({
590
+ .map(({ id, vendor, type, name, identity, binding, desired, refs, connection }) => ({
535
591
  id,
536
592
  vendor,
537
593
  type,
@@ -540,6 +596,7 @@ async function readDeclared(
540
596
  ...(binding !== undefined && { binding }),
541
597
  desired: withoutWriteOnly(types.get(`${vendor}.${type}`)!.fields, desired),
542
598
  refs,
599
+ ...(connection && { connection }),
543
600
  })),
544
601
  bindings,
545
602
  orphans,
@@ -563,11 +620,15 @@ function indexConnectors(list: Connector<any, any>[]) {
563
620
  }
564
621
 
565
622
  /**
566
- * The drivers' functions by operation id, refusing one no connector declares. A driver that
567
- * declares an `env` has each function guarded by it (`guarded`); a fake declares none, so a
568
- * sandbox run's calls are not.
623
+ * The drivers' functions by operation id, refusing one no connector declares. Named live
624
+ * calls resolve authentication here, after policy, inside the driver's execution boundary.
625
+ * Drivers without named authentication retain their declared environment guard.
569
626
  */
570
- export function indexDrivers(list: Driver[], ops: Map<string, Op>) {
627
+ export function indexDrivers(
628
+ list: Driver[],
629
+ ops: Map<string, Op>,
630
+ options: { connections?: ConnectionRegistry; sandbox?: boolean } = {},
631
+ ) {
571
632
  const map = new Map<string, DriverFn>();
572
633
  for (const d of list) {
573
634
  for (const [key, fn] of Object.entries(d.ops)) {
@@ -575,7 +636,36 @@ export function indexDrivers(list: Driver[], ops: Map<string, Op>) {
575
636
  if (!ops.has(id)) {
576
637
  throw new Error(`Driver "${d.vendor}" implements ${id}, which no connector in \`connectors\` declares`);
577
638
  }
578
- map.set(id, d.env ? guarded(d.vendor, d.env, fn) : fn);
639
+ const unselected = d.env ? guarded(d.vendor, d.env, fn) : fn;
640
+ map.set(id, async (input, call) => {
641
+ if (options.sandbox) return fn(input, call);
642
+ if (!call.connection) {
643
+ if (d.authentication?.length)
644
+ throw new DriverError("Select a named connection for this driver", { retryable: false });
645
+ return unselected(input, call);
646
+ }
647
+ if (!options.connections || !call.principal || !d.authentication?.includes(call.connection.method))
648
+ throw new DriverError("This driver does not support the selected connection", { retryable: false });
649
+ const authentication = await options.connections.authenticate(call.connection, call.principal.id);
650
+ try {
651
+ return await fn(input, {
652
+ ...call,
653
+ connection: structuredClone(call.connection),
654
+ authentication,
655
+ secrets: secretsForInput(input, call.secrets, ops.get(id)!.createsSecrets?.(input)),
656
+ });
657
+ } catch (error) {
658
+ if (isRunControlError(error)) throw error;
659
+ // SDK exceptions can contain the complete request, including authorization headers.
660
+ // Drivers may come from another installed copy of this package.
661
+ const failure = error as { code?: unknown; retryable?: unknown; status?: unknown } | null;
662
+ const status = failure?.code === "driver_failed" ? failure.status : undefined;
663
+ throw new DriverError(`${d.vendor} operation failed`, {
664
+ retryable: failure?.code === "driver_failed" && failure.retryable === true,
665
+ ...(typeof status === "number" && Number.isInteger(status) && status >= 100 && status <= 599 && { status }),
666
+ });
667
+ }
668
+ });
579
669
  }
580
670
  }
581
671
  return map;
@@ -583,12 +673,10 @@ export function indexDrivers(list: Driver[], ops: Map<string, Op>) {
583
673
 
584
674
  /**
585
675
  * `fn`, refused while the environment leaves a variable `env` declares missing or invalid. The
586
- * worker started without it (`startWorker` only warns), and stored credentials load into the
587
- * environment as they are set, so the check is made when the function is called, inside the
588
- * driver step, and nowhere earlier: not at start, and not before the policy's decision or an
589
- * approval hold, so a credential set on the connector page while a call waits for approval is
590
- * used when the approver approves. The refusal is the call's own failure, final: recorded as
591
- * the call's, and never retried.
676
+ * worker started without it (`startWorker` only warns). This checks only explicitly supplied
677
+ * process environment; stored credentials are never copied there. The guard runs inside the
678
+ * driver step after policy and approval. Named connections never use this path. The refusal
679
+ * is the call's own failure, final: recorded as the call's, and never retried.
592
680
  */
593
681
  function guarded(vendor: string, env: DriverEnv, fn: DriverFn): DriverFn {
594
682
  return async (input, call) => {