@fuzdev/fuz_app 0.62.0 → 0.64.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 (136) hide show
  1. package/dist/actions/CLAUDE.md +139 -24
  2. package/dist/actions/action_rpc.d.ts +10 -0
  3. package/dist/actions/action_rpc.d.ts.map +1 -1
  4. package/dist/actions/action_rpc.js +1 -1
  5. package/dist/actions/action_spec.d.ts +1 -1
  6. package/dist/actions/action_spec.js +1 -1
  7. package/dist/actions/connection_closer.d.ts +68 -0
  8. package/dist/actions/connection_closer.d.ts.map +1 -0
  9. package/dist/actions/connection_closer.js +41 -0
  10. package/dist/actions/perform_action.d.ts.map +1 -1
  11. package/dist/actions/perform_action.js +1 -0
  12. package/dist/actions/register_action_ws.d.ts.map +1 -1
  13. package/dist/actions/register_action_ws.js +23 -2
  14. package/dist/actions/register_ws_endpoint.d.ts +11 -9
  15. package/dist/actions/register_ws_endpoint.d.ts.map +1 -1
  16. package/dist/actions/register_ws_endpoint.js +5 -5
  17. package/dist/actions/transports_ws_auth_guard.d.ts +24 -8
  18. package/dist/actions/transports_ws_auth_guard.d.ts.map +1 -1
  19. package/dist/actions/transports_ws_auth_guard.js +23 -7
  20. package/dist/actions/ws_endpoint_spec.d.ts +119 -0
  21. package/dist/actions/ws_endpoint_spec.d.ts.map +1 -0
  22. package/dist/actions/ws_endpoint_spec.js +13 -0
  23. package/dist/auth/CLAUDE.md +124 -39
  24. package/dist/auth/account_action_specs.d.ts +7 -1
  25. package/dist/auth/account_action_specs.d.ts.map +1 -1
  26. package/dist/auth/account_action_specs.js +11 -4
  27. package/dist/auth/account_actions.d.ts +13 -0
  28. package/dist/auth/account_actions.d.ts.map +1 -1
  29. package/dist/auth/account_actions.js +40 -5
  30. package/dist/auth/account_routes.d.ts +12 -2
  31. package/dist/auth/account_routes.d.ts.map +1 -1
  32. package/dist/auth/account_routes.js +63 -12
  33. package/dist/auth/account_schema.d.ts +5 -5
  34. package/dist/auth/account_schema.js +2 -2
  35. package/dist/auth/actor_lookup_actions.d.ts +1 -1
  36. package/dist/auth/actor_lookup_actions.js +1 -1
  37. package/dist/auth/actor_lookup_queries.d.ts +1 -1
  38. package/dist/auth/actor_lookup_queries.js +1 -1
  39. package/dist/auth/actor_search_action_specs.d.ts +1 -1
  40. package/dist/auth/actor_search_action_specs.js +1 -1
  41. package/dist/auth/actor_search_actions.d.ts +1 -1
  42. package/dist/auth/actor_search_actions.js +1 -1
  43. package/dist/auth/actor_search_queries.d.ts +1 -1
  44. package/dist/auth/actor_search_queries.js +1 -1
  45. package/dist/auth/admin_action_specs.d.ts +8 -8
  46. package/dist/auth/admin_actions.d.ts +11 -0
  47. package/dist/auth/admin_actions.d.ts.map +1 -1
  48. package/dist/auth/admin_actions.js +25 -0
  49. package/dist/auth/all_action_spec_registries.d.ts +2 -2
  50. package/dist/auth/all_action_spec_registries.js +2 -2
  51. package/dist/auth/audit_emitter.d.ts +56 -12
  52. package/dist/auth/audit_emitter.d.ts.map +1 -1
  53. package/dist/auth/audit_emitter.js +38 -12
  54. package/dist/auth/audit_log_routes.d.ts +1 -1
  55. package/dist/auth/audit_log_routes.js +1 -1
  56. package/dist/auth/audit_log_schema.d.ts +30 -3
  57. package/dist/auth/audit_log_schema.d.ts.map +1 -1
  58. package/dist/auth/audit_log_schema.js +21 -3
  59. package/dist/auth/bootstrap_routes.d.ts +1 -1
  60. package/dist/auth/invite_schema.d.ts +2 -2
  61. package/dist/auth/request_context.d.ts +1 -1
  62. package/dist/auth/signup_routes.d.ts +1 -1
  63. package/dist/auth/standard_rpc_actions.d.ts +1 -0
  64. package/dist/auth/standard_rpc_actions.d.ts.map +1 -1
  65. package/dist/auth/standard_rpc_actions.js +1 -0
  66. package/dist/env/update_env_variable.js +1 -1
  67. package/dist/http/CLAUDE.md +42 -26
  68. package/dist/http/ip_canonical.d.ts +99 -0
  69. package/dist/http/ip_canonical.d.ts.map +1 -0
  70. package/dist/http/ip_canonical.js +191 -0
  71. package/dist/http/origin.d.ts +13 -5
  72. package/dist/http/origin.d.ts.map +1 -1
  73. package/dist/http/origin.js +13 -31
  74. package/dist/http/pending_effects.d.ts +1 -1
  75. package/dist/http/pending_effects.js +1 -1
  76. package/dist/http/proxy.d.ts +13 -5
  77. package/dist/http/proxy.d.ts.map +1 -1
  78. package/dist/http/proxy.js +15 -23
  79. package/dist/http/surface.d.ts +50 -0
  80. package/dist/http/surface.d.ts.map +1 -1
  81. package/dist/http/surface.js +27 -1
  82. package/dist/primitive_schemas.d.ts +20 -4
  83. package/dist/primitive_schemas.d.ts.map +1 -1
  84. package/dist/primitive_schemas.js +25 -4
  85. package/dist/realtime/sse_auth_guard.d.ts +16 -4
  86. package/dist/realtime/sse_auth_guard.d.ts.map +1 -1
  87. package/dist/realtime/sse_auth_guard.js +15 -3
  88. package/dist/server/app_backend.d.ts +66 -19
  89. package/dist/server/app_backend.d.ts.map +1 -1
  90. package/dist/server/app_backend.js +57 -34
  91. package/dist/server/app_server.d.ts +60 -0
  92. package/dist/server/app_server.d.ts.map +1 -1
  93. package/dist/server/app_server.js +95 -2
  94. package/dist/server/startup.d.ts.map +1 -1
  95. package/dist/server/startup.js +12 -0
  96. package/dist/testing/CLAUDE.md +91 -71
  97. package/dist/testing/admin_integration.d.ts.map +1 -1
  98. package/dist/testing/admin_integration.js +4 -5
  99. package/dist/testing/adversarial_headers.d.ts +6 -0
  100. package/dist/testing/adversarial_headers.d.ts.map +1 -1
  101. package/dist/testing/adversarial_headers.js +13 -5
  102. package/dist/testing/app_server.d.ts +33 -32
  103. package/dist/testing/app_server.d.ts.map +1 -1
  104. package/dist/testing/app_server.js +4 -13
  105. package/dist/testing/attack_surface.d.ts +8 -7
  106. package/dist/testing/attack_surface.d.ts.map +1 -1
  107. package/dist/testing/attack_surface.js +12 -8
  108. package/dist/testing/audit_completeness.d.ts.map +1 -1
  109. package/dist/testing/audit_completeness.js +20 -6
  110. package/dist/testing/audit_drift_guard.d.ts +116 -0
  111. package/dist/testing/audit_drift_guard.d.ts.map +1 -0
  112. package/dist/testing/audit_drift_guard.js +134 -0
  113. package/dist/testing/connection_closer_helpers.d.ts +44 -0
  114. package/dist/testing/connection_closer_helpers.d.ts.map +1 -0
  115. package/dist/testing/connection_closer_helpers.js +48 -0
  116. package/dist/testing/integration.d.ts.map +1 -1
  117. package/dist/testing/integration.js +7 -9
  118. package/dist/testing/rate_limiting.js +4 -4
  119. package/dist/testing/rpc_helpers.d.ts +2 -1
  120. package/dist/testing/rpc_helpers.d.ts.map +1 -1
  121. package/dist/testing/rpc_round_trip.d.ts.map +1 -1
  122. package/dist/testing/rpc_round_trip.js +6 -8
  123. package/dist/testing/sse_round_trip.d.ts.map +1 -1
  124. package/dist/testing/sse_round_trip.js +12 -6
  125. package/dist/testing/stubs.d.ts +11 -0
  126. package/dist/testing/stubs.d.ts.map +1 -1
  127. package/dist/testing/stubs.js +4 -0
  128. package/dist/testing/surface_invariants.d.ts +66 -1
  129. package/dist/testing/surface_invariants.d.ts.map +1 -1
  130. package/dist/testing/surface_invariants.js +103 -1
  131. package/dist/ui/CLAUDE.md +13 -18
  132. package/dist/ui/SurfaceExplorer.svelte +161 -2
  133. package/dist/ui/SurfaceExplorer.svelte.d.ts.map +1 -1
  134. package/dist/ui/keyed_async_slot.svelte.d.ts +1 -1
  135. package/dist/ui/keyed_async_slot.svelte.js +1 -1
  136. package/package.json +1 -1
