@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.
Files changed (98) hide show
  1. package/CLAUDE.md +29 -26
  2. package/README.md +98 -30
  3. package/package.json +4 -7
  4. package/src/actor.ts +9 -0
  5. package/src/address-class.ts +40 -4
  6. package/src/assert.ts +9 -5
  7. package/src/audit.ts +144 -0
  8. package/src/aws-sigv4.ts +275 -0
  9. package/src/backoff.ts +16 -0
  10. package/src/bunfs.ts +17 -0
  11. package/src/client-dispatch.ts +24 -3
  12. package/src/client-flight.ts +68 -13
  13. package/src/client-problem.ts +62 -6
  14. package/src/client-retry-after.ts +47 -0
  15. package/src/client-transport.ts +3 -1
  16. package/src/client-wire.ts +27 -3
  17. package/src/config-ai.ts +32 -0
  18. package/src/config-defaults.ts +53 -0
  19. package/src/config-fixes.ts +0 -10
  20. package/src/config-health.ts +9 -2
  21. package/src/config-jobs.ts +51 -0
  22. package/src/config-keys.ts +170 -0
  23. package/src/config-mail.ts +73 -0
  24. package/src/config-merge.ts +9 -1
  25. package/src/config-navigation.ts +1 -25
  26. package/src/config-pwa.ts +42 -5
  27. package/src/config-removed.ts +131 -0
  28. package/src/config-shape.ts +79 -0
  29. package/src/config-site.ts +14 -3
  30. package/src/config.ts +141 -173
  31. package/src/context.ts +29 -13
  32. package/src/cookie.ts +299 -0
  33. package/src/core-error-codes.ts +2 -0
  34. package/src/cursor-page.ts +41 -0
  35. package/src/cursor.ts +26 -5
  36. package/src/decimal-order.ts +5 -4
  37. package/src/deprecation.ts +77 -0
  38. package/src/dev-secrets.ts +18 -7
  39. package/src/drain-deadline.ts +43 -0
  40. package/src/env-example.ts +9 -29
  41. package/src/error-reporter-sentry.ts +7 -3
  42. package/src/errors.ts +18 -9
  43. package/src/exports/error-contract.ts +0 -1
  44. package/src/exports/observability.ts +1 -1
  45. package/src/exports/secrets.ts +3 -0
  46. package/src/finite-option.ts +1 -1
  47. package/src/flight-gate.ts +43 -16
  48. package/src/fnv1a.ts +19 -0
  49. package/src/generation-fence.ts +1 -1
  50. package/src/health-disclosure.ts +43 -0
  51. package/src/host-rules.ts +28 -1
  52. package/src/html-escape.ts +24 -0
  53. package/src/ids.ts +7 -7
  54. package/src/image/canvas.ts +76 -5
  55. package/src/image/errors.ts +3 -1
  56. package/src/image/pipeline.ts +17 -5
  57. package/src/image/png-pixels.ts +29 -6
  58. package/src/image/probe.ts +7 -2
  59. package/src/image/raster.ts +27 -2
  60. package/src/index.ts +99 -33
  61. package/src/iso-date.ts +1 -1
  62. package/src/lifecycle-errors.ts +1 -1
  63. package/src/lifecycle-readiness.ts +60 -2
  64. package/src/lifecycle-signals.ts +27 -2
  65. package/src/lifecycle-types.ts +96 -0
  66. package/src/lifecycle.ts +44 -133
  67. package/src/locale-direction.ts +1 -1
  68. package/src/logger.ts +92 -16
  69. package/src/mcp-exposure.ts +70 -8
  70. package/src/measurement-actor.ts +16 -1
  71. package/src/metric-errors.ts +32 -0
  72. package/src/metric-registry.ts +151 -0
  73. package/src/metric-series.ts +94 -0
  74. package/src/metrics.ts +9 -255
  75. package/src/nearest-name.ts +11 -2
  76. package/src/otlp-metric-exporter.ts +1 -1
  77. package/src/otlp-span-exporter.ts +1 -1
  78. package/src/otlp.ts +44 -13
  79. package/src/page.ts +4 -2
  80. package/src/pg-executor.ts +15 -0
  81. package/src/public-cause.ts +37 -0
  82. package/src/registrar.ts +22 -4
  83. package/src/retry.ts +40 -7
  84. package/src/route-rank.ts +36 -0
  85. package/src/same-origin.ts +1 -1
  86. package/src/sampler.ts +6 -2
  87. package/src/secrets-errors.ts +14 -3
  88. package/src/secrets-key-file.ts +139 -0
  89. package/src/secrets-store.ts +32 -16
  90. package/src/service.ts +5 -5
  91. package/src/single-flight.ts +1 -1
  92. package/src/source-mask.ts +14 -8
  93. package/src/store-mode.ts +23 -0
  94. package/src/telemetry.ts +1 -1
  95. package/src/theme-storage.ts +12 -0
  96. package/src/type-pins.ts +51 -1
  97. package/src/image/fixtures.ts +0 -263
  98. 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
