@ultimat3/http 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.
@@ -0,0 +1,94 @@
1
+ // One slice of the status table `error-map.ts` composes: `@ultimat3/http`'s own codes.
2
+ // A row here is a code owned by `@ultimat3/http`; `bun run new-error-code` appends to the slice its
3
+ // `--package` belongs to, so no one file grows with every code the framework adds.
4
+
5
+ export const HTTP_ERROR_STATUS = {
6
+ // @ultimat3/http
7
+ X_ROUTE_NOT_FOUND: 404,
8
+ X_METHOD_NOT_ALLOWED: 405,
9
+ // The request line itself is unreadable, so there is nothing to route: 400, and never a 500 —
10
+ // a malformed escape is the caller's typo, not this server's defect.
11
+ X_PATH_INVALID: 400,
12
+ X_BODY_INVALID: 422,
13
+ X_UNAUTHENTICATED: 401,
14
+ X_FORBIDDEN: 403,
15
+ X_RATE_LIMITED: 429,
16
+ X_BUILD_SKEW: 409,
17
+ X_ROUTE_CONFLICT: 500,
18
+ X_SERVER_NOT_STARTED: 500,
19
+ X_PIPELINE_NO_RESPONSE: 500,
20
+ // The request was answered and the answer could not be finished: the caller gets nothing usable
21
+ // either way, so this is the server's failure, never the caller's.
22
+ X_PIPELINE_FINALIZE_FAILED: 500,
23
+ // Both are wiring bugs, never a caller's mistake: reading a cookie where no request exists,
24
+ // and declaring a status the framework already owns. 500 is the honest answer to either.
25
+ X_NO_REQUEST: 500,
26
+ X_ERROR_STATUS_INVALID: 500,
27
+ X_PROBLEM_META_INVALID: 500,
28
+ // A `hive()` whose `split()` returned no members. The caller cannot fix it by sending
29
+ // different input — the guard belongs in the app, either by returning at least one member
30
+ // or by skipping the hive when the source is empty — so it is the server's bug, not theirs.
31
+ X_HIVE_EMPTY: 500,
32
+ // Thrown while `app.config.ts` resolves, so no request is ever answered with it — the row exists
33
+ // because a code with no status is a 500 anyway and this table is the closed one.
34
+ X_CORS_CONFIG_INVALID: 500,
35
+ // Core's code, borrowed by `defineHttpConfig` for a numeric knob that is not a count. Same
36
+ // construction-time shelf as the row above, and it needs a row for the same reason: this table
37
+ // is closed over every code the package can throw, borrowed ones included.
38
+ X_CONFIG_INVALID: 500,
39
+ X_CSP_DIRECTIVE_INVALID: 500,
40
+ // Thrown while the server is being constructed, so no request is ever answered with it either.
41
+ // The row exists because this table is the closed one: a code missing from it is a 500 anyway,
42
+ // and a code the framework owns must never fall through to the app's table.
43
+ X_RATE_LIMIT_NOT_SHARED: 500,
44
+ // Same construction-time class as the row above: a route and the config declare one bucket
45
+ // differently, and the process refuses to start rather than pick.
46
+ X_RATE_LIMIT_BUCKET_CONFLICT: 500,
47
+ // Construction time as well: the limiter installed cannot enforce a bucket a route declares.
48
+ X_RATE_LIMIT_BUCKET_UNBOUND: 500,
49
+ // `defineHttpConfig` time, all three: a declaration the deployment owes and did not make, or
50
+ // made against a bucket nothing declares.
51
+ X_RATE_LIMIT_SCOPE_UNSET: 500,
52
+ X_RATE_LIMIT_TENANT_BUCKET_UNKNOWN: 500,
53
+ X_TRUST_PROXY_UNSET: 500,
54
+ // Raised by `toBucket` while a route or an action is being projected, never on the request.
55
+ X_RATE_LIMIT_INVALID: 500,
56
+ // The shared store did not answer, so nothing decided. An operator's fault, never the caller's.
57
+ X_RATE_LIMIT_STORE_UNAVAILABLE: 500,
58
+ // The two the `admit` stage answers with, and the only 503s the pipeline produces. Both carry
59
+ // `retry-after`: a shed request that does not say when to come back is a request that comes
60
+ // back immediately, which is the load it was shed to avoid.
61
+ X_DRAINING: 503,
62
+ X_OVERLOADED: 503,
63
+ // Core's flight gate refusing past `maxQueued` is the same answer one tier down: it carries
64
+ // `retryAfterSeconds` in `meta`, which `stages.ts` renders onto `Retry-After`, so a caller that
65
+ // already handles a shed request needs no second branch for a refused one.
66
+ X_FLIGHT_GATE_OVERLOADED: 503,
67
+ // 403 and never 401: the caller IS authenticated — that is what makes the forged write work —
68
+ // so a 401 would send a signed-in user to a sign-in page they are already past.
69
+ X_CSRF_BLOCKED: 403,
70
+ // 401 for both, and never 400: an inbound webhook is well formed and carries a CREDENTIAL — a
71
+ // timestamped hmac over its own bytes — so what failed is authentication, not the request. Never
72
+ // 403 either, which means an authenticated caller was refused, and there is no authenticated
73
+ // caller here. Two codes rather than one because the repairs differ and a sender's dashboard
74
+ // shows the status: `INVALID` is the wrong secret or a rewritten body, `STALE` is a skewed clock
75
+ // or a delivery being replayed off a capture. Neither triggers `signInRedirect`, which keys on
76
+ // `X_UNAUTHENTICATED` alone — a webhook sender is not a browser and has no session to go get.
77
+ X_WEBHOOK_SIGNATURE_INVALID: 401,
78
+ X_WEBHOOK_SIGNATURE_STALE: 401,
79
+ // Boot refusals of the app's HTTP declaration (a bearer mount, an action's pinned path or the
80
+ // app's path style or a path derived before it, the openapi block, the MCP oauth block or an
81
+ // idempotent tool shadowing its key argument, a page's `post` or `navigation`) — never a request's
82
+ // answer; 500 if one ever escaped into a response (`x dev` re-evaluating an edited route module):
83
+ // the deployment is wrong, the caller is not.
84
+ X_BEARER_MOUNT_INVALID: 500,
85
+ X_ACTION_HTTP_PATH_INVALID: 500,
86
+ X_ACTION_PATH_STYLE_INVALID: 500,
87
+ X_ACTION_PATH_DERIVED_EARLY: 500,
88
+ X_OPENAPI_CONFIG_INVALID: 500,
89
+ X_MCP_OAUTH_INVALID: 500,
90
+ X_MCP_PATH_DUPLICATE: 500,
91
+ X_MCP_IDEMPOTENCY_KEY_SHADOWED: 500,
92
+ X_ROUTE_POST_INVALID: 500,
93
+ X_ROUTE_NAVIGATION_INVALID: 500,
94
+ } satisfies Readonly<Record<string, number>>;
@@ -0,0 +1,60 @@
1
+ // One slice of the status table `error-map.ts` composes: tier 0 — `core`, `schema`.
2
+ // A row here is a code owned by a tier-0 package; `bun run new-error-code` appends to the slice its
3
+ // `--package` belongs to, so no one file grows with every code the framework adds.
4
+
5
+ export const TIER_0_ERROR_STATUS = {
6
+ // @ultimat3/core (declared beside `assertLocale`; `@ultimat3/time` owned it until 16.x) — the
7
+ // sibling of the row above, and strictly more the caller's fault: not a tag at all. It was
8
+ // pinned as unable to reach a request because the `locale` stage negotiates and never throws,
9
+ // which is true of that stage and irrelevant to `?locale=`, a path segment or an action input
10
+ // reaching `formatDate` / `formatMoney` / `describeCron`. Those raised it and answered 500,
11
+ // paging the on-call for a string the caller typed.
12
+ X_LOCALE_INVALID: 400,
13
+ // @ultimat3/core — `seal()` / `open()`. All three are reachable from a request (a sealed column
14
+ // read inside an action) and none is the caller's doing: the deploy has no key, the ring lost a
15
+ // retired key, or a stored value did not authenticate. 500, stated rather than defaulted.
16
+ X_SEAL_INVALID: 500,
17
+ X_SEAL_KEY_MISSING: 500,
18
+ X_SEAL_KEY_UNKNOWN: 500,
19
+ // @ultimat3/core
20
+ // The caller asked for a format the pipeline cannot produce (`?f=avif`): the request names an
21
+ // unsupported representation, which is 415 — not a 500, which would blame the server for it.
22
+ X_IMAGE_UNSUPPORTED: 415,
23
+ // A page token this server minted and the caller echoed back, and it did not verify — tampered,
24
+ // or replayed against another read. The caller's value and the caller's repair ("request the
25
+ // first page again"), so it belongs beside `X_IMAGE_QUERY_INVALID` at 400 and not at 500.
26
+ X_CURSOR_INVALID: 400,
27
+ // Raised by `markReady()` while a role STARTS, in a process whose lifecycle already drained — so
28
+ // no request is ever answered with it, and the row exists for the reason the construction-time
29
+ // rows above do: this table is the closed one, and a code with no row is a 500 anyway.
30
+ X_LIFECYCLE_DRAINED: 500,
31
+ X_NOT_IMPLEMENTED: 501,
32
+ X_TIMEOUT: 504,
33
+ X_ABORTED: 499,
34
+ // The twin of `X_ABORTED`, and it answers the same because the outcome is the same: the caller
35
+ // went away there, the caller's generation moved on here, and in both cases nobody will act on
36
+ // the answer. Deliberately not 409 — that spelling asks the client to reconcile and try again,
37
+ // and a fenced answer has nothing to reconcile against.
38
+ X_SUPERSEDED: 499,
39
+ // Plan 101's client seam. A typed call made from a server handler: an upstream that gave no usable
40
+ // answer is 502, the fence is `X_SUPERSEDED`'s twin. The rest are browser-side or declaration-time
41
+ // refusals no request carries; their rows keep the table closed.
42
+ X_CLIENT_TRANSPORT_FAILED: 502,
43
+ X_CLIENT_RECORD_ENVELOPE_INVALID: 502,
44
+ X_CLIENT_SCOPE_CHANGED: 499,
45
+ X_CHANNEL_DECLARATION_INVALID: 500,
46
+ X_LOCAL_STORE_UNAVAILABLE: 500,
47
+ X_MUTATOR_CLOCK_MISSING: 500,
48
+ X_REALTIME_UNINSTALLED: 500,
49
+ // Boot-time: a realtime config and its environment that disagree refuse before any request.
50
+ X_REALTIME_TOPOLOGY: 500,
51
+ // The replicator's own Postgres connection failed TLS. A role that never answers a request, so the
52
+ // row keeps the table closed at 500, beside `X_REALTIME_TOPOLOGY`.
53
+ X_REPLICATION_TLS: 500,
54
+ // A browser page on a foreign origin asked for a socket — refused, not unauthenticated: 403.
55
+ X_SOCKET_ORIGIN_REFUSED: 403,
56
+ X_RECORD_KEY_MISSING: 500,
57
+ X_RECORD_REJECTED: 500,
58
+ X_SYNC_UNCONFIGURED: 500,
59
+ X_INTERNAL: 500,
60
+ } satisfies Readonly<Record<string, number>>;
@@ -0,0 +1,71 @@
1
+ // One slice of the status table `error-map.ts` composes: tier 1 — `i18n`, `money`, `time`, `cache`, `seo`, `db`, `storage`, `flags`.
2
+ // A row here is a code owned by a tier-1 package; `bun run new-error-code` appends to the slice its
3
+ // `--package` belongs to, so no one file grows with every code the framework adds.
4
+
5
+ export const TIER_1_ERROR_STATUS = {
6
+ // @ultimat3/db — the constraints a request trips, both 409. db's own `fix:` for the unique
7
+ // violation says "answer 409, which is what a raced signup is", and `X_ENTITY_DUPLICATE` — the
8
+ // same event one layer up — is 409 above; a foreign key rides with it because both halves of it
9
+ // are a conflict with the state that is there (the parent is missing, or the child still points
10
+ // at it), which 422 describes only for the insert.
11
+ X_DB_UNIQUE_VIOLATION: 409,
12
+ X_DB_FOREIGN_KEY_VIOLATION: 409,
13
+ // @ultimat3/i18n — a well-formed tag outside the set this app ships, asserted on a value the
14
+ // caller supplied (`assertSupportedLocale`). 400 rather than 406: the http `locale` stage
15
+ // negotiates `Accept-Language` and never throws, so the tag that reaches here came from a path,
16
+ // query or body the caller wrote — the same place `X_IMAGE_QUERY_INVALID` comes from.
17
+ X_LOCALE_UNSUPPORTED: 400,
18
+ // @ultimat3/money — a well-formed code this process carries no row for. The currency table is
19
+ // OPEN (`registerCurrency`), and every surface between the wire and the throw accepts any
20
+ // `^[A-Z]{3}$`: `@ultimat3/schema`'s `CURRENCY_CODE_PATTERN`, the OpenAPI `pattern` emitted from
21
+ // it, and `@ultimat3/entity`'s `char(3)` CHECK. So `{ minor: 100, currency: 'ZWL' }` parses,
22
+ // reaches `money()` -> `assertCurrency`, and with no row answered 500 — reporting a value the
23
+ // framework's own schema had just accepted to the error monitor. 400 rather than 422, beside
24
+ // `X_LOCALE_UNSUPPORTED`: the same shape of mistake, a well-formed value naming something
25
+ // outside the set this process carries, and money's `fix:` already instructs the caller.
26
+ X_CURRENCY_UNKNOWN: 400,
27
+ // @ultimat3/seo — a transform query the caller wrote, so the caller is the one who can fix it.
28
+ X_IMAGE_QUERY_INVALID: 400,
29
+ // @ultimat3/storage — every one of these is reachable from a route: `/media/*` already serves
30
+ // objects, and a mounted `/_storage` serves signed reads and takes signed writes. Without a row
31
+ // a missing image answers 500, which reads as an outage instead of a 404.
32
+ X_STORAGE_NOT_FOUND: 404,
33
+ X_STORAGE_PATH_UNSAFE: 400,
34
+ X_STORAGE_TOO_LARGE: 413,
35
+ X_STORAGE_TYPE_REJECTED: 415,
36
+ X_STORAGE_CHECKSUM_MISMATCH: 422,
37
+ X_STORAGE_URL_INVALID: 403,
38
+ X_STORAGE_URL_EXPIRED: 410,
39
+ // 500, not 403, and the distinction is the whole reason this code exists rather than
40
+ // reusing X_STORAGE_URL_INVALID: nothing is wrong with the caller's URL. The disk was
41
+ // built with no way to check a signature, which is the operator's misconfiguration and
42
+ // not an attacker — reporting it as 403 sends the on-call hunting somebody who is not there.
43
+ X_STORAGE_URL_UNVERIFIABLE: 500,
44
+ // 409, not the 500 it fell through to: the object exists and the request is well formed — the
45
+ // STATE is wrong. A validated upload lands under the quarantine segment and `promoteAttachment`
46
+ // refuses it until the app's own scanner calls `releaseQuarantine`, which is a thing the caller
47
+ // can do. A 500 would have read as "the server broke" for a workflow working exactly as built.
48
+ X_STORAGE_QUARANTINED: 409,
49
+ // 409 for the same reason: `promoteAttachment` on a key that is not a pending upload — promoted
50
+ // twice, or never uploaded — is a state the caller can see and fix, not a server fault.
51
+ X_STORAGE_NOT_PENDING: 409,
52
+ // 404, deliberately NOT 403: the org check fires before anything is read, so answering
53
+ // "forbidden" would confirm that a key exists to the one caller who must not learn it.
54
+ X_STORAGE_ORG_MISMATCH: 404,
55
+ // @ultimat3/seo — a `meta.links` entry an `ssr` page's `meta()` built from data: an unsafe href,
56
+ // a preload with no `as`. Thrown while that request renders its head, so it reaches the caller —
57
+ // an authoring defect, never the visitor's, hence 500 with the cause naming the link intact.
58
+ X_SEO_LINK_INVALID: 500,
59
+ // @ultimat3/db — the server rolled the transaction back
60
+ X_DB_TRANSACTION_ABORTED: 500,
61
+ // @ultimat3/db — the connection was lost while COMMIT was in flight
62
+ X_DB_COMMIT_UNKNOWN: 500,
63
+ // @ultimat3/db — a nested transaction scope waited too long for its sibling
64
+ X_DB_SIBLING_SCOPE_TIMEOUT: 500,
65
+ // @ultimat3/storage — the key collides with another key on the local disk
66
+ X_STORAGE_KEY_CONFLICT: 409,
67
+ // @ultimat3/storage — the object could not be written
68
+ X_STORAGE_PUT_FAILED: 500,
69
+ // @ultimat3/storage — the object could not be read
70
+ X_STORAGE_READ_FAILED: 500,
71
+ } satisfies Readonly<Record<string, number>>;
@@ -0,0 +1,118 @@
1
+ // One slice of the status table `error-map.ts` composes: tier 2 but `http` itself — `entity`, `policy`, `auth`.
2
+ // A row here is a code owned by `entity`, `policy` or `auth`; `bun run new-error-code` appends to the slice its
3
+ // `--package` belongs to, so no one file grows with every code the framework adds.
4
+
5
+ export const TIER_2_ERROR_STATUS = {
6
+ // @ultimat3/auth — every one of these is reachable from a request: the OAuth route descriptors
7
+ // are mounted by the app, and `authenticate` throws the session codes inside the pipeline. Without
8
+ // a row each fell to 500, so a user pressing Cancel on a consent screen paged the on-call and a
9
+ // provider this app never enabled read as an outage. `packages/auth/src/oauth-route.ts` answers
10
+ // from this table's values when its descriptors are driven OUTSIDE a pipeline; the pin that keeps
11
+ // the two identical is `scripts/oauth-route-status.test.ts`, since auth is this tier and cannot
12
+ // import this package.
13
+ X_SESSION_EXPIRED: 401,
14
+ X_MFA_REQUIRED: 401,
15
+ X_ACCOUNT_LOCKED: 429,
16
+ X_API_KEY_INVALID: 401,
17
+ X_OAUTH_STATE_INVALID: 400,
18
+ X_OAUTH_TOKEN_INVALID: 400,
19
+ X_OAUTH_PROVIDER_UNKNOWN: 404,
20
+ X_OAUTH_DENIED: 403,
21
+ // 502, not 500: the conversation that failed is with the provider's server, and the on-call
22
+ // question "is it us or them?" is the one a status is read for.
23
+ X_OAUTH_EXCHANGE_FAILED: 502,
24
+ // 422 and not 400: the body parsed, the field is a string, and a policy rejected its CONTENT —
25
+ // the same class as `X_BODY_INVALID` and `X_INVARIANT_VIOLATED` above. Unmapped, a visitor
26
+ // choosing "password" at a signup form was reported to the on-call monitor as a server fault.
27
+ X_PASSWORD_WEAK: 422,
28
+ // 422 for the same reason as the row above, and deliberately not 500: `enrolTotp` throws it for a
29
+ // secret the CALLER supplied — an import from another MFA system, or a value off a form — and
30
+ // what failed is that value's content, not the server. `verifyTotp` never throws it (an
31
+ // unreadable stored secret is a non-verdict there, the rule `verifyAgainst` follows for a hash
32
+ // Bun cannot read), so a login checking a broken row cannot reach this status at all.
33
+ X_MFA_SECRET_INVALID: 422,
34
+ // @ultimat3/entity
35
+ X_NOT_FOUND: 404,
36
+ X_ENTITY_DUPLICATE: 409,
37
+ X_INVARIANT_VIOLATED: 422,
38
+ X_TENANCY_UNSCOPED: 500,
39
+ X_DB_DRIFT: 500,
40
+ // Both are the server's verdict on a statement the app wrote, which is the app's fault and not
41
+ // the caller's: a request cannot repair a missing migration or a wrong column list.
42
+ X_DB_SCHEMA_STALE: 500,
43
+ X_DB_STATEMENT_FAILED: 500,
44
+ // Logged at boot, never thrown into a request; the row exists so the table stays closed.
45
+ X_MCP_APP_UNMOUNTED: 500,
46
+ // The three tenancy refusals, all 403, and all deliberately NOT the 404 `X_STORAGE_ORG_MISMATCH`
47
+ // takes: that one answers 404 because a 403 on a KEY the caller supplied confirms the key exists.
48
+ // These three name no resource and read no row — the comparison is the actor against an argument
49
+ // (`X_TENANCY_ACTOR_MISMATCH`), the actor against nothing at all (`X_TENANCY_ACTOR_ORG_REQUIRED`),
50
+ // or the actor's scopes at `crossTenant()` (`X_TENANCY_CROSS_DENIED`) — so the answer is the same
51
+ // whether or not the other tenant's row exists, and a 404 would buy no secrecy for the lie.
52
+ X_TENANCY_ACTOR_MISMATCH: 403,
53
+ // 403 and never 401, for the reason `X_CSRF_BLOCKED` is one: the actor may be fully
54
+ // authenticated and merely carry no org — a service actor minted without one — so a 401 sends a
55
+ // signed-in caller to a sign-in page that cannot give them a tenant.
56
+ X_TENANCY_ACTOR_ORG_REQUIRED: 403,
57
+ X_TENANCY_CROSS_DENIED: 403,
58
+ // The three aggregate refusals, all 500 and all deliberately NOT a 4xx, for the reason
59
+ // `X_QUERY_NOT_PAGEABLE` below is one: nothing the caller sends changes the answer, and the fix
60
+ // is an edit to the read itself. They earn ROWS rather than a pin in `scripts/error-map-backlog.ts`
61
+ // because each carries an instruction the app's author needs and an unmapped 5xx is blanked —
62
+ // `toProblem` replaces an undeclared code's cause with `INTERNAL_CAUSE`, so pinning them would
63
+ // answer "the server failed while handling this request" for a fault whose own `fix:` names the
64
+ // exact call to write instead. A row costs no extra page: `stages.ts` reports every `status >= 500`
65
+ // either way.
66
+ //
67
+ // Reached with a request waiting, all three, which is why they are not in the backlog's entity
68
+ // group ("misuse a handler's author makes") — the mixed-currency and the ±2^53 refusals are
69
+ // decided by the ROWS, so a read that answered for two years starts failing on the day the data
70
+ // crosses the line, and `approximateCount()` on a chain whose predicates came from the caller's
71
+ // own optional filters is one query string away.
72
+ X_AGGREGATE_UNSUPPORTED: 500,
73
+ X_AGGREGATE_MIXED_CURRENCY: 500,
74
+ X_APPROXIMATE_COUNT_FILTERED: 500,
75
+ // The two search refusals, 500 for the reason the three above are: nothing the caller sends
76
+ // changes either answer. An entity with no searchable column needs a `searchable()` on one, and
77
+ // a driver that cannot answer a full-text match needs the Postgres one — both are edits to the
78
+ // app, and both carry a `fix:` that an unmapped 5xx would blank (`toProblem` replaces an
79
+ // undeclared code's cause with `INTERNAL_CAUSE`).
80
+ X_SEARCH_UNDECLARED: 500,
81
+ X_SEARCH_IN_MEMORY: 500,
82
+ // The three state-machine refusals, and they are deliberately THREE statuses rather than one:
83
+ // the machine says the transition does not exist, the row says it is somewhere else, or the
84
+ // column says there is no machine at all — three different readers and three different repairs.
85
+ //
86
+ // 422 and not 400: the request is well formed and its schema passed. The transition the caller
87
+ // named is not one this machine has, which is the same shape as `X_INVARIANT_VIOLATED` above and
88
+ // takes its status. Refused before any statement opens a connection, so nothing was written.
89
+ X_STATE_TRANSITION_ILLEGAL: 422,
90
+ // 409, the lost update caught. The row moved between the read the caller decided on and the
91
+ // write it asked for — nothing is wrong with either, and the repair is re-read and retry, which
92
+ // is precisely what a 409 tells a client to do. A 422 would say "your request is unusable",
93
+ // which is false: the identical request succeeds a moment later.
94
+ X_STATE_CONFLICT: 409,
95
+ // 500, the same shelf as `X_SEARCH_UNDECLARED`: a column with no machine is a declaration the
96
+ // app has not written, and no request changes that.
97
+ X_STATE_UNDECLARED: 500,
98
+ // A sealed column named where the database would compare it, or in a view. Both are the app's
99
+ // own declaration or call, never the caller's input — the same shelf as the two above.
100
+ X_ENTITY_SEALED_PREDICATE: 500,
101
+ X_ENTITY_SEALED_IN_VIEW: 500,
102
+ // @ultimat3/policy
103
+ X_POLICY_MISSING: 500,
104
+ // A declaration fault raised at module evaluation, the same shelf as the line above. The row is
105
+ // not a claim it reaches a request — an unmapped code already answers 500 — it is the answer
106
+ // being REVIEWED instead of accidental, which is why this table is closed.
107
+ X_POLICY_CLAUSE_EMPTY: 500,
108
+ X_PERMISSION_UNKNOWN: 500,
109
+ // 500, and the page IS the point — deliberately not a 4xx to keep this table quiet.
110
+ // `enforce()` was handed a surface no adapter answers to, which reaches a request only through a
111
+ // config-driven route table, a surface name off the wire or a JS host; none of those is a value
112
+ // the caller can correct, and a 400 would tell them to fix a request that is not the problem.
113
+ // It is the third authz-dispatch fault beside the two rows above and takes their status for the
114
+ // same reason: the declaration is wrong, not the call.
115
+ X_POLICY_SURFACE_UNKNOWN: 500,
116
+ // @ultimat3/auth — a stored totp secret is not sealed, so it is never read as one
117
+ X_MFA_SECRET_UNSEALED: 500,
118
+ } satisfies Readonly<Record<string, number>>;
@@ -0,0 +1,92 @@
1
+ // One slice of the status table `error-map.ts` composes: tier 3 — `action`, `query`, `jobs`, `realtime`.
2
+ // A row here is a code owned by a tier-3 package; `bun run new-error-code` appends to the slice its
3
+ // `--package` belongs to, so no one file grows with every code the framework adds.
4
+
5
+ export const TIER_3_ERROR_STATUS = {
6
+ // @ultimat3/action — the code every primitive throws when the CALLER's input fails the schema
7
+ // the primitive declared. 400 because that is what the published OpenAPI operation promises for
8
+ // it, and because a missing row made a typo'd uuid a 500: the caller was told the server broke,
9
+ // and the `error-map` stage reported the caller's mistake to the on-call monitor.
10
+ X_INPUT_INVALID: 400,
11
+ // The one way a SERVER answers this code: a POST to the path an action has under a path style
12
+ // this app does not serve (`hooks.explainMiss`). 404 because it is still a miss — nothing is
13
+ // routed there and nothing ran, so the status a proxy or a monitor counts is the one the plain
14
+ // `X_ROUTE_NOT_FOUND` had; the code and the `fix:` are what changed. Every other raise of it is
15
+ // client-side or a contract test, and never reaches this table.
16
+ X_CONTRACT_DRIFT: 404,
17
+ // A retried `Idempotency-Key` naming a different payload, or one still in flight. 409 because
18
+ // that is what the action's own OpenAPI operation publishes for it — the runtime answered 500
19
+ // while the document promised 409, and a client written against the spec read the framework
20
+ // working exactly as designed as an outage.
21
+ X_IDEMPOTENCY_CONFLICT: 409,
22
+ // A blank or over-long `Idempotency-Key` HEADER, refused before the handler runs. 400 and not
23
+ // the 422 a body gets: what failed is a parameter the OpenAPI operation publishes a `maxLength`
24
+ // for, which is the same thing `X_INPUT_INVALID` is 400 for.
25
+ X_IDEMPOTENCY_KEY_INVALID: 400,
26
+ // 500, and deliberately not the 409 above. `IdempotencyReplayedFailureError` re-throws the FIRST
27
+ // attempt's own code whenever the store recorded one, so this literal code is reached only when
28
+ // that attempt failed carrying no code at all: an unclassified throw whose commit state nobody
29
+ // knows. That is the server's to explain, and it is worth reporting.
30
+ X_IDEMPOTENCY_REPLAYED_FAILURE: 500,
31
+ // Same shape as the line above and 500 for the same reason: the store holds a record this
32
+ // build cannot turn into a result. Deliberately NOT 503 — a rolling deploy is the usual
33
+ // cause, so a retry may well reach a newer pod and succeed, but this code carries no
34
+ // `retry-after` and the two 503s above are the only ones that do. Telling a caller to come
35
+ // back without saying when is the load-shedding mistake, one layer up.
36
+ X_IDEMPOTENCY_STATUS_UNKNOWN: 500,
37
+ // @ultimat3/jobs — the ONE jobs code with a row here, and the reason the rest are pinned in
38
+ // `scripts/error-map-backlog.ts` does not cover it. That pin says "a job runs with no socket
39
+ // attached; `ROLE=worker` opens no HTTP port at all" — true of `X_JOB_TIMEOUT` and every other
40
+ // worker-runtime code, and NOT true of a decode failure: `toJobRecord` runs wherever a row is
41
+ // READ, which includes the admin dashboard's job panel and `x jobs show` served over HTTP.
42
+ // 500, and it should page: a queue holding rows this build cannot read is an operator's
43
+ // problem, and nothing the caller sent is wrong.
44
+ X_JOB_ROW_STATUS_UNKNOWN: 500,
45
+ // A requeue of a job that is still live — the admin panel's retry, `x jobs retry` over HTTP. The
46
+ // job's STATE is wrong, not the server: 409, like `X_STORAGE_QUARANTINED`.
47
+ X_JOB_NOT_REQUEUEABLE: 409,
48
+ // Thrown by `job()` at declaration, while modules load; no request is answered with it. The row
49
+ // exists for `X_ACTION_JOB_UNBRIDGED`'s reason: the table is closed, and it is a 500 anyway.
50
+ X_JOB_DECLARATION_INVALID: 500,
51
+ // Thrown by `registerJobs()` while the app's modules load, so no request is ever answered with
52
+ // it either — the row exists for the reason `X_CORS_CONFIG_INVALID`'s does: this table is the
53
+ // closed one, and a code with no row is a 500 anyway.
54
+ X_ACTION_JOB_UNBRIDGED: 500,
55
+ // Every `X_WEBHOOK_*` below is OUTBOUND and is thrown inside a worker: `ROLE=worker` opens no
56
+ // HTTP port, so none of them ever answers a request. The rows exist for the reason
57
+ // `X_ACTION_JOB_UNBRIDGED`'s does — this table is the closed one, and a code with no row is a
58
+ // 500 anyway. The INBOUND pair (`X_WEBHOOK_SIGNATURE_*`, 401) is @ultimat3/http's and sits with
59
+ // the rest of this package's codes above; these are the ones a delivery ends on.
60
+ X_WEBHOOK_ENDPOINT_UNKNOWN: 500,
61
+ X_WEBHOOK_ENDPOINT_INVALID: 500,
62
+ X_WEBHOOK_ENDPOINT_DISABLED: 500,
63
+ X_WEBHOOK_EVENT_UNKNOWN: 500,
64
+ X_WEBHOOK_EVENT_INVALID: 500,
65
+ X_WEBHOOK_DELIVERY_FAILED: 500,
66
+ X_WEBHOOK_DELIVERY_THROTTLED: 500,
67
+ X_WEBHOOK_DELIVERY_REJECTED: 500,
68
+ // Same class again: an export pass runs in a worker, and both codes refuse the DECLARATION —
69
+ // a `row()` that answers columns nobody declared, and a page too big to hold. Neither is
70
+ // anything a caller sent.
71
+ X_EXPORT_ROW_INVALID: 500,
72
+ X_EXPORT_PART_TOO_LARGE: 500,
73
+ // @ultimat3/query — the read declares no id, so no cursor can name a position in it. The one
74
+ // paging failure that is NOT the caller's: the fix is an edit to the read's own select, nothing
75
+ // the client sends changes the answer, and the report to the on-call monitor is the point.
76
+ X_QUERY_NOT_PAGEABLE: 500,
77
+ // A read filtering or sorting on a column its loader never selected: the fix is the loader's
78
+ // `select`, nothing the caller sends changes it — the same server-side decision as the row above.
79
+ X_QUERY_COLUMN_UNSELECTED: 500,
80
+ // @ultimat3/jobs — the job is running and cannot be removed
81
+ X_JOB_NOT_REMOVABLE: 409,
82
+ // @ultimat3/jobs — the job is not waiting on its run time
83
+ X_JOB_NOT_PROMOTABLE: 409,
84
+ // @ultimat3/jobs — a job list page was asked for outside its bounds
85
+ X_JOB_PAGE_INVALID: 400,
86
+ // @ultimat3/action — a mutator is declared without idempotent: true
87
+ X_MUTATOR_NOT_IDEMPOTENT: 500,
88
+ // @ultimat3/action — an idempotent action's reservation was taken over before its transaction could settle it
89
+ X_IDEMPOTENCY_RESERVATION_LOST: 409,
90
+ // @ultimat3/jobs — the queue holds no job with this id
91
+ X_JOB_NOT_FOUND: 404,
92
+ } satisfies Readonly<Record<string, number>>;
@@ -0,0 +1,92 @@
1
+ // One slice of the status table `error-map.ts` composes: tier 4 — `render`, `pwa`, `mcp`, `ai`, `manifest`, `mail`, `ui`, `notify`.
2
+ // A row here is a code owned by a tier-4 package; `bun run new-error-code` appends to the slice its
3
+ // `--package` belongs to, so no one file grows with every code the framework adds.
4
+
5
+ export const TIER_4_ERROR_STATUS = {
6
+ // @ultimat3/notify — five 500s and one 502, and the split is who failed.
7
+ //
8
+ // The five are the app's own declaration: a notifier with no channels, one channel named twice,
9
+ // a digest window on a bulk channel, a store nothing installed, and a fan-out past the per-run
10
+ // ceiling. Every `fix:` on those five names a code edit or a boot call, so nothing a caller
11
+ // sends changes any of them — `X_NOTIFY_FANOUT_TOO_WIDE` is the only one a request can even
12
+ // INFLUENCE (an action that notifies a whole org), and the repair is still `bulkChannel()` or a
13
+ // paged `backfill()`, never the request.
14
+ X_NOTIFY_CHANNELS_EMPTY: 500,
15
+ X_NOTIFY_CHANNEL_DUPLICATE: 500,
16
+ X_NOTIFY_FANOUT_TOO_WIDE: 500,
17
+ X_NOTIFY_STORE_MISSING: 500,
18
+ X_NOTIFY_DIGEST_UNSUPPORTED: 500,
19
+ // 502, and it is the one row on this table that answers for somebody else's server. This code
20
+ // WRAPS a provider rejection — `NotifyDeliveryFailedError` takes the caught value and renders it
21
+ // — so the thing that failed is the channel's upstream, not this process. It is thrown inside a
22
+ // job step today (`x jobs show <notifier> --json` is its own `fix:`), so nothing reaches a
23
+ // request and the number is unobservable either way; the row is chosen for the day that stops
24
+ // being true, and the asymmetry decides it. A wrong 502 costs nothing. A wrong 500 pages the
25
+ // on-call for an email provider's outage, because `stages.ts` reports every `status >= 500` to
26
+ // the error monitor — which is the failure this whole table exists to stop.
27
+ X_NOTIFY_DELIVERY_FAILED: 502,
28
+ // @ultimat3/ui — a form control whose `name` is not a usable field path. The owning slice argued
29
+ // for NO ROW, on the grounds that this is a render-time developer error that can never reach
30
+ // HTTP, and the argument is right about the code and wrong about the table.
31
+ //
32
+ // `scripts/error-map-backlog.ts` is the only "no row" this table has, and its own header says
33
+ // what an entry there means: "NOT a claim that the code can never cross HTTP … a claim that
34
+ // nobody has decided yet", with the ratchet promising only that the undecided set never grows.
35
+ // This code HAS been decided, so a pin would record the opposite of what is known and grow the
36
+ // one list that may not grow.
37
+ //
38
+ // So it takes the answer every other decided-and-unreachable code takes — `X_CORS_CONFIG_INVALID`,
39
+ // `X_ACTION_JOB_UNBRIDGED`, `X_RATE_LIMIT_NOT_SHARED`. The row is NOT a claim that it reaches a
40
+ // request. A code with no row already answers 500 (`DEFAULT_STATUS`); the row changes nothing at
41
+ // runtime and makes that answer a reviewed one instead of an accident, which is the whole reason
42
+ // this table is closed.
43
+ X_UI_FORM_PATH_INVALID: 500,
44
+ // @ultimat3/ui — `defineTheme()` refused a brand palette whose RESOLVED channels put a pairing
45
+ // below WCAG 2.2 AA. Declaration-time in the ordinary case, and it takes a row rather than a
46
+ // backlog entry for the one case that is not: an app that renders a per-tenant brand stylesheet
47
+ // during a request throws this ON that request. The author's fault and never the caller's, so
48
+ // 500 is the honest class, and a DECLARED 500 rather than an unclassified one — `toProblem`
49
+ // blanks the cause of an unclassified failure outside dev, which would hide the one thing that
50
+ // makes this error actionable: the role and the measured ratio.
51
+ X_UI_CONTRAST_INSUFFICIENT: 500,
52
+ // @ultimat3/ui — `<QrCode>` was handed a value past the encoder's version-3 ceiling. The value
53
+ // is the app's own (a short URL it minted), never the caller's, so 500 is the honest class —
54
+ // and a declared 500 for the contrast code's reason: `toProblem` blanks an unclassified
55
+ // failure's cause outside dev, and the cause is where the byte count and the ceiling are.
56
+ X_UI_QR_CAPACITY: 500,
57
+ // @ultimat3/render — an island handed props it cannot carry: an undeclared key, a value that is
58
+ // not JSON, or a bag over `ISLAND_PROPS_MAX_BYTES`. The author's fault and never the caller's,
59
+ // so 500 is the honest class — and it HAS to be a declared 500. Without a row the code was an
60
+ // unclassified failure, and `toProblem` blanks the cause of one of those outside dev
61
+ // (`isUnclassifiedFailure`): a 34-row catalog over the cap took a page down with a problem
62
+ // document that said "the details are in this process's logs" about an error whose whole
63
+ // value is the sentence naming the prop and its bytes. Measured on ai-maxxing, 2026-09-05.
64
+ X_ISLAND_PROPS_INVALID: 500,
65
+ // A loader answered `withStatus` with a 3xx, or a status outside 200–599. Raised INSIDE the
66
+ // request that ran the loader, so it needs a row for the same reason the line above does: an
67
+ // unclassified 500 blanks the sentence naming the status and the `redirect()` to use instead.
68
+ X_ROUTE_STATUS_INVALID: 500,
69
+ // `asset('assets/…')` named a file that is not on disk. The static build refuses it, but an `ssr`
70
+ // page calls `asset()` INSIDE the request it renders, so it reaches a caller there — a deploy
71
+ // defect, never the visitor's, hence 500, declared so the cause naming the missing file survives.
72
+ X_ASSET_MISSING: 500,
73
+ // @ultimat3/mail
74
+ // The deployment configured no transport. It reaches a caller only through an inline
75
+ // `send(…, { sync: true })` inside a request; the queued path dead-letters instead. A server-side
76
+ // configuration fault either way, so 500 and never a 4xx — nothing the caller sent is wrong, and
77
+ // this is exactly the condition somebody should be paged for.
78
+ X_MAIL_CREDENTIAL_MISSING: 500,
79
+ // @ultimat3/mcp — the one MCP code that is answered on a REQUEST rather than inside a JSON-RPC
80
+ // envelope, which is what the rest of that package's backlog group says about the others: the
81
+ // transport refused before dispatch, so there is no call to answer. 429 because
82
+ // `mcpHttpRoute` already builds that response by hand (`transport-http.ts`'s `throttled`), with
83
+ // `retry-after` beside it. The row is what keeps the two surfaces from disagreeing the day the
84
+ // MCP host is mounted inside this pipeline — a code that renders 429 on one and 500 on the other
85
+ // is exactly the split this table exists to prevent.
86
+ X_MCP_RATE_LIMITED: 429,
87
+ // The second MCP code answered on a request before dispatch, and 413 for the same reason the row
88
+ // above is 429: `transport-http.ts` already answers it with that status by hand.
89
+ X_MCP_BODY_TOO_LARGE: 413,
90
+ // @ultimat3/ai — a vector store was read with no tenant bound inside an org request
91
+ X_VECTOR_UNSCOPED: 500,
92
+ } satisfies Readonly<Record<string, number>>;