@objectstack/core 17.3.0 → 17.4.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/CHANGELOG.md +649 -0
- package/dist/index.cjs +251 -246
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +264 -132
- package/dist/index.d.ts +264 -132
- package/dist/index.js +247 -245
- package/dist/index.js.map +1 -1
- package/dist/logger.cjs.map +1 -1
- package/dist/logger.js.map +1 -1
- package/package.json +7 -6
package/dist/index.js
CHANGED
|
@@ -829,6 +829,29 @@ function isServiceNotRegisteredError(err) {
|
|
|
829
829
|
return typeof err === "object" && err !== null && err[SERVICE_NOT_REGISTERED_BRAND] === true;
|
|
830
830
|
}
|
|
831
831
|
|
|
832
|
+
// src/plugin-contract.ts
|
|
833
|
+
import { PluginSchema } from "@objectstack/spec/kernel";
|
|
834
|
+
var PLUGIN_CONTRACT_VIOLATION_CODE = "PLUGIN_CONTRACT_VIOLATION";
|
|
835
|
+
function assertPluginContract(plugin) {
|
|
836
|
+
const result = PluginSchema.safeParse(plugin);
|
|
837
|
+
if (result.success) {
|
|
838
|
+
return;
|
|
839
|
+
}
|
|
840
|
+
const issues = result.error.issues.filter((issue) => issue.path[0] !== "version");
|
|
841
|
+
if (issues.length === 0) {
|
|
842
|
+
return;
|
|
843
|
+
}
|
|
844
|
+
const first = issues[0];
|
|
845
|
+
const at = first.path.length > 0 ? first.path.join(".") : "(root)";
|
|
846
|
+
const id = plugin.id;
|
|
847
|
+
const named = typeof id === "string" && id.length > 0 ? `'${plugin.name}' (id: ${id})` : `'${plugin.name}'`;
|
|
848
|
+
const error = new Error(
|
|
849
|
+
`${PLUGIN_CONTRACT_VIOLATION_CODE}: plugin ${named} is refused by the declared plugin contract at '${at}': ${first.message}`
|
|
850
|
+
);
|
|
851
|
+
error.code = PLUGIN_CONTRACT_VIOLATION_CODE;
|
|
852
|
+
throw error;
|
|
853
|
+
}
|
|
854
|
+
|
|
832
855
|
// src/plugin-loader.ts
|
|
833
856
|
var ServiceLifecycle = /* @__PURE__ */ ((ServiceLifecycle2) => {
|
|
834
857
|
ServiceLifecycle2["SINGLETON"] = "singleton";
|
|
@@ -866,6 +889,7 @@ var PluginLoader = class {
|
|
|
866
889
|
this.logger.info(`Loading plugin: ${plugin.name}`);
|
|
867
890
|
const metadata = this.toPluginMetadata(plugin);
|
|
868
891
|
this.validatePluginStructure(metadata);
|
|
892
|
+
this.validatePluginContract(metadata);
|
|
869
893
|
const versionCheck = this.checkVersionCompatibility(metadata);
|
|
870
894
|
if (!versionCheck.compatible) {
|
|
871
895
|
throw new Error(`Version incompatible: ${versionCheck.message}`);
|
|
@@ -1050,6 +1074,27 @@ var PluginLoader = class {
|
|
|
1050
1074
|
throw new Error(`Invalid semantic version: ${plugin.version}`);
|
|
1051
1075
|
}
|
|
1052
1076
|
}
|
|
1077
|
+
/**
|
|
1078
|
+
* Refuse a plugin object the DECLARED plugin contract refuses (#16049,
|
|
1079
|
+
* maintainer ruling 2026-09-06: "the protocol is the baseline; the runtime
|
|
1080
|
+
* aligns to it").
|
|
1081
|
+
*
|
|
1082
|
+
* The check itself — `PluginSchema.safeParse` for validation only, the
|
|
1083
|
+
* eight keys it reaches, the `version` exclusion and the
|
|
1084
|
+
* `PLUGIN_CONTRACT_VIOLATION` envelope — lives in `plugin-contract.ts`,
|
|
1085
|
+
* because since #16721 it is ONE statement run by BOTH kernels:
|
|
1086
|
+
* `LiteKernel.use()` calls it directly, and `ObjectKernel.use()` reaches
|
|
1087
|
+
* it here, through `loadPlugin`. That module's comment is the authority on
|
|
1088
|
+
* what is refused; this method adds nothing to it and subtracts nothing.
|
|
1089
|
+
*
|
|
1090
|
+
* What stays THIS loader's own, and is deliberately not shared: the
|
|
1091
|
+
* structural checks one call up ({@link validatePluginStructure} —
|
|
1092
|
+
* `name`, `init`, semver) and the version-compatibility check below.
|
|
1093
|
+
* The convergence is on the schema, not on the loader.
|
|
1094
|
+
*/
|
|
1095
|
+
validatePluginContract(plugin) {
|
|
1096
|
+
assertPluginContract(plugin);
|
|
1097
|
+
}
|
|
1053
1098
|
checkVersionCompatibility(plugin) {
|
|
1054
1099
|
const version = plugin.version;
|
|
1055
1100
|
if (!this.isValidSemanticVersion(version)) {
|
|
@@ -1259,6 +1304,7 @@ function createMemoryI18n() {
|
|
|
1259
1304
|
const authored = /* @__PURE__ */ new Map();
|
|
1260
1305
|
let defaultLocale = "en";
|
|
1261
1306
|
let supportedLocales;
|
|
1307
|
+
let fallbackLocale;
|
|
1262
1308
|
function resolveKey(data, key) {
|
|
1263
1309
|
const parts = key.split(".");
|
|
1264
1310
|
let current = data;
|
|
@@ -1295,7 +1341,11 @@ function createMemoryI18n() {
|
|
|
1295
1341
|
_serviceName: "i18n",
|
|
1296
1342
|
t(key, locale, params) {
|
|
1297
1343
|
const data = resolveTranslations(locale) ?? mergedLocale(defaultLocale);
|
|
1298
|
-
|
|
1344
|
+
let value = data ? resolveKey(data, key) : void 0;
|
|
1345
|
+
if (value === void 0 && fallbackLocale && fallbackLocale !== locale) {
|
|
1346
|
+
const fallbackData = resolveTranslations(fallbackLocale);
|
|
1347
|
+
value = fallbackData ? resolveKey(fallbackData, key) : void 0;
|
|
1348
|
+
}
|
|
1299
1349
|
if (value == null) return key;
|
|
1300
1350
|
if (!params) return value;
|
|
1301
1351
|
return value.replace(/\{\{(\w+)\}\}/g, (_, name) => String(params[name] ?? `{{${name}}}`));
|
|
@@ -1352,6 +1402,21 @@ function createMemoryI18n() {
|
|
|
1352
1402
|
},
|
|
1353
1403
|
setDefaultLocale(locale) {
|
|
1354
1404
|
defaultLocale = locale;
|
|
1405
|
+
},
|
|
1406
|
+
/**
|
|
1407
|
+
* @see II18nService.setFallbackLocale — [#15694]
|
|
1408
|
+
*
|
|
1409
|
+
* ⛔ There is deliberately NO `getFallbackLocale()` beside this. The two
|
|
1410
|
+
* are different questions: this one is what the provider was TOLD, the
|
|
1411
|
+
* accessor is what the serving layer ASKS it in order to build the
|
|
1412
|
+
* metadata-document translators' fallback chain (#14882). Answering the
|
|
1413
|
+
* second from `defaultLocale` — the only value that was always available
|
|
1414
|
+
* here — would settle the default-locale contract question #14882 leaves
|
|
1415
|
+
* deliberately open, from a degraded provider. Without the accessor those
|
|
1416
|
+
* reads keep the resolvers' own default, which is known and intentional.
|
|
1417
|
+
*/
|
|
1418
|
+
setFallbackLocale(locale) {
|
|
1419
|
+
fallbackLocale = locale;
|
|
1355
1420
|
}
|
|
1356
1421
|
};
|
|
1357
1422
|
}
|
|
@@ -1677,6 +1742,7 @@ async function raceWithTimeout(operation, timeoutMs, createTimeoutError) {
|
|
|
1677
1742
|
}
|
|
1678
1743
|
|
|
1679
1744
|
// src/kernel.ts
|
|
1745
|
+
var DEGRADED_CAPABILITIES_SERVICE = "kernel.degraded-capabilities";
|
|
1680
1746
|
var ObjectKernel = class {
|
|
1681
1747
|
constructor(config = {}) {
|
|
1682
1748
|
this.plugins = /* @__PURE__ */ new Map();
|
|
@@ -1684,7 +1750,12 @@ var ObjectKernel = class {
|
|
|
1684
1750
|
this.hooks = /* @__PURE__ */ new Map();
|
|
1685
1751
|
this.state = "idle";
|
|
1686
1752
|
this.startedPlugins = /* @__PURE__ */ new Set();
|
|
1687
|
-
|
|
1753
|
+
/**
|
|
1754
|
+
* Plugin name -> elapsed milliseconds that plugin's `start()` took. These
|
|
1755
|
+
* are DURATIONS, never start instants; the old spelling `pluginStartTimes`
|
|
1756
|
+
* said the opposite of what it held.
|
|
1757
|
+
*/
|
|
1758
|
+
this.pluginStartupDurations = /* @__PURE__ */ new Map();
|
|
1688
1759
|
this.shutdownHandlers = [];
|
|
1689
1760
|
this.config = {
|
|
1690
1761
|
defaultStartupTimeout: 3e4,
|
|
@@ -1871,9 +1942,40 @@ var ObjectKernel = class {
|
|
|
1871
1942
|
}
|
|
1872
1943
|
if (missingCoreServices.length > 0) {
|
|
1873
1944
|
this.logger.warn(`System started with degraded capabilities. Missing core services: ${missingCoreServices.join(", ")}`);
|
|
1945
|
+
this.publishDegradedCapabilities(missingCoreServices);
|
|
1874
1946
|
}
|
|
1875
1947
|
this.logger.info("System requirement check passed");
|
|
1876
1948
|
}
|
|
1949
|
+
/**
|
|
1950
|
+
* Publish this boot's degraded-capabilities conclusion on
|
|
1951
|
+
* {@link DEGRADED_CAPABILITIES_SERVICE} — the data half of the warning
|
|
1952
|
+
* `validateSystemRequirements()` just logged (#16630).
|
|
1953
|
+
*
|
|
1954
|
+
* ⛔ Best-effort, and silent on failure BY DESIGN: this is a diagnostic
|
|
1955
|
+
* readout, and a readout must never be able to fail a boot that the kernel
|
|
1956
|
+
* has just decided is good enough to run. The one way `registerService`
|
|
1957
|
+
* can throw here is a name collision, which the guard above already
|
|
1958
|
+
* forecloses; the `catch` is there so that stays true if either ever
|
|
1959
|
+
* changes. (`recordSeedOutcome` in `@objectstack/runtime` states the same
|
|
1960
|
+
* rule for the same reason.)
|
|
1961
|
+
*
|
|
1962
|
+
* The value is FROZEN and holds a COPY. `getService` hands out the stored
|
|
1963
|
+
* reference, so an unfrozen live array would let any reader edit the
|
|
1964
|
+
* kernel's own record of what was missing — and this record exists
|
|
1965
|
+
* precisely so that two packages cannot disagree about it.
|
|
1966
|
+
*/
|
|
1967
|
+
publishDegradedCapabilities(missingCoreServices) {
|
|
1968
|
+
try {
|
|
1969
|
+
if (this.services.has(DEGRADED_CAPABILITIES_SERVICE) || this.pluginLoader.hasService(DEGRADED_CAPABILITIES_SERVICE)) {
|
|
1970
|
+
return;
|
|
1971
|
+
}
|
|
1972
|
+
this.registerService(
|
|
1973
|
+
DEGRADED_CAPABILITIES_SERVICE,
|
|
1974
|
+
Object.freeze({ missingCoreServices: Object.freeze([...missingCoreServices]) })
|
|
1975
|
+
);
|
|
1976
|
+
} catch {
|
|
1977
|
+
}
|
|
1978
|
+
}
|
|
1877
1979
|
/**
|
|
1878
1980
|
* Bootstrap the kernel with enhanced features
|
|
1879
1981
|
*/
|
|
@@ -1986,11 +2088,23 @@ var ObjectKernel = class {
|
|
|
1986
2088
|
}
|
|
1987
2089
|
return results;
|
|
1988
2090
|
}
|
|
2091
|
+
/**
|
|
2092
|
+
* Per-plugin startup durations: plugin name -> elapsed milliseconds that
|
|
2093
|
+
* plugin's `start()` took. Not start instants -- see
|
|
2094
|
+
* {@link PluginStartupResult.durationMs}.
|
|
2095
|
+
*/
|
|
2096
|
+
getPluginStartupDurations() {
|
|
2097
|
+
return new Map(this.pluginStartupDurations);
|
|
2098
|
+
}
|
|
1989
2099
|
/**
|
|
1990
2100
|
* Get plugin startup metrics
|
|
2101
|
+
*
|
|
2102
|
+
* @deprecated Renamed to {@link ObjectKernel.getPluginStartupDurations},
|
|
2103
|
+
* which states what the values are. Retained as a delegating alias so
|
|
2104
|
+
* nothing has to change on this release; slated for removal.
|
|
1991
2105
|
*/
|
|
1992
2106
|
getPluginMetrics() {
|
|
1993
|
-
return
|
|
2107
|
+
return this.getPluginStartupDurations();
|
|
1994
2108
|
}
|
|
1995
2109
|
/**
|
|
1996
2110
|
* Whether a plugin with the given name has been registered on this kernel.
|
|
@@ -2112,11 +2226,14 @@ var ObjectKernel = class {
|
|
|
2112
2226
|
);
|
|
2113
2227
|
const duration = Date.now() - startTime;
|
|
2114
2228
|
this.startedPlugins.add(plugin.name);
|
|
2115
|
-
this.
|
|
2229
|
+
this.pluginStartupDurations.set(plugin.name, duration);
|
|
2116
2230
|
this.logger.debug(`Plugin started: ${plugin.name} (${duration}ms)`);
|
|
2117
2231
|
return {
|
|
2118
2232
|
success: true,
|
|
2119
2233
|
pluginName: plugin.name,
|
|
2234
|
+
durationMs: duration,
|
|
2235
|
+
// Deprecated alias carrying the same elapsed value; see
|
|
2236
|
+
// PluginStartupResult.startTime.
|
|
2120
2237
|
startTime: duration
|
|
2121
2238
|
};
|
|
2122
2239
|
} catch (error) {
|
|
@@ -2126,6 +2243,9 @@ var ObjectKernel = class {
|
|
|
2126
2243
|
success: false,
|
|
2127
2244
|
pluginName: plugin.name,
|
|
2128
2245
|
error,
|
|
2246
|
+
durationMs: duration,
|
|
2247
|
+
// Deprecated alias carrying the same elapsed value; see
|
|
2248
|
+
// PluginStartupResult.startTime.
|
|
2129
2249
|
startTime: duration,
|
|
2130
2250
|
timedOut: isTimeout
|
|
2131
2251
|
};
|
|
@@ -2313,6 +2433,26 @@ var LiteKernel = class extends ObjectKernelBase {
|
|
|
2313
2433
|
* Register a plugin
|
|
2314
2434
|
* @param plugin - Plugin instance
|
|
2315
2435
|
*
|
|
2436
|
+
* A plugin object the DECLARED plugin contract refuses is refused here,
|
|
2437
|
+
* with `PLUGIN_CONTRACT_VIOLATION` — the same check, the same envelope,
|
|
2438
|
+
* that `ObjectKernel.use()` runs through `PluginLoader` (`plugin-contract.ts`
|
|
2439
|
+
* is the one statement both kernels call; #16721, maintainer ruling
|
|
2440
|
+
* 2026-09-08, option A under #9864's precedent that the kernels converge).
|
|
2441
|
+
*
|
|
2442
|
+
* This method used to write the object straight into the registry, so the
|
|
2443
|
+
* same plugin was accepted by this kernel and refused by `ObjectKernel` —
|
|
2444
|
+
* and `AGENTS.md` names THIS kernel for tests, so a plugin could be green
|
|
2445
|
+
* in vitest and refused at production boot. Measured before converging
|
|
2446
|
+
* (#16721 step 1): of 813 `LiteKernel.use()` calls reachable in this
|
|
2447
|
+
* repository's suites, 807 were accepted by the schema unchanged and the
|
|
2448
|
+
* six refusals came from three test-local fixture objects, none of them
|
|
2449
|
+
* product code.
|
|
2450
|
+
*
|
|
2451
|
+
* Ordering, and why it is pinned: state first (`validateIdle`), then the
|
|
2452
|
+
* contract, then registration — a refused plugin never reaches the
|
|
2453
|
+
* registry, so it can neither be booted nor supersede an earlier
|
|
2454
|
+
* registration under its name.
|
|
2455
|
+
*
|
|
2316
2456
|
* Duplicate names OVERWRITE, with one `warn` naming both versions — the
|
|
2317
2457
|
* declared contract in `plugin-registration.ts`, applied identically by
|
|
2318
2458
|
* `ObjectKernel.use()` (#9864, maintainer ruling 2026-08-19).
|
|
@@ -2326,6 +2466,7 @@ var LiteKernel = class extends ObjectKernelBase {
|
|
|
2326
2466
|
*/
|
|
2327
2467
|
use(plugin) {
|
|
2328
2468
|
this.validateIdle();
|
|
2469
|
+
assertPluginContract(plugin);
|
|
2329
2470
|
registerPluginByName(this.plugins, plugin, this.logger);
|
|
2330
2471
|
return this;
|
|
2331
2472
|
}
|
|
@@ -3951,228 +4092,6 @@ var _PluginSandboxRuntime = class _PluginSandboxRuntime {
|
|
|
3951
4092
|
_PluginSandboxRuntime.MONITORING_INTERVAL_MS = 5e3;
|
|
3952
4093
|
var PluginSandboxRuntime = _PluginSandboxRuntime;
|
|
3953
4094
|
|
|
3954
|
-
// src/security/security-scanner.ts
|
|
3955
|
-
var PluginSecurityScanner = class {
|
|
3956
|
-
constructor(logger, config) {
|
|
3957
|
-
// Known vulnerabilities database (CVE cache)
|
|
3958
|
-
this.vulnerabilityDb = /* @__PURE__ */ new Map();
|
|
3959
|
-
// Scan results cache
|
|
3960
|
-
this.scanResults = /* @__PURE__ */ new Map();
|
|
3961
|
-
this.passThreshold = 70;
|
|
3962
|
-
this.logger = logger.child({ component: "SecurityScanner" });
|
|
3963
|
-
if (config?.passThreshold !== void 0) {
|
|
3964
|
-
this.passThreshold = config.passThreshold;
|
|
3965
|
-
}
|
|
3966
|
-
}
|
|
3967
|
-
/**
|
|
3968
|
-
* Perform a comprehensive security scan on a plugin
|
|
3969
|
-
*/
|
|
3970
|
-
async scan(target) {
|
|
3971
|
-
this.logger.info("Starting security scan", {
|
|
3972
|
-
pluginId: target.pluginId,
|
|
3973
|
-
version: target.version
|
|
3974
|
-
});
|
|
3975
|
-
const issues = [];
|
|
3976
|
-
try {
|
|
3977
|
-
const codeIssues = await this.scanCode(target);
|
|
3978
|
-
issues.push(...codeIssues);
|
|
3979
|
-
const depIssues = await this.scanDependencies(target);
|
|
3980
|
-
issues.push(...depIssues);
|
|
3981
|
-
const malwareIssues = await this.scanMalware(target);
|
|
3982
|
-
issues.push(...malwareIssues);
|
|
3983
|
-
const licenseIssues = await this.scanLicenses(target);
|
|
3984
|
-
issues.push(...licenseIssues);
|
|
3985
|
-
const configIssues = await this.scanConfiguration(target);
|
|
3986
|
-
issues.push(...configIssues);
|
|
3987
|
-
const score = this.calculateSecurityScore(issues);
|
|
3988
|
-
const result = {
|
|
3989
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3990
|
-
scanner: { name: "ObjectStack Security Scanner", version: "1.0.0" },
|
|
3991
|
-
status: score >= this.passThreshold ? "passed" : "failed",
|
|
3992
|
-
vulnerabilities: issues.map((issue) => ({
|
|
3993
|
-
id: issue.id,
|
|
3994
|
-
severity: issue.severity,
|
|
3995
|
-
category: issue.category,
|
|
3996
|
-
title: issue.title,
|
|
3997
|
-
description: issue.description,
|
|
3998
|
-
location: issue.location ? `${issue.location.file}:${issue.location.line}` : void 0,
|
|
3999
|
-
remediation: issue.remediation,
|
|
4000
|
-
affectedVersions: [],
|
|
4001
|
-
exploitAvailable: false,
|
|
4002
|
-
patchAvailable: false
|
|
4003
|
-
})),
|
|
4004
|
-
summary: {
|
|
4005
|
-
totalVulnerabilities: issues.length,
|
|
4006
|
-
criticalCount: issues.filter((i) => i.severity === "critical").length,
|
|
4007
|
-
highCount: issues.filter((i) => i.severity === "high").length,
|
|
4008
|
-
mediumCount: issues.filter((i) => i.severity === "medium").length,
|
|
4009
|
-
lowCount: issues.filter((i) => i.severity === "low").length,
|
|
4010
|
-
infoCount: issues.filter((i) => i.severity === "info").length
|
|
4011
|
-
}
|
|
4012
|
-
};
|
|
4013
|
-
this.scanResults.set(`${target.pluginId}:${target.version}`, result);
|
|
4014
|
-
this.logger.info("Security scan complete", {
|
|
4015
|
-
pluginId: target.pluginId,
|
|
4016
|
-
score,
|
|
4017
|
-
status: result.status,
|
|
4018
|
-
summary: result.summary
|
|
4019
|
-
});
|
|
4020
|
-
return result;
|
|
4021
|
-
} catch (error) {
|
|
4022
|
-
this.logger.error("Security scan failed", {
|
|
4023
|
-
pluginId: target.pluginId,
|
|
4024
|
-
error
|
|
4025
|
-
});
|
|
4026
|
-
throw error;
|
|
4027
|
-
}
|
|
4028
|
-
}
|
|
4029
|
-
/**
|
|
4030
|
-
* Scan code for vulnerabilities
|
|
4031
|
-
*/
|
|
4032
|
-
async scanCode(target) {
|
|
4033
|
-
const issues = [];
|
|
4034
|
-
this.logger.debug("Code scan complete", {
|
|
4035
|
-
pluginId: target.pluginId,
|
|
4036
|
-
issuesFound: issues.length
|
|
4037
|
-
});
|
|
4038
|
-
return issues;
|
|
4039
|
-
}
|
|
4040
|
-
/**
|
|
4041
|
-
* Scan dependencies for known vulnerabilities
|
|
4042
|
-
*/
|
|
4043
|
-
async scanDependencies(target) {
|
|
4044
|
-
const issues = [];
|
|
4045
|
-
if (!target.dependencies) {
|
|
4046
|
-
return issues;
|
|
4047
|
-
}
|
|
4048
|
-
for (const [depName, version] of Object.entries(target.dependencies)) {
|
|
4049
|
-
const vulnKey = `${depName}@${version}`;
|
|
4050
|
-
const vulnerability = this.vulnerabilityDb.get(vulnKey);
|
|
4051
|
-
if (vulnerability) {
|
|
4052
|
-
issues.push({
|
|
4053
|
-
id: `vuln-${vulnerability.cve || depName}`,
|
|
4054
|
-
severity: vulnerability.severity,
|
|
4055
|
-
category: "vulnerability",
|
|
4056
|
-
title: `Vulnerable dependency: ${depName}`,
|
|
4057
|
-
description: `${depName}@${version} has known security vulnerabilities`,
|
|
4058
|
-
remediation: vulnerability.fixedIn ? `Upgrade to ${vulnerability.fixedIn.join(" or ")}` : "No fix available",
|
|
4059
|
-
cve: vulnerability.cve
|
|
4060
|
-
});
|
|
4061
|
-
}
|
|
4062
|
-
}
|
|
4063
|
-
this.logger.debug("Dependency scan complete", {
|
|
4064
|
-
pluginId: target.pluginId,
|
|
4065
|
-
dependencies: Object.keys(target.dependencies).length,
|
|
4066
|
-
vulnerabilities: issues.length
|
|
4067
|
-
});
|
|
4068
|
-
return issues;
|
|
4069
|
-
}
|
|
4070
|
-
/**
|
|
4071
|
-
* Scan for malware patterns
|
|
4072
|
-
*/
|
|
4073
|
-
async scanMalware(target) {
|
|
4074
|
-
const issues = [];
|
|
4075
|
-
this.logger.debug("Malware scan complete", {
|
|
4076
|
-
pluginId: target.pluginId,
|
|
4077
|
-
issuesFound: issues.length
|
|
4078
|
-
});
|
|
4079
|
-
return issues;
|
|
4080
|
-
}
|
|
4081
|
-
/**
|
|
4082
|
-
* Check license compliance
|
|
4083
|
-
*/
|
|
4084
|
-
async scanLicenses(target) {
|
|
4085
|
-
const issues = [];
|
|
4086
|
-
if (!target.dependencies) {
|
|
4087
|
-
return issues;
|
|
4088
|
-
}
|
|
4089
|
-
this.logger.debug("License scan complete", {
|
|
4090
|
-
pluginId: target.pluginId,
|
|
4091
|
-
issuesFound: issues.length
|
|
4092
|
-
});
|
|
4093
|
-
return issues;
|
|
4094
|
-
}
|
|
4095
|
-
/**
|
|
4096
|
-
* Check configuration security
|
|
4097
|
-
*/
|
|
4098
|
-
async scanConfiguration(target) {
|
|
4099
|
-
const issues = [];
|
|
4100
|
-
this.logger.debug("Configuration scan complete", {
|
|
4101
|
-
pluginId: target.pluginId,
|
|
4102
|
-
issuesFound: issues.length
|
|
4103
|
-
});
|
|
4104
|
-
return issues;
|
|
4105
|
-
}
|
|
4106
|
-
/**
|
|
4107
|
-
* Calculate security score based on issues
|
|
4108
|
-
*/
|
|
4109
|
-
calculateSecurityScore(issues) {
|
|
4110
|
-
let score = 100;
|
|
4111
|
-
for (const issue of issues) {
|
|
4112
|
-
switch (issue.severity) {
|
|
4113
|
-
case "critical":
|
|
4114
|
-
score -= 20;
|
|
4115
|
-
break;
|
|
4116
|
-
case "high":
|
|
4117
|
-
score -= 10;
|
|
4118
|
-
break;
|
|
4119
|
-
case "medium":
|
|
4120
|
-
score -= 5;
|
|
4121
|
-
break;
|
|
4122
|
-
case "low":
|
|
4123
|
-
score -= 2;
|
|
4124
|
-
break;
|
|
4125
|
-
case "info":
|
|
4126
|
-
score -= 0;
|
|
4127
|
-
break;
|
|
4128
|
-
}
|
|
4129
|
-
}
|
|
4130
|
-
return Math.max(0, score);
|
|
4131
|
-
}
|
|
4132
|
-
/**
|
|
4133
|
-
* Add a vulnerability to the database
|
|
4134
|
-
*/
|
|
4135
|
-
addVulnerability(packageName, version, vulnerability) {
|
|
4136
|
-
const key = `${packageName}@${version}`;
|
|
4137
|
-
this.vulnerabilityDb.set(key, vulnerability);
|
|
4138
|
-
this.logger.debug("Vulnerability added to database", {
|
|
4139
|
-
package: packageName,
|
|
4140
|
-
version,
|
|
4141
|
-
cve: vulnerability.cve
|
|
4142
|
-
});
|
|
4143
|
-
}
|
|
4144
|
-
/**
|
|
4145
|
-
* Get scan result from cache
|
|
4146
|
-
*/
|
|
4147
|
-
getScanResult(pluginId, version) {
|
|
4148
|
-
return this.scanResults.get(`${pluginId}:${version}`);
|
|
4149
|
-
}
|
|
4150
|
-
/**
|
|
4151
|
-
* Clear scan results cache
|
|
4152
|
-
*/
|
|
4153
|
-
clearCache() {
|
|
4154
|
-
this.scanResults.clear();
|
|
4155
|
-
this.logger.debug("Scan results cache cleared");
|
|
4156
|
-
}
|
|
4157
|
-
/**
|
|
4158
|
-
* Update vulnerability database from external source
|
|
4159
|
-
*/
|
|
4160
|
-
async updateVulnerabilityDatabase() {
|
|
4161
|
-
this.logger.info("Updating vulnerability database");
|
|
4162
|
-
this.logger.info("Vulnerability database updated", {
|
|
4163
|
-
entries: this.vulnerabilityDb.size
|
|
4164
|
-
});
|
|
4165
|
-
}
|
|
4166
|
-
/**
|
|
4167
|
-
* Shutdown security scanner
|
|
4168
|
-
*/
|
|
4169
|
-
shutdown() {
|
|
4170
|
-
this.vulnerabilityDb.clear();
|
|
4171
|
-
this.scanResults.clear();
|
|
4172
|
-
this.logger.info("Security scanner shutdown complete");
|
|
4173
|
-
}
|
|
4174
|
-
};
|
|
4175
|
-
|
|
4176
4095
|
// src/security/api-key.ts
|
|
4177
4096
|
import { createHash as createHash2, randomBytes } from "crypto";
|
|
4178
4097
|
import { postureEnforcesWall, postureUsesUnionScope, normalizeTenancyPosture } from "@objectstack/spec/security";
|
|
@@ -4259,19 +4178,22 @@ async function resolveApiKeyAdmission(ql, headers, nowMs = Date.now(), tenancyPo
|
|
|
4259
4178
|
const userId = row.user_id ?? row.userId;
|
|
4260
4179
|
if (!userId || typeof userId !== "string") return { outcome: "none" };
|
|
4261
4180
|
const tenantId = typeof row.active_organization_id === "string" && row.active_organization_id ? row.active_organization_id : void 0;
|
|
4181
|
+
const keyId = typeof row.id === "string" && row.id ? row.id : void 0;
|
|
4262
4182
|
if (!tenantId && tenancyPosture) {
|
|
4263
4183
|
const posture = tenancyPosture;
|
|
4264
4184
|
if (postureEnforcesWall(posture) && !postureUsesUnionScope(posture)) {
|
|
4265
4185
|
return {
|
|
4266
4186
|
outcome: "refused",
|
|
4267
4187
|
reason: "organization_required",
|
|
4188
|
+
keyId,
|
|
4189
|
+
userId,
|
|
4268
4190
|
message: "This API key carries no organization and cannot be used under the `isolated` tenancy posture, where every organization-scoped read is walled to an active organization. Mint a replacement key \u2014 new keys inherit the minter\u2019s active organization."
|
|
4269
4191
|
};
|
|
4270
4192
|
}
|
|
4271
4193
|
}
|
|
4272
4194
|
return {
|
|
4273
4195
|
outcome: "admitted",
|
|
4274
|
-
principal: { userId, tenantId, scopes: parseScopes(row.scopes) }
|
|
4196
|
+
principal: { userId, keyId, tenantId, scopes: parseScopes(row.scopes) }
|
|
4275
4197
|
};
|
|
4276
4198
|
}
|
|
4277
4199
|
function readHeader(headers, name) {
|
|
@@ -4335,6 +4257,7 @@ import {
|
|
|
4335
4257
|
ORGANIZATION_ADMIN_GRANTS
|
|
4336
4258
|
} from "@objectstack/spec";
|
|
4337
4259
|
import { postureEnforcesWall as postureEnforcesWall2 } from "@objectstack/spec/security";
|
|
4260
|
+
import { isValueDomainMember } from "@objectstack/spec/shared";
|
|
4338
4261
|
|
|
4339
4262
|
// src/security/grant-validity.ts
|
|
4340
4263
|
function toEpochMs(value) {
|
|
@@ -4669,6 +4592,18 @@ function safeJsonParse2(s, fallback) {
|
|
|
4669
4592
|
return fallback;
|
|
4670
4593
|
}
|
|
4671
4594
|
}
|
|
4595
|
+
function warnApiKeyRefusal(details) {
|
|
4596
|
+
const { reason, keyId, userId, organizationId } = details;
|
|
4597
|
+
console.warn(
|
|
4598
|
+
`[security] API key refused (${reason}): key=${keyId ?? "<unknown>"} principal=${userId ?? "<unknown>"} organization=${organizationId ?? "<none>"}. The caller received the generic 401 UNAUTHENTICATED \u2014 this reason is server-side only.`
|
|
4599
|
+
);
|
|
4600
|
+
}
|
|
4601
|
+
function warnSessionOrganizationClaimDropped(details) {
|
|
4602
|
+
const { reason, sessionId, userId, organizationId } = details;
|
|
4603
|
+
console.warn(
|
|
4604
|
+
`[security] Session organization claim dropped (${reason}): session=${sessionId ?? "<unknown>"} principal=${userId ?? "<unknown>"} organization=${organizationId ?? "<none>"}. The session stays authenticated with NO active organization \u2014 the wire is unchanged.`
|
|
4605
|
+
);
|
|
4606
|
+
}
|
|
4672
4607
|
async function tryFind(ql, object, where, limit = 100, organizationId) {
|
|
4673
4608
|
if (!ql || typeof ql.find !== "function") return [];
|
|
4674
4609
|
try {
|
|
@@ -4692,9 +4627,15 @@ async function resolveAuthzContext(input) {
|
|
|
4692
4627
|
};
|
|
4693
4628
|
let userId;
|
|
4694
4629
|
let tenantId;
|
|
4630
|
+
let sessionId;
|
|
4695
4631
|
const admission = await resolveApiKeyAdmission(ql, headers, input.nowMs, input.tenancyPosture);
|
|
4696
4632
|
if (admission.outcome === "refused") {
|
|
4697
|
-
|
|
4633
|
+
warnApiKeyRefusal({
|
|
4634
|
+
reason: admission.reason,
|
|
4635
|
+
keyId: admission.keyId,
|
|
4636
|
+
userId: admission.userId,
|
|
4637
|
+
organizationId: admission.organizationId
|
|
4638
|
+
});
|
|
4698
4639
|
return ctx;
|
|
4699
4640
|
}
|
|
4700
4641
|
const keyPrincipal = admission.outcome === "admitted" ? admission.principal : void 0;
|
|
@@ -4710,6 +4651,8 @@ async function resolveAuthzContext(input) {
|
|
|
4710
4651
|
const sessionData = await input.getSession(headers);
|
|
4711
4652
|
userId = sessionData?.user?.id ?? sessionData?.session?.userId;
|
|
4712
4653
|
tenantId = tenantId ?? sessionData?.session?.activeOrganizationId;
|
|
4654
|
+
const rawSessionId = sessionData?.session?.id;
|
|
4655
|
+
sessionId = typeof rawSessionId === "string" && rawSessionId ? rawSessionId : void 0;
|
|
4713
4656
|
ctx.accessToken = sessionData?.session?.token ?? ctx.accessToken;
|
|
4714
4657
|
if (sessionData?.user?.email) ctx.email = String(sessionData.user.email);
|
|
4715
4658
|
} catch {
|
|
@@ -4719,7 +4662,7 @@ async function resolveAuthzContext(input) {
|
|
|
4719
4662
|
ctx.userId = userId;
|
|
4720
4663
|
if (tenantId) ctx.tenantId = tenantId;
|
|
4721
4664
|
if (!ql || typeof ql.find !== "function") return ctx;
|
|
4722
|
-
|
|
4665
|
+
let grants = await resolveUserAuthzGrants(ql, userId, {
|
|
4723
4666
|
tenantId,
|
|
4724
4667
|
nowMs: input.nowMs,
|
|
4725
4668
|
seedPermissions: ctx.permissions,
|
|
@@ -4728,19 +4671,37 @@ async function resolveAuthzContext(input) {
|
|
|
4728
4671
|
if (keyPrincipal?.tenantId && input.tenancyPosture) {
|
|
4729
4672
|
const posture = input.tenancyPosture;
|
|
4730
4673
|
if (postureEnforcesWall2(posture) && !grants.accessible_org_ids.includes(keyPrincipal.tenantId)) {
|
|
4674
|
+
warnApiKeyRefusal({
|
|
4675
|
+
reason: "organization_membership_ended",
|
|
4676
|
+
keyId: keyPrincipal.keyId,
|
|
4677
|
+
userId: keyPrincipal.userId,
|
|
4678
|
+
organizationId: keyPrincipal.tenantId
|
|
4679
|
+
});
|
|
4731
4680
|
return {
|
|
4732
4681
|
positions: [],
|
|
4733
4682
|
permissions: [],
|
|
4734
4683
|
systemPermissions: [],
|
|
4735
4684
|
org_user_ids: [],
|
|
4736
|
-
accessible_org_ids: []
|
|
4737
|
-
authRefusal: {
|
|
4738
|
-
reason: "organization_membership_ended",
|
|
4739
|
-
message: "This API key authenticates into an organization its owner is no longer a member of. The key was not revoked \u2014 the membership that backed it ended."
|
|
4740
|
-
}
|
|
4685
|
+
accessible_org_ids: []
|
|
4741
4686
|
};
|
|
4742
4687
|
}
|
|
4743
4688
|
}
|
|
4689
|
+
if (!keyPrincipal && tenantId && input.tenancyPosture && postureEnforcesWall2(input.tenancyPosture) && !grants.accessible_org_ids.includes(tenantId)) {
|
|
4690
|
+
warnSessionOrganizationClaimDropped({
|
|
4691
|
+
reason: "organization_membership_ended",
|
|
4692
|
+
sessionId,
|
|
4693
|
+
userId,
|
|
4694
|
+
organizationId: tenantId
|
|
4695
|
+
});
|
|
4696
|
+
tenantId = void 0;
|
|
4697
|
+
delete ctx.tenantId;
|
|
4698
|
+
grants = await resolveUserAuthzGrants(ql, userId, {
|
|
4699
|
+
tenantId,
|
|
4700
|
+
nowMs: input.nowMs,
|
|
4701
|
+
seedPermissions: ctx.permissions,
|
|
4702
|
+
seedEmail: ctx.email
|
|
4703
|
+
});
|
|
4704
|
+
}
|
|
4744
4705
|
ctx.positions = grants.positions;
|
|
4745
4706
|
ctx.permissions = grants.permissions;
|
|
4746
4707
|
ctx.systemPermissions = grants.systemPermissions;
|
|
@@ -4917,17 +4878,9 @@ async function hasPlatformAdminStanding(ql, userId, opts = {}) {
|
|
|
4917
4878
|
return false;
|
|
4918
4879
|
}
|
|
4919
4880
|
}
|
|
4920
|
-
function isValidTimeZone(tz) {
|
|
4921
|
-
try {
|
|
4922
|
-
new Intl.DateTimeFormat("en-US", { timeZone: tz });
|
|
4923
|
-
return true;
|
|
4924
|
-
} catch {
|
|
4925
|
-
return false;
|
|
4926
|
-
}
|
|
4927
|
-
}
|
|
4928
4881
|
function coerceTimeZone(value) {
|
|
4929
4882
|
const s = typeof value === "string" ? value.trim() : value != null ? String(value).trim() : "";
|
|
4930
|
-
return s &&
|
|
4883
|
+
return s && isValueDomainMember("iana_time_zone", s) ? s : void 0;
|
|
4931
4884
|
}
|
|
4932
4885
|
function coerceLocale(value) {
|
|
4933
4886
|
const s = typeof value === "string" ? value.trim() : value != null ? String(value).trim() : "";
|
|
@@ -6016,6 +5969,52 @@ function errText(err) {
|
|
|
6016
5969
|
}
|
|
6017
5970
|
}
|
|
6018
5971
|
|
|
5972
|
+
// src/utils/advisory-aggregation.ts
|
|
5973
|
+
import { AsyncLocalStorage } from "async_hooks";
|
|
5974
|
+
var ADVISORY_SAMPLE_ROWS = 5;
|
|
5975
|
+
var storage = new AsyncLocalStorage();
|
|
5976
|
+
function keyOf(object, rule) {
|
|
5977
|
+
return JSON.stringify([object, rule]);
|
|
5978
|
+
}
|
|
5979
|
+
function recordAdvisoryHit(hit) {
|
|
5980
|
+
const collector = storage.getStore();
|
|
5981
|
+
if (!collector) return false;
|
|
5982
|
+
const key = keyOf(hit.object, hit.rule);
|
|
5983
|
+
const existing = collector.groups.get(key);
|
|
5984
|
+
if (existing) {
|
|
5985
|
+
existing.rows += 1;
|
|
5986
|
+
if (hit.recordRef != null && existing.sampleRows.length < ADVISORY_SAMPLE_ROWS) {
|
|
5987
|
+
existing.sampleRows.push(hit.recordRef);
|
|
5988
|
+
}
|
|
5989
|
+
return true;
|
|
5990
|
+
}
|
|
5991
|
+
collector.groups.set(key, {
|
|
5992
|
+
object: hit.object,
|
|
5993
|
+
rule: hit.rule,
|
|
5994
|
+
severity: hit.severity,
|
|
5995
|
+
message: hit.message,
|
|
5996
|
+
rows: 1,
|
|
5997
|
+
sampleRows: hit.recordRef != null ? [hit.recordRef] : []
|
|
5998
|
+
});
|
|
5999
|
+
return true;
|
|
6000
|
+
}
|
|
6001
|
+
function isAggregatingAdvisories() {
|
|
6002
|
+
return storage.getStore() !== void 0;
|
|
6003
|
+
}
|
|
6004
|
+
async function runWithAdvisoryAggregation(fn, report) {
|
|
6005
|
+
const collector = { groups: /* @__PURE__ */ new Map() };
|
|
6006
|
+
try {
|
|
6007
|
+
return await storage.run(collector, fn);
|
|
6008
|
+
} finally {
|
|
6009
|
+
if (collector.groups.size > 0) {
|
|
6010
|
+
try {
|
|
6011
|
+
report([...collector.groups.values()]);
|
|
6012
|
+
} catch {
|
|
6013
|
+
}
|
|
6014
|
+
}
|
|
6015
|
+
}
|
|
6016
|
+
}
|
|
6017
|
+
|
|
6019
6018
|
// src/utils/filter-tokens.ts
|
|
6020
6019
|
import {
|
|
6021
6020
|
classifyFilterToken,
|
|
@@ -6534,7 +6533,7 @@ var PluginHealthMonitor = class {
|
|
|
6534
6533
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6535
6534
|
message,
|
|
6536
6535
|
metrics: {
|
|
6537
|
-
|
|
6536
|
+
uptimeMs: Date.now() - startTime
|
|
6538
6537
|
},
|
|
6539
6538
|
checks: checks.length > 0 ? checks : void 0
|
|
6540
6539
|
};
|
|
@@ -7395,6 +7394,7 @@ import { UNMATCHED_ROUTE_PATTERN } from "@objectstack/spec/contracts";
|
|
|
7395
7394
|
export {
|
|
7396
7395
|
ADMIN_STANDING_NON_TABLE_INPUTS,
|
|
7397
7396
|
ADMIN_STANDING_SURFACE,
|
|
7397
|
+
ADVISORY_SAMPLE_ROWS,
|
|
7398
7398
|
ANONYMOUS_DENY_BODY,
|
|
7399
7399
|
ANONYMOUS_DENY_CODE,
|
|
7400
7400
|
ANONYMOUS_DENY_MESSAGE,
|
|
@@ -7432,7 +7432,6 @@ export {
|
|
|
7432
7432
|
PluginPermissionEnforcer,
|
|
7433
7433
|
PluginPermissionManager,
|
|
7434
7434
|
PluginSandboxRuntime,
|
|
7435
|
-
PluginSecurityScanner,
|
|
7436
7435
|
PluginSignatureVerifier,
|
|
7437
7436
|
qa_exports as QA,
|
|
7438
7437
|
SERVICE_NOT_REGISTERED_CODE,
|
|
@@ -7483,6 +7482,7 @@ export {
|
|
|
7483
7482
|
hasPlatformAdminStanding,
|
|
7484
7483
|
hashApiKey,
|
|
7485
7484
|
hashMigrationPlan,
|
|
7485
|
+
isAggregatingAdvisories,
|
|
7486
7486
|
isAudienceBindingSuggestionStatus,
|
|
7487
7487
|
isAuthGateAllowlisted,
|
|
7488
7488
|
isAuthzStoreUnavailableError,
|
|
@@ -7507,6 +7507,7 @@ export {
|
|
|
7507
7507
|
readAuthoredTranslationLayer,
|
|
7508
7508
|
readAuthzGrantsCacheTtlMs,
|
|
7509
7509
|
readRunJournal,
|
|
7510
|
+
recordAdvisoryHit,
|
|
7510
7511
|
recordNotFoundError,
|
|
7511
7512
|
reportAuthzCachePosture,
|
|
7512
7513
|
reportLegacyPlatformAdminGrant,
|
|
@@ -7527,6 +7528,7 @@ export {
|
|
|
7527
7528
|
resumeMigrationJournal,
|
|
7528
7529
|
rethrowAuthzStoreUnavailable,
|
|
7529
7530
|
runMigrationJournal,
|
|
7531
|
+
runWithAdvisoryAggregation,
|
|
7530
7532
|
safeExit,
|
|
7531
7533
|
setPlatformAdminConfigSink,
|
|
7532
7534
|
shouldDenyAnonymous,
|