@@ -8,24 +8,25 @@
8
8
  * @module
9
9
  */
10
10
  import { convertIPv4ToBinary, convertIPv6ToBinary, distinctRemoteAddr } from 'hono/utils/ipaddr';
11
+ import { canonicalize_ip, IP_LITERAL_CHARS } from './ip_canonical.js';
11
12
  /**
12
13
  * Normalize an IP address for consistent matching and storage.
13
14
  *
14
- * - Strips `::ffff:` prefix from IPv4-mapped IPv6 addresses
15
- * (e.g. `::ffff:127.0.0.1` `127.0.0.1`)
16
- * - Lowercases for case-insensitive IPv6 comparison
17
- * - Idempotent: calling twice produces the same result
18
- * - Safe on non-IP strings: `normalize_ip('unknown')` returns `'unknown'`
15
+ * Delegates to `canonicalize_ip` from `ip_canonical.ts` collapses
16
+ * RFC 5952-equivalent IPv6 forms (`::1`, `::0001`, `0:0:0:0:0:0:0:1`)
17
+ * into a single key, emits IPv4-mapped IPv6 in dotted form, and
18
+ * strips the `::ffff:` prefix from dotted IPv4-mapped values so the
19
+ * bucket collapses to plain IPv4.
20
+ *
21
+ * - Lowercases for case-insensitive IPv6 comparison.
22
+ * - Idempotent: calling twice produces the same result.
23
+ * - Safe on non-IP strings: `normalize_ip('unknown')` returns `'unknown'`.
24
+ * Malformed inputs (`'attacker:controlled'`, `'::1\n'`,
25
+ * `'203.0.113.1:8080'`) pass through unchanged so downstream
26
+ * `validate_ip_strict` can still reject them — canonicalization
27
+ * never erases the malformed-form signal.
19
28
  */
20
- export const normalize_ip = (ip) => {
21
- const lowered = ip.toLowerCase();
22
- // Strip ::ffff: prefix only when remainder contains a dot (IPv4-mapped IPv6).
23
- // This distinguishes ::ffff:127.0.0.1 (IPv4-mapped) from ::ffff:1 (pure IPv6).
24
- if (lowered.startsWith('::ffff:') && lowered.substring(7).includes('.')) {
25
- return lowered.substring(7);
26
- }
27
- return lowered;
28
- };
29
+ export const normalize_ip = (ip) => canonicalize_ip(ip);
29
30
  /**
30
31
  * Parse a trusted proxy entry string into a structured form.
31
32
  *
@@ -91,15 +92,6 @@ const cidr_contains = (ip_binary, network, prefix, total_bits) => {
91
92
  const shift = BigInt(total_bits - prefix);
92
93
  return ip_binary >> shift === network >> shift;
93
94
  };
94
- /**
95
- * Allowed character set for a bare IP literal.
96
- *
97
- * Covers the union of IPv4 (digits + `.`), IPv6 (hex digits + `:`), and
98
- * IPv4-mapped IPv6 forms (`::ffff:127.0.0.1`). Anything outside this
99
- * set — brackets, whitespace, control bytes, letters g-z — disqualifies
100
- * the input regardless of what Hono's parser does with it.
101
- */
102
- const IP_LITERAL_CHARS = /^[0-9a-fA-F.:]+$/;
103
95
  /**
104
96
  * Strict IP validity check.
105
97
  *
@@ -13,6 +13,8 @@ import type { RouteSpec } from './route_spec.js';
13
13
  import type { RouteAuth } from './auth_shape.js';
14
14
  import type { RateLimitKey, RouteErrorSchemas } from './error_schemas.js';
15
15
  import type { RpcAction } from '../actions/action_rpc.js';
16
+ import type { ActionKind } from '../actions/action_spec.js';
17
+ import type { WsEndpointSpec } from '../actions/ws_endpoint_spec.js';
16
18
  import type { Sensitivity } from '../sensitivity.js';
17
19
  /** A route in the generated attack surface (JSON-serializable). */
