@fuzdev/fuz_app 0.140.0 → 0.142.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 (311) hide show
  1. package/dist/actions/CLAUDE.md +72 -22
  2. package/dist/actions/action_rpc.d.ts +5 -1
  3. package/dist/actions/action_rpc.d.ts.map +1 -1
  4. package/dist/actions/action_rpc.js +9 -1
  5. package/dist/actions/connection_closer.d.ts +44 -4
  6. package/dist/actions/connection_closer.d.ts.map +1 -1
  7. package/dist/actions/connection_closer.js +33 -3
  8. package/dist/actions/perform_action.d.ts +10 -0
  9. package/dist/actions/perform_action.d.ts.map +1 -1
  10. package/dist/actions/perform_action.js +23 -9
  11. package/dist/actions/register_action_ws.d.ts +42 -6
  12. package/dist/actions/register_action_ws.d.ts.map +1 -1
  13. package/dist/actions/register_action_ws.js +87 -19
  14. package/dist/actions/register_ws_endpoint.d.ts +15 -6
  15. package/dist/actions/register_ws_endpoint.d.ts.map +1 -1
  16. package/dist/actions/register_ws_endpoint.js +10 -4
  17. package/dist/actions/socket.svelte.d.ts +19 -4
  18. package/dist/actions/socket.svelte.d.ts.map +1 -1
  19. package/dist/actions/socket.svelte.js +31 -4
  20. package/dist/actions/transports.d.ts +30 -1
  21. package/dist/actions/transports.d.ts.map +1 -1
  22. package/dist/actions/transports.js +30 -1
  23. package/dist/actions/transports_ws_auth_guard.d.ts +13 -6
  24. package/dist/actions/transports_ws_auth_guard.d.ts.map +1 -1
  25. package/dist/actions/transports_ws_auth_guard.js +24 -10
  26. package/dist/actions/transports_ws_backend.d.ts +51 -5
  27. package/dist/actions/transports_ws_backend.d.ts.map +1 -1
  28. package/dist/actions/transports_ws_backend.js +62 -9
  29. package/dist/actions/ws_endpoint_spec.d.ts +11 -10
  30. package/dist/actions/ws_endpoint_spec.d.ts.map +1 -1
  31. package/dist/auth/CLAUDE.md +44 -22
  32. package/dist/auth/account_actions.d.ts.map +1 -1
  33. package/dist/auth/account_actions.js +12 -1
  34. package/dist/auth/account_route_schema.d.ts +1 -0
  35. package/dist/auth/account_route_schema.d.ts.map +1 -1
  36. package/dist/auth/account_route_schema.js +12 -0
  37. package/dist/auth/account_routes.d.ts +7 -2
  38. package/dist/auth/account_routes.d.ts.map +1 -1
  39. package/dist/auth/account_routes.js +240 -187
  40. package/dist/auth/admin_action_specs.d.ts +6 -1
  41. package/dist/auth/admin_action_specs.d.ts.map +1 -1
  42. package/dist/auth/admin_action_specs.js +6 -1
  43. package/dist/auth/admin_actions.d.ts.map +1 -1
  44. package/dist/auth/admin_actions.js +14 -2
  45. package/dist/auth/api_token.d.ts +20 -0
  46. package/dist/auth/api_token.d.ts.map +1 -1
  47. package/dist/auth/api_token.js +21 -1
  48. package/dist/auth/api_token_queries.d.ts +22 -4
  49. package/dist/auth/api_token_queries.d.ts.map +1 -1
  50. package/dist/auth/api_token_queries.js +11 -6
  51. package/dist/auth/audit_log_routes.d.ts +10 -0
  52. package/dist/auth/audit_log_routes.d.ts.map +1 -1
  53. package/dist/auth/audit_log_routes.js +20 -0
  54. package/dist/auth/audit_log_schema.d.ts +30 -5
  55. package/dist/auth/audit_log_schema.d.ts.map +1 -1
  56. package/dist/auth/audit_log_schema.js +30 -0
  57. package/dist/auth/bearer_auth.d.ts +4 -2
  58. package/dist/auth/bearer_auth.d.ts.map +1 -1
  59. package/dist/auth/bearer_auth.js +19 -8
  60. package/dist/auth/bootstrap_route_schema.d.ts +1 -0
  61. package/dist/auth/bootstrap_route_schema.d.ts.map +1 -1
  62. package/dist/auth/bootstrap_route_schema.js +1 -0
  63. package/dist/auth/bootstrap_routes.d.ts.map +1 -1
  64. package/dist/auth/bootstrap_routes.js +85 -76
  65. package/dist/auth/cell_action_specs.d.ts +7 -3
  66. package/dist/auth/cell_action_specs.d.ts.map +1 -1
  67. package/dist/auth/cell_action_specs.js +9 -5
  68. package/dist/auth/cell_actions.d.ts +3 -3
  69. package/dist/auth/cell_actions.d.ts.map +1 -1
  70. package/dist/auth/cell_actions.js +67 -38
  71. package/dist/auth/cell_authorize.d.ts +69 -8
  72. package/dist/auth/cell_authorize.d.ts.map +1 -1
  73. package/dist/auth/cell_authorize.js +89 -9
  74. package/dist/auth/cell_field_action_specs.d.ts +5 -4
  75. package/dist/auth/cell_field_action_specs.d.ts.map +1 -1
  76. package/dist/auth/cell_field_action_specs.js +7 -7
  77. package/dist/auth/cell_field_actions.d.ts +7 -5
  78. package/dist/auth/cell_field_actions.d.ts.map +1 -1
  79. package/dist/auth/cell_field_actions.js +24 -22
  80. package/dist/auth/cell_grant_action_specs.d.ts +6 -0
  81. package/dist/auth/cell_grant_action_specs.d.ts.map +1 -1
  82. package/dist/auth/cell_grant_action_specs.js +6 -0
  83. package/dist/auth/cell_grant_actions.d.ts +3 -1
  84. package/dist/auth/cell_grant_actions.d.ts.map +1 -1
  85. package/dist/auth/cell_grant_actions.js +16 -2
  86. package/dist/auth/cell_item_action_specs.d.ts +5 -4
  87. package/dist/auth/cell_item_action_specs.d.ts.map +1 -1
  88. package/dist/auth/cell_item_action_specs.js +7 -7
  89. package/dist/auth/cell_item_actions.d.ts +6 -5
  90. package/dist/auth/cell_item_actions.d.ts.map +1 -1
  91. package/dist/auth/cell_item_actions.js +23 -22
  92. package/dist/auth/cell_relation_list_limit.d.ts +20 -0
  93. package/dist/auth/cell_relation_list_limit.d.ts.map +1 -0
  94. package/dist/auth/cell_relation_list_limit.js +27 -0
  95. package/dist/auth/cell_relation_visibility.d.ts +4 -3
  96. package/dist/auth/cell_relation_visibility.d.ts.map +1 -1
  97. package/dist/auth/cell_relation_visibility.js +6 -5
  98. package/dist/auth/cell_steward.d.ts +45 -0
  99. package/dist/auth/cell_steward.d.ts.map +1 -0
  100. package/dist/auth/cell_steward.js +53 -0
  101. package/dist/auth/cleanup.d.ts +9 -9
  102. package/dist/auth/cleanup.js +9 -9
  103. package/dist/auth/credential_deadline.d.ts +95 -0
  104. package/dist/auth/credential_deadline.d.ts.map +1 -0
  105. package/dist/auth/credential_deadline.js +139 -0
  106. package/dist/auth/daemon_token_middleware.d.ts.map +1 -1
  107. package/dist/auth/daemon_token_middleware.js +4 -1
  108. package/dist/auth/middleware.d.ts +90 -12
  109. package/dist/auth/middleware.d.ts.map +1 -1
  110. package/dist/auth/middleware.js +115 -16
  111. package/dist/auth/request_context.d.ts +8 -2
  112. package/dist/auth/request_context.d.ts.map +1 -1
  113. package/dist/auth/request_context.js +20 -4
  114. package/dist/auth/resolved_auth.d.ts +13 -2
  115. package/dist/auth/resolved_auth.d.ts.map +1 -1
  116. package/dist/auth/resolved_auth.js +5 -4
  117. package/dist/auth/role_grant_offer_action_specs.d.ts +5 -0
  118. package/dist/auth/role_grant_offer_action_specs.d.ts.map +1 -1
  119. package/dist/auth/role_grant_offer_action_specs.js +6 -1
  120. package/dist/auth/role_grant_offer_actions.d.ts.map +1 -1
  121. package/dist/auth/role_grant_offer_actions.js +23 -16
  122. package/dist/auth/role_grant_offer_queries.d.ts +8 -6
  123. package/dist/auth/role_grant_offer_queries.d.ts.map +1 -1
  124. package/dist/auth/role_grant_offer_queries.js +16 -8
  125. package/dist/auth/session_cookie.d.ts +4 -0
  126. package/dist/auth/session_cookie.d.ts.map +1 -1
  127. package/dist/auth/session_cookie.js +3 -2
  128. package/dist/auth/session_middleware.d.ts +1 -1
  129. package/dist/auth/session_middleware.d.ts.map +1 -1
  130. package/dist/auth/session_middleware.js +4 -1
  131. package/dist/auth/session_queries.d.ts +19 -2
  132. package/dist/auth/session_queries.d.ts.map +1 -1
  133. package/dist/auth/session_queries.js +8 -2
  134. package/dist/auth/signup_route_schema.d.ts.map +1 -1
  135. package/dist/auth/signup_route_schema.js +1 -0
  136. package/dist/auth/signup_routes.d.ts +4 -3
  137. package/dist/auth/signup_routes.d.ts.map +1 -1
  138. package/dist/auth/signup_routes.js +157 -134
  139. package/dist/auth/token_scope.d.ts +7 -1
  140. package/dist/auth/token_scope.d.ts.map +1 -1
  141. package/dist/auth/token_scope.js +8 -2
  142. package/dist/db/CLAUDE.md +32 -19
  143. package/dist/db/cell_ddl.d.ts +1 -2
  144. package/dist/db/cell_ddl.d.ts.map +1 -1
  145. package/dist/db/cell_ddl.js +1 -2
  146. package/dist/db/cell_field_queries.d.ts +20 -18
  147. package/dist/db/cell_field_queries.d.ts.map +1 -1
  148. package/dist/db/cell_field_queries.js +28 -22
  149. package/dist/db/cell_item_queries.d.ts +20 -14
  150. package/dist/db/cell_item_queries.d.ts.map +1 -1
  151. package/dist/db/cell_item_queries.js +28 -18
  152. package/dist/db/cell_moderation.d.ts +67 -0
  153. package/dist/db/cell_moderation.d.ts.map +1 -0
  154. package/dist/db/cell_moderation.js +66 -0
  155. package/dist/db/cell_queries.d.ts +44 -14
  156. package/dist/db/cell_queries.d.ts.map +1 -1
  157. package/dist/db/cell_queries.js +68 -45
  158. package/dist/db/cell_view_sql.d.ts +102 -0
  159. package/dist/db/cell_view_sql.d.ts.map +1 -0
  160. package/dist/db/cell_view_sql.js +112 -0
  161. package/dist/db/db_pg.d.ts +1 -1
  162. package/dist/db/db_pg.js +1 -1
  163. package/dist/hono_context.d.ts +23 -0
  164. package/dist/hono_context.d.ts.map +1 -1
  165. package/dist/hono_context.js +19 -0
  166. package/dist/http/CLAUDE.md +41 -15
  167. package/dist/http/auth_shape.d.ts +5 -2
  168. package/dist/http/auth_shape.d.ts.map +1 -1
  169. package/dist/http/auth_shape.js +5 -2
  170. package/dist/http/ip_canonical.d.ts +30 -3
  171. package/dist/http/ip_canonical.d.ts.map +1 -1
  172. package/dist/http/ip_canonical.js +50 -3
  173. package/dist/http/middleware_spec.d.ts +16 -0
  174. package/dist/http/middleware_spec.d.ts.map +1 -1
  175. package/dist/http/origin.d.ts +113 -18
  176. package/dist/http/origin.d.ts.map +1 -1
  177. package/dist/http/origin.js +247 -127
  178. package/dist/http/route_spec.d.ts +21 -0
  179. package/dist/http/route_spec.d.ts.map +1 -1
  180. package/dist/http/schema_helpers.d.ts +22 -0
  181. package/dist/http/schema_helpers.d.ts.map +1 -1
  182. package/dist/http/schema_helpers.js +24 -0
  183. package/dist/http/surface.d.ts +6 -1
  184. package/dist/http/surface.d.ts.map +1 -1
  185. package/dist/http/surface.js +12 -5
  186. package/dist/primitive_schemas.d.ts +10 -1
  187. package/dist/primitive_schemas.d.ts.map +1 -1
  188. package/dist/primitive_schemas.js +11 -2
  189. package/dist/rate_limiter.d.ts +179 -12
  190. package/dist/rate_limiter.d.ts.map +1 -1
  191. package/dist/rate_limiter.js +352 -42
  192. package/dist/realtime/sse_auth_guard.d.ts +22 -6
  193. package/dist/realtime/sse_auth_guard.d.ts.map +1 -1
  194. package/dist/realtime/sse_auth_guard.js +31 -12
  195. package/dist/realtime/subscriber_registry.d.ts +10 -1
  196. package/dist/realtime/subscriber_registry.d.ts.map +1 -1
  197. package/dist/realtime/subscriber_registry.js +10 -1
  198. package/dist/server/app_server.d.ts +20 -7
  199. package/dist/server/app_server.d.ts.map +1 -1
  200. package/dist/server/app_server.js +8 -3
  201. package/dist/server/env.d.ts +2 -1
  202. package/dist/server/env.d.ts.map +1 -1
  203. package/dist/server/env.js +7 -17
  204. package/dist/server/serve_fact_route.d.ts +8 -4
  205. package/dist/server/serve_fact_route.d.ts.map +1 -1
  206. package/dist/server/serve_fact_route.js +16 -6
  207. package/dist/testing/CLAUDE.md +164 -25
  208. package/dist/testing/adversarial_headers.d.ts.map +1 -1
  209. package/dist/testing/adversarial_headers.js +13 -6
  210. package/dist/testing/connection_closer_helpers.d.ts +6 -1
  211. package/dist/testing/connection_closer_helpers.d.ts.map +1 -1
  212. package/dist/testing/connection_closer_helpers.js +4 -0
  213. package/dist/testing/cross_backend/account_lifecycle.d.ts.map +1 -1
  214. package/dist/testing/cross_backend/account_lifecycle.js +37 -2
  215. package/dist/testing/cross_backend/body_size.d.ts.map +1 -1
  216. package/dist/testing/cross_backend/body_size.js +14 -65
  217. package/dist/testing/cross_backend/capabilities.d.ts +26 -2
  218. package/dist/testing/cross_backend/capabilities.d.ts.map +1 -1
  219. package/dist/testing/cross_backend/capabilities.js +9 -3
  220. package/dist/testing/cross_backend/cell_gated_create.d.ts +38 -0
  221. package/dist/testing/cross_backend/cell_gated_create.d.ts.map +1 -1
  222. package/dist/testing/cross_backend/cell_gated_create.js +401 -5
  223. package/dist/testing/cross_backend/cell_relations.d.ts.map +1 -1
  224. package/dist/testing/cross_backend/cell_relations.js +145 -2
  225. package/dist/testing/cross_backend/conformance_case.d.ts +11 -3
  226. package/dist/testing/cross_backend/conformance_case.d.ts.map +1 -1
  227. package/dist/testing/cross_backend/conformance_case.js +6 -0
  228. package/dist/testing/cross_backend/conformance_table.d.ts.map +1 -1
  229. package/dist/testing/cross_backend/conformance_table.js +20 -19
  230. package/dist/testing/cross_backend/credential_deadline.d.ts +25 -0
  231. package/dist/testing/cross_backend/credential_deadline.d.ts.map +1 -0
  232. package/dist/testing/cross_backend/credential_deadline.js +185 -0
  233. package/dist/testing/cross_backend/default_backend_configs.d.ts.map +1 -1
  234. package/dist/testing/cross_backend/default_backend_configs.js +13 -2
  235. package/dist/testing/cross_backend/expected_schema.json +0 -1
  236. package/dist/testing/cross_backend/full_spine_mount.d.ts +3 -0
  237. package/dist/testing/cross_backend/full_spine_mount.d.ts.map +1 -1
  238. package/dist/testing/cross_backend/full_spine_mount.js +7 -1
  239. package/dist/testing/cross_backend/login_security.d.ts +16 -0
  240. package/dist/testing/cross_backend/login_security.d.ts.map +1 -1
  241. package/dist/testing/cross_backend/login_security.js +154 -0
  242. package/dist/testing/cross_backend/narrowed_api_token.d.ts +23 -0
  243. package/dist/testing/cross_backend/narrowed_api_token.d.ts.map +1 -0
  244. package/dist/testing/cross_backend/narrowed_api_token.js +40 -0
  245. package/dist/testing/cross_backend/origin.d.ts.map +1 -1
  246. package/dist/testing/cross_backend/origin.js +6 -26
  247. package/dist/testing/cross_backend/origin_conformance.d.ts +152 -0
  248. package/dist/testing/cross_backend/origin_conformance.d.ts.map +1 -0
  249. package/dist/testing/cross_backend/origin_conformance.js +136 -0
  250. package/dist/testing/cross_backend/origin_conformance.json +1679 -0
  251. package/dist/testing/cross_backend/origin_scope.d.ts +16 -0
  252. package/dist/testing/cross_backend/origin_scope.d.ts.map +1 -0
  253. package/dist/testing/cross_backend/origin_scope.js +140 -0
  254. package/dist/testing/cross_backend/query_shape.d.ts.map +1 -1
  255. package/dist/testing/cross_backend/query_shape.js +24 -2
  256. package/dist/testing/cross_backend/role_grant_cross_helpers.d.ts +35 -0
  257. package/dist/testing/cross_backend/role_grant_cross_helpers.d.ts.map +1 -0
  258. package/dist/testing/cross_backend/role_grant_cross_helpers.js +64 -0
  259. package/dist/testing/cross_backend/role_grant_offer_notification_ws.d.ts +8 -0
  260. package/dist/testing/cross_backend/role_grant_offer_notification_ws.d.ts.map +1 -1
  261. package/dist/testing/cross_backend/role_grant_offer_notification_ws.js +44 -33
  262. package/dist/testing/cross_backend/role_grant_participation.d.ts.map +1 -1
  263. package/dist/testing/cross_backend/role_grant_participation.js +64 -0
  264. package/dist/testing/cross_backend/rust_spine_stub_backend_config.d.ts.map +1 -1
  265. package/dist/testing/cross_backend/rust_spine_stub_backend_config.js +6 -4
  266. package/dist/testing/cross_backend/spine_surface_constants.d.ts +20 -1
  267. package/dist/testing/cross_backend/spine_surface_constants.d.ts.map +1 -1
  268. package/dist/testing/cross_backend/spine_surface_constants.js +23 -1
  269. package/dist/testing/cross_backend/testing_backdoor.d.ts.map +1 -1
  270. package/dist/testing/cross_backend/testing_backdoor.js +20 -7
  271. package/dist/testing/cross_backend/testing_reset_actions.d.ts +180 -5
  272. package/dist/testing/cross_backend/testing_reset_actions.d.ts.map +1 -1
  273. package/dist/testing/cross_backend/testing_reset_actions.js +154 -5
  274. package/dist/testing/cross_backend/token_scope_surface.d.ts.map +1 -1
  275. package/dist/testing/cross_backend/token_scope_surface.js +25 -21
  276. package/dist/testing/cross_backend/transport_composition.d.ts +60 -0
  277. package/dist/testing/cross_backend/transport_composition.d.ts.map +1 -0
  278. package/dist/testing/cross_backend/transport_composition.js +364 -0
  279. package/dist/testing/cross_backend/ts_spine_backend_config.d.ts.map +1 -1
  280. package/dist/testing/cross_backend/ts_spine_backend_config.js +21 -6
  281. package/dist/testing/cross_backend/ws_action_rate_limit.d.ts.map +1 -1
  282. package/dist/testing/cross_backend/ws_action_rate_limit.js +21 -1
  283. package/dist/testing/cross_backend/ws_role_gated.d.ts +38 -0
  284. package/dist/testing/cross_backend/ws_role_gated.d.ts.map +1 -0
  285. package/dist/testing/cross_backend/ws_role_gated.js +142 -0
  286. package/dist/testing/entities.d.ts +23 -0
  287. package/dist/testing/entities.d.ts.map +1 -1
  288. package/dist/testing/entities.js +37 -0
  289. package/dist/testing/integration.d.ts.map +1 -1
  290. package/dist/testing/integration.js +3 -1
  291. package/dist/testing/middleware.d.ts +10 -0
  292. package/dist/testing/middleware.d.ts.map +1 -1
  293. package/dist/testing/middleware.js +31 -7
  294. package/dist/testing/schema_introspect.js +1 -1
  295. package/dist/testing/schema_parity.d.ts +4 -3
  296. package/dist/testing/schema_parity.d.ts.map +1 -1
  297. package/dist/testing/schema_parity.js +4 -3
  298. package/dist/testing/schema_ready_fixture.d.ts +2 -4
  299. package/dist/testing/schema_ready_fixture.d.ts.map +1 -1
  300. package/dist/testing/schema_ready_fixture.js +2 -4
  301. package/dist/testing/stubs.d.ts +16 -3
  302. package/dist/testing/stubs.d.ts.map +1 -1
  303. package/dist/testing/stubs.js +20 -7
  304. package/dist/testing/surface_invariants.js +2 -2
  305. package/dist/ui/auth_state.svelte.d.ts +10 -0
  306. package/dist/ui/auth_state.svelte.d.ts.map +1 -1
  307. package/dist/ui/auth_state.svelte.js +22 -4
  308. package/package.json +1 -1
  309. package/dist/db/cell_history_ddl.d.ts +0 -38
  310. package/dist/db/cell_history_ddl.d.ts.map +0 -1
  311. package/dist/db/cell_history_ddl.js +0 -61
