@edge-base/server 0.1.4 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/admin-build/_app/immutable/chunks/{Bed8WcZp.js → 2nyN5wuZ.js} +1 -1
- package/admin-build/_app/immutable/chunks/{Bwq290TU.js → B-WlnirM.js} +1 -1
- package/admin-build/_app/immutable/chunks/{DYRfe1lC.js → B14gOIqE.js} +1 -1
- package/admin-build/_app/immutable/chunks/{CfT4rpr6.js → BKLsgaNT.js} +1 -1
- package/admin-build/_app/immutable/chunks/{FA-xxanK.js → BSfSfeDG.js} +1 -1
- package/admin-build/_app/immutable/chunks/{BebaNaL1.js → CN6aakgF.js} +1 -1
- package/admin-build/_app/immutable/chunks/CfPHB4r5.js +1 -0
- package/admin-build/_app/immutable/chunks/{ChX-qyfY.js → CkdaVlhQ.js} +1 -1
- package/admin-build/_app/immutable/chunks/{DMIs26Al.js → D43CH5ty.js} +1 -1
- package/admin-build/_app/immutable/chunks/{mVKEd0n6.js → D8Nrx_IG.js} +3 -3
- package/admin-build/_app/immutable/chunks/{mmI0365x.js → DP9kmlCd.js} +1 -1
- package/admin-build/_app/immutable/chunks/{Fw9oK_yh.js → DgxOZ3uv.js} +1 -1
- package/admin-build/_app/immutable/chunks/{DuldBlfT.js → DpuSetmN.js} +1 -1
- package/admin-build/_app/immutable/chunks/{B-bKFoyc.js → cqSkc6KP.js} +1 -1
- package/admin-build/_app/immutable/chunks/{6a5nHrK1.js → mD4EETH_.js} +1 -1
- package/admin-build/_app/immutable/chunks/uboHVq-x.js +1 -0
- package/admin-build/_app/immutable/entry/{app.B0Wfop7v.js → app.Dc071f6C.js} +2 -2
- package/admin-build/_app/immutable/entry/start.Bhlxoqtt.js +1 -0
- package/admin-build/_app/immutable/nodes/{0.DXd3Dmjw.js → 0.CCfcYVV2.js} +1 -1
- package/admin-build/_app/immutable/nodes/{1.DYs0DOEf.js → 1.rMaczUKT.js} +1 -1
- package/admin-build/_app/immutable/nodes/{10.C5zdvzz2.js → 10.DIOlO4hv.js} +1 -1
- package/admin-build/_app/immutable/nodes/{11.CZn94rTD.js → 11.WxD9E0Eq.js} +1 -1
- package/admin-build/_app/immutable/nodes/{12.zeHwsLsn.js → 12.CNcefK3l.js} +1 -1
- package/admin-build/_app/immutable/nodes/{13.nkWGiViq.js → 13.aAWsqDdR.js} +1 -1
- package/admin-build/_app/immutable/nodes/{14.C0cOviHd.js → 14.C9hdr3EN.js} +1 -1
- package/admin-build/_app/immutable/nodes/{15.q5eTZ0ns.js → 15.43r5uVx5.js} +1 -1
- package/admin-build/_app/immutable/nodes/{16.BrpfTEYF.js → 16.D519948J.js} +1 -1
- package/admin-build/_app/immutable/nodes/{17.3p6MQums.js → 17.ks4I4yoH.js} +1 -1
- package/admin-build/_app/immutable/nodes/{18.aj9cF39Y.js → 18.ZuNm22dY.js} +1 -1
- package/admin-build/_app/immutable/nodes/{19.BrpfTEYF.js → 19.D519948J.js} +1 -1
- package/admin-build/_app/immutable/nodes/{20.BUEsrK6V.js → 20.C9ASlwCn.js} +1 -1
- package/admin-build/_app/immutable/nodes/21.BhSD2EfX.js +1 -0
- package/admin-build/_app/immutable/nodes/{22.V2clGNAS.js → 22.6k8cg0Pr.js} +1 -1
- package/admin-build/_app/immutable/nodes/{23.3XSLKKOr.js → 23.B9hcFTU-.js} +1 -1
- package/admin-build/_app/immutable/nodes/{24.F-qrYmNi.js → 24.OsQM9QtS.js} +1 -1
- package/admin-build/_app/immutable/nodes/25.ClwkdaPp.js +2 -0
- package/admin-build/_app/immutable/nodes/{26.D0WXHf08.js → 26._-65WG0q.js} +1 -1
- package/admin-build/_app/immutable/nodes/{27.DvWhuH9-.js → 27.J1QASB3b.js} +1 -1
- package/admin-build/_app/immutable/nodes/{28.5Oz_jlro.js → 28.BKP1tVcZ.js} +1 -1
- package/admin-build/_app/immutable/nodes/{29.dDyAUaup.js → 29.mqIe62On.js} +1 -1
- package/admin-build/_app/immutable/nodes/{3.BcXkDIYV.js → 3.WkDZWDQC.js} +1 -1
- package/admin-build/_app/immutable/nodes/{30.2JCWdjTn.js → 30.BRk-4B3j.js} +1 -1
- package/admin-build/_app/immutable/nodes/{31.qG9kkJ9Q.js → 31.BBqGNVXN.js} +1 -1
- package/admin-build/_app/immutable/nodes/{4.Cl5grO75.js → 4.Bi91lv2V.js} +1 -1
- package/admin-build/_app/immutable/nodes/{5.Bel35v4W.js → 5.BumjsbNK.js} +1 -1
- package/admin-build/_app/immutable/nodes/{6.CjpfkMIg.js → 6.CMTP_7xN.js} +1 -1
- package/admin-build/_app/immutable/nodes/{7.CA1OBWip.js → 7.4T4wo7Kg.js} +1 -1
- package/admin-build/_app/immutable/nodes/{8.DS5xal_X.js → 8.MUZQPNsN.js} +1 -1
- package/admin-build/_app/immutable/nodes/{9.OYw1MaR9.js → 9.3SV00WXe.js} +1 -1
- package/admin-build/_app/version.json +1 -1
- package/admin-build/index.html +7 -7
- package/openapi.json +6710 -5866
- package/package.json +2 -2
- package/src/__tests__/functions-route.test.ts +153 -0
- package/src/__tests__/internal-request.test.ts +5 -5
- package/src/__tests__/meta-export-coverage.test.ts +3 -2
- package/src/__tests__/meta-route-registration.test.ts +3 -2
- package/src/__tests__/openapi-coverage.test.ts +5 -1
- package/src/__tests__/rate-limit.test.ts +0 -1
- package/src/__tests__/room-handler-context.test.ts +31 -0
- package/src/__tests__/room-runtime-routing.test.ts +48 -0
- package/src/__tests__/runtime-surface-accounting.test.ts +5 -4
- package/src/__tests__/smoke-skip-report.test.ts +3 -2
- package/src/durable-objects/database-do.ts +6 -1
- package/src/durable-objects/database-live-do.ts +22 -10
- package/src/durable-objects/rooms-do.ts +202 -0
- package/src/lib/internal-request.ts +5 -8
- package/src/lib/openapi.ts +1 -0
- package/src/routes/admin.ts +28 -1
- package/src/routes/auth.ts +67 -33
- package/src/routes/room.ts +42 -0
- package/src/types.ts +6 -0
- package/admin-build/_app/immutable/chunks/DSsNi9zA.js +0 -1
- package/admin-build/_app/immutable/chunks/eLBKp9m8.js +0 -1
- package/admin-build/_app/immutable/entry/start.C1a0bzUm.js +0 -1
- package/admin-build/_app/immutable/nodes/21.Bch9bUk6.js +0 -1
- package/admin-build/_app/immutable/nodes/25.BdrY4DyK.js +0 -2
|
@@ -115,6 +115,7 @@ const DEFAULT_MEMBER_RECONNECT_TIMEOUT_MS = 30000;
|
|
|
115
115
|
const SIGNAL_DENIED = Symbol('rooms.signal.denied');
|
|
116
116
|
const MEDIA_DENIED = Symbol('rooms.media.denied');
|
|
117
117
|
const WEBSOCKET_OPEN = 1;
|
|
118
|
+
const CLOUDFLARE_REALTIME_KIT_MEETING_STORAGE_KEY = 'cloudflareRealtimeKitMeetingId';
|
|
118
119
|
|
|
119
120
|
function computeStateDelta(
|
|
120
121
|
previous: Record<string, unknown>,
|
|
@@ -146,10 +147,16 @@ export class RoomsDO extends RoomRuntimeBaseDO {
|
|
|
146
147
|
private readonly memberRoles = new Map<string, string>();
|
|
147
148
|
private readonly memberMediaStates = new Map<string, RoomMemberMediaState>();
|
|
148
149
|
private readonly memberRealtimeSessions = new Map<string, RoomMemberRealtimeSession>();
|
|
150
|
+
private cloudflareRealtimeKitMeetingId: string | null = null;
|
|
151
|
+
private cloudflareRealtimeKitMeetingIdPromise: Promise<string> | null = null;
|
|
149
152
|
|
|
150
153
|
override async fetch(request: Request): Promise<Response> {
|
|
151
154
|
const url = new URL(request.url);
|
|
152
155
|
|
|
156
|
+
if (url.pathname === '/media/cloudflare_realtimekit/session' && request.method === 'POST') {
|
|
157
|
+
return this.handleCloudflareRealtimeKitSessionCreate(request, url);
|
|
158
|
+
}
|
|
159
|
+
|
|
153
160
|
if (url.pathname === '/media/realtime/session') {
|
|
154
161
|
if (request.method === 'POST') return this.handleRealtimeSessionCreate(request, url);
|
|
155
162
|
if (request.method === 'GET') return this.handleRealtimeSessionGet(request, url);
|
|
@@ -175,6 +182,53 @@ export class RoomsDO extends RoomRuntimeBaseDO {
|
|
|
175
182
|
return super.fetch(request);
|
|
176
183
|
}
|
|
177
184
|
|
|
185
|
+
private async handleCloudflareRealtimeKitSessionCreate(request: Request, url: URL): Promise<Response> {
|
|
186
|
+
try {
|
|
187
|
+
const body = await this.readJsonBody<{
|
|
188
|
+
connectionId?: string;
|
|
189
|
+
customParticipantId?: string;
|
|
190
|
+
name?: string;
|
|
191
|
+
picture?: string;
|
|
192
|
+
}>(request);
|
|
193
|
+
const { memberId, connectionId, meta } = await this.authenticateRealtimeRequest(
|
|
194
|
+
request,
|
|
195
|
+
url,
|
|
196
|
+
typeof body.connectionId === 'string' ? body.connectionId : undefined,
|
|
197
|
+
);
|
|
198
|
+
if (this.hasPublishedTracks(memberId)) {
|
|
199
|
+
return this.jsonResponse(409, {
|
|
200
|
+
code: 409,
|
|
201
|
+
message: 'Unpublish existing room media before creating a new Cloudflare RealtimeKit session',
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const config = this.getCloudflareRealtimeKitConfig();
|
|
206
|
+
const meetingId = await this.ensureCloudflareRealtimeKitMeetingId(config);
|
|
207
|
+
const participant = await this.createCloudflareRealtimeKitParticipant(config, meetingId, {
|
|
208
|
+
customParticipantId: this.buildCloudflareRealtimeKitParticipantId(memberId, body.customParticipantId),
|
|
209
|
+
name: typeof body.name === 'string' && body.name.trim()
|
|
210
|
+
? body.name.trim()
|
|
211
|
+
: meta.auth?.email ?? meta.userId ?? memberId,
|
|
212
|
+
picture: typeof body.picture === 'string' && body.picture.trim() ? body.picture.trim() : undefined,
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
return this.jsonResponse(200, {
|
|
216
|
+
sessionId: participant.id,
|
|
217
|
+
meetingId,
|
|
218
|
+
participantId: participant.id,
|
|
219
|
+
authToken: participant.token,
|
|
220
|
+
presetName: participant.presetName ?? config.presetName,
|
|
221
|
+
connectionId,
|
|
222
|
+
reused: false,
|
|
223
|
+
});
|
|
224
|
+
} catch (err) {
|
|
225
|
+
return this.jsonResponse(400, {
|
|
226
|
+
code: 400,
|
|
227
|
+
message: err instanceof Error ? err.message : 'Failed to create Cloudflare RealtimeKit session',
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
178
232
|
private async handleRealtimeSessionCreate(request: Request, url: URL): Promise<Response> {
|
|
179
233
|
try {
|
|
180
234
|
const body = await this.readJsonBody<{
|
|
@@ -422,6 +476,154 @@ export class RoomsDO extends RoomRuntimeBaseDO {
|
|
|
422
476
|
return createCloudflareRealtimeClient(this.env as unknown as Env);
|
|
423
477
|
}
|
|
424
478
|
|
|
479
|
+
private getCloudflareRealtimeKitConfig(): {
|
|
480
|
+
accountId: string;
|
|
481
|
+
apiToken: string;
|
|
482
|
+
appId: string;
|
|
483
|
+
presetName: string;
|
|
484
|
+
} {
|
|
485
|
+
const env = this.env as unknown as Env;
|
|
486
|
+
const accountId = env.CF_ACCOUNT_ID?.trim();
|
|
487
|
+
const apiToken = env.CF_API_TOKEN?.trim();
|
|
488
|
+
const appId = env.CF_REALTIME_APP_ID?.trim();
|
|
489
|
+
const presetName = env.CF_REALTIME_PRESET_NAME?.trim() || 'group_call_participant';
|
|
490
|
+
|
|
491
|
+
if (!accountId || !apiToken || !appId) {
|
|
492
|
+
throw new Error(
|
|
493
|
+
'Cloudflare Realtime is not configured. Set CF_ACCOUNT_ID, CF_API_TOKEN, and CF_REALTIME_APP_ID.',
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
return { accountId, apiToken, appId, presetName };
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
private buildCloudflareRealtimeKitParticipantId(memberId: string, provided?: string): string {
|
|
501
|
+
const trimmed = typeof provided === 'string' ? provided.trim() : '';
|
|
502
|
+
if (trimmed) {
|
|
503
|
+
return trimmed;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
return [
|
|
507
|
+
this.namespace ?? 'room',
|
|
508
|
+
this.roomId ?? 'unknown',
|
|
509
|
+
memberId,
|
|
510
|
+
Date.now().toString(36),
|
|
511
|
+
].join(':');
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
private async ensureCloudflareRealtimeKitMeetingId(config: {
|
|
515
|
+
accountId: string;
|
|
516
|
+
apiToken: string;
|
|
517
|
+
appId: string;
|
|
518
|
+
}): Promise<string> {
|
|
519
|
+
if (this.cloudflareRealtimeKitMeetingId) {
|
|
520
|
+
return this.cloudflareRealtimeKitMeetingId;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
if (this.cloudflareRealtimeKitMeetingIdPromise) {
|
|
524
|
+
return this.cloudflareRealtimeKitMeetingIdPromise;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
this.cloudflareRealtimeKitMeetingIdPromise = (async () => {
|
|
528
|
+
const storedMeetingId = await this.ctx.storage.get<string>(CLOUDFLARE_REALTIME_KIT_MEETING_STORAGE_KEY);
|
|
529
|
+
if (storedMeetingId) {
|
|
530
|
+
this.cloudflareRealtimeKitMeetingId = storedMeetingId;
|
|
531
|
+
return storedMeetingId;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
const response = await fetch(
|
|
535
|
+
`https://api.cloudflare.com/client/v4/accounts/${encodeURIComponent(config.accountId)}`
|
|
536
|
+
+ `/realtime/kit/${encodeURIComponent(config.appId)}/meetings`,
|
|
537
|
+
{
|
|
538
|
+
method: 'POST',
|
|
539
|
+
headers: {
|
|
540
|
+
Authorization: `Bearer ${config.apiToken}`,
|
|
541
|
+
'Content-Type': 'application/json',
|
|
542
|
+
},
|
|
543
|
+
body: JSON.stringify({
|
|
544
|
+
title: `${this.namespace ?? 'room'}::${this.roomId ?? 'unknown'}`,
|
|
545
|
+
}),
|
|
546
|
+
},
|
|
547
|
+
);
|
|
548
|
+
const data = await this.parseCloudflareApiEnvelope<{ id: string }>(response);
|
|
549
|
+
this.cloudflareRealtimeKitMeetingId = data.id;
|
|
550
|
+
await this.ctx.storage.put(CLOUDFLARE_REALTIME_KIT_MEETING_STORAGE_KEY, data.id);
|
|
551
|
+
return data.id;
|
|
552
|
+
})();
|
|
553
|
+
|
|
554
|
+
try {
|
|
555
|
+
return await this.cloudflareRealtimeKitMeetingIdPromise;
|
|
556
|
+
} finally {
|
|
557
|
+
this.cloudflareRealtimeKitMeetingIdPromise = null;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
private async createCloudflareRealtimeKitParticipant(
|
|
562
|
+
config: {
|
|
563
|
+
accountId: string;
|
|
564
|
+
apiToken: string;
|
|
565
|
+
appId: string;
|
|
566
|
+
presetName: string;
|
|
567
|
+
},
|
|
568
|
+
meetingId: string,
|
|
569
|
+
payload: {
|
|
570
|
+
customParticipantId: string;
|
|
571
|
+
name?: string;
|
|
572
|
+
picture?: string;
|
|
573
|
+
},
|
|
574
|
+
): Promise<{ id: string; token: string; presetName?: string }> {
|
|
575
|
+
const response = await fetch(
|
|
576
|
+
`https://api.cloudflare.com/client/v4/accounts/${encodeURIComponent(config.accountId)}`
|
|
577
|
+
+ `/realtime/kit/${encodeURIComponent(config.appId)}`
|
|
578
|
+
+ `/meetings/${encodeURIComponent(meetingId)}/participants`,
|
|
579
|
+
{
|
|
580
|
+
method: 'POST',
|
|
581
|
+
headers: {
|
|
582
|
+
Authorization: `Bearer ${config.apiToken}`,
|
|
583
|
+
'Content-Type': 'application/json',
|
|
584
|
+
},
|
|
585
|
+
body: JSON.stringify({
|
|
586
|
+
custom_participant_id: payload.customParticipantId,
|
|
587
|
+
preset_name: config.presetName,
|
|
588
|
+
name: payload.name,
|
|
589
|
+
picture: payload.picture,
|
|
590
|
+
}),
|
|
591
|
+
},
|
|
592
|
+
);
|
|
593
|
+
|
|
594
|
+
const data = await this.parseCloudflareApiEnvelope<{
|
|
595
|
+
id: string;
|
|
596
|
+
token: string;
|
|
597
|
+
preset_name?: string;
|
|
598
|
+
}>(response);
|
|
599
|
+
|
|
600
|
+
return {
|
|
601
|
+
id: data.id,
|
|
602
|
+
token: data.token,
|
|
603
|
+
presetName: data.preset_name,
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
private async parseCloudflareApiEnvelope<T>(response: Response): Promise<T> {
|
|
608
|
+
const payload = (await response.json().catch(() => ({}))) as {
|
|
609
|
+
success?: boolean;
|
|
610
|
+
errors?: Array<{ message?: string }>;
|
|
611
|
+
messages?: Array<{ message?: string }>;
|
|
612
|
+
result?: T;
|
|
613
|
+
data?: T;
|
|
614
|
+
};
|
|
615
|
+
|
|
616
|
+
if (!response.ok || payload.success === false) {
|
|
617
|
+
const message =
|
|
618
|
+
payload.errors?.find((entry) => typeof entry.message === 'string')?.message
|
|
619
|
+
?? payload.messages?.find((entry) => typeof entry.message === 'string')?.message
|
|
620
|
+
?? `Cloudflare API request failed (${response.status})`;
|
|
621
|
+
throw new Error(message);
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
return (payload.data ?? payload.result ?? {}) as T;
|
|
625
|
+
}
|
|
626
|
+
|
|
425
627
|
private async authenticateRealtimeRequest(
|
|
426
628
|
request: Request,
|
|
427
629
|
url: URL,
|
|
@@ -2,17 +2,14 @@ import type { Context } from 'hono';
|
|
|
2
2
|
import type { HonoEnv } from './hono.js';
|
|
3
3
|
import type { Env } from '../types.js';
|
|
4
4
|
|
|
5
|
-
export function isTrustedInternalRequestUrl(
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
} catch {
|
|
10
|
-
return false;
|
|
11
|
-
}
|
|
5
|
+
export function isTrustedInternalRequestUrl(_url: string): boolean {
|
|
6
|
+
// Requests are only trusted when the runtime marks them internal via context.
|
|
7
|
+
// URL hosts (including 'do' / 'internal') can be spoofed in self-hosted or proxy setups.
|
|
8
|
+
return false;
|
|
12
9
|
}
|
|
13
10
|
|
|
14
11
|
export function isTrustedInternalContext(c: Pick<Context<HonoEnv>, 'get' | 'req'>): boolean {
|
|
15
|
-
return c.get('isInternalRequest' as never) === true
|
|
12
|
+
return c.get('isInternalRequest' as never) === true;
|
|
16
13
|
}
|
|
17
14
|
|
|
18
15
|
export function buildInternalHandlerContext(options: {
|
package/src/lib/openapi.ts
CHANGED
package/src/routes/admin.ts
CHANGED
|
@@ -119,6 +119,15 @@ function quoteSqlIdentifier(identifier: string): string {
|
|
|
119
119
|
return `"${identifier}"`;
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
+
function isPublicAdminSetupAllowed(env: Env): boolean {
|
|
123
|
+
try {
|
|
124
|
+
const config = parseConfig(env);
|
|
125
|
+
return config.release !== true;
|
|
126
|
+
} catch {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
122
131
|
interface MonitoringStats {
|
|
123
132
|
subsystem?: string;
|
|
124
133
|
activeConnections: number;
|
|
@@ -361,7 +370,16 @@ const adminSetupStatus = createRoute({
|
|
|
361
370
|
adminRoute.openapi(adminSetupStatus, async (c) => {
|
|
362
371
|
await ensureAuthSchema(getAuthDb(c));
|
|
363
372
|
const exists = await adminExists(getAuthDb(c));
|
|
364
|
-
|
|
373
|
+
const needsSetup = !exists;
|
|
374
|
+
const publicSetupAllowed = needsSetup ? isPublicAdminSetupAllowed(c.env) : false;
|
|
375
|
+
return c.json({
|
|
376
|
+
needsSetup,
|
|
377
|
+
publicSetupAllowed,
|
|
378
|
+
setupMethod: needsSetup ? (publicSetupAllowed ? 'browser' : 'cli') : 'login',
|
|
379
|
+
message: needsSetup && !publicSetupAllowed
|
|
380
|
+
? 'Public admin setup is disabled for this deployment. Run `npx edgebase admin bootstrap` with a Service Key, or use the deploy/docker bootstrap flow instead.'
|
|
381
|
+
: undefined,
|
|
382
|
+
});
|
|
365
383
|
});
|
|
366
384
|
|
|
367
385
|
// POST /admin/api/setup — create the first admin account
|
|
@@ -387,6 +405,7 @@ const adminSetup = createRoute({
|
|
|
387
405
|
responses: {
|
|
388
406
|
201: { description: 'Admin created', content: { 'application/json': { schema: jsonResponseSchema } } },
|
|
389
407
|
400: { description: 'Bad request', content: { 'application/json': { schema: errorResponseSchema } } },
|
|
408
|
+
403: { description: 'Forbidden', content: { 'application/json': { schema: errorResponseSchema } } },
|
|
390
409
|
},
|
|
391
410
|
});
|
|
392
411
|
|
|
@@ -394,6 +413,14 @@ adminRoute.openapi(adminSetup, async (c) => {
|
|
|
394
413
|
await ensureAuthSchema(getAuthDb(c));
|
|
395
414
|
const exists = await adminExists(getAuthDb(c));
|
|
396
415
|
if (exists) throw new EdgeBaseError(400, 'Admin account already exists. Use login instead.', undefined, 'already-exists');
|
|
416
|
+
if (!isPublicAdminSetupAllowed(c.env)) {
|
|
417
|
+
throw new EdgeBaseError(
|
|
418
|
+
403,
|
|
419
|
+
'Public admin setup is disabled for this deployment. Run `npx edgebase admin bootstrap` with a Service Key, or use the deploy/docker bootstrap flow instead.',
|
|
420
|
+
undefined,
|
|
421
|
+
'forbidden',
|
|
422
|
+
);
|
|
423
|
+
}
|
|
397
424
|
|
|
398
425
|
const body = await c.req.json<{ email: string; password: string }>();
|
|
399
426
|
if (!body.email || !body.password) throw new EdgeBaseError(400, 'Email and password are required.', undefined, 'validation-failed');
|
package/src/routes/auth.ts
CHANGED
|
@@ -29,7 +29,7 @@ import {
|
|
|
29
29
|
} from '../lib/auth-redirect.js';
|
|
30
30
|
import {
|
|
31
31
|
signAccessToken, signRefreshToken, verifyRefreshTokenWithFallback,
|
|
32
|
-
parseDuration,
|
|
32
|
+
parseDuration, TokenExpiredError,
|
|
33
33
|
} from '../lib/jwt.js';
|
|
34
34
|
import { generateId } from '../lib/uuid.js';
|
|
35
35
|
import { captchaMiddleware } from '../middleware/captcha-verify.js';
|
|
@@ -108,6 +108,15 @@ authRoute.onError((err, c) => {
|
|
|
108
108
|
|
|
109
109
|
// ─── Helpers ───
|
|
110
110
|
|
|
111
|
+
function isReleaseMode(env: Env): boolean {
|
|
112
|
+
return parseConfig(env)?.release ?? false;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function shouldExposeAuthTestSecrets(env: Env): boolean {
|
|
116
|
+
return env.EDGEBASE_TEST === '1'
|
|
117
|
+
|| env.EDGEBASE_TEST === 'true';
|
|
118
|
+
}
|
|
119
|
+
|
|
111
120
|
function requireAuth(auth: AuthContext | null): string {
|
|
112
121
|
if (!auth) {
|
|
113
122
|
throw new EdgeBaseError(401, 'Authentication required.', undefined, 'unauthenticated');
|
|
@@ -1288,6 +1297,7 @@ authRoute.openapi(signinMagicLink, async (c) => {
|
|
|
1288
1297
|
const record = await lookupEmail(getAuthDb(c), body.email);
|
|
1289
1298
|
|
|
1290
1299
|
const db = getAuthDb(c);
|
|
1300
|
+
const exposeTestSecrets = shouldExposeAuthTestSecrets(c.env);
|
|
1291
1301
|
let debugToken: string | undefined;
|
|
1292
1302
|
let debugActionUrl: string | undefined;
|
|
1293
1303
|
|
|
@@ -1327,9 +1337,12 @@ authRoute.openapi(signinMagicLink, async (c) => {
|
|
|
1327
1337
|
type: 'magic-link',
|
|
1328
1338
|
state: redirect.state,
|
|
1329
1339
|
});
|
|
1340
|
+
if (exposeTestSecrets) {
|
|
1341
|
+
debugToken = token;
|
|
1342
|
+
debugActionUrl = magicLinkUrl;
|
|
1343
|
+
}
|
|
1330
1344
|
if (!provider) {
|
|
1331
|
-
|
|
1332
|
-
if (!release) {
|
|
1345
|
+
if (!isReleaseMode(c.env)) {
|
|
1333
1346
|
console.warn('[MagicLink] Email provider not configured. Token:', token);
|
|
1334
1347
|
debugToken = token;
|
|
1335
1348
|
debugActionUrl = magicLinkUrl;
|
|
@@ -1417,6 +1430,10 @@ authRoute.openapi(signinMagicLink, async (c) => {
|
|
|
1417
1430
|
type: 'magic-link',
|
|
1418
1431
|
state: redirect.state,
|
|
1419
1432
|
});
|
|
1433
|
+
if (exposeTestSecrets) {
|
|
1434
|
+
debugToken = token;
|
|
1435
|
+
debugActionUrl = magicLinkUrl;
|
|
1436
|
+
}
|
|
1420
1437
|
if (provider) {
|
|
1421
1438
|
const locale = resolveEmailLocale(c.env, reqLocale);
|
|
1422
1439
|
const html = renderMagicLink({
|
|
@@ -1431,8 +1448,7 @@ authRoute.openapi(signinMagicLink, async (c) => {
|
|
|
1431
1448
|
resolveSubject(c.env, 'magicLink', defaultSubject, locale), html, locale,
|
|
1432
1449
|
).catch(() => {});
|
|
1433
1450
|
} else {
|
|
1434
|
-
|
|
1435
|
-
if (!release) {
|
|
1451
|
+
if (!isReleaseMode(c.env)) {
|
|
1436
1452
|
debugToken = token;
|
|
1437
1453
|
debugActionUrl = magicLinkUrl;
|
|
1438
1454
|
}
|
|
@@ -1591,6 +1607,7 @@ authRoute.openapi(signinPhone, async (c) => {
|
|
|
1591
1607
|
// Look up phone in D1
|
|
1592
1608
|
const record = await lookupPhone(getAuthDb(c), phone);
|
|
1593
1609
|
|
|
1610
|
+
const exposeTestSecrets = shouldExposeAuthTestSecrets(c.env);
|
|
1594
1611
|
let devCode: string | undefined;
|
|
1595
1612
|
|
|
1596
1613
|
const db = getAuthDb(c);
|
|
@@ -1602,6 +1619,7 @@ authRoute.openapi(signinPhone, async (c) => {
|
|
|
1602
1619
|
if (!user) return c.json({ ok: true });
|
|
1603
1620
|
|
|
1604
1621
|
const code = generateOTP();
|
|
1622
|
+
if (exposeTestSecrets) devCode = code;
|
|
1605
1623
|
|
|
1606
1624
|
// Store OTP in KV with 5 min TTL
|
|
1607
1625
|
await c.env.KV.put(
|
|
@@ -1623,8 +1641,7 @@ authRoute.openapi(signinPhone, async (c) => {
|
|
|
1623
1641
|
`Your ${appName} verification code is: ${code}. Valid for 5 minutes.`,
|
|
1624
1642
|
);
|
|
1625
1643
|
} else {
|
|
1626
|
-
|
|
1627
|
-
if (!release) {
|
|
1644
|
+
if (!isReleaseMode(c.env)) {
|
|
1628
1645
|
console.warn('[Phone] SMS provider not configured. OTP:', code);
|
|
1629
1646
|
devCode = code;
|
|
1630
1647
|
}
|
|
@@ -1654,6 +1671,7 @@ authRoute.openapi(signinPhone, async (c) => {
|
|
|
1654
1671
|
await authService.updateUser(db, userId, { phone, phoneVerified: false });
|
|
1655
1672
|
|
|
1656
1673
|
const code = generateOTP();
|
|
1674
|
+
if (exposeTestSecrets) devCode = code;
|
|
1657
1675
|
|
|
1658
1676
|
// Store OTP in KV
|
|
1659
1677
|
await c.env.KV.put(
|
|
@@ -1675,8 +1693,7 @@ authRoute.openapi(signinPhone, async (c) => {
|
|
|
1675
1693
|
`Your ${appName} verification code is: ${code}. Valid for 5 minutes.`,
|
|
1676
1694
|
);
|
|
1677
1695
|
} else {
|
|
1678
|
-
|
|
1679
|
-
if (!release) {
|
|
1696
|
+
if (!isReleaseMode(c.env)) {
|
|
1680
1697
|
console.warn('[Phone] SMS provider not configured. OTP:', code);
|
|
1681
1698
|
devCode = code;
|
|
1682
1699
|
}
|
|
@@ -1690,8 +1707,7 @@ authRoute.openapi(signinPhone, async (c) => {
|
|
|
1690
1707
|
}
|
|
1691
1708
|
|
|
1692
1709
|
// Return OTP code only in dev mode (SMS provider not configured) for testing
|
|
1693
|
-
|
|
1694
|
-
return c.json(devCode && !release ? { ok: true, code: devCode } : { ok: true });
|
|
1710
|
+
return c.json(devCode ? { ok: true, code: devCode } : { ok: true });
|
|
1695
1711
|
});
|
|
1696
1712
|
|
|
1697
1713
|
// POST /verify-phone — verify OTP → create session
|
|
@@ -1871,6 +1887,7 @@ authRoute.openapi(linkPhone, async (c) => {
|
|
|
1871
1887
|
}
|
|
1872
1888
|
|
|
1873
1889
|
const code = generateOTP();
|
|
1890
|
+
const exposeTestSecrets = shouldExposeAuthTestSecrets(c.env);
|
|
1874
1891
|
|
|
1875
1892
|
// Store link OTP in KV (separate key pattern)
|
|
1876
1893
|
await c.env.KV.put(
|
|
@@ -1891,14 +1908,13 @@ authRoute.openapi(linkPhone, async (c) => {
|
|
|
1891
1908
|
phone,
|
|
1892
1909
|
`Your ${appName} phone linking code is: ${code}. Valid for 5 minutes.`,
|
|
1893
1910
|
);
|
|
1894
|
-
return c.json({ ok: true });
|
|
1911
|
+
return c.json(exposeTestSecrets ? { ok: true, code } : { ok: true });
|
|
1895
1912
|
} else {
|
|
1896
|
-
|
|
1897
|
-
if (!release) {
|
|
1913
|
+
if (!isReleaseMode(c.env)) {
|
|
1898
1914
|
console.warn('[Phone] SMS provider not configured. Link OTP:', code);
|
|
1899
1915
|
return c.json({ ok: true, code });
|
|
1900
1916
|
}
|
|
1901
|
-
return c.json({ ok: true });
|
|
1917
|
+
return c.json(exposeTestSecrets ? { ok: true, code } : { ok: true });
|
|
1902
1918
|
}
|
|
1903
1919
|
});
|
|
1904
1920
|
|
|
@@ -2048,6 +2064,7 @@ authRoute.openapi(signinEmailOtp, async (c) => {
|
|
|
2048
2064
|
// Look up email in D1
|
|
2049
2065
|
const record = await lookupEmail(getAuthDb(c), email);
|
|
2050
2066
|
|
|
2067
|
+
const exposeTestSecrets = shouldExposeAuthTestSecrets(c.env);
|
|
2051
2068
|
let devCode: string | undefined;
|
|
2052
2069
|
|
|
2053
2070
|
const db = getAuthDb(c);
|
|
@@ -2059,6 +2076,7 @@ authRoute.openapi(signinEmailOtp, async (c) => {
|
|
|
2059
2076
|
if (!user) return c.json({ ok: true });
|
|
2060
2077
|
|
|
2061
2078
|
const code = generateOTP();
|
|
2079
|
+
if (exposeTestSecrets) devCode = code;
|
|
2062
2080
|
|
|
2063
2081
|
// Store OTP in KV with 5 min TTL
|
|
2064
2082
|
await c.env.KV.put(
|
|
@@ -2079,8 +2097,7 @@ authRoute.openapi(signinEmailOtp, async (c) => {
|
|
|
2079
2097
|
resolveSubject(c.env, 'emailOtp', defaultSubject, locale), html, locale,
|
|
2080
2098
|
);
|
|
2081
2099
|
} else {
|
|
2082
|
-
|
|
2083
|
-
if (!release) {
|
|
2100
|
+
if (!isReleaseMode(c.env)) {
|
|
2084
2101
|
console.warn('[EmailOTP] Email provider not configured. OTP:', code);
|
|
2085
2102
|
devCode = code;
|
|
2086
2103
|
}
|
|
@@ -2117,6 +2134,7 @@ authRoute.openapi(signinEmailOtp, async (c) => {
|
|
|
2117
2134
|
});
|
|
2118
2135
|
|
|
2119
2136
|
const code = generateOTP();
|
|
2137
|
+
if (exposeTestSecrets) devCode = code;
|
|
2120
2138
|
|
|
2121
2139
|
// Store OTP in KV
|
|
2122
2140
|
await c.env.KV.put(
|
|
@@ -2137,8 +2155,7 @@ authRoute.openapi(signinEmailOtp, async (c) => {
|
|
|
2137
2155
|
resolveSubject(c.env, 'emailOtp', defaultSubject, locale), html, locale,
|
|
2138
2156
|
);
|
|
2139
2157
|
} else {
|
|
2140
|
-
|
|
2141
|
-
if (!release) {
|
|
2158
|
+
if (!isReleaseMode(c.env)) {
|
|
2142
2159
|
console.warn('[EmailOTP] Email provider not configured. OTP:', code);
|
|
2143
2160
|
devCode = code;
|
|
2144
2161
|
}
|
|
@@ -2152,8 +2169,7 @@ authRoute.openapi(signinEmailOtp, async (c) => {
|
|
|
2152
2169
|
}
|
|
2153
2170
|
|
|
2154
2171
|
// Return OTP code only in dev mode (email provider not configured) for testing
|
|
2155
|
-
|
|
2156
|
-
return c.json(devCode && !release ? { ok: true, code: devCode } : { ok: true });
|
|
2172
|
+
return c.json(devCode ? { ok: true, code: devCode } : { ok: true });
|
|
2157
2173
|
});
|
|
2158
2174
|
|
|
2159
2175
|
// POST /verify-email-otp — verify OTP → create session
|
|
@@ -2754,11 +2770,27 @@ authRoute.openapi(signout, async (c) => {
|
|
|
2754
2770
|
refreshToken: body.refreshToken,
|
|
2755
2771
|
});
|
|
2756
2772
|
|
|
2757
|
-
const payload = decodeTokenUnsafe(body.refreshToken);
|
|
2758
|
-
if (!payload?.sub) throw new EdgeBaseError(401, 'Invalid refresh token.', undefined, 'invalid-refresh-token');
|
|
2759
|
-
|
|
2760
2773
|
const db = getAuthDb(c);
|
|
2761
|
-
|
|
2774
|
+
let tokenPayload;
|
|
2775
|
+
try {
|
|
2776
|
+
tokenPayload = await verifyRefreshTokenWithFallback(
|
|
2777
|
+
body.refreshToken,
|
|
2778
|
+
getUserSecret(c.env),
|
|
2779
|
+
c.env.JWT_USER_SECRET_OLD,
|
|
2780
|
+
c.env.JWT_USER_SECRET_OLD_AT,
|
|
2781
|
+
);
|
|
2782
|
+
} catch (err) {
|
|
2783
|
+
if (err instanceof TokenExpiredError) {
|
|
2784
|
+
throw new EdgeBaseError(401, 'Refresh token expired.', undefined, 'refresh-token-expired');
|
|
2785
|
+
}
|
|
2786
|
+
throw new EdgeBaseError(401, 'Invalid refresh token.', undefined, 'invalid-refresh-token');
|
|
2787
|
+
}
|
|
2788
|
+
|
|
2789
|
+
const userId = tokenPayload.sub;
|
|
2790
|
+
const sessionResult = await authService.getSessionByRefreshToken(db, body.refreshToken, userId);
|
|
2791
|
+
if (!sessionResult) {
|
|
2792
|
+
throw new EdgeBaseError(401, 'Invalid refresh token.', undefined, 'invalid-refresh-token');
|
|
2793
|
+
}
|
|
2762
2794
|
|
|
2763
2795
|
// beforeSignOut hook — blocking
|
|
2764
2796
|
await executeAuthHook(c.env, c.executionCtx, 'beforeSignOut', { userId }, { blocking: true, workerUrl: getWorkerUrl(c.req.url, c.env) });
|
|
@@ -2991,8 +3023,8 @@ authRoute.openapi(changeEmail, async (c) => {
|
|
|
2991
3023
|
}
|
|
2992
3024
|
}
|
|
2993
3025
|
|
|
2994
|
-
const
|
|
2995
|
-
if (!
|
|
3026
|
+
const exposeTestSecrets = shouldExposeAuthTestSecrets(c.env);
|
|
3027
|
+
if (exposeTestSecrets || !isReleaseMode(c.env)) {
|
|
2996
3028
|
const emailCfg = getEmailConfig(c.env);
|
|
2997
3029
|
const fallbackVerifyUrl = emailCfg?.emailChangeUrl
|
|
2998
3030
|
? emailCfg.emailChangeUrl.replace('{token}', token)
|
|
@@ -3970,8 +4002,7 @@ authRoute.openapi(requestEmailVerification, async (c) => {
|
|
|
3970
4002
|
|
|
3971
4003
|
const provider = createEmailProvider(getEmailConfig(c.env), c.env);
|
|
3972
4004
|
if (!provider) {
|
|
3973
|
-
|
|
3974
|
-
if (!release) {
|
|
4005
|
+
if (shouldExposeAuthTestSecrets(c.env) || !isReleaseMode(c.env)) {
|
|
3975
4006
|
console.warn('[VerifyEmail] Email provider not configured. Verification email not sent. Token:', token);
|
|
3976
4007
|
return c.json({ ok: true, message: 'Email provider not configured.', token, actionUrl: verifyUrl });
|
|
3977
4008
|
}
|
|
@@ -3992,7 +4023,9 @@ authRoute.openapi(requestEmailVerification, async (c) => {
|
|
|
3992
4023
|
resolveSubject(c.env, 'verification', defaultSubject, locale), html, locale,
|
|
3993
4024
|
);
|
|
3994
4025
|
|
|
3995
|
-
return c.json(
|
|
4026
|
+
return c.json(shouldExposeAuthTestSecrets(c.env)
|
|
4027
|
+
? { ok: result.success, messageId: result.messageId, token, actionUrl: verifyUrl }
|
|
4028
|
+
: { ok: result.success, messageId: result.messageId });
|
|
3996
4029
|
});
|
|
3997
4030
|
|
|
3998
4031
|
// POST /verify-email — KV token→shardId lookup → direct Shard call
|
|
@@ -4123,8 +4156,7 @@ authRoute.openapi(requestPasswordReset, async (c) => {
|
|
|
4123
4156
|
|
|
4124
4157
|
const provider = createEmailProvider(getEmailConfig(c.env), c.env);
|
|
4125
4158
|
if (!provider) {
|
|
4126
|
-
|
|
4127
|
-
if (!release) {
|
|
4159
|
+
if (shouldExposeAuthTestSecrets(c.env) || !isReleaseMode(c.env)) {
|
|
4128
4160
|
console.warn('[Auth] Email provider not configured. Reset email not sent. Token:', token);
|
|
4129
4161
|
return c.json({ ok: true, message: 'Email provider not configured.', token, actionUrl: resetUrl });
|
|
4130
4162
|
}
|
|
@@ -4145,7 +4177,9 @@ authRoute.openapi(requestPasswordReset, async (c) => {
|
|
|
4145
4177
|
resolveSubject(c.env, 'passwordReset', defaultSubject, locale), html, locale,
|
|
4146
4178
|
);
|
|
4147
4179
|
|
|
4148
|
-
return c.json(
|
|
4180
|
+
return c.json(shouldExposeAuthTestSecrets(c.env)
|
|
4181
|
+
? { ok: result.success, messageId: result.messageId, token, actionUrl: resetUrl }
|
|
4182
|
+
: { ok: result.success, messageId: result.messageId });
|
|
4149
4183
|
});
|
|
4150
4184
|
|
|
4151
4185
|
// POST /reset-password — KV token→shardId lookup → direct Shard call
|
package/src/routes/room.ts
CHANGED
|
@@ -70,6 +70,12 @@ const roomRealtimeCreateSessionBodySchema = z.object({
|
|
|
70
70
|
thirdparty: z.boolean().optional().openapi({ description: 'Forward Cloudflare Realtime thirdparty mode' }),
|
|
71
71
|
sessionDescription: roomRealtimeSessionDescriptionSchema.optional(),
|
|
72
72
|
});
|
|
73
|
+
const roomCloudflareRealtimeKitCreateSessionBodySchema = z.object({
|
|
74
|
+
connectionId: z.string().optional().openapi({ description: 'Specific room connection ID to bind the Cloudflare RealtimeKit participant to' }),
|
|
75
|
+
customParticipantId: z.string().optional().openapi({ description: 'Optional custom participant identifier for the provisioned RealtimeKit participant' }),
|
|
76
|
+
name: z.string().optional().openapi({ description: 'Optional display name for the provisioned RealtimeKit participant' }),
|
|
77
|
+
picture: z.string().optional().openapi({ description: 'Optional avatar URL for the provisioned RealtimeKit participant' }),
|
|
78
|
+
});
|
|
73
79
|
const roomRealtimeCreateSessionResponseSchema = z.object({
|
|
74
80
|
sessionId: z.string().openapi({ description: 'Realtime provider session ID' }),
|
|
75
81
|
sessionDescription: roomRealtimeSessionDescriptionSchema.optional(),
|
|
@@ -78,6 +84,15 @@ const roomRealtimeCreateSessionResponseSchema = z.object({
|
|
|
78
84
|
connectionId: z.string().optional().openapi({ description: 'Room connection ID associated with the session' }),
|
|
79
85
|
reused: z.boolean().optional().openapi({ description: 'Whether an existing provider session was reused' }),
|
|
80
86
|
});
|
|
87
|
+
const roomCloudflareRealtimeKitCreateSessionResponseSchema = z.object({
|
|
88
|
+
sessionId: z.string().openapi({ description: 'Cloudflare RealtimeKit participant ID' }),
|
|
89
|
+
meetingId: z.string().openapi({ description: 'Cloudflare RealtimeKit meeting ID backing the room session' }),
|
|
90
|
+
participantId: z.string().openapi({ description: 'Cloudflare RealtimeKit participant ID' }),
|
|
91
|
+
authToken: z.string().openapi({ description: 'RealtimeKit auth token for the provisioned participant' }),
|
|
92
|
+
presetName: z.string().optional().openapi({ description: 'RealtimeKit preset used for the provisioned participant' }),
|
|
93
|
+
connectionId: z.string().optional().openapi({ description: 'Room connection ID associated with the session' }),
|
|
94
|
+
reused: z.boolean().optional().openapi({ description: 'Whether an existing provider participant was reused' }),
|
|
95
|
+
});
|
|
81
96
|
const roomRealtimeSessionStateSchema = z.object({
|
|
82
97
|
sessionId: z.string().openapi({ description: 'Realtime provider session ID' }),
|
|
83
98
|
connectionId: z.string().optional().openapi({ description: 'Room connection ID associated with the session' }),
|
|
@@ -525,6 +540,27 @@ const createRoomRealtimeSession = createRoute({
|
|
|
525
540
|
},
|
|
526
541
|
});
|
|
527
542
|
|
|
543
|
+
const createRoomCloudflareRealtimeKitSession = createRoute({
|
|
544
|
+
operationId: 'createRoomCloudflareRealtimeKitSession',
|
|
545
|
+
method: 'post',
|
|
546
|
+
path: '/media/cloudflare_realtimekit/session',
|
|
547
|
+
tags: ['client'],
|
|
548
|
+
summary: 'Create a room Cloudflare RealtimeKit session',
|
|
549
|
+
description: 'Creates a Cloudflare RealtimeKit session for the authenticated room member.',
|
|
550
|
+
request: {
|
|
551
|
+
query: roomQuerySchema,
|
|
552
|
+
body: { content: { 'application/json': { schema: roomCloudflareRealtimeKitCreateSessionBodySchema } }, required: false },
|
|
553
|
+
},
|
|
554
|
+
responses: {
|
|
555
|
+
200: { description: 'Cloudflare RealtimeKit session created', content: { 'application/json': { schema: roomCloudflareRealtimeKitCreateSessionResponseSchema } } },
|
|
556
|
+
400: { description: 'Bad request', content: { 'application/json': { schema: errorResponseSchema } } },
|
|
557
|
+
401: { description: 'Authentication required', content: { 'application/json': { schema: errorResponseSchema } } },
|
|
558
|
+
403: { description: 'Forbidden', content: { 'application/json': { schema: errorResponseSchema } } },
|
|
559
|
+
404: { description: 'Room runtime not found', content: { 'application/json': { schema: errorResponseSchema } } },
|
|
560
|
+
409: { description: 'Conflicting existing published media', content: { 'application/json': { schema: errorResponseSchema } } },
|
|
561
|
+
},
|
|
562
|
+
});
|
|
563
|
+
|
|
528
564
|
const createRoomRealtimeIceServers = createRoute({
|
|
529
565
|
operationId: 'createRoomRealtimeIceServers',
|
|
530
566
|
method: 'post',
|
|
@@ -637,3 +673,9 @@ roomRoute.openapi(closeRoomRealtimeTracks, async (c) =>
|
|
|
637
673
|
requireAuth: true,
|
|
638
674
|
validatedJson: c.req.valid('json'),
|
|
639
675
|
}));
|
|
676
|
+
|
|
677
|
+
roomRoute.openapi(createRoomCloudflareRealtimeKitSession, async (c) =>
|
|
678
|
+
proxyRoomDoRequest(c, '/media/cloudflare_realtimekit/session', 'POST', {
|
|
679
|
+
requireAuth: true,
|
|
680
|
+
validatedJson: c.req.valid('json'),
|
|
681
|
+
}));
|