@edge-base/server 0.1.5 → 0.2.1

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 (100) hide show
  1. package/admin-build/_app/immutable/assets/19.4Si2ZFC_.css +1 -0
  2. package/admin-build/_app/immutable/assets/{3.Dg81Pgmd.css → 3.BtHYobTg.css} +1 -1
  3. package/admin-build/_app/immutable/assets/SqlEditor.Bbp1RIk0.css +1 -0
  4. package/admin-build/_app/immutable/assets/TableSqlTab.yeNZfhgG.css +1 -0
  5. package/admin-build/_app/immutable/chunks/B0QyxC2M.js +128 -0
  6. package/admin-build/_app/immutable/chunks/{Bsp3uE8m.js → BCKr7yKd.js} +1 -1
  7. package/admin-build/_app/immutable/chunks/{CWHVkYdi.js → BFs_qStz.js} +1 -1
  8. package/admin-build/_app/immutable/chunks/{CKIubXVC.js → BTJcQFEp.js} +1 -1
  9. package/admin-build/_app/immutable/chunks/BY07qVPA.js +1 -0
  10. package/admin-build/_app/immutable/chunks/{Cn5ZQY9O.js → BcIUK2sk.js} +1 -1
  11. package/admin-build/_app/immutable/chunks/{CKFIU4S8.js → BsFiK_FJ.js} +1 -1
  12. package/admin-build/_app/immutable/chunks/{COYZ6F_d.js → CSGrwS7E.js} +1 -1
  13. package/admin-build/_app/immutable/chunks/{DLc6L4xD.js → CqUxCvs_.js} +1 -1
  14. package/admin-build/_app/immutable/chunks/{DLTo2HQr.js → D-x55wdW.js} +1 -1
  15. package/admin-build/_app/immutable/chunks/D755Tqat.js +1 -0
  16. package/admin-build/_app/immutable/chunks/{Jxx0jGlP.js → DjOEv9M9.js} +1 -1
  17. package/admin-build/_app/immutable/chunks/{DLRcaFHo.js → DnLqc9L1.js} +1 -1
  18. package/admin-build/_app/immutable/chunks/{BnlxEqP4.js → Dqk2TGNU.js} +1 -1
  19. package/admin-build/_app/immutable/chunks/{7FYfV8UU.js → k0CIJkw4.js} +1 -1
  20. package/admin-build/_app/immutable/chunks/lSpxLU5p.js +2 -0
  21. package/admin-build/_app/immutable/chunks/m9QZTyVV.js +1 -0
  22. package/admin-build/_app/immutable/entry/{app.C4kLStKR.js → app.BTsq3_xq.js} +2 -2
  23. package/admin-build/_app/immutable/entry/start.zXCirpgY.js +1 -0
  24. package/admin-build/_app/immutable/nodes/0.BZ00WDYH.js +1 -0
  25. package/admin-build/_app/immutable/nodes/{1.s1kW8gyv.js → 1.RzSJ3yyr.js} +1 -1
  26. package/admin-build/_app/immutable/nodes/{10.Cr9ml-GD.js → 10.D-rsiquF.js} +1 -1
  27. package/admin-build/_app/immutable/nodes/{11.DXTOyMEn.js → 11.l7-bgtFD.js} +1 -1
  28. package/admin-build/_app/immutable/nodes/{12.DzhitECA.js → 12.Dkq0H7B5.js} +1 -1
  29. package/admin-build/_app/immutable/nodes/{13.U_1VNA5x.js → 13.DtK_4oRz.js} +1 -1
  30. package/admin-build/_app/immutable/nodes/{14.x0SzmV0P.js → 14.BKo7-AMx.js} +1 -1
  31. package/admin-build/_app/immutable/nodes/{15.BJI1Z1hk.js → 15.CQAj_6lq.js} +1 -1
  32. package/admin-build/_app/immutable/nodes/{16.CUvRFqLW.js → 16.XVIG-Ffr.js} +1 -1
  33. package/admin-build/_app/immutable/nodes/{17.sDFD-Vbm.js → 17.g6raZLCM.js} +1 -1
  34. package/admin-build/_app/immutable/nodes/{18.DdoTnAXc.js → 18.IQz6a3T6.js} +1 -1
  35. package/admin-build/_app/immutable/nodes/19.CAAZ8i8h.js +2 -0
  36. package/admin-build/_app/immutable/nodes/{20.fxudyPKp.js → 20.BPcX3KPj.js} +1 -1
  37. package/admin-build/_app/immutable/nodes/21.DoPabrY_.js +1 -0
  38. package/admin-build/_app/immutable/nodes/{22.CY6ICoyn.js → 22.Br5AG_5Z.js} +1 -1
  39. package/admin-build/_app/immutable/nodes/{23.DmEaHsZw.js → 23.KjbrdXoE.js} +1 -1
  40. package/admin-build/_app/immutable/nodes/{24.Cqerdln2.js → 24.C3n2-hgw.js} +1 -1
  41. package/admin-build/_app/immutable/nodes/25.SFDSBzHd.js +2 -0
  42. package/admin-build/_app/immutable/nodes/26.D95vui6E.js +1 -0
  43. package/admin-build/_app/immutable/nodes/{27.DAeKhnG9.js → 27.FgLgdjwB.js} +1 -1
  44. package/admin-build/_app/immutable/nodes/{28.D_lJZpNR.js → 28.B9sYYm1F.js} +1 -1
  45. package/admin-build/_app/immutable/nodes/{29.Dx7iQCpT.js → 29.DyqZ_wbN.js} +1 -1
  46. package/admin-build/_app/immutable/nodes/3.Bzo2yVIO.js +2 -0
  47. package/admin-build/_app/immutable/nodes/{30.DcO-bjQZ.js → 30.c1CiNwiS.js} +1 -1
  48. package/admin-build/_app/immutable/nodes/{31.DesWAzIF.js → 31.CXty66Vh.js} +1 -1
  49. package/admin-build/_app/immutable/nodes/{4.B2rm4_4a.js → 4.BgQaXZ27.js} +1 -1
  50. package/admin-build/_app/immutable/nodes/{5.4Os1aYeW.js → 5.BuJrHvxH.js} +1 -1
  51. package/admin-build/_app/immutable/nodes/{6.BzR--5Dj.js → 6.CkBBC94k.js} +1 -1
  52. package/admin-build/_app/immutable/nodes/{7.QDC-47H1.js → 7.D2YBvNFM.js} +1 -1
  53. package/admin-build/_app/immutable/nodes/{8.DDPWEc96.js → 8.D8qQWo_z.js} +1 -1
  54. package/admin-build/_app/immutable/nodes/{9.fyfVIJPa.js → 9.BLDLX5hV.js} +1 -1
  55. package/admin-build/_app/version.json +1 -1
  56. package/admin-build/index.html +7 -7
  57. package/openapi.json +6710 -5866
  58. package/package.json +2 -2
  59. package/src/__tests__/functions-route.test.ts +153 -0
  60. package/src/__tests__/internal-request.test.ts +5 -5
  61. package/src/__tests__/meta-export-coverage.test.ts +3 -2
  62. package/src/__tests__/meta-route-registration.test.ts +3 -2
  63. package/src/__tests__/openapi-coverage.test.ts +5 -1
  64. package/src/__tests__/pagination.test.ts +12 -8
  65. package/src/__tests__/postgres-dialect.test.ts +2 -2
  66. package/src/__tests__/query.test.ts +7 -7
  67. package/src/__tests__/rate-limit.test.ts +0 -1
  68. package/src/__tests__/room-handler-context.test.ts +31 -0
  69. package/src/__tests__/room-runtime-routing.test.ts +48 -0
  70. package/src/__tests__/runtime-surface-accounting.test.ts +5 -4
  71. package/src/__tests__/smoke-skip-report.test.ts +3 -2
  72. package/src/durable-objects/database-do.ts +9 -4
  73. package/src/durable-objects/database-live-do.ts +22 -10
  74. package/src/durable-objects/logs-do.ts +2 -2
  75. package/src/durable-objects/rooms-do.ts +202 -0
  76. package/src/lib/auth-d1-service.ts +1 -1
  77. package/src/lib/auth-d1.ts +10 -0
  78. package/src/lib/d1-handler.ts +23 -4
  79. package/src/lib/internal-request.ts +5 -8
  80. package/src/lib/openapi.ts +1 -0
  81. package/src/lib/pagination.ts +3 -3
  82. package/src/lib/postgres-handler.ts +2 -2
  83. package/src/lib/query-engine.ts +2 -2
  84. package/src/middleware/rate-limit.ts +11 -11
  85. package/src/routes/admin.ts +30 -3
  86. package/src/routes/auth.ts +74 -33
  87. package/src/routes/room.ts +42 -0
  88. package/src/types.ts +6 -0
  89. package/admin-build/_app/immutable/assets/TableSqlTab.BHquaMBM.css +0 -1
  90. package/admin-build/_app/immutable/chunks/BPXFNSAT.js +0 -128
  91. package/admin-build/_app/immutable/chunks/DKjA1S1a.js +0 -1
  92. package/admin-build/_app/immutable/chunks/fBE8lw-R.js +0 -1
  93. package/admin-build/_app/immutable/chunks/gE9fQ_Ff.js +0 -2
  94. package/admin-build/_app/immutable/entry/start.CAAH6ztW.js +0 -1
  95. package/admin-build/_app/immutable/nodes/0.BywaJfpH.js +0 -1
  96. package/admin-build/_app/immutable/nodes/19.CUvRFqLW.js +0 -1
  97. package/admin-build/_app/immutable/nodes/21.N2QN0lbw.js +0 -1
  98. package/admin-build/_app/immutable/nodes/25.x3hL7Y-O.js +0 -2
  99. package/admin-build/_app/immutable/nodes/26.CEzfjTSO.js +0 -1
  100. package/admin-build/_app/immutable/nodes/3.CKC_yDnF.js +0 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@edge-base/server",
