@objectstack/runtime 7.2.1 → 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 +1169 -2155
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +112 -18
- package/dist/index.d.ts +112 -18
- package/dist/index.js +1135 -2121
- 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] ?? "");
|
|
@@ -702,6 +723,52 @@ var init_seed_loader = __esm({
|
|
|
702
723
|
}
|
|
703
724
|
});
|
|
704
725
|
|
|
726
|
+
// src/package-state-store.ts
|
|
727
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
728
|
+
import { dirname as dirname2, join } from "path";
|
|
729
|
+
function sanitizeEnvironmentId(environmentId) {
|
|
730
|
+
const raw = (environmentId ?? process.env.OS_ENVIRONMENT_ID ?? DEFAULT_ENVIRONMENT_ID).trim();
|
|
731
|
+
const safe = raw.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
732
|
+
return safe.length > 0 ? safe : DEFAULT_ENVIRONMENT_ID;
|
|
733
|
+
}
|
|
734
|
+
function stateFilePath(environmentId) {
|
|
735
|
+
return join(resolveObjectStackHome(), "package-state", `${sanitizeEnvironmentId(environmentId)}.json`);
|
|
736
|
+
}
|
|
737
|
+
function readState(environmentId) {
|
|
738
|
+
const file = stateFilePath(environmentId);
|
|
739
|
+
if (!existsSync(file)) return {};
|
|
740
|
+
try {
|
|
741
|
+
const parsed = JSON.parse(readFileSync(file, "utf8"));
|
|
742
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
743
|
+
} catch {
|
|
744
|
+
return {};
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
function writeState(environmentId, state) {
|
|
748
|
+
const file = stateFilePath(environmentId);
|
|
749
|
+
mkdirSync(dirname2(file), { recursive: true });
|
|
750
|
+
writeFileSync(file, `${JSON.stringify(state, null, 2)}
|
|
751
|
+
`, "utf8");
|
|
752
|
+
}
|
|
753
|
+
function loadDisabledPackageIds(environmentId) {
|
|
754
|
+
const disabled = readState(environmentId).disabled;
|
|
755
|
+
return new Set(Array.isArray(disabled) ? disabled.filter((id) => typeof id === "string") : []);
|
|
756
|
+
}
|
|
757
|
+
function setPackageDisabled(environmentId, packageId, disabled) {
|
|
758
|
+
const ids = loadDisabledPackageIds(environmentId);
|
|
759
|
+
if (disabled) ids.add(packageId);
|
|
760
|
+
else ids.delete(packageId);
|
|
761
|
+
writeState(environmentId, { disabled: Array.from(ids).sort() });
|
|
762
|
+
}
|
|
763
|
+
var DEFAULT_ENVIRONMENT_ID;
|
|
764
|
+
var init_package_state_store = __esm({
|
|
765
|
+
"src/package-state-store.ts"() {
|
|
766
|
+
"use strict";
|
|
767
|
+
init_standalone_stack();
|
|
768
|
+
DEFAULT_ENVIRONMENT_ID = "default";
|
|
769
|
+
}
|
|
770
|
+
});
|
|
771
|
+
|
|
705
772
|
// src/sandbox/quickjs-runner.ts
|
|
706
773
|
import {
|
|
707
774
|
newAsyncContext
|
|
@@ -1203,6 +1270,7 @@ __export(app_plugin_exports, {
|
|
|
1203
1270
|
collectBundleHooks: () => collectBundleHooks
|
|
1204
1271
|
});
|
|
1205
1272
|
import { readEnvWithDeprecation } from "@objectstack/types";
|
|
1273
|
+
import { SystemUserId } from "@objectstack/spec/system";
|
|
1206
1274
|
function collectBundleHooks(bundle) {
|
|
1207
1275
|
const out = [];
|
|
1208
1276
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -1267,6 +1335,7 @@ var init_app_plugin = __esm({
|
|
|
1267
1335
|
"src/app-plugin.ts"() {
|
|
1268
1336
|
"use strict";
|
|
1269
1337
|
init_seed_loader();
|
|
1338
|
+
init_package_state_store();
|
|
1270
1339
|
init_quickjs_runner();
|
|
1271
1340
|
init_body_runner();
|
|
1272
1341
|
AppPlugin = class {
|
|
@@ -1292,6 +1361,24 @@ var init_app_plugin = __esm({
|
|
|
1292
1361
|
console.warn(
|
|
1293
1362
|
`[AppPlugin:init] appId=${appId} keys=${Object.keys(servicePayload).join(",")} flows=${Array.isArray(servicePayload.flows) ? servicePayload.flows.length : "n/a"}`
|
|
1294
1363
|
);
|
|
1364
|
+
try {
|
|
1365
|
+
const ql = ctx.getService("objectql");
|
|
1366
|
+
const setter = ql?.registry?.setInitialDisabledPackageIds;
|
|
1367
|
+
if (typeof setter === "function") {
|
|
1368
|
+
const disabled = loadDisabledPackageIds(this.projectContext?.environmentId);
|
|
1369
|
+
if (disabled.size > 0) {
|
|
1370
|
+
setter.call(ql.registry, disabled);
|
|
1371
|
+
ctx.logger.info("[AppPlugin] seeded persisted disabled packages", {
|
|
1372
|
+
environmentId: this.projectContext?.environmentId,
|
|
1373
|
+
disabled: Array.from(disabled)
|
|
1374
|
+
});
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
} catch (err) {
|
|
1378
|
+
ctx.logger.warn("[AppPlugin] failed to seed persisted package state", {
|
|
1379
|
+
error: err?.message ?? String(err)
|
|
1380
|
+
});
|
|
1381
|
+
}
|
|
1295
1382
|
ctx.getService("manifest").register(servicePayload);
|
|
1296
1383
|
};
|
|
1297
1384
|
this.start = async (ctx) => {
|
|
@@ -1323,6 +1410,27 @@ var init_app_plugin = __esm({
|
|
|
1323
1410
|
});
|
|
1324
1411
|
ql.setDatasourceMapping(this.bundle.datasourceMapping);
|
|
1325
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
|
+
}
|
|
1326
1434
|
const stackBundle = this.bundle.default || this.bundle;
|
|
1327
1435
|
const runtime = stackBundle && typeof stackBundle.onEnable === "function" ? stackBundle : this.bundle;
|
|
1328
1436
|
if (runtime && typeof runtime.onEnable === "function") {
|
|
@@ -1417,49 +1525,6 @@ var init_app_plugin = __esm({
|
|
|
1417
1525
|
appId
|
|
1418
1526
|
});
|
|
1419
1527
|
}
|
|
1420
|
-
try {
|
|
1421
|
-
const approvals = Array.isArray(this.bundle.approvals) ? this.bundle.approvals : Array.isArray((this.bundle.manifest || {}).approvals) ? this.bundle.manifest.approvals : [];
|
|
1422
|
-
if (approvals.length > 0) {
|
|
1423
|
-
ctx.hook("kernel:ready", async () => {
|
|
1424
|
-
let svc;
|
|
1425
|
-
try {
|
|
1426
|
-
svc = ctx.getService("approvals");
|
|
1427
|
-
} catch {
|
|
1428
|
-
}
|
|
1429
|
-
if (!svc || typeof svc.defineProcess !== "function") {
|
|
1430
|
-
ctx.logger.warn("[AppPlugin] approvals service not registered \u2014 skipping declarative processes", {
|
|
1431
|
-
appId,
|
|
1432
|
-
processCount: approvals.length
|
|
1433
|
-
});
|
|
1434
|
-
return;
|
|
1435
|
-
}
|
|
1436
|
-
const sysCtx = { isSystem: true, roles: [], permissions: [] };
|
|
1437
|
-
let ok = 0;
|
|
1438
|
-
for (const proc of approvals) {
|
|
1439
|
-
try {
|
|
1440
|
-
await svc.defineProcess({
|
|
1441
|
-
name: proc.name,
|
|
1442
|
-
label: proc.label,
|
|
1443
|
-
object: proc.object,
|
|
1444
|
-
description: proc.description,
|
|
1445
|
-
active: proc.active !== false,
|
|
1446
|
-
definition: proc
|
|
1447
|
-
}, sysCtx);
|
|
1448
|
-
ok++;
|
|
1449
|
-
} catch (err) {
|
|
1450
|
-
ctx.logger.warn("[AppPlugin] Failed to register approval process", {
|
|
1451
|
-
appId,
|
|
1452
|
-
process: proc?.name,
|
|
1453
|
-
error: err?.message ?? String(err)
|
|
1454
|
-
});
|
|
1455
|
-
}
|
|
1456
|
-
}
|
|
1457
|
-
ctx.logger.info("[AppPlugin] Registered approval processes", { appId, count: ok });
|
|
1458
|
-
});
|
|
1459
|
-
}
|
|
1460
|
-
} catch (err) {
|
|
1461
|
-
ctx.logger.error("[AppPlugin] Failed to schedule approval-process registration", err, { appId });
|
|
1462
|
-
}
|
|
1463
1528
|
try {
|
|
1464
1529
|
const jobs = Array.isArray(this.bundle.jobs) ? this.bundle.jobs : Array.isArray((this.bundle.manifest || {}).jobs) ? this.bundle.manifest.jobs : [];
|
|
1465
1530
|
if (jobs.length > 0) {
|
|
@@ -1536,6 +1601,7 @@ var init_app_plugin = __esm({
|
|
|
1536
1601
|
...d,
|
|
1537
1602
|
object: d.object
|
|
1538
1603
|
}));
|
|
1604
|
+
const seedIdentity = await this.ensureSeedIdentity(ql, ctx.logger);
|
|
1539
1605
|
try {
|
|
1540
1606
|
const kernel = ctx.kernel;
|
|
1541
1607
|
const existing = (() => {
|
|
@@ -1577,7 +1643,12 @@ var init_app_plugin = __esm({
|
|
|
1577
1643
|
config: {
|
|
1578
1644
|
defaultMode: "upsert",
|
|
1579
1645
|
multiPass: true,
|
|
1580
|
-
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
|
|
1581
1652
|
}
|
|
1582
1653
|
});
|
|
1583
1654
|
const result = await seedLoader.load(request);
|
|
@@ -1605,14 +1676,34 @@ var init_app_plugin = __esm({
|
|
|
1605
1676
|
const { SeedLoaderRequestSchema } = await import("@objectstack/spec/data");
|
|
1606
1677
|
const request = SeedLoaderRequestSchema.parse({
|
|
1607
1678
|
datasets: normalizedDatasets,
|
|
1608
|
-
config: { defaultMode: "upsert", multiPass: true }
|
|
1679
|
+
config: { defaultMode: "upsert", multiPass: true, identity: seedIdentity }
|
|
1609
1680
|
});
|
|
1610
1681
|
const result = await seedLoader.load(request);
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
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
|
+
}
|
|
1616
1707
|
} else {
|
|
1617
1708
|
ctx.logger.debug("[Seeder] No metadata service; using basic insert fallback");
|
|
1618
1709
|
for (const dataset of normalizedDatasets) {
|
|
@@ -1714,6 +1805,64 @@ var init_app_plugin = __esm({
|
|
|
1714
1805
|
this.name = `plugin.app.${appId}`;
|
|
1715
1806
|
this.version = sys?.version;
|
|
1716
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
|
+
}
|
|
1717
1866
|
/**
|
|
1718
1867
|
* Emit a kernel hook so the control-plane `AppCatalogService` can
|
|
1719
1868
|
* upsert / delete the corresponding `sys_app` row. Silently no-ops
|
|
@@ -1836,209 +1985,167 @@ var init_app_plugin = __esm({
|
|
|
1836
1985
|
}
|
|
1837
1986
|
});
|
|
1838
1987
|
|
|
1839
|
-
// src/
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
import { createHmac, createHash } from "crypto";
|
|
1851
|
-
function derivePlatformSsoClientId(environmentId) {
|
|
1852
|
-
return `project_${environmentId}`;
|
|
1853
|
-
}
|
|
1854
|
-
function derivePlatformSsoClientSecret(baseSecret, environmentId) {
|
|
1855
|
-
return createHmac("sha256", baseSecret).update(`oauth-client:${environmentId}`).digest("hex");
|
|
1856
|
-
}
|
|
1857
|
-
function hashPlatformSsoClientSecret(plaintext) {
|
|
1858
|
-
return createHash("sha256").update(plaintext).digest("base64").replace(/=+$/, "").replace(/\+/g, "-").replace(/\//g, "_");
|
|
1859
|
-
}
|
|
1860
|
-
function buildPlatformSsoRedirectUri(hostname, basePath = "/api/v1/auth") {
|
|
1861
|
-
let host;
|
|
1862
|
-
if (hostname.startsWith("http://") || hostname.startsWith("https://")) {
|
|
1863
|
-
host = hostname;
|
|
1864
|
-
} else if (/(\.|^)localhost(:\d+)?$/i.test(hostname)) {
|
|
1865
|
-
const port = (process.env.OS_RUNTIME_PORT ?? "").trim();
|
|
1866
|
-
const hostWithPort = /:\d+$/.test(hostname) || !port ? hostname : `${hostname}:${port}`;
|
|
1867
|
-
host = `http://${hostWithPort}`;
|
|
1868
|
-
} else {
|
|
1869
|
-
host = `https://${hostname}`;
|
|
1988
|
+
// src/standalone-stack.ts
|
|
1989
|
+
import { resolve as resolvePath2 } from "path";
|
|
1990
|
+
import { mkdirSync as mkdirSync2 } from "fs";
|
|
1991
|
+
import { homedir } from "os";
|
|
1992
|
+
import { z } from "zod";
|
|
1993
|
+
import { readEnvWithDeprecation as readEnvWithDeprecation2 } from "@objectstack/types";
|
|
1994
|
+
function resolveObjectStackHome() {
|
|
1995
|
+
const raw = process.env.OS_HOME?.trim();
|
|
1996
|
+
if (raw && raw.length > 0) {
|
|
1997
|
+
if (raw.startsWith("~")) return resolvePath2(homedir(), raw.slice(1).replace(/^[/\\]/, ""));
|
|
1998
|
+
return resolvePath2(raw);
|
|
1870
1999
|
}
|
|
1871
|
-
|
|
1872
|
-
const path = basePath.replace(/\/+$/, "");
|
|
1873
|
-
return `${trimmed}${path}/oauth2/callback/${PLATFORM_SSO_PROVIDER_ID}`;
|
|
2000
|
+
return resolvePath2(homedir(), ".objectstack");
|
|
1874
2001
|
}
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
if (
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
if (
|
|
1902
|
-
const
|
|
2002
|
+
function detectDriverFromUrl(dbUrl) {
|
|
2003
|
+
if (/^memory:\/\//i.test(dbUrl)) return "memory";
|
|
2004
|
+
if (/^(postgres(ql)?|pg):\/\//i.test(dbUrl)) return "postgres";
|
|
2005
|
+
if (/^mongodb(\+srv)?:\/\//i.test(dbUrl)) return "mongodb";
|
|
2006
|
+
if (/^wasm-sqlite:\/\//i.test(dbUrl)) return "sqlite-wasm";
|
|
2007
|
+
if (/\.wasm\.db$/i.test(dbUrl)) return "sqlite-wasm";
|
|
2008
|
+
if (/^file:/i.test(dbUrl)) return "sqlite";
|
|
2009
|
+
if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(dbUrl)) return "sqlite";
|
|
2010
|
+
throw new Error(
|
|
2011
|
+
`[StandaloneStack] Unsupported database URL scheme: ${dbUrl}. Supported schemes: memory://, postgres://, pg://, mongodb://, mongodb+srv://, file:`
|
|
2012
|
+
);
|
|
2013
|
+
}
|
|
2014
|
+
async function createStandaloneStack(config) {
|
|
2015
|
+
const cfg = StandaloneStackConfigSchema.parse(config ?? {});
|
|
2016
|
+
const { ObjectQLPlugin } = await import("@objectstack/objectql");
|
|
2017
|
+
const { MetadataPlugin } = await import("@objectstack/metadata");
|
|
2018
|
+
const { DriverPlugin: DriverPlugin2 } = await Promise.resolve().then(() => (init_driver_plugin(), driver_plugin_exports));
|
|
2019
|
+
const { AppPlugin: AppPlugin2 } = await Promise.resolve().then(() => (init_app_plugin(), app_plugin_exports));
|
|
2020
|
+
const cwd = process.cwd();
|
|
2021
|
+
const environmentId = cfg.environmentId ?? process.env.OS_ENVIRONMENT_ID ?? "proj_local";
|
|
2022
|
+
const artifactPathInput = cfg.artifactPath ?? process.env.OS_ARTIFACT_PATH ?? resolvePath2(cwd, "dist/objectstack.json");
|
|
2023
|
+
const artifactPath = isHttpUrl(artifactPathInput) ? artifactPathInput : artifactPathInput.startsWith("/") ? artifactPathInput : resolvePath2(cwd, artifactPathInput);
|
|
2024
|
+
const dbUrl = cfg.databaseUrl ?? readEnvWithDeprecation2("OS_DATABASE_URL", "DATABASE_URL")?.trim() ?? process.env.TURSO_DATABASE_URL?.trim() ?? (process.env.OS_HOME?.trim() ? `file:${resolvePath2(resolveObjectStackHome(), "data/standalone.db")}` : cfg.projectRoot ? `file:${resolvePath2(cfg.projectRoot, ".objectstack/data/standalone.db")}` : `file:${resolvePath2(resolveObjectStackHome(), "data/standalone.db")}`);
|
|
2025
|
+
const explicitDriver = cfg.databaseDriver ?? process.env.OS_DATABASE_DRIVER?.trim();
|
|
2026
|
+
const dbDriver = explicitDriver ?? detectDriverFromUrl(dbUrl);
|
|
2027
|
+
let driverPlugin;
|
|
2028
|
+
if (dbDriver === "memory") {
|
|
2029
|
+
const { InMemoryDriver } = await import("@objectstack/driver-memory");
|
|
2030
|
+
driverPlugin = new DriverPlugin2(new InMemoryDriver());
|
|
2031
|
+
} else if (dbDriver === "postgres") {
|
|
2032
|
+
const { SqlDriver } = await import("@objectstack/driver-sql");
|
|
2033
|
+
driverPlugin = new DriverPlugin2(
|
|
2034
|
+
new SqlDriver({
|
|
2035
|
+
client: "pg",
|
|
2036
|
+
connection: dbUrl,
|
|
2037
|
+
pool: { min: 0, max: 5 }
|
|
2038
|
+
})
|
|
2039
|
+
);
|
|
2040
|
+
} else if (dbDriver === "mongodb") {
|
|
2041
|
+
let MongoDBDriver;
|
|
1903
2042
|
try {
|
|
1904
|
-
await
|
|
1905
|
-
id: `oauthc_${environmentId}`,
|
|
1906
|
-
name: `Project ${environmentId}`,
|
|
1907
|
-
client_id: clientId,
|
|
1908
|
-
client_secret: clientSecretStored,
|
|
1909
|
-
type: "web",
|
|
1910
|
-
redirect_uris: JSON.stringify(redirects),
|
|
1911
|
-
grant_types: JSON.stringify(["authorization_code", "refresh_token"]),
|
|
1912
|
-
response_types: JSON.stringify(["code"]),
|
|
1913
|
-
scopes: JSON.stringify(["openid", "email", "profile"]),
|
|
1914
|
-
token_endpoint_auth_method: "client_secret_basic",
|
|
1915
|
-
require_pkce: false,
|
|
1916
|
-
skip_consent: true,
|
|
1917
|
-
disabled: false,
|
|
1918
|
-
subject_type: "public",
|
|
1919
|
-
created_at: nowIso,
|
|
1920
|
-
updated_at: nowIso
|
|
1921
|
-
}, { context: { isSystem: true } });
|
|
1922
|
-
logger?.info?.("[platform-sso] sys_oauth_application row created", { environmentId, clientId });
|
|
2043
|
+
({ MongoDBDriver } = await import("@objectstack/driver-mongodb"));
|
|
1923
2044
|
} catch (err) {
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
});
|
|
1928
|
-
if (throwOnError) throw err;
|
|
2045
|
+
throw new Error(
|
|
2046
|
+
`[StandaloneStack] mongodb URL detected but @objectstack/driver-mongodb is not installed. Add it as a dependency or pass an explicit driverPlugin. (${err?.message ?? err})`
|
|
2047
|
+
);
|
|
1929
2048
|
}
|
|
1930
|
-
|
|
2049
|
+
driverPlugin = new DriverPlugin2(new MongoDBDriver({ url: dbUrl }));
|
|
2050
|
+
} else if (dbDriver === "sqlite-wasm") {
|
|
2051
|
+
const { SqliteWasmDriver } = await import("@objectstack/driver-sqlite-wasm");
|
|
2052
|
+
const filename = dbUrl.replace(/^wasm-sqlite:(\/\/)?/i, "").replace(/^file:(\/\/)?/i, "");
|
|
2053
|
+
if (filename && filename !== ":memory:") {
|
|
2054
|
+
mkdirSync2(resolvePath2(filename, ".."), { recursive: true });
|
|
2055
|
+
}
|
|
2056
|
+
driverPlugin = new DriverPlugin2(
|
|
2057
|
+
new SqliteWasmDriver({
|
|
2058
|
+
filename: filename || ":memory:",
|
|
2059
|
+
persist: filename && filename !== ":memory:" ? "on-write" : void 0
|
|
2060
|
+
})
|
|
2061
|
+
);
|
|
2062
|
+
} else {
|
|
2063
|
+
const { SqlDriver } = await import("@objectstack/driver-sql");
|
|
2064
|
+
const filename = dbUrl.replace(/^file:(\/\/)?/, "");
|
|
2065
|
+
if (!filename || /^[a-z][a-z0-9+.-]*:\/\//i.test(filename)) {
|
|
2066
|
+
throw new Error(
|
|
2067
|
+
`[StandaloneStack] sqlite driver was selected but the URL does not look like a file path: "${dbUrl}". Use file:/path/to/db.sqlite, or set OS_DATABASE_DRIVER explicitly.`
|
|
2068
|
+
);
|
|
2069
|
+
}
|
|
2070
|
+
mkdirSync2(resolvePath2(filename, ".."), { recursive: true });
|
|
2071
|
+
driverPlugin = new DriverPlugin2(
|
|
2072
|
+
new SqlDriver({
|
|
2073
|
+
client: "better-sqlite3",
|
|
2074
|
+
connection: { filename },
|
|
2075
|
+
useNullAsDefault: true
|
|
2076
|
+
})
|
|
2077
|
+
);
|
|
1931
2078
|
}
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
2079
|
+
const artifactBundle = await loadArtifactBundle(artifactPath, {
|
|
2080
|
+
tag: "[StandaloneStack]",
|
|
2081
|
+
unwrapEnvelope: true
|
|
2082
|
+
});
|
|
2083
|
+
if (artifactBundle) {
|
|
2084
|
+
const flowsCount = Array.isArray(artifactBundle?.flows) ? artifactBundle.flows.length : "n/a";
|
|
2085
|
+
console.warn(
|
|
2086
|
+
`[StandaloneStack] artifact loaded: path=${artifactPath} keys=${Object.keys(artifactBundle).join(",")} flows=${flowsCount}`
|
|
2087
|
+
);
|
|
1938
2088
|
}
|
|
1939
|
-
const
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
try {
|
|
1956
|
-
await ql.update(
|
|
1957
|
-
"sys_oauth_application",
|
|
1958
|
-
repairPatch,
|
|
1959
|
-
{ where: { id: existing.id } },
|
|
1960
|
-
{ context: { isSystem: true } }
|
|
1961
|
-
);
|
|
1962
|
-
logger?.info?.("[platform-sso] sys_oauth_application repaired", {
|
|
1963
|
-
environmentId,
|
|
1964
|
-
clientId,
|
|
1965
|
-
redirect_uris: mergedRedirects
|
|
1966
|
-
});
|
|
1967
|
-
} catch (err) {
|
|
1968
|
-
logger?.warn?.("[platform-sso] sys_oauth_application repair failed", {
|
|
2089
|
+
const plugins = [
|
|
2090
|
+
driverPlugin,
|
|
2091
|
+
new MetadataPlugin({
|
|
2092
|
+
// Source-file scanner OFF — declarative metadata is loaded
|
|
2093
|
+
// from the compiled artifact, not from yaml/json files on
|
|
2094
|
+
// disk. Scanning would also recursively watch the project
|
|
2095
|
+
// root (incl. node_modules), which is expensive and prone
|
|
2096
|
+
// to EMFILE.
|
|
2097
|
+
watch: false,
|
|
2098
|
+
// Artifact-file HMR ON in non-production so edits to
|
|
2099
|
+
// `*.view.ts` / `*.flow.ts` (which the CLI dev-mode watcher
|
|
2100
|
+
// recompiles into `dist/objectstack.json`) are picked up by
|
|
2101
|
+
// the running server WITHOUT requiring a manual restart.
|
|
2102
|
+
// Uses polling under the hood (see plugin.ts) to avoid
|
|
2103
|
+
// `fs.watch` EMFILE on macOS / busy dev hosts.
|
|
2104
|
+
artifactWatch: process.env.NODE_ENV !== "production",
|
|
1969
2105
|
environmentId,
|
|
1970
|
-
|
|
1971
|
-
})
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
const
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
} catch (err) {
|
|
1989
|
-
logger?.warn?.("[platform-sso] backfill: sys_environment read failed", {
|
|
1990
|
-
error: err?.message
|
|
1991
|
-
});
|
|
1992
|
-
return { scanned: 0, seeded: 0, alreadyExisted: 0, failures: [{ environmentId: "<scan>", error: err?.message ?? String(err) }] };
|
|
1993
|
-
}
|
|
1994
|
-
let seeded = 0;
|
|
1995
|
-
let alreadyExisted = 0;
|
|
1996
|
-
const failures = [];
|
|
1997
|
-
for (const p of projects) {
|
|
1998
|
-
if (!p?.id) continue;
|
|
1999
|
-
const before = await (async () => {
|
|
2000
|
-
try {
|
|
2001
|
-
const r = await ql.find("sys_oauth_application", {
|
|
2002
|
-
where: { client_id: derivePlatformSsoClientId(p.id) },
|
|
2003
|
-
limit: 1
|
|
2004
|
-
}, { context: { isSystem: true } });
|
|
2005
|
-
const list = Array.isArray(r) ? r : Array.isArray(r?.records) ? r.records : [];
|
|
2006
|
-
return list[0] ?? null;
|
|
2007
|
-
} catch {
|
|
2008
|
-
return null;
|
|
2009
|
-
}
|
|
2010
|
-
})();
|
|
2011
|
-
try {
|
|
2012
|
-
await seedPlatformSsoClient({ ql, environmentId: p.id, hostname: p.hostname, baseSecret, logger, throwOnError: true });
|
|
2013
|
-
if (before) alreadyExisted++;
|
|
2014
|
-
else {
|
|
2015
|
-
const after = await (async () => {
|
|
2016
|
-
try {
|
|
2017
|
-
const r = await ql.find("sys_oauth_application", {
|
|
2018
|
-
where: { client_id: derivePlatformSsoClientId(p.id) },
|
|
2019
|
-
limit: 1
|
|
2020
|
-
}, { context: { isSystem: true } });
|
|
2021
|
-
const list = Array.isArray(r) ? r : Array.isArray(r?.records) ? r.records : [];
|
|
2022
|
-
return list[0] ?? null;
|
|
2023
|
-
} catch (err) {
|
|
2024
|
-
return { _readErr: err?.message };
|
|
2025
|
-
}
|
|
2026
|
-
})();
|
|
2027
|
-
if (after && !after._readErr) seeded++;
|
|
2028
|
-
else failures.push({ environmentId: p.id, error: `post-insert read returned ${after ? JSON.stringify(after) : "null"}` });
|
|
2029
|
-
}
|
|
2030
|
-
} catch (err) {
|
|
2031
|
-
failures.push({ environmentId: p.id, error: err?.message ?? String(err) });
|
|
2032
|
-
}
|
|
2033
|
-
}
|
|
2034
|
-
logger?.info?.("[platform-sso] backfill complete", { scanned: projects.length, seeded, alreadyExisted, failures: failures.length });
|
|
2035
|
-
return { scanned: projects.length, seeded, alreadyExisted, failures };
|
|
2106
|
+
artifactSource: { mode: "local-file", path: artifactPath }
|
|
2107
|
+
}),
|
|
2108
|
+
new ObjectQLPlugin({ environmentId })
|
|
2109
|
+
];
|
|
2110
|
+
if (artifactBundle) plugins.push(new AppPlugin2(artifactBundle));
|
|
2111
|
+
const requires = Array.isArray(artifactBundle?.requires) ? artifactBundle.requires.filter((c) => typeof c === "string") : void 0;
|
|
2112
|
+
const objects = Array.isArray(artifactBundle?.objects) ? artifactBundle.objects : void 0;
|
|
2113
|
+
const manifest = artifactBundle?.manifest;
|
|
2114
|
+
return {
|
|
2115
|
+
plugins,
|
|
2116
|
+
api: {
|
|
2117
|
+
enableProjectScoping: false,
|
|
2118
|
+
projectResolution: "none"
|
|
2119
|
+
},
|
|
2120
|
+
...requires ? { requires } : {},
|
|
2121
|
+
...objects ? { objects } : {},
|
|
2122
|
+
...manifest ? { manifest } : {}
|
|
2123
|
+
};
|
|
2036
2124
|
}
|
|
2037
|
-
var
|
|
2038
|
-
var
|
|
2039
|
-
"src/
|
|
2125
|
+
var StandaloneStackConfigSchema;
|
|
2126
|
+
var init_standalone_stack = __esm({
|
|
2127
|
+
"src/standalone-stack.ts"() {
|
|
2040
2128
|
"use strict";
|
|
2041
|
-
|
|
2129
|
+
init_load_artifact_bundle();
|
|
2130
|
+
StandaloneStackConfigSchema = z.object({
|
|
2131
|
+
databaseUrl: z.string().optional(),
|
|
2132
|
+
databaseAuthToken: z.string().optional(),
|
|
2133
|
+
databaseDriver: z.enum(["sqlite", "sqlite-wasm", "memory", "postgres", "mongodb"]).optional(),
|
|
2134
|
+
environmentId: z.string().optional(),
|
|
2135
|
+
artifactPath: z.string().optional(),
|
|
2136
|
+
/**
|
|
2137
|
+
* Project root directory. When set (typically by the CLI after locating
|
|
2138
|
+
* `objectstack.config.ts`), the default sqlite database is placed under
|
|
2139
|
+
* `<projectRoot>/.objectstack/data/standalone.db` instead of the global
|
|
2140
|
+
* `~/.objectstack/data/standalone.db`. This keeps per-project data
|
|
2141
|
+
* scoped to the project folder so different examples / apps don't
|
|
2142
|
+
* share a single database by accident.
|
|
2143
|
+
*
|
|
2144
|
+
* Explicit `databaseUrl` / `OS_DATABASE_URL` / `OS_HOME` still take
|
|
2145
|
+
* precedence over this default.
|
|
2146
|
+
*/
|
|
2147
|
+
projectRoot: z.string().optional()
|
|
2148
|
+
});
|
|
2042
2149
|
}
|
|
2043
2150
|
});
|
|
2044
2151
|
|
|
@@ -2240,228 +2347,236 @@ var Runtime = class {
|
|
|
2240
2347
|
}
|
|
2241
2348
|
};
|
|
2242
2349
|
|
|
2243
|
-
// src/
|
|
2350
|
+
// src/index.ts
|
|
2351
|
+
init_standalone_stack();
|
|
2352
|
+
|
|
2353
|
+
// src/default-host.ts
|
|
2354
|
+
init_standalone_stack();
|
|
2244
2355
|
init_load_artifact_bundle();
|
|
2245
|
-
import { resolve as
|
|
2246
|
-
import { mkdirSync } from "fs";
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2356
|
+
import { resolve as resolvePath3 } from "path";
|
|
2357
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
2358
|
+
function resolveDefaultArtifactPath(explicitPath, cwd = process.cwd()) {
|
|
2359
|
+
const candidate = explicitPath ?? process.env.OS_ARTIFACT_PATH ?? resolvePath3(cwd, "dist/objectstack.json");
|
|
2360
|
+
if (isHttpUrl(candidate)) return candidate;
|
|
2361
|
+
if (explicitPath || process.env.OS_ARTIFACT_PATH) return candidate;
|
|
2362
|
+
return existsSync2(candidate) ? candidate : void 0;
|
|
2363
|
+
}
|
|
2364
|
+
async function createDefaultHostConfig(options = {}) {
|
|
2365
|
+
const { requireArtifact = true, ...standaloneOpts } = options;
|
|
2366
|
+
let resolvedArtifact = resolveDefaultArtifactPath(standaloneOpts.artifactPath);
|
|
2367
|
+
if (!resolvedArtifact && requireArtifact) {
|
|
2368
|
+
throw new Error(
|
|
2369
|
+
"[createDefaultHostConfig] No artifact source available. Set OS_ARTIFACT_PATH (file path or http(s):// URL), place the artifact at <cwd>/dist/objectstack.json, or pass `{ artifactPath: ... }` explicitly. To boot an empty kernel anyway, pass `{ requireArtifact: false }`."
|
|
2370
|
+
);
|
|
2255
2371
|
}
|
|
2256
|
-
|
|
2372
|
+
if (!resolvedArtifact && !requireArtifact) {
|
|
2373
|
+
const home = resolveObjectStackHome();
|
|
2374
|
+
const stubPath = resolvePath3(home, "dist/objectstack.json");
|
|
2375
|
+
if (!existsSync2(stubPath)) {
|
|
2376
|
+
mkdirSync3(resolvePath3(stubPath, ".."), { recursive: true });
|
|
2377
|
+
writeFileSync2(
|
|
2378
|
+
stubPath,
|
|
2379
|
+
JSON.stringify(
|
|
2380
|
+
{
|
|
2381
|
+
manifest: {
|
|
2382
|
+
id: "com.objectstack.empty",
|
|
2383
|
+
name: "empty",
|
|
2384
|
+
version: "0.0.0",
|
|
2385
|
+
type: "app",
|
|
2386
|
+
description: "Empty starter kernel \u2014 install apps via the Studio marketplace."
|
|
2387
|
+
},
|
|
2388
|
+
objects: [],
|
|
2389
|
+
views: [],
|
|
2390
|
+
apps: [],
|
|
2391
|
+
flows: [],
|
|
2392
|
+
requires: []
|
|
2393
|
+
},
|
|
2394
|
+
null,
|
|
2395
|
+
2
|
|
2396
|
+
),
|
|
2397
|
+
"utf8"
|
|
2398
|
+
);
|
|
2399
|
+
}
|
|
2400
|
+
resolvedArtifact = stubPath;
|
|
2401
|
+
}
|
|
2402
|
+
return createStandaloneStack({
|
|
2403
|
+
...standaloneOpts,
|
|
2404
|
+
artifactPath: resolvedArtifact
|
|
2405
|
+
});
|
|
2257
2406
|
}
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2407
|
+
|
|
2408
|
+
// src/index.ts
|
|
2409
|
+
init_driver_plugin();
|
|
2410
|
+
init_app_plugin();
|
|
2411
|
+
init_seed_loader();
|
|
2412
|
+
|
|
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
|
+
}
|
|
2264
2474
|
/**
|
|
2265
|
-
*
|
|
2266
|
-
* `
|
|
2267
|
-
*
|
|
2268
|
-
*
|
|
2269
|
-
* scoped to the project folder so different examples / apps don't
|
|
2270
|
-
* share a single database by accident.
|
|
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.
|
|
2271
2479
|
*
|
|
2272
|
-
*
|
|
2273
|
-
*
|
|
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.
|
|
2274
2483
|
*/
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
if (/^mongodb(\+srv)?:\/\//i.test(dbUrl)) return "mongodb";
|
|
2281
|
-
if (/^wasm-sqlite:\/\//i.test(dbUrl)) return "sqlite-wasm";
|
|
2282
|
-
if (/\.wasm\.db$/i.test(dbUrl)) return "sqlite-wasm";
|
|
2283
|
-
if (/^file:/i.test(dbUrl)) return "sqlite";
|
|
2284
|
-
if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(dbUrl)) return "sqlite";
|
|
2285
|
-
throw new Error(
|
|
2286
|
-
`[StandaloneStack] Unsupported database URL scheme: ${dbUrl}. Supported schemes: memory://, postgres://, pg://, mongodb://, mongodb+srv://, file:`
|
|
2287
|
-
);
|
|
2288
|
-
}
|
|
2289
|
-
async function createStandaloneStack(config) {
|
|
2290
|
-
const cfg = StandaloneStackConfigSchema.parse(config ?? {});
|
|
2291
|
-
const { ObjectQLPlugin } = await import("@objectstack/objectql");
|
|
2292
|
-
const { MetadataPlugin } = await import("@objectstack/metadata");
|
|
2293
|
-
const { DriverPlugin: DriverPlugin2 } = await Promise.resolve().then(() => (init_driver_plugin(), driver_plugin_exports));
|
|
2294
|
-
const { AppPlugin: AppPlugin2 } = await Promise.resolve().then(() => (init_app_plugin(), app_plugin_exports));
|
|
2295
|
-
const cwd = process.cwd();
|
|
2296
|
-
const environmentId = cfg.environmentId ?? process.env.OS_ENVIRONMENT_ID ?? "proj_local";
|
|
2297
|
-
const artifactPathInput = cfg.artifactPath ?? process.env.OS_ARTIFACT_PATH ?? resolvePath2(cwd, "dist/objectstack.json");
|
|
2298
|
-
const artifactPath = isHttpUrl(artifactPathInput) ? artifactPathInput : artifactPathInput.startsWith("/") ? artifactPathInput : resolvePath2(cwd, artifactPathInput);
|
|
2299
|
-
const dbUrl = cfg.databaseUrl ?? readEnvWithDeprecation2("OS_DATABASE_URL", "DATABASE_URL")?.trim() ?? process.env.TURSO_DATABASE_URL?.trim() ?? (process.env.OS_HOME?.trim() ? `file:${resolvePath2(resolveObjectStackHome(), "data/standalone.db")}` : cfg.projectRoot ? `file:${resolvePath2(cfg.projectRoot, ".objectstack/data/standalone.db")}` : `file:${resolvePath2(resolveObjectStackHome(), "data/standalone.db")}`);
|
|
2300
|
-
const explicitDriver = cfg.databaseDriver ?? process.env.OS_DATABASE_DRIVER?.trim();
|
|
2301
|
-
const dbDriver = explicitDriver ?? detectDriverFromUrl(dbUrl);
|
|
2302
|
-
let driverPlugin;
|
|
2303
|
-
if (dbDriver === "memory") {
|
|
2304
|
-
const { InMemoryDriver } = await import("@objectstack/driver-memory");
|
|
2305
|
-
driverPlugin = new DriverPlugin2(new InMemoryDriver());
|
|
2306
|
-
} else if (dbDriver === "postgres") {
|
|
2307
|
-
const { SqlDriver } = await import("@objectstack/driver-sql");
|
|
2308
|
-
driverPlugin = new DriverPlugin2(
|
|
2309
|
-
new SqlDriver({
|
|
2310
|
-
client: "pg",
|
|
2311
|
-
connection: dbUrl,
|
|
2312
|
-
pool: { min: 0, max: 5 }
|
|
2313
|
-
})
|
|
2314
|
-
);
|
|
2315
|
-
} else if (dbDriver === "mongodb") {
|
|
2316
|
-
let MongoDBDriver;
|
|
2484
|
+
async scheduleDriftChecks(ctx) {
|
|
2485
|
+
this.stop();
|
|
2486
|
+
const metadata = safeGet(ctx, "metadata");
|
|
2487
|
+
if (!metadata?.list) return;
|
|
2488
|
+
let datasources;
|
|
2317
2489
|
try {
|
|
2318
|
-
|
|
2490
|
+
datasources = await metadata.list("datasource");
|
|
2319
2491
|
} catch (err) {
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
);
|
|
2323
|
-
}
|
|
2324
|
-
driverPlugin = new DriverPlugin2(new MongoDBDriver({ url: dbUrl }));
|
|
2325
|
-
} else if (dbDriver === "sqlite-wasm") {
|
|
2326
|
-
const { SqliteWasmDriver } = await import("@objectstack/driver-sqlite-wasm");
|
|
2327
|
-
const filename = dbUrl.replace(/^wasm-sqlite:(\/\/)?/i, "").replace(/^file:(\/\/)?/i, "");
|
|
2328
|
-
if (filename && filename !== ":memory:") {
|
|
2329
|
-
mkdirSync(resolvePath2(filename, ".."), { recursive: true });
|
|
2492
|
+
ctx.logger?.warn?.("[external-validation] could not list datasources for drift checks", { err });
|
|
2493
|
+
return;
|
|
2330
2494
|
}
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
);
|
|
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
|
+
});
|
|
2344
2508
|
}
|
|
2345
|
-
mkdirSync(resolvePath2(filename, ".."), { recursive: true });
|
|
2346
|
-
driverPlugin = new DriverPlugin2(
|
|
2347
|
-
new SqlDriver({
|
|
2348
|
-
client: "better-sqlite3",
|
|
2349
|
-
connection: { filename },
|
|
2350
|
-
useNullAsDefault: true
|
|
2351
|
-
})
|
|
2352
|
-
);
|
|
2353
2509
|
}
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
);
|
|
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;
|
|
2363
2554
|
}
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
// Source-file scanner OFF — declarative metadata is loaded
|
|
2368
|
-
// from the compiled artifact, not from yaml/json files on
|
|
2369
|
-
// disk. Scanning would also recursively watch the project
|
|
2370
|
-
// root (incl. node_modules), which is expensive and prone
|
|
2371
|
-
// to EMFILE.
|
|
2372
|
-
watch: false,
|
|
2373
|
-
// Artifact-file HMR ON in non-production so edits to
|
|
2374
|
-
// `*.view.ts` / `*.flow.ts` (which the CLI dev-mode watcher
|
|
2375
|
-
// recompiles into `dist/objectstack.json`) are picked up by
|
|
2376
|
-
// the running server WITHOUT requiring a manual restart.
|
|
2377
|
-
// Uses polling under the hood (see plugin.ts) to avoid
|
|
2378
|
-
// `fs.watch` EMFILE on macOS / busy dev hosts.
|
|
2379
|
-
artifactWatch: process.env.NODE_ENV !== "production",
|
|
2380
|
-
environmentId,
|
|
2381
|
-
artifactSource: { mode: "local-file", path: artifactPath }
|
|
2382
|
-
}),
|
|
2383
|
-
new ObjectQLPlugin({ environmentId })
|
|
2384
|
-
];
|
|
2385
|
-
if (artifactBundle) plugins.push(new AppPlugin2(artifactBundle));
|
|
2386
|
-
const requires = Array.isArray(artifactBundle?.requires) ? artifactBundle.requires.filter((c) => typeof c === "string") : void 0;
|
|
2387
|
-
const objects = Array.isArray(artifactBundle?.objects) ? artifactBundle.objects : void 0;
|
|
2388
|
-
const manifest = artifactBundle?.manifest;
|
|
2389
|
-
return {
|
|
2390
|
-
plugins,
|
|
2391
|
-
api: {
|
|
2392
|
-
enableProjectScoping: false,
|
|
2393
|
-
projectResolution: "none"
|
|
2394
|
-
},
|
|
2395
|
-
...requires ? { requires } : {},
|
|
2396
|
-
...objects ? { objects } : {},
|
|
2397
|
-
...manifest ? { manifest } : {}
|
|
2398
|
-
};
|
|
2399
|
-
}
|
|
2400
|
-
|
|
2401
|
-
// src/default-host.ts
|
|
2402
|
-
import { resolve as resolvePath3 } from "path";
|
|
2403
|
-
import { existsSync, mkdirSync as mkdirSync2, writeFileSync } from "fs";
|
|
2404
|
-
init_load_artifact_bundle();
|
|
2405
|
-
function resolveDefaultArtifactPath(explicitPath, cwd = process.cwd()) {
|
|
2406
|
-
const candidate = explicitPath ?? process.env.OS_ARTIFACT_PATH ?? resolvePath3(cwd, "dist/objectstack.json");
|
|
2407
|
-
if (isHttpUrl(candidate)) return candidate;
|
|
2408
|
-
if (explicitPath || process.env.OS_ARTIFACT_PATH) return candidate;
|
|
2409
|
-
return existsSync(candidate) ? candidate : void 0;
|
|
2555
|
+
};
|
|
2556
|
+
function createExternalValidationPlugin() {
|
|
2557
|
+
return new ExternalValidationPlugin();
|
|
2410
2558
|
}
|
|
2411
|
-
async function
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
);
|
|
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";
|
|
2418
2565
|
}
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
stubPath,
|
|
2426
|
-
JSON.stringify(
|
|
2427
|
-
{
|
|
2428
|
-
manifest: {
|
|
2429
|
-
id: "com.objectstack.empty",
|
|
2430
|
-
name: "empty",
|
|
2431
|
-
version: "0.0.0",
|
|
2432
|
-
type: "app",
|
|
2433
|
-
description: "Empty starter kernel \u2014 install apps via the Studio marketplace."
|
|
2434
|
-
},
|
|
2435
|
-
objects: [],
|
|
2436
|
-
views: [],
|
|
2437
|
-
apps: [],
|
|
2438
|
-
flows: [],
|
|
2439
|
-
requires: []
|
|
2440
|
-
},
|
|
2441
|
-
null,
|
|
2442
|
-
2
|
|
2443
|
-
),
|
|
2444
|
-
"utf8"
|
|
2445
|
-
);
|
|
2446
|
-
}
|
|
2447
|
-
resolvedArtifact = stubPath;
|
|
2566
|
+
}
|
|
2567
|
+
function safeGet(ctx, name) {
|
|
2568
|
+
try {
|
|
2569
|
+
return ctx.getService(name);
|
|
2570
|
+
} catch {
|
|
2571
|
+
return void 0;
|
|
2448
2572
|
}
|
|
2449
|
-
return createStandaloneStack({
|
|
2450
|
-
...standaloneOpts,
|
|
2451
|
-
artifactPath: resolvedArtifact
|
|
2452
|
-
});
|
|
2453
2573
|
}
|
|
2454
2574
|
|
|
2455
|
-
// src/index.ts
|
|
2456
|
-
init_driver_plugin();
|
|
2457
|
-
init_app_plugin();
|
|
2458
|
-
init_seed_loader();
|
|
2459
|
-
|
|
2460
2575
|
// src/http-dispatcher.ts
|
|
2576
|
+
init_package_state_store();
|
|
2461
2577
|
import { getEnv, resolveLocale } from "@objectstack/core";
|
|
2462
|
-
import { readEnvWithDeprecation as readEnvWithDeprecation3 } from "@objectstack/types";
|
|
2463
2578
|
import { CoreServiceName } from "@objectstack/spec/system";
|
|
2464
|
-
import { pluralToSingular } from "@objectstack/spec/shared";
|
|
2579
|
+
import { pluralToSingular, PLURAL_TO_SINGULAR } from "@objectstack/spec/shared";
|
|
2465
2580
|
|
|
2466
2581
|
// src/security/resolve-execution-context.ts
|
|
2467
2582
|
function readHeader(headers, name) {
|
|
@@ -3288,6 +3403,17 @@ var _HttpDispatcher = class _HttpDispatcher {
|
|
|
3288
3403
|
}
|
|
3289
3404
|
return { handled: true, response: this.success({ types: ["object", "app", "plugin"] }) };
|
|
3290
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
|
+
}
|
|
3291
3417
|
if (parts.length >= 3 && parts[parts.length - 1] === "published" && (!method || method === "GET")) {
|
|
3292
3418
|
const type = parts[0];
|
|
3293
3419
|
const name = parts.slice(1, -1).join("/");
|
|
@@ -3316,7 +3442,7 @@ var _HttpDispatcher = class _HttpDispatcher {
|
|
|
3316
3442
|
if (protocol && typeof protocol.saveMetaItem === "function") {
|
|
3317
3443
|
try {
|
|
3318
3444
|
const organizationId = await this.resolveActiveOrganizationId(_context);
|
|
3319
|
-
const result = await protocol.saveMetaItem({ type, name, item: body, organizationId });
|
|
3445
|
+
const result = await protocol.saveMetaItem({ type, name, item: body, organizationId, ...packageId ? { packageId } : {} });
|
|
3320
3446
|
return { handled: true, response: this.success(result) };
|
|
3321
3447
|
} catch (e) {
|
|
3322
3448
|
return { handled: true, response: this.error(e.message, 400) };
|
|
@@ -3656,12 +3782,22 @@ var _HttpDispatcher = class _HttpDispatcher {
|
|
|
3656
3782
|
const id = decodeURIComponent(parts[0]);
|
|
3657
3783
|
const pkg = registry.enablePackage(id);
|
|
3658
3784
|
if (!pkg) return { handled: true, response: this.error(`Package '${id}' not found`, 404) };
|
|
3785
|
+
try {
|
|
3786
|
+
setPackageDisabled(_context?.environmentId, id, false);
|
|
3787
|
+
} catch (err) {
|
|
3788
|
+
console.warn("[handlePackages] failed to persist enable state", { id, error: err?.message });
|
|
3789
|
+
}
|
|
3659
3790
|
return { handled: true, response: this.success(pkg) };
|
|
3660
3791
|
}
|
|
3661
3792
|
if (parts.length === 2 && parts[1] === "disable" && m === "PATCH") {
|
|
3662
3793
|
const id = decodeURIComponent(parts[0]);
|
|
3663
3794
|
const pkg = registry.disablePackage(id);
|
|
3664
3795
|
if (!pkg) return { handled: true, response: this.error(`Package '${id}' not found`, 404) };
|
|
3796
|
+
try {
|
|
3797
|
+
setPackageDisabled(_context?.environmentId, id, true);
|
|
3798
|
+
} catch (err) {
|
|
3799
|
+
console.warn("[handlePackages] failed to persist disable state", { id, error: err?.message });
|
|
3800
|
+
}
|
|
3665
3801
|
return { handled: true, response: this.success(pkg) };
|
|
3666
3802
|
}
|
|
3667
3803
|
if (parts.length === 2 && parts[1] === "publish" && m === "POST") {
|
|
@@ -3682,6 +3818,14 @@ var _HttpDispatcher = class _HttpDispatcher {
|
|
|
3682
3818
|
}
|
|
3683
3819
|
return { handled: true, response: this.error("Metadata service not available", 503) };
|
|
3684
3820
|
}
|
|
3821
|
+
if (parts.length === 2 && parts[1] === "export" && m === "GET") {
|
|
3822
|
+
const id = decodeURIComponent(parts[0]);
|
|
3823
|
+
const manifest = await this.assemblePackageManifest(id, registry, _context);
|
|
3824
|
+
if (!manifest) {
|
|
3825
|
+
return { handled: true, response: this.error(`Package '${id}' not found`, 404) };
|
|
3826
|
+
}
|
|
3827
|
+
return { handled: true, response: this.success(manifest) };
|
|
3828
|
+
}
|
|
3685
3829
|
if (parts.length === 1 && m === "GET") {
|
|
3686
3830
|
const id = decodeURIComponent(parts[0]);
|
|
3687
3831
|
const pkg = registry.getPackage(id);
|
|
@@ -3699,6 +3843,83 @@ var _HttpDispatcher = class _HttpDispatcher {
|
|
|
3699
3843
|
}
|
|
3700
3844
|
return { handled: false };
|
|
3701
3845
|
}
|
|
3846
|
+
/**
|
|
3847
|
+
* Assemble a portable, offline-installable package manifest from the
|
|
3848
|
+
* `sys_metadata` overlay rows bound to `packageId`.
|
|
3849
|
+
*
|
|
3850
|
+
* The resulting shape mirrors what `marketplace-install-local` →
|
|
3851
|
+
* `manifestService.register()` → `engine.registerApp()` consumes:
|
|
3852
|
+
* `{ id, name, version, objects:[…], views:[…], flows:[…], … }`
|
|
3853
|
+
* where each category key is the PLURAL manifest name and its value is
|
|
3854
|
+
* an array of clean metadata bodies (provenance decorations stripped).
|
|
3855
|
+
*
|
|
3856
|
+
* Only the metadata categories that `registerApp` can actually consume
|
|
3857
|
+
* are exported. `datasources` and `emailTemplates` are intentionally
|
|
3858
|
+
* excluded (not registered by the import path). `tools` / `skills` ARE
|
|
3859
|
+
* round-tripped: they are registered by `registerApp` on import and
|
|
3860
|
+
* surfaced by `getMetaItems('tool' | 'skill')` on export.
|
|
3861
|
+
*
|
|
3862
|
+
* @returns the manifest object, or `null` if the package id is unknown
|
|
3863
|
+
* AND has no overlay-authored metadata.
|
|
3864
|
+
*/
|
|
3865
|
+
async assemblePackageManifest(packageId, registry, context) {
|
|
3866
|
+
const protocol = await this.resolveService("protocol");
|
|
3867
|
+
if (!protocol || typeof protocol.getMetaItems !== "function") return null;
|
|
3868
|
+
const organizationId = await this.resolveActiveOrganizationId(context);
|
|
3869
|
+
const PROVENANCE_KEYS = /* @__PURE__ */ new Set([
|
|
3870
|
+
"_packageId",
|
|
3871
|
+
"_packageVersionId",
|
|
3872
|
+
"_provenance",
|
|
3873
|
+
"_state",
|
|
3874
|
+
"_version",
|
|
3875
|
+
"_organizationId",
|
|
3876
|
+
"_source",
|
|
3877
|
+
"_id",
|
|
3878
|
+
"_rowId"
|
|
3879
|
+
]);
|
|
3880
|
+
const clean = (item) => {
|
|
3881
|
+
if (!item || typeof item !== "object") return item;
|
|
3882
|
+
const out = {};
|
|
3883
|
+
for (const [k, v] of Object.entries(item)) {
|
|
3884
|
+
if (k.startsWith("_") || PROVENANCE_KEYS.has(k)) continue;
|
|
3885
|
+
out[k] = v;
|
|
3886
|
+
}
|
|
3887
|
+
return out;
|
|
3888
|
+
};
|
|
3889
|
+
const exportPluralKeys = Object.keys(PLURAL_TO_SINGULAR).filter(
|
|
3890
|
+
(k) => k !== "datasources" && k !== "emailTemplates"
|
|
3891
|
+
);
|
|
3892
|
+
const manifest = {};
|
|
3893
|
+
let total = 0;
|
|
3894
|
+
for (const plural of exportPluralKeys) {
|
|
3895
|
+
const singular = PLURAL_TO_SINGULAR[plural];
|
|
3896
|
+
let items = [];
|
|
3897
|
+
try {
|
|
3898
|
+
const res = await protocol.getMetaItems({ type: singular, packageId, organizationId });
|
|
3899
|
+
items = Array.isArray(res?.items) ? res.items : [];
|
|
3900
|
+
} catch {
|
|
3901
|
+
continue;
|
|
3902
|
+
}
|
|
3903
|
+
if (items.length === 0) continue;
|
|
3904
|
+
manifest[plural] = items.map(clean);
|
|
3905
|
+
total += items.length;
|
|
3906
|
+
}
|
|
3907
|
+
const pkg = (() => {
|
|
3908
|
+
try {
|
|
3909
|
+
return registry?.getPackage?.(packageId);
|
|
3910
|
+
} catch {
|
|
3911
|
+
return void 0;
|
|
3912
|
+
}
|
|
3913
|
+
})();
|
|
3914
|
+
if (total === 0 && !pkg) return null;
|
|
3915
|
+
manifest.id = packageId;
|
|
3916
|
+
manifest.name = pkg?.manifest?.name ?? pkg?.name ?? packageId;
|
|
3917
|
+
manifest.version = pkg?.manifest?.version ?? pkg?.version ?? "1.0.0";
|
|
3918
|
+
if (pkg?.manifest?.label ?? pkg?.label) {
|
|
3919
|
+
manifest.label = pkg?.manifest?.label ?? pkg?.label;
|
|
3920
|
+
}
|
|
3921
|
+
return manifest;
|
|
3922
|
+
}
|
|
3702
3923
|
/**
|
|
3703
3924
|
* Cloud / Environment Control-Plane routes.
|
|
3704
3925
|
*
|
|
@@ -3712,1350 +3933,59 @@ var _HttpDispatcher = class _HttpDispatcher {
|
|
|
3712
3933
|
* - POST /cloud/environments/:id/retry → re-run provisioning for a failed environment
|
|
3713
3934
|
* - POST /cloud/environments/:id/activate → mark as active for session (stub)
|
|
3714
3935
|
* - POST /cloud/environments/:id/credentials/rotate → rotate credential
|
|
3715
|
-
* - GET /cloud/environments/:id/members → list members
|
|
3716
|
-
* - GET /cloud/environments/:id/packages → list installed packages
|
|
3717
|
-
* - POST /cloud/environments/:id/packages → install package into env
|
|
3718
|
-
* - GET /cloud/environments/:id/packages/:pkgId → get installation detail
|
|
3719
|
-
* - PATCH /cloud/environments/:id/packages/:pkgId/enable → enable package
|
|
3720
|
-
* - PATCH /cloud/environments/:id/packages/:pkgId/disable → disable package
|
|
3721
|
-
* - DELETE /cloud/environments/:id/packages/:pkgId → uninstall (scope=platform forbidden)
|
|
3722
|
-
* - POST /cloud/environments/:id/packages/:pkgId/upgrade → upgrade to newer version
|
|
3723
|
-
*
|
|
3724
|
-
* Driver binding
|
|
3725
|
-
* --------------
|
|
3726
|
-
* Environments are not tied to any specific driver. At provisioning time the
|
|
3727
|
-
* caller passes `driver` (a short name such as `memory`, `turso`, or any
|
|
3728
|
-
* future `sql` / `postgres` driver). The dispatcher validates the name
|
|
3729
|
-
* against the kernel's registered driver services (`driver.<name>`) and
|
|
3730
|
-
* derives an appropriate placeholder `database_url` for the chosen driver.
|
|
3731
|
-
* If `driver` is omitted, the dispatcher auto-selects the first available
|
|
3732
|
-
* in preference order: turso → memory → any other registered driver.
|
|
3733
|
-
*
|
|
3734
|
-
* Backed by ObjectQL sys_environment / sys_environment_credential /
|
|
3735
|
-
* sys_environment_member tables (registered by
|
|
3736
|
-
* `@objectstack/service-tenant`'s `createTenantPlugin`).
|
|
3737
|
-
* Physical database addressing (database_url, database_driver, etc.)
|
|
3738
|
-
* is stored directly on the sys_environment row.
|
|
3739
|
-
*/
|
|
3740
|
-
/**
|
|
3741
|
-
* Resolve the calling user id from the request session, if any.
|
|
3742
|
-
* Returns `undefined` for anonymous calls or when auth is not wired up.
|
|
3743
|
-
*/
|
|
3744
|
-
async resolveActiveOrganizationId(context) {
|
|
3745
|
-
try {
|
|
3746
|
-
const authService = await this.resolveService(CoreServiceName.enum.auth);
|
|
3747
|
-
const rawHeaders = context.request?.headers;
|
|
3748
|
-
let headers = rawHeaders;
|
|
3749
|
-
if (rawHeaders && typeof rawHeaders === "object" && typeof rawHeaders.get !== "function") {
|
|
3750
|
-
try {
|
|
3751
|
-
const h = new Headers();
|
|
3752
|
-
for (const [k, v] of Object.entries(rawHeaders)) {
|
|
3753
|
-
if (v == null) continue;
|
|
3754
|
-
h.set(k, Array.isArray(v) ? v.join(", ") : String(v));
|
|
3755
|
-
}
|
|
3756
|
-
headers = h;
|
|
3757
|
-
} catch {
|
|
3758
|
-
headers = rawHeaders;
|
|
3759
|
-
}
|
|
3760
|
-
}
|
|
3761
|
-
const apiObj = authService?.auth?.api ?? authService?.api;
|
|
3762
|
-
const sessionData = await apiObj?.getSession?.call(apiObj, { headers });
|
|
3763
|
-
const oid = sessionData?.session?.activeOrganizationId;
|
|
3764
|
-
return typeof oid === "string" && oid.length > 0 ? oid : void 0;
|
|
3765
|
-
} catch {
|
|
3766
|
-
return void 0;
|
|
3767
|
-
}
|
|
3768
|
-
}
|
|
3769
|
-
async resolveCallerUserId(context) {
|
|
3770
|
-
try {
|
|
3771
|
-
const authService = await this.resolveService(CoreServiceName.enum.auth);
|
|
3772
|
-
const rawHeaders = context.request?.headers;
|
|
3773
|
-
let headers = rawHeaders;
|
|
3774
|
-
if (rawHeaders && typeof rawHeaders === "object" && typeof rawHeaders.get !== "function") {
|
|
3775
|
-
try {
|
|
3776
|
-
const h = new Headers();
|
|
3777
|
-
for (const [k, v] of Object.entries(rawHeaders)) {
|
|
3778
|
-
if (v == null) continue;
|
|
3779
|
-
h.set(k, Array.isArray(v) ? v.join(", ") : String(v));
|
|
3780
|
-
}
|
|
3781
|
-
headers = h;
|
|
3782
|
-
} catch {
|
|
3783
|
-
headers = rawHeaders;
|
|
3784
|
-
}
|
|
3785
|
-
}
|
|
3786
|
-
const sessionData = await (authService?.auth?.api?.getSession ?? authService?.api?.getSession)?.call(
|
|
3787
|
-
authService?.auth?.api ?? authService?.api,
|
|
3788
|
-
{ headers }
|
|
3789
|
-
);
|
|
3790
|
-
return sessionData?.user?.id ?? sessionData?.session?.userId;
|
|
3791
|
-
} catch (e) {
|
|
3792
|
-
return void 0;
|
|
3793
|
-
}
|
|
3794
|
-
}
|
|
3795
|
-
async handleCloud(path, method, body, query, _context) {
|
|
3796
|
-
const m = method.toUpperCase();
|
|
3797
|
-
const parts = path.replace(/^\/+/, "").split("/").filter(Boolean);
|
|
3798
|
-
const qlService = await this.getObjectQLService();
|
|
3799
|
-
const ql = qlService ?? await this.resolveService("objectql");
|
|
3800
|
-
if (!ql) {
|
|
3801
|
-
return { handled: true, response: this.error("Project service not available (ObjectQL missing)", 503) };
|
|
3802
|
-
}
|
|
3803
|
-
const ENV = "sys_environment";
|
|
3804
|
-
const CRED = "sys_environment_credential";
|
|
3805
|
-
const MEM = "sys_environment_member";
|
|
3806
|
-
const PKG_INSTALL = "sys_package_installation";
|
|
3807
|
-
const PKG = "sys_package";
|
|
3808
|
-
const PKG_VERSION = "sys_package_version";
|
|
3809
|
-
const ensureSysPackage = async (manifestId, ownerOrgId, createdBy, manifest) => {
|
|
3810
|
-
const existing = await ql.findOne(PKG, { where: { manifest_id: manifestId } });
|
|
3811
|
-
if (existing?.id) return existing.id;
|
|
3812
|
-
const id = randomUUID();
|
|
3813
|
-
const nowIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
3814
|
-
await ql.insert(PKG, {
|
|
3815
|
-
id,
|
|
3816
|
-
manifest_id: manifestId,
|
|
3817
|
-
owner_org_id: ownerOrgId,
|
|
3818
|
-
display_name: manifest?.name ?? manifestId,
|
|
3819
|
-
description: manifest?.description ?? null,
|
|
3820
|
-
visibility: "private",
|
|
3821
|
-
created_by: createdBy,
|
|
3822
|
-
created_at: nowIso,
|
|
3823
|
-
updated_at: nowIso
|
|
3824
|
-
});
|
|
3825
|
-
return id;
|
|
3826
|
-
};
|
|
3827
|
-
const ensureSysPackageVersion = async (packageId, version, createdBy, manifest) => {
|
|
3828
|
-
const existing = await ql.findOne(PKG_VERSION, {
|
|
3829
|
-
where: { package_id: packageId, version }
|
|
3830
|
-
});
|
|
3831
|
-
if (existing?.id) return existing.id;
|
|
3832
|
-
const id = randomUUID();
|
|
3833
|
-
const nowIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
3834
|
-
await ql.insert(PKG_VERSION, {
|
|
3835
|
-
id,
|
|
3836
|
-
package_id: packageId,
|
|
3837
|
-
version,
|
|
3838
|
-
status: "published",
|
|
3839
|
-
manifest_json: manifest ? JSON.stringify(manifest) : null,
|
|
3840
|
-
is_pre_release: false,
|
|
3841
|
-
published_at: nowIso,
|
|
3842
|
-
published_by: createdBy,
|
|
3843
|
-
created_by: createdBy,
|
|
3844
|
-
created_at: nowIso,
|
|
3845
|
-
updated_at: nowIso
|
|
3846
|
-
});
|
|
3847
|
-
return id;
|
|
3848
|
-
};
|
|
3849
|
-
const findInstallByManifestId = async (envId, manifestId) => {
|
|
3850
|
-
const pkgRow = await ql.findOne(PKG, { where: { manifest_id: manifestId } });
|
|
3851
|
-
if (!pkgRow?.id) return null;
|
|
3852
|
-
return await ql.findOne(PKG_INSTALL, {
|
|
3853
|
-
where: { environment_id: envId, package_id: pkgRow.id }
|
|
3854
|
-
});
|
|
3855
|
-
};
|
|
3856
|
-
const toShortName = (driverId) => {
|
|
3857
|
-
const prefix = "com.objectstack.driver.";
|
|
3858
|
-
return driverId.startsWith(prefix) ? driverId.slice(prefix.length) : driverId;
|
|
3859
|
-
};
|
|
3860
|
-
const listRegisteredDrivers = () => {
|
|
3861
|
-
const services = this.getServicesMap();
|
|
3862
|
-
const registry = services["project-provisioning-adapters"];
|
|
3863
|
-
if (registry && typeof registry.list === "function") {
|
|
3864
|
-
try {
|
|
3865
|
-
const adapters = registry.list();
|
|
3866
|
-
const seen = /* @__PURE__ */ new Set();
|
|
3867
|
-
const drivers2 = [];
|
|
3868
|
-
for (const adapter of adapters ?? []) {
|
|
3869
|
-
const name = adapter?.driver;
|
|
3870
|
-
if (!name || seen.has(name)) continue;
|
|
3871
|
-
seen.add(name);
|
|
3872
|
-
drivers2.push({ name, driverId: `com.objectstack.driver.${name}` });
|
|
3873
|
-
}
|
|
3874
|
-
if (drivers2.length > 0) return drivers2;
|
|
3875
|
-
} catch {
|
|
3876
|
-
}
|
|
3877
|
-
}
|
|
3878
|
-
const drivers = [];
|
|
3879
|
-
for (const [serviceKey, svc] of Object.entries(services)) {
|
|
3880
|
-
if (!serviceKey.startsWith("driver.")) continue;
|
|
3881
|
-
const raw = serviceKey.slice("driver.".length);
|
|
3882
|
-
if (!raw || raw === "unknown") continue;
|
|
3883
|
-
const driverId = svc?.name ?? raw;
|
|
3884
|
-
drivers.push({ name: toShortName(driverId), driverId });
|
|
3885
|
-
}
|
|
3886
|
-
return drivers;
|
|
3887
|
-
};
|
|
3888
|
-
const resolveDriver = (requested) => {
|
|
3889
|
-
const registered = listRegisteredDrivers();
|
|
3890
|
-
if (requested) {
|
|
3891
|
-
const wanted = String(requested).toLowerCase();
|
|
3892
|
-
return registered.find((d) => d.name === wanted || d.driverId === wanted);
|
|
3893
|
-
}
|
|
3894
|
-
return registered.find((d) => d.name === "turso") ?? registered.find((d) => d.name === "memory") ?? registered[0];
|
|
3895
|
-
};
|
|
3896
|
-
const buildDatabaseUrl = (driverName, environmentId) => {
|
|
3897
|
-
const dbName = `env-${environmentId}`;
|
|
3898
|
-
switch (driverName) {
|
|
3899
|
-
case "memory":
|
|
3900
|
-
return `memory://${dbName}`;
|
|
3901
|
-
case "turso":
|
|
3902
|
-
return `libsql://${dbName}.mock-turso.local`;
|
|
3903
|
-
default:
|
|
3904
|
-
return `${driverName}://${dbName}`;
|
|
3905
|
-
}
|
|
3906
|
-
};
|
|
3907
|
-
const getRealAdapter = async (driverName) => {
|
|
3908
|
-
try {
|
|
3909
|
-
const registry = await this.resolveService("project-provisioning-adapters");
|
|
3910
|
-
const aliases = { sql: "sqlite" };
|
|
3911
|
-
const effective = aliases[driverName] ?? driverName;
|
|
3912
|
-
return registry?.get?.(effective) ?? registry?.get?.(driverName);
|
|
3913
|
-
} catch {
|
|
3914
|
-
return void 0;
|
|
3915
|
-
}
|
|
3916
|
-
};
|
|
3917
|
-
const findOne = async (obj, where) => {
|
|
3918
|
-
let rows = await ql.find(obj, { where });
|
|
3919
|
-
if (rows && rows.value) rows = rows.value;
|
|
3920
|
-
if (!Array.isArray(rows)) return void 0;
|
|
3921
|
-
return rows[0];
|
|
3922
|
-
};
|
|
3923
|
-
const cleanProjectRow = (row) => {
|
|
3924
|
-
if (!row) return row;
|
|
3925
|
-
let metadata = row.metadata;
|
|
3926
|
-
if (typeof metadata === "string") {
|
|
3927
|
-
try {
|
|
3928
|
-
metadata = JSON.parse(metadata);
|
|
3929
|
-
} catch {
|
|
3930
|
-
}
|
|
3931
|
-
}
|
|
3932
|
-
return { ...row, metadata };
|
|
3933
|
-
};
|
|
3934
|
-
try {
|
|
3935
|
-
if (parts.length === 1 && parts[0] === "drivers" && m === "GET") {
|
|
3936
|
-
const drivers = listRegisteredDrivers();
|
|
3937
|
-
return { handled: true, response: this.success({ drivers, total: drivers.length }) };
|
|
3938
|
-
}
|
|
3939
|
-
if (parts.length === 1 && parts[0] === "templates" && m === "GET") {
|
|
3940
|
-
try {
|
|
3941
|
-
const seeder = await this.resolveService("template-seeder");
|
|
3942
|
-
const templates = seeder?.listTemplates?.() ?? [];
|
|
3943
|
-
return { handled: true, response: this.success({ templates, total: templates.length }) };
|
|
3944
|
-
} catch (err) {
|
|
3945
|
-
try {
|
|
3946
|
-
console.error("[HttpDispatcher] /cloud/templates: failed to resolve template-seeder:", err?.message ?? err);
|
|
3947
|
-
} catch {
|
|
3948
|
-
}
|
|
3949
|
-
return { handled: true, response: this.success({ templates: [], total: 0 }) };
|
|
3950
|
-
}
|
|
3951
|
-
}
|
|
3952
|
-
if (parts.length === 3 && parts[0] === "admin" && parts[1] === "platform-sso" && parts[2] === "backfill" && m === "POST") {
|
|
3953
|
-
const baseSecret = (readEnvWithDeprecation3("OS_AUTH_SECRET", ["AUTH_SECRET", "BETTER_AUTH_SECRET"]) ?? "").trim();
|
|
3954
|
-
if (!baseSecret) {
|
|
3955
|
-
return { handled: true, response: this.error("OS_AUTH_SECRET not configured on this worker", 503) };
|
|
3956
|
-
}
|
|
3957
|
-
const rawHeaders = _context?.request?.headers;
|
|
3958
|
-
let authHeader;
|
|
3959
|
-
if (rawHeaders && typeof rawHeaders.get === "function") {
|
|
3960
|
-
authHeader = rawHeaders.get("authorization") ?? void 0;
|
|
3961
|
-
} else if (rawHeaders && typeof rawHeaders === "object") {
|
|
3962
|
-
authHeader = rawHeaders["authorization"] ?? rawHeaders["Authorization"];
|
|
3963
|
-
}
|
|
3964
|
-
const presented = typeof authHeader === "string" && authHeader.startsWith("Bearer ") ? authHeader.slice(7).trim() : "";
|
|
3965
|
-
if (!presented || presented !== baseSecret) {
|
|
3966
|
-
return { handled: true, response: this.error("forbidden: Bearer token must match OS_AUTH_SECRET", 403) };
|
|
3967
|
-
}
|
|
3968
|
-
try {
|
|
3969
|
-
const { backfillPlatformSsoClients: backfillPlatformSsoClients2 } = await Promise.resolve().then(() => (init_platform_sso(), platform_sso_exports));
|
|
3970
|
-
const result = await backfillPlatformSsoClients2({
|
|
3971
|
-
ql,
|
|
3972
|
-
baseSecret,
|
|
3973
|
-
logger: console
|
|
3974
|
-
});
|
|
3975
|
-
let sample = [];
|
|
3976
|
-
let total = 0;
|
|
3977
|
-
try {
|
|
3978
|
-
const rows = await ql.find("sys_oauth_application", { limit: 5 }, { context: { isSystem: true } });
|
|
3979
|
-
const list = Array.isArray(rows) ? rows : Array.isArray(rows?.records) ? rows.records : [];
|
|
3980
|
-
sample = list;
|
|
3981
|
-
total = typeof rows?.total === "number" ? rows.total : list.length;
|
|
3982
|
-
} catch (e) {
|
|
3983
|
-
sample = [{ _readErr: e?.message ?? String(e) }];
|
|
3984
|
-
}
|
|
3985
|
-
return { handled: true, response: this.success({ ...result, total, sample }) };
|
|
3986
|
-
} catch (err) {
|
|
3987
|
-
return { handled: true, response: this.error(`backfill failed: ${err?.message ?? String(err)}`, 500) };
|
|
3988
|
-
}
|
|
3989
|
-
}
|
|
3990
|
-
if (parts.length === 1 && parts[0] === "projects" && m === "GET") {
|
|
3991
|
-
const where = {};
|
|
3992
|
-
if (query?.organizationId) where.organization_id = query.organizationId;
|
|
3993
|
-
if (query?.status) where.status = query.status;
|
|
3994
|
-
let rows = await ql.find(ENV, Object.keys(where).length ? { where } : void 0);
|
|
3995
|
-
if (rows && rows.value) rows = rows.value;
|
|
3996
|
-
const projects = (Array.isArray(rows) ? rows : []).map(cleanProjectRow);
|
|
3997
|
-
return { handled: true, response: this.success({ projects, total: projects.length }) };
|
|
3998
|
-
}
|
|
3999
|
-
if (parts.length === 1 && parts[0] === "projects" && m === "POST") {
|
|
4000
|
-
const req = body || {};
|
|
4001
|
-
if (req.organization_id === "__session__" || req.created_by === "__session__") {
|
|
4002
|
-
try {
|
|
4003
|
-
const userId = await this.resolveCallerUserId(_context);
|
|
4004
|
-
if (req.created_by === "__session__") {
|
|
4005
|
-
req.created_by = userId ?? "system";
|
|
4006
|
-
}
|
|
4007
|
-
if (req.organization_id === "__session__") {
|
|
4008
|
-
const authService = await this.resolveService(CoreServiceName.enum.auth);
|
|
4009
|
-
const rawHeaders = _context?.request?.headers;
|
|
4010
|
-
let headers = rawHeaders;
|
|
4011
|
-
if (rawHeaders && typeof rawHeaders === "object" && typeof rawHeaders.get !== "function") {
|
|
4012
|
-
const h = new Headers();
|
|
4013
|
-
for (const [k, v] of Object.entries(rawHeaders)) {
|
|
4014
|
-
if (v == null) continue;
|
|
4015
|
-
h.set(k, Array.isArray(v) ? v.join(", ") : String(v));
|
|
4016
|
-
}
|
|
4017
|
-
headers = h;
|
|
4018
|
-
}
|
|
4019
|
-
const apiObj = authService?.auth?.api ?? authService?.api;
|
|
4020
|
-
const sessionData = await apiObj?.getSession?.call(apiObj, { headers });
|
|
4021
|
-
req.organization_id = sessionData?.session?.activeOrganizationId ?? void 0;
|
|
4022
|
-
}
|
|
4023
|
-
} catch {
|
|
4024
|
-
}
|
|
4025
|
-
}
|
|
4026
|
-
if (!req.organization_id || !req.display_name) {
|
|
4027
|
-
return { handled: true, response: this.error("organization_id and display_name are required", 400) };
|
|
4028
|
-
}
|
|
4029
|
-
const environmentId = randomUUID();
|
|
4030
|
-
const credentialId = randomUUID();
|
|
4031
|
-
const nowIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
4032
|
-
const resolved = resolveDriver(req.driver);
|
|
4033
|
-
if (!resolved) {
|
|
4034
|
-
const available = listRegisteredDrivers().map((d) => d.name);
|
|
4035
|
-
if (req.driver) {
|
|
4036
|
-
return {
|
|
4037
|
-
handled: true,
|
|
4038
|
-
response: this.error(
|
|
4039
|
-
`Unknown driver '${req.driver}'. Available drivers: [${available.join(", ") || "none"}]`,
|
|
4040
|
-
400
|
|
4041
|
-
)
|
|
4042
|
-
};
|
|
4043
|
-
}
|
|
4044
|
-
return {
|
|
4045
|
-
handled: true,
|
|
4046
|
-
response: this.error(
|
|
4047
|
-
"No ObjectQL driver is registered. Register at least one DriverPlugin (e.g. InMemoryDriver or SqlDriver).",
|
|
4048
|
-
503
|
|
4049
|
-
)
|
|
4050
|
-
};
|
|
4051
|
-
}
|
|
4052
|
-
const driver = resolved.name;
|
|
4053
|
-
let plaintextSecret = `mock-token-${environmentId}`;
|
|
4054
|
-
let computedHostname = req.hostname;
|
|
4055
|
-
if (!computedHostname) {
|
|
4056
|
-
const shortId = environmentId.slice(0, 8);
|
|
4057
|
-
try {
|
|
4058
|
-
const orgRow = await findOne("sys_organization", { id: req.organization_id });
|
|
4059
|
-
const orgSlug = orgRow?.slug || req.organization_id;
|
|
4060
|
-
const rootDomain = getEnv("OS_ROOT_DOMAIN") ?? getEnv("ROOT_DOMAIN", "objectstack.app");
|
|
4061
|
-
computedHostname = `${orgSlug}-${shortId}.${rootDomain}`;
|
|
4062
|
-
} catch {
|
|
4063
|
-
computedHostname = `${req.organization_id}-${shortId}.objectstack.app`;
|
|
4064
|
-
}
|
|
4065
|
-
}
|
|
4066
|
-
try {
|
|
4067
|
-
const existing = await findOne("sys_environment", {
|
|
4068
|
-
hostname: computedHostname
|
|
4069
|
-
});
|
|
4070
|
-
if (existing && existing.id !== environmentId) {
|
|
4071
|
-
return {
|
|
4072
|
-
handled: true,
|
|
4073
|
-
response: this.error(
|
|
4074
|
-
`Hostname '${computedHostname}' is already in use by another project.`,
|
|
4075
|
-
409,
|
|
4076
|
-
{ code: "HOSTNAME_TAKEN", hostname: computedHostname }
|
|
4077
|
-
)
|
|
4078
|
-
};
|
|
4079
|
-
}
|
|
4080
|
-
} catch {
|
|
4081
|
-
}
|
|
4082
|
-
const baseMetadata = { ...req.metadata ?? {} };
|
|
4083
|
-
const simulateFailure = Boolean(baseMetadata.__simulateFailure);
|
|
4084
|
-
const simulateDelayMs = Number(baseMetadata.__simulateDelayMs ?? 1500);
|
|
4085
|
-
try {
|
|
4086
|
-
let ownerUserId = req.created_by && req.created_by !== "system" ? String(req.created_by) : void 0;
|
|
4087
|
-
if (!ownerUserId) {
|
|
4088
|
-
ownerUserId = await this.resolveCallerUserId(_context);
|
|
4089
|
-
}
|
|
4090
|
-
if (ownerUserId) {
|
|
4091
|
-
const userRow = await ql.find("sys_user", { where: { id: ownerUserId } });
|
|
4092
|
-
const userRows = Array.isArray(userRow) ? userRow : userRow?.value ?? [];
|
|
4093
|
-
const u = Array.isArray(userRows) && userRows.length > 0 ? userRows[0] : null;
|
|
4094
|
-
if (u?.email) {
|
|
4095
|
-
baseMetadata.ownerSeed = {
|
|
4096
|
-
userId: String(ownerUserId),
|
|
4097
|
-
email: String(u.email),
|
|
4098
|
-
name: u.name ? String(u.name) : null,
|
|
4099
|
-
image: u.image ? String(u.image) : null
|
|
4100
|
-
};
|
|
4101
|
-
}
|
|
4102
|
-
}
|
|
4103
|
-
} catch {
|
|
4104
|
-
}
|
|
4105
|
-
try {
|
|
4106
|
-
const orgRow = await ql.find("sys_organization", { where: { id: req.organization_id } });
|
|
4107
|
-
const orgRows = Array.isArray(orgRow) ? orgRow : orgRow?.value ?? [];
|
|
4108
|
-
const org = Array.isArray(orgRows) && orgRows.length > 0 ? orgRows[0] : null;
|
|
4109
|
-
if (org?.id && org?.name) {
|
|
4110
|
-
baseMetadata.orgSeed = {
|
|
4111
|
-
id: String(org.id),
|
|
4112
|
-
name: String(org.name),
|
|
4113
|
-
slug: org.slug ? String(org.slug) : null,
|
|
4114
|
-
logo: org.logo ? String(org.logo) : null
|
|
4115
|
-
};
|
|
4116
|
-
}
|
|
4117
|
-
} catch {
|
|
4118
|
-
}
|
|
4119
|
-
await ql.insert(ENV, {
|
|
4120
|
-
id: environmentId,
|
|
4121
|
-
organization_id: req.organization_id,
|
|
4122
|
-
display_name: req.display_name,
|
|
4123
|
-
is_default: req.is_default ?? false,
|
|
4124
|
-
is_system: req.is_system ?? false,
|
|
4125
|
-
plan: req.plan ?? "free",
|
|
4126
|
-
status: "provisioning",
|
|
4127
|
-
created_by: req.created_by ?? "system",
|
|
4128
|
-
metadata: JSON.stringify(baseMetadata),
|
|
4129
|
-
created_at: nowIso,
|
|
4130
|
-
updated_at: nowIso,
|
|
4131
|
-
database_url: null,
|
|
4132
|
-
database_driver: driver,
|
|
4133
|
-
storage_limit_mb: req.storage_limit_mb ?? 1024,
|
|
4134
|
-
provisioned_at: null,
|
|
4135
|
-
hostname: computedHostname,
|
|
4136
|
-
visibility: (() => {
|
|
4137
|
-
const raw = String(req.visibility ?? "private");
|
|
4138
|
-
return raw === "unlisted" ? "private" : raw;
|
|
4139
|
-
})()
|
|
4140
|
-
});
|
|
4141
|
-
try {
|
|
4142
|
-
const { seedPlatformSsoClient: seedPlatformSsoClient2 } = await Promise.resolve().then(() => (init_platform_sso(), platform_sso_exports));
|
|
4143
|
-
const baseSecret = (readEnvWithDeprecation3("OS_AUTH_SECRET", ["AUTH_SECRET", "BETTER_AUTH_SECRET"]) ?? "").trim();
|
|
4144
|
-
if (baseSecret) {
|
|
4145
|
-
await seedPlatformSsoClient2({
|
|
4146
|
-
ql,
|
|
4147
|
-
environmentId,
|
|
4148
|
-
hostname: computedHostname,
|
|
4149
|
-
baseSecret,
|
|
4150
|
-
logger: console
|
|
4151
|
-
});
|
|
4152
|
-
}
|
|
4153
|
-
} catch (ssoErr) {
|
|
4154
|
-
console.warn?.("[http-dispatcher] platform SSO seed failed (non-fatal)", {
|
|
4155
|
-
environmentId,
|
|
4156
|
-
error: ssoErr?.message
|
|
4157
|
-
});
|
|
4158
|
-
}
|
|
4159
|
-
const runProvisioning = async () => {
|
|
4160
|
-
try {
|
|
4161
|
-
if (simulateDelayMs > 0) {
|
|
4162
|
-
await new Promise((r) => setTimeout(r, simulateDelayMs));
|
|
4163
|
-
}
|
|
4164
|
-
if (simulateFailure) {
|
|
4165
|
-
throw new Error("Simulated provisioning failure (metadata.__simulateFailure=true)");
|
|
4166
|
-
}
|
|
4167
|
-
let databaseUrl;
|
|
4168
|
-
try {
|
|
4169
|
-
const adapter = await getRealAdapter(driver);
|
|
4170
|
-
if (adapter) {
|
|
4171
|
-
const result = await adapter.createDatabase({
|
|
4172
|
-
environmentId,
|
|
4173
|
-
databaseName: `p-${environmentId.replace(/-/g, "").slice(0, 24)}`,
|
|
4174
|
-
region: "us-east-1",
|
|
4175
|
-
storageLimitMb: req.storage_limit_mb ?? 1024
|
|
4176
|
-
});
|
|
4177
|
-
databaseUrl = result.databaseUrl;
|
|
4178
|
-
if (result.plaintextSecret) plaintextSecret = result.plaintextSecret;
|
|
4179
|
-
} else {
|
|
4180
|
-
databaseUrl = buildDatabaseUrl(driver, environmentId);
|
|
4181
|
-
}
|
|
4182
|
-
} catch (adapterErr) {
|
|
4183
|
-
throw adapterErr instanceof Error ? adapterErr : new Error(String(adapterErr));
|
|
4184
|
-
}
|
|
4185
|
-
const seedStartedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4186
|
-
await ql.update(
|
|
4187
|
-
ENV,
|
|
4188
|
-
{
|
|
4189
|
-
database_url: databaseUrl,
|
|
4190
|
-
updated_at: seedStartedAt
|
|
4191
|
-
},
|
|
4192
|
-
{ where: { id: environmentId } }
|
|
4193
|
-
);
|
|
4194
|
-
await ql.insert(CRED, {
|
|
4195
|
-
id: credentialId,
|
|
4196
|
-
environment_id: environmentId,
|
|
4197
|
-
secret_ciphertext: plaintextSecret,
|
|
4198
|
-
encryption_key_id: "noop",
|
|
4199
|
-
authorization: "full_access",
|
|
4200
|
-
status: "active",
|
|
4201
|
-
created_at: seedStartedAt,
|
|
4202
|
-
updated_at: seedStartedAt
|
|
4203
|
-
});
|
|
4204
|
-
const templateId = req.template_id ?? "blank";
|
|
4205
|
-
if (templateId !== "blank") {
|
|
4206
|
-
try {
|
|
4207
|
-
const seeder = await this.resolveService("template-seeder");
|
|
4208
|
-
if (seeder) {
|
|
4209
|
-
await seeder.seed({ environmentId, templateId });
|
|
4210
|
-
}
|
|
4211
|
-
} catch (seedErr) {
|
|
4212
|
-
const seedMessage = seedErr instanceof Error ? seedErr.message : String(seedErr);
|
|
4213
|
-
try {
|
|
4214
|
-
const existing = await findOne(ENV, { id: environmentId });
|
|
4215
|
-
const existingMeta = typeof existing?.metadata === "string" ? JSON.parse(existing.metadata) : existing?.metadata ?? {};
|
|
4216
|
-
await ql.update(
|
|
4217
|
-
ENV,
|
|
4218
|
-
{
|
|
4219
|
-
metadata: JSON.stringify({
|
|
4220
|
-
...existingMeta,
|
|
4221
|
-
templateSeedError: { message: seedMessage, templateId }
|
|
4222
|
-
})
|
|
4223
|
-
},
|
|
4224
|
-
{ where: { id: environmentId } }
|
|
4225
|
-
);
|
|
4226
|
-
} catch {
|
|
4227
|
-
}
|
|
4228
|
-
}
|
|
4229
|
-
}
|
|
4230
|
-
const artifactPathRaw = baseMetadata.artifact_path;
|
|
4231
|
-
if (typeof artifactPathRaw === "string" && artifactPathRaw.length > 0) {
|
|
4232
|
-
try {
|
|
4233
|
-
const path2 = await import("path");
|
|
4234
|
-
const { isHttpUrl: isHttpUrl2, loadArtifactBundle: loadArtifactBundle2 } = await Promise.resolve().then(() => (init_load_artifact_bundle(), load_artifact_bundle_exports));
|
|
4235
|
-
const root = process.env.OS_PROJECT_ARTIFACT_ROOT ?? process.cwd();
|
|
4236
|
-
const resolved2 = isHttpUrl2(artifactPathRaw) ? artifactPathRaw : path2.isAbsolute(artifactPathRaw) ? artifactPathRaw : path2.resolve(root, artifactPathRaw);
|
|
4237
|
-
const bundle = await loadArtifactBundle2(resolved2, { tag: "[bind-artifact]" });
|
|
4238
|
-
if (!bundle) {
|
|
4239
|
-
throw new Error(`failed to load artifact bundle at '${resolved2}'`);
|
|
4240
|
-
}
|
|
4241
|
-
const seeder = await this.resolveService("template-seeder");
|
|
4242
|
-
if (seeder?.seedBundle) {
|
|
4243
|
-
await seeder.seedBundle({ environmentId, bundle });
|
|
4244
|
-
} else {
|
|
4245
|
-
throw new Error("template-seeder.seedBundle is unavailable");
|
|
4246
|
-
}
|
|
4247
|
-
} catch (bindErr) {
|
|
4248
|
-
const bindMessage = bindErr instanceof Error ? bindErr.message : String(bindErr);
|
|
4249
|
-
try {
|
|
4250
|
-
const existing = await findOne(ENV, { id: environmentId });
|
|
4251
|
-
const existingMeta = typeof existing?.metadata === "string" ? JSON.parse(existing.metadata) : existing?.metadata ?? {};
|
|
4252
|
-
await ql.update(
|
|
4253
|
-
ENV,
|
|
4254
|
-
{
|
|
4255
|
-
metadata: JSON.stringify({
|
|
4256
|
-
...existingMeta,
|
|
4257
|
-
artifactBindError: { message: bindMessage, artifactPath: artifactPathRaw }
|
|
4258
|
-
})
|
|
4259
|
-
},
|
|
4260
|
-
{ where: { id: environmentId } }
|
|
4261
|
-
);
|
|
4262
|
-
} catch {
|
|
4263
|
-
}
|
|
4264
|
-
}
|
|
4265
|
-
}
|
|
4266
|
-
const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4267
|
-
await ql.update(
|
|
4268
|
-
ENV,
|
|
4269
|
-
{
|
|
4270
|
-
status: "active",
|
|
4271
|
-
provisioned_at: finishedAt,
|
|
4272
|
-
updated_at: finishedAt
|
|
4273
|
-
},
|
|
4274
|
-
{ where: { id: environmentId } }
|
|
4275
|
-
);
|
|
4276
|
-
} catch (err) {
|
|
4277
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
4278
|
-
const failedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4279
|
-
await ql.update(
|
|
4280
|
-
ENV,
|
|
4281
|
-
{
|
|
4282
|
-
status: "failed",
|
|
4283
|
-
metadata: JSON.stringify({
|
|
4284
|
-
...baseMetadata,
|
|
4285
|
-
provisioningError: { message, failedAt }
|
|
4286
|
-
}),
|
|
4287
|
-
updated_at: failedAt
|
|
4288
|
-
},
|
|
4289
|
-
{ where: { id: environmentId } }
|
|
4290
|
-
);
|
|
4291
|
-
}
|
|
4292
|
-
};
|
|
4293
|
-
const provisionSyncEnv = process.env.OS_PROVISION_SYNC;
|
|
4294
|
-
const onServerless = !!(process.env.VERCEL || process.env.AWS_LAMBDA_FUNCTION_NAME || process.env.NETLIFY || process.env.CF_PAGES);
|
|
4295
|
-
const syncProvisioning = provisionSyncEnv === void 0 ? onServerless : provisionSyncEnv !== "0" && provisionSyncEnv !== "false";
|
|
4296
|
-
if (syncProvisioning) {
|
|
4297
|
-
await runProvisioning();
|
|
4298
|
-
} else {
|
|
4299
|
-
void runProvisioning();
|
|
4300
|
-
}
|
|
4301
|
-
const project = cleanProjectRow(await findOne(ENV, { id: environmentId }));
|
|
4302
|
-
const res = this.success({ project });
|
|
4303
|
-
res.status = syncProvisioning ? 201 : 202;
|
|
4304
|
-
return { handled: true, response: res };
|
|
4305
|
-
}
|
|
4306
|
-
if (parts.length === 2 && parts[0] === "projects") {
|
|
4307
|
-
const id = decodeURIComponent(parts[1]);
|
|
4308
|
-
if (m === "GET") {
|
|
4309
|
-
const envRow = await findOne(ENV, { id });
|
|
4310
|
-
if (!envRow) return { handled: true, response: this.error(`Project '${id}' not found`, 404) };
|
|
4311
|
-
const credRow = await findOne(CRED, { environment_id: id, status: "active" });
|
|
4312
|
-
const callerUserId = await this.resolveCallerUserId(_context);
|
|
4313
|
-
const membership = callerUserId ? await findOne(MEM, { environment_id: id, user_id: callerUserId }) : await findOne(MEM, { environment_id: id });
|
|
4314
|
-
const credMeta = credRow ? {
|
|
4315
|
-
id: credRow.id,
|
|
4316
|
-
status: credRow.status,
|
|
4317
|
-
authorization: credRow.authorization,
|
|
4318
|
-
activatedAt: credRow.created_at,
|
|
4319
|
-
expiresAt: credRow.expires_at
|
|
4320
|
-
} : void 0;
|
|
4321
|
-
const project = cleanProjectRow(envRow);
|
|
4322
|
-
const database = project.database_url ? {
|
|
4323
|
-
driver: project.database_driver,
|
|
4324
|
-
database_name: `env-${project.id}`,
|
|
4325
|
-
database_url: project.database_url,
|
|
4326
|
-
storage_limit_mb: project.storage_limit_mb,
|
|
4327
|
-
provisioned_at: project.provisioned_at
|
|
4328
|
-
} : void 0;
|
|
4329
|
-
return {
|
|
4330
|
-
handled: true,
|
|
4331
|
-
response: this.success({ project, database, credential: credMeta, membership })
|
|
4332
|
-
};
|
|
4333
|
-
}
|
|
4334
|
-
if (m === "PATCH") {
|
|
4335
|
-
const patch = {};
|
|
4336
|
-
if (body?.display_name !== void 0) patch.display_name = body.display_name;
|
|
4337
|
-
if (body?.plan !== void 0) patch.plan = body.plan;
|
|
4338
|
-
if (body?.status !== void 0) patch.status = body.status;
|
|
4339
|
-
if (body?.is_default !== void 0) patch.is_default = body.is_default;
|
|
4340
|
-
if (body?.visibility !== void 0) {
|
|
4341
|
-
let v = String(body.visibility);
|
|
4342
|
-
if (v === "unlisted") v = "private";
|
|
4343
|
-
if (!["private", "public"].includes(v)) {
|
|
4344
|
-
return { handled: true, response: this.error(`Invalid visibility '${v}' (expected private | public)`, 400) };
|
|
4345
|
-
}
|
|
4346
|
-
patch.visibility = v;
|
|
4347
|
-
}
|
|
4348
|
-
if (body?.metadata !== void 0) patch.metadata = JSON.stringify(body.metadata);
|
|
4349
|
-
patch.updated_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
4350
|
-
await ql.update(ENV, patch, { where: { id } });
|
|
4351
|
-
const envRow = await findOne(ENV, { id });
|
|
4352
|
-
if (!envRow) return { handled: true, response: this.error(`Project '${id}' not found`, 404) };
|
|
4353
|
-
return { handled: true, response: this.success({ project: cleanProjectRow(envRow) }) };
|
|
4354
|
-
}
|
|
4355
|
-
if (m === "DELETE") {
|
|
4356
|
-
const force = query?.force === "1" || query?.force === "true" || body?.force === true;
|
|
4357
|
-
const result = await this.deleteProjectCascade(id, { ql, findOne, getRealAdapter, force });
|
|
4358
|
-
if (!result.ok) {
|
|
4359
|
-
return { handled: true, response: this.error(result.error ?? "Delete failed", result.status ?? 500) };
|
|
4360
|
-
}
|
|
4361
|
-
return { handled: true, response: this.success({ deleted: true, environmentId: id, warnings: result.warnings }) };
|
|
4362
|
-
}
|
|
4363
|
-
}
|
|
4364
|
-
if (parts.length === 2 && parts[0] === "organizations" && m === "DELETE") {
|
|
4365
|
-
const orgId = decodeURIComponent(parts[1]);
|
|
4366
|
-
let projectRows = [];
|
|
4367
|
-
try {
|
|
4368
|
-
let rows = await ql.find(ENV, { where: { organization_id: orgId } });
|
|
4369
|
-
if (rows && rows.value) rows = rows.value;
|
|
4370
|
-
projectRows = Array.isArray(rows) ? rows : [];
|
|
4371
|
-
} catch {
|
|
4372
|
-
projectRows = [];
|
|
4373
|
-
}
|
|
4374
|
-
const warnings = [];
|
|
4375
|
-
let deletedProjects = 0;
|
|
4376
|
-
for (const row of projectRows) {
|
|
4377
|
-
const pid = row?.id;
|
|
4378
|
-
if (!pid) continue;
|
|
4379
|
-
try {
|
|
4380
|
-
const r = await this.deleteProjectCascade(pid, { ql, findOne, getRealAdapter, force: true });
|
|
4381
|
-
if (r.ok) deletedProjects++;
|
|
4382
|
-
if (r.warnings?.length) warnings.push(...r.warnings);
|
|
4383
|
-
if (!r.ok && r.error) warnings.push(`Project ${pid}: ${r.error}`);
|
|
4384
|
-
} catch (err) {
|
|
4385
|
-
warnings.push(
|
|
4386
|
-
`Failed to delete project ${pid}: ${err instanceof Error ? err.message : String(err)}`
|
|
4387
|
-
);
|
|
4388
|
-
}
|
|
4389
|
-
}
|
|
4390
|
-
let orgDeleted = false;
|
|
4391
|
-
try {
|
|
4392
|
-
const authService = await this.getService(CoreServiceName.enum.auth);
|
|
4393
|
-
const fn = authService?.api?.deleteOrganization;
|
|
4394
|
-
if (typeof fn === "function") {
|
|
4395
|
-
await fn.call(authService.api, {
|
|
4396
|
-
body: { organizationId: orgId },
|
|
4397
|
-
headers: _context?.request?.headers
|
|
4398
|
-
});
|
|
4399
|
-
orgDeleted = true;
|
|
4400
|
-
}
|
|
4401
|
-
} catch (err) {
|
|
4402
|
-
warnings.push(
|
|
4403
|
-
`auth.deleteOrganization failed: ${err instanceof Error ? err.message : String(err)}`
|
|
4404
|
-
);
|
|
4405
|
-
}
|
|
4406
|
-
if (!orgDeleted) {
|
|
4407
|
-
try {
|
|
4408
|
-
await ql.delete("sys_organization", { where: { id: orgId } });
|
|
4409
|
-
orgDeleted = true;
|
|
4410
|
-
} catch (err) {
|
|
4411
|
-
warnings.push(
|
|
4412
|
-
`Failed to delete sys_organization row: ${err instanceof Error ? err.message : String(err)}`
|
|
4413
|
-
);
|
|
4414
|
-
}
|
|
4415
|
-
}
|
|
4416
|
-
return {
|
|
4417
|
-
handled: true,
|
|
4418
|
-
response: this.success({
|
|
4419
|
-
deleted: orgDeleted,
|
|
4420
|
-
organizationId: orgId,
|
|
4421
|
-
deletedProjects,
|
|
4422
|
-
warnings
|
|
4423
|
-
})
|
|
4424
|
-
};
|
|
4425
|
-
}
|
|
4426
|
-
if (parts.length === 3 && parts[0] === "projects" && parts[2] === "hostname" && (m === "POST" || m === "PUT")) {
|
|
4427
|
-
const id = decodeURIComponent(parts[1]);
|
|
4428
|
-
const hostname = body?.hostname;
|
|
4429
|
-
if (!hostname || typeof hostname !== "string") {
|
|
4430
|
-
return { handled: true, response: this.error("hostname is required", 400) };
|
|
4431
|
-
}
|
|
4432
|
-
const normalized = hostname.trim().toLowerCase();
|
|
4433
|
-
if (!/^[a-z0-9]([a-z0-9\-\.]*[a-z0-9])?$/.test(normalized)) {
|
|
4434
|
-
return { handled: true, response: this.error("Invalid hostname format", 400) };
|
|
4435
|
-
}
|
|
4436
|
-
const envRow = await findOne(ENV, { id });
|
|
4437
|
-
if (!envRow) return { handled: true, response: this.error(`Project '${id}' not found`, 404) };
|
|
4438
|
-
let existing;
|
|
4439
|
-
try {
|
|
4440
|
-
const rows = await ql.find(ENV, { where: { hostname: normalized } });
|
|
4441
|
-
const arr = Array.isArray(rows) ? rows : rows?.value ?? [];
|
|
4442
|
-
existing = arr.find((r) => r.id !== id);
|
|
4443
|
-
} catch {
|
|
4444
|
-
}
|
|
4445
|
-
if (existing) {
|
|
4446
|
-
return {
|
|
4447
|
-
handled: true,
|
|
4448
|
-
response: this.error(
|
|
4449
|
-
`Hostname '${normalized}' is already in use by another project.`,
|
|
4450
|
-
409,
|
|
4451
|
-
{ code: "HOSTNAME_TAKEN", hostname: normalized }
|
|
4452
|
-
)
|
|
4453
|
-
};
|
|
4454
|
-
}
|
|
4455
|
-
const updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4456
|
-
await ql.update(ENV, { hostname: normalized, updated_at: updatedAt }, { where: { id } });
|
|
4457
|
-
if (this.envRegistry?.invalidate) {
|
|
4458
|
-
try {
|
|
4459
|
-
await this.envRegistry.invalidate(id);
|
|
4460
|
-
} catch {
|
|
4461
|
-
}
|
|
4462
|
-
}
|
|
4463
|
-
const updated = cleanProjectRow(await findOne(ENV, { id }));
|
|
4464
|
-
return { handled: true, response: this.success({ project: updated }) };
|
|
4465
|
-
}
|
|
4466
|
-
if (parts.length === 3 && parts[0] === "projects" && parts[2] === "retry" && m === "POST") {
|
|
4467
|
-
const id = decodeURIComponent(parts[1]);
|
|
4468
|
-
const envRow = await findOne(ENV, { id });
|
|
4469
|
-
if (!envRow) return { handled: true, response: this.error(`Project '${id}' not found`, 404) };
|
|
4470
|
-
if (envRow.status !== "failed" && envRow.status !== "provisioning") {
|
|
4471
|
-
return {
|
|
4472
|
-
handled: true,
|
|
4473
|
-
response: this.error(
|
|
4474
|
-
`Project '${id}' is '${envRow.status}'; only failed or provisioning projects can be retried.`,
|
|
4475
|
-
409
|
|
4476
|
-
)
|
|
4477
|
-
};
|
|
4478
|
-
}
|
|
4479
|
-
const driverName = envRow.database_driver;
|
|
4480
|
-
const resolved = resolveDriver(driverName);
|
|
4481
|
-
if (!resolved) {
|
|
4482
|
-
return {
|
|
4483
|
-
handled: true,
|
|
4484
|
-
response: this.error(
|
|
4485
|
-
`Driver '${driverName}' is no longer registered; retry aborted.`,
|
|
4486
|
-
503
|
|
4487
|
-
)
|
|
4488
|
-
};
|
|
4489
|
-
}
|
|
4490
|
-
let metadata = {};
|
|
4491
|
-
if (envRow.metadata) {
|
|
4492
|
-
if (typeof envRow.metadata === "string") {
|
|
4493
|
-
try {
|
|
4494
|
-
metadata = JSON.parse(envRow.metadata);
|
|
4495
|
-
} catch {
|
|
4496
|
-
metadata = {};
|
|
4497
|
-
}
|
|
4498
|
-
} else if (typeof envRow.metadata === "object") {
|
|
4499
|
-
metadata = { ...envRow.metadata };
|
|
4500
|
-
}
|
|
4501
|
-
}
|
|
4502
|
-
delete metadata.provisioningError;
|
|
4503
|
-
const retryStartedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4504
|
-
await ql.update(
|
|
4505
|
-
ENV,
|
|
4506
|
-
{
|
|
4507
|
-
status: "provisioning",
|
|
4508
|
-
metadata: JSON.stringify(metadata),
|
|
4509
|
-
updated_at: retryStartedAt
|
|
4510
|
-
},
|
|
4511
|
-
{ where: { id } }
|
|
4512
|
-
);
|
|
4513
|
-
const simulateRetryFailure = Boolean(metadata.__simulateFailure);
|
|
4514
|
-
const simulateRetryDelay = Number(metadata.__simulateDelayMs ?? 1500);
|
|
4515
|
-
const runRetry = async () => {
|
|
4516
|
-
try {
|
|
4517
|
-
if (simulateRetryDelay > 0) {
|
|
4518
|
-
await new Promise((r) => setTimeout(r, simulateRetryDelay));
|
|
4519
|
-
}
|
|
4520
|
-
if (simulateRetryFailure) {
|
|
4521
|
-
throw new Error("Simulated provisioning failure (metadata.__simulateFailure=true)");
|
|
4522
|
-
}
|
|
4523
|
-
let databaseUrl;
|
|
4524
|
-
let retrySecret = `mock-token-${id}`;
|
|
4525
|
-
try {
|
|
4526
|
-
const adapter = await getRealAdapter(resolved.name);
|
|
4527
|
-
if (adapter) {
|
|
4528
|
-
const result = await adapter.createDatabase({
|
|
4529
|
-
environmentId: id,
|
|
4530
|
-
databaseName: `p-${id.replace(/-/g, "").slice(0, 24)}`,
|
|
4531
|
-
region: "us-east-1",
|
|
4532
|
-
storageLimitMb: envRow.storage_limit_mb ?? 1024
|
|
4533
|
-
});
|
|
4534
|
-
databaseUrl = result.databaseUrl;
|
|
4535
|
-
if (result.plaintextSecret) retrySecret = result.plaintextSecret;
|
|
4536
|
-
} else {
|
|
4537
|
-
databaseUrl = buildDatabaseUrl(resolved.name, id);
|
|
4538
|
-
}
|
|
4539
|
-
} catch (adapterErr) {
|
|
4540
|
-
throw adapterErr instanceof Error ? adapterErr : new Error(String(adapterErr));
|
|
4541
|
-
}
|
|
4542
|
-
const nowIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
4543
|
-
await ql.update(
|
|
4544
|
-
ENV,
|
|
4545
|
-
{
|
|
4546
|
-
status: "active",
|
|
4547
|
-
database_url: databaseUrl,
|
|
4548
|
-
database_driver: resolved.name,
|
|
4549
|
-
provisioned_at: nowIso,
|
|
4550
|
-
updated_at: nowIso
|
|
4551
|
-
},
|
|
4552
|
-
{ where: { id } }
|
|
4553
|
-
);
|
|
4554
|
-
const existingCred = await findOne(CRED, { environment_id: id, status: "active" });
|
|
4555
|
-
if (!existingCred) {
|
|
4556
|
-
await ql.insert(CRED, {
|
|
4557
|
-
id: randomUUID(),
|
|
4558
|
-
environment_id: id,
|
|
4559
|
-
secret_ciphertext: retrySecret,
|
|
4560
|
-
encryption_key_id: "noop",
|
|
4561
|
-
authorization: "full_access",
|
|
4562
|
-
status: "active",
|
|
4563
|
-
created_at: nowIso,
|
|
4564
|
-
updated_at: nowIso
|
|
4565
|
-
});
|
|
4566
|
-
}
|
|
4567
|
-
} catch (err) {
|
|
4568
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
4569
|
-
const failedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4570
|
-
await ql.update(
|
|
4571
|
-
ENV,
|
|
4572
|
-
{
|
|
4573
|
-
status: "failed",
|
|
4574
|
-
metadata: JSON.stringify({
|
|
4575
|
-
...metadata,
|
|
4576
|
-
provisioningError: { message, failedAt }
|
|
4577
|
-
}),
|
|
4578
|
-
updated_at: failedAt
|
|
4579
|
-
},
|
|
4580
|
-
{ where: { id } }
|
|
4581
|
-
);
|
|
4582
|
-
}
|
|
4583
|
-
};
|
|
4584
|
-
void runRetry();
|
|
4585
|
-
const envAfter = cleanProjectRow(await findOne(ENV, { id }));
|
|
4586
|
-
const retryRes = this.success({ project: envAfter });
|
|
4587
|
-
retryRes.status = 202;
|
|
4588
|
-
return { handled: true, response: retryRes };
|
|
4589
|
-
}
|
|
4590
|
-
if (parts.length === 3 && parts[0] === "projects" && parts[2] === "activate" && m === "POST") {
|
|
4591
|
-
const id = decodeURIComponent(parts[1]);
|
|
4592
|
-
const envRow = await findOne(ENV, { id });
|
|
4593
|
-
if (!envRow) return { handled: true, response: this.error(`Project '${id}' not found`, 404) };
|
|
4594
|
-
return { handled: true, response: this.success({ project: cleanProjectRow(envRow), sessionUpdated: false }) };
|
|
4595
|
-
}
|
|
4596
|
-
if (parts.length === 4 && parts[0] === "projects" && parts[2] === "credentials" && parts[3] === "rotate" && m === "POST") {
|
|
4597
|
-
const id = decodeURIComponent(parts[1]);
|
|
4598
|
-
const plaintext = body?.plaintext;
|
|
4599
|
-
if (!plaintext || typeof plaintext !== "string") {
|
|
4600
|
-
return { handled: true, response: this.error("plaintext is required", 400) };
|
|
4601
|
-
}
|
|
4602
|
-
const envRow = await findOne(ENV, { id });
|
|
4603
|
-
if (!envRow) return { handled: true, response: this.error(`Project '${id}' not found`, 404) };
|
|
4604
|
-
const nowIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
4605
|
-
let existing = await ql.find(CRED, { where: { environment_id: id, status: "active" } });
|
|
4606
|
-
if (existing && existing.value) existing = existing.value;
|
|
4607
|
-
for (const row of Array.isArray(existing) ? existing : []) {
|
|
4608
|
-
await ql.update(CRED, {
|
|
4609
|
-
status: "revoked",
|
|
4610
|
-
revoked_at: nowIso,
|
|
4611
|
-
updated_at: nowIso
|
|
4612
|
-
}, { where: { id: row.id } });
|
|
4613
|
-
}
|
|
4614
|
-
const credentialId = randomUUID();
|
|
4615
|
-
await ql.insert(CRED, {
|
|
4616
|
-
id: credentialId,
|
|
4617
|
-
environment_id: id,
|
|
4618
|
-
secret_ciphertext: plaintext,
|
|
4619
|
-
encryption_key_id: "noop",
|
|
4620
|
-
authorization: "full_access",
|
|
4621
|
-
status: "active",
|
|
4622
|
-
created_at: nowIso,
|
|
4623
|
-
updated_at: nowIso
|
|
4624
|
-
});
|
|
4625
|
-
const credential = await findOne(CRED, { id: credentialId });
|
|
4626
|
-
const credMeta = credential ? {
|
|
4627
|
-
id: credential.id,
|
|
4628
|
-
status: credential.status,
|
|
4629
|
-
authorization: credential.authorization,
|
|
4630
|
-
activatedAt: credential.created_at
|
|
4631
|
-
} : void 0;
|
|
4632
|
-
return { handled: true, response: this.success({ credential: credMeta }) };
|
|
4633
|
-
}
|
|
4634
|
-
if (parts.length === 3 && parts[0] === "projects" && parts[2] === "members" && m === "GET") {
|
|
4635
|
-
const id = decodeURIComponent(parts[1]);
|
|
4636
|
-
let rows = await ql.find(MEM, { where: { environment_id: id } });
|
|
4637
|
-
if (rows && rows.value) rows = rows.value;
|
|
4638
|
-
const members = Array.isArray(rows) ? rows : [];
|
|
4639
|
-
const userIds = Array.from(new Set(members.map((mem) => mem.user_id).filter(Boolean)));
|
|
4640
|
-
const userMap = /* @__PURE__ */ new Map();
|
|
4641
|
-
for (const uid of userIds) {
|
|
4642
|
-
let row = null;
|
|
4643
|
-
for (const tableName of ["sys_user", "user"]) {
|
|
4644
|
-
try {
|
|
4645
|
-
const u = await ql.findOne(tableName, { where: { id: uid } });
|
|
4646
|
-
row = u?.value ?? u;
|
|
4647
|
-
if (row) break;
|
|
4648
|
-
} catch {
|
|
4649
|
-
}
|
|
4650
|
-
}
|
|
4651
|
-
if (row) userMap.set(String(uid), {
|
|
4652
|
-
id: row.id,
|
|
4653
|
-
name: row.name ?? row.display_name,
|
|
4654
|
-
email: row.email,
|
|
4655
|
-
image: row.image ?? row.avatar_url
|
|
4656
|
-
});
|
|
4657
|
-
}
|
|
4658
|
-
const enriched = members.map((mem) => ({
|
|
4659
|
-
...mem,
|
|
4660
|
-
user: userMap.get(String(mem.user_id)) ?? void 0
|
|
4661
|
-
}));
|
|
4662
|
-
return { handled: true, response: this.success({ members: enriched }) };
|
|
4663
|
-
}
|
|
4664
|
-
if (parts.length === 3 && parts[0] === "projects" && parts[2] === "members" && m === "POST") {
|
|
4665
|
-
const id = decodeURIComponent(parts[1]);
|
|
4666
|
-
const project = await findOne(ENV, { id });
|
|
4667
|
-
if (!project) return { handled: true, response: this.error(`Project '${id}' not found`, 404) };
|
|
4668
|
-
const callerId = await this.resolveCallerUserId(_context);
|
|
4669
|
-
if (!callerId) return { handled: true, response: this.error("Authentication required", 401) };
|
|
4670
|
-
const callerMem = await findOne(MEM, { environment_id: id, user_id: callerId });
|
|
4671
|
-
if (!callerMem || !["owner", "admin"].includes(String(callerMem.role))) {
|
|
4672
|
-
return { handled: true, response: this.error("Forbidden \u2014 owner or admin required", 403) };
|
|
4673
|
-
}
|
|
4674
|
-
const email = typeof body?.email === "string" ? String(body.email).trim().toLowerCase() : null;
|
|
4675
|
-
let inviteUserId = typeof body?.user_id === "string" ? String(body.user_id).trim() : null;
|
|
4676
|
-
let role = String(body?.role ?? "member").trim().toLowerCase();
|
|
4677
|
-
if (!["owner", "admin", "member", "viewer"].includes(role)) {
|
|
4678
|
-
return { handled: true, response: this.error(`Invalid role '${role}' (expected owner | admin | member | viewer)`, 400) };
|
|
4679
|
-
}
|
|
4680
|
-
if (!email && !inviteUserId) {
|
|
4681
|
-
return { handled: true, response: this.error("email or user_id is required", 400) };
|
|
4682
|
-
}
|
|
4683
|
-
if (!inviteUserId && email) {
|
|
4684
|
-
let row = null;
|
|
4685
|
-
for (const tableName of ["sys_user", "user"]) {
|
|
4686
|
-
try {
|
|
4687
|
-
const u = await ql.findOne(tableName, { where: { email } });
|
|
4688
|
-
row = u?.value ?? u;
|
|
4689
|
-
if (row) break;
|
|
4690
|
-
} catch {
|
|
4691
|
-
}
|
|
4692
|
-
}
|
|
4693
|
-
if (!row?.id) {
|
|
4694
|
-
return { handled: true, response: this.error(`No user found with email '${email}'`, 404) };
|
|
4695
|
-
}
|
|
4696
|
-
inviteUserId = String(row.id);
|
|
4697
|
-
}
|
|
4698
|
-
const existing = await findOne(MEM, { environment_id: id, user_id: inviteUserId });
|
|
4699
|
-
if (existing) {
|
|
4700
|
-
return { handled: true, response: this.success({ member: existing, alreadyMember: true }) };
|
|
4701
|
-
}
|
|
4702
|
-
try {
|
|
4703
|
-
const memberId = randomUUID();
|
|
4704
|
-
await ql.insert(MEM, {
|
|
4705
|
-
id: memberId,
|
|
4706
|
-
environment_id: id,
|
|
4707
|
-
user_id: inviteUserId,
|
|
4708
|
-
role,
|
|
4709
|
-
invited_by: callerId,
|
|
4710
|
-
organization_id: project.organization_id ?? null
|
|
4711
|
-
});
|
|
4712
|
-
const created = await findOne(MEM, { id: memberId });
|
|
4713
|
-
return { handled: true, response: this.success({ member: created, alreadyMember: false }) };
|
|
4714
|
-
} catch (e) {
|
|
4715
|
-
return { handled: true, response: this.error(e?.message ?? "Failed to add member", 500) };
|
|
4716
|
-
}
|
|
4717
|
-
}
|
|
4718
|
-
if (parts.length === 4 && parts[0] === "projects" && parts[2] === "members" && m === "PATCH") {
|
|
4719
|
-
const id = decodeURIComponent(parts[1]);
|
|
4720
|
-
const memberId = decodeURIComponent(parts[3]);
|
|
4721
|
-
const project = await findOne(ENV, { id });
|
|
4722
|
-
if (!project) return { handled: true, response: this.error(`Project '${id}' not found`, 404) };
|
|
4723
|
-
const callerId = await this.resolveCallerUserId(_context);
|
|
4724
|
-
if (!callerId) return { handled: true, response: this.error("Authentication required", 401) };
|
|
4725
|
-
const callerMem = await findOne(MEM, { environment_id: id, user_id: callerId });
|
|
4726
|
-
if (!callerMem || !["owner", "admin"].includes(String(callerMem.role))) {
|
|
4727
|
-
return { handled: true, response: this.error("Forbidden \u2014 owner or admin required", 403) };
|
|
4728
|
-
}
|
|
4729
|
-
const target = await findOne(MEM, { id: memberId, environment_id: id });
|
|
4730
|
-
if (!target) return { handled: true, response: this.error(`Member '${memberId}' not found`, 404) };
|
|
4731
|
-
const newRole = String(body?.role ?? "").trim().toLowerCase();
|
|
4732
|
-
if (!["owner", "admin", "member", "viewer"].includes(newRole)) {
|
|
4733
|
-
return { handled: true, response: this.error(`Invalid role '${newRole}'`, 400) };
|
|
4734
|
-
}
|
|
4735
|
-
if (target.role === "owner" && newRole !== "owner") {
|
|
4736
|
-
let owners = await ql.find(MEM, { where: { environment_id: id, role: "owner" } });
|
|
4737
|
-
if (owners && owners.value) owners = owners.value;
|
|
4738
|
-
const ownerCount = Array.isArray(owners) ? owners.length : 0;
|
|
4739
|
-
if (ownerCount <= 1) {
|
|
4740
|
-
return { handled: true, response: this.error("Cannot demote the last owner", 409) };
|
|
4741
|
-
}
|
|
4742
|
-
}
|
|
4743
|
-
try {
|
|
4744
|
-
await ql.update(MEM, { role: newRole, updated_at: (/* @__PURE__ */ new Date()).toISOString() }, { where: { id: memberId } });
|
|
4745
|
-
const updated = await findOne(MEM, { id: memberId });
|
|
4746
|
-
return { handled: true, response: this.success({ member: updated }) };
|
|
4747
|
-
} catch (e) {
|
|
4748
|
-
return { handled: true, response: this.error(e?.message ?? "Failed to update role", 500) };
|
|
4749
|
-
}
|
|
4750
|
-
}
|
|
4751
|
-
if (parts.length === 4 && parts[0] === "projects" && parts[2] === "members" && m === "DELETE") {
|
|
4752
|
-
const id = decodeURIComponent(parts[1]);
|
|
4753
|
-
const memberId = decodeURIComponent(parts[3]);
|
|
4754
|
-
const project = await findOne(ENV, { id });
|
|
4755
|
-
if (!project) return { handled: true, response: this.error(`Project '${id}' not found`, 404) };
|
|
4756
|
-
const callerId = await this.resolveCallerUserId(_context);
|
|
4757
|
-
if (!callerId) return { handled: true, response: this.error("Authentication required", 401) };
|
|
4758
|
-
const target = await findOne(MEM, { id: memberId, environment_id: id });
|
|
4759
|
-
if (!target) return { handled: true, response: this.error(`Member '${memberId}' not found`, 404) };
|
|
4760
|
-
const callerMem = await findOne(MEM, { environment_id: id, user_id: callerId });
|
|
4761
|
-
const isSelf = String(target.user_id) === String(callerId);
|
|
4762
|
-
const isPrivileged = callerMem && ["owner", "admin"].includes(String(callerMem.role));
|
|
4763
|
-
if (!isSelf && !isPrivileged) {
|
|
4764
|
-
return { handled: true, response: this.error("Forbidden \u2014 owner or admin required", 403) };
|
|
4765
|
-
}
|
|
4766
|
-
if (target.role === "owner") {
|
|
4767
|
-
let owners = await ql.find(MEM, { where: { environment_id: id, role: "owner" } });
|
|
4768
|
-
if (owners && owners.value) owners = owners.value;
|
|
4769
|
-
const ownerCount = Array.isArray(owners) ? owners.length : 0;
|
|
4770
|
-
if (ownerCount <= 1) {
|
|
4771
|
-
return { handled: true, response: this.error("Cannot remove the last owner", 409) };
|
|
4772
|
-
}
|
|
4773
|
-
}
|
|
4774
|
-
try {
|
|
4775
|
-
await ql.delete(MEM, { where: { id: memberId } });
|
|
4776
|
-
return { handled: true, response: this.success({ removed: true, memberId }) };
|
|
4777
|
-
} catch (e) {
|
|
4778
|
-
return { handled: true, response: this.error(e?.message ?? "Failed to remove member", 500) };
|
|
4779
|
-
}
|
|
4780
|
-
}
|
|
4781
|
-
if (parts.length === 3 && parts[0] === "projects" && parts[2] === "packages" && m === "GET") {
|
|
4782
|
-
const envId = decodeURIComponent(parts[1]);
|
|
4783
|
-
let rows = await ql.find(PKG_INSTALL, { where: { environment_id: envId } });
|
|
4784
|
-
if (rows && rows.value) rows = rows.value;
|
|
4785
|
-
const installs = Array.isArray(rows) ? rows : [];
|
|
4786
|
-
const packages = await Promise.all(
|
|
4787
|
-
installs.map(async (r) => {
|
|
4788
|
-
let manifestId = null;
|
|
4789
|
-
let versionStr = null;
|
|
4790
|
-
try {
|
|
4791
|
-
if (r.package_id) {
|
|
4792
|
-
const pkg = await ql.findOne(PKG, { where: { id: r.package_id } });
|
|
4793
|
-
manifestId = pkg?.manifest_id ?? null;
|
|
4794
|
-
}
|
|
4795
|
-
if (r.package_version_id) {
|
|
4796
|
-
const ver = await ql.findOne(PKG_VERSION, { where: { id: r.package_version_id } });
|
|
4797
|
-
versionStr = ver?.version ?? null;
|
|
4798
|
-
}
|
|
4799
|
-
} catch {
|
|
4800
|
-
}
|
|
4801
|
-
return {
|
|
4802
|
-
...r,
|
|
4803
|
-
// Surface user-facing identifiers expected by client SDK
|
|
4804
|
-
packageId: manifestId,
|
|
4805
|
-
package_id: manifestId ?? r.package_id,
|
|
4806
|
-
version: versionStr ?? r.version ?? null
|
|
4807
|
-
};
|
|
4808
|
-
})
|
|
4809
|
-
);
|
|
4810
|
-
return { handled: true, response: this.success({ packages, total: packages.length }) };
|
|
4811
|
-
}
|
|
4812
|
-
if (parts.length === 3 && parts[0] === "projects" && parts[2] === "packages" && m === "POST") {
|
|
4813
|
-
const envId = decodeURIComponent(parts[1]);
|
|
4814
|
-
const { packageId, version, settings, enableOnInstall } = body ?? {};
|
|
4815
|
-
if (!packageId) return { handled: true, response: this.error("packageId is required", 400) };
|
|
4816
|
-
const qlSvc = await this.getObjectQLService();
|
|
4817
|
-
const pkgRegistry = qlSvc?.registry;
|
|
4818
|
-
const allPkgs = pkgRegistry?.getAllPackages?.() ?? [];
|
|
4819
|
-
const manifestEntry = allPkgs.find((p) => (p?.manifest?.id ?? p?.id) === packageId);
|
|
4820
|
-
const manifest = manifestEntry?.manifest ?? manifestEntry;
|
|
4821
|
-
if (!manifest) {
|
|
4822
|
-
return { handled: true, response: this.error(`Package '${packageId}' is not registered on this server`, 404) };
|
|
4823
|
-
}
|
|
4824
|
-
const CLOUD_SCOPES = /* @__PURE__ */ new Set(["cloud", "system", "platform"]);
|
|
4825
|
-
if (CLOUD_SCOPES.has(manifest?.scope)) {
|
|
4826
|
-
return { handled: true, response: this.error(`Package '${packageId}' has scope=${manifest.scope} and cannot be installed per-project`, 403) };
|
|
4827
|
-
}
|
|
4828
|
-
const projectRow = await findOne(ENV, { id: envId });
|
|
4829
|
-
if (!projectRow) {
|
|
4830
|
-
return { handled: true, response: this.error(`Project '${envId}' not found`, 404) };
|
|
4831
|
-
}
|
|
4832
|
-
const ownerOrgId = projectRow.organization_id ?? "system";
|
|
4833
|
-
let userId = "system";
|
|
4834
|
-
try {
|
|
4835
|
-
const authService = await this.getService(CoreServiceName.enum.auth);
|
|
4836
|
-
const sessionData = await authService?.api?.getSession?.({
|
|
4837
|
-
headers: _context?.request?.headers
|
|
4838
|
-
});
|
|
4839
|
-
userId = sessionData?.user?.id ?? sessionData?.session?.userId ?? "system";
|
|
4840
|
-
} catch {
|
|
4841
|
-
}
|
|
4842
|
-
const resolvedVersion = version ?? manifest?.version ?? "1.0.0";
|
|
4843
|
-
const dup = await ql.findOne(PKG_INSTALL, {
|
|
4844
|
-
where: { environment_id: envId, package_id: packageId }
|
|
4845
|
-
});
|
|
4846
|
-
if (dup?.id) {
|
|
4847
|
-
return { handled: true, response: this.error(`Package '${packageId}' is already installed in this project`, 409) };
|
|
4848
|
-
}
|
|
4849
|
-
const sysPackageId = await ensureSysPackage(packageId, ownerOrgId, userId, manifest);
|
|
4850
|
-
const sysPackageVersionId = await ensureSysPackageVersion(sysPackageId, resolvedVersion, userId, manifest);
|
|
4851
|
-
const nowIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
4852
|
-
const recordId = randomUUID();
|
|
4853
|
-
await ql.insert(PKG_INSTALL, {
|
|
4854
|
-
id: recordId,
|
|
4855
|
-
environment_id: envId,
|
|
4856
|
-
package_id: sysPackageId,
|
|
4857
|
-
package_version_id: sysPackageVersionId,
|
|
4858
|
-
status: "installed",
|
|
4859
|
-
enabled: enableOnInstall !== false,
|
|
4860
|
-
installed_at: nowIso,
|
|
4861
|
-
installed_by: userId,
|
|
4862
|
-
updated_at: nowIso,
|
|
4863
|
-
settings: settings ? JSON.stringify(settings) : null
|
|
4864
|
-
});
|
|
4865
|
-
const record = await ql.findOne(PKG_INSTALL, { where: { id: recordId } });
|
|
4866
|
-
try {
|
|
4867
|
-
await this.kernelManager?.evict(envId);
|
|
4868
|
-
} catch {
|
|
4869
|
-
}
|
|
4870
|
-
return { handled: true, response: this.success({ package: record }) };
|
|
4871
|
-
}
|
|
4872
|
-
if (parts.length === 4 && parts[0] === "projects" && parts[2] === "packages" && m === "GET") {
|
|
4873
|
-
const envId = decodeURIComponent(parts[1]);
|
|
4874
|
-
const pkgId = decodeURIComponent(parts[3]);
|
|
4875
|
-
const record = await ql.findOne(PKG_INSTALL, { where: { environment_id: envId, package_id: pkgId } });
|
|
4876
|
-
if (!record) return { handled: true, response: this.error(`Package '${pkgId}' is not installed in this project`, 404) };
|
|
4877
|
-
return { handled: true, response: this.success({ package: record }) };
|
|
4878
|
-
}
|
|
4879
|
-
if (parts.length === 4 && parts[0] === "projects" && parts[2] === "packages" && m === "DELETE") {
|
|
4880
|
-
const envId = decodeURIComponent(parts[1]);
|
|
4881
|
-
const pkgId = decodeURIComponent(parts[3]);
|
|
4882
|
-
const record = await findInstallByManifestId(envId, pkgId);
|
|
4883
|
-
if (!record) return { handled: true, response: this.error(`Package '${pkgId}' is not installed in this project`, 404) };
|
|
4884
|
-
const allPkgs0 = this.kernel.packages?.getAll?.() ?? [];
|
|
4885
|
-
const m0 = allPkgs0.find((p) => (p.manifest?.id ?? p.id) === pkgId)?.manifest;
|
|
4886
|
-
if (m0?.scope && ["cloud", "system", "platform"].includes(m0.scope)) {
|
|
4887
|
-
return { handled: true, response: this.error(`Package '${pkgId}' with scope=${m0.scope} cannot be uninstalled`, 403) };
|
|
4888
|
-
}
|
|
4889
|
-
await ql.delete(PKG_INSTALL, { where: { id: record.id } });
|
|
4890
|
-
try {
|
|
4891
|
-
await this.kernelManager?.evict(envId);
|
|
4892
|
-
} catch {
|
|
4893
|
-
}
|
|
4894
|
-
return { handled: true, response: this.success({ id: record.id, success: true }) };
|
|
4895
|
-
}
|
|
4896
|
-
if (parts.length === 5 && parts[0] === "projects" && parts[2] === "packages" && parts[4] === "enable" && m === "PATCH") {
|
|
4897
|
-
const envId = decodeURIComponent(parts[1]);
|
|
4898
|
-
const pkgId = decodeURIComponent(parts[3]);
|
|
4899
|
-
const record = await findInstallByManifestId(envId, pkgId);
|
|
4900
|
-
if (!record) return { handled: true, response: this.error(`Package '${pkgId}' is not installed in this project`, 404) };
|
|
4901
|
-
const nowIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
4902
|
-
await ql.update(PKG_INSTALL, { enabled: true, status: "installed", updated_at: nowIso }, { where: { id: record.id } });
|
|
4903
|
-
const updated = await ql.findOne(PKG_INSTALL, { where: { id: record.id } });
|
|
4904
|
-
try {
|
|
4905
|
-
await this.kernelManager?.evict(envId);
|
|
4906
|
-
} catch {
|
|
4907
|
-
}
|
|
4908
|
-
return { handled: true, response: this.success({ package: updated }) };
|
|
4909
|
-
}
|
|
4910
|
-
if (parts.length === 5 && parts[0] === "projects" && parts[2] === "packages" && parts[4] === "disable" && m === "PATCH") {
|
|
4911
|
-
const envId = decodeURIComponent(parts[1]);
|
|
4912
|
-
const pkgId = decodeURIComponent(parts[3]);
|
|
4913
|
-
const record = await findInstallByManifestId(envId, pkgId);
|
|
4914
|
-
if (!record) return { handled: true, response: this.error(`Package '${pkgId}' is not installed in this project`, 404) };
|
|
4915
|
-
const allPkgs1 = this.kernel.packages?.getAll?.() ?? [];
|
|
4916
|
-
const m1 = allPkgs1.find((p) => (p.manifest?.id ?? p.id) === pkgId)?.manifest;
|
|
4917
|
-
if (m1?.scope && ["cloud", "system", "platform"].includes(m1.scope)) {
|
|
4918
|
-
return { handled: true, response: this.error(`Package '${pkgId}' with scope=${m1.scope} cannot be disabled`, 403) };
|
|
4919
|
-
}
|
|
4920
|
-
const nowIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
4921
|
-
await ql.update(PKG_INSTALL, { enabled: false, status: "disabled", updated_at: nowIso }, { where: { id: record.id } });
|
|
4922
|
-
const updated = await ql.findOne(PKG_INSTALL, { where: { id: record.id } });
|
|
4923
|
-
try {
|
|
4924
|
-
await this.kernelManager?.evict(envId);
|
|
4925
|
-
} catch {
|
|
4926
|
-
}
|
|
4927
|
-
return { handled: true, response: this.success({ package: updated }) };
|
|
4928
|
-
}
|
|
4929
|
-
if (parts.length === 5 && parts[0] === "projects" && parts[2] === "packages" && parts[4] === "upgrade" && m === "POST") {
|
|
4930
|
-
const envId = decodeURIComponent(parts[1]);
|
|
4931
|
-
const pkgId = decodeURIComponent(parts[3]);
|
|
4932
|
-
const record = await findInstallByManifestId(envId, pkgId);
|
|
4933
|
-
if (!record) return { handled: true, response: this.error(`Package '${pkgId}' is not installed in this project`, 404) };
|
|
4934
|
-
const { targetVersion } = body ?? {};
|
|
4935
|
-
const allPkgs2 = this.kernel.packages?.getAll?.() ?? [];
|
|
4936
|
-
const manifest2 = allPkgs2.find((p) => (p.manifest?.id ?? p.id) === pkgId)?.manifest;
|
|
4937
|
-
const currentVer = await ql.findOne(PKG_VERSION, { where: { id: record.package_version_id } });
|
|
4938
|
-
const newVersion = targetVersion ?? manifest2?.version ?? currentVer?.version ?? "1.0.0";
|
|
4939
|
-
if (newVersion === currentVer?.version) {
|
|
4940
|
-
return { handled: true, response: this.success({ package: record, message: "Already at target version" }) };
|
|
4941
|
-
}
|
|
4942
|
-
let userId = "system";
|
|
4943
|
-
try {
|
|
4944
|
-
const authService = await this.getService(CoreServiceName.enum.auth);
|
|
4945
|
-
const sessionData = await authService?.api?.getSession?.({
|
|
4946
|
-
headers: _context?.request?.headers
|
|
4947
|
-
});
|
|
4948
|
-
userId = sessionData?.user?.id ?? "system";
|
|
4949
|
-
} catch {
|
|
4950
|
-
}
|
|
4951
|
-
const newVersionId = await ensureSysPackageVersion(record.package_id, newVersion, userId, manifest2);
|
|
4952
|
-
const nowIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
4953
|
-
await ql.update(PKG_INSTALL, {
|
|
4954
|
-
package_version_id: newVersionId,
|
|
4955
|
-
status: "installed",
|
|
4956
|
-
updated_at: nowIso
|
|
4957
|
-
}, { where: { id: record.id } });
|
|
4958
|
-
const updated = await ql.findOne(PKG_INSTALL, { where: { id: record.id } });
|
|
4959
|
-
try {
|
|
4960
|
-
await this.kernelManager?.evict(envId);
|
|
4961
|
-
} catch {
|
|
4962
|
-
}
|
|
4963
|
-
return { handled: true, response: this.success({ package: updated }) };
|
|
4964
|
-
}
|
|
4965
|
-
} catch (e) {
|
|
4966
|
-
return { handled: true, response: this.error(e.message, e.statusCode || 500) };
|
|
4967
|
-
}
|
|
4968
|
-
return { handled: false };
|
|
4969
|
-
}
|
|
4970
|
-
/**
|
|
4971
|
-
* Cascade-delete a project: cred / member / package_installation rows,
|
|
4972
|
-
* then the physical database via the provisioning adapter, then the
|
|
4973
|
-
* `sys_environment` row itself. Used by both `DELETE /cloud/environments/:id`
|
|
4974
|
-
* and the org-cascade in `DELETE /cloud/organizations/:id`.
|
|
3936
|
+
* - GET /cloud/environments/:id/members → list members
|
|
3937
|
+
* - GET /cloud/environments/:id/packages → list installed packages
|
|
3938
|
+
* - POST /cloud/environments/:id/packages → install package into env
|
|
3939
|
+
* - GET /cloud/environments/:id/packages/:pkgId → get installation detail
|
|
3940
|
+
* - PATCH /cloud/environments/:id/packages/:pkgId/enable → enable package
|
|
3941
|
+
* - PATCH /cloud/environments/:id/packages/:pkgId/disable → disable package
|
|
3942
|
+
* - DELETE /cloud/environments/:id/packages/:pkgId → uninstall (scope=platform forbidden)
|
|
3943
|
+
* - POST /cloud/environments/:id/packages/:pkgId/upgrade → upgrade to newer version
|
|
3944
|
+
*
|
|
3945
|
+
* Driver binding
|
|
3946
|
+
* --------------
|
|
3947
|
+
* Environments are not tied to any specific driver. At provisioning time the
|
|
3948
|
+
* caller passes `driver` (a short name such as `memory`, `turso`, or any
|
|
3949
|
+
* future `sql` / `postgres` driver). The dispatcher validates the name
|
|
3950
|
+
* against the kernel's registered driver services (`driver.<name>`) and
|
|
3951
|
+
* derives an appropriate placeholder `database_url` for the chosen driver.
|
|
3952
|
+
* If `driver` is omitted, the dispatcher auto-selects the first available
|
|
3953
|
+
* in preference order: turso → memory → any other registered driver.
|
|
4975
3954
|
*
|
|
4976
|
-
*
|
|
4977
|
-
*
|
|
4978
|
-
*
|
|
3955
|
+
* Backed by ObjectQL sys_environment / sys_environment_credential /
|
|
3956
|
+
* sys_environment_member tables (registered by
|
|
3957
|
+
* `@objectstack/service-tenant`'s `createTenantPlugin`).
|
|
3958
|
+
* Physical database addressing (database_url, database_driver, etc.)
|
|
3959
|
+
* is stored directly on the sys_environment row.
|
|
4979
3960
|
*/
|
|
4980
|
-
|
|
4981
|
-
|
|
4982
|
-
|
|
4983
|
-
|
|
4984
|
-
|
|
4985
|
-
|
|
4986
|
-
|
|
4987
|
-
|
|
4988
|
-
|
|
4989
|
-
|
|
4990
|
-
|
|
4991
|
-
|
|
4992
|
-
|
|
4993
|
-
|
|
4994
|
-
|
|
4995
|
-
error: `Project '${environmentId}' is the default project for its organization. Pass ?force=1 to delete it.`,
|
|
4996
|
-
warnings
|
|
4997
|
-
};
|
|
4998
|
-
}
|
|
4999
|
-
const cascade = [
|
|
5000
|
-
{ object: "sys_environment_credential", field: "environment_id" },
|
|
5001
|
-
{ object: "sys_environment_member", field: "environment_id" },
|
|
5002
|
-
{ object: "sys_package_installation", field: "environment_id" }
|
|
5003
|
-
];
|
|
5004
|
-
for (const { object, field } of cascade) {
|
|
5005
|
-
try {
|
|
5006
|
-
let rows = await ql.find(object, { where: { [field]: environmentId } });
|
|
5007
|
-
if (rows && rows.value) rows = rows.value;
|
|
5008
|
-
if (Array.isArray(rows)) {
|
|
5009
|
-
for (const r of rows) {
|
|
5010
|
-
if (r?.id != null) {
|
|
5011
|
-
try {
|
|
5012
|
-
await ql.delete(object, { where: { id: r.id } });
|
|
5013
|
-
} catch (innerErr) {
|
|
5014
|
-
warnings.push(
|
|
5015
|
-
`Failed to delete ${object} ${r.id}: ${innerErr instanceof Error ? innerErr.message : String(innerErr)}`
|
|
5016
|
-
);
|
|
5017
|
-
}
|
|
5018
|
-
}
|
|
3961
|
+
/**
|
|
3962
|
+
* Resolve the calling user id from the request session, if any.
|
|
3963
|
+
* Returns `undefined` for anonymous calls or when auth is not wired up.
|
|
3964
|
+
*/
|
|
3965
|
+
async resolveActiveOrganizationId(context) {
|
|
3966
|
+
try {
|
|
3967
|
+
const authService = await this.resolveService(CoreServiceName.enum.auth);
|
|
3968
|
+
const rawHeaders = context.request?.headers;
|
|
3969
|
+
let headers = rawHeaders;
|
|
3970
|
+
if (rawHeaders && typeof rawHeaders === "object" && typeof rawHeaders.get !== "function") {
|
|
3971
|
+
try {
|
|
3972
|
+
const h = new Headers();
|
|
3973
|
+
for (const [k, v] of Object.entries(rawHeaders)) {
|
|
3974
|
+
if (v == null) continue;
|
|
3975
|
+
h.set(k, Array.isArray(v) ? v.join(", ") : String(v));
|
|
5019
3976
|
}
|
|
3977
|
+
headers = h;
|
|
3978
|
+
} catch {
|
|
3979
|
+
headers = rawHeaders;
|
|
5020
3980
|
}
|
|
5021
|
-
} catch (err) {
|
|
5022
|
-
warnings.push(
|
|
5023
|
-
`Failed to enumerate ${object} for project ${environmentId}: ${err instanceof Error ? err.message : String(err)}`
|
|
5024
|
-
);
|
|
5025
|
-
}
|
|
5026
|
-
}
|
|
5027
|
-
const driver = row.database_driver ?? "memory";
|
|
5028
|
-
const databaseUrl = row.database_url;
|
|
5029
|
-
const databaseName = `p-${String(environmentId).replace(/-/g, "").slice(0, 24)}`;
|
|
5030
|
-
try {
|
|
5031
|
-
const adapter = await getRealAdapter(driver);
|
|
5032
|
-
if (adapter?.deleteDatabase) {
|
|
5033
|
-
await adapter.deleteDatabase({ environmentId, databaseName, databaseUrl });
|
|
5034
|
-
} else {
|
|
5035
|
-
warnings.push(`No adapter for driver '${driver}'; physical DB for project ${environmentId} not released.`);
|
|
5036
|
-
}
|
|
5037
|
-
} catch (err) {
|
|
5038
|
-
warnings.push(
|
|
5039
|
-
`Failed to delete physical database for project ${environmentId}: ${err instanceof Error ? err.message : String(err)}`
|
|
5040
|
-
);
|
|
5041
|
-
}
|
|
5042
|
-
try {
|
|
5043
|
-
await ql.delete(ENV, { where: { id: environmentId } });
|
|
5044
|
-
} catch (err) {
|
|
5045
|
-
return {
|
|
5046
|
-
ok: false,
|
|
5047
|
-
status: 500,
|
|
5048
|
-
error: `Failed to delete sys_environment row: ${err instanceof Error ? err.message : String(err)}`,
|
|
5049
|
-
warnings
|
|
5050
|
-
};
|
|
5051
|
-
}
|
|
5052
|
-
if (this.envRegistry?.invalidate) {
|
|
5053
|
-
try {
|
|
5054
|
-
await this.envRegistry.invalidate(environmentId);
|
|
5055
|
-
} catch {
|
|
5056
3981
|
}
|
|
3982
|
+
const apiObj = authService?.auth?.api ?? authService?.api;
|
|
3983
|
+
const sessionData = await apiObj?.getSession?.call(apiObj, { headers });
|
|
3984
|
+
const oid = sessionData?.session?.activeOrganizationId;
|
|
3985
|
+
return typeof oid === "string" && oid.length > 0 ? oid : void 0;
|
|
3986
|
+
} catch {
|
|
3987
|
+
return void 0;
|
|
5057
3988
|
}
|
|
5058
|
-
return { ok: true, warnings };
|
|
5059
3989
|
}
|
|
5060
3990
|
/**
|
|
5061
3991
|
* Handles Storage requests
|
|
@@ -5127,6 +4057,8 @@ var _HttpDispatcher = class _HttpDispatcher {
|
|
|
5127
4057
|
*
|
|
5128
4058
|
* Routes:
|
|
5129
4059
|
* GET / → listFlows
|
|
4060
|
+
* GET /actions → getActionDescriptors (ADR-0018; ?paradigm/?source/?category filters)
|
|
4061
|
+
* GET /connectors → getConnectorDescriptors (ADR-0022; ?type filter)
|
|
5130
4062
|
* GET /:name → getFlow
|
|
5131
4063
|
* POST / → createFlow (registerFlow)
|
|
5132
4064
|
* PUT /:name → updateFlow
|
|
@@ -5135,6 +4067,8 @@ var _HttpDispatcher = class _HttpDispatcher {
|
|
|
5135
4067
|
* POST /:name/toggle → toggleFlow
|
|
5136
4068
|
* GET /:name/runs → listRuns
|
|
5137
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
|
|
5138
4072
|
*/
|
|
5139
4073
|
async handleAutomation(path, method, body, context, query) {
|
|
5140
4074
|
const automationService = await this.getService(CoreServiceName.enum.automation);
|
|
@@ -5164,6 +4098,32 @@ var _HttpDispatcher = class _HttpDispatcher {
|
|
|
5164
4098
|
return { handled: true, response: this.success(body) };
|
|
5165
4099
|
}
|
|
5166
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);
|
|
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 }) };
|
|
4114
|
+
}
|
|
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 }) };
|
|
4124
|
+
}
|
|
4125
|
+
return { handled: true, response: this.success({ connectors: [], total: 0 }) };
|
|
4126
|
+
}
|
|
5167
4127
|
if (parts.length >= 1) {
|
|
5168
4128
|
const name = parts[0];
|
|
5169
4129
|
if (parts[1] === "trigger" && m === "POST") {
|
|
@@ -5203,7 +4163,28 @@ var _HttpDispatcher = class _HttpDispatcher {
|
|
|
5203
4163
|
return { handled: true, response: this.success({ name, enabled: body?.enabled ?? true }) };
|
|
5204
4164
|
}
|
|
5205
4165
|
}
|
|
5206
|
-
if (parts[1] === "runs" && parts[2] && m === "
|
|
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) };
|
|
4176
|
+
}
|
|
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 }) };
|
|
4184
|
+
}
|
|
4185
|
+
return { handled: true, response: this.error("Screen lookup not supported", 501) };
|
|
4186
|
+
}
|
|
4187
|
+
if (parts[1] === "runs" && parts[2] && !parts[3] && m === "GET") {
|
|
5207
4188
|
if (typeof automationService.getRun === "function") {
|
|
5208
4189
|
const run = await automationService.getRun(parts[2]);
|
|
5209
4190
|
if (!run) return { handled: true, response: this.error("Execution not found", 404) };
|
|
@@ -5529,11 +4510,9 @@ var _HttpDispatcher = class _HttpDispatcher {
|
|
|
5529
4510
|
if (forbidden) {
|
|
5530
4511
|
return { handled: true, response: forbidden };
|
|
5531
4512
|
}
|
|
5532
|
-
|
|
5533
|
-
|
|
5534
|
-
|
|
5535
|
-
cleanPath = scopedMatch[1] ?? "";
|
|
5536
|
-
}
|
|
4513
|
+
const scopedMatch = cleanPath.match(/^\/projects\/[^/]+(\/.*)?$/);
|
|
4514
|
+
if (scopedMatch) {
|
|
4515
|
+
cleanPath = scopedMatch[1] ?? "";
|
|
5537
4516
|
}
|
|
5538
4517
|
try {
|
|
5539
4518
|
if ((cleanPath === "/discovery" || cleanPath === "") && method === "GET") {
|
|
@@ -5584,9 +4563,6 @@ var _HttpDispatcher = class _HttpDispatcher {
|
|
|
5584
4563
|
if (cleanPath.startsWith("/packages")) {
|
|
5585
4564
|
return this.handlePackages(cleanPath.substring(9), method, body, query, context);
|
|
5586
4565
|
}
|
|
5587
|
-
if (cleanPath.startsWith("/cloud")) {
|
|
5588
|
-
return this.handleCloud(cleanPath.substring(6), method, body, query, context);
|
|
5589
|
-
}
|
|
5590
4566
|
if (cleanPath.startsWith("/i18n")) {
|
|
5591
4567
|
return this.handleI18n(cleanPath.substring(5), method, query, context);
|
|
5592
4568
|
}
|
|
@@ -6291,257 +5267,57 @@ function createDispatcherPlugin(config = {}) {
|
|
|
6291
5267
|
errorResponse(err, res);
|
|
6292
5268
|
}
|
|
6293
5269
|
});
|
|
6294
|
-
server.get(`${prefix}/packages/:id`, async (req, res) => {
|
|
6295
|
-
try {
|
|
6296
|
-
const result = await dispatcher.handlePackages(`/${req.params.id}`, "GET", {}, req.query, { request: req });
|
|
6297
|
-
sendResult(result, res);
|
|
6298
|
-
} catch (err) {
|
|
6299
|
-
errorResponse(err, res);
|
|
6300
|
-
}
|
|
6301
|
-
});
|
|
6302
|
-
server.delete(`${prefix}/packages/:id`, async (req, res) => {
|
|
6303
|
-
try {
|
|
6304
|
-
const result = await dispatcher.handlePackages(`/${req.params.id}`, "DELETE", {}, {}, { request: req });
|
|
6305
|
-
sendResult(result, res);
|
|
6306
|
-
} catch (err) {
|
|
6307
|
-
errorResponse(err, res);
|
|
6308
|
-
}
|
|
6309
|
-
});
|
|
6310
|
-
server.patch(`${prefix}/packages/:id/enable`, async (req, res) => {
|
|
6311
|
-
try {
|
|
6312
|
-
const result = await dispatcher.handlePackages(`/${req.params.id}/enable`, "PATCH", {}, {}, { request: req });
|
|
6313
|
-
sendResult(result, res);
|
|
6314
|
-
} catch (err) {
|
|
6315
|
-
errorResponse(err, res);
|
|
6316
|
-
}
|
|
6317
|
-
});
|
|
6318
|
-
server.patch(`${prefix}/packages/:id/disable`, async (req, res) => {
|
|
6319
|
-
try {
|
|
6320
|
-
const result = await dispatcher.handlePackages(`/${req.params.id}/disable`, "PATCH", {}, {}, { request: req });
|
|
6321
|
-
sendResult(result, res);
|
|
6322
|
-
} catch (err) {
|
|
6323
|
-
errorResponse(err, res);
|
|
6324
|
-
}
|
|
6325
|
-
});
|
|
6326
|
-
server.post(`${prefix}/packages/:id/publish`, async (req, res) => {
|
|
6327
|
-
try {
|
|
6328
|
-
const result = await dispatcher.handlePackages(`/${req.params.id}/publish`, "POST", req.body, {}, { request: req });
|
|
6329
|
-
sendResult(result, res);
|
|
6330
|
-
} catch (err) {
|
|
6331
|
-
errorResponse(err, res);
|
|
6332
|
-
}
|
|
6333
|
-
});
|
|
6334
|
-
server.post(`${prefix}/packages/:id/revert`, async (req, res) => {
|
|
6335
|
-
try {
|
|
6336
|
-
const result = await dispatcher.handlePackages(`/${req.params.id}/revert`, "POST", req.body, {}, { request: req });
|
|
6337
|
-
sendResult(result, res);
|
|
6338
|
-
} catch (err) {
|
|
6339
|
-
errorResponse(err, res);
|
|
6340
|
-
}
|
|
6341
|
-
});
|
|
6342
|
-
server.get(`${prefix}/cloud/drivers`, async (req, res) => {
|
|
6343
|
-
try {
|
|
6344
|
-
const result = await dispatcher.handleCloud("/drivers", "GET", {}, req.query, { request: req });
|
|
6345
|
-
sendResult(result, res);
|
|
6346
|
-
} catch (err) {
|
|
6347
|
-
errorResponse(err, res);
|
|
6348
|
-
}
|
|
6349
|
-
});
|
|
6350
|
-
server.post(`${prefix}/cloud/admin/platform-sso/backfill`, async (req, res) => {
|
|
6351
|
-
try {
|
|
6352
|
-
const result = await dispatcher.handleCloud("/admin/platform-sso/backfill", "POST", req.body, req.query, { request: req });
|
|
6353
|
-
sendResult(result, res);
|
|
6354
|
-
} catch (err) {
|
|
6355
|
-
errorResponse(err, res);
|
|
6356
|
-
}
|
|
6357
|
-
});
|
|
6358
|
-
server.get(`${prefix}/cloud/templates`, async (req, res) => {
|
|
6359
|
-
try {
|
|
6360
|
-
const result = await dispatcher.handleCloud("/templates", "GET", {}, req.query, { request: req });
|
|
6361
|
-
sendResult(result, res);
|
|
6362
|
-
} catch (err) {
|
|
6363
|
-
errorResponse(err, res);
|
|
6364
|
-
}
|
|
6365
|
-
});
|
|
6366
|
-
server.get(`${prefix}/cloud/environments`, async (req, res) => {
|
|
6367
|
-
try {
|
|
6368
|
-
const result = await dispatcher.handleCloud("/projects", "GET", {}, req.query, { request: req });
|
|
6369
|
-
sendResult(result, res);
|
|
6370
|
-
} catch (err) {
|
|
6371
|
-
errorResponse(err, res);
|
|
6372
|
-
}
|
|
6373
|
-
});
|
|
6374
|
-
server.post(`${prefix}/cloud/environments`, async (req, res) => {
|
|
6375
|
-
try {
|
|
6376
|
-
const result = await dispatcher.handleCloud("/projects", "POST", req.body, {}, { request: req });
|
|
6377
|
-
sendResult(result, res);
|
|
6378
|
-
} catch (err) {
|
|
6379
|
-
errorResponse(err, res);
|
|
6380
|
-
}
|
|
6381
|
-
});
|
|
6382
|
-
server.get(`${prefix}/cloud/environments/:id`, async (req, res) => {
|
|
6383
|
-
try {
|
|
6384
|
-
const result = await dispatcher.handleCloud(`/projects/${req.params.id}`, "GET", {}, req.query, { request: req });
|
|
6385
|
-
sendResult(result, res);
|
|
6386
|
-
} catch (err) {
|
|
6387
|
-
errorResponse(err, res);
|
|
6388
|
-
}
|
|
6389
|
-
});
|
|
6390
|
-
server.patch(`${prefix}/cloud/environments/:id`, async (req, res) => {
|
|
6391
|
-
try {
|
|
6392
|
-
const result = await dispatcher.handleCloud(`/projects/${req.params.id}`, "PATCH", req.body, {}, { request: req });
|
|
6393
|
-
sendResult(result, res);
|
|
6394
|
-
} catch (err) {
|
|
6395
|
-
errorResponse(err, res);
|
|
6396
|
-
}
|
|
6397
|
-
});
|
|
6398
|
-
server.delete(`${prefix}/cloud/environments/:id`, async (req, res) => {
|
|
6399
|
-
try {
|
|
6400
|
-
const result = await dispatcher.handleCloud(`/projects/${req.params.id}`, "DELETE", {}, req.query, { request: req });
|
|
6401
|
-
sendResult(result, res);
|
|
6402
|
-
} catch (err) {
|
|
6403
|
-
errorResponse(err, res);
|
|
6404
|
-
}
|
|
6405
|
-
});
|
|
6406
|
-
server.delete(`${prefix}/cloud/organizations/:id`, async (req, res) => {
|
|
6407
|
-
try {
|
|
6408
|
-
const result = await dispatcher.handleCloud(`/organizations/${req.params.id}`, "DELETE", {}, req.query, { request: req });
|
|
6409
|
-
sendResult(result, res);
|
|
6410
|
-
} catch (err) {
|
|
6411
|
-
errorResponse(err, res);
|
|
6412
|
-
}
|
|
6413
|
-
});
|
|
6414
|
-
server.post(`${prefix}/cloud/environments/:id/hostname`, async (req, res) => {
|
|
6415
|
-
try {
|
|
6416
|
-
const result = await dispatcher.handleCloud(`/projects/${req.params.id}/hostname`, "POST", req.body, {}, { request: req });
|
|
6417
|
-
sendResult(result, res);
|
|
6418
|
-
} catch (err) {
|
|
6419
|
-
errorResponse(err, res);
|
|
6420
|
-
}
|
|
6421
|
-
});
|
|
6422
|
-
server.put(`${prefix}/cloud/environments/:id/hostname`, async (req, res) => {
|
|
6423
|
-
try {
|
|
6424
|
-
const result = await dispatcher.handleCloud(`/projects/${req.params.id}/hostname`, "PUT", req.body, {}, { request: req });
|
|
6425
|
-
sendResult(result, res);
|
|
6426
|
-
} catch (err) {
|
|
6427
|
-
errorResponse(err, res);
|
|
6428
|
-
}
|
|
6429
|
-
});
|
|
6430
|
-
server.post(`${prefix}/cloud/environments/:id/rotate-credential`, async (req, res) => {
|
|
6431
|
-
try {
|
|
6432
|
-
const result = await dispatcher.handleCloud(`/projects/${req.params.id}/rotate-credential`, "POST", req.body, {}, { request: req });
|
|
6433
|
-
sendResult(result, res);
|
|
6434
|
-
} catch (err) {
|
|
6435
|
-
errorResponse(err, res);
|
|
6436
|
-
}
|
|
6437
|
-
});
|
|
6438
|
-
server.post(`${prefix}/cloud/environments/:id/credentials/rotate`, async (req, res) => {
|
|
6439
|
-
try {
|
|
6440
|
-
const result = await dispatcher.handleCloud(`/projects/${req.params.id}/credentials/rotate`, "POST", req.body, {}, { request: req });
|
|
6441
|
-
sendResult(result, res);
|
|
6442
|
-
} catch (err) {
|
|
6443
|
-
errorResponse(err, res);
|
|
6444
|
-
}
|
|
6445
|
-
});
|
|
6446
|
-
server.post(`${prefix}/cloud/environments/:id/activate`, async (req, res) => {
|
|
6447
|
-
try {
|
|
6448
|
-
const result = await dispatcher.handleCloud(`/projects/${req.params.id}/activate`, "POST", req.body, {}, { request: req });
|
|
6449
|
-
sendResult(result, res);
|
|
6450
|
-
} catch (err) {
|
|
6451
|
-
errorResponse(err, res);
|
|
6452
|
-
}
|
|
6453
|
-
});
|
|
6454
|
-
server.post(`${prefix}/cloud/environments/:id/retry`, async (req, res) => {
|
|
6455
|
-
try {
|
|
6456
|
-
const result = await dispatcher.handleCloud(`/projects/${req.params.id}/retry`, "POST", req.body, {}, { request: req });
|
|
6457
|
-
sendResult(result, res);
|
|
6458
|
-
} catch (err) {
|
|
6459
|
-
errorResponse(err, res);
|
|
6460
|
-
}
|
|
6461
|
-
});
|
|
6462
|
-
server.get(`${prefix}/cloud/environments/:id/members`, async (req, res) => {
|
|
6463
|
-
try {
|
|
6464
|
-
const result = await dispatcher.handleCloud(`/projects/${req.params.id}/members`, "GET", {}, req.query, { request: req });
|
|
6465
|
-
sendResult(result, res);
|
|
6466
|
-
} catch (err) {
|
|
6467
|
-
errorResponse(err, res);
|
|
6468
|
-
}
|
|
6469
|
-
});
|
|
6470
|
-
server.post(`${prefix}/cloud/environments/:id/members`, async (req, res) => {
|
|
6471
|
-
try {
|
|
6472
|
-
const result = await dispatcher.handleCloud(`/projects/${req.params.id}/members`, "POST", req.body, {}, { request: req });
|
|
6473
|
-
sendResult(result, res);
|
|
6474
|
-
} catch (err) {
|
|
6475
|
-
errorResponse(err, res);
|
|
6476
|
-
}
|
|
6477
|
-
});
|
|
6478
|
-
server.patch(`${prefix}/cloud/environments/:id/members/:memberId`, async (req, res) => {
|
|
6479
|
-
try {
|
|
6480
|
-
const result = await dispatcher.handleCloud(`/projects/${req.params.id}/members/${req.params.memberId}`, "PATCH", req.body, {}, { request: req });
|
|
6481
|
-
sendResult(result, res);
|
|
6482
|
-
} catch (err) {
|
|
6483
|
-
errorResponse(err, res);
|
|
6484
|
-
}
|
|
6485
|
-
});
|
|
6486
|
-
server.delete(`${prefix}/cloud/environments/:id/members/:memberId`, async (req, res) => {
|
|
6487
|
-
try {
|
|
6488
|
-
const result = await dispatcher.handleCloud(`/projects/${req.params.id}/members/${req.params.memberId}`, "DELETE", req.body ?? {}, {}, { request: req });
|
|
6489
|
-
sendResult(result, res);
|
|
6490
|
-
} catch (err) {
|
|
6491
|
-
errorResponse(err, res);
|
|
6492
|
-
}
|
|
6493
|
-
});
|
|
6494
|
-
server.get(`${prefix}/cloud/environments/:id/packages`, async (req, res) => {
|
|
5270
|
+
server.get(`${prefix}/packages/:id/export`, async (req, res) => {
|
|
6495
5271
|
try {
|
|
6496
|
-
const result = await dispatcher.
|
|
5272
|
+
const result = await dispatcher.handlePackages(`/${req.params.id}/export`, "GET", {}, req.query, { request: req });
|
|
6497
5273
|
sendResult(result, res);
|
|
6498
5274
|
} catch (err) {
|
|
6499
5275
|
errorResponse(err, res);
|
|
6500
5276
|
}
|
|
6501
5277
|
});
|
|
6502
|
-
server.
|
|
5278
|
+
server.get(`${prefix}/packages/:id`, async (req, res) => {
|
|
6503
5279
|
try {
|
|
6504
|
-
const result = await dispatcher.
|
|
5280
|
+
const result = await dispatcher.handlePackages(`/${req.params.id}`, "GET", {}, req.query, { request: req });
|
|
6505
5281
|
sendResult(result, res);
|
|
6506
5282
|
} catch (err) {
|
|
6507
5283
|
errorResponse(err, res);
|
|
6508
5284
|
}
|
|
6509
5285
|
});
|
|
6510
|
-
server.
|
|
5286
|
+
server.delete(`${prefix}/packages/:id`, async (req, res) => {
|
|
6511
5287
|
try {
|
|
6512
|
-
const result = await dispatcher.
|
|
5288
|
+
const result = await dispatcher.handlePackages(`/${req.params.id}`, "DELETE", {}, {}, { request: req });
|
|
6513
5289
|
sendResult(result, res);
|
|
6514
5290
|
} catch (err) {
|
|
6515
5291
|
errorResponse(err, res);
|
|
6516
5292
|
}
|
|
6517
5293
|
});
|
|
6518
|
-
server.
|
|
5294
|
+
server.patch(`${prefix}/packages/:id/enable`, async (req, res) => {
|
|
6519
5295
|
try {
|
|
6520
|
-
const result = await dispatcher.
|
|
5296
|
+
const result = await dispatcher.handlePackages(`/${req.params.id}/enable`, "PATCH", {}, {}, { request: req });
|
|
6521
5297
|
sendResult(result, res);
|
|
6522
5298
|
} catch (err) {
|
|
6523
5299
|
errorResponse(err, res);
|
|
6524
5300
|
}
|
|
6525
5301
|
});
|
|
6526
|
-
server.patch(`${prefix}/
|
|
5302
|
+
server.patch(`${prefix}/packages/:id/disable`, async (req, res) => {
|
|
6527
5303
|
try {
|
|
6528
|
-
const result = await dispatcher.
|
|
5304
|
+
const result = await dispatcher.handlePackages(`/${req.params.id}/disable`, "PATCH", {}, {}, { request: req });
|
|
6529
5305
|
sendResult(result, res);
|
|
6530
5306
|
} catch (err) {
|
|
6531
5307
|
errorResponse(err, res);
|
|
6532
5308
|
}
|
|
6533
5309
|
});
|
|
6534
|
-
server.
|
|
5310
|
+
server.post(`${prefix}/packages/:id/publish`, async (req, res) => {
|
|
6535
5311
|
try {
|
|
6536
|
-
const result = await dispatcher.
|
|
5312
|
+
const result = await dispatcher.handlePackages(`/${req.params.id}/publish`, "POST", req.body, {}, { request: req });
|
|
6537
5313
|
sendResult(result, res);
|
|
6538
5314
|
} catch (err) {
|
|
6539
5315
|
errorResponse(err, res);
|
|
6540
5316
|
}
|
|
6541
5317
|
});
|
|
6542
|
-
server.post(`${prefix}/
|
|
5318
|
+
server.post(`${prefix}/packages/:id/revert`, async (req, res) => {
|
|
6543
5319
|
try {
|
|
6544
|
-
const result = await dispatcher.
|
|
5320
|
+
const result = await dispatcher.handlePackages(`/${req.params.id}/revert`, "POST", req.body, {}, { request: req });
|
|
6545
5321
|
sendResult(result, res);
|
|
6546
5322
|
} catch (err) {
|
|
6547
5323
|
errorResponse(err, res);
|
|
@@ -6668,6 +5444,22 @@ function createDispatcherPlugin(config = {}) {
|
|
|
6668
5444
|
errorResponse(err, res);
|
|
6669
5445
|
}
|
|
6670
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
|
+
});
|
|
6671
5463
|
};
|
|
6672
5464
|
const registerAIRoutes = (base) => {
|
|
6673
5465
|
const wildcards = [
|
|
@@ -7610,19 +6402,15 @@ init_driver_plugin();
|
|
|
7610
6402
|
init_app_plugin();
|
|
7611
6403
|
import { createHmac as createHmac2 } from "crypto";
|
|
7612
6404
|
import { ObjectKernel as ObjectKernel3 } from "@objectstack/core";
|
|
7613
|
-
import { readEnvWithDeprecation as
|
|
6405
|
+
import { readEnvWithDeprecation as readEnvWithDeprecation3 } from "@objectstack/types";
|
|
7614
6406
|
|
|
7615
6407
|
// src/cloud/capability-loader.ts
|
|
7616
6408
|
var CAPABILITY_PROVIDERS = {
|
|
7617
6409
|
automation: {
|
|
6410
|
+
// Self-contained: AutomationServicePlugin seeds all built-in node
|
|
6411
|
+
// executors itself (ADR-0018), so no companion node-pack plugins.
|
|
7618
6412
|
pkg: "@objectstack/service-automation",
|
|
7619
|
-
export: "AutomationServicePlugin"
|
|
7620
|
-
extras: [
|
|
7621
|
-
{ pkg: "@objectstack/service-automation", export: "CrudNodesPlugin" },
|
|
7622
|
-
{ pkg: "@objectstack/service-automation", export: "LogicNodesPlugin" },
|
|
7623
|
-
{ pkg: "@objectstack/service-automation", export: "HttpConnectorPlugin" },
|
|
7624
|
-
{ pkg: "@objectstack/service-automation", export: "ScreenNodesPlugin" }
|
|
7625
|
-
]
|
|
6413
|
+
export: "AutomationServicePlugin"
|
|
7626
6414
|
},
|
|
7627
6415
|
ai: {
|
|
7628
6416
|
pkg: "@objectstack/service-ai",
|
|
@@ -7653,6 +6441,19 @@ var CAPABILITY_PROVIDERS = {
|
|
|
7653
6441
|
pkg: "@objectstack/service-job",
|
|
7654
6442
|
export: "JobServicePlugin"
|
|
7655
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
|
+
},
|
|
7656
6457
|
realtime: {
|
|
7657
6458
|
pkg: "@objectstack/service-realtime",
|
|
7658
6459
|
export: "RealtimeServicePlugin"
|
|
@@ -7670,7 +6471,11 @@ async function loadCapabilities(opts) {
|
|
|
7670
6471
|
const { kernel, requires, bundle, environmentId } = opts;
|
|
7671
6472
|
const logger = opts.logger ?? console;
|
|
7672
6473
|
const installed = [];
|
|
7673
|
-
|
|
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) {
|
|
7674
6479
|
const spec = CAPABILITY_PROVIDERS[cap];
|
|
7675
6480
|
if (!spec) {
|
|
7676
6481
|
continue;
|
|
@@ -7737,8 +6542,197 @@ async function loadCapabilities(opts) {
|
|
|
7737
6542
|
return installed;
|
|
7738
6543
|
}
|
|
7739
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
|
+
|
|
7740
6735
|
// src/cloud/artifact-kernel-factory.ts
|
|
7741
|
-
init_platform_sso();
|
|
7742
6736
|
function deriveProjectAuthSecret(baseSecret, environmentId) {
|
|
7743
6737
|
return createHmac2("sha256", baseSecret).update(`project:${environmentId}`).digest("hex");
|
|
7744
6738
|
}
|
|
@@ -7748,7 +6742,7 @@ var ArtifactKernelFactory = class {
|
|
|
7748
6742
|
this.envRegistry = config.envRegistry;
|
|
7749
6743
|
this.logger = config.logger ?? console;
|
|
7750
6744
|
this.kernelConfig = config.kernelConfig;
|
|
7751
|
-
this.authBaseSecret = (config.authBaseSecret ??
|
|
6745
|
+
this.authBaseSecret = (config.authBaseSecret ?? readEnvWithDeprecation3("OS_AUTH_SECRET", ["AUTH_SECRET", "BETTER_AUTH_SECRET"]) ?? "").trim();
|
|
7752
6746
|
}
|
|
7753
6747
|
async create(environmentId) {
|
|
7754
6748
|
let cached = this.envRegistry.peekById(environmentId);
|
|
@@ -7861,7 +6855,7 @@ var ArtifactKernelFactory = class {
|
|
|
7861
6855
|
this.logger.warn?.("[ArtifactKernelFactory] OS_AUTH_SECRET not set \u2014 per-project AuthPlugin skipped (auth endpoints will return 404)", { environmentId });
|
|
7862
6856
|
}
|
|
7863
6857
|
try {
|
|
7864
|
-
const multiTenant = String(
|
|
6858
|
+
const multiTenant = String(readEnvWithDeprecation3("OS_MULTI_ORG_ENABLED", "OS_MULTI_TENANT") ?? "false").toLowerCase() !== "false";
|
|
7865
6859
|
if (multiTenant) {
|
|
7866
6860
|
try {
|
|
7867
6861
|
const { OrgScopingPlugin } = await import("@objectstack/plugin-org-scoping");
|
|
@@ -8387,7 +7381,7 @@ var AuthProxyPlugin = class {
|
|
|
8387
7381
|
};
|
|
8388
7382
|
|
|
8389
7383
|
// src/cloud/cloud-url.ts
|
|
8390
|
-
var DEFAULT_CLOUD_URL = "https://cloud.objectos.
|
|
7384
|
+
var DEFAULT_CLOUD_URL = "https://cloud.objectos.ai";
|
|
8391
7385
|
function resolveCloudUrl(explicit) {
|
|
8392
7386
|
const raw = (explicit ?? process.env.OS_CLOUD_URL ?? "").trim();
|
|
8393
7387
|
const lower = raw.toLowerCase();
|
|
@@ -9062,9 +8056,9 @@ async function createObjectOSStack(config) {
|
|
|
9062
8056
|
}
|
|
9063
8057
|
|
|
9064
8058
|
// src/cloud/marketplace-install-local-plugin.ts
|
|
9065
|
-
import { existsSync as
|
|
9066
|
-
import { join, resolve } from "path";
|
|
9067
|
-
import { readEnvWithDeprecation as
|
|
8059
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync4, readFileSync as readFileSync2, readdirSync, unlinkSync, writeFileSync as writeFileSync3 } from "fs";
|
|
8060
|
+
import { join as join2, resolve } from "path";
|
|
8061
|
+
import { readEnvWithDeprecation as readEnvWithDeprecation4 } from "@objectstack/types";
|
|
9068
8062
|
var ROUTE_BASE = "/api/v1/marketplace/install-local";
|
|
9069
8063
|
var DEFAULT_DIR = ".objectstack/installed-packages";
|
|
9070
8064
|
function safeFilename(manifestId) {
|
|
@@ -9139,9 +8133,6 @@ var MarketplaceInstallLocalPlugin = class {
|
|
|
9139
8133
|
}
|
|
9140
8134
|
};
|
|
9141
8135
|
this.handleInstall = async (c, ctx) => {
|
|
9142
|
-
if (!this.cloudUrl) {
|
|
9143
|
-
return c.json({ success: false, error: { code: "marketplace_unavailable", message: "OS_CLOUD_URL not configured." } }, 503);
|
|
9144
|
-
}
|
|
9145
8136
|
const userId = await this.requireAuthenticatedUser(c, ctx);
|
|
9146
8137
|
if (!userId) {
|
|
9147
8138
|
return c.json({ success: false, error: { code: "unauthorized", message: "Authentication required to install packages." } }, 401);
|
|
@@ -9151,69 +8142,87 @@ var MarketplaceInstallLocalPlugin = class {
|
|
|
9151
8142
|
body = await c.req.json();
|
|
9152
8143
|
} catch {
|
|
9153
8144
|
}
|
|
9154
|
-
const
|
|
9155
|
-
|
|
9156
|
-
|
|
9157
|
-
|
|
9158
|
-
|
|
9159
|
-
|
|
9160
|
-
|
|
9161
|
-
|
|
9162
|
-
|
|
8145
|
+
const inlineManifest = body?.manifest && typeof body.manifest === "object" ? body.manifest : null;
|
|
8146
|
+
let manifest;
|
|
8147
|
+
let resolvedVersionId;
|
|
8148
|
+
let version;
|
|
8149
|
+
let packageId;
|
|
8150
|
+
if (inlineManifest) {
|
|
8151
|
+
manifest = inlineManifest;
|
|
8152
|
+
packageId = String(manifest.id ?? manifest.name ?? "").trim();
|
|
8153
|
+
version = String(manifest.version ?? "unknown");
|
|
8154
|
+
resolvedVersionId = String(body?.versionId ?? version);
|
|
8155
|
+
if (!packageId) {
|
|
8156
|
+
return c.json({ success: false, error: { code: "invalid_manifest", message: 'Inline manifest must have an "id" or "name".' } }, 400);
|
|
8157
|
+
}
|
|
8158
|
+
} else {
|
|
8159
|
+
if (!this.cloudUrl) {
|
|
8160
|
+
return c.json({ success: false, error: { code: "marketplace_unavailable", message: "OS_CLOUD_URL not configured." } }, 503);
|
|
8161
|
+
}
|
|
8162
|
+
packageId = String(body?.packageId ?? "").trim();
|
|
8163
|
+
const versionId = String(body?.versionId ?? "latest").trim() || "latest";
|
|
8164
|
+
if (!packageId) {
|
|
8165
|
+
return c.json({ success: false, error: { code: "bad_request", message: "packageId is required." } }, 400);
|
|
8166
|
+
}
|
|
8167
|
+
let payload;
|
|
8168
|
+
const publicBase = resolveMarketplacePublicBaseUrl();
|
|
8169
|
+
const fetchAttempts = [];
|
|
8170
|
+
if (publicBase) {
|
|
8171
|
+
fetchAttempts.push({
|
|
8172
|
+
label: "public-r2",
|
|
8173
|
+
url: `${publicBase}/packages/${encodeURIComponent(packageId)}/versions/${encodeURIComponent(versionId)}/manifest.json`
|
|
8174
|
+
});
|
|
8175
|
+
}
|
|
9163
8176
|
fetchAttempts.push({
|
|
9164
|
-
label: "
|
|
9165
|
-
url: `${
|
|
8177
|
+
label: "cloud",
|
|
8178
|
+
url: `${this.cloudUrl}/api/v1/marketplace/packages/${encodeURIComponent(packageId)}/versions/${encodeURIComponent(versionId)}/manifest`
|
|
9166
8179
|
});
|
|
9167
|
-
|
|
9168
|
-
|
|
9169
|
-
|
|
9170
|
-
|
|
9171
|
-
|
|
9172
|
-
|
|
9173
|
-
|
|
9174
|
-
|
|
9175
|
-
|
|
9176
|
-
|
|
9177
|
-
|
|
9178
|
-
|
|
9179
|
-
|
|
9180
|
-
|
|
9181
|
-
|
|
9182
|
-
|
|
8180
|
+
let lastErrStatus = 0;
|
|
8181
|
+
let lastErrText = "";
|
|
8182
|
+
for (const attempt of fetchAttempts) {
|
|
8183
|
+
try {
|
|
8184
|
+
const resp = await fetch(attempt.url, { headers: { "Accept": "application/json" } });
|
|
8185
|
+
if (!resp.ok) {
|
|
8186
|
+
lastErrStatus = resp.status;
|
|
8187
|
+
lastErrText = (await resp.text().catch(() => "")).slice(0, 200);
|
|
8188
|
+
if (attempt.label === "public-r2" && resp.status === 404) {
|
|
8189
|
+
ctx.logger?.info?.(`[MarketplaceInstallLocal] public-r2 miss for ${packageId}@${versionId}, falling back to cloud`);
|
|
8190
|
+
continue;
|
|
8191
|
+
}
|
|
8192
|
+
if (attempt.label === "public-r2" && resp.status >= 500) {
|
|
8193
|
+
ctx.logger?.warn?.(`[MarketplaceInstallLocal] public-r2 ${resp.status}, falling back to cloud`);
|
|
8194
|
+
continue;
|
|
8195
|
+
}
|
|
8196
|
+
break;
|
|
9183
8197
|
}
|
|
9184
|
-
|
|
9185
|
-
|
|
8198
|
+
payload = await resp.json();
|
|
8199
|
+
lastErrStatus = 0;
|
|
8200
|
+
break;
|
|
8201
|
+
} catch (err) {
|
|
8202
|
+
if (attempt.label === "public-r2") {
|
|
8203
|
+
ctx.logger?.warn?.(`[MarketplaceInstallLocal] public-r2 fetch error: ${err?.message ?? err}, falling back to cloud`);
|
|
9186
8204
|
continue;
|
|
9187
8205
|
}
|
|
9188
|
-
|
|
9189
|
-
|
|
9190
|
-
|
|
9191
|
-
|
|
9192
|
-
break;
|
|
9193
|
-
} catch (err) {
|
|
9194
|
-
if (attempt.label === "public-r2") {
|
|
9195
|
-
ctx.logger?.warn?.(`[MarketplaceInstallLocal] public-r2 fetch error: ${err?.message ?? err}, falling back to cloud`);
|
|
9196
|
-
continue;
|
|
8206
|
+
return c.json({
|
|
8207
|
+
success: false,
|
|
8208
|
+
error: { code: "cloud_fetch_failed", message: err?.message ?? String(err) }
|
|
8209
|
+
}, 502);
|
|
9197
8210
|
}
|
|
8211
|
+
}
|
|
8212
|
+
if (!payload) {
|
|
9198
8213
|
return c.json({
|
|
9199
8214
|
success: false,
|
|
9200
|
-
error: { code: "cloud_fetch_failed", message:
|
|
9201
|
-
}, 502);
|
|
8215
|
+
error: { code: "cloud_fetch_failed", message: `Cloud returned ${lastErrStatus}: ${lastErrText}` }
|
|
8216
|
+
}, lastErrStatus === 404 ? 404 : 502);
|
|
9202
8217
|
}
|
|
8218
|
+
const data = payload?.data ?? payload;
|
|
8219
|
+
manifest = data?.manifest;
|
|
8220
|
+
resolvedVersionId = String(data?.version_id ?? versionId);
|
|
8221
|
+
version = String(data?.version ?? "unknown");
|
|
9203
8222
|
}
|
|
9204
|
-
if (!payload) {
|
|
9205
|
-
return c.json({
|
|
9206
|
-
success: false,
|
|
9207
|
-
error: { code: "cloud_fetch_failed", message: `Cloud returned ${lastErrStatus}: ${lastErrText}` }
|
|
9208
|
-
}, lastErrStatus === 404 ? 404 : 502);
|
|
9209
|
-
}
|
|
9210
|
-
const data = payload?.data ?? payload;
|
|
9211
|
-
const manifest = data?.manifest;
|
|
9212
|
-
const resolvedVersionId = String(data?.version_id ?? versionId);
|
|
9213
|
-
const version = String(data?.version ?? "unknown");
|
|
9214
8223
|
const manifestId = String(manifest?.id ?? manifest?.name ?? "");
|
|
9215
8224
|
if (!manifest || !manifestId) {
|
|
9216
|
-
return c.json({ success: false, error: { code: "invalid_manifest", message: "
|
|
8225
|
+
return c.json({ success: false, error: { code: "invalid_manifest", message: "Invalid manifest payload." } }, inlineManifest ? 400 : 502);
|
|
9217
8226
|
}
|
|
9218
8227
|
const conflict = this.findConflict(ctx, manifestId);
|
|
9219
8228
|
if (conflict === "user-code") {
|
|
@@ -9225,6 +8234,18 @@ var MarketplaceInstallLocalPlugin = class {
|
|
|
9225
8234
|
}
|
|
9226
8235
|
}, 409);
|
|
9227
8236
|
}
|
|
8237
|
+
try {
|
|
8238
|
+
const manifestService = ctx.getService("manifest");
|
|
8239
|
+
manifestService.register(manifest);
|
|
8240
|
+
} catch (err) {
|
|
8241
|
+
if (inlineManifest) {
|
|
8242
|
+
return c.json({
|
|
8243
|
+
success: false,
|
|
8244
|
+
error: { code: "register_failed", message: `Failed to register imported manifest: ${err?.message ?? err}` }
|
|
8245
|
+
}, 422);
|
|
8246
|
+
}
|
|
8247
|
+
ctx.logger?.warn?.(`[MarketplaceInstallLocal] hot-register failed for ${manifestId} (will load on next restart): ${err?.message ?? err}`);
|
|
8248
|
+
}
|
|
9228
8249
|
const entry = {
|
|
9229
8250
|
packageId,
|
|
9230
8251
|
versionId: resolvedVersionId,
|
|
@@ -9236,20 +8257,14 @@ var MarketplaceInstallLocalPlugin = class {
|
|
|
9236
8257
|
withSampleData: false
|
|
9237
8258
|
};
|
|
9238
8259
|
try {
|
|
9239
|
-
|
|
9240
|
-
|
|
8260
|
+
mkdirSync4(this.storageDir, { recursive: true });
|
|
8261
|
+
writeFileSync3(join2(this.storageDir, safeFilename(manifestId)), JSON.stringify(entry, null, 2), "utf8");
|
|
9241
8262
|
} catch (err) {
|
|
9242
8263
|
return c.json({
|
|
9243
8264
|
success: false,
|
|
9244
8265
|
error: { code: "storage_failed", message: `Failed to persist manifest: ${err?.message ?? err}` }
|
|
9245
8266
|
}, 500);
|
|
9246
8267
|
}
|
|
9247
|
-
try {
|
|
9248
|
-
const manifestService = ctx.getService("manifest");
|
|
9249
|
-
manifestService.register(manifest);
|
|
9250
|
-
} catch (err) {
|
|
9251
|
-
ctx.logger?.warn?.(`[MarketplaceInstallLocal] hot-register failed for ${manifestId} (will load on next restart): ${err?.message ?? err}`);
|
|
9252
|
-
}
|
|
9253
8268
|
try {
|
|
9254
8269
|
const ql = ctx.getService("objectql");
|
|
9255
8270
|
if (ql && typeof ql.syncSchemas === "function") {
|
|
@@ -9263,7 +8278,7 @@ var MarketplaceInstallLocalPlugin = class {
|
|
|
9263
8278
|
if (seededSummary.seeded.mode === "inline" && (seededSummary.seeded.inserted ?? 0) + (seededSummary.seeded.updated ?? 0) > 0) {
|
|
9264
8279
|
entry.withSampleData = true;
|
|
9265
8280
|
try {
|
|
9266
|
-
|
|
8281
|
+
writeFileSync3(join2(this.storageDir, safeFilename(manifestId)), JSON.stringify(entry, null, 2), "utf8");
|
|
9267
8282
|
} catch {
|
|
9268
8283
|
}
|
|
9269
8284
|
}
|
|
@@ -9310,8 +8325,8 @@ var MarketplaceInstallLocalPlugin = class {
|
|
|
9310
8325
|
if (!manifestId) {
|
|
9311
8326
|
return c.json({ success: false, error: { code: "bad_request", message: "manifestId path param required." } }, 400);
|
|
9312
8327
|
}
|
|
9313
|
-
const file =
|
|
9314
|
-
if (!
|
|
8328
|
+
const file = join2(this.storageDir, safeFilename(manifestId));
|
|
8329
|
+
if (!existsSync3(file)) {
|
|
9315
8330
|
return c.json({ success: false, error: { code: "not_found", message: `No marketplace install for ${manifestId}.` } }, 404);
|
|
9316
8331
|
}
|
|
9317
8332
|
try {
|
|
@@ -9338,7 +8353,7 @@ var MarketplaceInstallLocalPlugin = class {
|
|
|
9338
8353
|
* (refuse to avoid silently overwriting authored code)
|
|
9339
8354
|
*/
|
|
9340
8355
|
this.findConflict = (ctx, manifestId) => {
|
|
9341
|
-
if (
|
|
8356
|
+
if (existsSync3(join2(this.storageDir, safeFilename(manifestId)))) {
|
|
9342
8357
|
return "marketplace";
|
|
9343
8358
|
}
|
|
9344
8359
|
try {
|
|
@@ -9380,13 +8395,13 @@ var MarketplaceInstallLocalPlugin = class {
|
|
|
9380
8395
|
if (!manifestId) {
|
|
9381
8396
|
return c.json({ success: false, error: { code: "bad_request", message: "manifestId path param required." } }, 400);
|
|
9382
8397
|
}
|
|
9383
|
-
const file =
|
|
9384
|
-
if (!
|
|
8398
|
+
const file = join2(this.storageDir, safeFilename(manifestId));
|
|
8399
|
+
if (!existsSync3(file)) {
|
|
9385
8400
|
return c.json({ success: false, error: { code: "not_found", message: `No marketplace install for ${manifestId}.` } }, 404);
|
|
9386
8401
|
}
|
|
9387
8402
|
let entry;
|
|
9388
8403
|
try {
|
|
9389
|
-
entry = JSON.parse(
|
|
8404
|
+
entry = JSON.parse(readFileSync2(file, "utf8"));
|
|
9390
8405
|
} catch (err) {
|
|
9391
8406
|
return c.json({ success: false, error: { code: "storage_failed", message: `Failed to read manifest cache: ${err?.message ?? err}` } }, 500);
|
|
9392
8407
|
}
|
|
@@ -9402,7 +8417,7 @@ var MarketplaceInstallLocalPlugin = class {
|
|
|
9402
8417
|
}
|
|
9403
8418
|
try {
|
|
9404
8419
|
entry.withSampleData = true;
|
|
9405
|
-
|
|
8420
|
+
writeFileSync3(file, JSON.stringify(entry, null, 2), "utf8");
|
|
9406
8421
|
} catch {
|
|
9407
8422
|
}
|
|
9408
8423
|
return c.json({
|
|
@@ -9434,13 +8449,13 @@ var MarketplaceInstallLocalPlugin = class {
|
|
|
9434
8449
|
if (!manifestId) {
|
|
9435
8450
|
return c.json({ success: false, error: { code: "bad_request", message: "manifestId path param required." } }, 400);
|
|
9436
8451
|
}
|
|
9437
|
-
const file =
|
|
9438
|
-
if (!
|
|
8452
|
+
const file = join2(this.storageDir, safeFilename(manifestId));
|
|
8453
|
+
if (!existsSync3(file)) {
|
|
9439
8454
|
return c.json({ success: false, error: { code: "not_found", message: `No marketplace install for ${manifestId}.` } }, 404);
|
|
9440
8455
|
}
|
|
9441
8456
|
let entry;
|
|
9442
8457
|
try {
|
|
9443
|
-
entry = JSON.parse(
|
|
8458
|
+
entry = JSON.parse(readFileSync2(file, "utf8"));
|
|
9444
8459
|
} catch (err) {
|
|
9445
8460
|
return c.json({ success: false, error: { code: "storage_failed", message: `Failed to read manifest cache: ${err?.message ?? err}` } }, 500);
|
|
9446
8461
|
}
|
|
@@ -9489,7 +8504,7 @@ var MarketplaceInstallLocalPlugin = class {
|
|
|
9489
8504
|
}
|
|
9490
8505
|
try {
|
|
9491
8506
|
entry.withSampleData = false;
|
|
9492
|
-
|
|
8507
|
+
writeFileSync3(file, JSON.stringify(entry, null, 2), "utf8");
|
|
9493
8508
|
} catch {
|
|
9494
8509
|
}
|
|
9495
8510
|
ctx.logger?.info?.(`[MarketplaceInstallLocal] purged ${manifestId}: deleted=${deleted} skipped=${skipped} errors=${errors}`);
|
|
@@ -9587,7 +8602,7 @@ var MarketplaceInstallLocalPlugin = class {
|
|
|
9587
8602
|
}
|
|
9588
8603
|
}
|
|
9589
8604
|
if (opts.seedNow && datasets.length > 0) {
|
|
9590
|
-
const multiTenant = String(
|
|
8605
|
+
const multiTenant = String(readEnvWithDeprecation4("OS_MULTI_ORG_ENABLED", "OS_MULTI_TENANT") ?? "false").toLowerCase() !== "false";
|
|
9591
8606
|
try {
|
|
9592
8607
|
const ql = ctx.getService("objectql");
|
|
9593
8608
|
let metadata;
|
|
@@ -9688,12 +8703,12 @@ var MarketplaceInstallLocalPlugin = class {
|
|
|
9688
8703
|
return null;
|
|
9689
8704
|
};
|
|
9690
8705
|
this.readAll = () => {
|
|
9691
|
-
if (!
|
|
8706
|
+
if (!existsSync3(this.storageDir)) return [];
|
|
9692
8707
|
const out = [];
|
|
9693
8708
|
for (const name of readdirSync(this.storageDir)) {
|
|
9694
8709
|
if (!name.endsWith(".json")) continue;
|
|
9695
8710
|
try {
|
|
9696
|
-
const raw =
|
|
8711
|
+
const raw = readFileSync2(join2(this.storageDir, name), "utf8");
|
|
9697
8712
|
out.push(JSON.parse(raw));
|
|
9698
8713
|
} catch {
|
|
9699
8714
|
}
|
|
@@ -9705,9 +8720,6 @@ var MarketplaceInstallLocalPlugin = class {
|
|
|
9705
8720
|
}
|
|
9706
8721
|
};
|
|
9707
8722
|
|
|
9708
|
-
// src/index.ts
|
|
9709
|
-
init_platform_sso();
|
|
9710
|
-
|
|
9711
8723
|
// src/sandbox/script-runner.ts
|
|
9712
8724
|
var UnimplementedScriptRunner = class {
|
|
9713
8725
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
@@ -9738,7 +8750,7 @@ import {
|
|
|
9738
8750
|
createRestApiPlugin
|
|
9739
8751
|
} from "@objectstack/rest";
|
|
9740
8752
|
export * from "@objectstack/core";
|
|
9741
|
-
import { readEnvWithDeprecation as
|
|
8753
|
+
import { readEnvWithDeprecation as readEnvWithDeprecation5, _resetEnvDeprecationWarnings } from "@objectstack/types";
|
|
9742
8754
|
export {
|
|
9743
8755
|
AppPlugin,
|
|
9744
8756
|
ArtifactApiClient,
|
|
@@ -9748,6 +8760,7 @@ export {
|
|
|
9748
8760
|
DEFAULT_CLOUD_URL,
|
|
9749
8761
|
DEFAULT_RATE_LIMITS,
|
|
9750
8762
|
DriverPlugin,
|
|
8763
|
+
ExternalValidationPlugin,
|
|
9751
8764
|
FileArtifactApiClient,
|
|
9752
8765
|
HttpDispatcher,
|
|
9753
8766
|
HttpServer,
|
|
@@ -9786,6 +8799,7 @@ export {
|
|
|
9786
8799
|
collectBundleHooks,
|
|
9787
8800
|
createDefaultHostConfig,
|
|
9788
8801
|
createDispatcherPlugin,
|
|
8802
|
+
createExternalValidationPlugin,
|
|
9789
8803
|
createObjectOSStack,
|
|
9790
8804
|
createRestApiPlugin,
|
|
9791
8805
|
createStandaloneStack,
|
|
@@ -9801,7 +8815,7 @@ export {
|
|
|
9801
8815
|
mergeRuntimeModule,
|
|
9802
8816
|
parseTraceparent,
|
|
9803
8817
|
readArtifactSource,
|
|
9804
|
-
|
|
8818
|
+
readEnvWithDeprecation5 as readEnvWithDeprecation,
|
|
9805
8819
|
resolveCloudUrl,
|
|
9806
8820
|
resolveDefaultArtifactPath,
|
|
9807
8821
|
resolveErrorReporter,
|