- ...(report.meta ?? {}),
119
- ...(report.scope.extra ?? {}),
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
- import { describeErrorCode } from './error-codes';
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 title = singleLine(described.title);
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
@@ -63,7 +63,6 @@ export {
63
63
  NotImplementedError,
64
64
  notImplemented,
65
65
  toUltimateError,
66
- ULTIMATE_ERROR_BRAND,
67
66
  UltimateError,
68
67
  } from '../errors';
69
68
  export { SCHEMA_ERROR_CODE_TITLES } from '../schema-error-codes';
@@ -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,
@@ -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';
@@ -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
- `pass a whole ${option} to ${subject} — and parse an environment value before you pass it, because Number(process.env.…) is NaN when the variable is unset and Math.floor does not repair that`,
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
  }
@@ -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
- run<T>(work: () => Promise<T>): Promise<T>;
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 `createKdfGate` states the same rule; this is that function with
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 createFlightGate(
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: limits.maxConcurrent,
61
- maxQueued: limits.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 (active < limits.maxConcurrent) {
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
- if (waiters.length >= limits.maxQueued) {
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
- await new Promise<void>((resume) => {
77
- waiters.push(resume);
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>): 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 createFlightGate({ maxConcurrent, maxQueued }) call site — only if the box has the capacity the extra slots buy',
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
+ }
@@ -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 createFence(subject: string): GenerationFence {
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) => hostMatches(host, rule)), host };
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
+ ['&', '&amp;'],
8
+ ['<', '&lt;'],
9
+ ['>', '&gt;'],
10
+ ['"', '&quot;'],
11
+ ["'", '&#39;'],
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. `&#39;` rather than `&apos;`: 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 `uuid()` for nothing.
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 (`uuid`, `traceId`, `spanId`) wants a width.
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 uuid(clock: Clock = systemClock): string {
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 uuid() from @ultimat3/core',
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 uuid(clock) as Id<K>;
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: `uuid()` produces 36 characters with
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 `uuid()`. All-zero is invalid per the
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 {
@@ -5,7 +5,13 @@
5
5
 
6
6
  import { parseColor } from './color';
7
7
  import { imageUnsupported } from './errors';
8
- import { assertPixelBudget, createRaster, type ImageSize, type Raster } from './raster';
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
- /** What the source is resampled to before it is placed. */
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 drawn = scaledToFit(source, inner, spec.fit ?? 'contain');
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 = createRaster(box.width, box.height, 'resize');
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);
@@ -55,7 +55,9 @@ export const imageTooLarge = (
55
55
  ): ImageTooLargeError =>
56
56
  new ImageTooLargeError(
57
57
  cause,
58
- 'downscale the source before it reaches the pipeline, or raise MAX_IMAGE_PIXELS deliberately',
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
 
@@ -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(bytes);
120
- if (box.width !== source.width || box.height !== source.height) {
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(bytes).resize(drawn.width, drawn.height, { fit: 'fill' }).png().bytes(),
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());