@objectstack/core 17.2.0 → 17.3.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,6 +30,7 @@ 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,
34
35
  ANONYMOUS_DENY_BODY: () => ANONYMOUS_DENY_BODY,
35
36
  ANONYMOUS_DENY_CODE: () => ANONYMOUS_DENY_CODE,
@@ -38,11 +39,19 @@ __export(index_exports, {
38
39
  API_KEY_PREFIX: () => API_KEY_PREFIX,
39
40
  AUDIENCE_BINDING_SUGGESTION_STATUSES: () => AUDIENCE_BINDING_SUGGESTION_STATUSES,
40
41
  AUDIENCE_BINDING_SUGGESTION_STATUS_VALUES: () => AUDIENCE_BINDING_SUGGESTION_STATUS_VALUES,
42
+ AUTHZ_GRANTS_CACHE_TTL_ENV: () => AUTHZ_GRANTS_CACHE_TTL_ENV,
43
+ AUTHZ_INVALIDATED_CHANNEL: () => AUTHZ_INVALIDATED_CHANNEL,
44
+ AUTHZ_STORE_UNAVAILABLE_CODE: () => AUTHZ_STORE_UNAVAILABLE_CODE,
45
+ AUTHZ_STORE_UNAVAILABLE_MESSAGE: () => AUTHZ_STORE_UNAVAILABLE_MESSAGE,
46
+ AUTHZ_STORE_UNAVAILABLE_STATUS: () => AUTHZ_STORE_UNAVAILABLE_STATUS,
47
+ AuthzStoreUnavailableError: () => AuthzStoreUnavailableError,
41
48
  CORE_FALLBACK_FACTORIES: () => CORE_FALLBACK_FACTORIES,
42
49
  DependencyResolver: () => DependencyResolver,
43
50
  ENTRY_EXECUTION_CONTEXT_FIELDS: () => ENTRY_EXECUTION_CONTEXT_FIELDS,
44
51
  HotReloadManager: () => HotReloadManager,
52
+ InMemoryMetadataActivationStore: () => InMemoryMetadataActivationStore,
45
53
  LiteKernel: () => LiteKernel,
54
+ METADATA_ACTIVATION_TABLE: () => METADATA_ACTIVATION_TABLE,
46
55
  MigrationJournalRefusal: () => MigrationJournalRefusal,
47
56
  MigrationPlanRegistry: () => MigrationPlanRegistry,
48
57
  NamespaceResolver: () => NamespaceResolver,
@@ -50,10 +59,11 @@ __export(index_exports, {
50
59
  ObjectKernel: () => ObjectKernel,
51
60
  ObjectKernelBase: () => ObjectKernelBase,
52
61
  ObjectLogger: () => ObjectLogger,
62
+ ObjectStoreMetadataActivationStore: () => ObjectStoreMetadataActivationStore,
63
+ PLATFORM_ADMIN_EMAIL_SEPARATOR: () => PLATFORM_ADMIN_EMAIL_SEPARATOR,
53
64
  POSTURE_INJECTION_RULE: () => POSTURE_INJECTION_RULE,
54
65
  POSTURE_LADDER: () => POSTURE_LADDER,
55
66
  POSTURE_RANK: () => POSTURE_RANK,
56
- PluginConfigValidator: () => PluginConfigValidator,
57
67
  PluginHealthMonitor: () => PluginHealthMonitor,
58
68
  PluginLoader: () => PluginLoader,
59
69
  PluginPermissionEnforcer: () => PluginPermissionEnforcer,
@@ -62,6 +72,7 @@ __export(index_exports, {
62
72
  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,67 @@ __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,
110
123
  isAudienceBindingSuggestionStatus: () => isAudienceBindingSuggestionStatus,
111
124
  isAuthGateAllowlisted: () => isAuthGateAllowlisted,
125
+ isAuthzStoreUnavailableError: () => isAuthzStoreUnavailableError,
126
+ isConfiguredPlatformAdminEmail: () => isConfiguredPlatformAdminEmail,
112
127
  isExpired: () => isExpired,
113
128
  isGrantActive: () => isGrantActive,
114
129
  isGrantExpired: () => isGrantExpired,
115
130
  isNode: () => isNode,
116
131
  isRowActive: () => isRowActive,
132
+ isServiceNotRegisteredError: () => isServiceNotRegisteredError,
117
133
  isUninterpretableTemporalComparand: () => isUninterpretableTemporalComparand,
134
+ matchesConfiguredPlatformAdmin: () => matchesConfiguredPlatformAdmin,
118
135
  nextUtcCalendarDay: () => import_data.nextUtcCalendarDay,
119
136
  normalizeAuthGate: () => normalizeAuthGate,
137
+ normalizePlatformAdminEmail: () => normalizePlatformAdminEmail,
120
138
  omitInternalFieldsFromWriteResponse: () => omitInternalFieldsFromWriteResponse,
139
+ parsePlatformAdminEmails: () => parsePlatformAdminEmails,
121
140
  parseScopes: () => parseScopes,
122
141
  parseSignature: () => parseSignature,
123
142
  planChunks: () => planChunks,
124
143
  postureVisibleRows: () => postureVisibleRows,
125
144
  readAuthoredTranslationLayer: () => readAuthoredTranslationLayer,
145
+ readAuthzGrantsCacheTtlMs: () => readAuthzGrantsCacheTtlMs,
126
146
  readRunJournal: () => readRunJournal,
127
147
  recordNotFoundError: () => recordNotFoundError,
148
+ reportAuthzCachePosture: () => reportAuthzCachePosture,
149
+ reportLegacyPlatformAdminGrant: () => reportLegacyPlatformAdminGrant,
150
+ resetLegacyPlatformAdminGrantReport: () => resetLegacyPlatformAdminGrantReport,
151
+ resetPlatformAdminEmailMemo: () => resetPlatformAdminEmailMemo,
128
152
  resolveApiKeyAdmission: () => resolveApiKeyAdmission,
129
153
  resolveApiKeyPrincipal: () => resolveApiKeyPrincipal,
154
+ resolveArtifactPackageOrder: () => resolveArtifactPackageOrder,
155
+ resolveAuthzCachePosture: () => resolveAuthzCachePosture,
130
156
  resolveAuthzContext: () => resolveAuthzContext,
131
157
  resolveFilterToken: () => resolveFilterToken,
132
158
  resolveFilterTokens: () => resolveFilterTokens,
133
159
  resolveLocale: () => resolveLocale,
134
160
  resolveLocalizationContext: () => resolveLocalizationContext,
161
+ resolvePlatformAdminEmails: () => resolvePlatformAdminEmails,
135
162
  resolvePluginOrder: () => resolvePluginOrder,
136
163
  resolveUserAuthzGrants: () => resolveUserAuthzGrants,
137
164
  resumeMigrationJournal: () => resumeMigrationJournal,
165
+ rethrowAuthzStoreUnavailable: () => rethrowAuthzStoreUnavailable,
138
166
  runMigrationJournal: () => runMigrationJournal,
139
167
  safeExit: () => safeExit,
168
+ setPlatformAdminConfigSink: () => setPlatformAdminConfigSink,
140
169
  shouldDenyAnonymous: () => shouldDenyAnonymous,
141
170
  signPayload: () => signPayload,
142
171
  temporalComparandKind: () => temporalComparandKind,
143
172
  unknownAudienceBindingSuggestionStatusMessage: () => unknownAudienceBindingSuggestionStatusMessage,
144
173
  utcInstantMs: () => import_data.utcInstantMs,
145
174
  validateInitServiceContract: () => validateInitServiceContract,
175
+ verifyIntegrity: () => verifyIntegrity,
146
176
  verifyPayload: () => verifyPayload,
147
177
  verifyPlatformSignature: () => verifyPlatformSignature,
148
178
  verifyPluginArtifact: () => verifyPluginArtifact,
@@ -858,138 +888,6 @@ function createLogger(config) {
858
888
  // src/kernel.ts
859
889
  var import_system3 = require("@objectstack/spec/system");
860
890
 
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
891
  // src/security/plugin-artifact-signature.ts
994
892
  var import_node_crypto = require("crypto");
995
893
  var SIGNATURE_ALG = "ed25519";
@@ -1086,6 +984,20 @@ async function verifyPluginArtifact(input, keys) {
1086
984
  return { ok: true, publisherVerified: publisher.verified, platformVerified };
1087
985
  }
1088
986
 
987
+ // src/service-not-registered.ts
988
+ var SERVICE_NOT_REGISTERED_CODE = "SERVICE_NOT_REGISTERED";
989
+ var SERVICE_NOT_REGISTERED_BRAND = "__objectstackServiceNotRegistered";
990
+ function serviceNotRegisteredError(name) {
991
+ const err = new Error(`Service '${name}' not found`);
992
+ err[SERVICE_NOT_REGISTERED_BRAND] = true;
993
+ err.code = SERVICE_NOT_REGISTERED_CODE;
994
+ err.serviceName = name;
995
+ return err;
996
+ }
997
+ function isServiceNotRegisteredError(err) {
998
+ return typeof err === "object" && err !== null && err[SERVICE_NOT_REGISTERED_BRAND] === true;
999
+ }
1000
+
1089
1001
  // src/plugin-loader.ts
1090
1002
  var ServiceLifecycle = /* @__PURE__ */ ((ServiceLifecycle2) => {
1091
1003
  ServiceLifecycle2["SINGLETON"] = "singleton";
@@ -1101,7 +1013,6 @@ var PluginLoader = class {
1101
1013
  this.scopedServices = /* @__PURE__ */ new Map();
1102
1014
  this.creating = /* @__PURE__ */ new Set();
1103
1015
  this.logger = logger;
1104
- this.configValidator = new PluginConfigValidator(logger);
1105
1016
  }
1106
1017
  /**
1107
1018
  * Set the plugin context for service factories
@@ -1128,9 +1039,6 @@ var PluginLoader = class {
1128
1039
  if (!versionCheck.compatible) {
1129
1040
  throw new Error(`Version incompatible: ${versionCheck.message}`);
1130
1041
  }
1131
- if (metadata.configSchema) {
1132
- this.validatePluginConfig(metadata);
1133
- }
1134
1042
  if (metadata.signature) {
1135
1043
  await this.verifyPluginSignature(metadata);
1136
1044
  }
@@ -1169,7 +1077,7 @@ var PluginLoader = class {
1169
1077
  if (!registration) {
1170
1078
  const instance = this.serviceInstances.get(name);
1171
1079
  if (!instance) {
1172
- throw new Error(`Service '${name}' not found`);
1080
+ throw serviceNotRegisteredError(name);
1173
1081
  }
1174
1082
  return instance;
1175
1083
  }
@@ -1329,16 +1237,6 @@ var PluginLoader = class {
1329
1237
  const semverRegex = /^\d+\.\d+\.\d+(-[a-zA-Z0-9.-]+)?(\+[a-zA-Z0-9.-]+)?$/;
1330
1238
  return semverRegex.test(version);
1331
1239
  }
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
1240
  async verifyPluginSignature(plugin) {
1343
1241
  if (!plugin.signature) {
1344
1242
  return;
@@ -1628,10 +1526,10 @@ function createMemoryI18n() {
1628
1526
  }
1629
1527
 
1630
1528
  // src/metadata-service-contract.ts
1631
- var import_shared = require("@objectstack/spec/shared");
1529
+ var import_meta_spelling = require("@objectstack/spec/meta-spelling");
1632
1530
  var REGISTER_REFUSAL_CODE = "VALIDATION_ERROR";
1633
1531
  function canonicalMetadataServiceType(type) {
1634
- return (0, import_shared.pluralToSingular)(type);
1532
+ return (0, import_meta_spelling.pluralToSingular)(type);
1635
1533
  }
1636
1534
  function registerRefusal(message) {
1637
1535
  const err = new Error(message);
@@ -2512,6 +2410,67 @@ var ObjectKernel = class {
2512
2410
  }
2513
2411
  };
2514
2412
 
2413
+ // src/artifact-packages.ts
2414
+ var import_spec = require("@objectstack/spec");
2415
+ var MAX_REPORTED_ENTRY_ISSUES = 5;
2416
+ function refuse(code, message) {
2417
+ const err = new Error(message);
2418
+ err.code = code;
2419
+ err.status = 422;
2420
+ return err;
2421
+ }
2422
+ function artifactPackageId(manifest) {
2423
+ const id = manifest?.id || manifest?.name;
2424
+ return typeof id === "string" && id !== "" ? id : void 0;
2425
+ }
2426
+ function resolveArtifactPackageOrder(artifact) {
2427
+ const declared = artifact?.packages;
2428
+ if (declared === void 0 || declared === null) return [artifact];
2429
+ if (!Array.isArray(declared)) {
2430
+ throw refuse(
2431
+ "INVALID_ARTIFACT_PACKAGES",
2432
+ `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.`
2433
+ );
2434
+ }
2435
+ const nodes = /* @__PURE__ */ new Map();
2436
+ declared.forEach((entry, index) => {
2437
+ const verdict = import_spec.ArtifactPackageSchema.safeParse(entry);
2438
+ if (!verdict.success) {
2439
+ const issues = verdict.error.issues;
2440
+ const shown = issues.slice(0, MAX_REPORTED_ENTRY_ISSUES);
2441
+ throw refuse(
2442
+ "INVALID_ARTIFACT_PACKAGE_ENTRY",
2443
+ `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."
2444
+ );
2445
+ }
2446
+ const manifest = entry.manifest;
2447
+ const id = artifactPackageId(manifest);
2448
+ if (id === void 0) {
2449
+ throw refuse(
2450
+ "INVALID_ARTIFACT_PACKAGE_ENTRY",
2451
+ `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.`
2452
+ );
2453
+ }
2454
+ if (nodes.has(id)) {
2455
+ throw refuse(
2456
+ "DUPLICATE_ARTIFACT_PACKAGE",
2457
+ `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.`
2458
+ );
2459
+ }
2460
+ nodes.set(id, {
2461
+ // `name` is what `resolvePluginOrder` puts in its diagnostics; the MAP KEY
2462
+ // is what its edges resolve against. Both are the package id, so an error
2463
+ // it raises names the same string the artifact author wrote.
2464
+ name: id,
2465
+ optionalDependencies: Object.keys(
2466
+ manifest.dependencies ?? {}
2467
+ ),
2468
+ manifest
2469
+ });
2470
+ });
2471
+ return resolvePluginOrder(nodes).map((node) => node.manifest);
2472
+ }
2473
+
2515
2474
  // src/lite-kernel.ts
2516
2475
  var LiteKernel = class extends ObjectKernelBase {
2517
2476
  constructor(config) {
@@ -3241,6 +3200,55 @@ var PluginSignatureVerifier = class {
3241
3200
  }
3242
3201
  };
3243
3202
 
3203
+ // src/security/plugin-artifact-integrity.ts
3204
+ var import_node_crypto2 = require("crypto");
3205
+ var SRI_ALGORITHMS = /* @__PURE__ */ new Set(["sha256", "sha384", "sha512"]);
3206
+ function sriDigestFor(declared, data) {
3207
+ const dash = declared.indexOf("-");
3208
+ const alg = dash > 0 && SRI_ALGORITHMS.has(declared.slice(0, dash)) ? declared.slice(0, dash) : "sha256";
3209
+ return `${alg}-${(0, import_node_crypto2.createHash)(alg).update(data).digest("base64")}`;
3210
+ }
3211
+ function verifyIntegrity(files, integrity, options = {}) {
3212
+ if (integrity === null || integrity === void 0) {
3213
+ return { ok: true, skipped: true, checked: 0, violations: [] };
3214
+ }
3215
+ const exempt = new Set(options.exempt ?? []);
3216
+ const byPath = /* @__PURE__ */ new Map();
3217
+ for (const f of files) {
3218
+ if (!exempt.has(f.path)) byPath.set(f.path, f.data);
3219
+ }
3220
+ const violations = [];
3221
+ let checked = 0;
3222
+ for (const [path, declaredRaw] of Object.entries(integrity)) {
3223
+ if (exempt.has(path)) continue;
3224
+ const declared = typeof declaredRaw === "string" ? declaredRaw : String(declaredRaw);
3225
+ const data = byPath.get(path);
3226
+ if (data === void 0) {
3227
+ violations.push({ kind: "missing_file", path, declared });
3228
+ continue;
3229
+ }
3230
+ checked++;
3231
+ const actual = sriDigestFor(declared, data);
3232
+ if (actual !== declared) violations.push({ kind: "digest_mismatch", path, declared, actual });
3233
+ }
3234
+ for (const path of [...byPath.keys()].sort()) {
3235
+ if (!Object.prototype.hasOwnProperty.call(integrity, path)) {
3236
+ violations.push({ kind: "extra_file", path });
3237
+ }
3238
+ }
3239
+ return { ok: violations.length === 0, skipped: false, checked, violations };
3240
+ }
3241
+ function formatIntegrityViolation(v) {
3242
+ switch (v.kind) {
3243
+ case "digest_mismatch":
3244
+ return `${v.path}: digest mismatch \u2014 manifest declares ${v.declared}, artifact bytes hash to ${v.actual}`;
3245
+ case "missing_file":
3246
+ return `${v.path}: declared in the integrity map but absent from the artifact`;
3247
+ case "extra_file":
3248
+ return `${v.path}: present in the artifact but not in the integrity map`;
3249
+ }
3250
+ }
3251
+
3244
3252
  // src/security/plugin-permission-enforcer.ts
3245
3253
  var PluginPermissionEnforcer = class {
3246
3254
  constructor(logger) {
@@ -4335,16 +4343,16 @@ var PluginSecurityScanner = class {
4335
4343
  };
4336
4344
 
4337
4345
  // src/security/api-key.ts
4338
- var import_node_crypto2 = require("crypto");
4346
+ var import_node_crypto3 = require("crypto");
4339
4347
  var import_security = require("@objectstack/spec/security");
4340
4348
  var API_KEY_PREFIX = "osk_";
4341
4349
  var API_KEY_ENTROPY_BYTES = 32;
4342
4350
  var VISIBLE_PREFIX_LEN = 12;
4343
4351
  function hashApiKey(raw) {
4344
- return (0, import_node_crypto2.createHash)("sha256").update(raw, "utf8").digest("hex");
4352
+ return (0, import_node_crypto3.createHash)("sha256").update(raw, "utf8").digest("hex");
4345
4353
  }
4346
4354
  function generateApiKey(prefix = API_KEY_PREFIX) {
4347
- const secret = (0, import_node_crypto2.randomBytes)(API_KEY_ENTROPY_BYTES).toString("base64url");
4355
+ const secret = (0, import_node_crypto3.randomBytes)(API_KEY_ENTROPY_BYTES).toString("base64url");
4348
4356
  const raw = `${prefix}${secret}`;
4349
4357
  return {
4350
4358
  raw,
@@ -4458,8 +4466,37 @@ function safeJsonParse(s, fallback) {
4458
4466
  }
4459
4467
  }
4460
4468
 
4469
+ // src/security/authz-store-unavailable.ts
4470
+ var AUTHZ_STORE_UNAVAILABLE_STATUS = 503;
4471
+ var AUTHZ_STORE_UNAVAILABLE_CODE = "SERVICE_UNAVAILABLE";
4472
+ 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.";
4473
+ var AUTHZ_STORE_UNAVAILABLE_BRAND = "__objectstackAuthzStoreUnavailable";
4474
+ var _a, _b;
4475
+ var AuthzStoreUnavailableError = class extends (_b = Error, _a = AUTHZ_STORE_UNAVAILABLE_BRAND, _b) {
4476
+ constructor(object, cause) {
4477
+ super(`${AUTHZ_STORE_UNAVAILABLE_MESSAGE} (failed read: \`${object}\`)`);
4478
+ /** Brand — see the module doc on why this is not `instanceof`. */
4479
+ this[_a] = true;
4480
+ /** ADR-0112 wire code. */
4481
+ this.code = AUTHZ_STORE_UNAVAILABLE_CODE;
4482
+ /** HTTP status a transport should answer. */
4483
+ this.status = AUTHZ_STORE_UNAVAILABLE_STATUS;
4484
+ this.name = "AuthzStoreUnavailableError";
4485
+ this.object = object;
4486
+ if (cause !== void 0) this.cause = cause;
4487
+ }
4488
+ };
4489
+ function isAuthzStoreUnavailableError(err) {
4490
+ return typeof err === "object" && err !== null && err[AUTHZ_STORE_UNAVAILABLE_BRAND] === true;
4491
+ }
4492
+ function rethrowAuthzStoreUnavailable(err) {
4493
+ if (isAuthzStoreUnavailableError(err)) throw err;
4494
+ return void 0;
4495
+ }
4496
+
4461
4497
  // src/security/resolve-authz-context.ts
4462
- var import_spec = require("@objectstack/spec");
4498
+ var import_types2 = require("@objectstack/types");
4499
+ var import_spec2 = require("@objectstack/spec");
4463
4500
  var import_security2 = require("@objectstack/spec/security");
4464
4501
 
4465
4502
  // src/security/grant-validity.ts
@@ -4480,6 +4517,20 @@ function isGrantActive(row, nowMs) {
4480
4517
  if (until !== void 0 && !(nowMs < until)) return false;
4481
4518
  return true;
4482
4519
  }
4520
+ function nextGrantValidityBoundary(rows, nowMs) {
4521
+ let next;
4522
+ for (const row of rows) {
4523
+ if (!row) continue;
4524
+ const from = toEpochMs(row.valid_from ?? row.validFrom);
4525
+ const until = toEpochMs(row.valid_until ?? row.validUntil);
4526
+ for (const bound of [from, until]) {
4527
+ if (bound !== void 0 && bound > nowMs && (next === void 0 || bound < next)) {
4528
+ next = bound;
4529
+ }
4530
+ }
4531
+ }
4532
+ return next;
4533
+ }
4483
4534
  function isGrantExpired(row, nowMs) {
4484
4535
  if (!row) return false;
4485
4536
  const until = toEpochMs(row.valid_until ?? row.validUntil);
@@ -4487,6 +4538,227 @@ function isGrantExpired(row, nowMs) {
4487
4538
  return !(nowMs < until);
4488
4539
  }
4489
4540
 
4541
+ // src/security/authz-cache-posture.ts
4542
+ var AUTHZ_GRANTS_CACHE_TTL_ENV = "OS_AUTHZ_GRANTS_CACHE_TTL_MS";
4543
+ function resolveAuthzCachePosture(input) {
4544
+ const { ttlMs, bus, driver } = input;
4545
+ if (!(ttlMs > 0)) {
4546
+ return { posture: "disabled", loud: false, message: "" };
4547
+ }
4548
+ if (bus === "bridged") {
4549
+ return {
4550
+ posture: "bus-narrowed",
4551
+ loud: false,
4552
+ 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)."
4553
+ };
4554
+ }
4555
+ 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";
4556
+ return {
4557
+ posture: "ttl-only",
4558
+ loud: true,
4559
+ 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.`
4560
+ };
4561
+ }
4562
+ function readAuthzGrantsCacheTtlMs(env = typeof process !== "undefined" ? process.env : {}) {
4563
+ const raw = env[AUTHZ_GRANTS_CACHE_TTL_ENV];
4564
+ if (raw === void 0 || raw.trim() === "") {
4565
+ return { ttlMs: 0, malformed: false };
4566
+ }
4567
+ const parsed = Number(raw.trim());
4568
+ if (!Number.isFinite(parsed) || parsed < 0) {
4569
+ return { ttlMs: 0, raw, malformed: true };
4570
+ }
4571
+ return { ttlMs: Math.floor(parsed), raw, malformed: false };
4572
+ }
4573
+ function reportAuthzCachePosture(input, sink2) {
4574
+ if (input.malformedTtl) {
4575
+ sink2.warn(
4576
+ `[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.`
4577
+ );
4578
+ }
4579
+ const statement = resolveAuthzCachePosture(input);
4580
+ if (statement.posture === "disabled") return statement;
4581
+ if (statement.loud) sink2.warn(statement.message);
4582
+ else sink2.info?.(statement.message);
4583
+ return statement;
4584
+ }
4585
+
4586
+ // src/security/resolve-user-grants-cache.ts
4587
+ var GRANTS_CACHE_WATCHED_OBJECTS = /* @__PURE__ */ new Set([
4588
+ "sys_member",
4589
+ "sys_user_position",
4590
+ "sys_user_permission_set",
4591
+ "sys_position",
4592
+ "sys_position_permission_set",
4593
+ "sys_permission_set",
4594
+ "sys_user"
4595
+ ]);
4596
+ var grantsCacheStates = /* @__PURE__ */ new WeakMap();
4597
+ var WRITE_OPERATIONS = /* @__PURE__ */ new Set(["insert", "update", "delete"]);
4598
+ function grantsCacheState(ql) {
4599
+ const existing = grantsCacheStates.get(ql);
4600
+ if (existing !== void 0) return existing ?? void 0;
4601
+ const seamQl = ql;
4602
+ const epoch = seamQl.writeEpoch;
4603
+ const hasEpoch = !!epoch && typeof epoch === "object" && typeof epoch.current === "number" && typeof epoch.bump === "function" && typeof epoch.subscribe === "function";
4604
+ if (!hasEpoch || typeof seamQl.registerMiddleware !== "function") {
4605
+ return void 0;
4606
+ }
4607
+ const state = { gen: 0, entries: /* @__PURE__ */ new Map() };
4608
+ try {
4609
+ seamQl.registerMiddleware(async (ctx, next) => {
4610
+ if (typeof ctx?.operation !== "string" || !WRITE_OPERATIONS.has(ctx.operation) || typeof ctx?.object !== "string" || !GRANTS_CACHE_WATCHED_OBJECTS.has(ctx.object)) {
4611
+ return next();
4612
+ }
4613
+ try {
4614
+ await next();
4615
+ } finally {
4616
+ state.gen += 1;
4617
+ }
4618
+ });
4619
+ epoch.subscribe((_epoch, reason) => {
4620
+ if (reason !== "write") state.gen += 1;
4621
+ });
4622
+ } catch {
4623
+ grantsCacheStates.set(ql, null);
4624
+ return void 0;
4625
+ }
4626
+ grantsCacheStates.set(ql, state);
4627
+ return state;
4628
+ }
4629
+ function grantsCacheKey(userId, opts) {
4630
+ return JSON.stringify([
4631
+ userId,
4632
+ opts.tenantId ?? null,
4633
+ opts.seedEmail ?? null,
4634
+ Array.isArray(opts.seedPermissions) ? opts.seedPermissions : []
4635
+ ]);
4636
+ }
4637
+ var cloneGrants = (grants) => structuredClone(grants);
4638
+ function openUserGrantsCache(ql, userId, opts) {
4639
+ if (opts.bypassGrantsCache) return void 0;
4640
+ const { ttlMs } = readAuthzGrantsCacheTtlMs();
4641
+ if (ttlMs <= 0) return void 0;
4642
+ if (!ql || typeof ql !== "object" || typeof ql.find !== "function") {
4643
+ return void 0;
4644
+ }
4645
+ const state = grantsCacheState(ql);
4646
+ if (!state) return void 0;
4647
+ const key = grantsCacheKey(userId, opts);
4648
+ const now = opts.nowMs ?? Date.now();
4649
+ const genAtOpen = state.gen;
4650
+ const existing = state.entries.get(key);
4651
+ if (existing) {
4652
+ if (existing.gen === state.gen && existing.expiresAt > now) {
4653
+ return { hit: cloneGrants(existing.value), commit: () => {
4654
+ } };
4655
+ }
4656
+ state.entries.delete(key);
4657
+ }
4658
+ return {
4659
+ commit(grants, nextBoundaryMs) {
4660
+ const expiresAt = Math.min(now + ttlMs, nextBoundaryMs ?? Number.POSITIVE_INFINITY);
4661
+ if (expiresAt <= now) return;
4662
+ state.entries.set(key, { value: cloneGrants(grants), gen: genAtOpen, expiresAt });
4663
+ }
4664
+ };
4665
+ }
4666
+
4667
+ // src/security/platform-admin.ts
4668
+ var import_types = require("@objectstack/types");
4669
+ var PLATFORM_ADMIN_EMAIL_SEPARATOR = ",";
4670
+ function normalizePlatformAdminEmail(value) {
4671
+ return typeof value === "string" ? value.trim().toLowerCase() : "";
4672
+ }
4673
+ function isParseableAddress(entry) {
4674
+ if (/\s/.test(entry)) return false;
4675
+ const at = entry.indexOf("@");
4676
+ if (at <= 0) return false;
4677
+ if (entry.indexOf("@", at + 1) !== -1) return false;
4678
+ return at < entry.length - 1;
4679
+ }
4680
+ var EMPTY_CONFIG = Object.freeze({
4681
+ emails: Object.freeze([]),
4682
+ declaredSpellings: Object.freeze([])
4683
+ });
4684
+ function parsePlatformAdminEmails(raw) {
4685
+ if (raw == null) return EMPTY_CONFIG;
4686
+ const text = String(raw);
4687
+ if (text.trim() === "") return EMPTY_CONFIG;
4688
+ const emails = [];
4689
+ const declaredSpellings = [];
4690
+ for (const piece of text.split(PLATFORM_ADMIN_EMAIL_SEPARATOR)) {
4691
+ const entry = normalizePlatformAdminEmail(piece);
4692
+ if (entry === "") continue;
4693
+ if (!isParseableAddress(entry)) {
4694
+ return {
4695
+ emails: Object.freeze([]),
4696
+ declaredSpellings: Object.freeze([]),
4697
+ raw: text,
4698
+ 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.`
4699
+ };
4700
+ }
4701
+ if (!emails.includes(entry)) {
4702
+ emails.push(entry);
4703
+ declaredSpellings.push(piece.trim());
4704
+ }
4705
+ }
4706
+ return {
4707
+ emails: Object.freeze(emails),
4708
+ declaredSpellings: Object.freeze(declaredSpellings),
4709
+ raw: text
4710
+ };
4711
+ }
4712
+ var defaultSink = {
4713
+ error: (m) => console.error(m),
4714
+ warn: (m) => console.warn(m)
4715
+ };
4716
+ var sink = defaultSink;
4717
+ function setPlatformAdminConfigSink(next) {
4718
+ const prev = sink;
4719
+ sink = next ?? defaultSink;
4720
+ return prev;
4721
+ }
4722
+ var NOT_MEMOIZED = /* @__PURE__ */ Symbol("platform-admin-config-not-memoized");
4723
+ var memoKey = NOT_MEMOIZED;
4724
+ var memoValue = EMPTY_CONFIG;
4725
+ function resolvePlatformAdminEmails() {
4726
+ const raw = (0, import_types.resolvePlatformOwnerEmail)();
4727
+ if (memoKey !== NOT_MEMOIZED && memoKey === raw) return memoValue;
4728
+ const parsed = parsePlatformAdminEmails(raw);
4729
+ memoKey = raw;
4730
+ memoValue = parsed;
4731
+ if (parsed.refusal) sink.error(`[authz] ${parsed.refusal}`);
4732
+ return parsed;
4733
+ }
4734
+ function resetPlatformAdminEmailMemo() {
4735
+ memoKey = NOT_MEMOIZED;
4736
+ memoValue = EMPTY_CONFIG;
4737
+ }
4738
+ function matchesConfiguredPlatformAdmin(row, config) {
4739
+ if (config.emails.length === 0) return false;
4740
+ if (!row || typeof row !== "object") return false;
4741
+ if (!isConfiguredPlatformAdminEmail(row.email, config)) return false;
4742
+ return (0, import_types.isEmailVerifiedUserRow)(row);
4743
+ }
4744
+ function isConfiguredPlatformAdminEmail(email, config) {
4745
+ if (config.emails.length === 0) return false;
4746
+ const candidate = normalizePlatformAdminEmail(email);
4747
+ return candidate !== "" && config.emails.includes(candidate);
4748
+ }
4749
+ var legacyGrantPointerSaid = false;
4750
+ function reportLegacyPlatformAdminGrant(input) {
4751
+ if (legacyGrantPointerSaid) return;
4752
+ legacyGrantPointerSaid = true;
4753
+ const email = normalizePlatformAdminEmail(input.email);
4754
+ sink.warn(
4755
+ `[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.`
4756
+ );
4757
+ }
4758
+ function resetLegacyPlatformAdminGrantReport() {
4759
+ legacyGrantPointerSaid = false;
4760
+ }
4761
+
4490
4762
  // src/security/posture-ladder.ts
4491
4763
  var POSTURE_LADDER = [
4492
4764
  "PLATFORM_ADMIN",
@@ -4567,8 +4839,9 @@ async function tryFind(ql, object, where, limit = 100, organizationId) {
4567
4839
  let rows = await ql.find(object, { where, limit, context });
4568
4840
  if (rows && rows.value) rows = rows.value;
4569
4841
  return Array.isArray(rows) ? rows : [];
4570
- } catch {
4571
- return [];
4842
+ } catch (err) {
4843
+ if ((0, import_types2.isMissingTableError)(err, object)) return [];
4844
+ throw new AuthzStoreUnavailableError(object, err);
4572
4845
  }
4573
4846
  }
4574
4847
  async function resolveAuthzContext(input) {
@@ -4642,6 +4915,8 @@ async function resolveAuthzContext(input) {
4642
4915
  return ctx;
4643
4916
  }
4644
4917
  async function resolveUserAuthzGrants(ql, userId, opts = {}) {
4918
+ const grantsCache = openUserGrantsCache(ql, userId, opts);
4919
+ if (grantsCache?.hit) return grantsCache.hit;
4645
4920
  const { tenantId } = opts;
4646
4921
  const grants = {
4647
4922
  positions: [],
@@ -4662,7 +4937,8 @@ async function resolveUserAuthzGrants(ql, userId, opts = {}) {
4662
4937
  }
4663
4938
  return userRow;
4664
4939
  };
4665
- const needsUserRow = !grants.email || !grants.permissions.includes("ai_seat");
4940
+ const platformAdminConfig = resolvePlatformAdminEmails();
4941
+ const needsUserRow = !grants.email || !grants.permissions.includes("ai_seat") || platformAdminConfig.emails.length > 0;
4666
4942
  const [, members, userPositionRows, orgMembersLeg, upsRowsAll] = await Promise.all([
4667
4943
  needsUserRow ? getUserRow() : Promise.resolve(void 0),
4668
4944
  tryFind(ql, "sys_member", { user_id: userId }, 200),
@@ -4688,7 +4964,7 @@ async function resolveUserAuthzGrants(ql, userId, opts = {}) {
4688
4964
  for (const m of activeMembers) {
4689
4965
  if (m.role && typeof m.role === "string") {
4690
4966
  for (const raw of m.role.split(",").map((s) => s.trim()).filter(Boolean)) {
4691
- const r = (0, import_spec.mapMembershipRole)(raw);
4967
+ const r = (0, import_spec2.mapMembershipRole)(raw);
4692
4968
  if (!grants.positions.includes(r)) grants.positions.push(r);
4693
4969
  }
4694
4970
  }
@@ -4744,7 +5020,7 @@ async function resolveUserAuthzGrants(ql, userId, opts = {}) {
4744
5020
  const mergedTabs = {};
4745
5021
  for (const ps of psRows) {
4746
5022
  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;
5023
+ if (ps.name === import_spec2.ADMIN_FULL_ACCESS && unscopedUserPsIds.has(ps.id)) hasPlatformAdminGrant = true;
4748
5024
  const sysPerms = typeof ps.system_permissions === "string" ? safeJsonParse2(ps.system_permissions, []) : ps.system_permissions ?? ps.systemPermissions;
4749
5025
  if (Array.isArray(sysPerms)) {
4750
5026
  for (const p of sysPerms) {
@@ -4764,21 +5040,46 @@ async function resolveUserAuthzGrants(ql, userId, opts = {}) {
4764
5040
  }
4765
5041
  if (Object.keys(mergedTabs).length > 0) grants.tabPermissions = mergedTabs;
4766
5042
  }
4767
- if (hasPlatformAdminGrant && !grants.positions.includes(import_spec.BUILTIN_IDENTITY_PLATFORM_ADMIN)) {
4768
- grants.positions.unshift(import_spec.BUILTIN_IDENTITY_PLATFORM_ADMIN);
5043
+ const configConfersPlatformAdmin = platformAdminConfig.emails.length > 0 && matchesConfiguredPlatformAdmin(await getUserRow(), platformAdminConfig);
5044
+ if (configConfersPlatformAdmin) {
5045
+ hasPlatformAdminGrant = true;
5046
+ if (!grants.permissions.includes(import_spec2.ADMIN_FULL_ACCESS)) grants.permissions.push(import_spec2.ADMIN_FULL_ACCESS);
5047
+ for (const p of import_spec2.ADMIN_FULL_ACCESS_CAPABILITIES.systemPermissions ?? []) {
5048
+ if (!grants.systemPermissions.includes(p)) grants.systemPermissions.push(p);
5049
+ }
5050
+ } else if (hasPlatformAdminGrant) {
5051
+ if ((0, import_security2.postureEnforcesWall)((0, import_types2.resolveTenancyPosture)())) {
5052
+ reportLegacyPlatformAdminGrant({ userId, email: userRow?.email });
5053
+ }
5054
+ }
5055
+ if (hasPlatformAdminGrant && !grants.positions.includes(import_spec2.BUILTIN_IDENTITY_PLATFORM_ADMIN)) {
5056
+ grants.positions.unshift(import_spec2.BUILTIN_IDENTITY_PLATFORM_ADMIN);
4769
5057
  }
4770
5058
  grants.posture = derivePosture({
4771
5059
  isPlatformAdmin: hasPlatformAdminGrant,
4772
5060
  // [ADR-0105 D4] Either org-admin capability set resolves the rung — the
4773
5061
  // wall-less variant differs only by withholding the superuser bits.
4774
- isTenantAdmin: import_spec.ORGANIZATION_ADMIN_GRANTS.some((n) => grants.permissions.includes(n))
5062
+ isTenantAdmin: import_spec2.ORGANIZATION_ADMIN_GRANTS.some((n) => grants.permissions.includes(n))
4775
5063
  });
4776
5064
  if (!grants.permissions.includes("ai_seat")) {
4777
5065
  const aiAccess = (await getUserRow())?.ai_access;
4778
5066
  if (aiAccess === true || aiAccess === 1 || aiAccess === "1") grants.permissions.push("ai_seat");
4779
5067
  }
5068
+ grantsCache?.commit(
5069
+ grants,
5070
+ nextGrantValidityBoundary([...members, ...userPositionRows, ...upsRowsAll], nowMs)
5071
+ );
4780
5072
  return grants;
4781
5073
  }
5074
+ async function hasPlatformAdminStanding(ql, userId, opts = {}) {
5075
+ if (!ql || typeof userId !== "string" || userId.length === 0) return false;
5076
+ try {
5077
+ const grants = await resolveUserAuthzGrants(ql, userId, { nowMs: opts.nowMs });
5078
+ return grants.posture === "PLATFORM_ADMIN";
5079
+ } catch {
5080
+ return false;
5081
+ }
5082
+ }
4782
5083
  function isValidTimeZone(tz) {
4783
5084
  try {
4784
5085
  new Intl.DateTimeFormat("en-US", { timeZone: tz });
@@ -4800,25 +5101,98 @@ function coerceCurrency(value) {
4800
5101
  return /^[A-Z]{3}$/.test(s) ? s : void 0;
4801
5102
  }
4802
5103
  var LOCALIZATION_FAILURE_CACHE_TTL_MS = 3e4;
4803
- var localizationFailureCache = /* @__PURE__ */ new WeakMap();
5104
+ var LOCALIZATION_CACHE_TTL_ENV = "OS_LOCALIZATION_CACHE_TTL_MS";
5105
+ var LOCALIZATION_SUCCESS_CACHE_DEFAULT_TTL_MS = 3e4;
5106
+ function localizationSuccessCacheTtlMs(env = typeof process !== "undefined" ? process.env : {}) {
5107
+ const raw = env[LOCALIZATION_CACHE_TTL_ENV];
5108
+ if (raw === void 0 || raw.trim() === "") return LOCALIZATION_SUCCESS_CACHE_DEFAULT_TTL_MS;
5109
+ const parsed = Number(raw.trim());
5110
+ if (!Number.isFinite(parsed) || parsed < 0) return 0;
5111
+ return Math.floor(parsed);
5112
+ }
5113
+ function readWriteEpoch(ql) {
5114
+ if (!ql || typeof ql !== "object") return void 0;
5115
+ const epoch = ql.writeEpoch;
5116
+ if (!epoch || typeof epoch !== "object") return void 0;
5117
+ const seam = epoch;
5118
+ if (typeof seam.current !== "number" || typeof seam.bump !== "function" || typeof seam.subscribe !== "function") {
5119
+ return void 0;
5120
+ }
5121
+ return seam.current;
5122
+ }
5123
+ var localizationSettingsStates = /* @__PURE__ */ new WeakMap();
5124
+ var localizationNoSettingsState = { gen: 0 };
5125
+ function localizationSettingsState(settings) {
5126
+ if (!settings || typeof settings !== "object") return localizationNoSettingsState;
5127
+ const existing = localizationSettingsStates.get(settings);
5128
+ if (existing) return existing;
5129
+ const state = { gen: 0 };
5130
+ localizationSettingsStates.set(settings, state);
5131
+ const subscribe = settings.subscribe;
5132
+ if (typeof subscribe === "function") {
5133
+ try {
5134
+ subscribe.call(
5135
+ settings,
5136
+ "localization",
5137
+ () => {
5138
+ state.gen += 1;
5139
+ }
5140
+ );
5141
+ } catch {
5142
+ }
5143
+ }
5144
+ return state;
5145
+ }
5146
+ var localizationCache = /* @__PURE__ */ new WeakMap();
5147
+ function localizationEntryIsLive(entry, epoch, settings) {
5148
+ if (entry.kind === "failure") return true;
5149
+ return entry.epoch === epoch && entry.settings === settings && entry.settingsGen === settings.gen;
5150
+ }
5151
+ function putLocalizationEntry(ql, key, entry) {
5152
+ const bucket = localizationCache.get(ql) ?? /* @__PURE__ */ new Map();
5153
+ bucket.set(key, entry);
5154
+ localizationCache.set(ql, bucket);
5155
+ }
4804
5156
  async function resolveLocalizationContext(input) {
4805
- const { ql, tenantId, userId } = input;
5157
+ const { ql, settings, tenantId, userId } = input;
4806
5158
  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);
5159
+ const cacheable = Boolean(ql) && typeof ql === "object";
5160
+ const epoch = cacheable ? readWriteEpoch(ql) : void 0;
5161
+ const settingsState = localizationSettingsState(settings);
5162
+ if (cacheable) {
5163
+ const hit = localizationCache.get(ql)?.get(cacheKey);
5164
+ if (hit && hit.expiresAt > Date.now() && localizationEntryIsLive(hit, epoch, settingsState)) {
5165
+ return hit.value;
5166
+ }
5167
+ }
5168
+ const { value, backendFailed } = await resolveLocalizationContextUncached(input);
5169
+ if (!cacheable) return value;
5170
+ if (backendFailed) {
5171
+ putLocalizationEntry(ql, cacheKey, {
5172
+ value,
5173
+ expiresAt: Date.now() + LOCALIZATION_FAILURE_CACHE_TTL_MS,
5174
+ kind: "failure"
5175
+ });
5176
+ return value;
5177
+ }
5178
+ const ttlMs = localizationSuccessCacheTtlMs();
5179
+ if (epoch !== void 0 && ttlMs > 0) {
5180
+ putLocalizationEntry(ql, cacheKey, {
5181
+ value,
5182
+ expiresAt: Date.now() + ttlMs,
5183
+ kind: "success",
5184
+ epoch,
5185
+ settings: settingsState,
5186
+ settingsGen: settingsState.gen
5187
+ });
5188
+ } else {
5189
+ localizationCache.get(ql)?.delete(cacheKey);
4816
5190
  }
4817
5191
  return value;
4818
5192
  }
4819
5193
  async function resolveLocalizationContextUncached(input) {
4820
5194
  const { ql, settings, tenantId, userId } = input;
4821
- let failed = false;
5195
+ let backendFailed = false;
4822
5196
  try {
4823
5197
  if (settings && typeof settings.get === "function") {
4824
5198
  const sctx = { tenantId, userId };
@@ -4832,33 +5206,22 @@ async function resolveLocalizationContextUncached(input) {
4832
5206
  localeRes = many.locale;
4833
5207
  currencyRes = many.currency;
4834
5208
  } catch {
4835
- failed = true;
4836
5209
  }
4837
5210
  } else {
4838
5211
  [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
- })
5212
+ settings.get("localization", "timezone", sctx).catch(() => void 0),
5213
+ settings.get("localization", "locale", sctx).catch(() => void 0),
5214
+ settings.get("localization", "currency", sctx).catch(() => void 0)
4851
5215
  ]);
4852
5216
  }
4853
5217
  const tz = coerceTimeZone(tzRes?.value);
4854
5218
  const locale = coerceLocale(localeRes?.value);
4855
5219
  const currency = coerceCurrency(currencyRes?.value);
4856
5220
  if (tz || locale || currency) {
4857
- return { value: { timezone: tz ?? "UTC", locale: locale ?? "en-US", currency }, failed: false };
5221
+ return { value: { timezone: tz ?? "UTC", locale: locale ?? "en-US", currency }, backendFailed: false };
4858
5222
  }
4859
5223
  }
4860
5224
  } catch {
4861
- failed = true;
4862
5225
  }
4863
5226
  let rows = [];
4864
5227
  if (ql && typeof ql.find === "function") {
@@ -4871,7 +5234,7 @@ async function resolveLocalizationContextUncached(input) {
4871
5234
  if (result && result.value) result = result.value;
4872
5235
  rows = Array.isArray(result) ? result : [];
4873
5236
  } catch {
4874
- failed = true;
5237
+ backendFailed = true;
4875
5238
  }
4876
5239
  }
4877
5240
  const valueOf = (k) => rows.find((r) => r.key === k)?.value;
@@ -4881,7 +5244,7 @@ async function resolveLocalizationContextUncached(input) {
4881
5244
  locale: coerceLocale(valueOf("locale")) ?? "en-US",
4882
5245
  currency: coerceCurrency(valueOf("currency"))
4883
5246
  },
4884
- failed
5247
+ backendFailed
4885
5248
  };
4886
5249
  }
4887
5250
 
@@ -5043,7 +5406,7 @@ function shouldDenyAnonymous(input) {
5043
5406
  var ADMIN_STANDING_SURFACE = {
5044
5407
  sys_permission_set: {
5045
5408
  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.",
5409
+ 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
5410
  columns: [
5048
5411
  "id",
5049
5412
  "name",
@@ -5085,8 +5448,14 @@ var ADMIN_STANDING_SURFACE = {
5085
5448
  ]
5086
5449
  },
5087
5450
  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."
5451
+ role: "derives",
5452
+ 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.",
5453
+ columns: [
5454
+ "id",
5455
+ "email",
5456
+ "email_verified",
5457
+ "ai_access"
5458
+ ]
5090
5459
  },
5091
5460
  sys_user_position: {
5092
5461
  role: "reads-only",
@@ -5101,6 +5470,13 @@ var ADMIN_STANDING_SURFACE = {
5101
5470
  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
5471
  }
5103
5472
  };
5473
+ var ADMIN_STANDING_NON_TABLE_INPUTS = [
5474
+ {
5475
+ kind: "env",
5476
+ name: "OS_PLATFORM_OWNER_EMAIL",
5477
+ 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."
5478
+ }
5479
+ ];
5104
5480
  function adminStandingTables() {
5105
5481
  return Object.entries(ADMIN_STANDING_SURFACE).filter(([, t]) => t.role === "derives").map(([name]) => name).sort();
5106
5482
  }
@@ -5132,6 +5508,9 @@ function withoutOperationPrivateKeys(exec) {
5132
5508
  return out;
5133
5509
  }
5134
5510
 
5511
+ // src/security/authz-invalidation-channel.ts
5512
+ var AUTHZ_INVALIDATED_CHANNEL = "authz.invalidated";
5513
+
5135
5514
  // src/utils/datetime.ts
5136
5515
  var import_data = require("@objectstack/spec/data");
5137
5516
  function calendarPartsInTz(d, tz) {
@@ -5412,7 +5791,7 @@ function omitInternalFieldsFromWriteResponse(schema, records) {
5412
5791
  }
5413
5792
 
5414
5793
  // src/utils/migration-journal.ts
5415
- var import_node_crypto3 = require("crypto");
5794
+ var import_node_crypto4 = require("crypto");
5416
5795
  var import_system4 = require("@objectstack/spec/system");
5417
5796
  var SYSTEM_CTX = { isSystem: true };
5418
5797
  var DEFAULT_CHUNK_SIZE = 200;
@@ -5467,7 +5846,7 @@ function hashMigrationPlan(plan, chunks) {
5467
5846
  steps: plan.steps.map((s) => s.name),
5468
5847
  chunks: chunks.map((c) => [c.stepIndex, c.offset, c.length])
5469
5848
  });
5470
- return (0, import_node_crypto3.createHash)("sha256").update(shape, "utf8").digest("hex").slice(0, 32);
5849
+ return (0, import_node_crypto4.createHash)("sha256").update(shape, "utf8").digest("hex").slice(0, 32);
5471
5850
  }
5472
5851
  async function appendEvent(engine, event, execContext) {
5473
5852
  await engine.insert(
@@ -5540,7 +5919,7 @@ async function runMigrationJournal(engine, plan, options = {}) {
5540
5919
  }
5541
5920
  const { chunks, planHash, rowsByStep } = await loadPlan(engine, plan, options.chunkSize);
5542
5921
  const resuming = Boolean(options.runId);
5543
- const runId = options.runId ?? (0, import_node_crypto3.randomUUID)();
5922
+ const runId = options.runId ?? (0, import_node_crypto4.randomUUID)();
5544
5923
  let events = [];
5545
5924
  let seq = 0;
5546
5925
  let committed = /* @__PURE__ */ new Set();
@@ -6032,6 +6411,110 @@ function isUninterpretableTemporalComparand(kind, value) {
6032
6411
  return !(readsAsWallClock(s) || readsAsInstant(s));
6033
6412
  }
6034
6413
 
6414
+ // src/utils/metadata-activation-store.ts
6415
+ var METADATA_ACTIVATION_TABLE = "sys_metadata_activation";
6416
+ var SYSTEM_CTX2 = { isSystem: true, positions: [], permissions: [] };
6417
+ var InMemoryMetadataActivationStore = class {
6418
+ constructor() {
6419
+ this.rows = /* @__PURE__ */ new Map();
6420
+ }
6421
+ async list() {
6422
+ return [...this.rows.values()];
6423
+ }
6424
+ async setActive(row) {
6425
+ this.rows.set(row.name, { ...row });
6426
+ }
6427
+ };
6428
+ var ObjectStoreMetadataActivationStore = class {
6429
+ constructor(engine, metadataType) {
6430
+ this.engine = engine;
6431
+ this.metadataType = metadataType;
6432
+ }
6433
+ /**
6434
+ * Every row of this type. Read once at boot to hydrate the consumer's
6435
+ * projection.
6436
+ *
6437
+ * The only scoping is the `metadata_type` discriminator: the ledger is
6438
+ * deployment-wide and has no tenant column, so there is no second axis to
6439
+ * filter on (see the module header).
6440
+ */
6441
+ async list() {
6442
+ const rows = await this.engine.find(METADATA_ACTIVATION_TABLE, {
6443
+ where: { metadata_type: this.metadataType },
6444
+ context: SYSTEM_CTX2
6445
+ });
6446
+ if (!Array.isArray(rows)) return [];
6447
+ const out = [];
6448
+ for (const row of rows) {
6449
+ const r = row;
6450
+ if (typeof r.name !== "string" || !r.name) continue;
6451
+ out.push({
6452
+ name: r.name,
6453
+ packageId: typeof r.package_id === "string" ? r.package_id : "",
6454
+ // The column defaults to `true`; only an explicit `false`
6455
+ // disarms. A driver that round-trips booleans as 0/1
6456
+ // (SQLite/libsql) is read through the same `=== false || === 0`
6457
+ // test, so a `0` is not mistaken for `true`.
6458
+ active: !(r.active === false || r.active === 0)
6459
+ });
6460
+ }
6461
+ return out;
6462
+ }
6463
+ /**
6464
+ * Insert or update the row for one packaged artifact.
6465
+ *
6466
+ * Read-then-write rather than a blind upsert because the object's
6467
+ * uniqueness is a DECLARED index (`unique: 'global'` over
6468
+ * `(metadata_type, name)`), not a primary key this store controls: there is
6469
+ * no id to collide on, so an insert-and-catch could not tell "already
6470
+ * there" from a real store failure.
6471
+ *
6472
+ * That index is also why taking the FIRST match is taking the only one: the
6473
+ * read below is keyed on exactly the index's two columns, so it can match
6474
+ * at most one row. It used to pick the first row with a NULL organization
6475
+ * out of the result, back when the table carried a reserved tenant column;
6476
+ * with no such column the set it was choosing from can no longer hold more
6477
+ * than one member.
6478
+ */
6479
+ async setActive(row) {
6480
+ const existing = await this.engine.find(METADATA_ACTIVATION_TABLE, {
6481
+ where: { metadata_type: this.metadataType, name: row.name },
6482
+ context: SYSTEM_CTX2
6483
+ });
6484
+ const current = Array.isArray(existing) ? existing[0] : void 0;
6485
+ if (current && current.id != null) {
6486
+ await this.engine.update(
6487
+ METADATA_ACTIVATION_TABLE,
6488
+ { id: current.id, active: row.active, package_id: row.packageId },
6489
+ { context: SYSTEM_CTX2 }
6490
+ );
6491
+ return;
6492
+ }
6493
+ await this.engine.insert(
6494
+ METADATA_ACTIVATION_TABLE,
6495
+ {
6496
+ metadata_type: this.metadataType,
6497
+ name: row.name,
6498
+ package_id: row.packageId,
6499
+ active: row.active
6500
+ },
6501
+ { context: SYSTEM_CTX2 }
6502
+ );
6503
+ }
6504
+ /**
6505
+ * Read the backing table once so a misconfiguration surfaces at BOOT
6506
+ * rather than as a failed toggle later. Throws the driver error verbatim —
6507
+ * `no such table: sys_metadata_activation` means the object was never
6508
+ * registered (or its schema never synced) in this composition.
6509
+ *
6510
+ * ⚠️ Unscoped by design: the question is "does the TABLE read at all",
6511
+ * which is a property of the composition, not of one `metadata_type`.
6512
+ */
6513
+ async probe() {
6514
+ await this.engine.find(METADATA_ACTIVATION_TABLE, { where: {}, limit: 1, context: SYSTEM_CTX2 });
6515
+ }
6516
+ };
6517
+
6035
6518
  // src/utils/record-not-found.ts
6036
6519
  function recordNotFoundError(object, id) {
6037
6520
  const err = new Error(`Record ${id} not found in ${object}`);
@@ -6042,6 +6525,45 @@ function recordNotFoundError(object, id) {
6042
6525
  }
6043
6526
 
6044
6527
  // src/health-monitor.ts
6528
+ var RECOVERY_IS_THRESHOLD_GATED = {
6529
+ degraded: true,
6530
+ unhealthy: true,
6531
+ failed: true,
6532
+ recovering: true,
6533
+ healthy: false,
6534
+ unknown: false
6535
+ };
6536
+ function healthMonitorRefusal(message) {
6537
+ const err = new Error(message);
6538
+ err.code = "VALIDATION_ERROR";
6539
+ err.status = 400;
6540
+ return err;
6541
+ }
6542
+ var RETIRED_HEALTH_CHECK_KEYS = [
6543
+ [
6544
+ "autoRestart",
6545
+ "'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."
6546
+ ],
6547
+ [
6548
+ "maxRestartAttempts",
6549
+ "'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."
6550
+ ],
6551
+ [
6552
+ "restartBackoff",
6553
+ "'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."
6554
+ ]
6555
+ ];
6556
+ 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.";
6557
+ function assertNoRetiredKeys(pluginName, config) {
6558
+ for (const [key, guidance] of RETIRED_HEALTH_CHECK_KEYS) {
6559
+ if (!Object.prototype.hasOwnProperty.call(config, key)) {
6560
+ continue;
6561
+ }
6562
+ throw healthMonitorRefusal(
6563
+ `[HealthMonitor] Plugin '${pluginName}': ${guidance}${RESTART_IS_THE_HOSTS_JOB}`
6564
+ );
6565
+ }
6566
+ }
6045
6567
  var PluginHealthMonitor = class {
6046
6568
  constructor(logger) {
6047
6569
  this.healthChecks = /* @__PURE__ */ new Map();
@@ -6050,18 +6572,17 @@ var PluginHealthMonitor = class {
6050
6572
  this.checkIntervals = /* @__PURE__ */ new Map();
6051
6573
  this.failureCounters = /* @__PURE__ */ new Map();
6052
6574
  this.successCounters = /* @__PURE__ */ new Map();
6053
- this.restartAttempts = /* @__PURE__ */ new Map();
6054
6575
  this.logger = logger.child({ component: "HealthMonitor" });
6055
6576
  }
6056
6577
  /**
6057
6578
  * Register a plugin for health monitoring
6058
6579
  */
6059
6580
  registerPlugin(pluginName, config) {
6581
+ assertNoRetiredKeys(pluginName, config);
6060
6582
  this.healthChecks.set(pluginName, config);
6061
6583
  this.healthStatus.set(pluginName, "unknown");
6062
6584
  this.failureCounters.set(pluginName, 0);
6063
6585
  this.successCounters.set(pluginName, 0);
6064
- this.restartAttempts.set(pluginName, 0);
6065
6586
  this.logger.info("Plugin registered for health monitoring", {
6066
6587
  plugin: pluginName,
6067
6588
  interval: config.interval
@@ -6113,6 +6634,7 @@ var PluginHealthMonitor = class {
6113
6634
  let status = "healthy";
6114
6635
  let message;
6115
6636
  const checks = [];
6637
+ let failureRoute;
6116
6638
  try {
6117
6639
  if (config.checkMethod && typeof plugin[config.checkMethod] === "function") {
6118
6640
  const checkResult = await this.raceCheckTimeout(
@@ -6133,8 +6655,8 @@ var PluginHealthMonitor = class {
6133
6655
  if (status === "healthy") {
6134
6656
  this.successCounters.set(pluginName, (this.successCounters.get(pluginName) || 0) + 1);
6135
6657
  this.failureCounters.set(pluginName, 0);
6136
- const currentStatus = this.healthStatus.get(pluginName);
6137
- if (currentStatus === "unhealthy" || currentStatus === "degraded") {
6658
+ const currentStatus = this.healthStatus.get(pluginName) ?? "unknown";
6659
+ if (RECOVERY_IS_THRESHOLD_GATED[currentStatus]) {
6138
6660
  const successCount = this.successCounters.get(pluginName) || 0;
6139
6661
  if (successCount >= config.successThreshold) {
6140
6662
  this.healthStatus.set(pluginName, "healthy");
@@ -6146,27 +6668,11 @@ var PluginHealthMonitor = class {
6146
6668
  this.healthStatus.set(pluginName, "healthy");
6147
6669
  }
6148
6670
  } 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
- }
6671
+ failureRoute = "returned";
6164
6672
  }
6165
6673
  } catch (error) {
6166
6674
  status = "failed";
6167
6675
  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
6676
  checks.push({
6171
6677
  name: "health-check",
6172
6678
  status: "failed",
@@ -6176,6 +6682,10 @@ var PluginHealthMonitor = class {
6176
6682
  plugin: pluginName,
6177
6683
  error
6178
6684
  });
6685
+ failureRoute = "thrown";
6686
+ }
6687
+ if (failureRoute) {
6688
+ this.recordFailedRound(pluginName, config, failureRoute);
6179
6689
  }
6180
6690
  const report = {
6181
6691
  status: this.healthStatus.get(pluginName) || "unknown",
@@ -6189,56 +6699,42 @@ var PluginHealthMonitor = class {
6189
6699
  this.healthReports.set(pluginName, report);
6190
6700
  }
6191
6701
  /**
6192
- * Attempt to restart a plugin
6702
+ * Handle one failed round — the single path BOTH failure routes take.
6703
+ *
6704
+ * `performHealthCheck` can fail two disjoint ways: the check *returns* a
6705
+ * failure (`false` or `{ status: 'unhealthy' }`), or it *throws* — which by
6706
+ * `raceCheckTimeout` includes every `timeout` overrun, the severest case of
6707
+ * the two. The routes used to be handled in separate blocks, and only the
6708
+ * returned one cleared `successCounters`, so the counters a declared
6709
+ * `failureThreshold` / `successThreshold` are counted with depended on which
6710
+ * way the round happened to fail (#11852).
6711
+ *
6712
+ * What stays route-specific is the *status label*, deliberately. A throw is
6713
+ * the separate `failed` status applied immediately with no threshold — that
6714
+ * is the documented contract (`content/docs/protocol/kernel/lifecycle.mdx`,
6715
+ * "Custom Health Checks") and is pinned by the timeout test. Only the
6716
+ * counters are shared, because that is what `failureThreshold` declares, and
6717
+ * it does not name a route.
6718
+ *
6719
+ * This round ENDS here. Nothing is done TO the plugin — see the #12032 note
6720
+ * on the class: a monitor that cannot re-initialise a plugin has no business
6721
+ * destroying one.
6193
6722
  */
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
- });
6723
+ recordFailedRound(pluginName, config, route) {
6724
+ const failureCount = (this.failureCounters.get(pluginName) || 0) + 1;
6725
+ this.failureCounters.set(pluginName, failureCount);
6726
+ this.successCounters.set(pluginName, 0);
6727
+ const thresholdReached = failureCount >= config.failureThreshold;
6728
+ if (route === "thrown") {
6201
6729
  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", {
6730
+ } else if (thresholdReached) {
6731
+ this.healthStatus.set(pluginName, "unhealthy");
6732
+ this.logger.warn("Plugin marked as unhealthy", {
6222
6733
  plugin: pluginName,
6223
- error
6734
+ failures: failureCount
6224
6735
  });
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;
6736
+ } else {
6737
+ this.healthStatus.set(pluginName, "degraded");
6242
6738
  }
6243
6739
  }
6244
6740
  /**
@@ -6271,7 +6767,6 @@ var PluginHealthMonitor = class {
6271
6767
  this.healthReports.clear();
6272
6768
  this.failureCounters.clear();
6273
6769
  this.successCounters.clear();
6274
- this.restartAttempts.clear();
6275
6770
  this.logger.info("Health monitor shutdown complete");
6276
6771
  }
6277
6772
  /**
@@ -6312,7 +6807,7 @@ var PluginHealthMonitor = class {
6312
6807
  };
6313
6808
 
6314
6809
  // src/hot-reload.ts
6315
- var import_node_crypto4 = require("crypto");
6810
+ var import_node_crypto5 = require("crypto");
6316
6811
  var generateUUID = () => {
6317
6812
  if (typeof crypto !== "undefined" && crypto.randomUUID) {
6318
6813
  return crypto.randomUUID();
@@ -6323,6 +6818,42 @@ var generateUUID = () => {
6323
6818
  return v.toString(16);
6324
6819
  });
6325
6820
  };
6821
+ var HONOURED_STATE_STRATEGIES = ["memory", "none"];
6822
+ 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.";
6823
+ function hotReloadRefusal(message) {
6824
+ const err = new Error(message);
6825
+ err.code = "VALIDATION_ERROR";
6826
+ err.status = 400;
6827
+ return err;
6828
+ }
6829
+ function assertHonouredStateStrategy(pluginName, strategy) {
6830
+ if (HONOURED_STATE_STRATEGIES.includes(strategy)) {
6831
+ return;
6832
+ }
6833
+ const shown = typeof strategy === "string" ? `'${strategy}'` : String(strategy);
6834
+ const retired = strategy === "disk" || strategy === "distributed";
6835
+ throw hotReloadRefusal(
6836
+ `[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.")
6837
+ );
6838
+ }
6839
+ var RETIRED_HOT_RELOAD_KEYS = [
6840
+ [
6841
+ "distributedConfig",
6842
+ "'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."
6843
+ ],
6844
+ [
6845
+ "watchPatterns",
6846
+ "'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."
6847
+ ]
6848
+ ];
6849
+ function assertNoRetiredKeys2(pluginName, config) {
6850
+ for (const [key, guidance] of RETIRED_HOT_RELOAD_KEYS) {
6851
+ if (!Object.prototype.hasOwnProperty.call(config, key)) {
6852
+ continue;
6853
+ }
6854
+ throw hotReloadRefusal(`[HotReload] Plugin '${pluginName}': ${guidance}`);
6855
+ }
6856
+ }
6326
6857
  var PluginStateManager = class {
6327
6858
  constructor(logger) {
6328
6859
  this.stateSnapshots = /* @__PURE__ */ new Map();
@@ -6349,17 +6880,6 @@ var PluginStateManager = class {
6349
6880
  this.memoryStore.set(snapshotId, snapshot);
6350
6881
  this.logger.debug("State saved to memory", { pluginId, snapshotId });
6351
6882
  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
6883
  case "none":
6364
6884
  this.logger.debug("State persistence disabled", { pluginId });
6365
6885
  break;
@@ -6407,7 +6927,7 @@ var PluginStateManager = class {
6407
6927
  */
6408
6928
  calculateChecksum(state) {
6409
6929
  const stateStr = JSON.stringify(state);
6410
- return (0, import_node_crypto4.createHash)("sha256").update(stateStr).digest("hex");
6930
+ return (0, import_node_crypto5.createHash)("sha256").update(stateStr).digest("hex");
6411
6931
  }
6412
6932
  /**
6413
6933
  * Shutdown state manager
@@ -6421,7 +6941,6 @@ var PluginStateManager = class {
6421
6941
  var HotReloadManager = class {
6422
6942
  constructor(logger) {
6423
6943
  this.reloadConfigs = /* @__PURE__ */ new Map();
6424
- this.watchHandles = /* @__PURE__ */ new Map();
6425
6944
  this.reloadTimers = /* @__PURE__ */ new Map();
6426
6945
  this.logger = logger.child({ component: "HotReload" });
6427
6946
  this.stateManager = new PluginStateManager(logger);
@@ -6430,6 +6949,8 @@ var HotReloadManager = class {
6430
6949
  * Register a plugin for hot reload
6431
6950
  */
6432
6951
  registerPlugin(pluginName, config) {
6952
+ assertHonouredStateStrategy(pluginName, config.stateStrategy);
6953
+ assertNoRetiredKeys2(pluginName, config);
6433
6954
  if (!config.enabled) {
6434
6955
  this.logger.debug("Hot reload disabled for plugin", { plugin: pluginName });
6435
6956
  return;
@@ -6437,32 +6958,45 @@ var HotReloadManager = class {
6437
6958
  this.reloadConfigs.set(pluginName, config);
6438
6959
  this.logger.info("Plugin registered for hot reload", {
6439
6960
  plugin: pluginName,
6440
- watchPatterns: config.watchPatterns,
6441
6961
  stateStrategy: config.stateStrategy
6442
6962
  });
6443
6963
  }
6444
6964
  /**
6445
- * Start watching for changes (requires file system integration)
6965
+ * Refuse the file-watching call this class never implemented (#12428).
6966
+ *
6967
+ * The body used to be a guard plus `logger.info('File watching started')`
6968
+ * over an in-source note saying real watching "would require chokidar or
6969
+ * similar". Nothing was ever watched, so an operator who set
6970
+ * `enabled: true` and read that line at INFO had been told the opposite of
6971
+ * the truth — positive confirmation of a capability that did not exist.
6972
+ * ADR-0049 leaves three states and this surface qualified for none of the
6973
+ * other two: no runtime composes this class, so ENFORCE would build for a
6974
+ * caller that does not exist, and no roadmap entry anywhere claims the
6975
+ * feature, so EXPERIMENTAL would be a promise nobody made.
6976
+ *
6977
+ * Kept as a throwing door rather than deleted: removing the method leaves a
6978
+ * JavaScript host a bare `TypeError: not a function` with no prescription,
6979
+ * and this is the one place a caller of the old placeholder is guaranteed
6980
+ * to arrive. The refusal carries an ADR-0112 envelope so it can be asserted
6981
+ * rather than merely caught.
6446
6982
  */
6447
6983
  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
- });
6984
+ throw hotReloadRefusal(
6985
+ `[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.`
6986
+ );
6456
6987
  }
6457
6988
  /**
6458
- * Stop watching for changes
6989
+ * Cancel a pending debounced reload for a plugin.
6990
+ *
6991
+ * The name is historical (#12428). This never stopped a watcher, because
6992
+ * nothing in this class ever started one: its `watchHandles` cleanup branch
6993
+ * read a Map that had no writer anywhere in the tree, so the branch was
6994
+ * structurally unreachable rather than merely untaken, and it left with
6995
+ * `startWatching`'s placeholder. What survives is the half that always did
6996
+ * something — the debounce timer armed by `scheduleReload` is cleared, so a
6997
+ * reload that was scheduled but has not fired yet is cancelled.
6459
6998
  */
6460
6999
  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
7000
  const timer = this.reloadTimers.get(pluginName);
6467
7001
  if (timer) {
6468
7002
  clearTimeout(timer);
@@ -6605,14 +7139,10 @@ var HotReloadManager = class {
6605
7139
  * Shutdown hot reload manager
6606
7140
  */
6607
7141
  shutdown() {
6608
- for (const pluginName of this.watchHandles.keys()) {
6609
- this.stopWatching(pluginName);
6610
- }
6611
7142
  for (const timer of this.reloadTimers.values()) {
6612
7143
  clearTimeout(timer);
6613
7144
  }
6614
7145
  this.reloadConfigs.clear();
6615
- this.watchHandles.clear();
6616
7146
  this.reloadTimers.clear();
6617
7147
  this.stateManager.shutdown();
6618
7148
  this.logger.info("Hot reload manager shutdown complete");
@@ -7022,6 +7552,7 @@ var NamespaceResolver = class {
7022
7552
  var import_contracts = require("@objectstack/spec/contracts");
7023
7553
  // Annotate the CommonJS export names for ESM import in node:
7024
7554
  0 && (module.exports = {
7555
+ ADMIN_STANDING_NON_TABLE_INPUTS,
7025
7556
  ADMIN_STANDING_SURFACE,
7026
7557
  ANONYMOUS_DENY_BODY,
7027
7558
  ANONYMOUS_DENY_CODE,
@@ -7030,11 +7561,19 @@ var import_contracts = require("@objectstack/spec/contracts");
7030
7561
  API_KEY_PREFIX,
7031
7562
  AUDIENCE_BINDING_SUGGESTION_STATUSES,
7032
7563
  AUDIENCE_BINDING_SUGGESTION_STATUS_VALUES,
7564
+ AUTHZ_GRANTS_CACHE_TTL_ENV,
7565
+ AUTHZ_INVALIDATED_CHANNEL,
7566
+ AUTHZ_STORE_UNAVAILABLE_CODE,
7567
+ AUTHZ_STORE_UNAVAILABLE_MESSAGE,
7568
+ AUTHZ_STORE_UNAVAILABLE_STATUS,
7569
+ AuthzStoreUnavailableError,
7033
7570
  CORE_FALLBACK_FACTORIES,
7034
7571
  DependencyResolver,
7035
7572
  ENTRY_EXECUTION_CONTEXT_FIELDS,
7036
7573
  HotReloadManager,
7574
+ InMemoryMetadataActivationStore,
7037
7575
  LiteKernel,
7576
+ METADATA_ACTIVATION_TABLE,
7038
7577
  MigrationJournalRefusal,
7039
7578
  MigrationPlanRegistry,
7040
7579
  NamespaceResolver,
@@ -7042,10 +7581,11 @@ var import_contracts = require("@objectstack/spec/contracts");
7042
7581
  ObjectKernel,
7043
7582
  ObjectKernelBase,
7044
7583
  ObjectLogger,
7584
+ ObjectStoreMetadataActivationStore,
7585
+ PLATFORM_ADMIN_EMAIL_SEPARATOR,
7045
7586
  POSTURE_INJECTION_RULE,
7046
7587
  POSTURE_LADDER,
7047
7588
  POSTURE_RANK,
7048
- PluginConfigValidator,
7049
7589
  PluginHealthMonitor,
7050
7590
  PluginLoader,
7051
7591
  PluginPermissionEnforcer,
@@ -7054,6 +7594,7 @@ var import_contracts = require("@objectstack/spec/contracts");
7054
7594
  PluginSecurityScanner,
7055
7595
  PluginSignatureVerifier,
7056
7596
  QA,
7597
+ SERVICE_NOT_REGISTERED_CODE,
7057
7598
  SIGNATURE_ALG,
7058
7599
  SecurePluginContext,
7059
7600
  SemanticVersionManager,
@@ -7063,6 +7604,7 @@ var import_contracts = require("@objectstack/spec/contracts");
7063
7604
  UnresolvedFilterTokenError,
7064
7605
  adminStandingColumns,
7065
7606
  adminStandingTables,
7607
+ artifactPackageId,
7066
7608
  assembleExecutionContext,
7067
7609
  assembleExecutionContextOrGuest,
7068
7610
  assertInitServiceRequirements,
@@ -7081,7 +7623,6 @@ var import_contracts = require("@objectstack/spec/contracts");
7081
7623
  createMemoryJob,
7082
7624
  createMemoryMetadata,
7083
7625
  createMemoryQueue,
7084
- createPluginConfigValidator,
7085
7626
  createPluginPermissionEnforcer,
7086
7627
  deepMerge,
7087
7628
  defaultIsTransientError,
@@ -7093,48 +7634,67 @@ var import_contracts = require("@objectstack/spec/contracts");
7093
7634
  extractApiKey,
7094
7635
  filterTokenContextFrom,
7095
7636
  findInterruptedRuns,
7637
+ formatIntegrityViolation,
7096
7638
  generateApiKey,
7097
7639
  generateEd25519KeyPair,
7098
7640
  getEnv,
7099
7641
  getMemoryUsage,
7642
+ hasPlatformAdminStanding,
7100
7643
  hashApiKey,
7101
7644
  hashMigrationPlan,
7102
7645
  isAudienceBindingSuggestionStatus,
7103
7646
  isAuthGateAllowlisted,
7647
+ isAuthzStoreUnavailableError,
7648
+ isConfiguredPlatformAdminEmail,
7104
7649
  isExpired,
7105
7650
  isGrantActive,
7106
7651
  isGrantExpired,
7107
7652
  isNode,
7108
7653
  isRowActive,
7654
+ isServiceNotRegisteredError,
7109
7655
  isUninterpretableTemporalComparand,
7656
+ matchesConfiguredPlatformAdmin,
7110
7657
  nextUtcCalendarDay,
7111
7658
  normalizeAuthGate,
7659
+ normalizePlatformAdminEmail,
7112
7660
  omitInternalFieldsFromWriteResponse,
7661
+ parsePlatformAdminEmails,
7113
7662
  parseScopes,
7114
7663
  parseSignature,
7115
7664
  planChunks,
7116
7665
  postureVisibleRows,
7117
7666
  readAuthoredTranslationLayer,
7667
+ readAuthzGrantsCacheTtlMs,
7118
7668
  readRunJournal,
7119
7669
  recordNotFoundError,
7670
+ reportAuthzCachePosture,
7671
+ reportLegacyPlatformAdminGrant,
7672
+ resetLegacyPlatformAdminGrantReport,
7673
+ resetPlatformAdminEmailMemo,
7120
7674
  resolveApiKeyAdmission,
7121
7675
  resolveApiKeyPrincipal,
7676
+ resolveArtifactPackageOrder,
7677
+ resolveAuthzCachePosture,
7122
7678
  resolveAuthzContext,
7123
7679
  resolveFilterToken,
7124
7680
  resolveFilterTokens,
7125
7681
  resolveLocale,
7126
7682
  resolveLocalizationContext,
7683
+ resolvePlatformAdminEmails,
7127
7684
  resolvePluginOrder,
7128
7685
  resolveUserAuthzGrants,
7129
7686
  resumeMigrationJournal,
7687
+ rethrowAuthzStoreUnavailable,
7130
7688
  runMigrationJournal,
7131
7689
  safeExit,
7690
+ setPlatformAdminConfigSink,
7132
7691
  shouldDenyAnonymous,
7133
7692
  signPayload,
7134
7693
  temporalComparandKind,
7135
7694
  unknownAudienceBindingSuggestionStatusMessage,
7136
7695
  utcInstantMs,
7137
7696
  validateInitServiceContract,
7697
+ verifyIntegrity,
7138
7698
  verifyPayload,
7139
7699
  verifyPlatformSignature,
7140
7700
  verifyPluginArtifact,