18
20
  export interface AppSurfaceRoute {
@@ -79,6 +81,45 @@ export interface AppSurfaceRpcEndpoint {
79
81
  path: string;
80
82
  methods: Array<AppSurfaceRpcMethod>;
81
83
  }
84
+ /** A method within a WebSocket endpoint in the generated attack surface (JSON-serializable). */
85
+ export interface AppSurfaceWsMethod {
86
+ name: string;
87
+ /** `request_response` (inbound dispatch) or `remote_notification` (server → client). */
88
+ kind: ActionKind;
89
+ /**
90
+ * Per-action auth shape. `null` for `remote_notification` (server →
91
+ * client) — notifications have no inbound dispatch and therefore no
92
+ * auth axis. `request_response` always carries a `RouteAuth`.
93
+ */
94
+ auth: RouteAuth | null;
95
+ /** JSON Schema of the input schema. `null` for nullary inputs. */
96
+ input_schema: unknown;
97
+ /** JSON Schema of the output schema. */
98
+ output_schema: unknown;
99
+ description: string;
100
+ side_effects: boolean;
101
+ /** Rate limit key declared on the action spec. `null` when not rate-limited. */
102
+ rate_limit_key: RateLimitKey | null;
103
+ }
104
+ /** A WebSocket endpoint in the generated attack surface (JSON-serializable). */
105
+ export interface AppSurfaceWsEndpoint {
106
+ path: string;
107
+ /**
108
+ * Upgrade-time origin allowlist, one entry per `WsEndpointSpec.allowed_origins`
109
+ * regex stringified via `RegExp.prototype.toString()` (`'/<source>/<flags>'`).
110
+ * Empty array when no origins were declared (any-origin); reviewers read this
111
+ * as the exact pattern matched at the upgrade gate, not a wildcard
112
+ * approximation. Reconstruct via `new RegExp(source, flags)` if needed.
113
+ */
114
+ allowed_origins: ReadonlyArray<string>;
115
+ /**
116
+ * Upgrade-time role gate — empty array when no `required_roles` was
117
+ * declared (any-authenticated). Documents the coarse gate; per-action
118
+ * `auth` on each method covers per-message authorization.
119
+ */
120
+ required_roles: ReadonlyArray<string>;
121
+ methods: Array<AppSurfaceWsMethod>;
122
+ }
82
123
  /** Assembly-time diagnostic collected during surface generation or server assembly. */
83
124
  export interface AppSurfaceDiagnostic {
84
125
  level: 'warning' | 'info';
@@ -91,6 +132,7 @@ export interface AppSurface {
91
132
  middleware: Array<AppSurfaceMiddleware>;
92
133
  routes: Array<AppSurfaceRoute>;
93
134
  rpc_endpoints: Array<AppSurfaceRpcEndpoint>;
135
+ ws_endpoints: Array<AppSurfaceWsEndpoint>;
94
136
  env: Array<AppSurfaceEnv>;
95
137
  events: Array<AppSurfaceEvent>;
96
138
  diagnostics: Array<AppSurfaceDiagnostic>;
@@ -106,6 +148,7 @@ export interface AppSurfaceSpec {
106
148
  route_specs: Array<RouteSpec>;
107
149
  middleware_specs: Array<MiddlewareSpec>;
108
150
  rpc_endpoints: Array<RpcEndpointSpec>;
151
+ ws_endpoints: Array<WsEndpointSpec>;
109
152
  }
110
153
  /** An RPC endpoint definition for surface generation. */
111
154
  export interface RpcEndpointSpec {
@@ -119,6 +162,13 @@ export interface GenerateAppSurfaceOptions {
119
162
  env_schema?: z.ZodObject;
120
163
  event_specs?: Array<EventSpec>;
121
164
  rpc_endpoints?: Array<RpcEndpointSpec>;
165
+ /**
166
+ * Mounted WS endpoints (the same array `create_app_server.ws_endpoints`
167
+ * auto-mounts). Each entry's actions surface into
168
+ * `AppSurface.ws_endpoints[i].methods` for attack-surface tests +
169
+ * startup logging.
170
+ */
171
+ ws_endpoints?: ReadonlyArray<WsEndpointSpec>;
122
172
  }
123
173
  /**
124
174
  * Collect error schemas from all middleware that applies to a route path.
@@ -1 +1 @@
1
- {"version":3,"file":"surface.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/http/surface.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"surface.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/http/surface.ts"],"names":[],"mappings":"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"}
@@ -60,7 +60,7 @@ export const events_to_surface = (event_specs) => {
60
60
  * and optional env/event metadata.
61
61
  */
62
62
  export const generate_app_surface = (options) => {
63
- const { route_specs, middleware_specs, env_schema, event_specs, rpc_endpoints } = options;
63
+ const { route_specs, middleware_specs, env_schema, event_specs, rpc_endpoints, ws_endpoints } = options;
64
64
  const diagnostics = [];
65
65
  // Spec-level diagnostics: check for non-strict input schemas
66
66
  for (const r of route_specs) {
@@ -141,6 +141,31 @@ export const generate_app_surface = (options) => {
141
141
  })),
142
142
  }))
143
143
  : [],
144
+ ws_endpoints: ws_endpoints?.length
145
+ ? ws_endpoints.map((ep) => ({
146
+ path: ep.path,
147
+ allowed_origins: ep.allowed_origins.map((re) => re.toString()),
148
+ required_roles: ep.required_roles ?? [],
149
+ // `local_call` specs are frontend-side helpers — registry-only
150
+ // on the backend, never dispatched over WS. Drop them from the
151
+ // surface so attack-surface tests reflect dispatchable methods
152
+ // only. Notifications are kept (server → client emit).
153
+ methods: ep.actions
154
+ .filter((a) => a.spec.kind !== 'local_call')
155
+ .map((a) => ({
156
+ name: a.spec.method,
157
+ kind: a.spec.kind,
158
+ // `request_response` carries a `RouteAuth`; notifications
159
+ // have `auth: null` (server-pushed, no inbound dispatch).
160
+ auth: a.spec.auth,
161
+ input_schema: schema_to_surface(a.spec.input),
162
+ output_schema: schema_to_surface(a.spec.output),
163
+ description: a.spec.description,
164
+ side_effects: a.spec.side_effects,
165
+ rate_limit_key: a.spec.kind === 'request_response' ? (a.spec.rate_limit ?? null) : null,
166
+ })),
167
+ }))
168
+ : [],
144
169
  env: env_schema ? env_schema_to_surface(env_schema) : [],
145
170
  events: event_specs?.length ? events_to_surface(event_specs) : [],
146
171
  };
