@ultimat3/core 22.15.0 → 24.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 (50) hide show
  1. package/CLAUDE.md +23 -2
  2. package/README.md +93 -4
  3. package/package.json +2 -2
  4. package/src/client-paths.ts +26 -6
  5. package/src/config-defaults.ts +46 -0
  6. package/src/config-merge.ts +8 -0
  7. package/src/config-shape.ts +112 -0
  8. package/src/config-site.ts +14 -3
  9. package/src/config.ts +74 -83
  10. package/src/context.ts +13 -1
  11. package/src/cookie.ts +35 -0
  12. package/src/core-error-codes.ts +5 -0
  13. package/src/cursor.ts +4 -1
  14. package/src/decimal-order.ts +5 -4
  15. package/src/dev-secrets.ts +1 -1
  16. package/src/error-render.ts +4 -2
  17. package/src/error-reporter-sentry.ts +7 -3
  18. package/src/error-retry.ts +8 -0
  19. package/src/flight-gate.ts +16 -4
  20. package/src/fnv1a.ts +19 -0
  21. package/src/health-disclosure.ts +43 -0
  22. package/src/host-rules.ts +28 -1
  23. package/src/html-escape.ts +24 -0
  24. package/src/image/errors.ts +3 -1
  25. package/src/image/png-pixels.ts +29 -6
  26. package/src/image/probe.ts +7 -2
  27. package/src/image/raster.ts +3 -1
  28. package/src/index.ts +32 -0
  29. package/src/logger.ts +103 -10
  30. package/src/nearest-name.ts +11 -2
  31. package/src/otlp-metric-exporter.ts +1 -1
  32. package/src/otlp-span-exporter.ts +1 -1
  33. package/src/otlp.ts +44 -13
  34. package/src/page-meta.ts +7 -0
  35. package/src/page.ts +1 -0
  36. package/src/pg-executor.ts +15 -0
  37. package/src/process-metrics.ts +206 -0
  38. package/src/public-cause.ts +37 -0
  39. package/src/registrar.ts +21 -4
  40. package/src/retry.ts +15 -2
  41. package/src/route-rank.ts +36 -0
  42. package/src/same-origin.ts +1 -1
  43. package/src/sampler.ts +6 -2
  44. package/src/seal-errors.ts +76 -0
  45. package/src/seal-keys.ts +121 -0
  46. package/src/seal.ts +259 -0
  47. package/src/secrets-errors.ts +33 -1
  48. package/src/secrets.ts +21 -11
  49. package/src/source-mask.ts +14 -8
  50. package/src/store-mode.ts +23 -0
package/src/config.ts CHANGED
@@ -8,6 +8,7 @@ import { CURRENCY_CODE_PATTERN } from '@ultimat3/schema';
8
8
  // drift into two vocabularies with no map between them (issue #293).
9
9
  import { CACHE_TIERS, type CacheTierName } from './cache-vocabulary';
10
10
  import { countIssue } from './config-count';
11
+ import { configDefaults } from './config-defaults';
11
12
  import { BASE_FIX, CACHE_TIER_FIX, TIMEZONE_FIX } from './config-fixes';
12
13
  import type { DrainConfig, HealthConfig } from './config-health';
13
14
  import { readinessModeIssue } from './config-health';
@@ -18,15 +19,24 @@ import type { NavigationConfig, NavigationSectionInput } from './config-navigati
18
19
  import { mergeNavigation, navigationIssues } from './config-navigation';
19
20
  import type { PwaConfig, PwaOfflineConfig } from './config-pwa';
20
21
  import { PWA_FIX, pwaIssues } from './config-pwa';
22
+ import {
23
+ booleanIssue,
24
+ localeIssues,
25
+ nameListIssues,
26
+ oneOfIssue,
27
+ routePathIssue,
28
+ shapeIssues,
29
+ } from './config-shape';
21
30
  import type { SeoConfig, SiteConfig, SiteSectionsInput } from './config-site';
22
31
  import { mergeSite, siteIssues } from './config-site';
23
32
  import { describeValue } from './error-render';
24
33
  import { ConfigInvalidError } from './errors';
