@ultimat3/core 24.0.0 → 25.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 (76) hide show
  1. package/CLAUDE.md +24 -27
  2. package/README.md +71 -34
  3. package/package.json +4 -7
  4. package/src/actor.ts +9 -0
  5. package/src/address-class.ts +40 -4
  6. package/src/assert.ts +9 -5
  7. package/src/audit.ts +144 -0
  8. package/src/aws-sigv4.ts +275 -0
  9. package/src/backoff.ts +16 -0
  10. package/src/bunfs.ts +17 -0
  11. package/src/client-dispatch.ts +24 -3
  12. package/src/client-flight.ts +68 -13
  13. package/src/client-problem.ts +62 -6
  14. package/src/client-retry-after.ts +47 -0
  15. package/src/client-transport.ts +3 -1
  16. package/src/client-wire.ts +27 -3
  17. package/src/config-ai.ts +32 -0
  18. package/src/config-defaults.ts +18 -11
  19. package/src/config-fixes.ts +0 -10
  20. package/src/config-health.ts +9 -2
  21. package/src/config-jobs.ts +51 -0
  22. package/src/config-keys.ts +170 -0
  23. package/src/config-mail.ts +73 -0
  24. package/src/config-merge.ts +1 -1
  25. package/src/config-navigation.ts +1 -25
  26. package/src/config-pwa.ts +42 -5
  27. package/src/config-removed.ts +131 -0
  28. package/src/config-shape.ts +0 -33
  29. package/src/config.ts +71 -94
  30. package/src/context.ts +16 -12
  31. package/src/cookie.ts +267 -3
  32. package/src/core-error-codes.ts +2 -0
  33. package/src/cursor-page.ts +41 -0
  34. package/src/cursor.ts +23 -5
  35. package/src/deprecation.ts +77 -0
  36. package/src/dev-secrets.ts +18 -7
  37. package/src/drain-deadline.ts +43 -0
  38. package/src/env-example.ts +9 -29
  39. package/src/errors.ts +18 -9
  40. package/src/exports/error-contract.ts +0 -1
  41. package/src/exports/observability.ts +1 -1
  42. package/src/exports/secrets.ts +3 -0
  43. package/src/finite-option.ts +1 -1
  44. package/src/flight-gate.ts +29 -14
  45. package/src/generation-fence.ts +1 -1
  46. package/src/ids.ts +7 -7
  47. package/src/image/canvas.ts +76 -5
  48. package/src/image/pipeline.ts +17 -5
  49. package/src/image/raster.ts +24 -1
  50. package/src/index.ts +89 -35
  51. package/src/iso-date.ts +1 -1
  52. package/src/lifecycle-errors.ts +1 -1
  53. package/src/lifecycle-readiness.ts +60 -2
  54. package/src/lifecycle-signals.ts +27 -2
  55. package/src/lifecycle-types.ts +96 -0
  56. package/src/lifecycle.ts +44 -133
  57. package/src/locale-direction.ts +1 -1
  58. package/src/logger.ts +16 -7
  59. package/src/mcp-exposure.ts +70 -8
  60. package/src/measurement-actor.ts +16 -1
  61. package/src/metric-errors.ts +32 -0
  62. package/src/metric-registry.ts +151 -0
  63. package/src/metric-series.ts +94 -0
  64. package/src/metrics.ts +9 -255
  65. package/src/page.ts +4 -2
  66. package/src/registrar.ts +1 -0
  67. package/src/retry.ts +25 -5
  68. package/src/secrets-key-file.ts +139 -0
  69. package/src/secrets-store.ts +32 -16
  70. package/src/service.ts +5 -5
  71. package/src/single-flight.ts +1 -1
  72. package/src/telemetry.ts +1 -1
  73. package/src/theme-storage.ts +12 -0
  74. package/src/type-pins.ts +51 -1
  75. package/src/image/fixtures.ts +0 -263
  76. package/src/time-zone-name.ts +0 -14
