@objectstack/runtime 6.5.0 → 6.6.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.d.cts CHANGED
@@ -71,6 +71,20 @@ declare class Runtime {
71
71
  getKernel(): ObjectKernel;
72
72
  }
73
73
 
74
+ /**
75
+ * Resolve the ObjectStack home directory used to store cwd-independent
76
+ * runtime data (default sqlite database, downloaded marketplace apps,
77
+ * installed plugin cache).
78
+ *
79
+ * Resolution order:
80
+ * 1. `OS_HOME` env var (absolute path; `~` expanded)
81
+ * 2. `~/.objectstack` (cross-platform user-home default)
82
+ *
83
+ * The directory is created lazily by callers that actually write to it
84
+ * (e.g. the sqlite driver's `mkdirSync(...)`); this helper does not
85
+ * touch the filesystem.
86
+ */
87
+ declare function resolveObjectStackHome(): string;
74
88
  declare const StandaloneStackConfigSchema: z.ZodObject<{
75
89
  databaseUrl: z.ZodOptional<z.ZodString>;
76
90
  databaseAuthToken: z.ZodOptional<z.ZodString>;
@@ -820,6 +834,30 @@ interface KernelManagerConfig {
820
834
  warn?: (...a: any[]) => void;
821
835
  error?: (...a: any[]) => void;
822
836
  };
837
+ /**
838
+ * Optional upstream-change detector. When set, every cache hit older
839
+ * than `staleCheckIntervalMs` triggers this probe before returning the
840
+ * cached kernel. Returning `true` evicts the kernel and forces a
841
+ * rebuild, so changes to the control-plane state that don't reach
842
+ * this process via push (marketplace installs, artifact republish,
843
+ * etc.) become visible without waiting for the LRU TTL to expire.
844
+ *
845
+ * The probe should be cheap (single small GET). Errors thrown here
846
+ * are caught and treated as "still fresh" so a brief upstream
847
+ * outage doesn't churn every cached kernel — the worst case is
848
+ * stale-by-`ttlMs`, which is what we had before adding the probe.
849
+ *
850
+ * `builtAtMs` is the kernel's `createdAt` time so the probe can
851
+ * compare against an upstream "last changed at" timestamp.
852
+ */
853
+ freshnessProbe?: (environmentId: string, builtAtMs: number) => Promise<boolean>;
854
+ /**
855
+ * Minimum gap between successive freshness probes for the same env.
856
+ * Defaults to 10 seconds — enough to avoid hammering the control
857
+ * plane on tight render loops while still keeping the user's
858
+ * post-install refresh perceived as immediate.
859
+ */
860
+ staleCheckIntervalMs?: number;
823
861
  }
824
862
  /**
825
863
  * LRU + TTL cache of per-project {@link ObjectKernel} instances.
@@ -837,6 +875,8 @@ declare class KernelManager {
837
875
  private readonly logger;
838
876
  private readonly cache;
839
877
  private readonly pending;
878
+ private readonly freshnessProbe?;
879
+ private readonly staleCheckIntervalMs;
840
880
  constructor(config: KernelManagerConfig);
841
881
  /** Returns the currently cached environmentIds (ordered by insertion). */
842
882
  keys(): string[];
@@ -1716,6 +1756,24 @@ declare class ArtifactApiClient {
1716
1756
  commitId: string;
1717
1757
  publishedAt?: string | null;
1718
1758
  } | null>;
1759
+ /**
1760
+ * Cheap freshness probe — returns the env's `last_published_at`
1761
+ * (and best-effort current commit) without rebuilding the artifact.
1762
+ * Used by `KernelManager` on cache hits to detect when a per-env
1763
+ * kernel has been invalidated by an upstream change (marketplace
1764
+ * install/uninstall, artifact publish) so it can be rebuilt
1765
+ * without waiting for the 15-minute LRU TTL to expire.
1766
+ *
1767
+ * Returns `null` on definitive 404 / unknown env. Errors propagate
1768
+ * (caller decides whether to treat unreachable cloud as fresh or
1769
+ * stale — typically fresh, so a brief outage doesn't churn every
1770
+ * cached kernel).
1771
+ */
1772
+ getFreshness(environmentId: string): Promise<{
1773
+ environmentId: string;
1774
+ lastPublishedAt: string | null;
1775
+ commitId: string | null;
1776
+ } | null>;
1719
1777
  /** Drop cached entries for a project (and any matching hostname). */
1720
1778
  invalidate(environmentId: string): void;
1721
1779
  /** Drop everything. Used on shutdown / hot-reload. */
@@ -2521,4 +2579,4 @@ declare function actionBodyRunnerFactory(runner: ScriptRunner, opts: FactoryOpti
2521
2579
  timeoutMs?: number;
2522
2580
  }) => ((actionCtx: any) => Promise<unknown>) | undefined;
2523
2581
 
