@ultimat3/core 20.2.1 → 22.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 (59) hide show
  1. package/CLAUDE.md +189 -523
  2. package/README.md +62 -1
  3. package/package.json +6 -3
  4. package/src/actor.ts +14 -0
  5. package/src/address-class.ts +143 -0
  6. package/src/async-state.ts +19 -0
  7. package/src/canonical-json.ts +24 -1
  8. package/src/client-dispatch.ts +163 -0
  9. package/src/client-flight.ts +22 -3
  10. package/src/client-paths.ts +79 -0
  11. package/src/client-problem.ts +94 -0
  12. package/src/client-scope-error.ts +18 -0
  13. package/src/client-scope.ts +59 -0
  14. package/src/client-transport.ts +86 -0
  15. package/src/config-count.ts +19 -0
  16. package/src/config-fixes.ts +23 -0
  17. package/src/config-merge.ts +36 -0
  18. package/src/config.ts +122 -65
  19. package/src/conflict-policy.ts +48 -0
  20. package/src/context.ts +12 -1
  21. package/src/core-error-codes.ts +78 -0
  22. package/src/dev-secrets.ts +45 -0
  23. package/src/error-codes.ts +17 -66
  24. package/src/error-retry.ts +3 -0
  25. package/src/exports/error-contract.ts +2 -2
  26. package/src/exports/secrets.ts +1 -0
  27. package/src/generation-fence.ts +9 -2
  28. package/src/host-rules.ts +71 -0
  29. package/src/image/exif-orientation.ts +40 -0
  30. package/src/image/probe.ts +13 -1
  31. package/src/in-process-fetch.ts +39 -0
  32. package/src/index.ts +80 -30
  33. package/src/iso-date.ts +5 -0
  34. package/src/lifecycle-grace.ts +44 -0
  35. package/src/lifecycle-signals.ts +35 -0
  36. package/src/lifecycle.ts +44 -36
  37. package/src/logger.ts +22 -3
  38. package/src/measurement-actor.ts +52 -0
  39. package/src/metrics-text.ts +10 -2
  40. package/src/otlp-metric-exporter.ts +39 -12
  41. package/src/otlp-span-exporter.ts +38 -15
  42. package/src/outbound-headers.ts +16 -0
  43. package/src/outbox-drain.ts +14 -0
  44. package/src/page-meta.ts +41 -0
  45. package/src/page.ts +52 -0
  46. package/src/pending-records.ts +58 -0
  47. package/src/record-envelope-openapi.ts +41 -0
  48. package/src/record-envelope.ts +98 -0
  49. package/src/record-sink.ts +129 -0
  50. package/src/schema-error-codes.ts +1 -1
  51. package/src/secrets-errors.ts +1 -1
  52. package/src/secrets-store.ts +51 -5
  53. package/src/service.ts +9 -0
  54. package/src/source-mask.ts +30 -0
  55. package/src/telemetry.ts +3 -0
  56. package/src/type-pins.ts +9 -0
  57. package/src/write-digest.ts +29 -0
  58. package/src/write-origin.ts +31 -0
  59. package/src/result.ts +0 -78
