biz-a-cli 2.3.80-15381 → 2.3.80-15385
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/bin/app.js +30 -42
- package/bin/directHubEvent.js +6 -2
- package/bin/hub.js +9 -0
- package/bin/hubEvent.js +67 -12
- package/config/secrets.json +25 -19
- package/db/db.js +2 -2
- package/db/ds.js +21 -2
- package/engine/bpm/domainRules.js +71 -0
- package/engine/bpm/routes.js +2 -0
- package/engine/bpm/rules.d.ts +121 -0
- package/engine/bpm/rules.js +241 -0
- package/engine/bpm/validateReport.js +60 -0
- package/engine/bpm/validateWorkflow.d.ts +37 -0
- package/engine/bpm/validateWorkflow.js +400 -0
- package/engine/bpm/vmGuard.js +13 -0
- package/engine/bpm/workflow-runtime.js +193 -246
- package/engine/bpm/workflow.js +81 -54
- package/engine/cc/appMenus.d.ts +25 -0
- package/engine/cc/appMenus.js +154 -0
- package/engine/cc/draftStore.js +443 -0
- package/engine/cc/jsonDiff.js +49 -0
- package/engine/cc/jsonPatch.js +161 -0
- package/engine/cc/livePipelines.js +65 -0
- package/engine/cc/ownershipGuard.js +45 -0
- package/engine/cc/pgRepo.js +185 -0
- package/engine/cc/publish.js +264 -0
- package/engine/cc/published.js +51 -0
- package/engine/domain/appConfig.js +22 -26
- package/engine/domain/applicationConfig.js +3 -0
- package/engine/domain/entityConfigLoader.js +2 -1
- package/engine/domain/finaBlocks.js +121 -0
- package/engine/orm/apiRoute.js +38 -1
- package/engine/orm/companyLogo.js +53 -1
- package/engine/orm/companyTheme.js +12 -8
- package/engine/orm/finaEndpoint.js +7 -2
- package/engine/orm/principalGuards.js +12 -6
- package/engine/orm/requestScope.js +6 -1
- package/engine/orm/sqlDialect.js +32 -0
- package/engine/orm/tenantSql.js +55 -0
- package/engine/orm/userPreferences.js +16 -11
- package/envs/hosts.js +4 -0
- package/migrations/postgres/1789700000000__theme_audit.sql +2 -1
- package/migrations/postgres/1789800000000__cc_draft.sql +64 -0
- package/migrations/postgres/1789900000000__cc_draft_content_json.sql +10 -0
- package/migrations/postgres/1790000000000__cc_doc1.sql +139 -0
- package/migrations/postgres/1790100000000__cc_doc1_revoke.sql +16 -0
- package/migrations/postgres/1790200000000__theme_audit_revoke.sql +9 -0
- package/package.json +1 -1
- package/security/aiGatewayClient.js +123 -0
- package/security/whoami.js +25 -0
package/bin/app.js
CHANGED
|
@@ -33,7 +33,6 @@ import {
|
|
|
33
33
|
buildPostgresSetAppSql,
|
|
34
34
|
extractConfigVersion,
|
|
35
35
|
getChunkedUpsertBlocks,
|
|
36
|
-
getUpsertBlock,
|
|
37
36
|
isSemanticVersion,
|
|
38
37
|
parseConfigForVersion,
|
|
39
38
|
readConfigRows,
|
|
@@ -52,6 +51,7 @@ import {
|
|
|
52
51
|
renderApplicationConfig,
|
|
53
52
|
uploadSourceConfigName,
|
|
54
53
|
} from "../engine/domain/publishedAdditions.js";
|
|
54
|
+
import { guardCcOwnedDomains } from "../engine/cc/ownershipGuard.js";
|
|
55
55
|
import { validateTenancyDeclaration } from "../engine/orm/tenancyDeclaration.js";
|
|
56
56
|
import { logger } from "../logger.js";
|
|
57
57
|
import { findRawColorsInSource } from "../engine/domain/styleColors.js";
|
|
@@ -155,6 +155,13 @@ const addCommandOptions = {
|
|
|
155
155
|
demandOption: false,
|
|
156
156
|
default: false,
|
|
157
157
|
},
|
|
158
|
+
releaseCc: {
|
|
159
|
+
describe:
|
|
160
|
+
"Config mode: upload a domain that Control Center owns, handing it back to SVN. Without it such a domain is refused, so CC edits are never silently overwritten",
|
|
161
|
+
type: "boolean",
|
|
162
|
+
demandOption: false,
|
|
163
|
+
default: false,
|
|
164
|
+
},
|
|
158
165
|
};
|
|
159
166
|
|
|
160
167
|
const removeCommandOptions = {
|
|
@@ -377,9 +384,6 @@ const APPLICATION_CONFIG_FILE = "application.js";
|
|
|
377
384
|
/* Doc 6 §8.1 — the application theme source. JSON, because it is DATA that reaches the browser
|
|
378
385
|
via sys$config and must never be evaluated (AC 26). */
|
|
379
386
|
const THEME_CONFIG_FILE = "theme.json";
|
|
380
|
-
/* Parity with the admin editor (appConfig.component.ts executeBlockBodyLimit): the DataSnap
|
|
381
|
-
`block` call body must stay under 64KB, so an oversized config is upserted in chunks. */
|
|
382
|
-
const EXECUTE_BLOCK_BODY_LIMIT = 2 ** 16;
|
|
383
387
|
|
|
384
388
|
const findApplicationConfigFile = (fileNames = []) =>
|
|
385
389
|
fileNames.find(
|
|
@@ -405,7 +409,7 @@ const isConfigModeDir = (workingDir, files = null) => {
|
|
|
405
409
|
flattenSql(), whose `--` comment stripping mangles a config upsert — the whole config is embedded
|
|
406
410
|
as a single-line quoted literal, so the first `--` ANYWHERE in the config text (a JS comment, a
|
|
407
411
|
CSS custom property, an HTML template) silently truncates the rest of the statement and Firebird
|
|
408
|
-
reports "Unexpected end of command".
|
|
412
|
+
reports "Unexpected end of command". getChunkedUpsertBlocks already emits single-line SQL, so there is
|
|
409
413
|
nothing to flatten. This mirrors what the admin editor sends (dataService.executeBlock). */
|
|
410
414
|
const executeConfigBlock = async (blockSql, { server, apiPort, dbIndex }) => {
|
|
411
415
|
const url = `${server}:${apiPort}/fina/rest/TOrmMethod/%22block%22`;
|
|
@@ -425,43 +429,9 @@ const executeConfigBlock = async (blockSql, { server, apiPort, dbIndex }) => {
|
|
|
425
429
|
return parsed;
|
|
426
430
|
};
|
|
427
431
|
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
JSON.stringify({ dbIndex, method: "block", object: blockSql }),
|
|
432
|
-
],
|
|
433
|
-
}).length;
|
|
434
|
-
|
|
435
|
-
/* One upsert block when it fits, else halve the chunk size until every block fits. Mirrors
|
|
436
|
-
appConfig.component.ts resolveUpsertBlocksByBodyLimit. */
|
|
437
|
-
const resolveUpsertBlocks = (name, version, content, dbIndex) => {
|
|
438
|
-
const singleBlock = getUpsertBlock(name, version, content);
|
|
439
|
-
if (
|
|
440
|
-
estimateExecuteBlockBodyLength(singleBlock, dbIndex) <=
|
|
441
|
-
EXECUTE_BLOCK_BODY_LIMIT
|
|
442
|
-
) {
|
|
443
|
-
return [singleBlock];
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
let chunkSize = EXECUTE_BLOCK_BODY_LIMIT;
|
|
447
|
-
while (chunkSize > 0) {
|
|
448
|
-
const blocks = getChunkedUpsertBlocks(name, version, content, chunkSize);
|
|
449
|
-
if (
|
|
450
|
-
blocks.every(
|
|
451
|
-
(block) =>
|
|
452
|
-
estimateExecuteBlockBodyLength(block, dbIndex) <=
|
|
453
|
-
EXECUTE_BLOCK_BODY_LIMIT,
|
|
454
|
-
)
|
|
455
|
-
) {
|
|
456
|
-
return blocks;
|
|
457
|
-
}
|
|
458
|
-
chunkSize = Math.floor(chunkSize / 2);
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
throw new Error(
|
|
462
|
-
`Failed to split "${name}" into executeBlock-sized chunks.`,
|
|
463
|
-
);
|
|
464
|
-
};
|
|
432
|
+
/* The upsert blocks for one config. getChunkedUpsertBlocks sizes them by the finished SQL in bytes,
|
|
433
|
+
under Firebird's 65,535-byte statement limit (engine/domain/finaBlocks.js) — one block when it fits. */
|
|
434
|
+
const resolveUpsertBlocks = (name, version, content) => getChunkedUpsertBlocks(name, version, content);
|
|
465
435
|
|
|
466
436
|
/* PURE-ish (reads files, no network): resolve every config in the directory and validate the
|
|
467
437
|
WHOLE set before anything is uploaded, so a stray lib or a typo'd metadata can never leave a
|
|
@@ -728,6 +698,7 @@ async function uploadConfigs({
|
|
|
728
698
|
sub,
|
|
729
699
|
publish = false,
|
|
730
700
|
forceFull = false,
|
|
701
|
+
releaseCc = false,
|
|
731
702
|
serverMode = "prod",
|
|
732
703
|
}) {
|
|
733
704
|
/* Phase B2: ask biz-a for this tenant's database coordinates before anything reads the
|
|
@@ -750,6 +721,19 @@ async function uploadConfigs({
|
|
|
750
721
|
timeout: 60000,
|
|
751
722
|
};
|
|
752
723
|
|
|
724
|
+
/*
|
|
725
|
+
* Doc 1 step 0a.2 — ⚠️ one owner at a time. A domain Control Center owns is refused BEFORE anything
|
|
726
|
+
* is saved (the application config included), so a refused upload writes nothing at all.
|
|
727
|
+
*/
|
|
728
|
+
if (isPostgresIndex(dbIndex)) {
|
|
729
|
+
const { released } = await guardCcOwnedDomains({
|
|
730
|
+
exec: (sql) => execPostgres(sql, dbIndex),
|
|
731
|
+
domainNames: domains.map(({ name }) => name),
|
|
732
|
+
releaseCc,
|
|
733
|
+
});
|
|
734
|
+
if (released.length > 0) logger.info(`handed back to SVN (no longer owned by Control Center): ${released.join(", ")}`);
|
|
735
|
+
}
|
|
736
|
+
|
|
753
737
|
const saveOne = async ({ name, version, content }) => {
|
|
754
738
|
/* A PostgreSQL index writes straight to its pool. No chunking: the block splitting below
|
|
755
739
|
exists only because the FINA HTTP body has a size limit, which a direct connection does
|
|
@@ -1039,6 +1023,7 @@ async function addApp({
|
|
|
1039
1023
|
body = null,
|
|
1040
1024
|
publish = false,
|
|
1041
1025
|
forceFull = false,
|
|
1026
|
+
releaseCc = false,
|
|
1042
1027
|
serverMode = "prod",
|
|
1043
1028
|
/* Extra directories whose DOMAIN configs upload alongside this one's (see --include). */
|
|
1044
1029
|
include = [],
|
|
@@ -1095,6 +1080,7 @@ async function addApp({
|
|
|
1095
1080
|
sub,
|
|
1096
1081
|
publish,
|
|
1097
1082
|
forceFull,
|
|
1083
|
+
releaseCc,
|
|
1098
1084
|
serverMode,
|
|
1099
1085
|
});
|
|
1100
1086
|
}
|
|
@@ -1303,6 +1289,7 @@ async function runUnitTests(options) {
|
|
|
1303
1289
|
files: options.files,
|
|
1304
1290
|
publish: options.publish,
|
|
1305
1291
|
forceFull: options.forceFull,
|
|
1292
|
+
releaseCc: options.releaseCc,
|
|
1306
1293
|
serverMode: options.serverMode,
|
|
1307
1294
|
});
|
|
1308
1295
|
} else {
|
|
@@ -1336,6 +1323,7 @@ const buildCli = () =>
|
|
|
1336
1323
|
files: commandOptions.files,
|
|
1337
1324
|
publish: commandOptions.publish,
|
|
1338
1325
|
forceFull: commandOptions.forceFull,
|
|
1326
|
+
releaseCc: commandOptions.releaseCc,
|
|
1339
1327
|
serverMode: commandOptions.serverMode,
|
|
1340
1328
|
include: commandOptions.include,
|
|
1341
1329
|
});
|
package/bin/directHubEvent.js
CHANGED
|
@@ -9,7 +9,7 @@ import { extClientListener } from "../engine/ext/socketListener.js";
|
|
|
9
9
|
import { io as ioClient } from "socket.io-client";
|
|
10
10
|
|
|
11
11
|
import { prefixedLogger } from "../logger.js";
|
|
12
|
-
import { APP_URL, TEST_URL, ADMIN_URL, TEST_ADMIN_URL } from "../envs/hosts.js";
|
|
12
|
+
import { APP_URL, TEST_URL, ADMIN_URL, TEST_ADMIN_URL, CC_URL } from "../envs/hosts.js";
|
|
13
13
|
|
|
14
14
|
const logger = prefixedLogger("[DirectHub]");
|
|
15
15
|
|
|
@@ -203,10 +203,12 @@ export function createSocketServer(httpServer, cliIpAddress = "127.0.0.1") {
|
|
|
203
203
|
TEST_URL,
|
|
204
204
|
ADMIN_URL,
|
|
205
205
|
TEST_ADMIN_URL,
|
|
206
|
+
CC_URL,
|
|
206
207
|
// biz-a.id is still served alongside biz-a.app, so its browser origins stay allowed.
|
|
207
208
|
"https://biz-a.id",
|
|
208
209
|
"https://test.biz-a.id",
|
|
209
210
|
"https://admin.biz-a.id",
|
|
211
|
+
"https://cc.biz-a.id",
|
|
210
212
|
// NOT the wildcard (see above):
|
|
211
213
|
// /\.biz-a\.id$/,
|
|
212
214
|
"vscode-file://vscode-app",
|
|
@@ -217,8 +219,10 @@ export function createSocketServer(httpServer, cliIpAddress = "127.0.0.1") {
|
|
|
217
219
|
`http://${cliIpAddress}:4200`,
|
|
218
220
|
"http://localhost:4200",
|
|
219
221
|
"http://localhost",
|
|
222
|
+
// Control Center (bcc) dev server, beside the client on 4200.
|
|
223
|
+
"http://localhost:4300",
|
|
220
224
|
]
|
|
221
|
-
: [`http://${cliIpAddress}:4200`, "http://localhost:4200"],
|
|
225
|
+
: [`http://${cliIpAddress}:4200`, "http://localhost:4200", "http://localhost:4300"],
|
|
222
226
|
),
|
|
223
227
|
},
|
|
224
228
|
maxHttpBufferSize: 1e8, // 100 MB
|
package/bin/hub.js
CHANGED
|
@@ -121,6 +121,15 @@ const argv = yargs(process.argv.slice(2))
|
|
|
121
121
|
rebuilt to send a principal. A tenant opts in explicitly. */
|
|
122
122
|
default: "observe",
|
|
123
123
|
})
|
|
124
|
+
.options("ruleMode", {
|
|
125
|
+
describe:
|
|
126
|
+
"Doc 1 0b.2: DOMAIN_RULE guards run their rule script. 'observe' logs what a rule would block and lets it pass; 'enforce' blocks.",
|
|
127
|
+
type: "string",
|
|
128
|
+
choices: ["observe", "enforce"],
|
|
129
|
+
/* ⚠️ The DEFAULT STAYS `observe`, as for principalMode: a tenant switches to enforce once its logs
|
|
130
|
+
show its rules behave. */
|
|
131
|
+
default: "observe",
|
|
132
|
+
})
|
|
124
133
|
.options("services", {
|
|
125
134
|
describe:
|
|
126
135
|
"Comma-separated list of engine jobs this agent handles (e.g., 'BPM,EMAIL')",
|
package/bin/hubEvent.js
CHANGED
|
@@ -16,7 +16,8 @@ import { extDeriveRoomsHandler } from "../engine/ext/esb.js";
|
|
|
16
16
|
import { makeExtReqHandler } from "../engine/ext/hubProxy.js";
|
|
17
17
|
|
|
18
18
|
import { logger } from "../logger.js";
|
|
19
|
-
import { routeApiRequest } from "../engine/orm/apiRoute.js";
|
|
19
|
+
import { routeApiRequest, parseAuthRequest, withFinaLoginPrincipal } from "../engine/orm/apiRoute.js";
|
|
20
|
+
import { tenantSqlFor } from "../engine/orm/tenantSql.js";
|
|
20
21
|
import { ensureEntityConfig } from "../engine/domain/entityConfigPrimer.js";
|
|
21
22
|
import { runAsPrincipal, runTrusted, ACTIVE_TENANT_NOT_HELD } from "../engine/orm/tenantContext.js";
|
|
22
23
|
import { resolveRequestScope, SESSION_NOT_USABLE } from "../engine/orm/requestScope.js";
|
|
@@ -24,6 +25,9 @@ import { createSessionStore } from "../engine/orm/sessionStore.js";
|
|
|
24
25
|
import { resolveUserTenancy } from "../engine/orm/tenantResolution.js";
|
|
25
26
|
import { buildUserTenantSql } from "../engine/orm/finaAuth.js";
|
|
26
27
|
import { authorizeRequest, principalForCliScript } from "../security/internalPrincipal.js";
|
|
28
|
+
import { whoami } from "../security/whoami.js";
|
|
29
|
+
import { runDraftAction } from "../engine/cc/draftStore.js";
|
|
30
|
+
import { createPgDraftRepo } from "../engine/cc/pgRepo.js";
|
|
27
31
|
import {
|
|
28
32
|
readUserPreferences,
|
|
29
33
|
writeUserPreferences,
|
|
@@ -483,6 +487,14 @@ const handleCliCommand = async (data, cb, argv) => {
|
|
|
483
487
|
case "status":
|
|
484
488
|
cb(null, status(argv));
|
|
485
489
|
break;
|
|
490
|
+
/*
|
|
491
|
+
* Doc 1 step 0a.1 — who THIS CLI verified is asking. Control Center checks it before
|
|
492
|
+
* offering any editing: a successful login at biz-a does not prove the principal
|
|
493
|
+
* verifies here. Headers only; see security/whoami.js.
|
|
494
|
+
*/
|
|
495
|
+
case "whoami":
|
|
496
|
+
cb(null, whoami(data.headers, { mode: argv["principalMode"] || "observe" }));
|
|
497
|
+
break;
|
|
486
498
|
case "runcliscript":
|
|
487
499
|
const liveTriggerPayload = {
|
|
488
500
|
arguments: argv,
|
|
@@ -542,16 +554,19 @@ const handleCliCommand = async (data, cb, argv) => {
|
|
|
542
554
|
const prefPrincipal = principalForCliScript(data.headers, {
|
|
543
555
|
mode: argv["principalMode"] || "observe",
|
|
544
556
|
});
|
|
545
|
-
|
|
557
|
+
/* Doc 6 on Firebird (2026-10-01) — the tenant seam runs this on either engine. */
|
|
558
|
+
const prefDb = tenantSqlFor(argv["dbindex"]);
|
|
546
559
|
const prefs =
|
|
547
560
|
data.action === "write"
|
|
548
561
|
? await writeUserPreferences({
|
|
549
|
-
exec:
|
|
562
|
+
exec: prefDb.exec,
|
|
563
|
+
execStatements: prefDb.atomic,
|
|
564
|
+
sql: prefDb,
|
|
550
565
|
principal: prefPrincipal,
|
|
551
566
|
patch: data.patch,
|
|
552
567
|
})
|
|
553
568
|
: await readUserPreferences({
|
|
554
|
-
exec:
|
|
569
|
+
exec: prefDb.exec,
|
|
555
570
|
principal: prefPrincipal,
|
|
556
571
|
});
|
|
557
572
|
cb(null, { success: true, data: prefs });
|
|
@@ -573,12 +588,15 @@ const handleCliCommand = async (data, cb, argv) => {
|
|
|
573
588
|
const themePrincipal = principalForCliScript(data.headers, {
|
|
574
589
|
mode: argv["principalMode"] || "observe",
|
|
575
590
|
});
|
|
576
|
-
|
|
591
|
+
/* Doc 6 on Firebird (2026-10-01) — one transaction on either engine: a PostgreSQL
|
|
592
|
+
transaction, or ONE Firebird EXECUTE BLOCK (engine/orm/tenantSql.js). */
|
|
593
|
+
const themeDb = tenantSqlFor(argv["dbindex"]);
|
|
594
|
+
const themeExec = themeDb.exec;
|
|
577
595
|
if (data.action === "write") {
|
|
578
596
|
const saved = await writeCompanyTheme({
|
|
579
597
|
exec: themeExec,
|
|
580
|
-
execStatements:
|
|
581
|
-
|
|
598
|
+
execStatements: themeDb.atomic,
|
|
599
|
+
sql: themeDb,
|
|
582
600
|
principal: themePrincipal,
|
|
583
601
|
theme: data.theme,
|
|
584
602
|
});
|
|
@@ -612,14 +630,17 @@ const handleCliCommand = async (data, cb, argv) => {
|
|
|
612
630
|
const logoPrincipal = principalForCliScript(data.headers, {
|
|
613
631
|
mode: argv["principalMode"] || "observe",
|
|
614
632
|
});
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
633
|
+
/* Doc 6 on Firebird (2026-10-01) — the tenant seam; on Firebird a logo is staged in
|
|
634
|
+
blocks (runBlocks) and swapped in atomically (companyLogo.js). */
|
|
635
|
+
const logoDb = tenantSqlFor(argv["dbindex"]);
|
|
636
|
+
const logoExec = logoDb.exec;
|
|
618
637
|
|
|
619
638
|
if (data.action === "write") {
|
|
620
639
|
const saved = await writeCompanyLogo({
|
|
621
640
|
exec: logoExec,
|
|
622
|
-
execStatements:
|
|
641
|
+
execStatements: logoDb.atomic,
|
|
642
|
+
sql: logoDb,
|
|
643
|
+
runBlocks: logoDb.runBlocks,
|
|
623
644
|
principal: logoPrincipal,
|
|
624
645
|
dataUrl: data.dataUrl,
|
|
625
646
|
});
|
|
@@ -633,7 +654,8 @@ const handleCliCommand = async (data, cb, argv) => {
|
|
|
633
654
|
} else if (data.action === "clear") {
|
|
634
655
|
const cleared = await clearCompanyLogo({
|
|
635
656
|
exec: logoExec,
|
|
636
|
-
execStatements:
|
|
657
|
+
execStatements: logoDb.atomic,
|
|
658
|
+
sql: logoDb,
|
|
637
659
|
principal: logoPrincipal,
|
|
638
660
|
});
|
|
639
661
|
cb(null, { success: cleared.ok, problems: cleared.problems, data: null });
|
|
@@ -645,6 +667,34 @@ const handleCliCommand = async (data, cb, argv) => {
|
|
|
645
667
|
}
|
|
646
668
|
break;
|
|
647
669
|
}
|
|
670
|
+
/*
|
|
671
|
+
* Doc 1 step 0a.2 — Control Center drafts (engine/cc/draftStore.js). Administrators only,
|
|
672
|
+
* from the principal in the HEADERS; a draft never reaches SYS$CONFIG or SYS$BPM_* (only
|
|
673
|
+
* publish may). A refusal the user can act on is returned as `problems`, as companytheme
|
|
674
|
+
* does; `code` carries CC_DRAFT_STALE for a save made from an old revision.
|
|
675
|
+
*/
|
|
676
|
+
case "ccdraft": {
|
|
677
|
+
const ccResult = await runDraftAction({
|
|
678
|
+
repo: createPgDraftRepo({
|
|
679
|
+
exec: (sql) => execPostgres(sql, argv["dbindex"]),
|
|
680
|
+
execStatements: (statements) => execPostgresStatements(statements, argv["dbindex"]),
|
|
681
|
+
}),
|
|
682
|
+
principal: principalForCliScript(data.headers, { mode: argv["principalMode"] || "observe" }),
|
|
683
|
+
isPostgres: isPostgresIndex(argv["dbindex"]),
|
|
684
|
+
action: data.action,
|
|
685
|
+
args: data.args,
|
|
686
|
+
/* 0b.3: publish writes SYS$CONFIG, SYS$APPS and SYS$BPM_* through the existing pipelines —
|
|
687
|
+
loaded only for a publish (appConfig.js imports this file, and must not pull the BPM engine in). */
|
|
688
|
+
pipelines: data.action === "publish" ? ((m) => (userId) => m.createLivePipelines({ argv, userId }))(await import("../engine/cc/livePipelines.js")) : undefined,
|
|
689
|
+
});
|
|
690
|
+
cb(null, {
|
|
691
|
+
success: ccResult.ok,
|
|
692
|
+
...(ccResult.code ? { code: ccResult.code } : {}),
|
|
693
|
+
problems: ccResult.problems ?? [],
|
|
694
|
+
data: ccResult.data ?? null,
|
|
695
|
+
});
|
|
696
|
+
break;
|
|
697
|
+
}
|
|
648
698
|
case "runextcliscript": {
|
|
649
699
|
// External-access op invoker — enforces the capability boundary (dispatch.js),
|
|
650
700
|
// unlike raw `runcliscript`. Safe to expose: no valid token -> UNAUTHENTICATED.
|
|
@@ -836,6 +886,11 @@ export const clientListener = (socket, argv, via = "?") => {
|
|
|
836
886
|
maxContentLength: Infinity,
|
|
837
887
|
})
|
|
838
888
|
.then((response) => {
|
|
889
|
+
/* Doc 6 on Firebird — a login FINA answered gets its principal, on this
|
|
890
|
+
transport as on the HTTP one (finaEndpoint.js); see apiRoute.js. */
|
|
891
|
+
if (parseAuthRequest(reqData.path, reqBody)) {
|
|
892
|
+
response.data = withFinaLoginPrincipal(response.data, Number(argv["dbindex"]));
|
|
893
|
+
}
|
|
839
894
|
resCB(null, socketResponse(response));
|
|
840
895
|
})
|
|
841
896
|
.catch((error) => {
|
package/config/secrets.json
CHANGED
|
@@ -1,21 +1,27 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
2
|
+
"supertail.ledger": {
|
|
3
|
+
"currentKeyId": "st_ledger_v1",
|
|
4
|
+
"keys": {
|
|
5
|
+
"st_ledger_v1": "CdgBjUShgx7t9Wo/dfSOOJAHjHOD5KwAlRi6xwDMEfs="
|
|
6
|
+
}
|
|
7
|
+
},
|
|
8
|
+
"supertail.session": "3jepiFkSV6HxlbCSWiGqYWpmfapYxLHeHY/YgxyFQ8Q=",
|
|
9
|
+
"ext.capability": {
|
|
10
|
+
"currentKeyId": "ext_v1",
|
|
11
|
+
"keys": {
|
|
12
|
+
"ext_v1": "f4PLkG2QLn6OFyv0IlqWhq3tlSGs2VAyaYDM4kDKLZ8="
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"internal.principal": {
|
|
16
|
+
"currentKeyId": "ip_v1",
|
|
17
|
+
"keys": {
|
|
18
|
+
"ip_v1": "GsoRIpPG0D7c0q+AtmMNrtKpZ104Uv4/tfmLHtAtbIM="
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"biza.ai.studiob": {
|
|
22
|
+
"currentKeyId": "ai_v1",
|
|
23
|
+
"keys": {
|
|
24
|
+
"ai_v1": "QxE6YiZdcjVRK1UbWGRMLuEgBKM3zFUte9euFldduZc="
|
|
25
|
+
}
|
|
26
|
+
}
|
|
21
27
|
}
|
package/db/db.js
CHANGED
|
@@ -115,11 +115,11 @@ export const list = async (payload, config = {}) => {
|
|
|
115
115
|
};
|
|
116
116
|
|
|
117
117
|
/* Refused on PostgreSQL — the bodies are Firebird PSQL. See executor.js for the reasoning. */
|
|
118
|
-
export const executeBlock = async (sqlString, config = {}) => {
|
|
118
|
+
export const executeBlock = async (sqlString, config = {}, options = {}) => {
|
|
119
119
|
if (onPostgres(config)) {
|
|
120
120
|
return await executor.executeBlock(sqlString, resolveDbIndex(config));
|
|
121
121
|
}
|
|
122
|
-
return await adapter.executeBlock(sqlString, config);
|
|
122
|
+
return await adapter.executeBlock(sqlString, config, options);
|
|
123
123
|
};
|
|
124
124
|
|
|
125
125
|
/* An optimistic claim: flip one row's status if it is still in an expected state, and report
|
package/db/ds.js
CHANGED
|
@@ -261,15 +261,34 @@ function flattenSql(sqlString) {
|
|
|
261
261
|
.join(" "); // 5. Join safely with exactly one space
|
|
262
262
|
}
|
|
263
263
|
|
|
264
|
-
|
|
264
|
+
/* `flatten: false` sends the SQL as written — for a block that is single-line already and carries a
|
|
265
|
+
text VALUE, where flattenSql's `--` stripping would cut the value (engine/domain/finaBlocks.js). */
|
|
266
|
+
export const executeBlock = async (sqlString, config = {}, { flatten = true } = {}) => {
|
|
265
267
|
const cfg = resolveConfig(config);
|
|
266
268
|
const url = buildDataSnapUrl(cfg, "block");
|
|
267
|
-
const flatSql = flattenSql(sqlString);
|
|
269
|
+
const flatSql = flatten ? flattenSql(sqlString) : sqlString;
|
|
268
270
|
// const data = buildDataSnapPayload(cfg, "block", sqlString);
|
|
269
271
|
const data = buildDataSnapPayload(cfg, "block", flatSql);
|
|
270
272
|
return await dsReq(url, data, cfg.timeout);
|
|
271
273
|
};
|
|
272
274
|
|
|
275
|
+
/* FINA's ExecuteSQL: one raw statement, positional parameters `[sql, dbIndex]` (not the ORM envelope),
|
|
276
|
+
rows as a JSON string inside `result[0]` (an empty string when there are none). Keys are lower-cased,
|
|
277
|
+
as execPostgres returns them, so a caller reads one shape on either engine (engine/orm/tenantSql.js).
|
|
278
|
+
The SQL must be single-line: a raw newline ends the command on this transport. */
|
|
279
|
+
export const executeSql = async (sql, config = {}) => {
|
|
280
|
+
const cfg = resolveConfig({ ...config, methodClass: "TFinaMethod" });
|
|
281
|
+
const url = buildDataSnapUrl(cfg, "ExecuteSQL");
|
|
282
|
+
const res = await dsReq(url, JSON.stringify({ _parameters: [sql, String(cfg.dbindex)] }), cfg.timeout);
|
|
283
|
+
const first = res?.result?.[0];
|
|
284
|
+
if (first && typeof first === "object" && first.error) throw new Error(first.error);
|
|
285
|
+
if (typeof first !== "string" || first === "") return [];
|
|
286
|
+
const rows = JSON.parse(first)?.data;
|
|
287
|
+
return (Array.isArray(rows) ? rows : []).map((row) =>
|
|
288
|
+
Object.fromEntries(Object.entries(row).map(([key, value]) => [key.toLowerCase(), value])),
|
|
289
|
+
);
|
|
290
|
+
};
|
|
291
|
+
|
|
273
292
|
export const ormGet = async (payload, config = {}) => {
|
|
274
293
|
const cfg = resolveConfig(config);
|
|
275
294
|
const url = buildDataSnapUrl(cfg, "orm");
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Doc 1 step 0b.2 — DOMAIN_RULE guards (design CC-DRAFTS-AND-ENGINE-GAPS §5B).
|
|
3
|
+
*
|
|
4
|
+
* A rule key names a REGISTERED CLI script (SYS$CLI_SCRIPT.SCRIPT_NAME = the key). The engine runs it with
|
|
5
|
+
* { entityType, entityId, entityData, transitionCode, principal } and reads back { pass, hint? }:
|
|
6
|
+
*
|
|
7
|
+
* - FAIL-CLOSED: only `pass === true` passes. A missing script (the worker answers with a string, it does
|
|
8
|
+
* not throw), an error, or no answer within the limit all block, with a hint naming the rule.
|
|
9
|
+
* - Rolled out like --principalMode: `observe` evaluates and logs every would-block but lets the
|
|
10
|
+
* transition pass (today's behaviour, with evidence); `enforce` blocks. Absent = observe; an
|
|
11
|
+
* unrecognised value = enforce.
|
|
12
|
+
*
|
|
13
|
+
* The principal is the caller's CLAIM (userId, userRoles) marked `verified: false` — no verified principal
|
|
14
|
+
* reaches the BPM runtime yet (ruling 2026-09-29).
|
|
15
|
+
*
|
|
16
|
+
* Main-thread only (it runs scripts through the worker pool); rules.js stays pure and takes the evaluator
|
|
17
|
+
* it is given.
|
|
18
|
+
*/
|
|
19
|
+
export const RULE_TIMEOUT_MS = 2000;
|
|
20
|
+
|
|
21
|
+
export const ruleModeOf = (config) => {
|
|
22
|
+
const mode = config?.ruleMode;
|
|
23
|
+
if (!mode || mode === "observe") return "observe";
|
|
24
|
+
return "enforce";
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const seconds = (ms) => `${ms / 1000} s`;
|
|
28
|
+
|
|
29
|
+
/* The verdict of one rule script: { pass, hint }. Never throws. */
|
|
30
|
+
export async function judgeRule(key, ctx, { runScript, timeoutMs = RULE_TIMEOUT_MS }) {
|
|
31
|
+
let timer;
|
|
32
|
+
try {
|
|
33
|
+
const running = runScript(key, ctx);
|
|
34
|
+
/* workerpool can also stop the worker itself once the task leaves the queue. */
|
|
35
|
+
if (running && typeof running.timeout === "function") running.timeout(timeoutMs);
|
|
36
|
+
const expired = new Promise((_, reject) => {
|
|
37
|
+
timer = setTimeout(() => reject(Object.assign(new Error("timeout"), { ruleTimeout: true })), timeoutMs);
|
|
38
|
+
});
|
|
39
|
+
const result = await Promise.race([running, expired]);
|
|
40
|
+
|
|
41
|
+
if (typeof result === "string" && /not found/i.test(result)) {
|
|
42
|
+
return { pass: false, hint: `Rule ${key} is not registered: there is no CLI script of that name.` };
|
|
43
|
+
}
|
|
44
|
+
if (result && typeof result === "object" && result.pass === true) return { pass: true, hint: "" };
|
|
45
|
+
const hint = result && typeof result === "object" && typeof result.hint === "string" && result.hint ? result.hint : `Rule ${key} did not pass.`;
|
|
46
|
+
return { pass: false, hint };
|
|
47
|
+
} catch (error) {
|
|
48
|
+
if (error?.ruleTimeout || error?.name === "TimeoutError") return { pass: false, hint: `Rule ${key} did not answer within ${seconds(timeoutMs)}.` };
|
|
49
|
+
return { pass: false, hint: `Rule ${key} failed: ${error?.message ?? error}` };
|
|
50
|
+
} finally {
|
|
51
|
+
clearTimeout(timer);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/* evaluateDomain(key, ctx) → { pass, hint }, with the rollout mode applied. */
|
|
56
|
+
export function createDomainEvaluator({ mode, runScript, timeoutMs = RULE_TIMEOUT_MS, logger }) {
|
|
57
|
+
return async function evaluateDomain(key, ctx) {
|
|
58
|
+
const verdict = await judgeRule(key, ctx, { runScript, timeoutMs });
|
|
59
|
+
const where = `${ctx.transitionCode} on ${ctx.entityType} ${ctx.entityId}: ${key}`;
|
|
60
|
+
if (verdict.pass) {
|
|
61
|
+
logger?.debug?.(`[rules] passed ${where} (mode=${mode})`);
|
|
62
|
+
return verdict;
|
|
63
|
+
}
|
|
64
|
+
if (mode === "observe") {
|
|
65
|
+
logger?.warn?.(`[rules] would block ${where} — ${verdict.hint} (mode=observe)`);
|
|
66
|
+
return { pass: true, hint: "" };
|
|
67
|
+
}
|
|
68
|
+
logger?.info?.(`[rules] blocked ${where} — ${verdict.hint} (mode=${mode})`);
|
|
69
|
+
return verdict;
|
|
70
|
+
};
|
|
71
|
+
}
|
package/engine/bpm/routes.js
CHANGED
|
@@ -24,6 +24,8 @@ const expressWrapper = (engineFunction) => async (req, res) => {
|
|
|
24
24
|
// ==========================================
|
|
25
25
|
router.post("/workflow/save", expressWrapper(workflow.save));
|
|
26
26
|
router.post("/workflow/publish", expressWrapper(workflow.publish));
|
|
27
|
+
/* Doc 1 0b.1: what publish would refuse, without publishing (read-only). */
|
|
28
|
+
router.post("/workflow/validate", expressWrapper(workflow.validate));
|
|
27
29
|
router.post("/workflow/remove", expressWrapper(workflow.remove));
|
|
28
30
|
|
|
29
31
|
// ==========================================
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/* Types of rules.js for Control Center (bcc imports the engine's rules directly — Doc 1 0a.6). */
|
|
2
|
+
|
|
3
|
+
/* Doc 1 0b.2: a DOMAIN_RULE verdict (the engine injects the rule-script evaluator). */
|
|
4
|
+
export type DomainEvaluate = (key: string, transitionCode?: string) => Promise<{ pass: boolean; hint: string }>;
|
|
5
|
+
|
|
6
|
+
export type GuardEvaluate = (expr: string, m: unknown) => unknown | Promise<unknown>;
|
|
7
|
+
|
|
8
|
+
export interface CompiledRole {
|
|
9
|
+
role_code: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface CompiledEffect {
|
|
13
|
+
effect_type: string;
|
|
14
|
+
event_name: string;
|
|
15
|
+
payload_template: string | null;
|
|
16
|
+
sort_no: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface CompiledState {
|
|
20
|
+
state_code: string;
|
|
21
|
+
label: string;
|
|
22
|
+
task_enabled: 0 | 1;
|
|
23
|
+
task_name: string | null;
|
|
24
|
+
assignment_type: string | null;
|
|
25
|
+
assignment_value: string | null;
|
|
26
|
+
sla_duration_minutes: number | null;
|
|
27
|
+
timeout_action_type: string | null;
|
|
28
|
+
timeout_event_name: string | null;
|
|
29
|
+
sort_no: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface CompiledTransition {
|
|
33
|
+
transition_code: string;
|
|
34
|
+
from_state_code: string;
|
|
35
|
+
to_state_code: string;
|
|
36
|
+
trigger_type: string;
|
|
37
|
+
label: string | undefined;
|
|
38
|
+
guard_type: string | null;
|
|
39
|
+
guard_rule_key: string | null;
|
|
40
|
+
sort_no: number;
|
|
41
|
+
roles: CompiledRole[];
|
|
42
|
+
effects: CompiledEffect[];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface CompiledDefinition {
|
|
46
|
+
workflow: { workflow_key: string; name: string; entity_type: string; description: string; initial_state_code: string };
|
|
47
|
+
states: CompiledState[];
|
|
48
|
+
transitions: CompiledTransition[];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface GuardResult {
|
|
52
|
+
isPassed: boolean;
|
|
53
|
+
errorMsg: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export type WithGuard<T> = T & { isGuardPassed: boolean; guardHint: string };
|
|
57
|
+
|
|
58
|
+
export interface EntryTimer {
|
|
59
|
+
id: null;
|
|
60
|
+
sys$bpm_wf_instance_id: number | null;
|
|
61
|
+
sys$bpm_task_id?: null;
|
|
62
|
+
state_code: string;
|
|
63
|
+
timer_type: 'SLA_TIMEOUT';
|
|
64
|
+
sla_minutes: number;
|
|
65
|
+
status: 'OPEN';
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface EntryTask {
|
|
69
|
+
id: null;
|
|
70
|
+
state_code: string;
|
|
71
|
+
name: string | null;
|
|
72
|
+
status: 'OPEN';
|
|
73
|
+
assignment_type: string | null;
|
|
74
|
+
assignment_value: string | null;
|
|
75
|
+
assigned_user_id: number | null;
|
|
76
|
+
sla_minutes: number | null;
|
|
77
|
+
SYS$BPM_TIMER_JOB?: EntryTimer[];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export type TimerOutcome<T> =
|
|
81
|
+
| { kind: 'EMIT'; eventName: string }
|
|
82
|
+
| { kind: 'AUTO'; transition: T }
|
|
83
|
+
| { kind: 'ERROR'; message: string }
|
|
84
|
+
| { kind: 'NONE' };
|
|
85
|
+
|
|
86
|
+
export const DEFAULT_TRIGGER: 'USER_EVENT';
|
|
87
|
+
export const SYSTEM_ROLES: string[];
|
|
88
|
+
|
|
89
|
+
export function compileDefinition(json: Record<string, any>): CompiledDefinition;
|
|
90
|
+
export function permitsRoles(roleCodes: string[] | null | undefined, userRoles: string[]): boolean;
|
|
91
|
+
export function guardVerdict(
|
|
92
|
+
guardType: string | null,
|
|
93
|
+
guardRuleKey: string | null,
|
|
94
|
+
entityData: unknown,
|
|
95
|
+
evaluate: GuardEvaluate,
|
|
96
|
+
evaluateDomain?: (key: string) => Promise<{ pass: boolean; hint: string }>,
|
|
97
|
+
): Promise<GuardResult>;
|
|
98
|
+
export function withGuardVerdicts<T extends { guard_type: string | null; guard_rule_key: string | null }>(
|
|
99
|
+
transitions: T[],
|
|
100
|
+
entityData: unknown,
|
|
101
|
+
evaluate: GuardEvaluate,
|
|
102
|
+
evaluateDomain?: DomainEvaluate,
|
|
103
|
+
): Promise<WithGuard<T>[]>;
|
|
104
|
+
export function selectTransitions(
|
|
105
|
+
transitions: CompiledTransition[],
|
|
106
|
+
options: { fromState: string; triggerType?: string; userRoles?: string[]; m?: unknown; evaluate: GuardEvaluate; evaluateDomain?: DomainEvaluate },
|
|
107
|
+
): Promise<WithGuard<CompiledTransition>[]>;
|
|
108
|
+
export function isTerminal(transitions: CompiledTransition[], stateCode: string): boolean;
|
|
109
|
+
export function stateEntry(
|
|
110
|
+
targetStateDef: CompiledState | null | undefined,
|
|
111
|
+
terminal: boolean,
|
|
112
|
+
instanceId: number | string | null,
|
|
113
|
+
): { tasks: EntryTask[]; standaloneTimers: EntryTimer[] };
|
|
114
|
+
export function timerOutcome<T extends { guard_type: string | null; guard_rule_key: string | null }>(
|
|
115
|
+
stateRow: CompiledState | null | undefined,
|
|
116
|
+
stateCode: string,
|
|
117
|
+
loadTimerTransitions: () => Promise<T[]>,
|
|
118
|
+
evaluate: GuardEvaluate,
|
|
119
|
+
evaluateDomain?: DomainEvaluate,
|
|
120
|
+
): Promise<TimerOutcome<T>>;
|
|
121
|
+
export function effectPayload(payloadTemplate: string | null | undefined, effectData: Record<string, unknown> | null | undefined): string | null;
|