2524
- export { AppPlugin, ArtifactApiClient, type ArtifactApiClientConfig, ArtifactEnvironmentRegistry, type ArtifactEnvironmentRegistryConfig, ArtifactKernelFactory, type ArtifactKernelFactoryConfig, AuthProxyPlugin, type BackfillPlatformSsoClientsOptions, DEFAULT_CLOUD_URL, DEFAULT_RATE_LIMITS, type DefaultHostConfigOptions, type DefaultHostConfigResult, type DispatcherPluginConfig, DriverPlugin, type EnvironmentArtifactResponse, type EnvironmentDriverRegistry, type EnvironmentKernelFactory, type EnvironmentRuntimeConfig, FileArtifactApiClient, type FileArtifactApiClientConfig, HttpDispatcher, type HttpDispatcherResult, type HttpProtocolContext, HttpServer, KernelManager, type KernelManagerConfig, type LoadArtifactBundleOptions, MarketplaceInstallLocalPlugin, type MarketplaceInstallLocalPluginConfig, MarketplaceProxyPlugin, type MarketplaceProxyPluginConfig, MiddlewareManager, type ObjectOSStackConfig, type ObjectOSStackResult, ObservabilityServicePlugin, type ObservabilityServicePluginOptions, PLATFORM_SSO_PROVIDER_ID, QuickJSScriptRunner, type QuickJSScriptRunnerOptions, type RateLimitBucketConfig, type RateLimitDecision, type RateLimitDefaults, type RateLimitStore, RateLimiter, type ResolvedHostname, Runtime, type RuntimeConfig, RuntimeConfigPlugin, type RuntimeConfigPluginConfig, SYSTEM_ENVIRONMENT_ID, SandboxError, type ScriptContext, type ScriptOrigin, type ScriptResult, type ScriptRunOptions, type ScriptRunner, type SecurityHeadersOptions, SeedLoaderService, type SeedPlatformSsoClientOptions, type StandaloneStackConfig, type StandaloneStackResult, type SystemEnvironmentPluginConfig, type TraceContext, UnimplementedScriptRunner, actionBodyRunnerFactory, backfillPlatformSsoClients, buildPlatformSsoRedirectUri, buildSecurityHeaders, collectBundleActions, collectBundleFunctions, collectBundleHooks, createDefaultHostConfig, createDispatcherPlugin, createObjectOSStack, createStandaloneStack, createSystemEnvironmentPlugin, derivePlatformSsoClientId, derivePlatformSsoClientSecret, extractRequestId, formatTraceparent, generateRequestId, hookBodyRunnerFactory, isHttpUrl, loadArtifactBundle, mergeRuntimeModule, parseTraceparent, readArtifactSource, resolveCloudUrl, resolveDefaultArtifactPath, resolveErrorReporter, resolveMetrics, resolveRequestId, seedPlatformSsoClient };
2582
+ export { AppPlugin, ArtifactApiClient, type ArtifactApiClientConfig, ArtifactEnvironmentRegistry, type ArtifactEnvironmentRegistryConfig, ArtifactKernelFactory, type ArtifactKernelFactoryConfig, AuthProxyPlugin, type BackfillPlatformSsoClientsOptions, DEFAULT_CLOUD_URL, DEFAULT_RATE_LIMITS, type DefaultHostConfigOptions, type DefaultHostConfigResult, type DispatcherPluginConfig, DriverPlugin, type EnvironmentArtifactResponse, type EnvironmentDriverRegistry, type EnvironmentKernelFactory, type EnvironmentRuntimeConfig, FileArtifactApiClient, type FileArtifactApiClientConfig, HttpDispatcher, type HttpDispatcherResult, type HttpProtocolContext, HttpServer, KernelManager, type KernelManagerConfig, type LoadArtifactBundleOptions, MarketplaceInstallLocalPlugin, type MarketplaceInstallLocalPluginConfig, MarketplaceProxyPlugin, type MarketplaceProxyPluginConfig, MiddlewareManager, type ObjectOSStackConfig, type ObjectOSStackResult, ObservabilityServicePlugin, type ObservabilityServicePluginOptions, PLATFORM_SSO_PROVIDER_ID, QuickJSScriptRunner, type QuickJSScriptRunnerOptions, type RateLimitBucketConfig, type RateLimitDecision, type RateLimitDefaults, type RateLimitStore, RateLimiter, type ResolvedHostname, Runtime, type RuntimeConfig, RuntimeConfigPlugin, type RuntimeConfigPluginConfig, SYSTEM_ENVIRONMENT_ID, SandboxError, type ScriptContext, type ScriptOrigin, type ScriptResult, type ScriptRunOptions, type ScriptRunner, type SecurityHeadersOptions, SeedLoaderService, type SeedPlatformSsoClientOptions, type StandaloneStackConfig, type StandaloneStackResult, type SystemEnvironmentPluginConfig, type TraceContext, UnimplementedScriptRunner, actionBodyRunnerFactory, backfillPlatformSsoClients, buildPlatformSsoRedirectUri, buildSecurityHeaders, collectBundleActions, collectBundleFunctions, collectBundleHooks, createDefaultHostConfig, createDispatcherPlugin, createObjectOSStack, createStandaloneStack, createSystemEnvironmentPlugin, derivePlatformSsoClientId, derivePlatformSsoClientSecret, extractRequestId, formatTraceparent, generateRequestId, hookBodyRunnerFactory, isHttpUrl, loadArtifactBundle, mergeRuntimeModule, parseTraceparent, readArtifactSource, resolveCloudUrl, resolveDefaultArtifactPath, resolveErrorReporter, resolveMetrics, resolveObjectStackHome, resolveRequestId, seedPlatformSsoClient };
package/dist/index.d.ts CHANGED
@@ -71,6 +71,20 @@ declare class Runtime {
71
71
  getKernel(): ObjectKernel;
72
72
  }
73
73
 
