@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.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;
@@ -1445,6 +1459,66 @@ var init_app_plugin = __esm({
1445
1459
  } catch (err) {
1446
1460
  ctx.logger.error("[AppPlugin] Failed to schedule approval-process registration", err, { appId });
1447
1461
  }
1462
+ try {
1463
+ const jobs = Array.isArray(this.bundle.jobs) ? this.bundle.jobs : Array.isArray((this.bundle.manifest || {}).jobs) ? this.bundle.manifest.jobs : [];
1464
+ if (jobs.length > 0) {
1465
+ ctx.hook("kernel:ready", async () => {
1466
+ let svc;
1467
+ try {
1468
+ svc = ctx.getService("job");
1469
+ } catch {
1470
+ }
1471
+ if (!svc || typeof svc.schedule !== "function") {
1472
+ ctx.logger.warn("[AppPlugin] job service not registered \u2014 skipping declarative jobs", {
1473
+ appId,
1474
+ jobCount: jobs.length
1475
+ });
1476
+ return;
1477
+ }
1478
+ const fnMap = collectBundleFunctions(this.bundle);
1479
+ let ok = 0;
1480
+ for (const job of jobs) {
1481
+ const jobName = job?.name;
1482
+ if (!jobName) {
1483
+ ctx.logger.warn("[AppPlugin] skipping job without name", { appId, job });
1484
+ continue;
1485
+ }
1486
+ if (job.enabled === false) {
1487
+ ctx.logger.debug("[AppPlugin] job disabled \u2014 skipping", { appId, job: jobName });
1488
+ continue;
1489
+ }
1490
+ const handler = fnMap[job.handler];
1491
+ if (typeof handler !== "function") {
1492
+ ctx.logger.warn("[AppPlugin] job handler not found in bundle.functions \u2014 skipping", {
1493
+ appId,
1494
+ job: jobName,
1495
+ handler: job.handler
1496
+ });
1497
+ continue;
1498
+ }
1499
+ try {
1500
+ await svc.schedule(
1501
+ jobName,
1502
+ job.schedule,
1503
+ async (jobCtx) => {
1504
+ await handler({ ...jobCtx, jobId: jobName, bundle: this.bundle });
1505
+ }
1506
+ );
1507
+ ok++;
1508
+ } catch (err) {
1509
+ ctx.logger.warn("[AppPlugin] Failed to schedule job", {
1510
+ appId,
1511
+ job: jobName,
1512
+ error: err?.message ?? String(err)
1513
+ });
1514
+ }
1515
+ }
1516
+ ctx.logger.info("[AppPlugin] Scheduled background jobs", { appId, count: ok });
1517
+ });
1518
+ }
1519
+ } catch (err) {
1520
+ ctx.logger.error("[AppPlugin] Failed to schedule background-job registration", err, { appId });
1521
+ }
1448
1522
  this.emitCatalogEvent(ctx, "app:registered", sys);
1449
1523
  await this.loadTranslations(ctx, appId);
1450
1524
  const seedDatasets = [];
@@ -1517,51 +1591,68 @@ var init_app_plugin = __esm({
1517
1591
  } catch (e) {
1518
1592
  ctx.logger.warn("[Seeder] Failed to register seed-datasets/seed-replayer service", { error: e?.message });
1519
1593
  }
1520
- const multiTenant = String(process.env.OS_MULTI_TENANT ?? "true").toLowerCase() !== "false";
1594
+ const multiTenant = String(process.env.OS_MULTI_TENANT ?? "false").toLowerCase() !== "false";
1521
1595
  if (multiTenant) {
1522
1596
  ctx.logger.info("[Seeder] multi-tenant mode \u2014 skipping inline seed; per-org replay will run on sys_organization insert");
1523
1597
  } else {
1524
- try {
1525
- const metadata = ctx.getService("metadata");
1526
- if (metadata) {
1527
- const seedLoader = new SeedLoaderService(ql, metadata, ctx.logger);
1528
- const { SeedLoaderRequestSchema } = await import("@objectstack/spec/data");
1529
- const request = SeedLoaderRequestSchema.parse({
1530
- datasets: normalizedDatasets,
1531
- config: { defaultMode: "upsert", multiPass: true }
1532
- });
1533
- const result = await seedLoader.load(request);
1534
- ctx.logger.info("[Seeder] Seed loading complete", {
1535
- inserted: result.summary.totalInserted,
1536
- updated: result.summary.totalUpdated,
1537
- errors: result.errors.length
1538
- });
1539
- } else {
1540
- 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 });
1541
1631
  for (const dataset of normalizedDatasets) {
1542
- ctx.logger.info(`[Seeder] Seeding ${dataset.records.length} records for ${dataset.object}`);
1543
1632
  for (const record of dataset.records) {
1544
1633
  try {
1545
1634
  await ql.insert(dataset.object, record, { context: { isSystem: true } });
1546
- } catch (err) {
1547
- 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 });
1548
1637
  }
1549
1638
  }
1550
1639
  }
1551
- ctx.logger.info("[Seeder] Data seeding complete.");
1640
+ ctx.logger.info("[Seeder] Data seeding complete (fallback).");
1552
1641
  }