3
- "version": "0.1.5",
3
+ "version": "0.2.1",
4
4
  "description": "EdgeBase runtime assets consumed by the EdgeBase CLI",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -34,7 +34,7 @@
34
34
  "jose": "^6.0.0",
35
35
  "pg": "^8.16.3",
36
36
  "zod": "^4.3.6",
37
- "@edge-base/shared": "0.1.5"
37
+ "@edge-base/shared": "0.2.1"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@cloudflare/vitest-pool-workers": "^0.8.71",
@@ -1,4 +1,5 @@
1
1
  import { afterEach, describe, expect, it } from 'vitest';
2
+ import type { FunctionDefinition } from '@edge-base/shared';
2
3
  import { OpenAPIHono } from '../lib/hono.js';
3
4
  import { setConfig } from '../lib/do-router.js';
4
5
  import {
@@ -73,6 +74,29 @@ function createExecutionContext(): ExecutionContext {
73
74
  } as unknown as ExecutionContext;
74
75
  }
75
76
 
77
+ function httpFunction(
78
+ method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE',
79
+ handler: (ctx: Record<string, any>) => Promise<unknown> | unknown,
80
+ path?: string,
81
+ ): FunctionDefinition {
82
+ return {
83
+ trigger: {
84
+ type: 'http' as const,
85
+ method,
86
+ ...(path ? { path } : {}),
87
+ },
88
+ handler: async (ctx: unknown) => handler(ctx as Record<string, any>),
89
+ };
90
+ }
91
+
92
+ async function invokeFunction(path: string, method = 'GET') {
93
+ return createApp().fetch(
94
+ new Request(`http://localhost/api/functions/${path}`, { method }),
95
+ createEnv(),
96
+ createExecutionContext(),
97
+ );
98
+ }
99
+
76
100
  afterEach(() => {
77
101
  clearFunctionRegistry();
78
102
  clearMiddlewareRegistry();
@@ -137,3 +161,132 @@ describe('functionsRoute FunctionError compatibility', () => {
137
161
  });
138
162
  });
