@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
package/src/telemetry.ts CHANGED
@@ -5,8 +5,10 @@
5
5
  import { AsyncLocalStorage } from 'node:async_hooks';
6
6
  import { type Clock, systemClock } from './clock';
7
7
  import { tryUseContext } from './context';
8
+ import { renderThrowable } from './error-render';
8
9
  import { isUltimateError } from './errors';
9
- import { spanId as newSpanId, traceId as newTraceId } from './ids';
10
+ import { isSpanId, isTraceId, spanId as newSpanId, traceId as newTraceId } from './ids';
11
+ import { defaultSampler, resetDefaultSampler, type Sampler } from './sampler';
10
12
 
11
13
  export type SpanKind = 'internal' | 'server' | 'client' | 'producer' | 'consumer';
12
14
 
@@ -87,6 +89,8 @@ export interface TelemetryOptions {
87
89
  readonly serviceName?: string | undefined;
88
90
  readonly serviceVersion?: string | undefined;
89
91
  readonly enabled?: boolean | undefined;
92
+ /** Defaults to `defaultSampler()`: honour the parent, else the ratio the env asks for. */
93
+ readonly sampler?: Sampler | undefined;
90
94
  }
91
95
 
92
96
  export const noopExporter: SpanExporter = Object.freeze({
@@ -100,7 +104,7 @@ export interface MemoryExporter extends SpanExporter {
100
104
  reset(): void;
101
105
  }
102
106
 
103
- /** For tests and for `x trace --json`. */
107
+ /** For tests, and for reading back what a run traced with no collector on the box. */
104
108
  export function memoryExporter(): MemoryExporter {
105
109
  const spans: ReadableSpan[] = [];
106
110
  return {
@@ -119,12 +123,14 @@ const activeSpan = new AsyncLocalStorage<Span>();
119
123
  let exporter: SpanExporter = noopExporter;
120
124
  let clock: Clock = systemClock;
121
125
  let enabled = true;
126
+ let sampler: Sampler | undefined;
122
127
  let resource: SpanResource = Object.freeze({ serviceName: 'ultimate', serviceVersion: '0.0.1' });
123
128
 
124
129
  export function configureTelemetry(options: TelemetryOptions): void {
125
130
  if (options.exporter !== undefined) exporter = options.exporter;
126
131
  if (options.clock !== undefined) clock = options.clock;
127
132
  if (options.enabled !== undefined) enabled = options.enabled;
133
+ if (options.sampler !== undefined) sampler = options.sampler;
128
134
  if (options.serviceName !== undefined || options.serviceVersion !== undefined) {
129
135
  resource = Object.freeze({
130
136
  serviceName: options.serviceName ?? resource.serviceName,
@@ -137,6 +143,13 @@ export function resetTelemetry(): void {
137
143
  exporter = noopExporter;
138
144
  clock = systemClock;
139
145
  enabled = true;
146
+ sampler = undefined;
147
+ resetDefaultSampler();
148
+ }
149
+
150
+ /** The sampler in force: whatever `configureTelemetry` was given, else the env's. */
151
+ export function currentSampler(): Sampler {
152
+ return sampler ?? defaultSampler();
140
153
  }
141
154
 
142
155
  /**
@@ -162,12 +175,16 @@ export function currentSpanContext(): SpanContext | undefined {
162
175
 
163
176
  export function startSpan(name: string, options?: StartSpanOptions): Span {
164
177
  const parent = options?.parent ?? currentSpanContext();
178
+ const attributes: Record<string, AttributeValue> = { ...(options?.attributes ?? {}) };
179
+ // The bit is decided ONCE, here, and every child of this span inherits it through `parent` —
180
+ // so one trace is sampled or not sampled as a whole. Before this, `traceFlags` was hardcoded to
181
+ // 1 for a root and `end()` exported regardless, which made the bit a value the framework
182
+ // forwarded and nobody obeyed.
165
183
  const context: SpanContext = {
166
184
  traceId: parent?.traceId ?? newTraceId(),
167
185
  spanId: newSpanId(),
168
- traceFlags: parent?.traceFlags ?? 1,
186
+ traceFlags: currentSampler().shouldSample(name, parent, attributes) ? 1 : 0,
169
187
  };
170
- const attributes: Record<string, AttributeValue> = { ...(options?.attributes ?? {}) };
171
188
  const events: SpanEvent[] = [];
172
189
  const startedAt = clock.now().getTime();
173
190
  const startedMono = clock.monotonic();
@@ -198,7 +215,12 @@ export function startSpan(name: string, options?: StartSpanOptions): Span {
198
215
  },
199
216
  recordError(error) {
200
217
  const code = isUltimateError(error) ? error.code : 'X_INTERNAL';
201
- const message = error instanceof Error ? error.message : String(error);
218
+ // `renderThrowable`, never `error instanceof Error ? error.message : String(error)`: both
219
+ // halves are property reads on a value the framework did not build, and this runs inside
220
+ // `withSpan`'s catch — around `cache.invalidate`, `db.<verb>` and every HTTP and job span.
221
+ // A throw here substitutes the tracer's own TypeError for the caller's real failure and
222
+ // leaves the span it was annotating unended.
223
+ const message = renderThrowable(error);
202
224
  events.push({
203
225
  name: 'exception',
204
226
  at: clock.now().getTime(),
@@ -215,6 +237,10 @@ export function startSpan(name: string, options?: StartSpanOptions): Span {
215
237
  if (ended) return;
216
238
  ended = true;
217
239
  if (!enabled) return;
240
+ // The whole point of propagating a sampling bit is that somebody obeys it. A span still
241
+ // exists, still parents its children and still carries the decision onward in
242
+ // `traceparent`; it is simply not exported.
243
+ if ((context.traceFlags & 1) === 0) return;
218
244
  const endedAt = clock.now().getTime();
219
245
  const parentSpanId = parent === undefined || parent.spanId === '' ? undefined : parent.spanId;
220
246
  exporter.export({
@@ -272,8 +298,13 @@ export function withSpanContext<T>(context: SpanContext, name: string, fn: (span
272
298
 
273
299
  const TRACEPARENT_RE = /^00-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$/;
274
300
 
301
+ /**
302
+ * Round-trips with `traceId()` / `spanId()` from `ids.ts` and with nothing else. A context whose
303
+ * `traceId` came from `uuid()` renders a 36-character dashed header here that `parseTraceparent`
304
+ * — and every OTLP collector — rejects, so mint the pair with those two generators.
305
+ */
275
306
  export function traceparent(context: SpanContext): string {
276
- const flags = context.traceFlags.toString(16).padStart(2, '0');
307
+ const flags = (context.traceFlags & 0xff).toString(16).padStart(2, '0');
277
308
  return `00-${context.traceId}-${context.spanId}-${flags}`;
278
309
  }
279
310
 
@@ -281,11 +312,12 @@ export function parseTraceparent(header: string | null | undefined): SpanContext
281
312
  if (header === null || header === undefined) return undefined;
282
313
  const match = TRACEPARENT_RE.exec(header.trim());
283
314
  if (match === null) return undefined;
284
- return {
285
- traceId: match[1] as string,
286
- spanId: match[2] as string,
287
- traceFlags: Number.parseInt(match[3] as string, 16),
288
- };
315
+ const traceId = match[1] as string;
316
+ const spanId = match[2] as string;
317
+ // `ids.ts` owns what a valid id is, so the all-zero rejection the spec requires lives in one
318
+ // place instead of being a second regex here that drifts from the generator's.
319
+ if (!isTraceId(traceId) || !isSpanId(spanId)) return undefined;
320
+ return { traceId, spanId, traceFlags: Number.parseInt(match[3] as string, 16) };
289
321
  }
290
322
 
291
323
  function isPromiseLike(value: unknown): value is PromiseLike<unknown> {
@@ -0,0 +1,18 @@
1
+ // Single responsibility: a string comparison whose duration does not depend on where two secrets
2
+ // first differ. `@ultimat3/auth` and `@ultimat3/storage` both compared a signature or a hashed
3
+ // token this way and, being tier 1+ packages that both sit below `@ultimat3/core`, neither is the
4
+ // other's dependency — so the one copy lives here, at the tier both can reach.
5
+
6
+ /**
7
+ * Length is compared first and non-constant-time on purpose: every secret this compares is a
8
+ * fixed-width hash, token or signature, so the length carries no information, and the XOR
9
+ * accumulator below is what has to be branch-free.
10
+ */
11
+ export function timingSafeEqual(a: string, b: string): boolean {
12
+ if (a.length !== b.length) return false;
13
+ let diff = 0;
14
+ for (let index = 0; index < a.length; index += 1) {
15
+ diff |= a.charCodeAt(index) ^ b.charCodeAt(index);
16
+ }
17
+ return diff === 0;
18
+ }
@@ -0,0 +1,93 @@
1
+ // Compile-time pins for the actor-facts seam and the config surface. Source, not a `.test.ts`,
2
+ // on purpose:
3
+ // `tsconfig.json` excludes `src/**/*.test.ts`, so `tsc -b` never reads a test file and a
4
+ // type-level assertion written there can never fail. This module emits nothing and exports
5
+ // nothing anybody imports — a regression is a build error, the only enforcement that counts.
6
+
7
+ import type { Actor, ActorFactMap, FactKeysOf, FactMapOf } from './actor';
8
+ import type { AppConfigInput, DatabaseConfig } from './config';
9
+
10
+ /** Fails to compile when `T` is anything but `true`. The whole mechanism. */
11
+ type Assert<T extends true> = T;
12
+
13
+ interface Viewer {
14
+ readonly friendIds: ReadonlySet<string>;
15
+ }
16
+
17
+ /**
18
+ * A stand-in for what an app augments `ActorFacts` with. Declared locally rather than by
19
+ * augmenting the real interface: augmenting it HERE would declare `viewer` for every app that
20
+ * imports the framework, and a pin that changes the product it pins is not a pin.
21
+ */
22
+ interface SampleFacts {
23
+ readonly viewer: Viewer;
24
+ }
25
+
26
+ type SampleMap = FactMapOf<SampleFacts>;
27
+
28
+ /** A declared fact reads back as its own type — never `unknown`, never a bag. */
29
+ type _FactIsTyped = Assert<[NonNullable<SampleMap['viewer']>] extends [Viewer] ? true : false>;
30
+
31
+ type _FactIsNotUnknown = Assert<[unknown] extends [SampleMap['viewer']] ? false : true>;
32
+
33
+ /**
34
+ * The denial rule, as a type. Every fact is independently absent because nothing can prove one
35
+ * was resolved — a job runner, a test and an MCP token exchange all mint actors too. A predicate
36
+ * therefore has to branch on `undefined`, so an absent fact cannot silently read as a satisfied
37
+ * one.
38
+ */
39
+ type _AbsentFactIsRepresentable = Assert<undefined extends SampleMap['viewer'] ? true : false>;
40
+
41
+ /** A typo is a build error rather than a fact that is forever absent. */
42
+ type _UnknownFactIsNotAKey = Assert<'viewr' extends keyof SampleMap ? false : true>;
43
+
44
+ /** The phantom that keeps the empty interface from being `{}` is never itself a fact. */
45
+ type _PhantomIsNotAFactKey = Assert<'__ultimate' extends FactKeysOf<SampleFacts> ? false : true>;
46
+
47
+ /** An actor that resolved nothing is still an actor: every key is optional, always. */
48
+ type _NoFactsIsALegalFactMap = Assert<Record<string, never> extends SampleMap ? true : false>;
49
+
50
+ type _ActorFactMapAcceptsNothing = Assert<
51
+ Record<string, never> extends ActorFactMap ? true : false
52
+ >;
53
+
54
+ /**
55
+ * The seam is additive: an actor literal written before it existed still is one. `facts` is
56
+ * optional for that reason and not only for the denial rule — a required member would have been
57
+ * a breaking change to a tier-0 type every package depends on.
58
+ */
59
+ type _ActorWithoutFactsIsStillAnActor = Assert<
60
+ [
61
+ {
62
+ readonly kind: 'user';
63
+ readonly id: string;
64
+ readonly roles: readonly string[];
65
+ readonly scopes: readonly string[];
66
+ },
67
+ ] extends [Actor]
68
+ ? true
69
+ : false
70
+ >;
71
+
72
+ /**
73
+ * The three `config.database` fields deleted 2026-08 must stay deleted. Each produced neither a
74
+ * build error nor a runtime effect, which is the worst state a config field can be in: an SRE set
75
+ * `poolSize: 3`, redeployed, and nothing changed. Re-adding one silently restores that, so the
76
+ * pin is here rather than in a `.test.ts` — a `@ts-expect-error` in an excluded file asserts
77
+ * nothing.
78
+ *
79
+ * `DATABASE_POOL_MAX` is the pool knob that works, `DATABASE_URL` is read as a literal by
80
+ * `@ultimat3/db`'s `client.ts`, and nothing emits `SET search_path`.
81
+ */
82
+ type DeadDatabaseField = 'urlEnv' | 'poolSize' | 'schema';
83
+
84
+ type _DatabaseConfigCarriesNoDeadField = Assert<
85
+ Extract<keyof DatabaseConfig, DeadDatabaseField> extends never ? true : false
86
+ >;
87
+
88
+ /** And the input side with it — `Input<DatabaseConfig>` is what an `app.config.ts` writes. */
89
+ type _DatabaseInputCarriesNoDeadField = Assert<
90
+ Extract<keyof NonNullable<AppConfigInput['database']>, DeadDatabaseField> extends never
91
+ ? true
92
+ : false
93
+ >;
package/src/version.ts CHANGED
@@ -3,11 +3,12 @@
3
3
  // manifest is `private` and carries no `version`, and after `npm install` a walk above the package
4
4
  // lands in `node_modules/` where there is no manifest at all. Both mistakes fail silently.
5
5
 
6
- import { readFileSync } from 'node:fs';
6
+ import { existsSync, readFileSync } from 'node:fs';
7
7
  // Bun has no path-join primitive; `import.meta.dir` is the module's own directory in both the
8
8
  // checked-out `src/` layout and the published `dist/` one, each exactly one level below the
9
9
  // package root.
10
10
  import { resolve } from 'node:path';
11
+ import { renderCauseValue } from './error-render';
11
12
  import { UltimateError } from './errors';
12
13
 
13
14
  const SEMVER = /^\d+\.\d+\.\d+(?:[-+][\w.-]+)*$/;
@@ -15,10 +16,22 @@ const SEMVER = /^\d+\.\d+\.\d+(?:[-+][\w.-]+)*$/;
15
16
  /** `@ultimat3/core`'s own manifest — the only file that can answer what version shipped. */
16
17
  export const VERSION_MANIFEST = resolve(import.meta.dir, '..', 'package.json');
17
18
 
19
+ /**
20
+ * The bundler define that carries the version into a build with no manifest to read.
21
+ * `x build --target binary` passes it (`binaryArgs` in `@ultimat3/cli`); the name is declared here
22
+ * so the flag that writes it and the read below cannot drift.
23
+ */
24
+ export const VERSION_DEFINE = 'ULTIMATE_FRAMEWORK_VERSION';
25
+
26
+ // Replaced with a string literal by `bun build --define ULTIMATE_FRAMEWORK_VERSION='"1.2.3"'`, and
27
+ // declared by nothing at runtime — which is why the read is `typeof`-guarded. An unbundled process
28
+ // must see `undefined` here, not a `ReferenceError`.
29
+ declare const ULTIMATE_FRAMEWORK_VERSION: string | undefined;
30
+
18
31
  /**
19
32
  * A package with no readable version is a broken publish, not a runtime condition to degrade
20
33
  * through: an `undefined` version poisons the MCP handshake and every dependency a scaffold pins,
21
- * and does it quietly. Fail at import, where the fix is a release-script change.
34
+ * and does it quietly. Fail where the fix is a release-script change.
22
35
  */
23
36
  export function readPackageVersion(manifestPath: string): string {
24
37
  const raw: unknown = JSON.parse(readFileSync(manifestPath, 'utf-8'));
@@ -27,11 +40,47 @@ export function readPackageVersion(manifestPath: string): string {
27
40
  if (typeof version !== 'string' || !SEMVER.test(version)) {
28
41
  throw new UltimateError({
29
42
  code: 'X_INVARIANT',
30
- cause: `${manifestPath} has no valid semver "version" field (found ${JSON.stringify(version)})`,
43
+ cause: `${manifestPath} has no valid semver "version" field (found ${renderCauseValue(version)})`,
31
44
  fix: `set a semver "version" in ${manifestPath}, then re-run: bun run verify`,
32
45
  });
33
46
  }
34
47
  return version;
35
48
  }
36
49
 
37
- export const FRAMEWORK_VERSION: string = readPackageVersion(VERSION_MANIFEST);
50
+ /**
51
+ * Manifest first, build define second, throw last.
52
+ *
53
+ * A single-file executable carries no `package.json`, so a *missing* manifest is the one absence
54
+ * that is not a broken publish — it falls through to the define. A manifest that exists and
55
+ * declares no semver still throws, because that is the broken publish `readPackageVersion` was
56
+ * written for, and a define must not paper over it. Pure and exported so the compiled-binary case
57
+ * is a unit test rather than a `bun build --compile` nobody runs.
58
+ */
59
+ export function resolveVersion(manifestPath: string, defined: string | undefined): string {
60
+ if (existsSync(manifestPath)) return readPackageVersion(manifestPath);
61
+ if (defined !== undefined && SEMVER.test(defined)) return defined;
62
+ throw new UltimateError({
63
+ code: 'X_INVARIANT',
64
+ cause: `no manifest at ${manifestPath} and no valid ${VERSION_DEFINE} define (found ${JSON.stringify(defined)}) — only the builder that passes the define produces a bootable binary`,
65
+ fix: `x build --target binary`,
66
+ });
67
+ }
68
+
69
+ let resolved: string | undefined;
70
+
71
+ /**
72
+ * The framework's version, resolved on first call and cached for every call after.
73
+ *
74
+ * Lazy is the whole point. As a module-scope constant the read ran before `main` in every process
75
+ * that imported core, so `x build --target binary` produced an artifact that threw at import
76
+ * before any role started — the artifact compiled and could never boot.
77
+ */
78
+ export function frameworkVersion(): string {
79
+ if (resolved === undefined) {
80
+ resolved = resolveVersion(
81
+ VERSION_MANIFEST,
82
+ typeof ULTIMATE_FRAMEWORK_VERSION === 'string' ? ULTIMATE_FRAMEWORK_VERSION : undefined,
83
+ );
84
+ }
85
+ return resolved;
86
+ }