@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.
- package/CLAUDE.md +189 -523
- package/README.md +62 -1
- package/package.json +6 -3
- package/src/actor.ts +14 -0
- package/src/address-class.ts +143 -0
- package/src/async-state.ts +19 -0
- package/src/canonical-json.ts +24 -1
- package/src/client-dispatch.ts +163 -0
- package/src/client-flight.ts +22 -3
- package/src/client-paths.ts +79 -0
- package/src/client-problem.ts +94 -0
- package/src/client-scope-error.ts +18 -0
- package/src/client-scope.ts +59 -0
- package/src/client-transport.ts +86 -0
- package/src/config-count.ts +19 -0
- package/src/config-fixes.ts +23 -0
- package/src/config-merge.ts +36 -0
- package/src/config.ts +122 -65
- package/src/conflict-policy.ts +48 -0
- package/src/context.ts +12 -1
- package/src/core-error-codes.ts +78 -0
- package/src/dev-secrets.ts +45 -0
- package/src/error-codes.ts +17 -66
- package/src/error-retry.ts +3 -0
- package/src/exports/error-contract.ts +2 -2
- package/src/exports/secrets.ts +1 -0
- package/src/generation-fence.ts +9 -2
- package/src/host-rules.ts +71 -0
- package/src/image/exif-orientation.ts +40 -0
- package/src/image/probe.ts +13 -1
- package/src/in-process-fetch.ts +39 -0
- package/src/index.ts +80 -30
- package/src/iso-date.ts +5 -0
- package/src/lifecycle-grace.ts +44 -0
- package/src/lifecycle-signals.ts +35 -0
- package/src/lifecycle.ts +44 -36
- package/src/logger.ts +22 -3
- package/src/measurement-actor.ts +52 -0
- package/src/metrics-text.ts +10 -2
- package/src/otlp-metric-exporter.ts +39 -12
- package/src/otlp-span-exporter.ts +38 -15
- package/src/outbound-headers.ts +16 -0
- package/src/outbox-drain.ts +14 -0
- package/src/page-meta.ts +41 -0
- package/src/page.ts +52 -0
- package/src/pending-records.ts +58 -0
- package/src/record-envelope-openapi.ts +41 -0
- package/src/record-envelope.ts +98 -0
- package/src/record-sink.ts +129 -0
- package/src/schema-error-codes.ts +1 -1
- package/src/secrets-errors.ts +1 -1
- package/src/secrets-store.ts +51 -5
- package/src/service.ts +9 -0
- package/src/source-mask.ts +30 -0
- package/src/telemetry.ts +3 -0
- package/src/type-pins.ts +9 -0
- package/src/write-digest.ts +29 -0
- package/src/write-origin.ts +31 -0
- 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;
|
package/src/image/probe.ts
CHANGED
|
@@ -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
|
-
|
|
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 {
|
|
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 {
|
|
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';
|
package/src/iso-date.ts
ADDED
|
@@ -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
|
+
}
|