139
163
  });
164
+
165
+ describe('functionsRoute HTTP contracts', () => {
166
+ it('serializes plain objects as JSON responses', async () => {
167
+ registerFunction('reports/summary', httpFunction('GET', async () => ({
168
+ ok: true,
169
+ total: 3,
170
+ })));
171
+ rebuildCompiledRoutes();
172
+
173
+ const response = await invokeFunction('reports/summary');
174
+
175
+ expect(response.status).toBe(200);
176
+ expect(response.headers.get('content-type')).toContain('application/json');
177
+ await expect(response.json()).resolves.toEqual({ ok: true, total: 3 });
178
+ });
179
+
180
+ it('returns text/plain when handler returns a string', async () => {
181
+ registerFunction('health', httpFunction('GET', async () => 'healthy'));
182
+ rebuildCompiledRoutes();
183
+
184
+ const response = await invokeFunction('health');
185
+
186
+ expect(response.status).toBe(200);
187
+ expect(response.headers.get('content-type')).toContain('text/plain');
188
+ await expect(response.text()).resolves.toBe('healthy');
189
+ });
190
+
191
+ it('returns 204 when handler returns null', async () => {
192
+ registerFunction('empty', httpFunction('POST', async () => null));
193
+ rebuildCompiledRoutes();
194
+
195
+ const response = await invokeFunction('empty', 'POST');
196
+
197
+ expect(response.status).toBe(204);
198
+ await expect(response.text()).resolves.toBe('');
199
+ });
200
+
201
+ it('passes through native Response objects untouched', async () => {
202
+ registerFunction('created', httpFunction('POST', async () => (
203
+ new Response('created-body', {
204
+ status: 201,
205
+ headers: { 'content-type': 'text/plain', 'x-fn': 'direct-response' },
206
+ })
207
+ )));
208
+ rebuildCompiledRoutes();
209
+
210
+ const response = await invokeFunction('created', 'POST');
211
+
212
+ expect(response.status).toBe(201);
213
+ expect(response.headers.get('x-fn')).toBe('direct-response');
214
+ await expect(response.text()).resolves.toBe('created-body');
215
+ });
216
+
217
+ it('executes directory middleware before the handler', async () => {
218
+ const executionOrder: string[] = [];
219
+ registerMiddleware('secure', async () => {
220
+ executionOrder.push('middleware');
221
+ });
222
+ registerFunction('secure/audit', httpFunction('GET', async () => {
223
+ executionOrder.push('handler');
224
+ return { executionOrder };
225
+ }));
226
+ rebuildCompiledRoutes();
227
+
228
+ const response = await invokeFunction('secure/audit');
229
+
230
+ expect(response.status).toBe(200);
231
+ await expect(response.json()).resolves.toEqual({
232
+ executionOrder: ['middleware', 'handler'],
233
+ });
234
+ });
235
+
236
+ it('supports custom trigger.path params and preserves query strings', async () => {
237
+ registerFunction(
238
+ 'shortlink/resolve',
239
+ httpFunction(
240
+ 'GET',
241
+ async (ctx) => {
242
+ const requestUrl = new URL(ctx.request.url);
243
+ return {
244
+ code: ctx.params.code,
245
+ target: requestUrl.searchParams.get('target'),
246
+ };
247
+ },
248
+ '/s/:code',
249
+ ),
250
+ );
251
+ rebuildCompiledRoutes();
252
+
253
+ const response = await createApp().fetch(
254
+ new Request('http://localhost/api/functions/s/abc123?target=docs', { method: 'GET' }),
255
+ createEnv(),
256
+ createExecutionContext(),
257
+ );
258
+
259
+ expect(response.status).toBe(200);
260
+ await expect(response.json()).resolves.toEqual({
261
+ code: 'abc123',
262
+ target: 'docs',
263
+ });
264
+ });
265
+
266
+ it('supports catch-all params at execution time', async () => {
267
+ registerFunction('docs/[...slug]', httpFunction('GET', async (ctx) => ({
268
+ slug: ctx.params.slug,
269
+ })));
270
+ rebuildCompiledRoutes();
271
+
272
+ const response = await invokeFunction('docs/guides/getting-started/install');
273
+
274
+ expect(response.status).toBe(200);
275
+ await expect(response.json()).resolves.toEqual({
276
+ slug: 'guides/getting-started/install',
277
+ });
278
+ });
279
+
280
+ it('returns 405 with structured JSON when method does not match', async () => {
281
+ registerFunction('users', httpFunction('GET', async () => ({ ok: true })));
282
+ rebuildCompiledRoutes();
283
+
284
+ const response = await invokeFunction('users', 'POST');
285
+
286
+ expect(response.status).toBe(405);
287
+ await expect(response.json()).resolves.toEqual({
288
+ code: 405,
289
+ message: "Method POST not allowed for 'users'.",
290
+ });
291
+ });
292
+ });
@@ -21,16 +21,16 @@ function makeContext(
21
21
  }
