@objectstack/runtime 7.3.0 → 7.4.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
@@ -9,13 +9,6 @@ var __export = (target, all) => {
9
9
  };
10
10
 
11
11
  // src/load-artifact-bundle.ts
12
- var load_artifact_bundle_exports = {};
13
- __export(load_artifact_bundle_exports, {
14
- isHttpUrl: () => isHttpUrl,
15
- loadArtifactBundle: () => loadArtifactBundle,
16
- mergeRuntimeModule: () => mergeRuntimeModule,
17
- readArtifactSource: () => readArtifactSource
18
- });
19
12
  import { readFile } from "fs/promises";
20
13
  import { resolve as resolvePath, isAbsolute, dirname } from "path";
21
14
  import { pathToFileURL } from "url";
@@ -270,17 +263,37 @@ var init_seed_loader = __esm({
270
263
  }
271
264
  const objectRefs = refMap.get(objectName) || [];
272
265
  const seedNow = /* @__PURE__ */ new Date();
266
+ const seedIdentity = config.identity;
267
+ const baseEvalCtx = {
268
+ now: seedNow,
269
+ user: seedIdentity?.user,
270
+ // Fall back to the per-tenant organizationId so `os.org.id` resolves
271
+ // during per-org replay even without an explicit identity.org.
272
+ org: seedIdentity?.org ?? (config.organizationId ? { id: config.organizationId } : void 0),
273
+ env: config.env
274
+ };
273
275
  for (let i = 0; i < dataset.records.length; i++) {
274
276
  const seedResult = resolveSeedRecord(
275
277
  dataset.records[i],
276
- { now: seedNow }
278
+ baseEvalCtx
277
279
  );
278
- const record = seedResult.ok ? { ...seedResult.value } : { ...dataset.records[i] };
279
280
  if (!seedResult.ok) {
280
- this.logger.warn(
281
- `[SeedLoader] Failed to resolve dynamic values for ${objectName} record #${i}: ${seedResult.error.message}`
282
- );
281
+ errored++;
282
+ const error = {
283
+ sourceObject: objectName,
284
+ field: "(expression)",
285
+ targetObject: objectName,
286
+ targetField: "(expression)",
287
+ attemptedValue: dataset.records[i],
288
+ recordIndex: i,
289
+ message: `Cannot resolve dynamic seed values for ${objectName} record #${i}: ${seedResult.error.message}. Records using cel\`os.user.id\` / cel\`os.org.id\` require a seed identity \u2014 ensure a system/admin user exists before seeding (see SeedLoaderConfig.identity).`
290
+ };
291
+ errors.push(error);
292
+ allErrors.push(error);
293
+ this.logger.warn(`[SeedLoader] ${error.message}`);
294
+ continue;
283
295
  }
296
+ const record = { ...seedResult.value };
284
297
  if (config.organizationId && record["organization_id"] == null) {
285
298
  record["organization_id"] = config.organizationId;
286
299
  }
@@ -359,10 +372,18 @@ var init_seed_loader = __esm({
359
372
  }
360
373
  } catch (err) {
361
374
  errored++;
362
- this.logger.warn(`[SeedLoader] Failed to write ${objectName} record`, {
363
- error: err.message,
364
- recordIndex: i
365
- });
375
+ const error = {
376
+ sourceObject: objectName,
377
+ field: "(write)",
378
+ targetObject: objectName,
379
+ targetField: externalId,
380
+ attemptedValue: record[externalId] ?? null,
381
+ recordIndex: i,
382
+ message: `Failed to write ${objectName} record #${i} (${externalId}=${String(record[externalId] ?? "")}): ${err.message}`
383
+ };
384
+ errors.push(error);
385
+ allErrors.push(error);
386
+ this.logger.warn(`[SeedLoader] ${error.message}`, { recordIndex: i });
366
387
  }
367
388
  } else {
368
389
  const externalIdValue = String(record[externalId] ?? "");
@@ -1249,6 +1270,7 @@ __export(app_plugin_exports, {
1249
1270
  collectBundleHooks: () => collectBundleHooks
1250
1271
  });
1251
1272
  import { readEnvWithDeprecation } from "@objectstack/types";
1273
+ import { SystemUserId } from "@objectstack/spec/system";
1252
1274
  function collectBundleHooks(bundle) {
1253
1275
  const out = [];
1254
1276
  const seen = /* @__PURE__ */ new Set();
@@ -1388,6 +1410,27 @@ var init_app_plugin = __esm({
1388
1410
  });
1389
1411
  ql.setDatasourceMapping(this.bundle.datasourceMapping);
1390
1412
  }
1413
+ try {
1414
+ const dsDefs = this.bundle.datasources;
1415
+ const dsList = Array.isArray(dsDefs) ? dsDefs : dsDefs && typeof dsDefs === "object" ? Object.entries(dsDefs).map(([name, def]) => ({ name, ...def })) : [];
1416
+ if (dsList.length > 0) {
1417
+ const metadata = ctx.getService("metadata");
1418
+ if (typeof metadata?.registerInMemory === "function") {
1419
+ for (const ds of dsList) {
1420
+ if (!ds?.name) continue;
1421
+ metadata.registerInMemory("datasource", ds.name, { ...ds, origin: "code" });
1422
+ }
1423
+ ctx.logger.info("Registered code-defined datasources in metadata registry", {
1424
+ appId,
1425
+ count: dsList.length
1426
+ });
1427
+ }
1428
+ }
1429
+ } catch (err) {
1430
+ ctx.logger.warn("[AppPlugin] failed to register code-defined datasources", {
1431
+ error: err?.message ?? String(err)
1432
+ });
1433
+ }
1391
1434
  const stackBundle = this.bundle.default || this.bundle;
1392
1435
  const runtime = stackBundle && typeof stackBundle.onEnable === "function" ? stackBundle : this.bundle;
1393
1436
  if (runtime && typeof runtime.onEnable === "function") {
@@ -1482,49 +1525,6 @@ var init_app_plugin = __esm({
1482
1525
  appId
1483
1526
  });
1484
1527
  }
1485
- try {
1486
- const approvals = Array.isArray(this.bundle.approvals) ? this.bundle.approvals : Array.isArray((this.bundle.manifest || {}).approvals) ? this.bundle.manifest.approvals : [];
1487
- if (approvals.length > 0) {
1488
- ctx.hook("kernel:ready", async () => {
1489
- let svc;
1490
- try {
1491
- svc = ctx.getService("approvals");
1492
- } catch {
1493
- }
1494
- if (!svc || typeof svc.defineProcess !== "function") {
1495
- ctx.logger.warn("[AppPlugin] approvals service not registered \u2014 skipping declarative processes", {
1496
- appId,
1497
- processCount: approvals.length
1498
- });
1499
- return;
1500
- }
1501
- const sysCtx = { isSystem: true, roles: [], permissions: [] };
1502
- let ok = 0;
1503
- for (const proc of approvals) {
1504
- try {
1505
- await svc.defineProcess({
1506
- name: proc.name,
1507
- label: proc.label,
1508
- object: proc.object,
1509
- description: proc.description,
1510
- active: proc.active !== false,
1511
- definition: proc
1512
- }, sysCtx);
1513
- ok++;
1514
- } catch (err) {
1515
- ctx.logger.warn("[AppPlugin] Failed to register approval process", {
1516
- appId,
1517
- process: proc?.name,
1518
- error: err?.message ?? String(err)
1519
- });
1520
- }
1521
- }
1522
- ctx.logger.info("[AppPlugin] Registered approval processes", { appId, count: ok });
1523
- });
1524
- }
1525
- } catch (err) {
1526
- ctx.logger.error("[AppPlugin] Failed to schedule approval-process registration", err, { appId });
1527
- }
1528
1528
  try {
1529
1529
  const jobs = Array.isArray(this.bundle.jobs) ? this.bundle.jobs : Array.isArray((this.bundle.manifest || {}).jobs) ? this.bundle.manifest.jobs : [];
1530
1530
  if (jobs.length > 0) {
@@ -1601,6 +1601,7 @@ var init_app_plugin = __esm({
1601
1601
  ...d,
1602
1602
  object: d.object
1603
1603
  }));
1604
+ const seedIdentity = await this.ensureSeedIdentity(ql, ctx.logger);
1604
1605
  try {
1605
1606
  const kernel = ctx.kernel;
1606
1607
  const existing = (() => {
@@ -1642,7 +1643,12 @@ var init_app_plugin = __esm({
1642
1643
  config: {
1643
1644
  defaultMode: "upsert",
1644
1645
  multiPass: true,
1645
- organizationId
1646
+ organizationId,
1647
+ // Bind os.user (system identity) and os.org (this
1648
+ // tenant) so identity-derived seed values resolve
1649
+ // per-org. org.id falls back to organizationId
1650
+ // inside the loader when identity.org is absent.
1651
+ identity: seedIdentity
1646
1652
  }
1647
1653
  });
1648
1654
  const result = await seedLoader.load(request);
@@ -1670,14 +1676,34 @@ var init_app_plugin = __esm({
1670
1676
  const { SeedLoaderRequestSchema } = await import("@objectstack/spec/data");
1671
1677
  const request = SeedLoaderRequestSchema.parse({
1672
1678
  datasets: normalizedDatasets,
1673
- config: { defaultMode: "upsert", multiPass: true }
1679
+ config: { defaultMode: "upsert", multiPass: true, identity: seedIdentity }
1674
1680
  });
1675
1681
  const result = await seedLoader.load(request);
1676
- ctx.logger.info("[Seeder] Seed loading complete", {
1677
- inserted: result.summary.totalInserted,
1678
- updated: result.summary.totalUpdated,
1679
- errors: result.errors.length
1680
- });
1682
+ const { totalInserted, totalUpdated, totalSkipped, totalErrored } = result.summary;
1683
+ if (result.success) {
1684
+ ctx.logger.info("[Seeder] Seed loading complete", {
1685
+ inserted: totalInserted,
1686
+ updated: totalUpdated,
1687
+ skipped: totalSkipped,
1688
+ errored: totalErrored
1689
+ });
1690
+ } else {
1691
+ ctx.logger.warn(
1692
+ `[Seeder] Seed loading completed with ${totalErrored} dropped record(s) and ${result.errors.length} error(s) for ${appId}`,
1693
+ {
1694
+ inserted: totalInserted,
1695
+ updated: totalUpdated,
1696
+ skipped: totalSkipped,
1697
+ errored: totalErrored
1698
+ }
1699
+ );
1700
+ for (const e of result.errors.slice(0, 20)) {
1701
+ ctx.logger.warn(`[Seeder] \u2717 ${e.message}`);
1702
+ }
1703
+ if (result.errors.length > 20) {
1704
+ ctx.logger.warn(`[Seeder] \u2026and ${result.errors.length - 20} more error(s)`);
1705
+ }
1706
+ }
1681
1707
  } else {
1682
1708
  ctx.logger.debug("[Seeder] No metadata service; using basic insert fallback");
1683
1709
  for (const dataset of normalizedDatasets) {
@@ -1779,6 +1805,64 @@ var init_app_plugin = __esm({
1779
1805
  this.name = `plugin.app.${appId}`;
1780
1806
  this.version = sys?.version;
1781
1807
  }
1808
+ /**
1809
+ * Resolve the identity bound to `os.user` / `os.org` for seed CEL values.
1810
+ *
1811
+ * On a fresh boot there are zero users until the first human sign-up
1812
+ * (which the SeedLoader runs *before*), so identity-derived seeds like
1813
+ * `owner_id: cel`os.user.id`` had nothing to resolve against and were
1814
+ * dropped silently. To make seeds deterministic and self-sufficient we
1815
+ * upsert a single non-loginable **system user** (`usr_system`) and bind
1816
+ * it as `os.user`.
1817
+ *
1818
+ * Why a dedicated system user rather than the login admin:
1819
+ * - `sys_user` is better-auth-managed and schema-locked (ADR-0010); the
1820
+ * password lives in `sys_account`, so a *loginable* admin can only be
1821
+ * minted through better-auth (the CLI does this via HTTP sign-up after
1822
+ * boot). A raw insert here would bypass those invariants.
1823
+ * - `usr_system` is an owner identity only (no credential row), analogous
1824
+ * to Salesforce's "Automated Process" user. The human admin is created
1825
+ * independently and need not be the seed owner.
1826
+ *
1827
+ * Idempotent: matches by the stable id, inserts once, reuses thereafter.
1828
+ * Failures are non-fatal (logged) — records that actually need `os.user`
1829
+ * then fail loudly in the loader with an actionable message.
1830
+ */
1831
+ async ensureSeedIdentity(ql, logger) {
1832
+ const SYSTEM_USER_ID = SystemUserId.SYSTEM;
1833
+ const SYSTEM_USER_EMAIL = "system@objectstack.local";
1834
+ const identity = { user: { id: SYSTEM_USER_ID, role: "system", email: SYSTEM_USER_EMAIL } };
1835
+ const opts = { context: { isSystem: true } };
1836
+ try {
1837
+ const existing = await ql.find(
1838
+ "sys_user",
1839
+ { where: { id: SYSTEM_USER_ID }, limit: 1 },
1840
+ opts
1841
+ );
1842
+ if (Array.isArray(existing) && existing.length > 0) {
1843
+ return identity;
1844
+ }
1845
+ await ql.insert(
1846
+ "sys_user",
1847
+ {
1848
+ id: SYSTEM_USER_ID,
1849
+ name: "System",
1850
+ email: SYSTEM_USER_EMAIL,
1851
+ email_verified: true,
1852
+ role: "system"
1853
+ },
1854
+ opts
1855
+ );
1856
+ logger.info(
1857
+ `[Seeder] Provisioned deterministic system user (${SYSTEM_USER_ID}) as seed owner \u2014 binds os.user for identity-derived seed values`
1858
+ );
1859
+ } catch (err) {
1860
+ logger.warn("[Seeder] Failed to ensure system seed user; os.user-dependent seeds may be dropped", {
1861
+ error: err?.message ?? String(err)
1862
+ });
1863
+ }
1864
+ return identity;
1865
+ }
1782
1866
  /**
1783
1867
  * Emit a kernel hook so the control-plane `AppCatalogService` can
1784
1868
  * upsert / delete the corresponding `sys_app` row. Silently no-ops
@@ -2065,212 +2149,6 @@ var init_standalone_stack = __esm({
2065
2149
  }
2066
2150
  });
2067
2151
 
2068
- // src/cloud/platform-sso.ts
2069
- var platform_sso_exports = {};
2070
- __export(platform_sso_exports, {
2071
- PLATFORM_SSO_PROVIDER_ID: () => PLATFORM_SSO_PROVIDER_ID,
2072
- backfillPlatformSsoClients: () => backfillPlatformSsoClients,
2073
- buildPlatformSsoRedirectUri: () => buildPlatformSsoRedirectUri,
2074
- derivePlatformSsoClientId: () => derivePlatformSsoClientId,
2075
- derivePlatformSsoClientSecret: () => derivePlatformSsoClientSecret,
2076
- hashPlatformSsoClientSecret: () => hashPlatformSsoClientSecret,
2077
- seedPlatformSsoClient: () => seedPlatformSsoClient
2078
- });
2079
- import { createHmac, createHash } from "crypto";
2080
- function derivePlatformSsoClientId(environmentId) {
2081
- return `project_${environmentId}`;
2082
- }
2083
- function derivePlatformSsoClientSecret(baseSecret, environmentId) {
2084
- return createHmac("sha256", baseSecret).update(`oauth-client:${environmentId}`).digest("hex");
2085
- }
2086
- function hashPlatformSsoClientSecret(plaintext) {
2087
- return createHash("sha256").update(plaintext).digest("base64").replace(/=+$/, "").replace(/\+/g, "-").replace(/\//g, "_");
2088
- }
2089
- function buildPlatformSsoRedirectUri(hostname, basePath = "/api/v1/auth") {
2090
- let host;
2091
- if (hostname.startsWith("http://") || hostname.startsWith("https://")) {
2092
- host = hostname;
2093
- } else if (/(\.|^)localhost(:\d+)?$/i.test(hostname)) {
2094
- const port = (process.env.OS_RUNTIME_PORT ?? "").trim();
2095
- const hostWithPort = /:\d+$/.test(hostname) || !port ? hostname : `${hostname}:${port}`;
2096
- host = `http://${hostWithPort}`;
2097
- } else {
2098
- host = `https://${hostname}`;
2099
- }
2100
- const trimmed = host.replace(/\/+$/, "");
2101
- const path = basePath.replace(/\/+$/, "");
2102
- return `${trimmed}${path}/oauth2/callback/${PLATFORM_SSO_PROVIDER_ID}`;
2103
- }
2104
- async function seedPlatformSsoClient(opts) {
2105
- const { ql, environmentId, hostname, baseSecret, logger, throwOnError } = opts;
2106
- if (!baseSecret) {
2107
- logger?.warn?.("[platform-sso] OS_AUTH_SECRET not set \u2014 skipping client seed", { environmentId });
2108
- return;
2109
- }
2110
- const clientId = derivePlatformSsoClientId(environmentId);
2111
- const clientSecretPlaintext = derivePlatformSsoClientSecret(baseSecret, environmentId);
2112
- const clientSecretStored = hashPlatformSsoClientSecret(clientSecretPlaintext);
2113
- const desiredRedirect = hostname ? buildPlatformSsoRedirectUri(hostname) : null;
2114
- let existing = null;
2115
- try {
2116
- const rows = await ql.find("sys_oauth_application", {
2117
- where: { client_id: clientId },
2118
- limit: 1
2119
- }, { context: { isSystem: true } });
2120
- const list = Array.isArray(rows) ? rows : Array.isArray(rows?.records) ? rows.records : [];
2121
- existing = list[0] ?? null;
2122
- } catch (err) {
2123
- logger?.warn?.("[platform-sso] sys_oauth_application read failed \u2014 skipping seed", {
2124
- environmentId,
2125
- error: err?.message
2126
- });
2127
- return;
2128
- }
2129
- const nowIso = (/* @__PURE__ */ new Date()).toISOString();
2130
- if (!existing) {
2131
- const redirects = desiredRedirect ? [desiredRedirect] : [];
2132
- try {
2133
- await ql.insert("sys_oauth_application", {
2134
- id: `oauthc_${environmentId}`,
2135
- name: `Project ${environmentId}`,
2136
- client_id: clientId,
2137
- client_secret: clientSecretStored,
2138
- type: "web",
2139
- redirect_uris: JSON.stringify(redirects),
2140
- grant_types: JSON.stringify(["authorization_code", "refresh_token"]),
2141
- response_types: JSON.stringify(["code"]),
2142
- scopes: JSON.stringify(["openid", "email", "profile"]),
2143
- token_endpoint_auth_method: "client_secret_basic",
2144
- require_pkce: false,
2145
- skip_consent: true,
2146
- disabled: false,
2147
- subject_type: "public",
2148
- created_at: nowIso,
2149
- updated_at: nowIso
2150
- }, { context: { isSystem: true } });
2151
- logger?.info?.("[platform-sso] sys_oauth_application row created", { environmentId, clientId });
2152
- } catch (err) {
2153
- logger?.warn?.("[platform-sso] sys_oauth_application create failed", {
2154
- environmentId,
2155
- error: err?.message
2156
- });
2157
- if (throwOnError) throw err;
2158
- }
2159
- return;
2160
- }
2161
- let currentRedirects = [];
2162
- try {
2163
- const raw = existing.redirect_uris;
2164
- const parsed = typeof raw === "string" ? JSON.parse(raw) : raw;
2165
- if (Array.isArray(parsed)) currentRedirects = parsed.filter((s) => typeof s === "string");
2166
- } catch {
2167
- }
2168
- const mergedRedirects = desiredRedirect && !currentRedirects.includes(desiredRedirect) ? [...currentRedirects, desiredRedirect] : currentRedirects;
2169
- const repairPatch = {
2170
- name: existing.name || `Project ${environmentId}`,
2171
- client_secret: clientSecretStored,
2172
- type: existing.type || "web",
2173
- redirect_uris: JSON.stringify(mergedRedirects),
2174
- grant_types: JSON.stringify(["authorization_code", "refresh_token"]),
2175
- response_types: JSON.stringify(["code"]),
2176
- scopes: JSON.stringify(["openid", "email", "profile"]),
2177
- token_endpoint_auth_method: "client_secret_basic",
2178
- require_pkce: false,
2179
- skip_consent: true,
2180
- disabled: false,
2181
- subject_type: "public",
2182
- updated_at: nowIso
2183
- };
2184
- try {
2185
- await ql.update(
2186
- "sys_oauth_application",
2187
- repairPatch,
2188
- { where: { id: existing.id } },
2189
- { context: { isSystem: true } }
2190
- );
2191
- logger?.info?.("[platform-sso] sys_oauth_application repaired", {
2192
- environmentId,
2193
- clientId,
2194
- redirect_uris: mergedRedirects
2195
- });
2196
- } catch (err) {
2197
- logger?.warn?.("[platform-sso] sys_oauth_application repair failed", {
2198
- environmentId,
2199
- error: err?.message
2200
- });
2201
- if (throwOnError) throw err;
2202
- }
2203
- }
2204
- async function backfillPlatformSsoClients(opts) {
2205
- const { ql, baseSecret, logger, limit = 1e3 } = opts;
2206
- if (!baseSecret) {
2207
- logger?.warn?.("[platform-sso] backfill skipped \u2014 OS_AUTH_SECRET not set");
2208
- return { scanned: 0, seeded: 0, alreadyExisted: 0, failures: [] };
2209
- }
2210
- let projects = [];
2211
- try {
2212
- const rows = await ql.find("sys_environment", {
2213
- limit,
2214
- fields: ["id", "hostname", "status"]
2215
- }, { context: { isSystem: true } });
2216
- projects = Array.isArray(rows) ? rows : Array.isArray(rows?.records) ? rows.records : [];
2217
- } catch (err) {
2218
- logger?.warn?.("[platform-sso] backfill: sys_environment read failed", {
2219
- error: err?.message
2220
- });
2221
- return { scanned: 0, seeded: 0, alreadyExisted: 0, failures: [{ environmentId: "<scan>", error: err?.message ?? String(err) }] };
2222
- }
2223
- let seeded = 0;
2224
- let alreadyExisted = 0;
2225
- const failures = [];
2226
- for (const p of projects) {
2227
- if (!p?.id) continue;
2228
- const before = await (async () => {
2229
- try {
2230
- const r = await ql.find("sys_oauth_application", {
2231
- where: { client_id: derivePlatformSsoClientId(p.id) },
2232
- limit: 1
2233
- }, { context: { isSystem: true } });
2234
- const list = Array.isArray(r) ? r : Array.isArray(r?.records) ? r.records : [];
2235
- return list[0] ?? null;
2236
- } catch {
2237
- return null;
2238
- }
2239
- })();
2240
- try {
2241
- await seedPlatformSsoClient({ ql, environmentId: p.id, hostname: p.hostname, baseSecret, logger, throwOnError: true });
2242
- if (before) alreadyExisted++;
2243
- else {
2244
- const after = await (async () => {
2245
- try {
2246
- const r = await ql.find("sys_oauth_application", {
2247
- where: { client_id: derivePlatformSsoClientId(p.id) },
2248
- limit: 1
2249
- }, { context: { isSystem: true } });
2250
- const list = Array.isArray(r) ? r : Array.isArray(r?.records) ? r.records : [];
2251
- return list[0] ?? null;
2252
- } catch (err) {
2253
- return { _readErr: err?.message };
2254
- }
2255
- })();
2256
- if (after && !after._readErr) seeded++;
2257
- else failures.push({ environmentId: p.id, error: `post-insert read returned ${after ? JSON.stringify(after) : "null"}` });
2258
- }
2259
- } catch (err) {
2260
- failures.push({ environmentId: p.id, error: err?.message ?? String(err) });
2261
- }
2262
- }
2263
- logger?.info?.("[platform-sso] backfill complete", { scanned: projects.length, seeded, alreadyExisted, failures: failures.length });
2264
- return { scanned: projects.length, seeded, alreadyExisted, failures };
2265
- }
2266
- var PLATFORM_SSO_PROVIDER_ID;
2267
- var init_platform_sso = __esm({
2268
- "src/cloud/platform-sso.ts"() {
2269
- "use strict";
2270
- PLATFORM_SSO_PROVIDER_ID = "objectstack-cloud";
2271
- }
2272
- });
2273
-
2274
2152
  // src/cloud/environment-org-seed.ts
2275
2153
  var environment_org_seed_exports = {};
2276
2154
  __export(environment_org_seed_exports, {
@@ -2532,10 +2410,171 @@ init_driver_plugin();
2532
2410
  init_app_plugin();
2533
2411
  init_seed_loader();
2534
2412
 
2535
- // src/http-dispatcher.ts
2536
- init_package_state_store();
2413
+ // src/external-validation-plugin.ts
2414
+ import {
2415
+ ExternalSchemaMismatchError
2416
+ } from "@objectstack/spec/shared";
2417
+ var ExternalValidationPlugin = class {
2418
+ constructor() {
2419
+ this.name = "com.objectstack.external-validation";
2420
+ this.type = "standard";
2421
+ this.version = "1.0.0";
2422
+ /** Active background drift-check timers, keyed by datasource name. */
2423
+ this.driftTimers = /* @__PURE__ */ new Map();
2424
+ this.init = (_ctx) => {
2425
+ };
2426
+ this.start = (ctx) => {
2427
+ ctx.hook("kernel:ready", async () => {
2428
+ await this.runValidation(ctx);
2429
+ await this.scheduleDriftChecks(ctx);
2430
+ });
2431
+ };
2432
+ /** Tear down background drift-check timers (idempotent). */
2433
+ this.stop = () => {
2434
+ for (const timer of this.driftTimers.values()) clearInterval(timer);
2435
+ this.driftTimers.clear();
2436
+ };
2437
+ }
2438
+ /** Exposed for testing; invoked from the kernel:ready handler. */
2439
+ async runValidation(ctx) {
2440
+ const svc = safeGet(ctx, "external-datasource");
2441
+ if (!svc?.validateAll) {
2442
+ ctx.logger?.debug?.("[external-validation] service not registered; skipping");
2443
+ return;
2444
+ }
2445
+ const metadata = safeGet(ctx, "metadata");
2446
+ let report;
2447
+ try {
2448
+ report = await svc.validateAll();
2449
+ } catch (err) {
2450
+ ctx.logger?.warn?.("[external-validation] validateAll failed", { err });
2451
+ return;
2452
+ }
2453
+ const failures = report.results.filter((r) => !r.ok);
2454
+ if (failures.length === 0) {
2455
+ ctx.logger?.info?.("[external-validation] all federated objects match their remote schema", {
2456
+ objects: report.results.length
2457
+ });
2458
+ return;
2459
+ }
2460
+ for (const r of failures) {
2461
+ const mode = await resolveOnMismatch(metadata, r.datasource);
2462
+ if (mode === "ignore") continue;
2463
+ if (mode === "warn") {
2464
+ ctx.logger?.warn?.("[external-validation] external schema drift", {
2465
+ datasource: r.datasource,
2466
+ object: r.object,
2467
+ diffs: r.diffs
2468
+ });
2469
+ continue;
2470
+ }
2471
+ throw new ExternalSchemaMismatchError(r.datasource, r.object, r.diffs);
2472
+ }
2473
+ }
2474
+ /**
2475
+ * Arm a background drift checker for every federated datasource that declares
2476
+ * `external.validation.checkIntervalMs`. Each fires on its own interval and
2477
+ * emits `external.schema.drift` events — it never throws or aborts the
2478
+ * process, since drift past boot is observational, not fatal.
2479
+ *
2480
+ * No-op when metadata can't be enumerated or no datasource opts in. Re-arming
2481
+ * (e.g. a second `kernel:ready`) first clears existing timers so intervals
2482
+ * don't accumulate.
2483
+ */
2484
+ async scheduleDriftChecks(ctx) {
2485
+ this.stop();
2486
+ const metadata = safeGet(ctx, "metadata");
2487
+ if (!metadata?.list) return;
2488
+ let datasources;
2489
+ try {
2490
+ datasources = await metadata.list("datasource");
2491
+ } catch (err) {
2492
+ ctx.logger?.warn?.("[external-validation] could not list datasources for drift checks", { err });
2493
+ return;
2494
+ }
2495
+ for (const def of datasources) {
2496
+ const interval = def?.external?.validation?.checkIntervalMs;
2497
+ const name = def?.name;
2498
+ if (!name || typeof interval !== "number" || interval <= 0) continue;
2499
+ const timer = setInterval(() => {
2500
+ void this.runDriftCheck(ctx, name);
2501
+ }, interval);
2502
+ timer.unref?.();
2503
+ this.driftTimers.set(name, timer);
2504
+ ctx.logger?.info?.("[external-validation] armed background drift check", {
2505
+ datasource: name,
2506
+ intervalMs: interval
2507
+ });
2508
+ }
2509
+ }
2510
+ /**
2511
+ * Re-validate one datasource's federated objects and emit an
2512
+ * `external.schema.drift` event per mismatch. Exposed for testing; invoked
2513
+ * from the interval armed by {@link scheduleDriftChecks}. Never throws.
2514
+ *
2515
+ * @returns the number of drift events emitted.
2516
+ */
2517
+ async runDriftCheck(ctx, datasource) {
2518
+ const svc = safeGet(ctx, "external-datasource");
2519
+ if (!svc?.validateAll) return 0;
2520
+ let report;
2521
+ try {
2522
+ report = await svc.validateAll();
2523
+ } catch (err) {
2524
+ ctx.logger?.warn?.("[external-validation] drift check validateAll failed", {
2525
+ datasource,
2526
+ err
2527
+ });
2528
+ return 0;
2529
+ }
2530
+ const drifted = report.results.filter((r) => !r.ok && r.datasource === datasource);
2531
+ for (const r of drifted) {
2532
+ const event = {
2533
+ datasource: r.datasource,
2534
+ object: r.object,
2535
+ diffs: r.diffs
2536
+ };
2537
+ try {
2538
+ await ctx.trigger("external.schema.drift", event);
2539
+ } catch (err) {
2540
+ ctx.logger?.warn?.("[external-validation] failed to emit drift event", {
2541
+ datasource,
2542
+ object: r.object,
2543
+ err
2544
+ });
2545
+ }
2546
+ }
2547
+ if (drifted.length > 0) {
2548
+ ctx.logger?.warn?.("[external-validation] background drift detected", {
2549
+ datasource,
2550
+ objects: drifted.map((r) => r.object)
2551
+ });
2552
+ }
2553
+ return drifted.length;
2554
+ }
2555
+ };
2556
+ function createExternalValidationPlugin() {
2557
+ return new ExternalValidationPlugin();
2558
+ }
2559
+ async function resolveOnMismatch(metadata, datasource) {
2560
+ try {
2561
+ const ds = await metadata?.get?.("datasource", datasource);
2562
+ return ds?.external?.validation?.onMismatch ?? "fail";
2563
+ } catch {
2564
+ return "fail";
2565
+ }
2566
+ }
2567
+ function safeGet(ctx, name) {
2568
+ try {
2569
+ return ctx.getService(name);
2570
+ } catch {
2571
+ return void 0;
2572
+ }
2573
+ }
2574
+
2575
+ // src/http-dispatcher.ts
2576
+ init_package_state_store();
2537
2577
  import { getEnv, resolveLocale } from "@objectstack/core";
2538
- import { readEnvWithDeprecation as readEnvWithDeprecation3 } from "@objectstack/types";
2539
2578
  import { CoreServiceName } from "@objectstack/spec/system";
2540
2579
  import { pluralToSingular, PLURAL_TO_SINGULAR } from "@objectstack/spec/shared";
2541
2580
 
@@ -3364,6 +3403,17 @@ var _HttpDispatcher = class _HttpDispatcher {
3364
3403
  }
3365
3404
  return { handled: true, response: this.success({ types: ["object", "app", "plugin"] }) };
3366
3405
  }
3406
+ if (parts.length === 4 && (parts[0] === "objects" || parts[0] === "object") && parts[2] === "state" && (!method || method === "GET")) {
3407
+ const name = parts[1];
3408
+ const field = parts[3];
3409
+ const from = query?.from !== void 0 ? String(query.from) : void 0;
3410
+ const qlService = await this.getObjectQLService();
3411
+ const schema = qlService?.registry?.getObject(name);
3412
+ if (!schema) return { handled: true, response: this.error("Object not found", 404) };
3413
+ const { legalNextStates } = await import("@objectstack/objectql");
3414
+ const next = from === void 0 ? null : legalNextStates(schema, field, from);
3415
+ return { handled: true, response: this.success({ object: name, field, from: from ?? null, next }) };
3416
+ }
3367
3417
  if (parts.length >= 3 && parts[parts.length - 1] === "published" && (!method || method === "GET")) {
3368
3418
  const type = parts[0];
3369
3419
  const name = parts.slice(1, -1).join("/");
@@ -3937,1444 +3987,204 @@ var _HttpDispatcher = class _HttpDispatcher {
3937
3987
  return void 0;
3938
3988
  }
3939
3989
  }
3940
- async resolveCallerUserId(context) {
3941
- try {
3942
- const authService = await this.resolveService(CoreServiceName.enum.auth);
3943
- const rawHeaders = context.request?.headers;
3944
- let headers = rawHeaders;
3945
- if (rawHeaders && typeof rawHeaders === "object" && typeof rawHeaders.get !== "function") {
3946
- try {
3947
- const h = new Headers();
3948
- for (const [k, v] of Object.entries(rawHeaders)) {
3949
- if (v == null) continue;
3950
- h.set(k, Array.isArray(v) ? v.join(", ") : String(v));
3990
+ /**
3991
+ * Handles Storage requests
3992
+ * path: sub-path after /storage/
3993
+ */
3994
+ async handleStorage(path, method, file, context) {
3995
+ const storageService = await this.getService(CoreServiceName.enum["file-storage"]) || this.kernel.services?.["file-storage"];
3996
+ if (!storageService) {
3997
+ return { handled: true, response: this.error("File storage not configured", 501) };
3998
+ }
3999
+ const m = method.toUpperCase();
4000
+ const parts = path.replace(/^\/+/, "").split("/");
4001
+ if (parts[0] === "upload" && m === "POST") {
4002
+ if (!file) {
4003
+ return { handled: true, response: this.error("No file provided", 400) };
4004
+ }
4005
+ const result = await storageService.upload(file, { request: context.request });
4006
+ return { handled: true, response: this.success(result) };
4007
+ }
4008
+ if (parts[0] === "file" && parts[1] && m === "GET") {
4009
+ const id = parts[1];
4010
+ const result = await storageService.download(id, { request: context.request });
4011
+ if (result.url && result.redirect) {
4012
+ return { handled: true, result: { type: "redirect", url: result.url } };
4013
+ }
4014
+ if (result.stream) {
4015
+ return {
4016
+ handled: true,
4017
+ result: {
4018
+ type: "stream",
4019
+ stream: result.stream,
4020
+ headers: {
4021
+ "Content-Type": result.mimeType || "application/octet-stream",
4022
+ "Content-Length": result.size
4023
+ }
3951
4024
  }
3952
- headers = h;
3953
- } catch {
3954
- headers = rawHeaders;
3955
- }
4025
+ };
3956
4026
  }
3957
- const sessionData = await (authService?.auth?.api?.getSession ?? authService?.api?.getSession)?.call(
3958
- authService?.auth?.api ?? authService?.api,
3959
- { headers }
3960
- );
3961
- return sessionData?.user?.id ?? sessionData?.session?.userId;
3962
- } catch (e) {
3963
- return void 0;
4027
+ return { handled: true, response: this.success(result) };
3964
4028
  }
4029
+ return { handled: false };
3965
4030
  }
3966
- async handleCloud(path, method, body, query, _context) {
3967
- const m = method.toUpperCase();
4031
+ /**
4032
+ * Handles UI requests
4033
+ * path: sub-path after /ui/
4034
+ */
4035
+ async handleUi(path, query, _context) {
3968
4036
  const parts = path.replace(/^\/+/, "").split("/").filter(Boolean);
3969
- const qlService = await this.getObjectQLService();
3970
- const ql = qlService ?? await this.resolveService("objectql");
3971
- if (!ql) {
3972
- return { handled: true, response: this.error("Project service not available (ObjectQL missing)", 503) };
3973
- }
3974
- const ENV = "sys_environment";
3975
- const CRED = "sys_environment_credential";
3976
- const MEM = "sys_environment_member";
3977
- const PKG_INSTALL = "sys_package_installation";
3978
- const PKG = "sys_package";
3979
- const PKG_VERSION = "sys_package_version";
3980
- const ensureSysPackage = async (manifestId, ownerOrgId, createdBy, manifest) => {
3981
- const existing = await ql.findOne(PKG, { where: { manifest_id: manifestId } });
3982
- if (existing?.id) return existing.id;
3983
- const id = randomUUID();
3984
- const nowIso = (/* @__PURE__ */ new Date()).toISOString();
3985
- await ql.insert(PKG, {
3986
- id,
3987
- manifest_id: manifestId,
3988
- owner_org_id: ownerOrgId,
3989
- display_name: manifest?.name ?? manifestId,
3990
- description: manifest?.description ?? null,
3991
- visibility: "private",
3992
- created_by: createdBy,
3993
- created_at: nowIso,
3994
- updated_at: nowIso
3995
- });
3996
- return id;
3997
- };
3998
- const ensureSysPackageVersion = async (packageId, version, createdBy, manifest) => {
3999
- const existing = await ql.findOne(PKG_VERSION, {
4000
- where: { package_id: packageId, version }
4001
- });
4002
- if (existing?.id) return existing.id;
4003
- const id = randomUUID();
4004
- const nowIso = (/* @__PURE__ */ new Date()).toISOString();
4005
- await ql.insert(PKG_VERSION, {
4006
- id,
4007
- package_id: packageId,
4008
- version,
4009
- status: "published",
4010
- manifest_json: manifest ? JSON.stringify(manifest) : null,
4011
- is_pre_release: false,
4012
- published_at: nowIso,
4013
- published_by: createdBy,
4014
- created_by: createdBy,
4015
- created_at: nowIso,
4016
- updated_at: nowIso
4017
- });
4018
- return id;
4019
- };
4020
- const findInstallByManifestId = async (envId, manifestId) => {
4021
- const pkgRow = await ql.findOne(PKG, { where: { manifest_id: manifestId } });
4022
- if (!pkgRow?.id) return null;
4023
- return await ql.findOne(PKG_INSTALL, {
4024
- where: { environment_id: envId, package_id: pkgRow.id }
4025
- });
4026
- };
4027
- const toShortName = (driverId) => {
4028
- const prefix = "com.objectstack.driver.";
4029
- return driverId.startsWith(prefix) ? driverId.slice(prefix.length) : driverId;
4030
- };
4031
- const listRegisteredDrivers = () => {
4032
- const services = this.getServicesMap();
4033
- const registry = services["project-provisioning-adapters"];
4034
- if (registry && typeof registry.list === "function") {
4037
+ if (parts[0] === "view" && parts[1]) {
4038
+ const objectName = parts[1];
4039
+ const type = parts[2] || query?.type || "list";
4040
+ const protocol = await this.resolveService("protocol");
4041
+ if (protocol && typeof protocol.getUiView === "function") {
4035
4042
  try {
4036
- const adapters = registry.list();
4037
- const seen = /* @__PURE__ */ new Set();
4038
- const drivers2 = [];
4039
- for (const adapter of adapters ?? []) {
4040
- const name = adapter?.driver;
4041
- if (!name || seen.has(name)) continue;
4042
- seen.add(name);
4043
- drivers2.push({ name, driverId: `com.objectstack.driver.${name}` });
4044
- }
4045
- if (drivers2.length > 0) return drivers2;
4046
- } catch {
4043
+ const result = await protocol.getUiView({ object: objectName, type });
4044
+ return { handled: true, response: this.success(result) };
4045
+ } catch (e) {
4046
+ return { handled: true, response: this.error(e.message, 500) };
4047
4047
  }
4048
+ } else {
4049
+ return { handled: true, response: this.error("Protocol service not available", 503) };
4048
4050
  }
4049
- const drivers = [];
4050
- for (const [serviceKey, svc] of Object.entries(services)) {
4051
- if (!serviceKey.startsWith("driver.")) continue;
4052
- const raw = serviceKey.slice("driver.".length);
4053
- if (!raw || raw === "unknown") continue;
4054
- const driverId = svc?.name ?? raw;
4055
- drivers.push({ name: toShortName(driverId), driverId });
4051
+ }
4052
+ return { handled: false };
4053
+ }
4054
+ /**
4055
+ * Handles Automation requests
4056
+ * path: sub-path after /automation/
4057
+ *
4058
+ * Routes:
4059
+ * GET / → listFlows
4060
+ * GET /actions → getActionDescriptors (ADR-0018; ?paradigm/?source/?category filters)
4061
+ * GET /connectors → getConnectorDescriptors (ADR-0022; ?type filter)
4062
+ * GET /:name → getFlow
4063
+ * POST / → createFlow (registerFlow)
4064
+ * PUT /:name → updateFlow
4065
+ * DELETE /:name → deleteFlow (unregisterFlow)
4066
+ * POST /:name/trigger → execute (legacy: trigger/:name also supported)
4067
+ * POST /:name/toggle → toggleFlow
4068
+ * GET /:name/runs → listRuns
4069
+ * GET /:name/runs/:runId → getRun
4070
+ * POST /:name/runs/:runId/resume → resume a paused run (screen input / ADR-0019)
4071
+ * GET /:name/runs/:runId/screen → the screen a paused run awaits
4072
+ */
4073
+ async handleAutomation(path, method, body, context, query) {
4074
+ const automationService = await this.getService(CoreServiceName.enum.automation);
4075
+ if (!automationService) return { handled: false };
4076
+ const m = method.toUpperCase();
4077
+ const parts = path.replace(/^\/+/, "").split("/").filter(Boolean);
4078
+ if (parts[0] === "trigger" && parts[1] && m === "POST") {
4079
+ const triggerName = parts[1];
4080
+ if (typeof automationService.trigger === "function") {
4081
+ const result = await automationService.trigger(triggerName, body, { request: context.request });
4082
+ return { handled: true, response: this.success(result) };
4056
4083
  }
4057
- return drivers;
4058
- };
4059
- const resolveDriver = (requested) => {
4060
- const registered = listRegisteredDrivers();
4061
- if (requested) {
4062
- const wanted = String(requested).toLowerCase();
4063
- return registered.find((d) => d.name === wanted || d.driverId === wanted);
4084
+ if (typeof automationService.execute === "function") {
4085
+ const result = await automationService.execute(triggerName, body);
4086
+ return { handled: true, response: this.success(result) };
4064
4087
  }
4065
- return registered.find((d) => d.name === "turso") ?? registered.find((d) => d.name === "memory") ?? registered[0];
4066
- };
4067
- const buildDatabaseUrl = (driverName, environmentId) => {
4068
- const dbName = `env-${environmentId}`;
4069
- switch (driverName) {
4070
- case "memory":
4071
- return `memory://${dbName}`;
4072
- case "turso":
4073
- return `libsql://${dbName}.mock-turso.local`;
4074
- default:
4075
- return `${driverName}://${dbName}`;
4088
+ }
4089
+ if (parts.length === 0 && m === "GET") {
4090
+ if (typeof automationService.listFlows === "function") {
4091
+ const names = await automationService.listFlows();
4092
+ return { handled: true, response: this.success({ flows: names, total: names.length, hasMore: false }) };
4076
4093
  }
4077
- };
4078
- const getRealAdapter = async (driverName) => {
4079
- try {
4080
- const registry = await this.resolveService("project-provisioning-adapters");
4081
- const aliases = { sql: "sqlite" };
4082
- const effective = aliases[driverName] ?? driverName;
4083
- return registry?.get?.(effective) ?? registry?.get?.(driverName);
4084
- } catch {
4085
- return void 0;
4094
+ }
4095
+ if (parts.length === 0 && m === "POST") {
4096
+ if (typeof automationService.registerFlow === "function") {
4097
+ automationService.registerFlow(body?.name, body);
4098
+ return { handled: true, response: this.success(body) };
4086
4099
  }
4087
- };
4088
- const findOne = async (obj, where) => {
4089
- let rows = await ql.find(obj, { where });
4090
- if (rows && rows.value) rows = rows.value;
4091
- if (!Array.isArray(rows)) return void 0;
4092
- return rows[0];
4093
- };
4094
- const cleanProjectRow = (row) => {
4095
- if (!row) return row;
4096
- let metadata = row.metadata;
4097
- if (typeof metadata === "string") {
4098
- try {
4099
- metadata = JSON.parse(metadata);
4100
- } catch {
4100
+ }
4101
+ if (parts[0] === "actions" && parts.length === 1 && m === "GET") {
4102
+ if (typeof automationService.getActionDescriptors === "function") {
4103
+ let actions = automationService.getActionDescriptors() ?? [];
4104
+ if (query?.paradigm) {
4105
+ actions = actions.filter((a) => Array.isArray(a?.paradigms) && a.paradigms.includes(query.paradigm));
4106
+ }
4107
+ if (query?.source) {
4108
+ actions = actions.filter((a) => a?.source === query.source);
4101
4109
  }
4110
+ if (query?.category) {
4111
+ actions = actions.filter((a) => a?.category === query.category);
4112
+ }
4113
+ return { handled: true, response: this.success({ actions, total: actions.length }) };
4102
4114
  }
4103
- return { ...row, metadata };
4104
- };
4105
- try {
4106
- if (parts.length === 1 && parts[0] === "drivers" && m === "GET") {
4107
- const drivers = listRegisteredDrivers();
4108
- return { handled: true, response: this.success({ drivers, total: drivers.length }) };
4115
+ return { handled: true, response: this.success({ actions: [], total: 0 }) };
4116
+ }
4117
+ if (parts[0] === "connectors" && parts.length === 1 && m === "GET") {
4118
+ if (typeof automationService.getConnectorDescriptors === "function") {
4119
+ let connectors = automationService.getConnectorDescriptors() ?? [];
4120
+ if (query?.type) {
4121
+ connectors = connectors.filter((c) => c?.type === query.type);
4122
+ }
4123
+ return { handled: true, response: this.success({ connectors, total: connectors.length }) };
4109
4124
  }
4110
- if (parts.length === 1 && parts[0] === "templates" && m === "GET") {
4111
- try {
4112
- const seeder = await this.resolveService("template-seeder");
4113
- const templates = seeder?.listTemplates?.() ?? [];
4114
- return { handled: true, response: this.success({ templates, total: templates.length }) };
4115
- } catch (err) {
4116
- try {
4117
- console.error("[HttpDispatcher] /cloud/templates: failed to resolve template-seeder:", err?.message ?? err);
4118
- } catch {
4125
+ return { handled: true, response: this.success({ connectors: [], total: 0 }) };
4126
+ }
4127
+ if (parts.length >= 1) {
4128
+ const name = parts[0];
4129
+ if (parts[1] === "trigger" && m === "POST") {
4130
+ if (typeof automationService.execute === "function") {
4131
+ const ctxBody = body && typeof body === "object" ? body : {};
4132
+ const recordId = ctxBody.recordId;
4133
+ const objectName = ctxBody.objectName ?? ctxBody.object;
4134
+ const baseParams = ctxBody.params && typeof ctxBody.params === "object" ? { ...ctxBody.params } : {};
4135
+ if (!ctxBody.params) {
4136
+ const reserved = /* @__PURE__ */ new Set(["recordId", "objectName", "object", "event", "params"]);
4137
+ for (const [k, v] of Object.entries(ctxBody)) {
4138
+ if (reserved.has(k)) continue;
4139
+ if (baseParams[k] === void 0) baseParams[k] = v;
4140
+ }
4141
+ }
4142
+ if (recordId !== void 0 && baseParams.recordId === void 0) {
4143
+ baseParams.recordId = recordId;
4144
+ }
4145
+ if (recordId !== void 0 && objectName) {
4146
+ const alias = `${String(objectName).replace(/_([a-z])/g, (_, c) => c.toUpperCase())}Id`;
4147
+ if (baseParams[alias] === void 0) baseParams[alias] = recordId;
4119
4148
  }
4120
- return { handled: true, response: this.success({ templates: [], total: 0 }) };
4149
+ const automationContext = {
4150
+ params: baseParams,
4151
+ object: objectName,
4152
+ event: ctxBody.event ?? "manual"
4153
+ };
4154
+ const userIdFromAuth = context?.user?.id ?? context?.userId;
4155
+ if (userIdFromAuth) automationContext.userId = userIdFromAuth;
4156
+ const result = await automationService.execute(name, automationContext);
4157
+ return { handled: true, response: this.success(result) };
4121
4158
  }
4122
4159
  }
4123
- if (parts.length === 3 && parts[0] === "admin" && parts[1] === "platform-sso" && parts[2] === "backfill" && m === "POST") {
4124
- const baseSecret = (readEnvWithDeprecation3("OS_AUTH_SECRET", ["AUTH_SECRET", "BETTER_AUTH_SECRET"]) ?? "").trim();
4125
- if (!baseSecret) {
4126
- return { handled: true, response: this.error("OS_AUTH_SECRET not configured on this worker", 503) };
4160
+ if (parts[1] === "toggle" && m === "POST") {
4161
+ if (typeof automationService.toggleFlow === "function") {
4162
+ await automationService.toggleFlow(name, body?.enabled ?? true);
4163
+ return { handled: true, response: this.success({ name, enabled: body?.enabled ?? true }) };
4127
4164
  }
4128
- const rawHeaders = _context?.request?.headers;
4129
- let authHeader;
4130
- if (rawHeaders && typeof rawHeaders.get === "function") {
4131
- authHeader = rawHeaders.get("authorization") ?? void 0;
4132
- } else if (rawHeaders && typeof rawHeaders === "object") {
4133
- authHeader = rawHeaders["authorization"] ?? rawHeaders["Authorization"];
4165
+ }
4166
+ if (parts[1] === "runs" && parts[2] && parts[3] === "resume" && m === "POST") {
4167
+ if (typeof automationService.resume === "function") {
4168
+ const b = body && typeof body === "object" ? body : {};
4169
+ const inputs = b.inputs ?? b.variables;
4170
+ const signal = {};
4171
+ if (inputs && typeof inputs === "object") signal.variables = inputs;
4172
+ if (b.output && typeof b.output === "object") signal.output = b.output;
4173
+ if (typeof b.branchLabel === "string") signal.branchLabel = b.branchLabel;
4174
+ const result = await automationService.resume(parts[2], signal);
4175
+ return { handled: true, response: this.success(result) };
4134
4176
  }
4135
- const presented = typeof authHeader === "string" && authHeader.startsWith("Bearer ") ? authHeader.slice(7).trim() : "";
4136
- if (!presented || presented !== baseSecret) {
4137
- return { handled: true, response: this.error("forbidden: Bearer token must match OS_AUTH_SECRET", 403) };
4138
- }
4139
- try {
4140
- const { backfillPlatformSsoClients: backfillPlatformSsoClients2 } = await Promise.resolve().then(() => (init_platform_sso(), platform_sso_exports));
4141
- const result = await backfillPlatformSsoClients2({
4142
- ql,
4143
- baseSecret,
4144
- logger: console
4145
- });
4146
- let sample = [];
4147
- let total = 0;
4148
- try {
4149
- const rows = await ql.find("sys_oauth_application", { limit: 5 }, { context: { isSystem: true } });
4150
- const list = Array.isArray(rows) ? rows : Array.isArray(rows?.records) ? rows.records : [];
4151
- sample = list;
4152
- total = typeof rows?.total === "number" ? rows.total : list.length;
4153
- } catch (e) {
4154
- sample = [{ _readErr: e?.message ?? String(e) }];
4155
- }
4156
- return { handled: true, response: this.success({ ...result, total, sample }) };
4157
- } catch (err) {
4158
- return { handled: true, response: this.error(`backfill failed: ${err?.message ?? String(err)}`, 500) };
4159
- }
4160
- }
4161
- if (parts.length === 1 && parts[0] === "projects" && m === "GET") {
4162
- const where = {};
4163
- if (query?.organizationId) where.organization_id = query.organizationId;
4164
- if (query?.status) where.status = query.status;
4165
- let rows = await ql.find(ENV, Object.keys(where).length ? { where } : void 0);
4166
- if (rows && rows.value) rows = rows.value;
4167
- const projects = (Array.isArray(rows) ? rows : []).map(cleanProjectRow);
4168
- return { handled: true, response: this.success({ projects, total: projects.length }) };
4169
- }
4170
- if (parts.length === 1 && parts[0] === "projects" && m === "POST") {
4171
- const req = body || {};
4172
- if (req.organization_id === "__session__" || req.created_by === "__session__") {
4173
- try {
4174
- const userId = await this.resolveCallerUserId(_context);
4175
- if (req.created_by === "__session__") {
4176
- req.created_by = userId ?? "system";
4177
- }
4178
- if (req.organization_id === "__session__") {
4179
- const authService = await this.resolveService(CoreServiceName.enum.auth);
4180
- const rawHeaders = _context?.request?.headers;
4181
- let headers = rawHeaders;
4182
- if (rawHeaders && typeof rawHeaders === "object" && typeof rawHeaders.get !== "function") {
4183
- const h = new Headers();
4184
- for (const [k, v] of Object.entries(rawHeaders)) {
4185
- if (v == null) continue;
4186
- h.set(k, Array.isArray(v) ? v.join(", ") : String(v));
4187
- }
4188
- headers = h;
4189
- }
4190
- const apiObj = authService?.auth?.api ?? authService?.api;
4191
- const sessionData = await apiObj?.getSession?.call(apiObj, { headers });
4192
- req.organization_id = sessionData?.session?.activeOrganizationId ?? void 0;
4193
- }
4194
- } catch {
4195
- }
4196
- }
4197
- if (!req.organization_id || !req.display_name) {
4198
- return { handled: true, response: this.error("organization_id and display_name are required", 400) };
4199
- }
4200
- const environmentId = randomUUID();
4201
- const credentialId = randomUUID();
4202
- const nowIso = (/* @__PURE__ */ new Date()).toISOString();
4203
- const resolved = resolveDriver(req.driver);
4204
- if (!resolved) {
4205
- const available = listRegisteredDrivers().map((d) => d.name);
4206
- if (req.driver) {
4207
- return {
4208
- handled: true,
4209
- response: this.error(
4210
- `Unknown driver '${req.driver}'. Available drivers: [${available.join(", ") || "none"}]`,
4211
- 400
4212
- )
4213
- };
4214
- }
4215
- return {
4216
- handled: true,
4217
- response: this.error(
4218
- "No ObjectQL driver is registered. Register at least one DriverPlugin (e.g. InMemoryDriver or SqlDriver).",
4219
- 503
4220
- )
4221
- };
4222
- }
4223
- const driver = resolved.name;
4224
- let plaintextSecret = `mock-token-${environmentId}`;
4225
- let computedHostname = req.hostname;
4226
- if (!computedHostname) {
4227
- const shortId = environmentId.slice(0, 8);
4228
- try {
4229
- const orgRow = await findOne("sys_organization", { id: req.organization_id });
4230
- const orgSlug = orgRow?.slug || req.organization_id;
4231
- const rootDomain = getEnv("OS_ROOT_DOMAIN") ?? getEnv("ROOT_DOMAIN", "objectstack.app");
4232
- computedHostname = `${orgSlug}-${shortId}.${rootDomain}`;
4233
- } catch {
4234
- computedHostname = `${req.organization_id}-${shortId}.objectstack.app`;
4235
- }
4236
- }
4237
- try {
4238
- const existing = await findOne("sys_environment", {
4239
- hostname: computedHostname
4240
- });
4241
- if (existing && existing.id !== environmentId) {
4242
- return {
4243
- handled: true,
4244
- response: this.error(
4245
- `Hostname '${computedHostname}' is already in use by another project.`,
4246
- 409,
4247
- { code: "HOSTNAME_TAKEN", hostname: computedHostname }
4248
- )
4249
- };
4250
- }
4251
- } catch {
4252
- }
4253
- const baseMetadata = { ...req.metadata ?? {} };
4254
- const simulateFailure = Boolean(baseMetadata.__simulateFailure);
4255
- const simulateDelayMs = Number(baseMetadata.__simulateDelayMs ?? 1500);
4256
- try {
4257
- let ownerUserId = req.created_by && req.created_by !== "system" ? String(req.created_by) : void 0;
4258
- if (!ownerUserId) {
4259
- ownerUserId = await this.resolveCallerUserId(_context);
4260
- }
4261
- if (ownerUserId) {
4262
- const userRow = await ql.find("sys_user", { where: { id: ownerUserId } });
4263
- const userRows = Array.isArray(userRow) ? userRow : userRow?.value ?? [];
4264
- const u = Array.isArray(userRows) && userRows.length > 0 ? userRows[0] : null;
4265
- if (u?.email) {
4266
- baseMetadata.ownerSeed = {
4267
- userId: String(ownerUserId),
4268
- email: String(u.email),
4269
- name: u.name ? String(u.name) : null,
4270
- image: u.image ? String(u.image) : null
4271
- };
4272
- }
4273
- }
4274
- } catch {
4275
- }
4276
- try {
4277
- const orgRow = await ql.find("sys_organization", { where: { id: req.organization_id } });
4278
- const orgRows = Array.isArray(orgRow) ? orgRow : orgRow?.value ?? [];
4279
- const org = Array.isArray(orgRows) && orgRows.length > 0 ? orgRows[0] : null;
4280
- if (org?.id && org?.name) {
4281
- baseMetadata.orgSeed = {
4282
- id: String(org.id),
4283
- name: String(org.name),
4284
- slug: org.slug ? String(org.slug) : null,
4285
- logo: org.logo ? String(org.logo) : null
4286
- };
4287
- }
4288
- } catch {
4289
- }
4290
- await ql.insert(ENV, {
4291
- id: environmentId,
4292
- organization_id: req.organization_id,
4293
- display_name: req.display_name,
4294
- is_default: req.is_default ?? false,
4295
- is_system: req.is_system ?? false,
4296
- plan: req.plan ?? "free",
4297
- status: "provisioning",
4298
- created_by: req.created_by ?? "system",
4299
- metadata: JSON.stringify(baseMetadata),
4300
- created_at: nowIso,
4301
- updated_at: nowIso,
4302
- database_url: null,
4303
- database_driver: driver,
4304
- storage_limit_mb: req.storage_limit_mb ?? 1024,
4305
- provisioned_at: null,
4306
- hostname: computedHostname,
4307
- visibility: (() => {
4308
- const raw = String(req.visibility ?? "private");
4309
- return raw === "unlisted" ? "private" : raw;
4310
- })()
4311
- });
4312
- try {
4313
- const { seedPlatformSsoClient: seedPlatformSsoClient2 } = await Promise.resolve().then(() => (init_platform_sso(), platform_sso_exports));
4314
- const baseSecret = (readEnvWithDeprecation3("OS_AUTH_SECRET", ["AUTH_SECRET", "BETTER_AUTH_SECRET"]) ?? "").trim();
4315
- if (baseSecret) {
4316
- await seedPlatformSsoClient2({
4317
- ql,
4318
- environmentId,
4319
- hostname: computedHostname,
4320
- baseSecret,
4321
- logger: console
4322
- });
4323
- }
4324
- } catch (ssoErr) {
4325
- console.warn?.("[http-dispatcher] platform SSO seed failed (non-fatal)", {
4326
- environmentId,
4327
- error: ssoErr?.message
4328
- });
4329
- }
4330
- const runProvisioning = async () => {
4331
- try {
4332
- if (simulateDelayMs > 0) {
4333
- await new Promise((r) => setTimeout(r, simulateDelayMs));
4334
- }
4335
- if (simulateFailure) {
4336
- throw new Error("Simulated provisioning failure (metadata.__simulateFailure=true)");
4337
- }
4338
- let databaseUrl;
4339
- try {
4340
- const adapter = await getRealAdapter(driver);
4341
- if (adapter) {
4342
- const result = await adapter.createDatabase({
4343
- environmentId,
4344
- databaseName: `p-${environmentId.replace(/-/g, "").slice(0, 24)}`,
4345
- region: "us-east-1",
4346
- storageLimitMb: req.storage_limit_mb ?? 1024
4347
- });
4348
- databaseUrl = result.databaseUrl;
4349
- if (result.plaintextSecret) plaintextSecret = result.plaintextSecret;
4350
- } else {
4351
- databaseUrl = buildDatabaseUrl(driver, environmentId);
4352
- }
4353
- } catch (adapterErr) {
4354
- throw adapterErr instanceof Error ? adapterErr : new Error(String(adapterErr));
4355
- }
4356
- const seedStartedAt = (/* @__PURE__ */ new Date()).toISOString();
4357
- await ql.update(
4358
- ENV,
4359
- {
4360
- database_url: databaseUrl,
4361
- updated_at: seedStartedAt
4362
- },
4363
- { where: { id: environmentId } }
4364
- );
4365
- await ql.insert(CRED, {
4366
- id: credentialId,
4367
- environment_id: environmentId,
4368
- secret_ciphertext: plaintextSecret,
4369
- encryption_key_id: "noop",
4370
- authorization: "full_access",
4371
- status: "active",
4372
- created_at: seedStartedAt,
4373
- updated_at: seedStartedAt
4374
- });
4375
- const templateId = req.template_id ?? "blank";
4376
- if (templateId !== "blank") {
4377
- try {
4378
- const seeder = await this.resolveService("template-seeder");
4379
- if (seeder) {
4380
- await seeder.seed({ environmentId, templateId });
4381
- }
4382
- } catch (seedErr) {
4383
- const seedMessage = seedErr instanceof Error ? seedErr.message : String(seedErr);
4384
- try {
4385
- const existing = await findOne(ENV, { id: environmentId });
4386
- const existingMeta = typeof existing?.metadata === "string" ? JSON.parse(existing.metadata) : existing?.metadata ?? {};
4387
- await ql.update(
4388
- ENV,
4389
- {
4390
- metadata: JSON.stringify({
4391
- ...existingMeta,
4392
- templateSeedError: { message: seedMessage, templateId }
4393
- })
4394
- },
4395
- { where: { id: environmentId } }
4396
- );
4397
- } catch {
4398
- }
4399
- }
4400
- }
4401
- const artifactPathRaw = baseMetadata.artifact_path;
4402
- if (typeof artifactPathRaw === "string" && artifactPathRaw.length > 0) {
4403
- try {
4404
- const path2 = await import("path");
4405
- const { isHttpUrl: isHttpUrl2, loadArtifactBundle: loadArtifactBundle2 } = await Promise.resolve().then(() => (init_load_artifact_bundle(), load_artifact_bundle_exports));
4406
- const root = process.env.OS_PROJECT_ARTIFACT_ROOT ?? process.cwd();
4407
- const resolved2 = isHttpUrl2(artifactPathRaw) ? artifactPathRaw : path2.isAbsolute(artifactPathRaw) ? artifactPathRaw : path2.resolve(root, artifactPathRaw);
4408
- const bundle = await loadArtifactBundle2(resolved2, { tag: "[bind-artifact]" });
4409
- if (!bundle) {
4410
- throw new Error(`failed to load artifact bundle at '${resolved2}'`);
4411
- }
4412
- const seeder = await this.resolveService("template-seeder");
4413
- if (seeder?.seedBundle) {
4414
- await seeder.seedBundle({ environmentId, bundle });
4415
- } else {
4416
- throw new Error("template-seeder.seedBundle is unavailable");
4417
- }
4418
- } catch (bindErr) {
4419
- const bindMessage = bindErr instanceof Error ? bindErr.message : String(bindErr);
4420
- try {
4421
- const existing = await findOne(ENV, { id: environmentId });
4422
- const existingMeta = typeof existing?.metadata === "string" ? JSON.parse(existing.metadata) : existing?.metadata ?? {};
4423
- await ql.update(
4424
- ENV,
4425
- {
4426
- metadata: JSON.stringify({
4427
- ...existingMeta,
4428
- artifactBindError: { message: bindMessage, artifactPath: artifactPathRaw }
4429
- })
4430
- },
4431
- { where: { id: environmentId } }
4432
- );
4433
- } catch {
4434
- }
4435
- }
4436
- }
4437
- const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
4438
- await ql.update(
4439
- ENV,
4440
- {
4441
- status: "active",
4442
- provisioned_at: finishedAt,
4443
- updated_at: finishedAt
4444
- },
4445
- { where: { id: environmentId } }
4446
- );
4447
- } catch (err) {
4448
- const message = err instanceof Error ? err.message : String(err);
4449
- const failedAt = (/* @__PURE__ */ new Date()).toISOString();
4450
- await ql.update(
4451
- ENV,
4452
- {
4453
- status: "failed",
4454
- metadata: JSON.stringify({
4455
- ...baseMetadata,
4456
- provisioningError: { message, failedAt }
4457
- }),
4458
- updated_at: failedAt
4459
- },
4460
- { where: { id: environmentId } }
4461
- );
4462
- }
4463
- };
4464
- const provisionSyncEnv = process.env.OS_PROVISION_SYNC;
4465
- const onServerless = !!(process.env.VERCEL || process.env.AWS_LAMBDA_FUNCTION_NAME || process.env.NETLIFY || process.env.CF_PAGES);
4466
- const syncProvisioning = provisionSyncEnv === void 0 ? onServerless : provisionSyncEnv !== "0" && provisionSyncEnv !== "false";
4467
- if (syncProvisioning) {
4468
- await runProvisioning();
4469
- } else {
4470
- void runProvisioning();
4471
- }
4472
- const project = cleanProjectRow(await findOne(ENV, { id: environmentId }));
4473
- const res = this.success({ project });
4474
- res.status = syncProvisioning ? 201 : 202;
4475
- return { handled: true, response: res };
4476
- }
4477
- if (parts.length === 2 && parts[0] === "projects") {
4478
- const id = decodeURIComponent(parts[1]);
4479
- if (m === "GET") {
4480
- const envRow = await findOne(ENV, { id });
4481
- if (!envRow) return { handled: true, response: this.error(`Project '${id}' not found`, 404) };
4482
- const credRow = await findOne(CRED, { environment_id: id, status: "active" });
4483
- const callerUserId = await this.resolveCallerUserId(_context);
4484
- const membership = callerUserId ? await findOne(MEM, { environment_id: id, user_id: callerUserId }) : await findOne(MEM, { environment_id: id });
4485
- const credMeta = credRow ? {
4486
- id: credRow.id,
4487
- status: credRow.status,
4488
- authorization: credRow.authorization,
4489
- activatedAt: credRow.created_at,
4490
- expiresAt: credRow.expires_at
4491
- } : void 0;
4492
- const project = cleanProjectRow(envRow);
4493
- const database = project.database_url ? {
4494
- driver: project.database_driver,
4495
- database_name: `env-${project.id}`,
4496
- database_url: project.database_url,
4497
- storage_limit_mb: project.storage_limit_mb,
4498
- provisioned_at: project.provisioned_at
4499
- } : void 0;
4500
- return {
4501
- handled: true,
4502
- response: this.success({ project, database, credential: credMeta, membership })
4503
- };
4504
- }
4505
- if (m === "PATCH") {
4506
- const patch = {};
4507
- if (body?.display_name !== void 0) patch.display_name = body.display_name;
4508
- if (body?.plan !== void 0) patch.plan = body.plan;
4509
- if (body?.status !== void 0) patch.status = body.status;
4510
- if (body?.is_default !== void 0) patch.is_default = body.is_default;
4511
- if (body?.visibility !== void 0) {
4512
- let v = String(body.visibility);
4513
- if (v === "unlisted") v = "private";
4514
- if (!["private", "public"].includes(v)) {
4515
- return { handled: true, response: this.error(`Invalid visibility '${v}' (expected private | public)`, 400) };
4516
- }
4517
- patch.visibility = v;
4518
- }
4519
- if (body?.metadata !== void 0) patch.metadata = JSON.stringify(body.metadata);
4520
- patch.updated_at = (/* @__PURE__ */ new Date()).toISOString();
4521
- await ql.update(ENV, patch, { where: { id } });
4522
- const envRow = await findOne(ENV, { id });
4523
- if (!envRow) return { handled: true, response: this.error(`Project '${id}' not found`, 404) };
4524
- return { handled: true, response: this.success({ project: cleanProjectRow(envRow) }) };
4525
- }
4526
- if (m === "DELETE") {
4527
- const force = query?.force === "1" || query?.force === "true" || body?.force === true;
4528
- const result = await this.deleteProjectCascade(id, { ql, findOne, getRealAdapter, force });
4529
- if (!result.ok) {
4530
- return { handled: true, response: this.error(result.error ?? "Delete failed", result.status ?? 500) };
4531
- }
4532
- return { handled: true, response: this.success({ deleted: true, environmentId: id, warnings: result.warnings }) };
4533
- }
4534
- }
4535
- if (parts.length === 2 && parts[0] === "organizations" && m === "DELETE") {
4536
- const orgId = decodeURIComponent(parts[1]);
4537
- let projectRows = [];
4538
- try {
4539
- let rows = await ql.find(ENV, { where: { organization_id: orgId } });
4540
- if (rows && rows.value) rows = rows.value;
4541
- projectRows = Array.isArray(rows) ? rows : [];
4542
- } catch {
4543
- projectRows = [];
4544
- }
4545
- const warnings = [];
4546
- let deletedProjects = 0;
4547
- for (const row of projectRows) {
4548
- const pid = row?.id;
4549
- if (!pid) continue;
4550
- try {
4551
- const r = await this.deleteProjectCascade(pid, { ql, findOne, getRealAdapter, force: true });
4552
- if (r.ok) deletedProjects++;
4553
- if (r.warnings?.length) warnings.push(...r.warnings);
4554
- if (!r.ok && r.error) warnings.push(`Project ${pid}: ${r.error}`);
4555
- } catch (err) {
4556
- warnings.push(
4557
- `Failed to delete project ${pid}: ${err instanceof Error ? err.message : String(err)}`
4558
- );
4559
- }
4560
- }
4561
- let orgDeleted = false;
4562
- try {
4563
- const authService = await this.getService(CoreServiceName.enum.auth);
4564
- const fn = authService?.api?.deleteOrganization;
4565
- if (typeof fn === "function") {
4566
- await fn.call(authService.api, {
4567
- body: { organizationId: orgId },
4568
- headers: _context?.request?.headers
4569
- });
4570
- orgDeleted = true;
4571
- }
4572
- } catch (err) {
4573
- warnings.push(
4574
- `auth.deleteOrganization failed: ${err instanceof Error ? err.message : String(err)}`
4575
- );
4576
- }
4577
- if (!orgDeleted) {
4578
- try {
4579
- await ql.delete("sys_organization", { where: { id: orgId } });
4580
- orgDeleted = true;
4581
- } catch (err) {
4582
- warnings.push(
4583
- `Failed to delete sys_organization row: ${err instanceof Error ? err.message : String(err)}`
4584
- );
4585
- }
4586
- }
4587
- return {
4588
- handled: true,
4589
- response: this.success({
4590
- deleted: orgDeleted,
4591
- organizationId: orgId,
4592
- deletedProjects,
4593
- warnings
4594
- })
4595
- };
4596
- }
4597
- if (parts.length === 3 && parts[0] === "projects" && parts[2] === "hostname" && (m === "POST" || m === "PUT")) {
4598
- const id = decodeURIComponent(parts[1]);
4599
- const hostname = body?.hostname;
4600
- if (!hostname || typeof hostname !== "string") {
4601
- return { handled: true, response: this.error("hostname is required", 400) };
4602
- }
4603
- const normalized = hostname.trim().toLowerCase();
4604
- if (!/^[a-z0-9]([a-z0-9\-\.]*[a-z0-9])?$/.test(normalized)) {
4605
- return { handled: true, response: this.error("Invalid hostname format", 400) };
4606
- }
4607
- const envRow = await findOne(ENV, { id });
4608
- if (!envRow) return { handled: true, response: this.error(`Project '${id}' not found`, 404) };
4609
- let existing;
4610
- try {
4611
- const rows = await ql.find(ENV, { where: { hostname: normalized } });
4612
- const arr = Array.isArray(rows) ? rows : rows?.value ?? [];
4613
- existing = arr.find((r) => r.id !== id);
4614
- } catch {
4615
- }
4616
- if (existing) {
4617
- return {
4618
- handled: true,
4619
- response: this.error(
4620
- `Hostname '${normalized}' is already in use by another project.`,
4621
- 409,
4622
- { code: "HOSTNAME_TAKEN", hostname: normalized }
4623
- )
4624
- };
4625
- }
4626
- const updatedAt = (/* @__PURE__ */ new Date()).toISOString();
4627
- await ql.update(ENV, { hostname: normalized, updated_at: updatedAt }, { where: { id } });
4628
- if (this.envRegistry?.invalidate) {
4629
- try {
4630
- await this.envRegistry.invalidate(id);
4631
- } catch {
4632
- }
4633
- }
4634
- const updated = cleanProjectRow(await findOne(ENV, { id }));
4635
- return { handled: true, response: this.success({ project: updated }) };
4636
- }
4637
- if (parts.length === 3 && parts[0] === "projects" && parts[2] === "retry" && m === "POST") {
4638
- const id = decodeURIComponent(parts[1]);
4639
- const envRow = await findOne(ENV, { id });
4640
- if (!envRow) return { handled: true, response: this.error(`Project '${id}' not found`, 404) };
4641
- if (envRow.status !== "failed" && envRow.status !== "provisioning") {
4642
- return {
4643
- handled: true,
4644
- response: this.error(
4645
- `Project '${id}' is '${envRow.status}'; only failed or provisioning projects can be retried.`,
4646
- 409
4647
- )
4648
- };
4649
- }
4650
- const driverName = envRow.database_driver;
4651
- const resolved = resolveDriver(driverName);
4652
- if (!resolved) {
4653
- return {
4654
- handled: true,
4655
- response: this.error(
4656
- `Driver '${driverName}' is no longer registered; retry aborted.`,
4657
- 503
4658
- )
4659
- };
4660
- }
4661
- let metadata = {};
4662
- if (envRow.metadata) {
4663
- if (typeof envRow.metadata === "string") {
4664
- try {
4665
- metadata = JSON.parse(envRow.metadata);
4666
- } catch {
4667
- metadata = {};
4668
- }
4669
- } else if (typeof envRow.metadata === "object") {
4670
- metadata = { ...envRow.metadata };
4671
- }
4672
- }
4673
- delete metadata.provisioningError;
4674
- const retryStartedAt = (/* @__PURE__ */ new Date()).toISOString();
4675
- await ql.update(
4676
- ENV,
4677
- {
4678
- status: "provisioning",
4679
- metadata: JSON.stringify(metadata),
4680
- updated_at: retryStartedAt
4681
- },
4682
- { where: { id } }
4683
- );
4684
- const simulateRetryFailure = Boolean(metadata.__simulateFailure);
4685
- const simulateRetryDelay = Number(metadata.__simulateDelayMs ?? 1500);
4686
- const runRetry = async () => {
4687
- try {
4688
- if (simulateRetryDelay > 0) {
4689
- await new Promise((r) => setTimeout(r, simulateRetryDelay));
4690
- }
4691
- if (simulateRetryFailure) {
4692
- throw new Error("Simulated provisioning failure (metadata.__simulateFailure=true)");
4693
- }
4694
- let databaseUrl;
4695
- let retrySecret = `mock-token-${id}`;
4696
- try {
4697
- const adapter = await getRealAdapter(resolved.name);
4698
- if (adapter) {
4699
- const result = await adapter.createDatabase({
4700
- environmentId: id,
4701
- databaseName: `p-${id.replace(/-/g, "").slice(0, 24)}`,
4702
- region: "us-east-1",
4703
- storageLimitMb: envRow.storage_limit_mb ?? 1024
4704
- });
4705
- databaseUrl = result.databaseUrl;
4706
- if (result.plaintextSecret) retrySecret = result.plaintextSecret;
4707
- } else {
4708
- databaseUrl = buildDatabaseUrl(resolved.name, id);
4709
- }
4710
- } catch (adapterErr) {
4711
- throw adapterErr instanceof Error ? adapterErr : new Error(String(adapterErr));
4712
- }
4713
- const nowIso = (/* @__PURE__ */ new Date()).toISOString();
4714
- await ql.update(
4715
- ENV,
4716
- {
4717
- status: "active",
4718
- database_url: databaseUrl,
4719
- database_driver: resolved.name,
4720
- provisioned_at: nowIso,
4721
- updated_at: nowIso
4722
- },
4723
- { where: { id } }
4724
- );
4725
- const existingCred = await findOne(CRED, { environment_id: id, status: "active" });
4726
- if (!existingCred) {
4727
- await ql.insert(CRED, {
4728
- id: randomUUID(),
4729
- environment_id: id,
4730
- secret_ciphertext: retrySecret,
4731
- encryption_key_id: "noop",
4732
- authorization: "full_access",
4733
- status: "active",
4734
- created_at: nowIso,
4735
- updated_at: nowIso
4736
- });
4737
- }
4738
- } catch (err) {
4739
- const message = err instanceof Error ? err.message : String(err);
4740
- const failedAt = (/* @__PURE__ */ new Date()).toISOString();
4741
- await ql.update(
4742
- ENV,
4743
- {
4744
- status: "failed",
4745
- metadata: JSON.stringify({
4746
- ...metadata,
4747
- provisioningError: { message, failedAt }
4748
- }),
4749
- updated_at: failedAt
4750
- },
4751
- { where: { id } }
4752
- );
4753
- }
4754
- };
4755
- void runRetry();
4756
- const envAfter = cleanProjectRow(await findOne(ENV, { id }));
4757
- const retryRes = this.success({ project: envAfter });
4758
- retryRes.status = 202;
4759
- return { handled: true, response: retryRes };
4760
- }
4761
- if (parts.length === 3 && parts[0] === "projects" && parts[2] === "activate" && m === "POST") {
4762
- const id = decodeURIComponent(parts[1]);
4763
- const envRow = await findOne(ENV, { id });
4764
- if (!envRow) return { handled: true, response: this.error(`Project '${id}' not found`, 404) };
4765
- return { handled: true, response: this.success({ project: cleanProjectRow(envRow), sessionUpdated: false }) };
4766
- }
4767
- if (parts.length === 4 && parts[0] === "projects" && parts[2] === "credentials" && parts[3] === "rotate" && m === "POST") {
4768
- const id = decodeURIComponent(parts[1]);
4769
- const plaintext = body?.plaintext;
4770
- if (!plaintext || typeof plaintext !== "string") {
4771
- return { handled: true, response: this.error("plaintext is required", 400) };
4772
- }
4773
- const envRow = await findOne(ENV, { id });
4774
- if (!envRow) return { handled: true, response: this.error(`Project '${id}' not found`, 404) };
4775
- const nowIso = (/* @__PURE__ */ new Date()).toISOString();
4776
- let existing = await ql.find(CRED, { where: { environment_id: id, status: "active" } });
4777
- if (existing && existing.value) existing = existing.value;
4778
- for (const row of Array.isArray(existing) ? existing : []) {
4779
- await ql.update(CRED, {
4780
- status: "revoked",
4781
- revoked_at: nowIso,
4782
- updated_at: nowIso
4783
- }, { where: { id: row.id } });
4784
- }
4785
- const credentialId = randomUUID();
4786
- await ql.insert(CRED, {
4787
- id: credentialId,
4788
- environment_id: id,
4789
- secret_ciphertext: plaintext,
4790
- encryption_key_id: "noop",
4791
- authorization: "full_access",
4792
- status: "active",
4793
- created_at: nowIso,
4794
- updated_at: nowIso
4795
- });
4796
- const credential = await findOne(CRED, { id: credentialId });
4797
- const credMeta = credential ? {
4798
- id: credential.id,
4799
- status: credential.status,
4800
- authorization: credential.authorization,
4801
- activatedAt: credential.created_at
4802
- } : void 0;
4803
- return { handled: true, response: this.success({ credential: credMeta }) };
4804
- }
4805
- if (parts.length === 3 && parts[0] === "projects" && parts[2] === "members" && m === "GET") {
4806
- const id = decodeURIComponent(parts[1]);
4807
- let rows = await ql.find(MEM, { where: { environment_id: id } });
4808
- if (rows && rows.value) rows = rows.value;
4809
- const members = Array.isArray(rows) ? rows : [];
4810
- const userIds = Array.from(new Set(members.map((mem) => mem.user_id).filter(Boolean)));
4811
- const userMap = /* @__PURE__ */ new Map();
4812
- for (const uid of userIds) {
4813
- let row = null;
4814
- for (const tableName of ["sys_user", "user"]) {
4815
- try {
4816
- const u = await ql.findOne(tableName, { where: { id: uid } });
4817
- row = u?.value ?? u;
4818
- if (row) break;
4819
- } catch {
4820
- }
4821
- }
4822
- if (row) userMap.set(String(uid), {
4823
- id: row.id,
4824
- name: row.name ?? row.display_name,
4825
- email: row.email,
4826
- image: row.image ?? row.avatar_url
4827
- });
4828
- }
4829
- const enriched = members.map((mem) => ({
4830
- ...mem,
4831
- user: userMap.get(String(mem.user_id)) ?? void 0
4832
- }));
4833
- return { handled: true, response: this.success({ members: enriched }) };
4834
- }
4835
- if (parts.length === 3 && parts[0] === "projects" && parts[2] === "members" && m === "POST") {
4836
- const id = decodeURIComponent(parts[1]);
4837
- const project = await findOne(ENV, { id });
4838
- if (!project) return { handled: true, response: this.error(`Project '${id}' not found`, 404) };
4839
- const callerId = await this.resolveCallerUserId(_context);
4840
- if (!callerId) return { handled: true, response: this.error("Authentication required", 401) };
4841
- const callerMem = await findOne(MEM, { environment_id: id, user_id: callerId });
4842
- if (!callerMem || !["owner", "admin"].includes(String(callerMem.role))) {
4843
- return { handled: true, response: this.error("Forbidden \u2014 owner or admin required", 403) };
4844
- }
4845
- const email = typeof body?.email === "string" ? String(body.email).trim().toLowerCase() : null;
4846
- let inviteUserId = typeof body?.user_id === "string" ? String(body.user_id).trim() : null;
4847
- let role = String(body?.role ?? "member").trim().toLowerCase();
4848
- if (!["owner", "admin", "member", "viewer"].includes(role)) {
4849
- return { handled: true, response: this.error(`Invalid role '${role}' (expected owner | admin | member | viewer)`, 400) };
4850
- }
4851
- if (!email && !inviteUserId) {
4852
- return { handled: true, response: this.error("email or user_id is required", 400) };
4853
- }
4854
- if (!inviteUserId && email) {
4855
- let row = null;
4856
- for (const tableName of ["sys_user", "user"]) {
4857
- try {
4858
- const u = await ql.findOne(tableName, { where: { email } });
4859
- row = u?.value ?? u;
4860
- if (row) break;
4861
- } catch {
4862
- }
4863
- }
4864
- if (!row?.id) {
4865
- return { handled: true, response: this.error(`No user found with email '${email}'`, 404) };
4866
- }
4867
- inviteUserId = String(row.id);
4868
- }
4869
- const existing = await findOne(MEM, { environment_id: id, user_id: inviteUserId });
4870
- if (existing) {
4871
- return { handled: true, response: this.success({ member: existing, alreadyMember: true }) };
4872
- }
4873
- try {
4874
- const memberId = randomUUID();
4875
- await ql.insert(MEM, {
4876
- id: memberId,
4877
- environment_id: id,
4878
- user_id: inviteUserId,
4879
- role,
4880
- invited_by: callerId,
4881
- organization_id: project.organization_id ?? null
4882
- });
4883
- const created = await findOne(MEM, { id: memberId });
4884
- return { handled: true, response: this.success({ member: created, alreadyMember: false }) };
4885
- } catch (e) {
4886
- return { handled: true, response: this.error(e?.message ?? "Failed to add member", 500) };
4887
- }
4888
- }
4889
- if (parts.length === 4 && parts[0] === "projects" && parts[2] === "members" && m === "PATCH") {
4890
- const id = decodeURIComponent(parts[1]);
4891
- const memberId = decodeURIComponent(parts[3]);
4892
- const project = await findOne(ENV, { id });
4893
- if (!project) return { handled: true, response: this.error(`Project '${id}' not found`, 404) };
4894
- const callerId = await this.resolveCallerUserId(_context);
4895
- if (!callerId) return { handled: true, response: this.error("Authentication required", 401) };
4896
- const callerMem = await findOne(MEM, { environment_id: id, user_id: callerId });
4897
- if (!callerMem || !["owner", "admin"].includes(String(callerMem.role))) {
4898
- return { handled: true, response: this.error("Forbidden \u2014 owner or admin required", 403) };
4899
- }
4900
- const target = await findOne(MEM, { id: memberId, environment_id: id });
4901
- if (!target) return { handled: true, response: this.error(`Member '${memberId}' not found`, 404) };
4902
- const newRole = String(body?.role ?? "").trim().toLowerCase();
4903
- if (!["owner", "admin", "member", "viewer"].includes(newRole)) {
4904
- return { handled: true, response: this.error(`Invalid role '${newRole}'`, 400) };
4905
- }
4906
- if (target.role === "owner" && newRole !== "owner") {
4907
- let owners = await ql.find(MEM, { where: { environment_id: id, role: "owner" } });
4908
- if (owners && owners.value) owners = owners.value;
4909
- const ownerCount = Array.isArray(owners) ? owners.length : 0;
4910
- if (ownerCount <= 1) {
4911
- return { handled: true, response: this.error("Cannot demote the last owner", 409) };
4912
- }
4913
- }
4914
- try {
4915
- await ql.update(MEM, { role: newRole, updated_at: (/* @__PURE__ */ new Date()).toISOString() }, { where: { id: memberId } });
4916
- const updated = await findOne(MEM, { id: memberId });
4917
- return { handled: true, response: this.success({ member: updated }) };
4918
- } catch (e) {
4919
- return { handled: true, response: this.error(e?.message ?? "Failed to update role", 500) };
4920
- }
4921
- }
4922
- if (parts.length === 4 && parts[0] === "projects" && parts[2] === "members" && m === "DELETE") {
4923
- const id = decodeURIComponent(parts[1]);
4924
- const memberId = decodeURIComponent(parts[3]);
4925
- const project = await findOne(ENV, { id });
4926
- if (!project) return { handled: true, response: this.error(`Project '${id}' not found`, 404) };
4927
- const callerId = await this.resolveCallerUserId(_context);
4928
- if (!callerId) return { handled: true, response: this.error("Authentication required", 401) };
4929
- const target = await findOne(MEM, { id: memberId, environment_id: id });
4930
- if (!target) return { handled: true, response: this.error(`Member '${memberId}' not found`, 404) };
4931
- const callerMem = await findOne(MEM, { environment_id: id, user_id: callerId });
4932
- const isSelf = String(target.user_id) === String(callerId);
4933
- const isPrivileged = callerMem && ["owner", "admin"].includes(String(callerMem.role));
4934
- if (!isSelf && !isPrivileged) {
4935
- return { handled: true, response: this.error("Forbidden \u2014 owner or admin required", 403) };
4936
- }
4937
- if (target.role === "owner") {
4938
- let owners = await ql.find(MEM, { where: { environment_id: id, role: "owner" } });
4939
- if (owners && owners.value) owners = owners.value;
4940
- const ownerCount = Array.isArray(owners) ? owners.length : 0;
4941
- if (ownerCount <= 1) {
4942
- return { handled: true, response: this.error("Cannot remove the last owner", 409) };
4943
- }
4944
- }
4945
- try {
4946
- await ql.delete(MEM, { where: { id: memberId } });
4947
- return { handled: true, response: this.success({ removed: true, memberId }) };
4948
- } catch (e) {
4949
- return { handled: true, response: this.error(e?.message ?? "Failed to remove member", 500) };
4950
- }
4951
- }
4952
- if (parts.length === 3 && parts[0] === "projects" && parts[2] === "packages" && m === "GET") {
4953
- const envId = decodeURIComponent(parts[1]);
4954
- let rows = await ql.find(PKG_INSTALL, { where: { environment_id: envId } });
4955
- if (rows && rows.value) rows = rows.value;
4956
- const installs = Array.isArray(rows) ? rows : [];
4957
- const packages = await Promise.all(
4958
- installs.map(async (r) => {
4959
- let manifestId = null;
4960
- let versionStr = null;
4961
- try {
4962
- if (r.package_id) {
4963
- const pkg = await ql.findOne(PKG, { where: { id: r.package_id } });
4964
- manifestId = pkg?.manifest_id ?? null;
4965
- }
4966
- if (r.package_version_id) {
4967
- const ver = await ql.findOne(PKG_VERSION, { where: { id: r.package_version_id } });
4968
- versionStr = ver?.version ?? null;
4969
- }
4970
- } catch {
4971
- }
4972
- return {
4973
- ...r,
4974
- // Surface user-facing identifiers expected by client SDK
4975
- packageId: manifestId,
4976
- package_id: manifestId ?? r.package_id,
4977
- version: versionStr ?? r.version ?? null
4978
- };
4979
- })
4980
- );
4981
- return { handled: true, response: this.success({ packages, total: packages.length }) };
4982
- }
4983
- if (parts.length === 3 && parts[0] === "projects" && parts[2] === "packages" && m === "POST") {
4984
- const envId = decodeURIComponent(parts[1]);
4985
- const { packageId, version, settings, enableOnInstall } = body ?? {};
4986
- if (!packageId) return { handled: true, response: this.error("packageId is required", 400) };
4987
- const qlSvc = await this.getObjectQLService();
4988
- const pkgRegistry = qlSvc?.registry;
4989
- const allPkgs = pkgRegistry?.getAllPackages?.() ?? [];
4990
- const manifestEntry = allPkgs.find((p) => (p?.manifest?.id ?? p?.id) === packageId);
4991
- const manifest = manifestEntry?.manifest ?? manifestEntry;
4992
- if (!manifest) {
4993
- return { handled: true, response: this.error(`Package '${packageId}' is not registered on this server`, 404) };
4994
- }
4995
- const CLOUD_SCOPES = /* @__PURE__ */ new Set(["cloud", "system", "platform"]);
4996
- if (CLOUD_SCOPES.has(manifest?.scope)) {
4997
- return { handled: true, response: this.error(`Package '${packageId}' has scope=${manifest.scope} and cannot be installed per-project`, 403) };
4998
- }
4999
- const projectRow = await findOne(ENV, { id: envId });
5000
- if (!projectRow) {
5001
- return { handled: true, response: this.error(`Project '${envId}' not found`, 404) };
5002
- }
5003
- const ownerOrgId = projectRow.organization_id ?? "system";
5004
- let userId = "system";
5005
- try {
5006
- const authService = await this.getService(CoreServiceName.enum.auth);
5007
- const sessionData = await authService?.api?.getSession?.({
5008
- headers: _context?.request?.headers
5009
- });
5010
- userId = sessionData?.user?.id ?? sessionData?.session?.userId ?? "system";
5011
- } catch {
5012
- }
5013
- const resolvedVersion = version ?? manifest?.version ?? "1.0.0";
5014
- const dup = await ql.findOne(PKG_INSTALL, {
5015
- where: { environment_id: envId, package_id: packageId }
5016
- });
5017
- if (dup?.id) {
5018
- return { handled: true, response: this.error(`Package '${packageId}' is already installed in this project`, 409) };
5019
- }
5020
- const sysPackageId = await ensureSysPackage(packageId, ownerOrgId, userId, manifest);
5021
- const sysPackageVersionId = await ensureSysPackageVersion(sysPackageId, resolvedVersion, userId, manifest);
5022
- const nowIso = (/* @__PURE__ */ new Date()).toISOString();
5023
- const recordId = randomUUID();
5024
- await ql.insert(PKG_INSTALL, {
5025
- id: recordId,
5026
- environment_id: envId,
5027
- package_id: sysPackageId,
5028
- package_version_id: sysPackageVersionId,
5029
- status: "installed",
5030
- enabled: enableOnInstall !== false,
5031
- installed_at: nowIso,
5032
- installed_by: userId,
5033
- updated_at: nowIso,
5034
- settings: settings ? JSON.stringify(settings) : null
5035
- });
5036
- const record = await ql.findOne(PKG_INSTALL, { where: { id: recordId } });
5037
- try {
5038
- await this.kernelManager?.evict(envId);
5039
- } catch {
5040
- }
5041
- return { handled: true, response: this.success({ package: record }) };
5042
- }
5043
- if (parts.length === 4 && parts[0] === "projects" && parts[2] === "packages" && m === "GET") {
5044
- const envId = decodeURIComponent(parts[1]);
5045
- const pkgId = decodeURIComponent(parts[3]);
5046
- const record = await ql.findOne(PKG_INSTALL, { where: { environment_id: envId, package_id: pkgId } });
5047
- if (!record) return { handled: true, response: this.error(`Package '${pkgId}' is not installed in this project`, 404) };
5048
- return { handled: true, response: this.success({ package: record }) };
5049
- }
5050
- if (parts.length === 4 && parts[0] === "projects" && parts[2] === "packages" && m === "DELETE") {
5051
- const envId = decodeURIComponent(parts[1]);
5052
- const pkgId = decodeURIComponent(parts[3]);
5053
- const record = await findInstallByManifestId(envId, pkgId);
5054
- if (!record) return { handled: true, response: this.error(`Package '${pkgId}' is not installed in this project`, 404) };
5055
- const allPkgs0 = this.kernel.packages?.getAll?.() ?? [];
5056
- const m0 = allPkgs0.find((p) => (p.manifest?.id ?? p.id) === pkgId)?.manifest;
5057
- if (m0?.scope && ["cloud", "system", "platform"].includes(m0.scope)) {
5058
- return { handled: true, response: this.error(`Package '${pkgId}' with scope=${m0.scope} cannot be uninstalled`, 403) };
5059
- }
5060
- await ql.delete(PKG_INSTALL, { where: { id: record.id } });
5061
- try {
5062
- await this.kernelManager?.evict(envId);
5063
- } catch {
5064
- }
5065
- return { handled: true, response: this.success({ id: record.id, success: true }) };
5066
- }
5067
- if (parts.length === 5 && parts[0] === "projects" && parts[2] === "packages" && parts[4] === "enable" && m === "PATCH") {
5068
- const envId = decodeURIComponent(parts[1]);
5069
- const pkgId = decodeURIComponent(parts[3]);
5070
- const record = await findInstallByManifestId(envId, pkgId);
5071
- if (!record) return { handled: true, response: this.error(`Package '${pkgId}' is not installed in this project`, 404) };
5072
- const nowIso = (/* @__PURE__ */ new Date()).toISOString();
5073
- await ql.update(PKG_INSTALL, { enabled: true, status: "installed", updated_at: nowIso }, { where: { id: record.id } });
5074
- const updated = await ql.findOne(PKG_INSTALL, { where: { id: record.id } });
5075
- try {
5076
- await this.kernelManager?.evict(envId);
5077
- } catch {
5078
- }
5079
- return { handled: true, response: this.success({ package: updated }) };
5080
- }
5081
- if (parts.length === 5 && parts[0] === "projects" && parts[2] === "packages" && parts[4] === "disable" && m === "PATCH") {
5082
- const envId = decodeURIComponent(parts[1]);
5083
- const pkgId = decodeURIComponent(parts[3]);
5084
- const record = await findInstallByManifestId(envId, pkgId);
5085
- if (!record) return { handled: true, response: this.error(`Package '${pkgId}' is not installed in this project`, 404) };
5086
- const allPkgs1 = this.kernel.packages?.getAll?.() ?? [];
5087
- const m1 = allPkgs1.find((p) => (p.manifest?.id ?? p.id) === pkgId)?.manifest;
5088
- if (m1?.scope && ["cloud", "system", "platform"].includes(m1.scope)) {
5089
- return { handled: true, response: this.error(`Package '${pkgId}' with scope=${m1.scope} cannot be disabled`, 403) };
5090
- }
5091
- const nowIso = (/* @__PURE__ */ new Date()).toISOString();
5092
- await ql.update(PKG_INSTALL, { enabled: false, status: "disabled", updated_at: nowIso }, { where: { id: record.id } });
5093
- const updated = await ql.findOne(PKG_INSTALL, { where: { id: record.id } });
5094
- try {
5095
- await this.kernelManager?.evict(envId);
5096
- } catch {
5097
- }
5098
- return { handled: true, response: this.success({ package: updated }) };
5099
- }
5100
- if (parts.length === 5 && parts[0] === "projects" && parts[2] === "packages" && parts[4] === "upgrade" && m === "POST") {
5101
- const envId = decodeURIComponent(parts[1]);
5102
- const pkgId = decodeURIComponent(parts[3]);
5103
- const record = await findInstallByManifestId(envId, pkgId);
5104
- if (!record) return { handled: true, response: this.error(`Package '${pkgId}' is not installed in this project`, 404) };
5105
- const { targetVersion } = body ?? {};
5106
- const allPkgs2 = this.kernel.packages?.getAll?.() ?? [];
5107
- const manifest2 = allPkgs2.find((p) => (p.manifest?.id ?? p.id) === pkgId)?.manifest;
5108
- const currentVer = await ql.findOne(PKG_VERSION, { where: { id: record.package_version_id } });
5109
- const newVersion = targetVersion ?? manifest2?.version ?? currentVer?.version ?? "1.0.0";
5110
- if (newVersion === currentVer?.version) {
5111
- return { handled: true, response: this.success({ package: record, message: "Already at target version" }) };
5112
- }
5113
- let userId = "system";
5114
- try {
5115
- const authService = await this.getService(CoreServiceName.enum.auth);
5116
- const sessionData = await authService?.api?.getSession?.({
5117
- headers: _context?.request?.headers
5118
- });
5119
- userId = sessionData?.user?.id ?? "system";
5120
- } catch {
5121
- }
5122
- const newVersionId = await ensureSysPackageVersion(record.package_id, newVersion, userId, manifest2);
5123
- const nowIso = (/* @__PURE__ */ new Date()).toISOString();
5124
- await ql.update(PKG_INSTALL, {
5125
- package_version_id: newVersionId,
5126
- status: "installed",
5127
- updated_at: nowIso
5128
- }, { where: { id: record.id } });
5129
- const updated = await ql.findOne(PKG_INSTALL, { where: { id: record.id } });
5130
- try {
5131
- await this.kernelManager?.evict(envId);
5132
- } catch {
5133
- }
5134
- return { handled: true, response: this.success({ package: updated }) };
5135
- }
5136
- } catch (e) {
5137
- return { handled: true, response: this.error(e.message, e.statusCode || 500) };
5138
- }
5139
- return { handled: false };
5140
- }
5141
- /**
5142
- * Cascade-delete a project: cred / member / package_installation rows,
5143
- * then the physical database via the provisioning adapter, then the
5144
- * `sys_environment` row itself. Used by both `DELETE /cloud/environments/:id`
5145
- * and the org-cascade in `DELETE /cloud/organizations/:id`.
5146
- *
5147
- * Idempotent and best-effort: missing rows / unreachable adapters
5148
- * become warnings rather than hard failures, so a half-provisioned
5149
- * project can still be cleaned out.
5150
- */
5151
- async deleteProjectCascade(environmentId, deps) {
5152
- const { ql, findOne, getRealAdapter, force } = deps;
5153
- const ENV = "sys_environment";
5154
- const warnings = [];
5155
- const row = await findOne(ENV, { id: environmentId });
5156
- if (!row) {
5157
- return { ok: false, status: 404, error: `Project '${environmentId}' not found`, warnings };
5158
- }
5159
- if (row.is_system === true || row.is_system === 1) {
5160
- return { ok: false, status: 409, error: `Project '${environmentId}' is a system project and cannot be deleted`, warnings };
5161
- }
5162
- if ((row.is_default === true || row.is_default === 1) && !force) {
5163
- return {
5164
- ok: false,
5165
- status: 409,
5166
- error: `Project '${environmentId}' is the default project for its organization. Pass ?force=1 to delete it.`,
5167
- warnings
5168
- };
5169
- }
5170
- const cascade = [
5171
- { object: "sys_environment_credential", field: "environment_id" },
5172
- { object: "sys_environment_member", field: "environment_id" },
5173
- { object: "sys_package_installation", field: "environment_id" }
5174
- ];
5175
- for (const { object, field } of cascade) {
5176
- try {
5177
- let rows = await ql.find(object, { where: { [field]: environmentId } });
5178
- if (rows && rows.value) rows = rows.value;
5179
- if (Array.isArray(rows)) {
5180
- for (const r of rows) {
5181
- if (r?.id != null) {
5182
- try {
5183
- await ql.delete(object, { where: { id: r.id } });
5184
- } catch (innerErr) {
5185
- warnings.push(
5186
- `Failed to delete ${object} ${r.id}: ${innerErr instanceof Error ? innerErr.message : String(innerErr)}`
5187
- );
5188
- }
5189
- }
5190
- }
5191
- }
5192
- } catch (err) {
5193
- warnings.push(
5194
- `Failed to enumerate ${object} for project ${environmentId}: ${err instanceof Error ? err.message : String(err)}`
5195
- );
5196
- }
5197
- }
5198
- const driver = row.database_driver ?? "memory";
5199
- const databaseUrl = row.database_url;
5200
- const databaseName = `p-${String(environmentId).replace(/-/g, "").slice(0, 24)}`;
5201
- try {
5202
- const adapter = await getRealAdapter(driver);
5203
- if (adapter?.deleteDatabase) {
5204
- await adapter.deleteDatabase({ environmentId, databaseName, databaseUrl });
5205
- } else {
5206
- warnings.push(`No adapter for driver '${driver}'; physical DB for project ${environmentId} not released.`);
5207
- }
5208
- } catch (err) {
5209
- warnings.push(
5210
- `Failed to delete physical database for project ${environmentId}: ${err instanceof Error ? err.message : String(err)}`
5211
- );
5212
- }
5213
- try {
5214
- await ql.delete(ENV, { where: { id: environmentId } });
5215
- } catch (err) {
5216
- return {
5217
- ok: false,
5218
- status: 500,
5219
- error: `Failed to delete sys_environment row: ${err instanceof Error ? err.message : String(err)}`,
5220
- warnings
5221
- };
5222
- }
5223
- if (this.envRegistry?.invalidate) {
5224
- try {
5225
- await this.envRegistry.invalidate(environmentId);
5226
- } catch {
5227
- }
5228
- }
5229
- return { ok: true, warnings };
5230
- }
5231
- /**
5232
- * Handles Storage requests
5233
- * path: sub-path after /storage/
5234
- */
5235
- async handleStorage(path, method, file, context) {
5236
- const storageService = await this.getService(CoreServiceName.enum["file-storage"]) || this.kernel.services?.["file-storage"];
5237
- if (!storageService) {
5238
- return { handled: true, response: this.error("File storage not configured", 501) };
5239
- }
5240
- const m = method.toUpperCase();
5241
- const parts = path.replace(/^\/+/, "").split("/");
5242
- if (parts[0] === "upload" && m === "POST") {
5243
- if (!file) {
5244
- return { handled: true, response: this.error("No file provided", 400) };
5245
- }
5246
- const result = await storageService.upload(file, { request: context.request });
5247
- return { handled: true, response: this.success(result) };
5248
- }
5249
- if (parts[0] === "file" && parts[1] && m === "GET") {
5250
- const id = parts[1];
5251
- const result = await storageService.download(id, { request: context.request });
5252
- if (result.url && result.redirect) {
5253
- return { handled: true, result: { type: "redirect", url: result.url } };
5254
- }
5255
- if (result.stream) {
5256
- return {
5257
- handled: true,
5258
- result: {
5259
- type: "stream",
5260
- stream: result.stream,
5261
- headers: {
5262
- "Content-Type": result.mimeType || "application/octet-stream",
5263
- "Content-Length": result.size
5264
- }
5265
- }
5266
- };
5267
- }
5268
- return { handled: true, response: this.success(result) };
5269
- }
5270
- return { handled: false };
5271
- }
5272
- /**
5273
- * Handles UI requests
5274
- * path: sub-path after /ui/
5275
- */
5276
- async handleUi(path, query, _context) {
5277
- const parts = path.replace(/^\/+/, "").split("/").filter(Boolean);
5278
- if (parts[0] === "view" && parts[1]) {
5279
- const objectName = parts[1];
5280
- const type = parts[2] || query?.type || "list";
5281
- const protocol = await this.resolveService("protocol");
5282
- if (protocol && typeof protocol.getUiView === "function") {
5283
- try {
5284
- const result = await protocol.getUiView({ object: objectName, type });
5285
- return { handled: true, response: this.success(result) };
5286
- } catch (e) {
5287
- return { handled: true, response: this.error(e.message, 500) };
5288
- }
5289
- } else {
5290
- return { handled: true, response: this.error("Protocol service not available", 503) };
5291
- }
5292
- }
5293
- return { handled: false };
5294
- }
5295
- /**
5296
- * Handles Automation requests
5297
- * path: sub-path after /automation/
5298
- *
5299
- * Routes:
5300
- * GET / → listFlows
5301
- * GET /:name → getFlow
5302
- * POST / → createFlow (registerFlow)
5303
- * PUT /:name → updateFlow
5304
- * DELETE /:name → deleteFlow (unregisterFlow)
5305
- * POST /:name/trigger → execute (legacy: trigger/:name also supported)
5306
- * POST /:name/toggle → toggleFlow
5307
- * GET /:name/runs → listRuns
5308
- * GET /:name/runs/:runId → getRun
5309
- */
5310
- async handleAutomation(path, method, body, context, query) {
5311
- const automationService = await this.getService(CoreServiceName.enum.automation);
5312
- if (!automationService) return { handled: false };
5313
- const m = method.toUpperCase();
5314
- const parts = path.replace(/^\/+/, "").split("/").filter(Boolean);
5315
- if (parts[0] === "trigger" && parts[1] && m === "POST") {
5316
- const triggerName = parts[1];
5317
- if (typeof automationService.trigger === "function") {
5318
- const result = await automationService.trigger(triggerName, body, { request: context.request });
5319
- return { handled: true, response: this.success(result) };
5320
- }
5321
- if (typeof automationService.execute === "function") {
5322
- const result = await automationService.execute(triggerName, body);
5323
- return { handled: true, response: this.success(result) };
5324
- }
5325
- }
5326
- if (parts.length === 0 && m === "GET") {
5327
- if (typeof automationService.listFlows === "function") {
5328
- const names = await automationService.listFlows();
5329
- return { handled: true, response: this.success({ flows: names, total: names.length, hasMore: false }) };
5330
- }
5331
- }
5332
- if (parts.length === 0 && m === "POST") {
5333
- if (typeof automationService.registerFlow === "function") {
5334
- automationService.registerFlow(body?.name, body);
5335
- return { handled: true, response: this.success(body) };
5336
- }
5337
- }
5338
- if (parts.length >= 1) {
5339
- const name = parts[0];
5340
- if (parts[1] === "trigger" && m === "POST") {
5341
- if (typeof automationService.execute === "function") {
5342
- const ctxBody = body && typeof body === "object" ? body : {};
5343
- const recordId = ctxBody.recordId;
5344
- const objectName = ctxBody.objectName ?? ctxBody.object;
5345
- const baseParams = ctxBody.params && typeof ctxBody.params === "object" ? { ...ctxBody.params } : {};
5346
- if (!ctxBody.params) {
5347
- const reserved = /* @__PURE__ */ new Set(["recordId", "objectName", "object", "event", "params"]);
5348
- for (const [k, v] of Object.entries(ctxBody)) {
5349
- if (reserved.has(k)) continue;
5350
- if (baseParams[k] === void 0) baseParams[k] = v;
5351
- }
5352
- }
5353
- if (recordId !== void 0 && baseParams.recordId === void 0) {
5354
- baseParams.recordId = recordId;
5355
- }
5356
- if (recordId !== void 0 && objectName) {
5357
- const alias = `${String(objectName).replace(/_([a-z])/g, (_, c) => c.toUpperCase())}Id`;
5358
- if (baseParams[alias] === void 0) baseParams[alias] = recordId;
5359
- }
5360
- const automationContext = {
5361
- params: baseParams,
5362
- object: objectName,
5363
- event: ctxBody.event ?? "manual"
5364
- };
5365
- const userIdFromAuth = context?.user?.id ?? context?.userId;
5366
- if (userIdFromAuth) automationContext.userId = userIdFromAuth;
5367
- const result = await automationService.execute(name, automationContext);
5368
- return { handled: true, response: this.success(result) };
5369
- }
5370
- }
5371
- if (parts[1] === "toggle" && m === "POST") {
5372
- if (typeof automationService.toggleFlow === "function") {
5373
- await automationService.toggleFlow(name, body?.enabled ?? true);
5374
- return { handled: true, response: this.success({ name, enabled: body?.enabled ?? true }) };
4177
+ return { handled: true, response: this.error("Resume not supported", 501) };
4178
+ }
4179
+ if (parts[1] === "runs" && parts[2] && parts[3] === "screen" && m === "GET") {
4180
+ if (typeof automationService.getSuspendedScreen === "function") {
4181
+ const screen = automationService.getSuspendedScreen(parts[2]);
4182
+ if (!screen) return { handled: true, response: this.error("No pending screen for run", 404) };
4183
+ return { handled: true, response: this.success({ runId: parts[2], screen }) };
5375
4184
  }
4185
+ return { handled: true, response: this.error("Screen lookup not supported", 501) };
5376
4186
  }
5377
- if (parts[1] === "runs" && parts[2] && m === "GET") {
4187
+ if (parts[1] === "runs" && parts[2] && !parts[3] && m === "GET") {
5378
4188
  if (typeof automationService.getRun === "function") {
5379
4189
  const run = await automationService.getRun(parts[2]);
5380
4190
  if (!run) return { handled: true, response: this.error("Execution not found", 404) };
@@ -5700,11 +4510,9 @@ var _HttpDispatcher = class _HttpDispatcher {
5700
4510
  if (forbidden) {
5701
4511
  return { handled: true, response: forbidden };
5702
4512
  }
5703
- if (!cleanPath.startsWith("/cloud/")) {
5704
- const scopedMatch = cleanPath.match(/^\/projects\/[^/]+(\/.*)?$/);
5705
- if (scopedMatch) {
5706
- cleanPath = scopedMatch[1] ?? "";
5707
- }
4513
+ const scopedMatch = cleanPath.match(/^\/projects\/[^/]+(\/.*)?$/);
4514
+ if (scopedMatch) {
4515
+ cleanPath = scopedMatch[1] ?? "";
5708
4516
  }
5709
4517
  try {
5710
4518
  if ((cleanPath === "/discovery" || cleanPath === "") && method === "GET") {
@@ -5755,9 +4563,6 @@ var _HttpDispatcher = class _HttpDispatcher {
5755
4563
  if (cleanPath.startsWith("/packages")) {
5756
4564
  return this.handlePackages(cleanPath.substring(9), method, body, query, context);
5757
4565
  }
5758
- if (cleanPath.startsWith("/cloud")) {
5759
- return this.handleCloud(cleanPath.substring(6), method, body, query, context);
5760
- }
5761
4566
  if (cleanPath.startsWith("/i18n")) {
5762
4567
  return this.handleI18n(cleanPath.substring(5), method, query, context);
5763
4568
  }
@@ -6385,342 +5190,134 @@ function createDispatcherPlugin(config = {}) {
6385
5190
  }
6386
5191
  res.json({ data: await dispatcher.getDiscoveryInfo(prefix) });
6387
5192
  });
6388
- server.get(`${prefix}/discovery`, async (_req, res) => {
6389
- if (securityHeaders) {
6390
- for (const [k, v] of Object.entries(securityHeaders)) {
6391
- res.header(k, v);
6392
- }
6393
- }
6394
- res.json({ data: await dispatcher.getDiscoveryInfo(prefix) });
6395
- });
6396
- server.get(`${prefix}/health`, async (_req, res) => {
6397
- try {
6398
- const result = await dispatcher.dispatch("GET", "/health", void 0, {}, { request: _req });
6399
- sendResult(result, res);
6400
- } catch (err) {
6401
- errorResponse(err, res);
6402
- }
6403
- });
6404
- server.post(`${prefix}/auth/login`, async (req, res) => {
6405
- try {
6406
- const result = await dispatcher.handleAuth("login", "POST", req.body, { request: req });
6407
- sendResult(result, res);
6408
- } catch (err) {
6409
- errorResponse(err, res);
6410
- }
6411
- });
6412
- server.post(`${prefix}/graphql`, async (req, res) => {
6413
- try {
6414
- const result = await dispatcher.handleGraphQL(req.body, { request: req });
6415
- if (securityHeaders) {
6416
- for (const [k, v] of Object.entries(securityHeaders)) {
6417
- res.header(k, v);
6418
- }
6419
- }
6420
- res.json(result);
6421
- } catch (err) {
6422
- errorResponse(err, res);
6423
- }
6424
- });
6425
- server.post(`${prefix}/analytics/query`, async (req, res) => {
6426
- try {
6427
- const result = await dispatcher.dispatch("POST", "/analytics/query", req.body, req.query, { request: req });
6428
- sendResult(result, res);
6429
- } catch (err) {
6430
- errorResponse(err, res);
6431
- }
6432
- });
6433
- server.get(`${prefix}/analytics/meta`, async (req, res) => {
6434
- try {
6435
- const result = await dispatcher.dispatch("GET", "/analytics/meta", void 0, req.query, { request: req });
6436
- sendResult(result, res);
6437
- } catch (err) {
6438
- errorResponse(err, res);
6439
- }
6440
- });
6441
- server.post(`${prefix}/analytics/sql`, async (req, res) => {
6442
- try {
6443
- const result = await dispatcher.dispatch("POST", "/analytics/sql", req.body, req.query, { request: req });
6444
- sendResult(result, res);
6445
- } catch (err) {
6446
- errorResponse(err, res);
6447
- }
6448
- });
6449
- server.get(`${prefix}/packages`, async (req, res) => {
6450
- try {
6451
- const result = await dispatcher.handlePackages("", "GET", {}, req.query, { request: req });
6452
- sendResult(result, res);
6453
- } catch (err) {
6454
- errorResponse(err, res);
6455
- }
6456
- });
6457
- server.post(`${prefix}/packages`, async (req, res) => {
6458
- try {
6459
- const result = await dispatcher.handlePackages("", "POST", req.body, {}, { request: req });
6460
- sendResult(result, res);
6461
- } catch (err) {
6462
- errorResponse(err, res);
6463
- }
6464
- });
6465
- server.get(`${prefix}/packages/:id/export`, async (req, res) => {
6466
- try {
6467
- const result = await dispatcher.handlePackages(`/${req.params.id}/export`, "GET", {}, req.query, { request: req });
6468
- sendResult(result, res);
6469
- } catch (err) {
6470
- errorResponse(err, res);
6471
- }
6472
- });
6473
- server.get(`${prefix}/packages/:id`, async (req, res) => {
6474
- try {
6475
- const result = await dispatcher.handlePackages(`/${req.params.id}`, "GET", {}, req.query, { request: req });
6476
- sendResult(result, res);
6477
- } catch (err) {
6478
- errorResponse(err, res);
6479
- }
6480
- });
6481
- server.delete(`${prefix}/packages/:id`, async (req, res) => {
6482
- try {
6483
- const result = await dispatcher.handlePackages(`/${req.params.id}`, "DELETE", {}, {}, { request: req });
6484
- sendResult(result, res);
6485
- } catch (err) {
6486
- errorResponse(err, res);
6487
- }
6488
- });
6489
- server.patch(`${prefix}/packages/:id/enable`, async (req, res) => {
6490
- try {
6491
- const result = await dispatcher.handlePackages(`/${req.params.id}/enable`, "PATCH", {}, {}, { request: req });
6492
- sendResult(result, res);
6493
- } catch (err) {
6494
- errorResponse(err, res);
6495
- }
6496
- });
6497
- server.patch(`${prefix}/packages/:id/disable`, async (req, res) => {
6498
- try {
6499
- const result = await dispatcher.handlePackages(`/${req.params.id}/disable`, "PATCH", {}, {}, { request: req });
6500
- sendResult(result, res);
6501
- } catch (err) {
6502
- errorResponse(err, res);
6503
- }
6504
- });
6505
- server.post(`${prefix}/packages/:id/publish`, async (req, res) => {
6506
- try {
6507
- const result = await dispatcher.handlePackages(`/${req.params.id}/publish`, "POST", req.body, {}, { request: req });
6508
- sendResult(result, res);
6509
- } catch (err) {
6510
- errorResponse(err, res);
6511
- }
6512
- });
6513
- server.post(`${prefix}/packages/:id/revert`, async (req, res) => {
6514
- try {
6515
- const result = await dispatcher.handlePackages(`/${req.params.id}/revert`, "POST", req.body, {}, { request: req });
6516
- sendResult(result, res);
6517
- } catch (err) {
6518
- errorResponse(err, res);
6519
- }
6520
- });
6521
- server.get(`${prefix}/cloud/drivers`, async (req, res) => {
6522
- try {
6523
- const result = await dispatcher.handleCloud("/drivers", "GET", {}, req.query, { request: req });
6524
- sendResult(result, res);
6525
- } catch (err) {
6526
- errorResponse(err, res);
6527
- }
6528
- });
6529
- server.post(`${prefix}/cloud/admin/platform-sso/backfill`, async (req, res) => {
6530
- try {
6531
- const result = await dispatcher.handleCloud("/admin/platform-sso/backfill", "POST", req.body, req.query, { request: req });
6532
- sendResult(result, res);
6533
- } catch (err) {
6534
- errorResponse(err, res);
6535
- }
6536
- });
6537
- server.get(`${prefix}/cloud/templates`, async (req, res) => {
6538
- try {
6539
- const result = await dispatcher.handleCloud("/templates", "GET", {}, req.query, { request: req });
6540
- sendResult(result, res);
6541
- } catch (err) {
6542
- errorResponse(err, res);
6543
- }
6544
- });
6545
- server.get(`${prefix}/cloud/environments`, async (req, res) => {
6546
- try {
6547
- const result = await dispatcher.handleCloud("/projects", "GET", {}, req.query, { request: req });
6548
- sendResult(result, res);
6549
- } catch (err) {
6550
- errorResponse(err, res);
6551
- }
6552
- });
6553
- server.post(`${prefix}/cloud/environments`, async (req, res) => {
6554
- try {
6555
- const result = await dispatcher.handleCloud("/projects", "POST", req.body, {}, { request: req });
6556
- sendResult(result, res);
6557
- } catch (err) {
6558
- errorResponse(err, res);
6559
- }
6560
- });
6561
- server.get(`${prefix}/cloud/environments/:id`, async (req, res) => {
6562
- try {
6563
- const result = await dispatcher.handleCloud(`/projects/${req.params.id}`, "GET", {}, req.query, { request: req });
6564
- sendResult(result, res);
6565
- } catch (err) {
6566
- errorResponse(err, res);
6567
- }
6568
- });
6569
- server.patch(`${prefix}/cloud/environments/:id`, async (req, res) => {
6570
- try {
6571
- const result = await dispatcher.handleCloud(`/projects/${req.params.id}`, "PATCH", req.body, {}, { request: req });
6572
- sendResult(result, res);
6573
- } catch (err) {
6574
- errorResponse(err, res);
6575
- }
6576
- });
6577
- server.delete(`${prefix}/cloud/environments/:id`, async (req, res) => {
6578
- try {
6579
- const result = await dispatcher.handleCloud(`/projects/${req.params.id}`, "DELETE", {}, req.query, { request: req });
6580
- sendResult(result, res);
6581
- } catch (err) {
6582
- errorResponse(err, res);
6583
- }
6584
- });
6585
- server.delete(`${prefix}/cloud/organizations/:id`, async (req, res) => {
6586
- try {
6587
- const result = await dispatcher.handleCloud(`/organizations/${req.params.id}`, "DELETE", {}, req.query, { request: req });
6588
- sendResult(result, res);
6589
- } catch (err) {
6590
- errorResponse(err, res);
6591
- }
6592
- });
6593
- server.post(`${prefix}/cloud/environments/:id/hostname`, async (req, res) => {
6594
- try {
6595
- const result = await dispatcher.handleCloud(`/projects/${req.params.id}/hostname`, "POST", req.body, {}, { request: req });
6596
- sendResult(result, res);
6597
- } catch (err) {
6598
- errorResponse(err, res);
6599
- }
6600
- });
6601
- server.put(`${prefix}/cloud/environments/:id/hostname`, async (req, res) => {
6602
- try {
6603
- const result = await dispatcher.handleCloud(`/projects/${req.params.id}/hostname`, "PUT", req.body, {}, { request: req });
6604
- sendResult(result, res);
6605
- } catch (err) {
6606
- errorResponse(err, res);
5193
+ server.get(`${prefix}/discovery`, async (_req, res) => {
5194
+ if (securityHeaders) {
5195
+ for (const [k, v] of Object.entries(securityHeaders)) {
5196
+ res.header(k, v);
5197
+ }
6607
5198
  }
5199
+ res.json({ data: await dispatcher.getDiscoveryInfo(prefix) });
6608
5200
  });
6609
- server.post(`${prefix}/cloud/environments/:id/rotate-credential`, async (req, res) => {
5201
+ server.get(`${prefix}/health`, async (_req, res) => {
6610
5202
  try {
6611
- const result = await dispatcher.handleCloud(`/projects/${req.params.id}/rotate-credential`, "POST", req.body, {}, { request: req });
5203
+ const result = await dispatcher.dispatch("GET", "/health", void 0, {}, { request: _req });
6612
5204
  sendResult(result, res);
6613
5205
  } catch (err) {
6614
5206
  errorResponse(err, res);
6615
5207
  }
6616
5208
  });
6617
- server.post(`${prefix}/cloud/environments/:id/credentials/rotate`, async (req, res) => {
5209
+ server.post(`${prefix}/auth/login`, async (req, res) => {
6618
5210
  try {
6619
- const result = await dispatcher.handleCloud(`/projects/${req.params.id}/credentials/rotate`, "POST", req.body, {}, { request: req });
5211
+ const result = await dispatcher.handleAuth("login", "POST", req.body, { request: req });
6620
5212
  sendResult(result, res);
6621
5213
  } catch (err) {
6622
5214
  errorResponse(err, res);
6623
5215
  }
6624
5216
  });
6625
- server.post(`${prefix}/cloud/environments/:id/activate`, async (req, res) => {
5217
+ server.post(`${prefix}/graphql`, async (req, res) => {
6626
5218
  try {
6627
- const result = await dispatcher.handleCloud(`/projects/${req.params.id}/activate`, "POST", req.body, {}, { request: req });
6628
- sendResult(result, res);
5219
+ const result = await dispatcher.handleGraphQL(req.body, { request: req });
5220
+ if (securityHeaders) {
5221
+ for (const [k, v] of Object.entries(securityHeaders)) {
5222
+ res.header(k, v);
5223
+ }
5224
+ }
5225
+ res.json(result);
6629
5226
  } catch (err) {
6630
5227
  errorResponse(err, res);
6631
5228
  }
6632
5229
  });
6633
- server.post(`${prefix}/cloud/environments/:id/retry`, async (req, res) => {
5230
+ server.post(`${prefix}/analytics/query`, async (req, res) => {
6634
5231
  try {
6635
- const result = await dispatcher.handleCloud(`/projects/${req.params.id}/retry`, "POST", req.body, {}, { request: req });
5232
+ const result = await dispatcher.dispatch("POST", "/analytics/query", req.body, req.query, { request: req });
6636
5233
  sendResult(result, res);
6637
5234
  } catch (err) {
6638
5235
  errorResponse(err, res);
6639
5236
  }
6640
5237
  });
6641
- server.get(`${prefix}/cloud/environments/:id/members`, async (req, res) => {
5238
+ server.get(`${prefix}/analytics/meta`, async (req, res) => {
6642
5239
  try {
6643
- const result = await dispatcher.handleCloud(`/projects/${req.params.id}/members`, "GET", {}, req.query, { request: req });
5240
+ const result = await dispatcher.dispatch("GET", "/analytics/meta", void 0, req.query, { request: req });
6644
5241
  sendResult(result, res);
6645
5242
  } catch (err) {
6646
5243
  errorResponse(err, res);
6647
5244
  }
6648
5245
  });
6649
- server.post(`${prefix}/cloud/environments/:id/members`, async (req, res) => {
5246
+ server.post(`${prefix}/analytics/sql`, async (req, res) => {
6650
5247
  try {
6651
- const result = await dispatcher.handleCloud(`/projects/${req.params.id}/members`, "POST", req.body, {}, { request: req });
5248
+ const result = await dispatcher.dispatch("POST", "/analytics/sql", req.body, req.query, { request: req });
6652
5249
  sendResult(result, res);
6653
5250
  } catch (err) {
6654
5251
  errorResponse(err, res);
6655
5252
  }
6656
5253
  });
6657
- server.patch(`${prefix}/cloud/environments/:id/members/:memberId`, async (req, res) => {
5254
+ server.get(`${prefix}/packages`, async (req, res) => {
6658
5255
  try {
6659
- const result = await dispatcher.handleCloud(`/projects/${req.params.id}/members/${req.params.memberId}`, "PATCH", req.body, {}, { request: req });
5256
+ const result = await dispatcher.handlePackages("", "GET", {}, req.query, { request: req });
6660
5257
  sendResult(result, res);
6661
5258
  } catch (err) {
6662
5259
  errorResponse(err, res);
6663
5260
  }
6664
5261
  });
6665
- server.delete(`${prefix}/cloud/environments/:id/members/:memberId`, async (req, res) => {
5262
+ server.post(`${prefix}/packages`, async (req, res) => {
6666
5263
  try {
6667
- const result = await dispatcher.handleCloud(`/projects/${req.params.id}/members/${req.params.memberId}`, "DELETE", req.body ?? {}, {}, { request: req });
5264
+ const result = await dispatcher.handlePackages("", "POST", req.body, {}, { request: req });
6668
5265
  sendResult(result, res);
6669
5266
  } catch (err) {
6670
5267
  errorResponse(err, res);
6671
5268
  }
6672
5269
  });
6673
- server.get(`${prefix}/cloud/environments/:id/packages`, async (req, res) => {
5270
+ server.get(`${prefix}/packages/:id/export`, async (req, res) => {
6674
5271
  try {
6675
- const result = await dispatcher.handleCloud(`/projects/${req.params.id}/packages`, "GET", {}, req.query, { request: req });
5272
+ const result = await dispatcher.handlePackages(`/${req.params.id}/export`, "GET", {}, req.query, { request: req });
6676
5273
  sendResult(result, res);
6677
5274
  } catch (err) {
6678
5275
  errorResponse(err, res);
6679
5276
  }
6680
5277
  });
6681
- server.post(`${prefix}/cloud/environments/:id/packages`, async (req, res) => {
5278
+ server.get(`${prefix}/packages/:id`, async (req, res) => {
6682
5279
  try {
6683
- const result = await dispatcher.handleCloud(`/projects/${req.params.id}/packages`, "POST", req.body, {}, { request: req });
5280
+ const result = await dispatcher.handlePackages(`/${req.params.id}`, "GET", {}, req.query, { request: req });
6684
5281
  sendResult(result, res);
6685
5282
  } catch (err) {
6686
5283
  errorResponse(err, res);
6687
5284
  }
6688
5285
  });
6689
- server.get(`${prefix}/cloud/environments/:id/packages/:pkgId`, async (req, res) => {
5286
+ server.delete(`${prefix}/packages/:id`, async (req, res) => {
6690
5287
  try {
6691
- const result = await dispatcher.handleCloud(`/projects/${req.params.id}/packages/${req.params.pkgId}`, "GET", {}, req.query, { request: req });
5288
+ const result = await dispatcher.handlePackages(`/${req.params.id}`, "DELETE", {}, {}, { request: req });
6692
5289
  sendResult(result, res);
6693
5290
  } catch (err) {
6694
5291
  errorResponse(err, res);
6695
5292
  }
6696
5293
  });
6697
- server.delete(`${prefix}/cloud/environments/:id/packages/:pkgId`, async (req, res) => {
5294
+ server.patch(`${prefix}/packages/:id/enable`, async (req, res) => {
6698
5295
  try {
6699
- const result = await dispatcher.handleCloud(`/projects/${req.params.id}/packages/${req.params.pkgId}`, "DELETE", {}, {}, { request: req });
5296
+ const result = await dispatcher.handlePackages(`/${req.params.id}/enable`, "PATCH", {}, {}, { request: req });
6700
5297
  sendResult(result, res);
6701
5298
  } catch (err) {
6702
5299
  errorResponse(err, res);
6703
5300
  }
6704
5301
  });
6705
- server.patch(`${prefix}/cloud/environments/:id/packages/:pkgId/enable`, async (req, res) => {
5302
+ server.patch(`${prefix}/packages/:id/disable`, async (req, res) => {
6706
5303
  try {
6707
- const result = await dispatcher.handleCloud(`/projects/${req.params.id}/packages/${req.params.pkgId}/enable`, "PATCH", {}, {}, { request: req });
5304
+ const result = await dispatcher.handlePackages(`/${req.params.id}/disable`, "PATCH", {}, {}, { request: req });
6708
5305
  sendResult(result, res);
6709
5306
  } catch (err) {
6710
5307
  errorResponse(err, res);
6711
5308
  }
6712
5309
  });
6713
- server.patch(`${prefix}/cloud/environments/:id/packages/:pkgId/disable`, async (req, res) => {
5310
+ server.post(`${prefix}/packages/:id/publish`, async (req, res) => {
6714
5311
  try {
6715
- const result = await dispatcher.handleCloud(`/projects/${req.params.id}/packages/${req.params.pkgId}/disable`, "PATCH", {}, {}, { request: req });
5312
+ const result = await dispatcher.handlePackages(`/${req.params.id}/publish`, "POST", req.body, {}, { request: req });
6716
5313
  sendResult(result, res);
6717
5314
  } catch (err) {
6718
5315
  errorResponse(err, res);
6719
5316
  }
6720
5317
  });
6721
- server.post(`${prefix}/cloud/environments/:id/packages/:pkgId/upgrade`, async (req, res) => {
5318
+ server.post(`${prefix}/packages/:id/revert`, async (req, res) => {
6722
5319
  try {
6723
- const result = await dispatcher.handleCloud(`/projects/${req.params.id}/packages/${req.params.pkgId}/upgrade`, "POST", req.body, {}, { request: req });
5320
+ const result = await dispatcher.handlePackages(`/${req.params.id}/revert`, "POST", req.body, {}, { request: req });
6724
5321
  sendResult(result, res);
6725
5322
  } catch (err) {
6726
5323
  errorResponse(err, res);
@@ -6847,6 +5444,22 @@ function createDispatcherPlugin(config = {}) {
6847
5444
  errorResponse(err, res);
6848
5445
  }
6849
5446
  });
5447
+ server.post(`${base}/automation/:name/runs/:runId/resume`, async (req, res) => {
5448
+ try {
5449
+ const result = await dispatcher.dispatch("POST", `/automation/${req.params.name}/runs/${req.params.runId}/resume`, req.body, req.query, { request: req });
5450
+ sendResult(result, res);
5451
+ } catch (err) {
5452
+ errorResponse(err, res);
5453
+ }
5454
+ });
5455
+ server.get(`${base}/automation/:name/runs/:runId/screen`, async (req, res) => {
5456
+ try {
5457
+ const result = await dispatcher.dispatch("GET", `/automation/${req.params.name}/runs/${req.params.runId}/screen`, void 0, req.query, { request: req });
5458
+ sendResult(result, res);
5459
+ } catch (err) {
5460
+ errorResponse(err, res);
5461
+ }
5462
+ });
6850
5463
  };
6851
5464
  const registerAIRoutes = (base) => {
6852
5465
  const wildcards = [
@@ -7789,19 +6402,15 @@ init_driver_plugin();
7789
6402
  init_app_plugin();
7790
6403
  import { createHmac as createHmac2 } from "crypto";
7791
6404
  import { ObjectKernel as ObjectKernel3 } from "@objectstack/core";
7792
- import { readEnvWithDeprecation as readEnvWithDeprecation4 } from "@objectstack/types";
6405
+ import { readEnvWithDeprecation as readEnvWithDeprecation3 } from "@objectstack/types";
7793
6406
 
7794
6407
  // src/cloud/capability-loader.ts
7795
6408
  var CAPABILITY_PROVIDERS = {
7796
6409
  automation: {
6410
+ // Self-contained: AutomationServicePlugin seeds all built-in node
6411
+ // executors itself (ADR-0018), so no companion node-pack plugins.
7797
6412
  pkg: "@objectstack/service-automation",
7798
- export: "AutomationServicePlugin",
7799
- extras: [
7800
- { pkg: "@objectstack/service-automation", export: "CrudNodesPlugin" },
7801
- { pkg: "@objectstack/service-automation", export: "LogicNodesPlugin" },
7802
- { pkg: "@objectstack/service-automation", export: "HttpConnectorPlugin" },
7803
- { pkg: "@objectstack/service-automation", export: "ScreenNodesPlugin" }
7804
- ]
6413
+ export: "AutomationServicePlugin"
7805
6414
  },
7806
6415
  ai: {
7807
6416
  pkg: "@objectstack/service-ai",
@@ -7832,6 +6441,19 @@ var CAPABILITY_PROVIDERS = {
7832
6441
  pkg: "@objectstack/service-job",
7833
6442
  export: "JobServicePlugin"
7834
6443
  },
6444
+ messaging: {
6445
+ // Backs the `notify` flow node (ADR-0012): delivers to a user's
6446
+ // channels (inbox by default → `sys_inbox_message` rows).
6447
+ pkg: "@objectstack/service-messaging",
6448
+ export: "MessagingServicePlugin"
6449
+ },
6450
+ triggers: {
6451
+ // Concrete flow triggers — record-change (ObjectQL hooks) + schedule
6452
+ // (cron/interval via the job service; pair `triggers` with `job`).
6453
+ pkg: "@objectstack/plugin-trigger-record-change",
6454
+ export: "RecordChangeTriggerPlugin",
6455
+ extras: [{ pkg: "@objectstack/plugin-trigger-schedule", export: "ScheduleTriggerPlugin" }]
6456
+ },
7835
6457
  realtime: {
7836
6458
  pkg: "@objectstack/service-realtime",
7837
6459
  export: "RealtimeServicePlugin"
@@ -7849,7 +6471,11 @@ async function loadCapabilities(opts) {
7849
6471
  const { kernel, requires, bundle, environmentId } = opts;
7850
6472
  const logger = opts.logger ?? console;
7851
6473
  const installed = [];
7852
- for (const cap of requires) {
6474
+ const resolved = [...new Set(requires)];
6475
+ if (resolved.includes("audit") && !resolved.includes("messaging")) {
6476
+ resolved.push("messaging");
6477
+ }
6478
+ for (const cap of resolved) {
7853
6479
  const spec = CAPABILITY_PROVIDERS[cap];
7854
6480
  if (!spec) {
7855
6481
  continue;
@@ -7916,8 +6542,197 @@ async function loadCapabilities(opts) {
7916
6542
  return installed;
7917
6543
  }
7918
6544
 
6545
+ // src/cloud/platform-sso.ts
6546
+ import { createHmac, createHash } from "crypto";
6547
+ var PLATFORM_SSO_PROVIDER_ID = "objectstack-cloud";
6548
+ function derivePlatformSsoClientId(environmentId) {
6549
+ return `project_${environmentId}`;
6550
+ }
6551
+ function derivePlatformSsoClientSecret(baseSecret, environmentId) {
6552
+ return createHmac("sha256", baseSecret).update(`oauth-client:${environmentId}`).digest("hex");
6553
+ }
6554
+ function hashPlatformSsoClientSecret(plaintext) {
6555
+ return createHash("sha256").update(plaintext).digest("base64").replace(/=+$/, "").replace(/\+/g, "-").replace(/\//g, "_");
6556
+ }
6557
+ function buildPlatformSsoRedirectUri(hostname, basePath = "/api/v1/auth") {
6558
+ let host;
6559
+ if (hostname.startsWith("http://") || hostname.startsWith("https://")) {
6560
+ host = hostname;
6561
+ } else if (/(\.|^)localhost(:\d+)?$/i.test(hostname)) {
6562
+ const port = (process.env.OS_RUNTIME_PORT ?? "").trim();
6563
+ const hostWithPort = /:\d+$/.test(hostname) || !port ? hostname : `${hostname}:${port}`;
6564
+ host = `http://${hostWithPort}`;
6565
+ } else {
6566
+ host = `https://${hostname}`;
6567
+ }
6568
+ const trimmed = host.replace(/\/+$/, "");
6569
+ const path = basePath.replace(/\/+$/, "");
6570
+ return `${trimmed}${path}/oauth2/callback/${PLATFORM_SSO_PROVIDER_ID}`;
6571
+ }
6572
+ async function seedPlatformSsoClient(opts) {
6573
+ const { ql, environmentId, hostname, baseSecret, logger, throwOnError } = opts;
6574
+ if (!baseSecret) {
6575
+ logger?.warn?.("[platform-sso] OS_AUTH_SECRET not set \u2014 skipping client seed", { environmentId });
6576
+ return;
6577
+ }
6578
+ const clientId = derivePlatformSsoClientId(environmentId);
6579
+ const clientSecretPlaintext = derivePlatformSsoClientSecret(baseSecret, environmentId);
6580
+ const clientSecretStored = hashPlatformSsoClientSecret(clientSecretPlaintext);
6581
+ const desiredRedirect = hostname ? buildPlatformSsoRedirectUri(hostname) : null;
6582
+ let existing = null;
6583
+ try {
6584
+ const rows = await ql.find("sys_oauth_application", {
6585
+ where: { client_id: clientId },
6586
+ limit: 1
6587
+ }, { context: { isSystem: true } });
6588
+ const list = Array.isArray(rows) ? rows : Array.isArray(rows?.records) ? rows.records : [];
6589
+ existing = list[0] ?? null;
6590
+ } catch (err) {
6591
+ logger?.warn?.("[platform-sso] sys_oauth_application read failed \u2014 skipping seed", {
6592
+ environmentId,
6593
+ error: err?.message
6594
+ });
6595
+ return;
6596
+ }
6597
+ const nowIso = (/* @__PURE__ */ new Date()).toISOString();
6598
+ if (!existing) {
6599
+ const redirects = desiredRedirect ? [desiredRedirect] : [];
6600
+ try {
6601
+ await ql.insert("sys_oauth_application", {
6602
+ id: `oauthc_${environmentId}`,
6603
+ name: `Project ${environmentId}`,
6604
+ client_id: clientId,
6605
+ client_secret: clientSecretStored,
6606
+ type: "web",
6607
+ redirect_uris: JSON.stringify(redirects),
6608
+ grant_types: JSON.stringify(["authorization_code", "refresh_token"]),
6609
+ response_types: JSON.stringify(["code"]),
6610
+ scopes: JSON.stringify(["openid", "email", "profile"]),
6611
+ token_endpoint_auth_method: "client_secret_basic",
6612
+ require_pkce: false,
6613
+ skip_consent: true,
6614
+ disabled: false,
6615
+ subject_type: "public",
6616
+ created_at: nowIso,
6617
+ updated_at: nowIso
6618
+ }, { context: { isSystem: true } });
6619
+ logger?.info?.("[platform-sso] sys_oauth_application row created", { environmentId, clientId });
6620
+ } catch (err) {
6621
+ logger?.warn?.("[platform-sso] sys_oauth_application create failed", {
6622
+ environmentId,
6623
+ error: err?.message
6624
+ });
6625
+ if (throwOnError) throw err;
6626
+ }
6627
+ return;
6628
+ }
6629
+ let currentRedirects = [];
6630
+ try {
6631
+ const raw = existing.redirect_uris;
6632
+ const parsed = typeof raw === "string" ? JSON.parse(raw) : raw;
6633
+ if (Array.isArray(parsed)) currentRedirects = parsed.filter((s) => typeof s === "string");
6634
+ } catch {
6635
+ }
6636
+ const mergedRedirects = desiredRedirect && !currentRedirects.includes(desiredRedirect) ? [...currentRedirects, desiredRedirect] : currentRedirects;
6637
+ const repairPatch = {
6638
+ name: existing.name || `Project ${environmentId}`,
6639
+ client_secret: clientSecretStored,
6640
+ type: existing.type || "web",
6641
+ redirect_uris: JSON.stringify(mergedRedirects),
6642
+ grant_types: JSON.stringify(["authorization_code", "refresh_token"]),
6643
+ response_types: JSON.stringify(["code"]),
6644
+ scopes: JSON.stringify(["openid", "email", "profile"]),
6645
+ token_endpoint_auth_method: "client_secret_basic",
6646
+ require_pkce: false,
6647
+ skip_consent: true,
6648
+ disabled: false,
6649
+ subject_type: "public",
6650
+ updated_at: nowIso
6651
+ };
6652
+ try {
6653
+ await ql.update(
6654
+ "sys_oauth_application",
6655
+ repairPatch,
6656
+ { where: { id: existing.id } },
6657
+ { context: { isSystem: true } }
6658
+ );
6659
+ logger?.info?.("[platform-sso] sys_oauth_application repaired", {
6660
+ environmentId,
6661
+ clientId,
6662
+ redirect_uris: mergedRedirects
6663
+ });
6664
+ } catch (err) {
6665
+ logger?.warn?.("[platform-sso] sys_oauth_application repair failed", {
6666
+ environmentId,
6667
+ error: err?.message
6668
+ });
6669
+ if (throwOnError) throw err;
6670
+ }
6671
+ }
6672
+ async function backfillPlatformSsoClients(opts) {
6673
+ const { ql, baseSecret, logger, limit = 1e3 } = opts;
6674
+ if (!baseSecret) {
6675
+ logger?.warn?.("[platform-sso] backfill skipped \u2014 OS_AUTH_SECRET not set");
6676
+ return { scanned: 0, seeded: 0, alreadyExisted: 0, failures: [] };
6677
+ }
6678
+ let projects = [];
6679
+ try {
6680
+ const rows = await ql.find("sys_environment", {
6681
+ limit,
6682
+ fields: ["id", "hostname", "status"]
6683
+ }, { context: { isSystem: true } });
6684
+ projects = Array.isArray(rows) ? rows : Array.isArray(rows?.records) ? rows.records : [];
6685
+ } catch (err) {
6686
+ logger?.warn?.("[platform-sso] backfill: sys_environment read failed", {
6687
+ error: err?.message
6688
+ });
6689
+ return { scanned: 0, seeded: 0, alreadyExisted: 0, failures: [{ environmentId: "<scan>", error: err?.message ?? String(err) }] };
6690
+ }
6691
+ let seeded = 0;
6692
+ let alreadyExisted = 0;
6693
+ const failures = [];
6694
+ for (const p of projects) {
6695
+ if (!p?.id) continue;
6696
+ const before = await (async () => {
6697
+ try {
6698
+ const r = await ql.find("sys_oauth_application", {
6699
+ where: { client_id: derivePlatformSsoClientId(p.id) },
6700
+ limit: 1
6701
+ }, { context: { isSystem: true } });
6702
+ const list = Array.isArray(r) ? r : Array.isArray(r?.records) ? r.records : [];
6703
+ return list[0] ?? null;
6704
+ } catch {
6705
+ return null;
6706
+ }
6707
+ })();
6708
+ try {
6709
+ await seedPlatformSsoClient({ ql, environmentId: p.id, hostname: p.hostname, baseSecret, logger, throwOnError: true });
6710
+ if (before) alreadyExisted++;
6711
+ else {
6712
+ const after = await (async () => {
6713
+ try {
6714
+ const r = await ql.find("sys_oauth_application", {
6715
+ where: { client_id: derivePlatformSsoClientId(p.id) },
6716
+ limit: 1
6717
+ }, { context: { isSystem: true } });
6718
+ const list = Array.isArray(r) ? r : Array.isArray(r?.records) ? r.records : [];
6719
+ return list[0] ?? null;
6720
+ } catch (err) {
6721
+ return { _readErr: err?.message };
6722
+ }
6723
+ })();
6724
+ if (after && !after._readErr) seeded++;
6725
+ else failures.push({ environmentId: p.id, error: `post-insert read returned ${after ? JSON.stringify(after) : "null"}` });
6726
+ }
6727
+ } catch (err) {
6728
+ failures.push({ environmentId: p.id, error: err?.message ?? String(err) });
6729
+ }
6730
+ }
6731
+ logger?.info?.("[platform-sso] backfill complete", { scanned: projects.length, seeded, alreadyExisted, failures: failures.length });
6732
+ return { scanned: projects.length, seeded, alreadyExisted, failures };
6733
+ }
6734
+
7919
6735
  // src/cloud/artifact-kernel-factory.ts
7920
- init_platform_sso();
7921
6736
  function deriveProjectAuthSecret(baseSecret, environmentId) {
7922
6737
  return createHmac2("sha256", baseSecret).update(`project:${environmentId}`).digest("hex");
7923
6738
  }
@@ -7927,7 +6742,7 @@ var ArtifactKernelFactory = class {
7927
6742
  this.envRegistry = config.envRegistry;
7928
6743
  this.logger = config.logger ?? console;
7929
6744
  this.kernelConfig = config.kernelConfig;
7930
- this.authBaseSecret = (config.authBaseSecret ?? readEnvWithDeprecation4("OS_AUTH_SECRET", ["AUTH_SECRET", "BETTER_AUTH_SECRET"]) ?? "").trim();
6745
+ this.authBaseSecret = (config.authBaseSecret ?? readEnvWithDeprecation3("OS_AUTH_SECRET", ["AUTH_SECRET", "BETTER_AUTH_SECRET"]) ?? "").trim();
7931
6746
  }
7932
6747
  async create(environmentId) {
7933
6748
  let cached = this.envRegistry.peekById(environmentId);
@@ -8040,7 +6855,7 @@ var ArtifactKernelFactory = class {
8040
6855
  this.logger.warn?.("[ArtifactKernelFactory] OS_AUTH_SECRET not set \u2014 per-project AuthPlugin skipped (auth endpoints will return 404)", { environmentId });
8041
6856
  }
8042
6857
  try {
8043
- const multiTenant = String(readEnvWithDeprecation4("OS_MULTI_ORG_ENABLED", "OS_MULTI_TENANT") ?? "false").toLowerCase() !== "false";
6858
+ const multiTenant = String(readEnvWithDeprecation3("OS_MULTI_ORG_ENABLED", "OS_MULTI_TENANT") ?? "false").toLowerCase() !== "false";
8044
6859
  if (multiTenant) {
8045
6860
  try {
8046
6861
  const { OrgScopingPlugin } = await import("@objectstack/plugin-org-scoping");
@@ -9243,7 +8058,7 @@ async function createObjectOSStack(config) {
9243
8058
  // src/cloud/marketplace-install-local-plugin.ts
9244
8059
  import { existsSync as existsSync3, mkdirSync as mkdirSync4, readFileSync as readFileSync2, readdirSync, unlinkSync, writeFileSync as writeFileSync3 } from "fs";
9245
8060
  import { join as join2, resolve } from "path";
9246
- import { readEnvWithDeprecation as readEnvWithDeprecation5 } from "@objectstack/types";
8061
+ import { readEnvWithDeprecation as readEnvWithDeprecation4 } from "@objectstack/types";
9247
8062
  var ROUTE_BASE = "/api/v1/marketplace/install-local";
9248
8063
  var DEFAULT_DIR = ".objectstack/installed-packages";
9249
8064
  function safeFilename(manifestId) {
@@ -9787,7 +8602,7 @@ var MarketplaceInstallLocalPlugin = class {
9787
8602
  }
9788
8603
  }
9789
8604
  if (opts.seedNow && datasets.length > 0) {
9790
- const multiTenant = String(readEnvWithDeprecation5("OS_MULTI_ORG_ENABLED", "OS_MULTI_TENANT") ?? "false").toLowerCase() !== "false";
8605
+ const multiTenant = String(readEnvWithDeprecation4("OS_MULTI_ORG_ENABLED", "OS_MULTI_TENANT") ?? "false").toLowerCase() !== "false";
9791
8606
  try {
9792
8607
  const ql = ctx.getService("objectql");
9793
8608
  let metadata;
@@ -9905,9 +8720,6 @@ var MarketplaceInstallLocalPlugin = class {
9905
8720
  }
9906
8721
  };
9907
8722
 
9908
- // src/index.ts
9909
- init_platform_sso();
9910
-
9911
8723
  // src/sandbox/script-runner.ts
9912
8724
  var UnimplementedScriptRunner = class {
9913
8725
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
@@ -9938,7 +8750,7 @@ import {
9938
8750
  createRestApiPlugin
9939
8751
  } from "@objectstack/rest";
9940
8752
  export * from "@objectstack/core";
9941
- import { readEnvWithDeprecation as readEnvWithDeprecation6, _resetEnvDeprecationWarnings } from "@objectstack/types";
8753
+ import { readEnvWithDeprecation as readEnvWithDeprecation5, _resetEnvDeprecationWarnings } from "@objectstack/types";
9942
8754
  export {
9943
8755
  AppPlugin,
9944
8756
  ArtifactApiClient,
@@ -9948,6 +8760,7 @@ export {
9948
8760
  DEFAULT_CLOUD_URL,
9949
8761
  DEFAULT_RATE_LIMITS,
9950
8762
  DriverPlugin,
8763
+ ExternalValidationPlugin,
9951
8764
  FileArtifactApiClient,
9952
8765
  HttpDispatcher,
9953
8766
  HttpServer,
@@ -9986,6 +8799,7 @@ export {
9986
8799
  collectBundleHooks,
9987
8800
  createDefaultHostConfig,
9988
8801
  createDispatcherPlugin,
8802
+ createExternalValidationPlugin,
9989
8803
  createObjectOSStack,
9990
8804
  createRestApiPlugin,
9991
8805
  createStandaloneStack,
@@ -10001,7 +8815,7 @@ export {
10001
8815
  mergeRuntimeModule,
10002
8816
  parseTraceparent,
10003
8817
  readArtifactSource,
10004
- readEnvWithDeprecation6 as readEnvWithDeprecation,
8818
+ readEnvWithDeprecation5 as readEnvWithDeprecation,
10005
8819
  resolveCloudUrl,
10006
8820
  resolveDefaultArtifactPath,
10007
8821
  resolveErrorReporter,