@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.js CHANGED
@@ -9,13 +9,6 @@ var __export = (target, all) => {
9
9
  };
10
10
 
11
11
  // src/load-artifact-bundle.ts
12
- var load_artifact_bundle_exports = {};
13
- __export(load_artifact_bundle_exports, {
14
- isHttpUrl: () => isHttpUrl,
15
- loadArtifactBundle: () => loadArtifactBundle,
16
- mergeRuntimeModule: () => mergeRuntimeModule,
17
- readArtifactSource: () => readArtifactSource
18
- });
19
12
  import { readFile } from "fs/promises";
20
13
  import { resolve as resolvePath, isAbsolute, dirname } from "path";
21
14
  import { pathToFileURL } from "url";
@@ -270,17 +263,37 @@ var init_seed_loader = __esm({
270
263
  }
271
264
  const objectRefs = refMap.get(objectName) || [];
272
265
  const seedNow = /* @__PURE__ */ new Date();
266
+ const seedIdentity = config.identity;
267
+ const baseEvalCtx = {
268
+ now: seedNow,
269
+ user: seedIdentity?.user,
270
+ // Fall back to the per-tenant organizationId so `os.org.id` resolves
271
+ // during per-org replay even without an explicit identity.org.
272
+ org: seedIdentity?.org ?? (config.organizationId ? { id: config.organizationId } : void 0),
273
+ env: config.env
274
+ };
273
275
  for (let i = 0; i < dataset.records.length; i++) {
274
276
  const seedResult = resolveSeedRecord(
275
277
  dataset.records[i],
276
- { now: seedNow }
278
+ baseEvalCtx
277
279
  );
278
- const record = seedResult.ok ? { ...seedResult.value } : { ...dataset.records[i] };
279
280
  if (!seedResult.ok) {
280
- this.logger.warn(
281
- `[SeedLoader] Failed to resolve dynamic values for ${objectName} record #${i}: ${seedResult.error.message}`
282
- );
281
+ errored++;
282
+ const error = {
283
+ sourceObject: objectName,
284
+ field: "(expression)",
285
+ targetObject: objectName,
286
+ targetField: "(expression)",
287
+ attemptedValue: dataset.records[i],
288
+ recordIndex: i,
289
+ 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).`
290
+ };
291
+ errors.push(error);
292
+ allErrors.push(error);
293
+ this.logger.warn(`[SeedLoader] ${error.message}`);
294
+ continue;
283
295
  }
296
+ const record = { ...seedResult.value };
284
297
  if (config.organizationId && record["organization_id"] == null) {
285
298
  record["organization_id"] = config.organizationId;
286
299
  }
@@ -359,10 +372,18 @@ var init_seed_loader = __esm({
359
372
  }
360
373
  } catch (err) {
361
374
  errored++;
362
- this.logger.warn(`[SeedLoader] Failed to write ${objectName} record`, {
363
- error: err.message,
364
- recordIndex: i
365
- });
375
+ const error = {
376
+ sourceObject: objectName,
377
+ field: "(write)",
378
+ targetObject: objectName,
379
+ targetField: externalId,
380
+ attemptedValue: record[externalId] ?? null,
381
+ recordIndex: i,
382
+ message: `Failed to write ${objectName} record #${i} (${externalId}=${String(record[externalId] ?? "")}): ${err.message}`
383
+ };
384
+ errors.push(error);
385
+ allErrors.push(error);
386
+ this.logger.warn(`[SeedLoader] ${error.message}`, { recordIndex: i });
366
387
  }
367
388
  } else {
368
389
  const externalIdValue = String(record[externalId] ?? "");
@@ -702,6 +723,52 @@ var init_seed_loader = __esm({
702
723
  }
703
724
  });
704
725
 
726
+ // src/package-state-store.ts
727
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
728
+ import { dirname as dirname2, join } from "path";
729
+ function sanitizeEnvironmentId(environmentId) {
730
+ const raw = (environmentId ?? process.env.OS_ENVIRONMENT_ID ?? DEFAULT_ENVIRONMENT_ID).trim();
731
+ const safe = raw.replace(/[^a-zA-Z0-9._-]/g, "_");
732
+ return safe.length > 0 ? safe : DEFAULT_ENVIRONMENT_ID;
733
+ }
734
+ function stateFilePath(environmentId) {
735
+ return join(resolveObjectStackHome(), "package-state", `${sanitizeEnvironmentId(environmentId)}.json`);
736
+ }
737
+ function readState(environmentId) {
738
+ const file = stateFilePath(environmentId);
739
+ if (!existsSync(file)) return {};
740
+ try {
741
+ const parsed = JSON.parse(readFileSync(file, "utf8"));
742
+ return parsed && typeof parsed === "object" ? parsed : {};
743
+ } catch {
744
+ return {};
745
+ }
746
+ }
747
+ function writeState(environmentId, state) {
748
+ const file = stateFilePath(environmentId);
749
+ mkdirSync(dirname2(file), { recursive: true });
750
+ writeFileSync(file, `${JSON.stringify(state, null, 2)}
751
+ `, "utf8");
752
+ }
753
+ function loadDisabledPackageIds(environmentId) {
754
+ const disabled = readState(environmentId).disabled;
755
+ return new Set(Array.isArray(disabled) ? disabled.filter((id) => typeof id === "string") : []);
756
+ }
757
+ function setPackageDisabled(environmentId, packageId, disabled) {
758
+ const ids = loadDisabledPackageIds(environmentId);
759
+ if (disabled) ids.add(packageId);
760
+ else ids.delete(packageId);
761
+ writeState(environmentId, { disabled: Array.from(ids).sort() });
762
+ }
763
+ var DEFAULT_ENVIRONMENT_ID;
764
+ var init_package_state_store = __esm({
765
+ "src/package-state-store.ts"() {
766
+ "use strict";
767
+ init_standalone_stack();
768
+ DEFAULT_ENVIRONMENT_ID = "default";
769
+ }
770
+ });
771
+
705
772
  // src/sandbox/quickjs-runner.ts