25
- import { defaultReadinessGraceMs, readinessGraceIssue } from './lifecycle-grace';
34
+ import { readinessGraceIssue } from './lifecycle-grace';
26
35
  import { ROLES, type Role } from './roles';
27
36
  import { isIanaZoneName } from './time-zone-name';
28
37
 
29
- export type ThemeMode = 'light' | 'dark' | 'system';
38
+ export const THEME_MODES = ['light', 'dark', 'system'] as const;
39
+ export type ThemeMode = (typeof THEME_MODES)[number];
30
40
  /**
31
41
  * The buses `@ultimat3/realtime`'s `selectTransport` builds, and nothing else. `'redis'` was in
32
42
  * this union until 22.0.0 with no Redis transport anywhere: it booted whatever `NATS_URL` chose.
@@ -99,8 +109,10 @@ export const INBOX_RETENTION_KEYS = ['inboxReadRetentionMs', 'inboxUnreadRetenti
99
109
  * 3 applied to configuration: a value that produces neither a build error nor a runtime effect is
100
110
  * worse than no field, because an SRE sets `poolSize: 3`, redeploys, and nothing changes.
101
111
  */
112
+ const DATABASE_DRIVERS = ['postgres'] as const;
113
+
102
114
  export interface DatabaseConfig {
103
- readonly driver: 'postgres';
115
+ readonly driver: (typeof DATABASE_DRIVERS)[number];
104
116
  readonly ssl: boolean;
105
117
  }
106
118
 
@@ -125,6 +137,8 @@ export interface CacheConfig {
125
137
  readonly tiers: readonly CacheTierName[];
126
138
  }
127
139
 
