@fuzdev/fuz_app 0.135.0 → 0.137.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/dist/actions/CLAUDE.md +2 -2
  2. package/dist/actions/action_spec.d.ts +0 -48
  3. package/dist/actions/action_spec.d.ts.map +1 -1
  4. package/dist/actions/transports_ws_backend.d.ts +1 -1
  5. package/dist/actions/transports_ws_backend.js +1 -1
  6. package/dist/auth/CLAUDE.md +8 -2
  7. package/dist/auth/account_action_specs.d.ts +7 -0
  8. package/dist/auth/account_action_specs.d.ts.map +1 -1
  9. package/dist/auth/account_action_specs.js +12 -3
  10. package/dist/auth/account_schema.d.ts +1 -6
  11. package/dist/auth/account_schema.d.ts.map +1 -1
  12. package/dist/auth/account_schema.js +1 -0
  13. package/dist/auth/actor_search_action_specs.d.ts +1 -1
  14. package/dist/auth/actor_search_action_specs.d.ts.map +1 -1
  15. package/dist/auth/actor_search_action_specs.js +3 -6
  16. package/dist/auth/audit_emitter.d.ts +4 -0
  17. package/dist/auth/audit_emitter.d.ts.map +1 -1
  18. package/dist/auth/audit_emitter.js +18 -2
  19. package/dist/auth/cell_action_specs.d.ts +11 -1
  20. package/dist/auth/cell_action_specs.d.ts.map +1 -1
  21. package/dist/auth/cell_action_specs.js +18 -5
  22. package/dist/auth/cell_item_action_specs.d.ts.map +1 -1
  23. package/dist/auth/cell_item_action_specs.js +2 -4
  24. package/dist/auth/password.d.ts +2 -2
  25. package/dist/auth/password.d.ts.map +1 -1
  26. package/dist/auth/password.js +10 -12
  27. package/dist/auth/role_grant_offer_action_specs.d.ts.map +1 -1
  28. package/dist/auth/role_grant_offer_action_specs.js +9 -9
  29. package/dist/auth/role_grant_offer_notifications.d.ts.map +1 -1
  30. package/dist/auth/role_grant_offer_notifications.js +2 -1
  31. package/dist/auth/role_grant_offer_schema.d.ts +1 -1
  32. package/dist/auth/role_grant_offer_schema.d.ts.map +1 -1
  33. package/dist/auth/role_grant_offer_schema.js +4 -7
  34. package/dist/auth/role_schema.d.ts.map +1 -1
  35. package/dist/auth/role_schema.js +5 -4
  36. package/dist/auth/scope_kind_schema.d.ts +10 -0
  37. package/dist/auth/scope_kind_schema.d.ts.map +1 -1
  38. package/dist/auth/scope_kind_schema.js +15 -3
  39. package/dist/cli/args.d.ts +21 -15
  40. package/dist/cli/args.d.ts.map +1 -1
  41. package/dist/cli/args.js +40 -16
  42. package/dist/cli/daemon.d.ts +0 -5
  43. package/dist/cli/daemon.d.ts.map +1 -1
  44. package/dist/cli/help.d.ts +45 -6
  45. package/dist/cli/help.d.ts.map +1 -1
  46. package/dist/cli/help.js +81 -4
  47. package/dist/env/load.d.ts +1 -1
  48. package/dist/env/load.js +1 -1
  49. package/dist/http/CLAUDE.md +1 -1
  50. package/dist/http/auth_shape.d.ts +0 -27
  51. package/dist/http/auth_shape.d.ts.map +1 -1
  52. package/dist/http/body_limit.d.ts +27 -14
  53. package/dist/http/body_limit.d.ts.map +1 -1
  54. package/dist/http/body_limit.js +29 -16
  55. package/dist/http/db_routes.d.ts.map +1 -1
  56. package/dist/http/db_routes.js +5 -3
  57. package/dist/primitive_schemas.d.ts +38 -4
  58. package/dist/primitive_schemas.d.ts.map +1 -1
  59. package/dist/primitive_schemas.js +77 -13
  60. package/dist/server/x_accel.d.ts +0 -1
  61. package/dist/server/x_accel.d.ts.map +1 -1
  62. package/dist/testing/CLAUDE.md +36 -19
  63. package/dist/testing/audit_drift_guard.d.ts +18 -5
  64. package/dist/testing/audit_drift_guard.d.ts.map +1 -1
  65. package/dist/testing/audit_drift_guard.js +24 -8
  66. package/dist/testing/cross_backend/action_manifest.d.ts +0 -14
  67. package/dist/testing/cross_backend/action_manifest.d.ts.map +1 -1
  68. package/dist/testing/cross_backend/body_size.d.ts +18 -5
  69. package/dist/testing/cross_backend/body_size.d.ts.map +1 -1
  70. package/dist/testing/cross_backend/body_size.js +233 -34
  71. package/dist/testing/cross_backend/body_size_smuggling.d.ts +0 -12
  72. package/dist/testing/cross_backend/body_size_smuggling.d.ts.map +1 -1
  73. package/dist/testing/cross_backend/body_size_smuggling.js +92 -147
  74. package/dist/testing/cross_backend/capabilities.d.ts +3 -24
  75. package/dist/testing/cross_backend/capabilities.d.ts.map +1 -1
  76. package/dist/testing/cross_backend/capabilities.js +1 -4
  77. package/dist/testing/cross_backend/cell_crud.d.ts.map +1 -1
  78. package/dist/testing/cross_backend/cell_crud.js +31 -2
  79. package/dist/testing/cross_backend/conformance_case.d.ts +3 -3
  80. package/dist/testing/cross_backend/credential_header_robustness.d.ts.map +1 -1
  81. package/dist/testing/cross_backend/credential_header_robustness.js +18 -54
  82. package/dist/testing/cross_backend/default_backend_configs.d.ts.map +1 -1
  83. package/dist/testing/cross_backend/default_backend_configs.js +2 -8
  84. package/dist/testing/cross_backend/testing_backdoor.d.ts.map +1 -1
  85. package/dist/testing/cross_backend/testing_backdoor.js +2 -0
  86. package/dist/testing/cross_backend/testing_reset_actions.d.ts +5 -9
  87. package/dist/testing/cross_backend/testing_reset_actions.d.ts.map +1 -1
  88. package/dist/testing/cross_backend/testing_reset_actions.js +5 -0
  89. package/dist/testing/cross_backend/token_lifetime.d.ts.map +1 -1
  90. package/dist/testing/cross_backend/token_lifetime.js +34 -1
  91. package/dist/testing/cross_backend/ts_spine_backend_config.d.ts.map +1 -1
  92. package/dist/testing/cross_backend/ts_spine_backend_config.js +7 -14
  93. package/dist/testing/cross_backend/ws_outbound_overflow.d.ts.map +1 -1
  94. package/dist/testing/cross_backend/ws_outbound_overflow.js +65 -0
  95. package/dist/testing/schema_introspect.d.ts +24 -4
  96. package/dist/testing/schema_introspect.d.ts.map +1 -1
  97. package/dist/testing/schema_introspect.js +27 -4
  98. package/dist/testing/transports/raw_http.d.ts +78 -0
  99. package/dist/testing/transports/raw_http.d.ts.map +1 -0
  100. package/dist/testing/transports/raw_http.js +141 -0
  101. package/package.json +7 -5
  102. package/dist/env/resolve.d.ts +0 -163
  103. package/dist/env/resolve.d.ts.map +0 -1
  104. package/dist/env/resolve.js +0 -267
