@objectstack/runtime 6.9.0 → 7.1.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 +440 -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 +440 -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,39 @@ 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
|
+
"emailTemplates"
|
|
1691
|
+
];
|
|
1692
|
+
const hasAppPayload = APP_CATEGORY_KEYS.some((k) => {
|
|
1693
|
+
const v = (bundle && bundle[k]) ?? (sys && sys[k]);
|
|
1694
|
+
return Array.isArray(v) && v.length > 0;
|
|
1695
|
+
});
|
|
1696
|
+
if (!hasAppPayload) {
|
|
1697
|
+
this.empty = true;
|
|
1698
|
+
const envSlug = projectContext?.environmentId ? projectContext.environmentId.slice(0, 8) : "empty";
|
|
1699
|
+
this.name = `plugin.app.empty-${envSlug}`;
|
|
1700
|
+
return;
|
|
1701
|
+
}
|
|
1638
1702
|
const bundleKeys = bundle && typeof bundle === "object" ? Object.keys(bundle).slice(0, 20).join(",") : typeof bundle;
|
|
1639
1703
|
const sysKeys = sys && typeof sys === "object" ? Object.keys(sys).slice(0, 20).join(",") : typeof sys;
|
|
1640
1704
|
const ctxHint = projectContext ? ` projectContext=${JSON.stringify({
|
|
@@ -1643,7 +1707,7 @@ var init_app_plugin = __esm({
|
|
|
1643
1707
|
source: projectContext.source
|
|
1644
1708
|
})}` : "";
|
|
1645
1709
|
throw new Error(
|
|
1646
|
-
`[AppPlugin] bundle
|
|
1710
|
+
`[AppPlugin] bundle has app payload but no manifest.id / manifest.name \u2014 cannot register as a plugin. bundleKeys=[${bundleKeys}] sysKeys=[${sysKeys}]${ctxHint}`
|
|
1647
1711
|
);
|
|
1648
1712
|
}
|
|
1649
1713
|
this.name = `plugin.app.${appId}`;
|
|
@@ -2192,7 +2256,7 @@ function resolveObjectStackHome() {
|
|
|
2192
2256
|
var StandaloneStackConfigSchema = z.object({
|
|
2193
2257
|
databaseUrl: z.string().optional(),
|
|
2194
2258
|
databaseAuthToken: z.string().optional(),
|
|
2195
|
-
databaseDriver: z.enum(["sqlite", "sqlite-wasm", "
|
|
2259
|
+
databaseDriver: z.enum(["sqlite", "sqlite-wasm", "memory", "postgres", "mongodb"]).optional(),
|
|
2196
2260
|
environmentId: z.string().optional(),
|
|
2197
2261
|
artifactPath: z.string().optional(),
|
|
2198
2262
|
/**
|
|
@@ -2210,7 +2274,6 @@ var StandaloneStackConfigSchema = z.object({
|
|
|
2210
2274
|
});
|
|
2211
2275
|
function detectDriverFromUrl(dbUrl) {
|
|
2212
2276
|
if (/^memory:\/\//i.test(dbUrl)) return "memory";
|
|
2213
|
-
if (/^(libsql|https?):\/\//i.test(dbUrl)) return "turso";
|
|
2214
2277
|
if (/^(postgres(ql)?|pg):\/\//i.test(dbUrl)) return "postgres";
|
|
2215
2278
|
if (/^mongodb(\+srv)?:\/\//i.test(dbUrl)) return "mongodb";
|
|
2216
2279
|
if (/^wasm-sqlite:\/\//i.test(dbUrl)) return "sqlite-wasm";
|
|
@@ -2218,7 +2281,7 @@ function detectDriverFromUrl(dbUrl) {
|
|
|
2218
2281
|
if (/^file:/i.test(dbUrl)) return "sqlite";
|
|
2219
2282
|
if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(dbUrl)) return "sqlite";
|
|
2220
2283
|
throw new Error(
|
|
2221
|
-
`[StandaloneStack] Unsupported database URL scheme: ${dbUrl}. Supported schemes: memory://,
|
|
2284
|
+
`[StandaloneStack] Unsupported database URL scheme: ${dbUrl}. Supported schemes: memory://, postgres://, pg://, mongodb://, mongodb+srv://, file:`
|
|
2222
2285
|
);
|
|
2223
2286
|
}
|
|
2224
2287
|
async function createStandaloneStack(config) {
|
|
@@ -2232,25 +2295,12 @@ async function createStandaloneStack(config) {
|
|
|
2232
2295
|
const artifactPathInput = cfg.artifactPath ?? process.env.OS_ARTIFACT_PATH ?? resolvePath2(cwd, "dist/objectstack.json");
|
|
2233
2296
|
const artifactPath = isHttpUrl(artifactPathInput) ? artifactPathInput : artifactPathInput.startsWith("/") ? artifactPathInput : resolvePath2(cwd, artifactPathInput);
|
|
2234
2297
|
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
2298
|
const explicitDriver = cfg.databaseDriver ?? process.env.OS_DATABASE_DRIVER?.trim();
|
|
2237
2299
|
const dbDriver = explicitDriver ?? detectDriverFromUrl(dbUrl);
|
|
2238
2300
|
let driverPlugin;
|
|
2239
2301
|
if (dbDriver === "memory") {
|
|
2240
2302
|
const { InMemoryDriver } = await import("@objectstack/driver-memory");
|
|
2241
2303
|
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
2304
|
} else if (dbDriver === "postgres") {
|
|
2255
2305
|
const { SqlDriver } = await import("@objectstack/driver-sql");
|
|
2256
2306
|
driverPlugin = new DriverPlugin2(
|
|
@@ -3991,7 +4041,7 @@ var _HttpDispatcher = class _HttpDispatcher {
|
|
|
3991
4041
|
return {
|
|
3992
4042
|
handled: true,
|
|
3993
4043
|
response: this.error(
|
|
3994
|
-
"No ObjectQL driver is registered. Register at least one DriverPlugin (e.g. InMemoryDriver or
|
|
4044
|
+
"No ObjectQL driver is registered. Register at least one DriverPlugin (e.g. InMemoryDriver or SqlDriver).",
|
|
3995
4045
|
503
|
|
3996
4046
|
)
|
|
3997
4047
|
};
|
|
@@ -7493,6 +7543,27 @@ function extractRuntimeFromMetadata(metadata) {
|
|
|
7493
7543
|
}
|
|
7494
7544
|
async function createDriver(driverType, databaseUrl, authToken) {
|
|
7495
7545
|
switch (driverType) {
|
|
7546
|
+
case "libsql":
|
|
7547
|
+
case "turso": {
|
|
7548
|
+
let TursoDriver;
|
|
7549
|
+
try {
|
|
7550
|
+
({ TursoDriver } = await import("@objectstack/driver-turso"));
|
|
7551
|
+
} catch (primaryErr) {
|
|
7552
|
+
try {
|
|
7553
|
+
const { createRequire } = await import("module");
|
|
7554
|
+
const path = await import("path");
|
|
7555
|
+
const url = await import("url");
|
|
7556
|
+
const hostRequire = createRequire(path.join(process.cwd(), "noop.js"));
|
|
7557
|
+
const resolved = hostRequire.resolve("@objectstack/driver-turso");
|
|
7558
|
+
({ TursoDriver } = await import(url.pathToFileURL(resolved).href));
|
|
7559
|
+
} catch (fallbackErr) {
|
|
7560
|
+
throw new Error(
|
|
7561
|
+
`[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})`
|
|
7562
|
+
);
|
|
7563
|
+
}
|
|
7564
|
+
}
|
|
7565
|
+
return new TursoDriver({ url: databaseUrl, authToken });
|
|
7566
|
+
}
|
|
7496
7567
|
case "memory": {
|
|
7497
7568
|
const { InMemoryDriver } = await import("@objectstack/driver-memory");
|
|
7498
7569
|
const dbName = databaseUrl.replace(/^memory:\/\//, "").trim();
|
|
@@ -7511,18 +7582,6 @@ async function createDriver(driverType, databaseUrl, authToken) {
|
|
|
7511
7582
|
useNullAsDefault: true
|
|
7512
7583
|
});
|
|
7513
7584
|
}
|
|
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
7585
|
case "postgres":
|
|
7527
7586
|
case "postgresql":
|
|
7528
7587
|
case "pg": {
|
|
@@ -7798,9 +7857,20 @@ var ArtifactKernelFactory = class {
|
|
|
7798
7857
|
this.logger.warn?.("[ArtifactKernelFactory] OS_AUTH_SECRET not set \u2014 per-project AuthPlugin skipped (auth endpoints will return 404)", { environmentId });
|
|
7799
7858
|
}
|
|
7800
7859
|
try {
|
|
7801
|
-
const { SecurityPlugin } = await import("@objectstack/plugin-security");
|
|
7802
7860
|
const multiTenant = String(process.env.OS_MULTI_TENANT ?? "false").toLowerCase() !== "false";
|
|
7803
|
-
|
|
7861
|
+
if (multiTenant) {
|
|
7862
|
+
try {
|
|
7863
|
+
const { OrgScopingPlugin } = await import("@objectstack/plugin-org-scoping");
|
|
7864
|
+
await kernel.use(new OrgScopingPlugin());
|
|
7865
|
+
} catch (err) {
|
|
7866
|
+
this.logger.warn?.("[ArtifactKernelFactory] OrgScopingPlugin not registered (multi-tenant disabled)", {
|
|
7867
|
+
environmentId,
|
|
7868
|
+
error: err?.message
|
|
7869
|
+
});
|
|
7870
|
+
}
|
|
7871
|
+
}
|
|
7872
|
+
const { SecurityPlugin } = await import("@objectstack/plugin-security");
|
|
7873
|
+
await kernel.use(new SecurityPlugin());
|
|
7804
7874
|
} catch (err) {
|
|
7805
7875
|
this.logger.warn?.("[ArtifactKernelFactory] SecurityPlugin not registered", {
|
|
7806
7876
|
environmentId,
|
|
@@ -8326,6 +8396,40 @@ function resolveCloudUrl(explicit) {
|
|
|
8326
8396
|
|
|
8327
8397
|
// src/cloud/marketplace-proxy-plugin.ts
|
|
8328
8398
|
var MARKETPLACE_PREFIX = "/api/v1/marketplace";
|
|
8399
|
+
var DEFAULT_LRU_MAX = 200;
|
|
8400
|
+
var LIST_TTL_MS = 30 * 60 * 1e3;
|
|
8401
|
+
var PACKAGE_TTL_MS = 2 * 60 * 60 * 1e3;
|
|
8402
|
+
var VERSION_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
8403
|
+
function ttlForPath(pathname) {
|
|
8404
|
+
if (/\/packages\/[^/]+\/versions\//.test(pathname)) return VERSION_TTL_MS;
|
|
8405
|
+
if (/\/packages\/[^/]+/.test(pathname)) return PACKAGE_TTL_MS;
|
|
8406
|
+
return LIST_TTL_MS;
|
|
8407
|
+
}
|
|
8408
|
+
var LruTtlCache = class {
|
|
8409
|
+
constructor(max) {
|
|
8410
|
+
this.max = max;
|
|
8411
|
+
this.map = /* @__PURE__ */ new Map();
|
|
8412
|
+
}
|
|
8413
|
+
get(key) {
|
|
8414
|
+
const entry = this.map.get(key);
|
|
8415
|
+
if (!entry) return void 0;
|
|
8416
|
+
this.map.delete(key);
|
|
8417
|
+
this.map.set(key, entry);
|
|
8418
|
+
return entry;
|
|
8419
|
+
}
|
|
8420
|
+
set(key, entry) {
|
|
8421
|
+
if (this.map.has(key)) this.map.delete(key);
|
|
8422
|
+
this.map.set(key, entry);
|
|
8423
|
+
while (this.map.size > this.max) {
|
|
8424
|
+
const oldest = this.map.keys().next().value;
|
|
8425
|
+
if (oldest === void 0) break;
|
|
8426
|
+
this.map.delete(oldest);
|
|
8427
|
+
}
|
|
8428
|
+
}
|
|
8429
|
+
clear() {
|
|
8430
|
+
this.map.clear();
|
|
8431
|
+
}
|
|
8432
|
+
};
|
|
8329
8433
|
var MarketplaceProxyPlugin = class _MarketplaceProxyPlugin {
|
|
8330
8434
|
constructor(config = {}) {
|
|
8331
8435
|
this.name = "com.objectstack.runtime.marketplace-proxy";
|
|
@@ -8347,6 +8451,7 @@ var MarketplaceProxyPlugin = class _MarketplaceProxyPlugin {
|
|
|
8347
8451
|
}
|
|
8348
8452
|
const rawApp = httpServer.getRawApp();
|
|
8349
8453
|
const cloudUrl = this.cloudUrl;
|
|
8454
|
+
const cache = this.cache;
|
|
8350
8455
|
const handler = async (c, next) => {
|
|
8351
8456
|
if (!cloudUrl) {
|
|
8352
8457
|
return c.json({
|
|
@@ -8373,24 +8478,51 @@ var MarketplaceProxyPlugin = class _MarketplaceProxyPlugin {
|
|
|
8373
8478
|
}
|
|
8374
8479
|
}, 405);
|
|
8375
8480
|
}
|
|
8376
|
-
const
|
|
8377
|
-
|
|
8378
|
-
|
|
8379
|
-
|
|
8380
|
-
|
|
8381
|
-
|
|
8382
|
-
|
|
8383
|
-
|
|
8481
|
+
const accept = c.req.header("accept") ?? "application/json";
|
|
8482
|
+
const acceptLang = c.req.header("accept-language") ?? "";
|
|
8483
|
+
const cacheKey = `${incomingUrl.pathname}${incomingUrl.search}|al=${acceptLang}|a=${accept}`;
|
|
8484
|
+
const reqCacheCtl = (c.req.header("cache-control") ?? "").toLowerCase();
|
|
8485
|
+
const bypass = !cache || reqCacheCtl.includes("no-cache") || reqCacheCtl.includes("no-store");
|
|
8486
|
+
const now = Date.now();
|
|
8487
|
+
if (cache && !bypass) {
|
|
8488
|
+
const hit = cache.get(cacheKey);
|
|
8489
|
+
if (hit && hit.expiresAt > now) {
|
|
8490
|
+
return buildCachedResponse(hit, method, "HIT");
|
|
8384
8491
|
}
|
|
8385
|
-
|
|
8386
|
-
|
|
8387
|
-
|
|
8388
|
-
|
|
8389
|
-
|
|
8390
|
-
|
|
8492
|
+
if (hit) {
|
|
8493
|
+
const revalHeaders = {
|
|
8494
|
+
"Accept": accept,
|
|
8495
|
+
"User-Agent": `objectos-marketplace-proxy/${_MarketplaceProxyPlugin.prototype.version ?? "1.0.0"}`
|
|
8496
|
+
};
|
|
8497
|
+
if (acceptLang) revalHeaders["Accept-Language"] = acceptLang;
|
|
8498
|
+
if (hit.etag) revalHeaders["If-None-Match"] = hit.etag;
|
|
8499
|
+
if (hit.lastModified) revalHeaders["If-Modified-Since"] = hit.lastModified;
|
|
8500
|
+
const revalResp = await fetch(target, { method: "GET", headers: revalHeaders });
|
|
8501
|
+
if (revalResp.status === 304) {
|
|
8502
|
+
hit.expiresAt = now + hit.ttlMs;
|
|
8503
|
+
const newEtag = revalResp.headers.get("etag");
|
|
8504
|
+
const newLm = revalResp.headers.get("last-modified");
|
|
8505
|
+
if (newEtag) hit.etag = newEtag;
|
|
8506
|
+
if (newLm) hit.lastModified = newLm;
|
|
8507
|
+
cache.set(cacheKey, hit);
|
|
8508
|
+
return buildCachedResponse(hit, method, "REVALIDATED");
|
|
8509
|
+
}
|
|
8510
|
+
return await consumeAndMaybeCache(revalResp, cacheKey, incomingUrl.pathname, method, cache);
|
|
8511
|
+
}
|
|
8512
|
+
}
|
|
8513
|
+
const reqHeaders = {
|
|
8514
|
+
// Strip the inbound Host header — fetch will set
|
|
8515
|
+
// it to the cloud host. Forward only the
|
|
8516
|
+
// identifying headers cloud might log.
|
|
8517
|
+
"Accept": accept,
|
|
8518
|
+
"User-Agent": `objectos-marketplace-proxy/${_MarketplaceProxyPlugin.prototype.version ?? "1.0.0"}`
|
|
8519
|
+
};
|
|
8520
|
+
if (acceptLang) reqHeaders["Accept-Language"] = acceptLang;
|
|
8521
|
+
const resp = await fetch(target, { method: "GET", headers: reqHeaders });
|
|
8522
|
+
if (bypass || !cache) {
|
|
8523
|
+
return await passthroughResponse(resp, method, bypass ? "BYPASS" : "MISS");
|
|
8391
8524
|
}
|
|
8392
|
-
|
|
8393
|
-
return new Response(body, { status: resp.status, headers });
|
|
8525
|
+
return await consumeAndMaybeCache(resp, cacheKey, incomingUrl.pathname, method, cache);
|
|
8394
8526
|
} catch (err) {
|
|
8395
8527
|
const errObj = err instanceof Error ? err : new Error(err?.message ?? String(err));
|
|
8396
8528
|
ctx.logger?.error?.("[MarketplaceProxyPlugin] proxy failed", errObj);
|
|
@@ -8413,12 +8545,67 @@ var MarketplaceProxyPlugin = class _MarketplaceProxyPlugin {
|
|
|
8413
8545
|
}
|
|
8414
8546
|
}
|
|
8415
8547
|
}
|
|
8416
|
-
ctx.logger?.info?.(`[MarketplaceProxyPlugin] mounted at ${MARKETPLACE_PREFIX}/* \u2192 ${cloudUrl || "(unconfigured)"}`);
|
|
8548
|
+
ctx.logger?.info?.(`[MarketplaceProxyPlugin] mounted at ${MARKETPLACE_PREFIX}/* \u2192 ${cloudUrl || "(unconfigured)"} (cache=${this.cache ? "on" : "off"})`);
|
|
8417
8549
|
});
|
|
8418
8550
|
};
|
|
8419
8551
|
this.cloudUrl = resolveCloudUrl(config.controlPlaneUrl);
|
|
8552
|
+
const envFlag = (process.env.OS_MARKETPLACE_CACHE ?? "").trim().toLowerCase();
|
|
8553
|
+
const envDisabled = ["off", "false", "0", "no", "disable", "disabled"].includes(envFlag);
|
|
8554
|
+
const disabled = config.cacheDisabled ?? envDisabled;
|
|
8555
|
+
this.cache = disabled ? null : new LruTtlCache(Math.max(8, config.cacheMaxEntries ?? DEFAULT_LRU_MAX));
|
|
8420
8556
|
}
|
|
8421
8557
|
};
|
|
8558
|
+
var PASSTHROUGH_HEADERS = ["content-type", "cache-control", "etag", "last-modified", "vary"];
|
|
8559
|
+
function collectHeaders(src) {
|
|
8560
|
+
const out = {};
|
|
8561
|
+
for (const h of PASSTHROUGH_HEADERS) {
|
|
8562
|
+
const v = src.headers.get(h);
|
|
8563
|
+
if (v) out[h] = v;
|
|
8564
|
+
}
|
|
8565
|
+
return out;
|
|
8566
|
+
}
|
|
8567
|
+
function buildCachedResponse(entry, method, xCache) {
|
|
8568
|
+
const headers = new Headers(entry.headers);
|
|
8569
|
+
headers.set("X-Cache", xCache);
|
|
8570
|
+
const ageSec = Math.max(0, Math.floor((entry.expiresAt - entry.ttlMs - Date.now()) / -1e3));
|
|
8571
|
+
headers.set("Age", String(Math.max(0, ageSec)));
|
|
8572
|
+
const body = method === "HEAD" ? null : entry.body;
|
|
8573
|
+
return new Response(body, { status: entry.status, headers });
|
|
8574
|
+
}
|
|
8575
|
+
async function passthroughResponse(resp, method, xCache) {
|
|
8576
|
+
const headers = new Headers(collectHeaders(resp));
|
|
8577
|
+
headers.set("X-Cache", xCache);
|
|
8578
|
+
if (method === "HEAD") {
|
|
8579
|
+
try {
|
|
8580
|
+
await resp.arrayBuffer();
|
|
8581
|
+
} catch {
|
|
8582
|
+
}
|
|
8583
|
+
return new Response(null, { status: resp.status, headers });
|
|
8584
|
+
}
|
|
8585
|
+
const body = await resp.arrayBuffer();
|
|
8586
|
+
return new Response(body, { status: resp.status, headers });
|
|
8587
|
+
}
|
|
8588
|
+
async function consumeAndMaybeCache(resp, key, pathname, method, cache) {
|
|
8589
|
+
const body = await resp.arrayBuffer();
|
|
8590
|
+
const headers = collectHeaders(resp);
|
|
8591
|
+
if (resp.status >= 200 && resp.status < 300) {
|
|
8592
|
+
const ttlMs = ttlForPath(pathname);
|
|
8593
|
+
const entry = {
|
|
8594
|
+
status: resp.status,
|
|
8595
|
+
body,
|
|
8596
|
+
headers,
|
|
8597
|
+
etag: resp.headers.get("etag") ?? void 0,
|
|
8598
|
+
lastModified: resp.headers.get("last-modified") ?? void 0,
|
|
8599
|
+
expiresAt: Date.now() + ttlMs,
|
|
8600
|
+
ttlMs
|
|
8601
|
+
};
|
|
8602
|
+
cache.set(key, entry);
|
|
8603
|
+
}
|
|
8604
|
+
const respHeaders = new Headers(headers);
|
|
8605
|
+
respHeaders.set("X-Cache", "MISS");
|
|
8606
|
+
const outBody = method === "HEAD" ? null : body;
|
|
8607
|
+
return new Response(outBody, { status: resp.status, headers: respHeaders });
|
|
8608
|
+
}
|
|
8422
8609
|
|
|
8423
8610
|
// src/cloud/runtime-config-plugin.ts
|
|
8424
8611
|
var RuntimeConfigPlugin = class {
|
|
@@ -8456,12 +8643,14 @@ var RuntimeConfigPlugin = class {
|
|
|
8456
8643
|
let defaultEnvironmentId;
|
|
8457
8644
|
let defaultOrgId;
|
|
8458
8645
|
let resolvedSingleEnv = this.singleEnvironment;
|
|
8459
|
-
|
|
8646
|
+
const resolveFn = typeof envRegistry?.resolveByHostname === "function" ? envRegistry.resolveByHostname.bind(envRegistry) : typeof envRegistry?.resolveHostname === "function" ? envRegistry.resolveHostname.bind(envRegistry) : null;
|
|
8647
|
+
if (resolveFn && host) {
|
|
8460
8648
|
try {
|
|
8461
|
-
const resolved = await
|
|
8649
|
+
const resolved = await resolveFn(host);
|
|
8462
8650
|
if (resolved?.environmentId) {
|
|
8463
|
-
defaultEnvironmentId = resolved.environmentId;
|
|
8464
|
-
|
|
8651
|
+
defaultEnvironmentId = String(resolved.environmentId);
|
|
8652
|
+
const orgId = resolved.organizationId ?? resolved.organization_id;
|
|
8653
|
+
if (orgId) defaultOrgId = String(orgId);
|
|
8465
8654
|
resolvedSingleEnv = true;
|
|
8466
8655
|
}
|
|
8467
8656
|
} catch {
|
|
@@ -8472,7 +8661,11 @@ var RuntimeConfigPlugin = class {
|
|
|
8472
8661
|
singleEnvironment: resolvedSingleEnv,
|
|
8473
8662
|
defaultOrgId,
|
|
8474
8663
|
defaultEnvironmentId,
|
|
8475
|
-
features
|
|
8664
|
+
features,
|
|
8665
|
+
branding: {
|
|
8666
|
+
productName: this.productName,
|
|
8667
|
+
productShortName: this.productShortName
|
|
8668
|
+
}
|
|
8476
8669
|
});
|
|
8477
8670
|
};
|
|
8478
8671
|
rawApp.get("/api/v1/runtime/config", handler);
|
|
@@ -8489,6 +8682,10 @@ var RuntimeConfigPlugin = class {
|
|
|
8489
8682
|
this.cloudUrl = config.controlPlaneUrl === "" ? "" : resolveCloudUrl(config.controlPlaneUrl) ?? "";
|
|
8490
8683
|
this.installLocal = !!config.installLocal;
|
|
8491
8684
|
this.singleEnvironment = !!config.singleEnvironment;
|
|
8685
|
+
const envName = (typeof process !== "undefined" ? process.env?.OS_PRODUCT_NAME : void 0)?.trim();
|
|
8686
|
+
const envShort = (typeof process !== "undefined" ? process.env?.OS_PRODUCT_SHORT_NAME : void 0)?.trim();
|
|
8687
|
+
this.productName = (config.productName ?? envName ?? "ObjectOS").trim() || "ObjectOS";
|
|
8688
|
+
this.productShortName = (config.productShortName ?? envShort ?? this.productName).trim() || this.productName;
|
|
8492
8689
|
}
|
|
8493
8690
|
};
|
|
8494
8691
|
|
|
@@ -8778,9 +8975,15 @@ var MarketplaceInstallLocalPlugin = class {
|
|
|
8778
8975
|
const postHandler = async (c) => this.handleInstall(c, ctx);
|
|
8779
8976
|
const getHandler = async (c) => this.handleList(c);
|
|
8780
8977
|
const deleteHandler = async (c) => this.handleUninstall(c, ctx);
|
|
8978
|
+
const reseedHandler = async (c) => this.handleReseed(c, ctx);
|
|
8979
|
+
const purgeHandler = async (c) => this.handlePurge(c, ctx);
|
|
8781
8980
|
if (typeof rawApp.post === "function") rawApp.post(ROUTE_BASE, postHandler);
|
|
8782
8981
|
if (typeof rawApp.get === "function") rawApp.get(ROUTE_BASE, getHandler);
|
|
8783
8982
|
if (typeof rawApp.delete === "function") rawApp.delete(`${ROUTE_BASE}/:manifestId`, deleteHandler);
|
|
8983
|
+
if (typeof rawApp.post === "function") {
|
|
8984
|
+
rawApp.post(`${ROUTE_BASE}/:manifestId/reseed-sample-data`, reseedHandler);
|
|
8985
|
+
rawApp.post(`${ROUTE_BASE}/:manifestId/purge-sample-data`, purgeHandler);
|
|
8986
|
+
}
|
|
8784
8987
|
ctx.logger?.info?.(`[MarketplaceInstallLocal] mounted at ${ROUTE_BASE} (storage: ${this.storageDir})`);
|
|
8785
8988
|
});
|
|
8786
8989
|
};
|
|
@@ -8877,7 +9080,8 @@ var MarketplaceInstallLocalPlugin = class {
|
|
|
8877
9080
|
version,
|
|
8878
9081
|
manifest,
|
|
8879
9082
|
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8880
|
-
installedBy: userId
|
|
9083
|
+
installedBy: userId,
|
|
9084
|
+
withSampleData: false
|
|
8881
9085
|
};
|
|
8882
9086
|
try {
|
|
8883
9087
|
mkdirSync3(this.storageDir, { recursive: true });
|
|
@@ -8904,6 +9108,13 @@ var MarketplaceInstallLocalPlugin = class {
|
|
|
8904
9108
|
ctx.logger?.warn?.(`[MarketplaceInstallLocal] syncSchemas failed for ${manifestId}: ${err?.message ?? err}`);
|
|
8905
9109
|
}
|
|
8906
9110
|
const seededSummary = await this.applySideEffects(ctx, manifest, { seedNow: true, c });
|
|
9111
|
+
if (seededSummary.seeded.mode === "inline" && (seededSummary.seeded.inserted ?? 0) + (seededSummary.seeded.updated ?? 0) > 0) {
|
|
9112
|
+
entry.withSampleData = true;
|
|
9113
|
+
try {
|
|
9114
|
+
writeFileSync2(join(this.storageDir, safeFilename(manifestId)), JSON.stringify(entry, null, 2), "utf8");
|
|
9115
|
+
} catch {
|
|
9116
|
+
}
|
|
9117
|
+
}
|
|
8907
9118
|
return c.json({
|
|
8908
9119
|
success: true,
|
|
8909
9120
|
data: {
|
|
@@ -8930,7 +9141,8 @@ var MarketplaceInstallLocalPlugin = class {
|
|
|
8930
9141
|
manifestId: e.manifestId,
|
|
8931
9142
|
version: e.version,
|
|
8932
9143
|
installedAt: e.installedAt,
|
|
8933
|
-
installedBy: e.installedBy
|
|
9144
|
+
installedBy: e.installedBy,
|
|
9145
|
+
withSampleData: e.withSampleData ?? false
|
|
8934
9146
|
})),
|
|
8935
9147
|
total: entries.length,
|
|
8936
9148
|
storageDir: this.storageDir
|
|
@@ -8995,6 +9207,145 @@ var MarketplaceInstallLocalPlugin = class {
|
|
|
8995
9207
|
* dev / single-tenant runtimes. Stricter checks can be layered on
|
|
8996
9208
|
* via a middleware in cloud-hosted multi-tenant deployments.
|
|
8997
9209
|
*/
|
|
9210
|
+
/**
|
|
9211
|
+
* POST /api/v1/marketplace/install-local/:manifestId/reseed-sample-data
|
|
9212
|
+
*
|
|
9213
|
+
* Re-runs SeedLoaderService against the cached manifest's `data` arrays.
|
|
9214
|
+
* Idempotent (upsert by id). Useful when:
|
|
9215
|
+
* • The user installed an app and skipped sample data
|
|
9216
|
+
* • A purge was undone
|
|
9217
|
+
* • The user wants a clean baseline back after editing demo rows
|
|
9218
|
+
*
|
|
9219
|
+
* Multi-tenant: requires an active organization on the session (same
|
|
9220
|
+
* rule as install seed path).
|
|
9221
|
+
*/
|
|
9222
|
+
this.handleReseed = async (c, ctx) => {
|
|
9223
|
+
const userId = await this.requireAuthenticatedUser(c, ctx);
|
|
9224
|
+
if (!userId) {
|
|
9225
|
+
return c.json({ success: false, error: { code: "unauthorized", message: "Authentication required." } }, 401);
|
|
9226
|
+
}
|
|
9227
|
+
const manifestId = String(c.req.param?.("manifestId") ?? c.req.params?.manifestId ?? "").trim();
|
|
9228
|
+
if (!manifestId) {
|
|
9229
|
+
return c.json({ success: false, error: { code: "bad_request", message: "manifestId path param required." } }, 400);
|
|
9230
|
+
}
|
|
9231
|
+
const file = join(this.storageDir, safeFilename(manifestId));
|
|
9232
|
+
if (!existsSync2(file)) {
|
|
9233
|
+
return c.json({ success: false, error: { code: "not_found", message: `No marketplace install for ${manifestId}.` } }, 404);
|
|
9234
|
+
}
|
|
9235
|
+
let entry;
|
|
9236
|
+
try {
|
|
9237
|
+
entry = JSON.parse(readFileSync(file, "utf8"));
|
|
9238
|
+
} catch (err) {
|
|
9239
|
+
return c.json({ success: false, error: { code: "storage_failed", message: `Failed to read manifest cache: ${err?.message ?? err}` } }, 500);
|
|
9240
|
+
}
|
|
9241
|
+
const summary = await this.applySideEffects(ctx, entry.manifest, { seedNow: true, c });
|
|
9242
|
+
if (summary.seeded.mode === "skipped") {
|
|
9243
|
+
return c.json({
|
|
9244
|
+
success: false,
|
|
9245
|
+
error: {
|
|
9246
|
+
code: "reseed_skipped",
|
|
9247
|
+
message: `Reseed did not run: ${summary.seeded.reason ?? "unknown reason"}`
|
|
9248
|
+
}
|
|
9249
|
+
}, 400);
|
|
9250
|
+
}
|
|
9251
|
+
try {
|
|
9252
|
+
entry.withSampleData = true;
|
|
9253
|
+
writeFileSync2(file, JSON.stringify(entry, null, 2), "utf8");
|
|
9254
|
+
} catch {
|
|
9255
|
+
}
|
|
9256
|
+
return c.json({
|
|
9257
|
+
success: true,
|
|
9258
|
+
data: {
|
|
9259
|
+
manifestId,
|
|
9260
|
+
inserted: summary.seeded.inserted ?? 0,
|
|
9261
|
+
updated: summary.seeded.updated ?? 0,
|
|
9262
|
+
errors: summary.seeded.errors ?? 0,
|
|
9263
|
+
withSampleData: true
|
|
9264
|
+
}
|
|
9265
|
+
}, 200);
|
|
9266
|
+
};
|
|
9267
|
+
/**
|
|
9268
|
+
* POST /api/v1/marketplace/install-local/:manifestId/purge-sample-data
|
|
9269
|
+
*
|
|
9270
|
+
* Deletes every record whose id is declared in the cached manifest's
|
|
9271
|
+
* seed datasets. Uses the `driver` service directly to bypass ACL /
|
|
9272
|
+
* lifecycle hooks (same pattern as cloud purge). User-created records
|
|
9273
|
+
* are never touched — only ids declared in the package's bundled
|
|
9274
|
+
* datasets are removed. Already-deleted rows count as `skipped`.
|
|
9275
|
+
*/
|
|
9276
|
+
this.handlePurge = async (c, ctx) => {
|
|
9277
|
+
const userId = await this.requireAuthenticatedUser(c, ctx);
|
|
9278
|
+
if (!userId) {
|
|
9279
|
+
return c.json({ success: false, error: { code: "unauthorized", message: "Authentication required." } }, 401);
|
|
9280
|
+
}
|
|
9281
|
+
const manifestId = String(c.req.param?.("manifestId") ?? c.req.params?.manifestId ?? "").trim();
|
|
9282
|
+
if (!manifestId) {
|
|
9283
|
+
return c.json({ success: false, error: { code: "bad_request", message: "manifestId path param required." } }, 400);
|
|
9284
|
+
}
|
|
9285
|
+
const file = join(this.storageDir, safeFilename(manifestId));
|
|
9286
|
+
if (!existsSync2(file)) {
|
|
9287
|
+
return c.json({ success: false, error: { code: "not_found", message: `No marketplace install for ${manifestId}.` } }, 404);
|
|
9288
|
+
}
|
|
9289
|
+
let entry;
|
|
9290
|
+
try {
|
|
9291
|
+
entry = JSON.parse(readFileSync(file, "utf8"));
|
|
9292
|
+
} catch (err) {
|
|
9293
|
+
return c.json({ success: false, error: { code: "storage_failed", message: `Failed to read manifest cache: ${err?.message ?? err}` } }, 500);
|
|
9294
|
+
}
|
|
9295
|
+
const datasets = Array.isArray(entry.manifest?.data) ? entry.manifest.data.filter((d) => d && d.object && Array.isArray(d.records)) : [];
|
|
9296
|
+
if (datasets.length === 0) {
|
|
9297
|
+
return c.json({
|
|
9298
|
+
success: false,
|
|
9299
|
+
error: { code: "nothing_to_purge", message: "This package declares no seed datasets." }
|
|
9300
|
+
}, 400);
|
|
9301
|
+
}
|
|
9302
|
+
let driver;
|
|
9303
|
+
try {
|
|
9304
|
+
driver = ctx.getService("driver");
|
|
9305
|
+
} catch {
|
|
9306
|
+
}
|
|
9307
|
+
if (!driver || typeof driver.delete !== "function") {
|
|
9308
|
+
return c.json({
|
|
9309
|
+
success: false,
|
|
9310
|
+
error: { code: "driver_missing", message: "driver service unavailable \u2014 cannot purge." }
|
|
9311
|
+
}, 500);
|
|
9312
|
+
}
|
|
9313
|
+
let deleted = 0;
|
|
9314
|
+
let skipped = 0;
|
|
9315
|
+
let errors = 0;
|
|
9316
|
+
for (const ds of datasets) {
|
|
9317
|
+
const object = String(ds.object);
|
|
9318
|
+
for (const rec of ds.records) {
|
|
9319
|
+
const id = rec?.id;
|
|
9320
|
+
if (id === void 0 || id === null || id === "") {
|
|
9321
|
+
skipped++;
|
|
9322
|
+
continue;
|
|
9323
|
+
}
|
|
9324
|
+
try {
|
|
9325
|
+
const r = await driver.delete(object, id);
|
|
9326
|
+
if (r === false || r === 0 || r?.deleted === 0) skipped++;
|
|
9327
|
+
else deleted++;
|
|
9328
|
+
} catch (err) {
|
|
9329
|
+
const msg = String(err?.message ?? err);
|
|
9330
|
+
if (/not.?found|no row/i.test(msg)) skipped++;
|
|
9331
|
+
else {
|
|
9332
|
+
errors++;
|
|
9333
|
+
ctx.logger?.warn?.(`[MarketplaceInstallLocal] purge ${object}#${id}: ${msg}`);
|
|
9334
|
+
}
|
|
9335
|
+
}
|
|
9336
|
+
}
|
|
9337
|
+
}
|
|
9338
|
+
try {
|
|
9339
|
+
entry.withSampleData = false;
|
|
9340
|
+
writeFileSync2(file, JSON.stringify(entry, null, 2), "utf8");
|
|
9341
|
+
} catch {
|
|
9342
|
+
}
|
|
9343
|
+
ctx.logger?.info?.(`[MarketplaceInstallLocal] purged ${manifestId}: deleted=${deleted} skipped=${skipped} errors=${errors}`);
|
|
9344
|
+
return c.json({
|
|
9345
|
+
success: true,
|
|
9346
|
+
data: { manifestId, deleted, skipped, errors, withSampleData: false }
|
|
9347
|
+
}, 200);
|
|
9348
|
+
};
|
|
8998
9349
|
/**
|
|
8999
9350
|
* Replicate the start-time side-effects that AppPlugin runs for
|
|
9000
9351
|
* statically-declared apps but the `manifest` service does NOT:
|