@sanoma-ai/workflows 0.10.1 → 0.10.3
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 +140 -0
- package/KMS-SECRETS.md +99 -0
- package/README.md +49 -11
- package/SECRET-SUPPORT.md +1 -1
- package/dist/apply.js +1 -1
- package/dist/apply.js.map +1 -1
- package/dist/authentication.d.ts +7 -2
- package/dist/authentication.js +2 -2
- package/dist/authentication.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 +37 -5
- package/dist/client.js.map +1 -1
- package/dist/cloud-identity.d.ts +52 -0
- package/dist/cloud-identity.js +222 -0
- package/dist/cloud-identity.js.map +1 -0
- package/dist/config.d.ts +42 -4
- package/dist/config.js +71 -10
- package/dist/config.js.map +1 -1
- package/dist/connection.js +8 -2
- package/dist/connection.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 +22 -0
- package/dist/define.js +2 -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/host.d.ts +13 -4
- package/dist/host.js +14 -5
- package/dist/host.js.map +1 -1
- package/dist/index.d.ts +11 -7
- package/dist/index.js +8 -5
- package/dist/index.js.map +1 -1
- package/dist/kms-secrets.d.ts +24 -0
- package/dist/kms-secrets.js +209 -0
- package/dist/kms-secrets.js.map +1 -0
- 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 +78 -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/resource.d.ts +6 -4
- package/dist/resource.js +8 -5
- package/dist/resource.js.map +1 -1
- package/dist/resources.d.ts +4 -2
- package/dist/resources.js +15 -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 +123 -0
- package/dist/runtime-ops.js +222 -0
- package/dist/runtime-ops.js.map +1 -0
- 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 +3 -1
- package/package.json +15 -2
- package/src/apply.ts +1 -1
- package/src/authentication.ts +9 -2
- package/src/call.ts +88 -18
- package/src/change.ts +9 -20
- package/src/client.ts +50 -7
- package/src/cloud-identity.ts +266 -0
- package/src/config.ts +111 -17
- package/src/connection.ts +8 -2
- package/src/db.ts +8 -1
- package/src/deadlines.ts +66 -20
- package/src/define.ts +25 -0
- package/src/deployment-key.ts +34 -8
- package/src/describe.ts +39 -2
- package/src/description.ts +2 -0
- package/src/host.ts +22 -6
- package/src/index.ts +19 -1
- package/src/kms-secrets.ts +271 -0
- package/src/ledger.ts +52 -1
- package/src/login-code.ts +52 -0
- package/src/op.ts +84 -11
- package/src/outline.ts +3 -3
- package/src/resource.ts +8 -5
- package/src/resources.ts +13 -4
- package/src/run.ts +3 -0
- package/src/runtime-ops.ts +234 -0
- 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/deadlines.ts
CHANGED
|
@@ -8,7 +8,7 @@ import { warn } from "./log.ts";
|
|
|
8
8
|
import type { CallContext, DriverFn, Op } from "./op.ts";
|
|
9
9
|
import type { Secrets } from "./secret.ts";
|
|
10
10
|
import { errorMessage, WAKEUP_AGENT } from "./shared.ts";
|
|
11
|
-
import type
|
|
11
|
+
import { type EnvWakeup, WAKE_PATH, type WakeupRegistration } from "./wakeup.ts";
|
|
12
12
|
|
|
13
13
|
// What a worker knows of its own runs that DBOS cannot tell it: which are executing and which are
|
|
14
14
|
// blocked in a `ctx` wait, and until when. DBOS sees both as PENDING. A worker that registers a
|
|
@@ -133,15 +133,15 @@ export interface SelfStop {
|
|
|
133
133
|
at: string | null;
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
-
/**
|
|
136
|
+
/** Registers a deadline with the instance's wakeup, replacing what it held; throws when it could not. */
|
|
137
|
+
export type RegisterWakeup = (registration: WakeupRegistration) => Promise<void>;
|
|
138
|
+
|
|
139
|
+
/** What the stop rule acts on: the wakeup row, how a deadline is registered, and the worker. */
|
|
137
140
|
export interface StopRule {
|
|
138
141
|
/** The wakeup row's database. */
|
|
139
142
|
db: () => Promise<Db>;
|
|
140
|
-
/** The declared wakeup's
|
|
141
|
-
|
|
142
|
-
/** The wakeup type's register operation, and its driver (none when the connector attaches none). */
|
|
143
|
-
register: { op: Op; fn: DriverFn | undefined };
|
|
144
|
-
secrets: Secrets;
|
|
143
|
+
/** The declared wakeup's register operation (`declaredRegister`), or the host's own route (`envRegister`). */
|
|
144
|
+
register: RegisterWakeup;
|
|
145
145
|
deadlines: Deadlines;
|
|
146
146
|
idle: () => Promise<boolean>;
|
|
147
147
|
everyMs: number;
|
|
@@ -174,7 +174,7 @@ export function stopWhenIdle(rule: StopRule): { close(): void } {
|
|
|
174
174
|
const db = await rule.db();
|
|
175
175
|
if (at !== null) {
|
|
176
176
|
try {
|
|
177
|
-
await
|
|
177
|
+
await rule.register({ at });
|
|
178
178
|
} catch (err) {
|
|
179
179
|
const message = errorMessage(err);
|
|
180
180
|
await recordWakeupError(db, message);
|
|
@@ -214,18 +214,64 @@ export function stopWhenIdle(rule: StopRule): { close(): void } {
|
|
|
214
214
|
};
|
|
215
215
|
}
|
|
216
216
|
|
|
217
|
+
/** A declared wakeup's register operation, and what its call needs. */
|
|
218
|
+
export interface DeclaredRegister {
|
|
219
|
+
/** The wakeup type's register operation. */
|
|
220
|
+
op: Op;
|
|
221
|
+
/** Its driver: none when the connector attaches none. */
|
|
222
|
+
fn: DriverFn | undefined;
|
|
223
|
+
/** The instance's secrets, which the driver reads the wakeup's token from. */
|
|
224
|
+
secrets: Secrets;
|
|
225
|
+
/** The declared wakeup's binding, else its declaration's id: what scopes a registration's idempotency key to this instance. */
|
|
226
|
+
wakeup: string;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Registers through the declared wakeup's register operation, as `SanomaClient.call` calls a
|
|
231
|
+
* driver: its input parsed, a `CallContext` of the runtime's, and no ledger record, which is a run's.
|
|
232
|
+
*/
|
|
233
|
+
export function declaredRegister({ op, fn, secrets, wakeup }: DeclaredRegister): RegisterWakeup {
|
|
234
|
+
return async (registration) => {
|
|
235
|
+
if (!fn) throw new Error(`${op.id} has no driver: the connector package must attach one`);
|
|
236
|
+
// Fresh each time: a repeat of an earlier `at` after another registration must re-arm it, so no
|
|
237
|
+
// two registrations share a key (real drivers never dedupe on it anyway).
|
|
238
|
+
const key = `wakeup:${wakeup}:${randomUUID()}`;
|
|
239
|
+
const context: CallContext = {
|
|
240
|
+
idempotencyKey: key,
|
|
241
|
+
runId: key,
|
|
242
|
+
opId: op.id,
|
|
243
|
+
attempt: 1,
|
|
244
|
+
secrets,
|
|
245
|
+
by: WAKEUP_AGENT,
|
|
246
|
+
};
|
|
247
|
+
await fn(op.input.parse(registration), context);
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
217
251
|
/**
|
|
218
|
-
*
|
|
219
|
-
*
|
|
252
|
+
* Registers at the host's own wake route, as the environment names it (`EnvWakeup`): `PUT
|
|
253
|
+
* {url}/__sanoma/wake` with the token as a bearer and the registration as JSON, no driver and no
|
|
254
|
+
* secret store. 204 is taken; anything else fails with its status and the route's own `error`.
|
|
220
255
|
*/
|
|
221
|
-
export
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
256
|
+
export function envRegister({ url, token }: EnvWakeup): RegisterWakeup {
|
|
257
|
+
return async (registration) => {
|
|
258
|
+
const where = `PUT ${url}${WAKE_PATH}`;
|
|
259
|
+
let res: Response;
|
|
260
|
+
try {
|
|
261
|
+
res = await fetch(`${url}${WAKE_PATH}`, {
|
|
262
|
+
method: "PUT",
|
|
263
|
+
headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
|
|
264
|
+
body: JSON.stringify(registration),
|
|
265
|
+
// A redirect is no answer of the host's, and would carry the token elsewhere.
|
|
266
|
+
redirect: "manual",
|
|
267
|
+
signal: AbortSignal.timeout(30_000),
|
|
268
|
+
});
|
|
269
|
+
} catch (err) {
|
|
270
|
+
throw new Error(`${where}: no answer (${errorMessage(err)})`, { cause: err });
|
|
271
|
+
}
|
|
272
|
+
if (res.status === 204) return;
|
|
273
|
+
const body: unknown = await res.json().catch(() => undefined);
|
|
274
|
+
const error = (body as { error?: unknown } | undefined)?.error;
|
|
275
|
+
throw new Error(`${where} answered ${res.status}${typeof error === "string" ? `: ${error}` : ""}`);
|
|
276
|
+
};
|
|
231
277
|
}
|
package/src/define.ts
CHANGED
|
@@ -18,14 +18,37 @@ export interface Principal {
|
|
|
18
18
|
groups?: string[];
|
|
19
19
|
/** For an agent: the id of the person whose instruction it carries. */
|
|
20
20
|
onBehalfOf?: string;
|
|
21
|
+
/**
|
|
22
|
+
* For a visitor: the email their site's login verified (`LoginHalf.verify`), which the app
|
|
23
|
+
* passes on. Admission keeps it for a visitor alone; a run's `ctx.startedBy.email`.
|
|
24
|
+
*/
|
|
25
|
+
email?: string;
|
|
26
|
+
/**
|
|
27
|
+
* For an api token (`token:<id>`): the subject its row acts for, which `SanomaClient.start`
|
|
28
|
+
* reads from the row, never from the caller; a run's `ctx.startedBy.subject`.
|
|
29
|
+
*/
|
|
30
|
+
subject?: string;
|
|
21
31
|
}
|
|
22
32
|
|
|
23
33
|
export const Principal: z.ZodType<Principal> = z.object({
|
|
24
34
|
id: z.string().regex(/\S/, "id must not be blank"),
|
|
25
35
|
groups: z.array(z.string()).optional(),
|
|
26
36
|
onBehalfOf: z.string().regex(/\S/, "onBehalfOf must not be blank").optional(),
|
|
37
|
+
email: z.string().optional(),
|
|
38
|
+
subject: z.string().optional(),
|
|
27
39
|
});
|
|
28
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Who started a run, as its workflow sees it (`ctx.startedBy`): the principal's id (`alice`,
|
|
43
|
+
* `visitor:<login>:<subject>`, `token:<id>`), with the email a visitor's login verified, and for
|
|
44
|
+
* an api token the subject its row acts for, so a workflow can tell whose a record is.
|
|
45
|
+
*/
|
|
46
|
+
export interface StartedBy {
|
|
47
|
+
readonly id: string;
|
|
48
|
+
readonly email?: string;
|
|
49
|
+
readonly subject?: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
29
52
|
/** A built-in a workflow may call, as its description lists them. */
|
|
30
53
|
export type Builtin = WorkflowEntry["builtins"][number];
|
|
31
54
|
export type Use = Op | Builtin;
|
|
@@ -227,6 +250,8 @@ interface BaseCtx {
|
|
|
227
250
|
*/
|
|
228
251
|
now(): Promise<number>;
|
|
229
252
|
readonly runId: string;
|
|
253
|
+
/** Who started the run: read, never called (`StartedBy`). */
|
|
254
|
+
readonly startedBy: StartedBy;
|
|
230
255
|
}
|
|
231
256
|
|
|
232
257
|
export type Ctx<U extends readonly Use[]> = BaseCtx &
|
package/src/deployment-key.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
+
import { hkdfSync } from "node:crypto";
|
|
1
2
|
import { readFileSync } from "node:fs";
|
|
2
3
|
|
|
4
|
+
/** The deployment's own keys: bound only to the environment or bootstrap, never written by the runtime. */
|
|
5
|
+
export const DEPLOYMENT_KEYS: readonly string[] = ["SANOMA_SIGNING_KEY", "SANOMA_SECRETS_KEY"];
|
|
6
|
+
export type DeploymentKeyName = "SANOMA_SIGNING_KEY" | "SANOMA_SECRETS_KEY";
|
|
7
|
+
|
|
3
8
|
/** Bootstrap only: never resolves through the secret store that the key unlocks. */
|
|
4
|
-
export function deploymentKey(
|
|
5
|
-
name: "SANOMA_SECRETS_KEY" | "SANOMA_SIGNING_KEY",
|
|
6
|
-
env: NodeJS.ProcessEnv = process.env,
|
|
7
|
-
): Buffer {
|
|
9
|
+
export function deploymentKey(name: DeploymentKeyName, env: NodeJS.ProcessEnv = process.env): Buffer {
|
|
8
10
|
const value = env[name];
|
|
9
11
|
const file = env[`${name}_FILE`];
|
|
10
12
|
if (value && file) throw new Error(`Set only one of ${name} and ${name}_FILE`);
|
|
@@ -16,8 +18,32 @@ export function deploymentKey(
|
|
|
16
18
|
throw new Error(`${name}_FILE could not be read`);
|
|
17
19
|
}
|
|
18
20
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
const key = encoded ? keyBytes(encoded) : undefined;
|
|
22
|
+
if (!key) throw new Error(`${name} must be 32 random bytes in base64; supply ${name} or ${name}_FILE`);
|
|
23
|
+
return key;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The 32 bytes of a key written as text: padded base64 (`sanoma key`), or base64url without
|
|
28
|
+
* padding (a `secret(..., { generate: true })` marker's value). Anything else is `undefined`.
|
|
29
|
+
*/
|
|
30
|
+
export function keyBytes(text: string): Buffer | undefined {
|
|
31
|
+
if (/^[A-Za-z0-9+/]{43}=$/.test(text)) return Buffer.from(text, "base64");
|
|
32
|
+
if (/^[A-Za-z0-9_-]{43}$/.test(text)) return Buffer.from(text, "base64url");
|
|
33
|
+
return undefined;
|
|
23
34
|
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* A 32-byte key of its own for `purpose`, derived from a deployment's signing key (HKDF-SHA256
|
|
38
|
+
* over its base64 text), as that deployment derives it from its `SANOMA_SIGNING_KEY`.
|
|
39
|
+
*/
|
|
40
|
+
export const deriveSigningKey = (signingKey: Buffer, purpose: string): Buffer =>
|
|
41
|
+
Buffer.from(hkdfSync("sha256", signingKey.toString("base64"), "", purpose, 32));
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* A 32-byte key of its own for `purpose`, derived from `SANOMA_SIGNING_KEY` (`deriveSigningKey`):
|
|
45
|
+
* each use of the signing key (a login's cookies, the setup's state, sealed values) gets a key no
|
|
46
|
+
* other use shares.
|
|
47
|
+
*/
|
|
48
|
+
export const signingKeyFor = (purpose: string): Buffer =>
|
|
49
|
+
deriveSigningKey(deploymentKey("SANOMA_SIGNING_KEY"), purpose);
|
package/src/describe.ts
CHANGED
|
@@ -4,7 +4,8 @@ import { type AppEntry, appsAt } from "./apps.ts";
|
|
|
4
4
|
import { readSource, type SourceFile } from "./ast.ts";
|
|
5
5
|
import { readPolicyFiles } from "./cedar.ts";
|
|
6
6
|
import { recordSchemaOf } from "./collection.ts";
|
|
7
|
-
import {
|
|
7
|
+
import { decideCall } from "./call.ts";
|
|
8
|
+
import { connectorsOf, type ResolvedConfig, resolveConfig, rootOf, type SanomaConfig } from "./config.ts";
|
|
8
9
|
import { type Builtin, jsonSchemaOf, opIdsOf, type Use } from "./define.ts";
|
|
9
10
|
import {
|
|
10
11
|
CAPABILITIES,
|
|
@@ -17,6 +18,7 @@ import {
|
|
|
17
18
|
} from "./description.ts";
|
|
18
19
|
import { isDirect } from "./direct.ts";
|
|
19
20
|
import { type CredentialStatus, credentialsOf } from "./env.ts";
|
|
21
|
+
import { CODE_MAIL } from "./login-code.ts";
|
|
20
22
|
import { lintWorkflow } from "./lint.ts";
|
|
21
23
|
import { fresh, stampOf, stampsOf } from "./load.ts";
|
|
22
24
|
import { LOCKFILE } from "./lockfile.ts";
|
|
@@ -27,7 +29,17 @@ import { allowAll, policyOpOf } from "./policy.ts";
|
|
|
27
29
|
import { type Resource, resourceTypesOf, valueAt } from "./resource.ts";
|
|
28
30
|
import { type DeclaredResource, inAppPeople, loadDataFiles, type ResourceProblem } from "./resources.ts";
|
|
29
31
|
import { apiNameProblem } from "./sdk.ts";
|
|
30
|
-
import {
|
|
32
|
+
import {
|
|
33
|
+
apiNameOf,
|
|
34
|
+
BUILT_IN_WORKFLOWS,
|
|
35
|
+
DATA_VENDOR,
|
|
36
|
+
errorMessage,
|
|
37
|
+
LOGIN_AGENT,
|
|
38
|
+
LOGIN_CODE_WORKFLOW,
|
|
39
|
+
type PeopleResource,
|
|
40
|
+
principalOf,
|
|
41
|
+
problemLine,
|
|
42
|
+
} from "./shared.ts";
|
|
31
43
|
import { readSites, type SiteEntry } from "./sites.ts";
|
|
32
44
|
|
|
33
45
|
// `@sanoma-ai/workflows/describe`: what a UI renders from, apart from the main entry.
|
|
@@ -282,6 +294,7 @@ export function describeConfig(config: SanomaConfig): ConfigDescription {
|
|
|
282
294
|
...appProblems,
|
|
283
295
|
...appClashes(apps, collections, sites),
|
|
284
296
|
...domainProblems(dataFiles.resources, resources, sites),
|
|
297
|
+
...loginProblems(resolved, dataFiles.resources),
|
|
285
298
|
],
|
|
286
299
|
apps,
|
|
287
300
|
policy: {
|
|
@@ -291,9 +304,33 @@ export function describeConfig(config: SanomaConfig): ConfigDescription {
|
|
|
291
304
|
files: resolved.root === undefined ? [] : readPolicyFiles(resolved.root),
|
|
292
305
|
},
|
|
293
306
|
sites,
|
|
307
|
+
...(resolved.login && { login: { sender: resolved.login.sender.id } }),
|
|
294
308
|
};
|
|
295
309
|
}
|
|
296
310
|
|
|
311
|
+
/**
|
|
312
|
+
* The policy holding or denying the sign-in code: `login.sender` as the runtime's agent `login`
|
|
313
|
+
* in a run of `login-code` must be allowed, or nobody receives a code.
|
|
314
|
+
*/
|
|
315
|
+
function loginProblems(resolved: ResolvedConfig, people: readonly PeopleResource[]): ResourceProblem[] {
|
|
316
|
+
if (!resolved.login || resolved.policyProblems.length) return [];
|
|
317
|
+
const { sender } = resolved.login;
|
|
318
|
+
const agent = principalOf(LOGIN_AGENT, people)!;
|
|
319
|
+
const run = { id: "check", workflow: LOGIN_CODE_WORKFLOW, approvals: [] };
|
|
320
|
+
const decision = decideCall(resolved.policy, sender, CODE_MAIL, agent, run);
|
|
321
|
+
if (decision.kind === "allow") return [];
|
|
322
|
+
const does = decision.kind === "deny" ? "denies" : "holds";
|
|
323
|
+
const by = decision.reasons?.length ? ` (${decision.reasons.join(", ")})` : "";
|
|
324
|
+
return [
|
|
325
|
+
{
|
|
326
|
+
file: "policies",
|
|
327
|
+
message:
|
|
328
|
+
`the policy ${does} ${sender.id} for the sign-in code${by}, so nobody receives one: exempt the runtime's ` +
|
|
329
|
+
`agent, \`unless { principal == Sanoma::Principal::"${LOGIN_AGENT}" }\``,
|
|
330
|
+
},
|
|
331
|
+
];
|
|
332
|
+
}
|
|
333
|
+
|
|
297
334
|
/** That the root's `kind` `name`, in `file`, is named as an app is. */
|
|
298
335
|
const appClash = (kind: string, name: string, file: string): ResourceProblem => ({
|
|
299
336
|
file,
|
package/src/description.ts
CHANGED
|
@@ -422,5 +422,7 @@ export const ConfigDescriptionSchema = z.strictObject({
|
|
|
422
422
|
* the config does not have as its problems; none when the root has no `sites/`.
|
|
423
423
|
*/
|
|
424
424
|
sites: z.array(SiteEntrySchema),
|
|
425
|
+
/** The config's `login`: the operation its sign-in codes are sent with, by id. */
|
|
426
|
+
login: z.strictObject({ sender: z.string() }).optional(),
|
|
425
427
|
});
|
|
426
428
|
export type ConfigDescription = z.infer<typeof ConfigDescriptionSchema>;
|
package/src/host.ts
CHANGED
|
@@ -46,8 +46,11 @@ export interface HostSpec {
|
|
|
46
46
|
source: string;
|
|
47
47
|
/** The branch (or tag) it runs: `main`, never a commit. */
|
|
48
48
|
ref: string;
|
|
49
|
-
/**
|
|
50
|
-
|
|
49
|
+
/**
|
|
50
|
+
* The secrets it passes the runtime (`SANOMA_DATABASE_URL`, `SANOMA_SECRETS_KEY`): a write-only
|
|
51
|
+
* field. Absent for a host that binds its own bootstrap (`bindsBootstrap`).
|
|
52
|
+
*/
|
|
53
|
+
secrets?: string;
|
|
51
54
|
/** The port the app listens on. */
|
|
52
55
|
port: string;
|
|
53
56
|
/** A public site's domain, by site name: the app serves each site at the root of its domain (`SANOMA_DOMAINS`). */
|
|
@@ -58,6 +61,11 @@ export interface HostSpec {
|
|
|
58
61
|
* file may not declare it, so a merge to `ref` is a change to the host.
|
|
59
62
|
*/
|
|
60
63
|
revision: string;
|
|
64
|
+
/**
|
|
65
|
+
* True for a host whose vendor mints the instance's bootstrap (its database URL, keys and
|
|
66
|
+
* tokens) and binds it itself, as Sanoma Cloud's does: such a type takes no `secrets`.
|
|
67
|
+
*/
|
|
68
|
+
bindsBootstrap?: boolean;
|
|
61
69
|
outputs: (state: Record<string, unknown>) => HostOutputs;
|
|
62
70
|
}
|
|
63
71
|
|
|
@@ -83,8 +91,9 @@ function unwrap(schema: z.ZodType): z.ZodType {
|
|
|
83
91
|
|
|
84
92
|
/**
|
|
85
93
|
* Throws what is wrong with a resource type's `host`: an input or `revision` that names no field,
|
|
86
|
-
* `secrets` that is not write-only,
|
|
87
|
-
*
|
|
94
|
+
* `secrets` that is not write-only, missing (unless the host binds its own bootstrap) or given
|
|
95
|
+
* beside `bindsBootstrap`, a `revision` that is write-only, an unknown lifetime, a type apply
|
|
96
|
+
* cannot change (not `writable`).
|
|
88
97
|
*/
|
|
89
98
|
export function checkHost(
|
|
90
99
|
what: string,
|
|
@@ -96,10 +105,17 @@ export function checkHost(
|
|
|
96
105
|
if (!LIFETIMES.includes(host.capabilities?.lifetime)) {
|
|
97
106
|
problems.push(`capabilities.lifetime is one of ${LIFETIMES.join(", ")}`);
|
|
98
107
|
}
|
|
99
|
-
|
|
108
|
+
// An input given as undefined is one not given.
|
|
109
|
+
const given = Object.entries(host.inputs ?? {}).filter(([, path]) => path !== undefined);
|
|
110
|
+
for (const [input, path] of [...given, ["revision", host.revision] as const]) {
|
|
100
111
|
if (typeof path !== "string" || !hasPath(type.schema, path)) problems.push(`${input} names no field (${path})`);
|
|
101
112
|
}
|
|
102
|
-
|
|
113
|
+
const secrets = host.inputs?.secrets;
|
|
114
|
+
if (host.bindsBootstrap) {
|
|
115
|
+
if (secrets !== undefined) problems.push("inputs.secrets is not taken by a host that binds its own bootstrap");
|
|
116
|
+
} else if (secrets === undefined || !type.fields.writeOnly.includes(secrets)) {
|
|
117
|
+
problems.push("inputs.secrets must be a write-only field, unless the host binds its own bootstrap");
|
|
118
|
+
}
|
|
103
119
|
if (type.fields.writeOnly.includes(host.revision)) problems.push("revision must not be write-only: drift reads it");
|
|
104
120
|
if (typeof host.outputs !== "function") problems.push("outputs must be a function of the state");
|
|
105
121
|
if (problems.length) throw new Error(`${what}: its host: ${problems.join("; ")}`);
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export {
|
|
2
2
|
type CallContext,
|
|
3
3
|
CLIENT_IP_HEADER,
|
|
4
|
+
requestOrigin,
|
|
5
|
+
sameOrigin,
|
|
4
6
|
type Connector,
|
|
5
7
|
type ConnectorSetup,
|
|
6
8
|
type ConnectorVendor,
|
|
@@ -12,13 +14,17 @@ export {
|
|
|
12
14
|
type Effect,
|
|
13
15
|
isOp,
|
|
14
16
|
isSvgElement,
|
|
17
|
+
INVITE_SECONDS,
|
|
15
18
|
type LoginContext,
|
|
16
19
|
type LoginHalf,
|
|
20
|
+
type LoginIdentity,
|
|
21
|
+
type LoginMethod,
|
|
17
22
|
type ModelContext,
|
|
18
23
|
type Op,
|
|
19
24
|
type OpSpec,
|
|
20
25
|
type ResourceGroup,
|
|
21
26
|
retryableStatus,
|
|
27
|
+
type SignInMethod,
|
|
22
28
|
type VendorInfo,
|
|
23
29
|
withSetup,
|
|
24
30
|
} from "./op.ts";
|
|
@@ -33,6 +39,7 @@ export {
|
|
|
33
39
|
type Builtin,
|
|
34
40
|
type Ctx,
|
|
35
41
|
type SleepRequest,
|
|
42
|
+
type StartedBy,
|
|
36
43
|
type StepOptions,
|
|
37
44
|
type Trigger,
|
|
38
45
|
type Use,
|
|
@@ -56,6 +63,7 @@ export {
|
|
|
56
63
|
export {
|
|
57
64
|
declaredWakeup,
|
|
58
65
|
defineConfig,
|
|
66
|
+
envWakeup,
|
|
59
67
|
resolveConfig,
|
|
60
68
|
resolveDatabaseUrl,
|
|
61
69
|
type ResolvedConfig,
|
|
@@ -201,9 +209,16 @@ export {
|
|
|
201
209
|
storeDatabaseUrl,
|
|
202
210
|
} from "./database.ts";
|
|
203
211
|
export type { Chat, Chats } from "./chats.ts";
|
|
212
|
+
export type { Tokens } from "./tokens.ts";
|
|
213
|
+
export { type InviteRequest, openInvite, sealInvite, sealLoginCode } from "./seal.ts";
|
|
214
|
+
export { postInvite, sanoma } from "./runtime-ops.ts";
|
|
215
|
+
export { sourceKeyPair, sshPublicKey } from "./ssh-key.ts";
|
|
204
216
|
export {
|
|
205
217
|
checkWakeup,
|
|
206
218
|
type DeclaredWakeup,
|
|
219
|
+
type EnvWakeup,
|
|
220
|
+
HOST_URL,
|
|
221
|
+
WAKE_PATH,
|
|
207
222
|
type WakeupOutputs,
|
|
208
223
|
type WakeupRegistration,
|
|
209
224
|
type WakeupSpec,
|
|
@@ -243,6 +258,8 @@ export {
|
|
|
243
258
|
principalOf,
|
|
244
259
|
RUNTIME_AGENTS,
|
|
245
260
|
HOST_CHECK_AGENT,
|
|
261
|
+
LOGIN_AGENT,
|
|
262
|
+
LOGIN_CODE_WORKFLOW,
|
|
246
263
|
SCHEDULE_AGENT,
|
|
247
264
|
credentialReady,
|
|
248
265
|
DEFAULT_SLEEP_NAME,
|
|
@@ -292,12 +309,13 @@ export {
|
|
|
292
309
|
type AuthenticationProblem,
|
|
293
310
|
type Authentication,
|
|
294
311
|
type AuthenticationIdentity,
|
|
312
|
+
type AuthenticationContext,
|
|
295
313
|
type CallAuthentication,
|
|
296
314
|
} from "./authentication.ts";
|
|
297
315
|
export { checkSecretStore, type SecretStoreCheck } from "./secret-store-check.ts";
|
|
298
316
|
|
|
299
317
|
export { postgresSecretStore } from "./credentials.ts";
|
|
300
|
-
export { deploymentKey } from "./deployment-key.ts";
|
|
318
|
+
export { deploymentKey, signingKeyFor } from "./deployment-key.ts";
|
|
301
319
|
|
|
302
320
|
export {
|
|
303
321
|
defineConnection,
|