@objectstack/runtime 6.9.0 → 7.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +439 -89
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +47 -1
- package/dist/index.d.ts +47 -1
- package/dist/index.js +439 -89
- package/dist/index.js.map +1 -1
- package/package.json +18 -25
package/dist/index.js
CHANGED
|
@@ -1271,7 +1271,15 @@ var init_app_plugin = __esm({
|
|
|
1271
1271
|
AppPlugin = class {
|
|
1272
1272
|
constructor(bundle, projectContext) {
|
|
1273
1273
|
this.type = "app";
|
|
1274
|
+
/** When true, init/start become no-ops — env has no app payload. */
|
|
1275
|
+
this.empty = false;
|
|
1274
1276
|
this.init = async (ctx) => {
|
|
1277
|
+
if (this.empty) {
|
|
1278
|
+
ctx.logger.debug("[AppPlugin] empty env \u2014 no app payload, skipping init", {
|
|
1279
|
+
pluginName: this.name
|
|
1280
|
+
});
|
|
1281
|
+
return;
|
|
1282
|
+
}
|
|
1275
1283
|
const sys = this.bundle.manifest || this.bundle;
|
|
1276
1284
|
const appId = sys.id || sys.name;
|
|
1277
1285
|
ctx.logger.info("Registering App Service", {
|
|
@@ -1286,6 +1294,12 @@ var init_app_plugin = __esm({
|
|
|
1286
1294
|
ctx.getService("manifest").register(servicePayload);
|
|
1287
1295
|
};
|
|
1288
1296
|
this.start = async (ctx) => {
|
|
1297
|
+
if (this.empty) {
|
|
1298
|
+
ctx.logger.debug("[AppPlugin] empty env \u2014 no app payload, skipping start", {
|
|
1299
|
+
pluginName: this.name
|
|
1300
|
+
});
|
|
1301
|
+
return;
|
|
1302
|
+
}
|
|
1289
1303
|
const sys = this.bundle.manifest || this.bundle;
|
|
1290
1304
|
const appId = sys.id || sys.name;
|
|
1291
1305
|
let ql;
|
|
@@ -1581,47 +1595,64 @@ var init_app_plugin = __esm({
|
|
|
1581
1595
|
if (multiTenant) {
|
|
1582
1596
|
ctx.logger.info("[Seeder] multi-tenant mode \u2014 skipping inline seed; per-org replay will run on sys_organization insert");
|
|
1583
1597
|
} else {
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
const
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1598
|
+
const seedBudgetMs = Number(process.env.OS_INLINE_SEED_BUDGET_MS ?? 8e3);
|
|
1599
|
+
const seedPromise = (async () => {
|
|
1600
|
+
try {
|
|
1601
|
+
const metadata = ctx.getService("metadata");
|
|
1602
|
+
if (metadata) {
|
|
1603
|
+
const seedLoader = new SeedLoaderService(ql, metadata, ctx.logger);
|
|
1604
|
+
const { SeedLoaderRequestSchema } = await import("@objectstack/spec/data");
|
|
1605
|
+
const request = SeedLoaderRequestSchema.parse({
|
|
1606
|
+
datasets: normalizedDatasets,
|
|
1607
|
+
config: { defaultMode: "upsert", multiPass: true }
|
|
1608
|
+
});
|
|
1609
|
+
const result = await seedLoader.load(request);
|
|
1610
|
+
ctx.logger.info("[Seeder] Seed loading complete", {
|
|
1611
|
+
inserted: result.summary.totalInserted,
|
|
1612
|
+
updated: result.summary.totalUpdated,
|
|
1613
|
+
errors: result.errors.length
|
|
1614
|
+
});
|
|
1615
|
+
} else {
|
|
1616
|
+
ctx.logger.debug("[Seeder] No metadata service; using basic insert fallback");
|
|
1617
|
+
for (const dataset of normalizedDatasets) {
|
|
1618
|
+
ctx.logger.info(`[Seeder] Seeding ${dataset.records.length} records for ${dataset.object}`);
|
|
1619
|
+
for (const record of dataset.records) {
|
|
1620
|
+
try {
|
|
1621
|
+
await ql.insert(dataset.object, record, { context: { isSystem: true } });
|
|
1622
|
+
} catch (err) {
|
|
1623
|
+
ctx.logger.warn(`[Seeder] Failed to insert ${dataset.object} record:`, { error: err.message });
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
ctx.logger.info("[Seeder] Data seeding complete.");
|
|
1628
|
+
}
|
|
1629
|
+
} catch (err) {
|
|
1630
|
+
ctx.logger.warn("[Seeder] SeedLoaderService failed, falling back to basic insert", { error: err.message });
|
|
1601
1631
|
for (const dataset of normalizedDatasets) {
|
|
1602
|
-
ctx.logger.info(`[Seeder] Seeding ${dataset.records.length} records for ${dataset.object}`);
|
|
1603
1632
|
for (const record of dataset.records) {
|
|
1604
1633
|
try {
|
|
1605
1634
|
await ql.insert(dataset.object, record, { context: { isSystem: true } });
|
|
1606
|
-
} catch (
|
|
1607
|
-
ctx.logger.warn(`[Seeder] Failed to insert ${dataset.object} record:`, { error:
|
|
1635
|
+
} catch (insertErr) {
|
|
1636
|
+
ctx.logger.warn(`[Seeder] Failed to insert ${dataset.object} record:`, { error: insertErr.message });
|
|
1608
1637
|
}
|
|
1609
1638
|
}
|
|
1610
1639
|
}
|
|
1611
|
-
ctx.logger.info("[Seeder] Data seeding complete.");
|
|
1640
|
+
ctx.logger.info("[Seeder] Data seeding complete (fallback).");
|
|
1612
1641
|
}
|
|
1613
|
-
}
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
}
|
|
1623
|
-
|
|
1624
|
-
|
|
1642
|
+
})();
|
|
1643
|
+
let timer;
|
|
1644
|
+
const budget = new Promise((resolve2) => {
|
|
1645
|
+
timer = setTimeout(() => resolve2("budget"), seedBudgetMs);
|
|
1646
|
+
});
|
|
1647
|
+
const winner = await Promise.race([seedPromise.then(() => "done"), budget]);
|
|
1648
|
+
if (timer) clearTimeout(timer);
|
|
1649
|
+
if (winner === "budget") {
|
|
1650
|
+
ctx.logger.warn(
|
|
1651
|
+
`[Seeder] Inline seed exceeded ${seedBudgetMs}ms budget for ${appId}; continuing in background to avoid blocking kernel start.`
|
|
1652
|
+
);
|
|
1653
|
+
seedPromise.catch((err) => {
|
|
1654
|
+
ctx.logger.warn("[Seeder] Background seed failed after budget", { appId, error: err?.message ?? String(err) });
|
|
1655
|
+
});
|
|
1625
1656
|
}
|
|
1626
1657
|
}
|
|
1627
1658
|
}
|
|
@@ -1635,6 +1666,38 @@ var init_app_plugin = __esm({
|
|
|
1635
1666
|
const sys = bundle?.manifest || bundle;
|
|
1636
1667
|
const appId = sys?.id || sys?.name;
|
|
1637
1668
|
if (!appId) {
|
|
1669
|
+
const APP_CATEGORY_KEYS = [
|
|
1670
|
+
"objects",
|
|
1671
|
+
"views",
|
|
1672
|
+
"apps",
|
|
1673
|
+
"pages",
|
|
1674
|
+
"dashboards",
|
|
1675
|
+
"reports",
|
|
1676
|
+
"flows",
|
|
1677
|
+
"workflows",
|
|
1678
|
+
"triggers",
|
|
1679
|
+
"agents",
|
|
1680
|
+
"tools",
|
|
1681
|
+
"skills",
|
|
1682
|
+
"actions",
|
|
1683
|
+
"permissions",
|
|
1684
|
+
"roles",
|
|
1685
|
+
"profiles",
|
|
1686
|
+
"translations",
|
|
1687
|
+
"sharingRules",
|
|
1688
|
+
"ragPipelines",
|
|
1689
|
+
"data"
|
|
1690
|
+
];
|
|
1691
|
+
const hasAppPayload = APP_CATEGORY_KEYS.some((k) => {
|
|
1692
|
+
const v = (bundle && bundle[k]) ?? (sys && sys[k]);
|
|
1693
|
+
return Array.isArray(v) && v.length > 0;
|
|
1694
|
+
});
|
|
1695
|
+
if (!hasAppPayload) {
|
|
1696
|
+
this.empty = true;
|
|
1697
|
+
const envSlug = projectContext?.environmentId ? projectContext.environmentId.slice(0, 8) : "empty";
|
|
1698
|
+
this.name = `plugin.app.empty-${envSlug}`;
|
|
1699
|
+
return;
|
|
1700
|
+
}
|
|
1638
1701
|
const bundleKeys = bundle && typeof bundle === "object" ? Object.keys(bundle).slice(0, 20).join(",") : typeof bundle;
|
|
1639
1702
|
const sysKeys = sys && typeof sys === "object" ? Object.keys(sys).slice(0, 20).join(",") : typeof sys;
|
|
1640
1703
|
const ctxHint = projectContext ? ` projectContext=${JSON.stringify({
|
|
@@ -1643,7 +1706,7 @@ var init_app_plugin = __esm({
|
|
|
1643
1706
|
source: projectContext.source
|
|
1644
1707
|
})}` : "";
|
|
1645
1708
|
throw new Error(
|
|
1646
|
-
`[AppPlugin] bundle
|
|
1709
|
+
`[AppPlugin] bundle has app payload but no manifest.id / manifest.name \u2014 cannot register as a plugin. bundleKeys=[${bundleKeys}] sysKeys=[${sysKeys}]${ctxHint}`
|
|
1647
1710
|
);
|
|
1648
1711
|
}
|
|
1649
1712
|
this.name = `plugin.app.${appId}`;
|
|
@@ -2192,7 +2255,7 @@ function resolveObjectStackHome() {
|
|
|
2192
2255
|
var StandaloneStackConfigSchema = z.object({
|
|
2193
2256
|
databaseUrl: z.string().optional(),
|
|
2194
2257
|
databaseAuthToken: z.string().optional(),
|
|
2195
|
-
databaseDriver: z.enum(["sqlite", "sqlite-wasm", "
|
|
2258
|
+
databaseDriver: z.enum(["sqlite", "sqlite-wasm", "memory", "postgres", "mongodb"]).optional(),
|
|
2196
2259
|
environmentId: z.string().optional(),
|
|
2197
2260
|
artifactPath: z.string().optional(),
|
|
2198
2261
|
/**
|
|
@@ -2210,7 +2273,6 @@ var StandaloneStackConfigSchema = z.object({
|
|
|
2210
2273
|
});
|
|
2211
2274
|
function detectDriverFromUrl(dbUrl) {
|
|
2212
2275
|
if (/^memory:\/\//i.test(dbUrl)) return "memory";
|
|
2213
|
-
if (/^(libsql|https?):\/\//i.test(dbUrl)) return "turso";
|
|
2214
2276
|
if (/^(postgres(ql)?|pg):\/\//i.test(dbUrl)) return "postgres";
|
|
2215
2277
|
if (/^mongodb(\+srv)?:\/\//i.test(dbUrl)) return "mongodb";
|
|
2216
2278
|
if (/^wasm-sqlite:\/\//i.test(dbUrl)) return "sqlite-wasm";
|
|
@@ -2218,7 +2280,7 @@ function detectDriverFromUrl(dbUrl) {
|
|
|
2218
2280
|
if (/^file:/i.test(dbUrl)) return "sqlite";
|
|
2219
2281
|
if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(dbUrl)) return "sqlite";
|
|
2220
2282
|
throw new Error(
|
|
2221
|
-
`[StandaloneStack] Unsupported database URL scheme: ${dbUrl}. Supported schemes: memory://,
|
|
2283
|
+
`[StandaloneStack] Unsupported database URL scheme: ${dbUrl}. Supported schemes: memory://, postgres://, pg://, mongodb://, mongodb+srv://, file:`
|
|
2222
2284
|
);
|
|
2223
2285
|
}
|
|
2224
2286
|
async function createStandaloneStack(config) {
|
|
@@ -2232,25 +2294,12 @@ async function createStandaloneStack(config) {
|
|
|
2232
2294
|
const artifactPathInput = cfg.artifactPath ?? process.env.OS_ARTIFACT_PATH ?? resolvePath2(cwd, "dist/objectstack.json");
|
|
2233
2295
|
const artifactPath = isHttpUrl(artifactPathInput) ? artifactPathInput : artifactPathInput.startsWith("/") ? artifactPathInput : resolvePath2(cwd, artifactPathInput);
|
|
2234
2296
|
const dbUrl = cfg.databaseUrl ?? process.env.OS_DATABASE_URL?.trim() ?? process.env.TURSO_DATABASE_URL?.trim() ?? (process.env.OS_HOME?.trim() ? `file:${resolvePath2(resolveObjectStackHome(), "data/standalone.db")}` : cfg.projectRoot ? `file:${resolvePath2(cfg.projectRoot, ".objectstack/data/standalone.db")}` : `file:${resolvePath2(resolveObjectStackHome(), "data/standalone.db")}`);
|
|
2235
|
-
const dbAuthToken = cfg.databaseAuthToken ?? process.env.OS_DATABASE_AUTH_TOKEN?.trim() ?? process.env.TURSO_AUTH_TOKEN?.trim();
|
|
2236
2297
|
const explicitDriver = cfg.databaseDriver ?? process.env.OS_DATABASE_DRIVER?.trim();
|
|
2237
2298
|
const dbDriver = explicitDriver ?? detectDriverFromUrl(dbUrl);
|
|
2238
2299
|
let driverPlugin;
|
|
2239
2300
|
if (dbDriver === "memory") {
|
|
2240
2301
|
const { InMemoryDriver } = await import("@objectstack/driver-memory");
|
|
2241
2302
|
driverPlugin = new DriverPlugin2(new InMemoryDriver());
|
|
2242
|
-
} else if (dbDriver === "turso") {
|
|
2243
|
-
let TursoDriver;
|
|
2244
|
-
try {
|
|
2245
|
-
({ TursoDriver } = await import("@objectstack/driver-turso"));
|
|
2246
|
-
} catch (err) {
|
|
2247
|
-
throw new Error(
|
|
2248
|
-
`[StandaloneStack] libsql/turso URL detected ("${dbUrl}") but @objectstack/driver-turso is not installed. Install it with: npm install @objectstack/driver-turso (or use a file: URL to default to better-sqlite3). (${err?.message ?? err})`
|
|
2249
|
-
);
|
|
2250
|
-
}
|
|
2251
|
-
driverPlugin = new DriverPlugin2(
|
|
2252
|
-
new TursoDriver({ url: dbUrl, authToken: dbAuthToken })
|
|
2253
|
-
);
|
|
2254
2303
|
} else if (dbDriver === "postgres") {
|
|
2255
2304
|
const { SqlDriver } = await import("@objectstack/driver-sql");
|
|
2256
2305
|
driverPlugin = new DriverPlugin2(
|
|
@@ -3991,7 +4040,7 @@ var _HttpDispatcher = class _HttpDispatcher {
|
|
|
3991
4040
|
return {
|
|
3992
4041
|
handled: true,
|
|
3993
4042
|
response: this.error(
|
|
3994
|
-
"No ObjectQL driver is registered. Register at least one DriverPlugin (e.g. InMemoryDriver or
|
|
4043
|
+
"No ObjectQL driver is registered. Register at least one DriverPlugin (e.g. InMemoryDriver or SqlDriver).",
|
|
3995
4044
|
503
|
|
3996
4045
|
)
|
|
3997
4046
|
};
|
|
@@ -7493,6 +7542,27 @@ function extractRuntimeFromMetadata(metadata) {
|
|
|
7493
7542
|
}
|
|
7494
7543
|
async function createDriver(driverType, databaseUrl, authToken) {
|
|
7495
7544
|
switch (driverType) {
|
|
7545
|
+
case "libsql":
|
|
7546
|
+
case "turso": {
|
|
7547
|
+
let TursoDriver;
|
|
7548
|
+
try {
|
|
7549
|
+
({ TursoDriver } = await import("@objectstack/driver-turso"));
|
|
7550
|
+
} catch (primaryErr) {
|
|
7551
|
+
try {
|
|
7552
|
+
const { createRequire } = await import("module");
|
|
7553
|
+
const path = await import("path");
|
|
7554
|
+
const url = await import("url");
|
|
7555
|
+
const hostRequire = createRequire(path.join(process.cwd(), "noop.js"));
|
|
7556
|
+
const resolved = hostRequire.resolve("@objectstack/driver-turso");
|
|
7557
|
+
({ TursoDriver } = await import(url.pathToFileURL(resolved).href));
|
|
7558
|
+
} catch (fallbackErr) {
|
|
7559
|
+
throw new Error(
|
|
7560
|
+
`[ArtifactEnvironmentRegistry] libsql/turso driver requested but @objectstack/driver-turso is not resolvable. Install it from the cloud monorepo (cloud/packages/driver-turso) or via npm. (primary: ${primaryErr?.message ?? primaryErr}; fallback: ${fallbackErr?.message ?? fallbackErr})`
|
|
7561
|
+
);
|
|
7562
|
+
}
|
|
7563
|
+
}
|
|
7564
|
+
return new TursoDriver({ url: databaseUrl, authToken });
|
|
7565
|
+
}
|
|
7496
7566
|
case "memory": {
|
|
7497
7567
|
const { InMemoryDriver } = await import("@objectstack/driver-memory");
|
|
7498
7568
|
const dbName = databaseUrl.replace(/^memory:\/\//, "").trim();
|
|
@@ -7511,18 +7581,6 @@ async function createDriver(driverType, databaseUrl, authToken) {
|
|
|
7511
7581
|
useNullAsDefault: true
|
|
7512
7582
|
});
|
|
7513
7583
|
}
|
|
7514
|
-
case "libsql":
|
|
7515
|
-
case "turso": {
|
|
7516
|
-
let TursoDriver;
|
|
7517
|
-
try {
|
|
7518
|
-
({ TursoDriver } = await import("@objectstack/driver-turso"));
|
|
7519
|
-
} catch (err) {
|
|
7520
|
-
throw new Error(
|
|
7521
|
-
`[ArtifactEnvironmentRegistry] libsql/turso driver requested but @objectstack/driver-turso is not installed. Install it with: npm install @objectstack/driver-turso. (${err?.message ?? err})`
|
|
7522
|
-
);
|
|
7523
|
-
}
|
|
7524
|
-
return new TursoDriver({ url: databaseUrl, authToken });
|
|
7525
|
-
}
|
|
7526
7584
|
case "postgres":
|
|
7527
7585
|
case "postgresql":
|
|
7528
7586
|
case "pg": {
|
|
@@ -7798,9 +7856,20 @@ var ArtifactKernelFactory = class {
|
|
|
7798
7856
|
this.logger.warn?.("[ArtifactKernelFactory] OS_AUTH_SECRET not set \u2014 per-project AuthPlugin skipped (auth endpoints will return 404)", { environmentId });
|
|
7799
7857
|
}
|
|
7800
7858
|
try {
|
|
7801
|
-
const { SecurityPlugin } = await import("@objectstack/plugin-security");
|
|
7802
7859
|
const multiTenant = String(process.env.OS_MULTI_TENANT ?? "false").toLowerCase() !== "false";
|
|
7803
|
-
|
|
7860
|
+
if (multiTenant) {
|
|
7861
|
+
try {
|
|
7862
|
+
const { OrgScopingPlugin } = await import("@objectstack/plugin-org-scoping");
|
|
7863
|
+
await kernel.use(new OrgScopingPlugin());
|
|
7864
|
+
} catch (err) {
|
|
7865
|
+
this.logger.warn?.("[ArtifactKernelFactory] OrgScopingPlugin not registered (multi-tenant disabled)", {
|
|
7866
|
+
environmentId,
|
|
7867
|
+
error: err?.message
|
|
7868
|
+
});
|
|
7869
|
+
}
|
|
7870
|
+
}
|
|
7871
|
+
const { SecurityPlugin } = await import("@objectstack/plugin-security");
|
|
7872
|
+
await kernel.use(new SecurityPlugin());
|
|
7804
7873
|
} catch (err) {
|
|
7805
7874
|
this.logger.warn?.("[ArtifactKernelFactory] SecurityPlugin not registered", {
|
|
7806
7875
|
environmentId,
|
|
@@ -8326,6 +8395,40 @@ function resolveCloudUrl(explicit) {
|
|
|
8326
8395
|
|
|
8327
8396
|
// src/cloud/marketplace-proxy-plugin.ts
|
|
8328
8397
|
var MARKETPLACE_PREFIX = "/api/v1/marketplace";
|
|
8398
|
+
var DEFAULT_LRU_MAX = 200;
|
|
8399
|
+
var LIST_TTL_MS = 30 * 60 * 1e3;
|
|
8400
|
+
var PACKAGE_TTL_MS = 2 * 60 * 60 * 1e3;
|
|
8401
|
+
var VERSION_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
8402
|
+
function ttlForPath(pathname) {
|
|
8403
|
+
if (/\/packages\/[^/]+\/versions\//.test(pathname)) return VERSION_TTL_MS;
|
|
8404
|
+
if (/\/packages\/[^/]+/.test(pathname)) return PACKAGE_TTL_MS;
|
|
8405
|
+
return LIST_TTL_MS;
|
|
8406
|
+
}
|
|
8407
|
+
var LruTtlCache = class {
|
|
8408
|
+
constructor(max) {
|
|
8409
|
+
this.max = max;
|
|
8410
|
+
this.map = /* @__PURE__ */ new Map();
|
|
8411
|
+
}
|
|
8412
|
+
get(key) {
|
|
8413
|
+
const entry = this.map.get(key);
|
|
8414
|
+
if (!entry) return void 0;
|
|
8415
|
+
this.map.delete(key);
|
|
8416
|
+
this.map.set(key, entry);
|
|
8417
|
+
return entry;
|
|
8418
|
+
}
|
|
8419
|
+
set(key, entry) {
|
|
8420
|
+
if (this.map.has(key)) this.map.delete(key);
|
|
8421
|
+
this.map.set(key, entry);
|
|
8422
|
+
while (this.map.size > this.max) {
|
|
8423
|
+
const oldest = this.map.keys().next().value;
|
|
8424
|
+
if (oldest === void 0) break;
|
|
8425
|
+
this.map.delete(oldest);
|
|
8426
|
+
}
|
|
8427
|
+
}
|
|
8428
|
+
clear() {
|
|
8429
|
+
this.map.clear();
|
|
8430
|
+
}
|
|
8431
|
+
};
|
|
8329
8432
|
var MarketplaceProxyPlugin = class _MarketplaceProxyPlugin {
|
|
8330
8433
|
constructor(config = {}) {
|
|
8331
8434
|
this.name = "com.objectstack.runtime.marketplace-proxy";
|
|
@@ -8347,6 +8450,7 @@ var MarketplaceProxyPlugin = class _MarketplaceProxyPlugin {
|
|
|
8347
8450
|
}
|
|
8348
8451
|
const rawApp = httpServer.getRawApp();
|
|
8349
8452
|
const cloudUrl = this.cloudUrl;
|
|
8453
|
+
const cache = this.cache;
|
|
8350
8454
|
const handler = async (c, next) => {
|
|
8351
8455
|
if (!cloudUrl) {
|
|
8352
8456
|
return c.json({
|
|
@@ -8373,24 +8477,51 @@ var MarketplaceProxyPlugin = class _MarketplaceProxyPlugin {
|
|
|
8373
8477
|
}
|
|
8374
8478
|
}, 405);
|
|
8375
8479
|
}
|
|
8376
|
-
const
|
|
8377
|
-
|
|
8378
|
-
|
|
8379
|
-
|
|
8380
|
-
|
|
8381
|
-
|
|
8382
|
-
|
|
8383
|
-
|
|
8480
|
+
const accept = c.req.header("accept") ?? "application/json";
|
|
8481
|
+
const acceptLang = c.req.header("accept-language") ?? "";
|
|
8482
|
+
const cacheKey = `${incomingUrl.pathname}${incomingUrl.search}|al=${acceptLang}|a=${accept}`;
|
|
8483
|
+
const reqCacheCtl = (c.req.header("cache-control") ?? "").toLowerCase();
|
|
8484
|
+
const bypass = !cache || reqCacheCtl.includes("no-cache") || reqCacheCtl.includes("no-store");
|
|
8485
|
+
const now = Date.now();
|
|
8486
|
+
if (cache && !bypass) {
|
|
8487
|
+
const hit = cache.get(cacheKey);
|
|
8488
|
+
if (hit && hit.expiresAt > now) {
|
|
8489
|
+
return buildCachedResponse(hit, method, "HIT");
|
|
8384
8490
|
}
|
|
8385
|
-
|
|
8386
|
-
|
|
8387
|
-
|
|
8388
|
-
|
|
8389
|
-
|
|
8390
|
-
|
|
8491
|
+
if (hit) {
|
|
8492
|
+
const revalHeaders = {
|
|
8493
|
+
"Accept": accept,
|
|
8494
|
+
"User-Agent": `objectos-marketplace-proxy/${_MarketplaceProxyPlugin.prototype.version ?? "1.0.0"}`
|
|
8495
|
+
};
|
|
8496
|
+
if (acceptLang) revalHeaders["Accept-Language"] = acceptLang;
|
|
8497
|
+
if (hit.etag) revalHeaders["If-None-Match"] = hit.etag;
|
|
8498
|
+
if (hit.lastModified) revalHeaders["If-Modified-Since"] = hit.lastModified;
|
|
8499
|
+
const revalResp = await fetch(target, { method: "GET", headers: revalHeaders });
|
|
8500
|
+
if (revalResp.status === 304) {
|
|
8501
|
+
hit.expiresAt = now + hit.ttlMs;
|
|
8502
|
+
const newEtag = revalResp.headers.get("etag");
|
|
8503
|
+
const newLm = revalResp.headers.get("last-modified");
|
|
8504
|
+
if (newEtag) hit.etag = newEtag;
|
|
8505
|
+
if (newLm) hit.lastModified = newLm;
|
|
8506
|
+
cache.set(cacheKey, hit);
|
|
8507
|
+
return buildCachedResponse(hit, method, "REVALIDATED");
|
|
8508
|
+
}
|
|
8509
|
+
return await consumeAndMaybeCache(revalResp, cacheKey, incomingUrl.pathname, method, cache);
|
|
8510
|
+
}
|
|
8511
|
+
}
|
|
8512
|
+
const reqHeaders = {
|
|
8513
|
+
// Strip the inbound Host header — fetch will set
|
|
8514
|
+
// it to the cloud host. Forward only the
|
|
8515
|
+
// identifying headers cloud might log.
|
|
8516
|
+
"Accept": accept,
|
|
8517
|
+
"User-Agent": `objectos-marketplace-proxy/${_MarketplaceProxyPlugin.prototype.version ?? "1.0.0"}`
|
|
8518
|
+
};
|
|
8519
|
+
if (acceptLang) reqHeaders["Accept-Language"] = acceptLang;
|
|
8520
|
+
const resp = await fetch(target, { method: "GET", headers: reqHeaders });
|
|
8521
|
+
if (bypass || !cache) {
|
|
8522
|
+
return await passthroughResponse(resp, method, bypass ? "BYPASS" : "MISS");
|
|
8391
8523
|
}
|
|
8392
|
-
|
|
8393
|
-
return new Response(body, { status: resp.status, headers });
|
|
8524
|
+
return await consumeAndMaybeCache(resp, cacheKey, incomingUrl.pathname, method, cache);
|
|
8394
8525
|
} catch (err) {
|
|
8395
8526
|
const errObj = err instanceof Error ? err : new Error(err?.message ?? String(err));
|
|
8396
8527
|
ctx.logger?.error?.("[MarketplaceProxyPlugin] proxy failed", errObj);
|
|
@@ -8413,12 +8544,67 @@ var MarketplaceProxyPlugin = class _MarketplaceProxyPlugin {
|
|
|
8413
8544
|
}
|
|
8414
8545
|
}
|
|
8415
8546
|
}
|
|
8416
|
-
ctx.logger?.info?.(`[MarketplaceProxyPlugin] mounted at ${MARKETPLACE_PREFIX}/* \u2192 ${cloudUrl || "(unconfigured)"}`);
|
|
8547
|
+
ctx.logger?.info?.(`[MarketplaceProxyPlugin] mounted at ${MARKETPLACE_PREFIX}/* \u2192 ${cloudUrl || "(unconfigured)"} (cache=${this.cache ? "on" : "off"})`);
|
|
8417
8548
|
});
|
|
8418
8549
|
};
|
|
8419
8550
|
this.cloudUrl = resolveCloudUrl(config.controlPlaneUrl);
|
|
8551
|
+
const envFlag = (process.env.OS_MARKETPLACE_CACHE ?? "").trim().toLowerCase();
|
|
8552
|
+
const envDisabled = ["off", "false", "0", "no", "disable", "disabled"].includes(envFlag);
|
|
8553
|
+
const disabled = config.cacheDisabled ?? envDisabled;
|
|
8554
|
+
this.cache = disabled ? null : new LruTtlCache(Math.max(8, config.cacheMaxEntries ?? DEFAULT_LRU_MAX));
|
|
8420
8555
|
}
|
|
8421
8556
|
};
|
|
8557
|
+
var PASSTHROUGH_HEADERS = ["content-type", "cache-control", "etag", "last-modified", "vary"];
|
|
8558
|
+
function collectHeaders(src) {
|
|
8559
|
+
const out = {};
|
|
8560
|
+
for (const h of PASSTHROUGH_HEADERS) {
|
|
8561
|
+
const v = src.headers.get(h);
|
|
8562
|
+
if (v) out[h] = v;
|
|
8563
|
+
}
|
|
8564
|
+
return out;
|
|
8565
|
+
}
|
|
8566
|
+
function buildCachedResponse(entry, method, xCache) {
|
|
8567
|
+
const headers = new Headers(entry.headers);
|
|
8568
|
+
headers.set("X-Cache", xCache);
|
|
8569
|
+
const ageSec = Math.max(0, Math.floor((entry.expiresAt - entry.ttlMs - Date.now()) / -1e3));
|
|
8570
|
+
headers.set("Age", String(Math.max(0, ageSec)));
|
|
8571
|
+
const body = method === "HEAD" ? null : entry.body;
|
|
8572
|
+
return new Response(body, { status: entry.status, headers });
|
|
8573
|
+
}
|
|
8574
|
+
async function passthroughResponse(resp, method, xCache) {
|
|
8575
|
+
const headers = new Headers(collectHeaders(resp));
|
|
8576
|
+
headers.set("X-Cache", xCache);
|
|
8577
|
+
if (method === "HEAD") {
|
|
8578
|
+
try {
|
|
8579
|
+
await resp.arrayBuffer();
|
|
8580
|
+
} catch {
|
|
8581
|
+
}
|
|
8582
|
+
return new Response(null, { status: resp.status, headers });
|
|
8583
|
+
}
|
|
8584
|
+
const body = await resp.arrayBuffer();
|
|
8585
|
+
return new Response(body, { status: resp.status, headers });
|
|
8586
|
+
}
|
|
8587
|
+
async function consumeAndMaybeCache(resp, key, pathname, method, cache) {
|
|
8588
|
+
const body = await resp.arrayBuffer();
|
|
8589
|
+
const headers = collectHeaders(resp);
|
|
8590
|
+
if (resp.status >= 200 && resp.status < 300) {
|
|
8591
|
+
const ttlMs = ttlForPath(pathname);
|
|
8592
|
+
const entry = {
|
|
8593
|
+
status: resp.status,
|
|
8594
|
+
body,
|
|
8595
|
+
headers,
|
|
8596
|
+
etag: resp.headers.get("etag") ?? void 0,
|
|
8597
|
+
lastModified: resp.headers.get("last-modified") ?? void 0,
|
|
8598
|
+
expiresAt: Date.now() + ttlMs,
|
|
8599
|
+
ttlMs
|
|
8600
|
+
};
|
|
8601
|
+
cache.set(key, entry);
|
|
8602
|
+
}
|
|
8603
|
+
const respHeaders = new Headers(headers);
|
|
8604
|
+
respHeaders.set("X-Cache", "MISS");
|
|
8605
|
+
const outBody = method === "HEAD" ? null : body;
|
|
8606
|
+
return new Response(outBody, { status: resp.status, headers: respHeaders });
|
|
8607
|
+
}
|
|
8422
8608
|
|
|
8423
8609
|
// src/cloud/runtime-config-plugin.ts
|
|
8424
8610
|
var RuntimeConfigPlugin = class {
|
|
@@ -8456,12 +8642,14 @@ var RuntimeConfigPlugin = class {
|
|
|
8456
8642
|
let defaultEnvironmentId;
|
|
8457
8643
|
let defaultOrgId;
|
|
8458
8644
|
let resolvedSingleEnv = this.singleEnvironment;
|
|
8459
|
-
|
|
8645
|
+
const resolveFn = typeof envRegistry?.resolveByHostname === "function" ? envRegistry.resolveByHostname.bind(envRegistry) : typeof envRegistry?.resolveHostname === "function" ? envRegistry.resolveHostname.bind(envRegistry) : null;
|
|
8646
|
+
if (resolveFn && host) {
|
|
8460
8647
|
try {
|
|
8461
|
-
const resolved = await
|
|
8648
|
+
const resolved = await resolveFn(host);
|
|
8462
8649
|
if (resolved?.environmentId) {
|
|
8463
|
-
defaultEnvironmentId = resolved.environmentId;
|
|
8464
|
-
|
|
8650
|
+
defaultEnvironmentId = String(resolved.environmentId);
|
|
8651
|
+
const orgId = resolved.organizationId ?? resolved.organization_id;
|
|
8652
|
+
if (orgId) defaultOrgId = String(orgId);
|
|
8465
8653
|
resolvedSingleEnv = true;
|
|
8466
8654
|
}
|
|
8467
8655
|
} catch {
|
|
@@ -8472,7 +8660,11 @@ var RuntimeConfigPlugin = class {
|
|
|
8472
8660
|
singleEnvironment: resolvedSingleEnv,
|
|
8473
8661
|
defaultOrgId,
|
|
8474
8662
|
defaultEnvironmentId,
|
|
8475
|
-
features
|
|
8663
|
+
features,
|
|
8664
|
+
branding: {
|
|
8665
|
+
productName: this.productName,
|
|
8666
|
+
productShortName: this.productShortName
|
|
8667
|
+
}
|
|
8476
8668
|
});
|
|
8477
8669
|
};
|
|
8478
8670
|
rawApp.get("/api/v1/runtime/config", handler);
|
|
@@ -8489,6 +8681,10 @@ var RuntimeConfigPlugin = class {
|
|
|
8489
8681
|
this.cloudUrl = config.controlPlaneUrl === "" ? "" : resolveCloudUrl(config.controlPlaneUrl) ?? "";
|
|
8490
8682
|
this.installLocal = !!config.installLocal;
|
|
8491
8683
|
this.singleEnvironment = !!config.singleEnvironment;
|
|
8684
|
+
const envName = (typeof process !== "undefined" ? process.env?.OS_PRODUCT_NAME : void 0)?.trim();
|
|
8685
|
+
const envShort = (typeof process !== "undefined" ? process.env?.OS_PRODUCT_SHORT_NAME : void 0)?.trim();
|
|
8686
|
+
this.productName = (config.productName ?? envName ?? "ObjectOS").trim() || "ObjectOS";
|
|
8687
|
+
this.productShortName = (config.productShortName ?? envShort ?? this.productName).trim() || this.productName;
|
|
8492
8688
|
}
|
|
8493
8689
|
};
|
|
8494
8690
|
|
|
@@ -8778,9 +8974,15 @@ var MarketplaceInstallLocalPlugin = class {
|
|
|
8778
8974
|
const postHandler = async (c) => this.handleInstall(c, ctx);
|
|
8779
8975
|
const getHandler = async (c) => this.handleList(c);
|
|
8780
8976
|
const deleteHandler = async (c) => this.handleUninstall(c, ctx);
|
|
8977
|
+
const reseedHandler = async (c) => this.handleReseed(c, ctx);
|
|
8978
|
+
const purgeHandler = async (c) => this.handlePurge(c, ctx);
|
|
8781
8979
|
if (typeof rawApp.post === "function") rawApp.post(ROUTE_BASE, postHandler);
|
|
8782
8980
|
if (typeof rawApp.get === "function") rawApp.get(ROUTE_BASE, getHandler);
|
|
8783
8981
|
if (typeof rawApp.delete === "function") rawApp.delete(`${ROUTE_BASE}/:manifestId`, deleteHandler);
|
|
8982
|
+
if (typeof rawApp.post === "function") {
|
|
8983
|
+
rawApp.post(`${ROUTE_BASE}/:manifestId/reseed-sample-data`, reseedHandler);
|
|
8984
|
+
rawApp.post(`${ROUTE_BASE}/:manifestId/purge-sample-data`, purgeHandler);
|
|
8985
|
+
}
|
|
8784
8986
|
ctx.logger?.info?.(`[MarketplaceInstallLocal] mounted at ${ROUTE_BASE} (storage: ${this.storageDir})`);
|
|
8785
8987
|
});
|
|
8786
8988
|
};
|
|
@@ -8877,7 +9079,8 @@ var MarketplaceInstallLocalPlugin = class {
|
|
|
8877
9079
|
version,
|
|
8878
9080
|
manifest,
|
|
8879
9081
|
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8880
|
-
installedBy: userId
|
|
9082
|
+
installedBy: userId,
|
|
9083
|
+
withSampleData: false
|
|
8881
9084
|
};
|
|
8882
9085
|
try {
|
|
8883
9086
|
mkdirSync3(this.storageDir, { recursive: true });
|
|
@@ -8904,6 +9107,13 @@ var MarketplaceInstallLocalPlugin = class {
|
|
|
8904
9107
|
ctx.logger?.warn?.(`[MarketplaceInstallLocal] syncSchemas failed for ${manifestId}: ${err?.message ?? err}`);
|
|
8905
9108
|
}
|
|
8906
9109
|
const seededSummary = await this.applySideEffects(ctx, manifest, { seedNow: true, c });
|
|
9110
|
+
if (seededSummary.seeded.mode === "inline" && (seededSummary.seeded.inserted ?? 0) + (seededSummary.seeded.updated ?? 0) > 0) {
|
|
9111
|
+
entry.withSampleData = true;
|
|
9112
|
+
try {
|
|
9113
|
+
writeFileSync2(join(this.storageDir, safeFilename(manifestId)), JSON.stringify(entry, null, 2), "utf8");
|
|
9114
|
+
} catch {
|
|
9115
|
+
}
|
|
9116
|
+
}
|
|
8907
9117
|
return c.json({
|
|
8908
9118
|
success: true,
|
|
8909
9119
|
data: {
|
|
@@ -8930,7 +9140,8 @@ var MarketplaceInstallLocalPlugin = class {
|
|
|
8930
9140
|
manifestId: e.manifestId,
|
|
8931
9141
|
version: e.version,
|
|
8932
9142
|
installedAt: e.installedAt,
|
|
8933
|
-
installedBy: e.installedBy
|
|
9143
|
+
installedBy: e.installedBy,
|
|
9144
|
+
withSampleData: e.withSampleData ?? false
|
|
8934
9145
|
})),
|
|
8935
9146
|
total: entries.length,
|
|
8936
9147
|
storageDir: this.storageDir
|
|
@@ -8995,6 +9206,145 @@ var MarketplaceInstallLocalPlugin = class {
|
|
|
8995
9206
|
* dev / single-tenant runtimes. Stricter checks can be layered on
|
|
8996
9207
|
* via a middleware in cloud-hosted multi-tenant deployments.
|
|
8997
9208
|
*/
|
|
9209
|
+
/**
|
|
9210
|
+
* POST /api/v1/marketplace/install-local/:manifestId/reseed-sample-data
|
|
9211
|
+
*
|
|
9212
|
+
* Re-runs SeedLoaderService against the cached manifest's `data` arrays.
|
|
9213
|
+
* Idempotent (upsert by id). Useful when:
|
|
9214
|
+
* • The user installed an app and skipped sample data
|
|
9215
|
+
* • A purge was undone
|
|
9216
|
+
* • The user wants a clean baseline back after editing demo rows
|
|
9217
|
+
*
|
|
9218
|
+
* Multi-tenant: requires an active organization on the session (same
|
|
9219
|
+
* rule as install seed path).
|
|
9220
|
+
*/
|
|
9221
|
+
this.handleReseed = async (c, ctx) => {
|
|
9222
|
+
const userId = await this.requireAuthenticatedUser(c, ctx);
|
|
9223
|
+
if (!userId) {
|
|
9224
|
+
return c.json({ success: false, error: { code: "unauthorized", message: "Authentication required." } }, 401);
|
|
9225
|
+
}
|
|
9226
|
+
const manifestId = String(c.req.param?.("manifestId") ?? c.req.params?.manifestId ?? "").trim();
|
|
9227
|
+
if (!manifestId) {
|
|
9228
|
+
return c.json({ success: false, error: { code: "bad_request", message: "manifestId path param required." } }, 400);
|
|
9229
|
+
}
|
|
9230
|
+
const file = join(this.storageDir, safeFilename(manifestId));
|
|
9231
|
+
if (!existsSync2(file)) {
|
|
9232
|
+
return c.json({ success: false, error: { code: "not_found", message: `No marketplace install for ${manifestId}.` } }, 404);
|
|
9233
|
+
}
|
|
9234
|
+
let entry;
|
|
9235
|
+
try {
|
|
9236
|
+
entry = JSON.parse(readFileSync(file, "utf8"));
|
|
9237
|
+
} catch (err) {
|
|
9238
|
+
return c.json({ success: false, error: { code: "storage_failed", message: `Failed to read manifest cache: ${err?.message ?? err}` } }, 500);
|
|
9239
|
+
}
|
|
9240
|
+
const summary = await this.applySideEffects(ctx, entry.manifest, { seedNow: true, c });
|
|
9241
|
+
if (summary.seeded.mode === "skipped") {
|
|
9242
|
+
return c.json({
|
|
9243
|
+
success: false,
|
|
9244
|
+
error: {
|
|
9245
|
+
code: "reseed_skipped",
|
|
9246
|
+
message: `Reseed did not run: ${summary.seeded.reason ?? "unknown reason"}`
|
|
9247
|
+
}
|
|
9248
|
+
}, 400);
|
|
9249
|
+
}
|
|
9250
|
+
try {
|
|
9251
|
+
entry.withSampleData = true;
|
|
9252
|
+
writeFileSync2(file, JSON.stringify(entry, null, 2), "utf8");
|
|
9253
|
+
} catch {
|
|
9254
|
+
}
|
|
9255
|
+
return c.json({
|
|
9256
|
+
success: true,
|
|
9257
|
+
data: {
|
|
9258
|
+
manifestId,
|
|
9259
|
+
inserted: summary.seeded.inserted ?? 0,
|
|
9260
|
+
updated: summary.seeded.updated ?? 0,
|
|
9261
|
+
errors: summary.seeded.errors ?? 0,
|
|
9262
|
+
withSampleData: true
|
|
9263
|
+
}
|
|
9264
|
+
}, 200);
|
|
9265
|
+
};
|
|
9266
|
+
/**
|
|
9267
|
+
* POST /api/v1/marketplace/install-local/:manifestId/purge-sample-data
|
|
9268
|
+
*
|
|
9269
|
+
* Deletes every record whose id is declared in the cached manifest's
|
|
9270
|
+
* seed datasets. Uses the `driver` service directly to bypass ACL /
|
|
9271
|
+
* lifecycle hooks (same pattern as cloud purge). User-created records
|
|
9272
|
+
* are never touched — only ids declared in the package's bundled
|
|
9273
|
+
* datasets are removed. Already-deleted rows count as `skipped`.
|
|
9274
|
+
*/
|
|
9275
|
+
this.handlePurge = async (c, ctx) => {
|
|
9276
|
+
const userId = await this.requireAuthenticatedUser(c, ctx);
|
|
9277
|
+
if (!userId) {
|
|
9278
|
+
return c.json({ success: false, error: { code: "unauthorized", message: "Authentication required." } }, 401);
|
|
9279
|
+
}
|
|
9280
|
+
const manifestId = String(c.req.param?.("manifestId") ?? c.req.params?.manifestId ?? "").trim();
|
|
9281
|
+
if (!manifestId) {
|
|
9282
|
+
return c.json({ success: false, error: { code: "bad_request", message: "manifestId path param required." } }, 400);
|
|
9283
|
+
}
|
|
9284
|
+
const file = join(this.storageDir, safeFilename(manifestId));
|
|
9285
|
+
if (!existsSync2(file)) {
|
|
9286
|
+
return c.json({ success: false, error: { code: "not_found", message: `No marketplace install for ${manifestId}.` } }, 404);
|
|
9287
|
+
}
|
|
9288
|
+
let entry;
|
|
9289
|
+
try {
|
|
9290
|
+
entry = JSON.parse(readFileSync(file, "utf8"));
|
|
9291
|
+
} catch (err) {
|
|
9292
|
+
return c.json({ success: false, error: { code: "storage_failed", message: `Failed to read manifest cache: ${err?.message ?? err}` } }, 500);
|
|
9293
|
+
}
|
|
9294
|
+
const datasets = Array.isArray(entry.manifest?.data) ? entry.manifest.data.filter((d) => d && d.object && Array.isArray(d.records)) : [];
|
|
9295
|
+
if (datasets.length === 0) {
|
|
9296
|
+
return c.json({
|
|
9297
|
+
success: false,
|
|
9298
|
+
error: { code: "nothing_to_purge", message: "This package declares no seed datasets." }
|
|
9299
|
+
}, 400);
|
|
9300
|
+
}
|
|
9301
|
+
let driver;
|
|
9302
|
+
try {
|
|
9303
|
+
driver = ctx.getService("driver");
|
|
9304
|
+
} catch {
|
|
9305
|
+
}
|
|
9306
|
+
if (!driver || typeof driver.delete !== "function") {
|
|
9307
|
+
return c.json({
|
|
9308
|
+
success: false,
|
|
9309
|
+
error: { code: "driver_missing", message: "driver service unavailable \u2014 cannot purge." }
|
|
9310
|
+
}, 500);
|
|
9311
|
+
}
|
|
9312
|
+
let deleted = 0;
|
|
9313
|
+
let skipped = 0;
|
|
9314
|
+
let errors = 0;
|
|
9315
|
+
for (const ds of datasets) {
|
|
9316
|
+
const object = String(ds.object);
|
|
9317
|
+
for (const rec of ds.records) {
|
|
9318
|
+
const id = rec?.id;
|
|
9319
|
+
if (id === void 0 || id === null || id === "") {
|
|
9320
|
+
skipped++;
|
|
9321
|
+
continue;
|
|
9322
|
+
}
|
|
9323
|
+
try {
|
|
9324
|
+
const r = await driver.delete(object, id);
|
|
9325
|
+
if (r === false || r === 0 || r?.deleted === 0) skipped++;
|
|
9326
|
+
else deleted++;
|
|
9327
|
+
} catch (err) {
|
|
9328
|
+
const msg = String(err?.message ?? err);
|
|
9329
|
+
if (/not.?found|no row/i.test(msg)) skipped++;
|
|
9330
|
+
else {
|
|
9331
|
+
errors++;
|
|
9332
|
+
ctx.logger?.warn?.(`[MarketplaceInstallLocal] purge ${object}#${id}: ${msg}`);
|
|
9333
|
+
}
|
|
9334
|
+
}
|
|
9335
|
+
}
|
|
9336
|
+
}
|
|
9337
|
+
try {
|
|
9338
|
+
entry.withSampleData = false;
|
|
9339
|
+
writeFileSync2(file, JSON.stringify(entry, null, 2), "utf8");
|
|
9340
|
+
} catch {
|
|
9341
|
+
}
|
|
9342
|
+
ctx.logger?.info?.(`[MarketplaceInstallLocal] purged ${manifestId}: deleted=${deleted} skipped=${skipped} errors=${errors}`);
|
|
9343
|
+
return c.json({
|
|
9344
|
+
success: true,
|
|
9345
|
+
data: { manifestId, deleted, skipped, errors, withSampleData: false }
|
|
9346
|
+
}, 200);
|
|
9347
|
+
};
|
|
8998
9348
|
/**
|
|
8999
9349
|
* Replicate the start-time side-effects that AppPlugin runs for
|
|
9000
9350
|
* statically-declared apps but the `manifest` service does NOT:
|