74
+ /**
75
+ * Resolve the ObjectStack home directory used to store cwd-independent
76
+ * runtime data (default sqlite database, downloaded marketplace apps,
77
+ * installed plugin cache).
78
+ *
79
+ * Resolution order:
80
+ * 1. `OS_HOME` env var (absolute path; `~` expanded)
81
+ * 2. `~/.objectstack` (cross-platform user-home default)
82
+ *
83
+ * The directory is created lazily by callers that actually write to it
84
+ * (e.g. the sqlite driver's `mkdirSync(...)`); this helper does not
85
+ * touch the filesystem.
86
+ */
87
+ declare function resolveObjectStackHome(): string;
74
88
  declare const StandaloneStackConfigSchema: z.ZodObject<{
75
89
  databaseUrl: z.ZodOptional<z.ZodString>;
76
90
  databaseAuthToken: z.ZodOptional<z.ZodString>;
@@ -820,6 +834,30 @@ interface KernelManagerConfig {
820
834
  warn?: (...a: any[]) => void;
821
835
  error?: (...a: any[]) => void;
822
836
  };
837
+ /**
838
+ * Optional upstream-change detector. When set, every cache hit older
839
+ * than `staleCheckIntervalMs` triggers this probe before returning the
840
+ * cached kernel. Returning `true` evicts the kernel and forces a
841
+ * rebuild, so changes to the control-plane state that don't reach
842
+ * this process via push (marketplace installs, artifact republish,
843
+ * etc.) become visible without waiting for the LRU TTL to expire.
844
+ *
845
+ * The probe should be cheap (single small GET). Errors thrown here
846
+ * are caught and treated as "still fresh" so a brief upstream
847
+ * outage doesn't churn every cached kernel — the worst case is
848
+ * stale-by-`ttlMs`, which is what we had before adding the probe.
849
+ *
850
+ * `builtAtMs` is the kernel's `createdAt` time so the probe can
851
+ * compare against an upstream "last changed at" timestamp.
852
+ */
853
+ freshnessProbe?: (environmentId: string, builtAtMs: number) => Promise<boolean>;
854
+ /**
855
+ * Minimum gap between successive freshness probes for the same env.
856
+ * Defaults to 10 seconds — enough to avoid hammering the control
857
+ * plane on tight render loops while still keeping the user's
858
+ * post-install refresh perceived as immediate.
859
+ */
860
+ staleCheckIntervalMs?: number;
823
861
  }
824
862
  /**
825
863
  * LRU + TTL cache of per-project {@link ObjectKernel} instances.
@@ -837,6 +875,8 @@ declare class KernelManager {
837
875
  private readonly logger;
838
876
  private readonly cache;
839
877
  private readonly pending;
878
+ private readonly freshnessProbe?;
879
+ private readonly staleCheckIntervalMs;
840
880
  constructor(config: KernelManagerConfig);
841
881
  /** Returns the currently cached environmentIds (ordered by insertion). */
842
882
  keys(): string[];
@@ -1716,6 +1756,24 @@ declare class ArtifactApiClient {
1716
1756
  commitId: string;
1717
1757
  publishedAt?: string | null;
1718
1758
  } | null>;
1759
+ /**
1760
+ * Cheap freshness probe — returns the env's `last_published_at`
1761
+ * (and best-effort current commit) without rebuilding the artifact.
1762
+ * Used by `KernelManager` on cache hits to detect when a per-env
1763
+ * kernel has been invalidated by an upstream change (marketplace
1764
+ * install/uninstall, artifact publish) so it can be rebuilt
1765
+ * without waiting for the 15-minute LRU TTL to expire.
1766
+ *
1767
+ * Returns `null` on definitive 404 / unknown env. Errors propagate
1768
+ * (caller decides whether to treat unreachable cloud as fresh or
1769
+ * stale — typically fresh, so a brief outage doesn't churn every
1770
+ * cached kernel).
1771
+ */
1772
+ getFreshness(environmentId: string): Promise<{
1773
+ environmentId: string;
1774
+ lastPublishedAt: string | null;
1775
+ commitId: string | null;
1776
+ } | null>;
1719
1777
  /** Drop cached entries for a project (and any matching hostname). */
1720
1778
  invalidate(environmentId: string): void;
1721
1779
  /** Drop everything. Used on shutdown / hot-reload. */
@@ -2521,4 +2579,4 @@ declare function actionBodyRunnerFactory(runner: ScriptRunner, opts: FactoryOpti
2521
2579
  timeoutMs?: number;
2522
2580
  }) => ((actionCtx: any) => Promise<unknown>) | undefined;
2523
2581
 
