@objectstack/runtime 6.9.0 → 7.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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,38 @@ 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
+ ];
1691
+ const hasAppPayload = APP_CATEGORY_KEYS.some((k) => {
1692
+ const v = (bundle && bundle[k]) ?? (sys && sys[k]);
1693
+ return Array.isArray(v) && v.length > 0;
1694
+ });
1695
+ if (!hasAppPayload) {
1696
+ this.empty = true;
1697
+ const envSlug = projectContext?.environmentId ? projectContext.environmentId.slice(0, 8) : "empty";
1698
+ this.name = `plugin.app.empty-${envSlug}`;
1699
+ return;
1700
+ }
1638
1701
  const bundleKeys = bundle && typeof bundle === "object" ? Object.keys(bundle).slice(0, 20).join(",") : typeof bundle;
1639
1702
  const sysKeys = sys && typeof sys === "object" ? Object.keys(sys).slice(0, 20).join(",") : typeof sys;
1640
1703
  const ctxHint = projectContext ? ` projectContext=${JSON.stringify({
@@ -1643,7 +1706,7 @@ var init_app_plugin = __esm({
1643
1706
  source: projectContext.source
1644
1707
  })}` : "";
1645
1708
  throw new Error(
1646
- `[AppPlugin] bundle is missing manifest.id and manifest.name \u2014 cannot register as a plugin. bundleKeys=[${bundleKeys}] sysKeys=[${sysKeys}]${ctxHint}`
1709
+ `[AppPlugin] bundle has app payload but no manifest.id / manifest.name \u2014 cannot register as a plugin. bundleKeys=[${bundleKeys}] sysKeys=[${sysKeys}]${ctxHint}`
1647
1710
  );
1648
1711
  }
1649
1712
  this.name = `plugin.app.${appId}`;
