@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.js CHANGED
@@ -1271,7 +1271,15 @@ var init_app_plugin = __esm({
1271
1271
  AppPlugin = class {
1272
1272
  constructor(bundle, projectContext) {
1273
1273
  this.type = "app";
1274
+ /** When true, init/start become no-ops — env has no app payload. */
1275
+ this.empty = false;
1274
1276
  this.init = async (ctx) => {
1277
+ if (this.empty) {
1278
+ ctx.logger.debug("[AppPlugin] empty env \u2014 no app payload, skipping init", {
1279
+ pluginName: this.name
1280
+ });
1281
+ return;
1282
+ }
1275
1283
  const sys = this.bundle.manifest || this.bundle;
1276
1284
  const appId = sys.id || sys.name;
1277
1285
  ctx.logger.info("Registering App Service", {
@@ -1286,6 +1294,12 @@ var init_app_plugin = __esm({
1286
1294
  ctx.getService("manifest").register(servicePayload);
1287
1295
  };
1288
1296
  this.start = async (ctx) => {
1297
+ if (this.empty) {
1298
+ ctx.logger.debug("[AppPlugin] empty env \u2014 no app payload, skipping start", {
1299
+ pluginName: this.name
1300
+ });
1301
+ return;
1302
+ }
1289
1303
  const sys = this.bundle.manifest || this.bundle;
1290
1304
  const appId = sys.id || sys.name;
1291
1305
  let ql;
@@ -1581,47 +1595,64 @@ var init_app_plugin = __esm({
1581
1595
  if (multiTenant) {
1582
1596
  ctx.logger.info("[Seeder] multi-tenant mode \u2014 skipping inline seed; per-org replay will run on sys_organization insert");
1583
1597
  } else {
1584
- try {
1585
- const metadata = ctx.getService("metadata");
1586
- if (metadata) {
1587
- const seedLoader = new SeedLoaderService(ql, metadata, ctx.logger);
1588
- const { SeedLoaderRequestSchema } = await import("@objectstack/spec/data");
1589
- const request = SeedLoaderRequestSchema.parse({
1590
- datasets: normalizedDatasets,
1591
- config: { defaultMode: "upsert", multiPass: true }
1592
- });
1593
- const result = await seedLoader.load(request);
1594
- ctx.logger.info("[Seeder] Seed loading complete", {
1595
- inserted: result.summary.totalInserted,
1596
- updated: result.summary.totalUpdated,
1597
- errors: result.errors.length
1598
- });
1599
- } else {
1600
- ctx.logger.debug("[Seeder] No metadata service; using basic insert fallback");
1598
+ const seedBudgetMs = Number(process.env.OS_INLINE_SEED_BUDGET_MS ?? 8e3);
1599
+ const seedPromise = (async () => {
1600
+ try {
1601
+ const metadata = ctx.getService("metadata");
1602
+ if (metadata) {
1603
+ const seedLoader = new SeedLoaderService(ql, metadata, ctx.logger);
1604
+ const { SeedLoaderRequestSchema } = await import("@objectstack/spec/data");
1605
+ const request = SeedLoaderRequestSchema.parse({
1606
+ datasets: normalizedDatasets,
1607
+ config: { defaultMode: "upsert", multiPass: true }
1608
+ });
1609
+ const result = await seedLoader.load(request);
1610
+ ctx.logger.info("[Seeder] Seed loading complete", {
1611
+ inserted: result.summary.totalInserted,
1612
+ updated: result.summary.totalUpdated,
1613
+ errors: result.errors.length
1614
+ });
1615
+ } else {
1616
+ ctx.logger.debug("[Seeder] No metadata service; using basic insert fallback");
1617
+ for (const dataset of normalizedDatasets) {
1618
+ ctx.logger.info(`[Seeder] Seeding ${dataset.records.length} records for ${dataset.object}`);
1619
+ for (const record of dataset.records) {
1620
+ try {
1621
+ await ql.insert(dataset.object, record, { context: { isSystem: true } });
1622
+ } catch (err) {
1623
+ ctx.logger.warn(`[Seeder] Failed to insert ${dataset.object} record:`, { error: err.message });
1624
+ }
1625
+ }
1626
+ }
1627
+ ctx.logger.info("[Seeder] Data seeding complete.");
1628
+ }
1629
+ } catch (err) {
1630
+ ctx.logger.warn("[Seeder] SeedLoaderService failed, falling back to basic insert", { error: err.message });
1601
1631
  for (const dataset of normalizedDatasets) {
1602
- ctx.logger.info(`[Seeder] Seeding ${dataset.records.length} records for ${dataset.object}`);
1603
1632
  for (const record of dataset.records) {
1604
1633
  try {
1605
1634
  await ql.insert(dataset.object, record, { context: { isSystem: true } });
1606
- } catch (err) {
1607
- ctx.logger.warn(`[Seeder] Failed to insert ${dataset.object} record:`, { error: err.message });
1635
+ } catch (insertErr) {
1636
+ ctx.logger.warn(`[Seeder] Failed to insert ${dataset.object} record:`, { error: insertErr.message });
1608
1637
  }
1609
1638
  }
1610
1639
  }
1611
- ctx.logger.info("[Seeder] Data seeding complete.");
1640
+ ctx.logger.info("[Seeder] Data seeding complete (fallback).");
1612
1641
  }
1613
- } catch (err) {
1614
- ctx.logger.warn("[Seeder] SeedLoaderService failed, falling back to basic insert", { error: err.message });
1615
- for (const dataset of normalizedDatasets) {
1616
- for (const record of dataset.records) {
1617
- try {
1618
- await ql.insert(dataset.object, record, { context: { isSystem: true } });
1619
- } catch (insertErr) {
1620
- ctx.logger.warn(`[Seeder] Failed to insert ${dataset.object} record:`, { error: insertErr.message });
1621
- }
1622
- }
1623
- }
1624
- ctx.logger.info("[Seeder] Data seeding complete (fallback).");
1642
+ })();
1643
+ let timer;
1644
+ const budget = new Promise((resolve2) => {
1645
+ timer = setTimeout(() => resolve2("budget"), seedBudgetMs);
1646
+ });
1647
+ const winner = await Promise.race([seedPromise.then(() => "done"), budget]);
1648
+ if (timer) clearTimeout(timer);
1649
+ if (winner === "budget") {
1650
+ ctx.logger.warn(
1651
+ `[Seeder] Inline seed exceeded ${seedBudgetMs}ms budget for ${appId}; continuing in background to avoid blocking kernel start.`
1652
+ );
1653
+ seedPromise.catch((err) => {
1654
+ ctx.logger.warn("[Seeder] Background seed failed after budget", { appId, error: err?.message ?? String(err) });
1655
+ });
1625
1656
  }
1626
1657
  }
1627
1658
  }
@@ -1635,6 +1666,39 @@ var init_app_plugin = __esm({
1635
1666
  const sys = bundle?.manifest || bundle;
1636
1667
  const appId = sys?.id || sys?.name;
1637
1668
  if (!appId) {
1669
+ const APP_CATEGORY_KEYS = [
1670
+ "objects",
1671
+ "views",
1672
+ "apps",
1673
+ "pages",
1674
+ "dashboards",
1675
+ "reports",
1676
+ "flows",
1677
+ "workflows",
1678
+ "triggers",
1679
+ "agents",
1680
+ "tools",
1681
+ "skills",
1682
+ "actions",
1683
+ "permissions",
1684
+ "roles",
1685
+ "profiles",
1686
+ "translations",
1687
+ "sharingRules",
1688
+ "ragPipelines",
1689
+ "data",
1690
+ "emailTemplates"
1691
+ ];
1692
+ const hasAppPayload = APP_CATEGORY_KEYS.some((k) => {
1693
+ const v = (bundle && bundle[k]) ?? (sys && sys[k]);
1694
+ return Array.isArray(v) && v.length > 0;
1695
+ });
1696
+ if (!hasAppPayload) {
1697
+ this.empty = true;
1698
+ const envSlug = projectContext?.environmentId ? projectContext.environmentId.slice(0, 8) : "empty";
1699
+ this.name = `plugin.app.empty-${envSlug}`;
1700
+ return;
1701
+ }
1638
1702
  const bundleKeys = bundle && typeof bundle === "object" ? Object.keys(bundle).slice(0, 20).join(",") : typeof bundle;
1639
1703
  const sysKeys = sys && typeof sys === "object" ? Object.keys(sys).slice(0, 20).join(",") : typeof sys;
1640
1704
  const ctxHint = projectContext ? ` projectContext=${JSON.stringify({
@@ -1643,7 +1707,7 @@ var init_app_plugin = __esm({
1643
1707
  source: projectContext.source
1644
1708
  })}` : "";
1645
1709
  throw new Error(
1646
- `[AppPlugin] bundle is missing manifest.id and manifest.name \u2014 cannot register as a plugin. bundleKeys=[${bundleKeys}] sysKeys=[${sysKeys}]${ctxHint}`
1710
+ `[AppPlugin] bundle has app payload but no manifest.id / manifest.name \u2014 cannot register as a plugin. bundleKeys=[${bundleKeys}] sysKeys=[${sysKeys}]${ctxHint}`
1647
1711
  );
1648
1712
  }
1649
1713
  this.name = `plugin.app.${appId}`;
@@ -2192,7 +2256,7 @@ function resolveObjectStackHome() {
2192
2256
  var StandaloneStackConfigSchema = z.object({
2193
2257
  databaseUrl: z.string().optional(),
2194
2258
  databaseAuthToken: z.string().optional(),
2195
- databaseDriver: z.enum(["sqlite", "sqlite-wasm", "turso", "memory", "postgres", "mongodb"]).optional(),
2259
+ databaseDriver: z.enum(["sqlite", "sqlite-wasm", "memory", "postgres", "mongodb"]).optional(),
2196
2260
  environmentId: z.string().optional(),
2197
2261
  artifactPath: z.string().optional(),
2198
2262
  /**
@@ -2210,7 +2274,6 @@ var StandaloneStackConfigSchema = z.object({
2210
2274
  });
2211
2275
  function detectDriverFromUrl(dbUrl) {
2212
2276
  if (/^memory:\/\//i.test(dbUrl)) return "memory";
2213
- if (/^(libsql|https?):\/\//i.test(dbUrl)) return "turso";
2214
2277
  if (/^(postgres(ql)?|pg):\/\//i.test(dbUrl)) return "postgres";
2215
2278
  if (/^mongodb(\+srv)?:\/\//i.test(dbUrl)) return "mongodb";
2216
2279
  if (/^wasm-sqlite:\/\//i.test(dbUrl)) return "sqlite-wasm";
@@ -2218,7 +2281,7 @@ function detectDriverFromUrl(dbUrl) {
2218
2281
  if (/^file:/i.test(dbUrl)) return "sqlite";
2219
2282
  if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(dbUrl)) return "sqlite";
2220
2283
  throw new Error(
2221
- `[StandaloneStack] Unsupported database URL scheme: ${dbUrl}. Supported schemes: memory://, libsql://, https://, postgres://, pg://, mongodb://, mongodb+srv://, file:`
2284
+ `[StandaloneStack] Unsupported database URL scheme: ${dbUrl}. Supported schemes: memory://, postgres://, pg://, mongodb://, mongodb+srv://, file:`
2222
2285
  );
2223
2286
  }
2224
2287
  async function createStandaloneStack(config) {
@@ -2232,25 +2295,12 @@ async function createStandaloneStack(config) {
2232
2295
  const artifactPathInput = cfg.artifactPath ?? process.env.OS_ARTIFACT_PATH ?? resolvePath2(cwd, "dist/objectstack.json");
2233
2296
  const artifactPath = isHttpUrl(artifactPathInput) ? artifactPathInput : artifactPathInput.startsWith("/") ? artifactPathInput : resolvePath2(cwd, artifactPathInput);
2234
2297
  const dbUrl = cfg.databaseUrl ?? process.env.OS_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")}`);
2235
- const dbAuthToken = cfg.databaseAuthToken ?? process.env.OS_DATABASE_AUTH_TOKEN?.trim() ?? process.env.TURSO_AUTH_TOKEN?.trim();
2236
2298
  const explicitDriver = cfg.databaseDriver ?? process.env.OS_DATABASE_DRIVER?.trim();
2237
2299
  const dbDriver = explicitDriver ?? detectDriverFromUrl(dbUrl);
2238
2300
  let driverPlugin;
2239
2301
  if (dbDriver === "memory") {
2240
2302
  const { InMemoryDriver } = await import("@objectstack/driver-memory");
2241
2303
  driverPlugin = new DriverPlugin2(new InMemoryDriver());
2242
- } else if (dbDriver === "turso") {
2243
- let TursoDriver;
2244
- try {
2245
- ({ TursoDriver } = await import("@objectstack/driver-turso"));
2246
- } catch (err) {
2247
- throw new Error(
2248
- `[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})`
2249
- );
2250
- }
2251
- driverPlugin = new DriverPlugin2(
2252
- new TursoDriver({ url: dbUrl, authToken: dbAuthToken })
2253
- );
2254
2304
  } else if (dbDriver === "postgres") {
2255
2305
  const { SqlDriver } = await import("@objectstack/driver-sql");
2256
2306
  driverPlugin = new DriverPlugin2(
@@ -3991,7 +4041,7 @@ var _HttpDispatcher = class _HttpDispatcher {
3991
4041
  return {
3992
4042
  handled: true,
3993
4043
  response: this.error(
3994
- "No ObjectQL driver is registered. Register at least one DriverPlugin (e.g. InMemoryDriver or TursoDriver).",
4044
+ "No ObjectQL driver is registered. Register at least one DriverPlugin (e.g. InMemoryDriver or SqlDriver).",
3995
4045
  503
3996
4046
  )
3997
4047
  };
@@ -7493,6 +7543,27 @@ function extractRuntimeFromMetadata(metadata) {
7493
7543
  }
7494
7544
  async function createDriver(driverType, databaseUrl, authToken) {
7495
7545
  switch (driverType) {
7546
+ case "libsql":
7547
+ case "turso": {
7548
+ let TursoDriver;
7549
+ try {
7550
+ ({ TursoDriver } = await import("@objectstack/driver-turso"));
7551
+ } catch (primaryErr) {
7552
+ try {
7553
+ const { createRequire } = await import("module");
7554
+ const path = await import("path");
7555
+ const url = await import("url");
7556
+ const hostRequire = createRequire(path.join(process.cwd(), "noop.js"));
7557
+ const resolved = hostRequire.resolve("@objectstack/driver-turso");
7558
+ ({ TursoDriver } = await import(url.pathToFileURL(resolved).href));
7559
+ } catch (fallbackErr) {
7560
+ throw new Error(
7561
+ `[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})`
7562
+ );
7563
+ }
7564
+ }
7565
+ return new TursoDriver({ url: databaseUrl, authToken });
7566
+ }
7496
7567
  case "memory": {
7497
7568
  const { InMemoryDriver } = await import("@objectstack/driver-memory");
7498
7569
  const dbName = databaseUrl.replace(/^memory:\/\//, "").trim();
@@ -7511,18 +7582,6 @@ async function createDriver(driverType, databaseUrl, authToken) {
7511
7582
  useNullAsDefault: true
7512
7583
  });
7513
7584
  }
7514
- case "libsql":
7515
- case "turso": {
7516
- let TursoDriver;
7517
- try {
7518
- ({ TursoDriver } = await import("@objectstack/driver-turso"));
7519
- } catch (err) {
7520
- throw new Error(
7521
- `[ArtifactEnvironmentRegistry] libsql/turso driver requested but @objectstack/driver-turso is not installed. Install it with: npm install @objectstack/driver-turso. (${err?.message ?? err})`
7522
- );
7523
- }
7524
- return new TursoDriver({ url: databaseUrl, authToken });
7525
- }
7526
7585
  case "postgres":
7527
7586
  case "postgresql":
7528
7587
  case "pg": {
@@ -7798,9 +7857,20 @@ var ArtifactKernelFactory = class {
7798
7857
  this.logger.warn?.("[ArtifactKernelFactory] OS_AUTH_SECRET not set \u2014 per-project AuthPlugin skipped (auth endpoints will return 404)", { environmentId });
7799
7858
  }
7800
7859
  try {
7801
- const { SecurityPlugin } = await import("@objectstack/plugin-security");
7802
7860
  const multiTenant = String(process.env.OS_MULTI_TENANT ?? "false").toLowerCase() !== "false";
7803
- await kernel.use(new SecurityPlugin({ multiTenant }));
7861
+ if (multiTenant) {
7862
+ try {
7863
+ const { OrgScopingPlugin } = await import("@objectstack/plugin-org-scoping");
7864
+ await kernel.use(new OrgScopingPlugin());
7865
+ } catch (err) {
7866
+ this.logger.warn?.("[ArtifactKernelFactory] OrgScopingPlugin not registered (multi-tenant disabled)", {
7867
+ environmentId,
7868
+ error: err?.message
7869
+ });
7870
+ }
7871
+ }
7872
+ const { SecurityPlugin } = await import("@objectstack/plugin-security");
7873
+ await kernel.use(new SecurityPlugin());
7804
7874
  } catch (err) {
7805
7875
  this.logger.warn?.("[ArtifactKernelFactory] SecurityPlugin not registered", {
7806
7876
  environmentId,
@@ -8326,6 +8396,40 @@ function resolveCloudUrl(explicit) {
8326
8396
 
8327
8397
  // src/cloud/marketplace-proxy-plugin.ts
8328
8398
  var MARKETPLACE_PREFIX = "/api/v1/marketplace";
8399
+ var DEFAULT_LRU_MAX = 200;
8400
+ var LIST_TTL_MS = 30 * 60 * 1e3;
8401
+ var PACKAGE_TTL_MS = 2 * 60 * 60 * 1e3;
8402
+ var VERSION_TTL_MS = 24 * 60 * 60 * 1e3;
8403
+ function ttlForPath(pathname) {
8404
+ if (/\/packages\/[^/]+\/versions\//.test(pathname)) return VERSION_TTL_MS;
8405
+ if (/\/packages\/[^/]+/.test(pathname)) return PACKAGE_TTL_MS;
8406
+ return LIST_TTL_MS;
8407
+ }
8408
+ var LruTtlCache = class {
8409
+ constructor(max) {
8410
+ this.max = max;
8411
+ this.map = /* @__PURE__ */ new Map();
8412
+ }
8413
+ get(key) {
8414
+ const entry = this.map.get(key);
8415
+ if (!entry) return void 0;
8416
+ this.map.delete(key);
8417
+ this.map.set(key, entry);
8418
+ return entry;
8419
+ }
8420
+ set(key, entry) {
8421
+ if (this.map.has(key)) this.map.delete(key);
8422
+ this.map.set(key, entry);
8423
+ while (this.map.size > this.max) {
8424
+ const oldest = this.map.keys().next().value;
8425
+ if (oldest === void 0) break;
8426
+ this.map.delete(oldest);
8427
+ }
8428
+ }
8429
+ clear() {
8430
+ this.map.clear();
8431
+ }
8432
+ };
8329
8433
  var MarketplaceProxyPlugin = class _MarketplaceProxyPlugin {
8330
8434
  constructor(config = {}) {
8331
8435
  this.name = "com.objectstack.runtime.marketplace-proxy";
@@ -8347,6 +8451,7 @@ var MarketplaceProxyPlugin = class _MarketplaceProxyPlugin {
8347
8451
  }
8348
8452
  const rawApp = httpServer.getRawApp();
8349
8453
  const cloudUrl = this.cloudUrl;
8454
+ const cache = this.cache;
8350
8455
  const handler = async (c, next) => {
8351
8456
  if (!cloudUrl) {
8352
8457
  return c.json({
@@ -8373,24 +8478,51 @@ var MarketplaceProxyPlugin = class _MarketplaceProxyPlugin {
8373
8478
  }
8374
8479
  }, 405);
8375
8480
  }
8376
- const resp = await fetch(target, {
8377
- method,
8378
- headers: {
8379
- // Strip the inbound Host header fetch will set
8380
- // it to the cloud host. Forward only the
8381
- // identifying headers cloud might log.
8382
- "Accept": c.req.header("accept") ?? "application/json",
8383
- "User-Agent": `objectos-marketplace-proxy/${_MarketplaceProxyPlugin.prototype.version ?? "1.0.0"}`
8481
+ const accept = c.req.header("accept") ?? "application/json";
8482
+ const acceptLang = c.req.header("accept-language") ?? "";
8483
+ const cacheKey = `${incomingUrl.pathname}${incomingUrl.search}|al=${acceptLang}|a=${accept}`;
8484
+ const reqCacheCtl = (c.req.header("cache-control") ?? "").toLowerCase();
8485
+ const bypass = !cache || reqCacheCtl.includes("no-cache") || reqCacheCtl.includes("no-store");
8486
+ const now = Date.now();
8487
+ if (cache && !bypass) {
8488
+ const hit = cache.get(cacheKey);
8489
+ if (hit && hit.expiresAt > now) {
8490
+ return buildCachedResponse(hit, method, "HIT");
8384
8491
  }
8385
- });
8386
- const headers = new Headers();
8387
- const passthroughHeaders = ["content-type", "cache-control", "etag", "last-modified"];
8388
- for (const h of passthroughHeaders) {
8389
- const v = resp.headers.get(h);
8390
- if (v) headers.set(h, v);
8492
+ if (hit) {
8493
+ const revalHeaders = {
8494
+ "Accept": accept,
8495
+ "User-Agent": `objectos-marketplace-proxy/${_MarketplaceProxyPlugin.prototype.version ?? "1.0.0"}`
8496
+ };
8497
+ if (acceptLang) revalHeaders["Accept-Language"] = acceptLang;
8498
+ if (hit.etag) revalHeaders["If-None-Match"] = hit.etag;
8499
+ if (hit.lastModified) revalHeaders["If-Modified-Since"] = hit.lastModified;
8500
+ const revalResp = await fetch(target, { method: "GET", headers: revalHeaders });
8501
+ if (revalResp.status === 304) {
8502
+ hit.expiresAt = now + hit.ttlMs;
8503
+ const newEtag = revalResp.headers.get("etag");
8504
+ const newLm = revalResp.headers.get("last-modified");
8505
+ if (newEtag) hit.etag = newEtag;
8506
+ if (newLm) hit.lastModified = newLm;
8507
+ cache.set(cacheKey, hit);
8508
+ return buildCachedResponse(hit, method, "REVALIDATED");
8509
+ }
8510
+ return await consumeAndMaybeCache(revalResp, cacheKey, incomingUrl.pathname, method, cache);
8511
+ }
8512
+ }
8513
+ const reqHeaders = {
8514
+ // Strip the inbound Host header — fetch will set
8515
+ // it to the cloud host. Forward only the
8516
+ // identifying headers cloud might log.
8517
+ "Accept": accept,
8518
+ "User-Agent": `objectos-marketplace-proxy/${_MarketplaceProxyPlugin.prototype.version ?? "1.0.0"}`
8519
+ };
8520
+ if (acceptLang) reqHeaders["Accept-Language"] = acceptLang;
8521
+ const resp = await fetch(target, { method: "GET", headers: reqHeaders });
8522
+ if (bypass || !cache) {
8523
+ return await passthroughResponse(resp, method, bypass ? "BYPASS" : "MISS");
8391
8524
  }
8392
- const body = await resp.arrayBuffer();
8393
- return new Response(body, { status: resp.status, headers });
8525
+ return await consumeAndMaybeCache(resp, cacheKey, incomingUrl.pathname, method, cache);
8394
8526
  } catch (err) {
8395
8527
  const errObj = err instanceof Error ? err : new Error(err?.message ?? String(err));
8396
8528
  ctx.logger?.error?.("[MarketplaceProxyPlugin] proxy failed", errObj);
@@ -8413,12 +8545,67 @@ var MarketplaceProxyPlugin = class _MarketplaceProxyPlugin {
8413
8545
  }
8414
8546
  }
8415
8547
  }
8416
- ctx.logger?.info?.(`[MarketplaceProxyPlugin] mounted at ${MARKETPLACE_PREFIX}/* \u2192 ${cloudUrl || "(unconfigured)"}`);
8548
+ ctx.logger?.info?.(`[MarketplaceProxyPlugin] mounted at ${MARKETPLACE_PREFIX}/* \u2192 ${cloudUrl || "(unconfigured)"} (cache=${this.cache ? "on" : "off"})`);
8417
8549
  });
8418
8550
  };
8419
8551
  this.cloudUrl = resolveCloudUrl(config.controlPlaneUrl);
8552
+ const envFlag = (process.env.OS_MARKETPLACE_CACHE ?? "").trim().toLowerCase();
8553
+ const envDisabled = ["off", "false", "0", "no", "disable", "disabled"].includes(envFlag);
8554
+ const disabled = config.cacheDisabled ?? envDisabled;
8555
+ this.cache = disabled ? null : new LruTtlCache(Math.max(8, config.cacheMaxEntries ?? DEFAULT_LRU_MAX));
8420
8556
  }
8421
8557
  };
8558
+ var PASSTHROUGH_HEADERS = ["content-type", "cache-control", "etag", "last-modified", "vary"];
8559
+ function collectHeaders(src) {
8560
+ const out = {};
8561
+ for (const h of PASSTHROUGH_HEADERS) {
8562
+ const v = src.headers.get(h);
8563
+ if (v) out[h] = v;
8564
+ }
8565
+ return out;
8566
+ }
8567
+ function buildCachedResponse(entry, method, xCache) {
8568
+ const headers = new Headers(entry.headers);
8569
+ headers.set("X-Cache", xCache);
8570
+ const ageSec = Math.max(0, Math.floor((entry.expiresAt - entry.ttlMs - Date.now()) / -1e3));
8571
+ headers.set("Age", String(Math.max(0, ageSec)));
8572
+ const body = method === "HEAD" ? null : entry.body;
8573
+ return new Response(body, { status: entry.status, headers });
8574
+ }
8575
+ async function passthroughResponse(resp, method, xCache) {
8576
+ const headers = new Headers(collectHeaders(resp));
8577
+ headers.set("X-Cache", xCache);
8578
+ if (method === "HEAD") {
8579
+ try {
8580
+ await resp.arrayBuffer();
8581
+ } catch {
8582
+ }
8583
+ return new Response(null, { status: resp.status, headers });
8584
+ }
8585
+ const body = await resp.arrayBuffer();
8586
+ return new Response(body, { status: resp.status, headers });
8587
+ }
8588
+ async function consumeAndMaybeCache(resp, key, pathname, method, cache) {
8589
+ const body = await resp.arrayBuffer();
8590
+ const headers = collectHeaders(resp);
8591
+ if (resp.status >= 200 && resp.status < 300) {
8592
+ const ttlMs = ttlForPath(pathname);
8593
+ const entry = {
8594
+ status: resp.status,
8595
+ body,
8596
+ headers,
8597
+ etag: resp.headers.get("etag") ?? void 0,
8598
+ lastModified: resp.headers.get("last-modified") ?? void 0,
8599
+ expiresAt: Date.now() + ttlMs,
8600
+ ttlMs
8601
+ };
8602
+ cache.set(key, entry);
8603
+ }
8604
+ const respHeaders = new Headers(headers);
8605
+ respHeaders.set("X-Cache", "MISS");
8606
+ const outBody = method === "HEAD" ? null : body;
8607
+ return new Response(outBody, { status: resp.status, headers: respHeaders });
8608
+ }
8422
8609
 
8423
8610
  // src/cloud/runtime-config-plugin.ts
8424
8611
  var RuntimeConfigPlugin = class {
@@ -8456,12 +8643,14 @@ var RuntimeConfigPlugin = class {
8456
8643
  let defaultEnvironmentId;
8457
8644
  let defaultOrgId;
8458
8645
  let resolvedSingleEnv = this.singleEnvironment;
8459
- if (envRegistry && host && typeof envRegistry.resolveHostname === "function") {
8646
+ const resolveFn = typeof envRegistry?.resolveByHostname === "function" ? envRegistry.resolveByHostname.bind(envRegistry) : typeof envRegistry?.resolveHostname === "function" ? envRegistry.resolveHostname.bind(envRegistry) : null;
8647
+ if (resolveFn && host) {
8460
8648
  try {
8461
- const resolved = await envRegistry.resolveHostname(host);
8649
+ const resolved = await resolveFn(host);
8462
8650
  if (resolved?.environmentId) {
8463
- defaultEnvironmentId = resolved.environmentId;
8464
- if (resolved.organizationId) defaultOrgId = String(resolved.organizationId);
8651
+ defaultEnvironmentId = String(resolved.environmentId);
8652
+ const orgId = resolved.organizationId ?? resolved.organization_id;
8653
+ if (orgId) defaultOrgId = String(orgId);
8465
8654
  resolvedSingleEnv = true;
8466
8655
  }
8467
8656
  } catch {
@@ -8472,7 +8661,11 @@ var RuntimeConfigPlugin = class {
8472
8661
  singleEnvironment: resolvedSingleEnv,
8473
8662
  defaultOrgId,
8474
8663
  defaultEnvironmentId,
8475
- features
8664
+ features,
8665
+ branding: {
8666
+ productName: this.productName,
8667
+ productShortName: this.productShortName
8668
+ }
8476
8669
  });
8477
8670
  };
8478
8671
  rawApp.get("/api/v1/runtime/config", handler);
@@ -8489,6 +8682,10 @@ var RuntimeConfigPlugin = class {
8489
8682
  this.cloudUrl = config.controlPlaneUrl === "" ? "" : resolveCloudUrl(config.controlPlaneUrl) ?? "";
8490
8683
  this.installLocal = !!config.installLocal;
8491
8684
  this.singleEnvironment = !!config.singleEnvironment;
8685
+ const envName = (typeof process !== "undefined" ? process.env?.OS_PRODUCT_NAME : void 0)?.trim();
8686
+ const envShort = (typeof process !== "undefined" ? process.env?.OS_PRODUCT_SHORT_NAME : void 0)?.trim();
8687
+ this.productName = (config.productName ?? envName ?? "ObjectOS").trim() || "ObjectOS";
8688
+ this.productShortName = (config.productShortName ?? envShort ?? this.productName).trim() || this.productName;
8492
8689
  }
8493
8690
  };
8494
8691
 
@@ -8778,9 +8975,15 @@ var MarketplaceInstallLocalPlugin = class {
8778
8975
  const postHandler = async (c) => this.handleInstall(c, ctx);
8779
8976
  const getHandler = async (c) => this.handleList(c);
8780
8977
  const deleteHandler = async (c) => this.handleUninstall(c, ctx);
8978
+ const reseedHandler = async (c) => this.handleReseed(c, ctx);
8979
+ const purgeHandler = async (c) => this.handlePurge(c, ctx);
8781
8980
  if (typeof rawApp.post === "function") rawApp.post(ROUTE_BASE, postHandler);
8782
8981
  if (typeof rawApp.get === "function") rawApp.get(ROUTE_BASE, getHandler);
8783
8982
  if (typeof rawApp.delete === "function") rawApp.delete(`${ROUTE_BASE}/:manifestId`, deleteHandler);
8983
+ if (typeof rawApp.post === "function") {
8984
+ rawApp.post(`${ROUTE_BASE}/:manifestId/reseed-sample-data`, reseedHandler);
8985
+ rawApp.post(`${ROUTE_BASE}/:manifestId/purge-sample-data`, purgeHandler);
8986
+ }
8784
8987
  ctx.logger?.info?.(`[MarketplaceInstallLocal] mounted at ${ROUTE_BASE} (storage: ${this.storageDir})`);
8785
8988
  });
8786
8989
  };
@@ -8877,7 +9080,8 @@ var MarketplaceInstallLocalPlugin = class {
8877
9080
  version,
8878
9081
  manifest,
8879
9082
  installedAt: (/* @__PURE__ */ new Date()).toISOString(),
8880
- installedBy: userId
9083
+ installedBy: userId,
9084
+ withSampleData: false
8881
9085
  };
8882
9086
  try {
8883
9087
  mkdirSync3(this.storageDir, { recursive: true });
@@ -8904,6 +9108,13 @@ var MarketplaceInstallLocalPlugin = class {
8904
9108
  ctx.logger?.warn?.(`[MarketplaceInstallLocal] syncSchemas failed for ${manifestId}: ${err?.message ?? err}`);
8905
9109
  }
8906
9110
  const seededSummary = await this.applySideEffects(ctx, manifest, { seedNow: true, c });
9111
+ if (seededSummary.seeded.mode === "inline" && (seededSummary.seeded.inserted ?? 0) + (seededSummary.seeded.updated ?? 0) > 0) {
9112
+ entry.withSampleData = true;
9113
+ try {
9114
+ writeFileSync2(join(this.storageDir, safeFilename(manifestId)), JSON.stringify(entry, null, 2), "utf8");
9115
+ } catch {
9116
+ }
9117
+ }
8907
9118
  return c.json({
8908
9119
  success: true,
8909
9120
  data: {
@@ -8930,7 +9141,8 @@ var MarketplaceInstallLocalPlugin = class {
8930
9141
  manifestId: e.manifestId,
8931
9142
  version: e.version,
8932
9143
  installedAt: e.installedAt,
8933
- installedBy: e.installedBy
9144
+ installedBy: e.installedBy,
9145
+ withSampleData: e.withSampleData ?? false
8934
9146
  })),
8935
9147
  total: entries.length,
8936
9148
  storageDir: this.storageDir
@@ -8995,6 +9207,145 @@ var MarketplaceInstallLocalPlugin = class {
8995
9207
  * dev / single-tenant runtimes. Stricter checks can be layered on
8996
9208
  * via a middleware in cloud-hosted multi-tenant deployments.
8997
9209
  */
9210
+ /**
9211
+ * POST /api/v1/marketplace/install-local/:manifestId/reseed-sample-data
9212
+ *
9213
+ * Re-runs SeedLoaderService against the cached manifest's `data` arrays.
9214
+ * Idempotent (upsert by id). Useful when:
9215
+ * • The user installed an app and skipped sample data
9216
+ * • A purge was undone
9217
+ * • The user wants a clean baseline back after editing demo rows
9218
+ *
9219
+ * Multi-tenant: requires an active organization on the session (same
9220
+ * rule as install seed path).
9221
+ */
9222
+ this.handleReseed = async (c, ctx) => {
9223
+ const userId = await this.requireAuthenticatedUser(c, ctx);
9224
+ if (!userId) {
9225
+ return c.json({ success: false, error: { code: "unauthorized", message: "Authentication required." } }, 401);
9226
+ }
9227
+ const manifestId = String(c.req.param?.("manifestId") ?? c.req.params?.manifestId ?? "").trim();
9228
+ if (!manifestId) {
9229
+ return c.json({ success: false, error: { code: "bad_request", message: "manifestId path param required." } }, 400);
9230
+ }
9231
+ const file = join(this.storageDir, safeFilename(manifestId));
9232
+ if (!existsSync2(file)) {
9233
+ return c.json({ success: false, error: { code: "not_found", message: `No marketplace install for ${manifestId}.` } }, 404);
9234
+ }
9235
+ let entry;
9236
+ try {
9237
+ entry = JSON.parse(readFileSync(file, "utf8"));
9238
+ } catch (err) {
9239
+ return c.json({ success: false, error: { code: "storage_failed", message: `Failed to read manifest cache: ${err?.message ?? err}` } }, 500);
9240
+ }
9241
+ const summary = await this.applySideEffects(ctx, entry.manifest, { seedNow: true, c });
9242
+ if (summary.seeded.mode === "skipped") {
9243
+ return c.json({
9244
+ success: false,
9245
+ error: {
9246
+ code: "reseed_skipped",
9247
+ message: `Reseed did not run: ${summary.seeded.reason ?? "unknown reason"}`
9248
+ }
9249
+ }, 400);
9250
+ }
9251
+ try {
9252
+ entry.withSampleData = true;
9253
+ writeFileSync2(file, JSON.stringify(entry, null, 2), "utf8");
9254
+ } catch {
9255
+ }
9256
+ return c.json({
9257
+ success: true,
9258
+ data: {
9259
+ manifestId,
9260
+ inserted: summary.seeded.inserted ?? 0,
9261
+ updated: summary.seeded.updated ?? 0,
9262
+ errors: summary.seeded.errors ?? 0,
9263
+ withSampleData: true
9264
+ }
9265
+ }, 200);
9266
+ };
9267
+ /**
9268
+ * POST /api/v1/marketplace/install-local/:manifestId/purge-sample-data
9269
+ *
9270
+ * Deletes every record whose id is declared in the cached manifest's
9271
+ * seed datasets. Uses the `driver` service directly to bypass ACL /
9272
+ * lifecycle hooks (same pattern as cloud purge). User-created records
9273
+ * are never touched — only ids declared in the package's bundled
9274
+ * datasets are removed. Already-deleted rows count as `skipped`.
9275
+ */
9276
+ this.handlePurge = async (c, ctx) => {
9277
+ const userId = await this.requireAuthenticatedUser(c, ctx);
9278
+ if (!userId) {
9279
+ return c.json({ success: false, error: { code: "unauthorized", message: "Authentication required." } }, 401);
9280
+ }
9281
+ const manifestId = String(c.req.param?.("manifestId") ?? c.req.params?.manifestId ?? "").trim();
9282
+ if (!manifestId) {
9283
+ return c.json({ success: false, error: { code: "bad_request", message: "manifestId path param required." } }, 400);
9284
+ }
9285
+ const file = join(this.storageDir, safeFilename(manifestId));
9286
+ if (!existsSync2(file)) {
9287
+ return c.json({ success: false, error: { code: "not_found", message: `No marketplace install for ${manifestId}.` } }, 404);
9288
+ }
9289
+ let entry;
9290
+ try {
9291
+ entry = JSON.parse(readFileSync(file, "utf8"));
9292
+ } catch (err) {
9293
+ return c.json({ success: false, error: { code: "storage_failed", message: `Failed to read manifest cache: ${err?.message ?? err}` } }, 500);
9294
+ }
9295
+ const datasets = Array.isArray(entry.manifest?.data) ? entry.manifest.data.filter((d) => d && d.object && Array.isArray(d.records)) : [];
9296
+ if (datasets.length === 0) {
9297
+ return c.json({
9298
+ success: false,
9299
+ error: { code: "nothing_to_purge", message: "This package declares no seed datasets." }
9300
+ }, 400);
9301
+ }
9302
+ let driver;
9303
+ try {
9304
+ driver = ctx.getService("driver");
9305
+ } catch {
9306
+ }
9307
+ if (!driver || typeof driver.delete !== "function") {
9308
+ return c.json({
9309
+ success: false,
9310
+ error: { code: "driver_missing", message: "driver service unavailable \u2014 cannot purge." }
9311
+ }, 500);
9312
+ }
9313
+ let deleted = 0;
9314
+ let skipped = 0;
9315
+ let errors = 0;
9316
+ for (const ds of datasets) {
9317
+ const object = String(ds.object);
9318
+ for (const rec of ds.records) {
9319
+ const id = rec?.id;
9320
+ if (id === void 0 || id === null || id === "") {
9321
+ skipped++;
9322
+ continue;
9323
+ }
9324
+ try {
9325
+ const r = await driver.delete(object, id);
9326
+ if (r === false || r === 0 || r?.deleted === 0) skipped++;
9327
+ else deleted++;
9328
+ } catch (err) {
9329
+ const msg = String(err?.message ?? err);
9330
+ if (/not.?found|no row/i.test(msg)) skipped++;
9331
+ else {
9332
+ errors++;
9333
+ ctx.logger?.warn?.(`[MarketplaceInstallLocal] purge ${object}#${id}: ${msg}`);
9334
+ }
9335
+ }
9336
+ }
9337
+ }
9338
+ try {
9339
+ entry.withSampleData = false;
9340
+ writeFileSync2(file, JSON.stringify(entry, null, 2), "utf8");
9341
+ } catch {
9342
+ }
9343
+ ctx.logger?.info?.(`[MarketplaceInstallLocal] purged ${manifestId}: deleted=${deleted} skipped=${skipped} errors=${errors}`);
9344
+ return c.json({
9345
+ success: true,
9346
+ data: { manifestId, deleted, skipped, errors, withSampleData: false }
9347
+ }, 200);
9348
+ };
8998
9349
  /**
8999
9350
  * Replicate the start-time side-effects that AppPlugin runs for
9000
9351
  * statically-declared apps but the `manifest` service does NOT: