@objectstack/runtime 6.8.1 → 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.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;
@@ -1468,6 +1482,66 @@ var init_app_plugin = __esm({
1468
1482
  } catch (err) {
1469
1483
  ctx.logger.error("[AppPlugin] Failed to schedule approval-process registration", err, { appId });
1470
1484
  }
1485
+ try {
1486
+ const jobs = Array.isArray(this.bundle.jobs) ? this.bundle.jobs : Array.isArray((this.bundle.manifest || {}).jobs) ? this.bundle.manifest.jobs : [];
1487
+ if (jobs.length > 0) {
1488
+ ctx.hook("kernel:ready", async () => {
1489
+ let svc;
1490
+ try {
1491
+ svc = ctx.getService("job");
1492
+ } catch {
1493
+ }
1494
+ if (!svc || typeof svc.schedule !== "function") {
1495
+ ctx.logger.warn("[AppPlugin] job service not registered \u2014 skipping declarative jobs", {
1496
+ appId,
1497
+ jobCount: jobs.length
1498
+ });
1499
+ return;
1500
+ }
1501
+ const fnMap = collectBundleFunctions(this.bundle);
1502
+ let ok = 0;
1503
+ for (const job of jobs) {
1504
+ const jobName = job?.name;
1505
+ if (!jobName) {
1506
+ ctx.logger.warn("[AppPlugin] skipping job without name", { appId, job });
1507
+ continue;
1508
+ }
1509
+ if (job.enabled === false) {
1510
+ ctx.logger.debug("[AppPlugin] job disabled \u2014 skipping", { appId, job: jobName });
1511
+ continue;
1512
+ }
1513
+ const handler = fnMap[job.handler];
1514
+ if (typeof handler !== "function") {
1515
+ ctx.logger.warn("[AppPlugin] job handler not found in bundle.functions \u2014 skipping", {
1516
+ appId,
1517
+ job: jobName,
1518
+ handler: job.handler
1519
+ });
1520
+ continue;
1521
+ }
1522
+ try {
1523
+ await svc.schedule(
1524
+ jobName,
1525
+ job.schedule,
1526
+ async (jobCtx) => {
1527
+ await handler({ ...jobCtx, jobId: jobName, bundle: this.bundle });
1528
+ }
1529
+ );
1530
+ ok++;
1531
+ } catch (err) {
1532
+ ctx.logger.warn("[AppPlugin] Failed to schedule job", {
1533
+ appId,
1534
+ job: jobName,
1535
+ error: err?.message ?? String(err)
1536
+ });
1537
+ }
1538
+ }
1539
+ ctx.logger.info("[AppPlugin] Scheduled background jobs", { appId, count: ok });
1540
+ });
1541
+ }
1542
+ } catch (err) {
1543
+ ctx.logger.error("[AppPlugin] Failed to schedule background-job registration", err, { appId });
1544
+ }
1471
1545
  this.emitCatalogEvent(ctx, "app:registered", sys);
1472
1546
  await this.loadTranslations(ctx, appId);
1473
1547
  const seedDatasets = [];
@@ -1540,51 +1614,68 @@ var init_app_plugin = __esm({
1540
1614
  } catch (e) {
1541
1615
  ctx.logger.warn("[Seeder] Failed to register seed-datasets/seed-replayer service", { error: e?.message });
1542
1616
  }
1543
- const multiTenant = String(process.env.OS_MULTI_TENANT ?? "true").toLowerCase() !== "false";
1617
+ const multiTenant = String(process.env.OS_MULTI_TENANT ?? "false").toLowerCase() !== "false";
1544
1618
  if (multiTenant) {
1545
1619
  ctx.logger.info("[Seeder] multi-tenant mode \u2014 skipping inline seed; per-org replay will run on sys_organization insert");
1546
1620
  } else {
1547
- try {
1548
- const metadata = ctx.getService("metadata");
1549
- if (metadata) {
1550
- const seedLoader = new SeedLoaderService(ql, metadata, ctx.logger);
1551
- const { SeedLoaderRequestSchema } = await import("@objectstack/spec/data");
1552
- const request = SeedLoaderRequestSchema.parse({
1553
- datasets: normalizedDatasets,
1554
- config: { defaultMode: "upsert", multiPass: true }
1555
- });
1556
- const result = await seedLoader.load(request);
1557
- ctx.logger.info("[Seeder] Seed loading complete", {
1558
- inserted: result.summary.totalInserted,
1559
- updated: result.summary.totalUpdated,
1560
- errors: result.errors.length
1561
- });
1562
- } else {
1563
- 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 });
1564
1654
  for (const dataset of normalizedDatasets) {
1565
- ctx.logger.info(`[Seeder] Seeding ${dataset.records.length} records for ${dataset.object}`);
1566
1655
  for (const record of dataset.records) {
1567
1656
  try {
1568
1657
  await ql.insert(dataset.object, record, { context: { isSystem: true } });
1569
- } catch (err) {
1570
- 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 });
1571
1660
  }
1572
1661
  }
1573
1662
  }
1574
- ctx.logger.info("[Seeder] Data seeding complete.");
1663
+ ctx.logger.info("[Seeder] Data seeding complete (fallback).");
1575
1664
  }
1576
- } catch (err) {
1577
- ctx.logger.warn("[Seeder] SeedLoaderService failed, falling back to basic insert", { error: err.message });
1578
- for (const dataset of normalizedDatasets) {
1579
- for (const record of dataset.records) {
1580
- try {
1581
- await ql.insert(dataset.object, record, { context: { isSystem: true } });
1582
- } catch (insertErr) {
1583
- ctx.logger.warn(`[Seeder] Failed to insert ${dataset.object} record:`, { error: insertErr.message });
1584
- }
1585
- }
1586
- }
1587
- 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
+ });
1588
1679
  }
1589
1680
  }
1590
1681
  }