@@ -2192,7 +2255,7 @@ function resolveObjectStackHome() {
2192
2255
  var StandaloneStackConfigSchema = z.object({
2193
2256
  databaseUrl: z.string().optional(),
2194
2257
  databaseAuthToken: z.string().optional(),
2195
- databaseDriver: z.enum(["sqlite", "sqlite-wasm", "turso", "memory", "postgres", "mongodb"]).optional(),
2258
+ databaseDriver: z.enum(["sqlite", "sqlite-wasm", "memory", "postgres", "mongodb"]).optional(),
2196
2259
  environmentId: z.string().optional(),
2197
2260
  artifactPath: z.string().optional(),
2198
2261
  /**
@@ -2210,7 +2273,6 @@ var StandaloneStackConfigSchema = z.object({
2210
2273
  });
2211
2274
  function detectDriverFromUrl(dbUrl) {
2212
2275
  if (/^memory:\/\//i.test(dbUrl)) return "memory";
2213
- if (/^(libsql|https?):\/\//i.test(dbUrl)) return "turso";
2214
2276
  if (/^(postgres(ql)?|pg):\/\//i.test(dbUrl)) return "postgres";
2215
2277
  if (/^mongodb(\+srv)?:\/\//i.test(dbUrl)) return "mongodb";
2216
2278
  if (/^wasm-sqlite:\/\//i.test(dbUrl)) return "sqlite-wasm";
@@ -2218,7 +2280,7 @@ function detectDriverFromUrl(dbUrl) {
2218
2280
  if (/^file:/i.test(dbUrl)) return "sqlite";
2219
2281
  if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(dbUrl)) return "sqlite";
2220
2282
  throw new Error(
2221
- `[StandaloneStack] Unsupported database URL scheme: ${dbUrl}. Supported schemes: memory://, libsql://, https://, postgres://, pg://, mongodb://, mongodb+srv://, file:`
2283
+ `[StandaloneStack] Unsupported database URL scheme: ${dbUrl}. Supported schemes: memory://, postgres://, pg://, mongodb://, mongodb+srv://, file:`
2222
2284
  );
2223
2285
  }
2224
2286
  async function createStandaloneStack(config) {
@@ -2232,25 +2294,12 @@ async function createStandaloneStack(config) {
2232
2294
  const artifactPathInput = cfg.artifactPath ?? process.env.OS_ARTIFACT_PATH ?? resolvePath2(cwd, "dist/objectstack.json");
2233
2295
  const artifactPath = isHttpUrl(artifactPathInput) ? artifactPathInput : artifactPathInput.startsWith("/") ? artifactPathInput : resolvePath2(cwd, artifactPathInput);
2234
2296
  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
2297
  const explicitDriver = cfg.databaseDriver ?? process.env.OS_DATABASE_DRIVER?.trim();
2237
2298
  const dbDriver = explicitDriver ?? detectDriverFromUrl(dbUrl);
2238
2299
  let driverPlugin;
2239
2300
  if (dbDriver === "memory") {
2240
2301
  const { InMemoryDriver } = await import("@objectstack/driver-memory");
2241
2302
  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
2303
  } else if (dbDriver === "postgres") {
2255
2304
  const { SqlDriver } = await import("@objectstack/driver-sql");
2256
2305
  driverPlugin = new DriverPlugin2(
@@ -3991,7 +4040,7 @@ var _HttpDispatcher = class _HttpDispatcher {
3991
4040
  return {
3992
4041
  handled: true,
3993
4042
  response: this.error(
3994
- "No ObjectQL driver is registered. Register at least one DriverPlugin (e.g. InMemoryDriver or TursoDriver).",
4043
+ "No ObjectQL driver is registered. Register at least one DriverPlugin (e.g. InMemoryDriver or SqlDriver).",
3995
4044
  503
3996
4045
  )
3997
4046
  };
@@ -7493,6 +7542,27 @@ function extractRuntimeFromMetadata(metadata) {
7493
7542
  }
7494
7543
  async function createDriver(driverType, databaseUrl, authToken) {
7495
7544
  switch (driverType) {
7545
+ case "libsql":
7546
+ case "turso": {
7547
+ let TursoDriver;
7548
+ try {
7549
+ ({ TursoDriver } = await import("@objectstack/driver-turso"));
7550
+ } catch (primaryErr) {
7551
+ try {
7552
+ const { createRequire } = await import("module");
7553
+ const path = await import("path");
7554
+ const url = await import("url");
7555
+ const hostRequire = createRequire(path.join(process.cwd(), "noop.js"));
7556
+ const resolved = hostRequire.resolve("@objectstack/driver-turso");
7557
+ ({ TursoDriver } = await import(url.pathToFileURL(resolved).href));
7558
+ } catch (fallbackErr) {
7559
+ throw new Error(
7560
+ `[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})`
7561
+ );
7562
+ }
7563
+ }
7564
+ return new TursoDriver({ url: databaseUrl, authToken });
7565
+ }
7496
7566
  case "memory": {
7497
7567
  const { InMemoryDriver } = await import("@objectstack/driver-memory");
7498
7568
  const dbName = databaseUrl.replace(/^memory:\/\//, "").trim();
@@ -7511,18 +7581,6 @@ async function createDriver(driverType, databaseUrl, authToken) {
7511
7581
  useNullAsDefault: true
7512
7582
  });
7513
7583
  }
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
7584
  case "postgres":
7527
7585
  case "postgresql":
7528
7586
  case "pg": {
@@ -7798,9 +7856,20 @@ var ArtifactKernelFactory = class {
7798
7856
  this.logger.warn?.("[ArtifactKernelFactory] OS_AUTH_SECRET not set \u2014 per-project AuthPlugin skipped (auth endpoints will return 404)", { environmentId });
7799
7857
  }
7800
7858
  try {
7801
- const { SecurityPlugin } = await import("@objectstack/plugin-security");
7802
7859
  const multiTenant = String(process.env.OS_MULTI_TENANT ?? "false").toLowerCase() !== "false";
7803
- await kernel.use(new SecurityPlugin({ multiTenant }));
7860
+ if (multiTenant) {
7861
+ try {
7862
+ const { OrgScopingPlugin } = await import("@objectstack/plugin-org-scoping");
7863
+ await kernel.use(new OrgScopingPlugin());
7864
+ } catch (err) {
7865
+ this.logger.warn?.("[ArtifactKernelFactory] OrgScopingPlugin not registered (multi-tenant disabled)", {
7866
+ environmentId,
7867
+ error: err?.message
7868
+ });
7869
+ }
7870
+ }
7871
+ const { SecurityPlugin } = await import("@objectstack/plugin-security");
7872
+ await kernel.use(new SecurityPlugin());
7804
7873
  } catch (err) {
7805
7874
  this.logger.warn?.("[ArtifactKernelFactory] SecurityPlugin not registered", {
7806
7875
  environmentId,
@@ -8326,6 +8395,40 @@ function resolveCloudUrl(explicit) {
8326
8395
 
8327
8396
  // src/cloud/marketplace-proxy-plugin.ts
8328
8397
  var MARKETPLACE_PREFIX = "/api/v1/marketplace";
8398
+ var DEFAULT_LRU_MAX = 200;
8399
+ var LIST_TTL_MS = 30 * 60 * 1e3;
8400
+ var PACKAGE_TTL_MS = 2 * 60 * 60 * 1e3;
8401
+ var VERSION_TTL_MS = 24 * 60 * 60 * 1e3;
8402
+ function ttlForPath(pathname) {
8403
+ if (/\/packages\/[^/]+\/versions\//.test(pathname)) return VERSION_TTL_MS;
8404
+ if (/\/packages\/[^/]+/.test(pathname)) return PACKAGE_TTL_MS;
8405
+ return LIST_TTL_MS;
8406
+ }
8407
+ var LruTtlCache = class {
8408
+ constructor(max) {
8409
+ this.max = max;
8410
+ this.map = /* @__PURE__ */ new Map();
8411
+ }
8412
+ get(key) {
8413
+ const entry = this.map.get(key);
8414
+ if (!entry) return void 0;
8415
+ this.map.delete(key);
8416
+ this.map.set(key, entry);
8417
+ return entry;
8418
+ }
8419
+ set(key, entry) {
8420
+ if (this.map.has(key)) this.map.delete(key);
8421
+ this.map.set(key, entry);
8422
+ while (this.map.size > this.max) {
8423
+ const oldest = this.map.keys().next().value;
8424
+ if (oldest === void 0) break;
8425
+ this.map.delete(oldest);
8426
+ }
8427
+ }
8428
+ clear() {
8429
+ this.map.clear();
8430
+ }
8431
+ };
8329
8432
  var MarketplaceProxyPlugin = class _MarketplaceProxyPlugin {
8330
8433
  constructor(config = {}) {
8331
8434
  this.name = "com.objectstack.runtime.marketplace-proxy";
@@ -8347,6 +8450,7 @@ var MarketplaceProxyPlugin = class _MarketplaceProxyPlugin {
8347
8450
  }
8348
8451
  const rawApp = httpServer.getRawApp();
8349
8452
  const cloudUrl = this.cloudUrl;
8453
+ const cache = this.cache;
8350
8454
  const handler = async (c, next) => {
8351
8455
  if (!cloudUrl) {
8352
8456
  return c.json({
@@ -8373,24 +8477,51 @@ var MarketplaceProxyPlugin = class _MarketplaceProxyPlugin {
8373
8477
  }
8374
8478
  }, 405);
8375
8479
  }
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"}`
8480
+ const accept = c.req.header("accept") ?? "application/json";
8481
+ const acceptLang = c.req.header("accept-language") ?? "";
8482
+ const cacheKey = `${incomingUrl.pathname}${incomingUrl.search}|al=${acceptLang}|a=${accept}`;
8483
+ const reqCacheCtl = (c.req.header("cache-control") ?? "").toLowerCase();
8484
+ const bypass = !cache || reqCacheCtl.includes("no-cache") || reqCacheCtl.includes("no-store");
8485
+ const now = Date.now();
8486
+ if (cache && !bypass) {
8487
+ const hit = cache.get(cacheKey);
8488
+ if (hit && hit.expiresAt > now) {
8489
+ return buildCachedResponse(hit, method, "HIT");
8384
8490
  }
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);
8491
+ if (hit) {
8492
+ const revalHeaders = {
8493
+ "Accept": accept,
8494
+ "User-Agent": `objectos-marketplace-proxy/${_MarketplaceProxyPlugin.prototype.version ?? "1.0.0"}`
8495
+ };
8496
+ if (acceptLang) revalHeaders["Accept-Language"] = acceptLang;
8497
+ if (hit.etag) revalHeaders["If-None-Match"] = hit.etag;
8498
+ if (hit.lastModified) revalHeaders["If-Modified-Since"] = hit.lastModified;
8499
+ const revalResp = await fetch(target, { method: "GET", headers: revalHeaders });
8500
+ if (revalResp.status === 304) {
8501
+ hit.expiresAt = now + hit.ttlMs;
8502
+ const newEtag = revalResp.headers.get("etag");
8503
+ const newLm = revalResp.headers.get("last-modified");
8504
+ if (newEtag) hit.etag = newEtag;
8505
+ if (newLm) hit.lastModified = newLm;
8506
+ cache.set(cacheKey, hit);
8507
+ return buildCachedResponse(hit, method, "REVALIDATED");
8508
+ }
8509
+ return await consumeAndMaybeCache(revalResp, cacheKey, incomingUrl.pathname, method, cache);
8510
+ }
8511
+ }
8512
+ const reqHeaders = {
8513
+ // Strip the inbound Host header — fetch will set
8514
+ // it to the cloud host. Forward only the
8515
+ // identifying headers cloud might log.
8516
+ "Accept": accept,
8517
+ "User-Agent": `objectos-marketplace-proxy/${_MarketplaceProxyPlugin.prototype.version ?? "1.0.0"}`
8518
+ };
8519
+ if (acceptLang) reqHeaders["Accept-Language"] = acceptLang;
8520
+ const resp = await fetch(target, { method: "GET", headers: reqHeaders });
8521
+ if (bypass || !cache) {
8522
+ return await passthroughResponse(resp, method, bypass ? "BYPASS" : "MISS");
8391
8523
  }
8392
- const body = await resp.arrayBuffer();
8393
- return new Response(body, { status: resp.status, headers });
8524
+ return await consumeAndMaybeCache(resp, cacheKey, incomingUrl.pathname, method, cache);
8394
8525
  } catch (err) {
8395
8526
  const errObj = err instanceof Error ? err : new Error(err?.message ?? String(err));
8396
8527
  ctx.logger?.error?.("[MarketplaceProxyPlugin] proxy failed", errObj);
@@ -8413,12 +8544,67 @@ var MarketplaceProxyPlugin = class _MarketplaceProxyPlugin {
8413
8544
  }
8414
8545
  }
8415
8546
  }
8416
- ctx.logger?.info?.(`[MarketplaceProxyPlugin] mounted at ${MARKETPLACE_PREFIX}/* \u2192 ${cloudUrl || "(unconfigured)"}`);
8547
+ ctx.logger?.info?.(`[MarketplaceProxyPlugin] mounted at ${MARKETPLACE_PREFIX}/* \u2192 ${cloudUrl || "(unconfigured)"} (cache=${this.cache ? "on" : "off"})`);
8417
8548
  });