@@ -155,5 +180,6 @@ export const create_app_surface_spec = (options) => {
155
180
  route_specs: options.route_specs,
156
181
  middleware_specs: options.middleware_specs,
157
182
  rpc_endpoints: options.rpc_endpoints ?? [],
183
+ ws_endpoints: options.ws_endpoints ? [...options.ws_endpoints] : [],
158
184
  };
159
185
  };
@@ -21,11 +21,27 @@ export declare const USERNAME_LENGTH_MIN = 3;
21
21
  export declare const USERNAME_LENGTH_MAX = 39;
22
22
  /** Maximum length for username input on login/lookup — more permissive than `USERNAME_LENGTH_MAX` for forward-compatibility if the creation limit is raised. */
23
23
  export declare const USERNAME_PROVIDED_LENGTH_MAX = 255;
24
- /** Username for account creation — starts with letter, alphanumeric/dash/underscore middle, ends with alphanumeric. No @ or . allowed. */
25
- export declare const Username: z.ZodString;
24
+ /**
25
+ * Username for account creation — starts with letter, alphanumeric/dash/underscore middle, ends with alphanumeric. No @ or . allowed.
26
+ *
27
+ * Canonicalized to lowercase at parse time. The regex rejects whitespace
28
+ * outright, so `.trim()` is unnecessary here. Storage is canonical across
29
+ * every creation site (bootstrap, signup, admin-create, invite acceptance)
30
+ * because the schema is the single source of truth — eliminates the
31
+ * per-caller `trim().toLowerCase()` ritual and keeps the
32
+ * `LOWER(username) = LOWER($1)` lookup contract simple.
33
+ */
34
+ export declare const Username: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
26
35
  export type Username = z.infer<typeof Username>;
27
- /** Username submitted for login or lookup — minimal validation for forward-compatibility if format rules change. */
28
- export declare const UsernameProvided: z.ZodString;
36
+ /**
37
+ * Username submitted for login or lookup — minimal validation for forward-compatibility if format rules change.
38
+ *
39
+ * Canonicalized via `.trim().toLowerCase()` at parse time so login's
40
+ * per-account rate-limit key and DB lookup see a uniform value
41
+ * regardless of casing or surrounding whitespace. Mirrors the storage
42
+ * canonicalization on `Username` so submission and storage agree.
43
+ */
44
+ export declare const UsernameProvided: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
29
45
  export type UsernameProvided = z.infer<typeof UsernameProvided>;
30
46
  /**
31
47
  * Email validation. Lives here rather than `@fuzdev/fuz_util` because every
@@ -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,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAItB,2EAA2E;AAC3E,eAAO,MAAM,mBAAmB,IAAI,CAAC;AAErC,wDAAwD;AACxD,eAAO,MAAM,mBAAmB,KAAK,CAAC;AAEtC,gKAAgK;AAChK,eAAO,MAAM,4BAA4B,MAAM,CAAC;AAEhD,0IAA0I;AAC1I,eAAO,MAAM,QAAQ,aAIyB,CAAC;AAC/C,MAAM,MAAM,QAAQ,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,QAAQ,CAAC,CAAC;AAEhD,oHAAoH;AACpH,eAAO,MAAM,gBAAgB,aAAsD,CAAC;AACpF,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAEhE;;;;;;GAMG;AACH,eAAO,MAAM,KAAK,YAAY,CAAC;AAC/B,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,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAItB,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;;;;;;;GAOG;AACH,eAAO,MAAM,gBAAgB,wDAIa,CAAC;AAC3C,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAEhE;;;;;;GAMG;AACH,eAAO,MAAM,KAAK,YAAY,CAAC;AAC/B,MAAM,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,KAAK,CAAC,CAAC"}
@@ -22,14 +22,35 @@ export const USERNAME_LENGTH_MIN = 3;
22
22
  export const USERNAME_LENGTH_MAX = 39;
23
23
  /** Maximum length for username input on login/lookup — more permissive than `USERNAME_LENGTH_MAX` for forward-compatibility if the creation limit is raised. */
24
24
  export const USERNAME_PROVIDED_LENGTH_MAX = 255;
25
- /** Username for account creation — starts with letter, alphanumeric/dash/underscore middle, ends with alphanumeric. No @ or . allowed. */
25
+ /**
26
+ * Username for account creation — starts with letter, alphanumeric/dash/underscore middle, ends with alphanumeric. No @ or . allowed.
27
+ *
28
+ * Canonicalized to lowercase at parse time. The regex rejects whitespace
29
+ * outright, so `.trim()` is unnecessary here. Storage is canonical across
30
+ * every creation site (bootstrap, signup, admin-create, invite acceptance)
31
+ * because the schema is the single source of truth — eliminates the
32
+ * per-caller `trim().toLowerCase()` ritual and keeps the
33
+ * `LOWER(username) = LOWER($1)` lookup contract simple.
34
+ */
26
35
  export const Username = z
27
36
  .string()
28
37
  .min(USERNAME_LENGTH_MIN)
29
38
  .max(USERNAME_LENGTH_MAX)
