@objectstack/runtime 7.2.1 → 7.4.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/index.cjs CHANGED
@@ -32,13 +32,6 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
32
32
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
33
33
 
34
34
  // src/load-artifact-bundle.ts
35
- var load_artifact_bundle_exports = {};
36
- __export(load_artifact_bundle_exports, {
37
- isHttpUrl: () => isHttpUrl,
38
- loadArtifactBundle: () => loadArtifactBundle,
39
- mergeRuntimeModule: () => mergeRuntimeModule,
40
- readArtifactSource: () => readArtifactSource
41
- });
42
35
  function isHttpUrl(pathOrUrl) {
43
36
  return /^https?:\/\//i.test(pathOrUrl);
44
37
  }
@@ -294,17 +287,37 @@ var init_seed_loader = __esm({
294
287
  }
295
288
  const objectRefs = refMap.get(objectName) || [];
296
289
  const seedNow = /* @__PURE__ */ new Date();
290
+ const seedIdentity = config.identity;
291
+ const baseEvalCtx = {
292
+ now: seedNow,
293
+ user: seedIdentity?.user,
294
+ // Fall back to the per-tenant organizationId so `os.org.id` resolves
295
+ // during per-org replay even without an explicit identity.org.
296
+ org: seedIdentity?.org ?? (config.organizationId ? { id: config.organizationId } : void 0),
297
+ env: config.env
298
+ };
297
299
  for (let i = 0; i < dataset.records.length; i++) {
298
300
  const seedResult = (0, import_formula.resolveSeedRecord)(
299
301
  dataset.records[i],
300
- { now: seedNow }
302
+ baseEvalCtx
301
303
  );
302
- const record = seedResult.ok ? { ...seedResult.value } : { ...dataset.records[i] };
303
304
  if (!seedResult.ok) {
304
- this.logger.warn(
305
- `[SeedLoader] Failed to resolve dynamic values for ${objectName} record #${i}: ${seedResult.error.message}`
306
- );
305
+ errored++;
306
+ const error = {
307
+ sourceObject: objectName,
308
+ field: "(expression)",
309
+ targetObject: objectName,
310
+ targetField: "(expression)",
311
+ attemptedValue: dataset.records[i],
312
+ recordIndex: i,
313
+ message: `Cannot resolve dynamic seed values for ${objectName} record #${i}: ${seedResult.error.message}. Records using cel\`os.user.id\` / cel\`os.org.id\` require a seed identity \u2014 ensure a system/admin user exists before seeding (see SeedLoaderConfig.identity).`
314
+ };
315
+ errors.push(error);
316
+ allErrors.push(error);
317
+ this.logger.warn(`[SeedLoader] ${error.message}`);
318
+ continue;
307
319
  }
320
+ const record = { ...seedResult.value };
308
321
  if (config.organizationId && record["organization_id"] == null) {
309
322
  record["organization_id"] = config.organizationId;
310
323
  }
@@ -383,10 +396,18 @@ var init_seed_loader = __esm({
383
396
  }
384
397
  } catch (err) {
385
398
  errored++;
386
- this.logger.warn(`[SeedLoader] Failed to write ${objectName} record`, {
387
- error: err.message,
388
- recordIndex: i
389
- });
399
+ const error = {
400
+ sourceObject: objectName,
401
+ field: "(write)",
402
+ targetObject: objectName,
403
+ targetField: externalId,
404
+ attemptedValue: record[externalId] ?? null,
405
+ recordIndex: i,
406
+ message: `Failed to write ${objectName} record #${i} (${externalId}=${String(record[externalId] ?? "")}): ${err.message}`
407
+ };
408
+ errors.push(error);
409
+ allErrors.push(error);
410
+ this.logger.warn(`[SeedLoader] ${error.message}`, { recordIndex: i });
390
411
  }
391
412
  } else {
392
413
  const externalIdValue = String(record[externalId] ?? "");
@@ -726,6 +747,52 @@ var init_seed_loader = __esm({
726
747
  }
727
748
  });
728
749
 
750
+ // src/package-state-store.ts
751
+ function sanitizeEnvironmentId(environmentId) {
752
+ const raw = (environmentId ?? process.env.OS_ENVIRONMENT_ID ?? DEFAULT_ENVIRONMENT_ID).trim();
753
+ const safe = raw.replace(/[^a-zA-Z0-9._-]/g, "_");
754
+ return safe.length > 0 ? safe : DEFAULT_ENVIRONMENT_ID;
755
+ }
756
+ function stateFilePath(environmentId) {
757
+ return (0, import_node_path2.join)(resolveObjectStackHome(), "package-state", `${sanitizeEnvironmentId(environmentId)}.json`);
758
+ }
759
+ function readState(environmentId) {
760
+ const file = stateFilePath(environmentId);
761
+ if (!(0, import_node_fs.existsSync)(file)) return {};
762
+ try {
763
+ const parsed = JSON.parse((0, import_node_fs.readFileSync)(file, "utf8"));
764
+ return parsed && typeof parsed === "object" ? parsed : {};
765
+ } catch {
766
+ return {};
767
+ }
768
+ }
769
+ function writeState(environmentId, state) {
770
+ const file = stateFilePath(environmentId);
771
+ (0, import_node_fs.mkdirSync)((0, import_node_path2.dirname)(file), { recursive: true });
772
+ (0, import_node_fs.writeFileSync)(file, `${JSON.stringify(state, null, 2)}
773
+ `, "utf8");
774
+ }
775
+ function loadDisabledPackageIds(environmentId) {
776
+ const disabled = readState(environmentId).disabled;
777
+ return new Set(Array.isArray(disabled) ? disabled.filter((id) => typeof id === "string") : []);
778
+ }
779
+ function setPackageDisabled(environmentId, packageId, disabled) {
780
+ const ids = loadDisabledPackageIds(environmentId);
781
+ if (disabled) ids.add(packageId);
782
+ else ids.delete(packageId);
783
+ writeState(environmentId, { disabled: Array.from(ids).sort() });
784
+ }
785
+ var import_node_fs, import_node_path2, DEFAULT_ENVIRONMENT_ID;
786
+ var init_package_state_store = __esm({
787
+ "src/package-state-store.ts"() {
788
+ "use strict";
789
+ import_node_fs = require("fs");
790
+ import_node_path2 = require("path");
791
+ init_standalone_stack();
792
+ DEFAULT_ENVIRONMENT_ID = "default";
793
+ }
794
+ });
795
+
729
796
  // src/sandbox/quickjs-runner.ts
730
797
  function installApiMethod(vm, parent, method, objectName, ctx, caps, required, origin) {
731
798
  const fn = vm.newAsyncifiedFunction(method, async (...argHandles) => {
@@ -1284,12 +1351,14 @@ function collectBundleFunctions(bundle) {
1284
1351
  merge(bundle?.manifest?.functions);
1285
1352
  return out;
1286
1353
  }
1287
- var import_types, AppPlugin;
1354
+ var import_types, import_system, AppPlugin;
1288
1355
  var init_app_plugin = __esm({
1289
1356
  "src/app-plugin.ts"() {
1290
1357
  "use strict";
1291
1358
  import_types = require("@objectstack/types");
1292
1359
  init_seed_loader();
1360
+ init_package_state_store();
1361
+ import_system = require("@objectstack/spec/system");
1293
1362
  init_quickjs_runner();
1294
1363
  init_body_runner();
1295
1364
  AppPlugin = class {
@@ -1315,6 +1384,24 @@ var init_app_plugin = __esm({
1315
1384
  console.warn(
1316
1385
  `[AppPlugin:init] appId=${appId} keys=${Object.keys(servicePayload).join(",")} flows=${Array.isArray(servicePayload.flows) ? servicePayload.flows.length : "n/a"}`
1317
1386
  );
1387
+ try {
1388
+ const ql = ctx.getService("objectql");
1389
+ const setter = ql?.registry?.setInitialDisabledPackageIds;
1390
+ if (typeof setter === "function") {
1391
+ const disabled = loadDisabledPackageIds(this.projectContext?.environmentId);
1392
+ if (disabled.size > 0) {
1393
+ setter.call(ql.registry, disabled);
1394
+ ctx.logger.info("[AppPlugin] seeded persisted disabled packages", {
1395
+ environmentId: this.projectContext?.environmentId,
1396
+ disabled: Array.from(disabled)
1397
+ });
1398
+ }
1399
+ }
1400
+ } catch (err) {
1401
+ ctx.logger.warn("[AppPlugin] failed to seed persisted package state", {
1402
+ error: err?.message ?? String(err)
1403
+ });
1404
+ }
1318
1405
  ctx.getService("manifest").register(servicePayload);
1319
1406
  };
1320
1407
  this.start = async (ctx) => {
@@ -1346,6 +1433,27 @@ var init_app_plugin = __esm({
1346
1433
  });
1347
1434
  ql.setDatasourceMapping(this.bundle.datasourceMapping);
1348
1435
  }
1436
+ try {
1437
+ const dsDefs = this.bundle.datasources;
1438
+ const dsList = Array.isArray(dsDefs) ? dsDefs : dsDefs && typeof dsDefs === "object" ? Object.entries(dsDefs).map(([name, def]) => ({ name, ...def })) : [];
1439
+ if (dsList.length > 0) {
1440
+ const metadata = ctx.getService("metadata");
1441
+ if (typeof metadata?.registerInMemory === "function") {
1442
+ for (const ds of dsList) {
1443
+ if (!ds?.name) continue;
1444
+ metadata.registerInMemory("datasource", ds.name, { ...ds, origin: "code" });
1445
+ }
1446
+ ctx.logger.info("Registered code-defined datasources in metadata registry", {
1447
+ appId,
1448
+ count: dsList.length
1449
+ });
1450
+ }
1451
+ }
1452
+ } catch (err) {
1453
+ ctx.logger.warn("[AppPlugin] failed to register code-defined datasources", {
1454
+ error: err?.message ?? String(err)
1455
+ });
1456
+ }
1349
1457
  const stackBundle = this.bundle.default || this.bundle;
1350
1458
  const runtime = stackBundle && typeof stackBundle.onEnable === "function" ? stackBundle : this.bundle;
1351
1459
  if (runtime && typeof runtime.onEnable === "function") {
@@ -1440,49 +1548,6 @@ var init_app_plugin = __esm({
1440
1548
  appId
1441
1549
  });
1442
1550
  }
1443
- try {
1444
- const approvals = Array.isArray(this.bundle.approvals) ? this.bundle.approvals : Array.isArray((this.bundle.manifest || {}).approvals) ? this.bundle.manifest.approvals : [];
1445
- if (approvals.length > 0) {
1446
- ctx.hook("kernel:ready", async () => {
1447
- let svc;
1448
- try {
1449
- svc = ctx.getService("approvals");
1450
- } catch {
1451
- }
1452
- if (!svc || typeof svc.defineProcess !== "function") {
1453
- ctx.logger.warn("[AppPlugin] approvals service not registered \u2014 skipping declarative processes", {
1454
- appId,
1455
- processCount: approvals.length
1456
- });
1457
- return;
1458
- }
1459
- const sysCtx = { isSystem: true, roles: [], permissions: [] };
1460
- let ok = 0;
1461
- for (const proc of approvals) {
1462
- try {
1463
- await svc.defineProcess({
1464
- name: proc.name,
1465
- label: proc.label,
1466
- object: proc.object,
1467
- description: proc.description,
1468
- active: proc.active !== false,
1469
- definition: proc
1470
- }, sysCtx);
1471
- ok++;
1472
- } catch (err) {
1473
- ctx.logger.warn("[AppPlugin] Failed to register approval process", {
1474
- appId,
1475
- process: proc?.name,
1476
- error: err?.message ?? String(err)
1477
- });
1478
- }
1479
- }
1480
- ctx.logger.info("[AppPlugin] Registered approval processes", { appId, count: ok });
1481
- });
1482
- }
1483
- } catch (err) {
1484
- ctx.logger.error("[AppPlugin] Failed to schedule approval-process registration", err, { appId });
1485
- }
1486
1551
  try {
1487
1552
  const jobs = Array.isArray(this.bundle.jobs) ? this.bundle.jobs : Array.isArray((this.bundle.manifest || {}).jobs) ? this.bundle.manifest.jobs : [];
1488
1553
  if (jobs.length > 0) {
@@ -1559,6 +1624,7 @@ var init_app_plugin = __esm({
1559
1624
  ...d,
1560
1625
  object: d.object
1561
1626
  }));
1627
+ const seedIdentity = await this.ensureSeedIdentity(ql, ctx.logger);
1562
1628
  try {
1563
1629
  const kernel = ctx.kernel;
1564
1630
  const existing = (() => {
@@ -1600,7 +1666,12 @@ var init_app_plugin = __esm({
1600
1666
  config: {
1601
1667
  defaultMode: "upsert",
1602
1668
  multiPass: true,
1603
- organizationId
1669
+ organizationId,
1670
+ // Bind os.user (system identity) and os.org (this
1671
+ // tenant) so identity-derived seed values resolve
1672
+ // per-org. org.id falls back to organizationId
1673
+ // inside the loader when identity.org is absent.
1674
+ identity: seedIdentity
1604
1675
  }
1605
1676
  });
1606
1677
  const result = await seedLoader.load(request);
@@ -1628,14 +1699,34 @@ var init_app_plugin = __esm({
1628
1699
  const { SeedLoaderRequestSchema } = await import("@objectstack/spec/data");
1629
1700
  const request = SeedLoaderRequestSchema.parse({
1630
1701
  datasets: normalizedDatasets,
1631
- config: { defaultMode: "upsert", multiPass: true }
1702
+ config: { defaultMode: "upsert", multiPass: true, identity: seedIdentity }
1632
1703
  });
1633
1704
  const result = await seedLoader.load(request);
1634
- ctx.logger.info("[Seeder] Seed loading complete", {
1635
- inserted: result.summary.totalInserted,
1636
- updated: result.summary.totalUpdated,
1637
- errors: result.errors.length
1638
- });
1705
+ const { totalInserted, totalUpdated, totalSkipped, totalErrored } = result.summary;
1706
+ if (result.success) {
1707
+ ctx.logger.info("[Seeder] Seed loading complete", {
1708
+ inserted: totalInserted,
1709
+ updated: totalUpdated,
1710
+ skipped: totalSkipped,
1711
+ errored: totalErrored
1712
+ });
1713
+ } else {
1714
+ ctx.logger.warn(
1715
+ `[Seeder] Seed loading completed with ${totalErrored} dropped record(s) and ${result.errors.length} error(s) for ${appId}`,
1716
+ {
1717
+ inserted: totalInserted,
1718
+ updated: totalUpdated,
1719
+ skipped: totalSkipped,
1720
+ errored: totalErrored
1721
+ }
1722
+ );
1723
+ for (const e of result.errors.slice(0, 20)) {
1724
+ ctx.logger.warn(`[Seeder] \u2717 ${e.message}`);
1725
+ }
1726
+ if (result.errors.length > 20) {
1727
+ ctx.logger.warn(`[Seeder] \u2026and ${result.errors.length - 20} more error(s)`);
1728
+ }
1729
+ }
1639
1730
  } else {
1640
1731
  ctx.logger.debug("[Seeder] No metadata service; using basic insert fallback");
1641
1732
  for (const dataset of normalizedDatasets) {
@@ -1737,6 +1828,64 @@ var init_app_plugin = __esm({
1737
1828
  this.name = `plugin.app.${appId}`;
1738
1829
  this.version = sys?.version;
1739
1830
  }
1831
+ /**
1832
+ * Resolve the identity bound to `os.user` / `os.org` for seed CEL values.
1833
+ *
1834
+ * On a fresh boot there are zero users until the first human sign-up
1835
+ * (which the SeedLoader runs *before*), so identity-derived seeds like
1836
+ * `owner_id: cel`os.user.id`` had nothing to resolve against and were
1837
+ * dropped silently. To make seeds deterministic and self-sufficient we
1838
+ * upsert a single non-loginable **system user** (`usr_system`) and bind
1839
+ * it as `os.user`.
1840
+ *
1841
+ * Why a dedicated system user rather than the login admin:
1842
+ * - `sys_user` is better-auth-managed and schema-locked (ADR-0010); the
1843
+ * password lives in `sys_account`, so a *loginable* admin can only be
1844
+ * minted through better-auth (the CLI does this via HTTP sign-up after
1845
+ * boot). A raw insert here would bypass those invariants.
1846
+ * - `usr_system` is an owner identity only (no credential row), analogous
1847
+ * to Salesforce's "Automated Process" user. The human admin is created
1848
+ * independently and need not be the seed owner.
1849
+ *
1850
+ * Idempotent: matches by the stable id, inserts once, reuses thereafter.
1851
+ * Failures are non-fatal (logged) — records that actually need `os.user`
1852
+ * then fail loudly in the loader with an actionable message.
1853
+ */
1854
+ async ensureSeedIdentity(ql, logger) {
1855
+ const SYSTEM_USER_ID = import_system.SystemUserId.SYSTEM;
1856
+ const SYSTEM_USER_EMAIL = "system@objectstack.local";
1857
+ const identity = { user: { id: SYSTEM_USER_ID, role: "system", email: SYSTEM_USER_EMAIL } };
1858
+ const opts = { context: { isSystem: true } };
1859
+ try {
1860
+ const existing = await ql.find(
1861
+ "sys_user",
1862
+ { where: { id: SYSTEM_USER_ID }, limit: 1 },
1863
+ opts
1864
+ );
1865
+ if (Array.isArray(existing) && existing.length > 0) {
1866
+ return identity;
1867
+ }
1868
+ await ql.insert(
1869
+ "sys_user",
1870
+ {
1871
+ id: SYSTEM_USER_ID,
1872
+ name: "System",
1873
+ email: SYSTEM_USER_EMAIL,
1874
+ email_verified: true,
1875
+ role: "system"
1876
+ },
1877
+ opts
1878
+ );
1879
+ logger.info(
1880
+ `[Seeder] Provisioned deterministic system user (${SYSTEM_USER_ID}) as seed owner \u2014 binds os.user for identity-derived seed values`
1881
+ );
1882
+ } catch (err) {
1883
+ logger.warn("[Seeder] Failed to ensure system seed user; os.user-dependent seeds may be dropped", {
1884
+ error: err?.message ?? String(err)
1885
+ });
1886
+ }
1887
+ return identity;
1888
+ }
1740
1889
  /**
1741
1890
  * Emit a kernel hook so the control-plane `AppCatalogService` can
1742
1891
  * upsert / delete the corresponding `sys_app` row. Silently no-ops
@@ -1859,209 +2008,167 @@ var init_app_plugin = __esm({
1859
2008
  }
1860
2009
  });
1861
2010
 
1862
- // src/cloud/platform-sso.ts
1863
- var platform_sso_exports = {};
1864
- __export(platform_sso_exports, {
1865
- PLATFORM_SSO_PROVIDER_ID: () => PLATFORM_SSO_PROVIDER_ID,
1866
- backfillPlatformSsoClients: () => backfillPlatformSsoClients,
1867
- buildPlatformSsoRedirectUri: () => buildPlatformSsoRedirectUri,
1868
- derivePlatformSsoClientId: () => derivePlatformSsoClientId,
1869
- derivePlatformSsoClientSecret: () => derivePlatformSsoClientSecret,
1870
- hashPlatformSsoClientSecret: () => hashPlatformSsoClientSecret,
1871
- seedPlatformSsoClient: () => seedPlatformSsoClient
1872
- });
1873
- function derivePlatformSsoClientId(environmentId) {
1874
- return `project_${environmentId}`;
1875
- }
1876
- function derivePlatformSsoClientSecret(baseSecret, environmentId) {
1877
- return (0, import_node_crypto.createHmac)("sha256", baseSecret).update(`oauth-client:${environmentId}`).digest("hex");
2011
+ // src/standalone-stack.ts
2012
+ function resolveObjectStackHome() {
2013
+ const raw = process.env.OS_HOME?.trim();
2014
+ if (raw && raw.length > 0) {
2015
+ if (raw.startsWith("~")) return (0, import_node_path3.resolve)((0, import_node_os.homedir)(), raw.slice(1).replace(/^[/\\]/, ""));
2016
+ return (0, import_node_path3.resolve)(raw);
2017
+ }
2018
+ return (0, import_node_path3.resolve)((0, import_node_os.homedir)(), ".objectstack");
1878
2019
  }
1879
- function hashPlatformSsoClientSecret(plaintext) {
1880
- return (0, import_node_crypto.createHash)("sha256").update(plaintext).digest("base64").replace(/=+$/, "").replace(/\+/g, "-").replace(/\//g, "_");
2020
+ function detectDriverFromUrl(dbUrl) {
2021
+ if (/^memory:\/\//i.test(dbUrl)) return "memory";
2022
+ if (/^(postgres(ql)?|pg):\/\//i.test(dbUrl)) return "postgres";
2023
+ if (/^mongodb(\+srv)?:\/\//i.test(dbUrl)) return "mongodb";
2024
+ if (/^wasm-sqlite:\/\//i.test(dbUrl)) return "sqlite-wasm";
2025
+ if (/\.wasm\.db$/i.test(dbUrl)) return "sqlite-wasm";
2026
+ if (/^file:/i.test(dbUrl)) return "sqlite";
2027
+ if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(dbUrl)) return "sqlite";
2028
+ throw new Error(
2029
+ `[StandaloneStack] Unsupported database URL scheme: ${dbUrl}. Supported schemes: memory://, postgres://, pg://, mongodb://, mongodb+srv://, file:`
2030
+ );
1881
2031
  }
1882
- function buildPlatformSsoRedirectUri(hostname, basePath = "/api/v1/auth") {
1883
- let host;
1884
- if (hostname.startsWith("http://") || hostname.startsWith("https://")) {
1885
- host = hostname;
1886
- } else if (/(\.|^)localhost(:\d+)?$/i.test(hostname)) {
1887
- const port = (process.env.OS_RUNTIME_PORT ?? "").trim();
1888
- const hostWithPort = /:\d+$/.test(hostname) || !port ? hostname : `${hostname}:${port}`;
1889
- host = `http://${hostWithPort}`;
2032
+ async function createStandaloneStack(config) {
2033
+ const cfg = StandaloneStackConfigSchema.parse(config ?? {});
2034
+ const { ObjectQLPlugin } = await import("@objectstack/objectql");
2035
+ const { MetadataPlugin } = await import("@objectstack/metadata");
2036
+ const { DriverPlugin: DriverPlugin2 } = await Promise.resolve().then(() => (init_driver_plugin(), driver_plugin_exports));
2037
+ const { AppPlugin: AppPlugin2 } = await Promise.resolve().then(() => (init_app_plugin(), app_plugin_exports));
2038
+ const cwd = process.cwd();
2039
+ const environmentId = cfg.environmentId ?? process.env.OS_ENVIRONMENT_ID ?? "proj_local";
2040
+ const artifactPathInput = cfg.artifactPath ?? process.env.OS_ARTIFACT_PATH ?? (0, import_node_path3.resolve)(cwd, "dist/objectstack.json");
2041
+ const artifactPath = isHttpUrl(artifactPathInput) ? artifactPathInput : artifactPathInput.startsWith("/") ? artifactPathInput : (0, import_node_path3.resolve)(cwd, artifactPathInput);
2042
+ const dbUrl = cfg.databaseUrl ?? (0, import_types2.readEnvWithDeprecation)("OS_DATABASE_URL", "DATABASE_URL")?.trim() ?? process.env.TURSO_DATABASE_URL?.trim() ?? (process.env.OS_HOME?.trim() ? `file:${(0, import_node_path3.resolve)(resolveObjectStackHome(), "data/standalone.db")}` : cfg.projectRoot ? `file:${(0, import_node_path3.resolve)(cfg.projectRoot, ".objectstack/data/standalone.db")}` : `file:${(0, import_node_path3.resolve)(resolveObjectStackHome(), "data/standalone.db")}`);
2043
+ const explicitDriver = cfg.databaseDriver ?? process.env.OS_DATABASE_DRIVER?.trim();
2044
+ const dbDriver = explicitDriver ?? detectDriverFromUrl(dbUrl);
2045
+ let driverPlugin;
2046
+ if (dbDriver === "memory") {
2047
+ const { InMemoryDriver } = await import("@objectstack/driver-memory");
2048
+ driverPlugin = new DriverPlugin2(new InMemoryDriver());
2049
+ } else if (dbDriver === "postgres") {
2050
+ const { SqlDriver } = await import("@objectstack/driver-sql");
2051
+ driverPlugin = new DriverPlugin2(
2052
+ new SqlDriver({
2053
+ client: "pg",
2054
+ connection: dbUrl,
2055
+ pool: { min: 0, max: 5 }
2056
+ })
2057
+ );
2058
+ } else if (dbDriver === "mongodb") {
2059
+ let MongoDBDriver;
2060
+ try {
2061
+ ({ MongoDBDriver } = await import("@objectstack/driver-mongodb"));
2062
+ } catch (err) {
2063
+ throw new Error(
2064
+ `[StandaloneStack] mongodb URL detected but @objectstack/driver-mongodb is not installed. Add it as a dependency or pass an explicit driverPlugin. (${err?.message ?? err})`
2065
+ );
2066
+ }
2067
+ driverPlugin = new DriverPlugin2(new MongoDBDriver({ url: dbUrl }));
2068
+ } else if (dbDriver === "sqlite-wasm") {
2069
+ const { SqliteWasmDriver } = await import("@objectstack/driver-sqlite-wasm");
2070
+ const filename = dbUrl.replace(/^wasm-sqlite:(\/\/)?/i, "").replace(/^file:(\/\/)?/i, "");
2071
+ if (filename && filename !== ":memory:") {
2072
+ (0, import_node_fs2.mkdirSync)((0, import_node_path3.resolve)(filename, ".."), { recursive: true });
2073
+ }
2074
+ driverPlugin = new DriverPlugin2(
2075
+ new SqliteWasmDriver({
2076
+ filename: filename || ":memory:",
2077
+ persist: filename && filename !== ":memory:" ? "on-write" : void 0
2078
+ })
2079
+ );
1890
2080
  } else {
1891
- host = `https://${hostname}`;
2081
+ const { SqlDriver } = await import("@objectstack/driver-sql");
2082
+ const filename = dbUrl.replace(/^file:(\/\/)?/, "");
2083
+ if (!filename || /^[a-z][a-z0-9+.-]*:\/\//i.test(filename)) {
2084
+ throw new Error(
2085
+ `[StandaloneStack] sqlite driver was selected but the URL does not look like a file path: "${dbUrl}". Use file:/path/to/db.sqlite, or set OS_DATABASE_DRIVER explicitly.`
2086
+ );
2087
+ }
2088
+ (0, import_node_fs2.mkdirSync)((0, import_node_path3.resolve)(filename, ".."), { recursive: true });
2089
+ driverPlugin = new DriverPlugin2(
2090
+ new SqlDriver({
2091
+ client: "better-sqlite3",
2092
+ connection: { filename },
2093
+ useNullAsDefault: true
2094
+ })
2095
+ );
1892
2096
  }
1893
- const trimmed = host.replace(/\/+$/, "");
1894
- const path = basePath.replace(/\/+$/, "");
1895
- return `${trimmed}${path}/oauth2/callback/${PLATFORM_SSO_PROVIDER_ID}`;
1896
- }
1897
- async function seedPlatformSsoClient(opts) {
1898
- const { ql, environmentId, hostname, baseSecret, logger, throwOnError } = opts;
1899
- if (!baseSecret) {
1900
- logger?.warn?.("[platform-sso] OS_AUTH_SECRET not set \u2014 skipping client seed", { environmentId });
1901
- return;
2097
+ const artifactBundle = await loadArtifactBundle(artifactPath, {
2098
+ tag: "[StandaloneStack]",
2099
+ unwrapEnvelope: true
2100
+ });
2101
+ if (artifactBundle) {
2102
+ const flowsCount = Array.isArray(artifactBundle?.flows) ? artifactBundle.flows.length : "n/a";
2103
+ console.warn(
2104
+ `[StandaloneStack] artifact loaded: path=${artifactPath} keys=${Object.keys(artifactBundle).join(",")} flows=${flowsCount}`
2105
+ );
1902
2106
  }
1903
- const clientId = derivePlatformSsoClientId(environmentId);
1904
- const clientSecretPlaintext = derivePlatformSsoClientSecret(baseSecret, environmentId);
1905
- const clientSecretStored = hashPlatformSsoClientSecret(clientSecretPlaintext);
1906
- const desiredRedirect = hostname ? buildPlatformSsoRedirectUri(hostname) : null;
1907
- let existing = null;
1908
- try {
1909
- const rows = await ql.find("sys_oauth_application", {
1910
- where: { client_id: clientId },
1911
- limit: 1
1912
- }, { context: { isSystem: true } });
1913
- const list = Array.isArray(rows) ? rows : Array.isArray(rows?.records) ? rows.records : [];
1914
- existing = list[0] ?? null;
1915
- } catch (err) {
1916
- logger?.warn?.("[platform-sso] sys_oauth_application read failed \u2014 skipping seed", {
2107
+ const plugins = [
2108
+ driverPlugin,
2109
+ new MetadataPlugin({
2110
+ // Source-file scanner OFF declarative metadata is loaded
2111
+ // from the compiled artifact, not from yaml/json files on
2112
+ // disk. Scanning would also recursively watch the project
2113
+ // root (incl. node_modules), which is expensive and prone
2114
+ // to EMFILE.
2115
+ watch: false,
2116
+ // Artifact-file HMR ON in non-production so edits to
2117
+ // `*.view.ts` / `*.flow.ts` (which the CLI dev-mode watcher
2118
+ // recompiles into `dist/objectstack.json`) are picked up by
2119
+ // the running server WITHOUT requiring a manual restart.
2120
+ // Uses polling under the hood (see plugin.ts) to avoid
2121
+ // `fs.watch` EMFILE on macOS / busy dev hosts.
2122
+ artifactWatch: process.env.NODE_ENV !== "production",
1917
2123
  environmentId,
1918
- error: err?.message
2124
+ artifactSource: { mode: "local-file", path: artifactPath }
2125
+ }),
2126
+ new ObjectQLPlugin({ environmentId })
2127
+ ];
2128
+ if (artifactBundle) plugins.push(new AppPlugin2(artifactBundle));
2129
+ const requires = Array.isArray(artifactBundle?.requires) ? artifactBundle.requires.filter((c) => typeof c === "string") : void 0;
2130
+ const objects = Array.isArray(artifactBundle?.objects) ? artifactBundle.objects : void 0;
2131
+ const manifest = artifactBundle?.manifest;
2132
+ return {
2133
+ plugins,
2134
+ api: {
2135
+ enableProjectScoping: false,
2136
+ projectResolution: "none"
2137
+ },
2138
+ ...requires ? { requires } : {},
2139
+ ...objects ? { objects } : {},
2140
+ ...manifest ? { manifest } : {}
2141
+ };
2142
+ }
2143
+ var import_node_path3, import_node_fs2, import_node_os, import_zod, import_types2, StandaloneStackConfigSchema;
2144
+ var init_standalone_stack = __esm({
2145
+ "src/standalone-stack.ts"() {
2146
+ "use strict";
2147
+ import_node_path3 = require("path");
2148
+ import_node_fs2 = require("fs");
2149
+ import_node_os = require("os");
2150
+ import_zod = require("zod");
2151
+ import_types2 = require("@objectstack/types");
2152
+ init_load_artifact_bundle();
2153
+ StandaloneStackConfigSchema = import_zod.z.object({
2154
+ databaseUrl: import_zod.z.string().optional(),
2155
+ databaseAuthToken: import_zod.z.string().optional(),
2156
+ databaseDriver: import_zod.z.enum(["sqlite", "sqlite-wasm", "memory", "postgres", "mongodb"]).optional(),
2157
+ environmentId: import_zod.z.string().optional(),
2158
+ artifactPath: import_zod.z.string().optional(),
2159
+ /**
2160
+ * Project root directory. When set (typically by the CLI after locating
2161
+ * `objectstack.config.ts`), the default sqlite database is placed under
2162
+ * `<projectRoot>/.objectstack/data/standalone.db` instead of the global
2163
+ * `~/.objectstack/data/standalone.db`. This keeps per-project data
2164
+ * scoped to the project folder so different examples / apps don't
2165
+ * share a single database by accident.
2166
+ *
2167
+ * Explicit `databaseUrl` / `OS_DATABASE_URL` / `OS_HOME` still take
2168
+ * precedence over this default.
2169
+ */
2170
+ projectRoot: import_zod.z.string().optional()
1919
2171
  });
1920
- return;
1921
- }
1922
- const nowIso = (/* @__PURE__ */ new Date()).toISOString();
1923
- if (!existing) {
1924
- const redirects = desiredRedirect ? [desiredRedirect] : [];
1925
- try {
1926
- await ql.insert("sys_oauth_application", {
1927
- id: `oauthc_${environmentId}`,
1928
- name: `Project ${environmentId}`,
1929
- client_id: clientId,
1930
- client_secret: clientSecretStored,
1931
- type: "web",
1932
- redirect_uris: JSON.stringify(redirects),
1933
- grant_types: JSON.stringify(["authorization_code", "refresh_token"]),
1934
- response_types: JSON.stringify(["code"]),
1935
- scopes: JSON.stringify(["openid", "email", "profile"]),
1936
- token_endpoint_auth_method: "client_secret_basic",
1937
- require_pkce: false,
1938
- skip_consent: true,
1939
- disabled: false,
1940
- subject_type: "public",
1941
- created_at: nowIso,
1942
- updated_at: nowIso
1943
- }, { context: { isSystem: true } });
1944
- logger?.info?.("[platform-sso] sys_oauth_application row created", { environmentId, clientId });
1945
- } catch (err) {
1946
- logger?.warn?.("[platform-sso] sys_oauth_application create failed", {
1947
- environmentId,
1948
- error: err?.message
1949
- });
1950
- if (throwOnError) throw err;
1951
- }
1952
- return;
1953
- }
1954
- let currentRedirects = [];
1955
- try {
1956
- const raw = existing.redirect_uris;
1957
- const parsed = typeof raw === "string" ? JSON.parse(raw) : raw;
1958
- if (Array.isArray(parsed)) currentRedirects = parsed.filter((s) => typeof s === "string");
1959
- } catch {
1960
- }
1961
- const mergedRedirects = desiredRedirect && !currentRedirects.includes(desiredRedirect) ? [...currentRedirects, desiredRedirect] : currentRedirects;
1962
- const repairPatch = {
1963
- name: existing.name || `Project ${environmentId}`,
1964
- client_secret: clientSecretStored,
1965
- type: existing.type || "web",
1966
- redirect_uris: JSON.stringify(mergedRedirects),
1967
- grant_types: JSON.stringify(["authorization_code", "refresh_token"]),
1968
- response_types: JSON.stringify(["code"]),
1969
- scopes: JSON.stringify(["openid", "email", "profile"]),
1970
- token_endpoint_auth_method: "client_secret_basic",
1971
- require_pkce: false,
1972
- skip_consent: true,
1973
- disabled: false,
1974
- subject_type: "public",
1975
- updated_at: nowIso
1976
- };
1977
- try {
1978
- await ql.update(
1979
- "sys_oauth_application",
1980
- repairPatch,
1981
- { where: { id: existing.id } },
1982
- { context: { isSystem: true } }
1983
- );
1984
- logger?.info?.("[platform-sso] sys_oauth_application repaired", {
1985
- environmentId,
1986
- clientId,
1987
- redirect_uris: mergedRedirects
1988
- });
1989
- } catch (err) {
1990
- logger?.warn?.("[platform-sso] sys_oauth_application repair failed", {
1991
- environmentId,
1992
- error: err?.message
1993
- });
1994
- if (throwOnError) throw err;
1995
- }
1996
- }
1997
- async function backfillPlatformSsoClients(opts) {
1998
- const { ql, baseSecret, logger, limit = 1e3 } = opts;
1999
- if (!baseSecret) {
2000
- logger?.warn?.("[platform-sso] backfill skipped \u2014 OS_AUTH_SECRET not set");
2001
- return { scanned: 0, seeded: 0, alreadyExisted: 0, failures: [] };
2002
- }
2003
- let projects = [];
2004
- try {
2005
- const rows = await ql.find("sys_environment", {
2006
- limit,
2007
- fields: ["id", "hostname", "status"]
2008
- }, { context: { isSystem: true } });
2009
- projects = Array.isArray(rows) ? rows : Array.isArray(rows?.records) ? rows.records : [];
2010
- } catch (err) {
2011
- logger?.warn?.("[platform-sso] backfill: sys_environment read failed", {
2012
- error: err?.message
2013
- });
2014
- return { scanned: 0, seeded: 0, alreadyExisted: 0, failures: [{ environmentId: "<scan>", error: err?.message ?? String(err) }] };
2015
- }
2016
- let seeded = 0;
2017
- let alreadyExisted = 0;
2018
- const failures = [];
2019
- for (const p of projects) {
2020
- if (!p?.id) continue;
2021
- const before = await (async () => {
2022
- try {
2023
- const r = await ql.find("sys_oauth_application", {
2024
- where: { client_id: derivePlatformSsoClientId(p.id) },
2025
- limit: 1
2026
- }, { context: { isSystem: true } });
2027
- const list = Array.isArray(r) ? r : Array.isArray(r?.records) ? r.records : [];
2028
- return list[0] ?? null;
2029
- } catch {
2030
- return null;
2031
- }
2032
- })();
2033
- try {
2034
- await seedPlatformSsoClient({ ql, environmentId: p.id, hostname: p.hostname, baseSecret, logger, throwOnError: true });
2035
- if (before) alreadyExisted++;
2036
- else {
2037
- const after = await (async () => {
2038
- try {
2039
- const r = await ql.find("sys_oauth_application", {
2040
- where: { client_id: derivePlatformSsoClientId(p.id) },
2041
- limit: 1
2042
- }, { context: { isSystem: true } });
2043
- const list = Array.isArray(r) ? r : Array.isArray(r?.records) ? r.records : [];
2044
- return list[0] ?? null;
2045
- } catch (err) {
2046
- return { _readErr: err?.message };
2047
- }
2048
- })();
2049
- if (after && !after._readErr) seeded++;
2050
- else failures.push({ environmentId: p.id, error: `post-insert read returned ${after ? JSON.stringify(after) : "null"}` });
2051
- }
2052
- } catch (err) {
2053
- failures.push({ environmentId: p.id, error: err?.message ?? String(err) });
2054
- }
2055
- }
2056
- logger?.info?.("[platform-sso] backfill complete", { scanned: projects.length, seeded, alreadyExisted, failures: failures.length });
2057
- return { scanned: projects.length, seeded, alreadyExisted, failures };
2058
- }
2059
- var import_node_crypto, PLATFORM_SSO_PROVIDER_ID;
2060
- var init_platform_sso = __esm({
2061
- "src/cloud/platform-sso.ts"() {
2062
- "use strict";
2063
- import_node_crypto = require("crypto");
2064
- PLATFORM_SSO_PROVIDER_ID = "objectstack-cloud";
2065
2172
  }
2066
2173
  });
2067
2174
 
@@ -2222,6 +2329,7 @@ __export(index_exports, {
2222
2329
  DEFAULT_CLOUD_URL: () => DEFAULT_CLOUD_URL,
2223
2330
  DEFAULT_RATE_LIMITS: () => DEFAULT_RATE_LIMITS,
2224
2331
  DriverPlugin: () => DriverPlugin,
2332
+ ExternalValidationPlugin: () => ExternalValidationPlugin,
2225
2333
  FileArtifactApiClient: () => FileArtifactApiClient,
2226
2334
  HttpDispatcher: () => HttpDispatcher,
2227
2335
  HttpServer: () => HttpServer,
@@ -2250,7 +2358,7 @@ __export(index_exports, {
2250
2358
  SandboxError: () => SandboxError,
2251
2359
  SeedLoaderService: () => SeedLoaderService,
2252
2360
  UnimplementedScriptRunner: () => UnimplementedScriptRunner,
2253
- _resetEnvDeprecationWarnings: () => import_types6._resetEnvDeprecationWarnings,
2361
+ _resetEnvDeprecationWarnings: () => import_types5._resetEnvDeprecationWarnings,
2254
2362
  actionBodyRunnerFactory: () => actionBodyRunnerFactory,
2255
2363
  backfillPlatformSsoClients: () => backfillPlatformSsoClients,
2256
2364
  buildPlatformSsoRedirectUri: () => buildPlatformSsoRedirectUri,
@@ -2260,6 +2368,7 @@ __export(index_exports, {
2260
2368
  collectBundleHooks: () => collectBundleHooks,
2261
2369
  createDefaultHostConfig: () => createDefaultHostConfig,
2262
2370
  createDispatcherPlugin: () => createDispatcherPlugin,
2371
+ createExternalValidationPlugin: () => createExternalValidationPlugin,
2263
2372
  createObjectOSStack: () => createObjectOSStack,
2264
2373
  createRestApiPlugin: () => import_rest.createRestApiPlugin,
2265
2374
  createStandaloneStack: () => createStandaloneStack,
@@ -2275,7 +2384,7 @@ __export(index_exports, {
2275
2384
  mergeRuntimeModule: () => mergeRuntimeModule,
2276
2385
  parseTraceparent: () => parseTraceparent,
2277
2386
  readArtifactSource: () => readArtifactSource,
2278
- readEnvWithDeprecation: () => import_types6.readEnvWithDeprecation,
2387
+ readEnvWithDeprecation: () => import_types5.readEnvWithDeprecation,
2279
2388
  resolveCloudUrl: () => resolveCloudUrl,
2280
2389
  resolveDefaultArtifactPath: () => resolveDefaultArtifactPath,
2281
2390
  resolveErrorReporter: () => resolveErrorReporter,
@@ -2333,228 +2442,234 @@ var Runtime = class {
2333
2442
  }
2334
2443
  };
2335
2444
 
2336
- // src/standalone-stack.ts
2337
- var import_node_path2 = require("path");
2338
- var import_node_fs = require("fs");
2339
- var import_node_os = require("os");
2340
- var import_zod = require("zod");
2341
- var import_types2 = require("@objectstack/types");
2445
+ // src/index.ts
2446
+ init_standalone_stack();
2447
+
2448
+ // src/default-host.ts
2449
+ var import_node_path4 = require("path");
2450
+ var import_node_fs3 = require("fs");
2451
+ init_standalone_stack();
2342
2452
  init_load_artifact_bundle();
2343
- function resolveObjectStackHome() {
2344
- const raw = process.env.OS_HOME?.trim();
2345
- if (raw && raw.length > 0) {
2346
- if (raw.startsWith("~")) return (0, import_node_path2.resolve)((0, import_node_os.homedir)(), raw.slice(1).replace(/^[/\\]/, ""));
2347
- return (0, import_node_path2.resolve)(raw);
2453
+ function resolveDefaultArtifactPath(explicitPath, cwd = process.cwd()) {
2454
+ const candidate = explicitPath ?? process.env.OS_ARTIFACT_PATH ?? (0, import_node_path4.resolve)(cwd, "dist/objectstack.json");
2455
+ if (isHttpUrl(candidate)) return candidate;
2456
+ if (explicitPath || process.env.OS_ARTIFACT_PATH) return candidate;
2457
+ return (0, import_node_fs3.existsSync)(candidate) ? candidate : void 0;
2458
+ }
2459
+ async function createDefaultHostConfig(options = {}) {
2460
+ const { requireArtifact = true, ...standaloneOpts } = options;
2461
+ let resolvedArtifact = resolveDefaultArtifactPath(standaloneOpts.artifactPath);
2462
+ if (!resolvedArtifact && requireArtifact) {
2463
+ throw new Error(
2464
+ "[createDefaultHostConfig] No artifact source available. Set OS_ARTIFACT_PATH (file path or http(s):// URL), place the artifact at <cwd>/dist/objectstack.json, or pass `{ artifactPath: ... }` explicitly. To boot an empty kernel anyway, pass `{ requireArtifact: false }`."
2465
+ );
2466
+ }
2467
+ if (!resolvedArtifact && !requireArtifact) {
2468
+ const home = resolveObjectStackHome();
2469
+ const stubPath = (0, import_node_path4.resolve)(home, "dist/objectstack.json");
2470
+ if (!(0, import_node_fs3.existsSync)(stubPath)) {
2471
+ (0, import_node_fs3.mkdirSync)((0, import_node_path4.resolve)(stubPath, ".."), { recursive: true });
2472
+ (0, import_node_fs3.writeFileSync)(
2473
+ stubPath,
2474
+ JSON.stringify(
2475
+ {
2476
+ manifest: {
2477
+ id: "com.objectstack.empty",
2478
+ name: "empty",
2479
+ version: "0.0.0",
2480
+ type: "app",
2481
+ description: "Empty starter kernel \u2014 install apps via the Studio marketplace."
2482
+ },
2483
+ objects: [],
2484
+ views: [],
2485
+ apps: [],
2486
+ flows: [],
2487
+ requires: []
2488
+ },
2489
+ null,
2490
+ 2
2491
+ ),
2492
+ "utf8"
2493
+ );
2494
+ }
2495
+ resolvedArtifact = stubPath;
2348
2496
  }
2349
- return (0, import_node_path2.resolve)((0, import_node_os.homedir)(), ".objectstack");
2497
+ return createStandaloneStack({
2498
+ ...standaloneOpts,
2499
+ artifactPath: resolvedArtifact
2500
+ });
2350
2501
  }
2351
- var StandaloneStackConfigSchema = import_zod.z.object({
2352
- databaseUrl: import_zod.z.string().optional(),
2353
- databaseAuthToken: import_zod.z.string().optional(),
2354
- databaseDriver: import_zod.z.enum(["sqlite", "sqlite-wasm", "memory", "postgres", "mongodb"]).optional(),
2355
- environmentId: import_zod.z.string().optional(),
2356
- artifactPath: import_zod.z.string().optional(),
2502
+
2503
+ // src/index.ts
2504
+ init_driver_plugin();
2505
+ init_app_plugin();
2506
+ init_seed_loader();
2507
+
2508
+ // src/external-validation-plugin.ts
2509
+ var import_shared = require("@objectstack/spec/shared");
2510
+ var ExternalValidationPlugin = class {
2511
+ constructor() {
2512
+ this.name = "com.objectstack.external-validation";
2513
+ this.type = "standard";
2514
+ this.version = "1.0.0";
2515
+ /** Active background drift-check timers, keyed by datasource name. */
2516
+ this.driftTimers = /* @__PURE__ */ new Map();
2517
+ this.init = (_ctx) => {
2518
+ };
2519
+ this.start = (ctx) => {
2520
+ ctx.hook("kernel:ready", async () => {
2521
+ await this.runValidation(ctx);
2522
+ await this.scheduleDriftChecks(ctx);
2523
+ });
2524
+ };
2525
+ /** Tear down background drift-check timers (idempotent). */
2526
+ this.stop = () => {
2527
+ for (const timer of this.driftTimers.values()) clearInterval(timer);
2528
+ this.driftTimers.clear();
2529
+ };
2530
+ }
2531
+ /** Exposed for testing; invoked from the kernel:ready handler. */
2532
+ async runValidation(ctx) {
2533
+ const svc = safeGet(ctx, "external-datasource");
2534
+ if (!svc?.validateAll) {
2535
+ ctx.logger?.debug?.("[external-validation] service not registered; skipping");
2536
+ return;
2537
+ }
2538
+ const metadata = safeGet(ctx, "metadata");
2539
+ let report;
2540
+ try {
2541
+ report = await svc.validateAll();
2542
+ } catch (err) {
2543
+ ctx.logger?.warn?.("[external-validation] validateAll failed", { err });
2544
+ return;
2545
+ }
2546
+ const failures = report.results.filter((r) => !r.ok);
2547
+ if (failures.length === 0) {
2548
+ ctx.logger?.info?.("[external-validation] all federated objects match their remote schema", {
2549
+ objects: report.results.length
2550
+ });
2551
+ return;
2552
+ }
2553
+ for (const r of failures) {
2554
+ const mode = await resolveOnMismatch(metadata, r.datasource);
2555
+ if (mode === "ignore") continue;
2556
+ if (mode === "warn") {
2557
+ ctx.logger?.warn?.("[external-validation] external schema drift", {
2558
+ datasource: r.datasource,
2559
+ object: r.object,
2560
+ diffs: r.diffs
2561
+ });
2562
+ continue;
2563
+ }
2564
+ throw new import_shared.ExternalSchemaMismatchError(r.datasource, r.object, r.diffs);
2565
+ }
2566
+ }
2357
2567
  /**
2358
- * Project root directory. When set (typically by the CLI after locating
2359
- * `objectstack.config.ts`), the default sqlite database is placed under
2360
- * `<projectRoot>/.objectstack/data/standalone.db` instead of the global
2361
- * `~/.objectstack/data/standalone.db`. This keeps per-project data
2362
- * scoped to the project folder so different examples / apps don't
2363
- * share a single database by accident.
2568
+ * Arm a background drift checker for every federated datasource that declares
2569
+ * `external.validation.checkIntervalMs`. Each fires on its own interval and
2570
+ * emits `external.schema.drift` events it never throws or aborts the
2571
+ * process, since drift past boot is observational, not fatal.
2364
2572
  *
2365
- * Explicit `databaseUrl` / `OS_DATABASE_URL` / `OS_HOME` still take
2366
- * precedence over this default.
2573
+ * No-op when metadata can't be enumerated or no datasource opts in. Re-arming
2574
+ * (e.g. a second `kernel:ready`) first clears existing timers so intervals
2575
+ * don't accumulate.
2367
2576
  */
2368
- projectRoot: import_zod.z.string().optional()
2369
- });
2370
- function detectDriverFromUrl(dbUrl) {
2371
- if (/^memory:\/\//i.test(dbUrl)) return "memory";
2372
- if (/^(postgres(ql)?|pg):\/\//i.test(dbUrl)) return "postgres";
2373
- if (/^mongodb(\+srv)?:\/\//i.test(dbUrl)) return "mongodb";
2374
- if (/^wasm-sqlite:\/\//i.test(dbUrl)) return "sqlite-wasm";
2375
- if (/\.wasm\.db$/i.test(dbUrl)) return "sqlite-wasm";
2376
- if (/^file:/i.test(dbUrl)) return "sqlite";
2377
- if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(dbUrl)) return "sqlite";
2378
- throw new Error(
2379
- `[StandaloneStack] Unsupported database URL scheme: ${dbUrl}. Supported schemes: memory://, postgres://, pg://, mongodb://, mongodb+srv://, file:`
2380
- );
2381
- }
2382
- async function createStandaloneStack(config) {
2383
- const cfg = StandaloneStackConfigSchema.parse(config ?? {});
2384
- const { ObjectQLPlugin } = await import("@objectstack/objectql");
2385
- const { MetadataPlugin } = await import("@objectstack/metadata");
2386
- const { DriverPlugin: DriverPlugin2 } = await Promise.resolve().then(() => (init_driver_plugin(), driver_plugin_exports));
2387
- const { AppPlugin: AppPlugin2 } = await Promise.resolve().then(() => (init_app_plugin(), app_plugin_exports));
2388
- const cwd = process.cwd();
2389
- const environmentId = cfg.environmentId ?? process.env.OS_ENVIRONMENT_ID ?? "proj_local";
2390
- const artifactPathInput = cfg.artifactPath ?? process.env.OS_ARTIFACT_PATH ?? (0, import_node_path2.resolve)(cwd, "dist/objectstack.json");
2391
- const artifactPath = isHttpUrl(artifactPathInput) ? artifactPathInput : artifactPathInput.startsWith("/") ? artifactPathInput : (0, import_node_path2.resolve)(cwd, artifactPathInput);
2392
- const dbUrl = cfg.databaseUrl ?? (0, import_types2.readEnvWithDeprecation)("OS_DATABASE_URL", "DATABASE_URL")?.trim() ?? process.env.TURSO_DATABASE_URL?.trim() ?? (process.env.OS_HOME?.trim() ? `file:${(0, import_node_path2.resolve)(resolveObjectStackHome(), "data/standalone.db")}` : cfg.projectRoot ? `file:${(0, import_node_path2.resolve)(cfg.projectRoot, ".objectstack/data/standalone.db")}` : `file:${(0, import_node_path2.resolve)(resolveObjectStackHome(), "data/standalone.db")}`);
2393
- const explicitDriver = cfg.databaseDriver ?? process.env.OS_DATABASE_DRIVER?.trim();
2394
- const dbDriver = explicitDriver ?? detectDriverFromUrl(dbUrl);
2395
- let driverPlugin;
2396
- if (dbDriver === "memory") {
2397
- const { InMemoryDriver } = await import("@objectstack/driver-memory");
2398
- driverPlugin = new DriverPlugin2(new InMemoryDriver());
2399
- } else if (dbDriver === "postgres") {
2400
- const { SqlDriver } = await import("@objectstack/driver-sql");
2401
- driverPlugin = new DriverPlugin2(
2402
- new SqlDriver({
2403
- client: "pg",
2404
- connection: dbUrl,
2405
- pool: { min: 0, max: 5 }
2406
- })
2407
- );
2408
- } else if (dbDriver === "mongodb") {
2409
- let MongoDBDriver;
2577
+ async scheduleDriftChecks(ctx) {
2578
+ this.stop();
2579
+ const metadata = safeGet(ctx, "metadata");
2580
+ if (!metadata?.list) return;
2581
+ let datasources;
2410
2582
  try {
2411
- ({ MongoDBDriver } = await import("@objectstack/driver-mongodb"));
2583
+ datasources = await metadata.list("datasource");
2412
2584
  } catch (err) {
2413
- throw new Error(
2414
- `[StandaloneStack] mongodb URL detected but @objectstack/driver-mongodb is not installed. Add it as a dependency or pass an explicit driverPlugin. (${err?.message ?? err})`
2415
- );
2416
- }
2417
- driverPlugin = new DriverPlugin2(new MongoDBDriver({ url: dbUrl }));
2418
- } else if (dbDriver === "sqlite-wasm") {
2419
- const { SqliteWasmDriver } = await import("@objectstack/driver-sqlite-wasm");
2420
- const filename = dbUrl.replace(/^wasm-sqlite:(\/\/)?/i, "").replace(/^file:(\/\/)?/i, "");
2421
- if (filename && filename !== ":memory:") {
2422
- (0, import_node_fs.mkdirSync)((0, import_node_path2.resolve)(filename, ".."), { recursive: true });
2585
+ ctx.logger?.warn?.("[external-validation] could not list datasources for drift checks", { err });
2586
+ return;
2423
2587
  }
2424
- driverPlugin = new DriverPlugin2(
2425
- new SqliteWasmDriver({
2426
- filename: filename || ":memory:",
2427
- persist: filename && filename !== ":memory:" ? "on-write" : void 0
2428
- })
2429
- );
2430
- } else {
2431
- const { SqlDriver } = await import("@objectstack/driver-sql");
2432
- const filename = dbUrl.replace(/^file:(\/\/)?/, "");
2433
- if (!filename || /^[a-z][a-z0-9+.-]*:\/\//i.test(filename)) {
2434
- throw new Error(
2435
- `[StandaloneStack] sqlite driver was selected but the URL does not look like a file path: "${dbUrl}". Use file:/path/to/db.sqlite, or set OS_DATABASE_DRIVER explicitly.`
2436
- );
2588
+ for (const def of datasources) {
2589
+ const interval = def?.external?.validation?.checkIntervalMs;
2590
+ const name = def?.name;
2591
+ if (!name || typeof interval !== "number" || interval <= 0) continue;
2592
+ const timer = setInterval(() => {
2593
+ void this.runDriftCheck(ctx, name);
2594
+ }, interval);
2595
+ timer.unref?.();
2596
+ this.driftTimers.set(name, timer);
2597
+ ctx.logger?.info?.("[external-validation] armed background drift check", {
2598
+ datasource: name,
2599
+ intervalMs: interval
2600
+ });
2437
2601
  }
2438
- (0, import_node_fs.mkdirSync)((0, import_node_path2.resolve)(filename, ".."), { recursive: true });
2439
- driverPlugin = new DriverPlugin2(
2440
- new SqlDriver({
2441
- client: "better-sqlite3",
2442
- connection: { filename },
2443
- useNullAsDefault: true
2444
- })
2445
- );
2446
2602
  }
2447
- const artifactBundle = await loadArtifactBundle(artifactPath, {
2448
- tag: "[StandaloneStack]",
2449
- unwrapEnvelope: true
2450
- });
2451
- if (artifactBundle) {
2452
- const flowsCount = Array.isArray(artifactBundle?.flows) ? artifactBundle.flows.length : "n/a";
2453
- console.warn(
2454
- `[StandaloneStack] artifact loaded: path=${artifactPath} keys=${Object.keys(artifactBundle).join(",")} flows=${flowsCount}`
2455
- );
2603
+ /**
2604
+ * Re-validate one datasource's federated objects and emit an
2605
+ * `external.schema.drift` event per mismatch. Exposed for testing; invoked
2606
+ * from the interval armed by {@link scheduleDriftChecks}. Never throws.
2607
+ *
2608
+ * @returns the number of drift events emitted.
2609
+ */
2610
+ async runDriftCheck(ctx, datasource) {
2611
+ const svc = safeGet(ctx, "external-datasource");
2612
+ if (!svc?.validateAll) return 0;
2613
+ let report;
2614
+ try {
2615
+ report = await svc.validateAll();
2616
+ } catch (err) {
2617
+ ctx.logger?.warn?.("[external-validation] drift check validateAll failed", {
2618
+ datasource,
2619
+ err
2620
+ });
2621
+ return 0;
2622
+ }
2623
+ const drifted = report.results.filter((r) => !r.ok && r.datasource === datasource);
2624
+ for (const r of drifted) {
2625
+ const event = {
2626
+ datasource: r.datasource,
2627
+ object: r.object,
2628
+ diffs: r.diffs
2629
+ };
2630
+ try {
2631
+ await ctx.trigger("external.schema.drift", event);
2632
+ } catch (err) {
2633
+ ctx.logger?.warn?.("[external-validation] failed to emit drift event", {
2634
+ datasource,
2635
+ object: r.object,
2636
+ err
2637
+ });
2638
+ }
2639
+ }
2640
+ if (drifted.length > 0) {
2641
+ ctx.logger?.warn?.("[external-validation] background drift detected", {
2642
+ datasource,
2643
+ objects: drifted.map((r) => r.object)
2644
+ });
2645
+ }
2646
+ return drifted.length;
2456
2647
  }
2457
- const plugins = [
2458
- driverPlugin,
2459
- new MetadataPlugin({
2460
- // Source-file scanner OFF — declarative metadata is loaded
2461
- // from the compiled artifact, not from yaml/json files on
2462
- // disk. Scanning would also recursively watch the project
2463
- // root (incl. node_modules), which is expensive and prone
2464
- // to EMFILE.
2465
- watch: false,
2466
- // Artifact-file HMR ON in non-production so edits to
2467
- // `*.view.ts` / `*.flow.ts` (which the CLI dev-mode watcher
2468
- // recompiles into `dist/objectstack.json`) are picked up by
2469
- // the running server WITHOUT requiring a manual restart.
2470
- // Uses polling under the hood (see plugin.ts) to avoid
2471
- // `fs.watch` EMFILE on macOS / busy dev hosts.
2472
- artifactWatch: process.env.NODE_ENV !== "production",
2473
- environmentId,
2474
- artifactSource: { mode: "local-file", path: artifactPath }
2475
- }),
2476
- new ObjectQLPlugin({ environmentId })
2477
- ];
2478
- if (artifactBundle) plugins.push(new AppPlugin2(artifactBundle));
2479
- const requires = Array.isArray(artifactBundle?.requires) ? artifactBundle.requires.filter((c) => typeof c === "string") : void 0;
2480
- const objects = Array.isArray(artifactBundle?.objects) ? artifactBundle.objects : void 0;
2481
- const manifest = artifactBundle?.manifest;
2482
- return {
2483
- plugins,
2484
- api: {
2485
- enableProjectScoping: false,
2486
- projectResolution: "none"
2487
- },
2488
- ...requires ? { requires } : {},
2489
- ...objects ? { objects } : {},
2490
- ...manifest ? { manifest } : {}
2491
- };
2492
- }
2493
-
2494
- // src/default-host.ts
2495
- var import_node_path3 = require("path");
2496
- var import_node_fs2 = require("fs");
2497
- init_load_artifact_bundle();
2498
- function resolveDefaultArtifactPath(explicitPath, cwd = process.cwd()) {
2499
- const candidate = explicitPath ?? process.env.OS_ARTIFACT_PATH ?? (0, import_node_path3.resolve)(cwd, "dist/objectstack.json");
2500
- if (isHttpUrl(candidate)) return candidate;
2501
- if (explicitPath || process.env.OS_ARTIFACT_PATH) return candidate;
2502
- return (0, import_node_fs2.existsSync)(candidate) ? candidate : void 0;
2648
+ };
2649
+ function createExternalValidationPlugin() {
2650
+ return new ExternalValidationPlugin();
2503
2651
  }
2504
- async function createDefaultHostConfig(options = {}) {
2505
- const { requireArtifact = true, ...standaloneOpts } = options;
2506
- let resolvedArtifact = resolveDefaultArtifactPath(standaloneOpts.artifactPath);
2507
- if (!resolvedArtifact && requireArtifact) {
2508
- throw new Error(
2509
- "[createDefaultHostConfig] No artifact source available. Set OS_ARTIFACT_PATH (file path or http(s):// URL), place the artifact at <cwd>/dist/objectstack.json, or pass `{ artifactPath: ... }` explicitly. To boot an empty kernel anyway, pass `{ requireArtifact: false }`."
2510
- );
2652
+ async function resolveOnMismatch(metadata, datasource) {
2653
+ try {
2654
+ const ds = await metadata?.get?.("datasource", datasource);
2655
+ return ds?.external?.validation?.onMismatch ?? "fail";
2656
+ } catch {
2657
+ return "fail";
2511
2658
  }
2512
- if (!resolvedArtifact && !requireArtifact) {
2513
- const home = resolveObjectStackHome();
2514
- const stubPath = (0, import_node_path3.resolve)(home, "dist/objectstack.json");
2515
- if (!(0, import_node_fs2.existsSync)(stubPath)) {
2516
- (0, import_node_fs2.mkdirSync)((0, import_node_path3.resolve)(stubPath, ".."), { recursive: true });
2517
- (0, import_node_fs2.writeFileSync)(
2518
- stubPath,
2519
- JSON.stringify(
2520
- {
2521
- manifest: {
2522
- id: "com.objectstack.empty",
2523
- name: "empty",
2524
- version: "0.0.0",
2525
- type: "app",
2526
- description: "Empty starter kernel \u2014 install apps via the Studio marketplace."
2527
- },
2528
- objects: [],
2529
- views: [],
2530
- apps: [],
2531
- flows: [],
2532
- requires: []
2533
- },
2534
- null,
2535
- 2
2536
- ),
2537
- "utf8"
2538
- );
2539
- }
2540
- resolvedArtifact = stubPath;
2659
+ }
2660
+ function safeGet(ctx, name) {
2661
+ try {
2662
+ return ctx.getService(name);
2663
+ } catch {
2664
+ return void 0;
2541
2665
  }
2542
- return createStandaloneStack({
2543
- ...standaloneOpts,
2544
- artifactPath: resolvedArtifact
2545
- });
2546
2666
  }
2547
2667
 
2548
- // src/index.ts
2549
- init_driver_plugin();
2550
- init_app_plugin();
2551
- init_seed_loader();
2552
-
2553
2668
  // src/http-dispatcher.ts
2554
2669
  var import_core2 = require("@objectstack/core");
2555
- var import_types3 = require("@objectstack/types");
2556
- var import_system = require("@objectstack/spec/system");
2557
- var import_shared = require("@objectstack/spec/shared");
2670
+ var import_system2 = require("@objectstack/spec/system");
2671
+ var import_shared2 = require("@objectstack/spec/shared");
2672
+ init_package_state_store();
2558
2673
 
2559
2674
  // src/security/resolve-execution-context.ts
2560
2675
  function readHeader(headers, name) {
@@ -3016,7 +3131,7 @@ var _HttpDispatcher = class _HttpDispatcher {
3016
3131
  }
3017
3132
  }
3018
3133
  try {
3019
- const authService = await this.getService(import_system.CoreServiceName.enum.auth);
3134
+ const authService = await this.getService(import_system2.CoreServiceName.enum.auth);
3020
3135
  const sessionData = await authService?.api?.getSession?.({
3021
3136
  headers: context.request?.headers
3022
3137
  });
@@ -3097,7 +3212,7 @@ var _HttpDispatcher = class _HttpDispatcher {
3097
3212
  let userId;
3098
3213
  let activeOrganizationId;
3099
3214
  try {
3100
- const authService = await this.resolveService(import_system.CoreServiceName.enum.auth);
3215
+ const authService = await this.resolveService(import_system2.CoreServiceName.enum.auth);
3101
3216
  const sessionData = await authService?.api?.getSession?.({
3102
3217
  headers: context.request?.headers
3103
3218
  });
@@ -3166,21 +3281,21 @@ var _HttpDispatcher = class _HttpDispatcher {
3166
3281
  queueSvc,
3167
3282
  jobSvc
3168
3283
  ] = await Promise.all([
3169
- this.resolveService(import_system.CoreServiceName.enum.auth),
3170
- this.resolveService(import_system.CoreServiceName.enum.graphql),
3171
- this.resolveService(import_system.CoreServiceName.enum.search),
3172
- this.resolveService(import_system.CoreServiceName.enum.realtime),
3173
- this.resolveService(import_system.CoreServiceName.enum["file-storage"]),
3174
- this.resolveService(import_system.CoreServiceName.enum.analytics),
3175
- this.resolveService(import_system.CoreServiceName.enum.workflow),
3176
- this.resolveService(import_system.CoreServiceName.enum.ai),
3177
- this.resolveService(import_system.CoreServiceName.enum.notification),
3178
- this.resolveService(import_system.CoreServiceName.enum.i18n),
3179
- this.resolveService(import_system.CoreServiceName.enum.ui),
3180
- this.resolveService(import_system.CoreServiceName.enum.automation),
3181
- this.resolveService(import_system.CoreServiceName.enum.cache),
3182
- this.resolveService(import_system.CoreServiceName.enum.queue),
3183
- this.resolveService(import_system.CoreServiceName.enum.job)
3284
+ this.resolveService(import_system2.CoreServiceName.enum.auth),
3285
+ this.resolveService(import_system2.CoreServiceName.enum.graphql),
3286
+ this.resolveService(import_system2.CoreServiceName.enum.search),
3287
+ this.resolveService(import_system2.CoreServiceName.enum.realtime),
3288
+ this.resolveService(import_system2.CoreServiceName.enum["file-storage"]),
3289
+ this.resolveService(import_system2.CoreServiceName.enum.analytics),
3290
+ this.resolveService(import_system2.CoreServiceName.enum.workflow),
3291
+ this.resolveService(import_system2.CoreServiceName.enum.ai),
3292
+ this.resolveService(import_system2.CoreServiceName.enum.notification),
3293
+ this.resolveService(import_system2.CoreServiceName.enum.i18n),
3294
+ this.resolveService(import_system2.CoreServiceName.enum.ui),
3295
+ this.resolveService(import_system2.CoreServiceName.enum.automation),
3296
+ this.resolveService(import_system2.CoreServiceName.enum.cache),
3297
+ this.resolveService(import_system2.CoreServiceName.enum.queue),
3298
+ this.resolveService(import_system2.CoreServiceName.enum.job)
3184
3299
  ]);
3185
3300
  const hasAuth = !!authSvc;
3186
3301
  const hasGraphQL = !!(graphqlSvc || this.kernel.graphql);
@@ -3297,7 +3412,7 @@ var _HttpDispatcher = class _HttpDispatcher {
3297
3412
  * path: sub-path after /auth/
3298
3413
  */
3299
3414
  async handleAuth(path, method, body, context) {
3300
- const authService = await this.getService(import_system.CoreServiceName.enum.auth);
3415
+ const authService = await this.getService(import_system2.CoreServiceName.enum.auth);
3301
3416
  if (authService && typeof authService.handler === "function") {
3302
3417
  const response = await authService.handler(context.request, context.response);
3303
3418
  return { handled: true, result: response };
@@ -3381,10 +3496,21 @@ var _HttpDispatcher = class _HttpDispatcher {
3381
3496
  }
3382
3497
  return { handled: true, response: this.success({ types: ["object", "app", "plugin"] }) };
3383
3498
  }
3499
+ if (parts.length === 4 && (parts[0] === "objects" || parts[0] === "object") && parts[2] === "state" && (!method || method === "GET")) {
3500
+ const name = parts[1];
3501
+ const field = parts[3];
3502
+ const from = query?.from !== void 0 ? String(query.from) : void 0;
3503
+ const qlService = await this.getObjectQLService();
3504
+ const schema = qlService?.registry?.getObject(name);
3505
+ if (!schema) return { handled: true, response: this.error("Object not found", 404) };
3506
+ const { legalNextStates } = await import("@objectstack/objectql");
3507
+ const next = from === void 0 ? null : legalNextStates(schema, field, from);
3508
+ return { handled: true, response: this.success({ object: name, field, from: from ?? null, next }) };
3509
+ }
3384
3510
  if (parts.length >= 3 && parts[parts.length - 1] === "published" && (!method || method === "GET")) {
3385
3511
  const type = parts[0];
3386
3512
  const name = parts.slice(1, -1).join("/");
3387
- const metadataService = await this.getService(import_system.CoreServiceName.enum.metadata);
3513
+ const metadataService = await this.getService(import_system2.CoreServiceName.enum.metadata);
3388
3514
  if (metadataService && typeof metadataService.getPublished === "function") {
3389
3515
  const data = await metadataService.getPublished(type, name);
3390
3516
  if (data === void 0) return { handled: true, response: this.error("Not found", 404) };
@@ -3409,7 +3535,7 @@ var _HttpDispatcher = class _HttpDispatcher {
3409
3535
  if (protocol && typeof protocol.saveMetaItem === "function") {
3410
3536
  try {
3411
3537
  const organizationId = await this.resolveActiveOrganizationId(_context);
3412
- const result = await protocol.saveMetaItem({ type, name, item: body, organizationId });
3538
+ const result = await protocol.saveMetaItem({ type, name, item: body, organizationId, ...packageId ? { packageId } : {} });
3413
3539
  return { handled: true, response: this.success(result) };
3414
3540
  } catch (e) {
3415
3541
  return { handled: true, response: this.error(e.message, 400) };
@@ -3458,7 +3584,7 @@ var _HttpDispatcher = class _HttpDispatcher {
3458
3584
  }
3459
3585
  return { handled: true, response: this.error("Not found", 404) };
3460
3586
  }
3461
- const singularType = (0, import_shared.pluralToSingular)(type);
3587
+ const singularType = (0, import_shared2.pluralToSingular)(type);
3462
3588
  const protocol = await this.resolveService("protocol");
3463
3589
  if (protocol && typeof protocol.getMetaItem === "function") {
3464
3590
  try {
@@ -3495,7 +3621,7 @@ var _HttpDispatcher = class _HttpDispatcher {
3495
3621
  } catch {
3496
3622
  }
3497
3623
  }
3498
- const metadataService = await this.getService(import_system.CoreServiceName.enum.metadata);
3624
+ const metadataService = await this.getService(import_system2.CoreServiceName.enum.metadata);
3499
3625
  if (metadataService && typeof metadataService.list === "function") {
3500
3626
  try {
3501
3627
  let items = await metadataService.list(typeOrName);
@@ -3629,7 +3755,7 @@ var _HttpDispatcher = class _HttpDispatcher {
3629
3755
  * path: sub-path after /analytics/
3630
3756
  */
3631
3757
  async handleAnalytics(path, method, body, _context) {
3632
- const analyticsService = await this.getService(import_system.CoreServiceName.enum.analytics);
3758
+ const analyticsService = await this.getService(import_system2.CoreServiceName.enum.analytics);
3633
3759
  if (!analyticsService) return { handled: false };
3634
3760
  const m = method.toUpperCase();
3635
3761
  const subPath = path.replace(/^\/+/, "");
@@ -3659,7 +3785,7 @@ var _HttpDispatcher = class _HttpDispatcher {
3659
3785
  * GET /labels/:object?locale=xx → getFieldLabels (locale from query)
3660
3786
  */
3661
3787
  async handleI18n(path, method, query, _context) {
3662
- const i18nService = await this.getService(import_system.CoreServiceName.enum.i18n);
3788
+ const i18nService = await this.getService(import_system2.CoreServiceName.enum.i18n);
3663
3789
  if (!i18nService) return { handled: true, response: this.error("i18n service not available", 501) };
3664
3790
  const m = method.toUpperCase();
3665
3791
  const parts = path.replace(/^\/+/, "").split("/").filter(Boolean);
@@ -3749,17 +3875,27 @@ var _HttpDispatcher = class _HttpDispatcher {
3749
3875
  const id = decodeURIComponent(parts[0]);
3750
3876
  const pkg = registry.enablePackage(id);
3751
3877
  if (!pkg) return { handled: true, response: this.error(`Package '${id}' not found`, 404) };
3878
+ try {
3879
+ setPackageDisabled(_context?.environmentId, id, false);
3880
+ } catch (err) {
3881
+ console.warn("[handlePackages] failed to persist enable state", { id, error: err?.message });
3882
+ }
3752
3883
  return { handled: true, response: this.success(pkg) };
3753
3884
  }
3754
3885
  if (parts.length === 2 && parts[1] === "disable" && m === "PATCH") {
3755
3886
  const id = decodeURIComponent(parts[0]);
3756
3887
  const pkg = registry.disablePackage(id);
3757
3888
  if (!pkg) return { handled: true, response: this.error(`Package '${id}' not found`, 404) };
3889
+ try {
3890
+ setPackageDisabled(_context?.environmentId, id, true);
3891
+ } catch (err) {
3892
+ console.warn("[handlePackages] failed to persist disable state", { id, error: err?.message });
3893
+ }
3758
3894
  return { handled: true, response: this.success(pkg) };
3759
3895
  }
3760
3896
  if (parts.length === 2 && parts[1] === "publish" && m === "POST") {
3761
3897
  const id = decodeURIComponent(parts[0]);
3762
- const metadataService = await this.getService(import_system.CoreServiceName.enum.metadata);
3898
+ const metadataService = await this.getService(import_system2.CoreServiceName.enum.metadata);
3763
3899
  if (metadataService && typeof metadataService.publishPackage === "function") {
3764
3900
  const result = await metadataService.publishPackage(id, body || {});
3765
3901
  return { handled: true, response: this.success(result) };
@@ -3768,13 +3904,21 @@ var _HttpDispatcher = class _HttpDispatcher {
3768
3904
  }
3769
3905
  if (parts.length === 2 && parts[1] === "revert" && m === "POST") {
3770
3906
  const id = decodeURIComponent(parts[0]);
3771
- const metadataService = await this.getService(import_system.CoreServiceName.enum.metadata);
3907
+ const metadataService = await this.getService(import_system2.CoreServiceName.enum.metadata);
3772
3908
  if (metadataService && typeof metadataService.revertPackage === "function") {
3773
3909
  await metadataService.revertPackage(id);
3774
3910
  return { handled: true, response: this.success({ success: true }) };
3775
3911
  }
3776
3912
  return { handled: true, response: this.error("Metadata service not available", 503) };
3777
3913
  }
3914
+ if (parts.length === 2 && parts[1] === "export" && m === "GET") {
3915
+ const id = decodeURIComponent(parts[0]);
3916
+ const manifest = await this.assemblePackageManifest(id, registry, _context);
3917
+ if (!manifest) {
3918
+ return { handled: true, response: this.error(`Package '${id}' not found`, 404) };
3919
+ }
3920
+ return { handled: true, response: this.success(manifest) };
3921
+ }
3778
3922
  if (parts.length === 1 && m === "GET") {
3779
3923
  const id = decodeURIComponent(parts[0]);
3780
3924
  const pkg = registry.getPackage(id);
@@ -3792,6 +3936,83 @@ var _HttpDispatcher = class _HttpDispatcher {
3792
3936
  }
3793
3937
  return { handled: false };
3794
3938
  }
3939
+ /**
3940
+ * Assemble a portable, offline-installable package manifest from the
3941
+ * `sys_metadata` overlay rows bound to `packageId`.
3942
+ *
3943
+ * The resulting shape mirrors what `marketplace-install-local` →
3944
+ * `manifestService.register()` → `engine.registerApp()` consumes:
3945
+ * `{ id, name, version, objects:[…], views:[…], flows:[…], … }`
3946
+ * where each category key is the PLURAL manifest name and its value is
3947
+ * an array of clean metadata bodies (provenance decorations stripped).
3948
+ *
3949
+ * Only the metadata categories that `registerApp` can actually consume
3950
+ * are exported. `datasources` and `emailTemplates` are intentionally
3951
+ * excluded (not registered by the import path). `tools` / `skills` ARE
3952
+ * round-tripped: they are registered by `registerApp` on import and
3953
+ * surfaced by `getMetaItems('tool' | 'skill')` on export.
3954
+ *
3955
+ * @returns the manifest object, or `null` if the package id is unknown
3956
+ * AND has no overlay-authored metadata.
3957
+ */
3958
+ async assemblePackageManifest(packageId, registry, context) {
3959
+ const protocol = await this.resolveService("protocol");
3960
+ if (!protocol || typeof protocol.getMetaItems !== "function") return null;
3961
+ const organizationId = await this.resolveActiveOrganizationId(context);
3962
+ const PROVENANCE_KEYS = /* @__PURE__ */ new Set([
3963
+ "_packageId",
3964
+ "_packageVersionId",
3965
+ "_provenance",
3966
+ "_state",
3967
+ "_version",
3968
+ "_organizationId",
3969
+ "_source",
3970
+ "_id",
3971
+ "_rowId"
3972
+ ]);
3973
+ const clean = (item) => {
3974
+ if (!item || typeof item !== "object") return item;
3975
+ const out = {};
3976
+ for (const [k, v] of Object.entries(item)) {
3977
+ if (k.startsWith("_") || PROVENANCE_KEYS.has(k)) continue;
3978
+ out[k] = v;
3979
+ }
3980
+ return out;
3981
+ };
3982
+ const exportPluralKeys = Object.keys(import_shared2.PLURAL_TO_SINGULAR).filter(
3983
+ (k) => k !== "datasources" && k !== "emailTemplates"
3984
+ );
3985
+ const manifest = {};
3986
+ let total = 0;
3987
+ for (const plural of exportPluralKeys) {
3988
+ const singular = import_shared2.PLURAL_TO_SINGULAR[plural];
3989
+ let items = [];
3990
+ try {
3991
+ const res = await protocol.getMetaItems({ type: singular, packageId, organizationId });
3992
+ items = Array.isArray(res?.items) ? res.items : [];
3993
+ } catch {
3994
+ continue;
3995
+ }
3996
+ if (items.length === 0) continue;
3997
+ manifest[plural] = items.map(clean);
3998
+ total += items.length;
3999
+ }
4000
+ const pkg = (() => {
4001
+ try {
4002
+ return registry?.getPackage?.(packageId);
4003
+ } catch {
4004
+ return void 0;
4005
+ }
4006
+ })();
4007
+ if (total === 0 && !pkg) return null;
4008
+ manifest.id = packageId;
4009
+ manifest.name = pkg?.manifest?.name ?? pkg?.name ?? packageId;
4010
+ manifest.version = pkg?.manifest?.version ?? pkg?.version ?? "1.0.0";
4011
+ if (pkg?.manifest?.label ?? pkg?.label) {
4012
+ manifest.label = pkg?.manifest?.label ?? pkg?.label;
4013
+ }
4014
+ return manifest;
4015
+ }
3795
4016
  /**
3796
4017
  * Cloud / Environment Control-Plane routes.
3797
4018
  *
@@ -3814,1348 +4035,57 @@ var _HttpDispatcher = class _HttpDispatcher {
3814
4035
  * - DELETE /cloud/environments/:id/packages/:pkgId → uninstall (scope=platform forbidden)
3815
4036
  * - POST /cloud/environments/:id/packages/:pkgId/upgrade → upgrade to newer version
3816
4037
  *
3817
- * Driver binding
3818
- * --------------
3819
- * Environments are not tied to any specific driver. At provisioning time the
3820
- * caller passes `driver` (a short name such as `memory`, `turso`, or any
3821
- * future `sql` / `postgres` driver). The dispatcher validates the name
3822
- * against the kernel's registered driver services (`driver.<name>`) and
3823
- * derives an appropriate placeholder `database_url` for the chosen driver.
3824
- * If `driver` is omitted, the dispatcher auto-selects the first available
3825
- * in preference order: turso → memory → any other registered driver.
3826
- *
3827
- * Backed by ObjectQL sys_environment / sys_environment_credential /
3828
- * sys_environment_member tables (registered by
3829
- * `@objectstack/service-tenant`'s `createTenantPlugin`).
3830
- * Physical database addressing (database_url, database_driver, etc.)
3831
- * is stored directly on the sys_environment row.
3832
- */
3833
- /**
3834
- * Resolve the calling user id from the request session, if any.
3835
- * Returns `undefined` for anonymous calls or when auth is not wired up.
3836
- */
3837
- async resolveActiveOrganizationId(context) {
3838
- try {
3839
- const authService = await this.resolveService(import_system.CoreServiceName.enum.auth);
3840
- const rawHeaders = context.request?.headers;
3841
- let headers = rawHeaders;
3842
- if (rawHeaders && typeof rawHeaders === "object" && typeof rawHeaders.get !== "function") {
3843
- try {
3844
- const h = new Headers();
3845
- for (const [k, v] of Object.entries(rawHeaders)) {
3846
- if (v == null) continue;
3847
- h.set(k, Array.isArray(v) ? v.join(", ") : String(v));
3848
- }
3849
- headers = h;
3850
- } catch {
3851
- headers = rawHeaders;
3852
- }
3853
- }
3854
- const apiObj = authService?.auth?.api ?? authService?.api;
3855
- const sessionData = await apiObj?.getSession?.call(apiObj, { headers });
3856
- const oid = sessionData?.session?.activeOrganizationId;
3857
- return typeof oid === "string" && oid.length > 0 ? oid : void 0;
3858
- } catch {
3859
- return void 0;
3860
- }
3861
- }
3862
- async resolveCallerUserId(context) {
3863
- try {
3864
- const authService = await this.resolveService(import_system.CoreServiceName.enum.auth);
3865
- const rawHeaders = context.request?.headers;
3866
- let headers = rawHeaders;
3867
- if (rawHeaders && typeof rawHeaders === "object" && typeof rawHeaders.get !== "function") {
3868
- try {
3869
- const h = new Headers();
3870
- for (const [k, v] of Object.entries(rawHeaders)) {
3871
- if (v == null) continue;
3872
- h.set(k, Array.isArray(v) ? v.join(", ") : String(v));
3873
- }
3874
- headers = h;
3875
- } catch {
3876
- headers = rawHeaders;
3877
- }
3878
- }
3879
- const sessionData = await (authService?.auth?.api?.getSession ?? authService?.api?.getSession)?.call(
3880
- authService?.auth?.api ?? authService?.api,
3881
- { headers }
3882
- );
3883
- return sessionData?.user?.id ?? sessionData?.session?.userId;
3884
- } catch (e) {
3885
- return void 0;
3886
- }
3887
- }
3888
- async handleCloud(path, method, body, query, _context) {
3889
- const m = method.toUpperCase();
3890
- const parts = path.replace(/^\/+/, "").split("/").filter(Boolean);
3891
- const qlService = await this.getObjectQLService();
3892
- const ql = qlService ?? await this.resolveService("objectql");
3893
- if (!ql) {
3894
- return { handled: true, response: this.error("Project service not available (ObjectQL missing)", 503) };
3895
- }
3896
- const ENV = "sys_environment";
3897
- const CRED = "sys_environment_credential";
3898
- const MEM = "sys_environment_member";
3899
- const PKG_INSTALL = "sys_package_installation";
3900
- const PKG = "sys_package";
3901
- const PKG_VERSION = "sys_package_version";
3902
- const ensureSysPackage = async (manifestId, ownerOrgId, createdBy, manifest) => {
3903
- const existing = await ql.findOne(PKG, { where: { manifest_id: manifestId } });
3904
- if (existing?.id) return existing.id;
3905
- const id = randomUUID();
3906
- const nowIso = (/* @__PURE__ */ new Date()).toISOString();
3907
- await ql.insert(PKG, {
3908
- id,
3909
- manifest_id: manifestId,
3910
- owner_org_id: ownerOrgId,
3911
- display_name: manifest?.name ?? manifestId,
3912
- description: manifest?.description ?? null,
3913
- visibility: "private",
3914
- created_by: createdBy,
3915
- created_at: nowIso,
3916
- updated_at: nowIso
3917
- });
3918
- return id;
3919
- };
3920
- const ensureSysPackageVersion = async (packageId, version, createdBy, manifest) => {
3921
- const existing = await ql.findOne(PKG_VERSION, {
3922
- where: { package_id: packageId, version }
3923
- });
3924
- if (existing?.id) return existing.id;
3925
- const id = randomUUID();
3926
- const nowIso = (/* @__PURE__ */ new Date()).toISOString();
3927
- await ql.insert(PKG_VERSION, {
3928
- id,
3929
- package_id: packageId,
3930
- version,
3931
- status: "published",
3932
- manifest_json: manifest ? JSON.stringify(manifest) : null,
3933
- is_pre_release: false,
3934
- published_at: nowIso,
3935
- published_by: createdBy,
3936
- created_by: createdBy,
3937
- created_at: nowIso,
3938
- updated_at: nowIso
3939
- });
3940
- return id;
3941
- };
3942
- const findInstallByManifestId = async (envId, manifestId) => {
3943
- const pkgRow = await ql.findOne(PKG, { where: { manifest_id: manifestId } });
3944
- if (!pkgRow?.id) return null;
3945
- return await ql.findOne(PKG_INSTALL, {
3946
- where: { environment_id: envId, package_id: pkgRow.id }
3947
- });
3948
- };
3949
- const toShortName = (driverId) => {
3950
- const prefix = "com.objectstack.driver.";
3951
- return driverId.startsWith(prefix) ? driverId.slice(prefix.length) : driverId;
3952
- };
3953
- const listRegisteredDrivers = () => {
3954
- const services = this.getServicesMap();
3955
- const registry = services["project-provisioning-adapters"];
3956
- if (registry && typeof registry.list === "function") {
3957
- try {
3958
- const adapters = registry.list();
3959
- const seen = /* @__PURE__ */ new Set();
3960
- const drivers2 = [];
3961
- for (const adapter of adapters ?? []) {
3962
- const name = adapter?.driver;
3963
- if (!name || seen.has(name)) continue;
3964
- seen.add(name);
3965
- drivers2.push({ name, driverId: `com.objectstack.driver.${name}` });
3966
- }
3967
- if (drivers2.length > 0) return drivers2;
3968
- } catch {
3969
- }
3970
- }
3971
- const drivers = [];
3972
- for (const [serviceKey, svc] of Object.entries(services)) {
3973
- if (!serviceKey.startsWith("driver.")) continue;
3974
- const raw = serviceKey.slice("driver.".length);
3975
- if (!raw || raw === "unknown") continue;
3976
- const driverId = svc?.name ?? raw;
3977
- drivers.push({ name: toShortName(driverId), driverId });
3978
- }
3979
- return drivers;
3980
- };
3981
- const resolveDriver = (requested) => {
3982
- const registered = listRegisteredDrivers();
3983
- if (requested) {
3984
- const wanted = String(requested).toLowerCase();
3985
- return registered.find((d) => d.name === wanted || d.driverId === wanted);
3986
- }
3987
- return registered.find((d) => d.name === "turso") ?? registered.find((d) => d.name === "memory") ?? registered[0];
3988
- };
3989
- const buildDatabaseUrl = (driverName, environmentId) => {
3990
- const dbName = `env-${environmentId}`;
3991
- switch (driverName) {
3992
- case "memory":
3993
- return `memory://${dbName}`;
3994
- case "turso":
3995
- return `libsql://${dbName}.mock-turso.local`;
3996
- default:
3997
- return `${driverName}://${dbName}`;
3998
- }
3999
- };
4000
- const getRealAdapter = async (driverName) => {
4001
- try {
4002
- const registry = await this.resolveService("project-provisioning-adapters");
4003
- const aliases = { sql: "sqlite" };
4004
- const effective = aliases[driverName] ?? driverName;
4005
- return registry?.get?.(effective) ?? registry?.get?.(driverName);
4006
- } catch {
4007
- return void 0;
4008
- }
4009
- };
4010
- const findOne = async (obj, where) => {
4011
- let rows = await ql.find(obj, { where });
4012
- if (rows && rows.value) rows = rows.value;
4013
- if (!Array.isArray(rows)) return void 0;
4014
- return rows[0];
4015
- };
4016
- const cleanProjectRow = (row) => {
4017
- if (!row) return row;
4018
- let metadata = row.metadata;
4019
- if (typeof metadata === "string") {
4020
- try {
4021
- metadata = JSON.parse(metadata);
4022
- } catch {
4023
- }
4024
- }
4025
- return { ...row, metadata };
4026
- };
4027
- try {
4028
- if (parts.length === 1 && parts[0] === "drivers" && m === "GET") {
4029
- const drivers = listRegisteredDrivers();
4030
- return { handled: true, response: this.success({ drivers, total: drivers.length }) };
4031
- }
4032
- if (parts.length === 1 && parts[0] === "templates" && m === "GET") {
4033
- try {
4034
- const seeder = await this.resolveService("template-seeder");
4035
- const templates = seeder?.listTemplates?.() ?? [];
4036
- return { handled: true, response: this.success({ templates, total: templates.length }) };
4037
- } catch (err) {
4038
- try {
4039
- console.error("[HttpDispatcher] /cloud/templates: failed to resolve template-seeder:", err?.message ?? err);
4040
- } catch {
4041
- }
4042
- return { handled: true, response: this.success({ templates: [], total: 0 }) };
4043
- }
4044
- }
4045
- if (parts.length === 3 && parts[0] === "admin" && parts[1] === "platform-sso" && parts[2] === "backfill" && m === "POST") {
4046
- const baseSecret = ((0, import_types3.readEnvWithDeprecation)("OS_AUTH_SECRET", ["AUTH_SECRET", "BETTER_AUTH_SECRET"]) ?? "").trim();
4047
- if (!baseSecret) {
4048
- return { handled: true, response: this.error("OS_AUTH_SECRET not configured on this worker", 503) };
4049
- }
4050
- const rawHeaders = _context?.request?.headers;
4051
- let authHeader;
4052
- if (rawHeaders && typeof rawHeaders.get === "function") {
4053
- authHeader = rawHeaders.get("authorization") ?? void 0;
4054
- } else if (rawHeaders && typeof rawHeaders === "object") {
4055
- authHeader = rawHeaders["authorization"] ?? rawHeaders["Authorization"];
4056
- }
4057
- const presented = typeof authHeader === "string" && authHeader.startsWith("Bearer ") ? authHeader.slice(7).trim() : "";
4058
- if (!presented || presented !== baseSecret) {
4059
- return { handled: true, response: this.error("forbidden: Bearer token must match OS_AUTH_SECRET", 403) };
4060
- }
4061
- try {
4062
- const { backfillPlatformSsoClients: backfillPlatformSsoClients2 } = await Promise.resolve().then(() => (init_platform_sso(), platform_sso_exports));
4063
- const result = await backfillPlatformSsoClients2({
4064
- ql,
4065
- baseSecret,
4066
- logger: console
4067
- });
4068
- let sample = [];
4069
- let total = 0;
4070
- try {
4071
- const rows = await ql.find("sys_oauth_application", { limit: 5 }, { context: { isSystem: true } });
4072
- const list = Array.isArray(rows) ? rows : Array.isArray(rows?.records) ? rows.records : [];
4073
- sample = list;
4074
- total = typeof rows?.total === "number" ? rows.total : list.length;
4075
- } catch (e) {
4076
- sample = [{ _readErr: e?.message ?? String(e) }];
4077
- }
4078
- return { handled: true, response: this.success({ ...result, total, sample }) };
4079
- } catch (err) {
4080
- return { handled: true, response: this.error(`backfill failed: ${err?.message ?? String(err)}`, 500) };
4081
- }
4082
- }
4083
- if (parts.length === 1 && parts[0] === "projects" && m === "GET") {
4084
- const where = {};
4085
- if (query?.organizationId) where.organization_id = query.organizationId;
4086
- if (query?.status) where.status = query.status;
4087
- let rows = await ql.find(ENV, Object.keys(where).length ? { where } : void 0);
4088
- if (rows && rows.value) rows = rows.value;
4089
- const projects = (Array.isArray(rows) ? rows : []).map(cleanProjectRow);
4090
- return { handled: true, response: this.success({ projects, total: projects.length }) };
4091
- }
4092
- if (parts.length === 1 && parts[0] === "projects" && m === "POST") {
4093
- const req = body || {};
4094
- if (req.organization_id === "__session__" || req.created_by === "__session__") {
4095
- try {
4096
- const userId = await this.resolveCallerUserId(_context);
4097
- if (req.created_by === "__session__") {
4098
- req.created_by = userId ?? "system";
4099
- }
4100
- if (req.organization_id === "__session__") {
4101
- const authService = await this.resolveService(import_system.CoreServiceName.enum.auth);
4102
- const rawHeaders = _context?.request?.headers;
4103
- let headers = rawHeaders;
4104
- if (rawHeaders && typeof rawHeaders === "object" && typeof rawHeaders.get !== "function") {
4105
- const h = new Headers();
4106
- for (const [k, v] of Object.entries(rawHeaders)) {
4107
- if (v == null) continue;
4108
- h.set(k, Array.isArray(v) ? v.join(", ") : String(v));
4109
- }
4110
- headers = h;
4111
- }
4112
- const apiObj = authService?.auth?.api ?? authService?.api;
4113
- const sessionData = await apiObj?.getSession?.call(apiObj, { headers });
4114
- req.organization_id = sessionData?.session?.activeOrganizationId ?? void 0;
4115
- }
4116
- } catch {
4117
- }
4118
- }
4119
- if (!req.organization_id || !req.display_name) {
4120
- return { handled: true, response: this.error("organization_id and display_name are required", 400) };
4121
- }
4122
- const environmentId = randomUUID();
4123
- const credentialId = randomUUID();
4124
- const nowIso = (/* @__PURE__ */ new Date()).toISOString();
4125
- const resolved = resolveDriver(req.driver);
4126
- if (!resolved) {
4127
- const available = listRegisteredDrivers().map((d) => d.name);
4128
- if (req.driver) {
4129
- return {
4130
- handled: true,
4131
- response: this.error(
4132
- `Unknown driver '${req.driver}'. Available drivers: [${available.join(", ") || "none"}]`,
4133
- 400
4134
- )
4135
- };
4136
- }
4137
- return {
4138
- handled: true,
4139
- response: this.error(
4140
- "No ObjectQL driver is registered. Register at least one DriverPlugin (e.g. InMemoryDriver or SqlDriver).",
4141
- 503
4142
- )
4143
- };
4144
- }
4145
- const driver = resolved.name;
4146
- let plaintextSecret = `mock-token-${environmentId}`;
4147
- let computedHostname = req.hostname;
4148
- if (!computedHostname) {
4149
- const shortId = environmentId.slice(0, 8);
4150
- try {
4151
- const orgRow = await findOne("sys_organization", { id: req.organization_id });
4152
- const orgSlug = orgRow?.slug || req.organization_id;
4153
- const rootDomain = (0, import_core2.getEnv)("OS_ROOT_DOMAIN") ?? (0, import_core2.getEnv)("ROOT_DOMAIN", "objectstack.app");
4154
- computedHostname = `${orgSlug}-${shortId}.${rootDomain}`;
4155
- } catch {
4156
- computedHostname = `${req.organization_id}-${shortId}.objectstack.app`;
4157
- }
4158
- }
4159
- try {
4160
- const existing = await findOne("sys_environment", {
4161
- hostname: computedHostname
4162
- });
4163
- if (existing && existing.id !== environmentId) {
4164
- return {
4165
- handled: true,
4166
- response: this.error(
4167
- `Hostname '${computedHostname}' is already in use by another project.`,
4168
- 409,
4169
- { code: "HOSTNAME_TAKEN", hostname: computedHostname }
4170
- )
4171
- };
4172
- }
4173
- } catch {
4174
- }
4175
- const baseMetadata = { ...req.metadata ?? {} };
4176
- const simulateFailure = Boolean(baseMetadata.__simulateFailure);
4177
- const simulateDelayMs = Number(baseMetadata.__simulateDelayMs ?? 1500);
4178
- try {
4179
- let ownerUserId = req.created_by && req.created_by !== "system" ? String(req.created_by) : void 0;
4180
- if (!ownerUserId) {
4181
- ownerUserId = await this.resolveCallerUserId(_context);
4182
- }
4183
- if (ownerUserId) {
4184
- const userRow = await ql.find("sys_user", { where: { id: ownerUserId } });
4185
- const userRows = Array.isArray(userRow) ? userRow : userRow?.value ?? [];
4186
- const u = Array.isArray(userRows) && userRows.length > 0 ? userRows[0] : null;
4187
- if (u?.email) {
4188
- baseMetadata.ownerSeed = {
4189
- userId: String(ownerUserId),
4190
- email: String(u.email),
4191
- name: u.name ? String(u.name) : null,
4192
- image: u.image ? String(u.image) : null
4193
- };
4194
- }
4195
- }
4196
- } catch {
4197
- }
4198
- try {
4199
- const orgRow = await ql.find("sys_organization", { where: { id: req.organization_id } });
4200
- const orgRows = Array.isArray(orgRow) ? orgRow : orgRow?.value ?? [];
4201
- const org = Array.isArray(orgRows) && orgRows.length > 0 ? orgRows[0] : null;
4202
- if (org?.id && org?.name) {
4203
- baseMetadata.orgSeed = {
4204
- id: String(org.id),
4205
- name: String(org.name),
4206
- slug: org.slug ? String(org.slug) : null,
4207
- logo: org.logo ? String(org.logo) : null
4208
- };
4209
- }
4210
- } catch {
4211
- }
4212
- await ql.insert(ENV, {
4213
- id: environmentId,
4214
- organization_id: req.organization_id,
4215
- display_name: req.display_name,
4216
- is_default: req.is_default ?? false,
4217
- is_system: req.is_system ?? false,
4218
- plan: req.plan ?? "free",
4219
- status: "provisioning",
4220
- created_by: req.created_by ?? "system",
4221
- metadata: JSON.stringify(baseMetadata),
4222
- created_at: nowIso,
4223
- updated_at: nowIso,
4224
- database_url: null,
4225
- database_driver: driver,
4226
- storage_limit_mb: req.storage_limit_mb ?? 1024,
4227
- provisioned_at: null,
4228
- hostname: computedHostname,
4229
- visibility: (() => {
4230
- const raw = String(req.visibility ?? "private");
4231
- return raw === "unlisted" ? "private" : raw;
4232
- })()
4233
- });
4234
- try {
4235
- const { seedPlatformSsoClient: seedPlatformSsoClient2 } = await Promise.resolve().then(() => (init_platform_sso(), platform_sso_exports));
4236
- const baseSecret = ((0, import_types3.readEnvWithDeprecation)("OS_AUTH_SECRET", ["AUTH_SECRET", "BETTER_AUTH_SECRET"]) ?? "").trim();
4237
- if (baseSecret) {
4238
- await seedPlatformSsoClient2({
4239
- ql,
4240
- environmentId,
4241
- hostname: computedHostname,
4242
- baseSecret,
4243
- logger: console
4244
- });
4245
- }
4246
- } catch (ssoErr) {
4247
- console.warn?.("[http-dispatcher] platform SSO seed failed (non-fatal)", {
4248
- environmentId,
4249
- error: ssoErr?.message
4250
- });
4251
- }
4252
- const runProvisioning = async () => {
4253
- try {
4254
- if (simulateDelayMs > 0) {
4255
- await new Promise((r) => setTimeout(r, simulateDelayMs));
4256
- }
4257
- if (simulateFailure) {
4258
- throw new Error("Simulated provisioning failure (metadata.__simulateFailure=true)");
4259
- }
4260
- let databaseUrl;
4261
- try {
4262
- const adapter = await getRealAdapter(driver);
4263
- if (adapter) {
4264
- const result = await adapter.createDatabase({
4265
- environmentId,
4266
- databaseName: `p-${environmentId.replace(/-/g, "").slice(0, 24)}`,
4267
- region: "us-east-1",
4268
- storageLimitMb: req.storage_limit_mb ?? 1024
4269
- });
4270
- databaseUrl = result.databaseUrl;
4271
- if (result.plaintextSecret) plaintextSecret = result.plaintextSecret;
4272
- } else {
4273
- databaseUrl = buildDatabaseUrl(driver, environmentId);
4274
- }
4275
- } catch (adapterErr) {
4276
- throw adapterErr instanceof Error ? adapterErr : new Error(String(adapterErr));
4277
- }
4278
- const seedStartedAt = (/* @__PURE__ */ new Date()).toISOString();
4279
- await ql.update(
4280
- ENV,
4281
- {
4282
- database_url: databaseUrl,
4283
- updated_at: seedStartedAt
4284
- },
4285
- { where: { id: environmentId } }
4286
- );
4287
- await ql.insert(CRED, {
4288
- id: credentialId,
4289
- environment_id: environmentId,
4290
- secret_ciphertext: plaintextSecret,
4291
- encryption_key_id: "noop",
4292
- authorization: "full_access",
4293
- status: "active",
4294
- created_at: seedStartedAt,
4295
- updated_at: seedStartedAt
4296
- });
4297
- const templateId = req.template_id ?? "blank";
4298
- if (templateId !== "blank") {
4299
- try {
4300
- const seeder = await this.resolveService("template-seeder");
4301
- if (seeder) {
4302
- await seeder.seed({ environmentId, templateId });
4303
- }
4304
- } catch (seedErr) {
4305
- const seedMessage = seedErr instanceof Error ? seedErr.message : String(seedErr);
4306
- try {
4307
- const existing = await findOne(ENV, { id: environmentId });
4308
- const existingMeta = typeof existing?.metadata === "string" ? JSON.parse(existing.metadata) : existing?.metadata ?? {};
4309
- await ql.update(
4310
- ENV,
4311
- {
4312
- metadata: JSON.stringify({
4313
- ...existingMeta,
4314
- templateSeedError: { message: seedMessage, templateId }
4315
- })
4316
- },
4317
- { where: { id: environmentId } }
4318
- );
4319
- } catch {
4320
- }
4321
- }
4322
- }
4323
- const artifactPathRaw = baseMetadata.artifact_path;
4324
- if (typeof artifactPathRaw === "string" && artifactPathRaw.length > 0) {
4325
- try {
4326
- const path2 = await import("path");
4327
- const { isHttpUrl: isHttpUrl2, loadArtifactBundle: loadArtifactBundle2 } = await Promise.resolve().then(() => (init_load_artifact_bundle(), load_artifact_bundle_exports));
4328
- const root = process.env.OS_PROJECT_ARTIFACT_ROOT ?? process.cwd();
4329
- const resolved2 = isHttpUrl2(artifactPathRaw) ? artifactPathRaw : path2.isAbsolute(artifactPathRaw) ? artifactPathRaw : path2.resolve(root, artifactPathRaw);
4330
- const bundle = await loadArtifactBundle2(resolved2, { tag: "[bind-artifact]" });
4331
- if (!bundle) {
4332
- throw new Error(`failed to load artifact bundle at '${resolved2}'`);
4333
- }
4334
- const seeder = await this.resolveService("template-seeder");
4335
- if (seeder?.seedBundle) {
4336
- await seeder.seedBundle({ environmentId, bundle });
4337
- } else {
4338
- throw new Error("template-seeder.seedBundle is unavailable");
4339
- }
4340
- } catch (bindErr) {
4341
- const bindMessage = bindErr instanceof Error ? bindErr.message : String(bindErr);
4342
- try {
4343
- const existing = await findOne(ENV, { id: environmentId });
4344
- const existingMeta = typeof existing?.metadata === "string" ? JSON.parse(existing.metadata) : existing?.metadata ?? {};
4345
- await ql.update(
4346
- ENV,
4347
- {
4348
- metadata: JSON.stringify({
4349
- ...existingMeta,
4350
- artifactBindError: { message: bindMessage, artifactPath: artifactPathRaw }
4351
- })
4352
- },
4353
- { where: { id: environmentId } }
4354
- );
4355
- } catch {
4356
- }
4357
- }
4358
- }
4359
- const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
4360
- await ql.update(
4361
- ENV,
4362
- {
4363
- status: "active",
4364
- provisioned_at: finishedAt,
4365
- updated_at: finishedAt
4366
- },
4367
- { where: { id: environmentId } }
4368
- );
4369
- } catch (err) {
4370
- const message = err instanceof Error ? err.message : String(err);
4371
- const failedAt = (/* @__PURE__ */ new Date()).toISOString();
4372
- await ql.update(
4373
- ENV,
4374
- {
4375
- status: "failed",
4376
- metadata: JSON.stringify({
4377
- ...baseMetadata,
4378
- provisioningError: { message, failedAt }
4379
- }),
4380
- updated_at: failedAt
4381
- },
4382
- { where: { id: environmentId } }
4383
- );
4384
- }
4385
- };
4386
- const provisionSyncEnv = process.env.OS_PROVISION_SYNC;
4387
- const onServerless = !!(process.env.VERCEL || process.env.AWS_LAMBDA_FUNCTION_NAME || process.env.NETLIFY || process.env.CF_PAGES);
4388
- const syncProvisioning = provisionSyncEnv === void 0 ? onServerless : provisionSyncEnv !== "0" && provisionSyncEnv !== "false";
4389
- if (syncProvisioning) {
4390
- await runProvisioning();
4391
- } else {
4392
- void runProvisioning();
4393
- }
4394
- const project = cleanProjectRow(await findOne(ENV, { id: environmentId }));
4395
- const res = this.success({ project });
4396
- res.status = syncProvisioning ? 201 : 202;
4397
- return { handled: true, response: res };
4398
- }
4399
- if (parts.length === 2 && parts[0] === "projects") {
4400
- const id = decodeURIComponent(parts[1]);
4401
- if (m === "GET") {
4402
- const envRow = await findOne(ENV, { id });
4403
- if (!envRow) return { handled: true, response: this.error(`Project '${id}' not found`, 404) };
4404
- const credRow = await findOne(CRED, { environment_id: id, status: "active" });
4405
- const callerUserId = await this.resolveCallerUserId(_context);
4406
- const membership = callerUserId ? await findOne(MEM, { environment_id: id, user_id: callerUserId }) : await findOne(MEM, { environment_id: id });
4407
- const credMeta = credRow ? {
4408
- id: credRow.id,
4409
- status: credRow.status,
4410
- authorization: credRow.authorization,
4411
- activatedAt: credRow.created_at,
4412
- expiresAt: credRow.expires_at
4413
- } : void 0;
4414
- const project = cleanProjectRow(envRow);
4415
- const database = project.database_url ? {
4416
- driver: project.database_driver,
4417
- database_name: `env-${project.id}`,
4418
- database_url: project.database_url,
4419
- storage_limit_mb: project.storage_limit_mb,
4420
- provisioned_at: project.provisioned_at
4421
- } : void 0;
4422
- return {
4423
- handled: true,
4424
- response: this.success({ project, database, credential: credMeta, membership })
4425
- };
4426
- }
4427
- if (m === "PATCH") {
4428
- const patch = {};
4429
- if (body?.display_name !== void 0) patch.display_name = body.display_name;
4430
- if (body?.plan !== void 0) patch.plan = body.plan;
4431
- if (body?.status !== void 0) patch.status = body.status;
4432
- if (body?.is_default !== void 0) patch.is_default = body.is_default;
4433
- if (body?.visibility !== void 0) {
4434
- let v = String(body.visibility);
4435
- if (v === "unlisted") v = "private";
4436
- if (!["private", "public"].includes(v)) {
4437
- return { handled: true, response: this.error(`Invalid visibility '${v}' (expected private | public)`, 400) };
4438
- }
4439
- patch.visibility = v;
4440
- }
4441
- if (body?.metadata !== void 0) patch.metadata = JSON.stringify(body.metadata);
4442
- patch.updated_at = (/* @__PURE__ */ new Date()).toISOString();
4443
- await ql.update(ENV, patch, { where: { id } });
4444
- const envRow = await findOne(ENV, { id });
4445
- if (!envRow) return { handled: true, response: this.error(`Project '${id}' not found`, 404) };
4446
- return { handled: true, response: this.success({ project: cleanProjectRow(envRow) }) };
4447
- }
4448
- if (m === "DELETE") {
4449
- const force = query?.force === "1" || query?.force === "true" || body?.force === true;
4450
- const result = await this.deleteProjectCascade(id, { ql, findOne, getRealAdapter, force });
4451
- if (!result.ok) {
4452
- return { handled: true, response: this.error(result.error ?? "Delete failed", result.status ?? 500) };
4453
- }
4454
- return { handled: true, response: this.success({ deleted: true, environmentId: id, warnings: result.warnings }) };
4455
- }
4456
- }
4457
- if (parts.length === 2 && parts[0] === "organizations" && m === "DELETE") {
4458
- const orgId = decodeURIComponent(parts[1]);
4459
- let projectRows = [];
4460
- try {
4461
- let rows = await ql.find(ENV, { where: { organization_id: orgId } });
4462
- if (rows && rows.value) rows = rows.value;
4463
- projectRows = Array.isArray(rows) ? rows : [];
4464
- } catch {
4465
- projectRows = [];
4466
- }
4467
- const warnings = [];
4468
- let deletedProjects = 0;
4469
- for (const row of projectRows) {
4470
- const pid = row?.id;
4471
- if (!pid) continue;
4472
- try {
4473
- const r = await this.deleteProjectCascade(pid, { ql, findOne, getRealAdapter, force: true });
4474
- if (r.ok) deletedProjects++;
4475
- if (r.warnings?.length) warnings.push(...r.warnings);
4476
- if (!r.ok && r.error) warnings.push(`Project ${pid}: ${r.error}`);
4477
- } catch (err) {
4478
- warnings.push(
4479
- `Failed to delete project ${pid}: ${err instanceof Error ? err.message : String(err)}`
4480
- );
4481
- }
4482
- }
4483
- let orgDeleted = false;
4484
- try {
4485
- const authService = await this.getService(import_system.CoreServiceName.enum.auth);
4486
- const fn = authService?.api?.deleteOrganization;
4487
- if (typeof fn === "function") {
4488
- await fn.call(authService.api, {
4489
- body: { organizationId: orgId },
4490
- headers: _context?.request?.headers
4491
- });
4492
- orgDeleted = true;
4493
- }
4494
- } catch (err) {
4495
- warnings.push(
4496
- `auth.deleteOrganization failed: ${err instanceof Error ? err.message : String(err)}`
4497
- );
4498
- }
4499
- if (!orgDeleted) {
4500
- try {
4501
- await ql.delete("sys_organization", { where: { id: orgId } });
4502
- orgDeleted = true;
4503
- } catch (err) {
4504
- warnings.push(
4505
- `Failed to delete sys_organization row: ${err instanceof Error ? err.message : String(err)}`
4506
- );
4507
- }
4508
- }
4509
- return {
4510
- handled: true,
4511
- response: this.success({
4512
- deleted: orgDeleted,
4513
- organizationId: orgId,
4514
- deletedProjects,
4515
- warnings
4516
- })
4517
- };
4518
- }
4519
- if (parts.length === 3 && parts[0] === "projects" && parts[2] === "hostname" && (m === "POST" || m === "PUT")) {
4520
- const id = decodeURIComponent(parts[1]);
4521
- const hostname = body?.hostname;
4522
- if (!hostname || typeof hostname !== "string") {
4523
- return { handled: true, response: this.error("hostname is required", 400) };
4524
- }
4525
- const normalized = hostname.trim().toLowerCase();
4526
- if (!/^[a-z0-9]([a-z0-9\-\.]*[a-z0-9])?$/.test(normalized)) {
4527
- return { handled: true, response: this.error("Invalid hostname format", 400) };
4528
- }
4529
- const envRow = await findOne(ENV, { id });
4530
- if (!envRow) return { handled: true, response: this.error(`Project '${id}' not found`, 404) };
4531
- let existing;
4532
- try {
4533
- const rows = await ql.find(ENV, { where: { hostname: normalized } });
4534
- const arr = Array.isArray(rows) ? rows : rows?.value ?? [];
4535
- existing = arr.find((r) => r.id !== id);
4536
- } catch {
4537
- }
4538
- if (existing) {
4539
- return {
4540
- handled: true,
4541
- response: this.error(
4542
- `Hostname '${normalized}' is already in use by another project.`,
4543
- 409,
4544
- { code: "HOSTNAME_TAKEN", hostname: normalized }
4545
- )
4546
- };
4547
- }
4548
- const updatedAt = (/* @__PURE__ */ new Date()).toISOString();
4549
- await ql.update(ENV, { hostname: normalized, updated_at: updatedAt }, { where: { id } });
4550
- if (this.envRegistry?.invalidate) {
4551
- try {
4552
- await this.envRegistry.invalidate(id);
4553
- } catch {
4554
- }
4555
- }
4556
- const updated = cleanProjectRow(await findOne(ENV, { id }));
4557
- return { handled: true, response: this.success({ project: updated }) };
4558
- }
4559
- if (parts.length === 3 && parts[0] === "projects" && parts[2] === "retry" && m === "POST") {
4560
- const id = decodeURIComponent(parts[1]);
4561
- const envRow = await findOne(ENV, { id });
4562
- if (!envRow) return { handled: true, response: this.error(`Project '${id}' not found`, 404) };
4563
- if (envRow.status !== "failed" && envRow.status !== "provisioning") {
4564
- return {
4565
- handled: true,
4566
- response: this.error(
4567
- `Project '${id}' is '${envRow.status}'; only failed or provisioning projects can be retried.`,
4568
- 409
4569
- )
4570
- };
4571
- }
4572
- const driverName = envRow.database_driver;
4573
- const resolved = resolveDriver(driverName);
4574
- if (!resolved) {
4575
- return {
4576
- handled: true,
4577
- response: this.error(
4578
- `Driver '${driverName}' is no longer registered; retry aborted.`,
4579
- 503
4580
- )
4581
- };
4582
- }
4583
- let metadata = {};
4584
- if (envRow.metadata) {
4585
- if (typeof envRow.metadata === "string") {
4586
- try {
4587
- metadata = JSON.parse(envRow.metadata);
4588
- } catch {
4589
- metadata = {};
4590
- }
4591
- } else if (typeof envRow.metadata === "object") {
4592
- metadata = { ...envRow.metadata };
4593
- }
4594
- }
4595
- delete metadata.provisioningError;
4596
- const retryStartedAt = (/* @__PURE__ */ new Date()).toISOString();
4597
- await ql.update(
4598
- ENV,
4599
- {
4600
- status: "provisioning",
4601
- metadata: JSON.stringify(metadata),
4602
- updated_at: retryStartedAt
4603
- },
4604
- { where: { id } }
4605
- );
4606
- const simulateRetryFailure = Boolean(metadata.__simulateFailure);
4607
- const simulateRetryDelay = Number(metadata.__simulateDelayMs ?? 1500);
4608
- const runRetry = async () => {
4609
- try {
4610
- if (simulateRetryDelay > 0) {
4611
- await new Promise((r) => setTimeout(r, simulateRetryDelay));
4612
- }
4613
- if (simulateRetryFailure) {
4614
- throw new Error("Simulated provisioning failure (metadata.__simulateFailure=true)");
4615
- }
4616
- let databaseUrl;
4617
- let retrySecret = `mock-token-${id}`;
4618
- try {
4619
- const adapter = await getRealAdapter(resolved.name);
4620
- if (adapter) {
4621
- const result = await adapter.createDatabase({
4622
- environmentId: id,
4623
- databaseName: `p-${id.replace(/-/g, "").slice(0, 24)}`,
4624
- region: "us-east-1",
4625
- storageLimitMb: envRow.storage_limit_mb ?? 1024
4626
- });
4627
- databaseUrl = result.databaseUrl;
4628
- if (result.plaintextSecret) retrySecret = result.plaintextSecret;
4629
- } else {
4630
- databaseUrl = buildDatabaseUrl(resolved.name, id);
4631
- }
4632
- } catch (adapterErr) {
4633
- throw adapterErr instanceof Error ? adapterErr : new Error(String(adapterErr));
4634
- }
4635
- const nowIso = (/* @__PURE__ */ new Date()).toISOString();
4636
- await ql.update(
4637
- ENV,
4638
- {
4639
- status: "active",
4640
- database_url: databaseUrl,
4641
- database_driver: resolved.name,
4642
- provisioned_at: nowIso,
4643
- updated_at: nowIso
4644
- },
4645
- { where: { id } }
4646
- );
4647
- const existingCred = await findOne(CRED, { environment_id: id, status: "active" });
4648
- if (!existingCred) {
4649
- await ql.insert(CRED, {
4650
- id: randomUUID(),
4651
- environment_id: id,
4652
- secret_ciphertext: retrySecret,
4653
- encryption_key_id: "noop",
4654
- authorization: "full_access",
4655
- status: "active",
4656
- created_at: nowIso,
4657
- updated_at: nowIso
4658
- });
4659
- }
4660
- } catch (err) {
4661
- const message = err instanceof Error ? err.message : String(err);
4662
- const failedAt = (/* @__PURE__ */ new Date()).toISOString();
4663
- await ql.update(
4664
- ENV,
4665
- {
4666
- status: "failed",
4667
- metadata: JSON.stringify({
4668
- ...metadata,
4669
- provisioningError: { message, failedAt }
4670
- }),
4671
- updated_at: failedAt
4672
- },
4673
- { where: { id } }
4674
- );
4675
- }
4676
- };
4677
- void runRetry();
4678
- const envAfter = cleanProjectRow(await findOne(ENV, { id }));
4679
- const retryRes = this.success({ project: envAfter });
4680
- retryRes.status = 202;
4681
- return { handled: true, response: retryRes };
4682
- }
4683
- if (parts.length === 3 && parts[0] === "projects" && parts[2] === "activate" && m === "POST") {
4684
- const id = decodeURIComponent(parts[1]);
4685
- const envRow = await findOne(ENV, { id });
4686
- if (!envRow) return { handled: true, response: this.error(`Project '${id}' not found`, 404) };
4687
- return { handled: true, response: this.success({ project: cleanProjectRow(envRow), sessionUpdated: false }) };
4688
- }
4689
- if (parts.length === 4 && parts[0] === "projects" && parts[2] === "credentials" && parts[3] === "rotate" && m === "POST") {
4690
- const id = decodeURIComponent(parts[1]);
4691
- const plaintext = body?.plaintext;
4692
- if (!plaintext || typeof plaintext !== "string") {
4693
- return { handled: true, response: this.error("plaintext is required", 400) };
4694
- }
4695
- const envRow = await findOne(ENV, { id });
4696
- if (!envRow) return { handled: true, response: this.error(`Project '${id}' not found`, 404) };
4697
- const nowIso = (/* @__PURE__ */ new Date()).toISOString();
4698
- let existing = await ql.find(CRED, { where: { environment_id: id, status: "active" } });
4699
- if (existing && existing.value) existing = existing.value;
4700
- for (const row of Array.isArray(existing) ? existing : []) {
4701
- await ql.update(CRED, {
4702
- status: "revoked",
4703
- revoked_at: nowIso,
4704
- updated_at: nowIso
4705
- }, { where: { id: row.id } });
4706
- }
4707
- const credentialId = randomUUID();
4708
- await ql.insert(CRED, {
4709
- id: credentialId,
4710
- environment_id: id,
4711
- secret_ciphertext: plaintext,
4712
- encryption_key_id: "noop",
4713
- authorization: "full_access",
4714
- status: "active",
4715
- created_at: nowIso,
4716
- updated_at: nowIso
4717
- });
4718
- const credential = await findOne(CRED, { id: credentialId });
4719
- const credMeta = credential ? {
4720
- id: credential.id,
4721
- status: credential.status,
4722
- authorization: credential.authorization,
4723
- activatedAt: credential.created_at
4724
- } : void 0;
4725
- return { handled: true, response: this.success({ credential: credMeta }) };
4726
- }
4727
- if (parts.length === 3 && parts[0] === "projects" && parts[2] === "members" && m === "GET") {
4728
- const id = decodeURIComponent(parts[1]);
4729
- let rows = await ql.find(MEM, { where: { environment_id: id } });
4730
- if (rows && rows.value) rows = rows.value;
4731
- const members = Array.isArray(rows) ? rows : [];
4732
- const userIds = Array.from(new Set(members.map((mem) => mem.user_id).filter(Boolean)));
4733
- const userMap = /* @__PURE__ */ new Map();
4734
- for (const uid of userIds) {
4735
- let row = null;
4736
- for (const tableName of ["sys_user", "user"]) {
4737
- try {
4738
- const u = await ql.findOne(tableName, { where: { id: uid } });
4739
- row = u?.value ?? u;
4740
- if (row) break;
4741
- } catch {
4742
- }
4743
- }
4744
- if (row) userMap.set(String(uid), {
4745
- id: row.id,
4746
- name: row.name ?? row.display_name,
4747
- email: row.email,
4748
- image: row.image ?? row.avatar_url
4749
- });
4750
- }
4751
- const enriched = members.map((mem) => ({
4752
- ...mem,
4753
- user: userMap.get(String(mem.user_id)) ?? void 0
4754
- }));
4755
- return { handled: true, response: this.success({ members: enriched }) };
4756
- }
4757
- if (parts.length === 3 && parts[0] === "projects" && parts[2] === "members" && m === "POST") {
4758
- const id = decodeURIComponent(parts[1]);
4759
- const project = await findOne(ENV, { id });
4760
- if (!project) return { handled: true, response: this.error(`Project '${id}' not found`, 404) };
4761
- const callerId = await this.resolveCallerUserId(_context);
4762
- if (!callerId) return { handled: true, response: this.error("Authentication required", 401) };
4763
- const callerMem = await findOne(MEM, { environment_id: id, user_id: callerId });
4764
- if (!callerMem || !["owner", "admin"].includes(String(callerMem.role))) {
4765
- return { handled: true, response: this.error("Forbidden \u2014 owner or admin required", 403) };
4766
- }
4767
- const email = typeof body?.email === "string" ? String(body.email).trim().toLowerCase() : null;
4768
- let inviteUserId = typeof body?.user_id === "string" ? String(body.user_id).trim() : null;
4769
- let role = String(body?.role ?? "member").trim().toLowerCase();
4770
- if (!["owner", "admin", "member", "viewer"].includes(role)) {
4771
- return { handled: true, response: this.error(`Invalid role '${role}' (expected owner | admin | member | viewer)`, 400) };
4772
- }
4773
- if (!email && !inviteUserId) {
4774
- return { handled: true, response: this.error("email or user_id is required", 400) };
4775
- }
4776
- if (!inviteUserId && email) {
4777
- let row = null;
4778
- for (const tableName of ["sys_user", "user"]) {
4779
- try {
4780
- const u = await ql.findOne(tableName, { where: { email } });
4781
- row = u?.value ?? u;
4782
- if (row) break;
4783
- } catch {
4784
- }
4785
- }
4786
- if (!row?.id) {
4787
- return { handled: true, response: this.error(`No user found with email '${email}'`, 404) };
4788
- }
4789
- inviteUserId = String(row.id);
4790
- }
4791
- const existing = await findOne(MEM, { environment_id: id, user_id: inviteUserId });
4792
- if (existing) {
4793
- return { handled: true, response: this.success({ member: existing, alreadyMember: true }) };
4794
- }
4795
- try {
4796
- const memberId = randomUUID();
4797
- await ql.insert(MEM, {
4798
- id: memberId,
4799
- environment_id: id,
4800
- user_id: inviteUserId,
4801
- role,
4802
- invited_by: callerId,
4803
- organization_id: project.organization_id ?? null
4804
- });
4805
- const created = await findOne(MEM, { id: memberId });
4806
- return { handled: true, response: this.success({ member: created, alreadyMember: false }) };
4807
- } catch (e) {
4808
- return { handled: true, response: this.error(e?.message ?? "Failed to add member", 500) };
4809
- }
4810
- }
4811
- if (parts.length === 4 && parts[0] === "projects" && parts[2] === "members" && m === "PATCH") {
4812
- const id = decodeURIComponent(parts[1]);
4813
- const memberId = decodeURIComponent(parts[3]);
4814
- const project = await findOne(ENV, { id });
4815
- if (!project) return { handled: true, response: this.error(`Project '${id}' not found`, 404) };
4816
- const callerId = await this.resolveCallerUserId(_context);
4817
- if (!callerId) return { handled: true, response: this.error("Authentication required", 401) };
4818
- const callerMem = await findOne(MEM, { environment_id: id, user_id: callerId });
4819
- if (!callerMem || !["owner", "admin"].includes(String(callerMem.role))) {
4820
- return { handled: true, response: this.error("Forbidden \u2014 owner or admin required", 403) };
4821
- }
4822
- const target = await findOne(MEM, { id: memberId, environment_id: id });
4823
- if (!target) return { handled: true, response: this.error(`Member '${memberId}' not found`, 404) };
4824
- const newRole = String(body?.role ?? "").trim().toLowerCase();
4825
- if (!["owner", "admin", "member", "viewer"].includes(newRole)) {
4826
- return { handled: true, response: this.error(`Invalid role '${newRole}'`, 400) };
4827
- }
4828
- if (target.role === "owner" && newRole !== "owner") {
4829
- let owners = await ql.find(MEM, { where: { environment_id: id, role: "owner" } });
4830
- if (owners && owners.value) owners = owners.value;
4831
- const ownerCount = Array.isArray(owners) ? owners.length : 0;
4832
- if (ownerCount <= 1) {
4833
- return { handled: true, response: this.error("Cannot demote the last owner", 409) };
4834
- }
4835
- }
4836
- try {
4837
- await ql.update(MEM, { role: newRole, updated_at: (/* @__PURE__ */ new Date()).toISOString() }, { where: { id: memberId } });
4838
- const updated = await findOne(MEM, { id: memberId });
4839
- return { handled: true, response: this.success({ member: updated }) };
4840
- } catch (e) {
4841
- return { handled: true, response: this.error(e?.message ?? "Failed to update role", 500) };
4842
- }
4843
- }
4844
- if (parts.length === 4 && parts[0] === "projects" && parts[2] === "members" && m === "DELETE") {
4845
- const id = decodeURIComponent(parts[1]);
4846
- const memberId = decodeURIComponent(parts[3]);
4847
- const project = await findOne(ENV, { id });
4848
- if (!project) return { handled: true, response: this.error(`Project '${id}' not found`, 404) };
4849
- const callerId = await this.resolveCallerUserId(_context);
4850
- if (!callerId) return { handled: true, response: this.error("Authentication required", 401) };
4851
- const target = await findOne(MEM, { id: memberId, environment_id: id });
4852
- if (!target) return { handled: true, response: this.error(`Member '${memberId}' not found`, 404) };
4853
- const callerMem = await findOne(MEM, { environment_id: id, user_id: callerId });
4854
- const isSelf = String(target.user_id) === String(callerId);
4855
- const isPrivileged = callerMem && ["owner", "admin"].includes(String(callerMem.role));
4856
- if (!isSelf && !isPrivileged) {
4857
- return { handled: true, response: this.error("Forbidden \u2014 owner or admin required", 403) };
4858
- }
4859
- if (target.role === "owner") {
4860
- let owners = await ql.find(MEM, { where: { environment_id: id, role: "owner" } });
4861
- if (owners && owners.value) owners = owners.value;
4862
- const ownerCount = Array.isArray(owners) ? owners.length : 0;
4863
- if (ownerCount <= 1) {
4864
- return { handled: true, response: this.error("Cannot remove the last owner", 409) };
4865
- }
4866
- }
4867
- try {
4868
- await ql.delete(MEM, { where: { id: memberId } });
4869
- return { handled: true, response: this.success({ removed: true, memberId }) };
4870
- } catch (e) {
4871
- return { handled: true, response: this.error(e?.message ?? "Failed to remove member", 500) };
4872
- }
4873
- }
4874
- if (parts.length === 3 && parts[0] === "projects" && parts[2] === "packages" && m === "GET") {
4875
- const envId = decodeURIComponent(parts[1]);
4876
- let rows = await ql.find(PKG_INSTALL, { where: { environment_id: envId } });
4877
- if (rows && rows.value) rows = rows.value;
4878
- const installs = Array.isArray(rows) ? rows : [];
4879
- const packages = await Promise.all(
4880
- installs.map(async (r) => {
4881
- let manifestId = null;
4882
- let versionStr = null;
4883
- try {
4884
- if (r.package_id) {
4885
- const pkg = await ql.findOne(PKG, { where: { id: r.package_id } });
4886
- manifestId = pkg?.manifest_id ?? null;
4887
- }
4888
- if (r.package_version_id) {
4889
- const ver = await ql.findOne(PKG_VERSION, { where: { id: r.package_version_id } });
4890
- versionStr = ver?.version ?? null;
4891
- }
4892
- } catch {
4893
- }
4894
- return {
4895
- ...r,
4896
- // Surface user-facing identifiers expected by client SDK
4897
- packageId: manifestId,
4898
- package_id: manifestId ?? r.package_id,
4899
- version: versionStr ?? r.version ?? null
4900
- };
4901
- })
4902
- );
4903
- return { handled: true, response: this.success({ packages, total: packages.length }) };
4904
- }
4905
- if (parts.length === 3 && parts[0] === "projects" && parts[2] === "packages" && m === "POST") {
4906
- const envId = decodeURIComponent(parts[1]);
4907
- const { packageId, version, settings, enableOnInstall } = body ?? {};
4908
- if (!packageId) return { handled: true, response: this.error("packageId is required", 400) };
4909
- const qlSvc = await this.getObjectQLService();
4910
- const pkgRegistry = qlSvc?.registry;
4911
- const allPkgs = pkgRegistry?.getAllPackages?.() ?? [];
4912
- const manifestEntry = allPkgs.find((p) => (p?.manifest?.id ?? p?.id) === packageId);
4913
- const manifest = manifestEntry?.manifest ?? manifestEntry;
4914
- if (!manifest) {
4915
- return { handled: true, response: this.error(`Package '${packageId}' is not registered on this server`, 404) };
4916
- }
4917
- const CLOUD_SCOPES = /* @__PURE__ */ new Set(["cloud", "system", "platform"]);
4918
- if (CLOUD_SCOPES.has(manifest?.scope)) {
4919
- return { handled: true, response: this.error(`Package '${packageId}' has scope=${manifest.scope} and cannot be installed per-project`, 403) };
4920
- }
4921
- const projectRow = await findOne(ENV, { id: envId });
4922
- if (!projectRow) {
4923
- return { handled: true, response: this.error(`Project '${envId}' not found`, 404) };
4924
- }
4925
- const ownerOrgId = projectRow.organization_id ?? "system";
4926
- let userId = "system";
4927
- try {
4928
- const authService = await this.getService(import_system.CoreServiceName.enum.auth);
4929
- const sessionData = await authService?.api?.getSession?.({
4930
- headers: _context?.request?.headers
4931
- });
4932
- userId = sessionData?.user?.id ?? sessionData?.session?.userId ?? "system";
4933
- } catch {
4934
- }
4935
- const resolvedVersion = version ?? manifest?.version ?? "1.0.0";
4936
- const dup = await ql.findOne(PKG_INSTALL, {
4937
- where: { environment_id: envId, package_id: packageId }
4938
- });
4939
- if (dup?.id) {
4940
- return { handled: true, response: this.error(`Package '${packageId}' is already installed in this project`, 409) };
4941
- }
4942
- const sysPackageId = await ensureSysPackage(packageId, ownerOrgId, userId, manifest);
4943
- const sysPackageVersionId = await ensureSysPackageVersion(sysPackageId, resolvedVersion, userId, manifest);
4944
- const nowIso = (/* @__PURE__ */ new Date()).toISOString();
4945
- const recordId = randomUUID();
4946
- await ql.insert(PKG_INSTALL, {
4947
- id: recordId,
4948
- environment_id: envId,
4949
- package_id: sysPackageId,
4950
- package_version_id: sysPackageVersionId,
4951
- status: "installed",
4952
- enabled: enableOnInstall !== false,
4953
- installed_at: nowIso,
4954
- installed_by: userId,
4955
- updated_at: nowIso,
4956
- settings: settings ? JSON.stringify(settings) : null
4957
- });
4958
- const record = await ql.findOne(PKG_INSTALL, { where: { id: recordId } });
4959
- try {
4960
- await this.kernelManager?.evict(envId);
4961
- } catch {
4962
- }
4963
- return { handled: true, response: this.success({ package: record }) };
4964
- }
4965
- if (parts.length === 4 && parts[0] === "projects" && parts[2] === "packages" && m === "GET") {
4966
- const envId = decodeURIComponent(parts[1]);
4967
- const pkgId = decodeURIComponent(parts[3]);
4968
- const record = await ql.findOne(PKG_INSTALL, { where: { environment_id: envId, package_id: pkgId } });
4969
- if (!record) return { handled: true, response: this.error(`Package '${pkgId}' is not installed in this project`, 404) };
4970
- return { handled: true, response: this.success({ package: record }) };
4971
- }
4972
- if (parts.length === 4 && parts[0] === "projects" && parts[2] === "packages" && m === "DELETE") {
4973
- const envId = decodeURIComponent(parts[1]);
4974
- const pkgId = decodeURIComponent(parts[3]);
4975
- const record = await findInstallByManifestId(envId, pkgId);
4976
- if (!record) return { handled: true, response: this.error(`Package '${pkgId}' is not installed in this project`, 404) };
4977
- const allPkgs0 = this.kernel.packages?.getAll?.() ?? [];
4978
- const m0 = allPkgs0.find((p) => (p.manifest?.id ?? p.id) === pkgId)?.manifest;
4979
- if (m0?.scope && ["cloud", "system", "platform"].includes(m0.scope)) {
4980
- return { handled: true, response: this.error(`Package '${pkgId}' with scope=${m0.scope} cannot be uninstalled`, 403) };
4981
- }
4982
- await ql.delete(PKG_INSTALL, { where: { id: record.id } });
4983
- try {
4984
- await this.kernelManager?.evict(envId);
4985
- } catch {
4986
- }
4987
- return { handled: true, response: this.success({ id: record.id, success: true }) };
4988
- }
4989
- if (parts.length === 5 && parts[0] === "projects" && parts[2] === "packages" && parts[4] === "enable" && m === "PATCH") {
4990
- const envId = decodeURIComponent(parts[1]);
4991
- const pkgId = decodeURIComponent(parts[3]);
4992
- const record = await findInstallByManifestId(envId, pkgId);
4993
- if (!record) return { handled: true, response: this.error(`Package '${pkgId}' is not installed in this project`, 404) };
4994
- const nowIso = (/* @__PURE__ */ new Date()).toISOString();
4995
- await ql.update(PKG_INSTALL, { enabled: true, status: "installed", updated_at: nowIso }, { where: { id: record.id } });
4996
- const updated = await ql.findOne(PKG_INSTALL, { where: { id: record.id } });
4997
- try {
4998
- await this.kernelManager?.evict(envId);
4999
- } catch {
5000
- }
5001
- return { handled: true, response: this.success({ package: updated }) };
5002
- }
5003
- if (parts.length === 5 && parts[0] === "projects" && parts[2] === "packages" && parts[4] === "disable" && m === "PATCH") {
5004
- const envId = decodeURIComponent(parts[1]);
5005
- const pkgId = decodeURIComponent(parts[3]);
5006
- const record = await findInstallByManifestId(envId, pkgId);
5007
- if (!record) return { handled: true, response: this.error(`Package '${pkgId}' is not installed in this project`, 404) };
5008
- const allPkgs1 = this.kernel.packages?.getAll?.() ?? [];
5009
- const m1 = allPkgs1.find((p) => (p.manifest?.id ?? p.id) === pkgId)?.manifest;
5010
- if (m1?.scope && ["cloud", "system", "platform"].includes(m1.scope)) {
5011
- return { handled: true, response: this.error(`Package '${pkgId}' with scope=${m1.scope} cannot be disabled`, 403) };
5012
- }
5013
- const nowIso = (/* @__PURE__ */ new Date()).toISOString();
5014
- await ql.update(PKG_INSTALL, { enabled: false, status: "disabled", updated_at: nowIso }, { where: { id: record.id } });
5015
- const updated = await ql.findOne(PKG_INSTALL, { where: { id: record.id } });
5016
- try {
5017
- await this.kernelManager?.evict(envId);
5018
- } catch {
5019
- }
5020
- return { handled: true, response: this.success({ package: updated }) };
5021
- }
5022
- if (parts.length === 5 && parts[0] === "projects" && parts[2] === "packages" && parts[4] === "upgrade" && m === "POST") {
5023
- const envId = decodeURIComponent(parts[1]);
5024
- const pkgId = decodeURIComponent(parts[3]);
5025
- const record = await findInstallByManifestId(envId, pkgId);
5026
- if (!record) return { handled: true, response: this.error(`Package '${pkgId}' is not installed in this project`, 404) };
5027
- const { targetVersion } = body ?? {};
5028
- const allPkgs2 = this.kernel.packages?.getAll?.() ?? [];
5029
- const manifest2 = allPkgs2.find((p) => (p.manifest?.id ?? p.id) === pkgId)?.manifest;
5030
- const currentVer = await ql.findOne(PKG_VERSION, { where: { id: record.package_version_id } });
5031
- const newVersion = targetVersion ?? manifest2?.version ?? currentVer?.version ?? "1.0.0";
5032
- if (newVersion === currentVer?.version) {
5033
- return { handled: true, response: this.success({ package: record, message: "Already at target version" }) };
5034
- }
5035
- let userId = "system";
5036
- try {
5037
- const authService = await this.getService(import_system.CoreServiceName.enum.auth);
5038
- const sessionData = await authService?.api?.getSession?.({
5039
- headers: _context?.request?.headers
5040
- });
5041
- userId = sessionData?.user?.id ?? "system";
5042
- } catch {
5043
- }
5044
- const newVersionId = await ensureSysPackageVersion(record.package_id, newVersion, userId, manifest2);
5045
- const nowIso = (/* @__PURE__ */ new Date()).toISOString();
5046
- await ql.update(PKG_INSTALL, {
5047
- package_version_id: newVersionId,
5048
- status: "installed",
5049
- updated_at: nowIso
5050
- }, { where: { id: record.id } });
5051
- const updated = await ql.findOne(PKG_INSTALL, { where: { id: record.id } });
5052
- try {
5053
- await this.kernelManager?.evict(envId);
5054
- } catch {
5055
- }
5056
- return { handled: true, response: this.success({ package: updated }) };
5057
- }
5058
- } catch (e) {
5059
- return { handled: true, response: this.error(e.message, e.statusCode || 500) };
5060
- }
5061
- return { handled: false };
5062
- }
5063
- /**
5064
- * Cascade-delete a project: cred / member / package_installation rows,
5065
- * then the physical database via the provisioning adapter, then the
5066
- * `sys_environment` row itself. Used by both `DELETE /cloud/environments/:id`
5067
- * and the org-cascade in `DELETE /cloud/organizations/:id`.
5068
- *
5069
- * Idempotent and best-effort: missing rows / unreachable adapters
5070
- * become warnings rather than hard failures, so a half-provisioned
5071
- * project can still be cleaned out.
4038
+ * Driver binding
4039
+ * --------------
4040
+ * Environments are not tied to any specific driver. At provisioning time the
4041
+ * caller passes `driver` (a short name such as `memory`, `turso`, or any
4042
+ * future `sql` / `postgres` driver). The dispatcher validates the name
4043
+ * against the kernel's registered driver services (`driver.<name>`) and
4044
+ * derives an appropriate placeholder `database_url` for the chosen driver.
4045
+ * If `driver` is omitted, the dispatcher auto-selects the first available
4046
+ * in preference order: turso → memory → any other registered driver.
4047
+ *
4048
+ * Backed by ObjectQL sys_environment / sys_environment_credential /
4049
+ * sys_environment_member tables (registered by
4050
+ * `@objectstack/service-tenant`'s `createTenantPlugin`).
4051
+ * Physical database addressing (database_url, database_driver, etc.)
4052
+ * is stored directly on the sys_environment row.
5072
4053
  */
5073
- async deleteProjectCascade(environmentId, deps) {
5074
- const { ql, findOne, getRealAdapter, force } = deps;
5075
- const ENV = "sys_environment";
5076
- const warnings = [];
5077
- const row = await findOne(ENV, { id: environmentId });
5078
- if (!row) {
5079
- return { ok: false, status: 404, error: `Project '${environmentId}' not found`, warnings };
5080
- }
5081
- if (row.is_system === true || row.is_system === 1) {
5082
- return { ok: false, status: 409, error: `Project '${environmentId}' is a system project and cannot be deleted`, warnings };
5083
- }
5084
- if ((row.is_default === true || row.is_default === 1) && !force) {
5085
- return {
5086
- ok: false,
5087
- status: 409,
5088
- error: `Project '${environmentId}' is the default project for its organization. Pass ?force=1 to delete it.`,
5089
- warnings
5090
- };
5091
- }
5092
- const cascade = [
5093
- { object: "sys_environment_credential", field: "environment_id" },
5094
- { object: "sys_environment_member", field: "environment_id" },
5095
- { object: "sys_package_installation", field: "environment_id" }
5096
- ];
5097
- for (const { object, field } of cascade) {
5098
- try {
5099
- let rows = await ql.find(object, { where: { [field]: environmentId } });
5100
- if (rows && rows.value) rows = rows.value;
5101
- if (Array.isArray(rows)) {
5102
- for (const r of rows) {
5103
- if (r?.id != null) {
5104
- try {
5105
- await ql.delete(object, { where: { id: r.id } });
5106
- } catch (innerErr) {
5107
- warnings.push(
5108
- `Failed to delete ${object} ${r.id}: ${innerErr instanceof Error ? innerErr.message : String(innerErr)}`
5109
- );
5110
- }
5111
- }
4054
+ /**
4055
+ * Resolve the calling user id from the request session, if any.
4056
+ * Returns `undefined` for anonymous calls or when auth is not wired up.
4057
+ */
4058
+ async resolveActiveOrganizationId(context) {
4059
+ try {
4060
+ const authService = await this.resolveService(import_system2.CoreServiceName.enum.auth);
4061
+ const rawHeaders = context.request?.headers;
4062
+ let headers = rawHeaders;
4063
+ if (rawHeaders && typeof rawHeaders === "object" && typeof rawHeaders.get !== "function") {
4064
+ try {
4065
+ const h = new Headers();
4066
+ for (const [k, v] of Object.entries(rawHeaders)) {
4067
+ if (v == null) continue;
4068
+ h.set(k, Array.isArray(v) ? v.join(", ") : String(v));
5112
4069
  }
4070
+ headers = h;
4071
+ } catch {
4072
+ headers = rawHeaders;
5113
4073
  }
5114
- } catch (err) {
5115
- warnings.push(
5116
- `Failed to enumerate ${object} for project ${environmentId}: ${err instanceof Error ? err.message : String(err)}`
5117
- );
5118
- }
5119
- }
5120
- const driver = row.database_driver ?? "memory";
5121
- const databaseUrl = row.database_url;
5122
- const databaseName = `p-${String(environmentId).replace(/-/g, "").slice(0, 24)}`;
5123
- try {
5124
- const adapter = await getRealAdapter(driver);
5125
- if (adapter?.deleteDatabase) {
5126
- await adapter.deleteDatabase({ environmentId, databaseName, databaseUrl });
5127
- } else {
5128
- warnings.push(`No adapter for driver '${driver}'; physical DB for project ${environmentId} not released.`);
5129
- }
5130
- } catch (err) {
5131
- warnings.push(
5132
- `Failed to delete physical database for project ${environmentId}: ${err instanceof Error ? err.message : String(err)}`
5133
- );
5134
- }
5135
- try {
5136
- await ql.delete(ENV, { where: { id: environmentId } });
5137
- } catch (err) {
5138
- return {
5139
- ok: false,
5140
- status: 500,
5141
- error: `Failed to delete sys_environment row: ${err instanceof Error ? err.message : String(err)}`,
5142
- warnings
5143
- };
5144
- }
5145
- if (this.envRegistry?.invalidate) {
5146
- try {
5147
- await this.envRegistry.invalidate(environmentId);
5148
- } catch {
5149
4074
  }
4075
+ const apiObj = authService?.auth?.api ?? authService?.api;
4076
+ const sessionData = await apiObj?.getSession?.call(apiObj, { headers });
4077
+ const oid = sessionData?.session?.activeOrganizationId;
4078
+ return typeof oid === "string" && oid.length > 0 ? oid : void 0;
4079
+ } catch {
4080
+ return void 0;
5150
4081
  }
5151
- return { ok: true, warnings };
5152
4082
  }
5153
4083
  /**
5154
4084
  * Handles Storage requests
5155
4085
  * path: sub-path after /storage/
5156
4086
  */
5157
4087
  async handleStorage(path, method, file, context) {
5158
- const storageService = await this.getService(import_system.CoreServiceName.enum["file-storage"]) || this.kernel.services?.["file-storage"];
4088
+ const storageService = await this.getService(import_system2.CoreServiceName.enum["file-storage"]) || this.kernel.services?.["file-storage"];
5159
4089
  if (!storageService) {
5160
4090
  return { handled: true, response: this.error("File storage not configured", 501) };
5161
4091
  }
@@ -5220,6 +4150,8 @@ var _HttpDispatcher = class _HttpDispatcher {
5220
4150
  *
5221
4151
  * Routes:
5222
4152
  * GET / → listFlows
4153
+ * GET /actions → getActionDescriptors (ADR-0018; ?paradigm/?source/?category filters)
4154
+ * GET /connectors → getConnectorDescriptors (ADR-0022; ?type filter)
5223
4155
  * GET /:name → getFlow
5224
4156
  * POST / → createFlow (registerFlow)
5225
4157
  * PUT /:name → updateFlow
@@ -5228,9 +4160,11 @@ var _HttpDispatcher = class _HttpDispatcher {
5228
4160
  * POST /:name/toggle → toggleFlow
5229
4161
  * GET /:name/runs → listRuns
5230
4162
  * GET /:name/runs/:runId → getRun
4163
+ * POST /:name/runs/:runId/resume → resume a paused run (screen input / ADR-0019)
4164
+ * GET /:name/runs/:runId/screen → the screen a paused run awaits
5231
4165
  */
5232
4166
  async handleAutomation(path, method, body, context, query) {
5233
- const automationService = await this.getService(import_system.CoreServiceName.enum.automation);
4167
+ const automationService = await this.getService(import_system2.CoreServiceName.enum.automation);
5234
4168
  if (!automationService) return { handled: false };
5235
4169
  const m = method.toUpperCase();
5236
4170
  const parts = path.replace(/^\/+/, "").split("/").filter(Boolean);
@@ -5257,6 +4191,32 @@ var _HttpDispatcher = class _HttpDispatcher {
5257
4191
  return { handled: true, response: this.success(body) };
5258
4192
  }
5259
4193
  }
4194
+ if (parts[0] === "actions" && parts.length === 1 && m === "GET") {
4195
+ if (typeof automationService.getActionDescriptors === "function") {
4196
+ let actions = automationService.getActionDescriptors() ?? [];
4197
+ if (query?.paradigm) {
4198
+ actions = actions.filter((a) => Array.isArray(a?.paradigms) && a.paradigms.includes(query.paradigm));
4199
+ }
4200
+ if (query?.source) {
4201
+ actions = actions.filter((a) => a?.source === query.source);
4202
+ }
4203
+ if (query?.category) {
4204
+ actions = actions.filter((a) => a?.category === query.category);
4205
+ }
4206
+ return { handled: true, response: this.success({ actions, total: actions.length }) };
4207
+ }
4208
+ return { handled: true, response: this.success({ actions: [], total: 0 }) };
4209
+ }
4210
+ if (parts[0] === "connectors" && parts.length === 1 && m === "GET") {
4211
+ if (typeof automationService.getConnectorDescriptors === "function") {
4212
+ let connectors = automationService.getConnectorDescriptors() ?? [];
4213
+ if (query?.type) {
4214
+ connectors = connectors.filter((c) => c?.type === query.type);
4215
+ }
4216
+ return { handled: true, response: this.success({ connectors, total: connectors.length }) };
4217
+ }
4218
+ return { handled: true, response: this.success({ connectors: [], total: 0 }) };
4219
+ }
5260
4220
  if (parts.length >= 1) {
5261
4221
  const name = parts[0];
5262
4222
  if (parts[1] === "trigger" && m === "POST") {
@@ -5296,7 +4256,28 @@ var _HttpDispatcher = class _HttpDispatcher {
5296
4256
  return { handled: true, response: this.success({ name, enabled: body?.enabled ?? true }) };
5297
4257
  }
5298
4258
  }
5299
- if (parts[1] === "runs" && parts[2] && m === "GET") {
4259
+ if (parts[1] === "runs" && parts[2] && parts[3] === "resume" && m === "POST") {
4260
+ if (typeof automationService.resume === "function") {
4261
+ const b = body && typeof body === "object" ? body : {};
4262
+ const inputs = b.inputs ?? b.variables;
4263
+ const signal = {};
4264
+ if (inputs && typeof inputs === "object") signal.variables = inputs;
4265
+ if (b.output && typeof b.output === "object") signal.output = b.output;
4266
+ if (typeof b.branchLabel === "string") signal.branchLabel = b.branchLabel;
4267
+ const result = await automationService.resume(parts[2], signal);
4268
+ return { handled: true, response: this.success(result) };
4269
+ }
4270
+ return { handled: true, response: this.error("Resume not supported", 501) };
4271
+ }
4272
+ if (parts[1] === "runs" && parts[2] && parts[3] === "screen" && m === "GET") {
4273
+ if (typeof automationService.getSuspendedScreen === "function") {
4274
+ const screen = automationService.getSuspendedScreen(parts[2]);
4275
+ if (!screen) return { handled: true, response: this.error("No pending screen for run", 404) };
4276
+ return { handled: true, response: this.success({ runId: parts[2], screen }) };
4277
+ }
4278
+ return { handled: true, response: this.error("Screen lookup not supported", 501) };
4279
+ }
4280
+ if (parts[1] === "runs" && parts[2] && !parts[3] && m === "GET") {
5300
4281
  if (typeof automationService.getRun === "function") {
5301
4282
  const run = await automationService.getRun(parts[2]);
5302
4283
  if (!run) return { handled: true, response: this.error("Execution not found", 404) };
@@ -5622,11 +4603,9 @@ var _HttpDispatcher = class _HttpDispatcher {
5622
4603
  if (forbidden) {
5623
4604
  return { handled: true, response: forbidden };
5624
4605
  }
5625
- if (!cleanPath.startsWith("/cloud/")) {
5626
- const scopedMatch = cleanPath.match(/^\/projects\/[^/]+(\/.*)?$/);
5627
- if (scopedMatch) {
5628
- cleanPath = scopedMatch[1] ?? "";
5629
- }
4606
+ const scopedMatch = cleanPath.match(/^\/projects\/[^/]+(\/.*)?$/);
4607
+ if (scopedMatch) {
4608
+ cleanPath = scopedMatch[1] ?? "";
5630
4609
  }
5631
4610
  try {
5632
4611
  if ((cleanPath === "/discovery" || cleanPath === "") && method === "GET") {
@@ -5677,9 +4656,6 @@ var _HttpDispatcher = class _HttpDispatcher {
5677
4656
  if (cleanPath.startsWith("/packages")) {
5678
4657
  return this.handlePackages(cleanPath.substring(9), method, body, query, context);
5679
4658
  }
5680
- if (cleanPath.startsWith("/cloud")) {
5681
- return this.handleCloud(cleanPath.substring(6), method, body, query, context);
5682
- }
5683
4659
  if (cleanPath.startsWith("/i18n")) {
5684
4660
  return this.handleI18n(cleanPath.substring(5), method, query, context);
5685
4661
  }
@@ -6374,6 +5350,14 @@ function createDispatcherPlugin(config = {}) {
6374
5350
  errorResponse(err, res);
6375
5351
  }
6376
5352
  });
5353
+ server.get(`${prefix}/packages/:id/export`, async (req, res) => {
5354
+ try {
5355
+ const result = await dispatcher.handlePackages(`/${req.params.id}/export`, "GET", {}, req.query, { request: req });
5356
+ sendResult(result, res);
5357
+ } catch (err) {
5358
+ errorResponse(err, res);
5359
+ }
5360
+ });
6377
5361
  server.get(`${prefix}/packages/:id`, async (req, res) => {
6378
5362
  try {
6379
5363
  const result = await dispatcher.handlePackages(`/${req.params.id}`, "GET", {}, req.query, { request: req });
@@ -6422,214 +5406,6 @@ function createDispatcherPlugin(config = {}) {
6422
5406
  errorResponse(err, res);
6423
5407
  }
6424
5408
  });
6425
- server.get(`${prefix}/cloud/drivers`, async (req, res) => {
6426
- try {
6427
- const result = await dispatcher.handleCloud("/drivers", "GET", {}, req.query, { request: req });
6428
- sendResult(result, res);
6429
- } catch (err) {
6430
- errorResponse(err, res);
6431
- }
6432
- });
6433
- server.post(`${prefix}/cloud/admin/platform-sso/backfill`, async (req, res) => {
6434
- try {
6435
- const result = await dispatcher.handleCloud("/admin/platform-sso/backfill", "POST", req.body, req.query, { request: req });
6436
- sendResult(result, res);
6437
- } catch (err) {
6438
- errorResponse(err, res);
6439
- }
6440
- });
6441
- server.get(`${prefix}/cloud/templates`, async (req, res) => {
6442
- try {
6443
- const result = await dispatcher.handleCloud("/templates", "GET", {}, req.query, { request: req });
6444
- sendResult(result, res);
6445
- } catch (err) {
6446
- errorResponse(err, res);
6447
- }
6448
- });
6449
- server.get(`${prefix}/cloud/environments`, async (req, res) => {
6450
- try {
6451
- const result = await dispatcher.handleCloud("/projects", "GET", {}, req.query, { request: req });
6452
- sendResult(result, res);
6453
- } catch (err) {
6454
- errorResponse(err, res);
6455
- }
6456
- });
6457
- server.post(`${prefix}/cloud/environments`, async (req, res) => {
6458
- try {
6459
- const result = await dispatcher.handleCloud("/projects", "POST", req.body, {}, { request: req });
6460
- sendResult(result, res);
6461
- } catch (err) {
6462
- errorResponse(err, res);
6463
- }
6464
- });
6465
- server.get(`${prefix}/cloud/environments/:id`, async (req, res) => {
6466
- try {
6467
- const result = await dispatcher.handleCloud(`/projects/${req.params.id}`, "GET", {}, req.query, { request: req });
6468
- sendResult(result, res);
6469
- } catch (err) {
6470
- errorResponse(err, res);
6471
- }
6472
- });
6473
- server.patch(`${prefix}/cloud/environments/:id`, async (req, res) => {
6474
- try {
6475
- const result = await dispatcher.handleCloud(`/projects/${req.params.id}`, "PATCH", req.body, {}, { request: req });
6476
- sendResult(result, res);
6477
- } catch (err) {
6478
- errorResponse(err, res);
6479
- }
6480
- });
6481
- server.delete(`${prefix}/cloud/environments/:id`, async (req, res) => {
6482
- try {
6483
- const result = await dispatcher.handleCloud(`/projects/${req.params.id}`, "DELETE", {}, req.query, { request: req });
6484
- sendResult(result, res);
6485
- } catch (err) {
6486
- errorResponse(err, res);
6487
- }
6488
- });
6489
- server.delete(`${prefix}/cloud/organizations/:id`, async (req, res) => {
6490
- try {
6491
- const result = await dispatcher.handleCloud(`/organizations/${req.params.id}`, "DELETE", {}, req.query, { request: req });
6492
- sendResult(result, res);
6493
- } catch (err) {
6494
- errorResponse(err, res);
6495
- }
6496
- });
6497
- server.post(`${prefix}/cloud/environments/:id/hostname`, async (req, res) => {
6498
- try {
6499
- const result = await dispatcher.handleCloud(`/projects/${req.params.id}/hostname`, "POST", req.body, {}, { request: req });
6500
- sendResult(result, res);
6501
- } catch (err) {
6502
- errorResponse(err, res);
6503
- }
6504
- });
6505
- server.put(`${prefix}/cloud/environments/:id/hostname`, async (req, res) => {
6506
- try {
6507
- const result = await dispatcher.handleCloud(`/projects/${req.params.id}/hostname`, "PUT", req.body, {}, { request: req });
6508
- sendResult(result, res);
6509
- } catch (err) {
6510
- errorResponse(err, res);
6511
- }
6512
- });
6513
- server.post(`${prefix}/cloud/environments/:id/rotate-credential`, async (req, res) => {
6514
- try {
6515
- const result = await dispatcher.handleCloud(`/projects/${req.params.id}/rotate-credential`, "POST", req.body, {}, { request: req });
6516
- sendResult(result, res);
6517
- } catch (err) {
6518
- errorResponse(err, res);
6519
- }
6520
- });
6521
- server.post(`${prefix}/cloud/environments/:id/credentials/rotate`, async (req, res) => {
6522
- try {
6523
- const result = await dispatcher.handleCloud(`/projects/${req.params.id}/credentials/rotate`, "POST", req.body, {}, { request: req });
6524
- sendResult(result, res);
6525
- } catch (err) {
6526
- errorResponse(err, res);
6527
- }
6528
- });
6529
- server.post(`${prefix}/cloud/environments/:id/activate`, async (req, res) => {
6530
- try {
6531
- const result = await dispatcher.handleCloud(`/projects/${req.params.id}/activate`, "POST", req.body, {}, { request: req });
6532
- sendResult(result, res);
6533
- } catch (err) {
6534
- errorResponse(err, res);
6535
- }
6536
- });
6537
- server.post(`${prefix}/cloud/environments/:id/retry`, async (req, res) => {
6538
- try {
6539
- const result = await dispatcher.handleCloud(`/projects/${req.params.id}/retry`, "POST", req.body, {}, { request: req });
6540
- sendResult(result, res);
6541
- } catch (err) {
6542
- errorResponse(err, res);
6543
- }
6544
- });
6545
- server.get(`${prefix}/cloud/environments/:id/members`, async (req, res) => {
6546
- try {
6547
- const result = await dispatcher.handleCloud(`/projects/${req.params.id}/members`, "GET", {}, req.query, { request: req });
6548
- sendResult(result, res);
6549
- } catch (err) {
6550
- errorResponse(err, res);
6551
- }
6552
- });
6553
- server.post(`${prefix}/cloud/environments/:id/members`, async (req, res) => {
6554
- try {
6555
- const result = await dispatcher.handleCloud(`/projects/${req.params.id}/members`, "POST", req.body, {}, { request: req });
6556
- sendResult(result, res);
6557
- } catch (err) {
6558
- errorResponse(err, res);
6559
- }
6560
- });
6561
- server.patch(`${prefix}/cloud/environments/:id/members/:memberId`, async (req, res) => {
6562
- try {
6563
- const result = await dispatcher.handleCloud(`/projects/${req.params.id}/members/${req.params.memberId}`, "PATCH", req.body, {}, { request: req });
6564
- sendResult(result, res);
6565
- } catch (err) {
6566
- errorResponse(err, res);
6567
- }
6568
- });
6569
- server.delete(`${prefix}/cloud/environments/:id/members/:memberId`, async (req, res) => {
6570
- try {
6571
- const result = await dispatcher.handleCloud(`/projects/${req.params.id}/members/${req.params.memberId}`, "DELETE", req.body ?? {}, {}, { request: req });
6572
- sendResult(result, res);
6573
- } catch (err) {
6574
- errorResponse(err, res);
6575
- }
6576
- });
6577
- server.get(`${prefix}/cloud/environments/:id/packages`, async (req, res) => {
6578
- try {
6579
- const result = await dispatcher.handleCloud(`/projects/${req.params.id}/packages`, "GET", {}, req.query, { request: req });
6580
- sendResult(result, res);
6581
- } catch (err) {
6582
- errorResponse(err, res);
6583
- }
6584
- });
6585
- server.post(`${prefix}/cloud/environments/:id/packages`, async (req, res) => {
6586
- try {
6587
- const result = await dispatcher.handleCloud(`/projects/${req.params.id}/packages`, "POST", req.body, {}, { request: req });
6588
- sendResult(result, res);
6589
- } catch (err) {
6590
- errorResponse(err, res);
6591
- }
6592
- });
6593
- server.get(`${prefix}/cloud/environments/:id/packages/:pkgId`, async (req, res) => {
6594
- try {
6595
- const result = await dispatcher.handleCloud(`/projects/${req.params.id}/packages/${req.params.pkgId}`, "GET", {}, req.query, { request: req });
6596
- sendResult(result, res);
6597
- } catch (err) {
6598
- errorResponse(err, res);
6599
- }
6600
- });
6601
- server.delete(`${prefix}/cloud/environments/:id/packages/:pkgId`, async (req, res) => {
6602
- try {
6603
- const result = await dispatcher.handleCloud(`/projects/${req.params.id}/packages/${req.params.pkgId}`, "DELETE", {}, {}, { request: req });
6604
- sendResult(result, res);
6605
- } catch (err) {
6606
- errorResponse(err, res);
6607
- }
6608
- });
6609
- server.patch(`${prefix}/cloud/environments/:id/packages/:pkgId/enable`, async (req, res) => {
6610
- try {
6611
- const result = await dispatcher.handleCloud(`/projects/${req.params.id}/packages/${req.params.pkgId}/enable`, "PATCH", {}, {}, { request: req });
6612
- sendResult(result, res);
6613
- } catch (err) {
6614
- errorResponse(err, res);
6615
- }
6616
- });
6617
- server.patch(`${prefix}/cloud/environments/:id/packages/:pkgId/disable`, async (req, res) => {
6618
- try {
6619
- const result = await dispatcher.handleCloud(`/projects/${req.params.id}/packages/${req.params.pkgId}/disable`, "PATCH", {}, {}, { request: req });
6620
- sendResult(result, res);
6621
- } catch (err) {
6622
- errorResponse(err, res);
6623
- }
6624
- });
6625
- server.post(`${prefix}/cloud/environments/:id/packages/:pkgId/upgrade`, async (req, res) => {
6626
- try {
6627
- const result = await dispatcher.handleCloud(`/projects/${req.params.id}/packages/${req.params.pkgId}/upgrade`, "POST", req.body, {}, { request: req });
6628
- sendResult(result, res);
6629
- } catch (err) {
6630
- errorResponse(err, res);
6631
- }
6632
- });
6633
5409
  server.post(`${prefix}/storage/upload`, async (req, res) => {
6634
5410
  try {
6635
5411
  const result = await dispatcher.handleStorage("upload", "POST", req.body, { request: req });
@@ -6721,31 +5497,47 @@ function createDispatcherPlugin(config = {}) {
6721
5497
  });
6722
5498
  server.post(`${base}/automation/:name/trigger`, async (req, res) => {
6723
5499
  try {
6724
- const result = await dispatcher.dispatch("POST", `/automation/${req.params.name}/trigger`, req.body, req.query, { request: req });
5500
+ const result = await dispatcher.dispatch("POST", `/automation/${req.params.name}/trigger`, req.body, req.query, { request: req });
5501
+ sendResult(result, res);
5502
+ } catch (err) {
5503
+ errorResponse(err, res);
5504
+ }
5505
+ });
5506
+ server.post(`${base}/automation/:name/toggle`, async (req, res) => {
5507
+ try {
5508
+ const result = await dispatcher.dispatch("POST", `/automation/${req.params.name}/toggle`, req.body, req.query, { request: req });
5509
+ sendResult(result, res);
5510
+ } catch (err) {
5511
+ errorResponse(err, res);
5512
+ }
5513
+ });
5514
+ server.get(`${base}/automation/:name/runs`, async (req, res) => {
5515
+ try {
5516
+ const result = await dispatcher.dispatch("GET", `/automation/${req.params.name}/runs`, void 0, req.query, { request: req });
6725
5517
  sendResult(result, res);
6726
5518
  } catch (err) {
6727
5519
  errorResponse(err, res);
6728
5520
  }
6729
5521
  });
6730
- server.post(`${base}/automation/:name/toggle`, async (req, res) => {
5522
+ server.get(`${base}/automation/:name/runs/:runId`, async (req, res) => {
6731
5523
  try {
6732
- const result = await dispatcher.dispatch("POST", `/automation/${req.params.name}/toggle`, req.body, req.query, { request: req });
5524
+ const result = await dispatcher.dispatch("GET", `/automation/${req.params.name}/runs/${req.params.runId}`, void 0, req.query, { request: req });
6733
5525
  sendResult(result, res);
6734
5526
  } catch (err) {
6735
5527
  errorResponse(err, res);
6736
5528
  }
6737
5529
  });
6738
- server.get(`${base}/automation/:name/runs`, async (req, res) => {
5530
+ server.post(`${base}/automation/:name/runs/:runId/resume`, async (req, res) => {
6739
5531
  try {
6740
- const result = await dispatcher.dispatch("GET", `/automation/${req.params.name}/runs`, void 0, req.query, { request: req });
5532
+ const result = await dispatcher.dispatch("POST", `/automation/${req.params.name}/runs/${req.params.runId}/resume`, req.body, req.query, { request: req });
6741
5533
  sendResult(result, res);
6742
5534
  } catch (err) {
6743
5535
  errorResponse(err, res);
6744
5536
  }
6745
5537
  });
6746
- server.get(`${base}/automation/:name/runs/:runId`, async (req, res) => {
5538
+ server.get(`${base}/automation/:name/runs/:runId/screen`, async (req, res) => {
6747
5539
  try {
6748
- const result = await dispatcher.dispatch("GET", `/automation/${req.params.name}/runs/${req.params.runId}`, void 0, req.query, { request: req });
5540
+ const result = await dispatcher.dispatch("GET", `/automation/${req.params.name}/runs/${req.params.runId}/screen`, void 0, req.query, { request: req });
6749
5541
  sendResult(result, res);
6750
5542
  } catch (err) {
6751
5543
  errorResponse(err, res);
@@ -7481,7 +6273,7 @@ var ArtifactApiClient = class {
7481
6273
  };
7482
6274
 
7483
6275
  // src/cloud/artifact-environment-registry.ts
7484
- var import_node_path4 = require("path");
6276
+ var import_node_path5 = require("path");
7485
6277
  var ArtifactEnvironmentRegistry = class {
7486
6278
  constructor(config) {
7487
6279
  this.hostnameCache = /* @__PURE__ */ new Map();
@@ -7653,7 +6445,7 @@ async function createDriver(driverType, databaseUrl, authToken) {
7653
6445
  case "memory": {
7654
6446
  const { InMemoryDriver } = await import("@objectstack/driver-memory");
7655
6447
  const dbName = databaseUrl.replace(/^memory:\/\//, "").trim();
7656
- const filePath = dbName ? (0, import_node_path4.resolve)(process.cwd(), ".objectstack/data/projects", `${dbName}.json`) : void 0;
6448
+ const filePath = dbName ? (0, import_node_path5.resolve)(process.cwd(), ".objectstack/data/projects", `${dbName}.json`) : void 0;
7657
6449
  return new InMemoryDriver({
7658
6450
  persistence: filePath ? { type: "file", path: filePath } : "file"
7659
6451
  });
@@ -7691,21 +6483,17 @@ async function createDriver(driverType, databaseUrl, authToken) {
7691
6483
  // src/cloud/artifact-kernel-factory.ts
7692
6484
  var import_node_crypto2 = require("crypto");
7693
6485
  var import_core3 = require("@objectstack/core");
7694
- var import_types4 = require("@objectstack/types");
6486
+ var import_types3 = require("@objectstack/types");
7695
6487
  init_driver_plugin();
7696
6488
  init_app_plugin();
7697
6489
 
7698
6490
  // src/cloud/capability-loader.ts
7699
6491
  var CAPABILITY_PROVIDERS = {
7700
6492
  automation: {
6493
+ // Self-contained: AutomationServicePlugin seeds all built-in node
6494
+ // executors itself (ADR-0018), so no companion node-pack plugins.
7701
6495
  pkg: "@objectstack/service-automation",
7702
- export: "AutomationServicePlugin",
7703
- extras: [
7704
- { pkg: "@objectstack/service-automation", export: "CrudNodesPlugin" },
7705
- { pkg: "@objectstack/service-automation", export: "LogicNodesPlugin" },
7706
- { pkg: "@objectstack/service-automation", export: "HttpConnectorPlugin" },
7707
- { pkg: "@objectstack/service-automation", export: "ScreenNodesPlugin" }
7708
- ]
6496
+ export: "AutomationServicePlugin"
7709
6497
  },
7710
6498
  ai: {
7711
6499
  pkg: "@objectstack/service-ai",
@@ -7736,6 +6524,19 @@ var CAPABILITY_PROVIDERS = {
7736
6524
  pkg: "@objectstack/service-job",
7737
6525
  export: "JobServicePlugin"
7738
6526
  },
6527
+ messaging: {
6528
+ // Backs the `notify` flow node (ADR-0012): delivers to a user's
6529
+ // channels (inbox by default → `sys_inbox_message` rows).
6530
+ pkg: "@objectstack/service-messaging",
6531
+ export: "MessagingServicePlugin"
6532
+ },
6533
+ triggers: {
6534
+ // Concrete flow triggers — record-change (ObjectQL hooks) + schedule
6535
+ // (cron/interval via the job service; pair `triggers` with `job`).
6536
+ pkg: "@objectstack/plugin-trigger-record-change",
6537
+ export: "RecordChangeTriggerPlugin",
6538
+ extras: [{ pkg: "@objectstack/plugin-trigger-schedule", export: "ScheduleTriggerPlugin" }]
6539
+ },
7739
6540
  realtime: {
7740
6541
  pkg: "@objectstack/service-realtime",
7741
6542
  export: "RealtimeServicePlugin"
@@ -7753,7 +6554,11 @@ async function loadCapabilities(opts) {
7753
6554
  const { kernel, requires, bundle, environmentId } = opts;
7754
6555
  const logger = opts.logger ?? console;
7755
6556
  const installed = [];
7756
- for (const cap of requires) {
6557
+ const resolved = [...new Set(requires)];
6558
+ if (resolved.includes("audit") && !resolved.includes("messaging")) {
6559
+ resolved.push("messaging");
6560
+ }
6561
+ for (const cap of resolved) {
7757
6562
  const spec = CAPABILITY_PROVIDERS[cap];
7758
6563
  if (!spec) {
7759
6564
  continue;
@@ -7820,8 +6625,197 @@ async function loadCapabilities(opts) {
7820
6625
  return installed;
7821
6626
  }
7822
6627
 
6628
+ // src/cloud/platform-sso.ts
6629
+ var import_node_crypto = require("crypto");
6630
+ var PLATFORM_SSO_PROVIDER_ID = "objectstack-cloud";
6631
+ function derivePlatformSsoClientId(environmentId) {
6632
+ return `project_${environmentId}`;
6633
+ }
6634
+ function derivePlatformSsoClientSecret(baseSecret, environmentId) {
6635
+ return (0, import_node_crypto.createHmac)("sha256", baseSecret).update(`oauth-client:${environmentId}`).digest("hex");
6636
+ }
6637
+ function hashPlatformSsoClientSecret(plaintext) {
6638
+ return (0, import_node_crypto.createHash)("sha256").update(plaintext).digest("base64").replace(/=+$/, "").replace(/\+/g, "-").replace(/\//g, "_");
6639
+ }
6640
+ function buildPlatformSsoRedirectUri(hostname, basePath = "/api/v1/auth") {
6641
+ let host;
6642
+ if (hostname.startsWith("http://") || hostname.startsWith("https://")) {
6643
+ host = hostname;
6644
+ } else if (/(\.|^)localhost(:\d+)?$/i.test(hostname)) {
6645
+ const port = (process.env.OS_RUNTIME_PORT ?? "").trim();
6646
+ const hostWithPort = /:\d+$/.test(hostname) || !port ? hostname : `${hostname}:${port}`;
6647
+ host = `http://${hostWithPort}`;
6648
+ } else {
6649
+ host = `https://${hostname}`;
6650
+ }
6651
+ const trimmed = host.replace(/\/+$/, "");
6652
+ const path = basePath.replace(/\/+$/, "");
6653
+ return `${trimmed}${path}/oauth2/callback/${PLATFORM_SSO_PROVIDER_ID}`;
6654
+ }
6655
+ async function seedPlatformSsoClient(opts) {
6656
+ const { ql, environmentId, hostname, baseSecret, logger, throwOnError } = opts;
6657
+ if (!baseSecret) {
6658
+ logger?.warn?.("[platform-sso] OS_AUTH_SECRET not set \u2014 skipping client seed", { environmentId });
6659
+ return;
6660
+ }
6661
+ const clientId = derivePlatformSsoClientId(environmentId);
6662
+ const clientSecretPlaintext = derivePlatformSsoClientSecret(baseSecret, environmentId);
6663
+ const clientSecretStored = hashPlatformSsoClientSecret(clientSecretPlaintext);
6664
+ const desiredRedirect = hostname ? buildPlatformSsoRedirectUri(hostname) : null;
6665
+ let existing = null;
6666
+ try {
6667
+ const rows = await ql.find("sys_oauth_application", {
6668
+ where: { client_id: clientId },
6669
+ limit: 1
6670
+ }, { context: { isSystem: true } });
6671
+ const list = Array.isArray(rows) ? rows : Array.isArray(rows?.records) ? rows.records : [];
6672
+ existing = list[0] ?? null;
6673
+ } catch (err) {
6674
+ logger?.warn?.("[platform-sso] sys_oauth_application read failed \u2014 skipping seed", {
6675
+ environmentId,
6676
+ error: err?.message
6677
+ });
6678
+ return;
6679
+ }
6680
+ const nowIso = (/* @__PURE__ */ new Date()).toISOString();
6681
+ if (!existing) {
6682
+ const redirects = desiredRedirect ? [desiredRedirect] : [];
6683
+ try {
6684
+ await ql.insert("sys_oauth_application", {
6685
+ id: `oauthc_${environmentId}`,
6686
+ name: `Project ${environmentId}`,
6687
+ client_id: clientId,
6688
+ client_secret: clientSecretStored,
6689
+ type: "web",
6690
+ redirect_uris: JSON.stringify(redirects),
6691
+ grant_types: JSON.stringify(["authorization_code", "refresh_token"]),
6692
+ response_types: JSON.stringify(["code"]),
6693
+ scopes: JSON.stringify(["openid", "email", "profile"]),
6694
+ token_endpoint_auth_method: "client_secret_basic",
6695
+ require_pkce: false,
6696
+ skip_consent: true,
6697
+ disabled: false,
6698
+ subject_type: "public",
6699
+ created_at: nowIso,
6700
+ updated_at: nowIso
6701
+ }, { context: { isSystem: true } });
6702
+ logger?.info?.("[platform-sso] sys_oauth_application row created", { environmentId, clientId });
6703
+ } catch (err) {
6704
+ logger?.warn?.("[platform-sso] sys_oauth_application create failed", {
6705
+ environmentId,
6706
+ error: err?.message
6707
+ });
6708
+ if (throwOnError) throw err;
6709
+ }
6710
+ return;
6711
+ }
6712
+ let currentRedirects = [];
6713
+ try {
6714
+ const raw = existing.redirect_uris;
6715
+ const parsed = typeof raw === "string" ? JSON.parse(raw) : raw;
6716
+ if (Array.isArray(parsed)) currentRedirects = parsed.filter((s) => typeof s === "string");
6717
+ } catch {
6718
+ }
6719
+ const mergedRedirects = desiredRedirect && !currentRedirects.includes(desiredRedirect) ? [...currentRedirects, desiredRedirect] : currentRedirects;
6720
+ const repairPatch = {
6721
+ name: existing.name || `Project ${environmentId}`,
6722
+ client_secret: clientSecretStored,
6723
+ type: existing.type || "web",
6724
+ redirect_uris: JSON.stringify(mergedRedirects),
6725
+ grant_types: JSON.stringify(["authorization_code", "refresh_token"]),
6726
+ response_types: JSON.stringify(["code"]),
6727
+ scopes: JSON.stringify(["openid", "email", "profile"]),
6728
+ token_endpoint_auth_method: "client_secret_basic",
6729
+ require_pkce: false,
6730
+ skip_consent: true,
6731
+ disabled: false,
6732
+ subject_type: "public",
6733
+ updated_at: nowIso
6734
+ };
6735
+ try {
6736
+ await ql.update(
6737
+ "sys_oauth_application",
6738
+ repairPatch,
6739
+ { where: { id: existing.id } },
6740
+ { context: { isSystem: true } }
6741
+ );
6742
+ logger?.info?.("[platform-sso] sys_oauth_application repaired", {
6743
+ environmentId,
6744
+ clientId,
6745
+ redirect_uris: mergedRedirects
6746
+ });
6747
+ } catch (err) {
6748
+ logger?.warn?.("[platform-sso] sys_oauth_application repair failed", {
6749
+ environmentId,
6750
+ error: err?.message
6751
+ });
6752
+ if (throwOnError) throw err;
6753
+ }
6754
+ }
6755
+ async function backfillPlatformSsoClients(opts) {
6756
+ const { ql, baseSecret, logger, limit = 1e3 } = opts;
6757
+ if (!baseSecret) {
6758
+ logger?.warn?.("[platform-sso] backfill skipped \u2014 OS_AUTH_SECRET not set");
6759
+ return { scanned: 0, seeded: 0, alreadyExisted: 0, failures: [] };
6760
+ }
6761
+ let projects = [];
6762
+ try {
6763
+ const rows = await ql.find("sys_environment", {
6764
+ limit,
6765
+ fields: ["id", "hostname", "status"]
6766
+ }, { context: { isSystem: true } });
6767
+ projects = Array.isArray(rows) ? rows : Array.isArray(rows?.records) ? rows.records : [];
6768
+ } catch (err) {
6769
+ logger?.warn?.("[platform-sso] backfill: sys_environment read failed", {
6770
+ error: err?.message
6771
+ });
6772
+ return { scanned: 0, seeded: 0, alreadyExisted: 0, failures: [{ environmentId: "<scan>", error: err?.message ?? String(err) }] };
6773
+ }
6774
+ let seeded = 0;
6775
+ let alreadyExisted = 0;
6776
+ const failures = [];
6777
+ for (const p of projects) {
6778
+ if (!p?.id) continue;
6779
+ const before = await (async () => {
6780
+ try {
6781
+ const r = await ql.find("sys_oauth_application", {
6782
+ where: { client_id: derivePlatformSsoClientId(p.id) },
6783
+ limit: 1
6784
+ }, { context: { isSystem: true } });
6785
+ const list = Array.isArray(r) ? r : Array.isArray(r?.records) ? r.records : [];
6786
+ return list[0] ?? null;
6787
+ } catch {
6788
+ return null;
6789
+ }
6790
+ })();
6791
+ try {
6792
+ await seedPlatformSsoClient({ ql, environmentId: p.id, hostname: p.hostname, baseSecret, logger, throwOnError: true });
6793
+ if (before) alreadyExisted++;
6794
+ else {
6795
+ const after = await (async () => {
6796
+ try {
6797
+ const r = await ql.find("sys_oauth_application", {
6798
+ where: { client_id: derivePlatformSsoClientId(p.id) },
6799
+ limit: 1
6800
+ }, { context: { isSystem: true } });
6801
+ const list = Array.isArray(r) ? r : Array.isArray(r?.records) ? r.records : [];
6802
+ return list[0] ?? null;
6803
+ } catch (err) {
6804
+ return { _readErr: err?.message };
6805
+ }
6806
+ })();
6807
+ if (after && !after._readErr) seeded++;
6808
+ else failures.push({ environmentId: p.id, error: `post-insert read returned ${after ? JSON.stringify(after) : "null"}` });
6809
+ }
6810
+ } catch (err) {
6811
+ failures.push({ environmentId: p.id, error: err?.message ?? String(err) });
6812
+ }
6813
+ }
6814
+ logger?.info?.("[platform-sso] backfill complete", { scanned: projects.length, seeded, alreadyExisted, failures: failures.length });
6815
+ return { scanned: projects.length, seeded, alreadyExisted, failures };
6816
+ }
6817
+
7823
6818
  // src/cloud/artifact-kernel-factory.ts
7824
- init_platform_sso();
7825
6819
  function deriveProjectAuthSecret(baseSecret, environmentId) {
7826
6820
  return (0, import_node_crypto2.createHmac)("sha256", baseSecret).update(`project:${environmentId}`).digest("hex");
7827
6821
  }
@@ -7831,7 +6825,7 @@ var ArtifactKernelFactory = class {
7831
6825
  this.envRegistry = config.envRegistry;
7832
6826
  this.logger = config.logger ?? console;
7833
6827
  this.kernelConfig = config.kernelConfig;
7834
- this.authBaseSecret = (config.authBaseSecret ?? (0, import_types4.readEnvWithDeprecation)("OS_AUTH_SECRET", ["AUTH_SECRET", "BETTER_AUTH_SECRET"]) ?? "").trim();
6828
+ this.authBaseSecret = (config.authBaseSecret ?? (0, import_types3.readEnvWithDeprecation)("OS_AUTH_SECRET", ["AUTH_SECRET", "BETTER_AUTH_SECRET"]) ?? "").trim();
7835
6829
  }
7836
6830
  async create(environmentId) {
7837
6831
  let cached = this.envRegistry.peekById(environmentId);
@@ -7944,7 +6938,7 @@ var ArtifactKernelFactory = class {
7944
6938
  this.logger.warn?.("[ArtifactKernelFactory] OS_AUTH_SECRET not set \u2014 per-project AuthPlugin skipped (auth endpoints will return 404)", { environmentId });
7945
6939
  }
7946
6940
  try {
7947
- const multiTenant = String((0, import_types4.readEnvWithDeprecation)("OS_MULTI_ORG_ENABLED", "OS_MULTI_TENANT") ?? "false").toLowerCase() !== "false";
6941
+ const multiTenant = String((0, import_types3.readEnvWithDeprecation)("OS_MULTI_ORG_ENABLED", "OS_MULTI_TENANT") ?? "false").toLowerCase() !== "false";
7948
6942
  if (multiTenant) {
7949
6943
  try {
7950
6944
  const { OrgScopingPlugin } = await import("@objectstack/plugin-org-scoping");
@@ -8470,7 +7464,7 @@ var AuthProxyPlugin = class {
8470
7464
  };
8471
7465
 
8472
7466
  // src/cloud/cloud-url.ts
8473
- var DEFAULT_CLOUD_URL = "https://cloud.objectos.app";
7467
+ var DEFAULT_CLOUD_URL = "https://cloud.objectos.ai";
8474
7468
  function resolveCloudUrl(explicit) {
8475
7469
  const raw = (explicit ?? process.env.OS_CLOUD_URL ?? "").trim();
8476
7470
  const lower = raw.toLowerCase();
@@ -8892,11 +7886,11 @@ var RuntimeConfigPlugin = class {
8892
7886
 
8893
7887
  // src/cloud/file-artifact-api-client.ts
8894
7888
  var import_promises2 = require("fs/promises");
8895
- var import_node_path5 = require("path");
7889
+ var import_node_path6 = require("path");
8896
7890
  var FileArtifactApiClient = class {
8897
7891
  constructor(config = {}) {
8898
7892
  const cwd = process.cwd();
8899
- this.artifactPath = (0, import_node_path5.resolve)(
7893
+ this.artifactPath = (0, import_node_path6.resolve)(
8900
7894
  cwd,
8901
7895
  config.artifactPath ?? process.env.OS_ARTIFACT_PATH ?? "dist/objectstack.json"
8902
7896
  );
@@ -8992,7 +7986,7 @@ var FileArtifactApiClient = class {
8992
7986
  }
8993
7987
  defaultLocalSqliteRuntime() {
8994
7988
  const cwd = process.cwd();
8995
- const dbPath = (0, import_node_path5.resolve)(cwd, ".objectstack/data", `${this.environmentId}.db`);
7989
+ const dbPath = (0, import_node_path6.resolve)(cwd, ".objectstack/data", `${this.environmentId}.db`);
8996
7990
  return {
8997
7991
  databaseDriver: "sqlite",
8998
7992
  databaseUrl: `file:${dbPath}`
@@ -9145,9 +8139,9 @@ async function createObjectOSStack(config) {
9145
8139
  }
9146
8140
 
9147
8141
  // src/cloud/marketplace-install-local-plugin.ts
9148
- var import_node_fs3 = require("fs");
9149
- var import_node_path6 = require("path");
9150
- var import_types5 = require("@objectstack/types");
8142
+ var import_node_fs4 = require("fs");
8143
+ var import_node_path7 = require("path");
8144
+ var import_types4 = require("@objectstack/types");
9151
8145
  var ROUTE_BASE = "/api/v1/marketplace/install-local";
9152
8146
  var DEFAULT_DIR = ".objectstack/installed-packages";
9153
8147
  function safeFilename(manifestId) {
@@ -9222,9 +8216,6 @@ var MarketplaceInstallLocalPlugin = class {
9222
8216
  }
9223
8217
  };
9224
8218
  this.handleInstall = async (c, ctx) => {
9225
- if (!this.cloudUrl) {
9226
- return c.json({ success: false, error: { code: "marketplace_unavailable", message: "OS_CLOUD_URL not configured." } }, 503);
9227
- }
9228
8219
  const userId = await this.requireAuthenticatedUser(c, ctx);
9229
8220
  if (!userId) {
9230
8221
  return c.json({ success: false, error: { code: "unauthorized", message: "Authentication required to install packages." } }, 401);
@@ -9234,69 +8225,87 @@ var MarketplaceInstallLocalPlugin = class {
9234
8225
  body = await c.req.json();
9235
8226
  } catch {
9236
8227
  }
9237
- const packageId = String(body?.packageId ?? "").trim();
9238
- const versionId = String(body?.versionId ?? "latest").trim() || "latest";
9239
- if (!packageId) {
9240
- return c.json({ success: false, error: { code: "bad_request", message: "packageId is required." } }, 400);
9241
- }
9242
- let payload;
9243
- const publicBase = resolveMarketplacePublicBaseUrl();
9244
- const fetchAttempts = [];
9245
- if (publicBase) {
8228
+ const inlineManifest = body?.manifest && typeof body.manifest === "object" ? body.manifest : null;
8229
+ let manifest;
8230
+ let resolvedVersionId;
8231
+ let version;
8232
+ let packageId;
8233
+ if (inlineManifest) {
8234
+ manifest = inlineManifest;
8235
+ packageId = String(manifest.id ?? manifest.name ?? "").trim();
8236
+ version = String(manifest.version ?? "unknown");
8237
+ resolvedVersionId = String(body?.versionId ?? version);
8238
+ if (!packageId) {
8239
+ return c.json({ success: false, error: { code: "invalid_manifest", message: 'Inline manifest must have an "id" or "name".' } }, 400);
8240
+ }
8241
+ } else {
8242
+ if (!this.cloudUrl) {
8243
+ return c.json({ success: false, error: { code: "marketplace_unavailable", message: "OS_CLOUD_URL not configured." } }, 503);
8244
+ }
8245
+ packageId = String(body?.packageId ?? "").trim();
8246
+ const versionId = String(body?.versionId ?? "latest").trim() || "latest";
8247
+ if (!packageId) {
8248
+ return c.json({ success: false, error: { code: "bad_request", message: "packageId is required." } }, 400);
8249
+ }
8250
+ let payload;
8251
+ const publicBase = resolveMarketplacePublicBaseUrl();
8252
+ const fetchAttempts = [];
8253
+ if (publicBase) {
8254
+ fetchAttempts.push({
8255
+ label: "public-r2",
8256
+ url: `${publicBase}/packages/${encodeURIComponent(packageId)}/versions/${encodeURIComponent(versionId)}/manifest.json`
8257
+ });
8258
+ }
9246
8259
  fetchAttempts.push({
9247
- label: "public-r2",
9248
- url: `${publicBase}/packages/${encodeURIComponent(packageId)}/versions/${encodeURIComponent(versionId)}/manifest.json`
8260
+ label: "cloud",
8261
+ url: `${this.cloudUrl}/api/v1/marketplace/packages/${encodeURIComponent(packageId)}/versions/${encodeURIComponent(versionId)}/manifest`
9249
8262
  });
9250
- }
9251
- fetchAttempts.push({
9252
- label: "cloud",
9253
- url: `${this.cloudUrl}/api/v1/marketplace/packages/${encodeURIComponent(packageId)}/versions/${encodeURIComponent(versionId)}/manifest`
9254
- });
9255
- let lastErrStatus = 0;
9256
- let lastErrText = "";
9257
- for (const attempt of fetchAttempts) {
9258
- try {
9259
- const resp = await fetch(attempt.url, { headers: { "Accept": "application/json" } });
9260
- if (!resp.ok) {
9261
- lastErrStatus = resp.status;
9262
- lastErrText = (await resp.text().catch(() => "")).slice(0, 200);
9263
- if (attempt.label === "public-r2" && resp.status === 404) {
9264
- ctx.logger?.info?.(`[MarketplaceInstallLocal] public-r2 miss for ${packageId}@${versionId}, falling back to cloud`);
9265
- continue;
8263
+ let lastErrStatus = 0;
8264
+ let lastErrText = "";
8265
+ for (const attempt of fetchAttempts) {
8266
+ try {
8267
+ const resp = await fetch(attempt.url, { headers: { "Accept": "application/json" } });
8268
+ if (!resp.ok) {
8269
+ lastErrStatus = resp.status;
8270
+ lastErrText = (await resp.text().catch(() => "")).slice(0, 200);
8271
+ if (attempt.label === "public-r2" && resp.status === 404) {
8272
+ ctx.logger?.info?.(`[MarketplaceInstallLocal] public-r2 miss for ${packageId}@${versionId}, falling back to cloud`);
8273
+ continue;
8274
+ }
8275
+ if (attempt.label === "public-r2" && resp.status >= 500) {
8276
+ ctx.logger?.warn?.(`[MarketplaceInstallLocal] public-r2 ${resp.status}, falling back to cloud`);
8277
+ continue;
8278
+ }
8279
+ break;
9266
8280
  }
9267
- if (attempt.label === "public-r2" && resp.status >= 500) {
9268
- ctx.logger?.warn?.(`[MarketplaceInstallLocal] public-r2 ${resp.status}, falling back to cloud`);
8281
+ payload = await resp.json();
8282
+ lastErrStatus = 0;
8283
+ break;
8284
+ } catch (err) {
8285
+ if (attempt.label === "public-r2") {
8286
+ ctx.logger?.warn?.(`[MarketplaceInstallLocal] public-r2 fetch error: ${err?.message ?? err}, falling back to cloud`);
9269
8287
  continue;
9270
8288
  }
9271
- break;
9272
- }
9273
- payload = await resp.json();
9274
- lastErrStatus = 0;
9275
- break;
9276
- } catch (err) {
9277
- if (attempt.label === "public-r2") {
9278
- ctx.logger?.warn?.(`[MarketplaceInstallLocal] public-r2 fetch error: ${err?.message ?? err}, falling back to cloud`);
9279
- continue;
8289
+ return c.json({
8290
+ success: false,
8291
+ error: { code: "cloud_fetch_failed", message: err?.message ?? String(err) }
8292
+ }, 502);
9280
8293
  }
8294
+ }
8295
+ if (!payload) {
9281
8296
  return c.json({
9282
8297
  success: false,
9283
- error: { code: "cloud_fetch_failed", message: err?.message ?? String(err) }
9284
- }, 502);
8298
+ error: { code: "cloud_fetch_failed", message: `Cloud returned ${lastErrStatus}: ${lastErrText}` }
8299
+ }, lastErrStatus === 404 ? 404 : 502);
9285
8300
  }
8301
+ const data = payload?.data ?? payload;
8302
+ manifest = data?.manifest;
8303
+ resolvedVersionId = String(data?.version_id ?? versionId);
8304
+ version = String(data?.version ?? "unknown");
9286
8305
  }
9287
- if (!payload) {
9288
- return c.json({
9289
- success: false,
9290
- error: { code: "cloud_fetch_failed", message: `Cloud returned ${lastErrStatus}: ${lastErrText}` }
9291
- }, lastErrStatus === 404 ? 404 : 502);
9292
- }
9293
- const data = payload?.data ?? payload;
9294
- const manifest = data?.manifest;
9295
- const resolvedVersionId = String(data?.version_id ?? versionId);
9296
- const version = String(data?.version ?? "unknown");
9297
8306
  const manifestId = String(manifest?.id ?? manifest?.name ?? "");
9298
8307
  if (!manifest || !manifestId) {
9299
- return c.json({ success: false, error: { code: "invalid_manifest", message: "Cloud returned an invalid manifest payload." } }, 502);
8308
+ return c.json({ success: false, error: { code: "invalid_manifest", message: "Invalid manifest payload." } }, inlineManifest ? 400 : 502);
9300
8309
  }
9301
8310
  const conflict = this.findConflict(ctx, manifestId);
9302
8311
  if (conflict === "user-code") {
@@ -9308,6 +8317,18 @@ var MarketplaceInstallLocalPlugin = class {
9308
8317
  }
9309
8318
  }, 409);
9310
8319
  }
8320
+ try {
8321
+ const manifestService = ctx.getService("manifest");
8322
+ manifestService.register(manifest);
8323
+ } catch (err) {
8324
+ if (inlineManifest) {
8325
+ return c.json({
8326
+ success: false,
8327
+ error: { code: "register_failed", message: `Failed to register imported manifest: ${err?.message ?? err}` }
8328
+ }, 422);
8329
+ }
8330
+ ctx.logger?.warn?.(`[MarketplaceInstallLocal] hot-register failed for ${manifestId} (will load on next restart): ${err?.message ?? err}`);
8331
+ }
9311
8332
  const entry = {
9312
8333
  packageId,
9313
8334
  versionId: resolvedVersionId,
@@ -9319,20 +8340,14 @@ var MarketplaceInstallLocalPlugin = class {
9319
8340
  withSampleData: false
9320
8341
  };
9321
8342
  try {
9322
- (0, import_node_fs3.mkdirSync)(this.storageDir, { recursive: true });
9323
- (0, import_node_fs3.writeFileSync)((0, import_node_path6.join)(this.storageDir, safeFilename(manifestId)), JSON.stringify(entry, null, 2), "utf8");
8343
+ (0, import_node_fs4.mkdirSync)(this.storageDir, { recursive: true });
8344
+ (0, import_node_fs4.writeFileSync)((0, import_node_path7.join)(this.storageDir, safeFilename(manifestId)), JSON.stringify(entry, null, 2), "utf8");
9324
8345
  } catch (err) {
9325
8346
  return c.json({
9326
8347
  success: false,
9327
8348
  error: { code: "storage_failed", message: `Failed to persist manifest: ${err?.message ?? err}` }
9328
8349
  }, 500);
9329
8350
  }
9330
- try {
9331
- const manifestService = ctx.getService("manifest");
9332
- manifestService.register(manifest);
9333
- } catch (err) {
9334
- ctx.logger?.warn?.(`[MarketplaceInstallLocal] hot-register failed for ${manifestId} (will load on next restart): ${err?.message ?? err}`);
9335
- }
9336
8351
  try {
9337
8352
  const ql = ctx.getService("objectql");
9338
8353
  if (ql && typeof ql.syncSchemas === "function") {
@@ -9346,7 +8361,7 @@ var MarketplaceInstallLocalPlugin = class {
9346
8361
  if (seededSummary.seeded.mode === "inline" && (seededSummary.seeded.inserted ?? 0) + (seededSummary.seeded.updated ?? 0) > 0) {
9347
8362
  entry.withSampleData = true;
9348
8363
  try {
9349
- (0, import_node_fs3.writeFileSync)((0, import_node_path6.join)(this.storageDir, safeFilename(manifestId)), JSON.stringify(entry, null, 2), "utf8");
8364
+ (0, import_node_fs4.writeFileSync)((0, import_node_path7.join)(this.storageDir, safeFilename(manifestId)), JSON.stringify(entry, null, 2), "utf8");
9350
8365
  } catch {
9351
8366
  }
9352
8367
  }
@@ -9393,12 +8408,12 @@ var MarketplaceInstallLocalPlugin = class {
9393
8408
  if (!manifestId) {
9394
8409
  return c.json({ success: false, error: { code: "bad_request", message: "manifestId path param required." } }, 400);
9395
8410
  }
9396
- const file = (0, import_node_path6.join)(this.storageDir, safeFilename(manifestId));
9397
- if (!(0, import_node_fs3.existsSync)(file)) {
8411
+ const file = (0, import_node_path7.join)(this.storageDir, safeFilename(manifestId));
8412
+ if (!(0, import_node_fs4.existsSync)(file)) {
9398
8413
  return c.json({ success: false, error: { code: "not_found", message: `No marketplace install for ${manifestId}.` } }, 404);
9399
8414
  }
9400
8415
  try {
9401
- (0, import_node_fs3.unlinkSync)(file);
8416
+ (0, import_node_fs4.unlinkSync)(file);
9402
8417
  } catch (err) {
9403
8418
  return c.json({ success: false, error: { code: "storage_failed", message: err?.message ?? String(err) } }, 500);
9404
8419
  }
@@ -9421,7 +8436,7 @@ var MarketplaceInstallLocalPlugin = class {
9421
8436
  * (refuse to avoid silently overwriting authored code)
9422
8437
  */
9423
8438
  this.findConflict = (ctx, manifestId) => {
9424
- if ((0, import_node_fs3.existsSync)((0, import_node_path6.join)(this.storageDir, safeFilename(manifestId)))) {
8439
+ if ((0, import_node_fs4.existsSync)((0, import_node_path7.join)(this.storageDir, safeFilename(manifestId)))) {
9425
8440
  return "marketplace";
9426
8441
  }
9427
8442
  try {
@@ -9463,13 +8478,13 @@ var MarketplaceInstallLocalPlugin = class {
9463
8478
  if (!manifestId) {
9464
8479
  return c.json({ success: false, error: { code: "bad_request", message: "manifestId path param required." } }, 400);
9465
8480
  }
9466
- const file = (0, import_node_path6.join)(this.storageDir, safeFilename(manifestId));
9467
- if (!(0, import_node_fs3.existsSync)(file)) {
8481
+ const file = (0, import_node_path7.join)(this.storageDir, safeFilename(manifestId));
8482
+ if (!(0, import_node_fs4.existsSync)(file)) {
9468
8483
  return c.json({ success: false, error: { code: "not_found", message: `No marketplace install for ${manifestId}.` } }, 404);
9469
8484
  }
9470
8485
  let entry;
9471
8486
  try {
9472
- entry = JSON.parse((0, import_node_fs3.readFileSync)(file, "utf8"));
8487
+ entry = JSON.parse((0, import_node_fs4.readFileSync)(file, "utf8"));
9473
8488
  } catch (err) {
9474
8489
  return c.json({ success: false, error: { code: "storage_failed", message: `Failed to read manifest cache: ${err?.message ?? err}` } }, 500);
9475
8490
  }
@@ -9485,7 +8500,7 @@ var MarketplaceInstallLocalPlugin = class {
9485
8500
  }
9486
8501
  try {
9487
8502
  entry.withSampleData = true;
9488
- (0, import_node_fs3.writeFileSync)(file, JSON.stringify(entry, null, 2), "utf8");
8503
+ (0, import_node_fs4.writeFileSync)(file, JSON.stringify(entry, null, 2), "utf8");
9489
8504
  } catch {
9490
8505
  }
9491
8506
  return c.json({
@@ -9517,13 +8532,13 @@ var MarketplaceInstallLocalPlugin = class {
9517
8532
  if (!manifestId) {
9518
8533
  return c.json({ success: false, error: { code: "bad_request", message: "manifestId path param required." } }, 400);
9519
8534
  }
9520
- const file = (0, import_node_path6.join)(this.storageDir, safeFilename(manifestId));
9521
- if (!(0, import_node_fs3.existsSync)(file)) {
8535
+ const file = (0, import_node_path7.join)(this.storageDir, safeFilename(manifestId));
8536
+ if (!(0, import_node_fs4.existsSync)(file)) {
9522
8537
  return c.json({ success: false, error: { code: "not_found", message: `No marketplace install for ${manifestId}.` } }, 404);
9523
8538
  }
9524
8539
  let entry;
9525
8540
  try {
9526
- entry = JSON.parse((0, import_node_fs3.readFileSync)(file, "utf8"));
8541
+ entry = JSON.parse((0, import_node_fs4.readFileSync)(file, "utf8"));
9527
8542
  } catch (err) {
9528
8543
  return c.json({ success: false, error: { code: "storage_failed", message: `Failed to read manifest cache: ${err?.message ?? err}` } }, 500);
9529
8544
  }
@@ -9572,7 +8587,7 @@ var MarketplaceInstallLocalPlugin = class {
9572
8587
  }
9573
8588
  try {
9574
8589
  entry.withSampleData = false;
9575
- (0, import_node_fs3.writeFileSync)(file, JSON.stringify(entry, null, 2), "utf8");
8590
+ (0, import_node_fs4.writeFileSync)(file, JSON.stringify(entry, null, 2), "utf8");
9576
8591
  } catch {
9577
8592
  }
9578
8593
  ctx.logger?.info?.(`[MarketplaceInstallLocal] purged ${manifestId}: deleted=${deleted} skipped=${skipped} errors=${errors}`);
@@ -9670,7 +8685,7 @@ var MarketplaceInstallLocalPlugin = class {
9670
8685
  }
9671
8686
  }
9672
8687
  if (opts.seedNow && datasets.length > 0) {
9673
- const multiTenant = String((0, import_types5.readEnvWithDeprecation)("OS_MULTI_ORG_ENABLED", "OS_MULTI_TENANT") ?? "false").toLowerCase() !== "false";
8688
+ const multiTenant = String((0, import_types4.readEnvWithDeprecation)("OS_MULTI_ORG_ENABLED", "OS_MULTI_TENANT") ?? "false").toLowerCase() !== "false";
9674
8689
  try {
9675
8690
  const ql = ctx.getService("objectql");
9676
8691
  let metadata;
@@ -9771,12 +8786,12 @@ var MarketplaceInstallLocalPlugin = class {
9771
8786
  return null;
9772
8787
  };
9773
8788
  this.readAll = () => {
9774
- if (!(0, import_node_fs3.existsSync)(this.storageDir)) return [];
8789
+ if (!(0, import_node_fs4.existsSync)(this.storageDir)) return [];
9775
8790
  const out = [];
9776
- for (const name of (0, import_node_fs3.readdirSync)(this.storageDir)) {
8791
+ for (const name of (0, import_node_fs4.readdirSync)(this.storageDir)) {
9777
8792
  if (!name.endsWith(".json")) continue;
9778
8793
  try {
9779
- const raw = (0, import_node_fs3.readFileSync)((0, import_node_path6.join)(this.storageDir, name), "utf8");
8794
+ const raw = (0, import_node_fs4.readFileSync)((0, import_node_path7.join)(this.storageDir, name), "utf8");
9780
8795
  out.push(JSON.parse(raw));
9781
8796
  } catch {
9782
8797
  }
@@ -9784,13 +8799,10 @@ var MarketplaceInstallLocalPlugin = class {
9784
8799
  return out;
9785
8800
  };
9786
8801
  this.cloudUrl = resolveCloudUrl(config.controlPlaneUrl);
9787
- this.storageDir = config.storageDir ? (0, import_node_path6.resolve)(config.storageDir) : (0, import_node_path6.resolve)(process.cwd(), DEFAULT_DIR);
8802
+ this.storageDir = config.storageDir ? (0, import_node_path7.resolve)(config.storageDir) : (0, import_node_path7.resolve)(process.cwd(), DEFAULT_DIR);
9788
8803
  }
9789
8804
  };
9790
8805
 
9791
- // src/index.ts
9792
- init_platform_sso();
9793
-
9794
8806
  // src/sandbox/script-runner.ts
9795
8807
  var UnimplementedScriptRunner = class {
9796
8808
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
@@ -9816,7 +8828,7 @@ init_body_runner();
9816
8828
  // src/index.ts
9817
8829
  var import_rest = require("@objectstack/rest");
9818
8830
  __reExport(index_exports, require("@objectstack/core"), module.exports);
9819
- var import_types6 = require("@objectstack/types");
8831
+ var import_types5 = require("@objectstack/types");
9820
8832
  // Annotate the CommonJS export names for ESM import in node:
9821
8833
  0 && (module.exports = {
9822
8834
  AppPlugin,
@@ -9827,6 +8839,7 @@ var import_types6 = require("@objectstack/types");
9827
8839
  DEFAULT_CLOUD_URL,
9828
8840
  DEFAULT_RATE_LIMITS,
9829
8841
  DriverPlugin,
8842
+ ExternalValidationPlugin,
9830
8843
  FileArtifactApiClient,
9831
8844
  HttpDispatcher,
9832
8845
  HttpServer,
@@ -9865,6 +8878,7 @@ var import_types6 = require("@objectstack/types");
9865
8878
  collectBundleHooks,
9866
8879
  createDefaultHostConfig,
9867
8880
  createDispatcherPlugin,
8881
+ createExternalValidationPlugin,
9868
8882
  createObjectOSStack,
9869
8883
  createRestApiPlugin,
9870
8884
  createStandaloneStack,