@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/dist/index.cjs CHANGED
@@ -32,6 +32,7 @@ var index_exports = {};
32
32
  __export(index_exports, {
33
33
  ADMIN_STANDING_NON_TABLE_INPUTS: () => ADMIN_STANDING_NON_TABLE_INPUTS,
34
34
  ADMIN_STANDING_SURFACE: () => ADMIN_STANDING_SURFACE,
35
+ ADVISORY_SAMPLE_ROWS: () => ADVISORY_SAMPLE_ROWS,
35
36
  ANONYMOUS_DENY_BODY: () => ANONYMOUS_DENY_BODY,
36
37
  ANONYMOUS_DENY_CODE: () => ANONYMOUS_DENY_CODE,
37
38
  ANONYMOUS_DENY_MESSAGE: () => ANONYMOUS_DENY_MESSAGE,
@@ -69,7 +70,6 @@ __export(index_exports, {
69
70
  PluginPermissionEnforcer: () => PluginPermissionEnforcer,
70
71
  PluginPermissionManager: () => PluginPermissionManager,
71
72
  PluginSandboxRuntime: () => PluginSandboxRuntime,
72
- PluginSecurityScanner: () => PluginSecurityScanner,
73
73
  PluginSignatureVerifier: () => PluginSignatureVerifier,
74
74
  QA: () => qa_exports,
75
75
  SERVICE_NOT_REGISTERED_CODE: () => SERVICE_NOT_REGISTERED_CODE,
@@ -120,6 +120,7 @@ __export(index_exports, {
120
120
  hasPlatformAdminStanding: () => hasPlatformAdminStanding,
121
121
  hashApiKey: () => hashApiKey,
122
122
  hashMigrationPlan: () => hashMigrationPlan,
123
+ isAggregatingAdvisories: () => isAggregatingAdvisories,
123
124
  isAudienceBindingSuggestionStatus: () => isAudienceBindingSuggestionStatus,
124
125
  isAuthGateAllowlisted: () => isAuthGateAllowlisted,
125
126
  isAuthzStoreUnavailableError: () => isAuthzStoreUnavailableError,
@@ -144,6 +145,7 @@ __export(index_exports, {
144
145
  readAuthoredTranslationLayer: () => readAuthoredTranslationLayer,
145
146
  readAuthzGrantsCacheTtlMs: () => readAuthzGrantsCacheTtlMs,
146
147
  readRunJournal: () => readRunJournal,
148
+ recordAdvisoryHit: () => recordAdvisoryHit,
147
149
  recordNotFoundError: () => recordNotFoundError,
148
150
  reportAuthzCachePosture: () => reportAuthzCachePosture,
149
151
  reportLegacyPlatformAdminGrant: () => reportLegacyPlatformAdminGrant,
@@ -164,6 +166,7 @@ __export(index_exports, {
164
166
  resumeMigrationJournal: () => resumeMigrationJournal,
165
167
  rethrowAuthzStoreUnavailable: () => rethrowAuthzStoreUnavailable,
166
168
  runMigrationJournal: () => runMigrationJournal,
169
+ runWithAdvisoryAggregation: () => runWithAdvisoryAggregation,
167
170
  safeExit: () => safeExit,
168
171
  setPlatformAdminConfigSink: () => setPlatformAdminConfigSink,
169
172
  shouldDenyAnonymous: () => shouldDenyAnonymous,
@@ -998,6 +1001,29 @@ function isServiceNotRegisteredError(err) {
998
1001
  return typeof err === "object" && err !== null && err[SERVICE_NOT_REGISTERED_BRAND] === true;
999
1002
  }
1000
1003
 
1004
+ // src/plugin-contract.ts
1005
+ var import_kernel = require("@objectstack/spec/kernel");
1006
+ var PLUGIN_CONTRACT_VIOLATION_CODE = "PLUGIN_CONTRACT_VIOLATION";
1007
+ function assertPluginContract(plugin) {
1008
+ const result = import_kernel.PluginSchema.safeParse(plugin);
1009
+ if (result.success) {
1010
+ return;
1011
+ }
1012
+ const issues = result.error.issues.filter((issue) => issue.path[0] !== "version");
1013
+ if (issues.length === 0) {
1014
+ return;
1015
+ }
1016
+ const first = issues[0];
1017
+ const at = first.path.length > 0 ? first.path.join(".") : "(root)";
1018
+ const id = plugin.id;
1019
+ const named = typeof id === "string" && id.length > 0 ? `'${plugin.name}' (id: ${id})` : `'${plugin.name}'`;
1020
+ const error = new Error(
1021
+ `${PLUGIN_CONTRACT_VIOLATION_CODE}: plugin ${named} is refused by the declared plugin contract at '${at}': ${first.message}`
1022
+ );
1023
+ error.code = PLUGIN_CONTRACT_VIOLATION_CODE;
1024
+ throw error;
1025
+ }
1026
+
1001
1027
  // src/plugin-loader.ts
1002
1028
  var ServiceLifecycle = /* @__PURE__ */ ((ServiceLifecycle2) => {
1003
1029
  ServiceLifecycle2["SINGLETON"] = "singleton";
@@ -1035,6 +1061,7 @@ var PluginLoader = class {
1035
1061
  this.logger.info(`Loading plugin: ${plugin.name}`);
1036
1062
  const metadata = this.toPluginMetadata(plugin);
1037
1063
  this.validatePluginStructure(metadata);
1064
+ this.validatePluginContract(metadata);
1038
1065
  const versionCheck = this.checkVersionCompatibility(metadata);
1039
1066
  if (!versionCheck.compatible) {
1040
1067
  throw new Error(`Version incompatible: ${versionCheck.message}`);
@@ -1219,6 +1246,27 @@ var PluginLoader = class {
1219
1246
  throw new Error(`Invalid semantic version: ${plugin.version}`);
1220
1247
  }
1221
1248
  }
1249
+ /**
1250
+ * Refuse a plugin object the DECLARED plugin contract refuses (#16049,
1251
+ * maintainer ruling 2026-09-06: "the protocol is the baseline; the runtime
1252
+ * aligns to it").
1253
+ *
1254
+ * The check itself — `PluginSchema.safeParse` for validation only, the
1255
+ * eight keys it reaches, the `version` exclusion and the
1256
+ * `PLUGIN_CONTRACT_VIOLATION` envelope — lives in `plugin-contract.ts`,
1257
+ * because since #16721 it is ONE statement run by BOTH kernels:
1258
+ * `LiteKernel.use()` calls it directly, and `ObjectKernel.use()` reaches
1259
+ * it here, through `loadPlugin`. That module's comment is the authority on
1260
+ * what is refused; this method adds nothing to it and subtracts nothing.
1261
+ *
1262
+ * What stays THIS loader's own, and is deliberately not shared: the
1263
+ * structural checks one call up ({@link validatePluginStructure} —
1264
+ * `name`, `init`, semver) and the version-compatibility check below.
1265
+ * The convergence is on the schema, not on the loader.
1266
+ */
1267
+ validatePluginContract(plugin) {
1268
+ assertPluginContract(plugin);
1269
+ }
1222
1270
  checkVersionCompatibility(plugin) {
1223
1271
  const version = plugin.version;
1224
1272
  if (!this.isValidSemanticVersion(version)) {
@@ -1428,6 +1476,7 @@ function createMemoryI18n() {
1428
1476
  const authored = /* @__PURE__ */ new Map();
1429
1477
  let defaultLocale = "en";
1430
1478
  let supportedLocales;
1479
+ let fallbackLocale;
1431
1480
  function resolveKey(data, key) {
1432
1481
  const parts = key.split(".");
1433
1482
  let current = data;
@@ -1464,7 +1513,11 @@ function createMemoryI18n() {
1464
1513
  _serviceName: "i18n",
1465
1514
  t(key, locale, params) {
1466
1515
  const data = resolveTranslations(locale) ?? mergedLocale(defaultLocale);
1467
- const value = data ? resolveKey(data, key) : void 0;
1516
+ let value = data ? resolveKey(data, key) : void 0;
1517
+ if (value === void 0 && fallbackLocale && fallbackLocale !== locale) {
1518
+ const fallbackData = resolveTranslations(fallbackLocale);
1519
+ value = fallbackData ? resolveKey(fallbackData, key) : void 0;
1520
+ }
1468
1521
  if (value == null) return key;
1469
1522
  if (!params) return value;
1470
1523
  return value.replace(/\{\{(\w+)\}\}/g, (_, name) => String(params[name] ?? `{{${name}}}`));
@@ -1521,6 +1574,21 @@ function createMemoryI18n() {
1521
1574
  },
1522
1575
  setDefaultLocale(locale) {
1523
1576
  defaultLocale = locale;
1577
+ },
1578
+ /**
1579
+ * @see II18nService.setFallbackLocale — [#15694]
1580
+ *
1581
+ * ⛔ There is deliberately NO `getFallbackLocale()` beside this. The two
1582
+ * are different questions: this one is what the provider was TOLD, the
1583
+ * accessor is what the serving layer ASKS it in order to build the
1584
+ * metadata-document translators' fallback chain (#14882). Answering the
1585
+ * second from `defaultLocale` — the only value that was always available
1586
+ * here — would settle the default-locale contract question #14882 leaves
1587
+ * deliberately open, from a degraded provider. Without the accessor those
1588
+ * reads keep the resolvers' own default, which is known and intentional.
1589
+ */
1590
+ setFallbackLocale(locale) {
1591
+ fallbackLocale = locale;
1524
1592
  }
1525
1593
  };
1526
1594
  }
@@ -1846,6 +1914,7 @@ async function raceWithTimeout(operation, timeoutMs, createTimeoutError) {
1846
1914
  }
1847
1915
 
1848
1916
  // src/kernel.ts
1917
+ var DEGRADED_CAPABILITIES_SERVICE = "kernel.degraded-capabilities";
1849
1918
  var ObjectKernel = class {
1850
1919
  constructor(config = {}) {
1851
1920
  this.plugins = /* @__PURE__ */ new Map();
@@ -1853,7 +1922,12 @@ var ObjectKernel = class {
1853
1922
  this.hooks = /* @__PURE__ */ new Map();
1854
1923
  this.state = "idle";
1855
1924
  this.startedPlugins = /* @__PURE__ */ new Set();
1856
- this.pluginStartTimes = /* @__PURE__ */ new Map();
1925
+ /**
1926
+ * Plugin name -> elapsed milliseconds that plugin's `start()` took. These
1927
+ * are DURATIONS, never start instants; the old spelling `pluginStartTimes`
1928
+ * said the opposite of what it held.
1929
+ */
1930
+ this.pluginStartupDurations = /* @__PURE__ */ new Map();
1857
1931
  this.shutdownHandlers = [];
1858
1932
  this.config = {
1859
1933
  defaultStartupTimeout: 3e4,
@@ -2040,9 +2114,40 @@ var ObjectKernel = class {
2040
2114
  }
2041
2115
  if (missingCoreServices.length > 0) {
2042
2116
  this.logger.warn(`System started with degraded capabilities. Missing core services: ${missingCoreServices.join(", ")}`);
2117
+ this.publishDegradedCapabilities(missingCoreServices);
2043
2118
  }
2044
2119
  this.logger.info("System requirement check passed");
2045
2120
  }
2121
+ /**
2122
+ * Publish this boot's degraded-capabilities conclusion on
2123
+ * {@link DEGRADED_CAPABILITIES_SERVICE} — the data half of the warning
2124
+ * `validateSystemRequirements()` just logged (#16630).
2125
+ *
2126
+ * ⛔ Best-effort, and silent on failure BY DESIGN: this is a diagnostic
2127
+ * readout, and a readout must never be able to fail a boot that the kernel
2128
+ * has just decided is good enough to run. The one way `registerService`
2129
+ * can throw here is a name collision, which the guard above already
2130
+ * forecloses; the `catch` is there so that stays true if either ever
2131
+ * changes. (`recordSeedOutcome` in `@objectstack/runtime` states the same
2132
+ * rule for the same reason.)
2133
+ *
2134
+ * The value is FROZEN and holds a COPY. `getService` hands out the stored
2135
+ * reference, so an unfrozen live array would let any reader edit the
2136
+ * kernel's own record of what was missing — and this record exists
2137
+ * precisely so that two packages cannot disagree about it.
2138
+ */
2139
+ publishDegradedCapabilities(missingCoreServices) {
2140
+ try {
2141
+ if (this.services.has(DEGRADED_CAPABILITIES_SERVICE) || this.pluginLoader.hasService(DEGRADED_CAPABILITIES_SERVICE)) {
2142
+ return;
2143
+ }
2144
+ this.registerService(
2145
+ DEGRADED_CAPABILITIES_SERVICE,
2146
+ Object.freeze({ missingCoreServices: Object.freeze([...missingCoreServices]) })
2147
+ );
2148
+ } catch {
2149
+ }
2150
+ }
2046
2151
  /**
2047
2152
  * Bootstrap the kernel with enhanced features
2048
2153
  */
@@ -2155,11 +2260,23 @@ var ObjectKernel = class {
2155
2260
  }
2156
2261
  return results;
2157
2262
  }
2263
+ /**
2264
+ * Per-plugin startup durations: plugin name -> elapsed milliseconds that
2265
+ * plugin's `start()` took. Not start instants -- see
2266
+ * {@link PluginStartupResult.durationMs}.
2267
+ */
2268
+ getPluginStartupDurations() {
2269
+ return new Map(this.pluginStartupDurations);
2270
+ }
2158
2271
  /**
2159
2272
  * Get plugin startup metrics
2273
+ *
2274
+ * @deprecated Renamed to {@link ObjectKernel.getPluginStartupDurations},
2275
+ * which states what the values are. Retained as a delegating alias so
2276
+ * nothing has to change on this release; slated for removal.
2160
2277
  */
2161
2278
  getPluginMetrics() {
2162
- return new Map(this.pluginStartTimes);
2279
+ return this.getPluginStartupDurations();
2163
2280
  }
2164
2281
  /**
2165
2282
  * Whether a plugin with the given name has been registered on this kernel.
@@ -2281,11 +2398,14 @@ var ObjectKernel = class {
2281
2398
  );
2282
2399
  const duration = Date.now() - startTime;
2283
2400
  this.startedPlugins.add(plugin.name);
2284
- this.pluginStartTimes.set(plugin.name, duration);
2401
+ this.pluginStartupDurations.set(plugin.name, duration);
2285
2402
  this.logger.debug(`Plugin started: ${plugin.name} (${duration}ms)`);
2286
2403
  return {
2287
2404
  success: true,
2288
2405
  pluginName: plugin.name,
2406
+ durationMs: duration,
2407
+ // Deprecated alias carrying the same elapsed value; see
2408
+ // PluginStartupResult.startTime.
2289
2409
  startTime: duration
2290
2410
  };
2291
2411
  } catch (error) {
@@ -2295,6 +2415,9 @@ var ObjectKernel = class {
2295
2415
  success: false,
2296
2416
  pluginName: plugin.name,
2297
2417
  error,
2418
+ durationMs: duration,
2419
+ // Deprecated alias carrying the same elapsed value; see
2420
+ // PluginStartupResult.startTime.
2298
2421
  startTime: duration,
2299
2422
  timedOut: isTimeout
2300
2423
  };
@@ -2482,6 +2605,26 @@ var LiteKernel = class extends ObjectKernelBase {
2482
2605
  * Register a plugin
2483
2606
  * @param plugin - Plugin instance
2484
2607
  *
2608
+ * A plugin object the DECLARED plugin contract refuses is refused here,
2609
+ * with `PLUGIN_CONTRACT_VIOLATION` — the same check, the same envelope,
2610
+ * that `ObjectKernel.use()` runs through `PluginLoader` (`plugin-contract.ts`
2611
+ * is the one statement both kernels call; #16721, maintainer ruling
2612
+ * 2026-09-08, option A under #9864's precedent that the kernels converge).
2613
+ *
2614
+ * This method used to write the object straight into the registry, so the
2615
+ * same plugin was accepted by this kernel and refused by `ObjectKernel` —
2616
+ * and `AGENTS.md` names THIS kernel for tests, so a plugin could be green
2617
+ * in vitest and refused at production boot. Measured before converging
2618
+ * (#16721 step 1): of 813 `LiteKernel.use()` calls reachable in this
2619
+ * repository's suites, 807 were accepted by the schema unchanged and the
2620
+ * six refusals came from three test-local fixture objects, none of them
2621
+ * product code.
2622
+ *
2623
+ * Ordering, and why it is pinned: state first (`validateIdle`), then the
2624
+ * contract, then registration — a refused plugin never reaches the
2625
+ * registry, so it can neither be booted nor supersede an earlier
2626
+ * registration under its name.
2627
+ *
2485
2628
  * Duplicate names OVERWRITE, with one `warn` naming both versions — the
2486
2629
  * declared contract in `plugin-registration.ts`, applied identically by
2487
2630
  * `ObjectKernel.use()` (#9864, maintainer ruling 2026-08-19).
@@ -2495,6 +2638,7 @@ var LiteKernel = class extends ObjectKernelBase {
2495
2638
  */
2496
2639
  use(plugin) {
2497
2640
  this.validateIdle();
2641
+ assertPluginContract(plugin);
2498
2642
  registerPluginByName(this.plugins, plugin, this.logger);
2499
2643
  return this;
2500
2644
  }
@@ -4120,228 +4264,6 @@ var _PluginSandboxRuntime = class _PluginSandboxRuntime {
4120
4264
  _PluginSandboxRuntime.MONITORING_INTERVAL_MS = 5e3;
4121
4265
  var PluginSandboxRuntime = _PluginSandboxRuntime;
4122
4266
 
4123
- // src/security/security-scanner.ts
4124
- var PluginSecurityScanner = class {
4125
- constructor(logger, config) {
4126
- // Known vulnerabilities database (CVE cache)
4127
- this.vulnerabilityDb = /* @__PURE__ */ new Map();
4128
- // Scan results cache
4129
- this.scanResults = /* @__PURE__ */ new Map();
4130
- this.passThreshold = 70;
4131
- this.logger = logger.child({ component: "SecurityScanner" });
4132
- if (config?.passThreshold !== void 0) {
4133
- this.passThreshold = config.passThreshold;
4134
- }
4135
- }
4136
- /**
4137
- * Perform a comprehensive security scan on a plugin
4138
- */
4139
- async scan(target) {
4140
- this.logger.info("Starting security scan", {
4141
- pluginId: target.pluginId,
4142
- version: target.version
4143
- });
4144
- const issues = [];
4145
- try {
4146
- const codeIssues = await this.scanCode(target);
4147
- issues.push(...codeIssues);
4148
- const depIssues = await this.scanDependencies(target);
4149
- issues.push(...depIssues);
4150
- const malwareIssues = await this.scanMalware(target);
4151
- issues.push(...malwareIssues);
4152
- const licenseIssues = await this.scanLicenses(target);
4153
- issues.push(...licenseIssues);
4154
- const configIssues = await this.scanConfiguration(target);
4155
- issues.push(...configIssues);
4156
- const score = this.calculateSecurityScore(issues);
4157
- const result = {
4158
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
4159
- scanner: { name: "ObjectStack Security Scanner", version: "1.0.0" },
4160
- status: score >= this.passThreshold ? "passed" : "failed",
4161
- vulnerabilities: issues.map((issue) => ({
4162
- id: issue.id,
4163
- severity: issue.severity,
4164
- category: issue.category,
4165
- title: issue.title,
4166
- description: issue.description,
4167
- location: issue.location ? `${issue.location.file}:${issue.location.line}` : void 0,
4168
- remediation: issue.remediation,
4169
- affectedVersions: [],
4170
- exploitAvailable: false,
4171
- patchAvailable: false
4172
- })),
4173
- summary: {
4174
- totalVulnerabilities: issues.length,
4175
- criticalCount: issues.filter((i) => i.severity === "critical").length,
4176
- highCount: issues.filter((i) => i.severity === "high").length,
4177
- mediumCount: issues.filter((i) => i.severity === "medium").length,
4178
- lowCount: issues.filter((i) => i.severity === "low").length,
4179
- infoCount: issues.filter((i) => i.severity === "info").length
4180
- }
4181
- };
4182
- this.scanResults.set(`${target.pluginId}:${target.version}`, result);
4183
- this.logger.info("Security scan complete", {
4184
- pluginId: target.pluginId,
4185
- score,
4186
- status: result.status,
4187
- summary: result.summary
4188
- });
4189
- return result;
4190
- } catch (error) {
4191
- this.logger.error("Security scan failed", {
4192
- pluginId: target.pluginId,
4193
- error
4194
- });
4195
- throw error;
4196
- }
4197
- }
4198
- /**
4199
- * Scan code for vulnerabilities
4200
- */
4201
- async scanCode(target) {
4202
- const issues = [];
4203
- this.logger.debug("Code scan complete", {
4204
- pluginId: target.pluginId,
4205
- issuesFound: issues.length
4206
- });
4207
- return issues;
4208
- }
4209
- /**
4210
- * Scan dependencies for known vulnerabilities
4211
- */
4212
- async scanDependencies(target) {
4213
- const issues = [];
4214
- if (!target.dependencies) {
4215
- return issues;
4216
- }
4217
- for (const [depName, version] of Object.entries(target.dependencies)) {
4218
- const vulnKey = `${depName}@${version}`;
4219
- const vulnerability = this.vulnerabilityDb.get(vulnKey);
4220
- if (vulnerability) {
4221
- issues.push({
4222
- id: `vuln-${vulnerability.cve || depName}`,
4223
- severity: vulnerability.severity,
4224
- category: "vulnerability",
4225
- title: `Vulnerable dependency: ${depName}`,
4226
- description: `${depName}@${version} has known security vulnerabilities`,
4227
- remediation: vulnerability.fixedIn ? `Upgrade to ${vulnerability.fixedIn.join(" or ")}` : "No fix available",
4228
- cve: vulnerability.cve
4229
- });
4230
- }
4231
- }
4232
- this.logger.debug("Dependency scan complete", {
4233
- pluginId: target.pluginId,
4234
- dependencies: Object.keys(target.dependencies).length,
4235
- vulnerabilities: issues.length
4236
- });
4237
- return issues;
4238
- }
4239
- /**
4240
- * Scan for malware patterns
4241
- */
4242
- async scanMalware(target) {
4243
- const issues = [];
4244
- this.logger.debug("Malware scan complete", {
4245
- pluginId: target.pluginId,
4246
- issuesFound: issues.length
4247
- });
4248
- return issues;
4249
- }
4250
- /**
4251
- * Check license compliance
4252
- */
4253
- async scanLicenses(target) {
4254
- const issues = [];
4255
- if (!target.dependencies) {
4256
- return issues;
4257
- }
4258
- this.logger.debug("License scan complete", {
4259
- pluginId: target.pluginId,
4260
- issuesFound: issues.length
4261
- });
4262
- return issues;
4263
- }
4264
- /**
4265
- * Check configuration security
4266
- */
4267
- async scanConfiguration(target) {
4268
- const issues = [];
4269
- this.logger.debug("Configuration scan complete", {
4270
- pluginId: target.pluginId,
4271
- issuesFound: issues.length
4272
- });
4273
- return issues;
4274
- }
4275
- /**
4276
- * Calculate security score based on issues
4277
- */
4278
- calculateSecurityScore(issues) {
4279
- let score = 100;
4280
- for (const issue of issues) {
4281
- switch (issue.severity) {
4282
- case "critical":
4283
- score -= 20;
4284
- break;
4285
- case "high":
4286
- score -= 10;
4287
- break;
4288
- case "medium":
4289
- score -= 5;
4290
- break;
4291
- case "low":
4292
- score -= 2;
4293
- break;
4294
- case "info":
4295
- score -= 0;
4296
- break;
4297
- }
4298
- }
4299
- return Math.max(0, score);
4300
- }
4301
- /**
4302
- * Add a vulnerability to the database
4303
- */
4304
- addVulnerability(packageName, version, vulnerability) {
4305
- const key = `${packageName}@${version}`;
4306
- this.vulnerabilityDb.set(key, vulnerability);
4307
- this.logger.debug("Vulnerability added to database", {
4308
- package: packageName,
4309
- version,
4310
- cve: vulnerability.cve
4311
- });
4312
- }
4313
- /**
4314
- * Get scan result from cache
4315
- */
4316
- getScanResult(pluginId, version) {
4317
- return this.scanResults.get(`${pluginId}:${version}`);
4318
- }
4319
- /**
4320
- * Clear scan results cache
4321
- */
4322
- clearCache() {
4323
- this.scanResults.clear();
4324
- this.logger.debug("Scan results cache cleared");
4325
- }
4326
- /**
4327
- * Update vulnerability database from external source
4328
- */
4329
- async updateVulnerabilityDatabase() {
4330
- this.logger.info("Updating vulnerability database");
4331
- this.logger.info("Vulnerability database updated", {
4332
- entries: this.vulnerabilityDb.size
4333
- });
4334
- }
4335
- /**
4336
- * Shutdown security scanner
4337
- */
4338
- shutdown() {
4339
- this.vulnerabilityDb.clear();
4340
- this.scanResults.clear();
4341
- this.logger.info("Security scanner shutdown complete");
4342
- }
4343
- };
4344
-
4345
4267
  // src/security/api-key.ts
4346
4268
  var import_node_crypto3 = require("crypto");
4347
4269
  var import_security = require("@objectstack/spec/security");
@@ -4428,19 +4350,22 @@ async function resolveApiKeyAdmission(ql, headers, nowMs = Date.now(), tenancyPo
4428
4350
  const userId = row.user_id ?? row.userId;
4429
4351
  if (!userId || typeof userId !== "string") return { outcome: "none" };
4430
4352
  const tenantId = typeof row.active_organization_id === "string" && row.active_organization_id ? row.active_organization_id : void 0;
4353
+ const keyId = typeof row.id === "string" && row.id ? row.id : void 0;
4431
4354
  if (!tenantId && tenancyPosture) {
4432
4355
  const posture = tenancyPosture;
4433
4356
  if ((0, import_security.postureEnforcesWall)(posture) && !(0, import_security.postureUsesUnionScope)(posture)) {
4434
4357
  return {
4435
4358
  outcome: "refused",
4436
4359
  reason: "organization_required",
4360
+ keyId,
4361
+ userId,
4437
4362
  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."
4438
4363
  };
4439
4364
  }
4440
4365
  }
4441
4366
  return {
4442
4367
  outcome: "admitted",
4443
- principal: { userId, tenantId, scopes: parseScopes(row.scopes) }
4368
+ principal: { userId, keyId, tenantId, scopes: parseScopes(row.scopes) }
4444
4369
  };
4445
4370
  }
4446
4371
  function readHeader(headers, name) {
@@ -4498,6 +4423,7 @@ function rethrowAuthzStoreUnavailable(err) {
4498
4423
  var import_types2 = require("@objectstack/types");
4499
4424
  var import_spec2 = require("@objectstack/spec");
4500
4425
  var import_security2 = require("@objectstack/spec/security");
4426
+ var import_shared = require("@objectstack/spec/shared");
4501
4427
 
4502
4428
  // src/security/grant-validity.ts
4503
4429
  function toEpochMs(value) {
@@ -4832,6 +4758,18 @@ function safeJsonParse2(s, fallback) {
4832
4758
  return fallback;
4833
4759
  }
4834
4760
  }
4761
+ function warnApiKeyRefusal(details) {
4762
+ const { reason, keyId, userId, organizationId } = details;
4763
+ console.warn(
4764
+ `[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.`
4765
+ );
4766
+ }
4767
+ function warnSessionOrganizationClaimDropped(details) {
4768
+ const { reason, sessionId, userId, organizationId } = details;
4769
+ console.warn(
4770
+ `[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.`
4771
+ );
4772
+ }
4835
4773
  async function tryFind(ql, object, where, limit = 100, organizationId) {
4836
4774
  if (!ql || typeof ql.find !== "function") return [];
4837
4775
  try {
@@ -4855,9 +4793,15 @@ async function resolveAuthzContext(input) {
4855
4793
  };
4856
4794
  let userId;
4857
4795
  let tenantId;
4796
+ let sessionId;
4858
4797
  const admission = await resolveApiKeyAdmission(ql, headers, input.nowMs, input.tenancyPosture);
4859
4798
  if (admission.outcome === "refused") {
4860
- ctx.authRefusal = { reason: admission.reason, message: admission.message };
4799
+ warnApiKeyRefusal({
4800
+ reason: admission.reason,
4801
+ keyId: admission.keyId,
4802
+ userId: admission.userId,
4803
+ organizationId: admission.organizationId
4804
+ });
4861
4805
  return ctx;
4862
4806
  }
4863
4807
  const keyPrincipal = admission.outcome === "admitted" ? admission.principal : void 0;
@@ -4873,6 +4817,8 @@ async function resolveAuthzContext(input) {
4873
4817
  const sessionData = await input.getSession(headers);
4874
4818
  userId = sessionData?.user?.id ?? sessionData?.session?.userId;
4875
4819
  tenantId = tenantId ?? sessionData?.session?.activeOrganizationId;
4820
+ const rawSessionId = sessionData?.session?.id;
4821
+ sessionId = typeof rawSessionId === "string" && rawSessionId ? rawSessionId : void 0;
4876
4822
  ctx.accessToken = sessionData?.session?.token ?? ctx.accessToken;
4877
4823
  if (sessionData?.user?.email) ctx.email = String(sessionData.user.email);
4878
4824
  } catch {
@@ -4882,7 +4828,7 @@ async function resolveAuthzContext(input) {
4882
4828
  ctx.userId = userId;
4883
4829
  if (tenantId) ctx.tenantId = tenantId;
4884
4830
  if (!ql || typeof ql.find !== "function") return ctx;
4885
- const grants = await resolveUserAuthzGrants(ql, userId, {
4831
+ let grants = await resolveUserAuthzGrants(ql, userId, {
4886
4832
  tenantId,
4887
4833
  nowMs: input.nowMs,
4888
4834
  seedPermissions: ctx.permissions,
@@ -4891,19 +4837,37 @@ async function resolveAuthzContext(input) {
4891
4837
  if (keyPrincipal?.tenantId && input.tenancyPosture) {
4892
4838
  const posture = input.tenancyPosture;
4893
4839
  if ((0, import_security2.postureEnforcesWall)(posture) && !grants.accessible_org_ids.includes(keyPrincipal.tenantId)) {
4840
+ warnApiKeyRefusal({
4841
+ reason: "organization_membership_ended",
4842
+ keyId: keyPrincipal.keyId,
4843
+ userId: keyPrincipal.userId,
4844
+ organizationId: keyPrincipal.tenantId
4845
+ });
4894
4846
  return {
4895
4847
  positions: [],
4896
4848
  permissions: [],
4897
4849
  systemPermissions: [],
4898
4850
  org_user_ids: [],
4899
- accessible_org_ids: [],
4900
- authRefusal: {
4901
- reason: "organization_membership_ended",
4902
- 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."
4903
- }
4851
+ accessible_org_ids: []
4904
4852
  };
4905
4853
  }
4906
4854
  }
4855
+ if (!keyPrincipal && tenantId && input.tenancyPosture && (0, import_security2.postureEnforcesWall)(input.tenancyPosture) && !grants.accessible_org_ids.includes(tenantId)) {
4856
+ warnSessionOrganizationClaimDropped({
4857
+ reason: "organization_membership_ended",
4858
+ sessionId,
4859
+ userId,
4860
+ organizationId: tenantId
4861
+ });
4862
+ tenantId = void 0;
4863
+ delete ctx.tenantId;
4864
+ grants = await resolveUserAuthzGrants(ql, userId, {
4865
+ tenantId,
4866
+ nowMs: input.nowMs,
4867
+ seedPermissions: ctx.permissions,
4868
+ seedEmail: ctx.email
4869
+ });
4870
+ }
4907
4871
  ctx.positions = grants.positions;
4908
4872
  ctx.permissions = grants.permissions;
4909
4873
  ctx.systemPermissions = grants.systemPermissions;
@@ -5080,17 +5044,9 @@ async function hasPlatformAdminStanding(ql, userId, opts = {}) {
5080
5044
  return false;
5081
5045
  }
5082
5046
  }
5083
- function isValidTimeZone(tz) {
5084
- try {
5085
- new Intl.DateTimeFormat("en-US", { timeZone: tz });
5086
- return true;
5087
- } catch {
5088
- return false;
5089
- }
5090
- }
5091
5047
  function coerceTimeZone(value) {
5092
5048
  const s = typeof value === "string" ? value.trim() : value != null ? String(value).trim() : "";
5093
- return s && isValidTimeZone(s) ? s : void 0;
5049
+ return s && (0, import_shared.isValueDomainMember)("iana_time_zone", s) ? s : void 0;
5094
5050
  }
5095
5051
  function coerceLocale(value) {
5096
5052
  const s = typeof value === "string" ? value.trim() : value != null ? String(value).trim() : "";
@@ -6177,6 +6133,52 @@ function errText(err) {
6177
6133
  }
6178
6134
  }
6179
6135
 
6136
+ // src/utils/advisory-aggregation.ts
6137
+ var import_node_async_hooks = require("async_hooks");
6138
+ var ADVISORY_SAMPLE_ROWS = 5;
6139
+ var storage = new import_node_async_hooks.AsyncLocalStorage();
6140
+ function keyOf(object, rule) {
6141
+ return JSON.stringify([object, rule]);
6142
+ }
6143
+ function recordAdvisoryHit(hit) {
6144
+ const collector = storage.getStore();
6145
+ if (!collector) return false;
6146
+ const key = keyOf(hit.object, hit.rule);
6147
+ const existing = collector.groups.get(key);
6148
+ if (existing) {
6149
+ existing.rows += 1;
6150
+ if (hit.recordRef != null && existing.sampleRows.length < ADVISORY_SAMPLE_ROWS) {
6151
+ existing.sampleRows.push(hit.recordRef);
6152
+ }
6153
+ return true;
6154
+ }
6155
+ collector.groups.set(key, {
6156
+ object: hit.object,
6157
+ rule: hit.rule,
6158
+ severity: hit.severity,
6159
+ message: hit.message,
6160
+ rows: 1,
6161
+ sampleRows: hit.recordRef != null ? [hit.recordRef] : []
6162
+ });
6163
+ return true;
6164
+ }
6165
+ function isAggregatingAdvisories() {
6166
+ return storage.getStore() !== void 0;
6167
+ }
6168
+ async function runWithAdvisoryAggregation(fn, report) {
6169
+ const collector = { groups: /* @__PURE__ */ new Map() };
6170
+ try {
6171
+ return await storage.run(collector, fn);
6172
+ } finally {
6173
+ if (collector.groups.size > 0) {
6174
+ try {
6175
+ report([...collector.groups.values()]);
6176
+ } catch {
6177
+ }
6178
+ }
6179
+ }
6180
+ }
6181
+
6180
6182
  // src/utils/filter-tokens.ts
6181
6183
  var import_data2 = require("@objectstack/spec/data");
6182
6184
  var UnknownFilterTokenError = class extends Error {
@@ -6692,7 +6694,7 @@ var PluginHealthMonitor = class {
6692
6694
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
6693
6695
  message,
6694
6696
  metrics: {
6695
- uptime: Date.now() - startTime
6697
+ uptimeMs: Date.now() - startTime
6696
6698
  },
6697
6699
  checks: checks.length > 0 ? checks : void 0
6698
6700
  };
@@ -7554,6 +7556,7 @@ var import_contracts = require("@objectstack/spec/contracts");
7554
7556
  0 && (module.exports = {
7555
7557
  ADMIN_STANDING_NON_TABLE_INPUTS,
7556
7558
  ADMIN_STANDING_SURFACE,
7559
+ ADVISORY_SAMPLE_ROWS,
7557
7560
  ANONYMOUS_DENY_BODY,
7558
7561
  ANONYMOUS_DENY_CODE,
7559
7562
  ANONYMOUS_DENY_MESSAGE,
@@ -7591,7 +7594,6 @@ var import_contracts = require("@objectstack/spec/contracts");
7591
7594
  PluginPermissionEnforcer,
7592
7595
  PluginPermissionManager,
7593
7596
  PluginSandboxRuntime,
7594
- PluginSecurityScanner,
7595
7597
  PluginSignatureVerifier,
7596
7598
  QA,
7597
7599
  SERVICE_NOT_REGISTERED_CODE,
@@ -7642,6 +7644,7 @@ var import_contracts = require("@objectstack/spec/contracts");
7642
7644
  hasPlatformAdminStanding,
7643
7645
  hashApiKey,
7644
7646
  hashMigrationPlan,
7647
+ isAggregatingAdvisories,
7645
7648
  isAudienceBindingSuggestionStatus,
7646
7649
  isAuthGateAllowlisted,
7647
7650
  isAuthzStoreUnavailableError,
@@ -7666,6 +7669,7 @@ var import_contracts = require("@objectstack/spec/contracts");
7666
7669
  readAuthoredTranslationLayer,
7667
7670
  readAuthzGrantsCacheTtlMs,
7668
7671
  readRunJournal,
7672
+ recordAdvisoryHit,
7669
7673
  recordNotFoundError,
7670
7674
  reportAuthzCachePosture,
7671
7675
  reportLegacyPlatformAdminGrant,
@@ -7686,6 +7690,7 @@ var import_contracts = require("@objectstack/spec/contracts");
7686
7690
  resumeMigrationJournal,
7687
7691
  rethrowAuthzStoreUnavailable,
7688
7692
  runMigrationJournal,
7693
+ runWithAdvisoryAggregation,
7689
7694
  safeExit,
7690
7695
  setPlatformAdminConfigSink,
7691
7696
  shouldDenyAnonymous,