@sanoma-ai/workflows 0.11.2 → 0.11.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/dist/bundled.d.ts +29 -0
- package/dist/bundled.js +16 -0
- package/dist/bundled.js.map +1 -0
- package/dist/cedar.d.ts +4 -1
- package/dist/cedar.js +8 -1
- package/dist/cedar.js.map +1 -1
- package/dist/config.d.ts +8 -1
- package/dist/config.js +23 -4
- package/dist/config.js.map +1 -1
- package/dist/db.d.ts +7 -0
- package/dist/db.js +30 -1
- package/dist/db.js.map +1 -1
- package/dist/define.d.ts +2 -1
- package/dist/define.js +4 -2
- package/dist/define.js.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.js +6 -1
- package/dist/index.js.map +1 -1
- package/dist/load.js +3 -2
- package/dist/load.js.map +1 -1
- package/dist/resources.d.ts +1 -1
- package/dist/resources.js +5 -1
- package/dist/resources.js.map +1 -1
- package/dist/schema.d.ts +12 -2
- package/dist/schema.js +12 -6
- package/dist/schema.js.map +1 -1
- package/dist/shared.d.ts +25 -0
- package/dist/shared.js +7 -0
- package/dist/shared.js.map +1 -1
- package/package.json +1 -1
- package/src/bundled.ts +42 -0
- package/src/cedar.ts +7 -1
- package/src/config.ts +22 -5
- package/src/db.ts +35 -1
- package/src/define.ts +4 -2
- package/src/index.ts +6 -0
- package/src/load.ts +5 -2
- package/src/resources.ts +4 -1
- package/src/schema.ts +17 -4
- package/src/shared.ts +20 -0
package/src/config.ts
CHANGED
|
@@ -57,7 +57,8 @@ import {
|
|
|
57
57
|
VENDOR,
|
|
58
58
|
withSetup,
|
|
59
59
|
} from "./op.ts";
|
|
60
|
-
import { type
|
|
60
|
+
import { rootSnapshot, type RootSnapshot } from "./bundled.ts";
|
|
61
|
+
import { type CedarPolicySource, policyAt, readPolicyFiles } from "./cedar.ts";
|
|
61
62
|
import type { Policy } from "./policy.ts";
|
|
62
63
|
import { checkFromRefs, type Resource, resourceTypesOf, withoutWriteOnly } from "./resource.ts";
|
|
63
64
|
import { codeMail, loginCodeWorkflow } from "./login-code.ts";
|
|
@@ -76,7 +77,7 @@ import { tokenStore } from "./tokens.ts";
|
|
|
76
77
|
import type { Steps } from "./steps.ts";
|
|
77
78
|
import { checkSchedule } from "./schedules.ts";
|
|
78
79
|
import { changeCodeOf, computeVersion } from "./version.ts";
|
|
79
|
-
import type
|
|
80
|
+
import { type DeclaredResource, readPeople, type ResourceProblem } from "./resources.ts";
|
|
80
81
|
import { checkWakeupOps, type DeclaredWakeup, type EnvWakeup, HOST_URL } from "./wakeup.ts";
|
|
81
82
|
import { instanceUrl, WAKEUP_TOKEN } from "./wakeup-service.ts";
|
|
82
83
|
import { sanomaWakeupDriver, wakeupSteps } from "./wakeup-driver.ts";
|
|
@@ -425,6 +426,7 @@ export function resolveConfig(config: SanomaConfig): ResolvedConfig {
|
|
|
425
426
|
const login = config.login && checkLogin(config.login, ops, drivers, appName);
|
|
426
427
|
if (login) names.set(LOGIN_CODE_WORKFLOW, loginCodeWorkflow(login, appName));
|
|
427
428
|
const { policy, policies, problems: policyProblems } = policyAt(root, { ops, workflows: names, collections });
|
|
429
|
+
const bundled = rootSnapshot(root);
|
|
428
430
|
return {
|
|
429
431
|
secrets: config.secrets ?? {},
|
|
430
432
|
connections,
|
|
@@ -432,9 +434,10 @@ export function resolveConfig(config: SanomaConfig): ResolvedConfig {
|
|
|
432
434
|
identityProvisioning,
|
|
433
435
|
appName,
|
|
434
436
|
databaseUrl,
|
|
435
|
-
// The built-in's code and operations are part of the app's version, like the config's own.
|
|
436
|
-
|
|
437
|
-
|
|
437
|
+
// The built-in's code and operations are part of the app's version, like the config's own. A
|
|
438
|
+
// bundle's code is not the source's: it carries the hashes of the root it was read from.
|
|
439
|
+
version: bundled?.version ?? computeVersion({ appName, workflows: [...names.values()] }),
|
|
440
|
+
changeCode: bundled?.changeCode ?? changeCodeOf(names.get(CHANGE_WORKFLOW)!),
|
|
438
441
|
queueName: `sanoma:${appName}`,
|
|
439
442
|
...(login && { login }),
|
|
440
443
|
ops,
|
|
@@ -593,12 +596,26 @@ function makersOf<K extends keyof ConnectorSetup>(
|
|
|
593
596
|
return makers;
|
|
594
597
|
}
|
|
595
598
|
|
|
599
|
+
/**
|
|
600
|
+
* What the runtime reads from `config`'s root that a bundle carries where the files are not
|
|
601
|
+
* (`useRootSnapshot`): the app's name and versions as `resolved` has them, the policy files and
|
|
602
|
+
* the people the data files declare.
|
|
603
|
+
*/
|
|
604
|
+
export function readRootSnapshot(config: SanomaConfig, resolved: ResolvedConfig = resolveConfig(config)): RootSnapshot {
|
|
605
|
+
const root = rootOf(config);
|
|
606
|
+
if (root === undefined) throw new Error("A config with no root has no files to carry");
|
|
607
|
+
const { appName, version, changeCode } = resolved;
|
|
608
|
+
return { appName, version, changeCode, policies: readPolicyFiles(root), people: readPeople(root) };
|
|
609
|
+
}
|
|
610
|
+
|
|
596
611
|
/**
|
|
597
612
|
* The `name` of `<root>/package.json`, kept whole (`@acme/ops`), when that file exists and names
|
|
598
613
|
* one; else "sanoma", as for a config with no `root`. Never a `package.json` above the root: a
|
|
599
614
|
* parent project's name must not become the app's. A file that is not JSON is refused, naming it.
|
|
600
615
|
*/
|
|
601
616
|
function appNameAt(root: string | undefined): string {
|
|
617
|
+
const bundled = rootSnapshot(root);
|
|
618
|
+
if (bundled) return bundled.appName;
|
|
602
619
|
const file = root === undefined ? undefined : join(root, "package.json");
|
|
603
620
|
if (file === undefined || !existsSync(file)) return "sanoma";
|
|
604
621
|
let manifest: unknown;
|
package/src/db.ts
CHANGED
|
@@ -226,6 +226,19 @@ export function lazyPool(databaseUrl: string, what: string): LazyPool {
|
|
|
226
226
|
/** Each database prepared, or being prepared, by its URL. A failed preparation is forgotten. */
|
|
227
227
|
const prepared = new Map<string, Promise<void>>();
|
|
228
228
|
|
|
229
|
+
/** What prepares the databases instead of this process (`checkPreparedOnly`), which a refusal names. */
|
|
230
|
+
let preparedBy: string | undefined;
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* From now on, `prepareDatabase` makes nothing: it only checks that DBOS's tables and the
|
|
234
|
+
* runtime's are there, and refuses, naming `by`, what prepares them, when they are not. For a
|
|
235
|
+
* process beside the one that prepares the database, which must never migrate it: the Cloudflare
|
|
236
|
+
* host's Worker, whose container prepares it at its start.
|
|
237
|
+
*/
|
|
238
|
+
export function checkPreparedOnly(by: string): void {
|
|
239
|
+
preparedBy = by;
|
|
240
|
+
}
|
|
241
|
+
|
|
229
242
|
/**
|
|
230
243
|
* Makes the database at `databaseUrl` ready for the runtime, once per process and URL: the
|
|
231
244
|
* database itself and DBOS's tables when they are not there yet (as a worker's launch makes
|
|
@@ -237,7 +250,8 @@ const prepared = new Map<string, Promise<void>>();
|
|
|
237
250
|
export function prepareDatabase(databaseUrl: string): Promise<void> {
|
|
238
251
|
let preparing = prepared.get(databaseUrl);
|
|
239
252
|
if (!preparing) {
|
|
240
|
-
|
|
253
|
+
const made = preparedBy === undefined ? prepare(databaseUrl) : checkPrepared(databaseUrl, preparedBy);
|
|
254
|
+
preparing = made.catch((err: unknown) => {
|
|
241
255
|
if (prepared.get(databaseUrl) === preparing) prepared.delete(databaseUrl);
|
|
242
256
|
throw err;
|
|
243
257
|
});
|
|
@@ -267,6 +281,26 @@ async function prepare(databaseUrl: string): Promise<void> {
|
|
|
267
281
|
}
|
|
268
282
|
}
|
|
269
283
|
|
|
284
|
+
/** The last of the runtime's tables a preparation makes (`TABLES`), so its being there says they all are. */
|
|
285
|
+
const LAST_TABLE = API_TOKENS_TABLE;
|
|
286
|
+
|
|
287
|
+
/** Refuses when the database at `databaseUrl` lacks DBOS's tables or the runtime's: `by` makes them, not this process. */
|
|
288
|
+
async function checkPrepared(databaseUrl: string, by: string): Promise<void> {
|
|
289
|
+
const conn = await connect(databaseUrl);
|
|
290
|
+
try {
|
|
291
|
+
const { rows } = await conn.query<{ dbos: boolean; ours: boolean }>(
|
|
292
|
+
`SELECT to_regclass('dbos.workflow_status') IS NOT NULL AS dbos, to_regclass('${LAST_TABLE}') IS NOT NULL AS ours`,
|
|
293
|
+
);
|
|
294
|
+
const { dbos, ours } = rows[0]!;
|
|
295
|
+
if (!dbos || !ours) {
|
|
296
|
+
const missing = [...(dbos ? [] : ["DBOS's"]), ...(ours ? [] : ["Sanoma's"])].join(" and ");
|
|
297
|
+
throw new Error(`the database has no ${missing} tables yet: ${by} makes them, and this process never does`);
|
|
298
|
+
}
|
|
299
|
+
} finally {
|
|
300
|
+
await conn.end().catch(() => undefined);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
270
304
|
/**
|
|
271
305
|
* A connection to the database at `databaseUrl` (made first when missing) holding the
|
|
272
306
|
* preparation's session lock until it ends, so a second process waits, then finds everything
|
package/src/define.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { bundledFile } from "./bundled.ts";
|
|
1
2
|
import type { ConnectionRef } from "./connection.ts";
|
|
2
3
|
import type { WorkflowEntry } from "./description.ts";
|
|
3
4
|
import { fileURLToPath } from "node:url";
|
|
@@ -396,11 +397,12 @@ const isTimeZone = (zone: unknown): boolean => {
|
|
|
396
397
|
|
|
397
398
|
/**
|
|
398
399
|
* The file of the code that called the function calling this one (`defineWorkflow`'s caller),
|
|
399
|
-
* from V8's call sites, as a path;
|
|
400
|
+
* from V8's call sites, as a path; where they name none, the file a bundle says it is evaluating
|
|
401
|
+
* (`bundledFile`); undefined where there is neither.
|
|
400
402
|
*/
|
|
401
403
|
export function callerFile(): string | undefined {
|
|
402
404
|
// Frame 0 is this function, frame 1 its caller, frame 2 theirs: by index, since a bundle may hold all three.
|
|
403
|
-
return scriptPath(getCallSites(3)[2]?.scriptName);
|
|
405
|
+
return scriptPath(getCallSites(3)[2]?.scriptName) ?? bundledFile();
|
|
404
406
|
}
|
|
405
407
|
|
|
406
408
|
/** A V8 script name (a `file:` URL for a module, a path for a script) as a path; undefined for none. */
|
package/src/index.ts
CHANGED
|
@@ -67,6 +67,7 @@ export {
|
|
|
67
67
|
defineConfig,
|
|
68
68
|
envWakeup,
|
|
69
69
|
type Login,
|
|
70
|
+
readRootSnapshot,
|
|
70
71
|
resolveConfig,
|
|
71
72
|
resolveDatabaseUrl,
|
|
72
73
|
type ResolvedConfig,
|
|
@@ -242,6 +243,11 @@ export {
|
|
|
242
243
|
} from "./client.ts";
|
|
243
244
|
// A raw DBOS client on the runtime's database, for tests and tools that read DBOS directly.
|
|
244
245
|
export { dbosClient } from "./db.ts";
|
|
246
|
+
// The runtime bundled where a company's files are not (the Cloudflare host's Worker): the root's
|
|
247
|
+
// people, policies and app name as data, Cedar's web build, and a database it only checks.
|
|
248
|
+
export { type RootSnapshot, useRootSnapshot } from "./bundled.ts";
|
|
249
|
+
export { checkPreparedOnly } from "./db.ts";
|
|
250
|
+
export { type CedarWasm, useCedarWasm } from "./schema.ts";
|
|
245
251
|
// Also at `@sanoma-ai/workflows/shared`, which a browser bundle can import.
|
|
246
252
|
export {
|
|
247
253
|
AGENTS,
|
package/src/load.ts
CHANGED
|
@@ -89,7 +89,8 @@ const registry = ((globalThis as Record<symbol, unknown>)[Symbol.for("sanoma.loa
|
|
|
89
89
|
made: new WeakMap(),
|
|
90
90
|
}) as Registry;
|
|
91
91
|
|
|
92
|
-
|
|
92
|
+
/** This module's `require`, made on first use: a bundle with no file URL (the Cloudflare host's Worker) never loads a file. */
|
|
93
|
+
let require: NodeJS.Require | undefined;
|
|
93
94
|
|
|
94
95
|
/** Where `value` was made, when a company file made it with a constructor the loader served. */
|
|
95
96
|
export const madeOf = (value: unknown): Made | undefined =>
|
|
@@ -205,7 +206,9 @@ function loadOne(gen: Generation, file: string, rel: string): Loaded {
|
|
|
205
206
|
if (done) return done;
|
|
206
207
|
let loaded: Loaded;
|
|
207
208
|
try {
|
|
208
|
-
const { ns } = require(`${SCHEME}${gen.id}/${pathToFileURL(file).href}`) as {
|
|
209
|
+
const { ns } = (require ??= createRequire(import.meta.url))(`${SCHEME}${gen.id}/${pathToFileURL(file).href}`) as {
|
|
210
|
+
ns: Record<string, unknown>;
|
|
211
|
+
};
|
|
209
212
|
loaded = { exports: Object.entries(ns) };
|
|
210
213
|
} catch (error) {
|
|
211
214
|
// A file that imports one that threw throws the same object: reported once, where it was thrown.
|
package/src/resources.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { APPS_DIR, COLLECTIONS_FILE, filesUnder, kindDirs, relPath } from "./pat
|
|
|
9
9
|
import { peopleConnector, sanoma } from "./person.ts";
|
|
10
10
|
import { wakeupOps } from "./wakeup-type.ts";
|
|
11
11
|
import { DECLARED, type Declared, isDeclared, isObject, type Resource, resourceTypesOf } from "./resource.ts";
|
|
12
|
+
import { rootSnapshot } from "./bundled.ts";
|
|
12
13
|
import { Principal } from "./define.ts";
|
|
13
14
|
import { NotAdmittedError } from "./errors.ts";
|
|
14
15
|
import {
|
|
@@ -89,10 +90,12 @@ export const peopleSources = (root: string): ReadonlySet<string> => filesRead(pe
|
|
|
89
90
|
* Only the files that may declare them (`mayDeclarePeople`) are loaded, with the people's and the
|
|
90
91
|
* wakeup's constructors, which the same `sanoma` declares; loaded again only when one of them
|
|
91
92
|
* changed (by its size and time), so a new colleague is admitted once their file is saved, and a
|
|
92
|
-
* request costs a `stat` per file.
|
|
93
|
+
* request costs a `stat` per file. A root with a bundled snapshot (`useRootSnapshot`) answers its people.
|
|
93
94
|
*/
|
|
94
95
|
export function readPeople(root: string | undefined): DeclaredPeople {
|
|
95
96
|
if (root === undefined) return { people: [], problems: [] };
|
|
97
|
+
const bundled = rootSnapshot(root);
|
|
98
|
+
if (bundled) return bundled.people;
|
|
96
99
|
const loaded = peopleLoad(root);
|
|
97
100
|
const cached = peopleRead.get(loaded);
|
|
98
101
|
if (cached) return cached;
|
package/src/schema.ts
CHANGED
|
@@ -19,11 +19,24 @@ import { CHANGE_MERGE, DATA_VENDOR, EFFECTS, groupNamesOf, type PeopleResource,
|
|
|
19
19
|
// resolved config and the people, in memory for the evaluator, and written as text to
|
|
20
20
|
// `.sanoma/schema.cedarschema` for the policies' author.
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
/** Cedar's bindings: its builds (`/nodejs`, `/web`) export the same functions. */
|
|
23
|
+
export type CedarWasm = typeof import("@cedar-policy/cedar-wasm/nodejs");
|
|
24
|
+
let wasm: CedarWasm | undefined;
|
|
24
25
|
|
|
25
|
-
/**
|
|
26
|
-
|
|
26
|
+
/**
|
|
27
|
+
* `@cedar-policy/cedar-wasm`'s Node build, loaded (synchronously, once) on first use, so a process
|
|
28
|
+
* that never decides with Cedar never loads it; or the build `useCedarWasm` gave.
|
|
29
|
+
*/
|
|
30
|
+
export const cedarWasm = (): CedarWasm =>
|
|
31
|
+
(wasm ??= createRequire(import.meta.url)("@cedar-policy/cedar-wasm/nodejs") as CedarWasm);
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* From now on, Cedar is `bindings`: its web build, initialised, where the Node build cannot load
|
|
35
|
+
* (the Cloudflare host's Worker, which bundles the runtime).
|
|
36
|
+
*/
|
|
37
|
+
export function useCedarWasm(bindings: CedarWasm): void {
|
|
38
|
+
wasm = bindings;
|
|
39
|
+
}
|
|
27
40
|
|
|
28
41
|
/** The schema as Cedar's JSON format has it. */
|
|
29
42
|
export type CedarSchemaJson = SchemaJson<string>;
|
package/src/shared.ts
CHANGED
|
@@ -141,6 +141,26 @@ export const domainsText = (domains: Readonly<Record<string, string>>): string =
|
|
|
141
141
|
.map((site) => `${site}=${domains[site]}`)
|
|
142
142
|
.join(",");
|
|
143
143
|
|
|
144
|
+
/**
|
|
145
|
+
* Where a hosted container answers, with its instance token (`SANOMA_WAKEUP_TOKEN`), the bundle
|
|
146
|
+
* of its public sites that a host's Worker serves: the Cloudflare host downloads it at a deploy.
|
|
147
|
+
*/
|
|
148
|
+
export const SITES_BUNDLE_PATH = "/__sanoma/sites-bundle";
|
|
149
|
+
/** The header every answer of that route carries: a runtime without the route answers without it. */
|
|
150
|
+
export const SITES_BUNDLE_HEADER = "x-sanoma-sites-bundle";
|
|
151
|
+
|
|
152
|
+
/** What that route answers: what a host's Worker runs a company's public sites from (`buildWorker` in `@sanoma-ai/app/build`). */
|
|
153
|
+
export interface SitesBundle {
|
|
154
|
+
/** The sites it serves, by key: public, with a domain and no login. */
|
|
155
|
+
sites: string[];
|
|
156
|
+
/** The main module, among `modules`. */
|
|
157
|
+
mainModule: string;
|
|
158
|
+
/** The Worker's modules, by name relative to the main module, each base64. */
|
|
159
|
+
modules: { name: string; type: "esm" | "wasm" | "text" | "data"; content: string }[];
|
|
160
|
+
/** The Worker's static assets, by path from `/`, each base64: the client build and the prerendered pages. */
|
|
161
|
+
assets: { path: string; content: string }[];
|
|
162
|
+
}
|
|
163
|
+
|
|
144
164
|
/** Where a host with a `hostname` serves the app (`HostSpec`'s `hostname`): `https://<hostname>`. */
|
|
145
165
|
export const hostnameUrl = (hostname: string): string => `https://${hostname}`;
|
|
146
166
|
|