@objectstack/core 17.2.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
@@ -30,7 +30,9 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/index.ts
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
+ ADMIN_STANDING_NON_TABLE_INPUTS: () => ADMIN_STANDING_NON_TABLE_INPUTS,
33
34
  ADMIN_STANDING_SURFACE: () => ADMIN_STANDING_SURFACE,
35
+ ADVISORY_SAMPLE_ROWS: () => ADVISORY_SAMPLE_ROWS,
34
36
  ANONYMOUS_DENY_BODY: () => ANONYMOUS_DENY_BODY,
35
37
  ANONYMOUS_DENY_CODE: () => ANONYMOUS_DENY_CODE,
36
38
  ANONYMOUS_DENY_MESSAGE: () => ANONYMOUS_DENY_MESSAGE,
@@ -38,11 +40,19 @@ __export(index_exports, {
38
40
  API_KEY_PREFIX: () => API_KEY_PREFIX,
39
41
  AUDIENCE_BINDING_SUGGESTION_STATUSES: () => AUDIENCE_BINDING_SUGGESTION_STATUSES,
40
42
  AUDIENCE_BINDING_SUGGESTION_STATUS_VALUES: () => AUDIENCE_BINDING_SUGGESTION_STATUS_VALUES,
43
+ AUTHZ_GRANTS_CACHE_TTL_ENV: () => AUTHZ_GRANTS_CACHE_TTL_ENV,
44
+ AUTHZ_INVALIDATED_CHANNEL: () => AUTHZ_INVALIDATED_CHANNEL,
45
+ AUTHZ_STORE_UNAVAILABLE_CODE: () => AUTHZ_STORE_UNAVAILABLE_CODE,
46
+ AUTHZ_STORE_UNAVAILABLE_MESSAGE: () => AUTHZ_STORE_UNAVAILABLE_MESSAGE,
47
+ AUTHZ_STORE_UNAVAILABLE_STATUS: () => AUTHZ_STORE_UNAVAILABLE_STATUS,
48
+ AuthzStoreUnavailableError: () => AuthzStoreUnavailableError,
41
49
  CORE_FALLBACK_FACTORIES: () => CORE_FALLBACK_FACTORIES,
42
50
  DependencyResolver: () => DependencyResolver,
43
51
  ENTRY_EXECUTION_CONTEXT_FIELDS: () => ENTRY_EXECUTION_CONTEXT_FIELDS,
44
52
  HotReloadManager: () => HotReloadManager,
53
+ InMemoryMetadataActivationStore: () => InMemoryMetadataActivationStore,
45
54
  LiteKernel: () => LiteKernel,
55
+ METADATA_ACTIVATION_TABLE: () => METADATA_ACTIVATION_TABLE,
46
56
  MigrationJournalRefusal: () => MigrationJournalRefusal,
47
57
  MigrationPlanRegistry: () => MigrationPlanRegistry,
48
58
  NamespaceResolver: () => NamespaceResolver,
@@ -50,18 +60,19 @@ __export(index_exports, {
50
60
  ObjectKernel: () => ObjectKernel,
51
61
  ObjectKernelBase: () => ObjectKernelBase,
52
62
  ObjectLogger: () => ObjectLogger,
63
+ ObjectStoreMetadataActivationStore: () => ObjectStoreMetadataActivationStore,
64
+ PLATFORM_ADMIN_EMAIL_SEPARATOR: () => PLATFORM_ADMIN_EMAIL_SEPARATOR,
53
65
  POSTURE_INJECTION_RULE: () => POSTURE_INJECTION_RULE,
54
66
  POSTURE_LADDER: () => POSTURE_LADDER,
55
67
  POSTURE_RANK: () => POSTURE_RANK,
56
- PluginConfigValidator: () => PluginConfigValidator,
57
68
  PluginHealthMonitor: () => PluginHealthMonitor,
58
69
  PluginLoader: () => PluginLoader,
59
70
  PluginPermissionEnforcer: () => PluginPermissionEnforcer,
60
71
  PluginPermissionManager: () => PluginPermissionManager,
61
72
  PluginSandboxRuntime: () => PluginSandboxRuntime,
62
- PluginSecurityScanner: () => PluginSecurityScanner,
63
73
  PluginSignatureVerifier: () => PluginSignatureVerifier,
64
74
  QA: () => qa_exports,
75
+ SERVICE_NOT_REGISTERED_CODE: () => SERVICE_NOT_REGISTERED_CODE,
65
76
  SIGNATURE_ALG: () => SIGNATURE_ALG,
66
77
  SecurePluginContext: () => SecurePluginContext,
67
78
  SemanticVersionManager: () => SemanticVersionManager,
@@ -71,6 +82,7 @@ __export(index_exports, {
71
82
  UnresolvedFilterTokenError: () => UnresolvedFilterTokenError,
72
83
  adminStandingColumns: () => adminStandingColumns,
73
84
  adminStandingTables: () => adminStandingTables,
85
+ artifactPackageId: () => artifactPackageId,
74
86
  assembleExecutionContext: () => assembleExecutionContext,
75
87
  assembleExecutionContextOrGuest: () => assembleExecutionContextOrGuest,
76
88
  assertInitServiceRequirements: () => assertInitServiceRequirements,
@@ -89,7 +101,6 @@ __export(index_exports, {
89
101
  createMemoryJob: () => createMemoryJob,
90
102
  createMemoryMetadata: () => createMemoryMetadata,
91
103
  createMemoryQueue: () => createMemoryQueue,
92
- createPluginConfigValidator: () => createPluginConfigValidator,
93
104
  createPluginPermissionEnforcer: () => createPluginPermissionEnforcer,
94
105
  deepMerge: () => deepMerge,
95
106
  defaultIsTransientError: () => defaultIsTransientError,
@@ -101,48 +112,70 @@ __export(index_exports, {
101
112
  extractApiKey: () => extractApiKey,
102
113
  filterTokenContextFrom: () => filterTokenContextFrom,
103
114
  findInterruptedRuns: () => findInterruptedRuns,
115
+ formatIntegrityViolation: () => formatIntegrityViolation,
104
116
  generateApiKey: () => generateApiKey,
105
117
  generateEd25519KeyPair: () => generateEd25519KeyPair,
106
118
  getEnv: () => getEnv,
107
119
  getMemoryUsage: () => getMemoryUsage,
120
+ hasPlatformAdminStanding: () => hasPlatformAdminStanding,
108
121
  hashApiKey: () => hashApiKey,
109
122
  hashMigrationPlan: () => hashMigrationPlan,
123
+ isAggregatingAdvisories: () => isAggregatingAdvisories,
110
124
  isAudienceBindingSuggestionStatus: () => isAudienceBindingSuggestionStatus,
111
125
  isAuthGateAllowlisted: () => isAuthGateAllowlisted,
126
+ isAuthzStoreUnavailableError: () => isAuthzStoreUnavailableError,
127
+ isConfiguredPlatformAdminEmail: () => isConfiguredPlatformAdminEmail,
112
128
  isExpired: () => isExpired,
113
129
  isGrantActive: () => isGrantActive,
114
130
  isGrantExpired: () => isGrantExpired,
115
131
  isNode: () => isNode,
116
132
  isRowActive: () => isRowActive,
133
+ isServiceNotRegisteredError: () => isServiceNotRegisteredError,
117
134
  isUninterpretableTemporalComparand: () => isUninterpretableTemporalComparand,
135
+ matchesConfiguredPlatformAdmin: () => matchesConfiguredPlatformAdmin,
118
136
  nextUtcCalendarDay: () => import_data.nextUtcCalendarDay,
119
137
  normalizeAuthGate: () => normalizeAuthGate,
138
+ normalizePlatformAdminEmail: () => normalizePlatformAdminEmail,
120
139
  omitInternalFieldsFromWriteResponse: () => omitInternalFieldsFromWriteResponse,
140
+ parsePlatformAdminEmails: () => parsePlatformAdminEmails,
121
141
  parseScopes: () => parseScopes,
122
142
  parseSignature: () => parseSignature,
123
143
  planChunks: () => planChunks,
124
144
  postureVisibleRows: () => postureVisibleRows,
125
145
  readAuthoredTranslationLayer: () => readAuthoredTranslationLayer,
146
+ readAuthzGrantsCacheTtlMs: () => readAuthzGrantsCacheTtlMs,
126
147
  readRunJournal: () => readRunJournal,
148
+ recordAdvisoryHit: () => recordAdvisoryHit,
127
149
  recordNotFoundError: () => recordNotFoundError,
150
+ reportAuthzCachePosture: () => reportAuthzCachePosture,
151
+ reportLegacyPlatformAdminGrant: () => reportLegacyPlatformAdminGrant,
152
+ resetLegacyPlatformAdminGrantReport: () => resetLegacyPlatformAdminGrantReport,
153
+ resetPlatformAdminEmailMemo: () => resetPlatformAdminEmailMemo,
128
154
  resolveApiKeyAdmission: () => resolveApiKeyAdmission,
129
155
  resolveApiKeyPrincipal: () => resolveApiKeyPrincipal,
156
+ resolveArtifactPackageOrder: () => resolveArtifactPackageOrder,
157
+ resolveAuthzCachePosture: () => resolveAuthzCachePosture,
130
158
  resolveAuthzContext: () => resolveAuthzContext,
131
159
  resolveFilterToken: () => resolveFilterToken,
132
160
  resolveFilterTokens: () => resolveFilterTokens,
133
161
  resolveLocale: () => resolveLocale,
134
162
  resolveLocalizationContext: () => resolveLocalizationContext,
163
+ resolvePlatformAdminEmails: () => resolvePlatformAdminEmails,
135
164
  resolvePluginOrder: () => resolvePluginOrder,
136
165
  resolveUserAuthzGrants: () => resolveUserAuthzGrants,
137
166
  resumeMigrationJournal: () => resumeMigrationJournal,
167
+ rethrowAuthzStoreUnavailable: () => rethrowAuthzStoreUnavailable,
138
168
  runMigrationJournal: () => runMigrationJournal,
169
+ runWithAdvisoryAggregation: () => runWithAdvisoryAggregation,
139
170
  safeExit: () => safeExit,
171
+ setPlatformAdminConfigSink: () => setPlatformAdminConfigSink,
140
172
  shouldDenyAnonymous: () => shouldDenyAnonymous,
141
173
  signPayload: () => signPayload,
142
174
  temporalComparandKind: () => temporalComparandKind,
143
175
  unknownAudienceBindingSuggestionStatusMessage: () => unknownAudienceBindingSuggestionStatusMessage,
144
176
  utcInstantMs: () => import_data.utcInstantMs,
145
177
  validateInitServiceContract: () => validateInitServiceContract,
178
+ verifyIntegrity: () => verifyIntegrity,
146
179
  verifyPayload: () => verifyPayload,
147
180
  verifyPlatformSignature: () => verifyPlatformSignature,
148
181
  verifyPluginArtifact: () => verifyPluginArtifact,
@@ -858,138 +891,6 @@ function createLogger(config) {
858
891
  // src/kernel.ts
859
892
  var import_system3 = require("@objectstack/spec/system");
860
893
 
861
- // src/security/plugin-config-validator.ts
862
- var import_zod = require("zod");
863
- var PluginConfigValidator = class {
864
- constructor(logger) {
865
- this.logger = logger;
866
- }
867
- /**
868
- * Validate plugin configuration against its Zod schema
869
- *
870
- * @param plugin - Plugin metadata with configSchema
871
- * @param config - User-provided configuration
872
- * @returns Validated and typed configuration
873
- * @throws Error with detailed validation errors
874
- */
875
- validatePluginConfig(plugin, config) {
876
- if (!plugin.configSchema) {
877
- this.logger.debug(`Plugin ${plugin.name} has no config schema - skipping validation`);
878
- return config;
879
- }
880
- try {
881
- const validatedConfig = plugin.configSchema.parse(config);
882
- this.logger.debug(`\u2705 Plugin config validated: ${plugin.name}`, {
883
- plugin: plugin.name,
884
- configKeys: Object.keys(config || {}).length
885
- });
886
- return validatedConfig;
887
- } catch (error) {
888
- if (error instanceof import_zod.z.ZodError) {
889
- const formattedErrors = this.formatZodErrors(error);
890
- const errorMessage = [
891
- `Plugin ${plugin.name} configuration validation failed:`,
892
- ...formattedErrors.map((e) => ` - ${e.path}: ${e.message}`)
893
- ].join("\n");
894
- this.logger.error(errorMessage, void 0, {
895
- plugin: plugin.name,
896
- errors: formattedErrors
897
- });
898
- throw new Error(errorMessage);
899
- }
900
- throw error;
901
- }
902
- }
903
- /**
904
- * Validate partial configuration (for incremental updates)
905
- *
906
- * @param plugin - Plugin metadata
907
- * @param partialConfig - Partial configuration to validate
908
- * @returns Validated partial configuration
909
- */
910
- validatePartialConfig(plugin, partialConfig) {
911
- if (!plugin.configSchema) {
912
- return partialConfig;
913
- }
914
- try {
915
- const partialSchema = plugin.configSchema.partial();
916
- const validatedConfig = partialSchema.parse(partialConfig);
917
- this.logger.debug(`\u2705 Partial config validated: ${plugin.name}`);
918
- return validatedConfig;
919
- } catch (error) {
920
- if (error instanceof import_zod.z.ZodError) {
921
- const formattedErrors = this.formatZodErrors(error);
922
- const errorMessage = [
923
- `Plugin ${plugin.name} partial configuration validation failed:`,
924
- ...formattedErrors.map((e) => ` - ${e.path}: ${e.message}`)
925
- ].join("\n");
926
- throw new Error(errorMessage);
927
- }
928
- throw error;
929
- }
930
- }
931
- /**
932
- * Get default configuration from schema
933
- *
934
- * @param plugin - Plugin metadata
935
- * @returns Default configuration object
936
- */
937
- getDefaultConfig(plugin) {
938
- if (!plugin.configSchema) {
939
- return void 0;
940
- }
941
- try {
942
- const defaults = plugin.configSchema.parse({});
943
- this.logger.debug(`Default config extracted: ${plugin.name}`);
944
- return defaults;
945
- } catch (error) {
946
- this.logger.debug(`No default config available: ${plugin.name}`);
947
- return void 0;
948
- }
949
- }
950
- /**
951
- * Check if configuration is valid without throwing
952
- *
953
- * @param plugin - Plugin metadata
954
- * @param config - Configuration to check
955
- * @returns True if valid, false otherwise
956
- */
957
- isConfigValid(plugin, config) {
958
- if (!plugin.configSchema) {
959
- return true;
960
- }
961
- const result = plugin.configSchema.safeParse(config);
962
- return result.success;
963
- }
964
- /**
965
- * Get configuration errors without throwing
966
- *
967
- * @param plugin - Plugin metadata
968
- * @param config - Configuration to check
969
- * @returns Array of validation errors, or empty array if valid
970
- */
971
- getConfigErrors(plugin, config) {
972
- if (!plugin.configSchema) {
973
- return [];
974
- }
975
- const result = plugin.configSchema.safeParse(config);
976
- if (result.success) {
977
- return [];
978
- }
979
- return this.formatZodErrors(result.error);
980
- }
981
- // Private methods
982
- formatZodErrors(error) {
983
- return error.issues.map((e) => ({
984
- path: e.path.join(".") || "root",
985
- message: e.message
986
- }));
987
- }
988
- };
989
- function createPluginConfigValidator(logger) {
990
- return new PluginConfigValidator(logger);
991
- }
992
-
993
894
  // src/security/plugin-artifact-signature.ts
994
895
  var import_node_crypto = require("crypto");
995
896
  var SIGNATURE_ALG = "ed25519";
@@ -1086,6 +987,43 @@ async function verifyPluginArtifact(input, keys) {
1086
987
  return { ok: true, publisherVerified: publisher.verified, platformVerified };
1087
988
  }
1088
989
 
990
+ // src/service-not-registered.ts
991
+ var SERVICE_NOT_REGISTERED_CODE = "SERVICE_NOT_REGISTERED";
992
+ var SERVICE_NOT_REGISTERED_BRAND = "__objectstackServiceNotRegistered";
993
+ function serviceNotRegisteredError(name) {
994
+ const err = new Error(`Service '${name}' not found`);
995
+ err[SERVICE_NOT_REGISTERED_BRAND] = true;
996
+ err.code = SERVICE_NOT_REGISTERED_CODE;
997
+ err.serviceName = name;
998
+ return err;
999
+ }
1000
+ function isServiceNotRegisteredError(err) {
1001
+ return typeof err === "object" && err !== null && err[SERVICE_NOT_REGISTERED_BRAND] === true;
1002
+ }
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
+
1089
1027
  // src/plugin-loader.ts
1090
1028
  var ServiceLifecycle = /* @__PURE__ */ ((ServiceLifecycle2) => {
1091
1029
  ServiceLifecycle2["SINGLETON"] = "singleton";
@@ -1101,7 +1039,6 @@ var PluginLoader = class {
1101
1039
  this.scopedServices = /* @__PURE__ */ new Map();
1102
1040
  this.creating = /* @__PURE__ */ new Set();
1103
1041
  this.logger = logger;
1104
- this.configValidator = new PluginConfigValidator(logger);
1105
1042
  }
1106
1043
  /**
1107
1044
  * Set the plugin context for service factories
@@ -1124,13 +1061,11 @@ var PluginLoader = class {
1124
1061
  this.logger.info(`Loading plugin: ${plugin.name}`);
1125
1062
  const metadata = this.toPluginMetadata(plugin);
1126
1063
  this.validatePluginStructure(metadata);
1064
+ this.validatePluginContract(metadata);
1127
1065
  const versionCheck = this.checkVersionCompatibility(metadata);
1128
1066
  if (!versionCheck.compatible) {
1129
1067
  throw new Error(`Version incompatible: ${versionCheck.message}`);
1130
1068
  }
1131
- if (metadata.configSchema) {
1132
- this.validatePluginConfig(metadata);
1133
- }
1134
1069
  if (metadata.signature) {
1135
1070
  await this.verifyPluginSignature(metadata);
1136
1071
  }
@@ -1169,7 +1104,7 @@ var PluginLoader = class {
1169
1104
  if (!registration) {
1170
1105
  const instance = this.serviceInstances.get(name);
1171
1106
  if (!instance) {
1172
- throw new Error(`Service '${name}' not found`);
1107
+ throw serviceNotRegisteredError(name);
1173
1108
  }
1174
1109
  return instance;
1175
1110
  }
@@ -1311,6 +1246,27 @@ var PluginLoader = class {
1311
1246
  throw new Error(`Invalid semantic version: ${plugin.version}`);
1312
1247
  }
1313
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
+ }
1314
1270
  checkVersionCompatibility(plugin) {
1315
1271
  const version = plugin.version;
1316
1272
  if (!this.isValidSemanticVersion(version)) {
@@ -1329,16 +1285,6 @@ var PluginLoader = class {
1329
1285
  const semverRegex = /^\d+\.\d+\.\d+(-[a-zA-Z0-9.-]+)?(\+[a-zA-Z0-9.-]+)?$/;
1330
1286
  return semverRegex.test(version);
1331
1287
  }
1332
- validatePluginConfig(plugin, config) {
1333
- if (!plugin.configSchema) {
1334
- return;
1335
- }
1336
- if (config === void 0) {
1337
- this.logger.debug(`Plugin ${plugin.name} has configuration schema (config validation postponed)`);
1338
- return;
1339
- }
1340
- this.configValidator.validatePluginConfig(plugin, config);
1341
- }
1342
1288
  async verifyPluginSignature(plugin) {
1343
1289
  if (!plugin.signature) {
1344
1290
  return;
@@ -1530,6 +1476,7 @@ function createMemoryI18n() {
1530
1476
  const authored = /* @__PURE__ */ new Map();
1531
1477
  let defaultLocale = "en";
1532
1478
  let supportedLocales;
1479
+ let fallbackLocale;
1533
1480
  function resolveKey(data, key) {
1534
1481
  const parts = key.split(".");
1535
1482
  let current = data;
@@ -1566,7 +1513,11 @@ function createMemoryI18n() {
1566
1513
  _serviceName: "i18n",
1567
1514
  t(key, locale, params) {
1568
1515
  const data = resolveTranslations(locale) ?? mergedLocale(defaultLocale);
1569
- 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
+ }
1570
1521
  if (value == null) return key;
1571
1522
  if (!params) return value;
1572
1523
  return value.replace(/\{\{(\w+)\}\}/g, (_, name) => String(params[name] ?? `{{${name}}}`));
@@ -1623,15 +1574,30 @@ function createMemoryI18n() {
1623
1574
  },
1624
1575
  setDefaultLocale(locale) {
1625
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;
1626
1592
  }
1627
1593
  };
1628
1594
  }
1629
1595
 
1630
1596
  // src/metadata-service-contract.ts
1631
- var import_shared = require("@objectstack/spec/shared");
1597
+ var import_meta_spelling = require("@objectstack/spec/meta-spelling");
1632
1598
  var REGISTER_REFUSAL_CODE = "VALIDATION_ERROR";
1633
1599
  function canonicalMetadataServiceType(type) {
1634
- return (0, import_shared.pluralToSingular)(type);
1600
+ return (0, import_meta_spelling.pluralToSingular)(type);
1635
1601
  }
1636
1602
  function registerRefusal(message) {
1637
1603
  const err = new Error(message);
@@ -1948,6 +1914,7 @@ async function raceWithTimeout(operation, timeoutMs, createTimeoutError) {
1948
1914
  }
1949
1915
 
1950
1916
  // src/kernel.ts
1917
+ var DEGRADED_CAPABILITIES_SERVICE = "kernel.degraded-capabilities";
1951
1918
  var ObjectKernel = class {
1952
1919
  constructor(config = {}) {
1953
1920
  this.plugins = /* @__PURE__ */ new Map();
@@ -1955,7 +1922,12 @@ var ObjectKernel = class {
1955
1922
  this.hooks = /* @__PURE__ */ new Map();
1956
1923
  this.state = "idle";
1957
1924
  this.startedPlugins = /* @__PURE__ */ new Set();
1958
- 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();
1959
1931
  this.shutdownHandlers = [];
1960
1932
  this.config = {
1961
1933
  defaultStartupTimeout: 3e4,
@@ -2142,9 +2114,40 @@ var ObjectKernel = class {
2142
2114
  }
2143
2115
  if (missingCoreServices.length > 0) {
2144
2116
  this.logger.warn(`System started with degraded capabilities. Missing core services: ${missingCoreServices.join(", ")}`);
2117
+ this.publishDegradedCapabilities(missingCoreServices);
2145
2118
  }
2146
2119
  this.logger.info("System requirement check passed");
2147
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
+ }
2148
2151
  /**
2149
2152
  * Bootstrap the kernel with enhanced features
2150
2153
  */
@@ -2257,11 +2260,23 @@ var ObjectKernel = class {
2257
2260
  }
2258
2261
  return results;
2259
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
+ }
2260
2271
  /**
2261
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.
2262
2277
  */
2263
2278
  getPluginMetrics() {
2264
- return new Map(this.pluginStartTimes);
2279
+ return this.getPluginStartupDurations();
2265
2280
  }
2266
2281
  /**
2267
2282
  * Whether a plugin with the given name has been registered on this kernel.
@@ -2383,11 +2398,14 @@ var ObjectKernel = class {
2383
2398
  );
2384
2399
  const duration = Date.now() - startTime;
2385
2400
  this.startedPlugins.add(plugin.name);
2386
- this.pluginStartTimes.set(plugin.name, duration);
2401
+ this.pluginStartupDurations.set(plugin.name, duration);
2387
2402
  this.logger.debug(`Plugin started: ${plugin.name} (${duration}ms)`);
2388
2403
  return {
2389
2404
  success: true,
2390
2405
  pluginName: plugin.name,
2406
+ durationMs: duration,
2407
+ // Deprecated alias carrying the same elapsed value; see
2408
+ // PluginStartupResult.startTime.
2391
2409
  startTime: duration
2392
2410
  };
2393
2411
  } catch (error) {
@@ -2397,6 +2415,9 @@ var ObjectKernel = class {
2397
2415
  success: false,
2398
2416
  pluginName: plugin.name,
2399
2417
  error,
2418
+ durationMs: duration,
2419
+ // Deprecated alias carrying the same elapsed value; see
2420
+ // PluginStartupResult.startTime.
2400
2421
  startTime: duration,
2401
2422
  timedOut: isTimeout
2402
2423
  };
@@ -2512,6 +2533,67 @@ var ObjectKernel = class {
2512
2533
  }
2513
2534
  };
2514
2535
 
2536
+ // src/artifact-packages.ts
2537
+ var import_spec = require("@objectstack/spec");
2538
+ var MAX_REPORTED_ENTRY_ISSUES = 5;
2539
+ function refuse(code, message) {
2540
+ const err = new Error(message);
2541
+ err.code = code;
2542
+ err.status = 422;
2543
+ return err;
2544
+ }
2545
+ function artifactPackageId(manifest) {
2546
+ const id = manifest?.id || manifest?.name;
2547
+ return typeof id === "string" && id !== "" ? id : void 0;
2548
+ }
2549
+ function resolveArtifactPackageOrder(artifact) {
2550
+ const declared = artifact?.packages;
2551
+ if (declared === void 0 || declared === null) return [artifact];
2552
+ if (!Array.isArray(declared)) {
2553
+ throw refuse(
2554
+ "INVALID_ARTIFACT_PACKAGES",
2555
+ `A release artifact's \`packages\` must be an array of package entries (ADR-0130 D4, \`ArtifactPackageEntrySchema\`), but this artifact carries \`packages\` of type ${typeof declared}. Omit the key entirely for a single-package artifact \u2014 \`manifest\` is retained, not replaced.`
2556
+ );
2557
+ }
2558
+ const nodes = /* @__PURE__ */ new Map();
2559
+ declared.forEach((entry, index) => {
2560
+ const verdict = import_spec.ArtifactPackageSchema.safeParse(entry);
2561
+ if (!verdict.success) {
2562
+ const issues = verdict.error.issues;
2563
+ const shown = issues.slice(0, MAX_REPORTED_ENTRY_ISSUES);
2564
+ throw refuse(
2565
+ "INVALID_ARTIFACT_PACKAGE_ENTRY",
2566
+ `Release artifact \`packages[${index}]\` is not a package entry (ADR-0130 D4): ` + shown.map((i) => `${i.path.join(".") || "<entry>"}: ${i.message}`).join("; ") + (issues.length > shown.length ? ` (+${issues.length - shown.length} more)` : "") + ". Each entry is a WRAPPER object carrying its package under `manifest:` \u2014 wrap an inlined body as `{ manifest: { \u2026 } }`. The key position is reserved so a future external-segment form is an additive key rather than a reshape. The body under `manifest:` is the ASSEMBLED package body (`AssembledPackageBodySchema`): its `objects` / `datasources` are DEFINITIONS, not the authoring manifest's glob patterns \u2014 a compiled artifact has no files left to glob."
2567
+ );
2568
+ }
2569
+ const manifest = entry.manifest;
2570
+ const id = artifactPackageId(manifest);
2571
+ if (id === void 0) {
2572
+ throw refuse(
2573
+ "INVALID_ARTIFACT_PACKAGE_ENTRY",
2574
+ `Release artifact \`packages[${index}]\` carries a manifest with no usable package id: \`registerApp\` keys the installed package on \`id || name\`, so an entry without either cannot be ordered against its siblings or addressed after install.`
2575
+ );
2576
+ }
2577
+ if (nodes.has(id)) {
2578
+ throw refuse(
2579
+ "DUPLICATE_ARTIFACT_PACKAGE",
2580
+ `Release artifact declares package "${id}" more than once (\`packages[${index}]\` repeats an earlier entry). One artifact carries each package once \u2014 an artifact is one atomic delivery (ADR-0130 D1/D6), not a list with last-writer-wins.`
2581
+ );
2582
+ }
2583
+ nodes.set(id, {
2584
+ // `name` is what `resolvePluginOrder` puts in its diagnostics; the MAP KEY
2585
+ // is what its edges resolve against. Both are the package id, so an error
2586
+ // it raises names the same string the artifact author wrote.
2587
+ name: id,
2588
+ optionalDependencies: Object.keys(
2589
+ manifest.dependencies ?? {}
2590
+ ),
2591
+ manifest
2592
+ });
2593
+ });
2594
+ return resolvePluginOrder(nodes).map((node) => node.manifest);
2595
+ }
2596
+
2515
2597
  // src/lite-kernel.ts
2516
2598
  var LiteKernel = class extends ObjectKernelBase {
2517
2599
  constructor(config) {
@@ -2523,6 +2605,26 @@ var LiteKernel = class extends ObjectKernelBase {
2523
2605
  * Register a plugin
2524
2606
  * @param plugin - Plugin instance
2525
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
+ *
2526
2628
  * Duplicate names OVERWRITE, with one `warn` naming both versions — the
2527
2629
  * declared contract in `plugin-registration.ts`, applied identically by
2528
2630
  * `ObjectKernel.use()` (#9864, maintainer ruling 2026-08-19).
@@ -2536,6 +2638,7 @@ var LiteKernel = class extends ObjectKernelBase {
2536
2638
  */
2537
2639
  use(plugin) {
2538
2640
  this.validateIdle();
2641
+ assertPluginContract(plugin);
2539
2642
  registerPluginByName(this.plugins, plugin, this.logger);
2540
2643
  return this;
2541
2644
  }
@@ -3241,6 +3344,55 @@ var PluginSignatureVerifier = class {
3241
3344
  }
3242
3345
  };
3243
3346
 
3347
+ // src/security/plugin-artifact-integrity.ts
3348
+ var import_node_crypto2 = require("crypto");
3349
+ var SRI_ALGORITHMS = /* @__PURE__ */ new Set(["sha256", "sha384", "sha512"]);
3350
+ function sriDigestFor(declared, data) {
3351
+ const dash = declared.indexOf("-");
3352
+ const alg = dash > 0 && SRI_ALGORITHMS.has(declared.slice(0, dash)) ? declared.slice(0, dash) : "sha256";
3353
+ return `${alg}-${(0, import_node_crypto2.createHash)(alg).update(data).digest("base64")}`;
3354
+ }
3355
+ function verifyIntegrity(files, integrity, options = {}) {
3356
+ if (integrity === null || integrity === void 0) {
3357
+ return { ok: true, skipped: true, checked: 0, violations: [] };
3358
+ }
3359
+ const exempt = new Set(options.exempt ?? []);
3360
+ const byPath = /* @__PURE__ */ new Map();
3361
+ for (const f of files) {
3362
+ if (!exempt.has(f.path)) byPath.set(f.path, f.data);
3363
+ }
3364
+ const violations = [];
3365
+ let checked = 0;
3366
+ for (const [path, declaredRaw] of Object.entries(integrity)) {
3367
+ if (exempt.has(path)) continue;
3368
+ const declared = typeof declaredRaw === "string" ? declaredRaw : String(declaredRaw);
3369
+ const data = byPath.get(path);
3370
+ if (data === void 0) {
3371
+ violations.push({ kind: "missing_file", path, declared });
3372
+ continue;
3373
+ }
3374
+ checked++;
3375
+ const actual = sriDigestFor(declared, data);
3376
+ if (actual !== declared) violations.push({ kind: "digest_mismatch", path, declared, actual });
3377
+ }
3378
+ for (const path of [...byPath.keys()].sort()) {
3379
+ if (!Object.prototype.hasOwnProperty.call(integrity, path)) {
3380
+ violations.push({ kind: "extra_file", path });
3381
+ }
3382
+ }
3383
+ return { ok: violations.length === 0, skipped: false, checked, violations };
3384
+ }
3385
+ function formatIntegrityViolation(v) {
3386
+ switch (v.kind) {
3387
+ case "digest_mismatch":
3388
+ return `${v.path}: digest mismatch \u2014 manifest declares ${v.declared}, artifact bytes hash to ${v.actual}`;
3389
+ case "missing_file":
3390
+ return `${v.path}: declared in the integrity map but absent from the artifact`;
3391
+ case "extra_file":
3392
+ return `${v.path}: present in the artifact but not in the integrity map`;
3393
+ }
3394
+ }
3395
+
3244
3396
  // src/security/plugin-permission-enforcer.ts
3245
3397
  var PluginPermissionEnforcer = class {
3246
3398
  constructor(logger) {
@@ -4112,239 +4264,17 @@ var _PluginSandboxRuntime = class _PluginSandboxRuntime {
4112
4264
  _PluginSandboxRuntime.MONITORING_INTERVAL_MS = 5e3;
4113
4265
  var PluginSandboxRuntime = _PluginSandboxRuntime;
4114
4266
 
4115
- // src/security/security-scanner.ts
4116
- var PluginSecurityScanner = class {
4117
- constructor(logger, config) {
4118
- // Known vulnerabilities database (CVE cache)
4119
- this.vulnerabilityDb = /* @__PURE__ */ new Map();
4120
- // Scan results cache
4121
- this.scanResults = /* @__PURE__ */ new Map();
4122
- this.passThreshold = 70;
4123
- this.logger = logger.child({ component: "SecurityScanner" });
4124
- if (config?.passThreshold !== void 0) {
4125
- this.passThreshold = config.passThreshold;
4126
- }
4127
- }
4128
- /**
4129
- * Perform a comprehensive security scan on a plugin
4130
- */
4131
- async scan(target) {
4132
- this.logger.info("Starting security scan", {
4133
- pluginId: target.pluginId,
4134
- version: target.version
4135
- });
4136
- const issues = [];
4137
- try {
4138
- const codeIssues = await this.scanCode(target);
4139
- issues.push(...codeIssues);
4140
- const depIssues = await this.scanDependencies(target);
4141
- issues.push(...depIssues);
4142
- const malwareIssues = await this.scanMalware(target);
4143
- issues.push(...malwareIssues);
4144
- const licenseIssues = await this.scanLicenses(target);
4145
- issues.push(...licenseIssues);
4146
- const configIssues = await this.scanConfiguration(target);
4147
- issues.push(...configIssues);
4148
- const score = this.calculateSecurityScore(issues);
4149
- const result = {
4150
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
4151
- scanner: { name: "ObjectStack Security Scanner", version: "1.0.0" },
4152
- status: score >= this.passThreshold ? "passed" : "failed",
4153
- vulnerabilities: issues.map((issue) => ({
4154
- id: issue.id,
4155
- severity: issue.severity,
4156
- category: issue.category,
4157
- title: issue.title,
4158
- description: issue.description,
4159
- location: issue.location ? `${issue.location.file}:${issue.location.line}` : void 0,
4160
- remediation: issue.remediation,
4161
- affectedVersions: [],
4162
- exploitAvailable: false,
4163
- patchAvailable: false
4164
- })),
4165
- summary: {
4166
- totalVulnerabilities: issues.length,
4167
- criticalCount: issues.filter((i) => i.severity === "critical").length,
4168
- highCount: issues.filter((i) => i.severity === "high").length,
4169
- mediumCount: issues.filter((i) => i.severity === "medium").length,
4170
- lowCount: issues.filter((i) => i.severity === "low").length,
4171
- infoCount: issues.filter((i) => i.severity === "info").length
4172
- }
4173
- };
4174
- this.scanResults.set(`${target.pluginId}:${target.version}`, result);
4175
- this.logger.info("Security scan complete", {
4176
- pluginId: target.pluginId,
4177
- score,
4178
- status: result.status,
4179
- summary: result.summary
4180
- });
4181
- return result;
4182
- } catch (error) {
4183
- this.logger.error("Security scan failed", {
4184
- pluginId: target.pluginId,
4185
- error
4186
- });
4187
- throw error;
4188
- }
4189
- }
4190
- /**
4191
- * Scan code for vulnerabilities
4192
- */
4193
- async scanCode(target) {
4194
- const issues = [];
4195
- this.logger.debug("Code scan complete", {
4196
- pluginId: target.pluginId,
4197
- issuesFound: issues.length
4198
- });
4199
- return issues;
4200
- }
4201
- /**
4202
- * Scan dependencies for known vulnerabilities
4203
- */
4204
- async scanDependencies(target) {
4205
- const issues = [];
4206
- if (!target.dependencies) {
4207
- return issues;
4208
- }
4209
- for (const [depName, version] of Object.entries(target.dependencies)) {
4210
- const vulnKey = `${depName}@${version}`;
4211
- const vulnerability = this.vulnerabilityDb.get(vulnKey);
4212
- if (vulnerability) {
4213
- issues.push({
4214
- id: `vuln-${vulnerability.cve || depName}`,
4215
- severity: vulnerability.severity,
4216
- category: "vulnerability",
4217
- title: `Vulnerable dependency: ${depName}`,
4218
- description: `${depName}@${version} has known security vulnerabilities`,
4219
- remediation: vulnerability.fixedIn ? `Upgrade to ${vulnerability.fixedIn.join(" or ")}` : "No fix available",
4220
- cve: vulnerability.cve
4221
- });
4222
- }
4223
- }
4224
- this.logger.debug("Dependency scan complete", {
4225
- pluginId: target.pluginId,
4226
- dependencies: Object.keys(target.dependencies).length,
4227
- vulnerabilities: issues.length
4228
- });
4229
- return issues;
4230
- }
4231
- /**
4232
- * Scan for malware patterns
4233
- */
4234
- async scanMalware(target) {
4235
- const issues = [];
4236
- this.logger.debug("Malware scan complete", {
4237
- pluginId: target.pluginId,
4238
- issuesFound: issues.length
4239
- });
4240
- return issues;
4241
- }
4242
- /**
4243
- * Check license compliance
4244
- */
4245
- async scanLicenses(target) {
4246
- const issues = [];
4247
- if (!target.dependencies) {
4248
- return issues;
4249
- }
4250
- this.logger.debug("License scan complete", {
4251
- pluginId: target.pluginId,
4252
- issuesFound: issues.length
4253
- });
4254
- return issues;
4255
- }
4256
- /**
4257
- * Check configuration security
4258
- */
4259
- async scanConfiguration(target) {
4260
- const issues = [];
4261
- this.logger.debug("Configuration scan complete", {
4262
- pluginId: target.pluginId,
4263
- issuesFound: issues.length
4264
- });
4265
- return issues;
4266
- }
4267
- /**
4268
- * Calculate security score based on issues
4269
- */
4270
- calculateSecurityScore(issues) {
4271
- let score = 100;
4272
- for (const issue of issues) {
4273
- switch (issue.severity) {
4274
- case "critical":
4275
- score -= 20;
4276
- break;
4277
- case "high":
4278
- score -= 10;
4279
- break;
4280
- case "medium":
4281
- score -= 5;
4282
- break;
4283
- case "low":
4284
- score -= 2;
4285
- break;
4286
- case "info":
4287
- score -= 0;
4288
- break;
4289
- }
4290
- }
4291
- return Math.max(0, score);
4292
- }
4293
- /**
4294
- * Add a vulnerability to the database
4295
- */
4296
- addVulnerability(packageName, version, vulnerability) {
4297
- const key = `${packageName}@${version}`;
4298
- this.vulnerabilityDb.set(key, vulnerability);
4299
- this.logger.debug("Vulnerability added to database", {
4300
- package: packageName,
4301
- version,
4302
- cve: vulnerability.cve
4303
- });
4304
- }
4305
- /**
4306
- * Get scan result from cache
4307
- */
4308
- getScanResult(pluginId, version) {
4309
- return this.scanResults.get(`${pluginId}:${version}`);
4310
- }
4311
- /**
4312
- * Clear scan results cache
4313
- */
4314
- clearCache() {
4315
- this.scanResults.clear();
4316
- this.logger.debug("Scan results cache cleared");
4317
- }
4318
- /**
4319
- * Update vulnerability database from external source
4320
- */
4321
- async updateVulnerabilityDatabase() {
4322
- this.logger.info("Updating vulnerability database");
4323
- this.logger.info("Vulnerability database updated", {
4324
- entries: this.vulnerabilityDb.size
4325
- });
4326
- }
4327
- /**
4328
- * Shutdown security scanner
4329
- */
4330
- shutdown() {
4331
- this.vulnerabilityDb.clear();
4332
- this.scanResults.clear();
4333
- this.logger.info("Security scanner shutdown complete");
4334
- }
4335
- };
4336
-
4337
4267
  // src/security/api-key.ts
4338
- var import_node_crypto2 = require("crypto");
4268
+ var import_node_crypto3 = require("crypto");
4339
4269
  var import_security = require("@objectstack/spec/security");
4340
4270
  var API_KEY_PREFIX = "osk_";
4341
4271
  var API_KEY_ENTROPY_BYTES = 32;
4342
4272
  var VISIBLE_PREFIX_LEN = 12;
4343
4273
  function hashApiKey(raw) {
4344
- return (0, import_node_crypto2.createHash)("sha256").update(raw, "utf8").digest("hex");
4274
+ return (0, import_node_crypto3.createHash)("sha256").update(raw, "utf8").digest("hex");
4345
4275
  }
4346
4276
  function generateApiKey(prefix = API_KEY_PREFIX) {
4347
- const secret = (0, import_node_crypto2.randomBytes)(API_KEY_ENTROPY_BYTES).toString("base64url");
4277
+ const secret = (0, import_node_crypto3.randomBytes)(API_KEY_ENTROPY_BYTES).toString("base64url");
4348
4278
  const raw = `${prefix}${secret}`;
4349
4279
  return {
4350
4280
  raw,
@@ -4420,19 +4350,22 @@ async function resolveApiKeyAdmission(ql, headers, nowMs = Date.now(), tenancyPo
4420
4350
  const userId = row.user_id ?? row.userId;
4421
4351
  if (!userId || typeof userId !== "string") return { outcome: "none" };
4422
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;
4423
4354
  if (!tenantId && tenancyPosture) {
4424
4355
  const posture = tenancyPosture;
4425
4356
  if ((0, import_security.postureEnforcesWall)(posture) && !(0, import_security.postureUsesUnionScope)(posture)) {
4426
4357
  return {
4427
4358
  outcome: "refused",
4428
4359
  reason: "organization_required",
4360
+ keyId,
4361
+ userId,
4429
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."
4430
4363
  };
4431
4364
  }
4432
4365
  }
4433
4366
  return {
4434
4367
  outcome: "admitted",
4435
- principal: { userId, tenantId, scopes: parseScopes(row.scopes) }
4368
+ principal: { userId, keyId, tenantId, scopes: parseScopes(row.scopes) }
4436
4369
  };
4437
4370
  }
4438
4371
  function readHeader(headers, name) {
@@ -4458,9 +4391,39 @@ function safeJsonParse(s, fallback) {
4458
4391
  }
4459
4392
  }
4460
4393
 
4394
+ // src/security/authz-store-unavailable.ts
4395
+ var AUTHZ_STORE_UNAVAILABLE_STATUS = 503;
4396
+ var AUTHZ_STORE_UNAVAILABLE_CODE = "SERVICE_UNAVAILABLE";
4397
+ var AUTHZ_STORE_UNAVAILABLE_MESSAGE = "The authorization store could not be read, so this request's permissions were never determined. This is a server-side outage, not a permission denial.";
4398
+ var AUTHZ_STORE_UNAVAILABLE_BRAND = "__objectstackAuthzStoreUnavailable";
4399
+ var _a, _b;
4400
+ var AuthzStoreUnavailableError = class extends (_b = Error, _a = AUTHZ_STORE_UNAVAILABLE_BRAND, _b) {
4401
+ constructor(object, cause) {
4402
+ super(`${AUTHZ_STORE_UNAVAILABLE_MESSAGE} (failed read: \`${object}\`)`);
4403
+ /** Brand — see the module doc on why this is not `instanceof`. */
4404
+ this[_a] = true;
4405
+ /** ADR-0112 wire code. */
4406
+ this.code = AUTHZ_STORE_UNAVAILABLE_CODE;
4407
+ /** HTTP status a transport should answer. */
4408
+ this.status = AUTHZ_STORE_UNAVAILABLE_STATUS;
4409
+ this.name = "AuthzStoreUnavailableError";
4410
+ this.object = object;
4411
+ if (cause !== void 0) this.cause = cause;
4412
+ }
4413
+ };
4414
+ function isAuthzStoreUnavailableError(err) {
4415
+ return typeof err === "object" && err !== null && err[AUTHZ_STORE_UNAVAILABLE_BRAND] === true;
4416
+ }
4417
+ function rethrowAuthzStoreUnavailable(err) {
4418
+ if (isAuthzStoreUnavailableError(err)) throw err;
4419
+ return void 0;
4420
+ }
4421
+
4461
4422
  // src/security/resolve-authz-context.ts
4462
- var import_spec = require("@objectstack/spec");
4423
+ var import_types2 = require("@objectstack/types");
4424
+ var import_spec2 = require("@objectstack/spec");
4463
4425
  var import_security2 = require("@objectstack/spec/security");
4426
+ var import_shared = require("@objectstack/spec/shared");
4464
4427
 
4465
4428
  // src/security/grant-validity.ts
4466
4429
  function toEpochMs(value) {
@@ -4480,6 +4443,20 @@ function isGrantActive(row, nowMs) {
4480
4443
  if (until !== void 0 && !(nowMs < until)) return false;
4481
4444
  return true;
4482
4445
  }
4446
+ function nextGrantValidityBoundary(rows, nowMs) {
4447
+ let next;
4448
+ for (const row of rows) {
4449
+ if (!row) continue;
4450
+ const from = toEpochMs(row.valid_from ?? row.validFrom);
4451
+ const until = toEpochMs(row.valid_until ?? row.validUntil);
4452
+ for (const bound of [from, until]) {
4453
+ if (bound !== void 0 && bound > nowMs && (next === void 0 || bound < next)) {
4454
+ next = bound;
4455
+ }
4456
+ }
4457
+ }
4458
+ return next;
4459
+ }
4483
4460
  function isGrantExpired(row, nowMs) {
4484
4461
  if (!row) return false;
4485
4462
  const until = toEpochMs(row.valid_until ?? row.validUntil);
@@ -4487,6 +4464,227 @@ function isGrantExpired(row, nowMs) {
4487
4464
  return !(nowMs < until);
4488
4465
  }
4489
4466
 
4467
+ // src/security/authz-cache-posture.ts
4468
+ var AUTHZ_GRANTS_CACHE_TTL_ENV = "OS_AUTHZ_GRANTS_CACHE_TTL_MS";
4469
+ function resolveAuthzCachePosture(input) {
4470
+ const { ttlMs, bus, driver } = input;
4471
+ if (!(ttlMs > 0)) {
4472
+ return { posture: "disabled", loud: false, message: "" };
4473
+ }
4474
+ if (bus === "bridged") {
4475
+ return {
4476
+ posture: "bus-narrowed",
4477
+ loud: false,
4478
+ message: `[authz-cache] grants cache ENABLED (ttl=${ttlMs}ms) with the "authz.invalidated" bridge attached` + (driver ? ` (cluster driver "${driver}")` : "") + ". The bus narrows the TYPICAL convergence to one network hop; the TTL remains the correctness bound, because no shipped driver delivers better than at-most-once (cluster.mdx \xA74.2)."
4479
+ };
4480
+ }
4481
+ const why = bus === "in-process" ? `the cluster driver "${driver ?? "memory"}" is in-process and does not fan out across replicas` : "no cluster service is registered on this node";
4482
+ return {
4483
+ posture: "ttl-only",
4484
+ loud: true,
4485
+ message: `[authz-cache] grants cache ENABLED (ttl=${ttlMs}ms) with NO "authz.invalidated" invalidation bus \u2014 ${why}. A grant revoked on another replica is honoured by this one for up to ${ttlMs}ms. That is a supported configuration, not an error: the TTL is the correctness bound and it still holds. It is stated because a silently-absent invalidation bridge is how a security control gets disabled without anyone noticing (#4785). To narrow the typical window, configure a remote cluster driver; to remove it entirely, set ${AUTHZ_GRANTS_CACHE_TTL_ENV}=0.`
4486
+ };
4487
+ }
4488
+ function readAuthzGrantsCacheTtlMs(env = typeof process !== "undefined" ? process.env : {}) {
4489
+ const raw = env[AUTHZ_GRANTS_CACHE_TTL_ENV];
4490
+ if (raw === void 0 || raw.trim() === "") {
4491
+ return { ttlMs: 0, malformed: false };
4492
+ }
4493
+ const parsed = Number(raw.trim());
4494
+ if (!Number.isFinite(parsed) || parsed < 0) {
4495
+ return { ttlMs: 0, raw, malformed: true };
4496
+ }
4497
+ return { ttlMs: Math.floor(parsed), raw, malformed: false };
4498
+ }
4499
+ function reportAuthzCachePosture(input, sink2) {
4500
+ if (input.malformedTtl) {
4501
+ sink2.warn(
4502
+ `[authz-cache] ${AUTHZ_GRANTS_CACHE_TTL_ENV}=${JSON.stringify(input.malformedTtl.raw ?? "")} is not a non-negative number; the grants cache is treated as DISABLED. Set a millisecond count, or 0 to disable it deliberately.`
4503
+ );
4504
+ }
4505
+ const statement = resolveAuthzCachePosture(input);
4506
+ if (statement.posture === "disabled") return statement;
4507
+ if (statement.loud) sink2.warn(statement.message);
4508
+ else sink2.info?.(statement.message);
4509
+ return statement;
4510
+ }
4511
+
4512
+ // src/security/resolve-user-grants-cache.ts
4513
+ var GRANTS_CACHE_WATCHED_OBJECTS = /* @__PURE__ */ new Set([
4514
+ "sys_member",
4515
+ "sys_user_position",
4516
+ "sys_user_permission_set",
4517
+ "sys_position",
4518
+ "sys_position_permission_set",
4519
+ "sys_permission_set",
4520
+ "sys_user"
4521
+ ]);
4522
+ var grantsCacheStates = /* @__PURE__ */ new WeakMap();
4523
+ var WRITE_OPERATIONS = /* @__PURE__ */ new Set(["insert", "update", "delete"]);
4524
+ function grantsCacheState(ql) {
4525
+ const existing = grantsCacheStates.get(ql);
4526
+ if (existing !== void 0) return existing ?? void 0;
4527
+ const seamQl = ql;
4528
+ const epoch = seamQl.writeEpoch;
4529
+ const hasEpoch = !!epoch && typeof epoch === "object" && typeof epoch.current === "number" && typeof epoch.bump === "function" && typeof epoch.subscribe === "function";
4530
+ if (!hasEpoch || typeof seamQl.registerMiddleware !== "function") {
4531
+ return void 0;
4532
+ }
4533
+ const state = { gen: 0, entries: /* @__PURE__ */ new Map() };
4534
+ try {
4535
+ seamQl.registerMiddleware(async (ctx, next) => {
4536
+ if (typeof ctx?.operation !== "string" || !WRITE_OPERATIONS.has(ctx.operation) || typeof ctx?.object !== "string" || !GRANTS_CACHE_WATCHED_OBJECTS.has(ctx.object)) {
4537
+ return next();
4538
+ }
4539
+ try {
4540
+ await next();
4541
+ } finally {
4542
+ state.gen += 1;
4543
+ }
4544
+ });
4545
+ epoch.subscribe((_epoch, reason) => {
4546
+ if (reason !== "write") state.gen += 1;
4547
+ });
4548
+ } catch {
4549
+ grantsCacheStates.set(ql, null);
4550
+ return void 0;
4551
+ }
4552
+ grantsCacheStates.set(ql, state);
4553
+ return state;
4554
+ }
4555
+ function grantsCacheKey(userId, opts) {
4556
+ return JSON.stringify([
4557
+ userId,
4558
+ opts.tenantId ?? null,
4559
+ opts.seedEmail ?? null,
4560
+ Array.isArray(opts.seedPermissions) ? opts.seedPermissions : []
4561
+ ]);
4562
+ }
4563
+ var cloneGrants = (grants) => structuredClone(grants);
4564
+ function openUserGrantsCache(ql, userId, opts) {
4565
+ if (opts.bypassGrantsCache) return void 0;
4566
+ const { ttlMs } = readAuthzGrantsCacheTtlMs();
4567
+ if (ttlMs <= 0) return void 0;
4568
+ if (!ql || typeof ql !== "object" || typeof ql.find !== "function") {
4569
+ return void 0;
4570
+ }
4571
+ const state = grantsCacheState(ql);
4572
+ if (!state) return void 0;
4573
+ const key = grantsCacheKey(userId, opts);
4574
+ const now = opts.nowMs ?? Date.now();
4575
+ const genAtOpen = state.gen;
4576
+ const existing = state.entries.get(key);
4577
+ if (existing) {
4578
+ if (existing.gen === state.gen && existing.expiresAt > now) {
4579
+ return { hit: cloneGrants(existing.value), commit: () => {
4580
+ } };
4581
+ }
4582
+ state.entries.delete(key);
4583
+ }
4584
+ return {
4585
+ commit(grants, nextBoundaryMs) {
4586
+ const expiresAt = Math.min(now + ttlMs, nextBoundaryMs ?? Number.POSITIVE_INFINITY);
4587
+ if (expiresAt <= now) return;
4588
+ state.entries.set(key, { value: cloneGrants(grants), gen: genAtOpen, expiresAt });
4589
+ }
4590
+ };
4591
+ }
4592
+
4593
+ // src/security/platform-admin.ts
4594
+ var import_types = require("@objectstack/types");
4595
+ var PLATFORM_ADMIN_EMAIL_SEPARATOR = ",";
4596
+ function normalizePlatformAdminEmail(value) {
4597
+ return typeof value === "string" ? value.trim().toLowerCase() : "";
4598
+ }
4599
+ function isParseableAddress(entry) {
4600
+ if (/\s/.test(entry)) return false;
4601
+ const at = entry.indexOf("@");
4602
+ if (at <= 0) return false;
4603
+ if (entry.indexOf("@", at + 1) !== -1) return false;
4604
+ return at < entry.length - 1;
4605
+ }
4606
+ var EMPTY_CONFIG = Object.freeze({
4607
+ emails: Object.freeze([]),
4608
+ declaredSpellings: Object.freeze([])
4609
+ });
4610
+ function parsePlatformAdminEmails(raw) {
4611
+ if (raw == null) return EMPTY_CONFIG;
4612
+ const text = String(raw);
4613
+ if (text.trim() === "") return EMPTY_CONFIG;
4614
+ const emails = [];
4615
+ const declaredSpellings = [];
4616
+ for (const piece of text.split(PLATFORM_ADMIN_EMAIL_SEPARATOR)) {
4617
+ const entry = normalizePlatformAdminEmail(piece);
4618
+ if (entry === "") continue;
4619
+ if (!isParseableAddress(entry)) {
4620
+ return {
4621
+ emails: Object.freeze([]),
4622
+ declaredSpellings: Object.freeze([]),
4623
+ raw: text,
4624
+ refusal: `${import_types.PLATFORM_OWNER_EMAIL_ENV} entry ${JSON.stringify(piece)} is not an email address, so the WHOLE variable is refused and this deployment has ZERO config-derived platform administrators. The entry is not skipped on purpose: silently dropping it would leave a narrower administrator set than the operator declared, with nothing to notice. Fix the entry, or remove it \u2014 ${import_types.PLATFORM_OWNER_EMAIL_ENV} takes one address or a comma-separated list of them.`
4625
+ };
4626
+ }
4627
+ if (!emails.includes(entry)) {
4628
+ emails.push(entry);
4629
+ declaredSpellings.push(piece.trim());
4630
+ }
4631
+ }
4632
+ return {
4633
+ emails: Object.freeze(emails),
4634
+ declaredSpellings: Object.freeze(declaredSpellings),
4635
+ raw: text
4636
+ };
4637
+ }
4638
+ var defaultSink = {
4639
+ error: (m) => console.error(m),
4640
+ warn: (m) => console.warn(m)
4641
+ };
4642
+ var sink = defaultSink;
4643
+ function setPlatformAdminConfigSink(next) {
4644
+ const prev = sink;
4645
+ sink = next ?? defaultSink;
4646
+ return prev;
4647
+ }
4648
+ var NOT_MEMOIZED = /* @__PURE__ */ Symbol("platform-admin-config-not-memoized");
4649
+ var memoKey = NOT_MEMOIZED;
4650
+ var memoValue = EMPTY_CONFIG;
4651
+ function resolvePlatformAdminEmails() {
4652
+ const raw = (0, import_types.resolvePlatformOwnerEmail)();
4653
+ if (memoKey !== NOT_MEMOIZED && memoKey === raw) return memoValue;
4654
+ const parsed = parsePlatformAdminEmails(raw);
4655
+ memoKey = raw;
4656
+ memoValue = parsed;
4657
+ if (parsed.refusal) sink.error(`[authz] ${parsed.refusal}`);
4658
+ return parsed;
4659
+ }
4660
+ function resetPlatformAdminEmailMemo() {
4661
+ memoKey = NOT_MEMOIZED;
4662
+ memoValue = EMPTY_CONFIG;
4663
+ }
4664
+ function matchesConfiguredPlatformAdmin(row, config) {
4665
+ if (config.emails.length === 0) return false;
4666
+ if (!row || typeof row !== "object") return false;
4667
+ if (!isConfiguredPlatformAdminEmail(row.email, config)) return false;
4668
+ return (0, import_types.isEmailVerifiedUserRow)(row);
4669
+ }
4670
+ function isConfiguredPlatformAdminEmail(email, config) {
4671
+ if (config.emails.length === 0) return false;
4672
+ const candidate = normalizePlatformAdminEmail(email);
4673
+ return candidate !== "" && config.emails.includes(candidate);
4674
+ }
4675
+ var legacyGrantPointerSaid = false;
4676
+ function reportLegacyPlatformAdminGrant(input) {
4677
+ if (legacyGrantPointerSaid) return;
4678
+ legacyGrantPointerSaid = true;
4679
+ const email = normalizePlatformAdminEmail(input.email);
4680
+ sink.warn(
4681
+ `[authz] user ${input.userId} holds PLATFORM_ADMIN through the legacy unscoped 'admin_full_access' grant row, not through ${import_types.PLATFORM_OWNER_EMAIL_ENV}. The grant row is the OLD anchor and is honoured for now; it is removed in a later release. Re-anchor this deployment by declaring its administrators in configuration: ${import_types.PLATFORM_OWNER_EMAIL_ENV}=${email || "<the administrator's verified email address>"} (comma-separated for several), and make sure each account's email is VERIFIED \u2014 an unverified account holding a configured address is not an administrator. Reported once per process; further holders are not listed.`
4682
+ );
4683
+ }
4684
+ function resetLegacyPlatformAdminGrantReport() {
4685
+ legacyGrantPointerSaid = false;
4686
+ }
4687
+
4490
4688
  // src/security/posture-ladder.ts
4491
4689
  var POSTURE_LADDER = [
4492
4690
  "PLATFORM_ADMIN",
@@ -4560,6 +4758,18 @@ function safeJsonParse2(s, fallback) {
4560
4758
  return fallback;
4561
4759
  }
4562
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
+ }
4563
4773
  async function tryFind(ql, object, where, limit = 100, organizationId) {
4564
4774
  if (!ql || typeof ql.find !== "function") return [];
4565
4775
  try {
@@ -4567,8 +4777,9 @@ async function tryFind(ql, object, where, limit = 100, organizationId) {
4567
4777
  let rows = await ql.find(object, { where, limit, context });
4568
4778
  if (rows && rows.value) rows = rows.value;
4569
4779
  return Array.isArray(rows) ? rows : [];
4570
- } catch {
4571
- return [];
4780
+ } catch (err) {
4781
+ if ((0, import_types2.isMissingTableError)(err, object)) return [];
4782
+ throw new AuthzStoreUnavailableError(object, err);
4572
4783
  }
4573
4784
  }
4574
4785
  async function resolveAuthzContext(input) {
@@ -4582,9 +4793,15 @@ async function resolveAuthzContext(input) {
4582
4793
  };
4583
4794
  let userId;
4584
4795
  let tenantId;
4796
+ let sessionId;
4585
4797
  const admission = await resolveApiKeyAdmission(ql, headers, input.nowMs, input.tenancyPosture);
4586
4798
  if (admission.outcome === "refused") {
4587
- 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
+ });
4588
4805
  return ctx;
4589
4806
  }
4590
4807
  const keyPrincipal = admission.outcome === "admitted" ? admission.principal : void 0;
@@ -4600,6 +4817,8 @@ async function resolveAuthzContext(input) {
4600
4817
  const sessionData = await input.getSession(headers);
4601
4818
  userId = sessionData?.user?.id ?? sessionData?.session?.userId;
4602
4819
  tenantId = tenantId ?? sessionData?.session?.activeOrganizationId;
4820
+ const rawSessionId = sessionData?.session?.id;
4821
+ sessionId = typeof rawSessionId === "string" && rawSessionId ? rawSessionId : void 0;
4603
4822
  ctx.accessToken = sessionData?.session?.token ?? ctx.accessToken;
4604
4823
  if (sessionData?.user?.email) ctx.email = String(sessionData.user.email);
4605
4824
  } catch {
@@ -4609,7 +4828,7 @@ async function resolveAuthzContext(input) {
4609
4828
  ctx.userId = userId;
4610
4829
  if (tenantId) ctx.tenantId = tenantId;
4611
4830
  if (!ql || typeof ql.find !== "function") return ctx;
4612
- const grants = await resolveUserAuthzGrants(ql, userId, {
4831
+ let grants = await resolveUserAuthzGrants(ql, userId, {
4613
4832
  tenantId,
4614
4833
  nowMs: input.nowMs,
4615
4834
  seedPermissions: ctx.permissions,
@@ -4618,19 +4837,37 @@ async function resolveAuthzContext(input) {
4618
4837
  if (keyPrincipal?.tenantId && input.tenancyPosture) {
4619
4838
  const posture = input.tenancyPosture;
4620
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
+ });
4621
4846
  return {
4622
4847
  positions: [],
4623
4848
  permissions: [],
4624
4849
  systemPermissions: [],
4625
4850
  org_user_ids: [],
4626
- accessible_org_ids: [],
4627
- authRefusal: {
4628
- reason: "organization_membership_ended",
4629
- 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."
4630
- }
4851
+ accessible_org_ids: []
4631
4852
  };
4632
4853
  }
4633
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
+ }
4634
4871
  ctx.positions = grants.positions;
4635
4872
  ctx.permissions = grants.permissions;
4636
4873
  ctx.systemPermissions = grants.systemPermissions;
@@ -4642,6 +4879,8 @@ async function resolveAuthzContext(input) {
4642
4879
  return ctx;
4643
4880
  }
4644
4881
  async function resolveUserAuthzGrants(ql, userId, opts = {}) {
4882
+ const grantsCache = openUserGrantsCache(ql, userId, opts);
4883
+ if (grantsCache?.hit) return grantsCache.hit;
4645
4884
  const { tenantId } = opts;
4646
4885
  const grants = {
4647
4886
  positions: [],
@@ -4662,7 +4901,8 @@ async function resolveUserAuthzGrants(ql, userId, opts = {}) {
4662
4901
  }
4663
4902
  return userRow;
4664
4903
  };
4665
- const needsUserRow = !grants.email || !grants.permissions.includes("ai_seat");
4904
+ const platformAdminConfig = resolvePlatformAdminEmails();
4905
+ const needsUserRow = !grants.email || !grants.permissions.includes("ai_seat") || platformAdminConfig.emails.length > 0;
4666
4906
  const [, members, userPositionRows, orgMembersLeg, upsRowsAll] = await Promise.all([
4667
4907
  needsUserRow ? getUserRow() : Promise.resolve(void 0),
4668
4908
  tryFind(ql, "sys_member", { user_id: userId }, 200),
@@ -4688,7 +4928,7 @@ async function resolveUserAuthzGrants(ql, userId, opts = {}) {
4688
4928
  for (const m of activeMembers) {
4689
4929
  if (m.role && typeof m.role === "string") {
4690
4930
  for (const raw of m.role.split(",").map((s) => s.trim()).filter(Boolean)) {
4691
- const r = (0, import_spec.mapMembershipRole)(raw);
4931
+ const r = (0, import_spec2.mapMembershipRole)(raw);
4692
4932
  if (!grants.positions.includes(r)) grants.positions.push(r);
4693
4933
  }
4694
4934
  }
@@ -4744,7 +4984,7 @@ async function resolveUserAuthzGrants(ql, userId, opts = {}) {
4744
4984
  const mergedTabs = {};
4745
4985
  for (const ps of psRows) {
4746
4986
  if (ps.name && !grants.permissions.includes(ps.name)) grants.permissions.push(ps.name);
4747
- if (ps.name === import_spec.ADMIN_FULL_ACCESS && unscopedUserPsIds.has(ps.id)) hasPlatformAdminGrant = true;
4987
+ if (ps.name === import_spec2.ADMIN_FULL_ACCESS && unscopedUserPsIds.has(ps.id)) hasPlatformAdminGrant = true;
4748
4988
  const sysPerms = typeof ps.system_permissions === "string" ? safeJsonParse2(ps.system_permissions, []) : ps.system_permissions ?? ps.systemPermissions;
4749
4989
  if (Array.isArray(sysPerms)) {
4750
4990
  for (const p of sysPerms) {
@@ -4764,32 +5004,49 @@ async function resolveUserAuthzGrants(ql, userId, opts = {}) {
4764
5004
  }
4765
5005
  if (Object.keys(mergedTabs).length > 0) grants.tabPermissions = mergedTabs;
4766
5006
  }
4767
- if (hasPlatformAdminGrant && !grants.positions.includes(import_spec.BUILTIN_IDENTITY_PLATFORM_ADMIN)) {
4768
- grants.positions.unshift(import_spec.BUILTIN_IDENTITY_PLATFORM_ADMIN);
5007
+ const configConfersPlatformAdmin = platformAdminConfig.emails.length > 0 && matchesConfiguredPlatformAdmin(await getUserRow(), platformAdminConfig);
5008
+ if (configConfersPlatformAdmin) {
5009
+ hasPlatformAdminGrant = true;
5010
+ if (!grants.permissions.includes(import_spec2.ADMIN_FULL_ACCESS)) grants.permissions.push(import_spec2.ADMIN_FULL_ACCESS);
5011
+ for (const p of import_spec2.ADMIN_FULL_ACCESS_CAPABILITIES.systemPermissions ?? []) {
5012
+ if (!grants.systemPermissions.includes(p)) grants.systemPermissions.push(p);
5013
+ }
5014
+ } else if (hasPlatformAdminGrant) {
5015
+ if ((0, import_security2.postureEnforcesWall)((0, import_types2.resolveTenancyPosture)())) {
5016
+ reportLegacyPlatformAdminGrant({ userId, email: userRow?.email });
5017
+ }
5018
+ }
5019
+ if (hasPlatformAdminGrant && !grants.positions.includes(import_spec2.BUILTIN_IDENTITY_PLATFORM_ADMIN)) {
5020
+ grants.positions.unshift(import_spec2.BUILTIN_IDENTITY_PLATFORM_ADMIN);
4769
5021
  }
4770
5022
  grants.posture = derivePosture({
4771
5023
  isPlatformAdmin: hasPlatformAdminGrant,
4772
5024
  // [ADR-0105 D4] Either org-admin capability set resolves the rung — the
4773
5025
  // wall-less variant differs only by withholding the superuser bits.
4774
- isTenantAdmin: import_spec.ORGANIZATION_ADMIN_GRANTS.some((n) => grants.permissions.includes(n))
5026
+ isTenantAdmin: import_spec2.ORGANIZATION_ADMIN_GRANTS.some((n) => grants.permissions.includes(n))
4775
5027
  });
4776
5028
  if (!grants.permissions.includes("ai_seat")) {
4777
5029
  const aiAccess = (await getUserRow())?.ai_access;
4778
5030
  if (aiAccess === true || aiAccess === 1 || aiAccess === "1") grants.permissions.push("ai_seat");
4779
5031
  }
5032
+ grantsCache?.commit(
5033
+ grants,
5034
+ nextGrantValidityBoundary([...members, ...userPositionRows, ...upsRowsAll], nowMs)
5035
+ );
4780
5036
  return grants;
4781
5037
  }
4782
- function isValidTimeZone(tz) {
5038
+ async function hasPlatformAdminStanding(ql, userId, opts = {}) {
5039
+ if (!ql || typeof userId !== "string" || userId.length === 0) return false;
4783
5040
  try {
4784
- new Intl.DateTimeFormat("en-US", { timeZone: tz });
4785
- return true;
5041
+ const grants = await resolveUserAuthzGrants(ql, userId, { nowMs: opts.nowMs });
5042
+ return grants.posture === "PLATFORM_ADMIN";
4786
5043
  } catch {
4787
5044
  return false;
4788
5045
  }
4789
5046
  }
4790
5047
  function coerceTimeZone(value) {
4791
5048
  const s = typeof value === "string" ? value.trim() : value != null ? String(value).trim() : "";
4792
- return s && isValidTimeZone(s) ? s : void 0;
5049
+ return s && (0, import_shared.isValueDomainMember)("iana_time_zone", s) ? s : void 0;
4793
5050
  }
4794
5051
  function coerceLocale(value) {
4795
5052
  const s = typeof value === "string" ? value.trim() : value != null ? String(value).trim() : "";
@@ -4800,25 +5057,98 @@ function coerceCurrency(value) {
4800
5057
  return /^[A-Z]{3}$/.test(s) ? s : void 0;
4801
5058
  }
4802
5059
  var LOCALIZATION_FAILURE_CACHE_TTL_MS = 3e4;
4803
- var localizationFailureCache = /* @__PURE__ */ new WeakMap();
5060
+ var LOCALIZATION_CACHE_TTL_ENV = "OS_LOCALIZATION_CACHE_TTL_MS";
5061
+ var LOCALIZATION_SUCCESS_CACHE_DEFAULT_TTL_MS = 3e4;
5062
+ function localizationSuccessCacheTtlMs(env = typeof process !== "undefined" ? process.env : {}) {
5063
+ const raw = env[LOCALIZATION_CACHE_TTL_ENV];
5064
+ if (raw === void 0 || raw.trim() === "") return LOCALIZATION_SUCCESS_CACHE_DEFAULT_TTL_MS;
5065
+ const parsed = Number(raw.trim());
5066
+ if (!Number.isFinite(parsed) || parsed < 0) return 0;
5067
+ return Math.floor(parsed);
5068
+ }
5069
+ function readWriteEpoch(ql) {
5070
+ if (!ql || typeof ql !== "object") return void 0;
5071
+ const epoch = ql.writeEpoch;
5072
+ if (!epoch || typeof epoch !== "object") return void 0;
5073
+ const seam = epoch;
5074
+ if (typeof seam.current !== "number" || typeof seam.bump !== "function" || typeof seam.subscribe !== "function") {
5075
+ return void 0;
5076
+ }
5077
+ return seam.current;
5078
+ }
5079
+ var localizationSettingsStates = /* @__PURE__ */ new WeakMap();
5080
+ var localizationNoSettingsState = { gen: 0 };
5081
+ function localizationSettingsState(settings) {
5082
+ if (!settings || typeof settings !== "object") return localizationNoSettingsState;
5083
+ const existing = localizationSettingsStates.get(settings);
5084
+ if (existing) return existing;
5085
+ const state = { gen: 0 };
5086
+ localizationSettingsStates.set(settings, state);
5087
+ const subscribe = settings.subscribe;
5088
+ if (typeof subscribe === "function") {
5089
+ try {
5090
+ subscribe.call(
5091
+ settings,
5092
+ "localization",
5093
+ () => {
5094
+ state.gen += 1;
5095
+ }
5096
+ );
5097
+ } catch {
5098
+ }
5099
+ }
5100
+ return state;
5101
+ }
5102
+ var localizationCache = /* @__PURE__ */ new WeakMap();
5103
+ function localizationEntryIsLive(entry, epoch, settings) {
5104
+ if (entry.kind === "failure") return true;
5105
+ return entry.epoch === epoch && entry.settings === settings && entry.settingsGen === settings.gen;
5106
+ }
5107
+ function putLocalizationEntry(ql, key, entry) {
5108
+ const bucket = localizationCache.get(ql) ?? /* @__PURE__ */ new Map();
5109
+ bucket.set(key, entry);
5110
+ localizationCache.set(ql, bucket);
5111
+ }
4804
5112
  async function resolveLocalizationContext(input) {
4805
- const { ql, tenantId, userId } = input;
5113
+ const { ql, settings, tenantId, userId } = input;
4806
5114
  const cacheKey = `${tenantId ?? ""}|${userId ?? ""}`;
4807
- if (ql && typeof ql === "object") {
4808
- const hit = localizationFailureCache.get(ql)?.get(cacheKey);
4809
- if (hit && hit.expiresAt > Date.now()) return hit.value;
4810
- }
4811
- const { value, failed } = await resolveLocalizationContextUncached(input);
4812
- if (failed && ql && typeof ql === "object") {
4813
- const bucket = localizationFailureCache.get(ql) ?? /* @__PURE__ */ new Map();
4814
- bucket.set(cacheKey, { value, expiresAt: Date.now() + LOCALIZATION_FAILURE_CACHE_TTL_MS });
4815
- localizationFailureCache.set(ql, bucket);
5115
+ const cacheable = Boolean(ql) && typeof ql === "object";
5116
+ const epoch = cacheable ? readWriteEpoch(ql) : void 0;
5117
+ const settingsState = localizationSettingsState(settings);
5118
+ if (cacheable) {
5119
+ const hit = localizationCache.get(ql)?.get(cacheKey);
5120
+ if (hit && hit.expiresAt > Date.now() && localizationEntryIsLive(hit, epoch, settingsState)) {
5121
+ return hit.value;
5122
+ }
5123
+ }
5124
+ const { value, backendFailed } = await resolveLocalizationContextUncached(input);
5125
+ if (!cacheable) return value;
5126
+ if (backendFailed) {
5127
+ putLocalizationEntry(ql, cacheKey, {
5128
+ value,
5129
+ expiresAt: Date.now() + LOCALIZATION_FAILURE_CACHE_TTL_MS,
5130
+ kind: "failure"
5131
+ });
5132
+ return value;
5133
+ }
5134
+ const ttlMs = localizationSuccessCacheTtlMs();
5135
+ if (epoch !== void 0 && ttlMs > 0) {
5136
+ putLocalizationEntry(ql, cacheKey, {
5137
+ value,
5138
+ expiresAt: Date.now() + ttlMs,
5139
+ kind: "success",
5140
+ epoch,
5141
+ settings: settingsState,
5142
+ settingsGen: settingsState.gen
5143
+ });
5144
+ } else {
5145
+ localizationCache.get(ql)?.delete(cacheKey);
4816
5146
  }
4817
5147
  return value;
4818
5148
  }
4819
5149
  async function resolveLocalizationContextUncached(input) {
4820
5150
  const { ql, settings, tenantId, userId } = input;
4821
- let failed = false;
5151
+ let backendFailed = false;
4822
5152
  try {
4823
5153
  if (settings && typeof settings.get === "function") {
4824
5154
  const sctx = { tenantId, userId };
@@ -4832,33 +5162,22 @@ async function resolveLocalizationContextUncached(input) {
4832
5162
  localeRes = many.locale;
4833
5163
  currencyRes = many.currency;
4834
5164
  } catch {
4835
- failed = true;
4836
5165
  }
4837
5166
  } else {
4838
5167
  [tzRes, localeRes, currencyRes] = await Promise.all([
4839
- settings.get("localization", "timezone", sctx).catch(() => {
4840
- failed = true;
4841
- return void 0;
4842
- }),
4843
- settings.get("localization", "locale", sctx).catch(() => {
4844
- failed = true;
4845
- return void 0;
4846
- }),
4847
- settings.get("localization", "currency", sctx).catch(() => {
4848
- failed = true;
4849
- return void 0;
4850
- })
5168
+ settings.get("localization", "timezone", sctx).catch(() => void 0),
5169
+ settings.get("localization", "locale", sctx).catch(() => void 0),
5170
+ settings.get("localization", "currency", sctx).catch(() => void 0)
4851
5171
  ]);
4852
5172
  }
4853
5173
  const tz = coerceTimeZone(tzRes?.value);
4854
5174
  const locale = coerceLocale(localeRes?.value);
4855
5175
  const currency = coerceCurrency(currencyRes?.value);
4856
5176
  if (tz || locale || currency) {
4857
- return { value: { timezone: tz ?? "UTC", locale: locale ?? "en-US", currency }, failed: false };
5177
+ return { value: { timezone: tz ?? "UTC", locale: locale ?? "en-US", currency }, backendFailed: false };
4858
5178
  }
4859
5179
  }
4860
5180
  } catch {
4861
- failed = true;
4862
5181
  }
4863
5182
  let rows = [];
4864
5183
  if (ql && typeof ql.find === "function") {
@@ -4871,7 +5190,7 @@ async function resolveLocalizationContextUncached(input) {
4871
5190
  if (result && result.value) result = result.value;
4872
5191
  rows = Array.isArray(result) ? result : [];
4873
5192
  } catch {
4874
- failed = true;
5193
+ backendFailed = true;
4875
5194
  }
4876
5195
  }
4877
5196
  const valueOf = (k) => rows.find((r) => r.key === k)?.value;
@@ -4881,7 +5200,7 @@ async function resolveLocalizationContextUncached(input) {
4881
5200
  locale: coerceLocale(valueOf("locale")) ?? "en-US",
4882
5201
  currency: coerceCurrency(valueOf("currency"))
4883
5202
  },
4884
- failed
5203
+ backendFailed
4885
5204
  };
4886
5205
  }
4887
5206
 
@@ -5043,7 +5362,7 @@ function shouldDenyAnonymous(input) {
5043
5362
  var ADMIN_STANDING_SURFACE = {
5044
5363
  sys_permission_set: {
5045
5364
  role: "derives",
5046
- reason: "The row `platform_admin` is resolved BY NAME from (\xA76b). Renaming it, deleting it or switching it off (ADR-0049 `active`, read here since #8613) un-makes every platform admin at once, with no identity table touched.",
5365
+ reason: "The row `admin_full_access` is resolved BY NAME from (\xA76b) \u2014 `platform_admin` is the POSITION that row derives, not the row's own name. Renaming it, deleting it or switching it off (ADR-0049 `active`, read here since #8613) un-makes every GRANT-derived platform admin at once, with no identity table touched. \u26A0\uFE0F It does NOT un-make a CONFIG-derived one (\xA76b-config, #11970): that route sets the same standing from `ADMIN_FULL_ACCESS_CAPABILITIES` in `@objectstack/spec` and matches the caller's own stored `sys_user` row, so it touches an identity table and never reads this one. With `OS_PLATFORM_OWNER_EMAIL` unset the first sentence is the whole truth; with it declared, this row stops being the single point that un-makes every administrator.",
5047
5366
  columns: [
5048
5367
  "id",
5049
5368
  "name",
@@ -5085,8 +5404,14 @@ var ADMIN_STANDING_SURFACE = {
5085
5404
  ]
5086
5405
  },
5087
5406
  sys_user: {
5088
- role: "reads-only",
5089
- reason: "Read for the `current_user.email` RLS fallback and the ADR-0024 `ai_seat` synthesis (\xA77). Neither confers administrator standing. The guard does watch this table, but for the ban/delete WRITE SHAPES \u2014 `banned` is never read here, so it is not a derivation column and carries no standing-key list."
5407
+ role: "derives",
5408
+ reason: "[#11663 L2] RECLASSIFIED from `reads-only`. This table used to be read only for the `current_user.email` RLS fallback and the ADR-0024 `ai_seat` synthesis (\xA77), and the note here said so: \"Neither confers administrator standing.\" That sentence is now FALSE. The config anchor (\xA76b-config) matches the row's own `email` against the deployment's declared administrator list and requires `email_verified` to read verified, so a write that changes either column takes platform-admin standing away from a config-derived administrator \u2014 an address change and an email_verified reset are both ordinary, reachable writes, and neither touches a grant table. `banned` stays absent from the column list because the resolver still never reads it; the guard watches the ban/delete WRITE SHAPES on this table for its own reasons, which is a different question from what this resolver consumes.",
5409
+ columns: [
5410
+ "id",
5411
+ "email",
5412
+ "email_verified",
5413
+ "ai_access"
5414
+ ]
5090
5415
  },
5091
5416
  sys_user_position: {
5092
5417
  role: "reads-only",
@@ -5101,6 +5426,13 @@ var ADMIN_STANDING_SURFACE = {
5101
5426
  reason: "Position-bound permission sets (\xA76a). Contributes ids to `psIds` \u2014 and therefore names to `permissions` \u2014 but not to `unscopedUserPsIds`, which is the set \xA76b tests for platform-admin standing."
5102
5427
  }
5103
5428
  };
5429
+ var ADMIN_STANDING_NON_TABLE_INPUTS = [
5430
+ {
5431
+ kind: "env",
5432
+ name: "OS_PLATFORM_OWNER_EMAIL",
5433
+ reason: "The deployment's declared platform administrator(s) \u2014 one address or a comma-separated list, matched case-insensitively against `sys_user.email` and conferring standing only when that row's `email_verified` reads verified (\xA76b-config). Read live on every derivation with a per-process memo keyed on the raw string, so a rolled process picks up a change with no special path. Unset, blank, or carrying any unparseable entry means ZERO config-derived administrators, fail closed. No runtime write reaches it, so no break-glass guard can simulate a change to it: revocation is a configuration change plus a process roll, by design."
5434
+ }
5435
+ ];
5104
5436
  function adminStandingTables() {
5105
5437
  return Object.entries(ADMIN_STANDING_SURFACE).filter(([, t]) => t.role === "derives").map(([name]) => name).sort();
5106
5438
  }
@@ -5132,6 +5464,9 @@ function withoutOperationPrivateKeys(exec) {
5132
5464
  return out;
5133
5465
  }
5134
5466
 
5467
+ // src/security/authz-invalidation-channel.ts
5468
+ var AUTHZ_INVALIDATED_CHANNEL = "authz.invalidated";
5469
+
5135
5470
  // src/utils/datetime.ts
5136
5471
  var import_data = require("@objectstack/spec/data");
5137
5472
  function calendarPartsInTz(d, tz) {
@@ -5412,7 +5747,7 @@ function omitInternalFieldsFromWriteResponse(schema, records) {
5412
5747
  }
5413
5748
 
5414
5749
  // src/utils/migration-journal.ts
5415
- var import_node_crypto3 = require("crypto");
5750
+ var import_node_crypto4 = require("crypto");
5416
5751
  var import_system4 = require("@objectstack/spec/system");
5417
5752
  var SYSTEM_CTX = { isSystem: true };
5418
5753
  var DEFAULT_CHUNK_SIZE = 200;
@@ -5467,7 +5802,7 @@ function hashMigrationPlan(plan, chunks) {
5467
5802
  steps: plan.steps.map((s) => s.name),
5468
5803
  chunks: chunks.map((c) => [c.stepIndex, c.offset, c.length])
5469
5804
  });
5470
- return (0, import_node_crypto3.createHash)("sha256").update(shape, "utf8").digest("hex").slice(0, 32);
5805
+ return (0, import_node_crypto4.createHash)("sha256").update(shape, "utf8").digest("hex").slice(0, 32);
5471
5806
  }
5472
5807
  async function appendEvent(engine, event, execContext) {
5473
5808
  await engine.insert(
@@ -5540,7 +5875,7 @@ async function runMigrationJournal(engine, plan, options = {}) {
5540
5875
  }
5541
5876
  const { chunks, planHash, rowsByStep } = await loadPlan(engine, plan, options.chunkSize);
5542
5877
  const resuming = Boolean(options.runId);
5543
- const runId = options.runId ?? (0, import_node_crypto3.randomUUID)();
5878
+ const runId = options.runId ?? (0, import_node_crypto4.randomUUID)();
5544
5879
  let events = [];
5545
5880
  let seq = 0;
5546
5881
  let committed = /* @__PURE__ */ new Set();
@@ -5798,6 +6133,52 @@ function errText(err) {
5798
6133
  }
5799
6134
  }
5800
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
+
5801
6182
  // src/utils/filter-tokens.ts
5802
6183
  var import_data2 = require("@objectstack/spec/data");
5803
6184
  var UnknownFilterTokenError = class extends Error {
@@ -6032,6 +6413,110 @@ function isUninterpretableTemporalComparand(kind, value) {
6032
6413
  return !(readsAsWallClock(s) || readsAsInstant(s));
6033
6414
  }
6034
6415
 
6416
+ // src/utils/metadata-activation-store.ts
6417
+ var METADATA_ACTIVATION_TABLE = "sys_metadata_activation";
6418
+ var SYSTEM_CTX2 = { isSystem: true, positions: [], permissions: [] };
6419
+ var InMemoryMetadataActivationStore = class {
6420
+ constructor() {
6421
+ this.rows = /* @__PURE__ */ new Map();
6422
+ }
6423
+ async list() {
6424
+ return [...this.rows.values()];
6425
+ }
6426
+ async setActive(row) {
6427
+ this.rows.set(row.name, { ...row });
6428
+ }
6429
+ };
6430
+ var ObjectStoreMetadataActivationStore = class {
6431
+ constructor(engine, metadataType) {
6432
+ this.engine = engine;
6433
+ this.metadataType = metadataType;
6434
+ }
6435
+ /**
6436
+ * Every row of this type. Read once at boot to hydrate the consumer's
6437
+ * projection.
6438
+ *
6439
+ * The only scoping is the `metadata_type` discriminator: the ledger is
6440
+ * deployment-wide and has no tenant column, so there is no second axis to
6441
+ * filter on (see the module header).
6442
+ */
6443
+ async list() {
6444
+ const rows = await this.engine.find(METADATA_ACTIVATION_TABLE, {
6445
+ where: { metadata_type: this.metadataType },
6446
+ context: SYSTEM_CTX2
6447
+ });
6448
+ if (!Array.isArray(rows)) return [];
6449
+ const out = [];
6450
+ for (const row of rows) {
6451
+ const r = row;
6452
+ if (typeof r.name !== "string" || !r.name) continue;
6453
+ out.push({
6454
+ name: r.name,
6455
+ packageId: typeof r.package_id === "string" ? r.package_id : "",
6456
+ // The column defaults to `true`; only an explicit `false`
6457
+ // disarms. A driver that round-trips booleans as 0/1
6458
+ // (SQLite/libsql) is read through the same `=== false || === 0`
6459
+ // test, so a `0` is not mistaken for `true`.
6460
+ active: !(r.active === false || r.active === 0)
6461
+ });
6462
+ }
6463
+ return out;
6464
+ }
6465
+ /**
6466
+ * Insert or update the row for one packaged artifact.
6467
+ *
6468
+ * Read-then-write rather than a blind upsert because the object's
6469
+ * uniqueness is a DECLARED index (`unique: 'global'` over
6470
+ * `(metadata_type, name)`), not a primary key this store controls: there is
6471
+ * no id to collide on, so an insert-and-catch could not tell "already
6472
+ * there" from a real store failure.
6473
+ *
6474
+ * That index is also why taking the FIRST match is taking the only one: the
6475
+ * read below is keyed on exactly the index's two columns, so it can match
6476
+ * at most one row. It used to pick the first row with a NULL organization
6477
+ * out of the result, back when the table carried a reserved tenant column;
6478
+ * with no such column the set it was choosing from can no longer hold more
6479
+ * than one member.
6480
+ */
6481
+ async setActive(row) {
6482
+ const existing = await this.engine.find(METADATA_ACTIVATION_TABLE, {
6483
+ where: { metadata_type: this.metadataType, name: row.name },
6484
+ context: SYSTEM_CTX2
6485
+ });
6486
+ const current = Array.isArray(existing) ? existing[0] : void 0;
6487
+ if (current && current.id != null) {
6488
+ await this.engine.update(
6489
+ METADATA_ACTIVATION_TABLE,
6490
+ { id: current.id, active: row.active, package_id: row.packageId },
6491
+ { context: SYSTEM_CTX2 }
6492
+ );
6493
+ return;
6494
+ }
6495
+ await this.engine.insert(
6496
+ METADATA_ACTIVATION_TABLE,
6497
+ {
6498
+ metadata_type: this.metadataType,
6499
+ name: row.name,
6500
+ package_id: row.packageId,
6501
+ active: row.active
6502
+ },
6503
+ { context: SYSTEM_CTX2 }
6504
+ );
6505
+ }
6506
+ /**
6507
+ * Read the backing table once so a misconfiguration surfaces at BOOT
6508
+ * rather than as a failed toggle later. Throws the driver error verbatim —
6509
+ * `no such table: sys_metadata_activation` means the object was never
6510
+ * registered (or its schema never synced) in this composition.
6511
+ *
6512
+ * ⚠️ Unscoped by design: the question is "does the TABLE read at all",
6513
+ * which is a property of the composition, not of one `metadata_type`.
6514
+ */
6515
+ async probe() {
6516
+ await this.engine.find(METADATA_ACTIVATION_TABLE, { where: {}, limit: 1, context: SYSTEM_CTX2 });
6517
+ }
6518
+ };
6519
+
6035
6520
  // src/utils/record-not-found.ts
6036
6521
  function recordNotFoundError(object, id) {
6037
6522
  const err = new Error(`Record ${id} not found in ${object}`);
@@ -6042,6 +6527,45 @@ function recordNotFoundError(object, id) {
6042
6527
  }
6043
6528
 
6044
6529
  // src/health-monitor.ts
6530
+ var RECOVERY_IS_THRESHOLD_GATED = {
6531
+ degraded: true,
6532
+ unhealthy: true,
6533
+ failed: true,
6534
+ recovering: true,
6535
+ healthy: false,
6536
+ unknown: false
6537
+ };
6538
+ function healthMonitorRefusal(message) {
6539
+ const err = new Error(message);
6540
+ err.code = "VALIDATION_ERROR";
6541
+ err.status = 400;
6542
+ return err;
6543
+ }
6544
+ var RETIRED_HEALTH_CHECK_KEYS = [
6545
+ [
6546
+ "autoRestart",
6547
+ "'autoRestart' was removed from PluginHealthCheck in @objectstack/spec 18 (#12032, ADR-0049 enforce-or-remove) \u2014 it never restarted a plugin. `attemptRestart` called `plugin.destroy()` and stopped there, then logged 'Plugin restarted' and set status `recovering`, and the periodic checks carried on against the destroyed instance \u2014 which the default check (`{ name: 'plugin-loaded', status: 'passed' }`) passes forever, so a destroyed, never-re-initialised plugin ended up reported `healthy`. Delete the key. This monitor no longer destroys anything: a failing plugin is reported `unhealthy` or `failed` and left alone."
6548
+ ],
6549
+ [
6550
+ "maxRestartAttempts",
6551
+ "'maxRestartAttempts' was removed from PluginHealthCheck in @objectstack/spec 18 (#12032, ADR-0049 enforce-or-remove) \u2014 it capped a restart that never happened, so it only counted `destroy()` calls. Delete the key."
6552
+ ],
6553
+ [
6554
+ "restartBackoff",
6555
+ "'restartBackoff' was removed from PluginHealthCheck in @objectstack/spec 18 (#12032, ADR-0049 enforce-or-remove) \u2014 it delayed a restart that never happened, so it only moved when the `destroy()` landed. Delete the key."
6556
+ ]
6557
+ ];
6558
+ var RESTART_IS_THE_HOSTS_JOB = " Restarting a plugin is the HOST's job in this host-driven library: poll `getHealthStatus(pluginName)` / `getHealthReport(pluginName)` and act on `unhealthy` / `failed` at the level that owns the plugin's lifetime \u2014 recreate the kernel, or let your supervisor restart the process.";
6559
+ function assertNoRetiredKeys(pluginName, config) {
6560
+ for (const [key, guidance] of RETIRED_HEALTH_CHECK_KEYS) {
6561
+ if (!Object.prototype.hasOwnProperty.call(config, key)) {
6562
+ continue;
6563
+ }
6564
+ throw healthMonitorRefusal(
6565
+ `[HealthMonitor] Plugin '${pluginName}': ${guidance}${RESTART_IS_THE_HOSTS_JOB}`
6566
+ );
6567
+ }
6568
+ }
6045
6569
  var PluginHealthMonitor = class {
6046
6570
  constructor(logger) {
6047
6571
  this.healthChecks = /* @__PURE__ */ new Map();
@@ -6050,18 +6574,17 @@ var PluginHealthMonitor = class {
6050
6574
  this.checkIntervals = /* @__PURE__ */ new Map();
6051
6575
  this.failureCounters = /* @__PURE__ */ new Map();
6052
6576
  this.successCounters = /* @__PURE__ */ new Map();
6053
- this.restartAttempts = /* @__PURE__ */ new Map();
6054
6577
  this.logger = logger.child({ component: "HealthMonitor" });
6055
6578
  }
6056
6579
  /**
6057
6580
  * Register a plugin for health monitoring
6058
6581
  */
6059
6582
  registerPlugin(pluginName, config) {
6583
+ assertNoRetiredKeys(pluginName, config);
6060
6584
  this.healthChecks.set(pluginName, config);
6061
6585
  this.healthStatus.set(pluginName, "unknown");
6062
6586
  this.failureCounters.set(pluginName, 0);
6063
6587
  this.successCounters.set(pluginName, 0);
6064
- this.restartAttempts.set(pluginName, 0);
6065
6588
  this.logger.info("Plugin registered for health monitoring", {
6066
6589
  plugin: pluginName,
6067
6590
  interval: config.interval
@@ -6113,6 +6636,7 @@ var PluginHealthMonitor = class {
6113
6636
  let status = "healthy";
6114
6637
  let message;
6115
6638
  const checks = [];
6639
+ let failureRoute;
6116
6640
  try {
6117
6641
  if (config.checkMethod && typeof plugin[config.checkMethod] === "function") {
6118
6642
  const checkResult = await this.raceCheckTimeout(
@@ -6133,8 +6657,8 @@ var PluginHealthMonitor = class {
6133
6657
  if (status === "healthy") {
6134
6658
  this.successCounters.set(pluginName, (this.successCounters.get(pluginName) || 0) + 1);
6135
6659
  this.failureCounters.set(pluginName, 0);
6136
- const currentStatus = this.healthStatus.get(pluginName);
6137
- if (currentStatus === "unhealthy" || currentStatus === "degraded") {
6660
+ const currentStatus = this.healthStatus.get(pluginName) ?? "unknown";
6661
+ if (RECOVERY_IS_THRESHOLD_GATED[currentStatus]) {
6138
6662
  const successCount = this.successCounters.get(pluginName) || 0;
6139
6663
  if (successCount >= config.successThreshold) {
6140
6664
  this.healthStatus.set(pluginName, "healthy");
@@ -6146,27 +6670,11 @@ var PluginHealthMonitor = class {
6146
6670
  this.healthStatus.set(pluginName, "healthy");
6147
6671
  }
6148
6672
  } else {
6149
- this.failureCounters.set(pluginName, (this.failureCounters.get(pluginName) || 0) + 1);
6150
- this.successCounters.set(pluginName, 0);
6151
- const failureCount = this.failureCounters.get(pluginName) || 0;
6152
- if (failureCount >= config.failureThreshold) {
6153
- this.healthStatus.set(pluginName, "unhealthy");
6154
- this.logger.warn("Plugin marked as unhealthy", {
6155
- plugin: pluginName,
6156
- failures: failureCount
6157
- });
6158
- if (config.autoRestart) {
6159
- await this.attemptRestart(pluginName, plugin, config);
6160
- }
6161
- } else {
6162
- this.healthStatus.set(pluginName, "degraded");
6163
- }
6673
+ failureRoute = "returned";
6164
6674
  }
6165
6675
  } catch (error) {
6166
6676
  status = "failed";
6167
6677
  message = error instanceof Error ? error.message : "Unknown error";
6168
- this.failureCounters.set(pluginName, (this.failureCounters.get(pluginName) || 0) + 1);
6169
- this.healthStatus.set(pluginName, "failed");
6170
6678
  checks.push({
6171
6679
  name: "health-check",
6172
6680
  status: "failed",
@@ -6176,69 +6684,59 @@ var PluginHealthMonitor = class {
6176
6684
  plugin: pluginName,
6177
6685
  error
6178
6686
  });
6687
+ failureRoute = "thrown";
6688
+ }
6689
+ if (failureRoute) {
6690
+ this.recordFailedRound(pluginName, config, failureRoute);
6179
6691
  }
6180
6692
  const report = {
6181
6693
  status: this.healthStatus.get(pluginName) || "unknown",
6182
6694
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
6183
6695
  message,
6184
6696
  metrics: {
6185
- uptime: Date.now() - startTime
6697
+ uptimeMs: Date.now() - startTime
6186
6698
  },
6187
6699
  checks: checks.length > 0 ? checks : void 0
6188
6700
  };
6189
6701
  this.healthReports.set(pluginName, report);
6190
6702
  }
6191
6703
  /**
6192
- * Attempt to restart a plugin
6704
+ * Handle one failed round — the single path BOTH failure routes take.
6705
+ *
6706
+ * `performHealthCheck` can fail two disjoint ways: the check *returns* a
6707
+ * failure (`false` or `{ status: 'unhealthy' }`), or it *throws* — which by
6708
+ * `raceCheckTimeout` includes every `timeout` overrun, the severest case of
6709
+ * the two. The routes used to be handled in separate blocks, and only the
6710
+ * returned one cleared `successCounters`, so the counters a declared
6711
+ * `failureThreshold` / `successThreshold` are counted with depended on which
6712
+ * way the round happened to fail (#11852).
6713
+ *
6714
+ * What stays route-specific is the *status label*, deliberately. A throw is
6715
+ * the separate `failed` status applied immediately with no threshold — that
6716
+ * is the documented contract (`content/docs/protocol/kernel/lifecycle.mdx`,
6717
+ * "Custom Health Checks") and is pinned by the timeout test. Only the
6718
+ * counters are shared, because that is what `failureThreshold` declares, and
6719
+ * it does not name a route.
6720
+ *
6721
+ * This round ENDS here. Nothing is done TO the plugin — see the #12032 note
6722
+ * on the class: a monitor that cannot re-initialise a plugin has no business
6723
+ * destroying one.
6193
6724
  */
6194
- async attemptRestart(pluginName, plugin, config) {
6195
- const attempts = this.restartAttempts.get(pluginName) || 0;
6196
- if (attempts >= config.maxRestartAttempts) {
6197
- this.logger.error("Max restart attempts reached, giving up", {
6198
- plugin: pluginName,
6199
- attempts
6200
- });
6725
+ recordFailedRound(pluginName, config, route) {
6726
+ const failureCount = (this.failureCounters.get(pluginName) || 0) + 1;
6727
+ this.failureCounters.set(pluginName, failureCount);
6728
+ this.successCounters.set(pluginName, 0);
6729
+ const thresholdReached = failureCount >= config.failureThreshold;
6730
+ if (route === "thrown") {
6201
6731
  this.healthStatus.set(pluginName, "failed");
6202
- return;
6203
- }
6204
- this.restartAttempts.set(pluginName, attempts + 1);
6205
- const delay = this.calculateBackoff(attempts, config.restartBackoff);
6206
- this.logger.info("Scheduling plugin restart", {
6207
- plugin: pluginName,
6208
- attempt: attempts + 1,
6209
- delay
6210
- });
6211
- await new Promise((resolve) => setTimeout(resolve, delay));
6212
- try {
6213
- if (plugin.destroy) {
6214
- await plugin.destroy();
6215
- }
6216
- this.logger.info("Plugin restarted", { plugin: pluginName });
6217
- this.failureCounters.set(pluginName, 0);
6218
- this.successCounters.set(pluginName, 0);
6219
- this.healthStatus.set(pluginName, "recovering");
6220
- } catch (error) {
6221
- this.logger.error("Plugin restart failed", {
6732
+ } else if (thresholdReached) {
6733
+ this.healthStatus.set(pluginName, "unhealthy");
6734
+ this.logger.warn("Plugin marked as unhealthy", {
6222
6735
  plugin: pluginName,
6223
- error
6736
+ failures: failureCount
6224
6737
  });
6225
- this.healthStatus.set(pluginName, "failed");
6226
- }
6227
- }
6228
- /**
6229
- * Calculate backoff delay for restarts
6230
- */
6231
- calculateBackoff(attempt, strategy) {
6232
- const baseDelay = 1e3;
6233
- switch (strategy) {
6234
- case "fixed":
6235
- return baseDelay;
6236
- case "linear":
6237
- return baseDelay * (attempt + 1);
6238
- case "exponential":
6239
- return baseDelay * Math.pow(2, attempt);
6240
- default:
6241
- return baseDelay;
6738
+ } else {
6739
+ this.healthStatus.set(pluginName, "degraded");
6242
6740
  }
6243
6741
  }
6244
6742
  /**
@@ -6271,7 +6769,6 @@ var PluginHealthMonitor = class {
6271
6769
  this.healthReports.clear();
6272
6770
  this.failureCounters.clear();
6273
6771
  this.successCounters.clear();
6274
- this.restartAttempts.clear();
6275
6772
  this.logger.info("Health monitor shutdown complete");
6276
6773
  }
6277
6774
  /**
@@ -6312,7 +6809,7 @@ var PluginHealthMonitor = class {
6312
6809
  };
6313
6810
 
6314
6811
  // src/hot-reload.ts
6315
- var import_node_crypto4 = require("crypto");
6812
+ var import_node_crypto5 = require("crypto");
6316
6813
  var generateUUID = () => {
6317
6814
  if (typeof crypto !== "undefined" && crypto.randomUUID) {
6318
6815
  return crypto.randomUUID();
@@ -6323,6 +6820,42 @@ var generateUUID = () => {
6323
6820
  return v.toString(16);
6324
6821
  });
6325
6822
  };
6823
+ var HONOURED_STATE_STRATEGIES = ["memory", "none"];
6824
+ var RETIRED_STATE_STRATEGY_GUIDANCE = "'disk' and 'distributed' were removed from HotReloadConfig.stateStrategy in @objectstack/spec 18 (ADR-0049 enforce-or-remove) \u2014 neither was ever implemented. Both wrote to the same in-memory Map as 'memory' and reported it only at debug level, so a host that asked for durable or cluster-replicated state got process-local memory and no error. Use 'memory' for in-process state preservation across a reload, or 'none' to disable it. There is no in-tree replacement for durable or distributed plugin state \u2014 persist it in the host, which owns the process lifetime these strategies pretended to outlive.";
6825
+ function hotReloadRefusal(message) {
6826
+ const err = new Error(message);
6827
+ err.code = "VALIDATION_ERROR";
6828
+ err.status = 400;
6829
+ return err;
6830
+ }
6831
+ function assertHonouredStateStrategy(pluginName, strategy) {
6832
+ if (HONOURED_STATE_STRATEGIES.includes(strategy)) {
6833
+ return;
6834
+ }
6835
+ const shown = typeof strategy === "string" ? `'${strategy}'` : String(strategy);
6836
+ const retired = strategy === "disk" || strategy === "distributed";
6837
+ throw hotReloadRefusal(
6838
+ `[HotReload] Plugin '${pluginName}': unsupported stateStrategy ${shown}. Honoured values are ${HONOURED_STATE_STRATEGIES.map((v) => `'${v}'`).join(" and ")}. ` + (retired ? RETIRED_STATE_STRATEGY_GUIDANCE : "This value has never been implemented by PluginStateManager.")
6839
+ );
6840
+ }
6841
+ var RETIRED_HOT_RELOAD_KEYS = [
6842
+ [
6843
+ "distributedConfig",
6844
+ "'distributedConfig' was removed from HotReloadConfig in @objectstack/spec 18 (ADR-0049 enforce-or-remove) \u2014 nothing ever read it. A provider, endpoints, a key prefix, a TTL and a replication factor could all be declared and no connection was ever opened. It left with the stateStrategy: 'distributed' value it was documented as being required for. Delete the key; there is no in-tree replacement for distributed plugin state \u2014 persist it in the host."
6845
+ ],
6846
+ [
6847
+ "watchPatterns",
6848
+ "'watchPatterns' was removed from HotReloadConfig in @objectstack/spec 18 (ADR-0049 enforce-or-remove) \u2014 nothing ever read it. Its only two uses were log lines: no watcher was ever constructed from it, so an author could declare a glob and no file change ever triggered a reload. File watching is the HOST's job in this host-driven library. Delete the key, declare your globs wherever your own watcher reads them, and call `HotReloadManager.scheduleReload(pluginName, reloadFn)` when one matches \u2014 that is the debounced integration point this class does implement."
6849
+ ]
6850
+ ];
6851
+ function assertNoRetiredKeys2(pluginName, config) {
6852
+ for (const [key, guidance] of RETIRED_HOT_RELOAD_KEYS) {
6853
+ if (!Object.prototype.hasOwnProperty.call(config, key)) {
6854
+ continue;
6855
+ }
6856
+ throw hotReloadRefusal(`[HotReload] Plugin '${pluginName}': ${guidance}`);
6857
+ }
6858
+ }
6326
6859
  var PluginStateManager = class {
6327
6860
  constructor(logger) {
6328
6861
  this.stateSnapshots = /* @__PURE__ */ new Map();
@@ -6349,17 +6882,6 @@ var PluginStateManager = class {
6349
6882
  this.memoryStore.set(snapshotId, snapshot);
6350
6883
  this.logger.debug("State saved to memory", { pluginId, snapshotId });
6351
6884
  break;
6352
- case "disk":
6353
- this.memoryStore.set(snapshotId, snapshot);
6354
- this.logger.debug("State saved to disk (memory fallback)", { pluginId, snapshotId });
6355
- break;
6356
- case "distributed":
6357
- this.memoryStore.set(snapshotId, snapshot);
6358
- this.logger.debug("State saved to distributed store (memory fallback)", {
6359
- pluginId,
6360
- snapshotId
6361
- });
6362
- break;
6363
6885
  case "none":
6364
6886
  this.logger.debug("State persistence disabled", { pluginId });
6365
6887
  break;
@@ -6407,7 +6929,7 @@ var PluginStateManager = class {
6407
6929
  */
6408
6930
  calculateChecksum(state) {
6409
6931
  const stateStr = JSON.stringify(state);
6410
- return (0, import_node_crypto4.createHash)("sha256").update(stateStr).digest("hex");
6932
+ return (0, import_node_crypto5.createHash)("sha256").update(stateStr).digest("hex");
6411
6933
  }
6412
6934
  /**
6413
6935
  * Shutdown state manager
@@ -6421,7 +6943,6 @@ var PluginStateManager = class {
6421
6943
  var HotReloadManager = class {
6422
6944
  constructor(logger) {
6423
6945
  this.reloadConfigs = /* @__PURE__ */ new Map();
6424
- this.watchHandles = /* @__PURE__ */ new Map();
6425
6946
  this.reloadTimers = /* @__PURE__ */ new Map();
6426
6947
  this.logger = logger.child({ component: "HotReload" });
6427
6948
  this.stateManager = new PluginStateManager(logger);
@@ -6430,6 +6951,8 @@ var HotReloadManager = class {
6430
6951
  * Register a plugin for hot reload
6431
6952
  */
6432
6953
  registerPlugin(pluginName, config) {
6954
+ assertHonouredStateStrategy(pluginName, config.stateStrategy);
6955
+ assertNoRetiredKeys2(pluginName, config);
6433
6956
  if (!config.enabled) {
6434
6957
  this.logger.debug("Hot reload disabled for plugin", { plugin: pluginName });
6435
6958
  return;
@@ -6437,32 +6960,45 @@ var HotReloadManager = class {
6437
6960
  this.reloadConfigs.set(pluginName, config);
6438
6961
  this.logger.info("Plugin registered for hot reload", {
6439
6962
  plugin: pluginName,
6440
- watchPatterns: config.watchPatterns,
6441
6963
  stateStrategy: config.stateStrategy
6442
6964
  });
6443
6965
  }
6444
6966
  /**
6445
- * Start watching for changes (requires file system integration)
6967
+ * Refuse the file-watching call this class never implemented (#12428).
6968
+ *
6969
+ * The body used to be a guard plus `logger.info('File watching started')`
6970
+ * over an in-source note saying real watching "would require chokidar or
6971
+ * similar". Nothing was ever watched, so an operator who set
6972
+ * `enabled: true` and read that line at INFO had been told the opposite of
6973
+ * the truth — positive confirmation of a capability that did not exist.
6974
+ * ADR-0049 leaves three states and this surface qualified for none of the
6975
+ * other two: no runtime composes this class, so ENFORCE would build for a
6976
+ * caller that does not exist, and no roadmap entry anywhere claims the
6977
+ * feature, so EXPERIMENTAL would be a promise nobody made.
6978
+ *
6979
+ * Kept as a throwing door rather than deleted: removing the method leaves a
6980
+ * JavaScript host a bare `TypeError: not a function` with no prescription,
6981
+ * and this is the one place a caller of the old placeholder is guaranteed
6982
+ * to arrive. The refusal carries an ADR-0112 envelope so it can be asserted
6983
+ * rather than merely caught.
6446
6984
  */
6447
6985
  startWatching(pluginName) {
6448
- const config = this.reloadConfigs.get(pluginName);
6449
- if (!config || !config.enabled) {
6450
- return;
6451
- }
6452
- this.logger.info("File watching started", {
6453
- plugin: pluginName,
6454
- patterns: config.watchPatterns
6455
- });
6986
+ throw hotReloadRefusal(
6987
+ `[HotReload] Plugin '${pluginName}': startWatching() never watched anything and was removed in @objectstack/core 18 (ADR-0049 enforce-or-remove). It logged 'File watching started' at info level while no watcher was ever constructed, so no file change could ever trigger a reload. File watching is the HOST's job in this host-driven library: run your own watcher and call \`HotReloadManager.scheduleReload(pluginName, reloadFn)\` when a file changes \u2014 that is the debounced integration point this class does implement. \`HotReloadConfig.watchPatterns\` was removed in @objectstack/spec 18 for the same reason; declare your globs where your watcher reads them.`
6988
+ );
6456
6989
  }
6457
6990
  /**
6458
- * Stop watching for changes
6991
+ * Cancel a pending debounced reload for a plugin.
6992
+ *
6993
+ * The name is historical (#12428). This never stopped a watcher, because
6994
+ * nothing in this class ever started one: its `watchHandles` cleanup branch
6995
+ * read a Map that had no writer anywhere in the tree, so the branch was
6996
+ * structurally unreachable rather than merely untaken, and it left with
6997
+ * `startWatching`'s placeholder. What survives is the half that always did
6998
+ * something — the debounce timer armed by `scheduleReload` is cleared, so a
6999
+ * reload that was scheduled but has not fired yet is cancelled.
6459
7000
  */
6460
7001
  stopWatching(pluginName) {
6461
- const handle = this.watchHandles.get(pluginName);
6462
- if (handle) {
6463
- this.watchHandles.delete(pluginName);
6464
- this.logger.info("File watching stopped", { plugin: pluginName });
6465
- }
6466
7002
  const timer = this.reloadTimers.get(pluginName);
6467
7003
  if (timer) {
6468
7004
  clearTimeout(timer);
@@ -6605,14 +7141,10 @@ var HotReloadManager = class {
6605
7141
  * Shutdown hot reload manager
6606
7142
  */
6607
7143
  shutdown() {
6608
- for (const pluginName of this.watchHandles.keys()) {
6609
- this.stopWatching(pluginName);
6610
- }
6611
7144
  for (const timer of this.reloadTimers.values()) {
6612
7145
  clearTimeout(timer);
6613
7146
  }
6614
7147
  this.reloadConfigs.clear();
6615
- this.watchHandles.clear();
6616
7148
  this.reloadTimers.clear();
6617
7149
  this.stateManager.shutdown();
6618
7150
  this.logger.info("Hot reload manager shutdown complete");
@@ -7022,7 +7554,9 @@ var NamespaceResolver = class {
7022
7554
  var import_contracts = require("@objectstack/spec/contracts");
7023
7555
  // Annotate the CommonJS export names for ESM import in node:
7024
7556
  0 && (module.exports = {
7557
+ ADMIN_STANDING_NON_TABLE_INPUTS,
7025
7558
  ADMIN_STANDING_SURFACE,
7559
+ ADVISORY_SAMPLE_ROWS,
7026
7560
  ANONYMOUS_DENY_BODY,
7027
7561
  ANONYMOUS_DENY_CODE,
7028
7562
  ANONYMOUS_DENY_MESSAGE,
@@ -7030,11 +7564,19 @@ var import_contracts = require("@objectstack/spec/contracts");
7030
7564
  API_KEY_PREFIX,
7031
7565
  AUDIENCE_BINDING_SUGGESTION_STATUSES,
7032
7566
  AUDIENCE_BINDING_SUGGESTION_STATUS_VALUES,
7567
+ AUTHZ_GRANTS_CACHE_TTL_ENV,
7568
+ AUTHZ_INVALIDATED_CHANNEL,
7569
+ AUTHZ_STORE_UNAVAILABLE_CODE,
7570
+ AUTHZ_STORE_UNAVAILABLE_MESSAGE,
7571
+ AUTHZ_STORE_UNAVAILABLE_STATUS,
7572
+ AuthzStoreUnavailableError,
7033
7573
  CORE_FALLBACK_FACTORIES,
7034
7574
  DependencyResolver,
7035
7575
  ENTRY_EXECUTION_CONTEXT_FIELDS,
7036
7576
  HotReloadManager,
7577
+ InMemoryMetadataActivationStore,
7037
7578
  LiteKernel,
7579
+ METADATA_ACTIVATION_TABLE,
7038
7580
  MigrationJournalRefusal,
7039
7581
  MigrationPlanRegistry,
7040
7582
  NamespaceResolver,
@@ -7042,18 +7584,19 @@ var import_contracts = require("@objectstack/spec/contracts");
7042
7584
  ObjectKernel,
7043
7585
  ObjectKernelBase,
7044
7586
  ObjectLogger,
7587
+ ObjectStoreMetadataActivationStore,
7588
+ PLATFORM_ADMIN_EMAIL_SEPARATOR,
7045
7589
  POSTURE_INJECTION_RULE,
7046
7590
  POSTURE_LADDER,
7047
7591
  POSTURE_RANK,
7048
- PluginConfigValidator,
7049
7592
  PluginHealthMonitor,
7050
7593
  PluginLoader,
7051
7594
  PluginPermissionEnforcer,
7052
7595
  PluginPermissionManager,
7053
7596
  PluginSandboxRuntime,
7054
- PluginSecurityScanner,
7055
7597
  PluginSignatureVerifier,
7056
7598
  QA,
7599
+ SERVICE_NOT_REGISTERED_CODE,
7057
7600
  SIGNATURE_ALG,
7058
7601
  SecurePluginContext,
7059
7602
  SemanticVersionManager,
@@ -7063,6 +7606,7 @@ var import_contracts = require("@objectstack/spec/contracts");
7063
7606
  UnresolvedFilterTokenError,
7064
7607
  adminStandingColumns,
7065
7608
  adminStandingTables,
7609
+ artifactPackageId,
7066
7610
  assembleExecutionContext,
7067
7611
  assembleExecutionContextOrGuest,
7068
7612
  assertInitServiceRequirements,
@@ -7081,7 +7625,6 @@ var import_contracts = require("@objectstack/spec/contracts");
7081
7625
  createMemoryJob,
7082
7626
  createMemoryMetadata,
7083
7627
  createMemoryQueue,
7084
- createPluginConfigValidator,
7085
7628
  createPluginPermissionEnforcer,
7086
7629
  deepMerge,
7087
7630
  defaultIsTransientError,
@@ -7093,48 +7636,70 @@ var import_contracts = require("@objectstack/spec/contracts");
7093
7636
  extractApiKey,
7094
7637
  filterTokenContextFrom,
7095
7638
  findInterruptedRuns,
7639
+ formatIntegrityViolation,
7096
7640
  generateApiKey,
7097
7641
  generateEd25519KeyPair,
7098
7642
  getEnv,
7099
7643
  getMemoryUsage,
7644
+ hasPlatformAdminStanding,
7100
7645
  hashApiKey,
7101
7646
  hashMigrationPlan,
7647
+ isAggregatingAdvisories,
7102
7648
  isAudienceBindingSuggestionStatus,
7103
7649
  isAuthGateAllowlisted,
7650
+ isAuthzStoreUnavailableError,
7651
+ isConfiguredPlatformAdminEmail,
7104
7652
  isExpired,
7105
7653
  isGrantActive,
7106
7654
  isGrantExpired,
7107
7655
  isNode,
7108
7656
  isRowActive,
7657
+ isServiceNotRegisteredError,
7109
7658
  isUninterpretableTemporalComparand,
7659
+ matchesConfiguredPlatformAdmin,
7110
7660
  nextUtcCalendarDay,
7111
7661
  normalizeAuthGate,
7662
+ normalizePlatformAdminEmail,
7112
7663
  omitInternalFieldsFromWriteResponse,
7664
+ parsePlatformAdminEmails,
7113
7665
  parseScopes,
7114
7666
  parseSignature,
7115
7667
  planChunks,
7116
7668
  postureVisibleRows,
7117
7669
  readAuthoredTranslationLayer,
7670
+ readAuthzGrantsCacheTtlMs,
7118
7671
  readRunJournal,
7672
+ recordAdvisoryHit,
7119
7673
  recordNotFoundError,
7674
+ reportAuthzCachePosture,
7675
+ reportLegacyPlatformAdminGrant,
7676
+ resetLegacyPlatformAdminGrantReport,
7677
+ resetPlatformAdminEmailMemo,
7120
7678
  resolveApiKeyAdmission,
7121
7679
  resolveApiKeyPrincipal,
7680
+ resolveArtifactPackageOrder,
7681
+ resolveAuthzCachePosture,
7122
7682
  resolveAuthzContext,
7123
7683
  resolveFilterToken,
7124
7684
  resolveFilterTokens,
7125
7685
  resolveLocale,
7126
7686
  resolveLocalizationContext,
7687
+ resolvePlatformAdminEmails,
7127
7688
  resolvePluginOrder,
7128
7689
  resolveUserAuthzGrants,
7129
7690
  resumeMigrationJournal,
7691
+ rethrowAuthzStoreUnavailable,
7130
7692
  runMigrationJournal,
7693
+ runWithAdvisoryAggregation,
7131
7694
  safeExit,
7695
+ setPlatformAdminConfigSink,
7132
7696
  shouldDenyAnonymous,
7133
7697
  signPayload,
7134
7698
  temporalComparandKind,
7135
7699
  unknownAudienceBindingSuggestionStatusMessage,
7136
7700
  utcInstantMs,
7137
7701
  validateInitServiceContract,
7702
+ verifyIntegrity,
7138
7703
  verifyPayload,
7139
7704
  verifyPlatformSignature,
7140
7705
  verifyPluginArtifact,