8418
8549
  };
8419
8550
  this.cloudUrl = resolveCloudUrl(config.controlPlaneUrl);
8551
+ const envFlag = (process.env.OS_MARKETPLACE_CACHE ?? "").trim().toLowerCase();
8552
+ const envDisabled = ["off", "false", "0", "no", "disable", "disabled"].includes(envFlag);
8553
+ const disabled = config.cacheDisabled ?? envDisabled;
8554
+ this.cache = disabled ? null : new LruTtlCache(Math.max(8, config.cacheMaxEntries ?? DEFAULT_LRU_MAX));
8420
8555
  }
8421
8556
  };
8557
+ var PASSTHROUGH_HEADERS = ["content-type", "cache-control", "etag", "last-modified", "vary"];
8558
+ function collectHeaders(src) {
8559
+ const out = {};
8560
+ for (const h of PASSTHROUGH_HEADERS) {
8561
+ const v = src.headers.get(h);
8562
+ if (v) out[h] = v;
8563
+ }
8564
+ return out;
8565
+ }
8566
+ function buildCachedResponse(entry, method, xCache) {
8567
+ const headers = new Headers(entry.headers);
8568
+ headers.set("X-Cache", xCache);
8569
+ const ageSec = Math.max(0, Math.floor((entry.expiresAt - entry.ttlMs - Date.now()) / -1e3));
8570
+ headers.set("Age", String(Math.max(0, ageSec)));
8571
+ const body = method === "HEAD" ? null : entry.body;
8572
+ return new Response(body, { status: entry.status, headers });
8573
+ }
8574
+ async function passthroughResponse(resp, method, xCache) {
8575
+ const headers = new Headers(collectHeaders(resp));
8576
+ headers.set("X-Cache", xCache);
8577
+ if (method === "HEAD") {
8578
+ try {
8579
+ await resp.arrayBuffer();
8580
+ } catch {
8581
+ }
8582
+ return new Response(null, { status: resp.status, headers });
8583
+ }
8584
+ const body = await resp.arrayBuffer();
8585
+ return new Response(body, { status: resp.status, headers });
8586
+ }
8587
+ async function consumeAndMaybeCache(resp, key, pathname, method, cache) {
8588
+ const body = await resp.arrayBuffer();
8589
+ const headers = collectHeaders(resp);
8590
+ if (resp.status >= 200 && resp.status < 300) {
8591
+ const ttlMs = ttlForPath(pathname);
8592
+ const entry = {
8593
+ status: resp.status,
8594
+ body,
8595
+ headers,
8596
+ etag: resp.headers.get("etag") ?? void 0,
8597
+ lastModified: resp.headers.get("last-modified") ?? void 0,
8598
+ expiresAt: Date.now() + ttlMs,
8599
+ ttlMs
8600
+ };
8601
+ cache.set(key, entry);
8602
+ }
8603
+ const respHeaders = new Headers(headers);
8604
+ respHeaders.set("X-Cache", "MISS");
8605
+ const outBody = method === "HEAD" ? null : body;
8606
+ return new Response(outBody, { status: resp.status, headers: respHeaders });
8607
+ }
8422
8608
 
