biz-a-cli 2.3.80-15385 → 2.3.80-15397

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 (52) hide show
  1. package/bin/hub.js +5 -0
  2. package/bin/hubEvent.js +118 -53
  3. package/bin/tunnelTarget.js +60 -7
  4. package/engine/bpm/workflow-runtime.js +3 -3
  5. package/engine/cc/aiTurns.js +394 -0
  6. package/engine/cc/appMenus.d.ts +2 -0
  7. package/engine/cc/appMenus.js +14 -0
  8. package/engine/cc/ccGate.js +15 -0
  9. package/engine/cc/configTools.js +627 -0
  10. package/engine/cc/configValidate.js +85 -0
  11. package/engine/cc/currentConfig.js +31 -0
  12. package/engine/cc/derive.js +178 -0
  13. package/engine/cc/draftStore.js +2 -8
  14. package/engine/cc/drift.js +256 -0
  15. package/engine/cc/gapRepo.js +54 -0
  16. package/engine/cc/layer3Behavior.js +54 -0
  17. package/engine/cc/layer3Schema.d.ts +39 -0
  18. package/engine/cc/layer3Schema.js +153 -0
  19. package/engine/cc/mapCommit.js +77 -0
  20. package/engine/cc/mapPrint.d.ts +53 -0
  21. package/engine/cc/mapPrint.js +519 -0
  22. package/engine/cc/mapRepo.js +80 -0
  23. package/engine/cc/mapStore.js +190 -0
  24. package/engine/cc/mapTools.js +334 -0
  25. package/engine/cc/phaseA.js +164 -0
  26. package/engine/cc/phaseB.js +228 -0
  27. package/engine/cc/processMap.d.ts +79 -0
  28. package/engine/cc/processMap.js +470 -0
  29. package/engine/cc/prompts/derive.js +49 -0
  30. package/engine/cc/prompts/phaseA.js +41 -0
  31. package/engine/cc/prompts/phaseB.js +30 -0
  32. package/engine/cc/traceRepo.js +102 -0
  33. package/engine/cc/traceTargets.js +66 -0
  34. package/engine/cc/translate.js +96 -0
  35. package/engine/cc/turnCommit.js +157 -0
  36. package/engine/cc/turnLoop.js +87 -0
  37. package/engine/cc/turnRepo.js +141 -0
  38. package/engine/orm/apiRoute.js +2 -1
  39. package/engine/orm/principalGuards.js +2 -1
  40. package/engine/orm/registryClient.js +18 -6
  41. package/engine/orm/userPreferences.js +2 -1
  42. package/envs/env.dev.js +4 -5
  43. package/envs/env.js +9 -5
  44. package/migrations/postgres/1790300000000__cc_trace_baseline.sql +20 -0
  45. package/package.json +2 -3
  46. package/readme.md +17 -1
  47. package/scheduler/configController.js +57 -98
  48. package/scheduler/timer.js +4 -3
  49. package/security/aiGatewayClient.js +9 -1
  50. package/config/databases.json +0 -20
  51. package/config/ext-oidc.json +0 -7
  52. package/config/secrets.json +0 -27
package/bin/hub.js CHANGED
@@ -130,6 +130,11 @@ const argv = yargs(process.argv.slice(2))
130
130
  show its rules behave. */
131
131
  default: "observe",
132
132
  })
