@objectstack/runtime 7.9.0 → 8.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.js CHANGED
@@ -164,7 +164,7 @@ var init_seed_loader = __esm({
164
164
  const config = request.config;
165
165
  const allErrors = [];
166
166
  const allResults = [];
167
- const datasets = this.filterByEnv(request.datasets, config.env);
167
+ const datasets = this.filterByEnv(request.seeds, config.env);
168
168
  if (datasets.length === 0) {
169
169
  return this.buildEmptyResult(config, Date.now() - startTime);
170
170
  }
@@ -234,10 +234,10 @@ var init_seed_loader = __esm({
234
234
  }
235
235
  async validate(datasets, config) {
236
236
  const parsedConfig = SeedLoaderConfigSchema.parse({ ...config, dryRun: true });
237
- return this.load({ datasets, config: parsedConfig });
237
+ return this.load({ seeds: datasets, config: parsedConfig });
238
238
  }
239
239
  // ==========================================================================
240
- // Internal: Dataset Loading
240
+ // Internal: Seed Loading
241
241
  // ==========================================================================
242
242
  async loadDataset(dataset, config, refMap, insertedRecords, deferredUpdates, allErrors) {
243
243
  const objectName = dataset.object;
@@ -1657,7 +1657,7 @@ var init_app_plugin = __esm({
1657
1657
  const seedLoader = new SeedLoaderService(ql, md, loggerRef);
1658
1658
  const { SeedLoaderRequestSchema } = await import("@objectstack/spec/data");
1659
1659
  const request = SeedLoaderRequestSchema.parse({
1660
- datasets: datasetsNow,
1660
+ seeds: datasetsNow,
1661
1661
  config: {
1662
1662
  defaultMode: "upsert",
1663
1663
  multiPass: true,
@@ -1677,7 +1677,7 @@ var init_app_plugin = __esm({
1677
1677
  };
1678
1678
  };
1679
1679
  registerSvc("seed-replayer", replayer);
1680
- ctx.logger.info(`[Seeder] Registered ${normalizedDatasets.length} datasets + replayer on kernel (total datasets: ${merged.length})`);
1680
+ ctx.logger.info(`[Seeder] Registered ${normalizedDatasets.length} datasets + replayer on kernel (total seeds: ${merged.length})`);
1681
1681
  } catch (e) {
1682
1682
  ctx.logger.warn("[Seeder] Failed to register seed-datasets/seed-replayer service", { error: e?.message });
1683
1683
  }
@@ -1693,7 +1693,7 @@ var init_app_plugin = __esm({
1693
1693
  const seedLoader = new SeedLoaderService(ql, metadata, ctx.logger);
1694
1694
  const { SeedLoaderRequestSchema } = await import("@objectstack/spec/data");
1695
1695
  const request = SeedLoaderRequestSchema.parse({
1696
- datasets: normalizedDatasets,
1696
+ seeds: normalizedDatasets,
1697
1697
  config: { defaultMode: "upsert", multiPass: true, identity: seedIdentity }
1698
1698
  });
1699
1699
  const result = await seedLoader.load(request);
@@ -2167,154 +2167,8 @@ var init_standalone_stack = __esm({
2167
2167
  }
2168
2168
  });
2169
2169
 
2170
- // src/cloud/environment-org-seed.ts
2171
- var environment_org_seed_exports = {};
2172
- __export(environment_org_seed_exports, {
2173
- seedProjectMember: () => seedProjectMember,
2174
- seedProjectOrganization: () => seedProjectOrganization
2175
- });
2176
- async function seedProjectOrganization(kernel, seed, logger) {
2177
- if (!seed?.id || !seed?.name) return "skipped";
2178
- try {
2179
- const ql = kernel.getService("objectql");
2180
- if (!ql?.insert || !ql?.find) {
2181
- logger?.warn?.("[seedProjectOrganization] objectql service unavailable", { orgId: seed.id });
2182
- return "skipped";
2183
- }
2184
- try {
2185
- const existing = await ql.find(SYS_ORG, { where: { id: seed.id } });
2186
- const rows = Array.isArray(existing) ? existing : existing?.value ?? [];
2187
- if (Array.isArray(rows) && rows.length > 0) return "exists";
2188
- } catch {
2189
- }
2190
- const nowIso = (/* @__PURE__ */ new Date()).toISOString();
2191
- await ql.insert(SYS_ORG, {
2192
- id: seed.id,
2193
- name: seed.name,
2194
- slug: seed.slug ?? null,
2195
- logo: seed.logo ?? null,
2196
- metadata: null,
2197
- created_at: nowIso
2198
- });
2199
- logger?.info?.("[seedProjectOrganization] org seeded", {
2200
- orgId: seed.id,
2201
- name: seed.name
2202
- });
2203
- return "inserted";
2204
- } catch (err) {
2205
- logger?.warn?.("[seedProjectOrganization] failed (non-fatal)", {
2206
- orgId: seed.id,
2207
- error: err?.message
2208
- });
2209
- return "error";
2210
- }
2211
- }
2212
- async function seedProjectMember(kernel, args, logger) {
2213
- const { userId, organizationId } = args;
2214
- const role = args.role ?? "member";
2215
- if (!userId || !organizationId) return "skipped";
2216
- try {
2217
- const ql = kernel.getService("objectql");
2218
- if (!ql?.insert || !ql?.find) {
2219
- logger?.warn?.("[seedProjectMember] objectql service unavailable", { userId, organizationId });
2220
- return "skipped";
2221
- }
2222
- try {
2223
- const existing = await ql.find("sys_member", {
2224
- where: { user_id: userId, organization_id: organizationId }
2225
- });
2226
- const rows = Array.isArray(existing) ? existing : existing?.value ?? [];
2227
- if (Array.isArray(rows) && rows.length > 0) return "exists";
2228
- } catch {
2229
- }
2230
- const nowIso = (/* @__PURE__ */ new Date()).toISOString();
2231
- const memId = `mem_${Math.random().toString(36).slice(2, 14)}`;
2232
- await ql.insert("sys_member", {
2233
- id: memId,
2234
- organization_id: organizationId,
2235
- user_id: userId,
2236
- role,
2237
- created_at: nowIso
2238
- });
2239
- logger?.info?.("[seedProjectMember] member seeded", {
2240
- userId,
2241
- organizationId,
2242
- role
2243
- });
2244
- return "inserted";
2245
- } catch (err) {
2246
- logger?.warn?.("[seedProjectMember] failed (non-fatal)", {
2247
- userId,
2248
- organizationId,
2249
- error: err?.message
2250
- });
2251
- return "error";
2252
- }
2253
- }
2254
- var SYS_ORG;
2255
- var init_environment_org_seed = __esm({
2256
- "src/cloud/environment-org-seed.ts"() {
2257
- "use strict";
2258
- SYS_ORG = "sys_organization";
2259
- }
2260
- });
2261
-
2262
- // src/cloud/environment-owner-seed.ts
2263
- var environment_owner_seed_exports = {};
2264
- __export(environment_owner_seed_exports, {
2265
- seedProjectOwner: () => seedProjectOwner
2266
- });
2267
- async function seedProjectOwner(kernel, seed, logger) {
2268
- if (!seed?.userId || !seed?.email) return "skipped";
2269
- try {
2270
- const ql = kernel.getService("objectql");
2271
- if (!ql?.insert || !ql?.find) {
2272
- logger?.warn?.("[seedProjectOwner] objectql service unavailable", { userId: seed.userId });
2273
- return "skipped";
2274
- }
2275
- try {
2276
- const existing = await ql.find(SYS_USER, { where: { id: seed.userId } });
2277
- const rows = Array.isArray(existing) ? existing : existing?.value ?? [];
2278
- if (Array.isArray(rows) && rows.length > 0) return "exists";
2279
- } catch {
2280
- }
2281
- const nowIso = (/* @__PURE__ */ new Date()).toISOString();
2282
- await ql.insert(SYS_USER, {
2283
- id: seed.userId,
2284
- email: seed.email,
2285
- name: seed.name ?? seed.email.split("@")[0] ?? "Owner",
2286
- image: seed.image ?? null,
2287
- // Cloud already verified the upstream email. Marking it verified
2288
- // here is what unblocks better-auth's accountLinking check on
2289
- // the first SSO callback (alongside the trustedProviders config
2290
- // in plugin-auth/auth-manager.ts).
2291
- email_verified: true,
2292
- created_at: nowIso,
2293
- updated_at: nowIso
2294
- });
2295
- logger?.info?.("[seedProjectOwner] owner seeded", {
2296
- userId: seed.userId,
2297
- email: seed.email
2298
- });
2299
- return "inserted";
2300
- } catch (err) {
2301
- logger?.warn?.("[seedProjectOwner] failed (non-fatal)", {
2302
- userId: seed.userId,
2303
- error: err?.message
2304
- });
2305
- return "error";
2306
- }
2307
- }
2308
- var SYS_USER;
2309
- var init_environment_owner_seed = __esm({
2310
- "src/cloud/environment-owner-seed.ts"() {
2311
- "use strict";
2312
- SYS_USER = "sys_user";
2313
- }
2314
- });
2315
-
2316
2170
  // src/index.ts
2317
- import { ObjectKernel as ObjectKernel4 } from "@objectstack/core";
2171
+ import { ObjectKernel as ObjectKernel3 } from "@objectstack/core";
2318
2172
 
2319
2173
  // src/runtime.ts
2320
2174
  import { ObjectKernel } from "@objectstack/core";
@@ -2596,30 +2450,18 @@ import { getEnv, resolveLocale } from "@objectstack/core";
2596
2450
  import { CoreServiceName } from "@objectstack/spec/system";
2597
2451
  import { pluralToSingular, PLURAL_TO_SINGULAR } from "@objectstack/spec/shared";
2598
2452
 
2453
+ // src/security/api-key.ts
2454
+ import {
2455
+ API_KEY_PREFIX,
2456
+ hashApiKey,
2457
+ generateApiKey,
2458
+ extractApiKey,
2459
+ parseScopes,
2460
+ isExpired,
2461
+ resolveApiKeyPrincipal
2462
+ } from "@objectstack/core";
2463
+
2599
2464
  // src/security/resolve-execution-context.ts
2600
- function readHeader(headers, name) {
2601
- if (!headers) return void 0;
2602
- const lower = name.toLowerCase();
2603
- if (typeof headers.get === "function") {
2604
- const v = headers.get(name) ?? headers.get(lower);
2605
- return v == null ? void 0 : String(v);
2606
- }
2607
- for (const key of Object.keys(headers)) {
2608
- if (key.toLowerCase() === lower) {
2609
- const v = headers[key];
2610
- return Array.isArray(v) ? v[0] : v == null ? void 0 : String(v);
2611
- }
2612
- }
2613
- return void 0;
2614
- }
2615
- function extractApiKey(headers) {
2616
- const x = readHeader(headers, "x-api-key");
2617
- if (x) return x.trim();
2618
- const auth = readHeader(headers, "authorization");
2619
- if (!auth) return void 0;
2620
- const m = auth.match(/^ApiKey\s+(.+)$/i);
2621
- return m ? m[1].trim() : void 0;
2622
- }
2623
2465
  function toHeaders(input) {
2624
2466
  if (!input) return new Headers();
2625
2467
  if (typeof Headers !== "undefined" && input instanceof Headers) return input;
@@ -2662,34 +2504,12 @@ async function resolveExecutionContext(opts) {
2662
2504
  };
2663
2505
  let userId;
2664
2506
  let tenantId;
2665
- const apiKey = extractApiKey(headers);
2666
- if (apiKey) {
2667
- try {
2668
- const authService = await opts.getService("auth");
2669
- const verify = authService?.api?.verifyApiKey ?? authService?.api?.apiKey?.verify;
2670
- if (typeof verify === "function") {
2671
- const res = await verify({ body: { key: apiKey } });
2672
- const payload = res?.key ?? res;
2673
- if (payload?.userId) userId = payload.userId;
2674
- if (payload?.organizationId) tenantId = payload.organizationId;
2675
- if (Array.isArray(payload?.permissions)) {
2676
- ctx.permissions.push(...payload.permissions);
2677
- }
2678
- if (Array.isArray(payload?.scopes)) {
2679
- ctx.permissions.push(...payload.scopes);
2680
- }
2681
- }
2682
- } catch {
2683
- }
2684
- if (!userId) {
2685
- const ql2 = await opts.getQl();
2686
- const rows = await tryFind(ql2, "sys_api_key", { key: apiKey, active: true }, 1);
2687
- const row = rows[0];
2688
- if (row) {
2689
- userId = row.user_id ?? row.userId;
2690
- tenantId = row.organization_id ?? row.organizationId;
2691
- if (Array.isArray(row.scopes)) ctx.permissions.push(...row.scopes);
2692
- }
2507
+ const keyPrincipal = await resolveApiKeyPrincipal(await opts.getQl(), headers);
2508
+ if (keyPrincipal) {
2509
+ userId = keyPrincipal.userId;
2510
+ tenantId = keyPrincipal.tenantId;
2511
+ for (const scope of keyPrincipal.scopes) {
2512
+ if (!ctx.permissions.includes(scope)) ctx.permissions.push(scope);
2693
2513
  }
2694
2514
  }
2695
2515
  if (!userId) {
@@ -2965,6 +2785,253 @@ var _HttpDispatcher = class _HttpDispatcher {
2965
2785
  }
2966
2786
  throw { statusCode: 400, message: `Unknown data action: ${action}` };
2967
2787
  }
2788
+ /**
2789
+ * Handle an MCP request over the Streamable HTTP transport (`/mcp`).
2790
+ *
2791
+ * Gating + auth (fail-closed):
2792
+ * - **opt-in**: only served when `OS_MCP_SERVER_ENABLED=true` (single-env
2793
+ * runtime). Multi-tenant cloud overrides this gate per env. When off we
2794
+ * return 404 so the surface isn't advertised.
2795
+ * - **auth**: requires a principal already resolved by
2796
+ * `resolveExecutionContext` (the `sys_api_key` Bearer/header path or a
2797
+ * session). Anonymous → 401.
2798
+ *
2799
+ * Execution: the MCP runtime builds a stateless per-request server whose
2800
+ * object-CRUD tools run through {@link callData} bound to THIS request's
2801
+ * ExecutionContext — i.e. the exact permission + RLS path the REST API
2802
+ * uses. An external agent can never exceed the key's authority.
2803
+ */
2804
+ async handleMcp(body, context) {
2805
+ if (!_HttpDispatcher.isMcpEnabled()) {
2806
+ return { handled: true, response: this.error("MCP server is not enabled for this environment", 404) };
2807
+ }
2808
+ const mcp = await this.resolveService("mcp", context.environmentId);
2809
+ if (!mcp || typeof mcp.handleHttpRequest !== "function") {
2810
+ return { handled: true, response: this.error("MCP server is not available", 501) };
2811
+ }
2812
+ const ec = context.executionContext;
2813
+ if (!ec || !ec.userId && !ec.isSystem) {
2814
+ return { handled: true, response: this.error("Unauthorized: a valid API key is required", 401) };
2815
+ }
2816
+ const webRequest = this.toMcpWebRequest(context.request, body);
2817
+ if (!webRequest) {
2818
+ return { handled: true, response: this.error("MCP transport requires a standard HTTP request", 400) };
2819
+ }
2820
+ const bridge = this.buildMcpBridge(context);
2821
+ let webRes;
2822
+ try {
2823
+ webRes = await mcp.handleHttpRequest(webRequest, { bridge, parsedBody: body });
2824
+ } catch (err) {
2825
+ return { handled: true, response: this.error(err?.message ?? "MCP request failed", 500) };
2826
+ }
2827
+ const headers = {};
2828
+ try {
2829
+ webRes.headers.forEach((v, k) => {
2830
+ headers[k] = v;
2831
+ });
2832
+ } catch {
2833
+ }
2834
+ const text = await webRes.text().catch(() => "");
2835
+ let responseBody = null;
2836
+ if (text) {
2837
+ const ct = headers["content-type"] ?? "";
2838
+ if (ct.includes("application/json")) {
2839
+ try {
2840
+ responseBody = JSON.parse(text);
2841
+ } catch {
2842
+ responseBody = text;
2843
+ }
2844
+ } else {
2845
+ responseBody = text;
2846
+ }
2847
+ }
2848
+ return { handled: true, response: { status: webRes.status, headers, body: responseBody } };
2849
+ }
2850
+ /** Whether the MCP HTTP surface is opted in for this single-env runtime. */
2851
+ static isMcpEnabled() {
2852
+ return typeof process !== "undefined" && process.env?.OS_MCP_SERVER_ENABLED === "true";
2853
+ }
2854
+ /**
2855
+ * Normalise the inbound request into a Web-standard `Request` for the MCP
2856
+ * transport. Accepts an already-Web `Request`, or a node/Hono-style req
2857
+ * (plain `headers` object, path-only `url`). Returns undefined only if the
2858
+ * shape is unusable. The body is carried separately via `parsedBody`, so a
2859
+ * GET/DELETE (no body) and a POST (JSON-RPC) both normalise cleanly.
2860
+ */
2861
+ toMcpWebRequest(raw, parsedBody) {
2862
+ if (!raw) return void 0;
2863
+ if (typeof raw.headers?.get === "function" && typeof raw.url === "string" && typeof raw.method === "string") {
2864
+ return raw;
2865
+ }
2866
+ try {
2867
+ const method = String(raw.method ?? "POST").toUpperCase();
2868
+ const headers = new Headers();
2869
+ const h = raw.headers;
2870
+ if (h) {
2871
+ if (typeof h.forEach === "function") {
2872
+ h.forEach((v, k) => {
2873
+ if (v != null) headers.set(String(k), String(v));
2874
+ });
2875
+ } else {
2876
+ for (const k of Object.keys(h)) {
2877
+ const v = h[k];
2878
+ if (v != null) headers.set(k, Array.isArray(v) ? v.join(",") : String(v));
2879
+ }
2880
+ }
2881
+ }
2882
+ let url;
2883
+ try {
2884
+ url = new URL(String(raw.url)).toString();
2885
+ } catch {
2886
+ const host = headers.get("host") || "mcp.local";
2887
+ const path = typeof raw.url === "string" && raw.url ? raw.url : "/api/v1/mcp";
2888
+ url = `https://${host}${path.startsWith("/") ? path : `/${path}`}`;
2889
+ }
2890
+ const init = { method, headers };
2891
+ if (method !== "GET" && method !== "HEAD" && method !== "DELETE") {
2892
+ init.body = typeof parsedBody === "string" ? parsedBody : JSON.stringify(parsedBody ?? {});
2893
+ }
2894
+ return new Request(url, init);
2895
+ } catch {
2896
+ return void 0;
2897
+ }
2898
+ }
2899
+ /**
2900
+ * Build a principal-bound {@link McpDataBridge}: every method runs AS the
2901
+ * request's ExecutionContext through {@link callData} (RLS/permissions) and
2902
+ * the per-env metadata service. Keeps the MCP tool layer free of any direct
2903
+ * engine access.
2904
+ */
2905
+ buildMcpBridge(context) {
2906
+ const ec = context.executionContext;
2907
+ const envId = context.environmentId;
2908
+ const driver = context.dataDriver;
2909
+ const callData = this.callData.bind(this);
2910
+ const getMeta = () => this.resolveService("metadata", envId);
2911
+ return {
2912
+ listObjects: async () => {
2913
+ const meta = await getMeta();
2914
+ const objs = await meta?.listObjects?.() ?? [];
2915
+ return objs.map((o) => ({
2916
+ name: o.name,
2917
+ label: o.label ?? o.name,
2918
+ fieldCount: o.fields ? Object.keys(o.fields).length : void 0
2919
+ }));
2920
+ },
2921
+ describeObject: async (name) => {
2922
+ const meta = await getMeta();
2923
+ const def = await meta?.getObject?.(name);
2924
+ if (!def) return null;
2925
+ const fields = def.fields ?? {};
2926
+ return {
2927
+ name: def.name,
2928
+ label: def.label ?? def.name,
2929
+ fields: Object.entries(fields).map(([k, f]) => ({
2930
+ name: k,
2931
+ type: f?.type,
2932
+ label: f?.label ?? k,
2933
+ required: f?.required ?? false
2934
+ })),
2935
+ enableFeatures: def.enable ?? {}
2936
+ };
2937
+ },
2938
+ query: async (object, o) => {
2939
+ const query = {};
2940
+ if (o?.where) query.where = o.where;
2941
+ if (o?.fields) query.fields = o.fields;
2942
+ if (typeof o?.limit === "number") query.limit = o.limit;
2943
+ if (typeof o?.offset === "number") query.offset = o.offset;
2944
+ if (o?.orderBy) query.orderBy = o.orderBy;
2945
+ return await callData("query", { object, query }, driver, envId, ec);
2946
+ },
2947
+ get: async (object, id) => {
2948
+ const res = await callData("get", { object, id }, driver, envId, ec);
2949
+ return res?.record ?? res ?? null;
2950
+ },
2951
+ create: async (object, data) => await callData("create", { object, data }, driver, envId, ec),
2952
+ update: async (object, id, data) => await callData("update", { object, id, data }, driver, envId, ec),
2953
+ remove: async (object, id) => await callData("delete", { object, id }, driver, envId, ec)
2954
+ };
2955
+ }
2956
+ /**
2957
+ * Generate a `sys_api_key` and return the raw secret EXACTLY ONCE
2958
+ * (`POST /keys`). This is the only mint path — the raw key is never stored
2959
+ * (only its sha256 hash) and never re-displayable.
2960
+ *
2961
+ * Security (zero-tolerance):
2962
+ * - Requires an authenticated principal; `user_id` is PINNED to that
2963
+ * caller and is NEVER read from the request body (no impersonation).
2964
+ * - Body is whitelisted to `name` (+ optional `expires_at`); any
2965
+ * `key` / `id` / `user_id` / `revoked` in the body is ignored, so a
2966
+ * caller cannot forge a known-secret or escalate.
2967
+ * - `scopes` are intentionally NOT accepted from the body in v1: the
2968
+ * verify path ADDS scopes to the principal's permissions, so honouring
2969
+ * arbitrary body scopes would be an escalation vector. A generated key
2970
+ * therefore acts exactly AS the caller (via `user_id` resolution).
2971
+ * Narrowing/scoped keys need subset-enforcement — deferred.
2972
+ * - The raw key and its hash never enter logs or error messages.
2973
+ * - The row is written with an elevated `{ isSystem: true }` context
2974
+ * because `sys_api_key` is protection-locked; safe because the row's
2975
+ * contents are fully server-controlled (user_id pinned to caller).
2976
+ */
2977
+ async handleKeys(method, body, context) {
2978
+ if (method !== "POST") {
2979
+ return { handled: true, response: this.error("Method not allowed", 405) };
2980
+ }
2981
+ const ec = context.executionContext;
2982
+ if (!ec || !ec.userId) {
2983
+ return { handled: true, response: this.error("Unauthorized: sign in to generate an API key", 401) };
2984
+ }
2985
+ const rawName = typeof body?.name === "string" ? body.name.trim() : "";
2986
+ const name = rawName || "API Key";
2987
+ let expiresAt;
2988
+ if (body?.expires_at != null && body.expires_at !== "") {
2989
+ const ms = typeof body.expires_at === "number" ? body.expires_at < 1e12 ? body.expires_at * 1e3 : body.expires_at : Date.parse(String(body.expires_at));
2990
+ if (Number.isNaN(ms)) {
2991
+ return { handled: true, response: this.error("Invalid expires_at: must be a parseable date", 400) };
2992
+ }
2993
+ if (ms <= Date.now()) {
2994
+ return { handled: true, response: this.error("Invalid expires_at: must be in the future", 400) };
2995
+ }
2996
+ expiresAt = new Date(ms).toISOString();
2997
+ }
2998
+ const ql = await this.getObjectQLService(context.environmentId) ?? await this.resolveService("objectql", context.environmentId);
2999
+ if (!ql || typeof ql.insert !== "function") {
3000
+ return { handled: true, response: this.error("Data service not available", 503) };
3001
+ }
3002
+ const generated = generateApiKey();
3003
+ const row = {
3004
+ name,
3005
+ key: generated.hash,
3006
+ prefix: generated.prefix,
3007
+ user_id: ec.userId,
3008
+ revoked: false
3009
+ };
3010
+ if (expiresAt) row.expires_at = expiresAt;
3011
+ let inserted;
3012
+ try {
3013
+ inserted = await ql.insert("sys_api_key", row, { context: { isSystem: true } });
3014
+ } catch {
3015
+ return { handled: true, response: this.error("Failed to create API key", 500) };
3016
+ }
3017
+ const id = inserted?.id ?? (Array.isArray(inserted) ? inserted[0]?.id : void 0);
3018
+ return {
3019
+ handled: true,
3020
+ response: {
3021
+ status: 201,
3022
+ body: {
3023
+ success: true,
3024
+ data: {
3025
+ id,
3026
+ name,
3027
+ prefix: generated.prefix,
3028
+ key: generated.raw,
3029
+ ...expiresAt ? { expires_at: expiresAt } : {}
3030
+ }
3031
+ }
3032
+ }
3033
+ };
3034
+ }
2968
3035
  /**
2969
3036
  * Parse a project UUID out of a scoped URL path such as
2970
3037
  * `/api/v1/environments/abc-123/data/task` or `/projects/abc-123/meta`.
@@ -3251,7 +3318,11 @@ var _HttpDispatcher = class _HttpDispatcher {
3251
3318
  realtime: hasWebSockets ? `${prefix}/realtime` : void 0,
3252
3319
  notifications: hasNotification ? `${prefix}/notifications` : void 0,
3253
3320
  ai: hasAi ? `${prefix}/ai` : void 0,
3254
- i18n: hasI18n ? `${prefix}/i18n` : void 0
3321
+ i18n: hasI18n ? `${prefix}/i18n` : void 0,
3322
+ // MCP (Streamable HTTP) is opt-in per env — only advertised
3323
+ // when OS_MCP_SERVER_ENABLED=true so the surface isn't exposed
3324
+ // by default. The objectui Integrations page reads this.
3325
+ mcp: _HttpDispatcher.isMcpEnabled() ? `${prefix}/mcp` : void 0
3255
3326
  };
3256
3327
  const svcAvailable = (route, provider) => ({
3257
3328
  enabled: true,
@@ -3905,6 +3976,18 @@ var _HttpDispatcher = class _HttpDispatcher {
3905
3976
  ...organizationId ? { organizationId } : {},
3906
3977
  ...body?.actor ? { actor: body.actor } : {}
3907
3978
  });
3979
+ try {
3980
+ const seedNames = (result?.published ?? []).filter((p) => p?.type === "seed").map((p) => p.name);
3981
+ if (seedNames.length > 0) {
3982
+ result.seedApplied = await this.applyPublishedSeeds(
3983
+ seedNames,
3984
+ organizationId,
3985
+ _context
3986
+ );
3987
+ }
3988
+ } catch (e) {
3989
+ result.seedApplied = { success: false, error: e?.message ?? "seed apply failed" };
3990
+ }
3908
3991
  return { handled: true, response: this.success(result) };
3909
3992
  } catch (e) {
3910
3993
  return { handled: true, response: this.error(e.message, e.statusCode || 500) };
@@ -4097,6 +4180,66 @@ var _HttpDispatcher = class _HttpDispatcher {
4097
4180
  * Physical database addressing (database_url, database_driver, etc.)
4098
4181
  * is stored directly on the sys_environment row.
4099
4182
  */
4183
+ /**
4184
+ * Apply just-published `seed` metadata: load each seed's rows into its
4185
+ * target object so publishing a seed draft makes the data live (the runtime
4186
+ * counterpart to staging it). Reads each seed body via the protocol, then
4187
+ * runs the {@link SeedLoaderService} for the active org. Best-effort and
4188
+ * idempotent (upsert) — callers must never let this fail the publish.
4189
+ *
4190
+ * Lives at the runtime layer (not in the objectql publish primitive)
4191
+ * because the seed loader needs the data engine + metadata service, which
4192
+ * objectql cannot depend on without a layering cycle.
4193
+ */
4194
+ async applyPublishedSeeds(names, organizationId, _context) {
4195
+ const protocol = await this.resolveService("protocol");
4196
+ const metadata = await this.getService(CoreServiceName.enum.metadata);
4197
+ const ql = await this.resolveService("objectql");
4198
+ if (!protocol || typeof protocol.getMetaItem !== "function" || !ql || !metadata) {
4199
+ return { success: false, error: "seed apply: required services unavailable" };
4200
+ }
4201
+ const datasets = [];
4202
+ const readErrors = [];
4203
+ for (const name of names) {
4204
+ const attempts = organizationId ? [{ type: "seed", name, organizationId }, { type: "seed", name }] : [{ type: "seed", name }];
4205
+ let item;
4206
+ for (const args of attempts) {
4207
+ try {
4208
+ item = await protocol.getMetaItem(args);
4209
+ if (item) break;
4210
+ } catch (e) {
4211
+ readErrors.push(`read ${name}: ${e?.message ?? String(e)}`);
4212
+ }
4213
+ }
4214
+ const seed = item?.object && Array.isArray(item?.records) ? item : item?.item ?? item?.metadata ?? item?.body;
4215
+ if (seed?.object && Array.isArray(seed?.records)) {
4216
+ datasets.push(seed);
4217
+ } else {
4218
+ readErrors.push(`seed "${name}" body unreadable (keys: ${item ? Object.keys(item).join(",") : "none"})`);
4219
+ }
4220
+ }
4221
+ if (datasets.length === 0) {
4222
+ return { success: false, inserted: 0, updated: 0, error: "seed apply: no readable seed bodies", errors: readErrors };
4223
+ }
4224
+ const { SeedLoaderService: SeedLoaderService2 } = await Promise.resolve().then(() => (init_seed_loader(), seed_loader_exports));
4225
+ const { SeedLoaderRequestSchema } = await import("@objectstack/spec/data");
4226
+ const loader = new SeedLoaderService2(ql, metadata, this.logger ?? console);
4227
+ const request = SeedLoaderRequestSchema.parse({
4228
+ seeds: datasets,
4229
+ config: {
4230
+ defaultMode: "upsert",
4231
+ multiPass: true,
4232
+ ...organizationId ? { organizationId } : {}
4233
+ }
4234
+ });
4235
+ const r = await loader.load(request);
4236
+ return {
4237
+ success: r.success,
4238
+ inserted: r.summary.totalInserted,
4239
+ updated: r.summary.totalUpdated,
4240
+ errors: [...readErrors, ...r.errors ?? []]
4241
+ };
4242
+ }
4100
4243
  /**
4101
4244
  * Resolve the calling user id from the request session, if any.
4102
4245
  * Returns `undefined` for anonymous calls or when auth is not wired up.
@@ -4681,6 +4824,12 @@ var _HttpDispatcher = class _HttpDispatcher {
4681
4824
  if (cleanPath.startsWith("/data")) {
4682
4825
  return this.handleData(cleanPath.substring(5), method, body, query, context);
4683
4826
  }
4827
+ if (cleanPath === "/mcp" || cleanPath.startsWith("/mcp/") || cleanPath.startsWith("/mcp?")) {
4828
+ return this.handleMcp(body, context);
4829
+ }
4830
+ if (cleanPath === "/keys" || cleanPath.startsWith("/keys/") || cleanPath.startsWith("/keys?")) {
4831
+ return this.handleKeys(method, body, context);
4832
+ }
4684
4833
  if (cleanPath.startsWith("/graphql")) {
4685
4834
  if (method === "POST") return this.handleGraphQL(body, context);
4686
4835
  }
@@ -5393,6 +5542,28 @@ function createDispatcherPlugin(config = {}) {
5393
5542
  errorResponse(err, res);
5394
5543
  }
5395
5544
  });
5545
+ const mountMcp = (method) => {
5546
+ const register = method === "GET" ? server.get : method === "DELETE" ? server.delete : server.post;
5547
+ register.call(server, `${prefix}/mcp`, async (req, res) => {
5548
+ try {
5549
+ const result = await dispatcher.dispatch(method, "/mcp", req.body, req.query, { request: req });
5550
+ sendResult(result, res);
5551
+ } catch (err) {
5552
+ errorResponse(err, res);
5553
+ }
5554
+ });
5555
+ };
5556
+ mountMcp("POST");
5557
+ mountMcp("GET");
5558
+ mountMcp("DELETE");
5559
+ server.post(`${prefix}/keys`, async (req, res) => {
5560
+ try {
5561
+ const result = await dispatcher.dispatch("POST", "/keys", req.body, req.query, { request: req });
5562
+ sendResult(result, res);
5563
+ } catch (err) {
5564
+ errorResponse(err, res);
5565
+ }
5566
+ });
5396
5567
  server.get(`${prefix}/packages`, async (req, res) => {
5397
5568
  try {
5398
5569
  const result = await dispatcher.handlePackages("", "GET", {}, req.query, { request: req });
@@ -6038,1624 +6209,63 @@ var MiddlewareManager = class {
6038
6209
  // src/index.ts
6039
6210
  init_load_artifact_bundle();
6040
6211
 
6041
- // src/cloud/kernel-manager.ts
6042
- var KernelManager = class {
6043
- constructor(config) {
6044
- this.cache = /* @__PURE__ */ new Map();
6045
- this.pending = /* @__PURE__ */ new Map();
6046
- this.factory = config.factory;
6047
- this.maxSize = config.maxSize ?? 32;
6048
- this.ttlMs = config.ttlMs ?? 15 * 60 * 1e3;
6049
- this.logger = config.logger ?? console;
6050
- this.freshnessProbe = config.freshnessProbe;
6051
- this.staleCheckIntervalMs = config.staleCheckIntervalMs ?? 1e4;
6052
- }
6053
- /** Returns the currently cached environmentIds (ordered by insertion). */
6054
- keys() {
6055
- return Array.from(this.cache.keys());
6056
- }
6057
- /** Cache size for diagnostics. */
6058
- get size() {
6059
- return this.cache.size;
6060
- }
6061
- /**
6062
- * Resolve or construct the kernel for `environmentId`.
6063
- *
6064
- * - Cache hit (fresh): bumps `lastAccess` and returns immediately.
6065
- * - Cache hit (TTL expired): evicts then falls through to factory.
6066
- * - Cache miss: dedupes concurrent callers through `pending`.
6067
- */
6068
- async getOrCreate(environmentId) {
6069
- const existing = this.cache.get(environmentId);
6070
- if (existing) {
6071
- if (this.ttlMs > 0 && Date.now() - existing.lastAccess > this.ttlMs) {
6072
- await this.evict(environmentId);
6073
- } else {
6074
- if (this.freshnessProbe) {
6075
- const now = Date.now();
6076
- if (now - existing.lastStaleCheckAt >= this.staleCheckIntervalMs) {
6077
- existing.lastStaleCheckAt = now;
6078
- let stale = false;
6079
- try {
6080
- stale = await this.freshnessProbe(environmentId, existing.createdAt);
6081
- } catch (err) {
6082
- this.logger.warn?.("[KernelManager] freshness probe failed", { environmentId, err });
6083
- }
6084
- if (stale) {
6085
- this.logger.info?.("[KernelManager] kernel evicted by freshness probe", { environmentId });
6086
- await this.evict(environmentId);
6087
- } else {
6088
- existing.lastAccess = Date.now();
6089
- return existing.kernel;
6090
- }
6091
- } else {
6092
- existing.lastAccess = Date.now();
6093
- return existing.kernel;
6094
- }
6095
- } else {
6096
- existing.lastAccess = Date.now();
6097
- return existing.kernel;
6098
- }
6099
- }
6100
- }
6101
- const inflight = this.pending.get(environmentId);
6102
- if (inflight) return inflight;
6103
- const promise = (async () => {
6104
- const kernel = await this.factory.create(environmentId);
6105
- const now = Date.now();
6106
- this.cache.set(environmentId, { kernel, createdAt: now, lastAccess: now, lastStaleCheckAt: now });
6107
- await this.enforceMaxSize();
6108
- return kernel;
6109
- })();
6110
- this.pending.set(environmentId, promise);
6111
- try {
6112
- return await promise;
6113
- } finally {
6114
- this.pending.delete(environmentId);
6115
- }
6212
+ // src/cloud/cloud-url.ts
6213
+ var DEFAULT_CLOUD_URL = "https://cloud.objectos.ai";
6214
+ function resolveCloudUrl(explicit) {
6215
+ const raw = (explicit ?? process.env.OS_CLOUD_URL ?? "").trim();
6216
+ const lower = raw.toLowerCase();
6217
+ if (lower === "off" || lower === "none" || lower === "local" || lower === "disabled") {
6218
+ return "";
6116
6219
  }
6117
- /**
6118
- * Evict the kernel for `environmentId` and invoke `kernel.shutdown()`.
6119
- * No-op when the entry is absent.
6120
- */
6121
- async evict(environmentId) {
6122
- const entry = this.cache.get(environmentId);
6123
- if (!entry) return;
6124
- this.cache.delete(environmentId);
6125
- try {
6126
- await entry.kernel.shutdown();
6127
- } catch (err) {
6128
- this.logger.error?.("[KernelManager] shutdown failed", { environmentId, err });
6129
- }
6220
+ const picked = raw || DEFAULT_CLOUD_URL;
6221
+ return picked.replace(/\/+$/, "");
6222
+ }
6223
+
6224
+ // src/cloud/marketplace-public-url.ts
6225
+ function resolveMarketplacePublicBaseUrl(explicit) {
6226
+ const raw = (explicit ?? process.env.OS_MARKETPLACE_PUBLIC_BASE_URL ?? "").trim();
6227
+ const lower = raw.toLowerCase();
6228
+ if (!raw || lower === "off" || lower === "none" || lower === "disabled" || lower === "false") {
6229
+ return "";
6130
6230
  }
6131
- /** Evict all resident kernels. Used on runtime shutdown. */
6132
- async evictAll() {
6133
- const ids = Array.from(this.cache.keys());
6134
- await Promise.all(ids.map((id) => this.evict(id)));
6231
+ return raw.replace(/\/+$/, "");
6232
+ }
6233
+ function publicMarketplaceKeyForApiPath(pathname) {
6234
+ const prefix = "/api/v1/marketplace/packages";
6235
+ if (pathname === prefix) return "packages.json";
6236
+ if (!pathname.startsWith(`${prefix}/`)) return null;
6237
+ const tail = pathname.slice(prefix.length + 1);
6238
+ if (!tail) return null;
6239
+ const parts = tail.split("/");
6240
+ if (parts.length === 1) {
6241
+ const id = decodeURIComponent(parts[0] ?? "");
6242
+ if (!id) return null;
6243
+ return `packages/${encodeURIComponent(id)}.json`;
6135
6244
  }
6136
- async enforceMaxSize() {
6137
- while (this.cache.size > this.maxSize) {
6138
- let oldestKey;
6139
- let oldestAccess = Infinity;
6140
- for (const [key, entry] of this.cache) {
6141
- if (entry.lastAccess < oldestAccess) {
6142
- oldestAccess = entry.lastAccess;
6143
- oldestKey = key;
6144
- }
6145
- }
6146
- if (!oldestKey) return;
6147
- await this.evict(oldestKey);
6148
- }
6245
+ if (parts.length === 4 && parts[1] === "versions" && parts[3] === "manifest") {
6246
+ const id = decodeURIComponent(parts[0] ?? "");
6247
+ const versionId = decodeURIComponent(parts[2] ?? "");
6248
+ if (!id || !versionId) return null;
6249
+ return `packages/${encodeURIComponent(id)}/versions/${encodeURIComponent(versionId)}/manifest.json`;
6149
6250
  }
6150
- };
6251
+ return null;
6252
+ }
6151
6253
 
6152
- // src/cloud/artifact-api-client.ts
6153
- var ArtifactApiClient = class {
6154
- constructor(config) {
6155
- this.hostnameCache = /* @__PURE__ */ new Map();
6156
- this.artifactCache = /* @__PURE__ */ new Map();
6157
- this.pendingHostname = /* @__PURE__ */ new Map();
6158
- this.pendingArtifact = /* @__PURE__ */ new Map();
6159
- if (!config.controlPlaneUrl) {
6160
- throw new Error("[ArtifactApiClient] controlPlaneUrl is required");
6161
- }
6162
- this.base = config.controlPlaneUrl.replace(/\/+$/, "");
6163
- this.apiKey = config.apiKey;
6164
- this.cacheTtlMs = config.cacheTtlMs ?? 5 * 60 * 1e3;
6165
- this.requestTimeoutMs = config.requestTimeoutMs ?? 1e4;
6166
- this.fetchImpl = config.fetch ?? globalThis.fetch;
6167
- this.logger = config.logger ?? console;
6168
- if (typeof this.fetchImpl !== "function") {
6169
- throw new Error("[ArtifactApiClient] global fetch is not available \u2014 provide config.fetch");
6170
- }
6171
- }
6172
- /**
6173
- * Resolve a hostname to its project. Returns `null` on 404 or
6174
- * malformed responses. Errors (network / 5xx) are thrown so
6175
- * upstream callers can retry.
6176
- */
6177
- async resolveHostname(host) {
6178
- const cached = this.hostnameCache.get(host);
6179
- if (cached && cached.expiresAt > Date.now()) return cached.value;
6180
- const inflight = this.pendingHostname.get(host);
6181
- if (inflight) return inflight;
6182
- const promise = (async () => {
6183
- try {
6184
- const url = `${this.base}/api/v1/cloud/resolve-hostname?host=${encodeURIComponent(host)}`;
6185
- const res = await this.request(url);
6186
- if (res === null) return null;
6187
- const body = res.success === false ? null : res.data ?? res;
6188
- if (!body || typeof body.environmentId !== "string" || !body.environmentId) return null;
6189
- const value = {
6190
- environmentId: body.environmentId,
6191
- organizationId: body.organizationId,
6192
- runtime: body.runtime
6193
- };
6194
- this.hostnameCache.set(host, { value, expiresAt: Date.now() + this.cacheTtlMs });
6195
- return value;
6196
- } finally {
6197
- this.pendingHostname.delete(host);
6198
- }
6199
- })();
6200
- this.pendingHostname.set(host, promise);
6201
- return promise;
6202
- }
6203
- /**
6204
- * Fetch the compiled artifact for a project.
6205
- *
6206
- * When `opts.commit` is set, requests that specific revision via the
6207
- * existing `?commit=` query param. Different commits are cached
6208
- * independently (the cache key includes the commit id) so the preview
6209
- * runtime can hold multiple versions in memory simultaneously.
6210
- */
6211
- async fetchArtifact(environmentId, opts) {
6212
- const commit = opts?.commit?.trim() || "";
6213
- const cacheKey = commit ? `${environmentId}@${commit}` : environmentId;
6214
- const cached = this.artifactCache.get(cacheKey);
6215
- if (cached && cached.expiresAt > Date.now()) return cached.value;
6216
- const inflight = this.pendingArtifact.get(cacheKey);
6217
- if (inflight) return inflight;
6218
- const promise = (async () => {
6219
- try {
6220
- const qs = commit ? `?commit=${encodeURIComponent(commit)}` : "";
6221
- const url = `${this.base}/api/v1/cloud/environments/${encodeURIComponent(environmentId)}/artifact${qs}`;
6222
- const res = await this.request(url);
6223
- if (res === null) return null;
6224
- const body = res.success === false ? null : res.data ?? res;
6225
- if (!body || typeof body !== "object") return null;
6226
- if (!body.metadata) {
6227
- this.logger.warn?.("[ArtifactApiClient] artifact response missing `metadata`", { environmentId, commit });
6228
- return null;
6229
- }
6230
- const value = body;
6231
- this.artifactCache.set(cacheKey, { value, expiresAt: Date.now() + this.cacheTtlMs });
6232
- return value;
6233
- } finally {
6234
- this.pendingArtifact.delete(cacheKey);
6235
- }
6236
- })();
6237
- this.pendingArtifact.set(cacheKey, promise);
6238
- return promise;
6239
- }
6240
- /**
6241
- * Resolve an 8-hex project short id (first 8 hex chars of the UUID,
6242
- * dashes stripped) to the full environmentId. Used by the preview
6243
- * runtime, which encodes project ids in subdomains.
6244
- *
6245
- * Returns `null` on 404 or ambiguity (the control plane returns 409
6246
- * if the prefix matches more than one project).
6247
- */
6248
- async lookupProjectByShortId(shortId) {
6249
- const short = String(shortId ?? "").trim().toLowerCase();
6250
- if (!/^[0-9a-f]{8,}$/.test(short)) return null;
6251
- const url = `${this.base}/api/v1/cloud/environments-by-short-id/${encodeURIComponent(short)}`;
6252
- const res = await this.request(url);
6253
- if (res === null) return null;
6254
- const body = res.success === false ? null : res.data ?? res;
6255
- if (!body || typeof body.environmentId !== "string" || !body.environmentId) return null;
6256
- return { environmentId: body.environmentId, organizationId: body.organizationId };
6257
- }
6258
- /**
6259
- * Fetch the head commit of a branch. Returns the commit id (and the
6260
- * matching revision row's `published_at` for cache-validity checks).
6261
- * Reuses the existing `GET /cloud/environments/:id/branches` endpoint.
6262
- */
6263
- async fetchBranchHead(environmentId, branchName) {
6264
- const url = `${this.base}/api/v1/cloud/environments/${encodeURIComponent(environmentId)}/branches`;
6265
- const res = await this.request(url);
6266
- if (res === null) return null;
6267
- const body = res.success === false ? null : res.data ?? res;
6268
- const branches = Array.isArray(body?.branches) ? body.branches : [];
6269
- const target = String(branchName ?? "").trim().toLowerCase();
6270
- const found = branches.find((b) => String(b?.branch ?? "").toLowerCase() === target);
6271
- if (!found?.headCommitId) return null;
6272
- return { commitId: String(found.headCommitId), publishedAt: found.headPublishedAt ?? null };
6273
- }
6274
- /**
6275
- * Cheap freshness probe — returns the env's `last_published_at`
6276
- * (and best-effort current commit) without rebuilding the artifact.
6277
- * Used by `KernelManager` on cache hits to detect when a per-env
6278
- * kernel has been invalidated by an upstream change (marketplace
6279
- * install/uninstall, artifact publish) so it can be rebuilt
6280
- * without waiting for the 15-minute LRU TTL to expire.
6281
- *
6282
- * Returns `null` on definitive 404 / unknown env. Errors propagate
6283
- * (caller decides whether to treat unreachable cloud as fresh or
6284
- * stale — typically fresh, so a brief outage doesn't churn every
6285
- * cached kernel).
6286
- */
6287
- async getFreshness(environmentId) {
6288
- const url = `${this.base}/api/v1/cloud/environments/${encodeURIComponent(environmentId)}/freshness`;
6289
- const res = await this.request(url);
6290
- if (res === null) return null;
6291
- const body = res.success === false ? null : res.data ?? res;
6292
- if (!body || typeof body !== "object") return null;
6293
- const envId = typeof body.environmentId === "string" ? body.environmentId : environmentId;
6294
- const lastPublishedAt = typeof body.lastPublishedAt === "string" ? body.lastPublishedAt : null;
6295
- const commitId = typeof body.commitId === "string" ? body.commitId : null;
6296
- return { environmentId: envId, lastPublishedAt, commitId };
6297
- }
6298
- /** Drop cached entries for a project (and any matching hostname). */
6299
- invalidate(environmentId) {
6300
- this.artifactCache.delete(environmentId);
6301
- const prefix = `${environmentId}@`;
6302
- for (const key of Array.from(this.artifactCache.keys())) {
6303
- if (key.startsWith(prefix)) this.artifactCache.delete(key);
6304
- }
6305
- for (const [host, entry] of this.hostnameCache) {
6306
- if (entry.value.environmentId === environmentId) this.hostnameCache.delete(host);
6307
- }
6308
- }
6309
- /** Drop everything. Used on shutdown / hot-reload. */
6310
- clear() {
6311
- this.hostnameCache.clear();
6312
- this.artifactCache.clear();
6313
- }
6314
- async request(url) {
6315
- const controller = typeof AbortController !== "undefined" ? new AbortController() : null;
6316
- const timer = controller ? setTimeout(() => controller.abort(), this.requestTimeoutMs) : null;
6317
- try {
6318
- const res = await this.fetchImpl(url, {
6319
- method: "GET",
6320
- headers: this.buildHeaders(),
6321
- signal: controller?.signal
6322
- });
6323
- if (res.status === 404) return null;
6324
- if (!res.ok) {
6325
- throw new Error(`[ArtifactApiClient] ${url} \u2192 HTTP ${res.status}`);
6326
- }
6327
- return await res.json();
6328
- } finally {
6329
- if (timer) clearTimeout(timer);
6330
- }
6331
- }
6332
- buildHeaders() {
6333
- const headers = {
6334
- "accept": "application/json",
6335
- "user-agent": "objectos-runtime"
6336
- };
6337
- if (this.apiKey) headers["authorization"] = `Bearer ${this.apiKey}`;
6338
- return headers;
6339
- }
6340
- };
6341
-
6342
- // src/cloud/artifact-environment-registry.ts
6343
- import { resolve as resolvePathNode } from "path";
6344
- var ArtifactEnvironmentRegistry = class {
6345
- constructor(config) {
6346
- this.hostnameCache = /* @__PURE__ */ new Map();
6347
- this.idCache = /* @__PURE__ */ new Map();
6348
- this.pending = /* @__PURE__ */ new Map();
6349
- this.client = config.client;
6350
- this.cacheTTL = config.cacheTtlMs ?? 5 * 60 * 1e3;
6351
- this.logger = config.logger ?? console;
6352
- }
6353
- async resolveByHostname(host) {
6354
- const cached = this.hostnameCache.get(host);
6355
- if (cached && cached.expiresAt > Date.now()) {
6356
- return { environmentId: cached.environmentId, driver: cached.driver };
6357
- }
6358
- const key = `host:${host}`;
6359
- const inflight = this.pending.get(key);
6360
- if (inflight) {
6361
- const result = await inflight;
6362
- return result ? { environmentId: result.environmentId, driver: result.driver } : null;
6363
- }
6364
- const promise = (async () => {
6365
- try {
6366
- const resolved = await this.client.resolveHostname(host);
6367
- if (!resolved) return null;
6368
- const entry2 = await this.buildCacheEntry(resolved.environmentId, resolved.runtime, resolved.organizationId, host);
6369
- if (!entry2) return null;
6370
- this.hostnameCache.set(host, entry2);
6371
- this.idCache.set(entry2.environmentId, entry2);
6372
- return entry2;
6373
- } catch (err) {
6374
- this.logger.error?.("[ArtifactEnvironmentRegistry] resolveByHostname failed", {
6375
- host,
6376
- error: err?.message ?? err
6377
- });
6378
- return null;
6379
- } finally {
6380
- this.pending.delete(key);
6381
- }
6382
- })();
6383
- this.pending.set(key, promise);
6384
- const entry = await promise;
6385
- return entry ? { environmentId: entry.environmentId, driver: entry.driver } : null;
6386
- }
6387
- async resolveById(environmentId) {
6388
- const cached = this.idCache.get(environmentId);
6389
- if (cached && cached.expiresAt > Date.now()) return cached.driver;
6390
- const key = `id:${environmentId}`;
6391
- const inflight = this.pending.get(key);
6392
- if (inflight) {
6393
- const result = await inflight;
6394
- return result?.driver ?? null;
6395
- }
6396
- const promise = (async () => {
6397
- try {
6398
- const entry2 = await this.buildCacheEntry(environmentId, void 0, void 0, void 0);
6399
- if (!entry2) return null;
6400
- this.idCache.set(environmentId, entry2);
6401
- if (entry2.project?.hostname) this.hostnameCache.set(entry2.project.hostname, entry2);
6402
- return entry2;
6403
- } catch (err) {
6404
- this.logger.error?.("[ArtifactEnvironmentRegistry] resolveById failed", {
6405
- environmentId,
6406
- error: err?.message ?? err
6407
- });
6408
- return null;
6409
- } finally {
6410
- this.pending.delete(key);
6411
- }
6412
- })();
6413
- this.pending.set(key, promise);
6414
- const entry = await promise;
6415
- return entry?.driver ?? null;
6416
- }
6417
- peekById(environmentId) {
6418
- const cached = this.idCache.get(environmentId);
6419
- if (cached && cached.expiresAt > Date.now()) {
6420
- return { environmentId: cached.environmentId, driver: cached.driver, project: cached.project };
6421
- }
6422
- return null;
6423
- }
6424
- invalidate(environmentId) {
6425
- this.idCache.delete(environmentId);
6426
- for (const [host, entry] of this.hostnameCache) {
6427
- if (entry.environmentId === environmentId) this.hostnameCache.delete(host);
6428
- }
6429
- this.client.invalidate(environmentId);
6430
- }
6431
- async buildCacheEntry(environmentId, runtimeFromHostname, orgIdFromHostname, hostname) {
6432
- let runtime = runtimeFromHostname;
6433
- let organizationId = orgIdFromHostname;
6434
- let host = hostname;
6435
- let artifactProjectId = environmentId;
6436
- if (!runtime || !organizationId) {
6437
- const artifact = await this.client.fetchArtifact(environmentId);
6438
- if (!artifact) {
6439
- this.logger.warn?.("[ArtifactEnvironmentRegistry] artifact not found", { environmentId });
6440
- return null;
6441
- }
6442
- artifactProjectId = artifact.environmentId ?? environmentId;
6443
- if (!runtime) runtime = artifact.runtime ?? extractRuntimeFromMetadata(artifact.metadata);
6444
- if (!organizationId) organizationId = artifact.runtime?.organizationId;
6445
- if (!host) host = artifact.runtime?.hostname;
6446
- }
6447
- if (!runtime || !runtime.databaseUrl || !runtime.databaseDriver) {
6448
- this.logger.warn?.("[ArtifactEnvironmentRegistry] no runtime config for project", { environmentId });
6449
- return null;
6450
- }
6451
- const driver = await createDriver(runtime.databaseDriver, runtime.databaseUrl, runtime.databaseAuthToken ?? "");
6452
- const projectRow = {
6453
- id: artifactProjectId,
6454
- organization_id: organizationId,
6455
- hostname: host,
6456
- database_url: runtime.databaseUrl,
6457
- database_driver: runtime.databaseDriver,
6458
- metadata: runtime.metadata
6459
- };
6460
- return {
6461
- environmentId: artifactProjectId,
6462
- driver,
6463
- project: projectRow,
6464
- expiresAt: Date.now() + this.cacheTTL
6465
- };
6466
- }
6467
- };
6468
- function extractRuntimeFromMetadata(metadata) {
6469
- const datasources = metadata?.datasources;
6470
- if (!Array.isArray(datasources) || datasources.length === 0) return void 0;
6471
- const mapping = metadata?.datasourceMapping;
6472
- let preferredName;
6473
- if (mapping) {
6474
- const def = mapping.find((m) => m?.default === true);
6475
- if (def?.datasource) preferredName = def.datasource;
6476
- }
6477
- const ds = preferredName ? datasources.find((d) => d?.name === preferredName) : datasources[0];
6478
- if (!ds || typeof ds !== "object") return void 0;
6479
- const config = ds.config ?? {};
6480
- const url = config.url ?? config.connectionString ?? config.connection ?? config.filename;
6481
- const driver = ds.driver;
6482
- if (typeof driver !== "string" || typeof url !== "string") return void 0;
6483
- return {
6484
- databaseDriver: driver,
6485
- databaseUrl: url,
6486
- databaseAuthToken: typeof config.authToken === "string" ? config.authToken : void 0
6487
- };
6488
- }
6489
- async function createDriver(driverType, databaseUrl, authToken) {
6490
- switch (driverType) {
6491
- case "libsql":
6492
- case "turso": {
6493
- let TursoDriver;
6494
- try {
6495
- ({ TursoDriver } = await import("@objectstack/driver-turso"));
6496
- } catch (primaryErr) {
6497
- try {
6498
- const { createRequire } = await import("module");
6499
- const path = await import("path");
6500
- const url = await import("url");
6501
- const hostRequire = createRequire(path.join(process.cwd(), "noop.js"));
6502
- const resolved = hostRequire.resolve("@objectstack/driver-turso");
6503
- ({ TursoDriver } = await import(url.pathToFileURL(resolved).href));
6504
- } catch (fallbackErr) {
6505
- throw new Error(
6506
- `[ArtifactEnvironmentRegistry] libsql/turso driver requested but @objectstack/driver-turso is not resolvable. Install it from the cloud monorepo (cloud/packages/driver-turso) or via npm. (primary: ${primaryErr?.message ?? primaryErr}; fallback: ${fallbackErr?.message ?? fallbackErr})`
6507
- );
6508
- }
6509
- }
6510
- return new TursoDriver({ url: databaseUrl, authToken });
6511
- }
6512
- case "memory": {
6513
- const { InMemoryDriver } = await import("@objectstack/driver-memory");
6514
- const dbName = databaseUrl.replace(/^memory:\/\//, "").trim();
6515
- const filePath = dbName ? resolvePathNode(process.cwd(), ".objectstack/data/projects", `${dbName}.json`) : void 0;
6516
- return new InMemoryDriver({
6517
- persistence: filePath ? { type: "file", path: filePath } : "file"
6518
- });
6519
- }
6520
- case "sqlite":
6521
- case "sql": {
6522
- const filePath = databaseUrl.replace(/^file:/, "").replace(/^sql:\/\//, "");
6523
- const { SqlDriver } = await import("@objectstack/driver-sql");
6524
- return new SqlDriver({
6525
- client: "better-sqlite3",
6526
- connection: { filename: filePath },
6527
- useNullAsDefault: true
6528
- });
6529
- }
6530
- case "postgres":
6531
- case "postgresql":
6532
- case "pg": {
6533
- const { SqlDriver } = await import("@objectstack/driver-sql");
6534
- return new SqlDriver({
6535
- client: "pg",
6536
- connection: databaseUrl,
6537
- pool: { min: 0, max: 5 }
6538
- });
6539
- }
6540
- case "mongodb":
6541
- case "mongo": {
6542
- const { MongoDBDriver } = await import("@objectstack/driver-mongodb");
6543
- return new MongoDBDriver({ url: databaseUrl });
6544
- }
6545
- default:
6546
- throw new Error(`[ArtifactEnvironmentRegistry] Unsupported driver type: ${driverType}`);
6547
- }
6548
- }
6549
-
6550
- // src/cloud/artifact-kernel-factory.ts
6551
- init_driver_plugin();
6552
- init_app_plugin();
6553
- import { createHmac as createHmac2 } from "crypto";
6554
- import { ObjectKernel as ObjectKernel3 } from "@objectstack/core";
6555
- import { readEnvWithDeprecation as readEnvWithDeprecation3 } from "@objectstack/types";
6556
-
6557
- // src/cloud/capability-loader.ts
6558
- var CAPABILITY_PROVIDERS = {
6559
- automation: {
6560
- // Self-contained: AutomationServicePlugin seeds all built-in node
6561
- // executors itself (ADR-0018), so no companion node-pack plugins.
6562
- pkg: "@objectstack/service-automation",
6563
- export: "AutomationServicePlugin"
6564
- },
6565
- ai: {
6566
- pkg: "@objectstack/service-ai",
6567
- export: "AIServicePlugin"
6568
- },
6569
- // AI Studio — AI-driven metadata authoring ("online development"). This is
6570
- // a commercial capability that ships in the private @objectstack/service-ai-studio
6571
- // package (not part of the open-source framework). The dynamic import below
6572
- // silently skips when the package isn't installed, so the open-source build
6573
- // is unaffected; cloud and enterprise installs that ship the package light it
6574
- // up. Pair with `ai` in `requires` (it attaches via the `ai:ready` hook).
6575
- aiStudio: {
6576
- pkg: "@objectstack/service-ai-studio",
6577
- export: "AIStudioPlugin"
6578
- },
6579
- analytics: {
6580
- pkg: "@objectstack/service-analytics",
6581
- export: "AnalyticsServicePlugin",
6582
- configKey: "analyticsCubes"
6583
- },
6584
- audit: {
6585
- pkg: "@objectstack/plugin-audit",
6586
- export: "AuditPlugin"
6587
- },
6588
- cache: {
6589
- pkg: "@objectstack/service-cache",
6590
- export: "CacheServicePlugin"
6591
- },
6592
- storage: {
6593
- pkg: "@objectstack/service-storage",
6594
- export: "StorageServicePlugin"
6595
- },
6596
- queue: {
6597
- pkg: "@objectstack/service-queue",
6598
- export: "QueueServicePlugin"
6599
- },
6600
- job: {
6601
- pkg: "@objectstack/service-job",
6602
- export: "JobServicePlugin"
6603
- },
6604
- messaging: {
6605
- // Backs the `notify` flow node (ADR-0012): delivers to a user's
6606
- // channels (inbox by default → `sys_inbox_message` rows).
6607
- pkg: "@objectstack/service-messaging",
6608
- export: "MessagingServicePlugin"
6609
- },
6610
- triggers: {
6611
- // Concrete flow triggers — record-change (ObjectQL hooks) + schedule
6612
- // (cron/interval via the job service; pair `triggers` with `job`).
6613
- pkg: "@objectstack/plugin-trigger-record-change",
6614
- export: "RecordChangeTriggerPlugin",
6615
- extras: [{ pkg: "@objectstack/plugin-trigger-schedule", export: "ScheduleTriggerPlugin" }]
6616
- },
6617
- realtime: {
6618
- pkg: "@objectstack/service-realtime",
6619
- export: "RealtimeServicePlugin"
6620
- },
6621
- feed: {
6622
- pkg: "@objectstack/service-feed",
6623
- export: "FeedServicePlugin"
6624
- },
6625
- settings: {
6626
- pkg: "@objectstack/service-settings",
6627
- export: "SettingsServicePlugin"
6628
- }
6629
- };
6630
- async function loadCapabilities(opts) {
6631
- const { kernel, requires, bundle, environmentId } = opts;
6632
- const logger = opts.logger ?? console;
6633
- const installed = [];
6634
- const resolved = [...new Set(requires)];
6635
- if (resolved.includes("audit") && !resolved.includes("messaging")) {
6636
- resolved.push("messaging");
6637
- }
6638
- for (const cap of resolved) {
6639
- const spec = CAPABILITY_PROVIDERS[cap];
6640
- if (!spec) {
6641
- continue;
6642
- }
6643
- try {
6644
- const mod = await import(
6645
- /* webpackIgnore: true */
6646
- spec.pkg
6647
- );
6648
- const Ctor = mod[spec.export];
6649
- if (!Ctor) {
6650
- logger.warn?.(
6651
- `[CapabilityLoader] '${cap}': package '${spec.pkg}' did not export '${spec.export}'`,
6652
- { environmentId }
6653
- );
6654
- continue;
6655
- }
6656
- let arg;
6657
- if (spec.configKey) {
6658
- const v = bundle[spec.configKey];
6659
- if (spec.configKey === "analyticsCubes") {
6660
- arg = { cubes: Array.isArray(v) ? v : [] };
6661
- } else if (v !== void 0) {
6662
- arg = v;
6663
- }
6664
- }
6665
- await kernel.use(arg !== void 0 ? new Ctor(arg) : new Ctor());
6666
- installed.push(spec.export);
6667
- if (spec.extras) {
6668
- for (const ex of spec.extras) {
6669
- try {
6670
- const exMod = await import(
6671
- /* webpackIgnore: true */
6672
- ex.pkg
6673
- );
6674
- const ExCtor = exMod[ex.export];
6675
- if (ExCtor) {
6676
- await kernel.use(new ExCtor());
6677
- installed.push(ex.export);
6678
- }
6679
- } catch {
6680
- }
6681
- }
6682
- }
6683
- logger.info?.(
6684
- `[CapabilityLoader] '${cap}' installed (${spec.export}${spec.extras ? " + " + spec.extras.length + " extras" : ""})`,
6685
- { environmentId }
6686
- );
6687
- } catch (err) {
6688
- const msg = err?.message ?? String(err);
6689
- if (msg.includes("Cannot find module") || msg.includes("ERR_MODULE_NOT_FOUND")) {
6690
- logger.warn?.(
6691
- `[CapabilityLoader] '${cap}' requested but '${spec.pkg}' not installed in host \u2014 skipped`,
6692
- { environmentId }
6693
- );
6694
- } else {
6695
- logger.error?.(
6696
- `[CapabilityLoader] '${cap}' load failed: ${msg}`,
6697
- { environmentId }
6698
- );
6699
- }
6700
- }
6701
- }
6702
- return installed;
6703
- }
6704
-
6705
- // src/cloud/platform-sso.ts
6706
- import { createHmac, createHash } from "crypto";
6707
- var PLATFORM_SSO_PROVIDER_ID = "objectstack-cloud";
6708
- function derivePlatformSsoClientId(environmentId) {
6709
- return `project_${environmentId}`;
6710
- }
6711
- function derivePlatformSsoClientSecret(baseSecret, environmentId) {
6712
- return createHmac("sha256", baseSecret).update(`oauth-client:${environmentId}`).digest("hex");
6713
- }
6714
- function hashPlatformSsoClientSecret(plaintext) {
6715
- return createHash("sha256").update(plaintext).digest("base64").replace(/=+$/, "").replace(/\+/g, "-").replace(/\//g, "_");
6716
- }
6717
- function buildPlatformSsoRedirectUri(hostname, basePath = "/api/v1/auth") {
6718
- let host;
6719
- if (hostname.startsWith("http://") || hostname.startsWith("https://")) {
6720
- host = hostname;
6721
- } else if (/(\.|^)localhost(:\d+)?$/i.test(hostname)) {
6722
- const port = (process.env.OS_RUNTIME_PORT ?? "").trim();
6723
- const hostWithPort = /:\d+$/.test(hostname) || !port ? hostname : `${hostname}:${port}`;
6724
- host = `http://${hostWithPort}`;
6725
- } else {
6726
- host = `https://${hostname}`;
6727
- }
6728
- const trimmed = host.replace(/\/+$/, "");
6729
- const path = basePath.replace(/\/+$/, "");
6730
- return `${trimmed}${path}/oauth2/callback/${PLATFORM_SSO_PROVIDER_ID}`;
6731
- }
6732
- async function seedPlatformSsoClient(opts) {
6733
- const { ql, environmentId, hostname, baseSecret, logger, throwOnError } = opts;
6734
- if (!baseSecret) {
6735
- logger?.warn?.("[platform-sso] OS_AUTH_SECRET not set \u2014 skipping client seed", { environmentId });
6736
- return;
6737
- }
6738
- const clientId = derivePlatformSsoClientId(environmentId);
6739
- const clientSecretPlaintext = derivePlatformSsoClientSecret(baseSecret, environmentId);
6740
- const clientSecretStored = hashPlatformSsoClientSecret(clientSecretPlaintext);
6741
- const desiredRedirect = hostname ? buildPlatformSsoRedirectUri(hostname) : null;
6742
- let existing = null;
6743
- try {
6744
- const rows = await ql.find("sys_oauth_application", {
6745
- where: { client_id: clientId },
6746
- limit: 1
6747
- }, { context: { isSystem: true } });
6748
- const list = Array.isArray(rows) ? rows : Array.isArray(rows?.records) ? rows.records : [];
6749
- existing = list[0] ?? null;
6750
- } catch (err) {
6751
- logger?.warn?.("[platform-sso] sys_oauth_application read failed \u2014 skipping seed", {
6752
- environmentId,
6753
- error: err?.message
6754
- });
6755
- return;
6756
- }
6757
- const nowIso = (/* @__PURE__ */ new Date()).toISOString();
6758
- if (!existing) {
6759
- const redirects = desiredRedirect ? [desiredRedirect] : [];
6760
- try {
6761
- await ql.insert("sys_oauth_application", {
6762
- id: `oauthc_${environmentId}`,
6763
- name: `Project ${environmentId}`,
6764
- client_id: clientId,
6765
- client_secret: clientSecretStored,
6766
- type: "web",
6767
- redirect_uris: JSON.stringify(redirects),
6768
- grant_types: JSON.stringify(["authorization_code", "refresh_token"]),
6769
- response_types: JSON.stringify(["code"]),
6770
- scopes: JSON.stringify(["openid", "email", "profile"]),
6771
- token_endpoint_auth_method: "client_secret_basic",
6772
- require_pkce: false,
6773
- skip_consent: true,
6774
- disabled: false,
6775
- subject_type: "public",
6776
- created_at: nowIso,
6777
- updated_at: nowIso
6778
- }, { context: { isSystem: true } });
6779
- logger?.info?.("[platform-sso] sys_oauth_application row created", { environmentId, clientId });
6780
- } catch (err) {
6781
- logger?.warn?.("[platform-sso] sys_oauth_application create failed", {
6782
- environmentId,
6783
- error: err?.message
6784
- });
6785
- if (throwOnError) throw err;
6786
- }
6787
- return;
6788
- }
6789
- let currentRedirects = [];
6790
- try {
6791
- const raw = existing.redirect_uris;
6792
- const parsed = typeof raw === "string" ? JSON.parse(raw) : raw;
6793
- if (Array.isArray(parsed)) currentRedirects = parsed.filter((s) => typeof s === "string");
6794
- } catch {
6795
- }
6796
- const mergedRedirects = desiredRedirect && !currentRedirects.includes(desiredRedirect) ? [...currentRedirects, desiredRedirect] : currentRedirects;
6797
- const repairPatch = {
6798
- name: existing.name || `Project ${environmentId}`,
6799
- client_secret: clientSecretStored,
6800
- type: existing.type || "web",
6801
- redirect_uris: JSON.stringify(mergedRedirects),
6802
- grant_types: JSON.stringify(["authorization_code", "refresh_token"]),
6803
- response_types: JSON.stringify(["code"]),
6804
- scopes: JSON.stringify(["openid", "email", "profile"]),
6805
- token_endpoint_auth_method: "client_secret_basic",
6806
- require_pkce: false,
6807
- skip_consent: true,
6808
- disabled: false,
6809
- subject_type: "public",
6810
- updated_at: nowIso
6811
- };
6812
- try {
6813
- await ql.update(
6814
- "sys_oauth_application",
6815
- repairPatch,
6816
- { where: { id: existing.id } },
6817
- { context: { isSystem: true } }
6818
- );
6819
- logger?.info?.("[platform-sso] sys_oauth_application repaired", {
6820
- environmentId,
6821
- clientId,
6822
- redirect_uris: mergedRedirects
6823
- });
6824
- } catch (err) {
6825
- logger?.warn?.("[platform-sso] sys_oauth_application repair failed", {
6826
- environmentId,
6827
- error: err?.message
6828
- });
6829
- if (throwOnError) throw err;
6830
- }
6831
- }
6832
- async function backfillPlatformSsoClients(opts) {
6833
- const { ql, baseSecret, logger, limit = 1e3 } = opts;
6834
- if (!baseSecret) {
6835
- logger?.warn?.("[platform-sso] backfill skipped \u2014 OS_AUTH_SECRET not set");
6836
- return { scanned: 0, seeded: 0, alreadyExisted: 0, failures: [] };
6837
- }
6838
- let projects = [];
6839
- try {
6840
- const rows = await ql.find("sys_environment", {
6841
- limit,
6842
- fields: ["id", "hostname", "status"]
6843
- }, { context: { isSystem: true } });
6844
- projects = Array.isArray(rows) ? rows : Array.isArray(rows?.records) ? rows.records : [];
6845
- } catch (err) {
6846
- logger?.warn?.("[platform-sso] backfill: sys_environment read failed", {
6847
- error: err?.message
6848
- });
6849
- return { scanned: 0, seeded: 0, alreadyExisted: 0, failures: [{ environmentId: "<scan>", error: err?.message ?? String(err) }] };
6850
- }
6851
- let seeded = 0;
6852
- let alreadyExisted = 0;
6853
- const failures = [];
6854
- for (const p of projects) {
6855
- if (!p?.id) continue;
6856
- const before = await (async () => {
6857
- try {
6858
- const r = await ql.find("sys_oauth_application", {
6859
- where: { client_id: derivePlatformSsoClientId(p.id) },
6860
- limit: 1
6861
- }, { context: { isSystem: true } });
6862
- const list = Array.isArray(r) ? r : Array.isArray(r?.records) ? r.records : [];
6863
- return list[0] ?? null;
6864
- } catch {
6865
- return null;
6866
- }
6867
- })();
6868
- try {
6869
- await seedPlatformSsoClient({ ql, environmentId: p.id, hostname: p.hostname, baseSecret, logger, throwOnError: true });
6870
- if (before) alreadyExisted++;
6871
- else {
6872
- const after = await (async () => {
6873
- try {
6874
- const r = await ql.find("sys_oauth_application", {
6875
- where: { client_id: derivePlatformSsoClientId(p.id) },
6876
- limit: 1
6877
- }, { context: { isSystem: true } });
6878
- const list = Array.isArray(r) ? r : Array.isArray(r?.records) ? r.records : [];
6879
- return list[0] ?? null;
6880
- } catch (err) {
6881
- return { _readErr: err?.message };
6882
- }
6883
- })();
6884
- if (after && !after._readErr) seeded++;
6885
- else failures.push({ environmentId: p.id, error: `post-insert read returned ${after ? JSON.stringify(after) : "null"}` });
6886
- }
6887
- } catch (err) {
6888
- failures.push({ environmentId: p.id, error: err?.message ?? String(err) });
6889
- }
6890
- }
6891
- logger?.info?.("[platform-sso] backfill complete", { scanned: projects.length, seeded, alreadyExisted, failures: failures.length });
6892
- return { scanned: projects.length, seeded, alreadyExisted, failures };
6893
- }
6894
-
6895
- // src/cloud/artifact-kernel-factory.ts
6896
- function deriveProjectAuthSecret(baseSecret, environmentId) {
6897
- return createHmac2("sha256", baseSecret).update(`project:${environmentId}`).digest("hex");
6898
- }
6899
- var ArtifactKernelFactory = class {
6900
- constructor(config) {
6901
- this.client = config.client;
6902
- this.envRegistry = config.envRegistry;
6903
- this.logger = config.logger ?? console;
6904
- this.kernelConfig = config.kernelConfig;
6905
- this.defaultRequires = config.defaultRequires ?? [];
6906
- this.authBaseSecret = (config.authBaseSecret ?? readEnvWithDeprecation3("OS_AUTH_SECRET", ["AUTH_SECRET", "BETTER_AUTH_SECRET"]) ?? "").trim();
6907
- }
6908
- async create(environmentId) {
6909
- let cached = this.envRegistry.peekById(environmentId);
6910
- if (!cached) {
6911
- const driver2 = await this.envRegistry.resolveById(environmentId);
6912
- if (!driver2) {
6913
- throw new Error(`[ArtifactKernelFactory] Could not resolve driver for project '${environmentId}'`);
6914
- }
6915
- cached = this.envRegistry.peekById(environmentId);
6916
- if (!cached) {
6917
- throw new Error(`[ArtifactKernelFactory] envRegistry returned a driver but no cached entry for '${environmentId}'`);
6918
- }
6919
- }
6920
- const driver = cached.driver;
6921
- const project = cached.project;
6922
- const artifact = await this.client.fetchArtifact(environmentId);
6923
- if (!artifact) {
6924
- throw new Error(`[ArtifactKernelFactory] Artifact not available for project '${environmentId}'`);
6925
- }
6926
- const { ObjectQLPlugin } = await import("@objectstack/objectql");
6927
- const { MetadataPlugin } = await import("@objectstack/metadata");
6928
- const kernel = new ObjectKernel3(this.kernelConfig);
6929
- await kernel.use(new DriverPlugin(driver, { datasourceName: "cloud" }));
6930
- await kernel.use(new ObjectQLPlugin({ environmentId, skipSchemaSync: false }));
6931
- await kernel.use(new MetadataPlugin({
6932
- watch: false,
6933
- environmentId,
6934
- organizationId: project.organization_id,
6935
- // ADR-0005: customization overlays (user-created views, dashboards,
6936
- // edited objects, ...) are persisted by
6937
- // ObjectStackProtocolImplementation.saveMetaItem on whichever
6938
- // engine the protocol is attached to. For per-project kernels that
6939
- // means the project's own DB, so the sys_metadata + history tables
6940
- // MUST be provisioned here. The previous `false` setting caused
6941
- // "no such table: sys_metadata" errors on any PUT /api/v1/meta/*
6942
- // call (e.g. Studio "Create View") against a project deployment.
6943
- registerSystemObjects: true
6944
- }));
6945
- if (this.authBaseSecret) {
6946
- try {
6947
- const { AuthPlugin } = await import("@objectstack/plugin-auth");
6948
- const projectSecret = deriveProjectAuthSecret(this.authBaseSecret, environmentId);
6949
- const baseUrl = project.hostname ? project.hostname.startsWith("http") ? project.hostname : /(\.|^)localhost(:\d+)?$/i.test(project.hostname) ? (() => {
6950
- const runtimePort = (process.env.OS_RUNTIME_PORT ?? "").trim();
6951
- const hasPort = /:\d+$/.test(project.hostname);
6952
- const hostWithPort = hasPort || !runtimePort ? project.hostname : `${project.hostname}:${runtimePort}`;
6953
- return `http://${hostWithPort}`;
6954
- })() : `https://${project.hostname}` : void 0;
6955
- const trustedOriginsList = [];
6956
- if (baseUrl) trustedOriginsList.push(baseUrl);
6957
- const platformOrigins = (process.env.OS_TRUSTED_ORIGINS ?? "").split(",").map((s) => s.trim()).filter(Boolean);
6958
- for (const o of platformOrigins) {
6959
- if (!trustedOriginsList.includes(o)) trustedOriginsList.push(o);
6960
- }
6961
- const rootDomain = (process.env.OS_ROOT_DOMAIN ?? "").trim().replace(/^https?:\/\//, "");
6962
- if (rootDomain) {
6963
- const wildcard = `https://*.${rootDomain}`;
6964
- if (!trustedOriginsList.includes(wildcard)) trustedOriginsList.push(wildcard);
6965
- }
6966
- if (project.hostname) {
6967
- const bareHost = project.hostname.replace(/^https?:\/\//, "");
6968
- if (bareHost.endsWith(".localhost") || bareHost === "localhost") {
6969
- trustedOriginsList.push(`http://${bareHost}`);
6970
- trustedOriginsList.push(`http://${bareHost}:*`);
6971
- trustedOriginsList.push(`https://${bareHost}:*`);
6972
- }
6973
- }
6974
- const platformSsoEnabled = String(
6975
- process.env.OS_PLATFORM_SSO ?? "true"
6976
- ).toLowerCase() !== "false";
6977
- const cloudBaseUrl = (process.env.OS_CLOUD_URL ?? "").trim().replace(/\/+$/, "");
6978
- const oidcProviders = platformSsoEnabled && cloudBaseUrl && /^https?:\/\//.test(cloudBaseUrl) ? [{
6979
- providerId: PLATFORM_SSO_PROVIDER_ID,
6980
- name: "ObjectStack",
6981
- discoveryUrl: `${cloudBaseUrl}/.well-known/openid-configuration`,
6982
- clientId: derivePlatformSsoClientId(environmentId),
6983
- clientSecret: derivePlatformSsoClientSecret(this.authBaseSecret, environmentId),
6984
- scopes: ["openid", "email", "profile"]
6985
- }] : void 0;
6986
- await kernel.use(new AuthPlugin({
6987
- secret: projectSecret,
6988
- baseUrl,
6989
- // Project kernel has no http-server (host owns it). The
6990
- // dispatcher's handleAuth path resolves `auth` via
6991
- // getService and invokes the handler directly — route
6992
- // registration is unnecessary and would warn.
6993
- registerRoutes: false,
6994
- // Identity tables live in the project's own DB — keep
6995
- // sys_user/sys_session local to this kernel.
6996
- manifestDatasource: "default",
6997
- // Cookie scope: default to the project's own host. We
6998
- // intentionally do NOT pass crossSubDomainCookies here
6999
- // so cookies stay isolated per project subdomain.
7000
- trustedOrigins: trustedOriginsList.length ? trustedOriginsList : void 0,
7001
- ...oidcProviders ? { oidcProviders } : {}
7002
- }));
7003
- if (oidcProviders) {
7004
- this.logger.info?.("[ArtifactKernelFactory] platform SSO wired", {
7005
- environmentId,
7006
- cloudBaseUrl
7007
- });
7008
- }
7009
- } catch (err) {
7010
- this.logger.warn?.("[ArtifactKernelFactory] AuthPlugin not registered", {
7011
- environmentId,
7012
- error: err?.message
7013
- });
7014
- }
7015
- } else {
7016
- this.logger.warn?.("[ArtifactKernelFactory] OS_AUTH_SECRET not set \u2014 per-project AuthPlugin skipped (auth endpoints will return 404)", { environmentId });
7017
- }
7018
- try {
7019
- const multiTenant = String(readEnvWithDeprecation3("OS_MULTI_ORG_ENABLED", "OS_MULTI_TENANT") ?? "false").toLowerCase() !== "false";
7020
- if (multiTenant) {
7021
- try {
7022
- const { OrgScopingPlugin } = await import("@objectstack/plugin-org-scoping");
7023
- await kernel.use(new OrgScopingPlugin());
7024
- } catch (err) {
7025
- this.logger.warn?.("[ArtifactKernelFactory] OrgScopingPlugin not registered (multi-tenant disabled)", {
7026
- environmentId,
7027
- error: err?.message
7028
- });
7029
- }
7030
- }
7031
- const { SecurityPlugin } = await import("@objectstack/plugin-security");
7032
- await kernel.use(new SecurityPlugin());
7033
- } catch (err) {
7034
- this.logger.warn?.("[ArtifactKernelFactory] SecurityPlugin not registered", {
7035
- environmentId,
7036
- error: err?.message
7037
- });
7038
- }
7039
- const projectName = project.hostname ?? environmentId;
7040
- const artifactAny = artifact;
7041
- const topLevelManifest = artifactAny?.manifest && typeof artifactAny.manifest === "object" ? artifactAny.manifest : null;
7042
- const topLevelFunctions = Array.isArray(artifactAny?.functions) ? artifactAny.functions : [];
7043
- const bundle = {
7044
- ...artifact.metadata ?? {},
7045
- ...topLevelManifest ? { manifest: topLevelManifest } : {},
7046
- functions: topLevelFunctions
7047
- };
7048
- const sys = bundle.manifest ?? bundle;
7049
- const packageId = sys?.packageId ?? sys?.package_id ?? bundle?.packageId;
7050
- const i18nCfg = bundle?.i18n ?? sys?.i18n ?? {};
7051
- const trArr = Array.isArray(bundle?.translations) ? bundle.translations : Array.isArray(sys?.translations) ? sys.translations : [];
7052
- try {
7053
- const { I18nServicePlugin } = await import("@objectstack/service-i18n");
7054
- await kernel.use(new I18nServicePlugin({
7055
- defaultLocale: i18nCfg.defaultLocale,
7056
- fallbackLocale: i18nCfg.fallbackLocale ?? i18nCfg.defaultLocale ?? "en",
7057
- // Routes are dispatched by HttpDispatcher.handleI18n via
7058
- // kernel.getService('i18n'); the host worker owns the
7059
- // HTTP server. Skip self-registration to avoid warnings.
7060
- registerRoutes: false
7061
- }));
7062
- console.warn(
7063
- `[ArtifactKernelFactory] I18nServicePlugin registered (project=${environmentId}, translations=${trArr.length}, defaultLocale=${i18nCfg.defaultLocale ?? "en"})`
7064
- );
7065
- } catch (err) {
7066
- this.logger.warn?.("[ArtifactKernelFactory] I18nServicePlugin not registered", {
7067
- environmentId,
7068
- error: err?.message
7069
- });
7070
- }
7071
- const requiresRaw = (Array.isArray(bundle?.requires) ? bundle.requires : null) ?? (Array.isArray(sys?.requires) ? sys.requires : null) ?? [];
7072
- const requires = [
7073
- ...requiresRaw,
7074
- ...this.defaultRequires
7075
- ].filter((x) => typeof x === "string" && x.length > 0);
7076
- if (requires.length > 0) {
7077
- const installed = await loadCapabilities({
7078
- kernel,
7079
- requires,
7080
- bundle: { ...bundle ?? {}, ...sys ?? {} },
7081
- logger: this.logger,
7082
- environmentId
7083
- });
7084
- this.logger.info?.("[ArtifactKernelFactory] capabilities loaded", {
7085
- environmentId,
7086
- requires,
7087
- installed
7088
- });
7089
- }
7090
- await kernel.use(new AppPlugin(bundle, {
7091
- environmentId,
7092
- organizationId: project.organization_id ?? "",
7093
- projectName,
7094
- packageId,
7095
- source: packageId ? "package" : "user"
7096
- }));
7097
- await kernel.bootstrap();
7098
- try {
7099
- const projMeta = typeof project?.metadata === "string" ? JSON.parse(project.metadata) : project?.metadata ?? {};
7100
- const ownerSeed = projMeta?.ownerSeed;
7101
- const orgSeed = projMeta?.orgSeed;
7102
- if (orgSeed?.id && orgSeed?.name) {
7103
- try {
7104
- const { seedProjectOrganization: seedProjectOrganization2 } = await Promise.resolve().then(() => (init_environment_org_seed(), environment_org_seed_exports));
7105
- await seedProjectOrganization2(kernel, orgSeed, this.logger);
7106
- } catch (e) {
7107
- this.logger.warn?.("[ArtifactKernelFactory] orgSeed threw", {
7108
- environmentId,
7109
- error: e?.message
7110
- });
7111
- }
7112
- }
7113
- if (ownerSeed?.userId && ownerSeed?.email) {
7114
- try {
7115
- const { seedProjectOwner: seedProjectOwner2 } = await Promise.resolve().then(() => (init_environment_owner_seed(), environment_owner_seed_exports));
7116
- await seedProjectOwner2(kernel, ownerSeed, this.logger);
7117
- } catch (e) {
7118
- this.logger.warn?.("[ArtifactKernelFactory] ownerSeed threw", {
7119
- environmentId,
7120
- error: e?.message
7121
- });
7122
- }
7123
- if (orgSeed?.id) {
7124
- try {
7125
- const { seedProjectMember: seedProjectMember2 } = await Promise.resolve().then(() => (init_environment_org_seed(), environment_org_seed_exports));
7126
- await seedProjectMember2(
7127
- kernel,
7128
- { userId: ownerSeed.userId, organizationId: orgSeed.id, role: "owner" },
7129
- this.logger
7130
- );
7131
- } catch (e) {
7132
- this.logger.warn?.("[ArtifactKernelFactory] memberSeed threw", {
7133
- environmentId,
7134
- error: e?.message
7135
- });
7136
- }
7137
- }
7138
- }
7139
- } catch (err) {
7140
- this.logger.warn?.("[ArtifactKernelFactory] owner/org seed skipped", {
7141
- environmentId,
7142
- error: err?.message
7143
- });
7144
- }
7145
- try {
7146
- const datasetsNow = (() => {
7147
- try {
7148
- return kernel.getService?.("seed-datasets");
7149
- } catch {
7150
- return void 0;
7151
- }
7152
- })();
7153
- const replayer = (() => {
7154
- try {
7155
- return kernel.getService?.("seed-replayer");
7156
- } catch {
7157
- return void 0;
7158
- }
7159
- })();
7160
- if (Array.isArray(datasetsNow) && datasetsNow.length > 0 && typeof replayer === "function") {
7161
- const projMetaRaw = project?.metadata;
7162
- const projMeta = typeof projMetaRaw === "string" ? (() => {
7163
- try {
7164
- return JSON.parse(projMetaRaw);
7165
- } catch {
7166
- return {};
7167
- }
7168
- })() : projMetaRaw ?? {};
7169
- let primaryOrgId = projMeta?.orgSeed?.id;
7170
- if (!primaryOrgId) {
7171
- try {
7172
- const ql = kernel.getService?.("objectql");
7173
- if (ql?.find) {
7174
- const rows = await ql.find("sys_organization", { limit: 5, orderBy: [{ field: "created_at", direction: "asc" }] });
7175
- const list = Array.isArray(rows) ? rows : rows?.value ?? rows?.records ?? [];
7176
- if (Array.isArray(list) && list.length > 0 && list[0]?.id) {
7177
- primaryOrgId = String(list[0].id);
7178
- }
7179
- }
7180
- } catch {
7181
- }
7182
- }
7183
- if (primaryOrgId) {
7184
- try {
7185
- const summary = await replayer(primaryOrgId);
7186
- const inserted = summary?.inserted ?? 0;
7187
- const updated = summary?.updated ?? 0;
7188
- const errs = summary?.errors?.length ?? 0;
7189
- if (inserted > 0 || updated > 0 || errs > 0) {
7190
- this.logger.info?.("[ArtifactKernelFactory] post-bootstrap seed replay", {
7191
- environmentId,
7192
- organizationId: primaryOrgId,
7193
- datasets: datasetsNow.length,
7194
- inserted,
7195
- updated,
7196
- errors: errs
7197
- });
7198
- }
7199
- } catch (e) {
7200
- this.logger.warn?.("[ArtifactKernelFactory] post-bootstrap seed replay failed", {
7201
- environmentId,
7202
- organizationId: primaryOrgId,
7203
- error: e?.message
7204
- });
7205
- }
7206
- }
7207
- }
7208
- } catch (err) {
7209
- this.logger.warn?.("[ArtifactKernelFactory] post-bootstrap seed step threw", {
7210
- environmentId,
7211
- error: err?.message
7212
- });
7213
- }
7214
- let i18nSvc = null;
7215
- try {
7216
- i18nSvc = kernel.getService?.("i18n");
7217
- } catch {
7218
- i18nSvc = null;
7219
- }
7220
- try {
7221
- if (i18nSvc && typeof i18nSvc.loadTranslations === "function") {
7222
- if (i18nCfg.defaultLocale && typeof i18nSvc.setDefaultLocale === "function") {
7223
- i18nSvc.setDefaultLocale(i18nCfg.defaultLocale);
7224
- }
7225
- let loaded = 0;
7226
- for (const tbundle of trArr) {
7227
- if (!tbundle || typeof tbundle !== "object") continue;
7228
- for (const [locale, data] of Object.entries(tbundle)) {
7229
- if (data && typeof data === "object") {
7230
- try {
7231
- i18nSvc.loadTranslations(locale, data);
7232
- loaded++;
7233
- } catch (err) {
7234
- this.logger.warn?.("[ArtifactKernelFactory] i18n loadTranslations failed", {
7235
- environmentId,
7236
- locale,
7237
- error: err?.message
7238
- });
7239
- }
7240
- }
7241
- }
7242
- }
7243
- if (loaded > 0) {
7244
- this.logger.info?.("[ArtifactKernelFactory] i18n direct-load complete", {
7245
- environmentId,
7246
- locales: loaded,
7247
- bundles: trArr.length
7248
- });
7249
- }
7250
- }
7251
- } catch (err) {
7252
- this.logger.warn?.("[ArtifactKernelFactory] i18n direct-load failed", {
7253
- environmentId,
7254
- error: err?.message
7255
- });
7256
- }
7257
- this.logger.info?.("[ArtifactKernelFactory] kernel ready", {
7258
- environmentId,
7259
- commitId: artifact.commitId,
7260
- checksum: artifact.checksum,
7261
- authEnabled: Boolean(this.authBaseSecret)
7262
- });
7263
- return kernel;
7264
- }
7265
- };
7266
-
7267
- // src/cloud/auth-proxy-plugin.ts
7268
- import { createHmac as createHmac3, randomUUID as randomUUID2 } from "crypto";
7269
- var AUTH_PREFIX = "/api/v1/auth";
7270
- function signSessionCookieValue(rawToken, secret) {
7271
- const signature = createHmac3("sha256", secret).update(rawToken).digest("base64");
7272
- return encodeURIComponent(`${rawToken}.${signature}`);
7273
- }
7274
- function buildSetCookieHeader(name, encodedValue, attrs, maxAgeSec) {
7275
- const parts = [`${name}=${encodedValue}`];
7276
- const a = attrs ?? {};
7277
- if (a.path) parts.push(`Path=${a.path}`);
7278
- else parts.push("Path=/");
7279
- if (Number.isFinite(maxAgeSec) && maxAgeSec > 0) parts.push(`Max-Age=${Math.floor(maxAgeSec)}`);
7280
- if (a.domain) parts.push(`Domain=${a.domain}`);
7281
- if (a.sameSite) {
7282
- const ss = String(a.sameSite);
7283
- parts.push(`SameSite=${ss.charAt(0).toUpperCase() + ss.slice(1)}`);
7284
- } else {
7285
- parts.push("SameSite=Lax");
7286
- }
7287
- if (a.secure) parts.push("Secure");
7288
- if (a.httpOnly !== false) parts.push("HttpOnly");
7289
- if (a.partitioned) parts.push("Partitioned");
7290
- return parts.join("; ");
7291
- }
7292
- function pickHandler(svc) {
7293
- if (!svc) return void 0;
7294
- if (typeof svc.handleRequest === "function") return svc.handleRequest.bind(svc);
7295
- if (typeof svc.handler === "function") return svc.handler.bind(svc);
7296
- if (svc.api && typeof svc.api.handler === "function") return svc.api.handler.bind(svc.api);
7297
- if (svc.auth && typeof svc.auth.handler === "function") return svc.auth.handler.bind(svc.auth);
7298
- return void 0;
7299
- }
7300
- async function resolveAuthHandler(svc) {
7301
- const direct = pickHandler(svc);
7302
- if (direct) return direct;
7303
- if (typeof svc?.getApi === "function") {
7304
- try {
7305
- const api = await svc.getApi();
7306
- return pickHandler(api) ?? pickHandler({ api });
7307
- } catch {
7308
- return void 0;
7309
- }
7310
- }
7311
- return void 0;
7312
- }
7313
- var AuthProxyPlugin = class {
7314
- constructor() {
7315
- this.name = "com.objectstack.runtime.auth-proxy";
7316
- this.version = "1.0.0";
7317
- this.init = async (_ctx) => {
7318
- };
7319
- this.start = async (ctx) => {
7320
- ctx.hook("kernel:ready", async () => {
7321
- let httpServer;
7322
- try {
7323
- httpServer = ctx.getService("http-server");
7324
- } catch {
7325
- ctx.logger?.warn?.("[AuthProxyPlugin] http-server not available \u2014 auth routes not mounted");
7326
- return;
7327
- }
7328
- if (!httpServer || typeof httpServer.getRawApp !== "function") {
7329
- ctx.logger?.warn?.("[AuthProxyPlugin] http-server missing getRawApp() \u2014 auth routes not mounted");
7330
- return;
7331
- }
7332
- const rawApp = httpServer.getRawApp();
7333
- const kernelManager = ctx.getService("kernel-manager");
7334
- const envRegistry = ctx.getService("env-registry");
7335
- const handler = async (c) => {
7336
- try {
7337
- const url = new URL(c.req.url);
7338
- const host = url.hostname;
7339
- let environmentId;
7340
- try {
7341
- const env = await envRegistry.resolveByHostname(host);
7342
- environmentId = env?.environmentId;
7343
- } catch {
7344
- }
7345
- if (!environmentId) {
7346
- return c.json({ error: "project_not_found", host }, 404);
7347
- }
7348
- const projectKernel = await kernelManager.getOrCreate(environmentId);
7349
- let authSvc;
7350
- try {
7351
- authSvc = await projectKernel.getServiceAsync?.("auth");
7352
- } catch {
7353
- authSvc = void 0;
7354
- }
7355
- if (!authSvc) {
7356
- try {
7357
- authSvc = projectKernel.getService?.("auth");
7358
- } catch {
7359
- }
7360
- }
7361
- const subPath = url.pathname.startsWith(AUTH_PREFIX + "/") ? url.pathname.substring(AUTH_PREFIX.length + 1) : "";
7362
- if (c.req.method === "GET" && (subPath === "config" || subPath === "bootstrap-status")) {
7363
- if (subPath === "config") {
7364
- try {
7365
- const config = typeof authSvc?.getPublicConfig === "function" ? authSvc.getPublicConfig() : null;
7366
- if (config) {
7367
- return c.json({ success: true, data: config });
7368
- }
7369
- return c.json({ success: false, error: { code: "auth_config_unavailable", message: "AuthManager has no getPublicConfig()" } }, 503);
7370
- } catch (e) {
7371
- return c.json({ success: false, error: { code: "auth_config_error", message: String(e?.message ?? e) } }, 500);
7372
- }
7373
- }
7374
- try {
7375
- const dataEngine = typeof authSvc?.getDataEngine === "function" ? authSvc.getDataEngine() : null;
7376
- if (!dataEngine || typeof dataEngine.count !== "function") {
7377
- return c.json({ hasOwner: true });
7378
- }
7379
- const count = await dataEngine.count("sys_user", {});
7380
- return c.json({ hasOwner: (count ?? 0) > 0 });
7381
- } catch {
7382
- return c.json({ hasOwner: true });
7383
- }
7384
- }
7385
- if (c.req.method === "POST" && subPath === "sso-handoff-issue") {
7386
- try {
7387
- const expected = (process.env.OS_CLOUD_API_KEY ?? "").trim();
7388
- if (!expected) {
7389
- return c.json({ error: "sso_handoff_disabled", reason: "OS_CLOUD_API_KEY unset on env runtime" }, 503);
7390
- }
7391
- const authz = c.req.header("authorization") ?? "";
7392
- const provided = authz.toLowerCase().startsWith("bearer ") ? authz.slice(7).trim() : "";
7393
- if (!provided || provided !== expected) {
7394
- return c.json({ error: "unauthorized" }, 401);
7395
- }
7396
- if (typeof authSvc?.getAuthContext !== "function") {
7397
- return c.json({ error: "auth_service_unavailable" }, 503);
7398
- }
7399
- const handoffAuthCtx = await authSvc.getAuthContext();
7400
- const internal = handoffAuthCtx?.internalAdapter;
7401
- if (!internal?.createVerificationValue) {
7402
- return c.json({ error: "verification_api_unavailable" }, 503);
7403
- }
7404
- let body = {};
7405
- try {
7406
- body = await c.req.json();
7407
- } catch {
7408
- body = {};
7409
- }
7410
- const email = String(body?.email ?? "").toLowerCase().trim();
7411
- if (!email) return c.json({ error: "email_required" }, 400);
7412
- const name = body?.name == null ? null : String(body.name);
7413
- const by = body?.by == null ? "service" : String(body.by);
7414
- const envIdInBody = body?.envId == null ? null : String(body.envId);
7415
- const handoff = randomUUID2().replace(/-/g, "") + randomUUID2().replace(/-/g, "");
7416
- const ttlSec = 60;
7417
- const expiresAt = new Date(Date.now() + ttlSec * 1e3);
7418
- await internal.createVerificationValue({
7419
- identifier: `sso-handoff:${handoff}`,
7420
- value: JSON.stringify({ email, name, by, envId: envIdInBody ?? environmentId }),
7421
- expiresAt
7422
- });
7423
- return c.json({
7424
- token: handoff,
7425
- expiresAt: expiresAt.toISOString(),
7426
- ttlSec
7427
- });
7428
- } catch (err) {
7429
- ctx.logger?.error?.("[AuthProxyPlugin] sso-handoff-issue failed", err instanceof Error ? err : new Error(String(err)));
7430
- return c.json({ error: "sso_handoff_issue_failed", message: String(err?.message ?? err) }, 500);
7431
- }
7432
- }
7433
- if (c.req.method === "GET" && subPath === "sso-exchange") {
7434
- try {
7435
- const token = (url.searchParams.get("token") ?? "").trim();
7436
- const nextRaw = url.searchParams.get("next") ?? "/";
7437
- const next = nextRaw.startsWith("/") ? nextRaw : "/";
7438
- if (!token) return c.text("missing token", 400);
7439
- if (typeof authSvc?.getAuthContext !== "function") {
7440
- return c.text("auth service unavailable", 503);
7441
- }
7442
- const authCtx = await authSvc.getAuthContext();
7443
- const internal = authCtx?.internalAdapter;
7444
- if (!internal?.consumeVerificationValue) {
7445
- return c.text("verification API unavailable", 503);
7446
- }
7447
- const consumed = await internal.consumeVerificationValue(`sso-handoff:${token}`);
7448
- if (!consumed) return c.text("invalid or expired token", 401);
7449
- const expiresAt = consumed?.expiresAt ? new Date(consumed.expiresAt).getTime() : 0;
7450
- if (!expiresAt || expiresAt < Date.now()) return c.text("expired token", 401);
7451
- let payload = {};
7452
- try {
7453
- payload = JSON.parse(String(consumed.value));
7454
- } catch {
7455
- payload = { email: String(consumed.value) };
7456
- }
7457
- const email = String(payload.email ?? "").toLowerCase().trim();
7458
- if (!email) return c.text("handoff missing email", 400);
7459
- const found = await internal.findUserByEmail(email, { includeAccounts: true });
7460
- let userId = found?.user?.id;
7461
- let hasCredentialAccount = (found?.accounts ?? []).some((a) => a.providerId === "credential" && a.password);
7462
- if (!userId) {
7463
- const created = await internal.createUser({
7464
- email,
7465
- name: payload.name ?? email,
7466
- emailVerified: true
7467
- });
7468
- userId = created?.id;
7469
- hasCredentialAccount = false;
7470
- }
7471
- if (!userId) return c.text("failed to provision user", 500);
7472
- const session = await internal.createSession(userId, false);
7473
- const rawToken = session?.token;
7474
- const sessionExpiresAt = session?.expiresAt ? new Date(session.expiresAt) : new Date(Date.now() + 7 * 24 * 3600 * 1e3);
7475
- if (!rawToken) return c.text("failed to mint session", 500);
7476
- const secret = authCtx?.secret ?? "";
7477
- if (!secret) return c.text("auth secret unavailable", 503);
7478
- const cookieName = authCtx?.authCookies?.sessionToken?.name ?? "better-auth.session_token";
7479
- const cookieAttrs = authCtx?.authCookies?.sessionToken?.attributes ?? {};
7480
- const encoded = signSessionCookieValue(rawToken, secret);
7481
- const maxAgeSec = Math.max(60, Math.floor((sessionExpiresAt.getTime() - Date.now()) / 1e3));
7482
- const setCookie = buildSetCookieHeader(cookieName, encoded, cookieAttrs, maxAgeSec);
7483
- const finalNext = hasCredentialAccount ? next : `/_console/system/profile?recovery_needed=true&next=${encodeURIComponent(next)}`;
7484
- const headers = new Headers();
7485
- headers.set("Set-Cookie", setCookie);
7486
- headers.set("Location", finalNext);
7487
- headers.set("Cache-Control", "no-store");
7488
- return new Response(null, { status: 302, headers });
7489
- } catch (err) {
7490
- ctx.logger?.error?.("[AuthProxyPlugin] sso-exchange failed", err instanceof Error ? err : new Error(String(err)));
7491
- return c.text(`sso-exchange failed: ${err?.message ?? String(err)}`, 500);
7492
- }
7493
- }
7494
- if (c.req.method === "POST" && subPath === "set-initial-password") {
7495
- try {
7496
- let body = {};
7497
- try {
7498
- body = await c.req.json();
7499
- } catch {
7500
- body = {};
7501
- }
7502
- const newPassword = body?.newPassword;
7503
- if (typeof newPassword !== "string" || newPassword.length === 0) {
7504
- return c.json({ success: false, error: { code: "invalid_request", message: "newPassword is required" } }, 400);
7505
- }
7506
- if (typeof authSvc?.getAuthContext !== "function") {
7507
- return c.json({ success: false, error: { code: "unavailable", message: "Auth context unavailable" } }, 503);
7508
- }
7509
- let userId;
7510
- try {
7511
- const api = typeof authSvc.getApi === "function" ? await authSvc.getApi() : null;
7512
- const session = await api?.getSession?.({ headers: c.req.raw.headers });
7513
- userId = session?.user?.id ? String(session.user.id) : void 0;
7514
- } catch {
7515
- }
7516
- if (!userId) {
7517
- return c.json({ success: false, error: { code: "unauthorized", message: "Sign in first" } }, 401);
7518
- }
7519
- const setPwCtx = await authSvc.getAuthContext();
7520
- if (!setPwCtx?.internalAdapter || !setPwCtx?.password) {
7521
- return c.json({ success: false, error: { code: "unavailable", message: "Auth context unavailable" } }, 503);
7522
- }
7523
- const minLen = setPwCtx.password?.config?.minPasswordLength ?? 8;
7524
- const maxLen = setPwCtx.password?.config?.maxPasswordLength ?? 128;
7525
- if (newPassword.length < minLen) {
7526
- return c.json({ success: false, error: { code: "password_too_short", message: `Password must be at least ${minLen} characters` } }, 400);
7527
- }
7528
- if (newPassword.length > maxLen) {
7529
- return c.json({ success: false, error: { code: "password_too_long", message: `Password must be at most ${maxLen} characters` } }, 400);
7530
- }
7531
- const accounts = await setPwCtx.internalAdapter.findAccounts(userId);
7532
- const existingCredential = accounts?.find?.((a) => a.providerId === "credential" && a.password);
7533
- if (existingCredential) {
7534
- return c.json({ success: false, error: { code: "credential_account_exists", message: "A local password is already set for this account. Use change-password instead." } }, 409);
7535
- }
7536
- const passwordHash = await setPwCtx.password.hash(newPassword);
7537
- await setPwCtx.internalAdapter.createAccount({
7538
- userId,
7539
- providerId: "credential",
7540
- accountId: userId,
7541
- password: passwordHash
7542
- });
7543
- return c.json({ success: true });
7544
- } catch (err) {
7545
- ctx.logger?.error?.("[AuthProxyPlugin] set-initial-password failed", err instanceof Error ? err : new Error(String(err)));
7546
- return c.json({ success: false, error: { code: "set_password_failed", message: String(err?.message ?? err) } }, 500);
7547
- }
7548
- }
7549
- const fn = await resolveAuthHandler(authSvc);
7550
- if (!fn) {
7551
- return c.json({ error: "auth_service_unavailable", environmentId }, 503);
7552
- }
7553
- const resp = await fn(c.req.raw);
7554
- const rootDomain = process.env.OS_ROOT_DOMAIN || "";
7555
- if (rootDomain) {
7556
- const leakyDomain = rootDomain.startsWith(".") ? rootDomain : `.${rootDomain}`;
7557
- const leakyNames = [
7558
- "__Secure-better-auth.session_token",
7559
- "better-auth.session_token",
7560
- "__Secure-better-auth.state",
7561
- "better-auth.state",
7562
- "__Secure-better-auth.csrf_token",
7563
- "better-auth.csrf_token"
7564
- ];
7565
- try {
7566
- for (const n of leakyNames) {
7567
- const isSecure = n.startsWith("__Secure-");
7568
- const attrs = `Max-Age=0; Path=/; Domain=${leakyDomain}; SameSite=Lax${isSecure ? "; Secure" : ""}`;
7569
- resp.headers?.append?.("Set-Cookie", `${n}=; ${attrs}`);
7570
- }
7571
- } catch {
7572
- }
7573
- }
7574
- return resp;
7575
- } catch (err) {
7576
- ctx.logger?.error?.("[AuthProxyPlugin] auth dispatch failed", {
7577
- error: err?.message,
7578
- stack: err?.stack
7579
- });
7580
- return c.json({
7581
- error: "auth_dispatch_failed",
7582
- message: err?.message ?? String(err)
7583
- }, 500);
7584
- }
7585
- };
7586
- if (typeof rawApp.all === "function") {
7587
- rawApp.all(`${AUTH_PREFIX}/*`, handler);
7588
- } else {
7589
- for (const m of ["get", "post", "put", "delete", "patch", "options"]) {
7590
- try {
7591
- rawApp[m]?.(`${AUTH_PREFIX}/*`, handler);
7592
- } catch {
7593
- }
7594
- }
7595
- }
7596
- ctx.logger?.info?.(`[AuthProxyPlugin] auth proxy mounted at ${AUTH_PREFIX}/*`);
7597
- });
7598
- };
7599
- }
7600
- };
7601
-
7602
- // src/cloud/cloud-url.ts
7603
- var DEFAULT_CLOUD_URL = "https://cloud.objectos.ai";
7604
- function resolveCloudUrl(explicit) {
7605
- const raw = (explicit ?? process.env.OS_CLOUD_URL ?? "").trim();
7606
- const lower = raw.toLowerCase();
7607
- if (lower === "off" || lower === "none" || lower === "local" || lower === "disabled") {
7608
- return "";
7609
- }
7610
- const picked = raw || DEFAULT_CLOUD_URL;
7611
- return picked.replace(/\/+$/, "");
7612
- }
7613
-
7614
- // src/cloud/marketplace-public-url.ts
7615
- function resolveMarketplacePublicBaseUrl(explicit) {
7616
- const raw = (explicit ?? process.env.OS_MARKETPLACE_PUBLIC_BASE_URL ?? "").trim();
7617
- const lower = raw.toLowerCase();
7618
- if (!raw || lower === "off" || lower === "none" || lower === "disabled" || lower === "false") {
7619
- return "";
7620
- }
7621
- return raw.replace(/\/+$/, "");
7622
- }
7623
- function publicMarketplaceKeyForApiPath(pathname) {
7624
- const prefix = "/api/v1/marketplace/packages";
7625
- if (pathname === prefix) return "packages.json";
7626
- if (!pathname.startsWith(`${prefix}/`)) return null;
7627
- const tail = pathname.slice(prefix.length + 1);
7628
- if (!tail) return null;
7629
- const parts = tail.split("/");
7630
- if (parts.length === 1) {
7631
- const id = decodeURIComponent(parts[0] ?? "");
7632
- if (!id) return null;
7633
- return `packages/${encodeURIComponent(id)}.json`;
7634
- }
7635
- if (parts.length === 4 && parts[1] === "versions" && parts[3] === "manifest") {
7636
- const id = decodeURIComponent(parts[0] ?? "");
7637
- const versionId = decodeURIComponent(parts[2] ?? "");
7638
- if (!id || !versionId) return null;
7639
- return `packages/${encodeURIComponent(id)}/versions/${encodeURIComponent(versionId)}/manifest.json`;
7640
- }
7641
- return null;
7642
- }
7643
-
7644
- // src/cloud/marketplace-proxy-plugin.ts
7645
- var MARKETPLACE_PREFIX = "/api/v1/marketplace";
7646
- var DEFAULT_LRU_MAX = 200;
7647
- var LIST_TTL_MS = 30 * 60 * 1e3;
7648
- var PACKAGE_TTL_MS = 2 * 60 * 60 * 1e3;
7649
- var VERSION_TTL_MS = 24 * 60 * 60 * 1e3;
7650
- function ttlForPath(pathname) {
7651
- if (/\/packages\/[^/]+\/versions\//.test(pathname)) return VERSION_TTL_MS;
7652
- if (/\/packages\/[^/]+/.test(pathname)) return PACKAGE_TTL_MS;
7653
- return LIST_TTL_MS;
7654
- }
7655
- var LruTtlCache = class {
7656
- constructor(max) {
7657
- this.max = max;
7658
- this.map = /* @__PURE__ */ new Map();
6254
+ // src/cloud/marketplace-proxy-plugin.ts
6255
+ var MARKETPLACE_PREFIX = "/api/v1/marketplace";
6256
+ var DEFAULT_LRU_MAX = 200;
6257
+ var LIST_TTL_MS = 30 * 60 * 1e3;
6258
+ var PACKAGE_TTL_MS = 2 * 60 * 60 * 1e3;
6259
+ var VERSION_TTL_MS = 24 * 60 * 60 * 1e3;
6260
+ function ttlForPath(pathname) {
6261
+ if (/\/packages\/[^/]+\/versions\//.test(pathname)) return VERSION_TTL_MS;
6262
+ if (/\/packages\/[^/]+/.test(pathname)) return PACKAGE_TTL_MS;
6263
+ return LIST_TTL_MS;
6264
+ }
6265
+ var LruTtlCache = class {
6266
+ constructor(max) {
6267
+ this.max = max;
6268
+ this.map = /* @__PURE__ */ new Map();
7659
6269
  }
7660
6270
  get(key) {
7661
6271
  const entry = this.map.get(key);
@@ -7932,359 +6542,10 @@ async function consumeAndMaybeCache(resp, key, pathname, method, cache) {
7932
6542
  return new Response(outBody, { status: resp.status, headers: respHeaders });
7933
6543
  }
7934
6544
 
7935
- // src/cloud/runtime-config-plugin.ts
7936
- var RuntimeConfigPlugin = class {
7937
- constructor(config = {}) {
7938
- this.name = "com.objectstack.runtime.runtime-config";
7939
- this.version = "1.0.0";
7940
- this.init = async (_ctx) => {
7941
- };
7942
- this.start = async (ctx) => {
7943
- ctx.hook("kernel:ready", async () => {
7944
- let httpServer;
7945
- try {
7946
- httpServer = ctx.getService("http-server");
7947
- } catch {
7948
- ctx.logger?.warn?.("[RuntimeConfigPlugin] http-server not available \u2014 runtime/config not mounted");
7949
- return;
7950
- }
7951
- if (!httpServer || typeof httpServer.getRawApp !== "function") {
7952
- ctx.logger?.warn?.("[RuntimeConfigPlugin] http-server missing getRawApp() \u2014 runtime/config not mounted");
7953
- return;
7954
- }
7955
- const rawApp = httpServer.getRawApp();
7956
- const features = {
7957
- installLocal: this.installLocal,
7958
- marketplace: true,
7959
- aiStudio: this.aiStudio
7960
- };
7961
- let envRegistry = null;
7962
- try {
7963
- envRegistry = ctx.getService("env-registry");
7964
- } catch {
7965
- }
7966
- const handler = async (c) => {
7967
- const rawHost = c.req.header("host") ?? "";
7968
- const host = rawHost.split(":")[0].toLowerCase().trim();
7969
- let defaultEnvironmentId;
7970
- let defaultOrgId;
7971
- let resolvedSingleEnv = this.singleEnvironment;
7972
- const resolveFn = typeof envRegistry?.resolveByHostname === "function" ? envRegistry.resolveByHostname.bind(envRegistry) : typeof envRegistry?.resolveHostname === "function" ? envRegistry.resolveHostname.bind(envRegistry) : null;
7973
- if (resolveFn && host) {
7974
- try {
7975
- const resolved = await resolveFn(host);
7976
- if (resolved?.environmentId) {
7977
- defaultEnvironmentId = String(resolved.environmentId);
7978
- const orgId = resolved.organizationId ?? resolved.organization_id;
7979
- if (orgId) defaultOrgId = String(orgId);
7980
- resolvedSingleEnv = true;
7981
- }
7982
- } catch {
7983
- }
7984
- }
7985
- return c.json({
7986
- cloudUrl: this.cloudUrl,
7987
- singleEnvironment: resolvedSingleEnv,
7988
- defaultOrgId,
7989
- defaultEnvironmentId,
7990
- features,
7991
- branding: {
7992
- productName: this.productName,
7993
- productShortName: this.productShortName
7994
- }
7995
- });
7996
- };
7997
- rawApp.get("/api/v1/runtime/config", handler);
7998
- rawApp.get("/api/v1/studio/runtime-config", handler);
7999
- ctx.logger?.info?.("[RuntimeConfigPlugin] mounted /api/v1/runtime/config", {
8000
- cloudUrl: this.cloudUrl || "(empty)",
8001
- installLocal: this.installLocal,
8002
- perHostEnvResolution: !!envRegistry
8003
- });
8004
- });
8005
- };
8006
- this.destroy = async () => {
8007
- };
8008
- this.cloudUrl = config.controlPlaneUrl === "" ? "" : resolveCloudUrl(config.controlPlaneUrl) ?? "";
8009
- this.installLocal = !!config.installLocal;
8010
- this.aiStudio = config.aiStudio !== false;
8011
- this.singleEnvironment = !!config.singleEnvironment;
8012
- const envName = (typeof process !== "undefined" ? process.env?.OS_PRODUCT_NAME : void 0)?.trim();
8013
- const envShort = (typeof process !== "undefined" ? process.env?.OS_PRODUCT_SHORT_NAME : void 0)?.trim();
8014
- this.productName = (config.productName ?? envName ?? "ObjectOS").trim() || "ObjectOS";
8015
- this.productShortName = (config.productShortName ?? envShort ?? this.productName).trim() || this.productName;
8016
- }
8017
- };
8018
-
8019
- // src/cloud/file-artifact-api-client.ts
8020
- import { readFile as readFile2, stat } from "fs/promises";
8021
- import { resolve as resolvePath4 } from "path";
8022
- var FileArtifactApiClient = class {
8023
- constructor(config = {}) {
8024
- const cwd = process.cwd();
8025
- this.artifactPath = resolvePath4(
8026
- cwd,
8027
- config.artifactPath ?? process.env.OS_ARTIFACT_PATH ?? "dist/objectstack.json"
8028
- );
8029
- this.environmentId = config.environmentId ?? process.env.OS_ENVIRONMENT_ID ?? "proj_local";
8030
- this.organizationId = config.organizationId ?? process.env.OS_ORGANIZATION_ID ?? "org_local";
8031
- this.overrideRuntime = config.runtime;
8032
- this.watch = config.watch ?? true;
8033
- this.logger = config.logger ?? console;
8034
- }
8035
- async resolveHostname(_host) {
8036
- const runtime = this.overrideRuntime ?? await this.readRuntimeFromArtifact();
8037
- return {
8038
- environmentId: this.environmentId,
8039
- organizationId: this.organizationId,
8040
- ...runtime ? { runtime } : {}
8041
- };
8042
- }
8043
- async fetchArtifact(_environmentId, _opts) {
8044
- return this.loadArtifact();
8045
- }
8046
- async lookupProjectByShortId(_shortId) {
8047
- return { environmentId: this.environmentId, organizationId: this.organizationId };
8048
- }
8049
- async fetchBranchHead(_environmentId, _branchName) {
8050
- const artifact = await this.loadArtifact();
8051
- return artifact ? { commitId: artifact.commitId ?? "local", publishedAt: null } : null;
8052
- }
8053
- invalidate(_environmentId) {
8054
- this.cached = void 0;
8055
- }
8056
- clear() {
8057
- this.cached = void 0;
8058
- }
8059
- async loadArtifact() {
8060
- try {
8061
- const stats = await stat(this.artifactPath);
8062
- const mtimeMs = stats.mtimeMs;
8063
- if (!this.watch && this.cached) return this.cached.response;
8064
- if (this.cached && this.cached.mtimeMs === mtimeMs) return this.cached.response;
8065
- const raw = await readFile2(this.artifactPath, "utf8");
8066
- const parsed = JSON.parse(raw);
8067
- const isEnvelope = parsed && typeof parsed === "object" && typeof parsed.metadata === "object" && parsed.metadata !== null;
8068
- const metadata = isEnvelope ? parsed.metadata : parsed;
8069
- const runtime = this.overrideRuntime ?? (isEnvelope ? parsed.runtime : void 0) ?? this.deriveRuntimeFromMetadata(metadata) ?? this.defaultLocalSqliteRuntime();
8070
- const response = {
8071
- schemaVersion: parsed.schemaVersion ?? "1",
8072
- environmentId: parsed.environmentId ?? this.environmentId,
8073
- commitId: parsed.commitId ?? "local",
8074
- checksum: parsed.checksum ?? "",
8075
- publishedAt: parsed.publishedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
8076
- metadata,
8077
- functions: parsed.functions,
8078
- manifest: parsed.manifest,
8079
- runtime: {
8080
- organizationId: this.organizationId,
8081
- ...runtime
8082
- }
8083
- };
8084
- this.cached = { mtimeMs, response };
8085
- return response;
8086
- } catch (err) {
8087
- this.logger.error?.("[FileArtifactApiClient] failed to load artifact", {
8088
- artifactPath: this.artifactPath,
8089
- error: err?.message ?? err
8090
- });
8091
- return null;
8092
- }
8093
- }
8094
- async readRuntimeFromArtifact() {
8095
- const artifact = await this.loadArtifact();
8096
- return artifact?.runtime;
8097
- }
8098
- deriveRuntimeFromMetadata(metadata) {
8099
- const datasources = metadata?.datasources;
8100
- if (!Array.isArray(datasources) || datasources.length === 0) return void 0;
8101
- const mapping = metadata?.datasourceMapping;
8102
- let preferredName;
8103
- if (mapping) {
8104
- const def = mapping.find((m) => m?.default === true);
8105
- if (def?.datasource) preferredName = def.datasource;
8106
- }
8107
- const ds = preferredName ? datasources.find((d) => d?.name === preferredName) ?? datasources[0] : datasources[0];
8108
- if (!ds || typeof ds !== "object") return void 0;
8109
- const config = ds.config ?? {};
8110
- const url = config.url ?? config.connectionString ?? config.connection ?? config.filename;
8111
- const driver = ds.driver;
8112
- if (typeof driver !== "string" || typeof url !== "string") return void 0;
8113
- return {
8114
- databaseDriver: driver,
8115
- databaseUrl: url,
8116
- databaseAuthToken: typeof config.authToken === "string" ? config.authToken : void 0
8117
- };
8118
- }
8119
- defaultLocalSqliteRuntime() {
8120
- const cwd = process.cwd();
8121
- const dbPath = resolvePath4(cwd, ".objectstack/data", `${this.environmentId}.db`);
8122
- return {
8123
- databaseDriver: "sqlite",
8124
- databaseUrl: `file:${dbPath}`
8125
- };
8126
- }
8127
- };
8128
-
8129
- // src/cloud/objectos-stack.ts
8130
- async function createHostEnginePlugins() {
8131
- const { ObjectQLPlugin } = await import("@objectstack/objectql");
8132
- const { DriverPlugin: DriverPlugin2 } = await Promise.resolve().then(() => (init_driver_plugin(), driver_plugin_exports));
8133
- const { MetadataPlugin } = await import("@objectstack/metadata");
8134
- const { InMemoryDriver } = await import("@objectstack/driver-memory");
8135
- const driver = new InMemoryDriver();
8136
- const driverName = "memory";
8137
- const oqlRef = { ql: null };
8138
- const objectql = {
8139
- name: "com.objectstack.engine.objectql",
8140
- version: "0.0.0",
8141
- async init(ctx) {
8142
- const plugin = new ObjectQLPlugin();
8143
- this._inner = plugin;
8144
- if (plugin.init) await plugin.init(ctx);
8145
- oqlRef.ql = plugin.ql ?? plugin;
8146
- },
8147
- async start(ctx) {
8148
- const plugin = this._inner;
8149
- if (plugin?.start) await plugin.start(ctx);
8150
- },
8151
- async destroy() {
8152
- const plugin = this._inner;
8153
- if (plugin?.destroy) await plugin.destroy();
8154
- else if (plugin?.stop) await plugin.stop();
8155
- }
8156
- };
8157
- const datasourceMapping = {
8158
- name: "objectos-host-datasource-mapping",
8159
- version: "0.0.0",
8160
- dependencies: ["com.objectstack.engine.objectql"],
8161
- async init() {
8162
- const ql = oqlRef.ql;
8163
- if (ql?.setDatasourceMapping) {
8164
- ql.setDatasourceMapping([
8165
- { default: true, datasource: `com.objectstack.driver.${driverName}` }
8166
- ]);
8167
- }
8168
- }
8169
- };
8170
- const driverPlugin = new DriverPlugin2(driver, driverName);
8171
- const metadata = new MetadataPlugin({
8172
- watch: false,
8173
- // The host kernel is a routing shell. It doesn't own metadata —
8174
- // every per-project kernel registers its own.
8175
- registerSystemObjects: false
8176
- });
8177
- return [objectql, datasourceMapping, driverPlugin, metadata];
8178
- }
8179
- var ObjectOSEnvironmentPlugin = class {
8180
- constructor(config) {
8181
- this.name = "com.objectstack.runtime.objectos-environment";
8182
- this.version = "1.0.0";
8183
- this.init = async (ctx) => {
8184
- const client = this.config.client ?? (this.config.controlPlaneUrl === "file" ? new FileArtifactApiClient({
8185
- ...this.config.fileConfig ?? {},
8186
- logger: ctx.logger
8187
- }) : new ArtifactApiClient({
8188
- controlPlaneUrl: this.config.controlPlaneUrl,
8189
- apiKey: this.config.controlPlaneApiKey,
8190
- cacheTtlMs: this.config.artifactCacheTtlMs,
8191
- logger: ctx.logger
8192
- }));
8193
- this.client = client;
8194
- const envRegistry = new ArtifactEnvironmentRegistry({
8195
- client,
8196
- cacheTtlMs: this.config.envCacheTtlMs,
8197
- logger: ctx.logger
8198
- });
8199
- const factory = new ArtifactKernelFactory({
8200
- client,
8201
- envRegistry,
8202
- logger: ctx.logger,
8203
- defaultRequires: this.config.defaultRequires
8204
- });
8205
- const kernelManager = new KernelManager({
8206
- factory,
8207
- maxSize: this.config.kernelCacheSize,
8208
- ttlMs: this.config.kernelTtlMs,
8209
- logger: ctx.logger,
8210
- // Only the HTTP client exposes /freshness; file-mode (CLI dev)
8211
- // has no upstream to probe.
8212
- freshnessProbe: this.config.controlPlaneUrl === "file" ? void 0 : async (envId, builtAtMs) => {
8213
- const fresh = await client.getFreshness(envId);
8214
- if (!fresh) return false;
8215
- const t = fresh.lastPublishedAt ? Date.parse(fresh.lastPublishedAt) : NaN;
8216
- if (!Number.isFinite(t)) return false;
8217
- if (t <= builtAtMs) return false;
8218
- try {
8219
- client.invalidate(envId);
8220
- } catch {
8221
- }
8222
- return true;
8223
- }
8224
- });
8225
- this.kernelManager = kernelManager;
8226
- ctx.registerService("env-registry", envRegistry);
8227
- ctx.registerService("kernel-manager", kernelManager);
8228
- ctx.registerService("artifact-api-client", client);
8229
- ctx.logger.info?.("ObjectOSEnvironmentPlugin: registered env-registry + kernel-manager", {
8230
- mode: this.config.controlPlaneUrl === "file" ? "file" : "http",
8231
- controlPlaneUrl: this.config.controlPlaneUrl
8232
- });
8233
- };
8234
- this.destroy = async () => {
8235
- try {
8236
- await this.kernelManager?.evictAll();
8237
- } catch {
8238
- }
8239
- try {
8240
- this.client?.clear();
8241
- } catch {
8242
- }
8243
- };
8244
- this.config = config;
8245
- }
8246
- };
8247
- async function createObjectOSStack(config) {
8248
- if (!config.controlPlaneUrl && !config.client) {
8249
- throw new Error("[createObjectOSStack] either controlPlaneUrl or client is required");
8250
- }
8251
- const merged = {
8252
- ...config,
8253
- kernelCacheSize: Number(process.env.OS_KERNEL_CACHE_SIZE ?? config.kernelCacheSize ?? 32),
8254
- kernelTtlMs: Number(process.env.OS_KERNEL_TTL_MS ?? config.kernelTtlMs ?? 15 * 60 * 1e3),
8255
- envCacheTtlMs: Number(process.env.OS_ENV_CACHE_TTL_MS ?? config.envCacheTtlMs ?? 5 * 60 * 1e3),
8256
- artifactCacheTtlMs: Number(process.env.OS_ARTIFACT_CACHE_TTL_MS ?? config.artifactCacheTtlMs ?? 5 * 60 * 1e3)
8257
- };
8258
- const enginePlugins = await createHostEnginePlugins();
8259
- return {
8260
- plugins: [
8261
- ...enginePlugins,
8262
- new ObjectOSEnvironmentPlugin(merged),
8263
- new AuthProxyPlugin(),
8264
- new MarketplaceProxyPlugin({ controlPlaneUrl: merged.controlPlaneUrl === "file" ? void 0 : merged.controlPlaneUrl }),
8265
- new RuntimeConfigPlugin({ controlPlaneUrl: merged.controlPlaneUrl === "file" ? void 0 : merged.controlPlaneUrl, installLocal: false }),
8266
- // Host-supplied product/policy plugins (the official seam — see
8267
- // ObjectOSStackConfig.extraPlugins). Appended last so they mount
8268
- // after the framework defaults.
8269
- ...config.extraPlugins ?? []
8270
- ],
8271
- api: {
8272
- enableProjectScoping: true,
8273
- projectResolution: "auto",
8274
- // ObjectOS is multi-tenant: anonymous /api/v1/data/* must never
8275
- // leak per-project data across organisations. AuthProxyPlugin
8276
- // verifies upstream tokens and populates ctx.userId; requireAuth
8277
- // turns missing userId into 401 at the REST layer before the
8278
- // request reaches the per-project kernel.
8279
- requireAuth: true
8280
- }
8281
- };
8282
- }
8283
-
8284
6545
  // src/cloud/marketplace-install-local-plugin.ts
8285
6546
  import { existsSync as existsSync3, mkdirSync as mkdirSync4, readFileSync as readFileSync2, readdirSync, unlinkSync, writeFileSync as writeFileSync3 } from "fs";
8286
6547
  import { join as join2, resolve } from "path";
8287
- import { readEnvWithDeprecation as readEnvWithDeprecation4 } from "@objectstack/types";
6548
+ import { readEnvWithDeprecation as readEnvWithDeprecation3 } from "@objectstack/types";
8288
6549
  var ROUTE_BASE = "/api/v1/marketplace/install-local";
8289
6550
  var DEFAULT_DIR = ".objectstack/installed-packages";
8290
6551
  function safeFilename(manifestId) {
@@ -8828,7 +7089,7 @@ var MarketplaceInstallLocalPlugin = class {
8828
7089
  }
8829
7090
  }
8830
7091
  if (opts.seedNow && datasets.length > 0) {
8831
- const multiTenant = String(readEnvWithDeprecation4("OS_MULTI_ORG_ENABLED", "OS_MULTI_TENANT") ?? "false").toLowerCase() !== "false";
7092
+ const multiTenant = String(readEnvWithDeprecation3("OS_MULTI_ORG_ENABLED", "OS_MULTI_TENANT") ?? "false").toLowerCase() !== "false";
8832
7093
  try {
8833
7094
  const ql = ctx.getService("objectql");
8834
7095
  let metadata;
@@ -8946,6 +7207,90 @@ var MarketplaceInstallLocalPlugin = class {
8946
7207
  }
8947
7208
  };
8948
7209
 
7210
+ // src/cloud/runtime-config-plugin.ts
7211
+ var RuntimeConfigPlugin = class {
7212
+ constructor(config = {}) {
7213
+ this.name = "com.objectstack.runtime.runtime-config";
7214
+ this.version = "1.0.0";
7215
+ this.init = async (_ctx) => {
7216
+ };
7217
+ this.start = async (ctx) => {
7218
+ ctx.hook("kernel:ready", async () => {
7219
+ let httpServer;
7220
+ try {
7221
+ httpServer = ctx.getService("http-server");
7222
+ } catch {
7223
+ ctx.logger?.warn?.("[RuntimeConfigPlugin] http-server not available \u2014 runtime/config not mounted");
7224
+ return;
7225
+ }
7226
+ if (!httpServer || typeof httpServer.getRawApp !== "function") {
7227
+ ctx.logger?.warn?.("[RuntimeConfigPlugin] http-server missing getRawApp() \u2014 runtime/config not mounted");
7228
+ return;
7229
+ }
7230
+ const rawApp = httpServer.getRawApp();
7231
+ const features = {
7232
+ installLocal: this.installLocal,
7233
+ marketplace: true,
7234
+ aiStudio: this.aiStudio
7235
+ };
7236
+ let envRegistry = null;
7237
+ try {
7238
+ envRegistry = ctx.getService("env-registry");
7239
+ } catch {
7240
+ }
7241
+ const handler = async (c) => {
7242
+ const rawHost = c.req.header("host") ?? "";
7243
+ const host = rawHost.split(":")[0].toLowerCase().trim();
7244
+ let defaultEnvironmentId;
7245
+ let defaultOrgId;
7246
+ let resolvedSingleEnv = this.singleEnvironment;
7247
+ const resolveFn = typeof envRegistry?.resolveByHostname === "function" ? envRegistry.resolveByHostname.bind(envRegistry) : typeof envRegistry?.resolveHostname === "function" ? envRegistry.resolveHostname.bind(envRegistry) : null;
7248
+ if (resolveFn && host) {
7249
+ try {
7250
+ const resolved = await resolveFn(host);
7251
+ if (resolved?.environmentId) {
7252
+ defaultEnvironmentId = String(resolved.environmentId);
7253
+ const orgId = resolved.organizationId ?? resolved.organization_id;
7254
+ if (orgId) defaultOrgId = String(orgId);
7255
+ resolvedSingleEnv = true;
7256
+ }
7257
+ } catch {
7258
+ }
7259
+ }
7260
+ return c.json({
7261
+ cloudUrl: this.cloudUrl,
7262
+ singleEnvironment: resolvedSingleEnv,
7263
+ defaultOrgId,
7264
+ defaultEnvironmentId,
7265
+ features,
7266
+ branding: {
7267
+ productName: this.productName,
7268
+ productShortName: this.productShortName
7269
+ }
7270
+ });
7271
+ };
7272
+ rawApp.get("/api/v1/runtime/config", handler);
7273
+ rawApp.get("/api/v1/studio/runtime-config", handler);
7274
+ ctx.logger?.info?.("[RuntimeConfigPlugin] mounted /api/v1/runtime/config", {
7275
+ cloudUrl: this.cloudUrl || "(empty)",
7276
+ installLocal: this.installLocal,
7277
+ perHostEnvResolution: !!envRegistry
7278
+ });
7279
+ });
7280
+ };
7281
+ this.destroy = async () => {
7282
+ };
7283
+ this.cloudUrl = config.controlPlaneUrl === "" ? "" : resolveCloudUrl(config.controlPlaneUrl) ?? "";
7284
+ this.installLocal = !!config.installLocal;
7285
+ this.aiStudio = config.aiStudio !== false;
7286
+ this.singleEnvironment = !!config.singleEnvironment;
7287
+ const envName = (typeof process !== "undefined" ? process.env?.OS_PRODUCT_NAME : void 0)?.trim();
7288
+ const envShort = (typeof process !== "undefined" ? process.env?.OS_PRODUCT_SHORT_NAME : void 0)?.trim();
7289
+ this.productName = (config.productName ?? envName ?? "ObjectOS").trim() || "ObjectOS";
7290
+ this.productShortName = (config.productShortName ?? envShort ?? this.productName).trim() || this.productName;
7291
+ }
7292
+ };
7293
+
8949
7294
  // src/sandbox/script-runner.ts
8950
7295
  var UnimplementedScriptRunner = class {
8951
7296
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
@@ -8976,23 +7321,17 @@ import {
8976
7321
  createRestApiPlugin
8977
7322
  } from "@objectstack/rest";
8978
7323
  export * from "@objectstack/core";
8979
- import { readEnvWithDeprecation as readEnvWithDeprecation5, _resetEnvDeprecationWarnings } from "@objectstack/types";
7324
+ import { readEnvWithDeprecation as readEnvWithDeprecation4, _resetEnvDeprecationWarnings } from "@objectstack/types";
8980
7325
  export {
8981
7326
  AppPlugin,
8982
- ArtifactApiClient,
8983
- ArtifactEnvironmentRegistry,
8984
- ArtifactKernelFactory,
8985
- AuthProxyPlugin,
8986
7327
  DEFAULT_CLOUD_URL,
8987
7328
  DEFAULT_RATE_LIMITS,
8988
7329
  DriverPlugin,
8989
7330
  ExternalValidationPlugin,
8990
- FileArtifactApiClient,
8991
7331
  HttpDispatcher,
8992
7332
  HttpServer,
8993
7333
  InMemoryErrorReporter,
8994
7334
  InMemoryMetricsRegistry,
8995
- KernelManager,
8996
7335
  MarketplaceInstallLocalPlugin,
8997
7336
  MarketplaceProxyPlugin,
8998
7337
  MiddlewareManager,
@@ -9000,9 +7339,8 @@ export {
9000
7339
  NoopMetricsRegistry,
9001
7340
  OBSERVABILITY_ERRORS_SERVICE,
9002
7341
  OBSERVABILITY_METRICS_SERVICE,
9003
- ObjectKernel4 as ObjectKernel,
7342
+ ObjectKernel3 as ObjectKernel,
9004
7343
  ObservabilityServicePlugin,
9005
- PLATFORM_SSO_PROVIDER_ID,
9006
7344
  QuickJSScriptRunner,
9007
7345
  RUNTIME_METRICS,
9008
7346
  RateLimiter,
@@ -9017,8 +7355,6 @@ export {
9017
7355
  UnimplementedScriptRunner,
9018
7356
  _resetEnvDeprecationWarnings,
9019
7357
  actionBodyRunnerFactory,
9020
- backfillPlatformSsoClients,
9021
- buildPlatformSsoRedirectUri,
9022
7358
  buildSecurityHeaders,
9023
7359
  collectBundleActions,
9024
7360
  collectBundleFunctions,
@@ -9026,12 +7362,9 @@ export {
9026
7362
  createDefaultHostConfig,
9027
7363
  createDispatcherPlugin,
9028
7364
  createExternalValidationPlugin,
9029
- createObjectOSStack,
9030
7365
  createRestApiPlugin,
9031
7366
  createStandaloneStack,
9032
7367
  createSystemEnvironmentPlugin,
9033
- derivePlatformSsoClientId,
9034
- derivePlatformSsoClientSecret,
9035
7368
  extractRequestId,
9036
7369
  formatTraceparent,
9037
7370
  generateRequestId,
@@ -9041,13 +7374,12 @@ export {
9041
7374
  mergeRuntimeModule,
9042
7375
  parseTraceparent,
9043
7376
  readArtifactSource,
9044
- readEnvWithDeprecation5 as readEnvWithDeprecation,
7377
+ readEnvWithDeprecation4 as readEnvWithDeprecation,
9045
7378
  resolveCloudUrl,
9046
7379
  resolveDefaultArtifactPath,
9047
7380
  resolveErrorReporter,
9048
7381
  resolveMetrics,
9049
7382
  resolveObjectStackHome,
9050
- resolveRequestId,
9051
- seedPlatformSsoClient
7383
+ resolveRequestId
9052
7384
  };
9053
7385
  //# sourceMappingURL=index.js.map