biz-a-cli 2.3.80-15419 → 2.3.80

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 (80) hide show
  1. package/bin/app.js +20 -255
  2. package/bin/hub.js +0 -16
  3. package/bin/hubEvent.js +5 -98
  4. package/bin/tunnelTarget.js +0 -4
  5. package/engine/bpm/workflow.js +6 -7
  6. package/engine/cc/configValidate.js +0 -53
  7. package/engine/cc/guides/V2-SCREEN-AUTHORING.md +512 -531
  8. package/engine/cc/layer3Behavior.js +1 -2
  9. package/engine/cc/layer3Schema.js +1 -3
  10. package/engine/cc/livePipelines.js +3 -9
  11. package/engine/cc/publish.js +105 -86
  12. package/engine/domain/appConfig.js +18 -108
  13. package/engine/domain/applicationConfig.js +12 -87
  14. package/engine/domain/dialect.js +0 -138
  15. package/engine/domain/domainNavigation.js +50 -86
  16. package/engine/domain/entityConfigLoader.js +4 -16
  17. package/engine/domain/numericScale.js +0 -2
  18. package/engine/domain/system.js +0 -2
  19. package/engine/ext/publicTheme.js +1 -8
  20. package/engine/orm/apiRoute.js +61 -187
  21. package/engine/orm/entityConfig.js +2 -32
  22. package/engine/orm/finaAuth.js +0 -0
  23. package/engine/orm/finaEndpoint.js +0 -19
  24. package/engine/orm/imgQuery.js +0 -92
  25. package/engine/orm/listQuery.js +1 -6
  26. package/package.json +1 -1
  27. package/worker/cliScriptWorker.js +0 -5
  28. package/engine/attachments/store.js +0 -195
  29. package/engine/domain/domainRegistration.js +0 -65
  30. package/engine/fina/approval.js +0 -141
  31. package/engine/fina/approvalService.js +0 -59
  32. package/engine/fina/attachments.js +0 -165
  33. package/engine/fina/client.js +0 -84
  34. package/engine/fina/config.js +0 -26
  35. package/engine/fina/context.js +0 -58
  36. package/engine/fina/document.js +0 -67
  37. package/engine/fina/documentWrite.js +0 -173
  38. package/engine/fina/documents/index.js +0 -28
  39. package/engine/fina/documents/jv.js +0 -105
  40. package/engine/fina/documents/jvWrite.js +0 -260
  41. package/engine/fina/documents/si.js +0 -40
  42. package/engine/fina/errors.js +0 -17
  43. package/engine/fina/features.js +0 -86
  44. package/engine/fina/guard.js +0 -102
  45. package/engine/fina/identity.js +0 -87
  46. package/engine/fina/live.js +0 -19
  47. package/engine/fina/lock.js +0 -35
  48. package/engine/fina/master.js +0 -331
  49. package/engine/fina/masters/coa.js +0 -104
  50. package/engine/fina/masters/currency.js +0 -39
  51. package/engine/fina/masters/department.js +0 -32
  52. package/engine/fina/masters/index.js +0 -21
  53. package/engine/fina/masters/project.js +0 -53
  54. package/engine/fina/period.js +0 -56
  55. package/engine/fina/print.js +0 -136
  56. package/engine/fina/readPolicy.js +0 -67
  57. package/engine/fina/recurring.js +0 -200
  58. package/engine/fina/route.js +0 -61
  59. package/engine/fina/routes/attachment.js +0 -41
  60. package/engine/fina/routes/document.js +0 -66
  61. package/engine/fina/routes/master.js +0 -26
  62. package/engine/fina/routes/period.js +0 -24
  63. package/engine/fina/routes/print.js +0 -19
  64. package/engine/fina/routes/recurring.js +0 -27
  65. package/engine/fina/routes/session.js +0 -23
  66. package/engine/fina/routes/setup.js +0 -58
  67. package/engine/fina/routes/submission.js +0 -46
  68. package/engine/fina/routes/template.js +0 -31
  69. package/engine/fina/settings.js +0 -159
  70. package/engine/fina/sql.js +0 -34
  71. package/engine/fina/submission.js +0 -290
  72. package/engine/fina/submissionCompare.js +0 -171
  73. package/engine/fina/submissionRepo.js +0 -114
  74. package/engine/fina/template.js +0 -118
  75. package/engine/fina/userMap.js +0 -79
  76. package/engine/orm/companyLocale.js +0 -74
  77. package/engine/orm/userApiDataset.js +0 -153
  78. package/migrations/postgres/1790600000000__fina.sql +0 -33
  79. package/migrations/postgres/1790700000000__attachment.sql +0 -29
  80. package/migrations/postgres/1790800000000__fina_jv_submission.sql +0 -41
package/bin/app.js CHANGED
@@ -27,8 +27,6 @@ import {
27
27
  } from "../engine/domain/themeConfig.js";