1553
- } catch (err) {
1554
- ctx.logger.warn("[Seeder] SeedLoaderService failed, falling back to basic insert", { error: err.message });
1555
- for (const dataset of normalizedDatasets) {
1556
- for (const record of dataset.records) {
1557
- try {
1558
- await ql.insert(dataset.object, record, { context: { isSystem: true } });
1559
- } catch (insertErr) {
1560
- ctx.logger.warn(`[Seeder] Failed to insert ${dataset.object} record:`, { error: insertErr.message });
1561
- }
1562
- }
1563
- }
1564
- 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
+ });
1565
1656
  }
1566
1657
  }
1567
1658
  }
@@ -1572,10 +1663,54 @@ var init_app_plugin = __esm({
1572
1663
  };
1573
1664
  this.bundle = bundle;
1574
1665
  this.projectContext = projectContext;
1575
- const sys = bundle.manifest || bundle;
1576
- const appId = sys.id || sys.name || "unnamed-app";
1666
+ const sys = bundle?.manifest || bundle;
1667
+ const appId = sys?.id || sys?.name;
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
+ }
1701
+ const bundleKeys = bundle && typeof bundle === "object" ? Object.keys(bundle).slice(0, 20).join(",") : typeof bundle;
1702
+ const sysKeys = sys && typeof sys === "object" ? Object.keys(sys).slice(0, 20).join(",") : typeof sys;
1703
+ const ctxHint = projectContext ? ` projectContext=${JSON.stringify({
1704
+ environmentId: projectContext.environmentId,
1705
+ packageId: projectContext.packageId,
1706
+ source: projectContext.source
1707
+ })}` : "";
1708
+ throw new Error(
1709
+ `[AppPlugin] bundle has app payload but no manifest.id / manifest.name \u2014 cannot register as a plugin. bundleKeys=[${bundleKeys}] sysKeys=[${sysKeys}]${ctxHint}`
1710
+ );
1711
+ }
1577
1712
  this.name = `plugin.app.${appId}`;
1578
- this.version = sys.version;
1713
+ this.version = sys?.version;
1579
1714
  }