@@ -0,0 +1,71 @@
1
+ // `allowHosts`, as a decision every browser driver asks before a request leaves — never a note in
2
+ // a README. A headless browser inside your network is the widest SSRF surface an app can own: one
3
+ // injected `<img src="http://169.254.169.254/…">` on a page you do not control is a credential
4
+ // read. In core because two tier-5 packages drive a browser (`scraping`, and `cli`'s `x shot`) and
5
+ // neither may import the other; two copies of this rule would be two answers to "may it leave".
6
+
7
+ export type HostRule = string;
8
+
9
+ /** The one spelling that means "every host", written out so it is visible in review. */
10
+ export const ANY_HOST: HostRule = '*';
11
+
12
+ /**
13
+ * Schemes with no host to match. `about:blank` is where every browser starts, `data:` and `blob:`
14
+ * never leave the process — refusing them would refuse the first page load of every run.
15
+ */
16
+ const HOSTLESS_SCHEMES = new Set(['about:', 'data:', 'blob:']);
17
+
18
+ /**
19
+ * Hostless AND refused, which is why it is not on the line above — it sat there until 2026-09.
20
+ * `javascript:` has no host for the same reason `data:` has none, and that is the whole
21
+ * resemblance: the other three are inert content this process renders, while this one is code
22
+ * EXECUTED in the current document's origin, with the session's cookies and the session's
23
+ * `localStorage` already in scope. `allowHosts` cannot say anything about a URL with no host to
24
+ * name, so "no host, therefore allowed" was the allow list opting itself out of the one navigation
25
+ * that needs no host to exfiltrate through — `javascript:fetch('/admin').then(post_elsewhere)` is
26
+ * a same-origin read on an allow-listed site. Fail closed; there is no legitimate scrape verb that
27
+ * needs it (`page.eval` is the declared seam).
28
+ */
29
+ const REFUSED_SCHEMES = new Set(['javascript:']);
30
+
31
+ export interface HostDecision {
32
+ readonly allowed: boolean;
33
+ /** The host the URL resolved to, `''` for a hostless scheme. */
34
+ readonly host: string;
35
+ }
36
+
37
+ /**
38
+ * `example.com` matches that host EXACTLY. `*.example.com` matches any subdomain and NOT the
39
+ * apex — the two are written separately on purpose: an allow list that silently included every
40
+ * subdomain would let a `cdn-user-content.example.com` (whose contents somebody else controls)
41
+ * through a rule an author wrote for the apex.
42
+ */
43
+ export function hostMatches(host: string, rule: HostRule): boolean {
44
+ if (rule === ANY_HOST) return true;
45
+ const normalised = host.toLowerCase();
46
+ const cleaned = rule.trim().toLowerCase();
47
+ if (cleaned.startsWith('*.')) {
48
+ const suffix = cleaned.slice(1);
49
+ return normalised.endsWith(suffix) && normalised.length > suffix.length;
50
+ }
51
+ return normalised === cleaned;
52
+ }
53
+
54
+ /**
55
+ * Fails CLOSED: a URL that cannot be parsed is refused. A driver handed a malformed request has
56
+ * no way to know where it would have gone, and "we could not tell, so we let it through" is the
57
+ * decision that makes the whole list advisory.
58
+ */
59
+ export function hostDecision(url: string, allowHosts: readonly HostRule[]): HostDecision {
60
+ const scheme = url.slice(0, Math.max(0, url.indexOf(':') + 1)).toLowerCase();
61
+ if (REFUSED_SCHEMES.has(scheme)) return { allowed: false, host: '' };
62
+ if (HOSTLESS_SCHEMES.has(scheme)) return { allowed: true, host: '' };
63
+ let host: string;
64
+ try {
65
+ host = new URL(url).hostname;
66
+ } catch {
67
+ return { allowed: false, host: '' };
68
+ }
69
+ if (host === '') return { allowed: false, host };
70
+ return { allowed: allowHosts.some((rule) => hostMatches(host, rule)), host };
71
+ }
@@ -0,0 +1,40 @@
1
+ // Single responsibility: the EXIF orientation tag (0x0112) of one JPEG APP1 segment. The decoder
2
+ // applies it — `Bun.Image` reports a 40x20 sensor image tagged `6` as 20x40 — so a probe that
3
+ // ignored it reserved a layout box with width and height swapped.
4
+
5
+ const EXIF_HEADER = [0x45, 0x78, 0x69, 0x66, 0x00, 0x00]; // "Exif\0\0"
6
+ const ORIENTATION_TAG = 0x0112;
7
+ const SHORT = 3;
8
+
9
+ /**
10
+ * The orientation (1–8) an APP1 payload declares, or `1` when it declares none or cannot be read.
11
+ * Never throws: EXIF is metadata beside the image, and a malformed block is one the decoder
12
+ * ignores too — refusing the whole image over it would fail a file every browser renders.
13
+ *
14
+ * `payload` is the segment body after its length word.
15
+ */
16
+ export function exifOrientation(payload: Uint8Array): number {
17
+ if (payload.length < EXIF_HEADER.length + 8) return 1;
18
+ if (EXIF_HEADER.some((byte, index) => payload[index] !== byte)) return 1;
19
+ const tiff = payload.subarray(EXIF_HEADER.length);
20
+ const view = new DataView(tiff.buffer, tiff.byteOffset, tiff.byteLength);
21
+ const order = view.getUint16(0);
22
+ if (order !== 0x4949 && order !== 0x4d4d) return 1;
23
+ const little = order === 0x4949;
24
+ if (view.getUint16(2, little) !== 42) return 1;
25
+ const ifd = view.getUint32(4, little);
26
+ if (ifd + 2 > tiff.length) return 1;
27
+ const entries = view.getUint16(ifd, little);
28
+ for (let index = 0; index < entries; index += 1) {
29
+ const at = ifd + 2 + index * 12;
30
+ if (at + 12 > tiff.length) return 1;
31
+ if (view.getUint16(at, little) !== ORIENTATION_TAG) continue;
32
+ if (view.getUint16(at + 2, little) !== SHORT) return 1;
33
+ const value = view.getUint16(at + 8, little);
34
+ return value >= 1 && value <= 8 ? value : 1;
35
+ }
36
+ return 1;
37
+ }
38
+
39
+ /** Orientations 5–8 transpose the image: the stored width is the displayed height. */
40
+ export const swapsAxes = (orientation: number): boolean => orientation >= 5 && orientation <= 8;
@@ -5,6 +5,7 @@
5
5
  // than in header bytes, so that reading lives in `probe-svg.ts`.
