@electric-ax/agents-server 0.4.2 → 0.4.3

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.
@@ -36,6 +36,13 @@ const subscriptionProxyBodySchema = Type.Object(
36
36
 
37
37
  type SubscriptionProxyBody = Static<typeof subscriptionProxyBodySchema>
38
38
 
39
+ const subscriptionControlActions = [
40
+ `callback`,
41
+ `claim`,
42
+ `ack`,
43
+ `release`,
44
+ ] as const
45
+
39
46
  export type DurableStreamsRoutes = RouterType<
40
47
  IRequest,
41
48
  [TenantContext],
@@ -48,7 +55,34 @@ export const durableStreamsRouter: DurableStreamsRoutes = Router<
48
55
  Response | undefined
49
56
  >()
50
57
 
51
- durableStreamsRouter.all(`/v1/stream-meta/subscriptions/*`, subscriptionProxy)
58
+ durableStreamsRouter.put(
59
+ `/__ds/subscriptions/:subscriptionId`,
60
+ putSubscriptionBase
61
+ )
62
+ durableStreamsRouter.get(
63
+ `/__ds/subscriptions/:subscriptionId`,
64
+ getSubscriptionBase
65
+ )
66
+ durableStreamsRouter.delete(
67
+ `/__ds/subscriptions/:subscriptionId`,
68
+ deleteSubscriptionBase
69
+ )
70
+ durableStreamsRouter.post(
71
+ `/__ds/subscriptions/:subscriptionId/streams`,
72
+ postSubscriptionStreams
73
+ )
74
+ durableStreamsRouter.delete(
75
+ `/__ds/subscriptions/:subscriptionId/streams/:streamPath+`,
76
+ deleteSubscriptionStream
77
+ )
78
+ for (const action of subscriptionControlActions) {
79
+ durableStreamsRouter.post(
80
+ `/__ds/subscriptions/:subscriptionId/${action}`,
81
+ subscriptionAction(action)
82
+ )
83
+ }
84
+ durableStreamsRouter.all(`/__ds`, controlPassThrough)
85
+ durableStreamsRouter.all(`/__ds/*`, controlPassThrough)
52
86
  durableStreamsRouter.post(`*`, streamAppend)
53
87
  durableStreamsRouter.all(`*`, proxyPassThrough)
54
88
 
@@ -71,8 +105,9 @@ async function forwardToDurableStreams(
71
105
  ctx: TenantContext,
72
106
  request: IRequest,
73
107
  body?: Uint8Array,
74
- route: `stream` | `stream-meta` = `stream`,
75
- urlOverride?: string
108
+ route: `stream` | `control` = `stream`,
109
+ urlOverride?: string,
110
+ durableStreamsBearerMode: `overwrite` | `if-missing` = `overwrite`
76
111
  ): Promise<Response> {
77
112
  const headers = new Headers(request.headers)
78
113
  headers.delete(`host`)
@@ -94,11 +129,7 @@ async function forwardToDurableStreams(
94
129
  body: requestBody,
95
130
  durableStreamsUrl: ctx.durableStreamsUrl,
96
131
  durableStreamsBearer: ctx.durableStreamsBearer,
97
- durableStreamsBearerMode: usesSubscriptionScopedBearer(
98
- urlOverride ?? request.url
99
- )
100
- ? `if-missing`
101
- : `overwrite`,
132
+ durableStreamsBearerMode,
102
133
  durableStreamsRouting: ctx.durableStreamsRouting,
103
134
  serviceId: ctx.service,
104
135
  dispatcher: ctx.durableStreamsDispatcher,
@@ -106,23 +137,7 @@ async function forwardToDurableStreams(
106
137
  })
107
138
  }
108
139
 
109
- function subscriptionIdFromPath(pathname: string): string | null {
110
- const match = /^\/v1\/stream-meta\/subscriptions\/([^/]+)(?:\/.*)?$/.exec(
111
- pathname
112
- )
113
- return match ? decodeURIComponent(match[1]!) : null
114
- }
115
-
116
- function isSubscriptionBasePath(pathname: string): boolean {
117
- return /^\/v1\/stream-meta\/subscriptions\/[^/]+\/?$/.test(pathname)
118
- }
119
-
120
- function usesSubscriptionScopedBearer(requestUrl: string): boolean {
121
- const pathname = new URL(requestUrl, `http://localhost`).pathname
122
- return /^\/v1\/stream-meta\/subscriptions\/[^/]+\/(?:ack|release|callback)\/?$/.test(
123
- pathname
124
- )
125
- }
140
+ type SubscriptionControlAction = (typeof subscriptionControlActions)[number]
126
141
 
127
142
  function rewriteSubscriptionBodyForBackend(
128
143
  payload: Record<string, unknown>,
@@ -239,80 +254,82 @@ function decodeJson(bytes: Uint8Array): Record<string, unknown> | null {
239
254
  }
240
255
  }
241
256
 
242
- function rewriteSubscriptionStreamPathInUrl(
243
- requestUrl: URL,
244
- service: string,
245
- routingAdapter: DurableStreamsRoutingAdapter
246
- ): string {
247
- const match =
248
- /^(\/v1\/stream-meta\/subscriptions\/[^/]+\/streams\/)(.+)$/.exec(
249
- requestUrl.pathname
250
- )
251
- if (!match) return requestUrl.toString()
252
-
253
- const [, prefix, encodedPath] = match
254
- const streamPath = decodeURIComponent(encodedPath!)
255
- requestUrl.pathname = `${prefix}${encodeURIComponent(
256
- routingAdapter.toBackendStreamPath(service, streamPath)
257
- )}`
258
- return requestUrl.toString()
257
+ function routeParam(request: IRequest, name: string): string {
258
+ const value = request.params[name]
259
+ const raw = Array.isArray(value) ? value[0] : value
260
+ return decodeURIComponent(raw ?? ``)
259
261
  }
260
262
 
261
- async function subscriptionProxy(
262
- request: IRequest,
263
+ function subscriptionRoutingAdapter(
263
264
  ctx: TenantContext
264
- ): Promise<Response | undefined> {
265
- const url = new URL(request.url)
266
- const subscriptionId = subscriptionIdFromPath(url.pathname)
267
- if (!subscriptionId) return undefined
268
-
269
- const routingAdapter = resolveDurableStreamsRoutingAdapter(
270
- ctx.durableStreamsRouting
265
+ ): DurableStreamsRoutingAdapter {
266
+ return resolveDurableStreamsRoutingAdapter(
267
+ ctx.durableStreamsRouting,
268
+ ctx.durableStreamsUrl
271
269
  )
272
- let requestBody: Uint8Array | undefined
273
- let targetWebhookUrl: string | null = null
274
- let requestUrl = request.url
270
+ }
275
271
 
276
- if ([`PUT`, `POST`].includes(request.method.toUpperCase())) {
277
- requestBody = await readRequestBody(request as Request)
278
- if (requestBody.length > 0) {
279
- const validation = validateBody(subscriptionProxyBodySchema, requestBody)
280
- if (!validation.ok) return validation.response
281
- const payload = validation.value as SubscriptionProxyBody
282
- if (payload.webhook?.url !== undefined) {
283
- targetWebhookUrl =
284
- rewriteLoopbackWebhookUrl(payload.webhook.url) ?? null
285
- payload.webhook.url = appendPathToUrl(
286
- ctx.publicUrl,
287
- `/_electric/webhook-forward/${encodeURIComponent(subscriptionId)}`
288
- )
289
- }
290
- rewriteSubscriptionBodyForBackend(
291
- payload as Record<string, unknown>,
292
- ctx.service,
293
- routingAdapter
294
- )
295
- requestBody = new TextEncoder().encode(JSON.stringify(payload))
272
+ async function rewriteSubscriptionRequestBody(
273
+ request: IRequest,
274
+ ctx: TenantContext,
275
+ subscriptionId: string,
276
+ routingAdapter: DurableStreamsRoutingAdapter
277
+ ): Promise<
278
+ | {
279
+ ok: true
280
+ body: Uint8Array
281
+ targetWebhookUrl: string | null
296
282
  }
283
+ | { ok: false; response: Response }
284
+ > {
285
+ const body = await readRequestBody(request as Request)
286
+ if (body.length === 0) {
287
+ return { ok: true, body, targetWebhookUrl: null }
297
288
  }
298
289
 
299
- if (
300
- request.method.toUpperCase() === `DELETE` &&
301
- /\/streams\/.+$/.test(url.pathname)
302
- ) {
303
- requestUrl = rewriteSubscriptionStreamPathInUrl(
304
- url,
305
- ctx.service,
306
- routingAdapter
290
+ const validation = validateBody(subscriptionProxyBodySchema, body)
291
+ if (!validation.ok) return { ok: false, response: validation.response }
292
+
293
+ const payload = validation.value as SubscriptionProxyBody
294
+ let targetWebhookUrl: string | null = null
295
+ if (payload.webhook?.url !== undefined) {
296
+ targetWebhookUrl = rewriteLoopbackWebhookUrl(payload.webhook.url) ?? null
297
+ payload.webhook.url = appendPathToUrl(
298
+ ctx.publicUrl,
299
+ `/_electric/webhook-forward/${encodeURIComponent(subscriptionId)}`
307
300
  )
308
301
  }
309
302
 
303
+ rewriteSubscriptionBodyForBackend(
304
+ payload as Record<string, unknown>,
305
+ ctx.service,
306
+ routingAdapter
307
+ )
308
+
309
+ return {
310
+ ok: true,
311
+ body: new TextEncoder().encode(JSON.stringify(payload)),
312
+ targetWebhookUrl,
313
+ }
314
+ }
315
+
316
+ async function forwardSubscriptionRequest(
317
+ request: IRequest,
318
+ ctx: TenantContext,
319
+ routingAdapter: DurableStreamsRoutingAdapter,
320
+ opts: {
321
+ body?: Uint8Array
322
+ requestUrl?: string
323
+ bearerMode?: `overwrite` | `if-missing`
324
+ } = {}
325
+ ): Promise<{ upstream: Response; response: Response }> {
310
326
  const upstream = await forwardToDurableStreams(
311
327
  ctx,
312
328
  request,
313
- requestBody,
314
- `stream-meta`,
315
- requestUrl
329
+ opts.body,
330
+ `control`,
331
+ opts.requestUrl,
332
+ opts.bearerMode ?? `overwrite`
316
333
  )
317
334
  let responseBytes: Uint8Array = upstream.body
318
335
  ? new Uint8Array(await upstream.arrayBuffer())
@@ -323,42 +340,196 @@ async function subscriptionProxy(
323
340
  ctx.service,
324
341
  routingAdapter
325
342
  )
326
- const response = responseFromUpstream(upstream, responseBytes)
343
+ return {
344
+ upstream,
345
+ response: responseFromUpstream(upstream, responseBytes),
346
+ }
347
+ }
327
348
 
328
- if (!upstream.ok) return response
349
+ async function upsertSubscriptionWebhook(
350
+ ctx: TenantContext,
351
+ subscriptionId: string,
352
+ targetWebhookUrl: string
353
+ ): Promise<void> {
354
+ await ctx.pgDb
355
+ .insert(subscriptionWebhooks)
356
+ .values({
357
+ tenantId: ctx.service,
358
+ subscriptionId,
359
+ webhookUrl: targetWebhookUrl,
360
+ })
361
+ .onConflictDoUpdate({
362
+ target: [
363
+ subscriptionWebhooks.tenantId,
364
+ subscriptionWebhooks.subscriptionId,
365
+ ],
366
+ set: { webhookUrl: targetWebhookUrl },
367
+ })
368
+ }
329
369
 
330
- if (
331
- request.method.toUpperCase() === `DELETE` &&
332
- isSubscriptionBasePath(url.pathname)
333
- ) {
334
- await ctx.pgDb
335
- .delete(subscriptionWebhooks)
336
- .where(
337
- and(
338
- eq(subscriptionWebhooks.tenantId, ctx.service),
339
- eq(subscriptionWebhooks.subscriptionId, subscriptionId)
340
- )
370
+ async function deleteSubscriptionWebhook(
371
+ ctx: TenantContext,
372
+ subscriptionId: string
373
+ ): Promise<void> {
374
+ await ctx.pgDb
375
+ .delete(subscriptionWebhooks)
376
+ .where(
377
+ and(
378
+ eq(subscriptionWebhooks.tenantId, ctx.service),
379
+ eq(subscriptionWebhooks.subscriptionId, subscriptionId)
341
380
  )
342
- } else if (targetWebhookUrl) {
343
- await ctx.pgDb
344
- .insert(subscriptionWebhooks)
345
- .values({
346
- tenantId: ctx.service,
347
- subscriptionId,
348
- webhookUrl: targetWebhookUrl,
349
- })
350
- .onConflictDoUpdate({
351
- target: [
352
- subscriptionWebhooks.tenantId,
353
- subscriptionWebhooks.subscriptionId,
354
- ],
355
- set: { webhookUrl: targetWebhookUrl },
356
- })
381
+ )
382
+ }
383
+
384
+ function rewriteSubscriptionStreamPathInUrl(
385
+ requestUrl: URL,
386
+ service: string,
387
+ routingAdapter: DurableStreamsRoutingAdapter,
388
+ streamPath: string
389
+ ): string {
390
+ const prefix = requestUrl.pathname.slice(
391
+ 0,
392
+ requestUrl.pathname.indexOf(`/streams/`) + `/streams/`.length
393
+ )
394
+ requestUrl.pathname = `${prefix}${encodeURIComponent(
395
+ routingAdapter.toBackendStreamPath(service, streamPath)
396
+ )}`
397
+ return requestUrl.toString()
398
+ }
399
+
400
+ async function putSubscriptionBase(
401
+ request: IRequest,
402
+ ctx: TenantContext
403
+ ): Promise<Response> {
404
+ const subscriptionId = routeParam(request, `subscriptionId`)
405
+ const routingAdapter = subscriptionRoutingAdapter(ctx)
406
+ const rewrite = await rewriteSubscriptionRequestBody(
407
+ request,
408
+ ctx,
409
+ subscriptionId,
410
+ routingAdapter
411
+ )
412
+ if (!rewrite.ok) return rewrite.response
413
+
414
+ const { upstream, response } = await forwardSubscriptionRequest(
415
+ request,
416
+ ctx,
417
+ routingAdapter,
418
+ { body: rewrite.body }
419
+ )
420
+ if (upstream.ok && rewrite.targetWebhookUrl) {
421
+ await upsertSubscriptionWebhook(
422
+ ctx,
423
+ subscriptionId,
424
+ rewrite.targetWebhookUrl
425
+ )
357
426
  }
427
+ return response
428
+ }
429
+
430
+ async function getSubscriptionBase(
431
+ request: IRequest,
432
+ ctx: TenantContext
433
+ ): Promise<Response> {
434
+ const routingAdapter = subscriptionRoutingAdapter(ctx)
435
+ return (await forwardSubscriptionRequest(request, ctx, routingAdapter))
436
+ .response
437
+ }
358
438
 
439
+ async function deleteSubscriptionBase(
440
+ request: IRequest,
441
+ ctx: TenantContext
442
+ ): Promise<Response> {
443
+ const subscriptionId = routeParam(request, `subscriptionId`)
444
+ const routingAdapter = subscriptionRoutingAdapter(ctx)
445
+ const { upstream, response } = await forwardSubscriptionRequest(
446
+ request,
447
+ ctx,
448
+ routingAdapter
449
+ )
450
+ if (upstream.ok) {
451
+ await deleteSubscriptionWebhook(ctx, subscriptionId)
452
+ }
359
453
  return response
360
454
  }
361
455
 
456
+ async function postSubscriptionStreams(
457
+ request: IRequest,
458
+ ctx: TenantContext
459
+ ): Promise<Response> {
460
+ const subscriptionId = routeParam(request, `subscriptionId`)
461
+ const routingAdapter = subscriptionRoutingAdapter(ctx)
462
+ const rewrite = await rewriteSubscriptionRequestBody(
463
+ request,
464
+ ctx,
465
+ subscriptionId,
466
+ routingAdapter
467
+ )
468
+ if (!rewrite.ok) return rewrite.response
469
+
470
+ return (
471
+ await forwardSubscriptionRequest(request, ctx, routingAdapter, {
472
+ body: rewrite.body,
473
+ })
474
+ ).response
475
+ }
476
+
477
+ async function deleteSubscriptionStream(
478
+ request: IRequest,
479
+ ctx: TenantContext
480
+ ): Promise<Response> {
481
+ const routingAdapter = subscriptionRoutingAdapter(ctx)
482
+ const requestUrl = rewriteSubscriptionStreamPathInUrl(
483
+ new URL(request.url),
484
+ ctx.service,
485
+ routingAdapter,
486
+ routeParam(request, `streamPath`)
487
+ )
488
+ return (
489
+ await forwardSubscriptionRequest(request, ctx, routingAdapter, {
490
+ requestUrl,
491
+ })
492
+ ).response
493
+ }
494
+
495
+ function subscriptionAction(action: SubscriptionControlAction) {
496
+ return async (request: IRequest, ctx: TenantContext): Promise<Response> => {
497
+ const subscriptionId = routeParam(request, `subscriptionId`)
498
+ const routingAdapter = subscriptionRoutingAdapter(ctx)
499
+ const rewrite = await rewriteSubscriptionRequestBody(
500
+ request,
501
+ ctx,
502
+ subscriptionId,
503
+ routingAdapter
504
+ )
505
+ if (!rewrite.ok) return rewrite.response
506
+
507
+ const bearerMode =
508
+ action === `ack` || action === `release` || action === `callback`
509
+ ? `if-missing`
510
+ : `overwrite`
511
+ return (
512
+ await forwardSubscriptionRequest(request, ctx, routingAdapter, {
513
+ body: rewrite.body,
514
+ bearerMode,
515
+ })
516
+ ).response
517
+ }
518
+ }
519
+
520
+ async function controlPassThrough(
521
+ request: IRequest,
522
+ ctx: TenantContext
523
+ ): Promise<Response> {
524
+ const upstream = await forwardToDurableStreams(
525
+ ctx,
526
+ request,
527
+ undefined,
528
+ `control`
529
+ )
530
+ return responseFromUpstream(upstream)
531
+ }
532
+
362
533
  async function streamAppend(
363
534
  request: IRequest,
364
535
  ctx: TenantContext
@@ -391,13 +562,12 @@ async function proxyPassThrough(
391
562
  const upstream = await forwardToDurableStreams(ctx, request)
392
563
  const streamPath = new URL(request.url).pathname
393
564
  const method = request.method.toUpperCase()
394
- const isControlPath = streamPath.startsWith(`/v1/stream-meta/`)
395
565
  const endTrackedRead =
396
- method === `GET` && !isControlPath
566
+ method === `GET`
397
567
  ? await ctx.entityBridgeManager.beginClientRead(streamPath)
398
568
  : null
399
569
  try {
400
- if (method === `HEAD` && !isControlPath) {
570
+ if (method === `HEAD`) {
401
571
  await ctx.entityBridgeManager.touchByStreamPath(streamPath)
402
572
  }
403
573
  return responseFromUpstream(upstream)
@@ -1,8 +1,3 @@
1
- import {
2
- prefixTenantStreamPath,
3
- stripTenantStreamPrefix,
4
- } from './tenant-stream-paths.js'
5
-
6
1
  export interface DurableStreamsRoutingInput {
7
2
  durableStreamsUrl: string
8
3
  serviceId: string
@@ -11,7 +6,7 @@ export interface DurableStreamsRoutingInput {
11
6
 
12
7
  export interface DurableStreamsRoutingAdapter {
13
8
  streamUrl(input: DurableStreamsRoutingInput): URL
14
- streamMetaUrl(input: DurableStreamsRoutingInput): URL
9
+ controlUrl(input: DurableStreamsRoutingInput): URL
15
10
  toBackendStreamPath(serviceId: string, streamPath: string): string
16
11
  toRuntimeStreamPath(serviceId: string, streamPath: string): string
17
12
  }
@@ -26,71 +21,44 @@ function removeServiceQuery(target: URL): URL {
26
21
  return target
27
22
  }
28
23
 
29
- function logicalStreamPathFromRequest(
30
- requestUrl: string,
31
- serviceId: string
32
- ): { incomingUrl: URL; streamPath: string } {
33
- const incomingUrl = new URL(requestUrl, `http://localhost`)
34
- const segments = incomingUrl.pathname.split(`/`).filter(Boolean)
35
- if (segments[0] === `v1` && segments[1] === `stream`) {
36
- return {
37
- incomingUrl,
38
- streamPath: segments.length > 2 ? `/${segments.slice(3).join(`/`)}` : `/`,
39
- }
40
- }
41
-
42
- return {
43
- incomingUrl,
44
- streamPath: incomingUrl.pathname || `/${serviceId}`,
45
- }
24
+ function withoutTrailingSlash(pathname: string): string {
25
+ return pathname.replace(/\/+$/, ``) || `/`
46
26
  }
47
27
 
48
- function backendStreamUrl(
49
- input: DurableStreamsRoutingInput,
50
- backendStreamPath: string
51
- ): URL {
52
- const path = backendStreamPath.replace(/^\/+/, ``)
53
- const target = new URL(`/v1/stream/${path}`, input.durableStreamsUrl)
54
- return target
55
- }
56
-
57
- function streamMetaUrlWithoutService(input: DurableStreamsRoutingInput): URL {
28
+ function appendRequestPathToStreamRoot(input: DurableStreamsRoutingInput): URL {
58
29
  const incomingUrl = new URL(input.requestUrl, `http://localhost`)
59
- return removeServiceQuery(
60
- appendSearch(
61
- new URL(incomingUrl.pathname, input.durableStreamsUrl),
62
- incomingUrl
63
- )
64
- )
30
+ const path = incomingUrl.pathname.replace(/^\/+/, ``)
31
+ const target = new URL(input.durableStreamsUrl)
32
+ target.pathname = path
33
+ ? `${withoutTrailingSlash(target.pathname)}/${path}`
34
+ : withoutTrailingSlash(target.pathname)
35
+ return removeServiceQuery(appendSearch(target, incomingUrl))
65
36
  }
66
37
 
67
- export const pathPrefixedSingleTenantDurableStreamsRoutingAdapter: DurableStreamsRoutingAdapter =
38
+ export const streamRootDurableStreamsRoutingAdapter: DurableStreamsRoutingAdapter =
68
39
  {
69
- streamUrl(input) {
70
- const { incomingUrl, streamPath } = logicalStreamPathFromRequest(
71
- input.requestUrl,
72
- input.serviceId
73
- )
74
- const target = backendStreamUrl(
75
- input,
76
- prefixTenantStreamPath(streamPath, input.serviceId)
77
- )
78
- return removeServiceQuery(appendSearch(target, incomingUrl))
79
- },
40
+ streamUrl: appendRequestPathToStreamRoot,
80
41
 
81
- streamMetaUrl: streamMetaUrlWithoutService,
42
+ controlUrl: appendRequestPathToStreamRoot,
82
43
 
83
- toBackendStreamPath(serviceId, streamPath) {
84
- return prefixTenantStreamPath(streamPath, serviceId)
44
+ toBackendStreamPath(_serviceId, streamPath) {
45
+ return streamPath.replace(/^\/+/, ``)
85
46
  },
86
47
 
87
- toRuntimeStreamPath(serviceId, streamPath) {
88
- return stripTenantStreamPrefix(streamPath, serviceId)
48
+ toRuntimeStreamPath(_serviceId, streamPath) {
49
+ return streamPath.replace(/^\/+/, ``)
89
50
  },
90
51
  }
91
52
 
53
+ export const pathPrefixedSingleTenantDurableStreamsRoutingAdapter =
54
+ streamRootDurableStreamsRoutingAdapter
55
+
56
+ export const tenantRootDurableStreamsRoutingAdapter =
57
+ streamRootDurableStreamsRoutingAdapter
58
+
92
59
  export function resolveDurableStreamsRoutingAdapter(
93
- adapter?: DurableStreamsRoutingAdapter
60
+ adapter?: DurableStreamsRoutingAdapter,
61
+ _durableStreamsUrl?: string
94
62
  ): DurableStreamsRoutingAdapter {
95
- return adapter ?? pathPrefixedSingleTenantDurableStreamsRoutingAdapter
63
+ return adapter ?? streamRootDurableStreamsRoutingAdapter
96
64
  }
@@ -614,13 +614,13 @@ async function spawnEntity(
614
614
  wake: parsed.wake,
615
615
  created_by: principal.url,
616
616
  })
617
+ await linkEntityDispatchSubscription(ctx, entity)
617
618
  if (parsed.initialMessage !== undefined) {
618
619
  await ctx.entityManager.send(entity.url, {
619
620
  from: principal.url,
620
621
  payload: parsed.initialMessage,
621
622
  })
622
623
  }
623
- await linkEntityDispatchSubscription(ctx, entity)
624
624
 
625
625
  return json(
626
626
  { ...toPublicEntity(entity), txid: entity.txid },
@@ -294,7 +294,8 @@ async function webhookForward(
294
294
  const parsedBody = parsedBodyResult.value as WebhookForwardBody | undefined
295
295
  const newWebhook = newWebhookPayload(parsedBody)
296
296
  const routingAdapter = resolveDurableStreamsRoutingAdapter(
297
- ctx.durableStreamsRouting
297
+ ctx.durableStreamsRouting,
298
+ ctx.durableStreamsUrl
298
299
  )
299
300
 
300
301
  if (parsedBody) {
@@ -537,7 +538,10 @@ async function callbackForward(
537
538
  ctx.service,
538
539
  consumerId,
539
540
  requestBody,
540
- resolveDurableStreamsRoutingAdapter(ctx.durableStreamsRouting)
541
+ resolveDurableStreamsRoutingAdapter(
542
+ ctx.durableStreamsRouting,
543
+ ctx.durableStreamsUrl
544
+ )
541
545
  )
542
546
 
543
547
  let upstream: Response
@@ -113,6 +113,7 @@ async function registerRunner(
113
113
  400
114
114
  )
115
115
  }
116
+
116
117
  if (ctx.principal && ownerUserId !== ctx.principal.key) {
117
118
  throw new ElectricAgentsError(
118
119
  ErrCodeUnauthorized,
package/src/runtime.ts CHANGED
@@ -64,7 +64,9 @@ export class ElectricAgentsTenantRuntime {
64
64
  this.streamClient = options.streamClient
65
65
  } else if (options.durableStreamsUrl) {
66
66
  this.streamClient = new StreamClient(
67
- durableStreamsServiceUrl(options.durableStreamsUrl, this.serviceId),
67
+ durableStreamsServiceUrl(options.durableStreamsUrl, this.serviceId, {
68
+ scope: `stream-root`,
69
+ }),
68
70
  { bearer: options.durableStreamsBearer }
69
71
  )
70
72
  } else {