@objectstack/runtime 4.0.5 → 4.1.1
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/README.md +62 -0
- package/dist/index.cjs +2786 -121
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +930 -53
- package/dist/index.d.ts +930 -53
- package/dist/index.js +2758 -121
- package/dist/index.js.map +1 -1
- package/package.json +16 -13
package/dist/index.js
CHANGED
|
@@ -231,7 +231,7 @@ var init_seed_loader = __esm({
|
|
|
231
231
|
this.logger.info("[SeedLoader] Pass 2: resolving deferred references", {
|
|
232
232
|
count: deferredUpdates.length
|
|
233
233
|
});
|
|
234
|
-
await this.resolveDeferredUpdates(deferredUpdates, insertedRecords, allResults, allErrors);
|
|
234
|
+
await this.resolveDeferredUpdates(deferredUpdates, insertedRecords, allResults, allErrors, config.organizationId);
|
|
235
235
|
}
|
|
236
236
|
const durationMs = Date.now() - startTime;
|
|
237
237
|
return this.buildResult(config, graph, allResults, allErrors, durationMs);
|
|
@@ -288,7 +288,11 @@ var init_seed_loader = __esm({
|
|
|
288
288
|
}
|
|
289
289
|
let existingRecords;
|
|
290
290
|
if ((mode === "upsert" || mode === "update" || mode === "ignore") && !config.dryRun) {
|
|
291
|
-
existingRecords = await this.loadExistingRecords(
|
|
291
|
+
existingRecords = await this.loadExistingRecords(
|
|
292
|
+
objectName,
|
|
293
|
+
externalId,
|
|
294
|
+
config.organizationId
|
|
295
|
+
);
|
|
292
296
|
}
|
|
293
297
|
const objectRefs = refMap.get(objectName) || [];
|
|
294
298
|
const seedNow = /* @__PURE__ */ new Date();
|
|
@@ -303,6 +307,9 @@ var init_seed_loader = __esm({
|
|
|
303
307
|
`[SeedLoader] Failed to resolve dynamic values for ${objectName} record #${i}: ${seedResult.error.message}`
|
|
304
308
|
);
|
|
305
309
|
}
|
|
310
|
+
if (config.organizationId && record["organization_id"] == null) {
|
|
311
|
+
record["organization_id"] = config.organizationId;
|
|
312
|
+
}
|
|
306
313
|
for (const ref of objectRefs) {
|
|
307
314
|
const fieldValue = record[ref.field];
|
|
308
315
|
if (fieldValue === void 0 || fieldValue === null) continue;
|
|
@@ -313,7 +320,7 @@ var init_seed_loader = __esm({
|
|
|
313
320
|
record[ref.field] = resolvedId;
|
|
314
321
|
referencesResolved++;
|
|
315
322
|
} else if (!config.dryRun) {
|
|
316
|
-
const dbId = await this.resolveFromDatabase(ref.targetObject, ref.targetField, fieldValue);
|
|
323
|
+
const dbId = await this.resolveFromDatabase(ref.targetObject, ref.targetField, fieldValue, config.organizationId);
|
|
317
324
|
if (dbId) {
|
|
318
325
|
record[ref.field] = dbId;
|
|
319
326
|
referencesResolved++;
|
|
@@ -407,10 +414,12 @@ var init_seed_loader = __esm({
|
|
|
407
414
|
// ==========================================================================
|
|
408
415
|
// Internal: Reference Resolution
|
|
409
416
|
// ==========================================================================
|
|
410
|
-
async resolveFromDatabase(targetObject, targetField, value) {
|
|
417
|
+
async resolveFromDatabase(targetObject, targetField, value, organizationId) {
|
|
411
418
|
try {
|
|
419
|
+
const where = { [targetField]: value };
|
|
420
|
+
if (organizationId) where.organization_id = organizationId;
|
|
412
421
|
const records = await this.engine.find(targetObject, {
|
|
413
|
-
where
|
|
422
|
+
where,
|
|
414
423
|
fields: ["id"],
|
|
415
424
|
limit: 1,
|
|
416
425
|
context: { isSystem: true }
|
|
@@ -422,7 +431,7 @@ var init_seed_loader = __esm({
|
|
|
422
431
|
}
|
|
423
432
|
return null;
|
|
424
433
|
}
|
|
425
|
-
async resolveDeferredUpdates(deferredUpdates, insertedRecords, allResults, allErrors) {
|
|
434
|
+
async resolveDeferredUpdates(deferredUpdates, insertedRecords, allResults, allErrors, organizationId) {
|
|
426
435
|
for (const deferred of deferredUpdates) {
|
|
427
436
|
const targetMap = insertedRecords.get(deferred.targetObject);
|
|
428
437
|
let resolvedId = targetMap?.get(String(deferred.attemptedValue));
|
|
@@ -430,7 +439,8 @@ var init_seed_loader = __esm({
|
|
|
430
439
|
resolvedId = await this.resolveFromDatabase(
|
|
431
440
|
deferred.targetObject,
|
|
432
441
|
deferred.targetField,
|
|
433
|
-
deferred.attemptedValue
|
|
442
|
+
deferred.attemptedValue,
|
|
443
|
+
organizationId
|
|
434
444
|
) ?? void 0;
|
|
435
445
|
}
|
|
436
446
|
if (resolvedId) {
|
|
@@ -626,13 +636,15 @@ var init_seed_loader = __esm({
|
|
|
626
636
|
}
|
|
627
637
|
return map2;
|
|
628
638
|
}
|
|
629
|
-
async loadExistingRecords(objectName, externalId) {
|
|
639
|
+
async loadExistingRecords(objectName, externalId, organizationId) {
|
|
630
640
|
const map2 = /* @__PURE__ */ new Map();
|
|
631
641
|
try {
|
|
632
|
-
const
|
|
642
|
+
const findArgs = {
|
|
633
643
|
fields: ["id", externalId],
|
|
634
644
|
context: { isSystem: true }
|
|
635
|
-
}
|
|
645
|
+
};
|
|
646
|
+
if (organizationId) findArgs.where = { organization_id: organizationId };
|
|
647
|
+
const records = await this.engine.find(objectName, findArgs);
|
|
636
648
|
for (const record of records || []) {
|
|
637
649
|
const key = String(record[externalId] ?? "");
|
|
638
650
|
if (key) {
|
|
@@ -1161,9 +1173,12 @@ function buildSandboxApi(engineCtx, ql, errLabel) {
|
|
|
1161
1173
|
}
|
|
1162
1174
|
function buildSandboxContext(engineCtx, ql) {
|
|
1163
1175
|
const inputSnapshot = unwrapProxyToPlain(engineCtx?.input ?? engineCtx?.doc);
|
|
1176
|
+
const previousRaw = engineCtx?.previous ?? engineCtx?.previousDoc;
|
|
1164
1177
|
return {
|
|
1165
|
-
input: inputSnapshot,
|
|
1166
|
-
|
|
1178
|
+
input: inputSnapshot ?? {},
|
|
1179
|
+
// Preserve `undefined` for `previous` on insert events so hooks can
|
|
1180
|
+
// reliably distinguish create (`!ctx.previous`) from update/delete.
|
|
1181
|
+
previous: unwrapProxyToPlain(previousRaw),
|
|
1167
1182
|
user: engineCtx?.user ?? engineCtx?.session?.user,
|
|
1168
1183
|
session: engineCtx?.session,
|
|
1169
1184
|
event: typeof engineCtx?.event === "string" ? engineCtx.event : void 0,
|
|
@@ -1190,12 +1205,13 @@ function buildActionSandboxContext(actionCtx, ql) {
|
|
|
1190
1205
|
};
|
|
1191
1206
|
}
|
|
1192
1207
|
function unwrapProxyToPlain(v) {
|
|
1193
|
-
if (
|
|
1194
|
-
if (
|
|
1208
|
+
if (v === void 0 || v === null) return void 0;
|
|
1209
|
+
if (typeof v !== "object") return void 0;
|
|
1210
|
+
if (Array.isArray(v)) return void 0;
|
|
1195
1211
|
try {
|
|
1196
1212
|
return Object.fromEntries(Object.entries(v));
|
|
1197
1213
|
} catch {
|
|
1198
|
-
return
|
|
1214
|
+
return void 0;
|
|
1199
1215
|
}
|
|
1200
1216
|
}
|
|
1201
1217
|
var init_body_runner = __esm({
|
|
@@ -1412,8 +1428,51 @@ var init_app_plugin = __esm({
|
|
|
1412
1428
|
appId
|
|
1413
1429
|
});
|
|
1414
1430
|
}
|
|
1431
|
+
try {
|
|
1432
|
+
const approvals = Array.isArray(this.bundle.approvals) ? this.bundle.approvals : Array.isArray((this.bundle.manifest || {}).approvals) ? this.bundle.manifest.approvals : [];
|
|
1433
|
+
if (approvals.length > 0) {
|
|
1434
|
+
ctx.hook("kernel:ready", async () => {
|
|
1435
|
+
let svc;
|
|
1436
|
+
try {
|
|
1437
|
+
svc = ctx.getService("approvals");
|
|
1438
|
+
} catch {
|
|
1439
|
+
}
|
|
1440
|
+
if (!svc || typeof svc.defineProcess !== "function") {
|
|
1441
|
+
ctx.logger.warn("[AppPlugin] approvals service not registered \u2014 skipping declarative processes", {
|
|
1442
|
+
appId,
|
|
1443
|
+
processCount: approvals.length
|
|
1444
|
+
});
|
|
1445
|
+
return;
|
|
1446
|
+
}
|
|
1447
|
+
const sysCtx = { isSystem: true, roles: [], permissions: [] };
|
|
1448
|
+
let ok = 0;
|
|
1449
|
+
for (const proc of approvals) {
|
|
1450
|
+
try {
|
|
1451
|
+
await svc.defineProcess({
|
|
1452
|
+
name: proc.name,
|
|
1453
|
+
label: proc.label,
|
|
1454
|
+
object: proc.object,
|
|
1455
|
+
description: proc.description,
|
|
1456
|
+
active: proc.active !== false,
|
|
1457
|
+
definition: proc
|
|
1458
|
+
}, sysCtx);
|
|
1459
|
+
ok++;
|
|
1460
|
+
} catch (err) {
|
|
1461
|
+
ctx.logger.warn("[AppPlugin] Failed to register approval process", {
|
|
1462
|
+
appId,
|
|
1463
|
+
process: proc?.name,
|
|
1464
|
+
error: err?.message ?? String(err)
|
|
1465
|
+
});
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
ctx.logger.info("[AppPlugin] Registered approval processes", { appId, count: ok });
|
|
1469
|
+
});
|
|
1470
|
+
}
|
|
1471
|
+
} catch (err) {
|
|
1472
|
+
ctx.logger.error("[AppPlugin] Failed to schedule approval-process registration", err, { appId });
|
|
1473
|
+
}
|
|
1415
1474
|
this.emitCatalogEvent(ctx, "app:registered", sys);
|
|
1416
|
-
this.loadTranslations(ctx, appId);
|
|
1475
|
+
await this.loadTranslations(ctx, appId);
|
|
1417
1476
|
const seedDatasets = [];
|
|
1418
1477
|
if (Array.isArray(this.bundle.data)) {
|
|
1419
1478
|
seedDatasets.push(...this.bundle.data);
|
|
@@ -1429,46 +1488,107 @@ var init_app_plugin = __esm({
|
|
|
1429
1488
|
object: d.object
|
|
1430
1489
|
}));
|
|
1431
1490
|
try {
|
|
1432
|
-
const
|
|
1433
|
-
|
|
1434
|
-
|
|
1491
|
+
const kernel = ctx.kernel;
|
|
1492
|
+
const existing = (() => {
|
|
1493
|
+
try {
|
|
1494
|
+
return kernel?.getService?.("seed-datasets");
|
|
1495
|
+
} catch {
|
|
1496
|
+
return void 0;
|
|
1497
|
+
}
|
|
1498
|
+
})();
|
|
1499
|
+
const merged = Array.isArray(existing) ? [...existing, ...normalizedDatasets] : normalizedDatasets;
|
|
1500
|
+
const registerSvc = (name, value) => {
|
|
1501
|
+
if (kernel?.registerService) kernel.registerService(name, value);
|
|
1502
|
+
else if (typeof ctx.registerService === "function") ctx.registerService(name, value);
|
|
1503
|
+
};
|
|
1504
|
+
registerSvc("seed-datasets", merged);
|
|
1505
|
+
const metadataNow = ctx.getService("metadata");
|
|
1506
|
+
const loggerRef = ctx.logger;
|
|
1507
|
+
const replayer = async (organizationId) => {
|
|
1508
|
+
if (!organizationId) return { inserted: 0, updated: 0, errors: [] };
|
|
1509
|
+
const md = metadataNow ?? ctx.getService("metadata");
|
|
1510
|
+
if (!md) {
|
|
1511
|
+
loggerRef.warn("[seed-replayer] metadata service unavailable");
|
|
1512
|
+
return { inserted: 0, updated: 0, errors: [] };
|
|
1513
|
+
}
|
|
1514
|
+
const datasetsNow = (() => {
|
|
1515
|
+
try {
|
|
1516
|
+
return kernel?.getService?.("seed-datasets");
|
|
1517
|
+
} catch {
|
|
1518
|
+
return merged;
|
|
1519
|
+
}
|
|
1520
|
+
})() ?? merged;
|
|
1521
|
+
if (!Array.isArray(datasetsNow) || datasetsNow.length === 0) {
|
|
1522
|
+
return { inserted: 0, updated: 0, errors: [] };
|
|
1523
|
+
}
|
|
1524
|
+
const seedLoader = new SeedLoaderService(ql, md, loggerRef);
|
|
1435
1525
|
const { SeedLoaderRequestSchema } = await import("@objectstack/spec/data");
|
|
1436
1526
|
const request = SeedLoaderRequestSchema.parse({
|
|
1437
|
-
datasets:
|
|
1438
|
-
config: {
|
|
1527
|
+
datasets: datasetsNow,
|
|
1528
|
+
config: {
|
|
1529
|
+
defaultMode: "upsert",
|
|
1530
|
+
multiPass: true,
|
|
1531
|
+
organizationId
|
|
1532
|
+
}
|
|
1439
1533
|
});
|
|
1440
1534
|
const result = await seedLoader.load(request);
|
|
1441
|
-
|
|
1535
|
+
return {
|
|
1442
1536
|
inserted: result.summary.totalInserted,
|
|
1443
1537
|
updated: result.summary.totalUpdated,
|
|
1444
|
-
errors: result.errors
|
|
1445
|
-
}
|
|
1446
|
-
}
|
|
1447
|
-
|
|
1538
|
+
errors: result.errors
|
|
1539
|
+
};
|
|
1540
|
+
};
|
|
1541
|
+
registerSvc("seed-replayer", replayer);
|
|
1542
|
+
ctx.logger.info(`[Seeder] Registered ${normalizedDatasets.length} datasets + replayer on kernel (total datasets: ${merged.length})`);
|
|
1543
|
+
} catch (e) {
|
|
1544
|
+
ctx.logger.warn("[Seeder] Failed to register seed-datasets/seed-replayer service", { error: e?.message });
|
|
1545
|
+
}
|
|
1546
|
+
const multiTenant = String(process.env.OS_MULTI_TENANT ?? "true").toLowerCase() !== "false";
|
|
1547
|
+
if (multiTenant) {
|
|
1548
|
+
ctx.logger.info("[Seeder] multi-tenant mode \u2014 skipping inline seed; per-org replay will run on sys_organization insert");
|
|
1549
|
+
} else {
|
|
1550
|
+
try {
|
|
1551
|
+
const metadata = ctx.getService("metadata");
|
|
1552
|
+
if (metadata) {
|
|
1553
|
+
const seedLoader = new SeedLoaderService(ql, metadata, ctx.logger);
|
|
1554
|
+
const { SeedLoaderRequestSchema } = await import("@objectstack/spec/data");
|
|
1555
|
+
const request = SeedLoaderRequestSchema.parse({
|
|
1556
|
+
datasets: normalizedDatasets,
|
|
1557
|
+
config: { defaultMode: "upsert", multiPass: true }
|
|
1558
|
+
});
|
|
1559
|
+
const result = await seedLoader.load(request);
|
|
1560
|
+
ctx.logger.info("[Seeder] Seed loading complete", {
|
|
1561
|
+
inserted: result.summary.totalInserted,
|
|
1562
|
+
updated: result.summary.totalUpdated,
|
|
1563
|
+
errors: result.errors.length
|
|
1564
|
+
});
|
|
1565
|
+
} else {
|
|
1566
|
+
ctx.logger.debug("[Seeder] No metadata service; using basic insert fallback");
|
|
1567
|
+
for (const dataset of normalizedDatasets) {
|
|
1568
|
+
ctx.logger.info(`[Seeder] Seeding ${dataset.records.length} records for ${dataset.object}`);
|
|
1569
|
+
for (const record of dataset.records) {
|
|
1570
|
+
try {
|
|
1571
|
+
await ql.insert(dataset.object, record, { context: { isSystem: true } });
|
|
1572
|
+
} catch (err) {
|
|
1573
|
+
ctx.logger.warn(`[Seeder] Failed to insert ${dataset.object} record:`, { error: err.message });
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
ctx.logger.info("[Seeder] Data seeding complete.");
|
|
1578
|
+
}
|
|
1579
|
+
} catch (err) {
|
|
1580
|
+
ctx.logger.warn("[Seeder] SeedLoaderService failed, falling back to basic insert", { error: err.message });
|
|
1448
1581
|
for (const dataset of normalizedDatasets) {
|
|
1449
|
-
ctx.logger.info(`[Seeder] Seeding ${dataset.records.length} records for ${dataset.object}`);
|
|
1450
1582
|
for (const record of dataset.records) {
|
|
1451
1583
|
try {
|
|
1452
1584
|
await ql.insert(dataset.object, record, { context: { isSystem: true } });
|
|
1453
|
-
} catch (
|
|
1454
|
-
ctx.logger.warn(`[Seeder] Failed to insert ${dataset.object} record:`, { error:
|
|
1585
|
+
} catch (insertErr) {
|
|
1586
|
+
ctx.logger.warn(`[Seeder] Failed to insert ${dataset.object} record:`, { error: insertErr.message });
|
|
1455
1587
|
}
|
|
1456
1588
|
}
|
|
1457
1589
|
}
|
|
1458
|
-
ctx.logger.info("[Seeder] Data seeding complete.");
|
|
1590
|
+
ctx.logger.info("[Seeder] Data seeding complete (fallback).");
|
|
1459
1591
|
}
|
|
1460
|
-
} catch (err) {
|
|
1461
|
-
ctx.logger.warn("[Seeder] SeedLoaderService failed, falling back to basic insert", { error: err.message });
|
|
1462
|
-
for (const dataset of normalizedDatasets) {
|
|
1463
|
-
for (const record of dataset.records) {
|
|
1464
|
-
try {
|
|
1465
|
-
await ql.insert(dataset.object, record, { context: { isSystem: true } });
|
|
1466
|
-
} catch (insertErr) {
|
|
1467
|
-
ctx.logger.warn(`[Seeder] Failed to insert ${dataset.object} record:`, { error: insertErr.message });
|
|
1468
|
-
}
|
|
1469
|
-
}
|
|
1470
|
-
}
|
|
1471
|
-
ctx.logger.info("[Seeder] Data seeding complete (fallback).");
|
|
1472
1592
|
}
|
|
1473
1593
|
}
|
|
1474
1594
|
};
|
|
@@ -1527,7 +1647,7 @@ var init_app_plugin = __esm({
|
|
|
1527
1647
|
* Gracefully skips when the i18n service is not registered —
|
|
1528
1648
|
* this keeps AppPlugin resilient across server/dev/mock environments.
|
|
1529
1649
|
*/
|
|
1530
|
-
loadTranslations(ctx, appId) {
|
|
1650
|
+
async loadTranslations(ctx, appId) {
|
|
1531
1651
|
let i18nService;
|
|
1532
1652
|
try {
|
|
1533
1653
|
i18nService = ctx.getService("i18n");
|
|
@@ -1543,13 +1663,33 @@ var init_app_plugin = __esm({
|
|
|
1543
1663
|
}
|
|
1544
1664
|
if (!i18nService) {
|
|
1545
1665
|
if (bundles.length > 0) {
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1666
|
+
try {
|
|
1667
|
+
const mod = await import("@objectstack/core");
|
|
1668
|
+
const createMemoryI18n = mod.createMemoryI18n;
|
|
1669
|
+
if (typeof createMemoryI18n === "function") {
|
|
1670
|
+
const fallback = createMemoryI18n();
|
|
1671
|
+
ctx.registerService("i18n", fallback);
|
|
1672
|
+
i18nService = fallback;
|
|
1673
|
+
ctx.logger.info(
|
|
1674
|
+
`[i18n] Auto-registered in-memory i18n fallback for "${appId}" (${bundles.length} bundle(s) detected). Install I18nServicePlugin from @objectstack/service-i18n for file-based / production use.`
|
|
1675
|
+
);
|
|
1676
|
+
}
|
|
1677
|
+
} catch (err) {
|
|
1678
|
+
ctx.logger.warn(
|
|
1679
|
+
`[i18n] App "${appId}" has ${bundles.length} translation bundle(s) but auto-fallback failed: ${err?.message ?? err}.`
|
|
1680
|
+
);
|
|
1681
|
+
return;
|
|
1682
|
+
}
|
|
1683
|
+
if (!i18nService) {
|
|
1684
|
+
ctx.logger.warn(
|
|
1685
|
+
`[i18n] App "${appId}" has ${bundles.length} translation bundle(s) but no i18n service is registered.`
|
|
1686
|
+
);
|
|
1687
|
+
return;
|
|
1688
|
+
}
|
|
1549
1689
|
} else {
|
|
1550
1690
|
ctx.logger.debug("[i18n] No i18n service registered; skipping translation loading", { appId });
|
|
1691
|
+
return;
|
|
1551
1692
|
}
|
|
1552
|
-
return;
|
|
1553
1693
|
}
|
|
1554
1694
|
const i18nConfig = this.bundle.i18n || (this.bundle.manifest || this.bundle)?.i18n;
|
|
1555
1695
|
if (i18nConfig?.defaultLocale && typeof i18nService.setDefaultLocale === "function") {
|
|
@@ -34368,8 +34508,360 @@ var init_dist = __esm({
|
|
|
34368
34508
|
}
|
|
34369
34509
|
});
|
|
34370
34510
|
|
|
34511
|
+
// src/cloud/platform-sso.ts
|
|
34512
|
+
var platform_sso_exports = {};
|
|
34513
|
+
__export(platform_sso_exports, {
|
|
34514
|
+
PLATFORM_SSO_PROVIDER_ID: () => PLATFORM_SSO_PROVIDER_ID,
|
|
34515
|
+
backfillPlatformSsoClients: () => backfillPlatformSsoClients,
|
|
34516
|
+
buildPlatformSsoRedirectUri: () => buildPlatformSsoRedirectUri,
|
|
34517
|
+
derivePlatformSsoClientId: () => derivePlatformSsoClientId,
|
|
34518
|
+
derivePlatformSsoClientSecret: () => derivePlatformSsoClientSecret,
|
|
34519
|
+
hashPlatformSsoClientSecret: () => hashPlatformSsoClientSecret,
|
|
34520
|
+
seedPlatformSsoClient: () => seedPlatformSsoClient
|
|
34521
|
+
});
|
|
34522
|
+
import { createHmac, createHash } from "crypto";
|
|
34523
|
+
function derivePlatformSsoClientId(projectId) {
|
|
34524
|
+
return `project_${projectId}`;
|
|
34525
|
+
}
|
|
34526
|
+
function derivePlatformSsoClientSecret(baseSecret, projectId) {
|
|
34527
|
+
return createHmac("sha256", baseSecret).update(`oauth-client:${projectId}`).digest("hex");
|
|
34528
|
+
}
|
|
34529
|
+
function hashPlatformSsoClientSecret(plaintext) {
|
|
34530
|
+
return createHash("sha256").update(plaintext).digest("base64").replace(/=+$/, "").replace(/\+/g, "-").replace(/\//g, "_");
|
|
34531
|
+
}
|
|
34532
|
+
function buildPlatformSsoRedirectUri(hostname, basePath = "/api/v1/auth") {
|
|
34533
|
+
let host;
|
|
34534
|
+
if (hostname.startsWith("http://") || hostname.startsWith("https://")) {
|
|
34535
|
+
host = hostname;
|
|
34536
|
+
} else if (/(\.|^)localhost(:\d+)?$/i.test(hostname)) {
|
|
34537
|
+
const port = (process.env.OS_RUNTIME_PORT ?? "").trim();
|
|
34538
|
+
const hostWithPort = /:\d+$/.test(hostname) || !port ? hostname : `${hostname}:${port}`;
|
|
34539
|
+
host = `http://${hostWithPort}`;
|
|
34540
|
+
} else {
|
|
34541
|
+
host = `https://${hostname}`;
|
|
34542
|
+
}
|
|
34543
|
+
const trimmed = host.replace(/\/+$/, "");
|
|
34544
|
+
const path = basePath.replace(/\/+$/, "");
|
|
34545
|
+
return `${trimmed}${path}/oauth2/callback/${PLATFORM_SSO_PROVIDER_ID}`;
|
|
34546
|
+
}
|
|
34547
|
+
async function seedPlatformSsoClient(opts) {
|
|
34548
|
+
const { ql, projectId, hostname, baseSecret, logger, throwOnError } = opts;
|
|
34549
|
+
if (!baseSecret) {
|
|
34550
|
+
logger?.warn?.("[platform-sso] OS_AUTH_SECRET not set \u2014 skipping client seed", { projectId });
|
|
34551
|
+
return;
|
|
34552
|
+
}
|
|
34553
|
+
const clientId = derivePlatformSsoClientId(projectId);
|
|
34554
|
+
const clientSecretPlaintext = derivePlatformSsoClientSecret(baseSecret, projectId);
|
|
34555
|
+
const clientSecretStored = hashPlatformSsoClientSecret(clientSecretPlaintext);
|
|
34556
|
+
const desiredRedirect = hostname ? buildPlatformSsoRedirectUri(hostname) : null;
|
|
34557
|
+
let existing = null;
|
|
34558
|
+
try {
|
|
34559
|
+
const rows = await ql.find("sys_oauth_application", {
|
|
34560
|
+
where: { client_id: clientId },
|
|
34561
|
+
limit: 1
|
|
34562
|
+
}, { context: { isSystem: true } });
|
|
34563
|
+
const list = Array.isArray(rows) ? rows : Array.isArray(rows?.records) ? rows.records : [];
|
|
34564
|
+
existing = list[0] ?? null;
|
|
34565
|
+
} catch (err) {
|
|
34566
|
+
logger?.warn?.("[platform-sso] sys_oauth_application read failed \u2014 skipping seed", {
|
|
34567
|
+
projectId,
|
|
34568
|
+
error: err?.message
|
|
34569
|
+
});
|
|
34570
|
+
return;
|
|
34571
|
+
}
|
|
34572
|
+
const nowIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
34573
|
+
if (!existing) {
|
|
34574
|
+
const redirects = desiredRedirect ? [desiredRedirect] : [];
|
|
34575
|
+
try {
|
|
34576
|
+
await ql.insert("sys_oauth_application", {
|
|
34577
|
+
id: `oauthc_${projectId}`,
|
|
34578
|
+
name: `Project ${projectId}`,
|
|
34579
|
+
client_id: clientId,
|
|
34580
|
+
client_secret: clientSecretStored,
|
|
34581
|
+
type: "web",
|
|
34582
|
+
redirect_uris: JSON.stringify(redirects),
|
|
34583
|
+
grant_types: JSON.stringify(["authorization_code", "refresh_token"]),
|
|
34584
|
+
response_types: JSON.stringify(["code"]),
|
|
34585
|
+
scopes: JSON.stringify(["openid", "email", "profile"]),
|
|
34586
|
+
token_endpoint_auth_method: "client_secret_basic",
|
|
34587
|
+
require_pkce: false,
|
|
34588
|
+
skip_consent: true,
|
|
34589
|
+
disabled: false,
|
|
34590
|
+
subject_type: "public",
|
|
34591
|
+
created_at: nowIso,
|
|
34592
|
+
updated_at: nowIso
|
|
34593
|
+
}, { context: { isSystem: true } });
|
|
34594
|
+
logger?.info?.("[platform-sso] sys_oauth_application row created", { projectId, clientId });
|
|
34595
|
+
} catch (err) {
|
|
34596
|
+
logger?.warn?.("[platform-sso] sys_oauth_application create failed", {
|
|
34597
|
+
projectId,
|
|
34598
|
+
error: err?.message
|
|
34599
|
+
});
|
|
34600
|
+
if (throwOnError) throw err;
|
|
34601
|
+
}
|
|
34602
|
+
return;
|
|
34603
|
+
}
|
|
34604
|
+
let currentRedirects = [];
|
|
34605
|
+
try {
|
|
34606
|
+
const raw = existing.redirect_uris;
|
|
34607
|
+
const parsed = typeof raw === "string" ? JSON.parse(raw) : raw;
|
|
34608
|
+
if (Array.isArray(parsed)) currentRedirects = parsed.filter((s) => typeof s === "string");
|
|
34609
|
+
} catch {
|
|
34610
|
+
}
|
|
34611
|
+
const mergedRedirects = desiredRedirect && !currentRedirects.includes(desiredRedirect) ? [...currentRedirects, desiredRedirect] : currentRedirects;
|
|
34612
|
+
const repairPatch = {
|
|
34613
|
+
name: existing.name || `Project ${projectId}`,
|
|
34614
|
+
client_secret: clientSecretStored,
|
|
34615
|
+
type: existing.type || "web",
|
|
34616
|
+
redirect_uris: JSON.stringify(mergedRedirects),
|
|
34617
|
+
grant_types: JSON.stringify(["authorization_code", "refresh_token"]),
|
|
34618
|
+
response_types: JSON.stringify(["code"]),
|
|
34619
|
+
scopes: JSON.stringify(["openid", "email", "profile"]),
|
|
34620
|
+
token_endpoint_auth_method: "client_secret_basic",
|
|
34621
|
+
require_pkce: false,
|
|
34622
|
+
skip_consent: true,
|
|
34623
|
+
disabled: false,
|
|
34624
|
+
subject_type: "public",
|
|
34625
|
+
updated_at: nowIso
|
|
34626
|
+
};
|
|
34627
|
+
try {
|
|
34628
|
+
await ql.update(
|
|
34629
|
+
"sys_oauth_application",
|
|
34630
|
+
repairPatch,
|
|
34631
|
+
{ where: { id: existing.id } },
|
|
34632
|
+
{ context: { isSystem: true } }
|
|
34633
|
+
);
|
|
34634
|
+
logger?.info?.("[platform-sso] sys_oauth_application repaired", {
|
|
34635
|
+
projectId,
|
|
34636
|
+
clientId,
|
|
34637
|
+
redirect_uris: mergedRedirects
|
|
34638
|
+
});
|
|
34639
|
+
} catch (err) {
|
|
34640
|
+
logger?.warn?.("[platform-sso] sys_oauth_application repair failed", {
|
|
34641
|
+
projectId,
|
|
34642
|
+
error: err?.message
|
|
34643
|
+
});
|
|
34644
|
+
if (throwOnError) throw err;
|
|
34645
|
+
}
|
|
34646
|
+
}
|
|
34647
|
+
async function backfillPlatformSsoClients(opts) {
|
|
34648
|
+
const { ql, baseSecret, logger, limit = 1e3 } = opts;
|
|
34649
|
+
if (!baseSecret) {
|
|
34650
|
+
logger?.warn?.("[platform-sso] backfill skipped \u2014 OS_AUTH_SECRET not set");
|
|
34651
|
+
return { scanned: 0, seeded: 0, alreadyExisted: 0, failures: [] };
|
|
34652
|
+
}
|
|
34653
|
+
let projects = [];
|
|
34654
|
+
try {
|
|
34655
|
+
const rows = await ql.find("sys_environment", {
|
|
34656
|
+
limit,
|
|
34657
|
+
fields: ["id", "hostname", "status"]
|
|
34658
|
+
}, { context: { isSystem: true } });
|
|
34659
|
+
projects = Array.isArray(rows) ? rows : Array.isArray(rows?.records) ? rows.records : [];
|
|
34660
|
+
} catch (err) {
|
|
34661
|
+
logger?.warn?.("[platform-sso] backfill: sys_project read failed", {
|
|
34662
|
+
error: err?.message
|
|
34663
|
+
});
|
|
34664
|
+
return { scanned: 0, seeded: 0, alreadyExisted: 0, failures: [{ projectId: "<scan>", error: err?.message ?? String(err) }] };
|
|
34665
|
+
}
|
|
34666
|
+
let seeded = 0;
|
|
34667
|
+
let alreadyExisted = 0;
|
|
34668
|
+
const failures = [];
|
|
34669
|
+
for (const p of projects) {
|
|
34670
|
+
if (!p?.id) continue;
|
|
34671
|
+
const before = await (async () => {
|
|
34672
|
+
try {
|
|
34673
|
+
const r = await ql.find("sys_oauth_application", {
|
|
34674
|
+
where: { client_id: derivePlatformSsoClientId(p.id) },
|
|
34675
|
+
limit: 1
|
|
34676
|
+
}, { context: { isSystem: true } });
|
|
34677
|
+
const list = Array.isArray(r) ? r : Array.isArray(r?.records) ? r.records : [];
|
|
34678
|
+
return list[0] ?? null;
|
|
34679
|
+
} catch {
|
|
34680
|
+
return null;
|
|
34681
|
+
}
|
|
34682
|
+
})();
|
|
34683
|
+
try {
|
|
34684
|
+
await seedPlatformSsoClient({ ql, projectId: p.id, hostname: p.hostname, baseSecret, logger, throwOnError: true });
|
|
34685
|
+
if (before) alreadyExisted++;
|
|
34686
|
+
else {
|
|
34687
|
+
const after = await (async () => {
|
|
34688
|
+
try {
|
|
34689
|
+
const r = await ql.find("sys_oauth_application", {
|
|
34690
|
+
where: { client_id: derivePlatformSsoClientId(p.id) },
|
|
34691
|
+
limit: 1
|
|
34692
|
+
}, { context: { isSystem: true } });
|
|
34693
|
+
const list = Array.isArray(r) ? r : Array.isArray(r?.records) ? r.records : [];
|
|
34694
|
+
return list[0] ?? null;
|
|
34695
|
+
} catch (err) {
|
|
34696
|
+
return { _readErr: err?.message };
|
|
34697
|
+
}
|
|
34698
|
+
})();
|
|
34699
|
+
if (after && !after._readErr) seeded++;
|
|
34700
|
+
else failures.push({ projectId: p.id, error: `post-insert read returned ${after ? JSON.stringify(after) : "null"}` });
|
|
34701
|
+
}
|
|
34702
|
+
} catch (err) {
|
|
34703
|
+
failures.push({ projectId: p.id, error: err?.message ?? String(err) });
|
|
34704
|
+
}
|
|
34705
|
+
}
|
|
34706
|
+
logger?.info?.("[platform-sso] backfill complete", { scanned: projects.length, seeded, alreadyExisted, failures: failures.length });
|
|
34707
|
+
return { scanned: projects.length, seeded, alreadyExisted, failures };
|
|
34708
|
+
}
|
|
34709
|
+
var PLATFORM_SSO_PROVIDER_ID;
|
|
34710
|
+
var init_platform_sso = __esm({
|
|
34711
|
+
"src/cloud/platform-sso.ts"() {
|
|
34712
|
+
"use strict";
|
|
34713
|
+
PLATFORM_SSO_PROVIDER_ID = "objectstack-cloud";
|
|
34714
|
+
}
|
|
34715
|
+
});
|
|
34716
|
+
|
|
34717
|
+
// src/cloud/project-org-seed.ts
|
|
34718
|
+
var project_org_seed_exports = {};
|
|
34719
|
+
__export(project_org_seed_exports, {
|
|
34720
|
+
seedProjectMember: () => seedProjectMember,
|
|
34721
|
+
seedProjectOrganization: () => seedProjectOrganization
|
|
34722
|
+
});
|
|
34723
|
+
async function seedProjectOrganization(kernel, seed, logger) {
|
|
34724
|
+
if (!seed?.id || !seed?.name) return "skipped";
|
|
34725
|
+
try {
|
|
34726
|
+
const ql = kernel.getService("objectql");
|
|
34727
|
+
if (!ql?.insert || !ql?.find) {
|
|
34728
|
+
logger?.warn?.("[seedProjectOrganization] objectql service unavailable", { orgId: seed.id });
|
|
34729
|
+
return "skipped";
|
|
34730
|
+
}
|
|
34731
|
+
try {
|
|
34732
|
+
const existing = await ql.find(SYS_ORG, { where: { id: seed.id } });
|
|
34733
|
+
const rows = Array.isArray(existing) ? existing : existing?.value ?? [];
|
|
34734
|
+
if (Array.isArray(rows) && rows.length > 0) return "exists";
|
|
34735
|
+
} catch {
|
|
34736
|
+
}
|
|
34737
|
+
const nowIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
34738
|
+
await ql.insert(SYS_ORG, {
|
|
34739
|
+
id: seed.id,
|
|
34740
|
+
name: seed.name,
|
|
34741
|
+
slug: seed.slug ?? null,
|
|
34742
|
+
logo: seed.logo ?? null,
|
|
34743
|
+
metadata: null,
|
|
34744
|
+
created_at: nowIso
|
|
34745
|
+
});
|
|
34746
|
+
logger?.info?.("[seedProjectOrganization] org seeded", {
|
|
34747
|
+
orgId: seed.id,
|
|
34748
|
+
name: seed.name
|
|
34749
|
+
});
|
|
34750
|
+
return "inserted";
|
|
34751
|
+
} catch (err) {
|
|
34752
|
+
logger?.warn?.("[seedProjectOrganization] failed (non-fatal)", {
|
|
34753
|
+
orgId: seed.id,
|
|
34754
|
+
error: err?.message
|
|
34755
|
+
});
|
|
34756
|
+
return "error";
|
|
34757
|
+
}
|
|
34758
|
+
}
|
|
34759
|
+
async function seedProjectMember(kernel, args, logger) {
|
|
34760
|
+
const { userId, organizationId } = args;
|
|
34761
|
+
const role = args.role ?? "member";
|
|
34762
|
+
if (!userId || !organizationId) return "skipped";
|
|
34763
|
+
try {
|
|
34764
|
+
const ql = kernel.getService("objectql");
|
|
34765
|
+
if (!ql?.insert || !ql?.find) {
|
|
34766
|
+
logger?.warn?.("[seedProjectMember] objectql service unavailable", { userId, organizationId });
|
|
34767
|
+
return "skipped";
|
|
34768
|
+
}
|
|
34769
|
+
try {
|
|
34770
|
+
const existing = await ql.find("sys_member", {
|
|
34771
|
+
where: { user_id: userId, organization_id: organizationId }
|
|
34772
|
+
});
|
|
34773
|
+
const rows = Array.isArray(existing) ? existing : existing?.value ?? [];
|
|
34774
|
+
if (Array.isArray(rows) && rows.length > 0) return "exists";
|
|
34775
|
+
} catch {
|
|
34776
|
+
}
|
|
34777
|
+
const nowIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
34778
|
+
const memId = `mem_${Math.random().toString(36).slice(2, 14)}`;
|
|
34779
|
+
await ql.insert("sys_member", {
|
|
34780
|
+
id: memId,
|
|
34781
|
+
organization_id: organizationId,
|
|
34782
|
+
user_id: userId,
|
|
34783
|
+
role,
|
|
34784
|
+
created_at: nowIso
|
|
34785
|
+
});
|
|
34786
|
+
logger?.info?.("[seedProjectMember] member seeded", {
|
|
34787
|
+
userId,
|
|
34788
|
+
organizationId,
|
|
34789
|
+
role
|
|
34790
|
+
});
|
|
34791
|
+
return "inserted";
|
|
34792
|
+
} catch (err) {
|
|
34793
|
+
logger?.warn?.("[seedProjectMember] failed (non-fatal)", {
|
|
34794
|
+
userId,
|
|
34795
|
+
organizationId,
|
|
34796
|
+
error: err?.message
|
|
34797
|
+
});
|
|
34798
|
+
return "error";
|
|
34799
|
+
}
|
|
34800
|
+
}
|
|
34801
|
+
var SYS_ORG;
|
|
34802
|
+
var init_project_org_seed = __esm({
|
|
34803
|
+
"src/cloud/project-org-seed.ts"() {
|
|
34804
|
+
"use strict";
|
|
34805
|
+
SYS_ORG = "sys_organization";
|
|
34806
|
+
}
|
|
34807
|
+
});
|
|
34808
|
+
|
|
34809
|
+
// src/cloud/project-owner-seed.ts
|
|
34810
|
+
var project_owner_seed_exports = {};
|
|
34811
|
+
__export(project_owner_seed_exports, {
|
|
34812
|
+
seedProjectOwner: () => seedProjectOwner
|
|
34813
|
+
});
|
|
34814
|
+
async function seedProjectOwner(kernel, seed, logger) {
|
|
34815
|
+
if (!seed?.userId || !seed?.email) return "skipped";
|
|
34816
|
+
try {
|
|
34817
|
+
const ql = kernel.getService("objectql");
|
|
34818
|
+
if (!ql?.insert || !ql?.find) {
|
|
34819
|
+
logger?.warn?.("[seedProjectOwner] objectql service unavailable", { userId: seed.userId });
|
|
34820
|
+
return "skipped";
|
|
34821
|
+
}
|
|
34822
|
+
try {
|
|
34823
|
+
const existing = await ql.find(SYS_USER, { where: { id: seed.userId } });
|
|
34824
|
+
const rows = Array.isArray(existing) ? existing : existing?.value ?? [];
|
|
34825
|
+
if (Array.isArray(rows) && rows.length > 0) return "exists";
|
|
34826
|
+
} catch {
|
|
34827
|
+
}
|
|
34828
|
+
const nowIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
34829
|
+
await ql.insert(SYS_USER, {
|
|
34830
|
+
id: seed.userId,
|
|
34831
|
+
email: seed.email,
|
|
34832
|
+
name: seed.name ?? seed.email.split("@")[0] ?? "Owner",
|
|
34833
|
+
image: seed.image ?? null,
|
|
34834
|
+
// Cloud already verified the upstream email. Marking it verified
|
|
34835
|
+
// here is what unblocks better-auth's accountLinking check on
|
|
34836
|
+
// the first SSO callback (alongside the trustedProviders config
|
|
34837
|
+
// in plugin-auth/auth-manager.ts).
|
|
34838
|
+
email_verified: true,
|
|
34839
|
+
created_at: nowIso,
|
|
34840
|
+
updated_at: nowIso
|
|
34841
|
+
});
|
|
34842
|
+
logger?.info?.("[seedProjectOwner] owner seeded", {
|
|
34843
|
+
userId: seed.userId,
|
|
34844
|
+
email: seed.email
|
|
34845
|
+
});
|
|
34846
|
+
return "inserted";
|
|
34847
|
+
} catch (err) {
|
|
34848
|
+
logger?.warn?.("[seedProjectOwner] failed (non-fatal)", {
|
|
34849
|
+
userId: seed.userId,
|
|
34850
|
+
error: err?.message
|
|
34851
|
+
});
|
|
34852
|
+
return "error";
|
|
34853
|
+
}
|
|
34854
|
+
}
|
|
34855
|
+
var SYS_USER;
|
|
34856
|
+
var init_project_owner_seed = __esm({
|
|
34857
|
+
"src/cloud/project-owner-seed.ts"() {
|
|
34858
|
+
"use strict";
|
|
34859
|
+
SYS_USER = "sys_user";
|
|
34860
|
+
}
|
|
34861
|
+
});
|
|
34862
|
+
|
|
34371
34863
|
// src/index.ts
|
|
34372
|
-
import { ObjectKernel as
|
|
34864
|
+
import { ObjectKernel as ObjectKernel4 } from "@objectstack/core";
|
|
34373
34865
|
|
|
34374
34866
|
// src/runtime.ts
|
|
34375
34867
|
import { ObjectKernel } from "@objectstack/core";
|
|
@@ -34506,15 +34998,45 @@ async function createStandaloneStack(config) {
|
|
|
34506
34998
|
new ObjectQLPlugin({ projectId })
|
|
34507
34999
|
];
|
|
34508
35000
|
if (artifactBundle) plugins.push(new AppPlugin2(artifactBundle));
|
|
35001
|
+
const requires = Array.isArray(artifactBundle?.requires) ? artifactBundle.requires.filter((c) => typeof c === "string") : void 0;
|
|
35002
|
+
const objects = Array.isArray(artifactBundle?.objects) ? artifactBundle.objects : void 0;
|
|
35003
|
+
const manifest = artifactBundle?.manifest;
|
|
34509
35004
|
return {
|
|
34510
35005
|
plugins,
|
|
34511
35006
|
api: {
|
|
34512
35007
|
enableProjectScoping: false,
|
|
34513
35008
|
projectResolution: "none"
|
|
34514
|
-
}
|
|
35009
|
+
},
|
|
35010
|
+
...requires ? { requires } : {},
|
|
35011
|
+
...objects ? { objects } : {},
|
|
35012
|
+
...manifest ? { manifest } : {}
|
|
34515
35013
|
};
|
|
34516
35014
|
}
|
|
34517
35015
|
|
|
35016
|
+
// src/default-host.ts
|
|
35017
|
+
import { resolve as resolvePath3 } from "path";
|
|
35018
|
+
import { existsSync } from "fs";
|
|
35019
|
+
init_load_artifact_bundle();
|
|
35020
|
+
function resolveDefaultArtifactPath(explicitPath, cwd = process.cwd()) {
|
|
35021
|
+
const candidate = explicitPath ?? process.env.OS_ARTIFACT_PATH ?? resolvePath3(cwd, "dist/objectstack.json");
|
|
35022
|
+
if (isHttpUrl(candidate)) return candidate;
|
|
35023
|
+
if (explicitPath || process.env.OS_ARTIFACT_PATH) return candidate;
|
|
35024
|
+
return existsSync(candidate) ? candidate : void 0;
|
|
35025
|
+
}
|
|
35026
|
+
async function createDefaultHostConfig(options = {}) {
|
|
35027
|
+
const { requireArtifact = true, ...standaloneOpts } = options;
|
|
35028
|
+
const resolvedArtifact = resolveDefaultArtifactPath(standaloneOpts.artifactPath);
|
|
35029
|
+
if (!resolvedArtifact && requireArtifact) {
|
|
35030
|
+
throw new Error(
|
|
35031
|
+
"[createDefaultHostConfig] No artifact source available. Set OS_ARTIFACT_PATH (file path or http(s):// URL), place the artifact at <cwd>/dist/objectstack.json, or pass `{ artifactPath: ... }` explicitly. To boot an empty kernel anyway, pass `{ requireArtifact: false }`."
|
|
35032
|
+
);
|
|
35033
|
+
}
|
|
35034
|
+
return createStandaloneStack({
|
|
35035
|
+
...standaloneOpts,
|
|
35036
|
+
artifactPath: resolvedArtifact
|
|
35037
|
+
});
|
|
35038
|
+
}
|
|
35039
|
+
|
|
34518
35040
|
// src/index.ts
|
|
34519
35041
|
init_driver_plugin();
|
|
34520
35042
|
init_app_plugin();
|
|
@@ -34869,7 +35391,7 @@ var _HttpDispatcher = class _HttpDispatcher {
|
|
|
34869
35391
|
* so project-scoped meta routes can resolve their project).
|
|
34870
35392
|
*/
|
|
34871
35393
|
async resolveEnvironmentContext(context, path) {
|
|
34872
|
-
const skipPaths = ["/
|
|
35394
|
+
const skipPaths = ["/cloud", "/health", "/discovery"];
|
|
34873
35395
|
if (skipPaths.some((p) => path.startsWith(p))) {
|
|
34874
35396
|
return;
|
|
34875
35397
|
}
|
|
@@ -34941,7 +35463,7 @@ var _HttpDispatcher = class _HttpDispatcher {
|
|
|
34941
35463
|
const qlService = await this.getObjectQLService();
|
|
34942
35464
|
const ql = qlService ?? await this.resolveService("objectql");
|
|
34943
35465
|
if (ql) {
|
|
34944
|
-
let rows = await ql.find("
|
|
35466
|
+
let rows = await ql.find("sys_environment", {
|
|
34945
35467
|
where: {
|
|
34946
35468
|
organization_id: activeOrganizationId,
|
|
34947
35469
|
is_default: true
|
|
@@ -35028,8 +35550,8 @@ var _HttpDispatcher = class _HttpDispatcher {
|
|
|
35028
35550
|
const qlService = await this.getObjectQLService();
|
|
35029
35551
|
const ql = qlService ?? await this.resolveService("objectql");
|
|
35030
35552
|
if (!ql) return null;
|
|
35031
|
-
let rows = await ql.find("
|
|
35032
|
-
where: {
|
|
35553
|
+
let rows = await ql.find("sys_environment_member", {
|
|
35554
|
+
where: { environment_id: projectId, user_id: userId },
|
|
35033
35555
|
limit: 1
|
|
35034
35556
|
});
|
|
35035
35557
|
if (rows && rows.value) rows = rows.value;
|
|
@@ -35284,8 +35806,9 @@ var _HttpDispatcher = class _HttpDispatcher {
|
|
|
35284
35806
|
}
|
|
35285
35807
|
return { handled: true, response: this.success({ types: ["object", "app", "plugin"] }) };
|
|
35286
35808
|
}
|
|
35287
|
-
if (parts.length
|
|
35288
|
-
const
|
|
35809
|
+
if (parts.length >= 3 && parts[parts.length - 1] === "published" && (!method || method === "GET")) {
|
|
35810
|
+
const type = parts[0];
|
|
35811
|
+
const name = parts.slice(1, -1).join("/");
|
|
35289
35812
|
const metadataService = await this.getService(CoreServiceName.enum.metadata);
|
|
35290
35813
|
if (metadataService && typeof metadataService.getPublished === "function") {
|
|
35291
35814
|
const data = await metadataService.getPublished(type, name);
|
|
@@ -35302,14 +35825,16 @@ var _HttpDispatcher = class _HttpDispatcher {
|
|
|
35302
35825
|
}
|
|
35303
35826
|
return { handled: true, response: this.error("Not found", 404) };
|
|
35304
35827
|
}
|
|
35305
|
-
if (parts.length
|
|
35306
|
-
const
|
|
35828
|
+
if (parts.length >= 2) {
|
|
35829
|
+
const type = parts[0];
|
|
35830
|
+
const name = parts.slice(1).join("/");
|
|
35307
35831
|
const packageId = query?.package || void 0;
|
|
35308
35832
|
if (method === "PUT" && body) {
|
|
35309
35833
|
const protocol = await this.resolveService("protocol");
|
|
35310
35834
|
if (protocol && typeof protocol.saveMetaItem === "function") {
|
|
35311
35835
|
try {
|
|
35312
|
-
const
|
|
35836
|
+
const organizationId = await this.resolveActiveOrganizationId(_context);
|
|
35837
|
+
const result = await protocol.saveMetaItem({ type, name, item: body, organizationId });
|
|
35313
35838
|
return { handled: true, response: this.success(result) };
|
|
35314
35839
|
} catch (e) {
|
|
35315
35840
|
return { handled: true, response: this.error(e.message, 400) };
|
|
@@ -35333,7 +35858,8 @@ var _HttpDispatcher = class _HttpDispatcher {
|
|
|
35333
35858
|
const scoped = scopedEnv !== void 0;
|
|
35334
35859
|
if (scoped && typeof protocol2.getMetaItem === "function") {
|
|
35335
35860
|
try {
|
|
35336
|
-
const
|
|
35861
|
+
const organizationId = await this.resolveActiveOrganizationId(_context);
|
|
35862
|
+
const data = await protocol2.getMetaItem({ type: "object", name, organizationId });
|
|
35337
35863
|
if (data && (data.item ?? data)) {
|
|
35338
35864
|
return { handled: true, response: this.success(data) };
|
|
35339
35865
|
}
|
|
@@ -35347,7 +35873,8 @@ var _HttpDispatcher = class _HttpDispatcher {
|
|
|
35347
35873
|
}
|
|
35348
35874
|
if (!scoped && protocol2 && typeof protocol2.getMetaItem === "function") {
|
|
35349
35875
|
try {
|
|
35350
|
-
const
|
|
35876
|
+
const organizationId = await this.resolveActiveOrganizationId(_context);
|
|
35877
|
+
const data = await protocol2.getMetaItem({ type: "object", name, organizationId });
|
|
35351
35878
|
if (data && (data.item ?? data)) {
|
|
35352
35879
|
return { handled: true, response: this.success(data) };
|
|
35353
35880
|
}
|
|
@@ -35360,7 +35887,8 @@ var _HttpDispatcher = class _HttpDispatcher {
|
|
|
35360
35887
|
const protocol = await this.resolveService("protocol");
|
|
35361
35888
|
if (protocol && typeof protocol.getMetaItem === "function") {
|
|
35362
35889
|
try {
|
|
35363
|
-
const
|
|
35890
|
+
const organizationId = await this.resolveActiveOrganizationId(_context);
|
|
35891
|
+
const data = await protocol.getMetaItem({ type: singularType, name, packageId, organizationId });
|
|
35364
35892
|
return { handled: true, response: this.success(data) };
|
|
35365
35893
|
} catch (e) {
|
|
35366
35894
|
}
|
|
@@ -35384,7 +35912,8 @@ var _HttpDispatcher = class _HttpDispatcher {
|
|
|
35384
35912
|
const protocol = await this.resolveService("protocol");
|
|
35385
35913
|
if (protocol && typeof protocol.getMetaItems === "function") {
|
|
35386
35914
|
try {
|
|
35387
|
-
const
|
|
35915
|
+
const organizationId = await this.resolveActiveOrganizationId(_context);
|
|
35916
|
+
const data = await protocol.getMetaItems({ type: typeOrName, packageId, organizationId });
|
|
35388
35917
|
if (data && (data.items !== void 0 || Array.isArray(data))) {
|
|
35389
35918
|
return { handled: true, response: this.success(data) };
|
|
35390
35919
|
}
|
|
@@ -35723,6 +36252,61 @@ var _HttpDispatcher = class _HttpDispatcher {
|
|
|
35723
36252
|
* Physical database addressing (database_url, database_driver, etc.)
|
|
35724
36253
|
* is stored directly on the sys_project row.
|
|
35725
36254
|
*/
|
|
36255
|
+
/**
|
|
36256
|
+
* Resolve the calling user id from the request session, if any.
|
|
36257
|
+
* Returns `undefined` for anonymous calls or when auth is not wired up.
|
|
36258
|
+
*/
|
|
36259
|
+
async resolveActiveOrganizationId(context) {
|
|
36260
|
+
try {
|
|
36261
|
+
const authService = await this.resolveService(CoreServiceName.enum.auth);
|
|
36262
|
+
const rawHeaders = context.request?.headers;
|
|
36263
|
+
let headers = rawHeaders;
|
|
36264
|
+
if (rawHeaders && typeof rawHeaders === "object" && typeof rawHeaders.get !== "function") {
|
|
36265
|
+
try {
|
|
36266
|
+
const h = new Headers();
|
|
36267
|
+
for (const [k, v] of Object.entries(rawHeaders)) {
|
|
36268
|
+
if (v == null) continue;
|
|
36269
|
+
h.set(k, Array.isArray(v) ? v.join(", ") : String(v));
|
|
36270
|
+
}
|
|
36271
|
+
headers = h;
|
|
36272
|
+
} catch {
|
|
36273
|
+
headers = rawHeaders;
|
|
36274
|
+
}
|
|
36275
|
+
}
|
|
36276
|
+
const apiObj = authService?.auth?.api ?? authService?.api;
|
|
36277
|
+
const sessionData = await apiObj?.getSession?.call(apiObj, { headers });
|
|
36278
|
+
const oid = sessionData?.session?.activeOrganizationId;
|
|
36279
|
+
return typeof oid === "string" && oid.length > 0 ? oid : void 0;
|
|
36280
|
+
} catch {
|
|
36281
|
+
return void 0;
|
|
36282
|
+
}
|
|
36283
|
+
}
|
|
36284
|
+
async resolveCallerUserId(context) {
|
|
36285
|
+
try {
|
|
36286
|
+
const authService = await this.resolveService(CoreServiceName.enum.auth);
|
|
36287
|
+
const rawHeaders = context.request?.headers;
|
|
36288
|
+
let headers = rawHeaders;
|
|
36289
|
+
if (rawHeaders && typeof rawHeaders === "object" && typeof rawHeaders.get !== "function") {
|
|
36290
|
+
try {
|
|
36291
|
+
const h = new Headers();
|
|
36292
|
+
for (const [k, v] of Object.entries(rawHeaders)) {
|
|
36293
|
+
if (v == null) continue;
|
|
36294
|
+
h.set(k, Array.isArray(v) ? v.join(", ") : String(v));
|
|
36295
|
+
}
|
|
36296
|
+
headers = h;
|
|
36297
|
+
} catch {
|
|
36298
|
+
headers = rawHeaders;
|
|
36299
|
+
}
|
|
36300
|
+
}
|
|
36301
|
+
const sessionData = await (authService?.auth?.api?.getSession ?? authService?.api?.getSession)?.call(
|
|
36302
|
+
authService?.auth?.api ?? authService?.api,
|
|
36303
|
+
{ headers }
|
|
36304
|
+
);
|
|
36305
|
+
return sessionData?.user?.id ?? sessionData?.session?.userId;
|
|
36306
|
+
} catch (e) {
|
|
36307
|
+
return void 0;
|
|
36308
|
+
}
|
|
36309
|
+
}
|
|
35726
36310
|
async handleCloud(path, method, body, query, _context) {
|
|
35727
36311
|
const m = method.toUpperCase();
|
|
35728
36312
|
const parts = path.replace(/^\/+/, "").split("/").filter(Boolean);
|
|
@@ -35731,9 +36315,9 @@ var _HttpDispatcher = class _HttpDispatcher {
|
|
|
35731
36315
|
if (!ql) {
|
|
35732
36316
|
return { handled: true, response: this.error("Project service not available (ObjectQL missing)", 503) };
|
|
35733
36317
|
}
|
|
35734
|
-
const ENV = "
|
|
35735
|
-
const CRED = "
|
|
35736
|
-
const MEM = "
|
|
36318
|
+
const ENV = "sys_environment";
|
|
36319
|
+
const CRED = "sys_environment_credential";
|
|
36320
|
+
const MEM = "sys_environment_member";
|
|
35737
36321
|
const PKG_INSTALL = "sys_package_installation";
|
|
35738
36322
|
const PKG = "sys_package";
|
|
35739
36323
|
const PKG_VERSION = "sys_package_version";
|
|
@@ -35781,7 +36365,7 @@ var _HttpDispatcher = class _HttpDispatcher {
|
|
|
35781
36365
|
const pkgRow = await ql.findOne(PKG, { where: { manifest_id: manifestId } });
|
|
35782
36366
|
if (!pkgRow?.id) return null;
|
|
35783
36367
|
return await ql.findOne(PKG_INSTALL, {
|
|
35784
|
-
where: {
|
|
36368
|
+
where: { environment_id: envId, package_id: pkgRow.id }
|
|
35785
36369
|
});
|
|
35786
36370
|
};
|
|
35787
36371
|
const toShortName = (driverId) => {
|
|
@@ -35880,6 +36464,44 @@ var _HttpDispatcher = class _HttpDispatcher {
|
|
|
35880
36464
|
return { handled: true, response: this.success({ templates: [], total: 0 }) };
|
|
35881
36465
|
}
|
|
35882
36466
|
}
|
|
36467
|
+
if (parts.length === 3 && parts[0] === "admin" && parts[1] === "platform-sso" && parts[2] === "backfill" && m === "POST") {
|
|
36468
|
+
const baseSecret = (process.env.OS_AUTH_SECRET ?? process.env.AUTH_SECRET ?? "").trim();
|
|
36469
|
+
if (!baseSecret) {
|
|
36470
|
+
return { handled: true, response: this.error("OS_AUTH_SECRET not configured on this worker", 503) };
|
|
36471
|
+
}
|
|
36472
|
+
const rawHeaders = _context?.request?.headers;
|
|
36473
|
+
let authHeader;
|
|
36474
|
+
if (rawHeaders && typeof rawHeaders.get === "function") {
|
|
36475
|
+
authHeader = rawHeaders.get("authorization") ?? void 0;
|
|
36476
|
+
} else if (rawHeaders && typeof rawHeaders === "object") {
|
|
36477
|
+
authHeader = rawHeaders["authorization"] ?? rawHeaders["Authorization"];
|
|
36478
|
+
}
|
|
36479
|
+
const presented = typeof authHeader === "string" && authHeader.startsWith("Bearer ") ? authHeader.slice(7).trim() : "";
|
|
36480
|
+
if (!presented || presented !== baseSecret) {
|
|
36481
|
+
return { handled: true, response: this.error("forbidden: Bearer token must match OS_AUTH_SECRET", 403) };
|
|
36482
|
+
}
|
|
36483
|
+
try {
|
|
36484
|
+
const { backfillPlatformSsoClients: backfillPlatformSsoClients2 } = await Promise.resolve().then(() => (init_platform_sso(), platform_sso_exports));
|
|
36485
|
+
const result = await backfillPlatformSsoClients2({
|
|
36486
|
+
ql,
|
|
36487
|
+
baseSecret,
|
|
36488
|
+
logger: console
|
|
36489
|
+
});
|
|
36490
|
+
let sample = [];
|
|
36491
|
+
let total = 0;
|
|
36492
|
+
try {
|
|
36493
|
+
const rows = await ql.find("sys_oauth_application", { limit: 5 }, { context: { isSystem: true } });
|
|
36494
|
+
const list = Array.isArray(rows) ? rows : Array.isArray(rows?.records) ? rows.records : [];
|
|
36495
|
+
sample = list;
|
|
36496
|
+
total = typeof rows?.total === "number" ? rows.total : list.length;
|
|
36497
|
+
} catch (e) {
|
|
36498
|
+
sample = [{ _readErr: e?.message ?? String(e) }];
|
|
36499
|
+
}
|
|
36500
|
+
return { handled: true, response: this.success({ ...result, total, sample }) };
|
|
36501
|
+
} catch (err) {
|
|
36502
|
+
return { handled: true, response: this.error(`backfill failed: ${err?.message ?? String(err)}`, 500) };
|
|
36503
|
+
}
|
|
36504
|
+
}
|
|
35883
36505
|
if (parts.length === 1 && parts[0] === "projects" && m === "GET") {
|
|
35884
36506
|
const where = {};
|
|
35885
36507
|
if (query?.organizationId) where.organization_id = query.organizationId;
|
|
@@ -35893,16 +36515,26 @@ var _HttpDispatcher = class _HttpDispatcher {
|
|
|
35893
36515
|
const req = body || {};
|
|
35894
36516
|
if (req.organization_id === "__session__" || req.created_by === "__session__") {
|
|
35895
36517
|
try {
|
|
35896
|
-
const
|
|
35897
|
-
|
|
35898
|
-
|
|
35899
|
-
}
|
|
36518
|
+
const userId = await this.resolveCallerUserId(_context);
|
|
36519
|
+
if (req.created_by === "__session__") {
|
|
36520
|
+
req.created_by = userId ?? "system";
|
|
36521
|
+
}
|
|
35900
36522
|
if (req.organization_id === "__session__") {
|
|
36523
|
+
const authService = await this.resolveService(CoreServiceName.enum.auth);
|
|
36524
|
+
const rawHeaders = _context?.request?.headers;
|
|
36525
|
+
let headers = rawHeaders;
|
|
36526
|
+
if (rawHeaders && typeof rawHeaders === "object" && typeof rawHeaders.get !== "function") {
|
|
36527
|
+
const h = new Headers();
|
|
36528
|
+
for (const [k, v] of Object.entries(rawHeaders)) {
|
|
36529
|
+
if (v == null) continue;
|
|
36530
|
+
h.set(k, Array.isArray(v) ? v.join(", ") : String(v));
|
|
36531
|
+
}
|
|
36532
|
+
headers = h;
|
|
36533
|
+
}
|
|
36534
|
+
const apiObj = authService?.auth?.api ?? authService?.api;
|
|
36535
|
+
const sessionData = await apiObj?.getSession?.call(apiObj, { headers });
|
|
35901
36536
|
req.organization_id = sessionData?.session?.activeOrganizationId ?? void 0;
|
|
35902
36537
|
}
|
|
35903
|
-
if (req.created_by === "__session__") {
|
|
35904
|
-
req.created_by = sessionData?.user?.id ?? "system";
|
|
35905
|
-
}
|
|
35906
36538
|
} catch {
|
|
35907
36539
|
}
|
|
35908
36540
|
}
|
|
@@ -35940,14 +36572,14 @@ var _HttpDispatcher = class _HttpDispatcher {
|
|
|
35940
36572
|
try {
|
|
35941
36573
|
const orgRow = await findOne("sys_organization", { id: req.organization_id });
|
|
35942
36574
|
const orgSlug = orgRow?.slug || req.organization_id;
|
|
35943
|
-
const rootDomain = getEnv("ROOT_DOMAIN", "objectstack.app");
|
|
36575
|
+
const rootDomain = getEnv("OS_ROOT_DOMAIN") ?? getEnv("ROOT_DOMAIN", "objectstack.app");
|
|
35944
36576
|
computedHostname = `${orgSlug}-${shortId}.${rootDomain}`;
|
|
35945
36577
|
} catch {
|
|
35946
36578
|
computedHostname = `${req.organization_id}-${shortId}.objectstack.app`;
|
|
35947
36579
|
}
|
|
35948
36580
|
}
|
|
35949
36581
|
try {
|
|
35950
|
-
const existing = await findOne("
|
|
36582
|
+
const existing = await findOne("sys_environment", {
|
|
35951
36583
|
hostname: computedHostname
|
|
35952
36584
|
});
|
|
35953
36585
|
if (existing && existing.id !== projectId) {
|
|
@@ -35965,6 +36597,40 @@ var _HttpDispatcher = class _HttpDispatcher {
|
|
|
35965
36597
|
const baseMetadata = { ...req.metadata ?? {} };
|
|
35966
36598
|
const simulateFailure = Boolean(baseMetadata.__simulateFailure);
|
|
35967
36599
|
const simulateDelayMs = Number(baseMetadata.__simulateDelayMs ?? 1500);
|
|
36600
|
+
try {
|
|
36601
|
+
let ownerUserId = req.created_by && req.created_by !== "system" ? String(req.created_by) : void 0;
|
|
36602
|
+
if (!ownerUserId) {
|
|
36603
|
+
ownerUserId = await this.resolveCallerUserId(_context);
|
|
36604
|
+
}
|
|
36605
|
+
if (ownerUserId) {
|
|
36606
|
+
const userRow = await ql.find("sys_user", { where: { id: ownerUserId } });
|
|
36607
|
+
const userRows = Array.isArray(userRow) ? userRow : userRow?.value ?? [];
|
|
36608
|
+
const u = Array.isArray(userRows) && userRows.length > 0 ? userRows[0] : null;
|
|
36609
|
+
if (u?.email) {
|
|
36610
|
+
baseMetadata.ownerSeed = {
|
|
36611
|
+
userId: String(ownerUserId),
|
|
36612
|
+
email: String(u.email),
|
|
36613
|
+
name: u.name ? String(u.name) : null,
|
|
36614
|
+
image: u.image ? String(u.image) : null
|
|
36615
|
+
};
|
|
36616
|
+
}
|
|
36617
|
+
}
|
|
36618
|
+
} catch {
|
|
36619
|
+
}
|
|
36620
|
+
try {
|
|
36621
|
+
const orgRow = await ql.find("sys_organization", { where: { id: req.organization_id } });
|
|
36622
|
+
const orgRows = Array.isArray(orgRow) ? orgRow : orgRow?.value ?? [];
|
|
36623
|
+
const org = Array.isArray(orgRows) && orgRows.length > 0 ? orgRows[0] : null;
|
|
36624
|
+
if (org?.id && org?.name) {
|
|
36625
|
+
baseMetadata.orgSeed = {
|
|
36626
|
+
id: String(org.id),
|
|
36627
|
+
name: String(org.name),
|
|
36628
|
+
slug: org.slug ? String(org.slug) : null,
|
|
36629
|
+
logo: org.logo ? String(org.logo) : null
|
|
36630
|
+
};
|
|
36631
|
+
}
|
|
36632
|
+
} catch {
|
|
36633
|
+
}
|
|
35968
36634
|
await ql.insert(ENV, {
|
|
35969
36635
|
id: projectId,
|
|
35970
36636
|
organization_id: req.organization_id,
|
|
@@ -35981,8 +36647,30 @@ var _HttpDispatcher = class _HttpDispatcher {
|
|
|
35981
36647
|
database_driver: driver,
|
|
35982
36648
|
storage_limit_mb: req.storage_limit_mb ?? 1024,
|
|
35983
36649
|
provisioned_at: null,
|
|
35984
|
-
hostname: computedHostname
|
|
36650
|
+
hostname: computedHostname,
|
|
36651
|
+
visibility: (() => {
|
|
36652
|
+
const raw = String(req.visibility ?? "private");
|
|
36653
|
+
return raw === "unlisted" ? "private" : raw;
|
|
36654
|
+
})()
|
|
35985
36655
|
});
|
|
36656
|
+
try {
|
|
36657
|
+
const { seedPlatformSsoClient: seedPlatformSsoClient2 } = await Promise.resolve().then(() => (init_platform_sso(), platform_sso_exports));
|
|
36658
|
+
const baseSecret = (process.env.OS_AUTH_SECRET ?? process.env.AUTH_SECRET ?? "").trim();
|
|
36659
|
+
if (baseSecret) {
|
|
36660
|
+
await seedPlatformSsoClient2({
|
|
36661
|
+
ql,
|
|
36662
|
+
projectId,
|
|
36663
|
+
hostname: computedHostname,
|
|
36664
|
+
baseSecret,
|
|
36665
|
+
logger: console
|
|
36666
|
+
});
|
|
36667
|
+
}
|
|
36668
|
+
} catch (ssoErr) {
|
|
36669
|
+
console.warn?.("[http-dispatcher] platform SSO seed failed (non-fatal)", {
|
|
36670
|
+
projectId,
|
|
36671
|
+
error: ssoErr?.message
|
|
36672
|
+
});
|
|
36673
|
+
}
|
|
35986
36674
|
const runProvisioning = async () => {
|
|
35987
36675
|
try {
|
|
35988
36676
|
if (simulateDelayMs > 0) {
|
|
@@ -36020,7 +36708,7 @@ var _HttpDispatcher = class _HttpDispatcher {
|
|
|
36020
36708
|
);
|
|
36021
36709
|
await ql.insert(CRED, {
|
|
36022
36710
|
id: credentialId,
|
|
36023
|
-
|
|
36711
|
+
environment_id: projectId,
|
|
36024
36712
|
secret_ciphertext: plaintextSecret,
|
|
36025
36713
|
encryption_key_id: "noop",
|
|
36026
36714
|
authorization: "full_access",
|
|
@@ -36135,8 +36823,9 @@ var _HttpDispatcher = class _HttpDispatcher {
|
|
|
36135
36823
|
if (m === "GET") {
|
|
36136
36824
|
const envRow = await findOne(ENV, { id });
|
|
36137
36825
|
if (!envRow) return { handled: true, response: this.error(`Project '${id}' not found`, 404) };
|
|
36138
|
-
const credRow = await findOne(CRED, {
|
|
36139
|
-
const
|
|
36826
|
+
const credRow = await findOne(CRED, { environment_id: id, status: "active" });
|
|
36827
|
+
const callerUserId = await this.resolveCallerUserId(_context);
|
|
36828
|
+
const membership = callerUserId ? await findOne(MEM, { environment_id: id, user_id: callerUserId }) : await findOne(MEM, { environment_id: id });
|
|
36140
36829
|
const credMeta = credRow ? {
|
|
36141
36830
|
id: credRow.id,
|
|
36142
36831
|
status: credRow.status,
|
|
@@ -36163,6 +36852,14 @@ var _HttpDispatcher = class _HttpDispatcher {
|
|
|
36163
36852
|
if (body?.plan !== void 0) patch.plan = body.plan;
|
|
36164
36853
|
if (body?.status !== void 0) patch.status = body.status;
|
|
36165
36854
|
if (body?.is_default !== void 0) patch.is_default = body.is_default;
|
|
36855
|
+
if (body?.visibility !== void 0) {
|
|
36856
|
+
let v = String(body.visibility);
|
|
36857
|
+
if (v === "unlisted") v = "private";
|
|
36858
|
+
if (!["private", "public"].includes(v)) {
|
|
36859
|
+
return { handled: true, response: this.error(`Invalid visibility '${v}' (expected private | public)`, 400) };
|
|
36860
|
+
}
|
|
36861
|
+
patch.visibility = v;
|
|
36862
|
+
}
|
|
36166
36863
|
if (body?.metadata !== void 0) patch.metadata = JSON.stringify(body.metadata);
|
|
36167
36864
|
patch.updated_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
36168
36865
|
await ql.update(ENV, patch, { where: { id } });
|
|
@@ -36369,11 +37066,11 @@ var _HttpDispatcher = class _HttpDispatcher {
|
|
|
36369
37066
|
},
|
|
36370
37067
|
{ where: { id } }
|
|
36371
37068
|
);
|
|
36372
|
-
const existingCred = await findOne(CRED, {
|
|
37069
|
+
const existingCred = await findOne(CRED, { environment_id: id, status: "active" });
|
|
36373
37070
|
if (!existingCred) {
|
|
36374
37071
|
await ql.insert(CRED, {
|
|
36375
37072
|
id: randomUUID(),
|
|
36376
|
-
|
|
37073
|
+
environment_id: id,
|
|
36377
37074
|
secret_ciphertext: retrySecret,
|
|
36378
37075
|
encryption_key_id: "noop",
|
|
36379
37076
|
authorization: "full_access",
|
|
@@ -36420,7 +37117,7 @@ var _HttpDispatcher = class _HttpDispatcher {
|
|
|
36420
37117
|
const envRow = await findOne(ENV, { id });
|
|
36421
37118
|
if (!envRow) return { handled: true, response: this.error(`Project '${id}' not found`, 404) };
|
|
36422
37119
|
const nowIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
36423
|
-
let existing = await ql.find(CRED, { where: {
|
|
37120
|
+
let existing = await ql.find(CRED, { where: { environment_id: id, status: "active" } });
|
|
36424
37121
|
if (existing && existing.value) existing = existing.value;
|
|
36425
37122
|
for (const row of Array.isArray(existing) ? existing : []) {
|
|
36426
37123
|
await ql.update(CRED, {
|
|
@@ -36432,7 +37129,7 @@ var _HttpDispatcher = class _HttpDispatcher {
|
|
|
36432
37129
|
const credentialId = randomUUID();
|
|
36433
37130
|
await ql.insert(CRED, {
|
|
36434
37131
|
id: credentialId,
|
|
36435
|
-
|
|
37132
|
+
environment_id: id,
|
|
36436
37133
|
secret_ciphertext: plaintext,
|
|
36437
37134
|
encryption_key_id: "noop",
|
|
36438
37135
|
authorization: "full_access",
|
|
@@ -36451,14 +37148,154 @@ var _HttpDispatcher = class _HttpDispatcher {
|
|
|
36451
37148
|
}
|
|
36452
37149
|
if (parts.length === 3 && parts[0] === "projects" && parts[2] === "members" && m === "GET") {
|
|
36453
37150
|
const id = decodeURIComponent(parts[1]);
|
|
36454
|
-
let rows = await ql.find(MEM, { where: {
|
|
37151
|
+
let rows = await ql.find(MEM, { where: { environment_id: id } });
|
|
36455
37152
|
if (rows && rows.value) rows = rows.value;
|
|
36456
37153
|
const members = Array.isArray(rows) ? rows : [];
|
|
36457
|
-
|
|
37154
|
+
const userIds = Array.from(new Set(members.map((mem) => mem.user_id).filter(Boolean)));
|
|
37155
|
+
const userMap = /* @__PURE__ */ new Map();
|
|
37156
|
+
for (const uid of userIds) {
|
|
37157
|
+
let row = null;
|
|
37158
|
+
for (const tableName of ["sys_user", "user"]) {
|
|
37159
|
+
try {
|
|
37160
|
+
const u = await ql.findOne(tableName, { where: { id: uid } });
|
|
37161
|
+
row = u?.value ?? u;
|
|
37162
|
+
if (row) break;
|
|
37163
|
+
} catch {
|
|
37164
|
+
}
|
|
37165
|
+
}
|
|
37166
|
+
if (row) userMap.set(String(uid), {
|
|
37167
|
+
id: row.id,
|
|
37168
|
+
name: row.name ?? row.display_name,
|
|
37169
|
+
email: row.email,
|
|
37170
|
+
image: row.image ?? row.avatar_url
|
|
37171
|
+
});
|
|
37172
|
+
}
|
|
37173
|
+
const enriched = members.map((mem) => ({
|
|
37174
|
+
...mem,
|
|
37175
|
+
user: userMap.get(String(mem.user_id)) ?? void 0
|
|
37176
|
+
}));
|
|
37177
|
+
return { handled: true, response: this.success({ members: enriched }) };
|
|
37178
|
+
}
|
|
37179
|
+
if (parts.length === 3 && parts[0] === "projects" && parts[2] === "members" && m === "POST") {
|
|
37180
|
+
const id = decodeURIComponent(parts[1]);
|
|
37181
|
+
const project = await findOne(ENV, { id });
|
|
37182
|
+
if (!project) return { handled: true, response: this.error(`Project '${id}' not found`, 404) };
|
|
37183
|
+
const callerId = await this.resolveCallerUserId(_context);
|
|
37184
|
+
if (!callerId) return { handled: true, response: this.error("Authentication required", 401) };
|
|
37185
|
+
const callerMem = await findOne(MEM, { environment_id: id, user_id: callerId });
|
|
37186
|
+
if (!callerMem || !["owner", "admin"].includes(String(callerMem.role))) {
|
|
37187
|
+
return { handled: true, response: this.error("Forbidden \u2014 owner or admin required", 403) };
|
|
37188
|
+
}
|
|
37189
|
+
const email = typeof body?.email === "string" ? String(body.email).trim().toLowerCase() : null;
|
|
37190
|
+
let inviteUserId = typeof body?.user_id === "string" ? String(body.user_id).trim() : null;
|
|
37191
|
+
let role = String(body?.role ?? "member").trim().toLowerCase();
|
|
37192
|
+
if (!["owner", "admin", "member", "viewer"].includes(role)) {
|
|
37193
|
+
return { handled: true, response: this.error(`Invalid role '${role}' (expected owner | admin | member | viewer)`, 400) };
|
|
37194
|
+
}
|
|
37195
|
+
if (!email && !inviteUserId) {
|
|
37196
|
+
return { handled: true, response: this.error("email or user_id is required", 400) };
|
|
37197
|
+
}
|
|
37198
|
+
if (!inviteUserId && email) {
|
|
37199
|
+
let row = null;
|
|
37200
|
+
for (const tableName of ["sys_user", "user"]) {
|
|
37201
|
+
try {
|
|
37202
|
+
const u = await ql.findOne(tableName, { where: { email } });
|
|
37203
|
+
row = u?.value ?? u;
|
|
37204
|
+
if (row) break;
|
|
37205
|
+
} catch {
|
|
37206
|
+
}
|
|
37207
|
+
}
|
|
37208
|
+
if (!row?.id) {
|
|
37209
|
+
return { handled: true, response: this.error(`No user found with email '${email}'`, 404) };
|
|
37210
|
+
}
|
|
37211
|
+
inviteUserId = String(row.id);
|
|
37212
|
+
}
|
|
37213
|
+
const existing = await findOne(MEM, { environment_id: id, user_id: inviteUserId });
|
|
37214
|
+
if (existing) {
|
|
37215
|
+
return { handled: true, response: this.success({ member: existing, alreadyMember: true }) };
|
|
37216
|
+
}
|
|
37217
|
+
try {
|
|
37218
|
+
const memberId = randomUUID();
|
|
37219
|
+
await ql.insert(MEM, {
|
|
37220
|
+
id: memberId,
|
|
37221
|
+
environment_id: id,
|
|
37222
|
+
user_id: inviteUserId,
|
|
37223
|
+
role,
|
|
37224
|
+
invited_by: callerId,
|
|
37225
|
+
organization_id: project.organization_id ?? null
|
|
37226
|
+
});
|
|
37227
|
+
const created = await findOne(MEM, { id: memberId });
|
|
37228
|
+
return { handled: true, response: this.success({ member: created, alreadyMember: false }) };
|
|
37229
|
+
} catch (e) {
|
|
37230
|
+
return { handled: true, response: this.error(e?.message ?? "Failed to add member", 500) };
|
|
37231
|
+
}
|
|
37232
|
+
}
|
|
37233
|
+
if (parts.length === 4 && parts[0] === "projects" && parts[2] === "members" && m === "PATCH") {
|
|
37234
|
+
const id = decodeURIComponent(parts[1]);
|
|
37235
|
+
const memberId = decodeURIComponent(parts[3]);
|
|
37236
|
+
const project = await findOne(ENV, { id });
|
|
37237
|
+
if (!project) return { handled: true, response: this.error(`Project '${id}' not found`, 404) };
|
|
37238
|
+
const callerId = await this.resolveCallerUserId(_context);
|
|
37239
|
+
if (!callerId) return { handled: true, response: this.error("Authentication required", 401) };
|
|
37240
|
+
const callerMem = await findOne(MEM, { environment_id: id, user_id: callerId });
|
|
37241
|
+
if (!callerMem || !["owner", "admin"].includes(String(callerMem.role))) {
|
|
37242
|
+
return { handled: true, response: this.error("Forbidden \u2014 owner or admin required", 403) };
|
|
37243
|
+
}
|
|
37244
|
+
const target = await findOne(MEM, { id: memberId, environment_id: id });
|
|
37245
|
+
if (!target) return { handled: true, response: this.error(`Member '${memberId}' not found`, 404) };
|
|
37246
|
+
const newRole = String(body?.role ?? "").trim().toLowerCase();
|
|
37247
|
+
if (!["owner", "admin", "member", "viewer"].includes(newRole)) {
|
|
37248
|
+
return { handled: true, response: this.error(`Invalid role '${newRole}'`, 400) };
|
|
37249
|
+
}
|
|
37250
|
+
if (target.role === "owner" && newRole !== "owner") {
|
|
37251
|
+
let owners = await ql.find(MEM, { where: { environment_id: id, role: "owner" } });
|
|
37252
|
+
if (owners && owners.value) owners = owners.value;
|
|
37253
|
+
const ownerCount = Array.isArray(owners) ? owners.length : 0;
|
|
37254
|
+
if (ownerCount <= 1) {
|
|
37255
|
+
return { handled: true, response: this.error("Cannot demote the last owner", 409) };
|
|
37256
|
+
}
|
|
37257
|
+
}
|
|
37258
|
+
try {
|
|
37259
|
+
await ql.update(MEM, { role: newRole, updated_at: (/* @__PURE__ */ new Date()).toISOString() }, { where: { id: memberId } });
|
|
37260
|
+
const updated = await findOne(MEM, { id: memberId });
|
|
37261
|
+
return { handled: true, response: this.success({ member: updated }) };
|
|
37262
|
+
} catch (e) {
|
|
37263
|
+
return { handled: true, response: this.error(e?.message ?? "Failed to update role", 500) };
|
|
37264
|
+
}
|
|
37265
|
+
}
|
|
37266
|
+
if (parts.length === 4 && parts[0] === "projects" && parts[2] === "members" && m === "DELETE") {
|
|
37267
|
+
const id = decodeURIComponent(parts[1]);
|
|
37268
|
+
const memberId = decodeURIComponent(parts[3]);
|
|
37269
|
+
const project = await findOne(ENV, { id });
|
|
37270
|
+
if (!project) return { handled: true, response: this.error(`Project '${id}' not found`, 404) };
|
|
37271
|
+
const callerId = await this.resolveCallerUserId(_context);
|
|
37272
|
+
if (!callerId) return { handled: true, response: this.error("Authentication required", 401) };
|
|
37273
|
+
const target = await findOne(MEM, { id: memberId, environment_id: id });
|
|
37274
|
+
if (!target) return { handled: true, response: this.error(`Member '${memberId}' not found`, 404) };
|
|
37275
|
+
const callerMem = await findOne(MEM, { environment_id: id, user_id: callerId });
|
|
37276
|
+
const isSelf = String(target.user_id) === String(callerId);
|
|
37277
|
+
const isPrivileged = callerMem && ["owner", "admin"].includes(String(callerMem.role));
|
|
37278
|
+
if (!isSelf && !isPrivileged) {
|
|
37279
|
+
return { handled: true, response: this.error("Forbidden \u2014 owner or admin required", 403) };
|
|
37280
|
+
}
|
|
37281
|
+
if (target.role === "owner") {
|
|
37282
|
+
let owners = await ql.find(MEM, { where: { environment_id: id, role: "owner" } });
|
|
37283
|
+
if (owners && owners.value) owners = owners.value;
|
|
37284
|
+
const ownerCount = Array.isArray(owners) ? owners.length : 0;
|
|
37285
|
+
if (ownerCount <= 1) {
|
|
37286
|
+
return { handled: true, response: this.error("Cannot remove the last owner", 409) };
|
|
37287
|
+
}
|
|
37288
|
+
}
|
|
37289
|
+
try {
|
|
37290
|
+
await ql.delete(MEM, { where: { id: memberId } });
|
|
37291
|
+
return { handled: true, response: this.success({ removed: true, memberId }) };
|
|
37292
|
+
} catch (e) {
|
|
37293
|
+
return { handled: true, response: this.error(e?.message ?? "Failed to remove member", 500) };
|
|
37294
|
+
}
|
|
36458
37295
|
}
|
|
36459
37296
|
if (parts.length === 3 && parts[0] === "projects" && parts[2] === "packages" && m === "GET") {
|
|
36460
37297
|
const envId = decodeURIComponent(parts[1]);
|
|
36461
|
-
let rows = await ql.find(PKG_INSTALL, { where: {
|
|
37298
|
+
let rows = await ql.find(PKG_INSTALL, { where: { environment_id: envId } });
|
|
36462
37299
|
if (rows && rows.value) rows = rows.value;
|
|
36463
37300
|
const installs = Array.isArray(rows) ? rows : [];
|
|
36464
37301
|
const packages = await Promise.all(
|
|
@@ -36519,7 +37356,7 @@ var _HttpDispatcher = class _HttpDispatcher {
|
|
|
36519
37356
|
}
|
|
36520
37357
|
const resolvedVersion = version ?? manifest?.version ?? "1.0.0";
|
|
36521
37358
|
const dup = await ql.findOne(PKG_INSTALL, {
|
|
36522
|
-
where: {
|
|
37359
|
+
where: { environment_id: envId, package_id: packageId }
|
|
36523
37360
|
});
|
|
36524
37361
|
if (dup?.id) {
|
|
36525
37362
|
return { handled: true, response: this.error(`Package '${packageId}' is already installed in this project`, 409) };
|
|
@@ -36530,7 +37367,7 @@ var _HttpDispatcher = class _HttpDispatcher {
|
|
|
36530
37367
|
const recordId = randomUUID();
|
|
36531
37368
|
await ql.insert(PKG_INSTALL, {
|
|
36532
37369
|
id: recordId,
|
|
36533
|
-
|
|
37370
|
+
environment_id: envId,
|
|
36534
37371
|
package_id: sysPackageId,
|
|
36535
37372
|
package_version_id: sysPackageVersionId,
|
|
36536
37373
|
status: "installed",
|
|
@@ -36550,7 +37387,7 @@ var _HttpDispatcher = class _HttpDispatcher {
|
|
|
36550
37387
|
if (parts.length === 4 && parts[0] === "projects" && parts[2] === "packages" && m === "GET") {
|
|
36551
37388
|
const envId = decodeURIComponent(parts[1]);
|
|
36552
37389
|
const pkgId = decodeURIComponent(parts[3]);
|
|
36553
|
-
const record = await ql.findOne(PKG_INSTALL, { where: {
|
|
37390
|
+
const record = await ql.findOne(PKG_INSTALL, { where: { environment_id: envId, package_id: pkgId } });
|
|
36554
37391
|
if (!record) return { handled: true, response: this.error(`Package '${pkgId}' is not installed in this project`, 404) };
|
|
36555
37392
|
return { handled: true, response: this.success({ package: record }) };
|
|
36556
37393
|
}
|
|
@@ -36657,7 +37494,7 @@ var _HttpDispatcher = class _HttpDispatcher {
|
|
|
36657
37494
|
*/
|
|
36658
37495
|
async deleteProjectCascade(projectId, deps) {
|
|
36659
37496
|
const { ql, findOne, getRealAdapter, force } = deps;
|
|
36660
|
-
const ENV = "
|
|
37497
|
+
const ENV = "sys_environment";
|
|
36661
37498
|
const warnings = [];
|
|
36662
37499
|
const row = await findOne(ENV, { id: projectId });
|
|
36663
37500
|
if (!row) {
|
|
@@ -36675,9 +37512,9 @@ var _HttpDispatcher = class _HttpDispatcher {
|
|
|
36675
37512
|
};
|
|
36676
37513
|
}
|
|
36677
37514
|
const cascade = [
|
|
36678
|
-
{ object: "
|
|
36679
|
-
{ object: "
|
|
36680
|
-
{ object: "sys_package_installation", field: "
|
|
37515
|
+
{ object: "sys_environment_credential", field: "environment_id" },
|
|
37516
|
+
{ object: "sys_environment_member", field: "environment_id" },
|
|
37517
|
+
{ object: "sys_package_installation", field: "environment_id" }
|
|
36681
37518
|
];
|
|
36682
37519
|
for (const { object, field } of cascade) {
|
|
36683
37520
|
try {
|
|
@@ -37353,8 +38190,316 @@ _HttpDispatcher.SYSTEM_PROJECT_ID = "00000000-0000-0000-0000-000000000001";
|
|
|
37353
38190
|
_HttpDispatcher.PLATFORM_ORG_ID = "00000000-0000-0000-0000-000000000000";
|
|
37354
38191
|
var HttpDispatcher = _HttpDispatcher;
|
|
37355
38192
|
|
|
38193
|
+
// src/security/security-headers.ts
|
|
38194
|
+
function buildSecurityHeaders(opts = {}) {
|
|
38195
|
+
const h = {};
|
|
38196
|
+
if (opts.contentSecurityPolicy !== false) {
|
|
38197
|
+
h["Content-Security-Policy"] = opts.contentSecurityPolicy ?? "default-src 'none'; frame-ancestors 'none'";
|
|
38198
|
+
}
|
|
38199
|
+
if (opts.hsts) {
|
|
38200
|
+
const cfg = typeof opts.hsts === "object" ? opts.hsts : {};
|
|
38201
|
+
const maxAge = cfg.maxAge ?? 15552e3;
|
|
38202
|
+
const parts = [`max-age=${maxAge}`];
|
|
38203
|
+
if (cfg.includeSubDomains ?? true) parts.push("includeSubDomains");
|
|
38204
|
+
if (cfg.preload) parts.push("preload");
|
|
38205
|
+
h["Strict-Transport-Security"] = parts.join("; ");
|
|
38206
|
+
}
|
|
38207
|
+
h["X-Content-Type-Options"] = "nosniff";
|
|
38208
|
+
if (opts.frameOptions !== false) {
|
|
38209
|
+
h["X-Frame-Options"] = opts.frameOptions ?? "DENY";
|
|
38210
|
+
}
|
|
38211
|
+
if (opts.referrerPolicy !== false) {
|
|
38212
|
+
h["Referrer-Policy"] = opts.referrerPolicy ?? "no-referrer";
|
|
38213
|
+
}
|
|
38214
|
+
if (opts.permissionsPolicy !== false) {
|
|
38215
|
+
h["Permissions-Policy"] = opts.permissionsPolicy ?? "geolocation=(), camera=(), microphone=(), payment=()";
|
|
38216
|
+
}
|
|
38217
|
+
if (opts.corp !== false) {
|
|
38218
|
+
h["Cross-Origin-Resource-Policy"] = opts.corp ?? "same-origin";
|
|
38219
|
+
}
|
|
38220
|
+
if (opts.extra) {
|
|
38221
|
+
Object.assign(h, opts.extra);
|
|
38222
|
+
}
|
|
38223
|
+
return h;
|
|
38224
|
+
}
|
|
38225
|
+
|
|
38226
|
+
// src/security/rate-limit.ts
|
|
38227
|
+
var MemoryStore = class {
|
|
38228
|
+
constructor(maxEntries = 1e5) {
|
|
38229
|
+
this.buckets = /* @__PURE__ */ new Map();
|
|
38230
|
+
this.maxEntries = maxEntries;
|
|
38231
|
+
}
|
|
38232
|
+
get(key) {
|
|
38233
|
+
return this.buckets.get(key);
|
|
38234
|
+
}
|
|
38235
|
+
set(key, state) {
|
|
38236
|
+
if (this.buckets.size >= this.maxEntries) {
|
|
38237
|
+
const dropCount = Math.max(1, Math.floor(this.maxEntries / 10));
|
|
38238
|
+
const iter = this.buckets.keys();
|
|
38239
|
+
for (let i = 0; i < dropCount; i++) {
|
|
38240
|
+
const k = iter.next().value;
|
|
38241
|
+
if (!k) break;
|
|
38242
|
+
this.buckets.delete(k);
|
|
38243
|
+
}
|
|
38244
|
+
}
|
|
38245
|
+
this.buckets.set(key, state);
|
|
38246
|
+
}
|
|
38247
|
+
prune(olderThanMs) {
|
|
38248
|
+
const cutoff = Date.now() - olderThanMs;
|
|
38249
|
+
for (const [k, v] of this.buckets) {
|
|
38250
|
+
if (v.lastRefill < cutoff) this.buckets.delete(k);
|
|
38251
|
+
}
|
|
38252
|
+
}
|
|
38253
|
+
};
|
|
38254
|
+
var RateLimiter = class {
|
|
38255
|
+
constructor(config, opts = {}) {
|
|
38256
|
+
if (config.capacity <= 0) throw new Error("RateLimiter: capacity must be > 0");
|
|
38257
|
+
if (config.refillPerSec <= 0) throw new Error("RateLimiter: refillPerSec must be > 0");
|
|
38258
|
+
this.config = config;
|
|
38259
|
+
this.store = opts.store ?? new MemoryStore();
|
|
38260
|
+
this.now = opts.now ?? Date.now;
|
|
38261
|
+
}
|
|
38262
|
+
/**
|
|
38263
|
+
* Attempt to consume `cost` tokens for `key`. Returns a decision
|
|
38264
|
+
* describing whether the request should proceed and, if not, how
|
|
38265
|
+
* long the caller should wait before retrying.
|
|
38266
|
+
*/
|
|
38267
|
+
consume(key, cost = this.config.defaultCost ?? 1) {
|
|
38268
|
+
const now = this.now();
|
|
38269
|
+
const { capacity, refillPerSec } = this.config;
|
|
38270
|
+
let state = this.store.get(key);
|
|
38271
|
+
if (!state) {
|
|
38272
|
+
state = { tokens: capacity, lastRefill: now };
|
|
38273
|
+
} else {
|
|
38274
|
+
const elapsedSec = (now - state.lastRefill) / 1e3;
|
|
38275
|
+
if (elapsedSec > 0) {
|
|
38276
|
+
state = {
|
|
38277
|
+
tokens: Math.min(capacity, state.tokens + elapsedSec * refillPerSec),
|
|
38278
|
+
lastRefill: now
|
|
38279
|
+
};
|
|
38280
|
+
}
|
|
38281
|
+
}
|
|
38282
|
+
if (state.tokens >= cost) {
|
|
38283
|
+
state.tokens -= cost;
|
|
38284
|
+
this.store.set(key, state);
|
|
38285
|
+
return {
|
|
38286
|
+
allowed: true,
|
|
38287
|
+
remaining: Math.floor(state.tokens),
|
|
38288
|
+
retryAfterMs: 0,
|
|
38289
|
+
resetAt: now + Math.ceil((capacity - state.tokens) / refillPerSec * 1e3)
|
|
38290
|
+
};
|
|
38291
|
+
}
|
|
38292
|
+
const tokensNeeded = cost - state.tokens;
|
|
38293
|
+
const retryAfterMs = Math.ceil(tokensNeeded / refillPerSec * 1e3);
|
|
38294
|
+
this.store.set(key, state);
|
|
38295
|
+
return {
|
|
38296
|
+
allowed: false,
|
|
38297
|
+
remaining: Math.floor(state.tokens),
|
|
38298
|
+
retryAfterMs,
|
|
38299
|
+
resetAt: now + retryAfterMs
|
|
38300
|
+
};
|
|
38301
|
+
}
|
|
38302
|
+
/** Force-reset a key (e.g. after a successful auth flow). */
|
|
38303
|
+
reset(key) {
|
|
38304
|
+
this.store.set(key, { tokens: this.config.capacity, lastRefill: this.now() });
|
|
38305
|
+
}
|
|
38306
|
+
};
|
|
38307
|
+
var DEFAULT_RATE_LIMITS = {
|
|
38308
|
+
auth: { capacity: 10, refillPerSec: 10 / 60 },
|
|
38309
|
+
write: { capacity: 60, refillPerSec: 60 / 60 },
|
|
38310
|
+
read: { capacity: 600, refillPerSec: 600 / 60 }
|
|
38311
|
+
};
|
|
38312
|
+
|
|
38313
|
+
// src/observability/request-context.ts
|
|
38314
|
+
var MAX_REQUEST_ID_LENGTH = 200;
|
|
38315
|
+
var REQUEST_ID_PATTERN = /^[A-Za-z0-9._:-]+$/;
|
|
38316
|
+
function extractRequestId(headers) {
|
|
38317
|
+
if (!headers || typeof headers !== "object") return void 0;
|
|
38318
|
+
for (const [k, v] of Object.entries(headers)) {
|
|
38319
|
+
if (k.toLowerCase() !== "x-request-id") continue;
|
|
38320
|
+
const raw = Array.isArray(v) ? v[0] : v;
|
|
38321
|
+
if (typeof raw !== "string") return void 0;
|
|
38322
|
+
const trimmed = raw.trim();
|
|
38323
|
+
if (!trimmed || trimmed.length > MAX_REQUEST_ID_LENGTH) return void 0;
|
|
38324
|
+
if (!REQUEST_ID_PATTERN.test(trimmed)) return void 0;
|
|
38325
|
+
return trimmed;
|
|
38326
|
+
}
|
|
38327
|
+
return void 0;
|
|
38328
|
+
}
|
|
38329
|
+
function generateRequestId() {
|
|
38330
|
+
const g = globalThis.crypto;
|
|
38331
|
+
if (g && typeof g.randomUUID === "function") {
|
|
38332
|
+
return `req_${g.randomUUID().replace(/-/g, "")}`;
|
|
38333
|
+
}
|
|
38334
|
+
const t = Date.now().toString(36);
|
|
38335
|
+
const r = Math.random().toString(36).slice(2, 12);
|
|
38336
|
+
return `req_${t}${r}`;
|
|
38337
|
+
}
|
|
38338
|
+
function resolveRequestId(headers, generate = generateRequestId) {
|
|
38339
|
+
return extractRequestId(headers) ?? generate();
|
|
38340
|
+
}
|
|
38341
|
+
var TRACEPARENT_PATTERN = /^([0-9a-f]{2})-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$/;
|
|
38342
|
+
function parseTraceparent(value) {
|
|
38343
|
+
if (typeof value !== "string") return void 0;
|
|
38344
|
+
const m = TRACEPARENT_PATTERN.exec(value.trim().toLowerCase());
|
|
38345
|
+
if (!m) return void 0;
|
|
38346
|
+
const [, version, traceId, spanId, flags] = m;
|
|
38347
|
+
if (version !== "00") return void 0;
|
|
38348
|
+
if (/^0+$/.test(traceId) || /^0+$/.test(spanId)) return void 0;
|
|
38349
|
+
const sampled = (parseInt(flags, 16) & 1) === 1;
|
|
38350
|
+
return { traceId, spanId, sampled };
|
|
38351
|
+
}
|
|
38352
|
+
function formatTraceparent(ctx) {
|
|
38353
|
+
const flag = ctx.sampled ? "01" : "00";
|
|
38354
|
+
return `00-${ctx.traceId}-${ctx.spanId}-${flag}`;
|
|
38355
|
+
}
|
|
38356
|
+
|
|
38357
|
+
// src/observability/metrics.ts
|
|
38358
|
+
var NoopMetricsRegistry = class {
|
|
38359
|
+
counter() {
|
|
38360
|
+
}
|
|
38361
|
+
histogram() {
|
|
38362
|
+
}
|
|
38363
|
+
gauge() {
|
|
38364
|
+
}
|
|
38365
|
+
};
|
|
38366
|
+
var InMemoryMetricsRegistry = class {
|
|
38367
|
+
constructor() {
|
|
38368
|
+
this.samples = [];
|
|
38369
|
+
}
|
|
38370
|
+
counter(name, labels = {}, value = 1) {
|
|
38371
|
+
this.samples.push({ name, kind: "counter", value, labels, at: Date.now() });
|
|
38372
|
+
}
|
|
38373
|
+
histogram(name, value, labels = {}) {
|
|
38374
|
+
this.samples.push({ name, kind: "histogram", value, labels, at: Date.now() });
|
|
38375
|
+
}
|
|
38376
|
+
gauge(name, value, labels = {}) {
|
|
38377
|
+
this.samples.push({ name, kind: "gauge", value, labels, at: Date.now() });
|
|
38378
|
+
}
|
|
38379
|
+
/**
|
|
38380
|
+
* Sum of all counter increments matching `name` (and optionally a
|
|
38381
|
+
* label subset). Useful in tests: `metrics.totalCounter('http_requests_total', { status: '500' })`.
|
|
38382
|
+
*/
|
|
38383
|
+
totalCounter(name, labelMatch = {}) {
|
|
38384
|
+
return this.samples.filter(
|
|
38385
|
+
(s) => s.kind === "counter" && s.name === name && matchesLabels(s.labels, labelMatch)
|
|
38386
|
+
).reduce((acc, s) => acc + s.value, 0);
|
|
38387
|
+
}
|
|
38388
|
+
/**
|
|
38389
|
+
* All histogram observations matching `name` (and optionally a
|
|
38390
|
+
* label subset), as raw values.
|
|
38391
|
+
*/
|
|
38392
|
+
histogramValues(name, labelMatch = {}) {
|
|
38393
|
+
return this.samples.filter(
|
|
38394
|
+
(s) => s.kind === "histogram" && s.name === name && matchesLabels(s.labels, labelMatch)
|
|
38395
|
+
).map((s) => s.value);
|
|
38396
|
+
}
|
|
38397
|
+
/** Clear all recorded samples. */
|
|
38398
|
+
reset() {
|
|
38399
|
+
this.samples.length = 0;
|
|
38400
|
+
}
|
|
38401
|
+
};
|
|
38402
|
+
function matchesLabels(actual, expected) {
|
|
38403
|
+
for (const [k, v] of Object.entries(expected)) {
|
|
38404
|
+
if (actual[k] !== v) return false;
|
|
38405
|
+
}
|
|
38406
|
+
return true;
|
|
38407
|
+
}
|
|
38408
|
+
var RUNTIME_METRICS = {
|
|
38409
|
+
/** Counter, labels: method, route, status. */
|
|
38410
|
+
httpRequestsTotal: "http_requests_total",
|
|
38411
|
+
/** Histogram (ms), labels: method, route. */
|
|
38412
|
+
httpRequestDurationMs: "http_request_duration_ms",
|
|
38413
|
+
/** Counter, labels: method, route. Incremented when an in-flight handler throws (after the response is sent). */
|
|
38414
|
+
httpRequestErrorsTotal: "http_request_errors_total"
|
|
38415
|
+
};
|
|
38416
|
+
|
|
38417
|
+
// src/observability/error-reporter.ts
|
|
38418
|
+
var NoopErrorReporter = class {
|
|
38419
|
+
captureException() {
|
|
38420
|
+
}
|
|
38421
|
+
};
|
|
38422
|
+
var InMemoryErrorReporter = class {
|
|
38423
|
+
constructor() {
|
|
38424
|
+
this.captured = [];
|
|
38425
|
+
}
|
|
38426
|
+
captureException(error2, context = {}) {
|
|
38427
|
+
this.captured.push({ error: error2, context, at: Date.now() });
|
|
38428
|
+
}
|
|
38429
|
+
reset() {
|
|
38430
|
+
this.captured.length = 0;
|
|
38431
|
+
}
|
|
38432
|
+
};
|
|
38433
|
+
|
|
38434
|
+
// src/observability/instrument.ts
|
|
38435
|
+
function instrumentRouteHandler(method, route, handler, opts = {}) {
|
|
38436
|
+
const metrics = opts.metrics ?? new NoopMetricsRegistry();
|
|
38437
|
+
const errorReporter = opts.errorReporter ?? new NoopErrorReporter();
|
|
38438
|
+
const generateRequestId2 = opts.generateRequestId;
|
|
38439
|
+
const requestIdHeader = opts.requestIdHeader ?? "X-Request-Id";
|
|
38440
|
+
const now = opts.now ?? Date.now;
|
|
38441
|
+
return async (req, res) => {
|
|
38442
|
+
const requestId = resolveRequestId(req?.headers, generateRequestId2);
|
|
38443
|
+
try {
|
|
38444
|
+
req.requestId = requestId;
|
|
38445
|
+
} catch {
|
|
38446
|
+
}
|
|
38447
|
+
if (typeof res?.header === "function") {
|
|
38448
|
+
try {
|
|
38449
|
+
res.header(requestIdHeader, requestId);
|
|
38450
|
+
} catch {
|
|
38451
|
+
}
|
|
38452
|
+
}
|
|
38453
|
+
let status = 200;
|
|
38454
|
+
const origStatus = typeof res?.status === "function" ? res.status.bind(res) : void 0;
|
|
38455
|
+
if (origStatus) {
|
|
38456
|
+
res.status = (code) => {
|
|
38457
|
+
status = code;
|
|
38458
|
+
return origStatus(code);
|
|
38459
|
+
};
|
|
38460
|
+
}
|
|
38461
|
+
const startedAt = now();
|
|
38462
|
+
let threw = false;
|
|
38463
|
+
try {
|
|
38464
|
+
await handler(req, res);
|
|
38465
|
+
} catch (err) {
|
|
38466
|
+
threw = true;
|
|
38467
|
+
status = err?.statusCode ?? 500;
|
|
38468
|
+
metrics.counter(RUNTIME_METRICS.httpRequestErrorsTotal, { method, route });
|
|
38469
|
+
if (status >= 500) {
|
|
38470
|
+
safeReport(errorReporter, err, { requestId, method, route });
|
|
38471
|
+
}
|
|
38472
|
+
throw err;
|
|
38473
|
+
} finally {
|
|
38474
|
+
const elapsed = now() - startedAt;
|
|
38475
|
+
metrics.counter(RUNTIME_METRICS.httpRequestsTotal, {
|
|
38476
|
+
method,
|
|
38477
|
+
route,
|
|
38478
|
+
status: String(status)
|
|
38479
|
+
});
|
|
38480
|
+
metrics.histogram(
|
|
38481
|
+
RUNTIME_METRICS.httpRequestDurationMs,
|
|
38482
|
+
elapsed,
|
|
38483
|
+
{ method, route }
|
|
38484
|
+
);
|
|
38485
|
+
if (!threw && status >= 500) {
|
|
38486
|
+
const recorded = res?.__obsRecordedError;
|
|
38487
|
+
if (recorded !== void 0) {
|
|
38488
|
+
safeReport(errorReporter, recorded, { requestId, method, route });
|
|
38489
|
+
}
|
|
38490
|
+
}
|
|
38491
|
+
}
|
|
38492
|
+
};
|
|
38493
|
+
}
|
|
38494
|
+
function safeReport(reporter, err, ctx) {
|
|
38495
|
+
try {
|
|
38496
|
+
reporter.captureException(err, ctx);
|
|
38497
|
+
} catch {
|
|
38498
|
+
}
|
|
38499
|
+
}
|
|
38500
|
+
|
|
37356
38501
|
// src/dispatcher-plugin.ts
|
|
37357
|
-
function mountRouteOnServer(route, server, routePath) {
|
|
38502
|
+
function mountRouteOnServer(route, server, routePath, securityHeaders) {
|
|
37358
38503
|
const handler = async (req, res) => {
|
|
37359
38504
|
try {
|
|
37360
38505
|
const result = await route.handler({
|
|
@@ -37364,6 +38509,11 @@ function mountRouteOnServer(route, server, routePath) {
|
|
|
37364
38509
|
});
|
|
37365
38510
|
if (result.stream && result.events) {
|
|
37366
38511
|
res.status(result.status);
|
|
38512
|
+
if (securityHeaders) {
|
|
38513
|
+
for (const [k, v] of Object.entries(securityHeaders)) {
|
|
38514
|
+
res.header(k, v);
|
|
38515
|
+
}
|
|
38516
|
+
}
|
|
37367
38517
|
if (result.headers) {
|
|
37368
38518
|
for (const [k, v] of Object.entries(result.headers)) {
|
|
37369
38519
|
res.header(k, String(v));
|
|
@@ -37389,6 +38539,11 @@ function mountRouteOnServer(route, server, routePath) {
|
|
|
37389
38539
|
}
|
|
37390
38540
|
} else {
|
|
37391
38541
|
res.status(result.status);
|
|
38542
|
+
if (securityHeaders) {
|
|
38543
|
+
for (const [k, v] of Object.entries(securityHeaders)) {
|
|
38544
|
+
res.header(k, v);
|
|
38545
|
+
}
|
|
38546
|
+
}
|
|
37392
38547
|
if (result.body !== void 0) {
|
|
37393
38548
|
res.json(result.body);
|
|
37394
38549
|
} else {
|
|
@@ -37396,7 +38551,7 @@ function mountRouteOnServer(route, server, routePath) {
|
|
|
37396
38551
|
}
|
|
37397
38552
|
}
|
|
37398
38553
|
} catch (err) {
|
|
37399
|
-
|
|
38554
|
+
errorResponseBase(err, res, securityHeaders);
|
|
37400
38555
|
}
|
|
37401
38556
|
};
|
|
37402
38557
|
const m = route.method.toLowerCase();
|
|
@@ -37412,10 +38567,17 @@ function mountRouteOnServer(route, server, routePath) {
|
|
|
37412
38567
|
}
|
|
37413
38568
|
return false;
|
|
37414
38569
|
}
|
|
37415
|
-
function
|
|
38570
|
+
function sendResultBase(result, res, securityHeaders) {
|
|
38571
|
+
const applySecurityHeaders = () => {
|
|
38572
|
+
if (!securityHeaders) return;
|
|
38573
|
+
for (const [k, v] of Object.entries(securityHeaders)) {
|
|
38574
|
+
res.header(k, v);
|
|
38575
|
+
}
|
|
38576
|
+
};
|
|
37416
38577
|
if (result.handled) {
|
|
37417
38578
|
if (result.response) {
|
|
37418
38579
|
res.status(result.response.status);
|
|
38580
|
+
applySecurityHeaders();
|
|
37419
38581
|
if (result.response.headers) {
|
|
37420
38582
|
for (const [k, v] of Object.entries(result.response.headers)) {
|
|
37421
38583
|
res.header(k, v);
|
|
@@ -37425,11 +38587,15 @@ function sendResult(result, res) {
|
|
|
37425
38587
|
return;
|
|
37426
38588
|
}
|
|
37427
38589
|
if (result.result) {
|
|
37428
|
-
res.status(200)
|
|
38590
|
+
res.status(200);
|
|
38591
|
+
applySecurityHeaders();
|
|
38592
|
+
res.json(result.result);
|
|
37429
38593
|
return;
|
|
37430
38594
|
}
|
|
37431
38595
|
}
|
|
37432
|
-
res.status(404)
|
|
38596
|
+
res.status(404);
|
|
38597
|
+
applySecurityHeaders();
|
|
38598
|
+
res.json({
|
|
37433
38599
|
success: false,
|
|
37434
38600
|
error: {
|
|
37435
38601
|
message: "Not Found",
|
|
@@ -37439,9 +38605,21 @@ function sendResult(result, res) {
|
|
|
37439
38605
|
}
|
|
37440
38606
|
});
|
|
37441
38607
|
}
|
|
37442
|
-
function
|
|
38608
|
+
function errorResponseBase(err, res, securityHeaders) {
|
|
37443
38609
|
const code = err.statusCode || 500;
|
|
37444
|
-
res.status(code)
|
|
38610
|
+
res.status(code);
|
|
38611
|
+
if (securityHeaders) {
|
|
38612
|
+
for (const [k, v] of Object.entries(securityHeaders)) {
|
|
38613
|
+
res.header(k, v);
|
|
38614
|
+
}
|
|
38615
|
+
}
|
|
38616
|
+
if (code >= 500) {
|
|
38617
|
+
try {
|
|
38618
|
+
res.__obsRecordedError = err;
|
|
38619
|
+
} catch {
|
|
38620
|
+
}
|
|
38621
|
+
}
|
|
38622
|
+
res.json({
|
|
37445
38623
|
success: false,
|
|
37446
38624
|
error: { message: err.message || "Internal Server Error", code }
|
|
37447
38625
|
});
|
|
@@ -37466,10 +38644,52 @@ function createDispatcherPlugin(config = {}) {
|
|
|
37466
38644
|
enforceProjectMembership: enforceMembership
|
|
37467
38645
|
});
|
|
37468
38646
|
const prefix = config.prefix || "/api/v1";
|
|
38647
|
+
const securityHeaders = config.securityHeaders === false ? void 0 : buildSecurityHeaders(
|
|
38648
|
+
typeof config.securityHeaders === "object" ? config.securityHeaders : {}
|
|
38649
|
+
);
|
|
38650
|
+
const sendResult = (result, res) => sendResultBase(result, res, securityHeaders);
|
|
38651
|
+
const errorResponse = (err, res) => errorResponseBase(err, res, securityHeaders);
|
|
38652
|
+
const metrics = config.observability?.metrics ?? new NoopMetricsRegistry();
|
|
38653
|
+
const errorReporter = config.observability?.errorReporter ?? new NoopErrorReporter();
|
|
38654
|
+
const generateRequestId2 = config.observability?.generateRequestId;
|
|
38655
|
+
const requestIdHeader = config.observability?.requestIdHeader ?? "X-Request-Id";
|
|
38656
|
+
const rawServer = server;
|
|
38657
|
+
server = new Proxy(rawServer, {
|
|
38658
|
+
get(target, prop, receiver) {
|
|
38659
|
+
if (prop === "get" || prop === "post" || prop === "delete") {
|
|
38660
|
+
const method = String(prop).toUpperCase();
|
|
38661
|
+
const original = target[prop];
|
|
38662
|
+
if (typeof original !== "function") return original;
|
|
38663
|
+
return (route, handler) => {
|
|
38664
|
+
return original.call(
|
|
38665
|
+
target,
|
|
38666
|
+
route,
|
|
38667
|
+
instrumentRouteHandler(method, route, handler, {
|
|
38668
|
+
metrics,
|
|
38669
|
+
errorReporter,
|
|
38670
|
+
generateRequestId: generateRequestId2,
|
|
38671
|
+
requestIdHeader
|
|
38672
|
+
})
|
|
38673
|
+
);
|
|
38674
|
+
};
|
|
38675
|
+
}
|
|
38676
|
+
return Reflect.get(target, prop, receiver);
|
|
38677
|
+
}
|
|
38678
|
+
});
|
|
37469
38679
|
server.get("/.well-known/objectstack", async (_req, res) => {
|
|
38680
|
+
if (securityHeaders) {
|
|
38681
|
+
for (const [k, v] of Object.entries(securityHeaders)) {
|
|
38682
|
+
res.header(k, v);
|
|
38683
|
+
}
|
|
38684
|
+
}
|
|
37470
38685
|
res.json({ data: await dispatcher.getDiscoveryInfo(prefix) });
|
|
37471
38686
|
});
|
|
37472
38687
|
server.get(`${prefix}/discovery`, async (_req, res) => {
|
|
38688
|
+
if (securityHeaders) {
|
|
38689
|
+
for (const [k, v] of Object.entries(securityHeaders)) {
|
|
38690
|
+
res.header(k, v);
|
|
38691
|
+
}
|
|
38692
|
+
}
|
|
37473
38693
|
res.json({ data: await dispatcher.getDiscoveryInfo(prefix) });
|
|
37474
38694
|
});
|
|
37475
38695
|
server.get(`${prefix}/health`, async (_req, res) => {
|
|
@@ -37491,6 +38711,11 @@ function createDispatcherPlugin(config = {}) {
|
|
|
37491
38711
|
server.post(`${prefix}/graphql`, async (req, res) => {
|
|
37492
38712
|
try {
|
|
37493
38713
|
const result = await dispatcher.handleGraphQL(req.body, { request: req });
|
|
38714
|
+
if (securityHeaders) {
|
|
38715
|
+
for (const [k, v] of Object.entries(securityHeaders)) {
|
|
38716
|
+
res.header(k, v);
|
|
38717
|
+
}
|
|
38718
|
+
}
|
|
37494
38719
|
res.json(result);
|
|
37495
38720
|
} catch (err) {
|
|
37496
38721
|
errorResponse(err, res);
|
|
@@ -37498,7 +38723,7 @@ function createDispatcherPlugin(config = {}) {
|
|
|
37498
38723
|
});
|
|
37499
38724
|
server.post(`${prefix}/analytics/query`, async (req, res) => {
|
|
37500
38725
|
try {
|
|
37501
|
-
const result = await dispatcher.
|
|
38726
|
+
const result = await dispatcher.dispatch("POST", "/analytics/query", req.body, req.query, { request: req });
|
|
37502
38727
|
sendResult(result, res);
|
|
37503
38728
|
} catch (err) {
|
|
37504
38729
|
errorResponse(err, res);
|
|
@@ -37506,7 +38731,7 @@ function createDispatcherPlugin(config = {}) {
|
|
|
37506
38731
|
});
|
|
37507
38732
|
server.get(`${prefix}/analytics/meta`, async (req, res) => {
|
|
37508
38733
|
try {
|
|
37509
|
-
const result = await dispatcher.
|
|
38734
|
+
const result = await dispatcher.dispatch("GET", "/analytics/meta", void 0, req.query, { request: req });
|
|
37510
38735
|
sendResult(result, res);
|
|
37511
38736
|
} catch (err) {
|
|
37512
38737
|
errorResponse(err, res);
|
|
@@ -37514,7 +38739,7 @@ function createDispatcherPlugin(config = {}) {
|
|
|
37514
38739
|
});
|
|
37515
38740
|
server.post(`${prefix}/analytics/sql`, async (req, res) => {
|
|
37516
38741
|
try {
|
|
37517
|
-
const result = await dispatcher.
|
|
38742
|
+
const result = await dispatcher.dispatch("POST", "/analytics/sql", req.body, req.query, { request: req });
|
|
37518
38743
|
sendResult(result, res);
|
|
37519
38744
|
} catch (err) {
|
|
37520
38745
|
errorResponse(err, res);
|
|
@@ -37592,6 +38817,14 @@ function createDispatcherPlugin(config = {}) {
|
|
|
37592
38817
|
errorResponse(err, res);
|
|
37593
38818
|
}
|
|
37594
38819
|
});
|
|
38820
|
+
server.post(`${prefix}/cloud/admin/platform-sso/backfill`, async (req, res) => {
|
|
38821
|
+
try {
|
|
38822
|
+
const result = await dispatcher.handleCloud("/admin/platform-sso/backfill", "POST", req.body, req.query, { request: req });
|
|
38823
|
+
sendResult(result, res);
|
|
38824
|
+
} catch (err) {
|
|
38825
|
+
errorResponse(err, res);
|
|
38826
|
+
}
|
|
38827
|
+
});
|
|
37595
38828
|
server.get(`${prefix}/cloud/templates`, async (req, res) => {
|
|
37596
38829
|
try {
|
|
37597
38830
|
const result = await dispatcher.handleCloud("/templates", "GET", {}, req.query, { request: req });
|
|
@@ -37704,6 +38937,30 @@ function createDispatcherPlugin(config = {}) {
|
|
|
37704
38937
|
errorResponse(err, res);
|
|
37705
38938
|
}
|
|
37706
38939
|
});
|
|
38940
|
+
server.post(`${prefix}/cloud/projects/:id/members`, async (req, res) => {
|
|
38941
|
+
try {
|
|
38942
|
+
const result = await dispatcher.handleCloud(`/projects/${req.params.id}/members`, "POST", req.body, {}, { request: req });
|
|
38943
|
+
sendResult(result, res);
|
|
38944
|
+
} catch (err) {
|
|
38945
|
+
errorResponse(err, res);
|
|
38946
|
+
}
|
|
38947
|
+
});
|
|
38948
|
+
server.patch(`${prefix}/cloud/projects/:id/members/:memberId`, async (req, res) => {
|
|
38949
|
+
try {
|
|
38950
|
+
const result = await dispatcher.handleCloud(`/projects/${req.params.id}/members/${req.params.memberId}`, "PATCH", req.body, {}, { request: req });
|
|
38951
|
+
sendResult(result, res);
|
|
38952
|
+
} catch (err) {
|
|
38953
|
+
errorResponse(err, res);
|
|
38954
|
+
}
|
|
38955
|
+
});
|
|
38956
|
+
server.delete(`${prefix}/cloud/projects/:id/members/:memberId`, async (req, res) => {
|
|
38957
|
+
try {
|
|
38958
|
+
const result = await dispatcher.handleCloud(`/projects/${req.params.id}/members/${req.params.memberId}`, "DELETE", req.body ?? {}, {}, { request: req });
|
|
38959
|
+
sendResult(result, res);
|
|
38960
|
+
} catch (err) {
|
|
38961
|
+
errorResponse(err, res);
|
|
38962
|
+
}
|
|
38963
|
+
});
|
|
37707
38964
|
server.get(`${prefix}/cloud/projects/:id/packages`, async (req, res) => {
|
|
37708
38965
|
try {
|
|
37709
38966
|
const result = await dispatcher.handleCloud(`/projects/${req.params.id}/packages`, "GET", {}, req.query, { request: req });
|
|
@@ -37778,7 +39035,7 @@ function createDispatcherPlugin(config = {}) {
|
|
|
37778
39035
|
});
|
|
37779
39036
|
server.get(`${prefix}/i18n/locales`, async (req, res) => {
|
|
37780
39037
|
try {
|
|
37781
|
-
const result = await dispatcher.
|
|
39038
|
+
const result = await dispatcher.dispatch("GET", "/i18n/locales", void 0, req.query, { request: req });
|
|
37782
39039
|
sendResult(result, res);
|
|
37783
39040
|
} catch (err) {
|
|
37784
39041
|
errorResponse(err, res);
|
|
@@ -37786,7 +39043,7 @@ function createDispatcherPlugin(config = {}) {
|
|
|
37786
39043
|
});
|
|
37787
39044
|
server.get(`${prefix}/i18n/translations/:locale`, async (req, res) => {
|
|
37788
39045
|
try {
|
|
37789
|
-
const result = await dispatcher.
|
|
39046
|
+
const result = await dispatcher.dispatch("GET", `/i18n/translations/${req.params.locale}`, void 0, req.query, { request: req });
|
|
37790
39047
|
sendResult(result, res);
|
|
37791
39048
|
} catch (err) {
|
|
37792
39049
|
errorResponse(err, res);
|
|
@@ -37794,7 +39051,7 @@ function createDispatcherPlugin(config = {}) {
|
|
|
37794
39051
|
});
|
|
37795
39052
|
server.get(`${prefix}/i18n/labels/:object/:locale`, async (req, res) => {
|
|
37796
39053
|
try {
|
|
37797
|
-
const result = await dispatcher.
|
|
39054
|
+
const result = await dispatcher.dispatch("GET", `/i18n/labels/${req.params.object}/${req.params.locale}`, void 0, req.query, { request: req });
|
|
37798
39055
|
sendResult(result, res);
|
|
37799
39056
|
} catch (err) {
|
|
37800
39057
|
errorResponse(err, res);
|
|
@@ -37803,7 +39060,7 @@ function createDispatcherPlugin(config = {}) {
|
|
|
37803
39060
|
const registerAutomationRoutes = (base2) => {
|
|
37804
39061
|
server.get(`${base2}/automation`, async (req, res) => {
|
|
37805
39062
|
try {
|
|
37806
|
-
const result = await dispatcher.
|
|
39063
|
+
const result = await dispatcher.dispatch("GET", "/automation", void 0, req.query, { request: req });
|
|
37807
39064
|
sendResult(result, res);
|
|
37808
39065
|
} catch (err) {
|
|
37809
39066
|
errorResponse(err, res);
|
|
@@ -37811,7 +39068,7 @@ function createDispatcherPlugin(config = {}) {
|
|
|
37811
39068
|
});
|
|
37812
39069
|
server.post(`${base2}/automation`, async (req, res) => {
|
|
37813
39070
|
try {
|
|
37814
|
-
const result = await dispatcher.
|
|
39071
|
+
const result = await dispatcher.dispatch("POST", "/automation", req.body, req.query, { request: req });
|
|
37815
39072
|
sendResult(result, res);
|
|
37816
39073
|
} catch (err) {
|
|
37817
39074
|
errorResponse(err, res);
|
|
@@ -37819,7 +39076,7 @@ function createDispatcherPlugin(config = {}) {
|
|
|
37819
39076
|
});
|
|
37820
39077
|
server.get(`${base2}/automation/:name`, async (req, res) => {
|
|
37821
39078
|
try {
|
|
37822
|
-
const result = await dispatcher.
|
|
39079
|
+
const result = await dispatcher.dispatch("GET", `/automation/${req.params.name}`, void 0, req.query, { request: req });
|
|
37823
39080
|
sendResult(result, res);
|
|
37824
39081
|
} catch (err) {
|
|
37825
39082
|
errorResponse(err, res);
|
|
@@ -37827,7 +39084,7 @@ function createDispatcherPlugin(config = {}) {
|
|
|
37827
39084
|
});
|
|
37828
39085
|
server.put(`${base2}/automation/:name`, async (req, res) => {
|
|
37829
39086
|
try {
|
|
37830
|
-
const result = await dispatcher.
|
|
39087
|
+
const result = await dispatcher.dispatch("PUT", `/automation/${req.params.name}`, req.body, req.query, { request: req });
|
|
37831
39088
|
sendResult(result, res);
|
|
37832
39089
|
} catch (err) {
|
|
37833
39090
|
errorResponse(err, res);
|
|
@@ -37835,7 +39092,7 @@ function createDispatcherPlugin(config = {}) {
|
|
|
37835
39092
|
});
|
|
37836
39093
|
server.delete(`${base2}/automation/:name`, async (req, res) => {
|
|
37837
39094
|
try {
|
|
37838
|
-
const result = await dispatcher.
|
|
39095
|
+
const result = await dispatcher.dispatch("DELETE", `/automation/${req.params.name}`, void 0, req.query, { request: req });
|
|
37839
39096
|
sendResult(result, res);
|
|
37840
39097
|
} catch (err) {
|
|
37841
39098
|
errorResponse(err, res);
|
|
@@ -37843,7 +39100,7 @@ function createDispatcherPlugin(config = {}) {
|
|
|
37843
39100
|
});
|
|
37844
39101
|
server.post(`${base2}/automation/trigger/:name`, async (req, res) => {
|
|
37845
39102
|
try {
|
|
37846
|
-
const result = await dispatcher.
|
|
39103
|
+
const result = await dispatcher.dispatch("POST", `/automation/trigger/${req.params.name}`, req.body, req.query, { request: req });
|
|
37847
39104
|
sendResult(result, res);
|
|
37848
39105
|
} catch (err) {
|
|
37849
39106
|
errorResponse(err, res);
|
|
@@ -37851,7 +39108,7 @@ function createDispatcherPlugin(config = {}) {
|
|
|
37851
39108
|
});
|
|
37852
39109
|
server.post(`${base2}/automation/:name/trigger`, async (req, res) => {
|
|
37853
39110
|
try {
|
|
37854
|
-
const result = await dispatcher.
|
|
39111
|
+
const result = await dispatcher.dispatch("POST", `/automation/${req.params.name}/trigger`, req.body, req.query, { request: req });
|
|
37855
39112
|
sendResult(result, res);
|
|
37856
39113
|
} catch (err) {
|
|
37857
39114
|
errorResponse(err, res);
|
|
@@ -37859,7 +39116,7 @@ function createDispatcherPlugin(config = {}) {
|
|
|
37859
39116
|
});
|
|
37860
39117
|
server.post(`${base2}/automation/:name/toggle`, async (req, res) => {
|
|
37861
39118
|
try {
|
|
37862
|
-
const result = await dispatcher.
|
|
39119
|
+
const result = await dispatcher.dispatch("POST", `/automation/${req.params.name}/toggle`, req.body, req.query, { request: req });
|
|
37863
39120
|
sendResult(result, res);
|
|
37864
39121
|
} catch (err) {
|
|
37865
39122
|
errorResponse(err, res);
|
|
@@ -37867,7 +39124,7 @@ function createDispatcherPlugin(config = {}) {
|
|
|
37867
39124
|
});
|
|
37868
39125
|
server.get(`${base2}/automation/:name/runs`, async (req, res) => {
|
|
37869
39126
|
try {
|
|
37870
|
-
const result = await dispatcher.
|
|
39127
|
+
const result = await dispatcher.dispatch("GET", `/automation/${req.params.name}/runs`, void 0, req.query, { request: req });
|
|
37871
39128
|
sendResult(result, res);
|
|
37872
39129
|
} catch (err) {
|
|
37873
39130
|
errorResponse(err, res);
|
|
@@ -37875,13 +39132,34 @@ function createDispatcherPlugin(config = {}) {
|
|
|
37875
39132
|
});
|
|
37876
39133
|
server.get(`${base2}/automation/:name/runs/:runId`, async (req, res) => {
|
|
37877
39134
|
try {
|
|
37878
|
-
const result = await dispatcher.
|
|
39135
|
+
const result = await dispatcher.dispatch("GET", `/automation/${req.params.name}/runs/${req.params.runId}`, void 0, req.query, { request: req });
|
|
37879
39136
|
sendResult(result, res);
|
|
37880
39137
|
} catch (err) {
|
|
37881
39138
|
errorResponse(err, res);
|
|
37882
39139
|
}
|
|
37883
39140
|
});
|
|
37884
39141
|
};
|
|
39142
|
+
const registerAIRoutes = (base2) => {
|
|
39143
|
+
const wildcards = [
|
|
39144
|
+
["get", `${base2}/ai/*`],
|
|
39145
|
+
["post", `${base2}/ai/*`],
|
|
39146
|
+
["delete", `${base2}/ai/*`],
|
|
39147
|
+
["put", `${base2}/ai/*`]
|
|
39148
|
+
];
|
|
39149
|
+
for (const [method, pattern] of wildcards) {
|
|
39150
|
+
server[method](pattern, async (req, res) => {
|
|
39151
|
+
try {
|
|
39152
|
+
const fullPath = req.path ?? "";
|
|
39153
|
+
const idx = fullPath.lastIndexOf("/ai");
|
|
39154
|
+
const aiSubPath = idx >= 0 ? fullPath.slice(idx) : "/ai";
|
|
39155
|
+
const result = await dispatcher.dispatch(method.toUpperCase(), aiSubPath, req.body, req.query, { request: req });
|
|
39156
|
+
sendResult(result, res);
|
|
39157
|
+
} catch (err) {
|
|
39158
|
+
errorResponse(err, res);
|
|
39159
|
+
}
|
|
39160
|
+
});
|
|
39161
|
+
}
|
|
39162
|
+
};
|
|
37885
39163
|
const registerActionRoutes = (base2) => {
|
|
37886
39164
|
server.post(`${base2}/actions/:object/:action`, async (req, res) => {
|
|
37887
39165
|
try {
|
|
@@ -37909,12 +39187,15 @@ function createDispatcherPlugin(config = {}) {
|
|
|
37909
39187
|
if (enableProjectScoping && projectResolution === "required") {
|
|
37910
39188
|
registerAutomationRoutes(`${prefix}/projects/:projectId`);
|
|
37911
39189
|
registerActionRoutes(`${prefix}/projects/:projectId`);
|
|
39190
|
+
registerAIRoutes(`${prefix}/projects/:projectId`);
|
|
37912
39191
|
} else {
|
|
37913
39192
|
registerAutomationRoutes(prefix);
|
|
37914
39193
|
registerActionRoutes(prefix);
|
|
39194
|
+
registerAIRoutes(prefix);
|
|
37915
39195
|
if (enableProjectScoping) {
|
|
37916
39196
|
registerAutomationRoutes(`${prefix}/projects/:projectId`);
|
|
37917
39197
|
registerActionRoutes(`${prefix}/projects/:projectId`);
|
|
39198
|
+
registerAIRoutes(`${prefix}/projects/:projectId`);
|
|
37918
39199
|
}
|
|
37919
39200
|
}
|
|
37920
39201
|
ctx.logger.info("Dispatcher bridge routes registered", { prefix, enableProjectScoping, projectResolution });
|
|
@@ -37930,11 +39211,11 @@ function createDispatcherPlugin(config = {}) {
|
|
|
37930
39211
|
const routePath = route.path.startsWith("/api/v1") ? route.path : `${prefix}${route.path}`;
|
|
37931
39212
|
let count = 0;
|
|
37932
39213
|
if (enableProjectScoping && projectResolution === "required") {
|
|
37933
|
-
if (mountRouteOnServer(route, server, toScopedPath(routePath))) count++;
|
|
39214
|
+
if (mountRouteOnServer(route, server, toScopedPath(routePath), securityHeaders)) count++;
|
|
37934
39215
|
} else {
|
|
37935
|
-
if (mountRouteOnServer(route, server, routePath)) count++;
|
|
39216
|
+
if (mountRouteOnServer(route, server, routePath, securityHeaders)) count++;
|
|
37936
39217
|
if (enableProjectScoping) {
|
|
37937
|
-
if (mountRouteOnServer(route, server, toScopedPath(routePath))) count++;
|
|
39218
|
+
if (mountRouteOnServer(route, server, toScopedPath(routePath), securityHeaders)) count++;
|
|
37938
39219
|
}
|
|
37939
39220
|
}
|
|
37940
39221
|
return count;
|
|
@@ -38259,6 +39540,1334 @@ var MiddlewareManager = class {
|
|
|
38259
39540
|
// src/index.ts
|
|
38260
39541
|
init_load_artifact_bundle();
|
|
38261
39542
|
|
|
39543
|
+
// src/cloud/kernel-manager.ts
|
|
39544
|
+
var KernelManager = class {
|
|
39545
|
+
constructor(config) {
|
|
39546
|
+
this.cache = /* @__PURE__ */ new Map();
|
|
39547
|
+
this.pending = /* @__PURE__ */ new Map();
|
|
39548
|
+
this.factory = config.factory;
|
|
39549
|
+
this.maxSize = config.maxSize ?? 32;
|
|
39550
|
+
this.ttlMs = config.ttlMs ?? 15 * 60 * 1e3;
|
|
39551
|
+
this.logger = config.logger ?? console;
|
|
39552
|
+
}
|
|
39553
|
+
/** Returns the currently cached projectIds (ordered by insertion). */
|
|
39554
|
+
keys() {
|
|
39555
|
+
return Array.from(this.cache.keys());
|
|
39556
|
+
}
|
|
39557
|
+
/** Cache size for diagnostics. */
|
|
39558
|
+
get size() {
|
|
39559
|
+
return this.cache.size;
|
|
39560
|
+
}
|
|
39561
|
+
/**
|
|
39562
|
+
* Resolve or construct the kernel for `projectId`.
|
|
39563
|
+
*
|
|
39564
|
+
* - Cache hit (fresh): bumps `lastAccess` and returns immediately.
|
|
39565
|
+
* - Cache hit (TTL expired): evicts then falls through to factory.
|
|
39566
|
+
* - Cache miss: dedupes concurrent callers through `pending`.
|
|
39567
|
+
*/
|
|
39568
|
+
async getOrCreate(projectId) {
|
|
39569
|
+
const existing = this.cache.get(projectId);
|
|
39570
|
+
if (existing) {
|
|
39571
|
+
if (this.ttlMs > 0 && Date.now() - existing.lastAccess > this.ttlMs) {
|
|
39572
|
+
await this.evict(projectId);
|
|
39573
|
+
} else {
|
|
39574
|
+
existing.lastAccess = Date.now();
|
|
39575
|
+
return existing.kernel;
|
|
39576
|
+
}
|
|
39577
|
+
}
|
|
39578
|
+
const inflight = this.pending.get(projectId);
|
|
39579
|
+
if (inflight) return inflight;
|
|
39580
|
+
const promise = (async () => {
|
|
39581
|
+
const kernel = await this.factory.create(projectId);
|
|
39582
|
+
const now = Date.now();
|
|
39583
|
+
this.cache.set(projectId, { kernel, createdAt: now, lastAccess: now });
|
|
39584
|
+
await this.enforceMaxSize();
|
|
39585
|
+
return kernel;
|
|
39586
|
+
})();
|
|
39587
|
+
this.pending.set(projectId, promise);
|
|
39588
|
+
try {
|
|
39589
|
+
return await promise;
|
|
39590
|
+
} finally {
|
|
39591
|
+
this.pending.delete(projectId);
|
|
39592
|
+
}
|
|
39593
|
+
}
|
|
39594
|
+
/**
|
|
39595
|
+
* Evict the kernel for `projectId` and invoke `kernel.shutdown()`.
|
|
39596
|
+
* No-op when the entry is absent.
|
|
39597
|
+
*/
|
|
39598
|
+
async evict(projectId) {
|
|
39599
|
+
const entry = this.cache.get(projectId);
|
|
39600
|
+
if (!entry) return;
|
|
39601
|
+
this.cache.delete(projectId);
|
|
39602
|
+
try {
|
|
39603
|
+
await entry.kernel.shutdown();
|
|
39604
|
+
} catch (err) {
|
|
39605
|
+
this.logger.error?.("[KernelManager] shutdown failed", { projectId, err });
|
|
39606
|
+
}
|
|
39607
|
+
}
|
|
39608
|
+
/** Evict all resident kernels. Used on runtime shutdown. */
|
|
39609
|
+
async evictAll() {
|
|
39610
|
+
const ids = Array.from(this.cache.keys());
|
|
39611
|
+
await Promise.all(ids.map((id) => this.evict(id)));
|
|
39612
|
+
}
|
|
39613
|
+
async enforceMaxSize() {
|
|
39614
|
+
while (this.cache.size > this.maxSize) {
|
|
39615
|
+
let oldestKey;
|
|
39616
|
+
let oldestAccess = Infinity;
|
|
39617
|
+
for (const [key, entry] of this.cache) {
|
|
39618
|
+
if (entry.lastAccess < oldestAccess) {
|
|
39619
|
+
oldestAccess = entry.lastAccess;
|
|
39620
|
+
oldestKey = key;
|
|
39621
|
+
}
|
|
39622
|
+
}
|
|
39623
|
+
if (!oldestKey) return;
|
|
39624
|
+
await this.evict(oldestKey);
|
|
39625
|
+
}
|
|
39626
|
+
}
|
|
39627
|
+
};
|
|
39628
|
+
|
|
39629
|
+
// src/cloud/artifact-api-client.ts
|
|
39630
|
+
var ArtifactApiClient = class {
|
|
39631
|
+
constructor(config) {
|
|
39632
|
+
this.hostnameCache = /* @__PURE__ */ new Map();
|
|
39633
|
+
this.artifactCache = /* @__PURE__ */ new Map();
|
|
39634
|
+
this.pendingHostname = /* @__PURE__ */ new Map();
|
|
39635
|
+
this.pendingArtifact = /* @__PURE__ */ new Map();
|
|
39636
|
+
if (!config.controlPlaneUrl) {
|
|
39637
|
+
throw new Error("[ArtifactApiClient] controlPlaneUrl is required");
|
|
39638
|
+
}
|
|
39639
|
+
this.base = config.controlPlaneUrl.replace(/\/+$/, "");
|
|
39640
|
+
this.apiKey = config.apiKey;
|
|
39641
|
+
this.cacheTtlMs = config.cacheTtlMs ?? 5 * 60 * 1e3;
|
|
39642
|
+
this.requestTimeoutMs = config.requestTimeoutMs ?? 1e4;
|
|
39643
|
+
this.fetchImpl = config.fetch ?? globalThis.fetch;
|
|
39644
|
+
this.logger = config.logger ?? console;
|
|
39645
|
+
if (typeof this.fetchImpl !== "function") {
|
|
39646
|
+
throw new Error("[ArtifactApiClient] global fetch is not available \u2014 provide config.fetch");
|
|
39647
|
+
}
|
|
39648
|
+
}
|
|
39649
|
+
/**
|
|
39650
|
+
* Resolve a hostname to its project. Returns `null` on 404 or
|
|
39651
|
+
* malformed responses. Errors (network / 5xx) are thrown so
|
|
39652
|
+
* upstream callers can retry.
|
|
39653
|
+
*/
|
|
39654
|
+
async resolveHostname(host) {
|
|
39655
|
+
const cached = this.hostnameCache.get(host);
|
|
39656
|
+
if (cached && cached.expiresAt > Date.now()) return cached.value;
|
|
39657
|
+
const inflight = this.pendingHostname.get(host);
|
|
39658
|
+
if (inflight) return inflight;
|
|
39659
|
+
const promise = (async () => {
|
|
39660
|
+
try {
|
|
39661
|
+
const url = `${this.base}/api/v1/cloud/resolve-hostname?host=${encodeURIComponent(host)}`;
|
|
39662
|
+
const res = await this.request(url);
|
|
39663
|
+
if (res === null) return null;
|
|
39664
|
+
const body = res.success === false ? null : res.data ?? res;
|
|
39665
|
+
if (!body || typeof body.projectId !== "string" || !body.projectId) return null;
|
|
39666
|
+
const value = {
|
|
39667
|
+
projectId: body.projectId,
|
|
39668
|
+
organizationId: body.organizationId,
|
|
39669
|
+
runtime: body.runtime
|
|
39670
|
+
};
|
|
39671
|
+
this.hostnameCache.set(host, { value, expiresAt: Date.now() + this.cacheTtlMs });
|
|
39672
|
+
return value;
|
|
39673
|
+
} finally {
|
|
39674
|
+
this.pendingHostname.delete(host);
|
|
39675
|
+
}
|
|
39676
|
+
})();
|
|
39677
|
+
this.pendingHostname.set(host, promise);
|
|
39678
|
+
return promise;
|
|
39679
|
+
}
|
|
39680
|
+
/**
|
|
39681
|
+
* Fetch the compiled artifact for a project.
|
|
39682
|
+
*
|
|
39683
|
+
* When `opts.commit` is set, requests that specific revision via the
|
|
39684
|
+
* existing `?commit=` query param. Different commits are cached
|
|
39685
|
+
* independently (the cache key includes the commit id) so the preview
|
|
39686
|
+
* runtime can hold multiple versions in memory simultaneously.
|
|
39687
|
+
*/
|
|
39688
|
+
async fetchArtifact(projectId, opts) {
|
|
39689
|
+
const commit = opts?.commit?.trim() || "";
|
|
39690
|
+
const cacheKey = commit ? `${projectId}@${commit}` : projectId;
|
|
39691
|
+
const cached = this.artifactCache.get(cacheKey);
|
|
39692
|
+
if (cached && cached.expiresAt > Date.now()) return cached.value;
|
|
39693
|
+
const inflight = this.pendingArtifact.get(cacheKey);
|
|
39694
|
+
if (inflight) return inflight;
|
|
39695
|
+
const promise = (async () => {
|
|
39696
|
+
try {
|
|
39697
|
+
const qs = commit ? `?commit=${encodeURIComponent(commit)}` : "";
|
|
39698
|
+
const url = `${this.base}/api/v1/cloud/projects/${encodeURIComponent(projectId)}/artifact${qs}`;
|
|
39699
|
+
const res = await this.request(url);
|
|
39700
|
+
if (res === null) return null;
|
|
39701
|
+
const body = res.success === false ? null : res.data ?? res;
|
|
39702
|
+
if (!body || typeof body !== "object") return null;
|
|
39703
|
+
if (!body.metadata) {
|
|
39704
|
+
this.logger.warn?.("[ArtifactApiClient] artifact response missing `metadata`", { projectId, commit });
|
|
39705
|
+
return null;
|
|
39706
|
+
}
|
|
39707
|
+
const value = body;
|
|
39708
|
+
this.artifactCache.set(cacheKey, { value, expiresAt: Date.now() + this.cacheTtlMs });
|
|
39709
|
+
return value;
|
|
39710
|
+
} finally {
|
|
39711
|
+
this.pendingArtifact.delete(cacheKey);
|
|
39712
|
+
}
|
|
39713
|
+
})();
|
|
39714
|
+
this.pendingArtifact.set(cacheKey, promise);
|
|
39715
|
+
return promise;
|
|
39716
|
+
}
|
|
39717
|
+
/**
|
|
39718
|
+
* Resolve an 8-hex project short id (first 8 hex chars of the UUID,
|
|
39719
|
+
* dashes stripped) to the full projectId. Used by the preview
|
|
39720
|
+
* runtime, which encodes project ids in subdomains.
|
|
39721
|
+
*
|
|
39722
|
+
* Returns `null` on 404 or ambiguity (the control plane returns 409
|
|
39723
|
+
* if the prefix matches more than one project).
|
|
39724
|
+
*/
|
|
39725
|
+
async lookupProjectByShortId(shortId) {
|
|
39726
|
+
const short = String(shortId ?? "").trim().toLowerCase();
|
|
39727
|
+
if (!/^[0-9a-f]{8,}$/.test(short)) return null;
|
|
39728
|
+
const url = `${this.base}/api/v1/cloud/projects-by-short-id/${encodeURIComponent(short)}`;
|
|
39729
|
+
const res = await this.request(url);
|
|
39730
|
+
if (res === null) return null;
|
|
39731
|
+
const body = res.success === false ? null : res.data ?? res;
|
|
39732
|
+
if (!body || typeof body.projectId !== "string" || !body.projectId) return null;
|
|
39733
|
+
return { projectId: body.projectId, organizationId: body.organizationId };
|
|
39734
|
+
}
|
|
39735
|
+
/**
|
|
39736
|
+
* Fetch the head commit of a branch. Returns the commit id (and the
|
|
39737
|
+
* matching revision row's `published_at` for cache-validity checks).
|
|
39738
|
+
* Reuses the existing `GET /cloud/projects/:id/branches` endpoint.
|
|
39739
|
+
*/
|
|
39740
|
+
async fetchBranchHead(projectId, branchName) {
|
|
39741
|
+
const url = `${this.base}/api/v1/cloud/projects/${encodeURIComponent(projectId)}/branches`;
|
|
39742
|
+
const res = await this.request(url);
|
|
39743
|
+
if (res === null) return null;
|
|
39744
|
+
const body = res.success === false ? null : res.data ?? res;
|
|
39745
|
+
const branches = Array.isArray(body?.branches) ? body.branches : [];
|
|
39746
|
+
const target = String(branchName ?? "").trim().toLowerCase();
|
|
39747
|
+
const found = branches.find((b) => String(b?.branch ?? "").toLowerCase() === target);
|
|
39748
|
+
if (!found?.headCommitId) return null;
|
|
39749
|
+
return { commitId: String(found.headCommitId), publishedAt: found.headPublishedAt ?? null };
|
|
39750
|
+
}
|
|
39751
|
+
/** Drop cached entries for a project (and any matching hostname). */
|
|
39752
|
+
invalidate(projectId) {
|
|
39753
|
+
this.artifactCache.delete(projectId);
|
|
39754
|
+
const prefix = `${projectId}@`;
|
|
39755
|
+
for (const key of Array.from(this.artifactCache.keys())) {
|
|
39756
|
+
if (key.startsWith(prefix)) this.artifactCache.delete(key);
|
|
39757
|
+
}
|
|
39758
|
+
for (const [host, entry] of this.hostnameCache) {
|
|
39759
|
+
if (entry.value.projectId === projectId) this.hostnameCache.delete(host);
|
|
39760
|
+
}
|
|
39761
|
+
}
|
|
39762
|
+
/** Drop everything. Used on shutdown / hot-reload. */
|
|
39763
|
+
clear() {
|
|
39764
|
+
this.hostnameCache.clear();
|
|
39765
|
+
this.artifactCache.clear();
|
|
39766
|
+
}
|
|
39767
|
+
async request(url) {
|
|
39768
|
+
const controller = typeof AbortController !== "undefined" ? new AbortController() : null;
|
|
39769
|
+
const timer = controller ? setTimeout(() => controller.abort(), this.requestTimeoutMs) : null;
|
|
39770
|
+
try {
|
|
39771
|
+
const res = await this.fetchImpl(url, {
|
|
39772
|
+
method: "GET",
|
|
39773
|
+
headers: this.buildHeaders(),
|
|
39774
|
+
signal: controller?.signal
|
|
39775
|
+
});
|
|
39776
|
+
if (res.status === 404) return null;
|
|
39777
|
+
if (!res.ok) {
|
|
39778
|
+
throw new Error(`[ArtifactApiClient] ${url} \u2192 HTTP ${res.status}`);
|
|
39779
|
+
}
|
|
39780
|
+
return await res.json();
|
|
39781
|
+
} finally {
|
|
39782
|
+
if (timer) clearTimeout(timer);
|
|
39783
|
+
}
|
|
39784
|
+
}
|
|
39785
|
+
buildHeaders() {
|
|
39786
|
+
const headers = {
|
|
39787
|
+
"accept": "application/json",
|
|
39788
|
+
"user-agent": "objectos-runtime"
|
|
39789
|
+
};
|
|
39790
|
+
if (this.apiKey) headers["authorization"] = `Bearer ${this.apiKey}`;
|
|
39791
|
+
return headers;
|
|
39792
|
+
}
|
|
39793
|
+
};
|
|
39794
|
+
|
|
39795
|
+
// src/cloud/artifact-environment-registry.ts
|
|
39796
|
+
import { resolve as resolvePathNode } from "path";
|
|
39797
|
+
var ArtifactEnvironmentRegistry = class {
|
|
39798
|
+
constructor(config) {
|
|
39799
|
+
this.hostnameCache = /* @__PURE__ */ new Map();
|
|
39800
|
+
this.idCache = /* @__PURE__ */ new Map();
|
|
39801
|
+
this.pending = /* @__PURE__ */ new Map();
|
|
39802
|
+
this.client = config.client;
|
|
39803
|
+
this.cacheTTL = config.cacheTtlMs ?? 5 * 60 * 1e3;
|
|
39804
|
+
this.logger = config.logger ?? console;
|
|
39805
|
+
}
|
|
39806
|
+
async resolveByHostname(host) {
|
|
39807
|
+
const cached = this.hostnameCache.get(host);
|
|
39808
|
+
if (cached && cached.expiresAt > Date.now()) {
|
|
39809
|
+
return { projectId: cached.projectId, driver: cached.driver };
|
|
39810
|
+
}
|
|
39811
|
+
const key = `host:${host}`;
|
|
39812
|
+
const inflight = this.pending.get(key);
|
|
39813
|
+
if (inflight) {
|
|
39814
|
+
const result = await inflight;
|
|
39815
|
+
return result ? { projectId: result.projectId, driver: result.driver } : null;
|
|
39816
|
+
}
|
|
39817
|
+
const promise = (async () => {
|
|
39818
|
+
try {
|
|
39819
|
+
const resolved = await this.client.resolveHostname(host);
|
|
39820
|
+
if (!resolved) return null;
|
|
39821
|
+
const entry2 = await this.buildCacheEntry(resolved.projectId, resolved.runtime, resolved.organizationId, host);
|
|
39822
|
+
if (!entry2) return null;
|
|
39823
|
+
this.hostnameCache.set(host, entry2);
|
|
39824
|
+
this.idCache.set(entry2.projectId, entry2);
|
|
39825
|
+
return entry2;
|
|
39826
|
+
} catch (err) {
|
|
39827
|
+
this.logger.error?.("[ArtifactEnvironmentRegistry] resolveByHostname failed", {
|
|
39828
|
+
host,
|
|
39829
|
+
error: err?.message ?? err
|
|
39830
|
+
});
|
|
39831
|
+
return null;
|
|
39832
|
+
} finally {
|
|
39833
|
+
this.pending.delete(key);
|
|
39834
|
+
}
|
|
39835
|
+
})();
|
|
39836
|
+
this.pending.set(key, promise);
|
|
39837
|
+
const entry = await promise;
|
|
39838
|
+
return entry ? { projectId: entry.projectId, driver: entry.driver } : null;
|
|
39839
|
+
}
|
|
39840
|
+
async resolveById(projectId) {
|
|
39841
|
+
const cached = this.idCache.get(projectId);
|
|
39842
|
+
if (cached && cached.expiresAt > Date.now()) return cached.driver;
|
|
39843
|
+
const key = `id:${projectId}`;
|
|
39844
|
+
const inflight = this.pending.get(key);
|
|
39845
|
+
if (inflight) {
|
|
39846
|
+
const result = await inflight;
|
|
39847
|
+
return result?.driver ?? null;
|
|
39848
|
+
}
|
|
39849
|
+
const promise = (async () => {
|
|
39850
|
+
try {
|
|
39851
|
+
const entry2 = await this.buildCacheEntry(projectId, void 0, void 0, void 0);
|
|
39852
|
+
if (!entry2) return null;
|
|
39853
|
+
this.idCache.set(projectId, entry2);
|
|
39854
|
+
if (entry2.project?.hostname) this.hostnameCache.set(entry2.project.hostname, entry2);
|
|
39855
|
+
return entry2;
|
|
39856
|
+
} catch (err) {
|
|
39857
|
+
this.logger.error?.("[ArtifactEnvironmentRegistry] resolveById failed", {
|
|
39858
|
+
projectId,
|
|
39859
|
+
error: err?.message ?? err
|
|
39860
|
+
});
|
|
39861
|
+
return null;
|
|
39862
|
+
} finally {
|
|
39863
|
+
this.pending.delete(key);
|
|
39864
|
+
}
|
|
39865
|
+
})();
|
|
39866
|
+
this.pending.set(key, promise);
|
|
39867
|
+
const entry = await promise;
|
|
39868
|
+
return entry?.driver ?? null;
|
|
39869
|
+
}
|
|
39870
|
+
peekById(projectId) {
|
|
39871
|
+
const cached = this.idCache.get(projectId);
|
|
39872
|
+
if (cached && cached.expiresAt > Date.now()) {
|
|
39873
|
+
return { projectId: cached.projectId, driver: cached.driver, project: cached.project };
|
|
39874
|
+
}
|
|
39875
|
+
return null;
|
|
39876
|
+
}
|
|
39877
|
+
invalidate(projectId) {
|
|
39878
|
+
this.idCache.delete(projectId);
|
|
39879
|
+
for (const [host, entry] of this.hostnameCache) {
|
|
39880
|
+
if (entry.projectId === projectId) this.hostnameCache.delete(host);
|
|
39881
|
+
}
|
|
39882
|
+
this.client.invalidate(projectId);
|
|
39883
|
+
}
|
|
39884
|
+
async buildCacheEntry(projectId, runtimeFromHostname, orgIdFromHostname, hostname) {
|
|
39885
|
+
let runtime = runtimeFromHostname;
|
|
39886
|
+
let organizationId = orgIdFromHostname;
|
|
39887
|
+
let host = hostname;
|
|
39888
|
+
let artifactProjectId = projectId;
|
|
39889
|
+
if (!runtime || !organizationId) {
|
|
39890
|
+
const artifact = await this.client.fetchArtifact(projectId);
|
|
39891
|
+
if (!artifact) {
|
|
39892
|
+
this.logger.warn?.("[ArtifactEnvironmentRegistry] artifact not found", { projectId });
|
|
39893
|
+
return null;
|
|
39894
|
+
}
|
|
39895
|
+
artifactProjectId = artifact.projectId ?? projectId;
|
|
39896
|
+
if (!runtime) runtime = artifact.runtime ?? extractRuntimeFromMetadata(artifact.metadata);
|
|
39897
|
+
if (!organizationId) organizationId = artifact.runtime?.organizationId;
|
|
39898
|
+
if (!host) host = artifact.runtime?.hostname;
|
|
39899
|
+
}
|
|
39900
|
+
if (!runtime || !runtime.databaseUrl || !runtime.databaseDriver) {
|
|
39901
|
+
this.logger.warn?.("[ArtifactEnvironmentRegistry] no runtime config for project", { projectId });
|
|
39902
|
+
return null;
|
|
39903
|
+
}
|
|
39904
|
+
const driver = await createDriver(runtime.databaseDriver, runtime.databaseUrl, runtime.databaseAuthToken ?? "");
|
|
39905
|
+
const projectRow = {
|
|
39906
|
+
id: artifactProjectId,
|
|
39907
|
+
organization_id: organizationId,
|
|
39908
|
+
hostname: host,
|
|
39909
|
+
database_url: runtime.databaseUrl,
|
|
39910
|
+
database_driver: runtime.databaseDriver,
|
|
39911
|
+
metadata: runtime.metadata
|
|
39912
|
+
};
|
|
39913
|
+
return {
|
|
39914
|
+
projectId: artifactProjectId,
|
|
39915
|
+
driver,
|
|
39916
|
+
project: projectRow,
|
|
39917
|
+
expiresAt: Date.now() + this.cacheTTL
|
|
39918
|
+
};
|
|
39919
|
+
}
|
|
39920
|
+
};
|
|
39921
|
+
function extractRuntimeFromMetadata(metadata) {
|
|
39922
|
+
const datasources = metadata?.datasources;
|
|
39923
|
+
if (!Array.isArray(datasources) || datasources.length === 0) return void 0;
|
|
39924
|
+
const mapping = metadata?.datasourceMapping;
|
|
39925
|
+
let preferredName;
|
|
39926
|
+
if (mapping) {
|
|
39927
|
+
const def = mapping.find((m) => m?.default === true);
|
|
39928
|
+
if (def?.datasource) preferredName = def.datasource;
|
|
39929
|
+
}
|
|
39930
|
+
const ds = preferredName ? datasources.find((d) => d?.name === preferredName) : datasources[0];
|
|
39931
|
+
if (!ds || typeof ds !== "object") return void 0;
|
|
39932
|
+
const config = ds.config ?? {};
|
|
39933
|
+
const url = config.url ?? config.connectionString ?? config.connection ?? config.filename;
|
|
39934
|
+
const driver = ds.driver;
|
|
39935
|
+
if (typeof driver !== "string" || typeof url !== "string") return void 0;
|
|
39936
|
+
return {
|
|
39937
|
+
databaseDriver: driver,
|
|
39938
|
+
databaseUrl: url,
|
|
39939
|
+
databaseAuthToken: typeof config.authToken === "string" ? config.authToken : void 0
|
|
39940
|
+
};
|
|
39941
|
+
}
|
|
39942
|
+
async function createDriver(driverType, databaseUrl, authToken) {
|
|
39943
|
+
switch (driverType) {
|
|
39944
|
+
case "memory": {
|
|
39945
|
+
const { InMemoryDriver } = await import("@objectstack/driver-memory");
|
|
39946
|
+
const dbName = databaseUrl.replace(/^memory:\/\//, "").trim();
|
|
39947
|
+
const filePath = dbName ? resolvePathNode(process.cwd(), ".objectstack/data/projects", `${dbName}.json`) : void 0;
|
|
39948
|
+
return new InMemoryDriver({
|
|
39949
|
+
persistence: filePath ? { type: "file", path: filePath } : "file"
|
|
39950
|
+
});
|
|
39951
|
+
}
|
|
39952
|
+
case "sqlite":
|
|
39953
|
+
case "sql": {
|
|
39954
|
+
const filePath = databaseUrl.replace(/^file:/, "").replace(/^sql:\/\//, "");
|
|
39955
|
+
const { SqlDriver } = await import("@objectstack/driver-sql");
|
|
39956
|
+
return new SqlDriver({
|
|
39957
|
+
client: "better-sqlite3",
|
|
39958
|
+
connection: { filename: filePath },
|
|
39959
|
+
useNullAsDefault: true
|
|
39960
|
+
});
|
|
39961
|
+
}
|
|
39962
|
+
case "libsql":
|
|
39963
|
+
case "turso": {
|
|
39964
|
+
const { TursoDriver } = await import("@objectstack/driver-turso");
|
|
39965
|
+
return new TursoDriver({ url: databaseUrl, authToken });
|
|
39966
|
+
}
|
|
39967
|
+
case "postgres":
|
|
39968
|
+
case "postgresql":
|
|
39969
|
+
case "pg": {
|
|
39970
|
+
const { SqlDriver } = await import("@objectstack/driver-sql");
|
|
39971
|
+
return new SqlDriver({
|
|
39972
|
+
client: "pg",
|
|
39973
|
+
connection: databaseUrl,
|
|
39974
|
+
pool: { min: 0, max: 5 }
|
|
39975
|
+
});
|
|
39976
|
+
}
|
|
39977
|
+
case "mongodb":
|
|
39978
|
+
case "mongo": {
|
|
39979
|
+
const { MongoDBDriver: MongoDBDriver2 } = await Promise.resolve().then(() => (init_dist(), dist_exports));
|
|
39980
|
+
return new MongoDBDriver2({ url: databaseUrl });
|
|
39981
|
+
}
|
|
39982
|
+
default:
|
|
39983
|
+
throw new Error(`[ArtifactEnvironmentRegistry] Unsupported driver type: ${driverType}`);
|
|
39984
|
+
}
|
|
39985
|
+
}
|
|
39986
|
+
|
|
39987
|
+
// src/cloud/artifact-kernel-factory.ts
|
|
39988
|
+
init_driver_plugin();
|
|
39989
|
+
init_app_plugin();
|
|
39990
|
+
import { createHmac as createHmac2 } from "crypto";
|
|
39991
|
+
import { ObjectKernel as ObjectKernel3 } from "@objectstack/core";
|
|
39992
|
+
|
|
39993
|
+
// src/cloud/capability-loader.ts
|
|
39994
|
+
var CAPABILITY_PROVIDERS = {
|
|
39995
|
+
automation: {
|
|
39996
|
+
pkg: "@objectstack/service-automation",
|
|
39997
|
+
export: "AutomationServicePlugin",
|
|
39998
|
+
extras: [
|
|
39999
|
+
{ pkg: "@objectstack/service-automation", export: "CrudNodesPlugin" },
|
|
40000
|
+
{ pkg: "@objectstack/service-automation", export: "LogicNodesPlugin" },
|
|
40001
|
+
{ pkg: "@objectstack/service-automation", export: "HttpConnectorPlugin" },
|
|
40002
|
+
{ pkg: "@objectstack/service-automation", export: "ScreenNodesPlugin" }
|
|
40003
|
+
]
|
|
40004
|
+
},
|
|
40005
|
+
ai: {
|
|
40006
|
+
pkg: "@objectstack/service-ai",
|
|
40007
|
+
export: "AIServicePlugin"
|
|
40008
|
+
},
|
|
40009
|
+
analytics: {
|
|
40010
|
+
pkg: "@objectstack/service-analytics",
|
|
40011
|
+
export: "AnalyticsServicePlugin",
|
|
40012
|
+
configKey: "analyticsCubes"
|
|
40013
|
+
},
|
|
40014
|
+
audit: {
|
|
40015
|
+
pkg: "@objectstack/plugin-audit",
|
|
40016
|
+
export: "AuditPlugin"
|
|
40017
|
+
},
|
|
40018
|
+
cache: {
|
|
40019
|
+
pkg: "@objectstack/service-cache",
|
|
40020
|
+
export: "CacheServicePlugin"
|
|
40021
|
+
},
|
|
40022
|
+
storage: {
|
|
40023
|
+
pkg: "@objectstack/service-storage",
|
|
40024
|
+
export: "StorageServicePlugin"
|
|
40025
|
+
},
|
|
40026
|
+
queue: {
|
|
40027
|
+
pkg: "@objectstack/service-queue",
|
|
40028
|
+
export: "QueueServicePlugin"
|
|
40029
|
+
},
|
|
40030
|
+
job: {
|
|
40031
|
+
pkg: "@objectstack/service-job",
|
|
40032
|
+
export: "JobServicePlugin"
|
|
40033
|
+
},
|
|
40034
|
+
realtime: {
|
|
40035
|
+
pkg: "@objectstack/service-realtime",
|
|
40036
|
+
export: "RealtimeServicePlugin"
|
|
40037
|
+
},
|
|
40038
|
+
feed: {
|
|
40039
|
+
pkg: "@objectstack/service-feed",
|
|
40040
|
+
export: "FeedServicePlugin"
|
|
40041
|
+
},
|
|
40042
|
+
settings: {
|
|
40043
|
+
pkg: "@objectstack/service-settings",
|
|
40044
|
+
export: "SettingsServicePlugin"
|
|
40045
|
+
}
|
|
40046
|
+
};
|
|
40047
|
+
async function loadCapabilities(opts) {
|
|
40048
|
+
const { kernel, requires, bundle, projectId } = opts;
|
|
40049
|
+
const logger = opts.logger ?? console;
|
|
40050
|
+
const installed = [];
|
|
40051
|
+
for (const cap of requires) {
|
|
40052
|
+
const spec = CAPABILITY_PROVIDERS[cap];
|
|
40053
|
+
if (!spec) {
|
|
40054
|
+
continue;
|
|
40055
|
+
}
|
|
40056
|
+
try {
|
|
40057
|
+
const mod = await import(
|
|
40058
|
+
/* webpackIgnore: true */
|
|
40059
|
+
spec.pkg
|
|
40060
|
+
);
|
|
40061
|
+
const Ctor = mod[spec.export];
|
|
40062
|
+
if (!Ctor) {
|
|
40063
|
+
logger.warn?.(
|
|
40064
|
+
`[CapabilityLoader] '${cap}': package '${spec.pkg}' did not export '${spec.export}'`,
|
|
40065
|
+
{ projectId }
|
|
40066
|
+
);
|
|
40067
|
+
continue;
|
|
40068
|
+
}
|
|
40069
|
+
let arg;
|
|
40070
|
+
if (spec.configKey) {
|
|
40071
|
+
const v = bundle[spec.configKey];
|
|
40072
|
+
if (spec.configKey === "analyticsCubes") {
|
|
40073
|
+
arg = { cubes: Array.isArray(v) ? v : [] };
|
|
40074
|
+
} else if (v !== void 0) {
|
|
40075
|
+
arg = v;
|
|
40076
|
+
}
|
|
40077
|
+
}
|
|
40078
|
+
await kernel.use(arg !== void 0 ? new Ctor(arg) : new Ctor());
|
|
40079
|
+
installed.push(spec.export);
|
|
40080
|
+
if (spec.extras) {
|
|
40081
|
+
for (const ex of spec.extras) {
|
|
40082
|
+
try {
|
|
40083
|
+
const exMod = await import(
|
|
40084
|
+
/* webpackIgnore: true */
|
|
40085
|
+
ex.pkg
|
|
40086
|
+
);
|
|
40087
|
+
const ExCtor = exMod[ex.export];
|
|
40088
|
+
if (ExCtor) {
|
|
40089
|
+
await kernel.use(new ExCtor());
|
|
40090
|
+
installed.push(ex.export);
|
|
40091
|
+
}
|
|
40092
|
+
} catch {
|
|
40093
|
+
}
|
|
40094
|
+
}
|
|
40095
|
+
}
|
|
40096
|
+
logger.info?.(
|
|
40097
|
+
`[CapabilityLoader] '${cap}' installed (${spec.export}${spec.extras ? " + " + spec.extras.length + " extras" : ""})`,
|
|
40098
|
+
{ projectId }
|
|
40099
|
+
);
|
|
40100
|
+
} catch (err) {
|
|
40101
|
+
const msg = err?.message ?? String(err);
|
|
40102
|
+
if (msg.includes("Cannot find module") || msg.includes("ERR_MODULE_NOT_FOUND")) {
|
|
40103
|
+
logger.warn?.(
|
|
40104
|
+
`[CapabilityLoader] '${cap}' requested but '${spec.pkg}' not installed in host \u2014 skipped`,
|
|
40105
|
+
{ projectId }
|
|
40106
|
+
);
|
|
40107
|
+
} else {
|
|
40108
|
+
logger.error?.(
|
|
40109
|
+
`[CapabilityLoader] '${cap}' load failed: ${msg}`,
|
|
40110
|
+
{ projectId }
|
|
40111
|
+
);
|
|
40112
|
+
}
|
|
40113
|
+
}
|
|
40114
|
+
}
|
|
40115
|
+
return installed;
|
|
40116
|
+
}
|
|
40117
|
+
|
|
40118
|
+
// src/cloud/artifact-kernel-factory.ts
|
|
40119
|
+
init_platform_sso();
|
|
40120
|
+
function deriveProjectAuthSecret(baseSecret, projectId) {
|
|
40121
|
+
return createHmac2("sha256", baseSecret).update(`project:${projectId}`).digest("hex");
|
|
40122
|
+
}
|
|
40123
|
+
var ArtifactKernelFactory = class {
|
|
40124
|
+
constructor(config) {
|
|
40125
|
+
this.client = config.client;
|
|
40126
|
+
this.envRegistry = config.envRegistry;
|
|
40127
|
+
this.logger = config.logger ?? console;
|
|
40128
|
+
this.kernelConfig = config.kernelConfig;
|
|
40129
|
+
this.authBaseSecret = (config.authBaseSecret ?? process.env.OS_AUTH_SECRET ?? process.env.AUTH_SECRET ?? "").trim();
|
|
40130
|
+
}
|
|
40131
|
+
async create(projectId) {
|
|
40132
|
+
let cached = this.envRegistry.peekById(projectId);
|
|
40133
|
+
if (!cached) {
|
|
40134
|
+
const driver2 = await this.envRegistry.resolveById(projectId);
|
|
40135
|
+
if (!driver2) {
|
|
40136
|
+
throw new Error(`[ArtifactKernelFactory] Could not resolve driver for project '${projectId}'`);
|
|
40137
|
+
}
|
|
40138
|
+
cached = this.envRegistry.peekById(projectId);
|
|
40139
|
+
if (!cached) {
|
|
40140
|
+
throw new Error(`[ArtifactKernelFactory] envRegistry returned a driver but no cached entry for '${projectId}'`);
|
|
40141
|
+
}
|
|
40142
|
+
}
|
|
40143
|
+
const driver = cached.driver;
|
|
40144
|
+
const project = cached.project;
|
|
40145
|
+
const artifact = await this.client.fetchArtifact(projectId);
|
|
40146
|
+
if (!artifact) {
|
|
40147
|
+
throw new Error(`[ArtifactKernelFactory] Artifact not available for project '${projectId}'`);
|
|
40148
|
+
}
|
|
40149
|
+
const { ObjectQLPlugin } = await import("@objectstack/objectql");
|
|
40150
|
+
const { MetadataPlugin } = await import("@objectstack/metadata");
|
|
40151
|
+
const kernel = new ObjectKernel3(this.kernelConfig);
|
|
40152
|
+
await kernel.use(new DriverPlugin(driver, { datasourceName: "cloud" }));
|
|
40153
|
+
await kernel.use(new ObjectQLPlugin({ projectId, skipSchemaSync: false }));
|
|
40154
|
+
await kernel.use(new MetadataPlugin({
|
|
40155
|
+
watch: false,
|
|
40156
|
+
projectId,
|
|
40157
|
+
organizationId: project.organization_id,
|
|
40158
|
+
// ADR-0005: customization overlays (user-created views, dashboards,
|
|
40159
|
+
// edited objects, ...) are persisted by
|
|
40160
|
+
// ObjectStackProtocolImplementation.saveMetaItem on whichever
|
|
40161
|
+
// engine the protocol is attached to. For per-project kernels that
|
|
40162
|
+
// means the project's own DB, so the sys_metadata + history tables
|
|
40163
|
+
// MUST be provisioned here. The previous `false` setting caused
|
|
40164
|
+
// "no such table: sys_metadata" errors on any PUT /api/v1/meta/*
|
|
40165
|
+
// call (e.g. Studio "Create View") against a project deployment.
|
|
40166
|
+
registerSystemObjects: true
|
|
40167
|
+
}));
|
|
40168
|
+
if (this.authBaseSecret) {
|
|
40169
|
+
try {
|
|
40170
|
+
const { AuthPlugin } = await import("@objectstack/plugin-auth");
|
|
40171
|
+
const projectSecret = deriveProjectAuthSecret(this.authBaseSecret, projectId);
|
|
40172
|
+
const baseUrl = project.hostname ? project.hostname.startsWith("http") ? project.hostname : /(\.|^)localhost(:\d+)?$/i.test(project.hostname) ? (() => {
|
|
40173
|
+
const runtimePort = (process.env.OS_RUNTIME_PORT ?? "").trim();
|
|
40174
|
+
const hasPort = /:\d+$/.test(project.hostname);
|
|
40175
|
+
const hostWithPort = hasPort || !runtimePort ? project.hostname : `${project.hostname}:${runtimePort}`;
|
|
40176
|
+
return `http://${hostWithPort}`;
|
|
40177
|
+
})() : `https://${project.hostname}` : void 0;
|
|
40178
|
+
const trustedOriginsList = [];
|
|
40179
|
+
if (baseUrl) trustedOriginsList.push(baseUrl);
|
|
40180
|
+
const platformOrigins = (process.env.OS_TRUSTED_ORIGINS ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
40181
|
+
for (const o of platformOrigins) {
|
|
40182
|
+
if (!trustedOriginsList.includes(o)) trustedOriginsList.push(o);
|
|
40183
|
+
}
|
|
40184
|
+
const rootDomain = (process.env.OS_ROOT_DOMAIN ?? "").trim().replace(/^https?:\/\//, "");
|
|
40185
|
+
if (rootDomain) {
|
|
40186
|
+
const wildcard = `https://*.${rootDomain}`;
|
|
40187
|
+
if (!trustedOriginsList.includes(wildcard)) trustedOriginsList.push(wildcard);
|
|
40188
|
+
}
|
|
40189
|
+
if (project.hostname) {
|
|
40190
|
+
const bareHost = project.hostname.replace(/^https?:\/\//, "");
|
|
40191
|
+
if (bareHost.endsWith(".localhost") || bareHost === "localhost") {
|
|
40192
|
+
trustedOriginsList.push(`http://${bareHost}`);
|
|
40193
|
+
trustedOriginsList.push(`http://${bareHost}:*`);
|
|
40194
|
+
trustedOriginsList.push(`https://${bareHost}:*`);
|
|
40195
|
+
}
|
|
40196
|
+
}
|
|
40197
|
+
const platformSsoEnabled = String(
|
|
40198
|
+
process.env.OS_PLATFORM_SSO ?? "true"
|
|
40199
|
+
).toLowerCase() !== "false";
|
|
40200
|
+
const cloudBaseUrl = (process.env.OS_CLOUD_URL ?? "").trim().replace(/\/+$/, "");
|
|
40201
|
+
const oidcProviders = platformSsoEnabled && cloudBaseUrl && /^https?:\/\//.test(cloudBaseUrl) ? [{
|
|
40202
|
+
providerId: PLATFORM_SSO_PROVIDER_ID,
|
|
40203
|
+
name: "ObjectStack",
|
|
40204
|
+
discoveryUrl: `${cloudBaseUrl}/.well-known/openid-configuration`,
|
|
40205
|
+
clientId: derivePlatformSsoClientId(projectId),
|
|
40206
|
+
clientSecret: derivePlatformSsoClientSecret(this.authBaseSecret, projectId),
|
|
40207
|
+
scopes: ["openid", "email", "profile"]
|
|
40208
|
+
}] : void 0;
|
|
40209
|
+
await kernel.use(new AuthPlugin({
|
|
40210
|
+
secret: projectSecret,
|
|
40211
|
+
baseUrl,
|
|
40212
|
+
// Project kernel has no http-server (host owns it). The
|
|
40213
|
+
// dispatcher's handleAuth path resolves `auth` via
|
|
40214
|
+
// getService and invokes the handler directly — route
|
|
40215
|
+
// registration is unnecessary and would warn.
|
|
40216
|
+
registerRoutes: false,
|
|
40217
|
+
// Identity tables live in the project's own DB — keep
|
|
40218
|
+
// sys_user/sys_session local to this kernel.
|
|
40219
|
+
manifestDatasource: "default",
|
|
40220
|
+
// Cookie scope: default to the project's own host. We
|
|
40221
|
+
// intentionally do NOT pass crossSubDomainCookies here
|
|
40222
|
+
// so cookies stay isolated per project subdomain.
|
|
40223
|
+
trustedOrigins: trustedOriginsList.length ? trustedOriginsList : void 0,
|
|
40224
|
+
...oidcProviders ? { oidcProviders } : {}
|
|
40225
|
+
}));
|
|
40226
|
+
if (oidcProviders) {
|
|
40227
|
+
this.logger.info?.("[ArtifactKernelFactory] platform SSO wired", {
|
|
40228
|
+
projectId,
|
|
40229
|
+
cloudBaseUrl
|
|
40230
|
+
});
|
|
40231
|
+
}
|
|
40232
|
+
} catch (err) {
|
|
40233
|
+
this.logger.warn?.("[ArtifactKernelFactory] AuthPlugin not registered", {
|
|
40234
|
+
projectId,
|
|
40235
|
+
error: err?.message
|
|
40236
|
+
});
|
|
40237
|
+
}
|
|
40238
|
+
} else {
|
|
40239
|
+
this.logger.warn?.("[ArtifactKernelFactory] OS_AUTH_SECRET not set \u2014 per-project AuthPlugin skipped (auth endpoints will return 404)", { projectId });
|
|
40240
|
+
}
|
|
40241
|
+
try {
|
|
40242
|
+
const { SecurityPlugin } = await import("@objectstack/plugin-security");
|
|
40243
|
+
const multiTenant = String(process.env.OS_MULTI_TENANT ?? "true").toLowerCase() !== "false";
|
|
40244
|
+
await kernel.use(new SecurityPlugin({ multiTenant }));
|
|
40245
|
+
} catch (err) {
|
|
40246
|
+
this.logger.warn?.("[ArtifactKernelFactory] SecurityPlugin not registered", {
|
|
40247
|
+
projectId,
|
|
40248
|
+
error: err?.message
|
|
40249
|
+
});
|
|
40250
|
+
}
|
|
40251
|
+
const projectName = project.hostname ?? projectId;
|
|
40252
|
+
const bundle = artifact.metadata;
|
|
40253
|
+
const sys = bundle?.manifest ?? bundle;
|
|
40254
|
+
const packageId = sys?.packageId ?? sys?.package_id ?? bundle?.packageId;
|
|
40255
|
+
const i18nCfg = bundle?.i18n ?? sys?.i18n ?? {};
|
|
40256
|
+
const trArr = Array.isArray(bundle?.translations) ? bundle.translations : Array.isArray(sys?.translations) ? sys.translations : [];
|
|
40257
|
+
try {
|
|
40258
|
+
const { I18nServicePlugin } = await import("@objectstack/service-i18n");
|
|
40259
|
+
await kernel.use(new I18nServicePlugin({
|
|
40260
|
+
defaultLocale: i18nCfg.defaultLocale,
|
|
40261
|
+
fallbackLocale: i18nCfg.fallbackLocale ?? i18nCfg.defaultLocale ?? "en",
|
|
40262
|
+
// Routes are dispatched by HttpDispatcher.handleI18n via
|
|
40263
|
+
// kernel.getService('i18n'); the host worker owns the
|
|
40264
|
+
// HTTP server. Skip self-registration to avoid warnings.
|
|
40265
|
+
registerRoutes: false
|
|
40266
|
+
}));
|
|
40267
|
+
console.warn(
|
|
40268
|
+
`[ArtifactKernelFactory] I18nServicePlugin registered (project=${projectId}, translations=${trArr.length}, defaultLocale=${i18nCfg.defaultLocale ?? "en"})`
|
|
40269
|
+
);
|
|
40270
|
+
} catch (err) {
|
|
40271
|
+
this.logger.warn?.("[ArtifactKernelFactory] I18nServicePlugin not registered", {
|
|
40272
|
+
projectId,
|
|
40273
|
+
error: err?.message
|
|
40274
|
+
});
|
|
40275
|
+
}
|
|
40276
|
+
const requiresRaw = (Array.isArray(bundle?.requires) ? bundle.requires : null) ?? (Array.isArray(sys?.requires) ? sys.requires : null) ?? [];
|
|
40277
|
+
const requires = requiresRaw.filter((x) => typeof x === "string" && x.length > 0);
|
|
40278
|
+
if (requires.length > 0) {
|
|
40279
|
+
const installed = await loadCapabilities({
|
|
40280
|
+
kernel,
|
|
40281
|
+
requires,
|
|
40282
|
+
bundle: { ...bundle ?? {}, ...sys ?? {} },
|
|
40283
|
+
logger: this.logger,
|
|
40284
|
+
projectId
|
|
40285
|
+
});
|
|
40286
|
+
this.logger.info?.("[ArtifactKernelFactory] capabilities loaded", {
|
|
40287
|
+
projectId,
|
|
40288
|
+
requires,
|
|
40289
|
+
installed
|
|
40290
|
+
});
|
|
40291
|
+
}
|
|
40292
|
+
await kernel.use(new AppPlugin(bundle, {
|
|
40293
|
+
projectId,
|
|
40294
|
+
organizationId: project.organization_id ?? "",
|
|
40295
|
+
projectName,
|
|
40296
|
+
packageId,
|
|
40297
|
+
source: packageId ? "package" : "user"
|
|
40298
|
+
}));
|
|
40299
|
+
await kernel.bootstrap();
|
|
40300
|
+
try {
|
|
40301
|
+
const projMeta = typeof project?.metadata === "string" ? JSON.parse(project.metadata) : project?.metadata ?? {};
|
|
40302
|
+
const ownerSeed = projMeta?.ownerSeed;
|
|
40303
|
+
const orgSeed = projMeta?.orgSeed;
|
|
40304
|
+
if (orgSeed?.id && orgSeed?.name) {
|
|
40305
|
+
try {
|
|
40306
|
+
const { seedProjectOrganization: seedProjectOrganization2 } = await Promise.resolve().then(() => (init_project_org_seed(), project_org_seed_exports));
|
|
40307
|
+
await seedProjectOrganization2(kernel, orgSeed, this.logger);
|
|
40308
|
+
} catch (e) {
|
|
40309
|
+
this.logger.warn?.("[ArtifactKernelFactory] orgSeed threw", {
|
|
40310
|
+
projectId,
|
|
40311
|
+
error: e?.message
|
|
40312
|
+
});
|
|
40313
|
+
}
|
|
40314
|
+
}
|
|
40315
|
+
if (ownerSeed?.userId && ownerSeed?.email) {
|
|
40316
|
+
try {
|
|
40317
|
+
const { seedProjectOwner: seedProjectOwner2 } = await Promise.resolve().then(() => (init_project_owner_seed(), project_owner_seed_exports));
|
|
40318
|
+
await seedProjectOwner2(kernel, ownerSeed, this.logger);
|
|
40319
|
+
} catch (e) {
|
|
40320
|
+
this.logger.warn?.("[ArtifactKernelFactory] ownerSeed threw", {
|
|
40321
|
+
projectId,
|
|
40322
|
+
error: e?.message
|
|
40323
|
+
});
|
|
40324
|
+
}
|
|
40325
|
+
if (orgSeed?.id) {
|
|
40326
|
+
try {
|
|
40327
|
+
const { seedProjectMember: seedProjectMember2 } = await Promise.resolve().then(() => (init_project_org_seed(), project_org_seed_exports));
|
|
40328
|
+
await seedProjectMember2(
|
|
40329
|
+
kernel,
|
|
40330
|
+
{ userId: ownerSeed.userId, organizationId: orgSeed.id, role: "owner" },
|
|
40331
|
+
this.logger
|
|
40332
|
+
);
|
|
40333
|
+
} catch (e) {
|
|
40334
|
+
this.logger.warn?.("[ArtifactKernelFactory] memberSeed threw", {
|
|
40335
|
+
projectId,
|
|
40336
|
+
error: e?.message
|
|
40337
|
+
});
|
|
40338
|
+
}
|
|
40339
|
+
}
|
|
40340
|
+
}
|
|
40341
|
+
} catch (err) {
|
|
40342
|
+
this.logger.warn?.("[ArtifactKernelFactory] owner/org seed skipped", {
|
|
40343
|
+
projectId,
|
|
40344
|
+
error: err?.message
|
|
40345
|
+
});
|
|
40346
|
+
}
|
|
40347
|
+
try {
|
|
40348
|
+
const datasetsNow = (() => {
|
|
40349
|
+
try {
|
|
40350
|
+
return kernel.getService?.("seed-datasets");
|
|
40351
|
+
} catch {
|
|
40352
|
+
return void 0;
|
|
40353
|
+
}
|
|
40354
|
+
})();
|
|
40355
|
+
const replayer = (() => {
|
|
40356
|
+
try {
|
|
40357
|
+
return kernel.getService?.("seed-replayer");
|
|
40358
|
+
} catch {
|
|
40359
|
+
return void 0;
|
|
40360
|
+
}
|
|
40361
|
+
})();
|
|
40362
|
+
if (Array.isArray(datasetsNow) && datasetsNow.length > 0 && typeof replayer === "function") {
|
|
40363
|
+
const projMetaRaw = project?.metadata;
|
|
40364
|
+
const projMeta = typeof projMetaRaw === "string" ? (() => {
|
|
40365
|
+
try {
|
|
40366
|
+
return JSON.parse(projMetaRaw);
|
|
40367
|
+
} catch {
|
|
40368
|
+
return {};
|
|
40369
|
+
}
|
|
40370
|
+
})() : projMetaRaw ?? {};
|
|
40371
|
+
let primaryOrgId = projMeta?.orgSeed?.id;
|
|
40372
|
+
if (!primaryOrgId) {
|
|
40373
|
+
try {
|
|
40374
|
+
const ql = kernel.getService?.("objectql");
|
|
40375
|
+
if (ql?.find) {
|
|
40376
|
+
const rows = await ql.find("sys_organization", { limit: 5, orderBy: [{ field: "created_at", direction: "asc" }] });
|
|
40377
|
+
const list = Array.isArray(rows) ? rows : rows?.value ?? rows?.records ?? [];
|
|
40378
|
+
if (Array.isArray(list) && list.length > 0 && list[0]?.id) {
|
|
40379
|
+
primaryOrgId = String(list[0].id);
|
|
40380
|
+
}
|
|
40381
|
+
}
|
|
40382
|
+
} catch {
|
|
40383
|
+
}
|
|
40384
|
+
}
|
|
40385
|
+
if (primaryOrgId) {
|
|
40386
|
+
try {
|
|
40387
|
+
const summary = await replayer(primaryOrgId);
|
|
40388
|
+
const inserted = summary?.inserted ?? 0;
|
|
40389
|
+
const updated = summary?.updated ?? 0;
|
|
40390
|
+
const errs = summary?.errors?.length ?? 0;
|
|
40391
|
+
if (inserted > 0 || updated > 0 || errs > 0) {
|
|
40392
|
+
this.logger.info?.("[ArtifactKernelFactory] post-bootstrap seed replay", {
|
|
40393
|
+
projectId,
|
|
40394
|
+
organizationId: primaryOrgId,
|
|
40395
|
+
datasets: datasetsNow.length,
|
|
40396
|
+
inserted,
|
|
40397
|
+
updated,
|
|
40398
|
+
errors: errs
|
|
40399
|
+
});
|
|
40400
|
+
}
|
|
40401
|
+
} catch (e) {
|
|
40402
|
+
this.logger.warn?.("[ArtifactKernelFactory] post-bootstrap seed replay failed", {
|
|
40403
|
+
projectId,
|
|
40404
|
+
organizationId: primaryOrgId,
|
|
40405
|
+
error: e?.message
|
|
40406
|
+
});
|
|
40407
|
+
}
|
|
40408
|
+
}
|
|
40409
|
+
}
|
|
40410
|
+
} catch (err) {
|
|
40411
|
+
this.logger.warn?.("[ArtifactKernelFactory] post-bootstrap seed step threw", {
|
|
40412
|
+
projectId,
|
|
40413
|
+
error: err?.message
|
|
40414
|
+
});
|
|
40415
|
+
}
|
|
40416
|
+
let i18nSvc = null;
|
|
40417
|
+
try {
|
|
40418
|
+
i18nSvc = kernel.getService?.("i18n");
|
|
40419
|
+
} catch {
|
|
40420
|
+
i18nSvc = null;
|
|
40421
|
+
}
|
|
40422
|
+
try {
|
|
40423
|
+
if (i18nSvc && typeof i18nSvc.loadTranslations === "function") {
|
|
40424
|
+
if (i18nCfg.defaultLocale && typeof i18nSvc.setDefaultLocale === "function") {
|
|
40425
|
+
i18nSvc.setDefaultLocale(i18nCfg.defaultLocale);
|
|
40426
|
+
}
|
|
40427
|
+
let loaded = 0;
|
|
40428
|
+
for (const tbundle of trArr) {
|
|
40429
|
+
if (!tbundle || typeof tbundle !== "object") continue;
|
|
40430
|
+
for (const [locale, data] of Object.entries(tbundle)) {
|
|
40431
|
+
if (data && typeof data === "object") {
|
|
40432
|
+
try {
|
|
40433
|
+
i18nSvc.loadTranslations(locale, data);
|
|
40434
|
+
loaded++;
|
|
40435
|
+
} catch (err) {
|
|
40436
|
+
this.logger.warn?.("[ArtifactKernelFactory] i18n loadTranslations failed", {
|
|
40437
|
+
projectId,
|
|
40438
|
+
locale,
|
|
40439
|
+
error: err?.message
|
|
40440
|
+
});
|
|
40441
|
+
}
|
|
40442
|
+
}
|
|
40443
|
+
}
|
|
40444
|
+
}
|
|
40445
|
+
if (loaded > 0) {
|
|
40446
|
+
this.logger.info?.("[ArtifactKernelFactory] i18n direct-load complete", {
|
|
40447
|
+
projectId,
|
|
40448
|
+
locales: loaded,
|
|
40449
|
+
bundles: trArr.length
|
|
40450
|
+
});
|
|
40451
|
+
}
|
|
40452
|
+
}
|
|
40453
|
+
} catch (err) {
|
|
40454
|
+
this.logger.warn?.("[ArtifactKernelFactory] i18n direct-load failed", {
|
|
40455
|
+
projectId,
|
|
40456
|
+
error: err?.message
|
|
40457
|
+
});
|
|
40458
|
+
}
|
|
40459
|
+
this.logger.info?.("[ArtifactKernelFactory] kernel ready", {
|
|
40460
|
+
projectId,
|
|
40461
|
+
commitId: artifact.commitId,
|
|
40462
|
+
checksum: artifact.checksum,
|
|
40463
|
+
authEnabled: Boolean(this.authBaseSecret)
|
|
40464
|
+
});
|
|
40465
|
+
return kernel;
|
|
40466
|
+
}
|
|
40467
|
+
};
|
|
40468
|
+
|
|
40469
|
+
// src/cloud/auth-proxy-plugin.ts
|
|
40470
|
+
var AUTH_PREFIX = "/api/v1/auth";
|
|
40471
|
+
function pickHandler(svc) {
|
|
40472
|
+
if (!svc) return void 0;
|
|
40473
|
+
if (typeof svc.handleRequest === "function") return svc.handleRequest.bind(svc);
|
|
40474
|
+
if (typeof svc.handler === "function") return svc.handler.bind(svc);
|
|
40475
|
+
if (svc.api && typeof svc.api.handler === "function") return svc.api.handler.bind(svc.api);
|
|
40476
|
+
if (svc.auth && typeof svc.auth.handler === "function") return svc.auth.handler.bind(svc.auth);
|
|
40477
|
+
return void 0;
|
|
40478
|
+
}
|
|
40479
|
+
async function resolveAuthHandler(svc) {
|
|
40480
|
+
const direct = pickHandler(svc);
|
|
40481
|
+
if (direct) return direct;
|
|
40482
|
+
if (typeof svc?.getApi === "function") {
|
|
40483
|
+
try {
|
|
40484
|
+
const api = await svc.getApi();
|
|
40485
|
+
return pickHandler(api) ?? pickHandler({ api });
|
|
40486
|
+
} catch {
|
|
40487
|
+
return void 0;
|
|
40488
|
+
}
|
|
40489
|
+
}
|
|
40490
|
+
return void 0;
|
|
40491
|
+
}
|
|
40492
|
+
var AuthProxyPlugin = class {
|
|
40493
|
+
constructor() {
|
|
40494
|
+
this.name = "com.objectstack.runtime.auth-proxy";
|
|
40495
|
+
this.version = "1.0.0";
|
|
40496
|
+
this.init = async (_ctx) => {
|
|
40497
|
+
};
|
|
40498
|
+
this.start = async (ctx) => {
|
|
40499
|
+
ctx.hook("kernel:ready", async () => {
|
|
40500
|
+
let httpServer;
|
|
40501
|
+
try {
|
|
40502
|
+
httpServer = ctx.getService("http-server");
|
|
40503
|
+
} catch {
|
|
40504
|
+
ctx.logger?.warn?.("[AuthProxyPlugin] http-server not available \u2014 auth routes not mounted");
|
|
40505
|
+
return;
|
|
40506
|
+
}
|
|
40507
|
+
if (!httpServer || typeof httpServer.getRawApp !== "function") {
|
|
40508
|
+
ctx.logger?.warn?.("[AuthProxyPlugin] http-server missing getRawApp() \u2014 auth routes not mounted");
|
|
40509
|
+
return;
|
|
40510
|
+
}
|
|
40511
|
+
const rawApp = httpServer.getRawApp();
|
|
40512
|
+
const kernelManager = ctx.getService("kernel-manager");
|
|
40513
|
+
const envRegistry = ctx.getService("env-registry");
|
|
40514
|
+
const handler = async (c) => {
|
|
40515
|
+
try {
|
|
40516
|
+
const url = new URL(c.req.url);
|
|
40517
|
+
const host = url.hostname;
|
|
40518
|
+
let projectId;
|
|
40519
|
+
try {
|
|
40520
|
+
const env = await envRegistry.resolveByHostname(host);
|
|
40521
|
+
projectId = env?.projectId;
|
|
40522
|
+
} catch {
|
|
40523
|
+
}
|
|
40524
|
+
if (!projectId) {
|
|
40525
|
+
return c.json({ error: "project_not_found", host }, 404);
|
|
40526
|
+
}
|
|
40527
|
+
const projectKernel = await kernelManager.getOrCreate(projectId);
|
|
40528
|
+
let authSvc;
|
|
40529
|
+
try {
|
|
40530
|
+
authSvc = await projectKernel.getServiceAsync?.("auth");
|
|
40531
|
+
} catch {
|
|
40532
|
+
authSvc = void 0;
|
|
40533
|
+
}
|
|
40534
|
+
if (!authSvc) {
|
|
40535
|
+
try {
|
|
40536
|
+
authSvc = projectKernel.getService?.("auth");
|
|
40537
|
+
} catch {
|
|
40538
|
+
}
|
|
40539
|
+
}
|
|
40540
|
+
const subPath = url.pathname.startsWith(AUTH_PREFIX + "/") ? url.pathname.substring(AUTH_PREFIX.length + 1) : "";
|
|
40541
|
+
if (c.req.method === "GET" && (subPath === "config" || subPath === "bootstrap-status")) {
|
|
40542
|
+
if (subPath === "config") {
|
|
40543
|
+
try {
|
|
40544
|
+
const config = typeof authSvc?.getPublicConfig === "function" ? authSvc.getPublicConfig() : null;
|
|
40545
|
+
if (config) {
|
|
40546
|
+
return c.json({ success: true, data: config });
|
|
40547
|
+
}
|
|
40548
|
+
return c.json({ success: false, error: { code: "auth_config_unavailable", message: "AuthManager has no getPublicConfig()" } }, 503);
|
|
40549
|
+
} catch (e) {
|
|
40550
|
+
return c.json({ success: false, error: { code: "auth_config_error", message: String(e?.message ?? e) } }, 500);
|
|
40551
|
+
}
|
|
40552
|
+
}
|
|
40553
|
+
try {
|
|
40554
|
+
try {
|
|
40555
|
+
const pubCfg = typeof authSvc?.getPublicConfig === "function" ? authSvc.getPublicConfig() : null;
|
|
40556
|
+
const ssoProviders = Array.isArray(pubCfg?.socialProviders) ? pubCfg.socialProviders : [];
|
|
40557
|
+
const ssoWired = ssoProviders.some(
|
|
40558
|
+
(p) => p?.enabled !== false && p?.id === "objectstack-cloud"
|
|
40559
|
+
);
|
|
40560
|
+
if (ssoWired) {
|
|
40561
|
+
return c.json({ hasOwner: true });
|
|
40562
|
+
}
|
|
40563
|
+
} catch {
|
|
40564
|
+
}
|
|
40565
|
+
const dataEngine = typeof authSvc?.getDataEngine === "function" ? authSvc.getDataEngine() : null;
|
|
40566
|
+
if (!dataEngine || typeof dataEngine.count !== "function") {
|
|
40567
|
+
return c.json({ hasOwner: true });
|
|
40568
|
+
}
|
|
40569
|
+
const count = await dataEngine.count("sys_user", {});
|
|
40570
|
+
return c.json({ hasOwner: (count ?? 0) > 0 });
|
|
40571
|
+
} catch {
|
|
40572
|
+
return c.json({ hasOwner: true });
|
|
40573
|
+
}
|
|
40574
|
+
}
|
|
40575
|
+
const fn = await resolveAuthHandler(authSvc);
|
|
40576
|
+
if (!fn) {
|
|
40577
|
+
return c.json({ error: "auth_service_unavailable", projectId }, 503);
|
|
40578
|
+
}
|
|
40579
|
+
const resp = await fn(c.req.raw);
|
|
40580
|
+
const rootDomain = process.env.OS_ROOT_DOMAIN || "";
|
|
40581
|
+
if (rootDomain) {
|
|
40582
|
+
const leakyDomain = rootDomain.startsWith(".") ? rootDomain : `.${rootDomain}`;
|
|
40583
|
+
const leakyNames = [
|
|
40584
|
+
"__Secure-better-auth.session_token",
|
|
40585
|
+
"better-auth.session_token",
|
|
40586
|
+
"__Secure-better-auth.state",
|
|
40587
|
+
"better-auth.state",
|
|
40588
|
+
"__Secure-better-auth.csrf_token",
|
|
40589
|
+
"better-auth.csrf_token"
|
|
40590
|
+
];
|
|
40591
|
+
try {
|
|
40592
|
+
for (const n of leakyNames) {
|
|
40593
|
+
const isSecure = n.startsWith("__Secure-");
|
|
40594
|
+
const attrs = `Max-Age=0; Path=/; Domain=${leakyDomain}; SameSite=Lax${isSecure ? "; Secure" : ""}`;
|
|
40595
|
+
resp.headers?.append?.("Set-Cookie", `${n}=; ${attrs}`);
|
|
40596
|
+
}
|
|
40597
|
+
} catch {
|
|
40598
|
+
}
|
|
40599
|
+
}
|
|
40600
|
+
return resp;
|
|
40601
|
+
} catch (err) {
|
|
40602
|
+
ctx.logger?.error?.("[AuthProxyPlugin] auth dispatch failed", {
|
|
40603
|
+
error: err?.message,
|
|
40604
|
+
stack: err?.stack
|
|
40605
|
+
});
|
|
40606
|
+
return c.json({
|
|
40607
|
+
error: "auth_dispatch_failed",
|
|
40608
|
+
message: err?.message ?? String(err)
|
|
40609
|
+
}, 500);
|
|
40610
|
+
}
|
|
40611
|
+
};
|
|
40612
|
+
if (typeof rawApp.all === "function") {
|
|
40613
|
+
rawApp.all(`${AUTH_PREFIX}/*`, handler);
|
|
40614
|
+
} else {
|
|
40615
|
+
for (const m of ["get", "post", "put", "delete", "patch", "options"]) {
|
|
40616
|
+
try {
|
|
40617
|
+
rawApp[m]?.(`${AUTH_PREFIX}/*`, handler);
|
|
40618
|
+
} catch {
|
|
40619
|
+
}
|
|
40620
|
+
}
|
|
40621
|
+
}
|
|
40622
|
+
ctx.logger?.info?.(`[AuthProxyPlugin] auth proxy mounted at ${AUTH_PREFIX}/*`);
|
|
40623
|
+
});
|
|
40624
|
+
};
|
|
40625
|
+
}
|
|
40626
|
+
};
|
|
40627
|
+
|
|
40628
|
+
// src/cloud/file-artifact-api-client.ts
|
|
40629
|
+
import { readFile as readFile2, stat } from "fs/promises";
|
|
40630
|
+
import { resolve as resolvePath4 } from "path";
|
|
40631
|
+
var FileArtifactApiClient = class {
|
|
40632
|
+
constructor(config = {}) {
|
|
40633
|
+
const cwd = process.cwd();
|
|
40634
|
+
this.artifactPath = resolvePath4(
|
|
40635
|
+
cwd,
|
|
40636
|
+
config.artifactPath ?? process.env.OS_ARTIFACT_PATH ?? "dist/objectstack.json"
|
|
40637
|
+
);
|
|
40638
|
+
this.projectId = config.projectId ?? process.env.OS_PROJECT_ID ?? "proj_local";
|
|
40639
|
+
this.organizationId = config.organizationId ?? process.env.OS_ORGANIZATION_ID ?? "org_local";
|
|
40640
|
+
this.overrideRuntime = config.runtime;
|
|
40641
|
+
this.watch = config.watch ?? true;
|
|
40642
|
+
this.logger = config.logger ?? console;
|
|
40643
|
+
}
|
|
40644
|
+
async resolveHostname(_host) {
|
|
40645
|
+
const runtime = this.overrideRuntime ?? await this.readRuntimeFromArtifact();
|
|
40646
|
+
return {
|
|
40647
|
+
projectId: this.projectId,
|
|
40648
|
+
organizationId: this.organizationId,
|
|
40649
|
+
...runtime ? { runtime } : {}
|
|
40650
|
+
};
|
|
40651
|
+
}
|
|
40652
|
+
async fetchArtifact(_projectId, _opts) {
|
|
40653
|
+
return this.loadArtifact();
|
|
40654
|
+
}
|
|
40655
|
+
async lookupProjectByShortId(_shortId) {
|
|
40656
|
+
return { projectId: this.projectId, organizationId: this.organizationId };
|
|
40657
|
+
}
|
|
40658
|
+
async fetchBranchHead(_projectId, _branchName) {
|
|
40659
|
+
const artifact = await this.loadArtifact();
|
|
40660
|
+
return artifact ? { commitId: artifact.commitId ?? "local", publishedAt: null } : null;
|
|
40661
|
+
}
|
|
40662
|
+
invalidate(_projectId) {
|
|
40663
|
+
this.cached = void 0;
|
|
40664
|
+
}
|
|
40665
|
+
clear() {
|
|
40666
|
+
this.cached = void 0;
|
|
40667
|
+
}
|
|
40668
|
+
async loadArtifact() {
|
|
40669
|
+
try {
|
|
40670
|
+
const stats = await stat(this.artifactPath);
|
|
40671
|
+
const mtimeMs = stats.mtimeMs;
|
|
40672
|
+
if (!this.watch && this.cached) return this.cached.response;
|
|
40673
|
+
if (this.cached && this.cached.mtimeMs === mtimeMs) return this.cached.response;
|
|
40674
|
+
const raw = await readFile2(this.artifactPath, "utf8");
|
|
40675
|
+
const parsed = JSON.parse(raw);
|
|
40676
|
+
const isEnvelope = parsed && typeof parsed === "object" && typeof parsed.metadata === "object" && parsed.metadata !== null;
|
|
40677
|
+
const metadata = isEnvelope ? parsed.metadata : parsed;
|
|
40678
|
+
const runtime = this.overrideRuntime ?? (isEnvelope ? parsed.runtime : void 0) ?? this.deriveRuntimeFromMetadata(metadata) ?? this.defaultLocalSqliteRuntime();
|
|
40679
|
+
const response = {
|
|
40680
|
+
schemaVersion: parsed.schemaVersion ?? "1",
|
|
40681
|
+
projectId: parsed.projectId ?? this.projectId,
|
|
40682
|
+
commitId: parsed.commitId ?? "local",
|
|
40683
|
+
checksum: parsed.checksum ?? "",
|
|
40684
|
+
publishedAt: parsed.publishedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
40685
|
+
metadata,
|
|
40686
|
+
functions: parsed.functions,
|
|
40687
|
+
manifest: parsed.manifest,
|
|
40688
|
+
runtime: {
|
|
40689
|
+
organizationId: this.organizationId,
|
|
40690
|
+
...runtime
|
|
40691
|
+
}
|
|
40692
|
+
};
|
|
40693
|
+
this.cached = { mtimeMs, response };
|
|
40694
|
+
return response;
|
|
40695
|
+
} catch (err) {
|
|
40696
|
+
this.logger.error?.("[FileArtifactApiClient] failed to load artifact", {
|
|
40697
|
+
artifactPath: this.artifactPath,
|
|
40698
|
+
error: err?.message ?? err
|
|
40699
|
+
});
|
|
40700
|
+
return null;
|
|
40701
|
+
}
|
|
40702
|
+
}
|
|
40703
|
+
async readRuntimeFromArtifact() {
|
|
40704
|
+
const artifact = await this.loadArtifact();
|
|
40705
|
+
return artifact?.runtime;
|
|
40706
|
+
}
|
|
40707
|
+
deriveRuntimeFromMetadata(metadata) {
|
|
40708
|
+
const datasources = metadata?.datasources;
|
|
40709
|
+
if (!Array.isArray(datasources) || datasources.length === 0) return void 0;
|
|
40710
|
+
const mapping = metadata?.datasourceMapping;
|
|
40711
|
+
let preferredName;
|
|
40712
|
+
if (mapping) {
|
|
40713
|
+
const def = mapping.find((m) => m?.default === true);
|
|
40714
|
+
if (def?.datasource) preferredName = def.datasource;
|
|
40715
|
+
}
|
|
40716
|
+
const ds = preferredName ? datasources.find((d) => d?.name === preferredName) ?? datasources[0] : datasources[0];
|
|
40717
|
+
if (!ds || typeof ds !== "object") return void 0;
|
|
40718
|
+
const config = ds.config ?? {};
|
|
40719
|
+
const url = config.url ?? config.connectionString ?? config.connection ?? config.filename;
|
|
40720
|
+
const driver = ds.driver;
|
|
40721
|
+
if (typeof driver !== "string" || typeof url !== "string") return void 0;
|
|
40722
|
+
return {
|
|
40723
|
+
databaseDriver: driver,
|
|
40724
|
+
databaseUrl: url,
|
|
40725
|
+
databaseAuthToken: typeof config.authToken === "string" ? config.authToken : void 0
|
|
40726
|
+
};
|
|
40727
|
+
}
|
|
40728
|
+
defaultLocalSqliteRuntime() {
|
|
40729
|
+
const cwd = process.cwd();
|
|
40730
|
+
const dbPath = resolvePath4(cwd, ".objectstack/data", `${this.projectId}.db`);
|
|
40731
|
+
return {
|
|
40732
|
+
databaseDriver: "sqlite",
|
|
40733
|
+
databaseUrl: `file:${dbPath}`
|
|
40734
|
+
};
|
|
40735
|
+
}
|
|
40736
|
+
};
|
|
40737
|
+
|
|
40738
|
+
// src/cloud/objectos-stack.ts
|
|
40739
|
+
async function createHostEnginePlugins() {
|
|
40740
|
+
const { ObjectQLPlugin } = await import("@objectstack/objectql");
|
|
40741
|
+
const { DriverPlugin: DriverPlugin2 } = await Promise.resolve().then(() => (init_driver_plugin(), driver_plugin_exports));
|
|
40742
|
+
const { MetadataPlugin } = await import("@objectstack/metadata");
|
|
40743
|
+
const { InMemoryDriver } = await import("@objectstack/driver-memory");
|
|
40744
|
+
const driver = new InMemoryDriver();
|
|
40745
|
+
const driverName = "memory";
|
|
40746
|
+
const oqlRef = { ql: null };
|
|
40747
|
+
const objectql = {
|
|
40748
|
+
name: "com.objectstack.engine.objectql",
|
|
40749
|
+
version: "0.0.0",
|
|
40750
|
+
async init(ctx) {
|
|
40751
|
+
const plugin = new ObjectQLPlugin();
|
|
40752
|
+
this._inner = plugin;
|
|
40753
|
+
if (plugin.init) await plugin.init(ctx);
|
|
40754
|
+
oqlRef.ql = plugin.ql ?? plugin;
|
|
40755
|
+
},
|
|
40756
|
+
async start(ctx) {
|
|
40757
|
+
const plugin = this._inner;
|
|
40758
|
+
if (plugin?.start) await plugin.start(ctx);
|
|
40759
|
+
},
|
|
40760
|
+
async destroy() {
|
|
40761
|
+
const plugin = this._inner;
|
|
40762
|
+
if (plugin?.destroy) await plugin.destroy();
|
|
40763
|
+
else if (plugin?.stop) await plugin.stop();
|
|
40764
|
+
}
|
|
40765
|
+
};
|
|
40766
|
+
const datasourceMapping = {
|
|
40767
|
+
name: "objectos-host-datasource-mapping",
|
|
40768
|
+
version: "0.0.0",
|
|
40769
|
+
dependencies: ["com.objectstack.engine.objectql"],
|
|
40770
|
+
async init() {
|
|
40771
|
+
const ql = oqlRef.ql;
|
|
40772
|
+
if (ql?.setDatasourceMapping) {
|
|
40773
|
+
ql.setDatasourceMapping([
|
|
40774
|
+
{ default: true, datasource: `com.objectstack.driver.${driverName}` }
|
|
40775
|
+
]);
|
|
40776
|
+
}
|
|
40777
|
+
}
|
|
40778
|
+
};
|
|
40779
|
+
const driverPlugin = new DriverPlugin2(driver, driverName);
|
|
40780
|
+
const metadata = new MetadataPlugin({
|
|
40781
|
+
watch: false,
|
|
40782
|
+
// The host kernel is a routing shell. It doesn't own metadata —
|
|
40783
|
+
// every per-project kernel registers its own.
|
|
40784
|
+
registerSystemObjects: false
|
|
40785
|
+
});
|
|
40786
|
+
return [objectql, datasourceMapping, driverPlugin, metadata];
|
|
40787
|
+
}
|
|
40788
|
+
var ObjectOSProjectPlugin = class {
|
|
40789
|
+
constructor(config) {
|
|
40790
|
+
this.name = "com.objectstack.runtime.objectos-project";
|
|
40791
|
+
this.version = "1.0.0";
|
|
40792
|
+
this.init = async (ctx) => {
|
|
40793
|
+
const client = this.config.client ?? (this.config.controlPlaneUrl === "file" ? new FileArtifactApiClient({
|
|
40794
|
+
...this.config.fileConfig ?? {},
|
|
40795
|
+
logger: ctx.logger
|
|
40796
|
+
}) : new ArtifactApiClient({
|
|
40797
|
+
controlPlaneUrl: this.config.controlPlaneUrl,
|
|
40798
|
+
apiKey: this.config.controlPlaneApiKey,
|
|
40799
|
+
cacheTtlMs: this.config.artifactCacheTtlMs,
|
|
40800
|
+
logger: ctx.logger
|
|
40801
|
+
}));
|
|
40802
|
+
this.client = client;
|
|
40803
|
+
const envRegistry = new ArtifactEnvironmentRegistry({
|
|
40804
|
+
client,
|
|
40805
|
+
cacheTtlMs: this.config.envCacheTtlMs,
|
|
40806
|
+
logger: ctx.logger
|
|
40807
|
+
});
|
|
40808
|
+
const factory = new ArtifactKernelFactory({
|
|
40809
|
+
client,
|
|
40810
|
+
envRegistry,
|
|
40811
|
+
logger: ctx.logger
|
|
40812
|
+
});
|
|
40813
|
+
const kernelManager = new KernelManager({
|
|
40814
|
+
factory,
|
|
40815
|
+
maxSize: this.config.kernelCacheSize,
|
|
40816
|
+
ttlMs: this.config.kernelTtlMs,
|
|
40817
|
+
logger: ctx.logger
|
|
40818
|
+
});
|
|
40819
|
+
this.kernelManager = kernelManager;
|
|
40820
|
+
ctx.registerService("env-registry", envRegistry);
|
|
40821
|
+
ctx.registerService("kernel-manager", kernelManager);
|
|
40822
|
+
ctx.registerService("artifact-api-client", client);
|
|
40823
|
+
ctx.logger.info?.("ObjectOSProjectPlugin: registered env-registry + kernel-manager", {
|
|
40824
|
+
mode: this.config.controlPlaneUrl === "file" ? "file" : "http",
|
|
40825
|
+
controlPlaneUrl: this.config.controlPlaneUrl
|
|
40826
|
+
});
|
|
40827
|
+
};
|
|
40828
|
+
this.destroy = async () => {
|
|
40829
|
+
try {
|
|
40830
|
+
await this.kernelManager?.evictAll();
|
|
40831
|
+
} catch {
|
|
40832
|
+
}
|
|
40833
|
+
try {
|
|
40834
|
+
this.client?.clear();
|
|
40835
|
+
} catch {
|
|
40836
|
+
}
|
|
40837
|
+
};
|
|
40838
|
+
this.config = config;
|
|
40839
|
+
}
|
|
40840
|
+
};
|
|
40841
|
+
async function createObjectOSStack(config) {
|
|
40842
|
+
if (!config.controlPlaneUrl && !config.client) {
|
|
40843
|
+
throw new Error("[createObjectOSStack] either controlPlaneUrl or client is required");
|
|
40844
|
+
}
|
|
40845
|
+
const merged = {
|
|
40846
|
+
...config,
|
|
40847
|
+
kernelCacheSize: Number(process.env.OS_KERNEL_CACHE_SIZE ?? config.kernelCacheSize ?? 32),
|
|
40848
|
+
kernelTtlMs: Number(process.env.OS_KERNEL_TTL_MS ?? config.kernelTtlMs ?? 15 * 60 * 1e3),
|
|
40849
|
+
envCacheTtlMs: Number(process.env.OS_ENV_CACHE_TTL_MS ?? config.envCacheTtlMs ?? 5 * 60 * 1e3),
|
|
40850
|
+
artifactCacheTtlMs: Number(process.env.OS_ARTIFACT_CACHE_TTL_MS ?? config.artifactCacheTtlMs ?? 5 * 60 * 1e3)
|
|
40851
|
+
};
|
|
40852
|
+
const enginePlugins = await createHostEnginePlugins();
|
|
40853
|
+
return {
|
|
40854
|
+
plugins: [...enginePlugins, new ObjectOSProjectPlugin(merged), new AuthProxyPlugin()],
|
|
40855
|
+
api: {
|
|
40856
|
+
enableProjectScoping: true,
|
|
40857
|
+
projectResolution: "auto",
|
|
40858
|
+
// ObjectOS is multi-tenant: anonymous /api/v1/data/* must never
|
|
40859
|
+
// leak per-project data across organisations. AuthProxyPlugin
|
|
40860
|
+
// verifies upstream tokens and populates ctx.userId; requireAuth
|
|
40861
|
+
// turns missing userId into 401 at the REST layer before the
|
|
40862
|
+
// request reaches the per-project kernel.
|
|
40863
|
+
requireAuth: true
|
|
40864
|
+
}
|
|
40865
|
+
};
|
|
40866
|
+
}
|
|
40867
|
+
|
|
40868
|
+
// src/index.ts
|
|
40869
|
+
init_platform_sso();
|
|
40870
|
+
|
|
38262
40871
|
// src/sandbox/script-runner.ts
|
|
38263
40872
|
var UnimplementedScriptRunner = class {
|
|
38264
40873
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
@@ -38291,12 +40900,26 @@ import {
|
|
|
38291
40900
|
export * from "@objectstack/core";
|
|
38292
40901
|
export {
|
|
38293
40902
|
AppPlugin,
|
|
40903
|
+
ArtifactApiClient,
|
|
40904
|
+
ArtifactEnvironmentRegistry,
|
|
40905
|
+
ArtifactKernelFactory,
|
|
40906
|
+
AuthProxyPlugin,
|
|
40907
|
+
DEFAULT_RATE_LIMITS,
|
|
38294
40908
|
DriverPlugin,
|
|
40909
|
+
FileArtifactApiClient,
|
|
38295
40910
|
HttpDispatcher,
|
|
38296
40911
|
HttpServer,
|
|
40912
|
+
InMemoryErrorReporter,
|
|
40913
|
+
InMemoryMetricsRegistry,
|
|
40914
|
+
KernelManager,
|
|
38297
40915
|
MiddlewareManager,
|
|
38298
|
-
|
|
40916
|
+
NoopErrorReporter,
|
|
40917
|
+
NoopMetricsRegistry,
|
|
40918
|
+
ObjectKernel4 as ObjectKernel,
|
|
40919
|
+
PLATFORM_SSO_PROVIDER_ID,
|
|
38299
40920
|
QuickJSScriptRunner,
|
|
40921
|
+
RUNTIME_METRICS,
|
|
40922
|
+
RateLimiter,
|
|
38300
40923
|
RestServer,
|
|
38301
40924
|
RouteGroupBuilder,
|
|
38302
40925
|
RouteManager,
|
|
@@ -38306,17 +40929,31 @@ export {
|
|
|
38306
40929
|
SeedLoaderService,
|
|
38307
40930
|
UnimplementedScriptRunner,
|
|
38308
40931
|
actionBodyRunnerFactory,
|
|
40932
|
+
backfillPlatformSsoClients,
|
|
40933
|
+
buildPlatformSsoRedirectUri,
|
|
40934
|
+
buildSecurityHeaders,
|
|
38309
40935
|
collectBundleActions,
|
|
38310
40936
|
collectBundleFunctions,
|
|
38311
40937
|
collectBundleHooks,
|
|
40938
|
+
createDefaultHostConfig,
|
|
38312
40939
|
createDispatcherPlugin,
|
|
40940
|
+
createObjectOSStack,
|
|
38313
40941
|
createRestApiPlugin,
|
|
38314
40942
|
createStandaloneStack,
|
|
38315
40943
|
createSystemProjectPlugin,
|
|
40944
|
+
derivePlatformSsoClientId,
|
|
40945
|
+
derivePlatformSsoClientSecret,
|
|
40946
|
+
extractRequestId,
|
|
40947
|
+
formatTraceparent,
|
|
40948
|
+
generateRequestId,
|
|
38316
40949
|
hookBodyRunnerFactory,
|
|
38317
40950
|
isHttpUrl,
|
|
38318
40951
|
loadArtifactBundle,
|
|
38319
40952
|
mergeRuntimeModule,
|
|
38320
|
-
|
|
40953
|
+
parseTraceparent,
|
|
40954
|
+
readArtifactSource,
|
|
40955
|
+
resolveDefaultArtifactPath,
|
|
40956
|
+
resolveRequestId,
|
|
40957
|
+
seedPlatformSsoClient
|
|
38321
40958
|
};
|
|
38322
40959
|
//# sourceMappingURL=index.js.map
|