@fuzdev/fuz_app 0.88.0 → 0.89.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.
@@ -84,10 +84,16 @@ export declare const DEFAULT_MAX_SESSIONS = 5;
84
84
  /** Default maximum API tokens per account. */
85
85
  export declare const DEFAULT_MAX_TOKENS = 10;
86
86
  /**
87
- * The `GET /api/account/status` route shape minus its handler — pure
88
- * hono-free data. `create_account_status_route_spec` spreads this and
89
- * attaches the live handler (which reads the account id off the request
90
- * context); surface generation spreads it with a stub handler.
87
+ * The `GET /status` route shape minus its handler — pure hono-free data.
88
+ * `create_account_status_route_spec` spreads this and attaches the live handler
89
+ * (which reads the account id off the request context); surface generation
90
+ * spreads it with a stub handler.
91
+ *
92
+ * The path is **relative** like the sibling account shapes (`/login`,
93
+ * `/verify`), so it composes under `prefix_route_specs('/api/account', …)` into
94
+ * `/api/account/status`. `create_account_route_specs` bundles it (so every
95
+ * account surface serves `/status`, matching the Rust `account_router`);
96
+ * mirror Rust by mounting it as part of the account family, not separately.
91
97
  */
92
98
  export declare const account_status_route_shape: {
93
99
  method: "GET";
@@ -1 +1 @@
1
- {"version":3,"file":"account_route_schema.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/account_route_schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAKtB,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,uBAAuB,CAAC;AAQrD,kFAAkF;AAClF,eAAO,MAAM,kBAAkB,WAAW,CAAC;AAC3C,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAEpE,4CAA4C;AAC5C,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;kBAI9B,CAAC;AACH,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAEtE,4EAA4E;AAC5E,eAAO,MAAM,iCAAiC;;;iBAG5C,CAAC;AACH,MAAM,MAAM,iCAAiC,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iCAAiC,CAAC,CAAC;AAElG,oFAAoF;AACpF,eAAO,MAAM,UAAU;;;kBAGrB,CAAC;AACH,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,UAAU,CAAC,CAAC;AAEpD,6EAA6E;AAC7E,eAAO,MAAM,WAAW;;kBAEtB,CAAC;AACH,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAC;AAEtD,2EAA2E;AAC3E,eAAO,MAAM,WAAW,WAAW,CAAC;AACpC,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAC;AAEtD,wFAAwF;AACxF,eAAO,MAAM,YAAY;;;kBAGvB,CAAC;AACH,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,YAAY,CAAC,CAAC;AAExD,sHAAsH;AACtH,eAAO,MAAM,mBAAmB;;;kBAG9B,CAAC;AACH,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAEtE,uGAAuG;AACvG,eAAO,MAAM,oBAAoB;;;;kBAI/B,CAAC;AACH,MAAM,MAAM,oBAAoB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAExE,4CAA4C;AAC5C,eAAO,MAAM,oBAAoB,IAAI,CAAC;AAEtC,8CAA8C;AAC9C,eAAO,MAAM,kBAAkB,KAAK,CAAC;AAErC;;;;;GAKG;AACH,eAAO,MAAM,0BAA0B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAUD,CAAC;AAEvC,8EAA8E;AAC9E,MAAM,WAAW,wBAAwB;IACxC,8FAA8F;IAC9F,0BAA0B,EAAE,OAAO,CAAC;CACpC;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,2BAA2B,GACvC,SAAS,wBAAwB,KAC/B,CACF,IAAI,CAAC,SAAS,EAAE,SAAS,CAAC,EAC1B,IAAI,CAAC,SAAS,EAAE,SAAS,CAAC,EAC1B,IAAI,CAAC,SAAS,EAAE,SAAS,CAAC,EAC1B,IAAI,CAAC,SAAS,EAAE,SAAS,CAAC,CAoD1B,CAAC"}
1
+ {"version":3,"file":"account_route_schema.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/account_route_schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAKtB,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,uBAAuB,CAAC;AAQrD,kFAAkF;AAClF,eAAO,MAAM,kBAAkB,WAAW,CAAC;AAC3C,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAEpE,4CAA4C;AAC5C,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;kBAI9B,CAAC;AACH,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAEtE,4EAA4E;AAC5E,eAAO,MAAM,iCAAiC;;;iBAG5C,CAAC;AACH,MAAM,MAAM,iCAAiC,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iCAAiC,CAAC,CAAC;AAElG,oFAAoF;AACpF,eAAO,MAAM,UAAU;;;kBAGrB,CAAC;AACH,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,UAAU,CAAC,CAAC;AAEpD,6EAA6E;AAC7E,eAAO,MAAM,WAAW;;kBAEtB,CAAC;AACH,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAC;AAEtD,2EAA2E;AAC3E,eAAO,MAAM,WAAW,WAAW,CAAC;AACpC,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAC;AAEtD,wFAAwF;AACxF,eAAO,MAAM,YAAY;;;kBAGvB,CAAC;AACH,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,YAAY,CAAC,CAAC;AAExD,sHAAsH;AACtH,eAAO,MAAM,mBAAmB;;;kBAG9B,CAAC;AACH,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAEtE,uGAAuG;AACvG,eAAO,MAAM,oBAAoB;;;;kBAI/B,CAAC;AACH,MAAM,MAAM,oBAAoB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAExE,4CAA4C;AAC5C,eAAO,MAAM,oBAAoB,IAAI,CAAC;AAEtC,8CAA8C;AAC9C,eAAO,MAAM,kBAAkB,KAAK,CAAC;AAErC;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,0BAA0B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAUD,CAAC;AAEvC,8EAA8E;AAC9E,MAAM,WAAW,wBAAwB;IACxC,8FAA8F;IAC9F,0BAA0B,EAAE,OAAO,CAAC;CACpC;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,2BAA2B,GACvC,SAAS,wBAAwB,KAC/B,CACF,IAAI,CAAC,SAAS,EAAE,SAAS,CAAC,EAC1B,IAAI,CAAC,SAAS,EAAE,SAAS,CAAC,EAC1B,IAAI,CAAC,SAAS,EAAE,SAAS,CAAC,EAC1B,IAAI,CAAC,SAAS,EAAE,SAAS,CAAC,CAoD1B,CAAC"}
@@ -61,14 +61,20 @@ export const DEFAULT_MAX_SESSIONS = 5;
61
61
  /** Default maximum API tokens per account. */
62
62
  export const DEFAULT_MAX_TOKENS = 10;
63
63
  /**
64
- * The `GET /api/account/status` route shape minus its handler — pure
65
- * hono-free data. `create_account_status_route_spec` spreads this and
66
- * attaches the live handler (which reads the account id off the request
67
- * context); surface generation spreads it with a stub handler.
64
+ * The `GET /status` route shape minus its handler — pure hono-free data.
65
+ * `create_account_status_route_spec` spreads this and attaches the live handler
66
+ * (which reads the account id off the request context); surface generation
67
+ * spreads it with a stub handler.
68
+ *
69
+ * The path is **relative** like the sibling account shapes (`/login`,
70
+ * `/verify`), so it composes under `prefix_route_specs('/api/account', …)` into
71
+ * `/api/account/status`. `create_account_route_specs` bundles it (so every
72
+ * account surface serves `/status`, matching the Rust `account_router`);
73
+ * mirror Rust by mounting it as part of the account family, not separately.
68
74
  */
69
75
  export const account_status_route_shape = {
70
76
  method: 'GET',
71
- path: '/api/account/status',
77
+ path: '/status',
72
78
  auth: { account: 'none', actor: 'none' },
73
79
  description: 'Current account info (unauthenticated: 401 with bootstrap status)',
74
80
  input: AccountStatusInput,
@@ -107,16 +107,30 @@ export interface AccountRouteOptions extends AuthSessionRouteOptions {
107
107
  * `audit.on_event_chain`) runs.
108
108
  */
109
109
  connection_closer?: ConnectionCloser | null;
110
+ /**
111
+ * Runtime bootstrap status for the bundled `GET /status` route — when
112
+ * `available`, its unauthenticated 401 carries `bootstrap_available: true`
113
+ * so a fresh frontend can route to the bootstrap flow. Pass
114
+ * `ctx.bootstrap_status` (the live `BootstrapStatus` ref) so the flag tracks
115
+ * the one-shot bootstrap completing. Omit when no bootstrap flow is wired —
116
+ * `/status` is still served, just without the flag.
117
+ */
118
+ bootstrap_status?: {
119
+ available: boolean;
120
+ };
110
121
  }
111
122
  /**
112
123
  * Create account route specs for session-based auth.
113
124
  *
114
- * The returned specs cover the three flows that stay REST after the RPC
115
- * migration (login, logout, password change). Self-service session/token
116
- * management and verify are on `auth/account_actions.ts`.
125
+ * The returned specs cover the REST flows that stay after the RPC migration:
126
+ * `/status` (account info + bootstrap availability), `/verify` (nginx
127
+ * `auth_request` shim), `/login`, `/logout`, `/password`. `/status` is bundled
128
+ * here (relative path, prefixed to `/api/account/status` by the caller) so
129
+ * every account surface serves it, matching the Rust `account_router`.
130
+ * Self-service session/token management is on `auth/account_actions.ts`.
117
131
  *
118
132
  * @param deps - stateless capabilities (keyring, password, log)
119
- * @param options - per-factory configuration (session_options, ip_rate_limiter, login_account_rate_limiter)
133
+ * @param options - per-factory configuration (session_options, ip_rate_limiter, login_account_rate_limiter, bootstrap_status)
120
134
  * @returns route specs (not yet applied to Hono)
121
135
  */
122
136
  export declare const create_account_route_specs: (deps: RouteFactoryDeps, options: AccountRouteOptions) => Array<RouteSpec>;
@@ -1 +1 @@
1
- {"version":3,"file":"account_routes.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/account_routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,qBAAqB,CAAC;AA4BxD,OAAO,EAAkB,KAAK,SAAS,EAAC,MAAM,uBAAuB,CAAC;AAEtE,OAAO,EAA+B,KAAK,WAAW,EAAC,MAAM,oBAAoB,CAAC;AAClF,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,WAAW,CAAC;AAChD,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,iCAAiC,CAAC;AAGtE;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,gCAAgC,GAAI,UAAU,oBAAoB,KAAG,SA4EhF,CAAC;AAEH,iDAAiD;AACjD,MAAM,WAAW,oBAAoB;IACpC,yDAAyD;IACzD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,8FAA8F;IAC9F,gBAAgB,CAAC,EAAE;QAAC,SAAS,EAAE,OAAO,CAAA;KAAC,CAAC;CACxC;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,2BAA2B,MAAM,CAAC;AAE/C;;;;;;GAMG;AACH,eAAO,MAAM,4BAA4B,KAAK,CAAC;AAQ/C;;;;;GAKG;AACH,MAAM,WAAW,uBAAuB;IACvC,eAAe,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IACxC,kFAAkF;IAClF,eAAe,EAAE,WAAW,GAAG,IAAI,CAAC;CACpC;AAED;;GAEG;AACH,MAAM,WAAW,mBAAoB,SAAQ,uBAAuB;IACnE,4FAA4F;IAC5F,0BAA0B,EAAE,WAAW,GAAG,IAAI,CAAC;IAC/C,2FAA2F;IAC3F,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B;;;;OAIG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B;;;OAGG;IACH,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B;;;;;;;OAOG;IACH,iBAAiB,CAAC,EAAE,gBAAgB,GAAG,IAAI,CAAC;CAC5C;AAID;;;;;;;;;;GAUG;AACH,eAAO,MAAM,0BAA0B,GACtC,MAAM,gBAAgB,EACtB,SAAS,mBAAmB,KAC1B,KAAK,CAAC,SAAS,CAkRjB,CAAC"}
1
+ {"version":3,"file":"account_routes.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/account_routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,qBAAqB,CAAC;AA4BxD,OAAO,EAAkB,KAAK,SAAS,EAAC,MAAM,uBAAuB,CAAC;AAEtE,OAAO,EAA+B,KAAK,WAAW,EAAC,MAAM,oBAAoB,CAAC;AAClF,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,WAAW,CAAC;AAChD,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,iCAAiC,CAAC;AAGtE;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,gCAAgC,GAAI,UAAU,oBAAoB,KAAG,SA4EhF,CAAC;AAEH,iDAAiD;AACjD,MAAM,WAAW,oBAAoB;IACpC,yDAAyD;IACzD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,8FAA8F;IAC9F,gBAAgB,CAAC,EAAE;QAAC,SAAS,EAAE,OAAO,CAAA;KAAC,CAAC;CACxC;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,2BAA2B,MAAM,CAAC;AAE/C;;;;;;GAMG;AACH,eAAO,MAAM,4BAA4B,KAAK,CAAC;AAQ/C;;;;;GAKG;AACH,MAAM,WAAW,uBAAuB;IACvC,eAAe,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IACxC,kFAAkF;IAClF,eAAe,EAAE,WAAW,GAAG,IAAI,CAAC;CACpC;AAED;;GAEG;AACH,MAAM,WAAW,mBAAoB,SAAQ,uBAAuB;IACnE,4FAA4F;IAC5F,0BAA0B,EAAE,WAAW,GAAG,IAAI,CAAC;IAC/C,2FAA2F;IAC3F,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B;;;;OAIG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B;;;OAGG;IACH,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B;;;;;;;OAOG;IACH,iBAAiB,CAAC,EAAE,gBAAgB,GAAG,IAAI,CAAC;IAC5C;;;;;;;OAOG;IACH,gBAAgB,CAAC,EAAE;QAAC,SAAS,EAAE,OAAO,CAAA;KAAC,CAAC;CACxC;AAID;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,0BAA0B,GACtC,MAAM,gBAAgB,EACtB,SAAS,mBAAmB,KAC1B,KAAK,CAAC,SAAS,CAwRjB,CAAC"}
@@ -147,21 +147,29 @@ const login_fail_delay = (floor_ms, jitter_ms) => {
147
147
  /**
148
148
  * Create account route specs for session-based auth.
149
149
  *
150
- * The returned specs cover the three flows that stay REST after the RPC
151
- * migration (login, logout, password change). Self-service session/token
152
- * management and verify are on `auth/account_actions.ts`.
150
+ * The returned specs cover the REST flows that stay after the RPC migration:
151
+ * `/status` (account info + bootstrap availability), `/verify` (nginx
152
+ * `auth_request` shim), `/login`, `/logout`, `/password`. `/status` is bundled
153
+ * here (relative path, prefixed to `/api/account/status` by the caller) so
154
+ * every account surface serves it, matching the Rust `account_router`.
155
+ * Self-service session/token management is on `auth/account_actions.ts`.
153
156
  *
154
157
  * @param deps - stateless capabilities (keyring, password, log)
155
- * @param options - per-factory configuration (session_options, ip_rate_limiter, login_account_rate_limiter)
158
+ * @param options - per-factory configuration (session_options, ip_rate_limiter, login_account_rate_limiter, bootstrap_status)
156
159
  * @returns route specs (not yet applied to Hono)
157
160
  */
158
161
  export const create_account_route_specs = (deps, options) => {
159
162
  const { keyring, password } = deps;
160
- const { session_options, ip_rate_limiter, login_account_rate_limiter, max_sessions = DEFAULT_MAX_SESSIONS, login_fail_floor_ms = DEFAULT_LOGIN_FAIL_FLOOR_MS, login_fail_jitter_ms = DEFAULT_LOGIN_FAIL_JITTER_MS, connection_closer = null, } = options;
163
+ const { session_options, ip_rate_limiter, login_account_rate_limiter, max_sessions = DEFAULT_MAX_SESSIONS, login_fail_floor_ms = DEFAULT_LOGIN_FAIL_FLOOR_MS, login_fail_jitter_ms = DEFAULT_LOGIN_FAIL_JITTER_MS, connection_closer = null, bootstrap_status, } = options;
161
164
  const [verify_shape, login_shape, logout_shape, password_shape] = create_account_route_shapes({
162
165
  login_account_rate_limited: login_account_rate_limiter !== null,
163
166
  });
164
167
  return [
168
+ // `/status` is bundled into the account family (relative path, prefixed
169
+ // to `/api/account/status` by the caller) so every account surface serves
170
+ // it — matching the Rust `account_router`. The standalone
171
+ // `create_account_status_route_spec` stays the building block.
172
+ create_account_status_route_spec({ bootstrap_status }),
165
173
  {
166
174
  ...verify_shape,
167
175
  handler: (c) => {
@@ -858,7 +858,7 @@ source of truth for wire-shape conformance.
858
858
  - `testing/cross_backend/capabilities.ts` — `BackendCapabilities` vocabulary
859
859
  (`bearer_auth` / `trusted_proxy` / `login_rate_limit` / `ws` / `sse` /
860
860
  `cell_crud` / `cell_relations` / `account_lifecycle` / `fact_serving` /
861
- `ready`),
861
+ `ready` / `account_status` / `oversized_reject_closes_connection`),
862
862
  `test_if(cond, name, fn)`
863
863
  for capability-gated cases, and `in_process_capabilities` preset. `cell_crud`
864
864
  gates the CRUD parity suite, `cell_relations` the relation / ACL / audit
@@ -875,6 +875,14 @@ source of truth for wire-shape conformance.
875
875
  (anonymous `GET /ready` → `200 {ready: true}` on a clean spine bootstrap);
876
876
  like cells/sse the `/ready` deploy gate stays off the declared surface, `true`
877
877
  on every spine that live-mounts it over the shared `expected_schema.json`.
878
+ `account_status` gates the integration suite's `account status response body`
879
+ case — `GET /api/account/status` is bundled into `create_account_route_specs`,
880
+ so every spine serves it (`true`); the gate fails loud if a declaring backend
881
+ doesn't mount it (replacing the old runtime 404-sniff-skip).
882
+ `oversized_reject_closes_connection` gates the strong half of the body-size
883
+ smuggling probe (`true` Node/Deno/Rust — they close on an oversized reject;
884
+ `false` Bun — `Bun.serve` drains + keepalives but frames correctly, so the
885
+ suite's no-desync half still runs).
878
886
 
879
887
  ### `cross_backend/standard.ts` — `describe_standard_cross_process_tests`
880
888
 
@@ -5,6 +5,18 @@ export interface BodySizeSmugglingCrossTestOptions {
5
5
  readonly base_url: string;
6
6
  /** RPC endpoint path to target. Default `/api/rpc`. */
7
7
  readonly rpc_path?: string;
8
+ /**
9
+ * Whether the backend closes the connection on an oversized-body reject
10
+ * without reading the body (`capabilities.oversized_reject_closes_connection`).
11
+ * `true` (default) demands the strong posture — the pipelined GET is never
12
+ * reached, so **at most one** response comes back. `false` (Bun) relaxes to
13
+ * the no-desync property: the body is drained on `Content-Length` and the
14
+ * pipelined GET is framed correctly, so **at most two** responses come back
15
+ * and the body bytes are never reparsed as a request. Default `true` so a
16
+ * consumer that forgets to declare the flag fails loud rather than silently
17
+ * accepting a drain.
18
+ */
19
+ readonly closes_connection?: boolean;
8
20
  }
9
21
  export declare const describe_body_size_smuggling_cross_tests: (options: BodySizeSmugglingCrossTestOptions) => void;
10
22
  //# sourceMappingURL=body_size_smuggling.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"body_size_smuggling.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/body_size_smuggling.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAkD9B,4EAA4E;AAC5E,MAAM,WAAW,iCAAiC;IACjD,mFAAmF;IACnF,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,uDAAuD;IACvD,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;CAC3B;AAqDD,eAAO,MAAM,wCAAwC,GACpD,SAAS,iCAAiC,KACxC,IA4DF,CAAC"}
1
+ {"version":3,"file":"body_size_smuggling.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/body_size_smuggling.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AA0D9B,4EAA4E;AAC5E,MAAM,WAAW,iCAAiC;IACjD,mFAAmF;IACnF,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,uDAAuD;IACvD,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,iBAAiB,CAAC,EAAE,OAAO,CAAC;CACrC;AAqDD,eAAO,MAAM,wCAAwC,GACpD,SAAS,iCAAiC,KACxC,IAiFF,CAAC"}
@@ -4,37 +4,45 @@ import '../assert_dev_env.js';
4
4
  * handling — the security sibling of `body_size.ts`.
5
5
  *
6
6
  * When the server caps the request body it answers `413` on the
7
- * `Content-Length` header and closes the connection *without reading the
8
- * oversized body*. That close is load-bearing: HTTP/1.1 forbids reusing a
9
- * keep-alive connection whose request body wasn't consumed, because the unread
10
- * body bytes would otherwise be parsed as the start of the next request — a
11
- * classic request-smuggling vector. This suite proves the mitigation holds end
12
- * to end by **pipelining**: it opens a raw TCP socket and sends, in one write,
13
- * an oversized `POST` immediately followed by a second `GET` request. A correct
14
- * server rejects the POST with `413` and never processes the trailing GET (the
15
- * connection closes with the GET bytes unconsumed); a vulnerable one would
16
- * drain past the body and answer the smuggled GET too. The assertion is
17
- * therefore "**at most one** HTTP response comes back" a *second* response is
18
- * the smuggle. It's `<= 1` rather than "exactly the 413" because the impls close
19
- * differently at the TCP level (node-server graceful close delivers the 413
20
- * first; hyper's RST can drop the in-flight 413 before the client reads it), so
21
- * demanding a cleanly-read 413 would be flaky; the 413-ness itself is pinned
22
- * reliably over `fetch` by `describe_body_size_cross_tests`. A **positive
23
- * control** (two pipelined requests >= 2 responses) proves a second response
24
- * *would* be seen if the trailing request were processed without it the
25
- * `<= 1` assertion would be vacuous on a server that never reuses connections,
26
- * and it also proves the response counter isn't undercounting.
7
+ * `Content-Length` header. The strong (defense-in-depth) posture is to close
8
+ * the connection *without reading the oversized body*: HTTP/1.1 forbids reusing
9
+ * a keep-alive connection whose request body wasn't consumed, because unread
10
+ * body bytes would be parsed as the start of the next request — a classic
11
+ * request-smuggling vector. This suite probes the boundary by **pipelining**:
12
+ * it opens a raw TCP socket and sends, in one write, an oversized `POST`
13
+ * immediately followed by a second `GET`. The assertion forks on the backend's
14
+ * declared `oversized_reject_closes_connection` capability:
15
+ *
16
+ * - **Closes (Node / Deno / hyper)** — the reject closes the socket with the
17
+ * GET bytes unconsumed, so **at most one** response comes back. `<= 1` rather
18
+ * than "exactly the 413" because the impls close differently at the TCP level
19
+ * (node-server graceful close delivers the 413 first; hyper's RST can drop the
20
+ * in-flight 413 before the client reads it), so demanding a cleanly-read 413
21
+ * would be flaky.
22
+ * - **Drains + keepalives (Bun)** — `Bun.serve` reads the full declared
23
+ * `Content-Length` body and answers the *correctly-framed* pipelined GET, so
24
+ * **two** responses come back. This is **not** a smuggle: the GET is delimited
25
+ * by the body's `Content-Length`, not the unread body reinterpreted as a
26
+ * request Bun answers it with a clean `400` (`missing method`), not the `x`
27
+ * body bytes reparsed. The security property asserted here is **no desync**
28
+ * (`<= 2`): a real desync would reframe the 1 MiB of `x` into bogus request
29
+ * lines and push the count past two.
30
+ *
31
+ * Either way the oversized body is rejected *with* a 413 — pinned reliably over
32
+ * `fetch` by `describe_body_size_cross_tests`; this test owns only the
33
+ * connection-handling half. A **positive control** (two pipelined requests →
34
+ * `>= 2` responses) proves a second response *would* be seen if a trailing
35
+ * request were processed — without it the close-posture `<= 1` would be vacuous
36
+ * on a server that never reuses connections — and that the counter isn't
37
+ * undercounting.
27
38
  *
28
39
  * Raw-socket by necessity (the `FetchTransport` can't pipeline two requests on
29
40
  * one connection), so — unlike `body_size.ts` — this is **cross-process only**
30
- * (no in-process leg; there is no socket in-process) and ungated (the limit is
31
- * on every spine). Robust by construction: it counts responses until the
32
- * socket closes or a short read timeout, so a server that closes (the expected
33
- * path) and one that merely holds the connection open without smuggling both
34
- * read as one response; only an actual second response fails.
41
+ * (no in-process leg; there is no socket in-process). The connection-close half
42
+ * is capability-gated; the no-desync half holds on every spine.
35
43
  *
36
- * Cited property: `docs/security.md` §"Body Size Limiting" (connection close on
37
- * oversized reject).
44
+ * Cited property: `docs/security.md` §"Body Size Limiting" (connection handling
45
+ * on oversized reject).
38
46
  *
39
47
  * `$lib`-free by contract (relative + `node:` specifiers only).
40
48
  *
@@ -90,6 +98,7 @@ const count_responses = (raw) => (raw.match(/HTTP\/1\.[01] \d{3}/g) ?? []).lengt
90
98
  export const describe_body_size_smuggling_cross_tests = (options) => {
91
99
  const { base_url } = options;
92
100
  const rpc_path = options.rpc_path ?? SPINE_RPC_PATH;
101
+ const closes_connection = options.closes_connection ?? true;
93
102
  const host = new URL(base_url).host;
94
103
  describe('body-size limit — request-smuggling resistance', () => {
95
104
  // Positive control: prove the server returns >1 response on a single
@@ -118,21 +127,39 @@ export const describe_body_size_smuggling_cross_tests = (options) => {
118
127
  'x'.repeat(oversized_len) +
119
128
  `GET ${rpc_path} HTTP/1.1\r\nHost: ${host}\r\n\r\n`;
120
129
  const response = await send_raw(base_url, payload, 2000);
121
- // The security property is "the pipelined GET is not processed", i.e.
122
- // **at most one** response comes back (the 413, or none if the close
123
- // raced the read). A *second* response is the smuggle. We assert `<= 1`
124
- // rather than "exactly the 413" because the two impls close the
125
- // connection differently at the TCP level — node-server closes
126
- // gracefully (the 413 is delivered first), hyper sends an RST that can
127
- // drop the in-flight 413 before the client reads it — and demanding a
128
- // cleanly-read 413 here would be flaky. That the oversized body is
129
- // rejected *with* a 413 is pinned reliably (over `fetch`) by
130
- // `describe_body_size_cross_tests`; this test owns only the no-smuggle
131
- // half. The control above proves a second response *would* be seen if
132
- // the GET were processed, so `<= 1` is a real signal, not a vacuous one.
133
130
  const n = count_responses(response);
134
- assert.ok(n <= 1, `expected at most one response (the GET must not be smuggled in off the ` +
135
- `unread body); a second response means it was. Saw ${n}. Raw head: ${response.slice(0, 120)}`);
131
+ if (closes_connection) {
132
+ // Strong posture (Node / Deno / hyper): the reject closes the
133
+ // connection *without reading the body*, so the pipelined GET is
134
+ // never reached — **at most one** response comes back (the 413, or
135
+ // none if the close raced the read). A *second* response would mean
136
+ // the body was drained and the GET processed. We assert `<= 1` rather
137
+ // than "exactly the 413" because the impls close differently at the
138
+ // TCP level — node-server closes gracefully (413 delivered first),
139
+ // hyper sends an RST that can drop the in-flight 413 before the client
140
+ // reads it — and demanding a cleanly-read 413 would be flaky. The
141
+ // 413-ness itself is pinned reliably (over `fetch`) by
142
+ // `describe_body_size_cross_tests`. The control above proves a second
143
+ // response *would* be seen if the GET were processed, so `<= 1` is a
144
+ // real signal, not vacuous.
145
+ assert.ok(n <= 1, `expected at most one response (oversized reject closes the connection; the ` +
146
+ `pipelined GET must not be reached). Saw ${n}. Raw head: ${response.slice(0, 120)}`);
147
+ }
148
+ else {
149
+ // Drain-and-keepalive posture (Bun): `Bun.serve` reads the full
150
+ // declared `Content-Length` body and keeps the socket alive, so it
151
+ // answers the *correctly-framed* pipelined GET — **two** responses
152
+ // (the 413 + a well-formed reply to the GET). This is not a smuggle:
153
+ // the GET is delimited by the body's `Content-Length`, not the unread
154
+ // body reinterpreted as request bytes. The security property here is
155
+ // **no desync** — the body bytes are never reparsed as request(s). A
156
+ // real desync would reframe the 1 MiB of `x` into bogus request
157
+ // lines, pushing the count past two; `<= 2` is the no-desync bound,
158
+ // and the control above proves the counter isn't undercounting.
159
+ assert.ok(n <= 2, `expected at most two responses (the 413 + one framed reply to the ` +
160
+ `pipelined GET); more means the oversized body was reparsed as requests ` +
161
+ `(a desync). Saw ${n}. Raw head: ${response.slice(0, 120)}`);
162
+ }
136
163
  });
137
164
  });
138
165
  };
@@ -99,6 +99,35 @@ export interface BackendCapabilities {
99
99
  * readiness parity coverage. The drift → `503` path stays per-impl unit tests.
100
100
  */
101
101
  readonly ready: boolean;
102
+ /**
103
+ * The account surface serves `GET /api/account/status` (account info +
104
+ * `bootstrap_available` flag). Bundled into `create_account_route_specs`, so
105
+ * any backend mounting the account routes serves it — `true` for every
106
+ * spine. Gates the `account status response body` case in
107
+ * `describe_standard_integration_tests`: when `true` the case asserts the
108
+ * route is present (fail-loud on 404, no silent skip); when `false` it skips
109
+ * explicitly (a backend that deliberately omits the route).
110
+ */
111
+ readonly account_status: boolean;
112
+ /**
113
+ * On an oversized-body `413` reject the backend **closes the connection
114
+ * without reading the body** (the defense-in-depth posture), rather than
115
+ * draining the declared `Content-Length` and keeping the socket alive.
116
+ * Gates the strong half of `describe_body_size_smuggling_cross_tests`: when
117
+ * `true`, the pipelined GET is never reached (at most one response); when
118
+ * `false`, the suite instead asserts the weaker but still-load-bearing
119
+ * no-desync property (the body is framed on `Content-Length`, not reparsed
120
+ * as request bytes).
121
+ *
122
+ * `true` for the Node / Deno (`@hono/node-server` graceful close) and Rust
123
+ * (hyper RST) backends; `false` for Bun — `Bun.serve` reads the full body
124
+ * and processes the correctly-framed pipelined request even when the `413`
125
+ * carries `Connection: close`. Bun is not insecure (no desync — it answers
126
+ * the cleanly-delimited GET with a proper `400`); the flag records the
127
+ * connection-handling divergence so the suite stays green without losing the
128
+ * smuggle detector. See `docs/security.md` §"Body Size Limiting".
129
+ */
130
+ readonly oversized_reject_closes_connection: boolean;
102
131
  }
103
132
  /**
104
133
  * Capability declarations for the in-process Hono transport. Every flag
@@ -1 +1 @@
1
- {"version":3,"file":"capabilities.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/capabilities.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAmB9B;;;;GAIG;AACH,MAAM,WAAW,mBAAmB;IACnC;;;;;;;;;OASG;IACH,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC;IAC9B;;;;;;;;OAQG;IACH,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAC;IAChC;;;;;;;OAOG;IACH,QAAQ,CAAC,gBAAgB,EAAE,OAAO,CAAC;IACnC;;;;OAIG;IACH,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAC;IACrB;;;;;OAKG;IACH,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC;IACtB;;;;;;;OAOG;IACH,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,cAAc,EAAE,OAAO,CAAC;IACjC;;;;;;;;OAQG;IACH,QAAQ,CAAC,iBAAiB,EAAE,OAAO,CAAC;IACpC;;;;;;;;OAQG;IACH,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC;IAC/B;;;;;;;;OAQG;IACH,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;CACxB;AAED;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,EAAE,mBAWpC,CAAC;AAEH;;;;;;;;GAQG;AACH,eAAO,MAAM,OAAO,GAAI,MAAM,OAAO,EAAE,MAAM,MAAM,EAAE,IAAI,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,KAAG,IAMrF,CAAC"}
1
+ {"version":3,"file":"capabilities.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/capabilities.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAgC9B;;;;GAIG;AACH,MAAM,WAAW,mBAAmB;IACnC;;;;;;;;;OASG;IACH,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC;IAC9B;;;;;;;;OAQG;IACH,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAC;IAChC;;;;;;;OAOG;IACH,QAAQ,CAAC,gBAAgB,EAAE,OAAO,CAAC;IACnC;;;;OAIG;IACH,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAC;IACrB;;;;;OAKG;IACH,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC;IACtB;;;;;;;OAOG;IACH,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,cAAc,EAAE,OAAO,CAAC;IACjC;;;;;;;;OAQG;IACH,QAAQ,CAAC,iBAAiB,EAAE,OAAO,CAAC;IACpC;;;;;;;;OAQG;IACH,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC;IAC/B;;;;;;;;OAQG;IACH,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB;;;;;;;;OAQG;IACH,QAAQ,CAAC,cAAc,EAAE,OAAO,CAAC;IACjC;;;;;;;;;;;;;;;;;OAiBG;IACH,QAAQ,CAAC,kCAAkC,EAAE,OAAO,CAAC;CACrD;AAED;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,EAAE,mBAapC,CAAC;AAEH;;;;;;;;GAQG;AACH,eAAO,MAAM,OAAO,GAAI,MAAM,OAAO,EAAE,MAAM,MAAM,EAAE,IAAI,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,KAAG,IAMrF,CAAC"}
@@ -11,6 +11,19 @@ import '../assert_dev_env.js';
11
11
  * capability `true` (see `in_process_capabilities`). Cross-process
12
12
  * backends opt in per-flag on their `BackendConfig`.
13
13
  *
14
+ * **Where the per-backend declarations live** (this file owns only the
15
+ * vocabulary + the in-process preset):
16
+ *
17
+ * - `in_process_capabilities` — here; every flag `true`.
18
+ * - `ts_default_capabilities` / `rust_default_capabilities` — consumer-facing
19
+ * family defaults, in `default_backend_configs.ts` (full literals, so adding
20
+ * a capability is a compile error until each family declares it).
21
+ * - `ts_spine_capabilities` / `ts_spine_bun_capabilities` — fuz_app's own TS
22
+ * spine presets, in `ts_spine_backend_config.ts` (deltas off the family
23
+ * default; Bun flips `oversized_reject_closes_connection`).
24
+ * - `rust_spine_stub_capabilities` — fuz_app's Rust spine-stub preset, in
25
+ * `rust_spine_stub_backend_config.ts` (delta off the rust family default).
26
+ *
14
27
  * @module
15
28
  */
16
29
  import { test } from 'vitest';
@@ -31,6 +44,8 @@ export const in_process_capabilities = Object.freeze({
31
44
  account_lifecycle: true,
32
45
  fact_serving: true,
33
46
  ready: true,
47
+ account_status: true,
48
+ oversized_reject_closes_connection: true,
34
49
  });
35
50
  /**
36
51
  * Conditional `test()` wrapper — registers a vitest case only when
@@ -1 +1 @@
1
- {"version":3,"file":"default_backend_configs.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/default_backend_configs.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAE9B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEH,OAAO,KAAK,EAAC,sBAAsB,EAAE,aAAa,EAAC,MAAM,qBAAqB,CAAC;AAC/E,OAAO,KAAK,EAAC,mBAAmB,EAAC,MAAM,mBAAmB,CAAC;AAC3D,OAAO,EAA2B,KAAK,gBAAgB,EAAC,MAAM,+BAA+B,CAAC;AAQ9F;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,EAAE,mBAapC,CAAC;AAEH;;;;;GAKG;AACH,eAAO,MAAM,yBAAyB,EAAE,mBAatC,CAAC;AAeH,MAAM,WAAW,iCAAiC;IACjD,gFAAgF;IAChF,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,sCAAsC;IACtC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,4DAA4D;IAC5D,QAAQ,CAAC,aAAa,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IAC9C,oDAAoD;IACpD,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B,iEAAiE;IACjE,QAAQ,CAAC,SAAS,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACtD,6CAA6C;IAC7C,QAAQ,CAAC,YAAY,CAAC,EAAE,mBAAmB,CAAC;IAC5C,wEAAwE;IACxE,QAAQ,CAAC,KAAK,CAAC,EAAE,gBAAgB,CAAC;IAClC,sEAAsE;IACtE,QAAQ,CAAC,mBAAmB,CAAC,EAAE,OAAO,CAAC,sBAAsB,CAAC,CAAC;IAC/D;;;;OAIG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B;;;;;;OAMG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED;;;;;GAKG;AACH,eAAO,MAAM,8BAA8B,GAC1C,MAAM,iCAAiC,KACrC,aAoCF,CAAC;AAEF,MAAM,WAAW,mCAAmC;IACnD,gFAAgF;IAChF,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,sCAAsC;IACtC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,4DAA4D;IAC5D,QAAQ,CAAC,aAAa,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IAC9C;;;;OAIG;IACH,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,iEAAiE;IACjE,QAAQ,CAAC,SAAS,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACtD,+CAA+C;IAC/C,QAAQ,CAAC,YAAY,CAAC,EAAE,mBAAmB,CAAC;IAC5C,wEAAwE;IACxE,QAAQ,CAAC,KAAK,CAAC,EAAE,gBAAgB,CAAC;IAClC,sEAAsE;IACtE,QAAQ,CAAC,mBAAmB,CAAC,EAAE,OAAO,CAAC,sBAAsB,CAAC,CAAC;IAC/D;;;;OAIG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B;;;;OAIG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B;;;OAGG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED;;;;;GAKG;AACH,eAAO,MAAM,gCAAgC,GAC5C,MAAM,mCAAmC,KACvC,aA4CF,CAAC"}
1
+ {"version":3,"file":"default_backend_configs.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/default_backend_configs.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAE9B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEH,OAAO,KAAK,EAAC,sBAAsB,EAAE,aAAa,EAAC,MAAM,qBAAqB,CAAC;AAC/E,OAAO,KAAK,EAAC,mBAAmB,EAAC,MAAM,mBAAmB,CAAC;AAC3D,OAAO,EAA2B,KAAK,gBAAgB,EAAC,MAAM,+BAA+B,CAAC;AAQ9F;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,EAAE,mBAoBpC,CAAC;AAEH;;;;;GAKG;AACH,eAAO,MAAM,yBAAyB,EAAE,mBAmBtC,CAAC;AAeH,MAAM,WAAW,iCAAiC;IACjD,gFAAgF;IAChF,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,sCAAsC;IACtC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,4DAA4D;IAC5D,QAAQ,CAAC,aAAa,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IAC9C,oDAAoD;IACpD,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B,iEAAiE;IACjE,QAAQ,CAAC,SAAS,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACtD,6CAA6C;IAC7C,QAAQ,CAAC,YAAY,CAAC,EAAE,mBAAmB,CAAC;IAC5C,wEAAwE;IACxE,QAAQ,CAAC,KAAK,CAAC,EAAE,gBAAgB,CAAC;IAClC,sEAAsE;IACtE,QAAQ,CAAC,mBAAmB,CAAC,EAAE,OAAO,CAAC,sBAAsB,CAAC,CAAC;IAC/D;;;;OAIG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B;;;;;;OAMG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED;;;;;GAKG;AACH,eAAO,MAAM,8BAA8B,GAC1C,MAAM,iCAAiC,KACrC,aAoCF,CAAC;AAEF,MAAM,WAAW,mCAAmC;IACnD,gFAAgF;IAChF,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,sCAAsC;IACtC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,4DAA4D;IAC5D,QAAQ,CAAC,aAAa,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IAC9C;;;;OAIG;IACH,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,iEAAiE;IACjE,QAAQ,CAAC,SAAS,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACtD,+CAA+C;IAC/C,QAAQ,CAAC,YAAY,CAAC,EAAE,mBAAmB,CAAC;IAC5C,wEAAwE;IACxE,QAAQ,CAAC,KAAK,CAAC,EAAE,gBAAgB,CAAC;IAClC,sEAAsE;IACtE,QAAQ,CAAC,mBAAmB,CAAC,EAAE,OAAO,CAAC,sBAAsB,CAAC,CAAC;IAC/D;;;;OAIG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B;;;;OAIG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B;;;OAGG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED;;;;;GAKG;AACH,eAAO,MAAM,gCAAgC,GAC5C,MAAM,mCAAmC,KACvC,aA4CF,CAAC"}
@@ -20,6 +20,13 @@ export const ts_default_capabilities = Object.freeze({
20
20
  // Off by default like `sse` — a generic TS consumer backend may not mount
21
21
  // `/ready`. fuz_app's own spine configs (`ts_spine_*`) opt in.
22
22
  ready: false,
23
+ // `GET /api/account/status` is bundled into `create_account_route_specs`, so
24
+ // every TS backend mounting account routes serves it.
25
+ account_status: true,
26
+ // Node/Deno close the socket on an oversized-body reject (the default
27
+ // posture). A Bun-served consumer overrides to `false` (see the bun spine
28
+ // config) — fail-loud rather than silently skipping the smuggle detector.
29
+ oversized_reject_closes_connection: true,
23
30
  });
24
31
  /**
25
32
  * Capabilities for the Rust family. Adds `trusted_proxy: true` (the
@@ -40,6 +47,12 @@ export const rust_default_capabilities = Object.freeze({
40
47
  // Off by default like `sse`; the spine-stub preset opts in (it mounts
41
48
  // `/ready` over the env-supplied fixture path).
42
49
  ready: false,
50
+ // The Rust `account_router` bundles `/status` into the account routes, so
51
+ // every Rust spine serving the account surface serves it.
52
+ account_status: true,
53
+ // hyper sends an RST on the oversized-body reject — the connection closes
54
+ // and the pipelined request is never reached.
55
+ oversized_reject_closes_connection: true,
43
56
  });
44
57
  /** Bootstrap block built from the default secrets + supplied paths. */
45
58
  const build_default_bootstrap = (paths, overrides) => ({
@@ -1 +1 @@
1
- {"version":3,"file":"default_spine_surface.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/default_spine_surface.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAE9B;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,yBAAyB,CAAC;AAIpD,OAAO,KAAK,EAAC,kBAAkB,EAAC,MAAM,8CAA8C,CAAC;AACrF,OAAO,EAAqB,KAAK,gBAAgB,EAAC,MAAM,2BAA2B,CAAC;AACpF,OAAO,EAAwB,KAAK,cAAc,EAAC,MAAM,8BAA8B,CAAC;AAIxF,OAAO,EAAqB,KAAK,SAAS,EAAC,MAAM,0BAA0B,CAAC;AAC5E,OAAO,KAAK,EAAC,cAAc,EAAE,eAAe,EAAC,MAAM,uBAAuB,CAAC;AAC3E,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,oCAAoC,CAAC;AAGzE;;;GAGG;AACH,eAAO,MAAM,qBAAqB,EAAE,cAAc,CAAC,MAAM,CAAwC,CAAC;AAElG;;;;;;GAMG;AACH,eAAO,MAAM,sBAAsB,gBAAgB,CAAC;AAEpD;;;;;;GAMG;AACH,eAAO,MAAM,WAAW,EAAE,gBAExB,CAAC;AAEH,iEAAiE;AACjE,eAAO,MAAM,cAAc,aAAa,CAAC;AAEzC;;;;;;GAMG;AACH,eAAO,MAAM,cAAc,4BAA4B,CAAC;AAExD,+CAA+C;AAC/C,MAAM,WAAW,wBAAwB;IACxC;;;;;;;;;;;;;;;OAeG;IACH,QAAQ,CAAC,mBAAmB,CAAC,EAAE,kBAAkB,GAAG,IAAI,CAAC;CACzD;AAED;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,mBAAmB,GAC/B,KAAK,gBAAgB,EACrB,UAAU,wBAAwB,KAChC,KAAK,CAAC,eAAe,CAQvB,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,wBAAwB,GAAI,KAAK,gBAAgB,KAAG,KAAK,CAAC,SAAS,CAiB/E,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,yBAAyB,EAAE,GAAwD,CAAC;AAEjG;;;;;;;;;GASG;AACH,eAAO,MAAM,6BAA6B,GAAI,MAAM,MAAM,KAAG,SAC6B,CAAC;AAE3F;;;;;;;;GAQG;AACH,eAAO,MAAM,yBAAyB,QAAO,cAM1C,CAAC"}
1
+ {"version":3,"file":"default_spine_surface.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/default_spine_surface.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAE9B;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,yBAAyB,CAAC;AAIpD,OAAO,KAAK,EAAC,kBAAkB,EAAC,MAAM,8CAA8C,CAAC;AACrF,OAAO,EAAqB,KAAK,gBAAgB,EAAC,MAAM,2BAA2B,CAAC;AACpF,OAAO,EAAwB,KAAK,cAAc,EAAC,MAAM,8BAA8B,CAAC;AAIxF,OAAO,EAAqB,KAAK,SAAS,EAAC,MAAM,0BAA0B,CAAC;AAC5E,OAAO,KAAK,EAAC,cAAc,EAAE,eAAe,EAAC,MAAM,uBAAuB,CAAC;AAC3E,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,oCAAoC,CAAC;AAGzE;;;GAGG;AACH,eAAO,MAAM,qBAAqB,EAAE,cAAc,CAAC,MAAM,CAAwC,CAAC;AAElG;;;;;;GAMG;AACH,eAAO,MAAM,sBAAsB,gBAAgB,CAAC;AAEpD;;;;;;GAMG;AACH,eAAO,MAAM,WAAW,EAAE,gBAExB,CAAC;AAEH,iEAAiE;AACjE,eAAO,MAAM,cAAc,aAAa,CAAC;AAEzC;;;;;;GAMG;AACH,eAAO,MAAM,cAAc,4BAA4B,CAAC;AAExD,+CAA+C;AAC/C,MAAM,WAAW,wBAAwB;IACxC;;;;;;;;;;;;;;;OAeG;IACH,QAAQ,CAAC,mBAAmB,CAAC,EAAE,kBAAkB,GAAG,IAAI,CAAC;CACzD;AAED;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,mBAAmB,GAC/B,KAAK,gBAAgB,EACrB,UAAU,wBAAwB,KAChC,KAAK,CAAC,eAAe,CAQvB,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,wBAAwB,GAAI,KAAK,gBAAgB,KAAG,KAAK,CAAC,SAAS,CAkB/E,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,yBAAyB,EAAE,GAAwD,CAAC;AAEjG;;;;;;;;;GASG;AACH,eAAO,MAAM,6BAA6B,GAAI,MAAM,MAAM,KAAG,SAC6B,CAAC;AAE3F;;;;;;;;GAQG;AACH,eAAO,MAAM,yBAAyB,QAAO,cAM1C,CAAC"}
@@ -80,6 +80,7 @@ export const create_spine_route_specs = (ctx) => [
80
80
  ip_rate_limiter: null,
81
81
  login_account_rate_limiter: null,
82
82
  login_fail_floor_ms: 0,
83
+ bootstrap_status: ctx.bootstrap_status,
83
84
  }),
84
85
  ...create_signup_route_specs(ctx.deps, {
85
86
  session_options: spine_session_options,
@@ -1 +1 @@
1
- {"version":3,"file":"rust_spine_stub_backend_config.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/rust_spine_stub_backend_config.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAuC9B,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,qBAAqB,CAAC;AAQvD,uGAAuG;AACvG,eAAO,MAAM,uBAAuB,oCAAoC,CAAC;AAEzE;;;;;GAKG;AACH,eAAO,MAAM,wCAAwC,6CAA6C,CAAC;AAEnG,kGAAkG;AAClG,eAAO,MAAM,4BAA4B,OAAO,CAAC;AAEjD,0FAA0F;AAC1F,eAAO,MAAM,oCAAoC,sDACG,CAAC;AAErD,MAAM,WAAW,6BAA6B;IAC7C,8DAA8D;IAC9D,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,+EAA+E;IAC/E,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B;;;OAGG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,8BAA8B,GAC1C,UAAS,6BAAkC,KACzC,aA6CF,CAAC"}
1
+ {"version":3,"file":"rust_spine_stub_backend_config.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/rust_spine_stub_backend_config.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAuC9B,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,qBAAqB,CAAC;AAQvD,uGAAuG;AACvG,eAAO,MAAM,uBAAuB,oCAAoC,CAAC;AAEzE;;;;;GAKG;AACH,eAAO,MAAM,wCAAwC,6CAA6C,CAAC;AAenG,kGAAkG;AAClG,eAAO,MAAM,4BAA4B,OAAO,CAAC;AAEjD,0FAA0F;AAC1F,eAAO,MAAM,oCAAoC,sDACG,CAAC;AAErD,MAAM,WAAW,6BAA6B;IAC7C,8DAA8D;IAC9D,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,+EAA+E;IAC/E,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B;;;OAGG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,8BAA8B,GAC1C,UAAS,6BAAkC,KACzC,aAwCF,CAAC"}
@@ -46,6 +46,18 @@ export const RUST_SPINE_STUB_BIN_ENV = 'FUZ_TESTING_RUST_SPINE_STUB_BIN';
46
46
  * column-presence is engine-portable, so one file is the cross-impl contract.
47
47
  */
48
48
  export const RUST_SPINE_STUB_EXPECTED_SCHEMA_PATH_ENV = 'FUZ_RUST_SPINE_STUB_EXPECTED_SCHEMA_PATH';
49
+ /**
50
+ * Capabilities for the Rust `testing_spine_stub` — `rust_default_capabilities`
51
+ * plus `sse` (the stub serves `GET /api/admin/audit/stream` over the spine
52
+ * `fuz_realtime::SseRegistry` + audit listener) and `ready` (it live-mounts
53
+ * `/ready` over the env-supplied fixture path). Named (not inline) so every
54
+ * spine preset is greppable, mirroring `ts_spine_capabilities`.
55
+ */
56
+ const rust_spine_stub_capabilities = Object.freeze({
57
+ ...rust_default_capabilities,
58
+ sse: true,
59
+ ready: true,
60
+ });
49
61
  /** Default listening port — slots beside zzz's 1175/1176; matches the binary's `DEFAULT_PORT`. */
50
62
  export const RUST_SPINE_STUB_DEFAULT_PORT = 1177;
51
63
  /** Default Postgres database — real PG (PGlite isn't reachable from `tokio-postgres`). */
@@ -78,12 +90,7 @@ export const rust_spine_stub_backend_config = (options = {}) => {
78
90
  // is the lower-precedence fallback — both carry the same value.
79
91
  start_command: [binary_path, '--port', String(port)],
80
92
  database_url,
81
- // The stub serves `GET /api/admin/audit/stream` (the spine
82
- // `fuz_realtime::SseRegistry` + audit listener), so it advertises `sse`
83
- // like the TS spines — the cross-process SSE suite's three cases run. It
84
- // also live-mounts `/ready` over the env-supplied fixture path, so it
85
- // advertises `ready` for `describe_ready_cross_tests`.
86
- capabilities: { ...rust_default_capabilities, sse: true, ready: true },
93
+ capabilities: rust_spine_stub_capabilities,
87
94
  port_env_var: 'FUZ_RUST_SPINE_STUB_PORT',
88
95
  rust_log: 'info,testing_spine_stub=info',
89
96
  paths,
@@ -1 +1 @@
1
- {"version":3,"file":"ts_spine_backend_config.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/ts_spine_backend_config.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAE9B;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,qBAAqB,CAAC;AAOvD,8GAA8G;AAC9G,eAAO,MAAM,gBAAgB,6BAA6B,CAAC;AAE3D;;;;;;;;;GASG;AACH,eAAO,MAAM,iBAAiB,4BAA4B,CAAC;AAS3D,6FAA6F;AAC7F,eAAO,MAAM,0BAA0B,OAAO,CAAC;AAE/C,iDAAiD;AACjD,eAAO,MAAM,0BAA0B,OAAO,CAAC;AAE/C,gDAAgD;AAChD,eAAO,MAAM,yBAAyB,OAAO,CAAC;AAE9C,yDAAyD;AACzD,eAAO,MAAM,mBAAmB,wDAAwD,CAAC;AAEzF,yDAAyD;AACzD,eAAO,MAAM,mBAAmB,wDAAwD,CAAC;AAEzF,wDAAwD;AACxD,eAAO,MAAM,kBAAkB,uDAAuD,CAAC;AAEvF,MAAM,WAAW,2BAA2B;IAC3C,mFAAmF;IACnF,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,8DAA8D;IAC9D,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;CAC/B;AAED;;;GAGG;AACH,eAAO,MAAM,4BAA4B,GACxC,UAAS,2BAAgC,KACvC,aAgBF,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,2BAA2B,GACvC,UAAS,2BAAgC,KACvC,aAgBF,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,4BAA4B,GACxC,UAAS,2BAAgC,KACvC,aA6BF,CAAC"}
1
+ {"version":3,"file":"ts_spine_backend_config.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/ts_spine_backend_config.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAE9B;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,qBAAqB,CAAC;AAOvD,8GAA8G;AAC9G,eAAO,MAAM,gBAAgB,6BAA6B,CAAC;AAE3D;;;;;;;;;GASG;AACH,eAAO,MAAM,iBAAiB,4BAA4B,CAAC;AAyB3D,6FAA6F;AAC7F,eAAO,MAAM,0BAA0B,OAAO,CAAC;AAE/C,iDAAiD;AACjD,eAAO,MAAM,0BAA0B,OAAO,CAAC;AAE/C,gDAAgD;AAChD,eAAO,MAAM,yBAAyB,OAAO,CAAC;AAE9C,yDAAyD;AACzD,eAAO,MAAM,mBAAmB,wDAAwD,CAAC;AAEzF,yDAAyD;AACzD,eAAO,MAAM,mBAAmB,wDAAwD,CAAC;AAEzF,wDAAwD;AACxD,eAAO,MAAM,kBAAkB,uDAAuD,CAAC;AAEvF,MAAM,WAAW,2BAA2B;IAC3C,mFAAmF;IACnF,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,8DAA8D;IAC9D,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;CAC/B;AAED;;;GAGG;AACH,eAAO,MAAM,4BAA4B,GACxC,UAAS,2BAAgC,KACvC,aAgBF,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,2BAA2B,GACvC,UAAS,2BAAgC,KACvC,aAgBF,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,4BAA4B,GACxC,UAAS,2BAAgC,KACvC,aA6BF,CAAC"}
@@ -20,6 +20,21 @@ export const TS_SPINE_SSE_PATH = '/api/admin/audit/stream';
20
20
  * `/ready` deploy gate in `build_spine_app`).
21
21
  */
22
22
  const ts_spine_capabilities = Object.freeze({ ...ts_default_capabilities, sse: true, ready: true });
23
+ /**
24
+ * Capabilities for the **Bun** spine binary — `ts_spine_capabilities` with
25
+ * `oversized_reject_closes_connection: false`. `Bun.serve` drains the declared
26
+ * `Content-Length` of an oversized-body `413` reject and keeps the socket
27
+ * alive (processing the correctly-framed pipelined request) even when the
28
+ * response carries `Connection: close`, unlike `@hono/node-server` / Deno /
29
+ * hyper, which close. Bun is not insecure — it frames on `Content-Length`, so
30
+ * there is no desync — but the smuggling suite's strong "connection closes"
31
+ * assertion doesn't hold; this flag routes Bun onto the suite's no-desync arm.
32
+ * See `docs/security.md` §"Body Size Limiting".
33
+ */
34
+ const ts_spine_bun_capabilities = Object.freeze({
35
+ ...ts_spine_capabilities,
36
+ oversized_reject_closes_connection: false,
37
+ });
23
38
  /** Default port for the Node TS spine binary — slots beside the Rust `spine_stub` (1177). */
24
39
  export const TS_SPINE_NODE_DEFAULT_PORT = 1178;
25
40
  /** Default port for the Deno TS spine binary. */
@@ -71,7 +86,7 @@ export const ts_spine_bun_backend_config = (options = {}) => {
71
86
  database_url,
72
87
  paths,
73
88
  extra_env: { [TS_SPINE_DIR_ENV]: paths.root },
74
- capabilities: ts_spine_capabilities,
89
+ capabilities: ts_spine_bun_capabilities,
75
90
  }),
76
91
  sse_path: TS_SPINE_SSE_PATH,
77
92
  };
@@ -1 +1 @@
1
- {"version":3,"file":"integration.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/integration.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAsB7B,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,2BAA2B,CAAC;AAM9D,OAAO,EAKN,KAAK,uBAAuB,EAC5B,MAAM,kBAAkB,CAAC;AAkB1B,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,oBAAoB,CAAC;AACvD,OAAO,EAAC,KAAK,mBAAmB,EAAC,MAAM,iCAAiC,CAAC;AACzE,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,0BAA0B,CAAC;AAGxD;;GAEG;AACH,MAAM,WAAW,8BAA8B;IAC9C;;;;OAIG;IACH,UAAU,EAAE,SAAS,CAAC;IACtB;;;;;;;;;OASG;IACH,cAAc,EAAE,cAAc,CAAC;IAC/B,kEAAkE;IAClE,YAAY,EAAE,mBAAmB,CAAC;IAClC;;;;OAIG;IACH,eAAe,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IACxC;;;;;;;;;;;;OAYG;IACH,aAAa,EAAE,uBAAuB,CAAC;IACvC;;;;;;;OAOG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,eAAO,MAAM,mCAAmC,GAC/C,SAAS,8BAA8B,KACrC,IAw6CF,CAAC"}
1
+ {"version":3,"file":"integration.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/integration.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAsB7B,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,2BAA2B,CAAC;AAM9D,OAAO,EAKN,KAAK,uBAAuB,EAC5B,MAAM,kBAAkB,CAAC;AAkB1B,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,oBAAoB,CAAC;AACvD,OAAO,EAAU,KAAK,mBAAmB,EAAC,MAAM,iCAAiC,CAAC;AAClF,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,0BAA0B,CAAC;AAGxD;;GAEG;AACH,MAAM,WAAW,8BAA8B;IAC9C;;;;OAIG;IACH,UAAU,EAAE,SAAS,CAAC;IACtB;;;;;;;;;OASG;IACH,cAAc,EAAE,cAAc,CAAC;IAC/B,kEAAkE;IAClE,YAAY,EAAE,mBAAmB,CAAC;IAClC;;;;OAIG;IACH,eAAe,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IACxC;;;;;;;;;;;;OAYG;IACH,aAAa,EAAE,uBAAuB,CAAC;IACvC;;;;;;;OAOG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,eAAO,MAAM,mCAAmC,GAC/C,SAAS,8BAA8B,KACrC,IA66CF,CAAC"}
@@ -24,7 +24,7 @@ import { is_public_auth } from '../http/auth_shape.js';
24
24
  import { account_verify_action_spec, account_session_list_action_spec, account_session_revoke_action_spec, account_session_revoke_all_action_spec, account_token_create_action_spec, account_token_list_action_spec, account_token_revoke_action_spec, } from '../auth/account_action_specs.js';
25
25
  import { invite_create_action_spec } from '../auth/admin_action_specs.js';
26
26
  import { LoginOutput, AccountStatusOutput } from '../auth/account_route_schema.js';
27
- import {} from './cross_backend/capabilities.js';
27
+ import { test_if } from './cross_backend/capabilities.js';
28
28
  import { DEFAULT_TEST_PASSWORD } from './test_credentials.js';
29
29
  /**
30
30
  * Standard integration test suite for fuz_app auth routes.
@@ -316,15 +316,15 @@ export const describe_standard_integration_tests = (options) => {
316
316
  // fixture keeper is single-actor, so `actor` must be non-null and
317
317
  // `role_grants` populated (keeper holds keeper + admin globally).
318
318
  //
319
- // `/status` is mounted at `create_app_server` time (it needs the
320
- // `bootstrap_available` runtime state), not by `create_account_route_specs`
321
- // and not listed in the declared surface, so we can't gate on `route_specs`.
322
- // A backend that mounts only the account-route factory (e.g. a minimal
323
- // in-process route set) doesn't serve it probe at runtime and skip on
324
- // 404. The full spine surfaces (in-process + cross-process) serve it, so
325
- // the gate runs there. `find_auth_route` can't be used: `/status` isn't a
326
- // `RestAuthRouteSuffix`.
327
- test('authenticated status body strict-parses against AccountStatusOutput', async (ctx) => {
319
+ // `/status` is bundled into `create_account_route_specs` (relative
320
+ // `/status`, prefixed to `/api/account/status`), so every account
321
+ // surface serves it matching the Rust `account_router`. Gated on the
322
+ // declared `account_status` capability rather than a runtime 404-sniff:
323
+ // a backend that mounts account routes declares `account_status: true`
324
+ // and the gate **fails loud** if `/status` is missing (no more silent
325
+ // skip swallowing the coverage); a backend without it skips explicitly.
326
+ // `find_auth_route` can't be used: `/status` isn't a `RestAuthRouteSuffix`.
327
+ test_if(options.capabilities.account_status, 'authenticated status body strict-parses against AccountStatusOutput', async () => {
328
328
  const fixture = await options.setup_test();
329
329
  const login_route = find_auth_route(route_specs, '/login', 'POST');
330
330
  assert.ok(login_route, 'Expected POST /login route — ensure create_route_specs includes account routes');
@@ -334,12 +334,9 @@ export const describe_standard_integration_tests = (options) => {
334
334
  method: 'GET',
335
335
  headers: fixture.create_session_headers({ host: 'localhost' }),
336
336
  });
337
- if (res.status === 404) {
338
- // Backend doesn't mount /status (minimal route set) nothing to gate.
339
- ctx.skip();
340
- return;
341
- }
342
- assert.strictEqual(res.status, 200);
337
+ assert.strictEqual(res.status, 200, `expected 200 from ${status_path} — capabilities.account_status is true, so the ` +
338
+ `account surface must bundle GET /status. A 404 means the backend declares the ` +
339
+ `capability but doesn't mount the route.`);
343
340
  const body = await res.json();
344
341
  // Throws on any extra/missing field across account/actor/role_grants.
345
342
  const parsed = AccountStatusOutput.parse(body);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fuzdev/fuz_app",
3
- "version": "0.88.0",
3
+ "version": "0.89.0",
4
4
  "description": "fullstack app library",
5
5
  "glyph": "🗝",
6
6
  "logo": "logo.svg",
@@ -69,9 +69,9 @@
69
69
  "devDependencies": {
70
70
  "@electric-sql/pglite": "^0.4.5",
71
71
  "@fuzdev/blake3_wasm": "^0.1.1",
72
- "@fuzdev/fuz_code": "^0.46.0",
72
+ "@fuzdev/fuz_code": "^0.46.1",
73
73
  "@fuzdev/fuz_css": "^0.63.2",
74
- "@fuzdev/fuz_ui": "^0.205.0",
74
+ "@fuzdev/fuz_ui": "^0.205.1",
75
75
  "@fuzdev/fuz_util": "^0.65.1",
76
76
  "@fuzdev/gro": "^0.204.0",
77
77
  "@fuzdev/mdz": "^0.1.0",