@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/op.ts CHANGED
@@ -1,3 +1,6 @@
1
+ import type { Principal } from "./define.ts";
2
+ import type { Authentication, CallAuthentication } from "./authentication.ts";
3
+ import type { ConnectionSnapshot } from "./connection.ts";
1
4
  import type { ProviderV4 } from "@ai-sdk/provider";
2
5
  import type { z } from "zod";
3
6
  import type { Fake } from "./fake.ts";
@@ -38,6 +41,8 @@ export interface OpSpec<I extends z.ZodType = z.ZodType, O extends z.ZodType = z
38
41
  * (a resource type's `handle`): the ledger records each as `"<name>"`, never its value.
39
42
  */
40
43
  opaque?: readonly string[];
44
+ /** Logical destinations this operation may create with Secrets.ensure, derived from its authorized input. */
45
+ createsSecrets?: (input: any) => readonly string[];
41
46
  }
42
47
 
43
48
  /** One vendor operation, such as `ghost.post.publish`. Declares the contract only; drivers implement it. */
@@ -56,6 +61,7 @@ export interface Op<V extends string = string, R extends string = string, N exte
56
61
  readonly phrases?: { readonly given?: string; readonly expect?: string };
57
62
  /** From the spec: fields the ledger records by name only. */
58
63
  readonly opaque?: readonly string[];
64
+ readonly createsSecrets?: (input: any) => readonly string[];
59
65
  readonly input: z.ZodType<any, I>;
60
66
  readonly output: z.ZodType<O>;
61
67
  }
