@ultimat3/core 1.2.0 → 2.0.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/CLAUDE.md +252 -0
- package/README.md +210 -10
- package/package.json +2 -1
- package/src/actor.ts +118 -4
- package/src/app-version.ts +32 -0
- package/src/assert.ts +5 -1
- package/src/config.ts +47 -12
- package/src/context.ts +30 -3
- package/src/cursor.ts +25 -4
- package/src/env-example.ts +2 -1
- package/src/env.ts +14 -3
- package/src/environment.ts +39 -13
- package/src/error-codes.ts +13 -0
- package/src/error-render.ts +249 -0
- package/src/error-reporter-sentry.ts +175 -0
- package/src/error-reporter.ts +212 -0
- package/src/error-retry.ts +100 -0
- package/src/errors.ts +55 -7
- package/src/exports/error-contract.ts +61 -0
- package/src/exports/observability.ts +161 -0
- package/src/exports/secrets.ts +71 -0
- package/src/ids.ts +49 -7
- package/src/impersonate.ts +62 -0
- package/src/index.ts +277 -113
- package/src/lifecycle-deadline.ts +73 -0
- package/src/lifecycle-errors.ts +33 -0
- package/src/lifecycle.ts +178 -16
- package/src/logger.ts +99 -9
- package/src/mcp-exposure.ts +32 -0
- package/src/metrics.ts +0 -0
- package/src/otlp-metric-exporter.ts +136 -0
- package/src/otlp-span-exporter.ts +170 -0
- package/src/otlp.ts +217 -0
- package/src/read-capped.ts +47 -0
- package/src/runtime-metrics.ts +15 -0
- package/src/safe-url.ts +50 -0
- package/src/sampler.ts +126 -0
- package/src/schema-error-codes.ts +28 -0
- package/src/secrets-errors.ts +143 -0
- package/src/secrets-store.ts +173 -0
- package/src/secrets.ts +292 -0
- package/src/telemetry.ts +43 -11
- package/src/timing-safe-equal.ts +18 -0
- package/src/type-pins.ts +93 -0
- package/src/version.ts +53 -4
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// Single responsibility: acting as another actor, on the record. `withChildContext({ actor })` is
|
|
2
|
+
// the mechanism; this is the ONE door through it, because a swap with no reason and no origin is
|
|
3
|
+
// indistinguishable in an audit trail from the customer doing it themselves.
|
|
4
|
+
|
|
5
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
6
|
+
import { type Actor, actorLabel, actorOrigin } from './actor';
|
|
7
|
+
import { assert } from './assert';
|
|
8
|
+
import { useContext, withChildContext } from './context';
|
|
9
|
+
import { currentSpan } from './telemetry';
|
|
10
|
+
|
|
11
|
+
const storage = new AsyncLocalStorage<string>();
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Run `fn` as `actor`, recording who asked and why.
|
|
15
|
+
*
|
|
16
|
+
* ```ts
|
|
17
|
+
* await impersonate(customer, 'ticket 4821: reproduce the failed refund', async () => { … });
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* Three properties, none optional. **The original actor is preserved** — stamped onto the child as
|
|
21
|
+
* `onBehalfOf`, so every log line, span and `actorLabel()` inside renders `support:eng-7→user:…`
|
|
22
|
+
* and a refund issued here can never read as the customer's. **The reason is required and
|
|
23
|
+
* non-blank** because it *is* the mechanism: a swap with no argument is a pragma, and the next
|
|
24
|
+
* reader cannot tell a support session from a bug. **There must be a context** — outside one there
|
|
25
|
+
* is no original actor to preserve, so there is nothing to impersonate *from*, and
|
|
26
|
+
* `withChildContext` says so with `X_NO_CONTEXT`.
|
|
27
|
+
*
|
|
28
|
+
* Same template as `@ultimat3/entity`'s `crossTenant()`, deliberately: two escapes from the
|
|
29
|
+
* framework's default posture should not be two different-looking things.
|
|
30
|
+
*/
|
|
31
|
+
export function impersonate<T>(actor: Actor, reason: string, fn: () => T): T {
|
|
32
|
+
assert(
|
|
33
|
+
reason.trim() !== '',
|
|
34
|
+
'impersonate() was given a blank reason, so the identity swap it performs carries no argument',
|
|
35
|
+
"pass why one actor is acting as another: impersonate(customer, 'ticket 4821: reproduce the failed refund', fn)",
|
|
36
|
+
);
|
|
37
|
+
const parent = useContext();
|
|
38
|
+
const impersonated: Actor = Object.freeze({ ...actor, onBehalfOf: actorOrigin(parent.actor) });
|
|
39
|
+
const label = actorLabel(impersonated);
|
|
40
|
+
// The PARENT's logger, because the parent is who is performing this — and `warn`, not `info`,
|
|
41
|
+
// because this is rare, always interesting, and an audit query that has to read info-level to
|
|
42
|
+
// find it will be run at the wrong level during the one incident where it matters.
|
|
43
|
+
parent.logger.warn('impersonation', { event: 'impersonation', actor: label, reason });
|
|
44
|
+
currentSpan()?.addEvent('impersonation', {
|
|
45
|
+
'actor.label': label,
|
|
46
|
+
'impersonation.reason': reason,
|
|
47
|
+
});
|
|
48
|
+
return withChildContext({ actor: impersonated }, () => storage.run(reason, fn));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The innermost enclosing reason, or `undefined` outside every scope — which is every request in
|
|
53
|
+
* an app where nobody is impersonating. Read by an audit sink, and by nothing else.
|
|
54
|
+
*/
|
|
55
|
+
export function impersonationReason(): string | undefined {
|
|
56
|
+
return storage.getStore();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Is the caller acting as somebody else right now? */
|
|
60
|
+
export function isImpersonating(): boolean {
|
|
61
|
+
return storage.getStore() !== undefined;
|
|
62
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,10 +1,24 @@
|
|
|
1
1
|
// Single responsibility: the public API of @ultimat3/core. Explicit named exports only —
|
|
2
2
|
// every other package imports from here, so this list is the tier-0 contract.
|
|
3
|
+
//
|
|
4
|
+
// Three slices arrive through a barrel in `exports/` — observability, the error contract and
|
|
5
|
+
// secrets — because each is one subject spread over a dozen modules. Every name they carry is
|
|
6
|
+
// still written out below: `export *` would make the contract something a reader has to resolve.
|
|
3
7
|
|
|
4
|
-
export type {
|
|
8
|
+
export type {
|
|
9
|
+
Actor,
|
|
10
|
+
ActorFactKey,
|
|
11
|
+
ActorFactMap,
|
|
12
|
+
ActorFacts,
|
|
13
|
+
ActorInit,
|
|
14
|
+
ActorKind,
|
|
15
|
+
ActorOrigin,
|
|
16
|
+
} from './actor';
|
|
5
17
|
export {
|
|
6
18
|
ACTOR_KINDS,
|
|
19
|
+
actorFact,
|
|
7
20
|
actorLabel,
|
|
21
|
+
actorOrigin,
|
|
8
22
|
agentActor,
|
|
9
23
|
anonymousActor,
|
|
10
24
|
hasRole,
|
|
@@ -13,7 +27,9 @@ export {
|
|
|
13
27
|
isAnonymous,
|
|
14
28
|
serviceActor,
|
|
15
29
|
userActor,
|
|
30
|
+
withFacts,
|
|
16
31
|
} from './actor';
|
|
32
|
+
export { APP_VERSION_KEY, appVersion, DEFAULT_APP_VERSION } from './app-version';
|
|
17
33
|
export type { InvariantOptions } from './assert';
|
|
18
34
|
export { assert, assertNever, invariant } from './assert';
|
|
19
35
|
export type { Clock, FrozenClock } from './clock';
|
|
@@ -24,6 +40,7 @@ export type {
|
|
|
24
40
|
AppConfig,
|
|
25
41
|
AppConfigInput,
|
|
26
42
|
AppConfigOverlay,
|
|
43
|
+
AuthConfig,
|
|
27
44
|
CacheConfig,
|
|
28
45
|
CacheTier,
|
|
29
46
|
DatabaseConfig,
|
|
@@ -58,6 +75,7 @@ export {
|
|
|
58
75
|
configureCursorSigning,
|
|
59
76
|
decodeCursor,
|
|
60
77
|
encodeCursor,
|
|
78
|
+
resetCursorSigning,
|
|
61
79
|
usesDevCursorSecret,
|
|
62
80
|
} from './cursor';
|
|
63
81
|
export type {
|
|
@@ -95,44 +113,265 @@ export {
|
|
|
95
113
|
isLocal,
|
|
96
114
|
isProduction,
|
|
97
115
|
resolveEnvironment,
|
|
116
|
+
tryResolveEnvironment,
|
|
98
117
|
} from './environment';
|
|
99
118
|
export type {
|
|
119
|
+
CodedErrorInit,
|
|
100
120
|
CoreErrorCode,
|
|
101
121
|
ErrorCodeDeclaration,
|
|
102
122
|
ErrorCodeDescriptor,
|
|
103
123
|
ErrorCodeEntry,
|
|
104
|
-
|
|
105
|
-
export {
|
|
106
|
-
CORE_ERROR_CODES,
|
|
107
|
-
describeErrorCode,
|
|
108
|
-
ERROR_DOCS_BASE,
|
|
109
|
-
errorCodeSnapshot,
|
|
110
|
-
errorDocsUrl,
|
|
111
|
-
hasErrorCode,
|
|
112
|
-
listErrorCodes,
|
|
113
|
-
registerErrorCodes,
|
|
114
|
-
resetErrorCodes,
|
|
115
|
-
} from './error-codes';
|
|
116
|
-
export type {
|
|
117
|
-
CodedErrorInit,
|
|
124
|
+
ErrorRetry,
|
|
118
125
|
FormatErrorOptions,
|
|
119
126
|
UltimateErrorInit,
|
|
120
127
|
UltimateErrorJSON,
|
|
121
|
-
} from './
|
|
128
|
+
} from './exports/error-contract';
|
|
122
129
|
export {
|
|
130
|
+
CORE_ERROR_CODES,
|
|
123
131
|
ConfigInvalidError,
|
|
132
|
+
DEFAULT_ERROR_RETRY,
|
|
133
|
+
describeErrorCode,
|
|
134
|
+
describeValue,
|
|
124
135
|
EnvMissingError,
|
|
136
|
+
ERROR_DOCS_BASE,
|
|
137
|
+
ERROR_RETRY_KINDS,
|
|
138
|
+
errorCodeSnapshot,
|
|
139
|
+
errorDocsUrl,
|
|
140
|
+
errorRetry,
|
|
125
141
|
formatError,
|
|
142
|
+
hasErrorCode,
|
|
126
143
|
InternalError,
|
|
144
|
+
isErrorRetry,
|
|
145
|
+
isThrownError,
|
|
127
146
|
isUltimateError,
|
|
147
|
+
listErrorCodes,
|
|
148
|
+
MAX_RENDERED_LENGTH,
|
|
128
149
|
NotImplementedError,
|
|
129
150
|
notImplemented,
|
|
151
|
+
registerErrorCodes,
|
|
152
|
+
registerErrorRetry,
|
|
153
|
+
registeredErrorRetry,
|
|
154
|
+
renderCauseValue,
|
|
155
|
+
renderFixLiteral,
|
|
156
|
+
renderThrowable,
|
|
157
|
+
resetErrorCodes,
|
|
158
|
+
resetErrorRetry,
|
|
159
|
+
retryFor,
|
|
160
|
+
SCHEMA_ERROR_CODE_TITLES,
|
|
161
|
+
stringField,
|
|
130
162
|
toUltimateError,
|
|
131
163
|
ULTIMATE_ERROR_BRAND,
|
|
132
164
|
UltimateError,
|
|
133
|
-
} from './
|
|
165
|
+
} from './exports/error-contract';
|
|
166
|
+
export type {
|
|
167
|
+
AttributeValue,
|
|
168
|
+
Counter,
|
|
169
|
+
ErrorReport,
|
|
170
|
+
ErrorReporter,
|
|
171
|
+
ErrorReportingOptions,
|
|
172
|
+
ErrorScope,
|
|
173
|
+
ErrorSeverity,
|
|
174
|
+
ErrorSource,
|
|
175
|
+
Gauge,
|
|
176
|
+
GaugeOptions,
|
|
177
|
+
Histogram,
|
|
178
|
+
HistogramOptions,
|
|
179
|
+
HistogramPoint,
|
|
180
|
+
InstrumentOptions,
|
|
181
|
+
LogFields,
|
|
182
|
+
Logger,
|
|
183
|
+
LoggerOptions,
|
|
184
|
+
LogLevel,
|
|
185
|
+
MemoryErrorReporter,
|
|
186
|
+
MemoryExporter,
|
|
187
|
+
MemoryMetricExporter,
|
|
188
|
+
MetricAttributes,
|
|
189
|
+
MetricAttributeValue,
|
|
190
|
+
MetricCollection,
|
|
191
|
+
MetricDescriptor,
|
|
192
|
+
MetricExporter,
|
|
193
|
+
MetricKind,
|
|
194
|
+
MetricPoint,
|
|
195
|
+
MetricsOptions,
|
|
196
|
+
OtlpAnyValue,
|
|
197
|
+
OtlpKeyValue,
|
|
198
|
+
OtlpMetricExporter,
|
|
199
|
+
OtlpMetricExporterOptions,
|
|
200
|
+
OtlpSignal,
|
|
201
|
+
OtlpSpanExporter,
|
|
202
|
+
OtlpSpanExporterOptions,
|
|
203
|
+
ReadableMetric,
|
|
204
|
+
ReadableSpan,
|
|
205
|
+
ReportErrorOptions,
|
|
206
|
+
RequestSample,
|
|
207
|
+
Sampler,
|
|
208
|
+
SentryDsn,
|
|
209
|
+
SentryEnvelopeOptions,
|
|
210
|
+
SentryReporterOptions,
|
|
211
|
+
Span,
|
|
212
|
+
SpanAttributes,
|
|
213
|
+
SpanContext,
|
|
214
|
+
SpanEvent,
|
|
215
|
+
SpanExporter,
|
|
216
|
+
SpanKind,
|
|
217
|
+
SpanResource,
|
|
218
|
+
SpanStatus,
|
|
219
|
+
SpanStatusCode,
|
|
220
|
+
StartSpanOptions,
|
|
221
|
+
TelemetryOptions,
|
|
222
|
+
} from './exports/observability';
|
|
223
|
+
export {
|
|
224
|
+
alwaysOffSampler,
|
|
225
|
+
alwaysOnSampler,
|
|
226
|
+
collectMetrics,
|
|
227
|
+
configureErrorReporting,
|
|
228
|
+
configureMetrics,
|
|
229
|
+
configureTelemetry,
|
|
230
|
+
connections,
|
|
231
|
+
counter,
|
|
232
|
+
createLogger,
|
|
233
|
+
currentSampler,
|
|
234
|
+
currentSpan,
|
|
235
|
+
currentSpanContext,
|
|
236
|
+
DEFAULT_HISTOGRAM_BOUNDS,
|
|
237
|
+
DEFAULT_MAX_SERIES,
|
|
238
|
+
DEFAULT_SAMPLE_RATIO,
|
|
239
|
+
defaultSampler,
|
|
240
|
+
ERROR_SOURCES,
|
|
241
|
+
ErrorReporterDsnInvalidError,
|
|
242
|
+
errorReport,
|
|
243
|
+
exportMetrics,
|
|
244
|
+
gauge,
|
|
245
|
+
histogram,
|
|
246
|
+
isRedactedKey,
|
|
247
|
+
jobs,
|
|
248
|
+
LOG_LEVELS,
|
|
249
|
+
leasesLost,
|
|
250
|
+
logger,
|
|
251
|
+
METRICS_CONTENT_TYPE,
|
|
252
|
+
METRICS_PATH,
|
|
253
|
+
MetricCardinalityError,
|
|
254
|
+
MetricNameInvalidError,
|
|
255
|
+
MetricValueInvalidError,
|
|
256
|
+
memoryErrorReporter,
|
|
257
|
+
memoryExporter,
|
|
258
|
+
memoryMetricExporter,
|
|
259
|
+
metricsText,
|
|
260
|
+
noopErrorReporter,
|
|
261
|
+
noopExporter,
|
|
262
|
+
noopMetricExporter,
|
|
263
|
+
OTEL_SAMPLER_ARG_KEY,
|
|
264
|
+
OTEL_SAMPLER_KEY,
|
|
265
|
+
OTLP_ENDPOINT_KEY,
|
|
266
|
+
OTLP_HEADERS_KEY,
|
|
267
|
+
OTLP_PROTOCOL_KEY,
|
|
268
|
+
OTLP_SCOPE,
|
|
269
|
+
OtlpEndpointInvalidError,
|
|
270
|
+
OtlpProtocolUnsupportedError,
|
|
271
|
+
OVERFLOW_ATTRIBUTE,
|
|
272
|
+
otlpAttributes,
|
|
273
|
+
otlpEndpoint,
|
|
274
|
+
otlpHeaders,
|
|
275
|
+
otlpMetricExporter,
|
|
276
|
+
otlpMetricsRequest,
|
|
277
|
+
otlpResource,
|
|
278
|
+
otlpSpanExporter,
|
|
279
|
+
otlpTraceRequest,
|
|
280
|
+
parentBasedRatioSampler,
|
|
281
|
+
parseSentryDsn,
|
|
282
|
+
parseTraceparent,
|
|
283
|
+
queueDepth,
|
|
284
|
+
REDACTED,
|
|
285
|
+
ratioSampler,
|
|
286
|
+
recordConnection,
|
|
287
|
+
recordJob,
|
|
288
|
+
recordLeaseLost,
|
|
289
|
+
recordQueueDepth,
|
|
290
|
+
recordRequest,
|
|
291
|
+
redactKeys,
|
|
292
|
+
reportError,
|
|
293
|
+
requestDuration,
|
|
294
|
+
requests,
|
|
295
|
+
resetDefaultSampler,
|
|
296
|
+
resetErrorReporting,
|
|
297
|
+
resetMetrics,
|
|
298
|
+
resetTelemetry,
|
|
299
|
+
SCALING_METRICS,
|
|
300
|
+
samplerFromEnv,
|
|
301
|
+
sentryEnvelope,
|
|
302
|
+
sentryErrorReporter,
|
|
303
|
+
serviceResource,
|
|
304
|
+
setLoggerContextFields,
|
|
305
|
+
startMetricExport,
|
|
306
|
+
startSpan,
|
|
307
|
+
traceparent,
|
|
308
|
+
tryOtlpEndpoint,
|
|
309
|
+
unixNano,
|
|
310
|
+
withSpan,
|
|
311
|
+
withSpanContext,
|
|
312
|
+
} from './exports/observability';
|
|
313
|
+
export type {
|
|
314
|
+
MasterKeyRef,
|
|
315
|
+
MasterKeySource,
|
|
316
|
+
Secret,
|
|
317
|
+
SecretSummary,
|
|
318
|
+
SecretsEnvelope,
|
|
319
|
+
SecretsErrorCode,
|
|
320
|
+
SecretsInstallOptions,
|
|
321
|
+
SecretsInstallReport,
|
|
322
|
+
SecretsLocation,
|
|
323
|
+
SecretValues,
|
|
324
|
+
} from './exports/secrets';
|
|
325
|
+
export {
|
|
326
|
+
assertSecretValues,
|
|
327
|
+
describeSecrets,
|
|
328
|
+
findMasterKey,
|
|
329
|
+
generateMasterKey,
|
|
330
|
+
installSecrets,
|
|
331
|
+
isSecret,
|
|
332
|
+
masterKeyId,
|
|
333
|
+
masterKeyIdOf,
|
|
334
|
+
masterKeyPath,
|
|
335
|
+
openSecrets,
|
|
336
|
+
parseMasterKey,
|
|
337
|
+
parseSecretsEnvelope,
|
|
338
|
+
readSecretsFile,
|
|
339
|
+
requireMasterKey,
|
|
340
|
+
revealOptionalSecret,
|
|
341
|
+
revealSecret,
|
|
342
|
+
SECRET_BRAND,
|
|
343
|
+
SECRET_NAME,
|
|
344
|
+
SECRETS_ALG,
|
|
345
|
+
SECRETS_ERROR_CODES,
|
|
346
|
+
SECRETS_FILE,
|
|
347
|
+
SECRETS_IV_BYTES,
|
|
348
|
+
SECRETS_KEY_BYTES,
|
|
349
|
+
SECRETS_KEY_ENV,
|
|
350
|
+
SECRETS_KEY_FILE,
|
|
351
|
+
SECRETS_KEY_HEX_LENGTH,
|
|
352
|
+
SECRETS_KEY_ID_LENGTH,
|
|
353
|
+
SECRETS_KEY_MODE,
|
|
354
|
+
SECRETS_TAG_BYTES,
|
|
355
|
+
SECRETS_VERSION,
|
|
356
|
+
SecretsFileInvalidError,
|
|
357
|
+
SecretsFileMissingError,
|
|
358
|
+
SecretsKeyInvalidError,
|
|
359
|
+
SecretsKeyMismatchError,
|
|
360
|
+
SecretsKeyMissingError,
|
|
361
|
+
SecretsPlaintextInvalidError,
|
|
362
|
+
SecretsTamperedError,
|
|
363
|
+
sealSecrets,
|
|
364
|
+
secret,
|
|
365
|
+
secretsFileExists,
|
|
366
|
+
secretsPath,
|
|
367
|
+
serializeSecretValues,
|
|
368
|
+
writeMasterKeyFile,
|
|
369
|
+
writeSecretsFile,
|
|
370
|
+
} from './exports/secrets';
|
|
134
371
|
export type { Brand, Id } from './ids';
|
|
135
372
|
export {
|
|
373
|
+
isSpanId,
|
|
374
|
+
isTraceId,
|
|
136
375
|
isUuid,
|
|
137
376
|
nanoid,
|
|
138
377
|
parseId,
|
|
@@ -187,6 +426,7 @@ export {
|
|
|
187
426
|
} from './image/raster';
|
|
188
427
|
export type { ImageFit, ResizeSpec } from './image/resize';
|
|
189
428
|
export { fitBox, resizeRaster, scaledToFit } from './image/resize';
|
|
429
|
+
export { impersonate, impersonationReason, isImpersonating } from './impersonate';
|
|
190
430
|
export type {
|
|
191
431
|
HealthPayload,
|
|
192
432
|
HealthReport,
|
|
@@ -194,6 +434,8 @@ export type {
|
|
|
194
434
|
LifecycleOptions,
|
|
195
435
|
OnShutdownOptions,
|
|
196
436
|
ProcessSignal,
|
|
437
|
+
ReadinessCheck,
|
|
438
|
+
ReadinessStatus,
|
|
197
439
|
ShutdownHook,
|
|
198
440
|
ShutdownPhase,
|
|
199
441
|
ShutdownReason,
|
|
@@ -203,17 +445,23 @@ export {
|
|
|
203
445
|
beginWork,
|
|
204
446
|
configureLifecycle,
|
|
205
447
|
drain,
|
|
448
|
+
drainDeadlineMs,
|
|
206
449
|
healthReport,
|
|
207
450
|
healthzPayload,
|
|
451
|
+
idleWaiterCount,
|
|
208
452
|
inflightCount,
|
|
209
453
|
installSignalHandlers,
|
|
210
454
|
isDraining,
|
|
211
455
|
lifecycleState,
|
|
212
456
|
markReady,
|
|
213
457
|
onShutdown,
|
|
458
|
+
readinessCheckCount,
|
|
459
|
+
readinessChecks,
|
|
214
460
|
readyzPayload,
|
|
461
|
+
registerReadinessCheck,
|
|
215
462
|
resetLifecycle,
|
|
216
463
|
SHUTDOWN_PHASES,
|
|
464
|
+
shutdownHookCount,
|
|
217
465
|
} from './lifecycle';
|
|
218
466
|
export {
|
|
219
467
|
isSelfOrigin,
|
|
@@ -221,51 +469,10 @@ export {
|
|
|
221
469
|
markListening,
|
|
222
470
|
resetListeners,
|
|
223
471
|
} from './listeners';
|
|
224
|
-
export type {
|
|
225
|
-
export {
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
LOG_LEVELS,
|
|
229
|
-
logger,
|
|
230
|
-
REDACTED,
|
|
231
|
-
redactKeys,
|
|
232
|
-
setLoggerContextFields,
|
|
233
|
-
} from './logger';
|
|
234
|
-
export type {
|
|
235
|
-
Counter,
|
|
236
|
-
Gauge,
|
|
237
|
-
GaugeOptions,
|
|
238
|
-
Histogram,
|
|
239
|
-
HistogramOptions,
|
|
240
|
-
HistogramPoint,
|
|
241
|
-
InstrumentOptions,
|
|
242
|
-
MemoryMetricExporter,
|
|
243
|
-
MetricAttributes,
|
|
244
|
-
MetricAttributeValue,
|
|
245
|
-
MetricCollection,
|
|
246
|
-
MetricDescriptor,
|
|
247
|
-
MetricExporter,
|
|
248
|
-
MetricKind,
|
|
249
|
-
MetricPoint,
|
|
250
|
-
MetricsOptions,
|
|
251
|
-
ReadableMetric,
|
|
252
|
-
} from './metrics';
|
|
253
|
-
export {
|
|
254
|
-
collectMetrics,
|
|
255
|
-
configureMetrics,
|
|
256
|
-
counter,
|
|
257
|
-
DEFAULT_HISTOGRAM_BOUNDS,
|
|
258
|
-
exportMetrics,
|
|
259
|
-
gauge,
|
|
260
|
-
histogram,
|
|
261
|
-
MetricNameInvalidError,
|
|
262
|
-
MetricValueInvalidError,
|
|
263
|
-
memoryMetricExporter,
|
|
264
|
-
noopMetricExporter,
|
|
265
|
-
resetMetrics,
|
|
266
|
-
startMetricExport,
|
|
267
|
-
} from './metrics';
|
|
268
|
-
export { METRICS_CONTENT_TYPE, METRICS_PATH, metricsText } from './metrics-text';
|
|
472
|
+
export type { McpExposureDeclaration } from './mcp-exposure';
|
|
473
|
+
export { isMcpExposed } from './mcp-exposure';
|
|
474
|
+
export type { CappedBody } from './read-capped';
|
|
475
|
+
export { readWithinLimit } from './read-capped';
|
|
269
476
|
export type { ModuleRegistrar, PrimitiveKind, RegisteredPrimitive } from './registrar';
|
|
270
477
|
export {
|
|
271
478
|
hasPrimitiveRegistrar,
|
|
@@ -278,57 +485,14 @@ export type { Err, Ok, Result } from './result';
|
|
|
278
485
|
export { err, isErr, isOk, map, mapErr, ok, tryCatch, unwrap, unwrapOr } from './result';
|
|
279
486
|
export type { ResolveRoleOptions, Role, RoleInfo, ScalingSignal } from './roles';
|
|
280
487
|
export { DEFAULT_ROLE, isRole, ROLE_INFO, ROLES, resolveRole } from './roles';
|
|
281
|
-
export
|
|
282
|
-
export {
|
|
283
|
-
connections,
|
|
284
|
-
jobs,
|
|
285
|
-
queueDepth,
|
|
286
|
-
recordConnection,
|
|
287
|
-
recordJob,
|
|
288
|
-
recordQueueDepth,
|
|
289
|
-
recordRequest,
|
|
290
|
-
requestDuration,
|
|
291
|
-
requests,
|
|
292
|
-
SCALING_METRICS,
|
|
293
|
-
} from './runtime-metrics';
|
|
294
|
-
export type { Secret } from './secret';
|
|
295
|
-
export {
|
|
296
|
-
isSecret,
|
|
297
|
-
revealOptionalSecret,
|
|
298
|
-
revealSecret,
|
|
299
|
-
SECRET_BRAND,
|
|
300
|
-
secret,
|
|
301
|
-
} from './secret';
|
|
488
|
+
export { safeUrl, URL_ATTRIBUTES } from './safe-url';
|
|
302
489
|
export type { ServiceFactory } from './service';
|
|
303
490
|
export { defineService, resetServices } from './service';
|
|
304
|
-
export
|
|
305
|
-
AttributeValue,
|
|
306
|
-
MemoryExporter,
|
|
307
|
-
ReadableSpan,
|
|
308
|
-
Span,
|
|
309
|
-
SpanAttributes,
|
|
310
|
-
SpanContext,
|
|
311
|
-
SpanEvent,
|
|
312
|
-
SpanExporter,
|
|
313
|
-
SpanKind,
|
|
314
|
-
SpanResource,
|
|
315
|
-
SpanStatus,
|
|
316
|
-
SpanStatusCode,
|
|
317
|
-
StartSpanOptions,
|
|
318
|
-
TelemetryOptions,
|
|
319
|
-
} from './telemetry';
|
|
491
|
+
export { timingSafeEqual } from './timing-safe-equal';
|
|
320
492
|
export {
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
resetTelemetry,
|
|
328
|
-
serviceResource,
|
|
329
|
-
startSpan,
|
|
330
|
-
traceparent,
|
|
331
|
-
withSpan,
|
|
332
|
-
withSpanContext,
|
|
333
|
-
} from './telemetry';
|
|
334
|
-
export { FRAMEWORK_VERSION, readPackageVersion, VERSION_MANIFEST } from './version';
|
|
493
|
+
frameworkVersion,
|
|
494
|
+
readPackageVersion,
|
|
495
|
+
resolveVersion,
|
|
496
|
+
VERSION_DEFINE,
|
|
497
|
+
VERSION_MANIFEST,
|
|
498
|
+
} from './version';
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// Single responsibility: run one shutdown hook under the drain's remaining budget. A hook that
|
|
2
|
+
// overruns is ABANDONED — still running, no longer awaited — because a deadline that only logged
|
|
3
|
+
// leaves the kubelet to SIGKILL the process, which is the failure the budget exists to prevent.
|
|
4
|
+
// Split out of `lifecycle.ts` so the race has no access to the drain's module state.
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* What became of one hook. `abandoned` is the only outcome the caller must report: the hook is
|
|
8
|
+
* still running, nothing will ever read its result, and whatever it had in flight may be
|
|
9
|
+
* incomplete — which is the price of a deadline and has to be visible in the log line.
|
|
10
|
+
*/
|
|
11
|
+
export type HookOutcome =
|
|
12
|
+
| { readonly kind: 'settled' }
|
|
13
|
+
| { readonly kind: 'failed'; readonly error: unknown }
|
|
14
|
+
| { readonly kind: 'abandoned' };
|
|
15
|
+
|
|
16
|
+
const SETTLED: HookOutcome = Object.freeze({ kind: 'settled' });
|
|
17
|
+
const ABANDONED: HookOutcome = Object.freeze({ kind: 'abandoned' });
|
|
18
|
+
|
|
19
|
+
const failed = (error: unknown): HookOutcome => ({ kind: 'failed', error });
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* `work()` raced against `budgetMs` of REAL elapsed time. `budgetMs` is required and is a `number`:
|
|
23
|
+
* every drain has a budget, so "no budget" is not a case this has to answer for, and the signature
|
|
24
|
+
* is what stops it from becoming one.
|
|
25
|
+
*
|
|
26
|
+
* A synchronous throw and a rejection are one outcome (`failed`); the caller logs both the same
|
|
27
|
+
* way. Nothing here throws: a drain that rejects never reaches `process.exit(0)`.
|
|
28
|
+
*/
|
|
29
|
+
export function settleWithin(
|
|
30
|
+
work: () => void | Promise<void>,
|
|
31
|
+
budgetMs: number,
|
|
32
|
+
): Promise<HookOutcome> {
|
|
33
|
+
let started: void | Promise<void>;
|
|
34
|
+
try {
|
|
35
|
+
started = work();
|
|
36
|
+
} catch (error) {
|
|
37
|
+
return Promise.resolve(failed(error));
|
|
38
|
+
}
|
|
39
|
+
const pending = Promise.resolve(started);
|
|
40
|
+
return new Promise<HookOutcome>((resolve) => {
|
|
41
|
+
let decided = false;
|
|
42
|
+
const timer = setTimeout(
|
|
43
|
+
() => {
|
|
44
|
+
if (decided) return;
|
|
45
|
+
decided = true;
|
|
46
|
+
resolve(ABANDONED);
|
|
47
|
+
},
|
|
48
|
+
Math.max(0, budgetMs),
|
|
49
|
+
);
|
|
50
|
+
// A budget already spent still gives a synchronous hook its turn — a resolved promise settles
|
|
51
|
+
// on a microtask and this timer on a macrotask — so closing a pool costs nothing it does not
|
|
52
|
+
// already have. It must also never be the thing keeping a drained process alive.
|
|
53
|
+
timer.unref?.();
|
|
54
|
+
// Attached unconditionally, on both settle paths: an abandoned hook that rejects later has
|
|
55
|
+
// nobody left awaiting it, and an unhandled rejection would kill the process this drain is
|
|
56
|
+
// trying to end cleanly. After the decision the outcome is dropped on purpose — the overrun
|
|
57
|
+
// was already reported, and a second line about a hook nobody is waiting for is noise.
|
|
58
|
+
pending.then(
|
|
59
|
+
() => {
|
|
60
|
+
if (decided) return;
|
|
61
|
+
decided = true;
|
|
62
|
+
clearTimeout(timer);
|
|
63
|
+
resolve(SETTLED);
|
|
64
|
+
},
|
|
65
|
+
(error: unknown) => {
|
|
66
|
+
if (decided) return;
|
|
67
|
+
decided = true;
|
|
68
|
+
clearTimeout(timer);
|
|
69
|
+
resolve(failed(error));
|
|
70
|
+
},
|
|
71
|
+
);
|
|
72
|
+
});
|
|
73
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// Single responsibility: the one condition a lifecycle that has already drained raises, and the
|
|
2
|
+
// error that carries it. Split from `lifecycle.ts` so the state machine reads as a state machine,
|
|
3
|
+
// and registered here rather than in `error-codes.ts` for the reason `secrets-errors.ts` gives:
|
|
4
|
+
// the code and the module that throws it ship together.
|
|
5
|
+
|
|
6
|
+
import { registerErrorCodes } from './error-codes';
|
|
7
|
+
import { UltimateError } from './errors';
|
|
8
|
+
|
|
9
|
+
registerErrorCodes({
|
|
10
|
+
X_LIFECYCLE_DRAINED: { title: 'a drained process cannot become ready again' },
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* A role tried to become ready in a process whose lifecycle has already drained.
|
|
15
|
+
*
|
|
16
|
+
* Loud rather than tolerant, because the silent version was measured and is worse than a failed
|
|
17
|
+
* boot: `state` never leaves `stopped` and `drain()` has memoized its promise, so a second
|
|
18
|
+
* `createServer().start()` bound a real port, answered `X_DRAINING` (503) to every request, and was
|
|
19
|
+
* still accepting connections after its own `stop()` returned — a dead listener holding a port,
|
|
20
|
+
* with no log line naming what happened.
|
|
21
|
+
*
|
|
22
|
+
* The `fix` has to answer two readers, because there are exactly two ways here: a boot that starts
|
|
23
|
+
* a role after SIGTERM has already arrived, and a test that drained in one case and built a server
|
|
24
|
+
* in the next.
|
|
25
|
+
*/
|
|
26
|
+
export function lifecycleDrained(state: 'draining' | 'stopped'): UltimateError {
|
|
27
|
+
return new UltimateError({
|
|
28
|
+
code: 'X_LIFECYCLE_DRAINED',
|
|
29
|
+
cause: `this process is ${state} — its lifecycle has already drained, and a drain is terminal: one process, one lifecycle. A role marked ready now would bind a socket that answers 503 to every request, and drain() memoizes, so nothing would ever close it`,
|
|
30
|
+
fix: 'start every role BEFORE the first drain — a drained process is on its way out and is replaced, never reused. In a test, call resetLifecycle() from @ultimat3/core between the drain and the next start()',
|
|
31
|
+
meta: { state },
|
|
32
|
+
});
|
|
33
|
+
}
|