@ultimat3/core 23.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.
- package/CLAUDE.md +29 -26
- package/README.md +98 -30
- package/package.json +4 -7
- package/src/actor.ts +9 -0
- package/src/address-class.ts +40 -4
- package/src/assert.ts +9 -5
- package/src/audit.ts +144 -0
- package/src/aws-sigv4.ts +275 -0
- package/src/backoff.ts +16 -0
- package/src/bunfs.ts +17 -0
- package/src/client-dispatch.ts +24 -3
- package/src/client-flight.ts +68 -13
- package/src/client-problem.ts +62 -6
- package/src/client-retry-after.ts +47 -0
- package/src/client-transport.ts +3 -1
- package/src/client-wire.ts +27 -3
- package/src/config-ai.ts +32 -0
- package/src/config-defaults.ts +53 -0
- package/src/config-fixes.ts +0 -10
- package/src/config-health.ts +9 -2
- package/src/config-jobs.ts +51 -0
- package/src/config-keys.ts +170 -0
- package/src/config-mail.ts +73 -0
- package/src/config-merge.ts +9 -1
- package/src/config-navigation.ts +1 -25
- package/src/config-pwa.ts +42 -5
- package/src/config-removed.ts +131 -0
- package/src/config-shape.ts +79 -0
- package/src/config-site.ts +14 -3
- package/src/config.ts +141 -173
- package/src/context.ts +29 -13
- package/src/cookie.ts +299 -0
- package/src/core-error-codes.ts +2 -0
- package/src/cursor-page.ts +41 -0
- package/src/cursor.ts +26 -5
- package/src/decimal-order.ts +5 -4
- package/src/deprecation.ts +77 -0
- package/src/dev-secrets.ts +18 -7
- package/src/drain-deadline.ts +43 -0
- package/src/env-example.ts +9 -29
- package/src/error-reporter-sentry.ts +7 -3
- package/src/errors.ts +18 -9
- package/src/exports/error-contract.ts +0 -1
- package/src/exports/observability.ts +1 -1
- package/src/exports/secrets.ts +3 -0
- package/src/finite-option.ts +1 -1
- package/src/flight-gate.ts +43 -16
- package/src/fnv1a.ts +19 -0
- package/src/generation-fence.ts +1 -1
- package/src/health-disclosure.ts +43 -0
- package/src/host-rules.ts +28 -1
- package/src/html-escape.ts +24 -0
- package/src/ids.ts +7 -7
- package/src/image/canvas.ts +76 -5
- package/src/image/errors.ts +3 -1
- package/src/image/pipeline.ts +17 -5
- package/src/image/png-pixels.ts +29 -6
- package/src/image/probe.ts +7 -2
- package/src/image/raster.ts +27 -2
- package/src/index.ts +99 -33
- package/src/iso-date.ts +1 -1
- package/src/lifecycle-errors.ts +1 -1
- package/src/lifecycle-readiness.ts +60 -2
- package/src/lifecycle-signals.ts +27 -2
- package/src/lifecycle-types.ts +96 -0
- package/src/lifecycle.ts +44 -133
- package/src/locale-direction.ts +1 -1
- package/src/logger.ts +92 -16
- package/src/mcp-exposure.ts +70 -8
- package/src/measurement-actor.ts +16 -1
- package/src/metric-errors.ts +32 -0
- package/src/metric-registry.ts +151 -0
- package/src/metric-series.ts +94 -0
- package/src/metrics.ts +9 -255
- package/src/nearest-name.ts +11 -2
- package/src/otlp-metric-exporter.ts +1 -1
- package/src/otlp-span-exporter.ts +1 -1
- package/src/otlp.ts +44 -13
- package/src/page.ts +4 -2
- package/src/pg-executor.ts +15 -0
- package/src/public-cause.ts +37 -0
- package/src/registrar.ts +22 -4
- package/src/retry.ts +40 -7
- package/src/route-rank.ts +36 -0
- package/src/same-origin.ts +1 -1
- package/src/sampler.ts +6 -2
- package/src/secrets-errors.ts +14 -3
- package/src/secrets-key-file.ts +139 -0
- package/src/secrets-store.ts +32 -16
- package/src/service.ts +5 -5
- package/src/single-flight.ts +1 -1
- package/src/source-mask.ts +14 -8
- package/src/store-mode.ts +23 -0
- package/src/telemetry.ts +1 -1
- package/src/theme-storage.ts +12 -0
- package/src/type-pins.ts +51 -1
- package/src/image/fixtures.ts +0 -263
- package/src/time-zone-name.ts +0 -14
|
@@ -7,7 +7,7 @@ import { renderThrowable } from './error-render';
|
|
|
7
7
|
import type { ErrorReport, ErrorReporter, ErrorSeverity } from './error-reporter';
|
|
8
8
|
import { type CodedErrorInit, UltimateError } from './errors';
|
|
9
9
|
import { traceId } from './ids';
|
|
10
|
-
import { logger } from './logger';
|
|
10
|
+
import { logger, redactFields } from './logger';
|
|
11
11
|
|
|
12
12
|
export class ErrorReporterDsnInvalidError extends UltimateError {
|
|
13
13
|
static readonly code = 'X_ERROR_REPORTER_DSN_INVALID';
|
|
@@ -108,6 +108,10 @@ function payloadOf(report: ErrorReport, eventId: string): Record<string, unknown
|
|
|
108
108
|
},
|
|
109
109
|
}),
|
|
110
110
|
extra: {
|
|
111
|
+
// The caller's two records go through `redactFields`, and FIRST. Spread raw and last, a
|
|
112
|
+
// `bigint` or a cycle in `meta` made `JSON.stringify` throw — that error was never reported
|
|
113
|
+
// — `meta: { fix, stack }` replaced the framework's own, and `meta.password` left the box.
|
|
114
|
+
...redactFields(report.scope.extra ?? {}),
|
|
111
115
|
// The whole point of reporting the framework's contract instead of a message: whoever is
|
|
112
116
|
// paged reads the runnable fix next to the failure.
|
|
113
117
|
fix: report.fix,
|
|
@@ -115,8 +119,8 @@ function payloadOf(report: ErrorReport, eventId: string): Record<string, unknown
|
|
|
115
119
|
...(report.scope.requestId === undefined ? {} : { requestId: report.scope.requestId }),
|
|
116
120
|
...(report.scope.actorId === undefined ? {} : { actorId: report.scope.actorId }),
|
|
117
121
|
...(report.stack === undefined ? {} : { stack: report.stack }),
|
|
118
|
-
|
|
119
|
-
...(report.
|
|
122
|
+
// Under its own key, so no name an error author picks can collide with one above.
|
|
123
|
+
...(report.meta === undefined ? {} : { meta: redactFields(report.meta) }),
|
|
120
124
|
},
|
|
121
125
|
exception: { values: [{ type: report.code, value: `${report.title} — ${report.cause}` }] },
|
|
122
126
|
};
|
package/src/errors.ts
CHANGED
|
@@ -2,7 +2,14 @@
|
|
|
2
2
|
// Stable code + cause + exact fix command, rendered identically in the terminal, the browser
|
|
3
3
|
// overlay and `--json`. Never throw a bare Error anywhere in the framework.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Structural brand. `instanceof` is unreliable across duplicated module instances, so the guard is
|
|
7
|
+
* duck-typed on a well-known symbol instead. It is `@ultimat3/schema`'s — declared once, at the
|
|
8
|
+
* tier both error classes can reach (`core -> schema` is the declared edge) — and imported from
|
|
9
|
+
* there by anything that needs the symbol itself: core does not publish it a second time.
|
|
10
|
+
*/
|
|
11
|
+
import { ULTIMATE_ERROR_BRAND } from '@ultimat3/schema';
|
|
12
|
+
import { describeErrorCode, hasErrorCode } from './error-codes';
|
|
6
13
|
import {
|
|
7
14
|
isThrownError,
|
|
8
15
|
renderCauseValue,
|
|
@@ -12,13 +19,6 @@ import {
|
|
|
12
19
|
} from './error-render';
|
|
13
20
|
import { DEFAULT_ERROR_RETRY, type ErrorRetry, isErrorRetry, retryFor } from './error-retry';
|
|
14
21
|
|
|
15
|
-
/**
|
|
16
|
-
* Structural brand. `instanceof` is unreliable across duplicated module instances and across
|
|
17
|
-
* tier-0 packages that may not import each other (`@ultimat3/schema` cannot import
|
|
18
|
-
* `@ultimat3/core`), so the guard is duck-typed on a well-known symbol instead.
|
|
19
|
-
*/
|
|
20
|
-
export const ULTIMATE_ERROR_BRAND: unique symbol = Symbol.for('ultimate.error');
|
|
21
|
-
|
|
22
22
|
export interface UltimateErrorInit {
|
|
23
23
|
/** `SCREAMING_SNAKE`, prefixed `X_`. Must exist in the code registry to get a title. */
|
|
24
24
|
readonly code: string;
|
|
@@ -48,6 +48,14 @@ export interface UltimateErrorInit {
|
|
|
48
48
|
readonly retry?: ErrorRetry | undefined;
|
|
49
49
|
/** The underlying thrown value, when this error wraps one. */
|
|
50
50
|
readonly sourceError?: unknown;
|
|
51
|
+
/**
|
|
52
|
+
* A title off the WIRE — a problem document's `title` — used only when this realm registered
|
|
53
|
+
* none for `code`. A browser rebuilds a server's refusal by code and may never have loaded the
|
|
54
|
+
* package that titles it; the registry still wins wherever it has an answer, so a server can
|
|
55
|
+
* never retitle a code this realm owns. Untrusted display text: the decoder caps it, and the
|
|
56
|
+
* constructor makes it one line like every other field.
|
|
57
|
+
*/
|
|
58
|
+
readonly remoteTitle?: string | undefined;
|
|
51
59
|
}
|
|
52
60
|
|
|
53
61
|
export interface UltimateErrorJSON {
|
|
@@ -111,7 +119,8 @@ export class UltimateError extends Error {
|
|
|
111
119
|
// Escaping at construction is the one place that covers all of them, and it is what #97 called
|
|
112
120
|
// the real answer. `singleLine` is idempotent, so a call site that already escaped is unharmed.
|
|
113
121
|
const code = singleLine(init.code);
|
|
114
|
-
const
|
|
122
|
+
const remote = init.remoteTitle !== undefined && !hasErrorCode(init.code);
|
|
123
|
+
const title = singleLine(remote ? (init.remoteTitle ?? described.title) : described.title);
|
|
115
124
|
const cause = singleLine(init.cause);
|
|
116
125
|
// `message` carries the cause because it is the ONLY field a runtime prints when an
|
|
117
126
|
// error escapes uncaught — a worker log, a CI transcript, a stack trace. A message of
|
|
@@ -35,7 +35,6 @@ export {
|
|
|
35
35
|
} from '../error-reporter-sentry';
|
|
36
36
|
export type { LogFields, Logger, LoggerOptions, LogLevel } from '../logger';
|
|
37
37
|
export {
|
|
38
|
-
createLogger,
|
|
39
38
|
isRedactedKey,
|
|
40
39
|
LOG_LEVELS,
|
|
41
40
|
logger,
|
|
@@ -43,6 +42,7 @@ export {
|
|
|
43
42
|
redactKeys,
|
|
44
43
|
setLoggerContextFields,
|
|
45
44
|
setLogStream,
|
|
45
|
+
structuredLogger,
|
|
46
46
|
} from '../logger';
|
|
47
47
|
export type {
|
|
48
48
|
Counter,
|
package/src/exports/secrets.ts
CHANGED
|
@@ -47,6 +47,7 @@ export {
|
|
|
47
47
|
SecretsPlaintextInvalidError,
|
|
48
48
|
SecretsTamperedError,
|
|
49
49
|
} from '../secrets-errors';
|
|
50
|
+
export { SecretsKeyAclError } from '../secrets-key-file';
|
|
50
51
|
export type {
|
|
51
52
|
MasterKeyRef,
|
|
52
53
|
MasterKeySource,
|
|
@@ -58,6 +59,7 @@ export {
|
|
|
58
59
|
installSecrets,
|
|
59
60
|
masterKeyIdOf,
|
|
60
61
|
masterKeyPath,
|
|
62
|
+
promoteStagedMasterKey,
|
|
61
63
|
readSecretsFile,
|
|
62
64
|
requireMasterKey,
|
|
63
65
|
SECRETS_FILE,
|
|
@@ -67,6 +69,7 @@ export {
|
|
|
67
69
|
secretsFileExists,
|
|
68
70
|
secretsPath,
|
|
69
71
|
stagedMasterKeyPath,
|
|
72
|
+
stageMasterKeyFile,
|
|
70
73
|
writeMasterKeyFile,
|
|
71
74
|
writeSecretsFile,
|
|
72
75
|
} from '../secrets-store';
|
package/src/finite-option.ts
CHANGED
|
@@ -48,7 +48,7 @@ export function finiteCount(
|
|
|
48
48
|
assert(
|
|
49
49
|
Number.isSafeInteger(value) && value >= min,
|
|
50
50
|
`${subject} ${option} is ${String(value)}, and it counts things: it must be a whole number of ${min === 1 ? 'at least 1' : '0 or more'}, or the bound it sets is not one`,
|
|
51
|
-
|
|
51
|
+
`${option}: Number.parseInt(raw, 10) # a whole number ${min === 1 ? '≥ 1' : '≥ 0'} for ${subject}; parse an env value and check it, since Number(process.env.…) is NaN when unset and Math.floor does not repair that`,
|
|
52
52
|
);
|
|
53
53
|
return value;
|
|
54
54
|
}
|
package/src/flight-gate.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
// memory fault and answers it minutes late.
|
|
9
9
|
|
|
10
10
|
import { UltimateError } from './errors';
|
|
11
|
+
import { finiteCount } from './finite-option';
|
|
11
12
|
|
|
12
13
|
export interface FlightGateLimits {
|
|
13
14
|
/** Work running at once. */
|
|
@@ -34,7 +35,12 @@ export interface FlightGateOptions {
|
|
|
34
35
|
}
|
|
35
36
|
|
|
36
37
|
export interface FlightGate {
|
|
37
|
-
|
|
38
|
+
/**
|
|
39
|
+
* `signal` makes a QUEUED wait cancellable: on abort the waiter leaves the queue and the call
|
|
40
|
+
* rejects with the abort's reason. Once a slot is handed over the work runs; the signal is then
|
|
41
|
+
* the work's own business.
|
|
42
|
+
*/
|
|
43
|
+
run<T>(work: () => Promise<T>, signal?: AbortSignal): Promise<T>;
|
|
38
44
|
/** Running right now. */
|
|
39
45
|
readonly active: number;
|
|
40
46
|
/** Waiting for a slot right now. A count that does not fall back to 0 is a leak. */
|
|
@@ -45,36 +51,57 @@ export interface FlightGate {
|
|
|
45
51
|
* A slot is HANDED OVER on release rather than released and re-acquired: decrementing first would
|
|
46
52
|
* let a caller arriving in the same tick past the ceiling while a waiter's continuation is still a
|
|
47
53
|
* queued microtask, which is how a "bounded" pool goes over its bound under exactly the load it
|
|
48
|
-
* exists for. `@ultimat3/auth`'s `
|
|
54
|
+
* exists for. `@ultimat3/auth`'s `boundedKdfGate` states the same rule; this is that function with
|
|
49
55
|
* the refusal made injectable.
|
|
50
56
|
*/
|
|
51
|
-
export function
|
|
52
|
-
limits: FlightGateLimits,
|
|
53
|
-
options?: FlightGateOptions,
|
|
54
|
-
): FlightGate {
|
|
57
|
+
export function flightGate(limits: FlightGateLimits, options?: FlightGateOptions): FlightGate {
|
|
55
58
|
const subject = options?.subject ?? 'in-flight work';
|
|
59
|
+
// Refused at CONSTRUCTION, because this pair wedges rather than fails: `active < NaN` and
|
|
60
|
+
// `waiters.length >= NaN` are both false, so every caller parks in a queue with no bound. Zero
|
|
61
|
+
// is a real value at both — "never wait" and, at the width, "refuse everything".
|
|
62
|
+
const maxConcurrent = finiteCount(
|
|
63
|
+
`flightGate (${subject})`,
|
|
64
|
+
'maxConcurrent',
|
|
65
|
+
limits.maxConcurrent,
|
|
66
|
+
);
|
|
67
|
+
const maxQueued = finiteCount(`flightGate (${subject})`, 'maxQueued', limits.maxQueued);
|
|
56
68
|
const waiters: Array<() => void> = [];
|
|
57
69
|
let active = 0;
|
|
58
70
|
|
|
59
71
|
const state = (): FlightGateState => ({
|
|
60
|
-
maxConcurrent
|
|
61
|
-
maxQueued
|
|
72
|
+
maxConcurrent,
|
|
73
|
+
maxQueued,
|
|
62
74
|
active,
|
|
63
75
|
queued: waiters.length,
|
|
64
76
|
subject,
|
|
65
77
|
});
|
|
66
78
|
|
|
67
|
-
const acquire = async (): Promise<void> => {
|
|
68
|
-
if (
|
|
79
|
+
const acquire = async (signal: AbortSignal | undefined): Promise<void> => {
|
|
80
|
+
if (signal?.aborted === true) throw signal.reason;
|
|
81
|
+
if (active < maxConcurrent) {
|
|
69
82
|
active += 1;
|
|
70
83
|
return;
|
|
71
84
|
}
|
|
72
|
-
|
|
85
|
+
// A width of zero has no slot to hand over, so a waiter would never be resumed: the queue is
|
|
86
|
+
// for work that WILL run, and here none will.
|
|
87
|
+
if (maxConcurrent === 0 || waiters.length >= maxQueued) {
|
|
73
88
|
const current = state();
|
|
74
89
|
throw options?.overflow?.(current) ?? gateOverloaded(current);
|
|
75
90
|
}
|
|
76
|
-
|
|
77
|
-
|
|
91
|
+
// A waiter that only stored its resolver could not be taken back: a superseded or abandoned
|
|
92
|
+
// call stayed pending and kept its queue place until a slot reached it. The abort removes it.
|
|
93
|
+
await new Promise<void>((resume, refuse) => {
|
|
94
|
+
const onAbort = (): void => {
|
|
95
|
+
const at = waiters.indexOf(waiter);
|
|
96
|
+
if (at !== -1) waiters.splice(at, 1);
|
|
97
|
+
refuse(signal?.reason);
|
|
98
|
+
};
|
|
99
|
+
const waiter = (): void => {
|
|
100
|
+
signal?.removeEventListener('abort', onAbort);
|
|
101
|
+
resume();
|
|
102
|
+
};
|
|
103
|
+
waiters.push(waiter);
|
|
104
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
78
105
|
});
|
|
79
106
|
};
|
|
80
107
|
|
|
@@ -91,8 +118,8 @@ export function createFlightGate(
|
|
|
91
118
|
get queued(): number {
|
|
92
119
|
return waiters.length;
|
|
93
120
|
},
|
|
94
|
-
async run<T>(work: () => Promise<T
|
|
95
|
-
await acquire();
|
|
121
|
+
async run<T>(work: () => Promise<T>, signal?: AbortSignal): Promise<T> {
|
|
122
|
+
await acquire(signal);
|
|
96
123
|
try {
|
|
97
124
|
return await work();
|
|
98
125
|
} finally {
|
|
@@ -112,7 +139,7 @@ export function gateOverloaded(state: FlightGateState): UltimateError {
|
|
|
112
139
|
return new UltimateError({
|
|
113
140
|
code: 'X_FLIGHT_GATE_OVERLOADED',
|
|
114
141
|
cause: `${state.active} of ${state.subject} are running at the ceiling of ${state.maxConcurrent} and ${state.queued} more are queued at the limit of ${state.maxQueued}`,
|
|
115
|
-
fix: 'retry after the Retry-After header, or widen the ceiling at the
|
|
142
|
+
fix: 'retry after the Retry-After header, or widen the ceiling at the flightGate({ maxConcurrent, maxQueued }) call site — only if the box has the capacity the extra slots buy',
|
|
116
143
|
meta: {
|
|
117
144
|
active: state.active,
|
|
118
145
|
queued: state.queued,
|
package/src/fnv1a.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// 32-bit FNV-1a: a BUCKET, never a key. Rollout buckets and factory seeds need a hash every process
|
|
2
|
+
// computes identically and synchronously; anything that decides who shares what is `fingerprint`
|
|
3
|
+
// (`canonical-json.ts`), because 2^32 values collide offline in seconds.
|
|
4
|
+
|
|
5
|
+
const FNV_OFFSET_BASIS = 0x811c_9dc5;
|
|
6
|
+
const FNV_PRIME = 0x0100_0193;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The published 32-bit FNV-1a over UTF-16 code units, unsigned. Pure and dependency-free, which is
|
|
10
|
+
* the property its two callers need: two nodes place one subject in one bucket without talking.
|
|
11
|
+
*/
|
|
12
|
+
export function fnv1a(text: string): number {
|
|
13
|
+
let hash = FNV_OFFSET_BASIS;
|
|
14
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
15
|
+
hash ^= text.charCodeAt(index);
|
|
16
|
+
hash = Math.imul(hash, FNV_PRIME);
|
|
17
|
+
}
|
|
18
|
+
return hash >>> 0;
|
|
19
|
+
}
|
package/src/generation-fence.ts
CHANGED
|
@@ -18,7 +18,7 @@ export interface GenerationFence {
|
|
|
18
18
|
* `subject` names what the generation counts, and it reaches the `cause:` — "the live window was
|
|
19
19
|
* superseded" is actionable where "generation 3 != 4" is a puzzle.
|
|
20
20
|
*/
|
|
21
|
-
export function
|
|
21
|
+
export function generationFence(subject: string): GenerationFence {
|
|
22
22
|
let current = 0;
|
|
23
23
|
return {
|
|
24
24
|
generation: (): number => current,
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// What `/healthz` and `/readyz` say, and to whom — ONE rule for every role's listener. Both answer
|
|
2
|
+
// outside every pipeline, so the body is a stranger's to read: everyone gets the verdict, and the
|
|
3
|
+
// build id, the in-flight count and the readiness check names go only to a listed peer.
|
|
4
|
+
|
|
5
|
+
import { classifyAddress } from './address-class';
|
|
6
|
+
import type { HealthReport } from './lifecycle';
|
|
7
|
+
import type { Role } from './roles';
|
|
8
|
+
|
|
9
|
+
/** The box itself: `kubectl exec`, a port-forward, a compose healthcheck, a sidecar scraper. */
|
|
10
|
+
export const DEFAULT_HEALTH_DETAIL_PEERS: readonly string[] = ['loopback'];
|
|
11
|
+
|
|
12
|
+
/** The verdict a stranger gets. An allow-list, so a field `HealthReport` gains is withheld by default. */
|
|
13
|
+
export interface PublicHealthBody {
|
|
14
|
+
readonly state: HealthReport['state'];
|
|
15
|
+
readonly ready: boolean;
|
|
16
|
+
readonly role: Role;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** The body for one caller: the whole report for a listed peer, the verdict for anyone else. */
|
|
20
|
+
export function healthBody(
|
|
21
|
+
report: HealthReport,
|
|
22
|
+
role: Role,
|
|
23
|
+
detailed: boolean,
|
|
24
|
+
): PublicHealthBody | (HealthReport & { readonly role: Role }) {
|
|
25
|
+
return detailed ? { ...report, role } : { state: report.state, ready: report.ready, role };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Whether `address` is one the list names: an entry is an address CLASS (`loopback`, `private`, …)
|
|
30
|
+
* or one exact IP literal. Pure, and total — it runs on an unauthenticated probe path, so a list
|
|
31
|
+
* that is not a list, an entry that is not a string and an address that is not a literal all
|
|
32
|
+
* answer `false` rather than throw: nothing can vouch for them, whatever the list says.
|
|
33
|
+
*/
|
|
34
|
+
export function healthPeerListed(peers: readonly string[], address: string | null): boolean {
|
|
35
|
+
if (address === null || !Array.isArray(peers)) return false;
|
|
36
|
+
const kind = classifyAddress(address);
|
|
37
|
+
if (kind === undefined) return false;
|
|
38
|
+
const literal = address.trim().toLowerCase();
|
|
39
|
+
return peers.some(
|
|
40
|
+
(entry: unknown) =>
|
|
41
|
+
typeof entry === 'string' && (entry === kind || entry.trim().toLowerCase() === literal),
|
|
42
|
+
);
|
|
43
|
+
}
|
package/src/host-rules.ts
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
// read. In core because two tier-5 packages drive a browser (`scraping`, and `cli`'s `x shot`) and
|
|
5
5
|
// neither may import the other; two copies of this rule would be two answers to "may it leave".
|
|
6
6
|
|
|
7
|
+
import { classifyAddress } from './address-class';
|
|
8
|
+
|
|
7
9
|
export type HostRule = string;
|
|
8
10
|
|
|
9
11
|
/** The one spelling that means "every host", written out so it is visible in review. */
|
|
@@ -51,6 +53,31 @@ export function hostMatches(host: string, rule: HostRule): boolean {
|
|
|
51
53
|
return normalised === cleaned;
|
|
52
54
|
}
|
|
53
55
|
|
|
56
|
+
/** A rule that names a CLASS of hosts rather than one — the two spellings `hostMatches` widens. */
|
|
57
|
+
const isWildcard = (rule: HostRule): boolean => {
|
|
58
|
+
const cleaned = rule.trim();
|
|
59
|
+
return cleaned === ANY_HOST || cleaned.startsWith('*.');
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The address-class FLOOR. A wildcard means "any site", and an address literal inside the network
|
|
64
|
+
* — loopback, RFC 1918, link-local, the metadata endpoint — is not a site: it is the request this
|
|
65
|
+
* module's header names. So a wildcard never admits one, and the opt-out is NAMING it: an exact
|
|
66
|
+
* rule (`'127.0.0.1'`, `'[::1]'`) is a line a reviewer can see, which `'*'` is not.
|
|
67
|
+
*
|
|
68
|
+
* What this cannot do is see through a NAME. `allowHosts: ['*']` still admits a hostname that
|
|
69
|
+
* resolves inward, because this function is synchronous and has no resolver — pinning the
|
|
70
|
+
* resolved address belongs to the driver that opens the connection, as `@ultimat3/jobs`'
|
|
71
|
+
* `webhook-target.ts` does for a webhook. The URL parser has already folded the numeric
|
|
72
|
+
* spellings (`2130706433`, `0x7f.1`) to dotted form, so they are classified as what they are.
|
|
73
|
+
*/
|
|
74
|
+
function admits(host: string, rule: HostRule): boolean {
|
|
75
|
+
if (!hostMatches(host, rule)) return false;
|
|
76
|
+
if (!isWildcard(rule)) return true;
|
|
77
|
+
const kind = classifyAddress(host);
|
|
78
|
+
return kind === undefined || kind === 'public';
|
|
79
|
+
}
|
|
80
|
+
|
|
54
81
|
/**
|
|
55
82
|
* Fails CLOSED: a URL that cannot be parsed is refused. A driver handed a malformed request has
|
|
56
83
|
* no way to know where it would have gone, and "we could not tell, so we let it through" is the
|
|
@@ -67,5 +94,5 @@ export function hostDecision(url: string, allowHosts: readonly HostRule[]): Host
|
|
|
67
94
|
return { allowed: false, host: '' };
|
|
68
95
|
}
|
|
69
96
|
if (host === '') return { allowed: false, host };
|
|
70
|
-
return { allowed: allowHosts.some((rule) =>
|
|
97
|
+
return { allowed: allowHosts.some((rule) => admits(host, rule)), host };
|
|
71
98
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// The one HTML character table: how an untrusted value becomes inert text or attribute content.
|
|
2
|
+
// At tier 0 because every package that writes markup — http, mail, render, seo, the dashboards —
|
|
3
|
+
// can reach it, and a second table is one character away from a hole (`bun run flight-copies`).
|
|
4
|
+
|
|
5
|
+
/** A `Map`, so a lookup never reaches `Object.prototype` (`bun run proto-index`). */
|
|
6
|
+
const HTML_ESCAPES: ReadonlyMap<string, string> = new Map([
|
|
7
|
+
['&', '&'],
|
|
8
|
+
['<', '<'],
|
|
9
|
+
['>', '>'],
|
|
10
|
+
['"', '"'],
|
|
11
|
+
["'", '''],
|
|
12
|
+
]);
|
|
13
|
+
|
|
14
|
+
const HTML_SPECIAL = /[&<>"']/g;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Text content AND attribute values, in any quoting — one set for both, deliberately. A text-only
|
|
18
|
+
* subset is correct exactly until someone uses it for an attribute, and a no-`'` set until someone
|
|
19
|
+
* writes a single-quoted one. `'` rather than `'`: it is a numeric reference, so it means
|
|
20
|
+
* the same thing in HTML 4, HTML 5 and XML. One pass, so `&` is never escaped twice.
|
|
21
|
+
*/
|
|
22
|
+
export function escapeHtml(value: string): string {
|
|
23
|
+
return value.replace(HTML_SPECIAL, (char) => HTML_ESCAPES.get(char) ?? char);
|
|
24
|
+
}
|
package/src/ids.ts
CHANGED
|
@@ -33,7 +33,7 @@ function randomBytes(length: number): Uint8Array {
|
|
|
33
33
|
/**
|
|
34
34
|
* A full 10 bits, from both bytes. Reading `bytes[0]` alone masked an 8-bit value with a 10-bit
|
|
35
35
|
* mask, so the seed only ever reached 255 while `COUNTER_SEED_MASK` declared 1023 — the constant
|
|
36
|
-
* and the code disagreed, and the second byte was allocated on every `
|
|
36
|
+
* and the code disagreed, and the second byte was allocated on every `uuidV7()` for nothing.
|
|
37
37
|
*/
|
|
38
38
|
function seedCounter(): number {
|
|
39
39
|
const bytes = randomBytes(2);
|
|
@@ -43,7 +43,7 @@ function seedCounter(): number {
|
|
|
43
43
|
/**
|
|
44
44
|
* `new Uint8Array(NaN)` is a zero-length array, not a throw, so an unscreened length made this
|
|
45
45
|
* answer `''` — an id that is no id, minted silently. `min: 1`: zero bytes of randomness is the
|
|
46
|
-
* same empty string, and every caller here (`
|
|
46
|
+
* same empty string, and every caller here (`uuidV7`, `traceId`, `spanId`) wants a width.
|
|
47
47
|
*/
|
|
48
48
|
export function randomHex(byteLength: number): string {
|
|
49
49
|
const bytes = randomBytes(finiteCount('randomHex', 'byteLength', byteLength, 1));
|
|
@@ -60,7 +60,7 @@ export function randomHex(byteLength: number): string {
|
|
|
60
60
|
* Strictly increasing lexicographically even within the same millisecond, and never goes
|
|
61
61
|
* backwards when the wall clock does.
|
|
62
62
|
*/
|
|
63
|
-
export function
|
|
63
|
+
export function uuidV7(clock: Clock = systemClock): string {
|
|
64
64
|
let epochMs = clock.now().getTime();
|
|
65
65
|
if (epochMs < lastEpochMs) epochMs = lastEpochMs;
|
|
66
66
|
|
|
@@ -108,7 +108,7 @@ export function uuidTimestamp(id: string): Date {
|
|
|
108
108
|
// problem document, and the strings that arrive here wrong are session tokens and API keys
|
|
109
109
|
// as often as they are typos. The expected shape is the half that helps the reader.
|
|
110
110
|
cause: `expected a UUIDv7 (${UUID_SHAPE}), received ${describeValue(id)}`,
|
|
111
|
-
fix: 'generate ids with
|
|
111
|
+
fix: 'generate ids with uuidV7() from @ultimat3/core',
|
|
112
112
|
meta: { received: describeValue(id) },
|
|
113
113
|
});
|
|
114
114
|
}
|
|
@@ -131,7 +131,7 @@ export function nanoid(length = 21): string {
|
|
|
131
131
|
|
|
132
132
|
/** `typedId<'post'>()` — a UUIDv7 branded so it cannot be passed where a user id is wanted. */
|
|
133
133
|
export function typedId<K extends string>(clock: Clock = systemClock): Id<K> {
|
|
134
|
-
return
|
|
134
|
+
return uuidV7(clock) as Id<K>;
|
|
135
135
|
}
|
|
136
136
|
|
|
137
137
|
/** Validate an untrusted string into a branded id. Throws `X_ID_INVALID`. */
|
|
@@ -166,9 +166,9 @@ const ALL_ZERO = /^0+$/;
|
|
|
166
166
|
* The ONE definition of "is this a W3C trace id" — `traceparent` parsing, and any layer that
|
|
167
167
|
* accepts an id from outside, ask here rather than carrying a second regex.
|
|
168
168
|
*
|
|
169
|
-
* A dashed UUID is the failure this predicate exists to name: `
|
|
169
|
+
* A dashed UUID is the failure this predicate exists to name: `uuidV7()` produces 36 characters with
|
|
170
170
|
* hyphens, every OTLP collector rejects it, and nothing downstream said so — the trace simply
|
|
171
|
-
* never appeared. Mint trace ids with `traceId()`, never `
|
|
171
|
+
* never appeared. Mint trace ids with `traceId()`, never `uuidV7()`. All-zero is invalid per the
|
|
172
172
|
* spec: it is the wire's spelling of "no trace", not a trace whose id happens to be zero.
|
|
173
173
|
*/
|
|
174
174
|
export function isTraceId(value: unknown): boolean {
|
package/src/image/canvas.ts
CHANGED
|
@@ -5,7 +5,13 @@
|
|
|
5
5
|
|
|
6
6
|
import { parseColor } from './color';
|
|
7
7
|
import { imageUnsupported } from './errors';
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
assertPixelBudget,
|
|
10
|
+
blankRaster,
|
|
11
|
+
type ImageRegion,
|
|
12
|
+
type ImageSize,
|
|
13
|
+
type Raster,
|
|
14
|
+
} from './raster';
|
|
9
15
|
|
|
10
16
|
export type ImageFit = 'cover' | 'contain';
|
|
11
17
|
|
|
@@ -69,7 +75,15 @@ export interface Layout {
|
|
|
69
75
|
readonly pad: number;
|
|
70
76
|
/** The area inside the padding the artwork may occupy. */
|
|
71
77
|
readonly inner: ImageSize;
|
|
72
|
-
/**
|
|
78
|
+
/**
|
|
79
|
+
* The part of the source that is resampled, when it is not all of it: an UPSCALING `cover`
|
|
80
|
+
* whose aspect disagrees with the box. Resampling first would draw the whole source past the
|
|
81
|
+
* box only to clip it away — a 1x200 strip at 1000x200000 for a 1000x1000 box — so the visible
|
|
82
|
+
* region is cut first and `drawn` is the inner area. A downscaling `cover` resamples first: its
|
|
83
|
+
* `drawn` is never larger than the source, and no crop then means no full-size decode.
|
|
84
|
+
*/
|
|
85
|
+
readonly crop: ImageRegion | undefined;
|
|
86
|
+
/** What the source (or `crop`) is resampled to before it is placed: within `max(source, box)`. */
|
|
73
87
|
readonly drawn: ImageSize;
|
|
74
88
|
/** Parsed once, here, so an unspellable colour is refused before any pixel is produced. */
|
|
75
89
|
readonly background: readonly [number, number, number, number];
|
|
@@ -97,7 +111,13 @@ export function layOut(source: ImageSize, spec: ResizeSpec): Layout {
|
|
|
97
111
|
{ padding, pad, width: box.width, height: box.height },
|
|
98
112
|
);
|
|
99
113
|
}
|
|
100
|
-
const
|
|
114
|
+
const fit = spec.fit ?? 'contain';
|
|
115
|
+
const scaled = scaledToFit(source, inner, fit);
|
|
116
|
+
const crop = fit === 'cover' ? visibleRegion(source, inner, scaled) : undefined;
|
|
117
|
+
// The crop is whole source pixels covering the exact window, so its resample can overflow the
|
|
118
|
+
// inner area by under one scaled pixel a side; `composeOnto` centre-clips that, as it clips an
|
|
119
|
+
// uncropped cover, so both orders centre the same fractional window.
|
|
120
|
+
const drawn = crop === undefined ? scaled : drawnOf(crop, source, inner);
|
|
101
121
|
// Parsed even when the fast path will not use it: 'chartreuse' must be refused whether or not
|
|
102
122
|
// the geometry happens to hide the colour, or the rejection depends on the source's dimensions.
|
|
103
123
|
const background = parseColor(spec.background ?? 'transparent');
|
|
@@ -106,7 +126,58 @@ export function layOut(source: ImageSize, spec: ResizeSpec): Layout {
|
|
|
106
126
|
// the RGBA round trip entirely.
|
|
107
127
|
const needsCanvas =
|
|
108
128
|
drawn.width !== box.width || drawn.height !== box.height || background[3] !== 0;
|
|
109
|
-
return { box, pad, inner, drawn, background, needsCanvas };
|
|
129
|
+
return { box, pad, inner, crop, drawn, background, needsCanvas };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* The source region a `cover` shows, or `undefined` when resampling the whole source first costs
|
|
134
|
+
* no more than the source itself (`scaled` within it) or nothing overflows the inner area.
|
|
135
|
+
* Centred as `composeOnto` centres an overflowing draw, so both orders crop the same window.
|
|
136
|
+
*/
|
|
137
|
+
function visibleRegion(
|
|
138
|
+
source: ImageSize,
|
|
139
|
+
inner: ImageSize,
|
|
140
|
+
scaled: ImageSize,
|
|
141
|
+
): ImageRegion | undefined {
|
|
142
|
+
const overflows = scaled.width > inner.width || scaled.height > inner.height;
|
|
143
|
+
if (!overflows || scaled.width * scaled.height <= source.width * source.height) return undefined;
|
|
144
|
+
const scale = coverScale(source, inner);
|
|
145
|
+
// The exact window is fractional. The whole pixels that COVER it (floor to ceil) centre exactly
|
|
146
|
+
// once `composeOnto` clips the overflow; a ROUNDED window shifts a .5 offset half a source pixel
|
|
147
|
+
// off centre, which shows on a small source upscaled a lot. Covering costs up to one scaled pixel
|
|
148
|
+
// a side, so it is taken only while that stays under twice the inner area — a degenerate strip
|
|
149
|
+
// (1x200 into a square), where half a source pixel is invisible, keeps the rounded window.
|
|
150
|
+
const span = (total: number, wanted: number, cover: boolean): { start: number; size: number } => {
|
|
151
|
+
const exact = Math.min(total, wanted / scale);
|
|
152
|
+
const from = (total - exact) / 2;
|
|
153
|
+
if (!cover) {
|
|
154
|
+
const size = Math.min(total, Math.max(1, Math.round(exact)));
|
|
155
|
+
return { start: Math.round((total - size) / 2), size };
|
|
156
|
+
}
|
|
157
|
+
const start = Math.max(0, Math.floor(from));
|
|
158
|
+
return { start, size: Math.min(total, Math.ceil(from + exact)) - start };
|
|
159
|
+
};
|
|
160
|
+
const across = span(source.width, inner.width, true);
|
|
161
|
+
const down = span(source.height, inner.height, true);
|
|
162
|
+
const covered = { x: across.start, y: down.start, width: across.size, height: down.size };
|
|
163
|
+
const cost = drawnOf(covered, source, inner);
|
|
164
|
+
if (cost.width * cost.height < 2 * inner.width * inner.height) return covered;
|
|
165
|
+
const a = span(source.width, inner.width, false);
|
|
166
|
+
const d = span(source.height, inner.height, false);
|
|
167
|
+
return { x: a.start, y: d.start, width: a.size, height: d.size };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function coverScale(source: ImageSize, inner: ImageSize): number {
|
|
171
|
+
return Math.max(inner.width / source.width, inner.height / source.height);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** The cropped region at the cover scale: the inner area plus under one scaled pixel a side. */
|
|
175
|
+
function drawnOf(crop: ImageRegion, source: ImageSize, inner: ImageSize): ImageSize {
|
|
176
|
+
const scale = coverScale(source, inner);
|
|
177
|
+
return {
|
|
178
|
+
width: Math.max(inner.width, Math.round(crop.width * scale)),
|
|
179
|
+
height: Math.max(inner.height, Math.round(crop.height * scale)),
|
|
180
|
+
};
|
|
110
181
|
}
|
|
111
182
|
|
|
112
183
|
function fill(canvas: Raster, color: readonly [number, number, number, number]): void {
|
|
@@ -150,7 +221,7 @@ function blend(dst: Uint8ClampedArray, d: number, s: Uint8ClampedArray, p: numbe
|
|
|
150
221
|
*/
|
|
151
222
|
export function composeOnto(art: Raster, layout: Layout): Raster {
|
|
152
223
|
const { box, pad, inner } = layout;
|
|
153
|
-
const canvas =
|
|
224
|
+
const canvas = blankRaster(box.width, box.height, 'resize');
|
|
154
225
|
fill(canvas, layout.background);
|
|
155
226
|
const ox = pad + Math.round((inner.width - art.width) / 2);
|
|
156
227
|
const oy = pad + Math.round((inner.height - art.height) / 2);
|
package/src/image/errors.ts
CHANGED
|
@@ -55,7 +55,9 @@ export const imageTooLarge = (
|
|
|
55
55
|
): ImageTooLargeError =>
|
|
56
56
|
new ImageTooLargeError(
|
|
57
57
|
cause,
|
|
58
|
-
|
|
58
|
+
// No "or lift the ceiling": `MAX_IMAGE_PIXELS` is a constant, so a fix naming it as a knob
|
|
59
|
+
// sent a reader looking for a setting that does not exist.
|
|
60
|
+
'downscale the source below the 64-megapixel ceiling before it reaches the pipeline, or route it through an ImageTransformDriver (a CDN or an external encoder) — MAX_IMAGE_PIXELS is fixed, not a setting',
|
|
59
61
|
meta,
|
|
60
62
|
);
|
|
61
63
|
|
package/src/image/pipeline.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { imageFromBunError, imageUnsupported } from './errors';
|
|
|
9
9
|
import { unshared } from './png-bytes';
|
|
10
10
|
import { decodeImage, encodeImage } from './png-pixels';
|
|
11
11
|
import { IMAGE_MIME_TYPES, type ImageFormat } from './probe';
|
|
12
|
-
import { MAX_IMAGE_PIXELS } from './raster';
|
|
12
|
+
import { cropRaster, type ImageRegion, MAX_IMAGE_PIXELS } from './raster';
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* What the pipeline can produce, on every platform, byte for byte. `Bun.Image` also reaches
|
|
@@ -97,6 +97,15 @@ async function run<T>(doing: string, work: () => Promise<T>): Promise<T> {
|
|
|
97
97
|
}
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
/**
|
|
101
|
+
* `region` of the source as PNG. `Bun.Image` has no crop, so the source is decoded to a raster
|
|
102
|
+
* (within the same `MAX_IMAGE_PIXELS` its header was held to) and the region copied out of it.
|
|
103
|
+
*/
|
|
104
|
+
async function croppedPng(bytes: Uint8Array, region: ImageRegion): Promise<Uint8Array> {
|
|
105
|
+
const whole = await run('decoding the image', () => bunImage(bytes).png().bytes());
|
|
106
|
+
return encodeImage(cropRaster(decodeImage(whole), region));
|
|
107
|
+
}
|
|
108
|
+
|
|
100
109
|
/** The whole pipeline in one call: decode, resize, compose, encode. */
|
|
101
110
|
export async function transformImageBytes(
|
|
102
111
|
bytes: Uint8Array,
|
|
@@ -112,12 +121,15 @@ export async function transformImageBytes(
|
|
|
112
121
|
const source = await run('reading the image header', () => bunImage(bytes).metadata());
|
|
113
122
|
const output: EncodableFormat = format ?? (canEncode(source.format) ? source.format : 'png');
|
|
114
123
|
const layout = layOut(source, spec);
|
|
115
|
-
const { box, drawn } = layout;
|
|
124
|
+
const { box, drawn, crop } = layout;
|
|
125
|
+
// The cropped region stands in for the source from here on: what is resampled is what shows.
|
|
126
|
+
const input = crop === undefined ? bytes : await croppedPng(bytes, crop);
|
|
127
|
+
const inputSize = crop ?? source;
|
|
116
128
|
|
|
117
129
|
if (!layout.needsCanvas) {
|
|
118
130
|
return run('transforming the image', () => {
|
|
119
|
-
const image = bunImage(
|
|
120
|
-
if (box.width !==
|
|
131
|
+
const image = bunImage(input);
|
|
132
|
+
if (box.width !== inputSize.width || box.height !== inputSize.height) {
|
|
121
133
|
image.resize(box.width, box.height, { fit: 'fill' });
|
|
122
134
|
}
|
|
123
135
|
return withFormat(image, output, quality).bytes();
|
|
@@ -130,7 +142,7 @@ export async function transformImageBytes(
|
|
|
130
142
|
// is 1.2-1.8x libspng's bytes on a real icon (measured), and one writer for everything this
|
|
131
143
|
// function returns is also what makes "same input, same bytes" rest on the static codecs alone.
|
|
132
144
|
const art = await run('resampling the image', () =>
|
|
133
|
-
bunImage(
|
|
145
|
+
bunImage(input).resize(drawn.width, drawn.height, { fit: 'fill' }).png().bytes(),
|
|
134
146
|
);
|
|
135
147
|
const composed = encodeImage(composeOnto(decodeImage(art), layout));
|
|
136
148
|
return run('encoding the image', () => withFormat(bunImage(composed), output, quality).bytes());
|