@sanoma-ai/workflows 0.11.5 → 0.11.9
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 +16 -7
- package/dist/change.d.ts +2 -4
- package/dist/change.js +2 -3
- package/dist/change.js.map +1 -1
- package/dist/client.d.ts +6 -0
- package/dist/client.js +5 -1
- package/dist/client.js.map +1 -1
- package/dist/config.js +8 -1
- package/dist/config.js.map +1 -1
- package/dist/describe.js +1 -0
- package/dist/describe.js.map +1 -1
- package/dist/description.d.ts +12 -0
- package/dist/description.js +3 -0
- package/dist/description.js.map +1 -1
- package/dist/direct.d.ts +1 -1
- package/dist/direct.js +2 -1
- package/dist/direct.js.map +1 -1
- package/dist/host-derive.d.ts +3 -0
- package/dist/host-derive.js +9 -0
- package/dist/host-derive.js.map +1 -1
- package/dist/host.d.ts +2 -0
- package/dist/host.js +2 -1
- package/dist/host.js.map +1 -1
- package/dist/ledger.d.ts +6 -0
- package/dist/ledger.js +8 -0
- package/dist/ledger.js.map +1 -1
- package/dist/run-status.d.ts +10 -0
- package/dist/run-status.js +23 -0
- package/dist/run-status.js.map +1 -0
- package/dist/runtime-ops.d.ts +29 -2
- package/dist/runtime-ops.js +35 -8
- package/dist/runtime-ops.js.map +1 -1
- package/dist/secret-store.d.ts +4 -0
- package/dist/secret-store.js +8 -0
- package/dist/secret-store.js.map +1 -1
- package/dist/shared.d.ts +2 -0
- package/dist/shared.js +2 -0
- package/dist/shared.js.map +1 -1
- package/dist/source.d.ts +2 -0
- package/dist/source.js.map +1 -1
- package/package.json +1 -1
- package/src/change.ts +10 -3
- package/src/client.ts +14 -1
- package/src/config.ts +13 -1
- package/src/describe.ts +1 -0
- package/src/description.ts +3 -0
- package/src/direct.ts +2 -1
- package/src/host-derive.ts +11 -0
- package/src/host.ts +2 -1
- package/src/ledger.ts +12 -0
- package/src/run-status.ts +38 -0
- package/src/runtime-ops.ts +43 -7
- package/src/secret-store.ts +16 -3
- package/src/shared.ts +3 -0
- package/src/source.ts +2 -0
package/src/config.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { configuredSecrets } from "./credentials.ts";
|
|
3
|
+
import { inspectRun } from "./run-status.ts";
|
|
3
4
|
import {
|
|
4
5
|
configuredIdentityProvisioning,
|
|
5
6
|
type IdentityProvisioning,
|
|
@@ -378,7 +379,11 @@ export function resolveConfig(config: SanomaConfig): ResolvedConfig {
|
|
|
378
379
|
changeDriver(remotes),
|
|
379
380
|
connectionDriver(connections),
|
|
380
381
|
appsListDriver,
|
|
381
|
-
runtimeDriver({
|
|
382
|
+
runtimeDriver({
|
|
383
|
+
tokens: tokenStore(() => connectionPool.db()),
|
|
384
|
+
identities: identityProvisioning.store,
|
|
385
|
+
inspect: inspectRun(() => connectionPool.db(), appName),
|
|
386
|
+
}),
|
|
382
387
|
...(hostTyped ? [sourceDriver] : []),
|
|
383
388
|
...(dataStore ? [collectionsDriver(declared, dataStore)] : []),
|
|
384
389
|
],
|
|
@@ -412,6 +417,11 @@ export function resolveConfig(config: SanomaConfig): ResolvedConfig {
|
|
|
412
417
|
drivers: driversByVendor,
|
|
413
418
|
builtInStore: !config.secrets?.store,
|
|
414
419
|
signingKeyBound: config.secrets?.bindings?.SANOMA_SIGNING_KEY !== undefined,
|
|
420
|
+
hostEnvironment: Object.fromEntries(
|
|
421
|
+
Object.entries(config.secrets?.bindings ?? {}).flatMap(([name, binding]) =>
|
|
422
|
+
binding.host && binding.env !== undefined ? [[binding.env, name]] : [],
|
|
423
|
+
),
|
|
424
|
+
),
|
|
415
425
|
...(isOp(config.login?.sender) && { sender: config.login.sender.vendor }),
|
|
416
426
|
});
|
|
417
427
|
if (types.size) {
|
|
@@ -739,6 +749,7 @@ async function readDeclared(
|
|
|
739
749
|
drivers: ReadonlyMap<string, Driver>;
|
|
740
750
|
builtInStore: boolean;
|
|
741
751
|
signingKeyBound: boolean;
|
|
752
|
+
hostEnvironment: Readonly<Record<string, string>>;
|
|
742
753
|
sender?: string;
|
|
743
754
|
},
|
|
744
755
|
): Promise<Omit<ApplyDeclared, "rebinds">> {
|
|
@@ -760,6 +771,7 @@ async function readDeclared(
|
|
|
760
771
|
...(run && { secrets: run.state.secrets }),
|
|
761
772
|
builtInStore: options.builtInStore,
|
|
762
773
|
signingKeyBound: options.signingKeyBound,
|
|
774
|
+
hostEnvironment: options.hostEnvironment,
|
|
763
775
|
...(options.sender !== undefined && { sender: options.sender }),
|
|
764
776
|
mint: !!options.mint && !!run && !run.sandbox,
|
|
765
777
|
});
|
package/src/describe.ts
CHANGED
|
@@ -189,6 +189,7 @@ export function describeConfig(config: SanomaConfig): ConfigDescription {
|
|
|
189
189
|
.map((name) => `${id}.${name}`)
|
|
190
190
|
.toSorted(),
|
|
191
191
|
...(capabilities.length ? { capabilities } : {}),
|
|
192
|
+
...(spec.host && { host: { lifetime: spec.host.capabilities.lifetime } }),
|
|
192
193
|
};
|
|
193
194
|
})
|
|
194
195
|
.toSorted((a, b) => a.id.localeCompare(b.id));
|
package/src/description.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// types inferred from them. zod only, no Node: a page can parse a description it is sent, and the
|
|
3
3
|
// app extends `ConfigDescriptionSchema` with what it adds (`AppConfigSchema`).
|
|
4
4
|
import { z } from "zod";
|
|
5
|
+
import { LIFETIMES } from "./host.ts";
|
|
5
6
|
import type { Outline } from "./outline.ts";
|
|
6
7
|
import type { ResourceFields } from "./resource.ts";
|
|
7
8
|
import { EFFECTS, FIELD_TYPES } from "./shared.ts";
|
|
@@ -322,6 +323,8 @@ export const ResourceTypeEntrySchema = z.strictObject({
|
|
|
322
323
|
ops: z.array(z.string()),
|
|
323
324
|
/** The contracts it meets (`CAPABILITIES`), in that order; absent when it meets none. */
|
|
324
325
|
capabilities: z.array(z.enum(CAPABILITIES)).optional(),
|
|
326
|
+
/** For a host (`HostSpec`), how it keeps the runtime up (`HostLifetime`); absent for any other type. */
|
|
327
|
+
host: z.strictObject({ lifetime: z.enum(LIFETIMES) }).optional(),
|
|
325
328
|
});
|
|
326
329
|
export type ResourceTypeEntry = z.infer<typeof ResourceTypeEntrySchema>;
|
|
327
330
|
|
package/src/direct.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { APPS_LIST, DATA_VENDOR } from "./shared.ts";
|
|
|
5
5
|
/**
|
|
6
6
|
* Whether an operation may be called directly, outside a run (`SanomaClient.call`), as the
|
|
7
7
|
* person: a collection's (`data.<name>.<op>`), the runtime's listing of apps (`sanoma.apps.list`,
|
|
8
|
-
* which a page calls), or a `read` that is neither the runtime's own (the vendor `sanoma`) nor a
|
|
8
|
+
* which a page calls), run inspection, connection management, or a `read` that is neither the runtime's own (the vendor `sanoma`) nor a
|
|
9
9
|
* resource type's (`isResourceOp`), which only drift and apply call.
|
|
10
10
|
* Anything else (an effect at a vendor) is a workflow's. The one rule: the built-in `call`
|
|
11
11
|
* workflow, the SDK and the app read it, from here or from `OpEntry.direct`, and never derive it.
|
|
@@ -16,6 +16,7 @@ export const isDirect = (
|
|
|
16
16
|
): boolean =>
|
|
17
17
|
op.vendor === DATA_VENDOR ||
|
|
18
18
|
op.id === APPS_LIST ||
|
|
19
|
+
op.id === "sanoma.run.inspect" ||
|
|
19
20
|
op.id === "sanoma.connection.disconnect" ||
|
|
20
21
|
op.id === "sanoma.connection.bind" ||
|
|
21
22
|
op.id === "sanoma.connection.create" ||
|
package/src/host-derive.ts
CHANGED
|
@@ -78,6 +78,8 @@ export interface HostFacts {
|
|
|
78
78
|
unbound?: ReadonlyMap<string, readonly string[]>;
|
|
79
79
|
/** True when the config binds `SANOMA_SIGNING_KEY`: the host is given that key, else its own (`HOST_SIGNING_KEY`). */
|
|
80
80
|
signingKeyBound?: boolean;
|
|
81
|
+
/** Explicit environment delivery: destination variable to logical secret name, never values. */
|
|
82
|
+
hostEnvironment?: Readonly<Record<string, string>>;
|
|
81
83
|
/**
|
|
82
84
|
* While the config has no deploy-key type, the GitHub repositories a host may clone (`origin`'s,
|
|
83
85
|
* a declared `source`'s) that GitHub shows to no one: private (or not there). One GitHub does
|
|
@@ -352,10 +354,17 @@ export function deriveHosts(
|
|
|
352
354
|
]);
|
|
353
355
|
const carried = [...vendors].flatMap((vendor) => facts.carried.get(vendor) ?? []);
|
|
354
356
|
for (const name of [...carried, ...facts.githubApp]) secrets[name] = secret(name);
|
|
357
|
+
for (const [environment, name] of Object.entries(facts.hostEnvironment ?? {})) {
|
|
358
|
+
if (Object.hasOwn(secrets, environment) && secrets[environment]!.$secret !== name)
|
|
359
|
+
problem(`${environment} has a derived host binding and cannot be replaced by ${name}`);
|
|
360
|
+
else if (!Object.hasOwn(secrets, environment))
|
|
361
|
+
Object.defineProperty(secrets, environment, { value: secret(name), enumerable: true });
|
|
362
|
+
}
|
|
355
363
|
// One the driver reads here from the environment alone would be left off the host, and a
|
|
356
364
|
// Worker updated without it would lose it.
|
|
357
365
|
for (const vendor of vendors) {
|
|
358
366
|
for (const name of facts.unbound?.get(vendor) ?? []) {
|
|
367
|
+
if (Object.hasOwn(facts.hostEnvironment ?? {}, name)) continue;
|
|
359
368
|
problem(
|
|
360
369
|
`${name} is read from the environment here, and has no binding, so the host would not be given it: bind it in sanoma.config.ts, \`secrets: { bindings: { ${name}: { env: "${name}" } } }\`, or store it on the ${vendor} connector page`,
|
|
361
370
|
);
|
|
@@ -428,6 +437,7 @@ export async function hostFacts(options: {
|
|
|
428
437
|
builtInStore: boolean;
|
|
429
438
|
/** True when the config binds `SANOMA_SIGNING_KEY` (`{ env }` or `{ bootstrap }`). */
|
|
430
439
|
signingKeyBound?: boolean;
|
|
440
|
+
hostEnvironment?: Readonly<Record<string, string>>;
|
|
431
441
|
sender?: string;
|
|
432
442
|
mint: boolean;
|
|
433
443
|
}): Promise<HostFacts | undefined> {
|
|
@@ -484,6 +494,7 @@ export async function hostFacts(options: {
|
|
|
484
494
|
carried,
|
|
485
495
|
...(unbound.size > 0 && { unbound }),
|
|
486
496
|
...(options.signingKeyBound && { signingKeyBound: true }),
|
|
497
|
+
...(options.hostEnvironment && { hostEnvironment: options.hostEnvironment }),
|
|
487
498
|
...(options.sender !== undefined && { sender: options.sender }),
|
|
488
499
|
githubApp,
|
|
489
500
|
...(sourceKey !== undefined && { sourceKey }),
|
package/src/host.ts
CHANGED
|
@@ -130,7 +130,8 @@ export const UNNAMED = "(the repository's name)";
|
|
|
130
130
|
/** The port the runtime image's app listens on (its `PORT`): a host that derives runs it there. */
|
|
131
131
|
export const RUNTIME_PORT = 3000;
|
|
132
132
|
|
|
133
|
-
|
|
133
|
+
/** Every `HostLifetime`, for a schema of one. */
|
|
134
|
+
export const LIFETIMES = ["long-lived", "stops-when-quiet"] as const satisfies readonly HostLifetime[];
|
|
134
135
|
|
|
135
136
|
/** True when `schema` has a field at the dotted `path`, looking through optional, nullable and default wrappers. */
|
|
136
137
|
export function hasPath(schema: z.ZodType, path: string): boolean {
|
package/src/ledger.ts
CHANGED
|
@@ -190,6 +190,18 @@ export async function lastSignIns(
|
|
|
190
190
|
return Object.fromEntries(last.flat());
|
|
191
191
|
}
|
|
192
192
|
|
|
193
|
+
/** Who minted the latest invite for `person`, and until when (ms since the epoch): from their `invite:<person>` records; undefined when none was. */
|
|
194
|
+
export async function lastInvite(
|
|
195
|
+
store: LedgerStore,
|
|
196
|
+
person: string,
|
|
197
|
+
): Promise<{ by: string; at: number; expiresAt: number } | undefined> {
|
|
198
|
+
const records = await store.read(`invite:${person}`);
|
|
199
|
+
const latest = records.reduce<LedgerRecord | undefined>((a, r) => (a && a.at > r.at ? a : r), undefined);
|
|
200
|
+
return latest?.type === "invite.minted"
|
|
201
|
+
? { by: latest.by, at: latest.at, expiresAt: Date.parse(latest.expiresAt) }
|
|
202
|
+
: undefined;
|
|
203
|
+
}
|
|
204
|
+
|
|
193
205
|
/**
|
|
194
206
|
* The `ctx.all` member a record was written in. `id` is `all:<seq>`, the run's next `seq` when
|
|
195
207
|
* the `ctx.all` began, so a replay names it the same; `index` is the member's position, `size`
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { WorkflowStatusString } from "@dbos-inc/dbos-sdk";
|
|
2
|
+
import type { Db } from "./db.ts";
|
|
3
|
+
|
|
4
|
+
/** Metadata only: never read a run's inputs, outputs, error or authentication data. */
|
|
5
|
+
export type RunObservation = {
|
|
6
|
+
runId: string;
|
|
7
|
+
status: "missing" | "active" | "finished" | "failed" | "cancelled" | "interrupted";
|
|
8
|
+
workflow?: string;
|
|
9
|
+
};
|
|
10
|
+
export type InspectRun = (runId: string) => Promise<RunObservation>;
|
|
11
|
+
|
|
12
|
+
const statuses: Record<WorkflowStatusString, RunObservation["status"]> = {
|
|
13
|
+
ENQUEUED: "active",
|
|
14
|
+
DELAYED: "active",
|
|
15
|
+
PENDING: "active",
|
|
16
|
+
SUCCESS: "finished",
|
|
17
|
+
ERROR: "failed",
|
|
18
|
+
MAX_RECOVERY_ATTEMPTS_EXCEEDED: "interrupted",
|
|
19
|
+
CANCELLED: "cancelled",
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/** A snapshot of the current application's run; unknown DBOS states remain active. */
|
|
23
|
+
export const inspectRun =
|
|
24
|
+
(database: () => Promise<Db>, appName: string): InspectRun =>
|
|
25
|
+
async (runId) => {
|
|
26
|
+
const db = await database();
|
|
27
|
+
const { rows } = await db.query<{ name: string; status: string }>(
|
|
28
|
+
"SELECT name,status FROM dbos.workflow_status WHERE workflow_uuid=$1 AND application_name=$2",
|
|
29
|
+
[runId, appName],
|
|
30
|
+
);
|
|
31
|
+
const row = rows[0];
|
|
32
|
+
if (!row) return { runId, status: "missing" };
|
|
33
|
+
return {
|
|
34
|
+
runId,
|
|
35
|
+
workflow: row.name,
|
|
36
|
+
status: Object.hasOwn(statuses, row.status) ? statuses[row.status as WorkflowStatusString] : "active",
|
|
37
|
+
};
|
|
38
|
+
};
|
package/src/runtime-ops.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { sealInvite } from "./seal.ts";
|
|
|
7
7
|
import { errorMessage } from "./shared.ts";
|
|
8
8
|
import { sourceKeyPair, sshPublicKey } from "./ssh-key.ts";
|
|
9
9
|
import { memoryTokens, type Tokens } from "./tokens.ts";
|
|
10
|
+
import type { InspectRun } from "./run-status.ts";
|
|
10
11
|
import {
|
|
11
12
|
identityProvisioningCode,
|
|
12
13
|
memoryIdentityProvisioning,
|
|
@@ -25,11 +26,17 @@ import {
|
|
|
25
26
|
// registers them in every config with their driver (`runtimeDriver`), so a policy may name them;
|
|
26
27
|
// a sandbox run calls `sandboxRuntimeDriver`, over its own secrets and tokens, posting nothing.
|
|
27
28
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
29
|
+
const StoredSecretName = z.string().regex(SECRET_NAME, "a secret's name: upper case, A-Z, 0-9 and _");
|
|
30
|
+
/** Generation destinations exclude the runtime's keys and credentials. */
|
|
31
|
+
const SecretName = StoredSecretName.refine(
|
|
32
|
+
(name) => !name.startsWith("SANOMA_"),
|
|
33
|
+
"a SANOMA_ name is the runtime's own, not a workflow's to use",
|
|
34
|
+
);
|
|
35
|
+
/** Database exports are stored values a lifecycle workflow can retire; bootstrap keys remain protected. */
|
|
36
|
+
const DeletableSecretName = StoredSecretName.refine(
|
|
37
|
+
(name) => !name.startsWith("SANOMA_") || /^SANOMA_DATABASE_URL_[A-Z0-9_]+$/.test(name),
|
|
38
|
+
"a SANOMA_ name may be deleted only when it is an exported database URL",
|
|
39
|
+
);
|
|
33
40
|
|
|
34
41
|
/** How long an instance gets to answer `POST /auth/invite`. */
|
|
35
42
|
const INVITE_TIMEOUT_MS = 10_000;
|
|
@@ -59,6 +66,21 @@ async function identityWrite(write: () => Promise<void>): Promise<void> {
|
|
|
59
66
|
* `uses: [sanoma.secret.generate]` and `ctx.sanoma.secret.generate({ name, kind })`.
|
|
60
67
|
*/
|
|
61
68
|
export const sanoma = defineConnector("sanoma", {
|
|
69
|
+
run: {
|
|
70
|
+
inspect: {
|
|
71
|
+
effect: "read",
|
|
72
|
+
idempotent: true,
|
|
73
|
+
description:
|
|
74
|
+
"Inspect a run's status in this application without its input, output or error. A snapshot, not a lock or proof that cancelled vendor calls have stopped",
|
|
75
|
+
input: z.object({ runId: z.string().min(1).max(512) }),
|
|
76
|
+
output: z.object({
|
|
77
|
+
runId: z.string(),
|
|
78
|
+
status: z.enum(["missing", "active", "finished", "failed", "cancelled", "interrupted"]),
|
|
79
|
+
workflow: z.string().optional(),
|
|
80
|
+
}),
|
|
81
|
+
target: ({ runId }: { runId: string }) => runId,
|
|
82
|
+
},
|
|
83
|
+
},
|
|
62
84
|
secretStore: {
|
|
63
85
|
prepare: {
|
|
64
86
|
effect: "access",
|
|
@@ -127,8 +149,9 @@ export const sanoma = defineConnector("sanoma", {
|
|
|
127
149
|
delete: {
|
|
128
150
|
effect: "write",
|
|
129
151
|
idempotent: true,
|
|
130
|
-
description:
|
|
131
|
-
|
|
152
|
+
description:
|
|
153
|
+
"Delete a stored secret or exported database URL; one not stored is nothing to do. Runtime bootstrap secrets remain protected",
|
|
154
|
+
input: z.object({ name: DeletableSecretName }),
|
|
132
155
|
output: z.object({}),
|
|
133
156
|
target: ({ name }: { name: string }) => name,
|
|
134
157
|
createsSecrets: ({ name }: { name: string }) => [name],
|
|
@@ -235,15 +258,27 @@ export async function postInvite(host: string, request: string): Promise<{ url:
|
|
|
235
258
|
export function runtimeDriver({
|
|
236
259
|
tokens,
|
|
237
260
|
identities,
|
|
261
|
+
inspect,
|
|
238
262
|
storage = prepareSecretStore,
|
|
239
263
|
invite = postInvite,
|
|
240
264
|
}: {
|
|
241
265
|
tokens: Tokens;
|
|
242
266
|
identities?: IdentityProvisioning;
|
|
267
|
+
inspect?: InspectRun;
|
|
243
268
|
storage?: PrepareStorage;
|
|
244
269
|
invite?: (host: string, request: string, call: CallContext) => Promise<{ url: string }>;
|
|
245
270
|
}) {
|
|
246
271
|
return defineDriver(sanoma, {
|
|
272
|
+
run: {
|
|
273
|
+
async inspect({ runId }) {
|
|
274
|
+
if (!inspect) throw refuse("run inspection is not configured");
|
|
275
|
+
try {
|
|
276
|
+
return await inspect(runId);
|
|
277
|
+
} catch {
|
|
278
|
+
throw new DriverError("Run inspection unavailable", { retryable: true });
|
|
279
|
+
}
|
|
280
|
+
},
|
|
281
|
+
},
|
|
247
282
|
secretStore: {
|
|
248
283
|
async prepare(input, call) {
|
|
249
284
|
try {
|
|
@@ -344,6 +379,7 @@ export function runtimeDriver({
|
|
|
344
379
|
export const sandboxRuntimeDriver = () =>
|
|
345
380
|
runtimeDriver({
|
|
346
381
|
tokens: memoryTokens(),
|
|
382
|
+
inspect: async (runId) => ({ runId, status: "missing" }),
|
|
347
383
|
identities: memoryIdentityProvisioning(),
|
|
348
384
|
storage: memoryStorageProvisioning(),
|
|
349
385
|
invite: async (host, _request, call) => ({ url: new URL(`/auth/invite/sandbox-${tokenIdOf(call)}`, host).href }),
|
package/src/secret-store.ts
CHANGED
|
@@ -79,9 +79,16 @@ export const isSecretStoreFactory = (store: SecretStore | SecretStoreFactory): s
|
|
|
79
79
|
"kind" in store && store.kind === "runtime";
|
|
80
80
|
|
|
81
81
|
export type SecretBinding =
|
|
82
|
-
| { path: string; management: "sanoma" | "external"; env?: never; bootstrap?: never }
|
|
83
|
-
| {
|
|
84
|
-
|
|
82
|
+
| { path: string; management: "sanoma" | "external"; env?: never; bootstrap?: never; host?: never }
|
|
83
|
+
| {
|
|
84
|
+
env: string;
|
|
85
|
+
/** Deliver this binding to declared deriving hosts under its environment name. */
|
|
86
|
+
host?: boolean;
|
|
87
|
+
management?: "external";
|
|
88
|
+
path?: never;
|
|
89
|
+
bootstrap?: never;
|
|
90
|
+
}
|
|
91
|
+
| { bootstrap: DeploymentKeyName; management?: "external"; env?: never; path?: never; host?: never };
|
|
85
92
|
|
|
86
93
|
export interface SecretConfiguration {
|
|
87
94
|
/** Omitted: built-in encrypted Postgres, supplied by the runtime. */
|
|
@@ -161,8 +168,10 @@ export function checkSecretConfiguration(config: SecretConfiguration): void {
|
|
|
161
168
|
(!config.bindings || typeof config.bindings !== "object" || Array.isArray(config.bindings))
|
|
162
169
|
)
|
|
163
170
|
invalid();
|
|
171
|
+
const hostEnvironment = new Set<string>();
|
|
164
172
|
for (const [name, b] of Object.entries(config.bindings ?? {})) {
|
|
165
173
|
if (!namePattern.test(name) || !b || typeof b !== "object") invalid();
|
|
174
|
+
if (b.host !== undefined && (typeof b.env !== "string" || typeof b.host !== "boolean")) invalid();
|
|
166
175
|
if (DEPLOYMENT_KEYS.includes(name) && !("env" in b) && !("bootstrap" in b)) invalid();
|
|
167
176
|
if ("bootstrap" in b) {
|
|
168
177
|
if (
|
|
@@ -180,6 +189,10 @@ export function checkSecretConfiguration(config: SecretConfiguration): void {
|
|
|
180
189
|
(b.management !== undefined && b.management !== "external")
|
|
181
190
|
)
|
|
182
191
|
invalid();
|
|
192
|
+
if (b.host) {
|
|
193
|
+
if (hostEnvironment.has(b.env!)) invalid();
|
|
194
|
+
hostEnvironment.add(b.env!);
|
|
195
|
+
}
|
|
183
196
|
} else if (!nonempty(b.path) || !["sanoma", "external"].includes(b.management)) invalid();
|
|
184
197
|
}
|
|
185
198
|
}
|
package/src/shared.ts
CHANGED
|
@@ -387,6 +387,9 @@ export function refusalOf(
|
|
|
387
387
|
/** A declared resource as the people functions read it: only the runtime's people types count. */
|
|
388
388
|
export type PeopleResource = Pick<DeclaredResource, "vendor" | "type" | "desired">;
|
|
389
389
|
|
|
390
|
+
/** The group the runtime holds a change to the rules for, and the one that hands out invite links. */
|
|
391
|
+
export const ADMIN = "admin";
|
|
392
|
+
|
|
390
393
|
/** The group every agent is in, the runtime's own among them: no data file declares it. */
|
|
391
394
|
export const AGENTS = "agents";
|
|
392
395
|
|
package/src/source.ts
CHANGED
|
@@ -322,6 +322,8 @@ export interface RemoteHalf {
|
|
|
322
322
|
webhook?(request: Request, context: RemoteContext): Promise<RemoteEvent[]>;
|
|
323
323
|
/** The accounts the company's app on the host is installed on: where it acts with no person's token. */
|
|
324
324
|
owners?(context: RemoteContext): Promise<{ login: string; kind: "user" | "organization" }[]>;
|
|
325
|
+
/** The permissions the company's app on the host holds, by name (`contents: "write"`), as the host lists them; undefined without the app's keys. */
|
|
326
|
+
permissions?(context: RemoteContext): Promise<Record<string, string> | undefined>;
|
|
325
327
|
}
|
|
326
328
|
|
|
327
329
|
/** The remote half among `remotes` (`ResolvedConfig.remotes`) whose host `url` is on, with its vendor and the repository. */
|