8423
8609
  // src/cloud/runtime-config-plugin.ts
8424
8610
  var RuntimeConfigPlugin = class {
@@ -8456,12 +8642,14 @@ var RuntimeConfigPlugin = class {
8456
8642
  let defaultEnvironmentId;
8457
8643
  let defaultOrgId;
8458
8644
  let resolvedSingleEnv = this.singleEnvironment;
8459
- if (envRegistry && host && typeof envRegistry.resolveHostname === "function") {
8645
+ const resolveFn = typeof envRegistry?.resolveByHostname === "function" ? envRegistry.resolveByHostname.bind(envRegistry) : typeof envRegistry?.resolveHostname === "function" ? envRegistry.resolveHostname.bind(envRegistry) : null;
8646
+ if (resolveFn && host) {
8460
8647
  try {
8461
- const resolved = await envRegistry.resolveHostname(host);
8648
+ const resolved = await resolveFn(host);
8462
8649
  if (resolved?.environmentId) {
8463
- defaultEnvironmentId = resolved.environmentId;
8464
- if (resolved.organizationId) defaultOrgId = String(resolved.organizationId);
8650
+ defaultEnvironmentId = String(resolved.environmentId);
8651
+ const orgId = resolved.organizationId ?? resolved.organization_id;
8652
+ if (orgId) defaultOrgId = String(orgId);
8465
8653
  resolvedSingleEnv = true;
8466
8654
  }
8467
8655
  } catch {
@@ -8472,7 +8660,11 @@ var RuntimeConfigPlugin = class {
8472
8660
  singleEnvironment: resolvedSingleEnv,
8473
8661
  defaultOrgId,
8474
8662
  defaultEnvironmentId,
8475
- features
8663
+ features,
8664
+ branding: {
8665
+ productName: this.productName,
8666
+ productShortName: this.productShortName
8667
+ }
8476
8668
  });
8477
8669
  };
8478
8670
  rawApp.get("/api/v1/runtime/config", handler);
@@ -8489,6 +8681,10 @@ var RuntimeConfigPlugin = class {
8489
8681
  this.cloudUrl = config.controlPlaneUrl === "" ? "" : resolveCloudUrl(config.controlPlaneUrl) ?? "";
8490
8682
  this.installLocal = !!config.installLocal;
8491
8683
  this.singleEnvironment = !!config.singleEnvironment;
8684
+ const envName = (typeof process !== "undefined" ? process.env?.OS_PRODUCT_NAME : void 0)?.trim();
8685
+ const envShort = (typeof process !== "undefined" ? process.env?.OS_PRODUCT_SHORT_NAME : void 0)?.trim();
8686
+ this.productName = (config.productName ?? envName ?? "ObjectOS").trim() || "ObjectOS";
8687
+ this.productShortName = (config.productShortName ?? envShort ?? this.productName).trim() || this.productName;
8492
8688
  }
8493
8689
  };
8494
8690
 
@@ -8778,9 +8974,15 @@ var MarketplaceInstallLocalPlugin = class {
8778
8974
  const postHandler = async (c) => this.handleInstall(c, ctx);
8779
8975
  const getHandler = async (c) => this.handleList(c);
8780
8976
  const deleteHandler = async (c) => this.handleUninstall(c, ctx);
8977
+ const reseedHandler = async (c) => this.handleReseed(c, ctx);
8978
+ const purgeHandler = async (c) => this.handlePurge(c, ctx);
8781
8979
  if (typeof rawApp.post === "function") rawApp.post(ROUTE_BASE, postHandler);
8782
8980
  if (typeof rawApp.get === "function") rawApp.get(ROUTE_BASE, getHandler);
8783
8981
  if (typeof rawApp.delete === "function") rawApp.delete(`${ROUTE_BASE}/:manifestId`, deleteHandler);
8982
+ if (typeof rawApp.post === "function") {
8983
+ rawApp.post(`${ROUTE_BASE}/:manifestId/reseed-sample-data`, reseedHandler);
8984
+ rawApp.post(`${ROUTE_BASE}/:manifestId/purge-sample-data`, purgeHandler);
8985
+ }
8784
8986
  ctx.logger?.info?.(`[MarketplaceInstallLocal] mounted at ${ROUTE_BASE} (storage: ${this.storageDir})`);
8785
8987
  });
8786
8988
  };
