@objectstack/cloud-connection 9.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,1780 @@
1
+ // src/cloud-url.ts
2
+ var DEFAULT_CLOUD_URL = "https://cloud.objectos.ai";
3
+ function resolveCloudUrl(explicit) {
4
+ const raw = (explicit ?? process.env.OS_CLOUD_URL ?? "").trim();
5
+ const lower = raw.toLowerCase();
6
+ if (lower === "off" || lower === "none" || lower === "local" || lower === "disabled") {
7
+ return "";
8
+ }
9
+ const picked = raw || DEFAULT_CLOUD_URL;
10
+ return picked.replace(/\/+$/, "");
11
+ }
12
+
13
+ // src/marketplace-public-url.ts
14
+ function resolveMarketplacePublicBaseUrl(explicit) {
15
+ const raw = (explicit ?? process.env.OS_MARKETPLACE_PUBLIC_BASE_URL ?? "").trim();
16
+ const lower = raw.toLowerCase();
17
+ if (!raw || lower === "off" || lower === "none" || lower === "disabled" || lower === "false") {
18
+ return "";
19
+ }
20
+ return raw.replace(/\/+$/, "");
21
+ }
22
+ function publicMarketplaceKeyForApiPath(pathname) {
23
+ const prefix = "/api/v1/marketplace/packages";
24
+ if (pathname === prefix) return "packages.json";
25
+ if (!pathname.startsWith(`${prefix}/`)) return null;
26
+ const tail = pathname.slice(prefix.length + 1);
27
+ if (!tail) return null;
28
+ const parts = tail.split("/");
29
+ if (parts.length === 1) {
30
+ const id = decodeURIComponent(parts[0] ?? "");
31
+ if (!id) return null;
32
+ return `packages/${encodeURIComponent(id)}.json`;
33
+ }
34
+ if (parts.length === 4 && parts[1] === "versions" && parts[3] === "manifest") {
35
+ const id = decodeURIComponent(parts[0] ?? "");
36
+ const versionId = decodeURIComponent(parts[2] ?? "");
37
+ if (!id || !versionId) return null;
38
+ return `packages/${encodeURIComponent(id)}/versions/${encodeURIComponent(versionId)}/manifest.json`;
39
+ }
40
+ return null;
41
+ }
42
+
43
+ // src/marketplace-ui.ts
44
+ var MARKETPLACE_BROWSE_UI_BUNDLE = {
45
+ id: "com.objectstack.cloud-connection.marketplace-browse-ui",
46
+ namespace: "sys",
47
+ version: "0.1.0",
48
+ type: "plugin",
49
+ scope: "system",
50
+ name: "Marketplace Browse UI",
51
+ description: "Setup navigation for the public marketplace catalog (browse).",
52
+ navigationContributions: [
53
+ {
54
+ app: "setup",
55
+ group: "group_apps",
56
+ priority: 100,
57
+ items: [
58
+ { id: "nav_marketplace_browse", type: "url", label: "Browse Marketplace", url: "/apps/setup/system/marketplace", icon: "store" }
59
+ ]
60
+ }
61
+ ]
62
+ };
63
+ var MarketplaceInstalledPage = {
64
+ name: "marketplace_installed",
65
+ label: "Installed Apps",
66
+ type: "app",
67
+ template: "default",
68
+ kind: "full",
69
+ isDefault: false,
70
+ regions: [
71
+ {
72
+ name: "header",
73
+ width: "full",
74
+ components: [
75
+ {
76
+ type: "page:header",
77
+ properties: {
78
+ title: "Installed Apps",
79
+ subtitle: "Marketplace packages currently installed into this runtime's kernel.",
80
+ icon: "package-check"
81
+ }
82
+ }
83
+ ]
84
+ },
85
+ {
86
+ name: "main",
87
+ width: "large",
88
+ components: [
89
+ { type: "marketplace:installed-list", properties: {} }
90
+ ]
91
+ }
92
+ ]
93
+ };
94
+ var MARKETPLACE_INSTALLED_UI_BUNDLE = {
95
+ id: "com.objectstack.cloud-connection.marketplace-installed-ui",
96
+ namespace: "sys",
97
+ version: "0.2.0",
98
+ type: "plugin",
99
+ scope: "system",
100
+ name: "Marketplace Installed UI",
101
+ description: "Installed Apps page + Setup navigation for locally-installed marketplace packages.",
102
+ pages: [MarketplaceInstalledPage],
103
+ navigationContributions: [
104
+ {
105
+ app: "setup",
106
+ group: "group_apps",
107
+ priority: 110,
108
+ items: [
109
+ { id: "nav_marketplace_installed", type: "page", pageName: "marketplace_installed", label: "Installed Apps", icon: "package-check" }
110
+ ]
111
+ }
112
+ ]
113
+ };
114
+
115
+ // src/marketplace-proxy-plugin.ts
116
+ var MARKETPLACE_PREFIX = "/api/v1/marketplace";
117
+ var DEFAULT_LRU_MAX = 200;
118
+ var LIST_TTL_MS = 30 * 60 * 1e3;
119
+ var PACKAGE_TTL_MS = 2 * 60 * 60 * 1e3;
120
+ var VERSION_TTL_MS = 24 * 60 * 60 * 1e3;
121
+ function ttlForPath(pathname) {
122
+ if (/\/packages\/[^/]+\/versions\//.test(pathname)) return VERSION_TTL_MS;
123
+ if (/\/packages\/[^/]+/.test(pathname)) return PACKAGE_TTL_MS;
124
+ return LIST_TTL_MS;
125
+ }
126
+ var LruTtlCache = class {
127
+ constructor(max) {
128
+ this.max = max;
129
+ this.map = /* @__PURE__ */ new Map();
130
+ }
131
+ get(key) {
132
+ const entry = this.map.get(key);
133
+ if (!entry) return void 0;
134
+ this.map.delete(key);
135
+ this.map.set(key, entry);
136
+ return entry;
137
+ }
138
+ set(key, entry) {
139
+ if (this.map.has(key)) this.map.delete(key);
140
+ this.map.set(key, entry);
141
+ while (this.map.size > this.max) {
142
+ const oldest = this.map.keys().next().value;
143
+ if (oldest === void 0) break;
144
+ this.map.delete(oldest);
145
+ }
146
+ }
147
+ clear() {
148
+ this.map.clear();
149
+ }
150
+ };
151
+ var MarketplaceProxyPlugin = class _MarketplaceProxyPlugin {
152
+ constructor(config = {}) {
153
+ this.name = "com.objectstack.runtime.marketplace-proxy";
154
+ this.version = "1.1.0";
155
+ this.init = async (_ctx) => {
156
+ };
157
+ this.start = async (ctx) => {
158
+ ctx.hook("kernel:ready", async () => {
159
+ try {
160
+ const manifest = ctx.getService("manifest");
161
+ manifest?.register?.(MARKETPLACE_BROWSE_UI_BUNDLE);
162
+ } catch {
163
+ }
164
+ let httpServer;
165
+ try {
166
+ httpServer = ctx.getService("http-server");
167
+ } catch {
168
+ ctx.logger?.warn?.("[MarketplaceProxyPlugin] http-server not available \u2014 marketplace routes not mounted");
169
+ return;
170
+ }
171
+ if (!httpServer || typeof httpServer.getRawApp !== "function") {
172
+ ctx.logger?.warn?.("[MarketplaceProxyPlugin] http-server missing getRawApp() \u2014 marketplace routes not mounted");
173
+ return;
174
+ }
175
+ const rawApp = httpServer.getRawApp();
176
+ const cloudUrl = this.cloudUrl;
177
+ const publicBaseUrl = this.publicBaseUrl;
178
+ const cache = this.cache;
179
+ if (publicBaseUrl) {
180
+ ctx.logger?.info?.(`[MarketplaceProxyPlugin] public R2 fast-path enabled \u2192 ${publicBaseUrl}`);
181
+ }
182
+ const handler = async (c, next) => {
183
+ if (!cloudUrl) {
184
+ return c.json({
185
+ success: false,
186
+ error: {
187
+ code: "marketplace_unavailable",
188
+ message: "No control-plane URL configured for this runtime (OS_CLOUD_URL)."
189
+ }
190
+ }, 503);
191
+ }
192
+ try {
193
+ const incomingUrl = new URL(c.req.url);
194
+ if (incomingUrl.pathname.startsWith(`${MARKETPLACE_PREFIX}/install-local`)) {
195
+ return next();
196
+ }
197
+ const method = String(c.req.method ?? "GET").toUpperCase();
198
+ if (publicBaseUrl && (method === "GET" || method === "HEAD")) {
199
+ const r2Resp = await tryPublicMarketplaceFetch(
200
+ publicBaseUrl,
201
+ incomingUrl,
202
+ method,
203
+ c.req.header("accept"),
204
+ ctx.logger
205
+ );
206
+ if (r2Resp) return r2Resp;
207
+ }
208
+ const target = `${cloudUrl}${incomingUrl.pathname}${incomingUrl.search}`;
209
+ if (method !== "GET" && method !== "HEAD") {
210
+ return next();
211
+ }
212
+ const accept = c.req.header("accept") ?? "application/json";
213
+ const acceptLang = c.req.header("accept-language") ?? "";
214
+ const cacheKey = `${incomingUrl.pathname}${incomingUrl.search}|al=${acceptLang}|a=${accept}`;
215
+ const reqCacheCtl = (c.req.header("cache-control") ?? "").toLowerCase();
216
+ const bypass = !cache || reqCacheCtl.includes("no-cache") || reqCacheCtl.includes("no-store");
217
+ const now = Date.now();
218
+ if (cache && !bypass) {
219
+ const hit = cache.get(cacheKey);
220
+ if (hit && hit.expiresAt > now) {
221
+ return buildCachedResponse(hit, method, "HIT");
222
+ }
223
+ if (hit) {
224
+ const revalHeaders = {
225
+ "Accept": accept,
226
+ "User-Agent": `objectos-marketplace-proxy/${_MarketplaceProxyPlugin.prototype.version ?? "1.0.0"}`
227
+ };
228
+ if (acceptLang) revalHeaders["Accept-Language"] = acceptLang;
229
+ if (hit.etag) revalHeaders["If-None-Match"] = hit.etag;
230
+ if (hit.lastModified) revalHeaders["If-Modified-Since"] = hit.lastModified;
231
+ const revalResp = await fetch(target, { method: "GET", headers: revalHeaders });
232
+ if (revalResp.status === 304) {
233
+ hit.expiresAt = now + hit.ttlMs;
234
+ const newEtag = revalResp.headers.get("etag");
235
+ const newLm = revalResp.headers.get("last-modified");
236
+ if (newEtag) hit.etag = newEtag;
237
+ if (newLm) hit.lastModified = newLm;
238
+ cache.set(cacheKey, hit);
239
+ return buildCachedResponse(hit, method, "REVALIDATED");
240
+ }
241
+ return await consumeAndMaybeCache(revalResp, cacheKey, incomingUrl.pathname, method, cache);
242
+ }
243
+ }
244
+ const reqHeaders = {
245
+ // Strip the inbound Host header — fetch will set
246
+ // it to the cloud host. Forward only the
247
+ // identifying headers cloud might log.
248
+ "Accept": accept,
249
+ "User-Agent": `objectos-marketplace-proxy/${_MarketplaceProxyPlugin.prototype.version ?? "1.0.0"}`
250
+ };
251
+ if (acceptLang) reqHeaders["Accept-Language"] = acceptLang;
252
+ const resp = await fetch(target, { method: "GET", headers: reqHeaders });
253
+ if (bypass || !cache) {
254
+ return await passthroughResponse(resp, method, bypass ? "BYPASS" : "MISS");
255
+ }
256
+ return await consumeAndMaybeCache(resp, cacheKey, incomingUrl.pathname, method, cache);
257
+ } catch (err) {
258
+ const errObj = err instanceof Error ? err : new Error(err?.message ?? String(err));
259
+ ctx.logger?.error?.("[MarketplaceProxyPlugin] proxy failed", errObj);
260
+ return c.json({
261
+ success: false,
262
+ error: {
263
+ code: "marketplace_proxy_failed",
264
+ message: err?.message ?? String(err)
265
+ }
266
+ }, 502);
267
+ }
268
+ };
269
+ if (typeof rawApp.all === "function") {
270
+ rawApp.all(`${MARKETPLACE_PREFIX}/*`, handler);
271
+ } else {
272
+ for (const m of ["get", "head"]) {
273
+ try {
274
+ rawApp[m]?.(`${MARKETPLACE_PREFIX}/*`, handler);
275
+ } catch {
276
+ }
277
+ }
278
+ }
279
+ ctx.logger?.info?.(`[MarketplaceProxyPlugin] mounted at ${MARKETPLACE_PREFIX}/* \u2192 ${cloudUrl || "(unconfigured)"} (cache=${this.cache ? "on" : "off"})`);
280
+ });
281
+ };
282
+ this.cloudUrl = resolveCloudUrl(config.controlPlaneUrl);
283
+ this.publicBaseUrl = resolveMarketplacePublicBaseUrl(config.publicMarketplaceBaseUrl);
284
+ const envFlag = (process.env.OS_MARKETPLACE_CACHE ?? "").trim().toLowerCase();
285
+ const envDisabled = ["off", "false", "0", "no", "disable", "disabled"].includes(envFlag);
286
+ const disabled = config.cacheDisabled ?? envDisabled;
287
+ this.cache = disabled ? null : new LruTtlCache(Math.max(8, config.cacheMaxEntries ?? DEFAULT_LRU_MAX));
288
+ }
289
+ };
290
+ async function tryPublicMarketplaceFetch(publicBaseUrl, incomingUrl, method, acceptHeader, logger) {
291
+ const key = publicMarketplaceKeyForApiPath(incomingUrl.pathname);
292
+ if (!key) return null;
293
+ const target = `${publicBaseUrl}/${key}`;
294
+ let resp;
295
+ try {
296
+ resp = await fetch(target, {
297
+ method: "GET",
298
+ headers: {
299
+ "Accept": acceptHeader || "application/json",
300
+ "User-Agent": `objectos-marketplace-proxy/public-r2`
301
+ }
302
+ });
303
+ } catch (err) {
304
+ logger?.warn?.(`[MarketplaceProxyPlugin] public R2 fetch failed (${target}): ${err?.message ?? err}`);
305
+ return null;
306
+ }
307
+ if (resp.status === 404) return null;
308
+ if (!resp.ok) {
309
+ logger?.warn?.(`[MarketplaceProxyPlugin] public R2 ${target} returned ${resp.status} \u2014 falling back to cloud`);
310
+ return null;
311
+ }
312
+ const isList = key === "packages.json";
313
+ const hasFilters = isList && (incomingUrl.searchParams.has("q") || incomingUrl.searchParams.has("category") || incomingUrl.searchParams.has("limit") || incomingUrl.searchParams.has("offset"));
314
+ if (!hasFilters) {
315
+ const headers2 = new Headers();
316
+ const ct = resp.headers.get("content-type") ?? "application/json; charset=utf-8";
317
+ headers2.set("content-type", ct);
318
+ const cc = resp.headers.get("cache-control");
319
+ if (cc) headers2.set("cache-control", cc);
320
+ const etag = resp.headers.get("etag");
321
+ if (etag) headers2.set("etag", etag);
322
+ headers2.set("x-cache", "PUBLIC-R2");
323
+ const body2 = method === "HEAD" ? null : resp.body;
324
+ return new Response(body2, { status: 200, headers: headers2 });
325
+ }
326
+ let snapshot;
327
+ try {
328
+ snapshot = await resp.json();
329
+ } catch (err) {
330
+ logger?.warn?.(`[MarketplaceProxyPlugin] public R2 list snapshot parse failed: ${err?.message ?? err}`);
331
+ return null;
332
+ }
333
+ const items = Array.isArray(snapshot?.data?.items) ? snapshot.data.items : [];
334
+ const q = (incomingUrl.searchParams.get("q") ?? "").trim().toLowerCase();
335
+ const category = (incomingUrl.searchParams.get("category") ?? "").trim();
336
+ const limit = Math.min(Math.max(Number(incomingUrl.searchParams.get("limit") ?? 50), 1), 100);
337
+ const offset = Math.max(Number(incomingUrl.searchParams.get("offset") ?? 0), 0);
338
+ let filtered = items;
339
+ if (q) {
340
+ filtered = filtered.filter((r) => {
341
+ const dn = String(r?.display_name ?? "").toLowerCase();
342
+ const mid = String(r?.manifest_id ?? "").toLowerCase();
343
+ return dn.includes(q) || mid.includes(q);
344
+ });
345
+ }
346
+ if (category) {
347
+ filtered = filtered.filter((r) => String(r?.category ?? "") === category);
348
+ }
349
+ const total = filtered.length;
350
+ const page = filtered.slice(offset, offset + limit);
351
+ const body = JSON.stringify({ success: true, data: { items: page, total, limit, offset } });
352
+ const headers = new Headers({
353
+ "content-type": "application/json; charset=utf-8",
354
+ "cache-control": "public, max-age=30",
355
+ "x-cache": "PUBLIC-R2-FILTERED"
356
+ });
357
+ return new Response(method === "HEAD" ? null : body, { status: 200, headers });
358
+ }
359
+ var PASSTHROUGH_HEADERS = ["content-type", "cache-control", "etag", "last-modified", "vary"];
360
+ function collectHeaders(src) {
361
+ const out = {};
362
+ for (const h of PASSTHROUGH_HEADERS) {
363
+ const v = src.headers.get(h);
364
+ if (v) out[h] = v;
365
+ }
366
+ return out;
367
+ }
368
+ function buildCachedResponse(entry, method, xCache) {
369
+ const headers = new Headers(entry.headers);
370
+ headers.set("X-Cache", xCache);
371
+ const ageSec = Math.max(0, Math.floor((entry.expiresAt - entry.ttlMs - Date.now()) / -1e3));
372
+ headers.set("Age", String(Math.max(0, ageSec)));
373
+ const body = method === "HEAD" ? null : entry.body;
374
+ return new Response(body, { status: entry.status, headers });
375
+ }
376
+ async function passthroughResponse(resp, method, xCache) {
377
+ const headers = new Headers(collectHeaders(resp));
378
+ headers.set("X-Cache", xCache);
379
+ if (method === "HEAD") {
380
+ try {
381
+ await resp.arrayBuffer();
382
+ } catch {
383
+ }
384
+ return new Response(null, { status: resp.status, headers });
385
+ }
386
+ const body = await resp.arrayBuffer();
387
+ return new Response(body, { status: resp.status, headers });
388
+ }
389
+ async function consumeAndMaybeCache(resp, key, pathname, method, cache) {
390
+ const body = await resp.arrayBuffer();
391
+ const headers = collectHeaders(resp);
392
+ if (resp.status >= 200 && resp.status < 300) {
393
+ const ttlMs = ttlForPath(pathname);
394
+ const entry = {
395
+ status: resp.status,
396
+ body,
397
+ headers,
398
+ etag: resp.headers.get("etag") ?? void 0,
399
+ lastModified: resp.headers.get("last-modified") ?? void 0,
400
+ expiresAt: Date.now() + ttlMs,
401
+ ttlMs
402
+ };
403
+ cache.set(key, entry);
404
+ }
405
+ const respHeaders = new Headers(headers);
406
+ respHeaders.set("X-Cache", "MISS");
407
+ const outBody = method === "HEAD" ? null : body;
408
+ return new Response(outBody, { status: resp.status, headers: respHeaders });
409
+ }
410
+
411
+ // src/marketplace-install-local-plugin.ts
412
+ import { readEnvWithDeprecation } from "@objectstack/types";
413
+
414
+ // src/local-manifest-source.ts
415
+ import { existsSync, mkdirSync, readFileSync, readdirSync, unlinkSync, writeFileSync } from "fs";
416
+ import { join, resolve } from "path";
417
+ var DEFAULT_INSTALLED_PACKAGES_DIR = ".objectstack/installed-packages";
418
+ function safeFilename(manifestId) {
419
+ return manifestId.replace(/[^a-zA-Z0-9._-]/g, "_") + ".json";
420
+ }
421
+ var LocalManifestSource = class {
422
+ constructor(storageDir) {
423
+ this.dir = storageDir ? resolve(storageDir) : resolve(process.cwd(), DEFAULT_INSTALLED_PACKAGES_DIR);
424
+ }
425
+ /** Every valid entry in the ledger (corrupt files are skipped). */
426
+ list() {
427
+ if (!existsSync(this.dir)) return [];
428
+ const out = [];
429
+ for (const name of readdirSync(this.dir)) {
430
+ if (!name.endsWith(".json")) continue;
431
+ try {
432
+ const raw = readFileSync(join(this.dir, name), "utf8");
433
+ out.push(JSON.parse(raw));
434
+ } catch {
435
+ }
436
+ }
437
+ return out;
438
+ }
439
+ /** Read one entry; null when absent or unreadable. */
440
+ read(manifestId) {
441
+ const file = this.fileFor(manifestId);
442
+ if (!existsSync(file)) return null;
443
+ try {
444
+ return JSON.parse(readFileSync(file, "utf8"));
445
+ } catch {
446
+ return null;
447
+ }
448
+ }
449
+ /** Whether the ledger holds an entry for this manifest id. */
450
+ has(manifestId) {
451
+ return existsSync(this.fileFor(manifestId));
452
+ }
453
+ /** Create or replace an entry (upsert by manifestId). */
454
+ write(entry) {
455
+ mkdirSync(this.dir, { recursive: true });
456
+ writeFileSync(this.fileFor(entry.manifestId), JSON.stringify(entry, null, 2), "utf8");
457
+ }
458
+ /** Remove an entry. Returns false when it was not present. */
459
+ remove(manifestId) {
460
+ const file = this.fileFor(manifestId);
461
+ if (!existsSync(file)) return false;
462
+ unlinkSync(file);
463
+ return true;
464
+ }
465
+ fileFor(manifestId) {
466
+ return join(this.dir, safeFilename(manifestId));
467
+ }
468
+ };
469
+
470
+ // src/connection-credential-store.ts
471
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "fs";
472
+ import { dirname, resolve as resolve2 } from "path";
473
+ var DEFAULT_CONNECTION_CREDENTIAL_PATH = ".objectstack/cloud-connection.json";
474
+ var ConnectionCredentialStore = class {
475
+ constructor(path) {
476
+ this.path = path ? resolve2(path) : resolve2(process.cwd(), DEFAULT_CONNECTION_CREDENTIAL_PATH);
477
+ }
478
+ /**
479
+ * Read the stored credential; null when absent or unreadable.
480
+ *
481
+ * An IDENTITY RESIDUAL — `runtimeToken: ''` with a `runtimeId` — is a
482
+ * valid record: unbind leaves one behind so a later re-bind to the same
483
+ * org claims the same registration (ADR runtime-identity-binding §2.1).
484
+ * Callers already treat the empty token as "no credential".
485
+ */
486
+ read() {
487
+ if (!existsSync2(this.path)) return null;
488
+ try {
489
+ const parsed = JSON.parse(readFileSync2(this.path, "utf8"));
490
+ if (!parsed || typeof parsed.runtimeToken !== "string") return null;
491
+ if (!parsed.runtimeToken && !(typeof parsed.runtimeId === "string" && parsed.runtimeId)) return null;
492
+ return parsed;
493
+ } catch {
494
+ return null;
495
+ }
496
+ }
497
+ /** Persist (replace) the credential. Written 0600 — it is a secret. */
498
+ write(credential) {
499
+ mkdirSync2(dirname(this.path), { recursive: true });
500
+ writeFileSync2(this.path, JSON.stringify(credential, null, 2), { encoding: "utf8", mode: 384 });
501
+ }
502
+ /** Remove the credential (unbind). Returns false when nothing was stored. */
503
+ clear() {
504
+ if (!existsSync2(this.path)) return false;
505
+ unlinkSync2(this.path);
506
+ return true;
507
+ }
508
+ };
509
+
510
+ // src/marketplace-install-local-plugin.ts
511
+ var ROUTE_BASE = "/api/v1/marketplace/install-local";
512
+ var MarketplaceInstallLocalPlugin = class {
513
+ constructor(config = {}) {
514
+ this.name = "com.objectstack.runtime.marketplace-install-local";
515
+ this.version = "1.0.0";
516
+ this.init = async (_ctx) => {
517
+ };
518
+ this.start = async (ctx) => {
519
+ ctx.hook("kernel:ready", async () => {
520
+ try {
521
+ const manifest = ctx.getService("manifest");
522
+ manifest?.register?.(MARKETPLACE_INSTALLED_UI_BUNDLE);
523
+ } catch {
524
+ }
525
+ await this.rehydrate(ctx);
526
+ let httpServer;
527
+ try {
528
+ httpServer = ctx.getService("http-server");
529
+ } catch {
530
+ ctx.logger?.warn?.("[MarketplaceInstallLocal] http-server not available \u2014 install endpoints not mounted");
531
+ return;
532
+ }
533
+ if (!httpServer || typeof httpServer.getRawApp !== "function") {
534
+ ctx.logger?.warn?.("[MarketplaceInstallLocal] http-server missing getRawApp() \u2014 install endpoints not mounted");
535
+ return;
536
+ }
537
+ const rawApp = httpServer.getRawApp();
538
+ const postHandler = async (c) => this.handleInstall(c, ctx);
539
+ const getHandler = async (c) => this.handleList(c);
540
+ const deleteHandler = async (c) => this.handleUninstall(c, ctx);
541
+ const reseedHandler = async (c) => this.handleReseed(c, ctx);
542
+ const purgeHandler = async (c) => this.handlePurge(c, ctx);
543
+ if (typeof rawApp.post === "function") rawApp.post(ROUTE_BASE, postHandler);
544
+ if (typeof rawApp.get === "function") rawApp.get(ROUTE_BASE, getHandler);
545
+ if (typeof rawApp.delete === "function") rawApp.delete(`${ROUTE_BASE}/:manifestId`, deleteHandler);
546
+ if (typeof rawApp.post === "function") {
547
+ rawApp.post(`${ROUTE_BASE}/:manifestId/reseed-sample-data`, reseedHandler);
548
+ rawApp.post(`${ROUTE_BASE}/:manifestId/purge-sample-data`, purgeHandler);
549
+ }
550
+ ctx.logger?.info?.(`[MarketplaceInstallLocal] mounted at ${ROUTE_BASE} (storage: ${this.storageDir})`);
551
+ });
552
+ };
553
+ /**
554
+ * Re-register every cached manifest with the kernel's manifest service.
555
+ * Safe to call on a kernel that already has the same manifest_id (the
556
+ * underlying ObjectQL registry overwrites by id, but we still warn so
557
+ * a developer can spot the dev-time clash between their config.ts and
558
+ * a marketplace package).
559
+ */
560
+ this.rehydrate = async (ctx) => {
561
+ const entries = this.readAll();
562
+ if (entries.length === 0) return;
563
+ let manifestService = null;
564
+ try {
565
+ manifestService = ctx.getService("manifest");
566
+ } catch {
567
+ ctx.logger?.warn?.("[MarketplaceInstallLocal] no `manifest` service \u2014 rehydrate skipped");
568
+ return;
569
+ }
570
+ for (const entry of entries) {
571
+ try {
572
+ manifestService.register(entry.manifest);
573
+ try {
574
+ const ql = ctx.getService("objectql");
575
+ if (ql && typeof ql.syncSchemas === "function") await ql.syncSchemas();
576
+ } catch {
577
+ }
578
+ await this.applySideEffects(ctx, entry.manifest, { seedNow: false });
579
+ ctx.logger?.info?.(`[MarketplaceInstallLocal] rehydrated ${entry.manifestId}@${entry.version}`);
580
+ } catch (err) {
581
+ ctx.logger?.error?.(`[MarketplaceInstallLocal] rehydrate failed for ${entry.manifestId}`, err instanceof Error ? err : new Error(String(err)));
582
+ }
583
+ }
584
+ };
585
+ this.handleInstall = async (c, ctx) => {
586
+ const userId = await this.requireAuthenticatedUser(c, ctx);
587
+ if (!userId) {
588
+ return c.json({ success: false, error: { code: "unauthorized", message: "Authentication required to install packages." } }, 401);
589
+ }
590
+ let body = {};
591
+ try {
592
+ body = await c.req.json();
593
+ } catch {
594
+ }
595
+ const inlineManifest = body?.manifest && typeof body.manifest === "object" ? body.manifest : null;
596
+ const normalizeBundle = (m) => {
597
+ if (m && !m.id && !m.name && m.manifest && typeof m.manifest === "object" && (m.manifest.id || m.manifest.name)) {
598
+ const { manifest: meta, ...sections } = m;
599
+ return { ...meta, ...sections };
600
+ }
601
+ return m;
602
+ };
603
+ let manifest;
604
+ let resolvedVersionId;
605
+ let version;
606
+ let packageId;
607
+ if (inlineManifest) {
608
+ manifest = normalizeBundle(inlineManifest);
609
+ packageId = String(manifest.id ?? manifest.name ?? "").trim();
610
+ version = String(manifest.version ?? "unknown");
611
+ resolvedVersionId = String(body?.versionId ?? version);
612
+ if (!packageId) {
613
+ return c.json({ success: false, error: { code: "invalid_manifest", message: 'Inline manifest must have an "id" or "name".' } }, 400);
614
+ }
615
+ } else {
616
+ if (!this.cloudUrl) {
617
+ return c.json({ success: false, error: { code: "marketplace_unavailable", message: "OS_CLOUD_URL not configured." } }, 503);
618
+ }
619
+ packageId = String(body?.packageId ?? "").trim();
620
+ const versionId = String(body?.versionId ?? "latest").trim() || "latest";
621
+ if (!packageId) {
622
+ return c.json({ success: false, error: { code: "bad_request", message: "packageId is required." } }, 400);
623
+ }
624
+ let payload;
625
+ const publicBase = resolveMarketplacePublicBaseUrl();
626
+ const fetchAttempts = [];
627
+ if (publicBase) {
628
+ fetchAttempts.push({
629
+ label: "public-r2",
630
+ url: `${publicBase}/packages/${encodeURIComponent(packageId)}/versions/${encodeURIComponent(versionId)}/manifest.json`
631
+ });
632
+ }
633
+ fetchAttempts.push({
634
+ label: "cloud",
635
+ url: `${this.cloudUrl}/api/v1/marketplace/packages/${encodeURIComponent(packageId)}/versions/${encodeURIComponent(versionId)}/manifest`
636
+ });
637
+ const cloudCredential = (process.env.OS_CLOUD_API_KEY ?? "").trim() || this.credentials.read()?.runtimeToken || "";
638
+ let lastErrStatus = 0;
639
+ let lastErrText = "";
640
+ for (const attempt of fetchAttempts) {
641
+ try {
642
+ const headers = { Accept: "application/json" };
643
+ if (attempt.label === "cloud" && cloudCredential) headers.Authorization = `Bearer ${cloudCredential}`;
644
+ const resp = await fetch(attempt.url, { headers });
645
+ if (!resp.ok) {
646
+ lastErrStatus = resp.status;
647
+ lastErrText = (await resp.text().catch(() => "")).slice(0, 200);
648
+ if (attempt.label === "public-r2" && resp.status === 404) {
649
+ ctx.logger?.info?.(`[MarketplaceInstallLocal] public-r2 miss for ${packageId}@${versionId}, falling back to cloud`);
650
+ continue;
651
+ }
652
+ if (attempt.label === "public-r2" && resp.status >= 500) {
653
+ ctx.logger?.warn?.(`[MarketplaceInstallLocal] public-r2 ${resp.status}, falling back to cloud`);
654
+ continue;
655
+ }
656
+ break;
657
+ }
658
+ payload = await resp.json();
659
+ lastErrStatus = 0;
660
+ break;
661
+ } catch (err) {
662
+ if (attempt.label === "public-r2") {
663
+ ctx.logger?.warn?.(`[MarketplaceInstallLocal] public-r2 fetch error: ${err?.message ?? err}, falling back to cloud`);
664
+ continue;
665
+ }
666
+ return c.json({
667
+ success: false,
668
+ error: { code: "cloud_fetch_failed", message: err?.message ?? String(err) }
669
+ }, 502);
670
+ }
671
+ }
672
+ if (!payload) {
673
+ return c.json({
674
+ success: false,
675
+ error: { code: "cloud_fetch_failed", message: `Cloud returned ${lastErrStatus}: ${lastErrText}` }
676
+ }, lastErrStatus === 404 ? 404 : 502);
677
+ }
678
+ const data = payload?.data ?? payload;
679
+ manifest = normalizeBundle(data?.manifest);
680
+ resolvedVersionId = String(data?.version_id ?? versionId);
681
+ version = String(data?.version ?? "unknown");
682
+ }
683
+ const manifestId = String(manifest?.id ?? manifest?.name ?? "");
684
+ if (!manifest || !manifestId) {
685
+ return c.json({ success: false, error: { code: "invalid_manifest", message: "Invalid manifest payload." } }, inlineManifest ? 400 : 502);
686
+ }
687
+ const conflict = this.findConflict(ctx, manifestId);
688
+ if (conflict === "user-code") {
689
+ return c.json({
690
+ success: false,
691
+ error: {
692
+ code: "manifest_conflict",
693
+ message: `manifest_id "${manifestId}" is already defined by this runtime's local code. Refusing to overwrite. Uninstall the local definition first.`
694
+ }
695
+ }, 409);
696
+ }
697
+ try {
698
+ const manifestService = ctx.getService("manifest");
699
+ manifestService.register(manifest);
700
+ } catch (err) {
701
+ if (inlineManifest) {
702
+ return c.json({
703
+ success: false,
704
+ error: { code: "register_failed", message: `Failed to register imported manifest: ${err?.message ?? err}` }
705
+ }, 422);
706
+ }
707
+ ctx.logger?.warn?.(`[MarketplaceInstallLocal] hot-register failed for ${manifestId} (will load on next restart): ${err?.message ?? err}`);
708
+ }
709
+ const entry = {
710
+ packageId,
711
+ versionId: resolvedVersionId,
712
+ manifestId,
713
+ version,
714
+ manifest,
715
+ installedAt: (/* @__PURE__ */ new Date()).toISOString(),
716
+ installedBy: userId,
717
+ withSampleData: false
718
+ };
719
+ try {
720
+ this.ledger.write(entry);
721
+ } catch (err) {
722
+ return c.json({
723
+ success: false,
724
+ error: { code: "storage_failed", message: `Failed to persist manifest: ${err?.message ?? err}` }
725
+ }, 500);
726
+ }
727
+ try {
728
+ const ql = ctx.getService("objectql");
729
+ if (ql && typeof ql.syncSchemas === "function") {
730
+ await ql.syncSchemas();
731
+ ctx.logger?.info?.(`[MarketplaceInstallLocal] syncSchemas() ran after registering ${manifestId}`);
732
+ }
733
+ } catch (err) {
734
+ ctx.logger?.warn?.(`[MarketplaceInstallLocal] syncSchemas failed for ${manifestId}: ${err?.message ?? err}`);
735
+ }
736
+ const seededSummary = await this.applySideEffects(ctx, manifest, { seedNow: true, c });
737
+ if (seededSummary.seeded.mode === "inline" && (seededSummary.seeded.inserted ?? 0) + (seededSummary.seeded.updated ?? 0) > 0) {
738
+ entry.withSampleData = true;
739
+ try {
740
+ this.ledger.write(entry);
741
+ } catch {
742
+ }
743
+ }
744
+ return c.json({
745
+ success: true,
746
+ data: {
747
+ manifestId,
748
+ version,
749
+ versionId: resolvedVersionId,
750
+ installedAt: entry.installedAt,
751
+ hotLoaded: true,
752
+ upgradedFrom: conflict === "marketplace" ? "previous-marketplace-version" : null,
753
+ translationsLoaded: seededSummary.translationsLoaded,
754
+ seeded: seededSummary.seeded,
755
+ note: "App is now available in this runtime. Refresh the console to see it in the app switcher."
756
+ }
757
+ }, 200);
758
+ };
759
+ this.handleList = async (c) => {
760
+ const entries = this.readAll();
761
+ return c.json({
762
+ success: true,
763
+ data: {
764
+ items: entries.map((e) => ({
765
+ packageId: e.packageId,
766
+ versionId: e.versionId,
767
+ manifestId: e.manifestId,
768
+ version: e.version,
769
+ installedAt: e.installedAt,
770
+ installedBy: e.installedBy,
771
+ withSampleData: e.withSampleData ?? false
772
+ })),
773
+ total: entries.length,
774
+ storageDir: this.storageDir
775
+ }
776
+ }, 200);
777
+ };
778
+ this.handleUninstall = async (c, ctx) => {
779
+ const userId = await this.requireAuthenticatedUser(c, ctx);
780
+ if (!userId) {
781
+ return c.json({ success: false, error: { code: "unauthorized", message: "Authentication required." } }, 401);
782
+ }
783
+ const manifestId = String(c.req.param?.("manifestId") ?? c.req.params?.manifestId ?? "").trim();
784
+ if (!manifestId) {
785
+ return c.json({ success: false, error: { code: "bad_request", message: "manifestId path param required." } }, 400);
786
+ }
787
+ if (!this.ledger.has(manifestId)) {
788
+ return c.json({ success: false, error: { code: "not_found", message: `No marketplace install for ${manifestId}.` } }, 404);
789
+ }
790
+ try {
791
+ this.ledger.remove(manifestId);
792
+ } catch (err) {
793
+ return c.json({ success: false, error: { code: "storage_failed", message: err?.message ?? String(err) } }, 500);
794
+ }
795
+ ctx.logger?.info?.(`[MarketplaceInstallLocal] uninstalled ${manifestId} (cached manifest removed; restart runtime to unload from running kernel)`);
796
+ return c.json({
797
+ success: true,
798
+ data: {
799
+ manifestId,
800
+ note: "Cached manifest removed. The app remains loaded in the running kernel until the next restart (the kernel API does not support unregistering apps in-place)."
801
+ }
802
+ }, 200);
803
+ };
804
+ /**
805
+ * Detect whether `manifestId` is already known to the kernel and classify
806
+ * the source so we can refuse vs upgrade gracefully.
807
+ *
808
+ * 'none' — fresh install
809
+ * 'marketplace' — previously installed by this plugin (allow upgrade)
810
+ * 'user-code' — defined by AppPlugin from objectstack.config.ts
811
+ * (refuse to avoid silently overwriting authored code)
812
+ */
813
+ this.findConflict = (ctx, manifestId) => {
814
+ if (this.ledger.has(manifestId)) {
815
+ return "marketplace";
816
+ }
817
+ try {
818
+ const ql = ctx.getService("objectql");
819
+ const packages = ql?.registry?.getAllPackages?.() ?? [];
820
+ const hit = packages.find(
821
+ (p) => (p?.manifest?.id ?? p?.id ?? p?.manifest?.name) === manifestId
822
+ );
823
+ if (hit) return "user-code";
824
+ } catch {
825
+ }
826
+ return "none";
827
+ };
828
+ /**
829
+ * Pull a userId out of the request's better-auth session, if any.
830
+ * Returns null when there is no signed-in user. v1 does not check
831
+ * admin role — UI gating + the auth requirement is sufficient for
832
+ * dev / single-tenant runtimes. Stricter checks can be layered on
833
+ * via a middleware in cloud-hosted multi-tenant deployments.
834
+ */
835
+ /**
836
+ * POST /api/v1/marketplace/install-local/:manifestId/reseed-sample-data
837
+ *
838
+ * Re-runs SeedLoaderService against the cached manifest's `data` arrays.
839
+ * Idempotent (upsert by id). Useful when:
840
+ * • The user installed an app and skipped sample data
841
+ * • A purge was undone
842
+ * • The user wants a clean baseline back after editing demo rows
843
+ *
844
+ * Multi-tenant: requires an active organization on the session (same
845
+ * rule as install seed path).
846
+ */
847
+ this.handleReseed = async (c, ctx) => {
848
+ const userId = await this.requireAuthenticatedUser(c, ctx);
849
+ if (!userId) {
850
+ return c.json({ success: false, error: { code: "unauthorized", message: "Authentication required." } }, 401);
851
+ }
852
+ const manifestId = String(c.req.param?.("manifestId") ?? c.req.params?.manifestId ?? "").trim();
853
+ if (!manifestId) {
854
+ return c.json({ success: false, error: { code: "bad_request", message: "manifestId path param required." } }, 400);
855
+ }
856
+ if (!this.ledger.has(manifestId)) {
857
+ return c.json({ success: false, error: { code: "not_found", message: `No marketplace install for ${manifestId}.` } }, 404);
858
+ }
859
+ const entry = this.ledger.read(manifestId);
860
+ if (!entry) {
861
+ return c.json({ success: false, error: { code: "storage_failed", message: "Failed to read manifest cache." } }, 500);
862
+ }
863
+ const summary = await this.applySideEffects(ctx, entry.manifest, { seedNow: true, c });
864
+ if (summary.seeded.mode === "skipped") {
865
+ return c.json({
866
+ success: false,
867
+ error: {
868
+ code: "reseed_skipped",
869
+ message: `Reseed did not run: ${summary.seeded.reason ?? "unknown reason"}`
870
+ }
871
+ }, 400);
872
+ }
873
+ try {
874
+ entry.withSampleData = true;
875
+ this.ledger.write(entry);
876
+ } catch {
877
+ }
878
+ return c.json({
879
+ success: true,
880
+ data: {
881
+ manifestId,
882
+ inserted: summary.seeded.inserted ?? 0,
883
+ updated: summary.seeded.updated ?? 0,
884
+ errors: summary.seeded.errors ?? 0,
885
+ withSampleData: true
886
+ }
887
+ }, 200);
888
+ };
889
+ /**
890
+ * POST /api/v1/marketplace/install-local/:manifestId/purge-sample-data
891
+ *
892
+ * Deletes every record whose id is declared in the cached manifest's
893
+ * seed datasets. Uses the `driver` service directly to bypass ACL /
894
+ * lifecycle hooks (same pattern as cloud purge). User-created records
895
+ * are never touched — only ids declared in the package's bundled
896
+ * datasets are removed. Already-deleted rows count as `skipped`.
897
+ */
898
+ this.handlePurge = async (c, ctx) => {
899
+ const userId = await this.requireAuthenticatedUser(c, ctx);
900
+ if (!userId) {
901
+ return c.json({ success: false, error: { code: "unauthorized", message: "Authentication required." } }, 401);
902
+ }
903
+ const manifestId = String(c.req.param?.("manifestId") ?? c.req.params?.manifestId ?? "").trim();
904
+ if (!manifestId) {
905
+ return c.json({ success: false, error: { code: "bad_request", message: "manifestId path param required." } }, 400);
906
+ }
907
+ if (!this.ledger.has(manifestId)) {
908
+ return c.json({ success: false, error: { code: "not_found", message: `No marketplace install for ${manifestId}.` } }, 404);
909
+ }
910
+ const entry = this.ledger.read(manifestId);
911
+ if (!entry) {
912
+ return c.json({ success: false, error: { code: "storage_failed", message: "Failed to read manifest cache." } }, 500);
913
+ }
914
+ const datasets = Array.isArray(entry.manifest?.data) ? entry.manifest.data.filter((d) => d && d.object && Array.isArray(d.records)) : [];
915
+ if (datasets.length === 0) {
916
+ return c.json({
917
+ success: false,
918
+ error: { code: "nothing_to_purge", message: "This package declares no seed datasets." }
919
+ }, 400);
920
+ }
921
+ let driver;
922
+ try {
923
+ driver = ctx.getService("driver");
924
+ } catch {
925
+ }
926
+ if (!driver || typeof driver.delete !== "function") {
927
+ return c.json({
928
+ success: false,
929
+ error: { code: "driver_missing", message: "driver service unavailable \u2014 cannot purge." }
930
+ }, 500);
931
+ }
932
+ let deleted = 0;
933
+ let skipped = 0;
934
+ let errors = 0;
935
+ for (const ds of datasets) {
936
+ const object = String(ds.object);
937
+ for (const rec of ds.records) {
938
+ const id = rec?.id;
939
+ if (id === void 0 || id === null || id === "") {
940
+ skipped++;
941
+ continue;
942
+ }
943
+ try {
944
+ const r = await driver.delete(object, id);
945
+ if (r === false || r === 0 || r?.deleted === 0) skipped++;
946
+ else deleted++;
947
+ } catch (err) {
948
+ const msg = String(err?.message ?? err);
949
+ if (/not.?found|no row/i.test(msg)) skipped++;
950
+ else {
951
+ errors++;
952
+ ctx.logger?.warn?.(`[MarketplaceInstallLocal] purge ${object}#${id}: ${msg}`);
953
+ }
954
+ }
955
+ }
956
+ }
957
+ try {
958
+ entry.withSampleData = false;
959
+ this.ledger.write(entry);
960
+ } catch {
961
+ }
962
+ ctx.logger?.info?.(`[MarketplaceInstallLocal] purged ${manifestId}: deleted=${deleted} skipped=${skipped} errors=${errors}`);
963
+ return c.json({
964
+ success: true,
965
+ data: { manifestId, deleted, skipped, errors, withSampleData: false }
966
+ }, 200);
967
+ };
968
+ /**
969
+ * Replicate the start-time side-effects that AppPlugin runs for
970
+ * statically-declared apps but the `manifest` service does NOT:
971
+ *
972
+ * 1. Load `manifest.translations` (array of `Record<locale, data>`)
973
+ * into the i18n service — auto-creating an in-memory fallback if
974
+ * none is registered, matching AppPlugin's behaviour.
975
+ *
976
+ * 2. Merge `manifest.data` (an array of seed datasets) into the
977
+ * kernel's `seed-datasets` service so SecurityPlugin's per-org
978
+ * replay middleware picks them up on every future
979
+ * sys_organization insert.
980
+ *
981
+ * 3. When `seedNow=true`, also run the seed immediately so the user
982
+ * sees demo data without having to create a new org:
983
+ * • single-tenant: run SeedLoaderService inline (mirrors
984
+ * AppPlugin single-tenant branch)
985
+ * • multi-tenant: invoke `seed-replayer` for the caller's
986
+ * active org (resolved from the request session)
987
+ *
988
+ * Errors are logged but never thrown — install succeeds even if
989
+ * post-register side-effects partially fail (the manifest itself is
990
+ * already registered + cached). Returns a small summary for the
991
+ * response envelope.
992
+ */
993
+ this.applySideEffects = async (ctx, manifest, opts) => {
994
+ const appId = String(manifest?.id ?? "unknown");
995
+ let translationsLoaded = 0;
996
+ let seedSummary = { mode: "skipped", reason: "no-datasets" };
997
+ try {
998
+ const bundles = [];
999
+ if (Array.isArray(manifest?.translations)) bundles.push(...manifest.translations);
1000
+ if (Array.isArray(manifest?.i18n)) bundles.push(...manifest.i18n);
1001
+ if (bundles.length > 0) {
1002
+ let i18nService;
1003
+ try {
1004
+ i18nService = ctx.getService("i18n");
1005
+ } catch {
1006
+ }
1007
+ if (!i18nService) {
1008
+ try {
1009
+ const mod = await import("@objectstack/core");
1010
+ const createMemoryI18n = mod.createMemoryI18n;
1011
+ if (typeof createMemoryI18n === "function") {
1012
+ i18nService = createMemoryI18n();
1013
+ ctx.registerService?.("i18n", i18nService);
1014
+ ctx.logger?.info?.(`[MarketplaceInstallLocal] auto-registered in-memory i18n fallback for "${appId}"`);
1015
+ }
1016
+ } catch {
1017
+ }
1018
+ }
1019
+ if (i18nService?.loadTranslations) {
1020
+ for (const bundle of bundles) {
1021
+ for (const [locale, data] of Object.entries(bundle)) {
1022
+ if (data && typeof data === "object") {
1023
+ try {
1024
+ i18nService.loadTranslations(locale, data);
1025
+ translationsLoaded++;
1026
+ } catch (err) {
1027
+ ctx.logger?.warn?.(`[MarketplaceInstallLocal] failed to load ${appId} translations for ${locale}: ${err?.message ?? err}`);
1028
+ }
1029
+ }
1030
+ }
1031
+ }
1032
+ ctx.logger?.info?.(`[MarketplaceInstallLocal] loaded ${translationsLoaded} locale bundle(s) for ${appId}`);
1033
+ }
1034
+ }
1035
+ } catch (err) {
1036
+ ctx.logger?.warn?.(`[MarketplaceInstallLocal] i18n side-effect failed for ${appId}: ${err?.message ?? err}`);
1037
+ }
1038
+ const datasets = Array.isArray(manifest?.data) ? manifest.data.filter((d) => d && d.object && Array.isArray(d.records)) : [];
1039
+ if (datasets.length > 0) {
1040
+ try {
1041
+ const kernel = ctx.kernel;
1042
+ let existing = [];
1043
+ try {
1044
+ const v = kernel?.getService?.("seed-datasets");
1045
+ if (Array.isArray(v)) existing = v;
1046
+ } catch {
1047
+ }
1048
+ const merged = [...existing, ...datasets];
1049
+ if (kernel?.registerService) kernel.registerService("seed-datasets", merged);
1050
+ else ctx.registerService?.("seed-datasets", merged);
1051
+ ctx.logger?.info?.(`[MarketplaceInstallLocal] merged ${datasets.length} seed dataset(s) into kernel (total: ${merged.length})`);
1052
+ } catch (err) {
1053
+ ctx.logger?.warn?.(`[MarketplaceInstallLocal] failed to merge seed-datasets: ${err?.message ?? err}`);
1054
+ }
1055
+ }
1056
+ if (opts.seedNow && datasets.length > 0) {
1057
+ const multiTenant = String(readEnvWithDeprecation("OS_MULTI_ORG_ENABLED", "OS_MULTI_TENANT") ?? "false").toLowerCase() !== "false";
1058
+ try {
1059
+ const ql = ctx.getService("objectql");
1060
+ let metadata;
1061
+ try {
1062
+ metadata = ctx.getService("metadata");
1063
+ } catch {
1064
+ }
1065
+ if (!ql || !metadata) {
1066
+ seedSummary = { mode: "skipped", reason: "objectql-or-metadata-missing" };
1067
+ } else {
1068
+ let organizationId;
1069
+ if (multiTenant) {
1070
+ const resolved = await this.resolveActiveOrgId(opts.c, ctx);
1071
+ if (resolved) organizationId = resolved;
1072
+ else {
1073
+ seedSummary = { mode: "skipped", reason: "multi-tenant-no-active-org" };
1074
+ ctx.logger?.warn?.("[MarketplaceInstallLocal] multi-tenant: no active org on request \u2014 data not seeded");
1075
+ }
1076
+ }
1077
+ if (!multiTenant || organizationId) {
1078
+ const [{ SeedLoaderService }, { SeedLoaderRequestSchema }] = await Promise.all([
1079
+ import("@objectstack/runtime"),
1080
+ import("@objectstack/spec/data")
1081
+ ]);
1082
+ const seedLoader = new SeedLoaderService(ql, metadata, ctx.logger);
1083
+ const request = SeedLoaderRequestSchema.parse({
1084
+ // ADR-0036 / seed rename: the field is `seeds` (was `datasets`).
1085
+ seeds: datasets,
1086
+ config: {
1087
+ defaultMode: "upsert",
1088
+ multiPass: true,
1089
+ ...organizationId ? { organizationId } : {}
1090
+ }
1091
+ });
1092
+ const result = await seedLoader.load(request);
1093
+ seedSummary = {
1094
+ mode: "inline",
1095
+ inserted: result.summary.totalInserted,
1096
+ updated: result.summary.totalUpdated,
1097
+ errors: result.errors.length
1098
+ };
1099
+ ctx.logger?.info?.(`[MarketplaceInstallLocal] inline seed for ${appId}${organizationId ? ` (org=${organizationId})` : ""}: inserted=${seedSummary.inserted} updated=${seedSummary.updated} errors=${seedSummary.errors}`);
1100
+ }
1101
+ }
1102
+ } catch (err) {
1103
+ seedSummary = { mode: "skipped", reason: `seed-error: ${err?.message ?? err}` };
1104
+ ctx.logger?.warn?.(`[MarketplaceInstallLocal] seed run failed for ${appId}: ${err?.message ?? err}`);
1105
+ }
1106
+ }
1107
+ return { translationsLoaded, seeded: seedSummary };
1108
+ };
1109
+ /**
1110
+ * Best-effort active-org resolution. Reads the better-auth session
1111
+ * (same path as requireAuthenticatedUser) and returns
1112
+ * `session.activeOrganizationId`, falling back to the user's first
1113
+ * org membership.
1114
+ */
1115
+ this.resolveActiveOrgId = async (c, ctx) => {
1116
+ if (!c?.req?.raw?.headers) return null;
1117
+ try {
1118
+ const authService = ctx.getService("auth");
1119
+ let api = authService?.api;
1120
+ if (!api && typeof authService?.getApi === "function") api = await authService.getApi();
1121
+ if (!api?.getSession) return null;
1122
+ const session = await api.getSession({ headers: c.req.raw.headers });
1123
+ const direct = session?.session?.activeOrganizationId ?? session?.activeOrganizationId ?? null;
1124
+ if (direct) return String(direct);
1125
+ const userId = session?.user?.id;
1126
+ if (!userId) return null;
1127
+ try {
1128
+ const ql = ctx.getService("objectql");
1129
+ if (ql?.find) {
1130
+ const rows = await ql.find("sys_organization_member", { where: { user_id: userId }, limit: 1, context: { isSystem: true } });
1131
+ const row = Array.isArray(rows) ? rows[0] : rows?.items?.[0] ?? null;
1132
+ return row?.organization_id ? String(row.organization_id) : null;
1133
+ }
1134
+ } catch {
1135
+ }
1136
+ } catch {
1137
+ }
1138
+ return null;
1139
+ };
1140
+ this.requireAuthenticatedUser = async (c, ctx) => {
1141
+ try {
1142
+ const authService = ctx.getService("auth");
1143
+ let api = authService?.api;
1144
+ if (!api && typeof authService?.getApi === "function") {
1145
+ api = await authService.getApi();
1146
+ }
1147
+ if (api?.getSession && c?.req?.raw?.headers) {
1148
+ const session = await api.getSession({ headers: c.req.raw.headers });
1149
+ const userId = session?.user?.id ?? null;
1150
+ if (userId) return String(userId);
1151
+ }
1152
+ } catch {
1153
+ }
1154
+ const xUserId = c?.req?.header?.("x-user-id");
1155
+ if (xUserId) return String(xUserId);
1156
+ return null;
1157
+ };
1158
+ this.readAll = () => this.ledger.list();
1159
+ this.cloudUrl = resolveCloudUrl(config.controlPlaneUrl);
1160
+ this.ledger = new LocalManifestSource(config.storageDir);
1161
+ this.storageDir = this.ledger.dir;
1162
+ this.credentials = new ConnectionCredentialStore();
1163
+ }
1164
+ };
1165
+
1166
+ // src/cloud-connection-plugin.ts
1167
+ import { hostname } from "os";
1168
+
1169
+ // src/cloud-connection-ui.ts
1170
+ var CloudConnectionSettingsPage = {
1171
+ name: "cloud_connection_settings",
1172
+ label: "Cloud Connection",
1173
+ type: "app",
1174
+ template: "default",
1175
+ kind: "full",
1176
+ isDefault: false,
1177
+ regions: [
1178
+ {
1179
+ name: "header",
1180
+ width: "full",
1181
+ components: [
1182
+ {
1183
+ type: "page:header",
1184
+ properties: {
1185
+ title: "Cloud Connection",
1186
+ subtitle: "Connect this runtime to an ObjectStack control plane to browse your organization's private packages and install them here.",
1187
+ icon: "cloud"
1188
+ }
1189
+ }
1190
+ ]
1191
+ },
1192
+ {
1193
+ name: "main",
1194
+ width: "large",
1195
+ components: [
1196
+ {
1197
+ // Registered console widget — the RFC 8628 device-code
1198
+ // state machine (status → start → user-code display →
1199
+ // poll → bound / disconnect). Talks to the same-origin
1200
+ // /api/v1/cloud-connection/* routes this plugin mounts.
1201
+ type: "cloud-connection:panel",
1202
+ properties: {}
1203
+ }
1204
+ ]
1205
+ }
1206
+ ]
1207
+ };
1208
+ var CLOUD_CONNECTION_NAV_CONTRIBUTIONS = [
1209
+ {
1210
+ app: "setup",
1211
+ group: "group_apps",
1212
+ priority: 200,
1213
+ items: [
1214
+ {
1215
+ id: "nav_cloud_connection",
1216
+ type: "page",
1217
+ pageName: "cloud_connection_settings",
1218
+ label: "Cloud Connection",
1219
+ icon: "cloud"
1220
+ }
1221
+ ]
1222
+ }
1223
+ ];
1224
+ var CLOUD_CONNECTION_UI_BUNDLE = {
1225
+ id: "com.objectstack.cloud-connection.ui",
1226
+ namespace: "sys",
1227
+ version: "0.1.0",
1228
+ type: "plugin",
1229
+ scope: "system",
1230
+ name: "Cloud Connection UI",
1231
+ description: "Setup page + navigation for binding this runtime to a control plane.",
1232
+ pages: [CloudConnectionSettingsPage],
1233
+ navigationContributions: CLOUD_CONNECTION_NAV_CONTRIBUTIONS
1234
+ };
1235
+
1236
+ // src/cloud-connection-plugin.ts
1237
+ var CLOUD_CONNECTION_PREFIX = "/api/v1/cloud-connection";
1238
+ var CloudConnectionPlugin = class {
1239
+ constructor(config = {}) {
1240
+ this.name = "com.objectstack.cloud.connection";
1241
+ this.version = "0.3.0";
1242
+ this.init = async (_ctx) => {
1243
+ };
1244
+ this.start = async (ctx) => {
1245
+ ctx.hook("kernel:ready", async () => {
1246
+ const httpServer = (() => {
1247
+ try {
1248
+ return ctx.getService("http-server");
1249
+ } catch {
1250
+ return void 0;
1251
+ }
1252
+ })();
1253
+ if (!httpServer || typeof httpServer.getRawApp !== "function") {
1254
+ ctx.logger?.warn?.("[CloudConnectionPlugin] http-server unavailable \u2014 routes not mounted");
1255
+ return;
1256
+ }
1257
+ const rawApp = httpServer.getRawApp();
1258
+ const cloudUrl = (this.cfg.controlPlaneUrl ?? process.env.OS_CLOUD_URL ?? "").trim().replace(/\/+$/, "");
1259
+ const cloudApiKey = (this.cfg.controlPlaneApiKey ?? process.env.OS_CLOUD_API_KEY ?? "").trim();
1260
+ const deviceClientId = (this.cfg.deviceClientId ?? process.env.OS_CLI_CLIENT_ID ?? "objectstack-cli").trim();
1261
+ const deviceScope = "openid profile email";
1262
+ const credential = () => cloudApiKey || this.store.read()?.runtimeToken || "";
1263
+ const authHeaders = () => {
1264
+ const cred = credential();
1265
+ return cred ? { Authorization: `Bearer ${cred}` } : {};
1266
+ };
1267
+ const hostOf = (c) => {
1268
+ try {
1269
+ return new URL(c.req.url).hostname;
1270
+ } catch {
1271
+ return "";
1272
+ }
1273
+ };
1274
+ const resolveEnvironmentId = async (c) => {
1275
+ const fixed = (this.cfg.environmentId ?? process.env.OS_ENVIRONMENT_ID ?? "").trim();
1276
+ if (fixed && fixed !== "env_local" && fixed !== "proj_local") return fixed;
1277
+ if (this.cfg.singleEnvironment) {
1278
+ return this.store.read()?.environmentId || void 0;
1279
+ }
1280
+ try {
1281
+ const envRegistry = ctx.getService("env-registry");
1282
+ const env = await envRegistry?.resolveByHostname?.(hostOf(c));
1283
+ return env?.environmentId;
1284
+ } catch {
1285
+ return void 0;
1286
+ }
1287
+ };
1288
+ try {
1289
+ const manifest = ctx.getService("manifest");
1290
+ manifest?.register?.(CLOUD_CONNECTION_UI_BUNDLE);
1291
+ } catch {
1292
+ }
1293
+ const sessionFromAuthService = async (authSvc, rawReq) => {
1294
+ const api = typeof authSvc?.getApi === "function" ? await authSvc.getApi() : authSvc?.api ?? authSvc;
1295
+ const session = await api?.getSession?.({ headers: rawReq.headers });
1296
+ const userId = session?.user?.id ? String(session.user.id) : void 0;
1297
+ return userId ? { userId } : null;
1298
+ };
1299
+ const resolveSession = async (environmentId, rawReq) => {
1300
+ try {
1301
+ if (this.cfg.singleEnvironment) {
1302
+ let authSvc2;
1303
+ try {
1304
+ authSvc2 = ctx.getService("auth");
1305
+ } catch {
1306
+ }
1307
+ if (!authSvc2) return null;
1308
+ return await sessionFromAuthService(authSvc2, rawReq);
1309
+ }
1310
+ const kernelManager = ctx.getService("kernel-manager");
1311
+ const kernel = await kernelManager?.getOrCreate?.(environmentId);
1312
+ let authSvc;
1313
+ try {
1314
+ authSvc = await kernel?.getServiceAsync?.("auth");
1315
+ } catch {
1316
+ }
1317
+ if (!authSvc) {
1318
+ try {
1319
+ authSvc = kernel?.getService?.("auth");
1320
+ } catch {
1321
+ }
1322
+ }
1323
+ if (!authSvc) return null;
1324
+ return await sessionFromAuthService(authSvc, rawReq);
1325
+ } catch {
1326
+ return null;
1327
+ }
1328
+ };
1329
+ rawApp.get(`${CLOUD_CONNECTION_PREFIX}/status`, async (c) => {
1330
+ const environmentId = await resolveEnvironmentId(c);
1331
+ const stored = this.store.read();
1332
+ const runtimeId = stored?.runtimeId;
1333
+ if (!environmentId && !this.cfg.singleEnvironment) {
1334
+ return c.json({ success: false, error: { code: "environment_not_found" } }, 404);
1335
+ }
1336
+ if (!environmentId && !credential()) {
1337
+ return c.json({ success: true, data: { environmentId: null, runtimeId: runtimeId ?? null, bound: false, provider: "objectstack-cloud", connection: null } });
1338
+ }
1339
+ if (cloudUrl) {
1340
+ try {
1341
+ const qs = environmentId ? `?environment_id=${encodeURIComponent(environmentId)}` : runtimeId ? `?runtime_id=${encodeURIComponent(runtimeId)}` : "";
1342
+ const resp = await fetch(`${cloudUrl}/api/v1/cloud-connection/status${qs}`, {
1343
+ headers: authHeaders()
1344
+ });
1345
+ if (resp.ok) {
1346
+ const json = await resp.json().catch(() => null);
1347
+ const data = json?.data ?? {};
1348
+ const bound2 = Boolean(data.bound) || Boolean(credential());
1349
+ return c.json({ success: true, data: {
1350
+ environmentId: environmentId ?? null,
1351
+ runtimeId: data.runtime_id ?? runtimeId ?? null,
1352
+ bound: bound2,
1353
+ provider: "objectstack-cloud",
1354
+ connection: data.connection ?? null
1355
+ } });
1356
+ }
1357
+ } catch {
1358
+ }
1359
+ }
1360
+ const bound = Boolean(credential());
1361
+ return c.json({ success: true, data: { environmentId: environmentId ?? null, runtimeId: runtimeId ?? null, bound, provider: "objectstack-cloud", connection: null } });
1362
+ });
1363
+ rawApp.post(`${CLOUD_CONNECTION_PREFIX}/bind/start`, async (c) => {
1364
+ let body = {};
1365
+ try {
1366
+ body = await c.req.json();
1367
+ } catch {
1368
+ body = {};
1369
+ }
1370
+ let environmentId = await resolveEnvironmentId(c);
1371
+ if (!environmentId && this.cfg.singleEnvironment) {
1372
+ environmentId = String(body?.environment_id ?? body?.environmentId ?? "").trim() || void 0;
1373
+ }
1374
+ if (!environmentId && !this.cfg.singleEnvironment) {
1375
+ return c.json({ success: false, error: { code: "environment_not_found" } }, 404);
1376
+ }
1377
+ const session = await resolveSession(environmentId ?? "", c.req.raw);
1378
+ if (!session?.userId) return c.json({ success: false, error: { code: "unauthenticated", message: "Sign in to this environment to connect a cloud account." } }, 401);
1379
+ if (!cloudUrl) return c.json({ success: false, error: { code: "cloud_unconfigured", message: "No cloud control plane configured." } }, 503);
1380
+ try {
1381
+ const resp = await fetch(`${cloudUrl}/api/v1/auth/device/code`, {
1382
+ method: "POST",
1383
+ headers: { "Content-Type": "application/json" },
1384
+ body: JSON.stringify({ client_id: deviceClientId, scope: deviceScope })
1385
+ });
1386
+ const json = await resp.json().catch(() => ({}));
1387
+ if (!resp.ok) return c.json({ success: false, error: { code: "device_code_failed", message: json?.error ?? `device/code ${resp.status}` } }, 502);
1388
+ const withContext = (uri) => {
1389
+ if (typeof uri !== "string" || !uri) return void 0;
1390
+ try {
1391
+ const u = new URL(uri);
1392
+ try {
1393
+ u.searchParams.set("runtime_name", hostname());
1394
+ } catch {
1395
+ }
1396
+ const ver = (process.env.OS_RUNTIME_VERSION ?? this.version) || "";
1397
+ if (ver) u.searchParams.set("runtime_version", ver);
1398
+ return u.toString();
1399
+ } catch {
1400
+ return uri;
1401
+ }
1402
+ };
1403
+ return c.json({ success: true, data: {
1404
+ device_code: json.device_code,
1405
+ user_code: json.user_code,
1406
+ verification_uri: withContext(json.verification_uri),
1407
+ verification_uri_complete: withContext(json.verification_uri_complete),
1408
+ interval: json.interval ?? 5,
1409
+ expires_in: json.expires_in ?? 600
1410
+ } });
1411
+ } catch (err) {
1412
+ ctx.logger?.error?.("[CloudConnectionPlugin] bind/start failed", err instanceof Error ? err : new Error(String(err)));
1413
+ return c.json({ success: false, error: { code: "device_code_failed", message: String(err?.message ?? err) } }, 502);
1414
+ }
1415
+ });
1416
+ rawApp.post(`${CLOUD_CONNECTION_PREFIX}/bind/poll`, async (c) => {
1417
+ let body = {};
1418
+ try {
1419
+ body = await c.req.json();
1420
+ } catch {
1421
+ body = {};
1422
+ }
1423
+ let environmentId = await resolveEnvironmentId(c);
1424
+ if (!environmentId && this.cfg.singleEnvironment) {
1425
+ environmentId = String(body?.environment_id ?? body?.environmentId ?? "").trim() || void 0;
1426
+ }
1427
+ if (!environmentId && !this.cfg.singleEnvironment) {
1428
+ return c.json({ success: false, error: { code: "environment_not_found" } }, 404);
1429
+ }
1430
+ const session = await resolveSession(environmentId ?? "", c.req.raw);
1431
+ if (!session?.userId) return c.json({ success: false, error: { code: "unauthenticated" } }, 401);
1432
+ if (!cloudUrl) return c.json({ success: false, error: { code: "cloud_unconfigured" } }, 503);
1433
+ const deviceCode = String(body?.device_code ?? body?.deviceCode ?? "").trim();
1434
+ if (!deviceCode) return c.json({ success: false, error: { code: "invalid_request", message: "device_code is required" } }, 400);
1435
+ try {
1436
+ const tokResp = await fetch(`${cloudUrl}/api/v1/auth/device/token`, {
1437
+ method: "POST",
1438
+ headers: { "Content-Type": "application/json" },
1439
+ body: JSON.stringify({ grant_type: "urn:ietf:params:oauth:grant-type:device_code", device_code: deviceCode, client_id: deviceClientId })
1440
+ });
1441
+ const tok = await tokResp.json().catch(() => ({}));
1442
+ const accessToken = tok?.access_token;
1443
+ if (!accessToken) {
1444
+ const errCode = String(tok?.error ?? `device/token ${tokResp.status}`);
1445
+ const pending = errCode === "authorization_pending" || errCode === "slow_down";
1446
+ return c.json({ success: pending, data: { pending }, error: pending ? void 0 : { code: errCode } }, pending ? 200 : 400);
1447
+ }
1448
+ const stored = this.store.read();
1449
+ const bindResp = await fetch(`${cloudUrl}/api/v1/cloud-connection/bind`, {
1450
+ method: "POST",
1451
+ headers: { "Content-Type": "application/json", ...cloudApiKey ? { Authorization: `Bearer ${cloudApiKey}` } : {} },
1452
+ body: JSON.stringify({
1453
+ ...environmentId ? { environment_id: environmentId } : {},
1454
+ ...stored?.runtimeId ? { runtime_id: stored.runtimeId } : {},
1455
+ name: (() => {
1456
+ try {
1457
+ return hostname();
1458
+ } catch {
1459
+ return void 0;
1460
+ }
1461
+ })(),
1462
+ runtime_version: (process.env.OS_RUNTIME_VERSION ?? this.version) || void 0,
1463
+ token: accessToken,
1464
+ scope: deviceScope
1465
+ })
1466
+ });
1467
+ const bindJson = await bindResp.json().catch(() => ({ success: false, error: "bind failed (no body)" }));
1468
+ const runtimeToken = bindJson?.data?.runtime_token;
1469
+ if (bindResp.ok && typeof runtimeToken === "string" && runtimeToken) {
1470
+ try {
1471
+ this.store.write({
1472
+ runtimeToken,
1473
+ runtimeId: bindJson?.data?.runtime_id ?? bindJson?.data?.connection?.runtime_id ?? void 0,
1474
+ environmentId: environmentId || void 0,
1475
+ controlPlaneUrl: cloudUrl,
1476
+ organizationId: bindJson?.data?.connection?.organization_id ?? void 0,
1477
+ accountEmail: bindJson?.data?.connection?.account_email ?? void 0,
1478
+ boundAt: bindJson?.data?.connection?.bound_at ?? (/* @__PURE__ */ new Date()).toISOString()
1479
+ });
1480
+ } catch (err) {
1481
+ ctx.logger?.error?.("[CloudConnectionPlugin] failed to persist runtime credential", err instanceof Error ? err : new Error(String(err)));
1482
+ }
1483
+ delete bindJson.data.runtime_token;
1484
+ }
1485
+ return c.json(bindJson, bindResp.status);
1486
+ } catch (err) {
1487
+ ctx.logger?.error?.("[CloudConnectionPlugin] bind/poll failed", err instanceof Error ? err : new Error(String(err)));
1488
+ return c.json({ success: false, error: { code: "bind_failed", message: String(err?.message ?? err) } }, 502);
1489
+ }
1490
+ });
1491
+ rawApp.post(`${CLOUD_CONNECTION_PREFIX}/unbind`, async (c) => {
1492
+ const environmentId = await resolveEnvironmentId(c);
1493
+ if (!environmentId && !this.cfg.singleEnvironment) {
1494
+ return c.json({ success: false, error: { code: "environment_not_found" } }, 404);
1495
+ }
1496
+ const session = await resolveSession(environmentId ?? "", c.req.raw);
1497
+ if (!session?.userId) return c.json({ success: false, error: { code: "unauthenticated" } }, 401);
1498
+ let revoked = false;
1499
+ if (cloudUrl && credential()) {
1500
+ try {
1501
+ const resp = await fetch(`${cloudUrl}/api/v1/cloud-connection/revoke`, {
1502
+ method: "POST",
1503
+ headers: { "Content-Type": "application/json", ...authHeaders() },
1504
+ body: JSON.stringify(environmentId ? { environment_id: environmentId } : {})
1505
+ });
1506
+ revoked = resp.ok;
1507
+ } catch {
1508
+ }
1509
+ }
1510
+ const residualId = this.store.read()?.runtimeId;
1511
+ const cleared = (() => {
1512
+ try {
1513
+ if (residualId) {
1514
+ this.store.write({ runtimeToken: "", runtimeId: residualId });
1515
+ return true;
1516
+ }
1517
+ return this.store.clear();
1518
+ } catch {
1519
+ return false;
1520
+ }
1521
+ })();
1522
+ return c.json({ success: true, data: { environmentId: environmentId ?? null, revoked, cleared } });
1523
+ });
1524
+ rawApp.post(`${CLOUD_CONNECTION_PREFIX}/install`, async (c) => {
1525
+ const environmentId = await resolveEnvironmentId(c);
1526
+ if (!environmentId) return c.json({ success: false, error: { code: "environment_not_found" } }, 404);
1527
+ const session = await resolveSession(environmentId, c.req.raw);
1528
+ if (!session?.userId) {
1529
+ return c.json({ success: false, error: { code: "unauthenticated", message: "Sign in to this environment to install apps." } }, 401);
1530
+ }
1531
+ if (!cloudUrl || !credential()) {
1532
+ return c.json({ success: false, error: { code: "cloud_unconfigured", message: "This runtime is not connected to a cloud account; install is unavailable." } }, 503);
1533
+ }
1534
+ let body = {};
1535
+ try {
1536
+ body = await c.req.json();
1537
+ } catch {
1538
+ body = {};
1539
+ }
1540
+ const packageId = String(body?.package_id ?? body?.packageId ?? "").trim();
1541
+ if (!packageId) return c.json({ success: false, error: { code: "invalid_request", message: "package_id is required" } }, 400);
1542
+ const seedSampleData = body?.seed_sample_data === true || body?.seedSampleData === true;
1543
+ try {
1544
+ const resp = await fetch(`${cloudUrl}/api/v1/actions/sys_package/install_package`, {
1545
+ method: "POST",
1546
+ headers: {
1547
+ "Content-Type": "application/json",
1548
+ ...authHeaders()
1549
+ },
1550
+ body: JSON.stringify({ recordId: packageId, params: { environment_id: environmentId, seed_sample_data: seedSampleData } })
1551
+ });
1552
+ const json = await resp.json().catch(() => ({ success: false, error: "install failed (no body)" }));
1553
+ return c.json(json, resp.status);
1554
+ } catch (err) {
1555
+ ctx.logger?.error?.("[CloudConnectionPlugin] install failed", err instanceof Error ? err : new Error(String(err)));
1556
+ return c.json({ success: false, error: { code: "install_failed", message: String(err?.message ?? err) } }, 502);
1557
+ }
1558
+ });
1559
+ rawApp.get(`${CLOUD_CONNECTION_PREFIX}/installation`, async (c) => {
1560
+ const environmentId = await resolveEnvironmentId(c);
1561
+ if (!environmentId) {
1562
+ if (this.cfg.singleEnvironment) return c.json({ success: true, data: { installed: false } });
1563
+ return c.json({ success: false, error: { code: "environment_not_found" } }, 404);
1564
+ }
1565
+ const session = await resolveSession(environmentId, c.req.raw);
1566
+ if (!session?.userId) {
1567
+ return c.json({ success: false, error: { code: "unauthenticated", message: "Sign in to this environment." } }, 401);
1568
+ }
1569
+ let packageId = "";
1570
+ try {
1571
+ const u = new URL(c.req.url);
1572
+ packageId = String(u.searchParams.get("package_id") ?? u.searchParams.get("packageId") ?? "").trim();
1573
+ } catch {
1574
+ }
1575
+ if (!packageId) return c.json({ success: false, error: { code: "invalid_request", message: "package_id is required" } }, 400);
1576
+ if (!cloudUrl || !credential()) return c.json({ success: true, data: { installed: false } });
1577
+ try {
1578
+ const resp = await fetch(
1579
+ `${cloudUrl}/api/v1/cloud/environments/${encodeURIComponent(environmentId)}/installations/${encodeURIComponent(packageId)}`,
1580
+ { headers: { Accept: "application/json", ...authHeaders() } }
1581
+ );
1582
+ if (!resp.ok) return c.json({ success: true, data: { installed: false } });
1583
+ const json = await resp.json().catch(() => ({}));
1584
+ const data = json?.data ?? json ?? {};
1585
+ if (!data.installed) return c.json({ success: true, data: { installed: false } });
1586
+ return c.json({ success: true, data: {
1587
+ installed: true,
1588
+ installationId: String(data.installationId ?? ""),
1589
+ version: String(data.version ?? "installed"),
1590
+ withSampleData: data.withSampleData === true
1591
+ } });
1592
+ } catch (err) {
1593
+ ctx.logger?.warn?.(`[CloudConnectionPlugin] installation probe failed: ${String(err?.message ?? err)}`);
1594
+ return c.json({ success: true, data: { installed: false } });
1595
+ }
1596
+ });
1597
+ rawApp.get(`${CLOUD_CONNECTION_PREFIX}/installed`, async (c) => {
1598
+ const environmentId = await resolveEnvironmentId(c);
1599
+ if (!environmentId) {
1600
+ if (this.cfg.singleEnvironment) {
1601
+ return c.json({ success: true, data: { packages: [], total: 0, connected: Boolean(credential()) } });
1602
+ }
1603
+ return c.json({ success: false, error: { code: "environment_not_found" } }, 404);
1604
+ }
1605
+ const session = await resolveSession(environmentId, c.req.raw);
1606
+ if (!session?.userId) {
1607
+ return c.json({ success: false, error: { code: "unauthenticated", message: "Sign in to this environment." } }, 401);
1608
+ }
1609
+ if (!cloudUrl || !credential()) {
1610
+ return c.json({ success: true, data: { packages: [], total: 0, connected: false } });
1611
+ }
1612
+ try {
1613
+ const resp = await fetch(
1614
+ `${cloudUrl}/api/v1/cloud/environments/${encodeURIComponent(environmentId)}/packages`,
1615
+ { headers: { Accept: "application/json", ...authHeaders() } }
1616
+ );
1617
+ if (!resp.ok) return c.json({ success: true, data: { packages: [], total: 0, connected: true } });
1618
+ const json = await resp.json().catch(() => ({}));
1619
+ const data = json?.data ?? json ?? {};
1620
+ const packages = Array.isArray(data.packages) ? data.packages : [];
1621
+ return c.json({ success: true, data: { packages, total: packages.length, connected: true } });
1622
+ } catch (err) {
1623
+ ctx.logger?.warn?.(`[CloudConnectionPlugin] installed-list failed: ${String(err?.message ?? err)}`);
1624
+ return c.json({ success: true, data: { packages: [], total: 0, connected: true } });
1625
+ }
1626
+ });
1627
+ rawApp.get(`${CLOUD_CONNECTION_PREFIX}/org-packages`, async (c) => {
1628
+ const environmentId = await resolveEnvironmentId(c);
1629
+ if (!environmentId && !this.cfg.singleEnvironment) {
1630
+ return c.json({ success: false, error: { code: "environment_not_found" } }, 404);
1631
+ }
1632
+ const session = await resolveSession(environmentId ?? "", c.req.raw);
1633
+ if (!session?.userId) {
1634
+ return c.json({ success: false, error: { code: "unauthenticated", message: "Sign in to this environment." } }, 401);
1635
+ }
1636
+ if (!cloudUrl || !credential()) {
1637
+ return c.json({ success: true, data: { items: [], total: 0, connected: false } });
1638
+ }
1639
+ try {
1640
+ const qs = environmentId ? `?environment_id=${encodeURIComponent(environmentId)}` : "";
1641
+ const resp = await fetch(
1642
+ `${cloudUrl}/api/v1/cloud/org-packages${qs}`,
1643
+ { headers: { Accept: "application/json", ...authHeaders() } }
1644
+ );
1645
+ if (!resp.ok) return c.json({ success: true, data: { items: [], total: 0, connected: true } });
1646
+ const json = await resp.json().catch(() => ({}));
1647
+ const data = json?.data ?? json ?? {};
1648
+ const items = Array.isArray(data.items) ? data.items : [];
1649
+ return c.json({ success: true, data: { items, total: items.length, connected: true } });
1650
+ } catch (err) {
1651
+ ctx.logger?.warn?.(`[CloudConnectionPlugin] org-packages failed: ${String(err?.message ?? err)}`);
1652
+ return c.json({ success: true, data: { items: [], total: 0, connected: true } });
1653
+ }
1654
+ });
1655
+ ctx.logger?.info?.(`[CloudConnectionPlugin] mounted ${CLOUD_CONNECTION_PREFIX}/{status,bind/start,bind/poll,install,installation,installed,org-packages} \u2192 ${cloudUrl || "(cloud unconfigured)"}`);
1656
+ });
1657
+ };
1658
+ this.cfg = config;
1659
+ this.store = new ConnectionCredentialStore(config.credentialPath);
1660
+ }
1661
+ };
1662
+ function createCloudConnectionPlugin(config = {}) {
1663
+ return new CloudConnectionPlugin(config);
1664
+ }
1665
+
1666
+ // src/runtime-config-plugin.ts
1667
+ var RuntimeConfigPlugin = class {
1668
+ constructor(config = {}) {
1669
+ this.name = "com.objectstack.runtime.runtime-config";
1670
+ this.version = "1.0.0";
1671
+ this.init = async (_ctx) => {
1672
+ };
1673
+ this.start = async (ctx) => {
1674
+ ctx.hook("kernel:ready", async () => {
1675
+ let httpServer;
1676
+ try {
1677
+ httpServer = ctx.getService("http-server");
1678
+ } catch {
1679
+ ctx.logger?.warn?.("[RuntimeConfigPlugin] http-server not available \u2014 runtime/config not mounted");
1680
+ return;
1681
+ }
1682
+ if (!httpServer || typeof httpServer.getRawApp !== "function") {
1683
+ ctx.logger?.warn?.("[RuntimeConfigPlugin] http-server missing getRawApp() \u2014 runtime/config not mounted");
1684
+ return;
1685
+ }
1686
+ const rawApp = httpServer.getRawApp();
1687
+ let envRegistry = null;
1688
+ try {
1689
+ envRegistry = ctx.getService("env-registry");
1690
+ } catch {
1691
+ }
1692
+ const featuresFor = (plan, base) => {
1693
+ const derived = this.resolvePlanFeatures?.(plan);
1694
+ return {
1695
+ aiStudio: derived?.aiStudio ?? base.aiStudio,
1696
+ autoPublishAiBuilds: derived?.autoPublishAiBuilds ?? base.autoPublishAiBuilds
1697
+ };
1698
+ };
1699
+ const handler = async (c) => {
1700
+ const rawHost = c.req.header("host") ?? "";
1701
+ const host = rawHost.split(":")[0].toLowerCase().trim();
1702
+ let defaultEnvironmentId;
1703
+ let defaultOrgId;
1704
+ let resolvedSingleEnv = this.singleEnvironment;
1705
+ let features = featuresFor(void 0, { aiStudio: this.aiStudio, autoPublishAiBuilds: false });
1706
+ const resolveFn = typeof envRegistry?.resolveByHostname === "function" ? envRegistry.resolveByHostname.bind(envRegistry) : typeof envRegistry?.resolveHostname === "function" ? envRegistry.resolveHostname.bind(envRegistry) : null;
1707
+ if (resolveFn && host) {
1708
+ try {
1709
+ const resolved = await resolveFn(host);
1710
+ if (resolved?.environmentId) {
1711
+ defaultEnvironmentId = String(resolved.environmentId);
1712
+ const orgId = resolved.organizationId ?? resolved.organization_id;
1713
+ if (orgId) defaultOrgId = String(orgId);
1714
+ resolvedSingleEnv = true;
1715
+ if (typeof resolved.plan === "string" && resolved.plan.trim() !== "") {
1716
+ features = featuresFor(resolved.plan, features);
1717
+ }
1718
+ }
1719
+ } catch {
1720
+ }
1721
+ }
1722
+ return c.json({
1723
+ cloudUrl: this.cloudUrl,
1724
+ singleEnvironment: resolvedSingleEnv,
1725
+ defaultOrgId,
1726
+ defaultEnvironmentId,
1727
+ features: {
1728
+ installLocal: this.installLocal,
1729
+ marketplace: true,
1730
+ aiStudio: features.aiStudio,
1731
+ autoPublishAiBuilds: features.autoPublishAiBuilds
1732
+ },
1733
+ branding: {
1734
+ productName: this.productName,
1735
+ productShortName: this.productShortName
1736
+ }
1737
+ });
1738
+ };
1739
+ rawApp.get("/api/v1/runtime/config", handler);
1740
+ rawApp.get("/api/v1/studio/runtime-config", handler);
1741
+ ctx.logger?.info?.("[RuntimeConfigPlugin] mounted /api/v1/runtime/config", {
1742
+ cloudUrl: this.cloudUrl || "(empty)",
1743
+ installLocal: this.installLocal,
1744
+ perHostEnvResolution: !!envRegistry
1745
+ });
1746
+ });
1747
+ };
1748
+ this.destroy = async () => {
1749
+ };
1750
+ this.cloudUrl = config.controlPlaneUrl === "" ? "" : resolveCloudUrl(config.controlPlaneUrl) ?? "";
1751
+ this.installLocal = !!config.installLocal;
1752
+ this.aiStudio = config.aiStudio !== false;
1753
+ this.singleEnvironment = !!config.singleEnvironment;
1754
+ this.resolvePlanFeatures = config.resolvePlanFeatures;
1755
+ const envName = (typeof process !== "undefined" ? process.env?.OS_PRODUCT_NAME : void 0)?.trim();
1756
+ const envShort = (typeof process !== "undefined" ? process.env?.OS_PRODUCT_SHORT_NAME : void 0)?.trim();
1757
+ this.productName = (config.productName ?? envName ?? "ObjectOS").trim() || "ObjectOS";
1758
+ this.productShortName = (config.productShortName ?? envShort ?? this.productName).trim() || this.productName;
1759
+ }
1760
+ };
1761
+ export {
1762
+ CLOUD_CONNECTION_UI_BUNDLE,
1763
+ CloudConnectionPlugin,
1764
+ CloudConnectionSettingsPage,
1765
+ ConnectionCredentialStore,
1766
+ DEFAULT_CLOUD_URL,
1767
+ DEFAULT_CONNECTION_CREDENTIAL_PATH,
1768
+ DEFAULT_INSTALLED_PACKAGES_DIR,
1769
+ LocalManifestSource,
1770
+ MARKETPLACE_BROWSE_UI_BUNDLE,
1771
+ MARKETPLACE_INSTALLED_UI_BUNDLE,
1772
+ MarketplaceInstallLocalPlugin,
1773
+ MarketplaceProxyPlugin,
1774
+ RuntimeConfigPlugin,
1775
+ createCloudConnectionPlugin,
1776
+ publicMarketplaceKeyForApiPath,
1777
+ resolveCloudUrl,
1778
+ resolveMarketplacePublicBaseUrl
1779
+ };
1780
+ //# sourceMappingURL=index.js.map