@sanoma-ai/workflows 0.10.2 → 0.10.6
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/CLOUD-IDENTITY.md +128 -17
- package/README.md +56 -13
- package/dist/apply.d.ts +11 -4
- package/dist/apply.js +127 -36
- package/dist/apply.js.map +1 -1
- package/dist/call.d.ts +5 -2
- package/dist/call.js +65 -20
- package/dist/call.js.map +1 -1
- package/dist/change.d.ts +5 -6
- package/dist/change.js +6 -9
- package/dist/change.js.map +1 -1
- package/dist/client.d.ts +18 -0
- package/dist/client.js +35 -3
- package/dist/client.js.map +1 -1
- package/dist/cloud-identity.d.ts +5 -1
- package/dist/cloud-identity.js +13 -77
- package/dist/cloud-identity.js.map +1 -1
- package/dist/config.d.ts +45 -4
- package/dist/config.js +73 -11
- package/dist/config.js.map +1 -1
- package/dist/db.d.ts +2 -0
- package/dist/db.js +7 -0
- package/dist/db.js.map +1 -1
- package/dist/deadlines.d.ts +26 -13
- package/dist/deadlines.js +50 -11
- package/dist/deadlines.js.map +1 -1
- package/dist/define.d.ts +29 -0
- package/dist/define.js +3 -0
- package/dist/define.js.map +1 -1
- package/dist/deployment-key.d.ts +20 -1
- package/dist/deployment-key.js +28 -3
- package/dist/deployment-key.js.map +1 -1
- package/dist/describe.js +28 -1
- package/dist/describe.js.map +1 -1
- package/dist/description.d.ts +3 -0
- package/dist/description.js +2 -0
- package/dist/description.js.map +1 -1
- package/dist/dns.d.ts +20 -0
- package/dist/dns.js +26 -0
- package/dist/dns.js.map +1 -0
- package/dist/drift.d.ts +4 -2
- package/dist/drift.js +13 -4
- package/dist/drift.js.map +1 -1
- package/dist/host.d.ts +13 -4
- package/dist/host.js +14 -5
- package/dist/host.js.map +1 -1
- package/dist/identity-broker.d.ts +14 -0
- package/dist/identity-broker.js +84 -0
- package/dist/identity-broker.js.map +1 -0
- package/dist/identity-provisioning.d.ts +29 -0
- package/dist/identity-provisioning.js +178 -0
- package/dist/identity-provisioning.js.map +1 -0
- package/dist/identity-service.d.ts +25 -0
- package/dist/identity-service.js +148 -0
- package/dist/identity-service.js.map +1 -0
- package/dist/index.d.ts +11 -6
- package/dist/index.js +9 -5
- package/dist/index.js.map +1 -1
- package/dist/ledger.d.ts +30 -1
- package/dist/ledger.js +31 -0
- package/dist/ledger.js.map +1 -1
- package/dist/login-code.d.ts +17 -0
- package/dist/login-code.js +32 -0
- package/dist/login-code.js.map +1 -0
- package/dist/op.d.ts +85 -10
- package/dist/op.js +20 -0
- package/dist/op.js.map +1 -1
- package/dist/outline.js +3 -3
- package/dist/outline.js.map +1 -1
- package/dist/provisioning-identity.d.ts +27 -0
- package/dist/provisioning-identity.js +81 -0
- package/dist/provisioning-identity.js.map +1 -0
- package/dist/resource.d.ts +73 -10
- package/dist/resource.js +103 -14
- package/dist/resource.js.map +1 -1
- package/dist/resources.d.ts +4 -2
- package/dist/resources.js +16 -6
- package/dist/resources.js.map +1 -1
- package/dist/run.d.ts +3 -0
- package/dist/run.js.map +1 -1
- package/dist/runtime-ops.d.ts +182 -0
- package/dist/runtime-ops.js +294 -0
- package/dist/runtime-ops.js.map +1 -0
- package/dist/scenario.d.ts +6 -3
- package/dist/scenario.js +13 -1
- package/dist/scenario.js.map +1 -1
- package/dist/seal.d.ts +27 -0
- package/dist/seal.js +76 -0
- package/dist/seal.js.map +1 -0
- package/dist/secret-store.d.ts +6 -4
- package/dist/secret-store.js +11 -6
- package/dist/secret-store.js.map +1 -1
- package/dist/secret.d.ts +7 -0
- package/dist/secret.js +13 -1
- package/dist/secret.js.map +1 -1
- package/dist/shared.d.ts +15 -4
- package/dist/shared.js +27 -7
- package/dist/shared.js.map +1 -1
- package/dist/ssh-key.d.ts +11 -0
- package/dist/ssh-key.js +44 -0
- package/dist/ssh-key.js.map +1 -0
- package/dist/tokens.d.ts +35 -0
- package/dist/tokens.js +71 -0
- package/dist/tokens.js.map +1 -0
- package/dist/wakeup-service.js +2 -7
- package/dist/wakeup-service.js.map +1 -1
- package/dist/wakeup.d.ts +18 -0
- package/dist/wakeup.js +15 -0
- package/dist/wakeup.js.map +1 -1
- package/dist/worker.d.ts +2 -2
- package/dist/worker.js +17 -9
- package/dist/worker.js.map +1 -1
- package/oxlint.json +2 -1
- package/package.json +5 -1
- package/src/apply.ts +166 -48
- package/src/call.ts +88 -18
- package/src/change.ts +9 -20
- package/src/client.ts +48 -7
- package/src/cloud-identity.ts +16 -76
- package/src/config.ts +116 -18
- package/src/db.ts +8 -1
- package/src/deadlines.ts +66 -20
- package/src/define.ts +33 -0
- package/src/deployment-key.ts +34 -8
- package/src/describe.ts +39 -2
- package/src/description.ts +2 -0
- package/src/dns.ts +29 -0
- package/src/drift.ts +13 -4
- package/src/host.ts +22 -6
- package/src/identity-broker.ts +94 -0
- package/src/identity-provisioning.ts +208 -0
- package/src/identity-service.ts +179 -0
- package/src/index.ts +20 -1
- package/src/ledger.ts +52 -1
- package/src/login-code.ts +52 -0
- package/src/op.ts +94 -11
- package/src/outline.ts +3 -3
- package/src/provisioning-identity.ts +106 -0
- package/src/resource.ts +155 -20
- package/src/resources.ts +14 -4
- package/src/run.ts +3 -0
- package/src/runtime-ops.ts +313 -0
- package/src/scenario.ts +24 -6
- package/src/seal.ts +95 -0
- package/src/secret-store.ts +12 -9
- package/src/secret.ts +16 -1
- package/src/shared.ts +31 -7
- package/src/ssh-key.ts +48 -0
- package/src/tokens.ts +100 -0
- package/src/wakeup-service.ts +2 -8
- package/src/wakeup.ts +21 -0
- package/src/worker.ts +29 -11
package/src/resource.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { ConnectionRef } from "./connection.ts";
|
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { checkDatabase, type DatabaseSpec } from "./database.ts";
|
|
4
4
|
import { isRunControlError } from "./errors.ts";
|
|
5
|
-
import { checkHost, type HostSpec } from "./host.ts";
|
|
5
|
+
import { checkHost, hasPath, type HostSpec } from "./host.ts";
|
|
6
6
|
import { type Connector, type OpSpec, RESOURCE, VENDOR } from "./op.ts";
|
|
7
7
|
import { isSanomaSecret, isSecret, type Secret } from "./secret.ts";
|
|
8
8
|
import { errorMessage } from "./shared.ts";
|
|
@@ -37,6 +37,26 @@ export interface FieldReference {
|
|
|
37
37
|
* declared one. Without it, the field holds the resource's `name`, and is compared as it is.
|
|
38
38
|
*/
|
|
39
39
|
readonly by?: readonly string[];
|
|
40
|
+
/**
|
|
41
|
+
* A dotted path in that resource's state: apply and drift give this field the named resource's
|
|
42
|
+
* state at this path, as a whole (a list or an object), read in the same run or made by an
|
|
43
|
+
* earlier step of it (`records: mail` takes a `resend.domain`'s `dns_records`). A data file may
|
|
44
|
+
* give a literal value here instead. `desired` holds the resource's `name` until then, which the
|
|
45
|
+
* schema does not judge. Where the same apply changes that resource at the path (creates or
|
|
46
|
+
* replaces it, or updates it with the path `unknown` or another value there in its plan), or,
|
|
47
|
+
* for a create, it has no state yet or nothing at the path, a plan is given `null` there (known
|
|
48
|
+
* after apply: the type's `plan` must accept it and report the path in `unknown`), so a change it
|
|
49
|
+
* makes is planned, and approved, up front. A plan of a bound object leaves the field out while
|
|
50
|
+
* its resource is absent, gone or unreadable and not about to change, as drift does, and its step
|
|
51
|
+
* runs without it. A step given `null` for the field whose value is still not known (its
|
|
52
|
+
* resource's step failed or did not run) fails before its driver is called, and drift leaves the
|
|
53
|
+
* field out. An empty list or object at the path counts as known: a vendor that answers `[]`
|
|
54
|
+
* while it is still filling the list makes the dependent write an empty one. The path must be a
|
|
55
|
+
* field of each type it may name (`checkFromRefs`, when a config is resolved). Not on a
|
|
56
|
+
* write-only field, nor one holding a write-only field (refused). Not yet on a bridge-backed type
|
|
57
|
+
* (`tfConnector`): an OpenTofu provider's plan reads `null` as unset, not as known after apply.
|
|
58
|
+
*/
|
|
59
|
+
readonly from?: string;
|
|
40
60
|
}
|
|
41
61
|
|
|
42
62
|
/** A resource type's `fields.references`: by dotted path, the resource each names. */
|
|
@@ -196,9 +216,10 @@ export interface Declared<Id extends string = string, T = State> {
|
|
|
196
216
|
|
|
197
217
|
/**
|
|
198
218
|
* What a resource type's constructor takes: its fields, where `R` (its `fields.references`)
|
|
199
|
-
* names a path, a declared resource of that type instead of a value (`repository_id: site`),
|
|
200
|
-
*
|
|
201
|
-
*
|
|
219
|
+
* names a path, a declared resource of that type instead of a value (`repository_id: site`), in
|
|
220
|
+
* place of the whole field for a `from` reference (`records: mail`) and of each item for a list
|
|
221
|
+
* otherwise, and where a string goes, a secret (`secret("NAME")`), which the constructor allows
|
|
222
|
+
* at a write-only field only.
|
|
202
223
|
*/
|
|
203
224
|
export type Declaring<T, R extends References = {}> = [keyof R] extends [never]
|
|
204
225
|
? WithSecrets<T>
|
|
@@ -219,9 +240,11 @@ type DeclaringAt<T, R extends References, P extends string> = T extends readonly
|
|
|
219
240
|
: WithSecrets<T>;
|
|
220
241
|
|
|
221
242
|
type DeclaringField<V, R extends References, P extends string> = P extends keyof R
|
|
222
|
-
?
|
|
223
|
-
?
|
|
224
|
-
:
|
|
243
|
+
? R[P] extends { readonly from: string }
|
|
244
|
+
? WithSecrets<V> | DeclaredOf<R[P]["type"]>
|
|
245
|
+
: V extends readonly (infer I)[]
|
|
246
|
+
? (WithSecrets<I> | DeclaredOf<R[P]["type"]>)[]
|
|
247
|
+
: WithSecrets<V> | DeclaredOf<R[P]["type"]>
|
|
225
248
|
: [Extract<keyof R, `${P}.${string}`>] extends [never]
|
|
226
249
|
? WithSecrets<V>
|
|
227
250
|
: DeclaringAt<V, R, P>;
|
|
@@ -238,7 +261,7 @@ const record = z.record(z.string(), z.unknown());
|
|
|
238
261
|
export const PLAN_ACTIONS = ["none", "create", "update", "replace", "delete"] as const;
|
|
239
262
|
export type PlanAction = (typeof PLAN_ACTIONS)[number];
|
|
240
263
|
|
|
241
|
-
/** The operations of a writable type that change its objects:
|
|
264
|
+
/** The operations of a writable type that change its objects (`isChangeOp`): apply's for a declared object, any workflow's for another. */
|
|
242
265
|
export const CHANGE_OPS = ["create", "update", "delete"] as const;
|
|
243
266
|
|
|
244
267
|
const opsOf = <S extends z.ZodObject>(schema: S) => {
|
|
@@ -293,7 +316,8 @@ export type ResourceOps<S extends z.ZodObject, W extends boolean = false> = Spec
|
|
|
293
316
|
|
|
294
317
|
/**
|
|
295
318
|
* True for an operation that changes a writable type's objects (`CHANGE_OPS`), which only the
|
|
296
|
-
* apply workflow may call
|
|
319
|
+
* apply workflow may call on a declared object (a workflow's call is refused one, `refuseDeclared`
|
|
320
|
+
* in `call.ts`): by its vendor, resource and name, so a copy of a connector is told too.
|
|
297
321
|
*/
|
|
298
322
|
export function isChangeOp(
|
|
299
323
|
op: { readonly vendor: string; readonly resource: string; readonly name: string },
|
|
@@ -476,6 +500,65 @@ export function holderOf(value: unknown, path: string): { holder: State; key: st
|
|
|
476
500
|
return isObject(holder) || Array.isArray(holder) ? { holder: holder as State, key } : undefined;
|
|
477
501
|
}
|
|
478
502
|
|
|
503
|
+
/**
|
|
504
|
+
* Each place `refs` (a declaration's, by dotted path to the resource named there) holds a `from`
|
|
505
|
+
* reference (`FieldReference.from`): its path, the resource it names (`named`), and the path in
|
|
506
|
+
* that resource's state its value comes from.
|
|
507
|
+
*/
|
|
508
|
+
const fromRefs = (references: References | undefined, refs: Readonly<Record<string, string>>) =>
|
|
509
|
+
Object.entries(refs).flatMap(([at, named]) => {
|
|
510
|
+
const from = references?.[fieldPathOf(at)]?.from;
|
|
511
|
+
return from === undefined ? [] : [{ at, named, from }];
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
/** What `withFrom` makes of a `from` field it cannot fill: `null` (known after apply), or nothing. */
|
|
515
|
+
export type Missing = "unknown" | "drop";
|
|
516
|
+
|
|
517
|
+
/** A `from` reference `withFrom` could not fill: the resource it names has no state, or nothing at its path. */
|
|
518
|
+
export interface Unfilled {
|
|
519
|
+
at: string;
|
|
520
|
+
named: string;
|
|
521
|
+
from: string;
|
|
522
|
+
/** True when the resource has a state, with nothing at the path. */
|
|
523
|
+
stated: boolean;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* `desired` with each field a `from` reference names given the state of the resource it names
|
|
528
|
+
* at that path (a copy), `stateOf` that resource (by what `refs` holds there, and the path read);
|
|
529
|
+
* and the references it could not fill, where that resource has no state or nothing there, each
|
|
530
|
+
* such field `null` (`unknown`, a value known after apply) or left out (`drop`), as `missing` says
|
|
531
|
+
* (for each reference, given a function). A reference with no place in `desired` to fill is
|
|
532
|
+
* unfilled too. `desired` itself when there is no such field.
|
|
533
|
+
*/
|
|
534
|
+
export function withFrom(
|
|
535
|
+
references: References | undefined,
|
|
536
|
+
desired: State,
|
|
537
|
+
refs: Readonly<Record<string, string>>,
|
|
538
|
+
stateOf: (named: string, from: string) => State | undefined,
|
|
539
|
+
missing: Missing | ((named: string, from: string) => Missing),
|
|
540
|
+
): { desired: State; unfilled: Unfilled[] } {
|
|
541
|
+
const taken = fromRefs(references, refs);
|
|
542
|
+
if (!taken.length) return { desired, unfilled: [] };
|
|
543
|
+
const out = structuredClone(desired);
|
|
544
|
+
const unfilled: Unfilled[] = [];
|
|
545
|
+
for (const { at, named, from } of taken) {
|
|
546
|
+
const place = holderOf(out, at);
|
|
547
|
+
const state = stateOf(named, from);
|
|
548
|
+
const value = state === undefined ? undefined : valueAt(state, from);
|
|
549
|
+
if (place && value != null) {
|
|
550
|
+
// A copy: what a driver is given never shares an object with the resource's state.
|
|
551
|
+
place.holder[place.key] = structuredClone(value);
|
|
552
|
+
continue;
|
|
553
|
+
}
|
|
554
|
+
unfilled.push({ at, named, from, stated: state !== undefined });
|
|
555
|
+
if (!place) continue;
|
|
556
|
+
if ((typeof missing === "function" ? missing(named, from) : missing) === "drop") delete place.holder[place.key];
|
|
557
|
+
else place.holder[place.key] = null;
|
|
558
|
+
}
|
|
559
|
+
return { desired: out, unfilled };
|
|
560
|
+
}
|
|
561
|
+
|
|
479
562
|
/** A field's dotted path as `fields` names it, without a list item's index: `teams.0` is `teams`. */
|
|
480
563
|
export const fieldPathOf = (at: string) => at.replaceAll(/\.\d+(?=\.|$)/g, "");
|
|
481
564
|
|
|
@@ -484,7 +567,7 @@ export const valueAt = (value: unknown, path: string) =>
|
|
|
484
567
|
path.split(".").reduce<unknown>((v, key) => (isObject(v) || Array.isArray(v) ? (v as State)[key] : undefined), value);
|
|
485
568
|
|
|
486
569
|
/** True when the two are the same JSON, where a missing value is `null`. */
|
|
487
|
-
function same(a: unknown, b: unknown): boolean {
|
|
570
|
+
export function same(a: unknown, b: unknown): boolean {
|
|
488
571
|
if (a === b || (a == null && b == null)) return true;
|
|
489
572
|
if (Array.isArray(a) && Array.isArray(b)) return a.length === b.length && a.every((item, i) => same(item, b[i]));
|
|
490
573
|
if (isObject(a) && isObject(b)) return Object.keys({ ...a, ...b }).every((key) => same(a[key], b[key]));
|
|
@@ -565,8 +648,9 @@ function pathsIn(value: unknown, paths: ReadonlySet<string>, prefix = ""): strin
|
|
|
565
648
|
* returns the same: `{ gone: true }` when the object no longer exists, else its fresh
|
|
566
649
|
* `state`. A driver may import first when it is given no state.
|
|
567
650
|
*
|
|
568
|
-
* A `writable` type has the change operations too, which only the apply workflow may call
|
|
569
|
-
* (`isChangeOp`): `create`, `update` and `delete`
|
|
651
|
+
* A `writable` type has the change operations too, which only the apply workflow may call on a
|
|
652
|
+
* declared object, and any workflow on another (`isChangeOp`): `create`, `update` and `delete`
|
|
653
|
+
* effect `write` (`delete` idempotent), and `plan`:
|
|
570
654
|
*
|
|
571
655
|
* - `plan` (`read`, idempotent) takes `{ id?, state?, handle?, desired? }`, no `desired` for a
|
|
572
656
|
* delete and no `id` for a create, and returns `{ action, planned?, unknown, replace }`:
|
|
@@ -577,6 +661,12 @@ function pathsIn(value: unknown, paths: ReadonlySet<string>, prefix = ""): strin
|
|
|
577
661
|
* - `update` takes `{ id, state?, handle?, desired, expect? }` and returns the same; given
|
|
578
662
|
* `expect` (a plan's `action` and `replace`), it refuses a fresh plan that differs
|
|
579
663
|
* (`DriverError`, `vendorCode` `plan_changed`).
|
|
664
|
+
*
|
|
665
|
+
* A type whose fields take a `from` reference (`FieldReference.from`) is given `null` there by
|
|
666
|
+
* apply's `plan` while the value is known only after apply, so its `plan` must accept `null` and
|
|
667
|
+
* report the path in `unknown`; and its `update` must treat a field left out of `desired` as
|
|
668
|
+
* keeping what the vendor holds, since a bound object whose `from` resource is gone is updated
|
|
669
|
+
* with that field left out.
|
|
580
670
|
* - `delete` takes `{ id, state?, handle? }` and returns `{ id, gone: true }`.
|
|
581
671
|
*
|
|
582
672
|
* `desired` may hold secret markers at write-only fields, which the driver resolves; no output
|
|
@@ -589,8 +679,10 @@ function pathsIn(value: unknown, paths: ReadonlySet<string>, prefix = ""): strin
|
|
|
589
679
|
* schema does not have, fields the vendor owns (unless the identity names them), values the
|
|
590
680
|
* schema rejects, and an empty identity. Where `fields.references` allows it, a field may name
|
|
591
681
|
* another declared resource of that type (`repository_id: site`): `desired` holds, and the
|
|
592
|
-
* schema and `find` read, that resource's `name`, and `refs` says where it was named. A
|
|
593
|
-
*
|
|
682
|
+
* schema and `find` read, that resource's `name`, and `refs` says where it was named. A `from`
|
|
683
|
+
* reference names one in place of the whole field, which apply and drift fill from its state, so
|
|
684
|
+
* the schema does not judge the name there. A type without `find` declares resources without a
|
|
685
|
+
* `name`, found only by their bindings.
|
|
594
686
|
*/
|
|
595
687
|
export function defineResource<
|
|
596
688
|
S extends z.ZodObject,
|
|
@@ -670,6 +762,20 @@ export function defineResource<
|
|
|
670
762
|
const owned = new Set(fields.vendorOwned.filter((path) => !named.has(path)));
|
|
671
763
|
const references: References = fields.references ?? {};
|
|
672
764
|
const writeOnly = new Set(fields.writeOnly);
|
|
765
|
+
for (const [path, ref] of Object.entries(references)) {
|
|
766
|
+
if (ref.from !== undefined && ref.by !== undefined)
|
|
767
|
+
throw new Error(
|
|
768
|
+
`${what}: the reference at ${path} takes its value from its resource's state or names it by a field, not both`,
|
|
769
|
+
);
|
|
770
|
+
// A write-only field holds secret markers only, never a value from another resource's state.
|
|
771
|
+
if (ref.from !== undefined && underAny(path, writeOnly))
|
|
772
|
+
throw new Error(`${what}: ${path} is write-only, so it cannot take its value from another resource's state`);
|
|
773
|
+
const inside = ref.from === undefined ? undefined : fields.writeOnly.find((w) => w.startsWith(`${path}.`));
|
|
774
|
+
if (inside !== undefined)
|
|
775
|
+
throw new Error(
|
|
776
|
+
`${what}: ${inside} is write-only, so ${path}, which holds it, cannot take its value from another resource's state`,
|
|
777
|
+
);
|
|
778
|
+
}
|
|
673
779
|
const declare = (
|
|
674
780
|
given: Declaring<z.input<S>, R>,
|
|
675
781
|
options?: { connection?: ConnectionRef },
|
|
@@ -682,8 +788,9 @@ export function defineResource<
|
|
|
682
788
|
throw new Error(`${what}: select a declared connection of this connector`);
|
|
683
789
|
}
|
|
684
790
|
const refs: Record<string, string> = {};
|
|
685
|
-
// Where a secret is given, with list items' indexes:
|
|
686
|
-
|
|
791
|
+
// Where a secret, or a resource a `from` reference takes, is given, with list items' indexes:
|
|
792
|
+
// the schema does not judge the marker, nor the name that holds the place of the state.
|
|
793
|
+
const unjudged = new Set<string>();
|
|
687
794
|
// Each resource `given` names, where `references` allows one of its type, replaced by its name.
|
|
688
795
|
// `path` is the field's, as `references` names it; `at` has list items' indexes too.
|
|
689
796
|
const resolve = (value: unknown, path: string, at: string): unknown => {
|
|
@@ -691,10 +798,11 @@ export function defineResource<
|
|
|
691
798
|
if (!underAny(path, writeOnly)) {
|
|
692
799
|
throw new Error(`${what}: ${at} is not write-only, so it takes a value, not a secret (${value.$secret})`);
|
|
693
800
|
}
|
|
694
|
-
|
|
801
|
+
const hostSecrets = host?.inputs.secrets;
|
|
802
|
+
if (isSanomaSecret(value.$secret) && !(hostSecrets !== undefined && underAny(path, new Set([hostSecrets])))) {
|
|
695
803
|
throw new Error(`${what}: ${at} names ${value.$secret}, one of sanoma's own: only a host's secrets may`);
|
|
696
804
|
}
|
|
697
|
-
|
|
805
|
+
unjudged.add(at);
|
|
698
806
|
return value;
|
|
699
807
|
}
|
|
700
808
|
if (isDeclared(value)) {
|
|
@@ -706,10 +814,16 @@ export function defineResource<
|
|
|
706
814
|
throw new Error(`${what}: ${at} names a resource of type ${types.join(" or ")}, not ${id} (${value.name})`);
|
|
707
815
|
if (value.name === undefined)
|
|
708
816
|
throw new Error(`${what}: ${at} names a ${id}, which has no identity to name it by`);
|
|
817
|
+
if (wants.from !== undefined) unjudged.add(at);
|
|
709
818
|
refs[at] = `${id}:${value.name}`;
|
|
710
819
|
return value.name;
|
|
711
820
|
}
|
|
712
|
-
if (Array.isArray(value))
|
|
821
|
+
if (Array.isArray(value)) {
|
|
822
|
+
const from = references[path]?.from;
|
|
823
|
+
const listed = from === undefined ? undefined : value.find(isDeclared);
|
|
824
|
+
if (listed) throw new Error(`${what}: ${path} takes a ${listed[DECLARED]}'s ${from} as a whole, not in a list`);
|
|
825
|
+
return value.map((item, i) => resolve(item, path, `${at}.${i}`));
|
|
826
|
+
}
|
|
713
827
|
if (!isObject(value)) return value;
|
|
714
828
|
return Object.fromEntries(
|
|
715
829
|
Object.entries(value).map(([field, v]) => [field, resolve(v, joinPath(path, field), joinPath(at, field))]),
|
|
@@ -729,7 +843,7 @@ export function defineResource<
|
|
|
729
843
|
);
|
|
730
844
|
}
|
|
731
845
|
const parsed = schema.safeParse(desired);
|
|
732
|
-
const issues = parsed.error?.issues.filter((issue) => !
|
|
846
|
+
const issues = parsed.error?.issues.filter((issue) => !unjudged.has(issue.path.join(".")));
|
|
733
847
|
if (issues?.length) throw new Error(`${what}: ${z.prettifyError(new z.ZodError(issues))}`);
|
|
734
848
|
const name = find?.(desired);
|
|
735
849
|
if (find && (typeof name !== "string" || name === "")) throw new Error(`${what}: its ${identity} is missing`);
|
|
@@ -767,6 +881,27 @@ export function defineResource<
|
|
|
767
881
|
return Object.freeze(resource);
|
|
768
882
|
}
|
|
769
883
|
|
|
884
|
+
/**
|
|
885
|
+
* Throws where a `from` reference (`FieldReference.from`) names a path that a type it may name
|
|
886
|
+
* has no field at, among `types` (a config's): `dns_record` for `dns_records` would otherwise
|
|
887
|
+
* fail every create. A type not among them is left to the data files' check.
|
|
888
|
+
*/
|
|
889
|
+
export function checkFromRefs(types: ReadonlyMap<string, Resource>): void {
|
|
890
|
+
for (const type of types.values()) {
|
|
891
|
+
for (const [path, ref] of Object.entries(type.fields.references ?? {})) {
|
|
892
|
+
const named = ref.from === undefined ? [] : [ref.type].flat();
|
|
893
|
+
for (const held of named.map((id) => types.get(id)).filter((t) => t !== undefined)) {
|
|
894
|
+
if (!hasPath(held.schema, ref.from!)) {
|
|
895
|
+
const shape = (held.schema as Partial<z.ZodObject>).shape ?? {};
|
|
896
|
+
throw new Error(
|
|
897
|
+
`${type.vendor}.${type.type}: the reference at ${path} takes ${ref.from} from a ${held.vendor}.${held.type}'s state, which has no such field (its fields: ${Object.keys(shape).join(", ")})`,
|
|
898
|
+
);
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
|
|
770
905
|
/** Every resource type the connectors declare, by `<vendor>.<type>`. */
|
|
771
906
|
export function resourceTypesOf(connectors: readonly Connector<any, any>[]): Map<string, Resource> {
|
|
772
907
|
const types = new Map<string, Resource>();
|
package/src/resources.ts
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
ANONYMOUS_PRINCIPAL,
|
|
18
18
|
type At,
|
|
19
19
|
declaresPeople,
|
|
20
|
+
isToken,
|
|
20
21
|
isVisitor,
|
|
21
22
|
type PeopleResource,
|
|
22
23
|
personOf,
|
|
@@ -138,8 +139,10 @@ export function readPeople(root: string | undefined): DeclaredPeople {
|
|
|
138
139
|
* (`signIn`), of the person it matches (`personOf`: email, GitHub login, then id), never an agent.
|
|
139
140
|
* While no person is declared, whoever names an id is taken as given. A visitor's id (`isVisitor`)
|
|
140
141
|
* is taken whatever the files say, with the groups stamped here (`visitors`, or none for
|
|
141
|
-
* `anonymous`), never through a sign-in: the staff logins admit no visitor.
|
|
142
|
-
* `
|
|
142
|
+
* `anonymous`), never through a sign-in: the staff logins admit no visitor. A visitor keeps the
|
|
143
|
+
* `email` and `github` its login verified; a `subject` is never taken from the caller (a token's
|
|
144
|
+
* is its row's, which `SanomaClient.start` reads). Throws `NotAdmittedError` for anyone else, and
|
|
145
|
+
* for everyone while the people's files have problems.
|
|
143
146
|
*/
|
|
144
147
|
export function admit(
|
|
145
148
|
asking: { id?: string; email?: string; github?: string; groups?: string[]; onBehalfOf?: string },
|
|
@@ -149,7 +152,11 @@ export function admit(
|
|
|
149
152
|
const who = asking.github ?? asking.id ?? asking.email ?? "This sign-in";
|
|
150
153
|
if (asking.id !== undefined && isVisitor({ id: asking.id })) {
|
|
151
154
|
if (signIn) throw new NotAdmittedError(who);
|
|
152
|
-
|
|
155
|
+
if (asking.id === ANONYMOUS) return ANONYMOUS_PRINCIPAL;
|
|
156
|
+
// A token's groups are its row's (`SanomaClient.tokens`), which only the public front reads; a visitor's are stamped.
|
|
157
|
+
if (isToken({ id: asking.id })) return { id: asking.id, groups: [...(asking.groups ?? [])] };
|
|
158
|
+
const { email, github } = asking;
|
|
159
|
+
return { id: asking.id, groups: [VISITORS], ...(email && { email }), ...(github && { github }) };
|
|
153
160
|
}
|
|
154
161
|
if (problems.length) {
|
|
155
162
|
// The runtime's own agents are declared by no file, so a broken one does not stop them (`check`'s scenarios).
|
|
@@ -160,7 +167,10 @@ export function admit(
|
|
|
160
167
|
}
|
|
161
168
|
if (!declaresPeople(people)) {
|
|
162
169
|
const given = Principal.safeParse(asking).data;
|
|
163
|
-
if (given)
|
|
170
|
+
if (given) {
|
|
171
|
+
const { email: _email, github: _github, subject: _subject, ...principal } = given;
|
|
172
|
+
return principal;
|
|
173
|
+
}
|
|
164
174
|
} else {
|
|
165
175
|
const id = signIn ? personOf(asking, people) : asking.id;
|
|
166
176
|
const admitted = id === undefined ? undefined : principalOf(id, people);
|
package/src/run.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ConnectionRef, ConnectionRegistry, ConnectionSnapshot } from "./connection.ts";
|
|
2
2
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
3
3
|
import type { Collection } from "./collection.ts";
|
|
4
|
+
import type { DeclaredObjects } from "./config.ts";
|
|
4
5
|
import type { DeadlineRegistry } from "./deadlines.ts";
|
|
5
6
|
import type { ApprovalState, Approver, Principal, WorkflowDefinition } from "./define.ts";
|
|
6
7
|
import type { Fake } from "./fake.ts";
|
|
@@ -53,6 +54,8 @@ export interface WorkerState {
|
|
|
53
54
|
/** The worker's `source`, when it was given one. */
|
|
54
55
|
source?: Source;
|
|
55
56
|
types: ReadonlyMap<string, Resource>;
|
|
57
|
+
/** The objects the data files declare, read at each call: a workflow's change operation may not aim at one (`refuseDeclared`). */
|
|
58
|
+
declaredObjects(): Promise<DeclaredObjects>;
|
|
56
59
|
/** The collections, by name, which scenarios' steps may name (`data.<name>`). */
|
|
57
60
|
collections: ReadonlyMap<string, Collection>;
|
|
58
61
|
steps: readonly Steps[];
|