@@ -0,0 +1,94 @@
1
+ // Single responsibility: one series per label set under an instrument — its stable key, and the
2
+ // cardinality ceiling past which every new label set folds into one overflow series, reported once.
3
+
4
+ import { logger } from './logger';
5
+ import { MetricCardinalityError } from './metric-errors';
6
+ import { assertLabelNames } from './metric-names';
7
+ import type { Instrument, Series } from './metric-registry';
8
+ import type { MetricAttributes } from './metrics-types';
9
+
10
+ /**
11
+ * The label the folded series carries. OTel's own cardinality-limit spelling, deliberately NOT
12
+ * `__overflow`: Prometheus treats `__`-prefixed labels as internal and strips them during
13
+ * relabeling, so an overflow series named that way would merge back into the unlabelled series
14
+ * and the drop would be invisible in exactly the place it has to be visible.
15
+ */
16
+ export const OVERFLOW_ATTRIBUTE = 'otel_metric_overflow';
17
+
18
+ const OVERFLOW_ATTRIBUTES: MetricAttributes = Object.freeze({ [OVERFLOW_ATTRIBUTE]: true });
19
+
20
+ /**
21
+ * Stable series key: attribute order must not create a second series for one label set, and no
22
+ * label set may spell another one's key.
23
+ *
24
+ * `JSON.stringify` over the sorted pairs, because a DELIMITER cannot carry the second property:
25
+ * the key was the pairs joined by control characters (U+0000 inside a pair, U+0001 between them),
26
+ * and a value holding those bytes IS another set's key — `{ a: 'b\u0001c\u0000d' }` was
27
+ * `{ a: 'b', c: 'd' }`, so the point landed on whichever series arrived first and was exported
28
+ * under labels the caller never passed. Attribute values are app data. Quoting is the only total
29
+ * answer and is not slower: 644 ns/op against the join's 709, on a 3-label set. `String(value)`
30
+ * stays, so `1` and `'1'` are still one series rather than two rows an exporter renders alike.
31
+ */
32
+ function seriesKey(attributes: MetricAttributes): string {
33
+ const entries = Object.entries(attributes);
34
+ if (entries.length === 0) return '';
35
+ return JSON.stringify(
36
+ entries.sort(([a], [b]) => (a < b ? -1 : 1)).map(([key, value]) => [key, String(value)]),
37
+ );
38
+ }
39
+
40
+ /**
41
+ * Reported through the logger rather than thrown: the call site is `orderCounter.add(1, …)` deep
42
+ * inside a request, and killing that request would turn a metrics bug into a user-visible outage
43
+ * — which is the same trade `finite()` does NOT make, because a NaN is a caller bug at one call
44
+ * site while this is a design bug the whole instrument shares.
45
+ */
46
+ function reportOverflow(instrument: Instrument): void {
47
+ if (instrument.overflowed) return;
48
+ instrument.overflowed = true;
49
+ const { name, kind } = instrument.descriptor;
50
+ const error = new MetricCardinalityError({
51
+ cause: `${name} reached its ceiling of ${instrument.maxSeries} label set(s); every further label set folds into one ${OVERFLOW_ATTRIBUTE}="true" series`,
52
+ fix: `drop the unbounded label from the ${name} call site (an id, a path, an email is never a label), or raise it deliberately: ${kind}('${name}', { maxSeries: ${instrument.maxSeries * 2} })`,
53
+ meta: { metric: name, maxSeries: instrument.maxSeries },
54
+ });
55
+ logger.error(error.format(), { code: error.code, metric: name });
56
+ }
57
+
58
+ function createSeries(instrument: Instrument, key: string, attributes: MetricAttributes): Series {
59
+ const created: Series = {
60
+ attributes,
61
+ value: 0,
62
+ count: 0,
63
+ min: Number.POSITIVE_INFINITY,
64
+ max: Number.NEGATIVE_INFINITY,
65
+ buckets: new Array<number>(instrument.bounds.length + 1).fill(0),
66
+ };
67
+ instrument.series.set(key, created);
68
+ return created;
69
+ }
70
+
71
+ const OVERFLOW_KEY = seriesKey(OVERFLOW_ATTRIBUTES);
72
+
73
+ export function seriesFor(instrument: Instrument, attributes: MetricAttributes): Series {
74
+ const key = seriesKey(attributes);
75
+ const found = instrument.series.get(key);
76
+ if (found !== undefined) return found;
77
+ // On the MISS, ahead of the ceiling — never inside `createSeries`, which the overflow branch
78
+ // below returns without reaching. A screen that ran only where a series is BORN was a screen
79
+ // that depended on load: `bad"key` threw on a fresh process and was swallowed on a busy one,
80
+ // once the instrument had filled up, which is exactly when an unparseable label is likeliest to
81
+ // arrive. A hit needs no check — a key in the map passed this on the way in.
82
+ assertLabelNames(instrument.descriptor.name, attributes);
83
+ if (instrument.series.size >= instrument.maxSeries) {
84
+ reportOverflow(instrument);
85
+ // Created directly rather than through this function again: the overflow series is the ONE
86
+ // allocation the ceiling does not apply to, and routing it back through the check is an
87
+ // infinite recursion the first time the cap is hit.
88
+ return (
89
+ instrument.series.get(OVERFLOW_KEY) ??
90
+ createSeries(instrument, OVERFLOW_KEY, OVERFLOW_ATTRIBUTES)
91
+ );
92
+ }
93
+ return createSeries(instrument, key, attributes);
94
+ }
package/src/metrics.ts CHANGED
@@ -5,9 +5,10 @@
5
5
  import { assert } from './assert';
6
6
  import { type Clock, systemClock } from './clock';
7
7
  import { renderThrowable } from './error-render';
8
- import { type CodedErrorInit, UltimateError } from './errors';
9
8
  import { logger } from './logger';
