@objectstack/runtime 9.0.0 → 9.0.1

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
@@ -404,7 +404,7 @@ var init_quickjs_runner = __esm({
404
404
  evalRes.value.dispose();
405
405
  let pumps = 0;
406
406
  while (pumps < 1e3) {
407
- await new Promise((resolve2) => setImmediate(resolve2));
407
+ await new Promise((resolve) => setImmediate(resolve));
408
408
  const pending = runtime.executePendingJobs();
409
409
  if (pending.error) {
410
410
  const err = vm.dump(pending.error);
@@ -1184,8 +1184,8 @@ var init_app_plugin = __esm({
1184
1184
  }
1185
1185
  })();
1186
1186
  let timer;
1187
- const budget = new Promise((resolve2) => {
1188
- timer = setTimeout(() => resolve2("budget"), seedBudgetMs);
1187
+ const budget = new Promise((resolve) => {
1188
+ timer = setTimeout(() => resolve("budget"), seedBudgetMs);
1189
1189
  });
1190
1190
  const winner = await Promise.race([seedPromise.then(() => "done"), budget]);
1191
1191
  if (timer) clearTimeout(timer);
@@ -1604,7 +1604,6 @@ var init_standalone_stack = __esm({
1604
1604
  var index_exports = {};
1605
1605
  __export(index_exports, {
1606
1606
  AppPlugin: () => AppPlugin,
1607
- DEFAULT_CLOUD_URL: () => DEFAULT_CLOUD_URL,
1608
1607
  DEFAULT_RATE_LIMITS: () => DEFAULT_RATE_LIMITS,
1609
1608
  DriverPlugin: () => DriverPlugin,
1610
1609
  ExternalValidationPlugin: () => ExternalValidationPlugin,
@@ -1612,8 +1611,6 @@ __export(index_exports, {
1612
1611
  HttpServer: () => HttpServer,
1613
1612
  InMemoryErrorReporter: () => import_observability2.InMemoryErrorReporter,
1614
1613
  InMemoryMetricsRegistry: () => import_observability.InMemoryMetricsRegistry,
1615
- MarketplaceInstallLocalPlugin: () => MarketplaceInstallLocalPlugin,
1616
- MarketplaceProxyPlugin: () => MarketplaceProxyPlugin,
1617
1614
  MiddlewareManager: () => MiddlewareManager,
1618
1615
  NoopErrorReporter: () => import_observability2.NoopErrorReporter,
1619
1616
  NoopMetricsRegistry: () => import_observability.NoopMetricsRegistry,
@@ -1628,12 +1625,11 @@ __export(index_exports, {
1628
1625
  RouteGroupBuilder: () => import_rest.RouteGroupBuilder,
1629
1626
  RouteManager: () => import_rest.RouteManager,
1630
1627
  Runtime: () => Runtime,
1631
- RuntimeConfigPlugin: () => RuntimeConfigPlugin,
1632
1628
  SYSTEM_ENVIRONMENT_ID: () => SYSTEM_ENVIRONMENT_ID,
1633
1629
  SandboxError: () => SandboxError,
1634
1630
  SeedLoaderService: () => import_objectql.SeedLoaderService,
1635
1631
  UnimplementedScriptRunner: () => UnimplementedScriptRunner,
1636
- _resetEnvDeprecationWarnings: () => import_types4._resetEnvDeprecationWarnings,
1632
+ _resetEnvDeprecationWarnings: () => import_types3._resetEnvDeprecationWarnings,
1637
1633
  actionBodyRunnerFactory: () => actionBodyRunnerFactory,
1638
1634
  buildSecurityHeaders: () => buildSecurityHeaders,
1639
1635
  collectBundleActions: () => collectBundleActions,
@@ -1654,8 +1650,7 @@ __export(index_exports, {
1654
1650
  mergeRuntimeModule: () => mergeRuntimeModule,
1655
1651
  parseTraceparent: () => parseTraceparent,
1656
1652
  readArtifactSource: () => readArtifactSource,
1657
- readEnvWithDeprecation: () => import_types4.readEnvWithDeprecation,
1658
- resolveCloudUrl: () => resolveCloudUrl,
1653
+ readEnvWithDeprecation: () => import_types3.readEnvWithDeprecation,
1659
1654
  resolveDefaultArtifactPath: () => resolveDefaultArtifactPath,
1660
1655
  resolveErrorReporter: () => resolveErrorReporter,
1661
1656
  resolveMetrics: () => resolveMetrics,
@@ -2126,7 +2121,13 @@ function randomUUID() {
2126
2121
  });
2127
2122
  }
2128
2123
  var _HttpDispatcher = class _HttpDispatcher {
2129
- constructor(kernel, envRegistry, options) {
2124
+ /**
2125
+ * @param _envRegistryIgnored — RETIRED (ADR-0006 Phase 5). Environment
2126
+ * resolution moved behind the host's {@link KernelResolver}; the
2127
+ * positional parameter is kept so existing 3-arg callers keep compiling,
2128
+ * but its value is ignored.
2129
+ */
2130
+ constructor(kernel, _envRegistryIgnored, options) {
2130
2131
  /**
2131
2132
  * In-memory cache of positive membership checks, keyed by
2132
2133
  * `${environmentId}:${userId}`. Entries expire 60 seconds after insertion
@@ -2143,9 +2144,8 @@ var _HttpDispatcher = class _HttpDispatcher {
2143
2144
  return void 0;
2144
2145
  }
2145
2146
  };
2146
- this.envRegistry = envRegistry ?? resolveService("env-registry");
2147
2147
  this.enforceMembership = options?.enforceProjectMembership ?? true;
2148
- this.kernelManager = options?.kernelManager ?? resolveService("kernel-manager");
2148
+ this.kernelResolver = options?.kernelResolver ?? resolveService("kernel-resolver");
2149
2149
  this.scopeManager = options?.scopeManager ?? resolveService("scope-manager");
2150
2150
  }
2151
2151
  resolveDefaultProject() {
@@ -2533,130 +2533,20 @@ var _HttpDispatcher = class _HttpDispatcher {
2533
2533
  return candidate;
2534
2534
  }
2535
2535
  /**
2536
- * Resolve environment context for incoming request.
2537
- *
2538
- * Precedence:
2539
- * 0. URL path matches `/environments/:environmentId/...` OR request.params.environmentId set by router
2540
- * → envRegistry.resolveById(id)
2541
- * 1. request.headers.host → envRegistry.resolveByHostname(host)
2542
- * 2. request.headers['x-environment-id'] → envRegistry.resolveById(id)
2543
- * 3. session.activeEnvironmentId → envRegistry.resolveById(id)
2544
- * 4. session.activeOrganizationId → find default project → envRegistry.resolveById(id)
2545
- * 5. single-environment default (registered by `createSingleEnvironmentPlugin`)
2546
- * → envRegistry.resolveById(defaultProject.environmentId). Lets bare
2547
- * `/api/v1/data/...` URLs resolve to the lone project in
2548
- * `cloudUrl: 'local'` deployments.
2536
+ * Attach the dispatcher's parsing hints for the host's
2537
+ * {@link KernelResolver} (ADR-0006 Phase 5).
2549
2538
  *
2550
- * Skip for paths: /auth, /cloud, /health, /discovery (NOT /meta when scoped,
2551
- * so project-scoped meta routes can resolve their project).
2539
+ * Environment RESOLUTION (hostname / x-environment-id / session /
2540
+ * org-default / single-env-default environment + driver) is owned by
2541
+ * the host's resolver — the dispatcher no longer touches an environment
2542
+ * registry. What stays here is pure URL parsing (the dispatcher's own
2543
+ * routing convention): the scoped-path environment-id candidate and the
2544
+ * cleaned route path, both UNVALIDATED.
2552
2545
  */
2553
- async resolveEnvironmentContext(context, path) {
2554
- const skipPaths = ["/cloud", "/health", "/discovery"];
2555
- if (skipPaths.some((p) => path.startsWith(p))) {
2556
- return;
2557
- }
2558
- if (!this.envRegistry) {
2559
- return;
2560
- }
2561
- const headers = context.request?.headers;
2562
- const getHeader = (name) => {
2563
- if (!headers) return void 0;
2564
- const h = headers;
2565
- if (typeof h.get === "function") {
2566
- const v = h.get(name);
2567
- return v == null ? void 0 : String(v);
2568
- }
2569
- const lower = name.toLowerCase();
2570
- for (const k of Object.keys(h)) {
2571
- if (k.toLowerCase() === lower) {
2572
- const v = h[k];
2573
- return Array.isArray(v) ? v[0] : v == null ? void 0 : String(v);
2574
- }
2575
- }
2576
- return void 0;
2577
- };
2578
- try {
2579
- const urlEnvironmentId = this.extractEnvironmentIdFromPath(path) ?? context.request?.params?.environmentId;
2580
- if (urlEnvironmentId) {
2581
- const driver = await this.envRegistry.resolveById(urlEnvironmentId);
2582
- if (driver) {
2583
- context.environmentId = urlEnvironmentId;
2584
- context.dataDriver = driver;
2585
- return;
2586
- }
2587
- }
2588
- const host = getHeader("host");
2589
- if (host) {
2590
- const hostname = host.split(":")[0];
2591
- const result = await this.envRegistry.resolveByHostname(hostname);
2592
- if (result) {
2593
- context.environmentId = result.environmentId;
2594
- context.dataDriver = result.driver;
2595
- return;
2596
- }
2597
- }
2598
- const envIdHeader = getHeader("x-environment-id");
2599
- if (envIdHeader) {
2600
- const driver = await this.envRegistry.resolveById(envIdHeader);
2601
- if (driver) {
2602
- context.environmentId = envIdHeader;
2603
- context.dataDriver = driver;
2604
- return;
2605
- }
2606
- }
2607
- try {
2608
- const authService = await this.getService(import_system2.CoreServiceName.enum.auth);
2609
- const sessionData = await authService?.api?.getSession?.({
2610
- headers: context.request?.headers
2611
- });
2612
- const activeEnvironmentId = sessionData?.session?.activeEnvironmentId ?? sessionData?.session?.activeEnvironmentId;
2613
- if (activeEnvironmentId) {
2614
- const driver = await this.envRegistry.resolveById(activeEnvironmentId);
2615
- if (driver) {
2616
- context.environmentId = activeEnvironmentId;
2617
- context.dataDriver = driver;
2618
- return;
2619
- }
2620
- }
2621
- const activeOrganizationId = sessionData?.session?.activeOrganizationId;
2622
- if (activeOrganizationId) {
2623
- const qlService = await this.getObjectQLService();
2624
- const ql = qlService ?? await this.resolveService("objectql");
2625
- if (ql) {
2626
- let rows = await ql.find("sys_environment", {
2627
- where: {
2628
- organization_id: activeOrganizationId,
2629
- is_default: true
2630
- },
2631
- limit: 1
2632
- });
2633
- if (rows && rows.value) rows = rows.value;
2634
- if (Array.isArray(rows) && rows[0]) {
2635
- const defaultEnv = rows[0];
2636
- const driver = await this.envRegistry.resolveById(defaultEnv.id);
2637
- if (driver) {
2638
- context.environmentId = defaultEnv.id;
2639
- context.dataDriver = driver;
2640
- return;
2641
- }
2642
- }
2643
- }
2644
- }
2645
- } catch (sessionError) {
2646
- console.debug("[HttpDispatcher] Session resolution failed:", sessionError);
2647
- }
2648
- if (this.defaultProject?.environmentId || this.resolveDefaultProject()) {
2649
- const def = this.defaultProject;
2650
- const driver = await this.envRegistry.resolveById(def.environmentId);
2651
- if (driver) {
2652
- context.environmentId = def.environmentId;
2653
- context.dataDriver = driver;
2654
- return;
2655
- }
2656
- }
2657
- } catch (error) {
2658
- console.error("[HttpDispatcher] Environment resolution failed:", error);
2659
- }
2546
+ prepareResolverHints(context, path) {
2547
+ context.routePath = path;
2548
+ const urlEnvironmentId = this.extractEnvironmentIdFromPath(path) ?? context.request?.params?.environmentId;
2549
+ if (urlEnvironmentId) context.urlEnvironmentId = String(urlEnvironmentId);
2660
2550
  }
2661
2551
  /**
2662
2552
  * Check whether the authenticated user is a member of
@@ -3179,7 +3069,7 @@ var _HttpDispatcher = class _HttpDispatcher {
3179
3069
  if (!objectName) {
3180
3070
  return { handled: true, response: this.error("Object name required", 400) };
3181
3071
  }
3182
- if (!_context.dataDriver && this.envRegistry) {
3072
+ if (!_context.dataDriver && this.kernelResolver) {
3183
3073
  return {
3184
3074
  handled: true,
3185
3075
  response: this.error("Project not resolved. Please specify X-Environment-Id header or ensure hostname maps to a project.", 428)
@@ -4080,9 +3970,9 @@ var _HttpDispatcher = class _HttpDispatcher {
4080
3970
  if (def?.environmentId) _context.environmentId = def.environmentId;
4081
3971
  }
4082
3972
  let projectQl = null;
4083
- if (this.kernelManager && _context.environmentId && _context.environmentId !== "platform") {
3973
+ if (this.kernelResolver && _context.environmentId && _context.environmentId !== "platform") {
4084
3974
  try {
4085
- const projectKernel = await this.kernelManager.getOrCreate(_context.environmentId);
3975
+ const projectKernel = await this.kernelResolver.resolveKernel(_context, this.defaultKernel);
4086
3976
  if (projectKernel) {
4087
3977
  this.kernel = projectKernel;
4088
3978
  if (typeof projectKernel.getServiceAsync === "function") {
@@ -4255,9 +4145,9 @@ var _HttpDispatcher = class _HttpDispatcher {
4255
4145
  */
4256
4146
  async dispatch(method, path, body, query, context, prefix) {
4257
4147
  let cleanPath = path.replace(/\/$/, "");
4258
- await this.resolveEnvironmentContext(context, cleanPath);
4259
- if (this.kernelManager && context.environmentId && context.environmentId !== "platform") {
4260
- this.kernel = await this.kernelManager.getOrCreate(context.environmentId);
4148
+ this.prepareResolverHints(context, cleanPath);
4149
+ if (this.kernelResolver) {
4150
+ this.kernel = await this.kernelResolver.resolveKernel(context, this.defaultKernel) ?? this.defaultKernel;
4261
4151
  } else {
4262
4152
  this.kernel = this.defaultKernel;
4263
4153
  }
@@ -5683,1088 +5573,6 @@ var MiddlewareManager = class {
5683
5573
  // src/index.ts
5684
5574
  init_load_artifact_bundle();
5685
5575
 
5686
- // src/cloud/cloud-url.ts
5687
- var DEFAULT_CLOUD_URL = "https://cloud.objectos.ai";
5688
- function resolveCloudUrl(explicit) {
5689
- const raw = (explicit ?? process.env.OS_CLOUD_URL ?? "").trim();
5690
- const lower = raw.toLowerCase();
5691
- if (lower === "off" || lower === "none" || lower === "local" || lower === "disabled") {
5692
- return "";
5693
- }
5694
- const picked = raw || DEFAULT_CLOUD_URL;
5695
- return picked.replace(/\/+$/, "");
5696
- }
5697
-
5698
- // src/cloud/marketplace-public-url.ts
5699
- function resolveMarketplacePublicBaseUrl(explicit) {
5700
- const raw = (explicit ?? process.env.OS_MARKETPLACE_PUBLIC_BASE_URL ?? "").trim();
5701
- const lower = raw.toLowerCase();
5702
- if (!raw || lower === "off" || lower === "none" || lower === "disabled" || lower === "false") {
5703
- return "";
5704
- }
5705
- return raw.replace(/\/+$/, "");
5706
- }
5707
- function publicMarketplaceKeyForApiPath(pathname) {
5708
- const prefix = "/api/v1/marketplace/packages";
5709
- if (pathname === prefix) return "packages.json";
5710
- if (!pathname.startsWith(`${prefix}/`)) return null;
5711
- const tail = pathname.slice(prefix.length + 1);
5712
- if (!tail) return null;
5713
- const parts = tail.split("/");
5714
- if (parts.length === 1) {
5715
- const id = decodeURIComponent(parts[0] ?? "");
5716
- if (!id) return null;
5717
- return `packages/${encodeURIComponent(id)}.json`;
5718
- }
5719
- if (parts.length === 4 && parts[1] === "versions" && parts[3] === "manifest") {
5720
- const id = decodeURIComponent(parts[0] ?? "");
5721
- const versionId = decodeURIComponent(parts[2] ?? "");
5722
- if (!id || !versionId) return null;
5723
- return `packages/${encodeURIComponent(id)}/versions/${encodeURIComponent(versionId)}/manifest.json`;
5724
- }
5725
- return null;
5726
- }
5727
-
5728
- // src/cloud/marketplace-proxy-plugin.ts
5729
- var MARKETPLACE_PREFIX = "/api/v1/marketplace";
5730
- var DEFAULT_LRU_MAX = 200;
5731
- var LIST_TTL_MS = 30 * 60 * 1e3;
5732
- var PACKAGE_TTL_MS = 2 * 60 * 60 * 1e3;
5733
- var VERSION_TTL_MS = 24 * 60 * 60 * 1e3;
5734
- function ttlForPath(pathname) {
5735
- if (/\/packages\/[^/]+\/versions\//.test(pathname)) return VERSION_TTL_MS;
5736
- if (/\/packages\/[^/]+/.test(pathname)) return PACKAGE_TTL_MS;
5737
- return LIST_TTL_MS;
5738
- }
5739
- var LruTtlCache = class {
5740
- constructor(max) {
5741
- this.max = max;
5742
- this.map = /* @__PURE__ */ new Map();
5743
- }
5744
- get(key) {
5745
- const entry = this.map.get(key);
5746
- if (!entry) return void 0;
5747
- this.map.delete(key);
5748
- this.map.set(key, entry);
5749
- return entry;
5750
- }
5751
- set(key, entry) {
5752
- if (this.map.has(key)) this.map.delete(key);
5753
- this.map.set(key, entry);
5754
- while (this.map.size > this.max) {
5755
- const oldest = this.map.keys().next().value;
5756
- if (oldest === void 0) break;
5757
- this.map.delete(oldest);
5758
- }
5759
- }
5760
- clear() {
5761
- this.map.clear();
5762
- }
5763
- };
5764
- var MarketplaceProxyPlugin = class _MarketplaceProxyPlugin {
5765
- constructor(config = {}) {
5766
- this.name = "com.objectstack.runtime.marketplace-proxy";
5767
- this.version = "1.1.0";
5768
- this.init = async (_ctx) => {
5769
- };
5770
- this.start = async (ctx) => {
5771
- ctx.hook("kernel:ready", async () => {
5772
- let httpServer;
5773
- try {
5774
- httpServer = ctx.getService("http-server");
5775
- } catch {
5776
- ctx.logger?.warn?.("[MarketplaceProxyPlugin] http-server not available \u2014 marketplace routes not mounted");
5777
- return;
5778
- }
5779
- if (!httpServer || typeof httpServer.getRawApp !== "function") {
5780
- ctx.logger?.warn?.("[MarketplaceProxyPlugin] http-server missing getRawApp() \u2014 marketplace routes not mounted");
5781
- return;
5782
- }
5783
- const rawApp = httpServer.getRawApp();
5784
- const cloudUrl = this.cloudUrl;
5785
- const publicBaseUrl = this.publicBaseUrl;
5786
- const cache = this.cache;
5787
- if (publicBaseUrl) {
5788
- ctx.logger?.info?.(`[MarketplaceProxyPlugin] public R2 fast-path enabled \u2192 ${publicBaseUrl}`);
5789
- }
5790
- const handler = async (c, next) => {
5791
- if (!cloudUrl) {
5792
- return c.json({
5793
- success: false,
5794
- error: {
5795
- code: "marketplace_unavailable",
5796
- message: "No control-plane URL configured for this runtime (OS_CLOUD_URL)."
5797
- }
5798
- }, 503);
5799
- }
5800
- try {
5801
- const incomingUrl = new URL(c.req.url);
5802
- if (incomingUrl.pathname.startsWith(`${MARKETPLACE_PREFIX}/install-local`)) {
5803
- return next();
5804
- }
5805
- const method = String(c.req.method ?? "GET").toUpperCase();
5806
- if (publicBaseUrl && (method === "GET" || method === "HEAD")) {
5807
- const r2Resp = await tryPublicMarketplaceFetch(
5808
- publicBaseUrl,
5809
- incomingUrl,
5810
- method,
5811
- c.req.header("accept"),
5812
- ctx.logger
5813
- );
5814
- if (r2Resp) return r2Resp;
5815
- }
5816
- const target = `${cloudUrl}${incomingUrl.pathname}${incomingUrl.search}`;
5817
- if (method !== "GET" && method !== "HEAD") {
5818
- return next();
5819
- }
5820
- const accept = c.req.header("accept") ?? "application/json";
5821
- const acceptLang = c.req.header("accept-language") ?? "";
5822
- const cacheKey = `${incomingUrl.pathname}${incomingUrl.search}|al=${acceptLang}|a=${accept}`;
5823
- const reqCacheCtl = (c.req.header("cache-control") ?? "").toLowerCase();
5824
- const bypass = !cache || reqCacheCtl.includes("no-cache") || reqCacheCtl.includes("no-store");
5825
- const now = Date.now();
5826
- if (cache && !bypass) {
5827
- const hit = cache.get(cacheKey);
5828
- if (hit && hit.expiresAt > now) {
5829
- return buildCachedResponse(hit, method, "HIT");
5830
- }
5831
- if (hit) {
5832
- const revalHeaders = {
5833
- "Accept": accept,
5834
- "User-Agent": `objectos-marketplace-proxy/${_MarketplaceProxyPlugin.prototype.version ?? "1.0.0"}`
5835
- };
5836
- if (acceptLang) revalHeaders["Accept-Language"] = acceptLang;
5837
- if (hit.etag) revalHeaders["If-None-Match"] = hit.etag;
5838
- if (hit.lastModified) revalHeaders["If-Modified-Since"] = hit.lastModified;
5839
- const revalResp = await fetch(target, { method: "GET", headers: revalHeaders });
5840
- if (revalResp.status === 304) {
5841
- hit.expiresAt = now + hit.ttlMs;
5842
- const newEtag = revalResp.headers.get("etag");
5843
- const newLm = revalResp.headers.get("last-modified");
5844
- if (newEtag) hit.etag = newEtag;
5845
- if (newLm) hit.lastModified = newLm;
5846
- cache.set(cacheKey, hit);
5847
- return buildCachedResponse(hit, method, "REVALIDATED");
5848
- }
5849
- return await consumeAndMaybeCache(revalResp, cacheKey, incomingUrl.pathname, method, cache);
5850
- }
5851
- }
5852
- const reqHeaders = {
5853
- // Strip the inbound Host header — fetch will set
5854
- // it to the cloud host. Forward only the
5855
- // identifying headers cloud might log.
5856
- "Accept": accept,
5857
- "User-Agent": `objectos-marketplace-proxy/${_MarketplaceProxyPlugin.prototype.version ?? "1.0.0"}`
5858
- };
5859
- if (acceptLang) reqHeaders["Accept-Language"] = acceptLang;
5860
- const resp = await fetch(target, { method: "GET", headers: reqHeaders });
5861
- if (bypass || !cache) {
5862
- return await passthroughResponse(resp, method, bypass ? "BYPASS" : "MISS");
5863
- }
5864
- return await consumeAndMaybeCache(resp, cacheKey, incomingUrl.pathname, method, cache);
5865
- } catch (err) {
5866
- const errObj = err instanceof Error ? err : new Error(err?.message ?? String(err));
5867
- ctx.logger?.error?.("[MarketplaceProxyPlugin] proxy failed", errObj);
5868
- return c.json({
5869
- success: false,
5870
- error: {
5871
- code: "marketplace_proxy_failed",
5872
- message: err?.message ?? String(err)
5873
- }
5874
- }, 502);
5875
- }
5876
- };
5877
- if (typeof rawApp.all === "function") {
5878
- rawApp.all(`${MARKETPLACE_PREFIX}/*`, handler);
5879
- } else {
5880
- for (const m of ["get", "head"]) {
5881
- try {
5882
- rawApp[m]?.(`${MARKETPLACE_PREFIX}/*`, handler);
5883
- } catch {
5884
- }
5885
- }
5886
- }
5887
- ctx.logger?.info?.(`[MarketplaceProxyPlugin] mounted at ${MARKETPLACE_PREFIX}/* \u2192 ${cloudUrl || "(unconfigured)"} (cache=${this.cache ? "on" : "off"})`);
5888
- });
5889
- };
5890
- this.cloudUrl = resolveCloudUrl(config.controlPlaneUrl);
5891
- this.publicBaseUrl = resolveMarketplacePublicBaseUrl(config.publicMarketplaceBaseUrl);
5892
- const envFlag = (process.env.OS_MARKETPLACE_CACHE ?? "").trim().toLowerCase();
5893
- const envDisabled = ["off", "false", "0", "no", "disable", "disabled"].includes(envFlag);
5894
- const disabled = config.cacheDisabled ?? envDisabled;
5895
- this.cache = disabled ? null : new LruTtlCache(Math.max(8, config.cacheMaxEntries ?? DEFAULT_LRU_MAX));
5896
- }
5897
- };
5898
- async function tryPublicMarketplaceFetch(publicBaseUrl, incomingUrl, method, acceptHeader, logger) {
5899
- const key = publicMarketplaceKeyForApiPath(incomingUrl.pathname);
5900
- if (!key) return null;
5901
- const target = `${publicBaseUrl}/${key}`;
5902
- let resp;
5903
- try {
5904
- resp = await fetch(target, {
5905
- method: "GET",
5906
- headers: {
5907
- "Accept": acceptHeader || "application/json",
5908
- "User-Agent": `objectos-marketplace-proxy/public-r2`
5909
- }
5910
- });
5911
- } catch (err) {
5912
- logger?.warn?.(`[MarketplaceProxyPlugin] public R2 fetch failed (${target}): ${err?.message ?? err}`);
5913
- return null;
5914
- }
5915
- if (resp.status === 404) return null;
5916
- if (!resp.ok) {
5917
- logger?.warn?.(`[MarketplaceProxyPlugin] public R2 ${target} returned ${resp.status} \u2014 falling back to cloud`);
5918
- return null;
5919
- }
5920
- const isList = key === "packages.json";
5921
- const hasFilters = isList && (incomingUrl.searchParams.has("q") || incomingUrl.searchParams.has("category") || incomingUrl.searchParams.has("limit") || incomingUrl.searchParams.has("offset"));
5922
- if (!hasFilters) {
5923
- const headers2 = new Headers();
5924
- const ct = resp.headers.get("content-type") ?? "application/json; charset=utf-8";
5925
- headers2.set("content-type", ct);
5926
- const cc = resp.headers.get("cache-control");
5927
- if (cc) headers2.set("cache-control", cc);
5928
- const etag = resp.headers.get("etag");
5929
- if (etag) headers2.set("etag", etag);
5930
- headers2.set("x-cache", "PUBLIC-R2");
5931
- const body2 = method === "HEAD" ? null : resp.body;
5932
- return new Response(body2, { status: 200, headers: headers2 });
5933
- }
5934
- let snapshot;
5935
- try {
5936
- snapshot = await resp.json();
5937
- } catch (err) {
5938
- logger?.warn?.(`[MarketplaceProxyPlugin] public R2 list snapshot parse failed: ${err?.message ?? err}`);
5939
- return null;
5940
- }
5941
- const items = Array.isArray(snapshot?.data?.items) ? snapshot.data.items : [];
5942
- const q = (incomingUrl.searchParams.get("q") ?? "").trim().toLowerCase();
5943
- const category = (incomingUrl.searchParams.get("category") ?? "").trim();
5944
- const limit = Math.min(Math.max(Number(incomingUrl.searchParams.get("limit") ?? 50), 1), 100);
5945
- const offset = Math.max(Number(incomingUrl.searchParams.get("offset") ?? 0), 0);
5946
- let filtered = items;
5947
- if (q) {
5948
- filtered = filtered.filter((r) => {
5949
- const dn = String(r?.display_name ?? "").toLowerCase();
5950
- const mid = String(r?.manifest_id ?? "").toLowerCase();
5951
- return dn.includes(q) || mid.includes(q);
5952
- });
5953
- }
5954
- if (category) {
5955
- filtered = filtered.filter((r) => String(r?.category ?? "") === category);
5956
- }
5957
- const total = filtered.length;
5958
- const page = filtered.slice(offset, offset + limit);
5959
- const body = JSON.stringify({ success: true, data: { items: page, total, limit, offset } });
5960
- const headers = new Headers({
5961
- "content-type": "application/json; charset=utf-8",
5962
- "cache-control": "public, max-age=30",
5963
- "x-cache": "PUBLIC-R2-FILTERED"
5964
- });
5965
- return new Response(method === "HEAD" ? null : body, { status: 200, headers });
5966
- }
5967
- var PASSTHROUGH_HEADERS = ["content-type", "cache-control", "etag", "last-modified", "vary"];
5968
- function collectHeaders(src) {
5969
- const out = {};
5970
- for (const h of PASSTHROUGH_HEADERS) {
5971
- const v = src.headers.get(h);
5972
- if (v) out[h] = v;
5973
- }
5974
- return out;
5975
- }
5976
- function buildCachedResponse(entry, method, xCache) {
5977
- const headers = new Headers(entry.headers);
5978
- headers.set("X-Cache", xCache);
5979
- const ageSec = Math.max(0, Math.floor((entry.expiresAt - entry.ttlMs - Date.now()) / -1e3));
5980
- headers.set("Age", String(Math.max(0, ageSec)));
5981
- const body = method === "HEAD" ? null : entry.body;
5982
- return new Response(body, { status: entry.status, headers });
5983
- }
5984
- async function passthroughResponse(resp, method, xCache) {
5985
- const headers = new Headers(collectHeaders(resp));
5986
- headers.set("X-Cache", xCache);
5987
- if (method === "HEAD") {
5988
- try {
5989
- await resp.arrayBuffer();
5990
- } catch {
5991
- }
5992
- return new Response(null, { status: resp.status, headers });
5993
- }
5994
- const body = await resp.arrayBuffer();
5995
- return new Response(body, { status: resp.status, headers });
5996
- }
5997
- async function consumeAndMaybeCache(resp, key, pathname, method, cache) {
5998
- const body = await resp.arrayBuffer();
5999
- const headers = collectHeaders(resp);
6000
- if (resp.status >= 200 && resp.status < 300) {
6001
- const ttlMs = ttlForPath(pathname);
6002
- const entry = {
6003
- status: resp.status,
6004
- body,
6005
- headers,
6006
- etag: resp.headers.get("etag") ?? void 0,
6007
- lastModified: resp.headers.get("last-modified") ?? void 0,
6008
- expiresAt: Date.now() + ttlMs,
6009
- ttlMs
6010
- };
6011
- cache.set(key, entry);
6012
- }
6013
- const respHeaders = new Headers(headers);
6014
- respHeaders.set("X-Cache", "MISS");
6015
- const outBody = method === "HEAD" ? null : body;
6016
- return new Response(outBody, { status: resp.status, headers: respHeaders });
6017
- }
6018
-
6019
- // src/cloud/marketplace-install-local-plugin.ts
6020
- var import_node_fs4 = require("fs");
6021
- var import_node_path5 = require("path");
6022
- var import_types3 = require("@objectstack/types");
6023
- var ROUTE_BASE = "/api/v1/marketplace/install-local";
6024
- var DEFAULT_DIR = ".objectstack/installed-packages";
6025
- function safeFilename(manifestId) {
6026
- return manifestId.replace(/[^a-zA-Z0-9._-]/g, "_") + ".json";
6027
- }
6028
- var MarketplaceInstallLocalPlugin = class {
6029
- constructor(config = {}) {
6030
- this.name = "com.objectstack.runtime.marketplace-install-local";
6031
- this.version = "1.0.0";
6032
- this.init = async (_ctx) => {
6033
- };
6034
- this.start = async (ctx) => {
6035
- ctx.hook("kernel:ready", async () => {
6036
- await this.rehydrate(ctx);
6037
- let httpServer;
6038
- try {
6039
- httpServer = ctx.getService("http-server");
6040
- } catch {
6041
- ctx.logger?.warn?.("[MarketplaceInstallLocal] http-server not available \u2014 install endpoints not mounted");
6042
- return;
6043
- }
6044
- if (!httpServer || typeof httpServer.getRawApp !== "function") {
6045
- ctx.logger?.warn?.("[MarketplaceInstallLocal] http-server missing getRawApp() \u2014 install endpoints not mounted");
6046
- return;
6047
- }
6048
- const rawApp = httpServer.getRawApp();
6049
- const postHandler = async (c) => this.handleInstall(c, ctx);
6050
- const getHandler = async (c) => this.handleList(c);
6051
- const deleteHandler = async (c) => this.handleUninstall(c, ctx);
6052
- const reseedHandler = async (c) => this.handleReseed(c, ctx);
6053
- const purgeHandler = async (c) => this.handlePurge(c, ctx);
6054
- if (typeof rawApp.post === "function") rawApp.post(ROUTE_BASE, postHandler);
6055
- if (typeof rawApp.get === "function") rawApp.get(ROUTE_BASE, getHandler);
6056
- if (typeof rawApp.delete === "function") rawApp.delete(`${ROUTE_BASE}/:manifestId`, deleteHandler);
6057
- if (typeof rawApp.post === "function") {
6058
- rawApp.post(`${ROUTE_BASE}/:manifestId/reseed-sample-data`, reseedHandler);
6059
- rawApp.post(`${ROUTE_BASE}/:manifestId/purge-sample-data`, purgeHandler);
6060
- }
6061
- ctx.logger?.info?.(`[MarketplaceInstallLocal] mounted at ${ROUTE_BASE} (storage: ${this.storageDir})`);
6062
- });
6063
- };
6064
- /**
6065
- * Re-register every cached manifest with the kernel's manifest service.
6066
- * Safe to call on a kernel that already has the same manifest_id (the
6067
- * underlying ObjectQL registry overwrites by id, but we still warn so
6068
- * a developer can spot the dev-time clash between their config.ts and
6069
- * a marketplace package).
6070
- */
6071
- this.rehydrate = async (ctx) => {
6072
- const entries = this.readAll();
6073
- if (entries.length === 0) return;
6074
- let manifestService = null;
6075
- try {
6076
- manifestService = ctx.getService("manifest");
6077
- } catch {
6078
- ctx.logger?.warn?.("[MarketplaceInstallLocal] no `manifest` service \u2014 rehydrate skipped");
6079
- return;
6080
- }
6081
- for (const entry of entries) {
6082
- try {
6083
- manifestService.register(entry.manifest);
6084
- try {
6085
- const ql = ctx.getService("objectql");
6086
- if (ql && typeof ql.syncSchemas === "function") await ql.syncSchemas();
6087
- } catch {
6088
- }
6089
- await this.applySideEffects(ctx, entry.manifest, { seedNow: false });
6090
- ctx.logger?.info?.(`[MarketplaceInstallLocal] rehydrated ${entry.manifestId}@${entry.version}`);
6091
- } catch (err) {
6092
- ctx.logger?.error?.(`[MarketplaceInstallLocal] rehydrate failed for ${entry.manifestId}`, err instanceof Error ? err : new Error(String(err)));
6093
- }
6094
- }
6095
- };
6096
- this.handleInstall = async (c, ctx) => {
6097
- const userId = await this.requireAuthenticatedUser(c, ctx);
6098
- if (!userId) {
6099
- return c.json({ success: false, error: { code: "unauthorized", message: "Authentication required to install packages." } }, 401);
6100
- }
6101
- let body = {};
6102
- try {
6103
- body = await c.req.json();
6104
- } catch {
6105
- }
6106
- const inlineManifest = body?.manifest && typeof body.manifest === "object" ? body.manifest : null;
6107
- let manifest;
6108
- let resolvedVersionId;
6109
- let version;
6110
- let packageId;
6111
- if (inlineManifest) {
6112
- manifest = inlineManifest;
6113
- packageId = String(manifest.id ?? manifest.name ?? "").trim();
6114
- version = String(manifest.version ?? "unknown");
6115
- resolvedVersionId = String(body?.versionId ?? version);
6116
- if (!packageId) {
6117
- return c.json({ success: false, error: { code: "invalid_manifest", message: 'Inline manifest must have an "id" or "name".' } }, 400);
6118
- }
6119
- } else {
6120
- if (!this.cloudUrl) {
6121
- return c.json({ success: false, error: { code: "marketplace_unavailable", message: "OS_CLOUD_URL not configured." } }, 503);
6122
- }
6123
- packageId = String(body?.packageId ?? "").trim();
6124
- const versionId = String(body?.versionId ?? "latest").trim() || "latest";
6125
- if (!packageId) {
6126
- return c.json({ success: false, error: { code: "bad_request", message: "packageId is required." } }, 400);
6127
- }
6128
- let payload;
6129
- const publicBase = resolveMarketplacePublicBaseUrl();
6130
- const fetchAttempts = [];
6131
- if (publicBase) {
6132
- fetchAttempts.push({
6133
- label: "public-r2",
6134
- url: `${publicBase}/packages/${encodeURIComponent(packageId)}/versions/${encodeURIComponent(versionId)}/manifest.json`
6135
- });
6136
- }
6137
- fetchAttempts.push({
6138
- label: "cloud",
6139
- url: `${this.cloudUrl}/api/v1/marketplace/packages/${encodeURIComponent(packageId)}/versions/${encodeURIComponent(versionId)}/manifest`
6140
- });
6141
- let lastErrStatus = 0;
6142
- let lastErrText = "";
6143
- for (const attempt of fetchAttempts) {
6144
- try {
6145
- const resp = await fetch(attempt.url, { headers: { "Accept": "application/json" } });
6146
- if (!resp.ok) {
6147
- lastErrStatus = resp.status;
6148
- lastErrText = (await resp.text().catch(() => "")).slice(0, 200);
6149
- if (attempt.label === "public-r2" && resp.status === 404) {
6150
- ctx.logger?.info?.(`[MarketplaceInstallLocal] public-r2 miss for ${packageId}@${versionId}, falling back to cloud`);
6151
- continue;
6152
- }
6153
- if (attempt.label === "public-r2" && resp.status >= 500) {
6154
- ctx.logger?.warn?.(`[MarketplaceInstallLocal] public-r2 ${resp.status}, falling back to cloud`);
6155
- continue;
6156
- }
6157
- break;
6158
- }
6159
- payload = await resp.json();
6160
- lastErrStatus = 0;
6161
- break;
6162
- } catch (err) {
6163
- if (attempt.label === "public-r2") {
6164
- ctx.logger?.warn?.(`[MarketplaceInstallLocal] public-r2 fetch error: ${err?.message ?? err}, falling back to cloud`);
6165
- continue;
6166
- }
6167
- return c.json({
6168
- success: false,
6169
- error: { code: "cloud_fetch_failed", message: err?.message ?? String(err) }
6170
- }, 502);
6171
- }
6172
- }
6173
- if (!payload) {
6174
- return c.json({
6175
- success: false,
6176
- error: { code: "cloud_fetch_failed", message: `Cloud returned ${lastErrStatus}: ${lastErrText}` }
6177
- }, lastErrStatus === 404 ? 404 : 502);
6178
- }
6179
- const data = payload?.data ?? payload;
6180
- manifest = data?.manifest;
6181
- resolvedVersionId = String(data?.version_id ?? versionId);
6182
- version = String(data?.version ?? "unknown");
6183
- }
6184
- const manifestId = String(manifest?.id ?? manifest?.name ?? "");
6185
- if (!manifest || !manifestId) {
6186
- return c.json({ success: false, error: { code: "invalid_manifest", message: "Invalid manifest payload." } }, inlineManifest ? 400 : 502);
6187
- }
6188
- const conflict = this.findConflict(ctx, manifestId);
6189
- if (conflict === "user-code") {
6190
- return c.json({
6191
- success: false,
6192
- error: {
6193
- code: "manifest_conflict",
6194
- message: `manifest_id "${manifestId}" is already defined by this runtime's local code. Refusing to overwrite. Uninstall the local definition first.`
6195
- }
6196
- }, 409);
6197
- }
6198
- try {
6199
- const manifestService = ctx.getService("manifest");
6200
- manifestService.register(manifest);
6201
- } catch (err) {
6202
- if (inlineManifest) {
6203
- return c.json({
6204
- success: false,
6205
- error: { code: "register_failed", message: `Failed to register imported manifest: ${err?.message ?? err}` }
6206
- }, 422);
6207
- }
6208
- ctx.logger?.warn?.(`[MarketplaceInstallLocal] hot-register failed for ${manifestId} (will load on next restart): ${err?.message ?? err}`);
6209
- }
6210
- const entry = {
6211
- packageId,
6212
- versionId: resolvedVersionId,
6213
- manifestId,
6214
- version,
6215
- manifest,
6216
- installedAt: (/* @__PURE__ */ new Date()).toISOString(),
6217
- installedBy: userId,
6218
- withSampleData: false
6219
- };
6220
- try {
6221
- (0, import_node_fs4.mkdirSync)(this.storageDir, { recursive: true });
6222
- (0, import_node_fs4.writeFileSync)((0, import_node_path5.join)(this.storageDir, safeFilename(manifestId)), JSON.stringify(entry, null, 2), "utf8");
6223
- } catch (err) {
6224
- return c.json({
6225
- success: false,
6226
- error: { code: "storage_failed", message: `Failed to persist manifest: ${err?.message ?? err}` }
6227
- }, 500);
6228
- }
6229
- try {
6230
- const ql = ctx.getService("objectql");
6231
- if (ql && typeof ql.syncSchemas === "function") {
6232
- await ql.syncSchemas();
6233
- ctx.logger?.info?.(`[MarketplaceInstallLocal] syncSchemas() ran after registering ${manifestId}`);
6234
- }
6235
- } catch (err) {
6236
- ctx.logger?.warn?.(`[MarketplaceInstallLocal] syncSchemas failed for ${manifestId}: ${err?.message ?? err}`);
6237
- }
6238
- const seededSummary = await this.applySideEffects(ctx, manifest, { seedNow: true, c });
6239
- if (seededSummary.seeded.mode === "inline" && (seededSummary.seeded.inserted ?? 0) + (seededSummary.seeded.updated ?? 0) > 0) {
6240
- entry.withSampleData = true;
6241
- try {
6242
- (0, import_node_fs4.writeFileSync)((0, import_node_path5.join)(this.storageDir, safeFilename(manifestId)), JSON.stringify(entry, null, 2), "utf8");
6243
- } catch {
6244
- }
6245
- }
6246
- return c.json({
6247
- success: true,
6248
- data: {
6249
- manifestId,
6250
- version,
6251
- versionId: resolvedVersionId,
6252
- installedAt: entry.installedAt,
6253
- hotLoaded: true,
6254
- upgradedFrom: conflict === "marketplace" ? "previous-marketplace-version" : null,
6255
- translationsLoaded: seededSummary.translationsLoaded,
6256
- seeded: seededSummary.seeded,
6257
- note: "App is now available in this runtime. Refresh the console to see it in the app switcher."
6258
- }
6259
- }, 200);
6260
- };
6261
- this.handleList = async (c) => {
6262
- const entries = this.readAll();
6263
- return c.json({
6264
- success: true,
6265
- data: {
6266
- items: entries.map((e) => ({
6267
- packageId: e.packageId,
6268
- versionId: e.versionId,
6269
- manifestId: e.manifestId,
6270
- version: e.version,
6271
- installedAt: e.installedAt,
6272
- installedBy: e.installedBy,
6273
- withSampleData: e.withSampleData ?? false
6274
- })),
6275
- total: entries.length,
6276
- storageDir: this.storageDir
6277
- }
6278
- }, 200);
6279
- };
6280
- this.handleUninstall = async (c, ctx) => {
6281
- const userId = await this.requireAuthenticatedUser(c, ctx);
6282
- if (!userId) {
6283
- return c.json({ success: false, error: { code: "unauthorized", message: "Authentication required." } }, 401);
6284
- }
6285
- const manifestId = String(c.req.param?.("manifestId") ?? c.req.params?.manifestId ?? "").trim();
6286
- if (!manifestId) {
6287
- return c.json({ success: false, error: { code: "bad_request", message: "manifestId path param required." } }, 400);
6288
- }
6289
- const file = (0, import_node_path5.join)(this.storageDir, safeFilename(manifestId));
6290
- if (!(0, import_node_fs4.existsSync)(file)) {
6291
- return c.json({ success: false, error: { code: "not_found", message: `No marketplace install for ${manifestId}.` } }, 404);
6292
- }
6293
- try {
6294
- (0, import_node_fs4.unlinkSync)(file);
6295
- } catch (err) {
6296
- return c.json({ success: false, error: { code: "storage_failed", message: err?.message ?? String(err) } }, 500);
6297
- }
6298
- ctx.logger?.info?.(`[MarketplaceInstallLocal] uninstalled ${manifestId} (cached manifest removed; restart runtime to unload from running kernel)`);
6299
- return c.json({
6300
- success: true,
6301
- data: {
6302
- manifestId,
6303
- note: "Cached manifest removed. The app remains loaded in the running kernel until the next restart (the kernel API does not support unregistering apps in-place)."
6304
- }
6305
- }, 200);
6306
- };
6307
- /**
6308
- * Detect whether `manifestId` is already known to the kernel and classify
6309
- * the source so we can refuse vs upgrade gracefully.
6310
- *
6311
- * 'none' — fresh install
6312
- * 'marketplace' — previously installed by this plugin (allow upgrade)
6313
- * 'user-code' — defined by AppPlugin from objectstack.config.ts
6314
- * (refuse to avoid silently overwriting authored code)
6315
- */
6316
- this.findConflict = (ctx, manifestId) => {
6317
- if ((0, import_node_fs4.existsSync)((0, import_node_path5.join)(this.storageDir, safeFilename(manifestId)))) {
6318
- return "marketplace";
6319
- }
6320
- try {
6321
- const ql = ctx.getService("objectql");
6322
- const packages = ql?.registry?.getAllPackages?.() ?? [];
6323
- const hit = packages.find(
6324
- (p) => (p?.manifest?.id ?? p?.id ?? p?.manifest?.name) === manifestId
6325
- );
6326
- if (hit) return "user-code";
6327
- } catch {
6328
- }
6329
- return "none";
6330
- };
6331
- /**
6332
- * Pull a userId out of the request's better-auth session, if any.
6333
- * Returns null when there is no signed-in user. v1 does not check
6334
- * admin role — UI gating + the auth requirement is sufficient for
6335
- * dev / single-tenant runtimes. Stricter checks can be layered on
6336
- * via a middleware in cloud-hosted multi-tenant deployments.
6337
- */
6338
- /**
6339
- * POST /api/v1/marketplace/install-local/:manifestId/reseed-sample-data
6340
- *
6341
- * Re-runs SeedLoaderService against the cached manifest's `data` arrays.
6342
- * Idempotent (upsert by id). Useful when:
6343
- * • The user installed an app and skipped sample data
6344
- * • A purge was undone
6345
- * • The user wants a clean baseline back after editing demo rows
6346
- *
6347
- * Multi-tenant: requires an active organization on the session (same
6348
- * rule as install seed path).
6349
- */
6350
- this.handleReseed = async (c, ctx) => {
6351
- const userId = await this.requireAuthenticatedUser(c, ctx);
6352
- if (!userId) {
6353
- return c.json({ success: false, error: { code: "unauthorized", message: "Authentication required." } }, 401);
6354
- }
6355
- const manifestId = String(c.req.param?.("manifestId") ?? c.req.params?.manifestId ?? "").trim();
6356
- if (!manifestId) {
6357
- return c.json({ success: false, error: { code: "bad_request", message: "manifestId path param required." } }, 400);
6358
- }
6359
- const file = (0, import_node_path5.join)(this.storageDir, safeFilename(manifestId));
6360
- if (!(0, import_node_fs4.existsSync)(file)) {
6361
- return c.json({ success: false, error: { code: "not_found", message: `No marketplace install for ${manifestId}.` } }, 404);
6362
- }
6363
- let entry;
6364
- try {
6365
- entry = JSON.parse((0, import_node_fs4.readFileSync)(file, "utf8"));
6366
- } catch (err) {
6367
- return c.json({ success: false, error: { code: "storage_failed", message: `Failed to read manifest cache: ${err?.message ?? err}` } }, 500);
6368
- }
6369
- const summary = await this.applySideEffects(ctx, entry.manifest, { seedNow: true, c });
6370
- if (summary.seeded.mode === "skipped") {
6371
- return c.json({
6372
- success: false,
6373
- error: {
6374
- code: "reseed_skipped",
6375
- message: `Reseed did not run: ${summary.seeded.reason ?? "unknown reason"}`
6376
- }
6377
- }, 400);
6378
- }
6379
- try {
6380
- entry.withSampleData = true;
6381
- (0, import_node_fs4.writeFileSync)(file, JSON.stringify(entry, null, 2), "utf8");
6382
- } catch {
6383
- }
6384
- return c.json({
6385
- success: true,
6386
- data: {
6387
- manifestId,
6388
- inserted: summary.seeded.inserted ?? 0,
6389
- updated: summary.seeded.updated ?? 0,
6390
- errors: summary.seeded.errors ?? 0,
6391
- withSampleData: true
6392
- }
6393
- }, 200);
6394
- };
6395
- /**
6396
- * POST /api/v1/marketplace/install-local/:manifestId/purge-sample-data
6397
- *
6398
- * Deletes every record whose id is declared in the cached manifest's
6399
- * seed datasets. Uses the `driver` service directly to bypass ACL /
6400
- * lifecycle hooks (same pattern as cloud purge). User-created records
6401
- * are never touched — only ids declared in the package's bundled
6402
- * datasets are removed. Already-deleted rows count as `skipped`.
6403
- */
6404
- this.handlePurge = async (c, ctx) => {
6405
- const userId = await this.requireAuthenticatedUser(c, ctx);
6406
- if (!userId) {
6407
- return c.json({ success: false, error: { code: "unauthorized", message: "Authentication required." } }, 401);
6408
- }
6409
- const manifestId = String(c.req.param?.("manifestId") ?? c.req.params?.manifestId ?? "").trim();
6410
- if (!manifestId) {
6411
- return c.json({ success: false, error: { code: "bad_request", message: "manifestId path param required." } }, 400);
6412
- }
6413
- const file = (0, import_node_path5.join)(this.storageDir, safeFilename(manifestId));
6414
- if (!(0, import_node_fs4.existsSync)(file)) {
6415
- return c.json({ success: false, error: { code: "not_found", message: `No marketplace install for ${manifestId}.` } }, 404);
6416
- }
6417
- let entry;
6418
- try {
6419
- entry = JSON.parse((0, import_node_fs4.readFileSync)(file, "utf8"));
6420
- } catch (err) {
6421
- return c.json({ success: false, error: { code: "storage_failed", message: `Failed to read manifest cache: ${err?.message ?? err}` } }, 500);
6422
- }
6423
- const datasets = Array.isArray(entry.manifest?.data) ? entry.manifest.data.filter((d) => d && d.object && Array.isArray(d.records)) : [];
6424
- if (datasets.length === 0) {
6425
- return c.json({
6426
- success: false,
6427
- error: { code: "nothing_to_purge", message: "This package declares no seed datasets." }
6428
- }, 400);
6429
- }
6430
- let driver;
6431
- try {
6432
- driver = ctx.getService("driver");
6433
- } catch {
6434
- }
6435
- if (!driver || typeof driver.delete !== "function") {
6436
- return c.json({
6437
- success: false,
6438
- error: { code: "driver_missing", message: "driver service unavailable \u2014 cannot purge." }
6439
- }, 500);
6440
- }
6441
- let deleted = 0;
6442
- let skipped = 0;
6443
- let errors = 0;
6444
- for (const ds of datasets) {
6445
- const object = String(ds.object);
6446
- for (const rec of ds.records) {
6447
- const id = rec?.id;
6448
- if (id === void 0 || id === null || id === "") {
6449
- skipped++;
6450
- continue;
6451
- }
6452
- try {
6453
- const r = await driver.delete(object, id);
6454
- if (r === false || r === 0 || r?.deleted === 0) skipped++;
6455
- else deleted++;
6456
- } catch (err) {
6457
- const msg = String(err?.message ?? err);
6458
- if (/not.?found|no row/i.test(msg)) skipped++;
6459
- else {
6460
- errors++;
6461
- ctx.logger?.warn?.(`[MarketplaceInstallLocal] purge ${object}#${id}: ${msg}`);
6462
- }
6463
- }
6464
- }
6465
- }
6466
- try {
6467
- entry.withSampleData = false;
6468
- (0, import_node_fs4.writeFileSync)(file, JSON.stringify(entry, null, 2), "utf8");
6469
- } catch {
6470
- }
6471
- ctx.logger?.info?.(`[MarketplaceInstallLocal] purged ${manifestId}: deleted=${deleted} skipped=${skipped} errors=${errors}`);
6472
- return c.json({
6473
- success: true,
6474
- data: { manifestId, deleted, skipped, errors, withSampleData: false }
6475
- }, 200);
6476
- };
6477
- /**
6478
- * Replicate the start-time side-effects that AppPlugin runs for
6479
- * statically-declared apps but the `manifest` service does NOT:
6480
- *
6481
- * 1. Load `manifest.translations` (array of `Record<locale, data>`)
6482
- * into the i18n service — auto-creating an in-memory fallback if
6483
- * none is registered, matching AppPlugin's behaviour.
6484
- *
6485
- * 2. Merge `manifest.data` (an array of seed datasets) into the
6486
- * kernel's `seed-datasets` service so SecurityPlugin's per-org
6487
- * replay middleware picks them up on every future
6488
- * sys_organization insert.
6489
- *
6490
- * 3. When `seedNow=true`, also run the seed immediately so the user
6491
- * sees demo data without having to create a new org:
6492
- * • single-tenant: run SeedLoaderService inline (mirrors
6493
- * AppPlugin single-tenant branch)
6494
- * • multi-tenant: invoke `seed-replayer` for the caller's
6495
- * active org (resolved from the request session)
6496
- *
6497
- * Errors are logged but never thrown — install succeeds even if
6498
- * post-register side-effects partially fail (the manifest itself is
6499
- * already registered + cached). Returns a small summary for the
6500
- * response envelope.
6501
- */
6502
- this.applySideEffects = async (ctx, manifest, opts) => {
6503
- const appId = String(manifest?.id ?? "unknown");
6504
- let translationsLoaded = 0;
6505
- let seedSummary = { mode: "skipped", reason: "no-datasets" };
6506
- try {
6507
- const bundles = [];
6508
- if (Array.isArray(manifest?.translations)) bundles.push(...manifest.translations);
6509
- if (Array.isArray(manifest?.i18n)) bundles.push(...manifest.i18n);
6510
- if (bundles.length > 0) {
6511
- let i18nService;
6512
- try {
6513
- i18nService = ctx.getService("i18n");
6514
- } catch {
6515
- }
6516
- if (!i18nService) {
6517
- try {
6518
- const mod = await import("@objectstack/core");
6519
- const createMemoryI18n = mod.createMemoryI18n;
6520
- if (typeof createMemoryI18n === "function") {
6521
- i18nService = createMemoryI18n();
6522
- ctx.registerService?.("i18n", i18nService);
6523
- ctx.logger?.info?.(`[MarketplaceInstallLocal] auto-registered in-memory i18n fallback for "${appId}"`);
6524
- }
6525
- } catch {
6526
- }
6527
- }
6528
- if (i18nService?.loadTranslations) {
6529
- for (const bundle of bundles) {
6530
- for (const [locale, data] of Object.entries(bundle)) {
6531
- if (data && typeof data === "object") {
6532
- try {
6533
- i18nService.loadTranslations(locale, data);
6534
- translationsLoaded++;
6535
- } catch (err) {
6536
- ctx.logger?.warn?.(`[MarketplaceInstallLocal] failed to load ${appId} translations for ${locale}: ${err?.message ?? err}`);
6537
- }
6538
- }
6539
- }
6540
- }
6541
- ctx.logger?.info?.(`[MarketplaceInstallLocal] loaded ${translationsLoaded} locale bundle(s) for ${appId}`);
6542
- }
6543
- }
6544
- } catch (err) {
6545
- ctx.logger?.warn?.(`[MarketplaceInstallLocal] i18n side-effect failed for ${appId}: ${err?.message ?? err}`);
6546
- }
6547
- const datasets = Array.isArray(manifest?.data) ? manifest.data.filter((d) => d && d.object && Array.isArray(d.records)) : [];
6548
- if (datasets.length > 0) {
6549
- try {
6550
- const kernel = ctx.kernel;
6551
- let existing = [];
6552
- try {
6553
- const v = kernel?.getService?.("seed-datasets");
6554
- if (Array.isArray(v)) existing = v;
6555
- } catch {
6556
- }
6557
- const merged = [...existing, ...datasets];
6558
- if (kernel?.registerService) kernel.registerService("seed-datasets", merged);
6559
- else ctx.registerService?.("seed-datasets", merged);
6560
- ctx.logger?.info?.(`[MarketplaceInstallLocal] merged ${datasets.length} seed dataset(s) into kernel (total: ${merged.length})`);
6561
- } catch (err) {
6562
- ctx.logger?.warn?.(`[MarketplaceInstallLocal] failed to merge seed-datasets: ${err?.message ?? err}`);
6563
- }
6564
- }
6565
- if (opts.seedNow && datasets.length > 0) {
6566
- const multiTenant = String((0, import_types3.readEnvWithDeprecation)("OS_MULTI_ORG_ENABLED", "OS_MULTI_TENANT") ?? "false").toLowerCase() !== "false";
6567
- try {
6568
- const ql = ctx.getService("objectql");
6569
- let metadata;
6570
- try {
6571
- metadata = ctx.getService("metadata");
6572
- } catch {
6573
- }
6574
- if (!ql || !metadata) {
6575
- seedSummary = { mode: "skipped", reason: "objectql-or-metadata-missing" };
6576
- } else {
6577
- let organizationId;
6578
- if (multiTenant) {
6579
- const resolved = await this.resolveActiveOrgId(opts.c, ctx);
6580
- if (resolved) organizationId = resolved;
6581
- else {
6582
- seedSummary = { mode: "skipped", reason: "multi-tenant-no-active-org" };
6583
- ctx.logger?.warn?.("[MarketplaceInstallLocal] multi-tenant: no active org on request \u2014 data not seeded");
6584
- }
6585
- }
6586
- if (!multiTenant || organizationId) {
6587
- const [{ SeedLoaderService: SeedLoaderService2 }, { SeedLoaderRequestSchema }] = await Promise.all([
6588
- Promise.resolve().then(() => (init_seed_loader(), seed_loader_exports)),
6589
- import("@objectstack/spec/data")
6590
- ]);
6591
- const seedLoader = new SeedLoaderService2(ql, metadata, ctx.logger);
6592
- const request = SeedLoaderRequestSchema.parse({
6593
- datasets,
6594
- config: {
6595
- defaultMode: "upsert",
6596
- multiPass: true,
6597
- ...organizationId ? { organizationId } : {}
6598
- }
6599
- });
6600
- const result = await seedLoader.load(request);
6601
- seedSummary = {
6602
- mode: "inline",
6603
- inserted: result.summary.totalInserted,
6604
- updated: result.summary.totalUpdated,
6605
- errors: result.errors.length
6606
- };
6607
- ctx.logger?.info?.(`[MarketplaceInstallLocal] inline seed for ${appId}${organizationId ? ` (org=${organizationId})` : ""}: inserted=${seedSummary.inserted} updated=${seedSummary.updated} errors=${seedSummary.errors}`);
6608
- }
6609
- }
6610
- } catch (err) {
6611
- seedSummary = { mode: "skipped", reason: `seed-error: ${err?.message ?? err}` };
6612
- ctx.logger?.warn?.(`[MarketplaceInstallLocal] seed run failed for ${appId}: ${err?.message ?? err}`);
6613
- }
6614
- }
6615
- return { translationsLoaded, seeded: seedSummary };
6616
- };
6617
- /**
6618
- * Best-effort active-org resolution. Reads the better-auth session
6619
- * (same path as requireAuthenticatedUser) and returns
6620
- * `session.activeOrganizationId`, falling back to the user's first
6621
- * org membership.
6622
- */
6623
- this.resolveActiveOrgId = async (c, ctx) => {
6624
- if (!c?.req?.raw?.headers) return null;
6625
- try {
6626
- const authService = ctx.getService("auth");
6627
- let api = authService?.api;
6628
- if (!api && typeof authService?.getApi === "function") api = await authService.getApi();
6629
- if (!api?.getSession) return null;
6630
- const session = await api.getSession({ headers: c.req.raw.headers });
6631
- const direct = session?.session?.activeOrganizationId ?? session?.activeOrganizationId ?? null;
6632
- if (direct) return String(direct);
6633
- const userId = session?.user?.id;
6634
- if (!userId) return null;
6635
- try {
6636
- const ql = ctx.getService("objectql");
6637
- if (ql?.find) {
6638
- const rows = await ql.find("sys_organization_member", { where: { user_id: userId }, limit: 1, context: { isSystem: true } });
6639
- const row = Array.isArray(rows) ? rows[0] : rows?.items?.[0] ?? null;
6640
- return row?.organization_id ? String(row.organization_id) : null;
6641
- }
6642
- } catch {
6643
- }
6644
- } catch {
6645
- }
6646
- return null;
6647
- };
6648
- this.requireAuthenticatedUser = async (c, ctx) => {
6649
- try {
6650
- const authService = ctx.getService("auth");
6651
- let api = authService?.api;
6652
- if (!api && typeof authService?.getApi === "function") {
6653
- api = await authService.getApi();
6654
- }
6655
- if (api?.getSession && c?.req?.raw?.headers) {
6656
- const session = await api.getSession({ headers: c.req.raw.headers });
6657
- const userId = session?.user?.id ?? null;
6658
- if (userId) return String(userId);
6659
- }
6660
- } catch {
6661
- }
6662
- const xUserId = c?.req?.header?.("x-user-id");
6663
- if (xUserId) return String(xUserId);
6664
- return null;
6665
- };
6666
- this.readAll = () => {
6667
- if (!(0, import_node_fs4.existsSync)(this.storageDir)) return [];
6668
- const out = [];
6669
- for (const name of (0, import_node_fs4.readdirSync)(this.storageDir)) {
6670
- if (!name.endsWith(".json")) continue;
6671
- try {
6672
- const raw = (0, import_node_fs4.readFileSync)((0, import_node_path5.join)(this.storageDir, name), "utf8");
6673
- out.push(JSON.parse(raw));
6674
- } catch {
6675
- }
6676
- }
6677
- return out;
6678
- };
6679
- this.cloudUrl = resolveCloudUrl(config.controlPlaneUrl);
6680
- this.storageDir = config.storageDir ? (0, import_node_path5.resolve)(config.storageDir) : (0, import_node_path5.resolve)(process.cwd(), DEFAULT_DIR);
6681
- }
6682
- };
6683
-
6684
- // src/cloud/runtime-config-plugin.ts
6685
- var RuntimeConfigPlugin = class {
6686
- constructor(config = {}) {
6687
- this.name = "com.objectstack.runtime.runtime-config";
6688
- this.version = "1.0.0";
6689
- this.init = async (_ctx) => {
6690
- };
6691
- this.start = async (ctx) => {
6692
- ctx.hook("kernel:ready", async () => {
6693
- let httpServer;
6694
- try {
6695
- httpServer = ctx.getService("http-server");
6696
- } catch {
6697
- ctx.logger?.warn?.("[RuntimeConfigPlugin] http-server not available \u2014 runtime/config not mounted");
6698
- return;
6699
- }
6700
- if (!httpServer || typeof httpServer.getRawApp !== "function") {
6701
- ctx.logger?.warn?.("[RuntimeConfigPlugin] http-server missing getRawApp() \u2014 runtime/config not mounted");
6702
- return;
6703
- }
6704
- const rawApp = httpServer.getRawApp();
6705
- const features = {
6706
- installLocal: this.installLocal,
6707
- marketplace: true,
6708
- aiStudio: this.aiStudio
6709
- };
6710
- let envRegistry = null;
6711
- try {
6712
- envRegistry = ctx.getService("env-registry");
6713
- } catch {
6714
- }
6715
- const handler = async (c) => {
6716
- const rawHost = c.req.header("host") ?? "";
6717
- const host = rawHost.split(":")[0].toLowerCase().trim();
6718
- let defaultEnvironmentId;
6719
- let defaultOrgId;
6720
- let resolvedSingleEnv = this.singleEnvironment;
6721
- const resolveFn = typeof envRegistry?.resolveByHostname === "function" ? envRegistry.resolveByHostname.bind(envRegistry) : typeof envRegistry?.resolveHostname === "function" ? envRegistry.resolveHostname.bind(envRegistry) : null;
6722
- if (resolveFn && host) {
6723
- try {
6724
- const resolved = await resolveFn(host);
6725
- if (resolved?.environmentId) {
6726
- defaultEnvironmentId = String(resolved.environmentId);
6727
- const orgId = resolved.organizationId ?? resolved.organization_id;
6728
- if (orgId) defaultOrgId = String(orgId);
6729
- resolvedSingleEnv = true;
6730
- }
6731
- } catch {
6732
- }
6733
- }
6734
- return c.json({
6735
- cloudUrl: this.cloudUrl,
6736
- singleEnvironment: resolvedSingleEnv,
6737
- defaultOrgId,
6738
- defaultEnvironmentId,
6739
- features,
6740
- branding: {
6741
- productName: this.productName,
6742
- productShortName: this.productShortName
6743
- }
6744
- });
6745
- };
6746
- rawApp.get("/api/v1/runtime/config", handler);
6747
- rawApp.get("/api/v1/studio/runtime-config", handler);
6748
- ctx.logger?.info?.("[RuntimeConfigPlugin] mounted /api/v1/runtime/config", {
6749
- cloudUrl: this.cloudUrl || "(empty)",
6750
- installLocal: this.installLocal,
6751
- perHostEnvResolution: !!envRegistry
6752
- });
6753
- });
6754
- };
6755
- this.destroy = async () => {
6756
- };
6757
- this.cloudUrl = config.controlPlaneUrl === "" ? "" : resolveCloudUrl(config.controlPlaneUrl) ?? "";
6758
- this.installLocal = !!config.installLocal;
6759
- this.aiStudio = config.aiStudio !== false;
6760
- this.singleEnvironment = !!config.singleEnvironment;
6761
- const envName = (typeof process !== "undefined" ? process.env?.OS_PRODUCT_NAME : void 0)?.trim();
6762
- const envShort = (typeof process !== "undefined" ? process.env?.OS_PRODUCT_SHORT_NAME : void 0)?.trim();
6763
- this.productName = (config.productName ?? envName ?? "ObjectOS").trim() || "ObjectOS";
6764
- this.productShortName = (config.productShortName ?? envShort ?? this.productName).trim() || this.productName;
6765
- }
6766
- };
6767
-
6768
5576
  // src/sandbox/script-runner.ts
6769
5577
  var UnimplementedScriptRunner = class {
6770
5578
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
@@ -6790,11 +5598,10 @@ init_body_runner();
6790
5598
  // src/index.ts
6791
5599
  var import_rest = require("@objectstack/rest");
6792
5600
  __reExport(index_exports, require("@objectstack/core"), module.exports);
6793
- var import_types4 = require("@objectstack/types");
5601
+ var import_types3 = require("@objectstack/types");
6794
5602
  // Annotate the CommonJS export names for ESM import in node:
6795
5603
  0 && (module.exports = {
6796
5604
  AppPlugin,
6797
- DEFAULT_CLOUD_URL,
6798
5605
  DEFAULT_RATE_LIMITS,
6799
5606
  DriverPlugin,
6800
5607
  ExternalValidationPlugin,
@@ -6802,8 +5609,6 @@ var import_types4 = require("@objectstack/types");
6802
5609
  HttpServer,
6803
5610
  InMemoryErrorReporter,
6804
5611
  InMemoryMetricsRegistry,
6805
- MarketplaceInstallLocalPlugin,
6806
- MarketplaceProxyPlugin,
6807
5612
  MiddlewareManager,
6808
5613
  NoopErrorReporter,
6809
5614
  NoopMetricsRegistry,
@@ -6818,7 +5623,6 @@ var import_types4 = require("@objectstack/types");
6818
5623
  RouteGroupBuilder,
6819
5624
  RouteManager,
6820
5625
  Runtime,
6821
- RuntimeConfigPlugin,
6822
5626
  SYSTEM_ENVIRONMENT_ID,
6823
5627
  SandboxError,
6824
5628
  SeedLoaderService,
@@ -6845,7 +5649,6 @@ var import_types4 = require("@objectstack/types");
6845
5649
  parseTraceparent,
6846
5650
  readArtifactSource,
6847
5651
  readEnvWithDeprecation,
6848
- resolveCloudUrl,
6849
5652
  resolveDefaultArtifactPath,
6850
5653
  resolveErrorReporter,
6851
5654
  resolveMetrics,