2524
- export { AppPlugin, ArtifactApiClient, type ArtifactApiClientConfig, ArtifactEnvironmentRegistry, type ArtifactEnvironmentRegistryConfig, ArtifactKernelFactory, type ArtifactKernelFactoryConfig, AuthProxyPlugin, type BackfillPlatformSsoClientsOptions, DEFAULT_CLOUD_URL, DEFAULT_RATE_LIMITS, type DefaultHostConfigOptions, type DefaultHostConfigResult, type DispatcherPluginConfig, DriverPlugin, type EnvironmentArtifactResponse, type EnvironmentDriverRegistry, type EnvironmentKernelFactory, type EnvironmentRuntimeConfig, FileArtifactApiClient, type FileArtifactApiClientConfig, HttpDispatcher, type HttpDispatcherResult, type HttpProtocolContext, HttpServer, KernelManager, type KernelManagerConfig, type LoadArtifactBundleOptions, MarketplaceInstallLocalPlugin, type MarketplaceInstallLocalPluginConfig, MarketplaceProxyPlugin, type MarketplaceProxyPluginConfig, MiddlewareManager, type ObjectOSStackConfig, type ObjectOSStackResult, ObservabilityServicePlugin, type ObservabilityServicePluginOptions, PLATFORM_SSO_PROVIDER_ID, QuickJSScriptRunner, type QuickJSScriptRunnerOptions, type RateLimitBucketConfig, type RateLimitDecision, type RateLimitDefaults, type RateLimitStore, RateLimiter, type ResolvedHostname, Runtime, type RuntimeConfig, RuntimeConfigPlugin, type RuntimeConfigPluginConfig, SYSTEM_ENVIRONMENT_ID, SandboxError, type ScriptContext, type ScriptOrigin, type ScriptResult, type ScriptRunOptions, type ScriptRunner, type SecurityHeadersOptions, SeedLoaderService, type SeedPlatformSsoClientOptions, type StandaloneStackConfig, type StandaloneStackResult, type SystemEnvironmentPluginConfig, type TraceContext, UnimplementedScriptRunner, actionBodyRunnerFactory, backfillPlatformSsoClients, buildPlatformSsoRedirectUri, buildSecurityHeaders, collectBundleActions, collectBundleFunctions, collectBundleHooks, createDefaultHostConfig, createDispatcherPlugin, createObjectOSStack, createStandaloneStack, createSystemEnvironmentPlugin, derivePlatformSsoClientId, derivePlatformSsoClientSecret, extractRequestId, formatTraceparent, generateRequestId, hookBodyRunnerFactory, isHttpUrl, loadArtifactBundle, mergeRuntimeModule, parseTraceparent, readArtifactSource, resolveCloudUrl, resolveDefaultArtifactPath, resolveErrorReporter, resolveMetrics, resolveRequestId, seedPlatformSsoClient };
2582
+ export { AppPlugin, ArtifactApiClient, type ArtifactApiClientConfig, ArtifactEnvironmentRegistry, type ArtifactEnvironmentRegistryConfig, ArtifactKernelFactory, type ArtifactKernelFactoryConfig, AuthProxyPlugin, type BackfillPlatformSsoClientsOptions, DEFAULT_CLOUD_URL, DEFAULT_RATE_LIMITS, type DefaultHostConfigOptions, type DefaultHostConfigResult, type DispatcherPluginConfig, DriverPlugin, type EnvironmentArtifactResponse, type EnvironmentDriverRegistry, type EnvironmentKernelFactory, type EnvironmentRuntimeConfig, FileArtifactApiClient, type FileArtifactApiClientConfig, HttpDispatcher, type HttpDispatcherResult, type HttpProtocolContext, HttpServer, KernelManager, type KernelManagerConfig, type LoadArtifactBundleOptions, MarketplaceInstallLocalPlugin, type MarketplaceInstallLocalPluginConfig, MarketplaceProxyPlugin, type MarketplaceProxyPluginConfig, MiddlewareManager, type ObjectOSStackConfig, type ObjectOSStackResult, ObservabilityServicePlugin, type ObservabilityServicePluginOptions, PLATFORM_SSO_PROVIDER_ID, QuickJSScriptRunner, type QuickJSScriptRunnerOptions, type RateLimitBucketConfig, type RateLimitDecision, type RateLimitDefaults, type RateLimitStore, RateLimiter, type ResolvedHostname, Runtime, type RuntimeConfig, RuntimeConfigPlugin, type RuntimeConfigPluginConfig, SYSTEM_ENVIRONMENT_ID, SandboxError, type ScriptContext, type ScriptOrigin, type ScriptResult, type ScriptRunOptions, type ScriptRunner, type SecurityHeadersOptions, SeedLoaderService, type SeedPlatformSsoClientOptions, type StandaloneStackConfig, type StandaloneStackResult, type SystemEnvironmentPluginConfig, type TraceContext, UnimplementedScriptRunner, actionBodyRunnerFactory, backfillPlatformSsoClients, buildPlatformSsoRedirectUri, buildSecurityHeaders, collectBundleActions, collectBundleFunctions, collectBundleHooks, createDefaultHostConfig, createDispatcherPlugin, createObjectOSStack, createStandaloneStack, createSystemEnvironmentPlugin, derivePlatformSsoClientId, derivePlatformSsoClientSecret, extractRequestId, formatTraceparent, generateRequestId, hookBodyRunnerFactory, isHttpUrl, loadArtifactBundle, mergeRuntimeModule, parseTraceparent, readArtifactSource, resolveCloudUrl, resolveDefaultArtifactPath, resolveErrorReporter, resolveMetrics, resolveObjectStackHome, resolveRequestId, seedPlatformSsoClient };
package/dist/index.js CHANGED
@@ -2107,7 +2107,16 @@ var Runtime = class {
2107
2107
  init_load_artifact_bundle();
2108
2108
  import { resolve as resolvePath2 } from "path";
2109
2109
  import { mkdirSync } from "fs";
2110
+ import { homedir } from "os";
2110
2111
  import { z } from "zod";
2112
+ function resolveObjectStackHome() {
2113
+ const raw = process.env.OS_HOME?.trim();
2114
+ if (raw && raw.length > 0) {
2115
+ if (raw.startsWith("~")) return resolvePath2(homedir(), raw.slice(1).replace(/^[/\\]/, ""));
2116
+ return resolvePath2(raw);
2117
+ }
2118
+ return resolvePath2(homedir(), ".objectstack");
2119
+ }
2111
2120
  var StandaloneStackConfigSchema = z.object({
2112
2121
  databaseUrl: z.string().optional(),
2113
2122
  databaseAuthToken: z.string().optional(),
@@ -2138,7 +2147,7 @@ async function createStandaloneStack(config) {
2138
2147
  const environmentId = cfg.environmentId ?? process.env.OS_ENVIRONMENT_ID ?? "proj_local";
2139
2148
  const artifactPathInput = cfg.artifactPath ?? process.env.OS_ARTIFACT_PATH ?? resolvePath2(cwd, "dist/objectstack.json");
2140
2149
  const artifactPath = isHttpUrl(artifactPathInput) ? artifactPathInput : artifactPathInput.startsWith("/") ? artifactPathInput : resolvePath2(cwd, artifactPathInput);
2141
- const dbUrl = cfg.databaseUrl ?? process.env.OS_DATABASE_URL?.trim() ?? process.env.TURSO_DATABASE_URL?.trim() ?? `file:${resolvePath2(cwd, ".objectstack/data/standalone.db")}`;
2150
+ const dbUrl = cfg.databaseUrl ?? process.env.OS_DATABASE_URL?.trim() ?? process.env.TURSO_DATABASE_URL?.trim() ?? `file:${resolvePath2(resolveObjectStackHome(), "data/standalone.db")}`;
2142
2151
  const dbAuthToken = cfg.databaseAuthToken ?? process.env.OS_DATABASE_AUTH_TOKEN?.trim() ?? process.env.TURSO_AUTH_TOKEN?.trim();
2143
2152
  const explicitDriver = cfg.databaseDriver ?? process.env.OS_DATABASE_DRIVER?.trim();
2144
2153
  const dbDriver = explicitDriver ?? detectDriverFromUrl(dbUrl);
@@ -2248,7 +2257,7 @@ async function createStandaloneStack(config) {
2248
2257
 
2249
2258
  // src/default-host.ts
2250
2259
  import { resolve as resolvePath3 } from "path";
2251
- import { existsSync } from "fs";
2260
+ import { existsSync, mkdirSync as mkdirSync2, writeFileSync } from "fs";
2252
2261
  init_load_artifact_bundle();
2253
2262
  function resolveDefaultArtifactPath(explicitPath, cwd = process.cwd()) {
2254
2263
  const candidate = explicitPath ?? process.env.OS_ARTIFACT_PATH ?? resolvePath3(cwd, "dist/objectstack.json");
@@ -2258,12 +2267,42 @@ function resolveDefaultArtifactPath(explicitPath, cwd = process.cwd()) {
2258
2267
  }
2259
2268
  async function createDefaultHostConfig(options = {}) {
2260
2269
  const { requireArtifact = true, ...standaloneOpts } = options;
2261
- const resolvedArtifact = resolveDefaultArtifactPath(standaloneOpts.artifactPath);
2270
+ let resolvedArtifact = resolveDefaultArtifactPath(standaloneOpts.artifactPath);
2262
2271
  if (!resolvedArtifact && requireArtifact) {
2263
2272
  throw new Error(
2264
2273
  "[createDefaultHostConfig] No artifact source available. Set OS_ARTIFACT_PATH (file path or http(s):// URL), place the artifact at <cwd>/dist/objectstack.json, or pass `{ artifactPath: ... }` explicitly. To boot an empty kernel anyway, pass `{ requireArtifact: false }`."
2265
2274
  );
2266
2275
  }
2276
+ if (!resolvedArtifact && !requireArtifact) {
2277
+ const home = resolveObjectStackHome();
2278
+ const stubPath = resolvePath3(home, "dist/objectstack.json");
2279
+ if (!existsSync(stubPath)) {
2280
+ mkdirSync2(resolvePath3(stubPath, ".."), { recursive: true });
2281
+ writeFileSync(
2282
+ stubPath,
2283
+ JSON.stringify(
2284
+ {
2285
+ manifest: {
2286
+ id: "com.objectstack.empty",
2287
+ name: "empty",
2288
+ version: "0.0.0",
2289
+ type: "app",
2290
+ description: "Empty starter kernel \u2014 install apps via the Studio marketplace."
2291
+ },
2292
+ objects: [],
2293
+ views: [],
2294
+ apps: [],
2295
+ flows: [],
2296
+ requires: []
2297
+ },
2298
+ null,
2299
+ 2
2300
+ ),
2301
+ "utf8"
2302
+ );
2303
+ }
2304
+ resolvedArtifact = stubPath;
2305
+ }
2267
2306
  return createStandaloneStack({
2268
2307
  ...standaloneOpts,
2269
2308
  artifactPath: resolvedArtifact
@@ -6821,6 +6860,8 @@ var KernelManager = class {
6821
6860
  this.maxSize = config.maxSize ?? 32;
6822
6861
  this.ttlMs = config.ttlMs ?? 15 * 60 * 1e3;
6823
6862
  this.logger = config.logger ?? console;
6863
+ this.freshnessProbe = config.freshnessProbe;
6864
+ this.staleCheckIntervalMs = config.staleCheckIntervalMs ?? 1e4;
6824
6865
  }
6825
6866
  /** Returns the currently cached environmentIds (ordered by insertion). */
6826
6867
  keys() {
@@ -6843,8 +6884,31 @@ var KernelManager = class {
6843
6884
  if (this.ttlMs > 0 && Date.now() - existing.lastAccess > this.ttlMs) {
6844
6885
  await this.evict(environmentId);
6845
6886
  } else {
6846
- existing.lastAccess = Date.now();
6847
- return existing.kernel;
6887
+ if (this.freshnessProbe) {
6888
+ const now = Date.now();
6889
+ if (now - existing.lastStaleCheckAt >= this.staleCheckIntervalMs) {
6890
+ existing.lastStaleCheckAt = now;
6891
+ let stale = false;
6892
+ try {
6893
+ stale = await this.freshnessProbe(environmentId, existing.createdAt);
6894
+ } catch (err) {
6895
+ this.logger.warn?.("[KernelManager] freshness probe failed", { environmentId, err });
6896
+ }
6897
+ if (stale) {
6898
+ this.logger.info?.("[KernelManager] kernel evicted by freshness probe", { environmentId });
6899
+ await this.evict(environmentId);
6900
+ } else {
6901
+ existing.lastAccess = Date.now();
6902
+ return existing.kernel;
6903
+ }
6904
+ } else {
6905
+ existing.lastAccess = Date.now();
6906
+ return existing.kernel;
6907
+ }
6908
+ } else {
6909
+ existing.lastAccess = Date.now();
6910
+ return existing.kernel;
6911
+ }
6848
6912
  }
6849
6913
  }
6850
6914
  const inflight = this.pending.get(environmentId);
@@ -6852,7 +6916,7 @@ var KernelManager = class {
6852
6916
  const promise = (async () => {
6853
6917
  const kernel = await this.factory.create(environmentId);
6854
6918
  const now = Date.now();
6855
- this.cache.set(environmentId, { kernel, createdAt: now, lastAccess: now });
6919
+ this.cache.set(environmentId, { kernel, createdAt: now, lastAccess: now, lastStaleCheckAt: now });
6856
6920
  await this.enforceMaxSize();
6857
6921
  return kernel;
6858
6922
  })();
@@ -7020,6 +7084,30 @@ var ArtifactApiClient = class {
7020
7084
  if (!found?.headCommitId) return null;
7021
7085
  return { commitId: String(found.headCommitId), publishedAt: found.headPublishedAt ?? null };
7022
7086
  }
7087
+ /**
7088
+ * Cheap freshness probe — returns the env's `last_published_at`
7089
+ * (and best-effort current commit) without rebuilding the artifact.
7090
+ * Used by `KernelManager` on cache hits to detect when a per-env
7091
+ * kernel has been invalidated by an upstream change (marketplace
7092
+ * install/uninstall, artifact publish) so it can be rebuilt
7093
+ * without waiting for the 15-minute LRU TTL to expire.
7094
+ *
7095
+ * Returns `null` on definitive 404 / unknown env. Errors propagate
7096
+ * (caller decides whether to treat unreachable cloud as fresh or
7097
+ * stale — typically fresh, so a brief outage doesn't churn every
7098
+ * cached kernel).
7099
+ */
7100
+ async getFreshness(environmentId) {
7101
+ const url = `${this.base}/api/v1/cloud/environments/${encodeURIComponent(environmentId)}/freshness`;
7102
+ const res = await this.request(url);
7103
+ if (res === null) return null;
7104
+ const body = res.success === false ? null : res.data ?? res;
7105
+ if (!body || typeof body !== "object") return null;
7106
+ const envId = typeof body.environmentId === "string" ? body.environmentId : environmentId;
7107
+ const lastPublishedAt = typeof body.lastPublishedAt === "string" ? body.lastPublishedAt : null;
7108
+ const commitId = typeof body.commitId === "string" ? body.commitId : null;
7109
+ return { environmentId: envId, lastPublishedAt, commitId };
7110
+ }
7023
7111
  /** Drop cached entries for a project (and any matching hostname). */
7024
7112
  invalidate(environmentId) {
7025
7113
  this.artifactCache.delete(environmentId);
@@ -7771,7 +7859,30 @@ var ArtifactKernelFactory = class {
7771
7859
  };
7772
7860
 
7773
7861
  // src/cloud/auth-proxy-plugin.ts
7862
+ import { createHmac as createHmac3, randomUUID as randomUUID2 } from "crypto";
7774
7863
  var AUTH_PREFIX = "/api/v1/auth";
7864
+ function signSessionCookieValue(rawToken, secret) {
7865
+ const signature = createHmac3("sha256", secret).update(rawToken).digest("base64");
7866
+ return encodeURIComponent(`${rawToken}.${signature}`);
7867
+ }
7868
+ function buildSetCookieHeader(name, encodedValue, attrs, maxAgeSec) {
7869
+ const parts = [`${name}=${encodedValue}`];
7870
+ const a = attrs ?? {};
7871
+ if (a.path) parts.push(`Path=${a.path}`);
7872
+ else parts.push("Path=/");
7873
+ if (Number.isFinite(maxAgeSec) && maxAgeSec > 0) parts.push(`Max-Age=${Math.floor(maxAgeSec)}`);
7874
+ if (a.domain) parts.push(`Domain=${a.domain}`);
7875
+ if (a.sameSite) {
7876
+ const ss = String(a.sameSite);
7877
+ parts.push(`SameSite=${ss.charAt(0).toUpperCase() + ss.slice(1)}`);
7878
+ } else {
7879
+ parts.push("SameSite=Lax");
7880
+ }
7881
+ if (a.secure) parts.push("Secure");
7882
+ if (a.httpOnly !== false) parts.push("HttpOnly");
7883
+ if (a.partitioned) parts.push("Partitioned");
7884
+ return parts.join("; ");
7885
+ }
7775
7886
  function pickHandler(svc) {
7776
7887
  if (!svc) return void 0;
7777
7888
  if (typeof svc.handleRequest === "function") return svc.handleRequest.bind(svc);
@@ -7865,6 +7976,115 @@ var AuthProxyPlugin = class {
7865
7976
  return c.json({ hasOwner: true });
7866
7977
  }
7867
7978
  }
7979
+ if (c.req.method === "POST" && subPath === "sso-handoff-issue") {
7980
+ try {
7981
+ const expected = (process.env.OS_CLOUD_API_KEY ?? "").trim();
7982
+ if (!expected) {
7983
+ return c.json({ error: "sso_handoff_disabled", reason: "OS_CLOUD_API_KEY unset on env runtime" }, 503);
7984
+ }
7985
+ const authz = c.req.header("authorization") ?? "";
7986
+ const provided = authz.toLowerCase().startsWith("bearer ") ? authz.slice(7).trim() : "";
7987
+ if (!provided || provided !== expected) {
7988
+ return c.json({ error: "unauthorized" }, 401);
7989
+ }
7990
+ if (typeof authSvc?.getAuthContext !== "function") {
7991
+ return c.json({ error: "auth_service_unavailable" }, 503);
7992
+ }
7993
+ const handoffAuthCtx = await authSvc.getAuthContext();
7994
+ const internal = handoffAuthCtx?.internalAdapter;
7995
+ if (!internal?.createVerificationValue) {
7996
+ return c.json({ error: "verification_api_unavailable" }, 503);
7997
+ }
7998
+ let body = {};
7999
+ try {
8000
+ body = await c.req.json();
8001
+ } catch {
8002
+ body = {};
8003
+ }
8004
+ const email = String(body?.email ?? "").toLowerCase().trim();
8005
+ if (!email) return c.json({ error: "email_required" }, 400);
8006
+ const name = body?.name == null ? null : String(body.name);
8007
+ const by = body?.by == null ? "service" : String(body.by);
8008
+ const envIdInBody = body?.envId == null ? null : String(body.envId);
8009
+ const handoff = randomUUID2().replace(/-/g, "") + randomUUID2().replace(/-/g, "");
8010
+ const ttlSec = 60;
8011
+ const expiresAt = new Date(Date.now() + ttlSec * 1e3);
8012
+ await internal.createVerificationValue({
8013
+ identifier: `sso-handoff:${handoff}`,
8014
+ value: JSON.stringify({ email, name, by, envId: envIdInBody ?? environmentId }),
8015
+ expiresAt
8016
+ });
8017
+ return c.json({
8018
+ token: handoff,
8019
+ expiresAt: expiresAt.toISOString(),
8020
+ ttlSec
8021
+ });
8022
+ } catch (err) {
8023
+ ctx.logger?.error?.("[AuthProxyPlugin] sso-handoff-issue failed", err instanceof Error ? err : new Error(String(err)));
8024
+ return c.json({ error: "sso_handoff_issue_failed", message: String(err?.message ?? err) }, 500);
8025
+ }
8026
+ }
8027
+ if (c.req.method === "GET" && subPath === "sso-exchange") {
8028
+ try {
8029
+ const token = (url.searchParams.get("token") ?? "").trim();
8030
+ const nextRaw = url.searchParams.get("next") ?? "/";
8031
+ const next = nextRaw.startsWith("/") ? nextRaw : "/";
8032
+ if (!token) return c.text("missing token", 400);
8033
+ if (typeof authSvc?.getAuthContext !== "function") {
8034
+ return c.text("auth service unavailable", 503);
8035
+ }
8036
+ const authCtx = await authSvc.getAuthContext();
8037
+ const internal = authCtx?.internalAdapter;
8038
+ if (!internal?.consumeVerificationValue) {
8039
+ return c.text("verification API unavailable", 503);
8040
+ }
8041
+ const consumed = await internal.consumeVerificationValue(`sso-handoff:${token}`);
8042
+ if (!consumed) return c.text("invalid or expired token", 401);
8043
+ const expiresAt = consumed?.expiresAt ? new Date(consumed.expiresAt).getTime() : 0;
8044
+ if (!expiresAt || expiresAt < Date.now()) return c.text("expired token", 401);
8045
+ let payload = {};
8046
+ try {
8047
+ payload = JSON.parse(String(consumed.value));
8048
+ } catch {
8049
+ payload = { email: String(consumed.value) };
8050
+ }
8051
+ const email = String(payload.email ?? "").toLowerCase().trim();
8052
+ if (!email) return c.text("handoff missing email", 400);
8053
+ const found = await internal.findUserByEmail(email, { includeAccounts: true });
8054
+ let userId = found?.user?.id;
8055
+ let hasCredentialAccount = (found?.accounts ?? []).some((a) => a.providerId === "credential" && a.password);
8056
+ if (!userId) {
8057
+ const created = await internal.createUser({
8058
+ email,
8059
+ name: payload.name ?? email,
8060
+ emailVerified: true
8061
+ });
8062
+ userId = created?.id;
8063
+ hasCredentialAccount = false;
8064
+ }
8065
+ if (!userId) return c.text("failed to provision user", 500);
8066
+ const session = await internal.createSession(userId, false);
8067
+ const rawToken = session?.token;
8068
+ const sessionExpiresAt = session?.expiresAt ? new Date(session.expiresAt) : new Date(Date.now() + 7 * 24 * 3600 * 1e3);
8069
+ if (!rawToken) return c.text("failed to mint session", 500);
8070
+ const secret = authCtx?.secret ?? "";
8071
+ if (!secret) return c.text("auth secret unavailable", 503);
8072
+ const cookieName = authCtx?.authCookies?.sessionToken?.name ?? "better-auth.session_token";
8073
+ const cookieAttrs = authCtx?.authCookies?.sessionToken?.attributes ?? {};
8074
+ const encoded = signSessionCookieValue(rawToken, secret);
8075
+ const maxAgeSec = Math.max(60, Math.floor((sessionExpiresAt.getTime() - Date.now()) / 1e3));
8076
+ const setCookie = buildSetCookieHeader(cookieName, encoded, cookieAttrs, maxAgeSec);
8077
+ const finalNext = hasCredentialAccount ? next : `/_console/system/profile?recovery_needed=true&next=${encodeURIComponent(next)}`;
8078
+ const headers = new Headers();
8079
+ headers.set("Set-Cookie", setCookie);
8080
+ headers.set("Location", finalNext);
8081
+ headers.set("Cache-Control", "no-store");
8082
+ return new Response(null, { status: 302, headers });
8083
+ } catch (err) {
8084
+ ctx.logger?.error?.("[AuthProxyPlugin] sso-exchange failed", err instanceof Error ? err : new Error(String(err)));
8085
+ return c.text(`sso-exchange failed: ${err?.message ?? String(err)}`, 500);
8086
+ }
8087
+ }
7868
8088
  const fn = await resolveAuthHandler(authSvc);
7869
8089
  if (!fn) {
7870
8090
  return c.json({ error: "auth_service_unavailable", environmentId }, 503);
@@ -8047,20 +8267,46 @@ var RuntimeConfigPlugin = class {
8047
8267
  return;
8048
8268
  }
8049
8269
  const rawApp = httpServer.getRawApp();
8050
- const payload = {
8051
- cloudUrl: this.cloudUrl,
8052
- singleEnvironment: this.singleEnvironment,
8053
- features: {
8054
- installLocal: this.installLocal,
8055
- marketplace: true
8270
+ const features = {
8271
+ installLocal: this.installLocal,
8272
+ marketplace: true
8273
+ };
8274
+ let envRegistry = null;
8275
+ try {
8276
+ envRegistry = ctx.getService("env-registry");
8277
+ } catch {
8278
+ }
8279
+ const handler = async (c) => {
8280
+ const rawHost = c.req.header("host") ?? "";
8281
+ const host = rawHost.split(":")[0].toLowerCase().trim();
8282
+ let defaultEnvironmentId;
8283
+ let defaultOrgId;
8284
+ let resolvedSingleEnv = this.singleEnvironment;
8285
+ if (envRegistry && host && typeof envRegistry.resolveHostname === "function") {
8286
+ try {
8287
+ const resolved = await envRegistry.resolveHostname(host);
8288
+ if (resolved?.environmentId) {
8289
+ defaultEnvironmentId = resolved.environmentId;
8290
+ if (resolved.organizationId) defaultOrgId = String(resolved.organizationId);
8291
+ resolvedSingleEnv = true;
8292
+ }
8293
+ } catch {
8294
+ }
8056
8295
  }
8296
+ return c.json({
8297
+ cloudUrl: this.cloudUrl,
8298
+ singleEnvironment: resolvedSingleEnv,
8299
+ defaultOrgId,
8300
+ defaultEnvironmentId,
8301
+ features
8302
+ });
8057
8303
  };
8058
- const handler = (c) => c.json(payload);
8059
8304
  rawApp.get("/api/v1/runtime/config", handler);
8060
8305
  rawApp.get("/api/v1/studio/runtime-config", handler);
8061
8306
  ctx.logger?.info?.("[RuntimeConfigPlugin] mounted /api/v1/runtime/config", {
8062
8307
  cloudUrl: this.cloudUrl || "(empty)",
8063
- installLocal: this.installLocal
8308
+ installLocal: this.installLocal,
8309
+ perHostEnvResolution: !!envRegistry
8064
8310
  });
8065
8311
  });
8066
8312
  };
@@ -8261,7 +8507,21 @@ var ObjectOSEnvironmentPlugin = class {
8261
8507
  factory,
8262
8508
  maxSize: this.config.kernelCacheSize,
8263
8509
  ttlMs: this.config.kernelTtlMs,
8264
- logger: ctx.logger
8510
+ logger: ctx.logger,
8511
+ // Only the HTTP client exposes /freshness; file-mode (CLI dev)
8512
+ // has no upstream to probe.
8513
+ freshnessProbe: this.config.controlPlaneUrl === "file" ? void 0 : async (envId, builtAtMs) => {
8514
+ const fresh = await client.getFreshness(envId);
8515
+ if (!fresh) return false;
8516
+ const t = fresh.lastPublishedAt ? Date.parse(fresh.lastPublishedAt) : NaN;
8517
+ if (!Number.isFinite(t)) return false;
8518
+ if (t <= builtAtMs) return false;
8519
+ try {
8520
+ client.invalidate(envId);
8521
+ } catch {
8522
+ }
8523
+ return true;
8524
+ }
8265
8525
  });
8266
8526
  this.kernelManager = kernelManager;
8267
8527
  ctx.registerService("env-registry", envRegistry);
@@ -8313,7 +8573,7 @@ async function createObjectOSStack(config) {
8313
8573
  }
8314
8574
 
8315
8575
  // src/cloud/marketplace-install-local-plugin.ts
8316
- import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync, readdirSync, unlinkSync, writeFileSync } from "fs";
8576
+ import { existsSync as existsSync2, mkdirSync as mkdirSync3, readFileSync, readdirSync, unlinkSync, writeFileSync as writeFileSync2 } from "fs";
8317
8577
  import { join, resolve } from "path";
8318
8578
  var ROUTE_BASE = "/api/v1/marketplace/install-local";
8319
8579
  var DEFAULT_DIR = ".objectstack/installed-packages";
@@ -8446,8 +8706,8 @@ var MarketplaceInstallLocalPlugin = class {
8446
8706
  installedBy: userId
8447
8707
  };
8448
8708
  try {
8449
- mkdirSync2(this.storageDir, { recursive: true });
8450
- writeFileSync(join(this.storageDir, safeFilename(manifestId)), JSON.stringify(entry, null, 2), "utf8");
8709
+ mkdirSync3(this.storageDir, { recursive: true });
8710
+ writeFileSync2(join(this.storageDir, safeFilename(manifestId)), JSON.stringify(entry, null, 2), "utf8");
8451
8711
  } catch (err) {
8452
8712
  return c.json({
8453
8713
  success: false,
@@ -8866,6 +9126,7 @@ export {
8866
9126
  resolveDefaultArtifactPath,
8867
9127
  resolveErrorReporter,
8868
9128
  resolveMetrics,
9129
+ resolveObjectStackHome,
8869
9130
  resolveRequestId,
8870
9131
  seedPlatformSsoClient
8871
9132
  };