@fulmenhq/tsfulmen 0.3.3 → 0.4.1

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.
Files changed (123) hide show
  1. package/CHANGELOG.md +76 -1
  2. package/README.md +30 -1
  3. package/config/crucible-ts/taxonomy/metrics.yaml +1 -1
  4. package/dist/appidentity/index.js +9 -2209
  5. package/dist/appidentity/index.js.map +1 -1
  6. package/dist/assets/index.d.ts +184 -0
  7. package/dist/assets/index.js +4 -0
  8. package/dist/assets/index.js.map +1 -0
  9. package/dist/bin/prometheus-cli.js +15 -9139
  10. package/dist/bin/prometheus-cli.js.map +1 -1
  11. package/dist/bin/schema-cli.js +4 -6098
  12. package/dist/bin/schema-cli.js.map +1 -1
  13. package/dist/bin/signals-cli.js +15 -1910
  14. package/dist/bin/signals-cli.js.map +1 -1
  15. package/dist/chunk-24BAF63H.js +194 -0
  16. package/dist/chunk-24BAF63H.js.map +1 -0
  17. package/dist/chunk-3HISR2XI.js +36 -0
  18. package/dist/chunk-3HISR2XI.js.map +1 -0
  19. package/dist/chunk-56F2EEAG.js +328 -0
  20. package/dist/chunk-56F2EEAG.js.map +1 -0
  21. package/dist/chunk-5HZEJGZ5.js +725 -0
  22. package/dist/chunk-5HZEJGZ5.js.map +1 -0
  23. package/dist/chunk-7YWACZEJ.js +837 -0
  24. package/dist/chunk-7YWACZEJ.js.map +1 -0
  25. package/dist/chunk-BCO3FUUO.js +682 -0
  26. package/dist/chunk-BCO3FUUO.js.map +1 -0
  27. package/dist/chunk-GOTEQYAD.js +410 -0
  28. package/dist/chunk-GOTEQYAD.js.map +1 -0
  29. package/dist/chunk-HY6JQOFN.js +32 -0
  30. package/dist/chunk-HY6JQOFN.js.map +1 -0
  31. package/dist/chunk-LMNAUTW7.js +43 -0
  32. package/dist/chunk-LMNAUTW7.js.map +1 -0
  33. package/dist/chunk-LWAH2UG7.js +103 -0
  34. package/dist/chunk-LWAH2UG7.js.map +1 -0
  35. package/dist/chunk-OHE2AOH4.js +273 -0
  36. package/dist/chunk-OHE2AOH4.js.map +1 -0
  37. package/dist/chunk-P3DNOZOG.js +1488 -0
  38. package/dist/chunk-P3DNOZOG.js.map +1 -0
  39. package/dist/chunk-PQ6TKPUN.js +27 -0
  40. package/dist/chunk-PQ6TKPUN.js.map +1 -0
  41. package/dist/chunk-QCCN6POA.js +211 -0
  42. package/dist/chunk-QCCN6POA.js.map +1 -0
  43. package/dist/chunk-QGTNV4U2.js +985 -0
  44. package/dist/chunk-QGTNV4U2.js.map +1 -0
  45. package/dist/chunk-RSLFR4TO.js +631 -0
  46. package/dist/chunk-RSLFR4TO.js.map +1 -0
  47. package/dist/chunk-SNV5WG5V.js +9412 -0
  48. package/dist/chunk-SNV5WG5V.js.map +1 -0
  49. package/dist/chunk-UP2VWCW5.js +33 -0
  50. package/dist/chunk-UP2VWCW5.js.map +1 -0
  51. package/dist/chunk-VJIGX7MH.js +208 -0
  52. package/dist/chunk-VJIGX7MH.js.map +1 -0
  53. package/dist/chunk-WCCMRVZF.js +130 -0
  54. package/dist/chunk-WCCMRVZF.js.map +1 -0
  55. package/dist/chunk-WSMVLWLB.js +269 -0
  56. package/dist/chunk-WSMVLWLB.js.map +1 -0
  57. package/dist/config/index.js +6 -1220
  58. package/dist/config/index.js.map +1 -1
  59. package/dist/crucible/fulpack/index.js +2 -25
  60. package/dist/crucible/fulpack/index.js.map +1 -1
  61. package/dist/crucible/index.js +13 -1129
  62. package/dist/crucible/index.js.map +1 -1
  63. package/dist/docscribe/index.js +2 -680
  64. package/dist/docscribe/index.js.map +1 -1
  65. package/dist/errors/index.js +5 -1831
  66. package/dist/errors/index.js.map +1 -1
  67. package/dist/errors-D7KXV3QW.js +4 -0
  68. package/dist/errors-D7KXV3QW.js.map +1 -0
  69. package/dist/errors-R7ZODJXS.js +8 -0
  70. package/dist/errors-R7ZODJXS.js.map +1 -0
  71. package/dist/export-EA3RNQNS.js +6 -0
  72. package/dist/export-EA3RNQNS.js.map +1 -0
  73. package/dist/foundry/index.js +340 -3949
  74. package/dist/foundry/index.js.map +1 -1
  75. package/dist/foundry/similarity/index.js +3 -134
  76. package/dist/foundry/similarity/index.js.map +1 -1
  77. package/dist/fulencode/index.js +3 -319
  78. package/dist/fulencode/index.js.map +1 -1
  79. package/dist/fulhash/index.js +3 -611
  80. package/dist/fulhash/index.js.map +1 -1
  81. package/dist/fulpack/index.js +3 -1515
  82. package/dist/fulpack/index.js.map +1 -1
  83. package/dist/hash-YOJYVOWH.js +4 -0
  84. package/dist/hash-YOJYVOWH.js.map +1 -0
  85. package/dist/index.d.ts +1 -1
  86. package/dist/index.js +14 -4395
  87. package/dist/index.js.map +1 -1
  88. package/dist/loader-VP2ZIC4O.js +9 -0
  89. package/dist/loader-VP2ZIC4O.js.map +1 -0
  90. package/dist/logging/index.js +2 -723
  91. package/dist/logging/index.js.map +1 -1
  92. package/dist/pathfinder/index.js +18 -2341
  93. package/dist/pathfinder/index.js.map +1 -1
  94. package/dist/prom-client-PNEBGD3G.js +4801 -0
  95. package/dist/prom-client-PNEBGD3G.js.map +1 -0
  96. package/dist/reports/license-inventory.csv +1 -1
  97. package/dist/schema/index.d.ts +19 -6
  98. package/dist/schema/index.js +4 -6148
  99. package/dist/schema/index.js.map +1 -1
  100. package/dist/signals/index.js +8 -2767
  101. package/dist/signals/index.js.map +1 -1
  102. package/dist/similarity/index.js +3 -134
  103. package/dist/similarity/index.js.map +1 -1
  104. package/dist/telemetry/http/index.js +9 -930
  105. package/dist/telemetry/http/index.js.map +1 -1
  106. package/dist/telemetry/index.js +4 -1561
  107. package/dist/telemetry/index.js.map +1 -1
  108. package/dist/telemetry/prometheus/index.js +7 -8422
  109. package/dist/telemetry/prometheus/index.js.map +1 -1
  110. package/dist/validator-I3ASDVDB.js +6 -0
  111. package/dist/validator-I3ASDVDB.js.map +1 -0
  112. package/package.json +5 -1
  113. package/schemas/crucible-ts/library/module-manifest/v1.0.0/module-manifest.schema.json +1 -1
  114. package/schemas/crucible-ts/observability/logging/v1.0.0/definitions.schema.json +1 -1
  115. package/schemas/crucible-ts/observability/logging/v1.0.0/log-event.schema.json +13 -13
  116. package/schemas/crucible-ts/observability/logging/v1.0.0/logger-config.schema.json +2 -2
  117. package/schemas/crucible-ts/observability/logging/v1.0.0/logging-policy.schema.json +1 -1
  118. package/schemas/crucible-ts/observability/logging/v1.0.0/middleware-config.schema.json +2 -2
  119. package/schemas/crucible-ts/observability/logging/v1.0.0/severity-filter.schema.json +3 -3
  120. package/schemas/crucible-ts/upstream/3leaps/crucible/PROVENANCE.md +3 -3
  121. package/schemas/crucible-ts/upstream/3leaps/crucible/docs/catalog/classifiers/README.md +1 -1
  122. package/schemas/crucible-ts/upstream/3leaps/crucible/docs/standards/classifiers-framework.md +1 -1
  123. 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-24BAF63H.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-QGTNV4U2.js.map
985
+ //# sourceMappingURL=chunk-QGTNV4U2.js.map