1580
1715
  /**
1581
1716
  * Emit a kernel hook so the control-plane `AppCatalogService` can
@@ -2120,7 +2255,7 @@ function resolveObjectStackHome() {
2120
2255
  var StandaloneStackConfigSchema = z.object({
2121
2256
  databaseUrl: z.string().optional(),
2122
2257
  databaseAuthToken: z.string().optional(),
2123
- databaseDriver: z.enum(["sqlite", "sqlite-wasm", "turso", "memory", "postgres", "mongodb"]).optional(),
2258
+ databaseDriver: z.enum(["sqlite", "sqlite-wasm", "memory", "postgres", "mongodb"]).optional(),
2124
2259
  environmentId: z.string().optional(),
2125
2260
  artifactPath: z.string().optional(),
2126
2261
  /**
@@ -2138,7 +2273,6 @@ var StandaloneStackConfigSchema = z.object({
2138
2273
  });
2139
2274
  function detectDriverFromUrl(dbUrl) {
2140
2275
  if (/^memory:\/\//i.test(dbUrl)) return "memory";
2141
- if (/^(libsql|https?):\/\//i.test(dbUrl)) return "turso";
2142
2276
  if (/^(postgres(ql)?|pg):\/\//i.test(dbUrl)) return "postgres";
2143
2277
  if (/^mongodb(\+srv)?:\/\//i.test(dbUrl)) return "mongodb";
2144
2278
  if (/^wasm-sqlite:\/\//i.test(dbUrl)) return "sqlite-wasm";
@@ -2146,7 +2280,7 @@ function detectDriverFromUrl(dbUrl) {
2146
2280
  if (/^file:/i.test(dbUrl)) return "sqlite";
2147
2281
  if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(dbUrl)) return "sqlite";
2148
2282
  throw new Error(
2149
- `[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:`
2150
2284
  );
2151
2285
  }
2152
2286
  async function createStandaloneStack(config) {
@@ -2160,25 +2294,12 @@ async function createStandaloneStack(config) {
2160
2294
  const artifactPathInput = cfg.artifactPath ?? process.env.OS_ARTIFACT_PATH ?? resolvePath2(cwd, "dist/objectstack.json");
2161
2295
  const artifactPath = isHttpUrl(artifactPathInput) ? artifactPathInput : artifactPathInput.startsWith("/") ? artifactPathInput : resolvePath2(cwd, artifactPathInput);
2162
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")}`);
2163
- const dbAuthToken = cfg.databaseAuthToken ?? process.env.OS_DATABASE_AUTH_TOKEN?.trim() ?? process.env.TURSO_AUTH_TOKEN?.trim();
2164
2297
  const explicitDriver = cfg.databaseDriver ?? process.env.OS_DATABASE_DRIVER?.trim();
2165
2298
  const dbDriver = explicitDriver ?? detectDriverFromUrl(dbUrl);
2166
2299
  let driverPlugin;
2167
2300
  if (dbDriver === "memory") {
2168
2301
  const { InMemoryDriver } = await import("@objectstack/driver-memory");
2169
2302
  driverPlugin = new DriverPlugin2(new InMemoryDriver());
2170
- } else if (dbDriver === "turso") {
2171
- let TursoDriver;
2172
- try {
2173
- ({ TursoDriver } = await import("@objectstack/driver-turso"));
2174
- } catch (err) {
2175
- throw new Error(
2176
- `[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})`
2177
- );
2178
- }
2179
- driverPlugin = new DriverPlugin2(
2180
- new TursoDriver({ url: dbUrl, authToken: dbAuthToken })
2181
- );
2182
2303
  } else if (dbDriver === "postgres") {
2183
2304
  const { SqlDriver } = await import("@objectstack/driver-sql");
2184
2305
  driverPlugin = new DriverPlugin2(
@@ -2437,8 +2558,12 @@ async function resolveExecutionContext(opts) {
2437
2558
  if (!userId) {
2438
2559
  try {
2439
2560
  const authService = await opts.getService("auth");
2561
+ let api = authService?.api;
2562
+ if (!api && typeof authService?.getApi === "function") {
2563
+ api = await authService.getApi();
2564
+ }
2440
2565
  const headersInstance = toHeaders(headers);
2441
- const sessionData = await authService?.api?.getSession?.({ headers: headersInstance });
2566
+ const sessionData = await api?.getSession?.({ headers: headersInstance });
2442
2567
  userId = sessionData?.user?.id ?? sessionData?.session?.userId;
2443
2568
  tenantId = tenantId ?? sessionData?.session?.activeOrganizationId;
2444
2569
  ctx.accessToken = sessionData?.session?.token ?? ctx.accessToken;
@@ -3915,7 +4040,7 @@ var _HttpDispatcher = class _HttpDispatcher {
3915
4040
  return {
3916
4041
  handled: true,
3917
4042
  response: this.error(
3918
- "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).",
3919
4044
  503
3920
4045
  )
3921
4046
  };
@@ -5283,7 +5408,7 @@ var _HttpDispatcher = class _HttpDispatcher {
5283
5408
  * Handle AI service routes (/ai/chat, /ai/models, /ai/conversations, etc.)
5284
5409
  * Resolves the AI service and its built-in route handlers, then dispatches.
5285
5410
  */
