@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.
Files changed (45) hide show
  1. package/CLAUDE.md +252 -0
  2. package/README.md +210 -10
  3. package/package.json +2 -1
  4. package/src/actor.ts +118 -4
  5. package/src/app-version.ts +32 -0
  6. package/src/assert.ts +5 -1
  7. package/src/config.ts +47 -12
  8. package/src/context.ts +30 -3
  9. package/src/cursor.ts +25 -4
  10. package/src/env-example.ts +2 -1
  11. package/src/env.ts +14 -3
  12. package/src/environment.ts +39 -13
  13. package/src/error-codes.ts +13 -0
  14. package/src/error-render.ts +249 -0
  15. package/src/error-reporter-sentry.ts +175 -0
  16. package/src/error-reporter.ts +212 -0
  17. package/src/error-retry.ts +100 -0
  18. package/src/errors.ts +55 -7
  19. package/src/exports/error-contract.ts +61 -0
  20. package/src/exports/observability.ts +161 -0
  21. package/src/exports/secrets.ts +71 -0
  22. package/src/ids.ts +49 -7
  23. package/src/impersonate.ts +62 -0
  24. package/src/index.ts +277 -113
  25. package/src/lifecycle-deadline.ts +73 -0
  26. package/src/lifecycle-errors.ts +33 -0
  27. package/src/lifecycle.ts +178 -16
  28. package/src/logger.ts +99 -9
  29. package/src/mcp-exposure.ts +32 -0
  30. package/src/metrics.ts +0 -0
  31. package/src/otlp-metric-exporter.ts +136 -0
  32. package/src/otlp-span-exporter.ts +170 -0
  33. package/src/otlp.ts +217 -0
  34. package/src/read-capped.ts +47 -0
  35. package/src/runtime-metrics.ts +15 -0
  36. package/src/safe-url.ts +50 -0
  37. package/src/sampler.ts +126 -0
  38. package/src/schema-error-codes.ts +28 -0
  39. package/src/secrets-errors.ts +143 -0
  40. package/src/secrets-store.ts +173 -0
  41. package/src/secrets.ts +292 -0
  42. package/src/telemetry.ts +43 -11
  43. package/src/timing-safe-equal.ts +18 -0
  44. package/src/type-pins.ts +93 -0
  45. 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
- meta: this.meta,
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
- return typeof value === 'object' && value !== null && ULTIMATE_ERROR_BRAND in value;
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
- /** Normalise anything caught into an `UltimateError` without losing the original. */
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
- const cause =
146
- value instanceof Error
147
- ? `${value.name}: ${value.message}`
148
- : `non-error value thrown: ${String(value)}`;
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 = randomBytes(2)[0]! & COUNTER_SEED_MASK;
65
+ counter = seedCounter();
55
66
  }
56
67
  } else {
57
- counter = randomBytes(2)[0]! & COUNTER_SEED_MASK;
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
- const variantNibble = HEX[(Number.parseInt(tail[0]!, 16) & 0x3) | 0x8]!;
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
- cause: `"${id}" is not a UUIDv7`,
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 ${JSON.stringify(value)}`,
130
+ cause: `expected a ${kind} UUIDv7 (${UUID_SHAPE}), received ${describeValue(value)}`,
112
131
  fix: `pass an id produced by typedId<'${kind}'>()`,
113
- meta: { kind, value },
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;