@ultimat3/http 23.0.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.
- package/CLAUDE.md +40 -14
- package/README.md +30 -9
- package/package.json +5 -5
- package/src/cache-policy.ts +10 -2
- package/src/config.ts +36 -4
- package/src/context.ts +1 -1
- package/src/csrf.ts +17 -8
- package/src/deadline.ts +8 -3
- package/src/error-facts.ts +18 -2
- package/src/error-map-http.ts +94 -0
- package/src/error-map-tier-0.ts +60 -0
- package/src/error-map-tier-1.ts +71 -0
- package/src/error-map-tier-2.ts +118 -0
- package/src/error-map-tier-3.ts +92 -0
- package/src/error-map-tier-4.ts +92 -0
- package/src/error-map.ts +27 -474
- package/src/error-page.ts +1 -2
- package/src/errors.ts +3 -3
- package/src/forwarded.ts +16 -5
- package/src/health-disclosure.ts +69 -0
- package/src/html-render.ts +3 -17
- package/src/index.ts +7 -8
- package/src/locale-prefix.ts +5 -1
- package/src/locale.ts +0 -27
- package/src/navigation.ts +13 -0
- package/src/overlay.ts +3 -1
- package/src/peer-identity.ts +10 -6
- package/src/pipeline.ts +20 -5
- package/src/preferences.ts +1 -1
- package/src/problem-meta.ts +7 -21
- package/src/rate-limit-peek.ts +32 -0
- package/src/rate-limit-postgres.ts +27 -18
- package/src/rate-limit-stage.ts +123 -0
- package/src/rate-limit.ts +18 -0
- package/src/request.ts +10 -3
- package/src/response.ts +8 -0
- package/src/router.ts +20 -7
- package/src/server.ts +27 -6
- package/src/stages.ts +35 -45
|
@@ -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>>;
|