@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.
package/dist/index.cjs CHANGED
@@ -451,7 +451,7 @@ const ErrCodeForkInProgress = `FORK_IN_PROGRESS`;
451
451
  const ErrCodeForkWaitTimeout = `FORK_WAIT_TIMEOUT`;
452
452
  const ErrCodeEntityPersistFailed = `ENTITY_PERSIST_FAILED`;
453
453
  const ErrCodeSubscriptionNotFound = `SUBSCRIPTION_NOT_FOUND`;
454
- const ErrCodeCallbackNotFound = `CALLBACK_NOT_FOUND`;
454
+ const ErrCodeWakeCallbackNotFound = `WAKE_CALLBACK_NOT_FOUND`;
455
455
 
456
456
  //#endregion
457
457
  //#region src/tenant.ts
@@ -2532,7 +2532,7 @@ async function linkStreamToTargetSubscription(ctx, target, entity, subscriptionI
2532
2532
  }
2533
2533
  const webhookUrl = rewriteLoopbackWebhookUrl(target.url);
2534
2534
  if (!webhookUrl) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Webhook dispatch target must include a valid URL`, 400);
2535
- const forwardUrl = (0, __electric_ax_agents_runtime.appendPathToUrl)(ctx.publicUrl, `/_electric/webhook-forward/${encodeURIComponent(subscriptionId)}`);
2535
+ const forwardUrl = (0, __electric_ax_agents_runtime.appendPathToUrl)(ctx.publicUrl, `/_electric/subscription-webhooks/${encodeURIComponent(subscriptionId)}`);
2536
2536
  await ensureSubscriptionIncludesStream(ctx, subscriptionId, streamPath, {
2537
2537
  type: `webhook`,
2538
2538
  streams: [streamPath],
@@ -2653,6 +2653,10 @@ function extractManifestSourceUrl(manifest) {
2653
2653
  }
2654
2654
  if (manifest.sourceType === `entities`) return typeof manifest.sourceRef === `string` ? `/_entities/${manifest.sourceRef}` : void 0;
2655
2655
  if (manifest.sourceType === `db`) return typeof manifest.sourceRef === `string` ? (0, __electric_ax_agents_runtime.getSharedStateStreamPath)(manifest.sourceRef) : void 0;
2656
+ if (manifest.sourceType === `webhook`) {
2657
+ if (typeof config?.streamUrl === `string`) return config.streamUrl;
2658
+ if (typeof config?.endpointKey === `string`) return (0, __electric_ax_agents_runtime.getWebhookStreamPath)(config.endpointKey, typeof config.bucket === `string` ? config.bucket : void 0);
2659
+ }
2656
2660
  return void 0;
2657
2661
  }
2658
2662
  if (manifest.kind === `shared-state`) return typeof manifest.id === `string` ? (0, __electric_ax_agents_runtime.getSharedStateStreamPath)(manifest.id) : void 0;
@@ -2939,7 +2943,8 @@ var EntityManager = class {
2939
2943
  debounceMs: req.wake.debounceMs,
2940
2944
  timeoutMs: req.wake.timeoutMs,
2941
2945
  oneShot: false,
2942
- includeResponse: req.wake.includeResponse
2946
+ includeResponse: req.wake.includeResponse,
2947
+ manifestKey: req.wake.manifestKey
2943
2948
  });
2944
2949
  const contentType = `application/json`;
2945
2950
  const createdEvent = __electric_ax_agents_runtime.entityStateSchema.entityCreated.insert({
@@ -3612,7 +3617,7 @@ var EntityManager = class {
3612
3617
  };
3613
3618
  }
3614
3619
  /**
3615
- * Deliver a message to an entity's main stream, with optional input schema
3620
+ * Deliver a message to an entity's main stream, with optional inbox schema
3616
3621
  * validation.
3617
3622
  */
3618
3623
  async send(entityUrl, req, opts) {
@@ -3700,7 +3705,7 @@ var EntityManager = class {
3700
3705
  if (result.changed && this.entityBridgeManager) await this.entityBridgeManager.onEntityChanged(entityUrl);
3701
3706
  return updated;
3702
3707
  }
3703
- async removeTag(entityUrl, key, token) {
3708
+ async deleteTag(entityUrl, key, token) {
3704
3709
  const entity = await this.registry.getEntity(entityUrl);
3705
3710
  if (!entity) throw new ElectricAgentsError(ErrCodeNotFound, `Entity not found`, 404);
3706
3711
  if (!this.isValidWriteToken(entity, token)) throw new ElectricAgentsError(ErrCodeUnauthorized, `Invalid write token`, 401);
@@ -3711,7 +3716,7 @@ var EntityManager = class {
3711
3716
  if (result.changed && this.entityBridgeManager) await this.entityBridgeManager.onEntityChanged(entityUrl);
3712
3717
  return updated;
3713
3718
  }
3714
- async registerEntitiesSource(tags) {
3719
+ async ensureEntitiesMembershipStream(tags) {
3715
3720
  if (!this.entityBridgeManager) throw new Error(`Entity bridge manager not configured`);
3716
3721
  return this.entityBridgeManager.register(this.validateTags(tags));
3717
3722
  }
@@ -3833,6 +3838,35 @@ var EntityManager = class {
3833
3838
  await this.writeManifestEntry(entityUrl, manifestKey, `delete`, void 0, { txid });
3834
3839
  return { txid };
3835
3840
  }
3841
+ async upsertEventSourceSubscription(entityUrl, req) {
3842
+ const manifestKey = req.subscription.manifestKey;
3843
+ const txid = (0, node_crypto.randomUUID)();
3844
+ await this.writeManifestEntry(entityUrl, manifestKey, `upsert`, req.manifest, { txid });
3845
+ await this.wakeRegistry.unregisterByManifestKey(entityUrl, manifestKey, this.tenantId);
3846
+ await this.wakeRegistry.register({
3847
+ tenantId: this.tenantId,
3848
+ subscriberUrl: entityUrl,
3849
+ sourceUrl: req.subscription.sourceUrl,
3850
+ condition: {
3851
+ on: `change`,
3852
+ collections: [`webhook_event`],
3853
+ ops: [`insert`]
3854
+ },
3855
+ oneShot: false,
3856
+ manifestKey
3857
+ });
3858
+ return {
3859
+ txid,
3860
+ subscription: req.subscription
3861
+ };
3862
+ }
3863
+ async deleteEventSourceSubscription(entityUrl, req) {
3864
+ const manifestKey = (0, __electric_ax_agents_runtime.eventSourceSubscriptionManifestKey)(req.id);
3865
+ const txid = (0, node_crypto.randomUUID)();
3866
+ await this.writeManifestEntry(entityUrl, manifestKey, `delete`, void 0, { txid });
3867
+ await this.wakeRegistry.unregisterByManifestKey(entityUrl, manifestKey, this.tenantId);
3868
+ return { txid };
3869
+ }
3836
3870
  /**
3837
3871
  * Register a wake subscription from a subscriber to a source entity.
3838
3872
  */
@@ -4111,7 +4145,7 @@ var EntityManager = class {
4111
4145
  return null;
4112
4146
  }
4113
4147
  /**
4114
- * Add new input/output schema keys to an entity type directly in Postgres.
4148
+ * Add new inbox/state schema keys to an entity type directly in Postgres.
4115
4149
  */
4116
4150
  async amendSchemas(typeName, schemas) {
4117
4151
  if (typeName === `principal`) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Entity type "principal" is built in and cannot be amended`, 400);
@@ -4151,7 +4185,7 @@ var EntityManager = class {
4151
4185
  }
4152
4186
  /**
4153
4187
  * Enrich webhook payload with entity context.
4154
- * Called by ElectricAgentsServer during webhook forwarding to inject entity context.
4188
+ * Called by ElectricAgentsServer during subscription webhook dispatch to inject entity context.
4155
4189
  */
4156
4190
  async enrichPayload(payload, consumer) {
4157
4191
  const entity = await this.registry.getEntityByStream(consumer.primary_stream);
@@ -5410,7 +5444,7 @@ var WakeRegistry = class {
5410
5444
  try {
5411
5445
  for (const message of messages) {
5412
5446
  await this.applyShapeMessage(message);
5413
- if (!settled && `control` in message.headers && message.headers.control === `up-to-date`) {
5447
+ if (!settled && (0, __electric_sql_client.isControlMessage)(message) && message.headers.control === `up-to-date`) {
5414
5448
  settled = true;
5415
5449
  resolve$1();
5416
5450
  }
@@ -6197,7 +6231,7 @@ function validateParsedBody(schema, parsed) {
6197
6231
  //#region src/routing/durable-streams-routing-adapter.ts
6198
6232
  function appendSearch(target, source) {
6199
6233
  source.searchParams.forEach((value, key) => {
6200
- if (key !== `service`) target.searchParams.append(key, value);
6234
+ target.searchParams.append(key, value);
6201
6235
  });
6202
6236
  return target;
6203
6237
  }
@@ -6517,7 +6551,7 @@ async function rewriteSubscriptionRequestBody(request, ctx, subscriptionId, rout
6517
6551
  let targetWebhookUrl = null;
6518
6552
  if (payload.webhook?.url !== void 0) {
6519
6553
  targetWebhookUrl = rewriteLoopbackWebhookUrl(payload.webhook.url) ?? null;
6520
- payload.webhook.url = (0, __electric_ax_agents_runtime.appendPathToUrl)(ctx.publicUrl, `/_electric/webhook-forward/${encodeURIComponent(subscriptionId)}`);
6554
+ payload.webhook.url = (0, __electric_ax_agents_runtime.appendPathToUrl)(ctx.publicUrl, `/_electric/subscription-webhooks/${encodeURIComponent(subscriptionId)}`);
6521
6555
  }
6522
6556
  rewriteSubscriptionBodyForBackend(payload, ctx.service, routingAdapter);
6523
6557
  return {
@@ -6640,20 +6674,6 @@ async function proxyPassThrough(request, ctx) {
6640
6674
  }
6641
6675
  }
6642
6676
 
6643
- //#endregion
6644
- //#region src/routing/cron-router.ts
6645
- const cronRegisterBodySchema = __sinclair_typebox.Type.Object({
6646
- expression: __sinclair_typebox.Type.String(),
6647
- timezone: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String())
6648
- });
6649
- const cronRouter = (0, itty_router.Router)({ base: `/_electric/cron` });
6650
- cronRouter.post(`/register`, withSchema(cronRegisterBodySchema), registerCron);
6651
- async function registerCron(request, ctx) {
6652
- const parsed = routeBody(request);
6653
- const streamPath = await ctx.entityManager.getOrCreateCronStream(parsed.expression, parsed.timezone);
6654
- return (0, itty_router.json)({ streamUrl: streamPath });
6655
- }
6656
-
6657
6677
  //#endregion
6658
6678
  //#region src/routing/electric-proxy-router.ts
6659
6679
  const electricProxyRouter = (0, itty_router.Router)({ base: `/_electric/electric` });
@@ -6687,7 +6707,7 @@ async function proxyElectric(request, ctx) {
6687
6707
 
6688
6708
  //#endregion
6689
6709
  //#region src/routing/entities-router.ts
6690
- const stringRecordSchema = __sinclair_typebox.Type.Record(__sinclair_typebox.Type.String(), __sinclair_typebox.Type.String());
6710
+ const stringRecordSchema$1 = __sinclair_typebox.Type.Record(__sinclair_typebox.Type.String(), __sinclair_typebox.Type.String());
6691
6711
  function writeTokenFromRequest(request) {
6692
6712
  const electricClaimToken = request.headers.get(`electric-claim-token`)?.trim();
6693
6713
  if (electricClaimToken) return electricClaimToken;
@@ -6704,7 +6724,7 @@ const wakeConditionSchema = __sinclair_typebox.Type.Union([__sinclair_typebox.Ty
6704
6724
  })]);
6705
6725
  const spawnBodySchema = __sinclair_typebox.Type.Object({
6706
6726
  args: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Record(__sinclair_typebox.Type.String(), __sinclair_typebox.Type.Unknown())),
6707
- tags: __sinclair_typebox.Type.Optional(stringRecordSchema),
6727
+ tags: __sinclair_typebox.Type.Optional(stringRecordSchema$1),
6708
6728
  parent: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
6709
6729
  dispatch_policy: __sinclair_typebox.Type.Optional(dispatchPolicySchema),
6710
6730
  initialMessage: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Unknown()),
@@ -6713,7 +6733,8 @@ const spawnBodySchema = __sinclair_typebox.Type.Object({
6713
6733
  condition: wakeConditionSchema,
6714
6734
  debounceMs: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Number()),
6715
6735
  timeoutMs: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Number()),
6716
- includeResponse: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Boolean())
6736
+ includeResponse: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Boolean()),
6737
+ manifestKey: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String())
6717
6738
  }))
6718
6739
  });
6719
6740
  const sendBodySchema = __sinclair_typebox.Type.Object({
@@ -6779,10 +6800,24 @@ const scheduleBodySchema = __sinclair_typebox.Type.Union([__sinclair_typebox.Typ
6779
6800
  messageType: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
6780
6801
  from: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String())
6781
6802
  })]);
6782
- const entitiesRegisterBodySchema = __sinclair_typebox.Type.Object({ tags: __sinclair_typebox.Type.Optional(stringRecordSchema) });
6803
+ const subscriptionLifetimeSchema = __sinclair_typebox.Type.Union([
6804
+ __sinclair_typebox.Type.Object({ kind: __sinclair_typebox.Type.Literal(`until_entity_stopped`) }),
6805
+ __sinclair_typebox.Type.Object({
6806
+ kind: __sinclair_typebox.Type.Literal(`expires_at`),
6807
+ at: __sinclair_typebox.Type.String()
6808
+ }),
6809
+ __sinclair_typebox.Type.Object({ kind: __sinclair_typebox.Type.Literal(`manual`) })
6810
+ ]);
6811
+ const eventSourceSubscriptionBodySchema = __sinclair_typebox.Type.Object({
6812
+ sourceKey: __sinclair_typebox.Type.String(),
6813
+ bucketKey: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
6814
+ params: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Record(__sinclair_typebox.Type.String(), __sinclair_typebox.Type.Unknown())),
6815
+ filterKey: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
6816
+ lifetime: __sinclair_typebox.Type.Optional(subscriptionLifetimeSchema),
6817
+ reason: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String())
6818
+ });
6783
6819
  const entitiesRouter = (0, itty_router.Router)({ base: `/_electric/entities` });
6784
6820
  entitiesRouter.get(`/`, listEntities);
6785
- entitiesRouter.post(`/register`, withSchema(entitiesRegisterBodySchema), registerEntitiesSource);
6786
6821
  entitiesRouter.put(`/:type/:instanceId`, withSpawnableEntityType, withSchema(spawnBodySchema), spawnEntity);
6787
6822
  entitiesRouter.get(`/:type/:instanceId`, withExistingEntity, getEntity);
6788
6823
  entitiesRouter.head(`/:type/:instanceId`, withExistingEntity, headEntity);
@@ -6793,9 +6828,11 @@ entitiesRouter.patch(`/:type/:instanceId/inbox/:messageKey`, withExistingEntity,
6793
6828
  entitiesRouter.delete(`/:type/:instanceId/inbox/:messageKey`, withExistingEntity, deleteInboxMessage);
6794
6829
  entitiesRouter.post(`/:type/:instanceId/fork`, withExistingEntity, withSchema(forkBodySchema), forkEntity);
6795
6830
  entitiesRouter.post(`/:type/:instanceId/tags/:tagKey`, withExistingEntity, withSchema(setTagBodySchema), setTag);
6796
- entitiesRouter.delete(`/:type/:instanceId/tags/:tagKey`, withExistingEntity, removeTag);
6831
+ entitiesRouter.delete(`/:type/:instanceId/tags/:tagKey`, withExistingEntity, deleteTag);
6797
6832
  entitiesRouter.put(`/:type/:instanceId/schedules/:scheduleId`, withExistingEntity, withSchema(scheduleBodySchema), upsertSchedule);
6798
6833
  entitiesRouter.delete(`/:type/:instanceId/schedules/:scheduleId`, withExistingEntity, deleteSchedule);
6834
+ entitiesRouter.put(`/:type/:instanceId/event-source-subscriptions/:subscriptionId`, withExistingEntity, withSchema(eventSourceSubscriptionBodySchema), upsertEventSourceSubscription);
6835
+ entitiesRouter.delete(`/:type/:instanceId/event-source-subscriptions/:subscriptionId`, withExistingEntity, deleteEventSourceSubscription);
6799
6836
  function entityUrlFromSegments(type, instanceId) {
6800
6837
  if (!type || !instanceId) return null;
6801
6838
  if (type.startsWith(`_`) || type.includes(`*`) || instanceId.includes(`*`)) return null;
@@ -6859,11 +6896,6 @@ async function listEntities({ query }, ctx) {
6859
6896
  });
6860
6897
  return (0, itty_router.json)(entities$1.map((entity) => toPublicEntity(entity)));
6861
6898
  }
6862
- async function registerEntitiesSource(request, ctx) {
6863
- const parsed = routeBody(request);
6864
- const result = await ctx.entityManager.registerEntitiesSource(parsed.tags ?? {});
6865
- return (0, itty_router.json)(result);
6866
- }
6867
6899
  async function upsertSchedule(request, ctx) {
6868
6900
  const principalMutationError = rejectPrincipalEntityMutation(request, `scheduled`);
6869
6901
  if (principalMutationError) return principalMutationError;
@@ -6902,8 +6934,49 @@ async function deleteSchedule(request, ctx) {
6902
6934
  const result = await ctx.entityManager.deleteSchedule(entityUrl, { id: decodeURIComponent(request.params.scheduleId) });
6903
6935
  return (0, itty_router.json)(result);
6904
6936
  }
6937
+ async function upsertEventSourceSubscription(request, ctx) {
6938
+ const principalMutationError = rejectPrincipalEntityMutation(request, `subscribed to event sources`);
6939
+ if (principalMutationError) return principalMutationError;
6940
+ const catalog = ctx.eventSources;
6941
+ if (!catalog) return apiError(404, ErrCodeNotFound, `No event source catalog is configured`);
6942
+ const { entityUrl } = requireExistingEntityRoute(request);
6943
+ const parsed = routeBody(request);
6944
+ const source = await catalog.getEventSource(parsed.sourceKey);
6945
+ if (!source) return apiError(404, ErrCodeNotFound, `Event source "${parsed.sourceKey}" not found`);
6946
+ if (parsed.lifetime?.kind === `expires_at`) {
6947
+ const expiresAt = new Date(parsed.lifetime.at);
6948
+ if (Number.isNaN(expiresAt.getTime())) return apiError(400, ErrCodeInvalidRequest, `Invalid expires_at lifetime timestamp`);
6949
+ }
6950
+ let resolved;
6951
+ try {
6952
+ resolved = (0, __electric_ax_agents_runtime.resolveEventSourceSubscription)({
6953
+ contract: source,
6954
+ entityUrl,
6955
+ request: {
6956
+ ...parsed,
6957
+ id: decodeURIComponent(request.params.subscriptionId)
6958
+ },
6959
+ createdBy: `tool`
6960
+ });
6961
+ } catch (error) {
6962
+ return apiError(400, ErrCodeInvalidRequest, error instanceof Error ? error.message : String(error));
6963
+ }
6964
+ await ctx.ensureEventSourceWakeSource?.(resolved.subscription.sourceUrl);
6965
+ const result = await ctx.entityManager.upsertEventSourceSubscription(entityUrl, {
6966
+ subscription: resolved.subscription,
6967
+ manifest: (0, __electric_ax_agents_runtime.buildEventSourceManifestEntry)(resolved)
6968
+ });
6969
+ return (0, itty_router.json)(result);
6970
+ }
6971
+ async function deleteEventSourceSubscription(request, ctx) {
6972
+ const principalMutationError = rejectPrincipalEntityMutation(request, `unsubscribed from event sources`);
6973
+ if (principalMutationError) return principalMutationError;
6974
+ const { entityUrl } = requireExistingEntityRoute(request);
6975
+ const result = await ctx.entityManager.deleteEventSourceSubscription(entityUrl, { id: decodeURIComponent(request.params.subscriptionId) });
6976
+ return (0, itty_router.json)(result);
6977
+ }
6905
6978
  async function setTag(request, ctx) {
6906
- const principalMutationError = rejectPrincipalEntityMutation(request, `tagged`);
6979
+ const principalMutationError = rejectPrincipalEntityMutation(request, `tag updated`);
6907
6980
  if (principalMutationError) return principalMutationError;
6908
6981
  const parsed = routeBody(request);
6909
6982
  const { entityUrl } = requireExistingEntityRoute(request);
@@ -6911,12 +6984,12 @@ async function setTag(request, ctx) {
6911
6984
  const updated = await ctx.entityManager.setTag(entityUrl, decodeURIComponent(request.params.tagKey), { value: parsed.value }, token);
6912
6985
  return (0, itty_router.json)(toPublicEntity(updated));
6913
6986
  }
6914
- async function removeTag(request, ctx) {
6915
- const principalMutationError = rejectPrincipalEntityMutation(request, `untagged`);
6987
+ async function deleteTag(request, ctx) {
6988
+ const principalMutationError = rejectPrincipalEntityMutation(request, `tag deleted`);
6916
6989
  if (principalMutationError) return principalMutationError;
6917
6990
  const { entityUrl } = requireExistingEntityRoute(request);
6918
6991
  const token = writeTokenFromRequest(request);
6919
- const updated = await ctx.entityManager.removeTag(entityUrl, decodeURIComponent(request.params.tagKey), token);
6992
+ const updated = await ctx.entityManager.deleteTag(entityUrl, decodeURIComponent(request.params.tagKey), token);
6920
6993
  return (0, itty_router.json)(toPublicEntity(updated));
6921
6994
  }
6922
6995
  async function forkEntity(request, ctx) {
@@ -7048,17 +7121,13 @@ const registerEntityTypeBodySchema = __sinclair_typebox.Type.Object({
7048
7121
  creation_schema: __sinclair_typebox.Type.Optional(jsonObjectSchema),
7049
7122
  inbox_schemas: __sinclair_typebox.Type.Optional(schemaMapSchema),
7050
7123
  state_schemas: __sinclair_typebox.Type.Optional(schemaMapSchema),
7051
- input_schemas: __sinclair_typebox.Type.Optional(schemaMapSchema),
7052
- output_schemas: __sinclair_typebox.Type.Optional(schemaMapSchema),
7053
7124
  serve_endpoint: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
7054
7125
  default_dispatch_policy: __sinclair_typebox.Type.Optional(dispatchPolicySchema)
7055
- });
7126
+ }, { additionalProperties: false });
7056
7127
  const amendEntityTypeSchemasBodySchema = __sinclair_typebox.Type.Object({
7057
- input_schemas: __sinclair_typebox.Type.Optional(schemaMapSchema),
7058
- output_schemas: __sinclair_typebox.Type.Optional(schemaMapSchema),
7059
7128
  inbox_schemas: __sinclair_typebox.Type.Optional(schemaMapSchema),
7060
7129
  state_schemas: __sinclair_typebox.Type.Optional(schemaMapSchema)
7061
- });
7130
+ }, { additionalProperties: false });
7062
7131
  const entityTypesRouter = (0, itty_router.Router)({ base: `/_electric/entity-types` });
