@sanoma-ai/workflows 0.11.1 → 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/README.md +2 -2
- package/dist/apply.d.ts +2 -2
- package/dist/apply.js +4 -2
- package/dist/apply.js.map +1 -1
- 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/constructors.d.ts +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/describe.js +27 -4
- package/dist/describe.js.map +1 -1
- package/dist/host.d.ts +14 -0
- package/dist/host.js +2 -0
- package/dist/host.js.map +1 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.js +7 -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 +29 -0
- package/dist/shared.js +12 -1
- package/dist/shared.js.map +1 -1
- package/dist/wakeup-type.d.ts +1 -1
- package/dist/wakeup-type.js +3 -1
- package/dist/wakeup-type.js.map +1 -1
- package/package.json +1 -1
- package/src/apply.ts +5 -4
- 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/describe.ts +25 -3
- package/src/host.ts +15 -0
- package/src/index.ts +7 -0
- package/src/load.ts +5 -2
- package/src/resources.ts +4 -1
- package/src/schema.ts +17 -4
- package/src/shared.ts +28 -1
- package/src/wakeup-type.ts +3 -1
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/describe.ts
CHANGED
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
type PeopleResource,
|
|
40
40
|
principalOf,
|
|
41
41
|
problemLine,
|
|
42
|
+
siteAtHostname,
|
|
42
43
|
} from "./shared.ts";
|
|
43
44
|
import { readSites, type SiteEntry } from "./sites.ts";
|
|
44
45
|
|
|
@@ -361,8 +362,9 @@ function appClashes(
|
|
|
361
362
|
|
|
362
363
|
/**
|
|
363
364
|
* Each site a host's `domains` (its type's `host.inputs.domains`) names that is not a public
|
|
364
|
-
* site,
|
|
365
|
-
*
|
|
365
|
+
* site, each domain it gives more than one site, and a domain that is the host's `hostname` too,
|
|
366
|
+
* or another host's hostname or domain, at the host's declaration: the app serves only a public
|
|
367
|
+
* site at a domain, one site at each, and itself at another, and one host holds each.
|
|
366
368
|
*/
|
|
367
369
|
function domainProblems(
|
|
368
370
|
declared: readonly DeclaredResource[],
|
|
@@ -371,9 +373,16 @@ function domainProblems(
|
|
|
371
373
|
): ResourceProblem[] {
|
|
372
374
|
const isPublic = new Set(sites.filter((site) => site.public).map((site) => site.name));
|
|
373
375
|
const problems: ResourceProblem[] = [];
|
|
376
|
+
// Each hostname a host serves the app at, and each host's domains, lowercased: one host attaches each.
|
|
377
|
+
const hostnames: [string, DeclaredResource][] = [];
|
|
378
|
+
const held = new Map<string, Set<DeclaredResource>>();
|
|
379
|
+
const hold = (name: string, resource: DeclaredResource) =>
|
|
380
|
+
held.set(name.toLowerCase(), (held.get(name.toLowerCase()) ?? new Set()).add(resource));
|
|
374
381
|
for (const resource of declared) {
|
|
375
|
-
const
|
|
382
|
+
const inputs = types.get(`${resource.vendor}.${resource.type}`)?.host?.inputs;
|
|
383
|
+
const path = inputs?.domains;
|
|
376
384
|
const domains = (path === undefined ? {} : (valueAt(resource.desired, path) ?? {})) as Record<string, unknown>;
|
|
385
|
+
const hostname = inputs?.hostname === undefined ? undefined : valueAt(resource.desired, inputs.hostname);
|
|
377
386
|
const messages = Object.keys(domains)
|
|
378
387
|
.filter((name) => !isPublic.has(name))
|
|
379
388
|
.map((name) => `domains names "${name}", which is not a public site (defineSite({ public: true }))`);
|
|
@@ -385,7 +394,20 @@ function domainProblems(
|
|
|
385
394
|
const others = `${names.length === 2 ? "both " : ""}${names.slice(0, -1).join(", ")}`;
|
|
386
395
|
messages.push(`${domain} is the domain of ${others} and ${names.at(-1)}`);
|
|
387
396
|
}
|
|
397
|
+
const site = typeof hostname === "string" ? siteAtHostname(hostname, domains) : undefined;
|
|
398
|
+
if (site) messages.push(`${hostname as string} is the app's hostname and the domain of ${site}`);
|
|
388
399
|
problems.push(...messages.map((message) => ({ ...resource.at, message })));
|
|
400
|
+
for (const [, domain] of named) hold(domain as string, resource);
|
|
401
|
+
if (typeof hostname === "string") {
|
|
402
|
+
hostnames.push([hostname, resource]);
|
|
403
|
+
hold(hostname, resource);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
for (const [hostname, resource] of hostnames) {
|
|
407
|
+
const others = [...(held.get(hostname.toLowerCase()) ?? [])].filter((other) => other !== resource);
|
|
408
|
+
if (others.length === 0) continue;
|
|
409
|
+
const message = `${hostname} is the app's hostname here and is served by ${others.map((o) => o.id).join(", ")} too`;
|
|
410
|
+
problems.push({ ...resource.at, message });
|
|
389
411
|
}
|
|
390
412
|
return problems;
|
|
391
413
|
}
|
package/src/host.ts
CHANGED
|
@@ -29,11 +29,20 @@ export interface HostCapabilities {
|
|
|
29
29
|
/** What a host's state says of it: where the app answers, which commit it runs, and whether it is up. */
|
|
30
30
|
export interface HostOutputs {
|
|
31
31
|
url: string;
|
|
32
|
+
/**
|
|
33
|
+
* Where a wakeup (and a check of its process) reaches it, `url` when left out: the vendor's own
|
|
34
|
+
* URL of a host whose `url` may be its `hostname`, so a wake never depends on a custom domain's DNS and certificate, and a
|
|
35
|
+
* hostname declared or dropped leaves its wakeup as it was. Apply gives a wakeup this one.
|
|
36
|
+
*/
|
|
37
|
+
wakeUrl?: string;
|
|
32
38
|
/** The commit it runs, when its state says. */
|
|
33
39
|
revision?: string;
|
|
34
40
|
healthy: boolean;
|
|
35
41
|
}
|
|
36
42
|
|
|
43
|
+
/** Where a wakeup, and a check of the host's own process, reaches it: its `wakeUrl`, else its `url`. */
|
|
44
|
+
export const wakeUrlOf = (outputs: HostOutputs): string => outputs.wakeUrl ?? outputs.url;
|
|
45
|
+
|
|
37
46
|
/**
|
|
38
47
|
* A resource type's `host`: the fields it takes the runtime's inputs in, by dotted path into its
|
|
39
48
|
* declared fields, and how its state reads as `HostOutputs`. A host is drifted when
|
|
@@ -55,6 +64,12 @@ export interface HostSpec {
|
|
|
55
64
|
port: string;
|
|
56
65
|
/** A public site's domain, by site name: the app serves each site at the root of its domain (`SANOMA_DOMAINS`). */
|
|
57
66
|
domains?: string;
|
|
67
|
+
/**
|
|
68
|
+
* The hostname the app is served at. A host with one reports `url` as `https://<hostname>` (its
|
|
69
|
+
* vendor's URL otherwise), so the links made from it use the stable name, and its vendor's URL
|
|
70
|
+
* as `wakeUrl`, where its wakeup reaches it. It is never one of `domains`' values.
|
|
71
|
+
*/
|
|
72
|
+
hostname?: string;
|
|
58
73
|
};
|
|
59
74
|
/**
|
|
60
75
|
* The field apply fills with the commit `ref` resolves to, before it plans: hidden, as a data
|
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,
|
|
@@ -203,6 +204,7 @@ export {
|
|
|
203
204
|
type SourceHost,
|
|
204
205
|
} from "./source.ts";
|
|
205
206
|
export type { HostCapabilities, HostLifetime, HostOutputs, HostSpec } from "./host.ts";
|
|
207
|
+
export { wakeUrlOf } from "./host.ts";
|
|
206
208
|
export {
|
|
207
209
|
checkDatabase,
|
|
208
210
|
type DatabaseCapabilities,
|
|
@@ -241,6 +243,11 @@ export {
|
|
|
241
243
|
} from "./client.ts";
|
|
242
244
|
// A raw DBOS client on the runtime's database, for tests and tools that read DBOS directly.
|
|
243
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";
|
|
244
251
|
// Also at `@sanoma-ai/workflows/shared`, which a browser bundle can import.
|
|
245
252
|
export {
|
|
246
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,33 @@ 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
|
+
|
|
164
|
+
/** Where a host with a `hostname` serves the app (`HostSpec`'s `hostname`): `https://<hostname>`. */
|
|
165
|
+
export const hostnameUrl = (hostname: string): string => `https://${hostname}`;
|
|
166
|
+
|
|
167
|
+
/** The site whose domain is also the app's `hostname`, compared in lower case; undefined when none is. */
|
|
168
|
+
export const siteAtHostname = (hostname: string, domains: Readonly<Record<string, unknown>>): string | undefined =>
|
|
169
|
+
Object.entries(domains).find(([, domain]) => typeof domain === "string" && same(domain, hostname))?.[0];
|
|
170
|
+
|
|
144
171
|
/** The domains `domainsText` wrote, by site name; throws for a pair that is not `<site>=<hostname>`, or a site named twice. */
|
|
145
172
|
export function domainsOf(text: string): Record<string, string> {
|
|
146
173
|
const domains: Record<string, string> = {};
|
|
@@ -469,7 +496,7 @@ export function approverOf(name: string, resources: readonly PeopleResource[]):
|
|
|
469
496
|
return principalOf(name, resources) && name;
|
|
470
497
|
}
|
|
471
498
|
|
|
472
|
-
/** True for two strings the same but for case: an email
|
|
499
|
+
/** True for two strings the same but for case: an email, a GitHub login or a hostname. */
|
|
473
500
|
const same = (a: string | undefined, b: string | undefined) =>
|
|
474
501
|
a !== undefined && b !== undefined && a.toLowerCase() === b.toLowerCase();
|
|
475
502
|
|
package/src/wakeup-type.ts
CHANGED
|
@@ -49,7 +49,9 @@ export const wakeup = defineResource({
|
|
|
49
49
|
immutable: [],
|
|
50
50
|
vendorOwned: ["id", "at", "lastError"],
|
|
51
51
|
writeOnly: [],
|
|
52
|
-
|
|
52
|
+
// Where each host's wakeup reaches it, as apply gives it (`wakeUrlOf`): a Cloudflare host's
|
|
53
|
+
// `worker_url` once its `url` is its hostname, else its `url`.
|
|
54
|
+
references: { host: { type: HOSTS, by: ["worker_url", "url"] } },
|
|
53
55
|
},
|
|
54
56
|
writable: true,
|
|
55
57
|
// The service holds its registered wake: apply asks before it deletes one.
|