22
22
 
23
23
  describe('internal request helpers', () => {
24
- it('isTrustedInternalRequestUrl only trusts worker-internal hosts', () => {
25
- expect(isTrustedInternalRequestUrl('http://internal/api/db/shared')).toBe(true);
26
- expect(isTrustedInternalRequestUrl('http://do/internal/functions/reindex')).toBe(true);
24
+ it('isTrustedInternalRequestUrl never trusts hostnames', () => {
25
+ expect(isTrustedInternalRequestUrl('http://internal/api/db/shared')).toBe(false);
26
+ expect(isTrustedInternalRequestUrl('http://do/internal/functions/reindex')).toBe(false);
27
27
  expect(isTrustedInternalRequestUrl('http://localhost:8787/api/db/shared')).toBe(false);
28
28
  expect(isTrustedInternalRequestUrl('not-a-url')).toBe(false);
29
29
  });
30
30
 
31
- it('isTrustedInternalContext accepts explicit internal flags or trusted internal hosts', () => {
31
+ it('isTrustedInternalContext only accepts explicit internal flags', () => {
32
32
  expect(isTrustedInternalContext(makeContext('http://localhost/api/db/shared', true))).toBe(true);
33
- expect(isTrustedInternalContext(makeContext('http://do/api/db/shared'))).toBe(true);
33
+ expect(isTrustedInternalContext(makeContext('http://do/api/db/shared'))).toBe(false);
34
34
  expect(isTrustedInternalContext(makeContext('http://example.com/api/db/shared'))).toBe(false);
35
35
  });
36
36
 
@@ -9,10 +9,11 @@
9
9
  */
10
10
  import { readFileSync, readdirSync } from 'fs';
11
11
  import { resolve } from 'path';
12
+ import { fileURLToPath } from 'url';
12
13
  import { describe, it, expect } from 'vitest';
13
14
 
14
- const LIB_DIR = resolve(new URL('../lib', import.meta.url).pathname);
15
- const TEST_DIR = resolve(new URL('.', import.meta.url).pathname);
15
+ const LIB_DIR = resolve(fileURLToPath(new URL('../lib', import.meta.url)));
16
+ const TEST_DIR = resolve(fileURLToPath(new URL('.', import.meta.url)));
16
17
 
17
18
  // ─── Lib files that do NOT yet have dedicated tests ─────────────────────────
18
19
  // Remove entries as tests are added — each removal is a net win.
@@ -6,14 +6,15 @@
6
6
  */
7
7
  import { readFileSync, readdirSync } from 'fs';
8
8
  import { resolve } from 'path';
9
+ import { fileURLToPath } from 'url';
9
10
  import { describe, it, expect } from 'vitest';
10
11
 
11
12
  describe('index.ts route registration completeness', () => {
12
13
  const source = readFileSync(
13
- new URL('../index.ts', import.meta.url),
14
+ fileURLToPath(new URL('../index.ts', import.meta.url)),
14
15
  'utf-8',
15
16
  );
16
- const routesDir = resolve(new URL('../routes', import.meta.url).pathname);
17
+ const routesDir = resolve(fileURLToPath(new URL('../routes', import.meta.url)));
17
18
  const EXPECTED_ROUTES = readdirSync(routesDir)
18
19
  .filter((fileName) => fileName.endsWith('.ts'))
19
20
  .sort()
@@ -12,10 +12,11 @@
12
12
  */
13
13
  import { readFileSync, readdirSync } from 'fs';
14
14
  import { resolve } from 'path';
15
+ import { fileURLToPath } from 'url';
15
16
  import { describe, it, expect } from 'vitest';
16
17
  import { normalizeOpenApiDocument, type OpenApiSpec } from '../lib/openapi.js';
17
18
 
18
- const ROUTES_DIR = resolve(new URL('../routes', import.meta.url).pathname);
19
+ const ROUTES_DIR = resolve(fileURLToPath(new URL('../routes', import.meta.url)));
19
20
 
20
21
  // ─── Intentionally non-OpenAPI route registrations ───────────────────────────
21
22
  // Format: "filename.ts:<line-content-substring>"
@@ -118,6 +119,7 @@ describe('OpenAPI route coverage', () => {
118
119
  '/admin/api/setup': { get: {} },
119
120
  '/admin/api/data/users': { get: {} },
120
121
  '/api/room/media/realtime/session': { post: {} },
122
+ '/api/room/media/cloudflare_realtimekit/session': { post: {} },
121
123
  },
122
124
  };
123
125
 
@@ -134,6 +136,8 @@ describe('OpenAPI route coverage', () => {
134
136
  .toEqual([{ userBearerAuth: [] }]);
135
137
  expect((normalized.paths?.['/api/room/media/realtime/session'] as Record<string, { security?: unknown }>).post.security)
136
138
  .toEqual([{ userBearerAuth: [] }]);
139
+ expect((normalized.paths?.['/api/room/media/cloudflare_realtimekit/session'] as Record<string, { security?: unknown }>).post.security)
140
+ .toEqual([{ userBearerAuth: [] }]);
137
141
  expect((normalized.paths?.['/api/sql'] as Record<string, { security?: unknown }>).post.security)
138
142
  .toEqual([{ serviceKeyAuth: [] }]);
139
143
  expect((normalized.paths?.['/admin/api/setup'] as Record<string, { security?: unknown }>).get.security)
@@ -10,7 +10,7 @@ import { parsePagination } from '../lib/pagination.js';
10
10
  describe('parsePagination', () => {
11
11
  // ── Defaults ──
12
12
  it('returns defaults when no params provided', () => {
13
- expect(parsePagination(undefined, undefined)).toEqual({ limit: 20, offset: 0 });
13
+ expect(parsePagination(undefined, undefined)).toEqual({ limit: 100, offset: 0 });
14
14
  });
15
15
 
16
16
  // ── Valid values ──
@@ -23,21 +23,25 @@ describe('parsePagination', () => {
23
23
  });
24
24
 
25
25
  // ── Limit clamping ──
26
- it('clamps limit to 100', () => {
27
- expect(parsePagination('999', '0')).toEqual({ limit: 100, offset: 0 });
26
+ it('clamps limit to 1000', () => {
27
+ expect(parsePagination('9999', '0')).toEqual({ limit: 1000, offset: 0 });
28
+ });
29
+
30
+ it('accepts limit within range (500)', () => {
31
+ expect(parsePagination('500', '0')).toEqual({ limit: 500, offset: 0 });
28
32
  });
29
33
 
30
34
  it('rejects limit=0 (falls back to default)', () => {
31
- expect(parsePagination('0', '0')).toEqual({ limit: 20, offset: 0 });
35
+ expect(parsePagination('0', '0')).toEqual({ limit: 100, offset: 0 });
32
36
  });
33
37
 
34
38
  // ── REGRESSION: negative and NaN values ──
35
39
  it('rejects negative limit', () => {
36
- expect(parsePagination('-5', '0')).toEqual({ limit: 20, offset: 0 });
40
+ expect(parsePagination('-5', '0')).toEqual({ limit: 100, offset: 0 });
37
41
  });
38
42
 
39
43
  it('rejects NaN limit', () => {
40
- expect(parsePagination('abc', '0')).toEqual({ limit: 20, offset: 0 });
44
+ expect(parsePagination('abc', '0')).toEqual({ limit: 100, offset: 0 });
41
45
  });
42
46
 
43
47
  it('rejects negative offset', () => {
@@ -49,11 +53,11 @@ describe('parsePagination', () => {
49
53
  });
50
54
 
51
55
  it('rejects Infinity limit', () => {
52
- expect(parsePagination('Infinity', '0')).toEqual({ limit: 20, offset: 0 });
56
+ expect(parsePagination('Infinity', '0')).toEqual({ limit: 100, offset: 0 });
53
57
  });
54
58
 
55
59
  // ── Empty string (same as undefined) ──
56
60
  it('treats empty strings as defaults', () => {
57
- expect(parsePagination('', '')).toEqual({ limit: 20, offset: 0 });
61
+ expect(parsePagination('', '')).toEqual({ limit: 100, offset: 0 });
58
62
  });
59
63
  });
@@ -43,7 +43,7 @@ describe('PostgreSQL dialect — bind params', () => {
43
43
  const { sql, params } = buildListQuery('products', {}, 'postgres');
44
44
  expect(sql).toContain('LIMIT $1');
45
45
  expect(sql).not.toContain('?');
46
- expect(params).toEqual([20]);
46
+ expect(params).toEqual([100]);
47
47
  });
48
48
 
49
49
  it('buildListQuery with offset → $1 OFFSET $2', () => {
@@ -71,7 +71,7 @@ describe('PostgreSQL dialect — bind params', () => {
71
71
  expect(sql).toContain('"status" = $1');
72
72
  expect(sql).toContain('"price" > $2');
73
73
  expect(sql).toContain('LIMIT $3');
74
- expect(params).toEqual(['active', 100, 20]);
74
+ expect(params).toEqual(['active', 100, 100]);
75
75
  });
76
76
 
77
77
  it('buildListQuery cursor after → $1 for cursor, $2 for limit', () => {
@@ -79,10 +79,10 @@ describe('buildListQuery — no filters', () => {
79
79
  expect(sql).toContain('LIMIT');
80
80
  });
81
81
 
82
- it('default limit is 20', () => {
82
+ it('default limit is 100', () => {
83
83
  const { params } = buildListQuery('posts', {});
84
- // params should contain 20 as default limit
85
- expect(params).toContain(20);
84
+ // params should contain 100 as default limit
85
+ expect(params).toContain(100);
86
86
  });
87
87
 
88
88
  it('generates countSql for non-cursor pagination', () => {
@@ -313,9 +313,9 @@ describe('buildSearchQuery', () => {
313
313
  expect(sql).toContain('"posts_fts"');
314
314
  });
315
315
 
316
- it('default limit 20, offset 0', () => {
316
+ it('default limit 100, offset 0', () => {
317
317
  const { params } = buildSearchQuery('posts', 'q');
318
- expect(params[1]).toBe(20);
318
+ expect(params[1]).toBe(100);
319
319
  expect(params[2]).toBeUndefined();
320
320
  });
321
321
 
@@ -560,7 +560,7 @@ describe('3-way sync: QUERY_PARAM_KEYS ↔ Zod queryParamsSchema', () => {
560
560
  describe('buildListQuery — exact params verification', () => {
561
561
  it('no filters → params contains only default limit', () => {
562
562
  const { params } = buildListQuery('t', {});
563
- expect(params).toEqual([20]);
563
+ expect(params).toEqual([100]);
564
564
  });
565
565
 
566
566
  it('no filters → countParams is empty array', () => {
@@ -722,7 +722,7 @@ describe('buildSubstringSearchQuery', () => {
722
722
  const { sql, params } = buildSubstringSearchQuery('posts', '준규', { fields: ['title', 'content'] });
723
723
  expect(sql).toContain('instr(lower(CAST("title" AS TEXT)), lower(?)) > 0');
724
724
  expect(sql).toContain('instr(lower(CAST("content" AS TEXT)), lower(?)) > 0');
725
- expect(params).toEqual(['준규', '준규', 20]);
725
+ expect(params).toEqual(['준규', '준규', 100]);
726
726
  });
727
727
 
728
728
  it('passes the raw term through the SQLite instr() fallback', () => {
@@ -11,7 +11,6 @@ import {
11
11
  getLimit,
12
12
  parseWindow,
13
13
  FixedWindowCounter,
14
- RATE_LIMIT_DEFAULTS,
15
14
  RATE_LIMIT_DEV_DEFAULTS,
16
15
  rateLimitMiddleware,
17
16
  } from '../middleware/rate-limit.js';
@@ -127,4 +127,35 @@ describe('RoomsDO handler context', () => {
127
127
  }),
128
128
  );
129
129
  });
130
+
131
+ it('returns 409 when creating a Cloudflare RealtimeKit session while media is already published', async () => {
132
+ const { RoomsDO } = await import('../durable-objects/rooms-do.js');
133
+
134
+ const room: any = Object.create(RoomsDO.prototype);
135
+ room.readJsonBody = vi.fn().mockResolvedValue({ connectionId: 'conn-1' });
136
+ room.authenticateRealtimeRequest = vi.fn().mockResolvedValue({
137
+ memberId: 'member-1',
138
+ connectionId: 'conn-1',
139
+ meta: {
140
+ authenticated: true,
141
+ connectionId: 'conn-1',
142
+ },
143
+ });
144
+ room.hasPublishedTracks = vi.fn().mockReturnValue(true);
145
+
146
+ const response = await room.handleCloudflareRealtimeKitSessionCreate(
147
+ new Request('http://do/media/cloudflare_realtimekit/session?room=game::room-1', {
148
+ method: 'POST',
149
+ body: JSON.stringify({ connectionId: 'conn-1' }),
150
+ headers: { 'Content-Type': 'application/json' },
151
+ }),
152
+ new URL('http://do/media/cloudflare_realtimekit/session?room=game::room-1'),
153
+ );
154
+
155
+ expect(response.status).toBe(409);
156
+ await expect(response.json()).resolves.toEqual({
157
+ code: 409,
158
+ message: 'Unpublish existing room media before creating a new Cloudflare RealtimeKit session',
159
+ });
160
+ });
130
161
  });
@@ -219,4 +219,52 @@ describe('room route runtime routing', () => {
219
219
  expect(doFetch).toHaveBeenCalledTimes(1);
220
220
  expect(new URL((doFetch.mock.calls[0][0] as Request).url).searchParams.get('room')).toBe('game::room-1');
221
221
  });
222
+
223
+ it('routes room cloudflare realtimekit session requests to the rooms runtime', async () => {
224
+ setConfig(defineConfig({
225
+ rooms: {
226
+ game: {
227
+ runtime: {
228
+ target: 'rooms',
229
+ },
230
+ },
231
+ },
232
+ }));
233
+
234
+ const doFetch = vi.fn(async (request: Request) => new Response(JSON.stringify({
235
+ runtime: 'rooms',
236
+ path: new URL(request.url).pathname,
237
+ auth: request.headers.get('Authorization'),
238
+ body: await request.clone().json(),
239
+ }), {
240
+ headers: { 'Content-Type': 'application/json' },
241
+ status: 201,
242
+ }));
243
+
244
+ const env = createRoomRuntimeEnv();
245
+ env.ROOMS = {
246
+ idFromName: (name: string) => name as unknown as DurableObjectId,
247
+ get: () => ({ fetch: doFetch }),
248
+ } as unknown as DurableObjectNamespace;
249
+
250
+ const app = createAuthedRoomApp();
251
+ const response = await app.request('/api/room/media/cloudflare_realtimekit/session?namespace=game&id=room-1', {
252
+ method: 'POST',
253
+ headers: {
254
+ Authorization: 'Bearer room-token',
255
+ 'Content-Type': 'application/json',
256
+ },
257
+ body: JSON.stringify({ connectionId: 'conn-1' }),
258
+ }, env);
259
+
260
+ expect(response.status).toBe(201);
261
+ await expect(response.json()).resolves.toMatchObject({
262
+ runtime: 'rooms',
263
+ path: '/media/cloudflare_realtimekit/session',
264
+ auth: 'Bearer room-token',
265
+ body: { connectionId: 'conn-1' },
266
+ });
267
+ expect(doFetch).toHaveBeenCalledTimes(1);
268
+ expect(new URL((doFetch.mock.calls[0][0] as Request).url).searchParams.get('room')).toBe('game::room-1');
269
+ });
222
270
  });
@@ -7,10 +7,11 @@
7
7
  * indirectly covered.
8
8
  */
9
9
  import { readFileSync, readdirSync } from 'fs';
10
- import { resolve } from 'path';
10
+ import { basename, relative, resolve } from 'path';
11
+ import { fileURLToPath } from 'url';
11
12
  import { describe, expect, it } from 'vitest';
12
13
 
13
- const SRC_ROOT = resolve(new URL('..', import.meta.url).pathname);
14
+ const SRC_ROOT = resolve(fileURLToPath(new URL('..', import.meta.url)));
14
15
  const PACKAGE_ROOT = resolve(SRC_ROOT, '..');
15
16
  const RUNTIME_DIRS = [
16
17
  resolve(SRC_ROOT, 'routes'),
@@ -21,7 +22,7 @@ const TEST_DIRS = [
21
22
  resolve(SRC_ROOT, '__tests__'),
22
23
  resolve(PACKAGE_ROOT, 'test/integration'),
23
24
  ];
24
- const THIS_TEST_PATH = resolve(new URL(import.meta.url).pathname);
25
+ const THIS_TEST_PATH = resolve(fileURLToPath(import.meta.url));
25
26
 
26
27
  // Files below are exercised indirectly, but the test sources do not mention the
27
28
  // exact filename/stem yet. Tracking them here keeps the gap reviewable.
@@ -60,7 +61,7 @@ function collectFiles(dir: string, predicate: (path: string) => boolean): string
60
61
  function toRuntimeKey(absPath: string): string {
61
62
  for (const dir of RUNTIME_DIRS) {
62
63
  if (absPath.startsWith(dir)) {
63
- return absPath.slice(dir.length + 1).replace(/^/, `${dir.split('/').at(-1)}/`);
64
+ return `${basename(dir)}/${relative(dir, absPath).replace(/\\/g, '/')}`;
64
65
  }
65
66
  }
66
67
  throw new Error(`Unexpected runtime path: ${absPath}`);
@@ -1,5 +1,6 @@
1
1
  import { readFileSync } from 'fs';
2
2
  import { resolve } from 'path';
3
+ import { fileURLToPath } from 'url';
3
4
  import { describe, expect, it } from 'vitest';
4
5
 
5
6
  interface SmokeSkipEntry {
@@ -18,7 +19,7 @@ interface SmokeSkipReport {
18
19
  }
19
20
 
20
21
  const REPORT_PATH = resolve(
21
- new URL('../../test/integration/generated/smoke-skip-report.json', import.meta.url).pathname,
22
+ fileURLToPath(new URL('../../test/integration/generated/smoke-skip-report.json', import.meta.url)),
22
23
  );
23
24
 
24
25
  function readReport(): SmokeSkipReport {
@@ -37,7 +38,7 @@ describe('smoke skip report', () => {
37
38
  {
38
39
  "skippedRouteCount": 0,
39
40
  "summaryByReason": {},
40
- "totalRoutes": 192,
41
+ "totalRoutes": 197,
41
42
  }
42
43
  `);
43
44
  });
@@ -48,7 +48,7 @@ import {
48
48
  type FilterTuple,
49
49
  } from '../lib/query-engine.js';
50
50
  import { summarizeValidationErrors, validateInsert, validateUpdate } from '../lib/validation.js';
51
- import { hookRejectedError, validationError, notFoundError } from '../lib/errors.js';
51
+ import { hookRejectedError, validationError, notFoundError, normalizeDatabaseError } from '../lib/errors.js';
52
52
  import {
53
53
  executeDbTriggers,
54
54
  getRegisteredFunctions,
@@ -470,7 +470,7 @@ export class DatabaseDO extends DurableObject<DOEnv> {
470
470
  if (countSql && countParams) {
471
471
  const countResult = [...this.sql(countSql, ...countParams)];
472
472
  const total = (countResult[0]?.total as number) ?? 0;
473
- const perPage = options.pagination?.perPage ?? options.pagination?.limit ?? 20;
473
+ const perPage = options.pagination?.perPage ?? options.pagination?.limit ?? 100;
474
474
  response.total = total;
475
475
  response.page = options.pagination?.page ?? 1;
476
476
  response.perPage = perPage;
@@ -478,7 +478,7 @@ export class DatabaseDO extends DurableObject<DOEnv> {
478
478
 
479
479
  // Cursor pagination: always include cursor and hasMore when items exist
480
480
  // so clients can start cursor-based pagination from any page (including the first)
481
- const limit = options.pagination?.limit ?? options.pagination?.perPage ?? 20;
481
+ const limit = options.pagination?.limit ?? options.pagination?.perPage ?? 100;
482
482
  const hasMore = normalizedRows.length === limit;
483
483
  response.hasMore = hasMore;
484
484
  if (normalizedRows.length > 0) {
@@ -516,7 +516,7 @@ export class DatabaseDO extends DurableObject<DOEnv> {
516
516
  return c.json({ items: [] });
517
517
  }
518
518
 
519
- const limit = options.pagination?.limit ?? options.pagination?.perPage ?? 20;
519
+ const limit = options.pagination?.limit ?? options.pagination?.perPage ?? 100;
520
520
  const offset = options.pagination?.offset ?? ((options.pagination?.page ?? 1) - 1) * limit;
521
521
 
522
522
  const tableConfig = this.getTableConfig(name);
@@ -1936,6 +1936,11 @@ export class DatabaseDO extends DurableObject<DOEnv> {
1936
1936
  const e = err as { code: number; message: string; data?: unknown };
1937
1937
  return c.json({ code: e.code, message: e.message, data: e.data }, e.code as 200);
1938
1938
  }
1939
+ // Normalize well-known database errors (e.g. UNIQUE constraint violations → 409)
1940
+ const normalizedDbError = normalizeDatabaseError(err);
1941
+ if (normalizedDbError) {
1942
+ return c.json(normalizedDbError.toJSON(), normalizedDbError.code as 400);
1943
+ }
1939
1944
  console.error('DatabaseDO Error:', err);
1940
1945
  return c.json({ code: 500, message: 'Internal server error.' }, 500);
1941
1946
  });