@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.
Files changed (34) hide show
  1. package/dist/commands/compile.d.ts.map +1 -1
  2. package/dist/commands/compile.js +32 -0
  3. package/dist/commands/compile.js.map +1 -1
  4. package/dist/commands/generate.d.ts.map +1 -1
  5. package/dist/commands/generate.js +5 -0
  6. package/dist/commands/generate.js.map +1 -1
  7. package/dist/commands/package/publish.js +4 -4
  8. package/dist/commands/serve.d.ts.map +1 -1
  9. package/dist/commands/serve.js +125 -105
  10. package/dist/commands/serve.js.map +1 -1
  11. package/dist/commands/start.d.ts.map +1 -1
  12. package/dist/commands/start.js +10 -0
  13. package/dist/commands/start.js.map +1 -1
  14. package/dist/commands/validate.d.ts.map +1 -1
  15. package/dist/commands/validate.js +34 -1
  16. package/dist/commands/validate.js.map +1 -1
  17. package/dist/commands/verify.d.ts.map +1 -1
  18. package/dist/commands/verify.js +2 -4
  19. package/dist/commands/verify.js.map +1 -1
  20. package/dist/utils/lint-flow-patterns.d.ts +4 -0
  21. package/dist/utils/lint-flow-patterns.d.ts.map +1 -1
  22. package/dist/utils/lint-flow-patterns.js +138 -0
  23. package/dist/utils/lint-flow-patterns.js.map +1 -1
  24. package/dist/utils/lint-flow-patterns.test.js +130 -2
  25. package/dist/utils/lint-flow-patterns.test.js.map +1 -1
  26. package/package.json +47 -48
  27. package/dist/commands/publish.d.ts +0 -17
  28. package/dist/commands/publish.d.ts.map +0 -1
  29. package/dist/commands/publish.js +0 -135
  30. package/dist/commands/publish.js.map +0 -1
  31. package/dist/commands/rollback.d.ts +0 -13
  32. package/dist/commands/rollback.d.ts.map +0 -1
  33. package/dist/commands/rollback.js +0 -77
  34. package/dist/commands/rollback.js.map +0 -1
@@ -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
- await kernel.use(new DriverPlugin(new SqlDriver({
593
- client: 'better-sqlite3',
594
- connection: { filename: filePath },
595
- useNullAsDefault: true,
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
- trackPlugin('SqlDriver');
601
- resolvedDriverLabel = 'SqlDriver(sqlite)';
602
- resolvedDatabaseUrl = databaseUrl ?? ':memory:';
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 production-like SQL
641
- // semantics at native speed. When the native `better-sqlite3`
642
- // binary is unavailable not built, ABI mismatch after a Node
643
- // upgrade (e.g. Node 25 NODE_MODULE_VERSION mismatch), or a
644
- // blocked prebuild download fall back to the pure-JS wasm SQLite
645
- // driver, which keeps *real* SQL semantics (and on-disk
646
- // persistence) without any native build step. Only if wasm also
647
- // fails to load do we drop to the in-memory driver (mingo), which
648
- // is neither real SQL nor persistent.
649
- //
650
- // knex loads its client lazily (at first query, not at construction),
651
- // so the only reliable signal inside this registration window is to
652
- // actually open a connection: connect() runs `SELECT 1`, which forces
653
- // better-sqlite3 to load. If that throws we step down the chain here
654
- // instead of letting the failure surface much later — as a
655
- // missing-module crash on the first real query — or be swallowed by
656
- // the silent catch below, leaving the kernel with no driver at all.
657
- let sqliteDriver;
658
- let sqliteOk = false;
659
- try {
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 = String(readEnvWithDeprecation('OS_MULTI_ORG_ENABLED', 'OS_MULTI_TENANT') ?? 'false').toLowerCase() !== 'false';
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
- if (!hasAIPlugin && tierEnabled('ai')) {
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: String(readEnvWithDeprecation('OS_MULTI_ORG_ENABLED', 'OS_MULTI_TENANT') ?? 'false').toLowerCase() !== 'false',
1993
+ multiTenant: resolveMultiOrgEnabled(),
1974
1994
  seededAdmin,
1975
1995
  });
1976
1996
  // ── Publish the actually-bound port ────────────────────────────