@edge-base/server 0.2.2 → 0.2.4
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/{C85dMlzL.js → 5RQRbp5q.js} +1 -1
- package/admin-build/_app/immutable/chunks/{B8DT4fss.js → BME_U9TJ.js} +1 -1
- package/admin-build/_app/immutable/chunks/{BaCHY17I.js → BYI6CUvd.js} +1 -1
- package/admin-build/_app/immutable/chunks/{BWyDPAjM.js → BgDzp0i0.js} +1 -1
- package/admin-build/_app/immutable/chunks/{c5iKSdWY.js → BjWZuf8W.js} +1 -1
- package/admin-build/_app/immutable/chunks/{g3ZZdY-r.js → C6lpZLE2.js} +1 -1
- package/admin-build/_app/immutable/chunks/{C-DsDCNG.js → D5GswVnI.js} +3 -3
- package/admin-build/_app/immutable/chunks/DBsVqhuh.js +1 -0
- package/admin-build/_app/immutable/chunks/{BEYYl662.js → DYaCRWMA.js} +1 -1
- package/admin-build/_app/immutable/chunks/D__dwMuW.js +1 -0
- package/admin-build/_app/immutable/chunks/{4vlsb8ej.js → Dj-E9-FO.js} +1 -1
- package/admin-build/_app/immutable/chunks/{kiJ6KthZ.js → Dj0QUuOf.js} +1 -1
- package/admin-build/_app/immutable/chunks/{BKXmgPq4.js → XQM1k9PM.js} +1 -1
- package/admin-build/_app/immutable/chunks/{CTngeX8H.js → fYEKMQ-Z.js} +1 -1
- package/admin-build/_app/immutable/chunks/{CPdXvRUb.js → g_-Kpxu3.js} +1 -1
- package/admin-build/_app/immutable/chunks/{DzXaj-Ja.js → wCNueVYy.js} +1 -1
- package/admin-build/_app/immutable/entry/{app.BZxfavhF.js → app.C8ylfBe6.js} +2 -2
- package/admin-build/_app/immutable/entry/start.CtsqDyfj.js +1 -0
- package/admin-build/_app/immutable/nodes/{0.DlsaydXO.js → 0.CJJ6HZbp.js} +1 -1
- package/admin-build/_app/immutable/nodes/{1.D2NWN5eG.js → 1.B4sI5cB4.js} +1 -1
- package/admin-build/_app/immutable/nodes/{10.EMDaN3nw.js → 10.D6hvCer6.js} +1 -1
- package/admin-build/_app/immutable/nodes/{11.BasqQ_o9.js → 11.Dx7b8aQ5.js} +1 -1
- package/admin-build/_app/immutable/nodes/{12.DO31Ljs7.js → 12.Bqmy5KIF.js} +1 -1
- package/admin-build/_app/immutable/nodes/{13.DhyAy-GZ.js → 13.CC6KpXgS.js} +1 -1
- package/admin-build/_app/immutable/nodes/{14.CLecGWc4.js → 14.yCo1Ix8E.js} +1 -1
- package/admin-build/_app/immutable/nodes/{15.B9kp3W4e.js → 15.co0UfPlh.js} +1 -1
- package/admin-build/_app/immutable/nodes/{16.Pu_8T3RI.js → 16.D0xkPUBW.js} +1 -1
- package/admin-build/_app/immutable/nodes/{17.DX4z43t6.js → 17.CebNqPeh.js} +1 -1
- package/admin-build/_app/immutable/nodes/{18.BKsSaxrr.js → 18.JUoLOZxh.js} +1 -1
- package/admin-build/_app/immutable/nodes/{19.DXNF1htN.js → 19.ND8kmQJe.js} +1 -1
- package/admin-build/_app/immutable/nodes/{20.VRVb0wee.js → 20.DYb-q3W8.js} +1 -1
- package/admin-build/_app/immutable/nodes/21.cz3IN9Cc.js +1 -0
- package/admin-build/_app/immutable/nodes/{22.DqZf4CtH.js → 22.UOzm8WYV.js} +1 -1
- package/admin-build/_app/immutable/nodes/{23.DtyxMiQG.js → 23.BLgq21om.js} +1 -1
- package/admin-build/_app/immutable/nodes/{24.CloWNmTd.js → 24.DN9usmUs.js} +1 -1
- package/admin-build/_app/immutable/nodes/{25.CnZWMq7_.js → 25.BddRfAyE.js} +1 -1
- package/admin-build/_app/immutable/nodes/{26.DrV7XOmf.js → 26.Dl6XHIeT.js} +1 -1
- package/admin-build/_app/immutable/nodes/{27.DV8L32OF.js → 27.D0iNwALG.js} +1 -1
- package/admin-build/_app/immutable/nodes/{28.Stil2D4u.js → 28.9dKQmdGi.js} +1 -1
- package/admin-build/_app/immutable/nodes/{29.Zsm1e5Dc.js → 29.wXzfJUXp.js} +1 -1
- package/admin-build/_app/immutable/nodes/{3.CKoj2vNz.js → 3.z8ut3jS-.js} +1 -1
- package/admin-build/_app/immutable/nodes/{30.Ni0k5bER.js → 30.BtZETNsL.js} +1 -1
- package/admin-build/_app/immutable/nodes/{31.mnqj9EbV.js → 31.CYonj2Jh.js} +1 -1
- package/admin-build/_app/immutable/nodes/{4.B_-z9AzT.js → 4.COtDPQ9b.js} +1 -1
- package/admin-build/_app/immutable/nodes/{5.yiZ72j4k.js → 5.CTRCeIhp.js} +1 -1
- package/admin-build/_app/immutable/nodes/{6.BqykybBG.js → 6.ChHi3QkR.js} +1 -1
- package/admin-build/_app/immutable/nodes/{7.BDAHlhsF.js → 7.CCMtr6Ac.js} +1 -1
- package/admin-build/_app/immutable/nodes/{8.D8Xvy0lH.js → 8.DpWJ-X_-.js} +1 -1
- package/admin-build/_app/immutable/nodes/{9.Dddmd7_F.js → 9.DOkvfmir.js} +1 -1
- package/admin-build/_app/version.json +1 -1
- package/admin-build/index.html +7 -7
- package/package.json +3 -3
- package/src/__tests__/admin-data-routes.test.ts +29 -0
- package/src/__tests__/d1-live-broadcast-verification.test.ts +271 -0
- package/src/__tests__/database-do-route-validation.test.ts +105 -0
- package/src/__tests__/database-live-do.test.ts +50 -0
- package/src/__tests__/database-live-emitter.test.ts +116 -1
- package/src/__tests__/database-live-route.test.ts +82 -0
- package/src/__tests__/do-router.test.ts +116 -0
- package/src/__tests__/error-format.test.ts +63 -0
- package/src/__tests__/functions-context.test.ts +674 -33
- package/src/__tests__/functions-d1-proxy.test.ts +54 -0
- package/src/__tests__/plugin-migration-routing.test.ts +32 -0
- package/src/__tests__/postgres-field-ops-compat.test.ts +110 -0
- package/src/__tests__/provider-aware-sql.test.ts +163 -0
- package/src/__tests__/room-auth-state-loss.test.ts +124 -0
- package/src/__tests__/runtime-surface-accounting.test.ts +0 -4
- package/src/__tests__/scheduled.test.ts +55 -0
- package/src/__tests__/service-key-db-proxy.test.ts +122 -1
- package/src/__tests__/sql-route.test.ts +252 -75
- package/src/__tests__/table-hook-runtime.test.ts +137 -0
- package/src/durable-objects/database-do.ts +36 -45
- package/src/durable-objects/database-live-do.ts +46 -1
- package/src/durable-objects/room-runtime-base.ts +26 -2
- package/src/durable-objects/rooms-do.ts +1 -1
- package/src/index.ts +12 -6
- package/src/lib/admin-db-target.ts +30 -74
- package/src/lib/d1-handler.ts +55 -35
- package/src/lib/database-live-emitter.ts +57 -16
- package/src/lib/do-router.ts +135 -3
- package/src/lib/functions.ts +215 -143
- package/src/lib/internal-transport.ts +28 -12
- package/src/lib/plugin-migration-routing.ts +28 -0
- package/src/lib/plugin-migrations.ts +38 -38
- package/src/lib/postgres-handler.ts +51 -31
- package/src/lib/provider-aware-sql.ts +831 -0
- package/src/lib/table-hook-runtime.ts +62 -0
- package/src/routes/admin.ts +41 -41
- package/src/routes/auth.ts +7 -2
- package/src/routes/database-live.ts +110 -12
- package/src/routes/sql.ts +64 -84
- package/src/routes/storage.ts +7 -2
- package/src/routes/tables.ts +42 -29
- package/admin-build/_app/immutable/chunks/5PDcRlfX.js +0 -1
- package/admin-build/_app/immutable/chunks/qiZXAKh-.js +0 -1
- package/admin-build/_app/immutable/entry/start.Mr9mmopc.js +0 -1
- package/admin-build/_app/immutable/nodes/21.Ck3_0D2f.js +0 -1
|
@@ -54,15 +54,12 @@ import {
|
|
|
54
54
|
getRegisteredFunctions,
|
|
55
55
|
buildFunctionContext,
|
|
56
56
|
} from '../lib/functions.js';
|
|
57
|
-
import { parseDbDoName, parseConfig as getGlobalConfig } from '../lib/do-router.js';
|
|
57
|
+
import { parseDbDoName, parseConfig as getGlobalConfig, isDynamicDbBlock } from '../lib/do-router.js';
|
|
58
58
|
import { parseDuration } from '../lib/jwt.js';
|
|
59
|
-
import { createPushProvider } from '../lib/push-provider.js';
|
|
60
|
-
import { getDevicesForUser } from '../lib/push-token.js';
|
|
61
|
-
import { ensureAuthSchema } from '../lib/auth-d1.js';
|
|
62
|
-
import { resolveAuthDb, type AuthDb } from '../lib/auth-db-adapter.js';
|
|
63
59
|
import { buildDbLiveChannel, DATABASE_LIVE_HUB_DO_NAME } from '../lib/database-live-emitter.js';
|
|
64
60
|
import { resolveRootServiceKey } from '../lib/service-key.js';
|
|
65
61
|
import { resolveDbLiveBatchThreshold } from '../lib/database-live-config.js';
|
|
62
|
+
import { buildTableHookRuntimeServices } from '../lib/table-hook-runtime.js';
|
|
66
63
|
import type { Env } from '../types.js';
|
|
67
64
|
|
|
68
65
|
// ─── Types ───
|
|
@@ -104,8 +101,36 @@ export class DatabaseDO extends DurableObject<DOEnv> {
|
|
|
104
101
|
if (!this.initialized) {
|
|
105
102
|
// §36: Newly created DO must be authorized before initialization.
|
|
106
103
|
// If X-DO-Create-Authorized header is absent, signal Worker to evaluate canCreate.
|
|
107
|
-
//
|
|
108
|
-
const
|
|
104
|
+
// Single-instance DB blocks and internal/system DOs skip this gate.
|
|
105
|
+
const parsedDoName = this.doName ? parseDbDoName(this.doName) : null;
|
|
106
|
+
const dbBlock = parsedDoName ? this.config.databases?.[parsedDoName.namespace] : undefined;
|
|
107
|
+
const dynamicDbBlock = isDynamicDbBlock(dbBlock);
|
|
108
|
+
if (parsedDoName && dbBlock) {
|
|
109
|
+
if (dynamicDbBlock && !parsedDoName.id) {
|
|
110
|
+
return Response.json(
|
|
111
|
+
{
|
|
112
|
+
code: 400,
|
|
113
|
+
message: `instanceId is required for dynamic namespace '${parsedDoName.namespace}'`,
|
|
114
|
+
error: 'INVALID_DB_INSTANCE_ID',
|
|
115
|
+
},
|
|
116
|
+
{ status: 400 },
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
if (!dynamicDbBlock && parsedDoName.id) {
|
|
120
|
+
return Response.json(
|
|
121
|
+
{
|
|
122
|
+
code: 400,
|
|
123
|
+
message: `instanceId is not allowed for single-instance namespace '${parsedDoName.namespace}'`,
|
|
124
|
+
error: 'INVALID_DB_INSTANCE_ID',
|
|
125
|
+
},
|
|
126
|
+
{ status: 400 },
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
const isStaticDO =
|
|
131
|
+
!this.doName
|
|
132
|
+
|| this.doName.startsWith('_')
|
|
133
|
+
|| (!!dbBlock && !dynamicDbBlock && !parsedDoName?.id);
|
|
109
134
|
if (!isStaticDO && !request.headers.get('X-DO-Create-Authorized')) {
|
|
110
135
|
// Check if _meta table already exists (i.e., DO was previously initialized)
|
|
111
136
|
let alreadyExists = false;
|
|
@@ -118,9 +143,8 @@ export class DatabaseDO extends DurableObject<DOEnv> {
|
|
|
118
143
|
|
|
119
144
|
if (!alreadyExists) {
|
|
120
145
|
// Signal Worker: this DO needs canCreate evaluation before init
|
|
121
|
-
const parsed = this.doName ? parseDbDoName(this.doName) : null;
|
|
122
146
|
return Response.json(
|
|
123
|
-
{ needsCreate: true, namespace:
|
|
147
|
+
{ needsCreate: true, namespace: parsedDoName?.namespace ?? 'shared', id: parsedDoName?.id },
|
|
124
148
|
{ status: 201 },
|
|
125
149
|
);
|
|
126
150
|
}
|
|
@@ -173,6 +197,8 @@ export class DatabaseDO extends DurableObject<DOEnv> {
|
|
|
173
197
|
* db.get/list/exists use local SQL; databaseLive.broadcast uses emitDbLiveEvent.
|
|
174
198
|
*/
|
|
175
199
|
private buildHookCtx(_table: string): HookCtx {
|
|
200
|
+
const runtimeServices = buildTableHookRuntimeServices(this.config, this.env as unknown as Env);
|
|
201
|
+
|
|
176
202
|
return {
|
|
177
203
|
db: {
|
|
178
204
|
get: (tbl: string, id: string) => {
|
|
@@ -201,42 +227,7 @@ export class DatabaseDO extends DurableObject<DOEnv> {
|
|
|
201
227
|
return Promise.resolve(rows.length > 0);
|
|
202
228
|
},
|
|
203
229
|
},
|
|
204
|
-
|
|
205
|
-
broadcast: (channel: string, event: string, data: unknown) => {
|
|
206
|
-
return this.sendBroadcastToDatabaseLiveDO(
|
|
207
|
-
channel,
|
|
208
|
-
{ channel, event, payload: data ?? {} },
|
|
209
|
-
);
|
|
210
|
-
},
|
|
211
|
-
},
|
|
212
|
-
push: {
|
|
213
|
-
// Push from hooks — direct FCM via push-provider + KV device tokens
|
|
214
|
-
send: async (userId: string, payload: { title?: string; body: string }) => {
|
|
215
|
-
// Fire-and-forget — hooks are non-critical side effects
|
|
216
|
-
try {
|
|
217
|
-
if (!this.env.KV) return;
|
|
218
|
-
const provider = createPushProvider(this.config.push, this.env as unknown as Env);
|
|
219
|
-
if (!provider) return;
|
|
220
|
-
let tokenStore: KVNamespace | { kv: KVNamespace; authDb?: AuthDb | null } = this.env.KV;
|
|
221
|
-
try {
|
|
222
|
-
const authDb = resolveAuthDb(this.env as unknown as Record<string, unknown>);
|
|
223
|
-
await ensureAuthSchema(authDb);
|
|
224
|
-
tokenStore = { kv: this.env.KV, authDb };
|
|
225
|
-
} catch {
|
|
226
|
-
tokenStore = this.env.KV;
|
|
227
|
-
}
|
|
228
|
-
const devices = await getDevicesForUser(tokenStore, userId);
|
|
229
|
-
if (devices.length === 0) return;
|
|
230
|
-
await Promise.allSettled(
|
|
231
|
-
devices.map((device) =>
|
|
232
|
-
provider.send({ token: device.token, platform: device.platform, payload }),
|
|
233
|
-
),
|
|
234
|
-
);
|
|
235
|
-
} catch {
|
|
236
|
-
/* best-effort */
|
|
237
|
-
}
|
|
238
|
-
},
|
|
239
|
-
},
|
|
230
|
+
...runtimeServices,
|
|
240
231
|
waitUntil: (p: Promise<unknown>) => this.ctx.waitUntil(p),
|
|
241
232
|
};
|
|
242
233
|
}
|
|
@@ -19,6 +19,7 @@ interface DatabaseLiveEvent {
|
|
|
19
19
|
docId: string;
|
|
20
20
|
data: Record<string, unknown> | null;
|
|
21
21
|
timestamp: string;
|
|
22
|
+
deliveryId?: string;
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
export type DatabaseLiveFilterCondition = [
|
|
@@ -42,6 +43,8 @@ interface WSMeta {
|
|
|
42
43
|
}
|
|
43
44
|
|
|
44
45
|
const MAX_FILTER_CONDITIONS = 5;
|
|
46
|
+
const RECENT_DELIVERY_TTL_MS = 60_000;
|
|
47
|
+
const MAX_RECENT_DELIVERIES = 2048;
|
|
45
48
|
const VALID_FILTER_OPERATORS = new Set<FilterOperator>([
|
|
46
49
|
'==',
|
|
47
50
|
'!=',
|
|
@@ -138,6 +141,7 @@ export class DatabaseLiveDO extends DurableObject<DOEnv> {
|
|
|
138
141
|
private filterRecoveryNeeded = true;
|
|
139
142
|
private pendingAuth = new Map<string, ReturnType<typeof setTimeout>>();
|
|
140
143
|
private metaCache = new Map<WebSocket, WSMeta>();
|
|
144
|
+
private recentDeliveryIds = new Map<string, number>();
|
|
141
145
|
|
|
142
146
|
constructor(ctx: DurableObjectState, env: DOEnv) {
|
|
143
147
|
super(ctx, env);
|
|
@@ -520,6 +524,10 @@ export class DatabaseLiveDO extends DurableObject<DOEnv> {
|
|
|
520
524
|
return Response.json({ error: 'Invalid event body' }, { status: 400 });
|
|
521
525
|
}
|
|
522
526
|
|
|
527
|
+
if (this.isDuplicateDelivery(event.deliveryId)) {
|
|
528
|
+
return Response.json({ ok: true, duplicate: true });
|
|
529
|
+
}
|
|
530
|
+
|
|
523
531
|
await this.broadcastWithFilters({
|
|
524
532
|
type: 'db_change',
|
|
525
533
|
channel: event.channel,
|
|
@@ -540,6 +548,7 @@ export class DatabaseLiveDO extends DurableObject<DOEnv> {
|
|
|
540
548
|
table: string;
|
|
541
549
|
changes: Array<{ type: string; docId: string; data: Record<string, unknown> | null; timestamp: string }>;
|
|
542
550
|
total: number;
|
|
551
|
+
deliveryId?: string;
|
|
543
552
|
};
|
|
544
553
|
try {
|
|
545
554
|
batch = await request.json() as typeof batch;
|
|
@@ -547,6 +556,10 @@ export class DatabaseLiveDO extends DurableObject<DOEnv> {
|
|
|
547
556
|
return Response.json({ error: 'Invalid batch event body' }, { status: 400 });
|
|
548
557
|
}
|
|
549
558
|
|
|
559
|
+
if (this.isDuplicateDelivery(batch.deliveryId)) {
|
|
560
|
+
return Response.json({ ok: true, duplicate: true });
|
|
561
|
+
}
|
|
562
|
+
|
|
550
563
|
const sockets = this.ctx.getWebSockets();
|
|
551
564
|
const batchMsg = {
|
|
552
565
|
type: 'batch_changes' as const,
|
|
@@ -646,13 +659,17 @@ export class DatabaseLiveDO extends DurableObject<DOEnv> {
|
|
|
646
659
|
* Sends a custom broadcast_event to all clients subscribed to matching DB channels.
|
|
647
660
|
*/
|
|
648
661
|
private async handleInternalBroadcast(request: Request): Promise<Response> {
|
|
649
|
-
let body: { channel?: string; event?: string; payload?: Record<string, unknown
|
|
662
|
+
let body: { channel?: string; event?: string; payload?: Record<string, unknown>; deliveryId?: string };
|
|
650
663
|
try {
|
|
651
664
|
body = await request.json() as typeof body;
|
|
652
665
|
} catch {
|
|
653
666
|
return Response.json({ error: 'Invalid broadcast body' }, { status: 400 });
|
|
654
667
|
}
|
|
655
668
|
|
|
669
|
+
if (this.isDuplicateDelivery(body.deliveryId)) {
|
|
670
|
+
return Response.json({ ok: true, duplicate: true });
|
|
671
|
+
}
|
|
672
|
+
|
|
656
673
|
const { channel, event, payload } = body;
|
|
657
674
|
if (!channel || typeof channel !== 'string') {
|
|
658
675
|
return Response.json({ error: 'channel is required' }, { status: 400 });
|
|
@@ -791,6 +808,34 @@ export class DatabaseLiveDO extends DurableObject<DOEnv> {
|
|
|
791
808
|
this.metaCache.set(ws, meta);
|
|
792
809
|
}
|
|
793
810
|
|
|
811
|
+
private isDuplicateDelivery(deliveryId?: string): boolean {
|
|
812
|
+
if (!deliveryId) return false;
|
|
813
|
+
|
|
814
|
+
const now = Date.now();
|
|
815
|
+
this.pruneRecentDeliveries(now);
|
|
816
|
+
if (this.recentDeliveryIds.has(deliveryId)) {
|
|
817
|
+
return true;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
this.recentDeliveryIds.set(deliveryId, now);
|
|
821
|
+
while (this.recentDeliveryIds.size > MAX_RECENT_DELIVERIES) {
|
|
822
|
+
const oldest = this.recentDeliveryIds.keys().next().value;
|
|
823
|
+
if (!oldest) break;
|
|
824
|
+
this.recentDeliveryIds.delete(oldest);
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
return false;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
private pruneRecentDeliveries(now: number): void {
|
|
831
|
+
for (const [deliveryId, seenAt] of this.recentDeliveryIds) {
|
|
832
|
+
if (now - seenAt <= RECENT_DELIVERY_TTL_MS) {
|
|
833
|
+
break;
|
|
834
|
+
}
|
|
835
|
+
this.recentDeliveryIds.delete(deliveryId);
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
|
|
794
839
|
private getTableReadRule(tableName: string):
|
|
795
840
|
| ((auth: Record<string, unknown> | null, row: Record<string, unknown>) => boolean | Promise<boolean>)
|
|
796
841
|
| boolean
|
|
@@ -48,6 +48,7 @@ export interface RoomDOEnv {
|
|
|
48
48
|
|
|
49
49
|
export interface RoomWSMeta {
|
|
50
50
|
authenticated: boolean;
|
|
51
|
+
authStateLost?: boolean;
|
|
51
52
|
userId?: string;
|
|
52
53
|
role?: string;
|
|
53
54
|
auth?: SharedAuthContext;
|
|
@@ -70,6 +71,8 @@ const DEFAULT_DELTA_BATCH_MS = 50;
|
|
|
70
71
|
const DEFAULT_RATE_LIMIT_ACTIONS = 10;
|
|
71
72
|
const DEFAULT_RECONNECT_TIMEOUT_MS = 30000;
|
|
72
73
|
const ROOM_CLIENT_LEAVE_CLOSE_CODE = 4005;
|
|
74
|
+
const ROOM_AUTH_STATE_LOST_CLOSE_CODE = 4006;
|
|
75
|
+
const ROOM_AUTH_STATE_LOST_CLOSE_REASON = 'Room authentication state lost';
|
|
73
76
|
const EMPTY_ROOM_CLEANUP_DELAY_MS = 100;
|
|
74
77
|
const DEFAULT_IDLE_TIMEOUT_SEC = 300;
|
|
75
78
|
const ACTION_TIMEOUT_MS = 5000;
|
|
@@ -330,6 +333,7 @@ export class RoomRuntimeBaseDO extends DurableObject<RoomDOEnv> {
|
|
|
330
333
|
const connectionId = crypto.randomUUID();
|
|
331
334
|
const meta: RoomWSMeta = {
|
|
332
335
|
authenticated: false,
|
|
336
|
+
authStateLost: false,
|
|
333
337
|
connectionId,
|
|
334
338
|
ip: request.headers.get('CF-Connecting-IP')
|
|
335
339
|
|| request.headers.get('X-Forwarded-For')?.split(',')[0]?.trim()
|
|
@@ -407,7 +411,7 @@ export class RoomRuntimeBaseDO extends DurableObject<RoomDOEnv> {
|
|
|
407
411
|
|
|
408
412
|
// Everything else requires authentication
|
|
409
413
|
if (!meta.authenticated) {
|
|
410
|
-
this.
|
|
414
|
+
this.handleUnauthenticatedSocket(ws, meta);
|
|
411
415
|
return;
|
|
412
416
|
}
|
|
413
417
|
|
|
@@ -593,6 +597,7 @@ export class RoomRuntimeBaseDO extends DurableObject<RoomDOEnv> {
|
|
|
593
597
|
new Request('http://internal/api/room/auth', { headers }),
|
|
594
598
|
);
|
|
595
599
|
meta.authenticated = true;
|
|
600
|
+
meta.authStateLost = false;
|
|
596
601
|
meta.userId = auth.id;
|
|
597
602
|
meta.role = auth.role;
|
|
598
603
|
meta.auth = {
|
|
@@ -656,7 +661,7 @@ export class RoomRuntimeBaseDO extends DurableObject<RoomDOEnv> {
|
|
|
656
661
|
msg: Record<string, unknown>,
|
|
657
662
|
): Promise<void> {
|
|
658
663
|
if (!meta.authenticated || !meta.userId) {
|
|
659
|
-
this.
|
|
664
|
+
this.handleUnauthenticatedSocket(ws, meta);
|
|
660
665
|
return;
|
|
661
666
|
}
|
|
662
667
|
|
|
@@ -1582,6 +1587,7 @@ export class RoomRuntimeBaseDO extends DurableObject<RoomDOEnv> {
|
|
|
1582
1587
|
|
|
1583
1588
|
const meta: RoomWSMeta = {
|
|
1584
1589
|
authenticated: false, // Must re-auth after hibernation
|
|
1590
|
+
authStateLost: true,
|
|
1585
1591
|
connectionId,
|
|
1586
1592
|
ip,
|
|
1587
1593
|
};
|
|
@@ -1596,6 +1602,24 @@ export class RoomRuntimeBaseDO extends DurableObject<RoomDOEnv> {
|
|
|
1596
1602
|
this._metaCache.set(ws, meta);
|
|
1597
1603
|
}
|
|
1598
1604
|
|
|
1605
|
+
protected handleUnauthenticatedSocket(ws: WebSocket, meta: RoomWSMeta): void {
|
|
1606
|
+
if (meta.authStateLost) {
|
|
1607
|
+
this.safeSend(ws, {
|
|
1608
|
+
type: 'error',
|
|
1609
|
+
code: 'AUTH_STATE_LOST',
|
|
1610
|
+
message: 'Room authentication state lost. Reconnect required.',
|
|
1611
|
+
});
|
|
1612
|
+
try {
|
|
1613
|
+
ws.close(ROOM_AUTH_STATE_LOST_CLOSE_CODE, ROOM_AUTH_STATE_LOST_CLOSE_REASON);
|
|
1614
|
+
} catch {
|
|
1615
|
+
// Socket may already be closing.
|
|
1616
|
+
}
|
|
1617
|
+
return;
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
this.safeSend(ws, { type: 'error', code: 'NOT_AUTHENTICATED', message: 'Authenticate first' });
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1599
1623
|
// ─── Config ───
|
|
1600
1624
|
|
|
1601
1625
|
private parseConfig(env: RoomDOEnv): EdgeBaseConfig {
|
|
@@ -2359,7 +2359,7 @@ export class RoomsDO extends RoomRuntimeBaseDO {
|
|
|
2359
2359
|
}
|
|
2360
2360
|
|
|
2361
2361
|
if (!meta.authenticated) {
|
|
2362
|
-
this.
|
|
2362
|
+
this.handleUnauthenticatedSocket(ws, meta);
|
|
2363
2363
|
return null;
|
|
2364
2364
|
}
|
|
2365
2365
|
|
package/src/index.ts
CHANGED
|
@@ -35,6 +35,7 @@ import { createAdminAssetRequest } from './lib/admin-assets.js';
|
|
|
35
35
|
import { resolveAdminFaviconTarget, resolveAdminRedirectTarget } from './lib/admin-routing.js';
|
|
36
36
|
import { zodDefaultHook } from './lib/schemas.js';
|
|
37
37
|
import { executePluginMigrations } from './lib/plugin-migrations.js';
|
|
38
|
+
import { shouldRunPluginMigrationsForRequestPath } from './lib/plugin-migration-routing.js';
|
|
38
39
|
import { getFunctionsByTrigger, buildFunctionContext, getWorkerUrl } from './lib/functions.js';
|
|
39
40
|
import { parseCron, matchesCron } from './lib/cron.js';
|
|
40
41
|
import { parseDuration } from './lib/jwt.js';
|
|
@@ -104,15 +105,12 @@ app.use('*', corsMiddleware);
|
|
|
104
105
|
// middleware below rejects any request with this header unless it comes via internal
|
|
105
106
|
// stub.fetch (which is allowed by the x-internal whitelist). No stripping needed.
|
|
106
107
|
|
|
107
|
-
// Plugin migration middleware —
|
|
108
|
+
// Plugin migration middleware — lazily reconciles plugin control state before
|
|
109
|
+
// routes that can execute plugin code or touch plugin-managed tables.
|
|
108
110
|
app.use('*', async (c, next) => {
|
|
109
111
|
const config = parseConfig(c.env);
|
|
110
112
|
const requestPath = new URL(c.req.url).pathname;
|
|
111
|
-
|
|
112
|
-
requestPath.startsWith('/admin/api/backup/') ||
|
|
113
|
-
requestPath.startsWith('/admin/api/data/backup/') ||
|
|
114
|
-
requestPath.startsWith('/internal/backup/');
|
|
115
|
-
if (!shouldSkipPluginMigrations && config?.plugins?.length) {
|
|
113
|
+
if (config?.plugins?.length && shouldRunPluginMigrationsForRequestPath(requestPath)) {
|
|
116
114
|
await executePluginMigrations(config.plugins, c.env, config, getWorkerUrl(c.req.url, c.env));
|
|
117
115
|
}
|
|
118
116
|
return next();
|
|
@@ -306,6 +304,14 @@ export default {
|
|
|
306
304
|
*/
|
|
307
305
|
async scheduled(event: ScheduledEvent, env: Env, ctx: ExecutionContext): Promise<void> {
|
|
308
306
|
const config = parseConfig(env);
|
|
307
|
+
if (config.plugins?.length) {
|
|
308
|
+
await executePluginMigrations(
|
|
309
|
+
config.plugins,
|
|
310
|
+
env,
|
|
311
|
+
config,
|
|
312
|
+
getWorkerUrl('http://internal/scheduled', env),
|
|
313
|
+
);
|
|
314
|
+
}
|
|
309
315
|
const scheduleFns = getFunctionsByTrigger('schedule');
|
|
310
316
|
|
|
311
317
|
const now = new Date(event.scheduledTime);
|
|
@@ -4,16 +4,7 @@ import type {
|
|
|
4
4
|
AdminInstanceDiscoveryOption,
|
|
5
5
|
EdgeBaseConfig,
|
|
6
6
|
} from '@edge-base/shared';
|
|
7
|
-
import {
|
|
8
|
-
import { executeDoSql } from './do-sql.js';
|
|
9
|
-
import { getD1BindingName, shouldRouteToD1 } from './do-router.js';
|
|
10
|
-
import {
|
|
11
|
-
ensureLocalDevPostgresSchema,
|
|
12
|
-
getLocalDevPostgresExecOptions,
|
|
13
|
-
getProviderBindingName,
|
|
14
|
-
withPostgresConnection,
|
|
15
|
-
} from './postgres-executor.js';
|
|
16
|
-
import { ensurePgSchema } from './postgres-schema-init.js';
|
|
7
|
+
import { executeProviderAwareSql } from './provider-aware-sql.js';
|
|
17
8
|
import type { Env } from '../types.js';
|
|
18
9
|
|
|
19
10
|
export interface AdminDbQueryResult {
|
|
@@ -47,13 +38,15 @@ interface ResolveAdminInstanceOptions {
|
|
|
47
38
|
}
|
|
48
39
|
|
|
49
40
|
function isDynamicDbBlock(
|
|
50
|
-
dbBlock:
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
41
|
+
dbBlock:
|
|
42
|
+
| {
|
|
43
|
+
instance?: boolean;
|
|
44
|
+
access?: {
|
|
45
|
+
canCreate?: unknown;
|
|
46
|
+
access?: unknown;
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
| undefined,
|
|
57
50
|
): boolean {
|
|
58
51
|
if (!dbBlock) return false;
|
|
59
52
|
return !!(dbBlock.instance || dbBlock.access?.canCreate || dbBlock.access?.access);
|
|
@@ -108,7 +101,9 @@ function uniqueStrings(values: Array<string | undefined>): string[] {
|
|
|
108
101
|
return result;
|
|
109
102
|
}
|
|
110
103
|
|
|
111
|
-
function normalizeDiscoveryItems(
|
|
104
|
+
function normalizeDiscoveryItems(
|
|
105
|
+
items: AdminInstanceDiscoveryOption[],
|
|
106
|
+
): AdminInstanceDiscoveryOption[] {
|
|
112
107
|
const seen = new Set<string>();
|
|
113
108
|
const normalized: AdminInstanceDiscoveryOption[] = [];
|
|
114
109
|
for (const item of items) {
|
|
@@ -132,60 +127,17 @@ export async function executeAdminDbQuery({
|
|
|
132
127
|
sql,
|
|
133
128
|
params = [],
|
|
134
129
|
}: ExecuteAdminDbQueryOptions): Promise<AdminDbQueryResult> {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
const bindingName = getProviderBindingName(namespace);
|
|
142
|
-
const envRecord = env as unknown as Record<string, unknown>;
|
|
143
|
-
const hyperdrive = envRecord[bindingName] as { connectionString?: string } | undefined;
|
|
144
|
-
const envKey = dbBlock.connectionString ?? `${bindingName}_URL`;
|
|
145
|
-
const connectionString = hyperdrive?.connectionString ?? (envRecord[envKey] as string | undefined);
|
|
146
|
-
if (!connectionString) {
|
|
147
|
-
throw new Error(`PostgreSQL connection '${envKey}' not found.`);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
const localDevOptions = getLocalDevPostgresExecOptions(env as unknown as Record<string, unknown>, namespace);
|
|
151
|
-
if (localDevOptions) {
|
|
152
|
-
await ensureLocalDevPostgresSchema(localDevOptions);
|
|
153
|
-
}
|
|
154
|
-
return withPostgresConnection(connectionString, async (query) => {
|
|
155
|
-
if (!localDevOptions) {
|
|
156
|
-
await ensurePgSchema(connectionString, namespace, dbBlock.tables ?? {}, query);
|
|
157
|
-
}
|
|
158
|
-
return query(sql, params);
|
|
159
|
-
}, localDevOptions);
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
if (!id && shouldRouteToD1(namespace, config)) {
|
|
163
|
-
const bindingName = getD1BindingName(namespace);
|
|
164
|
-
const d1 = (env as unknown as Record<string, unknown>)[bindingName] as D1Database | undefined;
|
|
165
|
-
if (!d1) {
|
|
166
|
-
throw new Error(`D1 binding '${bindingName}' not found.`);
|
|
167
|
-
}
|
|
168
|
-
const result = await executeD1Sql(d1, sql, params);
|
|
169
|
-
const rows = result.rows;
|
|
170
|
-
return {
|
|
171
|
-
columns: rows.length > 0 ? Object.keys(rows[0]) : [],
|
|
172
|
-
rows,
|
|
173
|
-
rowCount: result.rowCount,
|
|
174
|
-
};
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
const rows = await executeDoSql({
|
|
178
|
-
databaseNamespace: env.DATABASE,
|
|
130
|
+
return executeProviderAwareSql(
|
|
131
|
+
{
|
|
132
|
+
env,
|
|
133
|
+
config,
|
|
134
|
+
databaseNamespace: env.DATABASE,
|
|
135
|
+
},
|
|
179
136
|
namespace,
|
|
180
137
|
id,
|
|
181
|
-
|
|
138
|
+
sql,
|
|
182
139
|
params,
|
|
183
|
-
|
|
184
|
-
return {
|
|
185
|
-
columns: rows.length > 0 ? Object.keys(rows[0]) : [],
|
|
186
|
-
rows,
|
|
187
|
-
rowCount: rows.length,
|
|
188
|
-
};
|
|
140
|
+
);
|
|
189
141
|
}
|
|
190
142
|
|
|
191
143
|
export async function resolveAdminInstanceOptions({
|
|
@@ -245,7 +197,11 @@ export async function resolveAdminInstanceOptions({
|
|
|
245
197
|
const sourceLimit = clampLimit(discovery.limit, 12);
|
|
246
198
|
const effectiveLimit = Math.min(requestedLimit, sourceLimit);
|
|
247
199
|
const sourceDbBlock = config.databases?.[sourceNamespace];
|
|
248
|
-
const usesPostgres = Boolean(
|
|
200
|
+
const usesPostgres = Boolean(
|
|
201
|
+
sourceDbBlock &&
|
|
202
|
+
!isDynamicDbBlock(sourceDbBlock) &&
|
|
203
|
+
(sourceDbBlock.provider === 'neon' || sourceDbBlock.provider === 'postgres'),
|
|
204
|
+
);
|
|
249
205
|
const idField = discovery.idField ?? 'id';
|
|
250
206
|
const labelField = discovery.labelField;
|
|
251
207
|
const descriptionField = discovery.descriptionField;
|
|
@@ -254,14 +210,14 @@ export async function resolveAdminInstanceOptions({
|
|
|
254
210
|
const aliasId = '__edgebase_id';
|
|
255
211
|
const aliasLabel = '__edgebase_label';
|
|
256
212
|
const aliasDescription = '__edgebase_description';
|
|
257
|
-
const selectParts = [
|
|
258
|
-
`${quoteIdentifier(idField)} AS ${quoteIdentifier(aliasId)}`,
|
|
259
|
-
];
|
|
213
|
+
const selectParts = [`${quoteIdentifier(idField)} AS ${quoteIdentifier(aliasId)}`];
|
|
260
214
|
if (labelField) {
|
|
261
215
|
selectParts.push(`${quoteIdentifier(labelField)} AS ${quoteIdentifier(aliasLabel)}`);
|
|
262
216
|
}
|
|
263
217
|
if (descriptionField) {
|
|
264
|
-
selectParts.push(
|
|
218
|
+
selectParts.push(
|
|
219
|
+
`${quoteIdentifier(descriptionField)} AS ${quoteIdentifier(aliasDescription)}`,
|
|
220
|
+
);
|
|
265
221
|
}
|
|
266
222
|
|
|
267
223
|
const params: unknown[] = [];
|