@electric-ax/agents-server 0.4.1 → 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.
@@ -302,7 +302,7 @@ function createDb(postgresUrl) {
302
302
  const poolMax = Number(process.env.ELECTRIC_AGENTS_PG_POOL_MAX ?? `100`);
303
303
  const client = postgres(postgresUrl, {
304
304
  max: poolMax,
305
- fetch_types: false
305
+ fetch_types: true
306
306
  });
307
307
  const db = drizzle(client, { schema: schema_exports });
308
308
  return {
@@ -331,76 +331,6 @@ async function runMigrations(postgresUrl) {
331
331
  await migrationClient.end();
332
332
  }
333
333
 
334
- //#endregion
335
- //#region src/routing/tenant-stream-paths.ts
336
- function withoutLeadingSlash(path$1) {
337
- return path$1.replace(/^\/+/, ``);
338
- }
339
- function withLeadingSlash(path$1) {
340
- return path$1.startsWith(`/`) ? path$1 : `/${path$1}`;
341
- }
342
- function prefixTenantStreamPath(path$1, tenantId) {
343
- const normalized = withoutLeadingSlash(path$1);
344
- if (!normalized || normalized === tenantId) return tenantId;
345
- if (normalized.startsWith(`${tenantId}/`)) return normalized;
346
- return `${tenantId}/${normalized}`;
347
- }
348
- function stripTenantStreamPrefix(path$1, tenantId) {
349
- const normalized = withoutLeadingSlash(path$1);
350
- if (normalized === tenantId) return ``;
351
- if (normalized.startsWith(`${tenantId}/`)) return normalized.slice(tenantId.length + 1);
352
- return normalized;
353
- }
354
-
355
- //#endregion
356
- //#region src/routing/durable-streams-routing-adapter.ts
357
- function appendSearch(target, source) {
358
- target.search = source.search;
359
- return target;
360
- }
361
- function removeServiceQuery(target) {
362
- target.searchParams.delete(`service`);
363
- return target;
364
- }
365
- function logicalStreamPathFromRequest(requestUrl, serviceId) {
366
- const incomingUrl = new URL(requestUrl, `http://localhost`);
367
- const segments = incomingUrl.pathname.split(`/`).filter(Boolean);
368
- if (segments[0] === `v1` && segments[1] === `stream`) return {
369
- incomingUrl,
370
- streamPath: segments.length > 2 ? `/${segments.slice(3).join(`/`)}` : `/`
371
- };
372
- return {
373
- incomingUrl,
374
- streamPath: incomingUrl.pathname || `/${serviceId}`
375
- };
376
- }
377
- function backendStreamUrl(input, backendStreamPath) {
378
- const path$1 = backendStreamPath.replace(/^\/+/, ``);
379
- const target = new URL(`/v1/stream/${path$1}`, input.durableStreamsUrl);
380
- return target;
381
- }
382
- function streamMetaUrlWithoutService(input) {
383
- const incomingUrl = new URL(input.requestUrl, `http://localhost`);
384
- return removeServiceQuery(appendSearch(new URL(incomingUrl.pathname, input.durableStreamsUrl), incomingUrl));
385
- }
386
- const pathPrefixedSingleTenantDurableStreamsRoutingAdapter = {
387
- streamUrl(input) {
388
- const { incomingUrl, streamPath } = logicalStreamPathFromRequest(input.requestUrl, input.serviceId);
389
- const target = backendStreamUrl(input, prefixTenantStreamPath(streamPath, input.serviceId));
390
- return removeServiceQuery(appendSearch(target, incomingUrl));
391
- },
392
- streamMetaUrl: streamMetaUrlWithoutService,
393
- toBackendStreamPath(serviceId, streamPath) {
394
- return prefixTenantStreamPath(streamPath, serviceId);
395
- },
396
- toRuntimeStreamPath(serviceId, streamPath) {
397
- return stripTenantStreamPrefix(streamPath, serviceId);
398
- }
399
- };
400
- function resolveDurableStreamsRoutingAdapter(adapter) {
401
- return adapter ?? pathPrefixedSingleTenantDurableStreamsRoutingAdapter;
402
- }
403
-
404
334
  //#endregion
405
335
  //#region src/electric-agents-http.ts
406
336
  function apiError(status$1, code, message, details) {
@@ -501,6 +431,40 @@ function electricUrlWithPath(electricUrl, path$1) {
501
431
  return target;
502
432
  }
503
433
 
434
+ //#endregion
435
+ //#region src/routing/durable-streams-routing-adapter.ts
436
+ function appendSearch(target, source) {
437
+ target.search = source.search;
438
+ return target;
439
+ }
440
+ function removeServiceQuery(target) {
441
+ target.searchParams.delete(`service`);
442
+ return target;
443
+ }
444
+ function withoutTrailingSlash(pathname) {
445
+ return pathname.replace(/\/+$/, ``) || `/`;
446
+ }
447
+ function appendRequestPathToStreamRoot(input) {
448
+ const incomingUrl = new URL(input.requestUrl, `http://localhost`);
449
+ const path$1 = incomingUrl.pathname.replace(/^\/+/, ``);
450
+ const target = new URL(input.durableStreamsUrl);
451
+ target.pathname = path$1 ? `${withoutTrailingSlash(target.pathname)}/${path$1}` : withoutTrailingSlash(target.pathname);
452
+ return removeServiceQuery(appendSearch(target, incomingUrl));
453
+ }
454
+ const streamRootDurableStreamsRoutingAdapter = {
455
+ streamUrl: appendRequestPathToStreamRoot,
456
+ controlUrl: appendRequestPathToStreamRoot,
457
+ toBackendStreamPath(_serviceId, streamPath) {
458
+ return streamPath.replace(/^\/+/, ``);
459
+ },
460
+ toRuntimeStreamPath(_serviceId, streamPath) {
461
+ return streamPath.replace(/^\/+/, ``);
462
+ }
463
+ };
464
+ function resolveDurableStreamsRoutingAdapter(adapter, _durableStreamsUrl) {
465
+ return adapter ?? streamRootDurableStreamsRoutingAdapter;
466
+ }
467
+
504
468
  //#endregion
505
469
  //#region src/tracing.ts
506
470
  const tracer = trace.getTracer(`agent-server`);
@@ -579,11 +543,18 @@ function durableStreamsBearerHeaders(bearer) {
579
543
  if (!bearer) return void 0;
580
544
  return { authorization: async () => await resolveDurableStreamsBearer(bearer) ?? `` };
581
545
  }
582
- function durableStreamsServiceUrl(baseUrl, serviceId) {
546
+ function durableStreamsServiceUrl(baseUrl, serviceId, options = {}) {
583
547
  const url = new URL(baseUrl);
584
- if (/^\/v1\/stream\/[^/]+\/?$/.test(url.pathname)) return baseUrl.replace(/\/+$/, ``);
585
- const base = baseUrl.replace(/\/+$/, ``);
586
- return `${base}/v1/stream/${encodeURIComponent(serviceId)}`;
548
+ if (/\/v1\/streams\/[^/]+\/?$/.test(url.pathname)) return baseUrl.replace(/\/+$/, ``);
549
+ if (/\/v1\/stream\/[^/]+\/?$/.test(url.pathname)) return baseUrl.replace(/\/+$/, ``);
550
+ const scope = options.scope ?? `service`;
551
+ const encodedServiceId = encodeURIComponent(serviceId);
552
+ const path$1 = url.pathname.replace(/\/+$/, ``) || `/`;
553
+ if (path$1.endsWith(`/v1/streams`)) url.pathname = `${path$1}/${encodedServiceId}`;
554
+ else if (path$1.endsWith(`/v1/stream`)) url.pathname = scope === `service` ? `${path$1}/${encodedServiceId}` : path$1;
555
+ else if (scope === `stream-root`) url.pathname = `${path$1 === `/` ? `` : path$1}/v1/stream`;
556
+ else url.pathname = `${path$1 === `/` ? `` : path$1}/v1/stream/${encodedServiceId}`;
557
+ return url.toString().replace(/\/+$/, ``);
587
558
  }
588
559
  function isNotFoundError(err) {
589
560
  return err instanceof DurableStreamError && err.code === ErrCodeNotFound || err instanceof FetchError && err.status === 404;
@@ -616,34 +587,15 @@ var StreamClient = class {
616
587
  await applyDurableStreamsBearer(headers, this.options.bearer, { overwrite: opts.overwriteBearer });
617
588
  return headers;
618
589
  }
619
- subscriptionServiceId() {
620
- const url = new URL(this.baseUrl);
621
- const match = /^(.*)\/v1\/stream\/([^/]+)\/?$/.exec(url.pathname);
622
- return match ? decodeURIComponent(match[2]) : null;
623
- }
624
590
  backendSubscriptionPath(path$1) {
625
- const normalized = normalizeSubscriptionPath(path$1);
626
- const serviceId = this.subscriptionServiceId();
627
- if (!serviceId) return normalized;
628
- if (normalized === serviceId || normalized.startsWith(`${serviceId}/`)) return normalized;
629
- return `${serviceId}/${normalized}`;
591
+ return normalizeSubscriptionPath(path$1);
630
592
  }
631
593
  runtimeSubscriptionPath(path$1) {
632
- const normalized = normalizeSubscriptionPath(path$1);
633
- const serviceId = this.subscriptionServiceId();
634
- if (!serviceId) return normalized;
635
- return normalized.startsWith(`${serviceId}/`) ? normalized.slice(serviceId.length + 1) : normalized;
594
+ return normalizeSubscriptionPath(path$1);
636
595
  }
637
596
  subscriptionUrl(subscriptionId) {
638
597
  const url = new URL(this.baseUrl);
639
- const match = /^(.*)\/v1\/stream\/([^/]+)\/?$/.exec(url.pathname);
640
- if (match) {
641
- const [, prefix = ``, serviceId] = match;
642
- url.pathname = `${prefix}/v1/stream-meta/subscriptions/${encodeURIComponent(subscriptionId)}`;
643
- url.searchParams.set(`service`, decodeURIComponent(serviceId));
644
- return url.toString();
645
- }
646
- url.pathname = `${url.pathname.replace(/\/+$/, ``)}/v1/stream-meta/subscriptions/${encodeURIComponent(subscriptionId)}`;
598
+ url.pathname = `${url.pathname.replace(/\/+$/, ``)}/__ds/subscriptions/${encodeURIComponent(subscriptionId)}`;
647
599
  return url.toString();
648
600
  }
649
601
  subscriptionChildUrl(subscriptionId, ...segments) {
@@ -1112,13 +1064,13 @@ function buildElectricProxyTarget(options) {
1112
1064
  return target;
1113
1065
  }
1114
1066
  async function forwardFetchRequest(options) {
1115
- const routingAdapter = resolveDurableStreamsRoutingAdapter(options.durableStreamsRouting);
1067
+ const routingAdapter = resolveDurableStreamsRoutingAdapter(options.durableStreamsRouting, options.durableStreamsUrl);
1116
1068
  const routingInput = {
1117
1069
  durableStreamsUrl: options.durableStreamsUrl,
1118
1070
  serviceId: options.serviceId,
1119
1071
  requestUrl: options.request.url
1120
1072
  };
1121
- const upstreamUrl = options.route === `stream-meta` ? routingAdapter.streamMetaUrl(routingInput) : routingAdapter.streamUrl(routingInput);
1073
+ const upstreamUrl = options.route === `control` ? routingAdapter.controlUrl(routingInput) : routingAdapter.streamUrl(routingInput);
1122
1074
  const headers = new Headers(options.request.headers);
1123
1075
  if (options.durableStreamsBearerMode !== `none`) await applyDurableStreamsBearer(headers, options.durableStreamsBearer, { overwrite: options.durableStreamsBearerMode !== `if-missing` });
1124
1076
  const init = {
@@ -1434,8 +1386,21 @@ function isLoopbackHostname(hostname) {
1434
1386
  //#endregion
1435
1387
  //#region src/routing/durable-streams-router.ts
1436
1388
  const subscriptionProxyBodySchema = Type.Object({ webhook: Type.Optional(Type.Object({ url: Type.String() }, { additionalProperties: true })) }, { additionalProperties: true });
1389
+ const subscriptionControlActions = [
1390
+ `callback`,
1391
+ `claim`,
1392
+ `ack`,
1393
+ `release`
1394
+ ];
1437
1395
  const durableStreamsRouter = Router();
1438
- durableStreamsRouter.all(`/v1/stream-meta/subscriptions/*`, subscriptionProxy);
1396
+ durableStreamsRouter.put(`/__ds/subscriptions/:subscriptionId`, putSubscriptionBase);
1397
+ durableStreamsRouter.get(`/__ds/subscriptions/:subscriptionId`, getSubscriptionBase);
1398
+ durableStreamsRouter.delete(`/__ds/subscriptions/:subscriptionId`, deleteSubscriptionBase);
1399
+ durableStreamsRouter.post(`/__ds/subscriptions/:subscriptionId/streams`, postSubscriptionStreams);
1400
+ durableStreamsRouter.delete(`/__ds/subscriptions/:subscriptionId/streams/:streamPath+`, deleteSubscriptionStream);
1401
+ for (const action of subscriptionControlActions) durableStreamsRouter.post(`/__ds/subscriptions/:subscriptionId/${action}`, subscriptionAction(action));
1402
+ durableStreamsRouter.all(`/__ds`, controlPassThrough);
1403
+ durableStreamsRouter.all(`/__ds/*`, controlPassThrough);
1439
1404
  durableStreamsRouter.post(`*`, streamAppend);
1440
1405
  durableStreamsRouter.all(`*`, proxyPassThrough);
1441
1406
  function bodyFromBytes$1(body) {
@@ -1448,7 +1413,7 @@ function responseFromUpstream$1(response, body) {
1448
1413
  headers: responseHeaders(response)
1449
1414
  });
1450
1415
  }
1451
- async function forwardToDurableStreams(ctx, request, body, route = `stream`, urlOverride) {
1416
+ async function forwardToDurableStreams(ctx, request, body, route = `stream`, urlOverride, durableStreamsBearerMode = `overwrite`) {
1452
1417
  const headers = new Headers(request.headers);
1453
1418
  headers.delete(`host`);
1454
1419
  let requestBody = body;
@@ -1462,24 +1427,13 @@ async function forwardToDurableStreams(ctx, request, body, route = `stream`, url
1462
1427
  body: requestBody,
1463
1428
  durableStreamsUrl: ctx.durableStreamsUrl,
1464
1429
  durableStreamsBearer: ctx.durableStreamsBearer,
1465
- durableStreamsBearerMode: usesSubscriptionScopedBearer(urlOverride ?? request.url) ? `if-missing` : `overwrite`,
1430
+ durableStreamsBearerMode,
1466
1431
  durableStreamsRouting: ctx.durableStreamsRouting,
1467
1432
  serviceId: ctx.service,
1468
1433
  dispatcher: ctx.durableStreamsDispatcher,
1469
1434
  route
1470
1435
  });
1471
1436
  }
1472
- function subscriptionIdFromPath(pathname) {
1473
- const match = /^\/v1\/stream-meta\/subscriptions\/([^/]+)(?:\/.*)?$/.exec(pathname);
1474
- return match ? decodeURIComponent(match[1]) : null;
1475
- }
1476
- function isSubscriptionBasePath(pathname) {
1477
- return /^\/v1\/stream-meta\/subscriptions\/[^/]+\/?$/.test(pathname);
1478
- }
1479
- function usesSubscriptionScopedBearer(requestUrl) {
1480
- const pathname = new URL(requestUrl, `http://localhost`).pathname;
1481
- return /^\/v1\/stream-meta\/subscriptions\/[^/]+\/(?:ack|release|callback)\/?$/.test(pathname);
1482
- }
1483
1437
  function rewriteSubscriptionBodyForBackend(payload, service, routingAdapter) {
1484
1438
  if (typeof payload.pattern === `string`) payload.pattern = routingAdapter.toBackendStreamPath(service, payload.pattern);
1485
1439
  if (Array.isArray(payload.streams)) payload.streams = payload.streams.map((stream) => typeof stream === `string` ? routingAdapter.toBackendStreamPath(service, stream) : stream);
@@ -1524,44 +1478,50 @@ function decodeJson(bytes) {
1524
1478
  return null;
1525
1479
  }
1526
1480
  }
1527
- function rewriteSubscriptionStreamPathInUrl(requestUrl, service, routingAdapter) {
1528
- const match = /^(\/v1\/stream-meta\/subscriptions\/[^/]+\/streams\/)(.+)$/.exec(requestUrl.pathname);
1529
- if (!match) return requestUrl.toString();
1530
- const [, prefix, encodedPath] = match;
1531
- const streamPath = decodeURIComponent(encodedPath);
1532
- requestUrl.pathname = `${prefix}${encodeURIComponent(routingAdapter.toBackendStreamPath(service, streamPath))}`;
1533
- return requestUrl.toString();
1481
+ function routeParam$2(request, name) {
1482
+ const value = request.params[name];
1483
+ const raw = Array.isArray(value) ? value[0] : value;
1484
+ return decodeURIComponent(raw ?? ``);
1534
1485
  }
1535
- async function subscriptionProxy(request, ctx) {
1536
- const url = new URL(request.url);
1537
- const subscriptionId = subscriptionIdFromPath(url.pathname);
1538
- if (!subscriptionId) return void 0;
1539
- const routingAdapter = resolveDurableStreamsRoutingAdapter(ctx.durableStreamsRouting);
1540
- let requestBody;
1486
+ function subscriptionRoutingAdapter(ctx) {
1487
+ return resolveDurableStreamsRoutingAdapter(ctx.durableStreamsRouting, ctx.durableStreamsUrl);
1488
+ }
1489
+ async function rewriteSubscriptionRequestBody(request, ctx, subscriptionId, routingAdapter) {
1490
+ const body = await readRequestBody(request);
1491
+ if (body.length === 0) return {
1492
+ ok: true,
1493
+ body,
1494
+ targetWebhookUrl: null
1495
+ };
1496
+ const validation = validateBody(subscriptionProxyBodySchema, body);
1497
+ if (!validation.ok) return {
1498
+ ok: false,
1499
+ response: validation.response
1500
+ };
1501
+ const payload = validation.value;
1541
1502
  let targetWebhookUrl = null;
1542
- let requestUrl = request.url;
1543
- if ([`PUT`, `POST`].includes(request.method.toUpperCase())) {
1544
- requestBody = await readRequestBody(request);
1545
- if (requestBody.length > 0) {
1546
- const validation = validateBody(subscriptionProxyBodySchema, requestBody);
1547
- if (!validation.ok) return validation.response;
1548
- const payload = validation.value;
1549
- if (payload.webhook?.url !== void 0) {
1550
- targetWebhookUrl = rewriteLoopbackWebhookUrl(payload.webhook.url) ?? null;
1551
- payload.webhook.url = appendPathToUrl(ctx.publicUrl, `/_electric/webhook-forward/${encodeURIComponent(subscriptionId)}`);
1552
- }
1553
- rewriteSubscriptionBodyForBackend(payload, ctx.service, routingAdapter);
1554
- requestBody = new TextEncoder().encode(JSON.stringify(payload));
1555
- }
1503
+ if (payload.webhook?.url !== void 0) {
1504
+ targetWebhookUrl = rewriteLoopbackWebhookUrl(payload.webhook.url) ?? null;
1505
+ payload.webhook.url = appendPathToUrl(ctx.publicUrl, `/_electric/webhook-forward/${encodeURIComponent(subscriptionId)}`);
1556
1506
  }
1557
- if (request.method.toUpperCase() === `DELETE` && /\/streams\/.+$/.test(url.pathname)) requestUrl = rewriteSubscriptionStreamPathInUrl(url, ctx.service, routingAdapter);
1558
- const upstream = await forwardToDurableStreams(ctx, request, requestBody, `stream-meta`, requestUrl);
1507
+ rewriteSubscriptionBodyForBackend(payload, ctx.service, routingAdapter);
1508
+ return {
1509
+ ok: true,
1510
+ body: new TextEncoder().encode(JSON.stringify(payload)),
1511
+ targetWebhookUrl
1512
+ };
1513
+ }
1514
+ async function forwardSubscriptionRequest(request, ctx, routingAdapter, opts = {}) {
1515
+ const upstream = await forwardToDurableStreams(ctx, request, opts.body, `control`, opts.requestUrl, opts.bearerMode ?? `overwrite`);
1559
1516
  let responseBytes = upstream.body ? new Uint8Array(await upstream.arrayBuffer()) : new Uint8Array();
1560
1517
  responseBytes = rewriteSubscriptionResponseForClient(responseBytes, upstream, ctx.service, routingAdapter);
1561
- const response = responseFromUpstream$1(upstream, responseBytes);
1562
- if (!upstream.ok) return response;
1563
- if (request.method.toUpperCase() === `DELETE` && isSubscriptionBasePath(url.pathname)) await ctx.pgDb.delete(subscriptionWebhooks).where(and(eq(subscriptionWebhooks.tenantId, ctx.service), eq(subscriptionWebhooks.subscriptionId, subscriptionId)));
1564
- else if (targetWebhookUrl) await ctx.pgDb.insert(subscriptionWebhooks).values({
1518
+ return {
1519
+ upstream,
1520
+ response: responseFromUpstream$1(upstream, responseBytes)
1521
+ };
1522
+ }
1523
+ async function upsertSubscriptionWebhook(ctx, subscriptionId, targetWebhookUrl) {
1524
+ await ctx.pgDb.insert(subscriptionWebhooks).values({
1565
1525
  tenantId: ctx.service,
1566
1526
  subscriptionId,
1567
1527
  webhookUrl: targetWebhookUrl
@@ -1569,8 +1529,64 @@ async function subscriptionProxy(request, ctx) {
1569
1529
  target: [subscriptionWebhooks.tenantId, subscriptionWebhooks.subscriptionId],
1570
1530
  set: { webhookUrl: targetWebhookUrl }
1571
1531
  });
1532
+ }
1533
+ async function deleteSubscriptionWebhook(ctx, subscriptionId) {
1534
+ await ctx.pgDb.delete(subscriptionWebhooks).where(and(eq(subscriptionWebhooks.tenantId, ctx.service), eq(subscriptionWebhooks.subscriptionId, subscriptionId)));
1535
+ }
1536
+ function rewriteSubscriptionStreamPathInUrl(requestUrl, service, routingAdapter, streamPath) {
1537
+ const prefix = requestUrl.pathname.slice(0, requestUrl.pathname.indexOf(`/streams/`) + `/streams/`.length);
1538
+ requestUrl.pathname = `${prefix}${encodeURIComponent(routingAdapter.toBackendStreamPath(service, streamPath))}`;
1539
+ return requestUrl.toString();
1540
+ }
1541
+ async function putSubscriptionBase(request, ctx) {
1542
+ const subscriptionId = routeParam$2(request, `subscriptionId`);
1543
+ const routingAdapter = subscriptionRoutingAdapter(ctx);
1544
+ const rewrite = await rewriteSubscriptionRequestBody(request, ctx, subscriptionId, routingAdapter);
1545
+ if (!rewrite.ok) return rewrite.response;
1546
+ const { upstream, response } = await forwardSubscriptionRequest(request, ctx, routingAdapter, { body: rewrite.body });
1547
+ if (upstream.ok && rewrite.targetWebhookUrl) await upsertSubscriptionWebhook(ctx, subscriptionId, rewrite.targetWebhookUrl);
1572
1548
  return response;
1573
1549
  }
1550
+ async function getSubscriptionBase(request, ctx) {
1551
+ const routingAdapter = subscriptionRoutingAdapter(ctx);
1552
+ return (await forwardSubscriptionRequest(request, ctx, routingAdapter)).response;
1553
+ }
1554
+ async function deleteSubscriptionBase(request, ctx) {
1555
+ const subscriptionId = routeParam$2(request, `subscriptionId`);
1556
+ const routingAdapter = subscriptionRoutingAdapter(ctx);
1557
+ const { upstream, response } = await forwardSubscriptionRequest(request, ctx, routingAdapter);
1558
+ if (upstream.ok) await deleteSubscriptionWebhook(ctx, subscriptionId);
1559
+ return response;
1560
+ }
1561
+ async function postSubscriptionStreams(request, ctx) {
1562
+ const subscriptionId = routeParam$2(request, `subscriptionId`);
1563
+ const routingAdapter = subscriptionRoutingAdapter(ctx);
1564
+ const rewrite = await rewriteSubscriptionRequestBody(request, ctx, subscriptionId, routingAdapter);
1565
+ if (!rewrite.ok) return rewrite.response;
1566
+ return (await forwardSubscriptionRequest(request, ctx, routingAdapter, { body: rewrite.body })).response;
1567
+ }
1568
+ async function deleteSubscriptionStream(request, ctx) {
1569
+ const routingAdapter = subscriptionRoutingAdapter(ctx);
1570
+ const requestUrl = rewriteSubscriptionStreamPathInUrl(new URL(request.url), ctx.service, routingAdapter, routeParam$2(request, `streamPath`));
1571
+ return (await forwardSubscriptionRequest(request, ctx, routingAdapter, { requestUrl })).response;
1572
+ }
1573
+ function subscriptionAction(action) {
1574
+ return async (request, ctx) => {
1575
+ const subscriptionId = routeParam$2(request, `subscriptionId`);
1576
+ const routingAdapter = subscriptionRoutingAdapter(ctx);
1577
+ const rewrite = await rewriteSubscriptionRequestBody(request, ctx, subscriptionId, routingAdapter);
1578
+ if (!rewrite.ok) return rewrite.response;
1579
+ const bearerMode = action === `ack` || action === `release` || action === `callback` ? `if-missing` : `overwrite`;
1580
+ return (await forwardSubscriptionRequest(request, ctx, routingAdapter, {
1581
+ body: rewrite.body,
1582
+ bearerMode
1583
+ })).response;
1584
+ };
1585
+ }
1586
+ async function controlPassThrough(request, ctx) {
1587
+ const upstream = await forwardToDurableStreams(ctx, request, void 0, `control`);
1588
+ return responseFromUpstream$1(upstream);
1589
+ }
1574
1590
  async function streamAppend(request, ctx) {
1575
1591
  return await electricAgentsStreamAppendRouter.fetch(createStreamAppendRouteRequest(request), ctx.runtime, (req, body) => forwardFetchRequest({
1576
1592
  request: {
@@ -1591,10 +1607,9 @@ async function proxyPassThrough(request, ctx) {
1591
1607
  const upstream = await forwardToDurableStreams(ctx, request);
1592
1608
  const streamPath = new URL(request.url).pathname;
1593
1609
  const method = request.method.toUpperCase();
1594
- const isControlPath = streamPath.startsWith(`/v1/stream-meta/`);
1595
- const endTrackedRead = method === `GET` && !isControlPath ? await ctx.entityBridgeManager.beginClientRead(streamPath) : null;
1610
+ const endTrackedRead = method === `GET` ? await ctx.entityBridgeManager.beginClientRead(streamPath) : null;
1596
1611
  try {
1597
- if (method === `HEAD` && !isControlPath) await ctx.entityBridgeManager.touchByStreamPath(streamPath);
1612
+ if (method === `HEAD`) await ctx.entityBridgeManager.touchByStreamPath(streamPath);
1598
1613
  return responseFromUpstream$1(upstream);
1599
1614
  } finally {
1600
1615
  await endTrackedRead?.();
@@ -4581,6 +4596,12 @@ function getRequestSpan(req) {
4581
4596
  return carrier(req)[SPAN_KEY];
4582
4597
  }
4583
4598
 
4599
+ //#endregion
4600
+ //#region src/routing/tenant-stream-paths.ts
4601
+ function withLeadingSlash(path$1) {
4602
+ return path$1.startsWith(`/`) ? path$1 : `/${path$1}`;
4603
+ }
4604
+
4584
4605
  //#endregion
4585
4606
  //#region src/routing/runners-router.ts
4586
4607
  const registerRunnerBodySchema = Type.Object({
@@ -4904,7 +4925,7 @@ async function webhookForward(request, ctx) {
4904
4925
  let runningEntityUrl = null;
4905
4926
  const parsedBody = parsedBodyResult.value;
4906
4927
  const newWebhook = newWebhookPayload(parsedBody);
4907
- const routingAdapter = resolveDurableStreamsRoutingAdapter(ctx.durableStreamsRouting);
4928
+ const routingAdapter = resolveDurableStreamsRoutingAdapter(ctx.durableStreamsRouting, ctx.durableStreamsUrl);
4908
4929
  if (parsedBody) {
4909
4930
  const rawPrimaryStream = newWebhook?.primaryStream ?? parsedBody.primary_stream ?? parsedBody.primaryStream ?? parsedBody.streamPath ?? null;
4910
4931
  const primaryStream = typeof rawPrimaryStream === `string` ? toRuntimeStreamPath(rawPrimaryStream, ctx.service, routingAdapter) : null;
@@ -5033,7 +5054,7 @@ async function callbackForward(request, ctx) {
5033
5054
  }
5034
5055
  return json(responseBody);
5035
5056
  }
5036
- const upstreamBody = encodeCallbackForwardBody(ctx.service, consumerId, requestBody, resolveDurableStreamsRoutingAdapter(ctx.durableStreamsRouting));
5057
+ const upstreamBody = encodeCallbackForwardBody(ctx.service, consumerId, requestBody, resolveDurableStreamsRoutingAdapter(ctx.durableStreamsRouting, ctx.durableStreamsUrl));
5037
5058
  let upstream;
5038
5059
  try {
5039
5060
  const subscriptionId = durableStreamsSubscriptionCallback(target.callbackUrl);
@@ -6202,7 +6223,7 @@ var ElectricAgentsTenantRuntime = class {
6202
6223
  this.service = this.serviceId;
6203
6224
  this.db = options.db;
6204
6225
  if (options.streamClient) this.streamClient = options.streamClient;
6205
- else if (options.durableStreamsUrl) this.streamClient = new StreamClient(durableStreamsServiceUrl(options.durableStreamsUrl, this.serviceId), { bearer: options.durableStreamsBearer });
6226
+ else if (options.durableStreamsUrl) this.streamClient = new StreamClient(durableStreamsServiceUrl(options.durableStreamsUrl, this.serviceId, { scope: `stream-root` }), { bearer: options.durableStreamsBearer });
6206
6227
  else throw new Error(`Either durableStreamsUrl or streamClient is required`);
6207
6228
  this.registry = options.registry ?? new PostgresRegistry(this.db, this.serviceId);
6208
6229
  this.wakeRegistry = options.wakeRegistry;
@@ -7120,7 +7141,7 @@ var WakeRegistry = class {
7120
7141
  //#region src/standalone-runtime.ts
7121
7142
  async function startStandaloneAgentsRuntime(options) {
7122
7143
  const serviceId = options.service ?? options.tenantId ?? DEFAULT_TENANT_ID;
7123
- const streamClient = options.streamClient ?? (options.durableStreamsUrl ? new StreamClient(durableStreamsServiceUrl(options.durableStreamsUrl, serviceId), { bearer: options.durableStreamsBearer }) : void 0);
7144
+ const streamClient = options.streamClient ?? (options.durableStreamsUrl ? new StreamClient(durableStreamsServiceUrl(options.durableStreamsUrl, serviceId, { scope: `stream-root` }), { bearer: options.durableStreamsBearer }) : void 0);
7124
7145
  if (!streamClient) throw new Error(`Either durableStreamsUrl or streamClient is required`);
7125
7146
  const registry = new PostgresRegistry(options.db, serviceId);
7126
7147
  const wakeRegistry = options.wakeRegistry ?? new WakeRegistry(options.db, serviceId);
@@ -7284,7 +7305,7 @@ var ElectricAgentsServer = class {
7284
7305
  constructor(options) {
7285
7306
  if (!options.durableStreamsUrl && !options.durableStreamsServer) throw new Error(`Either durableStreamsUrl or durableStreamsServer is required`);
7286
7307
  this.options = options;
7287
- this.streamClient = options.durableStreamsUrl ? new StreamClient(durableStreamsServiceUrl(options.durableStreamsUrl, this.tenantId), { bearer: options.durableStreamsBearer }) : null;
7308
+ this.streamClient = options.durableStreamsUrl ? new StreamClient(durableStreamsServiceUrl(options.durableStreamsUrl, this.tenantId, { scope: `stream-root` }), { bearer: options.durableStreamsBearer }) : null;
7288
7309
  }
7289
7310
  get url() {
7290
7311
  if (!this._url) throw new Error(`Server not started`);
@@ -7305,7 +7326,7 @@ var ElectricAgentsServer = class {
7305
7326
  const streamsUrl = await this.options.durableStreamsServer.start();
7306
7327
  serverLog.info(`[agent-server] durable streams server started at ${streamsUrl}`);
7307
7328
  this.options.durableStreamsUrl = streamsUrl;
7308
- this.streamClient = new StreamClient(durableStreamsServiceUrl(streamsUrl, this.tenantId), { bearer: this.options.durableStreamsBearer });
7329
+ this.streamClient = new StreamClient(durableStreamsServiceUrl(streamsUrl, this.tenantId, { scope: `stream-root` }), { bearer: this.options.durableStreamsBearer });
7309
7330
  }
7310
7331
  this.streamsAgent = new Agent({
7311
7332
  keepAliveTimeout: 6e4,
@@ -7435,9 +7456,9 @@ var ElectricAgentsServer = class {
7435
7456
  principal,
7436
7457
  publicUrl: this.publicUrl,
7437
7458
  localUrl: this._url,
7438
- durableStreamsUrl: this.options.durableStreamsUrl,
7459
+ durableStreamsUrl: this.streamClient.baseUrl,
7439
7460
  durableStreamsBearer: this.options.durableStreamsBearer,
7440
- durableStreamsRouting: this.options.durableStreamsRouting ?? pathPrefixedSingleTenantDurableStreamsRoutingAdapter,
7461
+ durableStreamsRouting: this.options.durableStreamsRouting,
7441
7462
  durableStreamsDispatcher: this.streamsAgent,
7442
7463
  electricUrl: this.options.electricUrl,
7443
7464
  electricSecret: this.options.electricSecret,