6
6
 
7
7
  import { imageDecodeFailed, imageUnsupported } from './errors';
8
+ import { exifOrientation, swapsAxes } from './exif-orientation';
8
9
  import { hasSvgRoot, probeSvg } from './probe-svg';
9
10
  import { assertPixelBudget, type ImageSize } from './raster';
10
11
 
@@ -137,9 +138,12 @@ const isStandaloneMarker = (marker: number): boolean =>
137
138
  /**
138
139
  * Walks segment lengths to the first SOF. Baseline, progressive and lossless all declare their
139
140
  * size the same way — probing is not decoding, so a format the decoder refuses still measures.
141
+ * An EXIF orientation met on the way (APP1, always ahead of the SOF) is applied to the answer,
142
+ * because the decoder applies it: the box reserved must be the box that renders.
140
143
  */
141
144
  function probeJpeg(bytes: Uint8Array): ImageSize {
142
145
  const view = viewOf(bytes);
146
+ let orientation = 1;
143
147
  let at = 2;
144
148
  while (at + 3 < bytes.length) {
145
149
  if (byteAt(bytes, at) !== 0xff) {
@@ -164,7 +168,15 @@ function probeJpeg(bytes: Uint8Array): ImageSize {
164
168
  const length = view.getUint16(at + 2);
165
169
  if (isSofMarker(marker)) {
166
170
  requireBytes(bytes, at + 9, 'JPEG', 'the SOF segment width and height');
167
- return { width: view.getUint16(at + 7), height: view.getUint16(at + 5) };
171
+ const width = view.getUint16(at + 7);
172
+ const height = view.getUint16(at + 5);
173
+ return swapsAxes(orientation) ? { width: height, height: width } : { width, height };
174
+ }
175
+ // The first APP1 declaring a turn decides; XMP also rides APP1 and reads as none.
176
+ if (marker === 0xe1 && orientation === 1 && length >= 2) {
177
+ orientation = exifOrientation(
178
+ bytes.subarray(at + 4, Math.min(bytes.length, at + 2 + length)),
179
+ );
168
180
  }
169
181
  if (length < 2) {
170
182
  throw imageDecodeFailed(`JPEG segment 0xFF${marker.toString(16)} declares length ${length}`, {
@@ -0,0 +1,39 @@
1
+ // Where a SERVER-side typed call goes when its answer lives in this process. The build's
2
+ // measurement render has no server to reach, so a route `load` reading the app's own queries over
3
+ // `queryClient()` failed as the network; the process that owns the route table answers instead.
4
+ //
5
+ // ZERO bytes in a browser, by construction: the transport's dispatch already calls
6
+ // `globalThis.fetch` at call time, so this module wraps THAT, once, in the process that first opens
7
+ // a scope — and only a server process ever imports this module. Outside a scope the wrapper hands
8
+ // every call to the fetch it wrapped, untouched. The first version added a slot read to the
9
+ // dispatch itself, and that was 46 B in every island that calls `rpc()` or `queryClient()`.
10
+ import { asyncContext } from './async-context';
11
+ import type { FetchLike } from './client-dispatch';
12
+
13
+ const scope = asyncContext<FetchLike>('the in-process dispatch');
14
+
15
+ const INSTALLED: unique symbol = Symbol.for('ultimate.in-process-fetch');
16
+
17
+ /** Wrap `globalThis.fetch` once per process; a re-wrap after someone replaced it wraps theirs. */
18
+ function install(): void {
19
+ const current = globalThis.fetch as typeof fetch & { [INSTALLED]?: true };
20
+ if (current[INSTALLED] === true) return;
21
+ const wrapped = (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
22
+ const inProcess = scope.get();
23
+ if (inProcess === undefined) return current(input, init);
24
+ const url = typeof input === 'string' ? input : input instanceof URL ? input.href : input.url;
25
+ return inProcess(url, init ?? {});
26
+ };
27
+ globalThis.fetch = Object.assign(wrapped, current, { [INSTALLED]: true as const });
28
+ }
29
+
30
+ /**
31
+ * Run `fn` with every `fetch` inside it — so every `clientTransport` call, `queryClient()` and
32
+ * `rpc()` included — answered by `fetchImpl`: the app's own HTTP pipeline handed a `Request`,
33
+ * rather than the network. A caller's explicit `fetchImpl` never reaches `globalThis.fetch` at all,
34
+ * so a test's double is never overridden by an ambient scope.
35
+ */
36
+ export function withInProcessFetch<T>(fetchImpl: FetchLike, fn: () => T): T {
37
+ install();
38
+ return scope.run(fetchImpl, fn);
39
+ }
package/src/index.ts CHANGED
@@ -14,6 +14,7 @@
14
14
  // are declared side-effecting too and are deliberately NOT anchored: each is reached by whatever
15
15
  // uses it, and anchoring `context.ts` alone measured +3,485 B on a browser chunk for a provider a
16
16
  // browser can never fire.
17
+ import './core-error-codes';
17
18
  import './schema-error-codes';
18
19
 
19
20
  export type {
@@ -40,13 +41,19 @@ export {
40
41
  userActor,
41
42
  withFacts,
42
43
  } from './actor';
44
+ export type { AddressClass } from './address-class';
45
+ export { classifyAddress, isPublicAddress } from './address-class';
43
46
  export { APP_VERSION_KEY, appVersion, DEFAULT_APP_VERSION } from './app-version';
44
47
  export { assert, assertNever, type InvariantOptions, invariant } from './assert';
45
48
  export { type AsyncContext, asyncContext } from './async-context';
49
+ /** The four shapes an async region can be in — produced by `realtime`, rendered by `ui`. */
50
+ export type { AsyncState } from './async-state';
46
51
  export type { BackoffCurve, BackoffOptions, JitterMode, Random } from './backoff';
47
52
  export { backoffDelay } from './backoff';
48
53
  export { CACHE_TIERS, type CacheTierName } from './cache-vocabulary';
49
54
  export { canonicalJson, fingerprint } from './canonical-json';
55
+ export type { FetchLike, TransportRequest } from './client-dispatch';
56
+ export { IDEMPOTENCY_HEADER } from './client-dispatch';
50
57
  /**
51
58
  * Flight control for a typed client, and OPT-IN by construction: `@ultimat3/action`'s and
52
59
  * `@ultimat3/query`'s `client.ts` each name `ClientFlight` as a TYPE only, so a caller that never
@@ -61,6 +68,23 @@ export type {
61
68
  FlightPlan,
62
69
  } from './client-flight';
63
70
  export { createClientFlight, DEFAULT_CLIENT_RETRY, isTransientFailure } from './client-flight';
71
+ export type { ActionRoute } from './client-paths';
72
+ export {
73
+ actionPath,
74
+ actionRoute,
75
+ pluralize,
76
+ QUERY_PATH_PREFIX,
77
+ queryPath,
78
+ splitWords,
79
+ } from './client-paths';
80
+ export type { TransportFailure } from './client-problem';
81
+ /**
82
+ * The browser seam (plan 101): ONE HTTP function, the records envelope it decodes, the per-tab
83
+ * page handle records land in, and the principal fence every client layer subscribes to.
84
+ */
85
+ export type { ClientScope } from './client-scope';
86
+ export { onRescope, rescope } from './client-scope';
87
+ export { clientTransport } from './client-transport';
64
88
  /** What a typed client puts on the wire. `retryForStatus` is what fills a failure's `retry`. */
65
89
  export type { WireAnswer } from './client-wire';
66
90
  export { FRAMEWORK_CODE, problemOf, retryForStatus, traceHeaders } from './client-wire';
@@ -74,6 +98,7 @@ export type {
74
98
  AuthConfig,
75
99
  CacheConfig,
76
100
  DatabaseConfig,
101
+ DrainConfig,
77
102
  JobsConfig,
78
103
  McpConfig,
79
104
  NotifyConfig,
@@ -86,6 +111,8 @@ export type {
86
111
  export { defineConfig, INBOX_RETENTION_KEYS } from './config';
87
112
  export type { PwaColors, PwaConfig, PwaOfflineConfig, PwaSchemeColors } from './config-pwa';
88
113
  export { PWA_COLOR_KEYS, PWA_SCHEMES } from './config-pwa';
114
+ export type { ConflictPolicy, ResolveConflictOptions, Row } from './conflict-policy';
115
+ export { resolveConflict } from './conflict-policy';
89
116
  export type { Ctx, CtxFacts, CtxInit, CtxPatch, CtxServices, ServiceBag } from './context';
90
117
  export {
91
118
  createContext,
@@ -109,6 +136,8 @@ export {
109
136
  usesDevCursorSecret,
110
137
  } from './cursor';
111
138
  export { compareDecimalText } from './decimal-order';
139
+ export type { DevSecretsOptions } from './dev-secrets';
140
+ export { assertNoDevSecretsOutsideLocal, CursorSecretDevError } from './dev-secrets';
112
141
  export type {
113
142
  Env,
114
143
  EnvBooleanVar,
@@ -296,22 +325,13 @@ export {
296
325
  noopErrorReporter,
297
326
  noopExporter,
298
327
  noopMetricExporter,
299
- OTEL_SAMPLER_ARG_KEY,
300
- OTEL_SAMPLER_KEY,
301
- OTLP_ENDPOINT_KEY,
302
- OTLP_HEADERS_KEY,
303
- OTLP_PROTOCOL_KEY,
304
- OTLP_SCOPE,
305
328
  OtlpEndpointInvalidError,
306
329
  OtlpHeadersInvalidError,
307
330
  OtlpProtocolUnsupportedError,
308
- OVERFLOW_ATTRIBUTE,
309
- otlpAttributes,
310
331
  otlpEndpoint,
311
332
  otlpHeaders,
312
333
  otlpMetricExporter,
313
334
  otlpMetricsRequest,
314
- otlpResource,
315
335
  otlpSpanExporter,
316
336
  otlpTraceRequest,
317
337
  parentBasedRatioSampler,
@@ -329,7 +349,6 @@ export {
329
349
  reportError,
330
350
  requestDuration,
331
351
  requests,
332
- resetDefaultSampler,
333
352
  resetErrorReporting,
334
353
  resetMetrics,
335
354
  resetTelemetry,
@@ -344,7 +363,6 @@ export {
344
363
  startSpan,
345
364
  traceparent,
346
365
  tryOtlpEndpoint,
347
- unixNano,
348
366
  withSpan,
349
367
  withSpanContext,
350
368
  } from './exports/observability';
@@ -372,25 +390,15 @@ export {
372
390
  masterKeyPath,
373
391
  openSecrets,
374
392
  parseMasterKey,
375
- parseSecretsEnvelope,
376
393
  readSecretsFile,
377
394
  requireMasterKey,
378
395
  revealOptionalSecret,
379
396
  revealSecret,
380
- SECRET_BRAND,
381
- SECRET_NAME,
382
- SECRETS_ALG,
383
397
  SECRETS_ERROR_CODES,
384
398
  SECRETS_FILE,
385
- SECRETS_IV_BYTES,
386
- SECRETS_KEY_BYTES,
387
399
  SECRETS_KEY_ENV,
388
400
  SECRETS_KEY_FILE,
389
- SECRETS_KEY_HEX_LENGTH,
390
- SECRETS_KEY_ID_LENGTH,
391
401
  SECRETS_KEY_MODE,
392
- SECRETS_TAG_BYTES,
393
- SECRETS_VERSION,
394
402
  SecretsFileInvalidError,
395
403
  SecretsFileMissingError,
396
404
  SecretsKeyInvalidError,
@@ -403,6 +411,7 @@ export {
403
411
  secretsFileExists,
404
412
  secretsPath,
405
413
  serializeSecretValues,
414
+ stagedMasterKeyPath,
406
415
  writeMasterKeyFile,
407
416
  writeSecretsFile,
408
417
  } from './exports/secrets';
@@ -417,6 +426,8 @@ export { createFlightGate, gateOverloaded } from './flight-gate';
417
426
  export { formatBytes } from './format-bytes';
418
427
  export type { GenerationFence } from './generation-fence';
419
428
  export { createFence, isSuperseded } from './generation-fence';
429
+ export type { HostDecision, HostRule } from './host-rules';
430
+ export { ANY_HOST, hostDecision, hostMatches } from './host-rules';
420
431
  export type { Brand, Id } from './ids';
421
432
  export {
422
433
  isSpanId,
@@ -432,7 +443,7 @@ export {
432
443
  uuid,
433
444
  uuidTimestamp,
434
445
  } from './ids';
435
- export { fitBox, type ImageFit, type ResizeSpec, scaledToFit } from './image/canvas';
446
+ export type { ImageFit, ResizeSpec } from './image/canvas';
436
447
  export { parseColor } from './image/color';
437
448
  export {
438
449
  ImageDecodeFailedError,
@@ -464,13 +475,12 @@ export type { ImageFormat, ImageInfo } from './image/probe';
464
475
  export { IMAGE_FORMATS, IMAGE_MIME_TYPES, probeImage, sniffImageFormat } from './image/probe';
465
476
  export type { ImageSize, Raster } from './image/raster';
466
477
  export {
467
- assertPixelBudget,
468
478
  createRaster,
469
479
  hasAlpha,
470
480
  MAX_IMAGE_PIXELS,
471
- rasterFrom,
472
481
  } from './image/raster';
473
482
  export { impersonate, impersonationReason, isImpersonating } from './impersonate';
483
+ export { withInProcessFetch } from './in-process-fetch';
474
484
  export {
475
485
  assertLocale,
476
486
  cachedFormatter,
@@ -479,6 +489,7 @@ export {
479
489
  MAX_CACHED_FORMATTERS,
480
490
  MAX_LOCALE_EXCERPT,
481
491
  } from './intl-cache';
492
+ export { isIsoDateTime } from './iso-date';
482
493
  export { isJsonObject } from './json-object';
483
494
  export type {
484
495
  HealthPayload,
@@ -492,7 +503,6 @@ export type {
492
503
  ShutdownHook,
493
504
  ShutdownPhase,
494
505
  ShutdownReason,
495
- SignalHandlerOptions,
496
506
  } from './lifecycle';
497
507
  export {
498
508
  beginWork,
@@ -503,25 +513,56 @@ export {
503
513
  healthzPayload,
504
514
  idleWaiterCount,
505
515
  inflightCount,
506
- installSignalHandlers,
507
516
  isDraining,
508
517
  lifecycleState,
509
518
  markReady,
510
519
  onShutdown,
511
520
  readinessCheckCount,
512
521
  readinessChecks,
522
+ readinessGraceMs,
513
523
  readyzPayload,
514
524
  registerReadinessCheck,
515
525
  resetLifecycle,
516
526
  SHUTDOWN_PHASES,
517
527
  shutdownHookCount,
518
528
  } from './lifecycle';
529
+ export {
530
+ defaultReadinessGraceMs,
531
+ READINESS_GRACE_DEFAULT_MS,
532
+ READINESS_GRACE_MAX_MS,
533
+ } from './lifecycle-grace';
534
+ export type { SignalHandlerOptions } from './lifecycle-signals';
535
+ export { installSignalHandlers } from './lifecycle-signals';
519
536
  export { isSelfOrigin, listeningOrigins, markListening, resetListeners } from './listeners';
520
537
  export type { Direction } from './locale-direction';
521
538
  export { directionOf, isRtl } from './locale-direction';
522
539
  export { isMcpExposed, type McpExposureDeclaration } from './mcp-exposure';
540
+ export type { MeasurementActorFactory } from './measurement-actor';
541
+ export {
542
+ defineMeasurementActor,
543
+ MEASUREMENT_ACTOR_ID,
544
+ measurementActor,
545
+ resetMeasurementActor,
546
+ } from './measurement-actor';
523
547
  export { nearestName } from './nearest-name';
548
+ /** The message pwa's `sw.js` posts and realtime's outbox listens for. */
549
+ export { OUTBOX_DRAIN_MESSAGE, type OutboxDrainMessage } from './outbox-drain';
550
+ /** The `<meta name>`s render writes and the page client, realtime and pwa read. */
551
+ export {
552
+ APP_UPDATE_MESSAGE,
553
+ CLIENT_BUILD_META,
554
+ CLIENT_PERSIST_META,
555
+ CLIENT_SCOPE_HEADER,
556
+ CLIENT_SCOPE_META,
557
+ CLIENT_SYNC_META,
558
+ CLIENT_SYNC_WORKER_META,
559
+ } from './page-meta';
524
560
  export { type CappedBody, readWithinLimit } from './read-capped';
561
+ export type { RecordEnvelope, RecordRows } from './record-envelope';
562
+ export { decodeRecordEnvelope, encodeRecordEnvelope, RECORDS_HEADER } from './record-envelope';
563
+ export { RECORDS_OPENAPI_HEADER, recordEnvelopeSchema } from './record-envelope-openapi';
564
+ export type { PageClient, RecordSink } from './record-sink';
565
+ export { pageClient } from './record-sink';
525
566
  export type {
526
567
  ModuleRegistrar,
527
568
  PrimitiveFactory,
@@ -541,8 +582,6 @@ export {
541
582
  REQUEST_TIMEOUT_HEADER,
542
583
  remainingBudgetMs,
543
584
  } from './request-budget';
544
- export type { Err, Ok, Result } from './result';
545
- export { err, isErr, isOk, map, mapErr, ok, tryCatch, unwrap, unwrapOr } from './result';
546
585
  export type { RetryDecision, RetryDeps, RetryPolicy, RetryStopReason } from './retry';
547
586
  export { retry, retryDecision } from './retry';
548
587
  export { isRetryableStatus, RETRYABLE_STATUSES } from './retryable-status';
@@ -551,7 +590,13 @@ export { DEFAULT_ROLE, isRole, ROLE_INFO, ROLES, resolveRole } from './roles';
551
590
  export type { HydrateStrategy, OfflineStrategy, RenderMode } from './route-vocabulary';
552
591
  export { HYDRATE_STRATEGIES, OFFLINE_STRATEGIES, RENDER_MODES } from './route-vocabulary';
553
592
  export { safeUrl, URL_ATTRIBUTES } from './safe-url';
554
- export { defineService, resetServices, type ServiceFactory } from './service';
593
+ export {
594
+ defineService,
595
+ installedServices,
596
+ registeredServiceNames,
597
+ resetServices,
598
+ type ServiceFactory,
599
+ } from './service';
555
600
  export type { FlightJoin, Scheduler, SingleFlight, SingleFlightOptions } from './single-flight';
556
601
  export { createSingleFlight } from './single-flight';
557
602
  export { endOfLiteral, maskLiterals, QUOTES, stripComments } from './source-mask';
@@ -561,7 +606,6 @@ export {
561
606
  readPackageVersion,
562
607
  resolveVersion,
563
608
  VERSION_DEFINE,
564
- VERSION_MANIFEST,
565
609
  } from './version';
566
610
  // The webhook wire format, at the tier both halves can reach — `@ultimat3/jobs` signs a delivery
567
611
  // and `@ultimat3/http` verifies one, and neither may import the other. Same argument
@@ -584,3 +628,9 @@ export {
584
628
  webhookSignature,
585
629
  webhookSigningString,
586
630
  } from './webhook-signature';
631
+ /**
632
+ * A write's public name — the digest of its idempotency key — and the server scope that carries it
633
+ * from `@ultimat3/action`'s HTTP projection to the layers that stamp it on a `records` frame.
634
+ */
635
+ export { isWriteDigest, WRITE_DIGEST_LENGTH, writeDigest } from './write-digest';
636
+ export { currentWriteOrigin, WRITE_ORIGIN_WAL_PREFIX, withWriteOrigin } from './write-origin';
@@ -0,0 +1,5 @@
1
+ // Single responsibility: re-export `@ultimat3/schema`'s ISO date-time predicate, so a package that
2
+ // depends on core and not on schema — `@ultimat3/ui` formats dates, it validates nothing — judges a
3
+ // date string with the ONE rule `t.date` and `timestamp()` use. The `time-zone-name.ts` shape.
4
+
5
+ export { isIsoDateTime } from '@ultimat3/schema';
@@ -0,0 +1,44 @@
1
+ // Single responsibility: the readiness grace's default and its domain. One module because the
2
+ // config validator and `configureLifecycle` must refuse the same values and default the same way.
3
+
4
+ import { countIssue } from './config-count';
5
+ import { tryResolveEnvironment } from './environment';
6
+
7
+ /**
8
+ * Outside a local environment. Long enough for the endpoints controller to observe `/readyz` at
9
+ * 503 and for kube-proxy/the ingress to stop routing here (`periodSeconds: 5` in the shipped chart
10
+ * is the dominant term); short enough to sit well inside a 30s `terminationGracePeriodSeconds`
11
+ * beside the 25s drain budget it is ADDED to.
12
+ */
13
+ export const READINESS_GRACE_DEFAULT_MS = 5000;
14
+
15
+ /** A grace past a minute is a stalled rollout, not a drain. */
16
+ export const READINESS_GRACE_MAX_MS = 60_000;
17
+
18
+ type EnvRecord = Readonly<Record<string, string | undefined>>;
19
+
20
+ /**
21
+ * `0` in `development`/`test`, where no load balancer is routing and a Ctrl-C should be instant;
22
+ * the full grace everywhere else. FAILS CLOSED: a process naming no environment — or a
23
+ * `ULTIMATE_ENV` that is not one — is production here, because the process that forgot to say is
24
+ * exactly the one a 502 on every deploy would reach.
25
+ */
26
+ export function defaultReadinessGraceMs(env?: EnvRecord): number {
27
+ const environment = tryResolveEnvironment({ env, fallback: 'production' });
28
+ return environment === 'development' || environment === 'test' ? 0 : READINESS_GRACE_DEFAULT_MS;
29
+ }
30
+
31
+ /**
32
+ * Why a value is not a grace, or `undefined` when it is one. A whole number of milliseconds in
33
+ * `0 ≤ v ≤ 60000`; a fraction is refused rather than rounded, since `setTimeout` would round it
34
+ * and nothing would say so.
35
+ */
36
+ export function readinessGraceIssue(value: unknown): string | undefined {
37
+ const count = countIssue(GRACE_KEY, value, 0);
38
+ if (count !== undefined) return count;
39
+ return (value as number) > READINESS_GRACE_MAX_MS
40
+ ? `${GRACE_KEY} must be at most ${READINESS_GRACE_MAX_MS} milliseconds — a longer grace is a stalled rollout, not a drain`
41
+ : undefined;
42
+ }
43
+
44
+ const GRACE_KEY = 'drain.readinessGraceMs';
@@ -0,0 +1,35 @@
1
+ // Single responsibility: wiring process signals to the one drain. Split from `lifecycle.ts`, which
2
+ // owns the state machine and the phases, when the readiness grace took it past its line ceiling.
3
+
4
+ import { drain, type ProcessSignal } from './lifecycle';
5
+
6
+ export interface SignalHandlerOptions {
7
+ readonly signals?: readonly ProcessSignal[] | undefined;
8
+ /** Call `process.exit()` once drained. Off in tests. */
9
+ readonly exit?: boolean | undefined;
10
+ }
11
+
12
+ /** Install SIGTERM/SIGINT handling. Returns an uninstall function. */
13
+ export function installSignalHandlers(options?: SignalHandlerOptions): () => void {
14
+ const signals: readonly ProcessSignal[] = options?.signals ?? ['SIGTERM', 'SIGINT'];
15
+ const handlers = new Map<ProcessSignal, () => void>();
16
+
17
+ for (const signal of signals) {
18
+ const handler = (): void => {
19
+ // Attached on BOTH settle paths, for the reason `settleWithin` gives: an unhandled rejection
20
+ // ends the process before the drain does, and the exit is what the kubelet is waiting for.
21
+ // `drain()` cannot reject today — that is `runDrain`'s `try/finally` in `lifecycle.ts`, not luck — and this is
22
+ // the one line that keeps it true when someone changes the body.
23
+ const done = (): void => {
24
+ if (options?.exit === true) process.exit(0);
25
+ };
26
+ void drain(signal).then(done, done);
27
+ };
28
+ handlers.set(signal, handler);
29
+ process.on(signal, handler);
30
+ }
31
+
32
+ return () => {
33
+ for (const [signal, handler] of handlers) process.off(signal, handler);
34
+ };
35
+ }