@fuzdev/fuz_app 0.127.0 → 0.129.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 (106) hide show
  1. package/dist/actions/CLAUDE.md +35 -18
  2. package/dist/actions/connection_closer.d.ts +16 -4
  3. package/dist/actions/connection_closer.d.ts.map +1 -1
  4. package/dist/actions/connection_closer.js +20 -4
  5. package/dist/actions/register_action_ws.d.ts +10 -3
  6. package/dist/actions/register_action_ws.d.ts.map +1 -1
  7. package/dist/actions/register_action_ws.js +6 -3
  8. package/dist/actions/register_ws_endpoint.d.ts +3 -1
  9. package/dist/actions/register_ws_endpoint.d.ts.map +1 -1
  10. package/dist/actions/register_ws_endpoint.js +3 -1
  11. package/dist/auth/CLAUDE.md +14 -10
  12. package/dist/auth/actor_lookup_action_specs.d.ts +2 -2
  13. package/dist/auth/actor_lookup_action_specs.js +2 -2
  14. package/dist/auth/audit_emitter.d.ts +73 -19
  15. package/dist/auth/audit_emitter.d.ts.map +1 -1
  16. package/dist/auth/audit_emitter.js +57 -21
  17. package/dist/auth/bearer_auth.d.ts +3 -2
  18. package/dist/auth/bearer_auth.d.ts.map +1 -1
  19. package/dist/auth/bearer_auth.js +3 -2
  20. package/dist/auth/middleware.d.ts +30 -0
  21. package/dist/auth/middleware.d.ts.map +1 -1
  22. package/dist/auth/middleware.js +59 -0
  23. package/dist/auth/request_context.d.ts +1 -1
  24. package/dist/auth/request_context.js +1 -1
  25. package/dist/auth/token_scope.d.ts +4 -2
  26. package/dist/auth/token_scope.d.ts.map +1 -1
  27. package/dist/auth/token_scope.js +10 -2
  28. package/dist/http/CLAUDE.md +3 -4
  29. package/dist/http/auth_shape.d.ts +3 -2
  30. package/dist/http/auth_shape.d.ts.map +1 -1
  31. package/dist/http/auth_shape.js +3 -2
  32. package/dist/http/common_routes.d.ts +1 -14
  33. package/dist/http/common_routes.d.ts.map +1 -1
  34. package/dist/http/common_routes.js +1 -19
  35. package/dist/http/ip_canonical.d.ts +1 -1
  36. package/dist/http/ip_canonical.js +1 -1
  37. package/dist/realtime/sse_auth_guard.d.ts +6 -2
  38. package/dist/realtime/sse_auth_guard.d.ts.map +1 -1
  39. package/dist/realtime/sse_auth_guard.js +3 -1
  40. package/dist/server/app_server.d.ts +157 -19
  41. package/dist/server/app_server.d.ts.map +1 -1
  42. package/dist/server/app_server.js +226 -128
  43. package/dist/server/surface_route.d.ts +32 -0
  44. package/dist/server/surface_route.d.ts.map +1 -0
  45. package/dist/server/surface_route.js +46 -0
  46. package/dist/testing/CLAUDE.md +50 -32
  47. package/dist/testing/app_server.d.ts +40 -5
  48. package/dist/testing/app_server.d.ts.map +1 -1
  49. package/dist/testing/app_server.js +84 -68
  50. package/dist/testing/attack_surface.d.ts +1 -1
  51. package/dist/testing/attack_surface.d.ts.map +1 -1
  52. package/dist/testing/audit_completeness.js +1 -1
  53. package/dist/testing/audit_drift_guard.d.ts +5 -3
  54. package/dist/testing/audit_drift_guard.d.ts.map +1 -1
  55. package/dist/testing/audit_drift_guard.js +14 -4
  56. package/dist/testing/cross_backend/account_lifecycle.d.ts.map +1 -1
  57. package/dist/testing/cross_backend/account_lifecycle.js +1 -2
  58. package/dist/testing/cross_backend/capabilities.d.ts +3 -1
  59. package/dist/testing/cross_backend/capabilities.d.ts.map +1 -1
  60. package/dist/testing/cross_backend/default_spine_surface.d.ts +2 -2
  61. package/dist/testing/cross_backend/default_spine_surface.d.ts.map +1 -1
  62. package/dist/testing/cross_backend/default_spine_surface.js +5 -2
  63. package/dist/testing/cross_backend/identity_parity.js +1 -1
  64. package/dist/testing/cross_backend/in_process_setup.d.ts.map +1 -1
  65. package/dist/testing/cross_backend/in_process_setup.js +4 -1
  66. package/dist/testing/cross_backend/method_coverage.d.ts +1 -1
  67. package/dist/testing/cross_backend/method_coverage.d.ts.map +1 -1
  68. package/dist/testing/cross_backend/method_coverage.js +2 -2
  69. package/dist/testing/cross_backend/peer_ping_ws.d.ts +1 -1
  70. package/dist/testing/cross_backend/peer_ping_ws.d.ts.map +1 -1
  71. package/dist/testing/cross_backend/role_grant_offer_notification_ws.d.ts +1 -1
  72. package/dist/testing/cross_backend/role_grant_offer_notification_ws.d.ts.map +1 -1
  73. package/dist/testing/cross_backend/setup.d.ts +4 -4
  74. package/dist/testing/cross_backend/setup.js +1 -1
  75. package/dist/testing/cross_backend/sse_round_trip.d.ts +1 -1
  76. package/dist/testing/cross_backend/sse_round_trip.d.ts.map +1 -1
  77. package/dist/testing/cross_backend/testing_reset_actions.d.ts +51 -32
  78. package/dist/testing/cross_backend/testing_reset_actions.d.ts.map +1 -1
  79. package/dist/testing/cross_backend/testing_reset_actions.js +70 -51
  80. package/dist/testing/cross_backend/testing_server_bun.d.ts +1 -1
  81. package/dist/testing/cross_backend/testing_server_bun.d.ts.map +1 -1
  82. package/dist/testing/cross_backend/testing_server_bun.js +80 -64
  83. package/dist/testing/cross_backend/testing_server_core.d.ts +10 -4
  84. package/dist/testing/cross_backend/testing_server_core.d.ts.map +1 -1
  85. package/dist/testing/cross_backend/testing_server_deno.d.ts +1 -1
  86. package/dist/testing/cross_backend/testing_server_deno.d.ts.map +1 -1
  87. package/dist/testing/cross_backend/testing_server_deno.js +1 -1
  88. package/dist/testing/cross_backend/testing_server_node.d.ts +1 -1
  89. package/dist/testing/cross_backend/testing_server_node.d.ts.map +1 -1
  90. package/dist/testing/cross_backend/testing_server_node.js +2 -2
  91. package/dist/testing/cross_backend/ws_round_trip.d.ts +3 -2
  92. package/dist/testing/cross_backend/ws_round_trip.d.ts.map +1 -1
  93. package/dist/testing/cross_backend/ws_round_trip.js +51 -13
  94. package/dist/testing/stubs.d.ts +65 -10
  95. package/dist/testing/stubs.d.ts.map +1 -1
  96. package/dist/testing/stubs.js +95 -41
  97. package/dist/testing/ws_round_trip.d.ts +1 -3
  98. package/dist/testing/ws_round_trip.d.ts.map +1 -1
  99. package/dist/ui/AdminSurface.svelte +3 -1
  100. package/dist/ui/AdminSurface.svelte.d.ts.map +1 -1
  101. package/dist/ui/CLAUDE.md +3 -1
  102. package/dist/ui/async_slot.svelte.d.ts +4 -4
  103. package/dist/ui/async_slot.svelte.js +4 -4
  104. package/dist/ui/keyed_async_slot.svelte.d.ts +4 -4
  105. package/dist/ui/keyed_async_slot.svelte.js +3 -3
  106. package/package.json +1 -1