@@ -1595,10 +1686,54 @@ var init_app_plugin = __esm({
1595
1686
  };
1596
1687
  this.bundle = bundle;
1597
1688
  this.projectContext = projectContext;
1598
- const sys = bundle.manifest || bundle;
1599
- const appId = sys.id || sys.name || "unnamed-app";
1689
+ const sys = bundle?.manifest || bundle;
1690
+ const appId = sys?.id || sys?.name;
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
+ ];
1714
+ const hasAppPayload = APP_CATEGORY_KEYS.some((k) => {
1715
+ const v = (bundle && bundle[k]) ?? (sys && sys[k]);
1716
+ return Array.isArray(v) && v.length > 0;
1717
+ });
1718
+ if (!hasAppPayload) {
1719
+ this.empty = true;
1720
+ const envSlug = projectContext?.environmentId ? projectContext.environmentId.slice(0, 8) : "empty";
1721
+ this.name = `plugin.app.empty-${envSlug}`;
1722
+ return;
1723
+ }
1724
+ const bundleKeys = bundle && typeof bundle === "object" ? Object.keys(bundle).slice(0, 20).join(",") : typeof bundle;
1725
+ const sysKeys = sys && typeof sys === "object" ? Object.keys(sys).slice(0, 20).join(",") : typeof sys;
1726
+ const ctxHint = projectContext ? ` projectContext=${JSON.stringify({
1727
+ environmentId: projectContext.environmentId,
1728
+ packageId: projectContext.packageId,
1729
+ source: projectContext.source
1730
+ })}` : "";
1731
+ throw new Error(
1732
+ `[AppPlugin] bundle has app payload but no manifest.id / manifest.name \u2014 cannot register as a plugin. bundleKeys=[${bundleKeys}] sysKeys=[${sysKeys}]${ctxHint}`
1733
+ );
1734
+ }
1600
1735
  this.name = `plugin.app.${appId}`;
1601
- this.version = sys.version;
1736
+ this.version = sys?.version;
1602
1737
  }
1603
1738
  /**
1604
1739
  * Emit a kernel hook so the control-plane `AppCatalogService` can
@@ -2211,7 +2346,7 @@ function resolveObjectStackHome() {
2211
2346
  var StandaloneStackConfigSchema = import_zod.z.object({
2212
2347
  databaseUrl: import_zod.z.string().optional(),
2213
2348
  databaseAuthToken: import_zod.z.string().optional(),
2214
- databaseDriver: import_zod.z.enum(["sqlite", "sqlite-wasm", "turso", "memory", "postgres", "mongodb"]).optional(),
2349
+ databaseDriver: import_zod.z.enum(["sqlite", "sqlite-wasm", "memory", "postgres", "mongodb"]).optional(),
2215
2350
  environmentId: import_zod.z.string().optional(),
2216
2351
  artifactPath: import_zod.z.string().optional(),
2217
2352
  /**
@@ -2229,7 +2364,6 @@ var StandaloneStackConfigSchema = import_zod.z.object({
2229
2364
  });
2230
2365
  function detectDriverFromUrl(dbUrl) {
2231
2366
  if (/^memory:\/\//i.test(dbUrl)) return "memory";
2232
- if (/^(libsql|https?):\/\//i.test(dbUrl)) return "turso";
2233
2367
  if (/^(postgres(ql)?|pg):\/\//i.test(dbUrl)) return "postgres";
2234
2368
  if (/^mongodb(\+srv)?:\/\//i.test(dbUrl)) return "mongodb";
2235
2369
  if (/^wasm-sqlite:\/\//i.test(dbUrl)) return "sqlite-wasm";
@@ -2237,7 +2371,7 @@ function detectDriverFromUrl(dbUrl) {
2237
2371
  if (/^file:/i.test(dbUrl)) return "sqlite";
2238
2372
  if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(dbUrl)) return "sqlite";
2239
2373
  throw new Error(
2240
- `[StandaloneStack] Unsupported database URL scheme: ${dbUrl}. Supported schemes: memory://, libsql://, https://, postgres://, pg://, mongodb://, mongodb+srv://, file:`
2374
+ `[StandaloneStack] Unsupported database URL scheme: ${dbUrl}. Supported schemes: memory://, postgres://, pg://, mongodb://, mongodb+srv://, file:`
2241
2375
  );
2242
2376
  }
2243
2377
  async function createStandaloneStack(config) {
@@ -2251,25 +2385,12 @@ async function createStandaloneStack(config) {
2251
2385
  const artifactPathInput = cfg.artifactPath ?? process.env.OS_ARTIFACT_PATH ?? (0, import_node_path2.resolve)(cwd, "dist/objectstack.json");
2252
2386
  const artifactPath = isHttpUrl(artifactPathInput) ? artifactPathInput : artifactPathInput.startsWith("/") ? artifactPathInput : (0, import_node_path2.resolve)(cwd, artifactPathInput);
2253
2387
  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")}`);
2254
- const dbAuthToken = cfg.databaseAuthToken ?? process.env.OS_DATABASE_AUTH_TOKEN?.trim() ?? process.env.TURSO_AUTH_TOKEN?.trim();
2255
2388
  const explicitDriver = cfg.databaseDriver ?? process.env.OS_DATABASE_DRIVER?.trim();
2256
2389
  const dbDriver = explicitDriver ?? detectDriverFromUrl(dbUrl);
2257
2390
  let driverPlugin;
2258
2391
  if (dbDriver === "memory") {
2259
2392
  const { InMemoryDriver } = await import("@objectstack/driver-memory");
2260
2393
  driverPlugin = new DriverPlugin2(new InMemoryDriver());
2261
- } else if (dbDriver === "turso") {
2262
- let TursoDriver;
2263
- try {
2264
- ({ TursoDriver } = await import("@objectstack/driver-turso"));
2265
- } catch (err) {
2266
- throw new Error(
2267
- `[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})`
2268
- );
2269
- }
2270
- driverPlugin = new DriverPlugin2(
2271
- new TursoDriver({ url: dbUrl, authToken: dbAuthToken })
2272
- );
2273
2394
  } else if (dbDriver === "postgres") {
2274
2395
  const { SqlDriver } = await import("@objectstack/driver-sql");
2275
2396
  driverPlugin = new DriverPlugin2(
@@ -2528,8 +2649,12 @@ async function resolveExecutionContext(opts) {
2528
2649
  if (!userId) {
2529
2650
  try {
2530
2651
  const authService = await opts.getService("auth");
2652
+ let api = authService?.api;
2653
+ if (!api && typeof authService?.getApi === "function") {
2654
+ api = await authService.getApi();
2655
+ }
2531
2656
  const headersInstance = toHeaders(headers);
2532
- const sessionData = await authService?.api?.getSession?.({ headers: headersInstance });
2657
+ const sessionData = await api?.getSession?.({ headers: headersInstance });
2533
2658
  userId = sessionData?.user?.id ?? sessionData?.session?.userId;
2534
2659
  tenantId = tenantId ?? sessionData?.session?.activeOrganizationId;
2535
2660
  ctx.accessToken = sessionData?.session?.token ?? ctx.accessToken;
@@ -4006,7 +4131,7 @@ var _HttpDispatcher = class _HttpDispatcher {
4006
4131
  return {
4007
4132
  handled: true,
4008
4133
  response: this.error(
4009
- "No ObjectQL driver is registered. Register at least one DriverPlugin (e.g. InMemoryDriver or TursoDriver).",
4134
+ "No ObjectQL driver is registered. Register at least one DriverPlugin (e.g. InMemoryDriver or SqlDriver).",
4010
4135
  503
4011
4136
  )
4012
4137
  };
@@ -5374,7 +5499,7 @@ var _HttpDispatcher = class _HttpDispatcher {
5374
5499
  * Handle AI service routes (/ai/chat, /ai/models, /ai/conversations, etc.)
5375
5500
  * Resolves the AI service and its built-in route handlers, then dispatches.
5376
5501
  */
