@nanobpm/urban 0.4.0 → 0.5.0

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.
Files changed (69) hide show
  1. package/README.md +4 -2
  2. package/dist/cli.d.ts +1 -0
  3. package/dist/cli.js +212 -0
  4. package/dist/index.d.ts +5 -0
  5. package/dist/index.js +25 -0
  6. package/dist/runtime/adapters/deno.d.ts +6 -0
  7. package/dist/runtime/adapters/deno.js +98 -0
  8. package/dist/runtime/adapters/detect.d.ts +9 -0
  9. package/dist/runtime/adapters/detect.js +13 -0
  10. package/dist/runtime/adapters/node.d.ts +7 -0
  11. package/dist/runtime/adapters/node.js +113 -0
  12. package/dist/runtime/core/context.d.ts +40 -0
  13. package/dist/runtime/core/context.js +1 -0
  14. package/dist/runtime/core/glob.d.ts +5 -0
  15. package/dist/runtime/core/glob.js +51 -0
  16. package/dist/runtime/core/host.d.ts +128 -0
  17. package/dist/runtime/core/host.js +5 -0
  18. package/dist/runtime/core/manifest.d.ts +83 -0
  19. package/dist/runtime/core/manifest.js +45 -0
  20. package/dist/runtime/core/modules/datasource.d.ts +38 -0
  21. package/dist/runtime/core/modules/datasource.js +172 -0
  22. package/dist/runtime/core/modules/deploy.d.ts +6 -0
  23. package/dist/runtime/core/modules/deploy.js +37 -0
  24. package/dist/runtime/core/modules/security.d.ts +11 -0
  25. package/dist/runtime/core/modules/security.js +32 -0
  26. package/dist/runtime/core/modules/surfaces.d.ts +9 -0
  27. package/dist/runtime/core/modules/surfaces.js +88 -0
  28. package/dist/runtime/core/modules/triggers.d.ts +15 -0
  29. package/dist/runtime/core/modules/triggers.js +119 -0
  30. package/dist/runtime/core/modules/workers.d.ts +16 -0
  31. package/dist/runtime/core/modules/workers.js +79 -0
  32. package/dist/runtime/core/router.d.ts +22 -0
  33. package/dist/runtime/core/router.js +39 -0
  34. package/dist/runtime/core/runtime.d.ts +46 -0
  35. package/dist/runtime/core/runtime.js +173 -0
  36. package/dist/runtime/core/validate.d.ts +13 -0
  37. package/dist/runtime/core/validate.js +108 -0
  38. package/dist/runtime/engine/nanosdk.d.ts +151 -0
  39. package/dist/runtime/engine/nanosdk.js +209 -0
  40. package/dist/runtime/engine/sdk.d.ts +3 -0
  41. package/dist/runtime/engine/sdk.js +13 -0
  42. package/dist/runtime/index.d.ts +21 -0
  43. package/dist/runtime/index.js +17 -0
  44. package/dist/runtime/run.d.ts +17 -0
  45. package/dist/runtime/run.js +69 -0
  46. package/dist/runtime/schema/nano-app.schema.json +471 -0
  47. package/dist/toolkit/artifact.d.ts +19 -0
  48. package/dist/toolkit/artifact.js +11 -0
  49. package/dist/toolkit/derivers/migrations.d.ts +26 -0
  50. package/dist/toolkit/derivers/migrations.js +73 -0
  51. package/dist/toolkit/derivers/worker-io.d.ts +38 -0
  52. package/dist/toolkit/derivers/worker-io.js +135 -0
  53. package/dist/toolkit/fsio.d.ts +2 -0
  54. package/dist/toolkit/fsio.js +35 -0
  55. package/dist/toolkit/gen.d.ts +28 -0
  56. package/dist/toolkit/gen.js +75 -0
  57. package/dist/toolkit/index.d.ts +13 -0
  58. package/dist/toolkit/index.js +15 -0
  59. package/package.json +19 -6
  60. package/src/bin.mjs +6 -11
  61. package/src/cli.ts +14 -1
  62. package/src/index.ts +7 -0
  63. package/src/runtime/core/context.ts +10 -0
  64. package/src/runtime/core/runtime.test.ts +46 -1
  65. package/src/runtime/core/runtime.ts +9 -0
  66. package/src/runtime/engine/nanosdk.test.ts +6 -0
  67. package/src/runtime/engine/nanosdk.ts +10 -0
  68. package/src/runtime/engine/sdk.ts +18 -0
  69. package/src/runtime/index.ts +1 -0