5286
- async handleAI(subPath, method, body, query, _context) {
5411
+ async handleAI(subPath, method, body, query, context) {
5287
5412
  let aiService;
5288
5413
  try {
5289
5414
  aiService = await this.resolveService("ai");
@@ -5327,7 +5452,23 @@ var _HttpDispatcher = class _HttpDispatcher {
5327
5452
  if (route.method !== method) continue;
5328
5453
  const params = matchRoute(route.path, fullPath);
5329
5454
  if (params === null) continue;
5330
- const result = await route.handler({ body, params, query });
5455
+ const ec = context.executionContext;
5456
+ const user = ec?.userId ? {
5457
+ userId: ec.userId,
5458
+ id: ec.userId,
5459
+ displayName: ec.userDisplayName ?? ec.userName ?? ec.userId,
5460
+ email: ec.userEmail,
5461
+ roles: Array.isArray(ec.roles) ? ec.roles : [],
5462
+ permissions: Array.isArray(ec.permissions) ? ec.permissions : [],
5463
+ organizationId: ec.tenantId
5464
+ } : void 0;
5465
+ const result = await route.handler({
5466
+ body,
5467
+ params,
5468
+ query,
5469
+ headers: context.request?.headers,
5470
+ user
5471
+ });
5331
5472
  if (result.stream && result.events) {
5332
5473
  return {
5333
5474
  handled: true,
@@ -5832,13 +5973,22 @@ function resolveErrorReporter(ctx, override) {
5832
5973
  }
5833
5974
 
5834
5975
  // src/dispatcher-plugin.ts
5835
- function mountRouteOnServer(route, server, routePath, securityHeaders) {
5976
+ function mountRouteOnServer(route, server, routePath, securityHeaders, resolveUser) {
5836
5977
  const handler = async (req, res) => {
5837
5978
  try {
5979
+ let user;
5980
+ if (resolveUser) {
5981
+ try {
5982
+ user = await resolveUser(req.headers ?? {});
5983
+ } catch {
5984
+ }
5985
+ }
5838
5986
  const result = await route.handler({
5839
5987
  body: req.body,
5840
5988
  params: req.params,
5841
- query: req.query
5989
+ query: req.query,
5990
+ headers: req.headers,
5991
+ user
5842
5992
  });
5843
5993
  if (result.stream && result.events) {
5844
5994
  res.status(result.status);
@@ -6575,6 +6725,32 @@ function createDispatcherPlugin(config = {}) {
6575
6725
  }
6576
6726
  }
6577
6727
  ctx.logger.info("Dispatcher bridge routes registered", { prefix, enableProjectScoping, projectResolution });
6728
+ const resolveRequestUser = async (headers) => {
6729
+ try {
6730
+ const authService = ctx.getService("auth");
6731
+ if (!authService) return void 0;
6732
+ let api = authService.api;
6733
+ if (!api && typeof authService.getApi === "function") {
6734
+ api = await authService.getApi();
6735
+ }
6736
+ if (!api?.getSession) return void 0;
6737
+ const headersInstance = headers instanceof Headers ? headers : new Headers(headers);
6738
+ const sessionData = await api.getSession({ headers: headersInstance });
6739
+ const userId = sessionData?.user?.id ?? sessionData?.session?.userId;
6740
+ if (!userId) return void 0;
6741
+ return {
6742
+ userId,
6743
+ id: userId,
6744
+ displayName: sessionData?.user?.name ?? sessionData?.user?.email ?? userId,
6745
+ email: sessionData?.user?.email,
6746
+ roles: [],
6747
+ permissions: [],
6748
+ organizationId: sessionData?.session?.activeOrganizationId
6749
+ };
6750
+ } catch {
6751
+ return void 0;
6752
+ }
6753
+ };
6578
6754
  const toScopedPath = (routePath) => {
6579
6755
  if (routePath.startsWith(prefix)) {
6580
6756
  const tail = routePath.slice(prefix.length);
@@ -6587,11 +6763,11 @@ function createDispatcherPlugin(config = {}) {
6587
6763
  const routePath = route.path.startsWith("/api/v1") ? route.path : `${prefix}${route.path}`;
6588
6764
  let count = 0;
6589
6765
  if (enableProjectScoping && projectResolution === "required") {
6590
- if (mountRouteOnServer(route, server, toScopedPath(routePath), securityHeaders)) count++;
6766
+ if (mountRouteOnServer(route, server, toScopedPath(routePath), securityHeaders, resolveRequestUser)) count++;
6591
6767
  } else {
6592
- if (mountRouteOnServer(route, server, routePath, securityHeaders)) count++;
6768
+ if (mountRouteOnServer(route, server, routePath, securityHeaders, resolveRequestUser)) count++;
6593
6769
  if (enableProjectScoping) {
6594
- if (mountRouteOnServer(route, server, toScopedPath(routePath), securityHeaders)) count++;
6770
+ if (mountRouteOnServer(route, server, toScopedPath(routePath), securityHeaders, resolveRequestUser)) count++;
6595
6771
  }
6596
6772
  }
6597
6773
  return count;
@@ -7366,6 +7542,27 @@ function extractRuntimeFromMetadata(metadata) {
7366
7542
  }
7367
7543
  async function createDriver(driverType, databaseUrl, authToken) {
7368
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
+ }
7369
7566
  case "memory": {
7370
7567
  const { InMemoryDriver } = await import("@objectstack/driver-memory");
7371
7568
  const dbName = databaseUrl.replace(/^memory:\/\//, "").trim();
@@ -7384,18 +7581,6 @@ async function createDriver(driverType, databaseUrl, authToken) {
7384
7581
  useNullAsDefault: true
7385
7582
  });
7386
7583
  }
7387
- case "libsql":
7388
- case "turso": {
7389
- let TursoDriver;
7390
- try {
7391
- ({ TursoDriver } = await import("@objectstack/driver-turso"));
7392
- } catch (err) {
7393
- throw new Error(
7394
- `[ArtifactEnvironmentRegistry] libsql/turso driver requested but @objectstack/driver-turso is not installed. Install it with: npm install @objectstack/driver-turso. (${err?.message ?? err})`
7395
- );
7396
- }
7397
- return new TursoDriver({ url: databaseUrl, authToken });
7398
- }
7399
7584
  case "postgres":
7400
7585
  case "postgresql":
7401
7586
  case "pg": {
@@ -7653,39 +7838,7 @@ var ArtifactKernelFactory = class {
7653
7838
  // intentionally do NOT pass crossSubDomainCookies here
7654
7839
  // so cookies stay isolated per project subdomain.
7655
7840
  trustedOrigins: trustedOriginsList.length ? trustedOriginsList : void 0,
7656
- ...oidcProviders ? { oidcProviders } : {},
7657
- // Auto-provision a personal organization for every new
7658
- // user. SecurityPlugin's ObjectQL middleware does this
7659
- // for direct `ql.insert` calls, but better-auth's
7660
- // adapter writes through `dataEngine` directly,
7661
- // bypassing that middleware — so JIT-created SSO users
7662
- // would otherwise land on the empty "create
7663
- // organization" screen on first login.
7664
- databaseHooks: {
7665
- user: {
7666
- create: {
7667
- after: async (user) => {
7668
- try {
7669
- const ql = kernel.getService("objectql");
7670
- if (!ql) return;
7671
- const [{ ensureUserHasOrganization, cloneTenantSeedData }] = await Promise.all([
7672
- import("@objectstack/plugin-security")
7673
- ]);
7674
- await ensureUserHasOrganization(ql, user, {
7675
- logger: this.logger,
7676
- cloneSeedData: cloneTenantSeedData
7677
- });
7678
- } catch (e) {
7679
- this.logger.warn?.("[ArtifactKernelFactory] auto-org provisioning hook failed", {
7680
- environmentId,
7681
- userId: user?.id,
7682
- error: e?.message
7683
- });
7684
- }
7685
- }
7686
- }
7687
- }
7688
- }
7841
+ ...oidcProviders ? { oidcProviders } : {}
7689
7842
  }));
7690
7843
  if (oidcProviders) {
7691
7844
  this.logger.info?.("[ArtifactKernelFactory] platform SSO wired", {
@@ -7703,9 +7856,20 @@ var ArtifactKernelFactory = class {
7703
7856
  this.logger.warn?.("[ArtifactKernelFactory] OS_AUTH_SECRET not set \u2014 per-project AuthPlugin skipped (auth endpoints will return 404)", { environmentId });
7704
7857
  }
7705
7858
  try {
7859
+ const multiTenant = String(process.env.OS_MULTI_TENANT ?? "false").toLowerCase() !== "false";
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
+ }
7706
7871
  const { SecurityPlugin } = await import("@objectstack/plugin-security");
7707
- const multiTenant = String(process.env.OS_MULTI_TENANT ?? "true").toLowerCase() !== "false";
7708
- await kernel.use(new SecurityPlugin({ multiTenant }));
7872
+ await kernel.use(new SecurityPlugin());
7709
7873
  } catch (err) {
7710
7874
  this.logger.warn?.("[ArtifactKernelFactory] SecurityPlugin not registered", {
7711
7875
  environmentId,
@@ -7713,8 +7877,15 @@ var ArtifactKernelFactory = class {
7713
7877
  });
7714
7878
  }
7715
7879
  const projectName = project.hostname ?? environmentId;
7716
- const bundle = artifact.metadata;
7717
- const sys = bundle?.manifest ?? bundle;
7880
+ const artifactAny = artifact;
7881
+ const topLevelManifest = artifactAny?.manifest && typeof artifactAny.manifest === "object" ? artifactAny.manifest : null;
7882
+ const topLevelFunctions = Array.isArray(artifactAny?.functions) ? artifactAny.functions : [];
7883
+ const bundle = {
7884
+ ...artifact.metadata ?? {},
7885
+ ...topLevelManifest ? { manifest: topLevelManifest } : {},
7886
+ functions: topLevelFunctions
7887
+ };
7888
+ const sys = bundle.manifest ?? bundle;
7718
7889
  const packageId = sys?.packageId ?? sys?.package_id ?? bundle?.packageId;
7719
7890
  const i18nCfg = bundle?.i18n ?? sys?.i18n ?? {};
7720
7891
  const trArr = Array.isArray(bundle?.translations) ? bundle.translations : Array.isArray(sys?.translations) ? sys.translations : [];
@@ -8224,6 +8395,40 @@ function resolveCloudUrl(explicit) {
8224
8395
 
8225
8396
  // src/cloud/marketplace-proxy-plugin.ts
8226
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
+ };
8227
8432
  var MarketplaceProxyPlugin = class _MarketplaceProxyPlugin {
8228
8433
  constructor(config = {}) {
8229
8434
  this.name = "com.objectstack.runtime.marketplace-proxy";
@@ -8245,6 +8450,7 @@ var MarketplaceProxyPlugin = class _MarketplaceProxyPlugin {
8245
8450
  }
8246
8451
  const rawApp = httpServer.getRawApp();
8247
8452
  const cloudUrl = this.cloudUrl;
8453
+ const cache = this.cache;
8248
8454
  const handler = async (c, next) => {
8249
8455
  if (!cloudUrl) {
8250
8456
  return c.json({
@@ -8271,24 +8477,51 @@ var MarketplaceProxyPlugin = class _MarketplaceProxyPlugin {
8271
8477
  }
8272
8478
  }, 405);
8273
8479
  }
8274
- const resp = await fetch(target, {
8275
- method,
8276
- headers: {
8277
- // Strip the inbound Host header fetch will set
8278
- // it to the cloud host. Forward only the
8279
- // identifying headers cloud might log.
8280
- "Accept": c.req.header("accept") ?? "application/json",
8281
- "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");
8282
8490
  }
8283
- });
8284
- const headers = new Headers();
8285
- const passthroughHeaders = ["content-type", "cache-control", "etag", "last-modified"];
8286
- for (const h of passthroughHeaders) {
8287
- const v = resp.headers.get(h);
8288
- 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");
8289
8523
  }
8290
- const body = await resp.arrayBuffer();
8291
- return new Response(body, { status: resp.status, headers });
8524
+ return await consumeAndMaybeCache(resp, cacheKey, incomingUrl.pathname, method, cache);
8292
8525
  } catch (err) {
8293
8526
  const errObj = err instanceof Error ? err : new Error(err?.message ?? String(err));
8294
8527
  ctx.logger?.error?.("[MarketplaceProxyPlugin] proxy failed", errObj);
@@ -8311,12 +8544,67 @@ var MarketplaceProxyPlugin = class _MarketplaceProxyPlugin {
8311
8544
  }
8312
8545
  }
8313
8546
  }
8314
- 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"})`);
8315
8548
  });
8316
8549
  };
8317
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));
8318
8555
  }
8319
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
+ }
8320
8608
 
8321
8609
  // src/cloud/runtime-config-plugin.ts
8322
8610
  var RuntimeConfigPlugin = class {
@@ -8354,12 +8642,14 @@ var RuntimeConfigPlugin = class {
8354
8642
  let defaultEnvironmentId;
8355
8643
  let defaultOrgId;
8356
8644
  let resolvedSingleEnv = this.singleEnvironment;
8357
- 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) {
8358
8647
  try {
8359
- const resolved = await envRegistry.resolveHostname(host);
8648
+ const resolved = await resolveFn(host);
8360
8649
  if (resolved?.environmentId) {
8361
- defaultEnvironmentId = resolved.environmentId;
8362
- 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);
8363
8653
  resolvedSingleEnv = true;
8364
8654
  }
8365
8655
  } catch {
@@ -8370,7 +8660,11 @@ var RuntimeConfigPlugin = class {
8370
8660
  singleEnvironment: resolvedSingleEnv,
8371
8661
  defaultOrgId,
8372
8662
  defaultEnvironmentId,
8373
- features
8663
+ features,
8664
+ branding: {
8665
+ productName: this.productName,
8666
+ productShortName: this.productShortName
8667
+ }
8374
8668
  });
8375
8669
  };
8376
8670
  rawApp.get("/api/v1/runtime/config", handler);
@@ -8387,6 +8681,10 @@ var RuntimeConfigPlugin = class {
8387
8681
  this.cloudUrl = config.controlPlaneUrl === "" ? "" : resolveCloudUrl(config.controlPlaneUrl) ?? "";
8388
8682
  this.installLocal = !!config.installLocal;
8389
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;
8390
8688
  }
8391
8689
  };
8392
8690
 
@@ -8676,9 +8974,15 @@ var MarketplaceInstallLocalPlugin = class {
8676
8974
  const postHandler = async (c) => this.handleInstall(c, ctx);
8677
8975
  const getHandler = async (c) => this.handleList(c);
8678
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);
8679
8979
  if (typeof rawApp.post === "function") rawApp.post(ROUTE_BASE, postHandler);
8680
8980
  if (typeof rawApp.get === "function") rawApp.get(ROUTE_BASE, getHandler);
8681
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
+ }
8682
8986
  ctx.logger?.info?.(`[MarketplaceInstallLocal] mounted at ${ROUTE_BASE} (storage: ${this.storageDir})`);
8683
8987
  });
8684
8988
  };
@@ -8775,7 +9079,8 @@ var MarketplaceInstallLocalPlugin = class {
8775
9079
  version,
8776
9080
  manifest,
8777
9081
  installedAt: (/* @__PURE__ */ new Date()).toISOString(),
8778
- installedBy: userId
9082
+ installedBy: userId,
9083
+ withSampleData: false
8779
9084
  };
8780
9085
  try {
8781
9086
  mkdirSync3(this.storageDir, { recursive: true });
@@ -8802,6 +9107,13 @@ var MarketplaceInstallLocalPlugin = class {
8802
9107
  ctx.logger?.warn?.(`[MarketplaceInstallLocal] syncSchemas failed for ${manifestId}: ${err?.message ?? err}`);
8803
9108
  }
8804
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
+ }
8805
9117
  return c.json({
8806
9118
  success: true,
8807
9119
  data: {
@@ -8828,7 +9140,8 @@ var MarketplaceInstallLocalPlugin = class {
8828
9140
  manifestId: e.manifestId,
8829
9141
  version: e.version,
8830
9142
  installedAt: e.installedAt,
8831
- installedBy: e.installedBy
9143
+ installedBy: e.installedBy,
9144
+ withSampleData: e.withSampleData ?? false
8832
9145
  })),
8833
9146
  total: entries.length,
8834
9147
  storageDir: this.storageDir
@@ -8893,6 +9206,145 @@ var MarketplaceInstallLocalPlugin = class {
8893
9206
  * dev / single-tenant runtimes. Stricter checks can be layered on
8894
9207
  * via a middleware in cloud-hosted multi-tenant deployments.
8895
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
+ };
8896
9348
  /**
8897
9349
  * Replicate the start-time side-effects that AppPlugin runs for
8898
9350
  * statically-declared apps but the `manifest` service does NOT:
@@ -8982,7 +9434,7 @@ var MarketplaceInstallLocalPlugin = class {
8982
9434
  }
8983
9435
  }
8984
9436
  if (opts.seedNow && datasets.length > 0) {
8985
- const multiTenant = String(process.env.OS_MULTI_TENANT ?? "true").toLowerCase() !== "false";
9437
+ const multiTenant = String(process.env.OS_MULTI_TENANT ?? "false").toLowerCase() !== "false";
8986
9438
  try {
8987
9439
  const ql = ctx.getService("objectql");
8988
9440
  let metadata;