5377
- async handleAI(subPath, method, body, query, _context) {
5502
+ async handleAI(subPath, method, body, query, context) {
5378
5503
  let aiService;
5379
5504
  try {
5380
5505
  aiService = await this.resolveService("ai");
@@ -5418,7 +5543,23 @@ var _HttpDispatcher = class _HttpDispatcher {
5418
5543
  if (route.method !== method) continue;
5419
5544
  const params = matchRoute(route.path, fullPath);
5420
5545
  if (params === null) continue;
5421
- const result = await route.handler({ body, params, query });
5546
+ const ec = context.executionContext;
5547
+ const user = ec?.userId ? {
5548
+ userId: ec.userId,
5549
+ id: ec.userId,
5550
+ displayName: ec.userDisplayName ?? ec.userName ?? ec.userId,
5551
+ email: ec.userEmail,
5552
+ roles: Array.isArray(ec.roles) ? ec.roles : [],
5553
+ permissions: Array.isArray(ec.permissions) ? ec.permissions : [],
5554
+ organizationId: ec.tenantId
5555
+ } : void 0;
5556
+ const result = await route.handler({
5557
+ body,
5558
+ params,
5559
+ query,
5560
+ headers: context.request?.headers,
5561
+ user
5562
+ });
5422
5563
  if (result.stream && result.events) {
5423
5564
  return {
5424
5565
  handled: true,
@@ -5913,13 +6054,22 @@ function resolveErrorReporter(ctx, override) {
5913
6054
  }
5914
6055
 
5915
6056
  // src/dispatcher-plugin.ts
5916
- function mountRouteOnServer(route, server, routePath, securityHeaders) {
6057
+ function mountRouteOnServer(route, server, routePath, securityHeaders, resolveUser) {
5917
6058
  const handler = async (req, res) => {
5918
6059
  try {
6060
+ let user;
6061
+ if (resolveUser) {
6062
+ try {
6063
+ user = await resolveUser(req.headers ?? {});
6064
+ } catch {
6065
+ }
6066
+ }
5919
6067
  const result = await route.handler({
5920
6068
  body: req.body,
5921
6069
  params: req.params,
5922
- query: req.query
6070
+ query: req.query,
6071
+ headers: req.headers,
6072
+ user
5923
6073
  });
5924
6074
  if (result.stream && result.events) {
5925
6075
  res.status(result.status);
@@ -6656,6 +6806,32 @@ function createDispatcherPlugin(config = {}) {
6656
6806
  }
6657
6807
  }
6658
6808
  ctx.logger.info("Dispatcher bridge routes registered", { prefix, enableProjectScoping, projectResolution });
6809
+ const resolveRequestUser = async (headers) => {
6810
+ try {
6811
+ const authService = ctx.getService("auth");
6812
+ if (!authService) return void 0;
6813
+ let api = authService.api;
6814
+ if (!api && typeof authService.getApi === "function") {
6815
+ api = await authService.getApi();
6816
+ }
6817
+ if (!api?.getSession) return void 0;
6818
+ const headersInstance = headers instanceof Headers ? headers : new Headers(headers);
6819
+ const sessionData = await api.getSession({ headers: headersInstance });
6820
+ const userId = sessionData?.user?.id ?? sessionData?.session?.userId;
6821
+ if (!userId) return void 0;
6822
+ return {
6823
+ userId,
6824
+ id: userId,
6825
+ displayName: sessionData?.user?.name ?? sessionData?.user?.email ?? userId,
6826
+ email: sessionData?.user?.email,
6827
+ roles: [],
6828
+ permissions: [],
6829
+ organizationId: sessionData?.session?.activeOrganizationId
6830
+ };
6831
+ } catch {
6832
+ return void 0;
6833
+ }
6834
+ };
6659
6835
  const toScopedPath = (routePath) => {
6660
6836
  if (routePath.startsWith(prefix)) {
6661
6837
  const tail = routePath.slice(prefix.length);
@@ -6668,11 +6844,11 @@ function createDispatcherPlugin(config = {}) {
6668
6844
  const routePath = route.path.startsWith("/api/v1") ? route.path : `${prefix}${route.path}`;
6669
6845
  let count = 0;
6670
6846
  if (enableProjectScoping && projectResolution === "required") {
6671
- if (mountRouteOnServer(route, server, toScopedPath(routePath), securityHeaders)) count++;
6847
+ if (mountRouteOnServer(route, server, toScopedPath(routePath), securityHeaders, resolveRequestUser)) count++;
6672
6848
  } else {
6673
- if (mountRouteOnServer(route, server, routePath, securityHeaders)) count++;
6849
+ if (mountRouteOnServer(route, server, routePath, securityHeaders, resolveRequestUser)) count++;
6674
6850
  if (enableProjectScoping) {
6675
- if (mountRouteOnServer(route, server, toScopedPath(routePath), securityHeaders)) count++;
6851
+ if (mountRouteOnServer(route, server, toScopedPath(routePath), securityHeaders, resolveRequestUser)) count++;
6676
6852
  }
6677
6853
  }
6678
6854
  return count;
@@ -7447,6 +7623,27 @@ function extractRuntimeFromMetadata(metadata) {
7447
7623
  }
7448
7624
  async function createDriver(driverType, databaseUrl, authToken) {
7449
7625
  switch (driverType) {
7626
+ case "libsql":
7627
+ case "turso": {
7628
+ let TursoDriver;
7629
+ try {
7630
+ ({ TursoDriver } = await import("@objectstack/driver-turso"));
7631
+ } catch (primaryErr) {
7632
+ try {
7633
+ const { createRequire } = await import("module");
7634
+ const path = await import("path");
7635
+ const url = await import("url");
7636
+ const hostRequire = createRequire(path.join(process.cwd(), "noop.js"));
7637
+ const resolved = hostRequire.resolve("@objectstack/driver-turso");
7638
+ ({ TursoDriver } = await import(url.pathToFileURL(resolved).href));
7639
+ } catch (fallbackErr) {
7640
+ throw new Error(
7641
+ `[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})`
7642
+ );
7643
+ }
7644
+ }
7645
+ return new TursoDriver({ url: databaseUrl, authToken });
7646
+ }
7450
7647
  case "memory": {
7451
7648
  const { InMemoryDriver } = await import("@objectstack/driver-memory");
7452
7649
  const dbName = databaseUrl.replace(/^memory:\/\//, "").trim();
@@ -7465,18 +7662,6 @@ async function createDriver(driverType, databaseUrl, authToken) {
7465
7662
  useNullAsDefault: true
7466
7663
  });
7467
7664
  }
7468
- case "libsql":
7469
- case "turso": {
7470
- let TursoDriver;
7471
- try {
7472
- ({ TursoDriver } = await import("@objectstack/driver-turso"));
7473
- } catch (err) {
7474
- throw new Error(
7475
- `[ArtifactEnvironmentRegistry] libsql/turso driver requested but @objectstack/driver-turso is not installed. Install it with: npm install @objectstack/driver-turso. (${err?.message ?? err})`
7476
- );
7477
- }
7478
- return new TursoDriver({ url: databaseUrl, authToken });
7479
- }
7480
7665
  case "postgres":
7481
7666
  case "postgresql":
7482
7667
  case "pg": {
@@ -7734,39 +7919,7 @@ var ArtifactKernelFactory = class {
7734
7919
  // intentionally do NOT pass crossSubDomainCookies here
7735
7920
  // so cookies stay isolated per project subdomain.
7736
7921
  trustedOrigins: trustedOriginsList.length ? trustedOriginsList : void 0,
7737
- ...oidcProviders ? { oidcProviders } : {},
7738
- // Auto-provision a personal organization for every new
7739
- // user. SecurityPlugin's ObjectQL middleware does this
7740
- // for direct `ql.insert` calls, but better-auth's
7741
- // adapter writes through `dataEngine` directly,
7742
- // bypassing that middleware — so JIT-created SSO users
7743
- // would otherwise land on the empty "create
7744
- // organization" screen on first login.
7745
- databaseHooks: {
7746
- user: {
7747
- create: {
7748
- after: async (user) => {
7749
- try {
7750
- const ql = kernel.getService("objectql");
7751
- if (!ql) return;
7752
- const [{ ensureUserHasOrganization, cloneTenantSeedData }] = await Promise.all([
7753
- import("@objectstack/plugin-security")
7754
- ]);
7755
- await ensureUserHasOrganization(ql, user, {
7756
- logger: this.logger,
7757
- cloneSeedData: cloneTenantSeedData
7758
- });
7759
- } catch (e) {
7760
- this.logger.warn?.("[ArtifactKernelFactory] auto-org provisioning hook failed", {
7761
- environmentId,
7762
- userId: user?.id,
7763
- error: e?.message
7764
- });
7765
- }
7766
- }
7767
- }
7768
- }
7769
- }
7922
+ ...oidcProviders ? { oidcProviders } : {}
7770
7923
  }));
7771
7924
  if (oidcProviders) {
7772
7925
  this.logger.info?.("[ArtifactKernelFactory] platform SSO wired", {
@@ -7784,9 +7937,20 @@ var ArtifactKernelFactory = class {
7784
7937
  this.logger.warn?.("[ArtifactKernelFactory] OS_AUTH_SECRET not set \u2014 per-project AuthPlugin skipped (auth endpoints will return 404)", { environmentId });
7785
7938
  }
7786
7939
  try {
7940
+ const multiTenant = String(process.env.OS_MULTI_TENANT ?? "false").toLowerCase() !== "false";
7941
+ if (multiTenant) {
7942
+ try {
7943
+ const { OrgScopingPlugin } = await import("@objectstack/plugin-org-scoping");
7944
+ await kernel.use(new OrgScopingPlugin());
7945
+ } catch (err) {
7946
+ this.logger.warn?.("[ArtifactKernelFactory] OrgScopingPlugin not registered (multi-tenant disabled)", {
7947
+ environmentId,
7948
+ error: err?.message
7949
+ });
7950
+ }
7951
+ }
7787
7952
  const { SecurityPlugin } = await import("@objectstack/plugin-security");
7788
- const multiTenant = String(process.env.OS_MULTI_TENANT ?? "true").toLowerCase() !== "false";
7789
- await kernel.use(new SecurityPlugin({ multiTenant }));
7953
+ await kernel.use(new SecurityPlugin());
7790
7954
  } catch (err) {
7791
7955
  this.logger.warn?.("[ArtifactKernelFactory] SecurityPlugin not registered", {
7792
7956
  environmentId,
@@ -7794,8 +7958,15 @@ var ArtifactKernelFactory = class {
7794
7958
  });
7795
7959
  }
7796
7960
  const projectName = project.hostname ?? environmentId;
7797
- const bundle = artifact.metadata;
7798
- const sys = bundle?.manifest ?? bundle;
7961
+ const artifactAny = artifact;
7962
+ const topLevelManifest = artifactAny?.manifest && typeof artifactAny.manifest === "object" ? artifactAny.manifest : null;
7963
+ const topLevelFunctions = Array.isArray(artifactAny?.functions) ? artifactAny.functions : [];
7964
+ const bundle = {
7965
+ ...artifact.metadata ?? {},
7966
+ ...topLevelManifest ? { manifest: topLevelManifest } : {},
7967
+ functions: topLevelFunctions
7968
+ };
7969
+ const sys = bundle.manifest ?? bundle;
7799
7970
  const packageId = sys?.packageId ?? sys?.package_id ?? bundle?.packageId;
7800
7971
  const i18nCfg = bundle?.i18n ?? sys?.i18n ?? {};
7801
7972
  const trArr = Array.isArray(bundle?.translations) ? bundle.translations : Array.isArray(sys?.translations) ? sys.translations : [];
@@ -8305,6 +8476,40 @@ function resolveCloudUrl(explicit) {
8305
8476
 
8306
8477
  // src/cloud/marketplace-proxy-plugin.ts
8307
8478
  var MARKETPLACE_PREFIX = "/api/v1/marketplace";
8479
+ var DEFAULT_LRU_MAX = 200;
8480
+ var LIST_TTL_MS = 30 * 60 * 1e3;
8481
+ var PACKAGE_TTL_MS = 2 * 60 * 60 * 1e3;
8482
+ var VERSION_TTL_MS = 24 * 60 * 60 * 1e3;
8483
+ function ttlForPath(pathname) {
8484
+ if (/\/packages\/[^/]+\/versions\//.test(pathname)) return VERSION_TTL_MS;
8485
+ if (/\/packages\/[^/]+/.test(pathname)) return PACKAGE_TTL_MS;
8486
+ return LIST_TTL_MS;
8487
+ }
8488
+ var LruTtlCache = class {
8489
+ constructor(max) {
8490
+ this.max = max;
8491
+ this.map = /* @__PURE__ */ new Map();
8492
+ }
8493
+ get(key) {
8494
+ const entry = this.map.get(key);
8495
+ if (!entry) return void 0;
8496
+ this.map.delete(key);
8497
+ this.map.set(key, entry);
8498
+ return entry;
8499
+ }
8500
+ set(key, entry) {
8501
+ if (this.map.has(key)) this.map.delete(key);
8502
+ this.map.set(key, entry);
8503
+ while (this.map.size > this.max) {
8504
+ const oldest = this.map.keys().next().value;
8505
+ if (oldest === void 0) break;
8506
+ this.map.delete(oldest);
8507
+ }
8508
+ }
8509
+ clear() {
8510
+ this.map.clear();
8511
+ }
8512
+ };
8308
8513
  var MarketplaceProxyPlugin = class _MarketplaceProxyPlugin {
8309
8514
  constructor(config = {}) {
8310
8515
  this.name = "com.objectstack.runtime.marketplace-proxy";
@@ -8326,6 +8531,7 @@ var MarketplaceProxyPlugin = class _MarketplaceProxyPlugin {
8326
8531
  }
8327
8532
  const rawApp = httpServer.getRawApp();
8328
8533
  const cloudUrl = this.cloudUrl;
8534
+ const cache = this.cache;
8329
8535
  const handler = async (c, next) => {
8330
8536
  if (!cloudUrl) {
8331
8537
  return c.json({
@@ -8352,24 +8558,51 @@ var MarketplaceProxyPlugin = class _MarketplaceProxyPlugin {
8352
8558
  }
8353
8559
  }, 405);
8354
8560
  }
8355
- const resp = await fetch(target, {
8356
- method,
8357
- headers: {
8358
- // Strip the inbound Host header fetch will set
8359
- // it to the cloud host. Forward only the
8360
- // identifying headers cloud might log.
8361
- "Accept": c.req.header("accept") ?? "application/json",
8362
- "User-Agent": `objectos-marketplace-proxy/${_MarketplaceProxyPlugin.prototype.version ?? "1.0.0"}`
8561
+ const accept = c.req.header("accept") ?? "application/json";
8562
+ const acceptLang = c.req.header("accept-language") ?? "";
8563
+ const cacheKey = `${incomingUrl.pathname}${incomingUrl.search}|al=${acceptLang}|a=${accept}`;
8564
+ const reqCacheCtl = (c.req.header("cache-control") ?? "").toLowerCase();
8565
+ const bypass = !cache || reqCacheCtl.includes("no-cache") || reqCacheCtl.includes("no-store");
8566
+ const now = Date.now();
8567
+ if (cache && !bypass) {
8568
+ const hit = cache.get(cacheKey);
8569
+ if (hit && hit.expiresAt > now) {
8570
+ return buildCachedResponse(hit, method, "HIT");
8363
8571
  }
8364
- });
8365
- const headers = new Headers();
8366
- const passthroughHeaders = ["content-type", "cache-control", "etag", "last-modified"];
8367
- for (const h of passthroughHeaders) {
8368
- const v = resp.headers.get(h);
8369
- if (v) headers.set(h, v);
8572
+ if (hit) {
8573
+ const revalHeaders = {
8574
+ "Accept": accept,
8575
+ "User-Agent": `objectos-marketplace-proxy/${_MarketplaceProxyPlugin.prototype.version ?? "1.0.0"}`
8576
+ };
8577
+ if (acceptLang) revalHeaders["Accept-Language"] = acceptLang;
8578
+ if (hit.etag) revalHeaders["If-None-Match"] = hit.etag;
8579
+ if (hit.lastModified) revalHeaders["If-Modified-Since"] = hit.lastModified;
8580
+ const revalResp = await fetch(target, { method: "GET", headers: revalHeaders });
8581
+ if (revalResp.status === 304) {
8582
+ hit.expiresAt = now + hit.ttlMs;
8583
+ const newEtag = revalResp.headers.get("etag");
8584
+ const newLm = revalResp.headers.get("last-modified");
8585
+ if (newEtag) hit.etag = newEtag;
8586
+ if (newLm) hit.lastModified = newLm;
8587
+ cache.set(cacheKey, hit);
8588
+ return buildCachedResponse(hit, method, "REVALIDATED");
8589
+ }
8590
+ return await consumeAndMaybeCache(revalResp, cacheKey, incomingUrl.pathname, method, cache);
8591
+ }
8592
+ }
8593
+ const reqHeaders = {
8594
+ // Strip the inbound Host header — fetch will set
8595
+ // it to the cloud host. Forward only the
8596
+ // identifying headers cloud might log.
8597
+ "Accept": accept,
8598
+ "User-Agent": `objectos-marketplace-proxy/${_MarketplaceProxyPlugin.prototype.version ?? "1.0.0"}`
8599
+ };
8600
+ if (acceptLang) reqHeaders["Accept-Language"] = acceptLang;
8601
+ const resp = await fetch(target, { method: "GET", headers: reqHeaders });
8602
+ if (bypass || !cache) {
8603
+ return await passthroughResponse(resp, method, bypass ? "BYPASS" : "MISS");
8370
8604
  }
8371
- const body = await resp.arrayBuffer();
8372
- return new Response(body, { status: resp.status, headers });
8605
+ return await consumeAndMaybeCache(resp, cacheKey, incomingUrl.pathname, method, cache);
8373
8606
  } catch (err) {
8374
8607
  const errObj = err instanceof Error ? err : new Error(err?.message ?? String(err));
8375
8608
  ctx.logger?.error?.("[MarketplaceProxyPlugin] proxy failed", errObj);
@@ -8392,12 +8625,67 @@ var MarketplaceProxyPlugin = class _MarketplaceProxyPlugin {
8392
8625
  }
8393
8626
  }
8394
8627
  }
8395
- ctx.logger?.info?.(`[MarketplaceProxyPlugin] mounted at ${MARKETPLACE_PREFIX}/* \u2192 ${cloudUrl || "(unconfigured)"}`);
8628
+ ctx.logger?.info?.(`[MarketplaceProxyPlugin] mounted at ${MARKETPLACE_PREFIX}/* \u2192 ${cloudUrl || "(unconfigured)"} (cache=${this.cache ? "on" : "off"})`);
8396
8629
  });
8397
8630
  };
8398
8631
  this.cloudUrl = resolveCloudUrl(config.controlPlaneUrl);
8632
+ const envFlag = (process.env.OS_MARKETPLACE_CACHE ?? "").trim().toLowerCase();
8633
+ const envDisabled = ["off", "false", "0", "no", "disable", "disabled"].includes(envFlag);
8634
+ const disabled = config.cacheDisabled ?? envDisabled;
8635
+ this.cache = disabled ? null : new LruTtlCache(Math.max(8, config.cacheMaxEntries ?? DEFAULT_LRU_MAX));
8399
8636
  }
8400
8637
  };
8638
+ var PASSTHROUGH_HEADERS = ["content-type", "cache-control", "etag", "last-modified", "vary"];
8639
+ function collectHeaders(src) {
8640
+ const out = {};
8641
+ for (const h of PASSTHROUGH_HEADERS) {
8642
+ const v = src.headers.get(h);
8643
+ if (v) out[h] = v;
8644
+ }
8645
+ return out;
8646
+ }
8647
+ function buildCachedResponse(entry, method, xCache) {
8648
+ const headers = new Headers(entry.headers);
8649
+ headers.set("X-Cache", xCache);
8650
+ const ageSec = Math.max(0, Math.floor((entry.expiresAt - entry.ttlMs - Date.now()) / -1e3));
8651
+ headers.set("Age", String(Math.max(0, ageSec)));
8652
+ const body = method === "HEAD" ? null : entry.body;
8653
+ return new Response(body, { status: entry.status, headers });
8654
+ }
8655
+ async function passthroughResponse(resp, method, xCache) {
8656
+ const headers = new Headers(collectHeaders(resp));
8657
+ headers.set("X-Cache", xCache);
8658
+ if (method === "HEAD") {
8659
+ try {
8660
+ await resp.arrayBuffer();
8661
+ } catch {
8662
+ }
8663
+ return new Response(null, { status: resp.status, headers });
8664
+ }
8665
+ const body = await resp.arrayBuffer();
8666
+ return new Response(body, { status: resp.status, headers });
8667
+ }
8668
+ async function consumeAndMaybeCache(resp, key, pathname, method, cache) {
8669
+ const body = await resp.arrayBuffer();
8670
+ const headers = collectHeaders(resp);
8671
+ if (resp.status >= 200 && resp.status < 300) {
8672
+ const ttlMs = ttlForPath(pathname);
8673
+ const entry = {
8674
+ status: resp.status,
8675
+ body,
8676
+ headers,
8677
+ etag: resp.headers.get("etag") ?? void 0,
8678
+ lastModified: resp.headers.get("last-modified") ?? void 0,
8679
+ expiresAt: Date.now() + ttlMs,
8680
+ ttlMs
8681
+ };
8682
+ cache.set(key, entry);
8683
+ }
8684
+ const respHeaders = new Headers(headers);
8685
+ respHeaders.set("X-Cache", "MISS");
8686
+ const outBody = method === "HEAD" ? null : body;
8687
+ return new Response(outBody, { status: resp.status, headers: respHeaders });
8688
+ }
8401
8689
 
8402
8690
  // src/cloud/runtime-config-plugin.ts
8403
8691
  var RuntimeConfigPlugin = class {
@@ -8435,12 +8723,14 @@ var RuntimeConfigPlugin = class {
8435
8723
  let defaultEnvironmentId;
8436
8724
  let defaultOrgId;
8437
8725
  let resolvedSingleEnv = this.singleEnvironment;
8438
- if (envRegistry && host && typeof envRegistry.resolveHostname === "function") {
8726
+ const resolveFn = typeof envRegistry?.resolveByHostname === "function" ? envRegistry.resolveByHostname.bind(envRegistry) : typeof envRegistry?.resolveHostname === "function" ? envRegistry.resolveHostname.bind(envRegistry) : null;
8727
+ if (resolveFn && host) {
8439
8728
  try {
8440
- const resolved = await envRegistry.resolveHostname(host);
8729
+ const resolved = await resolveFn(host);
8441
8730
  if (resolved?.environmentId) {
8442
- defaultEnvironmentId = resolved.environmentId;
8443
- if (resolved.organizationId) defaultOrgId = String(resolved.organizationId);
8731
+ defaultEnvironmentId = String(resolved.environmentId);
8732
+ const orgId = resolved.organizationId ?? resolved.organization_id;
8733
+ if (orgId) defaultOrgId = String(orgId);
8444
8734
  resolvedSingleEnv = true;
8445
8735
  }
8446
8736
  } catch {
@@ -8451,7 +8741,11 @@ var RuntimeConfigPlugin = class {
8451
8741
  singleEnvironment: resolvedSingleEnv,
8452
8742
  defaultOrgId,
8453
8743
  defaultEnvironmentId,
8454
- features
8744
+ features,
8745
+ branding: {
8746
+ productName: this.productName,
8747
+ productShortName: this.productShortName
8748
+ }
8455
8749
  });
8456
8750
  };
8457
8751
  rawApp.get("/api/v1/runtime/config", handler);
@@ -8468,6 +8762,10 @@ var RuntimeConfigPlugin = class {
8468
8762
  this.cloudUrl = config.controlPlaneUrl === "" ? "" : resolveCloudUrl(config.controlPlaneUrl) ?? "";
8469
8763
  this.installLocal = !!config.installLocal;
8470
8764
  this.singleEnvironment = !!config.singleEnvironment;
8765
+ const envName = (typeof process !== "undefined" ? process.env?.OS_PRODUCT_NAME : void 0)?.trim();
8766
+ const envShort = (typeof process !== "undefined" ? process.env?.OS_PRODUCT_SHORT_NAME : void 0)?.trim();
8767
+ this.productName = (config.productName ?? envName ?? "ObjectOS").trim() || "ObjectOS";
8768
+ this.productShortName = (config.productShortName ?? envShort ?? this.productName).trim() || this.productName;
8471
8769
  }
8472
8770
  };
8473
8771
 
@@ -8757,9 +9055,15 @@ var MarketplaceInstallLocalPlugin = class {
8757
9055
  const postHandler = async (c) => this.handleInstall(c, ctx);
8758
9056
  const getHandler = async (c) => this.handleList(c);
8759
9057
  const deleteHandler = async (c) => this.handleUninstall(c, ctx);
9058
+ const reseedHandler = async (c) => this.handleReseed(c, ctx);
9059
+ const purgeHandler = async (c) => this.handlePurge(c, ctx);
8760
9060
  if (typeof rawApp.post === "function") rawApp.post(ROUTE_BASE, postHandler);
8761
9061
  if (typeof rawApp.get === "function") rawApp.get(ROUTE_BASE, getHandler);
8762
9062
  if (typeof rawApp.delete === "function") rawApp.delete(`${ROUTE_BASE}/:manifestId`, deleteHandler);
9063
+ if (typeof rawApp.post === "function") {
9064
+ rawApp.post(`${ROUTE_BASE}/:manifestId/reseed-sample-data`, reseedHandler);
9065
+ rawApp.post(`${ROUTE_BASE}/:manifestId/purge-sample-data`, purgeHandler);
9066
+ }
8763
9067
  ctx.logger?.info?.(`[MarketplaceInstallLocal] mounted at ${ROUTE_BASE} (storage: ${this.storageDir})`);
8764
9068
  });
8765
9069
  };
@@ -8856,7 +9160,8 @@ var MarketplaceInstallLocalPlugin = class {
8856
9160
  version,
8857
9161
  manifest,
8858
9162
  installedAt: (/* @__PURE__ */ new Date()).toISOString(),
8859
- installedBy: userId
9163
+ installedBy: userId,
9164
+ withSampleData: false
8860
9165
  };
8861
9166
  try {
8862
9167
  (0, import_node_fs3.mkdirSync)(this.storageDir, { recursive: true });
@@ -8883,6 +9188,13 @@ var MarketplaceInstallLocalPlugin = class {
8883
9188
  ctx.logger?.warn?.(`[MarketplaceInstallLocal] syncSchemas failed for ${manifestId}: ${err?.message ?? err}`);
8884
9189
  }
8885
9190
  const seededSummary = await this.applySideEffects(ctx, manifest, { seedNow: true, c });
9191
+ if (seededSummary.seeded.mode === "inline" && (seededSummary.seeded.inserted ?? 0) + (seededSummary.seeded.updated ?? 0) > 0) {
9192
+ entry.withSampleData = true;
9193
+ try {
9194
+ (0, import_node_fs3.writeFileSync)((0, import_node_path6.join)(this.storageDir, safeFilename(manifestId)), JSON.stringify(entry, null, 2), "utf8");
9195
+ } catch {
9196
+ }
9197
+ }
8886
9198
  return c.json({
8887
9199
  success: true,
8888
9200
  data: {
@@ -8909,7 +9221,8 @@ var MarketplaceInstallLocalPlugin = class {
8909
9221
  manifestId: e.manifestId,
8910
9222
  version: e.version,
8911
9223
  installedAt: e.installedAt,
8912
- installedBy: e.installedBy
9224
+ installedBy: e.installedBy,
9225
+ withSampleData: e.withSampleData ?? false
8913
9226
  })),
8914
9227
  total: entries.length,
8915
9228
  storageDir: this.storageDir
@@ -8974,6 +9287,145 @@ var MarketplaceInstallLocalPlugin = class {
8974
9287
  * dev / single-tenant runtimes. Stricter checks can be layered on
8975
9288
  * via a middleware in cloud-hosted multi-tenant deployments.
8976
9289
  */
9290
+ /**
9291
+ * POST /api/v1/marketplace/install-local/:manifestId/reseed-sample-data
9292
+ *
9293
+ * Re-runs SeedLoaderService against the cached manifest's `data` arrays.
9294
+ * Idempotent (upsert by id). Useful when:
9295
+ * • The user installed an app and skipped sample data
9296
+ * • A purge was undone
9297
+ * • The user wants a clean baseline back after editing demo rows
9298
+ *
9299
+ * Multi-tenant: requires an active organization on the session (same
9300
+ * rule as install seed path).
9301
+ */
9302
+ this.handleReseed = async (c, ctx) => {
9303
+ const userId = await this.requireAuthenticatedUser(c, ctx);
9304
+ if (!userId) {
9305
+ return c.json({ success: false, error: { code: "unauthorized", message: "Authentication required." } }, 401);
9306
+ }
9307
+ const manifestId = String(c.req.param?.("manifestId") ?? c.req.params?.manifestId ?? "").trim();
9308
+ if (!manifestId) {
9309
+ return c.json({ success: false, error: { code: "bad_request", message: "manifestId path param required." } }, 400);
9310
+ }
9311
+ const file = (0, import_node_path6.join)(this.storageDir, safeFilename(manifestId));
9312
+ if (!(0, import_node_fs3.existsSync)(file)) {
9313
+ return c.json({ success: false, error: { code: "not_found", message: `No marketplace install for ${manifestId}.` } }, 404);
9314
+ }
9315
+ let entry;
9316
+ try {
9317
+ entry = JSON.parse((0, import_node_fs3.readFileSync)(file, "utf8"));
9318
+ } catch (err) {
9319
+ return c.json({ success: false, error: { code: "storage_failed", message: `Failed to read manifest cache: ${err?.message ?? err}` } }, 500);
9320
+ }
9321
+ const summary = await this.applySideEffects(ctx, entry.manifest, { seedNow: true, c });
9322
+ if (summary.seeded.mode === "skipped") {
9323
+ return c.json({
9324
+ success: false,
9325
+ error: {
9326
+ code: "reseed_skipped",
9327
+ message: `Reseed did not run: ${summary.seeded.reason ?? "unknown reason"}`
9328
+ }
9329
+ }, 400);
9330
+ }
9331
+ try {
9332
+ entry.withSampleData = true;
9333
+ (0, import_node_fs3.writeFileSync)(file, JSON.stringify(entry, null, 2), "utf8");
9334
+ } catch {
9335
+ }
9336
+ return c.json({
9337
+ success: true,
9338
+ data: {
9339
+ manifestId,
9340
+ inserted: summary.seeded.inserted ?? 0,
9341
+ updated: summary.seeded.updated ?? 0,
9342
+ errors: summary.seeded.errors ?? 0,
9343
+ withSampleData: true
9344
+ }
9345
+ }, 200);
9346
+ };
9347
+ /**
9348
+ * POST /api/v1/marketplace/install-local/:manifestId/purge-sample-data
9349
+ *
9350
+ * Deletes every record whose id is declared in the cached manifest's
9351
+ * seed datasets. Uses the `driver` service directly to bypass ACL /
9352
+ * lifecycle hooks (same pattern as cloud purge). User-created records
9353
+ * are never touched — only ids declared in the package's bundled
9354
+ * datasets are removed. Already-deleted rows count as `skipped`.
9355
+ */
9356
+ this.handlePurge = async (c, ctx) => {
9357
+ const userId = await this.requireAuthenticatedUser(c, ctx);
9358
+ if (!userId) {
9359
+ return c.json({ success: false, error: { code: "unauthorized", message: "Authentication required." } }, 401);
9360
+ }
9361
+ const manifestId = String(c.req.param?.("manifestId") ?? c.req.params?.manifestId ?? "").trim();
9362
+ if (!manifestId) {
9363
+ return c.json({ success: false, error: { code: "bad_request", message: "manifestId path param required." } }, 400);
9364
+ }
9365
+ const file = (0, import_node_path6.join)(this.storageDir, safeFilename(manifestId));
9366
+ if (!(0, import_node_fs3.existsSync)(file)) {
9367
+ return c.json({ success: false, error: { code: "not_found", message: `No marketplace install for ${manifestId}.` } }, 404);
9368
+ }
9369
+ let entry;
9370
+ try {
9371
+ entry = JSON.parse((0, import_node_fs3.readFileSync)(file, "utf8"));
9372
+ } catch (err) {
9373
+ return c.json({ success: false, error: { code: "storage_failed", message: `Failed to read manifest cache: ${err?.message ?? err}` } }, 500);
9374
+ }
9375
+ const datasets = Array.isArray(entry.manifest?.data) ? entry.manifest.data.filter((d) => d && d.object && Array.isArray(d.records)) : [];
9376
+ if (datasets.length === 0) {
9377
+ return c.json({
9378
+ success: false,
9379
+ error: { code: "nothing_to_purge", message: "This package declares no seed datasets." }
9380
+ }, 400);
9381
+ }
9382
+ let driver;
9383
+ try {
9384
+ driver = ctx.getService("driver");
9385
+ } catch {
9386
+ }
9387
+ if (!driver || typeof driver.delete !== "function") {
9388
+ return c.json({
9389
+ success: false,
9390
+ error: { code: "driver_missing", message: "driver service unavailable \u2014 cannot purge." }
9391
+ }, 500);
9392
+ }
9393
+ let deleted = 0;
9394
+ let skipped = 0;
9395
+ let errors = 0;
9396
+ for (const ds of datasets) {
9397
+ const object = String(ds.object);
9398
+ for (const rec of ds.records) {
9399
+ const id = rec?.id;
9400
+ if (id === void 0 || id === null || id === "") {
9401
+ skipped++;
9402
+ continue;
9403
+ }
9404
+ try {
9405
+ const r = await driver.delete(object, id);
9406
+ if (r === false || r === 0 || r?.deleted === 0) skipped++;
9407
+ else deleted++;
9408
+ } catch (err) {
9409
+ const msg = String(err?.message ?? err);
9410
+ if (/not.?found|no row/i.test(msg)) skipped++;
9411
+ else {
9412
+ errors++;
9413
+ ctx.logger?.warn?.(`[MarketplaceInstallLocal] purge ${object}#${id}: ${msg}`);
9414
+ }
9415
+ }
9416
+ }
9417
+ }
9418
+ try {
9419
+ entry.withSampleData = false;
9420
+ (0, import_node_fs3.writeFileSync)(file, JSON.stringify(entry, null, 2), "utf8");
9421
+ } catch {
9422
+ }
9423
+ ctx.logger?.info?.(`[MarketplaceInstallLocal] purged ${manifestId}: deleted=${deleted} skipped=${skipped} errors=${errors}`);
9424
+ return c.json({
9425
+ success: true,
9426
+ data: { manifestId, deleted, skipped, errors, withSampleData: false }
9427
+ }, 200);
9428
+ };
8977
9429
  /**
8978
9430
  * Replicate the start-time side-effects that AppPlugin runs for
8979
9431
  * statically-declared apps but the `manifest` service does NOT:
@@ -9063,7 +9515,7 @@ var MarketplaceInstallLocalPlugin = class {
9063
9515
  }
9064
9516
  }
9065
9517
  if (opts.seedNow && datasets.length > 0) {
9066
- const multiTenant = String(process.env.OS_MULTI_TENANT ?? "true").toLowerCase() !== "false";
9518
+ const multiTenant = String(process.env.OS_MULTI_TENANT ?? "false").toLowerCase() !== "false";
9067
9519
  try {
9068
9520
  const ql = ctx.getService("objectql");
9069
9521
  let metadata;