@@ -2,6 +2,7 @@ import { Hono } from "hono";
22import { streamSSE } from "hono/streaming" ;
33import { errorResponse , parseAndValidateBody , parseLimitParam } from "../../lib/helpers" ;
44import { CreateLeaseSchema , QueryStatesSchema , UpsertStateSchema } from "../../lib/validation" ;
5+ import { rateLimitMiddleware } from "../../middleware/rate-limit" ;
56import { scopedAuth } from "../../middleware/scoped-auth" ;
67import { createLease } from "../../services/leases" ;
78import {
@@ -47,45 +48,52 @@ function notifyStateEvent(c: any, event: StateEventResponse) {
4748// GET /watch — SSE state event stream
4849// ---------------------------------------------------------------------------
4950
50- router . get ( "/watch" , scopedAuth ( { scope : "state:watch" , allowQueryToken : true } ) , async ( c ) => {
51- const after =
52- parsePositiveInt ( c . req . query ( "after" ) ) ?? parsePositiveInt ( c . req . header ( "Last-Event-ID" ) ) ?? 0 ;
53- const once = c . req . query ( "once" ) === "true" ;
54- const hub = stateHub ( c ) ;
55-
56- if ( hub && ! once ) {
57- const url = new URL ( "https://state-stream.local/watch" ) ;
58- url . searchParams . set ( "after" , String ( after ) ) ;
59- return hub . fetch (
60- new Request ( url , {
61- headers : { "X-Project-Id" : c . get ( "projectId" ) } ,
62- } ) ,
63- ) ;
64- }
51+ router . get (
52+ "/watch" ,
53+ scopedAuth ( { scope : "state:watch" , allowQueryToken : true } ) ,
54+ rateLimitMiddleware ,
55+ async ( c ) => {
56+ const after =
57+ parsePositiveInt ( c . req . query ( "after" ) ) ??
58+ parsePositiveInt ( c . req . header ( "Last-Event-ID" ) ) ??
59+ 0 ;
60+ const once = c . req . query ( "once" ) === "true" ;
61+ const hub = stateHub ( c ) ;
62+
63+ if ( hub && ! once ) {
64+ const url = new URL ( "https://state-stream.local/watch" ) ;
65+ url . searchParams . set ( "after" , String ( after ) ) ;
66+ return hub . fetch (
67+ new Request ( url , {
68+ headers : { "X-Project-Id" : c . get ( "projectId" ) } ,
69+ } ) ,
70+ ) ;
71+ }
6572
66- return streamSSE ( c , async ( stream ) => {
67- const events = await c
68- . get ( "d1Db" )
69- . prepare (
70- `SELECT sequence, id, state_key, agent_id, event_type, data, metadata, tags, idempotency_key, created_at
73+ return streamSSE ( c , async ( stream ) => {
74+ const events = await c
75+ . get ( "d1Db" )
76+ . prepare (
77+ `SELECT sequence, id, state_key, agent_id, event_type, data, metadata, tags, idempotency_key, created_at
7178 FROM state_events
7279 WHERE project_id = ? AND sequence > ?
7380 ORDER BY sequence ASC
7481 LIMIT 1000` ,
75- )
76- . bind ( c . get ( "projectId" ) , after )
77- . all < any > ( ) ;
78-
79- for ( const event of events . results ?? [ ] ) {
80- await stream . writeSSE ( {
81- id : String ( event . sequence ) ,
82- event : `state.${ event . event_type } ` ,
83- data : JSON . stringify ( event ) ,
84- } ) ;
85- }
86- stream . close ( ) ;
87- } ) ;
88- } ) ;
82+ )
83+ . bind ( c . get ( "projectId" ) , after )
84+ . all < any > ( ) ;
85+
86+ for ( const event of events . results ?? [ ] ) {
87+ await stream . writeSSE ( {
88+ id : String ( event . sequence ) ,
89+ event : `state.${ event . event_type } ` ,
90+ data : JSON . stringify ( event ) ,
91+ } ) ;
92+ }
93+ stream . close ( ) ;
94+ } ) ;
95+ } ,
96+ ) ;
8997
9098// ---------------------------------------------------------------------------
9199// POST /query — Rich state query
@@ -98,7 +106,7 @@ router.get("/watch", scopedAuth({ scope: "state:watch", allowQueryToken: true })
98106// whose `after` cursor is ascending (`sequence > after`).
99107// ---------------------------------------------------------------------------
100108
101- router . post ( "/query" , scopedAuth ( { scope : "state:read" } ) , async ( c ) => {
109+ router . post ( "/query" , scopedAuth ( { scope : "state:read" } ) , rateLimitMiddleware , async ( c ) => {
102110 const { data, error } = await parseAndValidateBody ( c , QueryStatesSchema ) ;
103111 if ( error ) return error ;
104112 if ( ! data ) return errorResponse ( c , "BAD_REQUEST" , "Invalid request body" , 400 ) ;
@@ -122,55 +130,65 @@ router.post("/query", scopedAuth({ scope: "state:read" }), async (c) => {
122130// POST /:state_key/lease — Acquire lease
123131// ---------------------------------------------------------------------------
124132
125- router . post ( "/:state_key/lease" , scopedAuth ( { scope : "lease:write" } ) , async ( c ) => {
126- const { data, error } = await parseAndValidateBody ( c , CreateLeaseSchema ) ;
127- if ( error ) return error ;
128- if ( ! data ) return errorResponse ( c , "BAD_REQUEST" , "Invalid request body" , 400 ) ;
129-
130- const result = await createLease (
131- c . get ( "db" ) ,
132- c . get ( "projectId" ) ,
133- c . req . param ( "state_key" ) ,
134- data . holder ,
135- data . ttl_ms ,
136- ) ;
133+ router . post (
134+ "/:state_key/lease" ,
135+ scopedAuth ( { scope : "lease:write" } ) ,
136+ rateLimitMiddleware ,
137+ async ( c ) => {
138+ const { data, error } = await parseAndValidateBody ( c , CreateLeaseSchema ) ;
139+ if ( error ) return error ;
140+ if ( ! data ) return errorResponse ( c , "BAD_REQUEST" , "Invalid request body" , 400 ) ;
141+
142+ const result = await createLease (
143+ c . get ( "db" ) ,
144+ c . get ( "projectId" ) ,
145+ c . req . param ( "state_key" ) ,
146+ data . holder ,
147+ data . ttl_ms ,
148+ ) ;
137149
138- if ( result . error ) {
139- return errorResponse ( c , result . error . code , result . error . message , result . error . status ) ;
140- }
150+ if ( result . error ) {
151+ return errorResponse ( c , result . error . code , result . error . message , result . error . status ) ;
152+ }
141153
142- return c . json ( result . lease , 201 ) ;
143- } ) ;
154+ return c . json ( result . lease , 201 ) ;
155+ } ,
156+ ) ;
144157
145158// ---------------------------------------------------------------------------
146159// GET /:state_key/events — WAL events
147160// ---------------------------------------------------------------------------
148161
149- router . get ( "/:state_key/events" , scopedAuth ( { scope : "state:read" } ) , async ( c ) => {
150- const after = parsePositiveInt ( c . req . query ( "after" ) ) ?? 0 ;
151- const limit = parseLimitParam ( c . req . query ( "limit" ) , 50 , 500 ) ;
152- const events = await listStateEvents (
153- c . get ( "d1Db" ) ,
154- c . get ( "projectId" ) ,
155- c . req . param ( "state_key" ) ,
156- after ,
157- limit ,
158- ) ;
162+ router . get (
163+ "/:state_key/events" ,
164+ scopedAuth ( { scope : "state:read" } ) ,
165+ rateLimitMiddleware ,
166+ async ( c ) => {
167+ const after = parsePositiveInt ( c . req . query ( "after" ) ) ?? 0 ;
168+ const limit = parseLimitParam ( c . req . query ( "limit" ) , 50 , 500 ) ;
169+ const events = await listStateEvents (
170+ c . get ( "d1Db" ) ,
171+ c . get ( "projectId" ) ,
172+ c . req . param ( "state_key" ) ,
173+ after ,
174+ limit ,
175+ ) ;
159176
160- return c . json ( {
161- data : events ,
162- pagination : {
163- limit : events . length ,
164- next_cursor : events . length ? String ( events [ events . length - 1 ] . sequence ) : null ,
165- } ,
166- } ) ;
167- } ) ;
177+ return c . json ( {
178+ data : events ,
179+ pagination : {
180+ limit : events . length ,
181+ next_cursor : events . length ? String ( events [ events . length - 1 ] . sequence ) : null ,
182+ } ,
183+ } ) ;
184+ } ,
185+ ) ;
168186
169187// ---------------------------------------------------------------------------
170188// PUT /:state_key — Upsert full state
171189// ---------------------------------------------------------------------------
172190
173- router . put ( "/:state_key" , scopedAuth ( { scope : "state:write" } ) , async ( c ) => {
191+ router . put ( "/:state_key" , scopedAuth ( { scope : "state:write" } ) , rateLimitMiddleware , async ( c ) => {
174192 const { data, error } = await parseAndValidateBody ( c , UpsertStateSchema ) ;
175193 if ( error ) return error ;
176194 if ( ! data ) return errorResponse ( c , "BAD_REQUEST" , "Invalid request body" , 400 ) ;
@@ -219,7 +237,7 @@ router.put("/:state_key", scopedAuth({ scope: "state:write" }), async (c) => {
219237// GET /:state_key — Latest or historical state
220238// ---------------------------------------------------------------------------
221239
222- router . get ( "/:state_key" , scopedAuth ( { scope : "state:read" } ) , async ( c ) => {
240+ router . get ( "/:state_key" , scopedAuth ( { scope : "state:read" } ) , rateLimitMiddleware , async ( c ) => {
223241 const atSequence = parsePositiveInt ( c . req . query ( "at_sequence" ) ) ;
224242 const atTime = parsePositiveInt ( c . req . query ( "at_time" ) ) ;
225243 const state =
@@ -241,46 +259,53 @@ router.get("/:state_key", scopedAuth({ scope: "state:read" }), async (c) => {
241259// DELETE /:state_key — Tombstone state
242260// ---------------------------------------------------------------------------
243261
244- router . delete ( "/:state_key" , scopedAuth ( { scope : "state:write" } ) , async ( c ) => {
245- const stateKey = c . req . param ( "state_key" ) ;
246- const leaseId = c . req . header ( "X-Lease-Id" ) ?? c . req . query ( "lease_id" ) ;
247- const idempotencyKey = c . req . header ( "Idempotency-Key" ) ;
248- const requestHash = await buildIdempotencyHash ( "DELETE" , stateKey , { lease_id : leaseId ?? null } ) ;
249- const cached = await readIdempotency (
250- c . get ( "d1Db" ) ,
251- c . get ( "projectId" ) ,
252- idempotencyKey ,
253- requestHash ,
254- ) ;
255- if ( cached . error )
256- return errorResponse ( c , cached . error . code , cached . error . message , cached . error . status ) ;
257- if ( cached . replay ) return cached . replay ;
258-
259- const result = await deleteState (
260- c . get ( "d1Db" ) ,
261- c . get ( "projectId" ) ,
262- stateKey ,
263- leaseId ,
264- idempotencyKey ,
265- requestHash ,
266- ) ;
267- if ( result . error )
268- return errorResponse ( c , result . error . code , result . error . message , result . error . status ) ;
269- // A concurrent duplicate carried the same Idempotency-Key: this request's
270- // mutation was rolled back atomically; serve the winner's stored response.
271- if ( result . replay ) return result . replay ;
272- if ( ! result . result ) return errorResponse ( c , "INTERNAL_ERROR" , "State delete failed" , 500 ) ;
273-
274- await storeIdempotency (
275- c . get ( "d1Db" ) ,
276- c . get ( "projectId" ) ,
277- idempotencyKey ,
278- requestHash ,
279- result . result . status ,
280- result . result . body ,
281- ) ;
282- notifyStateEvent ( c , result . result . event ) ;
283- return c . json ( result . result . body , 200 ) ;
284- } ) ;
262+ router . delete (
263+ "/:state_key" ,
264+ scopedAuth ( { scope : "state:write" } ) ,
265+ rateLimitMiddleware ,
266+ async ( c ) => {
267+ const stateKey = c . req . param ( "state_key" ) ;
268+ const leaseId = c . req . header ( "X-Lease-Id" ) ?? c . req . query ( "lease_id" ) ;
269+ const idempotencyKey = c . req . header ( "Idempotency-Key" ) ;
270+ const requestHash = await buildIdempotencyHash ( "DELETE" , stateKey , {
271+ lease_id : leaseId ?? null ,
272+ } ) ;
273+ const cached = await readIdempotency (
274+ c . get ( "d1Db" ) ,
275+ c . get ( "projectId" ) ,
276+ idempotencyKey ,
277+ requestHash ,
278+ ) ;
279+ if ( cached . error )
280+ return errorResponse ( c , cached . error . code , cached . error . message , cached . error . status ) ;
281+ if ( cached . replay ) return cached . replay ;
282+
283+ const result = await deleteState (
284+ c . get ( "d1Db" ) ,
285+ c . get ( "projectId" ) ,
286+ stateKey ,
287+ leaseId ,
288+ idempotencyKey ,
289+ requestHash ,
290+ ) ;
291+ if ( result . error )
292+ return errorResponse ( c , result . error . code , result . error . message , result . error . status ) ;
293+ // A concurrent duplicate carried the same Idempotency-Key: this request's
294+ // mutation was rolled back atomically; serve the winner's stored response.
295+ if ( result . replay ) return result . replay ;
296+ if ( ! result . result ) return errorResponse ( c , "INTERNAL_ERROR" , "State delete failed" , 500 ) ;
297+
298+ await storeIdempotency (
299+ c . get ( "d1Db" ) ,
300+ c . get ( "projectId" ) ,
301+ idempotencyKey ,
302+ requestHash ,
303+ result . result . status ,
304+ result . result . body ,
305+ ) ;
306+ notifyStateEvent ( c , result . result . event ) ;
307+ return c . json ( result . result . body , 200 ) ;
308+ } ,
309+ ) ;
285310
286311export default router ;
0 commit comments