@@ -8877,7 +9079,8 @@ var MarketplaceInstallLocalPlugin = class {
8877
9079
  version,
8878
9080
  manifest,
8879
9081
  installedAt: (/* @__PURE__ */ new Date()).toISOString(),
8880
- installedBy: userId
9082
+ installedBy: userId,
9083
+ withSampleData: false
8881
9084
  };
8882
9085
  try {
8883
9086
  mkdirSync3(this.storageDir, { recursive: true });
@@ -8904,6 +9107,13 @@ var MarketplaceInstallLocalPlugin = class {
8904
9107
  ctx.logger?.warn?.(`[MarketplaceInstallLocal] syncSchemas failed for ${manifestId}: ${err?.message ?? err}`);
8905
9108
  }
8906
9109
  const seededSummary = await this.applySideEffects(ctx, manifest, { seedNow: true, c });
9110
+ if (seededSummary.seeded.mode === "inline" && (seededSummary.seeded.inserted ?? 0) + (seededSummary.seeded.updated ?? 0) > 0) {
9111
+ entry.withSampleData = true;
9112
+ try {
9113
+ writeFileSync2(join(this.storageDir, safeFilename(manifestId)), JSON.stringify(entry, null, 2), "utf8");
9114
+ } catch {
9115
+ }
9116
+ }
8907
9117
  return c.json({
8908
9118
  success: true,
8909
9119
  data: {
@@ -8930,7 +9140,8 @@ var MarketplaceInstallLocalPlugin = class {
8930
9140
  manifestId: e.manifestId,
8931
9141
  version: e.version,
8932
9142
  installedAt: e.installedAt,
8933
- installedBy: e.installedBy
9143
+ installedBy: e.installedBy,
9144
+ withSampleData: e.withSampleData ?? false
8934
9145
  })),
8935
9146
  total: entries.length,
8936
9147
  storageDir: this.storageDir
@@ -8995,6 +9206,145 @@ var MarketplaceInstallLocalPlugin = class {
8995
9206
  * dev / single-tenant runtimes. Stricter checks can be layered on
8996
9207
  * via a middleware in cloud-hosted multi-tenant deployments.
8997
9208
  */
9209
+ /**
9210
+ * POST /api/v1/marketplace/install-local/:manifestId/reseed-sample-data
9211
+ *
9212
+ * Re-runs SeedLoaderService against the cached manifest's `data` arrays.
9213
+ * Idempotent (upsert by id). Useful when:
9214
+ * • The user installed an app and skipped sample data
9215
+ * • A purge was undone
9216
+ * • The user wants a clean baseline back after editing demo rows
9217
+ *
9218
+ * Multi-tenant: requires an active organization on the session (same
9219
+ * rule as install seed path).
9220
+ */
9221
+ this.handleReseed = async (c, ctx) => {
9222
+ const userId = await this.requireAuthenticatedUser(c, ctx);
9223
+ if (!userId) {
9224
+ return c.json({ success: false, error: { code: "unauthorized", message: "Authentication required." } }, 401);
9225
+ }
9226
+ const manifestId = String(c.req.param?.("manifestId") ?? c.req.params?.manifestId ?? "").trim();
9227
+ if (!manifestId) {
9228
+ return c.json({ success: false, error: { code: "bad_request", message: "manifestId path param required." } }, 400);
9229
+ }
9230
+ const file = join(this.storageDir, safeFilename(manifestId));
9231
+ if (!existsSync2(file)) {
9232
+ return c.json({ success: false, error: { code: "not_found", message: `No marketplace install for ${manifestId}.` } }, 404);
9233
+ }
9234
+ let entry;
9235
+ try {
9236
+ entry = JSON.parse(readFileSync(file, "utf8"));
9237
+ } catch (err) {
9238
+ return c.json({ success: false, error: { code: "storage_failed", message: `Failed to read manifest cache: ${err?.message ?? err}` } }, 500);
9239
+ }
9240
+ const summary = await this.applySideEffects(ctx, entry.manifest, { seedNow: true, c });
9241
+ if (summary.seeded.mode === "skipped") {
9242
+ return c.json({
9243
+ success: false,
9244
+ error: {
9245
+ code: "reseed_skipped",
9246
+ message: `Reseed did not run: ${summary.seeded.reason ?? "unknown reason"}`
9247
+ }
9248
+ }, 400);
9249
+ }
9250
+ try {
9251
+ entry.withSampleData = true;
9252
+ writeFileSync2(file, JSON.stringify(entry, null, 2), "utf8");
9253
+ } catch {
9254
+ }
9255
+ return c.json({
9256
+ success: true,
9257
+ data: {
9258
+ manifestId,
9259
+ inserted: summary.seeded.inserted ?? 0,
9260
+ updated: summary.seeded.updated ?? 0,
9261
+ errors: summary.seeded.errors ?? 0,
9262
+ withSampleData: true
9263
+ }
9264
+ }, 200);
9265
+ };
9266
+ /**
9267
+ * POST /api/v1/marketplace/install-local/:manifestId/purge-sample-data
9268
+ *
9269
+ * Deletes every record whose id is declared in the cached manifest's
9270
+ * seed datasets. Uses the `driver` service directly to bypass ACL /
9271
+ * lifecycle hooks (same pattern as cloud purge). User-created records
9272
+ * are never touched — only ids declared in the package's bundled
9273
+ * datasets are removed. Already-deleted rows count as `skipped`.
9274
+ */
9275
+ this.handlePurge = async (c, ctx) => {
9276
+ const userId = await this.requireAuthenticatedUser(c, ctx);
9277
+ if (!userId) {
9278
+ return c.json({ success: false, error: { code: "unauthorized", message: "Authentication required." } }, 401);
9279
+ }
9280
+ const manifestId = String(c.req.param?.("manifestId") ?? c.req.params?.manifestId ?? "").trim();
9281
+ if (!manifestId) {
9282
+ return c.json({ success: false, error: { code: "bad_request", message: "manifestId path param required." } }, 400);
9283
+ }
9284
+ const file = join(this.storageDir, safeFilename(manifestId));
9285
+ if (!existsSync2(file)) {
9286
+ return c.json({ success: false, error: { code: "not_found", message: `No marketplace install for ${manifestId}.` } }, 404);
9287
+ }
9288
+ let entry;
9289
+ try {
9290
+ entry = JSON.parse(readFileSync(file, "utf8"));
9291
+ } catch (err) {
9292
+ return c.json({ success: false, error: { code: "storage_failed", message: `Failed to read manifest cache: ${err?.message ?? err}` } }, 500);
9293
+ }
9294
+ const datasets = Array.isArray(entry.manifest?.data) ? entry.manifest.data.filter((d) => d && d.object && Array.isArray(d.records)) : [];
9295
+ if (datasets.length === 0) {
9296
+ return c.json({
9297
+ success: false,
9298
+ error: { code: "nothing_to_purge", message: "This package declares no seed datasets." }
9299
+ }, 400);
9300
+ }
9301
+ let driver;
9302
+ try {
9303
+ driver = ctx.getService("driver");
9304
+ } catch {
9305
+ }
9306
+ if (!driver || typeof driver.delete !== "function") {
9307
+ return c.json({
9308
+ success: false,
9309
+ error: { code: "driver_missing", message: "driver service unavailable \u2014 cannot purge." }
9310
+ }, 500);
9311
+ }
9312
+ let deleted = 0;
9313
+ let skipped = 0;
9314
+ let errors = 0;
9315
+ for (const ds of datasets) {
9316
+ const object = String(ds.object);
9317
+ for (const rec of ds.records) {
9318
+ const id = rec?.id;
9319
+ if (id === void 0 || id === null || id === "") {
9320
+ skipped++;
9321
+ continue;
9322
+ }
9323
+ try {
9324
+ const r = await driver.delete(object, id);
9325
+ if (r === false || r === 0 || r?.deleted === 0) skipped++;
9326
+ else deleted++;
9327
+ } catch (err) {
9328
+ const msg = String(err?.message ?? err);
9329
+ if (/not.?found|no row/i.test(msg)) skipped++;
9330
+ else {
9331
+ errors++;
9332
+ ctx.logger?.warn?.(`[MarketplaceInstallLocal] purge ${object}#${id}: ${msg}`);
9333
+ }
9334
+ }
9335
+ }
9336
+ }
9337
+ try {
9338
+ entry.withSampleData = false;
9339
+ writeFileSync2(file, JSON.stringify(entry, null, 2), "utf8");
9340
+ } catch {
9341
+ }
9342
+ ctx.logger?.info?.(`[MarketplaceInstallLocal] purged ${manifestId}: deleted=${deleted} skipped=${skipped} errors=${errors}`);
9343
+ return c.json({
9344
+ success: true,
9345
+ data: { manifestId, deleted, skipped, errors, withSampleData: false }
9346
+ }, 200);
9347
+ };
8998
9348
  /**
8999
9349
  * Replicate the start-time side-effects that AppPlugin runs for
9000
9350
  * statically-declared apps but the `manifest` service does NOT: