@electric-ax/agents-server 0.4.7 → 0.4.11

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.
@@ -4,7 +4,7 @@ import { DurableStreamTestServer } from "@durable-streams/server";
4
4
  import { createServer } from "node:http";
5
5
  import { createServerAdapter } from "@whatwg-node/server";
6
6
  import { Agent } from "undici";
7
- import { appendPathToUrl, assertTags, buildTagsIndex, createEntityRegistry, createRuntimeHandler, entityStateSchema, getCronStreamPath, getCronStreamPathFromSpec, getEntitiesStreamPath, getNextCronFireAt, getSharedStateStreamPath, manifestChildKey, manifestSharedStateKey, manifestSourceKey, normalizeTags, parseCronStreamPath, resolveCronScheduleSpec, sourceRefForTags, verifyWebhookSignature } from "@electric-ax/agents-runtime";
7
+ import { appendPathToUrl, assertTags, buildEventSourceManifestEntry, buildTagsIndex, createEntityRegistry, createRuntimeHandler, entityStateSchema, eventSourceSubscriptionManifestKey, getCronStreamPath, getCronStreamPathFromSpec, getEntitiesStreamPath, getNextCronFireAt, getSharedStateStreamPath, getWebhookStreamPath, manifestChildKey, manifestSharedStateKey, manifestSourceKey, normalizeTags, parseCronStreamPath, resolveCronScheduleSpec, resolveEventSourceSubscription, sourceRefForTags, verifyWebhookSignature } from "@electric-ax/agents-runtime";
8
8
  import fs, { existsSync } from "node:fs";
9
9
  import path, { dirname, resolve } from "node:path";
10
10
  import { drizzle } from "drizzle-orm/postgres-js";
@@ -446,7 +446,7 @@ const ErrCodeForkWaitTimeout = `FORK_WAIT_TIMEOUT`;
446
446
  const ErrCodeEntityPersistFailed = `ENTITY_PERSIST_FAILED`;
447
447
  const ErrCodeAgentUiNotFound = `AGENT_UI_NOT_FOUND`;
448
448
  const ErrCodeSubscriptionNotFound = `SUBSCRIPTION_NOT_FOUND`;
449
- const ErrCodeCallbackNotFound = `CALLBACK_NOT_FOUND`;
449
+ const ErrCodeWakeCallbackNotFound = `WAKE_CALLBACK_NOT_FOUND`;
450
450
 
451
451
  //#endregion
452
452
  //#region src/utils/electric-url.ts
@@ -466,7 +466,7 @@ function electricUrlWithPath(electricUrl, path$1) {
466
466
  //#region src/routing/durable-streams-routing-adapter.ts
467
467
  function appendSearch(target, source) {
468
468
  source.searchParams.forEach((value, key) => {
469
- if (key !== `service`) target.searchParams.append(key, value);
469
+ target.searchParams.append(key, value);
470
470
  });
471
471
  return target;
472
472
  }
@@ -1610,7 +1610,7 @@ async function rewriteSubscriptionRequestBody(request, ctx, subscriptionId, rout
1610
1610
  let targetWebhookUrl = null;
1611
1611
  if (payload.webhook?.url !== void 0) {
1612
1612
  targetWebhookUrl = rewriteLoopbackWebhookUrl(payload.webhook.url) ?? null;
1613
- payload.webhook.url = appendPathToUrl(ctx.publicUrl, `/_electric/webhook-forward/${encodeURIComponent(subscriptionId)}`);
1613
+ payload.webhook.url = appendPathToUrl(ctx.publicUrl, `/_electric/subscription-webhooks/${encodeURIComponent(subscriptionId)}`);
1614
1614
  }
1615
1615
  rewriteSubscriptionBodyForBackend(payload, ctx.service, routingAdapter);
1616
1616
  return {
@@ -1733,20 +1733,6 @@ async function proxyPassThrough(request, ctx) {
1733
1733
  }
1734
1734
  }
1735
1735
 
1736
- //#endregion
1737
- //#region src/routing/cron-router.ts
1738
- const cronRegisterBodySchema = Type.Object({
1739
- expression: Type.String(),
1740
- timezone: Type.Optional(Type.String())
1741
- });
1742
- const cronRouter = Router({ base: `/_electric/cron` });
1743
- cronRouter.post(`/register`, withSchema(cronRegisterBodySchema), registerCron);
1744
- async function registerCron(request, ctx) {
1745
- const parsed = routeBody(request);
1746
- const streamPath = await ctx.entityManager.getOrCreateCronStream(parsed.expression, parsed.timezone);
1747
- return json({ streamUrl: streamPath });
1748
- }
1749
-
1750
1736
  //#endregion
1751
1737
  //#region src/routing/electric-proxy-router.ts
1752
1738
  const electricProxyRouter = Router({ base: `/_electric/electric` });
@@ -2668,6 +2654,10 @@ function extractManifestSourceUrl(manifest) {
2668
2654
  }
2669
2655
  if (manifest.sourceType === `entities`) return typeof manifest.sourceRef === `string` ? `/_entities/${manifest.sourceRef}` : void 0;
2670
2656
  if (manifest.sourceType === `db`) return typeof manifest.sourceRef === `string` ? getSharedStateStreamPath(manifest.sourceRef) : void 0;
2657
+ if (manifest.sourceType === `webhook`) {
2658
+ if (typeof config?.streamUrl === `string`) return config.streamUrl;
2659
+ if (typeof config?.endpointKey === `string`) return getWebhookStreamPath(config.endpointKey, typeof config.bucket === `string` ? config.bucket : void 0);
2660
+ }
2671
2661
  return void 0;
2672
2662
  }
2673
2663
  if (manifest.kind === `shared-state`) return typeof manifest.id === `string` ? getSharedStateStreamPath(manifest.id) : void 0;
@@ -2954,7 +2944,8 @@ var EntityManager = class {
2954
2944
  debounceMs: req.wake.debounceMs,
2955
2945
  timeoutMs: req.wake.timeoutMs,
2956
2946
  oneShot: false,
2957
- includeResponse: req.wake.includeResponse
2947
+ includeResponse: req.wake.includeResponse,
2948
+ manifestKey: req.wake.manifestKey
2958
2949
  });
2959
2950
  const contentType = `application/json`;
2960
2951
  const createdEvent = entityStateSchema.entityCreated.insert({
@@ -3627,7 +3618,7 @@ var EntityManager = class {
3627
3618
  };
3628
3619
  }
3629
3620
  /**
3630
- * Deliver a message to an entity's main stream, with optional input schema
3621
+ * Deliver a message to an entity's main stream, with optional inbox schema
3631
3622
  * validation.
3632
3623
  */
3633
3624
  async send(entityUrl, req, opts) {
@@ -3715,7 +3706,7 @@ var EntityManager = class {
3715
3706
  if (result.changed && this.entityBridgeManager) await this.entityBridgeManager.onEntityChanged(entityUrl);
3716
3707
  return updated;
3717
3708
  }
3718
- async removeTag(entityUrl, key, token) {
3709
+ async deleteTag(entityUrl, key, token) {
3719
3710
  const entity = await this.registry.getEntity(entityUrl);
3720
3711
  if (!entity) throw new ElectricAgentsError(ErrCodeNotFound, `Entity not found`, 404);
3721
3712
  if (!this.isValidWriteToken(entity, token)) throw new ElectricAgentsError(ErrCodeUnauthorized, `Invalid write token`, 401);
@@ -3726,7 +3717,7 @@ var EntityManager = class {
3726
3717
  if (result.changed && this.entityBridgeManager) await this.entityBridgeManager.onEntityChanged(entityUrl);
3727
3718
  return updated;
3728
3719
  }
3729
- async registerEntitiesSource(tags) {
3720
+ async ensureEntitiesMembershipStream(tags) {
3730
3721
  if (!this.entityBridgeManager) throw new Error(`Entity bridge manager not configured`);
3731
3722
  return this.entityBridgeManager.register(this.validateTags(tags));
3732
3723
  }
@@ -3848,6 +3839,35 @@ var EntityManager = class {
3848
3839
  await this.writeManifestEntry(entityUrl, manifestKey, `delete`, void 0, { txid });
3849
3840
  return { txid };
3850
3841
  }
3842
+ async upsertEventSourceSubscription(entityUrl, req) {
3843
+ const manifestKey = req.subscription.manifestKey;
3844
+ const txid = randomUUID();
3845
+ await this.writeManifestEntry(entityUrl, manifestKey, `upsert`, req.manifest, { txid });
3846
+ await this.wakeRegistry.unregisterByManifestKey(entityUrl, manifestKey, this.tenantId);
3847
+ await this.wakeRegistry.register({
3848
+ tenantId: this.tenantId,
3849
+ subscriberUrl: entityUrl,
3850
+ sourceUrl: req.subscription.sourceUrl,
3851
+ condition: {
3852
+ on: `change`,
3853
+ collections: [`webhook_event`],
3854
+ ops: [`insert`]
3855
+ },
3856
+ oneShot: false,
3857
+ manifestKey
3858
+ });
3859
+ return {
3860
+ txid,
3861
+ subscription: req.subscription
3862
+ };
3863
+ }
3864
+ async deleteEventSourceSubscription(entityUrl, req) {
3865
+ const manifestKey = eventSourceSubscriptionManifestKey(req.id);
3866
+ const txid = randomUUID();
3867
+ await this.writeManifestEntry(entityUrl, manifestKey, `delete`, void 0, { txid });
3868
+ await this.wakeRegistry.unregisterByManifestKey(entityUrl, manifestKey, this.tenantId);
3869
+ return { txid };
3870
+ }
3851
3871
  /**
3852
3872
  * Register a wake subscription from a subscriber to a source entity.
3853
3873
  */
@@ -4126,7 +4146,7 @@ var EntityManager = class {
4126
4146
  return null;
4127
4147
  }
4128
4148
  /**
4129
- * Add new input/output schema keys to an entity type directly in Postgres.
4149
+ * Add new inbox/state schema keys to an entity type directly in Postgres.
4130
4150
  */
4131
4151
  async amendSchemas(typeName, schemas) {
4132
4152
  if (typeName === `principal`) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Entity type "principal" is built in and cannot be amended`, 400);
@@ -4166,7 +4186,7 @@ var EntityManager = class {
4166
4186
  }
4167
4187
  /**
4168
4188
  * Enrich webhook payload with entity context.
4169
- * Called by ElectricAgentsServer during webhook forwarding to inject entity context.
4189
+ * Called by ElectricAgentsServer during subscription webhook dispatch to inject entity context.
4170
4190
  */
4171
4191
  async enrichPayload(payload, consumer) {
4172
4192
  const entity = await this.registry.getEntityByStream(consumer.primary_stream);
@@ -4431,7 +4451,7 @@ async function linkStreamToTargetSubscription(ctx, target, entity, subscriptionI
4431
4451
  }
4432
4452
  const webhookUrl = rewriteLoopbackWebhookUrl(target.url);
4433
4453
  if (!webhookUrl) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Webhook dispatch target must include a valid URL`, 400);
4434
- const forwardUrl = appendPathToUrl(ctx.publicUrl, `/_electric/webhook-forward/${encodeURIComponent(subscriptionId)}`);
4454
+ const forwardUrl = appendPathToUrl(ctx.publicUrl, `/_electric/subscription-webhooks/${encodeURIComponent(subscriptionId)}`);
4435
4455
  await ensureSubscriptionIncludesStream(ctx, subscriptionId, streamPath, {
4436
4456
  type: `webhook`,
4437
4457
  streams: [streamPath],
@@ -4450,7 +4470,7 @@ async function linkStreamToTargetSubscription(ctx, target, entity, subscriptionI
4450
4470
 
4451
4471
  //#endregion
4452
4472
  //#region src/routing/entities-router.ts
4453
- const stringRecordSchema = Type.Record(Type.String(), Type.String());
4473
+ const stringRecordSchema$1 = Type.Record(Type.String(), Type.String());
4454
4474
  function writeTokenFromRequest(request) {
4455
4475
  const electricClaimToken = request.headers.get(`electric-claim-token`)?.trim();
4456
4476
  if (electricClaimToken) return electricClaimToken;
@@ -4467,7 +4487,7 @@ const wakeConditionSchema = Type.Union([Type.Literal(`runFinished`), Type.Object
4467
4487
  })]);
4468
4488
  const spawnBodySchema = Type.Object({
4469
4489
  args: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
4470
- tags: Type.Optional(stringRecordSchema),
4490
+ tags: Type.Optional(stringRecordSchema$1),
4471
4491
  parent: Type.Optional(Type.String()),
4472
4492
  dispatch_policy: Type.Optional(dispatchPolicySchema),
4473
4493
  initialMessage: Type.Optional(Type.Unknown()),
@@ -4476,7 +4496,8 @@ const spawnBodySchema = Type.Object({
4476
4496
  condition: wakeConditionSchema,
4477
4497
  debounceMs: Type.Optional(Type.Number()),
4478
4498
  timeoutMs: Type.Optional(Type.Number()),
4479
- includeResponse: Type.Optional(Type.Boolean())
4499
+ includeResponse: Type.Optional(Type.Boolean()),
4500
+ manifestKey: Type.Optional(Type.String())
4480
4501
  }))
4481
4502
  });
4482
4503
  const sendBodySchema = Type.Object({
@@ -4542,10 +4563,24 @@ const scheduleBodySchema = Type.Union([Type.Object({
4542
4563
  messageType: Type.Optional(Type.String()),
4543
4564
  from: Type.Optional(Type.String())
4544
4565
  })]);
4545
- const entitiesRegisterBodySchema = Type.Object({ tags: Type.Optional(stringRecordSchema) });
4566
+ const subscriptionLifetimeSchema = Type.Union([
4567
+ Type.Object({ kind: Type.Literal(`until_entity_stopped`) }),
4568
+ Type.Object({
4569
+ kind: Type.Literal(`expires_at`),
4570
+ at: Type.String()
4571
+ }),
4572
+ Type.Object({ kind: Type.Literal(`manual`) })
4573
+ ]);
4574
+ const eventSourceSubscriptionBodySchema = Type.Object({
4575
+ sourceKey: Type.String(),
4576
+ bucketKey: Type.Optional(Type.String()),
4577
+ params: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
4578
+ filterKey: Type.Optional(Type.String()),
4579
+ lifetime: Type.Optional(subscriptionLifetimeSchema),
4580
+ reason: Type.Optional(Type.String())
4581
+ });
4546
4582
  const entitiesRouter = Router({ base: `/_electric/entities` });
4547
4583
  entitiesRouter.get(`/`, listEntities);
4548
- entitiesRouter.post(`/register`, withSchema(entitiesRegisterBodySchema), registerEntitiesSource);
4549
4584
  entitiesRouter.put(`/:type/:instanceId`, withSpawnableEntityType, withSchema(spawnBodySchema), spawnEntity);
4550
4585
  entitiesRouter.get(`/:type/:instanceId`, withExistingEntity, getEntity);
4551
4586
  entitiesRouter.head(`/:type/:instanceId`, withExistingEntity, headEntity);
@@ -4556,9 +4591,11 @@ entitiesRouter.patch(`/:type/:instanceId/inbox/:messageKey`, withExistingEntity,
4556
4591
  entitiesRouter.delete(`/:type/:instanceId/inbox/:messageKey`, withExistingEntity, deleteInboxMessage);
4557
4592
  entitiesRouter.post(`/:type/:instanceId/fork`, withExistingEntity, withSchema(forkBodySchema), forkEntity);
4558
4593
  entitiesRouter.post(`/:type/:instanceId/tags/:tagKey`, withExistingEntity, withSchema(setTagBodySchema), setTag);
4559
- entitiesRouter.delete(`/:type/:instanceId/tags/:tagKey`, withExistingEntity, removeTag);
4594
+ entitiesRouter.delete(`/:type/:instanceId/tags/:tagKey`, withExistingEntity, deleteTag);
4560
4595
  entitiesRouter.put(`/:type/:instanceId/schedules/:scheduleId`, withExistingEntity, withSchema(scheduleBodySchema), upsertSchedule);
4561
4596
  entitiesRouter.delete(`/:type/:instanceId/schedules/:scheduleId`, withExistingEntity, deleteSchedule);
4597
+ entitiesRouter.put(`/:type/:instanceId/event-source-subscriptions/:subscriptionId`, withExistingEntity, withSchema(eventSourceSubscriptionBodySchema), upsertEventSourceSubscription);
4598
+ entitiesRouter.delete(`/:type/:instanceId/event-source-subscriptions/:subscriptionId`, withExistingEntity, deleteEventSourceSubscription);
4562
4599
  function entityUrlFromSegments(type, instanceId) {
4563
4600
  if (!type || !instanceId) return null;
4564
4601
  if (type.startsWith(`_`) || type.includes(`*`) || instanceId.includes(`*`)) return null;
@@ -4622,11 +4659,6 @@ async function listEntities({ query }, ctx) {
4622
4659
  });
4623
4660
  return json(entities$1.map((entity) => toPublicEntity(entity)));
4624
4661
  }
4625
- async function registerEntitiesSource(request, ctx) {
4626
- const parsed = routeBody(request);
4627
- const result = await ctx.entityManager.registerEntitiesSource(parsed.tags ?? {});
4628
- return json(result);
4629
- }
4630
4662
  async function upsertSchedule(request, ctx) {
4631
4663
  const principalMutationError = rejectPrincipalEntityMutation(request, `scheduled`);
4632
4664
  if (principalMutationError) return principalMutationError;
@@ -4665,8 +4697,49 @@ async function deleteSchedule(request, ctx) {
4665
4697
  const result = await ctx.entityManager.deleteSchedule(entityUrl, { id: decodeURIComponent(request.params.scheduleId) });
4666
4698
  return json(result);
4667
4699
  }
4700
+ async function upsertEventSourceSubscription(request, ctx) {
4701
+ const principalMutationError = rejectPrincipalEntityMutation(request, `subscribed to event sources`);
4702
+ if (principalMutationError) return principalMutationError;
4703
+ const catalog = ctx.eventSources;
4704
+ if (!catalog) return apiError(404, ErrCodeNotFound, `No event source catalog is configured`);
4705
+ const { entityUrl } = requireExistingEntityRoute(request);
4706
+ const parsed = routeBody(request);
4707
+ const source = await catalog.getEventSource(parsed.sourceKey);
4708
+ if (!source) return apiError(404, ErrCodeNotFound, `Event source "${parsed.sourceKey}" not found`);
4709
+ if (parsed.lifetime?.kind === `expires_at`) {
4710
+ const expiresAt = new Date(parsed.lifetime.at);
4711
+ if (Number.isNaN(expiresAt.getTime())) return apiError(400, ErrCodeInvalidRequest, `Invalid expires_at lifetime timestamp`);
4712
+ }
4713
+ let resolved;
4714
+ try {
4715
+ resolved = resolveEventSourceSubscription({
4716
+ contract: source,
4717
+ entityUrl,
4718
+ request: {
4719
+ ...parsed,
4720
+ id: decodeURIComponent(request.params.subscriptionId)
4721
+ },
4722
+ createdBy: `tool`
4723
+ });
4724
+ } catch (error) {
4725
+ return apiError(400, ErrCodeInvalidRequest, error instanceof Error ? error.message : String(error));
4726
+ }
4727
+ await ctx.ensureEventSourceWakeSource?.(resolved.subscription.sourceUrl);
4728
+ const result = await ctx.entityManager.upsertEventSourceSubscription(entityUrl, {
4729
+ subscription: resolved.subscription,
4730
+ manifest: buildEventSourceManifestEntry(resolved)
4731
+ });
4732
+ return json(result);
4733
+ }
4734
+ async function deleteEventSourceSubscription(request, ctx) {
4735
+ const principalMutationError = rejectPrincipalEntityMutation(request, `unsubscribed from event sources`);
4736
+ if (principalMutationError) return principalMutationError;
4737
+ const { entityUrl } = requireExistingEntityRoute(request);
4738
+ const result = await ctx.entityManager.deleteEventSourceSubscription(entityUrl, { id: decodeURIComponent(request.params.subscriptionId) });
4739
+ return json(result);
4740
+ }
4668
4741
  async function setTag(request, ctx) {
4669
- const principalMutationError = rejectPrincipalEntityMutation(request, `tagged`);
4742
+ const principalMutationError = rejectPrincipalEntityMutation(request, `tag updated`);
4670
4743
  if (principalMutationError) return principalMutationError;
4671
4744
  const parsed = routeBody(request);
4672
4745
  const { entityUrl } = requireExistingEntityRoute(request);
@@ -4674,12 +4747,12 @@ async function setTag(request, ctx) {
4674
4747
  const updated = await ctx.entityManager.setTag(entityUrl, decodeURIComponent(request.params.tagKey), { value: parsed.value }, token);
4675
4748
  return json(toPublicEntity(updated));
4676
4749
  }
4677
- async function removeTag(request, ctx) {
4678
- const principalMutationError = rejectPrincipalEntityMutation(request, `untagged`);
4750
+ async function deleteTag(request, ctx) {
4751
+ const principalMutationError = rejectPrincipalEntityMutation(request, `tag deleted`);
4679
4752
  if (principalMutationError) return principalMutationError;
4680
4753
  const { entityUrl } = requireExistingEntityRoute(request);
4681
4754
  const token = writeTokenFromRequest(request);
4682
- const updated = await ctx.entityManager.removeTag(entityUrl, decodeURIComponent(request.params.tagKey), token);
4755
+ const updated = await ctx.entityManager.deleteTag(entityUrl, decodeURIComponent(request.params.tagKey), token);
4683
4756
  return json(toPublicEntity(updated));
4684
4757
  }
4685
4758
  async function forkEntity(request, ctx) {
@@ -4811,17 +4884,13 @@ const registerEntityTypeBodySchema = Type.Object({
4811
4884
  creation_schema: Type.Optional(jsonObjectSchema),
4812
4885
  inbox_schemas: Type.Optional(schemaMapSchema),
4813
4886
  state_schemas: Type.Optional(schemaMapSchema),
4814
- input_schemas: Type.Optional(schemaMapSchema),
4815
- output_schemas: Type.Optional(schemaMapSchema),
4816
4887
  serve_endpoint: Type.Optional(Type.String()),
4817
4888
  default_dispatch_policy: Type.Optional(dispatchPolicySchema)
4818
- });
4889
+ }, { additionalProperties: false });
4819
4890
  const amendEntityTypeSchemasBodySchema = Type.Object({
4820
- input_schemas: Type.Optional(schemaMapSchema),
4821
- output_schemas: Type.Optional(schemaMapSchema),
4822
4891
  inbox_schemas: Type.Optional(schemaMapSchema),
4823
4892
  state_schemas: Type.Optional(schemaMapSchema)
4824
- });
4893
+ }, { additionalProperties: false });
4825
4894
  const entityTypesRouter = Router({ base: `/_electric/entity-types` });
4826
4895
  entityTypesRouter.get(`/`, listEntityTypes);
4827
4896
  entityTypesRouter.post(`/`, withSchema(registerEntityTypeBodySchema), registerEntityType);
@@ -4861,8 +4930,8 @@ async function getEntityType(request, ctx) {
4861
4930
  async function amendSchemas(request, ctx) {
4862
4931
  const parsed = routeBody(request);
4863
4932
  const updated = await ctx.entityManager.amendSchemas(request.params.name, {
4864
- inbox_schemas: parsed.inbox_schemas ?? parsed.input_schemas,
4865
- state_schemas: parsed.state_schemas ?? parsed.output_schemas
4933
+ inbox_schemas: parsed.inbox_schemas,
4934
+ state_schemas: parsed.state_schemas
4866
4935
  });
4867
4936
  return json(toPublicEntityType(updated));
4868
4937
  }
@@ -4872,13 +4941,12 @@ async function deleteEntityType(request, ctx) {
4872
4941
  }
4873
4942
  function normalizeEntityTypeRequest(parsed) {
4874
4943
  const serveEndpoint = rewriteLoopbackWebhookUrl(parsed.serve_endpoint);
4875
- const compatibilityFields = parsed;
4876
4944
  return {
4877
4945
  name: parsed.name ?? ``,
4878
4946
  description: parsed.description ?? ``,
4879
4947
  creation_schema: parsed.creation_schema,
4880
- inbox_schemas: parsed.inbox_schemas ?? compatibilityFields.input_schemas,
4881
- state_schemas: parsed.state_schemas ?? compatibilityFields.output_schemas,
4948
+ inbox_schemas: parsed.inbox_schemas,
4949
+ state_schemas: parsed.state_schemas,
4882
4950
  serve_endpoint: serveEndpoint,
4883
4951
  default_dispatch_policy: parsed.default_dispatch_policy ?? (serveEndpoint ? { targets: [{
4884
4952
  type: `webhook`,
@@ -4889,8 +4957,6 @@ function normalizeEntityTypeRequest(parsed) {
4889
4957
  function toPublicEntityType(entityType) {
4890
4958
  return {
4891
4959
  ...entityType,
4892
- input_schemas: entityType.inbox_schemas,
4893
- output_schemas: entityType.state_schemas,
4894
4960
  revision: entityType.revision
4895
4961
  };
4896
4962
  }
@@ -4974,13 +5040,35 @@ function errorMapper(err, req) {
4974
5040
  function rejectIfShuttingDown(req, ctx) {
4975
5041
  if (!ctx.isShuttingDown()) return void 0;
4976
5042
  const path$1 = new URL(req.url).pathname;
4977
- if (!path$1.startsWith(`/_electric/webhook-forward/`)) return void 0;
5043
+ if (!path$1.startsWith(`/_electric/subscription-webhooks/`)) return void 0;
4978
5044
  return apiError(503, `SERVER_STOPPING`, `Server is shutting down`);
4979
5045
  }
4980
5046
  function getRequestSpan(req) {
4981
5047
  return carrier(req)[SPAN_KEY];
4982
5048
  }
4983
5049
 
5050
+ //#endregion
5051
+ //#region src/routing/observations-router.ts
5052
+ const stringRecordSchema = Type.Record(Type.String(), Type.String());
5053
+ const ensureEntitiesMembershipStreamBodySchema = Type.Object({ tags: Type.Optional(stringRecordSchema) });
5054
+ const ensureCronStreamBodySchema = Type.Object({
5055
+ expression: Type.String(),
5056
+ timezone: Type.Optional(Type.String())
5057
+ });
5058
+ const observationsRouter = Router({ base: `/_electric/observations` });
5059
+ observationsRouter.post(`/entities/ensure-stream`, withSchema(ensureEntitiesMembershipStreamBodySchema), ensureEntitiesMembershipStream);
5060
+ observationsRouter.post(`/cron/ensure-stream`, withSchema(ensureCronStreamBodySchema), ensureCronStream);
5061
+ async function ensureEntitiesMembershipStream(request, ctx) {
5062
+ const parsed = routeBody(request);
5063
+ const result = await ctx.entityManager.ensureEntitiesMembershipStream(parsed.tags ?? {});
5064
+ return json(result);
5065
+ }
5066
+ async function ensureCronStream(request, ctx) {
5067
+ const parsed = routeBody(request);
5068
+ const streamPath = await ctx.entityManager.getOrCreateCronStream(parsed.expression, parsed.timezone);
5069
+ return json({ streamUrl: streamPath });
5070
+ }
5071
+
4984
5072
  //#endregion
4985
5073
  //#region src/routing/tenant-stream-paths.ts
4986
5074
  function withLeadingSlash(path$1) {
@@ -5319,7 +5407,7 @@ async function notificationFromClaim(ctx, input) {
5319
5407
  wakeId: input.claim.wake_id,
5320
5408
  streamPath: primaryStream,
5321
5409
  streams: streams$1,
5322
- callback: appendPathToUrl(ctx.publicUrl, `/_electric/callback-forward/${encodeURIComponent(input.claim.wake_id)}`),
5410
+ callback: appendPathToUrl(ctx.publicUrl, `/_electric/wake-callbacks/${encodeURIComponent(input.claim.wake_id)}`),
5323
5411
  claimToken: input.claim.token,
5324
5412
  triggerEvent: `message_received`,
5325
5413
  entity: {
@@ -5354,7 +5442,7 @@ const wakeRegistrationBodySchema = Type.Object({
5354
5442
  includeResponse: Type.Optional(Type.Boolean()),
5355
5443
  manifestKey: Type.Optional(Type.String())
5356
5444
  });
5357
- const webhookForwardBodySchema = Type.Object({
5445
+ const subscriptionWebhookBodySchema = Type.Object({
5358
5446
  subscription_id: Type.Optional(Type.String()),
5359
5447
  wake_id: Type.Optional(Type.String()),
5360
5448
  generation: Type.Optional(Type.Number()),
@@ -5368,7 +5456,7 @@ const webhookForwardBodySchema = Type.Object({
5368
5456
  consumer_id: Type.Optional(Type.String()),
5369
5457
  callback: Type.Optional(Type.String())
5370
5458
  }, { additionalProperties: true });
5371
- const callbackForwardBodySchema = Type.Object({
5459
+ const wakeCallbackBodySchema = Type.Object({
5372
5460
  epoch: Type.Optional(Type.Number()),
5373
5461
  generation: Type.Optional(Type.Number()),
5374
5462
  wakeId: Type.Optional(Type.String()),
@@ -5379,14 +5467,15 @@ const callbackForwardBodySchema = Type.Object({
5379
5467
  const DS_SUBSCRIPTION_CALLBACK_PREFIX = `ds-subscription:`;
5380
5468
  const internalRouter = Router({ base: `/_electric` });
5381
5469
  internalRouter.get(`/health`, () => json({ status: `ok` }));
5470
+ internalRouter.get(`/event-sources`, listEventSources);
5382
5471
  internalRouter.post(`/wake`, withSchema(wakeRegistrationBodySchema), registerWake);
5383
- internalRouter.post(`/webhook-forward/:subscriptionId`, webhookForward);
5384
- internalRouter.post(`/callback-forward/:consumerId`, callbackForward);
5472
+ internalRouter.post(`/subscription-webhooks/:subscriptionId`, subscriptionWebhook);
5473
+ internalRouter.post(`/wake-callbacks/:consumerId`, wakeCallback);
5385
5474
  internalRouter.all(`/runners`, runnersRouter.fetch);
5386
5475
  internalRouter.all(`/runners/*`, runnersRouter.fetch);
5387
5476
  internalRouter.all(`/entities/*`, entitiesRouter.fetch);
5388
5477
  internalRouter.all(`/entity-types/*`, entityTypesRouter.fetch);
5389
- internalRouter.all(`/cron/*`, cronRouter.fetch);
5478
+ internalRouter.all(`/observations/*`, observationsRouter.fetch);
5390
5479
  internalRouter.get(`/electric/*`, electricProxyRouter.fetch);
5391
5480
  internalRouter.all(`*`, () => status(404));
5392
5481
  function routeParam(request, name) {
@@ -5419,13 +5508,30 @@ function resolveWebhookSigner(ctx) {
5419
5508
  return ctx.webhookSigner ?? getDefaultWebhookSigner();
5420
5509
  }
5421
5510
  function durableStreamsWebhookJwksUrl(ctx) {
5422
- if (!ctx.durableStreamsRouting) return appendPathToUrl(ctx.durableStreamsUrl, `/__ds/jwks.json`);
5511
+ if (!ctx.durableStreamsRouting) return appendPathToBackendUrl(ctx.durableStreamsUrl, `/__ds/jwks.json`);
5423
5512
  return resolveDurableStreamsRoutingAdapter(ctx.durableStreamsRouting, ctx.durableStreamsUrl).controlUrl({
5424
5513
  durableStreamsUrl: ctx.durableStreamsUrl,
5425
5514
  serviceId: ctx.service,
5426
5515
  requestUrl: appendPathToUrl(ctx.publicUrl, `/__ds/jwks.json`)
5427
5516
  }).toString();
5428
5517
  }
5518
+ function appendPathToBackendUrl(baseUrl, path$1) {
5519
+ const base = new URL(baseUrl);
5520
+ const pathUrl = new URL(path$1, `http://electric-agents.local`);
5521
+ const basePath = base.pathname === `/` ? `` : base.pathname.replace(/\/+$/, ``);
5522
+ const suffix = pathUrl.pathname.startsWith(`/`) ? pathUrl.pathname : `/${pathUrl.pathname}`;
5523
+ const target = new URL(base);
5524
+ target.pathname = `${basePath}${suffix}`;
5525
+ target.search = ``;
5526
+ target.hash = pathUrl.hash;
5527
+ base.searchParams.forEach((value, key) => {
5528
+ target.searchParams.append(key, value);
5529
+ });
5530
+ pathUrl.searchParams.forEach((value, key) => {
5531
+ target.searchParams.append(key, value);
5532
+ });
5533
+ return target.toString();
5534
+ }
5429
5535
  function durableStreamsJwksFetchClient(ctx) {
5430
5536
  return async (input, init) => {
5431
5537
  const headers = new Headers(init?.headers);
@@ -5482,10 +5588,17 @@ async function registerWake(request, ctx) {
5482
5588
  await ctx.entityManager.registerWake(opts);
5483
5589
  return status(204);
5484
5590
  }
5485
- async function webhookForward(request, ctx) {
5591
+ async function listEventSources(_request, ctx) {
5592
+ const eventSources = ctx.eventSources ? await ctx.eventSources.listEventSources() : [];
5593
+ return json({ eventSources: eventSources.filter(isAgentVisibleEventSource) });
5594
+ }
5595
+ function isAgentVisibleEventSource(source) {
5596
+ return source.agentVisible === true && source.status === `active`;
5597
+ }
5598
+ async function subscriptionWebhook(request, ctx) {
5486
5599
  const subscriptionId = routeParam(request, `subscriptionId`);
5487
5600
  const rootSpan = getRequestSpan(request);
5488
- rootSpan?.updateName(`webhook-forward`);
5601
+ rootSpan?.updateName(`subscription-webhook`);
5489
5602
  rootSpan?.setAttribute(`electric_agents.webhook.subscription_id`, subscriptionId);
5490
5603
  const body = await readRequestBody(request);
5491
5604
  const signatureError = await verifyDurableStreamsWebhook(request, ctx, body);
@@ -5499,7 +5612,7 @@ async function webhookForward(request, ctx) {
5499
5612
  }
5500
5613
  });
5501
5614
  if (!targetWebhookUrl) return apiError(404, ErrCodeSubscriptionNotFound, `Unknown webhook subscription`);
5502
- const parsedBodyResult = validateOptionalJsonBody(webhookForwardBodySchema, body, request.headers.get(`content-type`));
5615
+ const parsedBodyResult = validateOptionalJsonBody(subscriptionWebhookBodySchema, body, request.headers.get(`content-type`));
5503
5616
  if (!parsedBodyResult.ok) return parsedBodyResult.response;
5504
5617
  let forwardBody = body;
5505
5618
  let runningEntityUrl = null;
@@ -5545,7 +5658,7 @@ async function webhookForward(request, ctx) {
5545
5658
  span.end();
5546
5659
  }
5547
5660
  }).catch((err) => {
5548
- serverLog.warn(`[webhook-forward] consumerCallbacks upsert failed (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
5661
+ serverLog.warn(`[subscription-webhook] consumerCallbacks upsert failed (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
5549
5662
  }) : void 0;
5550
5663
  const [entity, enriched] = await Promise.all([entityPromise, enrichPromise]);
5551
5664
  if (entity?.status === `stopped` || entity?.status === `paused`) {
@@ -5566,7 +5679,7 @@ async function webhookForward(request, ctx) {
5566
5679
  runningEntityUrl = entity.url;
5567
5680
  }
5568
5681
  if (consumerId && callbackUrl) {
5569
- const callback = appendPathToUrl(ctx.publicUrl, `/_electric/callback-forward/${encodeURIComponent(consumerId)}`);
5682
+ const callback = appendPathToUrl(ctx.publicUrl, `/_electric/wake-callbacks/${encodeURIComponent(consumerId)}`);
5570
5683
  enriched.callback = callback;
5571
5684
  if (newWebhook) {
5572
5685
  enriched.consumerId = newWebhook.wakeId;
@@ -5603,21 +5716,21 @@ async function webhookForward(request, ctx) {
5603
5716
  });
5604
5717
  } catch (err) {
5605
5718
  if (runningEntityUrl) await ctx.entityManager.registry.updateStatus(runningEntityUrl, `idle`);
5606
- return apiError(502, `WEBHOOK_FORWARD_FAILED`, err instanceof Error ? err.message : String(err));
5719
+ return apiError(502, `SUBSCRIPTION_WEBHOOK_FAILED`, err instanceof Error ? err.message : String(err));
5607
5720
  }
5608
5721
  const responseBytes = upstream.body ? new Uint8Array(await upstream.arrayBuffer()) : new Uint8Array();
5609
5722
  return responseFromUpstream(upstream, responseBytes);
5610
5723
  }
5611
- async function callbackForward(request, ctx) {
5724
+ async function wakeCallback(request, ctx) {
5612
5725
  const consumerId = routeParam(request, `consumerId`);
5613
5726
  const rows = await ctx.pgDb.select().from(consumerCallbacks).where(and(eq(consumerCallbacks.tenantId, ctx.service), eq(consumerCallbacks.consumerId, consumerId))).limit(1);
5614
5727
  const target = rows[0] ? {
5615
5728
  callbackUrl: rows[0].callbackUrl,
5616
5729
  primaryStream: rows[0].primaryStream
5617
5730
  } : void 0;
5618
- if (!target) return apiError(404, ErrCodeCallbackNotFound, `Unknown callback-forward consumer`);
5731
+ if (!target) return apiError(404, ErrCodeWakeCallbackNotFound, `Unknown wake-callback consumer`);
5619
5732
  const body = await readRequestBody(request);
5620
- const parsedBodyResult = validateOptionalJsonBody(callbackForwardBodySchema, body, request.headers.get(`content-type`));
5733
+ const parsedBodyResult = validateOptionalJsonBody(wakeCallbackBodySchema, body, request.headers.get(`content-type`));
5621
5734
  if (!parsedBodyResult.ok) return parsedBodyResult.response;
5622
5735
  const requestBody = parsedBodyResult.value;
5623
5736
  const isClaimRequest = requestBody?.wakeId !== void 0 || requestBody?.wake_id !== void 0;
@@ -5635,14 +5748,14 @@ async function callbackForward(request, ctx) {
5635
5748
  }
5636
5749
  return json(responseBody);
5637
5750
  }
5638
- const upstreamBody = encodeCallbackForwardBody(ctx.service, consumerId, requestBody, resolveDurableStreamsRoutingAdapter(ctx.durableStreamsRouting, ctx.durableStreamsUrl));
5751
+ const upstreamBody = encodeWakeCallbackBody(ctx.service, consumerId, requestBody, resolveDurableStreamsRoutingAdapter(ctx.durableStreamsRouting, ctx.durableStreamsUrl));
5639
5752
  let upstream;
5640
5753
  try {
5641
5754
  const subscriptionId = durableStreamsSubscriptionCallback(target.callbackUrl);
5642
5755
  if (subscriptionId) {
5643
5756
  const token = claimTokenFromRequest(request);
5644
5757
  if (!token) return apiError(401, `UNAUTHORIZED`, `Missing claim token`);
5645
- const upstreamPayload = encodeCallbackForwardPayload(consumerId, requestBody, (stream) => stream.replace(/^\/+/, ``));
5758
+ const upstreamPayload = encodeWakeCallbackPayload(consumerId, requestBody, (stream) => stream.replace(/^\/+/, ``));
5646
5759
  const result = await ctx.streamClient.ackSubscription(subscriptionId, token, upstreamPayload);
5647
5760
  upstream = json(result);
5648
5761
  } else upstream = await fetch(target.callbackUrl, {
@@ -5651,7 +5764,7 @@ async function callbackForward(request, ctx) {
5651
5764
  body: bodyFromBytes(upstreamBody)
5652
5765
  });
5653
5766
  } catch (err) {
5654
- return apiError(502, `CALLBACK_FORWARD_FAILED`, err instanceof Error ? err.message : String(err));
5767
+ return apiError(502, `WAKE_CALLBACK_FAILED`, err instanceof Error ? err.message : String(err));
5655
5768
  }
5656
5769
  let responseBytes = upstream.body ? new Uint8Array(await upstream.arrayBuffer()) : new Uint8Array();
5657
5770
  if (isClaimRequest && upstream.ok && target.primaryStream) {
@@ -5671,7 +5784,7 @@ async function callbackForward(request, ctx) {
5671
5784
  epoch
5672
5785
  });
5673
5786
  if (upstream.ok && isDoneRequest && target.primaryStream) {
5674
- serverLog.info(`[callback-forward] done received for stream=${target.primaryStream} consumer=${consumerId}`);
5787
+ serverLog.info(`[wake-callback] done received for stream=${target.primaryStream} consumer=${consumerId}`);
5675
5788
  const stillOwnsClaim = ctx.runtime.claimWriteTokens.owns(ctx.service, target.primaryStream, consumerId);
5676
5789
  const entity = await ctx.entityManager.registry.getEntityByStream(target.primaryStream);
5677
5790
  let entityCleared = false;
@@ -5693,13 +5806,13 @@ async function callbackForward(request, ctx) {
5693
5806
  if (entity && (entityCleared || stillOwnsClaim)) {
5694
5807
  await ctx.entityManager.registry.updateStatus(entity.url, entity.status === `stopping` ? `stopped` : `idle`);
5695
5808
  await ctx.entityBridgeManager.onEntityChanged(entity.url);
5696
- serverLog.info(`[callback-forward] status updated after done for ${entity.url}`);
5697
- } else if (!entity) serverLog.warn(`[callback-forward] done received but no entity found for stream=${target.primaryStream}`);
5809
+ serverLog.info(`[wake-callback] status updated after done for ${entity.url}`);
5810
+ } else if (!entity) serverLog.warn(`[wake-callback] done received but no entity found for stream=${target.primaryStream}`);
5698
5811
  if (stillOwnsClaim) ctx.runtime.claimWriteTokens.clearStream(ctx.service, target.primaryStream);
5699
- else if (entity) serverLog.info(`[callback-forward] done arrived after in-memory token evicted (stream=${target.primaryStream} consumer=${consumerId})`);
5700
- } else if (requestBody?.done === true) serverLog.warn(`[callback-forward] done received but skipped: upstream.ok=${upstream.ok} primaryStream=${target.primaryStream ?? `null`} consumer=${consumerId}`);
5812
+ else if (entity) serverLog.info(`[wake-callback] done arrived after in-memory token evicted (stream=${target.primaryStream} consumer=${consumerId})`);
5813
+ } else if (requestBody?.done === true) serverLog.warn(`[wake-callback] done received but skipped: upstream.ok=${upstream.ok} primaryStream=${target.primaryStream ?? `null`} consumer=${consumerId}`);
5701
5814
  } catch (err) {
5702
- serverLog.error(`[callback-forward] error processing done for consumer=${consumerId}: ${err instanceof Error ? err.message : String(err)}`);
5815
+ serverLog.error(`[wake-callback] error processing done for consumer=${consumerId}: ${err instanceof Error ? err.message : String(err)}`);
5703
5816
  }
5704
5817
  return responseFromUpstream(upstream, responseBytes);
5705
5818
  }
@@ -5708,11 +5821,11 @@ async function mintClaimWriteToken(ctx, streamPath, consumerId) {
5708
5821
  if (!entity) return void 0;
5709
5822
  return ctx.runtime.claimWriteTokens.mint(ctx.service, streamPath, consumerId);
5710
5823
  }
5711
- function encodeCallbackForwardBody(service, consumerId, body, routingAdapter) {
5712
- const payload = encodeCallbackForwardPayload(consumerId, body, (stream) => routingAdapter.toBackendStreamPath(service, stream));
5824
+ function encodeWakeCallbackBody(service, consumerId, body, routingAdapter) {
5825
+ const payload = encodeWakeCallbackPayload(consumerId, body, (stream) => routingAdapter.toBackendStreamPath(service, stream));
5713
5826
  return new TextEncoder().encode(JSON.stringify(payload));
5714
5827
  }
5715
- function encodeCallbackForwardPayload(consumerId, body, mapStream) {
5828
+ function encodeWakeCallbackPayload(consumerId, body, mapStream) {
5716
5829
  if (!body) return {};
5717
5830
  const generation = body.generation ?? body.epoch;
5718
5831
  const wakeId = body.wake_id ?? body.wakeId ?? consumerId;
@@ -7294,7 +7407,7 @@ var WakeRegistry = class {
7294
7407
  try {
7295
7408
  for (const message of messages) {
7296
7409
  await this.applyShapeMessage(message);
7297
- if (!settled && `control` in message.headers && message.headers.control === `up-to-date`) {
7410
+ if (!settled && isControlMessage(message) && message.headers.control === `up-to-date`) {
7298
7411
  settled = true;
7299
7412
  resolve$1();
7300
7413
  }
@@ -8062,6 +8175,8 @@ var ElectricAgentsServer = class {
8062
8175
  streamClient: this.streamClient,
8063
8176
  runtime: this.standaloneRuntime.runtime,
8064
8177
  entityBridgeManager: this.entityBridgeManager,
8178
+ ...this.options.eventSources ? { eventSources: this.options.eventSources } : {},
8179
+ ...this.options.ensureEventSourceWakeSource ? { ensureEventSourceWakeSource: this.options.ensureEventSourceWakeSource } : {},
8065
8180
  isShuttingDown: () => this.shuttingDown,
8066
8181
  mockAgent: this.mockAgentBootstrap ? { runtime: this.mockAgentBootstrap.runtime } : void 0
8067
8182
  };