133
+ .options("aiGateway", {
134
+ describe:
135
+ "Doc 1 0b.6b: base URL of the central AI gateway (biz-a-server /ai) that ccai calls. Defaults to this hub's server (serverMode).",
136
+ type: "string",
137
+ })
133
138
  .options("services", {
134
139
  describe:
135
140
  "Comma-separated list of engine jobs this agent handles (e.g., 'BPM,EMAIL')",
package/bin/hubEvent.js CHANGED
@@ -28,6 +28,14 @@ import { authorizeRequest, principalForCliScript } from "../security/internalPri
28
28
  import { whoami } from "../security/whoami.js";
29
29
  import { runDraftAction } from "../engine/cc/draftStore.js";
30
30
  import { createPgDraftRepo } from "../engine/cc/pgRepo.js";
31
+ import { runMapAction } from "../engine/cc/mapStore.js";
32
+ import { createMapRepo } from "../engine/cc/mapRepo.js";
33
+ import { createGapRepo } from "../engine/cc/gapRepo.js";
34
+ import { createTraceRepo } from "../engine/cc/traceRepo.js";
35
+ import { createConfigReader } from "../engine/cc/currentConfig.js";
36
+ import { runAiAction } from "../engine/cc/aiTurns.js";
37
+ import { createTurnRepo } from "../engine/cc/turnRepo.js";
38
+ import { createAiGatewayClient } from "../security/aiGatewayClient.js";
31
39
  import {
32
40
  readUserPreferences,
33
41
  writeUserPreferences,
@@ -47,7 +55,7 @@ import {
47
55
  execPostgresTransaction,
48
56
  isPostgresIndex,
49
57
  } from "../engine/domain/dialect.js";
50
- import { resolveTunnelTarget } from "./tunnelTarget.js";
58
+ import { pipeTunnel } from "./tunnelTarget.js";
51
59
  /*
52
60
  * Doc 4 §8 — ⚠️ `runAsPrincipal` VALIDATES SYNCHRONOUSLY, so a browser-supplied active tenant outside
53
61
  * the signed claim throws out of the CALL, not out of the promise it would have returned. On this
@@ -232,59 +240,34 @@ export const streamEvent = async (socket, argv) =>
232
240
  },
233
241
  });
234
242
 
235
- // let client = net.connect(argv['port'], argv['hostname']);
236
- /* Where this pipe ends. A tenant this CLI serves on PostgreSQL is looped back to the
237
- hub's own FINA-compatible endpoint; every other tenant goes to FINA unchanged.
238
- Without this, biz-a's login — which only ever travels this tunnel — could not be
239
- answered at all on a tenant with no FINA running. */
240
- const target = resolveTunnelTarget(argv, isPostgresIndex);
241
- let client = target.secure
242
- ? tls.connect({
243
- port: target.port,
244
- host: target.host,
245
- // Looping back to ourselves, the certificate is our own.
246
- rejectUnauthorized: !target.local,
247
- })
248
- : net.connect(target.port, target.host);
249
- client.on("connect", () => {
250
- // logger.info(`client connected to ${argv['hostname']}:${argv['port']}`)
251
- let s = ss.createStream();
252
-
253
- // s.pipe(client).pipe(s);
254
- s.pipe(client).pipe(addCLIAddressAsResponseHeader).pipe(s);
255
-
256
- // use pipeline for better performance, back pressure, memory management, error handling and clean up
257
- // pipeline(s, client, addCLIAddressAsResponseHeader, s, (err)=>{if (err) {logger.error(err)}}) // Not work well with heroku
258
-
259
- s.on("end", () => {
260
- client.destroy();
261
- });
262
-
263
- socket.once(clientId, () => {
264
- // hub server shall notify us to end pipeline as soon as possible
265
- client.end();
266
- });
267
-
268
- ss(socket).emit(clientId, s);
269
- });
270
-
271
- client.setTimeout(IDLE_SOCKET_TIMEOUT_MILLISECONDS);
272
- client.on("timeout", () => {
273
- // in case client not notify to end pipeline, then inactivity timeout will end it
274
- client.end();
275
- });
276
-
277
- client.on("error", (err) => {
278
- // handle connection refusal (create a stream and immediately close it)
279
- logger.error(
280
- `API Error : tunnel could not reach ${target.local ? "this CLI's own endpoint" : "the API"} at ` +
281
- `${target.host}:${target.port} — `,
282
- err,
283
- );
284
- let s = ss.createStream();
285
- ss(socket).emit(clientId, s);
286
- s.end();
243
+ /*
244
+ * Where this pipe ends is decided by the request's FIRST BYTES (bin/tunnelTarget.js): a tenant
245
+ * this CLI serves on PostgreSQL, and — since 2026-10-01 — a Firebird tenant's LOGIN, are looped
246
+ * back to the hub's own FINA-compatible endpoint; every other request goes to FINA unchanged.
247
+ * Without the loop-back a Firebird login never passed through the CLI, so it never received
248
+ * the signed principal (Doc 6 on Firebird).
249
+ */
250
+ const s = ss.createStream();
251
+ pipeTunnel({
252
+ stream: s,
253
+ argv,
254
+ isPostgresIndex,
255
+ connect: (target) =>
256
+ target.secure
257
+ ? tls.connect({
258
+ port: target.port,
259
+ host: target.host,
260
+ // Looping back to ourselves, the certificate is our own.
261
+ rejectUnauthorized: !target.local,
262
+ })
263
+ : net.connect(target.port, target.host),
264
+ transform: () => addCLIAddressAsResponseHeader,
265
+ idleMs: IDLE_SOCKET_TIMEOUT_MILLISECONDS,
266
+ // hub server shall notify us to end pipeline as soon as possible
267
+ onClient: (client) => socket.once(clientId, () => client.end()),
268
+ logger,
287
269
  });
270
+ ss(socket).emit(clientId, s);
288
271
  };
289
272
 
290
273
  // const cliReqCb = async (data, callback) => {
@@ -480,6 +463,31 @@ const EngineRegistry = {
480
463
  import("../engine/domain/applicationConfig.js"),
481
464
  };
482
465
 
466
+ /* Doc 1 step 3 — what a Phase B turn needs: the draft store, the gap list, a draft opener that applies the
467
+ draft store's own rules (Layer 3, owned by Control Center) for the VERIFIED caller, one transaction, and
468
+ the publisher's DDL preview. */
469
+ async function phaseBDeps(argv, headers) {
470
+ const exec = (sql) => execPostgres(sql, argv["dbindex"]);
471
+ const execStatements = (statements) => execPostgresStatements(statements, argv["dbindex"]);
472
+ const draftRepo = createPgDraftRepo({ exec, execStatements });
473
+ const principal = principalForCliScript(headers, { mode: argv["principalMode"] || "observe" });
474
+ const { buildDomainDdlPreview } = await import("../engine/domain/appConfig.js");
475
+ return {
476
+ draftRepo,
477
+ gapRepo: createGapRepo({ exec }),
478
+ traceRepo: createTraceRepo({ exec }),
479
+ openDraft: (kind, name) => runDraftAction({ repo: draftRepo, principal, isPostgres: true, action: "open", args: { kind, name } }),
480
+ execStatements,
481
+ ddlWarnings: (content) => buildDomainDdlPreview(JSON.stringify(content), argv["dbindex"]).warnings ?? [],
482
+ };
483
+ }
484
+
485
+ /* Doc 1 step 4d — what a derive turn reads: the configs as they are now (the OPEN draft, else published). */
486
+ function deriveDeps(argv) {
487
+ const draftRepo = createPgDraftRepo({ exec: (sql) => execPostgres(sql, argv["dbindex"]) });
488
+ return { draftRepo, readConfig: createConfigReader(draftRepo) };
489
+ }
490
+
483
491
  const handleCliCommand = async (data, cb, argv) => {
484
492
  try {
485
493
  const command = data.command.trim().toLowerCase();
@@ -673,6 +681,38 @@ const handleCliCommand = async (data, cb, argv) => {
673
681
  * publish may). A refusal the user can act on is returned as `problems`, as companytheme
674
682
  * does; `code` carries CC_DRAFT_STALE for a save made from an old revision.
675
683
  */
684
+ /*
685
+ * Doc 1 0b.6b — an AI turn from Control Center, through the central gateway
686
+ * (engine/cc/aiTurns.js). `send` answers at once; the turn finishes in the background, is
687
+ * recorded in SYS$CC_TURN with its cost, and the VERIFIED user is told {turnId, outcome}.
688
+ * The CLI holds no AI key: the gateway client signs with this company's biza.ai key.
689
+ */
690
+ case "ccai": {
691
+ const aiResult = await runAiAction({
692
+ repo: createTurnRepo({ exec: (sql) => execPostgres(sql, argv["dbindex"]) }),
693
+ /* Doc 1 step 2: an interview turn commits the process map. Step 4d: a derive turn commits it WITH its
694
+ trace links, in one transaction. */
695
+ mapRepo: createMapRepo({ exec: (sql) => execPostgres(sql, argv["dbindex"]), execStatements: (statements) => execPostgresStatements(statements, argv["dbindex"]) }),
696
+ /* Doc 1 step 4: an interview turn never removes what config implements. */
697
+ traceRepo: createTraceRepo({ exec: (sql) => execPostgres(sql, argv["dbindex"]) }),
698
+ /* Doc 1 step 3: a Phase B turn writes drafts, gaps and trace links in ONE transaction. */
699
+ config: ["generate", "refine"].includes(data.action) ? await phaseBDeps(argv, data.headers) : data.action === "derive" ? deriveDeps(argv) : undefined,
700
+ gateway: createAiGatewayClient({ baseUrl: argv["aiGateway"] || argv["server"], subdomain: argv["subdomain"] }),
701
+ principal: principalForCliScript(data.headers, { mode: argv["principalMode"] || "observe" }),
702
+ isPostgres: isPostgresIndex(argv["dbindex"]),
703
+ action: data.action,
704
+ args: data.args,
705
+ notify: (userId, eventName, payload) => ClientEmitter.emitUsers([userId], eventName, payload),
706
+ logger,
707
+ });
708
+ cb(null, {
709
+ success: aiResult.ok,
710
+ ...(aiResult.code ? { code: aiResult.code } : {}),
711
+ problems: aiResult.problems ?? [],
712
+ data: aiResult.data ?? null,
713
+ });
714
+ break;
715
+ }
676
716
  case "ccdraft": {
677
717
  const ccResult = await runDraftAction({
678
718
  repo: createPgDraftRepo({
@@ -695,6 +735,31 @@ const handleCliCommand = async (data, cb, argv) => {
695
735
  });
696
736
  break;
697
737
  }
738
+ /*
739
+ * Doc 1 step 1 — the Business Process Map behind Process Studio (engine/cc/mapStore.js):
740
+ * get / versions / save. A save is a new SYS$CC_PROCESS_MAP version, merged per element.
741
+ */
742
+ case "ccmap": {
743
+ const mapResult = await runMapAction({
744
+ repo: createMapRepo({ exec: (sql) => execPostgres(sql, argv["dbindex"]) }),
745
+ gapRepo: createGapRepo({ exec: (sql) => execPostgres(sql, argv["dbindex"]) }),
746
+ /* Doc 1 step 4: the trace panel — the links, the turns that told the story, the config they point at. */
747
+ traceRepo: createTraceRepo({ exec: (sql) => execPostgres(sql, argv["dbindex"]) }),
748
+ turnRepo: createTurnRepo({ exec: (sql) => execPostgres(sql, argv["dbindex"]) }),
749
+ readConfig: createConfigReader(createPgDraftRepo({ exec: (sql) => execPostgres(sql, argv["dbindex"]) })),
750
+ principal: principalForCliScript(data.headers, { mode: argv["principalMode"] || "observe" }),
751
+ isPostgres: isPostgresIndex(argv["dbindex"]),
752
+ action: data.action,
753
+ args: data.args,
754
+ });
755
+ cb(null, {
756
+ success: mapResult.ok,
757
+ ...(mapResult.code ? { code: mapResult.code } : {}),
758
+ problems: mapResult.problems ?? [],
759
+ data: mapResult.data ?? null,
760
+ });
761
+ break;
762
+ }
698
763
  case "runextcliscript": {
699
764
  // External-access op invoker — enforces the capability boundary (dispatch.js),
700
765
  // unlike raw `runcliscript`. Safe to expose: no valid token -> UNAUTHENTICATED.
@@ -11,8 +11,35 @@
11
11
  * database index, as it is everywhere else in this port.
12
12
  */
13
13
 
14
+ /*
15
+ * ⚠️ Doc 6 on Firebird (found live 2026-10-01): the LOGIN of a Firebird tenant went straight to FINA
16
+ * through this tunnel, so the CLI never saw FINA's answer and never added the signed principal — and
17
+ * every principal-guarded command (company theme, logo, user preferences) refused on Firebird. The login
18
+ * is now looped back to the CLI's own endpoint too (finaEndpoint.js forwards it to FINA and appends the
19
+ * principal); every other Firebird request still goes straight to FINA, byte for byte.
20
+ *
21
+ * Recognised by its REQUEST LINE only (method + path), never by anything in a body.
22
+ */
23
+ const LOGIN_PATH = /%22get_valid_user_id%22|"get_valid_user_id"/i;
24
+
25
+ export const isLoginRequest = (firstChunk) => {
26
+ if (!firstChunk || firstChunk.length === 0) return false;
27
+ const head = Buffer.from(firstChunk).subarray(0, 2048).toString("latin1");
28
+ const lineEnd = head.indexOf("\r\n");
29
+ const requestLine = lineEnd === -1 ? head : head.slice(0, lineEnd);
30
+ const [method, path] = requestLine.split(" ");
31
+ return method === "POST" && typeof path === "string" && LOGIN_PATH.test(path);
32
+ };
33
+
34
+ const ownEndpoint = (argv) => ({
35
+ host: "127.0.0.1", // ourselves — never the public interface
36
+ port: argv.serverport,
37
+ secure: argv.isHttps === true, // how WE listen; argv.secure describes FINA
38
+ local: true,
39
+ });
40
+
14
41
  /* Kept separate from hubEvent.js so it can be exercised without importing the socket machinery. */
15
- export const resolveTunnelTarget = (argv, isPostgresIndex) => {
42
+ export const resolveTunnelTarget = (argv, isPostgresIndex, firstChunk) => {
16
43
  const fina = {
17
44
  host: argv.hostname,
18
45
  port: argv.port,
@@ -20,6 +47,7 @@ export const resolveTunnelTarget = (argv, isPostgresIndex) => {
20
47
  local: false,
21
48
  };
22
49
 
50
+ if (isLoginRequest(firstChunk)) return ownEndpoint(argv);
23
51
  if (!argv.dbindex) return fina;
24
52
 
25
53
  let servedHere = false;
@@ -32,10 +60,35 @@ export const resolveTunnelTarget = (argv, isPostgresIndex) => {
32
60
  }
33
61
  if (!servedHere) return fina;
34
62
 
35
- return {
36
- host: "127.0.0.1", // ourselves — never the public interface
37
- port: argv.serverport,
38
- secure: argv.isHttps === true, // how WE listen; argv.secure describes FINA
39
- local: true,
40
- };
63
+ return ownEndpoint(argv);
64
+ };
65
+
66
+ /*
67
+ * One tunnelled request: biz-a's bytes arrive on `stream`; the connection is opened only once the first
68
+ * bytes say where it must go (resolveTunnelTarget), that first chunk is written first so nothing is
69
+ * lost, and the rest is piped. The answer flows back through `transform()` (the response-header
70
+ * injection). A connection that fails ends the stream, so biz-a is not left waiting.
71
+ */
72
+ export const pipeTunnel = ({ stream, argv, isPostgresIndex, connect, transform, idleMs, onClient, logger = console }) => {
73
+ stream.once("data", (first) => {
74
+ stream.pause();
75
+ const target = resolveTunnelTarget(argv, isPostgresIndex, first);
76
+ const client = connect(target);
77
+ onClient?.(client);
78
+ if (idleMs) client.setTimeout(idleMs);
79
+ client.on("timeout", () => client.end());
80
+ client.on("connect", () => {
81
+ client.write(first);
82
+ stream.pipe(client).pipe(transform()).pipe(stream);
83
+ stream.resume();
84
+ });
85
+ client.on("error", (err) => {
86
+ logger.error?.(
87
+ `API Error : tunnel could not reach ${target.local ? "this CLI's own endpoint" : "the API"} at ${target.host}:${target.port} — `,
88
+ err,
89
+ );
90
+ stream.end();
91
+ });
92
+ stream.on("end", () => client.destroy());
93
+ });
41
94
  };
@@ -218,14 +218,14 @@ export async function initiate(payload) {
218
218
  current_state_code: def.initial_state_code,
219
219
  status: "ACTIVE",
220
220
  version_no: def.version,
221
- created_by: userId || null,
221
+ created_by: userId ?? null,
222
222
  SYS$BPM_STATE_HISTORY: [
223
223
  {
224
224
  from_state_code: null,
225
225
  to_state_code: def.initial_state_code,
226
226
  trigger_type: "SYSTEM",
227
227
  actor_type: "USER",
228
- actor_id: userId || null,
228
+ actor_id: userId ?? null,
229
229
  request_id: `START_${entityType}_${entityId}_${new Date().getTime()}`,
230
230
  },
231
231
  ],
@@ -773,7 +773,7 @@ export async function terminate(payload) {
773
773
  to_state_code: currentStateCode,
774
774
  trigger_type: "USER_EVENT",
775
775
  actor_type: "USER",
776
- actor_id: userId || null,
776
+ actor_id: userId ?? null,
777
777
  request_id: `TERMINATE_${instanceId}_${new Date().getTime()}`,
778
778
  },
779
779
  };