@objectstack/runtime 6.8.1 → 7.0.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 +588 -136
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +48 -2
- package/dist/index.d.ts +48 -2
- package/dist/index.js +588 -136
- package/dist/index.js.map +1 -1
- package/package.json +18 -25
package/dist/index.cjs
CHANGED
|
@@ -1294,7 +1294,15 @@ var init_app_plugin = __esm({
|
|
|
1294
1294
|
AppPlugin = class {
|
|
1295
1295
|
constructor(bundle, projectContext) {
|
|
1296
1296
|
this.type = "app";
|
|
1297
|
+
/** When true, init/start become no-ops — env has no app payload. */
|
|
1298
|
+
this.empty = false;
|
|
1297
1299
|
this.init = async (ctx) => {
|
|
1300
|
+
if (this.empty) {
|
|
1301
|
+
ctx.logger.debug("[AppPlugin] empty env \u2014 no app payload, skipping init", {
|
|
1302
|
+
pluginName: this.name
|
|
1303
|
+
});
|
|
1304
|
+
return;
|
|
1305
|
+
}
|
|
1298
1306
|
const sys = this.bundle.manifest || this.bundle;
|
|
1299
1307
|
const appId = sys.id || sys.name;
|
|
1300
1308
|
ctx.logger.info("Registering App Service", {
|
|
@@ -1309,6 +1317,12 @@ var init_app_plugin = __esm({
|
|
|
1309
1317
|
ctx.getService("manifest").register(servicePayload);
|
|
1310
1318
|
};
|
|
1311
1319
|
this.start = async (ctx) => {
|
|
1320
|
+
if (this.empty) {
|
|
1321
|
+
ctx.logger.debug("[AppPlugin] empty env \u2014 no app payload, skipping start", {
|
|
1322
|
+
pluginName: this.name
|
|
1323
|
+
});
|
|
1324
|
+
return;
|
|
1325
|
+
}
|
|
1312
1326
|
const sys = this.bundle.manifest || this.bundle;
|
|
1313
1327
|
const appId = sys.id || sys.name;
|
|
1314
1328
|
let ql;
|
|
@@ -1468,6 +1482,66 @@ var init_app_plugin = __esm({
|
|
|
1468
1482
|
} catch (err) {
|
|
1469
1483
|
ctx.logger.error("[AppPlugin] Failed to schedule approval-process registration", err, { appId });
|
|
1470
1484
|
}
|
|
1485
|
+
try {
|
|
1486
|
+
const jobs = Array.isArray(this.bundle.jobs) ? this.bundle.jobs : Array.isArray((this.bundle.manifest || {}).jobs) ? this.bundle.manifest.jobs : [];
|
|
1487
|
+
if (jobs.length > 0) {
|
|
1488
|
+
ctx.hook("kernel:ready", async () => {
|
|
1489
|
+
let svc;
|
|
1490
|
+
try {
|
|
1491
|
+
svc = ctx.getService("job");
|
|
1492
|
+
} catch {
|
|
1493
|
+
}
|
|
1494
|
+
if (!svc || typeof svc.schedule !== "function") {
|
|
1495
|
+
ctx.logger.warn("[AppPlugin] job service not registered \u2014 skipping declarative jobs", {
|
|
1496
|
+
appId,
|
|
1497
|
+
jobCount: jobs.length
|
|
1498
|
+
});
|
|
1499
|
+
return;
|
|
1500
|
+
}
|
|
1501
|
+
const fnMap = collectBundleFunctions(this.bundle);
|
|
1502
|
+
let ok = 0;
|
|
1503
|
+
for (const job of jobs) {
|
|
1504
|
+
const jobName = job?.name;
|
|
1505
|
+
if (!jobName) {
|
|
1506
|
+
ctx.logger.warn("[AppPlugin] skipping job without name", { appId, job });
|
|
1507
|
+
continue;
|
|
1508
|
+
}
|
|
1509
|
+
if (job.enabled === false) {
|
|
1510
|
+
ctx.logger.debug("[AppPlugin] job disabled \u2014 skipping", { appId, job: jobName });
|
|
1511
|
+
continue;
|
|
1512
|
+
}
|
|
1513
|
+
const handler = fnMap[job.handler];
|
|
1514
|
+
if (typeof handler !== "function") {
|
|
1515
|
+
ctx.logger.warn("[AppPlugin] job handler not found in bundle.functions \u2014 skipping", {
|
|
1516
|
+
appId,
|
|
1517
|
+
job: jobName,
|
|
1518
|
+
handler: job.handler
|
|
1519
|
+
});
|
|
1520
|
+
continue;
|
|
1521
|
+
}
|
|
1522
|
+
try {
|
|
1523
|
+
await svc.schedule(
|
|
1524
|
+
jobName,
|
|
1525
|
+
job.schedule,
|
|
1526
|
+
async (jobCtx) => {
|
|
1527
|
+
await handler({ ...jobCtx, jobId: jobName, bundle: this.bundle });
|
|
1528
|
+
}
|
|
1529
|
+
);
|
|
1530
|
+
ok++;
|
|
1531
|
+
} catch (err) {
|
|
1532
|
+
ctx.logger.warn("[AppPlugin] Failed to schedule job", {
|
|
1533
|
+
appId,
|
|
1534
|
+
job: jobName,
|
|
1535
|
+
error: err?.message ?? String(err)
|
|
1536
|
+
});
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
ctx.logger.info("[AppPlugin] Scheduled background jobs", { appId, count: ok });
|
|
1540
|
+
});
|
|
1541
|
+
}
|
|
1542
|
+
} catch (err) {
|
|
1543
|
+
ctx.logger.error("[AppPlugin] Failed to schedule background-job registration", err, { appId });
|
|
1544
|
+
}
|
|
1471
1545
|
this.emitCatalogEvent(ctx, "app:registered", sys);
|
|
1472
1546
|
await this.loadTranslations(ctx, appId);
|
|
1473
1547
|
const seedDatasets = [];
|
|
@@ -1540,51 +1614,68 @@ var init_app_plugin = __esm({
|
|
|
1540
1614
|
} catch (e) {
|
|
1541
1615
|
ctx.logger.warn("[Seeder] Failed to register seed-datasets/seed-replayer service", { error: e?.message });
|
|
1542
1616
|
}
|
|
1543
|
-
const multiTenant = String(process.env.OS_MULTI_TENANT ?? "
|
|
1617
|
+
const multiTenant = String(process.env.OS_MULTI_TENANT ?? "false").toLowerCase() !== "false";
|
|
1544
1618
|
if (multiTenant) {
|
|
1545
1619
|
ctx.logger.info("[Seeder] multi-tenant mode \u2014 skipping inline seed; per-org replay will run on sys_organization insert");
|
|
1546
1620
|
} else {
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
const
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1621
|
+
const seedBudgetMs = Number(process.env.OS_INLINE_SEED_BUDGET_MS ?? 8e3);
|
|
1622
|
+
const seedPromise = (async () => {
|
|
1623
|
+
try {
|
|
1624
|
+
const metadata = ctx.getService("metadata");
|
|
1625
|
+
if (metadata) {
|
|
1626
|
+
const seedLoader = new SeedLoaderService(ql, metadata, ctx.logger);
|
|
1627
|
+
const { SeedLoaderRequestSchema } = await import("@objectstack/spec/data");
|
|
1628
|
+
const request = SeedLoaderRequestSchema.parse({
|
|
1629
|
+
datasets: normalizedDatasets,
|
|
1630
|
+
config: { defaultMode: "upsert", multiPass: true }
|
|
1631
|
+
});
|
|
1632
|
+
const result = await seedLoader.load(request);
|
|
1633
|
+
ctx.logger.info("[Seeder] Seed loading complete", {
|
|
1634
|
+
inserted: result.summary.totalInserted,
|
|
1635
|
+
updated: result.summary.totalUpdated,
|
|
1636
|
+
errors: result.errors.length
|
|
1637
|
+
});
|
|
1638
|
+
} else {
|
|
1639
|
+
ctx.logger.debug("[Seeder] No metadata service; using basic insert fallback");
|
|
1640
|
+
for (const dataset of normalizedDatasets) {
|
|
1641
|
+
ctx.logger.info(`[Seeder] Seeding ${dataset.records.length} records for ${dataset.object}`);
|
|
1642
|
+
for (const record of dataset.records) {
|
|
1643
|
+
try {
|
|
1644
|
+
await ql.insert(dataset.object, record, { context: { isSystem: true } });
|
|
1645
|
+
} catch (err) {
|
|
1646
|
+
ctx.logger.warn(`[Seeder] Failed to insert ${dataset.object} record:`, { error: err.message });
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
ctx.logger.info("[Seeder] Data seeding complete.");
|
|
1651
|
+
}
|
|
1652
|
+
} catch (err) {
|
|
1653
|
+
ctx.logger.warn("[Seeder] SeedLoaderService failed, falling back to basic insert", { error: err.message });
|
|
1564
1654
|
for (const dataset of normalizedDatasets) {
|
|
1565
|
-
ctx.logger.info(`[Seeder] Seeding ${dataset.records.length} records for ${dataset.object}`);
|
|
1566
1655
|
for (const record of dataset.records) {
|
|
1567
1656
|
try {
|
|
1568
1657
|
await ql.insert(dataset.object, record, { context: { isSystem: true } });
|
|
1569
|
-
} catch (
|
|
1570
|
-
ctx.logger.warn(`[Seeder] Failed to insert ${dataset.object} record:`, { error:
|
|
1658
|
+
} catch (insertErr) {
|
|
1659
|
+
ctx.logger.warn(`[Seeder] Failed to insert ${dataset.object} record:`, { error: insertErr.message });
|
|
1571
1660
|
}
|
|
1572
1661
|
}
|
|
1573
1662
|
}
|
|
1574
|
-
ctx.logger.info("[Seeder] Data seeding complete.");
|
|
1663
|
+
ctx.logger.info("[Seeder] Data seeding complete (fallback).");
|
|
1575
1664
|
}
|
|
1576
|
-
}
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
}
|
|
1586
|
-
|
|
1587
|
-
|
|
1665
|
+
})();
|
|
1666
|
+
let timer;
|
|
1667
|
+
const budget = new Promise((resolve2) => {
|
|
1668
|
+
timer = setTimeout(() => resolve2("budget"), seedBudgetMs);
|
|
1669
|
+
});
|
|
1670
|
+
const winner = await Promise.race([seedPromise.then(() => "done"), budget]);
|
|
1671
|
+
if (timer) clearTimeout(timer);
|
|
1672
|
+
if (winner === "budget") {
|
|
1673
|
+
ctx.logger.warn(
|
|
1674
|
+
`[Seeder] Inline seed exceeded ${seedBudgetMs}ms budget for ${appId}; continuing in background to avoid blocking kernel start.`
|
|
1675
|
+
);
|
|
1676
|
+
seedPromise.catch((err) => {
|
|
1677
|
+
ctx.logger.warn("[Seeder] Background seed failed after budget", { appId, error: err?.message ?? String(err) });
|
|
1678
|
+
});
|
|
1588
1679
|
}
|
|
1589
1680
|
}
|
|
1590
1681
|
}
|
|
@@ -1595,10 +1686,54 @@ var init_app_plugin = __esm({
|
|
|
1595
1686
|
};
|
|
1596
1687
|
this.bundle = bundle;
|
|
1597
1688
|
this.projectContext = projectContext;
|
|
1598
|
-
const sys = bundle
|
|
1599
|
-
const appId = sys
|
|
1689
|
+
const sys = bundle?.manifest || bundle;
|
|
1690
|
+
const appId = sys?.id || sys?.name;
|
|
1691
|
+
if (!appId) {
|
|
1692
|
+
const APP_CATEGORY_KEYS = [
|
|
1693
|
+
"objects",
|
|
1694
|
+
"views",
|
|
1695
|
+
"apps",
|
|
1696
|
+
"pages",
|
|
1697
|
+
"dashboards",
|
|
1698
|
+
"reports",
|
|
1699
|
+
"flows",
|
|
1700
|
+
"workflows",
|
|
1701
|
+
"triggers",
|
|
1702
|
+
"agents",
|
|
1703
|
+
"tools",
|
|
1704
|
+
"skills",
|
|
1705
|
+
"actions",
|
|
1706
|
+
"permissions",
|
|
1707
|
+
"roles",
|
|
1708
|
+
"profiles",
|
|
1709
|
+
"translations",
|
|
1710
|
+
"sharingRules",
|
|
1711
|
+
"ragPipelines",
|
|
1712
|
+
"data"
|
|
1713
|
+
];
|
|
1714
|
+
const hasAppPayload = APP_CATEGORY_KEYS.some((k) => {
|
|
1715
|
+
const v = (bundle && bundle[k]) ?? (sys && sys[k]);
|
|
1716
|
+
return Array.isArray(v) && v.length > 0;
|
|
1717
|
+
});
|
|
1718
|
+
if (!hasAppPayload) {
|
|
1719
|
+
this.empty = true;
|
|
1720
|
+
const envSlug = projectContext?.environmentId ? projectContext.environmentId.slice(0, 8) : "empty";
|
|
1721
|
+
this.name = `plugin.app.empty-${envSlug}`;
|
|
1722
|
+
return;
|
|
1723
|
+
}
|
|
1724
|
+
const bundleKeys = bundle && typeof bundle === "object" ? Object.keys(bundle).slice(0, 20).join(",") : typeof bundle;
|
|
1725
|
+
const sysKeys = sys && typeof sys === "object" ? Object.keys(sys).slice(0, 20).join(",") : typeof sys;
|
|
1726
|
+
const ctxHint = projectContext ? ` projectContext=${JSON.stringify({
|
|
1727
|
+
environmentId: projectContext.environmentId,
|
|
1728
|
+
packageId: projectContext.packageId,
|
|
1729
|
+
source: projectContext.source
|
|
1730
|
+
})}` : "";
|
|
1731
|
+
throw new Error(
|
|
1732
|
+
`[AppPlugin] bundle has app payload but no manifest.id / manifest.name \u2014 cannot register as a plugin. bundleKeys=[${bundleKeys}] sysKeys=[${sysKeys}]${ctxHint}`
|
|
1733
|
+
);
|
|
1734
|
+
}
|
|
1600
1735
|
this.name = `plugin.app.${appId}`;
|
|
1601
|
-
this.version = sys
|
|
1736
|
+
this.version = sys?.version;
|
|
1602
1737
|
}
|
|
1603
1738
|
/**
|
|
1604
1739
|
* Emit a kernel hook so the control-plane `AppCatalogService` can
|
|
@@ -2211,7 +2346,7 @@ function resolveObjectStackHome() {
|
|
|
2211
2346
|
var StandaloneStackConfigSchema = import_zod.z.object({
|
|
2212
2347
|
databaseUrl: import_zod.z.string().optional(),
|
|
2213
2348
|
databaseAuthToken: import_zod.z.string().optional(),
|
|
2214
|
-
databaseDriver: import_zod.z.enum(["sqlite", "sqlite-wasm", "
|
|
2349
|
+
databaseDriver: import_zod.z.enum(["sqlite", "sqlite-wasm", "memory", "postgres", "mongodb"]).optional(),
|
|
2215
2350
|
environmentId: import_zod.z.string().optional(),
|
|
2216
2351
|
artifactPath: import_zod.z.string().optional(),
|
|
2217
2352
|
/**
|
|
@@ -2229,7 +2364,6 @@ var StandaloneStackConfigSchema = import_zod.z.object({
|
|
|
2229
2364
|
});
|
|
2230
2365
|
function detectDriverFromUrl(dbUrl) {
|
|
2231
2366
|
if (/^memory:\/\//i.test(dbUrl)) return "memory";
|
|
2232
|
-
if (/^(libsql|https?):\/\//i.test(dbUrl)) return "turso";
|
|
2233
2367
|
if (/^(postgres(ql)?|pg):\/\//i.test(dbUrl)) return "postgres";
|
|
2234
2368
|
if (/^mongodb(\+srv)?:\/\//i.test(dbUrl)) return "mongodb";
|
|
2235
2369
|
if (/^wasm-sqlite:\/\//i.test(dbUrl)) return "sqlite-wasm";
|
|
@@ -2237,7 +2371,7 @@ function detectDriverFromUrl(dbUrl) {
|
|
|
2237
2371
|
if (/^file:/i.test(dbUrl)) return "sqlite";
|
|
2238
2372
|
if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(dbUrl)) return "sqlite";
|
|
2239
2373
|
throw new Error(
|
|
2240
|
-
`[StandaloneStack] Unsupported database URL scheme: ${dbUrl}. Supported schemes: memory://,
|
|
2374
|
+
`[StandaloneStack] Unsupported database URL scheme: ${dbUrl}. Supported schemes: memory://, postgres://, pg://, mongodb://, mongodb+srv://, file:`
|
|
2241
2375
|
);
|
|
2242
2376
|
}
|
|
2243
2377
|
async function createStandaloneStack(config) {
|
|
@@ -2251,25 +2385,12 @@ async function createStandaloneStack(config) {
|
|
|
2251
2385
|
const artifactPathInput = cfg.artifactPath ?? process.env.OS_ARTIFACT_PATH ?? (0, import_node_path2.resolve)(cwd, "dist/objectstack.json");
|
|
2252
2386
|
const artifactPath = isHttpUrl(artifactPathInput) ? artifactPathInput : artifactPathInput.startsWith("/") ? artifactPathInput : (0, import_node_path2.resolve)(cwd, artifactPathInput);
|
|
2253
2387
|
const dbUrl = cfg.databaseUrl ?? process.env.OS_DATABASE_URL?.trim() ?? process.env.TURSO_DATABASE_URL?.trim() ?? (process.env.OS_HOME?.trim() ? `file:${(0, import_node_path2.resolve)(resolveObjectStackHome(), "data/standalone.db")}` : cfg.projectRoot ? `file:${(0, import_node_path2.resolve)(cfg.projectRoot, ".objectstack/data/standalone.db")}` : `file:${(0, import_node_path2.resolve)(resolveObjectStackHome(), "data/standalone.db")}`);
|
|
2254
|
-
const dbAuthToken = cfg.databaseAuthToken ?? process.env.OS_DATABASE_AUTH_TOKEN?.trim() ?? process.env.TURSO_AUTH_TOKEN?.trim();
|
|
2255
2388
|
const explicitDriver = cfg.databaseDriver ?? process.env.OS_DATABASE_DRIVER?.trim();
|
|
2256
2389
|
const dbDriver = explicitDriver ?? detectDriverFromUrl(dbUrl);
|
|
2257
2390
|
let driverPlugin;
|
|
2258
2391
|
if (dbDriver === "memory") {
|
|
2259
2392
|
const { InMemoryDriver } = await import("@objectstack/driver-memory");
|
|
2260
2393
|
driverPlugin = new DriverPlugin2(new InMemoryDriver());
|
|
2261
|
-
} else if (dbDriver === "turso") {
|
|
2262
|
-
let TursoDriver;
|
|
2263
|
-
try {
|
|
2264
|
-
({ TursoDriver } = await import("@objectstack/driver-turso"));
|
|
2265
|
-
} catch (err) {
|
|
2266
|
-
throw new Error(
|
|
2267
|
-
`[StandaloneStack] libsql/turso URL detected ("${dbUrl}") but @objectstack/driver-turso is not installed. Install it with: npm install @objectstack/driver-turso (or use a file: URL to default to better-sqlite3). (${err?.message ?? err})`
|
|
2268
|
-
);
|
|
2269
|
-
}
|
|
2270
|
-
driverPlugin = new DriverPlugin2(
|
|
2271
|
-
new TursoDriver({ url: dbUrl, authToken: dbAuthToken })
|
|
2272
|
-
);
|
|
2273
2394
|
} else if (dbDriver === "postgres") {
|
|
2274
2395
|
const { SqlDriver } = await import("@objectstack/driver-sql");
|
|
2275
2396
|
driverPlugin = new DriverPlugin2(
|
|
@@ -2528,8 +2649,12 @@ async function resolveExecutionContext(opts) {
|
|
|
2528
2649
|
if (!userId) {
|
|
2529
2650
|
try {
|
|
2530
2651
|
const authService = await opts.getService("auth");
|
|
2652
|
+
let api = authService?.api;
|
|
2653
|
+
if (!api && typeof authService?.getApi === "function") {
|
|
2654
|
+
api = await authService.getApi();
|
|
2655
|
+
}
|
|
2531
2656
|
const headersInstance = toHeaders(headers);
|
|
2532
|
-
const sessionData = await
|
|
2657
|
+
const sessionData = await api?.getSession?.({ headers: headersInstance });
|
|
2533
2658
|
userId = sessionData?.user?.id ?? sessionData?.session?.userId;
|
|
2534
2659
|
tenantId = tenantId ?? sessionData?.session?.activeOrganizationId;
|
|
2535
2660
|
ctx.accessToken = sessionData?.session?.token ?? ctx.accessToken;
|
|
@@ -4006,7 +4131,7 @@ var _HttpDispatcher = class _HttpDispatcher {
|
|
|
4006
4131
|
return {
|
|
4007
4132
|
handled: true,
|
|
4008
4133
|
response: this.error(
|
|
4009
|
-
"No ObjectQL driver is registered. Register at least one DriverPlugin (e.g. InMemoryDriver or
|
|
4134
|
+
"No ObjectQL driver is registered. Register at least one DriverPlugin (e.g. InMemoryDriver or SqlDriver).",
|
|
4010
4135
|
503
|
|
4011
4136
|
)
|
|
4012
4137
|
};
|
|
@@ -5374,7 +5499,7 @@ var _HttpDispatcher = class _HttpDispatcher {
|
|
|
5374
5499
|
* Handle AI service routes (/ai/chat, /ai/models, /ai/conversations, etc.)
|
|
5375
5500
|
* Resolves the AI service and its built-in route handlers, then dispatches.
|
|
5376
5501
|
*/
|
|
5377
|
-
async handleAI(subPath, method, body, query,
|
|
5502
|
+
async handleAI(subPath, method, body, query, context) {
|
|
5378
5503
|
let aiService;
|
|
5379
5504
|
try {
|
|
5380
5505
|
aiService = await this.resolveService("ai");
|
|
@@ -5418,7 +5543,23 @@ var _HttpDispatcher = class _HttpDispatcher {
|
|
|
5418
5543
|
if (route.method !== method) continue;
|
|
5419
5544
|
const params = matchRoute(route.path, fullPath);
|
|
5420
5545
|
if (params === null) continue;
|
|
5421
|
-
const
|
|
5546
|
+
const ec = context.executionContext;
|
|
5547
|
+
const user = ec?.userId ? {
|
|
5548
|
+
userId: ec.userId,
|
|
5549
|
+
id: ec.userId,
|
|
5550
|
+
displayName: ec.userDisplayName ?? ec.userName ?? ec.userId,
|
|
5551
|
+
email: ec.userEmail,
|
|
5552
|
+
roles: Array.isArray(ec.roles) ? ec.roles : [],
|
|
5553
|
+
permissions: Array.isArray(ec.permissions) ? ec.permissions : [],
|
|
5554
|
+
organizationId: ec.tenantId
|
|
5555
|
+
} : void 0;
|
|
5556
|
+
const result = await route.handler({
|
|
5557
|
+
body,
|
|
5558
|
+
params,
|
|
5559
|
+
query,
|
|
5560
|
+
headers: context.request?.headers,
|
|
5561
|
+
user
|
|
5562
|
+
});
|
|
5422
5563
|
if (result.stream && result.events) {
|
|
5423
5564
|
return {
|
|
5424
5565
|
handled: true,
|
|
@@ -5913,13 +6054,22 @@ function resolveErrorReporter(ctx, override) {
|
|
|
5913
6054
|
}
|
|
5914
6055
|
|
|
5915
6056
|
// src/dispatcher-plugin.ts
|
|
5916
|
-
function mountRouteOnServer(route, server, routePath, securityHeaders) {
|
|
6057
|
+
function mountRouteOnServer(route, server, routePath, securityHeaders, resolveUser) {
|
|
5917
6058
|
const handler = async (req, res) => {
|
|
5918
6059
|
try {
|
|
6060
|
+
let user;
|
|
6061
|
+
if (resolveUser) {
|
|
6062
|
+
try {
|
|
6063
|
+
user = await resolveUser(req.headers ?? {});
|
|
6064
|
+
} catch {
|
|
6065
|
+
}
|
|
6066
|
+
}
|
|
5919
6067
|
const result = await route.handler({
|
|
5920
6068
|
body: req.body,
|
|
5921
6069
|
params: req.params,
|
|
5922
|
-
query: req.query
|
|
6070
|
+
query: req.query,
|
|
6071
|
+
headers: req.headers,
|
|
6072
|
+
user
|
|
5923
6073
|
});
|
|
5924
6074
|
if (result.stream && result.events) {
|
|
5925
6075
|
res.status(result.status);
|
|
@@ -6656,6 +6806,32 @@ function createDispatcherPlugin(config = {}) {
|
|
|
6656
6806
|
}
|
|
6657
6807
|
}
|
|
6658
6808
|
ctx.logger.info("Dispatcher bridge routes registered", { prefix, enableProjectScoping, projectResolution });
|
|
6809
|
+
const resolveRequestUser = async (headers) => {
|
|
6810
|
+
try {
|
|
6811
|
+
const authService = ctx.getService("auth");
|
|
6812
|
+
if (!authService) return void 0;
|
|
6813
|
+
let api = authService.api;
|
|
6814
|
+
if (!api && typeof authService.getApi === "function") {
|
|
6815
|
+
api = await authService.getApi();
|
|
6816
|
+
}
|
|
6817
|
+
if (!api?.getSession) return void 0;
|
|
6818
|
+
const headersInstance = headers instanceof Headers ? headers : new Headers(headers);
|
|
6819
|
+
const sessionData = await api.getSession({ headers: headersInstance });
|
|
6820
|
+
const userId = sessionData?.user?.id ?? sessionData?.session?.userId;
|
|
6821
|
+
if (!userId) return void 0;
|
|
6822
|
+
return {
|
|
6823
|
+
userId,
|
|
6824
|
+
id: userId,
|
|
6825
|
+
displayName: sessionData?.user?.name ?? sessionData?.user?.email ?? userId,
|
|
6826
|
+
email: sessionData?.user?.email,
|
|
6827
|
+
roles: [],
|
|
6828
|
+
permissions: [],
|
|
6829
|
+
organizationId: sessionData?.session?.activeOrganizationId
|
|
6830
|
+
};
|
|
6831
|
+
} catch {
|
|
6832
|
+
return void 0;
|
|
6833
|
+
}
|
|
6834
|
+
};
|
|
6659
6835
|
const toScopedPath = (routePath) => {
|
|
6660
6836
|
if (routePath.startsWith(prefix)) {
|
|
6661
6837
|
const tail = routePath.slice(prefix.length);
|
|
@@ -6668,11 +6844,11 @@ function createDispatcherPlugin(config = {}) {
|
|
|
6668
6844
|
const routePath = route.path.startsWith("/api/v1") ? route.path : `${prefix}${route.path}`;
|
|
6669
6845
|
let count = 0;
|
|
6670
6846
|
if (enableProjectScoping && projectResolution === "required") {
|
|
6671
|
-
if (mountRouteOnServer(route, server, toScopedPath(routePath), securityHeaders)) count++;
|
|
6847
|
+
if (mountRouteOnServer(route, server, toScopedPath(routePath), securityHeaders, resolveRequestUser)) count++;
|
|
6672
6848
|
} else {
|
|
6673
|
-
if (mountRouteOnServer(route, server, routePath, securityHeaders)) count++;
|
|
6849
|
+
if (mountRouteOnServer(route, server, routePath, securityHeaders, resolveRequestUser)) count++;
|
|
6674
6850
|
if (enableProjectScoping) {
|
|
6675
|
-
if (mountRouteOnServer(route, server, toScopedPath(routePath), securityHeaders)) count++;
|
|
6851
|
+
if (mountRouteOnServer(route, server, toScopedPath(routePath), securityHeaders, resolveRequestUser)) count++;
|
|
6676
6852
|
}
|
|
6677
6853
|
}
|
|
6678
6854
|
return count;
|
|
@@ -7447,6 +7623,27 @@ function extractRuntimeFromMetadata(metadata) {
|
|
|
7447
7623
|
}
|
|
7448
7624
|
async function createDriver(driverType, databaseUrl, authToken) {
|
|
7449
7625
|
switch (driverType) {
|
|
7626
|
+
case "libsql":
|
|
7627
|
+
case "turso": {
|
|
7628
|
+
let TursoDriver;
|
|
7629
|
+
try {
|
|
7630
|
+
({ TursoDriver } = await import("@objectstack/driver-turso"));
|
|
7631
|
+
} catch (primaryErr) {
|
|
7632
|
+
try {
|
|
7633
|
+
const { createRequire } = await import("module");
|
|
7634
|
+
const path = await import("path");
|
|
7635
|
+
const url = await import("url");
|
|
7636
|
+
const hostRequire = createRequire(path.join(process.cwd(), "noop.js"));
|
|
7637
|
+
const resolved = hostRequire.resolve("@objectstack/driver-turso");
|
|
7638
|
+
({ TursoDriver } = await import(url.pathToFileURL(resolved).href));
|
|
7639
|
+
} catch (fallbackErr) {
|
|
7640
|
+
throw new Error(
|
|
7641
|
+
`[ArtifactEnvironmentRegistry] libsql/turso driver requested but @objectstack/driver-turso is not resolvable. Install it from the cloud monorepo (cloud/packages/driver-turso) or via npm. (primary: ${primaryErr?.message ?? primaryErr}; fallback: ${fallbackErr?.message ?? fallbackErr})`
|
|
7642
|
+
);
|
|
7643
|
+
}
|
|
7644
|
+
}
|
|
7645
|
+
return new TursoDriver({ url: databaseUrl, authToken });
|
|
7646
|
+
}
|
|
7450
7647
|
case "memory": {
|
|
7451
7648
|
const { InMemoryDriver } = await import("@objectstack/driver-memory");
|
|
7452
7649
|
const dbName = databaseUrl.replace(/^memory:\/\//, "").trim();
|
|
@@ -7465,18 +7662,6 @@ async function createDriver(driverType, databaseUrl, authToken) {
|
|
|
7465
7662
|
useNullAsDefault: true
|
|
7466
7663
|
});
|
|
7467
7664
|
}
|
|
7468
|
-
case "libsql":
|
|
7469
|
-
case "turso": {
|
|
7470
|
-
let TursoDriver;
|
|
7471
|
-
try {
|
|
7472
|
-
({ TursoDriver } = await import("@objectstack/driver-turso"));
|
|
7473
|
-
} catch (err) {
|
|
7474
|
-
throw new Error(
|
|
7475
|
-
`[ArtifactEnvironmentRegistry] libsql/turso driver requested but @objectstack/driver-turso is not installed. Install it with: npm install @objectstack/driver-turso. (${err?.message ?? err})`
|
|
7476
|
-
);
|
|
7477
|
-
}
|
|
7478
|
-
return new TursoDriver({ url: databaseUrl, authToken });
|
|
7479
|
-
}
|
|
7480
7665
|
case "postgres":
|
|
7481
7666
|
case "postgresql":
|
|
7482
7667
|
case "pg": {
|
|
@@ -7734,39 +7919,7 @@ var ArtifactKernelFactory = class {
|
|
|
7734
7919
|
// intentionally do NOT pass crossSubDomainCookies here
|
|
7735
7920
|
// so cookies stay isolated per project subdomain.
|
|
7736
7921
|
trustedOrigins: trustedOriginsList.length ? trustedOriginsList : void 0,
|
|
7737
|
-
...oidcProviders ? { oidcProviders } : {}
|
|
7738
|
-
// Auto-provision a personal organization for every new
|
|
7739
|
-
// user. SecurityPlugin's ObjectQL middleware does this
|
|
7740
|
-
// for direct `ql.insert` calls, but better-auth's
|
|
7741
|
-
// adapter writes through `dataEngine` directly,
|
|
7742
|
-
// bypassing that middleware — so JIT-created SSO users
|
|
7743
|
-
// would otherwise land on the empty "create
|
|
7744
|
-
// organization" screen on first login.
|
|
7745
|
-
databaseHooks: {
|
|
7746
|
-
user: {
|
|
7747
|
-
create: {
|
|
7748
|
-
after: async (user) => {
|
|
7749
|
-
try {
|
|
7750
|
-
const ql = kernel.getService("objectql");
|
|
7751
|
-
if (!ql) return;
|
|
7752
|
-
const [{ ensureUserHasOrganization, cloneTenantSeedData }] = await Promise.all([
|
|
7753
|
-
import("@objectstack/plugin-security")
|
|
7754
|
-
]);
|
|
7755
|
-
await ensureUserHasOrganization(ql, user, {
|
|
7756
|
-
logger: this.logger,
|
|
7757
|
-
cloneSeedData: cloneTenantSeedData
|
|
7758
|
-
});
|
|
7759
|
-
} catch (e) {
|
|
7760
|
-
this.logger.warn?.("[ArtifactKernelFactory] auto-org provisioning hook failed", {
|
|
7761
|
-
environmentId,
|
|
7762
|
-
userId: user?.id,
|
|
7763
|
-
error: e?.message
|
|
7764
|
-
});
|
|
7765
|
-
}
|
|
7766
|
-
}
|
|
7767
|
-
}
|
|
7768
|
-
}
|
|
7769
|
-
}
|
|
7922
|
+
...oidcProviders ? { oidcProviders } : {}
|
|
7770
7923
|
}));
|
|
7771
7924
|
if (oidcProviders) {
|
|
7772
7925
|
this.logger.info?.("[ArtifactKernelFactory] platform SSO wired", {
|
|
@@ -7784,9 +7937,20 @@ var ArtifactKernelFactory = class {
|
|
|
7784
7937
|
this.logger.warn?.("[ArtifactKernelFactory] OS_AUTH_SECRET not set \u2014 per-project AuthPlugin skipped (auth endpoints will return 404)", { environmentId });
|
|
7785
7938
|
}
|
|
7786
7939
|
try {
|
|
7940
|
+
const multiTenant = String(process.env.OS_MULTI_TENANT ?? "false").toLowerCase() !== "false";
|
|
7941
|
+
if (multiTenant) {
|
|
7942
|
+
try {
|
|
7943
|
+
const { OrgScopingPlugin } = await import("@objectstack/plugin-org-scoping");
|
|
7944
|
+
await kernel.use(new OrgScopingPlugin());
|
|
7945
|
+
} catch (err) {
|
|
7946
|
+
this.logger.warn?.("[ArtifactKernelFactory] OrgScopingPlugin not registered (multi-tenant disabled)", {
|
|
7947
|
+
environmentId,
|
|
7948
|
+
error: err?.message
|
|
7949
|
+
});
|
|
7950
|
+
}
|
|
7951
|
+
}
|
|
7787
7952
|
const { SecurityPlugin } = await import("@objectstack/plugin-security");
|
|
7788
|
-
|
|
7789
|
-
await kernel.use(new SecurityPlugin({ multiTenant }));
|
|
7953
|
+
await kernel.use(new SecurityPlugin());
|
|
7790
7954
|
} catch (err) {
|
|
7791
7955
|
this.logger.warn?.("[ArtifactKernelFactory] SecurityPlugin not registered", {
|
|
7792
7956
|
environmentId,
|
|
@@ -7794,8 +7958,15 @@ var ArtifactKernelFactory = class {
|
|
|
7794
7958
|
});
|
|
7795
7959
|
}
|
|
7796
7960
|
const projectName = project.hostname ?? environmentId;
|
|
7797
|
-
const
|
|
7798
|
-
const
|
|
7961
|
+
const artifactAny = artifact;
|
|
7962
|
+
const topLevelManifest = artifactAny?.manifest && typeof artifactAny.manifest === "object" ? artifactAny.manifest : null;
|
|
7963
|
+
const topLevelFunctions = Array.isArray(artifactAny?.functions) ? artifactAny.functions : [];
|
|
7964
|
+
const bundle = {
|
|
7965
|
+
...artifact.metadata ?? {},
|
|
7966
|
+
...topLevelManifest ? { manifest: topLevelManifest } : {},
|
|
7967
|
+
functions: topLevelFunctions
|
|
7968
|
+
};
|
|
7969
|
+
const sys = bundle.manifest ?? bundle;
|
|
7799
7970
|
const packageId = sys?.packageId ?? sys?.package_id ?? bundle?.packageId;
|
|
7800
7971
|
const i18nCfg = bundle?.i18n ?? sys?.i18n ?? {};
|
|
7801
7972
|
const trArr = Array.isArray(bundle?.translations) ? bundle.translations : Array.isArray(sys?.translations) ? sys.translations : [];
|
|
@@ -8305,6 +8476,40 @@ function resolveCloudUrl(explicit) {
|
|
|
8305
8476
|
|
|
8306
8477
|
// src/cloud/marketplace-proxy-plugin.ts
|
|
8307
8478
|
var MARKETPLACE_PREFIX = "/api/v1/marketplace";
|
|
8479
|
+
var DEFAULT_LRU_MAX = 200;
|
|
8480
|
+
var LIST_TTL_MS = 30 * 60 * 1e3;
|
|
8481
|
+
var PACKAGE_TTL_MS = 2 * 60 * 60 * 1e3;
|
|
8482
|
+
var VERSION_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
8483
|
+
function ttlForPath(pathname) {
|
|
8484
|
+
if (/\/packages\/[^/]+\/versions\//.test(pathname)) return VERSION_TTL_MS;
|
|
8485
|
+
if (/\/packages\/[^/]+/.test(pathname)) return PACKAGE_TTL_MS;
|
|
8486
|
+
return LIST_TTL_MS;
|
|
8487
|
+
}
|
|
8488
|
+
var LruTtlCache = class {
|
|
8489
|
+
constructor(max) {
|
|
8490
|
+
this.max = max;
|
|
8491
|
+
this.map = /* @__PURE__ */ new Map();
|
|
8492
|
+
}
|
|
8493
|
+
get(key) {
|
|
8494
|
+
const entry = this.map.get(key);
|
|
8495
|
+
if (!entry) return void 0;
|
|
8496
|
+
this.map.delete(key);
|
|
8497
|
+
this.map.set(key, entry);
|
|
8498
|
+
return entry;
|
|
8499
|
+
}
|
|
8500
|
+
set(key, entry) {
|
|
8501
|
+
if (this.map.has(key)) this.map.delete(key);
|
|
8502
|
+
this.map.set(key, entry);
|
|
8503
|
+
while (this.map.size > this.max) {
|
|
8504
|
+
const oldest = this.map.keys().next().value;
|
|
8505
|
+
if (oldest === void 0) break;
|
|
8506
|
+
this.map.delete(oldest);
|
|
8507
|
+
}
|
|
8508
|
+
}
|
|
8509
|
+
clear() {
|
|
8510
|
+
this.map.clear();
|
|
8511
|
+
}
|
|
8512
|
+
};
|
|
8308
8513
|
var MarketplaceProxyPlugin = class _MarketplaceProxyPlugin {
|
|
8309
8514
|
constructor(config = {}) {
|
|
8310
8515
|
this.name = "com.objectstack.runtime.marketplace-proxy";
|
|
@@ -8326,6 +8531,7 @@ var MarketplaceProxyPlugin = class _MarketplaceProxyPlugin {
|
|
|
8326
8531
|
}
|
|
8327
8532
|
const rawApp = httpServer.getRawApp();
|
|
8328
8533
|
const cloudUrl = this.cloudUrl;
|
|
8534
|
+
const cache = this.cache;
|
|
8329
8535
|
const handler = async (c, next) => {
|
|
8330
8536
|
if (!cloudUrl) {
|
|
8331
8537
|
return c.json({
|
|
@@ -8352,24 +8558,51 @@ var MarketplaceProxyPlugin = class _MarketplaceProxyPlugin {
|
|
|
8352
8558
|
}
|
|
8353
8559
|
}, 405);
|
|
8354
8560
|
}
|
|
8355
|
-
const
|
|
8356
|
-
|
|
8357
|
-
|
|
8358
|
-
|
|
8359
|
-
|
|
8360
|
-
|
|
8361
|
-
|
|
8362
|
-
|
|
8561
|
+
const accept = c.req.header("accept") ?? "application/json";
|
|
8562
|
+
const acceptLang = c.req.header("accept-language") ?? "";
|
|
8563
|
+
const cacheKey = `${incomingUrl.pathname}${incomingUrl.search}|al=${acceptLang}|a=${accept}`;
|
|
8564
|
+
const reqCacheCtl = (c.req.header("cache-control") ?? "").toLowerCase();
|
|
8565
|
+
const bypass = !cache || reqCacheCtl.includes("no-cache") || reqCacheCtl.includes("no-store");
|
|
8566
|
+
const now = Date.now();
|
|
8567
|
+
if (cache && !bypass) {
|
|
8568
|
+
const hit = cache.get(cacheKey);
|
|
8569
|
+
if (hit && hit.expiresAt > now) {
|
|
8570
|
+
return buildCachedResponse(hit, method, "HIT");
|
|
8363
8571
|
}
|
|
8364
|
-
|
|
8365
|
-
|
|
8366
|
-
|
|
8367
|
-
|
|
8368
|
-
|
|
8369
|
-
|
|
8572
|
+
if (hit) {
|
|
8573
|
+
const revalHeaders = {
|
|
8574
|
+
"Accept": accept,
|
|
8575
|
+
"User-Agent": `objectos-marketplace-proxy/${_MarketplaceProxyPlugin.prototype.version ?? "1.0.0"}`
|
|
8576
|
+
};
|
|
8577
|
+
if (acceptLang) revalHeaders["Accept-Language"] = acceptLang;
|
|
8578
|
+
if (hit.etag) revalHeaders["If-None-Match"] = hit.etag;
|
|
8579
|
+
if (hit.lastModified) revalHeaders["If-Modified-Since"] = hit.lastModified;
|
|
8580
|
+
const revalResp = await fetch(target, { method: "GET", headers: revalHeaders });
|
|
8581
|
+
if (revalResp.status === 304) {
|
|
8582
|
+
hit.expiresAt = now + hit.ttlMs;
|
|
8583
|
+
const newEtag = revalResp.headers.get("etag");
|
|
8584
|
+
const newLm = revalResp.headers.get("last-modified");
|
|
8585
|
+
if (newEtag) hit.etag = newEtag;
|
|
8586
|
+
if (newLm) hit.lastModified = newLm;
|
|
8587
|
+
cache.set(cacheKey, hit);
|
|
8588
|
+
return buildCachedResponse(hit, method, "REVALIDATED");
|
|
8589
|
+
}
|
|
8590
|
+
return await consumeAndMaybeCache(revalResp, cacheKey, incomingUrl.pathname, method, cache);
|
|
8591
|
+
}
|
|
8592
|
+
}
|
|
8593
|
+
const reqHeaders = {
|
|
8594
|
+
// Strip the inbound Host header — fetch will set
|
|
8595
|
+
// it to the cloud host. Forward only the
|
|
8596
|
+
// identifying headers cloud might log.
|
|
8597
|
+
"Accept": accept,
|
|
8598
|
+
"User-Agent": `objectos-marketplace-proxy/${_MarketplaceProxyPlugin.prototype.version ?? "1.0.0"}`
|
|
8599
|
+
};
|
|
8600
|
+
if (acceptLang) reqHeaders["Accept-Language"] = acceptLang;
|
|
8601
|
+
const resp = await fetch(target, { method: "GET", headers: reqHeaders });
|
|
8602
|
+
if (bypass || !cache) {
|
|
8603
|
+
return await passthroughResponse(resp, method, bypass ? "BYPASS" : "MISS");
|
|
8370
8604
|
}
|
|
8371
|
-
|
|
8372
|
-
return new Response(body, { status: resp.status, headers });
|
|
8605
|
+
return await consumeAndMaybeCache(resp, cacheKey, incomingUrl.pathname, method, cache);
|
|
8373
8606
|
} catch (err) {
|
|
8374
8607
|
const errObj = err instanceof Error ? err : new Error(err?.message ?? String(err));
|
|
8375
8608
|
ctx.logger?.error?.("[MarketplaceProxyPlugin] proxy failed", errObj);
|
|
@@ -8392,12 +8625,67 @@ var MarketplaceProxyPlugin = class _MarketplaceProxyPlugin {
|
|
|
8392
8625
|
}
|
|
8393
8626
|
}
|
|
8394
8627
|
}
|
|
8395
|
-
ctx.logger?.info?.(`[MarketplaceProxyPlugin] mounted at ${MARKETPLACE_PREFIX}/* \u2192 ${cloudUrl || "(unconfigured)"}`);
|
|
8628
|
+
ctx.logger?.info?.(`[MarketplaceProxyPlugin] mounted at ${MARKETPLACE_PREFIX}/* \u2192 ${cloudUrl || "(unconfigured)"} (cache=${this.cache ? "on" : "off"})`);
|
|
8396
8629
|
});
|
|
8397
8630
|
};
|
|
8398
8631
|
this.cloudUrl = resolveCloudUrl(config.controlPlaneUrl);
|
|
8632
|
+
const envFlag = (process.env.OS_MARKETPLACE_CACHE ?? "").trim().toLowerCase();
|
|
8633
|
+
const envDisabled = ["off", "false", "0", "no", "disable", "disabled"].includes(envFlag);
|
|
8634
|
+
const disabled = config.cacheDisabled ?? envDisabled;
|
|
8635
|
+
this.cache = disabled ? null : new LruTtlCache(Math.max(8, config.cacheMaxEntries ?? DEFAULT_LRU_MAX));
|
|
8399
8636
|
}
|
|
8400
8637
|
};
|
|
8638
|
+
var PASSTHROUGH_HEADERS = ["content-type", "cache-control", "etag", "last-modified", "vary"];
|
|
8639
|
+
function collectHeaders(src) {
|
|
8640
|
+
const out = {};
|
|
8641
|
+
for (const h of PASSTHROUGH_HEADERS) {
|
|
8642
|
+
const v = src.headers.get(h);
|
|
8643
|
+
if (v) out[h] = v;
|
|
8644
|
+
}
|
|
8645
|
+
return out;
|
|
8646
|
+
}
|
|
8647
|
+
function buildCachedResponse(entry, method, xCache) {
|
|
8648
|
+
const headers = new Headers(entry.headers);
|
|
8649
|
+
headers.set("X-Cache", xCache);
|
|
8650
|
+
const ageSec = Math.max(0, Math.floor((entry.expiresAt - entry.ttlMs - Date.now()) / -1e3));
|
|
8651
|
+
headers.set("Age", String(Math.max(0, ageSec)));
|
|
8652
|
+
const body = method === "HEAD" ? null : entry.body;
|
|
8653
|
+
return new Response(body, { status: entry.status, headers });
|
|
8654
|
+
}
|
|
8655
|
+
async function passthroughResponse(resp, method, xCache) {
|
|
8656
|
+
const headers = new Headers(collectHeaders(resp));
|
|
8657
|
+
headers.set("X-Cache", xCache);
|
|
8658
|
+
if (method === "HEAD") {
|
|
8659
|
+
try {
|
|
8660
|
+
await resp.arrayBuffer();
|
|
8661
|
+
} catch {
|
|
8662
|
+
}
|
|
8663
|
+
return new Response(null, { status: resp.status, headers });
|
|
8664
|
+
}
|
|
8665
|
+
const body = await resp.arrayBuffer();
|
|
8666
|
+
return new Response(body, { status: resp.status, headers });
|
|
8667
|
+
}
|
|
8668
|
+
async function consumeAndMaybeCache(resp, key, pathname, method, cache) {
|
|
8669
|
+
const body = await resp.arrayBuffer();
|
|
8670
|
+
const headers = collectHeaders(resp);
|
|
8671
|
+
if (resp.status >= 200 && resp.status < 300) {
|
|
8672
|
+
const ttlMs = ttlForPath(pathname);
|
|
8673
|
+
const entry = {
|
|
8674
|
+
status: resp.status,
|
|
8675
|
+
body,
|
|
8676
|
+
headers,
|
|
8677
|
+
etag: resp.headers.get("etag") ?? void 0,
|
|
8678
|
+
lastModified: resp.headers.get("last-modified") ?? void 0,
|
|
8679
|
+
expiresAt: Date.now() + ttlMs,
|
|
8680
|
+
ttlMs
|
|
8681
|
+
};
|
|
8682
|
+
cache.set(key, entry);
|
|
8683
|
+
}
|
|
8684
|
+
const respHeaders = new Headers(headers);
|
|
8685
|
+
respHeaders.set("X-Cache", "MISS");
|
|
8686
|
+
const outBody = method === "HEAD" ? null : body;
|
|
8687
|
+
return new Response(outBody, { status: resp.status, headers: respHeaders });
|
|
8688
|
+
}
|
|
8401
8689
|
|
|
8402
8690
|
// src/cloud/runtime-config-plugin.ts
|
|
8403
8691
|
var RuntimeConfigPlugin = class {
|
|
@@ -8435,12 +8723,14 @@ var RuntimeConfigPlugin = class {
|
|
|
8435
8723
|
let defaultEnvironmentId;
|
|
8436
8724
|
let defaultOrgId;
|
|
8437
8725
|
let resolvedSingleEnv = this.singleEnvironment;
|
|
8438
|
-
|
|
8726
|
+
const resolveFn = typeof envRegistry?.resolveByHostname === "function" ? envRegistry.resolveByHostname.bind(envRegistry) : typeof envRegistry?.resolveHostname === "function" ? envRegistry.resolveHostname.bind(envRegistry) : null;
|
|
8727
|
+
if (resolveFn && host) {
|
|
8439
8728
|
try {
|
|
8440
|
-
const resolved = await
|
|
8729
|
+
const resolved = await resolveFn(host);
|
|
8441
8730
|
if (resolved?.environmentId) {
|
|
8442
|
-
defaultEnvironmentId = resolved.environmentId;
|
|
8443
|
-
|
|
8731
|
+
defaultEnvironmentId = String(resolved.environmentId);
|
|
8732
|
+
const orgId = resolved.organizationId ?? resolved.organization_id;
|
|
8733
|
+
if (orgId) defaultOrgId = String(orgId);
|
|
8444
8734
|
resolvedSingleEnv = true;
|
|
8445
8735
|
}
|
|
8446
8736
|
} catch {
|
|
@@ -8451,7 +8741,11 @@ var RuntimeConfigPlugin = class {
|
|
|
8451
8741
|
singleEnvironment: resolvedSingleEnv,
|
|
8452
8742
|
defaultOrgId,
|
|
8453
8743
|
defaultEnvironmentId,
|
|
8454
|
-
features
|
|
8744
|
+
features,
|
|
8745
|
+
branding: {
|
|
8746
|
+
productName: this.productName,
|
|
8747
|
+
productShortName: this.productShortName
|
|
8748
|
+
}
|
|
8455
8749
|
});
|
|
8456
8750
|
};
|
|
8457
8751
|
rawApp.get("/api/v1/runtime/config", handler);
|
|
@@ -8468,6 +8762,10 @@ var RuntimeConfigPlugin = class {
|
|
|
8468
8762
|
this.cloudUrl = config.controlPlaneUrl === "" ? "" : resolveCloudUrl(config.controlPlaneUrl) ?? "";
|
|
8469
8763
|
this.installLocal = !!config.installLocal;
|
|
8470
8764
|
this.singleEnvironment = !!config.singleEnvironment;
|
|
8765
|
+
const envName = (typeof process !== "undefined" ? process.env?.OS_PRODUCT_NAME : void 0)?.trim();
|
|
8766
|
+
const envShort = (typeof process !== "undefined" ? process.env?.OS_PRODUCT_SHORT_NAME : void 0)?.trim();
|
|
8767
|
+
this.productName = (config.productName ?? envName ?? "ObjectOS").trim() || "ObjectOS";
|
|
8768
|
+
this.productShortName = (config.productShortName ?? envShort ?? this.productName).trim() || this.productName;
|
|
8471
8769
|
}
|
|
8472
8770
|
};
|
|
8473
8771
|
|
|
@@ -8757,9 +9055,15 @@ var MarketplaceInstallLocalPlugin = class {
|
|
|
8757
9055
|
const postHandler = async (c) => this.handleInstall(c, ctx);
|
|
8758
9056
|
const getHandler = async (c) => this.handleList(c);
|
|
8759
9057
|
const deleteHandler = async (c) => this.handleUninstall(c, ctx);
|
|
9058
|
+
const reseedHandler = async (c) => this.handleReseed(c, ctx);
|
|
9059
|
+
const purgeHandler = async (c) => this.handlePurge(c, ctx);
|
|
8760
9060
|
if (typeof rawApp.post === "function") rawApp.post(ROUTE_BASE, postHandler);
|
|
8761
9061
|
if (typeof rawApp.get === "function") rawApp.get(ROUTE_BASE, getHandler);
|
|
8762
9062
|
if (typeof rawApp.delete === "function") rawApp.delete(`${ROUTE_BASE}/:manifestId`, deleteHandler);
|
|
9063
|
+
if (typeof rawApp.post === "function") {
|
|
9064
|
+
rawApp.post(`${ROUTE_BASE}/:manifestId/reseed-sample-data`, reseedHandler);
|
|
9065
|
+
rawApp.post(`${ROUTE_BASE}/:manifestId/purge-sample-data`, purgeHandler);
|
|
9066
|
+
}
|
|
8763
9067
|
ctx.logger?.info?.(`[MarketplaceInstallLocal] mounted at ${ROUTE_BASE} (storage: ${this.storageDir})`);
|
|
8764
9068
|
});
|
|
8765
9069
|
};
|
|
@@ -8856,7 +9160,8 @@ var MarketplaceInstallLocalPlugin = class {
|
|
|
8856
9160
|
version,
|
|
8857
9161
|
manifest,
|
|
8858
9162
|
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8859
|
-
installedBy: userId
|
|
9163
|
+
installedBy: userId,
|
|
9164
|
+
withSampleData: false
|
|
8860
9165
|
};
|
|
8861
9166
|
try {
|
|
8862
9167
|
(0, import_node_fs3.mkdirSync)(this.storageDir, { recursive: true });
|
|
@@ -8883,6 +9188,13 @@ var MarketplaceInstallLocalPlugin = class {
|
|
|
8883
9188
|
ctx.logger?.warn?.(`[MarketplaceInstallLocal] syncSchemas failed for ${manifestId}: ${err?.message ?? err}`);
|
|
8884
9189
|
}
|
|
8885
9190
|
const seededSummary = await this.applySideEffects(ctx, manifest, { seedNow: true, c });
|
|
9191
|
+
if (seededSummary.seeded.mode === "inline" && (seededSummary.seeded.inserted ?? 0) + (seededSummary.seeded.updated ?? 0) > 0) {
|
|
9192
|
+
entry.withSampleData = true;
|
|
9193
|
+
try {
|
|
9194
|
+
(0, import_node_fs3.writeFileSync)((0, import_node_path6.join)(this.storageDir, safeFilename(manifestId)), JSON.stringify(entry, null, 2), "utf8");
|
|
9195
|
+
} catch {
|
|
9196
|
+
}
|
|
9197
|
+
}
|
|
8886
9198
|
return c.json({
|
|
8887
9199
|
success: true,
|
|
8888
9200
|
data: {
|
|
@@ -8909,7 +9221,8 @@ var MarketplaceInstallLocalPlugin = class {
|
|
|
8909
9221
|
manifestId: e.manifestId,
|
|
8910
9222
|
version: e.version,
|
|
8911
9223
|
installedAt: e.installedAt,
|
|
8912
|
-
installedBy: e.installedBy
|
|
9224
|
+
installedBy: e.installedBy,
|
|
9225
|
+
withSampleData: e.withSampleData ?? false
|
|
8913
9226
|
})),
|
|
8914
9227
|
total: entries.length,
|
|
8915
9228
|
storageDir: this.storageDir
|
|
@@ -8974,6 +9287,145 @@ var MarketplaceInstallLocalPlugin = class {
|
|
|
8974
9287
|
* dev / single-tenant runtimes. Stricter checks can be layered on
|
|
8975
9288
|
* via a middleware in cloud-hosted multi-tenant deployments.
|
|
8976
9289
|
*/
|
|
9290
|
+
/**
|
|
9291
|
+
* POST /api/v1/marketplace/install-local/:manifestId/reseed-sample-data
|
|
9292
|
+
*
|
|
9293
|
+
* Re-runs SeedLoaderService against the cached manifest's `data` arrays.
|
|
9294
|
+
* Idempotent (upsert by id). Useful when:
|
|
9295
|
+
* • The user installed an app and skipped sample data
|
|
9296
|
+
* • A purge was undone
|
|
9297
|
+
* • The user wants a clean baseline back after editing demo rows
|
|
9298
|
+
*
|
|
9299
|
+
* Multi-tenant: requires an active organization on the session (same
|
|
9300
|
+
* rule as install seed path).
|
|
9301
|
+
*/
|
|
9302
|
+
this.handleReseed = async (c, ctx) => {
|
|
9303
|
+
const userId = await this.requireAuthenticatedUser(c, ctx);
|
|
9304
|
+
if (!userId) {
|
|
9305
|
+
return c.json({ success: false, error: { code: "unauthorized", message: "Authentication required." } }, 401);
|
|
9306
|
+
}
|
|
9307
|
+
const manifestId = String(c.req.param?.("manifestId") ?? c.req.params?.manifestId ?? "").trim();
|
|
9308
|
+
if (!manifestId) {
|
|
9309
|
+
return c.json({ success: false, error: { code: "bad_request", message: "manifestId path param required." } }, 400);
|
|
9310
|
+
}
|
|
9311
|
+
const file = (0, import_node_path6.join)(this.storageDir, safeFilename(manifestId));
|
|
9312
|
+
if (!(0, import_node_fs3.existsSync)(file)) {
|
|
9313
|
+
return c.json({ success: false, error: { code: "not_found", message: `No marketplace install for ${manifestId}.` } }, 404);
|
|
9314
|
+
}
|
|
9315
|
+
let entry;
|
|
9316
|
+
try {
|
|
9317
|
+
entry = JSON.parse((0, import_node_fs3.readFileSync)(file, "utf8"));
|
|
9318
|
+
} catch (err) {
|
|
9319
|
+
return c.json({ success: false, error: { code: "storage_failed", message: `Failed to read manifest cache: ${err?.message ?? err}` } }, 500);
|
|
9320
|
+
}
|
|
9321
|
+
const summary = await this.applySideEffects(ctx, entry.manifest, { seedNow: true, c });
|
|
9322
|
+
if (summary.seeded.mode === "skipped") {
|
|
9323
|
+
return c.json({
|
|
9324
|
+
success: false,
|
|
9325
|
+
error: {
|
|
9326
|
+
code: "reseed_skipped",
|
|
9327
|
+
message: `Reseed did not run: ${summary.seeded.reason ?? "unknown reason"}`
|
|
9328
|
+
}
|
|
9329
|
+
}, 400);
|
|
9330
|
+
}
|
|
9331
|
+
try {
|
|
9332
|
+
entry.withSampleData = true;
|
|
9333
|
+
(0, import_node_fs3.writeFileSync)(file, JSON.stringify(entry, null, 2), "utf8");
|
|
9334
|
+
} catch {
|
|
9335
|
+
}
|
|
9336
|
+
return c.json({
|
|
9337
|
+
success: true,
|
|
9338
|
+
data: {
|
|
9339
|
+
manifestId,
|
|
9340
|
+
inserted: summary.seeded.inserted ?? 0,
|
|
9341
|
+
updated: summary.seeded.updated ?? 0,
|
|
9342
|
+
errors: summary.seeded.errors ?? 0,
|
|
9343
|
+
withSampleData: true
|
|
9344
|
+
}
|
|
9345
|
+
}, 200);
|
|
9346
|
+
};
|
|
9347
|
+
/**
|
|
9348
|
+
* POST /api/v1/marketplace/install-local/:manifestId/purge-sample-data
|
|
9349
|
+
*
|
|
9350
|
+
* Deletes every record whose id is declared in the cached manifest's
|
|
9351
|
+
* seed datasets. Uses the `driver` service directly to bypass ACL /
|
|
9352
|
+
* lifecycle hooks (same pattern as cloud purge). User-created records
|
|
9353
|
+
* are never touched — only ids declared in the package's bundled
|
|
9354
|
+
* datasets are removed. Already-deleted rows count as `skipped`.
|
|
9355
|
+
*/
|
|
9356
|
+
this.handlePurge = async (c, ctx) => {
|
|
9357
|
+
const userId = await this.requireAuthenticatedUser(c, ctx);
|
|
9358
|
+
if (!userId) {
|
|
9359
|
+
return c.json({ success: false, error: { code: "unauthorized", message: "Authentication required." } }, 401);
|
|
9360
|
+
}
|
|
9361
|
+
const manifestId = String(c.req.param?.("manifestId") ?? c.req.params?.manifestId ?? "").trim();
|
|
9362
|
+
if (!manifestId) {
|
|
9363
|
+
return c.json({ success: false, error: { code: "bad_request", message: "manifestId path param required." } }, 400);
|
|
9364
|
+
}
|
|
9365
|
+
const file = (0, import_node_path6.join)(this.storageDir, safeFilename(manifestId));
|
|
9366
|
+
if (!(0, import_node_fs3.existsSync)(file)) {
|
|
9367
|
+
return c.json({ success: false, error: { code: "not_found", message: `No marketplace install for ${manifestId}.` } }, 404);
|
|
9368
|
+
}
|
|
9369
|
+
let entry;
|
|
9370
|
+
try {
|
|
9371
|
+
entry = JSON.parse((0, import_node_fs3.readFileSync)(file, "utf8"));
|
|
9372
|
+
} catch (err) {
|
|
9373
|
+
return c.json({ success: false, error: { code: "storage_failed", message: `Failed to read manifest cache: ${err?.message ?? err}` } }, 500);
|
|
9374
|
+
}
|
|
9375
|
+
const datasets = Array.isArray(entry.manifest?.data) ? entry.manifest.data.filter((d) => d && d.object && Array.isArray(d.records)) : [];
|
|
9376
|
+
if (datasets.length === 0) {
|
|
9377
|
+
return c.json({
|
|
9378
|
+
success: false,
|
|
9379
|
+
error: { code: "nothing_to_purge", message: "This package declares no seed datasets." }
|
|
9380
|
+
}, 400);
|
|
9381
|
+
}
|
|
9382
|
+
let driver;
|
|
9383
|
+
try {
|
|
9384
|
+
driver = ctx.getService("driver");
|
|
9385
|
+
} catch {
|
|
9386
|
+
}
|
|
9387
|
+
if (!driver || typeof driver.delete !== "function") {
|
|
9388
|
+
return c.json({
|
|
9389
|
+
success: false,
|
|
9390
|
+
error: { code: "driver_missing", message: "driver service unavailable \u2014 cannot purge." }
|
|
9391
|
+
}, 500);
|
|
9392
|
+
}
|
|
9393
|
+
let deleted = 0;
|
|
9394
|
+
let skipped = 0;
|
|
9395
|
+
let errors = 0;
|
|
9396
|
+
for (const ds of datasets) {
|
|
9397
|
+
const object = String(ds.object);
|
|
9398
|
+
for (const rec of ds.records) {
|
|
9399
|
+
const id = rec?.id;
|
|
9400
|
+
if (id === void 0 || id === null || id === "") {
|
|
9401
|
+
skipped++;
|
|
9402
|
+
continue;
|
|
9403
|
+
}
|
|
9404
|
+
try {
|
|
9405
|
+
const r = await driver.delete(object, id);
|
|
9406
|
+
if (r === false || r === 0 || r?.deleted === 0) skipped++;
|
|
9407
|
+
else deleted++;
|
|
9408
|
+
} catch (err) {
|
|
9409
|
+
const msg = String(err?.message ?? err);
|
|
9410
|
+
if (/not.?found|no row/i.test(msg)) skipped++;
|
|
9411
|
+
else {
|
|
9412
|
+
errors++;
|
|
9413
|
+
ctx.logger?.warn?.(`[MarketplaceInstallLocal] purge ${object}#${id}: ${msg}`);
|
|
9414
|
+
}
|
|
9415
|
+
}
|
|
9416
|
+
}
|
|
9417
|
+
}
|
|
9418
|
+
try {
|
|
9419
|
+
entry.withSampleData = false;
|
|
9420
|
+
(0, import_node_fs3.writeFileSync)(file, JSON.stringify(entry, null, 2), "utf8");
|
|
9421
|
+
} catch {
|
|
9422
|
+
}
|
|
9423
|
+
ctx.logger?.info?.(`[MarketplaceInstallLocal] purged ${manifestId}: deleted=${deleted} skipped=${skipped} errors=${errors}`);
|
|
9424
|
+
return c.json({
|
|
9425
|
+
success: true,
|
|
9426
|
+
data: { manifestId, deleted, skipped, errors, withSampleData: false }
|
|
9427
|
+
}, 200);
|
|
9428
|
+
};
|
|
8977
9429
|
/**
|
|
8978
9430
|
* Replicate the start-time side-effects that AppPlugin runs for
|
|
8979
9431
|
* statically-declared apps but the `manifest` service does NOT:
|
|
@@ -9063,7 +9515,7 @@ var MarketplaceInstallLocalPlugin = class {
|
|
|
9063
9515
|
}
|
|
9064
9516
|
}
|
|
9065
9517
|
if (opts.seedNow && datasets.length > 0) {
|
|
9066
|
-
const multiTenant = String(process.env.OS_MULTI_TENANT ?? "
|
|
9518
|
+
const multiTenant = String(process.env.OS_MULTI_TENANT ?? "false").toLowerCase() !== "false";
|
|
9067
9519
|
try {
|
|
9068
9520
|
const ql = ctx.getService("objectql");
|
|
9069
9521
|
let metadata;
|