28
28
  import {
29
29
  APP_CONFIG_NAME,
30
- appConfigNameFor,
31
- secondaryAppNameOf,
32
30
  buildEnsureConfigTableSql,
33
31
  buildPostgresConfigUpsertSql,
34
32
  buildPostgresApplicationConfigLookupSql,
@@ -58,7 +56,6 @@ import { validateTenancyDeclaration } from "../engine/orm/tenancyDeclaration.js"
58
56
  import { logger } from "../logger.js";
59
57
  import { findRawColorsInSource } from "../engine/domain/styleColors.js";
60
58
  import { resolveMenuLayout } from "../engine/domain/domainNavigation.js";
61
- import { bumpPatch, registerDomainVersions, unregisterDomains } from "../engine/domain/domainRegistration.js";
62
59
 
63
60
  const getKeyFolderPath = () => {
64
61
  const scriptPath =
@@ -165,13 +162,6 @@ const addCommandOptions = {
165
162
  demandOption: false,
166
163
  default: false,
167
164
  },
168
- register: {
169
- describe:
170
- "Config mode for a directory of DOMAIN configs only (no application.js): upload them and ADD them to the tenant's application config, keeping everything else in it (other apps, Control Center's domains, menus). For a second application on a tenant. PostgreSQL only",
171
- type: "boolean",
172
- demandOption: false,
173
- default: false,
174
- },
175
165
  };
176
166
 
177
167
  const removeCommandOptions = {
@@ -405,9 +395,7 @@ const findApplicationConfigFile = (fileNames = []) =>
405
395
  gate exists to validate app scripts before they are bundled, and a config directory has nothing
406
396
  to test — worse, jest exits non-zero there ("Could not find a config file", after traversing up),
407
397
  which would abort the upload outright. */
408
- const isConfigModeDir = (workingDir, files = null, { register = false } = {}) => {
409
- /* Doc 8 — `--register` ships domain configs with no application.js: config mode by declaration. */
410
- if (register) return true;
398
+ const isConfigModeDir = (workingDir, files = null) => {
411
399
  try {
412
400
  return !!findApplicationConfigFile(
413
401
  getFileList({ workingDir: path.resolve(workingDir), files }),
@@ -537,15 +525,7 @@ const collectConfigUploads = ({ workingDir, fileNames = [], include = [] }) => {
537
525
  `${path.basename(applicationFile)}: metadata.version "${version}" must be semantic (x.y.z).`,
538
526
  );
539
527
  }
540
- /* Doc 8 (2026-10-10): an application.js declaring `metadata.app` is ANOTHER application on the tenant, saved
541
- as `APPLICATION_CONFIG.<app>` beside the host's (engine/domain/appConfig.js appConfigNameFor). */
542
- let name = APP_CONFIG_NAME;
543
- try {
544
- name = appConfigNameFor(parseConfigForVersion(content)?.metadata);
545
- } catch (error) {
546
- problems.push(`${path.basename(applicationFile)}: ${error?.message ?? error}`);
547
- }
548
- application = { name, version, content };
528
+ application = { name: APP_CONFIG_NAME, version, content };
549
529
  }
550
530
 
551
531
  /*
@@ -563,12 +543,7 @@ const collectConfigUploads = ({ workingDir, fileNames = [], include = [] }) => {
563
543
  const themeFile = fileNames.find(
564
544
  (f) => path.basename(String(f)).toLowerCase() === THEME_CONFIG_FILE,
565
545
  );
566
- if (themeFile && secondaryAppNameOf(application?.name)) {
567
- problems.push(
568
- `${path.basename(String(themeFile))}: the theme is the tenant's, published with its host application — ` +
569
- `not with ${application.name}.`,
570
- );
571
- } else if (themeFile) {
546
+ if (themeFile) {
572
547
  const { theme: parsedTheme, problems: themeProblems } = validateThemeSource(
573
548
  readConfig(themeFile),
574
549
  );
@@ -713,89 +688,6 @@ const preserveApplicationAdditions = async (application, apiConfig) => {
713
688
  };
714
689
  };
715
690
 
716
- /*
717
- * Doc 8 — `--register`: what the domains are added to, read BEFORE anything is written.
718
- *
719
- * ⚠️ The tenant's NEWEST application config, the row every reader takes (buildPostgresApplicationConfigLookupSql).
720
- * Refused when the directory carries its own application.js (that is the replacing mode), on Firebird (PostgreSQL
721
- * only, as Control Center), and when the tenant has no application config yet (there is nothing to add to — the
722
- * host application comes first).
723
- */
724
- const readRegistrationBase = async ({ application, dbIndex }) => {
725
- if (application) {
726
- throw new Error(
727
- "--register adds domains to the tenant's application config, but this directory has application.js, which would " +
728
- "REPLACE it. Upload the domains without application.js, or drop --register.",
729
- );
730
- }
731
- if (!isPostgresIndex(dbIndex)) {
732
- throw new Error(`--register needs a PostgreSQL database; index ${dbIndex} is not one.`);
733
- }
734
- const row = (await execPostgres(buildPostgresApplicationConfigLookupSql(), dbIndex))?.[0];
735
- if (!row?.data) {
736
- throw new Error(
737
- "--register: this database has no application config to add the domains to. Upload the host application first.",
738
- );
739
- }
740
- return { version: String(row.version ?? ""), config: parseConfigForVersion(String(row.data)) };
741
- };
742
-
743
- /*
744
- * Doc 8 — `--register`: name the uploaded domains in the application config, as Control Center's publish does
745
- * (engine/domain/domainRegistration.js). Answers the row the application publish must rebuild.
746
- *
747
- * Already named at these versions → nothing is written. Otherwise a NEW patch version, so the row every reader takes
748
- * is the one that names them; every other key (other apps' domains, menus, theme) comes through unchanged.
749
- */
750
- const registerUploadedDomains = async ({ registration, domains, dbIndex, saveOne }) => {
751
- const next = registerDomainVersions(registration.config, new Map(domains.map(({ name, version }) => [name, version])));
752
- if (JSON.stringify(next) === JSON.stringify(registration.config)) {
753
- logger.info(`${APP_CONFIG_NAME}@${registration.version} already names ${domains.map((d) => `${d.name}@${d.version}`).join(", ")}; not rewritten`);
754
- return { name: APP_CONFIG_NAME, version: registration.version };
755
- }
756
-
757
- const version = bumpPatch(registration.version);
758
- if (next.metadata && typeof next.metadata === "object" && next.metadata.version !== undefined) next.metadata.version = version;
759
- await saveOne({ name: APP_CONFIG_NAME, version, content: renderApplicationConfig(next) });
760
- logger.info(`registered ${domains.map((d) => d.name).join(", ")} in ${APP_CONFIG_NAME}@${version}`);
761
-
762
- /* Control Center checks a draft against the row it started from, so an open application draft is now behind. */
763
- try {
764
- const open = await execPostgres("SELECT ID FROM SYS$CC_DRAFT WHERE KIND = 'APPLICATION' AND STATUS = 'OPEN'", dbIndex);
765
- if (open?.length) {
766
- logger.warn(
767
- "Control Center has an open application draft, which started from the previous application config. " +
768
- "Publishing it will be refused as stale until it is re-opened from the current one.",
769
- );
770
- }
771
- } catch {
772
- /* No Control Center tables on this tenant: nothing to warn about. */
773
- }
774
- return { name: APP_CONFIG_NAME, version };
775
- };
776
-
777
- /*
778
- * Doc 8 (2026-10-10) — a SECONDARY application (`APPLICATION_CONFIG.<app>`), checked BEFORE anything is written.
779
- *
780
- * PostgreSQL only, as --register. And never the host's own SYS$APPS row: the host's row is keyed by its display name,
781
- * so an app named like it would publish over the host's bundle.
782
- */
783
- const assertSecondaryApplication = async ({ application, dbIndex }) => {
784
- const app = secondaryAppNameOf(application.name);
785
- if (!isPostgresIndex(dbIndex)) {
786
- throw new Error(`${application.name}: a second application on a tenant needs a PostgreSQL database; index ${dbIndex} is not one.`);
787
- }
788
- const host = (await execPostgres(buildPostgresApplicationConfigLookupSql(), dbIndex))?.[0];
789
- const hostConfig = host?.data ? parseConfigForVersion(String(host.data)) : null;
790
- const hostApp = normalizeAppName(hostConfig?.metadata?.name);
791
- if (hostApp && hostApp === app) {
792
- throw new Error(
793
- `metadata.app "${app}" is the host application's own row (${APP_CONFIG_NAME} "${hostConfig.metadata.name}"); ` +
794
- "name the second application something else.",
795
- );
796
- }
797
- };
798
-
799
691
  async function uploadConfigs({
800
692
  workingDir,
801
693
  fileNames,
@@ -808,7 +700,6 @@ async function uploadConfigs({
808
700
  forceFull = false,
809
701
  releaseCc = false,
810
702
  serverMode = "prod",
811
- register = false,
812
703
  }) {
813
704
  /* Phase B2: ask biz-a for this tenant's database coordinates before anything reads the
814
705
  dialect. A deployed CLI has no config/databases.json, so without this the registry
@@ -824,11 +715,6 @@ async function uploadConfigs({
824
715
  include,
825
716
  });
826
717
 
827
- /* Doc 8 — `--register`: these domains are ADDED to the tenant's application config, never replace it. Every
828
- refusal happens here, before anything is written. */
829
- const registration = register ? await readRegistrationBase({ application, dbIndex }) : null;
830
- if (secondaryAppNameOf(application?.name)) await assertSecondaryApplication({ application, dbIndex });
831
-
832
718
  const apiConfig = {
833
719
  url: `${server}:${apiPort}`,
834
720
  dbindex: dbIndex,
@@ -905,20 +791,12 @@ async function uploadConfigs({
905
791
  await saveOne(domain);
906
792
  }
907
793
 
908
- /* The application config the publish below rebuilds: the uploaded one, or — with --register — the tenant's
909
- newest one, now naming these domains. */
910
- let applicationTarget = application ? { name: application.name, version: application.version } : null;
911
- if (registration) {
912
- applicationTarget = await registerUploadedDomains({ registration, domains, dbIndex, saveOne });
913
- }
914
-
915
794
  if (!publish) {
916
795
  return {
917
796
  success: true,
918
797
  data: {
919
798
  applicationConfig: application,
920
799
  domains: domains.map(({ name, version }) => ({ name, version })),
921
- ...(registration ? { registeredIn: applicationTarget } : {}),
922
800
  },
923
801
  };
924
802
  }
@@ -960,7 +838,7 @@ async function uploadConfigs({
960
838
  logger.info(`published ${domain.name}@${domain.version}`);
961
839
  }
962
840
 
963
- if (applicationTarget) {
841
+ if (application) {
964
842
  /* publishApplicationConfig -> publishApp -> addApp({ workingDir: process.cwd() }), and
965
843
  addApp folds getFileList(cwd) into the bundle. cwd is the CONFIG directory right now, so
966
844
  without this the configs themselves would be packed as app scripts. Run the publish from
@@ -973,8 +851,8 @@ async function uploadConfigs({
973
851
  process.chdir(scratchDir);
974
852
  const result = await publishApplicationConfig(
975
853
  {
976
- applicationConfigName: applicationTarget.name,
977
- applicationConfigVersion: applicationTarget.version,
854
+ applicationConfigName: application.name,
855
+ applicationConfigVersion: application.version,
978
856
  dbIndex,
979
857
  url: apiConfig.url,
980
858
  subdomain: sub,
@@ -983,10 +861,10 @@ async function uploadConfigs({
983
861
  );
984
862
  if (!result || result.success !== true) {
985
863
  throw new Error(
986
- `Publish ${applicationTarget.name}@${applicationTarget.version} failed: ${result?.error ?? "no error message from server"}`,
864
+ `Publish ${application.name}@${application.version} failed: ${result?.error ?? "no error message from server"}`,
987
865
  );
988
866
  }
989
- logger.info(`published ${applicationTarget.name}@${applicationTarget.version}`);
867
+ logger.info(`published ${application.name}@${application.version}`);
990
868
  } finally {
991
869
  process.chdir(previousCwd);
992
870
  fs.rmSync(scratchDir, { force: true, recursive: true });
@@ -1003,79 +881,6 @@ async function uploadConfigs({
1003
881
  };
1004
882
  }
1005
883
 
1006
- /*
1007
- * Doc 8 (2026-10-10) — `biza unregister --domains a,b [--publish]`: the inverse of --register. Takes the domains OUT of
1008
- * the host APPLICATION_CONFIG (their entries and the menu leaves their publishes stamped; engine/domain/
1009
- * domainRegistration.js unregisterDomains) as a new patch version, and with --publish rebuilds the host from it.
1010
- *
1011
- * The domain configs themselves stay in sys$config, and their tables stay in the database — only the host stops
1012
- * naming them. How a domain moves out of the host into its own application.
1013
- *
1014
- * PostgreSQL only, as --register. A domain Control Center owns is refused: it is CC's to publish.
1015
- */
1016
- async function unregisterAppDomains({
1017
- domains,
1018
- server = "http://localhost",
1019
- apiPort = 212,
1020
- dbIndex = 2,
1021
- sub,
1022
- publish = false,
1023
- serverMode = "prod",
1024
- } = {}) {
1025
- try {
1026
- await primeRegistry({ subdomain: sub, serverLink: resolveServerLink(serverMode) });
1027
-
1028
- const names = String(domains ?? "")
1029
- .split(",")
1030
- .map((name) => name.trim())
1031
- .filter(Boolean);
1032
- if (names.length === 0) throw new Error("unregister: name the domains to take out with --domains a,b.");
1033
- if (!isPostgresIndex(dbIndex)) throw new Error(`unregister needs a PostgreSQL database; index ${dbIndex} is not one.`);
1034
-
1035
- await guardCcOwnedDomains({ exec: (sql) => execPostgres(sql, dbIndex), domainNames: names, releaseCc: false });
1036
-
1037
- const row = (await execPostgres(buildPostgresApplicationConfigLookupSql(), dbIndex))?.[0];
1038
- if (!row?.data) throw new Error("unregister: this database has no application config.");
1039
- const current = parseConfigForVersion(String(row.data));
1040
- const { config: next, removed } = unregisterDomains(current, names);
1041
- if (removed.length === 0) {
1042
- logger.info(`${APP_CONFIG_NAME}@${row.version} names none of ${names.join(", ")}; not rewritten`);
1043
- return { success: true, data: { removed } };
1044
- }
1045
-
1046
- const version = bumpPatch(String(row.version ?? ""));
1047
- if (next.metadata && typeof next.metadata === "object" && next.metadata.version !== undefined) next.metadata.version = version;
1048
- await execPostgres(buildEnsureConfigTableSql(), dbIndex);
1049
- await execPostgres(buildPostgresConfigUpsertSql(APP_CONFIG_NAME, version, renderApplicationConfig(next), false), dbIndex);
1050
- logger.info(`unregistered ${removed.join(", ")} from ${APP_CONFIG_NAME}@${version}`);
1051
-
1052
- if (publish) {
1053
- const { publishApplicationConfig } = await import("../engine/domain/applicationConfig.js");
1054
- /* From an empty scratch dir, as uploadConfigs: addApp folds the working dir's own files into the bundle. */
1055
- const scratchDir = fs.mkdtempSync(path.join(os.tmpdir(), "biza-publish-"));
1056
- const previousCwd = process.cwd();
1057
- try {
1058
- process.chdir(scratchDir);
1059
- const result = await publishApplicationConfig(
1060
- { applicationConfigName: APP_CONFIG_NAME, applicationConfigVersion: version, dbIndex, url: `${server}:${apiPort}`, subdomain: sub },
1061
- { server, port: apiPort, dbindex: dbIndex, subdomain: sub, serverMode },
1062
- );
1063
- if (!result || result.success !== true) {
1064
- throw new Error(`Publish ${APP_CONFIG_NAME}@${version} failed: ${result?.error ?? "no error message from server"}`);
1065
- }
1066
- logger.info(`published ${APP_CONFIG_NAME}@${version}`);
1067
- } finally {
1068
- process.chdir(previousCwd);
1069
- fs.rmSync(scratchDir, { force: true, recursive: true });
1070
- }
1071
- }
1072
- return { success: true, data: { removed, version, published: !!publish } };
1073
- } catch (error) {
1074
- logger.error(error?.message ?? error);
1075
- return { success: false, error: error?.message ?? String(error) };
1076
- }
1077
- }
1078
-
1079
884
  const parseApplicationConfigData = (rawConfig) => {
1080
885
  if (rawConfig == null) {
1081
886
  return null;
@@ -1222,12 +1027,6 @@ async function addApp({
1222
1027
  serverMode = "prod",
1223
1028
  /* Extra directories whose DOMAIN configs upload alongside this one's (see --include). */
1224
1029
  include = [],
1225
- /* Doc 8 — add the domains to the tenant's application config instead of replacing it (see --register). */
1226
- register = false,
1227
- /* Doc 8 (2026-10-10) — the SYS$APPS row this publish writes, from the config being published
1228
- (applicationConfig.js appMetadataOf): { appName, displayName, menuLayout }. Without it the row is named
1229
- from the host APPLICATION_CONFIG, as before. */
1230
- appMeta = null,
1231
1030
  } = {}) {
1232
1031
  const oldCwd = process.cwd();
1233
1032
 
@@ -1270,7 +1069,7 @@ async function addApp({
1270
1069
  SYS$CONFIG rows, not libs — upload the configs instead of packing a bundle. Guarded on
1271
1070
  `body == null` so the publish path (publishApplicationConfig -> publishApp -> addApp with
1272
1071
  generated template scripts) can never recurse back into config mode. */
1273
- if (body == null && (register || findApplicationConfigFile(mergedSourceFiles))) {
1072
+ if (body == null && findApplicationConfigFile(mergedSourceFiles)) {
1274
1073
  return await uploadConfigs({
1275
1074
  workingDir: process.cwd(),
1276
1075
  fileNames: mergedSourceFiles,
@@ -1283,7 +1082,6 @@ async function addApp({
1283
1082
  forceFull,
1284
1083
  releaseCc,
1285
1084
  serverMode,
1286
- register,
1287
1085
  });
1288
1086
  }
1289
1087
 
@@ -1373,14 +1171,13 @@ async function addApp({
1373
1171
  : `${server}:${apiPort}/${baseUrl}`;
1374
1172
 
1375
1173
  const headers = { "Content-Type": "text/plain" };
1376
- const { displayName: appDisplayName, menuLayout } = appMeta?.appName
1377
- ? { displayName: String(appMeta.displayName ?? "").trim(), menuLayout: appMeta.menuLayout ?? null }
1378
- : await resolveAppMetadataFromApplicationConfig({
1379
- server,
1380
- apiPort,
1381
- dbIndex,
1382
- sub,
1383
- });
1174
+ const { displayName: appDisplayName, menuLayout } =
1175
+ await resolveAppMetadataFromApplicationConfig({
1176
+ server,
1177
+ apiPort,
1178
+ dbIndex,
1179
+ sub,
1180
+ });
1384
1181
  const useApplicationConfigName = body != null;
1385
1182
  if (useApplicationConfigName && appDisplayName.length > 0) {
1386
1183
  keys.metadata["name"] = appDisplayName;
@@ -1390,11 +1187,9 @@ async function addApp({
1390
1187
  if (useApplicationConfigName && menuLayout) {
1391
1188
  keys.metadata["menuLayout"] = menuLayout;
1392
1189
  }
1393
- const uploadAppName = appMeta?.appName
1394
- ? normalizeAppName(appMeta.appName)
1395
- : normalizeAppName(
1396
- (useApplicationConfigName ? appDisplayName : "") || bundleName,
1397
- );
1190
+ const uploadAppName = normalizeAppName(
1191
+ (useApplicationConfigName ? appDisplayName : "") || bundleName,
1192
+ );
1398
1193
  /* SYS$APPS is a Biz-A table, not a FINA one, so a PostgreSQL tenant is written
1399
1194
  directly — no FINA server in the path at all. On Firebird the FINA stored
1400
1195
  procedure TOrmMethod/"setApp" still owns the write. */
@@ -1517,7 +1312,7 @@ const buildCli = () =>
1517
1312
  addCommandOptions,
1518
1313
  async (commandOptions) => {
1519
1314
  /* Config mode uploads SYS$CONFIG rows, not app scripts — skip the jest gate. */
1520
- if (isConfigModeDir(commandOptions.workingDir, commandOptions.files, { register: commandOptions.register })) {
1315
+ if (isConfigModeDir(commandOptions.workingDir, commandOptions.files)) {
1521
1316
  await addApp({
1522
1317
  workingDir: path.resolve(commandOptions.workingDir),
1523
1318
  verbose: commandOptions.verbose,
@@ -1531,7 +1326,6 @@ const buildCli = () =>
1531
1326
  releaseCc: commandOptions.releaseCc,
1532
1327
  serverMode: commandOptions.serverMode,
1533
1328
  include: commandOptions.include,
1534
- register: commandOptions.register,
1535
1329
  });
1536
1330
  return;
1537
1331
  }
@@ -1592,34 +1386,6 @@ const buildCli = () =>
1592
1386
  })();
1593
1387
  },
1594
1388
  )
1595
- .command(
1596
- "unregister",
1597
- "Take domains out of the host application config (Doc 8), e.g. when they move to their own application",
1598
- (yargs) =>
1599
- yargs
1600
- .option("domains", {
1601
- describe: "Comma-separated domain names to take out",
1602
- type: "string",
1603
- demandOption: true,
1604
- })
1605
- .option("publish", {
1606
- describe: "Republish the host application config afterwards",
1607
- type: "boolean",
1608
- default: false,
1609
- }),
1610
- async (commandOptions) => {
1611
- const result = await unregisterAppDomains({
1612
- domains: commandOptions.domains,
1613
- server: commandOptions.server,
1614
- apiPort: commandOptions.apiPort,
1615
- dbIndex: Number(commandOptions.dbIndex),
1616
- sub: commandOptions.sub,
1617
- publish: commandOptions.publish,
1618
- serverMode: commandOptions.serverMode,
1619
- });
1620
- if (!result.success) process.exitCode = 1;
1621
- },
1622
- )
1623
1389
  .command(
1624
1390
  "migrate:new",
1625
1391
  "Create a new blank migration file with an epoch timestamp",
@@ -1740,5 +1506,4 @@ export {
1740
1506
  resolveUpsertBlocks,
1741
1507
  uploadConfigs,
1742
1508
  isConfigModeDir,
1743
- unregisterAppDomains,
1744
1509
  };
package/bin/hub.js CHANGED
@@ -136,13 +136,6 @@ const argv = yargs(process.argv.slice(2))
136
136
  "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).",
137
137
  type: "string",
138
138
  })
139
- .options("finaCloudDbindex", {
140
- describe:
141
- "Doc 8: the FINA API database FINA Cloud works on (FINA's own index, not Biz-A's). Absent = FINA Cloud off. When set, browser writes to that database are refused: they go through engine/fina.",
142
- type: "number",
143
- /* ⚠️ No default, and an operator setting rather than a console one: one FINA API can serve several
144
- companies, and the index chooses whose books these are. */
145
- })
146
139
  .options("services", {
147
140
  describe:
148
141
  "Comma-separated list of engine jobs this agent handles (e.g., 'BPM,EMAIL')",
@@ -210,7 +203,6 @@ import { createExtRouter } from "../engine/ext/routes.js";
210
203
  import { createLiveExtDeps } from "../engine/ext/live.js";
211
204
  import axios from "axios";
212
205
  import { createFinaRouter } from "../engine/orm/finaEndpoint.js";
213
- import { createLiveFinaReadPolicy } from "../engine/fina/readPolicy.js";
214
206
  import { createSessionStore } from "../engine/orm/sessionStore.js";
215
207
  import { resolveUserTenancy } from "../engine/orm/tenantResolution.js";
216
208
  import { buildUserTenantSql } from "../engine/orm/finaAuth.js";
@@ -340,14 +332,6 @@ app.use(
340
332
  /* A v2 save that creates a record of an entity declaring its workflow starts it — the same
341
333
  starter the socket entrance (bin/hubEvent.js) hands the router. */
342
334
  startWorkflow: startWorkflowOnCreate,
343
- /* Doc 8 D2 — the FINA Cloud write guard, the same one the socket entrance runs (bin/hubEvent.js). */
344
- finaCloudDbIndex: argv.finaCloudDbindex,
345
- /* Doc 8 M8 — and the read policy (a mapped FINA user), as the socket entrance runs it. */
346
- finaReadPolicy: createLiveFinaReadPolicy({
347
- argv,
348
- isPostgresIndex,
349
- readSession: hubSessionStore ? (sid) => hubSessionStore.read(sid) : null,
350
- }),
351
335
  /* So a locally-served response still tells the client where to open its sockets — the
352
336
  tunnel's byte-level injection only fires for real FINA responses. */
353
337
  cliAddress: () => (argv.cliAddress ? argv.cliAddress() : {}),
package/bin/hubEvent.js CHANGED
@@ -21,9 +21,6 @@ import { tenantSqlFor } from "../engine/orm/tenantSql.js";
21
21
  import { ensureEntityConfig } from "../engine/domain/entityConfigPrimer.js";
22
22
  import { startWorkflowOnCreate } from "../engine/bpm/workflowOnCreate.js";
23
23
  import { bpmContextFromHeaders } from "../engine/bpm/bpmContext.js";
24
- import { finaContext } from "../engine/fina/context.js";
25
- import { refuseFinaCloudWrite } from "../engine/fina/guard.js";
26
- import { createLiveFinaReadPolicy } from "../engine/fina/readPolicy.js";
27
24
  import { runAsPrincipal, runTrusted, ACTIVE_TENANT_NOT_HELD } from "../engine/orm/tenantContext.js";
28
25
  import { resolveRequestScope, SESSION_NOT_USABLE } from "../engine/orm/requestScope.js";
29
26
  import { createSessionStore } from "../engine/orm/sessionStore.js";
@@ -54,7 +51,6 @@ import {
54
51
  writeCompanyLogo,
55
52
  clearCompanyLogo,
56
53
  } from "../engine/orm/companyLogo.js";
57
- import { readCompanyLocale, writeCompanyLocale } from "../engine/orm/companyLocale.js";
58
54
  import {
59
55
  execPostgres,
60
56
  execPostgresStatements,
@@ -96,23 +92,6 @@ const sessionStoreFor = (dbIndex) => {
96
92
  }
97
93
  return sessionStores.get(dbIndex);
98
94
  };
99
- /* Doc 8 M8 — the FINA read policy (S-02: reading FINA takes a mapped FINA user), one per hub, built on first use from
100
- the hub's own argv and session store: the same context the `fina/*` engine routes get. */
101
- let finaReadPolicyOfHub = null;
102
- const finaReadPolicyFor = (argv) => {
103
- if (!finaReadPolicyOfHub) {
104
- finaReadPolicyOfHub = createLiveFinaReadPolicy({
105
- argv,
106
- isPostgresIndex,
107
- readSession: (sid) => {
108
- const store = sessionStoreFor(Number(argv["dbindex"]));
109
- return store ? store.read(sid) : null;
110
- },
111
- });
112
- }
113
- return finaReadPolicyOfHub;
114
- };
115
-
116
95
  const openSessionFor = (dbIndex) => {
117
96
  const store = sessionStoreFor(dbIndex);
118
97
  return store ? (args) => store.open(args) : null;
@@ -521,21 +500,6 @@ const EngineRegistry = {
521
500
  "domain/appConfig": () => import("../engine/domain/appConfig.js"),
522
501
  "domain/applicationConfig": () =>
523
502
  import("../engine/domain/applicationConfig.js"),
524
- /* Doc 8 — FINA Cloud. ⚠️ Route files only: every export of a registered module is callable from a browser. */
525
- "fina/setup": () => import("../engine/fina/routes/setup.js"),
526
- "fina/session": () => import("../engine/fina/routes/session.js"),
527
- "fina/master": () => import("../engine/fina/routes/master.js"),
528
- /* Doc 8 M4 — the template engine's server side. */
529
- "fina/template": () => import("../engine/fina/routes/template.js"),
530
- "fina/document": () => import("../engine/fina/routes/document.js"),
531
- "fina/print": () => import("../engine/fina/routes/print.js"),
532
- "fina/recurring": () => import("../engine/fina/routes/recurring.js"),
533
- /* Doc 8 M5b — which months FINA keeps open, for the rows a list shows (L-08). */
534
- "fina/period": () => import("../engine/fina/routes/period.js"),
535
- /* Doc 8 M6 — a FINA document's attachments, kept in Biz-A (SYS$ATTACHMENT). */
536
- "fina/attachment": () => import("../engine/fina/routes/attachment.js"),
537
- /* Doc 8 M7 — JV submissions (pengajuan) with Need Approval on; the approval workflow posts them. */
538
- "fina/submission": () => import("../engine/fina/routes/submission.js"),
539
503
  };
540
504
 
541
505
  /* Doc 1 step 3 — what a Phase B turn needs: the draft store, the gap list, a draft opener that applies the
@@ -701,30 +665,6 @@ const handleCliCommand = async (data, cb, argv) => {
701
665
  }
702
666
  break;
703
667
  }
704
- /*
705
- * Agung 2026-10-10 — the company number format (id / en-US), the theme's path: read by anyone signed
706
- * in (every screen formats numbers by it), written only by a verified administrator
707
- * (companyLocale.js), audited. A refused value is an answer with `problems`, not an error.
708
- */
709
- case "companylocale": {
710
- const localePrincipal = principalForCliScript(data.headers, {
711
- mode: argv["principalMode"] || "observe",
712
- });
713
- const localeDb = tenantSqlFor(argv["dbindex"]);
714
- if (data.action === "write") {
715
- const saved = await writeCompanyLocale({
716
- exec: localeDb.exec,
717
- execStatements: localeDb.atomic,
718
- sql: localeDb,
719
- principal: localePrincipal,
720
- locale: data.locale,
721
- });
722
- cb(null, { success: saved.ok, problems: saved.problems, data: saved.locale });
723
- } else {
724
- cb(null, { success: true, data: await readCompanyLocale({ exec: localeDb.exec }) });
725
- }
726
- break;
727
- }
728
668
  /*
729
669
  * Doc 6 §8.2 — the company logo, under its OWN sys$config key.
730
670
  *
@@ -906,23 +846,12 @@ const handleCliCommand = async (data, cb, argv) => {
906
846
  * These are engine operations invoked by an administrator, not tenant data reads, so
907
847
  * they are trusted for the same reason everything behind db/db.js is.
908
848
  */
909
- /* Who is asking — the signed principal in `data.headers`, never the caller-shaped payload — for the
910
- modules that act on it: the BPM runtime, for transitions that ask for verified roles
911
- (bpm/bpmContext.js), and FINA Cloud, whose every route acts as the verified user (Doc 8 D2,
912
- engine/fina/context.js). */
849
+ /* The BPM runtime alone gets who is asking — the signed principal in `data.headers`, never the
850
+ caller-shaped payload — for transitions that ask for verified roles (bpm/bpmContext.js). */
913
851
  const engineContext =
914
852
  modulePath === "bpm/workflowRT"
915
853
  ? bpmContextFromHeaders(data.headers, { mode: argv["principalMode"] || "observe" })
916
- : modulePath.startsWith("fina/")
917
- ? await finaContext(data.headers, {
918
- argv,
919
- isPostgresIndex,
920
- readSession: (() => {
921
- const store = sessionStoreFor(Number(argv["dbindex"]));
922
- return store ? (sid) => store.read(sid) : null;
923
- })(),
924
- })
925
- : undefined;
854
+ : undefined;
926
855
  const result = await runTrusted(() =>
927
856
  engineContext
928
857
  ? engineModule[methodName](data.payload, argv, engineContext)
@@ -1059,28 +988,7 @@ export const clientListener = (socket, argv, via = "?") => {
1059
988
  : undefined,
1060
989
  });
1061
990
  if (argv["subdomain"].localeCompare(reqData.subDomain) == 0) {
1062
- /* Doc 8 D2 — a browser may not write to the FINA Cloud database directly: engine/fina does it,
1063
- checking the user's FINA rights and the date lock. Checked HERE, the one place this transport
1064
- hands a request to FINA; the HTTP entrance runs the same guard (finaEndpoint.js). */
1065
- /* Doc 8 M8 — and READING FINA takes a Biz-A user mapped to a FINA user (S-02, engine/fina/readPolicy.js). */
1066
- const finaReadPolicy = finaReadPolicyFor(argv);
1067
- const forwardToApi = async () => {
1068
- const refused =
1069
- refuseFinaCloudWrite({ path: reqData.path, body: reqBody, finaCloudDbIndex: argv["finaCloudDbindex"] }) ??
1070
- (await finaReadPolicy({ path: reqData.path, body: reqBody, headers: reqData.headers }));
1071
- if (refused) {
1072
- logger.warn(`[apiRequest] refused ${reqData.path}: ${refused.body.error}`);
1073
- return resCB(null, {
1074
- status: refused.status,
1075
- statusText: "Forbidden",
1076
- headers: { "content-type": "application/json", ...cliAddressHeaders() },
1077
- body: refused.body,
1078
- url: apiAddress + reqData.path,
1079
- });
1080
- }
1081
- return forwardToFina();
1082
- };
1083
- const forwardToFina = () =>
991
+ const forwardToApi = () =>
1084
992
  axios
1085
993
  .request({
1086
994
  timeout:
@@ -1208,8 +1116,7 @@ export const clientListener = (socket, argv, via = "?") => {
1208
1116
  status: routed.status,
1209
1117
  statusText: routed.status === 200 ? "OK" : "Error",
1210
1118
  headers: {
1211
- /* An image read answers bytes with its own type (apiRoute.js `img`). */
1212
- "content-type": routed.contentType ?? "application/json",
1119
+ "content-type": "application/json",
1213
1120
  ...cliAddressHeaders(),
1214
1121
  },
1215
1122
  body: routed.body,
@@ -48,10 +48,6 @@ export const resolveTunnelTarget = (argv, isPostgresIndex, firstChunk) => {
48
48
  };
49
49
 
50
50
  if (isLoginRequest(firstChunk)) return ownEndpoint(argv);
51
- /* Doc 8 D2 — with FINA Cloud on, everything loops back: the FINA Cloud write guard sits on the CLI's own
52
- endpoint (finaEndpoint.js), and a byte pipe straight to FINA would pass it by. */
53
- const finaCloud = Number(argv.finaCloudDbindex);
54
- if (Number.isInteger(finaCloud) && finaCloud > 0) return ownEndpoint(argv);
55
51
  if (!argv.dbindex) return fina;
56
52
 
57
53
  let servedHere = false;
@@ -4,7 +4,6 @@ import { logger } from "../../logger.js";
4
4
  import { compileDefinition } from "./rules.js";
5
5
  import { validateWorkflow } from "./validateWorkflow.js";
6
6
  import { ruleModeOf } from "./domainRules.js";
7
- import { afterCommit } from "../domain/dialect.js";
8
7
 
9
8
  async function processUpsertResult(requesterPayload, apiConfig, result) {
10
9
  if (result.success == "OK") {
@@ -27,12 +26,12 @@ async function processUpsertResult(requesterPayload, apiConfig, result) {
27
26
  apiConfig,
28
27
  );
29
28
 
30
- /* Atomic publish (2026-10-08): inside a publish transaction, clients hear of it only after the COMMIT — and
31
- never if it rolls back. Outside one, at once. */
32
- afterCommit(() => {
33
- ClientEmitter.broadcastClient("SYS$BPM_WORKFLOW_DEF_UPSERT", newRecord);
34
- ClientEmitter.notifyRequester(requesterPayload, newRecord);
35
- });
29
+ ClientEmitter.broadcastClient(
30
+ "SYS$BPM_WORKFLOW_DEF_UPSERT",
31
+ newRecord,
32
+ );
33
+
34
+ ClientEmitter.notifyRequester(requesterPayload, newRecord);
36
35
  }
37
36
  } catch (esbError) {
38
37
  logger.error(