10
- import { assertLabelNames, assertMetricName, MetricNameInvalidError } from './metric-names';
9
+ import { finite, MetricValueInvalidError } from './metric-errors';
10
+ import { declare, type Instrument, instruments } from './metric-registry';
11
+ import { seriesFor } from './metric-series';
11
12
  import type {
12
13
  Counter,
13
14
  Gauge,
@@ -15,19 +16,19 @@ import type {
15
16
  Histogram,
16
17
  HistogramOptions,
17
18
  InstrumentOptions,
18
- MetricAttributes,
19
19
  MetricCollection,
20
- MetricDescriptor,
21
20
  MetricExporter,
22
- MetricKind,
23
21
  MetricPoint,
24
22
  } from './metrics-types';
25
23
  import { serviceResource } from './telemetry';
26
24
 
27
- // The data model and the identifier grammar are modules of their own; the public surface is
28
- // unchanged, so nothing that imports a metric type or the name error from here learns a second
29
- // path.
25
+ // The data model, the identifier grammar, the refusals, the registry and the series store are
26
+ // modules of their own; the public surface is unchanged, so nothing that imports a metric type, a
27
+ // constant or an error from here learns a second path.
28
+ export { MetricCardinalityError, MetricValueInvalidError } from './metric-errors';
30
29
  export { MetricNameInvalidError } from './metric-names';
30
+ export { DEFAULT_HISTOGRAM_BOUNDS, DEFAULT_MAX_SERIES } from './metric-registry';
31
+ export { OVERFLOW_ATTRIBUTE } from './metric-series';
31
32
  export type {
32
33
  Counter,
33
34
  Gauge,
@@ -46,44 +47,6 @@ export type {
46
47
  ReadableMetric,
47
48
  } from './metrics-types';
48
49
 
49
- export class MetricValueInvalidError extends UltimateError {
50
- static readonly code = 'X_METRIC_VALUE_INVALID';
51
- override readonly name = 'MetricValueInvalidError';
52
- constructor(init: CodedErrorInit) {
53
- super({ ...init, code: MetricValueInvalidError.code });
54
- }
55
- }
56
-
57
- export class MetricCardinalityError extends UltimateError {
58
- static readonly code = 'X_METRIC_CARDINALITY';
59
- override readonly name = 'MetricCardinalityError';
60
- constructor(init: CodedErrorInit) {
61
- super({ ...init, code: MetricCardinalityError.code });
62
- }
63
- }
64
-
65
- /**
66
- * The per-instrument series ceiling. 2000 is roomy for a bounded label set — every route pattern
67
- * times every status class times every method — and small enough that the process notices an
68
- * unbounded one long before the scrape body does.
69
- */
70
- export const DEFAULT_MAX_SERIES = 2000;
71
-
72
- /**
73
- * The label the folded series carries. OTel's own cardinality-limit spelling, deliberately NOT
74
- * `__overflow`: Prometheus treats `__`-prefixed labels as internal and strips them during
75
- * relabeling, so an overflow series named that way would merge back into the unlabelled series
76
- * and the drop would be invisible in exactly the place it has to be visible.
77
- */
78
- export const OVERFLOW_ATTRIBUTE = 'otel_metric_overflow';
79
-
80
- const OVERFLOW_ATTRIBUTES: MetricAttributes = Object.freeze({ [OVERFLOW_ATTRIBUTE]: true });
81
-
82
- /** OTel's default explicit bucket boundaries for a duration histogram, in seconds. */
83
- export const DEFAULT_HISTOGRAM_BOUNDS: readonly number[] = Object.freeze([
84
- 0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, 0.75, 1, 2.5, 5, 7.5, 10,
85
- ]);
86
-
87
50
  export const noopMetricExporter: MetricExporter = Object.freeze({
88
51
  export(): void {
89
52
  // Intentionally empty: instruments are always live, and free until an exporter is configured.
@@ -115,29 +78,6 @@ export interface MetricsOptions {
115
78
  readonly enabled?: boolean | undefined;
116
79
  }
117
80
 
118
- interface Series {
119
- readonly attributes: MetricAttributes;
120
- value: number;
121
- count: number;
122
- min: number;
123
- max: number;
124
- buckets: number[];
125
- }
126
-
127
- interface Instrument {
128
- readonly descriptor: MetricDescriptor;
129
- readonly series: Map<string, Series>;
130
- readonly bounds: readonly number[];
131
- readonly observe: (() => number) | undefined;
132
- readonly maxSeries: number;
133
- /** Reported once. A cardinality blow-up is one bug, not one log line per call. */
134
- overflowed: boolean;
135
- /** Reported once, for the same reason: a scrape every 15s must not become a log every 15s. */
136
- observeFailed: boolean;
137
- }
138
-
139
- const instruments = new Map<string, Instrument>();
140
-
141
81
  let exporter: MetricExporter = noopMetricExporter;
142
82
  let clock: Clock = systemClock;
143
83
  let enabled = true;
@@ -164,154 +104,6 @@ export function resetMetrics(): void {
164
104
  }
165
105
  }
166
106
 
167
- /**
168
- * Stable series key: attribute order must not create a second series for one label set, and no
169
- * label set may spell another one's key.
170
- *
171
- * `JSON.stringify` over the sorted pairs, because a DELIMITER cannot carry the second property:
172
- * the key was the pairs joined by control characters (U+0000 inside a pair, U+0001 between them),
173
- * and a value holding those bytes IS another set's key — `{ a: 'b\u0001c\u0000d' }` was
174
- * `{ a: 'b', c: 'd' }`, so the point landed on whichever series arrived first and was exported
175
- * under labels the caller never passed. Attribute values are app data. Quoting is the only total
176
- * answer and is not slower: 644 ns/op against the join's 709, on a 3-label set. `String(value)`
177
- * stays, so `1` and `'1'` are still one series rather than two rows an exporter renders alike.
178
- */
179
- function seriesKey(attributes: MetricAttributes): string {
180
- const entries = Object.entries(attributes);
181
- if (entries.length === 0) return '';
182
- return JSON.stringify(
183
- entries.sort(([a], [b]) => (a < b ? -1 : 1)).map(([key, value]) => [key, String(value)]),
184
- );
185
- }
186
-
187
- function finite(name: string, value: number): number {
188
- if (!Number.isFinite(value)) {
189
- throw new MetricValueInvalidError({
190
- cause: `${name} was given ${String(value)}, which is not a finite number`,
191
- fix: `guard the value at the call site: Number.isFinite(v) before recording into ${name}`,
192
- meta: { metric: name, received: String(value) },
193
- });
194
- }
195
- return value;
196
- }
197
-
198
- /**
199
- * Bounds are strictly ascending finite numbers, refused at DECLARATION like `maxSeries` beside it.
200
- * `record` takes the first bound an observation fits, and the exposition format emits one
201
- * cumulative `le` series per bound in array order — so `[1, 0.5, 5]` both counted observations
202
- * into a bucket that was not theirs and rendered a non-monotonic `le` series that Prometheus and
203
- * OpenMetrics each reject. Two wrong numbers, neither visible from the other, and nothing at the
204
- * call site to notice: the observations themselves were all valid.
205
- */
206
- function assertBounds(name: string, bounds: readonly number[] | undefined): void {
207
- if (bounds === undefined) return;
208
- const bad = bounds.findIndex((bound, index) => {
209
- const previous = index === 0 ? Number.NEGATIVE_INFINITY : (bounds[index - 1] as number);
210
- return !Number.isFinite(bound) || bound <= previous;
211
- });
212
- if (bad === -1) return;
213
- const repaired = [...new Set(bounds.filter((bound) => Number.isFinite(bound)))].sort(
214
- (left, right) => left - right,
215
- );
216
- throw new MetricNameInvalidError({
217
- cause: `${name} declared bounds [${bounds.map((bound) => String(bound)).join(', ')}], which are not strictly ascending finite numbers — [${String(bad)}] is ${String(bounds[bad])}`,
218
- fix: `sort the bounds and drop the duplicates: histogram('${name}', { bounds: [${repaired.join(', ')}] })`,
219
- meta: { metric: name, bounds: bounds.map((bound) => String(bound)), at: bad },
220
- });
221
- }
222
-
223
- function declare(name: string, kind: MetricKind, options: GaugeOptions & HistogramOptions) {
224
- assertMetricName(name);
225
- assertBounds(name, options.bounds);
226
- const existing = instruments.get(name);
227
- if (existing !== undefined) {
228
- if (existing.descriptor.kind !== kind) {
229
- throw new MetricNameInvalidError({
230
- cause: `"${name}" is already declared as a ${existing.descriptor.kind}, redeclared as a ${kind}`,
231
- fix: `rename one of the two instruments named "${name}" — one metric name, one kind`,
232
- meta: { name, declared: existing.descriptor.kind, requested: kind },
233
- });
234
- }
235
- assertSameDeclaration(name, existing, options);
236
- return existing;
237
- }
238
- const maxSeries = options.maxSeries ?? DEFAULT_MAX_SERIES;
239
- if (!Number.isInteger(maxSeries) || maxSeries < 1) {
240
- throw new MetricCardinalityError({
241
- cause: `${name} declared maxSeries ${String(maxSeries)}, which is not a positive integer`,
242
- fix: `pass a positive integer: counter('${name}', { maxSeries: ${DEFAULT_MAX_SERIES} })`,
243
- meta: { metric: name, maxSeries: String(maxSeries) },
244
- });
245
- }
246
- const instrument: Instrument = {
247
- descriptor: {
248
- name,
249
- kind,
250
- unit: options.unit ?? '1',
251
- description: options.description ?? '',
252
- },
253
- series: new Map<string, Series>(),
254
- bounds: options.bounds ?? DEFAULT_HISTOGRAM_BOUNDS,
255
- observe: options.observe,
256
- maxSeries,
257
- overflowed: false,
258
- observeFailed: false,
259
- };
260
- instruments.set(name, instrument);
261
- return instrument;
262
- }
263
-
264
- /**
265
- * A second declaration that STATES a different shape is refused. The first declaration wins, so a
266
- * second `histogram(name, { bounds })` recorded into buckets another module chose and a second
267
- * `gauge(name, { observe })` was collected through the first module's observer — silently, in both
268
- * cases, which is the whole failure. An OMITTED option is not a conflict: `gauge(name)` is how a
269
- * module takes a handle on an instrument someone else declared, and `maxSeries` keeps its shipped
270
- * first-declaration-wins rule because it decides a ceiling rather than what gets recorded.
271
- */
272
- function assertSameDeclaration(
273
- name: string,
274
- existing: Instrument,
275
- options: GaugeOptions & HistogramOptions,
276
- ): void {
277
- const { bounds, observe } = options;
278
- if (bounds !== undefined && !sameBounds(existing.bounds, bounds)) {
279
- throw new MetricNameInvalidError({
280
- cause: `"${name}" is already declared with bounds [${existing.bounds.join(', ')}] and is redeclared with [${bounds.join(', ')}]; the first declaration wins, so the second set would never be used`,
281
- fix: `declare "${name}" once and export the handle — import it where you record — or give the second instrument its own name`,
282
- meta: { name, declared: existing.bounds.join(','), requested: bounds.join(',') },
283
- });
284
- }
285
- if (observe !== undefined && observe !== existing.observe) {
286
- throw new MetricNameInvalidError({
287
- cause: `"${name}" is already declared with an observe() callback and is redeclared with a different one; the first declaration wins, so the second callback would never be read`,
288
- fix: `declare "${name}" once and export the handle — import it where you read — or give the second gauge its own name`,
289
- meta: { name },
290
- });
291
- }
292
- }
293
-
294
- const sameBounds = (left: readonly number[], right: readonly number[]): boolean =>
295
- left.length === right.length && left.every((bound, index) => bound === right[index]);
296
-
297
- /**
298
- * Reported through the logger rather than thrown: the call site is `orderCounter.add(1, …)` deep
299
- * inside a request, and killing that request would turn a metrics bug into a user-visible outage
300
- * — which is the same trade `finite()` does NOT make, because a NaN is a caller bug at one call
301
- * site while this is a design bug the whole instrument shares.
302
- */
303
- function reportOverflow(instrument: Instrument): void {
304
- if (instrument.overflowed) return;
305
- instrument.overflowed = true;
306
- const { name, kind } = instrument.descriptor;
307
- const error = new MetricCardinalityError({
308
- cause: `${name} reached its ceiling of ${instrument.maxSeries} label set(s); every further label set folds into one ${OVERFLOW_ATTRIBUTE}="true" series`,
309
- fix: `drop the unbounded label from the ${name} call site (an id, a path, an email is never a label), or raise it deliberately: ${kind}('${name}', { maxSeries: ${instrument.maxSeries * 2} })`,
310
- meta: { metric: name, maxSeries: instrument.maxSeries },
311
- });
312
- logger.error(error.format(), { code: error.code, metric: name });
313
- }
314
-
315
107
  /**
316
108
  * Reported through the logger for `reportOverflow`'s reason and once for the same one — a scrape
317
109
  * runs on a timer, so a permanently broken observer would otherwise write a log line every
@@ -332,44 +124,6 @@ function reportObserveFailure(instrument: Instrument, thrown: unknown): void {
332
124
  logger.error(error.format(), { code: error.code, metric: name });
333
125
  }
334
126
 
335
- function createSeries(instrument: Instrument, key: string, attributes: MetricAttributes): Series {
336
- const created: Series = {
337
- attributes,
338
- value: 0,
339
- count: 0,
340
- min: Number.POSITIVE_INFINITY,
341
- max: Number.NEGATIVE_INFINITY,
342
- buckets: new Array<number>(instrument.bounds.length + 1).fill(0),
343
- };
344
- instrument.series.set(key, created);
345
- return created;
346
- }
347
-
348
- const OVERFLOW_KEY = seriesKey(OVERFLOW_ATTRIBUTES);
349
-
350
- function seriesFor(instrument: Instrument, attributes: MetricAttributes): Series {
351
- const key = seriesKey(attributes);
352
- const found = instrument.series.get(key);
353
- if (found !== undefined) return found;
354
- // On the MISS, ahead of the ceiling — never inside `createSeries`, which the overflow branch
355
- // below returns without reaching. A screen that ran only where a series is BORN was a screen
356
- // that depended on load: `bad"key` threw on a fresh process and was swallowed on a busy one,
357
- // once the instrument had filled up, which is exactly when an unparseable label is likeliest to
358
- // arrive. A hit needs no check — a key in the map passed this on the way in.
359
- assertLabelNames(instrument.descriptor.name, attributes);
360
- if (instrument.series.size >= instrument.maxSeries) {
361
- reportOverflow(instrument);
362
- // Created directly rather than through this function again: the overflow series is the ONE
363
- // allocation the ceiling does not apply to, and routing it back through the check is an
364
- // infinite recursion the first time the cap is hit.
365
- return (
366
- instrument.series.get(OVERFLOW_KEY) ??
367
- createSeries(instrument, OVERFLOW_KEY, OVERFLOW_ATTRIBUTES)
368
- );
369
- }
370
- return createSeries(instrument, key, attributes);
371
- }
372
-
373
127
  /** Monotonic sum. A negative `add` is a bug in the caller, never a silent decrement. */
374
128
  export function counter(name: string, options?: InstrumentOptions): Counter {
375
129
  const instrument = declare(name, 'counter', options ?? {});
package/src/page.ts CHANGED
@@ -10,7 +10,7 @@
10
10
  // The client seam's own functions, for the hooks a browser island calls: the one transport, the
11
11
  // URL rule, the principal-supersession reader, and the small helpers realtime's browser half uses.
12
12
  // Each reaches no titles table — `page-bundle.test.ts` builds all of them together to prove it.
13
- export { invariant } from './assert';
13
+ export { assertCoded } from './assert';
14
14
  export type { AsyncState } from './async-state';
15
15
  export type { JitterMode, Random } from './backoff';
16
16
  export { backoffDelay } from './backoff';
@@ -32,7 +32,7 @@ export type { UltimateErrorInit } from './errors';
32
32
  export { isUltimateError, UltimateError } from './errors';
33
33
  export { finiteCount, finiteOption } from './finite-option';
34
34
  export { isSuperseded } from './generation-fence';
35
- export { uuid } from './ids';
35
+ export { uuidV7 } from './ids';
36
36
  export { isJsonObject } from './json-object';
37
37
  export type { OutboxDrainMessage } from './outbox-drain';
38
38
  export { OUTBOX_DRAIN_MESSAGE } from './outbox-drain';
@@ -54,5 +54,7 @@ export type { RecordEnvelope, RecordRows } from './record-envelope';
54
54
  export { decodeRecordEnvelope, RECORDS_HEADER } from './record-envelope';
55
55
  export type { PageClient, RecordSink } from './record-sink';
56
56
  export { pageClient } from './record-sink';
57
+ // The key the theme boot script reads and the toggle island writes.
58
+ export { THEME_STORAGE_KEY } from './theme-storage';
57
59
  // A write's public name, so the page's store can recognise the `records` frame its own write made.
58
60
  export { isWriteDigest, WRITE_DIGEST_LENGTH, writeDigest } from './write-digest';
package/src/registrar.ts CHANGED
@@ -84,6 +84,7 @@ export const PRIMITIVE_FACTORIES = Object.freeze<readonly PrimitiveFactory[]>(
84
84
  { factory: 'exportRows', pkg: '@ultimat3/jobs', kind: 'job' },
85
85
  { factory: 'purge', pkg: '@ultimat3/jobs', kind: 'job' },
86
86
  { factory: 'webhook', pkg: '@ultimat3/jobs', kind: 'job' },
87
+ { factory: 'mcpConfirmations', pkg: '@ultimat3/mcp', kind: 'action' },
87
88
  { factory: 'notifier', pkg: '@ultimat3/notify', kind: 'job' },
88
89
  { factory: 'scrape', pkg: '@ultimat3/scraping', kind: 'job' },
89
90
  ] satisfies readonly PrimitiveFactory[]
package/src/retry.ts CHANGED
@@ -3,7 +3,13 @@
3
3
  // nothing in the tree consulted it before deciding to try again, so four packages each shipped
4
4
  // their own loop and only `@ultimat3/jobs`' asked the classification at all.
5
5
 
6
- import { type BackoffCurve, backoffDelay, type JitterMode, type Random } from './backoff';
6
+ import {
7
+ type BackoffCurve,
8
+ backoffDelay,
9
+ type JitterMode,
10
+ jitterStatedDelay,
11
+ type Random,
12
+ } from './backoff';
7
13
  import { systemClock } from './clock';
8
14
  import { classifyThrown, type ErrorRetry, statedDelayMs } from './error-retry';
9
15
  import { finiteCount, finiteOption } from './finite-option';
@@ -13,7 +19,11 @@ export interface RetryPolicy {
13
19
  readonly attempts: number;
14
20
  /** The first delay, in ms. */
15
21
  readonly base: number;
16
- /** Ceiling for any single delay, in ms — a `retry-after` the responder named included. */
22
+ /**
23
+ * Ceiling for any single delay, in ms. A `retry-after` the responder named is clamped to it
24
+ * too, and then gets its spread on top — at most half again, so a burst told one delay does
25
+ * not wake together (`jitterStatedDelay`).
26
+ */
17
27
  readonly max: number;
18
28
  /**
19
29
  * Required, unlike `backoffDelay`'s, and that is the point: a retry loop with no jitter is the
@@ -92,9 +102,19 @@ export function retryDecision(
92
102
  const stated = classification === 'retry-after' ? statedDelayMs(error) : undefined;
93
103
  return {
94
104
  retry: true,
95
- // Clamped by the policy's own ceiling, which is what `max` is for: a responder naming a day is
96
- // still a responder this deployment has not agreed to wait a day for.
97
- delayMs: stated === undefined ? computed : Math.min(stated, policy.max),
105
+ // The stated delay is a FLOOR plus a spread (`jitterStatedDelay`), never the bare number: a
106
+ // shedding server tells a whole burst `Retry-After: 1`, and waiting exactly that replays the
107
+ // burst in lockstep every second. The floor is clamped by the policy's own ceiling, which is
108
+ // what `max` is for — a responder naming a day is still a responder this deployment has not
109
+ // agreed to wait a day for — and the spread on top is at most half of it.
110
+ // `jitter: 'none'` is honoured here too — the caller's one decision, for a test or a printable
111
+ // schedule — and only that mode gets the bare floor.
112
+ delayMs:
113
+ stated === undefined
114
+ ? computed
115
+ : policy.jitter === 'none'
116
+ ? Math.min(stated, policy.max)
117
+ : jitterStatedDelay(Math.min(stated, policy.max), policy.max, random),
98
118
  attempt,
99
119
  nextAttempt: attempt + 1,
100
120
  classification,
@@ -0,0 +1,139 @@
1
+ // Single responsibility: writing a master-key file that only its owner can read, on every platform,
2
+ // and renaming one over another without losing to a reader that holds it open. POSIX gets the first
3
+ // from the create mode. Windows ignores the mode — the file inherits the profile directory's ACL —
4
+ // so there the temp file's ACL is cut to the current account BEFORE the rename makes it live.
5
+
6
+ // why: `node:fs` sync — Bun.write takes no mode and has no atomic rename; both run once, at a CLI
7
+ // command or at boot, before anything is served, so there is nothing for an async call to overlap.
8
+ import { renameSync, rmSync, writeFileSync } from 'node:fs';
9
+ import { backoffDelay } from './backoff';
10
+ import { renderCauseValue, stringField } from './error-render';
11
+ import { UltimateError } from './errors';
12
+
13
+ /** How long `icacls` or `whoami` may take before the write is refused as a failed ACL. */
14
+ const ACL_TIMEOUT_MS = 10_000;
15
+
16
+ /** Owner read/write — the mode every key file is CREATED with. */
17
+ export const OWNER_ONLY_MODE = 0o600;
18
+
19
+ /**
20
+ * Tries for a rename over a file another process holds open. Windows refuses that rename with
21
+ * EPERM or EBUSY rather than replacing the name the way POSIX does — an editor, an indexer or an
22
+ * antivirus scan holding `.secrets.key` for a moment — so a short, bounded retry outlasts the
23
+ * holder; a holder that never lets go is still an error, after ~150 ms rather than never.
24
+ */
25
+ export const RENAME_ATTEMPTS = 5;
26
+ const isHeldOpen = (code: string | undefined): boolean => code === 'EPERM' || code === 'EBUSY';
27
+
28
+ /** Every effect, injectable: the Windows branch is proven on Linux with a fake. */
29
+ export interface KeyFileIo {
30
+ readonly platform: string;
31
+ readonly env: Readonly<Record<string, string | undefined>>;
32
+ readonly writeExclusive: (path: string, text: string, mode: number) => void;
33
+ readonly rename: (from: string, to: string) => void;
34
+ readonly remove: (path: string) => void;
35
+ readonly runAcl: (argv: readonly string[]) => { exitCode: number; output: string };
36
+ readonly username: () => string;
37
+ readonly sleep: (ms: number) => void;
38
+ }
39
+
40
+ /** Built per call, never at import: the barrel reaches this module from browser bundles too. */
41
+ const systemIo = (): KeyFileIo => ({
42
+ platform: process.platform,
43
+ env: process.env,
44
+ // `wx`: the mode applies only when a write CREATES the file, so never write into a leftover.
45
+ writeExclusive: (path, text, mode) =>
46
+ writeFileSync(path, text, { encoding: 'utf-8', mode, flag: 'wx' }),
47
+ rename: renameSync,
48
+ remove: (path) => rmSync(path, { force: true }),
49
+ runAcl: (argv) => {
50
+ // Bounded: icacls on one local file answers in milliseconds; a hang must not hold the boot.
51
+ const ran = Bun.spawnSync([...argv], {
52
+ stdout: 'pipe',
53
+ stderr: 'pipe',
54
+ timeout: ACL_TIMEOUT_MS,
55
+ });
56
+ return {
57
+ exitCode: ran.exitCode,
58
+ output: `${ran.stdout.toString()}${ran.stderr.toString()}`.trim(),
59
+ };
60
+ },
61
+ // `whoami` prints `DOMAIN\user`, the form icacls resolves. Never `node:os`'s `userInfo`: the
62
+ // browser polyfill lacks it, and the barrel reaches this module from every island's bundle.
63
+ username: () =>
64
+ Bun.spawnSync(['whoami'], { stdout: 'pipe', timeout: ACL_TIMEOUT_MS }).stdout.toString().trim(),
65
+ sleep: (ms) => Bun.sleepSync(ms),
66
+ });
67
+
68
+ /** The account the key is granted to: `DOMAIN\user` where Windows names both. */
69
+ export function aclPrincipal(
70
+ env: Readonly<Record<string, string | undefined>>,
71
+ username: () => string,
72
+ ): string {
73
+ const user = env['USERNAME'];
74
+ const domain = env['USERDOMAIN'];
75
+ if (user !== undefined && user !== '') {
76
+ return domain !== undefined && domain !== '' ? `${domain}\\${user}` : user;
77
+ }
78
+ return username();
79
+ }
80
+
81
+ /**
82
+ * `/inheritance:r` drops every ACE the directory handed down; `/grant:r` REPLACES any explicit one
83
+ * for the principal with full control. What is left is one entry: the account that wrote the key.
84
+ */
85
+ export function ownerOnlyAclArgv(path: string, principal: string): readonly string[] {
86
+ return ['icacls', path, '/inheritance:r', '/grant:r', `${principal}:F`];
87
+ }
88
+
89
+ /** icacls refused, so the key was never written: no file is better than a readable one. */
90
+ export class SecretsKeyAclError extends UltimateError {
91
+ constructor(input: { path: string; principal: string; exitCode: number; output: string }) {
92
+ super({
93
+ code: 'X_SECRETS_KEY_ACL_FAILED',
94
+ cause: `icacls could not restrict ${renderCauseValue(input.path)} to ${renderCauseValue(input.principal)} (exit ${input.exitCode}: ${renderCauseValue(input.output)}), so the key was not written — on Windows a file's mode does not keep other accounts out, its ACL does`,
95
+ fix: 'x doctor --json # on Windows: where.exe icacls must resolve (C:\\Windows\\System32) and whoami names the account the key is granted to — or keep no key file and set ULTIMATE_SECRETS_KEY instead',
96
+ meta: { path: input.path, exitCode: input.exitCode },
97
+ });
98
+ }
99
+ }
100
+
101
+ /** On win32, cut `path`'s ACL to the current account. A no-op elsewhere: the mode did it. */
102
+ export function restrictToOwner(path: string, io: KeyFileIo = systemIo()): void {
103
+ if (io.platform !== 'win32') return;
104
+ const principal = aclPrincipal(io.env, io.username);
105
+ const ran = io.runAcl(ownerOnlyAclArgv(path, principal));
106
+ if (ran.exitCode !== 0) {
107
+ throw new SecretsKeyAclError({ path, principal, exitCode: ran.exitCode, output: ran.output });
108
+ }
109
+ }
110
+
111
+ /** `rename(from, to)`, retried on EPERM/EBUSY only, `RENAME_ATTEMPTS` tries in all. */
112
+ export function renameOver(from: string, to: string, io: KeyFileIo = systemIo()): void {
113
+ for (let attempt = 1; ; attempt++) {
114
+ try {
115
+ io.rename(from, to);
116
+ return;
117
+ } catch (error) {
118
+ if (attempt >= RENAME_ATTEMPTS || !isHeldOpen(stringField(error, 'code'))) throw error;
119
+ io.sleep(backoffDelay({ attempt, base: 10, max: 160 }));
120
+ }
121
+ }
122
+ }
123
+
124
+ /**
125
+ * Write `text` to `path`, readable by its owner alone, atomically: a fresh temp file created at
126
+ * 0600 (and ACL-restricted on Windows) is renamed over the target, so a reader sees the old file or
127
+ * the new one, and rotating over a key that was world-readable never leaves the new key so.
128
+ */
129
+ export function writeOwnerOnlyFile(path: string, text: string, io: KeyFileIo = systemIo()): void {
130
+ const temp = `${path}.${crypto.randomUUID()}.tmp`;
131
+ try {
132
+ io.writeExclusive(temp, text, OWNER_ONLY_MODE);
133
+ restrictToOwner(temp, io);
134
+ renameOver(temp, path, io);
135
+ } catch (error) {
136
+ io.remove(temp);
137
+ throw error;
138
+ }
139
+ }