30
- .regex(/^[a-zA-Z][0-9a-zA-Z_-]*[0-9a-zA-Z]$/);
31
- /** Username submitted for login or lookup — minimal validation for forward-compatibility if format rules change. */
32
- export const UsernameProvided = z.string().min(1).max(USERNAME_PROVIDED_LENGTH_MAX);
39
+ .regex(/^[a-zA-Z][0-9a-zA-Z_-]*[0-9a-zA-Z]$/)
40
+ .transform((s) => s.toLowerCase());
41
+ /**
42
+ * Username submitted for login or lookup — minimal validation for forward-compatibility if format rules change.
43
+ *
44
+ * Canonicalized via `.trim().toLowerCase()` at parse time so login's
45
+ * per-account rate-limit key and DB lookup see a uniform value
46
+ * regardless of casing or surrounding whitespace. Mirrors the storage
47
+ * canonicalization on `Username` so submission and storage agree.
48
+ */
49
+ export const UsernameProvided = z
50
+ .string()
51
+ .min(1)
52
+ .max(USERNAME_PROVIDED_LENGTH_MAX)
53
+ .transform((s) => s.trim().toLowerCase());
33
54
  /**
34
55
  * Email validation. Lives here rather than `@fuzdev/fuz_util` because every
35
56
  * current consumer pairs it with `Username` (signup, invites, audit log) —
@@ -58,7 +58,7 @@ export interface AuditLogSse {
58
58
  subscribe: (stream: SseStream<SseNotification>, options?: SubscribeOptions) => () => void;
59
59
  /** Logger — pass as part of `stream` option to `create_audit_log_route_specs`. */
60
60
  log: Logger;
61
- /** Combined broadcast + guard callback. Pass as `on_audit_event` on `CreateAppBackendOptions`. */
61
+ /** Combined broadcast + guard callback. Wired by `create_app_server`'s `audit_log_sse` option, or compose inside the consumer's `audit_factory` body. */
62
62
  on_audit_event: (event: AuditLogEvent) => void;
63
63
  /** The underlying registry — exposed for subscriber count monitoring. */
64
64
  registry: SubscriberRegistry<SseNotification>;
@@ -86,7 +86,15 @@ export declare const AUDIT_LOG_SSE_MAX_PER_SCOPE = 10;
86
86
  *
87
87
  * Combines `SubscriberRegistry`, `create_sse_auth_guard`, and the broadcast
88
88
  * call into a single object. The result satisfies `AuditLogRouteOptions['stream']`
89
- * and provides the `on_audit_event` callback for `CreateAppBackendOptions`.
89
+ * and provides the `on_audit_event` listener for the audit emitter's chain.
90
+ *
91
+ * Most consumers pass `audit_log_sse: true` to `create_app_server` and never
92
+ * touch this directly — the factory builds an `AuditLogSse`, appends
93
+ * `audit_sse.on_audit_event` to `backend.deps.audit.on_event_chain`, and
94
+ * exposes it via `AppServerContext.audit_sse`. Reach for the manual path
95
+ * (compose inside `audit_factory` body, or
96
+ * `audit.on_event_chain.push(audit_sse.on_audit_event)` post-assembly) only
97
+ * when wiring outside `create_app_server`.
90
98
  *
91
99
  * @param options - factory options
92
100
  * @returns audit log SSE setup (stream options + `on_audit_event` + registry)
@@ -95,8 +103,12 @@ export declare const AUDIT_LOG_SSE_MAX_PER_SCOPE = 10;
95
103
  * ```ts
96
104
  * const audit_sse = create_audit_log_sse({log});
97
105
  *
98
- * // In create_app_backend options:
99
- * on_audit_event: audit_sse.on_audit_event,
106
+ * // Inside the audit_factory body on CreateAppBackendOptions:
107
+ * audit_factory: ({db, log}) => create_audit_emitter({
108
+ * db,
109
+ * log,
110
+ * on_audit_event: audit_sse.on_audit_event,
111
+ * }),
100
112
  *
101
113
  * // In create_route_specs:
102
114
  * create_audit_log_route_specs({stream: audit_sse});
@@ -1 +1 @@
1
- {"version":3,"file":"sse_auth_guard.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/realtime/sse_auth_guard.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,yBAAyB,CAAC;AAEpD,OAAO,EAGN,KAAK,aAAa,EAClB,MAAM,6BAA6B,CAAC;AACrC,OAAO,EAAC,kBAAkB,EAAE,KAAK,gBAAgB,EAAC,MAAM,0BAA0B,CAAC;AACnF,OAAO,KAAK,EAAC,SAAS,EAAE,eAAe,EAAE,SAAS,EAAC,MAAM,UAAU,CAAC;AAEpE,2DAA2D;AAC3D,eAAO,MAAM,iBAAiB,cAAc,CAAC;AAE7C;;;;;;;;;;GAUG;AACH,eAAO,MAAM,sBAAsB,EAAE,WAAW,CAAC,MAAM,CAKrD,CAAC;AAEH;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,qBAAqB,GAAI,CAAC,EACtC,UAAU,kBAAkB,CAAC,CAAC,CAAC,EAC/B,eAAe,MAAM,GAAG,IAAI,EAC5B,KAAK,MAAM,KACT,CAAC,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CA6CjC,CAAC;AAEF;;;;;GAKG;AACH,MAAM,WAAW,WAAW;IAC3B,8FAA8F;IAC9F,SAAS,EAAE,CAAC,MAAM,EAAE,SAAS,CAAC,eAAe,CAAC,EAAE,OAAO,CAAC,EAAE,gBAAgB,KAAK,MAAM,IAAI,CAAC;IAC1F,kFAAkF;IAClF,GAAG,EAAE,MAAM,CAAC;IACZ,kGAAkG;IAClG,cAAc,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CAAC;IAC/C,yEAAyE;IACzE,QAAQ,EAAE,kBAAkB,CAAC,eAAe,CAAC,CAAC;CAC9C;AAED;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,EAAE,KAAK,CAAC,SAAS,CAOlD,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,2BAA2B,KAAK,CAAC;AAE9C;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,eAAO,MAAM,oBAAoB,GAAI,SAAS;IAC7C,mEAAmE;IACnE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9B,KAAG,WAgBH,CAAC"}
1
+ {"version":3,"file":"sse_auth_guard.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/realtime/sse_auth_guard.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,yBAAyB,CAAC;AAEpD,OAAO,EAGN,KAAK,aAAa,EAClB,MAAM,6BAA6B,CAAC;AACrC,OAAO,EAAC,kBAAkB,EAAE,KAAK,gBAAgB,EAAC,MAAM,0BAA0B,CAAC;AACnF,OAAO,KAAK,EAAC,SAAS,EAAE,eAAe,EAAE,SAAS,EAAC,MAAM,UAAU,CAAC;AAEpE,2DAA2D;AAC3D,eAAO,MAAM,iBAAiB,cAAc,CAAC;AAE7C;;;;;;;;;;GAUG;AACH,eAAO,MAAM,sBAAsB,EAAE,WAAW,CAAC,MAAM,CAKrD,CAAC;AAEH;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,qBAAqB,GAAI,CAAC,EACtC,UAAU,kBAAkB,CAAC,CAAC,CAAC,EAC/B,eAAe,MAAM,GAAG,IAAI,EAC5B,KAAK,MAAM,KACT,CAAC,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CA6CjC,CAAC;AAEF;;;;;GAKG;AACH,MAAM,WAAW,WAAW;IAC3B,8FAA8F;IAC9F,SAAS,EAAE,CAAC,MAAM,EAAE,SAAS,CAAC,eAAe,CAAC,EAAE,OAAO,CAAC,EAAE,gBAAgB,KAAK,MAAM,IAAI,CAAC;IAC1F,kFAAkF;IAClF,GAAG,EAAE,MAAM,CAAC;IACZ,yJAAyJ;IACzJ,cAAc,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CAAC;IAC/C,yEAAyE;IACzE,QAAQ,EAAE,kBAAkB,CAAC,eAAe,CAAC,CAAC;CAC9C;AAED;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,EAAE,KAAK,CAAC,SAAS,CAOlD,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,2BAA2B,KAAK,CAAC;AAE9C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,eAAO,MAAM,oBAAoB,GAAI,SAAS;IAC7C,mEAAmE;IACnE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9B,KAAG,WAgBH,CAAC"}
@@ -120,7 +120,15 @@ export const AUDIT_LOG_SSE_MAX_PER_SCOPE = 10;
120
120
  *
121
121
  * Combines `SubscriberRegistry`, `create_sse_auth_guard`, and the broadcast
122
122
  * call into a single object. The result satisfies `AuditLogRouteOptions['stream']`
123
- * and provides the `on_audit_event` callback for `CreateAppBackendOptions`.
123
+ * and provides the `on_audit_event` listener for the audit emitter's chain.
124
+ *
125
+ * Most consumers pass `audit_log_sse: true` to `create_app_server` and never
126
+ * touch this directly — the factory builds an `AuditLogSse`, appends
127
+ * `audit_sse.on_audit_event` to `backend.deps.audit.on_event_chain`, and
128
+ * exposes it via `AppServerContext.audit_sse`. Reach for the manual path
129
+ * (compose inside `audit_factory` body, or
130
+ * `audit.on_event_chain.push(audit_sse.on_audit_event)` post-assembly) only
131
+ * when wiring outside `create_app_server`.
124
132
  *
125
133
  * @param options - factory options
126
134
  * @returns audit log SSE setup (stream options + `on_audit_event` + registry)
@@ -129,8 +137,12 @@ export const AUDIT_LOG_SSE_MAX_PER_SCOPE = 10;
129
137
  * ```ts
