@fulmenhq/tsfulmen 0.3.3 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +51 -1
- package/README.md +30 -1
- package/config/crucible-ts/taxonomy/metrics.yaml +1 -1
- package/dist/appidentity/index.js +9 -2209
- package/dist/appidentity/index.js.map +1 -1
- package/dist/assets/index.d.ts +179 -0
- package/dist/assets/index.js +4 -0
- package/dist/assets/index.js.map +1 -0
- package/dist/bin/prometheus-cli.js +15 -9139
- package/dist/bin/prometheus-cli.js.map +1 -1
- package/dist/bin/schema-cli.js +4 -6098
- package/dist/bin/schema-cli.js.map +1 -1
- package/dist/bin/signals-cli.js +15 -1910
- package/dist/bin/signals-cli.js.map +1 -1
- package/dist/chunk-3HISR2XI.js +36 -0
- package/dist/chunk-3HISR2XI.js.map +1 -0
- package/dist/chunk-5HZEJGZ5.js +725 -0
- package/dist/chunk-5HZEJGZ5.js.map +1 -0
- package/dist/chunk-7SVYOPJ3.js +9412 -0
- package/dist/chunk-7SVYOPJ3.js.map +1 -0
- package/dist/chunk-BCO3FUUO.js +682 -0
- package/dist/chunk-BCO3FUUO.js.map +1 -0
- package/dist/chunk-BJDL6JEX.js +208 -0
- package/dist/chunk-BJDL6JEX.js.map +1 -0
- package/dist/chunk-BTUS6PZZ.js +302 -0
- package/dist/chunk-BTUS6PZZ.js.map +1 -0
- package/dist/chunk-CW4CSNVF.js +410 -0
- package/dist/chunk-CW4CSNVF.js.map +1 -0
- package/dist/chunk-HIBZ2AY7.js +194 -0
- package/dist/chunk-HIBZ2AY7.js.map +1 -0
- package/dist/chunk-HS4F3BRR.js +985 -0
- package/dist/chunk-HS4F3BRR.js.map +1 -0
- package/dist/chunk-HY6JQOFN.js +32 -0
- package/dist/chunk-HY6JQOFN.js.map +1 -0
- package/dist/chunk-K2FYT43R.js +837 -0
- package/dist/chunk-K2FYT43R.js.map +1 -0
- package/dist/chunk-K7BMFGQP.js +43 -0
- package/dist/chunk-K7BMFGQP.js.map +1 -0
- package/dist/chunk-LWAH2UG7.js +103 -0
- package/dist/chunk-LWAH2UG7.js.map +1 -0
- package/dist/chunk-OHE2AOH4.js +273 -0
- package/dist/chunk-OHE2AOH4.js.map +1 -0
- package/dist/chunk-P3DNOZOG.js +1488 -0
- package/dist/chunk-P3DNOZOG.js.map +1 -0
- package/dist/chunk-PQ6TKPUN.js +27 -0
- package/dist/chunk-PQ6TKPUN.js.map +1 -0
- package/dist/chunk-QCCN6POA.js +211 -0
- package/dist/chunk-QCCN6POA.js.map +1 -0
- package/dist/chunk-RSLFR4TO.js +631 -0
- package/dist/chunk-RSLFR4TO.js.map +1 -0
- package/dist/chunk-UP2VWCW5.js +33 -0
- package/dist/chunk-UP2VWCW5.js.map +1 -0
- package/dist/chunk-W6TQFTTY.js +130 -0
- package/dist/chunk-W6TQFTTY.js.map +1 -0
- package/dist/chunk-WSMVLWLB.js +269 -0
- package/dist/chunk-WSMVLWLB.js.map +1 -0
- package/dist/config/index.js +6 -1220
- package/dist/config/index.js.map +1 -1
- package/dist/crucible/fulpack/index.js +2 -25
- package/dist/crucible/fulpack/index.js.map +1 -1
- package/dist/crucible/index.js +13 -1129
- package/dist/crucible/index.js.map +1 -1
- package/dist/docscribe/index.js +2 -680
- package/dist/docscribe/index.js.map +1 -1
- package/dist/errors/index.js +5 -1831
- package/dist/errors/index.js.map +1 -1
- package/dist/errors-D7KXV3QW.js +4 -0
- package/dist/errors-D7KXV3QW.js.map +1 -0
- package/dist/errors-RBJX6ETJ.js +8 -0
- package/dist/errors-RBJX6ETJ.js.map +1 -0
- package/dist/export-BWRETGYS.js +6 -0
- package/dist/export-BWRETGYS.js.map +1 -0
- package/dist/foundry/index.js +340 -3949
- package/dist/foundry/index.js.map +1 -1
- package/dist/foundry/similarity/index.js +3 -134
- package/dist/foundry/similarity/index.js.map +1 -1
- package/dist/fulencode/index.js +3 -319
- package/dist/fulencode/index.js.map +1 -1
- package/dist/fulhash/index.js +3 -611
- package/dist/fulhash/index.js.map +1 -1
- package/dist/fulpack/index.js +3 -1515
- package/dist/fulpack/index.js.map +1 -1
- package/dist/hash-YOJYVOWH.js +4 -0
- package/dist/hash-YOJYVOWH.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +14 -4395
- package/dist/index.js.map +1 -1
- package/dist/loader-H5K7HSO5.js +9 -0
- package/dist/loader-H5K7HSO5.js.map +1 -0
- package/dist/logging/index.js +2 -723
- package/dist/logging/index.js.map +1 -1
- package/dist/pathfinder/index.js +18 -2341
- package/dist/pathfinder/index.js.map +1 -1
- package/dist/prom-client-PNEBGD3G.js +4801 -0
- package/dist/prom-client-PNEBGD3G.js.map +1 -0
- package/dist/reports/license-inventory.csv +1 -1
- package/dist/schema/index.d.ts +19 -6
- package/dist/schema/index.js +4 -6148
- package/dist/schema/index.js.map +1 -1
- package/dist/signals/index.js +8 -2767
- package/dist/signals/index.js.map +1 -1
- package/dist/similarity/index.js +3 -134
- package/dist/similarity/index.js.map +1 -1
- package/dist/telemetry/http/index.js +9 -930
- package/dist/telemetry/http/index.js.map +1 -1
- package/dist/telemetry/index.js +4 -1561
- package/dist/telemetry/index.js.map +1 -1
- package/dist/telemetry/prometheus/index.js +7 -8422
- package/dist/telemetry/prometheus/index.js.map +1 -1
- package/dist/validator-U4CKD2Y3.js +6 -0
- package/dist/validator-U4CKD2Y3.js.map +1 -0
- package/package.json +5 -1
- package/schemas/crucible-ts/library/module-manifest/v1.0.0/module-manifest.schema.json +1 -1
- package/schemas/crucible-ts/observability/logging/v1.0.0/definitions.schema.json +1 -1
- package/schemas/crucible-ts/observability/logging/v1.0.0/log-event.schema.json +13 -13
- package/schemas/crucible-ts/observability/logging/v1.0.0/logger-config.schema.json +2 -2
- package/schemas/crucible-ts/observability/logging/v1.0.0/logging-policy.schema.json +1 -1
- package/schemas/crucible-ts/observability/logging/v1.0.0/middleware-config.schema.json +2 -2
- package/schemas/crucible-ts/observability/logging/v1.0.0/severity-filter.schema.json +3 -3
- package/schemas/crucible-ts/upstream/3leaps/crucible/PROVENANCE.md +3 -3
- package/schemas/crucible-ts/upstream/3leaps/crucible/docs/catalog/classifiers/README.md +1 -1
- package/schemas/crucible-ts/upstream/3leaps/crucible/docs/standards/classifiers-framework.md +1 -1
- package/schemas/crucible-ts/upstream/3leaps/crucible/docs/standards/data-sensitivity-classification.md +1 -1
|
@@ -0,0 +1,985 @@
|
|
|
1
|
+
import { getSignal, supportsSignal, isWindows, isPOSIX } from './chunk-HIBZ2AY7.js';
|
|
2
|
+
import { buildRuntimeInfo } from './chunk-HY6JQOFN.js';
|
|
3
|
+
import { FoundryCatalogError } from './chunk-3HISR2XI.js';
|
|
4
|
+
|
|
5
|
+
// src/foundry/signals/config-reload-endpoint.ts
|
|
6
|
+
function createConfigReloadEndpoint(options) {
|
|
7
|
+
const { loader, validator, onReload: onReload2, auth, rateLimit, logger, telemetry } = options;
|
|
8
|
+
return async (payload, req) => {
|
|
9
|
+
const correlationId = payload.correlation_id ?? generateCorrelationId();
|
|
10
|
+
const authResult = await auth(req);
|
|
11
|
+
if (!authResult.authenticated) {
|
|
12
|
+
if (logger) {
|
|
13
|
+
logger.warn("Config reload endpoint: authentication failed", {
|
|
14
|
+
correlation_id: correlationId,
|
|
15
|
+
reason: authResult.reason
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
if (telemetry) {
|
|
19
|
+
telemetry.emit("fulmen.config.http_endpoint.auth_failed", {
|
|
20
|
+
correlation_id: correlationId
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
status: "error",
|
|
25
|
+
error: "authentication_failed",
|
|
26
|
+
message: authResult.reason || "Authentication required",
|
|
27
|
+
statusCode: 401
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
const identity = authResult.identity || "unknown";
|
|
31
|
+
if (rateLimit) {
|
|
32
|
+
const rateLimitResult = await rateLimit(identity);
|
|
33
|
+
if (!rateLimitResult.allowed) {
|
|
34
|
+
if (logger) {
|
|
35
|
+
logger.warn("Config reload endpoint: rate limit exceeded", {
|
|
36
|
+
correlation_id: correlationId,
|
|
37
|
+
identity
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
if (telemetry) {
|
|
41
|
+
telemetry.emit("fulmen.config.http_endpoint.rate_limited", {
|
|
42
|
+
correlation_id: correlationId
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
status: "error",
|
|
47
|
+
error: "rate_limit_exceeded",
|
|
48
|
+
message: "Rate limit exceeded. Please try again later.",
|
|
49
|
+
statusCode: 429
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (telemetry) {
|
|
54
|
+
telemetry.emit("fulmen.config.http_endpoint.reload_requested", {
|
|
55
|
+
correlation_id: correlationId
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
const config = await loader();
|
|
60
|
+
if (validator) {
|
|
61
|
+
const validation = await validator(config);
|
|
62
|
+
if (!validation.valid) {
|
|
63
|
+
if (logger) {
|
|
64
|
+
logger.warn("Config reload endpoint: validation failed", {
|
|
65
|
+
correlation_id: correlationId,
|
|
66
|
+
error_count: validation.errors?.length ?? 0
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
if (telemetry) {
|
|
70
|
+
telemetry.emit("fulmen.config.http_endpoint.reload_rejected", {
|
|
71
|
+
correlation_id: correlationId,
|
|
72
|
+
reason: "validation_failed"
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
status: "error",
|
|
77
|
+
error: "validation_failed",
|
|
78
|
+
message: "Configuration validation failed",
|
|
79
|
+
validation_errors: validation.errors,
|
|
80
|
+
statusCode: 422
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (onReload2) {
|
|
85
|
+
await onReload2(config);
|
|
86
|
+
}
|
|
87
|
+
if (telemetry) {
|
|
88
|
+
telemetry.emit("fulmen.config.http_endpoint.reload_accepted", {
|
|
89
|
+
correlation_id: correlationId
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
if (logger) {
|
|
93
|
+
logger.info("Config reload endpoint: reload accepted", {
|
|
94
|
+
correlation_id: correlationId,
|
|
95
|
+
reason: payload.reason
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
status: "reloaded",
|
|
100
|
+
correlation_id: correlationId,
|
|
101
|
+
message: "Configuration reloaded",
|
|
102
|
+
statusCode: 200
|
|
103
|
+
};
|
|
104
|
+
} catch (error) {
|
|
105
|
+
if (logger) {
|
|
106
|
+
logger.warn("Config reload endpoint: reload failed", {
|
|
107
|
+
correlation_id: correlationId,
|
|
108
|
+
error: error instanceof Error ? error.message : String(error)
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
if (telemetry) {
|
|
112
|
+
telemetry.emit("fulmen.config.http_endpoint.reload_error", {
|
|
113
|
+
correlation_id: correlationId,
|
|
114
|
+
error_type: error instanceof Error ? error.constructor.name : "unknown"
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
status: "error",
|
|
119
|
+
error: "reload_failed",
|
|
120
|
+
message: error instanceof Error ? error.message : String(error),
|
|
121
|
+
statusCode: 500
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
function generateCorrelationId() {
|
|
127
|
+
return `cfg-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// src/foundry/signals/control-discovery-endpoint.ts
|
|
131
|
+
function createControlDiscoveryEndpoint(options) {
|
|
132
|
+
const { identity, version, endpoints, auth, authSummary, logger, telemetry } = options;
|
|
133
|
+
return async (req) => {
|
|
134
|
+
if (auth) {
|
|
135
|
+
const authResult = await auth(req);
|
|
136
|
+
if (!authResult.authenticated) {
|
|
137
|
+
if (logger) {
|
|
138
|
+
logger.warn("Control discovery endpoint: authentication failed", {
|
|
139
|
+
reason: authResult.reason
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
if (telemetry) {
|
|
143
|
+
telemetry.emit("fulmen.control.discovery.auth_failed", {
|
|
144
|
+
service: identity.app.binary_name
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
return {
|
|
148
|
+
status: "error",
|
|
149
|
+
error: "authentication_failed",
|
|
150
|
+
message: authResult.reason || "Authentication required",
|
|
151
|
+
statusCode: 401
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
if (telemetry) {
|
|
156
|
+
telemetry.emit("fulmen.control.discovery.served", {
|
|
157
|
+
service: identity.app.binary_name
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
const runtime = buildRuntimeInfo({ identity, version });
|
|
161
|
+
return {
|
|
162
|
+
status: "ok",
|
|
163
|
+
service: {
|
|
164
|
+
name: identity.app.binary_name,
|
|
165
|
+
vendor: identity.app.vendor,
|
|
166
|
+
version
|
|
167
|
+
},
|
|
168
|
+
runtime: {
|
|
169
|
+
name: runtime.runtime.name,
|
|
170
|
+
version: runtime.runtime.version,
|
|
171
|
+
platform: runtime.platform.os,
|
|
172
|
+
arch: runtime.platform.arch
|
|
173
|
+
},
|
|
174
|
+
auth_summary: authSummary,
|
|
175
|
+
endpoints,
|
|
176
|
+
statusCode: 200
|
|
177
|
+
};
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// src/foundry/signals/convenience.ts
|
|
182
|
+
async function onShutdown(manager, handler, options = {}) {
|
|
183
|
+
await manager.register("SIGTERM", handler, options);
|
|
184
|
+
await manager.register("SIGINT", handler, options);
|
|
185
|
+
}
|
|
186
|
+
async function onReload(manager, handler, options = {}) {
|
|
187
|
+
await manager.register("SIGHUP", handler, options);
|
|
188
|
+
}
|
|
189
|
+
async function onUSR1(manager, handler, options = {}) {
|
|
190
|
+
await manager.register("SIGUSR1", handler, options);
|
|
191
|
+
}
|
|
192
|
+
async function onUSR2(manager, handler, options = {}) {
|
|
193
|
+
await manager.register("SIGUSR2", handler, options);
|
|
194
|
+
}
|
|
195
|
+
async function onEmergencyQuit(manager, handler, options = {}) {
|
|
196
|
+
await manager.register("SIGQUIT", handler, options);
|
|
197
|
+
}
|
|
198
|
+
async function onAnyShutdown(manager, handler, options = {}) {
|
|
199
|
+
await manager.register("SIGTERM", handler, options);
|
|
200
|
+
await manager.register("SIGINT", handler, options);
|
|
201
|
+
await manager.register("SIGQUIT", handler, options);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// src/foundry/signals/double-tap.ts
|
|
205
|
+
async function createDoubleTapTracker(signalName, config = {}) {
|
|
206
|
+
const signal = await getSignal(signalName);
|
|
207
|
+
const defaultWindowMs = signal?.double_tap_window_seconds ? signal.double_tap_window_seconds * 1e3 : 2e3;
|
|
208
|
+
const defaultExitCode = signal?.double_tap_exit_code || 130;
|
|
209
|
+
const defaultHintMessage = signal?.double_tap_message || "Press Ctrl+C again within 2s to force quit";
|
|
210
|
+
return {
|
|
211
|
+
firstTapTime: null,
|
|
212
|
+
windowMs: config.windowMs ?? defaultWindowMs,
|
|
213
|
+
exitCode: config.exitCode ?? defaultExitCode,
|
|
214
|
+
hintMessage: config.hintMessage ?? defaultHintMessage,
|
|
215
|
+
logger: config.logger,
|
|
216
|
+
testMode: config.testMode ?? false
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
function handleDoubleTap(state) {
|
|
220
|
+
const now = Date.now();
|
|
221
|
+
if (state.firstTapTime === null) {
|
|
222
|
+
state.firstTapTime = now;
|
|
223
|
+
if (state.logger) {
|
|
224
|
+
state.logger.info(state.hintMessage);
|
|
225
|
+
} else {
|
|
226
|
+
console.log(state.hintMessage);
|
|
227
|
+
}
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
const elapsed = now - state.firstTapTime;
|
|
231
|
+
if (elapsed < state.windowMs) {
|
|
232
|
+
if (state.logger) {
|
|
233
|
+
state.logger.info("Force quitting...");
|
|
234
|
+
} else {
|
|
235
|
+
console.log("Force quitting...");
|
|
236
|
+
}
|
|
237
|
+
if (!state.testMode) {
|
|
238
|
+
process.exit(state.exitCode);
|
|
239
|
+
}
|
|
240
|
+
return true;
|
|
241
|
+
}
|
|
242
|
+
state.firstTapTime = now;
|
|
243
|
+
if (state.logger) {
|
|
244
|
+
state.logger.info(state.hintMessage);
|
|
245
|
+
} else {
|
|
246
|
+
console.log(state.hintMessage);
|
|
247
|
+
}
|
|
248
|
+
return false;
|
|
249
|
+
}
|
|
250
|
+
function resetDoubleTap(state) {
|
|
251
|
+
state.firstTapTime = null;
|
|
252
|
+
}
|
|
253
|
+
function isWithinWindow(state) {
|
|
254
|
+
if (state.firstTapTime === null) {
|
|
255
|
+
return false;
|
|
256
|
+
}
|
|
257
|
+
const elapsed = Date.now() - state.firstTapTime;
|
|
258
|
+
return elapsed < state.windowMs;
|
|
259
|
+
}
|
|
260
|
+
function getWindowTimeRemaining(state) {
|
|
261
|
+
if (state.firstTapTime === null) {
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
const elapsed = Date.now() - state.firstTapTime;
|
|
265
|
+
const remaining = state.windowMs - elapsed;
|
|
266
|
+
return remaining > 0 ? remaining : null;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// src/foundry/signals/windows.ts
|
|
270
|
+
var defaultLogger = {
|
|
271
|
+
info(message, meta) {
|
|
272
|
+
if (meta) {
|
|
273
|
+
console.info(message, meta);
|
|
274
|
+
} else {
|
|
275
|
+
console.info(message);
|
|
276
|
+
}
|
|
277
|
+
},
|
|
278
|
+
warn(message, meta) {
|
|
279
|
+
if (meta) {
|
|
280
|
+
console.warn(message, meta);
|
|
281
|
+
} else {
|
|
282
|
+
console.warn(message);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
async function handleWindowsFallback(signalName, options = {}) {
|
|
287
|
+
const signal = await getSignal(signalName);
|
|
288
|
+
if (!signal) {
|
|
289
|
+
return {
|
|
290
|
+
supported: false,
|
|
291
|
+
logged: false
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
if (signal.windows_event !== null) {
|
|
295
|
+
return {
|
|
296
|
+
supported: true,
|
|
297
|
+
logged: false
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
const fallback = signal.windows_fallback;
|
|
301
|
+
if (!fallback) {
|
|
302
|
+
return {
|
|
303
|
+
supported: false,
|
|
304
|
+
logged: false
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
if (options.silent) {
|
|
308
|
+
return {
|
|
309
|
+
supported: false,
|
|
310
|
+
fallback,
|
|
311
|
+
logged: false
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
const logger = options.logger || defaultLogger;
|
|
315
|
+
const telemetry = options.telemetry;
|
|
316
|
+
const logMeta = {
|
|
317
|
+
signal: signal.name,
|
|
318
|
+
platform: "windows",
|
|
319
|
+
fallback: fallback.fallback_behavior,
|
|
320
|
+
operation_hint: fallback.operation_hint
|
|
321
|
+
};
|
|
322
|
+
logger.info(fallback.log_message, logMeta);
|
|
323
|
+
if (telemetry) {
|
|
324
|
+
telemetry.emit(fallback.telemetry_event, fallback.telemetry_tags);
|
|
325
|
+
}
|
|
326
|
+
return {
|
|
327
|
+
supported: false,
|
|
328
|
+
fallback,
|
|
329
|
+
logged: true
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
async function getFallbackMetadata(signalName) {
|
|
333
|
+
const signal = await getSignal(signalName);
|
|
334
|
+
if (!signal || !signal.windows_fallback) {
|
|
335
|
+
return null;
|
|
336
|
+
}
|
|
337
|
+
return signal.windows_fallback;
|
|
338
|
+
}
|
|
339
|
+
async function requiresFallback(signalName) {
|
|
340
|
+
const signal = await getSignal(signalName);
|
|
341
|
+
if (!signal) {
|
|
342
|
+
return false;
|
|
343
|
+
}
|
|
344
|
+
return signal.windows_event === null && signal.windows_fallback !== void 0;
|
|
345
|
+
}
|
|
346
|
+
async function getHttpFallbackGuidance(signalName) {
|
|
347
|
+
const fallback = await getFallbackMetadata(signalName);
|
|
348
|
+
if (!fallback || fallback.fallback_behavior !== "http_admin_endpoint") {
|
|
349
|
+
return null;
|
|
350
|
+
}
|
|
351
|
+
return fallback.operation_hint;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// src/foundry/signals/guards.ts
|
|
355
|
+
async function ensureSupported(signalName, options = {}) {
|
|
356
|
+
const { includeGuidance = true } = options;
|
|
357
|
+
const signal = await getSignal(signalName);
|
|
358
|
+
if (!signal) {
|
|
359
|
+
throw FoundryCatalogError.invalidSchema(
|
|
360
|
+
"signals",
|
|
361
|
+
`Signal "${signalName}" not found in catalog. Valid signals: SIGTERM, SIGINT, SIGHUP, SIGQUIT, SIGPIPE, SIGALRM, SIGUSR1, SIGUSR2`
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
const supported = await supportsSignal(signalName);
|
|
365
|
+
if (supported) {
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
let message = `Signal ${signal.name} is not supported on this platform`;
|
|
369
|
+
if (isWindows() && includeGuidance) {
|
|
370
|
+
const fallback = await getFallbackMetadata(signalName);
|
|
371
|
+
if (fallback) {
|
|
372
|
+
message += `. ${fallback.log_message}`;
|
|
373
|
+
switch (fallback.fallback_behavior) {
|
|
374
|
+
case "http_admin_endpoint":
|
|
375
|
+
message += `. Use HTTP endpoint: ${fallback.operation_hint}`;
|
|
376
|
+
break;
|
|
377
|
+
case "exception_handling":
|
|
378
|
+
message += `. Alternative: ${fallback.operation_hint}`;
|
|
379
|
+
break;
|
|
380
|
+
case "timer_api":
|
|
381
|
+
message += `. Alternative: ${fallback.operation_hint}`;
|
|
382
|
+
break;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
throw FoundryCatalogError.invalidSchema("signals", message);
|
|
387
|
+
}
|
|
388
|
+
function ensureSignalExitCodesSupported() {
|
|
389
|
+
if (!isPOSIX()) {
|
|
390
|
+
throw FoundryCatalogError.invalidSchema(
|
|
391
|
+
"signals",
|
|
392
|
+
"Signal-based exit codes (128+N pattern) are not supported on this platform. Windows does not propagate signal numbers via exit codes. Use explicit exit codes or monitor via HTTP admin endpoint."
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
function ensurePOSIX() {
|
|
397
|
+
if (!isPOSIX()) {
|
|
398
|
+
throw FoundryCatalogError.invalidSchema(
|
|
399
|
+
"signals",
|
|
400
|
+
"This operation requires a POSIX-compliant platform (Linux, macOS, FreeBSD). Current platform does not support native signal handling."
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
function ensureWindows() {
|
|
405
|
+
if (!isWindows()) {
|
|
406
|
+
throw FoundryCatalogError.invalidSchema(
|
|
407
|
+
"signals",
|
|
408
|
+
"This operation requires Windows platform. Current platform uses native signals."
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// src/foundry/signals/http-helper.ts
|
|
414
|
+
function createSignalEndpoint(options) {
|
|
415
|
+
const { manager, auth, rateLimit, logger, telemetry, allowedSignals } = options;
|
|
416
|
+
return async (payload, req) => {
|
|
417
|
+
const correlationId = payload.correlation_id ?? generateCorrelationId2();
|
|
418
|
+
const authResult = await auth(req);
|
|
419
|
+
if (!authResult.authenticated) {
|
|
420
|
+
if (logger) {
|
|
421
|
+
logger.warn("Signal endpoint: authentication failed", {
|
|
422
|
+
correlation_id: correlationId,
|
|
423
|
+
reason: authResult.reason
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
if (telemetry) {
|
|
427
|
+
telemetry.emit("fulmen.signal.http_endpoint.auth_failed", {
|
|
428
|
+
correlation_id: correlationId
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
return {
|
|
432
|
+
status: "error",
|
|
433
|
+
error: "authentication_failed",
|
|
434
|
+
message: authResult.reason || "Authentication required",
|
|
435
|
+
statusCode: 401
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
const identity = authResult.identity || "unknown";
|
|
439
|
+
if (!payload.signal) {
|
|
440
|
+
return {
|
|
441
|
+
status: "error",
|
|
442
|
+
error: "invalid_request",
|
|
443
|
+
message: "Signal name is required",
|
|
444
|
+
statusCode: 400
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
const signalName = normalizeSignalName(payload.signal);
|
|
448
|
+
const defaultAllowed = [
|
|
449
|
+
"SIGHUP",
|
|
450
|
+
"SIGTERM",
|
|
451
|
+
"SIGINT",
|
|
452
|
+
"SIGQUIT",
|
|
453
|
+
"SIGUSR1",
|
|
454
|
+
"SIGUSR2",
|
|
455
|
+
"SIGPIPE",
|
|
456
|
+
"SIGALRM"
|
|
457
|
+
];
|
|
458
|
+
const allowed = (allowedSignals || defaultAllowed).map((s) => normalizeSignalName(s));
|
|
459
|
+
if (!allowed.includes(signalName)) {
|
|
460
|
+
return {
|
|
461
|
+
status: "error",
|
|
462
|
+
error: "invalid_signal",
|
|
463
|
+
message: `Signal '${payload.signal}' is not recognized. Valid signals: ${allowed.join(", ")}`,
|
|
464
|
+
valid_signals: allowed,
|
|
465
|
+
statusCode: 400
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
const supported = await supportsSignal(signalName);
|
|
469
|
+
if (!supported) {
|
|
470
|
+
return {
|
|
471
|
+
status: "error",
|
|
472
|
+
error: "signal_not_supported",
|
|
473
|
+
message: `Signal ${signalName} is not supported on this platform`,
|
|
474
|
+
statusCode: 400
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
if (rateLimit) {
|
|
478
|
+
const rateLimitResult = await rateLimit(identity, signalName);
|
|
479
|
+
if (!rateLimitResult.allowed) {
|
|
480
|
+
if (logger) {
|
|
481
|
+
logger.warn("Signal endpoint: rate limit exceeded", {
|
|
482
|
+
correlation_id: correlationId,
|
|
483
|
+
identity,
|
|
484
|
+
signal: signalName
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
if (telemetry) {
|
|
488
|
+
telemetry.emit("fulmen.signal.http_endpoint.rate_limited", {
|
|
489
|
+
correlation_id: correlationId,
|
|
490
|
+
signal: signalName
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
return {
|
|
494
|
+
status: "error",
|
|
495
|
+
error: "rate_limit_exceeded",
|
|
496
|
+
message: "Rate limit exceeded. Please try again later.",
|
|
497
|
+
statusCode: 429
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
if (logger) {
|
|
502
|
+
logger.info("Signal endpoint: signal received", {
|
|
503
|
+
correlation_id: correlationId,
|
|
504
|
+
identity,
|
|
505
|
+
signal: signalName,
|
|
506
|
+
reason: payload.reason
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
if (telemetry) {
|
|
510
|
+
telemetry.emit("fulmen.signal.http_endpoint.signal_received", {
|
|
511
|
+
correlation_id: correlationId,
|
|
512
|
+
signal: signalName
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
void manager.trigger(signalName).catch((error) => {
|
|
516
|
+
if (logger) {
|
|
517
|
+
logger.warn("Signal handler execution failed", {
|
|
518
|
+
correlation_id: correlationId,
|
|
519
|
+
signal: signalName,
|
|
520
|
+
error: error instanceof Error ? error.message : String(error)
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
});
|
|
524
|
+
return {
|
|
525
|
+
status: "accepted",
|
|
526
|
+
signal: signalName,
|
|
527
|
+
correlation_id: correlationId,
|
|
528
|
+
message: "Signal will be processed asynchronously",
|
|
529
|
+
statusCode: 202
|
|
530
|
+
};
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
function normalizeSignalName(signal) {
|
|
534
|
+
const upper = signal.toUpperCase();
|
|
535
|
+
if (upper.startsWith("SIG")) {
|
|
536
|
+
return upper;
|
|
537
|
+
}
|
|
538
|
+
return `SIG${upper}`;
|
|
539
|
+
}
|
|
540
|
+
function generateCorrelationId2() {
|
|
541
|
+
return `sig-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
|
|
542
|
+
}
|
|
543
|
+
function createBearerTokenAuth(expectedToken) {
|
|
544
|
+
return (req) => {
|
|
545
|
+
const headers = req.headers;
|
|
546
|
+
const authHeader = headers?.authorization || headers?.Authorization;
|
|
547
|
+
if (!authHeader) {
|
|
548
|
+
return {
|
|
549
|
+
authenticated: false,
|
|
550
|
+
reason: "Missing Authorization header"
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
const [scheme, token] = authHeader.split(" ");
|
|
554
|
+
if (scheme !== "Bearer" || token !== expectedToken) {
|
|
555
|
+
return {
|
|
556
|
+
authenticated: false,
|
|
557
|
+
reason: "Invalid bearer token"
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
return {
|
|
561
|
+
authenticated: true,
|
|
562
|
+
identity: "bearer-token-user"
|
|
563
|
+
};
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
function createSimpleRateLimiter(requestsPerMinute) {
|
|
567
|
+
const requests = /* @__PURE__ */ new Map();
|
|
568
|
+
const windowMs = 6e4;
|
|
569
|
+
return (identity) => {
|
|
570
|
+
const now = Date.now();
|
|
571
|
+
const key = identity;
|
|
572
|
+
let timestamps = requests.get(key) || [];
|
|
573
|
+
timestamps = timestamps.filter((ts) => now - ts < windowMs);
|
|
574
|
+
if (timestamps.length >= requestsPerMinute) {
|
|
575
|
+
const oldestTimestamp = Math.min(...timestamps);
|
|
576
|
+
const resetAt = oldestTimestamp + windowMs;
|
|
577
|
+
return {
|
|
578
|
+
allowed: false,
|
|
579
|
+
remaining: 0,
|
|
580
|
+
reset_at: resetAt
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
timestamps.push(now);
|
|
584
|
+
requests.set(key, timestamps);
|
|
585
|
+
return {
|
|
586
|
+
allowed: true,
|
|
587
|
+
remaining: requestsPerMinute - timestamps.length,
|
|
588
|
+
reset_at: now + windowMs
|
|
589
|
+
};
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// src/foundry/signals/manager.ts
|
|
594
|
+
var SignalManager = class {
|
|
595
|
+
handlers = /* @__PURE__ */ new Map();
|
|
596
|
+
nativeListeners = /* @__PURE__ */ new Map();
|
|
597
|
+
options;
|
|
598
|
+
handlerIdCounter = 0;
|
|
599
|
+
doubleTapTrackers = /* @__PURE__ */ new Map();
|
|
600
|
+
shuttingDown = false;
|
|
601
|
+
constructor(options = {}) {
|
|
602
|
+
this.options = {
|
|
603
|
+
defaultTimeoutMs: options.defaultTimeoutMs ?? 3e4,
|
|
604
|
+
timeoutBehavior: options.timeoutBehavior ?? "log_and_continue",
|
|
605
|
+
testMode: options.testMode ?? false,
|
|
606
|
+
doubleTapWindowMs: options.doubleTapWindowMs ?? 2e3,
|
|
607
|
+
doubleTapExitCode: options.doubleTapExitCode ?? 130,
|
|
608
|
+
logger: options.logger,
|
|
609
|
+
telemetry: options.telemetry
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
/**
|
|
613
|
+
* Register a signal handler
|
|
614
|
+
*
|
|
615
|
+
* @param signal - Signal name (e.g., "SIGTERM") or NodeJS.Signals
|
|
616
|
+
* @param handler - Handler function
|
|
617
|
+
* @param options - Handler options
|
|
618
|
+
*/
|
|
619
|
+
async register(signal, handler, options = {}) {
|
|
620
|
+
const signalName = typeof signal === "string" ? signal : signal;
|
|
621
|
+
const supported = await supportsSignal(signalName);
|
|
622
|
+
if (!supported) {
|
|
623
|
+
if (isWindows()) {
|
|
624
|
+
await handleWindowsFallback(signalName, {
|
|
625
|
+
logger: this.options.logger,
|
|
626
|
+
telemetry: this.options.telemetry
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
if (!this.handlers.has(signalName)) {
|
|
632
|
+
this.handlers.set(signalName, []);
|
|
633
|
+
}
|
|
634
|
+
const registration = {
|
|
635
|
+
signal: signalName,
|
|
636
|
+
handler,
|
|
637
|
+
priority: options.priority ?? 0,
|
|
638
|
+
timeoutMs: options.timeoutMs ?? this.options.defaultTimeoutMs,
|
|
639
|
+
id: options.id ?? `handler-${++this.handlerIdCounter}`,
|
|
640
|
+
registeredAt: Date.now()
|
|
641
|
+
};
|
|
642
|
+
const handlers = this.handlers.get(signalName);
|
|
643
|
+
if (!handlers) {
|
|
644
|
+
throw new Error(`Handler list not initialized for signal ${signalName}`);
|
|
645
|
+
}
|
|
646
|
+
handlers.push(registration);
|
|
647
|
+
handlers.sort((a, b) => {
|
|
648
|
+
if (a.priority !== b.priority) {
|
|
649
|
+
return b.priority - a.priority;
|
|
650
|
+
}
|
|
651
|
+
return a.registeredAt - b.registeredAt;
|
|
652
|
+
});
|
|
653
|
+
if (!this.nativeListeners.has(signalName)) {
|
|
654
|
+
await this.registerNativeListener(signalName);
|
|
655
|
+
}
|
|
656
|
+
if (this.options.logger) {
|
|
657
|
+
this.options.logger.info(`Signal handler registered: ${signalName}`, {
|
|
658
|
+
handler_id: registration.id,
|
|
659
|
+
priority: registration.priority,
|
|
660
|
+
timeout_ms: registration.timeoutMs
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
if (this.options.telemetry) {
|
|
664
|
+
this.options.telemetry.emit("fulmen.signal.handler_registered", {
|
|
665
|
+
signal: signalName,
|
|
666
|
+
handler_id: registration.id,
|
|
667
|
+
priority: String(registration.priority)
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
/**
|
|
672
|
+
* Register native Node.js signal listener
|
|
673
|
+
*/
|
|
674
|
+
async registerNativeListener(signalName) {
|
|
675
|
+
const signal = await getSignal(signalName);
|
|
676
|
+
if (!signal) {
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
const useDoubleTap = signal.default_behavior === "graceful_shutdown_with_double_tap";
|
|
680
|
+
if (useDoubleTap) {
|
|
681
|
+
const tracker = await createDoubleTapTracker(signalName, {
|
|
682
|
+
windowMs: this.options.doubleTapWindowMs,
|
|
683
|
+
exitCode: this.options.doubleTapExitCode,
|
|
684
|
+
logger: this.options.logger,
|
|
685
|
+
testMode: this.options.testMode
|
|
686
|
+
});
|
|
687
|
+
this.doubleTapTrackers.set(signalName, tracker);
|
|
688
|
+
}
|
|
689
|
+
const listener = async (sig) => {
|
|
690
|
+
if (useDoubleTap) {
|
|
691
|
+
const tracker = this.doubleTapTrackers.get(signalName);
|
|
692
|
+
if (tracker) {
|
|
693
|
+
const forceQuit = handleDoubleTap(tracker);
|
|
694
|
+
if (forceQuit) {
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
await this.executeHandlers(signalName, sig);
|
|
700
|
+
};
|
|
701
|
+
process.on(signalName, listener);
|
|
702
|
+
this.nativeListeners.set(signalName, listener);
|
|
703
|
+
}
|
|
704
|
+
/**
|
|
705
|
+
* Execute all registered handlers for a signal
|
|
706
|
+
*/
|
|
707
|
+
async executeHandlers(signalName, signal) {
|
|
708
|
+
const handlers = this.handlers.get(signalName);
|
|
709
|
+
if (!handlers || handlers.length === 0) {
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
this.shuttingDown = true;
|
|
713
|
+
if (this.options.logger) {
|
|
714
|
+
this.options.logger.info(`Signal received: ${signalName}`, {
|
|
715
|
+
handler_count: handlers.length
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
for (const registration of handlers) {
|
|
719
|
+
try {
|
|
720
|
+
await this.executeWithTimeout(registration, signal);
|
|
721
|
+
} catch (error) {
|
|
722
|
+
if (this.options.logger) {
|
|
723
|
+
this.options.logger.warn(`Signal handler failed: ${signalName}`, {
|
|
724
|
+
handler_id: registration.id,
|
|
725
|
+
error: error instanceof Error ? error.message : String(error)
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
if (this.options.telemetry) {
|
|
729
|
+
this.options.telemetry.emit("fulmen.signal.handler_error", {
|
|
730
|
+
signal: signalName,
|
|
731
|
+
handler_id: registration.id,
|
|
732
|
+
error_type: error instanceof Error ? error.constructor.name : "unknown"
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
if (this.options.timeoutBehavior === "force_exit") {
|
|
736
|
+
const exitCode = (await getSignal(signalName))?.exit_code ?? 1;
|
|
737
|
+
if (!this.options.testMode) {
|
|
738
|
+
process.exit(exitCode);
|
|
739
|
+
}
|
|
740
|
+
return;
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
const tracker = this.doubleTapTrackers.get(signalName);
|
|
745
|
+
if (tracker) {
|
|
746
|
+
resetDoubleTap(tracker);
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
/**
|
|
750
|
+
* Execute a handler with timeout enforcement
|
|
751
|
+
*/
|
|
752
|
+
async executeWithTimeout(registration, signal) {
|
|
753
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
754
|
+
setTimeout(() => {
|
|
755
|
+
reject(new Error(`Handler timeout exceeded: ${registration.timeoutMs}ms`));
|
|
756
|
+
}, registration.timeoutMs);
|
|
757
|
+
});
|
|
758
|
+
const handlerPromise = Promise.resolve(registration.handler(signal));
|
|
759
|
+
try {
|
|
760
|
+
await Promise.race([handlerPromise, timeoutPromise]);
|
|
761
|
+
if (this.options.telemetry) {
|
|
762
|
+
this.options.telemetry.emit("fulmen.signal.handler_completed", {
|
|
763
|
+
signal: registration.signal,
|
|
764
|
+
handler_id: registration.id
|
|
765
|
+
});
|
|
766
|
+
}
|
|
767
|
+
} catch (error) {
|
|
768
|
+
if (this.options.telemetry) {
|
|
769
|
+
this.options.telemetry.emit("fulmen.signal.handler_timeout", {
|
|
770
|
+
signal: registration.signal,
|
|
771
|
+
handler_id: registration.id,
|
|
772
|
+
timeout_ms: String(registration.timeoutMs)
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
throw error;
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
/**
|
|
779
|
+
* Unregister a signal handler
|
|
780
|
+
*
|
|
781
|
+
* @param signal - Signal name
|
|
782
|
+
* @param handler - Handler to remove (if not provided, removes all handlers)
|
|
783
|
+
*/
|
|
784
|
+
unregister(signal, handler) {
|
|
785
|
+
const handlers = this.handlers.get(signal);
|
|
786
|
+
if (!handlers) {
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
789
|
+
if (handler) {
|
|
790
|
+
const index = handlers.findIndex((h) => h.handler === handler);
|
|
791
|
+
if (index !== -1) {
|
|
792
|
+
handlers.splice(index, 1);
|
|
793
|
+
}
|
|
794
|
+
} else {
|
|
795
|
+
handlers.length = 0;
|
|
796
|
+
}
|
|
797
|
+
if (handlers.length === 0) {
|
|
798
|
+
const listener = this.nativeListeners.get(signal);
|
|
799
|
+
if (listener) {
|
|
800
|
+
process.off(signal, listener);
|
|
801
|
+
this.nativeListeners.delete(signal);
|
|
802
|
+
}
|
|
803
|
+
this.handlers.delete(signal);
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
/**
|
|
807
|
+
* Check if a signal has registered handlers
|
|
808
|
+
*/
|
|
809
|
+
isRegistered(signal) {
|
|
810
|
+
const handlers = this.handlers.get(signal);
|
|
811
|
+
return handlers !== void 0 && handlers.length > 0;
|
|
812
|
+
}
|
|
813
|
+
/**
|
|
814
|
+
* Get handler count for a signal
|
|
815
|
+
*/
|
|
816
|
+
getHandlerCount(signal) {
|
|
817
|
+
const handlers = this.handlers.get(signal);
|
|
818
|
+
return handlers?.length ?? 0;
|
|
819
|
+
}
|
|
820
|
+
/**
|
|
821
|
+
* Trigger signal handlers manually (for testing)
|
|
822
|
+
*
|
|
823
|
+
* @param signal - Signal name
|
|
824
|
+
*/
|
|
825
|
+
async trigger(signal) {
|
|
826
|
+
const handlers = this.handlers.get(signal);
|
|
827
|
+
if (!handlers || handlers.length === 0) {
|
|
828
|
+
return;
|
|
829
|
+
}
|
|
830
|
+
await this.executeHandlers(signal, signal);
|
|
831
|
+
}
|
|
832
|
+
/**
|
|
833
|
+
* Shutdown the signal manager and cleanup all handlers
|
|
834
|
+
*/
|
|
835
|
+
async shutdown() {
|
|
836
|
+
for (const [signal, listener] of this.nativeListeners.entries()) {
|
|
837
|
+
process.off(signal, listener);
|
|
838
|
+
}
|
|
839
|
+
this.nativeListeners.clear();
|
|
840
|
+
this.handlers.clear();
|
|
841
|
+
this.doubleTapTrackers.clear();
|
|
842
|
+
this.shuttingDown = false;
|
|
843
|
+
}
|
|
844
|
+
/**
|
|
845
|
+
* Check if manager is currently shutting down
|
|
846
|
+
*/
|
|
847
|
+
isShuttingDown() {
|
|
848
|
+
return this.shuttingDown;
|
|
849
|
+
}
|
|
850
|
+
};
|
|
851
|
+
function createSignalManager(options = {}) {
|
|
852
|
+
return new SignalManager(options);
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
// src/foundry/signals/reload.ts
|
|
856
|
+
function createConfigReloadHandler(options) {
|
|
857
|
+
const {
|
|
858
|
+
loader,
|
|
859
|
+
validator,
|
|
860
|
+
onValidated,
|
|
861
|
+
exitCode = 129,
|
|
862
|
+
logger,
|
|
863
|
+
telemetry,
|
|
864
|
+
testMode = false
|
|
865
|
+
} = options;
|
|
866
|
+
return async () => {
|
|
867
|
+
if (logger) {
|
|
868
|
+
logger.info("Config reload requested (SIGHUP)");
|
|
869
|
+
}
|
|
870
|
+
if (telemetry) {
|
|
871
|
+
telemetry.emit("fulmen.signal.config_reload_requested", {
|
|
872
|
+
signal: "SIGHUP"
|
|
873
|
+
});
|
|
874
|
+
}
|
|
875
|
+
try {
|
|
876
|
+
const newConfig = await loader();
|
|
877
|
+
const result = await validator(newConfig);
|
|
878
|
+
if (!result.valid) {
|
|
879
|
+
if (logger) {
|
|
880
|
+
logger.warn("Config validation failed - continuing with current config", {
|
|
881
|
+
error_count: result.errors?.length ?? 0,
|
|
882
|
+
errors: result.errors
|
|
883
|
+
});
|
|
884
|
+
}
|
|
885
|
+
if (telemetry) {
|
|
886
|
+
telemetry.emit("fulmen.signal.config_reload_rejected", {
|
|
887
|
+
signal: "SIGHUP",
|
|
888
|
+
reason: "validation_failed",
|
|
889
|
+
error_count: String(result.errors?.length ?? 0)
|
|
890
|
+
});
|
|
891
|
+
}
|
|
892
|
+
return;
|
|
893
|
+
}
|
|
894
|
+
if (logger) {
|
|
895
|
+
logger.info("Config validation succeeded - exiting for restart");
|
|
896
|
+
}
|
|
897
|
+
if (telemetry) {
|
|
898
|
+
telemetry.emit("fulmen.signal.config_reload_accepted", {
|
|
899
|
+
signal: "SIGHUP"
|
|
900
|
+
});
|
|
901
|
+
}
|
|
902
|
+
if (onValidated) {
|
|
903
|
+
await onValidated(newConfig);
|
|
904
|
+
}
|
|
905
|
+
if (!testMode) {
|
|
906
|
+
process.exit(exitCode);
|
|
907
|
+
}
|
|
908
|
+
} catch (error) {
|
|
909
|
+
if (logger) {
|
|
910
|
+
logger.warn("Config reload failed with error - continuing with current config", {
|
|
911
|
+
error: error instanceof Error ? error.message : String(error)
|
|
912
|
+
});
|
|
913
|
+
}
|
|
914
|
+
if (telemetry) {
|
|
915
|
+
telemetry.emit("fulmen.signal.config_reload_error", {
|
|
916
|
+
signal: "SIGHUP",
|
|
917
|
+
error_type: error instanceof Error ? error.constructor.name : "unknown"
|
|
918
|
+
});
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
};
|
|
922
|
+
}
|
|
923
|
+
var ConfigReloadTracker = class {
|
|
924
|
+
failures = 0;
|
|
925
|
+
lastFailureTime = null;
|
|
926
|
+
maxFailures;
|
|
927
|
+
logger;
|
|
928
|
+
telemetry;
|
|
929
|
+
constructor(options) {
|
|
930
|
+
this.maxFailures = options.maxFailures ?? 3;
|
|
931
|
+
this.logger = options.logger;
|
|
932
|
+
this.telemetry = options.telemetry;
|
|
933
|
+
}
|
|
934
|
+
/**
|
|
935
|
+
* Record a reload failure
|
|
936
|
+
*
|
|
937
|
+
* @returns true if threshold exceeded, false otherwise
|
|
938
|
+
*/
|
|
939
|
+
recordFailure() {
|
|
940
|
+
this.failures++;
|
|
941
|
+
this.lastFailureTime = Date.now();
|
|
942
|
+
if (this.failures >= this.maxFailures) {
|
|
943
|
+
if (this.logger) {
|
|
944
|
+
this.logger.warn(
|
|
945
|
+
`${this.failures} consecutive config reload failures - check config source`,
|
|
946
|
+
{
|
|
947
|
+
failure_count: this.failures,
|
|
948
|
+
threshold: this.maxFailures
|
|
949
|
+
}
|
|
950
|
+
);
|
|
951
|
+
}
|
|
952
|
+
if (this.telemetry) {
|
|
953
|
+
this.telemetry.emit("fulmen.signal.config_reload_threshold_exceeded", {
|
|
954
|
+
failure_count: String(this.failures),
|
|
955
|
+
threshold: String(this.maxFailures)
|
|
956
|
+
});
|
|
957
|
+
}
|
|
958
|
+
return true;
|
|
959
|
+
}
|
|
960
|
+
return false;
|
|
961
|
+
}
|
|
962
|
+
/**
|
|
963
|
+
* Record a successful reload (resets counter)
|
|
964
|
+
*/
|
|
965
|
+
recordSuccess() {
|
|
966
|
+
this.failures = 0;
|
|
967
|
+
this.lastFailureTime = null;
|
|
968
|
+
}
|
|
969
|
+
/**
|
|
970
|
+
* Get current failure count
|
|
971
|
+
*/
|
|
972
|
+
getFailureCount() {
|
|
973
|
+
return this.failures;
|
|
974
|
+
}
|
|
975
|
+
/**
|
|
976
|
+
* Get last failure timestamp
|
|
977
|
+
*/
|
|
978
|
+
getLastFailureTime() {
|
|
979
|
+
return this.lastFailureTime;
|
|
980
|
+
}
|
|
981
|
+
};
|
|
982
|
+
|
|
983
|
+
export { ConfigReloadTracker, SignalManager, createBearerTokenAuth, createConfigReloadEndpoint, createConfigReloadHandler, createControlDiscoveryEndpoint, createDoubleTapTracker, createSignalEndpoint, createSignalManager, createSimpleRateLimiter, ensurePOSIX, ensureSignalExitCodesSupported, ensureSupported, ensureWindows, getFallbackMetadata, getHttpFallbackGuidance, getWindowTimeRemaining, handleDoubleTap, handleWindowsFallback, isWithinWindow, onAnyShutdown, onEmergencyQuit, onReload, onShutdown, onUSR1, onUSR2, requiresFallback, resetDoubleTap };
|
|
984
|
+
//# sourceMappingURL=chunk-HS4F3BRR.js.map
|
|
985
|
+
//# sourceMappingURL=chunk-HS4F3BRR.js.map
|