package/dist/cli/help.js CHANGED
@@ -5,10 +5,87 @@
5
5
  * once with `create_help` and get back `generate_main_help`,
6
6
  * `generate_command_help`, and `get_help_text`.
7
7
  *
8
+ * Schemas are `@fuzdev/fuz_schema` object schemas, the ones `parse_command_args`
9
+ * and `create_extract_global_flags` take, read with fuz_schema's `introspect.ts`:
10
+ * each entry's `description` and `aliases` metadata, its default, and its type
11
+ * as `format_schema_type` renders it.
12
+ *
8
13
  * @module
9
14
  */
10
- import { zod_to_schema_properties, zod_format_value } from '@fuzdev/fuz_util/zod.ts';
15
+ import { format_schema_type, get_schema_fields, get_schema_meta } from '@fuzdev/fuz_schema/introspect.ts';
11
16
  import { colors } from './util.js';
17
+ /**
18
+ * Read the args of an object schema for help output, in order.
19
+ *
20
+ * An entry whose `no-` negation is also an entry is left out, so a pair like
21
+ * `color` and `no-color` is shown once, as `--no-color`.
22
+ *
23
+ * @param schema - fuz_schema object schema
24
+ * @returns the args, none when `schema` is not an object schema
25
+ */
26
+ export const to_help_arg_properties = (schema) => {
27
+ const fields = get_schema_fields(schema);
28
+ const names = new Set(fields.map((f) => f.name));
29
+ const properties = [];
30
+ for (const field of fields) {
31
+ if (names.has('no-' + field.name))
32
+ continue;
33
+ properties.push({
34
+ name: field.name,
35
+ type: format_schema_type(field.schema),
36
+ description: field.description ?? '',
37
+ default: to_field_default(field),
38
+ aliases: get_schema_meta(field.schema, 'aliases') ?? []
39
+ });
40
+ }
41
+ return properties;
42
+ };
43
+ // the default a field's help shows: a `with_default_fn`'s is made by calling its `make`,
44
+ // as each parse that fills the key in does, found by walking the layers `get_schema_fields`
45
+ // reads, whose first `optional` or default it is when `default_computed`
46
+ const to_field_default = (field) => {
47
+ if (!field.default_computed)
48
+ return field.default;
49
+ let layer = field.schema;
50
+ for (;;) {
51
+ switch (layer.kind) {
52
+ case 'with_default_fn':
53
+ return layer.def.make();
54
+ case 'optional':
55
+ case 'nullable':
56
+ case 'with_default':
57
+ layer = layer.def.schema;
58
+ break;
59
+ case 'pipe':
60
+ layer = layer.def[0];
61
+ break;
62
+ case 'lazy':
63
+ layer = layer.def();
64
+ break;
65
+ default:
66
+ return undefined;
67
+ }
68
+ }
69
+ };
70
+ /**
71
+ * Format a default value for display, `''` for none.
72
+ *
73
+ * Strings and arrays are written as JSON, as the type column writes a string literal.
74
+ *
75
+ * @param value - the default
76
+ * @returns display text
77
+ */
78
+ export const format_arg_default = (value) => {
79
+ if (value === undefined)
80
+ return '';
81
+ if (value === null)
82
+ return 'null';
83
+ if (typeof value === 'string' || typeof value === 'object')
84
+ return JSON.stringify(value);
85
+ if (typeof value === 'boolean' || typeof value === 'number')
86
+ return String(value);
87
+ return '';
88
+ };
12
89
  /**
13
90
  * Get maximum length from array.
14
91
  *
@@ -50,7 +127,7 @@ export const create_help = (options) => {
50
127
  ? colors
51
128
  : { green: '', yellow: '', blue: '', red: '', cyan: '', dim: '', bold: '', reset: '' };
52
129
  const generate_global_options = () => {
53
- const properties = zod_to_schema_properties(options.global_args_schema);
130
+ const properties = to_help_arg_properties(options.global_args_schema);
54
131
  const max_width = to_max_length(properties, (p) => ` ${format_arg_name(p)}`);
55
132
  return properties.map((prop) => {
56
133
  const name = format_arg_name(prop);
@@ -65,7 +142,7 @@ export const create_help = (options) => {
65
142
  lines.push(`${c.yellow}Usage${c.reset}: ${meta.usage}`);
66
143
  lines.push('');
67
144
  if (meta.schema) {
68
- const properties = zod_to_schema_properties(meta.schema);
145
+ const properties = to_help_arg_properties(meta.schema);
69
146
  const flag_props = properties.filter((p) => p.name !== '_');
70
147
  const positional_prop = properties.find((p) => p.name === '_');
71
148
  if (positional_prop?.description) {
@@ -79,7 +156,7 @@ export const create_help = (options) => {
79
156
  for (const prop of flag_props) {
80
157
  const name = format_arg_name(prop).padEnd(longest_name);
81
158
  const type = prop.type.padEnd(longest_type);
82
- const def = zod_format_value(prop.default);
159
+ const def = format_arg_default(prop.default);
83
160
  const desc = prop.description || '';
84
161
  const default_str = def ? ` (default: ${def})` : '';
85
162
  lines.push(` ${name} ${type} ${desc}${default_str}`);
@@ -35,7 +35,7 @@ export declare class EnvValidationError extends Error {
35
35
  * `error.all_undefined` before calling this.
36
36
  *
37
37
  * @param error - the env validation error
38
- * @param label - optional prefix for log lines (e.g., 'zap daemon', 'env')
38
+ * @param label - optional prefix for log lines (e.g., 'server', 'env')
39
39
  */
40
40
  export declare const log_env_validation_error: (error: EnvValidationError, label?: string) => void;
41
41
  /**
package/dist/env/load.js CHANGED
@@ -42,7 +42,7 @@ export class EnvValidationError extends Error {
42
42
  * `error.all_undefined` before calling this.
43
43
  *
44
44
  * @param error - the env validation error
45
- * @param label - optional prefix for log lines (e.g., 'zap daemon', 'env')
45
+ * @param label - optional prefix for log lines (e.g., 'server', 'env')
46
46
  */