130
138
  * const audit_sse = create_audit_log_sse({log});
131
139
  *
132
- * // In create_app_backend options:
133
- * on_audit_event: audit_sse.on_audit_event,
140
+ * // Inside the audit_factory body on CreateAppBackendOptions:
141
+ * audit_factory: ({db, log}) => create_audit_emitter({
142
+ * db,
143
+ * log,
144
+ * on_audit_event: audit_sse.on_audit_event,
145
+ * }),
134
146
  *
135
147
  * // In create_route_specs:
136
148
  * create_audit_log_route_specs({stream: audit_sse});
@@ -12,8 +12,8 @@
12
12
  */
13
13
  import { Logger } from '@fuzdev/fuz_util/log.js';
14
14
  import type { AppDeps } from '../auth/deps.js';
15
- import type { AuditLogConfig, AuditLogEvent } from '../auth/audit_log_schema.js';
16
- import type { DbType } from '../db/db.js';
15
+ import { type AuditEmitter } from '../auth/audit_emitter.js';
16
+ import type { DbType, Db } from '../db/db.js';
17
17
  import type { Keyring } from '../auth/keyring.js';
18
18
  import type { PasswordHashDeps } from '../auth/password.js';
19
19
  import type { StatResult } from '../runtime/deps.js';
@@ -33,6 +33,49 @@ export interface AppBackend {
33
33
  /** Close the database connection. Bound to the actual driver. */
34
34
  close: () => Promise<void>;
35
35
  }
36
+ /**
37
+ * Callback that builds the bound `AuditEmitter` after the backend's pool
38
+ * `Db` and `Logger` exist. Required on `CreateAppBackendOptions` so the
39
+ * consumer owns subscriber-chain composition and `AuditLogConfig`
40
+ * selection without the factory holding a default.
41
+ *
42
+ * The factory is invoked exactly once during `create_app_backend`, after
43
+ * `create_db` resolves and migrations run. The emitter it returns lands
44
+ * on `AppDeps.audit` and is captured by every query/handler that reaches
45
+ * `deps.audit.emit(...)`.
46
+ *
47
+ * The canonical body is a one-liner over `create_audit_emitter`:
48
+ *
49
+ * ```ts
50
+ * audit_factory: ({db, log}) => create_audit_emitter({
51
+ * db,
52
+ * log,
53
+ * on_audit_event,
54
+ * audit_log_config,
55
+ * })
56
+ * ```
57
+ *
58
+ * Returning an emitter built against a different `db` than the one passed
59
+ * in would route audit writes to a different pool than handlers query —
60
+ * the callback shape exists specifically to make that mistake structurally
61
+ * impossible.
62
+ */
63
+ export type AuditFactory = (params: {
64
+ db: Db;
65
+ log: Logger;
66
+ }) => AuditEmitter;
67
+ /**
68
+ * Trivial `AuditFactory` for consumers that don't compose `on_audit_event`
69
+ * or `audit_log_config`. Equivalent to
70
+ * `({db, log}) => create_audit_emitter({db, log})` — exported so the
71
+ * default case stays a single-symbol reference rather than five tokens
72
+ * of boilerplate at every consumer.
73
+ *
74
+ * Use the inline form when you need to thread `on_audit_event` /
75
+ * `audit_log_config` / `emit_decorator`; the factory composes those
76
+ * three fields itself so there's nothing this constant can pass through.
77
+ */
78
+ export declare const default_audit_factory: AuditFactory;
36
79
  /**
37
80
  * Input for `create_app_backend()`.
38
81
  *
@@ -55,21 +98,25 @@ export interface CreateAppBackendOptions {
55
98
  /** Structured logger instance. Omit for default (`new Logger('server')`). */