@@ -108,7 +114,7 @@ export type Connector<V extends string, S extends Specs> = {
108
114
  * and `resolveConfig` reads only a record of its own shape (`checkConnector`): change it with
109
115
  * the record, so a build refuses a connector another build made in another shape.
110
116
  */
111
- export const CONNECTOR_FORMAT = 1;
117
+ export const CONNECTOR_FORMAT = 3;
112
118
 
113
119
  /**
114
120
  * A connector's vendor: its id, who it is (`defineConnector`'s third argument, for a UI), its
@@ -209,6 +215,7 @@ export function defineConnector<const V extends string, const S extends Specs>(
209
215
  target: spec.target,
210
216
  phrases: spec.phrases,
211
217
  ...(spec.opaque?.length ? { opaque: spec.opaque } : {}),
218
+ ...(spec.createsSecrets && { createsSecrets: spec.createsSecrets }),
212
219
  input: spec.input,
213
220
  output: spec.output,
214
221
  } satisfies Op);
@@ -236,6 +243,12 @@ export function defineConnector<const V extends string, const S extends Specs>(
236
243
  * not repeat the side effect.
237
244
  */
238
245
  export interface CallContext {
246
+ /** Authenticated initiating principal; never supplied by operation input. */
247
+ principal?: Principal;
248
+ /** Authorized account metadata. Credential material stays inside the driver boundary. */
249
+ connection?: ConnectionSnapshot;
250
+ /** Resolved after policy, for this call alone. Never return it from a driver. */
251
+ authentication?: CallAuthentication;
239
252
  idempotencyKey: string;
240
253
  runId: string;
241
254
  opId: string;
@@ -295,13 +308,15 @@ export function retryableStatus(status: number): boolean {
295
308
  export type DriverEnv = z.ZodObject<Record<string, z.ZodType<string | undefined, string | undefined>>>;
296
309
 
297
310
  /**
298
- * Implements a vendor's operations, keyed `"resource.name"`. A driver reads its credentials
299
- * from the environment when it is called, never from the config file; `env` declares them.
311
+ * Implements a vendor's operations, keyed `"resource.name"`. Named authentication is supplied
312
+ * in the call context. `env` describes a driver that has not added named-account support.
300
313
  */
301
314
  export interface Driver {
302
315
  readonly vendor: string;
303
316
  readonly ops: Readonly<Record<string, DriverFn>>;
304
317
  readonly env?: DriverEnv;
318
+ /** Methods this driver consumes from CallContext.authentication. Others fail closed. */
319
+ readonly authentication?: readonly string[];
305
320
  }
306
321
 
307
322
  /**
@@ -313,7 +328,9 @@ export interface Driver {
313
328
  * no repository a change is reviewed on.
314
329
  */
315
330
  export interface ConnectorSetup {
316
- /** Makes the live driver, which reads its credentials from the environment when called: once per resolved config. */
331
+ /** Auth methods available to named connections; login identity is separate. */
332
+ authentication?: Readonly<Record<string, Authentication>>;
333
+ /** Makes the live driver once per resolved config; credentials belong to each invocation. */
317
334
  driver?: () => Driver;
318
335
  /** Makes a fake of the vendor (`defineFake`): once per sandbox run, its own. */
319
336
  fake?: () => Fake<any, any>;
@@ -457,8 +474,8 @@ export type OpIdOf<V extends string, S extends Specs> = {
457
474
  */
458
475
  export function defineDriver<V extends string, S extends Specs>(
459
476
  connector: Connector<V, S>,
460
- impl: DriverImpl<S>,
461
- options: { env?: DriverEnv } = {},
477
+ impl: DriverImpl<S> | ((call: CallContext) => DriverImpl<S>),
478
+ options: { env?: DriverEnv; authentication?: readonly string[] } = {},
462
479
  ): Driver {
463
480
  const declared = new Map<string, Op>();
464
481
  for (const resource of Object.values(connector as Record<string, Record<string, unknown>>)) {
@@ -466,6 +483,18 @@ export function defineDriver<V extends string, S extends Specs>(
466
483
  }
467
484
  const vendor = connector[VENDOR].id;
468
485
  const ops: Record<string, DriverFn> = {};
486
+ if (typeof impl === "function") {
487
+ // Build clients only once the runtime has authorized and authenticated this invocation.
488
+ for (const key of declared.keys()) {
489
+ ops[key] = async (input, call) => defineDriver(connector, impl(call), options).ops[key]!(input, call);
490
+ }
491
+ return Object.freeze({
492
+ vendor,
493
+ ops: Object.freeze(ops),
494
+ ...(options.env && { env: options.env }),
495
+ ...(options.authentication && { authentication: Object.freeze([...options.authentication]) }),
496
+ });
497
+ }
469
498
  const extra: string[] = [];
470
499
  for (const [resource, fns] of Object.entries(impl as Record<string, Record<string, unknown>>)) {
471
500
  for (const [name, fn] of Object.entries(fns ?? {})) {
@@ -480,5 +509,10 @@ export function defineDriver<V extends string, S extends Specs>(
480
509
  ...(extra.length ? [`it implements ${extra.join(", ")}, which the connector does not declare`] : []),
481
510
  ];
482
511
  if (problems.length) throw new Error(`defineDriver("${vendor}"): ${problems.join("; ")}`);
483
- return Object.freeze({ vendor, ops: Object.freeze(ops), ...(options.env && { env: options.env }) });
512
+ return Object.freeze({
513
+ vendor,
514
+ ops: Object.freeze(ops),
515
+ ...(options.env && { env: options.env }),
516
+ ...(options.authentication && { authentication: Object.freeze([...options.authentication]) }),
517
+ });
484
518
  }
package/src/outline.ts CHANGED
@@ -44,7 +44,7 @@ export type OutlineNode = { path: string; span: Span } &
44
44
  */
45
45
  /** `name`: the one the call gives (`ctx.x.y.z(input, { name: "…" })`), else the id when no segment is computed. */
46
46
  (
47
- | { kind: "op"; id: string; ops?: string[]; name?: string }
47
+ | { kind: "op"; id: string; ops?: string[]; name?: string; connection?: { id: string } | { dynamic: true } }
48
48
  /**
49
49
  * `ctx.approval(...)`, with its title, and the title as its `name`, when the first argument is
50
50
  * a string literal or a top-level `const` holding one.
@@ -391,12 +391,25 @@ export function outlineBody(
391
391
  * A step's name as the call gives it, in `{ name: "…" }` after the call's input: a string
392
392
  * literal or a top-level const, not an operation's id or `Sleep`, which unnamed calls are called.
393
393
  */
394
- const nameIn = (options: Node | undefined): string | undefined => {
394
+ const nameIn = (options: Node | undefined, connection = false): string | undefined => {
395
395
  if (options === undefined) return undefined;
396
396
  const literal = unwrap(options);
397
- const property =
398
- literal?.type === "ObjectExpression" && literal.properties.length === 1 ? literal.properties[0] : undefined;
399
- const key = property?.type === "Property" && !property.computed ? (property.key.name ?? property.key.value) : "";
397
+ if (
398
+ literal?.type !== "ObjectExpression" ||
399
+ literal.properties.some(
400
+ (p: Node) =>
401
+ p.type !== "Property" ||
402
+ p.computed ||
403
+ !(connection ? ["name", "connection"] : ["name"]).includes(p.key.name ?? p.key.value),
404
+ )
405
+ ) {
406
+ problem(options, NAME);
407
+ return undefined;
408
+ }
409
+ const property = literal.properties.find((p: Node) => (p.key.name ?? p.key.value) === "name");
410
+ if (!property && literal.properties.some((p: Node) => (p.key.name ?? p.key.value) === "connection"))
411
+ return undefined;
412
+ const key = property?.key.name ?? property?.key.value;
400
413
  const name = key === "name" ? literalOf(property.value, consts) : undefined;
401
414
  if (name === undefined || !/\S/.test(name)) problem(options, NAME);
402
415
  else if (name === DEFAULT_SLEEP_NAME || OP_ID.test(name)) problem(options, DEFAULT_LIKE(name));
@@ -404,6 +417,31 @@ export function outlineBody(
404
417
  return undefined;
405
418
  };
406
419
 
420
+ // Dependency information only. Execution still resolves and authorizes the actual reference.
421
+ // An imported/computed reference stays unknown rather than being guessed from its variable name.
422
+ const connectionIn = (options: Node | undefined): { id: string } | { dynamic: true } | undefined => {
423
+ if (!options) return undefined;
424
+ const object = options && unwrap(options);
425
+ if (object?.type !== "ObjectExpression") return { dynamic: true };
426
+ if (object.properties.some((p: Node) => p.type !== "Property" || p.computed)) return { dynamic: true };
427
+ const property =
428
+ object?.type === "ObjectExpression" &&
429
+ object.properties.findLast(
430
+ (p: Node) => p.type === "Property" && !p.computed && (p.key.name ?? p.key.value) === "connection",
431
+ );
432
+ if (!property) return undefined;
433
+ const ref = unwrap(property.value);
434
+ if (ref?.type !== "ObjectExpression") return { dynamic: true };
435
+ if (ref.properties.some((p: Node) => p.type !== "Property" || p.computed)) return { dynamic: true };
436
+ const key =
437
+ ref?.type === "ObjectExpression" &&
438
+ ref.properties.findLast(
439
+ (p: Node) => p.type === "Property" && !p.computed && (p.key.name ?? p.key.value) === "$connection",
440
+ );
441
+ const id = key && literalOf(key.value, consts);
442
+ return typeof id === "string" ? { id } : { dynamic: true };
443
+ };
444
+
407
445
  // The arguments are evaluated before the call is made, so their calls come first.
408
446
  const call = (node: Node, at: Where): OutlineNode[] => {
409
447
  const args: Node[] = node.arguments;
@@ -428,10 +466,18 @@ export function outlineBody(
428
466
  default:
429
467
  if (isOpPath(members)) {
430
468
  // A computed id's own name is the operation it calls, known only in a run.
431
- const name = nameIn(args[1]) ?? (id.includes("*") ? undefined : id);
469
+ const name = nameIn(args[1], true) ?? (id.includes("*") ? undefined : id);
470
+ const connection = connectionIn(args[1]);
432
471
  return [
433
472
  ...walk(args, at),
434
- { kind: "op", path, id, span: span(node), ...(name === undefined ? {} : { name }) },
473
+ {
474
+ kind: "op",
475
+ path,
476
+ id,
477
+ span: span(node),
478
+ ...(name === undefined ? {} : { name }),
479
+ ...(connection && { connection }),
480
+ },
435
481
  ];
436
482
  }
437
483
  return READS.has(id) ? walk(args, at) : [...walk(args, at), ...problem(node, CTX_CALL(id))];
package/src/paths.ts CHANGED
@@ -1,11 +1,26 @@
1
1
  import { existsSync, readdirSync, statSync } from "node:fs";
2
2
  import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
3
- import { APP_NAME } from "./shared.ts";
3
+ import { APP_NAME, APPS_DIR, appOfPath } from "./shared.ts";
4
4
 
5
5
  // Where an import may reach, as the lints and the data-file reader all decide it, the one walk up
6
6
  // the directories and the one walk down (`filesUnder`). Node's `path` and `fs` only, no parser:
7
7
  // the oxlint plugin loads it.
8
8
 
9
+ /**
10
+ * Where a data file imports `secret` from, and `sanoma`, the runtime's own resource constructors
11
+ * (its people: `sanoma.person`, `.agent`, `.group`): what the loader serves for them.
12
+ */
13
+ export const SECRET_ENTRIES: ReadonlySet<string> = new Set(["sanoma/resources", "@sanoma-ai/workflows/resources"]);
14
+ /** Where a site imports `api`, `RunForm` and `defineSite` from: `sanoma/site`, which re-exports the app's. The loader serves `defineSite`. */
15
+ export const SITE_ENTRIES: ReadonlySet<string> = new Set(["sanoma/site", "@sanoma-ai/app/site"]);
16
+ /** Where `resources/data.ts` imports `collection` from: what the loader serves for it. */
17
+ export const DATA_ENTRIES: ReadonlySet<string> = new Set(["sanoma/data", "@sanoma-ai/workflows/data"]);
18
+ /**
19
+ * The one data file that holds collections, relative to `resources/`. Workflows and the runtime
20
+ * import it, so it holds collections only.
21
+ */
22
+ export const COLLECTIONS_FILE = "data.ts";
23
+
9
24
  /** A relative import specifier: `./x.ts`, `../y.ts`. */
10
25
  export const RELATIVE = /^\.\.?\//;
11
26
 
@@ -34,6 +49,9 @@ export function inside(dir: string, path: string): boolean {
34
49
  return rel === "" || (rel.split(sep)[0] !== ".." && !isAbsolute(rel));
35
50
  }
36
51
 
52
+ /** `path`, under `root`, relative to it with `/`: `resources/github.ts`. */
53
+ export const relPath = (root: string, path: string): string => relative(root, path).split(sep).join("/");
54
+
37
55
  /** True when `path` is a directory. */
38
56
  export const isDirectory = (path: string): boolean => statSync(path, { throwIfNoEntry: false })?.isDirectory() ?? false;
39
57
 
@@ -47,8 +65,7 @@ export function filesUnder(dir: string, keep: (name: string) => boolean): string
47
65
  .toSorted();
48
66
  }
49
67
 
50
- /** Where a company's apps are, under its root: `apps/<app>/`. */
51
- export const APPS_DIR = "apps";
68
+ export { APPS_DIR, appOfPath };
52
69
 
53
70
  /** Each directory's company root (the nearest one with a `sanoma.config.ts`, short of a `node_modules`), once found. */
54
71
  const roots = new Map<string, string>();
@@ -77,9 +94,7 @@ export function appOfFile(file: string | undefined): string | undefined {
77
94
  // Found roots only: a directory that is no company's yet may become one (`sanoma` in an empty folder).
78
95
  if (root !== null) roots.set(dir, root);
79
96
  }
80
- if (root === null) return undefined;
81
- const [top, app, ...rest] = relative(root, file).split(sep);
82
- return top === APPS_DIR && app !== undefined && rest.length && APP_NAME.test(app) ? app : undefined;
97
+ return root === null ? undefined : appOfPath(relPath(root, file));
83
98
  }
84
99
 
85
100
  /** The apps' folders under `root` whose names are an `APP_NAME`, sorted: `apps/<app>/`, absolute. */
package/src/plugin.ts CHANGED
@@ -1,9 +1,6 @@
1
- import { readDataFile } from "./datafile.ts";
2
-
3
- // An oxlint JS plugin with two rules, on the trees oxlint has already parsed, which the package's
4
- // `oxlint.json` turns on: `sanoma/data-file` for `resources/**`, the data-file subset
5
- // `readResources` reads, the same check as `lintResources`; and `sanoma/date` for `workflows/**`,
6
- // `Date` reading the clock. Typed here by the little they use, so the package needs no oxlint types.
1
+ // An oxlint JS plugin with one rule, on the trees oxlint has already parsed, which the package's
2
+ // `oxlint.json` turns on for `workflows/**`: `sanoma/date`, `Date` reading the clock. Typed here by
3
+ // the little it uses, so the package needs no oxlint types.
7
4
 
8
5
  interface Context {
9
6
  filename: string;
@@ -65,24 +62,6 @@ function readsNoClock(node: Node): boolean {
65
62
  const plugin = {
66
63
  meta: { name: "sanoma" },
67
64
  rules: {
68
- "data-file": {
69
- meta: {
70
- type: "problem",
71
- docs: {
72
- description:
73
- "A data file under resources/ holds only imports, `export const <name> = <vendor>.<type>({ … })` with literal fields, and `export default [ … ]`; resources/data.ts holds only collections, `collection({ … })` from sanoma/data",
74
- },
75
- },
76
- create(context: Context) {
77
- return {
78
- Program(program: unknown) {
79
- for (const { start, end, message } of readDataFile(program, context.filename).problems) {
80
- context.report({ message, node: { range: [start, end] } });
81
- }
82
- },
83
- };
84
- },
85
- },
86
65
  // `Date` reads the clock in `Date.now()`, `new Date()` and `Date()`, which a replay would read
87
66
  // again; a workflow reads it with `ctx.now()`, recorded. Every other use of `Date` is pure:
88
67
  // `new Date(value)` (of `await ctx.now()`, or of a date the workflow has), `Date.parse`, `Date.UTC`.
package/src/policy.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { ConnectionSnapshot } from "./connection.ts";
1
2
  import { type ApprovalState, type Approver, type Principal, sameApprover } from "./define.ts";
2
3
  import type { Effect, Op } from "./op.ts";
3
4
  import { CALL_WORKFLOW, isVisitor } from "./shared.ts";
@@ -16,6 +17,7 @@ export const policyOpOf = ({ id, vendor, resource, name, effect }: Op): PolicyOp
16
17
 
17
18
  /** What a policy sees for each operation a run calls: plain data. */
18
19
  export interface PolicyCall {
20
+ connection?: ConnectionSnapshot;
19
21
  op: PolicyOp;
20
22
  /** The same as `op.effect`. */
21
23
  effect: Effect;
package/src/resource.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { ConnectionRef } from "./connection.ts";
1
2
  import { z } from "zod";
2
3
  import { checkDatabase, type DatabaseSpec } from "./database.ts";
3
4
  import { isRunControlError } from "./errors.ts";
@@ -173,7 +174,7 @@ export async function callAfterCreate(
173
174
  */
174
175
  export const DECLARED: unique symbol = Symbol.for("sanoma.declared");
175
176
 
176
- /** A resource a data file declares: `github.repository({ name: "sanoma" })`. */
177
+ /** A resource a data file declares: `github.repository({ owner: "acme", name: "sanoma" })`. */
177
178
  export interface Declared<Id extends string = string, T = State> {
178
179
  readonly [DECLARED]: Id;
179
180
  readonly vendor: string;
@@ -185,6 +186,7 @@ export interface Declared<Id extends string = string, T = State> {
185
186
  * checked, and what a drift check compares.
186
187
  */
187
188
  readonly desired: T;
189
+ readonly connection?: ConnectionRef;
188
190
  /**
189
191
  * Where it names another resource: the field's dotted path (a list item's with its index,
190
192
  * `teams.0`) to that resource's `<vendor>.<type>:<name>`.
@@ -331,7 +333,10 @@ export interface Resource<
331
333
  >
332
334
  >,
333
335
  Readonly<Pick<ResourceSpec<S>, "identity" | "find" | "host" | "wakeup" | "database" | "afterCreate">> {
334
- (desired: Declaring<z.input<S>, R>): Declared<Id, z.input<S>>;
336
+ (
337
+ desired: Declaring<z.input<S>, R>,
338
+ options?: { connection?: ConnectionRef<Id extends `${infer V}.${string}` ? V : string> },
339
+ ): Declared<Id, z.input<S>>;
335
340
  /** `read` and `import`, each effect `read`, idempotent, with the id as `target`; for a writable type, the change operations too. */
336
341
  readonly ops: ResourceOps<S, W>;
337
342
  /** True when apply may change its objects: it has the change operations. */
@@ -349,7 +354,9 @@ export const isObject = (v: unknown): v is State => typeof v === "object" && v !
349
354
  /** `path.name`, or `name` at the top: a field's dotted path. */
350
355
  export const joinPath = (path: string, name: string) => (path ? `${path}.${name}` : name);
351
356
 
352
- const isDeclared = (v: unknown): v is Declared => isObject(v) && typeof (v as Partial<Declared>)[DECLARED] === "string";
357
+ /** A declared resource: what a constructor returns, made by any copy of this package. */
358
+ export const isDeclared = (v: unknown): v is Declared =>
359
+ isObject(v) && typeof (v as Partial<Declared>)[DECLARED] === "string";
353
360
 
354
361
  /** One order for a list compared as a multiset: by each item's JSON. */
355
362
  const canonical = (items: unknown[]) =>
@@ -577,8 +584,8 @@ function pathsIn(value: unknown, paths: ReadonlySet<string>, prefix = ""): strin
577
584
  *
578
585
  * The ledger records a `handle` as `"<handle>"`, never its bytes (the operations' `opaque`).
579
586
  *
580
- * Calling the result declares one resource, for a data file: `repository({ name: "sanoma" })`
581
- * is a `Declared`, `{ vendor, type, name: "sanoma", desired, refs: {} }`. It refuses fields the
587
+ * Calling the result declares one resource, for a data file: `repository({ owner: "acme", name: "sanoma" })`
588
+ * is a `Declared`, `{ vendor, type, name: "acme/sanoma", desired, refs: {} }`. It refuses fields the
582
589
  * schema does not have, fields the vendor owns (unless the identity names them), values the
583
590
  * schema rejects, and an empty identity. Where `fields.references` allows it, a field may name
584
591
  * another declared resource of that type (`repository_id: site`): `desired` holds, and the
@@ -663,7 +670,17 @@ export function defineResource<
663
670
  const owned = new Set(fields.vendorOwned.filter((path) => !named.has(path)));
664
671
  const references: References = fields.references ?? {};
665
672
  const writeOnly = new Set(fields.writeOnly);
666
- const declare = (given: Declaring<z.input<S>, R>): Declared<`${V}.${T}`, z.input<S>> => {
673
+ const declare = (
674
+ given: Declaring<z.input<S>, R>,
675
+ options?: { connection?: ConnectionRef },
676
+ ): Declared<`${V}.${T}`, z.input<S>> => {
677
+ const connection = options?.connection;
678
+ if (
679
+ connection &&
680
+ (connection.vendor !== vendor || !new RegExp(`^${vendor}/[a-z][a-z0-9-]{0,63}$`).test(connection.$connection))
681
+ ) {
682
+ throw new Error(`${what}: select a declared connection of this connector`);
683
+ }
667
684
  const refs: Record<string, string> = {};
668
685
  // Where a secret is given, with list items' indexes: the schema does not judge its marker.
669
686
  const secrets = new Set<string>();
@@ -723,6 +740,7 @@ export function defineResource<
723
740
  ...(name !== undefined && { name }),
724
741
  desired,
725
742
  refs: Object.freeze(refs),
743
+ ...(connection && { connection: Object.freeze({ ...connection }) }),
726
744
  });
727
745
  };
728
746
  const resource = Object.assign(declare, {