706
773
  import {
707
774
  newAsyncContext
@@ -1203,6 +1270,7 @@ __export(app_plugin_exports, {
1203
1270
  collectBundleHooks: () => collectBundleHooks
1204
1271
  });
1205
1272
  import { readEnvWithDeprecation } from "@objectstack/types";
1273
+ import { SystemUserId } from "@objectstack/spec/system";
1206
1274
  function collectBundleHooks(bundle) {
1207
1275
  const out = [];
1208
1276
  const seen = /* @__PURE__ */ new Set();
@@ -1267,6 +1335,7 @@ var init_app_plugin = __esm({
1267
1335
  "src/app-plugin.ts"() {
1268
1336
  "use strict";
1269
1337
  init_seed_loader();
1338
+ init_package_state_store();
1270
1339
  init_quickjs_runner();
1271
1340
  init_body_runner();
1272
1341
  AppPlugin = class {
@@ -1292,6 +1361,24 @@ var init_app_plugin = __esm({
1292
1361
  console.warn(
1293
1362
  `[AppPlugin:init] appId=${appId} keys=${Object.keys(servicePayload).join(",")} flows=${Array.isArray(servicePayload.flows) ? servicePayload.flows.length : "n/a"}`
1294
1363
  );
1364
+ try {
1365
+ const ql = ctx.getService("objectql");
1366
+ const setter = ql?.registry?.setInitialDisabledPackageIds;
1367
+ if (typeof setter === "function") {
1368
+ const disabled = loadDisabledPackageIds(this.projectContext?.environmentId);
1369
+ if (disabled.size > 0) {
1370
+ setter.call(ql.registry, disabled);
1371
+ ctx.logger.info("[AppPlugin] seeded persisted disabled packages", {
1372
+ environmentId: this.projectContext?.environmentId,
1373
+ disabled: Array.from(disabled)
1374
+ });
1375
+ }
1376
+ }
1377
+ } catch (err) {
1378
+ ctx.logger.warn("[AppPlugin] failed to seed persisted package state", {
1379
+ error: err?.message ?? String(err)
1380
+ });
1381
+ }
1295
1382
  ctx.getService("manifest").register(servicePayload);
1296
1383
  };
1297
1384
  this.start = async (ctx) => {
@@ -1323,6 +1410,27 @@ var init_app_plugin = __esm({
1323
1410
  });
1324
1411
  ql.setDatasourceMapping(this.bundle.datasourceMapping);
1325
1412
  }
1413
+ try {
1414
+ const dsDefs = this.bundle.datasources;
1415
+ const dsList = Array.isArray(dsDefs) ? dsDefs : dsDefs && typeof dsDefs === "object" ? Object.entries(dsDefs).map(([name, def]) => ({ name, ...def })) : [];
1416
+ if (dsList.length > 0) {
1417
+ const metadata = ctx.getService("metadata");
1418
+ if (typeof metadata?.registerInMemory === "function") {
1419
+ for (const ds of dsList) {
1420
+ if (!ds?.name) continue;
1421
+ metadata.registerInMemory("datasource", ds.name, { ...ds, origin: "code" });
1422
+ }
1423
+ ctx.logger.info("Registered code-defined datasources in metadata registry", {
1424
+ appId,
1425
+ count: dsList.length
1426
+ });
1427
+ }
1428
+ }
1429
+ } catch (err) {
1430
+ ctx.logger.warn("[AppPlugin] failed to register code-defined datasources", {
1431
+ error: err?.message ?? String(err)
1432
+ });
1433
+ }
1326
1434
  const stackBundle = this.bundle.default || this.bundle;
1327
1435
  const runtime = stackBundle && typeof stackBundle.onEnable === "function" ? stackBundle : this.bundle;
1328
1436
  if (runtime && typeof runtime.onEnable === "function") {
@@ -1417,49 +1525,6 @@ var init_app_plugin = __esm({
1417
1525
  appId
1418
1526
  });
1419
1527
  }
1420
- try {
1421
- const approvals = Array.isArray(this.bundle.approvals) ? this.bundle.approvals : Array.isArray((this.bundle.manifest || {}).approvals) ? this.bundle.manifest.approvals : [];
1422
- if (approvals.length > 0) {
1423
- ctx.hook("kernel:ready", async () => {
1424
- let svc;
1425
- try {
1426
- svc = ctx.getService("approvals");
1427
- } catch {
1428
- }
1429
- if (!svc || typeof svc.defineProcess !== "function") {
1430
- ctx.logger.warn("[AppPlugin] approvals service not registered \u2014 skipping declarative processes", {
1431
- appId,
1432
- processCount: approvals.length
1433
- });
1434
- return;
1435
- }
1436
- const sysCtx = { isSystem: true, roles: [], permissions: [] };
1437
- let ok = 0;
1438
- for (const proc of approvals) {
1439
- try {
1440
- await svc.defineProcess({
1441
- name: proc.name,
1442
- label: proc.label,
1443
- object: proc.object,
1444
- description: proc.description,
1445
- active: proc.active !== false,
1446
- definition: proc
1447
- }, sysCtx);
1448
- ok++;
1449
- } catch (err) {
1450
- ctx.logger.warn("[AppPlugin] Failed to register approval process", {
1451
- appId,
1452
- process: proc?.name,
1453
- error: err?.message ?? String(err)
1454
- });
1455
- }
1456
- }
1457
- ctx.logger.info("[AppPlugin] Registered approval processes", { appId, count: ok });
1458
- });
1459
- }
1460
- } catch (err) {
1461
- ctx.logger.error("[AppPlugin] Failed to schedule approval-process registration", err, { appId });
1462
- }
1463
1528
  try {
1464
1529
  const jobs = Array.isArray(this.bundle.jobs) ? this.bundle.jobs : Array.isArray((this.bundle.manifest || {}).jobs) ? this.bundle.manifest.jobs : [];
1465
1530
  if (jobs.length > 0) {
@@ -1536,6 +1601,7 @@ var init_app_plugin = __esm({
1536
1601
  ...d,
1537
1602
  object: d.object
1538
1603
  }));
1604
+ const seedIdentity = await this.ensureSeedIdentity(ql, ctx.logger);
1539
1605
  try {
1540
1606
  const kernel = ctx.kernel;
1541
1607
  const existing = (() => {
@@ -1577,7 +1643,12 @@ var init_app_plugin = __esm({
1577
1643
  config: {
1578
1644
  defaultMode: "upsert",
1579
1645
  multiPass: true,
1580
- organizationId
1646
+ organizationId,
1647
+ // Bind os.user (system identity) and os.org (this
1648
+ // tenant) so identity-derived seed values resolve
1649
+ // per-org. org.id falls back to organizationId
1650
+ // inside the loader when identity.org is absent.
1651
+ identity: seedIdentity
1581
1652
  }
1582
1653
  });
1583
1654
  const result = await seedLoader.load(request);
@@ -1605,14 +1676,34 @@ var init_app_plugin = __esm({
1605
1676
  const { SeedLoaderRequestSchema } = await import("@objectstack/spec/data");
1606
1677
  const request = SeedLoaderRequestSchema.parse({
1607
1678
  datasets: normalizedDatasets,
1608
- config: { defaultMode: "upsert", multiPass: true }
1679
+ config: { defaultMode: "upsert", multiPass: true, identity: seedIdentity }
1609
1680
  });
1610
1681
  const result = await seedLoader.load(request);
1611
- ctx.logger.info("[Seeder] Seed loading complete", {
1612
- inserted: result.summary.totalInserted,
1613
- updated: result.summary.totalUpdated,
1614
- errors: result.errors.length
1615
- });
1682
+ const { totalInserted, totalUpdated, totalSkipped, totalErrored } = result.summary;
1683
+ if (result.success) {
1684
+ ctx.logger.info("[Seeder] Seed loading complete", {
1685
+ inserted: totalInserted,
1686
+ updated: totalUpdated,
1687
+ skipped: totalSkipped,
1688
+ errored: totalErrored
1689
+ });
1690
+ } else {
1691
+ ctx.logger.warn(
1692
+ `[Seeder] Seed loading completed with ${totalErrored} dropped record(s) and ${result.errors.length} error(s) for ${appId}`,
1693
+ {
1694
+ inserted: totalInserted,
1695
+ updated: totalUpdated,
1696
+ skipped: totalSkipped,
1697
+ errored: totalErrored
1698
+ }
1699
+ );
1700
+ for (const e of result.errors.slice(0, 20)) {
1701
+ ctx.logger.warn(`[Seeder] \u2717 ${e.message}`);
1702
+ }
1703
+ if (result.errors.length > 20) {
1704
+ ctx.logger.warn(`[Seeder] \u2026and ${result.errors.length - 20} more error(s)`);
1705
+ }
1706
+ }
1616
1707
  } else {
1617
1708
  ctx.logger.debug("[Seeder] No metadata service; using basic insert fallback");
1618
1709
  for (const dataset of normalizedDatasets) {
@@ -1714,6 +1805,64 @@ var init_app_plugin = __esm({
1714
1805
  this.name = `plugin.app.${appId}`;
1715
1806
  this.version = sys?.version;
1716
1807
  }
1808
+ /**
1809
+ * Resolve the identity bound to `os.user` / `os.org` for seed CEL values.
1810
+ *
1811
+ * On a fresh boot there are zero users until the first human sign-up
1812
+ * (which the SeedLoader runs *before*), so identity-derived seeds like
1813
+ * `owner_id: cel`os.user.id`` had nothing to resolve against and were
1814
+ * dropped silently. To make seeds deterministic and self-sufficient we
1815
+ * upsert a single non-loginable **system user** (`usr_system`) and bind
1816
+ * it as `os.user`.
1817
+ *
1818
+ * Why a dedicated system user rather than the login admin:
1819
+ * - `sys_user` is better-auth-managed and schema-locked (ADR-0010); the
1820
+ * password lives in `sys_account`, so a *loginable* admin can only be
1821
+ * minted through better-auth (the CLI does this via HTTP sign-up after
1822
+ * boot). A raw insert here would bypass those invariants.
1823
+ * - `usr_system` is an owner identity only (no credential row), analogous
1824
+ * to Salesforce's "Automated Process" user. The human admin is created
1825
+ * independently and need not be the seed owner.
1826
+ *
1827
+ * Idempotent: matches by the stable id, inserts once, reuses thereafter.
1828
+ * Failures are non-fatal (logged) — records that actually need `os.user`
1829
+ * then fail loudly in the loader with an actionable message.
1830
+ */
1831
+ async ensureSeedIdentity(ql, logger) {
1832
+ const SYSTEM_USER_ID = SystemUserId.SYSTEM;
1833
+ const SYSTEM_USER_EMAIL = "system@objectstack.local";
1834
+ const identity = { user: { id: SYSTEM_USER_ID, role: "system", email: SYSTEM_USER_EMAIL } };
1835
+ const opts = { context: { isSystem: true } };
1836
+ try {
1837
+ const existing = await ql.find(
1838
+ "sys_user",
1839
+ { where: { id: SYSTEM_USER_ID }, limit: 1 },
1840
+ opts
1841
+ );
1842
+ if (Array.isArray(existing) && existing.length > 0) {
1843
+ return identity;
1844
+ }
1845
+ await ql.insert(
1846
+ "sys_user",
1847
+ {
1848
+ id: SYSTEM_USER_ID,
1849
+ name: "System",
1850
+ email: SYSTEM_USER_EMAIL,
1851
+ email_verified: true,
1852
+ role: "system"
1853
+ },
1854
+ opts
1855
+ );
1856
+ logger.info(
1857
+ `[Seeder] Provisioned deterministic system user (${SYSTEM_USER_ID}) as seed owner \u2014 binds os.user for identity-derived seed values`
1858
+ );
1859
+ } catch (err) {
1860
+ logger.warn("[Seeder] Failed to ensure system seed user; os.user-dependent seeds may be dropped", {
1861
+ error: err?.message ?? String(err)
1862
+ });
1863
+ }
1864
+ return identity;
1865
+ }
1717
1866
  /**
1718
1867
  * Emit a kernel hook so the control-plane `AppCatalogService` can
1719
1868
  * upsert / delete the corresponding `sys_app` row. Silently no-ops
@@ -1836,209 +1985,167 @@ var init_app_plugin = __esm({
1836
1985
  }
1837
1986
  });
1838
1987
 
1839
- // src/cloud/platform-sso.ts
1840
- var platform_sso_exports = {};
1841
- __export(platform_sso_exports, {
1842
- PLATFORM_SSO_PROVIDER_ID: () => PLATFORM_SSO_PROVIDER_ID,
1843
- backfillPlatformSsoClients: () => backfillPlatformSsoClients,
1844
- buildPlatformSsoRedirectUri: () => buildPlatformSsoRedirectUri,
1845
- derivePlatformSsoClientId: () => derivePlatformSsoClientId,
1846
- derivePlatformSsoClientSecret: () => derivePlatformSsoClientSecret,
1847
- hashPlatformSsoClientSecret: () => hashPlatformSsoClientSecret,
1848
- seedPlatformSsoClient: () => seedPlatformSsoClient
1849
- });
1850
- import { createHmac, createHash } from "crypto";
1851
- function derivePlatformSsoClientId(environmentId) {
1852
- return `project_${environmentId}`;
1853
- }
1854
- function derivePlatformSsoClientSecret(baseSecret, environmentId) {
1855
- return createHmac("sha256", baseSecret).update(`oauth-client:${environmentId}`).digest("hex");
1856
- }
1857
- function hashPlatformSsoClientSecret(plaintext) {
1858
- return createHash("sha256").update(plaintext).digest("base64").replace(/=+$/, "").replace(/\+/g, "-").replace(/\//g, "_");
1859
- }
1860
- function buildPlatformSsoRedirectUri(hostname, basePath = "/api/v1/auth") {
1861
- let host;
1862
- if (hostname.startsWith("http://") || hostname.startsWith("https://")) {
1863
- host = hostname;
1864
- } else if (/(\.|^)localhost(:\d+)?$/i.test(hostname)) {
1865
- const port = (process.env.OS_RUNTIME_PORT ?? "").trim();
1866
- const hostWithPort = /:\d+$/.test(hostname) || !port ? hostname : `${hostname}:${port}`;
1867
- host = `http://${hostWithPort}`;
1868
- } else {
1869
- host = `https://${hostname}`;
1988
+ // src/standalone-stack.ts
1989
+ import { resolve as resolvePath2 } from "path";
1990
+ import { mkdirSync as mkdirSync2 } from "fs";
1991
+ import { homedir } from "os";
1992
+ import { z } from "zod";
1993
+ import { readEnvWithDeprecation as readEnvWithDeprecation2 } from "@objectstack/types";
1994
+ function resolveObjectStackHome() {
1995
+ const raw = process.env.OS_HOME?.trim();
1996
+ if (raw && raw.length > 0) {
1997
+ if (raw.startsWith("~")) return resolvePath2(homedir(), raw.slice(1).replace(/^[/\\]/, ""));
1998
+ return resolvePath2(raw);
1870
1999
  }
1871
- const trimmed = host.replace(/\/+$/, "");
1872
- const path = basePath.replace(/\/+$/, "");
1873
- return `${trimmed}${path}/oauth2/callback/${PLATFORM_SSO_PROVIDER_ID}`;
2000
+ return resolvePath2(homedir(), ".objectstack");
1874
2001
  }
1875
- async function seedPlatformSsoClient(opts) {
1876
- const { ql, environmentId, hostname, baseSecret, logger, throwOnError } = opts;
1877
- if (!baseSecret) {
1878
- logger?.warn?.("[platform-sso] OS_AUTH_SECRET not set \u2014 skipping client seed", { environmentId });
1879
- return;
1880
- }
1881
- const clientId = derivePlatformSsoClientId(environmentId);
1882
- const clientSecretPlaintext = derivePlatformSsoClientSecret(baseSecret, environmentId);
1883
- const clientSecretStored = hashPlatformSsoClientSecret(clientSecretPlaintext);
1884
- const desiredRedirect = hostname ? buildPlatformSsoRedirectUri(hostname) : null;
1885
- let existing = null;
1886
- try {
1887
- const rows = await ql.find("sys_oauth_application", {
1888
- where: { client_id: clientId },
1889
- limit: 1
1890
- }, { context: { isSystem: true } });
1891
- const list = Array.isArray(rows) ? rows : Array.isArray(rows?.records) ? rows.records : [];
1892
- existing = list[0] ?? null;
1893
- } catch (err) {
1894
- logger?.warn?.("[platform-sso] sys_oauth_application read failed \u2014 skipping seed", {
1895
- environmentId,
1896
- error: err?.message
1897
- });
1898
- return;
1899
- }
1900
- const nowIso = (/* @__PURE__ */ new Date()).toISOString();
1901
- if (!existing) {
1902
- const redirects = desiredRedirect ? [desiredRedirect] : [];
2002
+ function detectDriverFromUrl(dbUrl) {
2003
+ if (/^memory:\/\//i.test(dbUrl)) return "memory";
2004
+ if (/^(postgres(ql)?|pg):\/\//i.test(dbUrl)) return "postgres";
2005
+ if (/^mongodb(\+srv)?:\/\//i.test(dbUrl)) return "mongodb";
2006
+ if (/^wasm-sqlite:\/\//i.test(dbUrl)) return "sqlite-wasm";
2007
+ if (/\.wasm\.db$/i.test(dbUrl)) return "sqlite-wasm";
2008
+ if (/^file:/i.test(dbUrl)) return "sqlite";
2009
+ if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(dbUrl)) return "sqlite";
2010
+ throw new Error(
2011
+ `[StandaloneStack] Unsupported database URL scheme: ${dbUrl}. Supported schemes: memory://, postgres://, pg://, mongodb://, mongodb+srv://, file:`
2012
+ );
2013
+ }
2014
+ async function createStandaloneStack(config) {
2015
+ const cfg = StandaloneStackConfigSchema.parse(config ?? {});
2016
+ const { ObjectQLPlugin } = await import("@objectstack/objectql");
2017
+ const { MetadataPlugin } = await import("@objectstack/metadata");
2018
+ const { DriverPlugin: DriverPlugin2 } = await Promise.resolve().then(() => (init_driver_plugin(), driver_plugin_exports));
2019
+ const { AppPlugin: AppPlugin2 } = await Promise.resolve().then(() => (init_app_plugin(), app_plugin_exports));
2020
+ const cwd = process.cwd();
2021
+ const environmentId = cfg.environmentId ?? process.env.OS_ENVIRONMENT_ID ?? "proj_local";
2022
+ const artifactPathInput = cfg.artifactPath ?? process.env.OS_ARTIFACT_PATH ?? resolvePath2(cwd, "dist/objectstack.json");
2023
+ const artifactPath = isHttpUrl(artifactPathInput) ? artifactPathInput : artifactPathInput.startsWith("/") ? artifactPathInput : resolvePath2(cwd, artifactPathInput);
2024
+ const dbUrl = cfg.databaseUrl ?? readEnvWithDeprecation2("OS_DATABASE_URL", "DATABASE_URL")?.trim() ?? process.env.TURSO_DATABASE_URL?.trim() ?? (process.env.OS_HOME?.trim() ? `file:${resolvePath2(resolveObjectStackHome(), "data/standalone.db")}` : cfg.projectRoot ? `file:${resolvePath2(cfg.projectRoot, ".objectstack/data/standalone.db")}` : `file:${resolvePath2(resolveObjectStackHome(), "data/standalone.db")}`);
2025
+ const explicitDriver = cfg.databaseDriver ?? process.env.OS_DATABASE_DRIVER?.trim();
2026
+ const dbDriver = explicitDriver ?? detectDriverFromUrl(dbUrl);
2027
+ let driverPlugin;
2028
+ if (dbDriver === "memory") {
2029
+ const { InMemoryDriver } = await import("@objectstack/driver-memory");
2030
+ driverPlugin = new DriverPlugin2(new InMemoryDriver());
2031
+ } else if (dbDriver === "postgres") {
2032
+ const { SqlDriver } = await import("@objectstack/driver-sql");
2033
+ driverPlugin = new DriverPlugin2(
2034
+ new SqlDriver({
2035
+ client: "pg",
2036
+ connection: dbUrl,
2037
+ pool: { min: 0, max: 5 }
2038
+ })
2039
+ );
2040
+ } else if (dbDriver === "mongodb") {
2041
+ let MongoDBDriver;
1903
2042
  try {
1904
- await ql.insert("sys_oauth_application", {
1905
- id: `oauthc_${environmentId}`,
1906
- name: `Project ${environmentId}`,
1907
- client_id: clientId,
1908
- client_secret: clientSecretStored,
1909
- type: "web",
1910
- redirect_uris: JSON.stringify(redirects),
1911
- grant_types: JSON.stringify(["authorization_code", "refresh_token"]),
1912
- response_types: JSON.stringify(["code"]),
1913
- scopes: JSON.stringify(["openid", "email", "profile"]),
1914
- token_endpoint_auth_method: "client_secret_basic",
1915
- require_pkce: false,
1916
- skip_consent: true,
1917
- disabled: false,
1918
- subject_type: "public",
1919
- created_at: nowIso,
1920
- updated_at: nowIso
1921
- }, { context: { isSystem: true } });
1922
- logger?.info?.("[platform-sso] sys_oauth_application row created", { environmentId, clientId });
2043
+ ({ MongoDBDriver } = await import("@objectstack/driver-mongodb"));
1923
2044
  } catch (err) {
1924
- logger?.warn?.("[platform-sso] sys_oauth_application create failed", {
1925
- environmentId,
1926
- error: err?.message
1927
- });
1928
- if (throwOnError) throw err;
2045
+ throw new Error(
2046
+ `[StandaloneStack] mongodb URL detected but @objectstack/driver-mongodb is not installed. Add it as a dependency or pass an explicit driverPlugin. (${err?.message ?? err})`
2047
+ );
1929
2048
  }
1930
- return;
2049
+ driverPlugin = new DriverPlugin2(new MongoDBDriver({ url: dbUrl }));
2050
+ } else if (dbDriver === "sqlite-wasm") {
2051
+ const { SqliteWasmDriver } = await import("@objectstack/driver-sqlite-wasm");
2052
+ const filename = dbUrl.replace(/^wasm-sqlite:(\/\/)?/i, "").replace(/^file:(\/\/)?/i, "");
2053
+ if (filename && filename !== ":memory:") {
2054
+ mkdirSync2(resolvePath2(filename, ".."), { recursive: true });
2055
+ }
2056
+ driverPlugin = new DriverPlugin2(
2057
+ new SqliteWasmDriver({
2058
+ filename: filename || ":memory:",
2059
+ persist: filename && filename !== ":memory:" ? "on-write" : void 0
2060
+ })
2061
+ );
2062
+ } else {
2063
+ const { SqlDriver } = await import("@objectstack/driver-sql");
2064
+ const filename = dbUrl.replace(/^file:(\/\/)?/, "");
2065
+ if (!filename || /^[a-z][a-z0-9+.-]*:\/\//i.test(filename)) {
2066
+ throw new Error(
2067
+ `[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.`
2068
+ );
2069
+ }
2070
+ mkdirSync2(resolvePath2(filename, ".."), { recursive: true });
2071
+ driverPlugin = new DriverPlugin2(
2072
+ new SqlDriver({
2073
+ client: "better-sqlite3",
2074
+ connection: { filename },
2075
+ useNullAsDefault: true
2076
+ })
2077
+ );
1931
2078
  }
1932
- let currentRedirects = [];
1933
- try {
1934
- const raw = existing.redirect_uris;
1935
- const parsed = typeof raw === "string" ? JSON.parse(raw) : raw;
1936
- if (Array.isArray(parsed)) currentRedirects = parsed.filter((s) => typeof s === "string");
1937
- } catch {
2079
+ const artifactBundle = await loadArtifactBundle(artifactPath, {
2080
+ tag: "[StandaloneStack]",
2081
+ unwrapEnvelope: true
2082
+ });
2083
+ if (artifactBundle) {
2084
+ const flowsCount = Array.isArray(artifactBundle?.flows) ? artifactBundle.flows.length : "n/a";
2085
+ console.warn(
2086
+ `[StandaloneStack] artifact loaded: path=${artifactPath} keys=${Object.keys(artifactBundle).join(",")} flows=${flowsCount}`
2087
+ );
1938
2088
  }
1939
- const mergedRedirects = desiredRedirect && !currentRedirects.includes(desiredRedirect) ? [...currentRedirects, desiredRedirect] : currentRedirects;
1940
- const repairPatch = {
1941
- name: existing.name || `Project ${environmentId}`,
1942
- client_secret: clientSecretStored,
1943
- type: existing.type || "web",
1944
- redirect_uris: JSON.stringify(mergedRedirects),
1945
- grant_types: JSON.stringify(["authorization_code", "refresh_token"]),
1946
- response_types: JSON.stringify(["code"]),
1947
- scopes: JSON.stringify(["openid", "email", "profile"]),
1948
- token_endpoint_auth_method: "client_secret_basic",
1949
- require_pkce: false,
1950
- skip_consent: true,
1951
- disabled: false,
1952
- subject_type: "public",
1953
- updated_at: nowIso
1954
- };
1955
- try {
1956
- await ql.update(
1957
- "sys_oauth_application",
1958
- repairPatch,
1959
- { where: { id: existing.id } },
1960
- { context: { isSystem: true } }
1961
- );
1962
- logger?.info?.("[platform-sso] sys_oauth_application repaired", {
1963
- environmentId,
1964
- clientId,
1965
- redirect_uris: mergedRedirects
1966
- });
1967
- } catch (err) {
1968
- logger?.warn?.("[platform-sso] sys_oauth_application repair failed", {
2089
+ const plugins = [
2090
+ driverPlugin,
2091
+ new MetadataPlugin({
2092
+ // Source-file scanner OFF — declarative metadata is loaded
2093
+ // from the compiled artifact, not from yaml/json files on
2094
+ // disk. Scanning would also recursively watch the project
2095
+ // root (incl. node_modules), which is expensive and prone
2096
+ // to EMFILE.
2097
+ watch: false,
2098
+ // Artifact-file HMR ON in non-production so edits to
2099
+ // `*.view.ts` / `*.flow.ts` (which the CLI dev-mode watcher
2100
+ // recompiles into `dist/objectstack.json`) are picked up by
2101
+ // the running server WITHOUT requiring a manual restart.
2102
+ // Uses polling under the hood (see plugin.ts) to avoid
2103
+ // `fs.watch` EMFILE on macOS / busy dev hosts.
2104
+ artifactWatch: process.env.NODE_ENV !== "production",
1969
2105
  environmentId,
1970
- error: err?.message
1971
- });
1972
- if (throwOnError) throw err;
1973
- }
1974
- }
1975
- async function backfillPlatformSsoClients(opts) {
1976
- const { ql, baseSecret, logger, limit = 1e3 } = opts;
1977
- if (!baseSecret) {
1978
- logger?.warn?.("[platform-sso] backfill skipped \u2014 OS_AUTH_SECRET not set");
1979
- return { scanned: 0, seeded: 0, alreadyExisted: 0, failures: [] };
1980
- }
1981
- let projects = [];
1982
- try {
1983
- const rows = await ql.find("sys_environment", {
1984
- limit,
1985
- fields: ["id", "hostname", "status"]
1986
- }, { context: { isSystem: true } });
1987
- projects = Array.isArray(rows) ? rows : Array.isArray(rows?.records) ? rows.records : [];
1988
- } catch (err) {
1989
- logger?.warn?.("[platform-sso] backfill: sys_environment read failed", {
1990
- error: err?.message
1991
- });
1992
- return { scanned: 0, seeded: 0, alreadyExisted: 0, failures: [{ environmentId: "<scan>", error: err?.message ?? String(err) }] };
1993
- }
1994
- let seeded = 0;
1995
- let alreadyExisted = 0;
1996
- const failures = [];
1997
- for (const p of projects) {
1998
- if (!p?.id) continue;
1999
- const before = await (async () => {
2000
- try {
2001
- const r = await ql.find("sys_oauth_application", {
2002
- where: { client_id: derivePlatformSsoClientId(p.id) },
2003
- limit: 1
2004
- }, { context: { isSystem: true } });
2005
- const list = Array.isArray(r) ? r : Array.isArray(r?.records) ? r.records : [];
2006
- return list[0] ?? null;
2007
- } catch {
2008
- return null;
2009
- }
2010
- })();
2011
- try {
2012
- await seedPlatformSsoClient({ ql, environmentId: p.id, hostname: p.hostname, baseSecret, logger, throwOnError: true });
2013
- if (before) alreadyExisted++;
2014
- else {
2015
- const after = await (async () => {
2016
- try {
2017
- const r = await ql.find("sys_oauth_application", {
2018
- where: { client_id: derivePlatformSsoClientId(p.id) },
2019
- limit: 1
2020
- }, { context: { isSystem: true } });
2021
- const list = Array.isArray(r) ? r : Array.isArray(r?.records) ? r.records : [];
2022
- return list[0] ?? null;
2023
- } catch (err) {
2024
- return { _readErr: err?.message };
2025
- }
2026
- })();
2027
- if (after && !after._readErr) seeded++;
2028
- else failures.push({ environmentId: p.id, error: `post-insert read returned ${after ? JSON.stringify(after) : "null"}` });
2029
- }
2030
- } catch (err) {
2031
- failures.push({ environmentId: p.id, error: err?.message ?? String(err) });
2032
- }
2033
- }
2034
- logger?.info?.("[platform-sso] backfill complete", { scanned: projects.length, seeded, alreadyExisted, failures: failures.length });
2035
- return { scanned: projects.length, seeded, alreadyExisted, failures };
2106
+ artifactSource: { mode: "local-file", path: artifactPath }
2107
+ }),
2108
+ new ObjectQLPlugin({ environmentId })
2109
+ ];
2110
+ if (artifactBundle) plugins.push(new AppPlugin2(artifactBundle));
2111
+ const requires = Array.isArray(artifactBundle?.requires) ? artifactBundle.requires.filter((c) => typeof c === "string") : void 0;
2112
+ const objects = Array.isArray(artifactBundle?.objects) ? artifactBundle.objects : void 0;
2113
+ const manifest = artifactBundle?.manifest;
2114
+ return {
2115
+ plugins,
2116
+ api: {
2117
+ enableProjectScoping: false,
2118
+ projectResolution: "none"
2119
+ },
2120
+ ...requires ? { requires } : {},
2121
+ ...objects ? { objects } : {},
2122
+ ...manifest ? { manifest } : {}
2123
+ };
2036
2124
  }
2037
- var PLATFORM_SSO_PROVIDER_ID;
2038
- var init_platform_sso = __esm({
2039
- "src/cloud/platform-sso.ts"() {
2125
+ var StandaloneStackConfigSchema;
2126
+ var init_standalone_stack = __esm({
2127
+ "src/standalone-stack.ts"() {
2040
2128
  "use strict";
2041
- PLATFORM_SSO_PROVIDER_ID = "objectstack-cloud";
2129
+ init_load_artifact_bundle();
2130
+ StandaloneStackConfigSchema = z.object({
2131
+ databaseUrl: z.string().optional(),
2132
+ databaseAuthToken: z.string().optional(),
2133
+ databaseDriver: z.enum(["sqlite", "sqlite-wasm", "memory", "postgres", "mongodb"]).optional(),
2134
+ environmentId: z.string().optional(),
2135
+ artifactPath: z.string().optional(),
2136
+ /**
2137
+ * Project root directory. When set (typically by the CLI after locating
2138
+ * `objectstack.config.ts`), the default sqlite database is placed under
2139
+ * `<projectRoot>/.objectstack/data/standalone.db` instead of the global
2140
+ * `~/.objectstack/data/standalone.db`. This keeps per-project data
2141
+ * scoped to the project folder so different examples / apps don't
2142
+ * share a single database by accident.
2143
+ *
2144
+ * Explicit `databaseUrl` / `OS_DATABASE_URL` / `OS_HOME` still take
2145
+ * precedence over this default.
2146
+ */
2147
+ projectRoot: z.string().optional()
2148
+ });
2042
2149
  }
2043
2150
  });
2044
2151
 
@@ -2240,228 +2347,236 @@ var Runtime = class {
2240
2347
  }
2241
2348
  };
2242
2349
 
2243
- // src/standalone-stack.ts
2350
+ // src/index.ts
2351
+ init_standalone_stack();
2352
+
2353
+ // src/default-host.ts
2354
+ init_standalone_stack();
2244
2355
  init_load_artifact_bundle();
2245
- import { resolve as resolvePath2 } from "path";
2246
- import { mkdirSync } from "fs";
2247
- import { homedir } from "os";
2248
- import { z } from "zod";
2249
- import { readEnvWithDeprecation as readEnvWithDeprecation2 } from "@objectstack/types";
2250
- function resolveObjectStackHome() {
2251
- const raw = process.env.OS_HOME?.trim();
2252
- if (raw && raw.length > 0) {
2253
- if (raw.startsWith("~")) return resolvePath2(homedir(), raw.slice(1).replace(/^[/\\]/, ""));
2254
- return resolvePath2(raw);
2356
+ import { resolve as resolvePath3 } from "path";
2357
+ import { existsSync as existsSync2, mkdirSync as mkdirSync3, writeFileSync as writeFileSync2 } from "fs";
2358
+ function resolveDefaultArtifactPath(explicitPath, cwd = process.cwd()) {
2359
+ const candidate = explicitPath ?? process.env.OS_ARTIFACT_PATH ?? resolvePath3(cwd, "dist/objectstack.json");
2360
+ if (isHttpUrl(candidate)) return candidate;
2361
+ if (explicitPath || process.env.OS_ARTIFACT_PATH) return candidate;
2362
+ return existsSync2(candidate) ? candidate : void 0;
2363
+ }
2364
+ async function createDefaultHostConfig(options = {}) {
2365
+ const { requireArtifact = true, ...standaloneOpts } = options;
2366
+ let resolvedArtifact = resolveDefaultArtifactPath(standaloneOpts.artifactPath);
2367
+ if (!resolvedArtifact && requireArtifact) {
2368
+ throw new Error(
2369
+ "[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 }`."
2370
+ );
2255
2371
  }
2256
- return resolvePath2(homedir(), ".objectstack");
2372
+ if (!resolvedArtifact && !requireArtifact) {
2373
+ const home = resolveObjectStackHome();
2374
+ const stubPath = resolvePath3(home, "dist/objectstack.json");
2375
+ if (!existsSync2(stubPath)) {
2376
+ mkdirSync3(resolvePath3(stubPath, ".."), { recursive: true });
2377
+ writeFileSync2(
2378
+ stubPath,
2379
+ JSON.stringify(
2380
+ {
2381
+ manifest: {
2382
+ id: "com.objectstack.empty",
2383
+ name: "empty",
2384
+ version: "0.0.0",
2385
+ type: "app",
2386
+ description: "Empty starter kernel \u2014 install apps via the Studio marketplace."
2387
+ },
2388
+ objects: [],
2389
+ views: [],
2390
+ apps: [],
2391
+ flows: [],
2392
+ requires: []
2393
+ },
2394
+ null,
2395
+ 2
2396
+ ),
2397
+ "utf8"
2398
+ );
2399
+ }
2400
+ resolvedArtifact = stubPath;
2401
+ }
2402
+ return createStandaloneStack({
2403
+ ...standaloneOpts,
2404
+ artifactPath: resolvedArtifact
2405
+ });
2257
2406
  }
2258
- var StandaloneStackConfigSchema = z.object({
2259
- databaseUrl: z.string().optional(),
2260
- databaseAuthToken: z.string().optional(),
2261
- databaseDriver: z.enum(["sqlite", "sqlite-wasm", "memory", "postgres", "mongodb"]).optional(),
2262
- environmentId: z.string().optional(),
2263
- artifactPath: z.string().optional(),
2407
+
2408
+ // src/index.ts
2409
+ init_driver_plugin();
2410
+ init_app_plugin();
2411
+ init_seed_loader();
2412
+
2413
+ // src/external-validation-plugin.ts
2414
+ import {
2415
+ ExternalSchemaMismatchError
2416
+ } from "@objectstack/spec/shared";
2417
+ var ExternalValidationPlugin = class {
2418
+ constructor() {
2419
+ this.name = "com.objectstack.external-validation";
2420
+ this.type = "standard";
2421
+ this.version = "1.0.0";
2422
+ /** Active background drift-check timers, keyed by datasource name. */
2423
+ this.driftTimers = /* @__PURE__ */ new Map();
2424
+ this.init = (_ctx) => {
2425
+ };
2426
+ this.start = (ctx) => {
2427
+ ctx.hook("kernel:ready", async () => {
2428
+ await this.runValidation(ctx);
2429
+ await this.scheduleDriftChecks(ctx);
2430
+ });
2431
+ };
2432
+ /** Tear down background drift-check timers (idempotent). */
2433
+ this.stop = () => {
2434
+ for (const timer of this.driftTimers.values()) clearInterval(timer);
2435
+ this.driftTimers.clear();
2436
+ };
2437
+ }
2438
+ /** Exposed for testing; invoked from the kernel:ready handler. */
2439
+ async runValidation(ctx) {
2440
+ const svc = safeGet(ctx, "external-datasource");
2441
+ if (!svc?.validateAll) {
2442
+ ctx.logger?.debug?.("[external-validation] service not registered; skipping");
2443
+ return;
2444
+ }
2445
+ const metadata = safeGet(ctx, "metadata");
2446
+ let report;
2447
+ try {
2448
+ report = await svc.validateAll();
2449
+ } catch (err) {
2450
+ ctx.logger?.warn?.("[external-validation] validateAll failed", { err });
2451
+ return;
2452
+ }
2453
+ const failures = report.results.filter((r) => !r.ok);
2454
+ if (failures.length === 0) {
2455
+ ctx.logger?.info?.("[external-validation] all federated objects match their remote schema", {
2456
+ objects: report.results.length
2457
+ });
2458
+ return;
2459
+ }
2460
+ for (const r of failures) {
2461
+ const mode = await resolveOnMismatch(metadata, r.datasource);
2462
+ if (mode === "ignore") continue;
2463
+ if (mode === "warn") {
2464
+ ctx.logger?.warn?.("[external-validation] external schema drift", {
2465
+ datasource: r.datasource,
2466
+ object: r.object,
2467
+ diffs: r.diffs
2468
+ });
2469
+ continue;
2470
+ }
2471
+ throw new ExternalSchemaMismatchError(r.datasource, r.object, r.diffs);
2472
+ }
2473
+ }
2264
2474
  /**
2265
- * Project root directory. When set (typically by the CLI after locating
2266
- * `objectstack.config.ts`), the default sqlite database is placed under
2267
- * `<projectRoot>/.objectstack/data/standalone.db` instead of the global
2268
- * `~/.objectstack/data/standalone.db`. This keeps per-project data
2269
- * scoped to the project folder so different examples / apps don't
2270
- * share a single database by accident.
2475
+ * Arm a background drift checker for every federated datasource that declares
2476
+ * `external.validation.checkIntervalMs`. Each fires on its own interval and
2477
+ * emits `external.schema.drift` events it never throws or aborts the
2478
+ * process, since drift past boot is observational, not fatal.
2271
2479
  *
2272
- * Explicit `databaseUrl` / `OS_DATABASE_URL` / `OS_HOME` still take
2273
- * precedence over this default.
2480
+ * No-op when metadata can't be enumerated or no datasource opts in. Re-arming
2481
+ * (e.g. a second `kernel:ready`) first clears existing timers so intervals
2482
+ * don't accumulate.
2274
2483
  */
2275
- projectRoot: z.string().optional()
2276
- });
2277
- function detectDriverFromUrl(dbUrl) {
2278
- if (/^memory:\/\//i.test(dbUrl)) return "memory";
2279
- if (/^(postgres(ql)?|pg):\/\//i.test(dbUrl)) return "postgres";
2280
- if (/^mongodb(\+srv)?:\/\//i.test(dbUrl)) return "mongodb";
2281
- if (/^wasm-sqlite:\/\//i.test(dbUrl)) return "sqlite-wasm";
2282
- if (/\.wasm\.db$/i.test(dbUrl)) return "sqlite-wasm";
2283
- if (/^file:/i.test(dbUrl)) return "sqlite";
2284
- if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(dbUrl)) return "sqlite";
2285
- throw new Error(
2286
- `[StandaloneStack] Unsupported database URL scheme: ${dbUrl}. Supported schemes: memory://, postgres://, pg://, mongodb://, mongodb+srv://, file:`
2287
- );
2288
- }
2289
- async function createStandaloneStack(config) {
2290
- const cfg = StandaloneStackConfigSchema.parse(config ?? {});
2291
- const { ObjectQLPlugin } = await import("@objectstack/objectql");
2292
- const { MetadataPlugin } = await import("@objectstack/metadata");
2293
- const { DriverPlugin: DriverPlugin2 } = await Promise.resolve().then(() => (init_driver_plugin(), driver_plugin_exports));
2294
- const { AppPlugin: AppPlugin2 } = await Promise.resolve().then(() => (init_app_plugin(), app_plugin_exports));
2295
- const cwd = process.cwd();
2296
- const environmentId = cfg.environmentId ?? process.env.OS_ENVIRONMENT_ID ?? "proj_local";
2297
- const artifactPathInput = cfg.artifactPath ?? process.env.OS_ARTIFACT_PATH ?? resolvePath2(cwd, "dist/objectstack.json");
2298
- const artifactPath = isHttpUrl(artifactPathInput) ? artifactPathInput : artifactPathInput.startsWith("/") ? artifactPathInput : resolvePath2(cwd, artifactPathInput);
2299
- const dbUrl = cfg.databaseUrl ?? readEnvWithDeprecation2("OS_DATABASE_URL", "DATABASE_URL")?.trim() ?? process.env.TURSO_DATABASE_URL?.trim() ?? (process.env.OS_HOME?.trim() ? `file:${resolvePath2(resolveObjectStackHome(), "data/standalone.db")}` : cfg.projectRoot ? `file:${resolvePath2(cfg.projectRoot, ".objectstack/data/standalone.db")}` : `file:${resolvePath2(resolveObjectStackHome(), "data/standalone.db")}`);
2300
- const explicitDriver = cfg.databaseDriver ?? process.env.OS_DATABASE_DRIVER?.trim();
2301
- const dbDriver = explicitDriver ?? detectDriverFromUrl(dbUrl);
2302
- let driverPlugin;
2303
- if (dbDriver === "memory") {
2304
- const { InMemoryDriver } = await import("@objectstack/driver-memory");
2305
- driverPlugin = new DriverPlugin2(new InMemoryDriver());
2306
- } else if (dbDriver === "postgres") {
2307
- const { SqlDriver } = await import("@objectstack/driver-sql");
2308
- driverPlugin = new DriverPlugin2(
2309
- new SqlDriver({
2310
- client: "pg",
2311
- connection: dbUrl,
2312
- pool: { min: 0, max: 5 }
2313
- })
2314
- );
2315
- } else if (dbDriver === "mongodb") {
2316
- let MongoDBDriver;
2484
+ async scheduleDriftChecks(ctx) {
2485
+ this.stop();
2486
+ const metadata = safeGet(ctx, "metadata");
2487
+ if (!metadata?.list) return;
2488
+ let datasources;
2317
2489
  try {
2318
- ({ MongoDBDriver } = await import("@objectstack/driver-mongodb"));
2490
+ datasources = await metadata.list("datasource");
2319
2491
  } catch (err) {
2320
- throw new Error(
2321
- `[StandaloneStack] mongodb URL detected but @objectstack/driver-mongodb is not installed. Add it as a dependency or pass an explicit driverPlugin. (${err?.message ?? err})`
2322
- );
2323
- }
2324
- driverPlugin = new DriverPlugin2(new MongoDBDriver({ url: dbUrl }));
2325
- } else if (dbDriver === "sqlite-wasm") {
2326
- const { SqliteWasmDriver } = await import("@objectstack/driver-sqlite-wasm");
2327
- const filename = dbUrl.replace(/^wasm-sqlite:(\/\/)?/i, "").replace(/^file:(\/\/)?/i, "");
2328
- if (filename && filename !== ":memory:") {
2329
- mkdirSync(resolvePath2(filename, ".."), { recursive: true });
2492
+ ctx.logger?.warn?.("[external-validation] could not list datasources for drift checks", { err });
2493
+ return;
2330
2494
  }
2331
- driverPlugin = new DriverPlugin2(
2332
- new SqliteWasmDriver({
2333
- filename: filename || ":memory:",
2334
- persist: filename && filename !== ":memory:" ? "on-write" : void 0
2335
- })
2336
- );
2337
- } else {
2338
- const { SqlDriver } = await import("@objectstack/driver-sql");
2339
- const filename = dbUrl.replace(/^file:(\/\/)?/, "");
2340
- if (!filename || /^[a-z][a-z0-9+.-]*:\/\//i.test(filename)) {
2341
- throw new Error(
2342
- `[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.`
2343
- );
2495
+ for (const def of datasources) {
2496
+ const interval = def?.external?.validation?.checkIntervalMs;
2497
+ const name = def?.name;
2498
+ if (!name || typeof interval !== "number" || interval <= 0) continue;
2499
+ const timer = setInterval(() => {
2500
+ void this.runDriftCheck(ctx, name);
2501
+ }, interval);
2502
+ timer.unref?.();
2503
+ this.driftTimers.set(name, timer);
2504
+ ctx.logger?.info?.("[external-validation] armed background drift check", {
2505
+ datasource: name,
2506
+ intervalMs: interval
2507
+ });
2344
2508
  }
2345
- mkdirSync(resolvePath2(filename, ".."), { recursive: true });
2346
- driverPlugin = new DriverPlugin2(
2347
- new SqlDriver({
2348
- client: "better-sqlite3",
2349
- connection: { filename },
2350
- useNullAsDefault: true
2351
- })
2352
- );
2353
2509
  }
2354
- const artifactBundle = await loadArtifactBundle(artifactPath, {
2355
- tag: "[StandaloneStack]",
2356
- unwrapEnvelope: true
2357
- });
2358
- if (artifactBundle) {
2359
- const flowsCount = Array.isArray(artifactBundle?.flows) ? artifactBundle.flows.length : "n/a";
2360
- console.warn(
2361
- `[StandaloneStack] artifact loaded: path=${artifactPath} keys=${Object.keys(artifactBundle).join(",")} flows=${flowsCount}`
2362
- );
2510
+ /**
2511
+ * Re-validate one datasource's federated objects and emit an
2512
+ * `external.schema.drift` event per mismatch. Exposed for testing; invoked
2513
+ * from the interval armed by {@link scheduleDriftChecks}. Never throws.
2514
+ *
2515
+ * @returns the number of drift events emitted.
2516
+ */
2517
+ async runDriftCheck(ctx, datasource) {
2518
+ const svc = safeGet(ctx, "external-datasource");
2519
+ if (!svc?.validateAll) return 0;
2520
+ let report;
2521
+ try {
2522
+ report = await svc.validateAll();
2523
+ } catch (err) {
2524
+ ctx.logger?.warn?.("[external-validation] drift check validateAll failed", {
2525
+ datasource,
2526
+ err
2527
+ });
2528
+ return 0;
2529
+ }
2530
+ const drifted = report.results.filter((r) => !r.ok && r.datasource === datasource);
2531
+ for (const r of drifted) {
2532
+ const event = {
2533
+ datasource: r.datasource,
2534
+ object: r.object,
2535
+ diffs: r.diffs
2536
+ };
2537
+ try {
2538
+ await ctx.trigger("external.schema.drift", event);
2539
+ } catch (err) {
2540
+ ctx.logger?.warn?.("[external-validation] failed to emit drift event", {
2541
+ datasource,
2542
+ object: r.object,
2543
+ err
2544
+ });
2545
+ }
2546
+ }
2547
+ if (drifted.length > 0) {
2548
+ ctx.logger?.warn?.("[external-validation] background drift detected", {
2549
+ datasource,
2550
+ objects: drifted.map((r) => r.object)
2551
+ });
2552
+ }
2553
+ return drifted.length;
2363
2554
  }
2364
- const plugins = [
2365
- driverPlugin,
2366
- new MetadataPlugin({
2367
- // Source-file scanner OFF — declarative metadata is loaded
2368
- // from the compiled artifact, not from yaml/json files on
2369
- // disk. Scanning would also recursively watch the project
2370
- // root (incl. node_modules), which is expensive and prone
2371
- // to EMFILE.
2372
- watch: false,
2373
- // Artifact-file HMR ON in non-production so edits to
2374
- // `*.view.ts` / `*.flow.ts` (which the CLI dev-mode watcher
2375
- // recompiles into `dist/objectstack.json`) are picked up by
2376
- // the running server WITHOUT requiring a manual restart.
2377
- // Uses polling under the hood (see plugin.ts) to avoid
2378
- // `fs.watch` EMFILE on macOS / busy dev hosts.
2379
- artifactWatch: process.env.NODE_ENV !== "production",
2380
- environmentId,
2381
- artifactSource: { mode: "local-file", path: artifactPath }
2382
- }),
2383
- new ObjectQLPlugin({ environmentId })
2384
- ];
2385
- if (artifactBundle) plugins.push(new AppPlugin2(artifactBundle));
2386
- const requires = Array.isArray(artifactBundle?.requires) ? artifactBundle.requires.filter((c) => typeof c === "string") : void 0;
2387
- const objects = Array.isArray(artifactBundle?.objects) ? artifactBundle.objects : void 0;
2388
- const manifest = artifactBundle?.manifest;
2389
- return {
2390
- plugins,
2391
- api: {
2392
- enableProjectScoping: false,
2393
- projectResolution: "none"
2394
- },
2395
- ...requires ? { requires } : {},
2396
- ...objects ? { objects } : {},
2397
- ...manifest ? { manifest } : {}
2398
- };
2399
- }
2400
-
2401
- // src/default-host.ts
2402
- import { resolve as resolvePath3 } from "path";
2403
- import { existsSync, mkdirSync as mkdirSync2, writeFileSync } from "fs";
2404
- init_load_artifact_bundle();
2405
- function resolveDefaultArtifactPath(explicitPath, cwd = process.cwd()) {
2406
- const candidate = explicitPath ?? process.env.OS_ARTIFACT_PATH ?? resolvePath3(cwd, "dist/objectstack.json");
2407
- if (isHttpUrl(candidate)) return candidate;
2408
- if (explicitPath || process.env.OS_ARTIFACT_PATH) return candidate;
2409
- return existsSync(candidate) ? candidate : void 0;
2555
+ };
2556
+ function createExternalValidationPlugin() {
2557
+ return new ExternalValidationPlugin();
2410
2558
  }
2411
- async function createDefaultHostConfig(options = {}) {
2412
- const { requireArtifact = true, ...standaloneOpts } = options;
2413
- let resolvedArtifact = resolveDefaultArtifactPath(standaloneOpts.artifactPath);
2414
- if (!resolvedArtifact && requireArtifact) {
2415
- throw new Error(
2416
- "[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 }`."
2417
- );
2559
+ async function resolveOnMismatch(metadata, datasource) {
2560
+ try {
2561
+ const ds = await metadata?.get?.("datasource", datasource);
2562
+ return ds?.external?.validation?.onMismatch ?? "fail";
2563
+ } catch {
2564
+ return "fail";
2418
2565
  }
2419
- if (!resolvedArtifact && !requireArtifact) {
2420
- const home = resolveObjectStackHome();
2421
- const stubPath = resolvePath3(home, "dist/objectstack.json");
2422
- if (!existsSync(stubPath)) {
2423
- mkdirSync2(resolvePath3(stubPath, ".."), { recursive: true });
2424
- writeFileSync(
2425
- stubPath,
2426
- JSON.stringify(
2427
- {
2428
- manifest: {
2429
- id: "com.objectstack.empty",
2430
- name: "empty",
2431
- version: "0.0.0",
2432
- type: "app",
2433
- description: "Empty starter kernel \u2014 install apps via the Studio marketplace."
2434
- },
2435
- objects: [],
2436
- views: [],
2437
- apps: [],
2438
- flows: [],
2439
- requires: []
2440
- },
2441
- null,
2442
- 2
2443
- ),
2444
- "utf8"
2445
- );
2446
- }
2447
- resolvedArtifact = stubPath;
2566
+ }
2567
+ function safeGet(ctx, name) {
2568
+ try {
2569
+ return ctx.getService(name);
2570
+ } catch {
2571
+ return void 0;
2448
2572
  }
2449
- return createStandaloneStack({
2450
- ...standaloneOpts,
2451
- artifactPath: resolvedArtifact
2452
- });
2453
2573
  }
2454
2574
 
2455
- // src/index.ts
2456
- init_driver_plugin();
2457
- init_app_plugin();
2458
- init_seed_loader();
2459
-
2460
2575
  // src/http-dispatcher.ts
2576
+ init_package_state_store();
2461
2577
  import { getEnv, resolveLocale } from "@objectstack/core";
2462
- import { readEnvWithDeprecation as readEnvWithDeprecation3 } from "@objectstack/types";
2463
2578
  import { CoreServiceName } from "@objectstack/spec/system";
2464
- import { pluralToSingular } from "@objectstack/spec/shared";
2579
+ import { pluralToSingular, PLURAL_TO_SINGULAR } from "@objectstack/spec/shared";
2465
2580
 
2466
2581
  // src/security/resolve-execution-context.ts
2467
2582
  function readHeader(headers, name) {
@@ -3288,6 +3403,17 @@ var _HttpDispatcher = class _HttpDispatcher {
3288
3403
  }
3289
3404
  return { handled: true, response: this.success({ types: ["object", "app", "plugin"] }) };
3290
3405
  }
3406
+ if (parts.length === 4 && (parts[0] === "objects" || parts[0] === "object") && parts[2] === "state" && (!method || method === "GET")) {
3407
+ const name = parts[1];
3408
+ const field = parts[3];
3409
+ const from = query?.from !== void 0 ? String(query.from) : void 0;
3410
+ const qlService = await this.getObjectQLService();
3411
+ const schema = qlService?.registry?.getObject(name);
3412
+ if (!schema) return { handled: true, response: this.error("Object not found", 404) };
3413
+ const { legalNextStates } = await import("@objectstack/objectql");
3414
+ const next = from === void 0 ? null : legalNextStates(schema, field, from);
3415
+ return { handled: true, response: this.success({ object: name, field, from: from ?? null, next }) };
3416
+ }
3291
3417
  if (parts.length >= 3 && parts[parts.length - 1] === "published" && (!method || method === "GET")) {
3292
3418
  const type = parts[0];
3293
3419
  const name = parts.slice(1, -1).join("/");
@@ -3316,7 +3442,7 @@ var _HttpDispatcher = class _HttpDispatcher {
3316
3442
  if (protocol && typeof protocol.saveMetaItem === "function") {
3317
3443
  try {
3318
3444
  const organizationId = await this.resolveActiveOrganizationId(_context);
3319
- const result = await protocol.saveMetaItem({ type, name, item: body, organizationId });
3445
+ const result = await protocol.saveMetaItem({ type, name, item: body, organizationId, ...packageId ? { packageId } : {} });
3320
3446
  return { handled: true, response: this.success(result) };
3321
3447
  } catch (e) {
3322
3448
  return { handled: true, response: this.error(e.message, 400) };
@@ -3656,12 +3782,22 @@ var _HttpDispatcher = class _HttpDispatcher {
3656
3782
  const id = decodeURIComponent(parts[0]);
3657
3783
  const pkg = registry.enablePackage(id);
3658
3784
  if (!pkg) return { handled: true, response: this.error(`Package '${id}' not found`, 404) };
3785
+ try {
3786
+ setPackageDisabled(_context?.environmentId, id, false);
3787
+ } catch (err) {
3788
+ console.warn("[handlePackages] failed to persist enable state", { id, error: err?.message });
3789
+ }
3659
3790
  return { handled: true, response: this.success(pkg) };
3660
3791
  }
3661
3792
  if (parts.length === 2 && parts[1] === "disable" && m === "PATCH") {
3662
3793
  const id = decodeURIComponent(parts[0]);
3663
3794
  const pkg = registry.disablePackage(id);
3664
3795
  if (!pkg) return { handled: true, response: this.error(`Package '${id}' not found`, 404) };
3796
+ try {
3797
+ setPackageDisabled(_context?.environmentId, id, true);
3798
+ } catch (err) {
3799
+ console.warn("[handlePackages] failed to persist disable state", { id, error: err?.message });
3800
+ }
3665
3801
  return { handled: true, response: this.success(pkg) };
3666
3802
  }
3667
3803
  if (parts.length === 2 && parts[1] === "publish" && m === "POST") {
@@ -3682,6 +3818,14 @@ var _HttpDispatcher = class _HttpDispatcher {
3682
3818
  }
3683
3819
  return { handled: true, response: this.error("Metadata service not available", 503) };
3684
3820
  }
3821
+ if (parts.length === 2 && parts[1] === "export" && m === "GET") {
3822
+ const id = decodeURIComponent(parts[0]);
3823
+ const manifest = await this.assemblePackageManifest(id, registry, _context);
3824
+ if (!manifest) {
3825
+ return { handled: true, response: this.error(`Package '${id}' not found`, 404) };
3826
+ }
3827
+ return { handled: true, response: this.success(manifest) };
3828
+ }
3685
3829
  if (parts.length === 1 && m === "GET") {
3686
3830
  const id = decodeURIComponent(parts[0]);
3687
3831
  const pkg = registry.getPackage(id);
@@ -3699,6 +3843,83 @@ var _HttpDispatcher = class _HttpDispatcher {
3699
3843
  }
3700
3844
  return { handled: false };
3701
3845
  }
3846
+ /**
3847
+ * Assemble a portable, offline-installable package manifest from the
3848
+ * `sys_metadata` overlay rows bound to `packageId`.
3849
+ *
3850
+ * The resulting shape mirrors what `marketplace-install-local` →
3851
+ * `manifestService.register()` → `engine.registerApp()` consumes:
3852
+ * `{ id, name, version, objects:[…], views:[…], flows:[…], … }`
3853
+ * where each category key is the PLURAL manifest name and its value is
3854
+ * an array of clean metadata bodies (provenance decorations stripped).
3855
+ *
3856
+ * Only the metadata categories that `registerApp` can actually consume
3857
+ * are exported. `datasources` and `emailTemplates` are intentionally
3858
+ * excluded (not registered by the import path). `tools` / `skills` ARE
3859
+ * round-tripped: they are registered by `registerApp` on import and
3860
+ * surfaced by `getMetaItems('tool' | 'skill')` on export.
3861
+ *
3862
+ * @returns the manifest object, or `null` if the package id is unknown
3863
+ * AND has no overlay-authored metadata.
3864
+ */
3865
+ async assemblePackageManifest(packageId, registry, context) {
3866
+ const protocol = await this.resolveService("protocol");
3867
+ if (!protocol || typeof protocol.getMetaItems !== "function") return null;
3868
+ const organizationId = await this.resolveActiveOrganizationId(context);
3869
+ const PROVENANCE_KEYS = /* @__PURE__ */ new Set([
3870
+ "_packageId",
3871
+ "_packageVersionId",
3872
+ "_provenance",
3873
+ "_state",
3874
+ "_version",
3875
+ "_organizationId",
3876
+ "_source",
3877
+ "_id",
3878
+ "_rowId"
3879
+ ]);
3880
+ const clean = (item) => {
3881
+ if (!item || typeof item !== "object") return item;
3882
+ const out = {};
3883
+ for (const [k, v] of Object.entries(item)) {
3884
+ if (k.startsWith("_") || PROVENANCE_KEYS.has(k)) continue;
3885
+ out[k] = v;
3886
+ }
3887
+ return out;
3888
+ };
3889
+ const exportPluralKeys = Object.keys(PLURAL_TO_SINGULAR).filter(
3890
+ (k) => k !== "datasources" && k !== "emailTemplates"
3891
+ );
3892
+ const manifest = {};
3893
+ let total = 0;
3894
+ for (const plural of exportPluralKeys) {
3895
+ const singular = PLURAL_TO_SINGULAR[plural];
3896
+ let items = [];
3897
+ try {
3898
+ const res = await protocol.getMetaItems({ type: singular, packageId, organizationId });
3899
+ items = Array.isArray(res?.items) ? res.items : [];
3900
+ } catch {
3901
+ continue;
3902
+ }
3903
+ if (items.length === 0) continue;
3904
+ manifest[plural] = items.map(clean);
3905
+ total += items.length;
3906
+ }
3907
+ const pkg = (() => {
3908
+ try {
3909
+ return registry?.getPackage?.(packageId);
3910
+ } catch {
3911
+ return void 0;
3912
+ }
3913
+ })();
3914
+ if (total === 0 && !pkg) return null;
3915
+ manifest.id = packageId;
3916
+ manifest.name = pkg?.manifest?.name ?? pkg?.name ?? packageId;
3917
+ manifest.version = pkg?.manifest?.version ?? pkg?.version ?? "1.0.0";
3918
+ if (pkg?.manifest?.label ?? pkg?.label) {
3919
+ manifest.label = pkg?.manifest?.label ?? pkg?.label;
3920
+ }
3921
+ return manifest;
3922
+ }
3702
3923
  /**
3703
3924
  * Cloud / Environment Control-Plane routes.
3704
3925
  *
@@ -3712,1350 +3933,59 @@ var _HttpDispatcher = class _HttpDispatcher {
3712
3933
  * - POST /cloud/environments/:id/retry → re-run provisioning for a failed environment
3713
3934
  * - POST /cloud/environments/:id/activate → mark as active for session (stub)
3714
3935
  * - POST /cloud/environments/:id/credentials/rotate → rotate credential
3715
- * - GET /cloud/environments/:id/members → list members
3716
- * - GET /cloud/environments/:id/packages → list installed packages
3717
- * - POST /cloud/environments/:id/packages → install package into env
3718
- * - GET /cloud/environments/:id/packages/:pkgId → get installation detail
3719
- * - PATCH /cloud/environments/:id/packages/:pkgId/enable → enable package
3720
- * - PATCH /cloud/environments/:id/packages/:pkgId/disable → disable package
3721
- * - DELETE /cloud/environments/:id/packages/:pkgId → uninstall (scope=platform forbidden)
3722
- * - POST /cloud/environments/:id/packages/:pkgId/upgrade → upgrade to newer version
3723
- *
3724
- * Driver binding
3725
- * --------------
3726
- * Environments are not tied to any specific driver. At provisioning time the
3727
- * caller passes `driver` (a short name such as `memory`, `turso`, or any
3728
- * future `sql` / `postgres` driver). The dispatcher validates the name
3729
- * against the kernel's registered driver services (`driver.<name>`) and
3730
- * derives an appropriate placeholder `database_url` for the chosen driver.
3731
- * If `driver` is omitted, the dispatcher auto-selects the first available
3732
- * in preference order: turso → memory → any other registered driver.
3733
- *
3734
- * Backed by ObjectQL sys_environment / sys_environment_credential /
3735
- * sys_environment_member tables (registered by
3736
- * `@objectstack/service-tenant`'s `createTenantPlugin`).
3737
- * Physical database addressing (database_url, database_driver, etc.)
3738
- * is stored directly on the sys_environment row.
3739
- */
3740
- /**
3741
- * Resolve the calling user id from the request session, if any.
3742
- * Returns `undefined` for anonymous calls or when auth is not wired up.
3743
- */
3744
- async resolveActiveOrganizationId(context) {
3745
- try {
3746
- const authService = await this.resolveService(CoreServiceName.enum.auth);
3747
- const rawHeaders = context.request?.headers;
3748
- let headers = rawHeaders;
3749
- if (rawHeaders && typeof rawHeaders === "object" && typeof rawHeaders.get !== "function") {
3750
- try {
3751
- const h = new Headers();
3752
- for (const [k, v] of Object.entries(rawHeaders)) {
3753
- if (v == null) continue;
3754
- h.set(k, Array.isArray(v) ? v.join(", ") : String(v));
3755
- }
3756
- headers = h;
3757
- } catch {
3758
- headers = rawHeaders;
3759
- }
3760
- }
3761
- const apiObj = authService?.auth?.api ?? authService?.api;
3762
- const sessionData = await apiObj?.getSession?.call(apiObj, { headers });
3763
- const oid = sessionData?.session?.activeOrganizationId;
3764
- return typeof oid === "string" && oid.length > 0 ? oid : void 0;
3765
- } catch {
3766
- return void 0;
3767
- }
3768
- }
3769
- async resolveCallerUserId(context) {
3770
- try {
3771
- const authService = await this.resolveService(CoreServiceName.enum.auth);
3772
- const rawHeaders = context.request?.headers;
3773
- let headers = rawHeaders;
3774
- if (rawHeaders && typeof rawHeaders === "object" && typeof rawHeaders.get !== "function") {
3775
- try {
3776
- const h = new Headers();
3777
- for (const [k, v] of Object.entries(rawHeaders)) {
3778
- if (v == null) continue;
3779
- h.set(k, Array.isArray(v) ? v.join(", ") : String(v));
3780
- }
3781
- headers = h;
3782
- } catch {
3783
- headers = rawHeaders;
3784
- }
3785
- }
3786
- const sessionData = await (authService?.auth?.api?.getSession ?? authService?.api?.getSession)?.call(
3787
- authService?.auth?.api ?? authService?.api,
3788
- { headers }
3789
- );
3790
- return sessionData?.user?.id ?? sessionData?.session?.userId;
3791
- } catch (e) {
3792
- return void 0;
3793
- }
3794
- }
3795
- async handleCloud(path, method, body, query, _context) {
3796
- const m = method.toUpperCase();
3797
- const parts = path.replace(/^\/+/, "").split("/").filter(Boolean);
3798
- const qlService = await this.getObjectQLService();
3799
- const ql = qlService ?? await this.resolveService("objectql");
3800
- if (!ql) {
3801
- return { handled: true, response: this.error("Project service not available (ObjectQL missing)", 503) };
3802
- }
3803
- const ENV = "sys_environment";
3804
- const CRED = "sys_environment_credential";
3805
- const MEM = "sys_environment_member";
3806
- const PKG_INSTALL = "sys_package_installation";
3807
- const PKG = "sys_package";
3808
- const PKG_VERSION = "sys_package_version";
3809
- const ensureSysPackage = async (manifestId, ownerOrgId, createdBy, manifest) => {
3810
- const existing = await ql.findOne(PKG, { where: { manifest_id: manifestId } });
3811
- if (existing?.id) return existing.id;
3812
- const id = randomUUID();
3813
- const nowIso = (/* @__PURE__ */ new Date()).toISOString();
3814
- await ql.insert(PKG, {
3815
- id,
3816
- manifest_id: manifestId,
3817
- owner_org_id: ownerOrgId,
3818
- display_name: manifest?.name ?? manifestId,
3819
- description: manifest?.description ?? null,
3820
- visibility: "private",
3821
- created_by: createdBy,
3822
- created_at: nowIso,
3823
- updated_at: nowIso
3824
- });
3825
- return id;
3826
- };
3827
- const ensureSysPackageVersion = async (packageId, version, createdBy, manifest) => {
3828
- const existing = await ql.findOne(PKG_VERSION, {
3829
- where: { package_id: packageId, version }
3830
- });
3831
- if (existing?.id) return existing.id;
3832
- const id = randomUUID();
3833
- const nowIso = (/* @__PURE__ */ new Date()).toISOString();
3834
- await ql.insert(PKG_VERSION, {
3835
- id,
3836
- package_id: packageId,
3837
- version,
3838
- status: "published",
3839
- manifest_json: manifest ? JSON.stringify(manifest) : null,
3840
- is_pre_release: false,
3841
- published_at: nowIso,
3842
- published_by: createdBy,
3843
- created_by: createdBy,
3844
- created_at: nowIso,
3845
- updated_at: nowIso
3846
- });
3847
- return id;
3848
- };
3849
- const findInstallByManifestId = async (envId, manifestId) => {
3850
- const pkgRow = await ql.findOne(PKG, { where: { manifest_id: manifestId } });
3851
- if (!pkgRow?.id) return null;
3852
- return await ql.findOne(PKG_INSTALL, {
3853
- where: { environment_id: envId, package_id: pkgRow.id }
3854
- });
3855
- };
3856
- const toShortName = (driverId) => {
3857
- const prefix = "com.objectstack.driver.";
3858
- return driverId.startsWith(prefix) ? driverId.slice(prefix.length) : driverId;
3859
- };
3860
- const listRegisteredDrivers = () => {
3861
- const services = this.getServicesMap();
3862
- const registry = services["project-provisioning-adapters"];
3863
- if (registry && typeof registry.list === "function") {
3864
- try {
3865
- const adapters = registry.list();
3866
- const seen = /* @__PURE__ */ new Set();
3867
- const drivers2 = [];
3868
- for (const adapter of adapters ?? []) {
3869
- const name = adapter?.driver;
3870
- if (!name || seen.has(name)) continue;
3871
- seen.add(name);
3872
- drivers2.push({ name, driverId: `com.objectstack.driver.${name}` });
3873
- }
3874
- if (drivers2.length > 0) return drivers2;
3875
- } catch {
3876
- }
3877
- }
3878
- const drivers = [];
3879
- for (const [serviceKey, svc] of Object.entries(services)) {
3880
- if (!serviceKey.startsWith("driver.")) continue;
3881
- const raw = serviceKey.slice("driver.".length);
3882
- if (!raw || raw === "unknown") continue;
3883
- const driverId = svc?.name ?? raw;
3884
- drivers.push({ name: toShortName(driverId), driverId });
3885
- }
3886
- return drivers;
3887
- };
3888
- const resolveDriver = (requested) => {
3889
- const registered = listRegisteredDrivers();
3890
- if (requested) {
3891
- const wanted = String(requested).toLowerCase();
3892
- return registered.find((d) => d.name === wanted || d.driverId === wanted);
3893
- }
3894
- return registered.find((d) => d.name === "turso") ?? registered.find((d) => d.name === "memory") ?? registered[0];
3895
- };
3896
- const buildDatabaseUrl = (driverName, environmentId) => {
3897
- const dbName = `env-${environmentId}`;
3898
- switch (driverName) {
3899
- case "memory":
3900
- return `memory://${dbName}`;
3901
- case "turso":
3902
- return `libsql://${dbName}.mock-turso.local`;
3903
- default:
3904
- return `${driverName}://${dbName}`;
3905
- }
3906
- };
3907
- const getRealAdapter = async (driverName) => {
3908
- try {
3909
- const registry = await this.resolveService("project-provisioning-adapters");
3910
- const aliases = { sql: "sqlite" };
3911
- const effective = aliases[driverName] ?? driverName;
3912
- return registry?.get?.(effective) ?? registry?.get?.(driverName);
3913
- } catch {
3914
- return void 0;
3915
- }
3916
- };
3917
- const findOne = async (obj, where) => {
3918
- let rows = await ql.find(obj, { where });
3919
- if (rows && rows.value) rows = rows.value;
3920
- if (!Array.isArray(rows)) return void 0;
3921
- return rows[0];
3922
- };
3923
- const cleanProjectRow = (row) => {
3924
- if (!row) return row;
3925
- let metadata = row.metadata;
3926
- if (typeof metadata === "string") {
3927
- try {
3928
- metadata = JSON.parse(metadata);
3929
- } catch {
3930
- }
3931
- }
3932
- return { ...row, metadata };
3933
- };
3934
- try {
3935
- if (parts.length === 1 && parts[0] === "drivers" && m === "GET") {
3936
- const drivers = listRegisteredDrivers();
3937
- return { handled: true, response: this.success({ drivers, total: drivers.length }) };
3938
- }
3939
- if (parts.length === 1 && parts[0] === "templates" && m === "GET") {
3940
- try {
3941
- const seeder = await this.resolveService("template-seeder");
3942
- const templates = seeder?.listTemplates?.() ?? [];
3943
- return { handled: true, response: this.success({ templates, total: templates.length }) };
3944
- } catch (err) {
3945
- try {
3946
- console.error("[HttpDispatcher] /cloud/templates: failed to resolve template-seeder:", err?.message ?? err);
3947
- } catch {
3948
- }
3949
- return { handled: true, response: this.success({ templates: [], total: 0 }) };
3950
- }
3951
- }
3952
- if (parts.length === 3 && parts[0] === "admin" && parts[1] === "platform-sso" && parts[2] === "backfill" && m === "POST") {
3953
- const baseSecret = (readEnvWithDeprecation3("OS_AUTH_SECRET", ["AUTH_SECRET", "BETTER_AUTH_SECRET"]) ?? "").trim();
3954
- if (!baseSecret) {
3955
- return { handled: true, response: this.error("OS_AUTH_SECRET not configured on this worker", 503) };
3956
- }
3957
- const rawHeaders = _context?.request?.headers;
3958
- let authHeader;
3959
- if (rawHeaders && typeof rawHeaders.get === "function") {
3960
- authHeader = rawHeaders.get("authorization") ?? void 0;
3961
- } else if (rawHeaders && typeof rawHeaders === "object") {
3962
- authHeader = rawHeaders["authorization"] ?? rawHeaders["Authorization"];
3963
- }
3964
- const presented = typeof authHeader === "string" && authHeader.startsWith("Bearer ") ? authHeader.slice(7).trim() : "";
3965
- if (!presented || presented !== baseSecret) {
3966
- return { handled: true, response: this.error("forbidden: Bearer token must match OS_AUTH_SECRET", 403) };
3967
- }
3968
- try {
3969
- const { backfillPlatformSsoClients: backfillPlatformSsoClients2 } = await Promise.resolve().then(() => (init_platform_sso(), platform_sso_exports));
3970
- const result = await backfillPlatformSsoClients2({
3971
- ql,
3972
- baseSecret,
3973
- logger: console
3974
- });
3975
- let sample = [];
3976
- let total = 0;
3977
- try {
3978
- const rows = await ql.find("sys_oauth_application", { limit: 5 }, { context: { isSystem: true } });
3979
- const list = Array.isArray(rows) ? rows : Array.isArray(rows?.records) ? rows.records : [];
3980
- sample = list;
3981
- total = typeof rows?.total === "number" ? rows.total : list.length;
3982
- } catch (e) {
3983
- sample = [{ _readErr: e?.message ?? String(e) }];
3984
- }
3985
- return { handled: true, response: this.success({ ...result, total, sample }) };
3986
- } catch (err) {
3987
- return { handled: true, response: this.error(`backfill failed: ${err?.message ?? String(err)}`, 500) };
3988
- }
3989
- }
3990
- if (parts.length === 1 && parts[0] === "projects" && m === "GET") {
3991
- const where = {};
3992
- if (query?.organizationId) where.organization_id = query.organizationId;
3993
- if (query?.status) where.status = query.status;
3994
- let rows = await ql.find(ENV, Object.keys(where).length ? { where } : void 0);
3995
- if (rows && rows.value) rows = rows.value;
3996
- const projects = (Array.isArray(rows) ? rows : []).map(cleanProjectRow);
3997
- return { handled: true, response: this.success({ projects, total: projects.length }) };
3998
- }
3999
- if (parts.length === 1 && parts[0] === "projects" && m === "POST") {
4000
- const req = body || {};
4001
- if (req.organization_id === "__session__" || req.created_by === "__session__") {
4002
- try {
4003
- const userId = await this.resolveCallerUserId(_context);
4004
- if (req.created_by === "__session__") {
4005
- req.created_by = userId ?? "system";
4006
- }
4007
- if (req.organization_id === "__session__") {
4008
- const authService = await this.resolveService(CoreServiceName.enum.auth);
4009
- const rawHeaders = _context?.request?.headers;
4010
- let headers = rawHeaders;
4011
- if (rawHeaders && typeof rawHeaders === "object" && typeof rawHeaders.get !== "function") {
4012
- const h = new Headers();
4013
- for (const [k, v] of Object.entries(rawHeaders)) {
4014
- if (v == null) continue;
4015
- h.set(k, Array.isArray(v) ? v.join(", ") : String(v));
4016
- }
4017
- headers = h;
4018
- }
4019
- const apiObj = authService?.auth?.api ?? authService?.api;
4020
- const sessionData = await apiObj?.getSession?.call(apiObj, { headers });
4021
- req.organization_id = sessionData?.session?.activeOrganizationId ?? void 0;
4022
- }
4023
- } catch {
4024
- }
4025
- }
4026
- if (!req.organization_id || !req.display_name) {
4027
- return { handled: true, response: this.error("organization_id and display_name are required", 400) };
4028
- }
4029
- const environmentId = randomUUID();
4030
- const credentialId = randomUUID();
4031
- const nowIso = (/* @__PURE__ */ new Date()).toISOString();
4032
- const resolved = resolveDriver(req.driver);
4033
- if (!resolved) {
4034
- const available = listRegisteredDrivers().map((d) => d.name);
4035
- if (req.driver) {
4036
- return {
4037
- handled: true,
4038
- response: this.error(
4039
- `Unknown driver '${req.driver}'. Available drivers: [${available.join(", ") || "none"}]`,
4040
- 400
4041
- )
4042
- };
4043
- }
4044
- return {
4045
- handled: true,
4046
- response: this.error(
4047
- "No ObjectQL driver is registered. Register at least one DriverPlugin (e.g. InMemoryDriver or SqlDriver).",
4048
- 503
4049
- )
4050
- };
4051
- }
4052
- const driver = resolved.name;
4053
- let plaintextSecret = `mock-token-${environmentId}`;
4054
- let computedHostname = req.hostname;
4055
- if (!computedHostname) {
4056
- const shortId = environmentId.slice(0, 8);
4057
- try {
4058
- const orgRow = await findOne("sys_organization", { id: req.organization_id });
4059
- const orgSlug = orgRow?.slug || req.organization_id;
4060
- const rootDomain = getEnv("OS_ROOT_DOMAIN") ?? getEnv("ROOT_DOMAIN", "objectstack.app");
4061
- computedHostname = `${orgSlug}-${shortId}.${rootDomain}`;
4062
- } catch {
4063
- computedHostname = `${req.organization_id}-${shortId}.objectstack.app`;
4064
- }
4065
- }
4066
- try {
4067
- const existing = await findOne("sys_environment", {
4068
- hostname: computedHostname
4069
- });
4070
- if (existing && existing.id !== environmentId) {
4071
- return {
4072
- handled: true,
4073
- response: this.error(
4074
- `Hostname '${computedHostname}' is already in use by another project.`,
4075
- 409,
4076
- { code: "HOSTNAME_TAKEN", hostname: computedHostname }
4077
- )
4078
- };
4079
- }
4080
- } catch {
4081
- }
4082
- const baseMetadata = { ...req.metadata ?? {} };
4083
- const simulateFailure = Boolean(baseMetadata.__simulateFailure);
4084
- const simulateDelayMs = Number(baseMetadata.__simulateDelayMs ?? 1500);
4085
- try {
4086
- let ownerUserId = req.created_by && req.created_by !== "system" ? String(req.created_by) : void 0;
4087
- if (!ownerUserId) {
4088
- ownerUserId = await this.resolveCallerUserId(_context);
4089
- }
4090
- if (ownerUserId) {
4091
- const userRow = await ql.find("sys_user", { where: { id: ownerUserId } });
4092
- const userRows = Array.isArray(userRow) ? userRow : userRow?.value ?? [];
4093
- const u = Array.isArray(userRows) && userRows.length > 0 ? userRows[0] : null;
4094
- if (u?.email) {
4095
- baseMetadata.ownerSeed = {
4096
- userId: String(ownerUserId),
4097
- email: String(u.email),
4098
- name: u.name ? String(u.name) : null,
4099
- image: u.image ? String(u.image) : null
4100
- };
4101
- }
4102
- }
4103
- } catch {
4104
- }
4105
- try {
4106
- const orgRow = await ql.find("sys_organization", { where: { id: req.organization_id } });
4107
- const orgRows = Array.isArray(orgRow) ? orgRow : orgRow?.value ?? [];
4108
- const org = Array.isArray(orgRows) && orgRows.length > 0 ? orgRows[0] : null;
4109
- if (org?.id && org?.name) {
4110
- baseMetadata.orgSeed = {
4111
- id: String(org.id),
4112
- name: String(org.name),
4113
- slug: org.slug ? String(org.slug) : null,
4114
- logo: org.logo ? String(org.logo) : null
4115
- };
4116
- }
4117
- } catch {
4118
- }
4119
- await ql.insert(ENV, {
4120
- id: environmentId,
4121
- organization_id: req.organization_id,
4122
- display_name: req.display_name,
4123
- is_default: req.is_default ?? false,
4124
- is_system: req.is_system ?? false,
4125
- plan: req.plan ?? "free",
4126
- status: "provisioning",
4127
- created_by: req.created_by ?? "system",
4128
- metadata: JSON.stringify(baseMetadata),
4129
- created_at: nowIso,
4130
- updated_at: nowIso,
4131
- database_url: null,
4132
- database_driver: driver,
4133
- storage_limit_mb: req.storage_limit_mb ?? 1024,
4134
- provisioned_at: null,
4135
- hostname: computedHostname,
4136
- visibility: (() => {
4137
- const raw = String(req.visibility ?? "private");
4138
- return raw === "unlisted" ? "private" : raw;
4139
- })()
4140
- });
4141
- try {
4142
- const { seedPlatformSsoClient: seedPlatformSsoClient2 } = await Promise.resolve().then(() => (init_platform_sso(), platform_sso_exports));
4143
- const baseSecret = (readEnvWithDeprecation3("OS_AUTH_SECRET", ["AUTH_SECRET", "BETTER_AUTH_SECRET"]) ?? "").trim();
4144
- if (baseSecret) {
4145
- await seedPlatformSsoClient2({
4146
- ql,
4147
- environmentId,
4148
- hostname: computedHostname,
4149
- baseSecret,
4150
- logger: console
4151
- });
4152
- }
4153
- } catch (ssoErr) {
4154
- console.warn?.("[http-dispatcher] platform SSO seed failed (non-fatal)", {
4155
- environmentId,
4156
- error: ssoErr?.message
4157
- });
4158
- }
4159
- const runProvisioning = async () => {
4160
- try {
4161
- if (simulateDelayMs > 0) {
4162
- await new Promise((r) => setTimeout(r, simulateDelayMs));
4163
- }
4164
- if (simulateFailure) {
4165
- throw new Error("Simulated provisioning failure (metadata.__simulateFailure=true)");
4166
- }
4167
- let databaseUrl;
4168
- try {
4169
- const adapter = await getRealAdapter(driver);
4170
- if (adapter) {
4171
- const result = await adapter.createDatabase({
4172
- environmentId,
4173
- databaseName: `p-${environmentId.replace(/-/g, "").slice(0, 24)}`,
4174
- region: "us-east-1",
4175
- storageLimitMb: req.storage_limit_mb ?? 1024
4176
- });
4177
- databaseUrl = result.databaseUrl;
4178
- if (result.plaintextSecret) plaintextSecret = result.plaintextSecret;
4179
- } else {
4180
- databaseUrl = buildDatabaseUrl(driver, environmentId);
4181
- }
4182
- } catch (adapterErr) {
4183
- throw adapterErr instanceof Error ? adapterErr : new Error(String(adapterErr));
4184
- }
4185
- const seedStartedAt = (/* @__PURE__ */ new Date()).toISOString();
4186
- await ql.update(
4187
- ENV,
4188
- {
4189
- database_url: databaseUrl,
4190
- updated_at: seedStartedAt
4191
- },
4192
- { where: { id: environmentId } }
4193
- );
4194
- await ql.insert(CRED, {
4195
- id: credentialId,
4196
- environment_id: environmentId,
4197
- secret_ciphertext: plaintextSecret,
4198
- encryption_key_id: "noop",
4199
- authorization: "full_access",
4200
- status: "active",
4201
- created_at: seedStartedAt,
4202
- updated_at: seedStartedAt
4203
- });
4204
- const templateId = req.template_id ?? "blank";
4205
- if (templateId !== "blank") {
4206
- try {
4207
- const seeder = await this.resolveService("template-seeder");
4208
- if (seeder) {
4209
- await seeder.seed({ environmentId, templateId });
4210
- }
4211
- } catch (seedErr) {
4212
- const seedMessage = seedErr instanceof Error ? seedErr.message : String(seedErr);
4213
- try {
4214
- const existing = await findOne(ENV, { id: environmentId });
4215
- const existingMeta = typeof existing?.metadata === "string" ? JSON.parse(existing.metadata) : existing?.metadata ?? {};
4216
- await ql.update(
4217
- ENV,
4218
- {
4219
- metadata: JSON.stringify({
4220
- ...existingMeta,
4221
- templateSeedError: { message: seedMessage, templateId }
4222
- })
4223
- },
4224
- { where: { id: environmentId } }
4225
- );
4226
- } catch {
4227
- }
4228
- }
4229
- }
4230
- const artifactPathRaw = baseMetadata.artifact_path;
4231
- if (typeof artifactPathRaw === "string" && artifactPathRaw.length > 0) {
4232
- try {
4233
- const path2 = await import("path");
4234
- const { isHttpUrl: isHttpUrl2, loadArtifactBundle: loadArtifactBundle2 } = await Promise.resolve().then(() => (init_load_artifact_bundle(), load_artifact_bundle_exports));
4235
- const root = process.env.OS_PROJECT_ARTIFACT_ROOT ?? process.cwd();
4236
- const resolved2 = isHttpUrl2(artifactPathRaw) ? artifactPathRaw : path2.isAbsolute(artifactPathRaw) ? artifactPathRaw : path2.resolve(root, artifactPathRaw);
4237
- const bundle = await loadArtifactBundle2(resolved2, { tag: "[bind-artifact]" });
4238
- if (!bundle) {
4239
- throw new Error(`failed to load artifact bundle at '${resolved2}'`);
4240
- }
4241
- const seeder = await this.resolveService("template-seeder");
4242
- if (seeder?.seedBundle) {
4243
- await seeder.seedBundle({ environmentId, bundle });
4244
- } else {
4245
- throw new Error("template-seeder.seedBundle is unavailable");
4246
- }
4247
- } catch (bindErr) {
4248
- const bindMessage = bindErr instanceof Error ? bindErr.message : String(bindErr);
4249
- try {
4250
- const existing = await findOne(ENV, { id: environmentId });
4251
- const existingMeta = typeof existing?.metadata === "string" ? JSON.parse(existing.metadata) : existing?.metadata ?? {};
4252
- await ql.update(
4253
- ENV,
4254
- {
4255
- metadata: JSON.stringify({
4256
- ...existingMeta,
4257
- artifactBindError: { message: bindMessage, artifactPath: artifactPathRaw }
4258
- })
4259
- },
4260
- { where: { id: environmentId } }
4261
- );
4262
- } catch {
4263
- }
4264
- }
4265
- }
4266
- const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
4267
- await ql.update(
4268
- ENV,
4269
- {
4270
- status: "active",
4271
- provisioned_at: finishedAt,
4272
- updated_at: finishedAt
4273
- },
4274
- { where: { id: environmentId } }
4275
- );
4276
- } catch (err) {
4277
- const message = err instanceof Error ? err.message : String(err);
4278
- const failedAt = (/* @__PURE__ */ new Date()).toISOString();
4279
- await ql.update(
4280
- ENV,
4281
- {
4282
- status: "failed",
4283
- metadata: JSON.stringify({
4284
- ...baseMetadata,
4285
- provisioningError: { message, failedAt }
4286
- }),
4287
- updated_at: failedAt
4288
- },
4289
- { where: { id: environmentId } }
4290
- );
4291
- }
4292
- };
4293
- const provisionSyncEnv = process.env.OS_PROVISION_SYNC;
4294
- const onServerless = !!(process.env.VERCEL || process.env.AWS_LAMBDA_FUNCTION_NAME || process.env.NETLIFY || process.env.CF_PAGES);
4295
- const syncProvisioning = provisionSyncEnv === void 0 ? onServerless : provisionSyncEnv !== "0" && provisionSyncEnv !== "false";
4296
- if (syncProvisioning) {
4297
- await runProvisioning();
4298
- } else {
4299
- void runProvisioning();
4300
- }
4301
- const project = cleanProjectRow(await findOne(ENV, { id: environmentId }));
4302
- const res = this.success({ project });
4303
- res.status = syncProvisioning ? 201 : 202;
4304
- return { handled: true, response: res };
4305
- }
4306
- if (parts.length === 2 && parts[0] === "projects") {
4307
- const id = decodeURIComponent(parts[1]);
4308
- if (m === "GET") {
4309
- const envRow = await findOne(ENV, { id });
4310
- if (!envRow) return { handled: true, response: this.error(`Project '${id}' not found`, 404) };
4311
- const credRow = await findOne(CRED, { environment_id: id, status: "active" });
4312
- const callerUserId = await this.resolveCallerUserId(_context);
4313
- const membership = callerUserId ? await findOne(MEM, { environment_id: id, user_id: callerUserId }) : await findOne(MEM, { environment_id: id });
4314
- const credMeta = credRow ? {
4315
- id: credRow.id,
4316
- status: credRow.status,
4317
- authorization: credRow.authorization,
4318
- activatedAt: credRow.created_at,
4319
- expiresAt: credRow.expires_at
4320
- } : void 0;
4321
- const project = cleanProjectRow(envRow);
4322
- const database = project.database_url ? {
4323
- driver: project.database_driver,
4324
- database_name: `env-${project.id}`,
4325
- database_url: project.database_url,
4326
- storage_limit_mb: project.storage_limit_mb,
4327
- provisioned_at: project.provisioned_at
4328
- } : void 0;
4329
- return {
4330
- handled: true,
4331
- response: this.success({ project, database, credential: credMeta, membership })
4332
- };
4333
- }
4334
- if (m === "PATCH") {
4335
- const patch = {};
4336
- if (body?.display_name !== void 0) patch.display_name = body.display_name;
4337
- if (body?.plan !== void 0) patch.plan = body.plan;
4338
- if (body?.status !== void 0) patch.status = body.status;
4339
- if (body?.is_default !== void 0) patch.is_default = body.is_default;
4340
- if (body?.visibility !== void 0) {
4341
- let v = String(body.visibility);
4342
- if (v === "unlisted") v = "private";
4343
- if (!["private", "public"].includes(v)) {
4344
- return { handled: true, response: this.error(`Invalid visibility '${v}' (expected private | public)`, 400) };
4345
- }
4346
- patch.visibility = v;
4347
- }
4348
- if (body?.metadata !== void 0) patch.metadata = JSON.stringify(body.metadata);
4349
- patch.updated_at = (/* @__PURE__ */ new Date()).toISOString();
4350
- await ql.update(ENV, patch, { where: { id } });
4351
- const envRow = await findOne(ENV, { id });
4352
- if (!envRow) return { handled: true, response: this.error(`Project '${id}' not found`, 404) };
4353
- return { handled: true, response: this.success({ project: cleanProjectRow(envRow) }) };
4354
- }
4355
- if (m === "DELETE") {
4356
- const force = query?.force === "1" || query?.force === "true" || body?.force === true;
4357
- const result = await this.deleteProjectCascade(id, { ql, findOne, getRealAdapter, force });
4358
- if (!result.ok) {
4359
- return { handled: true, response: this.error(result.error ?? "Delete failed", result.status ?? 500) };
4360
- }
4361
- return { handled: true, response: this.success({ deleted: true, environmentId: id, warnings: result.warnings }) };
4362
- }
4363
- }
4364
- if (parts.length === 2 && parts[0] === "organizations" && m === "DELETE") {
4365
- const orgId = decodeURIComponent(parts[1]);
4366
- let projectRows = [];
4367
- try {
4368
- let rows = await ql.find(ENV, { where: { organization_id: orgId } });
4369
- if (rows && rows.value) rows = rows.value;
4370
- projectRows = Array.isArray(rows) ? rows : [];
4371
- } catch {
4372
- projectRows = [];
4373
- }
4374
- const warnings = [];
4375
- let deletedProjects = 0;
4376
- for (const row of projectRows) {
4377
- const pid = row?.id;
4378
- if (!pid) continue;
4379
- try {
4380
- const r = await this.deleteProjectCascade(pid, { ql, findOne, getRealAdapter, force: true });
4381
- if (r.ok) deletedProjects++;
4382
- if (r.warnings?.length) warnings.push(...r.warnings);
4383
- if (!r.ok && r.error) warnings.push(`Project ${pid}: ${r.error}`);
4384
- } catch (err) {
4385
- warnings.push(
4386
- `Failed to delete project ${pid}: ${err instanceof Error ? err.message : String(err)}`
4387
- );
4388
- }
4389
- }
4390
- let orgDeleted = false;
4391
- try {
4392
- const authService = await this.getService(CoreServiceName.enum.auth);
4393
- const fn = authService?.api?.deleteOrganization;
4394
- if (typeof fn === "function") {
4395
- await fn.call(authService.api, {
4396
- body: { organizationId: orgId },
4397
- headers: _context?.request?.headers
4398
- });
4399
- orgDeleted = true;
4400
- }
4401
- } catch (err) {
4402
- warnings.push(
4403
- `auth.deleteOrganization failed: ${err instanceof Error ? err.message : String(err)}`
4404
- );
4405
- }
4406
- if (!orgDeleted) {
4407
- try {
4408
- await ql.delete("sys_organization", { where: { id: orgId } });
4409
- orgDeleted = true;
4410
- } catch (err) {
4411
- warnings.push(
4412
- `Failed to delete sys_organization row: ${err instanceof Error ? err.message : String(err)}`
4413
- );
4414
- }
4415
- }
4416
- return {
4417
- handled: true,
4418
- response: this.success({
4419
- deleted: orgDeleted,
4420
- organizationId: orgId,
4421
- deletedProjects,
4422
- warnings
4423
- })
4424
- };
4425
- }
4426
- if (parts.length === 3 && parts[0] === "projects" && parts[2] === "hostname" && (m === "POST" || m === "PUT")) {
4427
- const id = decodeURIComponent(parts[1]);
4428
- const hostname = body?.hostname;
4429
- if (!hostname || typeof hostname !== "string") {
4430
- return { handled: true, response: this.error("hostname is required", 400) };
4431
- }
4432
- const normalized = hostname.trim().toLowerCase();
4433
- if (!/^[a-z0-9]([a-z0-9\-\.]*[a-z0-9])?$/.test(normalized)) {
4434
- return { handled: true, response: this.error("Invalid hostname format", 400) };
4435
- }
4436
- const envRow = await findOne(ENV, { id });
4437
- if (!envRow) return { handled: true, response: this.error(`Project '${id}' not found`, 404) };
4438
- let existing;
4439
- try {
4440
- const rows = await ql.find(ENV, { where: { hostname: normalized } });
4441
- const arr = Array.isArray(rows) ? rows : rows?.value ?? [];
4442
- existing = arr.find((r) => r.id !== id);
4443
- } catch {
4444
- }
4445
- if (existing) {
4446
- return {
4447
- handled: true,
4448
- response: this.error(
4449
- `Hostname '${normalized}' is already in use by another project.`,
4450
- 409,
4451
- { code: "HOSTNAME_TAKEN", hostname: normalized }
4452
- )
4453
- };
4454
- }
4455
- const updatedAt = (/* @__PURE__ */ new Date()).toISOString();
4456
- await ql.update(ENV, { hostname: normalized, updated_at: updatedAt }, { where: { id } });
4457
- if (this.envRegistry?.invalidate) {
4458
- try {
4459
- await this.envRegistry.invalidate(id);
4460
- } catch {
4461
- }
4462
- }
4463
- const updated = cleanProjectRow(await findOne(ENV, { id }));
4464
- return { handled: true, response: this.success({ project: updated }) };
4465
- }
4466
- if (parts.length === 3 && parts[0] === "projects" && parts[2] === "retry" && m === "POST") {
4467
- const id = decodeURIComponent(parts[1]);
4468
- const envRow = await findOne(ENV, { id });
4469
- if (!envRow) return { handled: true, response: this.error(`Project '${id}' not found`, 404) };
4470
- if (envRow.status !== "failed" && envRow.status !== "provisioning") {
4471
- return {
4472
- handled: true,
4473
- response: this.error(
4474
- `Project '${id}' is '${envRow.status}'; only failed or provisioning projects can be retried.`,
4475
- 409
4476
- )
4477
- };
4478
- }
4479
- const driverName = envRow.database_driver;
4480
- const resolved = resolveDriver(driverName);
4481
- if (!resolved) {
4482
- return {
4483
- handled: true,
4484
- response: this.error(
4485
- `Driver '${driverName}' is no longer registered; retry aborted.`,
4486
- 503
4487
- )
4488
- };
4489
- }
4490
- let metadata = {};
4491
- if (envRow.metadata) {
4492
- if (typeof envRow.metadata === "string") {
4493
- try {
4494
- metadata = JSON.parse(envRow.metadata);
4495
- } catch {
4496
- metadata = {};
4497
- }
4498
- } else if (typeof envRow.metadata === "object") {
4499
- metadata = { ...envRow.metadata };
4500
- }
4501
- }
4502
- delete metadata.provisioningError;
4503
- const retryStartedAt = (/* @__PURE__ */ new Date()).toISOString();
4504
- await ql.update(
4505
- ENV,
4506
- {
4507
- status: "provisioning",
4508
- metadata: JSON.stringify(metadata),
4509
- updated_at: retryStartedAt
4510
- },
4511
- { where: { id } }
4512
- );
4513
- const simulateRetryFailure = Boolean(metadata.__simulateFailure);
4514
- const simulateRetryDelay = Number(metadata.__simulateDelayMs ?? 1500);
4515
- const runRetry = async () => {
4516
- try {
4517
- if (simulateRetryDelay > 0) {
4518
- await new Promise((r) => setTimeout(r, simulateRetryDelay));
4519
- }
4520
- if (simulateRetryFailure) {
4521
- throw new Error("Simulated provisioning failure (metadata.__simulateFailure=true)");
4522
- }
4523
- let databaseUrl;
4524
- let retrySecret = `mock-token-${id}`;
4525
- try {
4526
- const adapter = await getRealAdapter(resolved.name);
4527
- if (adapter) {
4528
- const result = await adapter.createDatabase({
4529
- environmentId: id,
4530
- databaseName: `p-${id.replace(/-/g, "").slice(0, 24)}`,
4531
- region: "us-east-1",
4532
- storageLimitMb: envRow.storage_limit_mb ?? 1024
4533
- });
4534
- databaseUrl = result.databaseUrl;
4535
- if (result.plaintextSecret) retrySecret = result.plaintextSecret;
4536
- } else {
4537
- databaseUrl = buildDatabaseUrl(resolved.name, id);
4538
- }
4539
- } catch (adapterErr) {
4540
- throw adapterErr instanceof Error ? adapterErr : new Error(String(adapterErr));
4541
- }
4542
- const nowIso = (/* @__PURE__ */ new Date()).toISOString();
4543
- await ql.update(
4544
- ENV,
4545
- {
4546
- status: "active",
4547
- database_url: databaseUrl,
4548
- database_driver: resolved.name,
4549
- provisioned_at: nowIso,
4550
- updated_at: nowIso
4551
- },
4552
- { where: { id } }
4553
- );
4554
- const existingCred = await findOne(CRED, { environment_id: id, status: "active" });
4555
- if (!existingCred) {
4556
- await ql.insert(CRED, {
4557
- id: randomUUID(),
4558
- environment_id: id,
4559
- secret_ciphertext: retrySecret,
4560
- encryption_key_id: "noop",
4561
- authorization: "full_access",
4562
- status: "active",
4563
- created_at: nowIso,
4564
- updated_at: nowIso
4565
- });
4566
- }
4567
- } catch (err) {
4568
- const message = err instanceof Error ? err.message : String(err);
4569
- const failedAt = (/* @__PURE__ */ new Date()).toISOString();
4570
- await ql.update(
4571
- ENV,
4572
- {
4573
- status: "failed",
4574
- metadata: JSON.stringify({
4575
- ...metadata,
4576
- provisioningError: { message, failedAt }
4577
- }),
4578
- updated_at: failedAt
4579
- },
4580
- { where: { id } }
4581
- );
4582
- }
4583
- };
4584
- void runRetry();
4585
- const envAfter = cleanProjectRow(await findOne(ENV, { id }));
4586
- const retryRes = this.success({ project: envAfter });
4587
- retryRes.status = 202;
4588
- return { handled: true, response: retryRes };
4589
- }
4590
- if (parts.length === 3 && parts[0] === "projects" && parts[2] === "activate" && m === "POST") {
4591
- const id = decodeURIComponent(parts[1]);
4592
- const envRow = await findOne(ENV, { id });
4593
- if (!envRow) return { handled: true, response: this.error(`Project '${id}' not found`, 404) };
4594
- return { handled: true, response: this.success({ project: cleanProjectRow(envRow), sessionUpdated: false }) };
4595
- }
4596
- if (parts.length === 4 && parts[0] === "projects" && parts[2] === "credentials" && parts[3] === "rotate" && m === "POST") {
4597
- const id = decodeURIComponent(parts[1]);
4598
- const plaintext = body?.plaintext;
4599
- if (!plaintext || typeof plaintext !== "string") {
4600
- return { handled: true, response: this.error("plaintext is required", 400) };
4601
- }
4602
- const envRow = await findOne(ENV, { id });
4603
- if (!envRow) return { handled: true, response: this.error(`Project '${id}' not found`, 404) };
4604
- const nowIso = (/* @__PURE__ */ new Date()).toISOString();
4605
- let existing = await ql.find(CRED, { where: { environment_id: id, status: "active" } });
4606
- if (existing && existing.value) existing = existing.value;
4607
- for (const row of Array.isArray(existing) ? existing : []) {
4608
- await ql.update(CRED, {
4609
- status: "revoked",
4610
- revoked_at: nowIso,
4611
- updated_at: nowIso
4612
- }, { where: { id: row.id } });
4613
- }
4614
- const credentialId = randomUUID();
4615
- await ql.insert(CRED, {
4616
- id: credentialId,
4617
- environment_id: id,
4618
- secret_ciphertext: plaintext,
4619
- encryption_key_id: "noop",
4620
- authorization: "full_access",
4621
- status: "active",
4622
- created_at: nowIso,
4623
- updated_at: nowIso
4624
- });
4625
- const credential = await findOne(CRED, { id: credentialId });
4626
- const credMeta = credential ? {
4627
- id: credential.id,
4628
- status: credential.status,
4629
- authorization: credential.authorization,
4630
- activatedAt: credential.created_at
4631
- } : void 0;
4632
- return { handled: true, response: this.success({ credential: credMeta }) };
4633
- }
4634
- if (parts.length === 3 && parts[0] === "projects" && parts[2] === "members" && m === "GET") {
4635
- const id = decodeURIComponent(parts[1]);
4636
- let rows = await ql.find(MEM, { where: { environment_id: id } });
4637
- if (rows && rows.value) rows = rows.value;
4638
- const members = Array.isArray(rows) ? rows : [];
4639
- const userIds = Array.from(new Set(members.map((mem) => mem.user_id).filter(Boolean)));
4640
- const userMap = /* @__PURE__ */ new Map();
4641
- for (const uid of userIds) {
4642
- let row = null;
4643
- for (const tableName of ["sys_user", "user"]) {
4644
- try {
4645
- const u = await ql.findOne(tableName, { where: { id: uid } });
4646
- row = u?.value ?? u;
4647
- if (row) break;
4648
- } catch {
4649
- }
4650
- }
4651
- if (row) userMap.set(String(uid), {
4652
- id: row.id,
4653
- name: row.name ?? row.display_name,
4654
- email: row.email,
4655
- image: row.image ?? row.avatar_url
4656
- });
4657
- }
4658
- const enriched = members.map((mem) => ({
4659
- ...mem,
4660
- user: userMap.get(String(mem.user_id)) ?? void 0
4661
- }));
4662
- return { handled: true, response: this.success({ members: enriched }) };
4663
- }
4664
- if (parts.length === 3 && parts[0] === "projects" && parts[2] === "members" && m === "POST") {
4665
- const id = decodeURIComponent(parts[1]);
4666
- const project = await findOne(ENV, { id });
4667
- if (!project) return { handled: true, response: this.error(`Project '${id}' not found`, 404) };
4668
- const callerId = await this.resolveCallerUserId(_context);
4669
- if (!callerId) return { handled: true, response: this.error("Authentication required", 401) };
4670
- const callerMem = await findOne(MEM, { environment_id: id, user_id: callerId });
4671
- if (!callerMem || !["owner", "admin"].includes(String(callerMem.role))) {
4672
- return { handled: true, response: this.error("Forbidden \u2014 owner or admin required", 403) };
4673
- }
4674
- const email = typeof body?.email === "string" ? String(body.email).trim().toLowerCase() : null;
4675
- let inviteUserId = typeof body?.user_id === "string" ? String(body.user_id).trim() : null;
4676
- let role = String(body?.role ?? "member").trim().toLowerCase();
4677
- if (!["owner", "admin", "member", "viewer"].includes(role)) {
4678
- return { handled: true, response: this.error(`Invalid role '${role}' (expected owner | admin | member | viewer)`, 400) };
4679
- }
4680
- if (!email && !inviteUserId) {
4681
- return { handled: true, response: this.error("email or user_id is required", 400) };
4682
- }
4683
- if (!inviteUserId && email) {
4684
- let row = null;
4685
- for (const tableName of ["sys_user", "user"]) {
4686
- try {
4687
- const u = await ql.findOne(tableName, { where: { email } });
4688
- row = u?.value ?? u;
4689
- if (row) break;
4690
- } catch {
4691
- }
4692
- }
4693
- if (!row?.id) {
4694
- return { handled: true, response: this.error(`No user found with email '${email}'`, 404) };
4695
- }
4696
- inviteUserId = String(row.id);
4697
- }
4698
- const existing = await findOne(MEM, { environment_id: id, user_id: inviteUserId });
4699
- if (existing) {
4700
- return { handled: true, response: this.success({ member: existing, alreadyMember: true }) };
4701
- }
4702
- try {
4703
- const memberId = randomUUID();
4704
- await ql.insert(MEM, {
4705
- id: memberId,
4706
- environment_id: id,
4707
- user_id: inviteUserId,
4708
- role,
4709
- invited_by: callerId,
4710
- organization_id: project.organization_id ?? null
4711
- });
4712
- const created = await findOne(MEM, { id: memberId });
4713
- return { handled: true, response: this.success({ member: created, alreadyMember: false }) };
4714
- } catch (e) {
4715
- return { handled: true, response: this.error(e?.message ?? "Failed to add member", 500) };
4716
- }
4717
- }
4718
- if (parts.length === 4 && parts[0] === "projects" && parts[2] === "members" && m === "PATCH") {
4719
- const id = decodeURIComponent(parts[1]);
4720
- const memberId = decodeURIComponent(parts[3]);
4721
- const project = await findOne(ENV, { id });
4722
- if (!project) return { handled: true, response: this.error(`Project '${id}' not found`, 404) };
4723
- const callerId = await this.resolveCallerUserId(_context);
4724
- if (!callerId) return { handled: true, response: this.error("Authentication required", 401) };
4725
- const callerMem = await findOne(MEM, { environment_id: id, user_id: callerId });
4726
- if (!callerMem || !["owner", "admin"].includes(String(callerMem.role))) {
4727
- return { handled: true, response: this.error("Forbidden \u2014 owner or admin required", 403) };
4728
- }
4729
- const target = await findOne(MEM, { id: memberId, environment_id: id });
4730
- if (!target) return { handled: true, response: this.error(`Member '${memberId}' not found`, 404) };
4731
- const newRole = String(body?.role ?? "").trim().toLowerCase();
4732
- if (!["owner", "admin", "member", "viewer"].includes(newRole)) {
4733
- return { handled: true, response: this.error(`Invalid role '${newRole}'`, 400) };
4734
- }
4735
- if (target.role === "owner" && newRole !== "owner") {
4736
- let owners = await ql.find(MEM, { where: { environment_id: id, role: "owner" } });
4737
- if (owners && owners.value) owners = owners.value;
4738
- const ownerCount = Array.isArray(owners) ? owners.length : 0;
4739
- if (ownerCount <= 1) {
4740
- return { handled: true, response: this.error("Cannot demote the last owner", 409) };
4741
- }
4742
- }
4743
- try {
4744
- await ql.update(MEM, { role: newRole, updated_at: (/* @__PURE__ */ new Date()).toISOString() }, { where: { id: memberId } });
4745
- const updated = await findOne(MEM, { id: memberId });
4746
- return { handled: true, response: this.success({ member: updated }) };
4747
- } catch (e) {
4748
- return { handled: true, response: this.error(e?.message ?? "Failed to update role", 500) };
4749
- }
4750
- }
4751
- if (parts.length === 4 && parts[0] === "projects" && parts[2] === "members" && m === "DELETE") {
4752
- const id = decodeURIComponent(parts[1]);
4753
- const memberId = decodeURIComponent(parts[3]);
4754
- const project = await findOne(ENV, { id });
4755
- if (!project) return { handled: true, response: this.error(`Project '${id}' not found`, 404) };
4756
- const callerId = await this.resolveCallerUserId(_context);
4757
- if (!callerId) return { handled: true, response: this.error("Authentication required", 401) };
4758
- const target = await findOne(MEM, { id: memberId, environment_id: id });
4759
- if (!target) return { handled: true, response: this.error(`Member '${memberId}' not found`, 404) };
4760
- const callerMem = await findOne(MEM, { environment_id: id, user_id: callerId });
4761
- const isSelf = String(target.user_id) === String(callerId);
4762
- const isPrivileged = callerMem && ["owner", "admin"].includes(String(callerMem.role));
4763
- if (!isSelf && !isPrivileged) {
4764
- return { handled: true, response: this.error("Forbidden \u2014 owner or admin required", 403) };
4765
- }
4766
- if (target.role === "owner") {
4767
- let owners = await ql.find(MEM, { where: { environment_id: id, role: "owner" } });
4768
- if (owners && owners.value) owners = owners.value;
4769
- const ownerCount = Array.isArray(owners) ? owners.length : 0;
4770
- if (ownerCount <= 1) {
4771
- return { handled: true, response: this.error("Cannot remove the last owner", 409) };
4772
- }
4773
- }
4774
- try {
4775
- await ql.delete(MEM, { where: { id: memberId } });
4776
- return { handled: true, response: this.success({ removed: true, memberId }) };
4777
- } catch (e) {
4778
- return { handled: true, response: this.error(e?.message ?? "Failed to remove member", 500) };
4779
- }
4780
- }
4781
- if (parts.length === 3 && parts[0] === "projects" && parts[2] === "packages" && m === "GET") {
4782
- const envId = decodeURIComponent(parts[1]);
4783
- let rows = await ql.find(PKG_INSTALL, { where: { environment_id: envId } });
4784
- if (rows && rows.value) rows = rows.value;
4785
- const installs = Array.isArray(rows) ? rows : [];
4786
- const packages = await Promise.all(
4787
- installs.map(async (r) => {
4788
- let manifestId = null;
4789
- let versionStr = null;
4790
- try {
4791
- if (r.package_id) {
4792
- const pkg = await ql.findOne(PKG, { where: { id: r.package_id } });
4793
- manifestId = pkg?.manifest_id ?? null;
4794
- }
4795
- if (r.package_version_id) {
4796
- const ver = await ql.findOne(PKG_VERSION, { where: { id: r.package_version_id } });
4797
- versionStr = ver?.version ?? null;
4798
- }
4799
- } catch {
4800
- }
4801
- return {
4802
- ...r,
4803
- // Surface user-facing identifiers expected by client SDK
4804
- packageId: manifestId,
4805
- package_id: manifestId ?? r.package_id,
4806
- version: versionStr ?? r.version ?? null
4807
- };
4808
- })
4809
- );
4810
- return { handled: true, response: this.success({ packages, total: packages.length }) };
4811
- }
4812
- if (parts.length === 3 && parts[0] === "projects" && parts[2] === "packages" && m === "POST") {
4813
- const envId = decodeURIComponent(parts[1]);
4814
- const { packageId, version, settings, enableOnInstall } = body ?? {};
4815
- if (!packageId) return { handled: true, response: this.error("packageId is required", 400) };
4816
- const qlSvc = await this.getObjectQLService();
4817
- const pkgRegistry = qlSvc?.registry;
4818
- const allPkgs = pkgRegistry?.getAllPackages?.() ?? [];
4819
- const manifestEntry = allPkgs.find((p) => (p?.manifest?.id ?? p?.id) === packageId);
4820
- const manifest = manifestEntry?.manifest ?? manifestEntry;
4821
- if (!manifest) {
4822
- return { handled: true, response: this.error(`Package '${packageId}' is not registered on this server`, 404) };
4823
- }
4824
- const CLOUD_SCOPES = /* @__PURE__ */ new Set(["cloud", "system", "platform"]);
4825
- if (CLOUD_SCOPES.has(manifest?.scope)) {
4826
- return { handled: true, response: this.error(`Package '${packageId}' has scope=${manifest.scope} and cannot be installed per-project`, 403) };
4827
- }
4828
- const projectRow = await findOne(ENV, { id: envId });
4829
- if (!projectRow) {
4830
- return { handled: true, response: this.error(`Project '${envId}' not found`, 404) };
4831
- }
4832
- const ownerOrgId = projectRow.organization_id ?? "system";
4833
- let userId = "system";
4834
- try {
4835
- const authService = await this.getService(CoreServiceName.enum.auth);
4836
- const sessionData = await authService?.api?.getSession?.({
4837
- headers: _context?.request?.headers
4838
- });
4839
- userId = sessionData?.user?.id ?? sessionData?.session?.userId ?? "system";
4840
- } catch {
4841
- }
4842
- const resolvedVersion = version ?? manifest?.version ?? "1.0.0";
4843
- const dup = await ql.findOne(PKG_INSTALL, {
4844
- where: { environment_id: envId, package_id: packageId }
4845
- });
4846
- if (dup?.id) {
4847
- return { handled: true, response: this.error(`Package '${packageId}' is already installed in this project`, 409) };
4848
- }
4849
- const sysPackageId = await ensureSysPackage(packageId, ownerOrgId, userId, manifest);
4850
- const sysPackageVersionId = await ensureSysPackageVersion(sysPackageId, resolvedVersion, userId, manifest);
4851
- const nowIso = (/* @__PURE__ */ new Date()).toISOString();
4852
- const recordId = randomUUID();
4853
- await ql.insert(PKG_INSTALL, {
4854
- id: recordId,
4855
- environment_id: envId,
4856
- package_id: sysPackageId,
4857
- package_version_id: sysPackageVersionId,
4858
- status: "installed",
4859
- enabled: enableOnInstall !== false,
4860
- installed_at: nowIso,
4861
- installed_by: userId,
4862
- updated_at: nowIso,
4863
- settings: settings ? JSON.stringify(settings) : null
4864
- });
4865
- const record = await ql.findOne(PKG_INSTALL, { where: { id: recordId } });
4866
- try {
4867
- await this.kernelManager?.evict(envId);
4868
- } catch {
4869
- }
4870
- return { handled: true, response: this.success({ package: record }) };
4871
- }
4872
- if (parts.length === 4 && parts[0] === "projects" && parts[2] === "packages" && m === "GET") {
4873
- const envId = decodeURIComponent(parts[1]);
4874
- const pkgId = decodeURIComponent(parts[3]);
4875
- const record = await ql.findOne(PKG_INSTALL, { where: { environment_id: envId, package_id: pkgId } });
4876
- if (!record) return { handled: true, response: this.error(`Package '${pkgId}' is not installed in this project`, 404) };
4877
- return { handled: true, response: this.success({ package: record }) };
4878
- }
4879
- if (parts.length === 4 && parts[0] === "projects" && parts[2] === "packages" && m === "DELETE") {
4880
- const envId = decodeURIComponent(parts[1]);
4881
- const pkgId = decodeURIComponent(parts[3]);
4882
- const record = await findInstallByManifestId(envId, pkgId);
4883
- if (!record) return { handled: true, response: this.error(`Package '${pkgId}' is not installed in this project`, 404) };
4884
- const allPkgs0 = this.kernel.packages?.getAll?.() ?? [];
4885
- const m0 = allPkgs0.find((p) => (p.manifest?.id ?? p.id) === pkgId)?.manifest;
4886
- if (m0?.scope && ["cloud", "system", "platform"].includes(m0.scope)) {
4887
- return { handled: true, response: this.error(`Package '${pkgId}' with scope=${m0.scope} cannot be uninstalled`, 403) };
4888
- }
4889
- await ql.delete(PKG_INSTALL, { where: { id: record.id } });
4890
- try {
4891
- await this.kernelManager?.evict(envId);
4892
- } catch {
4893
- }
4894
- return { handled: true, response: this.success({ id: record.id, success: true }) };
4895
- }
4896
- if (parts.length === 5 && parts[0] === "projects" && parts[2] === "packages" && parts[4] === "enable" && m === "PATCH") {
4897
- const envId = decodeURIComponent(parts[1]);
4898
- const pkgId = decodeURIComponent(parts[3]);
4899
- const record = await findInstallByManifestId(envId, pkgId);
4900
- if (!record) return { handled: true, response: this.error(`Package '${pkgId}' is not installed in this project`, 404) };
4901
- const nowIso = (/* @__PURE__ */ new Date()).toISOString();
4902
- await ql.update(PKG_INSTALL, { enabled: true, status: "installed", updated_at: nowIso }, { where: { id: record.id } });
4903
- const updated = await ql.findOne(PKG_INSTALL, { where: { id: record.id } });
4904
- try {
4905
- await this.kernelManager?.evict(envId);
4906
- } catch {
4907
- }
4908
- return { handled: true, response: this.success({ package: updated }) };
4909
- }
4910
- if (parts.length === 5 && parts[0] === "projects" && parts[2] === "packages" && parts[4] === "disable" && m === "PATCH") {
4911
- const envId = decodeURIComponent(parts[1]);
4912
- const pkgId = decodeURIComponent(parts[3]);
4913
- const record = await findInstallByManifestId(envId, pkgId);
4914
- if (!record) return { handled: true, response: this.error(`Package '${pkgId}' is not installed in this project`, 404) };
4915
- const allPkgs1 = this.kernel.packages?.getAll?.() ?? [];
4916
- const m1 = allPkgs1.find((p) => (p.manifest?.id ?? p.id) === pkgId)?.manifest;
4917
- if (m1?.scope && ["cloud", "system", "platform"].includes(m1.scope)) {
4918
- return { handled: true, response: this.error(`Package '${pkgId}' with scope=${m1.scope} cannot be disabled`, 403) };
4919
- }
4920
- const nowIso = (/* @__PURE__ */ new Date()).toISOString();
4921
- await ql.update(PKG_INSTALL, { enabled: false, status: "disabled", updated_at: nowIso }, { where: { id: record.id } });
4922
- const updated = await ql.findOne(PKG_INSTALL, { where: { id: record.id } });
4923
- try {
4924
- await this.kernelManager?.evict(envId);
4925
- } catch {
4926
- }
4927
- return { handled: true, response: this.success({ package: updated }) };
4928
- }
4929
- if (parts.length === 5 && parts[0] === "projects" && parts[2] === "packages" && parts[4] === "upgrade" && m === "POST") {
4930
- const envId = decodeURIComponent(parts[1]);
4931
- const pkgId = decodeURIComponent(parts[3]);
4932
- const record = await findInstallByManifestId(envId, pkgId);
4933
- if (!record) return { handled: true, response: this.error(`Package '${pkgId}' is not installed in this project`, 404) };
4934
- const { targetVersion } = body ?? {};
4935
- const allPkgs2 = this.kernel.packages?.getAll?.() ?? [];
4936
- const manifest2 = allPkgs2.find((p) => (p.manifest?.id ?? p.id) === pkgId)?.manifest;
4937
- const currentVer = await ql.findOne(PKG_VERSION, { where: { id: record.package_version_id } });
4938
- const newVersion = targetVersion ?? manifest2?.version ?? currentVer?.version ?? "1.0.0";
4939
- if (newVersion === currentVer?.version) {
4940
- return { handled: true, response: this.success({ package: record, message: "Already at target version" }) };
4941
- }
4942
- let userId = "system";
4943
- try {
4944
- const authService = await this.getService(CoreServiceName.enum.auth);
4945
- const sessionData = await authService?.api?.getSession?.({
4946
- headers: _context?.request?.headers
4947
- });
4948
- userId = sessionData?.user?.id ?? "system";
4949
- } catch {
4950
- }
4951
- const newVersionId = await ensureSysPackageVersion(record.package_id, newVersion, userId, manifest2);
4952
- const nowIso = (/* @__PURE__ */ new Date()).toISOString();
4953
- await ql.update(PKG_INSTALL, {
4954
- package_version_id: newVersionId,
4955
- status: "installed",
4956
- updated_at: nowIso
4957
- }, { where: { id: record.id } });
4958
- const updated = await ql.findOne(PKG_INSTALL, { where: { id: record.id } });
4959
- try {
4960
- await this.kernelManager?.evict(envId);
4961
- } catch {
4962
- }
4963
- return { handled: true, response: this.success({ package: updated }) };
4964
- }
4965
- } catch (e) {
4966
- return { handled: true, response: this.error(e.message, e.statusCode || 500) };
4967
- }
4968
- return { handled: false };
4969
- }
4970
- /**
4971
- * Cascade-delete a project: cred / member / package_installation rows,
4972
- * then the physical database via the provisioning adapter, then the
4973
- * `sys_environment` row itself. Used by both `DELETE /cloud/environments/:id`
4974
- * and the org-cascade in `DELETE /cloud/organizations/:id`.
3936
+ * - GET /cloud/environments/:id/members → list members
3937
+ * - GET /cloud/environments/:id/packages → list installed packages
3938
+ * - POST /cloud/environments/:id/packages → install package into env
3939
+ * - GET /cloud/environments/:id/packages/:pkgId → get installation detail
3940
+ * - PATCH /cloud/environments/:id/packages/:pkgId/enable → enable package
3941
+ * - PATCH /cloud/environments/:id/packages/:pkgId/disable → disable package
3942
+ * - DELETE /cloud/environments/:id/packages/:pkgId → uninstall (scope=platform forbidden)
3943
+ * - POST /cloud/environments/:id/packages/:pkgId/upgrade → upgrade to newer version
3944
+ *
3945
+ * Driver binding
3946
+ * --------------
3947
+ * Environments are not tied to any specific driver. At provisioning time the
3948
+ * caller passes `driver` (a short name such as `memory`, `turso`, or any
3949
+ * future `sql` / `postgres` driver). The dispatcher validates the name
3950
+ * against the kernel's registered driver services (`driver.<name>`) and
3951
+ * derives an appropriate placeholder `database_url` for the chosen driver.
3952
+ * If `driver` is omitted, the dispatcher auto-selects the first available
3953
+ * in preference order: turso → memory → any other registered driver.
4975
3954
  *
4976
- * Idempotent and best-effort: missing rows / unreachable adapters
4977
- * become warnings rather than hard failures, so a half-provisioned
4978
- * project can still be cleaned out.
3955
+ * Backed by ObjectQL sys_environment / sys_environment_credential /
3956
+ * sys_environment_member tables (registered by
3957
+ * `@objectstack/service-tenant`'s `createTenantPlugin`).
3958
+ * Physical database addressing (database_url, database_driver, etc.)
3959
+ * is stored directly on the sys_environment row.
4979
3960
  */
4980
- async deleteProjectCascade(environmentId, deps) {
4981
- const { ql, findOne, getRealAdapter, force } = deps;
4982
- const ENV = "sys_environment";
4983
- const warnings = [];
4984
- const row = await findOne(ENV, { id: environmentId });
4985
- if (!row) {
4986
- return { ok: false, status: 404, error: `Project '${environmentId}' not found`, warnings };
4987
- }
4988
- if (row.is_system === true || row.is_system === 1) {
4989
- return { ok: false, status: 409, error: `Project '${environmentId}' is a system project and cannot be deleted`, warnings };
4990
- }
4991
- if ((row.is_default === true || row.is_default === 1) && !force) {
4992
- return {
4993
- ok: false,
4994
- status: 409,
4995
- error: `Project '${environmentId}' is the default project for its organization. Pass ?force=1 to delete it.`,
4996
- warnings
4997
- };
4998
- }
4999
- const cascade = [
5000
- { object: "sys_environment_credential", field: "environment_id" },
5001
- { object: "sys_environment_member", field: "environment_id" },
5002
- { object: "sys_package_installation", field: "environment_id" }
5003
- ];
5004
- for (const { object, field } of cascade) {
5005
- try {
5006
- let rows = await ql.find(object, { where: { [field]: environmentId } });
5007
- if (rows && rows.value) rows = rows.value;
5008
- if (Array.isArray(rows)) {
5009
- for (const r of rows) {
5010
- if (r?.id != null) {
5011
- try {
5012
- await ql.delete(object, { where: { id: r.id } });
5013
- } catch (innerErr) {
5014
- warnings.push(
5015
- `Failed to delete ${object} ${r.id}: ${innerErr instanceof Error ? innerErr.message : String(innerErr)}`
5016
- );
5017
- }
5018
- }
3961
+ /**
3962
+ * Resolve the calling user id from the request session, if any.
3963
+ * Returns `undefined` for anonymous calls or when auth is not wired up.
3964
+ */
3965
+ async resolveActiveOrganizationId(context) {
3966
+ try {
3967
+ const authService = await this.resolveService(CoreServiceName.enum.auth);
3968
+ const rawHeaders = context.request?.headers;
3969
+ let headers = rawHeaders;
3970
+ if (rawHeaders && typeof rawHeaders === "object" && typeof rawHeaders.get !== "function") {
3971
+ try {
3972
+ const h = new Headers();
3973
+ for (const [k, v] of Object.entries(rawHeaders)) {
3974
+ if (v == null) continue;
3975
+ h.set(k, Array.isArray(v) ? v.join(", ") : String(v));
5019
3976
  }
3977
+ headers = h;
3978
+ } catch {
3979
+ headers = rawHeaders;
5020
3980
  }
5021
- } catch (err) {
5022
- warnings.push(
5023
- `Failed to enumerate ${object} for project ${environmentId}: ${err instanceof Error ? err.message : String(err)}`
5024
- );
5025
- }
5026
- }
5027
- const driver = row.database_driver ?? "memory";
5028
- const databaseUrl = row.database_url;
5029
- const databaseName = `p-${String(environmentId).replace(/-/g, "").slice(0, 24)}`;
5030
- try {
5031
- const adapter = await getRealAdapter(driver);
5032
- if (adapter?.deleteDatabase) {
5033
- await adapter.deleteDatabase({ environmentId, databaseName, databaseUrl });
5034
- } else {
5035
- warnings.push(`No adapter for driver '${driver}'; physical DB for project ${environmentId} not released.`);
5036
- }
5037
- } catch (err) {
5038
- warnings.push(
5039
- `Failed to delete physical database for project ${environmentId}: ${err instanceof Error ? err.message : String(err)}`
5040
- );
5041
- }
5042
- try {
5043
- await ql.delete(ENV, { where: { id: environmentId } });
5044
- } catch (err) {
5045
- return {
5046
- ok: false,
5047
- status: 500,
5048
- error: `Failed to delete sys_environment row: ${err instanceof Error ? err.message : String(err)}`,
5049
- warnings
5050
- };
5051
- }
5052
- if (this.envRegistry?.invalidate) {
5053
- try {
5054
- await this.envRegistry.invalidate(environmentId);
5055
- } catch {
5056
3981
  }
3982
+ const apiObj = authService?.auth?.api ?? authService?.api;
3983
+ const sessionData = await apiObj?.getSession?.call(apiObj, { headers });
3984
+ const oid = sessionData?.session?.activeOrganizationId;
3985
+ return typeof oid === "string" && oid.length > 0 ? oid : void 0;
3986
+ } catch {
3987
+ return void 0;
5057
3988
  }
5058
- return { ok: true, warnings };
5059
3989
  }
5060
3990
  /**
5061
3991
  * Handles Storage requests
@@ -5127,6 +4057,8 @@ var _HttpDispatcher = class _HttpDispatcher {
5127
4057
  *
5128
4058
  * Routes:
5129
4059
  * GET / → listFlows
4060
+ * GET /actions → getActionDescriptors (ADR-0018; ?paradigm/?source/?category filters)
4061
+ * GET /connectors → getConnectorDescriptors (ADR-0022; ?type filter)
5130
4062
  * GET /:name → getFlow
5131
4063
  * POST / → createFlow (registerFlow)
5132
4064
  * PUT /:name → updateFlow
@@ -5135,6 +4067,8 @@ var _HttpDispatcher = class _HttpDispatcher {
5135
4067
  * POST /:name/toggle → toggleFlow
5136
4068
  * GET /:name/runs → listRuns
5137
4069
  * GET /:name/runs/:runId → getRun
4070
+ * POST /:name/runs/:runId/resume → resume a paused run (screen input / ADR-0019)
4071
+ * GET /:name/runs/:runId/screen → the screen a paused run awaits
5138
4072
  */
5139
4073
  async handleAutomation(path, method, body, context, query) {
5140
4074
  const automationService = await this.getService(CoreServiceName.enum.automation);
@@ -5164,6 +4098,32 @@ var _HttpDispatcher = class _HttpDispatcher {
5164
4098
  return { handled: true, response: this.success(body) };
5165
4099
  }
5166
4100
  }
4101
+ if (parts[0] === "actions" && parts.length === 1 && m === "GET") {
4102
+ if (typeof automationService.getActionDescriptors === "function") {
4103
+ let actions = automationService.getActionDescriptors() ?? [];
4104
+ if (query?.paradigm) {
4105
+ actions = actions.filter((a) => Array.isArray(a?.paradigms) && a.paradigms.includes(query.paradigm));
4106
+ }
4107
+ if (query?.source) {
4108
+ actions = actions.filter((a) => a?.source === query.source);
4109
+ }
4110
+ if (query?.category) {
4111
+ actions = actions.filter((a) => a?.category === query.category);
4112
+ }
4113
+ return { handled: true, response: this.success({ actions, total: actions.length }) };
4114
+ }
4115
+ return { handled: true, response: this.success({ actions: [], total: 0 }) };
4116
+ }
4117
+ if (parts[0] === "connectors" && parts.length === 1 && m === "GET") {
4118
+ if (typeof automationService.getConnectorDescriptors === "function") {
4119
+ let connectors = automationService.getConnectorDescriptors() ?? [];
4120
+ if (query?.type) {
4121
+ connectors = connectors.filter((c) => c?.type === query.type);
4122
+ }
4123
+ return { handled: true, response: this.success({ connectors, total: connectors.length }) };
4124
+ }
4125
+ return { handled: true, response: this.success({ connectors: [], total: 0 }) };
4126
+ }
5167
4127
  if (parts.length >= 1) {
5168
4128
  const name = parts[0];
5169
4129
  if (parts[1] === "trigger" && m === "POST") {
@@ -5203,7 +4163,28 @@ var _HttpDispatcher = class _HttpDispatcher {
5203
4163
  return { handled: true, response: this.success({ name, enabled: body?.enabled ?? true }) };
5204
4164
  }
5205
4165
  }
5206
- if (parts[1] === "runs" && parts[2] && m === "GET") {
4166
+ if (parts[1] === "runs" && parts[2] && parts[3] === "resume" && m === "POST") {
4167
+ if (typeof automationService.resume === "function") {
4168
+ const b = body && typeof body === "object" ? body : {};
4169
+ const inputs = b.inputs ?? b.variables;
4170
+ const signal = {};
4171
+ if (inputs && typeof inputs === "object") signal.variables = inputs;
4172
+ if (b.output && typeof b.output === "object") signal.output = b.output;
4173
+ if (typeof b.branchLabel === "string") signal.branchLabel = b.branchLabel;
4174
+ const result = await automationService.resume(parts[2], signal);
4175
+ return { handled: true, response: this.success(result) };
4176
+ }
4177
+ return { handled: true, response: this.error("Resume not supported", 501) };
4178
+ }
4179
+ if (parts[1] === "runs" && parts[2] && parts[3] === "screen" && m === "GET") {
4180
+ if (typeof automationService.getSuspendedScreen === "function") {
4181
+ const screen = automationService.getSuspendedScreen(parts[2]);
4182
+ if (!screen) return { handled: true, response: this.error("No pending screen for run", 404) };
4183
+ return { handled: true, response: this.success({ runId: parts[2], screen }) };
4184
+ }
4185
+ return { handled: true, response: this.error("Screen lookup not supported", 501) };
4186
+ }
4187
+ if (parts[1] === "runs" && parts[2] && !parts[3] && m === "GET") {
5207
4188
  if (typeof automationService.getRun === "function") {
5208
4189
  const run = await automationService.getRun(parts[2]);
5209
4190
  if (!run) return { handled: true, response: this.error("Execution not found", 404) };
@@ -5529,11 +4510,9 @@ var _HttpDispatcher = class _HttpDispatcher {
5529
4510
  if (forbidden) {
5530
4511
  return { handled: true, response: forbidden };
5531
4512
  }
5532
- if (!cleanPath.startsWith("/cloud/")) {
5533
- const scopedMatch = cleanPath.match(/^\/projects\/[^/]+(\/.*)?$/);
5534
- if (scopedMatch) {
5535
- cleanPath = scopedMatch[1] ?? "";
5536
- }
4513
+ const scopedMatch = cleanPath.match(/^\/projects\/[^/]+(\/.*)?$/);
4514
+ if (scopedMatch) {
4515
+ cleanPath = scopedMatch[1] ?? "";
5537
4516
  }
5538
4517
  try {
5539
4518
  if ((cleanPath === "/discovery" || cleanPath === "") && method === "GET") {
@@ -5584,9 +4563,6 @@ var _HttpDispatcher = class _HttpDispatcher {
5584
4563
  if (cleanPath.startsWith("/packages")) {
5585
4564
  return this.handlePackages(cleanPath.substring(9), method, body, query, context);
5586
4565
  }
5587
- if (cleanPath.startsWith("/cloud")) {
5588
- return this.handleCloud(cleanPath.substring(6), method, body, query, context);
5589
- }
5590
4566
  if (cleanPath.startsWith("/i18n")) {
5591
4567
  return this.handleI18n(cleanPath.substring(5), method, query, context);
5592
4568
  }
@@ -6291,257 +5267,57 @@ function createDispatcherPlugin(config = {}) {
6291
5267
  errorResponse(err, res);
6292
5268
  }
6293
5269
  });
6294
- server.get(`${prefix}/packages/:id`, async (req, res) => {
6295
- try {
6296
- const result = await dispatcher.handlePackages(`/${req.params.id}`, "GET", {}, req.query, { request: req });
6297
- sendResult(result, res);
6298
- } catch (err) {
6299
- errorResponse(err, res);
6300
- }
6301
- });
6302
- server.delete(`${prefix}/packages/:id`, async (req, res) => {
6303
- try {
6304
- const result = await dispatcher.handlePackages(`/${req.params.id}`, "DELETE", {}, {}, { request: req });
6305
- sendResult(result, res);
6306
- } catch (err) {
6307
- errorResponse(err, res);
6308
- }
6309
- });
6310
- server.patch(`${prefix}/packages/:id/enable`, async (req, res) => {
6311
- try {
6312
- const result = await dispatcher.handlePackages(`/${req.params.id}/enable`, "PATCH", {}, {}, { request: req });
6313
- sendResult(result, res);
6314
- } catch (err) {
6315
- errorResponse(err, res);
6316
- }
6317
- });
6318
- server.patch(`${prefix}/packages/:id/disable`, async (req, res) => {
6319
- try {
6320
- const result = await dispatcher.handlePackages(`/${req.params.id}/disable`, "PATCH", {}, {}, { request: req });
6321
- sendResult(result, res);
6322
- } catch (err) {
6323
- errorResponse(err, res);
6324
- }
6325
- });
6326
- server.post(`${prefix}/packages/:id/publish`, async (req, res) => {
6327
- try {
6328
- const result = await dispatcher.handlePackages(`/${req.params.id}/publish`, "POST", req.body, {}, { request: req });
6329
- sendResult(result, res);
6330
- } catch (err) {
6331
- errorResponse(err, res);
6332
- }
6333
- });
6334
- server.post(`${prefix}/packages/:id/revert`, async (req, res) => {
6335
- try {
6336
- const result = await dispatcher.handlePackages(`/${req.params.id}/revert`, "POST", req.body, {}, { request: req });
6337
- sendResult(result, res);
6338
- } catch (err) {
6339
- errorResponse(err, res);
6340
- }
6341
- });
6342
- server.get(`${prefix}/cloud/drivers`, async (req, res) => {
6343
- try {
6344
- const result = await dispatcher.handleCloud("/drivers", "GET", {}, req.query, { request: req });
6345
- sendResult(result, res);
6346
- } catch (err) {
6347
- errorResponse(err, res);
6348
- }
6349
- });
6350
- server.post(`${prefix}/cloud/admin/platform-sso/backfill`, async (req, res) => {
6351
- try {
6352
- const result = await dispatcher.handleCloud("/admin/platform-sso/backfill", "POST", req.body, req.query, { request: req });
6353
- sendResult(result, res);
6354
- } catch (err) {
6355
- errorResponse(err, res);
6356
- }
6357
- });
6358
- server.get(`${prefix}/cloud/templates`, async (req, res) => {
6359
- try {
6360
- const result = await dispatcher.handleCloud("/templates", "GET", {}, req.query, { request: req });
6361
- sendResult(result, res);
6362
- } catch (err) {
6363
- errorResponse(err, res);
6364
- }
6365
- });
6366
- server.get(`${prefix}/cloud/environments`, async (req, res) => {
6367
- try {
6368
- const result = await dispatcher.handleCloud("/projects", "GET", {}, req.query, { request: req });
6369
- sendResult(result, res);
6370
- } catch (err) {
6371
- errorResponse(err, res);
6372
- }
6373
- });
6374
- server.post(`${prefix}/cloud/environments`, async (req, res) => {
6375
- try {
6376
- const result = await dispatcher.handleCloud("/projects", "POST", req.body, {}, { request: req });
6377
- sendResult(result, res);
6378
- } catch (err) {
6379
- errorResponse(err, res);
6380
- }
6381
- });
6382
- server.get(`${prefix}/cloud/environments/:id`, async (req, res) => {
6383
- try {
6384
- const result = await dispatcher.handleCloud(`/projects/${req.params.id}`, "GET", {}, req.query, { request: req });
6385
- sendResult(result, res);
6386
- } catch (err) {
6387
- errorResponse(err, res);
6388
- }
6389
- });
6390
- server.patch(`${prefix}/cloud/environments/:id`, async (req, res) => {
6391
- try {
6392
- const result = await dispatcher.handleCloud(`/projects/${req.params.id}`, "PATCH", req.body, {}, { request: req });
6393
- sendResult(result, res);
6394
- } catch (err) {
6395
- errorResponse(err, res);
6396
- }
6397
- });
6398
- server.delete(`${prefix}/cloud/environments/:id`, async (req, res) => {
6399
- try {
6400
- const result = await dispatcher.handleCloud(`/projects/${req.params.id}`, "DELETE", {}, req.query, { request: req });
6401
- sendResult(result, res);
6402
- } catch (err) {
6403
- errorResponse(err, res);
6404
- }
6405
- });
6406
- server.delete(`${prefix}/cloud/organizations/:id`, async (req, res) => {
6407
- try {
6408
- const result = await dispatcher.handleCloud(`/organizations/${req.params.id}`, "DELETE", {}, req.query, { request: req });
6409
- sendResult(result, res);
6410
- } catch (err) {
6411
- errorResponse(err, res);
6412
- }
6413
- });
6414
- server.post(`${prefix}/cloud/environments/:id/hostname`, async (req, res) => {
6415
- try {
6416
- const result = await dispatcher.handleCloud(`/projects/${req.params.id}/hostname`, "POST", req.body, {}, { request: req });
6417
- sendResult(result, res);
6418
- } catch (err) {
6419
- errorResponse(err, res);
6420
- }
6421
- });
6422
- server.put(`${prefix}/cloud/environments/:id/hostname`, async (req, res) => {
6423
- try {
6424
- const result = await dispatcher.handleCloud(`/projects/${req.params.id}/hostname`, "PUT", req.body, {}, { request: req });
6425
- sendResult(result, res);
6426
- } catch (err) {
6427
- errorResponse(err, res);
6428
- }
6429
- });
6430
- server.post(`${prefix}/cloud/environments/:id/rotate-credential`, async (req, res) => {
6431
- try {
6432
- const result = await dispatcher.handleCloud(`/projects/${req.params.id}/rotate-credential`, "POST", req.body, {}, { request: req });
6433
- sendResult(result, res);
6434
- } catch (err) {
6435
- errorResponse(err, res);
6436
- }
6437
- });
6438
- server.post(`${prefix}/cloud/environments/:id/credentials/rotate`, async (req, res) => {
6439
- try {
6440
- const result = await dispatcher.handleCloud(`/projects/${req.params.id}/credentials/rotate`, "POST", req.body, {}, { request: req });
6441
- sendResult(result, res);
6442
- } catch (err) {
6443
- errorResponse(err, res);
6444
- }
6445
- });
6446
- server.post(`${prefix}/cloud/environments/:id/activate`, async (req, res) => {
6447
- try {
6448
- const result = await dispatcher.handleCloud(`/projects/${req.params.id}/activate`, "POST", req.body, {}, { request: req });
6449
- sendResult(result, res);
6450
- } catch (err) {
6451
- errorResponse(err, res);
6452
- }
6453
- });
6454
- server.post(`${prefix}/cloud/environments/:id/retry`, async (req, res) => {
6455
- try {
6456
- const result = await dispatcher.handleCloud(`/projects/${req.params.id}/retry`, "POST", req.body, {}, { request: req });
6457
- sendResult(result, res);
6458
- } catch (err) {
6459
- errorResponse(err, res);
6460
- }
6461
- });
6462
- server.get(`${prefix}/cloud/environments/:id/members`, async (req, res) => {
6463
- try {
6464
- const result = await dispatcher.handleCloud(`/projects/${req.params.id}/members`, "GET", {}, req.query, { request: req });
6465
- sendResult(result, res);
6466
- } catch (err) {
6467
- errorResponse(err, res);
6468
- }
6469
- });
6470
- server.post(`${prefix}/cloud/environments/:id/members`, async (req, res) => {
6471
- try {
6472
- const result = await dispatcher.handleCloud(`/projects/${req.params.id}/members`, "POST", req.body, {}, { request: req });
6473
- sendResult(result, res);
6474
- } catch (err) {
6475
- errorResponse(err, res);
6476
- }
6477
- });
6478
- server.patch(`${prefix}/cloud/environments/:id/members/:memberId`, async (req, res) => {
6479
- try {
6480
- const result = await dispatcher.handleCloud(`/projects/${req.params.id}/members/${req.params.memberId}`, "PATCH", req.body, {}, { request: req });
6481
- sendResult(result, res);
6482
- } catch (err) {
6483
- errorResponse(err, res);
6484
- }
6485
- });
6486
- server.delete(`${prefix}/cloud/environments/:id/members/:memberId`, async (req, res) => {
6487
- try {
6488
- const result = await dispatcher.handleCloud(`/projects/${req.params.id}/members/${req.params.memberId}`, "DELETE", req.body ?? {}, {}, { request: req });
6489
- sendResult(result, res);
6490
- } catch (err) {
6491
- errorResponse(err, res);
6492
- }
6493
- });
6494
- server.get(`${prefix}/cloud/environments/:id/packages`, async (req, res) => {
5270
+ server.get(`${prefix}/packages/:id/export`, async (req, res) => {
6495
5271
  try {
6496
- const result = await dispatcher.handleCloud(`/projects/${req.params.id}/packages`, "GET", {}, req.query, { request: req });
5272
+ const result = await dispatcher.handlePackages(`/${req.params.id}/export`, "GET", {}, req.query, { request: req });
6497
5273
  sendResult(result, res);
6498
5274
  } catch (err) {
6499
5275
  errorResponse(err, res);
6500
5276
  }
6501
5277
  });
6502
- server.post(`${prefix}/cloud/environments/:id/packages`, async (req, res) => {
5278
+ server.get(`${prefix}/packages/:id`, async (req, res) => {
6503
5279
  try {
6504
- const result = await dispatcher.handleCloud(`/projects/${req.params.id}/packages`, "POST", req.body, {}, { request: req });
5280
+ const result = await dispatcher.handlePackages(`/${req.params.id}`, "GET", {}, req.query, { request: req });
6505
5281
  sendResult(result, res);
6506
5282
  } catch (err) {
6507
5283
  errorResponse(err, res);
6508
5284
  }
6509
5285
  });
6510
- server.get(`${prefix}/cloud/environments/:id/packages/:pkgId`, async (req, res) => {
5286
+ server.delete(`${prefix}/packages/:id`, async (req, res) => {
6511
5287
  try {
6512
- const result = await dispatcher.handleCloud(`/projects/${req.params.id}/packages/${req.params.pkgId}`, "GET", {}, req.query, { request: req });
5288
+ const result = await dispatcher.handlePackages(`/${req.params.id}`, "DELETE", {}, {}, { request: req });
6513
5289
  sendResult(result, res);
6514
5290
  } catch (err) {
6515
5291
  errorResponse(err, res);
6516
5292
  }
6517
5293
  });
6518
- server.delete(`${prefix}/cloud/environments/:id/packages/:pkgId`, async (req, res) => {
5294
+ server.patch(`${prefix}/packages/:id/enable`, async (req, res) => {
6519
5295
  try {
6520
- const result = await dispatcher.handleCloud(`/projects/${req.params.id}/packages/${req.params.pkgId}`, "DELETE", {}, {}, { request: req });
5296
+ const result = await dispatcher.handlePackages(`/${req.params.id}/enable`, "PATCH", {}, {}, { request: req });
6521
5297
  sendResult(result, res);
6522
5298
  } catch (err) {
6523
5299
  errorResponse(err, res);
6524
5300
  }
6525
5301
  });
6526
- server.patch(`${prefix}/cloud/environments/:id/packages/:pkgId/enable`, async (req, res) => {
5302
+ server.patch(`${prefix}/packages/:id/disable`, async (req, res) => {
6527
5303
  try {
6528
- const result = await dispatcher.handleCloud(`/projects/${req.params.id}/packages/${req.params.pkgId}/enable`, "PATCH", {}, {}, { request: req });
5304
+ const result = await dispatcher.handlePackages(`/${req.params.id}/disable`, "PATCH", {}, {}, { request: req });
6529
5305
  sendResult(result, res);
6530
5306
  } catch (err) {
6531
5307
  errorResponse(err, res);
6532
5308
  }
6533
5309
  });
6534
- server.patch(`${prefix}/cloud/environments/:id/packages/:pkgId/disable`, async (req, res) => {
5310
+ server.post(`${prefix}/packages/:id/publish`, async (req, res) => {
6535
5311
  try {
6536
- const result = await dispatcher.handleCloud(`/projects/${req.params.id}/packages/${req.params.pkgId}/disable`, "PATCH", {}, {}, { request: req });
5312
+ const result = await dispatcher.handlePackages(`/${req.params.id}/publish`, "POST", req.body, {}, { request: req });
6537
5313
  sendResult(result, res);
6538
5314
  } catch (err) {
6539
5315
  errorResponse(err, res);
6540
5316
  }
6541
5317
  });
6542
- server.post(`${prefix}/cloud/environments/:id/packages/:pkgId/upgrade`, async (req, res) => {
5318
+ server.post(`${prefix}/packages/:id/revert`, async (req, res) => {
6543
5319
  try {
6544
- const result = await dispatcher.handleCloud(`/projects/${req.params.id}/packages/${req.params.pkgId}/upgrade`, "POST", req.body, {}, { request: req });
5320
+ const result = await dispatcher.handlePackages(`/${req.params.id}/revert`, "POST", req.body, {}, { request: req });
6545
5321
  sendResult(result, res);
6546
5322
  } catch (err) {
6547
5323
  errorResponse(err, res);
@@ -6668,6 +5444,22 @@ function createDispatcherPlugin(config = {}) {
6668
5444
  errorResponse(err, res);
6669
5445
  }
6670
5446
  });
5447
+ server.post(`${base}/automation/:name/runs/:runId/resume`, async (req, res) => {
5448
+ try {
5449
+ const result = await dispatcher.dispatch("POST", `/automation/${req.params.name}/runs/${req.params.runId}/resume`, req.body, req.query, { request: req });
5450
+ sendResult(result, res);
5451
+ } catch (err) {
5452
+ errorResponse(err, res);
5453
+ }
5454
+ });
5455
+ server.get(`${base}/automation/:name/runs/:runId/screen`, async (req, res) => {
5456
+ try {
5457
+ const result = await dispatcher.dispatch("GET", `/automation/${req.params.name}/runs/${req.params.runId}/screen`, void 0, req.query, { request: req });
5458
+ sendResult(result, res);
5459
+ } catch (err) {
5460
+ errorResponse(err, res);
5461
+ }
5462
+ });
6671
5463
  };
6672
5464
  const registerAIRoutes = (base) => {
6673
5465
  const wildcards = [
@@ -7610,19 +6402,15 @@ init_driver_plugin();
7610
6402
  init_app_plugin();
7611
6403
  import { createHmac as createHmac2 } from "crypto";
7612
6404
  import { ObjectKernel as ObjectKernel3 } from "@objectstack/core";
7613
- import { readEnvWithDeprecation as readEnvWithDeprecation4 } from "@objectstack/types";
6405
+ import { readEnvWithDeprecation as readEnvWithDeprecation3 } from "@objectstack/types";
7614
6406
 
7615
6407
  // src/cloud/capability-loader.ts
7616
6408
  var CAPABILITY_PROVIDERS = {
7617
6409
  automation: {
6410
+ // Self-contained: AutomationServicePlugin seeds all built-in node
6411
+ // executors itself (ADR-0018), so no companion node-pack plugins.
7618
6412
  pkg: "@objectstack/service-automation",
7619
- export: "AutomationServicePlugin",
7620
- extras: [
7621
- { pkg: "@objectstack/service-automation", export: "CrudNodesPlugin" },
7622
- { pkg: "@objectstack/service-automation", export: "LogicNodesPlugin" },
7623
- { pkg: "@objectstack/service-automation", export: "HttpConnectorPlugin" },
7624
- { pkg: "@objectstack/service-automation", export: "ScreenNodesPlugin" }
7625
- ]
6413
+ export: "AutomationServicePlugin"
7626
6414
  },
7627
6415
  ai: {
7628
6416
  pkg: "@objectstack/service-ai",
@@ -7653,6 +6441,19 @@ var CAPABILITY_PROVIDERS = {
7653
6441
  pkg: "@objectstack/service-job",
7654
6442
  export: "JobServicePlugin"
7655
6443
  },
6444
+ messaging: {
6445
+ // Backs the `notify` flow node (ADR-0012): delivers to a user's
6446
+ // channels (inbox by default → `sys_inbox_message` rows).
6447
+ pkg: "@objectstack/service-messaging",
6448
+ export: "MessagingServicePlugin"
6449
+ },
6450
+ triggers: {
6451
+ // Concrete flow triggers — record-change (ObjectQL hooks) + schedule
6452
+ // (cron/interval via the job service; pair `triggers` with `job`).
6453
+ pkg: "@objectstack/plugin-trigger-record-change",
6454
+ export: "RecordChangeTriggerPlugin",
6455
+ extras: [{ pkg: "@objectstack/plugin-trigger-schedule", export: "ScheduleTriggerPlugin" }]
6456
+ },
7656
6457
  realtime: {
7657
6458
  pkg: "@objectstack/service-realtime",
7658
6459
  export: "RealtimeServicePlugin"
@@ -7670,7 +6471,11 @@ async function loadCapabilities(opts) {
7670
6471
  const { kernel, requires, bundle, environmentId } = opts;
7671
6472
  const logger = opts.logger ?? console;
7672
6473
  const installed = [];
7673
- for (const cap of requires) {
6474
+ const resolved = [...new Set(requires)];
6475
+ if (resolved.includes("audit") && !resolved.includes("messaging")) {
6476
+ resolved.push("messaging");
6477
+ }
6478
+ for (const cap of resolved) {
7674
6479
  const spec = CAPABILITY_PROVIDERS[cap];
7675
6480
  if (!spec) {
7676
6481
  continue;
@@ -7737,8 +6542,197 @@ async function loadCapabilities(opts) {
7737
6542
  return installed;
7738
6543
  }
7739
6544
 
6545
+ // src/cloud/platform-sso.ts
6546
+ import { createHmac, createHash } from "crypto";
6547
+ var PLATFORM_SSO_PROVIDER_ID = "objectstack-cloud";
6548
+ function derivePlatformSsoClientId(environmentId) {
6549
+ return `project_${environmentId}`;
6550
+ }
6551
+ function derivePlatformSsoClientSecret(baseSecret, environmentId) {
6552
+ return createHmac("sha256", baseSecret).update(`oauth-client:${environmentId}`).digest("hex");
6553
+ }
6554
+ function hashPlatformSsoClientSecret(plaintext) {
6555
+ return createHash("sha256").update(plaintext).digest("base64").replace(/=+$/, "").replace(/\+/g, "-").replace(/\//g, "_");
6556
+ }
6557
+ function buildPlatformSsoRedirectUri(hostname, basePath = "/api/v1/auth") {
6558
+ let host;
6559
+ if (hostname.startsWith("http://") || hostname.startsWith("https://")) {
6560
+ host = hostname;
6561
+ } else if (/(\.|^)localhost(:\d+)?$/i.test(hostname)) {
6562
+ const port = (process.env.OS_RUNTIME_PORT ?? "").trim();
6563
+ const hostWithPort = /:\d+$/.test(hostname) || !port ? hostname : `${hostname}:${port}`;
6564
+ host = `http://${hostWithPort}`;
6565
+ } else {
6566
+ host = `https://${hostname}`;
6567
+ }
6568
+ const trimmed = host.replace(/\/+$/, "");
6569
+ const path = basePath.replace(/\/+$/, "");
6570
+ return `${trimmed}${path}/oauth2/callback/${PLATFORM_SSO_PROVIDER_ID}`;
6571
+ }
6572
+ async function seedPlatformSsoClient(opts) {
6573
+ const { ql, environmentId, hostname, baseSecret, logger, throwOnError } = opts;
6574
+ if (!baseSecret) {
6575
+ logger?.warn?.("[platform-sso] OS_AUTH_SECRET not set \u2014 skipping client seed", { environmentId });
6576
+ return;
6577
+ }
6578
+ const clientId = derivePlatformSsoClientId(environmentId);
6579
+ const clientSecretPlaintext = derivePlatformSsoClientSecret(baseSecret, environmentId);
6580
+ const clientSecretStored = hashPlatformSsoClientSecret(clientSecretPlaintext);
6581
+ const desiredRedirect = hostname ? buildPlatformSsoRedirectUri(hostname) : null;
6582
+ let existing = null;
6583
+ try {
6584
+ const rows = await ql.find("sys_oauth_application", {
6585
+ where: { client_id: clientId },
6586
+ limit: 1
6587
+ }, { context: { isSystem: true } });
6588
+ const list = Array.isArray(rows) ? rows : Array.isArray(rows?.records) ? rows.records : [];
6589
+ existing = list[0] ?? null;
6590
+ } catch (err) {
6591
+ logger?.warn?.("[platform-sso] sys_oauth_application read failed \u2014 skipping seed", {
6592
+ environmentId,
6593
+ error: err?.message
6594
+ });
6595
+ return;
6596
+ }
6597
+ const nowIso = (/* @__PURE__ */ new Date()).toISOString();
6598
+ if (!existing) {
6599
+ const redirects = desiredRedirect ? [desiredRedirect] : [];
6600
+ try {
6601
+ await ql.insert("sys_oauth_application", {
6602
+ id: `oauthc_${environmentId}`,
6603
+ name: `Project ${environmentId}`,
6604
+ client_id: clientId,
6605
+ client_secret: clientSecretStored,
6606
+ type: "web",
6607
+ redirect_uris: JSON.stringify(redirects),
6608
+ grant_types: JSON.stringify(["authorization_code", "refresh_token"]),
6609
+ response_types: JSON.stringify(["code"]),
6610
+ scopes: JSON.stringify(["openid", "email", "profile"]),
6611
+ token_endpoint_auth_method: "client_secret_basic",
6612
+ require_pkce: false,
6613
+ skip_consent: true,
6614
+ disabled: false,
6615
+ subject_type: "public",
6616
+ created_at: nowIso,
6617
+ updated_at: nowIso
6618
+ }, { context: { isSystem: true } });
6619
+ logger?.info?.("[platform-sso] sys_oauth_application row created", { environmentId, clientId });
6620
+ } catch (err) {
6621
+ logger?.warn?.("[platform-sso] sys_oauth_application create failed", {
6622
+ environmentId,
6623
+ error: err?.message
6624
+ });
6625
+ if (throwOnError) throw err;
6626
+ }
6627
+ return;
6628
+ }
6629
+ let currentRedirects = [];
6630
+ try {
6631
+ const raw = existing.redirect_uris;
6632
+ const parsed = typeof raw === "string" ? JSON.parse(raw) : raw;
6633
+ if (Array.isArray(parsed)) currentRedirects = parsed.filter((s) => typeof s === "string");
6634
+ } catch {
6635
+ }
6636
+ const mergedRedirects = desiredRedirect && !currentRedirects.includes(desiredRedirect) ? [...currentRedirects, desiredRedirect] : currentRedirects;
6637
+ const repairPatch = {
6638
+ name: existing.name || `Project ${environmentId}`,
6639
+ client_secret: clientSecretStored,
6640
+ type: existing.type || "web",
6641
+ redirect_uris: JSON.stringify(mergedRedirects),
6642
+ grant_types: JSON.stringify(["authorization_code", "refresh_token"]),
6643
+ response_types: JSON.stringify(["code"]),
6644
+ scopes: JSON.stringify(["openid", "email", "profile"]),
6645
+ token_endpoint_auth_method: "client_secret_basic",
6646
+ require_pkce: false,
6647
+ skip_consent: true,
6648
+ disabled: false,
6649
+ subject_type: "public",
6650
+ updated_at: nowIso
6651
+ };
6652
+ try {
6653
+ await ql.update(
6654
+ "sys_oauth_application",
6655
+ repairPatch,
6656
+ { where: { id: existing.id } },
6657
+ { context: { isSystem: true } }
6658
+ );
6659
+ logger?.info?.("[platform-sso] sys_oauth_application repaired", {
6660
+ environmentId,
6661
+ clientId,
6662
+ redirect_uris: mergedRedirects
6663
+ });
6664
+ } catch (err) {
6665
+ logger?.warn?.("[platform-sso] sys_oauth_application repair failed", {
6666
+ environmentId,
6667
+ error: err?.message
6668
+ });
6669
+ if (throwOnError) throw err;
6670
+ }
6671
+ }
6672
+ async function backfillPlatformSsoClients(opts) {
6673
+ const { ql, baseSecret, logger, limit = 1e3 } = opts;
6674
+ if (!baseSecret) {
6675
+ logger?.warn?.("[platform-sso] backfill skipped \u2014 OS_AUTH_SECRET not set");
6676
+ return { scanned: 0, seeded: 0, alreadyExisted: 0, failures: [] };
6677
+ }
6678
+ let projects = [];
6679
+ try {
6680
+ const rows = await ql.find("sys_environment", {
6681
+ limit,
6682
+ fields: ["id", "hostname", "status"]
6683
+ }, { context: { isSystem: true } });
6684
+ projects = Array.isArray(rows) ? rows : Array.isArray(rows?.records) ? rows.records : [];
6685
+ } catch (err) {
6686
+ logger?.warn?.("[platform-sso] backfill: sys_environment read failed", {
6687
+ error: err?.message
6688
+ });
6689
+ return { scanned: 0, seeded: 0, alreadyExisted: 0, failures: [{ environmentId: "<scan>", error: err?.message ?? String(err) }] };
6690
+ }
6691
+ let seeded = 0;
6692
+ let alreadyExisted = 0;
6693
+ const failures = [];
6694
+ for (const p of projects) {
6695
+ if (!p?.id) continue;
6696
+ const before = await (async () => {
6697
+ try {
6698
+ const r = await ql.find("sys_oauth_application", {
6699
+ where: { client_id: derivePlatformSsoClientId(p.id) },
6700
+ limit: 1
6701
+ }, { context: { isSystem: true } });
6702
+ const list = Array.isArray(r) ? r : Array.isArray(r?.records) ? r.records : [];
6703
+ return list[0] ?? null;
6704
+ } catch {
6705
+ return null;
6706
+ }
6707
+ })();
6708
+ try {
6709
+ await seedPlatformSsoClient({ ql, environmentId: p.id, hostname: p.hostname, baseSecret, logger, throwOnError: true });
6710
+ if (before) alreadyExisted++;
6711
+ else {
6712
+ const after = await (async () => {
6713
+ try {
6714
+ const r = await ql.find("sys_oauth_application", {
6715
+ where: { client_id: derivePlatformSsoClientId(p.id) },
6716
+ limit: 1
6717
+ }, { context: { isSystem: true } });
6718
+ const list = Array.isArray(r) ? r : Array.isArray(r?.records) ? r.records : [];
6719
+ return list[0] ?? null;
6720
+ } catch (err) {
6721
+ return { _readErr: err?.message };
6722
+ }
6723
+ })();
6724
+ if (after && !after._readErr) seeded++;
6725
+ else failures.push({ environmentId: p.id, error: `post-insert read returned ${after ? JSON.stringify(after) : "null"}` });
6726
+ }
6727
+ } catch (err) {
6728
+ failures.push({ environmentId: p.id, error: err?.message ?? String(err) });
6729
+ }
6730
+ }
6731
+ logger?.info?.("[platform-sso] backfill complete", { scanned: projects.length, seeded, alreadyExisted, failures: failures.length });
6732
+ return { scanned: projects.length, seeded, alreadyExisted, failures };
6733
+ }
6734
+
7740
6735
  // src/cloud/artifact-kernel-factory.ts
7741
- init_platform_sso();
7742
6736
  function deriveProjectAuthSecret(baseSecret, environmentId) {
7743
6737
  return createHmac2("sha256", baseSecret).update(`project:${environmentId}`).digest("hex");
7744
6738
  }
@@ -7748,7 +6742,7 @@ var ArtifactKernelFactory = class {
7748
6742
  this.envRegistry = config.envRegistry;
7749
6743
  this.logger = config.logger ?? console;
7750
6744
  this.kernelConfig = config.kernelConfig;
7751
- this.authBaseSecret = (config.authBaseSecret ?? readEnvWithDeprecation4("OS_AUTH_SECRET", ["AUTH_SECRET", "BETTER_AUTH_SECRET"]) ?? "").trim();
6745
+ this.authBaseSecret = (config.authBaseSecret ?? readEnvWithDeprecation3("OS_AUTH_SECRET", ["AUTH_SECRET", "BETTER_AUTH_SECRET"]) ?? "").trim();
7752
6746
  }
7753
6747
  async create(environmentId) {
7754
6748
  let cached = this.envRegistry.peekById(environmentId);
@@ -7861,7 +6855,7 @@ var ArtifactKernelFactory = class {
7861
6855
  this.logger.warn?.("[ArtifactKernelFactory] OS_AUTH_SECRET not set \u2014 per-project AuthPlugin skipped (auth endpoints will return 404)", { environmentId });
7862
6856
  }
7863
6857
  try {
7864
- const multiTenant = String(readEnvWithDeprecation4("OS_MULTI_ORG_ENABLED", "OS_MULTI_TENANT") ?? "false").toLowerCase() !== "false";
6858
+ const multiTenant = String(readEnvWithDeprecation3("OS_MULTI_ORG_ENABLED", "OS_MULTI_TENANT") ?? "false").toLowerCase() !== "false";
7865
6859
  if (multiTenant) {
7866
6860
  try {
7867
6861
  const { OrgScopingPlugin } = await import("@objectstack/plugin-org-scoping");
@@ -8387,7 +7381,7 @@ var AuthProxyPlugin = class {
8387
7381
  };
8388
7382
 
8389
7383
  // src/cloud/cloud-url.ts
8390
- var DEFAULT_CLOUD_URL = "https://cloud.objectos.app";
7384
+ var DEFAULT_CLOUD_URL = "https://cloud.objectos.ai";
8391
7385
  function resolveCloudUrl(explicit) {
8392
7386
  const raw = (explicit ?? process.env.OS_CLOUD_URL ?? "").trim();
8393
7387
  const lower = raw.toLowerCase();
@@ -9062,9 +8056,9 @@ async function createObjectOSStack(config) {
9062
8056
  }
9063
8057
 
9064
8058
  // src/cloud/marketplace-install-local-plugin.ts
9065
- import { existsSync as existsSync2, mkdirSync as mkdirSync3, readFileSync, readdirSync, unlinkSync, writeFileSync as writeFileSync2 } from "fs";
9066
- import { join, resolve } from "path";
9067
- import { readEnvWithDeprecation as readEnvWithDeprecation5 } from "@objectstack/types";
8059
+ import { existsSync as existsSync3, mkdirSync as mkdirSync4, readFileSync as readFileSync2, readdirSync, unlinkSync, writeFileSync as writeFileSync3 } from "fs";
8060
+ import { join as join2, resolve } from "path";
8061
+ import { readEnvWithDeprecation as readEnvWithDeprecation4 } from "@objectstack/types";
9068
8062
  var ROUTE_BASE = "/api/v1/marketplace/install-local";
9069
8063
  var DEFAULT_DIR = ".objectstack/installed-packages";
9070
8064
  function safeFilename(manifestId) {
@@ -9139,9 +8133,6 @@ var MarketplaceInstallLocalPlugin = class {
9139
8133
  }
9140
8134
  };
9141
8135
  this.handleInstall = async (c, ctx) => {
9142
- if (!this.cloudUrl) {
9143
- return c.json({ success: false, error: { code: "marketplace_unavailable", message: "OS_CLOUD_URL not configured." } }, 503);
9144
- }
9145
8136
  const userId = await this.requireAuthenticatedUser(c, ctx);
9146
8137
  if (!userId) {
9147
8138
  return c.json({ success: false, error: { code: "unauthorized", message: "Authentication required to install packages." } }, 401);
@@ -9151,69 +8142,87 @@ var MarketplaceInstallLocalPlugin = class {
9151
8142
  body = await c.req.json();
9152
8143
  } catch {
9153
8144
  }
9154
- const packageId = String(body?.packageId ?? "").trim();
9155
- const versionId = String(body?.versionId ?? "latest").trim() || "latest";
9156
- if (!packageId) {
9157
- return c.json({ success: false, error: { code: "bad_request", message: "packageId is required." } }, 400);
9158
- }
9159
- let payload;
9160
- const publicBase = resolveMarketplacePublicBaseUrl();
9161
- const fetchAttempts = [];
9162
- if (publicBase) {
8145
+ const inlineManifest = body?.manifest && typeof body.manifest === "object" ? body.manifest : null;
8146
+ let manifest;
8147
+ let resolvedVersionId;
8148
+ let version;
8149
+ let packageId;
8150
+ if (inlineManifest) {
8151
+ manifest = inlineManifest;
8152
+ packageId = String(manifest.id ?? manifest.name ?? "").trim();
8153
+ version = String(manifest.version ?? "unknown");
8154
+ resolvedVersionId = String(body?.versionId ?? version);
8155
+ if (!packageId) {
8156
+ return c.json({ success: false, error: { code: "invalid_manifest", message: 'Inline manifest must have an "id" or "name".' } }, 400);
8157
+ }
8158
+ } else {
8159
+ if (!this.cloudUrl) {
8160
+ return c.json({ success: false, error: { code: "marketplace_unavailable", message: "OS_CLOUD_URL not configured." } }, 503);
8161
+ }
8162
+ packageId = String(body?.packageId ?? "").trim();
8163
+ const versionId = String(body?.versionId ?? "latest").trim() || "latest";
8164
+ if (!packageId) {
8165
+ return c.json({ success: false, error: { code: "bad_request", message: "packageId is required." } }, 400);
8166
+ }
8167
+ let payload;
8168
+ const publicBase = resolveMarketplacePublicBaseUrl();
8169
+ const fetchAttempts = [];
8170
+ if (publicBase) {
8171
+ fetchAttempts.push({
8172
+ label: "public-r2",
8173
+ url: `${publicBase}/packages/${encodeURIComponent(packageId)}/versions/${encodeURIComponent(versionId)}/manifest.json`
8174
+ });
8175
+ }
9163
8176
  fetchAttempts.push({
9164
- label: "public-r2",
9165
- url: `${publicBase}/packages/${encodeURIComponent(packageId)}/versions/${encodeURIComponent(versionId)}/manifest.json`
8177
+ label: "cloud",
8178
+ url: `${this.cloudUrl}/api/v1/marketplace/packages/${encodeURIComponent(packageId)}/versions/${encodeURIComponent(versionId)}/manifest`
9166
8179
  });
9167
- }
9168
- fetchAttempts.push({
9169
- label: "cloud",
9170
- url: `${this.cloudUrl}/api/v1/marketplace/packages/${encodeURIComponent(packageId)}/versions/${encodeURIComponent(versionId)}/manifest`
9171
- });
9172
- let lastErrStatus = 0;
9173
- let lastErrText = "";
9174
- for (const attempt of fetchAttempts) {
9175
- try {
9176
- const resp = await fetch(attempt.url, { headers: { "Accept": "application/json" } });
9177
- if (!resp.ok) {
9178
- lastErrStatus = resp.status;
9179
- lastErrText = (await resp.text().catch(() => "")).slice(0, 200);
9180
- if (attempt.label === "public-r2" && resp.status === 404) {
9181
- ctx.logger?.info?.(`[MarketplaceInstallLocal] public-r2 miss for ${packageId}@${versionId}, falling back to cloud`);
9182
- continue;
8180
+ let lastErrStatus = 0;
8181
+ let lastErrText = "";
8182
+ for (const attempt of fetchAttempts) {
8183
+ try {
8184
+ const resp = await fetch(attempt.url, { headers: { "Accept": "application/json" } });
8185
+ if (!resp.ok) {
8186
+ lastErrStatus = resp.status;
8187
+ lastErrText = (await resp.text().catch(() => "")).slice(0, 200);
8188
+ if (attempt.label === "public-r2" && resp.status === 404) {
8189
+ ctx.logger?.info?.(`[MarketplaceInstallLocal] public-r2 miss for ${packageId}@${versionId}, falling back to cloud`);
8190
+ continue;
8191
+ }
8192
+ if (attempt.label === "public-r2" && resp.status >= 500) {
8193
+ ctx.logger?.warn?.(`[MarketplaceInstallLocal] public-r2 ${resp.status}, falling back to cloud`);
8194
+ continue;
8195
+ }
8196
+ break;
9183
8197
  }
9184
- if (attempt.label === "public-r2" && resp.status >= 500) {
9185
- ctx.logger?.warn?.(`[MarketplaceInstallLocal] public-r2 ${resp.status}, falling back to cloud`);
8198
+ payload = await resp.json();
8199
+ lastErrStatus = 0;
8200
+ break;
8201
+ } catch (err) {
8202
+ if (attempt.label === "public-r2") {
8203
+ ctx.logger?.warn?.(`[MarketplaceInstallLocal] public-r2 fetch error: ${err?.message ?? err}, falling back to cloud`);
9186
8204
  continue;
9187
8205
  }
9188
- break;
9189
- }
9190
- payload = await resp.json();
9191
- lastErrStatus = 0;
9192
- break;
9193
- } catch (err) {
9194
- if (attempt.label === "public-r2") {
9195
- ctx.logger?.warn?.(`[MarketplaceInstallLocal] public-r2 fetch error: ${err?.message ?? err}, falling back to cloud`);
9196
- continue;
8206
+ return c.json({
8207
+ success: false,
8208
+ error: { code: "cloud_fetch_failed", message: err?.message ?? String(err) }
8209
+ }, 502);
9197
8210
  }
8211
+ }
8212
+ if (!payload) {
9198
8213
  return c.json({
9199
8214
  success: false,
9200
- error: { code: "cloud_fetch_failed", message: err?.message ?? String(err) }
9201
- }, 502);
8215
+ error: { code: "cloud_fetch_failed", message: `Cloud returned ${lastErrStatus}: ${lastErrText}` }
8216
+ }, lastErrStatus === 404 ? 404 : 502);
9202
8217
  }
8218
+ const data = payload?.data ?? payload;
8219
+ manifest = data?.manifest;
8220
+ resolvedVersionId = String(data?.version_id ?? versionId);
8221
+ version = String(data?.version ?? "unknown");
9203
8222
  }
9204
- if (!payload) {
9205
- return c.json({
9206
- success: false,
9207
- error: { code: "cloud_fetch_failed", message: `Cloud returned ${lastErrStatus}: ${lastErrText}` }
9208
- }, lastErrStatus === 404 ? 404 : 502);
9209
- }
9210
- const data = payload?.data ?? payload;
9211
- const manifest = data?.manifest;
9212
- const resolvedVersionId = String(data?.version_id ?? versionId);
9213
- const version = String(data?.version ?? "unknown");
9214
8223
  const manifestId = String(manifest?.id ?? manifest?.name ?? "");
9215
8224
  if (!manifest || !manifestId) {
9216
- return c.json({ success: false, error: { code: "invalid_manifest", message: "Cloud returned an invalid manifest payload." } }, 502);
8225
+ return c.json({ success: false, error: { code: "invalid_manifest", message: "Invalid manifest payload." } }, inlineManifest ? 400 : 502);
9217
8226
  }
9218
8227
  const conflict = this.findConflict(ctx, manifestId);
9219
8228
  if (conflict === "user-code") {
@@ -9225,6 +8234,18 @@ var MarketplaceInstallLocalPlugin = class {
9225
8234
  }
9226
8235
  }, 409);
9227
8236
  }
8237
+ try {
8238
+ const manifestService = ctx.getService("manifest");
8239
+ manifestService.register(manifest);
8240
+ } catch (err) {
8241
+ if (inlineManifest) {
8242
+ return c.json({
8243
+ success: false,
8244
+ error: { code: "register_failed", message: `Failed to register imported manifest: ${err?.message ?? err}` }
8245
+ }, 422);
8246
+ }
8247
+ ctx.logger?.warn?.(`[MarketplaceInstallLocal] hot-register failed for ${manifestId} (will load on next restart): ${err?.message ?? err}`);
8248
+ }
9228
8249
  const entry = {
9229
8250
  packageId,
9230
8251
  versionId: resolvedVersionId,
@@ -9236,20 +8257,14 @@ var MarketplaceInstallLocalPlugin = class {
9236
8257
  withSampleData: false
9237
8258
  };
9238
8259
  try {
9239
- mkdirSync3(this.storageDir, { recursive: true });
9240
- writeFileSync2(join(this.storageDir, safeFilename(manifestId)), JSON.stringify(entry, null, 2), "utf8");
8260
+ mkdirSync4(this.storageDir, { recursive: true });
8261
+ writeFileSync3(join2(this.storageDir, safeFilename(manifestId)), JSON.stringify(entry, null, 2), "utf8");
9241
8262
  } catch (err) {
9242
8263
  return c.json({
9243
8264
  success: false,
9244
8265
  error: { code: "storage_failed", message: `Failed to persist manifest: ${err?.message ?? err}` }
9245
8266
  }, 500);
9246
8267
  }
9247
- try {
9248
- const manifestService = ctx.getService("manifest");
9249
- manifestService.register(manifest);
9250
- } catch (err) {
9251
- ctx.logger?.warn?.(`[MarketplaceInstallLocal] hot-register failed for ${manifestId} (will load on next restart): ${err?.message ?? err}`);
9252
- }
9253
8268
  try {
9254
8269
  const ql = ctx.getService("objectql");
9255
8270
  if (ql && typeof ql.syncSchemas === "function") {
@@ -9263,7 +8278,7 @@ var MarketplaceInstallLocalPlugin = class {
9263
8278
  if (seededSummary.seeded.mode === "inline" && (seededSummary.seeded.inserted ?? 0) + (seededSummary.seeded.updated ?? 0) > 0) {
9264
8279
  entry.withSampleData = true;
9265
8280
  try {
9266
- writeFileSync2(join(this.storageDir, safeFilename(manifestId)), JSON.stringify(entry, null, 2), "utf8");
8281
+ writeFileSync3(join2(this.storageDir, safeFilename(manifestId)), JSON.stringify(entry, null, 2), "utf8");
9267
8282
  } catch {
9268
8283
  }
9269
8284
  }
@@ -9310,8 +8325,8 @@ var MarketplaceInstallLocalPlugin = class {
9310
8325
  if (!manifestId) {
9311
8326
  return c.json({ success: false, error: { code: "bad_request", message: "manifestId path param required." } }, 400);
9312
8327
  }
9313
- const file = join(this.storageDir, safeFilename(manifestId));
9314
- if (!existsSync2(file)) {
8328
+ const file = join2(this.storageDir, safeFilename(manifestId));
8329
+ if (!existsSync3(file)) {
9315
8330
  return c.json({ success: false, error: { code: "not_found", message: `No marketplace install for ${manifestId}.` } }, 404);
9316
8331
  }
9317
8332
  try {
@@ -9338,7 +8353,7 @@ var MarketplaceInstallLocalPlugin = class {
9338
8353
  * (refuse to avoid silently overwriting authored code)
9339
8354
  */
9340
8355
  this.findConflict = (ctx, manifestId) => {
9341
- if (existsSync2(join(this.storageDir, safeFilename(manifestId)))) {
8356
+ if (existsSync3(join2(this.storageDir, safeFilename(manifestId)))) {
9342
8357
  return "marketplace";
9343
8358
  }
9344
8359
  try {
@@ -9380,13 +8395,13 @@ var MarketplaceInstallLocalPlugin = class {
9380
8395
  if (!manifestId) {
9381
8396
  return c.json({ success: false, error: { code: "bad_request", message: "manifestId path param required." } }, 400);
9382
8397
  }
9383
- const file = join(this.storageDir, safeFilename(manifestId));
9384
- if (!existsSync2(file)) {
8398
+ const file = join2(this.storageDir, safeFilename(manifestId));
8399
+ if (!existsSync3(file)) {
9385
8400
  return c.json({ success: false, error: { code: "not_found", message: `No marketplace install for ${manifestId}.` } }, 404);
9386
8401
  }
9387
8402
  let entry;
9388
8403
  try {
9389
- entry = JSON.parse(readFileSync(file, "utf8"));
8404
+ entry = JSON.parse(readFileSync2(file, "utf8"));
9390
8405
  } catch (err) {
9391
8406
  return c.json({ success: false, error: { code: "storage_failed", message: `Failed to read manifest cache: ${err?.message ?? err}` } }, 500);
9392
8407
  }
@@ -9402,7 +8417,7 @@ var MarketplaceInstallLocalPlugin = class {
9402
8417
  }
9403
8418
  try {
9404
8419
  entry.withSampleData = true;
9405
- writeFileSync2(file, JSON.stringify(entry, null, 2), "utf8");
8420
+ writeFileSync3(file, JSON.stringify(entry, null, 2), "utf8");
9406
8421
  } catch {
9407
8422
  }
9408
8423
  return c.json({
@@ -9434,13 +8449,13 @@ var MarketplaceInstallLocalPlugin = class {
9434
8449
  if (!manifestId) {
9435
8450
  return c.json({ success: false, error: { code: "bad_request", message: "manifestId path param required." } }, 400);
9436
8451
  }
9437
- const file = join(this.storageDir, safeFilename(manifestId));
9438
- if (!existsSync2(file)) {
8452
+ const file = join2(this.storageDir, safeFilename(manifestId));
8453
+ if (!existsSync3(file)) {
9439
8454
  return c.json({ success: false, error: { code: "not_found", message: `No marketplace install for ${manifestId}.` } }, 404);
9440
8455
  }
9441
8456
  let entry;
9442
8457
  try {
9443
- entry = JSON.parse(readFileSync(file, "utf8"));
8458
+ entry = JSON.parse(readFileSync2(file, "utf8"));
9444
8459
  } catch (err) {
9445
8460
  return c.json({ success: false, error: { code: "storage_failed", message: `Failed to read manifest cache: ${err?.message ?? err}` } }, 500);
9446
8461
  }
@@ -9489,7 +8504,7 @@ var MarketplaceInstallLocalPlugin = class {
9489
8504
  }
9490
8505
  try {
9491
8506
  entry.withSampleData = false;
9492
- writeFileSync2(file, JSON.stringify(entry, null, 2), "utf8");
8507
+ writeFileSync3(file, JSON.stringify(entry, null, 2), "utf8");
9493
8508
  } catch {
9494
8509
  }
9495
8510
  ctx.logger?.info?.(`[MarketplaceInstallLocal] purged ${manifestId}: deleted=${deleted} skipped=${skipped} errors=${errors}`);
@@ -9587,7 +8602,7 @@ var MarketplaceInstallLocalPlugin = class {
9587
8602
  }
9588
8603
  }
9589
8604
  if (opts.seedNow && datasets.length > 0) {
9590
- const multiTenant = String(readEnvWithDeprecation5("OS_MULTI_ORG_ENABLED", "OS_MULTI_TENANT") ?? "false").toLowerCase() !== "false";
8605
+ const multiTenant = String(readEnvWithDeprecation4("OS_MULTI_ORG_ENABLED", "OS_MULTI_TENANT") ?? "false").toLowerCase() !== "false";
9591
8606
  try {
9592
8607
  const ql = ctx.getService("objectql");
9593
8608
  let metadata;
@@ -9688,12 +8703,12 @@ var MarketplaceInstallLocalPlugin = class {
9688
8703
  return null;
9689
8704
  };
9690
8705
  this.readAll = () => {
9691
- if (!existsSync2(this.storageDir)) return [];
8706
+ if (!existsSync3(this.storageDir)) return [];
9692
8707
  const out = [];
9693
8708
  for (const name of readdirSync(this.storageDir)) {
9694
8709
  if (!name.endsWith(".json")) continue;
9695
8710
  try {
9696
- const raw = readFileSync(join(this.storageDir, name), "utf8");
8711
+ const raw = readFileSync2(join2(this.storageDir, name), "utf8");
9697
8712
  out.push(JSON.parse(raw));
9698
8713
  } catch {
9699
8714
  }
@@ -9705,9 +8720,6 @@ var MarketplaceInstallLocalPlugin = class {
9705
8720
  }
9706
8721
  };
9707
8722
 
9708
- // src/index.ts
9709
- init_platform_sso();
9710
-
9711
8723
  // src/sandbox/script-runner.ts
9712
8724
  var UnimplementedScriptRunner = class {
9713
8725
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
@@ -9738,7 +8750,7 @@ import {
9738
8750
  createRestApiPlugin
9739
8751
  } from "@objectstack/rest";
9740
8752
  export * from "@objectstack/core";
9741
- import { readEnvWithDeprecation as readEnvWithDeprecation6, _resetEnvDeprecationWarnings } from "@objectstack/types";
8753
+ import { readEnvWithDeprecation as readEnvWithDeprecation5, _resetEnvDeprecationWarnings } from "@objectstack/types";
9742
8754
  export {
9743
8755
  AppPlugin,
9744
8756
  ArtifactApiClient,
@@ -9748,6 +8760,7 @@ export {
9748
8760
  DEFAULT_CLOUD_URL,
9749
8761
  DEFAULT_RATE_LIMITS,
9750
8762
  DriverPlugin,
8763
+ ExternalValidationPlugin,
9751
8764
  FileArtifactApiClient,
9752
8765
  HttpDispatcher,
9753
8766
  HttpServer,
@@ -9786,6 +8799,7 @@ export {
9786
8799
  collectBundleHooks,
9787
8800
  createDefaultHostConfig,
9788
8801
  createDispatcherPlugin,
8802
+ createExternalValidationPlugin,
9789
8803
  createObjectOSStack,
9790
8804
  createRestApiPlugin,
9791
8805
  createStandaloneStack,
@@ -9801,7 +8815,7 @@ export {
9801
8815
  mergeRuntimeModule,
9802
8816
  parseTraceparent,
9803
8817
  readArtifactSource,
9804
- readEnvWithDeprecation6 as readEnvWithDeprecation,
8818
+ readEnvWithDeprecation5 as readEnvWithDeprecation,
9805
8819
  resolveCloudUrl,
9806
8820
  resolveDefaultArtifactPath,
9807
8821
  resolveErrorReporter,