@ultimat3/core 21.0.0 → 22.1.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 -527
- package/README.md +39 -1
- package/package.json +2 -2
- package/src/address-class.ts +143 -0
- package/src/canonical-json.ts +24 -1
- package/src/client-flight.ts +5 -2
- 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/core-error-codes.ts +1 -0
- package/src/dev-secrets.ts +45 -0
- package/src/exports/secrets.ts +1 -0
- 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 +31 -29
- 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/metrics.ts +8 -5
- package/src/otlp-metric-exporter.ts +39 -12
- package/src/otlp-span-exporter.ts +38 -15
- package/src/same-origin.ts +51 -0
- package/src/secrets-store.ts +51 -5
- package/src/source-mask.ts +30 -0
- package/src/type-pins.ts +9 -0
- package/src/result.ts +0 -78
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// Single responsibility: the boot refusal of a shipped development signing secret outside a local
|
|
2
|
+
// environment. `x doctor` reported it; nothing failed — so a production pod that forgot
|
|
3
|
+
// ULTIMATE_CURSOR_SECRET signed every cursor with a key published in this package.
|
|
4
|
+
|
|
5
|
+
import { usesDevCursorSecret } from './cursor';
|
|
6
|
+
import { isLocal } from './environment';
|
|
7
|
+
import { UltimateError } from './errors';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* `X_CURSOR_SECRET_DEV`, the code `x doctor` already reports for this key — one condition, one code.
|
|
11
|
+
* Doctor warns; this refuses the boot.
|
|
12
|
+
*/
|
|
13
|
+
export class CursorSecretDevError extends UltimateError {
|
|
14
|
+
static readonly code = 'X_CURSOR_SECRET_DEV';
|
|
15
|
+
override readonly name = 'CursorSecretDevError';
|
|
16
|
+
|
|
17
|
+
// One secret today, so the fix is a literal: a spliced name would be a value in a pasted command.
|
|
18
|
+
constructor() {
|
|
19
|
+
super({
|
|
20
|
+
code: CursorSecretDevError.code,
|
|
21
|
+
cause:
|
|
22
|
+
'ULTIMATE_CURSOR_SECRET is unset, so this process signs cursors with the development key the framework ships — anyone can forge a page position',
|
|
23
|
+
fix: "x secrets set ULTIMATE_CURSOR_SECRET — or export ULTIMATE_CURSOR_SECRET from the platform's secret store",
|
|
24
|
+
meta: { variable: 'ULTIMATE_CURSOR_SECRET' },
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface DevSecretsOptions {
|
|
30
|
+
/** Which environment the boot is — defaults to `process.env`. */
|
|
31
|
+
readonly env?: Readonly<Record<string, string | undefined>> | undefined;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Throws when a shipped dev secret is in use and the environment is not `development`/`test`.
|
|
36
|
+
*
|
|
37
|
+
* FAILS CLOSED: a process with neither `ULTIMATE_ENV` nor `NODE_ENV` resolves as `production`
|
|
38
|
+
* here, the answer `templates/scaffold-auth.ts` already gives — `isLocal()`'s own fallback is
|
|
39
|
+
* `development`, which is exactly the process that forgot to say. The secret itself is read where
|
|
40
|
+
* signing reads it, so this refuses what the process WILL sign with, not what `env` claims.
|
|
41
|
+
*/
|
|
42
|
+
export function assertNoDevSecretsOutsideLocal(options: DevSecretsOptions = {}): void {
|
|
43
|
+
if (isLocal({ env: options.env, fallback: 'production' })) return;
|
|
44
|
+
if (usesDevCursorSecret()) throw new CursorSecretDevError();
|
|
45
|
+
}
|
package/src/exports/secrets.ts
CHANGED
|
@@ -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
|
@@ -41,6 +41,8 @@ export {
|
|
|
41
41
|
userActor,
|
|
42
42
|
withFacts,
|
|
43
43
|
} from './actor';
|
|
44
|
+
export type { AddressClass } from './address-class';
|
|
45
|
+
export { classifyAddress, isPublicAddress } from './address-class';
|
|
44
46
|
export { APP_VERSION_KEY, appVersion, DEFAULT_APP_VERSION } from './app-version';
|
|
45
47
|
export { assert, assertNever, type InvariantOptions, invariant } from './assert';
|
|
46
48
|
export { type AsyncContext, asyncContext } from './async-context';
|
|
@@ -96,6 +98,7 @@ export type {
|
|
|
96
98
|
AuthConfig,
|
|
97
99
|
CacheConfig,
|
|
98
100
|
DatabaseConfig,
|
|
101
|
+
DrainConfig,
|
|
99
102
|
JobsConfig,
|
|
100
103
|
McpConfig,
|
|
101
104
|
NotifyConfig,
|
|
@@ -133,6 +136,8 @@ export {
|
|
|
133
136
|
usesDevCursorSecret,
|
|
134
137
|
} from './cursor';
|
|
135
138
|
export { compareDecimalText } from './decimal-order';
|
|
139
|
+
export type { DevSecretsOptions } from './dev-secrets';
|
|
140
|
+
export { assertNoDevSecretsOutsideLocal, CursorSecretDevError } from './dev-secrets';
|
|
136
141
|
export type {
|
|
137
142
|
Env,
|
|
138
143
|
EnvBooleanVar,
|
|
@@ -320,22 +325,13 @@ export {
|
|
|
320
325
|
noopErrorReporter,
|
|
321
326
|
noopExporter,
|
|
322
327
|
noopMetricExporter,
|
|
323
|
-
OTEL_SAMPLER_ARG_KEY,
|
|
324
|
-
OTEL_SAMPLER_KEY,
|
|
325
|
-
OTLP_ENDPOINT_KEY,
|
|
326
|
-
OTLP_HEADERS_KEY,
|
|
327
|
-
OTLP_PROTOCOL_KEY,
|
|
328
|
-
OTLP_SCOPE,
|
|
329
328
|
OtlpEndpointInvalidError,
|
|
330
329
|
OtlpHeadersInvalidError,
|
|
331
330
|
OtlpProtocolUnsupportedError,
|
|
332
|
-
OVERFLOW_ATTRIBUTE,
|
|
333
|
-
otlpAttributes,
|
|
334
331
|
otlpEndpoint,
|
|
335
332
|
otlpHeaders,
|
|
336
333
|
otlpMetricExporter,
|
|
337
334
|
otlpMetricsRequest,
|
|
338
|
-
otlpResource,
|
|
339
335
|
otlpSpanExporter,
|
|
340
336
|
otlpTraceRequest,
|
|
341
337
|
parentBasedRatioSampler,
|
|
@@ -353,7 +349,6 @@ export {
|
|
|
353
349
|
reportError,
|
|
354
350
|
requestDuration,
|
|
355
351
|
requests,
|
|
356
|
-
resetDefaultSampler,
|
|
357
352
|
resetErrorReporting,
|
|
358
353
|
resetMetrics,
|
|
359
354
|
resetTelemetry,
|
|
@@ -368,7 +363,6 @@ export {
|
|
|
368
363
|
startSpan,
|
|
369
364
|
traceparent,
|
|
370
365
|
tryOtlpEndpoint,
|
|
371
|
-
unixNano,
|
|
372
366
|
withSpan,
|
|
373
367
|
withSpanContext,
|
|
374
368
|
} from './exports/observability';
|
|
@@ -396,25 +390,15 @@ export {
|
|
|
396
390
|
masterKeyPath,
|
|
397
391
|
openSecrets,
|
|
398
392
|
parseMasterKey,
|
|
399
|
-
parseSecretsEnvelope,
|
|
400
393
|
readSecretsFile,
|
|
401
394
|
requireMasterKey,
|
|
402
395
|
revealOptionalSecret,
|
|
403
396
|
revealSecret,
|
|
404
|
-
SECRET_BRAND,
|
|
405
|
-
SECRET_NAME,
|
|
406
|
-
SECRETS_ALG,
|
|
407
397
|
SECRETS_ERROR_CODES,
|
|
408
398
|
SECRETS_FILE,
|
|
409
|
-
SECRETS_IV_BYTES,
|
|
410
|
-
SECRETS_KEY_BYTES,
|
|
411
399
|
SECRETS_KEY_ENV,
|
|
412
400
|
SECRETS_KEY_FILE,
|
|
413
|
-
SECRETS_KEY_HEX_LENGTH,
|
|
414
|
-
SECRETS_KEY_ID_LENGTH,
|
|
415
401
|
SECRETS_KEY_MODE,
|
|
416
|
-
SECRETS_TAG_BYTES,
|
|
417
|
-
SECRETS_VERSION,
|
|
418
402
|
SecretsFileInvalidError,
|
|
419
403
|
SecretsFileMissingError,
|
|
420
404
|
SecretsKeyInvalidError,
|
|
@@ -427,6 +411,7 @@ export {
|
|
|
427
411
|
secretsFileExists,
|
|
428
412
|
secretsPath,
|
|
429
413
|
serializeSecretValues,
|
|
414
|
+
stagedMasterKeyPath,
|
|
430
415
|
writeMasterKeyFile,
|
|
431
416
|
writeSecretsFile,
|
|
432
417
|
} from './exports/secrets';
|
|
@@ -441,6 +426,8 @@ export { createFlightGate, gateOverloaded } from './flight-gate';
|
|
|
441
426
|
export { formatBytes } from './format-bytes';
|
|
442
427
|
export type { GenerationFence } from './generation-fence';
|
|
443
428
|
export { createFence, isSuperseded } from './generation-fence';
|
|
429
|
+
export type { HostDecision, HostRule } from './host-rules';
|
|
430
|
+
export { ANY_HOST, hostDecision, hostMatches } from './host-rules';
|
|
444
431
|
export type { Brand, Id } from './ids';
|
|
445
432
|
export {
|
|
446
433
|
isSpanId,
|
|
@@ -456,7 +443,7 @@ export {
|
|
|
456
443
|
uuid,
|
|
457
444
|
uuidTimestamp,
|
|
458
445
|
} from './ids';
|
|
459
|
-
export {
|
|
446
|
+
export type { ImageFit, ResizeSpec } from './image/canvas';
|
|
460
447
|
export { parseColor } from './image/color';
|
|
461
448
|
export {
|
|
462
449
|
ImageDecodeFailedError,
|
|
@@ -488,13 +475,12 @@ export type { ImageFormat, ImageInfo } from './image/probe';
|
|
|
488
475
|
export { IMAGE_FORMATS, IMAGE_MIME_TYPES, probeImage, sniffImageFormat } from './image/probe';
|
|
489
476
|
export type { ImageSize, Raster } from './image/raster';
|
|
490
477
|
export {
|
|
491
|
-
assertPixelBudget,
|
|
492
478
|
createRaster,
|
|
493
479
|
hasAlpha,
|
|
494
480
|
MAX_IMAGE_PIXELS,
|
|
495
|
-
rasterFrom,
|
|
496
481
|
} from './image/raster';
|
|
497
482
|
export { impersonate, impersonationReason, isImpersonating } from './impersonate';
|
|
483
|
+
export { withInProcessFetch } from './in-process-fetch';
|
|
498
484
|
export {
|
|
499
485
|
assertLocale,
|
|
500
486
|
cachedFormatter,
|
|
@@ -503,6 +489,7 @@ export {
|
|
|
503
489
|
MAX_CACHED_FORMATTERS,
|
|
504
490
|
MAX_LOCALE_EXCERPT,
|
|
505
491
|
} from './intl-cache';
|
|
492
|
+
export { isIsoDateTime } from './iso-date';
|
|
506
493
|
export { isJsonObject } from './json-object';
|
|
507
494
|
export type {
|
|
508
495
|
HealthPayload,
|
|
@@ -516,7 +503,6 @@ export type {
|
|
|
516
503
|
ShutdownHook,
|
|
517
504
|
ShutdownPhase,
|
|
518
505
|
ShutdownReason,
|
|
519
|
-
SignalHandlerOptions,
|
|
520
506
|
} from './lifecycle';
|
|
521
507
|
export {
|
|
522
508
|
beginWork,
|
|
@@ -527,23 +513,37 @@ export {
|
|
|
527
513
|
healthzPayload,
|
|
528
514
|
idleWaiterCount,
|
|
529
515
|
inflightCount,
|
|
530
|
-
installSignalHandlers,
|
|
531
516
|
isDraining,
|
|
532
517
|
lifecycleState,
|
|
533
518
|
markReady,
|
|
534
519
|
onShutdown,
|
|
535
520
|
readinessCheckCount,
|
|
536
521
|
readinessChecks,
|
|
522
|
+
readinessGraceMs,
|
|
537
523
|
readyzPayload,
|
|
538
524
|
registerReadinessCheck,
|
|
539
525
|
resetLifecycle,
|
|
540
526
|
SHUTDOWN_PHASES,
|
|
541
527
|
shutdownHookCount,
|
|
542
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';
|
|
543
536
|
export { isSelfOrigin, listeningOrigins, markListening, resetListeners } from './listeners';
|
|
544
537
|
export type { Direction } from './locale-direction';
|
|
545
538
|
export { directionOf, isRtl } from './locale-direction';
|
|
546
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';
|
|
547
547
|
export { nearestName } from './nearest-name';
|
|
548
548
|
/** The message pwa's `sw.js` posts and realtime's outbox listens for. */
|
|
549
549
|
export { OUTBOX_DRAIN_MESSAGE, type OutboxDrainMessage } from './outbox-drain';
|
|
@@ -582,8 +582,6 @@ export {
|
|
|
582
582
|
REQUEST_TIMEOUT_HEADER,
|
|
583
583
|
remainingBudgetMs,
|
|
584
584
|
} from './request-budget';
|
|
585
|
-
export type { Err, Ok, Result } from './result';
|
|
586
|
-
export { err, isErr, isOk, map, mapErr, ok, tryCatch, unwrap, unwrapOr } from './result';
|
|
587
585
|
export type { RetryDecision, RetryDeps, RetryPolicy, RetryStopReason } from './retry';
|
|
588
586
|
export { retry, retryDecision } from './retry';
|
|
589
587
|
export { isRetryableStatus, RETRYABLE_STATUSES } from './retryable-status';
|
|
@@ -592,6 +590,11 @@ export { DEFAULT_ROLE, isRole, ROLE_INFO, ROLES, resolveRole } from './roles';
|
|
|
592
590
|
export type { HydrateStrategy, OfflineStrategy, RenderMode } from './route-vocabulary';
|
|
593
591
|
export { HYDRATE_STRATEGIES, OFFLINE_STRATEGIES, RENDER_MODES } from './route-vocabulary';
|
|
594
592
|
export { safeUrl, URL_ATTRIBUTES } from './safe-url';
|
|
593
|
+
export {
|
|
594
|
+
type OriginEvidence,
|
|
595
|
+
type OriginVerdict,
|
|
596
|
+
proveSameOrigin,
|
|
597
|
+
} from './same-origin';
|
|
595
598
|
export {
|
|
596
599
|
defineService,
|
|
597
600
|
installedServices,
|
|
@@ -608,7 +611,6 @@ export {
|
|
|
608
611
|
readPackageVersion,
|
|
609
612
|
resolveVersion,
|
|
610
613
|
VERSION_DEFINE,
|
|
611
|
-
VERSION_MANIFEST,
|
|
612
614
|
} from './version';
|
|
613
615
|
// The webhook wire format, at the tier both halves can reach — `@ultimat3/jobs` signs a delivery
|
|
614
616
|
// and `@ultimat3/http` verifies one, and neither may import the other. Same argument
|
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
|
+
}
|