7063
7132
  entityTypesRouter.get(`/`, listEntityTypes);
7064
7133
  entityTypesRouter.post(`/`, withSchema(registerEntityTypeBodySchema), registerEntityType);
@@ -7098,8 +7167,8 @@ async function getEntityType(request, ctx) {
7098
7167
  async function amendSchemas(request, ctx) {
7099
7168
  const parsed = routeBody(request);
7100
7169
  const updated = await ctx.entityManager.amendSchemas(request.params.name, {
7101
- inbox_schemas: parsed.inbox_schemas ?? parsed.input_schemas,
7102
- state_schemas: parsed.state_schemas ?? parsed.output_schemas
7170
+ inbox_schemas: parsed.inbox_schemas,
7171
+ state_schemas: parsed.state_schemas
7103
7172
  });
7104
7173
  return (0, itty_router.json)(toPublicEntityType(updated));
7105
7174
  }
@@ -7109,13 +7178,12 @@ async function deleteEntityType(request, ctx) {
7109
7178
  }
7110
7179
  function normalizeEntityTypeRequest(parsed) {
7111
7180
  const serveEndpoint = rewriteLoopbackWebhookUrl(parsed.serve_endpoint);
7112
- const compatibilityFields = parsed;
7113
7181
  return {
7114
7182
  name: parsed.name ?? ``,
7115
7183
  description: parsed.description ?? ``,
7116
7184
  creation_schema: parsed.creation_schema,
7117
- inbox_schemas: parsed.inbox_schemas ?? compatibilityFields.input_schemas,
7118
- state_schemas: parsed.state_schemas ?? compatibilityFields.output_schemas,
7185
+ inbox_schemas: parsed.inbox_schemas,
7186
+ state_schemas: parsed.state_schemas,
7119
7187
  serve_endpoint: serveEndpoint,
7120
7188
  default_dispatch_policy: parsed.default_dispatch_policy ?? (serveEndpoint ? { targets: [{
7121
7189
  type: `webhook`,
@@ -7126,8 +7194,6 @@ function normalizeEntityTypeRequest(parsed) {
7126
7194
  function toPublicEntityType(entityType) {
7127
7195
  return {
7128
7196
  ...entityType,
7129
- input_schemas: entityType.inbox_schemas,
7130
- output_schemas: entityType.state_schemas,
7131
7197
  revision: entityType.revision
7132
7198
  };
7133
7199
  }
@@ -7211,13 +7277,35 @@ function errorMapper(err, req) {
7211
7277
  function rejectIfShuttingDown(req, ctx) {
7212
7278
  if (!ctx.isShuttingDown()) return void 0;
7213
7279
  const path$2 = new URL(req.url).pathname;
7214
- if (!path$2.startsWith(`/_electric/webhook-forward/`)) return void 0;
7280
+ if (!path$2.startsWith(`/_electric/subscription-webhooks/`)) return void 0;
7215
7281
  return apiError(503, `SERVER_STOPPING`, `Server is shutting down`);
7216
7282
  }
7217
7283
  function getRequestSpan(req) {
7218
7284
  return carrier(req)[SPAN_KEY];
7219
7285
  }
7220
7286
 
7287
+ //#endregion
7288
+ //#region src/routing/observations-router.ts
7289
+ const stringRecordSchema = __sinclair_typebox.Type.Record(__sinclair_typebox.Type.String(), __sinclair_typebox.Type.String());
7290
+ const ensureEntitiesMembershipStreamBodySchema = __sinclair_typebox.Type.Object({ tags: __sinclair_typebox.Type.Optional(stringRecordSchema) });
7291
+ const ensureCronStreamBodySchema = __sinclair_typebox.Type.Object({
7292
+ expression: __sinclair_typebox.Type.String(),
7293
+ timezone: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String())
7294
+ });
7295
+ const observationsRouter = (0, itty_router.Router)({ base: `/_electric/observations` });
7296
+ observationsRouter.post(`/entities/ensure-stream`, withSchema(ensureEntitiesMembershipStreamBodySchema), ensureEntitiesMembershipStream);
7297
+ observationsRouter.post(`/cron/ensure-stream`, withSchema(ensureCronStreamBodySchema), ensureCronStream);
7298
+ async function ensureEntitiesMembershipStream(request, ctx) {
7299
+ const parsed = routeBody(request);
7300
+ const result = await ctx.entityManager.ensureEntitiesMembershipStream(parsed.tags ?? {});
7301
+ return (0, itty_router.json)(result);
7302
+ }
7303
+ async function ensureCronStream(request, ctx) {
7304
+ const parsed = routeBody(request);
7305
+ const streamPath = await ctx.entityManager.getOrCreateCronStream(parsed.expression, parsed.timezone);
7306
+ return (0, itty_router.json)({ streamUrl: streamPath });
7307
+ }
7308
+
7221
7309
  //#endregion
7222
7310
  //#region src/routing/tenant-stream-paths.ts
7223
7311
  function withLeadingSlash(path$2) {
@@ -7556,7 +7644,7 @@ async function notificationFromClaim(ctx, input) {
7556
7644
  wakeId: input.claim.wake_id,
7557
7645
  streamPath: primaryStream,
7558
7646
  streams: streams$1,
7559
- callback: (0, __electric_ax_agents_runtime.appendPathToUrl)(ctx.publicUrl, `/_electric/callback-forward/${encodeURIComponent(input.claim.wake_id)}`),
7647
+ callback: (0, __electric_ax_agents_runtime.appendPathToUrl)(ctx.publicUrl, `/_electric/wake-callbacks/${encodeURIComponent(input.claim.wake_id)}`),
7560
7648
  claimToken: input.claim.token,
7561
7649
  triggerEvent: `message_received`,
7562
7650
  entity: {
@@ -7591,7 +7679,7 @@ const wakeRegistrationBodySchema = __sinclair_typebox.Type.Object({
7591
7679
  includeResponse: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Boolean()),
7592
7680
  manifestKey: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String())
7593
7681
  });
7594
- const webhookForwardBodySchema = __sinclair_typebox.Type.Object({
7682
+ const subscriptionWebhookBodySchema = __sinclair_typebox.Type.Object({
7595
7683
  subscription_id: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
7596
7684
  wake_id: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
7597
7685
  generation: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Number()),
@@ -7605,7 +7693,7 @@ const webhookForwardBodySchema = __sinclair_typebox.Type.Object({
7605
7693
  consumer_id: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
7606
7694
  callback: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String())
7607
7695
  }, { additionalProperties: true });
7608
- const callbackForwardBodySchema = __sinclair_typebox.Type.Object({
7696
+ const wakeCallbackBodySchema = __sinclair_typebox.Type.Object({
7609
7697
  epoch: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Number()),
7610
7698
  generation: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Number()),
7611
7699
  wakeId: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
@@ -7616,14 +7704,15 @@ const callbackForwardBodySchema = __sinclair_typebox.Type.Object({
7616
7704
  const DS_SUBSCRIPTION_CALLBACK_PREFIX = `ds-subscription:`;
7617
7705
  const internalRouter = (0, itty_router.Router)({ base: `/_electric` });
7618
7706
  internalRouter.get(`/health`, () => (0, itty_router.json)({ status: `ok` }));
7707
+ internalRouter.get(`/event-sources`, listEventSources);
7619
7708
  internalRouter.post(`/wake`, withSchema(wakeRegistrationBodySchema), registerWake);
7620
- internalRouter.post(`/webhook-forward/:subscriptionId`, webhookForward);
7621
- internalRouter.post(`/callback-forward/:consumerId`, callbackForward);
7709
+ internalRouter.post(`/subscription-webhooks/:subscriptionId`, subscriptionWebhook);
7710
+ internalRouter.post(`/wake-callbacks/:consumerId`, wakeCallback);
7622
7711
  internalRouter.all(`/runners`, runnersRouter.fetch);
7623
7712
  internalRouter.all(`/runners/*`, runnersRouter.fetch);
7624
7713
  internalRouter.all(`/entities/*`, entitiesRouter.fetch);
7625
7714
  internalRouter.all(`/entity-types/*`, entityTypesRouter.fetch);
7626
- internalRouter.all(`/cron/*`, cronRouter.fetch);
7715
+ internalRouter.all(`/observations/*`, observationsRouter.fetch);
7627
7716
  internalRouter.get(`/electric/*`, electricProxyRouter.fetch);
7628
7717
  internalRouter.all(`*`, () => (0, itty_router.status)(404));
7629
7718
  function routeParam(request, name) {
@@ -7656,13 +7745,30 @@ function resolveWebhookSigner(ctx) {
7656
7745
  return ctx.webhookSigner ?? getDefaultWebhookSigner();
7657
7746
  }
7658
7747
  function durableStreamsWebhookJwksUrl(ctx) {
7659
- if (!ctx.durableStreamsRouting) return (0, __electric_ax_agents_runtime.appendPathToUrl)(ctx.durableStreamsUrl, `/__ds/jwks.json`);
7748
+ if (!ctx.durableStreamsRouting) return appendPathToBackendUrl(ctx.durableStreamsUrl, `/__ds/jwks.json`);
7660
7749
  return resolveDurableStreamsRoutingAdapter(ctx.durableStreamsRouting, ctx.durableStreamsUrl).controlUrl({
7661
7750
  durableStreamsUrl: ctx.durableStreamsUrl,
7662
7751
  serviceId: ctx.service,
7663
7752
  requestUrl: (0, __electric_ax_agents_runtime.appendPathToUrl)(ctx.publicUrl, `/__ds/jwks.json`)
7664
7753
  }).toString();
7665
7754
  }
7755
+ function appendPathToBackendUrl(baseUrl, path$2) {
7756
+ const base = new URL(baseUrl);
7757
+ const pathUrl = new URL(path$2, `http://electric-agents.local`);
7758
+ const basePath = base.pathname === `/` ? `` : base.pathname.replace(/\/+$/, ``);
7759
+ const suffix = pathUrl.pathname.startsWith(`/`) ? pathUrl.pathname : `/${pathUrl.pathname}`;
7760
+ const target = new URL(base);
7761
+ target.pathname = `${basePath}${suffix}`;
7762
+ target.search = ``;
7763
+ target.hash = pathUrl.hash;
7764
+ base.searchParams.forEach((value, key) => {
7765
+ target.searchParams.append(key, value);
7766
+ });
7767
+ pathUrl.searchParams.forEach((value, key) => {
7768
+ target.searchParams.append(key, value);
7769
+ });
7770
+ return target.toString();
7771
+ }
7666
7772
  function durableStreamsJwksFetchClient(ctx) {
7667
7773
  return async (input, init) => {
7668
7774
  const headers = new Headers(init?.headers);
@@ -7719,10 +7825,17 @@ async function registerWake(request, ctx) {
7719
7825
  await ctx.entityManager.registerWake(opts);
7720
7826
  return (0, itty_router.status)(204);
7721
7827
  }
7722
- async function webhookForward(request, ctx) {
7828
+ async function listEventSources(_request, ctx) {
7829
+ const eventSources = ctx.eventSources ? await ctx.eventSources.listEventSources() : [];
7830
+ return (0, itty_router.json)({ eventSources: eventSources.filter(isAgentVisibleEventSource) });
7831
+ }
7832
+ function isAgentVisibleEventSource(source) {
7833
+ return source.agentVisible === true && source.status === `active`;
7834
+ }
7835
+ async function subscriptionWebhook(request, ctx) {
7723
7836
  const subscriptionId = routeParam(request, `subscriptionId`);
7724
7837
  const rootSpan = getRequestSpan(request);
7725
- rootSpan?.updateName(`webhook-forward`);
7838
+ rootSpan?.updateName(`subscription-webhook`);
7726
7839
  rootSpan?.setAttribute(`electric_agents.webhook.subscription_id`, subscriptionId);
7727
7840
  const body = await readRequestBody(request);
7728
7841
  const signatureError = await verifyDurableStreamsWebhook(request, ctx, body);
@@ -7736,7 +7849,7 @@ async function webhookForward(request, ctx) {
7736
7849
  }
7737
7850
  });
7738
7851
  if (!targetWebhookUrl) return apiError(404, ErrCodeSubscriptionNotFound, `Unknown webhook subscription`);
7739
- const parsedBodyResult = validateOptionalJsonBody(webhookForwardBodySchema, body, request.headers.get(`content-type`));
7852
+ const parsedBodyResult = validateOptionalJsonBody(subscriptionWebhookBodySchema, body, request.headers.get(`content-type`));
7740
7853
  if (!parsedBodyResult.ok) return parsedBodyResult.response;
7741
7854
  let forwardBody = body;
7742
7855
  let runningEntityUrl = null;
@@ -7782,7 +7895,7 @@ async function webhookForward(request, ctx) {
7782
7895
  span.end();
7783
7896
  }
7784
7897
  }).catch((err) => {
7785
- serverLog.warn(`[webhook-forward] consumerCallbacks upsert failed (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
7898
+ serverLog.warn(`[subscription-webhook] consumerCallbacks upsert failed (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
7786
7899
  }) : void 0;
7787
7900
  const [entity, enriched] = await Promise.all([entityPromise, enrichPromise]);
7788
7901
  if (entity?.status === `stopped` || entity?.status === `paused`) {
@@ -7803,7 +7916,7 @@ async function webhookForward(request, ctx) {
7803
7916
  runningEntityUrl = entity.url;
7804
7917
  }
7805
7918
  if (consumerId && callbackUrl) {
7806
- const callback = (0, __electric_ax_agents_runtime.appendPathToUrl)(ctx.publicUrl, `/_electric/callback-forward/${encodeURIComponent(consumerId)}`);
7919
+ const callback = (0, __electric_ax_agents_runtime.appendPathToUrl)(ctx.publicUrl, `/_electric/wake-callbacks/${encodeURIComponent(consumerId)}`);
7807
7920
  enriched.callback = callback;
7808
7921
  if (newWebhook) {
7809
7922
  enriched.consumerId = newWebhook.wakeId;
@@ -7840,21 +7953,21 @@ async function webhookForward(request, ctx) {
7840
7953
  });
7841
7954
  } catch (err) {
7842
7955
  if (runningEntityUrl) await ctx.entityManager.registry.updateStatus(runningEntityUrl, `idle`);
7843
- return apiError(502, `WEBHOOK_FORWARD_FAILED`, err instanceof Error ? err.message : String(err));
7956
+ return apiError(502, `SUBSCRIPTION_WEBHOOK_FAILED`, err instanceof Error ? err.message : String(err));
7844
7957
  }
7845
7958
  const responseBytes = upstream.body ? new Uint8Array(await upstream.arrayBuffer()) : new Uint8Array();
7846
7959
  return responseFromUpstream(upstream, responseBytes);
7847
7960
  }
7848
- async function callbackForward(request, ctx) {
7961
+ async function wakeCallback(request, ctx) {
7849
7962
  const consumerId = routeParam(request, `consumerId`);
7850
7963
  const rows = await ctx.pgDb.select().from(consumerCallbacks).where((0, drizzle_orm.and)((0, drizzle_orm.eq)(consumerCallbacks.tenantId, ctx.service), (0, drizzle_orm.eq)(consumerCallbacks.consumerId, consumerId))).limit(1);
7851
7964
  const target = rows[0] ? {
7852
7965
  callbackUrl: rows[0].callbackUrl,
7853
7966
  primaryStream: rows[0].primaryStream
7854
7967
  } : void 0;
7855
- if (!target) return apiError(404, ErrCodeCallbackNotFound, `Unknown callback-forward consumer`);
7968
+ if (!target) return apiError(404, ErrCodeWakeCallbackNotFound, `Unknown wake-callback consumer`);
7856
7969
  const body = await readRequestBody(request);
7857
- const parsedBodyResult = validateOptionalJsonBody(callbackForwardBodySchema, body, request.headers.get(`content-type`));
7970
+ const parsedBodyResult = validateOptionalJsonBody(wakeCallbackBodySchema, body, request.headers.get(`content-type`));
7858
7971
  if (!parsedBodyResult.ok) return parsedBodyResult.response;
7859
7972
  const requestBody = parsedBodyResult.value;
7860
7973
  const isClaimRequest = requestBody?.wakeId !== void 0 || requestBody?.wake_id !== void 0;
@@ -7872,14 +7985,14 @@ async function callbackForward(request, ctx) {
7872
7985
  }
7873
7986
  return (0, itty_router.json)(responseBody);
7874
7987
  }
7875
- const upstreamBody = encodeCallbackForwardBody(ctx.service, consumerId, requestBody, resolveDurableStreamsRoutingAdapter(ctx.durableStreamsRouting, ctx.durableStreamsUrl));
7988
+ const upstreamBody = encodeWakeCallbackBody(ctx.service, consumerId, requestBody, resolveDurableStreamsRoutingAdapter(ctx.durableStreamsRouting, ctx.durableStreamsUrl));
7876
7989
  let upstream;
7877
7990
  try {
7878
7991
  const subscriptionId = durableStreamsSubscriptionCallback(target.callbackUrl);
7879
7992
  if (subscriptionId) {
7880
7993
  const token = claimTokenFromRequest(request);
7881
7994
  if (!token) return apiError(401, `UNAUTHORIZED`, `Missing claim token`);
7882
- const upstreamPayload = encodeCallbackForwardPayload(consumerId, requestBody, (stream) => stream.replace(/^\/+/, ``));
7995
+ const upstreamPayload = encodeWakeCallbackPayload(consumerId, requestBody, (stream) => stream.replace(/^\/+/, ``));
7883
7996
  const result = await ctx.streamClient.ackSubscription(subscriptionId, token, upstreamPayload);
7884
7997
  upstream = (0, itty_router.json)(result);
7885
7998
  } else upstream = await fetch(target.callbackUrl, {
@@ -7888,7 +8001,7 @@ async function callbackForward(request, ctx) {
7888
8001
  body: bodyFromBytes(upstreamBody)
7889
8002
  });
7890
8003
  } catch (err) {
7891
- return apiError(502, `CALLBACK_FORWARD_FAILED`, err instanceof Error ? err.message : String(err));
8004
+ return apiError(502, `WAKE_CALLBACK_FAILED`, err instanceof Error ? err.message : String(err));
7892
8005
  }
7893
8006
  let responseBytes = upstream.body ? new Uint8Array(await upstream.arrayBuffer()) : new Uint8Array();
7894
8007
  if (isClaimRequest && upstream.ok && target.primaryStream) {
@@ -7908,7 +8021,7 @@ async function callbackForward(request, ctx) {
7908
8021
  epoch
7909
8022
  });
7910
8023
  if (upstream.ok && isDoneRequest && target.primaryStream) {
7911
- serverLog.info(`[callback-forward] done received for stream=${target.primaryStream} consumer=${consumerId}`);
8024
+ serverLog.info(`[wake-callback] done received for stream=${target.primaryStream} consumer=${consumerId}`);
7912
8025
  const stillOwnsClaim = ctx.runtime.claimWriteTokens.owns(ctx.service, target.primaryStream, consumerId);
7913
8026
  const entity = await ctx.entityManager.registry.getEntityByStream(target.primaryStream);
7914
8027
  let entityCleared = false;
@@ -7930,13 +8043,13 @@ async function callbackForward(request, ctx) {
7930
8043
  if (entity && (entityCleared || stillOwnsClaim)) {
7931
8044
  await ctx.entityManager.registry.updateStatus(entity.url, entity.status === `stopping` ? `stopped` : `idle`);
7932
8045
  await ctx.entityBridgeManager.onEntityChanged(entity.url);
7933
- serverLog.info(`[callback-forward] status updated after done for ${entity.url}`);
7934
- } else if (!entity) serverLog.warn(`[callback-forward] done received but no entity found for stream=${target.primaryStream}`);
8046
+ serverLog.info(`[wake-callback] status updated after done for ${entity.url}`);
8047
+ } else if (!entity) serverLog.warn(`[wake-callback] done received but no entity found for stream=${target.primaryStream}`);
7935
8048
  if (stillOwnsClaim) ctx.runtime.claimWriteTokens.clearStream(ctx.service, target.primaryStream);
7936
- else if (entity) serverLog.info(`[callback-forward] done arrived after in-memory token evicted (stream=${target.primaryStream} consumer=${consumerId})`);
7937
- } else if (requestBody?.done === true) serverLog.warn(`[callback-forward] done received but skipped: upstream.ok=${upstream.ok} primaryStream=${target.primaryStream ?? `null`} consumer=${consumerId}`);
8049
+ else if (entity) serverLog.info(`[wake-callback] done arrived after in-memory token evicted (stream=${target.primaryStream} consumer=${consumerId})`);
8050
+ } else if (requestBody?.done === true) serverLog.warn(`[wake-callback] done received but skipped: upstream.ok=${upstream.ok} primaryStream=${target.primaryStream ?? `null`} consumer=${consumerId}`);
7938
8051
  } catch (err) {
7939
- serverLog.error(`[callback-forward] error processing done for consumer=${consumerId}: ${err instanceof Error ? err.message : String(err)}`);
8052
+ serverLog.error(`[wake-callback] error processing done for consumer=${consumerId}: ${err instanceof Error ? err.message : String(err)}`);
7940
8053
  }
7941
8054
  return responseFromUpstream(upstream, responseBytes);
7942
8055
  }
@@ -7945,11 +8058,11 @@ async function mintClaimWriteToken(ctx, streamPath, consumerId) {
7945
8058
  if (!entity) return void 0;
7946
8059
  return ctx.runtime.claimWriteTokens.mint(ctx.service, streamPath, consumerId);
7947
8060
  }
7948
- function encodeCallbackForwardBody(service, consumerId, body, routingAdapter) {
7949
- const payload = encodeCallbackForwardPayload(consumerId, body, (stream) => routingAdapter.toBackendStreamPath(service, stream));
8061
+ function encodeWakeCallbackBody(service, consumerId, body, routingAdapter) {
8062
+ const payload = encodeWakeCallbackPayload(consumerId, body, (stream) => routingAdapter.toBackendStreamPath(service, stream));
7950
8063
  return new TextEncoder().encode(JSON.stringify(payload));
7951
8064
  }
7952
- function encodeCallbackForwardPayload(consumerId, body, mapStream) {
8065
+ function encodeWakeCallbackPayload(consumerId, body, mapStream) {
7953
8066
  if (!body) return {};
7954
8067
  const generation = body.generation ?? body.epoch;
7955
8068
  const wakeId = body.wake_id ?? body.wakeId ?? consumerId;