@@ -565,11 +565,17 @@ fail-open. Failure outcomes (`revoked: false`, a 404) queue no close — they
565
565
  carry caller-supplied ids.
566
566
 
567
567
  **One closer for every transport.** `create_realtime_closer()` returns a
568
- `RealtimeCloser` — a `ConnectionCloser` plus `add(closer)` (idempotent by
569
- reference) — whose every close fans out to each member and returns the sum.
568
+ `RealtimeCloser` — a `ConnectionCloser` plus `add(closer)` and `member_count()`
569
+ — whose every close fans out to each member and returns the sum. Membership is
570
+ by reference: a closer added twice is one member, reached once. `add` returns a
571
+ remover for that addition (idempotent); additions are counted, so the closer
572
+ stays a member until every addition is removed — one owner releasing a shared
573
+ transport leaves it reachable for the others.
570
574
  `create_app_backend` puts one on `AppDeps.connection_closer`; `create_app_server`
571
- adds each mounted WS transport and the audit stream registry, and its
572
- `AppServer.close` runs `close_all_sockets` across them all. Twin of
575
+ adds each mounted WS transport and the audit stream registry itself (passing
576
+ `connection_closer: null` to the mount functions so it holds the removers), and
577
+ removes them when its assembly fails or `AppServer.close` runs — after
578
+ `close_all_sockets` across them all. Twin of
573
579
  `RealtimeRevoker`. A member that throws does not stop the fan-out — every
574
580
  member is called, then the first error is thrown for the caller to log;
575
581
  `BackendWebsocketTransport` and `SubscriberRegistry.close_by_identity` treat a
@@ -586,7 +592,8 @@ the connections of each expired session it deletes — `auth/CLAUDE.md`
586
592
  §Cleanup), and the
587
593
  `connection_closer: RealtimeCloser | null` option of `register_action_ws` /
588
594
  `register_ws_endpoint` / `create_audit_log_sse`, which add their transport to it
589
- (`null` is the explicit opt-out).
595
+ (`null` is the explicit opt-out, or for a caller that adds the transport itself
596
+ to hold the remover, as `create_app_server` does).
590
597
 
591
598
  ## WS auth guard (`actions/transports_ws_auth_guard.ts`)
592
599
 
@@ -676,10 +683,10 @@ enforced.
676
683
 
677
684
  Mounted transport reachable at `app_server.ws_endpoints[path]`
678
685
  (`Readonly<Record<string, BackendWebsocketTransport>>`). Duplicate paths
679
- across `WsEndpointSpec`s throw at mount time. Cross-surface collisions
680
- (same `GET <path>` on both `RouteSpec` and `WsEndpointSpec`) throw with
681
- exact-string match. Pattern overlap (e.g. `GET /api/:resource` vs
682
- `/api/ws`) is not detected — Hono's specific-before-wildcard routing keeps
686
+ across `WsEndpointSpec`s throw at assembly, before any endpoint mounts.
687
+ Cross-surface collisions (same `GET <path>` on both `RouteSpec` and
688
+ `WsEndpointSpec`) throw with exact-string match. Pattern overlap (e.g.
689
+ `GET /api/:resource` vs `/api/ws`) is not detected — Hono's specific-before-wildcard routing keeps
683
690
  those working but avoid the overlap.
684
691
 
685
692
  `auth_guard: true` does NOT close sockets on `role_grant_revoke`
@@ -746,8 +753,13 @@ revocation closes only after its commit — see
746
753
 
747
754
  `connection_closer: RealtimeCloser | null` is a required option: the endpoint's
748
755
  transport is added to it, so the revocation handlers' post-commit closes reach
749
- the sockets opened here. `null` is for a transport no revocation has to reach
750
- (a harness driving the dispatcher directly).
756
+ the sockets opened here. The add is the mount's last step, so a mount that
757
+ throws (a registry that fails to compile) adds nothing; the addition is
758
+ permanent, its remover not returned. `null` is for a transport no revocation
759
+ has to reach (a harness driving the dispatcher directly), or one the caller
760
+ adds itself to hold the remover — `create_app_server` passes `null` and adds
761
+ the transport after the mount returns, releasing it when assembly fails or
762
+ the server closes.
751
763
 
752
764
  Because `onOpen` awaits, an adapter can deliver `onMessage` before admission
