@objectstack/cli 10.2.0 → 11.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/compile.d.ts.map +1 -1
- package/dist/commands/compile.js +32 -0
- package/dist/commands/compile.js.map +1 -1
- package/dist/commands/generate.d.ts.map +1 -1
- package/dist/commands/generate.js +5 -0
- package/dist/commands/generate.js.map +1 -1
- package/dist/commands/package/publish.js +4 -4
- package/dist/commands/serve.d.ts.map +1 -1
- package/dist/commands/serve.js +125 -105
- package/dist/commands/serve.js.map +1 -1
- package/dist/commands/start.d.ts.map +1 -1
- package/dist/commands/start.js +10 -0
- package/dist/commands/start.js.map +1 -1
- package/dist/commands/validate.d.ts.map +1 -1
- package/dist/commands/validate.js +34 -1
- package/dist/commands/validate.js.map +1 -1
- package/dist/commands/verify.d.ts.map +1 -1
- package/dist/commands/verify.js +2 -4
- package/dist/commands/verify.js.map +1 -1
- package/dist/utils/lint-flow-patterns.d.ts +4 -0
- package/dist/utils/lint-flow-patterns.d.ts.map +1 -1
- package/dist/utils/lint-flow-patterns.js +138 -0
- package/dist/utils/lint-flow-patterns.js.map +1 -1
- package/dist/utils/lint-flow-patterns.test.js +130 -2
- package/dist/utils/lint-flow-patterns.test.js.map +1 -1
- package/package.json +47 -48
- package/dist/commands/publish.d.ts +0 -17
- package/dist/commands/publish.d.ts.map +0 -1
- package/dist/commands/publish.js +0 -135
- package/dist/commands/publish.js.map +0 -1
- package/dist/commands/rollback.d.ts +0 -13
- package/dist/commands/rollback.d.ts.map +0 -1
- package/dist/commands/rollback.js +0 -77
- package/dist/commands/rollback.js.map +0 -1
package/dist/commands/serve.js
CHANGED
|
@@ -7,7 +7,7 @@ import chalk from 'chalk';
|
|
|
7
7
|
import { bundleRequire } from 'bundle-require';
|
|
8
8
|
import { BUNDLE_REQUIRE_EXTERNALS } from '../utils/config.js';
|
|
9
9
|
import { shouldBootWithLibrary } from '../utils/plugin-detection.js';
|
|
10
|
-
import { readEnvWithDeprecation } from '@objectstack/types';
|
|
10
|
+
import { readEnvWithDeprecation, resolveMultiOrgEnabled } from '@objectstack/types';
|
|
11
11
|
import { resolveObjectStackHome } from '@objectstack/runtime';
|
|
12
12
|
import { LOG_LEVELS, resolveLogLevel, readLogLevelEnv } from '../utils/log-level.js';
|
|
13
13
|
import { printError, printServerReady, } from '../utils/format.js';
|
|
@@ -412,7 +412,7 @@ export default class Serve extends Command {
|
|
|
412
412
|
// "missing artifact" error and assemble a bare kernel that
|
|
413
413
|
// can later install marketplace apps at runtime.
|
|
414
414
|
const { createDefaultHostConfig } = await import('@objectstack/runtime');
|
|
415
|
-
const bootResult = await createDefaultHostConfig({ requireArtifact: !useEmptyBoot });
|
|
415
|
+
const bootResult = await createDefaultHostConfig({ requireArtifact: !useEmptyBoot, dev: isDev });
|
|
416
416
|
config = { ...originalConfig, ...bootResult };
|
|
417
417
|
}
|
|
418
418
|
else if (resolvedMode === 'standalone') {
|
|
@@ -423,6 +423,9 @@ export default class Serve extends Command {
|
|
|
423
423
|
const standaloneInput = {
|
|
424
424
|
...(config.standalone ?? {}),
|
|
425
425
|
projectRoot: (config.standalone?.projectRoot ?? path.dirname(absolutePath)),
|
|
426
|
+
// #2229: dev enables the native-better-sqlite3 → wasm → in-memory
|
|
427
|
+
// step-down in the shared datasource factory; prod fails loudly.
|
|
428
|
+
dev: isDev,
|
|
426
429
|
};
|
|
427
430
|
const bootResult = await createStandaloneStack(standaloneInput);
|
|
428
431
|
config = { ...originalConfig, ...bootResult };
|
|
@@ -516,10 +519,39 @@ export default class Serve extends Command {
|
|
|
516
519
|
envLevel: readLogLevelEnv(),
|
|
517
520
|
}),
|
|
518
521
|
};
|
|
522
|
+
// Cluster wiring: env-driven driver selection (mirrors OS_DATABASE_URL).
|
|
523
|
+
// The remote driver self-registers on import; import it dynamically so it
|
|
524
|
+
// works in BOTH config-boot and compiled-artifact mode. Open-core ships
|
|
525
|
+
// only the in-memory driver — remote drivers (e.g. redis) come from the EE
|
|
526
|
+
// distribution; if absent we fall back to the in-memory cluster.
|
|
527
|
+
let clusterConfig;
|
|
528
|
+
const __clusterDriver = process.env.OS_CLUSTER_DRIVER?.trim();
|
|
529
|
+
if (__clusterDriver && __clusterDriver !== 'memory') {
|
|
530
|
+
// Multi-node authorization gate (open mechanism): a distribution (e.g.
|
|
531
|
+
// an EE license) may deny multi-node. On denial, downgrade to
|
|
532
|
+
// single-node rather than fail — multi-node is an add-on, never brick.
|
|
533
|
+
// Dynamic, non-literal specifier so the CLI does not statically depend
|
|
534
|
+
// on the cluster package (mirrors the remote-driver import below).
|
|
535
|
+
const __clusterPkg = '@objectstack/service-cluster';
|
|
536
|
+
const { checkMultiNodeAllowed } = (await import(__clusterPkg));
|
|
537
|
+
const __gate = checkMultiNodeAllowed();
|
|
538
|
+
if (!__gate.allowed) {
|
|
539
|
+
console.warn(`[cluster] multi-node not authorized (${__gate.reason ?? 'denied'}) — ` +
|
|
540
|
+
`downgrading to single-node (in-memory cluster). Remove OS_CLUSTER_DRIVER to silence.`);
|
|
541
|
+
}
|
|
542
|
+
else {
|
|
543
|
+
try {
|
|
544
|
+
await import(`@objectstack/service-cluster-${__clusterDriver}`);
|
|
545
|
+
}
|
|
546
|
+
catch { /* may already be registered by the loaded config */ }
|
|
547
|
+
clusterConfig = { driver: __clusterDriver, url: process.env.OS_REDIS_URL };
|
|
548
|
+
}
|
|
549
|
+
}
|
|
519
550
|
const runtime = new Runtime({
|
|
520
551
|
kernel: {
|
|
521
552
|
logger: loggerConfig
|
|
522
|
-
}
|
|
553
|
+
},
|
|
554
|
+
cluster: clusterConfig,
|
|
523
555
|
});
|
|
524
556
|
const kernel = runtime.getKernel();
|
|
525
557
|
// Load plugins from configuration
|
|
@@ -587,19 +619,25 @@ export default class Serve extends Command {
|
|
|
587
619
|
resolvedDatabaseUrl = databaseUrl ?? 'mongodb://localhost:27017/objectstack';
|
|
588
620
|
}
|
|
589
621
|
else if (driverType === 'sqlite' || driverType === 'sql') {
|
|
590
|
-
const { SqlDriver } = await import('@objectstack/driver-sql');
|
|
591
622
|
const filePath = (databaseUrl ?? ':memory:').replace(/^file:/, '').replace(/^sqlite:/, '').replace(/^sql:\/\//, '');
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
623
|
+
// Probe-by-connect with a dev-only native → wasm → in-memory
|
|
624
|
+
// step-down (#2229). better-sqlite3 loads its native addon lazily
|
|
625
|
+
// (first query), so an ABI mismatch is invisible here and would
|
|
626
|
+
// otherwise surface much later as a runtime crash. resolveSqliteDriver
|
|
627
|
+
// forces the load and degrades gracefully in dev / fails loudly in prod.
|
|
628
|
+
const { resolveSqliteDriver } = await import('@objectstack/service-datasource');
|
|
629
|
+
const resolved = await resolveSqliteDriver({
|
|
630
|
+
filename: filePath,
|
|
631
|
+
dev: isDev,
|
|
596
632
|
// #2186: in dev, self-heal a persisted DB when a metadata change
|
|
597
633
|
// relaxes a constraint (loosen-only; never destructive / never in prod).
|
|
598
634
|
autoMigrate: isDev ? 'safe' : undefined,
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
635
|
+
warn: (m) => console.warn(chalk.yellow(m)),
|
|
636
|
+
});
|
|
637
|
+
await kernel.use(new DriverPlugin(resolved.driver));
|
|
638
|
+
trackPlugin(resolved.engine === 'memory' ? 'MemoryDriver' : resolved.engine === 'sqlite-wasm' ? 'SqliteWasmDriver' : 'SqlDriver');
|
|
639
|
+
resolvedDriverLabel = resolved.label;
|
|
640
|
+
resolvedDatabaseUrl = resolved.engine === 'memory' ? '(in-memory)' : (databaseUrl ?? ':memory:');
|
|
603
641
|
}
|
|
604
642
|
else if (driverType === 'sqlite-wasm' || driverType === 'wasm-sqlite' || driverType === 'wasm') {
|
|
605
643
|
const { SqliteWasmDriver } = await import('@objectstack/driver-sqlite-wasm');
|
|
@@ -637,94 +675,26 @@ export default class Serve extends Command {
|
|
|
637
675
|
resolvedDatabaseUrl = databaseUrl;
|
|
638
676
|
}
|
|
639
677
|
else if (isDev) {
|
|
640
|
-
// Default in dev: prefer native SQLite for
|
|
641
|
-
//
|
|
642
|
-
//
|
|
643
|
-
//
|
|
644
|
-
//
|
|
645
|
-
//
|
|
646
|
-
//
|
|
647
|
-
//
|
|
648
|
-
//
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
const { SqlDriver } = await import('@objectstack/driver-sql');
|
|
661
|
-
sqliteDriver = new SqlDriver({
|
|
662
|
-
client: 'better-sqlite3',
|
|
663
|
-
connection: { filename: ':memory:' },
|
|
664
|
-
useNullAsDefault: true,
|
|
665
|
-
autoMigrate: 'safe', // #2186 dev loosen-only self-heal
|
|
666
|
-
});
|
|
667
|
-
await sqliteDriver.connect();
|
|
668
|
-
sqliteOk = true;
|
|
669
|
-
}
|
|
670
|
-
catch {
|
|
671
|
-
sqliteOk = false;
|
|
672
|
-
if (sqliteDriver?.disconnect) {
|
|
673
|
-
try {
|
|
674
|
-
await sqliteDriver.disconnect();
|
|
675
|
-
}
|
|
676
|
-
catch { /* ignore */ }
|
|
677
|
-
}
|
|
678
|
-
}
|
|
679
|
-
if (sqliteOk) {
|
|
680
|
-
await kernel.use(new DriverPlugin(sqliteDriver));
|
|
681
|
-
trackPlugin('SqlDriver');
|
|
682
|
-
resolvedDriverLabel = 'SqlDriver(sqlite)';
|
|
683
|
-
resolvedDatabaseUrl = ':memory:';
|
|
684
|
-
}
|
|
685
|
-
else {
|
|
686
|
-
// Native unavailable → try the pure-JS wasm SQLite driver before
|
|
687
|
-
// giving up on SQL fidelity entirely. Same probe-by-connect
|
|
688
|
-
// approach: actually open the connection so a load failure is
|
|
689
|
-
// caught here rather than on the first real query.
|
|
690
|
-
let wasmDriver;
|
|
691
|
-
let wasmOk = false;
|
|
692
|
-
try {
|
|
693
|
-
const { SqliteWasmDriver } = await import('@objectstack/driver-sqlite-wasm');
|
|
694
|
-
wasmDriver = new SqliteWasmDriver({
|
|
695
|
-
filename: ':memory:',
|
|
696
|
-
persist: 'on-disconnect',
|
|
697
|
-
});
|
|
698
|
-
await wasmDriver.connect();
|
|
699
|
-
wasmOk = true;
|
|
700
|
-
}
|
|
701
|
-
catch {
|
|
702
|
-
wasmOk = false;
|
|
703
|
-
if (wasmDriver?.disconnect) {
|
|
704
|
-
try {
|
|
705
|
-
await wasmDriver.disconnect();
|
|
706
|
-
}
|
|
707
|
-
catch { /* ignore */ }
|
|
708
|
-
}
|
|
709
|
-
}
|
|
710
|
-
if (wasmOk) {
|
|
711
|
-
await kernel.use(new DriverPlugin(wasmDriver));
|
|
712
|
-
trackPlugin('SqliteWasmDriver');
|
|
713
|
-
resolvedDriverLabel = 'SqliteWasmDriver';
|
|
714
|
-
resolvedDatabaseUrl = ':memory:';
|
|
715
|
-
console.warn(chalk.yellow(' ⚠ native better-sqlite3 unavailable (ABI mismatch or not built) — dev using wasm SQLite (real SQL, slower).\n' +
|
|
716
|
-
' Rebuild better-sqlite3 for native speed, or set OS_DATABASE_DRIVER=sqlite-wasm to silence this.'));
|
|
717
|
-
}
|
|
718
|
-
else {
|
|
719
|
-
const { InMemoryDriver } = await import('@objectstack/driver-memory');
|
|
720
|
-
await kernel.use(new DriverPlugin(new InMemoryDriver()));
|
|
721
|
-
trackPlugin('MemoryDriver');
|
|
722
|
-
resolvedDriverLabel = 'InMemoryDriver';
|
|
723
|
-
resolvedDatabaseUrl = '(in-memory)';
|
|
724
|
-
console.warn(chalk.yellow(' ⚠ neither native nor wasm SQLite available — dev falling back to InMemoryDriver (mingo, not real SQL).\n' +
|
|
725
|
-
' Rebuild better-sqlite3, or set OS_DATABASE_URL / OS_DATABASE_DRIVER for SQL fidelity.'));
|
|
726
|
-
}
|
|
727
|
-
}
|
|
678
|
+
// Default in dev (no DB configured): prefer native SQLite for
|
|
679
|
+
// production-like SQL at native speed, with a graceful step-down to
|
|
680
|
+
// wasm SQLite (real SQL + on-disk persistence) then in-memory when the
|
|
681
|
+
// native better-sqlite3 binary is unavailable — not built, ABI mismatch
|
|
682
|
+
// after a Node upgrade (e.g. NODE_MODULE_VERSION change), or a blocked
|
|
683
|
+
// prebuild download. Shared with the explicit-file branch and the
|
|
684
|
+
// datasource factory via resolveSqliteDriver (#2229), which probes by
|
|
685
|
+
// actually opening a connection + running SELECT 1 (better-sqlite3 loads
|
|
686
|
+
// its native addon lazily at first query, not at construction).
|
|
687
|
+
const { resolveSqliteDriver } = await import('@objectstack/service-datasource');
|
|
688
|
+
const resolved = await resolveSqliteDriver({
|
|
689
|
+
filename: ':memory:',
|
|
690
|
+
dev: true,
|
|
691
|
+
autoMigrate: 'safe', // #2186 dev loosen-only self-heal
|
|
692
|
+
warn: (m) => console.warn(chalk.yellow(m)),
|
|
693
|
+
});
|
|
694
|
+
await kernel.use(new DriverPlugin(resolved.driver));
|
|
695
|
+
trackPlugin(resolved.engine === 'memory' ? 'MemoryDriver' : resolved.engine === 'sqlite-wasm' ? 'SqliteWasmDriver' : 'SqlDriver');
|
|
696
|
+
resolvedDriverLabel = resolved.label;
|
|
697
|
+
resolvedDatabaseUrl = resolved.engine === 'memory' ? '(in-memory)' : ':memory:';
|
|
728
698
|
}
|
|
729
699
|
}
|
|
730
700
|
catch (e) {
|
|
@@ -876,9 +846,23 @@ export default class Serve extends Command {
|
|
|
876
846
|
return next();
|
|
877
847
|
const sub = host === __rootDomain ? '' : host.slice(0, -(__rootDomain.length + 1));
|
|
878
848
|
const head = sub.split('.').pop() || '';
|
|
879
|
-
if (RESERVED.has(sub) || RESERVED.has(head))
|
|
880
|
-
return next();
|
|
881
849
|
const p = c.req.path;
|
|
850
|
+
if (RESERVED.has(sub) || RESERVED.has(head)) {
|
|
851
|
+
// A browser loading the Console on a bare/reserved platform host
|
|
852
|
+
// (the apex or `www`/`app`/… — none bind a tenant env) gets the
|
|
853
|
+
// Console SPA, but its `/api/v1/auth/*` calls 404 (no env → no
|
|
854
|
+
// auth) → a dead "Auth request failed with status 404" login.
|
|
855
|
+
// When this runtime is cloud-connected (`OS_CLOUD_URL` set), send
|
|
856
|
+
// Console requests to the cloud control plane to pick/open an
|
|
857
|
+
// environment instead. A self-hosted single-env runtime (no
|
|
858
|
+
// `OS_CLOUD_URL`) keeps the prior pass-through. Non-console paths
|
|
859
|
+
// (infra, health, /api) fall through below unchanged.
|
|
860
|
+
const cloudUrl = (process.env.OS_CLOUD_URL || '').trim();
|
|
861
|
+
if (cloudUrl && (p === '/_console' || p.startsWith('/_console/'))) {
|
|
862
|
+
return c.redirect(`${cloudUrl.replace(/\/+$/, '')}/_console/`, 302);
|
|
863
|
+
}
|
|
864
|
+
return next();
|
|
865
|
+
}
|
|
882
866
|
if (p.startsWith('/_admin/') || p === '/_admin' || p.startsWith('/.well-known/')) {
|
|
883
867
|
return next();
|
|
884
868
|
}
|
|
@@ -1235,6 +1219,11 @@ export default class Serve extends Command {
|
|
|
1235
1219
|
plugins: {
|
|
1236
1220
|
admin: String(process.env.OS_AUTH_ADMIN ?? 'true').toLowerCase() !== 'false',
|
|
1237
1221
|
twoFactor: String(process.env.OS_AUTH_TWO_FACTOR ?? 'false').toLowerCase() === 'true',
|
|
1222
|
+
// ADR-0069 D1: reject breached passwords (Have I Been Pwned).
|
|
1223
|
+
// Opt-in; the auth Settings toggle (password_reject_breached) is
|
|
1224
|
+
// the primary control, OS_AUTH_PASSWORD_REJECT_BREACHED the
|
|
1225
|
+
// operator override (env wins in buildPluginList()).
|
|
1226
|
+
passwordRejectBreached: String(process.env.OS_AUTH_PASSWORD_REJECT_BREACHED ?? 'false').toLowerCase() === 'true',
|
|
1238
1227
|
},
|
|
1239
1228
|
advanced: process.env.OS_COOKIE_DOMAIN
|
|
1240
1229
|
? {
|
|
@@ -1269,7 +1258,7 @@ export default class Serve extends Command {
|
|
|
1269
1258
|
// the `org-scoping` service at start() time and conditionally
|
|
1270
1259
|
// strips the wildcard `tenant_isolation` RLS when this plugin
|
|
1271
1260
|
// is absent — so registration order matters.
|
|
1272
|
-
const multiTenant =
|
|
1261
|
+
const multiTenant = resolveMultiOrgEnabled();
|
|
1273
1262
|
if (multiTenant) {
|
|
1274
1263
|
try {
|
|
1275
1264
|
const orgScopingPkg = '@objectstack/plugin-org-scoping';
|
|
@@ -1360,6 +1349,11 @@ export default class Serve extends Command {
|
|
|
1360
1349
|
const apiConfig = config.api ?? {};
|
|
1361
1350
|
const enableProjectScoping = apiConfig.enableProjectScoping ?? false;
|
|
1362
1351
|
const projectResolution = apiConfig.projectResolution ?? 'auto';
|
|
1352
|
+
// Per-project membership (sys_environment_member 403 gate) is, by
|
|
1353
|
+
// default, ON whenever project-scoping is on. A host can opt OUT
|
|
1354
|
+
// (env-native auth IS the membership — ADR-0024 D9) by setting
|
|
1355
|
+
// `api.enforceProjectMembership: false`. Undefined → dispatcher default.
|
|
1356
|
+
const enforceProjectMembership = apiConfig.enforceProjectMembership;
|
|
1363
1357
|
// `requireAuth: true` rejects anonymous requests on `/api/v1/data/*`
|
|
1364
1358
|
// with HTTP 401 before they reach ObjectQL. Default-on when the
|
|
1365
1359
|
// stack opts in OR when the resolved tier set includes `auth`
|
|
@@ -1385,6 +1379,7 @@ export default class Serve extends Command {
|
|
|
1385
1379
|
const observability = await buildServeObservability();
|
|
1386
1380
|
await kernel.use(createDispatcherPlugin({
|
|
1387
1381
|
scoping: { enableProjectScoping, projectResolution },
|
|
1382
|
+
enforceProjectMembership,
|
|
1388
1383
|
observability,
|
|
1389
1384
|
}));
|
|
1390
1385
|
trackPlugin('Dispatcher');
|
|
@@ -1416,7 +1411,32 @@ export default class Serve extends Command {
|
|
|
1416
1411
|
return import(/* webpackIgnore: true */ pkg);
|
|
1417
1412
|
}
|
|
1418
1413
|
};
|
|
1419
|
-
|
|
1414
|
+
// [CE AI opt-in] Auto-register the headless AI service ONLY when the host
|
|
1415
|
+
// app DECLARES the AI service (or the cloud AI Studio that builds on it).
|
|
1416
|
+
// Declaration is the edition boundary: a Community-Edition app that omits
|
|
1417
|
+
// both gets no AI service, no
|
|
1418
|
+
// agents, and no `services.ai` in discovery (so the console hides its AI
|
|
1419
|
+
// surface), while MCP and every other capability are unaffected. Gating on
|
|
1420
|
+
// a *declared* dep — not mere resolvability — makes this reliable in a
|
|
1421
|
+
// workspace/monorepo, where the package stays hoist-resolvable when undeclared.
|
|
1422
|
+
const _fs = await import('node:fs');
|
|
1423
|
+
const hostDeclaresDependency = (pkg) => {
|
|
1424
|
+
try {
|
|
1425
|
+
const hostPkg = JSON.parse(_fs.readFileSync(_hostRequire.resolve('./package.json'), 'utf8'));
|
|
1426
|
+
return Boolean(hostPkg.dependencies?.[pkg] ?? hostPkg.devDependencies?.[pkg]
|
|
1427
|
+
?? hostPkg.optionalDependencies?.[pkg] ?? hostPkg.peerDependencies?.[pkg]);
|
|
1428
|
+
}
|
|
1429
|
+
catch {
|
|
1430
|
+
return false;
|
|
1431
|
+
}
|
|
1432
|
+
};
|
|
1433
|
+
// AI Studio (`@objectstack/service-ai-studio`) attaches its personas via the
|
|
1434
|
+
// `ai:ready` hook the base service fires, so declaring Studio implies the base
|
|
1435
|
+
// service — load it even when only Studio is in the deps (the base is a
|
|
1436
|
+
// transitive dep of Studio, so it stays resolvable).
|
|
1437
|
+
const wantsAiService = hostDeclaresDependency('@objectstack/service-ai')
|
|
1438
|
+
|| hostDeclaresDependency('@objectstack/service-ai-studio');
|
|
1439
|
+
if (!hasAIPlugin && tierEnabled('ai') && wantsAiService) {
|
|
1420
1440
|
try {
|
|
1421
1441
|
const aiPkg = '@objectstack/service-ai';
|
|
1422
1442
|
const { AIServicePlugin } = await importFromHost(aiPkg);
|
|
@@ -1970,7 +1990,7 @@ export default class Serve extends Command {
|
|
|
1970
1990
|
consolePath: loadedPlugins.includes('ConsoleUI') ? CONSOLE_PATH : undefined,
|
|
1971
1991
|
driverLabel: resolvedDriverLabel,
|
|
1972
1992
|
databaseUrl: redactDbUrl(resolvedDatabaseUrl),
|
|
1973
|
-
multiTenant:
|
|
1993
|
+
multiTenant: resolveMultiOrgEnabled(),
|
|
1974
1994
|
seededAdmin,
|
|
1975
1995
|
});
|
|
1976
1996
|
// ── Publish the actually-bound port ────────────────────────────
|