@objectstack/core 17.1.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/CHANGELOG.md +1646 -0
- package/dist/index.cjs +1002 -349
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1238 -124
- package/dist/index.d.ts +1238 -124
- package/dist/index.js +962 -338
- package/dist/index.js.map +1 -1
- package/package.json +9 -4
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
|
|
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;
|
|
@@ -1494,35 +1392,6 @@ function createMemoryQueue() {
|
|
|
1494
1392
|
};
|
|
1495
1393
|
}
|
|
1496
1394
|
|
|
1497
|
-
// src/fallbacks/memory-job.ts
|
|
1498
|
-
function createMemoryJob() {
|
|
1499
|
-
const jobs = /* @__PURE__ */ new Map();
|
|
1500
|
-
return {
|
|
1501
|
-
__serviceInfo: {
|
|
1502
|
-
status: "degraded",
|
|
1503
|
-
handlerReady: false,
|
|
1504
|
-
message: "In-process job registry \u2014 trigger() runs handlers, but scheduled jobs never fire on their own (no timer). Register a job plugin (e.g. Agenda) for real scheduling."
|
|
1505
|
-
},
|
|
1506
|
-
_serviceName: "job",
|
|
1507
|
-
async schedule(name, schedule, handler) {
|
|
1508
|
-
jobs.set(name, { schedule, handler });
|
|
1509
|
-
},
|
|
1510
|
-
async cancel(name) {
|
|
1511
|
-
jobs.delete(name);
|
|
1512
|
-
},
|
|
1513
|
-
async trigger(name, data) {
|
|
1514
|
-
const job = jobs.get(name);
|
|
1515
|
-
if (job?.handler) await job.handler({ jobId: name, data });
|
|
1516
|
-
},
|
|
1517
|
-
async getExecutions() {
|
|
1518
|
-
return [];
|
|
1519
|
-
},
|
|
1520
|
-
async listJobs() {
|
|
1521
|
-
return [...jobs.keys()];
|
|
1522
|
-
}
|
|
1523
|
-
};
|
|
1524
|
-
}
|
|
1525
|
-
|
|
1526
1395
|
// src/fallbacks/memory-i18n.ts
|
|
1527
1396
|
var import_system = require("@objectstack/spec/system");
|
|
1528
1397
|
function deepMerge(target, source) {
|
|
@@ -1657,10 +1526,10 @@ function createMemoryI18n() {
|
|
|
1657
1526
|
}
|
|
1658
1527
|
|
|
1659
1528
|
// src/metadata-service-contract.ts
|
|
1660
|
-
var
|
|
1529
|
+
var import_meta_spelling = require("@objectstack/spec/meta-spelling");
|
|
1661
1530
|
var REGISTER_REFUSAL_CODE = "VALIDATION_ERROR";
|
|
1662
1531
|
function canonicalMetadataServiceType(type) {
|
|
1663
|
-
return (0,
|
|
1532
|
+
return (0, import_meta_spelling.pluralToSingular)(type);
|
|
1664
1533
|
}
|
|
1665
1534
|
function registerRefusal(message) {
|
|
1666
1535
|
const err = new Error(message);
|
|
@@ -1750,6 +1619,35 @@ function createMemoryMetadata() {
|
|
|
1750
1619
|
};
|
|
1751
1620
|
}
|
|
1752
1621
|
|
|
1622
|
+
// src/fallbacks/memory-job.ts
|
|
1623
|
+
function createMemoryJob() {
|
|
1624
|
+
const jobs = /* @__PURE__ */ new Map();
|
|
1625
|
+
return {
|
|
1626
|
+
__serviceInfo: {
|
|
1627
|
+
status: "degraded",
|
|
1628
|
+
handlerReady: false,
|
|
1629
|
+
message: "In-process job registry \u2014 trigger() runs handlers, but scheduled jobs never fire on their own (no timer). Register a job plugin (e.g. Agenda) for real scheduling."
|
|
1630
|
+
},
|
|
1631
|
+
_serviceName: "job",
|
|
1632
|
+
async schedule(name, schedule, handler) {
|
|
1633
|
+
jobs.set(name, { schedule, handler });
|
|
1634
|
+
},
|
|
1635
|
+
async cancel(name) {
|
|
1636
|
+
jobs.delete(name);
|
|
1637
|
+
},
|
|
1638
|
+
async trigger(name, data) {
|
|
1639
|
+
const job = jobs.get(name);
|
|
1640
|
+
if (job?.handler) await job.handler({ jobId: name, data });
|
|
1641
|
+
},
|
|
1642
|
+
async getExecutions() {
|
|
1643
|
+
return [];
|
|
1644
|
+
},
|
|
1645
|
+
async listJobs() {
|
|
1646
|
+
return [...jobs.keys()];
|
|
1647
|
+
}
|
|
1648
|
+
};
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1753
1651
|
// src/fallbacks/authored-translation-sync.ts
|
|
1754
1652
|
var import_system2 = require("@objectstack/spec/system");
|
|
1755
1653
|
var OWNER_PROP = "__authoredTranslationSyncOwner";
|
|
@@ -1882,7 +1780,6 @@ var CORE_FALLBACK_FACTORIES = {
|
|
|
1882
1780
|
metadata: createMemoryMetadata,
|
|
1883
1781
|
cache: createMemoryCache,
|
|
1884
1782
|
queue: createMemoryQueue,
|
|
1885
|
-
job: createMemoryJob,
|
|
1886
1783
|
i18n: createMemoryI18n
|
|
1887
1784
|
};
|
|
1888
1785
|
|
|
@@ -1906,6 +1803,48 @@ function registerPluginByName(registry, plugin, logger) {
|
|
|
1906
1803
|
return previous;
|
|
1907
1804
|
}
|
|
1908
1805
|
|
|
1806
|
+
// src/timeout-guard.ts
|
|
1807
|
+
var TimeoutGuard = class {
|
|
1808
|
+
constructor(timeoutMs, createTimeoutError) {
|
|
1809
|
+
/**
|
|
1810
|
+
* Settles `expiry` without a value. `Promise<never>` has no resolvable
|
|
1811
|
+
* value in the type system, but settling it is the entire point: it is
|
|
1812
|
+
* only ever called from `reclaim()`, i.e. after the race it guarded has
|
|
1813
|
+
* already been decided, so the resolution is discarded by construction and
|
|
1814
|
+
* can never become a race winner. The cast localises that argument here
|
|
1815
|
+
* rather than pushing a lie into every caller's return type.
|
|
1816
|
+
*/
|
|
1817
|
+
this.settleExpiry = () => {
|
|
1818
|
+
};
|
|
1819
|
+
this.expiry = new Promise((resolve, reject) => {
|
|
1820
|
+
this.settleExpiry = resolve;
|
|
1821
|
+
this.timer = setTimeout(() => reject(createTimeoutError()), timeoutMs);
|
|
1822
|
+
});
|
|
1823
|
+
}
|
|
1824
|
+
/**
|
|
1825
|
+
* Reclaim the guard once the race it protects has been decided. Both
|
|
1826
|
+
* halves, always: the timer is cleared so it cannot fire against a
|
|
1827
|
+
* lifecycle phase that is already over, and `expiry` is settled so neither
|
|
1828
|
+
* it nor the race's reaction on it is retained.
|
|
1829
|
+
*
|
|
1830
|
+
* Idempotent — `clearTimeout` on a cleared handle and a second resolve on
|
|
1831
|
+
* a settled promise are both no-ops.
|
|
1832
|
+
*/
|
|
1833
|
+
reclaim() {
|
|
1834
|
+
clearTimeout(this.timer);
|
|
1835
|
+
this.timer = void 0;
|
|
1836
|
+
this.settleExpiry();
|
|
1837
|
+
}
|
|
1838
|
+
};
|
|
1839
|
+
async function raceWithTimeout(operation, timeoutMs, createTimeoutError) {
|
|
1840
|
+
const guard = new TimeoutGuard(timeoutMs, createTimeoutError);
|
|
1841
|
+
try {
|
|
1842
|
+
return await Promise.race([operation, guard.expiry]);
|
|
1843
|
+
} finally {
|
|
1844
|
+
guard.reclaim();
|
|
1845
|
+
}
|
|
1846
|
+
}
|
|
1847
|
+
|
|
1909
1848
|
// src/kernel.ts
|
|
1910
1849
|
var ObjectKernel = class {
|
|
1911
1850
|
constructor(config = {}) {
|
|
@@ -2176,14 +2115,11 @@ var ObjectKernel = class {
|
|
|
2176
2115
|
this.logger.info("Graceful shutdown started");
|
|
2177
2116
|
const shutdownTimeoutError = new Error("Shutdown timeout exceeded");
|
|
2178
2117
|
try {
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
if (t.unref) t.unref();
|
|
2185
|
-
});
|
|
2186
|
-
await Promise.race([shutdownPromise, timeoutPromise]);
|
|
2118
|
+
await raceWithTimeout(
|
|
2119
|
+
this.performShutdown(),
|
|
2120
|
+
this.config.shutdownTimeout,
|
|
2121
|
+
() => shutdownTimeoutError
|
|
2122
|
+
);
|
|
2187
2123
|
this.state = "stopped";
|
|
2188
2124
|
this.logger.info("\u2705 Graceful shutdown complete");
|
|
2189
2125
|
} catch (error) {
|
|
@@ -2301,25 +2237,17 @@ var ObjectKernel = class {
|
|
|
2301
2237
|
* as well: if the hook never settles and nothing else keeps the loop alive,
|
|
2302
2238
|
* Node exits before the timer can fire and the timeout is never reported.
|
|
2303
2239
|
* The guard has to stay ref'd exactly as long as the race is undecided,
|
|
2304
|
-
* which is what
|
|
2240
|
+
* which is what clearing on settle expresses.
|
|
2305
2241
|
*
|
|
2306
|
-
*
|
|
2307
|
-
*
|
|
2308
|
-
*
|
|
2309
|
-
*
|
|
2242
|
+
* Clearing the timer was only half of it, though (#10604): the promise the
|
|
2243
|
+
* race still holds a reaction on has to SETTLE, or it and that reaction are
|
|
2244
|
+
* retained past the end of the run — two leaking promises per boot, which
|
|
2245
|
+
* is what `vitest --detectAsyncLeaks` names here. Both halves now live in
|
|
2246
|
+
* `TimeoutGuard.reclaim()`, shared with `shutdown()`, so the two sites
|
|
2247
|
+
* cannot drift into doing one half each again.
|
|
2310
2248
|
*/
|
|
2311
2249
|
async raceStartupTimeout(operation, timeout, message) {
|
|
2312
|
-
|
|
2313
|
-
const timeoutPromise = new Promise((_, reject) => {
|
|
2314
|
-
guard = setTimeout(() => {
|
|
2315
|
-
reject(new Error(message));
|
|
2316
|
-
}, timeout);
|
|
2317
|
-
});
|
|
2318
|
-
try {
|
|
2319
|
-
return await Promise.race([operation, timeoutPromise]);
|
|
2320
|
-
} finally {
|
|
2321
|
-
clearTimeout(guard);
|
|
2322
|
-
}
|
|
2250
|
+
return raceWithTimeout(operation, timeout, () => new Error(message));
|
|
2323
2251
|
}
|
|
2324
2252
|
/**
|
|
2325
2253
|
* Whether a service is resolvable on this kernel right now — direct
|
|
@@ -2482,6 +2410,67 @@ var ObjectKernel = class {
|
|
|
2482
2410
|
}
|
|
2483
2411
|
};
|
|
2484
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
|
+
|
|
2485
2474
|
// src/lite-kernel.ts
|
|
2486
2475
|
var LiteKernel = class extends ObjectKernelBase {
|
|
2487
2476
|
constructor(config) {
|
|
@@ -3211,6 +3200,55 @@ var PluginSignatureVerifier = class {
|
|
|
3211
3200
|
}
|
|
3212
3201
|
};
|
|
3213
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
|
+
|
|
3214
3252
|
// src/security/plugin-permission-enforcer.ts
|
|
3215
3253
|
var PluginPermissionEnforcer = class {
|
|
3216
3254
|
constructor(logger) {
|
|
@@ -4305,16 +4343,16 @@ var PluginSecurityScanner = class {
|
|
|
4305
4343
|
};
|
|
4306
4344
|
|
|
4307
4345
|
// src/security/api-key.ts
|
|
4308
|
-
var
|
|
4346
|
+
var import_node_crypto3 = require("crypto");
|
|
4309
4347
|
var import_security = require("@objectstack/spec/security");
|
|
4310
4348
|
var API_KEY_PREFIX = "osk_";
|
|
4311
4349
|
var API_KEY_ENTROPY_BYTES = 32;
|
|
4312
4350
|
var VISIBLE_PREFIX_LEN = 12;
|
|
4313
4351
|
function hashApiKey(raw) {
|
|
4314
|
-
return (0,
|
|
4352
|
+
return (0, import_node_crypto3.createHash)("sha256").update(raw, "utf8").digest("hex");
|
|
4315
4353
|
}
|
|
4316
4354
|
function generateApiKey(prefix = API_KEY_PREFIX) {
|
|
4317
|
-
const secret = (0,
|
|
4355
|
+
const secret = (0, import_node_crypto3.randomBytes)(API_KEY_ENTROPY_BYTES).toString("base64url");
|
|
4318
4356
|
const raw = `${prefix}${secret}`;
|
|
4319
4357
|
return {
|
|
4320
4358
|
raw,
|
|
@@ -4428,8 +4466,37 @@ function safeJsonParse(s, fallback) {
|
|
|
4428
4466
|
}
|
|
4429
4467
|
}
|
|
4430
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
|
+
|
|
4431
4497
|
// src/security/resolve-authz-context.ts
|
|
4432
|
-
var
|
|
4498
|
+
var import_types2 = require("@objectstack/types");
|
|
4499
|
+
var import_spec2 = require("@objectstack/spec");
|
|
4433
4500
|
var import_security2 = require("@objectstack/spec/security");
|
|
4434
4501
|
|
|
4435
4502
|
// src/security/grant-validity.ts
|
|
@@ -4450,6 +4517,20 @@ function isGrantActive(row, nowMs) {
|
|
|
4450
4517
|
if (until !== void 0 && !(nowMs < until)) return false;
|
|
4451
4518
|
return true;
|
|
4452
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
|
+
}
|
|
4453
4534
|
function isGrantExpired(row, nowMs) {
|
|
4454
4535
|
if (!row) return false;
|
|
4455
4536
|
const until = toEpochMs(row.valid_until ?? row.validUntil);
|
|
@@ -4457,6 +4538,227 @@ function isGrantExpired(row, nowMs) {
|
|
|
4457
4538
|
return !(nowMs < until);
|
|
4458
4539
|
}
|
|
4459
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
|
+
|
|
4460
4762
|
// src/security/posture-ladder.ts
|
|
4461
4763
|
var POSTURE_LADDER = [
|
|
4462
4764
|
"PLATFORM_ADMIN",
|
|
@@ -4530,14 +4832,16 @@ function safeJsonParse2(s, fallback) {
|
|
|
4530
4832
|
return fallback;
|
|
4531
4833
|
}
|
|
4532
4834
|
}
|
|
4533
|
-
async function tryFind(ql, object, where, limit = 100) {
|
|
4835
|
+
async function tryFind(ql, object, where, limit = 100, organizationId) {
|
|
4534
4836
|
if (!ql || typeof ql.find !== "function") return [];
|
|
4535
4837
|
try {
|
|
4536
|
-
|
|
4838
|
+
const context = organizationId ? { isSystem: true, tenantId: organizationId } : { isSystem: true };
|
|
4839
|
+
let rows = await ql.find(object, { where, limit, context });
|
|
4537
4840
|
if (rows && rows.value) rows = rows.value;
|
|
4538
4841
|
return Array.isArray(rows) ? rows : [];
|
|
4539
|
-
} catch {
|
|
4540
|
-
return [];
|
|
4842
|
+
} catch (err) {
|
|
4843
|
+
if ((0, import_types2.isMissingTableError)(err, object)) return [];
|
|
4844
|
+
throw new AuthzStoreUnavailableError(object, err);
|
|
4541
4845
|
}
|
|
4542
4846
|
}
|
|
4543
4847
|
async function resolveAuthzContext(input) {
|
|
@@ -4611,6 +4915,8 @@ async function resolveAuthzContext(input) {
|
|
|
4611
4915
|
return ctx;
|
|
4612
4916
|
}
|
|
4613
4917
|
async function resolveUserAuthzGrants(ql, userId, opts = {}) {
|
|
4918
|
+
const grantsCache = openUserGrantsCache(ql, userId, opts);
|
|
4919
|
+
if (grantsCache?.hit) return grantsCache.hit;
|
|
4614
4920
|
const { tenantId } = opts;
|
|
4615
4921
|
const grants = {
|
|
4616
4922
|
positions: [],
|
|
@@ -4631,12 +4937,20 @@ async function resolveUserAuthzGrants(ql, userId, opts = {}) {
|
|
|
4631
4937
|
}
|
|
4632
4938
|
return userRow;
|
|
4633
4939
|
};
|
|
4940
|
+
const platformAdminConfig = resolvePlatformAdminEmails();
|
|
4941
|
+
const needsUserRow = !grants.email || !grants.permissions.includes("ai_seat") || platformAdminConfig.emails.length > 0;
|
|
4942
|
+
const [, members, userPositionRows, orgMembersLeg, upsRowsAll] = await Promise.all([
|
|
4943
|
+
needsUserRow ? getUserRow() : Promise.resolve(void 0),
|
|
4944
|
+
tryFind(ql, "sys_member", { user_id: userId }, 200),
|
|
4945
|
+
tryFind(ql, "sys_user_position", { user_id: userId }, 200),
|
|
4946
|
+
tenantId ? tryFind(ql, "sys_member", { organization_id: tenantId }, 1e3) : Promise.resolve([]),
|
|
4947
|
+
tryFind(ql, "sys_user_permission_set", { user_id: userId }, 100)
|
|
4948
|
+
]);
|
|
4634
4949
|
if (!grants.email) {
|
|
4635
4950
|
const u = await getUserRow();
|
|
4636
4951
|
if (u?.email) grants.email = String(u.email);
|
|
4637
4952
|
}
|
|
4638
4953
|
const nowMs = opts.nowMs ?? Date.now();
|
|
4639
|
-
const members = await tryFind(ql, "sys_member", { user_id: userId }, 200);
|
|
4640
4954
|
const accessibleOrgIds = /* @__PURE__ */ new Set();
|
|
4641
4955
|
for (const m of members) {
|
|
4642
4956
|
if (!isGrantActive(m, nowMs)) continue;
|
|
@@ -4644,16 +4958,17 @@ async function resolveUserAuthzGrants(ql, userId, opts = {}) {
|
|
|
4644
4958
|
if (typeof org === "string" && org) accessibleOrgIds.add(org);
|
|
4645
4959
|
}
|
|
4646
4960
|
grants.accessible_org_ids = Array.from(accessibleOrgIds);
|
|
4647
|
-
const activeMembers =
|
|
4961
|
+
const activeMembers = members.filter(
|
|
4962
|
+
(m) => isGrantActive(m, nowMs) && (!tenantId || (m.organization_id ?? m.organizationId) === tenantId)
|
|
4963
|
+
);
|
|
4648
4964
|
for (const m of activeMembers) {
|
|
4649
4965
|
if (m.role && typeof m.role === "string") {
|
|
4650
4966
|
for (const raw of m.role.split(",").map((s) => s.trim()).filter(Boolean)) {
|
|
4651
|
-
const r = (0,
|
|
4967
|
+
const r = (0, import_spec2.mapMembershipRole)(raw);
|
|
4652
4968
|
if (!grants.positions.includes(r)) grants.positions.push(r);
|
|
4653
4969
|
}
|
|
4654
4970
|
}
|
|
4655
4971
|
}
|
|
4656
|
-
const userPositionRows = await tryFind(ql, "sys_user_position", { user_id: userId }, 200);
|
|
4657
4972
|
for (const ur of userPositionRows) {
|
|
4658
4973
|
const org = ur.organization_id ?? null;
|
|
4659
4974
|
if (org && tenantId && org !== tenantId) continue;
|
|
@@ -4662,14 +4977,13 @@ async function resolveUserAuthzGrants(ql, userId, opts = {}) {
|
|
|
4662
4977
|
if (typeof r === "string" && r && !grants.positions.includes(r)) grants.positions.push(r);
|
|
4663
4978
|
}
|
|
4664
4979
|
if (tenantId) {
|
|
4665
|
-
const orgMembers =
|
|
4980
|
+
const orgMembers = orgMembersLeg;
|
|
4666
4981
|
const ids = new Set(
|
|
4667
4982
|
orgMembers.map((m) => m.user_id ?? m.userId).filter((v) => typeof v === "string" && v.length > 0)
|
|
4668
4983
|
);
|
|
4669
4984
|
ids.add(userId);
|
|
4670
4985
|
grants.org_user_ids = Array.from(ids);
|
|
4671
4986
|
}
|
|
4672
|
-
const upsRowsAll = await tryFind(ql, "sys_user_permission_set", { user_id: userId }, 100);
|
|
4673
4987
|
const upsRows = upsRowsAll.filter((r) => isGrantActive(r, nowMs));
|
|
4674
4988
|
const psIds = new Set(
|
|
4675
4989
|
upsRows.filter((r) => {
|
|
@@ -4683,7 +4997,7 @@ async function resolveUserAuthzGrants(ql, userId, opts = {}) {
|
|
|
4683
4997
|
let hasPlatformAdminGrant = false;
|
|
4684
4998
|
if (!grants.positions.includes("everyone")) grants.positions.push("everyone");
|
|
4685
4999
|
if (grants.positions.length > 0) {
|
|
4686
|
-
const positionRows = await tryFind(ql, "sys_position", { name: { $in: grants.positions } },
|
|
5000
|
+
const positionRows = await tryFind(ql, "sys_position", { name: { $in: grants.positions } }, 200, tenantId);
|
|
4687
5001
|
const deactivatedNames = new Set(
|
|
4688
5002
|
positionRows.filter((r) => !isRowActive(r)).map((r) => r.name).filter(Boolean)
|
|
4689
5003
|
);
|
|
@@ -4706,7 +5020,7 @@ async function resolveUserAuthzGrants(ql, userId, opts = {}) {
|
|
|
4706
5020
|
const mergedTabs = {};
|
|
4707
5021
|
for (const ps of psRows) {
|
|
4708
5022
|
if (ps.name && !grants.permissions.includes(ps.name)) grants.permissions.push(ps.name);
|
|
4709
|
-
if (ps.name ===
|
|
5023
|
+
if (ps.name === import_spec2.ADMIN_FULL_ACCESS && unscopedUserPsIds.has(ps.id)) hasPlatformAdminGrant = true;
|
|
4710
5024
|
const sysPerms = typeof ps.system_permissions === "string" ? safeJsonParse2(ps.system_permissions, []) : ps.system_permissions ?? ps.systemPermissions;
|
|
4711
5025
|
if (Array.isArray(sysPerms)) {
|
|
4712
5026
|
for (const p of sysPerms) {
|
|
@@ -4726,21 +5040,46 @@ async function resolveUserAuthzGrants(ql, userId, opts = {}) {
|
|
|
4726
5040
|
}
|
|
4727
5041
|
if (Object.keys(mergedTabs).length > 0) grants.tabPermissions = mergedTabs;
|
|
4728
5042
|
}
|
|
4729
|
-
|
|
4730
|
-
|
|
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);
|
|
4731
5057
|
}
|
|
4732
5058
|
grants.posture = derivePosture({
|
|
4733
5059
|
isPlatformAdmin: hasPlatformAdminGrant,
|
|
4734
5060
|
// [ADR-0105 D4] Either org-admin capability set resolves the rung — the
|
|
4735
5061
|
// wall-less variant differs only by withholding the superuser bits.
|
|
4736
|
-
isTenantAdmin:
|
|
5062
|
+
isTenantAdmin: import_spec2.ORGANIZATION_ADMIN_GRANTS.some((n) => grants.permissions.includes(n))
|
|
4737
5063
|
});
|
|
4738
5064
|
if (!grants.permissions.includes("ai_seat")) {
|
|
4739
5065
|
const aiAccess = (await getUserRow())?.ai_access;
|
|
4740
5066
|
if (aiAccess === true || aiAccess === 1 || aiAccess === "1") grants.permissions.push("ai_seat");
|
|
4741
5067
|
}
|
|
5068
|
+
grantsCache?.commit(
|
|
5069
|
+
grants,
|
|
5070
|
+
nextGrantValidityBoundary([...members, ...userPositionRows, ...upsRowsAll], nowMs)
|
|
5071
|
+
);
|
|
4742
5072
|
return grants;
|
|
4743
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
|
+
}
|
|
4744
5083
|
function isValidTimeZone(tz) {
|
|
4745
5084
|
try {
|
|
4746
5085
|
new Intl.DateTimeFormat("en-US", { timeZone: tz });
|
|
@@ -4761,34 +5100,151 @@ function coerceCurrency(value) {
|
|
|
4761
5100
|
const s = typeof value === "string" ? value.trim().toUpperCase() : "";
|
|
4762
5101
|
return /^[A-Z]{3}$/.test(s) ? s : void 0;
|
|
4763
5102
|
}
|
|
5103
|
+
var LOCALIZATION_FAILURE_CACHE_TTL_MS = 3e4;
|
|
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
|
+
}
|
|
4764
5156
|
async function resolveLocalizationContext(input) {
|
|
4765
5157
|
const { ql, settings, tenantId, userId } = input;
|
|
5158
|
+
const cacheKey = `${tenantId ?? ""}|${userId ?? ""}`;
|
|
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);
|
|
5190
|
+
}
|
|
5191
|
+
return value;
|
|
5192
|
+
}
|
|
5193
|
+
async function resolveLocalizationContextUncached(input) {
|
|
5194
|
+
const { ql, settings, tenantId, userId } = input;
|
|
5195
|
+
let backendFailed = false;
|
|
4766
5196
|
try {
|
|
4767
5197
|
if (settings && typeof settings.get === "function") {
|
|
4768
5198
|
const sctx = { tenantId, userId };
|
|
4769
|
-
|
|
4770
|
-
|
|
4771
|
-
|
|
4772
|
-
|
|
4773
|
-
|
|
5199
|
+
let tzRes;
|
|
5200
|
+
let localeRes;
|
|
5201
|
+
let currencyRes;
|
|
5202
|
+
if (typeof settings.getMany === "function") {
|
|
5203
|
+
try {
|
|
5204
|
+
const many = await settings.getMany("localization", ["timezone", "locale", "currency"], sctx);
|
|
5205
|
+
tzRes = many.timezone;
|
|
5206
|
+
localeRes = many.locale;
|
|
5207
|
+
currencyRes = many.currency;
|
|
5208
|
+
} catch {
|
|
5209
|
+
}
|
|
5210
|
+
} else {
|
|
5211
|
+
[tzRes, localeRes, currencyRes] = await Promise.all([
|
|
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)
|
|
5215
|
+
]);
|
|
5216
|
+
}
|
|
4774
5217
|
const tz = coerceTimeZone(tzRes?.value);
|
|
4775
5218
|
const locale = coerceLocale(localeRes?.value);
|
|
4776
5219
|
const currency = coerceCurrency(currencyRes?.value);
|
|
4777
|
-
if (tz || locale || currency)
|
|
5220
|
+
if (tz || locale || currency) {
|
|
5221
|
+
return { value: { timezone: tz ?? "UTC", locale: locale ?? "en-US", currency }, backendFailed: false };
|
|
5222
|
+
}
|
|
4778
5223
|
}
|
|
4779
5224
|
} catch {
|
|
4780
5225
|
}
|
|
4781
|
-
|
|
4782
|
-
|
|
4783
|
-
|
|
4784
|
-
|
|
4785
|
-
|
|
4786
|
-
|
|
5226
|
+
let rows = [];
|
|
5227
|
+
if (ql && typeof ql.find === "function") {
|
|
5228
|
+
try {
|
|
5229
|
+
let result = await ql.find("sys_setting", {
|
|
5230
|
+
where: { namespace: "localization", key: { $in: ["timezone", "locale", "currency"] }, scope: "tenant" },
|
|
5231
|
+
limit: 10,
|
|
5232
|
+
context: { isSystem: true }
|
|
5233
|
+
});
|
|
5234
|
+
if (result && result.value) result = result.value;
|
|
5235
|
+
rows = Array.isArray(result) ? result : [];
|
|
5236
|
+
} catch {
|
|
5237
|
+
backendFailed = true;
|
|
5238
|
+
}
|
|
5239
|
+
}
|
|
4787
5240
|
const valueOf = (k) => rows.find((r) => r.key === k)?.value;
|
|
4788
5241
|
return {
|
|
4789
|
-
|
|
4790
|
-
|
|
4791
|
-
|
|
5242
|
+
value: {
|
|
5243
|
+
timezone: coerceTimeZone(valueOf("timezone")) ?? "UTC",
|
|
5244
|
+
locale: coerceLocale(valueOf("locale")) ?? "en-US",
|
|
5245
|
+
currency: coerceCurrency(valueOf("currency"))
|
|
5246
|
+
},
|
|
5247
|
+
backendFailed
|
|
4792
5248
|
};
|
|
4793
5249
|
}
|
|
4794
5250
|
|
|
@@ -4950,7 +5406,7 @@ function shouldDenyAnonymous(input) {
|
|
|
4950
5406
|
var ADMIN_STANDING_SURFACE = {
|
|
4951
5407
|
sys_permission_set: {
|
|
4952
5408
|
role: "derives",
|
|
4953
|
-
reason: "The row `
|
|
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.",
|
|
4954
5410
|
columns: [
|
|
4955
5411
|
"id",
|
|
4956
5412
|
"name",
|
|
@@ -4992,8 +5448,14 @@ var ADMIN_STANDING_SURFACE = {
|
|
|
4992
5448
|
]
|
|
4993
5449
|
},
|
|
4994
5450
|
sys_user: {
|
|
4995
|
-
role: "
|
|
4996
|
-
reason: "
|
|
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
|
+
]
|
|
4997
5459
|
},
|
|
4998
5460
|
sys_user_position: {
|
|
4999
5461
|
role: "reads-only",
|
|
@@ -5008,6 +5470,13 @@ var ADMIN_STANDING_SURFACE = {
|
|
|
5008
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."
|
|
5009
5471
|
}
|
|
5010
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
|
+
];
|
|
5011
5480
|
function adminStandingTables() {
|
|
5012
5481
|
return Object.entries(ADMIN_STANDING_SURFACE).filter(([, t]) => t.role === "derives").map(([name]) => name).sort();
|
|
5013
5482
|
}
|
|
@@ -5039,6 +5508,9 @@ function withoutOperationPrivateKeys(exec) {
|
|
|
5039
5508
|
return out;
|
|
5040
5509
|
}
|
|
5041
5510
|
|
|
5511
|
+
// src/security/authz-invalidation-channel.ts
|
|
5512
|
+
var AUTHZ_INVALIDATED_CHANNEL = "authz.invalidated";
|
|
5513
|
+
|
|
5042
5514
|
// src/utils/datetime.ts
|
|
5043
5515
|
var import_data = require("@objectstack/spec/data");
|
|
5044
5516
|
function calendarPartsInTz(d, tz) {
|
|
@@ -5319,7 +5791,7 @@ function omitInternalFieldsFromWriteResponse(schema, records) {
|
|
|
5319
5791
|
}
|
|
5320
5792
|
|
|
5321
5793
|
// src/utils/migration-journal.ts
|
|
5322
|
-
var
|
|
5794
|
+
var import_node_crypto4 = require("crypto");
|
|
5323
5795
|
var import_system4 = require("@objectstack/spec/system");
|
|
5324
5796
|
var SYSTEM_CTX = { isSystem: true };
|
|
5325
5797
|
var DEFAULT_CHUNK_SIZE = 200;
|
|
@@ -5374,7 +5846,7 @@ function hashMigrationPlan(plan, chunks) {
|
|
|
5374
5846
|
steps: plan.steps.map((s) => s.name),
|
|
5375
5847
|
chunks: chunks.map((c) => [c.stepIndex, c.offset, c.length])
|
|
5376
5848
|
});
|
|
5377
|
-
return (0,
|
|
5849
|
+
return (0, import_node_crypto4.createHash)("sha256").update(shape, "utf8").digest("hex").slice(0, 32);
|
|
5378
5850
|
}
|
|
5379
5851
|
async function appendEvent(engine, event, execContext) {
|
|
5380
5852
|
await engine.insert(
|
|
@@ -5447,7 +5919,7 @@ async function runMigrationJournal(engine, plan, options = {}) {
|
|
|
5447
5919
|
}
|
|
5448
5920
|
const { chunks, planHash, rowsByStep } = await loadPlan(engine, plan, options.chunkSize);
|
|
5449
5921
|
const resuming = Boolean(options.runId);
|
|
5450
|
-
const runId = options.runId ?? (0,
|
|
5922
|
+
const runId = options.runId ?? (0, import_node_crypto4.randomUUID)();
|
|
5451
5923
|
let events = [];
|
|
5452
5924
|
let seq = 0;
|
|
5453
5925
|
let committed = /* @__PURE__ */ new Set();
|
|
@@ -5939,6 +6411,110 @@ function isUninterpretableTemporalComparand(kind, value) {
|
|
|
5939
6411
|
return !(readsAsWallClock(s) || readsAsInstant(s));
|
|
5940
6412
|
}
|
|
5941
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
|
+
|
|
5942
6518
|
// src/utils/record-not-found.ts
|
|
5943
6519
|
function recordNotFoundError(object, id) {
|
|
5944
6520
|
const err = new Error(`Record ${id} not found in ${object}`);
|
|
@@ -5949,6 +6525,45 @@ function recordNotFoundError(object, id) {
|
|
|
5949
6525
|
}
|
|
5950
6526
|
|
|
5951
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
|
+
}
|
|
5952
6567
|
var PluginHealthMonitor = class {
|
|
5953
6568
|
constructor(logger) {
|
|
5954
6569
|
this.healthChecks = /* @__PURE__ */ new Map();
|
|
@@ -5957,18 +6572,17 @@ var PluginHealthMonitor = class {
|
|
|
5957
6572
|
this.checkIntervals = /* @__PURE__ */ new Map();
|
|
5958
6573
|
this.failureCounters = /* @__PURE__ */ new Map();
|
|
5959
6574
|
this.successCounters = /* @__PURE__ */ new Map();
|
|
5960
|
-
this.restartAttempts = /* @__PURE__ */ new Map();
|
|
5961
6575
|
this.logger = logger.child({ component: "HealthMonitor" });
|
|
5962
6576
|
}
|
|
5963
6577
|
/**
|
|
5964
6578
|
* Register a plugin for health monitoring
|
|
5965
6579
|
*/
|
|
5966
6580
|
registerPlugin(pluginName, config) {
|
|
6581
|
+
assertNoRetiredKeys(pluginName, config);
|
|
5967
6582
|
this.healthChecks.set(pluginName, config);
|
|
5968
6583
|
this.healthStatus.set(pluginName, "unknown");
|
|
5969
6584
|
this.failureCounters.set(pluginName, 0);
|
|
5970
6585
|
this.successCounters.set(pluginName, 0);
|
|
5971
|
-
this.restartAttempts.set(pluginName, 0);
|
|
5972
6586
|
this.logger.info("Plugin registered for health monitoring", {
|
|
5973
6587
|
plugin: pluginName,
|
|
5974
6588
|
interval: config.interval
|
|
@@ -6020,6 +6634,7 @@ var PluginHealthMonitor = class {
|
|
|
6020
6634
|
let status = "healthy";
|
|
6021
6635
|
let message;
|
|
6022
6636
|
const checks = [];
|
|
6637
|
+
let failureRoute;
|
|
6023
6638
|
try {
|
|
6024
6639
|
if (config.checkMethod && typeof plugin[config.checkMethod] === "function") {
|
|
6025
6640
|
const checkResult = await this.raceCheckTimeout(
|
|
@@ -6040,8 +6655,8 @@ var PluginHealthMonitor = class {
|
|
|
6040
6655
|
if (status === "healthy") {
|
|
6041
6656
|
this.successCounters.set(pluginName, (this.successCounters.get(pluginName) || 0) + 1);
|
|
6042
6657
|
this.failureCounters.set(pluginName, 0);
|
|
6043
|
-
const currentStatus = this.healthStatus.get(pluginName);
|
|
6044
|
-
if (currentStatus
|
|
6658
|
+
const currentStatus = this.healthStatus.get(pluginName) ?? "unknown";
|
|
6659
|
+
if (RECOVERY_IS_THRESHOLD_GATED[currentStatus]) {
|
|
6045
6660
|
const successCount = this.successCounters.get(pluginName) || 0;
|
|
6046
6661
|
if (successCount >= config.successThreshold) {
|
|
6047
6662
|
this.healthStatus.set(pluginName, "healthy");
|
|
@@ -6053,27 +6668,11 @@ var PluginHealthMonitor = class {
|
|
|
6053
6668
|
this.healthStatus.set(pluginName, "healthy");
|
|
6054
6669
|
}
|
|
6055
6670
|
} else {
|
|
6056
|
-
|
|
6057
|
-
this.successCounters.set(pluginName, 0);
|
|
6058
|
-
const failureCount = this.failureCounters.get(pluginName) || 0;
|
|
6059
|
-
if (failureCount >= config.failureThreshold) {
|
|
6060
|
-
this.healthStatus.set(pluginName, "unhealthy");
|
|
6061
|
-
this.logger.warn("Plugin marked as unhealthy", {
|
|
6062
|
-
plugin: pluginName,
|
|
6063
|
-
failures: failureCount
|
|
6064
|
-
});
|
|
6065
|
-
if (config.autoRestart) {
|
|
6066
|
-
await this.attemptRestart(pluginName, plugin, config);
|
|
6067
|
-
}
|
|
6068
|
-
} else {
|
|
6069
|
-
this.healthStatus.set(pluginName, "degraded");
|
|
6070
|
-
}
|
|
6671
|
+
failureRoute = "returned";
|
|
6071
6672
|
}
|
|
6072
6673
|
} catch (error) {
|
|
6073
6674
|
status = "failed";
|
|
6074
6675
|
message = error instanceof Error ? error.message : "Unknown error";
|
|
6075
|
-
this.failureCounters.set(pluginName, (this.failureCounters.get(pluginName) || 0) + 1);
|
|
6076
|
-
this.healthStatus.set(pluginName, "failed");
|
|
6077
6676
|
checks.push({
|
|
6078
6677
|
name: "health-check",
|
|
6079
6678
|
status: "failed",
|
|
@@ -6083,6 +6682,10 @@ var PluginHealthMonitor = class {
|
|
|
6083
6682
|
plugin: pluginName,
|
|
6084
6683
|
error
|
|
6085
6684
|
});
|
|
6685
|
+
failureRoute = "thrown";
|
|
6686
|
+
}
|
|
6687
|
+
if (failureRoute) {
|
|
6688
|
+
this.recordFailedRound(pluginName, config, failureRoute);
|
|
6086
6689
|
}
|
|
6087
6690
|
const report = {
|
|
6088
6691
|
status: this.healthStatus.get(pluginName) || "unknown",
|
|
@@ -6096,56 +6699,42 @@ var PluginHealthMonitor = class {
|
|
|
6096
6699
|
this.healthReports.set(pluginName, report);
|
|
6097
6700
|
}
|
|
6098
6701
|
/**
|
|
6099
|
-
*
|
|
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.
|
|
6100
6722
|
*/
|
|
6101
|
-
|
|
6102
|
-
const
|
|
6103
|
-
|
|
6104
|
-
|
|
6105
|
-
|
|
6106
|
-
|
|
6107
|
-
});
|
|
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") {
|
|
6108
6729
|
this.healthStatus.set(pluginName, "failed");
|
|
6109
|
-
|
|
6110
|
-
|
|
6111
|
-
|
|
6112
|
-
const delay = this.calculateBackoff(attempts, config.restartBackoff);
|
|
6113
|
-
this.logger.info("Scheduling plugin restart", {
|
|
6114
|
-
plugin: pluginName,
|
|
6115
|
-
attempt: attempts + 1,
|
|
6116
|
-
delay
|
|
6117
|
-
});
|
|
6118
|
-
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
6119
|
-
try {
|
|
6120
|
-
if (plugin.destroy) {
|
|
6121
|
-
await plugin.destroy();
|
|
6122
|
-
}
|
|
6123
|
-
this.logger.info("Plugin restarted", { plugin: pluginName });
|
|
6124
|
-
this.failureCounters.set(pluginName, 0);
|
|
6125
|
-
this.successCounters.set(pluginName, 0);
|
|
6126
|
-
this.healthStatus.set(pluginName, "recovering");
|
|
6127
|
-
} catch (error) {
|
|
6128
|
-
this.logger.error("Plugin restart failed", {
|
|
6730
|
+
} else if (thresholdReached) {
|
|
6731
|
+
this.healthStatus.set(pluginName, "unhealthy");
|
|
6732
|
+
this.logger.warn("Plugin marked as unhealthy", {
|
|
6129
6733
|
plugin: pluginName,
|
|
6130
|
-
|
|
6734
|
+
failures: failureCount
|
|
6131
6735
|
});
|
|
6132
|
-
|
|
6133
|
-
|
|
6134
|
-
}
|
|
6135
|
-
/**
|
|
6136
|
-
* Calculate backoff delay for restarts
|
|
6137
|
-
*/
|
|
6138
|
-
calculateBackoff(attempt, strategy) {
|
|
6139
|
-
const baseDelay = 1e3;
|
|
6140
|
-
switch (strategy) {
|
|
6141
|
-
case "fixed":
|
|
6142
|
-
return baseDelay;
|
|
6143
|
-
case "linear":
|
|
6144
|
-
return baseDelay * (attempt + 1);
|
|
6145
|
-
case "exponential":
|
|
6146
|
-
return baseDelay * Math.pow(2, attempt);
|
|
6147
|
-
default:
|
|
6148
|
-
return baseDelay;
|
|
6736
|
+
} else {
|
|
6737
|
+
this.healthStatus.set(pluginName, "degraded");
|
|
6149
6738
|
}
|
|
6150
6739
|
}
|
|
6151
6740
|
/**
|
|
@@ -6178,7 +6767,6 @@ var PluginHealthMonitor = class {
|
|
|
6178
6767
|
this.healthReports.clear();
|
|
6179
6768
|
this.failureCounters.clear();
|
|
6180
6769
|
this.successCounters.clear();
|
|
6181
|
-
this.restartAttempts.clear();
|
|
6182
6770
|
this.logger.info("Health monitor shutdown complete");
|
|
6183
6771
|
}
|
|
6184
6772
|
/**
|
|
@@ -6219,7 +6807,7 @@ var PluginHealthMonitor = class {
|
|
|
6219
6807
|
};
|
|
6220
6808
|
|
|
6221
6809
|
// src/hot-reload.ts
|
|
6222
|
-
var
|
|
6810
|
+
var import_node_crypto5 = require("crypto");
|
|
6223
6811
|
var generateUUID = () => {
|
|
6224
6812
|
if (typeof crypto !== "undefined" && crypto.randomUUID) {
|
|
6225
6813
|
return crypto.randomUUID();
|
|
@@ -6230,6 +6818,42 @@ var generateUUID = () => {
|
|
|
6230
6818
|
return v.toString(16);
|
|
6231
6819
|
});
|
|
6232
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
|
+
}
|
|
6233
6857
|
var PluginStateManager = class {
|
|
6234
6858
|
constructor(logger) {
|
|
6235
6859
|
this.stateSnapshots = /* @__PURE__ */ new Map();
|
|
@@ -6256,17 +6880,6 @@ var PluginStateManager = class {
|
|
|
6256
6880
|
this.memoryStore.set(snapshotId, snapshot);
|
|
6257
6881
|
this.logger.debug("State saved to memory", { pluginId, snapshotId });
|
|
6258
6882
|
break;
|
|
6259
|
-
case "disk":
|
|
6260
|
-
this.memoryStore.set(snapshotId, snapshot);
|
|
6261
|
-
this.logger.debug("State saved to disk (memory fallback)", { pluginId, snapshotId });
|
|
6262
|
-
break;
|
|
6263
|
-
case "distributed":
|
|
6264
|
-
this.memoryStore.set(snapshotId, snapshot);
|
|
6265
|
-
this.logger.debug("State saved to distributed store (memory fallback)", {
|
|
6266
|
-
pluginId,
|
|
6267
|
-
snapshotId
|
|
6268
|
-
});
|
|
6269
|
-
break;
|
|
6270
6883
|
case "none":
|
|
6271
6884
|
this.logger.debug("State persistence disabled", { pluginId });
|
|
6272
6885
|
break;
|
|
@@ -6314,7 +6927,7 @@ var PluginStateManager = class {
|
|
|
6314
6927
|
*/
|
|
6315
6928
|
calculateChecksum(state) {
|
|
6316
6929
|
const stateStr = JSON.stringify(state);
|
|
6317
|
-
return (0,
|
|
6930
|
+
return (0, import_node_crypto5.createHash)("sha256").update(stateStr).digest("hex");
|
|
6318
6931
|
}
|
|
6319
6932
|
/**
|
|
6320
6933
|
* Shutdown state manager
|
|
@@ -6328,7 +6941,6 @@ var PluginStateManager = class {
|
|
|
6328
6941
|
var HotReloadManager = class {
|
|
6329
6942
|
constructor(logger) {
|
|
6330
6943
|
this.reloadConfigs = /* @__PURE__ */ new Map();
|
|
6331
|
-
this.watchHandles = /* @__PURE__ */ new Map();
|
|
6332
6944
|
this.reloadTimers = /* @__PURE__ */ new Map();
|
|
6333
6945
|
this.logger = logger.child({ component: "HotReload" });
|
|
6334
6946
|
this.stateManager = new PluginStateManager(logger);
|
|
@@ -6337,6 +6949,8 @@ var HotReloadManager = class {
|
|
|
6337
6949
|
* Register a plugin for hot reload
|
|
6338
6950
|
*/
|
|
6339
6951
|
registerPlugin(pluginName, config) {
|
|
6952
|
+
assertHonouredStateStrategy(pluginName, config.stateStrategy);
|
|
6953
|
+
assertNoRetiredKeys2(pluginName, config);
|
|
6340
6954
|
if (!config.enabled) {
|
|
6341
6955
|
this.logger.debug("Hot reload disabled for plugin", { plugin: pluginName });
|
|
6342
6956
|
return;
|
|
@@ -6344,32 +6958,45 @@ var HotReloadManager = class {
|
|
|
6344
6958
|
this.reloadConfigs.set(pluginName, config);
|
|
6345
6959
|
this.logger.info("Plugin registered for hot reload", {
|
|
6346
6960
|
plugin: pluginName,
|
|
6347
|
-
watchPatterns: config.watchPatterns,
|
|
6348
6961
|
stateStrategy: config.stateStrategy
|
|
6349
6962
|
});
|
|
6350
6963
|
}
|
|
6351
6964
|
/**
|
|
6352
|
-
*
|
|
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.
|
|
6353
6982
|
*/
|
|
6354
6983
|
startWatching(pluginName) {
|
|
6355
|
-
|
|
6356
|
-
|
|
6357
|
-
|
|
6358
|
-
}
|
|
6359
|
-
this.logger.info("File watching started", {
|
|
6360
|
-
plugin: pluginName,
|
|
6361
|
-
patterns: config.watchPatterns
|
|
6362
|
-
});
|
|
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
|
+
);
|
|
6363
6987
|
}
|
|
6364
6988
|
/**
|
|
6365
|
-
*
|
|
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.
|
|
6366
6998
|
*/
|
|
6367
6999
|
stopWatching(pluginName) {
|
|
6368
|
-
const handle = this.watchHandles.get(pluginName);
|
|
6369
|
-
if (handle) {
|
|
6370
|
-
this.watchHandles.delete(pluginName);
|
|
6371
|
-
this.logger.info("File watching stopped", { plugin: pluginName });
|
|
6372
|
-
}
|
|
6373
7000
|
const timer = this.reloadTimers.get(pluginName);
|
|
6374
7001
|
if (timer) {
|
|
6375
7002
|
clearTimeout(timer);
|
|
@@ -6512,14 +7139,10 @@ var HotReloadManager = class {
|
|
|
6512
7139
|
* Shutdown hot reload manager
|
|
6513
7140
|
*/
|
|
6514
7141
|
shutdown() {
|
|
6515
|
-
for (const pluginName of this.watchHandles.keys()) {
|
|
6516
|
-
this.stopWatching(pluginName);
|
|
6517
|
-
}
|
|
6518
7142
|
for (const timer of this.reloadTimers.values()) {
|
|
6519
7143
|
clearTimeout(timer);
|
|
6520
7144
|
}
|
|
6521
7145
|
this.reloadConfigs.clear();
|
|
6522
|
-
this.watchHandles.clear();
|
|
6523
7146
|
this.reloadTimers.clear();
|
|
6524
7147
|
this.stateManager.shutdown();
|
|
6525
7148
|
this.logger.info("Hot reload manager shutdown complete");
|
|
@@ -6929,6 +7552,7 @@ var NamespaceResolver = class {
|
|
|
6929
7552
|
var import_contracts = require("@objectstack/spec/contracts");
|
|
6930
7553
|
// Annotate the CommonJS export names for ESM import in node:
|
|
6931
7554
|
0 && (module.exports = {
|
|
7555
|
+
ADMIN_STANDING_NON_TABLE_INPUTS,
|
|
6932
7556
|
ADMIN_STANDING_SURFACE,
|
|
6933
7557
|
ANONYMOUS_DENY_BODY,
|
|
6934
7558
|
ANONYMOUS_DENY_CODE,
|
|
@@ -6937,11 +7561,19 @@ var import_contracts = require("@objectstack/spec/contracts");
|
|
|
6937
7561
|
API_KEY_PREFIX,
|
|
6938
7562
|
AUDIENCE_BINDING_SUGGESTION_STATUSES,
|
|
6939
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,
|
|
6940
7570
|
CORE_FALLBACK_FACTORIES,
|
|
6941
7571
|
DependencyResolver,
|
|
6942
7572
|
ENTRY_EXECUTION_CONTEXT_FIELDS,
|
|
6943
7573
|
HotReloadManager,
|
|
7574
|
+
InMemoryMetadataActivationStore,
|
|
6944
7575
|
LiteKernel,
|
|
7576
|
+
METADATA_ACTIVATION_TABLE,
|
|
6945
7577
|
MigrationJournalRefusal,
|
|
6946
7578
|
MigrationPlanRegistry,
|
|
6947
7579
|
NamespaceResolver,
|
|
@@ -6949,10 +7581,11 @@ var import_contracts = require("@objectstack/spec/contracts");
|
|
|
6949
7581
|
ObjectKernel,
|
|
6950
7582
|
ObjectKernelBase,
|
|
6951
7583
|
ObjectLogger,
|
|
7584
|
+
ObjectStoreMetadataActivationStore,
|
|
7585
|
+
PLATFORM_ADMIN_EMAIL_SEPARATOR,
|
|
6952
7586
|
POSTURE_INJECTION_RULE,
|
|
6953
7587
|
POSTURE_LADDER,
|
|
6954
7588
|
POSTURE_RANK,
|
|
6955
|
-
PluginConfigValidator,
|
|
6956
7589
|
PluginHealthMonitor,
|
|
6957
7590
|
PluginLoader,
|
|
6958
7591
|
PluginPermissionEnforcer,
|
|
@@ -6961,6 +7594,7 @@ var import_contracts = require("@objectstack/spec/contracts");
|
|
|
6961
7594
|
PluginSecurityScanner,
|
|
6962
7595
|
PluginSignatureVerifier,
|
|
6963
7596
|
QA,
|
|
7597
|
+
SERVICE_NOT_REGISTERED_CODE,
|
|
6964
7598
|
SIGNATURE_ALG,
|
|
6965
7599
|
SecurePluginContext,
|
|
6966
7600
|
SemanticVersionManager,
|
|
@@ -6970,6 +7604,7 @@ var import_contracts = require("@objectstack/spec/contracts");
|
|
|
6970
7604
|
UnresolvedFilterTokenError,
|
|
6971
7605
|
adminStandingColumns,
|
|
6972
7606
|
adminStandingTables,
|
|
7607
|
+
artifactPackageId,
|
|
6973
7608
|
assembleExecutionContext,
|
|
6974
7609
|
assembleExecutionContextOrGuest,
|
|
6975
7610
|
assertInitServiceRequirements,
|
|
@@ -6988,7 +7623,6 @@ var import_contracts = require("@objectstack/spec/contracts");
|
|
|
6988
7623
|
createMemoryJob,
|
|
6989
7624
|
createMemoryMetadata,
|
|
6990
7625
|
createMemoryQueue,
|
|
6991
|
-
createPluginConfigValidator,
|
|
6992
7626
|
createPluginPermissionEnforcer,
|
|
6993
7627
|
deepMerge,
|
|
6994
7628
|
defaultIsTransientError,
|
|
@@ -7000,48 +7634,67 @@ var import_contracts = require("@objectstack/spec/contracts");
|
|
|
7000
7634
|
extractApiKey,
|
|
7001
7635
|
filterTokenContextFrom,
|
|
7002
7636
|
findInterruptedRuns,
|
|
7637
|
+
formatIntegrityViolation,
|
|
7003
7638
|
generateApiKey,
|
|
7004
7639
|
generateEd25519KeyPair,
|
|
7005
7640
|
getEnv,
|
|
7006
7641
|
getMemoryUsage,
|
|
7642
|
+
hasPlatformAdminStanding,
|
|
7007
7643
|
hashApiKey,
|
|
7008
7644
|
hashMigrationPlan,
|
|
7009
7645
|
isAudienceBindingSuggestionStatus,
|
|
7010
7646
|
isAuthGateAllowlisted,
|
|
7647
|
+
isAuthzStoreUnavailableError,
|
|
7648
|
+
isConfiguredPlatformAdminEmail,
|
|
7011
7649
|
isExpired,
|
|
7012
7650
|
isGrantActive,
|
|
7013
7651
|
isGrantExpired,
|
|
7014
7652
|
isNode,
|
|
7015
7653
|
isRowActive,
|
|
7654
|
+
isServiceNotRegisteredError,
|
|
7016
7655
|
isUninterpretableTemporalComparand,
|
|
7656
|
+
matchesConfiguredPlatformAdmin,
|
|
7017
7657
|
nextUtcCalendarDay,
|
|
7018
7658
|
normalizeAuthGate,
|
|
7659
|
+
normalizePlatformAdminEmail,
|
|
7019
7660
|
omitInternalFieldsFromWriteResponse,
|
|
7661
|
+
parsePlatformAdminEmails,
|
|
7020
7662
|
parseScopes,
|
|
7021
7663
|
parseSignature,
|
|
7022
7664
|
planChunks,
|
|
7023
7665
|
postureVisibleRows,
|
|
7024
7666
|
readAuthoredTranslationLayer,
|
|
7667
|
+
readAuthzGrantsCacheTtlMs,
|
|
7025
7668
|
readRunJournal,
|
|
7026
7669
|
recordNotFoundError,
|
|
7670
|
+
reportAuthzCachePosture,
|
|
7671
|
+
reportLegacyPlatformAdminGrant,
|
|
7672
|
+
resetLegacyPlatformAdminGrantReport,
|
|
7673
|
+
resetPlatformAdminEmailMemo,
|
|
7027
7674
|
resolveApiKeyAdmission,
|
|
7028
7675
|
resolveApiKeyPrincipal,
|
|
7676
|
+
resolveArtifactPackageOrder,
|
|
7677
|
+
resolveAuthzCachePosture,
|
|
7029
7678
|
resolveAuthzContext,
|
|
7030
7679
|
resolveFilterToken,
|
|
7031
7680
|
resolveFilterTokens,
|
|
7032
7681
|
resolveLocale,
|
|
7033
7682
|
resolveLocalizationContext,
|
|
7683
|
+
resolvePlatformAdminEmails,
|
|
7034
7684
|
resolvePluginOrder,
|
|
7035
7685
|
resolveUserAuthzGrants,
|
|
7036
7686
|
resumeMigrationJournal,
|
|
7687
|
+
rethrowAuthzStoreUnavailable,
|
|
7037
7688
|
runMigrationJournal,
|
|
7038
7689
|
safeExit,
|
|
7690
|
+
setPlatformAdminConfigSink,
|
|
7039
7691
|
shouldDenyAnonymous,
|
|
7040
7692
|
signPayload,
|
|
7041
7693
|
temporalComparandKind,
|
|
7042
7694
|
unknownAudienceBindingSuggestionStatusMessage,
|
|
7043
7695
|
utcInstantMs,
|
|
7044
7696
|
validateInitServiceContract,
|
|
7697
|
+
verifyIntegrity,
|
|
7045
7698
|
verifyPayload,
|
|
7046
7699
|
verifyPlatformSignature,
|
|
7047
7700
|
verifyPluginArtifact,
|