@edge-base/server 0.2.5 → 0.2.7
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.
- package/admin-build/_app/immutable/chunks/{DILS_-VJ.js → B3CvhH3c.js} +1 -1
- package/admin-build/_app/immutable/chunks/BDYewzou.js +1 -0
- package/admin-build/_app/immutable/chunks/{Cdm5zBRA.js → BEM1BeVF.js} +1 -1
- package/admin-build/_app/immutable/chunks/{Dt4vL4Df.js → BYL_uBga.js} +1 -1
- package/admin-build/_app/immutable/chunks/{B94PilAN.js → BYyykAbh.js} +1 -1
- package/admin-build/_app/immutable/chunks/BaUG2TJ-.js +1 -0
- package/admin-build/_app/immutable/chunks/{C72lTcG0.js → Bcs4KYNp.js} +1 -1
- package/admin-build/_app/immutable/chunks/{D2j3I1VQ.js → BfpUQYr3.js} +1 -1
- package/admin-build/_app/immutable/chunks/BhCO1Fpt.js +1 -0
- package/admin-build/_app/immutable/chunks/{B8s_s9QY.js → BkZCgsc3.js} +1 -1
- package/admin-build/_app/immutable/chunks/CIOC1v_q.js +128 -0
- package/admin-build/_app/immutable/chunks/CjcrXziO.js +2 -0
- package/admin-build/_app/immutable/chunks/CvczjTXx.js +1 -0
- package/admin-build/_app/immutable/chunks/D1u3u7xu.js +1 -0
- package/admin-build/_app/immutable/chunks/{B0HRJ657.js → DOOPbWwG.js} +1 -1
- package/admin-build/_app/immutable/chunks/{BqTb6Mxk.js → DaXO-sFP.js} +1 -1
- package/admin-build/_app/immutable/chunks/DnpbvAPi.js +1 -0
- package/admin-build/_app/immutable/chunks/{B6MschND.js → Dz9cUCuv.js} +1 -1
- package/admin-build/_app/immutable/chunks/{CaVKAiCe.js → Tea2dBJ8.js} +1 -1
- package/admin-build/_app/immutable/chunks/{Z41NK6i6.js → bguI1TeA.js} +1 -1
- package/admin-build/_app/immutable/chunks/{J2Gw0SMu.js → ejoEf2I5.js} +1 -1
- package/admin-build/_app/immutable/chunks/{B2TnDKF7.js → iEyeblJR.js} +1 -1
- package/admin-build/_app/immutable/chunks/{_teD5ji5.js → nlAMTi52.js} +1 -1
- package/admin-build/_app/immutable/chunks/qKdzaeX3.js +1 -0
- package/admin-build/_app/immutable/entry/{app.D3flihMw.js → app.DoUaxnew.js} +2 -2
- package/admin-build/_app/immutable/entry/start.MmZh8oBH.js +1 -0
- package/admin-build/_app/immutable/nodes/{0.CdczqZLK.js → 0.Dsxi8s7i.js} +1 -1
- package/admin-build/_app/immutable/nodes/1.Cp2l-hol.js +1 -0
- package/admin-build/_app/immutable/nodes/10.4oY6m8Nz.js +1 -0
- package/admin-build/_app/immutable/nodes/11.DfcozD4J.js +1 -0
- package/admin-build/_app/immutable/nodes/12.uJgZdCIA.js +1 -0
- package/admin-build/_app/immutable/nodes/13.CaN1kRev.js +110 -0
- package/admin-build/_app/immutable/nodes/14.DQ5xIi3s.js +3 -0
- package/admin-build/_app/immutable/nodes/15.B_EkebTJ.js +1 -0
- package/admin-build/_app/immutable/nodes/{16.BR7WwQrS.js → 16.Tko1ZX8-.js} +1 -1
- package/admin-build/_app/immutable/nodes/{17.Cm57KKXV.js → 17.BCmWMJX9.js} +1 -1
- package/admin-build/_app/immutable/nodes/18.hmGhl1O2.js +1 -0
- package/admin-build/_app/immutable/nodes/19.D-1infOo.js +2 -0
- package/admin-build/_app/immutable/nodes/{20.DnHeFlTv.js → 20.CY4KKcBL.js} +1 -1
- package/admin-build/_app/immutable/nodes/21.B9lbNUQr.js +1 -0
- package/admin-build/_app/immutable/nodes/22.14Vd7bnt.js +1 -0
- package/admin-build/_app/immutable/nodes/{23.CWSGMcKJ.js → 23.Be6jK77o.js} +2 -2
- package/admin-build/_app/immutable/nodes/24.CSTFkr6R.js +2 -0
- package/admin-build/_app/immutable/nodes/25.DRTg8fHc.js +2 -0
- package/admin-build/_app/immutable/nodes/26.DKt-9lwQ.js +1 -0
- package/admin-build/_app/immutable/nodes/27.D5caPu0F.js +1 -0
- package/admin-build/_app/immutable/nodes/28.hJhlnlyY.js +1 -0
- package/admin-build/_app/immutable/nodes/29.CDYBzFyT.js +1 -0
- package/admin-build/_app/immutable/nodes/{3.B6q-7qr8.js → 3.DMyKwkGn.js} +1 -1
- package/admin-build/_app/immutable/nodes/30.BaHNeEmc.js +1 -0
- package/admin-build/_app/immutable/nodes/31.C6PV5L-2.js +1 -0
- package/admin-build/_app/immutable/nodes/4.9E118Ftm.js +1 -0
- package/admin-build/_app/immutable/nodes/5.D8guAl3v.js +1 -0
- package/admin-build/_app/immutable/nodes/6.D1u__DtT.js +1 -0
- package/admin-build/_app/immutable/nodes/7.DWXHnRFf.js +1 -0
- package/admin-build/_app/immutable/nodes/8.Dojd8krc.js +1 -0
- package/admin-build/_app/immutable/nodes/9.CLtrr0K_.js +1 -0
- package/admin-build/_app/version.json +1 -1
- package/admin-build/index.html +7 -7
- package/openapi.json +6 -1941
- package/package.json +3 -3
- package/src/__tests__/openapi-coverage.test.ts +0 -6
- package/src/__tests__/push-handlers.test.ts +1 -1
- package/src/__tests__/room-auth-state-loss.test.ts +6 -0
- package/src/__tests__/room-handler-context.test.ts +0 -31
- package/src/__tests__/room-rate-limit-scopes.test.ts +1 -5
- package/src/__tests__/room-runtime-routing.test.ts +24 -111
- package/src/__tests__/route-parser.test.ts +6 -0
- package/src/__tests__/schema.test.ts +15 -6
- package/src/__tests__/smoke-skip-report.test.ts +1 -1
- package/src/durable-objects/database-do.ts +7 -1
- package/src/durable-objects/room-runtime-base.ts +290 -57
- package/src/durable-objects/rooms-do.ts +212 -1336
- package/src/index.ts +23 -9
- package/src/lib/d1-handler.ts +32 -17
- package/src/lib/openapi.ts +1 -4
- package/src/lib/postgres-handler.ts +24 -12
- package/src/lib/route-parser.ts +3 -0
- package/src/lib/schemas.ts +12 -2
- package/src/middleware/captcha-verify.ts +16 -3
- package/src/middleware/error-handler.ts +1 -1
- package/src/middleware/rules.ts +28 -9
- package/src/routes/admin-auth.ts +3 -3
- package/src/routes/admin.ts +13 -8
- package/src/routes/analytics-api.ts +3 -3
- package/src/routes/auth.ts +1 -1
- package/src/routes/backup.ts +1 -1
- package/src/routes/d1.ts +14 -7
- package/src/routes/database-live.ts +13 -6
- package/src/routes/kv.ts +21 -10
- package/src/routes/oauth.ts +1 -1
- package/src/routes/push.ts +119 -77
- package/src/routes/room.ts +203 -280
- package/src/routes/schema-endpoint.ts +2 -2
- package/src/routes/sql.ts +10 -6
- package/src/routes/storage.ts +4 -2
- package/src/routes/vectorize.ts +16 -4
- package/src/types.ts +1 -14
- package/admin-build/_app/immutable/chunks/6oMK_164.js +0 -1
- package/admin-build/_app/immutable/chunks/BEW7Ez_g.js +0 -1
- package/admin-build/_app/immutable/chunks/BoOooyH6.js +0 -1
- package/admin-build/_app/immutable/chunks/BvHnF5tV.js +0 -1
- package/admin-build/_app/immutable/chunks/CoI6jjbg.js +0 -2
- package/admin-build/_app/immutable/chunks/CrOZMmdF.js +0 -1
- package/admin-build/_app/immutable/chunks/Cw6OYcq-.js +0 -1
- package/admin-build/_app/immutable/chunks/DPdQ7z0T.js +0 -128
- package/admin-build/_app/immutable/chunks/pUxw8jfq.js +0 -1
- package/admin-build/_app/immutable/entry/start.Cl6sLxnz.js +0 -1
- package/admin-build/_app/immutable/nodes/1.DxcSsEqS.js +0 -1
- package/admin-build/_app/immutable/nodes/10.DuAd4aIm.js +0 -1
- package/admin-build/_app/immutable/nodes/11.0jgHQL92.js +0 -1
- package/admin-build/_app/immutable/nodes/12.CKNPqmyy.js +0 -1
- package/admin-build/_app/immutable/nodes/13.B1p2POXS.js +0 -110
- package/admin-build/_app/immutable/nodes/14.Bb-REBND.js +0 -3
- package/admin-build/_app/immutable/nodes/15.1uBFCX0X.js +0 -1
- package/admin-build/_app/immutable/nodes/18.CoiwfAuQ.js +0 -1
- package/admin-build/_app/immutable/nodes/19.B8ZdLlXj.js +0 -2
- package/admin-build/_app/immutable/nodes/21.CJFaf0Ia.js +0 -1
- package/admin-build/_app/immutable/nodes/22.CItETFzy.js +0 -1
- package/admin-build/_app/immutable/nodes/24.CWbEqNMB.js +0 -2
- package/admin-build/_app/immutable/nodes/25.DRkLEhKi.js +0 -2
- package/admin-build/_app/immutable/nodes/26.BRxO8AYH.js +0 -1
- package/admin-build/_app/immutable/nodes/27.BLs-nVHz.js +0 -1
- package/admin-build/_app/immutable/nodes/28.G79qkdBK.js +0 -1
- package/admin-build/_app/immutable/nodes/29.BOcI6g0N.js +0 -1
- package/admin-build/_app/immutable/nodes/30.DAIC7dKd.js +0 -1
- package/admin-build/_app/immutable/nodes/31.pl0XXjXF.js +0 -1
- package/admin-build/_app/immutable/nodes/4.DOdvVlZj.js +0 -1
- package/admin-build/_app/immutable/nodes/5.BW_zlgye.js +0 -1
- package/admin-build/_app/immutable/nodes/6.Dxy1CAI2.js +0 -1
- package/admin-build/_app/immutable/nodes/7.BG98w_o7.js +0 -1
- package/admin-build/_app/immutable/nodes/8.DoG5R2rG.js +0 -1
- package/admin-build/_app/immutable/nodes/9.Dmxf6zAC.js +0 -1
- package/src/__tests__/cloudflare-realtime.test.ts +0 -113
- package/src/lib/cloudflare-realtime.ts +0 -251
package/src/index.ts
CHANGED
|
@@ -12,6 +12,14 @@ export { LogsDO } from './durable-objects/logs-do.js';
|
|
|
12
12
|
|
|
13
13
|
let appPromise: Promise<Awaited<ReturnType<typeof buildApp>>> | null = null;
|
|
14
14
|
|
|
15
|
+
function assetUnavailableMessage(
|
|
16
|
+
assetName: 'admin dashboard' | 'harness assets',
|
|
17
|
+
): string {
|
|
18
|
+
const label = `${assetName[0].toUpperCase()}${assetName.slice(1)}`;
|
|
19
|
+
const verb = assetName === 'admin dashboard' ? 'is' : 'are';
|
|
20
|
+
return `${label} ${verb} not deployed for this worker. Deploy the assets bundle or configure ADMIN_ORIGIN if they are hosted elsewhere.`;
|
|
21
|
+
}
|
|
22
|
+
|
|
15
23
|
async function buildApp() {
|
|
16
24
|
await ensureServerStartup();
|
|
17
25
|
|
|
@@ -179,7 +187,7 @@ async function buildApp() {
|
|
|
179
187
|
}
|
|
180
188
|
|
|
181
189
|
if (!env.ASSETS) {
|
|
182
|
-
return c.json({ code: 404, message: '
|
|
190
|
+
return c.json({ code: 404, message: assetUnavailableMessage('admin dashboard') }, 404);
|
|
183
191
|
}
|
|
184
192
|
|
|
185
193
|
const url = new URL(c.req.url);
|
|
@@ -198,7 +206,7 @@ async function buildApp() {
|
|
|
198
206
|
return env.ASSETS.fetch(c.req.raw);
|
|
199
207
|
}
|
|
200
208
|
|
|
201
|
-
return c.json({ code: 404, message: '
|
|
209
|
+
return c.json({ code: 404, message: assetUnavailableMessage('admin dashboard') }, 404);
|
|
202
210
|
});
|
|
203
211
|
|
|
204
212
|
app.get('/_app/*', async (c) => {
|
|
@@ -207,7 +215,7 @@ async function buildApp() {
|
|
|
207
215
|
return env.ASSETS.fetch(c.req.raw);
|
|
208
216
|
}
|
|
209
217
|
|
|
210
|
-
return c.json({ code: 404, message: '
|
|
218
|
+
return c.json({ code: 404, message: assetUnavailableMessage('admin dashboard') }, 404);
|
|
211
219
|
});
|
|
212
220
|
|
|
213
221
|
app.get('/admin/*', async (c) => {
|
|
@@ -219,7 +227,7 @@ async function buildApp() {
|
|
|
219
227
|
if (env.ASSETS) {
|
|
220
228
|
return env.ASSETS.fetch(createAdminAssetRequest(c.req.raw));
|
|
221
229
|
}
|
|
222
|
-
return c.json({ code: 404, message: '
|
|
230
|
+
return c.json({ code: 404, message: assetUnavailableMessage('admin dashboard') }, 404);
|
|
223
231
|
});
|
|
224
232
|
|
|
225
233
|
app.get('/admin', async (c) => {
|
|
@@ -231,7 +239,7 @@ async function buildApp() {
|
|
|
231
239
|
if (env.ASSETS) {
|
|
232
240
|
return env.ASSETS.fetch(createAdminAssetRequest(c.req.raw));
|
|
233
241
|
}
|
|
234
|
-
return c.json({ code: 404, message: '
|
|
242
|
+
return c.json({ code: 404, message: assetUnavailableMessage('admin dashboard') }, 404);
|
|
235
243
|
});
|
|
236
244
|
|
|
237
245
|
app.get('/harness', (c) => {
|
|
@@ -243,7 +251,7 @@ async function buildApp() {
|
|
|
243
251
|
if (env.ASSETS) {
|
|
244
252
|
return env.ASSETS.fetch(c.req.raw);
|
|
245
253
|
}
|
|
246
|
-
return c.json({ code: 404, message: '
|
|
254
|
+
return c.json({ code: 404, message: assetUnavailableMessage('harness assets') }, 404);
|
|
247
255
|
});
|
|
248
256
|
|
|
249
257
|
app.get('/harness/assets/*', async (c) => {
|
|
@@ -251,7 +259,7 @@ async function buildApp() {
|
|
|
251
259
|
if (env.ASSETS) {
|
|
252
260
|
return env.ASSETS.fetch(c.req.raw);
|
|
253
261
|
}
|
|
254
|
-
return c.json({ code: 404, message: '
|
|
262
|
+
return c.json({ code: 404, message: assetUnavailableMessage('harness assets') }, 404);
|
|
255
263
|
});
|
|
256
264
|
|
|
257
265
|
app.get('/harness/*', (c) => {
|
|
@@ -268,7 +276,10 @@ async function buildApp() {
|
|
|
268
276
|
});
|
|
269
277
|
|
|
270
278
|
app.notFound((c) => {
|
|
271
|
-
return c.json({
|
|
279
|
+
return c.json({
|
|
280
|
+
code: 404,
|
|
281
|
+
message: `Path '${new URL(c.req.url).pathname}' was not found on this EdgeBase server.`,
|
|
282
|
+
}, 404);
|
|
272
283
|
});
|
|
273
284
|
|
|
274
285
|
app.onError((err, c) => {
|
|
@@ -302,7 +313,10 @@ async function buildApp() {
|
|
|
302
313
|
return c.json(body, e.code as number as 400);
|
|
303
314
|
}
|
|
304
315
|
console.error('Unhandled error:', err);
|
|
305
|
-
return c.json({
|
|
316
|
+
return c.json({
|
|
317
|
+
code: 500,
|
|
318
|
+
message: `Internal server error while handling '${new URL(c.req.url).pathname}'. Check the worker logs for the original exception.`,
|
|
319
|
+
}, 500);
|
|
306
320
|
});
|
|
307
321
|
|
|
308
322
|
return app;
|
package/src/lib/d1-handler.ts
CHANGED
|
@@ -126,6 +126,21 @@ export async function handleD1Request(
|
|
|
126
126
|
return c.json({ code: 405, message: 'Method not allowed' }, 405);
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
+
function invalidD1BodyMessage(context: string): string {
|
|
130
|
+
return `Invalid JSON body for ${context}. Send application/json with the expected fields.`;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function d1RuleRejectedMessage(
|
|
134
|
+
tableName: string,
|
|
135
|
+
action: 'read' | 'insert' | 'delete' | 'list' | 'count' | 'search',
|
|
136
|
+
id?: string,
|
|
137
|
+
): string {
|
|
138
|
+
if (id) {
|
|
139
|
+
return `Access denied. The '${action}' access rule for table '${tableName}' rejected record '${id}'.`;
|
|
140
|
+
}
|
|
141
|
+
return `Access denied. The '${action}' access rule for table '${tableName}' rejected this request.`;
|
|
142
|
+
}
|
|
143
|
+
|
|
129
144
|
// ─── D1 Binding Resolution ───
|
|
130
145
|
|
|
131
146
|
function resolveD1Binding(env: Env, namespace: string): D1ResolvedDb {
|
|
@@ -395,7 +410,7 @@ async function handleList(
|
|
|
395
410
|
): Promise<Response> {
|
|
396
411
|
const tableAccess = getTableAccess(tableConfig);
|
|
397
412
|
if (!isServiceKey && tableAccess?.read === false) {
|
|
398
|
-
const error = forbiddenError(
|
|
413
|
+
const error = forbiddenError(d1RuleRejectedMessage(tableName, 'list'));
|
|
399
414
|
return c.json(error.toJSON(), error.status as 403);
|
|
400
415
|
}
|
|
401
416
|
|
|
@@ -478,7 +493,7 @@ async function handleCount(
|
|
|
478
493
|
): Promise<Response> {
|
|
479
494
|
const tableAccess = getTableAccess(tableConfig);
|
|
480
495
|
if (!isServiceKey && tableAccess?.read === false) {
|
|
481
|
-
const error = forbiddenError(
|
|
496
|
+
const error = forbiddenError(d1RuleRejectedMessage(tableName, 'count'));
|
|
482
497
|
return c.json(error.toJSON(), error.status as 403);
|
|
483
498
|
}
|
|
484
499
|
|
|
@@ -501,7 +516,7 @@ async function handleSearch(
|
|
|
501
516
|
): Promise<Response> {
|
|
502
517
|
const tableAccess = getTableAccess(tableConfig);
|
|
503
518
|
if (!isServiceKey && tableAccess?.read === false) {
|
|
504
|
-
const error = forbiddenError(
|
|
519
|
+
const error = forbiddenError(d1RuleRejectedMessage(tableName, 'search'));
|
|
505
520
|
return c.json(error.toJSON(), error.status as 403);
|
|
506
521
|
}
|
|
507
522
|
|
|
@@ -595,7 +610,7 @@ async function handleGet(
|
|
|
595
610
|
const tableHooks = getTableHooks(tableConfig);
|
|
596
611
|
if (!isServiceKey && tableAccess?.read !== undefined) {
|
|
597
612
|
if (!(await evalRowRule(tableAccess.read, auth, row))) {
|
|
598
|
-
return c.json({ code: 403, message:
|
|
613
|
+
return c.json({ code: 403, message: d1RuleRejectedMessage(tableName, 'read', id) }, 403);
|
|
599
614
|
}
|
|
600
615
|
}
|
|
601
616
|
|
|
@@ -627,7 +642,7 @@ async function handleInsert(
|
|
|
627
642
|
try {
|
|
628
643
|
body = await c.req.json();
|
|
629
644
|
} catch {
|
|
630
|
-
return c.json({ code: 400, message:
|
|
645
|
+
return c.json({ code: 400, message: invalidD1BodyMessage(`inserting into table '${tableName}'`) }, 400);
|
|
631
646
|
}
|
|
632
647
|
body = applySchemaFieldAliases(body, tableConfig.schema);
|
|
633
648
|
|
|
@@ -636,7 +651,7 @@ async function handleInsert(
|
|
|
636
651
|
const tableHooks = getTableHooks(tableConfig);
|
|
637
652
|
if (!isServiceKey && tableAccess?.insert !== undefined) {
|
|
638
653
|
if (!(await evalInsertRule(tableAccess.insert, auth))) {
|
|
639
|
-
return c.json({ code: 403, message:
|
|
654
|
+
return c.json({ code: 403, message: d1RuleRejectedMessage(tableName, 'insert') }, 403);
|
|
640
655
|
}
|
|
641
656
|
}
|
|
642
657
|
|
|
@@ -798,14 +813,14 @@ async function handleUpdate(
|
|
|
798
813
|
try {
|
|
799
814
|
body = await c.req.json();
|
|
800
815
|
} catch {
|
|
801
|
-
return c.json({ code: 400, message:
|
|
816
|
+
return c.json({ code: 400, message: invalidD1BodyMessage(`updating table '${tableName}'`) }, 400);
|
|
802
817
|
}
|
|
803
818
|
body = applySchemaFieldAliases(body, tableConfig.schema);
|
|
804
819
|
|
|
805
820
|
// Validate against schema
|
|
806
821
|
const validation = validateUpdate(body, tableConfig.schema);
|
|
807
822
|
if (!validation.valid) {
|
|
808
|
-
return c.json({ code: 400, message: '
|
|
823
|
+
return c.json({ code: 400, message: `Update payload for table '${tableName}' failed validation. See data for field-level errors.`, data: Object.fromEntries(Object.entries(validation.errors).map(([k, v]) => [k, { code: 'invalid', message: v }])) }, 400);
|
|
809
824
|
}
|
|
810
825
|
|
|
811
826
|
// Fetch existing record to check rules
|
|
@@ -946,7 +961,7 @@ async function handleDelete(
|
|
|
946
961
|
const tableHooks = getTableHooks(tableConfig);
|
|
947
962
|
if (!isServiceKey && tableAccess?.delete !== undefined) {
|
|
948
963
|
if (!(await evalRowRule(tableAccess.delete, auth, existingRow))) {
|
|
949
|
-
return c.json({ code: 403, message: '
|
|
964
|
+
return c.json({ code: 403, message: d1RuleRejectedMessage(tableName, 'delete', id) }, 403);
|
|
950
965
|
}
|
|
951
966
|
}
|
|
952
967
|
|
|
@@ -1016,7 +1031,7 @@ async function handleBatch(
|
|
|
1016
1031
|
try {
|
|
1017
1032
|
body = await c.req.json();
|
|
1018
1033
|
} catch {
|
|
1019
|
-
return c.json({ code: 400, message:
|
|
1034
|
+
return c.json({ code: 400, message: invalidD1BodyMessage(`batch operations on table '${tableName}'`) }, 400);
|
|
1020
1035
|
}
|
|
1021
1036
|
|
|
1022
1037
|
// Batch size limit: 500 total ops
|
|
@@ -1030,7 +1045,7 @@ async function handleBatch(
|
|
|
1030
1045
|
const tableAccess = getTableAccess(tableConfig);
|
|
1031
1046
|
if (!isServiceKey && body.inserts?.length && tableAccess?.insert !== undefined) {
|
|
1032
1047
|
if (!(await evalInsertRule(tableAccess.insert, auth))) {
|
|
1033
|
-
return c.json({ code: 403, message:
|
|
1048
|
+
return c.json({ code: 403, message: d1RuleRejectedMessage(tableName, 'insert') }, 403);
|
|
1034
1049
|
}
|
|
1035
1050
|
}
|
|
1036
1051
|
|
|
@@ -1059,7 +1074,7 @@ async function handleBatch(
|
|
|
1059
1074
|
for (const item of body.inserts) {
|
|
1060
1075
|
const validation = validateInsert(item, tableConfig.schema);
|
|
1061
1076
|
if (!validation.valid) {
|
|
1062
|
-
return c.json({ code: 400, message:
|
|
1077
|
+
return c.json({ code: 400, message: `Batch insert payload for table '${tableName}' failed validation. See data for field-level errors.`, data: Object.fromEntries(Object.entries(validation.errors).map(([k, v]) => [k, { code: 'invalid', message: v }])) }, 400);
|
|
1063
1078
|
}
|
|
1064
1079
|
}
|
|
1065
1080
|
}
|
|
@@ -1072,14 +1087,14 @@ async function handleBatch(
|
|
|
1072
1087
|
}));
|
|
1073
1088
|
for (const entry of body.updates) {
|
|
1074
1089
|
if (!entry.id) {
|
|
1075
|
-
return c.json({ code: 400, message:
|
|
1090
|
+
return c.json({ code: 400, message: `Each batch update entry for table '${tableName}' must include an id.` }, 400);
|
|
1076
1091
|
}
|
|
1077
1092
|
if (!entry.data || typeof entry.data !== 'object') {
|
|
1078
|
-
return c.json({ code: 400, message:
|
|
1093
|
+
return c.json({ code: 400, message: `Each batch update entry for table '${tableName}' must include a data object.` }, 400);
|
|
1079
1094
|
}
|
|
1080
1095
|
const validation = validateUpdate(entry.data, tableConfig.schema);
|
|
1081
1096
|
if (!validation.valid) {
|
|
1082
|
-
return c.json({ code: 400, message:
|
|
1097
|
+
return c.json({ code: 400, message: `Batch update payload for table '${tableName}' failed validation. See data for field-level errors.`, data: Object.fromEntries(Object.entries(validation.errors).map(([k, v]) => [k, { code: 'invalid', message: v }])) }, 400);
|
|
1083
1098
|
}
|
|
1084
1099
|
}
|
|
1085
1100
|
}
|
|
@@ -1087,7 +1102,7 @@ async function handleBatch(
|
|
|
1087
1102
|
// Check delete rules (table-level)
|
|
1088
1103
|
if (!isServiceKey && body.deletes?.length && tableAccess?.delete !== undefined) {
|
|
1089
1104
|
if (!(await evalRowRule(tableAccess.delete, auth, {}))) {
|
|
1090
|
-
return c.json({ code: 403, message:
|
|
1105
|
+
return c.json({ code: 403, message: d1RuleRejectedMessage(tableName, 'delete') }, 403);
|
|
1091
1106
|
}
|
|
1092
1107
|
}
|
|
1093
1108
|
|
|
@@ -1249,7 +1264,7 @@ async function handleBatchByFilter(
|
|
|
1249
1264
|
try {
|
|
1250
1265
|
body = await c.req.json();
|
|
1251
1266
|
} catch {
|
|
1252
|
-
return c.json({ code: 400, message:
|
|
1267
|
+
return c.json({ code: 400, message: invalidD1BodyMessage(`batch-by-filter on table '${tableName}'`) }, 400);
|
|
1253
1268
|
}
|
|
1254
1269
|
|
|
1255
1270
|
if (!body.action || !['delete', 'update'].includes(body.action)) {
|
package/src/lib/openapi.ts
CHANGED
|
@@ -47,10 +47,7 @@ const USER_BEARER_PATHS = new Set([
|
|
|
47
47
|
'/api/push/topic/unsubscribe',
|
|
48
48
|
]);
|
|
49
49
|
|
|
50
|
-
const USER_BEARER_PREFIXES = [
|
|
51
|
-
'/api/room/media/realtime/',
|
|
52
|
-
'/api/room/media/cloudflare_realtimekit/',
|
|
53
|
-
];
|
|
50
|
+
const USER_BEARER_PREFIXES: string[] = [];
|
|
54
51
|
|
|
55
52
|
const SERVICE_KEY_ONLY_PATHS = new Set([
|
|
56
53
|
'/api/db/broadcast',
|
|
@@ -61,6 +61,18 @@ interface PgResolvedDb {
|
|
|
61
61
|
namespace: string;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
function postgresInvalidJsonMessage(context: string, tableName: string): string {
|
|
65
|
+
return `Invalid JSON body for ${context} on table '${tableName}'. Send application/json with the expected fields.`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function postgresRuleRejectedMessage(tableName: string, action: 'insert' | 'update' | 'delete'): string {
|
|
69
|
+
return `Access denied by access rules for ${action} on table '${tableName}'.`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function postgresValidationMessage(context: string, tableName: string): string {
|
|
73
|
+
return `Validation failed for ${context} on table '${tableName}'. See data for field-level errors.`;
|
|
74
|
+
}
|
|
75
|
+
|
|
64
76
|
// ─── Main Handler ───
|
|
65
77
|
|
|
66
78
|
/**
|
|
@@ -526,7 +538,7 @@ async function handleInsert(
|
|
|
526
538
|
try {
|
|
527
539
|
body = await c.req.json();
|
|
528
540
|
} catch {
|
|
529
|
-
return c.json({ code: 400, message: '
|
|
541
|
+
return c.json({ code: 400, message: postgresInvalidJsonMessage('insert', tableName) }, 400);
|
|
530
542
|
}
|
|
531
543
|
|
|
532
544
|
// Check insert rule
|
|
@@ -534,7 +546,7 @@ async function handleInsert(
|
|
|
534
546
|
const tableHooks = getTableHooks(tableConfig);
|
|
535
547
|
if (!isServiceKey && tableAccess?.insert !== undefined) {
|
|
536
548
|
if (!(await evalInsertRule(tableAccess.insert, auth))) {
|
|
537
|
-
return c.json({ code: 403, message:
|
|
549
|
+
return c.json({ code: 403, message: postgresRuleRejectedMessage(tableName, 'insert') }, 403);
|
|
538
550
|
}
|
|
539
551
|
}
|
|
540
552
|
|
|
@@ -663,7 +675,7 @@ async function handleUpdate(
|
|
|
663
675
|
try {
|
|
664
676
|
body = await c.req.json();
|
|
665
677
|
} catch {
|
|
666
|
-
return c.json({ code: 400, message:
|
|
678
|
+
return c.json({ code: 400, message: postgresInvalidJsonMessage(`update record '${id}'`, tableName) }, 400);
|
|
667
679
|
}
|
|
668
680
|
|
|
669
681
|
// Validate against schema
|
|
@@ -671,7 +683,7 @@ async function handleUpdate(
|
|
|
671
683
|
if (!validation.valid) {
|
|
672
684
|
return c.json({
|
|
673
685
|
code: 400,
|
|
674
|
-
message: '
|
|
686
|
+
message: postgresValidationMessage(`update record '${id}'`, tableName),
|
|
675
687
|
data: toFieldErrorData(validation.errors),
|
|
676
688
|
errors: validation.errors,
|
|
677
689
|
}, 400);
|
|
@@ -690,7 +702,7 @@ async function handleUpdate(
|
|
|
690
702
|
const tableHooks = getTableHooks(tableConfig);
|
|
691
703
|
if (!isServiceKey && tableAccess?.update !== undefined) {
|
|
692
704
|
if (!(await evalRowRule(tableAccess.update, auth, existingRow))) {
|
|
693
|
-
return c.json({ code: 403, message:
|
|
705
|
+
return c.json({ code: 403, message: postgresRuleRejectedMessage(tableName, 'update') }, 403);
|
|
694
706
|
}
|
|
695
707
|
}
|
|
696
708
|
|
|
@@ -792,7 +804,7 @@ async function handleDelete(
|
|
|
792
804
|
const tableHooks = getTableHooks(tableConfig);
|
|
793
805
|
if (!isServiceKey && tableAccess?.delete !== undefined) {
|
|
794
806
|
if (!(await evalRowRule(tableAccess.delete, auth, existingRow))) {
|
|
795
|
-
return c.json({ code: 403, message:
|
|
807
|
+
return c.json({ code: 403, message: postgresRuleRejectedMessage(tableName, 'delete') }, 403);
|
|
796
808
|
}
|
|
797
809
|
}
|
|
798
810
|
|
|
@@ -870,7 +882,7 @@ async function handleBatch(
|
|
|
870
882
|
try {
|
|
871
883
|
body = await c.req.json();
|
|
872
884
|
} catch {
|
|
873
|
-
return c.json({ code: 400, message: '
|
|
885
|
+
return c.json({ code: 400, message: postgresInvalidJsonMessage('batch insert', tableName) }, 400);
|
|
874
886
|
}
|
|
875
887
|
|
|
876
888
|
const inserts = Array.isArray(body.inserts)
|
|
@@ -901,7 +913,7 @@ async function handleBatch(
|
|
|
901
913
|
const tableAccess = getTableAccess(tableConfig);
|
|
902
914
|
if (!isServiceKey && inserts.length > 0 && tableAccess?.insert !== undefined) {
|
|
903
915
|
if (!(await evalInsertRule(tableAccess.insert, auth))) {
|
|
904
|
-
return c.json({ code: 403, message:
|
|
916
|
+
return c.json({ code: 403, message: postgresRuleRejectedMessage(tableName, 'insert') }, 403);
|
|
905
917
|
}
|
|
906
918
|
}
|
|
907
919
|
|
|
@@ -915,7 +927,7 @@ async function handleBatch(
|
|
|
915
927
|
if (!validation.valid) {
|
|
916
928
|
return c.json({
|
|
917
929
|
code: 400,
|
|
918
|
-
message: '
|
|
930
|
+
message: postgresValidationMessage('batch insert', tableName),
|
|
919
931
|
data: toFieldErrorData(validation.errors),
|
|
920
932
|
errors: validation.errors,
|
|
921
933
|
}, 400);
|
|
@@ -1000,7 +1012,7 @@ async function handleBatchByFilter(
|
|
|
1000
1012
|
try {
|
|
1001
1013
|
body = await c.req.json();
|
|
1002
1014
|
} catch {
|
|
1003
|
-
return c.json({ code: 400, message: '
|
|
1015
|
+
return c.json({ code: 400, message: postgresInvalidJsonMessage('batch-by-filter', tableName) }, 400);
|
|
1004
1016
|
}
|
|
1005
1017
|
|
|
1006
1018
|
if (!body.action || !['delete', 'update'].includes(body.action)) {
|
|
@@ -1038,7 +1050,7 @@ async function handleBatchByFilter(
|
|
|
1038
1050
|
const tableAccess = getTableAccess(tableConfig);
|
|
1039
1051
|
if (!isServiceKey && tableAccess?.delete !== undefined) {
|
|
1040
1052
|
if (typeof tableAccess.delete === 'boolean' && !tableAccess.delete) {
|
|
1041
|
-
return c.json({ code: 403, message:
|
|
1053
|
+
return c.json({ code: 403, message: postgresRuleRejectedMessage(tableName, 'delete') }, 403);
|
|
1042
1054
|
}
|
|
1043
1055
|
}
|
|
1044
1056
|
|
|
@@ -1071,7 +1083,7 @@ async function handleBatchByFilter(
|
|
|
1071
1083
|
const tableAccess = getTableAccess(tableConfig);
|
|
1072
1084
|
if (!isServiceKey && tableAccess?.update !== undefined) {
|
|
1073
1085
|
if (typeof tableAccess.update === 'boolean' && !tableAccess.update) {
|
|
1074
|
-
return c.json({ code: 403, message:
|
|
1086
|
+
return c.json({ code: 403, message: postgresRuleRejectedMessage(tableName, 'update') }, 403);
|
|
1075
1087
|
}
|
|
1076
1088
|
}
|
|
1077
1089
|
|
package/src/lib/route-parser.ts
CHANGED
|
@@ -360,6 +360,9 @@ export function parseRoute(method: string, path: string): ParsedRoute {
|
|
|
360
360
|
if (segments[2] === 'metadata') {
|
|
361
361
|
result.subcategory = 'metadata';
|
|
362
362
|
result.operation = 'getMetadata';
|
|
363
|
+
} else if (segments[2] === 'summary') {
|
|
364
|
+
result.subcategory = 'summary';
|
|
365
|
+
result.operation = 'getSummary';
|
|
363
366
|
} else if (segments[2] === 'connect-check') {
|
|
364
367
|
result.subcategory = 'connect-check';
|
|
365
368
|
result.operation = 'connectCheck';
|
package/src/lib/schemas.ts
CHANGED
|
@@ -139,13 +139,23 @@ export const trackEventsBodySchema = z.object({
|
|
|
139
139
|
* Returns Zod validation errors in the standard { code, message } format.
|
|
140
140
|
*/
|
|
141
141
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
142
|
-
|
|
142
|
+
function formatZodIssue(issue: { message?: string; path?: Array<string | number> }): string {
|
|
143
|
+
const message = typeof issue.message === 'string' ? issue.message : 'Invalid value';
|
|
144
|
+
const path = Array.isArray(issue.path) && issue.path.length > 0
|
|
145
|
+
? issue.path.map((segment) => typeof segment === 'number' ? `[${segment}]` : String(segment)).join('.').replace(/\.\[/g, '[')
|
|
146
|
+
: '';
|
|
147
|
+
return path ? `${path}: ${message}` : message;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function zodDefaultHook(result: { success: boolean; error?: { issues?: Array<{ message: string; path?: Array<string | number> }>; errors?: Array<{ message: string; path?: Array<string | number> }> } }, c: any) {
|
|
143
151
|
if (!result.success) {
|
|
144
152
|
// Zod v4 uses `issues`, Zod v3 uses `errors`
|
|
145
153
|
const items = (result.error as any)?.issues ?? (result.error as any)?.errors ?? [];
|
|
146
154
|
return c.json({
|
|
147
155
|
code: 400,
|
|
148
|
-
message: items.
|
|
156
|
+
message: items.length > 0
|
|
157
|
+
? items.map((e: any) => formatZodIssue(e)).join(', ')
|
|
158
|
+
: 'Request validation failed.',
|
|
149
159
|
}, 400);
|
|
150
160
|
}
|
|
151
161
|
}
|
|
@@ -18,6 +18,7 @@ interface CaptchaConfig {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
type HonoContext = Context<{ Bindings: Env }>;
|
|
21
|
+
const captchaWarnings = new Set<string>();
|
|
21
22
|
|
|
22
23
|
interface SiteverifyResponse {
|
|
23
24
|
success: boolean;
|
|
@@ -139,7 +140,14 @@ export function captchaMiddleware(expectedAction: string) {
|
|
|
139
140
|
try {
|
|
140
141
|
const config = parseConfig(c.env);
|
|
141
142
|
if (config?.captcha === true) {
|
|
142
|
-
|
|
143
|
+
if (!captchaWarnings.has('missing-turnstile-keys')) {
|
|
144
|
+
captchaWarnings.add('missing-turnstile-keys');
|
|
145
|
+
console.warn(
|
|
146
|
+
'[Auth] CAPTCHA is enabled, but Turnstile keys are missing. '
|
|
147
|
+
+ 'Requests will continue without CAPTCHA in local dev. '
|
|
148
|
+
+ 'Add captchaSiteKey and TURNSTILE_SECRET, or set captcha: false to silence this warning.',
|
|
149
|
+
);
|
|
150
|
+
}
|
|
143
151
|
}
|
|
144
152
|
} catch { /* ignore */ }
|
|
145
153
|
await next();
|
|
@@ -170,7 +178,13 @@ export function captchaMiddleware(expectedAction: string) {
|
|
|
170
178
|
// Handle siteverify API failure (timeout, network error)
|
|
171
179
|
if (result['error-codes']?.includes('timeout-or-network-error')) {
|
|
172
180
|
if (failMode === 'open') {
|
|
173
|
-
|
|
181
|
+
if (!captchaWarnings.has('siteverify-fail-open')) {
|
|
182
|
+
captchaWarnings.add('siteverify-fail-open');
|
|
183
|
+
console.warn(
|
|
184
|
+
'[Auth] Turnstile siteverify failed because of a timeout or network error. '
|
|
185
|
+
+ 'The request is being allowed because captcha.failMode is set to "open".',
|
|
186
|
+
);
|
|
187
|
+
}
|
|
174
188
|
await next();
|
|
175
189
|
return;
|
|
176
190
|
}
|
|
@@ -214,4 +228,3 @@ export const _test = {
|
|
|
214
228
|
hasServiceKey,
|
|
215
229
|
siteverify,
|
|
216
230
|
};
|
|
217
|
-
|
|
@@ -45,7 +45,7 @@ export const errorHandlerMiddleware: MiddlewareHandler<HonoEnv> = async (c, next
|
|
|
45
45
|
return c.json(
|
|
46
46
|
{
|
|
47
47
|
code: 500,
|
|
48
|
-
message:
|
|
48
|
+
message: `Internal server error while handling '${c.req.path}'. Check the worker logs for the original exception.`,
|
|
49
49
|
slug: 'internal-error',
|
|
50
50
|
},
|
|
51
51
|
500,
|
package/src/middleware/rules.ts
CHANGED
|
@@ -41,6 +41,13 @@ type HonoContext = Context<{ Bindings: Env }>;
|
|
|
41
41
|
const WORKER_RULE_TIMEOUT_MS = 50;
|
|
42
42
|
const DB_ACCESS_RULE_TIMEOUT_MS = 2000;
|
|
43
43
|
|
|
44
|
+
function tableRuleRejected(tableName: string, action: string): EdgeBaseError {
|
|
45
|
+
return new EdgeBaseError(
|
|
46
|
+
403,
|
|
47
|
+
`Access denied. The '${action}' access rule for table '${tableName}' rejected this request.`,
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
44
51
|
/**
|
|
45
52
|
* Normalize a raw rule value (function | boolean | string) into a callable.
|
|
46
53
|
*
|
|
@@ -174,7 +181,7 @@ export async function rulesMiddleware(c: HonoContext, next: Next): Promise<Respo
|
|
|
174
181
|
return next();
|
|
175
182
|
}
|
|
176
183
|
if (keyResult === 'invalid') {
|
|
177
|
-
throw new EdgeBaseError(401,
|
|
184
|
+
throw new EdgeBaseError(401, `Invalid X-EdgeBase-Service-Key for scope '${requiredScope}'.`);
|
|
178
185
|
}
|
|
179
186
|
// keyResult === 'missing' → continue to normal rules evaluation
|
|
180
187
|
|
|
@@ -182,7 +189,10 @@ export async function rulesMiddleware(c: HonoContext, next: Next): Promise<Respo
|
|
|
182
189
|
if (!config.databases) {
|
|
183
190
|
// No databases config → release mode check
|
|
184
191
|
if (!config.release) return next();
|
|
185
|
-
throw new EdgeBaseError(
|
|
192
|
+
throw new EdgeBaseError(
|
|
193
|
+
403,
|
|
194
|
+
'Access denied. No databases config is defined for this server. Add config.databases or set release: false while developing locally.',
|
|
195
|
+
);
|
|
186
196
|
}
|
|
187
197
|
|
|
188
198
|
// namespace comes directly from the URL (/api/db/:namespace/...)
|
|
@@ -192,13 +202,19 @@ export async function rulesMiddleware(c: HonoContext, next: Next): Promise<Respo
|
|
|
192
202
|
if (!dbBlock) {
|
|
193
203
|
// Namespace not found in config → deny
|
|
194
204
|
if (!config.release) return next();
|
|
195
|
-
throw new EdgeBaseError(
|
|
205
|
+
throw new EdgeBaseError(
|
|
206
|
+
403,
|
|
207
|
+
`Access denied. Namespace '${tableNamespace}' is not configured. Check the API path or add this namespace to config.databases.`,
|
|
208
|
+
);
|
|
196
209
|
}
|
|
197
210
|
|
|
198
211
|
if (dbBlock.tables && !dbBlock.tables[tableName]) {
|
|
199
212
|
// Table not defined in this DB block
|
|
200
213
|
if (!config.release) return next();
|
|
201
|
-
throw new EdgeBaseError(
|
|
214
|
+
throw new EdgeBaseError(
|
|
215
|
+
403,
|
|
216
|
+
`Access denied. Table '${tableName}' is missing access rules. Add access.* rules or disable release mode for local-only development.`,
|
|
217
|
+
);
|
|
202
218
|
}
|
|
203
219
|
|
|
204
220
|
// ── Step 3: DB-level access check (§4) ──
|
|
@@ -232,7 +248,10 @@ export async function rulesMiddleware(c: HonoContext, next: Next): Promise<Respo
|
|
|
232
248
|
|
|
233
249
|
if (!tableRules) {
|
|
234
250
|
if (!config.release) return next();
|
|
235
|
-
throw new EdgeBaseError(
|
|
251
|
+
throw new EdgeBaseError(
|
|
252
|
+
403,
|
|
253
|
+
`Access denied. No access rules are defined for table '${tableName}'. Add access.* rules or disable release mode for local-only development.`,
|
|
254
|
+
);
|
|
236
255
|
}
|
|
237
256
|
|
|
238
257
|
// Determine action
|
|
@@ -259,7 +278,7 @@ export async function rulesMiddleware(c: HonoContext, next: Next): Promise<Respo
|
|
|
259
278
|
: !config.release;
|
|
260
279
|
|
|
261
280
|
if (!insertPass && !updatePass) {
|
|
262
|
-
throw
|
|
281
|
+
throw tableRuleRejected(tableName, 'upsert');
|
|
263
282
|
}
|
|
264
283
|
return next();
|
|
265
284
|
}
|
|
@@ -274,9 +293,9 @@ export async function rulesMiddleware(c: HonoContext, next: Next): Promise<Respo
|
|
|
274
293
|
}
|
|
275
294
|
if (insertRuleFn) {
|
|
276
295
|
const canInsert = await evalWithTimeout(() => insertRuleFn(auth), WORKER_RULE_TIMEOUT_MS);
|
|
277
|
-
if (!canInsert) throw
|
|
296
|
+
if (!canInsert) throw tableRuleRejected(tableName, 'batch insert');
|
|
278
297
|
} else if (config.release) {
|
|
279
|
-
throw
|
|
298
|
+
throw tableRuleRejected(tableName, 'batch insert');
|
|
280
299
|
}
|
|
281
300
|
}
|
|
282
301
|
return next();
|
|
@@ -292,7 +311,7 @@ export async function rulesMiddleware(c: HonoContext, next: Next): Promise<Respo
|
|
|
292
311
|
throw new EdgeBaseError(403, `Access denied. No 'insert' rule defined for '${tableName}'.`);
|
|
293
312
|
}
|
|
294
313
|
const canInsert = await evalWithTimeout(() => insertRuleFn(auth), WORKER_RULE_TIMEOUT_MS);
|
|
295
|
-
if (!canInsert) throw
|
|
314
|
+
if (!canInsert) throw tableRuleRejected(tableName, 'insert');
|
|
296
315
|
return next();
|
|
297
316
|
}
|
|
298
317
|
|
package/src/routes/admin-auth.ts
CHANGED
|
@@ -51,7 +51,7 @@ adminAuthRoute.onError((err, c) => {
|
|
|
51
51
|
return c.json(err.toJSON(), err.code as 400);
|
|
52
52
|
}
|
|
53
53
|
console.error('Admin Auth unhandled error:', err);
|
|
54
|
-
return c.json({ code: 500, message: '
|
|
54
|
+
return c.json({ code: 500, message: 'Admin auth request failed unexpectedly. Check the worker logs for the original exception.' }, 500);
|
|
55
55
|
});
|
|
56
56
|
|
|
57
57
|
// Service Key middleware — scoped validation
|
|
@@ -65,10 +65,10 @@ adminAuthRoute.use('*', async (c, next) => {
|
|
|
65
65
|
const provided = explicitServiceKey ?? resolveServiceKeyCandidate(c.req, extractBearerToken(c.req));
|
|
66
66
|
const { result } = validateKey(provided, 'auth:admin:*:*', config, c.env, undefined, buildConstraintCtx(c.env, c.req));
|
|
67
67
|
if (result === 'missing') {
|
|
68
|
-
throw new EdgeBaseError(403, 'Service
|
|
68
|
+
throw new EdgeBaseError(403, 'X-EdgeBase-Service-Key is required for admin auth operations.');
|
|
69
69
|
}
|
|
70
70
|
if (result === 'invalid') {
|
|
71
|
-
throw new EdgeBaseError(401, '
|
|
71
|
+
throw new EdgeBaseError(401, 'Invalid X-EdgeBase-Service-Key for admin auth operations.');
|
|
72
72
|
}
|
|
73
73
|
await ensureAuthSchema(getAuthDb(c));
|
|
74
74
|
await next();
|