@objectstack/runtime 6.9.0 → 7.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -1294,7 +1294,15 @@ var init_app_plugin = __esm({
1294
1294
  AppPlugin = class {
1295
1295
  constructor(bundle, projectContext) {
1296
1296
  this.type = "app";
1297
+ /** When true, init/start become no-ops — env has no app payload. */
1298
+ this.empty = false;
1297
1299
  this.init = async (ctx) => {
1300
+ if (this.empty) {
1301
+ ctx.logger.debug("[AppPlugin] empty env \u2014 no app payload, skipping init", {
1302
+ pluginName: this.name
1303
+ });
1304
+ return;
1305
+ }
1298
1306
  const sys = this.bundle.manifest || this.bundle;
1299
1307
  const appId = sys.id || sys.name;
1300
1308
  ctx.logger.info("Registering App Service", {
@@ -1309,6 +1317,12 @@ var init_app_plugin = __esm({
1309
1317
  ctx.getService("manifest").register(servicePayload);
1310
1318
  };
1311
1319
  this.start = async (ctx) => {
1320
+ if (this.empty) {
1321
+ ctx.logger.debug("[AppPlugin] empty env \u2014 no app payload, skipping start", {
1322
+ pluginName: this.name
1323
+ });
1324
+ return;
1325
+ }
1312
1326
  const sys = this.bundle.manifest || this.bundle;
1313
1327
  const appId = sys.id || sys.name;
1314
1328
  let ql;
@@ -1604,47 +1618,64 @@ var init_app_plugin = __esm({
1604
1618
  if (multiTenant) {
1605
1619
  ctx.logger.info("[Seeder] multi-tenant mode \u2014 skipping inline seed; per-org replay will run on sys_organization insert");
1606
1620
  } else {
1607
- try {
1608
- const metadata = ctx.getService("metadata");
1609
- if (metadata) {
1610
- const seedLoader = new SeedLoaderService(ql, metadata, ctx.logger);
1611
- const { SeedLoaderRequestSchema } = await import("@objectstack/spec/data");
1612
- const request = SeedLoaderRequestSchema.parse({
1613
- datasets: normalizedDatasets,
1614
- config: { defaultMode: "upsert", multiPass: true }
1615
- });
1616
- const result = await seedLoader.load(request);
1617
- ctx.logger.info("[Seeder] Seed loading complete", {
1618
- inserted: result.summary.totalInserted,
1619
- updated: result.summary.totalUpdated,
1620
- errors: result.errors.length
1621
- });
1622
- } else {
1623
- ctx.logger.debug("[Seeder] No metadata service; using basic insert fallback");
1621
+ const seedBudgetMs = Number(process.env.OS_INLINE_SEED_BUDGET_MS ?? 8e3);
1622
+ const seedPromise = (async () => {
1623
+ try {
1624
+ const metadata = ctx.getService("metadata");
1625
+ if (metadata) {
1626
+ const seedLoader = new SeedLoaderService(ql, metadata, ctx.logger);
1627
+ const { SeedLoaderRequestSchema } = await import("@objectstack/spec/data");
1628
+ const request = SeedLoaderRequestSchema.parse({
1629
+ datasets: normalizedDatasets,
1630
+ config: { defaultMode: "upsert", multiPass: true }
1631
+ });
1632
+ const result = await seedLoader.load(request);
1633
+ ctx.logger.info("[Seeder] Seed loading complete", {
1634
+ inserted: result.summary.totalInserted,
1635
+ updated: result.summary.totalUpdated,
1636
+ errors: result.errors.length
1637
+ });
1638
+ } else {
1639
+ ctx.logger.debug("[Seeder] No metadata service; using basic insert fallback");
1640
+ for (const dataset of normalizedDatasets) {
1641
+ ctx.logger.info(`[Seeder] Seeding ${dataset.records.length} records for ${dataset.object}`);
1642
+ for (const record of dataset.records) {
1643
+ try {
1644
+ await ql.insert(dataset.object, record, { context: { isSystem: true } });
1645
+ } catch (err) {
1646
+ ctx.logger.warn(`[Seeder] Failed to insert ${dataset.object} record:`, { error: err.message });
1647
+ }
1648
+ }
1649
+ }
1650
+ ctx.logger.info("[Seeder] Data seeding complete.");
1651
+ }
1652
+ } catch (err) {
1653
+ ctx.logger.warn("[Seeder] SeedLoaderService failed, falling back to basic insert", { error: err.message });
1624
1654
  for (const dataset of normalizedDatasets) {
1625
- ctx.logger.info(`[Seeder] Seeding ${dataset.records.length} records for ${dataset.object}`);
1626
1655
  for (const record of dataset.records) {
1627
1656
  try {
1628
1657
  await ql.insert(dataset.object, record, { context: { isSystem: true } });
1629
- } catch (err) {
1630
- ctx.logger.warn(`[Seeder] Failed to insert ${dataset.object} record:`, { error: err.message });
1658
+ } catch (insertErr) {
1659
+ ctx.logger.warn(`[Seeder] Failed to insert ${dataset.object} record:`, { error: insertErr.message });
1631
1660
  }
1632
1661
  }
1633
1662
  }
1634
- ctx.logger.info("[Seeder] Data seeding complete.");
1663
+ ctx.logger.info("[Seeder] Data seeding complete (fallback).");
1635
1664
  }
1636
- } catch (err) {
1637
- ctx.logger.warn("[Seeder] SeedLoaderService failed, falling back to basic insert", { error: err.message });
1638
- for (const dataset of normalizedDatasets) {
1639
- for (const record of dataset.records) {
1640
- try {
1641
- await ql.insert(dataset.object, record, { context: { isSystem: true } });
1642
- } catch (insertErr) {
1643
- ctx.logger.warn(`[Seeder] Failed to insert ${dataset.object} record:`, { error: insertErr.message });
1644
- }
1645
- }
1646
- }
1647
- ctx.logger.info("[Seeder] Data seeding complete (fallback).");
1665
+ })();
1666
+ let timer;
1667
+ const budget = new Promise((resolve2) => {
1668
+ timer = setTimeout(() => resolve2("budget"), seedBudgetMs);
1669
+ });
1670
+ const winner = await Promise.race([seedPromise.then(() => "done"), budget]);
1671
+ if (timer) clearTimeout(timer);
1672
+ if (winner === "budget") {
1673
+ ctx.logger.warn(
1674
+ `[Seeder] Inline seed exceeded ${seedBudgetMs}ms budget for ${appId}; continuing in background to avoid blocking kernel start.`
1675
+ );
1676
+ seedPromise.catch((err) => {
1677
+ ctx.logger.warn("[Seeder] Background seed failed after budget", { appId, error: err?.message ?? String(err) });
1678
+ });
1648
1679
  }
1649
1680
  }
1650
1681
  }
@@ -1658,6 +1689,39 @@ var init_app_plugin = __esm({
1658
1689
  const sys = bundle?.manifest || bundle;
1659
1690
  const appId = sys?.id || sys?.name;
1660
1691
  if (!appId) {
1692
+ const APP_CATEGORY_KEYS = [
1693
+ "objects",
1694
+ "views",
1695
+ "apps",
1696
+ "pages",
1697
+ "dashboards",
1698
+ "reports",
1699
+ "flows",
1700
+ "workflows",
1701
+ "triggers",
1702
+ "agents",
1703
+ "tools",
1704
+ "skills",
1705
+ "actions",
1706
+ "permissions",
1707
+ "roles",
1708
+ "profiles",
1709
+ "translations",
1710
+ "sharingRules",
1711
+ "ragPipelines",
1712
+ "data",
1713
+ "emailTemplates"
1714
+ ];
1715
+ const hasAppPayload = APP_CATEGORY_KEYS.some((k) => {
1716
+ const v = (bundle && bundle[k]) ?? (sys && sys[k]);
1717
+ return Array.isArray(v) && v.length > 0;
1718
+ });
1719
+ if (!hasAppPayload) {
1720
+ this.empty = true;
1721
+ const envSlug = projectContext?.environmentId ? projectContext.environmentId.slice(0, 8) : "empty";
1722
+ this.name = `plugin.app.empty-${envSlug}`;
1723
+ return;
1724
+ }
1661
1725
  const bundleKeys = bundle && typeof bundle === "object" ? Object.keys(bundle).slice(0, 20).join(",") : typeof bundle;
1662
1726
  const sysKeys = sys && typeof sys === "object" ? Object.keys(sys).slice(0, 20).join(",") : typeof sys;
1663
1727
  const ctxHint = projectContext ? ` projectContext=${JSON.stringify({
@@ -1666,7 +1730,7 @@ var init_app_plugin = __esm({
1666
1730
  source: projectContext.source
1667
1731
  })}` : "";
1668
1732
  throw new Error(
1669
- `[AppPlugin] bundle is missing manifest.id and manifest.name \u2014 cannot register as a plugin. bundleKeys=[${bundleKeys}] sysKeys=[${sysKeys}]${ctxHint}`
1733
+ `[AppPlugin] bundle has app payload but no manifest.id / manifest.name \u2014 cannot register as a plugin. bundleKeys=[${bundleKeys}] sysKeys=[${sysKeys}]${ctxHint}`
1670
1734
  );
1671
1735
  }
1672
1736
  this.name = `plugin.app.${appId}`;
@@ -2283,7 +2347,7 @@ function resolveObjectStackHome() {
2283
2347
  var StandaloneStackConfigSchema = import_zod.z.object({
2284
2348
  databaseUrl: import_zod.z.string().optional(),
2285
2349
  databaseAuthToken: import_zod.z.string().optional(),
2286
- databaseDriver: import_zod.z.enum(["sqlite", "sqlite-wasm", "turso", "memory", "postgres", "mongodb"]).optional(),
2350
+ databaseDriver: import_zod.z.enum(["sqlite", "sqlite-wasm", "memory", "postgres", "mongodb"]).optional(),
2287
2351
  environmentId: import_zod.z.string().optional(),
2288
2352
  artifactPath: import_zod.z.string().optional(),
2289
2353
  /**
@@ -2301,7 +2365,6 @@ var StandaloneStackConfigSchema = import_zod.z.object({
2301
2365
  });
2302
2366
  function detectDriverFromUrl(dbUrl) {
2303
2367
  if (/^memory:\/\//i.test(dbUrl)) return "memory";
2304
- if (/^(libsql|https?):\/\//i.test(dbUrl)) return "turso";
2305
2368
  if (/^(postgres(ql)?|pg):\/\//i.test(dbUrl)) return "postgres";
2306
2369
  if (/^mongodb(\+srv)?:\/\//i.test(dbUrl)) return "mongodb";
2307
2370
  if (/^wasm-sqlite:\/\//i.test(dbUrl)) return "sqlite-wasm";
@@ -2309,7 +2372,7 @@ function detectDriverFromUrl(dbUrl) {
2309
2372
  if (/^file:/i.test(dbUrl)) return "sqlite";
2310
2373
  if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(dbUrl)) return "sqlite";
2311
2374
  throw new Error(
2312
- `[StandaloneStack] Unsupported database URL scheme: ${dbUrl}. Supported schemes: memory://, libsql://, https://, postgres://, pg://, mongodb://, mongodb+srv://, file:`
2375
+ `[StandaloneStack] Unsupported database URL scheme: ${dbUrl}. Supported schemes: memory://, postgres://, pg://, mongodb://, mongodb+srv://, file:`
2313
2376
  );
2314
2377
  }
2315
2378
  async function createStandaloneStack(config) {
@@ -2323,25 +2386,12 @@ async function createStandaloneStack(config) {
2323
2386
  const artifactPathInput = cfg.artifactPath ?? process.env.OS_ARTIFACT_PATH ?? (0, import_node_path2.resolve)(cwd, "dist/objectstack.json");
2324
2387
  const artifactPath = isHttpUrl(artifactPathInput) ? artifactPathInput : artifactPathInput.startsWith("/") ? artifactPathInput : (0, import_node_path2.resolve)(cwd, artifactPathInput);
2325
2388
  const dbUrl = cfg.databaseUrl ?? process.env.OS_DATABASE_URL?.trim() ?? process.env.TURSO_DATABASE_URL?.trim() ?? (process.env.OS_HOME?.trim() ? `file:${(0, import_node_path2.resolve)(resolveObjectStackHome(), "data/standalone.db")}` : cfg.projectRoot ? `file:${(0, import_node_path2.resolve)(cfg.projectRoot, ".objectstack/data/standalone.db")}` : `file:${(0, import_node_path2.resolve)(resolveObjectStackHome(), "data/standalone.db")}`);
2326
- const dbAuthToken = cfg.databaseAuthToken ?? process.env.OS_DATABASE_AUTH_TOKEN?.trim() ?? process.env.TURSO_AUTH_TOKEN?.trim();
2327
2389
  const explicitDriver = cfg.databaseDriver ?? process.env.OS_DATABASE_DRIVER?.trim();
2328
2390
  const dbDriver = explicitDriver ?? detectDriverFromUrl(dbUrl);
2329
2391
  let driverPlugin;
2330
2392
  if (dbDriver === "memory") {
2331
2393
  const { InMemoryDriver } = await import("@objectstack/driver-memory");
2332
2394
  driverPlugin = new DriverPlugin2(new InMemoryDriver());
2333
- } else if (dbDriver === "turso") {
2334
- let TursoDriver;
2335
- try {
2336
- ({ TursoDriver } = await import("@objectstack/driver-turso"));
2337
- } catch (err) {
2338
- throw new Error(
2339
- `[StandaloneStack] libsql/turso URL detected ("${dbUrl}") but @objectstack/driver-turso is not installed. Install it with: npm install @objectstack/driver-turso (or use a file: URL to default to better-sqlite3). (${err?.message ?? err})`
2340
- );
2341
- }
2342
- driverPlugin = new DriverPlugin2(
2343
- new TursoDriver({ url: dbUrl, authToken: dbAuthToken })
2344
- );
2345
2395
  } else if (dbDriver === "postgres") {
2346
2396
  const { SqlDriver } = await import("@objectstack/driver-sql");
2347
2397
  driverPlugin = new DriverPlugin2(
@@ -4082,7 +4132,7 @@ var _HttpDispatcher = class _HttpDispatcher {
4082
4132
  return {
4083
4133
  handled: true,
4084
4134
  response: this.error(
4085
- "No ObjectQL driver is registered. Register at least one DriverPlugin (e.g. InMemoryDriver or TursoDriver).",
4135
+ "No ObjectQL driver is registered. Register at least one DriverPlugin (e.g. InMemoryDriver or SqlDriver).",
4086
4136
  503
4087
4137
  )
4088
4138
  };
@@ -7574,6 +7624,27 @@ function extractRuntimeFromMetadata(metadata) {
7574
7624
  }
7575
7625
  async function createDriver(driverType, databaseUrl, authToken) {
7576
7626
  switch (driverType) {
7627
+ case "libsql":
7628
+ case "turso": {
7629
+ let TursoDriver;
7630
+ try {
7631
+ ({ TursoDriver } = await import("@objectstack/driver-turso"));
7632
+ } catch (primaryErr) {
7633
+ try {
7634
+ const { createRequire } = await import("module");
7635
+ const path = await import("path");
7636
+ const url = await import("url");
7637
+ const hostRequire = createRequire(path.join(process.cwd(), "noop.js"));
7638
+ const resolved = hostRequire.resolve("@objectstack/driver-turso");
7639
+ ({ TursoDriver } = await import(url.pathToFileURL(resolved).href));
7640
+ } catch (fallbackErr) {
7641
+ throw new Error(
7642
+ `[ArtifactEnvironmentRegistry] libsql/turso driver requested but @objectstack/driver-turso is not resolvable. Install it from the cloud monorepo (cloud/packages/driver-turso) or via npm. (primary: ${primaryErr?.message ?? primaryErr}; fallback: ${fallbackErr?.message ?? fallbackErr})`
7643
+ );
7644
+ }
7645
+ }
7646
+ return new TursoDriver({ url: databaseUrl, authToken });
7647
+ }
7577
7648
  case "memory": {
7578
7649
  const { InMemoryDriver } = await import("@objectstack/driver-memory");
7579
7650
  const dbName = databaseUrl.replace(/^memory:\/\//, "").trim();
@@ -7592,18 +7663,6 @@ async function createDriver(driverType, databaseUrl, authToken) {
7592
7663
  useNullAsDefault: true
7593
7664
  });
7594
7665
  }
7595
- case "libsql":
7596
- case "turso": {
7597
- let TursoDriver;
7598
- try {
7599
- ({ TursoDriver } = await import("@objectstack/driver-turso"));
7600
- } catch (err) {
7601
- throw new Error(
7602
- `[ArtifactEnvironmentRegistry] libsql/turso driver requested but @objectstack/driver-turso is not installed. Install it with: npm install @objectstack/driver-turso. (${err?.message ?? err})`
7603
- );
7604
- }
7605
- return new TursoDriver({ url: databaseUrl, authToken });
7606
- }
7607
7666
  case "postgres":
7608
7667
  case "postgresql":
7609
7668
  case "pg": {
@@ -7879,9 +7938,20 @@ var ArtifactKernelFactory = class {
7879
7938
  this.logger.warn?.("[ArtifactKernelFactory] OS_AUTH_SECRET not set \u2014 per-project AuthPlugin skipped (auth endpoints will return 404)", { environmentId });
7880
7939
  }
7881
7940
  try {
7882
- const { SecurityPlugin } = await import("@objectstack/plugin-security");
7883
7941
  const multiTenant = String(process.env.OS_MULTI_TENANT ?? "false").toLowerCase() !== "false";
7884
- await kernel.use(new SecurityPlugin({ multiTenant }));
7942
+ if (multiTenant) {
7943
+ try {
7944
+ const { OrgScopingPlugin } = await import("@objectstack/plugin-org-scoping");
7945
+ await kernel.use(new OrgScopingPlugin());
7946
+ } catch (err) {
7947
+ this.logger.warn?.("[ArtifactKernelFactory] OrgScopingPlugin not registered (multi-tenant disabled)", {
7948
+ environmentId,
7949
+ error: err?.message
7950
+ });
7951
+ }
7952
+ }
7953
+ const { SecurityPlugin } = await import("@objectstack/plugin-security");
7954
+ await kernel.use(new SecurityPlugin());
7885
7955
  } catch (err) {
7886
7956
  this.logger.warn?.("[ArtifactKernelFactory] SecurityPlugin not registered", {
7887
7957
  environmentId,
@@ -8407,6 +8477,40 @@ function resolveCloudUrl(explicit) {
8407
8477
 
8408
8478
  // src/cloud/marketplace-proxy-plugin.ts
8409
8479
  var MARKETPLACE_PREFIX = "/api/v1/marketplace";
8480
+ var DEFAULT_LRU_MAX = 200;
8481
+ var LIST_TTL_MS = 30 * 60 * 1e3;
8482
+ var PACKAGE_TTL_MS = 2 * 60 * 60 * 1e3;
8483
+ var VERSION_TTL_MS = 24 * 60 * 60 * 1e3;
8484
+ function ttlForPath(pathname) {
8485
+ if (/\/packages\/[^/]+\/versions\//.test(pathname)) return VERSION_TTL_MS;
8486
+ if (/\/packages\/[^/]+/.test(pathname)) return PACKAGE_TTL_MS;
8487
+ return LIST_TTL_MS;
8488
+ }
8489
+ var LruTtlCache = class {
8490
+ constructor(max) {
8491
+ this.max = max;
8492
+ this.map = /* @__PURE__ */ new Map();
8493
+ }
8494
+ get(key) {
8495
+ const entry = this.map.get(key);
8496
+ if (!entry) return void 0;
8497
+ this.map.delete(key);
8498
+ this.map.set(key, entry);
8499
+ return entry;
8500
+ }
8501
+ set(key, entry) {
8502
+ if (this.map.has(key)) this.map.delete(key);
8503
+ this.map.set(key, entry);
8504
+ while (this.map.size > this.max) {
8505
+ const oldest = this.map.keys().next().value;
8506
+ if (oldest === void 0) break;
8507
+ this.map.delete(oldest);
8508
+ }
8509
+ }
8510
+ clear() {
8511
+ this.map.clear();
8512
+ }
8513
+ };
8410
8514
  var MarketplaceProxyPlugin = class _MarketplaceProxyPlugin {
8411
8515
  constructor(config = {}) {
8412
8516
  this.name = "com.objectstack.runtime.marketplace-proxy";
@@ -8428,6 +8532,7 @@ var MarketplaceProxyPlugin = class _MarketplaceProxyPlugin {
8428
8532
  }
8429
8533
  const rawApp = httpServer.getRawApp();
8430
8534
  const cloudUrl = this.cloudUrl;
8535
+ const cache = this.cache;
8431
8536
  const handler = async (c, next) => {
8432
8537
  if (!cloudUrl) {
8433
8538
  return c.json({
@@ -8454,24 +8559,51 @@ var MarketplaceProxyPlugin = class _MarketplaceProxyPlugin {
8454
8559
  }
8455
8560
  }, 405);
8456
8561
  }
8457
- const resp = await fetch(target, {
8458
- method,
8459
- headers: {
8460
- // Strip the inbound Host header fetch will set
8461
- // it to the cloud host. Forward only the
8462
- // identifying headers cloud might log.
8463
- "Accept": c.req.header("accept") ?? "application/json",
8464
- "User-Agent": `objectos-marketplace-proxy/${_MarketplaceProxyPlugin.prototype.version ?? "1.0.0"}`
8562
+ const accept = c.req.header("accept") ?? "application/json";
8563
+ const acceptLang = c.req.header("accept-language") ?? "";
8564
+ const cacheKey = `${incomingUrl.pathname}${incomingUrl.search}|al=${acceptLang}|a=${accept}`;
8565
+ const reqCacheCtl = (c.req.header("cache-control") ?? "").toLowerCase();
8566
+ const bypass = !cache || reqCacheCtl.includes("no-cache") || reqCacheCtl.includes("no-store");
8567
+ const now = Date.now();
8568
+ if (cache && !bypass) {
8569
+ const hit = cache.get(cacheKey);
8570
+ if (hit && hit.expiresAt > now) {
8571
+ return buildCachedResponse(hit, method, "HIT");
8465
8572
  }
8466
- });
8467
- const headers = new Headers();
8468
- const passthroughHeaders = ["content-type", "cache-control", "etag", "last-modified"];
8469
- for (const h of passthroughHeaders) {
8470
- const v = resp.headers.get(h);
8471
- if (v) headers.set(h, v);
8573
+ if (hit) {
8574
+ const revalHeaders = {
8575
+ "Accept": accept,
8576
+ "User-Agent": `objectos-marketplace-proxy/${_MarketplaceProxyPlugin.prototype.version ?? "1.0.0"}`
8577
+ };
8578
+ if (acceptLang) revalHeaders["Accept-Language"] = acceptLang;
8579
+ if (hit.etag) revalHeaders["If-None-Match"] = hit.etag;
8580
+ if (hit.lastModified) revalHeaders["If-Modified-Since"] = hit.lastModified;
8581
+ const revalResp = await fetch(target, { method: "GET", headers: revalHeaders });
8582
+ if (revalResp.status === 304) {
8583
+ hit.expiresAt = now + hit.ttlMs;
8584
+ const newEtag = revalResp.headers.get("etag");
8585
+ const newLm = revalResp.headers.get("last-modified");
8586
+ if (newEtag) hit.etag = newEtag;
8587
+ if (newLm) hit.lastModified = newLm;
8588
+ cache.set(cacheKey, hit);
8589
+ return buildCachedResponse(hit, method, "REVALIDATED");
8590
+ }
8591
+ return await consumeAndMaybeCache(revalResp, cacheKey, incomingUrl.pathname, method, cache);
8592
+ }
8593
+ }
8594
+ const reqHeaders = {
8595
+ // Strip the inbound Host header — fetch will set
8596
+ // it to the cloud host. Forward only the
8597
+ // identifying headers cloud might log.
8598
+ "Accept": accept,
8599
+ "User-Agent": `objectos-marketplace-proxy/${_MarketplaceProxyPlugin.prototype.version ?? "1.0.0"}`
8600
+ };
8601
+ if (acceptLang) reqHeaders["Accept-Language"] = acceptLang;
8602
+ const resp = await fetch(target, { method: "GET", headers: reqHeaders });
8603
+ if (bypass || !cache) {
8604
+ return await passthroughResponse(resp, method, bypass ? "BYPASS" : "MISS");
8472
8605
  }
8473
- const body = await resp.arrayBuffer();
8474
- return new Response(body, { status: resp.status, headers });
8606
+ return await consumeAndMaybeCache(resp, cacheKey, incomingUrl.pathname, method, cache);
8475
8607
  } catch (err) {
8476
8608
  const errObj = err instanceof Error ? err : new Error(err?.message ?? String(err));
8477
8609
  ctx.logger?.error?.("[MarketplaceProxyPlugin] proxy failed", errObj);
@@ -8494,12 +8626,67 @@ var MarketplaceProxyPlugin = class _MarketplaceProxyPlugin {
8494
8626
  }
8495
8627
  }
8496
8628
  }
8497
- ctx.logger?.info?.(`[MarketplaceProxyPlugin] mounted at ${MARKETPLACE_PREFIX}/* \u2192 ${cloudUrl || "(unconfigured)"}`);
8629
+ ctx.logger?.info?.(`[MarketplaceProxyPlugin] mounted at ${MARKETPLACE_PREFIX}/* \u2192 ${cloudUrl || "(unconfigured)"} (cache=${this.cache ? "on" : "off"})`);
8498
8630
  });
8499
8631
  };
8500
8632
  this.cloudUrl = resolveCloudUrl(config.controlPlaneUrl);
8633
+ const envFlag = (process.env.OS_MARKETPLACE_CACHE ?? "").trim().toLowerCase();
8634
+ const envDisabled = ["off", "false", "0", "no", "disable", "disabled"].includes(envFlag);
8635
+ const disabled = config.cacheDisabled ?? envDisabled;
8636
+ this.cache = disabled ? null : new LruTtlCache(Math.max(8, config.cacheMaxEntries ?? DEFAULT_LRU_MAX));
8501
8637
  }
8502
8638
  };
8639
+ var PASSTHROUGH_HEADERS = ["content-type", "cache-control", "etag", "last-modified", "vary"];
8640
+ function collectHeaders(src) {
8641
+ const out = {};
8642
+ for (const h of PASSTHROUGH_HEADERS) {
8643
+ const v = src.headers.get(h);
8644
+ if (v) out[h] = v;
8645
+ }
8646
+ return out;
8647
+ }
8648
+ function buildCachedResponse(entry, method, xCache) {
8649
+ const headers = new Headers(entry.headers);
8650
+ headers.set("X-Cache", xCache);
8651
+ const ageSec = Math.max(0, Math.floor((entry.expiresAt - entry.ttlMs - Date.now()) / -1e3));
8652
+ headers.set("Age", String(Math.max(0, ageSec)));
8653
+ const body = method === "HEAD" ? null : entry.body;
8654
+ return new Response(body, { status: entry.status, headers });
8655
+ }
8656
+ async function passthroughResponse(resp, method, xCache) {
8657
+ const headers = new Headers(collectHeaders(resp));
8658
+ headers.set("X-Cache", xCache);
8659
+ if (method === "HEAD") {
8660
+ try {
8661
+ await resp.arrayBuffer();
8662
+ } catch {
8663
+ }
8664
+ return new Response(null, { status: resp.status, headers });
8665
+ }
8666
+ const body = await resp.arrayBuffer();
8667
+ return new Response(body, { status: resp.status, headers });
8668
+ }
8669
+ async function consumeAndMaybeCache(resp, key, pathname, method, cache) {
8670
+ const body = await resp.arrayBuffer();
8671
+ const headers = collectHeaders(resp);
8672
+ if (resp.status >= 200 && resp.status < 300) {
8673
+ const ttlMs = ttlForPath(pathname);
8674
+ const entry = {
8675
+ status: resp.status,
8676
+ body,
8677
+ headers,
8678
+ etag: resp.headers.get("etag") ?? void 0,
8679
+ lastModified: resp.headers.get("last-modified") ?? void 0,
8680
+ expiresAt: Date.now() + ttlMs,
8681
+ ttlMs
8682
+ };
8683
+ cache.set(key, entry);
8684
+ }
8685
+ const respHeaders = new Headers(headers);
8686
+ respHeaders.set("X-Cache", "MISS");
8687
+ const outBody = method === "HEAD" ? null : body;
8688
+ return new Response(outBody, { status: resp.status, headers: respHeaders });
8689
+ }
8503
8690
 
8504
8691
  // src/cloud/runtime-config-plugin.ts
8505
8692
  var RuntimeConfigPlugin = class {
@@ -8537,12 +8724,14 @@ var RuntimeConfigPlugin = class {
8537
8724
  let defaultEnvironmentId;
8538
8725
  let defaultOrgId;
8539
8726
  let resolvedSingleEnv = this.singleEnvironment;
8540
- if (envRegistry && host && typeof envRegistry.resolveHostname === "function") {
8727
+ const resolveFn = typeof envRegistry?.resolveByHostname === "function" ? envRegistry.resolveByHostname.bind(envRegistry) : typeof envRegistry?.resolveHostname === "function" ? envRegistry.resolveHostname.bind(envRegistry) : null;
8728
+ if (resolveFn && host) {
8541
8729
  try {
8542
- const resolved = await envRegistry.resolveHostname(host);
8730
+ const resolved = await resolveFn(host);
8543
8731
  if (resolved?.environmentId) {
8544
- defaultEnvironmentId = resolved.environmentId;
8545
- if (resolved.organizationId) defaultOrgId = String(resolved.organizationId);
8732
+ defaultEnvironmentId = String(resolved.environmentId);
8733
+ const orgId = resolved.organizationId ?? resolved.organization_id;
8734
+ if (orgId) defaultOrgId = String(orgId);
8546
8735
  resolvedSingleEnv = true;
8547
8736
  }
8548
8737
  } catch {
@@ -8553,7 +8742,11 @@ var RuntimeConfigPlugin = class {
8553
8742
  singleEnvironment: resolvedSingleEnv,
8554
8743
  defaultOrgId,
8555
8744
  defaultEnvironmentId,
8556
- features
8745
+ features,
8746
+ branding: {
8747
+ productName: this.productName,
8748
+ productShortName: this.productShortName
8749
+ }
8557
8750
  });
8558
8751
  };
8559
8752
  rawApp.get("/api/v1/runtime/config", handler);
@@ -8570,6 +8763,10 @@ var RuntimeConfigPlugin = class {
8570
8763
  this.cloudUrl = config.controlPlaneUrl === "" ? "" : resolveCloudUrl(config.controlPlaneUrl) ?? "";
8571
8764
  this.installLocal = !!config.installLocal;
8572
8765
  this.singleEnvironment = !!config.singleEnvironment;
8766
+ const envName = (typeof process !== "undefined" ? process.env?.OS_PRODUCT_NAME : void 0)?.trim();
8767
+ const envShort = (typeof process !== "undefined" ? process.env?.OS_PRODUCT_SHORT_NAME : void 0)?.trim();
8768
+ this.productName = (config.productName ?? envName ?? "ObjectOS").trim() || "ObjectOS";
8769
+ this.productShortName = (config.productShortName ?? envShort ?? this.productName).trim() || this.productName;
8573
8770
  }
8574
8771
  };
8575
8772
 
@@ -8859,9 +9056,15 @@ var MarketplaceInstallLocalPlugin = class {
8859
9056
  const postHandler = async (c) => this.handleInstall(c, ctx);
8860
9057
  const getHandler = async (c) => this.handleList(c);
8861
9058
  const deleteHandler = async (c) => this.handleUninstall(c, ctx);
9059
+ const reseedHandler = async (c) => this.handleReseed(c, ctx);
9060
+ const purgeHandler = async (c) => this.handlePurge(c, ctx);
8862
9061
  if (typeof rawApp.post === "function") rawApp.post(ROUTE_BASE, postHandler);
8863
9062
  if (typeof rawApp.get === "function") rawApp.get(ROUTE_BASE, getHandler);
8864
9063
  if (typeof rawApp.delete === "function") rawApp.delete(`${ROUTE_BASE}/:manifestId`, deleteHandler);
9064
+ if (typeof rawApp.post === "function") {
9065
+ rawApp.post(`${ROUTE_BASE}/:manifestId/reseed-sample-data`, reseedHandler);
9066
+ rawApp.post(`${ROUTE_BASE}/:manifestId/purge-sample-data`, purgeHandler);
9067
+ }
8865
9068
  ctx.logger?.info?.(`[MarketplaceInstallLocal] mounted at ${ROUTE_BASE} (storage: ${this.storageDir})`);
8866
9069
  });
8867
9070
  };
@@ -8958,7 +9161,8 @@ var MarketplaceInstallLocalPlugin = class {
8958
9161
  version,
8959
9162
  manifest,
8960
9163
  installedAt: (/* @__PURE__ */ new Date()).toISOString(),
8961
- installedBy: userId
9164
+ installedBy: userId,
9165
+ withSampleData: false
8962
9166
  };
8963
9167
  try {
8964
9168
  (0, import_node_fs3.mkdirSync)(this.storageDir, { recursive: true });
@@ -8985,6 +9189,13 @@ var MarketplaceInstallLocalPlugin = class {
8985
9189
  ctx.logger?.warn?.(`[MarketplaceInstallLocal] syncSchemas failed for ${manifestId}: ${err?.message ?? err}`);
8986
9190
  }
8987
9191
  const seededSummary = await this.applySideEffects(ctx, manifest, { seedNow: true, c });
9192
+ if (seededSummary.seeded.mode === "inline" && (seededSummary.seeded.inserted ?? 0) + (seededSummary.seeded.updated ?? 0) > 0) {
9193
+ entry.withSampleData = true;
9194
+ try {
9195
+ (0, import_node_fs3.writeFileSync)((0, import_node_path6.join)(this.storageDir, safeFilename(manifestId)), JSON.stringify(entry, null, 2), "utf8");
9196
+ } catch {
9197
+ }
9198
+ }
8988
9199
  return c.json({
8989
9200
  success: true,
8990
9201
  data: {
@@ -9011,7 +9222,8 @@ var MarketplaceInstallLocalPlugin = class {
9011
9222
  manifestId: e.manifestId,
9012
9223
  version: e.version,
9013
9224
  installedAt: e.installedAt,
9014
- installedBy: e.installedBy
9225
+ installedBy: e.installedBy,
9226
+ withSampleData: e.withSampleData ?? false
9015
9227
  })),
9016
9228
  total: entries.length,
9017
9229
  storageDir: this.storageDir
@@ -9076,6 +9288,145 @@ var MarketplaceInstallLocalPlugin = class {
9076
9288
  * dev / single-tenant runtimes. Stricter checks can be layered on
9077
9289
  * via a middleware in cloud-hosted multi-tenant deployments.
9078
9290
  */
9291
+ /**
9292
+ * POST /api/v1/marketplace/install-local/:manifestId/reseed-sample-data
9293
+ *
9294
+ * Re-runs SeedLoaderService against the cached manifest's `data` arrays.
9295
+ * Idempotent (upsert by id). Useful when:
9296
+ * • The user installed an app and skipped sample data
9297
+ * • A purge was undone
9298
+ * • The user wants a clean baseline back after editing demo rows
9299
+ *
9300
+ * Multi-tenant: requires an active organization on the session (same
9301
+ * rule as install seed path).
9302
+ */
9303
+ this.handleReseed = async (c, ctx) => {
9304
+ const userId = await this.requireAuthenticatedUser(c, ctx);
9305
+ if (!userId) {
9306
+ return c.json({ success: false, error: { code: "unauthorized", message: "Authentication required." } }, 401);
9307
+ }
9308
+ const manifestId = String(c.req.param?.("manifestId") ?? c.req.params?.manifestId ?? "").trim();
9309
+ if (!manifestId) {
9310
+ return c.json({ success: false, error: { code: "bad_request", message: "manifestId path param required." } }, 400);
9311
+ }
9312
+ const file = (0, import_node_path6.join)(this.storageDir, safeFilename(manifestId));
9313
+ if (!(0, import_node_fs3.existsSync)(file)) {
9314
+ return c.json({ success: false, error: { code: "not_found", message: `No marketplace install for ${manifestId}.` } }, 404);
9315
+ }
9316
+ let entry;
9317
+ try {
9318
+ entry = JSON.parse((0, import_node_fs3.readFileSync)(file, "utf8"));
9319
+ } catch (err) {
9320
+ return c.json({ success: false, error: { code: "storage_failed", message: `Failed to read manifest cache: ${err?.message ?? err}` } }, 500);
9321
+ }
9322
+ const summary = await this.applySideEffects(ctx, entry.manifest, { seedNow: true, c });
9323
+ if (summary.seeded.mode === "skipped") {
9324
+ return c.json({
9325
+ success: false,
9326
+ error: {
9327
+ code: "reseed_skipped",
9328
+ message: `Reseed did not run: ${summary.seeded.reason ?? "unknown reason"}`
9329
+ }
9330
+ }, 400);
9331
+ }
9332
+ try {
9333
+ entry.withSampleData = true;
9334
+ (0, import_node_fs3.writeFileSync)(file, JSON.stringify(entry, null, 2), "utf8");
9335
+ } catch {
9336
+ }
9337
+ return c.json({
9338
+ success: true,
9339
+ data: {
9340
+ manifestId,
9341
+ inserted: summary.seeded.inserted ?? 0,
9342
+ updated: summary.seeded.updated ?? 0,
9343
+ errors: summary.seeded.errors ?? 0,
9344
+ withSampleData: true
9345
+ }
9346
+ }, 200);
9347
+ };
9348
+ /**
9349
+ * POST /api/v1/marketplace/install-local/:manifestId/purge-sample-data
9350
+ *
9351
+ * Deletes every record whose id is declared in the cached manifest's
9352
+ * seed datasets. Uses the `driver` service directly to bypass ACL /
9353
+ * lifecycle hooks (same pattern as cloud purge). User-created records
9354
+ * are never touched — only ids declared in the package's bundled
9355
+ * datasets are removed. Already-deleted rows count as `skipped`.
9356
+ */
9357
+ this.handlePurge = async (c, ctx) => {
9358
+ const userId = await this.requireAuthenticatedUser(c, ctx);
9359
+ if (!userId) {
9360
+ return c.json({ success: false, error: { code: "unauthorized", message: "Authentication required." } }, 401);
9361
+ }
9362
+ const manifestId = String(c.req.param?.("manifestId") ?? c.req.params?.manifestId ?? "").trim();
9363
+ if (!manifestId) {
9364
+ return c.json({ success: false, error: { code: "bad_request", message: "manifestId path param required." } }, 400);
9365
+ }
9366
+ const file = (0, import_node_path6.join)(this.storageDir, safeFilename(manifestId));
9367
+ if (!(0, import_node_fs3.existsSync)(file)) {
9368
+ return c.json({ success: false, error: { code: "not_found", message: `No marketplace install for ${manifestId}.` } }, 404);
9369
+ }
9370
+ let entry;
9371
+ try {
9372
+ entry = JSON.parse((0, import_node_fs3.readFileSync)(file, "utf8"));
9373
+ } catch (err) {
9374
+ return c.json({ success: false, error: { code: "storage_failed", message: `Failed to read manifest cache: ${err?.message ?? err}` } }, 500);
9375
+ }
9376
+ const datasets = Array.isArray(entry.manifest?.data) ? entry.manifest.data.filter((d) => d && d.object && Array.isArray(d.records)) : [];
9377
+ if (datasets.length === 0) {
9378
+ return c.json({
9379
+ success: false,
9380
+ error: { code: "nothing_to_purge", message: "This package declares no seed datasets." }
9381
+ }, 400);
9382
+ }
9383
+ let driver;
9384
+ try {
9385
+ driver = ctx.getService("driver");
9386
+ } catch {
9387
+ }
9388
+ if (!driver || typeof driver.delete !== "function") {
9389
+ return c.json({
9390
+ success: false,
9391
+ error: { code: "driver_missing", message: "driver service unavailable \u2014 cannot purge." }
9392
+ }, 500);
9393
+ }
9394
+ let deleted = 0;
9395
+ let skipped = 0;
9396
+ let errors = 0;
9397
+ for (const ds of datasets) {
9398
+ const object = String(ds.object);
9399
+ for (const rec of ds.records) {
9400
+ const id = rec?.id;
9401
+ if (id === void 0 || id === null || id === "") {
9402
+ skipped++;
9403
+ continue;
9404
+ }
9405
+ try {
9406
+ const r = await driver.delete(object, id);
9407
+ if (r === false || r === 0 || r?.deleted === 0) skipped++;
9408
+ else deleted++;
9409
+ } catch (err) {
9410
+ const msg = String(err?.message ?? err);
9411
+ if (/not.?found|no row/i.test(msg)) skipped++;
9412
+ else {
9413
+ errors++;
9414
+ ctx.logger?.warn?.(`[MarketplaceInstallLocal] purge ${object}#${id}: ${msg}`);
9415
+ }
9416
+ }
9417
+ }
9418
+ }
9419
+ try {
9420
+ entry.withSampleData = false;
9421
+ (0, import_node_fs3.writeFileSync)(file, JSON.stringify(entry, null, 2), "utf8");
9422
+ } catch {
9423
+ }
9424
+ ctx.logger?.info?.(`[MarketplaceInstallLocal] purged ${manifestId}: deleted=${deleted} skipped=${skipped} errors=${errors}`);
9425
+ return c.json({
9426
+ success: true,
9427
+ data: { manifestId, deleted, skipped, errors, withSampleData: false }
9428
+ }, 200);
9429
+ };
9079
9430
  /**
9080
9431
  * Replicate the start-time side-effects that AppPlugin runs for
9081
9432
  * statically-declared apps but the `manifest` service does NOT: