@electric-ax/agents-server 0.4.7 → 0.4.9
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/entrypoint.js +107 -3
- package/dist/index.cjs +104 -2
- package/dist/index.d.cts +21 -2
- package/dist/index.d.ts +21 -2
- package/dist/index.js +105 -3
- package/package.json +5 -5
- package/src/electric-agents-types.ts +1 -0
- package/src/entity-manager.ts +64 -0
- package/src/index.ts +9 -1
- package/src/manifest-side-effects.ts +11 -0
- package/src/routing/context.ts +18 -1
- package/src/routing/entities-router.ts +130 -0
- package/src/routing/internal-router.ts +19 -1
- package/src/server.ts +12 -0
package/dist/entrypoint.js
CHANGED
|
@@ -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";
|
|
@@ -2668,6 +2668,10 @@ function extractManifestSourceUrl(manifest) {
|
|
|
2668
2668
|
}
|
|
2669
2669
|
if (manifest.sourceType === `entities`) return typeof manifest.sourceRef === `string` ? `/_entities/${manifest.sourceRef}` : void 0;
|
|
2670
2670
|
if (manifest.sourceType === `db`) return typeof manifest.sourceRef === `string` ? getSharedStateStreamPath(manifest.sourceRef) : void 0;
|
|
2671
|
+
if (manifest.sourceType === `webhook`) {
|
|
2672
|
+
if (typeof config?.streamUrl === `string`) return config.streamUrl;
|
|
2673
|
+
if (typeof config?.endpointKey === `string`) return getWebhookStreamPath(config.endpointKey, typeof config.bucket === `string` ? config.bucket : void 0);
|
|
2674
|
+
}
|
|
2671
2675
|
return void 0;
|
|
2672
2676
|
}
|
|
2673
2677
|
if (manifest.kind === `shared-state`) return typeof manifest.id === `string` ? getSharedStateStreamPath(manifest.id) : void 0;
|
|
@@ -2954,7 +2958,8 @@ var EntityManager = class {
|
|
|
2954
2958
|
debounceMs: req.wake.debounceMs,
|
|
2955
2959
|
timeoutMs: req.wake.timeoutMs,
|
|
2956
2960
|
oneShot: false,
|
|
2957
|
-
includeResponse: req.wake.includeResponse
|
|
2961
|
+
includeResponse: req.wake.includeResponse,
|
|
2962
|
+
manifestKey: req.wake.manifestKey
|
|
2958
2963
|
});
|
|
2959
2964
|
const contentType = `application/json`;
|
|
2960
2965
|
const createdEvent = entityStateSchema.entityCreated.insert({
|
|
@@ -3848,6 +3853,35 @@ var EntityManager = class {
|
|
|
3848
3853
|
await this.writeManifestEntry(entityUrl, manifestKey, `delete`, void 0, { txid });
|
|
3849
3854
|
return { txid };
|
|
3850
3855
|
}
|
|
3856
|
+
async upsertEventSourceSubscription(entityUrl, req) {
|
|
3857
|
+
const manifestKey = req.subscription.manifestKey;
|
|
3858
|
+
const txid = randomUUID();
|
|
3859
|
+
await this.writeManifestEntry(entityUrl, manifestKey, `upsert`, req.manifest, { txid });
|
|
3860
|
+
await this.wakeRegistry.unregisterByManifestKey(entityUrl, manifestKey, this.tenantId);
|
|
3861
|
+
await this.wakeRegistry.register({
|
|
3862
|
+
tenantId: this.tenantId,
|
|
3863
|
+
subscriberUrl: entityUrl,
|
|
3864
|
+
sourceUrl: req.subscription.sourceUrl,
|
|
3865
|
+
condition: {
|
|
3866
|
+
on: `change`,
|
|
3867
|
+
collections: [`webhook_event`],
|
|
3868
|
+
ops: [`insert`]
|
|
3869
|
+
},
|
|
3870
|
+
oneShot: false,
|
|
3871
|
+
manifestKey
|
|
3872
|
+
});
|
|
3873
|
+
return {
|
|
3874
|
+
txid,
|
|
3875
|
+
subscription: req.subscription
|
|
3876
|
+
};
|
|
3877
|
+
}
|
|
3878
|
+
async deleteEventSourceSubscription(entityUrl, req) {
|
|
3879
|
+
const manifestKey = eventSourceSubscriptionManifestKey(req.id);
|
|
3880
|
+
const txid = randomUUID();
|
|
3881
|
+
await this.writeManifestEntry(entityUrl, manifestKey, `delete`, void 0, { txid });
|
|
3882
|
+
await this.wakeRegistry.unregisterByManifestKey(entityUrl, manifestKey, this.tenantId);
|
|
3883
|
+
return { txid };
|
|
3884
|
+
}
|
|
3851
3885
|
/**
|
|
3852
3886
|
* Register a wake subscription from a subscriber to a source entity.
|
|
3853
3887
|
*/
|
|
@@ -4476,7 +4510,8 @@ const spawnBodySchema = Type.Object({
|
|
|
4476
4510
|
condition: wakeConditionSchema,
|
|
4477
4511
|
debounceMs: Type.Optional(Type.Number()),
|
|
4478
4512
|
timeoutMs: Type.Optional(Type.Number()),
|
|
4479
|
-
includeResponse: Type.Optional(Type.Boolean())
|
|
4513
|
+
includeResponse: Type.Optional(Type.Boolean()),
|
|
4514
|
+
manifestKey: Type.Optional(Type.String())
|
|
4480
4515
|
}))
|
|
4481
4516
|
});
|
|
4482
4517
|
const sendBodySchema = Type.Object({
|
|
@@ -4542,6 +4577,22 @@ const scheduleBodySchema = Type.Union([Type.Object({
|
|
|
4542
4577
|
messageType: Type.Optional(Type.String()),
|
|
4543
4578
|
from: Type.Optional(Type.String())
|
|
4544
4579
|
})]);
|
|
4580
|
+
const subscriptionLifetimeSchema = Type.Union([
|
|
4581
|
+
Type.Object({ kind: Type.Literal(`until_entity_stopped`) }),
|
|
4582
|
+
Type.Object({
|
|
4583
|
+
kind: Type.Literal(`expires_at`),
|
|
4584
|
+
at: Type.String()
|
|
4585
|
+
}),
|
|
4586
|
+
Type.Object({ kind: Type.Literal(`manual`) })
|
|
4587
|
+
]);
|
|
4588
|
+
const eventSourceSubscriptionBodySchema = Type.Object({
|
|
4589
|
+
sourceKey: Type.String(),
|
|
4590
|
+
bucketKey: Type.Optional(Type.String()),
|
|
4591
|
+
params: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
|
|
4592
|
+
filterKey: Type.Optional(Type.String()),
|
|
4593
|
+
lifetime: Type.Optional(subscriptionLifetimeSchema),
|
|
4594
|
+
reason: Type.Optional(Type.String())
|
|
4595
|
+
});
|
|
4545
4596
|
const entitiesRegisterBodySchema = Type.Object({ tags: Type.Optional(stringRecordSchema) });
|
|
4546
4597
|
const entitiesRouter = Router({ base: `/_electric/entities` });
|
|
4547
4598
|
entitiesRouter.get(`/`, listEntities);
|
|
@@ -4559,6 +4610,8 @@ entitiesRouter.post(`/:type/:instanceId/tags/:tagKey`, withExistingEntity, withS
|
|
|
4559
4610
|
entitiesRouter.delete(`/:type/:instanceId/tags/:tagKey`, withExistingEntity, removeTag);
|
|
4560
4611
|
entitiesRouter.put(`/:type/:instanceId/schedules/:scheduleId`, withExistingEntity, withSchema(scheduleBodySchema), upsertSchedule);
|
|
4561
4612
|
entitiesRouter.delete(`/:type/:instanceId/schedules/:scheduleId`, withExistingEntity, deleteSchedule);
|
|
4613
|
+
entitiesRouter.put(`/:type/:instanceId/event-source-subscriptions/:subscriptionId`, withExistingEntity, withSchema(eventSourceSubscriptionBodySchema), upsertEventSourceSubscription);
|
|
4614
|
+
entitiesRouter.delete(`/:type/:instanceId/event-source-subscriptions/:subscriptionId`, withExistingEntity, deleteEventSourceSubscription);
|
|
4562
4615
|
function entityUrlFromSegments(type, instanceId) {
|
|
4563
4616
|
if (!type || !instanceId) return null;
|
|
4564
4617
|
if (type.startsWith(`_`) || type.includes(`*`) || instanceId.includes(`*`)) return null;
|
|
@@ -4665,6 +4718,47 @@ async function deleteSchedule(request, ctx) {
|
|
|
4665
4718
|
const result = await ctx.entityManager.deleteSchedule(entityUrl, { id: decodeURIComponent(request.params.scheduleId) });
|
|
4666
4719
|
return json(result);
|
|
4667
4720
|
}
|
|
4721
|
+
async function upsertEventSourceSubscription(request, ctx) {
|
|
4722
|
+
const principalMutationError = rejectPrincipalEntityMutation(request, `subscribed to event sources`);
|
|
4723
|
+
if (principalMutationError) return principalMutationError;
|
|
4724
|
+
const catalog = ctx.eventSources;
|
|
4725
|
+
if (!catalog) return apiError(404, ErrCodeNotFound, `No event source catalog is configured`);
|
|
4726
|
+
const { entityUrl } = requireExistingEntityRoute(request);
|
|
4727
|
+
const parsed = routeBody(request);
|
|
4728
|
+
const source = await catalog.getEventSource(parsed.sourceKey);
|
|
4729
|
+
if (!source) return apiError(404, ErrCodeNotFound, `Event source "${parsed.sourceKey}" not found`);
|
|
4730
|
+
if (parsed.lifetime?.kind === `expires_at`) {
|
|
4731
|
+
const expiresAt = new Date(parsed.lifetime.at);
|
|
4732
|
+
if (Number.isNaN(expiresAt.getTime())) return apiError(400, ErrCodeInvalidRequest, `Invalid expires_at lifetime timestamp`);
|
|
4733
|
+
}
|
|
4734
|
+
let resolved;
|
|
4735
|
+
try {
|
|
4736
|
+
resolved = resolveEventSourceSubscription({
|
|
4737
|
+
contract: source,
|
|
4738
|
+
entityUrl,
|
|
4739
|
+
request: {
|
|
4740
|
+
...parsed,
|
|
4741
|
+
id: decodeURIComponent(request.params.subscriptionId)
|
|
4742
|
+
},
|
|
4743
|
+
createdBy: `tool`
|
|
4744
|
+
});
|
|
4745
|
+
} catch (error) {
|
|
4746
|
+
return apiError(400, ErrCodeInvalidRequest, error instanceof Error ? error.message : String(error));
|
|
4747
|
+
}
|
|
4748
|
+
await ctx.ensureEventSourceWakeSource?.(resolved.subscription.sourceUrl);
|
|
4749
|
+
const result = await ctx.entityManager.upsertEventSourceSubscription(entityUrl, {
|
|
4750
|
+
subscription: resolved.subscription,
|
|
4751
|
+
manifest: buildEventSourceManifestEntry(resolved)
|
|
4752
|
+
});
|
|
4753
|
+
return json(result);
|
|
4754
|
+
}
|
|
4755
|
+
async function deleteEventSourceSubscription(request, ctx) {
|
|
4756
|
+
const principalMutationError = rejectPrincipalEntityMutation(request, `unsubscribed from event sources`);
|
|
4757
|
+
if (principalMutationError) return principalMutationError;
|
|
4758
|
+
const { entityUrl } = requireExistingEntityRoute(request);
|
|
4759
|
+
const result = await ctx.entityManager.deleteEventSourceSubscription(entityUrl, { id: decodeURIComponent(request.params.subscriptionId) });
|
|
4760
|
+
return json(result);
|
|
4761
|
+
}
|
|
4668
4762
|
async function setTag(request, ctx) {
|
|
4669
4763
|
const principalMutationError = rejectPrincipalEntityMutation(request, `tagged`);
|
|
4670
4764
|
if (principalMutationError) return principalMutationError;
|
|
@@ -5379,6 +5473,7 @@ const callbackForwardBodySchema = Type.Object({
|
|
|
5379
5473
|
const DS_SUBSCRIPTION_CALLBACK_PREFIX = `ds-subscription:`;
|
|
5380
5474
|
const internalRouter = Router({ base: `/_electric` });
|
|
5381
5475
|
internalRouter.get(`/health`, () => json({ status: `ok` }));
|
|
5476
|
+
internalRouter.get(`/event-sources`, listEventSources);
|
|
5382
5477
|
internalRouter.post(`/wake`, withSchema(wakeRegistrationBodySchema), registerWake);
|
|
5383
5478
|
internalRouter.post(`/webhook-forward/:subscriptionId`, webhookForward);
|
|
5384
5479
|
internalRouter.post(`/callback-forward/:consumerId`, callbackForward);
|
|
@@ -5482,6 +5577,13 @@ async function registerWake(request, ctx) {
|
|
|
5482
5577
|
await ctx.entityManager.registerWake(opts);
|
|
5483
5578
|
return status(204);
|
|
5484
5579
|
}
|
|
5580
|
+
async function listEventSources(_request, ctx) {
|
|
5581
|
+
const eventSources = ctx.eventSources ? await ctx.eventSources.listEventSources() : [];
|
|
5582
|
+
return json({ eventSources: eventSources.filter(isAgentVisibleEventSource) });
|
|
5583
|
+
}
|
|
5584
|
+
function isAgentVisibleEventSource(source) {
|
|
5585
|
+
return source.agentVisible === true && source.status === `active`;
|
|
5586
|
+
}
|
|
5485
5587
|
async function webhookForward(request, ctx) {
|
|
5486
5588
|
const subscriptionId = routeParam(request, `subscriptionId`);
|
|
5487
5589
|
const rootSpan = getRequestSpan(request);
|
|
@@ -8062,6 +8164,8 @@ var ElectricAgentsServer = class {
|
|
|
8062
8164
|
streamClient: this.streamClient,
|
|
8063
8165
|
runtime: this.standaloneRuntime.runtime,
|
|
8064
8166
|
entityBridgeManager: this.entityBridgeManager,
|
|
8167
|
+
...this.options.eventSources ? { eventSources: this.options.eventSources } : {},
|
|
8168
|
+
...this.options.ensureEventSourceWakeSource ? { ensureEventSourceWakeSource: this.options.ensureEventSourceWakeSource } : {},
|
|
8065
8169
|
isShuttingDown: () => this.shuttingDown,
|
|
8066
8170
|
mockAgent: this.mockAgentBootstrap ? { runtime: this.mockAgentBootstrap.runtime } : void 0
|
|
8067
8171
|
};
|
package/dist/index.cjs
CHANGED
|
@@ -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({
|
|
@@ -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
|
*/
|
|
@@ -6713,7 +6747,8 @@ const spawnBodySchema = __sinclair_typebox.Type.Object({
|
|
|
6713
6747
|
condition: wakeConditionSchema,
|
|
6714
6748
|
debounceMs: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Number()),
|
|
6715
6749
|
timeoutMs: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Number()),
|
|
6716
|
-
includeResponse: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Boolean())
|
|
6750
|
+
includeResponse: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Boolean()),
|
|
6751
|
+
manifestKey: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String())
|
|
6717
6752
|
}))
|
|
6718
6753
|
});
|
|
6719
6754
|
const sendBodySchema = __sinclair_typebox.Type.Object({
|
|
@@ -6779,6 +6814,22 @@ const scheduleBodySchema = __sinclair_typebox.Type.Union([__sinclair_typebox.Typ
|
|
|
6779
6814
|
messageType: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
|
|
6780
6815
|
from: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String())
|
|
6781
6816
|
})]);
|
|
6817
|
+
const subscriptionLifetimeSchema = __sinclair_typebox.Type.Union([
|
|
6818
|
+
__sinclair_typebox.Type.Object({ kind: __sinclair_typebox.Type.Literal(`until_entity_stopped`) }),
|
|
6819
|
+
__sinclair_typebox.Type.Object({
|
|
6820
|
+
kind: __sinclair_typebox.Type.Literal(`expires_at`),
|
|
6821
|
+
at: __sinclair_typebox.Type.String()
|
|
6822
|
+
}),
|
|
6823
|
+
__sinclair_typebox.Type.Object({ kind: __sinclair_typebox.Type.Literal(`manual`) })
|
|
6824
|
+
]);
|
|
6825
|
+
const eventSourceSubscriptionBodySchema = __sinclair_typebox.Type.Object({
|
|
6826
|
+
sourceKey: __sinclair_typebox.Type.String(),
|
|
6827
|
+
bucketKey: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
|
|
6828
|
+
params: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Record(__sinclair_typebox.Type.String(), __sinclair_typebox.Type.Unknown())),
|
|
6829
|
+
filterKey: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
|
|
6830
|
+
lifetime: __sinclair_typebox.Type.Optional(subscriptionLifetimeSchema),
|
|
6831
|
+
reason: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String())
|
|
6832
|
+
});
|
|
6782
6833
|
const entitiesRegisterBodySchema = __sinclair_typebox.Type.Object({ tags: __sinclair_typebox.Type.Optional(stringRecordSchema) });
|
|
6783
6834
|
const entitiesRouter = (0, itty_router.Router)({ base: `/_electric/entities` });
|
|
6784
6835
|
entitiesRouter.get(`/`, listEntities);
|
|
@@ -6796,6 +6847,8 @@ entitiesRouter.post(`/:type/:instanceId/tags/:tagKey`, withExistingEntity, withS
|
|
|
6796
6847
|
entitiesRouter.delete(`/:type/:instanceId/tags/:tagKey`, withExistingEntity, removeTag);
|
|
6797
6848
|
entitiesRouter.put(`/:type/:instanceId/schedules/:scheduleId`, withExistingEntity, withSchema(scheduleBodySchema), upsertSchedule);
|
|
6798
6849
|
entitiesRouter.delete(`/:type/:instanceId/schedules/:scheduleId`, withExistingEntity, deleteSchedule);
|
|
6850
|
+
entitiesRouter.put(`/:type/:instanceId/event-source-subscriptions/:subscriptionId`, withExistingEntity, withSchema(eventSourceSubscriptionBodySchema), upsertEventSourceSubscription);
|
|
6851
|
+
entitiesRouter.delete(`/:type/:instanceId/event-source-subscriptions/:subscriptionId`, withExistingEntity, deleteEventSourceSubscription);
|
|
6799
6852
|
function entityUrlFromSegments(type, instanceId) {
|
|
6800
6853
|
if (!type || !instanceId) return null;
|
|
6801
6854
|
if (type.startsWith(`_`) || type.includes(`*`) || instanceId.includes(`*`)) return null;
|
|
@@ -6902,6 +6955,47 @@ async function deleteSchedule(request, ctx) {
|
|
|
6902
6955
|
const result = await ctx.entityManager.deleteSchedule(entityUrl, { id: decodeURIComponent(request.params.scheduleId) });
|
|
6903
6956
|
return (0, itty_router.json)(result);
|
|
6904
6957
|
}
|
|
6958
|
+
async function upsertEventSourceSubscription(request, ctx) {
|
|
6959
|
+
const principalMutationError = rejectPrincipalEntityMutation(request, `subscribed to event sources`);
|
|
6960
|
+
if (principalMutationError) return principalMutationError;
|
|
6961
|
+
const catalog = ctx.eventSources;
|
|
6962
|
+
if (!catalog) return apiError(404, ErrCodeNotFound, `No event source catalog is configured`);
|
|
6963
|
+
const { entityUrl } = requireExistingEntityRoute(request);
|
|
6964
|
+
const parsed = routeBody(request);
|
|
6965
|
+
const source = await catalog.getEventSource(parsed.sourceKey);
|
|
6966
|
+
if (!source) return apiError(404, ErrCodeNotFound, `Event source "${parsed.sourceKey}" not found`);
|
|
6967
|
+
if (parsed.lifetime?.kind === `expires_at`) {
|
|
6968
|
+
const expiresAt = new Date(parsed.lifetime.at);
|
|
6969
|
+
if (Number.isNaN(expiresAt.getTime())) return apiError(400, ErrCodeInvalidRequest, `Invalid expires_at lifetime timestamp`);
|
|
6970
|
+
}
|
|
6971
|
+
let resolved;
|
|
6972
|
+
try {
|
|
6973
|
+
resolved = (0, __electric_ax_agents_runtime.resolveEventSourceSubscription)({
|
|
6974
|
+
contract: source,
|
|
6975
|
+
entityUrl,
|
|
6976
|
+
request: {
|
|
6977
|
+
...parsed,
|
|
6978
|
+
id: decodeURIComponent(request.params.subscriptionId)
|
|
6979
|
+
},
|
|
6980
|
+
createdBy: `tool`
|
|
6981
|
+
});
|
|
6982
|
+
} catch (error) {
|
|
6983
|
+
return apiError(400, ErrCodeInvalidRequest, error instanceof Error ? error.message : String(error));
|
|
6984
|
+
}
|
|
6985
|
+
await ctx.ensureEventSourceWakeSource?.(resolved.subscription.sourceUrl);
|
|
6986
|
+
const result = await ctx.entityManager.upsertEventSourceSubscription(entityUrl, {
|
|
6987
|
+
subscription: resolved.subscription,
|
|
6988
|
+
manifest: (0, __electric_ax_agents_runtime.buildEventSourceManifestEntry)(resolved)
|
|
6989
|
+
});
|
|
6990
|
+
return (0, itty_router.json)(result);
|
|
6991
|
+
}
|
|
6992
|
+
async function deleteEventSourceSubscription(request, ctx) {
|
|
6993
|
+
const principalMutationError = rejectPrincipalEntityMutation(request, `unsubscribed from event sources`);
|
|
6994
|
+
if (principalMutationError) return principalMutationError;
|
|
6995
|
+
const { entityUrl } = requireExistingEntityRoute(request);
|
|
6996
|
+
const result = await ctx.entityManager.deleteEventSourceSubscription(entityUrl, { id: decodeURIComponent(request.params.subscriptionId) });
|
|
6997
|
+
return (0, itty_router.json)(result);
|
|
6998
|
+
}
|
|
6905
6999
|
async function setTag(request, ctx) {
|
|
6906
7000
|
const principalMutationError = rejectPrincipalEntityMutation(request, `tagged`);
|
|
6907
7001
|
if (principalMutationError) return principalMutationError;
|
|
@@ -7616,6 +7710,7 @@ const callbackForwardBodySchema = __sinclair_typebox.Type.Object({
|
|
|
7616
7710
|
const DS_SUBSCRIPTION_CALLBACK_PREFIX = `ds-subscription:`;
|
|
7617
7711
|
const internalRouter = (0, itty_router.Router)({ base: `/_electric` });
|
|
7618
7712
|
internalRouter.get(`/health`, () => (0, itty_router.json)({ status: `ok` }));
|
|
7713
|
+
internalRouter.get(`/event-sources`, listEventSources);
|
|
7619
7714
|
internalRouter.post(`/wake`, withSchema(wakeRegistrationBodySchema), registerWake);
|
|
7620
7715
|
internalRouter.post(`/webhook-forward/:subscriptionId`, webhookForward);
|
|
7621
7716
|
internalRouter.post(`/callback-forward/:consumerId`, callbackForward);
|
|
@@ -7719,6 +7814,13 @@ async function registerWake(request, ctx) {
|
|
|
7719
7814
|
await ctx.entityManager.registerWake(opts);
|
|
7720
7815
|
return (0, itty_router.status)(204);
|
|
7721
7816
|
}
|
|
7817
|
+
async function listEventSources(_request, ctx) {
|
|
7818
|
+
const eventSources = ctx.eventSources ? await ctx.eventSources.listEventSources() : [];
|
|
7819
|
+
return (0, itty_router.json)({ eventSources: eventSources.filter(isAgentVisibleEventSource) });
|
|
7820
|
+
}
|
|
7821
|
+
function isAgentVisibleEventSource(source) {
|
|
7822
|
+
return source.agentVisible === true && source.status === `active`;
|
|
7823
|
+
}
|
|
7722
7824
|
async function webhookForward(request, ctx) {
|
|
7723
7825
|
const subscriptionId = routeParam(request, `subscriptionId`);
|
|
7724
7826
|
const rootSpan = getRequestSpan(request);
|
package/dist/index.d.cts
CHANGED
|
@@ -187,7 +187,7 @@ import * as drizzle_orm_pg_core238 from "drizzle-orm/pg-core";
|
|
|
187
187
|
import * as drizzle_orm_pg_core239 from "drizzle-orm/pg-core";
|
|
188
188
|
import * as drizzle_orm73 from "drizzle-orm";
|
|
189
189
|
import * as drizzle_orm74 from "drizzle-orm";
|
|
190
|
-
import { EntityTags, WebhookNotification, WebhookSignatureVerifierConfig } from "@electric-ax/agents-runtime";
|
|
190
|
+
import { EntityTags, EventSourceBucket, EventSourceContract, EventSourceContract as EventSourceContract$1, EventSourceFilter, EventSourceSubscription, EventSourceSubscription as EventSourceSubscription$1, EventSourceSubscriptionInput, SubscriptionLifetime, WebhookNotification, WebhookSignatureVerifierConfig } from "@electric-ax/agents-runtime";
|
|
191
191
|
import "@sinclair/typebox";
|
|
192
192
|
import { MaybePromise } from "@durable-streams/client";
|
|
193
193
|
import { AutoRouterType, IRequest } from "itty-router";
|
|
@@ -3441,6 +3441,7 @@ interface TypedSpawnRequest {
|
|
|
3441
3441
|
debounceMs?: number;
|
|
3442
3442
|
timeoutMs?: number;
|
|
3443
3443
|
includeResponse?: boolean;
|
|
3444
|
+
manifestKey?: string;
|
|
3444
3445
|
};
|
|
3445
3446
|
}
|
|
3446
3447
|
interface SendRequest {
|
|
@@ -4205,6 +4206,18 @@ declare class EntityManager {
|
|
|
4205
4206
|
}): Promise<{
|
|
4206
4207
|
txid: string;
|
|
4207
4208
|
}>;
|
|
4209
|
+
upsertEventSourceSubscription(entityUrl: string, req: {
|
|
4210
|
+
subscription: EventSourceSubscription$1;
|
|
4211
|
+
manifest: Record<string, unknown>;
|
|
4212
|
+
}): Promise<{
|
|
4213
|
+
txid: string;
|
|
4214
|
+
subscription: EventSourceSubscription$1;
|
|
4215
|
+
}>;
|
|
4216
|
+
deleteEventSourceSubscription(entityUrl: string, req: {
|
|
4217
|
+
id: string;
|
|
4218
|
+
}): Promise<{
|
|
4219
|
+
txid: string;
|
|
4220
|
+
}>;
|
|
4208
4221
|
/**
|
|
4209
4222
|
* Register a wake subscription from a subscriber to a source entity.
|
|
4210
4223
|
*/
|
|
@@ -4454,6 +4467,10 @@ declare function webhookSigningMetadata(signer: WebhookSigner, streamRootUrl: st
|
|
|
4454
4467
|
|
|
4455
4468
|
//#endregion
|
|
4456
4469
|
//#region src/routing/context.d.ts
|
|
4470
|
+
interface EventSourceCatalog {
|
|
4471
|
+
listEventSources: () => Array<EventSourceContract$1> | Promise<Array<EventSourceContract$1>>;
|
|
4472
|
+
getEventSource: (sourceKey: string) => EventSourceContract$1 | undefined | Promise<EventSourceContract$1 | undefined>;
|
|
4473
|
+
}
|
|
4457
4474
|
/**
|
|
4458
4475
|
* Per-request tenant context passed through every router and handler.
|
|
4459
4476
|
*
|
|
@@ -4480,6 +4497,8 @@ interface TenantContext {
|
|
|
4480
4497
|
streamClient: StreamClient;
|
|
4481
4498
|
runtime: ElectricAgentsTenantRuntime;
|
|
4482
4499
|
entityBridgeManager: EntityBridgeCoordinator;
|
|
4500
|
+
eventSources?: EventSourceCatalog;
|
|
4501
|
+
ensureEventSourceWakeSource?: (sourceUrl: string) => Promise<void> | void;
|
|
4483
4502
|
isShuttingDown: () => boolean;
|
|
4484
4503
|
}
|
|
4485
4504
|
|
|
@@ -4499,4 +4518,4 @@ declare class UnregisteredTenantError extends Error {
|
|
|
4499
4518
|
declare function isUnregisteredTenantError(error: unknown): error is UnregisteredTenantError;
|
|
4500
4519
|
|
|
4501
4520
|
//#endregion
|
|
4502
|
-
export { AgentsHost, AgentsHostOptions, AgentsHostTenantConfig, AgentsHostTenantRuntime, AuthenticateRequest, ConsumerClaim, DEFAULT_TENANT_ID, DispatchPolicy, DispatchTarget, DrizzleDB, DurableStreamsBearerProvider, DurableStreamsRoutingAdapter, DurableStreamsRoutingInput, Ed25519WebhookSignerOptions, ElectricAgentsEntity, ElectricAgentsEntityRow, ElectricAgentsEntityType, ElectricAgentsRunner, ElectricAgentsUser, EntityBridgeCoordinator, EntityDispatchState, EntityListFilter, EntitySignal, EntityStatus, GlobalRoutes, PgClient, Principal, PrincipalKind, PublicElectricAgentsEntity, PublicWakeNotification, RegisterEntityTypeRequest, RegisterRunnerRequest, RequestPrincipal, RunnerAdminStatus, RunnerHeartbeatRequest, RunnerKind, RunnerLiveness, SendRequest, SignalRequest, SignalResponse, SourceStreamOffset, StreamClient, StreamClientOptions, SubscriptionClaimResponse, SubscriptionCreateInput, SubscriptionResponse, SubscriptionStreamInfo, TenantContext, TypedSpawnRequest, UnregisteredTenantError, WakeNotificationRow, WebhookJwks, WebhookPublicJwk, WebhookSigner, WebhookSigningKeyInput, WebhookSigningMetadata, assertEntitySignal, assertEntityStatus, createDb, createEd25519WebhookSigner, expectedSignalStatus, getDefaultWebhookSigner, globalRouter, isTerminalEntityStatus, isUnregisteredTenantError, pathPrefixedSingleTenantDurableStreamsRoutingAdapter, rejectsNormalWrites, runMigrations, streamRootDurableStreamsRoutingAdapter, tenantRootDurableStreamsRoutingAdapter, toPublicEntity, webhookSigningMetadata };
|
|
4521
|
+
export { AgentsHost, AgentsHostOptions, AgentsHostTenantConfig, AgentsHostTenantRuntime, AuthenticateRequest, ConsumerClaim, DEFAULT_TENANT_ID, DispatchPolicy, DispatchTarget, DrizzleDB, DurableStreamsBearerProvider, DurableStreamsRoutingAdapter, DurableStreamsRoutingInput, Ed25519WebhookSignerOptions, ElectricAgentsEntity, ElectricAgentsEntityRow, ElectricAgentsEntityType, ElectricAgentsRunner, ElectricAgentsUser, EntityBridgeCoordinator, EntityDispatchState, EntityListFilter, EntitySignal, EntityStatus, EventSourceBucket, EventSourceCatalog, EventSourceContract, EventSourceFilter, EventSourceSubscription, EventSourceSubscriptionInput, GlobalRoutes, PgClient, Principal, PrincipalKind, PublicElectricAgentsEntity, PublicWakeNotification, RegisterEntityTypeRequest, RegisterRunnerRequest, RequestPrincipal, RunnerAdminStatus, RunnerHeartbeatRequest, RunnerKind, RunnerLiveness, SendRequest, SignalRequest, SignalResponse, SourceStreamOffset, StreamClient, StreamClientOptions, SubscriptionClaimResponse, SubscriptionCreateInput, SubscriptionLifetime, SubscriptionResponse, SubscriptionStreamInfo, TenantContext, TypedSpawnRequest, UnregisteredTenantError, WakeNotificationRow, WebhookJwks, WebhookPublicJwk, WebhookSigner, WebhookSigningKeyInput, WebhookSigningMetadata, assertEntitySignal, assertEntityStatus, createDb, createEd25519WebhookSigner, expectedSignalStatus, getDefaultWebhookSigner, globalRouter, isTerminalEntityStatus, isUnregisteredTenantError, pathPrefixedSingleTenantDurableStreamsRoutingAdapter, rejectsNormalWrites, runMigrations, streamRootDurableStreamsRoutingAdapter, tenantRootDurableStreamsRoutingAdapter, toPublicEntity, webhookSigningMetadata };
|
package/dist/index.d.ts
CHANGED
|
@@ -189,7 +189,7 @@ import * as drizzle_orm_pg_core237 from "drizzle-orm/pg-core";
|
|
|
189
189
|
import * as drizzle_orm_pg_core238 from "drizzle-orm/pg-core";
|
|
190
190
|
import * as drizzle_orm_pg_core239 from "drizzle-orm/pg-core";
|
|
191
191
|
import { JsonWebKey, KeyObject } from "node:crypto";
|
|
192
|
-
import { EntityTags, WebhookNotification, WebhookSignatureVerifierConfig } from "@electric-ax/agents-runtime";
|
|
192
|
+
import { EntityTags, EventSourceBucket, EventSourceContract, EventSourceContract as EventSourceContract$1, EventSourceFilter, EventSourceSubscription, EventSourceSubscription as EventSourceSubscription$1, EventSourceSubscriptionInput, SubscriptionLifetime, WebhookNotification, WebhookSignatureVerifierConfig } from "@electric-ax/agents-runtime";
|
|
193
193
|
import { MaybePromise } from "@durable-streams/client";
|
|
194
194
|
import "@sinclair/typebox";
|
|
195
195
|
import { AutoRouterType, IRequest } from "itty-router";
|
|
@@ -3442,6 +3442,7 @@ interface TypedSpawnRequest {
|
|
|
3442
3442
|
debounceMs?: number;
|
|
3443
3443
|
timeoutMs?: number;
|
|
3444
3444
|
includeResponse?: boolean;
|
|
3445
|
+
manifestKey?: string;
|
|
3445
3446
|
};
|
|
3446
3447
|
}
|
|
3447
3448
|
interface SendRequest {
|
|
@@ -4206,6 +4207,18 @@ declare class EntityManager {
|
|
|
4206
4207
|
}): Promise<{
|
|
4207
4208
|
txid: string;
|
|
4208
4209
|
}>;
|
|
4210
|
+
upsertEventSourceSubscription(entityUrl: string, req: {
|
|
4211
|
+
subscription: EventSourceSubscription$1;
|
|
4212
|
+
manifest: Record<string, unknown>;
|
|
4213
|
+
}): Promise<{
|
|
4214
|
+
txid: string;
|
|
4215
|
+
subscription: EventSourceSubscription$1;
|
|
4216
|
+
}>;
|
|
4217
|
+
deleteEventSourceSubscription(entityUrl: string, req: {
|
|
4218
|
+
id: string;
|
|
4219
|
+
}): Promise<{
|
|
4220
|
+
txid: string;
|
|
4221
|
+
}>;
|
|
4209
4222
|
/**
|
|
4210
4223
|
* Register a wake subscription from a subscriber to a source entity.
|
|
4211
4224
|
*/
|
|
@@ -4455,6 +4468,10 @@ declare function webhookSigningMetadata(signer: WebhookSigner, streamRootUrl: st
|
|
|
4455
4468
|
|
|
4456
4469
|
//#endregion
|
|
4457
4470
|
//#region src/routing/context.d.ts
|
|
4471
|
+
interface EventSourceCatalog {
|
|
4472
|
+
listEventSources: () => Array<EventSourceContract$1> | Promise<Array<EventSourceContract$1>>;
|
|
4473
|
+
getEventSource: (sourceKey: string) => EventSourceContract$1 | undefined | Promise<EventSourceContract$1 | undefined>;
|
|
4474
|
+
}
|
|
4458
4475
|
/**
|
|
4459
4476
|
* Per-request tenant context passed through every router and handler.
|
|
4460
4477
|
*
|
|
@@ -4481,6 +4498,8 @@ interface TenantContext {
|
|
|
4481
4498
|
streamClient: StreamClient;
|
|
4482
4499
|
runtime: ElectricAgentsTenantRuntime;
|
|
4483
4500
|
entityBridgeManager: EntityBridgeCoordinator;
|
|
4501
|
+
eventSources?: EventSourceCatalog;
|
|
4502
|
+
ensureEventSourceWakeSource?: (sourceUrl: string) => Promise<void> | void;
|
|
4484
4503
|
isShuttingDown: () => boolean;
|
|
4485
4504
|
}
|
|
4486
4505
|
|
|
@@ -4500,4 +4519,4 @@ declare class UnregisteredTenantError extends Error {
|
|
|
4500
4519
|
declare function isUnregisteredTenantError(error: unknown): error is UnregisteredTenantError;
|
|
4501
4520
|
|
|
4502
4521
|
//#endregion
|
|
4503
|
-
export { AgentsHost, AgentsHostOptions, AgentsHostTenantConfig, AgentsHostTenantRuntime, AuthenticateRequest, ConsumerClaim, DEFAULT_TENANT_ID, DispatchPolicy, DispatchTarget, DrizzleDB, DurableStreamsBearerProvider, DurableStreamsRoutingAdapter, DurableStreamsRoutingInput, Ed25519WebhookSignerOptions, ElectricAgentsEntity, ElectricAgentsEntityRow, ElectricAgentsEntityType, ElectricAgentsRunner, ElectricAgentsUser, EntityBridgeCoordinator, EntityDispatchState, EntityListFilter, EntitySignal, EntityStatus, GlobalRoutes, PgClient, Principal, PrincipalKind, PublicElectricAgentsEntity, PublicWakeNotification, RegisterEntityTypeRequest, RegisterRunnerRequest, RequestPrincipal, RunnerAdminStatus, RunnerHeartbeatRequest, RunnerKind, RunnerLiveness, SendRequest, SignalRequest, SignalResponse, SourceStreamOffset, StreamClient, StreamClientOptions, SubscriptionClaimResponse, SubscriptionCreateInput, SubscriptionResponse, SubscriptionStreamInfo, TenantContext, TypedSpawnRequest, UnregisteredTenantError, WakeNotificationRow, WebhookJwks, WebhookPublicJwk, WebhookSigner, WebhookSigningKeyInput, WebhookSigningMetadata, assertEntitySignal, assertEntityStatus, createDb, createEd25519WebhookSigner, expectedSignalStatus, getDefaultWebhookSigner, globalRouter, isTerminalEntityStatus, isUnregisteredTenantError, pathPrefixedSingleTenantDurableStreamsRoutingAdapter, rejectsNormalWrites, runMigrations, streamRootDurableStreamsRoutingAdapter, tenantRootDurableStreamsRoutingAdapter, toPublicEntity, webhookSigningMetadata };
|
|
4522
|
+
export { AgentsHost, AgentsHostOptions, AgentsHostTenantConfig, AgentsHostTenantRuntime, AuthenticateRequest, ConsumerClaim, DEFAULT_TENANT_ID, DispatchPolicy, DispatchTarget, DrizzleDB, DurableStreamsBearerProvider, DurableStreamsRoutingAdapter, DurableStreamsRoutingInput, Ed25519WebhookSignerOptions, ElectricAgentsEntity, ElectricAgentsEntityRow, ElectricAgentsEntityType, ElectricAgentsRunner, ElectricAgentsUser, EntityBridgeCoordinator, EntityDispatchState, EntityListFilter, EntitySignal, EntityStatus, EventSourceBucket, EventSourceCatalog, EventSourceContract, EventSourceFilter, EventSourceSubscription, EventSourceSubscriptionInput, GlobalRoutes, PgClient, Principal, PrincipalKind, PublicElectricAgentsEntity, PublicWakeNotification, RegisterEntityTypeRequest, RegisterRunnerRequest, RequestPrincipal, RunnerAdminStatus, RunnerHeartbeatRequest, RunnerKind, RunnerLiveness, SendRequest, SignalRequest, SignalResponse, SourceStreamOffset, StreamClient, StreamClientOptions, SubscriptionClaimResponse, SubscriptionCreateInput, SubscriptionLifetime, SubscriptionResponse, SubscriptionStreamInfo, TenantContext, TypedSpawnRequest, UnregisteredTenantError, WakeNotificationRow, WebhookJwks, WebhookPublicJwk, WebhookSigner, WebhookSigningKeyInput, WebhookSigningMetadata, assertEntitySignal, assertEntityStatus, createDb, createEd25519WebhookSigner, expectedSignalStatus, getDefaultWebhookSigner, globalRouter, isTerminalEntityStatus, isUnregisteredTenantError, pathPrefixedSingleTenantDurableStreamsRoutingAdapter, rejectsNormalWrites, runMigrations, streamRootDurableStreamsRoutingAdapter, tenantRootDurableStreamsRoutingAdapter, toPublicEntity, webhookSigningMetadata };
|
package/dist/index.js
CHANGED
|
@@ -8,7 +8,7 @@ import postgres from "postgres";
|
|
|
8
8
|
import { and, desc, eq, lt, ne, sql } from "drizzle-orm";
|
|
9
9
|
import { bigint, bigserial, boolean, check, index, integer, jsonb, pgTable, primaryKey, serial, text, timestamp, unique } from "drizzle-orm/pg-core";
|
|
10
10
|
import { createHash, createPrivateKey, createPublicKey, generateKeyPairSync, randomUUID, sign } from "node:crypto";
|
|
11
|
-
import { appendPathToUrl, assertTags, buildTagsIndex, entityStateSchema, getCronStreamPath, getCronStreamPathFromSpec, getEntitiesStreamPath, getNextCronFireAt, getSharedStateStreamPath, manifestChildKey, manifestSharedStateKey, manifestSourceKey, normalizeTags, parseCronStreamPath, resolveCronScheduleSpec, sourceRefForTags, verifyWebhookSignature } from "@electric-ax/agents-runtime";
|
|
11
|
+
import { appendPathToUrl, assertTags, buildEventSourceManifestEntry, buildTagsIndex, entityStateSchema, eventSourceSubscriptionManifestKey, getCronStreamPath, getCronStreamPathFromSpec, getEntitiesStreamPath, getNextCronFireAt, getSharedStateStreamPath, getWebhookStreamPath, manifestChildKey, manifestSharedStateKey, manifestSourceKey, normalizeTags, parseCronStreamPath, resolveCronScheduleSpec, resolveEventSourceSubscription, sourceRefForTags, verifyWebhookSignature } from "@electric-ax/agents-runtime";
|
|
12
12
|
import { DurableStream, DurableStreamError, FetchError, IdempotentProducer } from "@durable-streams/client";
|
|
13
13
|
import { ShapeStream, isChangeMessage, isControlMessage } from "@electric-sql/client";
|
|
14
14
|
import pino from "pino";
|
|
@@ -2624,6 +2624,10 @@ function extractManifestSourceUrl(manifest) {
|
|
|
2624
2624
|
}
|
|
2625
2625
|
if (manifest.sourceType === `entities`) return typeof manifest.sourceRef === `string` ? `/_entities/${manifest.sourceRef}` : void 0;
|
|
2626
2626
|
if (manifest.sourceType === `db`) return typeof manifest.sourceRef === `string` ? getSharedStateStreamPath(manifest.sourceRef) : void 0;
|
|
2627
|
+
if (manifest.sourceType === `webhook`) {
|
|
2628
|
+
if (typeof config?.streamUrl === `string`) return config.streamUrl;
|
|
2629
|
+
if (typeof config?.endpointKey === `string`) return getWebhookStreamPath(config.endpointKey, typeof config.bucket === `string` ? config.bucket : void 0);
|
|
2630
|
+
}
|
|
2627
2631
|
return void 0;
|
|
2628
2632
|
}
|
|
2629
2633
|
if (manifest.kind === `shared-state`) return typeof manifest.id === `string` ? getSharedStateStreamPath(manifest.id) : void 0;
|
|
@@ -2910,7 +2914,8 @@ var EntityManager = class {
|
|
|
2910
2914
|
debounceMs: req.wake.debounceMs,
|
|
2911
2915
|
timeoutMs: req.wake.timeoutMs,
|
|
2912
2916
|
oneShot: false,
|
|
2913
|
-
includeResponse: req.wake.includeResponse
|
|
2917
|
+
includeResponse: req.wake.includeResponse,
|
|
2918
|
+
manifestKey: req.wake.manifestKey
|
|
2914
2919
|
});
|
|
2915
2920
|
const contentType = `application/json`;
|
|
2916
2921
|
const createdEvent = entityStateSchema.entityCreated.insert({
|
|
@@ -3804,6 +3809,35 @@ var EntityManager = class {
|
|
|
3804
3809
|
await this.writeManifestEntry(entityUrl, manifestKey, `delete`, void 0, { txid });
|
|
3805
3810
|
return { txid };
|
|
3806
3811
|
}
|
|
3812
|
+
async upsertEventSourceSubscription(entityUrl, req) {
|
|
3813
|
+
const manifestKey = req.subscription.manifestKey;
|
|
3814
|
+
const txid = randomUUID();
|
|
3815
|
+
await this.writeManifestEntry(entityUrl, manifestKey, `upsert`, req.manifest, { txid });
|
|
3816
|
+
await this.wakeRegistry.unregisterByManifestKey(entityUrl, manifestKey, this.tenantId);
|
|
3817
|
+
await this.wakeRegistry.register({
|
|
3818
|
+
tenantId: this.tenantId,
|
|
3819
|
+
subscriberUrl: entityUrl,
|
|
3820
|
+
sourceUrl: req.subscription.sourceUrl,
|
|
3821
|
+
condition: {
|
|
3822
|
+
on: `change`,
|
|
3823
|
+
collections: [`webhook_event`],
|
|
3824
|
+
ops: [`insert`]
|
|
3825
|
+
},
|
|
3826
|
+
oneShot: false,
|
|
3827
|
+
manifestKey
|
|
3828
|
+
});
|
|
3829
|
+
return {
|
|
3830
|
+
txid,
|
|
3831
|
+
subscription: req.subscription
|
|
3832
|
+
};
|
|
3833
|
+
}
|
|
3834
|
+
async deleteEventSourceSubscription(entityUrl, req) {
|
|
3835
|
+
const manifestKey = eventSourceSubscriptionManifestKey(req.id);
|
|
3836
|
+
const txid = randomUUID();
|
|
3837
|
+
await this.writeManifestEntry(entityUrl, manifestKey, `delete`, void 0, { txid });
|
|
3838
|
+
await this.wakeRegistry.unregisterByManifestKey(entityUrl, manifestKey, this.tenantId);
|
|
3839
|
+
return { txid };
|
|
3840
|
+
}
|
|
3807
3841
|
/**
|
|
3808
3842
|
* Register a wake subscription from a subscriber to a source entity.
|
|
3809
3843
|
*/
|
|
@@ -6684,7 +6718,8 @@ const spawnBodySchema = Type.Object({
|
|
|
6684
6718
|
condition: wakeConditionSchema,
|
|
6685
6719
|
debounceMs: Type.Optional(Type.Number()),
|
|
6686
6720
|
timeoutMs: Type.Optional(Type.Number()),
|
|
6687
|
-
includeResponse: Type.Optional(Type.Boolean())
|
|
6721
|
+
includeResponse: Type.Optional(Type.Boolean()),
|
|
6722
|
+
manifestKey: Type.Optional(Type.String())
|
|
6688
6723
|
}))
|
|
6689
6724
|
});
|
|
6690
6725
|
const sendBodySchema = Type.Object({
|
|
@@ -6750,6 +6785,22 @@ const scheduleBodySchema = Type.Union([Type.Object({
|
|
|
6750
6785
|
messageType: Type.Optional(Type.String()),
|
|
6751
6786
|
from: Type.Optional(Type.String())
|
|
6752
6787
|
})]);
|
|
6788
|
+
const subscriptionLifetimeSchema = Type.Union([
|
|
6789
|
+
Type.Object({ kind: Type.Literal(`until_entity_stopped`) }),
|
|
6790
|
+
Type.Object({
|
|
6791
|
+
kind: Type.Literal(`expires_at`),
|
|
6792
|
+
at: Type.String()
|
|
6793
|
+
}),
|
|
6794
|
+
Type.Object({ kind: Type.Literal(`manual`) })
|
|
6795
|
+
]);
|
|
6796
|
+
const eventSourceSubscriptionBodySchema = Type.Object({
|
|
6797
|
+
sourceKey: Type.String(),
|
|
6798
|
+
bucketKey: Type.Optional(Type.String()),
|
|
6799
|
+
params: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
|
|
6800
|
+
filterKey: Type.Optional(Type.String()),
|
|
6801
|
+
lifetime: Type.Optional(subscriptionLifetimeSchema),
|
|
6802
|
+
reason: Type.Optional(Type.String())
|
|
6803
|
+
});
|
|
6753
6804
|
const entitiesRegisterBodySchema = Type.Object({ tags: Type.Optional(stringRecordSchema) });
|
|
6754
6805
|
const entitiesRouter = Router({ base: `/_electric/entities` });
|
|
6755
6806
|
entitiesRouter.get(`/`, listEntities);
|
|
@@ -6767,6 +6818,8 @@ entitiesRouter.post(`/:type/:instanceId/tags/:tagKey`, withExistingEntity, withS
|
|
|
6767
6818
|
entitiesRouter.delete(`/:type/:instanceId/tags/:tagKey`, withExistingEntity, removeTag);
|
|
6768
6819
|
entitiesRouter.put(`/:type/:instanceId/schedules/:scheduleId`, withExistingEntity, withSchema(scheduleBodySchema), upsertSchedule);
|
|
6769
6820
|
entitiesRouter.delete(`/:type/:instanceId/schedules/:scheduleId`, withExistingEntity, deleteSchedule);
|
|
6821
|
+
entitiesRouter.put(`/:type/:instanceId/event-source-subscriptions/:subscriptionId`, withExistingEntity, withSchema(eventSourceSubscriptionBodySchema), upsertEventSourceSubscription);
|
|
6822
|
+
entitiesRouter.delete(`/:type/:instanceId/event-source-subscriptions/:subscriptionId`, withExistingEntity, deleteEventSourceSubscription);
|
|
6770
6823
|
function entityUrlFromSegments(type, instanceId) {
|
|
6771
6824
|
if (!type || !instanceId) return null;
|
|
6772
6825
|
if (type.startsWith(`_`) || type.includes(`*`) || instanceId.includes(`*`)) return null;
|
|
@@ -6873,6 +6926,47 @@ async function deleteSchedule(request, ctx) {
|
|
|
6873
6926
|
const result = await ctx.entityManager.deleteSchedule(entityUrl, { id: decodeURIComponent(request.params.scheduleId) });
|
|
6874
6927
|
return json(result);
|
|
6875
6928
|
}
|
|
6929
|
+
async function upsertEventSourceSubscription(request, ctx) {
|
|
6930
|
+
const principalMutationError = rejectPrincipalEntityMutation(request, `subscribed to event sources`);
|
|
6931
|
+
if (principalMutationError) return principalMutationError;
|
|
6932
|
+
const catalog = ctx.eventSources;
|
|
6933
|
+
if (!catalog) return apiError(404, ErrCodeNotFound, `No event source catalog is configured`);
|
|
6934
|
+
const { entityUrl } = requireExistingEntityRoute(request);
|
|
6935
|
+
const parsed = routeBody(request);
|
|
6936
|
+
const source = await catalog.getEventSource(parsed.sourceKey);
|
|
6937
|
+
if (!source) return apiError(404, ErrCodeNotFound, `Event source "${parsed.sourceKey}" not found`);
|
|
6938
|
+
if (parsed.lifetime?.kind === `expires_at`) {
|
|
6939
|
+
const expiresAt = new Date(parsed.lifetime.at);
|
|
6940
|
+
if (Number.isNaN(expiresAt.getTime())) return apiError(400, ErrCodeInvalidRequest, `Invalid expires_at lifetime timestamp`);
|
|
6941
|
+
}
|
|
6942
|
+
let resolved;
|
|
6943
|
+
try {
|
|
6944
|
+
resolved = resolveEventSourceSubscription({
|
|
6945
|
+
contract: source,
|
|
6946
|
+
entityUrl,
|
|
6947
|
+
request: {
|
|
6948
|
+
...parsed,
|
|
6949
|
+
id: decodeURIComponent(request.params.subscriptionId)
|
|
6950
|
+
},
|
|
6951
|
+
createdBy: `tool`
|
|
6952
|
+
});
|
|
6953
|
+
} catch (error) {
|
|
6954
|
+
return apiError(400, ErrCodeInvalidRequest, error instanceof Error ? error.message : String(error));
|
|
6955
|
+
}
|
|
6956
|
+
await ctx.ensureEventSourceWakeSource?.(resolved.subscription.sourceUrl);
|
|
6957
|
+
const result = await ctx.entityManager.upsertEventSourceSubscription(entityUrl, {
|
|
6958
|
+
subscription: resolved.subscription,
|
|
6959
|
+
manifest: buildEventSourceManifestEntry(resolved)
|
|
6960
|
+
});
|
|
6961
|
+
return json(result);
|
|
6962
|
+
}
|
|
6963
|
+
async function deleteEventSourceSubscription(request, ctx) {
|
|
6964
|
+
const principalMutationError = rejectPrincipalEntityMutation(request, `unsubscribed from event sources`);
|
|
6965
|
+
if (principalMutationError) return principalMutationError;
|
|
6966
|
+
const { entityUrl } = requireExistingEntityRoute(request);
|
|
6967
|
+
const result = await ctx.entityManager.deleteEventSourceSubscription(entityUrl, { id: decodeURIComponent(request.params.subscriptionId) });
|
|
6968
|
+
return json(result);
|
|
6969
|
+
}
|
|
6876
6970
|
async function setTag(request, ctx) {
|
|
6877
6971
|
const principalMutationError = rejectPrincipalEntityMutation(request, `tagged`);
|
|
6878
6972
|
if (principalMutationError) return principalMutationError;
|
|
@@ -7587,6 +7681,7 @@ const callbackForwardBodySchema = Type.Object({
|
|
|
7587
7681
|
const DS_SUBSCRIPTION_CALLBACK_PREFIX = `ds-subscription:`;
|
|
7588
7682
|
const internalRouter = Router({ base: `/_electric` });
|
|
7589
7683
|
internalRouter.get(`/health`, () => json({ status: `ok` }));
|
|
7684
|
+
internalRouter.get(`/event-sources`, listEventSources);
|
|
7590
7685
|
internalRouter.post(`/wake`, withSchema(wakeRegistrationBodySchema), registerWake);
|
|
7591
7686
|
internalRouter.post(`/webhook-forward/:subscriptionId`, webhookForward);
|
|
7592
7687
|
internalRouter.post(`/callback-forward/:consumerId`, callbackForward);
|
|
@@ -7690,6 +7785,13 @@ async function registerWake(request, ctx) {
|
|
|
7690
7785
|
await ctx.entityManager.registerWake(opts);
|
|
7691
7786
|
return status(204);
|
|
7692
7787
|
}
|
|
7788
|
+
async function listEventSources(_request, ctx) {
|
|
7789
|
+
const eventSources = ctx.eventSources ? await ctx.eventSources.listEventSources() : [];
|
|
7790
|
+
return json({ eventSources: eventSources.filter(isAgentVisibleEventSource) });
|
|
7791
|
+
}
|
|
7792
|
+
function isAgentVisibleEventSource(source) {
|
|
7793
|
+
return source.agentVisible === true && source.status === `active`;
|
|
7794
|
+
}
|
|
7693
7795
|
async function webhookForward(request, ctx) {
|
|
7694
7796
|
const subscriptionId = routeParam(request, `subscriptionId`);
|
|
7695
7797
|
const rootSpan = getRequestSpan(request);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@electric-ax/agents-server",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.9",
|
|
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@5d5c217",
|
|
40
40
|
"@durable-streams/server": "https://pkg.pr.new/durable-streams/durable-streams/@durable-streams/server@eac712f",
|
|
41
41
|
"@durable-streams/state": "https://pkg.pr.new/durable-streams/durable-streams/@durable-streams/state@5d5c217",
|
|
42
|
-
"@electric-sql/client": "^1.5.
|
|
42
|
+
"@electric-sql/client": "^1.5.19",
|
|
43
43
|
"@mariozechner/pi-agent-core": "^0.70.2",
|
|
44
44
|
"@opentelemetry/api": "^1.9.1",
|
|
45
45
|
"@sinclair/typebox": "^0.34.48",
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"pino-pretty": "^13.0.0",
|
|
55
55
|
"postgres": "^3.4.0",
|
|
56
56
|
"undici": "^7.24.7",
|
|
57
|
-
"@electric-ax/agents-runtime": "0.3.
|
|
57
|
+
"@electric-ax/agents-runtime": "0.3.4"
|
|
58
58
|
},
|
|
59
59
|
"devDependencies": {
|
|
60
60
|
"@types/node": "^22.19.15",
|
|
@@ -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.8",
|
|
68
69
|
"@electric-ax/agents-server-conformance-tests": "0.1.7",
|
|
69
|
-
"@electric-ax/agents": "0.4.
|
|
70
|
-
"@electric-ax/agents-server-ui": "0.4.7"
|
|
70
|
+
"@electric-ax/agents-server-ui": "0.4.9"
|
|
71
71
|
},
|
|
72
72
|
"files": [
|
|
73
73
|
"dist",
|
package/src/entity-manager.ts
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
getCronStreamPath,
|
|
7
7
|
getSharedStateStreamPath,
|
|
8
8
|
getNextCronFireAt,
|
|
9
|
+
eventSourceSubscriptionManifestKey,
|
|
9
10
|
manifestChildKey,
|
|
10
11
|
manifestSharedStateKey,
|
|
11
12
|
manifestSourceKey,
|
|
@@ -50,6 +51,7 @@ import type { queueAsPromised } from 'fastq'
|
|
|
50
51
|
import type { SchedulerClient } from './scheduler.js'
|
|
51
52
|
import type { WakeEvalResult, WakeRegistry } from './wake-registry.js'
|
|
52
53
|
import type { WakeMessage } from '@electric-ax/agents-runtime'
|
|
54
|
+
import type { EventSourceSubscription } from '@electric-ax/agents-runtime'
|
|
53
55
|
import type { PostgresRegistry } from './entity-registry.js'
|
|
54
56
|
import type { SchemaValidator } from './electric-agents/schema-validator.js'
|
|
55
57
|
import type { StreamClient } from './stream-client.js'
|
|
@@ -554,6 +556,7 @@ export class EntityManager {
|
|
|
554
556
|
timeoutMs: req.wake.timeoutMs,
|
|
555
557
|
oneShot: false,
|
|
556
558
|
includeResponse: req.wake.includeResponse,
|
|
559
|
+
manifestKey: req.wake.manifestKey,
|
|
557
560
|
})
|
|
558
561
|
}
|
|
559
562
|
|
|
@@ -2162,6 +2165,67 @@ export class EntityManager {
|
|
|
2162
2165
|
return { txid }
|
|
2163
2166
|
}
|
|
2164
2167
|
|
|
2168
|
+
async upsertEventSourceSubscription(
|
|
2169
|
+
entityUrl: string,
|
|
2170
|
+
req: {
|
|
2171
|
+
subscription: EventSourceSubscription
|
|
2172
|
+
manifest: Record<string, unknown>
|
|
2173
|
+
}
|
|
2174
|
+
): Promise<{ txid: string; subscription: EventSourceSubscription }> {
|
|
2175
|
+
const manifestKey = req.subscription.manifestKey
|
|
2176
|
+
const txid = randomUUID()
|
|
2177
|
+
await this.writeManifestEntry(
|
|
2178
|
+
entityUrl,
|
|
2179
|
+
manifestKey,
|
|
2180
|
+
`upsert`,
|
|
2181
|
+
req.manifest,
|
|
2182
|
+
{
|
|
2183
|
+
txid,
|
|
2184
|
+
}
|
|
2185
|
+
)
|
|
2186
|
+
|
|
2187
|
+
// The manifest is the durable source of truth. Register side effects after
|
|
2188
|
+
// it is appended so failures can be repaired by manifest replay.
|
|
2189
|
+
await this.wakeRegistry.unregisterByManifestKey(
|
|
2190
|
+
entityUrl,
|
|
2191
|
+
manifestKey,
|
|
2192
|
+
this.tenantId
|
|
2193
|
+
)
|
|
2194
|
+
await this.wakeRegistry.register({
|
|
2195
|
+
tenantId: this.tenantId,
|
|
2196
|
+
subscriberUrl: entityUrl,
|
|
2197
|
+
sourceUrl: req.subscription.sourceUrl,
|
|
2198
|
+
condition: {
|
|
2199
|
+
on: `change`,
|
|
2200
|
+
collections: [`webhook_event`],
|
|
2201
|
+
ops: [`insert`],
|
|
2202
|
+
},
|
|
2203
|
+
oneShot: false,
|
|
2204
|
+
manifestKey,
|
|
2205
|
+
})
|
|
2206
|
+
|
|
2207
|
+
return { txid, subscription: req.subscription }
|
|
2208
|
+
}
|
|
2209
|
+
|
|
2210
|
+
async deleteEventSourceSubscription(
|
|
2211
|
+
entityUrl: string,
|
|
2212
|
+
req: { id: string }
|
|
2213
|
+
): Promise<{ txid: string }> {
|
|
2214
|
+
const manifestKey = eventSourceSubscriptionManifestKey(req.id)
|
|
2215
|
+
const txid = randomUUID()
|
|
2216
|
+
await this.writeManifestEntry(entityUrl, manifestKey, `delete`, undefined, {
|
|
2217
|
+
txid,
|
|
2218
|
+
})
|
|
2219
|
+
|
|
2220
|
+
await this.wakeRegistry.unregisterByManifestKey(
|
|
2221
|
+
entityUrl,
|
|
2222
|
+
manifestKey,
|
|
2223
|
+
this.tenantId
|
|
2224
|
+
)
|
|
2225
|
+
|
|
2226
|
+
return { txid }
|
|
2227
|
+
}
|
|
2228
|
+
|
|
2165
2229
|
// ==========================================================================
|
|
2166
2230
|
// Wake Evaluation
|
|
2167
2231
|
// ==========================================================================
|
package/src/index.ts
CHANGED
|
@@ -53,10 +53,18 @@ export type {
|
|
|
53
53
|
SignalResponse,
|
|
54
54
|
TypedSpawnRequest,
|
|
55
55
|
} from './electric-agents-types.js'
|
|
56
|
+
export type {
|
|
57
|
+
EventSourceBucket,
|
|
58
|
+
EventSourceContract,
|
|
59
|
+
EventSourceFilter,
|
|
60
|
+
EventSourceSubscription,
|
|
61
|
+
EventSourceSubscriptionInput,
|
|
62
|
+
SubscriptionLifetime,
|
|
63
|
+
} from '@electric-ax/agents-runtime'
|
|
56
64
|
export type { Principal, PrincipalKind } from './principal.js'
|
|
57
65
|
export { globalRouter } from './routing/global-router.js'
|
|
58
66
|
export type { GlobalRoutes } from './routing/global-router.js'
|
|
59
|
-
export type { TenantContext } from './routing/context.js'
|
|
67
|
+
export type { EventSourceCatalog, TenantContext } from './routing/context.js'
|
|
60
68
|
export {
|
|
61
69
|
streamRootDurableStreamsRoutingAdapter,
|
|
62
70
|
pathPrefixedSingleTenantDurableStreamsRoutingAdapter,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
getCronStreamPathFromSpec,
|
|
3
|
+
getWebhookStreamPath,
|
|
3
4
|
getSharedStateStreamPath,
|
|
4
5
|
resolveCronScheduleSpec,
|
|
5
6
|
} from '@electric-ax/agents-runtime'
|
|
@@ -56,6 +57,16 @@ export function extractManifestSourceUrl(
|
|
|
56
57
|
: undefined
|
|
57
58
|
}
|
|
58
59
|
|
|
60
|
+
if (manifest.sourceType === `webhook`) {
|
|
61
|
+
if (typeof config?.streamUrl === `string`) return config.streamUrl
|
|
62
|
+
if (typeof config?.endpointKey === `string`) {
|
|
63
|
+
return getWebhookStreamPath(
|
|
64
|
+
config.endpointKey,
|
|
65
|
+
typeof config.bucket === `string` ? config.bucket : undefined
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
59
70
|
return undefined
|
|
60
71
|
}
|
|
61
72
|
|
package/src/routing/context.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import type { Agent } from 'undici'
|
|
2
|
-
import type {
|
|
2
|
+
import type {
|
|
3
|
+
EventSourceContract,
|
|
4
|
+
WebhookSignatureVerifierConfig,
|
|
5
|
+
} from '@electric-ax/agents-runtime'
|
|
3
6
|
import type { DrizzleDB } from '../db/index.js'
|
|
4
7
|
import type { EntityBridgeCoordinator } from '../entity-bridge-manager.js'
|
|
5
8
|
import type { EntityManager } from '../entity-manager.js'
|
|
@@ -10,6 +13,18 @@ import type { Principal } from '../principal.js'
|
|
|
10
13
|
import type { DurableStreamsBearerProvider } from '../stream-client.js'
|
|
11
14
|
import type { WebhookSigner } from '../webhook-signing.js'
|
|
12
15
|
|
|
16
|
+
export interface EventSourceCatalog {
|
|
17
|
+
listEventSources: () =>
|
|
18
|
+
| Array<EventSourceContract>
|
|
19
|
+
| Promise<Array<EventSourceContract>>
|
|
20
|
+
getEventSource: (
|
|
21
|
+
sourceKey: string
|
|
22
|
+
) =>
|
|
23
|
+
| EventSourceContract
|
|
24
|
+
| undefined
|
|
25
|
+
| Promise<EventSourceContract | undefined>
|
|
26
|
+
}
|
|
27
|
+
|
|
13
28
|
/**
|
|
14
29
|
* Per-request tenant context passed through every router and handler.
|
|
15
30
|
*
|
|
@@ -38,5 +53,7 @@ export interface TenantContext {
|
|
|
38
53
|
streamClient: StreamClient
|
|
39
54
|
runtime: ElectricAgentsTenantRuntime
|
|
40
55
|
entityBridgeManager: EntityBridgeCoordinator
|
|
56
|
+
eventSources?: EventSourceCatalog
|
|
57
|
+
ensureEventSourceWakeSource?: (sourceUrl: string) => Promise<void> | void
|
|
41
58
|
isShuttingDown: () => boolean
|
|
42
59
|
}
|
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { Type, type Static } from '@sinclair/typebox'
|
|
6
|
+
import {
|
|
7
|
+
buildEventSourceManifestEntry,
|
|
8
|
+
resolveEventSourceSubscription,
|
|
9
|
+
} from '@electric-ax/agents-runtime'
|
|
6
10
|
import { Router, json, status } from 'itty-router'
|
|
7
11
|
import { apiError } from '../electric-agents-http.js'
|
|
8
12
|
import { parsePrincipalKey, principalUrl } from '../principal.js'
|
|
@@ -26,6 +30,7 @@ import type { ElectricAgentsEntity } from '../electric-agents-types.js'
|
|
|
26
30
|
import type { JsonRouteRequest } from './schema.js'
|
|
27
31
|
import type { RouterType } from 'itty-router'
|
|
28
32
|
import type { TenantContext } from './context.js'
|
|
33
|
+
import type { EventSourceSubscriptionInput } from '@electric-ax/agents-runtime'
|
|
29
34
|
|
|
30
35
|
interface AgentsRouteRequest extends JsonRouteRequest {
|
|
31
36
|
entityRoute?: ExistingEntityRoute
|
|
@@ -84,6 +89,7 @@ const spawnBodySchema = Type.Object({
|
|
|
84
89
|
debounceMs: Type.Optional(Type.Number()),
|
|
85
90
|
timeoutMs: Type.Optional(Type.Number()),
|
|
86
91
|
includeResponse: Type.Optional(Type.Boolean()),
|
|
92
|
+
manifestKey: Type.Optional(Type.String()),
|
|
87
93
|
})
|
|
88
94
|
),
|
|
89
95
|
})
|
|
@@ -169,6 +175,24 @@ const scheduleBodySchema = Type.Union([
|
|
|
169
175
|
}),
|
|
170
176
|
])
|
|
171
177
|
|
|
178
|
+
const subscriptionLifetimeSchema = Type.Union([
|
|
179
|
+
Type.Object({ kind: Type.Literal(`until_entity_stopped`) }),
|
|
180
|
+
Type.Object({
|
|
181
|
+
kind: Type.Literal(`expires_at`),
|
|
182
|
+
at: Type.String(),
|
|
183
|
+
}),
|
|
184
|
+
Type.Object({ kind: Type.Literal(`manual`) }),
|
|
185
|
+
])
|
|
186
|
+
|
|
187
|
+
const eventSourceSubscriptionBodySchema = Type.Object({
|
|
188
|
+
sourceKey: Type.String(),
|
|
189
|
+
bucketKey: Type.Optional(Type.String()),
|
|
190
|
+
params: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
|
|
191
|
+
filterKey: Type.Optional(Type.String()),
|
|
192
|
+
lifetime: Type.Optional(subscriptionLifetimeSchema),
|
|
193
|
+
reason: Type.Optional(Type.String()),
|
|
194
|
+
})
|
|
195
|
+
|
|
172
196
|
const entitiesRegisterBodySchema = Type.Object({
|
|
173
197
|
tags: Type.Optional(stringRecordSchema),
|
|
174
198
|
})
|
|
@@ -180,6 +204,9 @@ type ForkBody = Static<typeof forkBodySchema>
|
|
|
180
204
|
type SetTagBody = Static<typeof setTagBodySchema>
|
|
181
205
|
type SignalBody = Static<typeof signalBodySchema>
|
|
182
206
|
type ScheduleBody = Static<typeof scheduleBodySchema>
|
|
207
|
+
type EventSourceSubscriptionBody = Static<
|
|
208
|
+
typeof eventSourceSubscriptionBodySchema
|
|
209
|
+
>
|
|
183
210
|
type EntitiesRegisterBody = Static<typeof entitiesRegisterBodySchema>
|
|
184
211
|
|
|
185
212
|
export const entitiesRouter: EntitiesRoutes = Router<
|
|
@@ -256,6 +283,17 @@ entitiesRouter.delete(
|
|
|
256
283
|
withExistingEntity,
|
|
257
284
|
deleteSchedule
|
|
258
285
|
)
|
|
286
|
+
entitiesRouter.put(
|
|
287
|
+
`/:type/:instanceId/event-source-subscriptions/:subscriptionId`,
|
|
288
|
+
withExistingEntity,
|
|
289
|
+
withSchema(eventSourceSubscriptionBodySchema),
|
|
290
|
+
upsertEventSourceSubscription
|
|
291
|
+
)
|
|
292
|
+
entitiesRouter.delete(
|
|
293
|
+
`/:type/:instanceId/event-source-subscriptions/:subscriptionId`,
|
|
294
|
+
withExistingEntity,
|
|
295
|
+
deleteEventSourceSubscription
|
|
296
|
+
)
|
|
259
297
|
|
|
260
298
|
function entityUrlFromSegments(
|
|
261
299
|
type: string,
|
|
@@ -467,6 +505,98 @@ async function deleteSchedule(
|
|
|
467
505
|
return json(result)
|
|
468
506
|
}
|
|
469
507
|
|
|
508
|
+
async function upsertEventSourceSubscription(
|
|
509
|
+
request: AgentsRouteRequest,
|
|
510
|
+
ctx: TenantContext
|
|
511
|
+
): Promise<Response> {
|
|
512
|
+
const principalMutationError = rejectPrincipalEntityMutation(
|
|
513
|
+
request,
|
|
514
|
+
`subscribed to event sources`
|
|
515
|
+
)
|
|
516
|
+
if (principalMutationError) return principalMutationError
|
|
517
|
+
|
|
518
|
+
const catalog = ctx.eventSources
|
|
519
|
+
if (!catalog) {
|
|
520
|
+
return apiError(
|
|
521
|
+
404,
|
|
522
|
+
ErrCodeNotFound,
|
|
523
|
+
`No event source catalog is configured`
|
|
524
|
+
)
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
const { entityUrl } = requireExistingEntityRoute(request)
|
|
528
|
+
const parsed = routeBody<EventSourceSubscriptionBody>(request)
|
|
529
|
+
const source = await catalog.getEventSource(parsed.sourceKey)
|
|
530
|
+
if (!source) {
|
|
531
|
+
return apiError(
|
|
532
|
+
404,
|
|
533
|
+
ErrCodeNotFound,
|
|
534
|
+
`Event source "${parsed.sourceKey}" not found`
|
|
535
|
+
)
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
if (parsed.lifetime?.kind === `expires_at`) {
|
|
539
|
+
const expiresAt = new Date(parsed.lifetime.at)
|
|
540
|
+
if (Number.isNaN(expiresAt.getTime())) {
|
|
541
|
+
return apiError(
|
|
542
|
+
400,
|
|
543
|
+
ErrCodeInvalidRequest,
|
|
544
|
+
`Invalid expires_at lifetime timestamp`
|
|
545
|
+
)
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
let resolved: ReturnType<typeof resolveEventSourceSubscription>
|
|
550
|
+
try {
|
|
551
|
+
resolved = resolveEventSourceSubscription({
|
|
552
|
+
contract: source,
|
|
553
|
+
entityUrl,
|
|
554
|
+
request: {
|
|
555
|
+
...(parsed as EventSourceSubscriptionInput),
|
|
556
|
+
id: decodeURIComponent(request.params.subscriptionId),
|
|
557
|
+
},
|
|
558
|
+
createdBy: `tool`,
|
|
559
|
+
})
|
|
560
|
+
} catch (error) {
|
|
561
|
+
return apiError(
|
|
562
|
+
400,
|
|
563
|
+
ErrCodeInvalidRequest,
|
|
564
|
+
error instanceof Error ? error.message : String(error)
|
|
565
|
+
)
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
await ctx.ensureEventSourceWakeSource?.(resolved.subscription.sourceUrl)
|
|
569
|
+
|
|
570
|
+
const result = await ctx.entityManager.upsertEventSourceSubscription(
|
|
571
|
+
entityUrl,
|
|
572
|
+
{
|
|
573
|
+
subscription: resolved.subscription,
|
|
574
|
+
manifest: buildEventSourceManifestEntry(resolved),
|
|
575
|
+
}
|
|
576
|
+
)
|
|
577
|
+
return json(result)
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
async function deleteEventSourceSubscription(
|
|
581
|
+
request: AgentsRouteRequest,
|
|
582
|
+
ctx: TenantContext
|
|
583
|
+
): Promise<Response> {
|
|
584
|
+
const principalMutationError = rejectPrincipalEntityMutation(
|
|
585
|
+
request,
|
|
586
|
+
`unsubscribed from event sources`
|
|
587
|
+
)
|
|
588
|
+
if (principalMutationError) return principalMutationError
|
|
589
|
+
|
|
590
|
+
const { entityUrl } = requireExistingEntityRoute(request)
|
|
591
|
+
const result = await ctx.entityManager.deleteEventSourceSubscription(
|
|
592
|
+
entityUrl,
|
|
593
|
+
{
|
|
594
|
+
id: decodeURIComponent(request.params.subscriptionId),
|
|
595
|
+
}
|
|
596
|
+
)
|
|
597
|
+
return json(result)
|
|
598
|
+
}
|
|
599
|
+
|
|
470
600
|
async function setTag(
|
|
471
601
|
request: AgentsRouteRequest,
|
|
472
602
|
ctx: TenantContext
|
|
@@ -36,7 +36,10 @@ import { runnersRouter } from './runners-router.js'
|
|
|
36
36
|
import { routeBody, validateOptionalJsonBody, withSchema } from './schema.js'
|
|
37
37
|
import { withLeadingSlash } from './tenant-stream-paths.js'
|
|
38
38
|
import type { IRequest, RouterType } from 'itty-router'
|
|
39
|
-
import type {
|
|
39
|
+
import type {
|
|
40
|
+
EventSourceContract,
|
|
41
|
+
WebhookSignatureVerifierConfig,
|
|
42
|
+
} from '@electric-ax/agents-runtime'
|
|
40
43
|
import type { TenantContext } from './context.js'
|
|
41
44
|
import type { DurableStreamsRoutingAdapter } from './durable-streams-routing-adapter.js'
|
|
42
45
|
import type { WebhookSigner } from '../webhook-signing.js'
|
|
@@ -117,6 +120,7 @@ export const internalRouter: InternalRoutes = Router<
|
|
|
117
120
|
})
|
|
118
121
|
|
|
119
122
|
internalRouter.get(`/health`, () => json({ status: `ok` }))
|
|
123
|
+
internalRouter.get(`/event-sources`, listEventSources)
|
|
120
124
|
internalRouter.post(
|
|
121
125
|
`/wake`,
|
|
122
126
|
withSchema(wakeRegistrationBodySchema),
|
|
@@ -335,6 +339,20 @@ async function registerWake(
|
|
|
335
339
|
return status(204)
|
|
336
340
|
}
|
|
337
341
|
|
|
342
|
+
async function listEventSources(
|
|
343
|
+
_request: IRequest,
|
|
344
|
+
ctx: TenantContext
|
|
345
|
+
): Promise<Response> {
|
|
346
|
+
const eventSources = ctx.eventSources
|
|
347
|
+
? await ctx.eventSources.listEventSources()
|
|
348
|
+
: []
|
|
349
|
+
return json({ eventSources: eventSources.filter(isAgentVisibleEventSource) })
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function isAgentVisibleEventSource(source: EventSourceContract): boolean {
|
|
353
|
+
return source.agentVisible === true && source.status === `active`
|
|
354
|
+
}
|
|
355
|
+
|
|
338
356
|
async function webhookForward(
|
|
339
357
|
request: IRequest,
|
|
340
358
|
ctx: TenantContext
|
package/src/server.ts
CHANGED
|
@@ -34,6 +34,7 @@ import type { Principal } from './principal.js'
|
|
|
34
34
|
import type { EntityBridgeCoordinator } from './entity-bridge-manager.js'
|
|
35
35
|
import type { DurableStreamsRoutingAdapter } from './routing/durable-streams-routing-adapter.js'
|
|
36
36
|
import type { OssServerContext } from './routing/oss-server-router.js'
|
|
37
|
+
import type { EventSourceCatalog } from './routing/context.js'
|
|
37
38
|
import type { StartedStandaloneAgentsRuntime } from './standalone-runtime.js'
|
|
38
39
|
import type { DurableStreamsBearerProvider } from './stream-client.js'
|
|
39
40
|
import type {
|
|
@@ -67,6 +68,8 @@ export interface ElectricAgentsServerOptions {
|
|
|
67
68
|
request: Request
|
|
68
69
|
) => Promise<Principal | null> | Principal | null
|
|
69
70
|
allowDevPrincipalFallback?: boolean
|
|
71
|
+
eventSources?: EventSourceCatalog
|
|
72
|
+
ensureEventSourceWakeSource?: (sourceUrl: string) => Promise<void> | void
|
|
70
73
|
/**
|
|
71
74
|
* Disabled by default. When set to a positive interval, periodically
|
|
72
75
|
* recovers expired dispatch claims and stale outstanding wakes.
|
|
@@ -441,6 +444,15 @@ export class ElectricAgentsServer {
|
|
|
441
444
|
streamClient: this.streamClient,
|
|
442
445
|
runtime: this.standaloneRuntime.runtime,
|
|
443
446
|
entityBridgeManager: this.entityBridgeManager,
|
|
447
|
+
...(this.options.eventSources
|
|
448
|
+
? { eventSources: this.options.eventSources }
|
|
449
|
+
: {}),
|
|
450
|
+
...(this.options.ensureEventSourceWakeSource
|
|
451
|
+
? {
|
|
452
|
+
ensureEventSourceWakeSource:
|
|
453
|
+
this.options.ensureEventSourceWakeSource,
|
|
454
|
+
}
|
|
455
|
+
: {}),
|
|
444
456
|
isShuttingDown: () => this.shuttingDown,
|
|
445
457
|
mockAgent: this.mockAgentBootstrap
|
|
446
458
|
? { runtime: this.mockAgentBootstrap.runtime }
|