@nanobpm/urban 0.94.2 → 0.96.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.
- package/dist/runtime/adapters/deno.js +30 -0
- package/dist/runtime/adapters/node.js +31 -1
- package/dist/runtime/core/host.d.ts +54 -11
- package/dist/runtime/core/modules/mcp.js +5 -1
- package/dist/runtime/core/modules/pages.d.ts +17 -0
- package/dist/runtime/core/modules/pages.js +54 -10
- package/dist/runtime/core/process-identity.d.ts +21 -0
- package/dist/runtime/core/process-identity.js +56 -0
- package/dist/runtime/engine/nanosdk.d.ts +3 -5
- package/dist/runtime/engine/nanosdk.js +12 -16
- package/dist/runtime/index.d.ts +2 -1
- package/dist/runtime/index.js +5 -0
- package/package.json +2 -2
- package/src/runtime/adapters/deno.ts +32 -1
- package/src/runtime/adapters/node.test.ts +153 -0
- package/src/runtime/adapters/node.ts +27 -1
- package/src/runtime/core/glob.test.ts +6 -0
- package/src/runtime/core/host.ts +54 -8
- package/src/runtime/core/modules/actions.test.ts +2 -0
- package/src/runtime/core/modules/agent.test.ts +1 -0
- package/src/runtime/core/modules/api.test.ts +2 -0
- package/src/runtime/core/modules/connectors.test.ts +1 -0
- package/src/runtime/core/modules/datasource.test.ts +1 -0
- package/src/runtime/core/modules/mcp.test.ts +25 -0
- package/src/runtime/core/modules/mcp.ts +7 -2
- package/src/runtime/core/modules/pages.test.ts +136 -1
- package/src/runtime/core/modules/pages.ts +75 -11
- package/src/runtime/core/modules/surfaces.test.ts +1 -0
- package/src/runtime/core/modules/triggers.test.ts +1 -0
- package/src/runtime/core/modules/workers.test.ts +1 -0
- package/src/runtime/core/process-identity.test.ts +72 -0
- package/src/runtime/core/process-identity.ts +66 -0
- package/src/runtime/core/runtime.test.ts +27 -0
- package/src/runtime/devserver.test.ts +1 -0
- package/src/runtime/engine/nanosdk.test.ts +82 -0
- package/src/runtime/engine/nanosdk.ts +18 -16
- package/src/runtime/index.ts +11 -0
|
@@ -94,6 +94,36 @@ export function createDenoHost(opts = {}) {
|
|
|
94
94
|
return false;
|
|
95
95
|
}
|
|
96
96
|
},
|
|
97
|
+
async statFile(p) {
|
|
98
|
+
// Classify with a bare `stat` FIRST, then only open a *regular file* to confirm it is
|
|
99
|
+
// readable. The order matters: a read-only open of a FIFO (named pipe) blocks indefinitely
|
|
100
|
+
// waiting for a writer, so opening before the type check would hang every shell request on
|
|
101
|
+
// a FIFO sitting at this conventional asset path. A plain `stat` never blocks, so it safely
|
|
102
|
+
// rejects non-regular entries (directory, FIFO, socket, device) up front. The subsequent
|
|
103
|
+
// open/stat still answers the real serving condition — a file `stat` reports as regular can
|
|
104
|
+
// be unreadable (ACLs / permissions), and the shell must not link an asset that then 404s.
|
|
105
|
+
// Missing path → `null`; non-regular → `isFile: false`; unreadable → `null`.
|
|
106
|
+
try {
|
|
107
|
+
const meta = await Deno.stat(abs(p));
|
|
108
|
+
if (!meta.isFile)
|
|
109
|
+
return { isFile: false };
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
let file;
|
|
115
|
+
try {
|
|
116
|
+
file = await Deno.open(abs(p), { read: true });
|
|
117
|
+
const s = await file.stat();
|
|
118
|
+
return { isFile: s.isFile };
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
finally {
|
|
124
|
+
file?.close();
|
|
125
|
+
}
|
|
126
|
+
},
|
|
97
127
|
openSqlite(path) {
|
|
98
128
|
const db = new DatabaseSync(abs(path));
|
|
99
129
|
return wrapSqlite(db);
|
|
@@ -8,7 +8,7 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
8
8
|
}
|
|
9
9
|
return path;
|
|
10
10
|
};
|
|
11
|
-
import { readFile, readdir, stat } from "node:fs/promises";
|
|
11
|
+
import { open, readFile, readdir, stat } from "node:fs/promises";
|
|
12
12
|
import { existsSync } from "node:fs";
|
|
13
13
|
import { watch as fsWatch } from "node:fs";
|
|
14
14
|
import { createServer } from "node:http";
|
|
@@ -133,6 +133,36 @@ export function createNodeHost(opts = {}) {
|
|
|
133
133
|
return false;
|
|
134
134
|
}
|
|
135
135
|
},
|
|
136
|
+
async statFile(p) {
|
|
137
|
+
// Classify with a bare `stat` FIRST, then only open a *regular file* to confirm it is
|
|
138
|
+
// readable. The order matters: a read-only open of a FIFO (named pipe) blocks indefinitely
|
|
139
|
+
// waiting for a writer, so opening before the type check would hang every shell request on
|
|
140
|
+
// a FIFO sitting at this conventional asset path. A plain `stat` never blocks, so it safely
|
|
141
|
+
// rejects non-regular entries (directory, FIFO, socket, device) up front. The subsequent
|
|
142
|
+
// handle open/stat still answers the real serving condition — a file `stat` reports as
|
|
143
|
+
// regular can be unreadable (ACLs / permissions), and the shell must not link an asset that
|
|
144
|
+
// then 404s. Missing path → `null`; non-regular → `isFile: false`; unreadable → `null`.
|
|
145
|
+
try {
|
|
146
|
+
const meta = await stat(abs(p));
|
|
147
|
+
if (!meta.isFile())
|
|
148
|
+
return { isFile: false };
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
let handle;
|
|
154
|
+
try {
|
|
155
|
+
handle = await open(abs(p), "r");
|
|
156
|
+
const s = await handle.stat();
|
|
157
|
+
return { isFile: s.isFile() };
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
finally {
|
|
163
|
+
await handle?.close();
|
|
164
|
+
}
|
|
165
|
+
},
|
|
136
166
|
openSqlite(path) {
|
|
137
167
|
const db = new DatabaseSync(abs(path));
|
|
138
168
|
return wrapNodeSqlite(db);
|
|
@@ -80,6 +80,27 @@ export interface HostContext {
|
|
|
80
80
|
listSubdirs?(dir: string): Promise<string[]>;
|
|
81
81
|
/** True if the path exists (file or directory). */
|
|
82
82
|
exists(path: string): Promise<boolean>;
|
|
83
|
+
/**
|
|
84
|
+
* Probe a path as a READABLE REGULAR FILE without reading its bytes: `{ isFile }` when it
|
|
85
|
+
* can be opened for reading, or `null` when it is absent or unreadable. The pages surface
|
|
86
|
+
* uses this to probe the app-owned `app.css`/`app.js` convention on the per-request shell
|
|
87
|
+
* hot path: `exists` is too broad (true for a DIRECTORY, which the shell would then link and
|
|
88
|
+
* the browser would 404 on), and probing via `readTextFile` reads and allocates a
|
|
89
|
+
* potentially large file just to answer "is it there?". A bare metadata `stat` is not
|
|
90
|
+
* enough either — it can succeed for a regular file that `readTextFile` cannot open (ACLs /
|
|
91
|
+
* permissions), so the shell would link an asset that then 404s on serving. Implementations
|
|
92
|
+
* therefore OPEN the file for reading (consuming no bytes) and stat the open handle, so this
|
|
93
|
+
* answers the same success condition as serving.
|
|
94
|
+
*
|
|
95
|
+
* OPTIONAL: `HostContext` is a public, externally-implementable seam (re-exported from
|
|
96
|
+
* `runtime/index.ts`, documented in the README as a supported custom-host extension point),
|
|
97
|
+
* so a new required member would be source-breaking. The pages surface falls back to a
|
|
98
|
+
* read-based probe when a host does not provide it; the built-in adapters do, as an
|
|
99
|
+
* allocation-free optimization on the hot path.
|
|
100
|
+
*/
|
|
101
|
+
statFile?(path: string): Promise<{
|
|
102
|
+
isFile: boolean;
|
|
103
|
+
} | null>;
|
|
83
104
|
/** Open (creating if needed) a SQLite database at a filesystem path. */
|
|
84
105
|
openSqlite(path: string): SqliteDb;
|
|
85
106
|
/**
|
|
@@ -195,14 +216,33 @@ export type ProcessInstanceState = "ACTIVE" | "COMPLETED" | "TERMINATED";
|
|
|
195
216
|
* never surface in the `taskInbox` as if it were actionable.
|
|
196
217
|
*/
|
|
197
218
|
export type UserTaskState = "CREATED" | "COMPLETED" | "CANCELED" | "FAILED";
|
|
198
|
-
/**
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
219
|
+
/**
|
|
220
|
+
* Which deployed process definition an instance runs — shared by the instance a
|
|
221
|
+
* {@link EngineClient.createInstance} starts and the {@link ProcessInstanceSnapshot} a
|
|
222
|
+
* {@link EngineClient.searchProcessInstances} reports, so the two describe the same instance
|
|
223
|
+
* identically. Each field is absent when the engine omits it (or reports it blank/malformed).
|
|
224
|
+
*/
|
|
225
|
+
export interface ProcessDefinitionIdentity {
|
|
202
226
|
/** The key of the deployed process definition this instance runs — the handle a caller feeds to
|
|
203
227
|
* {@link EngineClient.getProcessDefinitionXml} to fetch the deployed BPMN (the instance →
|
|
204
|
-
* `processDefinitionKey` → deployed XML path, no repo checkout).
|
|
228
|
+
* `processDefinitionKey` → deployed XML path, no repo checkout). Unique per deployed version. */
|
|
205
229
|
readonly processDefinitionKey?: string;
|
|
230
|
+
/** The BPMN process id (`<bpmn:process id>`) — stable across versions; the selector
|
|
231
|
+
* {@link EngineClient.searchProcessInstances} accepts as `processDefinitionId`. */
|
|
232
|
+
readonly processDefinitionId?: string;
|
|
233
|
+
/** The deployed version of `processDefinitionId` this instance runs (1-based). */
|
|
234
|
+
readonly processDefinitionVersion?: number;
|
|
235
|
+
}
|
|
236
|
+
/** The instance {@link EngineClient.createInstance} started: its key, the exact deployed
|
|
237
|
+
* definition version it runs, and — when `awaitCompletion` was requested — its final variables. */
|
|
238
|
+
export interface CreatedProcessInstance extends ProcessDefinitionIdentity {
|
|
239
|
+
readonly processInstanceKey: string;
|
|
240
|
+
readonly variables?: Record<string, unknown>;
|
|
241
|
+
}
|
|
242
|
+
/** A single process instance's lifecycle snapshot returned by {@link EngineClient.searchProcessInstances}. */
|
|
243
|
+
export interface ProcessInstanceSnapshot extends ProcessDefinitionIdentity {
|
|
244
|
+
readonly processInstanceKey: string;
|
|
245
|
+
readonly state: ProcessInstanceState;
|
|
206
246
|
/** The key of this instance's *immediate* parent process instance — the caller that instantiated
|
|
207
247
|
* it as a native child (a `<bpmn:callActivity>`). `undefined` for a top-level (root) instance,
|
|
208
248
|
* and absent when the engine omits it (same optional/engine-provided convention as
|
|
@@ -643,15 +683,14 @@ export interface EngineClient {
|
|
|
643
683
|
}[]): Promise<{
|
|
644
684
|
deployed: number;
|
|
645
685
|
}>;
|
|
646
|
-
/** Start a process instance
|
|
686
|
+
/** Start a process instance of the latest deployed version of `processDefinitionId`. Returns
|
|
687
|
+
* the new instance's key plus the {@link ProcessDefinitionIdentity} (key, id, version) of the
|
|
688
|
+
* definition it actually runs. */
|
|
647
689
|
createInstance(input: {
|
|
648
690
|
processDefinitionId: string;
|
|
649
691
|
variables?: Record<string, unknown>;
|
|
650
692
|
awaitCompletion?: boolean;
|
|
651
|
-
}): Promise<
|
|
652
|
-
processInstanceKey: string;
|
|
653
|
-
variables?: Record<string, unknown>;
|
|
654
|
-
}>;
|
|
693
|
+
}): Promise<CreatedProcessInstance>;
|
|
655
694
|
/** Cancel a running process instance (the pages surface's row-cancel action). */
|
|
656
695
|
cancelInstance(input: {
|
|
657
696
|
processInstanceKey: string;
|
|
@@ -704,7 +743,8 @@ export interface EngineClient {
|
|
|
704
743
|
/** Complete a user task. */
|
|
705
744
|
completeUserTask(userTaskKey: string, variables?: Record<string, unknown>): Promise<void>;
|
|
706
745
|
/**
|
|
707
|
-
* Search process instances by key
|
|
746
|
+
* Search process instances by key, BPMN process id (`processDefinitionId`, any version),
|
|
747
|
+
* and/or lifecycle state. The instance-tracking
|
|
708
748
|
* reconciler uses this to detect a tracked instance reaching a terminal state
|
|
709
749
|
* (`TERMINATED`/`COMPLETED`) even though no completion worker ran for it — the
|
|
710
750
|
* read-model row it backs would otherwise stay "active" forever. An eventually
|
|
@@ -714,6 +754,9 @@ export interface EngineClient {
|
|
|
714
754
|
searchProcessInstances(filter?: {
|
|
715
755
|
processInstanceKeys?: string[];
|
|
716
756
|
state?: ProcessInstanceState;
|
|
757
|
+
/** Select only instances of this BPMN process id (every deployed version). A blank or
|
|
758
|
+
* whitespace-only value is treated as absent (no filter). */
|
|
759
|
+
processDefinitionId?: string;
|
|
717
760
|
/** Select only instances whose *immediate* parent is this process instance — the reduced-path
|
|
718
761
|
* query for "the native children spawned by this subject". */
|
|
719
762
|
parentProcessInstanceKey?: string;
|
|
@@ -428,11 +428,12 @@ function buildDebugTools(app) {
|
|
|
428
428
|
return [
|
|
429
429
|
{
|
|
430
430
|
name: `${DEBUG_PREFIX}search_process_instances`,
|
|
431
|
-
description: "Engine truth: search process instances by key and/or lifecycle state.",
|
|
431
|
+
description: "Engine truth: search process instances by key, BPMN process id (processDefinitionId, any version), and/or lifecycle state.",
|
|
432
432
|
inputSchema: {
|
|
433
433
|
type: "object",
|
|
434
434
|
properties: {
|
|
435
435
|
processInstanceKeys: { type: "array", items: OPTIONAL_STRING },
|
|
436
|
+
processDefinitionId: OPTIONAL_STRING,
|
|
436
437
|
state: { type: "string", enum: ["ACTIVE", "COMPLETED", "TERMINATED"] },
|
|
437
438
|
},
|
|
438
439
|
},
|
|
@@ -444,6 +445,9 @@ function buildDebugTools(app) {
|
|
|
444
445
|
const state = readProcessInstanceState(args, "state");
|
|
445
446
|
if (state)
|
|
446
447
|
filter.state = state;
|
|
448
|
+
const processDefinitionId = readPresentString(args, "processDefinitionId");
|
|
449
|
+
if (processDefinitionId)
|
|
450
|
+
filter.processDefinitionId = processDefinitionId;
|
|
447
451
|
return app.engine.searchProcessInstances(filter);
|
|
448
452
|
},
|
|
449
453
|
},
|
|
@@ -50,6 +50,21 @@ export interface PagesDeps {
|
|
|
50
50
|
* Injectable for tests. Rejects (→ 404) when the file is missing.
|
|
51
51
|
*/
|
|
52
52
|
readAsset?(path: string): Promise<string>;
|
|
53
|
+
/**
|
|
54
|
+
* Probe whether a static asset exists as a READABLE REGULAR FILE at an app-root-relative
|
|
55
|
+
* path, WITHOUT reading its bytes. The per-request shell build uses this for the app-owned
|
|
56
|
+
* `app.css`/`app.js` convention (#578): those files can be large and the shell is revalidated
|
|
57
|
+
* on every load, so probing via a full `readAsset` would put an avoidable read + allocation
|
|
58
|
+
* on the HTML hot path (and read the file twice — once here, once when the browser fetches
|
|
59
|
+
* it). Must answer the same question the `readAsset` fallback answers — "would serving this
|
|
60
|
+
* path succeed?" — so a directory (or unreadable entry) at the path is a NO. `mountPages`
|
|
61
|
+
* wires this to the optional `HostContext.statFile` (which opens the file for reading to
|
|
62
|
+
* confirm a readable regular file — never `HostContext.exists`, which is true for directories
|
|
63
|
+
* too, and a `pages/app.css/` DIRECTORY would link an asset the browser then 404s on).
|
|
64
|
+
* Optional; falls back to a read-based probe when absent.
|
|
65
|
+
* Injectable for tests.
|
|
66
|
+
*/
|
|
67
|
+
existsAsset?(path: string): Promise<boolean>;
|
|
53
68
|
/**
|
|
54
69
|
* List the available page ids (the `*.page.json` basenames under `pagesDir`),
|
|
55
70
|
* powering the `/app/pages` index endpoint and a `nav` node's `items: "auto"`.
|
|
@@ -64,6 +79,7 @@ export interface PagesDeps {
|
|
|
64
79
|
*/
|
|
65
80
|
cancel?(processInstanceKey: string): Promise<CancelInstanceResult>;
|
|
66
81
|
}
|
|
82
|
+
export declare const APP_ASSETS: readonly ["app.css", "app.js"];
|
|
67
83
|
/**
|
|
68
84
|
* Build the page-runtime routes over an injected datasource + engine. Pure over its deps
|
|
69
85
|
* so it is unit-testable without a real host/server (see pages.test.ts).
|
|
@@ -80,3 +96,4 @@ export interface PagesHandle {
|
|
|
80
96
|
* Returns no routes when the surface is disabled or absent.
|
|
81
97
|
*/
|
|
82
98
|
export declare function mountPages(ctx: RuntimeContext, app: AppApi): PagesHandle;
|
|
99
|
+
export declare const RUNTIME_JS_PATH: string;
|
|
@@ -46,6 +46,12 @@ function javascript(body, status = 200, extraHeaders) {
|
|
|
46
46
|
body,
|
|
47
47
|
};
|
|
48
48
|
}
|
|
49
|
+
// App-owned shell customisation by convention (nano-ide#578): an app MAY ship `pagesDir/app.css`
|
|
50
|
+
// (linked AFTER the built-in renderer CSS, so its rules win — themes, restyling) and/or
|
|
51
|
+
// `pagesDir/app.js` (an ES module loaded after the runtime — e.g. a theme picker). Each is served
|
|
52
|
+
// as a root sidecar and referenced from the shell ONLY when the file exists, so apps without them
|
|
53
|
+
// see no change and no 404.
|
|
54
|
+
export const APP_ASSETS = ["app.css", "app.js"];
|
|
49
55
|
// The `appView` sidecar documents an app ships alongside its `*.page.json` under
|
|
50
56
|
// `pagesDir` (ADR 0057 / #416 / #420). The renderer (#419) mounts an <iframe> whose
|
|
51
57
|
// src is the node's base-relative `embed` (`./embed.html`), which — resolved against
|
|
@@ -58,7 +64,7 @@ function javascript(body, status = 200, extraHeaders) {
|
|
|
58
64
|
// `./board/embed.html`, …) — those are served by the nested prefix route below (#442);
|
|
59
65
|
// this flat set stays as the single-appView back-compat case (#420). The `dist/` tree the
|
|
60
66
|
// import-map references is served separately under the `/dist/` prefix below.
|
|
61
|
-
const SIDECAR_ASSETS = ["embed.html", "standalone.html", "mount.js", "cockpit.css"];
|
|
67
|
+
const SIDECAR_ASSETS = ["embed.html", "standalone.html", "mount.js", "cockpit.css", ...APP_ASSETS];
|
|
62
68
|
// The app `dist/` directory (app-root sibling of the pages dir) the sidecar import-maps
|
|
63
69
|
// resolve `../dist/…` onto. Because the embed is served at the mount root, `../dist`
|
|
64
70
|
// resolves to `/dist` regardless of `pagesDir`, so the URL→disk mapping is fixed.
|
|
@@ -151,6 +157,9 @@ export function createPagesRoutes(opts, deps) {
|
|
|
151
157
|
// Falls back to `readPage` when no dedicated reader is injected — both delegate to the same
|
|
152
158
|
// `host.readTextFile`, so there is one source of file bytes, not two.
|
|
153
159
|
const readAsset = deps.readAsset ?? readPage;
|
|
160
|
+
// Existence probe for the app-owned shell assets (#578): a dedicated `existsAsset` when the
|
|
161
|
+
// host offers one (a metadata stat — no file bytes cross the seam), else a read-based probe.
|
|
162
|
+
const existsAsset = deps.existsAsset ?? ((path) => readAsset(path).then(() => true, () => false));
|
|
154
163
|
// The table-name whitelist is memoised: an Urban app runs its migrations at boot
|
|
155
164
|
// (before serving), so the schema is stable for the process lifetime, and the renderer
|
|
156
165
|
// refreshes grids repeatedly — re-introspecting on every `/app/data` hit would be a hot
|
|
@@ -181,15 +190,24 @@ export function createPagesRoutes(opts, deps) {
|
|
|
181
190
|
return p;
|
|
182
191
|
};
|
|
183
192
|
const routes = [];
|
|
184
|
-
const shell = html(rendererShell(homePage, opts.apiDocsPath, opts.gridLayout ?? "auto"));
|
|
185
193
|
// The shell must never be pinned by the browser: it carries the *current* fingerprinted
|
|
186
194
|
// runtime URL, so a stale shell would keep pointing at an old module hash. `no-cache` forces
|
|
187
195
|
// a revalidation on every load; the shell body is tiny, and the expensive module it references
|
|
188
196
|
// is what gets the long-lived immutable caching (via its content-hashed URL).
|
|
189
|
-
|
|
197
|
+
// Built per request: it probes for the app-owned `app.css`/`app.js` (#578) so adding or removing
|
|
198
|
+
// one takes effect without a restart.
|
|
199
|
+
const shell = async () => {
|
|
200
|
+
const [appCss, appJs] = await Promise.all([
|
|
201
|
+
existsAsset(`${pagesDir}/app.css`),
|
|
202
|
+
existsAsset(`${pagesDir}/app.js`),
|
|
203
|
+
]);
|
|
204
|
+
const res = html(rendererShell(homePage, opts.apiDocsPath, opts.gridLayout ?? "auto", { appCss, appJs }));
|
|
205
|
+
res.headers = { ...res.headers, "cache-control": "no-cache" };
|
|
206
|
+
return res;
|
|
207
|
+
};
|
|
190
208
|
// ── the renderer shell + module ─────────────────────────────────────────
|
|
191
|
-
routes.push({ method: "GET", path: "/", source: "surface:pages", handler:
|
|
192
|
-
routes.push({ method: "GET", path: "/index.html", source: "surface:pages", handler:
|
|
209
|
+
routes.push({ method: "GET", path: "/", source: "surface:pages", handler: shell });
|
|
210
|
+
routes.push({ method: "GET", path: "/index.html", source: "surface:pages", handler: shell });
|
|
193
211
|
// The fingerprinted module URL (referenced by the shell): unique per content, so cache it hard.
|
|
194
212
|
routes.push({
|
|
195
213
|
method: "GET",
|
|
@@ -216,11 +234,22 @@ export function createPagesRoutes(opts, deps) {
|
|
|
216
234
|
// onto `pagesDir/<name>` (exact routes — never a root catch-all, which would shadow the
|
|
217
235
|
// `/healthz` liveness route the runtime appends after the pages surface).
|
|
218
236
|
for (const name of SIDECAR_ASSETS) {
|
|
237
|
+
// The app-owned `app.css`/`app.js` are MUTABLE at stable URLs — the whole point of the
|
|
238
|
+
// convention is that an edit (or a remove/re-add) takes effect without a restart, and the
|
|
239
|
+
// shell re-probes on every load. Serve them `no-cache` (like `/app/runtime.js`) so a
|
|
240
|
+
// browser/intermediary must revalidate instead of replaying a heuristically-cached 200/404.
|
|
241
|
+
// The other sidecars are deploy-time fixtures, so they keep the generic response.
|
|
242
|
+
const mutable = APP_ASSETS.some((a) => a === name);
|
|
219
243
|
routes.push({
|
|
220
244
|
method: "GET",
|
|
221
245
|
path: `/${name}`,
|
|
222
246
|
source: "surface:pages",
|
|
223
|
-
handler: () =>
|
|
247
|
+
handler: async () => {
|
|
248
|
+
const res = await serveAsset(readAsset, `${pagesDir}/${name}`);
|
|
249
|
+
if (mutable)
|
|
250
|
+
res.headers = { ...res.headers, "cache-control": "no-cache" };
|
|
251
|
+
return res;
|
|
252
|
+
},
|
|
224
253
|
});
|
|
225
254
|
}
|
|
226
255
|
// The `dist/` tree the sidecars' import-maps reference (`../dist/cockpit/index.js` →
|
|
@@ -580,12 +609,24 @@ export function mountPages(ctx, app) {
|
|
|
580
609
|
};
|
|
581
610
|
const sourceName = opts.sourceName ?? "app";
|
|
582
611
|
const bindings = ctx.manifest.instanceTracking ?? [];
|
|
612
|
+
// Probe "would serving this app-owned asset succeed?" — a READABLE REGULAR FILE, not the
|
|
613
|
+
// broader `HostContext.exists` (which is true for directories too, and a `pages/app.css/`
|
|
614
|
+
// DIRECTORY would link an asset the browser then 404s on). `statFile` is an OPTIONAL public
|
|
615
|
+
// host capability (a custom host need not implement it), and it opens the file for reading so
|
|
616
|
+
// no bytes cross the seam on the per-request shell hot path. Only wire the optimized probe
|
|
617
|
+
// when the host provides it; otherwise `createPagesRoutes` falls back to a read-based probe
|
|
618
|
+
// that answers the identical question.
|
|
619
|
+
const statFile = ctx.host.statFile?.bind(ctx.host);
|
|
620
|
+
const existsAsset = statFile
|
|
621
|
+
? async (p) => (await statFile(p))?.isFile === true
|
|
622
|
+
: undefined;
|
|
583
623
|
const routes = createPagesRoutes(opts, {
|
|
584
624
|
db: app.data.open(sourceName),
|
|
585
625
|
engine: app.engine,
|
|
586
626
|
cancel: (key) => cancelInstanceReconciling(app, bindings, key),
|
|
587
627
|
readPage: (p) => ctx.host.readTextFile(p),
|
|
588
628
|
readAsset: (p) => ctx.host.readTextFile(p),
|
|
629
|
+
existsAsset,
|
|
589
630
|
listPages: async () => {
|
|
590
631
|
const dir = opts.pagesDir ?? "pages";
|
|
591
632
|
const names = await ctx.host.listDir(dir).catch(() => []);
|
|
@@ -604,7 +645,7 @@ export function mountPages(ctx, app) {
|
|
|
604
645
|
function escapeAttr(s) {
|
|
605
646
|
return s.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
|
606
647
|
}
|
|
607
|
-
function rendererShell(homePage, apiDocsPath, gridLayout) {
|
|
648
|
+
function rendererShell(homePage, apiDocsPath, gridLayout, app = { appCss: false, appJs: false }) {
|
|
608
649
|
// A persistent "API docs" badge (spec-first apps get their Swagger UI linked from their own
|
|
609
650
|
// UI for free). `target="_blank"` + hardened `rel` so the docs open without giving the docs
|
|
610
651
|
// tab a handle back to this window (reverse-tabnabbing).
|
|
@@ -628,12 +669,12 @@ function rendererShell(homePage, apiDocsPath, gridLayout) {
|
|
|
628
669
|
<meta charset="utf-8" />
|
|
629
670
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
630
671
|
<title>Urban App</title>
|
|
631
|
-
<style>${RENDERER_CSS}</style
|
|
672
|
+
<style>${RENDERER_CSS}</style>${app.appCss ? '\n <link rel="stylesheet" href="./app.css" />' : ""}
|
|
632
673
|
</head>
|
|
633
674
|
<body data-grid-layout="${gridLayout}">${apiDocsBadge}
|
|
634
675
|
<main id="page" data-home="${escapeAttr(homePage)}"><p class="pc-empty">Loading…</p></main>
|
|
635
676
|
<script>${FORMJS_JS}</script>
|
|
636
|
-
<script type="module" src=".${RUNTIME_JS_PATH}"></script>
|
|
677
|
+
<script type="module" src=".${RUNTIME_JS_PATH}"></script>${app.appJs ? '\n <script type="module" src="./app.js"></script>' : ""}
|
|
637
678
|
</body>
|
|
638
679
|
</html>`;
|
|
639
680
|
}
|
|
@@ -1015,5 +1056,8 @@ function fnv1aHex(s) {
|
|
|
1015
1056
|
// other page action) do nothing after an upgrade until a hard refresh. Because the URL is unique
|
|
1016
1057
|
// per content, the response itself is served `immutable` with a one-year max-age.
|
|
1017
1058
|
const RUNTIME_JS_HASH = fnv1aHex(RENDERER_JS);
|
|
1018
|
-
|
|
1059
|
+
// Exported so tests can locate the actual fingerprinted runtime <script> tag in the shell
|
|
1060
|
+
// (asserting against the bare word "runtime" is unsound — it also appears in the embedded
|
|
1061
|
+
// renderer CSS, so such a test passes even if the app script were moved before the runtime).
|
|
1062
|
+
export const RUNTIME_JS_PATH = `/app/runtime.${RUNTIME_JS_HASH}.js`;
|
|
1019
1063
|
const IMMUTABLE_CACHE = "public, max-age=31536000, immutable";
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ProcessDefinitionIdentity } from "./host.ts";
|
|
2
|
+
export type { ProcessDefinitionIdentity };
|
|
3
|
+
/** A non-empty string form of an engine key/id, or `undefined` when absent/blank (including a
|
|
4
|
+
* whitespace-only string). Coerces a *finite* numeric key to a string (the engine may serialize a
|
|
5
|
+
* key either way) but never `String(...)`-coerces an arbitrary object into a garbage
|
|
6
|
+
* `"[object Object]"` id, nor a non-finite number into `"NaN"`/`"Infinity"`. The blank check trims,
|
|
7
|
+
* matching the read-path blank-key guards, so a `" "` key can never leak into a result. */
|
|
8
|
+
export declare function presentEngineKey(value: unknown): string | undefined;
|
|
9
|
+
/** A present string-only identifier (e.g. a BPMN process id) under the shared trim rule; a
|
|
10
|
+
* non-string is absent and never `String(...)`-coerced. */
|
|
11
|
+
export declare function presentString(value: unknown): string | undefined;
|
|
12
|
+
/** The {@link ProcessDefinitionIdentity} an engine row (a create response or a process-instance
|
|
13
|
+
* search row) reports — the single mapping both `createInstance` and `searchProcessInstances` use,
|
|
14
|
+
* so a created instance and its later snapshot describe the same definition identically. Each field
|
|
15
|
+
* passes the shared presence rule; a version that is not a positive integer number is omitted
|
|
16
|
+
* (never coerced). */
|
|
17
|
+
export declare function pickProcessDefinitionIdentity(row: {
|
|
18
|
+
processDefinitionKey?: unknown;
|
|
19
|
+
processDefinitionId?: unknown;
|
|
20
|
+
processDefinitionVersion?: unknown;
|
|
21
|
+
}): ProcessDefinitionIdentity;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// The single, transport-agnostic normalizer for a {@link ProcessDefinitionIdentity}. Both engine
|
|
2
|
+
// adapters — the live SDK/REST adapter (`engine/nanosdk.ts`, `SdkEngineClient`) and the in-process
|
|
3
|
+
// WASM test adapter (`@nanobpm/urban-testkit`, `WasmEngineClient`) — report which deployed
|
|
4
|
+
// definition an instance runs on *both* `createInstance` and `searchProcessInstances`. Each used to
|
|
5
|
+
// carry its own hand-mirrored `pickProcessDefinitionIdentity` (plus a re-declared identity type), so
|
|
6
|
+
// the two could silently diverge the moment identity validation changed — exactly the drift class
|
|
7
|
+
// the shared form-contract normalizer exists to kill (`./form-contract.ts`, issue #252). Centralizing
|
|
8
|
+
// the mapper (and reusing the canonical {@link ProcessDefinitionIdentity}) here removes that surface:
|
|
9
|
+
// a created instance and its later snapshot describe the same definition identically wherever this
|
|
10
|
+
// normalizer runs, because they run the *same* code.
|
|
11
|
+
//
|
|
12
|
+
// Ownership today: only the SDK adapter runs this implementation. `WasmEngineClient` still defines
|
|
13
|
+
// and calls its own behaviourally identical copy (`urban-testkit/src/wasm-engine.ts`) — a deliberate,
|
|
14
|
+
// temporary fork: this module is introduced by the same change, so it exists in no published urban
|
|
15
|
+
// release, and a value import would force the testkit's peer floor up to an urban version that does
|
|
16
|
+
// not yet exist (AGENTS.md: depend on an unreleased sibling only after it is published). Follow-up
|
|
17
|
+
// #585 switches the WASM adapter to this canonical import once the urban release carrying it is
|
|
18
|
+
// published; until then the testkit's own identity cases pin its copy to this normalizer's behaviour.
|
|
19
|
+
/** A non-empty string form of an engine key/id, or `undefined` when absent/blank (including a
|
|
20
|
+
* whitespace-only string). Coerces a *finite* numeric key to a string (the engine may serialize a
|
|
21
|
+
* key either way) but never `String(...)`-coerces an arbitrary object into a garbage
|
|
22
|
+
* `"[object Object]"` id, nor a non-finite number into `"NaN"`/`"Infinity"`. The blank check trims,
|
|
23
|
+
* matching the read-path blank-key guards, so a `" "` key can never leak into a result. */
|
|
24
|
+
export function presentEngineKey(value) {
|
|
25
|
+
if (typeof value === "number") {
|
|
26
|
+
return Number.isFinite(value) ? String(value) : undefined;
|
|
27
|
+
}
|
|
28
|
+
if (typeof value !== "string")
|
|
29
|
+
return undefined;
|
|
30
|
+
const trimmed = value.trim();
|
|
31
|
+
return trimmed === "" ? undefined : trimmed;
|
|
32
|
+
}
|
|
33
|
+
/** A present string-only identifier (e.g. a BPMN process id) under the shared trim rule; a
|
|
34
|
+
* non-string is absent and never `String(...)`-coerced. */
|
|
35
|
+
export function presentString(value) {
|
|
36
|
+
if (typeof value !== "string")
|
|
37
|
+
return undefined;
|
|
38
|
+
const trimmed = value.trim();
|
|
39
|
+
return trimmed === "" ? undefined : trimmed;
|
|
40
|
+
}
|
|
41
|
+
/** The {@link ProcessDefinitionIdentity} an engine row (a create response or a process-instance
|
|
42
|
+
* search row) reports — the single mapping both `createInstance` and `searchProcessInstances` use,
|
|
43
|
+
* so a created instance and its later snapshot describe the same definition identically. Each field
|
|
44
|
+
* passes the shared presence rule; a version that is not a positive integer number is omitted
|
|
45
|
+
* (never coerced). */
|
|
46
|
+
export function pickProcessDefinitionIdentity(row) {
|
|
47
|
+
const processDefinitionKey = presentEngineKey(row.processDefinitionKey);
|
|
48
|
+
const processDefinitionId = presentString(row.processDefinitionId);
|
|
49
|
+
const version = row.processDefinitionVersion;
|
|
50
|
+
const processDefinitionVersion = typeof version === "number" && Number.isInteger(version) && version > 0 ? version : undefined;
|
|
51
|
+
return {
|
|
52
|
+
...(processDefinitionKey ? { processDefinitionKey } : {}),
|
|
53
|
+
...(processDefinitionId ? { processDefinitionId } : {}),
|
|
54
|
+
...(processDefinitionVersion !== undefined ? { processDefinitionVersion } : {}),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { EngineClient, AgentHistoryFilter, AgentHistoryRecord, AgentInstanceFilter, AgentInstanceSummary, ElementInstanceState, ElementInstanceSummary, ElementInstanceFilter, ElementInstanceWaitState, ElementInstanceWaitStateFilter, WaitStateType, FormSchema, IncidentFilter, IncidentState, IncidentSummary, JobFilter, JobHandler, JobSummary, ProcessInstanceSnapshot, ProcessInstanceState, UserTaskState, UserTaskSummary, UserTaskFilter, VariableFilter, VariableSummary, WorkerSubscription } from "../core/host.ts";
|
|
1
|
+
import type { EngineClient, AgentHistoryFilter, AgentHistoryRecord, AgentInstanceFilter, AgentInstanceSummary, ElementInstanceState, ElementInstanceSummary, ElementInstanceFilter, ElementInstanceWaitState, ElementInstanceWaitStateFilter, WaitStateType, FormSchema, IncidentFilter, IncidentState, IncidentSummary, JobFilter, JobHandler, JobSummary, CreatedProcessInstance, ProcessInstanceSnapshot, ProcessInstanceState, UserTaskState, UserTaskSummary, UserTaskFilter, VariableFilter, VariableSummary, WorkerSubscription } from "../core/host.ts";
|
|
2
2
|
import type { EngineSdkClient } from "./sdk.ts";
|
|
3
3
|
/** Coerce an engine response's process-instance key to a non-empty string, or
|
|
4
4
|
* throw — a missing key means a malformed/partial response, not a real instance. */
|
|
@@ -233,10 +233,7 @@ export declare class SdkEngineClient implements EngineClient {
|
|
|
233
233
|
processDefinitionId: string;
|
|
234
234
|
variables?: Record<string, unknown>;
|
|
235
235
|
awaitCompletion?: boolean;
|
|
236
|
-
}): Promise<
|
|
237
|
-
processInstanceKey: string;
|
|
238
|
-
variables?: Record<string, unknown>;
|
|
239
|
-
}>;
|
|
236
|
+
}): Promise<CreatedProcessInstance>;
|
|
240
237
|
cancelInstance(input: {
|
|
241
238
|
processInstanceKey: string;
|
|
242
239
|
}): Promise<void>;
|
|
@@ -260,6 +257,7 @@ export declare class SdkEngineClient implements EngineClient {
|
|
|
260
257
|
searchProcessInstances(filter?: {
|
|
261
258
|
processInstanceKeys?: string[];
|
|
262
259
|
state?: ProcessInstanceState;
|
|
260
|
+
processDefinitionId?: string;
|
|
263
261
|
parentProcessInstanceKey?: string;
|
|
264
262
|
rootProcessInstanceKey?: string;
|
|
265
263
|
}): Promise<ProcessInstanceSnapshot[]>;
|
|
@@ -19,6 +19,7 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
19
19
|
return path;
|
|
20
20
|
};
|
|
21
21
|
import { assertDeployedWaitStateType, isBpmnError } from "../core/host.js";
|
|
22
|
+
import { pickProcessDefinitionIdentity, presentEngineKey, presentString, } from "../core/process-identity.js";
|
|
22
23
|
import { buildFormSchema, parseFormSchema, pickFormLinkage, resolveFormIdentifier, } from "../core/form-contract.js";
|
|
23
24
|
import { isRecord } from "../core/guards.js";
|
|
24
25
|
import { applyAmbientLineage } from "../core/lineage.js";
|
|
@@ -111,20 +112,11 @@ export function normalizeIncidentState(raw) {
|
|
|
111
112
|
return "UNKNOWN";
|
|
112
113
|
}
|
|
113
114
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
*
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
function presentEngineKey(value) {
|
|
120
|
-
if (typeof value === "number") {
|
|
121
|
-
return Number.isFinite(value) ? String(value) : undefined;
|
|
122
|
-
}
|
|
123
|
-
if (typeof value !== "string")
|
|
124
|
-
return undefined;
|
|
125
|
-
const trimmed = value.trim();
|
|
126
|
-
return trimmed === "" ? undefined : trimmed;
|
|
127
|
-
}
|
|
115
|
+
// Engine-key / process-definition-identity normalization now lives in the shared, adapter-agnostic
|
|
116
|
+
// `../core/process-identity.ts` (imported above), so this adapter's `createInstance` and
|
|
117
|
+
// `searchProcessInstances` run the *same* mapping — see that module's header for the drift class
|
|
118
|
+
// this removes. The WASM test adapter keeps a mirrored local copy until #585 (that module header's
|
|
119
|
+
// "Ownership today" note), so the shared mapping is not yet a cross-adapter guarantee.
|
|
128
120
|
/** The non-empty, trimmed string form of a *required* engine key, or throws when the value is
|
|
129
121
|
* absent/blank (including a whitespace-only string). A mutating seam operation addresses a
|
|
130
122
|
* single entity by key, so a blank key is a caller bug that must fail fast with an actionable
|
|
@@ -632,6 +624,7 @@ export class SdkEngineClient {
|
|
|
632
624
|
const variables = isRecord(body.variables) ? body.variables : undefined;
|
|
633
625
|
return {
|
|
634
626
|
processInstanceKey: requireProcessInstanceKey(key),
|
|
627
|
+
...pickProcessDefinitionIdentity(body),
|
|
635
628
|
variables,
|
|
636
629
|
};
|
|
637
630
|
}
|
|
@@ -749,6 +742,10 @@ export class SdkEngineClient {
|
|
|
749
742
|
const f = {};
|
|
750
743
|
if (filter?.state)
|
|
751
744
|
f.state = filter.state;
|
|
745
|
+
// A blank BPMN process-id selector is absent, not an empty filter that matches nothing.
|
|
746
|
+
const processDefinitionId = presentString(filter?.processDefinitionId);
|
|
747
|
+
if (processDefinitionId)
|
|
748
|
+
f.processDefinitionId = processDefinitionId;
|
|
752
749
|
// Normalize each requested key through the shared presence rule (drop blank/whitespace-only
|
|
753
750
|
// entries, trim padded ones like `" 123 "`) so the `$in` filter carries only present keys that
|
|
754
751
|
// match the normalized keys we surface — mirroring `WasmEngineClient` and the selector
|
|
@@ -792,13 +789,12 @@ export class SdkEngineClient {
|
|
|
792
789
|
});
|
|
793
790
|
return [];
|
|
794
791
|
}
|
|
795
|
-
const processDefinitionKey = presentEngineKey(it.processDefinitionKey);
|
|
796
792
|
const parentProcessInstanceKey = presentEngineKey(it.parentProcessInstanceKey);
|
|
797
793
|
const rootProcessInstanceKey = presentEngineKey(it.rootProcessInstanceKey);
|
|
798
794
|
return [{
|
|
799
795
|
processInstanceKey,
|
|
800
796
|
state,
|
|
801
|
-
...(
|
|
797
|
+
...pickProcessDefinitionIdentity(it),
|
|
802
798
|
...(parentProcessInstanceKey ? { parentProcessInstanceKey } : {}),
|
|
803
799
|
...(rootProcessInstanceKey ? { rootProcessInstanceKey } : {}),
|
|
804
800
|
}];
|
package/dist/runtime/index.d.ts
CHANGED
|
@@ -8,9 +8,10 @@ export { bindModeToHost, expandEnv, expandEnvString, isBindMode, loadManifest, p
|
|
|
8
8
|
export type { ActionDecl, AppManifest, BindMode, ChatSurface, DataSource, DomainField, DomainType, InstanceTracking, LlmBinding, NetworkConfig, PagesSurface, Surfaces, TaskInboxSurface, Trigger, Worker, } from "./core/manifest.ts";
|
|
9
9
|
export { collectManifestIssues, ManifestValidationError, validateManifest, } from "./core/validate.ts";
|
|
10
10
|
export type { ValidationIssue } from "./core/validate.ts";
|
|
11
|
-
export type { EngineClient, AgentHistoryFilter, AgentHistoryRecord, AgentInstanceFilter, AgentInstanceMetrics, AgentInstanceSummary, ElementInstanceState, ElementInstanceSummary, ElementInstanceFilter, ElementInstanceWaitState, ElementInstanceWaitStateFilter, WaitStateType, EngineJob, FormSchema, HostContext, HttpHandler, HttpRequest, HttpResponse, HttpServer, IncidentState, EngineIncidentState, IncidentFilter, IncidentSummary, JobFilter, JobHandler, JobSummary, ProcessInstanceSnapshot, ProcessInstanceState, SqliteDb, UserTaskState, UserTaskSummary, UserTaskFilter, VariableFilter, VariableSummary, WatchHandle, WorkerSubscription, } from "./core/host.ts";
|
|
11
|
+
export type { EngineClient, AgentHistoryFilter, AgentHistoryRecord, AgentInstanceFilter, AgentInstanceMetrics, AgentInstanceSummary, ElementInstanceState, ElementInstanceSummary, ElementInstanceFilter, ElementInstanceWaitState, ElementInstanceWaitStateFilter, WaitStateType, EngineJob, FormSchema, HostContext, HttpHandler, HttpRequest, HttpResponse, HttpServer, IncidentState, EngineIncidentState, IncidentFilter, IncidentSummary, JobFilter, JobHandler, JobSummary, CreatedProcessInstance, ProcessDefinitionIdentity, ProcessInstanceSnapshot, ProcessInstanceState, SqliteDb, UserTaskState, UserTaskSummary, UserTaskFilter, VariableFilter, VariableSummary, WatchHandle, WorkerSubscription, } from "./core/host.ts";
|
|
12
12
|
export type { TranscriptContentBlock, TranscriptContentType, TranscriptToolCall, TranscriptTurnMetrics, TranscriptTurnRole, } from "@nanobpm/agentic/transcript";
|
|
13
13
|
export { buildFormSchema, parseFormSchema, pickFormLinkage, presentFormIdentifier, resolveFormIdentifier, } from "./core/form-contract.ts";
|
|
14
|
+
export { pickProcessDefinitionIdentity, presentEngineKey, presentString, } from "./core/process-identity.ts";
|
|
14
15
|
export type { AppApi, Mounted, RuntimeContext } from "./core/context.ts";
|
|
15
16
|
export { createLogger } from "./core/logger.ts";
|
|
16
17
|
export type { Logger, LogLevel, LogFields, LogSink } from "./core/logger.ts";
|
package/dist/runtime/index.js
CHANGED
|
@@ -12,6 +12,11 @@ export { collectManifestIssues, ManifestValidationError, validateManifest, } fro
|
|
|
12
12
|
// both engine adapters (SDK/REST + WASM test double) call, so their form-identifier,
|
|
13
13
|
// form-schema, and user-task form-linkage handling cannot drift.
|
|
14
14
|
export { buildFormSchema, parseFormSchema, pickFormLinkage, presentFormIdentifier, resolveFormIdentifier, } from "./core/form-contract.js";
|
|
15
|
+
// The adapter-agnostic process-definition-identity normalizer (the single mapping both
|
|
16
|
+
// `createInstance` and `searchProcessInstances` use), owned today by the live SDK adapter; the WASM
|
|
17
|
+
// test adapter keeps a mirrored local copy until #585, so cross-adapter drift-freedom is not yet
|
|
18
|
+
// claimed. `ProcessDefinitionIdentity` itself is re-exported from `./core/host.ts`.
|
|
19
|
+
export { pickProcessDefinitionIdentity, presentEngineKey, presentString, } from "./core/process-identity.js";
|
|
15
20
|
// Structured logging surface (see core/logger.ts): the shape of `AppApi.log`, plus `createLogger`
|
|
16
21
|
// so consumers can build a Logger for a custom sink or a no-op test double (`createLogger(() => {})`).
|
|
17
22
|
export { createLogger } from "./core/logger.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nanobpm/urban",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.96.0",
|
|
4
4
|
"description": "Urban: build and run code-first apps on Nano — runtime, derivation toolkit, and CLI in one. Author durable processes with @nanobpm/workflow (defineFlow), a typed datasource, forms, triggers and surfaces from a nano.app.json manifest, on Node or Deno.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -106,7 +106,7 @@
|
|
|
106
106
|
"@nanobpm/agentic": "^0.15.0",
|
|
107
107
|
"@nanobpm/nano-app-schema": "^0.11.0",
|
|
108
108
|
"@nanobpm/nano-sdk": "^1.2.5",
|
|
109
|
-
"@nanobpm/workflow": "^0.
|
|
109
|
+
"@nanobpm/workflow": "^0.15.0",
|
|
110
110
|
"bpmn-auto-layout": "^2.0.0-alpha.2",
|
|
111
111
|
"create-urban-app": "^0.15.2",
|
|
112
112
|
"yaml": "^2.9.0"
|