@objectstack/runtime 4.0.5 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -231,7 +231,7 @@ var init_seed_loader = __esm({
231
231
  this.logger.info("[SeedLoader] Pass 2: resolving deferred references", {
232
232
  count: deferredUpdates.length
233
233
  });
234
- await this.resolveDeferredUpdates(deferredUpdates, insertedRecords, allResults, allErrors);
234
+ await this.resolveDeferredUpdates(deferredUpdates, insertedRecords, allResults, allErrors, config.organizationId);
235
235
  }
236
236
  const durationMs = Date.now() - startTime;
237
237
  return this.buildResult(config, graph, allResults, allErrors, durationMs);
@@ -288,7 +288,11 @@ var init_seed_loader = __esm({
288
288
  }
289
289
  let existingRecords;
290
290
  if ((mode === "upsert" || mode === "update" || mode === "ignore") && !config.dryRun) {
291
- existingRecords = await this.loadExistingRecords(objectName, externalId);
291
+ existingRecords = await this.loadExistingRecords(
292
+ objectName,
293
+ externalId,
294
+ config.organizationId
295
+ );
292
296
  }
293
297
  const objectRefs = refMap.get(objectName) || [];
294
298
  const seedNow = /* @__PURE__ */ new Date();
@@ -303,6 +307,9 @@ var init_seed_loader = __esm({
303
307
  `[SeedLoader] Failed to resolve dynamic values for ${objectName} record #${i}: ${seedResult.error.message}`
304
308
  );
305
309
  }
310
+ if (config.organizationId && record["organization_id"] == null) {
311
+ record["organization_id"] = config.organizationId;
312
+ }
306
313
  for (const ref of objectRefs) {
307
314
  const fieldValue = record[ref.field];
308
315
  if (fieldValue === void 0 || fieldValue === null) continue;
@@ -313,7 +320,7 @@ var init_seed_loader = __esm({
313
320
  record[ref.field] = resolvedId;
314
321
  referencesResolved++;
315
322
  } else if (!config.dryRun) {
316
- const dbId = await this.resolveFromDatabase(ref.targetObject, ref.targetField, fieldValue);
323
+ const dbId = await this.resolveFromDatabase(ref.targetObject, ref.targetField, fieldValue, config.organizationId);
317
324
  if (dbId) {
318
325
  record[ref.field] = dbId;
319
326
  referencesResolved++;
@@ -407,10 +414,12 @@ var init_seed_loader = __esm({
407
414
  // ==========================================================================
408
415
  // Internal: Reference Resolution
409
416
  // ==========================================================================
410
- async resolveFromDatabase(targetObject, targetField, value) {
417
+ async resolveFromDatabase(targetObject, targetField, value, organizationId) {
411
418
  try {
419
+ const where = { [targetField]: value };
420
+ if (organizationId) where.organization_id = organizationId;
412
421
  const records = await this.engine.find(targetObject, {
413
- where: { [targetField]: value },
422
+ where,
414
423
  fields: ["id"],
415
424
  limit: 1,
416
425
  context: { isSystem: true }
@@ -422,7 +431,7 @@ var init_seed_loader = __esm({
422
431
  }
423
432
  return null;
424
433
  }
425
- async resolveDeferredUpdates(deferredUpdates, insertedRecords, allResults, allErrors) {
434
+ async resolveDeferredUpdates(deferredUpdates, insertedRecords, allResults, allErrors, organizationId) {
426
435
  for (const deferred of deferredUpdates) {
427
436
  const targetMap = insertedRecords.get(deferred.targetObject);
428
437
  let resolvedId = targetMap?.get(String(deferred.attemptedValue));
@@ -430,7 +439,8 @@ var init_seed_loader = __esm({
430
439
  resolvedId = await this.resolveFromDatabase(
431
440
  deferred.targetObject,
432
441
  deferred.targetField,
433
- deferred.attemptedValue
442
+ deferred.attemptedValue,
443
+ organizationId
434
444
  ) ?? void 0;
435
445
  }
436
446
  if (resolvedId) {
@@ -626,13 +636,15 @@ var init_seed_loader = __esm({
626
636
  }
627
637
  return map2;
628
638
  }
629
- async loadExistingRecords(objectName, externalId) {
639
+ async loadExistingRecords(objectName, externalId, organizationId) {
630
640
  const map2 = /* @__PURE__ */ new Map();
631
641
  try {
632
- const records = await this.engine.find(objectName, {
642
+ const findArgs = {
633
643
  fields: ["id", externalId],
634
644
  context: { isSystem: true }
635
- });
645
+ };
646
+ if (organizationId) findArgs.where = { organization_id: organizationId };
647
+ const records = await this.engine.find(objectName, findArgs);
636
648
  for (const record of records || []) {
637
649
  const key = String(record[externalId] ?? "");
638
650
  if (key) {
@@ -1412,6 +1424,49 @@ var init_app_plugin = __esm({
1412
1424
  appId
1413
1425
  });
1414
1426
  }
1427
+ try {
1428
+ const approvals = Array.isArray(this.bundle.approvals) ? this.bundle.approvals : Array.isArray((this.bundle.manifest || {}).approvals) ? this.bundle.manifest.approvals : [];
1429
+ if (approvals.length > 0) {
1430
+ ctx.hook("kernel:ready", async () => {
1431
+ let svc;
1432
+ try {
1433
+ svc = ctx.getService("approvals");
1434
+ } catch {
1435
+ }
1436
+ if (!svc || typeof svc.defineProcess !== "function") {
1437
+ ctx.logger.warn("[AppPlugin] approvals service not registered \u2014 skipping declarative processes", {
1438
+ appId,
1439
+ processCount: approvals.length
1440
+ });
1441
+ return;
1442
+ }
1443
+ const sysCtx = { isSystem: true, roles: [], permissions: [] };
1444
+ let ok = 0;
1445
+ for (const proc of approvals) {
1446
+ try {
1447
+ await svc.defineProcess({
1448
+ name: proc.name,
1449
+ label: proc.label,
1450
+ object: proc.object,
1451
+ description: proc.description,
1452
+ active: proc.active !== false,
1453
+ definition: proc
1454
+ }, sysCtx);
1455
+ ok++;
1456
+ } catch (err) {
1457
+ ctx.logger.warn("[AppPlugin] Failed to register approval process", {
1458
+ appId,
1459
+ process: proc?.name,
1460
+ error: err?.message ?? String(err)
1461
+ });
1462
+ }
1463
+ }
1464
+ ctx.logger.info("[AppPlugin] Registered approval processes", { appId, count: ok });
1465
+ });
1466
+ }
1467
+ } catch (err) {
1468
+ ctx.logger.error("[AppPlugin] Failed to schedule approval-process registration", err, { appId });
1469
+ }
1415
1470
  this.emitCatalogEvent(ctx, "app:registered", sys);
1416
1471
  this.loadTranslations(ctx, appId);
1417
1472
  const seedDatasets = [];
@@ -1429,46 +1484,107 @@ var init_app_plugin = __esm({
1429
1484
  object: d.object
1430
1485
  }));
1431
1486
  try {
1432
- const metadata = ctx.getService("metadata");
1433
- if (metadata) {
1434
- const seedLoader = new SeedLoaderService(ql, metadata, ctx.logger);
1487
+ const kernel = ctx.kernel;
1488
+ const existing = (() => {
1489
+ try {
1490
+ return kernel?.getService?.("seed-datasets");
1491
+ } catch {
1492
+ return void 0;
1493
+ }
1494
+ })();
1495
+ const merged = Array.isArray(existing) ? [...existing, ...normalizedDatasets] : normalizedDatasets;
1496
+ const registerSvc = (name, value) => {
1497
+ if (kernel?.registerService) kernel.registerService(name, value);
1498
+ else if (typeof ctx.registerService === "function") ctx.registerService(name, value);
1499
+ };
1500
+ registerSvc("seed-datasets", merged);
1501
+ const metadataNow = ctx.getService("metadata");
1502
+ const loggerRef = ctx.logger;
1503
+ const replayer = async (organizationId) => {
1504
+ if (!organizationId) return { inserted: 0, updated: 0, errors: [] };
1505
+ const md = metadataNow ?? ctx.getService("metadata");
1506
+ if (!md) {
1507
+ loggerRef.warn("[seed-replayer] metadata service unavailable");
1508
+ return { inserted: 0, updated: 0, errors: [] };
1509
+ }
1510
+ const datasetsNow = (() => {
1511
+ try {
1512
+ return kernel?.getService?.("seed-datasets");
1513
+ } catch {
1514
+ return merged;
1515
+ }
1516
+ })() ?? merged;
1517
+ if (!Array.isArray(datasetsNow) || datasetsNow.length === 0) {
1518
+ return { inserted: 0, updated: 0, errors: [] };
1519
+ }
1520
+ const seedLoader = new SeedLoaderService(ql, md, loggerRef);
1435
1521
  const { SeedLoaderRequestSchema } = await import("@objectstack/spec/data");
1436
1522
  const request = SeedLoaderRequestSchema.parse({
1437
- datasets: normalizedDatasets,
1438
- config: { defaultMode: "upsert", multiPass: true }
1523
+ datasets: datasetsNow,
1524
+ config: {
1525
+ defaultMode: "upsert",
1526
+ multiPass: true,
1527
+ organizationId
1528
+ }
1439
1529
  });
1440
1530
  const result = await seedLoader.load(request);
1441
- ctx.logger.info("[Seeder] Seed loading complete", {
1531
+ return {
1442
1532
  inserted: result.summary.totalInserted,
1443
1533
  updated: result.summary.totalUpdated,
1444
- errors: result.errors.length
1445
- });
1446
- } else {
1447
- ctx.logger.debug("[Seeder] No metadata service; using basic insert fallback");
1534
+ errors: result.errors
1535
+ };
1536
+ };
1537
+ registerSvc("seed-replayer", replayer);
1538
+ ctx.logger.info(`[Seeder] Registered ${normalizedDatasets.length} datasets + replayer on kernel (total datasets: ${merged.length})`);
1539
+ } catch (e) {
1540
+ ctx.logger.warn("[Seeder] Failed to register seed-datasets/seed-replayer service", { error: e?.message });
1541
+ }
1542
+ const multiTenant = String(process.env.OS_MULTI_TENANT ?? "true").toLowerCase() !== "false";
1543
+ if (multiTenant) {
1544
+ ctx.logger.info("[Seeder] multi-tenant mode \u2014 skipping inline seed; per-org replay will run on sys_organization insert");
1545
+ } else {
1546
+ try {
1547
+ const metadata = ctx.getService("metadata");
1548
+ if (metadata) {
1549
+ const seedLoader = new SeedLoaderService(ql, metadata, ctx.logger);
1550
+ const { SeedLoaderRequestSchema } = await import("@objectstack/spec/data");
1551
+ const request = SeedLoaderRequestSchema.parse({
1552
+ datasets: normalizedDatasets,
1553
+ config: { defaultMode: "upsert", multiPass: true }
1554
+ });
1555
+ const result = await seedLoader.load(request);
1556
+ ctx.logger.info("[Seeder] Seed loading complete", {
1557
+ inserted: result.summary.totalInserted,
1558
+ updated: result.summary.totalUpdated,
1559
+ errors: result.errors.length
1560
+ });
1561
+ } else {
1562
+ ctx.logger.debug("[Seeder] No metadata service; using basic insert fallback");
1563
+ for (const dataset of normalizedDatasets) {
1564
+ ctx.logger.info(`[Seeder] Seeding ${dataset.records.length} records for ${dataset.object}`);
1565
+ for (const record of dataset.records) {
1566
+ try {
1567
+ await ql.insert(dataset.object, record, { context: { isSystem: true } });
1568
+ } catch (err) {
1569
+ ctx.logger.warn(`[Seeder] Failed to insert ${dataset.object} record:`, { error: err.message });
1570
+ }
1571
+ }
1572
+ }
1573
+ ctx.logger.info("[Seeder] Data seeding complete.");
1574
+ }
1575
+ } catch (err) {
1576
+ ctx.logger.warn("[Seeder] SeedLoaderService failed, falling back to basic insert", { error: err.message });
1448
1577
  for (const dataset of normalizedDatasets) {
1449
- ctx.logger.info(`[Seeder] Seeding ${dataset.records.length} records for ${dataset.object}`);
1450
1578
  for (const record of dataset.records) {
1451
1579
  try {
1452
1580
  await ql.insert(dataset.object, record, { context: { isSystem: true } });
1453
- } catch (err) {
1454
- ctx.logger.warn(`[Seeder] Failed to insert ${dataset.object} record:`, { error: err.message });
1581
+ } catch (insertErr) {
1582
+ ctx.logger.warn(`[Seeder] Failed to insert ${dataset.object} record:`, { error: insertErr.message });
1455
1583
  }
1456
1584
  }
1457
1585
  }
1458
- ctx.logger.info("[Seeder] Data seeding complete.");
1459
- }
1460
- } catch (err) {
1461
- ctx.logger.warn("[Seeder] SeedLoaderService failed, falling back to basic insert", { error: err.message });
1462
- for (const dataset of normalizedDatasets) {
1463
- for (const record of dataset.records) {
1464
- try {
1465
- await ql.insert(dataset.object, record, { context: { isSystem: true } });
1466
- } catch (insertErr) {
1467
- ctx.logger.warn(`[Seeder] Failed to insert ${dataset.object} record:`, { error: insertErr.message });
1468
- }
1469
- }
1586
+ ctx.logger.info("[Seeder] Data seeding complete (fallback).");
1470
1587
  }
1471
- ctx.logger.info("[Seeder] Data seeding complete (fallback).");
1472
1588
  }
1473
1589
  }
1474
1590
  };
@@ -34368,8 +34484,360 @@ var init_dist = __esm({
34368
34484
  }
34369
34485
  });
34370
34486
 
34487
+ // src/cloud/platform-sso.ts
34488
+ var platform_sso_exports = {};
34489
+ __export(platform_sso_exports, {
34490
+ PLATFORM_SSO_PROVIDER_ID: () => PLATFORM_SSO_PROVIDER_ID,
34491
+ backfillPlatformSsoClients: () => backfillPlatformSsoClients,
34492
+ buildPlatformSsoRedirectUri: () => buildPlatformSsoRedirectUri,
34493
+ derivePlatformSsoClientId: () => derivePlatformSsoClientId,
34494
+ derivePlatformSsoClientSecret: () => derivePlatformSsoClientSecret,
34495
+ hashPlatformSsoClientSecret: () => hashPlatformSsoClientSecret,
34496
+ seedPlatformSsoClient: () => seedPlatformSsoClient
34497
+ });
34498
+ import { createHmac, createHash } from "crypto";
34499
+ function derivePlatformSsoClientId(projectId) {
34500
+ return `project_${projectId}`;
34501
+ }
34502
+ function derivePlatformSsoClientSecret(baseSecret, projectId) {
34503
+ return createHmac("sha256", baseSecret).update(`oauth-client:${projectId}`).digest("hex");
34504
+ }
34505
+ function hashPlatformSsoClientSecret(plaintext) {
34506
+ return createHash("sha256").update(plaintext).digest("base64").replace(/=+$/, "").replace(/\+/g, "-").replace(/\//g, "_");
34507
+ }
34508
+ function buildPlatformSsoRedirectUri(hostname, basePath = "/api/v1/auth") {
34509
+ let host;
34510
+ if (hostname.startsWith("http://") || hostname.startsWith("https://")) {
34511
+ host = hostname;
34512
+ } else if (/(\.|^)localhost(:\d+)?$/i.test(hostname)) {
34513
+ const port = (process.env.OS_RUNTIME_PORT ?? "").trim();
34514
+ const hostWithPort = /:\d+$/.test(hostname) || !port ? hostname : `${hostname}:${port}`;
34515
+ host = `http://${hostWithPort}`;
34516
+ } else {
34517
+ host = `https://${hostname}`;
34518
+ }
34519
+ const trimmed = host.replace(/\/+$/, "");
34520
+ const path = basePath.replace(/\/+$/, "");
34521
+ return `${trimmed}${path}/oauth2/callback/${PLATFORM_SSO_PROVIDER_ID}`;
34522
+ }
34523
+ async function seedPlatformSsoClient(opts) {
34524
+ const { ql, projectId, hostname, baseSecret, logger, throwOnError } = opts;
34525
+ if (!baseSecret) {
34526
+ logger?.warn?.("[platform-sso] OS_AUTH_SECRET not set \u2014 skipping client seed", { projectId });
34527
+ return;
34528
+ }
34529
+ const clientId = derivePlatformSsoClientId(projectId);
34530
+ const clientSecretPlaintext = derivePlatformSsoClientSecret(baseSecret, projectId);
34531
+ const clientSecretStored = hashPlatformSsoClientSecret(clientSecretPlaintext);
34532
+ const desiredRedirect = hostname ? buildPlatformSsoRedirectUri(hostname) : null;
34533
+ let existing = null;
34534
+ try {
34535
+ const rows = await ql.find("sys_oauth_application", {
34536
+ where: { client_id: clientId },
34537
+ limit: 1
34538
+ }, { context: { isSystem: true } });
34539
+ const list = Array.isArray(rows) ? rows : Array.isArray(rows?.records) ? rows.records : [];
34540
+ existing = list[0] ?? null;
34541
+ } catch (err) {
34542
+ logger?.warn?.("[platform-sso] sys_oauth_application read failed \u2014 skipping seed", {
34543
+ projectId,
34544
+ error: err?.message
34545
+ });
34546
+ return;
34547
+ }
34548
+ const nowIso = (/* @__PURE__ */ new Date()).toISOString();
34549
+ if (!existing) {
34550
+ const redirects = desiredRedirect ? [desiredRedirect] : [];
34551
+ try {
34552
+ await ql.insert("sys_oauth_application", {
34553
+ id: `oauthc_${projectId}`,
34554
+ name: `Project ${projectId}`,
34555
+ client_id: clientId,
34556
+ client_secret: clientSecretStored,
34557
+ type: "web",
34558
+ redirect_uris: JSON.stringify(redirects),
34559
+ grant_types: JSON.stringify(["authorization_code", "refresh_token"]),
34560
+ response_types: JSON.stringify(["code"]),
34561
+ scopes: JSON.stringify(["openid", "email", "profile"]),
34562
+ token_endpoint_auth_method: "client_secret_basic",
34563
+ require_pkce: false,
34564
+ skip_consent: true,
34565
+ disabled: false,
34566
+ subject_type: "public",
34567
+ created_at: nowIso,
34568
+ updated_at: nowIso
34569
+ }, { context: { isSystem: true } });
34570
+ logger?.info?.("[platform-sso] sys_oauth_application row created", { projectId, clientId });
34571
+ } catch (err) {
34572
+ logger?.warn?.("[platform-sso] sys_oauth_application create failed", {
34573
+ projectId,
34574
+ error: err?.message
34575
+ });
34576
+ if (throwOnError) throw err;
34577
+ }
34578
+ return;
34579
+ }
34580
+ let currentRedirects = [];
34581
+ try {
34582
+ const raw = existing.redirect_uris;
34583
+ const parsed = typeof raw === "string" ? JSON.parse(raw) : raw;
34584
+ if (Array.isArray(parsed)) currentRedirects = parsed.filter((s) => typeof s === "string");
34585
+ } catch {
34586
+ }
34587
+ const mergedRedirects = desiredRedirect && !currentRedirects.includes(desiredRedirect) ? [...currentRedirects, desiredRedirect] : currentRedirects;
34588
+ const repairPatch = {
34589
+ name: existing.name || `Project ${projectId}`,
34590
+ client_secret: clientSecretStored,
34591
+ type: existing.type || "web",
34592
+ redirect_uris: JSON.stringify(mergedRedirects),
34593
+ grant_types: JSON.stringify(["authorization_code", "refresh_token"]),
34594
+ response_types: JSON.stringify(["code"]),
34595
+ scopes: JSON.stringify(["openid", "email", "profile"]),
34596
+ token_endpoint_auth_method: "client_secret_basic",
34597
+ require_pkce: false,
34598
+ skip_consent: true,
34599
+ disabled: false,
34600
+ subject_type: "public",
34601
+ updated_at: nowIso
34602
+ };
34603
+ try {
34604
+ await ql.update(
34605
+ "sys_oauth_application",
34606
+ repairPatch,
34607
+ { where: { id: existing.id } },
34608
+ { context: { isSystem: true } }
34609
+ );
34610
+ logger?.info?.("[platform-sso] sys_oauth_application repaired", {
34611
+ projectId,
34612
+ clientId,
34613
+ redirect_uris: mergedRedirects
34614
+ });
34615
+ } catch (err) {
34616
+ logger?.warn?.("[platform-sso] sys_oauth_application repair failed", {
34617
+ projectId,
34618
+ error: err?.message
34619
+ });
34620
+ if (throwOnError) throw err;
34621
+ }
34622
+ }
34623
+ async function backfillPlatformSsoClients(opts) {
34624
+ const { ql, baseSecret, logger, limit = 1e3 } = opts;
34625
+ if (!baseSecret) {
34626
+ logger?.warn?.("[platform-sso] backfill skipped \u2014 OS_AUTH_SECRET not set");
34627
+ return { scanned: 0, seeded: 0, alreadyExisted: 0, failures: [] };
34628
+ }
34629
+ let projects = [];
34630
+ try {
34631
+ const rows = await ql.find("sys_environment", {
34632
+ limit,
34633
+ fields: ["id", "hostname", "status"]
34634
+ }, { context: { isSystem: true } });
34635
+ projects = Array.isArray(rows) ? rows : Array.isArray(rows?.records) ? rows.records : [];
34636
+ } catch (err) {
34637
+ logger?.warn?.("[platform-sso] backfill: sys_project read failed", {
34638
+ error: err?.message
34639
+ });
34640
+ return { scanned: 0, seeded: 0, alreadyExisted: 0, failures: [{ projectId: "<scan>", error: err?.message ?? String(err) }] };
34641
+ }
34642
+ let seeded = 0;
34643
+ let alreadyExisted = 0;
34644
+ const failures = [];
34645
+ for (const p of projects) {
34646
+ if (!p?.id) continue;
34647
+ const before = await (async () => {
34648
+ try {
34649
+ const r = await ql.find("sys_oauth_application", {
34650
+ where: { client_id: derivePlatformSsoClientId(p.id) },
34651
+ limit: 1
34652
+ }, { context: { isSystem: true } });
34653
+ const list = Array.isArray(r) ? r : Array.isArray(r?.records) ? r.records : [];
34654
+ return list[0] ?? null;
34655
+ } catch {
34656
+ return null;
34657
+ }
34658
+ })();
34659
+ try {
34660
+ await seedPlatformSsoClient({ ql, projectId: p.id, hostname: p.hostname, baseSecret, logger, throwOnError: true });
34661
+ if (before) alreadyExisted++;
34662
+ else {
34663
+ const after = await (async () => {
34664
+ try {
34665
+ const r = await ql.find("sys_oauth_application", {
34666
+ where: { client_id: derivePlatformSsoClientId(p.id) },
34667
+ limit: 1
34668
+ }, { context: { isSystem: true } });
34669
+ const list = Array.isArray(r) ? r : Array.isArray(r?.records) ? r.records : [];
34670
+ return list[0] ?? null;
34671
+ } catch (err) {
34672
+ return { _readErr: err?.message };
34673
+ }
34674
+ })();
34675
+ if (after && !after._readErr) seeded++;
34676
+ else failures.push({ projectId: p.id, error: `post-insert read returned ${after ? JSON.stringify(after) : "null"}` });
34677
+ }
34678
+ } catch (err) {
34679
+ failures.push({ projectId: p.id, error: err?.message ?? String(err) });
34680
+ }
34681
+ }
34682
+ logger?.info?.("[platform-sso] backfill complete", { scanned: projects.length, seeded, alreadyExisted, failures: failures.length });
34683
+ return { scanned: projects.length, seeded, alreadyExisted, failures };
34684
+ }
34685
+ var PLATFORM_SSO_PROVIDER_ID;
34686
+ var init_platform_sso = __esm({
34687
+ "src/cloud/platform-sso.ts"() {
34688
+ "use strict";
34689
+ PLATFORM_SSO_PROVIDER_ID = "objectstack-cloud";
34690
+ }
34691
+ });
34692
+
34693
+ // src/cloud/project-org-seed.ts
34694
+ var project_org_seed_exports = {};
34695
+ __export(project_org_seed_exports, {
34696
+ seedProjectMember: () => seedProjectMember,
34697
+ seedProjectOrganization: () => seedProjectOrganization
34698
+ });
34699
+ async function seedProjectOrganization(kernel, seed, logger) {
34700
+ if (!seed?.id || !seed?.name) return "skipped";
34701
+ try {
34702
+ const ql = kernel.getService("objectql");
34703
+ if (!ql?.insert || !ql?.find) {
34704
+ logger?.warn?.("[seedProjectOrganization] objectql service unavailable", { orgId: seed.id });
34705
+ return "skipped";
34706
+ }
34707
+ try {
34708
+ const existing = await ql.find(SYS_ORG, { where: { id: seed.id } });
34709
+ const rows = Array.isArray(existing) ? existing : existing?.value ?? [];
34710
+ if (Array.isArray(rows) && rows.length > 0) return "exists";
34711
+ } catch {
34712
+ }
34713
+ const nowIso = (/* @__PURE__ */ new Date()).toISOString();
34714
+ await ql.insert(SYS_ORG, {
34715
+ id: seed.id,
34716
+ name: seed.name,
34717
+ slug: seed.slug ?? null,
34718
+ logo: seed.logo ?? null,
34719
+ metadata: null,
34720
+ created_at: nowIso
34721
+ });
34722
+ logger?.info?.("[seedProjectOrganization] org seeded", {
34723
+ orgId: seed.id,
34724
+ name: seed.name
34725
+ });
34726
+ return "inserted";
34727
+ } catch (err) {
34728
+ logger?.warn?.("[seedProjectOrganization] failed (non-fatal)", {
34729
+ orgId: seed.id,
34730
+ error: err?.message
34731
+ });
34732
+ return "error";
34733
+ }
34734
+ }
34735
+ async function seedProjectMember(kernel, args, logger) {
34736
+ const { userId, organizationId } = args;
34737
+ const role = args.role ?? "member";
34738
+ if (!userId || !organizationId) return "skipped";
34739
+ try {
34740
+ const ql = kernel.getService("objectql");
34741
+ if (!ql?.insert || !ql?.find) {
34742
+ logger?.warn?.("[seedProjectMember] objectql service unavailable", { userId, organizationId });
34743
+ return "skipped";
34744
+ }
34745
+ try {
34746
+ const existing = await ql.find("sys_member", {
34747
+ where: { user_id: userId, organization_id: organizationId }
34748
+ });
34749
+ const rows = Array.isArray(existing) ? existing : existing?.value ?? [];
34750
+ if (Array.isArray(rows) && rows.length > 0) return "exists";
34751
+ } catch {
34752
+ }
34753
+ const nowIso = (/* @__PURE__ */ new Date()).toISOString();
34754
+ const memId = `mem_${Math.random().toString(36).slice(2, 14)}`;
34755
+ await ql.insert("sys_member", {
34756
+ id: memId,
34757
+ organization_id: organizationId,
34758
+ user_id: userId,
34759
+ role,
34760
+ created_at: nowIso
34761
+ });
34762
+ logger?.info?.("[seedProjectMember] member seeded", {
34763
+ userId,
34764
+ organizationId,
34765
+ role
34766
+ });
34767
+ return "inserted";
34768
+ } catch (err) {
34769
+ logger?.warn?.("[seedProjectMember] failed (non-fatal)", {
34770
+ userId,
34771
+ organizationId,
34772
+ error: err?.message
34773
+ });
34774
+ return "error";
34775
+ }
34776
+ }
34777
+ var SYS_ORG;
34778
+ var init_project_org_seed = __esm({
34779
+ "src/cloud/project-org-seed.ts"() {
34780
+ "use strict";
34781
+ SYS_ORG = "sys_organization";
34782
+ }
34783
+ });
34784
+
34785
+ // src/cloud/project-owner-seed.ts
34786
+ var project_owner_seed_exports = {};
34787
+ __export(project_owner_seed_exports, {
34788
+ seedProjectOwner: () => seedProjectOwner
34789
+ });
34790
+ async function seedProjectOwner(kernel, seed, logger) {
34791
+ if (!seed?.userId || !seed?.email) return "skipped";
34792
+ try {
34793
+ const ql = kernel.getService("objectql");
34794
+ if (!ql?.insert || !ql?.find) {
34795
+ logger?.warn?.("[seedProjectOwner] objectql service unavailable", { userId: seed.userId });
34796
+ return "skipped";
34797
+ }
34798
+ try {
34799
+ const existing = await ql.find(SYS_USER, { where: { id: seed.userId } });
34800
+ const rows = Array.isArray(existing) ? existing : existing?.value ?? [];
34801
+ if (Array.isArray(rows) && rows.length > 0) return "exists";
34802
+ } catch {
34803
+ }
34804
+ const nowIso = (/* @__PURE__ */ new Date()).toISOString();
34805
+ await ql.insert(SYS_USER, {
34806
+ id: seed.userId,
34807
+ email: seed.email,
34808
+ name: seed.name ?? seed.email.split("@")[0] ?? "Owner",
34809
+ image: seed.image ?? null,
34810
+ // Cloud already verified the upstream email. Marking it verified
34811
+ // here is what unblocks better-auth's accountLinking check on
34812
+ // the first SSO callback (alongside the trustedProviders config
34813
+ // in plugin-auth/auth-manager.ts).
34814
+ email_verified: true,
34815
+ created_at: nowIso,
34816
+ updated_at: nowIso
34817
+ });
34818
+ logger?.info?.("[seedProjectOwner] owner seeded", {
34819
+ userId: seed.userId,
34820
+ email: seed.email
34821
+ });
34822
+ return "inserted";
34823
+ } catch (err) {
34824
+ logger?.warn?.("[seedProjectOwner] failed (non-fatal)", {
34825
+ userId: seed.userId,
34826
+ error: err?.message
34827
+ });
34828
+ return "error";
34829
+ }
34830
+ }
34831
+ var SYS_USER;
34832
+ var init_project_owner_seed = __esm({
34833
+ "src/cloud/project-owner-seed.ts"() {
34834
+ "use strict";
34835
+ SYS_USER = "sys_user";
34836
+ }
34837
+ });
34838
+
34371
34839
  // src/index.ts
34372
- import { ObjectKernel as ObjectKernel3 } from "@objectstack/core";
34840
+ import { ObjectKernel as ObjectKernel4 } from "@objectstack/core";
34373
34841
 
34374
34842
  // src/runtime.ts
34375
34843
  import { ObjectKernel } from "@objectstack/core";
@@ -34506,15 +34974,45 @@ async function createStandaloneStack(config) {
34506
34974
  new ObjectQLPlugin({ projectId })
34507
34975
  ];
34508
34976
  if (artifactBundle) plugins.push(new AppPlugin2(artifactBundle));
34977
+ const requires = Array.isArray(artifactBundle?.requires) ? artifactBundle.requires.filter((c) => typeof c === "string") : void 0;
34978
+ const objects = Array.isArray(artifactBundle?.objects) ? artifactBundle.objects : void 0;
34979
+ const manifest = artifactBundle?.manifest;
34509
34980
  return {
34510
34981
  plugins,
34511
34982
  api: {
34512
34983
  enableProjectScoping: false,
34513
34984
  projectResolution: "none"
34514
- }
34985
+ },
34986
+ ...requires ? { requires } : {},
34987
+ ...objects ? { objects } : {},
34988
+ ...manifest ? { manifest } : {}
34515
34989
  };
34516
34990
  }
34517
34991
 
34992
+ // src/default-host.ts
34993
+ import { resolve as resolvePath3 } from "path";
34994
+ import { existsSync } from "fs";
34995
+ init_load_artifact_bundle();
34996
+ function resolveDefaultArtifactPath(explicitPath, cwd = process.cwd()) {
34997
+ const candidate = explicitPath ?? process.env.OS_ARTIFACT_PATH ?? resolvePath3(cwd, "dist/objectstack.json");
34998
+ if (isHttpUrl(candidate)) return candidate;
34999
+ if (explicitPath || process.env.OS_ARTIFACT_PATH) return candidate;
35000
+ return existsSync(candidate) ? candidate : void 0;
35001
+ }
35002
+ async function createDefaultHostConfig(options = {}) {
35003
+ const { requireArtifact = true, ...standaloneOpts } = options;
35004
+ const resolvedArtifact = resolveDefaultArtifactPath(standaloneOpts.artifactPath);
35005
+ if (!resolvedArtifact && requireArtifact) {
35006
+ throw new Error(
35007
+ "[createDefaultHostConfig] No artifact source available. Set OS_ARTIFACT_PATH (file path or http(s):// URL), place the artifact at <cwd>/dist/objectstack.json, or pass `{ artifactPath: ... }` explicitly. To boot an empty kernel anyway, pass `{ requireArtifact: false }`."
35008
+ );
35009
+ }
35010
+ return createStandaloneStack({
35011
+ ...standaloneOpts,
35012
+ artifactPath: resolvedArtifact
35013
+ });
35014
+ }
35015
+
34518
35016
  // src/index.ts
34519
35017
  init_driver_plugin();
34520
35018
  init_app_plugin();
@@ -34869,7 +35367,7 @@ var _HttpDispatcher = class _HttpDispatcher {
34869
35367
  * so project-scoped meta routes can resolve their project).
34870
35368
  */
34871
35369
  async resolveEnvironmentContext(context, path) {
34872
- const skipPaths = ["/auth", "/cloud", "/health", "/discovery"];
35370
+ const skipPaths = ["/cloud", "/health", "/discovery"];
34873
35371
  if (skipPaths.some((p) => path.startsWith(p))) {
34874
35372
  return;
34875
35373
  }
@@ -34941,7 +35439,7 @@ var _HttpDispatcher = class _HttpDispatcher {
34941
35439
  const qlService = await this.getObjectQLService();
34942
35440
  const ql = qlService ?? await this.resolveService("objectql");
34943
35441
  if (ql) {
34944
- let rows = await ql.find("sys_project", {
35442
+ let rows = await ql.find("sys_environment", {
34945
35443
  where: {
34946
35444
  organization_id: activeOrganizationId,
34947
35445
  is_default: true
@@ -35028,8 +35526,8 @@ var _HttpDispatcher = class _HttpDispatcher {
35028
35526
  const qlService = await this.getObjectQLService();
35029
35527
  const ql = qlService ?? await this.resolveService("objectql");
35030
35528
  if (!ql) return null;
35031
- let rows = await ql.find("sys_project_member", {
35032
- where: { project_id: projectId, user_id: userId },
35529
+ let rows = await ql.find("sys_environment_member", {
35530
+ where: { environment_id: projectId, user_id: userId },
35033
35531
  limit: 1
35034
35532
  });
35035
35533
  if (rows && rows.value) rows = rows.value;
@@ -35284,8 +35782,9 @@ var _HttpDispatcher = class _HttpDispatcher {
35284
35782
  }
35285
35783
  return { handled: true, response: this.success({ types: ["object", "app", "plugin"] }) };
35286
35784
  }
35287
- if (parts.length === 3 && parts[2] === "published" && (!method || method === "GET")) {
35288
- const [type, name] = parts;
35785
+ if (parts.length >= 3 && parts[parts.length - 1] === "published" && (!method || method === "GET")) {
35786
+ const type = parts[0];
35787
+ const name = parts.slice(1, -1).join("/");
35289
35788
  const metadataService = await this.getService(CoreServiceName.enum.metadata);
35290
35789
  if (metadataService && typeof metadataService.getPublished === "function") {
35291
35790
  const data = await metadataService.getPublished(type, name);
@@ -35302,14 +35801,16 @@ var _HttpDispatcher = class _HttpDispatcher {
35302
35801
  }
35303
35802
  return { handled: true, response: this.error("Not found", 404) };
35304
35803
  }
35305
- if (parts.length === 2) {
35306
- const [type, name] = parts;
35804
+ if (parts.length >= 2) {
35805
+ const type = parts[0];
35806
+ const name = parts.slice(1).join("/");
35307
35807
  const packageId = query?.package || void 0;
35308
35808
  if (method === "PUT" && body) {
35309
35809
  const protocol = await this.resolveService("protocol");
35310
35810
  if (protocol && typeof protocol.saveMetaItem === "function") {
35311
35811
  try {
35312
- const result = await protocol.saveMetaItem({ type, name, item: body });
35812
+ const organizationId = await this.resolveActiveOrganizationId(_context);
35813
+ const result = await protocol.saveMetaItem({ type, name, item: body, organizationId });
35313
35814
  return { handled: true, response: this.success(result) };
35314
35815
  } catch (e) {
35315
35816
  return { handled: true, response: this.error(e.message, 400) };
@@ -35333,7 +35834,8 @@ var _HttpDispatcher = class _HttpDispatcher {
35333
35834
  const scoped = scopedEnv !== void 0;
35334
35835
  if (scoped && typeof protocol2.getMetaItem === "function") {
35335
35836
  try {
35336
- const data = await protocol2.getMetaItem({ type: "object", name });
35837
+ const organizationId = await this.resolveActiveOrganizationId(_context);
35838
+ const data = await protocol2.getMetaItem({ type: "object", name, organizationId });
35337
35839
  if (data && (data.item ?? data)) {
35338
35840
  return { handled: true, response: this.success(data) };
35339
35841
  }
@@ -35347,7 +35849,8 @@ var _HttpDispatcher = class _HttpDispatcher {
35347
35849
  }
35348
35850
  if (!scoped && protocol2 && typeof protocol2.getMetaItem === "function") {
35349
35851
  try {
35350
- const data = await protocol2.getMetaItem({ type: "object", name });
35852
+ const organizationId = await this.resolveActiveOrganizationId(_context);
35853
+ const data = await protocol2.getMetaItem({ type: "object", name, organizationId });
35351
35854
  if (data && (data.item ?? data)) {
35352
35855
  return { handled: true, response: this.success(data) };
35353
35856
  }
@@ -35360,7 +35863,8 @@ var _HttpDispatcher = class _HttpDispatcher {
35360
35863
  const protocol = await this.resolveService("protocol");
35361
35864
  if (protocol && typeof protocol.getMetaItem === "function") {
35362
35865
  try {
35363
- const data = await protocol.getMetaItem({ type: singularType, name, packageId });
35866
+ const organizationId = await this.resolveActiveOrganizationId(_context);
35867
+ const data = await protocol.getMetaItem({ type: singularType, name, packageId, organizationId });
35364
35868
  return { handled: true, response: this.success(data) };
35365
35869
  } catch (e) {
35366
35870
  }
@@ -35384,7 +35888,8 @@ var _HttpDispatcher = class _HttpDispatcher {
35384
35888
  const protocol = await this.resolveService("protocol");
35385
35889
  if (protocol && typeof protocol.getMetaItems === "function") {
35386
35890
  try {
35387
- const data = await protocol.getMetaItems({ type: typeOrName, packageId });
35891
+ const organizationId = await this.resolveActiveOrganizationId(_context);
35892
+ const data = await protocol.getMetaItems({ type: typeOrName, packageId, organizationId });
35388
35893
  if (data && (data.items !== void 0 || Array.isArray(data))) {
35389
35894
  return { handled: true, response: this.success(data) };
35390
35895
  }
@@ -35723,6 +36228,61 @@ var _HttpDispatcher = class _HttpDispatcher {
35723
36228
  * Physical database addressing (database_url, database_driver, etc.)
35724
36229
  * is stored directly on the sys_project row.
35725
36230
  */
36231
+ /**
36232
+ * Resolve the calling user id from the request session, if any.
36233
+ * Returns `undefined` for anonymous calls or when auth is not wired up.
36234
+ */
36235
+ async resolveActiveOrganizationId(context) {
36236
+ try {
36237
+ const authService = await this.resolveService(CoreServiceName.enum.auth);
36238
+ const rawHeaders = context.request?.headers;
36239
+ let headers = rawHeaders;
36240
+ if (rawHeaders && typeof rawHeaders === "object" && typeof rawHeaders.get !== "function") {
36241
+ try {
36242
+ const h = new Headers();
36243
+ for (const [k, v] of Object.entries(rawHeaders)) {
36244
+ if (v == null) continue;
36245
+ h.set(k, Array.isArray(v) ? v.join(", ") : String(v));
36246
+ }
36247
+ headers = h;
36248
+ } catch {
36249
+ headers = rawHeaders;
36250
+ }
36251
+ }
36252
+ const apiObj = authService?.auth?.api ?? authService?.api;
36253
+ const sessionData = await apiObj?.getSession?.call(apiObj, { headers });
36254
+ const oid = sessionData?.session?.activeOrganizationId;
36255
+ return typeof oid === "string" && oid.length > 0 ? oid : void 0;
36256
+ } catch {
36257
+ return void 0;
36258
+ }
36259
+ }
36260
+ async resolveCallerUserId(context) {
36261
+ try {
36262
+ const authService = await this.resolveService(CoreServiceName.enum.auth);
36263
+ const rawHeaders = context.request?.headers;
36264
+ let headers = rawHeaders;
36265
+ if (rawHeaders && typeof rawHeaders === "object" && typeof rawHeaders.get !== "function") {
36266
+ try {
36267
+ const h = new Headers();
36268
+ for (const [k, v] of Object.entries(rawHeaders)) {
36269
+ if (v == null) continue;
36270
+ h.set(k, Array.isArray(v) ? v.join(", ") : String(v));
36271
+ }
36272
+ headers = h;
36273
+ } catch {
36274
+ headers = rawHeaders;
36275
+ }
36276
+ }
36277
+ const sessionData = await (authService?.auth?.api?.getSession ?? authService?.api?.getSession)?.call(
36278
+ authService?.auth?.api ?? authService?.api,
36279
+ { headers }
36280
+ );
36281
+ return sessionData?.user?.id ?? sessionData?.session?.userId;
36282
+ } catch (e) {
36283
+ return void 0;
36284
+ }
36285
+ }
35726
36286
  async handleCloud(path, method, body, query, _context) {
35727
36287
  const m = method.toUpperCase();
35728
36288
  const parts = path.replace(/^\/+/, "").split("/").filter(Boolean);
@@ -35731,9 +36291,9 @@ var _HttpDispatcher = class _HttpDispatcher {
35731
36291
  if (!ql) {
35732
36292
  return { handled: true, response: this.error("Project service not available (ObjectQL missing)", 503) };
35733
36293
  }
35734
- const ENV = "sys_project";
35735
- const CRED = "sys_project_credential";
35736
- const MEM = "sys_project_member";
36294
+ const ENV = "sys_environment";
36295
+ const CRED = "sys_environment_credential";
36296
+ const MEM = "sys_environment_member";
35737
36297
  const PKG_INSTALL = "sys_package_installation";
35738
36298
  const PKG = "sys_package";
35739
36299
  const PKG_VERSION = "sys_package_version";
@@ -35781,7 +36341,7 @@ var _HttpDispatcher = class _HttpDispatcher {
35781
36341
  const pkgRow = await ql.findOne(PKG, { where: { manifest_id: manifestId } });
35782
36342
  if (!pkgRow?.id) return null;
35783
36343
  return await ql.findOne(PKG_INSTALL, {
35784
- where: { project_id: envId, package_id: pkgRow.id }
36344
+ where: { environment_id: envId, package_id: pkgRow.id }
35785
36345
  });
35786
36346
  };
35787
36347
  const toShortName = (driverId) => {
@@ -35880,6 +36440,44 @@ var _HttpDispatcher = class _HttpDispatcher {
35880
36440
  return { handled: true, response: this.success({ templates: [], total: 0 }) };
35881
36441
  }
35882
36442
  }
36443
+ if (parts.length === 3 && parts[0] === "admin" && parts[1] === "platform-sso" && parts[2] === "backfill" && m === "POST") {
36444
+ const baseSecret = (process.env.OS_AUTH_SECRET ?? process.env.AUTH_SECRET ?? "").trim();
36445
+ if (!baseSecret) {
36446
+ return { handled: true, response: this.error("OS_AUTH_SECRET not configured on this worker", 503) };
36447
+ }
36448
+ const rawHeaders = _context?.request?.headers;
36449
+ let authHeader;
36450
+ if (rawHeaders && typeof rawHeaders.get === "function") {
36451
+ authHeader = rawHeaders.get("authorization") ?? void 0;
36452
+ } else if (rawHeaders && typeof rawHeaders === "object") {
36453
+ authHeader = rawHeaders["authorization"] ?? rawHeaders["Authorization"];
36454
+ }
36455
+ const presented = typeof authHeader === "string" && authHeader.startsWith("Bearer ") ? authHeader.slice(7).trim() : "";
36456
+ if (!presented || presented !== baseSecret) {
36457
+ return { handled: true, response: this.error("forbidden: Bearer token must match OS_AUTH_SECRET", 403) };
36458
+ }
36459
+ try {
36460
+ const { backfillPlatformSsoClients: backfillPlatformSsoClients2 } = await Promise.resolve().then(() => (init_platform_sso(), platform_sso_exports));
36461
+ const result = await backfillPlatformSsoClients2({
36462
+ ql,
36463
+ baseSecret,
36464
+ logger: console
36465
+ });
36466
+ let sample = [];
36467
+ let total = 0;
36468
+ try {
36469
+ const rows = await ql.find("sys_oauth_application", { limit: 5 }, { context: { isSystem: true } });
36470
+ const list = Array.isArray(rows) ? rows : Array.isArray(rows?.records) ? rows.records : [];
36471
+ sample = list;
36472
+ total = typeof rows?.total === "number" ? rows.total : list.length;
36473
+ } catch (e) {
36474
+ sample = [{ _readErr: e?.message ?? String(e) }];
36475
+ }
36476
+ return { handled: true, response: this.success({ ...result, total, sample }) };
36477
+ } catch (err) {
36478
+ return { handled: true, response: this.error(`backfill failed: ${err?.message ?? String(err)}`, 500) };
36479
+ }
36480
+ }
35883
36481
  if (parts.length === 1 && parts[0] === "projects" && m === "GET") {
35884
36482
  const where = {};
35885
36483
  if (query?.organizationId) where.organization_id = query.organizationId;
@@ -35893,16 +36491,26 @@ var _HttpDispatcher = class _HttpDispatcher {
35893
36491
  const req = body || {};
35894
36492
  if (req.organization_id === "__session__" || req.created_by === "__session__") {
35895
36493
  try {
35896
- const authService = await this.getService(CoreServiceName.enum.auth);
35897
- const sessionData = await authService?.api?.getSession?.({
35898
- headers: _context?.request?.headers
35899
- });
36494
+ const userId = await this.resolveCallerUserId(_context);
36495
+ if (req.created_by === "__session__") {
36496
+ req.created_by = userId ?? "system";
36497
+ }
35900
36498
  if (req.organization_id === "__session__") {
36499
+ const authService = await this.resolveService(CoreServiceName.enum.auth);
36500
+ const rawHeaders = _context?.request?.headers;
36501
+ let headers = rawHeaders;
36502
+ if (rawHeaders && typeof rawHeaders === "object" && typeof rawHeaders.get !== "function") {
36503
+ const h = new Headers();
36504
+ for (const [k, v] of Object.entries(rawHeaders)) {
36505
+ if (v == null) continue;
36506
+ h.set(k, Array.isArray(v) ? v.join(", ") : String(v));
36507
+ }
36508
+ headers = h;
36509
+ }
36510
+ const apiObj = authService?.auth?.api ?? authService?.api;
36511
+ const sessionData = await apiObj?.getSession?.call(apiObj, { headers });
35901
36512
  req.organization_id = sessionData?.session?.activeOrganizationId ?? void 0;
35902
36513
  }
35903
- if (req.created_by === "__session__") {
35904
- req.created_by = sessionData?.user?.id ?? "system";
35905
- }
35906
36514
  } catch {
35907
36515
  }
35908
36516
  }
@@ -35940,14 +36548,14 @@ var _HttpDispatcher = class _HttpDispatcher {
35940
36548
  try {
35941
36549
  const orgRow = await findOne("sys_organization", { id: req.organization_id });
35942
36550
  const orgSlug = orgRow?.slug || req.organization_id;
35943
- const rootDomain = getEnv("ROOT_DOMAIN", "objectstack.app");
36551
+ const rootDomain = getEnv("OS_ROOT_DOMAIN") ?? getEnv("ROOT_DOMAIN", "objectstack.app");
35944
36552
  computedHostname = `${orgSlug}-${shortId}.${rootDomain}`;
35945
36553
  } catch {
35946
36554
  computedHostname = `${req.organization_id}-${shortId}.objectstack.app`;
35947
36555
  }
35948
36556
  }
35949
36557
  try {
35950
- const existing = await findOne("sys_project", {
36558
+ const existing = await findOne("sys_environment", {
35951
36559
  hostname: computedHostname
35952
36560
  });
35953
36561
  if (existing && existing.id !== projectId) {
@@ -35965,6 +36573,40 @@ var _HttpDispatcher = class _HttpDispatcher {
35965
36573
  const baseMetadata = { ...req.metadata ?? {} };
35966
36574
  const simulateFailure = Boolean(baseMetadata.__simulateFailure);
35967
36575
  const simulateDelayMs = Number(baseMetadata.__simulateDelayMs ?? 1500);
36576
+ try {
36577
+ let ownerUserId = req.created_by && req.created_by !== "system" ? String(req.created_by) : void 0;
36578
+ if (!ownerUserId) {
36579
+ ownerUserId = await this.resolveCallerUserId(_context);
36580
+ }
36581
+ if (ownerUserId) {
36582
+ const userRow = await ql.find("sys_user", { where: { id: ownerUserId } });
36583
+ const userRows = Array.isArray(userRow) ? userRow : userRow?.value ?? [];
36584
+ const u = Array.isArray(userRows) && userRows.length > 0 ? userRows[0] : null;
36585
+ if (u?.email) {
36586
+ baseMetadata.ownerSeed = {
36587
+ userId: String(ownerUserId),
36588
+ email: String(u.email),
36589
+ name: u.name ? String(u.name) : null,
36590
+ image: u.image ? String(u.image) : null
36591
+ };
36592
+ }
36593
+ }
36594
+ } catch {
36595
+ }
36596
+ try {
36597
+ const orgRow = await ql.find("sys_organization", { where: { id: req.organization_id } });
36598
+ const orgRows = Array.isArray(orgRow) ? orgRow : orgRow?.value ?? [];
36599
+ const org = Array.isArray(orgRows) && orgRows.length > 0 ? orgRows[0] : null;
36600
+ if (org?.id && org?.name) {
36601
+ baseMetadata.orgSeed = {
36602
+ id: String(org.id),
36603
+ name: String(org.name),
36604
+ slug: org.slug ? String(org.slug) : null,
36605
+ logo: org.logo ? String(org.logo) : null
36606
+ };
36607
+ }
36608
+ } catch {
36609
+ }
35968
36610
  await ql.insert(ENV, {
35969
36611
  id: projectId,
35970
36612
  organization_id: req.organization_id,
@@ -35981,8 +36623,30 @@ var _HttpDispatcher = class _HttpDispatcher {
35981
36623
  database_driver: driver,
35982
36624
  storage_limit_mb: req.storage_limit_mb ?? 1024,
35983
36625
  provisioned_at: null,
35984
- hostname: computedHostname
36626
+ hostname: computedHostname,
36627
+ visibility: (() => {
36628
+ const raw = String(req.visibility ?? "private");
36629
+ return raw === "unlisted" ? "private" : raw;
36630
+ })()
35985
36631
  });
36632
+ try {
36633
+ const { seedPlatformSsoClient: seedPlatformSsoClient2 } = await Promise.resolve().then(() => (init_platform_sso(), platform_sso_exports));
36634
+ const baseSecret = (process.env.OS_AUTH_SECRET ?? process.env.AUTH_SECRET ?? "").trim();
36635
+ if (baseSecret) {
36636
+ await seedPlatformSsoClient2({
36637
+ ql,
36638
+ projectId,
36639
+ hostname: computedHostname,
36640
+ baseSecret,
36641
+ logger: console
36642
+ });
36643
+ }
36644
+ } catch (ssoErr) {
36645
+ console.warn?.("[http-dispatcher] platform SSO seed failed (non-fatal)", {
36646
+ projectId,
36647
+ error: ssoErr?.message
36648
+ });
36649
+ }
35986
36650
  const runProvisioning = async () => {
35987
36651
  try {
35988
36652
  if (simulateDelayMs > 0) {
@@ -36020,7 +36684,7 @@ var _HttpDispatcher = class _HttpDispatcher {
36020
36684
  );
36021
36685
  await ql.insert(CRED, {
36022
36686
  id: credentialId,
36023
- project_id: projectId,
36687
+ environment_id: projectId,
36024
36688
  secret_ciphertext: plaintextSecret,
36025
36689
  encryption_key_id: "noop",
36026
36690
  authorization: "full_access",
@@ -36135,8 +36799,9 @@ var _HttpDispatcher = class _HttpDispatcher {
36135
36799
  if (m === "GET") {
36136
36800
  const envRow = await findOne(ENV, { id });
36137
36801
  if (!envRow) return { handled: true, response: this.error(`Project '${id}' not found`, 404) };
36138
- const credRow = await findOne(CRED, { project_id: id, status: "active" });
36139
- const membership = await findOne(MEM, { project_id: id });
36802
+ const credRow = await findOne(CRED, { environment_id: id, status: "active" });
36803
+ const callerUserId = await this.resolveCallerUserId(_context);
36804
+ const membership = callerUserId ? await findOne(MEM, { environment_id: id, user_id: callerUserId }) : await findOne(MEM, { environment_id: id });
36140
36805
  const credMeta = credRow ? {
36141
36806
  id: credRow.id,
36142
36807
  status: credRow.status,
@@ -36163,6 +36828,14 @@ var _HttpDispatcher = class _HttpDispatcher {
36163
36828
  if (body?.plan !== void 0) patch.plan = body.plan;
36164
36829
  if (body?.status !== void 0) patch.status = body.status;
36165
36830
  if (body?.is_default !== void 0) patch.is_default = body.is_default;
36831
+ if (body?.visibility !== void 0) {
36832
+ let v = String(body.visibility);
36833
+ if (v === "unlisted") v = "private";
36834
+ if (!["private", "public"].includes(v)) {
36835
+ return { handled: true, response: this.error(`Invalid visibility '${v}' (expected private | public)`, 400) };
36836
+ }
36837
+ patch.visibility = v;
36838
+ }
36166
36839
  if (body?.metadata !== void 0) patch.metadata = JSON.stringify(body.metadata);
36167
36840
  patch.updated_at = (/* @__PURE__ */ new Date()).toISOString();
36168
36841
  await ql.update(ENV, patch, { where: { id } });
@@ -36369,11 +37042,11 @@ var _HttpDispatcher = class _HttpDispatcher {
36369
37042
  },
36370
37043
  { where: { id } }
36371
37044
  );
36372
- const existingCred = await findOne(CRED, { project_id: id, status: "active" });
37045
+ const existingCred = await findOne(CRED, { environment_id: id, status: "active" });
36373
37046
  if (!existingCred) {
36374
37047
  await ql.insert(CRED, {
36375
37048
  id: randomUUID(),
36376
- project_id: id,
37049
+ environment_id: id,
36377
37050
  secret_ciphertext: retrySecret,
36378
37051
  encryption_key_id: "noop",
36379
37052
  authorization: "full_access",
@@ -36420,7 +37093,7 @@ var _HttpDispatcher = class _HttpDispatcher {
36420
37093
  const envRow = await findOne(ENV, { id });
36421
37094
  if (!envRow) return { handled: true, response: this.error(`Project '${id}' not found`, 404) };
36422
37095
  const nowIso = (/* @__PURE__ */ new Date()).toISOString();
36423
- let existing = await ql.find(CRED, { where: { project_id: id, status: "active" } });
37096
+ let existing = await ql.find(CRED, { where: { environment_id: id, status: "active" } });
36424
37097
  if (existing && existing.value) existing = existing.value;
36425
37098
  for (const row of Array.isArray(existing) ? existing : []) {
36426
37099
  await ql.update(CRED, {
@@ -36432,7 +37105,7 @@ var _HttpDispatcher = class _HttpDispatcher {
36432
37105
  const credentialId = randomUUID();
36433
37106
  await ql.insert(CRED, {
36434
37107
  id: credentialId,
36435
- project_id: id,
37108
+ environment_id: id,
36436
37109
  secret_ciphertext: plaintext,
36437
37110
  encryption_key_id: "noop",
36438
37111
  authorization: "full_access",
@@ -36451,14 +37124,154 @@ var _HttpDispatcher = class _HttpDispatcher {
36451
37124
  }
36452
37125
  if (parts.length === 3 && parts[0] === "projects" && parts[2] === "members" && m === "GET") {
36453
37126
  const id = decodeURIComponent(parts[1]);
36454
- let rows = await ql.find(MEM, { where: { project_id: id } });
37127
+ let rows = await ql.find(MEM, { where: { environment_id: id } });
36455
37128
  if (rows && rows.value) rows = rows.value;
36456
37129
  const members = Array.isArray(rows) ? rows : [];
36457
- return { handled: true, response: this.success({ members }) };
37130
+ const userIds = Array.from(new Set(members.map((mem) => mem.user_id).filter(Boolean)));
37131
+ const userMap = /* @__PURE__ */ new Map();
37132
+ for (const uid of userIds) {
37133
+ let row = null;
37134
+ for (const tableName of ["sys_user", "user"]) {
37135
+ try {
37136
+ const u = await ql.findOne(tableName, { where: { id: uid } });
37137
+ row = u?.value ?? u;
37138
+ if (row) break;
37139
+ } catch {
37140
+ }
37141
+ }
37142
+ if (row) userMap.set(String(uid), {
37143
+ id: row.id,
37144
+ name: row.name ?? row.display_name,
37145
+ email: row.email,
37146
+ image: row.image ?? row.avatar_url
37147
+ });
37148
+ }
37149
+ const enriched = members.map((mem) => ({
37150
+ ...mem,
37151
+ user: userMap.get(String(mem.user_id)) ?? void 0
37152
+ }));
37153
+ return { handled: true, response: this.success({ members: enriched }) };
37154
+ }
37155
+ if (parts.length === 3 && parts[0] === "projects" && parts[2] === "members" && m === "POST") {
37156
+ const id = decodeURIComponent(parts[1]);
37157
+ const project = await findOne(ENV, { id });
37158
+ if (!project) return { handled: true, response: this.error(`Project '${id}' not found`, 404) };
37159
+ const callerId = await this.resolveCallerUserId(_context);
37160
+ if (!callerId) return { handled: true, response: this.error("Authentication required", 401) };
37161
+ const callerMem = await findOne(MEM, { environment_id: id, user_id: callerId });
37162
+ if (!callerMem || !["owner", "admin"].includes(String(callerMem.role))) {
37163
+ return { handled: true, response: this.error("Forbidden \u2014 owner or admin required", 403) };
37164
+ }
37165
+ const email = typeof body?.email === "string" ? String(body.email).trim().toLowerCase() : null;
37166
+ let inviteUserId = typeof body?.user_id === "string" ? String(body.user_id).trim() : null;
37167
+ let role = String(body?.role ?? "member").trim().toLowerCase();
37168
+ if (!["owner", "admin", "member", "viewer"].includes(role)) {
37169
+ return { handled: true, response: this.error(`Invalid role '${role}' (expected owner | admin | member | viewer)`, 400) };
37170
+ }
37171
+ if (!email && !inviteUserId) {
37172
+ return { handled: true, response: this.error("email or user_id is required", 400) };
37173
+ }
37174
+ if (!inviteUserId && email) {
37175
+ let row = null;
37176
+ for (const tableName of ["sys_user", "user"]) {
37177
+ try {
37178
+ const u = await ql.findOne(tableName, { where: { email } });
37179
+ row = u?.value ?? u;
37180
+ if (row) break;
37181
+ } catch {
37182
+ }
37183
+ }
37184
+ if (!row?.id) {
37185
+ return { handled: true, response: this.error(`No user found with email '${email}'`, 404) };
37186
+ }
37187
+ inviteUserId = String(row.id);
37188
+ }
37189
+ const existing = await findOne(MEM, { environment_id: id, user_id: inviteUserId });
37190
+ if (existing) {
37191
+ return { handled: true, response: this.success({ member: existing, alreadyMember: true }) };
37192
+ }
37193
+ try {
37194
+ const memberId = randomUUID();
37195
+ await ql.insert(MEM, {
37196
+ id: memberId,
37197
+ environment_id: id,
37198
+ user_id: inviteUserId,
37199
+ role,
37200
+ invited_by: callerId,
37201
+ organization_id: project.organization_id ?? null
37202
+ });
37203
+ const created = await findOne(MEM, { id: memberId });
37204
+ return { handled: true, response: this.success({ member: created, alreadyMember: false }) };
37205
+ } catch (e) {
37206
+ return { handled: true, response: this.error(e?.message ?? "Failed to add member", 500) };
37207
+ }
37208
+ }
37209
+ if (parts.length === 4 && parts[0] === "projects" && parts[2] === "members" && m === "PATCH") {
37210
+ const id = decodeURIComponent(parts[1]);
37211
+ const memberId = decodeURIComponent(parts[3]);
37212
+ const project = await findOne(ENV, { id });
37213
+ if (!project) return { handled: true, response: this.error(`Project '${id}' not found`, 404) };
37214
+ const callerId = await this.resolveCallerUserId(_context);
37215
+ if (!callerId) return { handled: true, response: this.error("Authentication required", 401) };
37216
+ const callerMem = await findOne(MEM, { environment_id: id, user_id: callerId });
37217
+ if (!callerMem || !["owner", "admin"].includes(String(callerMem.role))) {
37218
+ return { handled: true, response: this.error("Forbidden \u2014 owner or admin required", 403) };
37219
+ }
37220
+ const target = await findOne(MEM, { id: memberId, environment_id: id });
37221
+ if (!target) return { handled: true, response: this.error(`Member '${memberId}' not found`, 404) };
37222
+ const newRole = String(body?.role ?? "").trim().toLowerCase();
37223
+ if (!["owner", "admin", "member", "viewer"].includes(newRole)) {
37224
+ return { handled: true, response: this.error(`Invalid role '${newRole}'`, 400) };
37225
+ }
37226
+ if (target.role === "owner" && newRole !== "owner") {
37227
+ let owners = await ql.find(MEM, { where: { environment_id: id, role: "owner" } });
37228
+ if (owners && owners.value) owners = owners.value;
37229
+ const ownerCount = Array.isArray(owners) ? owners.length : 0;
37230
+ if (ownerCount <= 1) {
37231
+ return { handled: true, response: this.error("Cannot demote the last owner", 409) };
37232
+ }
37233
+ }
37234
+ try {
37235
+ await ql.update(MEM, { role: newRole, updated_at: (/* @__PURE__ */ new Date()).toISOString() }, { where: { id: memberId } });
37236
+ const updated = await findOne(MEM, { id: memberId });
37237
+ return { handled: true, response: this.success({ member: updated }) };
37238
+ } catch (e) {
37239
+ return { handled: true, response: this.error(e?.message ?? "Failed to update role", 500) };
37240
+ }
37241
+ }
37242
+ if (parts.length === 4 && parts[0] === "projects" && parts[2] === "members" && m === "DELETE") {
37243
+ const id = decodeURIComponent(parts[1]);
37244
+ const memberId = decodeURIComponent(parts[3]);
37245
+ const project = await findOne(ENV, { id });
37246
+ if (!project) return { handled: true, response: this.error(`Project '${id}' not found`, 404) };
37247
+ const callerId = await this.resolveCallerUserId(_context);
37248
+ if (!callerId) return { handled: true, response: this.error("Authentication required", 401) };
37249
+ const target = await findOne(MEM, { id: memberId, environment_id: id });
37250
+ if (!target) return { handled: true, response: this.error(`Member '${memberId}' not found`, 404) };
37251
+ const callerMem = await findOne(MEM, { environment_id: id, user_id: callerId });
37252
+ const isSelf = String(target.user_id) === String(callerId);
37253
+ const isPrivileged = callerMem && ["owner", "admin"].includes(String(callerMem.role));
37254
+ if (!isSelf && !isPrivileged) {
37255
+ return { handled: true, response: this.error("Forbidden \u2014 owner or admin required", 403) };
37256
+ }
37257
+ if (target.role === "owner") {
37258
+ let owners = await ql.find(MEM, { where: { environment_id: id, role: "owner" } });
37259
+ if (owners && owners.value) owners = owners.value;
37260
+ const ownerCount = Array.isArray(owners) ? owners.length : 0;
37261
+ if (ownerCount <= 1) {
37262
+ return { handled: true, response: this.error("Cannot remove the last owner", 409) };
37263
+ }
37264
+ }
37265
+ try {
37266
+ await ql.delete(MEM, { where: { id: memberId } });
37267
+ return { handled: true, response: this.success({ removed: true, memberId }) };
37268
+ } catch (e) {
37269
+ return { handled: true, response: this.error(e?.message ?? "Failed to remove member", 500) };
37270
+ }
36458
37271
  }
36459
37272
  if (parts.length === 3 && parts[0] === "projects" && parts[2] === "packages" && m === "GET") {
36460
37273
  const envId = decodeURIComponent(parts[1]);
36461
- let rows = await ql.find(PKG_INSTALL, { where: { project_id: envId } });
37274
+ let rows = await ql.find(PKG_INSTALL, { where: { environment_id: envId } });
36462
37275
  if (rows && rows.value) rows = rows.value;
36463
37276
  const installs = Array.isArray(rows) ? rows : [];
36464
37277
  const packages = await Promise.all(
@@ -36519,7 +37332,7 @@ var _HttpDispatcher = class _HttpDispatcher {
36519
37332
  }
36520
37333
  const resolvedVersion = version ?? manifest?.version ?? "1.0.0";
36521
37334
  const dup = await ql.findOne(PKG_INSTALL, {
36522
- where: { project_id: envId, package_id: packageId }
37335
+ where: { environment_id: envId, package_id: packageId }
36523
37336
  });
36524
37337
  if (dup?.id) {
36525
37338
  return { handled: true, response: this.error(`Package '${packageId}' is already installed in this project`, 409) };
@@ -36530,7 +37343,7 @@ var _HttpDispatcher = class _HttpDispatcher {
36530
37343
  const recordId = randomUUID();
36531
37344
  await ql.insert(PKG_INSTALL, {
36532
37345
  id: recordId,
36533
- project_id: envId,
37346
+ environment_id: envId,
36534
37347
  package_id: sysPackageId,
36535
37348
  package_version_id: sysPackageVersionId,
36536
37349
  status: "installed",
@@ -36550,7 +37363,7 @@ var _HttpDispatcher = class _HttpDispatcher {
36550
37363
  if (parts.length === 4 && parts[0] === "projects" && parts[2] === "packages" && m === "GET") {
36551
37364
  const envId = decodeURIComponent(parts[1]);
36552
37365
  const pkgId = decodeURIComponent(parts[3]);
36553
- const record = await ql.findOne(PKG_INSTALL, { where: { project_id: envId, package_id: pkgId } });
37366
+ const record = await ql.findOne(PKG_INSTALL, { where: { environment_id: envId, package_id: pkgId } });
36554
37367
  if (!record) return { handled: true, response: this.error(`Package '${pkgId}' is not installed in this project`, 404) };
36555
37368
  return { handled: true, response: this.success({ package: record }) };
36556
37369
  }
@@ -36657,7 +37470,7 @@ var _HttpDispatcher = class _HttpDispatcher {
36657
37470
  */
36658
37471
  async deleteProjectCascade(projectId, deps) {
36659
37472
  const { ql, findOne, getRealAdapter, force } = deps;
36660
- const ENV = "sys_project";
37473
+ const ENV = "sys_environment";
36661
37474
  const warnings = [];
36662
37475
  const row = await findOne(ENV, { id: projectId });
36663
37476
  if (!row) {
@@ -36675,9 +37488,9 @@ var _HttpDispatcher = class _HttpDispatcher {
36675
37488
  };
36676
37489
  }
36677
37490
  const cascade = [
36678
- { object: "sys_project_credential", field: "project_id" },
36679
- { object: "sys_project_member", field: "project_id" },
36680
- { object: "sys_package_installation", field: "project_id" }
37491
+ { object: "sys_environment_credential", field: "environment_id" },
37492
+ { object: "sys_environment_member", field: "environment_id" },
37493
+ { object: "sys_package_installation", field: "environment_id" }
36681
37494
  ];
36682
37495
  for (const { object, field } of cascade) {
36683
37496
  try {
@@ -37353,8 +38166,316 @@ _HttpDispatcher.SYSTEM_PROJECT_ID = "00000000-0000-0000-0000-000000000001";
37353
38166
  _HttpDispatcher.PLATFORM_ORG_ID = "00000000-0000-0000-0000-000000000000";
37354
38167
  var HttpDispatcher = _HttpDispatcher;
37355
38168
 
38169
+ // src/security/security-headers.ts
38170
+ function buildSecurityHeaders(opts = {}) {
38171
+ const h = {};
38172
+ if (opts.contentSecurityPolicy !== false) {
38173
+ h["Content-Security-Policy"] = opts.contentSecurityPolicy ?? "default-src 'none'; frame-ancestors 'none'";
38174
+ }
38175
+ if (opts.hsts) {
38176
+ const cfg = typeof opts.hsts === "object" ? opts.hsts : {};
38177
+ const maxAge = cfg.maxAge ?? 15552e3;
38178
+ const parts = [`max-age=${maxAge}`];
38179
+ if (cfg.includeSubDomains ?? true) parts.push("includeSubDomains");
38180
+ if (cfg.preload) parts.push("preload");
38181
+ h["Strict-Transport-Security"] = parts.join("; ");
38182
+ }
38183
+ h["X-Content-Type-Options"] = "nosniff";
38184
+ if (opts.frameOptions !== false) {
38185
+ h["X-Frame-Options"] = opts.frameOptions ?? "DENY";
38186
+ }
38187
+ if (opts.referrerPolicy !== false) {
38188
+ h["Referrer-Policy"] = opts.referrerPolicy ?? "no-referrer";
38189
+ }
38190
+ if (opts.permissionsPolicy !== false) {
38191
+ h["Permissions-Policy"] = opts.permissionsPolicy ?? "geolocation=(), camera=(), microphone=(), payment=()";
38192
+ }
38193
+ if (opts.corp !== false) {
38194
+ h["Cross-Origin-Resource-Policy"] = opts.corp ?? "same-origin";
38195
+ }
38196
+ if (opts.extra) {
38197
+ Object.assign(h, opts.extra);
38198
+ }
38199
+ return h;
38200
+ }
38201
+
38202
+ // src/security/rate-limit.ts
38203
+ var MemoryStore = class {
38204
+ constructor(maxEntries = 1e5) {
38205
+ this.buckets = /* @__PURE__ */ new Map();
38206
+ this.maxEntries = maxEntries;
38207
+ }
38208
+ get(key) {
38209
+ return this.buckets.get(key);
38210
+ }
38211
+ set(key, state) {
38212
+ if (this.buckets.size >= this.maxEntries) {
38213
+ const dropCount = Math.max(1, Math.floor(this.maxEntries / 10));
38214
+ const iter = this.buckets.keys();
38215
+ for (let i = 0; i < dropCount; i++) {
38216
+ const k = iter.next().value;
38217
+ if (!k) break;
38218
+ this.buckets.delete(k);
38219
+ }
38220
+ }
38221
+ this.buckets.set(key, state);
38222
+ }
38223
+ prune(olderThanMs) {
38224
+ const cutoff = Date.now() - olderThanMs;
38225
+ for (const [k, v] of this.buckets) {
38226
+ if (v.lastRefill < cutoff) this.buckets.delete(k);
38227
+ }
38228
+ }
38229
+ };
38230
+ var RateLimiter = class {
38231
+ constructor(config, opts = {}) {
38232
+ if (config.capacity <= 0) throw new Error("RateLimiter: capacity must be > 0");
38233
+ if (config.refillPerSec <= 0) throw new Error("RateLimiter: refillPerSec must be > 0");
38234
+ this.config = config;
38235
+ this.store = opts.store ?? new MemoryStore();
38236
+ this.now = opts.now ?? Date.now;
38237
+ }
38238
+ /**
38239
+ * Attempt to consume `cost` tokens for `key`. Returns a decision
38240
+ * describing whether the request should proceed and, if not, how
38241
+ * long the caller should wait before retrying.
38242
+ */
38243
+ consume(key, cost = this.config.defaultCost ?? 1) {
38244
+ const now = this.now();
38245
+ const { capacity, refillPerSec } = this.config;
38246
+ let state = this.store.get(key);
38247
+ if (!state) {
38248
+ state = { tokens: capacity, lastRefill: now };
38249
+ } else {
38250
+ const elapsedSec = (now - state.lastRefill) / 1e3;
38251
+ if (elapsedSec > 0) {
38252
+ state = {
38253
+ tokens: Math.min(capacity, state.tokens + elapsedSec * refillPerSec),
38254
+ lastRefill: now
38255
+ };
38256
+ }
38257
+ }
38258
+ if (state.tokens >= cost) {
38259
+ state.tokens -= cost;
38260
+ this.store.set(key, state);
38261
+ return {
38262
+ allowed: true,
38263
+ remaining: Math.floor(state.tokens),
38264
+ retryAfterMs: 0,
38265
+ resetAt: now + Math.ceil((capacity - state.tokens) / refillPerSec * 1e3)
38266
+ };
38267
+ }
38268
+ const tokensNeeded = cost - state.tokens;
38269
+ const retryAfterMs = Math.ceil(tokensNeeded / refillPerSec * 1e3);
38270
+ this.store.set(key, state);
38271
+ return {
38272
+ allowed: false,
38273
+ remaining: Math.floor(state.tokens),
38274
+ retryAfterMs,
38275
+ resetAt: now + retryAfterMs
38276
+ };
38277
+ }
38278
+ /** Force-reset a key (e.g. after a successful auth flow). */
38279
+ reset(key) {
38280
+ this.store.set(key, { tokens: this.config.capacity, lastRefill: this.now() });
38281
+ }
38282
+ };
38283
+ var DEFAULT_RATE_LIMITS = {
38284
+ auth: { capacity: 10, refillPerSec: 10 / 60 },
38285
+ write: { capacity: 60, refillPerSec: 60 / 60 },
38286
+ read: { capacity: 600, refillPerSec: 600 / 60 }
38287
+ };
38288
+
38289
+ // src/observability/request-context.ts
38290
+ var MAX_REQUEST_ID_LENGTH = 200;
38291
+ var REQUEST_ID_PATTERN = /^[A-Za-z0-9._:-]+$/;
38292
+ function extractRequestId(headers) {
38293
+ if (!headers || typeof headers !== "object") return void 0;
38294
+ for (const [k, v] of Object.entries(headers)) {
38295
+ if (k.toLowerCase() !== "x-request-id") continue;
38296
+ const raw = Array.isArray(v) ? v[0] : v;
38297
+ if (typeof raw !== "string") return void 0;
38298
+ const trimmed = raw.trim();
38299
+ if (!trimmed || trimmed.length > MAX_REQUEST_ID_LENGTH) return void 0;
38300
+ if (!REQUEST_ID_PATTERN.test(trimmed)) return void 0;
38301
+ return trimmed;
38302
+ }
38303
+ return void 0;
38304
+ }
38305
+ function generateRequestId() {
38306
+ const g = globalThis.crypto;
38307
+ if (g && typeof g.randomUUID === "function") {
38308
+ return `req_${g.randomUUID().replace(/-/g, "")}`;
38309
+ }
38310
+ const t = Date.now().toString(36);
38311
+ const r = Math.random().toString(36).slice(2, 12);
38312
+ return `req_${t}${r}`;
38313
+ }
38314
+ function resolveRequestId(headers, generate = generateRequestId) {
38315
+ return extractRequestId(headers) ?? generate();
38316
+ }
38317
+ var TRACEPARENT_PATTERN = /^([0-9a-f]{2})-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$/;
38318
+ function parseTraceparent(value) {
38319
+ if (typeof value !== "string") return void 0;
38320
+ const m = TRACEPARENT_PATTERN.exec(value.trim().toLowerCase());
38321
+ if (!m) return void 0;
38322
+ const [, version, traceId, spanId, flags] = m;
38323
+ if (version !== "00") return void 0;
38324
+ if (/^0+$/.test(traceId) || /^0+$/.test(spanId)) return void 0;
38325
+ const sampled = (parseInt(flags, 16) & 1) === 1;
38326
+ return { traceId, spanId, sampled };
38327
+ }
38328
+ function formatTraceparent(ctx) {
38329
+ const flag = ctx.sampled ? "01" : "00";
38330
+ return `00-${ctx.traceId}-${ctx.spanId}-${flag}`;
38331
+ }
38332
+
38333
+ // src/observability/metrics.ts
38334
+ var NoopMetricsRegistry = class {
38335
+ counter() {
38336
+ }
38337
+ histogram() {
38338
+ }
38339
+ gauge() {
38340
+ }
38341
+ };
38342
+ var InMemoryMetricsRegistry = class {
38343
+ constructor() {
38344
+ this.samples = [];
38345
+ }
38346
+ counter(name, labels = {}, value = 1) {
38347
+ this.samples.push({ name, kind: "counter", value, labels, at: Date.now() });
38348
+ }
38349
+ histogram(name, value, labels = {}) {
38350
+ this.samples.push({ name, kind: "histogram", value, labels, at: Date.now() });
38351
+ }
38352
+ gauge(name, value, labels = {}) {
38353
+ this.samples.push({ name, kind: "gauge", value, labels, at: Date.now() });
38354
+ }
38355
+ /**
38356
+ * Sum of all counter increments matching `name` (and optionally a
38357
+ * label subset). Useful in tests: `metrics.totalCounter('http_requests_total', { status: '500' })`.
38358
+ */
38359
+ totalCounter(name, labelMatch = {}) {
38360
+ return this.samples.filter(
38361
+ (s) => s.kind === "counter" && s.name === name && matchesLabels(s.labels, labelMatch)
38362
+ ).reduce((acc, s) => acc + s.value, 0);
38363
+ }
38364
+ /**
38365
+ * All histogram observations matching `name` (and optionally a
38366
+ * label subset), as raw values.
38367
+ */
38368
+ histogramValues(name, labelMatch = {}) {
38369
+ return this.samples.filter(
38370
+ (s) => s.kind === "histogram" && s.name === name && matchesLabels(s.labels, labelMatch)
38371
+ ).map((s) => s.value);
38372
+ }
38373
+ /** Clear all recorded samples. */
38374
+ reset() {
38375
+ this.samples.length = 0;
38376
+ }
38377
+ };
38378
+ function matchesLabels(actual, expected) {
38379
+ for (const [k, v] of Object.entries(expected)) {
38380
+ if (actual[k] !== v) return false;
38381
+ }
38382
+ return true;
38383
+ }
38384
+ var RUNTIME_METRICS = {
38385
+ /** Counter, labels: method, route, status. */
38386
+ httpRequestsTotal: "http_requests_total",
38387
+ /** Histogram (ms), labels: method, route. */
38388
+ httpRequestDurationMs: "http_request_duration_ms",
38389
+ /** Counter, labels: method, route. Incremented when an in-flight handler throws (after the response is sent). */
38390
+ httpRequestErrorsTotal: "http_request_errors_total"
38391
+ };
38392
+
38393
+ // src/observability/error-reporter.ts
38394
+ var NoopErrorReporter = class {
38395
+ captureException() {
38396
+ }
38397
+ };
38398
+ var InMemoryErrorReporter = class {
38399
+ constructor() {
38400
+ this.captured = [];
38401
+ }
38402
+ captureException(error2, context = {}) {
38403
+ this.captured.push({ error: error2, context, at: Date.now() });
38404
+ }
38405
+ reset() {
38406
+ this.captured.length = 0;
38407
+ }
38408
+ };
38409
+
38410
+ // src/observability/instrument.ts
38411
+ function instrumentRouteHandler(method, route, handler, opts = {}) {
38412
+ const metrics = opts.metrics ?? new NoopMetricsRegistry();
38413
+ const errorReporter = opts.errorReporter ?? new NoopErrorReporter();
38414
+ const generateRequestId2 = opts.generateRequestId;
38415
+ const requestIdHeader = opts.requestIdHeader ?? "X-Request-Id";
38416
+ const now = opts.now ?? Date.now;
38417
+ return async (req, res) => {
38418
+ const requestId = resolveRequestId(req?.headers, generateRequestId2);
38419
+ try {
38420
+ req.requestId = requestId;
38421
+ } catch {
38422
+ }
38423
+ if (typeof res?.header === "function") {
38424
+ try {
38425
+ res.header(requestIdHeader, requestId);
38426
+ } catch {
38427
+ }
38428
+ }
38429
+ let status = 200;
38430
+ const origStatus = typeof res?.status === "function" ? res.status.bind(res) : void 0;
38431
+ if (origStatus) {
38432
+ res.status = (code) => {
38433
+ status = code;
38434
+ return origStatus(code);
38435
+ };
38436
+ }
38437
+ const startedAt = now();
38438
+ let threw = false;
38439
+ try {
38440
+ await handler(req, res);
38441
+ } catch (err) {
38442
+ threw = true;
38443
+ status = err?.statusCode ?? 500;
38444
+ metrics.counter(RUNTIME_METRICS.httpRequestErrorsTotal, { method, route });
38445
+ if (status >= 500) {
38446
+ safeReport(errorReporter, err, { requestId, method, route });
38447
+ }
38448
+ throw err;
38449
+ } finally {
38450
+ const elapsed = now() - startedAt;
38451
+ metrics.counter(RUNTIME_METRICS.httpRequestsTotal, {
38452
+ method,
38453
+ route,
38454
+ status: String(status)
38455
+ });
38456
+ metrics.histogram(
38457
+ RUNTIME_METRICS.httpRequestDurationMs,
38458
+ elapsed,
38459
+ { method, route }
38460
+ );
38461
+ if (!threw && status >= 500) {
38462
+ const recorded = res?.__obsRecordedError;
38463
+ if (recorded !== void 0) {
38464
+ safeReport(errorReporter, recorded, { requestId, method, route });
38465
+ }
38466
+ }
38467
+ }
38468
+ };
38469
+ }
38470
+ function safeReport(reporter, err, ctx) {
38471
+ try {
38472
+ reporter.captureException(err, ctx);
38473
+ } catch {
38474
+ }
38475
+ }
38476
+
37356
38477
  // src/dispatcher-plugin.ts
37357
- function mountRouteOnServer(route, server, routePath) {
38478
+ function mountRouteOnServer(route, server, routePath, securityHeaders) {
37358
38479
  const handler = async (req, res) => {
37359
38480
  try {
37360
38481
  const result = await route.handler({
@@ -37364,6 +38485,11 @@ function mountRouteOnServer(route, server, routePath) {
37364
38485
  });
37365
38486
  if (result.stream && result.events) {
37366
38487
  res.status(result.status);
38488
+ if (securityHeaders) {
38489
+ for (const [k, v] of Object.entries(securityHeaders)) {
38490
+ res.header(k, v);
38491
+ }
38492
+ }
37367
38493
  if (result.headers) {
37368
38494
  for (const [k, v] of Object.entries(result.headers)) {
37369
38495
  res.header(k, String(v));
@@ -37389,6 +38515,11 @@ function mountRouteOnServer(route, server, routePath) {
37389
38515
  }
37390
38516
  } else {
37391
38517
  res.status(result.status);
38518
+ if (securityHeaders) {
38519
+ for (const [k, v] of Object.entries(securityHeaders)) {
38520
+ res.header(k, v);
38521
+ }
38522
+ }
37392
38523
  if (result.body !== void 0) {
37393
38524
  res.json(result.body);
37394
38525
  } else {
@@ -37396,7 +38527,7 @@ function mountRouteOnServer(route, server, routePath) {
37396
38527
  }
37397
38528
  }
37398
38529
  } catch (err) {
37399
- errorResponse(err, res);
38530
+ errorResponseBase(err, res, securityHeaders);
37400
38531
  }
37401
38532
  };
37402
38533
  const m = route.method.toLowerCase();
@@ -37412,10 +38543,17 @@ function mountRouteOnServer(route, server, routePath) {
37412
38543
  }
37413
38544
  return false;
37414
38545
  }
37415
- function sendResult(result, res) {
38546
+ function sendResultBase(result, res, securityHeaders) {
38547
+ const applySecurityHeaders = () => {
38548
+ if (!securityHeaders) return;
38549
+ for (const [k, v] of Object.entries(securityHeaders)) {
38550
+ res.header(k, v);
38551
+ }
38552
+ };
37416
38553
  if (result.handled) {
37417
38554
  if (result.response) {
37418
38555
  res.status(result.response.status);
38556
+ applySecurityHeaders();
37419
38557
  if (result.response.headers) {
37420
38558
  for (const [k, v] of Object.entries(result.response.headers)) {
37421
38559
  res.header(k, v);
@@ -37425,11 +38563,15 @@ function sendResult(result, res) {
37425
38563
  return;
37426
38564
  }
37427
38565
  if (result.result) {
37428
- res.status(200).json(result.result);
38566
+ res.status(200);
38567
+ applySecurityHeaders();
38568
+ res.json(result.result);
37429
38569
  return;
37430
38570
  }
37431
38571
  }
37432
- res.status(404).json({
38572
+ res.status(404);
38573
+ applySecurityHeaders();
38574
+ res.json({
37433
38575
  success: false,
37434
38576
  error: {
37435
38577
  message: "Not Found",
@@ -37439,9 +38581,21 @@ function sendResult(result, res) {
37439
38581
  }
37440
38582
  });
37441
38583
  }
37442
- function errorResponse(err, res) {
38584
+ function errorResponseBase(err, res, securityHeaders) {
37443
38585
  const code = err.statusCode || 500;
37444
- res.status(code).json({
38586
+ res.status(code);
38587
+ if (securityHeaders) {
38588
+ for (const [k, v] of Object.entries(securityHeaders)) {
38589
+ res.header(k, v);
38590
+ }
38591
+ }
38592
+ if (code >= 500) {
38593
+ try {
38594
+ res.__obsRecordedError = err;
38595
+ } catch {
38596
+ }
38597
+ }
38598
+ res.json({
37445
38599
  success: false,
37446
38600
  error: { message: err.message || "Internal Server Error", code }
37447
38601
  });
@@ -37466,10 +38620,52 @@ function createDispatcherPlugin(config = {}) {
37466
38620
  enforceProjectMembership: enforceMembership
37467
38621
  });
37468
38622
  const prefix = config.prefix || "/api/v1";
38623
+ const securityHeaders = config.securityHeaders === false ? void 0 : buildSecurityHeaders(
38624
+ typeof config.securityHeaders === "object" ? config.securityHeaders : {}
38625
+ );
38626
+ const sendResult = (result, res) => sendResultBase(result, res, securityHeaders);
38627
+ const errorResponse = (err, res) => errorResponseBase(err, res, securityHeaders);
38628
+ const metrics = config.observability?.metrics ?? new NoopMetricsRegistry();
38629
+ const errorReporter = config.observability?.errorReporter ?? new NoopErrorReporter();
38630
+ const generateRequestId2 = config.observability?.generateRequestId;
38631
+ const requestIdHeader = config.observability?.requestIdHeader ?? "X-Request-Id";
38632
+ const rawServer = server;
38633
+ server = new Proxy(rawServer, {
38634
+ get(target, prop, receiver) {
38635
+ if (prop === "get" || prop === "post" || prop === "delete") {
38636
+ const method = String(prop).toUpperCase();
38637
+ const original = target[prop];
38638
+ if (typeof original !== "function") return original;
38639
+ return (route, handler) => {
38640
+ return original.call(
38641
+ target,
38642
+ route,
38643
+ instrumentRouteHandler(method, route, handler, {
38644
+ metrics,
38645
+ errorReporter,
38646
+ generateRequestId: generateRequestId2,
38647
+ requestIdHeader
38648
+ })
38649
+ );
38650
+ };
38651
+ }
38652
+ return Reflect.get(target, prop, receiver);
38653
+ }
38654
+ });
37469
38655
  server.get("/.well-known/objectstack", async (_req, res) => {
38656
+ if (securityHeaders) {
38657
+ for (const [k, v] of Object.entries(securityHeaders)) {
38658
+ res.header(k, v);
38659
+ }
38660
+ }
37470
38661
  res.json({ data: await dispatcher.getDiscoveryInfo(prefix) });
37471
38662
  });
37472
38663
  server.get(`${prefix}/discovery`, async (_req, res) => {
38664
+ if (securityHeaders) {
38665
+ for (const [k, v] of Object.entries(securityHeaders)) {
38666
+ res.header(k, v);
38667
+ }
38668
+ }
37473
38669
  res.json({ data: await dispatcher.getDiscoveryInfo(prefix) });
37474
38670
  });
37475
38671
  server.get(`${prefix}/health`, async (_req, res) => {
@@ -37491,6 +38687,11 @@ function createDispatcherPlugin(config = {}) {
37491
38687
  server.post(`${prefix}/graphql`, async (req, res) => {
37492
38688
  try {
37493
38689
  const result = await dispatcher.handleGraphQL(req.body, { request: req });
38690
+ if (securityHeaders) {
38691
+ for (const [k, v] of Object.entries(securityHeaders)) {
38692
+ res.header(k, v);
38693
+ }
38694
+ }
37494
38695
  res.json(result);
37495
38696
  } catch (err) {
37496
38697
  errorResponse(err, res);
@@ -37498,7 +38699,7 @@ function createDispatcherPlugin(config = {}) {
37498
38699
  });
37499
38700
  server.post(`${prefix}/analytics/query`, async (req, res) => {
37500
38701
  try {
37501
- const result = await dispatcher.handleAnalytics("query", "POST", req.body, { request: req });
38702
+ const result = await dispatcher.dispatch("POST", "/analytics/query", req.body, req.query, { request: req });
37502
38703
  sendResult(result, res);
37503
38704
  } catch (err) {
37504
38705
  errorResponse(err, res);
@@ -37506,7 +38707,7 @@ function createDispatcherPlugin(config = {}) {
37506
38707
  });
37507
38708
  server.get(`${prefix}/analytics/meta`, async (req, res) => {
37508
38709
  try {
37509
- const result = await dispatcher.handleAnalytics("meta", "GET", {}, { request: req });
38710
+ const result = await dispatcher.dispatch("GET", "/analytics/meta", void 0, req.query, { request: req });
37510
38711
  sendResult(result, res);
37511
38712
  } catch (err) {
37512
38713
  errorResponse(err, res);
@@ -37514,7 +38715,7 @@ function createDispatcherPlugin(config = {}) {
37514
38715
  });
37515
38716
  server.post(`${prefix}/analytics/sql`, async (req, res) => {
37516
38717
  try {
37517
- const result = await dispatcher.handleAnalytics("sql", "POST", req.body, { request: req });
38718
+ const result = await dispatcher.dispatch("POST", "/analytics/sql", req.body, req.query, { request: req });
37518
38719
  sendResult(result, res);
37519
38720
  } catch (err) {
37520
38721
  errorResponse(err, res);
@@ -37592,6 +38793,14 @@ function createDispatcherPlugin(config = {}) {
37592
38793
  errorResponse(err, res);
37593
38794
  }
37594
38795
  });
38796
+ server.post(`${prefix}/cloud/admin/platform-sso/backfill`, async (req, res) => {
38797
+ try {
38798
+ const result = await dispatcher.handleCloud("/admin/platform-sso/backfill", "POST", req.body, req.query, { request: req });
38799
+ sendResult(result, res);
38800
+ } catch (err) {
38801
+ errorResponse(err, res);
38802
+ }
38803
+ });
37595
38804
  server.get(`${prefix}/cloud/templates`, async (req, res) => {
37596
38805
  try {
37597
38806
  const result = await dispatcher.handleCloud("/templates", "GET", {}, req.query, { request: req });
@@ -37704,6 +38913,30 @@ function createDispatcherPlugin(config = {}) {
37704
38913
  errorResponse(err, res);
37705
38914
  }
37706
38915
  });
38916
+ server.post(`${prefix}/cloud/projects/:id/members`, async (req, res) => {
38917
+ try {
38918
+ const result = await dispatcher.handleCloud(`/projects/${req.params.id}/members`, "POST", req.body, {}, { request: req });
38919
+ sendResult(result, res);
38920
+ } catch (err) {
38921
+ errorResponse(err, res);
38922
+ }
38923
+ });
38924
+ server.patch(`${prefix}/cloud/projects/:id/members/:memberId`, async (req, res) => {
38925
+ try {
38926
+ const result = await dispatcher.handleCloud(`/projects/${req.params.id}/members/${req.params.memberId}`, "PATCH", req.body, {}, { request: req });
38927
+ sendResult(result, res);
38928
+ } catch (err) {
38929
+ errorResponse(err, res);
38930
+ }
38931
+ });
38932
+ server.delete(`${prefix}/cloud/projects/:id/members/:memberId`, async (req, res) => {
38933
+ try {
38934
+ const result = await dispatcher.handleCloud(`/projects/${req.params.id}/members/${req.params.memberId}`, "DELETE", req.body ?? {}, {}, { request: req });
38935
+ sendResult(result, res);
38936
+ } catch (err) {
38937
+ errorResponse(err, res);
38938
+ }
38939
+ });
37707
38940
  server.get(`${prefix}/cloud/projects/:id/packages`, async (req, res) => {
37708
38941
  try {
37709
38942
  const result = await dispatcher.handleCloud(`/projects/${req.params.id}/packages`, "GET", {}, req.query, { request: req });
@@ -37778,7 +39011,7 @@ function createDispatcherPlugin(config = {}) {
37778
39011
  });
37779
39012
  server.get(`${prefix}/i18n/locales`, async (req, res) => {
37780
39013
  try {
37781
- const result = await dispatcher.handleI18n("/locales", "GET", req.query, { request: req });
39014
+ const result = await dispatcher.dispatch("GET", "/i18n/locales", void 0, req.query, { request: req });
37782
39015
  sendResult(result, res);
37783
39016
  } catch (err) {
37784
39017
  errorResponse(err, res);
@@ -37786,7 +39019,7 @@ function createDispatcherPlugin(config = {}) {
37786
39019
  });
37787
39020
  server.get(`${prefix}/i18n/translations/:locale`, async (req, res) => {
37788
39021
  try {
37789
- const result = await dispatcher.handleI18n(`/translations/${req.params.locale}`, "GET", req.query, { request: req });
39022
+ const result = await dispatcher.dispatch("GET", `/i18n/translations/${req.params.locale}`, void 0, req.query, { request: req });
37790
39023
  sendResult(result, res);
37791
39024
  } catch (err) {
37792
39025
  errorResponse(err, res);
@@ -37794,7 +39027,7 @@ function createDispatcherPlugin(config = {}) {
37794
39027
  });
37795
39028
  server.get(`${prefix}/i18n/labels/:object/:locale`, async (req, res) => {
37796
39029
  try {
37797
- const result = await dispatcher.handleI18n(`/labels/${req.params.object}/${req.params.locale}`, "GET", req.query, { request: req });
39030
+ const result = await dispatcher.dispatch("GET", `/i18n/labels/${req.params.object}/${req.params.locale}`, void 0, req.query, { request: req });
37798
39031
  sendResult(result, res);
37799
39032
  } catch (err) {
37800
39033
  errorResponse(err, res);
@@ -37803,7 +39036,7 @@ function createDispatcherPlugin(config = {}) {
37803
39036
  const registerAutomationRoutes = (base2) => {
37804
39037
  server.get(`${base2}/automation`, async (req, res) => {
37805
39038
  try {
37806
- const result = await dispatcher.handleAutomation("", "GET", {}, { request: req });
39039
+ const result = await dispatcher.dispatch("GET", "/automation", void 0, req.query, { request: req });
37807
39040
  sendResult(result, res);
37808
39041
  } catch (err) {
37809
39042
  errorResponse(err, res);
@@ -37811,7 +39044,7 @@ function createDispatcherPlugin(config = {}) {
37811
39044
  });
37812
39045
  server.post(`${base2}/automation`, async (req, res) => {
37813
39046
  try {
37814
- const result = await dispatcher.handleAutomation("", "POST", req.body, { request: req });
39047
+ const result = await dispatcher.dispatch("POST", "/automation", req.body, req.query, { request: req });
37815
39048
  sendResult(result, res);
37816
39049
  } catch (err) {
37817
39050
  errorResponse(err, res);
@@ -37819,7 +39052,7 @@ function createDispatcherPlugin(config = {}) {
37819
39052
  });
37820
39053
  server.get(`${base2}/automation/:name`, async (req, res) => {
37821
39054
  try {
37822
- const result = await dispatcher.handleAutomation(`${req.params.name}`, "GET", {}, { request: req });
39055
+ const result = await dispatcher.dispatch("GET", `/automation/${req.params.name}`, void 0, req.query, { request: req });
37823
39056
  sendResult(result, res);
37824
39057
  } catch (err) {
37825
39058
  errorResponse(err, res);
@@ -37827,7 +39060,7 @@ function createDispatcherPlugin(config = {}) {
37827
39060
  });
37828
39061
  server.put(`${base2}/automation/:name`, async (req, res) => {
37829
39062
  try {
37830
- const result = await dispatcher.handleAutomation(`${req.params.name}`, "PUT", req.body, { request: req });
39063
+ const result = await dispatcher.dispatch("PUT", `/automation/${req.params.name}`, req.body, req.query, { request: req });
37831
39064
  sendResult(result, res);
37832
39065
  } catch (err) {
37833
39066
  errorResponse(err, res);
@@ -37835,7 +39068,7 @@ function createDispatcherPlugin(config = {}) {
37835
39068
  });
37836
39069
  server.delete(`${base2}/automation/:name`, async (req, res) => {
37837
39070
  try {
37838
- const result = await dispatcher.handleAutomation(`${req.params.name}`, "DELETE", {}, { request: req });
39071
+ const result = await dispatcher.dispatch("DELETE", `/automation/${req.params.name}`, void 0, req.query, { request: req });
37839
39072
  sendResult(result, res);
37840
39073
  } catch (err) {
37841
39074
  errorResponse(err, res);
@@ -37843,7 +39076,7 @@ function createDispatcherPlugin(config = {}) {
37843
39076
  });
37844
39077
  server.post(`${base2}/automation/trigger/:name`, async (req, res) => {
37845
39078
  try {
37846
- const result = await dispatcher.handleAutomation(`trigger/${req.params.name}`, "POST", req.body, { request: req });
39079
+ const result = await dispatcher.dispatch("POST", `/automation/trigger/${req.params.name}`, req.body, req.query, { request: req });
37847
39080
  sendResult(result, res);
37848
39081
  } catch (err) {
37849
39082
  errorResponse(err, res);
@@ -37851,7 +39084,7 @@ function createDispatcherPlugin(config = {}) {
37851
39084
  });
37852
39085
  server.post(`${base2}/automation/:name/trigger`, async (req, res) => {
37853
39086
  try {
37854
- const result = await dispatcher.handleAutomation(`${req.params.name}/trigger`, "POST", req.body, { request: req });
39087
+ const result = await dispatcher.dispatch("POST", `/automation/${req.params.name}/trigger`, req.body, req.query, { request: req });
37855
39088
  sendResult(result, res);
37856
39089
  } catch (err) {
37857
39090
  errorResponse(err, res);
@@ -37859,7 +39092,7 @@ function createDispatcherPlugin(config = {}) {
37859
39092
  });
37860
39093
  server.post(`${base2}/automation/:name/toggle`, async (req, res) => {
37861
39094
  try {
37862
- const result = await dispatcher.handleAutomation(`${req.params.name}/toggle`, "POST", req.body, { request: req });
39095
+ const result = await dispatcher.dispatch("POST", `/automation/${req.params.name}/toggle`, req.body, req.query, { request: req });
37863
39096
  sendResult(result, res);
37864
39097
  } catch (err) {
37865
39098
  errorResponse(err, res);
@@ -37867,7 +39100,7 @@ function createDispatcherPlugin(config = {}) {
37867
39100
  });
37868
39101
  server.get(`${base2}/automation/:name/runs`, async (req, res) => {
37869
39102
  try {
37870
- const result = await dispatcher.handleAutomation(`${req.params.name}/runs`, "GET", {}, { request: req }, req.query);
39103
+ const result = await dispatcher.dispatch("GET", `/automation/${req.params.name}/runs`, void 0, req.query, { request: req });
37871
39104
  sendResult(result, res);
37872
39105
  } catch (err) {
37873
39106
  errorResponse(err, res);
@@ -37875,13 +39108,34 @@ function createDispatcherPlugin(config = {}) {
37875
39108
  });
37876
39109
  server.get(`${base2}/automation/:name/runs/:runId`, async (req, res) => {
37877
39110
  try {
37878
- const result = await dispatcher.handleAutomation(`${req.params.name}/runs/${req.params.runId}`, "GET", {}, { request: req });
39111
+ const result = await dispatcher.dispatch("GET", `/automation/${req.params.name}/runs/${req.params.runId}`, void 0, req.query, { request: req });
37879
39112
  sendResult(result, res);
37880
39113
  } catch (err) {
37881
39114
  errorResponse(err, res);
37882
39115
  }
37883
39116
  });
37884
39117
  };
39118
+ const registerAIRoutes = (base2) => {
39119
+ const wildcards = [
39120
+ ["get", `${base2}/ai/*`],
39121
+ ["post", `${base2}/ai/*`],
39122
+ ["delete", `${base2}/ai/*`],
39123
+ ["put", `${base2}/ai/*`]
39124
+ ];
39125
+ for (const [method, pattern] of wildcards) {
39126
+ server[method](pattern, async (req, res) => {
39127
+ try {
39128
+ const fullPath = req.path ?? "";
39129
+ const idx = fullPath.lastIndexOf("/ai");
39130
+ const aiSubPath = idx >= 0 ? fullPath.slice(idx) : "/ai";
39131
+ const result = await dispatcher.dispatch(method.toUpperCase(), aiSubPath, req.body, req.query, { request: req });
39132
+ sendResult(result, res);
39133
+ } catch (err) {
39134
+ errorResponse(err, res);
39135
+ }
39136
+ });
39137
+ }
39138
+ };
37885
39139
  const registerActionRoutes = (base2) => {
37886
39140
  server.post(`${base2}/actions/:object/:action`, async (req, res) => {
37887
39141
  try {
@@ -37909,12 +39163,15 @@ function createDispatcherPlugin(config = {}) {
37909
39163
  if (enableProjectScoping && projectResolution === "required") {
37910
39164
  registerAutomationRoutes(`${prefix}/projects/:projectId`);
37911
39165
  registerActionRoutes(`${prefix}/projects/:projectId`);
39166
+ registerAIRoutes(`${prefix}/projects/:projectId`);
37912
39167
  } else {
37913
39168
  registerAutomationRoutes(prefix);
37914
39169
  registerActionRoutes(prefix);
39170
+ registerAIRoutes(prefix);
37915
39171
  if (enableProjectScoping) {
37916
39172
  registerAutomationRoutes(`${prefix}/projects/:projectId`);
37917
39173
  registerActionRoutes(`${prefix}/projects/:projectId`);
39174
+ registerAIRoutes(`${prefix}/projects/:projectId`);
37918
39175
  }
37919
39176
  }
37920
39177
  ctx.logger.info("Dispatcher bridge routes registered", { prefix, enableProjectScoping, projectResolution });
@@ -37930,11 +39187,11 @@ function createDispatcherPlugin(config = {}) {
37930
39187
  const routePath = route.path.startsWith("/api/v1") ? route.path : `${prefix}${route.path}`;
37931
39188
  let count = 0;
37932
39189
  if (enableProjectScoping && projectResolution === "required") {
37933
- if (mountRouteOnServer(route, server, toScopedPath(routePath))) count++;
39190
+ if (mountRouteOnServer(route, server, toScopedPath(routePath), securityHeaders)) count++;
37934
39191
  } else {
37935
- if (mountRouteOnServer(route, server, routePath)) count++;
39192
+ if (mountRouteOnServer(route, server, routePath, securityHeaders)) count++;
37936
39193
  if (enableProjectScoping) {
37937
- if (mountRouteOnServer(route, server, toScopedPath(routePath))) count++;
39194
+ if (mountRouteOnServer(route, server, toScopedPath(routePath), securityHeaders)) count++;
37938
39195
  }
37939
39196
  }
37940
39197
  return count;
@@ -38259,6 +39516,1334 @@ var MiddlewareManager = class {
38259
39516
  // src/index.ts
38260
39517
  init_load_artifact_bundle();
38261
39518
 
39519
+ // src/cloud/kernel-manager.ts
39520
+ var KernelManager = class {
39521
+ constructor(config) {
39522
+ this.cache = /* @__PURE__ */ new Map();
39523
+ this.pending = /* @__PURE__ */ new Map();
39524
+ this.factory = config.factory;
39525
+ this.maxSize = config.maxSize ?? 32;
39526
+ this.ttlMs = config.ttlMs ?? 15 * 60 * 1e3;
39527
+ this.logger = config.logger ?? console;
39528
+ }
39529
+ /** Returns the currently cached projectIds (ordered by insertion). */
39530
+ keys() {
39531
+ return Array.from(this.cache.keys());
39532
+ }
39533
+ /** Cache size for diagnostics. */
39534
+ get size() {
39535
+ return this.cache.size;
39536
+ }
39537
+ /**
39538
+ * Resolve or construct the kernel for `projectId`.
39539
+ *
39540
+ * - Cache hit (fresh): bumps `lastAccess` and returns immediately.
39541
+ * - Cache hit (TTL expired): evicts then falls through to factory.
39542
+ * - Cache miss: dedupes concurrent callers through `pending`.
39543
+ */
39544
+ async getOrCreate(projectId) {
39545
+ const existing = this.cache.get(projectId);
39546
+ if (existing) {
39547
+ if (this.ttlMs > 0 && Date.now() - existing.lastAccess > this.ttlMs) {
39548
+ await this.evict(projectId);
39549
+ } else {
39550
+ existing.lastAccess = Date.now();
39551
+ return existing.kernel;
39552
+ }
39553
+ }
39554
+ const inflight = this.pending.get(projectId);
39555
+ if (inflight) return inflight;
39556
+ const promise = (async () => {
39557
+ const kernel = await this.factory.create(projectId);
39558
+ const now = Date.now();
39559
+ this.cache.set(projectId, { kernel, createdAt: now, lastAccess: now });
39560
+ await this.enforceMaxSize();
39561
+ return kernel;
39562
+ })();
39563
+ this.pending.set(projectId, promise);
39564
+ try {
39565
+ return await promise;
39566
+ } finally {
39567
+ this.pending.delete(projectId);
39568
+ }
39569
+ }
39570
+ /**
39571
+ * Evict the kernel for `projectId` and invoke `kernel.shutdown()`.
39572
+ * No-op when the entry is absent.
39573
+ */
39574
+ async evict(projectId) {
39575
+ const entry = this.cache.get(projectId);
39576
+ if (!entry) return;
39577
+ this.cache.delete(projectId);
39578
+ try {
39579
+ await entry.kernel.shutdown();
39580
+ } catch (err) {
39581
+ this.logger.error?.("[KernelManager] shutdown failed", { projectId, err });
39582
+ }
39583
+ }
39584
+ /** Evict all resident kernels. Used on runtime shutdown. */
39585
+ async evictAll() {
39586
+ const ids = Array.from(this.cache.keys());
39587
+ await Promise.all(ids.map((id) => this.evict(id)));
39588
+ }
39589
+ async enforceMaxSize() {
39590
+ while (this.cache.size > this.maxSize) {
39591
+ let oldestKey;
39592
+ let oldestAccess = Infinity;
39593
+ for (const [key, entry] of this.cache) {
39594
+ if (entry.lastAccess < oldestAccess) {
39595
+ oldestAccess = entry.lastAccess;
39596
+ oldestKey = key;
39597
+ }
39598
+ }
39599
+ if (!oldestKey) return;
39600
+ await this.evict(oldestKey);
39601
+ }
39602
+ }
39603
+ };
39604
+
39605
+ // src/cloud/artifact-api-client.ts
39606
+ var ArtifactApiClient = class {
39607
+ constructor(config) {
39608
+ this.hostnameCache = /* @__PURE__ */ new Map();
39609
+ this.artifactCache = /* @__PURE__ */ new Map();
39610
+ this.pendingHostname = /* @__PURE__ */ new Map();
39611
+ this.pendingArtifact = /* @__PURE__ */ new Map();
39612
+ if (!config.controlPlaneUrl) {
39613
+ throw new Error("[ArtifactApiClient] controlPlaneUrl is required");
39614
+ }
39615
+ this.base = config.controlPlaneUrl.replace(/\/+$/, "");
39616
+ this.apiKey = config.apiKey;
39617
+ this.cacheTtlMs = config.cacheTtlMs ?? 5 * 60 * 1e3;
39618
+ this.requestTimeoutMs = config.requestTimeoutMs ?? 1e4;
39619
+ this.fetchImpl = config.fetch ?? globalThis.fetch;
39620
+ this.logger = config.logger ?? console;
39621
+ if (typeof this.fetchImpl !== "function") {
39622
+ throw new Error("[ArtifactApiClient] global fetch is not available \u2014 provide config.fetch");
39623
+ }
39624
+ }
39625
+ /**
39626
+ * Resolve a hostname to its project. Returns `null` on 404 or
39627
+ * malformed responses. Errors (network / 5xx) are thrown so
39628
+ * upstream callers can retry.
39629
+ */
39630
+ async resolveHostname(host) {
39631
+ const cached = this.hostnameCache.get(host);
39632
+ if (cached && cached.expiresAt > Date.now()) return cached.value;
39633
+ const inflight = this.pendingHostname.get(host);
39634
+ if (inflight) return inflight;
39635
+ const promise = (async () => {
39636
+ try {
39637
+ const url = `${this.base}/api/v1/cloud/resolve-hostname?host=${encodeURIComponent(host)}`;
39638
+ const res = await this.request(url);
39639
+ if (res === null) return null;
39640
+ const body = res.success === false ? null : res.data ?? res;
39641
+ if (!body || typeof body.projectId !== "string" || !body.projectId) return null;
39642
+ const value = {
39643
+ projectId: body.projectId,
39644
+ organizationId: body.organizationId,
39645
+ runtime: body.runtime
39646
+ };
39647
+ this.hostnameCache.set(host, { value, expiresAt: Date.now() + this.cacheTtlMs });
39648
+ return value;
39649
+ } finally {
39650
+ this.pendingHostname.delete(host);
39651
+ }
39652
+ })();
39653
+ this.pendingHostname.set(host, promise);
39654
+ return promise;
39655
+ }
39656
+ /**
39657
+ * Fetch the compiled artifact for a project.
39658
+ *
39659
+ * When `opts.commit` is set, requests that specific revision via the
39660
+ * existing `?commit=` query param. Different commits are cached
39661
+ * independently (the cache key includes the commit id) so the preview
39662
+ * runtime can hold multiple versions in memory simultaneously.
39663
+ */
39664
+ async fetchArtifact(projectId, opts) {
39665
+ const commit = opts?.commit?.trim() || "";
39666
+ const cacheKey = commit ? `${projectId}@${commit}` : projectId;
39667
+ const cached = this.artifactCache.get(cacheKey);
39668
+ if (cached && cached.expiresAt > Date.now()) return cached.value;
39669
+ const inflight = this.pendingArtifact.get(cacheKey);
39670
+ if (inflight) return inflight;
39671
+ const promise = (async () => {
39672
+ try {
39673
+ const qs = commit ? `?commit=${encodeURIComponent(commit)}` : "";
39674
+ const url = `${this.base}/api/v1/cloud/projects/${encodeURIComponent(projectId)}/artifact${qs}`;
39675
+ const res = await this.request(url);
39676
+ if (res === null) return null;
39677
+ const body = res.success === false ? null : res.data ?? res;
39678
+ if (!body || typeof body !== "object") return null;
39679
+ if (!body.metadata) {
39680
+ this.logger.warn?.("[ArtifactApiClient] artifact response missing `metadata`", { projectId, commit });
39681
+ return null;
39682
+ }
39683
+ const value = body;
39684
+ this.artifactCache.set(cacheKey, { value, expiresAt: Date.now() + this.cacheTtlMs });
39685
+ return value;
39686
+ } finally {
39687
+ this.pendingArtifact.delete(cacheKey);
39688
+ }
39689
+ })();
39690
+ this.pendingArtifact.set(cacheKey, promise);
39691
+ return promise;
39692
+ }
39693
+ /**
39694
+ * Resolve an 8-hex project short id (first 8 hex chars of the UUID,
39695
+ * dashes stripped) to the full projectId. Used by the preview
39696
+ * runtime, which encodes project ids in subdomains.
39697
+ *
39698
+ * Returns `null` on 404 or ambiguity (the control plane returns 409
39699
+ * if the prefix matches more than one project).
39700
+ */
39701
+ async lookupProjectByShortId(shortId) {
39702
+ const short = String(shortId ?? "").trim().toLowerCase();
39703
+ if (!/^[0-9a-f]{8,}$/.test(short)) return null;
39704
+ const url = `${this.base}/api/v1/cloud/projects-by-short-id/${encodeURIComponent(short)}`;
39705
+ const res = await this.request(url);
39706
+ if (res === null) return null;
39707
+ const body = res.success === false ? null : res.data ?? res;
39708
+ if (!body || typeof body.projectId !== "string" || !body.projectId) return null;
39709
+ return { projectId: body.projectId, organizationId: body.organizationId };
39710
+ }
39711
+ /**
39712
+ * Fetch the head commit of a branch. Returns the commit id (and the
39713
+ * matching revision row's `published_at` for cache-validity checks).
39714
+ * Reuses the existing `GET /cloud/projects/:id/branches` endpoint.
39715
+ */
39716
+ async fetchBranchHead(projectId, branchName) {
39717
+ const url = `${this.base}/api/v1/cloud/projects/${encodeURIComponent(projectId)}/branches`;
39718
+ const res = await this.request(url);
39719
+ if (res === null) return null;
39720
+ const body = res.success === false ? null : res.data ?? res;
39721
+ const branches = Array.isArray(body?.branches) ? body.branches : [];
39722
+ const target = String(branchName ?? "").trim().toLowerCase();
39723
+ const found = branches.find((b) => String(b?.branch ?? "").toLowerCase() === target);
39724
+ if (!found?.headCommitId) return null;
39725
+ return { commitId: String(found.headCommitId), publishedAt: found.headPublishedAt ?? null };
39726
+ }
39727
+ /** Drop cached entries for a project (and any matching hostname). */
39728
+ invalidate(projectId) {
39729
+ this.artifactCache.delete(projectId);
39730
+ const prefix = `${projectId}@`;
39731
+ for (const key of Array.from(this.artifactCache.keys())) {
39732
+ if (key.startsWith(prefix)) this.artifactCache.delete(key);
39733
+ }
39734
+ for (const [host, entry] of this.hostnameCache) {
39735
+ if (entry.value.projectId === projectId) this.hostnameCache.delete(host);
39736
+ }
39737
+ }
39738
+ /** Drop everything. Used on shutdown / hot-reload. */
39739
+ clear() {
39740
+ this.hostnameCache.clear();
39741
+ this.artifactCache.clear();
39742
+ }
39743
+ async request(url) {
39744
+ const controller = typeof AbortController !== "undefined" ? new AbortController() : null;
39745
+ const timer = controller ? setTimeout(() => controller.abort(), this.requestTimeoutMs) : null;
39746
+ try {
39747
+ const res = await this.fetchImpl(url, {
39748
+ method: "GET",
39749
+ headers: this.buildHeaders(),
39750
+ signal: controller?.signal
39751
+ });
39752
+ if (res.status === 404) return null;
39753
+ if (!res.ok) {
39754
+ throw new Error(`[ArtifactApiClient] ${url} \u2192 HTTP ${res.status}`);
39755
+ }
39756
+ return await res.json();
39757
+ } finally {
39758
+ if (timer) clearTimeout(timer);
39759
+ }
39760
+ }
39761
+ buildHeaders() {
39762
+ const headers = {
39763
+ "accept": "application/json",
39764
+ "user-agent": "objectos-runtime"
39765
+ };
39766
+ if (this.apiKey) headers["authorization"] = `Bearer ${this.apiKey}`;
39767
+ return headers;
39768
+ }
39769
+ };
39770
+
39771
+ // src/cloud/artifact-environment-registry.ts
39772
+ import { resolve as resolvePathNode } from "path";
39773
+ var ArtifactEnvironmentRegistry = class {
39774
+ constructor(config) {
39775
+ this.hostnameCache = /* @__PURE__ */ new Map();
39776
+ this.idCache = /* @__PURE__ */ new Map();
39777
+ this.pending = /* @__PURE__ */ new Map();
39778
+ this.client = config.client;
39779
+ this.cacheTTL = config.cacheTtlMs ?? 5 * 60 * 1e3;
39780
+ this.logger = config.logger ?? console;
39781
+ }
39782
+ async resolveByHostname(host) {
39783
+ const cached = this.hostnameCache.get(host);
39784
+ if (cached && cached.expiresAt > Date.now()) {
39785
+ return { projectId: cached.projectId, driver: cached.driver };
39786
+ }
39787
+ const key = `host:${host}`;
39788
+ const inflight = this.pending.get(key);
39789
+ if (inflight) {
39790
+ const result = await inflight;
39791
+ return result ? { projectId: result.projectId, driver: result.driver } : null;
39792
+ }
39793
+ const promise = (async () => {
39794
+ try {
39795
+ const resolved = await this.client.resolveHostname(host);
39796
+ if (!resolved) return null;
39797
+ const entry2 = await this.buildCacheEntry(resolved.projectId, resolved.runtime, resolved.organizationId, host);
39798
+ if (!entry2) return null;
39799
+ this.hostnameCache.set(host, entry2);
39800
+ this.idCache.set(entry2.projectId, entry2);
39801
+ return entry2;
39802
+ } catch (err) {
39803
+ this.logger.error?.("[ArtifactEnvironmentRegistry] resolveByHostname failed", {
39804
+ host,
39805
+ error: err?.message ?? err
39806
+ });
39807
+ return null;
39808
+ } finally {
39809
+ this.pending.delete(key);
39810
+ }
39811
+ })();
39812
+ this.pending.set(key, promise);
39813
+ const entry = await promise;
39814
+ return entry ? { projectId: entry.projectId, driver: entry.driver } : null;
39815
+ }
39816
+ async resolveById(projectId) {
39817
+ const cached = this.idCache.get(projectId);
39818
+ if (cached && cached.expiresAt > Date.now()) return cached.driver;
39819
+ const key = `id:${projectId}`;
39820
+ const inflight = this.pending.get(key);
39821
+ if (inflight) {
39822
+ const result = await inflight;
39823
+ return result?.driver ?? null;
39824
+ }
39825
+ const promise = (async () => {
39826
+ try {
39827
+ const entry2 = await this.buildCacheEntry(projectId, void 0, void 0, void 0);
39828
+ if (!entry2) return null;
39829
+ this.idCache.set(projectId, entry2);
39830
+ if (entry2.project?.hostname) this.hostnameCache.set(entry2.project.hostname, entry2);
39831
+ return entry2;
39832
+ } catch (err) {
39833
+ this.logger.error?.("[ArtifactEnvironmentRegistry] resolveById failed", {
39834
+ projectId,
39835
+ error: err?.message ?? err
39836
+ });
39837
+ return null;
39838
+ } finally {
39839
+ this.pending.delete(key);
39840
+ }
39841
+ })();
39842
+ this.pending.set(key, promise);
39843
+ const entry = await promise;
39844
+ return entry?.driver ?? null;
39845
+ }
39846
+ peekById(projectId) {
39847
+ const cached = this.idCache.get(projectId);
39848
+ if (cached && cached.expiresAt > Date.now()) {
39849
+ return { projectId: cached.projectId, driver: cached.driver, project: cached.project };
39850
+ }
39851
+ return null;
39852
+ }
39853
+ invalidate(projectId) {
39854
+ this.idCache.delete(projectId);
39855
+ for (const [host, entry] of this.hostnameCache) {
39856
+ if (entry.projectId === projectId) this.hostnameCache.delete(host);
39857
+ }
39858
+ this.client.invalidate(projectId);
39859
+ }
39860
+ async buildCacheEntry(projectId, runtimeFromHostname, orgIdFromHostname, hostname) {
39861
+ let runtime = runtimeFromHostname;
39862
+ let organizationId = orgIdFromHostname;
39863
+ let host = hostname;
39864
+ let artifactProjectId = projectId;
39865
+ if (!runtime || !organizationId) {
39866
+ const artifact = await this.client.fetchArtifact(projectId);
39867
+ if (!artifact) {
39868
+ this.logger.warn?.("[ArtifactEnvironmentRegistry] artifact not found", { projectId });
39869
+ return null;
39870
+ }
39871
+ artifactProjectId = artifact.projectId ?? projectId;
39872
+ if (!runtime) runtime = artifact.runtime ?? extractRuntimeFromMetadata(artifact.metadata);
39873
+ if (!organizationId) organizationId = artifact.runtime?.organizationId;
39874
+ if (!host) host = artifact.runtime?.hostname;
39875
+ }
39876
+ if (!runtime || !runtime.databaseUrl || !runtime.databaseDriver) {
39877
+ this.logger.warn?.("[ArtifactEnvironmentRegistry] no runtime config for project", { projectId });
39878
+ return null;
39879
+ }
39880
+ const driver = await createDriver(runtime.databaseDriver, runtime.databaseUrl, runtime.databaseAuthToken ?? "");
39881
+ const projectRow = {
39882
+ id: artifactProjectId,
39883
+ organization_id: organizationId,
39884
+ hostname: host,
39885
+ database_url: runtime.databaseUrl,
39886
+ database_driver: runtime.databaseDriver,
39887
+ metadata: runtime.metadata
39888
+ };
39889
+ return {
39890
+ projectId: artifactProjectId,
39891
+ driver,
39892
+ project: projectRow,
39893
+ expiresAt: Date.now() + this.cacheTTL
39894
+ };
39895
+ }
39896
+ };
39897
+ function extractRuntimeFromMetadata(metadata) {
39898
+ const datasources = metadata?.datasources;
39899
+ if (!Array.isArray(datasources) || datasources.length === 0) return void 0;
39900
+ const mapping = metadata?.datasourceMapping;
39901
+ let preferredName;
39902
+ if (mapping) {
39903
+ const def = mapping.find((m) => m?.default === true);
39904
+ if (def?.datasource) preferredName = def.datasource;
39905
+ }
39906
+ const ds = preferredName ? datasources.find((d) => d?.name === preferredName) : datasources[0];
39907
+ if (!ds || typeof ds !== "object") return void 0;
39908
+ const config = ds.config ?? {};
39909
+ const url = config.url ?? config.connectionString ?? config.connection ?? config.filename;
39910
+ const driver = ds.driver;
39911
+ if (typeof driver !== "string" || typeof url !== "string") return void 0;
39912
+ return {
39913
+ databaseDriver: driver,
39914
+ databaseUrl: url,
39915
+ databaseAuthToken: typeof config.authToken === "string" ? config.authToken : void 0
39916
+ };
39917
+ }
39918
+ async function createDriver(driverType, databaseUrl, authToken) {
39919
+ switch (driverType) {
39920
+ case "memory": {
39921
+ const { InMemoryDriver } = await import("@objectstack/driver-memory");
39922
+ const dbName = databaseUrl.replace(/^memory:\/\//, "").trim();
39923
+ const filePath = dbName ? resolvePathNode(process.cwd(), ".objectstack/data/projects", `${dbName}.json`) : void 0;
39924
+ return new InMemoryDriver({
39925
+ persistence: filePath ? { type: "file", path: filePath } : "file"
39926
+ });
39927
+ }
39928
+ case "sqlite":
39929
+ case "sql": {
39930
+ const filePath = databaseUrl.replace(/^file:/, "").replace(/^sql:\/\//, "");
39931
+ const { SqlDriver } = await import("@objectstack/driver-sql");
39932
+ return new SqlDriver({
39933
+ client: "better-sqlite3",
39934
+ connection: { filename: filePath },
39935
+ useNullAsDefault: true
39936
+ });
39937
+ }
39938
+ case "libsql":
39939
+ case "turso": {
39940
+ const { TursoDriver } = await import("@objectstack/driver-turso");
39941
+ return new TursoDriver({ url: databaseUrl, authToken });
39942
+ }
39943
+ case "postgres":
39944
+ case "postgresql":
39945
+ case "pg": {
39946
+ const { SqlDriver } = await import("@objectstack/driver-sql");
39947
+ return new SqlDriver({
39948
+ client: "pg",
39949
+ connection: databaseUrl,
39950
+ pool: { min: 0, max: 5 }
39951
+ });
39952
+ }
39953
+ case "mongodb":
39954
+ case "mongo": {
39955
+ const { MongoDBDriver: MongoDBDriver2 } = await Promise.resolve().then(() => (init_dist(), dist_exports));
39956
+ return new MongoDBDriver2({ url: databaseUrl });
39957
+ }
39958
+ default:
39959
+ throw new Error(`[ArtifactEnvironmentRegistry] Unsupported driver type: ${driverType}`);
39960
+ }
39961
+ }
39962
+
39963
+ // src/cloud/artifact-kernel-factory.ts
39964
+ init_driver_plugin();
39965
+ init_app_plugin();
39966
+ import { createHmac as createHmac2 } from "crypto";
39967
+ import { ObjectKernel as ObjectKernel3 } from "@objectstack/core";
39968
+
39969
+ // src/cloud/capability-loader.ts
39970
+ var CAPABILITY_PROVIDERS = {
39971
+ automation: {
39972
+ pkg: "@objectstack/service-automation",
39973
+ export: "AutomationServicePlugin",
39974
+ extras: [
39975
+ { pkg: "@objectstack/service-automation", export: "CrudNodesPlugin" },
39976
+ { pkg: "@objectstack/service-automation", export: "LogicNodesPlugin" },
39977
+ { pkg: "@objectstack/service-automation", export: "HttpConnectorPlugin" },
39978
+ { pkg: "@objectstack/service-automation", export: "ScreenNodesPlugin" }
39979
+ ]
39980
+ },
39981
+ ai: {
39982
+ pkg: "@objectstack/service-ai",
39983
+ export: "AIServicePlugin"
39984
+ },
39985
+ analytics: {
39986
+ pkg: "@objectstack/service-analytics",
39987
+ export: "AnalyticsServicePlugin",
39988
+ configKey: "analyticsCubes"
39989
+ },
39990
+ audit: {
39991
+ pkg: "@objectstack/plugin-audit",
39992
+ export: "AuditPlugin"
39993
+ },
39994
+ cache: {
39995
+ pkg: "@objectstack/service-cache",
39996
+ export: "CacheServicePlugin"
39997
+ },
39998
+ storage: {
39999
+ pkg: "@objectstack/service-storage",
40000
+ export: "StorageServicePlugin"
40001
+ },
40002
+ queue: {
40003
+ pkg: "@objectstack/service-queue",
40004
+ export: "QueueServicePlugin"
40005
+ },
40006
+ job: {
40007
+ pkg: "@objectstack/service-job",
40008
+ export: "JobServicePlugin"
40009
+ },
40010
+ realtime: {
40011
+ pkg: "@objectstack/service-realtime",
40012
+ export: "RealtimeServicePlugin"
40013
+ },
40014
+ feed: {
40015
+ pkg: "@objectstack/service-feed",
40016
+ export: "FeedServicePlugin"
40017
+ },
40018
+ settings: {
40019
+ pkg: "@objectstack/service-settings",
40020
+ export: "SettingsServicePlugin"
40021
+ }
40022
+ };
40023
+ async function loadCapabilities(opts) {
40024
+ const { kernel, requires, bundle, projectId } = opts;
40025
+ const logger = opts.logger ?? console;
40026
+ const installed = [];
40027
+ for (const cap of requires) {
40028
+ const spec = CAPABILITY_PROVIDERS[cap];
40029
+ if (!spec) {
40030
+ continue;
40031
+ }
40032
+ try {
40033
+ const mod = await import(
40034
+ /* webpackIgnore: true */
40035
+ spec.pkg
40036
+ );
40037
+ const Ctor = mod[spec.export];
40038
+ if (!Ctor) {
40039
+ logger.warn?.(
40040
+ `[CapabilityLoader] '${cap}': package '${spec.pkg}' did not export '${spec.export}'`,
40041
+ { projectId }
40042
+ );
40043
+ continue;
40044
+ }
40045
+ let arg;
40046
+ if (spec.configKey) {
40047
+ const v = bundle[spec.configKey];
40048
+ if (spec.configKey === "analyticsCubes") {
40049
+ arg = { cubes: Array.isArray(v) ? v : [] };
40050
+ } else if (v !== void 0) {
40051
+ arg = v;
40052
+ }
40053
+ }
40054
+ await kernel.use(arg !== void 0 ? new Ctor(arg) : new Ctor());
40055
+ installed.push(spec.export);
40056
+ if (spec.extras) {
40057
+ for (const ex of spec.extras) {
40058
+ try {
40059
+ const exMod = await import(
40060
+ /* webpackIgnore: true */
40061
+ ex.pkg
40062
+ );
40063
+ const ExCtor = exMod[ex.export];
40064
+ if (ExCtor) {
40065
+ await kernel.use(new ExCtor());
40066
+ installed.push(ex.export);
40067
+ }
40068
+ } catch {
40069
+ }
40070
+ }
40071
+ }
40072
+ logger.info?.(
40073
+ `[CapabilityLoader] '${cap}' installed (${spec.export}${spec.extras ? " + " + spec.extras.length + " extras" : ""})`,
40074
+ { projectId }
40075
+ );
40076
+ } catch (err) {
40077
+ const msg = err?.message ?? String(err);
40078
+ if (msg.includes("Cannot find module") || msg.includes("ERR_MODULE_NOT_FOUND")) {
40079
+ logger.warn?.(
40080
+ `[CapabilityLoader] '${cap}' requested but '${spec.pkg}' not installed in host \u2014 skipped`,
40081
+ { projectId }
40082
+ );
40083
+ } else {
40084
+ logger.error?.(
40085
+ `[CapabilityLoader] '${cap}' load failed: ${msg}`,
40086
+ { projectId }
40087
+ );
40088
+ }
40089
+ }
40090
+ }
40091
+ return installed;
40092
+ }
40093
+
40094
+ // src/cloud/artifact-kernel-factory.ts
40095
+ init_platform_sso();
40096
+ function deriveProjectAuthSecret(baseSecret, projectId) {
40097
+ return createHmac2("sha256", baseSecret).update(`project:${projectId}`).digest("hex");
40098
+ }
40099
+ var ArtifactKernelFactory = class {
40100
+ constructor(config) {
40101
+ this.client = config.client;
40102
+ this.envRegistry = config.envRegistry;
40103
+ this.logger = config.logger ?? console;
40104
+ this.kernelConfig = config.kernelConfig;
40105
+ this.authBaseSecret = (config.authBaseSecret ?? process.env.OS_AUTH_SECRET ?? process.env.AUTH_SECRET ?? "").trim();
40106
+ }
40107
+ async create(projectId) {
40108
+ let cached = this.envRegistry.peekById(projectId);
40109
+ if (!cached) {
40110
+ const driver2 = await this.envRegistry.resolveById(projectId);
40111
+ if (!driver2) {
40112
+ throw new Error(`[ArtifactKernelFactory] Could not resolve driver for project '${projectId}'`);
40113
+ }
40114
+ cached = this.envRegistry.peekById(projectId);
40115
+ if (!cached) {
40116
+ throw new Error(`[ArtifactKernelFactory] envRegistry returned a driver but no cached entry for '${projectId}'`);
40117
+ }
40118
+ }
40119
+ const driver = cached.driver;
40120
+ const project = cached.project;
40121
+ const artifact = await this.client.fetchArtifact(projectId);
40122
+ if (!artifact) {
40123
+ throw new Error(`[ArtifactKernelFactory] Artifact not available for project '${projectId}'`);
40124
+ }
40125
+ const { ObjectQLPlugin } = await import("@objectstack/objectql");
40126
+ const { MetadataPlugin } = await import("@objectstack/metadata");
40127
+ const kernel = new ObjectKernel3(this.kernelConfig);
40128
+ await kernel.use(new DriverPlugin(driver, { datasourceName: "cloud" }));
40129
+ await kernel.use(new ObjectQLPlugin({ projectId, skipSchemaSync: false }));
40130
+ await kernel.use(new MetadataPlugin({
40131
+ watch: false,
40132
+ projectId,
40133
+ organizationId: project.organization_id,
40134
+ // ADR-0005: customization overlays (user-created views, dashboards,
40135
+ // edited objects, ...) are persisted by
40136
+ // ObjectStackProtocolImplementation.saveMetaItem on whichever
40137
+ // engine the protocol is attached to. For per-project kernels that
40138
+ // means the project's own DB, so the sys_metadata + history tables
40139
+ // MUST be provisioned here. The previous `false` setting caused
40140
+ // "no such table: sys_metadata" errors on any PUT /api/v1/meta/*
40141
+ // call (e.g. Studio "Create View") against a project deployment.
40142
+ registerSystemObjects: true
40143
+ }));
40144
+ if (this.authBaseSecret) {
40145
+ try {
40146
+ const { AuthPlugin } = await import("@objectstack/plugin-auth");
40147
+ const projectSecret = deriveProjectAuthSecret(this.authBaseSecret, projectId);
40148
+ const baseUrl = project.hostname ? project.hostname.startsWith("http") ? project.hostname : /(\.|^)localhost(:\d+)?$/i.test(project.hostname) ? (() => {
40149
+ const runtimePort = (process.env.OS_RUNTIME_PORT ?? "").trim();
40150
+ const hasPort = /:\d+$/.test(project.hostname);
40151
+ const hostWithPort = hasPort || !runtimePort ? project.hostname : `${project.hostname}:${runtimePort}`;
40152
+ return `http://${hostWithPort}`;
40153
+ })() : `https://${project.hostname}` : void 0;
40154
+ const trustedOriginsList = [];
40155
+ if (baseUrl) trustedOriginsList.push(baseUrl);
40156
+ const platformOrigins = (process.env.OS_TRUSTED_ORIGINS ?? "").split(",").map((s) => s.trim()).filter(Boolean);
40157
+ for (const o of platformOrigins) {
40158
+ if (!trustedOriginsList.includes(o)) trustedOriginsList.push(o);
40159
+ }
40160
+ const rootDomain = (process.env.OS_ROOT_DOMAIN ?? "").trim().replace(/^https?:\/\//, "");
40161
+ if (rootDomain) {
40162
+ const wildcard = `https://*.${rootDomain}`;
40163
+ if (!trustedOriginsList.includes(wildcard)) trustedOriginsList.push(wildcard);
40164
+ }
40165
+ if (project.hostname) {
40166
+ const bareHost = project.hostname.replace(/^https?:\/\//, "");
40167
+ if (bareHost.endsWith(".localhost") || bareHost === "localhost") {
40168
+ trustedOriginsList.push(`http://${bareHost}`);
40169
+ trustedOriginsList.push(`http://${bareHost}:*`);
40170
+ trustedOriginsList.push(`https://${bareHost}:*`);
40171
+ }
40172
+ }
40173
+ const platformSsoEnabled = String(
40174
+ process.env.OS_PLATFORM_SSO ?? "true"
40175
+ ).toLowerCase() !== "false";
40176
+ const cloudBaseUrl = (process.env.OS_CLOUD_URL ?? "").trim().replace(/\/+$/, "");
40177
+ const oidcProviders = platformSsoEnabled && cloudBaseUrl && /^https?:\/\//.test(cloudBaseUrl) ? [{
40178
+ providerId: PLATFORM_SSO_PROVIDER_ID,
40179
+ name: "ObjectStack",
40180
+ discoveryUrl: `${cloudBaseUrl}/.well-known/openid-configuration`,
40181
+ clientId: derivePlatformSsoClientId(projectId),
40182
+ clientSecret: derivePlatformSsoClientSecret(this.authBaseSecret, projectId),
40183
+ scopes: ["openid", "email", "profile"]
40184
+ }] : void 0;
40185
+ await kernel.use(new AuthPlugin({
40186
+ secret: projectSecret,
40187
+ baseUrl,
40188
+ // Project kernel has no http-server (host owns it). The
40189
+ // dispatcher's handleAuth path resolves `auth` via
40190
+ // getService and invokes the handler directly — route
40191
+ // registration is unnecessary and would warn.
40192
+ registerRoutes: false,
40193
+ // Identity tables live in the project's own DB — keep
40194
+ // sys_user/sys_session local to this kernel.
40195
+ manifestDatasource: "default",
40196
+ // Cookie scope: default to the project's own host. We
40197
+ // intentionally do NOT pass crossSubDomainCookies here
40198
+ // so cookies stay isolated per project subdomain.
40199
+ trustedOrigins: trustedOriginsList.length ? trustedOriginsList : void 0,
40200
+ ...oidcProviders ? { oidcProviders } : {}
40201
+ }));
40202
+ if (oidcProviders) {
40203
+ this.logger.info?.("[ArtifactKernelFactory] platform SSO wired", {
40204
+ projectId,
40205
+ cloudBaseUrl
40206
+ });
40207
+ }
40208
+ } catch (err) {
40209
+ this.logger.warn?.("[ArtifactKernelFactory] AuthPlugin not registered", {
40210
+ projectId,
40211
+ error: err?.message
40212
+ });
40213
+ }
40214
+ } else {
40215
+ this.logger.warn?.("[ArtifactKernelFactory] OS_AUTH_SECRET not set \u2014 per-project AuthPlugin skipped (auth endpoints will return 404)", { projectId });
40216
+ }
40217
+ try {
40218
+ const { SecurityPlugin } = await import("@objectstack/plugin-security");
40219
+ const multiTenant = String(process.env.OS_MULTI_TENANT ?? "true").toLowerCase() !== "false";
40220
+ await kernel.use(new SecurityPlugin({ multiTenant }));
40221
+ } catch (err) {
40222
+ this.logger.warn?.("[ArtifactKernelFactory] SecurityPlugin not registered", {
40223
+ projectId,
40224
+ error: err?.message
40225
+ });
40226
+ }
40227
+ const projectName = project.hostname ?? projectId;
40228
+ const bundle = artifact.metadata;
40229
+ const sys = bundle?.manifest ?? bundle;
40230
+ const packageId = sys?.packageId ?? sys?.package_id ?? bundle?.packageId;
40231
+ const i18nCfg = bundle?.i18n ?? sys?.i18n ?? {};
40232
+ const trArr = Array.isArray(bundle?.translations) ? bundle.translations : Array.isArray(sys?.translations) ? sys.translations : [];
40233
+ try {
40234
+ const { I18nServicePlugin } = await import("@objectstack/service-i18n");
40235
+ await kernel.use(new I18nServicePlugin({
40236
+ defaultLocale: i18nCfg.defaultLocale,
40237
+ fallbackLocale: i18nCfg.fallbackLocale ?? i18nCfg.defaultLocale ?? "en",
40238
+ // Routes are dispatched by HttpDispatcher.handleI18n via
40239
+ // kernel.getService('i18n'); the host worker owns the
40240
+ // HTTP server. Skip self-registration to avoid warnings.
40241
+ registerRoutes: false
40242
+ }));
40243
+ console.warn(
40244
+ `[ArtifactKernelFactory] I18nServicePlugin registered (project=${projectId}, translations=${trArr.length}, defaultLocale=${i18nCfg.defaultLocale ?? "en"})`
40245
+ );
40246
+ } catch (err) {
40247
+ this.logger.warn?.("[ArtifactKernelFactory] I18nServicePlugin not registered", {
40248
+ projectId,
40249
+ error: err?.message
40250
+ });
40251
+ }
40252
+ const requiresRaw = (Array.isArray(bundle?.requires) ? bundle.requires : null) ?? (Array.isArray(sys?.requires) ? sys.requires : null) ?? [];
40253
+ const requires = requiresRaw.filter((x) => typeof x === "string" && x.length > 0);
40254
+ if (requires.length > 0) {
40255
+ const installed = await loadCapabilities({
40256
+ kernel,
40257
+ requires,
40258
+ bundle: { ...bundle ?? {}, ...sys ?? {} },
40259
+ logger: this.logger,
40260
+ projectId
40261
+ });
40262
+ this.logger.info?.("[ArtifactKernelFactory] capabilities loaded", {
40263
+ projectId,
40264
+ requires,
40265
+ installed
40266
+ });
40267
+ }
40268
+ await kernel.use(new AppPlugin(bundle, {
40269
+ projectId,
40270
+ organizationId: project.organization_id ?? "",
40271
+ projectName,
40272
+ packageId,
40273
+ source: packageId ? "package" : "user"
40274
+ }));
40275
+ await kernel.bootstrap();
40276
+ try {
40277
+ const projMeta = typeof project?.metadata === "string" ? JSON.parse(project.metadata) : project?.metadata ?? {};
40278
+ const ownerSeed = projMeta?.ownerSeed;
40279
+ const orgSeed = projMeta?.orgSeed;
40280
+ if (orgSeed?.id && orgSeed?.name) {
40281
+ try {
40282
+ const { seedProjectOrganization: seedProjectOrganization2 } = await Promise.resolve().then(() => (init_project_org_seed(), project_org_seed_exports));
40283
+ await seedProjectOrganization2(kernel, orgSeed, this.logger);
40284
+ } catch (e) {
40285
+ this.logger.warn?.("[ArtifactKernelFactory] orgSeed threw", {
40286
+ projectId,
40287
+ error: e?.message
40288
+ });
40289
+ }
40290
+ }
40291
+ if (ownerSeed?.userId && ownerSeed?.email) {
40292
+ try {
40293
+ const { seedProjectOwner: seedProjectOwner2 } = await Promise.resolve().then(() => (init_project_owner_seed(), project_owner_seed_exports));
40294
+ await seedProjectOwner2(kernel, ownerSeed, this.logger);
40295
+ } catch (e) {
40296
+ this.logger.warn?.("[ArtifactKernelFactory] ownerSeed threw", {
40297
+ projectId,
40298
+ error: e?.message
40299
+ });
40300
+ }
40301
+ if (orgSeed?.id) {
40302
+ try {
40303
+ const { seedProjectMember: seedProjectMember2 } = await Promise.resolve().then(() => (init_project_org_seed(), project_org_seed_exports));
40304
+ await seedProjectMember2(
40305
+ kernel,
40306
+ { userId: ownerSeed.userId, organizationId: orgSeed.id, role: "owner" },
40307
+ this.logger
40308
+ );
40309
+ } catch (e) {
40310
+ this.logger.warn?.("[ArtifactKernelFactory] memberSeed threw", {
40311
+ projectId,
40312
+ error: e?.message
40313
+ });
40314
+ }
40315
+ }
40316
+ }
40317
+ } catch (err) {
40318
+ this.logger.warn?.("[ArtifactKernelFactory] owner/org seed skipped", {
40319
+ projectId,
40320
+ error: err?.message
40321
+ });
40322
+ }
40323
+ try {
40324
+ const datasetsNow = (() => {
40325
+ try {
40326
+ return kernel.getService?.("seed-datasets");
40327
+ } catch {
40328
+ return void 0;
40329
+ }
40330
+ })();
40331
+ const replayer = (() => {
40332
+ try {
40333
+ return kernel.getService?.("seed-replayer");
40334
+ } catch {
40335
+ return void 0;
40336
+ }
40337
+ })();
40338
+ if (Array.isArray(datasetsNow) && datasetsNow.length > 0 && typeof replayer === "function") {
40339
+ const projMetaRaw = project?.metadata;
40340
+ const projMeta = typeof projMetaRaw === "string" ? (() => {
40341
+ try {
40342
+ return JSON.parse(projMetaRaw);
40343
+ } catch {
40344
+ return {};
40345
+ }
40346
+ })() : projMetaRaw ?? {};
40347
+ let primaryOrgId = projMeta?.orgSeed?.id;
40348
+ if (!primaryOrgId) {
40349
+ try {
40350
+ const ql = kernel.getService?.("objectql");
40351
+ if (ql?.find) {
40352
+ const rows = await ql.find("sys_organization", { limit: 5, orderBy: [{ field: "created_at", direction: "asc" }] });
40353
+ const list = Array.isArray(rows) ? rows : rows?.value ?? rows?.records ?? [];
40354
+ if (Array.isArray(list) && list.length > 0 && list[0]?.id) {
40355
+ primaryOrgId = String(list[0].id);
40356
+ }
40357
+ }
40358
+ } catch {
40359
+ }
40360
+ }
40361
+ if (primaryOrgId) {
40362
+ try {
40363
+ const summary = await replayer(primaryOrgId);
40364
+ const inserted = summary?.inserted ?? 0;
40365
+ const updated = summary?.updated ?? 0;
40366
+ const errs = summary?.errors?.length ?? 0;
40367
+ if (inserted > 0 || updated > 0 || errs > 0) {
40368
+ this.logger.info?.("[ArtifactKernelFactory] post-bootstrap seed replay", {
40369
+ projectId,
40370
+ organizationId: primaryOrgId,
40371
+ datasets: datasetsNow.length,
40372
+ inserted,
40373
+ updated,
40374
+ errors: errs
40375
+ });
40376
+ }
40377
+ } catch (e) {
40378
+ this.logger.warn?.("[ArtifactKernelFactory] post-bootstrap seed replay failed", {
40379
+ projectId,
40380
+ organizationId: primaryOrgId,
40381
+ error: e?.message
40382
+ });
40383
+ }
40384
+ }
40385
+ }
40386
+ } catch (err) {
40387
+ this.logger.warn?.("[ArtifactKernelFactory] post-bootstrap seed step threw", {
40388
+ projectId,
40389
+ error: err?.message
40390
+ });
40391
+ }
40392
+ let i18nSvc = null;
40393
+ try {
40394
+ i18nSvc = kernel.getService?.("i18n");
40395
+ } catch {
40396
+ i18nSvc = null;
40397
+ }
40398
+ try {
40399
+ if (i18nSvc && typeof i18nSvc.loadTranslations === "function") {
40400
+ if (i18nCfg.defaultLocale && typeof i18nSvc.setDefaultLocale === "function") {
40401
+ i18nSvc.setDefaultLocale(i18nCfg.defaultLocale);
40402
+ }
40403
+ let loaded = 0;
40404
+ for (const tbundle of trArr) {
40405
+ if (!tbundle || typeof tbundle !== "object") continue;
40406
+ for (const [locale, data] of Object.entries(tbundle)) {
40407
+ if (data && typeof data === "object") {
40408
+ try {
40409
+ i18nSvc.loadTranslations(locale, data);
40410
+ loaded++;
40411
+ } catch (err) {
40412
+ this.logger.warn?.("[ArtifactKernelFactory] i18n loadTranslations failed", {
40413
+ projectId,
40414
+ locale,
40415
+ error: err?.message
40416
+ });
40417
+ }
40418
+ }
40419
+ }
40420
+ }
40421
+ if (loaded > 0) {
40422
+ this.logger.info?.("[ArtifactKernelFactory] i18n direct-load complete", {
40423
+ projectId,
40424
+ locales: loaded,
40425
+ bundles: trArr.length
40426
+ });
40427
+ }
40428
+ }
40429
+ } catch (err) {
40430
+ this.logger.warn?.("[ArtifactKernelFactory] i18n direct-load failed", {
40431
+ projectId,
40432
+ error: err?.message
40433
+ });
40434
+ }
40435
+ this.logger.info?.("[ArtifactKernelFactory] kernel ready", {
40436
+ projectId,
40437
+ commitId: artifact.commitId,
40438
+ checksum: artifact.checksum,
40439
+ authEnabled: Boolean(this.authBaseSecret)
40440
+ });
40441
+ return kernel;
40442
+ }
40443
+ };
40444
+
40445
+ // src/cloud/auth-proxy-plugin.ts
40446
+ var AUTH_PREFIX = "/api/v1/auth";
40447
+ function pickHandler(svc) {
40448
+ if (!svc) return void 0;
40449
+ if (typeof svc.handleRequest === "function") return svc.handleRequest.bind(svc);
40450
+ if (typeof svc.handler === "function") return svc.handler.bind(svc);
40451
+ if (svc.api && typeof svc.api.handler === "function") return svc.api.handler.bind(svc.api);
40452
+ if (svc.auth && typeof svc.auth.handler === "function") return svc.auth.handler.bind(svc.auth);
40453
+ return void 0;
40454
+ }
40455
+ async function resolveAuthHandler(svc) {
40456
+ const direct = pickHandler(svc);
40457
+ if (direct) return direct;
40458
+ if (typeof svc?.getApi === "function") {
40459
+ try {
40460
+ const api = await svc.getApi();
40461
+ return pickHandler(api) ?? pickHandler({ api });
40462
+ } catch {
40463
+ return void 0;
40464
+ }
40465
+ }
40466
+ return void 0;
40467
+ }
40468
+ var AuthProxyPlugin = class {
40469
+ constructor() {
40470
+ this.name = "com.objectstack.runtime.auth-proxy";
40471
+ this.version = "1.0.0";
40472
+ this.init = async (_ctx) => {
40473
+ };
40474
+ this.start = async (ctx) => {
40475
+ ctx.hook("kernel:ready", async () => {
40476
+ let httpServer;
40477
+ try {
40478
+ httpServer = ctx.getService("http-server");
40479
+ } catch {
40480
+ ctx.logger?.warn?.("[AuthProxyPlugin] http-server not available \u2014 auth routes not mounted");
40481
+ return;
40482
+ }
40483
+ if (!httpServer || typeof httpServer.getRawApp !== "function") {
40484
+ ctx.logger?.warn?.("[AuthProxyPlugin] http-server missing getRawApp() \u2014 auth routes not mounted");
40485
+ return;
40486
+ }
40487
+ const rawApp = httpServer.getRawApp();
40488
+ const kernelManager = ctx.getService("kernel-manager");
40489
+ const envRegistry = ctx.getService("env-registry");
40490
+ const handler = async (c) => {
40491
+ try {
40492
+ const url = new URL(c.req.url);
40493
+ const host = url.hostname;
40494
+ let projectId;
40495
+ try {
40496
+ const env = await envRegistry.resolveByHostname(host);
40497
+ projectId = env?.projectId;
40498
+ } catch {
40499
+ }
40500
+ if (!projectId) {
40501
+ return c.json({ error: "project_not_found", host }, 404);
40502
+ }
40503
+ const projectKernel = await kernelManager.getOrCreate(projectId);
40504
+ let authSvc;
40505
+ try {
40506
+ authSvc = await projectKernel.getServiceAsync?.("auth");
40507
+ } catch {
40508
+ authSvc = void 0;
40509
+ }
40510
+ if (!authSvc) {
40511
+ try {
40512
+ authSvc = projectKernel.getService?.("auth");
40513
+ } catch {
40514
+ }
40515
+ }
40516
+ const subPath = url.pathname.startsWith(AUTH_PREFIX + "/") ? url.pathname.substring(AUTH_PREFIX.length + 1) : "";
40517
+ if (c.req.method === "GET" && (subPath === "config" || subPath === "bootstrap-status")) {
40518
+ if (subPath === "config") {
40519
+ try {
40520
+ const config = typeof authSvc?.getPublicConfig === "function" ? authSvc.getPublicConfig() : null;
40521
+ if (config) {
40522
+ return c.json({ success: true, data: config });
40523
+ }
40524
+ return c.json({ success: false, error: { code: "auth_config_unavailable", message: "AuthManager has no getPublicConfig()" } }, 503);
40525
+ } catch (e) {
40526
+ return c.json({ success: false, error: { code: "auth_config_error", message: String(e?.message ?? e) } }, 500);
40527
+ }
40528
+ }
40529
+ try {
40530
+ try {
40531
+ const pubCfg = typeof authSvc?.getPublicConfig === "function" ? authSvc.getPublicConfig() : null;
40532
+ const ssoProviders = Array.isArray(pubCfg?.socialProviders) ? pubCfg.socialProviders : [];
40533
+ const ssoWired = ssoProviders.some(
40534
+ (p) => p?.enabled !== false && p?.id === "objectstack-cloud"
40535
+ );
40536
+ if (ssoWired) {
40537
+ return c.json({ hasOwner: true });
40538
+ }
40539
+ } catch {
40540
+ }
40541
+ const dataEngine = typeof authSvc?.getDataEngine === "function" ? authSvc.getDataEngine() : null;
40542
+ if (!dataEngine || typeof dataEngine.count !== "function") {
40543
+ return c.json({ hasOwner: true });
40544
+ }
40545
+ const count = await dataEngine.count("sys_user", {});
40546
+ return c.json({ hasOwner: (count ?? 0) > 0 });
40547
+ } catch {
40548
+ return c.json({ hasOwner: true });
40549
+ }
40550
+ }
40551
+ const fn = await resolveAuthHandler(authSvc);
40552
+ if (!fn) {
40553
+ return c.json({ error: "auth_service_unavailable", projectId }, 503);
40554
+ }
40555
+ const resp = await fn(c.req.raw);
40556
+ const rootDomain = process.env.OS_ROOT_DOMAIN || "";
40557
+ if (rootDomain) {
40558
+ const leakyDomain = rootDomain.startsWith(".") ? rootDomain : `.${rootDomain}`;
40559
+ const leakyNames = [
40560
+ "__Secure-better-auth.session_token",
40561
+ "better-auth.session_token",
40562
+ "__Secure-better-auth.state",
40563
+ "better-auth.state",
40564
+ "__Secure-better-auth.csrf_token",
40565
+ "better-auth.csrf_token"
40566
+ ];
40567
+ try {
40568
+ for (const n of leakyNames) {
40569
+ const isSecure = n.startsWith("__Secure-");
40570
+ const attrs = `Max-Age=0; Path=/; Domain=${leakyDomain}; SameSite=Lax${isSecure ? "; Secure" : ""}`;
40571
+ resp.headers?.append?.("Set-Cookie", `${n}=; ${attrs}`);
40572
+ }
40573
+ } catch {
40574
+ }
40575
+ }
40576
+ return resp;
40577
+ } catch (err) {
40578
+ ctx.logger?.error?.("[AuthProxyPlugin] auth dispatch failed", {
40579
+ error: err?.message,
40580
+ stack: err?.stack
40581
+ });
40582
+ return c.json({
40583
+ error: "auth_dispatch_failed",
40584
+ message: err?.message ?? String(err)
40585
+ }, 500);
40586
+ }
40587
+ };
40588
+ if (typeof rawApp.all === "function") {
40589
+ rawApp.all(`${AUTH_PREFIX}/*`, handler);
40590
+ } else {
40591
+ for (const m of ["get", "post", "put", "delete", "patch", "options"]) {
40592
+ try {
40593
+ rawApp[m]?.(`${AUTH_PREFIX}/*`, handler);
40594
+ } catch {
40595
+ }
40596
+ }
40597
+ }
40598
+ ctx.logger?.info?.(`[AuthProxyPlugin] auth proxy mounted at ${AUTH_PREFIX}/*`);
40599
+ });
40600
+ };
40601
+ }
40602
+ };
40603
+
40604
+ // src/cloud/file-artifact-api-client.ts
40605
+ import { readFile as readFile2, stat } from "fs/promises";
40606
+ import { resolve as resolvePath4 } from "path";
40607
+ var FileArtifactApiClient = class {
40608
+ constructor(config = {}) {
40609
+ const cwd = process.cwd();
40610
+ this.artifactPath = resolvePath4(
40611
+ cwd,
40612
+ config.artifactPath ?? process.env.OS_ARTIFACT_PATH ?? "dist/objectstack.json"
40613
+ );
40614
+ this.projectId = config.projectId ?? process.env.OS_PROJECT_ID ?? "proj_local";
40615
+ this.organizationId = config.organizationId ?? process.env.OS_ORGANIZATION_ID ?? "org_local";
40616
+ this.overrideRuntime = config.runtime;
40617
+ this.watch = config.watch ?? true;
40618
+ this.logger = config.logger ?? console;
40619
+ }
40620
+ async resolveHostname(_host) {
40621
+ const runtime = this.overrideRuntime ?? await this.readRuntimeFromArtifact();
40622
+ return {
40623
+ projectId: this.projectId,
40624
+ organizationId: this.organizationId,
40625
+ ...runtime ? { runtime } : {}
40626
+ };
40627
+ }
40628
+ async fetchArtifact(_projectId, _opts) {
40629
+ return this.loadArtifact();
40630
+ }
40631
+ async lookupProjectByShortId(_shortId) {
40632
+ return { projectId: this.projectId, organizationId: this.organizationId };
40633
+ }
40634
+ async fetchBranchHead(_projectId, _branchName) {
40635
+ const artifact = await this.loadArtifact();
40636
+ return artifact ? { commitId: artifact.commitId ?? "local", publishedAt: null } : null;
40637
+ }
40638
+ invalidate(_projectId) {
40639
+ this.cached = void 0;
40640
+ }
40641
+ clear() {
40642
+ this.cached = void 0;
40643
+ }
40644
+ async loadArtifact() {
40645
+ try {
40646
+ const stats = await stat(this.artifactPath);
40647
+ const mtimeMs = stats.mtimeMs;
40648
+ if (!this.watch && this.cached) return this.cached.response;
40649
+ if (this.cached && this.cached.mtimeMs === mtimeMs) return this.cached.response;
40650
+ const raw = await readFile2(this.artifactPath, "utf8");
40651
+ const parsed = JSON.parse(raw);
40652
+ const isEnvelope = parsed && typeof parsed === "object" && typeof parsed.metadata === "object" && parsed.metadata !== null;
40653
+ const metadata = isEnvelope ? parsed.metadata : parsed;
40654
+ const runtime = this.overrideRuntime ?? (isEnvelope ? parsed.runtime : void 0) ?? this.deriveRuntimeFromMetadata(metadata) ?? this.defaultLocalSqliteRuntime();
40655
+ const response = {
40656
+ schemaVersion: parsed.schemaVersion ?? "1",
40657
+ projectId: parsed.projectId ?? this.projectId,
40658
+ commitId: parsed.commitId ?? "local",
40659
+ checksum: parsed.checksum ?? "",
40660
+ publishedAt: parsed.publishedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
40661
+ metadata,
40662
+ functions: parsed.functions,
40663
+ manifest: parsed.manifest,
40664
+ runtime: {
40665
+ organizationId: this.organizationId,
40666
+ ...runtime
40667
+ }
40668
+ };
40669
+ this.cached = { mtimeMs, response };
40670
+ return response;
40671
+ } catch (err) {
40672
+ this.logger.error?.("[FileArtifactApiClient] failed to load artifact", {
40673
+ artifactPath: this.artifactPath,
40674
+ error: err?.message ?? err
40675
+ });
40676
+ return null;
40677
+ }
40678
+ }
40679
+ async readRuntimeFromArtifact() {
40680
+ const artifact = await this.loadArtifact();
40681
+ return artifact?.runtime;
40682
+ }
40683
+ deriveRuntimeFromMetadata(metadata) {
40684
+ const datasources = metadata?.datasources;
40685
+ if (!Array.isArray(datasources) || datasources.length === 0) return void 0;
40686
+ const mapping = metadata?.datasourceMapping;
40687
+ let preferredName;
40688
+ if (mapping) {
40689
+ const def = mapping.find((m) => m?.default === true);
40690
+ if (def?.datasource) preferredName = def.datasource;
40691
+ }
40692
+ const ds = preferredName ? datasources.find((d) => d?.name === preferredName) ?? datasources[0] : datasources[0];
40693
+ if (!ds || typeof ds !== "object") return void 0;
40694
+ const config = ds.config ?? {};
40695
+ const url = config.url ?? config.connectionString ?? config.connection ?? config.filename;
40696
+ const driver = ds.driver;
40697
+ if (typeof driver !== "string" || typeof url !== "string") return void 0;
40698
+ return {
40699
+ databaseDriver: driver,
40700
+ databaseUrl: url,
40701
+ databaseAuthToken: typeof config.authToken === "string" ? config.authToken : void 0
40702
+ };
40703
+ }
40704
+ defaultLocalSqliteRuntime() {
40705
+ const cwd = process.cwd();
40706
+ const dbPath = resolvePath4(cwd, ".objectstack/data", `${this.projectId}.db`);
40707
+ return {
40708
+ databaseDriver: "sqlite",
40709
+ databaseUrl: `file:${dbPath}`
40710
+ };
40711
+ }
40712
+ };
40713
+
40714
+ // src/cloud/objectos-stack.ts
40715
+ async function createHostEnginePlugins() {
40716
+ const { ObjectQLPlugin } = await import("@objectstack/objectql");
40717
+ const { DriverPlugin: DriverPlugin2 } = await Promise.resolve().then(() => (init_driver_plugin(), driver_plugin_exports));
40718
+ const { MetadataPlugin } = await import("@objectstack/metadata");
40719
+ const { InMemoryDriver } = await import("@objectstack/driver-memory");
40720
+ const driver = new InMemoryDriver();
40721
+ const driverName = "memory";
40722
+ const oqlRef = { ql: null };
40723
+ const objectql = {
40724
+ name: "com.objectstack.engine.objectql",
40725
+ version: "0.0.0",
40726
+ async init(ctx) {
40727
+ const plugin = new ObjectQLPlugin();
40728
+ this._inner = plugin;
40729
+ if (plugin.init) await plugin.init(ctx);
40730
+ oqlRef.ql = plugin.ql ?? plugin;
40731
+ },
40732
+ async start(ctx) {
40733
+ const plugin = this._inner;
40734
+ if (plugin?.start) await plugin.start(ctx);
40735
+ },
40736
+ async destroy() {
40737
+ const plugin = this._inner;
40738
+ if (plugin?.destroy) await plugin.destroy();
40739
+ else if (plugin?.stop) await plugin.stop();
40740
+ }
40741
+ };
40742
+ const datasourceMapping = {
40743
+ name: "objectos-host-datasource-mapping",
40744
+ version: "0.0.0",
40745
+ dependencies: ["com.objectstack.engine.objectql"],
40746
+ async init() {
40747
+ const ql = oqlRef.ql;
40748
+ if (ql?.setDatasourceMapping) {
40749
+ ql.setDatasourceMapping([
40750
+ { default: true, datasource: `com.objectstack.driver.${driverName}` }
40751
+ ]);
40752
+ }
40753
+ }
40754
+ };
40755
+ const driverPlugin = new DriverPlugin2(driver, driverName);
40756
+ const metadata = new MetadataPlugin({
40757
+ watch: false,
40758
+ // The host kernel is a routing shell. It doesn't own metadata —
40759
+ // every per-project kernel registers its own.
40760
+ registerSystemObjects: false
40761
+ });
40762
+ return [objectql, datasourceMapping, driverPlugin, metadata];
40763
+ }
40764
+ var ObjectOSProjectPlugin = class {
40765
+ constructor(config) {
40766
+ this.name = "com.objectstack.runtime.objectos-project";
40767
+ this.version = "1.0.0";
40768
+ this.init = async (ctx) => {
40769
+ const client = this.config.client ?? (this.config.controlPlaneUrl === "file" ? new FileArtifactApiClient({
40770
+ ...this.config.fileConfig ?? {},
40771
+ logger: ctx.logger
40772
+ }) : new ArtifactApiClient({
40773
+ controlPlaneUrl: this.config.controlPlaneUrl,
40774
+ apiKey: this.config.controlPlaneApiKey,
40775
+ cacheTtlMs: this.config.artifactCacheTtlMs,
40776
+ logger: ctx.logger
40777
+ }));
40778
+ this.client = client;
40779
+ const envRegistry = new ArtifactEnvironmentRegistry({
40780
+ client,
40781
+ cacheTtlMs: this.config.envCacheTtlMs,
40782
+ logger: ctx.logger
40783
+ });
40784
+ const factory = new ArtifactKernelFactory({
40785
+ client,
40786
+ envRegistry,
40787
+ logger: ctx.logger
40788
+ });
40789
+ const kernelManager = new KernelManager({
40790
+ factory,
40791
+ maxSize: this.config.kernelCacheSize,
40792
+ ttlMs: this.config.kernelTtlMs,
40793
+ logger: ctx.logger
40794
+ });
40795
+ this.kernelManager = kernelManager;
40796
+ ctx.registerService("env-registry", envRegistry);
40797
+ ctx.registerService("kernel-manager", kernelManager);
40798
+ ctx.registerService("artifact-api-client", client);
40799
+ ctx.logger.info?.("ObjectOSProjectPlugin: registered env-registry + kernel-manager", {
40800
+ mode: this.config.controlPlaneUrl === "file" ? "file" : "http",
40801
+ controlPlaneUrl: this.config.controlPlaneUrl
40802
+ });
40803
+ };
40804
+ this.destroy = async () => {
40805
+ try {
40806
+ await this.kernelManager?.evictAll();
40807
+ } catch {
40808
+ }
40809
+ try {
40810
+ this.client?.clear();
40811
+ } catch {
40812
+ }
40813
+ };
40814
+ this.config = config;
40815
+ }
40816
+ };
40817
+ async function createObjectOSStack(config) {
40818
+ if (!config.controlPlaneUrl && !config.client) {
40819
+ throw new Error("[createObjectOSStack] either controlPlaneUrl or client is required");
40820
+ }
40821
+ const merged = {
40822
+ ...config,
40823
+ kernelCacheSize: Number(process.env.OS_KERNEL_CACHE_SIZE ?? config.kernelCacheSize ?? 32),
40824
+ kernelTtlMs: Number(process.env.OS_KERNEL_TTL_MS ?? config.kernelTtlMs ?? 15 * 60 * 1e3),
40825
+ envCacheTtlMs: Number(process.env.OS_ENV_CACHE_TTL_MS ?? config.envCacheTtlMs ?? 5 * 60 * 1e3),
40826
+ artifactCacheTtlMs: Number(process.env.OS_ARTIFACT_CACHE_TTL_MS ?? config.artifactCacheTtlMs ?? 5 * 60 * 1e3)
40827
+ };
40828
+ const enginePlugins = await createHostEnginePlugins();
40829
+ return {
40830
+ plugins: [...enginePlugins, new ObjectOSProjectPlugin(merged), new AuthProxyPlugin()],
40831
+ api: {
40832
+ enableProjectScoping: true,
40833
+ projectResolution: "auto",
40834
+ // ObjectOS is multi-tenant: anonymous /api/v1/data/* must never
40835
+ // leak per-project data across organisations. AuthProxyPlugin
40836
+ // verifies upstream tokens and populates ctx.userId; requireAuth
40837
+ // turns missing userId into 401 at the REST layer before the
40838
+ // request reaches the per-project kernel.
40839
+ requireAuth: true
40840
+ }
40841
+ };
40842
+ }
40843
+
40844
+ // src/index.ts
40845
+ init_platform_sso();
40846
+
38262
40847
  // src/sandbox/script-runner.ts
38263
40848
  var UnimplementedScriptRunner = class {
38264
40849
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
@@ -38291,12 +40876,26 @@ import {
38291
40876
  export * from "@objectstack/core";
38292
40877
  export {
38293
40878
  AppPlugin,
40879
+ ArtifactApiClient,
40880
+ ArtifactEnvironmentRegistry,
40881
+ ArtifactKernelFactory,
40882
+ AuthProxyPlugin,
40883
+ DEFAULT_RATE_LIMITS,
38294
40884
  DriverPlugin,
40885
+ FileArtifactApiClient,
38295
40886
  HttpDispatcher,
38296
40887
  HttpServer,
40888
+ InMemoryErrorReporter,
40889
+ InMemoryMetricsRegistry,
40890
+ KernelManager,
38297
40891
  MiddlewareManager,
38298
- ObjectKernel3 as ObjectKernel,
40892
+ NoopErrorReporter,
40893
+ NoopMetricsRegistry,
40894
+ ObjectKernel4 as ObjectKernel,
40895
+ PLATFORM_SSO_PROVIDER_ID,
38299
40896
  QuickJSScriptRunner,
40897
+ RUNTIME_METRICS,
40898
+ RateLimiter,
38300
40899
  RestServer,
38301
40900
  RouteGroupBuilder,
38302
40901
  RouteManager,
@@ -38306,17 +40905,31 @@ export {
38306
40905
  SeedLoaderService,
38307
40906
  UnimplementedScriptRunner,
38308
40907
  actionBodyRunnerFactory,
40908
+ backfillPlatformSsoClients,
40909
+ buildPlatformSsoRedirectUri,
40910
+ buildSecurityHeaders,
38309
40911
  collectBundleActions,
38310
40912
  collectBundleFunctions,
38311
40913
  collectBundleHooks,
40914
+ createDefaultHostConfig,
38312
40915
  createDispatcherPlugin,
40916
+ createObjectOSStack,
38313
40917
  createRestApiPlugin,
38314
40918
  createStandaloneStack,
38315
40919
  createSystemProjectPlugin,
40920
+ derivePlatformSsoClientId,
40921
+ derivePlatformSsoClientSecret,
40922
+ extractRequestId,
40923
+ formatTraceparent,
40924
+ generateRequestId,
38316
40925
  hookBodyRunnerFactory,
38317
40926
  isHttpUrl,
38318
40927
  loadArtifactBundle,
38319
40928
  mergeRuntimeModule,
38320
- readArtifactSource
40929
+ parseTraceparent,
40930
+ readArtifactSource,
40931
+ resolveDefaultArtifactPath,
40932
+ resolveRequestId,
40933
+ seedPlatformSsoClient
38321
40934
  };
38322
40935
  //# sourceMappingURL=index.js.map