56
99
  log?: Logger;
57
100
  /**
58
- * Initial subscriber appended to `AppDeps.audit.on_event_chain`.
59
- * Use to broadcast audit events via SSE / WS. Additional subscribers
60
- * (e.g. the factory-managed audit-log SSE) are appended at server
61
- * assembly via `audit.on_event_chain.push(listener)` no shallow-copy
62
- * of `AppDeps` required.
63
- */
64
- on_audit_event?: (event: AuditLogEvent) => void;
65
- /**
66
- * Audit-log config for consumer event-type extensions. Built once at
67
- * startup via `create_audit_log_config({extra_events})` and captured
68
- * inside `AppDeps.audit` so consumer handlers cannot silently fall
69
- * back to the builtin config. Omit to use `builtin_audit_log_config`
70
- * (no extra events).
101
+ * Build the bound `AuditEmitter` once the backend's pool `Db` + `Logger`
102
+ * exist. Required the factory owns subscriber-chain composition and
103
+ * `AuditLogConfig` selection without `create_app_backend` holding a
104
+ * default. Typical body:
105
+ *
106
+ * ```ts
107
+ * audit_factory: ({db, log}) => create_audit_emitter({
108
+ * db,
109
+ * log,
110
+ * on_audit_event,
111
+ * audit_log_config,
112
+ * })
113
+ * ```
114
+ *
115
+ * Additional listeners (factory-managed audit SSE, per-endpoint WS
116
+ * auth guards) are appended at `create_app_server` time via
117
+ * `audit.on_event_chain.push(...)`.
71
118
  */
72
- audit_log_config?: AuditLogConfig;
119
+ audit_factory: AuditFactory;
73
120
  /**
74
121
  * Additional migration namespaces to run after the builtin auth namespace.
75
122
  * The shared `schema_version` table records one row per applied migration
@@ -87,10 +134,10 @@ export interface CreateAppBackendOptions {
87
134
  * Initialize the backend: database + auth migrations + deps.
88
135
  *
89
136
  * Calls `create_db` → `run_migrations` (auth namespace, then any
90
- * `migration_namespaces` from options in order) and bundles the result
91
- * with the provided keyring and password deps.
137
+ * `migration_namespaces` from options in order) `audit_factory({db, log})`
138
+ * and bundles the result with the provided keyring and password deps.
92
139
  *
93
- * @param options - keyring, password deps, optional database URL, and optional `migration_namespaces`
140
+ * @param options - keyring, password deps, `audit_factory`, optional database URL, and optional `migration_namespaces`
94
141
  * @returns app backend with deps, database metadata, and combined migration results
95
142
  * @throws Error if `migration_namespaces` contains a namespace in `reserved_migration_namespaces`
96
143
  */
@@ -1 +1 @@
1
- {"version":3,"file":"app_backend.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/server/app_backend.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAC,MAAM,EAAC,MAAM,yBAAyB,CAAC;AAE/C,OAAO,KAAK,EAAC,OAAO,EAAC,MAAM,iBAAiB,CAAC;AAC7C,OAAO,KAAK,EAAC,cAAc,EAAE,aAAa,EAAC,MAAM,6BAA6B,CAAC;AAE/E,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,aAAa,CAAC;AACxC,OAAO,KAAK,EAAC,OAAO,EAAC,MAAM,oBAAoB,CAAC;AAChD,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,qBAAqB,CAAC;AAC1D,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,oBAAoB,CAAC;AACnD,OAAO,EAAiB,KAAK,kBAAkB,EAAE,KAAK,eAAe,EAAC,MAAM,kBAAkB,CAAC;AAI/F;;;;;GAKG;AACH,MAAM,WAAW,UAAU;IAC1B,IAAI,EAAE,OAAO,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,oIAAoI;IACpI,QAAQ,CAAC,iBAAiB,EAAE,aAAa,CAAC,eAAe,CAAC,CAAC;IAC3D,iEAAiE;IACjE,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC3B;AAED;;;;;GAKG;AACH,MAAM,WAAW,uBAAuB;IACvC,+DAA+D;IAC/D,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAAC;IACnD,2BAA2B;IAC3B,cAAc,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IAClD,qBAAqB;IACrB,WAAW,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7C,0EAA0E;IAC1E,YAAY,EAAE,MAAM,CAAC;IACrB,wCAAwC;IACxC,OAAO,EAAE,OAAO,CAAC;IACjB,iFAAiF;IACjF,QAAQ,EAAE,gBAAgB,CAAC;IAC3B,6EAA6E;IAC7E,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;;;;;;OAMG;IACH,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CAAC;IAChD;;;;;;OAMG;IACH,gBAAgB,CAAC,EAAE,cAAc,CAAC;IAClC;;;;;;;;;;OAUG;IACH,oBAAoB,CAAC,EAAE,aAAa,CAAC,kBAAkB,CAAC,CAAC;CACzD;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,kBAAkB,GAAU,SAAS,uBAAuB,KAAG,OAAO,CAAC,UAAU,CAuC7F,CAAC"}
1
+ {"version":3,"file":"app_backend.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/server/app_backend.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAC,MAAM,EAAC,MAAM,yBAAyB,CAAC;AAE/C,OAAO,KAAK,EAAC,OAAO,EAAC,MAAM,iBAAiB,CAAC;AAC7C,OAAO,EAAuB,KAAK,YAAY,EAAC,MAAM,0BAA0B,CAAC;AACjF,OAAO,KAAK,EAAC,MAAM,EAAE,EAAE,EAAC,MAAM,aAAa,CAAC;AAC5C,OAAO,KAAK,EAAC,OAAO,EAAC,MAAM,oBAAoB,CAAC;AAChD,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,qBAAqB,CAAC;AAC1D,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,oBAAoB,CAAC;AACnD,OAAO,EAAiB,KAAK,kBAAkB,EAAE,KAAK,eAAe,EAAC,MAAM,kBAAkB,CAAC;AAI/F;;;;;GAKG;AACH,MAAM,WAAW,UAAU;IAC1B,IAAI,EAAE,OAAO,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,oIAAoI;IACpI,QAAQ,CAAC,iBAAiB,EAAE,aAAa,CAAC,eAAe,CAAC,CAAC;IAC3D,iEAAiE;IACjE,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC3B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,MAAM,MAAM,YAAY,GAAG,CAAC,MAAM,EAAE;IAAC,EAAE,EAAE,EAAE,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAC,KAAK,YAAY,CAAC;AAE3E;;;;;;;;;;GAUG;AACH,eAAO,MAAM,qBAAqB,EAAE,YAA6D,CAAC;AAElG;;;;;GAKG;AACH,MAAM,WAAW,uBAAuB;IACvC,+DAA+D;IAC/D,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAAC;IACnD,2BAA2B;IAC3B,cAAc,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IAClD,qBAAqB;IACrB,WAAW,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7C,0EAA0E;IAC1E,YAAY,EAAE,MAAM,CAAC;IACrB,wCAAwC;IACxC,OAAO,EAAE,OAAO,CAAC;IACjB,iFAAiF;IACjF,QAAQ,EAAE,gBAAgB,CAAC;IAC3B,6EAA6E;IAC7E,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;;;;;;;;;;;;;;;;;;OAkBG;IACH,aAAa,EAAE,YAAY,CAAC;IAC5B;;;;;;;;;;OAUG;IACH,oBAAoB,CAAC,EAAE,aAAa,CAAC,kBAAkB,CAAC,CAAC;CACzD;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,kBAAkB,GAAU,SAAS,uBAAuB,KAAG,OAAO,CAAC,UAAU,CAiD7F,CAAC"}
@@ -15,52 +15,75 @@ import { create_audit_emitter } from '../auth/audit_emitter.js';
15
15
  import { run_migrations } from '../db/migrate.js';