47
47
  export const log_env_validation_error = (error, label) => {
48
48
  const prefix = label ? `[${label}] ` : '';
@@ -19,7 +19,7 @@ effects, see ../../../docs/architecture.md.
19
19
  - `http/error_schemas.ts` — `ERROR_*` constants, standard error shapes, `derive_error_schemas`.
20
20
  - `http/schema_helpers.ts` — shared Zod introspection (null/void/strict/surface/merge/middleware-applies) + `to_void_params` / `to_input_params` / `safe_parse_dropping_unknown_keys`.
21
21
  - `http/middleware_spec.ts` — `MiddlewareSpec` interface.
22
- - `http/body_limit.ts` — `create_body_limit_middleware(max_size)`: the `413` body cap `create_app_server` mounts. Refuses on `Content-Length` without reading `c.req.raw.body` and answers `Connection: close` — `413` past the cap, `400` `ERROR_INVALID_CONTENT_LENGTH` when the value is not a plain run of ASCII digits (a length the cap cannot read, which Hono's `bodyLimit` would pass uncounted) — else falls through to Hono's streaming `bodyLimit` (no `Content-Length`, or one beside `Transfer-Encoding`; that `413` carries no `Connection: close`). Mounted with `app.use`, not as a `MiddlewareSpec`, so neither status appears in a route's merged error schemas. After the header `413` Node closes at once (the header is what makes `@hono/node-server` do so), Deno closes once the declared body has arrived, and Bun keeps the connection and serves the next request (../../../docs/security.md §Body Size Limiting).
22
+ - `http/body_limit.ts` — `create_body_limit_middleware(max_size)`: the `413` body cap `create_app_server` mounts. Refuses on `Content-Length` without reading `c.req.raw.body` and answers `Connection: close` — `413` past the cap, `400` `ERROR_INVALID_CONTENT_LENGTH` when the value is not a plain run of ASCII digits (a length the cap cannot read, which Hono's `bodyLimit` would pass uncounted) — else falls through to Hono's `bodyLimit` (no `Content-Length`, or one beside `Transfer-Encoding`), which reads the body into memory up to the cap before the next middleware runs, so its `413` — no `Connection: close` — also precedes origin and auth. Mounted with `app.use`, not as a `MiddlewareSpec`, so neither status appears in a route's merged error schemas. After the header `413` Node and Bun close at once (the header is what makes them — Bun 1.4.3 is the tested floor, an older Bun kept the connection and served the next request) and Deno closes once the declared body has arrived (../../../docs/security.md §Body Size Limiting).
23
23
  - `http/surface.ts` — `AppSurface`, `AppSurfaceSpec`, `generate_app_surface`, diagnostics.
24
24
  - `http/surface_query.ts` — pure filters/groupings over `AppSurface`.
25
25
  - `http/proxy.ts` — trusted-proxy middleware, CIDR parsing, rightmost-first XFF resolution. Pulls `hono/utils/ipaddr` for CIDR matching.
@@ -134,33 +134,6 @@ export declare const RouteAuth: z.ZodObject<{
134
134
  }>;
135
135
  roles: z.ZodOptional<z.ZodReadonly<z.ZodArray<z.ZodString>>>;
136
136
  credential_types: z.ZodOptional<z.ZodReadonly<z.ZodArray<z.ZodString>>>;
137
- /**
138
- * The capability a **narrowed** api token's scope must admit to reach this
139
- * route, in the `<section>:<id>` vocabulary a denial reports back as
140
- * `required_scope`:
141
- *
142
- * - `surface:<name>` — a non-RPC surface, refused to every narrowed token
143
- * whatever its method list says (token scoping's rule that a narrowed
144
- * token is RPC-only). `surface:audit_stream`, `surface:fact_bare`, and
145
- * `surface:app_surface` are the spine's route-spec ones; a consumer names
146
- * its own without registering it.
147
- * - `rpc:<method>` — one action method, refused unless the token lists it.
148
- * What a route bridged from an `ActionSpec` declares: it carries the
149
- * method's identity but never reaches the dispatcher's per-method gate.
150
- *
151
- * Declaring it makes the guard resolver mount the refusal ahead of the role
152
- * gate, instead of the route hand-rolling the check in its handler (which
153
- * the auth phase has already role-gated by then, so a narrowed token whose
154
- * account lacks the role would be told about the role rather than about the
155
- * scope).
156
- *
157
- * Opt-in rather than blanket: the account REST routes cross no boundary, and
158
- * the RPC endpoint runs its own per-method check inside the dispatcher — and
159
- * both are route specs too.
160
- *
161
- * Typed as a plain string here to keep `http/` from importing `auth/`; the
162
- * resolver parses it at registration and throws on a malformed one.
163
- */
164
137
  required_scope: z.ZodOptional<z.ZodString>;
165
138
  }, z.core.$strict>;
166
139
  export type RouteAuth = z.infer<typeof RouteAuth>;
@@ -1 +1 @@
1
- {"version":3,"file":"auth_shape.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/http/auth_shape.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,WAAW,6DAGtB,CAAC;AACH,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAC;AAEtD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,YAAY,GAAI,OAAO,OAAO,KAAG,MAAM,GAAG,SAGtD,CAAC;AAEF;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,aAAa;;;;EAA2C,CAAC;AACtE,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,aAAa,CAAC,CAAC;AAE1D;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,eAAO,MAAM,SAAS;;;;;;;;;;;;;IAMpB;;;;;;;;;;;;;;;;;;;;;;;;;;OA0BG;;kBAyCF,CAAC;AACJ,MAAM,MAAM,SAAS,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,SAAS,CAAC,CAAC;AAUlD;;;;;GAKG;AACH,eAAO,MAAM,cAAc,GAAI,MAAM,SAAS,KAAG,OACA,CAAC;AAElD;;;;;;GAMG;AACH,eAAO,MAAM,WAAW,GAAI,MAAM,SAAS,KAAG,OAAgC,CAAC;AAE/E;;;;GAIG;AACH,eAAO,MAAM,aAAa,GAAI,MAAM,SAAS,KAAG,OAAkC,CAAC;AAEnF,wEAAwE;AACxE,eAAO,MAAM,YAAY,GAAI,MAAM,SAAS,KAAG,OAA+B,CAAC;AAE/E,8FAA8F;AAC9F,eAAO,MAAM,wBAAwB,GAAI,MAAM,SAAS,KAAG,OAC3B,CAAC;AAEjC;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,cAAc,GAAI,MAAM,SAAS,KAAG,OACQ,CAAC;AAE1D;;;;;;;;;;GAUG;AACH,eAAO,MAAM,2BAA2B,GAAI,MAAM,SAAS,KAAG,OACwB,CAAC;AAYvF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,4BAA4B,GAAI,QAAQ,CAAC,CAAC,OAAO,KAAG,OAIhE,CAAC;AAEF;;;;;GAKG;AACH,MAAM,WAAW,WAAW;IAC3B,KAAK,EAAE,CAAC,CAAC,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,CAAC,CAAC,OAAO,CAAC;CAClB;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,sCAAsC,GAClD,MAAM,SAAS,EACf,OAAO,WAAW,EAClB,SAAS,MAAM,KACb,IAmBF,CAAC"}
1
+ {"version":3,"file":"auth_shape.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/http/auth_shape.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,WAAW,6DAGtB,CAAC;AACH,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAC;AAEtD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,YAAY,GAAI,OAAO,OAAO,KAAG,MAAM,GAAG,SAGtD,CAAC;AAEF;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,aAAa;;;;EAA2C,CAAC;AACtE,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,aAAa,CAAC,CAAC;AAE1D;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,eAAO,MAAM,SAAS;;;;;;;;;;;;;;kBAyEnB,CAAC;AACJ,MAAM,MAAM,SAAS,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,SAAS,CAAC,CAAC;AAUlD;;;;;GAKG;AACH,eAAO,MAAM,cAAc,GAAI,MAAM,SAAS,KAAG,OACA,CAAC;AAElD;;;;;;GAMG;AACH,eAAO,MAAM,WAAW,GAAI,MAAM,SAAS,KAAG,OAAgC,CAAC;AAE/E;;;;GAIG;AACH,eAAO,MAAM,aAAa,GAAI,MAAM,SAAS,KAAG,OAAkC,CAAC;AAEnF,wEAAwE;AACxE,eAAO,MAAM,YAAY,GAAI,MAAM,SAAS,KAAG,OAA+B,CAAC;AAE/E,8FAA8F;AAC9F,eAAO,MAAM,wBAAwB,GAAI,MAAM,SAAS,KAAG,OAC3B,CAAC;AAEjC;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,cAAc,GAAI,MAAM,SAAS,KAAG,OACQ,CAAC;AAE1D;;;;;;;;;;GAUG;AACH,eAAO,MAAM,2BAA2B,GAAI,MAAM,SAAS,KAAG,OACwB,CAAC;AAYvF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,4BAA4B,GAAI,QAAQ,CAAC,CAAC,OAAO,KAAG,OAIhE,CAAC;AAEF;;;;;GAKG;AACH,MAAM,WAAW,WAAW;IAC3B,KAAK,EAAE,CAAC,CAAC,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,CAAC,CAAC,OAAO,CAAC;CAClB;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,sCAAsC,GAClD,MAAM,SAAS,EACf,OAAO,WAAW,EAClB,SAAS,MAAM,KACb,IAmBF,CAAC"}
@@ -11,25 +11,36 @@
11
11
  * The runtimes' HTTP parsers answer their own bodiless `400` to most of
12
12
  * these before the application sees them, so this branch is defense in
13
13
  * depth where they do and the refusal where they do not: Node's refuses
14
- * them all; Deno hands over a repeated equal header as one combined value
15
- * (`50, 50`); Bun hands over an empty value and a repeated header, equal or
16
- * differing (`5, 50`), framing the body on the first (observed on Node
17
- * 24.14.1, Deno 2.8.3, Bun 1.3.11). The Rust spine's parser (hyper) refuses
18
- * a malformed value and differing repeats, and accepts an equal repeat as
19
- * the one value.
14
+ * them all; Deno and Bun hand over a repeated equal header as one combined
15
+ * value (`50, 50`) and refuse the rest (observed on Node 24.14.1, Deno
16
+ * 2.8.3, Bun 1.4.3). The Rust spine's parser (hyper) refuses a malformed
17
+ * value and differing repeats, and accepts an equal repeat as the one
18
+ * value.
20
19
  * - A request whose `Content-Length` exceeds the cap is refused with `413`
21
20
  * `{error: 'payload_too_large'}`, whatever its method. A digit run of any
22
21
  * length compares as a number, so one too long to represent exactly is over
23
- * the cap, never unreadable.
22
+ * the cap, never unreadable. Over a socket that branch is reached on Node
23
+ * and Deno, whose parsers hand over any length below 2^64 and refuse a
24
+ * longer one with their own `400`. `Bun.serve` hands none over: with its
25
+ * default options it answers a length past 128 MiB with its own `413`, and
26
+ * one of 2^63 - 1 or more with its own `400` (observed on Node 24.14.1,
27
+ * Deno 2.8.3, Bun 1.4.3).
24
28
  * - Both refusals answer on the header alone — without touching
25
29
  * `c.req.raw.body` — and carry `Connection: close`.
26
30
  * - A request with a well-formed `Content-Length` beside `Transfer-Encoding`,
27
- * or with no `Content-Length`, falls through to `bodyLimit`'s streaming
28
- * count: with `Transfer-Encoding` the declared length does not frame the
29
- * body, so the bytes that arrive are what is measured. That `413` is sent
30
- * once more than the cap has been read and carries no `Connection: close`.
31
+ * or with no `Content-Length`, falls through to `bodyLimit`, which measures
32
+ * the bytes that arrive: with `Transfer-Encoding` the declared length does
33
+ * not frame the body. `bodyLimit` reads the body into memory, up to the
34
+ * cap, **before** the next middleware runs, so that `413` — sent once more
35
+ * than the cap has been read, with no `Connection: close` — also comes
36
+ * ahead of the origin check, auth and the handler. The Rust spine counts
37
+ * such a body as its handler reads it instead, so there a refusal made on
38
+ * the headers answers first; which status an over-cap body with no declared
39
+ * length gets when another refusal also applies is implementation-defined
40
+ * across the two (`docs/security.md` §"Body Size Limiting").
31
41
  * The Node, Deno and Bun parsers refuse a request carrying both headers
32
- * with their own `400`; hyper accepts one and frames its body as chunked.
42
+ * with their own `400`; hyper accepts one, drops its `Content-Length`,
43
+ * frames the body as chunked, and closes the connection after the response.
33
44
  * - A well-formed `Content-Length` within the cap is trusted to frame the
34
45
  * body, as the runtime's HTTP parser enforces.
35
46
  *
@@ -45,8 +56,10 @@
45
56
  * connection open while a client withholds it; a request pipelined behind
46
57
  * the body is not served. Neither the header nor what the handler does with
47
58
  * the body changes that (observed on Deno 2.8.3).
48
- * - **Bun** keeps the connection, skips the declared body, and serves the
49
- * next request on it (observed on Bun 1.3.11).
59
+ * - **Bun** closes at once, the body unread, honouring the header. Bun 1.4.3
60
+ * is the tested floor: an older Bun kept the connection after the `413` and
61
+ * served the next request (correctly framed), so run Bun at the floor or
62
+ * later.
50
63
  *
51
64
  * @module
52
65
  */
@@ -1 +1 @@
1
- {"version":3,"file":"body_limit.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/http/body_limit.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmDG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAC;AAQ9C;;;;;GAKG;AACH,eAAO,MAAM,4BAA4B,GAAI,UAAU,MAAM,KAAG,iBAwB/D,CAAC"}
1
+ {"version":3,"file":"body_limit.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/http/body_limit.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgEG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAC;AAQ9C;;;;;GAKG;AACH,eAAO,MAAM,4BAA4B,GAAI,UAAU,MAAM,KAAG,iBAwB/D,CAAC"}
@@ -11,25 +11,36 @@
11
11
  * The runtimes' HTTP parsers answer their own bodiless `400` to most of
12
12
  * these before the application sees them, so this branch is defense in
13
13
  * depth where they do and the refusal where they do not: Node's refuses
14
- * them all; Deno hands over a repeated equal header as one combined value
15
- * (`50, 50`); Bun hands over an empty value and a repeated header, equal or
16
- * differing (`5, 50`), framing the body on the first (observed on Node
17
- * 24.14.1, Deno 2.8.3, Bun 1.3.11). The Rust spine's parser (hyper) refuses
18
- * a malformed value and differing repeats, and accepts an equal repeat as
19
- * the one value.
14
+ * them all; Deno and Bun hand over a repeated equal header as one combined
15
+ * value (`50, 50`) and refuse the rest (observed on Node 24.14.1, Deno
16
+ * 2.8.3, Bun 1.4.3). The Rust spine's parser (hyper) refuses a malformed
17
+ * value and differing repeats, and accepts an equal repeat as the one
18
+ * value.
20
19
  * - A request whose `Content-Length` exceeds the cap is refused with `413`
21
20
  * `{error: 'payload_too_large'}`, whatever its method. A digit run of any
22
21
  * length compares as a number, so one too long to represent exactly is over
23
- * the cap, never unreadable.
22
+ * the cap, never unreadable. Over a socket that branch is reached on Node
23
+ * and Deno, whose parsers hand over any length below 2^64 and refuse a
24
+ * longer one with their own `400`. `Bun.serve` hands none over: with its
25
+ * default options it answers a length past 128 MiB with its own `413`, and
26
+ * one of 2^63 - 1 or more with its own `400` (observed on Node 24.14.1,
27
+ * Deno 2.8.3, Bun 1.4.3).
24
28
  * - Both refusals answer on the header alone — without touching
25
29
  * `c.req.raw.body` — and carry `Connection: close`.
26
30
  * - A request with a well-formed `Content-Length` beside `Transfer-Encoding`,
27
- * or with no `Content-Length`, falls through to `bodyLimit`'s streaming
28
- * count: with `Transfer-Encoding` the declared length does not frame the
29
- * body, so the bytes that arrive are what is measured. That `413` is sent
30
- * once more than the cap has been read and carries no `Connection: close`.
31
+ * or with no `Content-Length`, falls through to `bodyLimit`, which measures
32
+ * the bytes that arrive: with `Transfer-Encoding` the declared length does
33
+ * not frame the body. `bodyLimit` reads the body into memory, up to the
34
+ * cap, **before** the next middleware runs, so that `413` — sent once more
35
+ * than the cap has been read, with no `Connection: close` — also comes
36
+ * ahead of the origin check, auth and the handler. The Rust spine counts
37
+ * such a body as its handler reads it instead, so there a refusal made on
38
+ * the headers answers first; which status an over-cap body with no declared
39
+ * length gets when another refusal also applies is implementation-defined
40
+ * across the two (`docs/security.md` §"Body Size Limiting").
31
41
  * The Node, Deno and Bun parsers refuse a request carrying both headers
32
- * with their own `400`; hyper accepts one and frames its body as chunked.
42
+ * with their own `400`; hyper accepts one, drops its `Content-Length`,
43
+ * frames the body as chunked, and closes the connection after the response.
33
44
  * - A well-formed `Content-Length` within the cap is trusted to frame the
34
45
  * body, as the runtime's HTTP parser enforces.
35
46
  *
@@ -45,8 +56,10 @@
45
56
  * connection open while a client withholds it; a request pipelined behind
46
57
  * the body is not served. Neither the header nor what the handler does with
47
58
  * the body changes that (observed on Deno 2.8.3).
48
- * - **Bun** keeps the connection, skips the declared body, and serves the
49
- * next request on it (observed on Bun 1.3.11).
59
+ * - **Bun** closes at once, the body unread, honouring the header. Bun 1.4.3
60
+ * is the tested floor: an older Bun kept the connection after the `413` and
61
+ * served the next request (correctly framed), so run Bun at the floor or
62
+ * later.
50
63
  *
51
64
  * @module
52
65
  */
@@ -77,8 +90,8 @@ export const create_body_limit_middleware = (max_size) => {
77
90
  // still a huge number (or `Infinity`), so it compares as over the cap
78
91
  if (!c.req.header('transfer-encoding') && Number(content_length) > max_size) {
79
92
  // refused on the header alone, the body untouched; `Connection: close`
80
- // makes `@hono/node-server` close at once instead of holding the
81
- // connection or serving the request behind the body
93
+ // makes `@hono/node-server` and Bun close at once instead of holding
94
+ // the connection or serving the request behind the body
82
95
  return c.json({ error: ERROR_PAYLOAD_TOO_LARGE }, 413, { connection: 'close' });
83
96
  }
84
97
  }
@@ -1 +1 @@
1
- {"version":3,"file":"db_routes.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/http/db_routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAGH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AACtD,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,wBAAwB,CAAC;AAEnD,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,EAAqC,KAAK,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAiBpF;;GAEG;AACH,MAAM,WAAW,SAAS;IACzB,UAAU,EAAE,MAAM,CAAC;CACnB;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;CAClB;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC9B,WAAW,EAAE,MAAM,CAAC;CACpB;AAED;;GAEG;AACH,MAAM,WAAW,UAAU;IAC1B,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;CACpB;AAED,sDAAsD;AACtD,eAAO,MAAM,2BAA2B,MAAM,CAAC;AAC/C,sDAAsD;AACtD,eAAO,MAAM,uBAAuB,OAAO,CAAC;AAE5C;;;;;;GAMG;AACH,eAAO,MAAM,6BAA6B,OAAO,CAAC;AAElD;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,oBAAoB,EAAE,aAAa,CAAC,MAAM,CAKrD,CAAC;AAEH;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,eAAO,MAAM,oBAAoB,EAAE,aAAa,CAAC,MAAM,CAKrD,CAAC;AAEH;;;;;GAKG;AACH,MAAM,WAAW,WAAW;IAC3B;;;;;;;;OAQG;IACH,KAAK,EAAE;QACN,IAAI,EAAE,CACL,GAAG,EAAE;YACJ,eAAe,EAAE,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;YACtC,mBAAmB,EAAE,KAAK,CAAC,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;SACvD,EACD,KAAK,EAAE;YACN,UAAU,EAAE,qBAAqB,CAAC;YAClC,UAAU,EAAE,IAAI,GAAG,IAAI,CAAC;YACxB,EAAE,EAAE,MAAM,CAAC;YACX,QAAQ,EAAE;gBAAE,KAAK,EAAE,MAAM,CAAC;gBAAC,SAAS,EAAE,MAAM,CAAC;gBAAC,EAAE,EAAE,MAAM,CAAA;aAAE,CAAC;SAC3D,KACG,IAAI,CAAC;KACV,CAAC;CACF;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB;;;;;;;OAOG;IACH,gBAAgB,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IACxC,8EAA8E;IAC9E,WAAW,CAAC,EAAE,CAAC,EAAE,EAAE,EAAE,KAAK,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IAC3D,+EAA+E;IAC/E,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;;;OAGG;IACH,oBAAoB,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;CAC7C;AA2CD;;GAEG;AACH,eAAO,MAAM,qBAAqB,GACjC,MAAM,WAAW,EACjB,SAAS,cAAc,KACrB,KAAK,CAAC,SAAS,CA4XjB,CAAC"}
1
+ {"version":3,"file":"db_routes.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/http/db_routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAGH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AACtD,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,wBAAwB,CAAC;AAEnD,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,EAAqC,KAAK,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAiBpF;;GAEG;AACH,MAAM,WAAW,SAAS;IACzB,UAAU,EAAE,MAAM,CAAC;CACnB;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;CAClB;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC9B,WAAW,EAAE,MAAM,CAAC;CACpB;AAED;;GAEG;AACH,MAAM,WAAW,UAAU;IAC1B,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;CACpB;AAED,sDAAsD;AACtD,eAAO,MAAM,2BAA2B,MAAM,CAAC;AAC/C,sDAAsD;AACtD,eAAO,MAAM,uBAAuB,OAAO,CAAC;AAE5C;;;;;;GAMG;AACH,eAAO,MAAM,6BAA6B,OAAO,CAAC;AAElD;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,oBAAoB,EAAE,aAAa,CAAC,MAAM,CAKrD,CAAC;AAEH;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,eAAO,MAAM,oBAAoB,EAAE,aAAa,CAAC,MAAM,CAKrD,CAAC;AAEH;;;;;GAKG;AACH,MAAM,WAAW,WAAW;IAC3B;;;;;;;;OAQG;IACH,KAAK,EAAE;QACN,IAAI,EAAE,CACL,GAAG,EAAE;YACJ,eAAe,EAAE,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;YACtC,mBAAmB,EAAE,KAAK,CAAC,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;SACvD,EACD,KAAK,EAAE;YACN,UAAU,EAAE,qBAAqB,CAAC;YAClC,UAAU,EAAE,IAAI,GAAG,IAAI,CAAC;YACxB,EAAE,EAAE,MAAM,CAAC;YACX,QAAQ,EAAE;gBAAE,KAAK,EAAE,MAAM,CAAC;gBAAC,SAAS,EAAE,MAAM,CAAC;gBAAC,EAAE,EAAE,MAAM,CAAA;aAAE,CAAC;SAC3D,KACG,IAAI,CAAC;KACV,CAAC;CACF;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB;;;;;;;OAOG;IACH,gBAAgB,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IACxC,8EAA8E;IAC9E,WAAW,CAAC,EAAE,CAAC,EAAE,EAAE,EAAE,KAAK,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IAC3D,+EAA+E;IAC/E,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;;;OAGG;IACH,oBAAoB,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;CAC7C;AA2CD;;GAEG;AACH,eAAO,MAAM,qBAAqB,GACjC,MAAM,WAAW,EACjB,SAAS,cAAc,KACrB,KAAK,CAAC,SAAS,CA8XjB,CAAC"}
@@ -424,9 +424,11 @@ export const create_db_route_specs = (deps, options) => {
424
424
  return c.json({ error: ERROR_ROW_NOT_FOUND }, 404);
425
425
  }
426
426
  // 23503 foreign_key_violation / 23001 restrict_violation — the
427
- // same referencing-row refusal: PG 18+ raises 23001 for `ON
428
- // DELETE RESTRICT` constraints where PG ≤17 raises 23503 for
429
- // both.
427
+ // same referencing-row refusal. Which one depends on the
428
+ // constraint: `ON DELETE RESTRICT` raises 23001 on PG 18, PGlite
429
+ // and pglet (23503 on PG ≤17), and `NO ACTION` — the default,
430
+ // when the constraint names no action — raises 23503 on all of
431
+ // them.
430
432
  if (code === '23503' || code === '23001') {
431
433
  const pg_err = err;
432
434
  log?.warn('Foreign key violation:', pg_err.detail, pg_err.constraint);
@@ -1,6 +1,6 @@
1
1
  /**
2
- * Reusable validator-schema primitives — `Username`, `UsernameProvided`,
3
- * `Email`. Lives at the top level (not inside `auth/` or `http/`) because
2
+ * Reusable validator-schema primitives — `bounded_string`, `Username`,
3
+ * `UsernameProvided`, `Email`. Lives at the top level (not inside `auth/` or `http/`) because
4
4
  * these shapes don't depend on or imply any domain — accounts hold a
5
5
  * username and email, but invites, password resets, future cell-sharing,
6
6
  * and other surfaces all reach for the same primitives without going
@@ -15,11 +15,45 @@
15
15
  * @module
16
16
  */
17
17
  import { z } from 'zod';
18
+ /** Length bounds for `bounded_string`, in UTF-16 code units. */
19
+ export interface BoundedStringOptions {
20
+ /** Shortest string admitted. Omit for no lower bound. */
21
+ min?: number;
22
+ /** Longest string admitted. */
23
+ max: number;
24
+ /** Message for the over-`max` issue, in place of Zod's default. */
25
+ max_message?: string;
26
+ }
27
+ /**
28
+ * A string schema whose length is bounded in UTF-16 code units — what
29
+ * `String.length` measures, and what the Rust spine counts
30
+ * (`encode_utf16().count()`), so an astral-plane character costs two on both.
31
+ * Every `.min()` / `.max()` length bound on a wire string in the package is
32
+ * built here, bar a bare `.min(1)`, which reads the same in either count.
33
+ *
34
+ * Zod's `.min()` / `.max()` alone don't pin the unit: depending on the
35
+ * installed Zod they count code units or Unicode code points, and the peer
36
+ * range admits both. The schema still carries them — they are what puts
37
+ * `minLength` / `maxLength` in the generated JSON Schema — and a check beside
38
+ * them settles the verdict on `String.length`: it adds the over-`max` or
39
+ * under-`min` issue when Zod raised none, and withdraws one Zod raised for a
40
+ * string that is within the bound in units. The issue it adds has the fields
41
+ * of Zod's own and, like Zod's, lets the checks chained after it run, so a
42
+ * refusal reads the same whichever count Zod used.
43
+ *
44
+ * Returns a plain `ZodString`, so format checks, `.brand()`, `.nullish()` and
45
+ * `.meta()` chain as usual. Don't chain a further `.min()` / `.max()` — pass
46
+ * the bound here.
47
+ *
48
+ * @param options - the bounds, in UTF-16 code units
49
+ * @returns the bounded string schema
50
+ */
51
+ export declare const bounded_string: ({ min, max, max_message }: BoundedStringOptions) => z.ZodString;
18
52
  /** Minimum username length (must have start + middle + end characters). */
19
53
  export declare const USERNAME_LENGTH_MIN = 3;
20
- /** Maximum username length (matches GitHub's limit). */
54
+ /** Maximum username length (matches GitHub's limit). The grammar is ASCII, so units and characters agree. */
21
55
  export declare const USERNAME_LENGTH_MAX = 39;
22
- /** Maximum length for username input on login/lookup — more permissive than `USERNAME_LENGTH_MAX` for forward-compatibility if the creation limit is raised. */
56
+ /** Maximum length for username input on login/lookup, in UTF-16 code units, measured before trimming — more permissive than `USERNAME_LENGTH_MAX` for forward-compatibility if the creation limit is raised. */
23
57
  export declare const USERNAME_PROVIDED_LENGTH_MAX = 255;
24
58
  /**
25
59
  * Username for account creation — starts with letter, alphanumeric/dash/underscore middle, ends with alphanumeric. No @ or . allowed.
@@ -1 +1 @@
1
- {"version":3,"file":"primitive_schemas.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/primitive_schemas.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB,2EAA2E;AAC3E,eAAO,MAAM,mBAAmB,IAAI,CAAC;AAErC,wDAAwD;AACxD,eAAO,MAAM,mBAAmB,KAAK,CAAC;AAEtC,gKAAgK;AAChK,eAAO,MAAM,4BAA4B,MAAM,CAAC;AAEhD;;;;;;;;;GASG;AACH,eAAO,MAAM,QAAQ,wDAKc,CAAC;AACpC,MAAM,MAAM,QAAQ,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,QAAQ,CAAC,CAAC;AAEhD;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,gBAAgB,wDAKqE,CAAC;AACnG,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAEhE;;;;GAIG;AACH,eAAO,MAAM,gBAAgB,MAAM,CAAC;AAqBpC;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,KAAK,aAYhB,CAAC;AACH,MAAM,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,KAAK,CAAC,CAAC"}
1
+ {"version":3,"file":"primitive_schemas.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/primitive_schemas.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB,gEAAgE;AAChE,MAAM,WAAW,oBAAoB;IACpC,yDAAyD;IACzD,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,+BAA+B;IAC/B,GAAG,EAAE,MAAM,CAAC;IACZ,mEAAmE;IACnE,WAAW,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,eAAO,MAAM,cAAc,GAAI,2BAA2B,oBAAoB,KAAG,CAAC,CAAC,SA8ClF,CAAC;AAEF,2EAA2E;AAC3E,eAAO,MAAM,mBAAmB,IAAI,CAAC;AAErC,6GAA6G;AAC7G,eAAO,MAAM,mBAAmB,KAAK,CAAC;AAEtC,gNAAgN;AAChN,eAAO,MAAM,4BAA4B,MAAM,CAAC;AAEhD;;;;;;;;;GASG;AACH,eAAO,MAAM,QAAQ,wDAEc,CAAC;AACpC,MAAM,MAAM,QAAQ,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,QAAQ,CAAC,CAAC;AAEhD;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,gBAAgB,wDAEqE,CAAC;AACnG,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAEhE;;;;GAIG;AACH,eAAO,MAAM,gBAAgB,MAAM,CAAC;AAqBpC;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,KAAK,aAYhB,CAAC;AACH,MAAM,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,KAAK,CAAC,CAAC"}
@@ -1,6 +1,6 @@
1
1
  /**
2
- * Reusable validator-schema primitives — `Username`, `UsernameProvided`,
3
- * `Email`. Lives at the top level (not inside `auth/` or `http/`) because
2
+ * Reusable validator-schema primitives — `bounded_string`, `Username`,
3
+ * `UsernameProvided`, `Email`. Lives at the top level (not inside `auth/` or `http/`) because
4
4
  * these shapes don't depend on or imply any domain — accounts hold a
5
5
  * username and email, but invites, password resets, future cell-sharing,
6
6
  * and other surfaces all reach for the same primitives without going
@@ -15,12 +15,82 @@
15
15
  * @module
16
16
  */
17
17
  import { z } from 'zod';
18
- // TODO consider `.brand()` on Username and Email for compile-time safety
18
+ /**
19
+ * A string schema whose length is bounded in UTF-16 code units — what
20
+ * `String.length` measures, and what the Rust spine counts
21
+ * (`encode_utf16().count()`), so an astral-plane character costs two on both.
22
+ * Every `.min()` / `.max()` length bound on a wire string in the package is
23
+ * built here, bar a bare `.min(1)`, which reads the same in either count.
24
+ *
25
+ * Zod's `.min()` / `.max()` alone don't pin the unit: depending on the
26
+ * installed Zod they count code units or Unicode code points, and the peer
27
+ * range admits both. The schema still carries them — they are what puts
28
+ * `minLength` / `maxLength` in the generated JSON Schema — and a check beside
29
+ * them settles the verdict on `String.length`: it adds the over-`max` or
30
+ * under-`min` issue when Zod raised none, and withdraws one Zod raised for a
31
+ * string that is within the bound in units. The issue it adds has the fields
32
+ * of Zod's own and, like Zod's, lets the checks chained after it run, so a
33
+ * refusal reads the same whichever count Zod used.
34
+ *
35
+ * Returns a plain `ZodString`, so format checks, `.brand()`, `.nullish()` and
36
+ * `.meta()` chain as usual. Don't chain a further `.min()` / `.max()` — pass
37
+ * the bound here.
38
+ *
39
+ * @param options - the bounds, in UTF-16 code units
40
+ * @returns the bounded string schema
41
+ */
42
+ export const bounded_string = ({ min, max, max_message }) => {
43
+ let schema = z.string();
44
+ if (min !== undefined)
45
+ schema = schema.min(min);
46
+ schema = max_message === undefined ? schema.max(max) : schema.max(max, max_message);
47
+ return schema.check((ctx) => {
48
+ const { length } = ctx.value;
49
+ const { issues } = ctx;
50
+ const too_big = issues.findIndex((i) => i.code === 'too_big' && i.origin === 'string' && i.maximum === max);
51
+ if (length > max) {
52
+ if (too_big === -1) {
53
+ issues.push({
54
+ origin: 'string',
55
+ code: 'too_big',
56
+ maximum: max,
57
+ inclusive: true,
58
+ input: ctx.value,
59
+ // as on Zod's own length issues: the checks chained after still run
60
+ continue: true,
61
+ // `path` ahead of `message`, the key order of Zod's finalized issue
62
+ ...(max_message !== undefined && { path: [], message: max_message })
63
+ });
64
+ }
65
+ }
66
+ else if (too_big !== -1) {
67
+ issues.splice(too_big, 1);
68
+ }
69
+ if (min === undefined)
70
+ return;
71
+ const too_small = issues.findIndex((i) => i.code === 'too_small' && i.origin === 'string' && i.minimum === min);
72
+ if (length < min) {
73
+ if (too_small === -1) {
74
+ issues.push({
75
+ origin: 'string',
76
+ code: 'too_small',
77
+ minimum: min,
78
+ inclusive: true,
79
+ input: ctx.value,
80
+ continue: true
81
+ });
82
+ }
83
+ }
84
+ else if (too_small !== -1) {
85
+ issues.splice(too_small, 1);
86
+ }
87
+ });
88
+ };
19
89
  /** Minimum username length (must have start + middle + end characters). */
20
90
  export const USERNAME_LENGTH_MIN = 3;
21
- /** Maximum username length (matches GitHub's limit). */
91
+ /** Maximum username length (matches GitHub's limit). The grammar is ASCII, so units and characters agree. */
22
92
  export const USERNAME_LENGTH_MAX = 39;
23
- /** Maximum length for username input on login/lookup — more permissive than `USERNAME_LENGTH_MAX` for forward-compatibility if the creation limit is raised. */
93
+ /** Maximum length for username input on login/lookup, in UTF-16 code units, measured before trimming — more permissive than `USERNAME_LENGTH_MAX` for forward-compatibility if the creation limit is raised. */
24
94
  export const USERNAME_PROVIDED_LENGTH_MAX = 255;
25
95
  /**
26
96
  * Username for account creation — starts with letter, alphanumeric/dash/underscore middle, ends with alphanumeric. No @ or . allowed.
@@ -32,10 +102,7 @@ export const USERNAME_PROVIDED_LENGTH_MAX = 255;
32
102
  * per-caller `trim().toLowerCase()` ritual and keeps the
33
103
  * `LOWER(username) = LOWER($1)` lookup contract simple.
34
104
  */
35
- export const Username = z
36
- .string()
37
- .min(USERNAME_LENGTH_MIN)
38
- .max(USERNAME_LENGTH_MAX)
105
+ export const Username = bounded_string({ min: USERNAME_LENGTH_MIN, max: USERNAME_LENGTH_MAX })
39
106
  .regex(/^[a-zA-Z][0-9a-zA-Z_-]*[0-9a-zA-Z]$/)
40
107
  .transform((s) => s.toLowerCase());
41
108
  /**
@@ -53,10 +120,7 @@ export const Username = z
53
120
  * input, not a wrong credential. Keeps the Rust spine's
54
121
  * `account_login` (which rejects empty-after-trim) in parity.
55
122
  */
56
- export const UsernameProvided = z
57
- .string()
58
- .min(1)
59
- .max(USERNAME_PROVIDED_LENGTH_MAX)
123
+ export const UsernameProvided = bounded_string({ min: 1, max: USERNAME_PROVIDED_LENGTH_MAX })
60
124
  .transform((s) => s.trim().toLowerCase())
61
125
  .refine((s) => s.length > 0, { message: 'Username must not be empty after trimming whitespace' });
62
126
  /**
@@ -50,7 +50,6 @@ export declare const validate_facts_internal_location: (config: string, facts_lo
50
50
  * an explicit cast, so the factory is the only ordinary construction path.
51
51
  */
52
52
  export declare const XAccelConfig: z.core.$ZodBranded<z.ZodObject<{
53
- /** The validated redirect prefix the serving path prepends (e.g. `/_facts/`). */
54
53
  redirect_prefix: z.ZodString;
55
54
  }, z.core.$strict>, "XAccelConfig", "out">;
56
55
  export type XAccelConfig = z.infer<typeof XAccelConfig>;
@@ -1 +1 @@
1
- {"version":3,"file":"x_accel.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/server/x_accel.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,0CAA0C;AAC1C,MAAM,WAAW,oBAAoB;IACpC,gEAAgE;IAChE,EAAE,EAAE,OAAO,CAAC;IACZ,iEAAiE;IACjE,MAAM,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;CACtB;AAuDD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,gCAAgC,GAC5C,QAAQ,MAAM,EACd,gBAAgB,MAAM,KACpB,oBAqBF,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,YAAY;IAEvB,iFAAiF;;0CAG5D,CAAC;AACxB,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,YAAY,CAAC,CAAC;AAExD;;;GAGG;AACH,qBAAa,iBAAkB,SAAQ,KAAK;IAC3C,0DAA0D;IAC1D,QAAQ,CAAC,MAAM,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;gBACnB,MAAM,EAAE,KAAK,CAAC,MAAM,CAAC;CAKjC;AAED;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,qBAAqB,GACjC,iBAAiB,MAAM,EACvB,cAAc,MAAM,KAClB,YAMF,CAAC"}
1
+ {"version":3,"file":"x_accel.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/server/x_accel.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,0CAA0C;AAC1C,MAAM,WAAW,oBAAoB;IACpC,gEAAgE;IAChE,EAAE,EAAE,OAAO,CAAC;IACZ,iEAAiE;IACjE,MAAM,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;CACtB;AAuDD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,gCAAgC,GAC5C,QAAQ,MAAM,EACd,gBAAgB,MAAM,KACpB,oBAqBF,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,YAAY;;0CAKF,CAAC;AACxB,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,YAAY,CAAC,CAAC;AAExD;;;GAGG;AACH,qBAAa,iBAAkB,SAAQ,KAAK;IAC3C,0DAA0D;IAC1D,QAAQ,CAAC,MAAM,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;gBACnB,MAAM,EAAE,KAAK,CAAC,MAAM,CAAC;CAKjC;AAED;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,qBAAqB,GACjC,iBAAiB,MAAM,EACvB,cAAc,MAAM,KAClB,YAMF,CAAC"}