753
765
  (`@hono/node-ws` doesn't await `onOpen` at all). Those frames are **queued, not
@@ -800,13 +812,18 @@ Per-message side-effect queues: `pending_effects` (eager) drains via
800
812
  `flush_pending_effects`; `post_commit_effects` (deferred — pushed by
801
813
  handlers via `emit_after_commit`) drains via `flush_post_commit_effects`.
802
814
  Both flush in the same `try/finally` that releases the request controller,
803
- so fire-and-forget audit / notification effects pushed by the handler
804
- complete (or reject visibly) before the next message dispatches. The
805
- deferred thunks are invoked first and the eager writes awaited after, so a
806
- revocation's close never waits on a slow audit INSERT — held behind one, the
807
- socket would stay open on a credential already committed gone and dispatch its
808
- next frame. The
809
- deferred queue is **discarded on rollback** before it reaches that flush (a
815
+ after the reply is sent, and every rejection is logged. Nothing orders that
816
+ flush against other frames: frames on an admitted socket are not serialized
817
+ (`handle_frame` calls aren't chained, and an adapter need not await
818
+ `onMessage` — `@hono/node-ws` doesn't), so a message's effects can still be
819
+ in flight when the caller reads its reply and when the next message
820
+ dispatches. A test that reads the audit log after a WS reply awaits
821
+ `_testing_drain_effects` first, over an emitter built with
822
+ `track_inflight: true` (`src/lib/testing/CLAUDE.md`). The deferred thunks are
823
+ invoked first and the eager writes awaited after, so a revocation's close never
824
+ waits on a slow audit INSERT — held behind one, the socket would stay open on a
825
+ credential already committed gone and dispatch its next frame. The deferred
826
+ queue is **discarded on rollback** before it reaches that flush (a
810
827
  rolled-back message fires no post-commit effect). See `http/CLAUDE.md`
811
828
  §Pending Effects.
812
829
 
@@ -139,20 +139,32 @@ export declare const noop_connection_closer: ConnectionCloser;
139
139
  */
140
140
  export interface RealtimeCloser extends ConnectionCloser {
141
141
  /**
142
- * Add a transport's closer. Idempotent by reference, so two endpoints
143
- * sharing one transport add it once.
142
+ * Add a transport's closer, returning a remover for this addition.
143
+ *
144
+ * Membership is by reference: a closer added twice — two endpoints
145
+ * sharing one transport — is one member, and every close reaches it
146
+ * once. Each `add` is counted, and its remover releases only that count:
147
+ * the closer stays a member until every addition is removed, so one
148
+ * owner releasing a shared transport leaves it reachable for the others.
149
+ * Calling a remover again is a no-op.
144
150
  *
145
151
  * Members must be the same instances connections are registered on — a
146
152
  * closer over a fresh registry closes nothing.
153
+ *
154
+ * @param closer - the transport's closer
155
+ * @returns a remover for this addition, idempotent
147
156
  */
148
- add: (closer: ConnectionCloser) => void;
157
+ add: (closer: ConnectionCloser) => () => void;
158
+ /** Count of distinct members — introspection for tests and diagnostics. */
159
+ member_count: () => number;
149
160
  }
150
161
  /**
151
162
  * Create an empty `RealtimeCloser`.
152
163
  *
153
164
  * `create_app_backend` creates the one on `AppDeps.connection_closer`, and
154
165
  * `create_app_server` adds each WebSocket transport it mounts and its audit
155
- * stream registry. A consumer that mounts a transport by hand adds it through
166
+ * stream registry, removing them when its assembly fails or the server
167
+ * closes. A consumer that mounts a transport by hand adds it through
156
168
  * the `connection_closer` option of `register_ws_endpoint` /
157
169
  * `create_audit_log_sse`, or with `add`.
158
170
  */
@@ -1 +1 @@
1
- {"version":3,"file":"connection_closer.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/connection_closer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AAEH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,wBAAwB,CAAC;AAGnD,OAAO,EAAqB,KAAK,sBAAsB,EAAE,MAAM,4BAA4B,CAAC;AAE5F;;;;;;;;;;;;;;GAcG;AACH,MAAM,WAAW,gBAAgB;IAChC,qGAAqG;IACrG,yBAAyB,EAAE,CAAC,kBAAkB,EAAE,MAAM,KAAK,MAAM,CAAC;IAClE,4EAA4E;IAC5E,uBAAuB,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,MAAM,CAAC;IAC1D;;;;OAIG;IACH,yBAAyB,EAAE,CAAC,UAAU,EAAE,IAAI,KAAK,MAAM,CAAC;IACxD;;;;;OAKG;IACH,iBAAiB,EAAE,MAAM,MAAM,CAAC;CAChC;AAED;;;;GAIG;AACH,MAAM,MAAM,qBAAqB,GAC9B;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,kBAAkB,EAAE,MAAM,CAAA;CAAE,GAC/C;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,GACvC;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,UAAU,EAAE,IAAI,CAAA;CAAE,CAAC;AAEzC;;;;;;;GAOG;AACH,eAAO,MAAM,iBAAiB,GAC7B,QAAQ,gBAAgB,EACxB,QAAQ,qBAAqB,KAC3B,MAWF,CAAC;AAEF;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,sBAAsB,GAClC,KAAK,sBAAsB,EAC3B,QAAQ,gBAAgB,EACxB,QAAQ,qBAAqB,KAC3B,IAIF,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,sBAAsB,EAAE,gBAKnC,CAAC;AAEH;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,cAAe,SAAQ,gBAAgB;IACvD;;;;;;OAMG;IACH,GAAG,EAAE,CAAC,MAAM,EAAE,gBAAgB,KAAK,IAAI,CAAC;CACxC;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,sBAAsB,QAAO,cAiCzC,CAAC"}
1
+ {"version":3,"file":"connection_closer.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/connection_closer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AAEH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,wBAAwB,CAAC;AAGnD,OAAO,EAAqB,KAAK,sBAAsB,EAAE,MAAM,4BAA4B,CAAC;AAE5F;;;;;;;;;;;;;;GAcG;AACH,MAAM,WAAW,gBAAgB;IAChC,qGAAqG;IACrG,yBAAyB,EAAE,CAAC,kBAAkB,EAAE,MAAM,KAAK,MAAM,CAAC;IAClE,4EAA4E;IAC5E,uBAAuB,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,MAAM,CAAC;IAC1D;;;;OAIG;IACH,yBAAyB,EAAE,CAAC,UAAU,EAAE,IAAI,KAAK,MAAM,CAAC;IACxD;;;;;OAKG;IACH,iBAAiB,EAAE,MAAM,MAAM,CAAC;CAChC;AAED;;;;GAIG;AACH,MAAM,MAAM,qBAAqB,GAC9B;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,kBAAkB,EAAE,MAAM,CAAA;CAAE,GAC/C;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,GACvC;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,UAAU,EAAE,IAAI,CAAA;CAAE,CAAC;AAEzC;;;;;;;GAOG;AACH,eAAO,MAAM,iBAAiB,GAC7B,QAAQ,gBAAgB,EACxB,QAAQ,qBAAqB,KAC3B,MAWF,CAAC;AAEF;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,sBAAsB,GAClC,KAAK,sBAAsB,EAC3B,QAAQ,gBAAgB,EACxB,QAAQ,qBAAqB,KAC3B,IAIF,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,sBAAsB,EAAE,gBAKnC,CAAC;AAEH;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,cAAe,SAAQ,gBAAgB;IACvD;;;;;;;;;;;;;;;OAeG;IACH,GAAG,EAAE,CAAC,MAAM,EAAE,gBAAgB,KAAK,MAAM,IAAI,CAAC;IAC9C,2EAA2E;IAC3E,YAAY,EAAE,MAAM,MAAM,CAAC;CAC3B;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,sBAAsB,QAAO,cA4CzC,CAAC"}
@@ -100,17 +100,19 @@ export const noop_connection_closer = Object.freeze({
100
100
  *
101
101
  * `create_app_backend` creates the one on `AppDeps.connection_closer`, and
102
102
  * `create_app_server` adds each WebSocket transport it mounts and its audit
103
- * stream registry. A consumer that mounts a transport by hand adds it through
103
+ * stream registry, removing them when its assembly fails or the server
104
+ * closes. A consumer that mounts a transport by hand adds it through
104
105
  * the `connection_closer` option of `register_ws_endpoint` /
105
106
  * `create_audit_log_sse`, or with `add`.
106
107
  */
107
108
  export const create_realtime_closer = () => {
108
- const members = new Set();
109
+ // member → how many additions hold it; iteration order is first addition
110
+ const members = new Map();
109
111
  const fan_out = (close) => {
110
112
  let count = 0;
111
113
  let failed = false;
112
114
  let first_error;
113
- for (const member of members) {
115
+ for (const member of members.keys()) {
114
116
  try {
115
117
  count += close(member);
116
118
  }
@@ -129,8 +131,22 @@ export const create_realtime_closer = () => {
129
131
  };
130
132
  return Object.freeze({
131
133
  add: (closer) => {
132
- members.add(closer);
134
+ members.set(closer, (members.get(closer) ?? 0) + 1);
135
+ let removed = false;
136
+ return () => {
137
+ if (removed)
138
+ return;
139
+ removed = true;
140
+ const count = members.get(closer);
141
+ if (count === undefined)
142
+ return;
143
+ if (count <= 1)
144
+ members.delete(closer);
145
+ else
146
+ members.set(closer, count - 1);
147
+ };
133
148
  },
149
+ member_count: () => members.size,
134
150
  close_sockets_for_session: (session_token_hash) => fan_out((member) => member.close_sockets_for_session(session_token_hash)),
135
151
  close_sockets_for_token: (api_token_id) => fan_out((member) => member.close_sockets_for_token(api_token_id)),
136
152
  close_sockets_for_account: (account_id) => fan_out((member) => member.close_sockets_for_account(account_id)),
@@ -95,7 +95,6 @@ import type { Db } from '../db/db.js';
95
95
  import { type Action } from './action_types.js';
96
96
  import type { RealtimeCloser } from './connection_closer.js';
97
97
  import { BackendWebsocketTransport, type ConnectionIdentity } from './transports_ws_backend.js';
98
- export type { Action };
99
98
  /** Default inactivity window before the server closes a silent socket. */
100
99
  export declare const DEFAULT_SERVER_HEARTBEAT_TIMEOUT = 60000;
101
100
  /**
@@ -244,6 +243,12 @@ export interface RegisterActionWsOptions {
244
243
  * reaches. Pass `null` only when no revocation has to reach this transport
245
244
  * (a harness driving the dispatcher directly), or when the transport was
246
245
  * added by hand.
246
+ *
247
+ * The transport is added once the mount succeeds, so a mount that throws
248
+ * adds nothing. The addition is permanent — its remover is not returned.
249
+ * A caller that must release the membership (as `create_app_server` does
250
+ * on close) passes `null` and adds the transport itself, holding the
251
+ * remover `RealtimeCloser.add` returns.
247
252
  */
248
253
  connection_closer: RealtimeCloser | null;
249
254
  /**
@@ -355,9 +360,11 @@ export interface RegisterActionWsResult {
355
360
  * @mutates options.app - registers a `GET path` route via `upgradeWebSocket`
356
361
  * @mutates options.transport - per socket, registers, admits, and removes a
357
362
  * connection via `register_pending` / `admit` / `remove_connection`
358
- * @mutates options.connection_closer - adds the endpoint's transport
363
+ * @mutates options.connection_closer - adds the endpoint's transport, once the
364
+ * mount has succeeded
359
365
  * @throws Error when `max_connections_per_account` is passed alongside
360
- * `transport`, or is neither `null` nor a positive integer
366
+ * `transport`, or is neither `null` nor a positive integer, or when
367
+ * `actions` fails to compile (`compile_action_registry`)
361
368
  */
362
369
  export declare const register_action_ws: (options: RegisterActionWsOptions) => RegisterActionWsResult;
363
370
  //# sourceMappingURL=register_action_ws.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"register_action_ws.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/register_action_ws.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"register_action_ws.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/register_action_ws.ts"],"names":[],"mappings":"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"}
@@ -208,9 +208,11 @@ const is_peer_response = (json) => {
208
208
  * @mutates options.app - registers a `GET path` route via `upgradeWebSocket`
209
209
  * @mutates options.transport - per socket, registers, admits, and removes a
210
210
  * connection via `register_pending` / `admit` / `remove_connection`
211
- * @mutates options.connection_closer - adds the endpoint's transport
211
+ * @mutates options.connection_closer - adds the endpoint's transport, once the
212
+ * mount has succeeded
212
213
  * @throws Error when `max_connections_per_account` is passed alongside
213
- * `transport`, or is neither `null` nor a positive integer
214
+ * `transport`, or is neither `null` nor a positive integer, or when
215
+ * `actions` fails to compile (`compile_action_registry`)
214
216
  */
215
217
  export const register_action_ws = (options) => {
216
218
  if (options.transport && options.max_connections_per_account !== undefined) {
@@ -224,7 +226,6 @@ export const register_action_ws = (options) => {
224
226
  max_connections_per_account: options.max_connections_per_account,
225
227
  log
226
228
  });
227
- options.connection_closer?.add(transport);
228
229
  // Build the dispatcher's per-method lookup. Only request_response
229
230
  // specs with a handler reach `action_map` — perform_action is the
230
231
  // only site that calls handlers, and it requires an `RpcAction`.
@@ -800,6 +801,8 @@ export const register_action_ws = (options) => {
800
801
  }
801
802
  };
802
803
  }));
804
+ // last, after everything that can throw, so a failed mount leaves no member
805
+ options.connection_closer?.add(transport);
803
806
  return { transport };
804
807
  };
805
808
  /**
@@ -62,7 +62,9 @@ export interface RegisterWsEndpointOptions extends RegisterActionWsOptions {
62
62
  * @mutates options.app - applies origin/auth/scope/authorization/role middleware
63
63
  * via `app.use`, then registers the `GET path` route via the inner
64
64
  * `register_action_ws`
65
- * @mutates options.connection_closer - adds the endpoint's transport
65
+ * @mutates options.connection_closer - adds the endpoint's transport, once the
66
+ * mount has succeeded, for good — pass `null` and add it yourself to hold the
67
+ * remover
66
68
  */
67
69
  export declare const register_ws_endpoint: (options: RegisterWsEndpointOptions) => RegisterActionWsResult;
68
70
  //# sourceMappingURL=register_ws_endpoint.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"register_ws_endpoint.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/register_ws_endpoint.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAaH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AAGvD,OAAO,EAEN,KAAK,uBAAuB,EAC5B,KAAK,sBAAsB,EAC3B,MAAM,yBAAyB,CAAC;AAEjC,0CAA0C;AAC1C,MAAM,WAAW,yBAA0B,SAAQ,uBAAuB;IACzE;;;;OAIG;IACH,eAAe,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IACvC;;;;;;;;OAQG;IACH,cAAc,CAAC,EAAE,aAAa,CAAC,QAAQ,CAAC,CAAC;CACzC;AAgDD;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,oBAAoB,GAChC,SAAS,yBAAyB,KAChC,sBAoBF,CAAC"}
1
+ {"version":3,"file":"register_ws_endpoint.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/register_ws_endpoint.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAaH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AAGvD,OAAO,EAEN,KAAK,uBAAuB,EAC5B,KAAK,sBAAsB,EAC3B,MAAM,yBAAyB,CAAC;AAEjC,0CAA0C;AAC1C,MAAM,WAAW,yBAA0B,SAAQ,uBAAuB;IACzE;;;;OAIG;IACH,eAAe,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IACvC;;;;;;;;OAQG;IACH,cAAc,CAAC,EAAE,aAAa,CAAC,QAAQ,CAAC,CAAC;CACzC;AAgDD;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,oBAAoB,GAChC,SAAS,yBAAyB,KAChC,sBAoBF,CAAC"}
@@ -86,7 +86,9 @@ const create_ws_authorization_middleware = (db) => {
86
86
  * @mutates options.app - applies origin/auth/scope/authorization/role middleware
87
87
  * via `app.use`, then registers the `GET path` route via the inner
88
88
  * `register_action_ws`
89
- * @mutates options.connection_closer - adds the endpoint's transport
89
+ * @mutates options.connection_closer - adds the endpoint's transport, once the
90
+ * mount has succeeded, for good — pass `null` and add it yourself to hold the
91
+ * remover
90
92
  */
91
93
  export const register_ws_endpoint = (options) => {
92
94
  const { app, path, allowed_origins, db, required_roles, log = new Logger('[ws]'), ...rest } = options;
@@ -121,20 +121,24 @@ upstream in `auth/signup_routes.ts` via `query_invite_find_unclaimed_match_for_u
121
121
  `auth/audit_emitter.ts` defines the `AuditEmitter` capability that lives on
122
122
  `AppDeps.audit`. Built once at backend assembly via the consumer's
123
123
  `audit_factory` callback over `create_audit_emitter`; closes over the pool +
124
- its registered listeners + optional `AuditLogConfig`. Six methods:
124
+ its registered listeners + optional `AuditLogConfig`. Its methods:
125
125
 
126
126
  - `emit(ctx, input)` — fire-and-forget pool write, pushes to `ctx.pending_effects`; a success row's listener fan-out goes on `ctx.post_commit_effects`
127
- - `emit_role_grant_target(ctx, auth, input)` — lifts `actor_id` / `account_id` / `ip` boilerplate for role-grant-shape events
127
+ - `emit_role_grant_target(ctx, auth, input)` — lifts `actor_id` / `account_id` / `ip` boilerplate for role-grant-shape events; `input` is an `AuditEmitRoleGrantInput`
128
128
  - `emit_pool(input)` — awaitable pool write for code paths without a request context (ad-hoc maintenance scripts; not for success audits paired with a mutation — those write in-tx and `notify` post-commit). Writes, then notifies, whatever the outcome
129
129
  - `notify(event)` — fan out an already-written row to listeners (used by in-tx audit batches like `query_accept_offer.audit_events`)
130
- - `add_listener(listener)` — append-only listener registration (twin of Rust `fuz_auth` `AuditEmitter::add_listener`)
130
+ - `add_listener(listener)` — listener registration, returning a remover for that registration (idempotent; the same function registered twice is two registrations, each with its own remover). Twin of Rust `fuz_auth` `AuditEmitter::add_listener` in firing order and snapshot-at-notify
131
131
  - `listener_count()` — registered-listener count, for tests / diagnostics
132
-
133
- Listeners are closure-private and append-only. `create_app_server` registers
134
- the audit-log SSE listener and per-endpoint WS auth guards via
135
- `add_listener` so SSE + WS fan-out compose on top of the consumer's
136
- `on_audit_event` callback without shallow-copying `AppDeps`. `notify` iterates a
137
- snapshot so a listener registered mid-fan-out fires only on the next event
132
+ - `drain_inflight()` — test-binary barrier: waits until no `emit` write is in flight, writes started while waiting included. Only an emitter built with `track_inflight: true` tracks; otherwise it resolves at once (production never tracks). Backs `_testing_drain_effects` (twin of Rust `AuditEmitter::drain_inflight` / `new_with_inflight_tracking`)
133
+
134
+ Listeners are closure-private; they change only through `add_listener` and
135
+ its removers. `create_app_server` registers the audit-log SSE listener,
136
+ per-endpoint WS auth guards, and any `extra_audit_handlers` via `add_listener`
137
+ so SSE + WS fan-out compose on top of the consumer's `on_audit_event` callback
138
+ without shallow-copying `AppDeps`, and removes them when its assembly fails or
139
+ `AppServer.close` runs — the emitter outlives any one server on its backend.
140
+ `notify` iterates a snapshot so a listener registered mid-fan-out fires only on
141
+ the next event, and one removed mid-fan-out still hears the in-flight event
138
142
  (converges with the Rust twin's cloned vec).
139
143
 
140
144
  **When listeners hear about a row.** `emit` always writes at once, on the pool
@@ -211,7 +215,7 @@ Everything else listed under §RPC action surfaces.
211
215
 
212
216
  ### Middleware
213
217
 
214
- - `auth/middleware.ts` — `create_auth_middleware_specs(deps, options)` assembles `[origin, session, request_context, bearer_auth]` + optional `daemon_token`.
218
+ - `auth/middleware.ts` — `create_auth_middleware_specs(deps, options)` assembles `[origin, session, request_context, bearer_auth]` + optional `daemon_token`. `AUTH_MIDDLEWARE_PATH` (`'/api/*'`) is where `create_app_server` mounts the stack; two assembly asserts lean on it: `assert_endpoint_in_auth_scope` (an `rpc_endpoints` / `ws_endpoints` path must sit inside it) and `assert_middleware_stack_mounted(stack_specs, middleware_specs)`, run on a `transform_middleware` result against the pre-transform stack (`[proxy, ...auth]`) — each stack spec, matched by its `handler` closure (not its spoofable `name`), mounted exactly once at its original path (`'*'` for the trusted proxy, `AUTH_MIDDLEWARE_PATH` for auth), in its original order; other specs may sit anywhere, including before the proxy and between the auth layers. The proxy is in the stack because dropping it turns every `get_client_ip` into `'unknown'`, one shared bucket for every IP-keyed limiter. The transform receives shallow copies in a fresh array, so an in-place mutation can't rewrite the originals the check reads. The check pins where those layers sit, not what added layers do — a layer you add runs with full context access. Both run in `create_test_app_surface_spec` too.
215
219
  - `auth/request_context.ts` — `RequestContext`, `resolve_acting_actor`, `build_request_context`, predicates (`has_role`, `has_scoped_role`, `has_any_scoped_role` — the dispatcher's role gate and every builtin-role check read the **global** grant via `has_scoped_role(_, role, null)` / `has_any_scoped_role(_, roles, null)`; scope-blind `has_role` is never the gate for a builtin role), guards (`require_auth`, `require_role`, `require_credential_types`, `require_token_scope`), `token_scope_surface_denial` (the direct-call form, for surfaces that aren't route specs), `refresh_role_grants`.
216
220
  - `auth/session_middleware.ts` — `process_session_cookie` integration, `create_session_and_set_cookie` (shared by login / signup / bootstrap; returns the token hashes of the sessions the cap evicted, which the caller closes after its commit — only login passes a cap).
217
221
  - `auth/resolved_auth.ts` — `ResolvedAuth` (the resolved credential as one value: `account_id`, `credential_type`, `token_hash`, `api_token_id`), `get_resolved_auth(c)` (gathers it from the context keys the auth middleware set), and `revalidate_resolved_auth(deps, resolved)`, the no-touch re-read a long-lived connection runs once it is registered: session row / token row (by id) still live and still on the account, account not soft-deleted; a daemon token re-reads its account only. Reuses the middleware's own queries and predicates. Called by the WS upgrade (`actions/register_action_ws.ts`) and the audit SSE route between registering a connection pending and admitting it. Twin of the Rust `revalidate_resolved_auth`.
@@ -12,7 +12,7 @@
12
12
  * Account-grain — only that the caller is signed in matters, not which
13
13
  * actor is calling, so resolution skips the actor phase. The auth gate
14
14
  * + per-account rate limit (default 1200/15min) + the
15
- * {@link ACTOR_LOOKUP_IDS_MAX | per-call cap} bound the batched
15
+ * per-call cap (`ACTOR_LOOKUP_IDS_MAX`) bound the batched
16
16
  * username-enumeration surface that the `cell_list` ↔ `actor_lookup`
17
17
  * pair would otherwise present.
18
18
  *
@@ -38,7 +38,7 @@
38
38
  * ids), bounded by:
39
39
  *
40
40
  * 1. rate-limit (per-account, see above),
41
- * 2. {@link ACTOR_LOOKUP_IDS_MAX} cap per call,
41
+ * 2. `ACTOR_LOOKUP_IDS_MAX` cap per call,
42
42
  * 3. actor-uuid intractability (122-bit random),
43
43
  * 4. hard-deleted actors are indistinguishable from never-existed (no
44
44
  * tombstone oracle — see `auth/actor_lookup_queries.ts`).
@@ -12,7 +12,7 @@
12
12
  * Account-grain — only that the caller is signed in matters, not which
13
13
  * actor is calling, so resolution skips the actor phase. The auth gate
14
14
  * + per-account rate limit (default 1200/15min) + the
15
- * {@link ACTOR_LOOKUP_IDS_MAX | per-call cap} bound the batched
15
+ * per-call cap (`ACTOR_LOOKUP_IDS_MAX`) bound the batched
16
16
  * username-enumeration surface that the `cell_list` ↔ `actor_lookup`
17
17
  * pair would otherwise present.
18
18
  *
@@ -38,7 +38,7 @@
38
38
  * ids), bounded by:
39
39
  *
40
40
  * 1. rate-limit (per-account, see above),
41
- * 2. {@link ACTOR_LOOKUP_IDS_MAX} cap per call,
41
+ * 2. `ACTOR_LOOKUP_IDS_MAX` cap per call,
42
42
  * 3. actor-uuid intractability (122-bit random),
43
43
  * 4. hard-deleted actors are indistinguishable from never-existed (no
44
44
  * tombstone oracle — see `auth/actor_lookup_queries.ts`).
@@ -29,6 +29,8 @@
29
29
  * the query layer, or the offer-expiry rows `auth/cleanup.ts` writes in
30
30
  * its sweep transaction). Runs every registered listener; per-listener
31
31
  * throws are isolated.
32
+ * - `drain_inflight()` — test-binary barrier: await every in-flight `emit`
33
+ * write, on an emitter built with `track_inflight: true`.
32
34
  *
33
35
  * ## When listeners hear about a row
34
36
  *
@@ -56,9 +58,10 @@
56
58
  * registers additional listeners via `add_listener` after the backend is
57
59
  * built (the factory-managed audit-log SSE, per-endpoint WS auth guards, any
58
60
  * `extra_audit_handlers` on a `WsEndpointSpec`) before the first request
59
- * runs. Consumers can also register listeners directly on the emitter they
60
- * return from `audit_factory` for setups that don't pass through
61
- * `create_app_server`.
61
+ * runs, and removes each through the remover `add_listener` returns when its
62
+ * assembly fails or the server closes. Consumers can also register
63
+ * listeners directly on the emitter they return from `audit_factory` for
64
+ * setups that don't pass through `create_app_server`.
62
65
  *
63
66
  * @module
64
67
  */
@@ -99,6 +102,19 @@ export interface AuditEmitRoleGrantContext extends AuditEmitterContext {
99
102
  /** Resolved client IP from the trusted-proxy middleware — `'unknown'` if not resolved. */
100
103
  client_ip: string;
101
104
  }
105
+ /**
106
+ * Input to `AuditEmitter.emit_role_grant_target` — the event fields that
107
+ * vary per call site; `actor_id`, `account_id`, and `ip` are lifted from
108
+ * the auth and request contexts.
109
+ */
110
+ export interface AuditEmitRoleGrantInput<T extends string> {
111
+ event_type: T;
112
+ target_account_id: Uuid | null;
113
+ target_actor_id: Uuid | null;
114
+ metadata: AuditLogInput<T>['metadata'];
115
+ /** Default `'success'`, as for `emit`. */
116
+ outcome?: 'success' | 'failure';
117
+ }
102
118
  /**
103
119
  * Bound audit-emit capability. Built once at backend assembly via
104
120
  * `create_audit_emitter`; lives on `AppDeps.audit` so factories never see
@@ -134,13 +150,7 @@ export interface AuditEmitter {
134
150
  * Reach for the lower-level `emit` only when the event is non-role-grant
135
151
  * shape (e.g. `app_settings_update`, bootstrap, signup).
136
152
  */
137
- emit_role_grant_target<T extends string>(ctx: AuditEmitRoleGrantContext, auth: RequestActorContext, input: {
138
- event_type: T;
139
- target_account_id: Uuid | null;
140
- target_actor_id: Uuid | null;
141
- metadata: AuditLogInput<T>['metadata'];
142
- outcome?: 'success' | 'failure';
143
- }): void;
153
+ emit_role_grant_target<T extends string>(ctx: AuditEmitRoleGrantContext, auth: RequestActorContext, input: AuditEmitRoleGrantInput<T>): void;
144
154
  /**
145
155
  * Awaitable pool write for code paths without a request context.
146
156
  *
@@ -168,22 +178,55 @@ export interface AuditEmitter {
168
178
  */
169
179
  notify(event: AuditLogEvent): void;
170
180
  /**
171
- * Register an audit-event listener. Append-only — listeners fire in
172
- * registration order for every row `emit` / `emit_pool` wrote and on
173
- * every `notify`.
181
+ * Register an audit-event listener. Listeners fire in registration order
182
+ * for every row `emit` / `emit_pool` wrote and on every `notify`.
183
+ *
184
+ * Returns a remover that unregisters this registration. Each call is its
185
+ * own registration: the same function registered twice fires twice, and
186
+ * each remover takes out only the registration it was returned for,
187
+ * leaving the others in place and in order. Calling a remover again is a
188
+ * no-op. A `notify` already in progress still runs a listener removed
189
+ * during its fan-out — it iterates a snapshot — and the next one does not.
174
190
  *
175
191
  * `create_app_server` registers the factory-managed audit-log SSE
176
192
  * listener and per-endpoint WS auth guards here so
177
193
  * SSE + WS fan-out compose on top of the consumer's `on_audit_event`
178
- * callback without shallow-copying `AppDeps`. Consumers can also
179
- * register listeners directly for setups that don't run through
180
- * `create_app_server`.
194
+ * callback without shallow-copying `AppDeps`, and removes them when its
195
+ * assembly fails or the server closes. Consumers can also register
196
+ * listeners directly for setups that don't run through
197
+ * `create_app_server`. The `on_audit_event` passed at construction has
198
+ * no remover — it lives as long as the emitter.
181
199
  *
182
- * Twin of the Rust `fuz_auth` `AuditEmitter::add_listener`.
200
+ * Twin of the Rust `fuz_auth` `AuditEmitter::add_listener` in what
201
+ * registration means — firing order, snapshot-at-notify.
202
+ *
203
+ * @param listener - called with each announced audit row
204
+ * @returns a remover for this registration, idempotent
205
+ */
206
+ add_listener(listener: (event: AuditLogEvent) => void): () => void;
207
+ /**
208
+ * Count of registered listeners, the construction-time `on_audit_event`
209
+ * included — introspection for tests and diagnostics.
183
210
  */
184
- add_listener(listener: (event: AuditLogEvent) => void): void;
185
- /** Count of registered listeners — introspection for tests and diagnostics. */
186
211
  listener_count(): number;
212
+ /**
213
+ * Await every fire-and-forget `emit` write in flight — the deterministic
214
+ * barrier behind the `_testing_drain_effects` test action.
215
+ *
216
+ * Waits until no tracked write is outstanding, so a write that starts
217
+ * while the drain is waiting (an emit from a post-commit thunk) is awaited
218
+ * too. Once it resolves, a following `audit_log_list` read sees every row
219
+ * whose `emit` ran before the drain was called. A write that starts after
220
+ * the drain resolves is not covered. A success row's listener fan-out runs
221
+ * as a post-commit effect and is not tracked, so an SSE broadcast or an
222
+ * auth-guard close driven by one isn't guaranteed done when the drain
223
+ * resolves — the row itself is.
224
+ *
225
+ * Resolves at once unless the emitter was built with
226
+ * `track_inflight: true` — production never tracks, so there is nothing
227
+ * to wait for. Twin of the Rust `fuz_auth` `AuditEmitter::drain_inflight`.
228
+ */
229
+ drain_inflight(): Promise<void>;
187
230
  }
188
231
  /**
189
232
  * Signature of `AuditEmitter.emit` — captured by the inner closure so
@@ -230,6 +273,17 @@ export interface CreateAuditEmitterOptions {
230
273
  * Leave unset in production — it is a test instrumentation seam.
231
274
  */
232
275
  emit_decorator?: EmitDecorator;
276
+ /**
277
+ * Track every in-flight `emit` write so `drain_inflight` can await it.
278
+ * Defaults to `false`, which makes `drain_inflight` resolve at once.
279
+ *
280
+ * Test binaries only. A transport can reply before a request's writes
281
+ * settle — a WebSocket message is answered before its effects flush — so
282
+ * a test reading the audit log after a reply needs this barrier. In
283
+ * production the tracking is pure overhead and nothing drains it. Twin of
284
+ * the Rust `fuz_auth` `AuditEmitter::new_with_inflight_tracking`.
285
+ */
286
+ track_inflight?: boolean;
233
287
  }
234
288
  /**
235
289
  * Build a bound `AuditEmitter`. Typical caller is the consumer's
@@ -1 +1 @@
1
- {"version":3,"file":"audit_emitter.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/audit_emitter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+DG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AACtD,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,wBAAwB,CAAC;AAEnD,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,aAAa,CAAC;AACtC,OAAO,EAAqB,KAAK,sBAAsB,EAAE,MAAM,4BAA4B,CAAC;AAC5F,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAEhE,OAAO,EAEN,KAAK,cAAc,EACnB,KAAK,aAAa,EAClB,KAAK,aAAa,EAClB,MAAM,uBAAuB,CAAC;AAE/B;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,WAAW,mBAAoB,SAAQ,sBAAsB;IAClE,eAAe,EAAE,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;CACtC;AAED;;;;GAIG;AACH,MAAM,WAAW,yBAA0B,SAAQ,mBAAmB;IACrE,0FAA0F;IAC1F,SAAS,EAAE,MAAM,CAAC;CAClB;AAED;;;;GAIG;AACH,MAAM,WAAW,YAAY;IAC5B;;;;;;;;;;;;;;;;;;;OAmBG;IACH,IAAI,CAAC,CAAC,SAAS,MAAM,EAAE,GAAG,EAAE,mBAAmB,EAAE,KAAK,EAAE,aAAa,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC;IAChF;;;;;;;OAOG;IACH,sBAAsB,CAAC,CAAC,SAAS,MAAM,EACtC,GAAG,EAAE,yBAAyB,EAC9B,IAAI,EAAE,mBAAmB,EACzB,KAAK,EAAE;QACN,UAAU,EAAE,CAAC,CAAC;QACd,iBAAiB,EAAE,IAAI,GAAG,IAAI,CAAC;QAC/B,eAAe,EAAE,IAAI,GAAG,IAAI,CAAC;QAC7B,QAAQ,EAAE,aAAa,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC;QACvC,OAAO,CAAC,EAAE,SAAS,GAAG,SAAS,CAAC;KAChC,GACC,IAAI,CAAC;IACR;;;;;;;;;;;;;;;OAeG;IACH,SAAS,CAAC,CAAC,SAAS,MAAM,EAAE,KAAK,EAAE,aAAa,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACpE;;;;;;;OAOG;IACH,MAAM,CAAC,KAAK,EAAE,aAAa,GAAG,IAAI,CAAC;IACnC;;;;;;;;;;;;;OAaG;IACH,YAAY,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,GAAG,IAAI,CAAC;IAC7D,+EAA+E;IAC/E,cAAc,IAAI,MAAM,CAAC;CACzB;AAED;;;;;GAKG;AACH,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,SAAS,MAAM,EAC1C,GAAG,EAAE,mBAAmB,EACxB,KAAK,EAAE,aAAa,CAAC,CAAC,CAAC,KACnB,IAAI,CAAC;AAEV;;;;;;;;;;GAUG;AACH,MAAM,MAAM,aAAa,GAAG,CAAC,KAAK,EAAE,WAAW,KAAK,WAAW,CAAC;AAEhE,0CAA0C;AAC1C,MAAM,WAAW,yBAAyB;IACzC,oDAAoD;IACpD,EAAE,EAAE,EAAE,CAAC;IACP,qDAAqD;IACrD,GAAG,EAAE,MAAM,CAAC;IACZ;;;OAGG;IACH,cAAc,CAAC,EAAE,CAAC,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CAAC,GAAG,IAAI,CAAC;IACzD;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,cAAc,CAAC;IAClC;;;;;;;OAOG;IACH,cAAc,CAAC,EAAE,aAAa,CAAC;CAC/B;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,oBAAoB,GAAI,SAAS,yBAAyB,KAAG,YA4GzE,CAAC"}
1
+ {"version":3,"file":"audit_emitter.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/audit_emitter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkEG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AACtD,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,wBAAwB,CAAC;AAEnD,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,aAAa,CAAC;AACtC,OAAO,EAAqB,KAAK,sBAAsB,EAAE,MAAM,4BAA4B,CAAC;AAC5F,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAEhE,OAAO,EAEN,KAAK,cAAc,EACnB,KAAK,aAAa,EAClB,KAAK,aAAa,EAClB,MAAM,uBAAuB,CAAC;AAE/B;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,WAAW,mBAAoB,SAAQ,sBAAsB;IAClE,eAAe,EAAE,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;CACtC;AAED;;;;GAIG;AACH,MAAM,WAAW,yBAA0B,SAAQ,mBAAmB;IACrE,0FAA0F;IAC1F,SAAS,EAAE,MAAM,CAAC;CAClB;AAED;;;;GAIG;AACH,MAAM,WAAW,uBAAuB,CAAC,CAAC,SAAS,MAAM;IACxD,UAAU,EAAE,CAAC,CAAC;IACd,iBAAiB,EAAE,IAAI,GAAG,IAAI,CAAC;IAC/B,eAAe,EAAE,IAAI,GAAG,IAAI,CAAC;IAC7B,QAAQ,EAAE,aAAa,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC;IACvC,0CAA0C;IAC1C,OAAO,CAAC,EAAE,SAAS,GAAG,SAAS,CAAC;CAChC;AAED;;;;GAIG;AACH,MAAM,WAAW,YAAY;IAC5B;;;;;;;;;;;;;;;;;;;OAmBG;IACH,IAAI,CAAC,CAAC,SAAS,MAAM,EAAE,GAAG,EAAE,mBAAmB,EAAE,KAAK,EAAE,aAAa,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC;IAChF;;;;;;;OAOG;IACH,sBAAsB,CAAC,CAAC,SAAS,MAAM,EACtC,GAAG,EAAE,yBAAyB,EAC9B,IAAI,EAAE,mBAAmB,EACzB,KAAK,EAAE,uBAAuB,CAAC,CAAC,CAAC,GAC/B,IAAI,CAAC;IACR;;;;;;;;;;;;;;;OAeG;IACH,SAAS,CAAC,CAAC,SAAS,MAAM,EAAE,KAAK,EAAE,aAAa,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACpE;;;;;;;OAOG;IACH,MAAM,CAAC,KAAK,EAAE,aAAa,GAAG,IAAI,CAAC;IACnC;;;;;;;;;;;;;;;;;;;;;;;;;OAyBG;IACH,YAAY,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;IACnE;;;OAGG;IACH,cAAc,IAAI,MAAM,CAAC;IACzB;;;;;;;;;;;;;;;;OAgBG;IACH,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAChC;AAED;;;;;GAKG;AACH,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,SAAS,MAAM,EAC1C,GAAG,EAAE,mBAAmB,EACxB,KAAK,EAAE,aAAa,CAAC,CAAC,CAAC,KACnB,IAAI,CAAC;AAEV;;;;;;;;;;GAUG;AACH,MAAM,MAAM,aAAa,GAAG,CAAC,KAAK,EAAE,WAAW,KAAK,WAAW,CAAC;AAEhE,0CAA0C;AAC1C,MAAM,WAAW,yBAAyB;IACzC,oDAAoD;IACpD,EAAE,EAAE,EAAE,CAAC;IACP,qDAAqD;IACrD,GAAG,EAAE,MAAM,CAAC;IACZ;;;OAGG;IACH,cAAc,CAAC,EAAE,CAAC,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CAAC,GAAG,IAAI,CAAC;IACzD;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,cAAc,CAAC;IAClC;;;;;;;OAOG;IACH,cAAc,CAAC,EAAE,aAAa,CAAC;IAC/B;;;;;;;;;OASG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;CACzB;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,oBAAoB,GAAI,SAAS,yBAAyB,KAAG,YA6IzE,CAAC"}