@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
@@ -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
  */
@@ -75,18 +78,21 @@ import { builtin_audit_log_config } from './audit_log_schema.js';
75
78
  * @returns the bound emitter; closes over the pool + config + listener chain
76
79
  */
77
80
  export const create_audit_emitter = (options) => {
78
- const { db, log, audit_log_config = builtin_audit_log_config, emit_decorator } = options;
81
+ const { db, log, audit_log_config = builtin_audit_log_config, emit_decorator, track_inflight = false } = options;
79
82
  // Closure-private listener list — no mutable array is exposed on the
80
83
  // returned (frozen) emitter; registration goes through `add_listener`.
84
+ // One entry object per registration, so a remover takes out its own
85
+ // registration even when the same function is registered twice.
81
86
  const listeners = [];
82
87
  if (options.on_audit_event)
83
- listeners.push(options.on_audit_event);
88
+ listeners.push({ listener: options.on_audit_event });
84
89
  const notify = (event) => {
85
90
  // Snapshot-at-notify: iterate a copy so a listener that registers
86
91
  // another listener mid-fan-out doesn't have the newcomer fire for the
87
- // in-flight event (it fires on the next `notify`). Converges with the
88
- // Rust twin, which clones the listener vec before iterating.
89
- for (const listener of [...listeners]) {
92
+ // in-flight event (it fires on the next `notify`), and one removed
93
+ // mid-fan-out still hears the in-flight event. Converges with the Rust
94
+ // twin, which clones the listener vec before iterating.
95
+ for (const { listener } of [...listeners]) {
90
96
  try {
91
97
  listener(event);
92
98
  }
@@ -111,16 +117,41 @@ export const create_audit_emitter = (options) => {
111
117
  if (event)
112
118
  notify(event);
113
119
  };
120
+ // In-flight `emit` writes, when tracked — each removed as it settles, so
121
+ // the set is empty exactly when nothing is outstanding. Neither tracked
122
+ // promise rejects (`write` and `notify` log their failures).
123
+ const inflight = track_inflight ? new Set() : null;
124
+ const track = (promise) => {
125
+ if (inflight) {
126
+ inflight.add(promise);
127
+ const remove = () => {
128
+ inflight.delete(promise);
129
+ };
130
+ void promise.then(remove, remove);
131
+ }
132
+ return promise;
133
+ };
134
+ const drain_inflight = async () => {
135
+ if (!inflight)
136
+ return;
137
+ // Wait for zero, not for a snapshot: a write started while waiting —
138
+ // an emit from a post-commit thunk — joins the set and is awaited on
139
+ // the next pass. The removal reactions were registered before these,
140
+ // so a settled write is gone from the set by the time a pass resumes.
141
+ while (inflight.size > 0) {
142
+ await Promise.allSettled(inflight);
143
+ }
144
+ };
114
145
  const base_emit = (ctx, input) => {
115
146
  if (input.outcome === 'failure') {
116
147
  // an attempt happened whether or not the transaction commits
117
- ctx.pending_effects.push(emit_pool(input));
148
+ ctx.pending_effects.push(track(emit_pool(input)));
118
149
  return;
119
150
  }
120
151
  // A success row is written now and announced after the commit — and
121
152
  // not at all on rollback, when the queue is discarded. See the module
122
153
  // doc, "When listeners hear about a row".
123
- const written = write(input);
154
+ const written = track(write(input));
124
155
  ctx.pending_effects.push(written.then(() => undefined));
125
156
  emit_after_commit(ctx, async () => {
126
157
  const event = await written;
@@ -146,26 +177,31 @@ export const create_audit_emitter = (options) => {
146
177
  });
147
178
  };
148
179
  const add_listener = (listener) => {
149
- listeners.push(listener);
180
+ const entry = { listener };
181
+ listeners.push(entry);
182
+ return () => {
183
+ const index = listeners.indexOf(entry);
184
+ if (index !== -1)
185
+ listeners.splice(index, 1);
186
+ };
150
187
  };
151
188
  const listener_count = () => listeners.length;
152
189
  // Freeze the slot layout so consumers cannot hot-patch `emit` /
153
- // `emit_role_grant_target` / `emit_pool` / `notify` after construction.
154
- // The previous test helper `patch_audit_emit_capture` did exactly this
155
- // and only happened to work because the four slots were writable —
190
+ // `emit_role_grant_target` / `emit_pool` / `notify` after construction —
191
+ // a patched `emit` would miss role-grant-shape emits anyway, since
156
192
  // `emit_role_grant_target` calls the closed-over inner `emit`, not
157
- // `this.emit`, so the patch silently bypassed role-grant-shape emits.
158
- // Tests that need instrumentation pass `emit_decorator` so the wrap
159
- // is captured by the closure before the freeze. The listener list stays
160
- // closure-private; registration is append-only via `add_listener`
161
- // (`create_app_server` registers its SSE + WS listeners post-assembly,
162
- // by design).
193
+ // `this.emit`. Tests that need instrumentation pass `emit_decorator` so the
194
+ // wrap is captured by the closure before the freeze. The listener list stays
195
+ // closure-private; it changes only through `add_listener` and the remover
196
+ // it returns (`create_app_server` registers its SSE + WS listeners after
197
+ // the emitter is built, by design, and removes them on close).
163
198
  return Object.freeze({
164
199
  emit,
165
200
  emit_role_grant_target,
166
201
  emit_pool,
167
202
  notify,
168
203
  add_listener,
169
- listener_count
204
+ listener_count,
205
+ drain_inflight
170
206
  });
171
207
  };
@@ -28,8 +28,9 @@ import type { QueryDeps } from '../db/query_deps.js';
28
28
  * (`invalid_token`, `account_not_found`) that could aid enumeration attacks,
29
29
  * and ensures public actions are not blocked by bad credentials.
30
30
  *
31
- * Rejects bearer tokens when an `Origin` or `Referer` header is present —
32
- * browsers must use cookie auth to reduce attack surface.
31
+ * Discards bearer tokens when an `Origin` or `Referer` header is present —
32
+ * `next()` with no identity, as for a bad token — so browsers must use cookie
33
+ * auth, reducing attack surface.
33
34
  * Auth scheme matching is case-insensitive per RFC 7235.
34
35
  * On success, sets `c.var.auth_account_id`, `CREDENTIAL_TYPE_KEY = 'api_token'`,
35
36
  * and `AUTH_API_TOKEN_ID_KEY`. Skips when an account is already authenticated
@@ -1 +1 @@
1
- {"version":3,"file":"bearer_auth.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/bearer_auth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAGH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAC;AAC9C,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AAUtD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAIrD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,eAAO,MAAM,6BAA6B,GAAI,MAAM,SAAS,EAAE,KAAK,MAAM,KAAG,iBAuE5E,CAAC"}
1
+ {"version":3,"file":"bearer_auth.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/bearer_auth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAGH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAC;AAC9C,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AAUtD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAIrD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,eAAO,MAAM,6BAA6B,GAAI,MAAM,SAAS,EAAE,KAAK,MAAM,KAAG,iBAuE5E,CAAC"}
@@ -31,8 +31,9 @@ import { is_browser_context } from '../http/origin.js';
31
31
  * (`invalid_token`, `account_not_found`) that could aid enumeration attacks,
32
32
  * and ensures public actions are not blocked by bad credentials.
33
33
  *
34
- * Rejects bearer tokens when an `Origin` or `Referer` header is present —
35
- * browsers must use cookie auth to reduce attack surface.
34
+ * Discards bearer tokens when an `Origin` or `Referer` header is present —
35
+ * `next()` with no identity, as for a bad token — so browsers must use cookie
36
+ * auth, reducing attack surface.
36
37
  * Auth scheme matching is case-insensitive per RFC 7235.
37
38
  * On success, sets `c.var.auth_account_id`, `CREDENTIAL_TYPE_KEY = 'api_token'`,
38
39
  * and `AUTH_API_TOKEN_ID_KEY`. Skips when an account is already authenticated
@@ -38,6 +38,36 @@ export declare const AUTH_MIDDLEWARE_PATH = "/api/*";
38
38
  * @throws Error when `path` is outside `AUTH_MIDDLEWARE_PATH`
39
39
  */
40
40
  export declare const assert_endpoint_in_auth_scope: (option: "rpc_endpoints" | "ws_endpoints", path: string) => void;
41
+ /**
42
+ * Throw unless a middleware list still mounts every spec of the stack it was
43
+ * built from — the trusted-proxy spec and the auth middleware — exactly once,
44
+ * at its original path, in its original order.
45
+ *
46
+ * `create_app_server` and `create_test_app_surface_spec` run it on the list
47
+ * a `transform_middleware` option returns, against the stack they handed it
48
+ * (`[proxy, ...auth]`). The endpoint auth-scope check
49
+ * (`assert_endpoint_in_auth_scope`) assumes the auth stack is mounted at
50
+ * `AUTH_MIDDLEWARE_PATH`, and every IP-keyed rate limiter assumes the proxy
51
+ * spec ran first — without it `get_client_ip` answers `'unknown'` for every
52
+ * caller, collapsing each limiter into one shared bucket. So a transform that
53
+ * moves, drops, duplicates, reorders, wraps, or replaces a stack layer is
54
+ * refused. Other specs may sit anywhere, including before the proxy spec and
55
+ * between the auth layers. The check pins where the stack's layers sit, not
56
+ * what added layers do — a layer you add runs with full context access.
57
+ *
58
+ * Each stack spec is matched by its `handler` — the closure its factory built
59
+ * — not by `name`, which any spec can carry, so a same-named replacement or a
60
+ * wrapped handler counts as missing. A spread copy (`{...spec}`) keeps the
61
+ * handler and still matches. Its path is compared with the original's, so
62
+ * pass specs the transform could not reach: the assemblies hand the transform
63
+ * shallow copies in a fresh array and keep the originals here.
64
+ *
65
+ * @param stack_specs - the stack the transform was built from, in mount order
66
+ * @param middleware_specs - the middleware list the transform returned
67
+ * @throws Error when a stack spec is missing, mounted more than once, mounted
68
+ * at another path, or out of order
69
+ */
70
+ export declare const assert_middleware_stack_mounted: (stack_specs: ReadonlyArray<MiddlewareSpec>, middleware_specs: ReadonlyArray<MiddlewareSpec>) => void;
41
71
  /**
42
72
  * Per-factory configuration for the standard auth middleware stack.
43
73
  */
@@ -1 +1 @@
1
- {"version":3,"file":"middleware.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/middleware.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAC1D,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAC1D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAIjE;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,oBAAoB,WAAW,CAAC;AAE7C;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,6BAA6B,GACzC,QAAQ,eAAe,GAAG,cAAc,EACxC,MAAM,MAAM,KACV,IAOF,CAAC;AAEF;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACrC,eAAe,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IAC/B,eAAe,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IACxC,iFAAiF;IACjF,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,mFAAmF;IACnF,kBAAkB,CAAC,EAAE,gBAAgB,CAAC;CACtC;AAED;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,4BAA4B,GACxC,MAAM,OAAO,EACb,SAAS,qBAAqB,KAC5B,OAAO,CAAC,KAAK,CAAC,cAAc,CAAC,CA4E/B,CAAC"}
1
+ {"version":3,"file":"middleware.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/middleware.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAC1D,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAC1D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAIjE;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,oBAAoB,WAAW,CAAC;AAE7C;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,6BAA6B,GACzC,QAAQ,eAAe,GAAG,cAAc,EACxC,MAAM,MAAM,KACV,IAOF,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,eAAO,MAAM,+BAA+B,GAC3C,aAAa,aAAa,CAAC,cAAc,CAAC,EAC1C,kBAAkB,aAAa,CAAC,cAAc,CAAC,KAC7C,IA+BF,CAAC;AAEF;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACrC,eAAe,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IAC/B,eAAe,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IACxC,iFAAiF;IACjF,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,mFAAmF;IACnF,kBAAkB,CAAC,EAAE,gBAAgB,CAAC;CACtC;AAED;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,4BAA4B,GACxC,MAAM,OAAO,EACb,SAAS,qBAAqB,KAC5B,OAAO,CAAC,KAAK,CAAC,cAAc,CAAC,CA4E/B,CAAC"}
@@ -41,6 +41,65 @@ export const assert_endpoint_in_auth_scope = (option, path) => {
41
41
  `(${AUTH_MIDDLEWARE_PATH}) — it would see every caller as unauthenticated — mount it under /api/`);
42
42
  }
43
43
  };
44
+ /**
45
+ * Throw unless a middleware list still mounts every spec of the stack it was
46
+ * built from — the trusted-proxy spec and the auth middleware — exactly once,
47
+ * at its original path, in its original order.
48
+ *
49
+ * `create_app_server` and `create_test_app_surface_spec` run it on the list
50
+ * a `transform_middleware` option returns, against the stack they handed it
51
+ * (`[proxy, ...auth]`). The endpoint auth-scope check
52
+ * (`assert_endpoint_in_auth_scope`) assumes the auth stack is mounted at
53
+ * `AUTH_MIDDLEWARE_PATH`, and every IP-keyed rate limiter assumes the proxy
54
+ * spec ran first — without it `get_client_ip` answers `'unknown'` for every
55
+ * caller, collapsing each limiter into one shared bucket. So a transform that
56
+ * moves, drops, duplicates, reorders, wraps, or replaces a stack layer is
57
+ * refused. Other specs may sit anywhere, including before the proxy spec and
58
+ * between the auth layers. The check pins where the stack's layers sit, not
59
+ * what added layers do — a layer you add runs with full context access.
60
+ *
61
+ * Each stack spec is matched by its `handler` — the closure its factory built
62
+ * — not by `name`, which any spec can carry, so a same-named replacement or a
63
+ * wrapped handler counts as missing. A spread copy (`{...spec}`) keeps the
64
+ * handler and still matches. Its path is compared with the original's, so
65
+ * pass specs the transform could not reach: the assemblies hand the transform
66
+ * shallow copies in a fresh array and keep the originals here.
67
+ *
68
+ * @param stack_specs - the stack the transform was built from, in mount order
69
+ * @param middleware_specs - the middleware list the transform returned
70
+ * @throws Error when a stack spec is missing, mounted more than once, mounted
71
+ * at another path, or out of order
72
+ */
73
+ export const assert_middleware_stack_mounted = (stack_specs, middleware_specs) => {
74
+ let previous_index = -1;
75
+ for (const stack_spec of stack_specs) {
76
+ let index = -1;
77
+ let count = 0;
78
+ for (let i = 0; i < middleware_specs.length; i++) {
79
+ if (middleware_specs[i].handler === stack_spec.handler) {
80
+ if (count === 0)
81
+ index = i;
82
+ count++;
83
+ }
84
+ }
85
+ const found = count === 0
86
+ ? 'missing'
87
+ : count > 1
88
+ ? `mounted ${count} times`
89
+ : middleware_specs[index].path !== stack_spec.path
90
+ ? `mounted at ${middleware_specs[index].path}`
91
+ : index < previous_index
92
+ ? 'out of order'
93
+ : null;
94
+ if (found) {
95
+ throw new Error(`transform_middleware must keep the middleware '${stack_spec.name}' mounted once ` +
96
+ `at ${stack_spec.path} in its original order (${found}) — endpoint auth-scope checks ` +
97
+ 'and client-IP rate limiting assume it; add layers around the stack, ' +
98
+ "don't move, drop, wrap, or replace it");
99
+ }
100
+ previous_index = index;
101
+ }
102
+ };
44
103
  /**
45
104
  * Create the auth middleware stack.
46
105
  *
@@ -251,7 +251,7 @@ export declare const create_request_context_middleware: (deps: QueryDeps, sessio
251
251
  * across route-spec declarations and direct calls alike, and the twin of the
252
252
  * Rust signature. It needs no parse: the surface arm's identifier is pure label,
253
253
  * since rule 3 never asks *which* surface. And it is deliberately not narrowed
254
- * to the spine's four — the name decides nothing, so there is nothing for a
254
+ * to the spine's own — the name decides nothing, so there is nothing for a
255
255
  * closed set to protect, while a consumer surface would have nothing to pass.
256
256
  * fuz_app's own uses are pinned two ways by the surface census: the exact call
257
257
  * site, and the scan asserting every `'surface:<name>'` literal in `src/lib`
@@ -281,7 +281,7 @@ const token_scope_denial = (c, capability) => {
281
281
  * across route-spec declarations and direct calls alike, and the twin of the
282
282
  * Rust signature. It needs no parse: the surface arm's identifier is pure label,
283
283
  * since rule 3 never asks *which* surface. And it is deliberately not narrowed
284
- * to the spine's four — the name decides nothing, so there is nothing for a
284
+ * to the spine's own — the name decides nothing, so there is nothing for a
285
285
  * closed set to protect, while a consumer surface would have nothing to pass.
286
286
  * fuz_app's own uses are pinned two ways by the surface census: the exact call
287
287
  * site, and the scan asserting every `'surface:<name>'` literal in `src/lib`
@@ -106,7 +106,9 @@ export declare const token_scope_admits_method: (scope: TokenScope, method: stri
106
106
  export declare const token_scope_admits_non_rpc: (scope: TokenScope) => boolean;
107
107
  /**
108
108
  * The non-RPC surfaces the **spine itself** mounts (rule 3). Twin of the Rust
109
- * `TokenSurface` enum, name for name.
109
+ * `TokenSurface` enum, name for name, except `app_surface` — the opt-in
110
+ * `GET /api/surface` explorer (`server/surface_route.ts`), which only the TS
111
+ * spine serves.
110
112
  *
111
113
  * Not a registry a consumer registers into. `RouteAuth.required_scope` accepts
112
114
  * any well-formed `surface:<name>` (see `parse_token_scope_capability`), so a
@@ -120,7 +122,7 @@ export declare const token_scope_admits_non_rpc: (scope: TokenScope) => boolean;
120
122
  * refused, in the `required_scope` a denial reports. That is the same field
121
123
  * whose `rpc:<method>` arm has always carried arbitrary consumer method names.
122
124
  */
123
- export declare const TOKEN_SURFACES: readonly ["db_admin", "fact_bare", "audit_stream", "ws_upgrade"];
125
+ export declare const TOKEN_SURFACES: readonly ["db_admin", "fact_bare", "audit_stream", "ws_upgrade", "app_surface"];
124
126
  export type TokenSurface = (typeof TOKEN_SURFACES)[number];
125
127
  /**
126
128
  * Is `value` one of the surfaces the spine mounts?
@@ -1 +1 @@
1
- {"version":3,"file":"token_scope.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/token_scope.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,wFAAwF;AACxF,eAAO,MAAM,mBAAmB,IAAI,CAAC;AAErC;;;GAGG;AACH,eAAO,MAAM,qBAAqB,QAAW,CAAC;AAE9C;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,UAAU;;;;;;;;;4BAYrB,CAAC;AACH,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,UAAU,CAAC,CAAC;AAEpD;;;;;;;;GAQG;AACH,eAAO,MAAM,eAAe;;;;;4BAG1B,CAAC;AACH,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,eAAe,CAAC,CAAC;AAE9D,kDAAkD;AAClD,eAAO,MAAM,gBAAgB,QAAO,UAGlC,CAAC;AAEH,uCAAuC;AACvC,eAAO,MAAM,mBAAmB,GAAI,SAAS,KAAK,CAAC,MAAM,CAAC,KAAG,UAI3D,CAAC;AAEH,qDAAqD;AACrD,eAAO,MAAM,sBAAsB,GAAI,OAAO,eAAe,KAAG,UACgB,CAAC;AAEjF;;;;GAIG;AACH,eAAO,MAAM,yBAAyB,GAAI,OAAO,UAAU,EAAE,QAAQ,MAAM,KAAG,OACtB,CAAC;AAEzD;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,0BAA0B,GAAI,OAAO,UAAU,KAAG,OAAgC,CAAC;AAEhG;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,cAAc,kEAAmE,CAAC;AAC/F,MAAM,MAAM,YAAY,GAAG,CAAC,OAAO,cAAc,CAAC,CAAC,MAAM,CAAC,CAAC;AAE3D;;;;;;;;GAQG;AACH,eAAO,MAAM,gBAAgB,GAAI,OAAO,MAAM,KAAG,KAAK,IAAI,YACA,CAAC;AAE3D;;;;;;;;GAQG;AACH,eAAO,MAAM,wBAAwB,QAAgC,CAAC;AAQtE;;;;;;GAMG;AACH,eAAO,MAAM,6BAA6B,GAAI,QAAQ,MAAM,KAAG,MAC3B,CAAC;AAErC;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,oBAAoB,GAC/B;IAAE,IAAI,EAAE,KAAK,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,CAAC;AAE/F;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,4BAA4B,GAAI,OAAO,MAAM,KAAG,oBAAoB,GAAG,IAUnF,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,6BAA6B,GACzC,OAAO,UAAU,EACjB,YAAY,oBAAoB,KAC9B,OAGkC,CAAC;AAEtC;;;;;;;;;GASG;AACH,eAAO,MAAM,uBAAuB,GACnC,YAAY,MAAM,KAChB;IAAE,KAAK,EAAE,sBAAsB,CAAC;IAAC,cAAc,EAAE,MAAM,CAAA;CAGxD,CAAC;AAEH,4CAA4C;AAC5C,eAAO,MAAM,iBAAiB,GAAI,OAAO,UAAU,KAAG,MAGrD,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,iBAAiB,GAAI,KAAK,MAAM,KAAG,UAAU,GAAG,IAU5D,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,GAAI,OAAO,UAAU,KAAG,MAQzD,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,wBAAwB,GAAI,OAAO,OAAO,KAAG,UAAU,GAAG,IAQtE,CAAC"}
1
+ {"version":3,"file":"token_scope.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/token_scope.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,wFAAwF;AACxF,eAAO,MAAM,mBAAmB,IAAI,CAAC;AAErC;;;GAGG;AACH,eAAO,MAAM,qBAAqB,QAAW,CAAC;AAE9C;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,UAAU;;;;;;;;;4BAYrB,CAAC;AACH,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,UAAU,CAAC,CAAC;AAEpD;;;;;;;;GAQG;AACH,eAAO,MAAM,eAAe;;;;;4BAG1B,CAAC;AACH,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,eAAe,CAAC,CAAC;AAE9D,kDAAkD;AAClD,eAAO,MAAM,gBAAgB,QAAO,UAGlC,CAAC;AAEH,uCAAuC;AACvC,eAAO,MAAM,mBAAmB,GAAI,SAAS,KAAK,CAAC,MAAM,CAAC,KAAG,UAI3D,CAAC;AAEH,qDAAqD;AACrD,eAAO,MAAM,sBAAsB,GAAI,OAAO,eAAe,KAAG,UACgB,CAAC;AAEjF;;;;GAIG;AACH,eAAO,MAAM,yBAAyB,GAAI,OAAO,UAAU,EAAE,QAAQ,MAAM,KAAG,OACtB,CAAC;AAEzD;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,0BAA0B,GAAI,OAAO,UAAU,KAAG,OAAgC,CAAC;AAEhG;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,cAAc,iFAMjB,CAAC;AACX,MAAM,MAAM,YAAY,GAAG,CAAC,OAAO,cAAc,CAAC,CAAC,MAAM,CAAC,CAAC;AAE3D;;;;;;;;GAQG;AACH,eAAO,MAAM,gBAAgB,GAAI,OAAO,MAAM,KAAG,KAAK,IAAI,YACA,CAAC;AAE3D;;;;;;;;GAQG;AACH,eAAO,MAAM,wBAAwB,QAAgC,CAAC;AAQtE;;;;;;GAMG;AACH,eAAO,MAAM,6BAA6B,GAAI,QAAQ,MAAM,KAAG,MAC3B,CAAC;AAErC;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,oBAAoB,GAC/B;IAAE,IAAI,EAAE,KAAK,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,CAAC;AAE/F;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,4BAA4B,GAAI,OAAO,MAAM,KAAG,oBAAoB,GAAG,IAUnF,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,6BAA6B,GACzC,OAAO,UAAU,EACjB,YAAY,oBAAoB,KAC9B,OAGkC,CAAC;AAEtC;;;;;;;;;GASG;AACH,eAAO,MAAM,uBAAuB,GACnC,YAAY,MAAM,KAChB;IAAE,KAAK,EAAE,sBAAsB,CAAC;IAAC,cAAc,EAAE,MAAM,CAAA;CAGxD,CAAC;AAEH,4CAA4C;AAC5C,eAAO,MAAM,iBAAiB,GAAI,OAAO,UAAU,KAAG,MAGrD,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,iBAAiB,GAAI,KAAK,MAAM,KAAG,UAAU,GAAG,IAU5D,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,GAAI,OAAO,UAAU,KAAG,MAQzD,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,wBAAwB,GAAI,OAAO,OAAO,KAAG,UAAU,GAAG,IAQtE,CAAC"}
@@ -112,7 +112,9 @@ export const token_scope_admits_method = (scope, method) => scope.kind === 'full
112
112
  export const token_scope_admits_non_rpc = (scope) => scope.kind === 'full';
113
113
  /**
114
114
  * The non-RPC surfaces the **spine itself** mounts (rule 3). Twin of the Rust
115
- * `TokenSurface` enum, name for name.
115
+ * `TokenSurface` enum, name for name, except `app_surface` — the opt-in
116
+ * `GET /api/surface` explorer (`server/surface_route.ts`), which only the TS
117
+ * spine serves.
116
118
  *
117
119
  * Not a registry a consumer registers into. `RouteAuth.required_scope` accepts
118
120
  * any well-formed `surface:<name>` (see `parse_token_scope_capability`), so a
@@ -126,7 +128,13 @@ export const token_scope_admits_non_rpc = (scope) => scope.kind === 'full';
126
128
  * refused, in the `required_scope` a denial reports. That is the same field
127
129
  * whose `rpc:<method>` arm has always carried arbitrary consumer method names.
128
130
  */
129
- export const TOKEN_SURFACES = ['db_admin', 'fact_bare', 'audit_stream', 'ws_upgrade'];
131
+ export const TOKEN_SURFACES = [
132
+ 'db_admin',
133
+ 'fact_bare',
134
+ 'audit_stream',
135
+ 'ws_upgrade',
136
+ 'app_surface'
137
+ ];
130
138
  /**
131
139
  * Is `value` one of the surfaces the spine mounts?
132
140
  *
@@ -28,7 +28,7 @@ effects, see ../../../docs/architecture.md.
28
28
  - `http/jsonrpc.ts` — JSON-RPC 2.0 envelope schemas (MCP superset), `JsonrpcErrorCode`, `_meta`.
29
29
  - `http/jsonrpc_errors.ts` — `ThrownJsonrpcError`, `jsonrpc_errors` throwers, HTTP-status mappings.
30
30
  - `http/jsonrpc_helpers.ts` — message builders, type guards, input/result normalizers.
31
- - `http/common_routes.ts` — health check + readiness probe (`/ready` schema-drift deploy gate) + authenticated server-status + surface route specs.
31
+ - `http/common_routes.ts` — health check + readiness probe (`/ready` schema-drift deploy gate) + authenticated server-status route specs (the surface explorer route lives in `server/surface_route.ts`).
32
32
  - `http/db_routes.ts` — allowlist-gated keeper-only table browser route specs (`public` schema, minus the `NON_BROWSABLE_TABLES` credential floor).
33
33
  - `http/pending_effects.ts` — `emit_after_commit` + `dispatch_with_post_commit_rollback` (the shared rollback-discard wrapper both dispatch sites use) + `flush_pending_effects` + `flush_post_commit_effects` + `EmitAfterCommitContext`.
34
34
 
@@ -484,13 +484,12 @@ at send time.
484
484
 
485
485
  ## Common Routes
486
486
 
487
- `http/common_routes.ts` exposes four generic route-spec factories with no
487
+ `http/common_routes.ts` exposes three generic route-spec factories with no
488
488
  auth-domain dependencies:
489
489
 
490
490
  - `create_health_route_spec()` — `GET /health`, public, returns `{status: 'ok'}`. Infrastructure endpoint for uptime monitors
491
- - `create_ready_route_spec({expected, log})` — `GET /ready`, public, the deploy gate. Introspects the live DB's columns (`db/schema_ready.ts`) and compares against the committed `expected` column map: `200 {ready: true}` on match, else `503 {error: 'schema_drift' | 'db_unreachable'}`. Detailed drift logs server-side only (minimal body — no schema leak, mirrors why `/api/surface` is authenticated). Throws at assembly on an empty `expected` (fail-loud). Opt-in like `/health`; pair with `load_expected_schema(url)` — loads + URL-caches a consumer's committed `expected_schema.json` and throws on an empty map
491
+ - `create_ready_route_spec({expected, log})` — `GET /ready`, public, the deploy gate. Introspects the live DB's columns (`db/schema_ready.ts`) and compares against the committed `expected` column map: `200 {ready: true}` on match, else `503 {error: 'schema_drift' | 'db_unreachable'}`. Detailed drift logs server-side only (minimal body — no schema leak, mirrors why `/api/surface` is admin-only). Throws at assembly on an empty `expected` (fail-loud). Opt-in like `/health`; pair with `load_expected_schema(url)` — loads + URL-caches a consumer's committed `expected_schema.json` and throws on an empty map
492
492
  - `create_server_status_route_spec({version, get_uptime_ms})` — `GET /api/server/status`, authenticated, returns `{version, uptime_ms}`
493
- - `create_surface_route_spec({surface})` — `GET /api/surface`, authenticated, serves the `AppSurface` JSON. Authenticated because surface data reveals API structure (schemas, auth, routes)
494
493
 
495
494
  Auth-aware variants (account status, bootstrap status) live in `auth/` —
496
495
  `http/common_routes.ts` stays generic.
@@ -141,8 +141,9 @@ export declare const RouteAuth: z.ZodObject<{
141
141
  *
142
142
  * - `surface:<name>` — a non-RPC surface, refused to every narrowed token
143
143
  * whatever its method list says (token scoping's rule that a narrowed
144
- * token is RPC-only). `surface:audit_stream` and `surface:fact_bare` are
145
- * the spine's; a consumer names its own without registering it.
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.
146
147
  * - `rpc:<method>` — one action method, refused unless the token lists it.
147
148
  * What a route bridged from an `ActionSpec` declares: it carries the
148
149
  * method's identity but never reaches the dispatcher's per-method gate.
@@ -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;;;;;;;;;;;;;;;;;;;;;;;;;OAyBG;;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;;;;;;;;;;;;;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"}
@@ -135,8 +135,9 @@ export const RouteAuth = z
135
135
  *
136
136
  * - `surface:<name>` — a non-RPC surface, refused to every narrowed token
137
137
  * whatever its method list says (token scoping's rule that a narrowed
138
- * token is RPC-only). `surface:audit_stream` and `surface:fact_bare` are
139
- * the spine's; a consumer names its own without registering it.
138
+ * token is RPC-only). `surface:audit_stream`, `surface:fact_bare`, and
139
+ * `surface:app_surface` are the spine's route-spec ones; a consumer names
140
+ * its own without registering it.
140
141
  * - `rpc:<method>` — one action method, refused unless the token lists it.
141
142
  * What a route bridged from an `ActionSpec` declares: it carries the
142
143
  * method's identity but never reaches the dispatcher's per-method gate.
@@ -8,7 +8,6 @@
8
8
  */
9
9
  import type { Logger } from '@fuzdev/fuz_util/log.ts';
10
10
  import type { RouteSpec } from './route_spec.js';
11
- import type { AppSurface } from './surface.js';
12
11
  import { type ExpectedSchema } from '../db/schema_ready.js';
13
12
  /**
14
13
  * Create a public health check route spec.
@@ -57,7 +56,7 @@ export interface ReadyRouteOptions {
57
56
  * else `503 {error}` (`schema_drift` when columns are missing, `db_unreachable`
58
57
  * when the introspection query throws). The detailed drift goes to the server
59
58
  * log only — the public body stays a minimal code so the endpoint doesn't leak
60
- * schema structure (mirrors why `/api/surface` is authenticated). A deploy poll
59
+ * schema structure (mirrors why `/api/surface` is admin-only). A deploy poll
61
60
  * treats `503` as a failed release and rolls back, turning a silent
62
61
  * schema-drift auth outage into a loud blocked deploy. See `db/schema_ready.ts`
63
62
  * for the column-presence rationale and `auth/migrations.ts` for the
@@ -78,16 +77,4 @@ export interface ServerStatusOptions {
78
77
  * this requires authentication.
79
78
  */
80
79
  export declare const create_server_status_route_spec: (options: ServerStatusOptions) => RouteSpec;
81
- /** Options for the surface explorer route. */
82
- export interface SurfaceRouteOptions {
83
- /** The generated app surface to serve. */
84
- surface: AppSurface;
85
- }
86
- /**
87
- * Create an authenticated route spec that serves the `AppSurface` as JSON.
88
- *
89
- * Surface data reveals API structure (routes, auth, schemas), so this
90
- * requires authentication like the server status route.
91
- */
92
- export declare const create_surface_route_spec: (options: SurfaceRouteOptions) => RouteSpec;
93
80
  //# sourceMappingURL=common_routes.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"common_routes.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/http/common_routes.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAKH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AAEtD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AACjD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC/C,OAAO,EAIN,KAAK,cAAc,EACnB,MAAM,uBAAuB,CAAC;AAE/B;;;;;GAKG;AACH,eAAO,MAAM,wBAAwB,QAAO,SAQ1C,CAAC;AAKH;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,oBAAoB,GAAI,KAAK,GAAG,GAAG,MAAM,KAAG,cAkBxD,CAAC;AAEF,6CAA6C;AAC7C,MAAM,WAAW,iBAAiB;IACjC;;;;OAIG;IACH,QAAQ,EAAE,cAAc,CAAC;IACzB,gFAAgF;IAChF,GAAG,CAAC,EAAE,MAAM,CAAC;CACb;AAED;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,uBAAuB,GAAI,SAAS,iBAAiB,KAAG,SAoCpE,CAAC;AAEF,yDAAyD;AACzD,MAAM,WAAW,mBAAmB;IACnC,kCAAkC;IAClC,OAAO,EAAE,MAAM,CAAC;IAChB,+CAA+C;IAC/C,aAAa,EAAE,MAAM,MAAM,CAAC;CAC5B;AAED;;;;;GAKG;AACH,eAAO,MAAM,+BAA+B,GAAI,SAAS,mBAAmB,KAAG,SAQ7E,CAAC;AAEH,8CAA8C;AAC9C,MAAM,WAAW,mBAAmB;IACnC,0CAA0C;IAC1C,OAAO,EAAE,UAAU,CAAC;CACpB;AAED;;;;;GAKG;AACH,eAAO,MAAM,yBAAyB,GAAI,SAAS,mBAAmB,KAAG,SAWvE,CAAC"}
1
+ {"version":3,"file":"common_routes.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/http/common_routes.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAKH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AAEtD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AACjD,OAAO,EAIN,KAAK,cAAc,EACnB,MAAM,uBAAuB,CAAC;AAE/B;;;;;GAKG;AACH,eAAO,MAAM,wBAAwB,QAAO,SAQ1C,CAAC;AAKH;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,oBAAoB,GAAI,KAAK,GAAG,GAAG,MAAM,KAAG,cAkBxD,CAAC;AAEF,6CAA6C;AAC7C,MAAM,WAAW,iBAAiB;IACjC;;;;OAIG;IACH,QAAQ,EAAE,cAAc,CAAC;IACzB,gFAAgF;IAChF,GAAG,CAAC,EAAE,MAAM,CAAC;CACb;AAED;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,uBAAuB,GAAI,SAAS,iBAAiB,KAAG,SAoCpE,CAAC;AAEF,yDAAyD;AACzD,MAAM,WAAW,mBAAmB;IACnC,kCAAkC;IAClC,OAAO,EAAE,MAAM,CAAC;IAChB,+CAA+C;IAC/C,aAAa,EAAE,MAAM,MAAM,CAAC;CAC5B;AAED;;;;;GAKG;AACH,eAAO,MAAM,+BAA+B,GAAI,SAAS,mBAAmB,KAAG,SAQ7E,CAAC"}
@@ -71,7 +71,7 @@ export const load_expected_schema = (url) => {
71
71
  * else `503 {error}` (`schema_drift` when columns are missing, `db_unreachable`
72
72
  * when the introspection query throws). The detailed drift goes to the server
73
73
  * log only — the public body stays a minimal code so the endpoint doesn't leak
74
- * schema structure (mirrors why `/api/surface` is authenticated). A deploy poll
74
+ * schema structure (mirrors why `/api/surface` is admin-only). A deploy poll
75
75
  * treats `503` as a failed release and rolls back, turning a silent
76
76
  * schema-drift auth outage into a loud blocked deploy. See `db/schema_ready.ts`
77
77
  * for the column-presence rationale and `auth/migrations.ts` for the
@@ -129,21 +129,3 @@ export const create_server_status_route_spec = (options) => ({
129
129
  input: z.null(),
130
130
  output: z.looseObject({ version: z.string(), uptime_ms: z.number() })
131
131
  });
132
- /**
133
- * Create an authenticated route spec that serves the `AppSurface` as JSON.
134
- *
135
- * Surface data reveals API structure (routes, auth, schemas), so this
136
- * requires authentication like the server status route.
137
- */
138
- export const create_surface_route_spec = (options) => ({
139
- method: 'GET',
140
- path: '/api/surface',
141
- auth: { account: 'required', actor: 'none' },
142
- handler: (c) => c.json(options.surface),
143
- description: 'Application surface (routes, middleware, schemas)',
144
- input: z.null(),
145
- output: z.looseObject({
146
- routes: z.array(z.looseObject({})),
147
- middleware: z.array(z.looseObject({}))
148
- })
149
- });
@@ -12,7 +12,7 @@
12
12
  * IPv6 forms (`::ffff:127.0.0.1` vs `0:0:0:0:0:ffff:7f00:1` vs the
13
13
  * bare `127.0.0.1`) — three keys for one address.
14
14
  *
15
- * Canonicalization runs through {@link canonicalize_ip} which:
15
+ * Canonicalization runs through `canonicalize_ip` which:
16
16
  *
17
17
  * 1. Lowercases and char-set filters (`IP_LITERAL_CHARS`) — non-IP
18
18
  * strings (`'unknown'`, `'attacker:controlled'`, `'::1\n'`) pass
@@ -12,7 +12,7 @@
12
12
  * IPv6 forms (`::ffff:127.0.0.1` vs `0:0:0:0:0:ffff:7f00:1` vs the
13
13
  * bare `127.0.0.1`) — three keys for one address.
14
14
  *
15
- * Canonicalization runs through {@link canonicalize_ip} which:
15
+ * Canonicalization runs through `canonicalize_ip` which:
16
16
  *
17
17
  * 1. Lowercases and char-set filters (`IP_LITERAL_CHARS`) — non-IP
18
18
  * strings (`'unknown'`, `'attacker:controlled'`, `'::1\n'`) pass
@@ -118,7 +118,9 @@ export declare const AUDIT_LOG_SSE_MAX_PER_SCOPE = 10;
118
118
  * connection closer.
119
119
  *
120
120
  * Combines `SubscriberRegistry`, `create_sse_auth_guard`, and the broadcast
121
- * call into a single object, and adds the registry to `connection_closer`.
121
+ * call into a single object, and adds the registry to `connection_closer` for
122
+ * good — the remover `add` returns is dropped, so a caller that must take the
123
+ * registry out again passes `null` and adds it itself.
122
124
  * The result satisfies `AuditLogRouteOptions['stream']`
123
125
  * and provides the `on_audit_event` listener for the audit emitter.
124
126
  *
@@ -164,7 +166,9 @@ export declare const create_audit_log_sse: (options: {
164
166
  * Required so it can't be forgotten: without it a revoked credential's
165
167
  * streams close only through the audit listener, which a failed audit write
166
168
  * or a cap eviction never reaches. Pass `null` only for a registry no
167
- * revocation has to reach (tests of the registry itself).
169
+ * revocation has to reach (tests of the registry itself), or when the
170
+ * caller adds `create_sse_connection_closer(registry)` itself — as
171
+ * `create_app_server` does, to hold the remover `add` returns.
168
172
  */
169
173
  connection_closer: RealtimeCloser | null;
170
174
  /**
@@ -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;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AAGtD,OAAO,EAIN,KAAK,aAAa,EAClB,MAAM,6BAA6B,CAAC;AACrC,OAAO,KAAK,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,iCAAiC,CAAC;AACxF,OAAO,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAC9D,OAAO,KAAK,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAE3D,2DAA2D;AAC3D,eAAO,MAAM,iBAAiB,cAAc,CAAC;AAE7C;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,4BAA4B,GAAI,CAAC,EAC7C,UAAU,kBAAkB,CAAC,CAAC,CAAC,KAC7B,gBAKD,CAAC;AAEH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;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,CA4DjC,CAAC;AAEF;;;;;GAKG;AACH,MAAM,WAAW,WAAW;IAC3B;;;;;OAKG;IACH,QAAQ,EAAE,kBAAkB,CAAC,eAAe,CAAC,CAAC;IAC9C,kFAAkF;IAClF,GAAG,EAAE,MAAM,CAAC;IACZ,yJAAyJ;IACzJ,cAAc,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CAAC;CAC/C;AAED;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,EAAE,KAAK,CAAC,SAAS,CAOlD,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,2BAA2B,KAAK,CAAC;AAE9C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AACH,eAAO,MAAM,oBAAoB,GAAI,SAAS;IAC7C,mEAAmE;IACnE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ;;;;;;;OAOG;IACH,iBAAiB,EAAE,cAAc,GAAG,IAAI,CAAC;IACzC;;;;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;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AAGtD,OAAO,EAIN,KAAK,aAAa,EAClB,MAAM,6BAA6B,CAAC;AACrC,OAAO,KAAK,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,iCAAiC,CAAC;AACxF,OAAO,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAC9D,OAAO,KAAK,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAE3D,2DAA2D;AAC3D,eAAO,MAAM,iBAAiB,cAAc,CAAC;AAE7C;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,4BAA4B,GAAI,CAAC,EAC7C,UAAU,kBAAkB,CAAC,CAAC,CAAC,KAC7B,gBAKD,CAAC;AAEH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;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,CA4DjC,CAAC;AAEF;;;;;GAKG;AACH,MAAM,WAAW,WAAW;IAC3B;;;;;OAKG;IACH,QAAQ,EAAE,kBAAkB,CAAC,eAAe,CAAC,CAAC;IAC9C,kFAAkF;IAClF,GAAG,EAAE,MAAM,CAAC;IACZ,yJAAyJ;IACzJ,cAAc,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CAAC;CAC/C;AAED;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,EAAE,KAAK,CAAC,SAAS,CAOlD,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,2BAA2B,KAAK,CAAC;AAE9C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AACH,eAAO,MAAM,oBAAoB,GAAI,SAAS;IAC7C,mEAAmE;IACnE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ;;;;;;;;;OASG;IACH,iBAAiB,EAAE,cAAc,GAAG,IAAI,CAAC;IACzC;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9B,KAAG,WAgBH,CAAC"}
@@ -164,7 +164,9 @@ export const AUDIT_LOG_SSE_MAX_PER_SCOPE = 10;
164
164
  * connection closer.
165
165
  *
166
166
  * Combines `SubscriberRegistry`, `create_sse_auth_guard`, and the broadcast
167
- * call into a single object, and adds the registry to `connection_closer`.
167
+ * call into a single object, and adds the registry to `connection_closer` for
168
+ * good — the remover `add` returns is dropped, so a caller that must take the
169
+ * registry out again passes `null` and adds it itself.
168
170
  * The result satisfies `AuditLogRouteOptions['stream']`
169
171
  * and provides the `on_audit_event` listener for the audit emitter.
170
172
  *