@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.
- package/README.md +193 -50
- package/SECRET-SUPPORT.md +33 -0
- package/dist/apply-steps.js +5 -2
- package/dist/apply-steps.js.map +1 -1
- package/dist/apply.d.ts +3 -0
- package/dist/apply.js +27 -13
- package/dist/apply.js.map +1 -1
- package/dist/approvals.d.ts +1 -0
- package/dist/approvals.js +6 -1
- package/dist/approvals.js.map +1 -1
- package/dist/apps.d.ts +13 -24
- package/dist/apps.js +69 -68
- package/dist/apps.js.map +1 -1
- package/dist/ast.d.ts +9 -7
- package/dist/ast.js +2 -0
- package/dist/ast.js.map +1 -1
- package/dist/authentication.d.ts +54 -0
- package/dist/authentication.js +138 -0
- package/dist/authentication.js.map +1 -0
- package/dist/call-workflow.d.ts +5 -0
- package/dist/call-workflow.js +4 -3
- package/dist/call-workflow.js.map +1 -1
- package/dist/call.d.ts +3 -2
- package/dist/call.js +36 -7
- package/dist/call.js.map +1 -1
- package/dist/cedar.js +14 -1
- package/dist/cedar.js.map +1 -1
- package/dist/change-steps.js +7 -5
- package/dist/change-steps.js.map +1 -1
- package/dist/change.d.ts +7 -0
- package/dist/change.js +7 -0
- package/dist/change.js.map +1 -1
- package/dist/client.d.ts +62 -10
- package/dist/client.js +272 -47
- package/dist/client.js.map +1 -1
- package/dist/collection.d.ts +7 -0
- package/dist/collection.js +5 -2
- package/dist/collection.js.map +1 -1
- package/dist/config.d.ts +16 -4
- package/dist/config.js +89 -21
- package/dist/config.js.map +1 -1
- package/dist/connection.d.ts +389 -0
- package/dist/connection.js +657 -0
- package/dist/connection.js.map +1 -0
- package/dist/credentials.d.ts +7 -53
- package/dist/credentials.js +84 -247
- package/dist/credentials.js.map +1 -1
- package/dist/db.js +34 -2
- package/dist/db.js.map +1 -1
- package/dist/define.d.ts +9 -2
- package/dist/define.js.map +1 -1
- package/dist/definitions.d.ts +20 -0
- package/dist/definitions.js +16 -0
- package/dist/definitions.js.map +1 -0
- package/dist/deployment-key.d.ts +2 -0
- package/dist/deployment-key.js +22 -0
- package/dist/deployment-key.js.map +1 -0
- package/dist/describe.d.ts +15 -175
- package/dist/describe.js +122 -35
- package/dist/describe.js.map +1 -1
- package/dist/description.d.ts +532 -0
- package/dist/description.js +381 -0
- package/dist/description.js.map +1 -0
- package/dist/direct.js +7 -0
- package/dist/direct.js.map +1 -1
- package/dist/drift.d.ts +1 -1
- package/dist/drift.js +1 -1
- package/dist/drift.js.map +1 -1
- package/dist/env.d.ts +2 -0
- package/dist/env.js +2 -2
- package/dist/env.js.map +1 -1
- package/dist/fake.d.ts +13 -1
- package/dist/fake.js +70 -0
- package/dist/fake.js.map +1 -1
- package/dist/index.d.ts +11 -3
- package/dist/index.js +10 -2
- package/dist/index.js.map +1 -1
- package/dist/ledger.d.ts +2 -0
- package/dist/ledger.js +1 -1
- package/dist/ledger.js.map +1 -1
- package/dist/lint.d.ts +0 -9
- package/dist/lint.js +2 -17
- package/dist/lint.js.map +1 -1
- package/dist/load.d.ts +85 -0
- package/dist/load.js +391 -0
- package/dist/load.js.map +1 -0
- package/dist/lockfile.d.ts +2 -0
- package/dist/lockfile.js +8 -3
- package/dist/lockfile.js.map +1 -1
- package/dist/op.d.ts +22 -5
- package/dist/op.js +20 -2
- package/dist/op.js.map +1 -1
- package/dist/outline.d.ts +5 -0
- package/dist/outline.js +46 -5
- package/dist/outline.js.map +1 -1
- package/dist/paths.d.ts +18 -2
- package/dist/paths.js +19 -7
- package/dist/paths.js.map +1 -1
- package/dist/plugin.d.ts +0 -11
- package/dist/plugin.js +3 -18
- package/dist/plugin.js.map +1 -1
- package/dist/policy.d.ts +2 -0
- package/dist/policy.js.map +1 -1
- package/dist/resource.d.ts +10 -4
- package/dist/resource.js +11 -4
- package/dist/resource.js.map +1 -1
- package/dist/resources.d.ts +33 -76
- package/dist/resources.js +219 -227
- package/dist/resources.js.map +1 -1
- package/dist/run.d.ts +4 -0
- package/dist/run.js.map +1 -1
- package/dist/sandbox.d.ts +5 -2
- package/dist/sandbox.js +56 -7
- package/dist/sandbox.js.map +1 -1
- package/dist/scenario.d.ts +5 -0
- package/dist/scenario.js +3 -2
- package/dist/scenario.js.map +1 -1
- package/dist/scenarios.js +7 -1
- package/dist/scenarios.js.map +1 -1
- package/dist/schema.js +8 -0
- package/dist/schema.js.map +1 -1
- package/dist/secret-store-check.d.ts +11 -0
- package/dist/secret-store-check.js +76 -0
- package/dist/secret-store-check.js.map +1 -0
- package/dist/secret-store.d.ts +106 -0
- package/dist/secret-store.js +228 -0
- package/dist/secret-store.js.map +1 -0
- package/dist/secret.d.ts +2 -0
- package/dist/secret.js +32 -0
- package/dist/secret.js.map +1 -1
- package/dist/shared.d.ts +21 -11
- package/dist/shared.js +41 -7
- package/dist/shared.js.map +1 -1
- package/dist/sites.d.ts +4 -93
- package/dist/sites.js +76 -84
- package/dist/sites.js.map +1 -1
- package/dist/steps.d.ts +3 -0
- package/dist/steps.js.map +1 -1
- package/dist/version.d.ts +2 -1
- package/dist/version.js +2 -1
- package/dist/version.js.map +1 -1
- package/dist/wakeup-driver.js +15 -6
- package/dist/wakeup-driver.js.map +1 -1
- package/dist/worker.js +12 -8
- package/dist/worker.js.map +1 -1
- package/dist/writer.d.ts +53 -14
- package/dist/writer.js +113 -43
- package/dist/writer.js.map +1 -1
- package/oxlint.json +1 -5
- package/package.json +6 -1
- package/src/apply-steps.ts +5 -2
- package/src/apply.ts +54 -21
- package/src/approvals.ts +7 -1
- package/src/apps.ts +71 -86
- package/src/ast.ts +6 -6
- package/src/authentication.ts +191 -0
- package/src/call-workflow.ts +5 -4
- package/src/call.ts +52 -9
- package/src/cedar.ts +14 -1
- package/src/change-steps.ts +7 -4
- package/src/change.ts +10 -0
- package/src/client.ts +346 -45
- package/src/collection.ts +10 -2
- package/src/config.ts +107 -19
- package/src/connection.ts +912 -0
- package/src/credentials.ts +94 -281
- package/src/db.ts +34 -2
- package/src/define.ts +7 -2
- package/src/definitions.ts +31 -0
- package/src/deployment-key.ts +23 -0
- package/src/describe.ts +150 -227
- package/src/description.ts +426 -0
- package/src/direct.ts +7 -0
- package/src/drift.ts +10 -2
- package/src/env.ts +4 -2
- package/src/fake.ts +71 -0
- package/src/index.ts +53 -1
- package/src/ledger.ts +3 -1
- package/src/lint.ts +2 -17
- package/src/load.ts +466 -0
- package/src/lockfile.ts +10 -3
- package/src/op.ts +41 -7
- package/src/outline.ts +53 -7
- package/src/paths.ts +21 -6
- package/src/plugin.ts +3 -24
- package/src/policy.ts +2 -0
- package/src/resource.ts +24 -6
- package/src/resources.ts +246 -301
- package/src/run.ts +4 -0
- package/src/sandbox.ts +61 -7
- package/src/scenario.ts +24 -6
- package/src/scenarios.ts +7 -1
- package/src/schema.ts +10 -0
- package/src/secret-store-check.ts +93 -0
- package/src/secret-store.ts +314 -0
- package/src/secret.ts +29 -0
- package/src/shared.ts +44 -10
- package/src/sites.ts +85 -179
- package/src/steps.ts +3 -2
- package/src/version.ts +2 -1
- package/src/wakeup-driver.ts +22 -6
- package/src/worker.ts +12 -8
- package/src/writer.ts +145 -56
- package/dist/datafile.d.ts +0 -97
- package/dist/datafile.js +0 -479
- package/dist/datafile.js.map +0 -1
- 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 =
|
|
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"`.
|
|
299
|
-
*
|
|
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
|
-
/**
|
|
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({
|
|
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
|
-
|
|
398
|
-
literal?.type
|
|
399
|
-
|
|
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
|
-
{
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2
|
-
|
|
3
|
-
//
|
|
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
|
-
(
|
|
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
|
-
|
|
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 = (
|
|
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, {
|