@@ -13,6 +13,7 @@ import { z } from 'zod';
13
13
  import { Uuid, bounded_string } from '../primitive_schemas.js';
14
14
  import { FRACTIONAL_INDEX_LENGTH_MAX, FRACTIONAL_INDEX_REGEX } from '@fuzdev/fuz_util/fractional_index.ts';
15
15
  import { ActingActor } from '../http/auth_shape.js';
16
+ import { CellRelationListLimit } from './cell_relation_list_limit.js';
16
17
  // -- Error reasons ----------------------------------------------------------
17
18
  /** Error reason — `cell_item_list` got neither `parent_id` nor `child_id`. */
18
19
  export const ERROR_CELL_ITEM_LIST_REQUIRES_PARENT_OR_CHILD = 'cell_item_list_requires_parent_or_child';
@@ -101,10 +102,11 @@ export const CellItemDeleteOutput = z.strictObject({
101
102
  * authz (child view-check gates the call; per-parent view-check filters
102
103
  * the rows).
103
104
  *
104
- * Forward listing supports cursor pagination via `position_after`
105
- * (return rows with `position > position_after`). The reverse listing
106
- * doesn't paginate (the result set is small in practice — number of
107
- * parents containing a given child).
105
+ * Either direction returns a page of up to `limit` items (default
106
+ * `CELL_RELATION_LIST_LIMIT_DEFAULT`) whose other end the caller can view —
107
+ * the view check runs before the limit. Forward listing pages on via
108
+ * `position_after` (return rows with `position > position_after`); the
109
+ * reverse listing has no cursor, oldest item first.
108
110
  */
109
111
  export const CellItemListInput = z
110
112
  .strictObject({
@@ -117,9 +119,7 @@ export const CellItemListInput = z
117
119
  position_after: CellItemPosition.optional().meta({
118
120
  description: 'Cursor for forward pagination — return rows whose position > this.'
119
121
  }),
120
- limit: z.number().int().positive().max(500).optional().meta({
121
- description: 'Page size cap (max 500). Omit for unbounded — explicit list calls escape the bundled `cell_get` cap.'
122
- }),
122
+ limit: CellRelationListLimit,
123
123
  acting: ActingActor
124
124
  })
125
125
  .refine((v) => Boolean(v.parent_id) !== Boolean(v.child_id), {
@@ -12,11 +12,12 @@
12
12
  * collision-error shape as insert.
13
13
  * - `cell_item_delete` — admin / owner / editor-grant on `parent`.
14
14
  * Idempotent: `deleted: false` when no row matched.
15
- * - `cell_item_list` — bidirectional. Forward (pass `parent_id`) is
16
- * gated on `can_view_cell(parent)` and filters children to those the
17
- * caller may view (strict target-visibility, batched). Reverse (pass
18
- * `child_id`) has 2-layer authz: gate on `can_view_cell(child)`, then
19
- * filter rows by `can_view_cell(parent)`.
15
+ * - `cell_item_list` — bidirectional, a page of up to `limit` items whose
16
+ * other end the caller may view (filtered in SQL, before the limit).
17
+ * Forward (pass `parent_id`) is gated on `can_view_cell(parent)` and
18
+ * pages the items whose child is viewable (strict target-visibility).
19
+ * Reverse (pass `child_id`) has 2-layer authz: gate on
20
+ * `can_view_cell(child)`, then page the items whose parent is viewable.
20
21
  *
21
22
  * IDOR-mask 404s on cell-miss / cell-unviewable, mirroring the existence-
22
23
  * leak guards in `auth/cell_actions.ts`.
@@ -1 +1 @@
1
- {"version":3,"file":"cell_item_actions.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/cell_item_actions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,OAAO,EAIN,KAAK,SAAS,EACd,MAAM,0BAA0B,CAAC;AAGlC,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAEnD,OAAO,EAeN,KAAK,QAAQ,EACb,MAAM,6BAA6B,CAAC;AAMrC,OAAO,EAMN,KAAK,WAAW,EAChB,MAAM,4BAA4B,CAAC;AAOpC,MAAM,MAAM,kBAAkB,GAAG,iBAAiB,CAAC;AAEnD,eAAO,MAAM,YAAY,GAAI,KAAK,WAAW,KAAG,QAS9C,CAAC;AAOH,iDAAiD;AACjD,eAAO,MAAM,wBAAwB,GAAI,MAAM,kBAAkB,KAAG,KAAK,CAAC,SAAS,CA2KlF,CAAC"}
1
+ {"version":3,"file":"cell_item_actions.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/cell_item_actions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,OAAO,EAIN,KAAK,SAAS,EACd,MAAM,0BAA0B,CAAC;AAGlC,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAEnD,OAAO,EAeN,KAAK,QAAQ,EACb,MAAM,6BAA6B,CAAC;AAMrC,OAAO,EAMN,KAAK,WAAW,EAChB,MAAM,4BAA4B,CAAC;AAOpC,MAAM,MAAM,kBAAkB,GAAG,iBAAiB,CAAC;AAEnD,eAAO,MAAM,YAAY,GAAI,KAAK,WAAW,KAAG,QAS9C,CAAC;AAOH,iDAAiD;AACjD,eAAO,MAAM,wBAAwB,GAAI,MAAM,kBAAkB,KAAG,KAAK,CAAC,SAAS,CAwKlF,CAAC"}
@@ -12,11 +12,12 @@
12
12
  * collision-error shape as insert.
13
13
  * - `cell_item_delete` — admin / owner / editor-grant on `parent`.
14
14
  * Idempotent: `deleted: false` when no row matched.
15
- * - `cell_item_list` — bidirectional. Forward (pass `parent_id`) is
16
- * gated on `can_view_cell(parent)` and filters children to those the
17
- * caller may view (strict target-visibility, batched). Reverse (pass
18
- * `child_id`) has 2-layer authz: gate on `can_view_cell(child)`, then
19
- * filter rows by `can_view_cell(parent)`.
15
+ * - `cell_item_list` — bidirectional, a page of up to `limit` items whose
16
+ * other end the caller may view (filtered in SQL, before the limit).
17
+ * Forward (pass `parent_id`) is gated on `can_view_cell(parent)` and
18
+ * pages the items whose child is viewable (strict target-visibility).
19
+ * Reverse (pass `child_id`) has 2-layer authz: gate on
20
+ * `can_view_cell(child)`, then page the items whose parent is viewable.
20
21
  *
21
22
  * IDOR-mask 404s on cell-miss / cell-unviewable, mirroring the existence-
22
23
  * leak guards in `auth/cell_actions.ts`.
@@ -31,8 +32,8 @@ import { jsonrpc_errors } from '../http/jsonrpc_errors.js';
31
32
  import { is_pg_unique_violation } from '../db/pg_error.js';
32
33
  import { cell_item_insert_action_spec, cell_item_move_action_spec, cell_item_delete_action_spec, cell_item_list_action_spec, ERROR_CELL_ITEM_POSITION_TAKEN } from './cell_item_action_specs.js';
33
34
  import { ERROR_CELL_NOT_FOUND } from './cell_action_specs.js';
34
- import { can_view_cell, can_edit_cell } from './cell_authorize.js';
35
- import { filter_visible_target_ids } from './cell_relation_visibility.js';
35
+ import { can_view_cell, can_edit_cell, cell_viewer_from_auth } from './cell_authorize.js';
36
+ import { CELL_RELATION_LIST_LIMIT_DEFAULT } from './cell_relation_list_limit.js';
36
37
  import { query_cell_get } from '../db/cell_queries.js';
37
38
  import { query_cell_grant_list_for_cell } from '../db/cell_grant_queries.js';
38
39
  import { query_cell_item_insert, query_cell_item_move, query_cell_item_delete, query_cell_item_list_for_parent, query_cell_item_list_for_child } from '../db/cell_item_queries.js';
@@ -160,8 +161,11 @@ export const create_cell_item_actions = (deps) => {
160
161
  };
161
162
  const list_handler = async (input, ctx) => {
162
163
  const auth = ctx.auth;
163
- // Forward listing: gate on can_view_cell(parent), then filter the
164
- // children to those the caller may view (strict target-visibility).
164
+ // an omitted limit is the default page, either direction
165
+ const limit = input.limit ?? CELL_RELATION_LIST_LIMIT_DEFAULT;
166
+ // Forward listing: gate on can_view_cell(parent), then page the items
167
+ // whose child the caller may view (strict target-visibility, in SQL
168
+ // before the limit).
165
169
  if (input.parent_id !== undefined) {
166
170
  const parent = await query_cell_get(ctx, input.parent_id);
167
171
  if (!parent) {
@@ -171,12 +175,8 @@ export const create_cell_item_actions = (deps) => {
171
175
  if (!can_view_cell(auth, parent, parent_grants)) {
172
176
  throw jsonrpc_errors.not_found('cell', { reason: ERROR_CELL_NOT_FOUND });
173
177
  }
174
- const rows = await query_cell_item_list_for_parent(ctx, parent.id, {
175
- limit: input.limit,
176
- position_after: input.position_after
177
- });
178
- const visible_children = await filter_visible_target_ids(ctx, auth, rows.map((r) => r.child_id));
179
- return { items: rows.filter((r) => visible_children.has(r.child_id)).map(to_item_json) };
178
+ const rows = await query_cell_item_list_for_parent(ctx, cell_viewer_from_auth(auth), parent.id, { limit, position_after: input.position_after });
179
+ return { items: rows.map(to_item_json) };
180
180
  }
181
181
  // Reverse listing: 2-layer authz. First, can_view_cell(child).
182
182
  const child = await query_cell_get(ctx, input.child_id);
@@ -187,13 +187,14 @@ export const create_cell_item_actions = (deps) => {
187
187
  if (!can_view_cell(auth, child, child_grants)) {
188
188
  throw jsonrpc_errors.not_found('cell', { reason: ERROR_CELL_NOT_FOUND });
189
189
  }
190
- // Then filter rows by per-parent can_view_cell. Batched (not N+1):
191
- // one bulk visibility filter over all parent ids, same as the forward
192
- // branch. Bounded by `limit` at the query so a heavily inbound-linked
193
- // child can't force an unbounded fetch on this public endpoint.
194
- const rows = await query_cell_item_list_for_child(ctx, child.id, { limit: input.limit });
195
- const visible_parents = await filter_visible_target_ids(ctx, auth, rows.map((r) => r.parent_id));
196
- return { items: rows.filter((r) => visible_parents.has(r.parent_id)).map(to_item_json) };
190
+ // Then page the items whose parent the caller may view — filtered in
191
+ // SQL before the limit, so hidden parents never crowd out visible ones,
192
+ // and bounded so a heavily inbound-linked child can't force an
193
+ // unbounded read on this public endpoint.
194
+ const rows = await query_cell_item_list_for_child(ctx, cell_viewer_from_auth(auth), child.id, {
195
+ limit
196
+ });
197
+ return { items: rows.map(to_item_json) };
197
198
  };
198
199
  return [
199
200
  rpc_action(cell_item_insert_action_spec, insert_handler),
@@ -0,0 +1,20 @@
1
+ /**
2
+ * The page bound the two relation lists share — `cell_field_list` and
3
+ * `cell_item_list`, forward and reverse alike. A page is up to `limit` edges
4
+ * whose other end the caller can view. Both are public reads, so an omitted
5
+ * `limit` is the default page, never unbounded: anyone can pad a public
6
+ * cell's inbound links, and the reverse lists have no cursor.
7
+ *
8
+ * Twin of the Rust spine's `CELL_RELATION_LIST_LIMIT_MAX` /
9
+ * `CELL_RELATION_LIST_LIMIT_DEFAULT`.
10
+ *
11
+ * @module
12
+ */
13
+ import { z } from 'zod';
14
+ /** Cap on a relation-list page, forward and reverse alike. */
15
+ export declare const CELL_RELATION_LIST_LIMIT_MAX = 500;
16
+ /** The page size a relation list that omits `limit` gets, forward and reverse alike. */
17
+ export declare const CELL_RELATION_LIST_LIMIT_DEFAULT = 50;
18
+ /** The `limit` input of `cell_field_list` and `cell_item_list`. */
19
+ export declare const CellRelationListLimit: z.ZodOptional<z.ZodNumber>;
20
+ //# sourceMappingURL=cell_relation_list_limit.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cell_relation_list_limit.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/cell_relation_list_limit.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,8DAA8D;AAC9D,eAAO,MAAM,4BAA4B,MAAM,CAAC;AAEhD,wFAAwF;AACxF,eAAO,MAAM,gCAAgC,KAAK,CAAC;AAEnD,mEAAmE;AACnE,eAAO,MAAM,qBAAqB,4BAQ/B,CAAC"}
@@ -0,0 +1,27 @@
1
+ /**
2
+ * The page bound the two relation lists share — `cell_field_list` and
3
+ * `cell_item_list`, forward and reverse alike. A page is up to `limit` edges
4
+ * whose other end the caller can view. Both are public reads, so an omitted
5
+ * `limit` is the default page, never unbounded: anyone can pad a public
6
+ * cell's inbound links, and the reverse lists have no cursor.
7
+ *
8
+ * Twin of the Rust spine's `CELL_RELATION_LIST_LIMIT_MAX` /
9
+ * `CELL_RELATION_LIST_LIMIT_DEFAULT`.
10
+ *
11
+ * @module
12
+ */
13
+ import { z } from 'zod';
14
+ /** Cap on a relation-list page, forward and reverse alike. */
15
+ export const CELL_RELATION_LIST_LIMIT_MAX = 500;
16
+ /** The page size a relation list that omits `limit` gets, forward and reverse alike. */
17
+ export const CELL_RELATION_LIST_LIMIT_DEFAULT = 50;
18
+ /** The `limit` input of `cell_field_list` and `cell_item_list`. */
19
+ export const CellRelationListLimit = z
20
+ .number()
21
+ .int()
22
+ .positive()
23
+ .max(CELL_RELATION_LIST_LIMIT_MAX)
24
+ .optional()
25
+ .meta({
26
+ description: `The most edges a page holds, each one whose other end the caller can view — forward and reverse alike: default ${CELL_RELATION_LIST_LIMIT_DEFAULT}, max ${CELL_RELATION_LIST_LIMIT_MAX}.`
27
+ });
@@ -6,9 +6,10 @@
6
6
  * target the caller cannot view — otherwise an editor of a public parent
7
7
  * could enumerate private children by id, or a public cell could leak the
8
8
  * existence of private linked cells. This helper bulk-loads the target
9
- * cells + their grants and runs `can_view_cell` per target in memory,
10
- * returning the set of viewable target ids. Batched (two queries for the
11
- * whole id-set) to avoid the N+1 a naive per-row check would cause.
9
+ * cells' view facts (`CELL_VIEW_COLUMNS` — never the `data` payload) and
10
+ * their grants, then runs `can_view_cell` per target in memory, returning
11
+ * the set of viewable target ids. Batched (two queries for the whole
12
+ * id-set) to avoid the N+1 a naive per-row check would cause.
12
13
  *
13
14
  * @module
14
15
  */
@@ -1 +1 @@
1
- {"version":3,"file":"cell_relation_visibility.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/cell_relation_visibility.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,yBAAyB,CAAC;AAEpD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAK3D;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,yBAAyB,GACrC,MAAM,SAAS,EACf,MAAM,cAAc,GAAG,IAAI,EAC3B,YAAY,aAAa,CAAC,IAAI,CAAC,KAC7B,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAwBnB,CAAC"}
1
+ {"version":3,"file":"cell_relation_visibility.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/cell_relation_visibility.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,yBAAyB,CAAC;AAEpD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAK3D;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,yBAAyB,GACrC,MAAM,SAAS,EACf,MAAM,cAAc,GAAG,IAAI,EAC3B,YAAY,aAAa,CAAC,IAAI,CAAC,KAC7B,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAwBnB,CAAC"}
@@ -6,14 +6,15 @@
6
6
  * target the caller cannot view — otherwise an editor of a public parent
7
7
  * could enumerate private children by id, or a public cell could leak the
8
8
  * existence of private linked cells. This helper bulk-loads the target
9
- * cells + their grants and runs `can_view_cell` per target in memory,
10
- * returning the set of viewable target ids. Batched (two queries for the
11
- * whole id-set) to avoid the N+1 a naive per-row check would cause.
9
+ * cells' view facts (`CELL_VIEW_COLUMNS` — never the `data` payload) and
10
+ * their grants, then runs `can_view_cell` per target in memory, returning
11
+ * the set of viewable target ids. Batched (two queries for the whole
12
+ * id-set) to avoid the N+1 a naive per-row check would cause.
12
13
  *
13
14
  * @module
14
15
  */
15
16
  import { can_view_cell } from './cell_authorize.js';
16
- import { query_cell_load_many } from '../db/cell_queries.js';
17
+ import { query_cell_view_load_many } from '../db/cell_queries.js';
17
18
  import { query_cell_grant_list_for_cells } from '../db/cell_grant_queries.js';
18
19
  /**
19
20
  * Return the subset of `target_ids` the caller may view.
@@ -33,7 +34,7 @@ export const filter_visible_target_ids = async (deps, auth, target_ids) => {
33
34
  if (target_ids.length === 0)
34
35
  return visible;
35
36
  const unique = [...new Set(target_ids)];
36
- const cells = await query_cell_load_many(deps, unique);
37
+ const cells = await query_cell_view_load_many(deps, unique);
37
38
  // Grants only matter for authenticated callers — null auth admits via
38
39
  // the public branch alone, so skip the grant load entirely.
39
40
  const grants_by_cell = new Map();
@@ -0,0 +1,45 @@
1
+ /**
2
+ * The steward read arm, with the governing-root load it needs.
3
+ *
4
+ * `can_steward_cell` (`auth/cell_authorize.ts`) is pure over
5
+ * `(auth, cell, root)`; `cell_steward_admits` is the composition every steward
6
+ * site runs after `can_view_cell` denies: it loads the governing root (active
7
+ * rows only, so a soft-deleted root stewards nothing) and applies the
8
+ * predicate. It short-circuits with no query unless the caller is
9
+ * authenticated and the cell is a pending or rejected contribution.
10
+ *
11
+ * **Where the steward arm applies** — the surfaces a moderator works from:
12
+ * `cell_get`, `cell_moderate` (whose root-authority check it lets a non-admin
13
+ * root owner reach), the cell-scoped fact route, and `cell_list`'s general
14
+ * predicate (the SQL twin in `query_cell_list`).
15
+ *
16
+ * **Where it must not** — every other read keeps `can_view_cell` alone:
17
+ * `cell_clone` (a steward copying a contribution out from under its
18
+ * moderation), the parent check on `cell_create` (a steward contributing under
19
+ * a pending post), the relation endpoint gates, `filter_visible_target_ids`
20
+ * (the `cell_get` bundle), the field / item lists' SQL view filter
21
+ * (`db/cell_view_sql.ts`),
22
+ * `cell_list`'s `shared_with` semi-join (a grant footprint, not a moderation
23
+ * queue), and `cell_grant_revoke`'s `still_admitted` recompute. Widening one of
24
+ * these is a decision, not a sweep. Twin of the Rust `fuz_cell::cell_steward`.
25
+ *
26
+ * @module
27
+ */
28
+ import type { QueryDeps } from '../db/query_deps.js';
29
+ import { type CellRow } from '../db/cell_queries.js';
30
+ import type { RequestContext } from './request_context.js';
31
+ /**
32
+ * Whether `auth` stewards `cell` — loads the governing root and applies
33
+ * `can_steward_cell`.
34
+ *
35
+ * Resolves `false` without a query for an unauthenticated caller, a root, or a
36
+ * cell whose moderation isn't `'pending'` / `'rejected'`; a missing or
37
+ * soft-deleted root is `false` too.
38
+ *
39
+ * @param deps - query deps
40
+ * @param auth - request context, or `null` for unauthenticated callers
41
+ * @param cell - the cell the caller couldn't otherwise view
42
+ * @returns whether the steward arm admits the caller
43
+ */
44
+ export declare const cell_steward_admits: (deps: QueryDeps, auth: RequestContext | null, cell: CellRow) => Promise<boolean>;
45
+ //# sourceMappingURL=cell_steward.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cell_steward.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/cell_steward.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,EAAkB,KAAK,OAAO,EAAE,MAAM,uBAAuB,CAAC;AAErE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAG3D;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,mBAAmB,GAC/B,MAAM,SAAS,EACf,MAAM,cAAc,GAAG,IAAI,EAC3B,MAAM,OAAO,KACX,OAAO,CAAC,OAAO,CAMjB,CAAC"}
@@ -0,0 +1,53 @@
1
+ /**
2
+ * The steward read arm, with the governing-root load it needs.
3
+ *
4
+ * `can_steward_cell` (`auth/cell_authorize.ts`) is pure over
5
+ * `(auth, cell, root)`; `cell_steward_admits` is the composition every steward
6
+ * site runs after `can_view_cell` denies: it loads the governing root (active
7
+ * rows only, so a soft-deleted root stewards nothing) and applies the
8
+ * predicate. It short-circuits with no query unless the caller is
9
+ * authenticated and the cell is a pending or rejected contribution.
10
+ *
11
+ * **Where the steward arm applies** — the surfaces a moderator works from:
12
+ * `cell_get`, `cell_moderate` (whose root-authority check it lets a non-admin
13
+ * root owner reach), the cell-scoped fact route, and `cell_list`'s general
14
+ * predicate (the SQL twin in `query_cell_list`).
15
+ *
16
+ * **Where it must not** — every other read keeps `can_view_cell` alone:
17
+ * `cell_clone` (a steward copying a contribution out from under its
18
+ * moderation), the parent check on `cell_create` (a steward contributing under
19
+ * a pending post), the relation endpoint gates, `filter_visible_target_ids`
20
+ * (the `cell_get` bundle), the field / item lists' SQL view filter
21
+ * (`db/cell_view_sql.ts`),
22
+ * `cell_list`'s `shared_with` semi-join (a grant footprint, not a moderation
23
+ * queue), and `cell_grant_revoke`'s `still_admitted` recompute. Widening one of
24
+ * these is a decision, not a sweep. Twin of the Rust `fuz_cell::cell_steward`.
25
+ *
26
+ * @module
27
+ */
28
+ import { query_cell_get } from '../db/cell_queries.js';
29
+ import { cell_moderation_is_stewardable } from '../db/cell_moderation.js';
30
+ import { can_steward_cell } from './cell_authorize.js';
31
+ /**
32
+ * Whether `auth` stewards `cell` — loads the governing root and applies
33
+ * `can_steward_cell`.
34
+ *
35
+ * Resolves `false` without a query for an unauthenticated caller, a root, or a
36
+ * cell whose moderation isn't `'pending'` / `'rejected'`; a missing or
37
+ * soft-deleted root is `false` too.
38
+ *
39
+ * @param deps - query deps
40
+ * @param auth - request context, or `null` for unauthenticated callers
41
+ * @param cell - the cell the caller couldn't otherwise view
42
+ * @returns whether the steward arm admits the caller
43
+ */
44
+ export const cell_steward_admits = async (deps, auth, cell) => {
45
+ if (auth === null || cell.root_id === null)
46
+ return false;
47
+ if (!cell_moderation_is_stewardable(cell.moderation))
48
+ return false;
49
+ const root = await query_cell_get(deps, cell.root_id);
50
+ if (!root)
51
+ return false;
52
+ return can_steward_cell(auth, cell, root);
53
+ };
@@ -22,12 +22,12 @@
22
22
  * stream ends). Otherwise a swept session's connections would be reachable
23
23
  * only by an account-wide revocation.
24
24
  *
25
- * The contract that follows: **a connection outlives its session's expiry by
26
- * at most one cleanup interval** (plus the pass itself). In a process that
27
- * does not schedule the sweep, expiry ends no connection at all. The contract
28
- * is for sessions only: an expired API token's row is not swept, so nothing
29
- * here closes a connection opened on one — and `account_token_revoke` still
30
- * finds the row to close for.
25
+ * The sweep's close is the second on expiry, not the first: every connection
26
+ * already closes at its credential's deadline (`auth/credential_deadline.ts`)
27
+ * in the process holding it, whether or not a sweep runs. The sweep is for
28
+ * sessions only: an expired API token's row is not swept — its connections
29
+ * close at their deadline, and `account_token_revoke` still finds the row to
30
+ * close for.
31
31
  *
32
32
  * The closes run after the delete's commit, which is the rule every
33
33
  * revocation close keeps (see `actions/connection_closer.ts`). An expired
@@ -118,9 +118,9 @@ import type { AuditEmitter } from './audit_emitter.js';
118
118
  * `DEFAULT_AUTH_CLEANUP_INTERVAL`.
119
119
  *
120
120
  * An expired session or offer is already refused by every check that reads
121
- * it, so the interval bounds three things: how long a connection outlives
122
- * the expiry of the session it opened on, how late an offer expiry's audit
123
- * row lands, and how long dead rows linger.
121
+ * it, and a connection closes at its credential's deadline on its own, so the
122
+ * interval bounds two things: how late an offer expiry's audit row lands, and
123
+ * how long dead rows linger.
124
124
  */
125
125
  export declare const DEFAULT_AUTH_CLEANUP_INTERVAL_MS: number;
126
126
  /**
@@ -22,12 +22,12 @@
22
22
  * stream ends). Otherwise a swept session's connections would be reachable
23
23
  * only by an account-wide revocation.
24
24
  *
25
- * The contract that follows: **a connection outlives its session's expiry by
26
- * at most one cleanup interval** (plus the pass itself). In a process that
27
- * does not schedule the sweep, expiry ends no connection at all. The contract
28
- * is for sessions only: an expired API token's row is not swept, so nothing
29
- * here closes a connection opened on one — and `account_token_revoke` still
30
- * finds the row to close for.
25
+ * The sweep's close is the second on expiry, not the first: every connection
26
+ * already closes at its credential's deadline (`auth/credential_deadline.ts`)
27
+ * in the process holding it, whether or not a sweep runs. The sweep is for
28
+ * sessions only: an expired API token's row is not swept — its connections
29
+ * close at their deadline, and `account_token_revoke` still finds the row to
30
+ * close for.
31
31
  *
32
32
  * The closes run after the delete's commit, which is the rule every
33
33
  * revocation close keeps (see `actions/connection_closer.ts`). An expired
@@ -117,9 +117,9 @@ import { query_audit_log } from './audit_log_queries.js';
117
117
  * `DEFAULT_AUTH_CLEANUP_INTERVAL`.
118
118
  *
119
119
  * An expired session or offer is already refused by every check that reads
120
- * it, so the interval bounds three things: how long a connection outlives
121
- * the expiry of the session it opened on, how late an offer expiry's audit
122
- * row lands, and how long dead rows linger.
120
+ * it, and a connection closes at its credential's deadline on its own, so the
121
+ * interval bounds two things: how late an offer expiry's audit row lands, and
122
+ * how long dead rows linger.
123
123
  */
124
124
  export const DEFAULT_AUTH_CLEANUP_INTERVAL_MS = 10 * 60 * 1000;
125
125
  /**
@@ -0,0 +1,95 @@
1
+ /**
2
+ * The credential deadline — the moment a resolved credential stops
3
+ * authenticating, which a long-lived connection opened on it must not outlive.
4
+ *
5
+ * A WebSocket or an SSE stream is authorized once, at open, and closed on
6
+ * revocation by identity. Expiry has no revocation event: nothing is written
7
+ * when a session or token passes its `expires_at`. So the auth middleware
8
+ * computes the deadline once (`CREDENTIAL_DEADLINE_KEY`, gathered onto
9
+ * `ResolvedAuth.credential_deadline`) and the transport closes the connection
10
+ * when it arrives (`schedule_credential_deadline`).
11
+ *
12
+ * The deadline is the earliest of what the credential carries:
13
+ *
14
+ * - **Session** — the signed cookie's embedded expiry and the `auth_session`
15
+ * row's `expires_at`.
16
+ * - **API token** — the `api_token` row's `expires_at`; an eternal token
17
+ * (`expires_at IS NULL`) has none.
18
+ * - **Daemon token** — none: it has no row and no expiry, and a rotation
19
+ * closes no connection.
20
+ *
21
+ * A row's expiry is read as **remaining time measured on the database clock**
22
+ * (`EXPIRES_IN_MS_SQL`, `expires_at - NOW()` in the same statement that checks
23
+ * `expires_at > NOW()`), then anchored to this process's monotonic clock
24
+ * (`performance.now()`) — so a skew between the database's clock and the
25
+ * server's moves no deadline. The cookie's expiry is a wall-clock second this
26
+ * server signed, read against the same wall clock the cookie check reads.
27
+ *
28
+ * The monotonic clock stops while the host is suspended, so a deadline on it
29
+ * alone would hold a connection past its credential by the time asleep. The
30
+ * wait (`schedule_credential_deadline`) therefore also carries the deadline on
31
+ * the wall clock (`Date.now()`), which keeps running through a suspend, and
32
+ * fires at whichever is reached first: after a resume, within one timer step
33
+ * (`CREDENTIAL_DEADLINE_TIMER_CAP_MS`, an hour). The wall deadline is taken
34
+ * from the time left when the wait starts, so a skew between the database's
35
+ * clock and the server's still moves nothing. A wall clock stepped forward
36
+ * can fire it early; the client reconnects through the gates, the fail-closed
37
+ * side of the trade.
38
+ *
39
+ * The twin of the Rust spine's `fuz_auth::ResolvedAuth::credential_deadline`
40
+ * and `fuz_realtime`'s deadline wait.
41
+ *
42
+ * @module
43
+ */
44
+ /**
45
+ * SQL expression for a row's remaining lifetime in milliseconds, measured on
46
+ * the database clock — `NULL` when `expires_at` is `NULL` (an eternal API
47
+ * token). `float8`, so every driver reads it as a JS number.
48
+ */
49
+ export declare const EXPIRES_IN_MS_SQL = "(EXTRACT(EPOCH FROM (expires_at - NOW())) * 1000)::float8";
50
+ /**
51
+ * The longest single timer the deadline wait sets — one hour, far inside
52
+ * `setTimeout`'s ceiling (2^31 - 1 ms, about 24.8 days, past which a timer
53
+ * fires at once) and the Rust twin's tokio ceiling, and short enough that a
54
+ * resume from suspend is noticed within it. A longer deadline is reached
55
+ * through several.
56
+ */
57
+ export declare const CREDENTIAL_DEADLINE_TIMER_CAP_MS = 3600000;
58
+ /**
59
+ * The deadline `now` plus the shortest of `remaining_ms`, as a monotonic
60
+ * `performance.now()` timestamp, or `null` when no entry carries one.
61
+ *
62
+ * A negative or `NaN` entry — a row its own `expires_at > NOW()` check
63
+ * already refused, read at the boundary — counts as zero.
64
+ *
65
+ * @param remaining_ms - each source's remaining lifetime in ms, `null` for none
66
+ * @param now - the monotonic clock reading to anchor to
67
+ * @returns the deadline, or `null` for a credential with no expiry
68
+ */
69
+ export declare const credential_deadline_after: (remaining_ms: ReadonlyArray<number | null>, now?: number) => number | null;
70
+ /**
71
+ * The next timer toward `deadline` from `now`: what remains, capped at
72
+ * `CREDENTIAL_DEADLINE_TIMER_CAP_MS`, or `null` once the deadline is reached.
73
+ *
74
+ * @param deadline - the monotonic deadline
75
+ * @param now - the monotonic clock reading
76
+ * @returns the next delay in ms, or `null` when the deadline has passed
77
+ */
78
+ export declare const next_credential_deadline_delay: (deadline: number, now: number) => number | null;
79
+ /**
80
+ * Call `on_expire` once `deadline` has passed, waiting in timers of at most
81
+ * `CREDENTIAL_DEADLINE_TIMER_CAP_MS`, each re-reading the clock against the
82
+ * absolute deadline — so a session's 30 days (past `setTimeout`'s ceiling)
83
+ * does not fire at once. Each wake-up also checks the same deadline on the
84
+ * wall clock, so a host suspend, which stops the monotonic clock, delays the
85
+ * close by at most one timer after the resume (module doc).
86
+ *
87
+ * A `null` deadline schedules nothing. The returned function cancels the wait
88
+ * and is safe to call more than once, before or after it fires.
89
+ *
90
+ * @param deadline - the monotonic deadline (`ResolvedAuth.credential_deadline`)
91
+ * @param on_expire - run once, when the deadline passes
92
+ * @returns a cancel function
93
+ */
94
+ export declare const schedule_credential_deadline: (deadline: number | null, on_expire: () => void) => (() => void);
95
+ //# sourceMappingURL=credential_deadline.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"credential_deadline.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/credential_deadline.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AAEH;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,8DAA8D,CAAC;AAE7F;;;;;;GAMG;AACH,eAAO,MAAM,gCAAgC,UAAY,CAAC;AAE1D;;;;;;;;;;GAUG;AACH,eAAO,MAAM,yBAAyB,GACrC,cAAc,aAAa,CAAC,MAAM,GAAG,IAAI,CAAC,EAC1C,MAAK,MAA0B,KAC7B,MAAM,GAAG,IAQX,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,8BAA8B,GAAI,UAAU,MAAM,EAAE,KAAK,MAAM,KAAG,MAAM,GAAG,IAIvF,CAAC;AAEF;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,4BAA4B,GACxC,UAAU,MAAM,GAAG,IAAI,EACvB,WAAW,MAAM,IAAI,KACnB,CAAC,MAAM,IAAI,CA2Bb,CAAC"}