16
16
  import { auth_migration_ns, reserved_migration_namespaces } from '../auth/migrations.js';
17
17
  import { create_db } from '../db/create_db.js';
18
+ /**
19
+ * Trivial `AuditFactory` for consumers that don't compose `on_audit_event`
20
+ * or `audit_log_config`. Equivalent to
21
+ * `({db, log}) => create_audit_emitter({db, log})` — exported so the
22
+ * default case stays a single-symbol reference rather than five tokens
23
+ * of boilerplate at every consumer.
24
+ *
25
+ * Use the inline form when you need to thread `on_audit_event` /
26
+ * `audit_log_config` / `emit_decorator`; the factory composes those
27
+ * three fields itself so there's nothing this constant can pass through.
28
+ */
29
+ export const default_audit_factory = ({ db, log }) => create_audit_emitter({ db, log });
18
30
  /**
19
31
  * Initialize the backend: database + auth migrations + deps.
20
32
  *
21
33
  * Calls `create_db` → `run_migrations` (auth namespace, then any
22
- * `migration_namespaces` from options in order) and bundles the result
23
- * with the provided keyring and password deps.
34
+ * `migration_namespaces` from options in order) `audit_factory({db, log})`
35
+ * and bundles the result with the provided keyring and password deps.
24
36
  *
25
- * @param options - keyring, password deps, optional database URL, and optional `migration_namespaces`
37
+ * @param options - keyring, password deps, `audit_factory`, optional database URL, and optional `migration_namespaces`
26
38
  * @returns app backend with deps, database metadata, and combined migration results
27
39
  * @throws Error if `migration_namespaces` contains a namespace in `reserved_migration_namespaces`
28
40
  */
29
41
  export const create_app_backend = async (options) => {
30
- const { database_url, keyring, password, stat, read_text_file, delete_file } = options;
42
+ const { database_url, keyring, password, stat, read_text_file, delete_file, audit_factory } = options;
31
43
  const log = options.log ?? new Logger('server');
32
44
  const { db, close, db_type, db_name } = await create_db(database_url);
33
- if (options.migration_namespaces?.length) {
34
- for (const ns of options.migration_namespaces) {
35
- if (reserved_migration_namespaces.includes(ns.namespace)) {
36
- throw new Error(`Migration namespace "${ns.namespace}" is reserved by fuz_app choose a different namespace`);
45
+ // Everything after `create_db` can throw — reserved-namespace check,
46
+ // `run_migrations` (seven MigrationError kinds), `audit_factory`.
47
+ // Without this guard the pool leaks because `close` is only returned
48
+ // on the success path. Cleanup errors are logged and swallowed so the
49
+ // caller sees the original failure, not a teardown-shaped one.
50
+ try {
51
+ if (options.migration_namespaces?.length) {
52
+ for (const ns of options.migration_namespaces) {
53
+ if (reserved_migration_namespaces.includes(ns.namespace)) {
54
+ throw new Error(`Migration namespace "${ns.namespace}" is reserved by fuz_app — choose a different namespace`);
55
+ }
37
56
  }
38
57
  }
58
+ const migration_results = await run_migrations(db, [
59
+ auth_migration_ns,
60
+ ...(options.migration_namespaces ?? []),
61
+ ]);
62
+ const audit = audit_factory({ db, log });
63
+ return {
64
+ db_type,
65
+ db_name,
66
+ migration_results,
67
+ close,
68
+ deps: {
69
+ keyring,
70
+ password,
71
+ db,
72
+ stat,
73
+ read_text_file,
74
+ delete_file,
75
+ log,
76
+ audit,
77
+ },
78
+ };
79
+ }
80
+ catch (err) {
81
+ try {
82
+ await close();
83
+ }
84
+ catch (close_err) {
85
+ log.error('create_app_backend: failed to close db after init error:', close_err);
86
+ }
87
+ throw err;
39
88
  }
40
- const migration_results = await run_migrations(db, [
41
- auth_migration_ns,
42
- ...(options.migration_namespaces ?? []),
43
- ]);
44
- const audit = create_audit_emitter({
45
- db,
46
- log,
47
- on_audit_event: options.on_audit_event,
48
- audit_log_config: options.audit_log_config,
49
- });
50
- return {
51
- db_type,
52
- db_name,
53
- migration_results,
54
- close,
55
- deps: {
56
- keyring,
57
- password,
58
- db,
59
- stat,
60
- read_text_file,
61
- delete_file,
62
- log,
63
- audit,
64
- },
65
- };
66
89
  };