@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.js
CHANGED
|
@@ -1271,7 +1271,15 @@ var init_app_plugin = __esm({
|
|
|
1271
1271
|
AppPlugin = class {
|
|
1272
1272
|
constructor(bundle, projectContext) {
|
|
1273
1273
|
this.type = "app";
|
|
1274
|
+
/** When true, init/start become no-ops — env has no app payload. */
|
|
1275
|
+
this.empty = false;
|
|
1274
1276
|
this.init = async (ctx) => {
|
|
1277
|
+
if (this.empty) {
|
|
1278
|
+
ctx.logger.debug("[AppPlugin] empty env \u2014 no app payload, skipping init", {
|
|
1279
|
+
pluginName: this.name
|
|
1280
|
+
});
|
|
1281
|
+
return;
|
|
1282
|
+
}
|
|
1275
1283
|
const sys = this.bundle.manifest || this.bundle;
|
|
1276
1284
|
const appId = sys.id || sys.name;
|
|
1277
1285
|
ctx.logger.info("Registering App Service", {
|
|
@@ -1286,6 +1294,12 @@ var init_app_plugin = __esm({
|
|
|
1286
1294
|
ctx.getService("manifest").register(servicePayload);
|
|
1287
1295
|
};
|
|
1288
1296
|
this.start = async (ctx) => {
|
|
1297
|
+
if (this.empty) {
|
|
1298
|
+
ctx.logger.debug("[AppPlugin] empty env \u2014 no app payload, skipping start", {
|
|
1299
|
+
pluginName: this.name
|
|
1300
|
+
});
|
|
1301
|
+
return;
|
|
1302
|
+
}
|
|
1289
1303
|
const sys = this.bundle.manifest || this.bundle;
|
|
1290
1304
|
const appId = sys.id || sys.name;
|
|
1291
1305
|
let ql;
|
|
@@ -1445,6 +1459,66 @@ var init_app_plugin = __esm({
|
|
|
1445
1459
|
} catch (err) {
|
|
1446
1460
|
ctx.logger.error("[AppPlugin] Failed to schedule approval-process registration", err, { appId });
|
|
1447
1461
|
}
|
|
1462
|
+
try {
|
|
1463
|
+
const jobs = Array.isArray(this.bundle.jobs) ? this.bundle.jobs : Array.isArray((this.bundle.manifest || {}).jobs) ? this.bundle.manifest.jobs : [];
|
|
1464
|
+
if (jobs.length > 0) {
|
|
1465
|
+
ctx.hook("kernel:ready", async () => {
|
|
1466
|
+
let svc;
|
|
1467
|
+
try {
|
|
1468
|
+
svc = ctx.getService("job");
|
|
1469
|
+
} catch {
|
|
1470
|
+
}
|
|
1471
|
+
if (!svc || typeof svc.schedule !== "function") {
|
|
1472
|
+
ctx.logger.warn("[AppPlugin] job service not registered \u2014 skipping declarative jobs", {
|
|
1473
|
+
appId,
|
|
1474
|
+
jobCount: jobs.length
|
|
1475
|
+
});
|
|
1476
|
+
return;
|
|
1477
|
+
}
|
|
1478
|
+
const fnMap = collectBundleFunctions(this.bundle);
|
|
1479
|
+
let ok = 0;
|
|
1480
|
+
for (const job of jobs) {
|
|
1481
|
+
const jobName = job?.name;
|
|
1482
|
+
if (!jobName) {
|
|
1483
|
+
ctx.logger.warn("[AppPlugin] skipping job without name", { appId, job });
|
|
1484
|
+
continue;
|
|
1485
|
+
}
|
|
1486
|
+
if (job.enabled === false) {
|
|
1487
|
+
ctx.logger.debug("[AppPlugin] job disabled \u2014 skipping", { appId, job: jobName });
|
|
1488
|
+
continue;
|
|
1489
|
+
}
|
|
1490
|
+
const handler = fnMap[job.handler];
|
|
1491
|
+
if (typeof handler !== "function") {
|
|
1492
|
+
ctx.logger.warn("[AppPlugin] job handler not found in bundle.functions \u2014 skipping", {
|
|
1493
|
+
appId,
|
|
1494
|
+
job: jobName,
|
|
1495
|
+
handler: job.handler
|
|
1496
|
+
});
|
|
1497
|
+
continue;
|
|
1498
|
+
}
|
|
1499
|
+
try {
|
|
1500
|
+
await svc.schedule(
|
|
1501
|
+
jobName,
|
|
1502
|
+
job.schedule,
|
|
1503
|
+
async (jobCtx) => {
|
|
1504
|
+
await handler({ ...jobCtx, jobId: jobName, bundle: this.bundle });
|
|
1505
|
+
}
|
|
1506
|
+
);
|
|
1507
|
+
ok++;
|
|
1508
|
+
} catch (err) {
|
|
1509
|
+
ctx.logger.warn("[AppPlugin] Failed to schedule job", {
|
|
1510
|
+
appId,
|
|
1511
|
+
job: jobName,
|
|
1512
|
+
error: err?.message ?? String(err)
|
|
1513
|
+
});
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
ctx.logger.info("[AppPlugin] Scheduled background jobs", { appId, count: ok });
|
|
1517
|
+
});
|
|
1518
|
+
}
|
|
1519
|
+
} catch (err) {
|
|
1520
|
+
ctx.logger.error("[AppPlugin] Failed to schedule background-job registration", err, { appId });
|
|
1521
|
+
}
|
|
1448
1522
|
this.emitCatalogEvent(ctx, "app:registered", sys);
|
|
1449
1523
|
await this.loadTranslations(ctx, appId);
|
|
1450
1524
|
const seedDatasets = [];
|
|
@@ -1517,51 +1591,68 @@ var init_app_plugin = __esm({
|
|
|
1517
1591
|
} catch (e) {
|
|
1518
1592
|
ctx.logger.warn("[Seeder] Failed to register seed-datasets/seed-replayer service", { error: e?.message });
|
|
1519
1593
|
}
|
|
1520
|
-
const multiTenant = String(process.env.OS_MULTI_TENANT ?? "
|
|
1594
|
+
const multiTenant = String(process.env.OS_MULTI_TENANT ?? "false").toLowerCase() !== "false";
|
|
1521
1595
|
if (multiTenant) {
|
|
1522
1596
|
ctx.logger.info("[Seeder] multi-tenant mode \u2014 skipping inline seed; per-org replay will run on sys_organization insert");
|
|
1523
1597
|
} else {
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
const
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1598
|
+
const seedBudgetMs = Number(process.env.OS_INLINE_SEED_BUDGET_MS ?? 8e3);
|
|
1599
|
+
const seedPromise = (async () => {
|
|
1600
|
+
try {
|
|
1601
|
+
const metadata = ctx.getService("metadata");
|
|
1602
|
+
if (metadata) {
|
|
1603
|
+
const seedLoader = new SeedLoaderService(ql, metadata, ctx.logger);
|
|
1604
|
+
const { SeedLoaderRequestSchema } = await import("@objectstack/spec/data");
|
|
1605
|
+
const request = SeedLoaderRequestSchema.parse({
|
|
1606
|
+
datasets: normalizedDatasets,
|
|
1607
|
+
config: { defaultMode: "upsert", multiPass: true }
|
|
1608
|
+
});
|
|
1609
|
+
const result = await seedLoader.load(request);
|
|
1610
|
+
ctx.logger.info("[Seeder] Seed loading complete", {
|
|
1611
|
+
inserted: result.summary.totalInserted,
|
|
1612
|
+
updated: result.summary.totalUpdated,
|
|
1613
|
+
errors: result.errors.length
|
|
1614
|
+
});
|
|
1615
|
+
} else {
|
|
1616
|
+
ctx.logger.debug("[Seeder] No metadata service; using basic insert fallback");
|
|
1617
|
+
for (const dataset of normalizedDatasets) {
|
|
1618
|
+
ctx.logger.info(`[Seeder] Seeding ${dataset.records.length} records for ${dataset.object}`);
|
|
1619
|
+
for (const record of dataset.records) {
|
|
1620
|
+
try {
|
|
1621
|
+
await ql.insert(dataset.object, record, { context: { isSystem: true } });
|
|
1622
|
+
} catch (err) {
|
|
1623
|
+
ctx.logger.warn(`[Seeder] Failed to insert ${dataset.object} record:`, { error: err.message });
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
ctx.logger.info("[Seeder] Data seeding complete.");
|
|
1628
|
+
}
|
|
1629
|
+
} catch (err) {
|
|
1630
|
+
ctx.logger.warn("[Seeder] SeedLoaderService failed, falling back to basic insert", { error: err.message });
|
|
1541
1631
|
for (const dataset of normalizedDatasets) {
|
|
1542
|
-
ctx.logger.info(`[Seeder] Seeding ${dataset.records.length} records for ${dataset.object}`);
|
|
1543
1632
|
for (const record of dataset.records) {
|
|
1544
1633
|
try {
|
|
1545
1634
|
await ql.insert(dataset.object, record, { context: { isSystem: true } });
|
|
1546
|
-
} catch (
|
|
1547
|
-
ctx.logger.warn(`[Seeder] Failed to insert ${dataset.object} record:`, { error:
|
|
1635
|
+
} catch (insertErr) {
|
|
1636
|
+
ctx.logger.warn(`[Seeder] Failed to insert ${dataset.object} record:`, { error: insertErr.message });
|
|
1548
1637
|
}
|
|
1549
1638
|
}
|
|
1550
1639
|
}
|
|
1551
|
-
ctx.logger.info("[Seeder] Data seeding complete.");
|
|
1640
|
+
ctx.logger.info("[Seeder] Data seeding complete (fallback).");
|
|
1552
1641
|
}
|
|
1553
|
-
}
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
}
|
|
1563
|
-
|
|
1564
|
-
|
|
1642
|
+
})();
|
|
1643
|
+
let timer;
|
|
1644
|
+
const budget = new Promise((resolve2) => {
|
|
1645
|
+
timer = setTimeout(() => resolve2("budget"), seedBudgetMs);
|
|
1646
|
+
});
|
|
1647
|
+
const winner = await Promise.race([seedPromise.then(() => "done"), budget]);
|
|
1648
|
+
if (timer) clearTimeout(timer);
|
|
1649
|
+
if (winner === "budget") {
|
|
1650
|
+
ctx.logger.warn(
|
|
1651
|
+
`[Seeder] Inline seed exceeded ${seedBudgetMs}ms budget for ${appId}; continuing in background to avoid blocking kernel start.`
|
|
1652
|
+
);
|
|
1653
|
+
seedPromise.catch((err) => {
|
|
1654
|
+
ctx.logger.warn("[Seeder] Background seed failed after budget", { appId, error: err?.message ?? String(err) });
|
|
1655
|
+
});
|
|
1565
1656
|
}
|
|
1566
1657
|
}
|
|
1567
1658
|
}
|
|
@@ -1572,10 +1663,54 @@ var init_app_plugin = __esm({
|
|
|
1572
1663
|
};
|
|
1573
1664
|
this.bundle = bundle;
|
|
1574
1665
|
this.projectContext = projectContext;
|
|
1575
|
-
const sys = bundle
|
|
1576
|
-
const appId = sys
|
|
1666
|
+
const sys = bundle?.manifest || bundle;
|
|
1667
|
+
const appId = sys?.id || sys?.name;
|
|
1668
|
+
if (!appId) {
|
|
1669
|
+
const APP_CATEGORY_KEYS = [
|
|
1670
|
+
"objects",
|
|
1671
|
+
"views",
|
|
1672
|
+
"apps",
|
|
1673
|
+
"pages",
|
|
1674
|
+
"dashboards",
|
|
1675
|
+
"reports",
|
|
1676
|
+
"flows",
|
|
1677
|
+
"workflows",
|
|
1678
|
+
"triggers",
|
|
1679
|
+
"agents",
|
|
1680
|
+
"tools",
|
|
1681
|
+
"skills",
|
|
1682
|
+
"actions",
|
|
1683
|
+
"permissions",
|
|
1684
|
+
"roles",
|
|
1685
|
+
"profiles",
|
|
1686
|
+
"translations",
|
|
1687
|
+
"sharingRules",
|
|
1688
|
+
"ragPipelines",
|
|
1689
|
+
"data"
|
|
1690
|
+
];
|
|
1691
|
+
const hasAppPayload = APP_CATEGORY_KEYS.some((k) => {
|
|
1692
|
+
const v = (bundle && bundle[k]) ?? (sys && sys[k]);
|
|
1693
|
+
return Array.isArray(v) && v.length > 0;
|
|
1694
|
+
});
|
|
1695
|
+
if (!hasAppPayload) {
|
|
1696
|
+
this.empty = true;
|
|
1697
|
+
const envSlug = projectContext?.environmentId ? projectContext.environmentId.slice(0, 8) : "empty";
|
|
1698
|
+
this.name = `plugin.app.empty-${envSlug}`;
|
|
1699
|
+
return;
|
|
1700
|
+
}
|
|
1701
|
+
const bundleKeys = bundle && typeof bundle === "object" ? Object.keys(bundle).slice(0, 20).join(",") : typeof bundle;
|
|
1702
|
+
const sysKeys = sys && typeof sys === "object" ? Object.keys(sys).slice(0, 20).join(",") : typeof sys;
|
|
1703
|
+
const ctxHint = projectContext ? ` projectContext=${JSON.stringify({
|
|
1704
|
+
environmentId: projectContext.environmentId,
|
|
1705
|
+
packageId: projectContext.packageId,
|
|
1706
|
+
source: projectContext.source
|
|
1707
|
+
})}` : "";
|
|
1708
|
+
throw new Error(
|
|
1709
|
+
`[AppPlugin] bundle has app payload but no manifest.id / manifest.name \u2014 cannot register as a plugin. bundleKeys=[${bundleKeys}] sysKeys=[${sysKeys}]${ctxHint}`
|
|
1710
|
+
);
|
|
1711
|
+
}
|
|
1577
1712
|
this.name = `plugin.app.${appId}`;
|
|
1578
|
-
this.version = sys
|
|
1713
|
+
this.version = sys?.version;
|
|
1579
1714
|
}
|
|
1580
1715
|
/**
|
|
1581
1716
|
* Emit a kernel hook so the control-plane `AppCatalogService` can
|
|
@@ -2120,7 +2255,7 @@ function resolveObjectStackHome() {
|
|
|
2120
2255
|
var StandaloneStackConfigSchema = z.object({
|
|
2121
2256
|
databaseUrl: z.string().optional(),
|
|
2122
2257
|
databaseAuthToken: z.string().optional(),
|
|
2123
|
-
databaseDriver: z.enum(["sqlite", "sqlite-wasm", "
|
|
2258
|
+
databaseDriver: z.enum(["sqlite", "sqlite-wasm", "memory", "postgres", "mongodb"]).optional(),
|
|
2124
2259
|
environmentId: z.string().optional(),
|
|
2125
2260
|
artifactPath: z.string().optional(),
|
|
2126
2261
|
/**
|
|
@@ -2138,7 +2273,6 @@ var StandaloneStackConfigSchema = z.object({
|
|
|
2138
2273
|
});
|
|
2139
2274
|
function detectDriverFromUrl(dbUrl) {
|
|
2140
2275
|
if (/^memory:\/\//i.test(dbUrl)) return "memory";
|
|
2141
|
-
if (/^(libsql|https?):\/\//i.test(dbUrl)) return "turso";
|
|
2142
2276
|
if (/^(postgres(ql)?|pg):\/\//i.test(dbUrl)) return "postgres";
|
|
2143
2277
|
if (/^mongodb(\+srv)?:\/\//i.test(dbUrl)) return "mongodb";
|
|
2144
2278
|
if (/^wasm-sqlite:\/\//i.test(dbUrl)) return "sqlite-wasm";
|
|
@@ -2146,7 +2280,7 @@ function detectDriverFromUrl(dbUrl) {
|
|
|
2146
2280
|
if (/^file:/i.test(dbUrl)) return "sqlite";
|
|
2147
2281
|
if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(dbUrl)) return "sqlite";
|
|
2148
2282
|
throw new Error(
|
|
2149
|
-
`[StandaloneStack] Unsupported database URL scheme: ${dbUrl}. Supported schemes: memory://,
|
|
2283
|
+
`[StandaloneStack] Unsupported database URL scheme: ${dbUrl}. Supported schemes: memory://, postgres://, pg://, mongodb://, mongodb+srv://, file:`
|
|
2150
2284
|
);
|
|
2151
2285
|
}
|
|
2152
2286
|
async function createStandaloneStack(config) {
|
|
@@ -2160,25 +2294,12 @@ async function createStandaloneStack(config) {
|
|
|
2160
2294
|
const artifactPathInput = cfg.artifactPath ?? process.env.OS_ARTIFACT_PATH ?? resolvePath2(cwd, "dist/objectstack.json");
|
|
2161
2295
|
const artifactPath = isHttpUrl(artifactPathInput) ? artifactPathInput : artifactPathInput.startsWith("/") ? artifactPathInput : resolvePath2(cwd, artifactPathInput);
|
|
2162
2296
|
const dbUrl = cfg.databaseUrl ?? process.env.OS_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")}`);
|
|
2163
|
-
const dbAuthToken = cfg.databaseAuthToken ?? process.env.OS_DATABASE_AUTH_TOKEN?.trim() ?? process.env.TURSO_AUTH_TOKEN?.trim();
|
|
2164
2297
|
const explicitDriver = cfg.databaseDriver ?? process.env.OS_DATABASE_DRIVER?.trim();
|
|
2165
2298
|
const dbDriver = explicitDriver ?? detectDriverFromUrl(dbUrl);
|
|
2166
2299
|
let driverPlugin;
|
|
2167
2300
|
if (dbDriver === "memory") {
|
|
2168
2301
|
const { InMemoryDriver } = await import("@objectstack/driver-memory");
|
|
2169
2302
|
driverPlugin = new DriverPlugin2(new InMemoryDriver());
|
|
2170
|
-
} else if (dbDriver === "turso") {
|
|
2171
|
-
let TursoDriver;
|
|
2172
|
-
try {
|
|
2173
|
-
({ TursoDriver } = await import("@objectstack/driver-turso"));
|
|
2174
|
-
} catch (err) {
|
|
2175
|
-
throw new Error(
|
|
2176
|
-
`[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})`
|
|
2177
|
-
);
|
|
2178
|
-
}
|
|
2179
|
-
driverPlugin = new DriverPlugin2(
|
|
2180
|
-
new TursoDriver({ url: dbUrl, authToken: dbAuthToken })
|
|
2181
|
-
);
|
|
2182
2303
|
} else if (dbDriver === "postgres") {
|
|
2183
2304
|
const { SqlDriver } = await import("@objectstack/driver-sql");
|
|
2184
2305
|
driverPlugin = new DriverPlugin2(
|
|
@@ -2437,8 +2558,12 @@ async function resolveExecutionContext(opts) {
|
|
|
2437
2558
|
if (!userId) {
|
|
2438
2559
|
try {
|
|
2439
2560
|
const authService = await opts.getService("auth");
|
|
2561
|
+
let api = authService?.api;
|
|
2562
|
+
if (!api && typeof authService?.getApi === "function") {
|
|
2563
|
+
api = await authService.getApi();
|
|
2564
|
+
}
|
|
2440
2565
|
const headersInstance = toHeaders(headers);
|
|
2441
|
-
const sessionData = await
|
|
2566
|
+
const sessionData = await api?.getSession?.({ headers: headersInstance });
|
|
2442
2567
|
userId = sessionData?.user?.id ?? sessionData?.session?.userId;
|
|
2443
2568
|
tenantId = tenantId ?? sessionData?.session?.activeOrganizationId;
|
|
2444
2569
|
ctx.accessToken = sessionData?.session?.token ?? ctx.accessToken;
|
|
@@ -3915,7 +4040,7 @@ var _HttpDispatcher = class _HttpDispatcher {
|
|
|
3915
4040
|
return {
|
|
3916
4041
|
handled: true,
|
|
3917
4042
|
response: this.error(
|
|
3918
|
-
"No ObjectQL driver is registered. Register at least one DriverPlugin (e.g. InMemoryDriver or
|
|
4043
|
+
"No ObjectQL driver is registered. Register at least one DriverPlugin (e.g. InMemoryDriver or SqlDriver).",
|
|
3919
4044
|
503
|
|
3920
4045
|
)
|
|
3921
4046
|
};
|
|
@@ -5283,7 +5408,7 @@ var _HttpDispatcher = class _HttpDispatcher {
|
|
|
5283
5408
|
* Handle AI service routes (/ai/chat, /ai/models, /ai/conversations, etc.)
|
|
5284
5409
|
* Resolves the AI service and its built-in route handlers, then dispatches.
|
|
5285
5410
|
*/
|
|
5286
|
-
async handleAI(subPath, method, body, query,
|
|
5411
|
+
async handleAI(subPath, method, body, query, context) {
|
|
5287
5412
|
let aiService;
|
|
5288
5413
|
try {
|
|
5289
5414
|
aiService = await this.resolveService("ai");
|
|
@@ -5327,7 +5452,23 @@ var _HttpDispatcher = class _HttpDispatcher {
|
|
|
5327
5452
|
if (route.method !== method) continue;
|
|
5328
5453
|
const params = matchRoute(route.path, fullPath);
|
|
5329
5454
|
if (params === null) continue;
|
|
5330
|
-
const
|
|
5455
|
+
const ec = context.executionContext;
|
|
5456
|
+
const user = ec?.userId ? {
|
|
5457
|
+
userId: ec.userId,
|
|
5458
|
+
id: ec.userId,
|
|
5459
|
+
displayName: ec.userDisplayName ?? ec.userName ?? ec.userId,
|
|
5460
|
+
email: ec.userEmail,
|
|
5461
|
+
roles: Array.isArray(ec.roles) ? ec.roles : [],
|
|
5462
|
+
permissions: Array.isArray(ec.permissions) ? ec.permissions : [],
|
|
5463
|
+
organizationId: ec.tenantId
|
|
5464
|
+
} : void 0;
|
|
5465
|
+
const result = await route.handler({
|
|
5466
|
+
body,
|
|
5467
|
+
params,
|
|
5468
|
+
query,
|
|
5469
|
+
headers: context.request?.headers,
|
|
5470
|
+
user
|
|
5471
|
+
});
|
|
5331
5472
|
if (result.stream && result.events) {
|
|
5332
5473
|
return {
|
|
5333
5474
|
handled: true,
|
|
@@ -5832,13 +5973,22 @@ function resolveErrorReporter(ctx, override) {
|
|
|
5832
5973
|
}
|
|
5833
5974
|
|
|
5834
5975
|
// src/dispatcher-plugin.ts
|
|
5835
|
-
function mountRouteOnServer(route, server, routePath, securityHeaders) {
|
|
5976
|
+
function mountRouteOnServer(route, server, routePath, securityHeaders, resolveUser) {
|
|
5836
5977
|
const handler = async (req, res) => {
|
|
5837
5978
|
try {
|
|
5979
|
+
let user;
|
|
5980
|
+
if (resolveUser) {
|
|
5981
|
+
try {
|
|
5982
|
+
user = await resolveUser(req.headers ?? {});
|
|
5983
|
+
} catch {
|
|
5984
|
+
}
|
|
5985
|
+
}
|
|
5838
5986
|
const result = await route.handler({
|
|
5839
5987
|
body: req.body,
|
|
5840
5988
|
params: req.params,
|
|
5841
|
-
query: req.query
|
|
5989
|
+
query: req.query,
|
|
5990
|
+
headers: req.headers,
|
|
5991
|
+
user
|
|
5842
5992
|
});
|
|
5843
5993
|
if (result.stream && result.events) {
|
|
5844
5994
|
res.status(result.status);
|
|
@@ -6575,6 +6725,32 @@ function createDispatcherPlugin(config = {}) {
|
|
|
6575
6725
|
}
|
|
6576
6726
|
}
|
|
6577
6727
|
ctx.logger.info("Dispatcher bridge routes registered", { prefix, enableProjectScoping, projectResolution });
|
|
6728
|
+
const resolveRequestUser = async (headers) => {
|
|
6729
|
+
try {
|
|
6730
|
+
const authService = ctx.getService("auth");
|
|
6731
|
+
if (!authService) return void 0;
|
|
6732
|
+
let api = authService.api;
|
|
6733
|
+
if (!api && typeof authService.getApi === "function") {
|
|
6734
|
+
api = await authService.getApi();
|
|
6735
|
+
}
|
|
6736
|
+
if (!api?.getSession) return void 0;
|
|
6737
|
+
const headersInstance = headers instanceof Headers ? headers : new Headers(headers);
|
|
6738
|
+
const sessionData = await api.getSession({ headers: headersInstance });
|
|
6739
|
+
const userId = sessionData?.user?.id ?? sessionData?.session?.userId;
|
|
6740
|
+
if (!userId) return void 0;
|
|
6741
|
+
return {
|
|
6742
|
+
userId,
|
|
6743
|
+
id: userId,
|
|
6744
|
+
displayName: sessionData?.user?.name ?? sessionData?.user?.email ?? userId,
|
|
6745
|
+
email: sessionData?.user?.email,
|
|
6746
|
+
roles: [],
|
|
6747
|
+
permissions: [],
|
|
6748
|
+
organizationId: sessionData?.session?.activeOrganizationId
|
|
6749
|
+
};
|
|
6750
|
+
} catch {
|
|
6751
|
+
return void 0;
|
|
6752
|
+
}
|
|
6753
|
+
};
|
|
6578
6754
|
const toScopedPath = (routePath) => {
|
|
6579
6755
|
if (routePath.startsWith(prefix)) {
|
|
6580
6756
|
const tail = routePath.slice(prefix.length);
|
|
@@ -6587,11 +6763,11 @@ function createDispatcherPlugin(config = {}) {
|
|
|
6587
6763
|
const routePath = route.path.startsWith("/api/v1") ? route.path : `${prefix}${route.path}`;
|
|
6588
6764
|
let count = 0;
|
|
6589
6765
|
if (enableProjectScoping && projectResolution === "required") {
|
|
6590
|
-
if (mountRouteOnServer(route, server, toScopedPath(routePath), securityHeaders)) count++;
|
|
6766
|
+
if (mountRouteOnServer(route, server, toScopedPath(routePath), securityHeaders, resolveRequestUser)) count++;
|
|
6591
6767
|
} else {
|
|
6592
|
-
if (mountRouteOnServer(route, server, routePath, securityHeaders)) count++;
|
|
6768
|
+
if (mountRouteOnServer(route, server, routePath, securityHeaders, resolveRequestUser)) count++;
|
|
6593
6769
|
if (enableProjectScoping) {
|
|
6594
|
-
if (mountRouteOnServer(route, server, toScopedPath(routePath), securityHeaders)) count++;
|
|
6770
|
+
if (mountRouteOnServer(route, server, toScopedPath(routePath), securityHeaders, resolveRequestUser)) count++;
|
|
6595
6771
|
}
|
|
6596
6772
|
}
|
|
6597
6773
|
return count;
|
|
@@ -7366,6 +7542,27 @@ function extractRuntimeFromMetadata(metadata) {
|
|
|
7366
7542
|
}
|
|
7367
7543
|
async function createDriver(driverType, databaseUrl, authToken) {
|
|
7368
7544
|
switch (driverType) {
|
|
7545
|
+
case "libsql":
|
|
7546
|
+
case "turso": {
|
|
7547
|
+
let TursoDriver;
|
|
7548
|
+
try {
|
|
7549
|
+
({ TursoDriver } = await import("@objectstack/driver-turso"));
|
|
7550
|
+
} catch (primaryErr) {
|
|
7551
|
+
try {
|
|
7552
|
+
const { createRequire } = await import("module");
|
|
7553
|
+
const path = await import("path");
|
|
7554
|
+
const url = await import("url");
|
|
7555
|
+
const hostRequire = createRequire(path.join(process.cwd(), "noop.js"));
|
|
7556
|
+
const resolved = hostRequire.resolve("@objectstack/driver-turso");
|
|
7557
|
+
({ TursoDriver } = await import(url.pathToFileURL(resolved).href));
|
|
7558
|
+
} catch (fallbackErr) {
|
|
7559
|
+
throw new Error(
|
|
7560
|
+
`[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})`
|
|
7561
|
+
);
|
|
7562
|
+
}
|
|
7563
|
+
}
|
|
7564
|
+
return new TursoDriver({ url: databaseUrl, authToken });
|
|
7565
|
+
}
|
|
7369
7566
|
case "memory": {
|
|
7370
7567
|
const { InMemoryDriver } = await import("@objectstack/driver-memory");
|
|
7371
7568
|
const dbName = databaseUrl.replace(/^memory:\/\//, "").trim();
|
|
@@ -7384,18 +7581,6 @@ async function createDriver(driverType, databaseUrl, authToken) {
|
|
|
7384
7581
|
useNullAsDefault: true
|
|
7385
7582
|
});
|
|
7386
7583
|
}
|
|
7387
|
-
case "libsql":
|
|
7388
|
-
case "turso": {
|
|
7389
|
-
let TursoDriver;
|
|
7390
|
-
try {
|
|
7391
|
-
({ TursoDriver } = await import("@objectstack/driver-turso"));
|
|
7392
|
-
} catch (err) {
|
|
7393
|
-
throw new Error(
|
|
7394
|
-
`[ArtifactEnvironmentRegistry] libsql/turso driver requested but @objectstack/driver-turso is not installed. Install it with: npm install @objectstack/driver-turso. (${err?.message ?? err})`
|
|
7395
|
-
);
|
|
7396
|
-
}
|
|
7397
|
-
return new TursoDriver({ url: databaseUrl, authToken });
|
|
7398
|
-
}
|
|
7399
7584
|
case "postgres":
|
|
7400
7585
|
case "postgresql":
|
|
7401
7586
|
case "pg": {
|
|
@@ -7653,39 +7838,7 @@ var ArtifactKernelFactory = class {
|
|
|
7653
7838
|
// intentionally do NOT pass crossSubDomainCookies here
|
|
7654
7839
|
// so cookies stay isolated per project subdomain.
|
|
7655
7840
|
trustedOrigins: trustedOriginsList.length ? trustedOriginsList : void 0,
|
|
7656
|
-
...oidcProviders ? { oidcProviders } : {}
|
|
7657
|
-
// Auto-provision a personal organization for every new
|
|
7658
|
-
// user. SecurityPlugin's ObjectQL middleware does this
|
|
7659
|
-
// for direct `ql.insert` calls, but better-auth's
|
|
7660
|
-
// adapter writes through `dataEngine` directly,
|
|
7661
|
-
// bypassing that middleware — so JIT-created SSO users
|
|
7662
|
-
// would otherwise land on the empty "create
|
|
7663
|
-
// organization" screen on first login.
|
|
7664
|
-
databaseHooks: {
|
|
7665
|
-
user: {
|
|
7666
|
-
create: {
|
|
7667
|
-
after: async (user) => {
|
|
7668
|
-
try {
|
|
7669
|
-
const ql = kernel.getService("objectql");
|
|
7670
|
-
if (!ql) return;
|
|
7671
|
-
const [{ ensureUserHasOrganization, cloneTenantSeedData }] = await Promise.all([
|
|
7672
|
-
import("@objectstack/plugin-security")
|
|
7673
|
-
]);
|
|
7674
|
-
await ensureUserHasOrganization(ql, user, {
|
|
7675
|
-
logger: this.logger,
|
|
7676
|
-
cloneSeedData: cloneTenantSeedData
|
|
7677
|
-
});
|
|
7678
|
-
} catch (e) {
|
|
7679
|
-
this.logger.warn?.("[ArtifactKernelFactory] auto-org provisioning hook failed", {
|
|
7680
|
-
environmentId,
|
|
7681
|
-
userId: user?.id,
|
|
7682
|
-
error: e?.message
|
|
7683
|
-
});
|
|
7684
|
-
}
|
|
7685
|
-
}
|
|
7686
|
-
}
|
|
7687
|
-
}
|
|
7688
|
-
}
|
|
7841
|
+
...oidcProviders ? { oidcProviders } : {}
|
|
7689
7842
|
}));
|
|
7690
7843
|
if (oidcProviders) {
|
|
7691
7844
|
this.logger.info?.("[ArtifactKernelFactory] platform SSO wired", {
|
|
@@ -7703,9 +7856,20 @@ var ArtifactKernelFactory = class {
|
|
|
7703
7856
|
this.logger.warn?.("[ArtifactKernelFactory] OS_AUTH_SECRET not set \u2014 per-project AuthPlugin skipped (auth endpoints will return 404)", { environmentId });
|
|
7704
7857
|
}
|
|
7705
7858
|
try {
|
|
7859
|
+
const multiTenant = String(process.env.OS_MULTI_TENANT ?? "false").toLowerCase() !== "false";
|
|
7860
|
+
if (multiTenant) {
|
|
7861
|
+
try {
|
|
7862
|
+
const { OrgScopingPlugin } = await import("@objectstack/plugin-org-scoping");
|
|
7863
|
+
await kernel.use(new OrgScopingPlugin());
|
|
7864
|
+
} catch (err) {
|
|
7865
|
+
this.logger.warn?.("[ArtifactKernelFactory] OrgScopingPlugin not registered (multi-tenant disabled)", {
|
|
7866
|
+
environmentId,
|
|
7867
|
+
error: err?.message
|
|
7868
|
+
});
|
|
7869
|
+
}
|
|
7870
|
+
}
|
|
7706
7871
|
const { SecurityPlugin } = await import("@objectstack/plugin-security");
|
|
7707
|
-
|
|
7708
|
-
await kernel.use(new SecurityPlugin({ multiTenant }));
|
|
7872
|
+
await kernel.use(new SecurityPlugin());
|
|
7709
7873
|
} catch (err) {
|
|
7710
7874
|
this.logger.warn?.("[ArtifactKernelFactory] SecurityPlugin not registered", {
|
|
7711
7875
|
environmentId,
|
|
@@ -7713,8 +7877,15 @@ var ArtifactKernelFactory = class {
|
|
|
7713
7877
|
});
|
|
7714
7878
|
}
|
|
7715
7879
|
const projectName = project.hostname ?? environmentId;
|
|
7716
|
-
const
|
|
7717
|
-
const
|
|
7880
|
+
const artifactAny = artifact;
|
|
7881
|
+
const topLevelManifest = artifactAny?.manifest && typeof artifactAny.manifest === "object" ? artifactAny.manifest : null;
|
|
7882
|
+
const topLevelFunctions = Array.isArray(artifactAny?.functions) ? artifactAny.functions : [];
|
|
7883
|
+
const bundle = {
|
|
7884
|
+
...artifact.metadata ?? {},
|
|
7885
|
+
...topLevelManifest ? { manifest: topLevelManifest } : {},
|
|
7886
|
+
functions: topLevelFunctions
|
|
7887
|
+
};
|
|
7888
|
+
const sys = bundle.manifest ?? bundle;
|
|
7718
7889
|
const packageId = sys?.packageId ?? sys?.package_id ?? bundle?.packageId;
|
|
7719
7890
|
const i18nCfg = bundle?.i18n ?? sys?.i18n ?? {};
|
|
7720
7891
|
const trArr = Array.isArray(bundle?.translations) ? bundle.translations : Array.isArray(sys?.translations) ? sys.translations : [];
|
|
@@ -8224,6 +8395,40 @@ function resolveCloudUrl(explicit) {
|
|
|
8224
8395
|
|
|
8225
8396
|
// src/cloud/marketplace-proxy-plugin.ts
|
|
8226
8397
|
var MARKETPLACE_PREFIX = "/api/v1/marketplace";
|
|
8398
|
+
var DEFAULT_LRU_MAX = 200;
|
|
8399
|
+
var LIST_TTL_MS = 30 * 60 * 1e3;
|
|
8400
|
+
var PACKAGE_TTL_MS = 2 * 60 * 60 * 1e3;
|
|
8401
|
+
var VERSION_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
8402
|
+
function ttlForPath(pathname) {
|
|
8403
|
+
if (/\/packages\/[^/]+\/versions\//.test(pathname)) return VERSION_TTL_MS;
|
|
8404
|
+
if (/\/packages\/[^/]+/.test(pathname)) return PACKAGE_TTL_MS;
|
|
8405
|
+
return LIST_TTL_MS;
|
|
8406
|
+
}
|
|
8407
|
+
var LruTtlCache = class {
|
|
8408
|
+
constructor(max) {
|
|
8409
|
+
this.max = max;
|
|
8410
|
+
this.map = /* @__PURE__ */ new Map();
|
|
8411
|
+
}
|
|
8412
|
+
get(key) {
|
|
8413
|
+
const entry = this.map.get(key);
|
|
8414
|
+
if (!entry) return void 0;
|
|
8415
|
+
this.map.delete(key);
|
|
8416
|
+
this.map.set(key, entry);
|
|
8417
|
+
return entry;
|
|
8418
|
+
}
|
|
8419
|
+
set(key, entry) {
|
|
8420
|
+
if (this.map.has(key)) this.map.delete(key);
|
|
8421
|
+
this.map.set(key, entry);
|
|
8422
|
+
while (this.map.size > this.max) {
|
|
8423
|
+
const oldest = this.map.keys().next().value;
|
|
8424
|
+
if (oldest === void 0) break;
|
|
8425
|
+
this.map.delete(oldest);
|
|
8426
|
+
}
|
|
8427
|
+
}
|
|
8428
|
+
clear() {
|
|
8429
|
+
this.map.clear();
|
|
8430
|
+
}
|
|
8431
|
+
};
|
|
8227
8432
|
var MarketplaceProxyPlugin = class _MarketplaceProxyPlugin {
|
|
8228
8433
|
constructor(config = {}) {
|
|
8229
8434
|
this.name = "com.objectstack.runtime.marketplace-proxy";
|
|
@@ -8245,6 +8450,7 @@ var MarketplaceProxyPlugin = class _MarketplaceProxyPlugin {
|
|
|
8245
8450
|
}
|
|
8246
8451
|
const rawApp = httpServer.getRawApp();
|
|
8247
8452
|
const cloudUrl = this.cloudUrl;
|
|
8453
|
+
const cache = this.cache;
|
|
8248
8454
|
const handler = async (c, next) => {
|
|
8249
8455
|
if (!cloudUrl) {
|
|
8250
8456
|
return c.json({
|
|
@@ -8271,24 +8477,51 @@ var MarketplaceProxyPlugin = class _MarketplaceProxyPlugin {
|
|
|
8271
8477
|
}
|
|
8272
8478
|
}, 405);
|
|
8273
8479
|
}
|
|
8274
|
-
const
|
|
8275
|
-
|
|
8276
|
-
|
|
8277
|
-
|
|
8278
|
-
|
|
8279
|
-
|
|
8280
|
-
|
|
8281
|
-
|
|
8480
|
+
const accept = c.req.header("accept") ?? "application/json";
|
|
8481
|
+
const acceptLang = c.req.header("accept-language") ?? "";
|
|
8482
|
+
const cacheKey = `${incomingUrl.pathname}${incomingUrl.search}|al=${acceptLang}|a=${accept}`;
|
|
8483
|
+
const reqCacheCtl = (c.req.header("cache-control") ?? "").toLowerCase();
|
|
8484
|
+
const bypass = !cache || reqCacheCtl.includes("no-cache") || reqCacheCtl.includes("no-store");
|
|
8485
|
+
const now = Date.now();
|
|
8486
|
+
if (cache && !bypass) {
|
|
8487
|
+
const hit = cache.get(cacheKey);
|
|
8488
|
+
if (hit && hit.expiresAt > now) {
|
|
8489
|
+
return buildCachedResponse(hit, method, "HIT");
|
|
8282
8490
|
}
|
|
8283
|
-
|
|
8284
|
-
|
|
8285
|
-
|
|
8286
|
-
|
|
8287
|
-
|
|
8288
|
-
|
|
8491
|
+
if (hit) {
|
|
8492
|
+
const revalHeaders = {
|
|
8493
|
+
"Accept": accept,
|
|
8494
|
+
"User-Agent": `objectos-marketplace-proxy/${_MarketplaceProxyPlugin.prototype.version ?? "1.0.0"}`
|
|
8495
|
+
};
|
|
8496
|
+
if (acceptLang) revalHeaders["Accept-Language"] = acceptLang;
|
|
8497
|
+
if (hit.etag) revalHeaders["If-None-Match"] = hit.etag;
|
|
8498
|
+
if (hit.lastModified) revalHeaders["If-Modified-Since"] = hit.lastModified;
|
|
8499
|
+
const revalResp = await fetch(target, { method: "GET", headers: revalHeaders });
|
|
8500
|
+
if (revalResp.status === 304) {
|
|
8501
|
+
hit.expiresAt = now + hit.ttlMs;
|
|
8502
|
+
const newEtag = revalResp.headers.get("etag");
|
|
8503
|
+
const newLm = revalResp.headers.get("last-modified");
|
|
8504
|
+
if (newEtag) hit.etag = newEtag;
|
|
8505
|
+
if (newLm) hit.lastModified = newLm;
|
|
8506
|
+
cache.set(cacheKey, hit);
|
|
8507
|
+
return buildCachedResponse(hit, method, "REVALIDATED");
|
|
8508
|
+
}
|
|
8509
|
+
return await consumeAndMaybeCache(revalResp, cacheKey, incomingUrl.pathname, method, cache);
|
|
8510
|
+
}
|
|
8511
|
+
}
|
|
8512
|
+
const reqHeaders = {
|
|
8513
|
+
// Strip the inbound Host header — fetch will set
|
|
8514
|
+
// it to the cloud host. Forward only the
|
|
8515
|
+
// identifying headers cloud might log.
|
|
8516
|
+
"Accept": accept,
|
|
8517
|
+
"User-Agent": `objectos-marketplace-proxy/${_MarketplaceProxyPlugin.prototype.version ?? "1.0.0"}`
|
|
8518
|
+
};
|
|
8519
|
+
if (acceptLang) reqHeaders["Accept-Language"] = acceptLang;
|
|
8520
|
+
const resp = await fetch(target, { method: "GET", headers: reqHeaders });
|
|
8521
|
+
if (bypass || !cache) {
|
|
8522
|
+
return await passthroughResponse(resp, method, bypass ? "BYPASS" : "MISS");
|
|
8289
8523
|
}
|
|
8290
|
-
|
|
8291
|
-
return new Response(body, { status: resp.status, headers });
|
|
8524
|
+
return await consumeAndMaybeCache(resp, cacheKey, incomingUrl.pathname, method, cache);
|
|
8292
8525
|
} catch (err) {
|
|
8293
8526
|
const errObj = err instanceof Error ? err : new Error(err?.message ?? String(err));
|
|
8294
8527
|
ctx.logger?.error?.("[MarketplaceProxyPlugin] proxy failed", errObj);
|
|
@@ -8311,12 +8544,67 @@ var MarketplaceProxyPlugin = class _MarketplaceProxyPlugin {
|
|
|
8311
8544
|
}
|
|
8312
8545
|
}
|
|
8313
8546
|
}
|
|
8314
|
-
ctx.logger?.info?.(`[MarketplaceProxyPlugin] mounted at ${MARKETPLACE_PREFIX}/* \u2192 ${cloudUrl || "(unconfigured)"}`);
|
|
8547
|
+
ctx.logger?.info?.(`[MarketplaceProxyPlugin] mounted at ${MARKETPLACE_PREFIX}/* \u2192 ${cloudUrl || "(unconfigured)"} (cache=${this.cache ? "on" : "off"})`);
|
|
8315
8548
|
});
|
|
8316
8549
|
};
|
|
8317
8550
|
this.cloudUrl = resolveCloudUrl(config.controlPlaneUrl);
|
|
8551
|
+
const envFlag = (process.env.OS_MARKETPLACE_CACHE ?? "").trim().toLowerCase();
|
|
8552
|
+
const envDisabled = ["off", "false", "0", "no", "disable", "disabled"].includes(envFlag);
|
|
8553
|
+
const disabled = config.cacheDisabled ?? envDisabled;
|
|
8554
|
+
this.cache = disabled ? null : new LruTtlCache(Math.max(8, config.cacheMaxEntries ?? DEFAULT_LRU_MAX));
|
|
8318
8555
|
}
|
|
8319
8556
|
};
|
|
8557
|
+
var PASSTHROUGH_HEADERS = ["content-type", "cache-control", "etag", "last-modified", "vary"];
|
|
8558
|
+
function collectHeaders(src) {
|
|
8559
|
+
const out = {};
|
|
8560
|
+
for (const h of PASSTHROUGH_HEADERS) {
|
|
8561
|
+
const v = src.headers.get(h);
|
|
8562
|
+
if (v) out[h] = v;
|
|
8563
|
+
}
|
|
8564
|
+
return out;
|
|
8565
|
+
}
|
|
8566
|
+
function buildCachedResponse(entry, method, xCache) {
|
|
8567
|
+
const headers = new Headers(entry.headers);
|
|
8568
|
+
headers.set("X-Cache", xCache);
|
|
8569
|
+
const ageSec = Math.max(0, Math.floor((entry.expiresAt - entry.ttlMs - Date.now()) / -1e3));
|
|
8570
|
+
headers.set("Age", String(Math.max(0, ageSec)));
|
|
8571
|
+
const body = method === "HEAD" ? null : entry.body;
|
|
8572
|
+
return new Response(body, { status: entry.status, headers });
|
|
8573
|
+
}
|
|
8574
|
+
async function passthroughResponse(resp, method, xCache) {
|
|
8575
|
+
const headers = new Headers(collectHeaders(resp));
|
|
8576
|
+
headers.set("X-Cache", xCache);
|
|
8577
|
+
if (method === "HEAD") {
|
|
8578
|
+
try {
|
|
8579
|
+
await resp.arrayBuffer();
|
|
8580
|
+
} catch {
|
|
8581
|
+
}
|
|
8582
|
+
return new Response(null, { status: resp.status, headers });
|
|
8583
|
+
}
|
|
8584
|
+
const body = await resp.arrayBuffer();
|
|
8585
|
+
return new Response(body, { status: resp.status, headers });
|
|
8586
|
+
}
|
|
8587
|
+
async function consumeAndMaybeCache(resp, key, pathname, method, cache) {
|
|
8588
|
+
const body = await resp.arrayBuffer();
|
|
8589
|
+
const headers = collectHeaders(resp);
|
|
8590
|
+
if (resp.status >= 200 && resp.status < 300) {
|
|
8591
|
+
const ttlMs = ttlForPath(pathname);
|
|
8592
|
+
const entry = {
|
|
8593
|
+
status: resp.status,
|
|
8594
|
+
body,
|
|
8595
|
+
headers,
|
|
8596
|
+
etag: resp.headers.get("etag") ?? void 0,
|
|
8597
|
+
lastModified: resp.headers.get("last-modified") ?? void 0,
|
|
8598
|
+
expiresAt: Date.now() + ttlMs,
|
|
8599
|
+
ttlMs
|
|
8600
|
+
};
|
|
8601
|
+
cache.set(key, entry);
|
|
8602
|
+
}
|
|
8603
|
+
const respHeaders = new Headers(headers);
|
|
8604
|
+
respHeaders.set("X-Cache", "MISS");
|
|
8605
|
+
const outBody = method === "HEAD" ? null : body;
|
|
8606
|
+
return new Response(outBody, { status: resp.status, headers: respHeaders });
|
|
8607
|
+
}
|
|
8320
8608
|
|
|
8321
8609
|
// src/cloud/runtime-config-plugin.ts
|
|
8322
8610
|
var RuntimeConfigPlugin = class {
|
|
@@ -8354,12 +8642,14 @@ var RuntimeConfigPlugin = class {
|
|
|
8354
8642
|
let defaultEnvironmentId;
|
|
8355
8643
|
let defaultOrgId;
|
|
8356
8644
|
let resolvedSingleEnv = this.singleEnvironment;
|
|
8357
|
-
|
|
8645
|
+
const resolveFn = typeof envRegistry?.resolveByHostname === "function" ? envRegistry.resolveByHostname.bind(envRegistry) : typeof envRegistry?.resolveHostname === "function" ? envRegistry.resolveHostname.bind(envRegistry) : null;
|
|
8646
|
+
if (resolveFn && host) {
|
|
8358
8647
|
try {
|
|
8359
|
-
const resolved = await
|
|
8648
|
+
const resolved = await resolveFn(host);
|
|
8360
8649
|
if (resolved?.environmentId) {
|
|
8361
|
-
defaultEnvironmentId = resolved.environmentId;
|
|
8362
|
-
|
|
8650
|
+
defaultEnvironmentId = String(resolved.environmentId);
|
|
8651
|
+
const orgId = resolved.organizationId ?? resolved.organization_id;
|
|
8652
|
+
if (orgId) defaultOrgId = String(orgId);
|
|
8363
8653
|
resolvedSingleEnv = true;
|
|
8364
8654
|
}
|
|
8365
8655
|
} catch {
|
|
@@ -8370,7 +8660,11 @@ var RuntimeConfigPlugin = class {
|
|
|
8370
8660
|
singleEnvironment: resolvedSingleEnv,
|
|
8371
8661
|
defaultOrgId,
|
|
8372
8662
|
defaultEnvironmentId,
|
|
8373
|
-
features
|
|
8663
|
+
features,
|
|
8664
|
+
branding: {
|
|
8665
|
+
productName: this.productName,
|
|
8666
|
+
productShortName: this.productShortName
|
|
8667
|
+
}
|
|
8374
8668
|
});
|
|
8375
8669
|
};
|
|
8376
8670
|
rawApp.get("/api/v1/runtime/config", handler);
|
|
@@ -8387,6 +8681,10 @@ var RuntimeConfigPlugin = class {
|
|
|
8387
8681
|
this.cloudUrl = config.controlPlaneUrl === "" ? "" : resolveCloudUrl(config.controlPlaneUrl) ?? "";
|
|
8388
8682
|
this.installLocal = !!config.installLocal;
|
|
8389
8683
|
this.singleEnvironment = !!config.singleEnvironment;
|
|
8684
|
+
const envName = (typeof process !== "undefined" ? process.env?.OS_PRODUCT_NAME : void 0)?.trim();
|
|
8685
|
+
const envShort = (typeof process !== "undefined" ? process.env?.OS_PRODUCT_SHORT_NAME : void 0)?.trim();
|
|
8686
|
+
this.productName = (config.productName ?? envName ?? "ObjectOS").trim() || "ObjectOS";
|
|
8687
|
+
this.productShortName = (config.productShortName ?? envShort ?? this.productName).trim() || this.productName;
|
|
8390
8688
|
}
|
|
8391
8689
|
};
|
|
8392
8690
|
|
|
@@ -8676,9 +8974,15 @@ var MarketplaceInstallLocalPlugin = class {
|
|
|
8676
8974
|
const postHandler = async (c) => this.handleInstall(c, ctx);
|
|
8677
8975
|
const getHandler = async (c) => this.handleList(c);
|
|
8678
8976
|
const deleteHandler = async (c) => this.handleUninstall(c, ctx);
|
|
8977
|
+
const reseedHandler = async (c) => this.handleReseed(c, ctx);
|
|
8978
|
+
const purgeHandler = async (c) => this.handlePurge(c, ctx);
|
|
8679
8979
|
if (typeof rawApp.post === "function") rawApp.post(ROUTE_BASE, postHandler);
|
|
8680
8980
|
if (typeof rawApp.get === "function") rawApp.get(ROUTE_BASE, getHandler);
|
|
8681
8981
|
if (typeof rawApp.delete === "function") rawApp.delete(`${ROUTE_BASE}/:manifestId`, deleteHandler);
|
|
8982
|
+
if (typeof rawApp.post === "function") {
|
|
8983
|
+
rawApp.post(`${ROUTE_BASE}/:manifestId/reseed-sample-data`, reseedHandler);
|
|
8984
|
+
rawApp.post(`${ROUTE_BASE}/:manifestId/purge-sample-data`, purgeHandler);
|
|
8985
|
+
}
|
|
8682
8986
|
ctx.logger?.info?.(`[MarketplaceInstallLocal] mounted at ${ROUTE_BASE} (storage: ${this.storageDir})`);
|
|
8683
8987
|
});
|
|
8684
8988
|
};
|
|
@@ -8775,7 +9079,8 @@ var MarketplaceInstallLocalPlugin = class {
|
|
|
8775
9079
|
version,
|
|
8776
9080
|
manifest,
|
|
8777
9081
|
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8778
|
-
installedBy: userId
|
|
9082
|
+
installedBy: userId,
|
|
9083
|
+
withSampleData: false
|
|
8779
9084
|
};
|
|
8780
9085
|
try {
|
|
8781
9086
|
mkdirSync3(this.storageDir, { recursive: true });
|
|
@@ -8802,6 +9107,13 @@ var MarketplaceInstallLocalPlugin = class {
|
|
|
8802
9107
|
ctx.logger?.warn?.(`[MarketplaceInstallLocal] syncSchemas failed for ${manifestId}: ${err?.message ?? err}`);
|
|
8803
9108
|
}
|
|
8804
9109
|
const seededSummary = await this.applySideEffects(ctx, manifest, { seedNow: true, c });
|
|
9110
|
+
if (seededSummary.seeded.mode === "inline" && (seededSummary.seeded.inserted ?? 0) + (seededSummary.seeded.updated ?? 0) > 0) {
|
|
9111
|
+
entry.withSampleData = true;
|
|
9112
|
+
try {
|
|
9113
|
+
writeFileSync2(join(this.storageDir, safeFilename(manifestId)), JSON.stringify(entry, null, 2), "utf8");
|
|
9114
|
+
} catch {
|
|
9115
|
+
}
|
|
9116
|
+
}
|
|
8805
9117
|
return c.json({
|
|
8806
9118
|
success: true,
|
|
8807
9119
|
data: {
|
|
@@ -8828,7 +9140,8 @@ var MarketplaceInstallLocalPlugin = class {
|
|
|
8828
9140
|
manifestId: e.manifestId,
|
|
8829
9141
|
version: e.version,
|
|
8830
9142
|
installedAt: e.installedAt,
|
|
8831
|
-
installedBy: e.installedBy
|
|
9143
|
+
installedBy: e.installedBy,
|
|
9144
|
+
withSampleData: e.withSampleData ?? false
|
|
8832
9145
|
})),
|
|
8833
9146
|
total: entries.length,
|
|
8834
9147
|
storageDir: this.storageDir
|
|
@@ -8893,6 +9206,145 @@ var MarketplaceInstallLocalPlugin = class {
|
|
|
8893
9206
|
* dev / single-tenant runtimes. Stricter checks can be layered on
|
|
8894
9207
|
* via a middleware in cloud-hosted multi-tenant deployments.
|
|
8895
9208
|
*/
|
|
9209
|
+
/**
|
|
9210
|
+
* POST /api/v1/marketplace/install-local/:manifestId/reseed-sample-data
|
|
9211
|
+
*
|
|
9212
|
+
* Re-runs SeedLoaderService against the cached manifest's `data` arrays.
|
|
9213
|
+
* Idempotent (upsert by id). Useful when:
|
|
9214
|
+
* • The user installed an app and skipped sample data
|
|
9215
|
+
* • A purge was undone
|
|
9216
|
+
* • The user wants a clean baseline back after editing demo rows
|
|
9217
|
+
*
|
|
9218
|
+
* Multi-tenant: requires an active organization on the session (same
|
|
9219
|
+
* rule as install seed path).
|
|
9220
|
+
*/
|
|
9221
|
+
this.handleReseed = async (c, ctx) => {
|
|
9222
|
+
const userId = await this.requireAuthenticatedUser(c, ctx);
|
|
9223
|
+
if (!userId) {
|
|
9224
|
+
return c.json({ success: false, error: { code: "unauthorized", message: "Authentication required." } }, 401);
|
|
9225
|
+
}
|
|
9226
|
+
const manifestId = String(c.req.param?.("manifestId") ?? c.req.params?.manifestId ?? "").trim();
|
|
9227
|
+
if (!manifestId) {
|
|
9228
|
+
return c.json({ success: false, error: { code: "bad_request", message: "manifestId path param required." } }, 400);
|
|
9229
|
+
}
|
|
9230
|
+
const file = join(this.storageDir, safeFilename(manifestId));
|
|
9231
|
+
if (!existsSync2(file)) {
|
|
9232
|
+
return c.json({ success: false, error: { code: "not_found", message: `No marketplace install for ${manifestId}.` } }, 404);
|
|
9233
|
+
}
|
|
9234
|
+
let entry;
|
|
9235
|
+
try {
|
|
9236
|
+
entry = JSON.parse(readFileSync(file, "utf8"));
|
|
9237
|
+
} catch (err) {
|
|
9238
|
+
return c.json({ success: false, error: { code: "storage_failed", message: `Failed to read manifest cache: ${err?.message ?? err}` } }, 500);
|
|
9239
|
+
}
|
|
9240
|
+
const summary = await this.applySideEffects(ctx, entry.manifest, { seedNow: true, c });
|
|
9241
|
+
if (summary.seeded.mode === "skipped") {
|
|
9242
|
+
return c.json({
|
|
9243
|
+
success: false,
|
|
9244
|
+
error: {
|
|
9245
|
+
code: "reseed_skipped",
|
|
9246
|
+
message: `Reseed did not run: ${summary.seeded.reason ?? "unknown reason"}`
|
|
9247
|
+
}
|
|
9248
|
+
}, 400);
|
|
9249
|
+
}
|
|
9250
|
+
try {
|
|
9251
|
+
entry.withSampleData = true;
|
|
9252
|
+
writeFileSync2(file, JSON.stringify(entry, null, 2), "utf8");
|
|
9253
|
+
} catch {
|
|
9254
|
+
}
|
|
9255
|
+
return c.json({
|
|
9256
|
+
success: true,
|
|
9257
|
+
data: {
|
|
9258
|
+
manifestId,
|
|
9259
|
+
inserted: summary.seeded.inserted ?? 0,
|
|
9260
|
+
updated: summary.seeded.updated ?? 0,
|
|
9261
|
+
errors: summary.seeded.errors ?? 0,
|
|
9262
|
+
withSampleData: true
|
|
9263
|
+
}
|
|
9264
|
+
}, 200);
|
|
9265
|
+
};
|
|
9266
|
+
/**
|
|
9267
|
+
* POST /api/v1/marketplace/install-local/:manifestId/purge-sample-data
|
|
9268
|
+
*
|
|
9269
|
+
* Deletes every record whose id is declared in the cached manifest's
|
|
9270
|
+
* seed datasets. Uses the `driver` service directly to bypass ACL /
|
|
9271
|
+
* lifecycle hooks (same pattern as cloud purge). User-created records
|
|
9272
|
+
* are never touched — only ids declared in the package's bundled
|
|
9273
|
+
* datasets are removed. Already-deleted rows count as `skipped`.
|
|
9274
|
+
*/
|
|
9275
|
+
this.handlePurge = async (c, ctx) => {
|
|
9276
|
+
const userId = await this.requireAuthenticatedUser(c, ctx);
|
|
9277
|
+
if (!userId) {
|
|
9278
|
+
return c.json({ success: false, error: { code: "unauthorized", message: "Authentication required." } }, 401);
|
|
9279
|
+
}
|
|
9280
|
+
const manifestId = String(c.req.param?.("manifestId") ?? c.req.params?.manifestId ?? "").trim();
|
|
9281
|
+
if (!manifestId) {
|
|
9282
|
+
return c.json({ success: false, error: { code: "bad_request", message: "manifestId path param required." } }, 400);
|
|
9283
|
+
}
|
|
9284
|
+
const file = join(this.storageDir, safeFilename(manifestId));
|
|
9285
|
+
if (!existsSync2(file)) {
|
|
9286
|
+
return c.json({ success: false, error: { code: "not_found", message: `No marketplace install for ${manifestId}.` } }, 404);
|
|
9287
|
+
}
|
|
9288
|
+
let entry;
|
|
9289
|
+
try {
|
|
9290
|
+
entry = JSON.parse(readFileSync(file, "utf8"));
|
|
9291
|
+
} catch (err) {
|
|
9292
|
+
return c.json({ success: false, error: { code: "storage_failed", message: `Failed to read manifest cache: ${err?.message ?? err}` } }, 500);
|
|
9293
|
+
}
|
|
9294
|
+
const datasets = Array.isArray(entry.manifest?.data) ? entry.manifest.data.filter((d) => d && d.object && Array.isArray(d.records)) : [];
|
|
9295
|
+
if (datasets.length === 0) {
|
|
9296
|
+
return c.json({
|
|
9297
|
+
success: false,
|
|
9298
|
+
error: { code: "nothing_to_purge", message: "This package declares no seed datasets." }
|
|
9299
|
+
}, 400);
|
|
9300
|
+
}
|
|
9301
|
+
let driver;
|
|
9302
|
+
try {
|
|
9303
|
+
driver = ctx.getService("driver");
|
|
9304
|
+
} catch {
|
|
9305
|
+
}
|
|
9306
|
+
if (!driver || typeof driver.delete !== "function") {
|
|
9307
|
+
return c.json({
|
|
9308
|
+
success: false,
|
|
9309
|
+
error: { code: "driver_missing", message: "driver service unavailable \u2014 cannot purge." }
|
|
9310
|
+
}, 500);
|
|
9311
|
+
}
|
|
9312
|
+
let deleted = 0;
|
|
9313
|
+
let skipped = 0;
|
|
9314
|
+
let errors = 0;
|
|
9315
|
+
for (const ds of datasets) {
|
|
9316
|
+
const object = String(ds.object);
|
|
9317
|
+
for (const rec of ds.records) {
|
|
9318
|
+
const id = rec?.id;
|
|
9319
|
+
if (id === void 0 || id === null || id === "") {
|
|
9320
|
+
skipped++;
|
|
9321
|
+
continue;
|
|
9322
|
+
}
|
|
9323
|
+
try {
|
|
9324
|
+
const r = await driver.delete(object, id);
|
|
9325
|
+
if (r === false || r === 0 || r?.deleted === 0) skipped++;
|
|
9326
|
+
else deleted++;
|
|
9327
|
+
} catch (err) {
|
|
9328
|
+
const msg = String(err?.message ?? err);
|
|
9329
|
+
if (/not.?found|no row/i.test(msg)) skipped++;
|
|
9330
|
+
else {
|
|
9331
|
+
errors++;
|
|
9332
|
+
ctx.logger?.warn?.(`[MarketplaceInstallLocal] purge ${object}#${id}: ${msg}`);
|
|
9333
|
+
}
|
|
9334
|
+
}
|
|
9335
|
+
}
|
|
9336
|
+
}
|
|
9337
|
+
try {
|
|
9338
|
+
entry.withSampleData = false;
|
|
9339
|
+
writeFileSync2(file, JSON.stringify(entry, null, 2), "utf8");
|
|
9340
|
+
} catch {
|
|
9341
|
+
}
|
|
9342
|
+
ctx.logger?.info?.(`[MarketplaceInstallLocal] purged ${manifestId}: deleted=${deleted} skipped=${skipped} errors=${errors}`);
|
|
9343
|
+
return c.json({
|
|
9344
|
+
success: true,
|
|
9345
|
+
data: { manifestId, deleted, skipped, errors, withSampleData: false }
|
|
9346
|
+
}, 200);
|
|
9347
|
+
};
|
|
8896
9348
|
/**
|
|
8897
9349
|
* Replicate the start-time side-effects that AppPlugin runs for
|
|
8898
9350
|
* statically-declared apps but the `manifest` service does NOT:
|
|
@@ -8982,7 +9434,7 @@ var MarketplaceInstallLocalPlugin = class {
|
|
|
8982
9434
|
}
|
|
8983
9435
|
}
|
|
8984
9436
|
if (opts.seedNow && datasets.length > 0) {
|
|
8985
|
-
const multiTenant = String(process.env.OS_MULTI_TENANT ?? "
|
|
9437
|
+
const multiTenant = String(process.env.OS_MULTI_TENANT ?? "false").toLowerCase() !== "false";
|
|
8986
9438
|
try {
|
|
8987
9439
|
const ql = ctx.getService("objectql");
|
|
8988
9440
|
let metadata;
|