biz-a-cli 2.3.80-15375 → 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 +72 -55
- package/bin/directHubEvent.js +87 -5
- 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/domainNavigation.js +17 -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 +2 -2
- 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,9 +51,11 @@ 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";
|
|
58
|
+
import { resolveMenuLayout } from "../engine/domain/domainNavigation.js";
|
|
58
59
|
|
|
59
60
|
const getKeyFolderPath = () => {
|
|
60
61
|
const scriptPath =
|
|
@@ -154,6 +155,13 @@ const addCommandOptions = {
|
|
|
154
155
|
demandOption: false,
|
|
155
156
|
default: false,
|
|
156
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
|
+
},
|
|
157
165
|
};
|
|
158
166
|
|
|
159
167
|
const removeCommandOptions = {
|
|
@@ -376,9 +384,6 @@ const APPLICATION_CONFIG_FILE = "application.js";
|
|
|
376
384
|
/* Doc 6 §8.1 — the application theme source. JSON, because it is DATA that reaches the browser
|
|
377
385
|
via sys$config and must never be evaluated (AC 26). */
|
|
378
386
|
const THEME_CONFIG_FILE = "theme.json";
|
|
379
|
-
/* Parity with the admin editor (appConfig.component.ts executeBlockBodyLimit): the DataSnap
|
|
380
|
-
`block` call body must stay under 64KB, so an oversized config is upserted in chunks. */
|
|
381
|
-
const EXECUTE_BLOCK_BODY_LIMIT = 2 ** 16;
|
|
382
387
|
|
|
383
388
|
const findApplicationConfigFile = (fileNames = []) =>
|
|
384
389
|
fileNames.find(
|
|
@@ -404,7 +409,7 @@ const isConfigModeDir = (workingDir, files = null) => {
|
|
|
404
409
|
flattenSql(), whose `--` comment stripping mangles a config upsert — the whole config is embedded
|
|
405
410
|
as a single-line quoted literal, so the first `--` ANYWHERE in the config text (a JS comment, a
|
|
406
411
|
CSS custom property, an HTML template) silently truncates the rest of the statement and Firebird
|
|
407
|
-
reports "Unexpected end of command".
|
|
412
|
+
reports "Unexpected end of command". getChunkedUpsertBlocks already emits single-line SQL, so there is
|
|
408
413
|
nothing to flatten. This mirrors what the admin editor sends (dataService.executeBlock). */
|
|
409
414
|
const executeConfigBlock = async (blockSql, { server, apiPort, dbIndex }) => {
|
|
410
415
|
const url = `${server}:${apiPort}/fina/rest/TOrmMethod/%22block%22`;
|
|
@@ -424,43 +429,9 @@ const executeConfigBlock = async (blockSql, { server, apiPort, dbIndex }) => {
|
|
|
424
429
|
return parsed;
|
|
425
430
|
};
|
|
426
431
|
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
JSON.stringify({ dbIndex, method: "block", object: blockSql }),
|
|
431
|
-
],
|
|
432
|
-
}).length;
|
|
433
|
-
|
|
434
|
-
/* One upsert block when it fits, else halve the chunk size until every block fits. Mirrors
|
|
435
|
-
appConfig.component.ts resolveUpsertBlocksByBodyLimit. */
|
|
436
|
-
const resolveUpsertBlocks = (name, version, content, dbIndex) => {
|
|
437
|
-
const singleBlock = getUpsertBlock(name, version, content);
|
|
438
|
-
if (
|
|
439
|
-
estimateExecuteBlockBodyLength(singleBlock, dbIndex) <=
|
|
440
|
-
EXECUTE_BLOCK_BODY_LIMIT
|
|
441
|
-
) {
|
|
442
|
-
return [singleBlock];
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
let chunkSize = EXECUTE_BLOCK_BODY_LIMIT;
|
|
446
|
-
while (chunkSize > 0) {
|
|
447
|
-
const blocks = getChunkedUpsertBlocks(name, version, content, chunkSize);
|
|
448
|
-
if (
|
|
449
|
-
blocks.every(
|
|
450
|
-
(block) =>
|
|
451
|
-
estimateExecuteBlockBodyLength(block, dbIndex) <=
|
|
452
|
-
EXECUTE_BLOCK_BODY_LIMIT,
|
|
453
|
-
)
|
|
454
|
-
) {
|
|
455
|
-
return blocks;
|
|
456
|
-
}
|
|
457
|
-
chunkSize = Math.floor(chunkSize / 2);
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
throw new Error(
|
|
461
|
-
`Failed to split "${name}" into executeBlock-sized chunks.`,
|
|
462
|
-
);
|
|
463
|
-
};
|
|
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);
|
|
464
435
|
|
|
465
436
|
/* PURE-ish (reads files, no network): resolve every config in the directory and validate the
|
|
466
437
|
WHOLE set before anything is uploaded, so a stray lib or a typo'd metadata can never leave a
|
|
@@ -727,6 +698,7 @@ async function uploadConfigs({
|
|
|
727
698
|
sub,
|
|
728
699
|
publish = false,
|
|
729
700
|
forceFull = false,
|
|
701
|
+
releaseCc = false,
|
|
730
702
|
serverMode = "prod",
|
|
731
703
|
}) {
|
|
732
704
|
/* Phase B2: ask biz-a for this tenant's database coordinates before anything reads the
|
|
@@ -749,6 +721,19 @@ async function uploadConfigs({
|
|
|
749
721
|
timeout: 60000,
|
|
750
722
|
};
|
|
751
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
|
+
|
|
752
737
|
const saveOne = async ({ name, version, content }) => {
|
|
753
738
|
/* A PostgreSQL index writes straight to its pool. No chunking: the block splitting below
|
|
754
739
|
exists only because the FINA HTTP body has a size limit, which a direct connection does
|
|
@@ -922,13 +907,39 @@ const parseApplicationConfigData = (rawConfig) => {
|
|
|
922
907
|
}
|
|
923
908
|
};
|
|
924
909
|
|
|
925
|
-
|
|
910
|
+
/*
|
|
911
|
+
* What the stored APPLICATION_CONFIG contributes to the SYS$APPS metadata: the display name (which
|
|
912
|
+
* also keys the row) and the menu layout the client shell draws.
|
|
913
|
+
*/
|
|
914
|
+
const resolveAppMetadataFromApplicationConfig = async ({
|
|
926
915
|
server,
|
|
927
916
|
apiPort,
|
|
928
917
|
dbIndex,
|
|
929
918
|
sub,
|
|
930
919
|
}) => {
|
|
931
|
-
|
|
920
|
+
const applicationConfig = await readStoredApplicationConfig({
|
|
921
|
+
server,
|
|
922
|
+
apiPort,
|
|
923
|
+
dbIndex,
|
|
924
|
+
sub,
|
|
925
|
+
});
|
|
926
|
+
const displayName = applicationConfig?.metadata?.name;
|
|
927
|
+
const declaredLayout = applicationConfig?.navigation?.layout;
|
|
928
|
+
const menuLayout = resolveMenuLayout(applicationConfig);
|
|
929
|
+
if (declaredLayout != null && menuLayout == null) {
|
|
930
|
+
logger.warn(
|
|
931
|
+
`[Add] navigation.layout ${JSON.stringify(declaredLayout)} is not one of ` +
|
|
932
|
+
`"horizontal" or "vertical"; the client keeps its default menu.`,
|
|
933
|
+
);
|
|
934
|
+
}
|
|
935
|
+
return {
|
|
936
|
+
displayName: typeof displayName === "string" ? displayName.trim() : "",
|
|
937
|
+
menuLayout,
|
|
938
|
+
};
|
|
939
|
+
};
|
|
940
|
+
|
|
941
|
+
const readStoredApplicationConfig = async ({ server, apiPort, dbIndex, sub }) => {
|
|
942
|
+
/* A PostgreSQL tenant reads SYS$CONFIG directly. This must not silently come back empty: the
|
|
932
943
|
display name keys the SYS$APPS row, so falling back to the random bundle name would insert
|
|
933
944
|
a fresh app row on every publish instead of updating the existing one. */
|
|
934
945
|
if (isPostgresIndex(dbIndex)) {
|
|
@@ -937,15 +948,14 @@ const resolveAppDisplayNameFromApplicationConfig = async ({
|
|
|
937
948
|
buildPostgresApplicationConfigLookupSql(),
|
|
938
949
|
dbIndex,
|
|
939
950
|
);
|
|
940
|
-
|
|
941
|
-
return typeof displayName === "string" ? displayName.trim() : "";
|
|
951
|
+
return parseApplicationConfigData(rows?.[0]?.data);
|
|
942
952
|
} catch {
|
|
943
|
-
return
|
|
953
|
+
return null;
|
|
944
954
|
}
|
|
945
955
|
}
|
|
946
956
|
|
|
947
957
|
if (typeof axios.request !== "function") {
|
|
948
|
-
return
|
|
958
|
+
return null;
|
|
949
959
|
}
|
|
950
960
|
|
|
951
961
|
const baseUrl = "fina/rest/TOrmMethod/%22list%22";
|
|
@@ -989,18 +999,16 @@ const resolveAppDisplayNameFromApplicationConfig = async ({
|
|
|
989
999
|
return name === "APPLICATION_CONFIG";
|
|
990
1000
|
});
|
|
991
1001
|
if (!applicationConfigRow) {
|
|
992
|
-
return
|
|
1002
|
+
return null;
|
|
993
1003
|
}
|
|
994
1004
|
|
|
995
|
-
|
|
1005
|
+
return parseApplicationConfigData(
|
|
996
1006
|
applicationConfigRow?.["SYS$CONFIG.DATA"] ??
|
|
997
1007
|
applicationConfigRow?.DATA ??
|
|
998
1008
|
applicationConfigRow?.data,
|
|
999
1009
|
);
|
|
1000
|
-
const displayName = applicationConfig?.metadata?.name;
|
|
1001
|
-
return typeof displayName === "string" ? displayName.trim() : "";
|
|
1002
1010
|
} catch {
|
|
1003
|
-
return
|
|
1011
|
+
return null;
|
|
1004
1012
|
}
|
|
1005
1013
|
};
|
|
1006
1014
|
|
|
@@ -1015,6 +1023,7 @@ async function addApp({
|
|
|
1015
1023
|
body = null,
|
|
1016
1024
|
publish = false,
|
|
1017
1025
|
forceFull = false,
|
|
1026
|
+
releaseCc = false,
|
|
1018
1027
|
serverMode = "prod",
|
|
1019
1028
|
/* Extra directories whose DOMAIN configs upload alongside this one's (see --include). */
|
|
1020
1029
|
include = [],
|
|
@@ -1071,6 +1080,7 @@ async function addApp({
|
|
|
1071
1080
|
sub,
|
|
1072
1081
|
publish,
|
|
1073
1082
|
forceFull,
|
|
1083
|
+
releaseCc,
|
|
1074
1084
|
serverMode,
|
|
1075
1085
|
});
|
|
1076
1086
|
}
|
|
@@ -1161,8 +1171,8 @@ async function addApp({
|
|
|
1161
1171
|
: `${server}:${apiPort}/${baseUrl}`;
|
|
1162
1172
|
|
|
1163
1173
|
const headers = { "Content-Type": "text/plain" };
|
|
1164
|
-
const appDisplayName =
|
|
1165
|
-
await
|
|
1174
|
+
const { displayName: appDisplayName, menuLayout } =
|
|
1175
|
+
await resolveAppMetadataFromApplicationConfig({
|
|
1166
1176
|
server,
|
|
1167
1177
|
apiPort,
|
|
1168
1178
|
dbIndex,
|
|
@@ -1172,6 +1182,11 @@ async function addApp({
|
|
|
1172
1182
|
if (useApplicationConfigName && appDisplayName.length > 0) {
|
|
1173
1183
|
keys.metadata["name"] = appDisplayName;
|
|
1174
1184
|
}
|
|
1185
|
+
/* navigation.layout -> how the client shell draws this app's menu. Only a value it
|
|
1186
|
+
understands is copied; without one the metadata stays exactly as before. */
|
|
1187
|
+
if (useApplicationConfigName && menuLayout) {
|
|
1188
|
+
keys.metadata["menuLayout"] = menuLayout;
|
|
1189
|
+
}
|
|
1175
1190
|
const uploadAppName = normalizeAppName(
|
|
1176
1191
|
(useApplicationConfigName ? appDisplayName : "") || bundleName,
|
|
1177
1192
|
);
|
|
@@ -1274,6 +1289,7 @@ async function runUnitTests(options) {
|
|
|
1274
1289
|
files: options.files,
|
|
1275
1290
|
publish: options.publish,
|
|
1276
1291
|
forceFull: options.forceFull,
|
|
1292
|
+
releaseCc: options.releaseCc,
|
|
1277
1293
|
serverMode: options.serverMode,
|
|
1278
1294
|
});
|
|
1279
1295
|
} else {
|
|
@@ -1307,6 +1323,7 @@ const buildCli = () =>
|
|
|
1307
1323
|
files: commandOptions.files,
|
|
1308
1324
|
publish: commandOptions.publish,
|
|
1309
1325
|
forceFull: commandOptions.forceFull,
|
|
1326
|
+
releaseCc: commandOptions.releaseCc,
|
|
1310
1327
|
serverMode: commandOptions.serverMode,
|
|
1311
1328
|
include: commandOptions.include,
|
|
1312
1329
|
});
|
package/bin/directHubEvent.js
CHANGED
|
@@ -6,36 +6,109 @@ import { CLIENT_ROOM } from "../message-broker/clientEmitter.js";
|
|
|
6
6
|
import { ClientEmitter } from "../message-broker/clientEmitter.js";
|
|
7
7
|
import { joinExtRooms } from "../engine/ext/esb.js";
|
|
8
8
|
import { extClientListener } from "../engine/ext/socketListener.js";
|
|
9
|
+
import { io as ioClient } from "socket.io-client";
|
|
10
|
+
|
|
9
11
|
import { prefixedLogger } from "../logger.js";
|
|
10
|
-
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";
|
|
11
13
|
|
|
12
14
|
const logger = prefixedLogger("[DirectHub]");
|
|
13
15
|
|
|
14
16
|
export async function localhostTunnel(port, notifier) {
|
|
15
17
|
let tunnelUrl = "";
|
|
16
18
|
let qt = QuickTunnel.quick("127.0.0.1:" + port);
|
|
19
|
+
let isReconnecting = false;
|
|
20
|
+
let loopbackSocket = null;
|
|
21
|
+
|
|
22
|
+
function destroyLoopback() {
|
|
23
|
+
if (loopbackSocket) {
|
|
24
|
+
loopbackSocket.disconnect();
|
|
25
|
+
loopbackSocket.close();
|
|
26
|
+
loopbackSocket = null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const gracefulProcessShutdown = () => {
|
|
31
|
+
destroyLoopback();
|
|
32
|
+
if (qt && typeof qt.stop === "function") {
|
|
33
|
+
try {
|
|
34
|
+
qt.stop();
|
|
35
|
+
} catch (e) {}
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
process.once("SIGINT", gracefulProcessShutdown);
|
|
39
|
+
process.once("SIGTERM", gracefulProcessShutdown);
|
|
40
|
+
|
|
17
41
|
function reconnectTunnel() {
|
|
42
|
+
process.off("SIGINT", gracefulProcessShutdown);
|
|
43
|
+
process.off("SIGTERM", gracefulProcessShutdown);
|
|
44
|
+
destroyLoopback();
|
|
45
|
+
|
|
46
|
+
if (isReconnecting) return;
|
|
47
|
+
isReconnecting = true;
|
|
48
|
+
|
|
49
|
+
if (qt && typeof qt.stop === "function") {
|
|
50
|
+
try {
|
|
51
|
+
qt.stop();
|
|
52
|
+
} catch (e) {}
|
|
53
|
+
}
|
|
54
|
+
|
|
18
55
|
setTimeout(() => {
|
|
19
56
|
qt = localhostTunnel(port, notifier);
|
|
20
57
|
}, RECONNECT_SOCKET_DELAY);
|
|
21
58
|
}
|
|
59
|
+
|
|
60
|
+
function startDirectHubHeartbeat() {
|
|
61
|
+
if (loopbackSocket && loopbackSocket.connected) return;
|
|
62
|
+
|
|
63
|
+
if (!tunnelUrl || !tunnelUrl.includes("trycloudflare.com")) {
|
|
64
|
+
logger.warn(`Invalid remote URL detected. Heartbeat aborted.`);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
destroyLoopback();
|
|
69
|
+
|
|
70
|
+
loopbackSocket = ioClient(tunnelUrl, {
|
|
71
|
+
query: { isLoopback: true },
|
|
72
|
+
transports: ["websocket"],
|
|
73
|
+
reconnectionDelay: 5000,
|
|
74
|
+
reconnectionAttempts: 3,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
loopbackSocket.on("connect", () => {
|
|
78
|
+
logger.debug(
|
|
79
|
+
`Heartbeat established via public internet. Cloudflare idle-drop prevented.`,
|
|
80
|
+
);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
loopbackSocket.io.on("reconnect_failed", () => {
|
|
84
|
+
logger.warn(
|
|
85
|
+
`Heartbeat failed to reach Edge. Ghost tunnel confirmed. Terminating...`,
|
|
86
|
+
);
|
|
87
|
+
notifier("");
|
|
88
|
+
destroyLoopback();
|
|
89
|
+
if (qt && typeof qt.stop === "function")
|
|
90
|
+
try {
|
|
91
|
+
qt.stop();
|
|
92
|
+
} catch (e) {}
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
22
96
|
qt.on("url", (qtUrl) => {
|
|
23
97
|
tunnelUrl = qtUrl;
|
|
24
98
|
})
|
|
25
99
|
.on("connected", (conn) => {
|
|
26
100
|
notifier(tunnelUrl);
|
|
27
101
|
logger.info(`Connected to Direct Hub at public URL ${tunnelUrl}`);
|
|
102
|
+
startDirectHubHeartbeat();
|
|
28
103
|
})
|
|
29
104
|
.on("disconnected", (conn) => {
|
|
30
105
|
notifier("");
|
|
31
|
-
qt.stop();
|
|
32
106
|
logger.info(
|
|
33
107
|
`Disconnected from edge node. Awaiting native reconnect...`,
|
|
34
108
|
);
|
|
35
109
|
})
|
|
36
110
|
.on("error", (err) => {
|
|
37
111
|
notifier("");
|
|
38
|
-
qt.stop();
|
|
39
112
|
logger.error(
|
|
40
113
|
`Tunnel error: ${err.message || err}. Awaiting native reconnect...`,
|
|
41
114
|
);
|
|
@@ -91,7 +164,10 @@ export function directHubEvent(serverSocket, argv) {
|
|
|
91
164
|
if (clientInstanceId) {
|
|
92
165
|
clientSocket.join(clientInstanceId);
|
|
93
166
|
clientSocket.on("ESB_ACK", (msgId) => {
|
|
94
|
-
ClientEmitter.acknowledgeMessage(
|
|
167
|
+
ClientEmitter.acknowledgeMessage(
|
|
168
|
+
msgId,
|
|
169
|
+
clientInstanceId,
|
|
170
|
+
);
|
|
95
171
|
});
|
|
96
172
|
}
|
|
97
173
|
|
|
@@ -108,6 +184,8 @@ export function directHubEvent(serverSocket, argv) {
|
|
|
108
184
|
} else if (clientSocket.handshake.query.isDeploy) {
|
|
109
185
|
setConnectListeners(clientSocket);
|
|
110
186
|
deploymentListenerForVSCode(clientSocket, argv);
|
|
187
|
+
} else if (clientSocket.handshake.query.isLoopback) {
|
|
188
|
+
setConnectListeners(clientSocket);
|
|
111
189
|
} else {
|
|
112
190
|
clientSocket.disconnect();
|
|
113
191
|
}
|
|
@@ -125,10 +203,12 @@ export function createSocketServer(httpServer, cliIpAddress = "127.0.0.1") {
|
|
|
125
203
|
TEST_URL,
|
|
126
204
|
ADMIN_URL,
|
|
127
205
|
TEST_ADMIN_URL,
|
|
206
|
+
CC_URL,
|
|
128
207
|
// biz-a.id is still served alongside biz-a.app, so its browser origins stay allowed.
|
|
129
208
|
"https://biz-a.id",
|
|
130
209
|
"https://test.biz-a.id",
|
|
131
210
|
"https://admin.biz-a.id",
|
|
211
|
+
"https://cc.biz-a.id",
|
|
132
212
|
// NOT the wildcard (see above):
|
|
133
213
|
// /\.biz-a\.id$/,
|
|
134
214
|
"vscode-file://vscode-app",
|
|
@@ -139,8 +219,10 @@ export function createSocketServer(httpServer, cliIpAddress = "127.0.0.1") {
|
|
|
139
219
|
`http://${cliIpAddress}:4200`,
|
|
140
220
|
"http://localhost:4200",
|
|
141
221
|
"http://localhost",
|
|
222
|
+
// Control Center (bcc) dev server, beside the client on 4200.
|
|
223
|
+
"http://localhost:4300",
|
|
142
224
|
]
|
|
143
|
-
: [`http://${cliIpAddress}:4200`, "http://localhost:4200"],
|
|
225
|
+
: [`http://${cliIpAddress}:4200`, "http://localhost:4200", "http://localhost:4300"],
|
|
144
226
|
),
|
|
145
227
|
},
|
|
146
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
|