@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.
package/dist/index.js CHANGED
@@ -4177,11 +4177,18 @@ function durableStreamsBearerHeaders(bearer) {
4177
4177
  if (!bearer) return void 0;
4178
4178
  return { authorization: async () => await resolveDurableStreamsBearer(bearer) ?? `` };
4179
4179
  }
4180
- function durableStreamsServiceUrl(baseUrl, serviceId) {
4180
+ function durableStreamsServiceUrl(baseUrl, serviceId, options = {}) {
4181
4181
  const url = new URL(baseUrl);
4182
- if (/^\/v1\/stream\/[^/]+\/?$/.test(url.pathname)) return baseUrl.replace(/\/+$/, ``);
4183
- const base = baseUrl.replace(/\/+$/, ``);
4184
- return `${base}/v1/stream/${encodeURIComponent(serviceId)}`;
4182
+ if (/\/v1\/streams\/[^/]+\/?$/.test(url.pathname)) return baseUrl.replace(/\/+$/, ``);
4183
+ if (/\/v1\/stream\/[^/]+\/?$/.test(url.pathname)) return baseUrl.replace(/\/+$/, ``);
4184
+ const scope = options.scope ?? `service`;
4185
+ const encodedServiceId = encodeURIComponent(serviceId);
4186
+ const path$1 = url.pathname.replace(/\/+$/, ``) || `/`;
4187
+ if (path$1.endsWith(`/v1/streams`)) url.pathname = `${path$1}/${encodedServiceId}`;
4188
+ else if (path$1.endsWith(`/v1/stream`)) url.pathname = scope === `service` ? `${path$1}/${encodedServiceId}` : path$1;
4189
+ else if (scope === `stream-root`) url.pathname = `${path$1 === `/` ? `` : path$1}/v1/stream`;
4190
+ else url.pathname = `${path$1 === `/` ? `` : path$1}/v1/stream/${encodedServiceId}`;
4191
+ return url.toString().replace(/\/+$/, ``);
4185
4192
  }
4186
4193
  function isNotFoundError(err) {
4187
4194
  return err instanceof DurableStreamError && err.code === ErrCodeNotFound || err instanceof FetchError && err.status === 404;
@@ -4214,34 +4221,15 @@ var StreamClient = class {
4214
4221
  await applyDurableStreamsBearer(headers, this.options.bearer, { overwrite: opts.overwriteBearer });
4215
4222
  return headers;
4216
4223
  }
4217
- subscriptionServiceId() {
4218
- const url = new URL(this.baseUrl);
4219
- const match = /^(.*)\/v1\/stream\/([^/]+)\/?$/.exec(url.pathname);
4220
- return match ? decodeURIComponent(match[2]) : null;
4221
- }
4222
4224
  backendSubscriptionPath(path$1) {
4223
- const normalized = normalizeSubscriptionPath(path$1);
4224
- const serviceId = this.subscriptionServiceId();
4225
- if (!serviceId) return normalized;
4226
- if (normalized === serviceId || normalized.startsWith(`${serviceId}/`)) return normalized;
4227
- return `${serviceId}/${normalized}`;
4225
+ return normalizeSubscriptionPath(path$1);
4228
4226
  }
4229
4227
  runtimeSubscriptionPath(path$1) {
4230
- const normalized = normalizeSubscriptionPath(path$1);
4231
- const serviceId = this.subscriptionServiceId();
4232
- if (!serviceId) return normalized;
4233
- return normalized.startsWith(`${serviceId}/`) ? normalized.slice(serviceId.length + 1) : normalized;
4228
+ return normalizeSubscriptionPath(path$1);
4234
4229
  }
4235
4230
  subscriptionUrl(subscriptionId) {
4236
4231
  const url = new URL(this.baseUrl);
4237
- const match = /^(.*)\/v1\/stream\/([^/]+)\/?$/.exec(url.pathname);
4238
- if (match) {
4239
- const [, prefix = ``, serviceId] = match;
4240
- url.pathname = `${prefix}/v1/stream-meta/subscriptions/${encodeURIComponent(subscriptionId)}`;
4241
- url.searchParams.set(`service`, decodeURIComponent(serviceId));
4242
- return url.toString();
4243
- }
4244
- url.pathname = `${url.pathname.replace(/\/+$/, ``)}/v1/stream-meta/subscriptions/${encodeURIComponent(subscriptionId)}`;
4232
+ url.pathname = `${url.pathname.replace(/\/+$/, ``)}/__ds/subscriptions/${encodeURIComponent(subscriptionId)}`;
4245
4233
  return url.toString();
4246
4234
  }
4247
4235
  subscriptionChildUrl(subscriptionId, ...segments) {
@@ -4656,7 +4644,7 @@ var ElectricAgentsTenantRuntime = class {
4656
4644
  this.service = this.serviceId;
4657
4645
  this.db = options.db;
4658
4646
  if (options.streamClient) this.streamClient = options.streamClient;
4659
- else if (options.durableStreamsUrl) this.streamClient = new StreamClient(durableStreamsServiceUrl(options.durableStreamsUrl, this.serviceId), { bearer: options.durableStreamsBearer });
4647
+ else if (options.durableStreamsUrl) this.streamClient = new StreamClient(durableStreamsServiceUrl(options.durableStreamsUrl, this.serviceId, { scope: `stream-root` }), { bearer: options.durableStreamsBearer });
4660
4648
  else throw new Error(`Either durableStreamsUrl or streamClient is required`);
4661
4649
  this.registry = options.registry ?? new PostgresRegistry(this.db, this.serviceId);
4662
4650
  this.wakeRegistry = options.wakeRegistry;
@@ -5925,27 +5913,6 @@ function validateParsedBody(schema, parsed) {
5925
5913
  };
5926
5914
  }
5927
5915
 
5928
- //#endregion
5929
- //#region src/routing/tenant-stream-paths.ts
5930
- function withoutLeadingSlash(path$1) {
5931
- return path$1.replace(/^\/+/, ``);
5932
- }
5933
- function withLeadingSlash(path$1) {
5934
- return path$1.startsWith(`/`) ? path$1 : `/${path$1}`;
5935
- }
5936
- function prefixTenantStreamPath(path$1, tenantId) {
5937
- const normalized = withoutLeadingSlash(path$1);
5938
- if (!normalized || normalized === tenantId) return tenantId;
5939
- if (normalized.startsWith(`${tenantId}/`)) return normalized;
5940
- return `${tenantId}/${normalized}`;
5941
- }
5942
- function stripTenantStreamPrefix(path$1, tenantId) {
5943
- const normalized = withoutLeadingSlash(path$1);
5944
- if (normalized === tenantId) return ``;
5945
- if (normalized.startsWith(`${tenantId}/`)) return normalized.slice(tenantId.length + 1);
5946
- return normalized;
5947
- }
5948
-
5949
5916
  //#endregion
5950
5917
  //#region src/routing/durable-streams-routing-adapter.ts
5951
5918
  function appendSearch(target, source) {
@@ -5956,43 +5923,30 @@ function removeServiceQuery(target) {
5956
5923
  target.searchParams.delete(`service`);
5957
5924
  return target;
5958
5925
  }
5959
- function logicalStreamPathFromRequest(requestUrl, serviceId) {
5960
- const incomingUrl = new URL(requestUrl, `http://localhost`);
5961
- const segments = incomingUrl.pathname.split(`/`).filter(Boolean);
5962
- if (segments[0] === `v1` && segments[1] === `stream`) return {
5963
- incomingUrl,
5964
- streamPath: segments.length > 2 ? `/${segments.slice(3).join(`/`)}` : `/`
5965
- };
5966
- return {
5967
- incomingUrl,
5968
- streamPath: incomingUrl.pathname || `/${serviceId}`
5969
- };
5970
- }
5971
- function backendStreamUrl(input, backendStreamPath) {
5972
- const path$1 = backendStreamPath.replace(/^\/+/, ``);
5973
- const target = new URL(`/v1/stream/${path$1}`, input.durableStreamsUrl);
5974
- return target;
5926
+ function withoutTrailingSlash(pathname) {
5927
+ return pathname.replace(/\/+$/, ``) || `/`;
5975
5928
  }
5976
- function streamMetaUrlWithoutService(input) {
5929
+ function appendRequestPathToStreamRoot(input) {
5977
5930
  const incomingUrl = new URL(input.requestUrl, `http://localhost`);
5978
- return removeServiceQuery(appendSearch(new URL(incomingUrl.pathname, input.durableStreamsUrl), incomingUrl));
5979
- }
5980
- const pathPrefixedSingleTenantDurableStreamsRoutingAdapter = {
5981
- streamUrl(input) {
5982
- const { incomingUrl, streamPath } = logicalStreamPathFromRequest(input.requestUrl, input.serviceId);
5983
- const target = backendStreamUrl(input, prefixTenantStreamPath(streamPath, input.serviceId));
5984
- return removeServiceQuery(appendSearch(target, incomingUrl));
5985
- },
5986
- streamMetaUrl: streamMetaUrlWithoutService,
5987
- toBackendStreamPath(serviceId, streamPath) {
5988
- return prefixTenantStreamPath(streamPath, serviceId);
5931
+ const path$1 = incomingUrl.pathname.replace(/^\/+/, ``);
5932
+ const target = new URL(input.durableStreamsUrl);
5933
+ target.pathname = path$1 ? `${withoutTrailingSlash(target.pathname)}/${path$1}` : withoutTrailingSlash(target.pathname);
5934
+ return removeServiceQuery(appendSearch(target, incomingUrl));
5935
+ }
5936
+ const streamRootDurableStreamsRoutingAdapter = {
5937
+ streamUrl: appendRequestPathToStreamRoot,
5938
+ controlUrl: appendRequestPathToStreamRoot,
5939
+ toBackendStreamPath(_serviceId, streamPath) {
5940
+ return streamPath.replace(/^\/+/, ``);
5989
5941
  },
5990
- toRuntimeStreamPath(serviceId, streamPath) {
5991
- return stripTenantStreamPrefix(streamPath, serviceId);
5942
+ toRuntimeStreamPath(_serviceId, streamPath) {
5943
+ return streamPath.replace(/^\/+/, ``);
5992
5944
  }
5993
5945
  };
5994
- function resolveDurableStreamsRoutingAdapter(adapter) {
5995
- return adapter ?? pathPrefixedSingleTenantDurableStreamsRoutingAdapter;
5946
+ const pathPrefixedSingleTenantDurableStreamsRoutingAdapter = streamRootDurableStreamsRoutingAdapter;
5947
+ const tenantRootDurableStreamsRoutingAdapter = streamRootDurableStreamsRoutingAdapter;
5948
+ function resolveDurableStreamsRoutingAdapter(adapter, _durableStreamsUrl) {
5949
+ return adapter ?? streamRootDurableStreamsRoutingAdapter;
5996
5950
  }
5997
5951
 
5998
5952
  //#endregion
@@ -6029,13 +5983,13 @@ function buildElectricProxyTarget(options) {
6029
5983
  return target;
6030
5984
  }
6031
5985
  async function forwardFetchRequest(options) {
6032
- const routingAdapter = resolveDurableStreamsRoutingAdapter(options.durableStreamsRouting);
5986
+ const routingAdapter = resolveDurableStreamsRoutingAdapter(options.durableStreamsRouting, options.durableStreamsUrl);
6033
5987
  const routingInput = {
6034
5988
  durableStreamsUrl: options.durableStreamsUrl,
6035
5989
  serviceId: options.serviceId,
6036
5990
  requestUrl: options.request.url
6037
5991
  };
6038
- const upstreamUrl = options.route === `stream-meta` ? routingAdapter.streamMetaUrl(routingInput) : routingAdapter.streamUrl(routingInput);
5992
+ const upstreamUrl = options.route === `control` ? routingAdapter.controlUrl(routingInput) : routingAdapter.streamUrl(routingInput);
6039
5993
  const headers = new Headers(options.request.headers);
6040
5994
  if (options.durableStreamsBearerMode !== `none`) await applyDurableStreamsBearer(headers, options.durableStreamsBearer, { overwrite: options.durableStreamsBearerMode !== `if-missing` });
6041
5995
  const init = {
@@ -6073,8 +6027,21 @@ function sqlStringLiteral(value) {
6073
6027
  //#endregion
6074
6028
  //#region src/routing/durable-streams-router.ts
6075
6029
  const subscriptionProxyBodySchema = Type.Object({ webhook: Type.Optional(Type.Object({ url: Type.String() }, { additionalProperties: true })) }, { additionalProperties: true });
6030
+ const subscriptionControlActions = [
6031
+ `callback`,
6032
+ `claim`,
6033
+ `ack`,
6034
+ `release`
6035
+ ];
6076
6036
  const durableStreamsRouter = Router();
6077
- durableStreamsRouter.all(`/v1/stream-meta/subscriptions/*`, subscriptionProxy);
6037
+ durableStreamsRouter.put(`/__ds/subscriptions/:subscriptionId`, putSubscriptionBase);
6038
+ durableStreamsRouter.get(`/__ds/subscriptions/:subscriptionId`, getSubscriptionBase);
6039
+ durableStreamsRouter.delete(`/__ds/subscriptions/:subscriptionId`, deleteSubscriptionBase);
6040
+ durableStreamsRouter.post(`/__ds/subscriptions/:subscriptionId/streams`, postSubscriptionStreams);
6041
+ durableStreamsRouter.delete(`/__ds/subscriptions/:subscriptionId/streams/:streamPath+`, deleteSubscriptionStream);
6042
+ for (const action of subscriptionControlActions) durableStreamsRouter.post(`/__ds/subscriptions/:subscriptionId/${action}`, subscriptionAction(action));
6043
+ durableStreamsRouter.all(`/__ds`, controlPassThrough);
6044
+ durableStreamsRouter.all(`/__ds/*`, controlPassThrough);
6078
6045
  durableStreamsRouter.post(`*`, streamAppend);
6079
6046
  durableStreamsRouter.all(`*`, proxyPassThrough);
6080
6047
  function bodyFromBytes$1(body) {
@@ -6087,7 +6054,7 @@ function responseFromUpstream$1(response, body) {
6087
6054
  headers: responseHeaders(response)
6088
6055
  });
6089
6056
  }
6090
- async function forwardToDurableStreams(ctx, request, body, route = `stream`, urlOverride) {
6057
+ async function forwardToDurableStreams(ctx, request, body, route = `stream`, urlOverride, durableStreamsBearerMode = `overwrite`) {
6091
6058
  const headers = new Headers(request.headers);
6092
6059
  headers.delete(`host`);
6093
6060
  let requestBody = body;
@@ -6101,24 +6068,13 @@ async function forwardToDurableStreams(ctx, request, body, route = `stream`, url
6101
6068
  body: requestBody,
6102
6069
  durableStreamsUrl: ctx.durableStreamsUrl,
6103
6070
  durableStreamsBearer: ctx.durableStreamsBearer,
6104
- durableStreamsBearerMode: usesSubscriptionScopedBearer(urlOverride ?? request.url) ? `if-missing` : `overwrite`,
6071
+ durableStreamsBearerMode,
6105
6072
  durableStreamsRouting: ctx.durableStreamsRouting,
6106
6073
  serviceId: ctx.service,
6107
6074
  dispatcher: ctx.durableStreamsDispatcher,
6108
6075
  route
6109
6076
  });
6110
6077
  }
6111
- function subscriptionIdFromPath(pathname) {
6112
- const match = /^\/v1\/stream-meta\/subscriptions\/([^/]+)(?:\/.*)?$/.exec(pathname);
6113
- return match ? decodeURIComponent(match[1]) : null;
6114
- }
6115
- function isSubscriptionBasePath(pathname) {
6116
- return /^\/v1\/stream-meta\/subscriptions\/[^/]+\/?$/.test(pathname);
6117
- }
6118
- function usesSubscriptionScopedBearer(requestUrl) {
6119
- const pathname = new URL(requestUrl, `http://localhost`).pathname;
6120
- return /^\/v1\/stream-meta\/subscriptions\/[^/]+\/(?:ack|release|callback)\/?$/.test(pathname);
6121
- }
6122
6078
  function rewriteSubscriptionBodyForBackend(payload, service, routingAdapter) {
6123
6079
  if (typeof payload.pattern === `string`) payload.pattern = routingAdapter.toBackendStreamPath(service, payload.pattern);
6124
6080
  if (Array.isArray(payload.streams)) payload.streams = payload.streams.map((stream) => typeof stream === `string` ? routingAdapter.toBackendStreamPath(service, stream) : stream);
@@ -6163,44 +6119,50 @@ function decodeJson(bytes) {
6163
6119
  return null;
6164
6120
  }
6165
6121
  }
6166
- function rewriteSubscriptionStreamPathInUrl(requestUrl, service, routingAdapter) {
6167
- const match = /^(\/v1\/stream-meta\/subscriptions\/[^/]+\/streams\/)(.+)$/.exec(requestUrl.pathname);
6168
- if (!match) return requestUrl.toString();
6169
- const [, prefix, encodedPath] = match;
6170
- const streamPath = decodeURIComponent(encodedPath);
6171
- requestUrl.pathname = `${prefix}${encodeURIComponent(routingAdapter.toBackendStreamPath(service, streamPath))}`;
6172
- return requestUrl.toString();
6122
+ function routeParam$2(request, name) {
6123
+ const value = request.params[name];
6124
+ const raw = Array.isArray(value) ? value[0] : value;
6125
+ return decodeURIComponent(raw ?? ``);
6126
+ }
6127
+ function subscriptionRoutingAdapter(ctx) {
6128
+ return resolveDurableStreamsRoutingAdapter(ctx.durableStreamsRouting, ctx.durableStreamsUrl);
6173
6129
  }
6174
- async function subscriptionProxy(request, ctx) {
6175
- const url = new URL(request.url);
6176
- const subscriptionId = subscriptionIdFromPath(url.pathname);
6177
- if (!subscriptionId) return void 0;
6178
- const routingAdapter = resolveDurableStreamsRoutingAdapter(ctx.durableStreamsRouting);
6179
- let requestBody;
6130
+ async function rewriteSubscriptionRequestBody(request, ctx, subscriptionId, routingAdapter) {
6131
+ const body = await readRequestBody(request);
6132
+ if (body.length === 0) return {
6133
+ ok: true,
6134
+ body,
6135
+ targetWebhookUrl: null
6136
+ };
6137
+ const validation = validateBody(subscriptionProxyBodySchema, body);
6138
+ if (!validation.ok) return {
6139
+ ok: false,
6140
+ response: validation.response
6141
+ };
6142
+ const payload = validation.value;
6180
6143
  let targetWebhookUrl = null;
6181
- let requestUrl = request.url;
6182
- if ([`PUT`, `POST`].includes(request.method.toUpperCase())) {
6183
- requestBody = await readRequestBody(request);
6184
- if (requestBody.length > 0) {
6185
- const validation = validateBody(subscriptionProxyBodySchema, requestBody);
6186
- if (!validation.ok) return validation.response;
6187
- const payload = validation.value;
6188
- if (payload.webhook?.url !== void 0) {
6189
- targetWebhookUrl = rewriteLoopbackWebhookUrl(payload.webhook.url) ?? null;
6190
- payload.webhook.url = appendPathToUrl(ctx.publicUrl, `/_electric/webhook-forward/${encodeURIComponent(subscriptionId)}`);
6191
- }
6192
- rewriteSubscriptionBodyForBackend(payload, ctx.service, routingAdapter);
6193
- requestBody = new TextEncoder().encode(JSON.stringify(payload));
6194
- }
6144
+ if (payload.webhook?.url !== void 0) {
6145
+ targetWebhookUrl = rewriteLoopbackWebhookUrl(payload.webhook.url) ?? null;
6146
+ payload.webhook.url = appendPathToUrl(ctx.publicUrl, `/_electric/webhook-forward/${encodeURIComponent(subscriptionId)}`);
6195
6147
  }
6196
- if (request.method.toUpperCase() === `DELETE` && /\/streams\/.+$/.test(url.pathname)) requestUrl = rewriteSubscriptionStreamPathInUrl(url, ctx.service, routingAdapter);
6197
- const upstream = await forwardToDurableStreams(ctx, request, requestBody, `stream-meta`, requestUrl);
6148
+ rewriteSubscriptionBodyForBackend(payload, ctx.service, routingAdapter);
6149
+ return {
6150
+ ok: true,
6151
+ body: new TextEncoder().encode(JSON.stringify(payload)),
6152
+ targetWebhookUrl
6153
+ };
6154
+ }
6155
+ async function forwardSubscriptionRequest(request, ctx, routingAdapter, opts = {}) {
6156
+ const upstream = await forwardToDurableStreams(ctx, request, opts.body, `control`, opts.requestUrl, opts.bearerMode ?? `overwrite`);
6198
6157
  let responseBytes = upstream.body ? new Uint8Array(await upstream.arrayBuffer()) : new Uint8Array();
6199
6158
  responseBytes = rewriteSubscriptionResponseForClient(responseBytes, upstream, ctx.service, routingAdapter);
6200
- const response = responseFromUpstream$1(upstream, responseBytes);
6201
- if (!upstream.ok) return response;
6202
- if (request.method.toUpperCase() === `DELETE` && isSubscriptionBasePath(url.pathname)) await ctx.pgDb.delete(subscriptionWebhooks).where(and(eq(subscriptionWebhooks.tenantId, ctx.service), eq(subscriptionWebhooks.subscriptionId, subscriptionId)));
6203
- else if (targetWebhookUrl) await ctx.pgDb.insert(subscriptionWebhooks).values({
6159
+ return {
6160
+ upstream,
6161
+ response: responseFromUpstream$1(upstream, responseBytes)
6162
+ };
6163
+ }
6164
+ async function upsertSubscriptionWebhook(ctx, subscriptionId, targetWebhookUrl) {
6165
+ await ctx.pgDb.insert(subscriptionWebhooks).values({
6204
6166
  tenantId: ctx.service,
6205
6167
  subscriptionId,
6206
6168
  webhookUrl: targetWebhookUrl
@@ -6208,8 +6170,64 @@ async function subscriptionProxy(request, ctx) {
6208
6170
  target: [subscriptionWebhooks.tenantId, subscriptionWebhooks.subscriptionId],
6209
6171
  set: { webhookUrl: targetWebhookUrl }
6210
6172
  });
6173
+ }
6174
+ async function deleteSubscriptionWebhook(ctx, subscriptionId) {
6175
+ await ctx.pgDb.delete(subscriptionWebhooks).where(and(eq(subscriptionWebhooks.tenantId, ctx.service), eq(subscriptionWebhooks.subscriptionId, subscriptionId)));
6176
+ }
6177
+ function rewriteSubscriptionStreamPathInUrl(requestUrl, service, routingAdapter, streamPath) {
6178
+ const prefix = requestUrl.pathname.slice(0, requestUrl.pathname.indexOf(`/streams/`) + `/streams/`.length);
6179
+ requestUrl.pathname = `${prefix}${encodeURIComponent(routingAdapter.toBackendStreamPath(service, streamPath))}`;
6180
+ return requestUrl.toString();
6181
+ }
6182
+ async function putSubscriptionBase(request, ctx) {
6183
+ const subscriptionId = routeParam$2(request, `subscriptionId`);
6184
+ const routingAdapter = subscriptionRoutingAdapter(ctx);
6185
+ const rewrite = await rewriteSubscriptionRequestBody(request, ctx, subscriptionId, routingAdapter);
6186
+ if (!rewrite.ok) return rewrite.response;
6187
+ const { upstream, response } = await forwardSubscriptionRequest(request, ctx, routingAdapter, { body: rewrite.body });
6188
+ if (upstream.ok && rewrite.targetWebhookUrl) await upsertSubscriptionWebhook(ctx, subscriptionId, rewrite.targetWebhookUrl);
6189
+ return response;
6190
+ }
6191
+ async function getSubscriptionBase(request, ctx) {
6192
+ const routingAdapter = subscriptionRoutingAdapter(ctx);
6193
+ return (await forwardSubscriptionRequest(request, ctx, routingAdapter)).response;
6194
+ }
6195
+ async function deleteSubscriptionBase(request, ctx) {
6196
+ const subscriptionId = routeParam$2(request, `subscriptionId`);
6197
+ const routingAdapter = subscriptionRoutingAdapter(ctx);
6198
+ const { upstream, response } = await forwardSubscriptionRequest(request, ctx, routingAdapter);
6199
+ if (upstream.ok) await deleteSubscriptionWebhook(ctx, subscriptionId);
6211
6200
  return response;
6212
6201
  }
6202
+ async function postSubscriptionStreams(request, ctx) {
6203
+ const subscriptionId = routeParam$2(request, `subscriptionId`);
6204
+ const routingAdapter = subscriptionRoutingAdapter(ctx);
6205
+ const rewrite = await rewriteSubscriptionRequestBody(request, ctx, subscriptionId, routingAdapter);
6206
+ if (!rewrite.ok) return rewrite.response;
6207
+ return (await forwardSubscriptionRequest(request, ctx, routingAdapter, { body: rewrite.body })).response;
6208
+ }
6209
+ async function deleteSubscriptionStream(request, ctx) {
6210
+ const routingAdapter = subscriptionRoutingAdapter(ctx);
6211
+ const requestUrl = rewriteSubscriptionStreamPathInUrl(new URL(request.url), ctx.service, routingAdapter, routeParam$2(request, `streamPath`));
6212
+ return (await forwardSubscriptionRequest(request, ctx, routingAdapter, { requestUrl })).response;
6213
+ }
6214
+ function subscriptionAction(action) {
6215
+ return async (request, ctx) => {
6216
+ const subscriptionId = routeParam$2(request, `subscriptionId`);
6217
+ const routingAdapter = subscriptionRoutingAdapter(ctx);
6218
+ const rewrite = await rewriteSubscriptionRequestBody(request, ctx, subscriptionId, routingAdapter);
6219
+ if (!rewrite.ok) return rewrite.response;
6220
+ const bearerMode = action === `ack` || action === `release` || action === `callback` ? `if-missing` : `overwrite`;
6221
+ return (await forwardSubscriptionRequest(request, ctx, routingAdapter, {
6222
+ body: rewrite.body,
6223
+ bearerMode
6224
+ })).response;
6225
+ };
6226
+ }
6227
+ async function controlPassThrough(request, ctx) {
6228
+ const upstream = await forwardToDurableStreams(ctx, request, void 0, `control`);
6229
+ return responseFromUpstream$1(upstream);
6230
+ }
6213
6231
  async function streamAppend(request, ctx) {
6214
6232
  return await electricAgentsStreamAppendRouter.fetch(createStreamAppendRouteRequest(request), ctx.runtime, (req, body) => forwardFetchRequest({
6215
6233
  request: {
@@ -6230,10 +6248,9 @@ async function proxyPassThrough(request, ctx) {
6230
6248
  const upstream = await forwardToDurableStreams(ctx, request);
6231
6249
  const streamPath = new URL(request.url).pathname;
6232
6250
  const method = request.method.toUpperCase();
6233
- const isControlPath = streamPath.startsWith(`/v1/stream-meta/`);
6234
- const endTrackedRead = method === `GET` && !isControlPath ? await ctx.entityBridgeManager.beginClientRead(streamPath) : null;
6251
+ const endTrackedRead = method === `GET` ? await ctx.entityBridgeManager.beginClientRead(streamPath) : null;
6235
6252
  try {
6236
- if (method === `HEAD` && !isControlPath) await ctx.entityBridgeManager.touchByStreamPath(streamPath);
6253
+ if (method === `HEAD`) await ctx.entityBridgeManager.touchByStreamPath(streamPath);
6237
6254
  return responseFromUpstream$1(upstream);
6238
6255
  } finally {
6239
6256
  await endTrackedRead?.();
@@ -6577,11 +6594,11 @@ async function spawnEntity(request, ctx) {
6577
6594
  wake: parsed.wake,
6578
6595
  created_by: principal.url
6579
6596
  });
6597
+ await linkEntityDispatchSubscription(ctx, entity);
6580
6598
  if (parsed.initialMessage !== void 0) await ctx.entityManager.send(entity.url, {
6581
6599
  from: principal.url,
6582
6600
  payload: parsed.initialMessage
6583
6601
  });
6584
- await linkEntityDispatchSubscription(ctx, entity);
6585
6602
  return json({
6586
6603
  ...toPublicEntity(entity),
6587
6604
  txid: entity.txid
@@ -6780,6 +6797,12 @@ function getRequestSpan(req) {
6780
6797
  return carrier(req)[SPAN_KEY];
6781
6798
  }
6782
6799
 
6800
+ //#endregion
6801
+ //#region src/routing/tenant-stream-paths.ts
6802
+ function withLeadingSlash(path$1) {
6803
+ return path$1.startsWith(`/`) ? path$1 : `/${path$1}`;
6804
+ }
6805
+
6783
6806
  //#endregion
6784
6807
  //#region src/routing/runners-router.ts
6785
6808
  const registerRunnerBodySchema = Type.Object({
@@ -7103,7 +7126,7 @@ async function webhookForward(request, ctx) {
7103
7126
  let runningEntityUrl = null;
7104
7127
  const parsedBody = parsedBodyResult.value;
7105
7128
  const newWebhook = newWebhookPayload(parsedBody);
7106
- const routingAdapter = resolveDurableStreamsRoutingAdapter(ctx.durableStreamsRouting);
7129
+ const routingAdapter = resolveDurableStreamsRoutingAdapter(ctx.durableStreamsRouting, ctx.durableStreamsUrl);
7107
7130
  if (parsedBody) {
7108
7131
  const rawPrimaryStream = newWebhook?.primaryStream ?? parsedBody.primary_stream ?? parsedBody.primaryStream ?? parsedBody.streamPath ?? null;
7109
7132
  const primaryStream = typeof rawPrimaryStream === `string` ? toRuntimeStreamPath(rawPrimaryStream, ctx.service, routingAdapter) : null;
@@ -7232,7 +7255,7 @@ async function callbackForward(request, ctx) {
7232
7255
  }
7233
7256
  return json(responseBody);
7234
7257
  }
7235
- const upstreamBody = encodeCallbackForwardBody(ctx.service, consumerId, requestBody, resolveDurableStreamsRoutingAdapter(ctx.durableStreamsRouting));
7258
+ const upstreamBody = encodeCallbackForwardBody(ctx.service, consumerId, requestBody, resolveDurableStreamsRoutingAdapter(ctx.durableStreamsRouting, ctx.durableStreamsUrl));
7236
7259
  let upstream;
7237
7260
  try {
7238
7261
  const subscriptionId = durableStreamsSubscriptionCallback(target.callbackUrl);
@@ -7343,4 +7366,4 @@ globalRouter.all(`/_electric/*`, internalRouter.fetch);
7343
7366
  globalRouter.all(`*`, durableStreamsRouter.fetch);
7344
7367
 
7345
7368
  //#endregion
7346
- export { AgentsHost, DEFAULT_TENANT_ID, StreamClient, UnregisteredTenantError, createDb, globalRouter, isUnregisteredTenantError, pathPrefixedSingleTenantDurableStreamsRoutingAdapter, runMigrations };
7369
+ export { AgentsHost, DEFAULT_TENANT_ID, StreamClient, UnregisteredTenantError, createDb, globalRouter, isUnregisteredTenantError, pathPrefixedSingleTenantDurableStreamsRoutingAdapter, runMigrations, streamRootDurableStreamsRoutingAdapter, tenantRootDurableStreamsRoutingAdapter };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@electric-ax/agents-server",
3
- "version": "0.4.2",
3
+ "version": "0.4.3",
4
4
  "description": "Electric Agents entity runtime server",
5
5
  "author": "Durable Stream contributors",
6
6
  "bin": {
@@ -39,7 +39,7 @@
39
39
  "@durable-streams/client": "https://pkg.pr.new/durable-streams/durable-streams/@durable-streams/client@350",
40
40
  "@durable-streams/server": "https://pkg.pr.new/durable-streams/durable-streams/@durable-streams/server@350",
41
41
  "@durable-streams/state": "https://pkg.pr.new/durable-streams/durable-streams/@durable-streams/state@350",
42
- "@electric-sql/client": "^1.5.17",
42
+ "@electric-sql/client": "^1.5.18",
43
43
  "@mariozechner/pi-agent-core": "^0.70.2",
44
44
  "@opentelemetry/api": "^1.9.1",
45
45
  "@sinclair/typebox": "^0.34.48",
@@ -65,9 +65,9 @@
65
65
  "tsx": "^4.19.0",
66
66
  "typescript": "^5.0.0",
67
67
  "vitest": "^4.1.0",
68
- "@electric-ax/agents": "0.4.1",
69
- "@electric-ax/agents-server-conformance-tests": "0.1.4",
70
- "@electric-ax/agents-server-ui": "0.4.2"
68
+ "@electric-ax/agents": "0.4.2",
69
+ "@electric-ax/agents-server-conformance-tests": "0.1.5",
70
+ "@electric-ax/agents-server-ui": "0.4.3"
71
71
  },
72
72
  "files": [
73
73
  "dist",
package/src/index.ts CHANGED
@@ -37,7 +37,11 @@ export type { Principal, PrincipalKind } from './principal.js'
37
37
  export { globalRouter } from './routing/global-router.js'
38
38
  export type { GlobalRoutes } from './routing/global-router.js'
39
39
  export type { TenantContext } from './routing/context.js'
40
- export { pathPrefixedSingleTenantDurableStreamsRoutingAdapter } from './routing/durable-streams-routing-adapter.js'
40
+ export {
41
+ streamRootDurableStreamsRoutingAdapter,
42
+ pathPrefixedSingleTenantDurableStreamsRoutingAdapter,
43
+ tenantRootDurableStreamsRoutingAdapter,
44
+ } from './routing/durable-streams-routing-adapter.js'
41
45
  export type {
42
46
  DurableStreamsRoutingAdapter,
43
47
  DurableStreamsRoutingInput,
@@ -19,6 +19,7 @@ export interface TenantContext {
19
19
  principal: Principal
20
20
  publicUrl: string
21
21
  localUrl?: string
22
+ /** Resolved Durable Streams root URL for this tenant. */
22
23
  durableStreamsUrl: string
23
24
  durableStreamsBearer?: DurableStreamsBearerProvider
24
25
  durableStreamsRouting?: DurableStreamsRoutingAdapter