140
+ const JOB_BACKOFFS = ['exponential', 'fixed'] as const;
141
+
128
142
  export interface JobsConfig {
129
143
  /**
130
144
  * No `driver`. It accepted `'postgres' | 'redis' | 'nats'`, was read by NOTHING, and boot always
@@ -140,7 +154,7 @@ export interface JobsConfig {
140
154
  readonly queues: readonly string[];
141
155
  readonly concurrency: number;
142
156
  readonly maxAttempts: number;
143
- readonly backoff: 'exponential' | 'fixed';
157
+ readonly backoff: (typeof JOB_BACKOFFS)[number];
144
158
  readonly visibilityTimeoutMs: number;
145
159
  }
146
160
 
@@ -255,52 +269,6 @@ const NAME_RE = /^[a-z][a-z0-9-]{1,63}$/;
255
269
  */
256
270
  const CURRENCY_RE = new RegExp(CURRENCY_CODE_PATTERN);
257
271
 
258
- function isLocale(value: string): boolean {
259
- try {
260
- return Intl.getCanonicalLocales(value).length === 1;
261
- } catch {
262
- return false;
263
- }
264
- }
265
-
266
- type Sectioned = 'name' | 'site' | 'seo' | 'navigation' | 'islands';
267
- function defaults(name: string): Omit<AppConfig, Sectioned> {
268
- return {
269
- locales: ['en'],
270
- defaultLocale: 'en',
271
- defaultTimeZone: 'UTC',
272
- defaultCurrency: 'USD',
273
- theme: { defaultMode: 'system', tokens: {} },
274
- auth: { signInPath: null },
275
- pwa: {
276
- enabled: false,
277
- offline: { fallback: null, image: null, font: null, neverCache: [], personalPages: 'never' },
278
- backgroundSync: false,
279
- push: false,
280
- name: '',
281
- colors: undefined,
282
- },
283
- roles: [...ROLES],
284
- database: { driver: 'postgres', ssl: false },
285
- cache: { defaultTtlMs: 60_000, tiers: ['request-memo', 'lru'] },
286
- jobs: {
287
- queues: [`${name}-default`],
288
- concurrency: 8,
289
- maxAttempts: 5,
290
- backoff: 'exponential',
291
- visibilityTimeoutMs: 30_000,
292
- },
293
- // ON by default since 22.0.0, when the boot began obeying the key: an app with no section
294
- // keeps the `sync` node it always got, and `enabled: false` is the explicit opt-out.
295
- realtime: { enabled: true, transport: 'memory', urlEnv: undefined },
296
- notify: { inboxReadRetentionMs: undefined, inboxUnreadRetentionMs: undefined },
297
- ai: { mcp: { expose: true, path: '/mcp' } },
298
- // Read from the process env when the config is DEFINED — the same env the drain will run in.
299
- drain: { readinessGraceMs: defaultReadinessGraceMs() },
300
- health: { readiness: 'dependencies' },
301
- };
302
- }
303
-
304
272
  function validate(config: AppConfig): void {
305
273
  const issues: string[] = [];
306
274
  // Zero or one entry: the zone's own remedy, carried only when the zone is what failed.
@@ -311,16 +279,13 @@ function validate(config: AppConfig): void {
311
279
  // Same shape again: carried only when an installable app is missing what an install needs.
312
280
  const pwaFix: string[] = [];
313
281
 
314
- if (!NAME_RE.test(config.name)) {
282
+ // `typeof` first: `NAME_RE.test(undefined)` tests the string "undefined", which matches.
283
+ if (typeof config.name !== 'string') {
284
+ issues.push(`name must be a string like "my-app", not ${describeValue(config.name)}`);
285
+ } else if (!NAME_RE.test(config.name)) {
315
286
  issues.push(`name "${config.name}" must match ${String(NAME_RE)}`);
316
287
  }
317
- if (config.locales.length === 0) issues.push('locales must list at least one locale');
318
- for (const locale of config.locales) {
319
- if (!isLocale(locale)) issues.push(`locales contains "${locale}", not a BCP-47 tag`);
320
- }
321
- if (!config.locales.includes(config.defaultLocale)) {
322
- issues.push(`defaultLocale "${config.defaultLocale}" is not in locales`);
323
- }
288
+ localeIssues(config.locales, config.defaultLocale, issues);
324
289
  // `@ultimat3/time`'s rule, restated because tier 0 cannot import tier 1 — see
325
290
  // `time-zone-name.ts`. One validator means a zone `app.config.ts` accepts is a zone every
326
291
  // `format` call, `task()` and `toZoned` below it can then do arithmetic in.
@@ -334,25 +299,33 @@ function validate(config: AppConfig): void {
334
299
  issues.push(`defaultCurrency "${config.defaultCurrency}" is not a 3-letter ISO 4217 code`);
335
300
  }
336
301
  if (config.roles.length === 0) issues.push('roles must list at least one runtime role');
337
- // A domain per numeric key, never a bare `< 1`: every comparison with `NaN` is false, so the old
338
- // `concurrency < 1` passed `NaN`, `2.5` and `Infinity`, and nothing screened the other three.
339
- const counts: readonly (string | undefined)[] = [
302
+ // ONE check per key, each against the key's own domain. A number is never a bare `< 1` — every
303
+ // comparison with `NaN` is false, so `concurrency < 1` passed `NaN`, `2.5` and `Infinity` — and
304
+ // a switch is never read for truthiness: `ssl: 'false'` and `enabled: 'false'` were both ON.
305
+ const perKey: readonly (string | undefined)[] = [
306
+ ...config.roles.map((role) => oneOfIssue('roles', role, ROLES)),
340
307
  countIssue('jobs.concurrency', config.jobs.concurrency, 1),
341
308
  countIssue('jobs.maxAttempts', config.jobs.maxAttempts, 1),
342
309
  countIssue('jobs.visibilityTimeoutMs', config.jobs.visibilityTimeoutMs, 1),
310
+ oneOfIssue('jobs.backoff', config.jobs.backoff, JOB_BACKOFFS),
343
311
  countIssue('cache.defaultTtlMs', config.cache.defaultTtlMs, 0),
312
+ oneOfIssue('database.driver', config.database.driver, DATABASE_DRIVERS),
313
+ booleanIssue('database.ssl', config.database.ssl),
314
+ oneOfIssue('theme.defaultMode', config.theme.defaultMode, THEME_MODES),
315
+ // `null` is the documented "no redirect"; a path that is said must be one a browser can follow.
316
+ config.auth.signInPath === null
317
+ ? undefined
318
+ : routePathIssue('auth.signInPath', config.auth.signInPath),
319
+ booleanIssue('ai.mcp.expose', config.ai.mcp.expose),
320
+ routePathIssue('ai.mcp.path', config.ai.mcp.path),
321
+ booleanIssue('realtime.enabled', config.realtime.enabled),
322
+ oneOfIssue('realtime.transport', config.realtime.transport, REALTIME_TRANSPORTS),
344
323
  readinessGraceIssue(config.drain.readinessGraceMs),
345
324
  readinessModeIssue(config.health.readiness),
346
325
  ];
347
- for (const issue of counts) if (issue !== undefined) issues.push(issue);
348
- if (config.jobs.queues.length === 0) issues.push('jobs.queues must list at least one queue');
349
- // An untyped config reaches here with whatever it wrote: a string is a name worth echoing, and
350
- // anything else goes through `describeValue` rather than `${…}`.
351
- const transport: unknown = config.realtime.transport;
352
- if (!REALTIME_TRANSPORTS.some((known) => known === transport)) {
353
- const said = typeof transport === 'string' ? `"${transport}"` : describeValue(transport);
354
- issues.push(`realtime.transport ${said} is not one of ${REALTIME_TRANSPORTS.join(', ')}`);
355
- } else if (transport === 'nats' && config.realtime.urlEnv === undefined) {
326
+ for (const issue of perKey) if (issue !== undefined) issues.push(issue);
327
+ nameListIssues('jobs.queues', config.jobs.queues, 'queue', issues);
328
+ if (config.realtime.transport === 'nats' && config.realtime.urlEnv === undefined) {
356
329
  issues.push(`realtime.transport "nats" requires realtime.urlEnv`);
357
330
  }
358
331
  // BOTH RETENTION WINDOWS OR NEITHER — `undefined` is a real value here (never swept) and the
@@ -381,6 +354,9 @@ function validate(config: AppConfig): void {
381
354
  // A rung the ladder cannot build is the defect this key had: `sortTiers` places a name by its
382
355
  // index in `CACHE_TIERS`, and a name missing from it sorts to `-1` — AHEAD of the request memo.
383
356
  // So an unknown tier is refused at boot rather than silently ignored or silently placed first.
357
+ // An EMPTY ladder is refused with the unknown rung: it builds no tier at all, so every read
358
+ // misses and nothing says the cache was configured away.
359
+ if (config.cache.tiers.length === 0) issues.push('cache.tiers must list at least one tier');
384
360
  for (const tier of config.cache.tiers) {
385
361
  if (CACHE_TIERS.includes(tier)) continue;
386
362
  issues.push(`cache.tiers contains "${tier}", which is not one of ${CACHE_TIERS.join(', ')}`);
@@ -399,20 +375,14 @@ function validate(config: AppConfig): void {
399
375
  }
400
376
 
401
377
  /**
402
- * The single config entry point. Later overlays win, so `config/jobs.ts` can own jobs without
403
- * touching `app.config.ts`.
378
+ * Every layer, in order, merged per section and KEY BY KEY — see `layered`. `name` comes from the
379
+ * input alone because it identifies the app: an overlay may not rename it. No layer at all is the
380
+ * defaults, which is the reference `shapeIssues` compares each layer against.
404
381
  */
405
- export function defineConfig(
406
- input: AppConfigInput,
407
- ...overlays: readonly AppConfigOverlay[]
408
- ): AppConfig {
409
- const base = defaults(input.name);
410
- // Every layer, in order, merged per section and KEY BY KEY — see `layered`. `name` comes from
411
- // the input alone because it identifies the app: an overlay may not rename it.
412
- const layers: readonly AppConfigOverlay[] = [input, ...overlays];
413
-
414
- const config: AppConfig = {
415
- name: input.name,
382
+ function merge(name: string, layers: readonly AppConfigOverlay[]): AppConfig {
383
+ const base = configDefaults(name);
384
+ return {
385
+ name,
416
386
  locales: lastSaid(
417
387
  base.locales,
418
388
  layers.map((layer) => layer.locales),
@@ -492,7 +462,28 @@ export function defineConfig(
492
462
  ...mergeNavigation(layers),
493
463
  ...mergeIslands(layers),
494
464
  };
465
+ }
495
466
 
467
+ /**
468
+ * The single config entry point. Later overlays win, so `config/jobs.ts` can own jobs without
469
+ * touching `app.config.ts`.
470
+ */
471
+ export function defineConfig(
472
+ input: AppConfigInput,
473
+ ...overlays: readonly AppConfigOverlay[]
474
+ ): AppConfig {
475
+ const layers: readonly AppConfigOverlay[] = [input, ...overlays];
476
+ // Structure FIRST, per layer and before the merge: a section written as `null` or a list
477
+ // written as a string is what `Object.entries` and `.length` raised a native `TypeError` on.
478
+ const issues: string[] = [];
479
+ // `navigation` is left out: `config-navigation.ts` carries a wrong shape through AS WRITTEN and
480
+ // refuses it in its own words, with the surfaces it accepts.
481
+ const reference = { ...merge(input.name, []), navigation: undefined };
482
+ for (const layer of layers) shapeIssues(reference, layer, issues);
483
+ if (issues.length > 0) {
484
+ throw new ConfigInvalidError({ cause: issues.join('; '), fix: BASE_FIX, meta: { issues } });
485
+ }
486
+ const config = merge(input.name, layers);
496
487
  validate(config);
497
488
  return Object.freeze(config);
498
489
  }
package/src/context.ts CHANGED
@@ -272,6 +272,18 @@ function screenDeadline(value: number | null | undefined): number | null {
272
272
  return finiteOption('the request context', 'deadlineAt', value);
273
273
  }
274
274
 
275
+ /**
276
+ * A patched signal is ADDED to the parent's, never swapped for it — the deadline's own rule, one
277
+ * field over. `patch ?? parent` made a step that brought its own signal blind to the request it
278
+ * runs inside: the client disconnected, the request timed out, and `ctx.signal.aborted` stayed
279
+ * false in the child. The shared never-aborting default is skipped rather than composed, so a
280
+ * context with no request behind it does not grow a listener per child.
281
+ */
282
+ function composeSignal(parent: AbortSignal, patch: AbortSignal | undefined): AbortSignal {
283
+ if (patch === undefined || patch === parent) return parent;
284
+ return parent === neverAborted ? patch : AbortSignal.any([parent, patch]);
285
+ }
286
+
275
287
  /**
276
288
  * Derive a narrowed context — impersonation, a locale switch, a per-step abort signal.
277
289
  * `requestId` is deliberately not patchable: one request, one id.
@@ -301,7 +313,7 @@ export function withChildContext<T>(patch: CtxPatch, fn: () => T): T {
301
313
  // that and did not do it — a patched hour replaced a parent's second outright, and
302
314
  // `remainingBudgetMs` then put the hour on `x-request-timeout-ms` for the next hop.
303
315
  deadlineAt: earliest(screenDeadline(patch.deadlineAt) ?? undefined, parent.deadlineAt),
304
- signal: patch.signal ?? parent.signal,
316
+ signal: composeSignal(parent.signal, patch.signal),
305
317
  services: { ...carried, ...(patch.services ?? {}) },
306
318
  });
307
319
  return requestContext.run(child, fn);
package/src/cookie.ts ADDED
@@ -0,0 +1,35 @@
1
+ // The one `Cookie:` request-header reader. At tier 0 because three packages need it — auth and http
2
+ // at tier 2 cannot import each other, i18n sits below both — and each copy had to rediscover the
3
+ // same thrown `URIError` on its own (`bun run flight-copies` refuses a fourth).
4
+
5
+ /**
6
+ * A `Cookie:` header is attacker-controlled, and `decodeURIComponent('%')` throws a bare
7
+ * `URIError` — which escapes every coded path that reads through here: an OAuth callback would
8
+ * answer 500 instead of `X_OAUTH_STATE_INVALID`, and `curl -H 'Cookie: x-locale=%'` paged the
9
+ * on-call from the `locale` stage. The raw value is returned instead, so the caller's own
10
+ * rejection stays the readable failure; a raw value is still checked against a signature, a stored
11
+ * hash or a `supported` list, and none of them match a mangled one.
12
+ */
13
+ const decodeCookieValue = (raw: string): string => {
14
+ try {
15
+ return decodeURIComponent(raw);
16
+ } catch {
17
+ return raw;
18
+ }
19
+ };
20
+
21
+ /**
22
+ * The value of cookie `name` in a `Cookie:` header, or `null` when the header or the cookie is
23
+ * absent. Never throws: the header is client-authored, so an unreadable value is the raw value and
24
+ * a header that is not a string at all is no cookie.
25
+ */
26
+ export function readCookie(header: string | null | undefined, name: string): string | null {
27
+ if (typeof header !== 'string') return null;
28
+ for (const part of header.split(';')) {
29
+ const equals = part.indexOf('=');
30
+ if (equals === -1) continue;
31
+ if (part.slice(0, equals).trim() !== name) continue;
32
+ return decodeCookieValue(part.slice(equals + 1).trim());
33
+ }
34
+ return null;
35
+ }
@@ -54,6 +54,11 @@ const CORE_CODE_TITLES = {
54
54
  X_REGISTRAR_CONFLICT: 'two different registrars are loaded for one primitive kind',
55
55
  X_REGISTRAR_MISSING: 'no registrar is loaded for a primitive kind',
56
56
  X_ROLE_INVALID: 'ROLE is not a known runtime role',
57
+ // `seal.ts`'s three. Titled here, not registered beside their classes the way the X_SECRETS_*
58
+ // set is, so `seal-errors.ts` runs nothing at import and is no `sideEffects` anchor.
59
+ X_SEAL_INVALID: 'a sealed value did not authenticate, or is not a sealed value',
60
+ X_SEAL_KEY_MISSING: 'no master key to seal or open a value with',
61
+ X_SEAL_KEY_UNKNOWN: 'a sealed value names a master key this process does not declare',
57
62
  X_SERVICE_DUPLICATE: 'a service name is registered twice',
58
63
  X_SERVICE_MISSING: 'service is not registered on the request context',
59
64
  X_SHUTDOWN_TIMEOUT: 'graceful shutdown exceeded its deadline',
package/src/cursor.ts CHANGED
@@ -49,7 +49,10 @@ let configured: string | undefined;
49
49
  * warned and nothing failed.
50
50
  */
51
51
  function currentSecret(): string {
52
- return configured ?? Bun.env['ULTIMATE_CURSOR_SECRET'] ?? DEV_SECRET;
52
+ // `||`, never `??`: `ULTIMATE_CURSOR_SECRET=` (a blank compose or chart value) is the EMPTY
53
+ // string, which `??` keeps — an HMAC keyed by '' that anyone can forge, while
54
+ // `usesDevCursorSecret()` answered `false` and the boot check passed. Empty is unset.
55
+ return configured || Bun.env['ULTIMATE_CURSOR_SECRET'] || DEV_SECRET;
53
56
  }
54
57
 
55
58
  /**
@@ -10,10 +10,11 @@
10
10
  * and a keyset page boundary was cut where the database never cuts one.
11
11
  *
12
12
  * It answers `undefined` rather than guessing, and that is the whole of its contract: a caller
13
- * that knows the column's declared kind (`@ultimat3/entity`'s `compareByKind`) asks; a caller that
14
- * does NOT know it — `@ultimat3/query`, whose `OrderKey` is a name and a direction — must not,
15
- * because Postgres orders a `text` column holding `"10"` and `"9"` lexically and a comparator
16
- * guessing "both sides look like decimals" would disagree with the SQL it printed.
13
+ * that knows the column's declared kind asks — `@ultimat3/entity`'s `numericOrder`, behind
14
+ * `compareByKind`, which `@ultimat3/query` calls with the kind it resolves from the entity — and a
15
+ * caller with NO kind in hand must not, because Postgres orders a `text` column holding `"10"` and
16
+ * `"9"` lexically and a comparator guessing "both sides look like decimals" would disagree with
17
+ * the SQL it printed.
17
18
  */
18
19
 
19
20
  /** A decimal, split so two of them can be compared exactly however long the digits run. */
@@ -19,7 +19,7 @@ export class CursorSecretDevError extends UltimateError {
19
19
  super({
20
20
  code: CursorSecretDevError.code,
21
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',
22
+ 'ULTIMATE_CURSOR_SECRET is unset or empty, so this process signs cursors with the development key the framework ships — anyone can forge a page position',
23
23
  fix: "x secrets set ULTIMATE_CURSOR_SECRET — or export ULTIMATE_CURSOR_SECRET from the platform's secret store",
24
24
  meta: { variable: 'ULTIMATE_CURSOR_SECRET' },
25
25
  });
@@ -188,9 +188,11 @@ export function renderFixLiteral(value: unknown, placeholder: string): string {
188
188
  * denylist has to be right about every character every shell will ever read, and this only has to
189
189
  * be right about the ones a fix line needs. No space, so a value is always one word; no leading
190
190
  * `-` or `~`, because an argument starting with either is an OPTION or a home directory rather
191
- * than the value it reads as.
191
+ * than the value it reads as. A leading `@` IS carried: a scoped package name starts with one, and
192
+ * `@` opens nothing in a POSIX shell — `$@` needs the `$`, an extglob `@(…)` the parenthesis, and
193
+ * neither is in the set.
192
194
  */
193
- const SHELL_ARG_SAFE = /^[A-Za-z0-9/][A-Za-z0-9._:/@=+,%~-]*$/;
195
+ const SHELL_ARG_SAFE = /^[A-Za-z0-9/@][A-Za-z0-9._:/@=+,%~-]*$/;
194
196
 
195
197
  /**
196
198
  * The same value where the text is read by a SHELL. A `fix:` is a command meant to be pasted, so a
@@ -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
  };
@@ -61,6 +61,14 @@ const CORE_ERROR_RETRY: ReadonlyMap<string, ErrorRetry> = new Map(
61
61
  // The principal fence's twin of `X_SUPERSEDED`, listed for the same reason: re-sending a read
62
62
  // from the previous principal's scope is refused identically every time.
63
63
  X_CLIENT_SCOPE_CHANGED: 'terminal',
64
+ // `seal.ts`'s three, listed for `X_NOT_IMPLEMENTED`'s reason: a missing key, an undeclared
65
+ // key and a value that failed its tag are each the same answer on attempt five, and left
66
+ // unclassified a job opening a sealed column would spend its whole retry policy re-proving it.
67
+ // Here rather than through `registerErrorRetry` because they are core's own codes, which that
68
+ // function refuses, and a module-scope call would make `seal-errors.ts` a side-effect anchor.
69
+ X_SEAL_INVALID: 'terminal',
70
+ X_SEAL_KEY_MISSING: 'terminal',
71
+ X_SEAL_KEY_UNKNOWN: 'terminal',
64
72
  } as const),
65
73
  );
66
74
 
@@ -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. */
@@ -53,23 +54,34 @@ export function createFlightGate(
53
54
  options?: FlightGateOptions,
54
55
  ): FlightGate {
55
56
  const subject = options?.subject ?? 'in-flight work';
57
+ // Refused at CONSTRUCTION, because this pair wedges rather than fails: `active < NaN` and
58
+ // `waiters.length >= NaN` are both false, so every caller parks in a queue with no bound. Zero
59
+ // is a real value at both — "never wait" and, at the width, "refuse everything".
60
+ const maxConcurrent = finiteCount(
61
+ `createFlightGate (${subject})`,
62
+ 'maxConcurrent',
63
+ limits.maxConcurrent,
64
+ );
65
+ const maxQueued = finiteCount(`createFlightGate (${subject})`, 'maxQueued', limits.maxQueued);
56
66
  const waiters: Array<() => void> = [];
57
67
  let active = 0;
58
68
 
59
69
  const state = (): FlightGateState => ({
60
- maxConcurrent: limits.maxConcurrent,
61
- maxQueued: limits.maxQueued,
70
+ maxConcurrent,
71
+ maxQueued,
62
72
  active,
63
73
  queued: waiters.length,
64
74
  subject,
65
75
  });
66
76
 
67
77
  const acquire = async (): Promise<void> => {
68
- if (active < limits.maxConcurrent) {
78
+ if (active < maxConcurrent) {
69
79
  active += 1;
70
80
  return;
71
81
  }
72
- if (waiters.length >= limits.maxQueued) {
82
+ // A width of zero has no slot to hand over, so a waiter would never be resumed: the queue is
83
+ // for work that WILL run, and here none will.
84
+ if (maxConcurrent === 0 || waiters.length >= maxQueued) {
73
85
  const current = state();
74
86
  throw options?.overflow?.(current) ?? gateOverloaded(current);
75
87
  }
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
+ }
@@ -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
+ }
@@ -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