@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.cjs +769 -1955
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +88 -14
- package/dist/index.d.ts +88 -14
- package/dist/index.js +782 -1968
- package/dist/index.js.map +1 -1
- package/package.json +18 -18
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
|
-
|
|
278
|
+
baseEvalCtx
|
|
277
279
|
);
|
|
278
|
-
const record = seedResult.ok ? { ...seedResult.value } : { ...dataset.records[i] };
|
|
279
280
|
if (!seedResult.ok) {
|
|
280
|
-
|
|
281
|
-
|
|
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
|
-
|
|
363
|
-
|
|
364
|
-
|
|
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
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
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/
|
|
2536
|
-
|
|
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
|
-
|
|
3941
|
-
|
|
3942
|
-
|
|
3943
|
-
|
|
3944
|
-
|
|
3945
|
-
|
|
3946
|
-
|
|
3947
|
-
|
|
3948
|
-
|
|
3949
|
-
|
|
3950
|
-
|
|
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
|
-
|
|
3953
|
-
} catch {
|
|
3954
|
-
headers = rawHeaders;
|
|
3955
|
-
}
|
|
4025
|
+
};
|
|
3956
4026
|
}
|
|
3957
|
-
|
|
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
|
-
|
|
3967
|
-
|
|
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
|
-
|
|
3970
|
-
|
|
3971
|
-
|
|
3972
|
-
|
|
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
|
|
4037
|
-
|
|
4038
|
-
|
|
4039
|
-
|
|
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
|
-
|
|
4050
|
-
|
|
4051
|
-
|
|
4052
|
-
|
|
4053
|
-
|
|
4054
|
-
|
|
4055
|
-
|
|
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
|
-
|
|
4058
|
-
|
|
4059
|
-
|
|
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
|
-
|
|
4066
|
-
|
|
4067
|
-
|
|
4068
|
-
|
|
4069
|
-
|
|
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
|
-
|
|
4079
|
-
|
|
4080
|
-
|
|
4081
|
-
|
|
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
|
-
|
|
4089
|
-
|
|
4090
|
-
|
|
4091
|
-
|
|
4092
|
-
|
|
4093
|
-
|
|
4094
|
-
|
|
4095
|
-
|
|
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 {
|
|
4104
|
-
}
|
|
4105
|
-
|
|
4106
|
-
if (
|
|
4107
|
-
|
|
4108
|
-
|
|
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
|
-
|
|
4111
|
-
|
|
4112
|
-
|
|
4113
|
-
|
|
4114
|
-
|
|
4115
|
-
|
|
4116
|
-
|
|
4117
|
-
|
|
4118
|
-
|
|
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
|
-
|
|
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
|
|
4124
|
-
|
|
4125
|
-
|
|
4126
|
-
return { handled: true, response: this.
|
|
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
|
-
|
|
4129
|
-
|
|
4130
|
-
if (
|
|
4131
|
-
|
|
4132
|
-
|
|
4133
|
-
|
|
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
|
-
|
|
4136
|
-
|
|
4137
|
-
|
|
4138
|
-
|
|
4139
|
-
|
|
4140
|
-
|
|
4141
|
-
|
|
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
|
-
|
|
5704
|
-
|
|
5705
|
-
|
|
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.
|
|
5201
|
+
server.get(`${prefix}/health`, async (_req, res) => {
|
|
6610
5202
|
try {
|
|
6611
|
-
const result = await dispatcher.
|
|
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}/
|
|
5209
|
+
server.post(`${prefix}/auth/login`, async (req, res) => {
|
|
6618
5210
|
try {
|
|
6619
|
-
const result = await dispatcher.
|
|
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}/
|
|
5217
|
+
server.post(`${prefix}/graphql`, async (req, res) => {
|
|
6626
5218
|
try {
|
|
6627
|
-
const result = await dispatcher.
|
|
6628
|
-
|
|
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}/
|
|
5230
|
+
server.post(`${prefix}/analytics/query`, async (req, res) => {
|
|
6634
5231
|
try {
|
|
6635
|
-
const result = await dispatcher.
|
|
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}/
|
|
5238
|
+
server.get(`${prefix}/analytics/meta`, async (req, res) => {
|
|
6642
5239
|
try {
|
|
6643
|
-
const result = await dispatcher.
|
|
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}/
|
|
5246
|
+
server.post(`${prefix}/analytics/sql`, async (req, res) => {
|
|
6650
5247
|
try {
|
|
6651
|
-
const result = await dispatcher.
|
|
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.
|
|
5254
|
+
server.get(`${prefix}/packages`, async (req, res) => {
|
|
6658
5255
|
try {
|
|
6659
|
-
const result = await dispatcher.
|
|
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.
|
|
5262
|
+
server.post(`${prefix}/packages`, async (req, res) => {
|
|
6666
5263
|
try {
|
|
6667
|
-
const result = await dispatcher.
|
|
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}/
|
|
5270
|
+
server.get(`${prefix}/packages/:id/export`, async (req, res) => {
|
|
6674
5271
|
try {
|
|
6675
|
-
const result = await dispatcher.
|
|
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.
|
|
5278
|
+
server.get(`${prefix}/packages/:id`, async (req, res) => {
|
|
6682
5279
|
try {
|
|
6683
|
-
const result = await dispatcher.
|
|
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.
|
|
5286
|
+
server.delete(`${prefix}/packages/:id`, async (req, res) => {
|
|
6690
5287
|
try {
|
|
6691
|
-
const result = await dispatcher.
|
|
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.
|
|
5294
|
+
server.patch(`${prefix}/packages/:id/enable`, async (req, res) => {
|
|
6698
5295
|
try {
|
|
6699
|
-
const result = await dispatcher.
|
|
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}/
|
|
5302
|
+
server.patch(`${prefix}/packages/:id/disable`, async (req, res) => {
|
|
6706
5303
|
try {
|
|
6707
|
-
const result = await dispatcher.
|
|
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.
|
|
5310
|
+
server.post(`${prefix}/packages/:id/publish`, async (req, res) => {
|
|
6714
5311
|
try {
|
|
6715
|
-
const result = await dispatcher.
|
|
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}/
|
|
5318
|
+
server.post(`${prefix}/packages/:id/revert`, async (req, res) => {
|
|
6722
5319
|
try {
|
|
6723
|
-
const result = await dispatcher.
|
|
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
|
|
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
|
-
|
|
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 ??
|
|
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(
|
|
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
|
|
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(
|
|
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
|
|
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
|
-
|
|
8818
|
+
readEnvWithDeprecation5 as readEnvWithDeprecation,
|
|
10005
8819
|
resolveCloudUrl,
|
|
10006
8820
|
resolveDefaultArtifactPath,
|
|
10007
8821
|
resolveErrorReporter,
|