@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,100 @@
|
|
|
1
|
+
// Single responsibility: is this error worth trying again? One classification per code, carried on
|
|
2
|
+
// every `UltimateError` and in `--json`, so a client never has to hardcode a table of codes.
|
|
3
|
+
// The cycle with ./errors is the same deliberate one ./error-codes has: nothing here touches
|
|
4
|
+
// UltimateError at module-evaluation time.
|
|
5
|
+
|
|
6
|
+
import { UltimateError } from './errors';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* `terminal` — the same call will fail the same way forever (a config fault, a validation error,
|
|
10
|
+
* a permission denial). `retryable` — a transient failure; back off and try again. `retry-after`
|
|
11
|
+
* — retryable, but the responder said WHEN, and the client must read `Retry-After` / the error's
|
|
12
|
+
* `meta` rather than guessing.
|
|
13
|
+
*/
|
|
14
|
+
export type ErrorRetry = 'terminal' | 'retryable' | 'retry-after';
|
|
15
|
+
|
|
16
|
+
export const ERROR_RETRY_KINDS = ['terminal', 'retryable', 'retry-after'] as const;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Fail closed. An unclassified code is a code nobody thought about, and a client that retries one
|
|
20
|
+
* during an incident triples the load on a service that is already broken — `X_TENANCY_UNSCOPED`
|
|
21
|
+
* and `X_DB_DRIFT` are 500s and permanent config faults, so `status >= 500` was never the answer.
|
|
22
|
+
*/
|
|
23
|
+
export const DEFAULT_ERROR_RETRY: ErrorRetry = 'terminal';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Core's own codes, and closed: an app that could reclassify `X_DRAINING` as terminal would be an
|
|
27
|
+
* app whose clients stop retrying a rolling restart, which is the one case retrying always wins.
|
|
28
|
+
* Only the exceptions are listed — everything else is `terminal` by the default above.
|
|
29
|
+
*/
|
|
30
|
+
const CORE_ERROR_RETRY: Readonly<Record<string, ErrorRetry>> = Object.freeze({
|
|
31
|
+
X_DRAINING: 'retryable',
|
|
32
|
+
// A deadline that expired is the canonical back-off-and-try-again case: nothing about the
|
|
33
|
+
// request was wrong, the budget ran out. Deliberately NOT `retry-after` — that spelling means
|
|
34
|
+
// the responder named a time, and a timeout by definition produced no such answer. Its twin
|
|
35
|
+
// `X_ABORTED` (the caller went away) is left to the `terminal` DEFAULT rather than listed here:
|
|
36
|
+
// the answer is the same, and listing it would close a door nobody has asked to open.
|
|
37
|
+
X_TIMEOUT: 'retryable',
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const REGISTERED = new Map<string, ErrorRetry>();
|
|
41
|
+
|
|
42
|
+
export function isErrorRetry(value: unknown): value is ErrorRetry {
|
|
43
|
+
return typeof value === 'string' && (ERROR_RETRY_KINDS as readonly string[]).includes(value);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function retryInvalid(code: string, cause: string): UltimateError {
|
|
47
|
+
return new UltimateError({
|
|
48
|
+
code: 'X_ERROR_RETRY_INVALID',
|
|
49
|
+
cause: `${code}: ${cause}`,
|
|
50
|
+
fix: `x errors list --json # then registerErrorRetry({ ${code}: 'retryable' }) with one of ${ERROR_RETRY_KINDS.join(' | ')}`,
|
|
51
|
+
meta: { code },
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Declare how the codes this package or app throws should be retried. Call it once at boot beside
|
|
57
|
+
* the module that declares the codes — importing that module IS the registration, the convention
|
|
58
|
+
* `registerErrorCodes` and `registerErrorStatus` already use.
|
|
59
|
+
*
|
|
60
|
+
* ```ts
|
|
61
|
+
* registerErrorRetry({ X_OAUTH_EXCHANGE_FAILED: 'retryable', X_RATE_LIMITED: 'retry-after' });
|
|
62
|
+
* ```
|
|
63
|
+
*
|
|
64
|
+
* Re-registering the same value is fine — a module imported twice is not a bug. Registering a
|
|
65
|
+
* DIFFERENT value throws, so two packages can never disagree about whether a code is safe to
|
|
66
|
+
* hammer, and core's own codes cannot be moved at all.
|
|
67
|
+
*/
|
|
68
|
+
export function registerErrorRetry(retries: Readonly<Record<string, ErrorRetry>>): void {
|
|
69
|
+
for (const [code, retry] of Object.entries(retries)) {
|
|
70
|
+
if (!isErrorRetry(retry)) {
|
|
71
|
+
throw retryInvalid(code, `"${String(retry)}" is not ${ERROR_RETRY_KINDS.join(' | ')}`);
|
|
72
|
+
}
|
|
73
|
+
const core = CORE_ERROR_RETRY[code];
|
|
74
|
+
if (core !== undefined) {
|
|
75
|
+
throw retryInvalid(code, `the framework already classifies it as ${core}`);
|
|
76
|
+
}
|
|
77
|
+
const existing = REGISTERED.get(code);
|
|
78
|
+
if (existing !== undefined && existing !== retry) {
|
|
79
|
+
throw retryInvalid(code, `already registered as ${existing}`);
|
|
80
|
+
}
|
|
81
|
+
REGISTERED.set(code, retry);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Test seam. Production registers once at boot and never unregisters. */
|
|
86
|
+
export function resetErrorRetry(): void {
|
|
87
|
+
REGISTERED.clear();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Core table first: `registerErrorRetry` already refuses those codes, so the order is
|
|
91
|
+
// belt-and-braces — but it is the belt that keeps "core's classifications are fixed" true even if
|
|
92
|
+
// a future caller reaches the map some other way.
|
|
93
|
+
export function retryFor(code: string): ErrorRetry {
|
|
94
|
+
return CORE_ERROR_RETRY[code] ?? REGISTERED.get(code) ?? DEFAULT_ERROR_RETRY;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Every classification a package or app declared, for `x errors list` and the manifest. */
|
|
98
|
+
export function registeredErrorRetry(): Readonly<Record<string, ErrorRetry>> {
|
|
99
|
+
return Object.fromEntries([...REGISTERED].sort(([a], [b]) => a.localeCompare(b)));
|
|
100
|
+
}
|
package/src/errors.ts
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
// overlay and `--json`. Never throw a bare Error anywhere in the framework.
|
|
4
4
|
|
|
5
5
|
import { describeErrorCode } from './error-codes';
|
|
6
|
+
import { isThrownError, renderCauseValue, renderMetaRecord, renderThrowable } from './error-render';
|
|
7
|
+
import { DEFAULT_ERROR_RETRY, type ErrorRetry, isErrorRetry, retryFor } from './error-retry';
|
|
6
8
|
|
|
7
9
|
/**
|
|
8
10
|
* Structural brand. `instanceof` is unreliable across duplicated module instances and across
|
|
@@ -20,6 +22,12 @@ export interface UltimateErrorInit {
|
|
|
20
22
|
readonly fix: string;
|
|
21
23
|
readonly docs?: string | undefined;
|
|
22
24
|
readonly meta?: Readonly<Record<string, unknown>> | undefined;
|
|
25
|
+
/**
|
|
26
|
+
* Overrides the code's registered classification for this one throw — the same code can be
|
|
27
|
+
* transient at one call site and permanent at another. Defaults to `retryFor(code)`, which
|
|
28
|
+
* defaults to `terminal`.
|
|
29
|
+
*/
|
|
30
|
+
readonly retry?: ErrorRetry | undefined;
|
|
23
31
|
/** The underlying thrown value, when this error wraps one. */
|
|
24
32
|
readonly sourceError?: unknown;
|
|
25
33
|
}
|
|
@@ -30,6 +38,8 @@ export interface UltimateErrorJSON {
|
|
|
30
38
|
readonly cause: string;
|
|
31
39
|
readonly fix: string;
|
|
32
40
|
readonly docs: string;
|
|
41
|
+
/** Whether a client may try again. Always present — a client never has to infer it. */
|
|
42
|
+
readonly retry: ErrorRetry;
|
|
33
43
|
readonly meta?: Readonly<Record<string, unknown>> | undefined;
|
|
34
44
|
readonly stack?: string | undefined;
|
|
35
45
|
}
|
|
@@ -48,6 +58,7 @@ export class UltimateError extends Error {
|
|
|
48
58
|
declare readonly cause: string;
|
|
49
59
|
readonly fix: string;
|
|
50
60
|
readonly docs: string;
|
|
61
|
+
readonly retry: ErrorRetry;
|
|
51
62
|
readonly meta: Readonly<Record<string, unknown>> | undefined;
|
|
52
63
|
readonly sourceError: unknown;
|
|
53
64
|
|
|
@@ -63,6 +74,7 @@ export class UltimateError extends Error {
|
|
|
63
74
|
this.title = described.title;
|
|
64
75
|
this.fix = init.fix;
|
|
65
76
|
this.docs = init.docs ?? described.docs;
|
|
77
|
+
this.retry = init.retry ?? retryFor(init.code);
|
|
66
78
|
this.meta = init.meta;
|
|
67
79
|
this.sourceError = init.sourceError;
|
|
68
80
|
}
|
|
@@ -82,6 +94,13 @@ export class UltimateError extends Error {
|
|
|
82
94
|
return lines.join('\n');
|
|
83
95
|
}
|
|
84
96
|
|
|
97
|
+
/**
|
|
98
|
+
* `meta` is the only field here the framework does not build itself — `parseId` puts the value
|
|
99
|
+
* it rejected straight in — so it goes through `renderMetaRecord`, which returns a record that
|
|
100
|
+
* serialises unchanged and degrades only the keys that would have thrown. `--json` on every
|
|
101
|
+
* error is a promise this method keeps; a `meta` that throws breaks it one layer past the
|
|
102
|
+
* constructor.
|
|
103
|
+
*/
|
|
85
104
|
toJSON(): UltimateErrorJSON {
|
|
86
105
|
return {
|
|
87
106
|
code: this.code,
|
|
@@ -89,14 +108,22 @@ export class UltimateError extends Error {
|
|
|
89
108
|
cause: this.cause,
|
|
90
109
|
fix: this.fix,
|
|
91
110
|
docs: this.docs,
|
|
92
|
-
|
|
111
|
+
retry: this.retry,
|
|
112
|
+
meta: renderMetaRecord(this.meta),
|
|
93
113
|
stack: this.stack,
|
|
94
114
|
};
|
|
95
115
|
}
|
|
96
116
|
}
|
|
97
117
|
|
|
98
118
|
export function isUltimateError(value: unknown): value is UltimateError {
|
|
99
|
-
|
|
119
|
+
// TOTAL, like `isThrownError`: `in` runs a `Proxy`'s `has` trap, and every caller asks this
|
|
120
|
+
// question inside a `catch` block that has nothing left to answer with if the probe itself
|
|
121
|
+
// throws. `false` is the honest answer for a value that refuses to be examined.
|
|
122
|
+
try {
|
|
123
|
+
return typeof value === 'object' && value !== null && ULTIMATE_ERROR_BRAND in value;
|
|
124
|
+
} catch {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
100
127
|
}
|
|
101
128
|
|
|
102
129
|
/** Init for a subclass that owns its code. */
|
|
@@ -139,13 +166,21 @@ export function notImplemented(feature: string, fix: string): never {
|
|
|
139
166
|
throw new NotImplementedError({ cause: `${feature} is not implemented by this driver`, fix });
|
|
140
167
|
}
|
|
141
168
|
|
|
142
|
-
/**
|
|
169
|
+
/**
|
|
170
|
+
* Normalise anything caught into an `UltimateError` without losing the original.
|
|
171
|
+
*
|
|
172
|
+
* `renderCauseValue`, not `String(value)`: this is the framework's universal normaliser — the CLI's
|
|
173
|
+
* every catch, `formatError`, the HTTP 500 path — and `String()` runs the value's own `toString`,
|
|
174
|
+
* so a thrown object could make the wrapper throw and take both errors with it.
|
|
175
|
+
*/
|
|
143
176
|
export function toUltimateError(value: unknown, fix?: string): UltimateError {
|
|
144
177
|
if (isUltimateError(value)) return value;
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
178
|
+
// `isThrownError` / `renderThrowable`, not `instanceof` and `.message` directly: a `Proxy` traps
|
|
179
|
+
// `getPrototypeOf` and a subclass can put a getter on `message`, so both reads throw where this
|
|
180
|
+
// function is the last thing standing between a caught value and a surface that must answer.
|
|
181
|
+
const cause = isThrownError(value)
|
|
182
|
+
? renderThrowable(value)
|
|
183
|
+
: `non-error value thrown: ${renderCauseValue(value)}`;
|
|
149
184
|
return new InternalError({
|
|
150
185
|
cause,
|
|
151
186
|
fix: fix ?? 'fix the underlying failure named in cause, then re-run',
|
|
@@ -153,6 +188,19 @@ export function toUltimateError(value: unknown, fix?: string): UltimateError {
|
|
|
153
188
|
});
|
|
154
189
|
}
|
|
155
190
|
|
|
191
|
+
/**
|
|
192
|
+
* May a client try this again? The one question a retry loop asks, answered from the error rather
|
|
193
|
+
* than from a status code — `X_DB_DRIFT` and `X_TENANCY_UNSCOPED` are both 500s and neither is
|
|
194
|
+
* worth a second attempt. A value that is not an Ultimate error is `terminal`: fail closed.
|
|
195
|
+
*/
|
|
196
|
+
export function errorRetry(value: unknown): ErrorRetry {
|
|
197
|
+
if (!isUltimateError(value)) return DEFAULT_ERROR_RETRY;
|
|
198
|
+
// Read defensively: the brand is duck-typed across duplicated module instances, so a value from
|
|
199
|
+
// an older copy of this package can satisfy the guard without carrying the field.
|
|
200
|
+
const retry: unknown = value.retry;
|
|
201
|
+
return isErrorRetry(retry) ? retry : DEFAULT_ERROR_RETRY;
|
|
202
|
+
}
|
|
203
|
+
|
|
156
204
|
/** Render any caught value with the 3-line contract, so CLI output never varies. */
|
|
157
205
|
export function formatError(value: unknown, options?: FormatErrorOptions): string {
|
|
158
206
|
return toUltimateError(value).format(options);
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// The error-contract slice of `@ultimat3/core`'s public surface: `UltimateError` and its shipped
|
|
2
|
+
// subclasses, the code registry every package registers into, the safe renderers a message is
|
|
3
|
+
// built with, and the retry classification a code carries. One group because a code, its title,
|
|
4
|
+
// its rendering and its retry class are one contract; `index.ts` re-exports every name explicitly.
|
|
5
|
+
|
|
6
|
+
export type {
|
|
7
|
+
CoreErrorCode,
|
|
8
|
+
ErrorCodeDeclaration,
|
|
9
|
+
ErrorCodeDescriptor,
|
|
10
|
+
ErrorCodeEntry,
|
|
11
|
+
} from '../error-codes';
|
|
12
|
+
export {
|
|
13
|
+
CORE_ERROR_CODES,
|
|
14
|
+
describeErrorCode,
|
|
15
|
+
ERROR_DOCS_BASE,
|
|
16
|
+
errorCodeSnapshot,
|
|
17
|
+
errorDocsUrl,
|
|
18
|
+
hasErrorCode,
|
|
19
|
+
listErrorCodes,
|
|
20
|
+
registerErrorCodes,
|
|
21
|
+
resetErrorCodes,
|
|
22
|
+
} from '../error-codes';
|
|
23
|
+
export {
|
|
24
|
+
describeValue,
|
|
25
|
+
isThrownError,
|
|
26
|
+
MAX_RENDERED_LENGTH,
|
|
27
|
+
renderCauseValue,
|
|
28
|
+
renderFixLiteral,
|
|
29
|
+
renderThrowable,
|
|
30
|
+
stringField,
|
|
31
|
+
} from '../error-render';
|
|
32
|
+
export type { ErrorRetry } from '../error-retry';
|
|
33
|
+
export {
|
|
34
|
+
DEFAULT_ERROR_RETRY,
|
|
35
|
+
ERROR_RETRY_KINDS,
|
|
36
|
+
isErrorRetry,
|
|
37
|
+
registerErrorRetry,
|
|
38
|
+
registeredErrorRetry,
|
|
39
|
+
resetErrorRetry,
|
|
40
|
+
retryFor,
|
|
41
|
+
} from '../error-retry';
|
|
42
|
+
export type {
|
|
43
|
+
CodedErrorInit,
|
|
44
|
+
FormatErrorOptions,
|
|
45
|
+
UltimateErrorInit,
|
|
46
|
+
UltimateErrorJSON,
|
|
47
|
+
} from '../errors';
|
|
48
|
+
export {
|
|
49
|
+
ConfigInvalidError,
|
|
50
|
+
EnvMissingError,
|
|
51
|
+
errorRetry,
|
|
52
|
+
formatError,
|
|
53
|
+
InternalError,
|
|
54
|
+
isUltimateError,
|
|
55
|
+
NotImplementedError,
|
|
56
|
+
notImplemented,
|
|
57
|
+
toUltimateError,
|
|
58
|
+
ULTIMATE_ERROR_BRAND,
|
|
59
|
+
UltimateError,
|
|
60
|
+
} from '../errors';
|
|
61
|
+
export { SCHEMA_ERROR_CODE_TITLES } from '../schema-error-codes';
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
// The observability slice of `@ultimat3/core`'s public surface, in one place: logging, metrics,
|
|
2
|
+
// tracing, sampling, the OTLP transports and error reporting. One group because they are one
|
|
3
|
+
// subject — a process's own account of what it did — and `index.ts` re-exports every name below
|
|
4
|
+
// explicitly, so this file changes what a reader looks at and never what the package exports.
|
|
5
|
+
|
|
6
|
+
export type {
|
|
7
|
+
ErrorReport,
|
|
8
|
+
ErrorReporter,
|
|
9
|
+
ErrorReportingOptions,
|
|
10
|
+
ErrorScope,
|
|
11
|
+
ErrorSeverity,
|
|
12
|
+
ErrorSource,
|
|
13
|
+
MemoryErrorReporter,
|
|
14
|
+
ReportErrorOptions,
|
|
15
|
+
} from '../error-reporter';
|
|
16
|
+
export {
|
|
17
|
+
configureErrorReporting,
|
|
18
|
+
ERROR_SOURCES,
|
|
19
|
+
errorReport,
|
|
20
|
+
memoryErrorReporter,
|
|
21
|
+
noopErrorReporter,
|
|
22
|
+
reportError,
|
|
23
|
+
resetErrorReporting,
|
|
24
|
+
} from '../error-reporter';
|
|
25
|
+
export type {
|
|
26
|
+
SentryDsn,
|
|
27
|
+
SentryEnvelopeOptions,
|
|
28
|
+
SentryReporterOptions,
|
|
29
|
+
} from '../error-reporter-sentry';
|
|
30
|
+
export {
|
|
31
|
+
ErrorReporterDsnInvalidError,
|
|
32
|
+
parseSentryDsn,
|
|
33
|
+
sentryEnvelope,
|
|
34
|
+
sentryErrorReporter,
|
|
35
|
+
} from '../error-reporter-sentry';
|
|
36
|
+
export type { LogFields, Logger, LoggerOptions, LogLevel } from '../logger';
|
|
37
|
+
export {
|
|
38
|
+
createLogger,
|
|
39
|
+
isRedactedKey,
|
|
40
|
+
LOG_LEVELS,
|
|
41
|
+
logger,
|
|
42
|
+
REDACTED,
|
|
43
|
+
redactKeys,
|
|
44
|
+
setLoggerContextFields,
|
|
45
|
+
} from '../logger';
|
|
46
|
+
export type {
|
|
47
|
+
Counter,
|
|
48
|
+
Gauge,
|
|
49
|
+
GaugeOptions,
|
|
50
|
+
Histogram,
|
|
51
|
+
HistogramOptions,
|
|
52
|
+
HistogramPoint,
|
|
53
|
+
InstrumentOptions,
|
|
54
|
+
MemoryMetricExporter,
|
|
55
|
+
MetricAttributes,
|
|
56
|
+
MetricAttributeValue,
|
|
57
|
+
MetricCollection,
|
|
58
|
+
MetricDescriptor,
|
|
59
|
+
MetricExporter,
|
|
60
|
+
MetricKind,
|
|
61
|
+
MetricPoint,
|
|
62
|
+
MetricsOptions,
|
|
63
|
+
ReadableMetric,
|
|
64
|
+
} from '../metrics';
|
|
65
|
+
export {
|
|
66
|
+
collectMetrics,
|
|
67
|
+
configureMetrics,
|
|
68
|
+
counter,
|
|
69
|
+
DEFAULT_HISTOGRAM_BOUNDS,
|
|
70
|
+
DEFAULT_MAX_SERIES,
|
|
71
|
+
exportMetrics,
|
|
72
|
+
gauge,
|
|
73
|
+
histogram,
|
|
74
|
+
MetricCardinalityError,
|
|
75
|
+
MetricNameInvalidError,
|
|
76
|
+
MetricValueInvalidError,
|
|
77
|
+
memoryMetricExporter,
|
|
78
|
+
noopMetricExporter,
|
|
79
|
+
OVERFLOW_ATTRIBUTE,
|
|
80
|
+
resetMetrics,
|
|
81
|
+
startMetricExport,
|
|
82
|
+
} from '../metrics';
|
|
83
|
+
export { METRICS_CONTENT_TYPE, METRICS_PATH, metricsText } from '../metrics-text';
|
|
84
|
+
export type { OtlpAnyValue, OtlpKeyValue, OtlpSignal } from '../otlp';
|
|
85
|
+
export {
|
|
86
|
+
OTLP_ENDPOINT_KEY,
|
|
87
|
+
OTLP_HEADERS_KEY,
|
|
88
|
+
OTLP_PROTOCOL_KEY,
|
|
89
|
+
OTLP_SCOPE,
|
|
90
|
+
OtlpEndpointInvalidError,
|
|
91
|
+
OtlpProtocolUnsupportedError,
|
|
92
|
+
otlpAttributes,
|
|
93
|
+
otlpEndpoint,
|
|
94
|
+
otlpHeaders,
|
|
95
|
+
otlpResource,
|
|
96
|
+
tryOtlpEndpoint,
|
|
97
|
+
unixNano,
|
|
98
|
+
} from '../otlp';
|
|
99
|
+
export type { OtlpMetricExporter, OtlpMetricExporterOptions } from '../otlp-metric-exporter';
|
|
100
|
+
export { otlpMetricExporter, otlpMetricsRequest } from '../otlp-metric-exporter';
|
|
101
|
+
export type { OtlpSpanExporter, OtlpSpanExporterOptions } from '../otlp-span-exporter';
|
|
102
|
+
export { otlpSpanExporter, otlpTraceRequest } from '../otlp-span-exporter';
|
|
103
|
+
export type { RequestSample } from '../runtime-metrics';
|
|
104
|
+
export {
|
|
105
|
+
connections,
|
|
106
|
+
jobs,
|
|
107
|
+
leasesLost,
|
|
108
|
+
queueDepth,
|
|
109
|
+
recordConnection,
|
|
110
|
+
recordJob,
|
|
111
|
+
recordLeaseLost,
|
|
112
|
+
recordQueueDepth,
|
|
113
|
+
recordRequest,
|
|
114
|
+
requestDuration,
|
|
115
|
+
requests,
|
|
116
|
+
SCALING_METRICS,
|
|
117
|
+
} from '../runtime-metrics';
|
|
118
|
+
export type { Sampler } from '../sampler';
|
|
119
|
+
export {
|
|
120
|
+
alwaysOffSampler,
|
|
121
|
+
alwaysOnSampler,
|
|
122
|
+
DEFAULT_SAMPLE_RATIO,
|
|
123
|
+
defaultSampler,
|
|
124
|
+
OTEL_SAMPLER_ARG_KEY,
|
|
125
|
+
OTEL_SAMPLER_KEY,
|
|
126
|
+
parentBasedRatioSampler,
|
|
127
|
+
ratioSampler,
|
|
128
|
+
resetDefaultSampler,
|
|
129
|
+
samplerFromEnv,
|
|
130
|
+
} from '../sampler';
|
|
131
|
+
export type {
|
|
132
|
+
AttributeValue,
|
|
133
|
+
MemoryExporter,
|
|
134
|
+
ReadableSpan,
|
|
135
|
+
Span,
|
|
136
|
+
SpanAttributes,
|
|
137
|
+
SpanContext,
|
|
138
|
+
SpanEvent,
|
|
139
|
+
SpanExporter,
|
|
140
|
+
SpanKind,
|
|
141
|
+
SpanResource,
|
|
142
|
+
SpanStatus,
|
|
143
|
+
SpanStatusCode,
|
|
144
|
+
StartSpanOptions,
|
|
145
|
+
TelemetryOptions,
|
|
146
|
+
} from '../telemetry';
|
|
147
|
+
export {
|
|
148
|
+
configureTelemetry,
|
|
149
|
+
currentSampler,
|
|
150
|
+
currentSpan,
|
|
151
|
+
currentSpanContext,
|
|
152
|
+
memoryExporter,
|
|
153
|
+
noopExporter,
|
|
154
|
+
parseTraceparent,
|
|
155
|
+
resetTelemetry,
|
|
156
|
+
serviceResource,
|
|
157
|
+
startSpan,
|
|
158
|
+
traceparent,
|
|
159
|
+
withSpan,
|
|
160
|
+
withSpanContext,
|
|
161
|
+
} from '../telemetry';
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// The secrets slice of `@ultimat3/core`'s public surface: the redacted-by-value `Secret`, the
|
|
2
|
+
// committed envelope, the files and install path around it, and the codes it throws. One group
|
|
3
|
+
// because they are one path — a committed ciphertext to a value `defineEnv` can read — and
|
|
4
|
+
// `index.ts` re-exports every name below explicitly, so the package's surface is unchanged.
|
|
5
|
+
|
|
6
|
+
export type { Secret } from '../secret';
|
|
7
|
+
export {
|
|
8
|
+
isSecret,
|
|
9
|
+
revealOptionalSecret,
|
|
10
|
+
revealSecret,
|
|
11
|
+
SECRET_BRAND,
|
|
12
|
+
secret,
|
|
13
|
+
} from '../secret';
|
|
14
|
+
export type {
|
|
15
|
+
SecretSummary,
|
|
16
|
+
SecretsEnvelope,
|
|
17
|
+
SecretsLocation,
|
|
18
|
+
SecretValues,
|
|
19
|
+
} from '../secrets';
|
|
20
|
+
export {
|
|
21
|
+
assertSecretValues,
|
|
22
|
+
describeSecrets,
|
|
23
|
+
generateMasterKey,
|
|
24
|
+
masterKeyId,
|
|
25
|
+
openSecrets,
|
|
26
|
+
parseMasterKey,
|
|
27
|
+
parseSecretsEnvelope,
|
|
28
|
+
SECRET_NAME,
|
|
29
|
+
SECRETS_ALG,
|
|
30
|
+
SECRETS_IV_BYTES,
|
|
31
|
+
SECRETS_KEY_BYTES,
|
|
32
|
+
SECRETS_KEY_HEX_LENGTH,
|
|
33
|
+
SECRETS_KEY_ID_LENGTH,
|
|
34
|
+
SECRETS_TAG_BYTES,
|
|
35
|
+
SECRETS_VERSION,
|
|
36
|
+
sealSecrets,
|
|
37
|
+
serializeSecretValues,
|
|
38
|
+
} from '../secrets';
|
|
39
|
+
export type { SecretsErrorCode } from '../secrets-errors';
|
|
40
|
+
export {
|
|
41
|
+
SECRETS_ERROR_CODES,
|
|
42
|
+
SecretsFileInvalidError,
|
|
43
|
+
SecretsFileMissingError,
|
|
44
|
+
SecretsKeyInvalidError,
|
|
45
|
+
SecretsKeyMismatchError,
|
|
46
|
+
SecretsKeyMissingError,
|
|
47
|
+
SecretsPlaintextInvalidError,
|
|
48
|
+
SecretsTamperedError,
|
|
49
|
+
} from '../secrets-errors';
|
|
50
|
+
export type {
|
|
51
|
+
MasterKeyRef,
|
|
52
|
+
MasterKeySource,
|
|
53
|
+
SecretsInstallOptions,
|
|
54
|
+
SecretsInstallReport,
|
|
55
|
+
} from '../secrets-store';
|
|
56
|
+
export {
|
|
57
|
+
findMasterKey,
|
|
58
|
+
installSecrets,
|
|
59
|
+
masterKeyIdOf,
|
|
60
|
+
masterKeyPath,
|
|
61
|
+
readSecretsFile,
|
|
62
|
+
requireMasterKey,
|
|
63
|
+
SECRETS_FILE,
|
|
64
|
+
SECRETS_KEY_ENV,
|
|
65
|
+
SECRETS_KEY_FILE,
|
|
66
|
+
SECRETS_KEY_MODE,
|
|
67
|
+
secretsFileExists,
|
|
68
|
+
secretsPath,
|
|
69
|
+
writeMasterKeyFile,
|
|
70
|
+
writeSecretsFile,
|
|
71
|
+
} from '../secrets-store';
|
package/src/ids.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// database indexes, cursors and log sorting all want time-ordered keys.
|
|
3
3
|
|
|
4
4
|
import { type Clock, systemClock } from './clock';
|
|
5
|
+
import { describeValue } from './error-render';
|
|
5
6
|
import { UltimateError } from './errors';
|
|
6
7
|
|
|
7
8
|
/** Nominal typing without a runtime cost. `Brand<string, 'post'>` never mixes with `'user'`. */
|
|
@@ -28,6 +29,16 @@ function randomBytes(length: number): Uint8Array {
|
|
|
28
29
|
return bytes;
|
|
29
30
|
}
|
|
30
31
|
|
|
32
|
+
/**
|
|
33
|
+
* A full 10 bits, from both bytes. Reading `bytes[0]` alone masked an 8-bit value with a 10-bit
|
|
34
|
+
* mask, so the seed only ever reached 255 while `COUNTER_SEED_MASK` declared 1023 — the constant
|
|
35
|
+
* and the code disagreed, and the second byte was allocated on every `uuid()` for nothing.
|
|
36
|
+
*/
|
|
37
|
+
function seedCounter(): number {
|
|
38
|
+
const bytes = randomBytes(2);
|
|
39
|
+
return (((bytes[0] ?? 0) << 8) | (bytes[1] ?? 0)) & COUNTER_SEED_MASK;
|
|
40
|
+
}
|
|
41
|
+
|
|
31
42
|
export function randomHex(byteLength: number): string {
|
|
32
43
|
const bytes = randomBytes(byteLength);
|
|
33
44
|
let out = '';
|
|
@@ -51,10 +62,10 @@ export function uuid(clock: Clock = systemClock): string {
|
|
|
51
62
|
counter += 1;
|
|
52
63
|
if (counter > COUNTER_MAX) {
|
|
53
64
|
epochMs += 1;
|
|
54
|
-
counter =
|
|
65
|
+
counter = seedCounter();
|
|
55
66
|
}
|
|
56
67
|
} else {
|
|
57
|
-
counter =
|
|
68
|
+
counter = seedCounter();
|
|
58
69
|
}
|
|
59
70
|
lastEpochMs = epochMs;
|
|
60
71
|
|
|
@@ -62,7 +73,9 @@ export function uuid(clock: Clock = systemClock): string {
|
|
|
62
73
|
const randA = counter.toString(16).padStart(3, '0');
|
|
63
74
|
const tail = randomHex(8);
|
|
64
75
|
// Force the RFC variant bits (0b10) into the first nibble of `rand_b`.
|
|
65
|
-
|
|
76
|
+
// charAt, not [], because the index is provably 0x8–0xb: a non-null assertion here would be
|
|
77
|
+
// unenforceable style debt in the one file that made `noNonNullAssertion` unraisable.
|
|
78
|
+
const variantNibble = HEX.charAt((Number.parseInt(tail.charAt(0), 16) & 0x3) | 0x8);
|
|
66
79
|
|
|
67
80
|
return [
|
|
68
81
|
timeHex.slice(0, 8),
|
|
@@ -77,14 +90,20 @@ export function isUuid(value: unknown): boolean {
|
|
|
77
90
|
return typeof value === 'string' && UUID_RE.test(value);
|
|
78
91
|
}
|
|
79
92
|
|
|
93
|
+
/** The actionable half of an id rejection: what was wanted. Carries no caller data, ever. */
|
|
94
|
+
const UUID_SHAPE = '8-4-4-4-12 lowercase hex, version 7';
|
|
95
|
+
|
|
80
96
|
/** Recover the generation instant from a v7 id — cheap debugging and cursor windows. */
|
|
81
97
|
export function uuidTimestamp(id: string): Date {
|
|
82
98
|
if (!isUuid(id)) {
|
|
83
99
|
throw new UltimateError({
|
|
84
100
|
code: 'X_ID_INVALID',
|
|
85
|
-
|
|
101
|
+
// `describeValue`, never the id itself: this `cause` reaches the log index and the HTTP
|
|
102
|
+
// problem document, and the strings that arrive here wrong are session tokens and API keys
|
|
103
|
+
// as often as they are typos. The expected shape is the half that helps the reader.
|
|
104
|
+
cause: `expected a UUIDv7 (${UUID_SHAPE}), received ${describeValue(id)}`,
|
|
86
105
|
fix: 'generate ids with uuid() from @ultimat3/core',
|
|
87
|
-
meta: { id },
|
|
106
|
+
meta: { received: describeValue(id) },
|
|
88
107
|
});
|
|
89
108
|
}
|
|
90
109
|
return new Date(Number.parseInt(id.slice(0, 8) + id.slice(9, 13), 16));
|
|
@@ -108,9 +127,11 @@ export function parseId<K extends string>(kind: K, value: unknown): Id<K> {
|
|
|
108
127
|
if (!isUuid(value)) {
|
|
109
128
|
throw new UltimateError({
|
|
110
129
|
code: 'X_ID_INVALID',
|
|
111
|
-
cause: `expected a ${kind} UUIDv7, received ${
|
|
130
|
+
cause: `expected a ${kind} UUIDv7 (${UUID_SHAPE}), received ${describeValue(value)}`,
|
|
112
131
|
fix: `pass an id produced by typedId<'${kind}'>()`,
|
|
113
|
-
|
|
132
|
+
// `received`, not `value`: `meta` rides into the problem document and the log line too, and
|
|
133
|
+
// redaction is by key — there is no key here that a redaction list could ever cover.
|
|
134
|
+
meta: { kind, received: describeValue(value) },
|
|
114
135
|
});
|
|
115
136
|
}
|
|
116
137
|
return value as Id<K>;
|
|
@@ -125,6 +146,27 @@ export function spanId(): string {
|
|
|
125
146
|
return randomHex(8);
|
|
126
147
|
}
|
|
127
148
|
|
|
149
|
+
const TRACE_ID_RE = /^[0-9a-f]{32}$/;
|
|
150
|
+
const SPAN_ID_RE = /^[0-9a-f]{16}$/;
|
|
151
|
+
const ALL_ZERO = /^0+$/;
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* The ONE definition of "is this a W3C trace id" — `traceparent` parsing, and any layer that
|
|
155
|
+
* accepts an id from outside, ask here rather than carrying a second regex.
|
|
156
|
+
*
|
|
157
|
+
* A dashed UUID is the failure this predicate exists to name: `uuid()` produces 36 characters with
|
|
158
|
+
* hyphens, every OTLP collector rejects it, and nothing downstream said so — the trace simply
|
|
159
|
+
* never appeared. Mint trace ids with `traceId()`, never `uuid()`. All-zero is invalid per the
|
|
160
|
+
* spec: it is the wire's spelling of "no trace", not a trace whose id happens to be zero.
|
|
161
|
+
*/
|
|
162
|
+
export function isTraceId(value: unknown): boolean {
|
|
163
|
+
return typeof value === 'string' && TRACE_ID_RE.test(value) && !ALL_ZERO.test(value);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function isSpanId(value: unknown): boolean {
|
|
167
|
+
return typeof value === 'string' && SPAN_ID_RE.test(value) && !ALL_ZERO.test(value);
|
|
168
|
+
}
|
|
169
|
+
|
|
128
170
|
/** Test-only: reset the monotonic counter so a frozen clock produces a fresh sequence. */
|
|
129
171
|
export function resetIdCounter(): void {
|
|
130
172
|
lastEpochMs = -1;
|