@@ -0,0 +1,128 @@
1
+ /** A minimal HTTP request as seen by a mounted surface/trigger handler. */
2
+ export interface HttpRequest {
3
+ method: string;
4
+ /** Path portion of the URL, e.g. "/tasks". */
5
+ path: string;
6
+ /** Parsed query parameters. */
7
+ query: URLSearchParams;
8
+ headers: Headers;
9
+ /** Read the raw request body as text (empty string when there is none). */
10
+ text(): Promise<string>;
11
+ }
12
+ /** A minimal HTTP response returned by a handler. */
13
+ export interface HttpResponse {
14
+ status?: number;
15
+ headers?: Record<string, string>;
16
+ body?: string;
17
+ }
18
+ export type HttpHandler = (req: HttpRequest) => Promise<HttpResponse> | HttpResponse;
19
+ /** A running HTTP server handle. */
20
+ export interface HttpServer {
21
+ readonly port: number;
22
+ stop(): Promise<void>;
23
+ }
24
+ /** A tiny synchronous SQLite handle — the subset the runtime needs. */
25
+ export interface SqliteDb {
26
+ /** Execute one or more statements with no result (DDL, PRAGMA, migrations). */
27
+ exec(sql: string): void;
28
+ /** Run a parameterised statement, returning the changed-row count. */
29
+ run(sql: string, params?: unknown[]): {
30
+ changes: number;
31
+ lastInsertRowid: number | bigint;
32
+ };
33
+ /** Run a query, returning all rows as plain objects. */
34
+ all<T = Record<string, unknown>>(sql: string, params?: unknown[]): T[];
35
+ close(): void;
36
+ }
37
+ /**
38
+ * HostContext is the runtime-agnostic capability surface. A Node adapter and a Deno
39
+ * adapter each implement it; core code depends only on this.
40
+ */
41
+ export interface HostContext {
42
+ /** Which concrete runtime is backing this host. */
43
+ readonly runtime: "node" | "deno";
44
+ /** Read an environment variable. */
45
+ env(name: string): string | undefined;
46
+ /** Read a UTF-8 text file relative to the app root. */
47
+ readTextFile(path: string): Promise<string>;
48
+ /** List file names (not directories) directly under `dir`. Returns [] if missing. */
49
+ listDir(dir: string): Promise<string[]>;
50
+ /** True if the path exists (file or directory). */
51
+ exists(path: string): Promise<boolean>;
52
+ /** Open (creating if needed) a SQLite database at a filesystem path. */
53
+ openSqlite(path: string): SqliteDb;
54
+ /**
55
+ * Dynamically import a handler/module by app-relative path. Path resolution differs
56
+ * between runtimes, so it lives behind the host seam.
57
+ */
58
+ importModule(path: string): Promise<Record<string, unknown>>;
59
+ /** Start an HTTP server. Routing is done by the caller inside `handler`. */
60
+ serveHttp(port: number, handler: HttpHandler): Promise<HttpServer>;
61
+ /** Current wall-clock time in ms since epoch (seam for tests). */
62
+ now(): number;
63
+ /** Structured log sink. */
64
+ log(level: "info" | "warn" | "error", msg: string, fields?: Record<string, unknown>): void;
65
+ }
66
+ /** A job handed to a worker handler, plus the ack/fail callbacks. */
67
+ export interface EngineJob {
68
+ jobKey: string;
69
+ jobType: string;
70
+ processInstanceKey?: string;
71
+ elementId?: string;
72
+ variables: Record<string, unknown>;
73
+ }
74
+ export type JobHandler = (job: EngineJob) => Promise<Record<string, unknown> | void> | Record<string, unknown> | void;
75
+ /** A registered worker subscription. */
76
+ export interface WorkerSubscription {
77
+ readonly jobType: string;
78
+ unsubscribe(): Promise<void>;
79
+ }
80
+ /**
81
+ * EngineClient is the seam onto a Nano engine. The SDK/REST-backed adapter implements
82
+ * it against a live engine; tests implement it in-memory. Core modules depend only on this.
83
+ */
84
+ export interface EngineClient {
85
+ /** Deploy model resources (BPMN/DMN/forms). Returns the number deployed. */
86
+ deployResources(resources: {
87
+ name: string;
88
+ content: string;
89
+ contentType: string;
90
+ }[]): Promise<{
91
+ deployed: number;
92
+ }>;
93
+ /** Start a process instance. */
94
+ createInstance(input: {
95
+ processDefinitionId: string;
96
+ variables?: Record<string, unknown>;
97
+ awaitCompletion?: boolean;
98
+ }): Promise<{
99
+ processInstanceKey: string;
100
+ variables?: Record<string, unknown>;
101
+ }>;
102
+ /** Publish a message for correlation. */
103
+ publishMessage(input: {
104
+ name: string;
105
+ correlationKey?: string;
106
+ variables?: Record<string, unknown>;
107
+ }): Promise<void>;
108
+ /** Search open user tasks (optionally by process instance). */
109
+ searchUserTasks(filter?: {
110
+ processInstanceKey?: string;
111
+ assignee?: string;
112
+ candidateGroup?: string;
113
+ }): Promise<{
114
+ userTaskKey: string;
115
+ elementId?: string;
116
+ variables?: Record<string, unknown>;
117
+ }[]>;
118
+ /** Complete a user task. */
119
+ completeUserTask(userTaskKey: string, variables?: Record<string, unknown>): Promise<void>;
120
+ /** Register a push worker for a job type. Draining is handled by the adapter. */
121
+ registerWorker(jobType: string, handler: JobHandler, options?: {
122
+ workerName?: string;
123
+ maxParallelJobs?: number;
124
+ fetchVariables?: string[];
125
+ }): Promise<WorkerSubscription>;
126
+ /** Tear down all connections. */
127
+ close(): Promise<void>;
128
+ }
@@ -0,0 +1,5 @@
1
+ // The host contract — the ONLY seam through which the runtime touches a concrete
2
+ // JavaScript runtime (Node or Deno) or the Nano engine. Everything under core/ is
3
+ // written against these interfaces and MUST NOT import `node:*` or reference `Deno`.
4
+ // The adapters/ directory supplies concrete implementations.
5
+ export {};
@@ -0,0 +1,83 @@
1
+ import type { HostContext } from "./host.ts";
2
+ export interface DataSource {
3
+ driver: string;
4
+ url: string;
5
+ migrations?: string;
6
+ }
7
+ export interface TypeField {
8
+ type: string;
9
+ optional?: boolean;
10
+ }
11
+ export interface DomainType {
12
+ name?: string;
13
+ table?: string;
14
+ fields?: Record<string, TypeField>;
15
+ }
16
+ export interface WorkerDecl {
17
+ /** Job/task type the worker subscribes to. Schema allows `taskType` or `type`. */
18
+ taskType?: string;
19
+ type?: string;
20
+ /** Path to the handler module, relative to the app root. Mutually exclusive with `llm`. */
21
+ handler?: string;
22
+ /** Name of an `llm[]` binding used as the worker (LLM-as-worker). Mutually exclusive with `handler`. */
23
+ llm?: string;
24
+ }
25
+ export interface TriggerDecl {
26
+ id: string;
27
+ type: string;
28
+ path?: string;
29
+ auth?: string;
30
+ action?: {
31
+ message?: string;
32
+ correlationKey?: string;
33
+ };
34
+ }
35
+ export interface SurfaceDecl {
36
+ enabled?: boolean;
37
+ path?: string;
38
+ agent?: string;
39
+ }
40
+ export interface AppManifest {
41
+ $schema?: string;
42
+ schemaVersion: 1;
43
+ id: string;
44
+ name: string;
45
+ codename?: string;
46
+ runtime?: {
47
+ engine?: string;
48
+ node?: string;
49
+ };
50
+ models?: {
51
+ processes?: string[];
52
+ decisions?: string[];
53
+ forms?: string[];
54
+ };
55
+ data?: {
56
+ default?: string;
57
+ sources?: Record<string, DataSource>;
58
+ };
59
+ types?: Record<string, DomainType>;
60
+ triggers?: TriggerDecl[];
61
+ connections?: Record<string, {
62
+ type: string;
63
+ [k: string]: unknown;
64
+ }>;
65
+ surfaces?: Record<string, SurfaceDecl>;
66
+ workers?: WorkerDecl[];
67
+ llm?: Record<string, unknown>;
68
+ security?: Record<string, unknown>;
69
+ [k: string]: unknown;
70
+ }
71
+ /** The normalised job type for a worker declaration (`taskType` preferred, `type` fallback). */
72
+ export declare function workerJobType(w: WorkerDecl): string | undefined;
73
+ /**
74
+ * Expand `${VAR}` and `${VAR:-default}` placeholders in a string using `lookup`.
75
+ * Unset vars with no default expand to "" (matching common shell semantics).
76
+ */
77
+ export declare function expandEnvString(input: string, lookup: (name: string) => string | undefined): string;
78
+ /** Recursively expand env placeholders across every string in a JSON-ish value. */
79
+ export declare function expandEnv<T>(value: T, lookup: (name: string) => string | undefined): T;
80
+ /** Parse a manifest from JSON text and expand env placeholders. Does not validate. */
81
+ export declare function parseManifest(json: string, lookup?: (name: string) => string | undefined): AppManifest;
82
+ /** Load and parse the manifest from the app root using the host (env-expanded). */
83
+ export declare function loadManifest(host: HostContext, manifestPath: string): Promise<AppManifest>;
@@ -0,0 +1,45 @@
1
+ // Manifest types (a hand-mirrored subset of nano-app.schema.json — the block shapes the
2
+ // runtime actually consumes) plus env-placeholder expansion and a host-driven loader.
3
+ // The schema itself (src/schema/nano-app.schema.json) remains the source of truth for
4
+ // validation; see validate.ts.
5
+ /** The normalised job type for a worker declaration (`taskType` preferred, `type` fallback). */
6
+ export function workerJobType(w) {
7
+ return w.taskType ?? w.type;
8
+ }
9
+ const ENV_RE = /\$\{([A-Za-z_][A-Za-z0-9_]*)(?::-([^}]*))?\}/g;
10
+ /**
11
+ * Expand `${VAR}` and `${VAR:-default}` placeholders in a string using `lookup`.
12
+ * Unset vars with no default expand to "" (matching common shell semantics).
13
+ */
14
+ export function expandEnvString(input, lookup) {
15
+ return input.replace(ENV_RE, (_m, name, dflt) => {
16
+ const v = lookup(name);
17
+ if (v !== undefined && v !== "")
18
+ return v;
19
+ return dflt ?? "";
20
+ });
21
+ }
22
+ /** Recursively expand env placeholders across every string in a JSON-ish value. */
23
+ export function expandEnv(value, lookup) {
24
+ if (typeof value === "string")
25
+ return expandEnvString(value, lookup);
26
+ if (Array.isArray(value))
27
+ return value.map((v) => expandEnv(v, lookup));
28
+ if (value && typeof value === "object") {
29
+ const out = {};
30
+ for (const [k, v] of Object.entries(value))
31
+ out[k] = expandEnv(v, lookup);
32
+ return out;
33
+ }
34
+ return value;
35
+ }
36
+ /** Parse a manifest from JSON text and expand env placeholders. Does not validate. */
37
+ export function parseManifest(json, lookup = () => undefined) {
38
+ const raw = JSON.parse(json);
39
+ return expandEnv(raw, lookup);
40
+ }
41
+ /** Load and parse the manifest from the app root using the host (env-expanded). */
42
+ export async function loadManifest(host, manifestPath) {
43
+ const text = await host.readTextFile(manifestPath);
44
+ return parseManifest(text, (n) => host.env(n));
45
+ }
@@ -0,0 +1,38 @@
1
+ import type { RuntimeContext } from "../context.ts";
2
+ import type { SqliteDb } from "../host.ts";
3
+ import type { DomainType } from "../manifest.ts";
4
+ /** A typed accessor over a table declared in manifest `types`. */
5
+ export declare class TypeRepo {
6
+ private readonly db;
7
+ readonly typeName: string;
8
+ private readonly def;
9
+ constructor(db: SqliteDb, typeName: string, def: DomainType);
10
+ private table;
11
+ private assertFields;
12
+ insert(row: Record<string, unknown>): {
13
+ changes: number;
14
+ lastInsertRowid: number | bigint;
15
+ };
16
+ all<T = Record<string, unknown>>(): T[];
17
+ query<T = Record<string, unknown>>(where: Record<string, unknown>): T[];
18
+ }
19
+ export interface ProvisionedSource {
20
+ readonly name: string;
21
+ readonly driver: string;
22
+ readonly db: SqliteDb;
23
+ readonly migrationsApplied: string[];
24
+ close(): void;
25
+ }
26
+ export declare class DataLayer {
27
+ private readonly sources;
28
+ private readonly defaultSource;
29
+ private readonly types;
30
+ constructor(sources: Map<string, ProvisionedSource>, defaultSource: string | undefined, types: Record<string, DomainType>);
31
+ source(name?: string): ProvisionedSource;
32
+ /** A typed accessor for a declared domain type (uses the default source). */
33
+ repo(typeName: string, sourceName?: string): TypeRepo;
34
+ describe(): Record<string, unknown>;
35
+ closeAll(): void;
36
+ }
37
+ /** Provision every declared source and return the typed data layer. */
38
+ export declare function provisionData(ctx: RuntimeContext): Promise<DataLayer>;
@@ -0,0 +1,172 @@
1
+ // datasource — provision the manifest `data` sources and expose typed accessors for the
2
+ // `types` domain model. Only the `sqlite` driver is implemented; other drivers hit an
3
+ // explicit seam (they are declared in the manifest but not yet provisioned). This is the
4
+ // runtime side of ADR 0024 (datasource) + ADR 0040 (domain model), scoped to what an app needs.
5
+ function sqlitePathFromUrl(url) {
6
+ // Accept "file:./x.db", "file:x.db", "sqlite:./x.db" or a bare path.
7
+ return url.replace(/^(file|sqlite):(\/\/)?/, "");
8
+ }
9
+ /** A safe unquoted SQL identifier. Table/column names are interpolated directly into SQL,
10
+ * so reject anything that isn't a plain identifier to prevent invalid SQL / injection. */
11
+ const SQL_IDENT = /^[A-Za-z_][A-Za-z0-9_]*$/;
12
+ function assertSqlIdent(kind, name) {
13
+ if (!SQL_IDENT.test(name)) {
14
+ throw new Error(`invalid ${kind} "${name}": must match ${SQL_IDENT.source}`);
15
+ }
16
+ return name;
17
+ }
18
+ function parentDir(path) {
19
+ const i = path.lastIndexOf("/");
20
+ return i > 0 ? path.slice(0, i) : "";
21
+ }
22
+ /** A typed accessor over a table declared in manifest `types`. */
23
+ export class TypeRepo {
24
+ db;
25
+ typeName;
26
+ def;
27
+ constructor(db, typeName, def) {
28
+ this.db = db;
29
+ this.typeName = typeName;
30
+ this.def = def;
31
+ }
32
+ table() {
33
+ if (!this.def.table)
34
+ throw new Error(`type "${this.typeName}" has no table`);
35
+ return assertSqlIdent("table name", this.def.table);
36
+ }
37
+ assertFields(row) {
38
+ const declared = this.def.fields;
39
+ if (!declared)
40
+ return;
41
+ for (const key of Object.keys(row)) {
42
+ if (!(key in declared)) {
43
+ throw new Error(`field "${key}" is not declared on type "${this.typeName}" (guards schema drift)`);
44
+ }
45
+ assertSqlIdent("field name", key);
46
+ }
47
+ }
48
+ insert(row) {
49
+ this.assertFields(row);
50
+ const keys = Object.keys(row);
51
+ if (keys.length === 0) {
52
+ return this.db.run(`INSERT INTO ${this.table()} DEFAULT VALUES`, []);
53
+ }
54
+ const cols = keys.join(", ");
55
+ const placeholders = keys.map(() => "?").join(", ");
56
+ const sql = `INSERT INTO ${this.table()} (${cols}) VALUES (${placeholders})`;
57
+ return this.db.run(sql, keys.map((k) => row[k]));
58
+ }
59
+ all() {
60
+ return this.db.all(`SELECT * FROM ${this.table()}`);
61
+ }
62
+ query(where) {
63
+ this.assertFields(where);
64
+ const keys = Object.keys(where);
65
+ if (keys.length === 0)
66
+ return this.all();
67
+ const clause = keys.map((k) => `${k} = ?`).join(" AND ");
68
+ return this.db.all(`SELECT * FROM ${this.table()} WHERE ${clause}`, keys.map((k) => where[k]));
69
+ }
70
+ }
71
+ export class DataLayer {
72
+ sources;
73
+ defaultSource;
74
+ types;
75
+ constructor(sources, defaultSource, types) {
76
+ this.sources = sources;
77
+ this.defaultSource = defaultSource;
78
+ this.types = types;
79
+ }
80
+ source(name) {
81
+ const key = name ?? this.defaultSource;
82
+ if (!key)
83
+ throw new Error("no data source specified and no default configured");
84
+ const s = this.sources.get(key);
85
+ if (!s)
86
+ throw new Error(`no such data source "${key}"`);
87
+ return s;
88
+ }
89
+ /** A typed accessor for a declared domain type (uses the default source). */
90
+ repo(typeName, sourceName) {
91
+ const def = this.types[typeName];
92
+ if (!def)
93
+ throw new Error(`no such type "${typeName}"`);
94
+ return new TypeRepo(this.source(sourceName).db, typeName, def);
95
+ }
96
+ describe() {
97
+ return {
98
+ default: this.defaultSource,
99
+ sources: [...this.sources.values()].map((s) => ({
100
+ name: s.name,
101
+ driver: s.driver,
102
+ migrations: s.migrationsApplied.length,
103
+ })),
104
+ };
105
+ }
106
+ closeAll() {
107
+ for (const s of this.sources.values())
108
+ s.close();
109
+ }
110
+ }
111
+ async function applyMigrations(host, db, root, migrationsDir) {
112
+ const dir = `${root.replace(/\/+$/, "")}/${migrationsDir.replace(/^\/+/, "")}`;
113
+ if (!(await host.exists(dir)))
114
+ return [];
115
+ const files = (await host.listDir(dir)).filter((f) => f.endsWith(".sql")).sort();
116
+ db.exec("CREATE TABLE IF NOT EXISTS _urban_migrations (name TEXT PRIMARY KEY, applied_at TEXT NOT NULL)");
117
+ const applied = new Set(db.all("SELECT name FROM _urban_migrations").map((r) => r.name));
118
+ const newlyApplied = [];
119
+ for (const file of files) {
120
+ if (applied.has(file))
121
+ continue;
122
+ const sql = await host.readTextFile(`${dir}/${file}`);
123
+ db.exec(sql);
124
+ db.run("INSERT INTO _urban_migrations (name, applied_at) VALUES (?, ?)", [
125
+ file,
126
+ new Date(host.now()).toISOString(),
127
+ ]);
128
+ newlyApplied.push(file);
129
+ }
130
+ return newlyApplied;
131
+ }
132
+ async function provisionSqlite(ctx, name, src) {
133
+ const dbPath = sqlitePathFromUrl(src.url);
134
+ const abs = dbPath.startsWith("/") ? dbPath : `${ctx.root.replace(/\/+$/, "")}/${dbPath}`;
135
+ const dir = parentDir(abs);
136
+ if (dir && !(await ctx.host.exists(dir))) {
137
+ // The host API doesn't expose mkdir and openSqlite won't create parent dirs,
138
+ // so fail fast with a clear, actionable message instead of a low-level
139
+ // "cannot open" error.
140
+ throw new Error(`datasource "${name}": directory "${dir}" does not exist — create it before running (the SQLite file cannot be opened otherwise)`);
141
+ }
142
+ const db = ctx.host.openSqlite(abs);
143
+ db.exec("PRAGMA journal_mode=WAL");
144
+ const migrationsApplied = src.migrations
145
+ ? await applyMigrations(ctx.host, db, ctx.root, src.migrations)
146
+ : [];
147
+ ctx.host.log("info", `datasource: provisioned "${name}"`, {
148
+ driver: "sqlite",
149
+ path: abs,
150
+ migrationsApplied,
151
+ });
152
+ return {
153
+ name,
154
+ driver: "sqlite",
155
+ db,
156
+ migrationsApplied,
157
+ close: () => db.close(),
158
+ };
159
+ }
160
+ /** Provision every declared source and return the typed data layer. */
161
+ export async function provisionData(ctx) {
162
+ const data = ctx.manifest.data;
163
+ const sources = new Map();
164
+ for (const [name, src] of Object.entries(data?.sources ?? {})) {
165
+ if (src.driver !== "sqlite") {
166
+ throw new Error(`data source "${name}" uses driver "${src.driver}"; only "sqlite" is implemented ` +
167
+ `(the driver seam is intentionally open for future drivers)`);
168
+ }
169
+ sources.set(name, await provisionSqlite(ctx, name, src));
170
+ }
171
+ return new DataLayer(sources, data?.default, ctx.manifest.types ?? {});
172
+ }
@@ -0,0 +1,6 @@
1
+ import type { RuntimeContext } from "../context.ts";
2
+ /** Deploy all processes, decisions and forms declared under `models`. */
3
+ export declare function deployModels(ctx: RuntimeContext): Promise<{
4
+ deployed: number;
5
+ files: string[];
6
+ }>;
@@ -0,0 +1,37 @@
1
+ // deploy — resolve the manifest `models` patterns and deploy the resources to the engine.
2
+ import { expandPatterns } from "../glob.js";
3
+ function contentTypeFor(path) {
4
+ if (path.endsWith(".bpmn"))
5
+ return "text/xml";
6
+ if (path.endsWith(".dmn"))
7
+ return "text/xml";
8
+ if (path.endsWith(".form"))
9
+ return "application/json";
10
+ return "application/octet-stream";
11
+ }
12
+ function baseName(path) {
13
+ const i = path.lastIndexOf("/");
14
+ return i >= 0 ? path.slice(i + 1) : path;
15
+ }
16
+ /** Deploy all processes, decisions and forms declared under `models`. */
17
+ export async function deployModels(ctx) {
18
+ const models = ctx.manifest.models ?? {};
19
+ const patterns = [
20
+ ...(models.processes ?? []),
21
+ ...(models.decisions ?? []),
22
+ ...(models.forms ?? []),
23
+ ];
24
+ const files = await expandPatterns(ctx.host, ctx.root, patterns);
25
+ if (files.length === 0) {
26
+ ctx.host.log("info", "deploy: no model files matched", { patterns });
27
+ return { deployed: 0, files: [] };
28
+ }
29
+ const resources = await Promise.all(files.map(async (path) => ({
30
+ name: baseName(path),
31
+ content: await ctx.host.readTextFile(path),
32
+ contentType: contentTypeFor(path),
33
+ })));
34
+ const { deployed } = await ctx.engine.deployResources(resources);
35
+ ctx.host.log("info", "deploy: models deployed", { deployed, files });
36
+ return { deployed, files };
37
+ }
@@ -0,0 +1,11 @@
1
+ import type { RuntimeContext } from "../context.ts";
2
+ export interface SecurityPolicy {
3
+ readonly mode: string;
4
+ readonly roles: string[];
5
+ /** Roles allowed to reach a surface path (empty/undefined ⇒ open). */
6
+ surfaceRoles(path: string): string[] | undefined;
7
+ /** Roles allowed to perform an action key (supports `prefix/*` rules). */
8
+ actionRoles(action: string): string[] | undefined;
9
+ describe(): Record<string, unknown>;
10
+ }
11
+ export declare function mountSecurity(ctx: RuntimeContext): SecurityPolicy;
@@ -0,0 +1,32 @@
1
+ // security — parse the manifest `security` block into a policy the runtime can consult
2
+ // (ADR 0028). Provider login (OIDC/password) is out of scope for this slice; what we
3
+ // materialize is the declarative role→action/surface/data rule set plus a `local` mode
4
+ // fallback, so hosts can enforce access consistently. Enforcement middleware can layer on
5
+ // this policy later.
6
+ function matchRule(rules, key) {
7
+ if (!rules)
8
+ return undefined;
9
+ if (rules[key])
10
+ return rules[key];
11
+ // support "prefix/*" wildcard rules
12
+ for (const [pat, roles] of Object.entries(rules)) {
13
+ if (pat.endsWith("/*") && key.startsWith(pat.slice(0, -1)))
14
+ return roles;
15
+ }
16
+ return undefined;
17
+ }
18
+ export function mountSecurity(ctx) {
19
+ const sec = (ctx.manifest.security ?? {});
20
+ const mode = sec.mode ?? "local";
21
+ const roles = sec.roles ?? [];
22
+ const surfaces = sec.rules?.surfaces;
23
+ const actions = sec.rules?.actions;
24
+ ctx.host.log("info", "security policy loaded", { mode, roles });
25
+ return {
26
+ mode,
27
+ roles,
28
+ surfaceRoles: (path) => matchRule(surfaces, path),
29
+ actionRoles: (action) => matchRule(actions, action),
30
+ describe: () => ({ mode, roles, surfaces: surfaces ?? {}, actions: actions ?? {} }),
31
+ };
32
+ }
@@ -0,0 +1,9 @@
1
+ import type { AppApi, RuntimeContext } from "../context.ts";
2
+ import { type Route } from "../router.ts";
3
+ export interface SurfacesHandle {
4
+ readonly name: string;
5
+ routes: Route[];
6
+ describe(): Record<string, unknown>;
7
+ }
8
+ /** Mount the enabled surfaces and return their routes. */
9
+ export declare function mountSurfaces(ctx: RuntimeContext, app: AppApi): SurfacesHandle;
@@ -0,0 +1,88 @@
1
+ // surfaces — the hosted human surfaces (ADR 0026). Implements the `taskInbox` surface: a
2
+ // small JSON+HTML task list backed by the engine's user-task search, that renders the
3
+ // linked `.form` and posts completion back. The `chat` surface is stubbed as a mount point
4
+ // (LLM wiring is a separate concern). Surfaces contribute routes to the shared server.
5
+ import { html, json, normalizeRoutePath } from "../router.js";
6
+ /** Escape HTML-significant characters before embedding a value in markup. */
7
+ function escapeHtml(s) {
8
+ return s.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
9
+ }
10
+ function inboxPage(basePath) {
11
+ return `<!doctype html><meta charset="utf-8"><title>Task inbox</title>
12
+ <style>body{font:14px system-ui,sans-serif;margin:2rem;max-width:52rem}
13
+ li{margin:.4rem 0}code{background:#f4f4f4;padding:.1rem .3rem;border-radius:3px}</style>
14
+ <h1>Task inbox</h1><ul id="tasks"><li>loading…</li></ul>
15
+ <script>
16
+ fetch(${JSON.stringify(basePath)}+'/api/tasks').then(r=>r.json()).then(ts=>{
17
+ const ul=document.getElementById('tasks');
18
+ ul.replaceChildren();
19
+ if(!ts.length){const li=document.createElement('li');li.textContent='No open tasks.';ul.appendChild(li);return;}
20
+ for(const t of ts){const li=document.createElement('li');
21
+ const code=document.createElement('code');
22
+ code.textContent=t.elementId||t.userTaskKey;
23
+ li.appendChild(code);
24
+ li.appendChild(document.createTextNode(' — key '+t.userTaskKey));
25
+ ul.appendChild(li);}
26
+ });
27
+ </script>`;
28
+ }
29
+ /** Mount the enabled surfaces and return their routes. */
30
+ export function mountSurfaces(ctx, app) {
31
+ const routes = [];
32
+ const surfaces = ctx.manifest.surfaces ?? {};
33
+ const enabled = [];
34
+ const inbox = surfaces.taskInbox;
35
+ if (inbox?.enabled) {
36
+ const base = normalizeRoutePath(inbox.path, "/tasks");
37
+ enabled.push(`taskInbox@${base}`);
38
+ routes.push({
39
+ method: "GET",
40
+ path: base,
41
+ source: "surface:taskInbox",
42
+ handler: () => html(inboxPage(base)),
43
+ });
44
+ routes.push({
45
+ method: "GET",
46
+ path: `${base}/api/tasks`,
47
+ source: "surface:taskInbox",
48
+ handler: async (req) => {
49
+ const pik = req.query.get("processInstanceKey") ?? undefined;
50
+ const tasks = await app.engine.searchUserTasks(pik ? { processInstanceKey: pik } : undefined);
51
+ return json(tasks);
52
+ },
53
+ });
54
+ routes.push({
55
+ method: "POST",
56
+ path: `${base}/api/complete`,
57
+ source: "surface:taskInbox",
58
+ handler: async (req) => {
59
+ let body;
60
+ try {
61
+ body = JSON.parse((await req.text()) || "{}");
62
+ }
63
+ catch {
64
+ return json({ error: "invalid JSON body" }, 400);
65
+ }
66
+ if (!body.userTaskKey)
67
+ return json({ error: "userTaskKey required" }, 400);
68
+ await app.engine.completeUserTask(body.userTaskKey, body.variables);
69
+ return json({ ok: true });
70
+ },
71
+ });
72
+ }
73
+ const chat = surfaces.chat;
74
+ if (chat?.enabled) {
75
+ const base = normalizeRoutePath(chat.path, "/chat");
76
+ enabled.push(`chat@${base}`);
77
+ routes.push({
78
+ method: "GET",
79
+ path: base,
80
+ source: "surface:chat",
81
+ handler: () => html(`<!doctype html><meta charset="utf-8"><title>Chat</title>
82
+ <body style="font:14px system-ui;margin:2rem"><h1>Chat</h1>
83
+ <p>Chat surface mount point (agent: <code>${escapeHtml(chat.agent ?? "?")}</code>). LLM wiring pending.</p>`),
84
+ });
85
+ }
86
+ ctx.host.log("info", "surfaces mounted", { enabled });
87
+ return { name: "surfaces", routes, describe: () => ({ enabled }) };
88
+ }