@electric-ax/agents-server 0.4.9 → 0.4.12
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 +91 -80
- package/dist/index.cjs +91 -80
- package/dist/index.d.cts +5 -5
- package/dist/index.d.ts +5 -5
- package/dist/index.js +91 -80
- package/package.json +5 -5
- package/src/electric-agents/default-entity-schemas.ts +1 -1
- package/src/electric-agents-types.ts +1 -1
- package/src/entity-manager.ts +5 -5
- package/src/routing/dispatch-policy.ts +1 -1
- package/src/routing/durable-streams-router.ts +1 -1
- package/src/routing/durable-streams-routing-adapter.ts +1 -3
- package/src/routing/entities-router.ts +5 -26
- package/src/routing/entity-types-router.ts +23 -26
- package/src/routing/hooks.ts +1 -1
- package/src/routing/internal-router.ts +64 -37
- package/src/routing/observations-router.ts +74 -0
- package/src/routing/runners-router.ts +1 -1
- package/src/wake-registry.ts +1 -1
- package/src/routing/cron-router.ts +0 -45
package/dist/entrypoint.js
CHANGED
|
@@ -446,7 +446,7 @@ const ErrCodeForkWaitTimeout = `FORK_WAIT_TIMEOUT`;
|
|
|
446
446
|
const ErrCodeEntityPersistFailed = `ENTITY_PERSIST_FAILED`;
|
|
447
447
|
const ErrCodeAgentUiNotFound = `AGENT_UI_NOT_FOUND`;
|
|
448
448
|
const ErrCodeSubscriptionNotFound = `SUBSCRIPTION_NOT_FOUND`;
|
|
449
|
-
const
|
|
449
|
+
const ErrCodeWakeCallbackNotFound = `WAKE_CALLBACK_NOT_FOUND`;
|
|
450
450
|
|
|
451
451
|
//#endregion
|
|
452
452
|
//#region src/utils/electric-url.ts
|
|
@@ -466,7 +466,7 @@ function electricUrlWithPath(electricUrl, path$1) {
|
|
|
466
466
|
//#region src/routing/durable-streams-routing-adapter.ts
|
|
467
467
|
function appendSearch(target, source) {
|
|
468
468
|
source.searchParams.forEach((value, key) => {
|
|
469
|
-
|
|
469
|
+
target.searchParams.append(key, value);
|
|
470
470
|
});
|
|
471
471
|
return target;
|
|
472
472
|
}
|
|
@@ -1610,7 +1610,7 @@ async function rewriteSubscriptionRequestBody(request, ctx, subscriptionId, rout
|
|
|
1610
1610
|
let targetWebhookUrl = null;
|
|
1611
1611
|
if (payload.webhook?.url !== void 0) {
|
|
1612
1612
|
targetWebhookUrl = rewriteLoopbackWebhookUrl(payload.webhook.url) ?? null;
|
|
1613
|
-
payload.webhook.url = appendPathToUrl(ctx.publicUrl, `/_electric/
|
|
1613
|
+
payload.webhook.url = appendPathToUrl(ctx.publicUrl, `/_electric/subscription-webhooks/${encodeURIComponent(subscriptionId)}`);
|
|
1614
1614
|
}
|
|
1615
1615
|
rewriteSubscriptionBodyForBackend(payload, ctx.service, routingAdapter);
|
|
1616
1616
|
return {
|
|
@@ -1733,20 +1733,6 @@ async function proxyPassThrough(request, ctx) {
|
|
|
1733
1733
|
}
|
|
1734
1734
|
}
|
|
1735
1735
|
|
|
1736
|
-
//#endregion
|
|
1737
|
-
//#region src/routing/cron-router.ts
|
|
1738
|
-
const cronRegisterBodySchema = Type.Object({
|
|
1739
|
-
expression: Type.String(),
|
|
1740
|
-
timezone: Type.Optional(Type.String())
|
|
1741
|
-
});
|
|
1742
|
-
const cronRouter = Router({ base: `/_electric/cron` });
|
|
1743
|
-
cronRouter.post(`/register`, withSchema(cronRegisterBodySchema), registerCron);
|
|
1744
|
-
async function registerCron(request, ctx) {
|
|
1745
|
-
const parsed = routeBody(request);
|
|
1746
|
-
const streamPath = await ctx.entityManager.getOrCreateCronStream(parsed.expression, parsed.timezone);
|
|
1747
|
-
return json({ streamUrl: streamPath });
|
|
1748
|
-
}
|
|
1749
|
-
|
|
1750
1736
|
//#endregion
|
|
1751
1737
|
//#region src/routing/electric-proxy-router.ts
|
|
1752
1738
|
const electricProxyRouter = Router({ base: `/_electric/electric` });
|
|
@@ -3632,7 +3618,7 @@ var EntityManager = class {
|
|
|
3632
3618
|
};
|
|
3633
3619
|
}
|
|
3634
3620
|
/**
|
|
3635
|
-
* Deliver a message to an entity's main stream, with optional
|
|
3621
|
+
* Deliver a message to an entity's main stream, with optional inbox schema
|
|
3636
3622
|
* validation.
|
|
3637
3623
|
*/
|
|
3638
3624
|
async send(entityUrl, req, opts) {
|
|
@@ -3720,7 +3706,7 @@ var EntityManager = class {
|
|
|
3720
3706
|
if (result.changed && this.entityBridgeManager) await this.entityBridgeManager.onEntityChanged(entityUrl);
|
|
3721
3707
|
return updated;
|
|
3722
3708
|
}
|
|
3723
|
-
async
|
|
3709
|
+
async deleteTag(entityUrl, key, token) {
|
|
3724
3710
|
const entity = await this.registry.getEntity(entityUrl);
|
|
3725
3711
|
if (!entity) throw new ElectricAgentsError(ErrCodeNotFound, `Entity not found`, 404);
|
|
3726
3712
|
if (!this.isValidWriteToken(entity, token)) throw new ElectricAgentsError(ErrCodeUnauthorized, `Invalid write token`, 401);
|
|
@@ -3731,7 +3717,7 @@ var EntityManager = class {
|
|
|
3731
3717
|
if (result.changed && this.entityBridgeManager) await this.entityBridgeManager.onEntityChanged(entityUrl);
|
|
3732
3718
|
return updated;
|
|
3733
3719
|
}
|
|
3734
|
-
async
|
|
3720
|
+
async ensureEntitiesMembershipStream(tags) {
|
|
3735
3721
|
if (!this.entityBridgeManager) throw new Error(`Entity bridge manager not configured`);
|
|
3736
3722
|
return this.entityBridgeManager.register(this.validateTags(tags));
|
|
3737
3723
|
}
|
|
@@ -4160,7 +4146,7 @@ var EntityManager = class {
|
|
|
4160
4146
|
return null;
|
|
4161
4147
|
}
|
|
4162
4148
|
/**
|
|
4163
|
-
* Add new
|
|
4149
|
+
* Add new inbox/state schema keys to an entity type directly in Postgres.
|
|
4164
4150
|
*/
|
|
4165
4151
|
async amendSchemas(typeName, schemas) {
|
|
4166
4152
|
if (typeName === `principal`) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Entity type "principal" is built in and cannot be amended`, 400);
|
|
@@ -4200,7 +4186,7 @@ var EntityManager = class {
|
|
|
4200
4186
|
}
|
|
4201
4187
|
/**
|
|
4202
4188
|
* Enrich webhook payload with entity context.
|
|
4203
|
-
* Called by ElectricAgentsServer during webhook
|
|
4189
|
+
* Called by ElectricAgentsServer during subscription webhook dispatch to inject entity context.
|
|
4204
4190
|
*/
|
|
4205
4191
|
async enrichPayload(payload, consumer) {
|
|
4206
4192
|
const entity = await this.registry.getEntityByStream(consumer.primary_stream);
|
|
@@ -4465,7 +4451,7 @@ async function linkStreamToTargetSubscription(ctx, target, entity, subscriptionI
|
|
|
4465
4451
|
}
|
|
4466
4452
|
const webhookUrl = rewriteLoopbackWebhookUrl(target.url);
|
|
4467
4453
|
if (!webhookUrl) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Webhook dispatch target must include a valid URL`, 400);
|
|
4468
|
-
const forwardUrl = appendPathToUrl(ctx.publicUrl, `/_electric/
|
|
4454
|
+
const forwardUrl = appendPathToUrl(ctx.publicUrl, `/_electric/subscription-webhooks/${encodeURIComponent(subscriptionId)}`);
|
|
4469
4455
|
await ensureSubscriptionIncludesStream(ctx, subscriptionId, streamPath, {
|
|
4470
4456
|
type: `webhook`,
|
|
4471
4457
|
streams: [streamPath],
|
|
@@ -4484,7 +4470,7 @@ async function linkStreamToTargetSubscription(ctx, target, entity, subscriptionI
|
|
|
4484
4470
|
|
|
4485
4471
|
//#endregion
|
|
4486
4472
|
//#region src/routing/entities-router.ts
|
|
4487
|
-
const stringRecordSchema = Type.Record(Type.String(), Type.String());
|
|
4473
|
+
const stringRecordSchema$1 = Type.Record(Type.String(), Type.String());
|
|
4488
4474
|
function writeTokenFromRequest(request) {
|
|
4489
4475
|
const electricClaimToken = request.headers.get(`electric-claim-token`)?.trim();
|
|
4490
4476
|
if (electricClaimToken) return electricClaimToken;
|
|
@@ -4501,7 +4487,7 @@ const wakeConditionSchema = Type.Union([Type.Literal(`runFinished`), Type.Object
|
|
|
4501
4487
|
})]);
|
|
4502
4488
|
const spawnBodySchema = Type.Object({
|
|
4503
4489
|
args: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
|
|
4504
|
-
tags: Type.Optional(stringRecordSchema),
|
|
4490
|
+
tags: Type.Optional(stringRecordSchema$1),
|
|
4505
4491
|
parent: Type.Optional(Type.String()),
|
|
4506
4492
|
dispatch_policy: Type.Optional(dispatchPolicySchema),
|
|
4507
4493
|
initialMessage: Type.Optional(Type.Unknown()),
|
|
@@ -4593,10 +4579,8 @@ const eventSourceSubscriptionBodySchema = Type.Object({
|
|
|
4593
4579
|
lifetime: Type.Optional(subscriptionLifetimeSchema),
|
|
4594
4580
|
reason: Type.Optional(Type.String())
|
|
4595
4581
|
});
|
|
4596
|
-
const entitiesRegisterBodySchema = Type.Object({ tags: Type.Optional(stringRecordSchema) });
|
|
4597
4582
|
const entitiesRouter = Router({ base: `/_electric/entities` });
|
|
4598
4583
|
entitiesRouter.get(`/`, listEntities);
|
|
4599
|
-
entitiesRouter.post(`/register`, withSchema(entitiesRegisterBodySchema), registerEntitiesSource);
|
|
4600
4584
|
entitiesRouter.put(`/:type/:instanceId`, withSpawnableEntityType, withSchema(spawnBodySchema), spawnEntity);
|
|
4601
4585
|
entitiesRouter.get(`/:type/:instanceId`, withExistingEntity, getEntity);
|
|
4602
4586
|
entitiesRouter.head(`/:type/:instanceId`, withExistingEntity, headEntity);
|
|
@@ -4607,7 +4591,7 @@ entitiesRouter.patch(`/:type/:instanceId/inbox/:messageKey`, withExistingEntity,
|
|
|
4607
4591
|
entitiesRouter.delete(`/:type/:instanceId/inbox/:messageKey`, withExistingEntity, deleteInboxMessage);
|
|
4608
4592
|
entitiesRouter.post(`/:type/:instanceId/fork`, withExistingEntity, withSchema(forkBodySchema), forkEntity);
|
|
4609
4593
|
entitiesRouter.post(`/:type/:instanceId/tags/:tagKey`, withExistingEntity, withSchema(setTagBodySchema), setTag);
|
|
4610
|
-
entitiesRouter.delete(`/:type/:instanceId/tags/:tagKey`, withExistingEntity,
|
|
4594
|
+
entitiesRouter.delete(`/:type/:instanceId/tags/:tagKey`, withExistingEntity, deleteTag);
|
|
4611
4595
|
entitiesRouter.put(`/:type/:instanceId/schedules/:scheduleId`, withExistingEntity, withSchema(scheduleBodySchema), upsertSchedule);
|
|
4612
4596
|
entitiesRouter.delete(`/:type/:instanceId/schedules/:scheduleId`, withExistingEntity, deleteSchedule);
|
|
4613
4597
|
entitiesRouter.put(`/:type/:instanceId/event-source-subscriptions/:subscriptionId`, withExistingEntity, withSchema(eventSourceSubscriptionBodySchema), upsertEventSourceSubscription);
|
|
@@ -4675,11 +4659,6 @@ async function listEntities({ query }, ctx) {
|
|
|
4675
4659
|
});
|
|
4676
4660
|
return json(entities$1.map((entity) => toPublicEntity(entity)));
|
|
4677
4661
|
}
|
|
4678
|
-
async function registerEntitiesSource(request, ctx) {
|
|
4679
|
-
const parsed = routeBody(request);
|
|
4680
|
-
const result = await ctx.entityManager.registerEntitiesSource(parsed.tags ?? {});
|
|
4681
|
-
return json(result);
|
|
4682
|
-
}
|
|
4683
4662
|
async function upsertSchedule(request, ctx) {
|
|
4684
4663
|
const principalMutationError = rejectPrincipalEntityMutation(request, `scheduled`);
|
|
4685
4664
|
if (principalMutationError) return principalMutationError;
|
|
@@ -4760,7 +4739,7 @@ async function deleteEventSourceSubscription(request, ctx) {
|
|
|
4760
4739
|
return json(result);
|
|
4761
4740
|
}
|
|
4762
4741
|
async function setTag(request, ctx) {
|
|
4763
|
-
const principalMutationError = rejectPrincipalEntityMutation(request, `
|
|
4742
|
+
const principalMutationError = rejectPrincipalEntityMutation(request, `tag updated`);
|
|
4764
4743
|
if (principalMutationError) return principalMutationError;
|
|
4765
4744
|
const parsed = routeBody(request);
|
|
4766
4745
|
const { entityUrl } = requireExistingEntityRoute(request);
|
|
@@ -4768,12 +4747,12 @@ async function setTag(request, ctx) {
|
|
|
4768
4747
|
const updated = await ctx.entityManager.setTag(entityUrl, decodeURIComponent(request.params.tagKey), { value: parsed.value }, token);
|
|
4769
4748
|
return json(toPublicEntity(updated));
|
|
4770
4749
|
}
|
|
4771
|
-
async function
|
|
4772
|
-
const principalMutationError = rejectPrincipalEntityMutation(request, `
|
|
4750
|
+
async function deleteTag(request, ctx) {
|
|
4751
|
+
const principalMutationError = rejectPrincipalEntityMutation(request, `tag deleted`);
|
|
4773
4752
|
if (principalMutationError) return principalMutationError;
|
|
4774
4753
|
const { entityUrl } = requireExistingEntityRoute(request);
|
|
4775
4754
|
const token = writeTokenFromRequest(request);
|
|
4776
|
-
const updated = await ctx.entityManager.
|
|
4755
|
+
const updated = await ctx.entityManager.deleteTag(entityUrl, decodeURIComponent(request.params.tagKey), token);
|
|
4777
4756
|
return json(toPublicEntity(updated));
|
|
4778
4757
|
}
|
|
4779
4758
|
async function forkEntity(request, ctx) {
|
|
@@ -4905,17 +4884,13 @@ const registerEntityTypeBodySchema = Type.Object({
|
|
|
4905
4884
|
creation_schema: Type.Optional(jsonObjectSchema),
|
|
4906
4885
|
inbox_schemas: Type.Optional(schemaMapSchema),
|
|
4907
4886
|
state_schemas: Type.Optional(schemaMapSchema),
|
|
4908
|
-
input_schemas: Type.Optional(schemaMapSchema),
|
|
4909
|
-
output_schemas: Type.Optional(schemaMapSchema),
|
|
4910
4887
|
serve_endpoint: Type.Optional(Type.String()),
|
|
4911
4888
|
default_dispatch_policy: Type.Optional(dispatchPolicySchema)
|
|
4912
|
-
});
|
|
4889
|
+
}, { additionalProperties: false });
|
|
4913
4890
|
const amendEntityTypeSchemasBodySchema = Type.Object({
|
|
4914
|
-
input_schemas: Type.Optional(schemaMapSchema),
|
|
4915
|
-
output_schemas: Type.Optional(schemaMapSchema),
|
|
4916
4891
|
inbox_schemas: Type.Optional(schemaMapSchema),
|
|
4917
4892
|
state_schemas: Type.Optional(schemaMapSchema)
|
|
4918
|
-
});
|
|
4893
|
+
}, { additionalProperties: false });
|
|
4919
4894
|
const entityTypesRouter = Router({ base: `/_electric/entity-types` });
|
|
4920
4895
|
entityTypesRouter.get(`/`, listEntityTypes);
|
|
4921
4896
|
entityTypesRouter.post(`/`, withSchema(registerEntityTypeBodySchema), registerEntityType);
|
|
@@ -4955,8 +4930,8 @@ async function getEntityType(request, ctx) {
|
|
|
4955
4930
|
async function amendSchemas(request, ctx) {
|
|
4956
4931
|
const parsed = routeBody(request);
|
|
4957
4932
|
const updated = await ctx.entityManager.amendSchemas(request.params.name, {
|
|
4958
|
-
inbox_schemas: parsed.inbox_schemas
|
|
4959
|
-
state_schemas: parsed.state_schemas
|
|
4933
|
+
inbox_schemas: parsed.inbox_schemas,
|
|
4934
|
+
state_schemas: parsed.state_schemas
|
|
4960
4935
|
});
|
|
4961
4936
|
return json(toPublicEntityType(updated));
|
|
4962
4937
|
}
|
|
@@ -4966,13 +4941,12 @@ async function deleteEntityType(request, ctx) {
|
|
|
4966
4941
|
}
|
|
4967
4942
|
function normalizeEntityTypeRequest(parsed) {
|
|
4968
4943
|
const serveEndpoint = rewriteLoopbackWebhookUrl(parsed.serve_endpoint);
|
|
4969
|
-
const compatibilityFields = parsed;
|
|
4970
4944
|
return {
|
|
4971
4945
|
name: parsed.name ?? ``,
|
|
4972
4946
|
description: parsed.description ?? ``,
|
|
4973
4947
|
creation_schema: parsed.creation_schema,
|
|
4974
|
-
inbox_schemas: parsed.inbox_schemas
|
|
4975
|
-
state_schemas: parsed.state_schemas
|
|
4948
|
+
inbox_schemas: parsed.inbox_schemas,
|
|
4949
|
+
state_schemas: parsed.state_schemas,
|
|
4976
4950
|
serve_endpoint: serveEndpoint,
|
|
4977
4951
|
default_dispatch_policy: parsed.default_dispatch_policy ?? (serveEndpoint ? { targets: [{
|
|
4978
4952
|
type: `webhook`,
|
|
@@ -4983,8 +4957,6 @@ function normalizeEntityTypeRequest(parsed) {
|
|
|
4983
4957
|
function toPublicEntityType(entityType) {
|
|
4984
4958
|
return {
|
|
4985
4959
|
...entityType,
|
|
4986
|
-
input_schemas: entityType.inbox_schemas,
|
|
4987
|
-
output_schemas: entityType.state_schemas,
|
|
4988
4960
|
revision: entityType.revision
|
|
4989
4961
|
};
|
|
4990
4962
|
}
|
|
@@ -5068,13 +5040,35 @@ function errorMapper(err, req) {
|
|
|
5068
5040
|
function rejectIfShuttingDown(req, ctx) {
|
|
5069
5041
|
if (!ctx.isShuttingDown()) return void 0;
|
|
5070
5042
|
const path$1 = new URL(req.url).pathname;
|
|
5071
|
-
if (!path$1.startsWith(`/_electric/
|
|
5043
|
+
if (!path$1.startsWith(`/_electric/subscription-webhooks/`)) return void 0;
|
|
5072
5044
|
return apiError(503, `SERVER_STOPPING`, `Server is shutting down`);
|
|
5073
5045
|
}
|
|
5074
5046
|
function getRequestSpan(req) {
|
|
5075
5047
|
return carrier(req)[SPAN_KEY];
|
|
5076
5048
|
}
|
|
5077
5049
|
|
|
5050
|
+
//#endregion
|
|
5051
|
+
//#region src/routing/observations-router.ts
|
|
5052
|
+
const stringRecordSchema = Type.Record(Type.String(), Type.String());
|
|
5053
|
+
const ensureEntitiesMembershipStreamBodySchema = Type.Object({ tags: Type.Optional(stringRecordSchema) });
|
|
5054
|
+
const ensureCronStreamBodySchema = Type.Object({
|
|
5055
|
+
expression: Type.String(),
|
|
5056
|
+
timezone: Type.Optional(Type.String())
|
|
5057
|
+
});
|
|
5058
|
+
const observationsRouter = Router({ base: `/_electric/observations` });
|
|
5059
|
+
observationsRouter.post(`/entities/ensure-stream`, withSchema(ensureEntitiesMembershipStreamBodySchema), ensureEntitiesMembershipStream);
|
|
5060
|
+
observationsRouter.post(`/cron/ensure-stream`, withSchema(ensureCronStreamBodySchema), ensureCronStream);
|
|
5061
|
+
async function ensureEntitiesMembershipStream(request, ctx) {
|
|
5062
|
+
const parsed = routeBody(request);
|
|
5063
|
+
const result = await ctx.entityManager.ensureEntitiesMembershipStream(parsed.tags ?? {});
|
|
5064
|
+
return json(result);
|
|
5065
|
+
}
|
|
5066
|
+
async function ensureCronStream(request, ctx) {
|
|
5067
|
+
const parsed = routeBody(request);
|
|
5068
|
+
const streamPath = await ctx.entityManager.getOrCreateCronStream(parsed.expression, parsed.timezone);
|
|
5069
|
+
return json({ streamUrl: streamPath });
|
|
5070
|
+
}
|
|
5071
|
+
|
|
5078
5072
|
//#endregion
|
|
5079
5073
|
//#region src/routing/tenant-stream-paths.ts
|
|
5080
5074
|
function withLeadingSlash(path$1) {
|
|
@@ -5413,7 +5407,7 @@ async function notificationFromClaim(ctx, input) {
|
|
|
5413
5407
|
wakeId: input.claim.wake_id,
|
|
5414
5408
|
streamPath: primaryStream,
|
|
5415
5409
|
streams: streams$1,
|
|
5416
|
-
callback: appendPathToUrl(ctx.publicUrl, `/_electric/
|
|
5410
|
+
callback: appendPathToUrl(ctx.publicUrl, `/_electric/wake-callbacks/${encodeURIComponent(input.claim.wake_id)}`),
|
|
5417
5411
|
claimToken: input.claim.token,
|
|
5418
5412
|
triggerEvent: `message_received`,
|
|
5419
5413
|
entity: {
|
|
@@ -5448,7 +5442,7 @@ const wakeRegistrationBodySchema = Type.Object({
|
|
|
5448
5442
|
includeResponse: Type.Optional(Type.Boolean()),
|
|
5449
5443
|
manifestKey: Type.Optional(Type.String())
|
|
5450
5444
|
});
|
|
5451
|
-
const
|
|
5445
|
+
const subscriptionWebhookBodySchema = Type.Object({
|
|
5452
5446
|
subscription_id: Type.Optional(Type.String()),
|
|
5453
5447
|
wake_id: Type.Optional(Type.String()),
|
|
5454
5448
|
generation: Type.Optional(Type.Number()),
|
|
@@ -5462,7 +5456,7 @@ const webhookForwardBodySchema = Type.Object({
|
|
|
5462
5456
|
consumer_id: Type.Optional(Type.String()),
|
|
5463
5457
|
callback: Type.Optional(Type.String())
|
|
5464
5458
|
}, { additionalProperties: true });
|
|
5465
|
-
const
|
|
5459
|
+
const wakeCallbackBodySchema = Type.Object({
|
|
5466
5460
|
epoch: Type.Optional(Type.Number()),
|
|
5467
5461
|
generation: Type.Optional(Type.Number()),
|
|
5468
5462
|
wakeId: Type.Optional(Type.String()),
|
|
@@ -5475,13 +5469,13 @@ const internalRouter = Router({ base: `/_electric` });
|
|
|
5475
5469
|
internalRouter.get(`/health`, () => json({ status: `ok` }));
|
|
5476
5470
|
internalRouter.get(`/event-sources`, listEventSources);
|
|
5477
5471
|
internalRouter.post(`/wake`, withSchema(wakeRegistrationBodySchema), registerWake);
|
|
5478
|
-
internalRouter.post(`/
|
|
5479
|
-
internalRouter.post(`/
|
|
5472
|
+
internalRouter.post(`/subscription-webhooks/:subscriptionId`, subscriptionWebhook);
|
|
5473
|
+
internalRouter.post(`/wake-callbacks/:consumerId`, wakeCallback);
|
|
5480
5474
|
internalRouter.all(`/runners`, runnersRouter.fetch);
|
|
5481
5475
|
internalRouter.all(`/runners/*`, runnersRouter.fetch);
|
|
5482
5476
|
internalRouter.all(`/entities/*`, entitiesRouter.fetch);
|
|
5483
5477
|
internalRouter.all(`/entity-types/*`, entityTypesRouter.fetch);
|
|
5484
|
-
internalRouter.all(`/
|
|
5478
|
+
internalRouter.all(`/observations/*`, observationsRouter.fetch);
|
|
5485
5479
|
internalRouter.get(`/electric/*`, electricProxyRouter.fetch);
|
|
5486
5480
|
internalRouter.all(`*`, () => status(404));
|
|
5487
5481
|
function routeParam(request, name) {
|
|
@@ -5514,13 +5508,30 @@ function resolveWebhookSigner(ctx) {
|
|
|
5514
5508
|
return ctx.webhookSigner ?? getDefaultWebhookSigner();
|
|
5515
5509
|
}
|
|
5516
5510
|
function durableStreamsWebhookJwksUrl(ctx) {
|
|
5517
|
-
if (!ctx.durableStreamsRouting) return
|
|
5511
|
+
if (!ctx.durableStreamsRouting) return appendPathToBackendUrl(ctx.durableStreamsUrl, `/__ds/jwks.json`);
|
|
5518
5512
|
return resolveDurableStreamsRoutingAdapter(ctx.durableStreamsRouting, ctx.durableStreamsUrl).controlUrl({
|
|
5519
5513
|
durableStreamsUrl: ctx.durableStreamsUrl,
|
|
5520
5514
|
serviceId: ctx.service,
|
|
5521
5515
|
requestUrl: appendPathToUrl(ctx.publicUrl, `/__ds/jwks.json`)
|
|
5522
5516
|
}).toString();
|
|
5523
5517
|
}
|
|
5518
|
+
function appendPathToBackendUrl(baseUrl, path$1) {
|
|
5519
|
+
const base = new URL(baseUrl);
|
|
5520
|
+
const pathUrl = new URL(path$1, `http://electric-agents.local`);
|
|
5521
|
+
const basePath = base.pathname === `/` ? `` : base.pathname.replace(/\/+$/, ``);
|
|
5522
|
+
const suffix = pathUrl.pathname.startsWith(`/`) ? pathUrl.pathname : `/${pathUrl.pathname}`;
|
|
5523
|
+
const target = new URL(base);
|
|
5524
|
+
target.pathname = `${basePath}${suffix}`;
|
|
5525
|
+
target.search = ``;
|
|
5526
|
+
target.hash = pathUrl.hash;
|
|
5527
|
+
base.searchParams.forEach((value, key) => {
|
|
5528
|
+
target.searchParams.append(key, value);
|
|
5529
|
+
});
|
|
5530
|
+
pathUrl.searchParams.forEach((value, key) => {
|
|
5531
|
+
target.searchParams.append(key, value);
|
|
5532
|
+
});
|
|
5533
|
+
return target.toString();
|
|
5534
|
+
}
|
|
5524
5535
|
function durableStreamsJwksFetchClient(ctx) {
|
|
5525
5536
|
return async (input, init) => {
|
|
5526
5537
|
const headers = new Headers(init?.headers);
|
|
@@ -5584,10 +5595,10 @@ async function listEventSources(_request, ctx) {
|
|
|
5584
5595
|
function isAgentVisibleEventSource(source) {
|
|
5585
5596
|
return source.agentVisible === true && source.status === `active`;
|
|
5586
5597
|
}
|
|
5587
|
-
async function
|
|
5598
|
+
async function subscriptionWebhook(request, ctx) {
|
|
5588
5599
|
const subscriptionId = routeParam(request, `subscriptionId`);
|
|
5589
5600
|
const rootSpan = getRequestSpan(request);
|
|
5590
|
-
rootSpan?.updateName(`webhook
|
|
5601
|
+
rootSpan?.updateName(`subscription-webhook`);
|
|
5591
5602
|
rootSpan?.setAttribute(`electric_agents.webhook.subscription_id`, subscriptionId);
|
|
5592
5603
|
const body = await readRequestBody(request);
|
|
5593
5604
|
const signatureError = await verifyDurableStreamsWebhook(request, ctx, body);
|
|
@@ -5601,7 +5612,7 @@ async function webhookForward(request, ctx) {
|
|
|
5601
5612
|
}
|
|
5602
5613
|
});
|
|
5603
5614
|
if (!targetWebhookUrl) return apiError(404, ErrCodeSubscriptionNotFound, `Unknown webhook subscription`);
|
|
5604
|
-
const parsedBodyResult = validateOptionalJsonBody(
|
|
5615
|
+
const parsedBodyResult = validateOptionalJsonBody(subscriptionWebhookBodySchema, body, request.headers.get(`content-type`));
|
|
5605
5616
|
if (!parsedBodyResult.ok) return parsedBodyResult.response;
|
|
5606
5617
|
let forwardBody = body;
|
|
5607
5618
|
let runningEntityUrl = null;
|
|
@@ -5647,7 +5658,7 @@ async function webhookForward(request, ctx) {
|
|
|
5647
5658
|
span.end();
|
|
5648
5659
|
}
|
|
5649
5660
|
}).catch((err) => {
|
|
5650
|
-
serverLog.warn(`[webhook
|
|
5661
|
+
serverLog.warn(`[subscription-webhook] consumerCallbacks upsert failed (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
|
|
5651
5662
|
}) : void 0;
|
|
5652
5663
|
const [entity, enriched] = await Promise.all([entityPromise, enrichPromise]);
|
|
5653
5664
|
if (entity?.status === `stopped` || entity?.status === `paused`) {
|
|
@@ -5668,7 +5679,7 @@ async function webhookForward(request, ctx) {
|
|
|
5668
5679
|
runningEntityUrl = entity.url;
|
|
5669
5680
|
}
|
|
5670
5681
|
if (consumerId && callbackUrl) {
|
|
5671
|
-
const callback = appendPathToUrl(ctx.publicUrl, `/_electric/
|
|
5682
|
+
const callback = appendPathToUrl(ctx.publicUrl, `/_electric/wake-callbacks/${encodeURIComponent(consumerId)}`);
|
|
5672
5683
|
enriched.callback = callback;
|
|
5673
5684
|
if (newWebhook) {
|
|
5674
5685
|
enriched.consumerId = newWebhook.wakeId;
|
|
@@ -5705,21 +5716,21 @@ async function webhookForward(request, ctx) {
|
|
|
5705
5716
|
});
|
|
5706
5717
|
} catch (err) {
|
|
5707
5718
|
if (runningEntityUrl) await ctx.entityManager.registry.updateStatus(runningEntityUrl, `idle`);
|
|
5708
|
-
return apiError(502, `
|
|
5719
|
+
return apiError(502, `SUBSCRIPTION_WEBHOOK_FAILED`, err instanceof Error ? err.message : String(err));
|
|
5709
5720
|
}
|
|
5710
5721
|
const responseBytes = upstream.body ? new Uint8Array(await upstream.arrayBuffer()) : new Uint8Array();
|
|
5711
5722
|
return responseFromUpstream(upstream, responseBytes);
|
|
5712
5723
|
}
|
|
5713
|
-
async function
|
|
5724
|
+
async function wakeCallback(request, ctx) {
|
|
5714
5725
|
const consumerId = routeParam(request, `consumerId`);
|
|
5715
5726
|
const rows = await ctx.pgDb.select().from(consumerCallbacks).where(and(eq(consumerCallbacks.tenantId, ctx.service), eq(consumerCallbacks.consumerId, consumerId))).limit(1);
|
|
5716
5727
|
const target = rows[0] ? {
|
|
5717
5728
|
callbackUrl: rows[0].callbackUrl,
|
|
5718
5729
|
primaryStream: rows[0].primaryStream
|
|
5719
5730
|
} : void 0;
|
|
5720
|
-
if (!target) return apiError(404,
|
|
5731
|
+
if (!target) return apiError(404, ErrCodeWakeCallbackNotFound, `Unknown wake-callback consumer`);
|
|
5721
5732
|
const body = await readRequestBody(request);
|
|
5722
|
-
const parsedBodyResult = validateOptionalJsonBody(
|
|
5733
|
+
const parsedBodyResult = validateOptionalJsonBody(wakeCallbackBodySchema, body, request.headers.get(`content-type`));
|
|
5723
5734
|
if (!parsedBodyResult.ok) return parsedBodyResult.response;
|
|
5724
5735
|
const requestBody = parsedBodyResult.value;
|
|
5725
5736
|
const isClaimRequest = requestBody?.wakeId !== void 0 || requestBody?.wake_id !== void 0;
|
|
@@ -5737,14 +5748,14 @@ async function callbackForward(request, ctx) {
|
|
|
5737
5748
|
}
|
|
5738
5749
|
return json(responseBody);
|
|
5739
5750
|
}
|
|
5740
|
-
const upstreamBody =
|
|
5751
|
+
const upstreamBody = encodeWakeCallbackBody(ctx.service, consumerId, requestBody, resolveDurableStreamsRoutingAdapter(ctx.durableStreamsRouting, ctx.durableStreamsUrl));
|
|
5741
5752
|
let upstream;
|
|
5742
5753
|
try {
|
|
5743
5754
|
const subscriptionId = durableStreamsSubscriptionCallback(target.callbackUrl);
|
|
5744
5755
|
if (subscriptionId) {
|
|
5745
5756
|
const token = claimTokenFromRequest(request);
|
|
5746
5757
|
if (!token) return apiError(401, `UNAUTHORIZED`, `Missing claim token`);
|
|
5747
|
-
const upstreamPayload =
|
|
5758
|
+
const upstreamPayload = encodeWakeCallbackPayload(consumerId, requestBody, (stream) => stream.replace(/^\/+/, ``));
|
|
5748
5759
|
const result = await ctx.streamClient.ackSubscription(subscriptionId, token, upstreamPayload);
|
|
5749
5760
|
upstream = json(result);
|
|
5750
5761
|
} else upstream = await fetch(target.callbackUrl, {
|
|
@@ -5753,7 +5764,7 @@ async function callbackForward(request, ctx) {
|
|
|
5753
5764
|
body: bodyFromBytes(upstreamBody)
|
|
5754
5765
|
});
|
|
5755
5766
|
} catch (err) {
|
|
5756
|
-
return apiError(502, `
|
|
5767
|
+
return apiError(502, `WAKE_CALLBACK_FAILED`, err instanceof Error ? err.message : String(err));
|
|
5757
5768
|
}
|
|
5758
5769
|
let responseBytes = upstream.body ? new Uint8Array(await upstream.arrayBuffer()) : new Uint8Array();
|
|
5759
5770
|
if (isClaimRequest && upstream.ok && target.primaryStream) {
|
|
@@ -5773,7 +5784,7 @@ async function callbackForward(request, ctx) {
|
|
|
5773
5784
|
epoch
|
|
5774
5785
|
});
|
|
5775
5786
|
if (upstream.ok && isDoneRequest && target.primaryStream) {
|
|
5776
|
-
serverLog.info(`[callback
|
|
5787
|
+
serverLog.info(`[wake-callback] done received for stream=${target.primaryStream} consumer=${consumerId}`);
|
|
5777
5788
|
const stillOwnsClaim = ctx.runtime.claimWriteTokens.owns(ctx.service, target.primaryStream, consumerId);
|
|
5778
5789
|
const entity = await ctx.entityManager.registry.getEntityByStream(target.primaryStream);
|
|
5779
5790
|
let entityCleared = false;
|
|
@@ -5795,13 +5806,13 @@ async function callbackForward(request, ctx) {
|
|
|
5795
5806
|
if (entity && (entityCleared || stillOwnsClaim)) {
|
|
5796
5807
|
await ctx.entityManager.registry.updateStatus(entity.url, entity.status === `stopping` ? `stopped` : `idle`);
|
|
5797
5808
|
await ctx.entityBridgeManager.onEntityChanged(entity.url);
|
|
5798
|
-
serverLog.info(`[callback
|
|
5799
|
-
} else if (!entity) serverLog.warn(`[callback
|
|
5809
|
+
serverLog.info(`[wake-callback] status updated after done for ${entity.url}`);
|
|
5810
|
+
} else if (!entity) serverLog.warn(`[wake-callback] done received but no entity found for stream=${target.primaryStream}`);
|
|
5800
5811
|
if (stillOwnsClaim) ctx.runtime.claimWriteTokens.clearStream(ctx.service, target.primaryStream);
|
|
5801
|
-
else if (entity) serverLog.info(`[callback
|
|
5802
|
-
} else if (requestBody?.done === true) serverLog.warn(`[callback
|
|
5812
|
+
else if (entity) serverLog.info(`[wake-callback] done arrived after in-memory token evicted (stream=${target.primaryStream} consumer=${consumerId})`);
|
|
5813
|
+
} else if (requestBody?.done === true) serverLog.warn(`[wake-callback] done received but skipped: upstream.ok=${upstream.ok} primaryStream=${target.primaryStream ?? `null`} consumer=${consumerId}`);
|
|
5803
5814
|
} catch (err) {
|
|
5804
|
-
serverLog.error(`[callback
|
|
5815
|
+
serverLog.error(`[wake-callback] error processing done for consumer=${consumerId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
5805
5816
|
}
|
|
5806
5817
|
return responseFromUpstream(upstream, responseBytes);
|
|
5807
5818
|
}
|
|
@@ -5810,11 +5821,11 @@ async function mintClaimWriteToken(ctx, streamPath, consumerId) {
|
|
|
5810
5821
|
if (!entity) return void 0;
|
|
5811
5822
|
return ctx.runtime.claimWriteTokens.mint(ctx.service, streamPath, consumerId);
|
|
5812
5823
|
}
|
|
5813
|
-
function
|
|
5814
|
-
const payload =
|
|
5824
|
+
function encodeWakeCallbackBody(service, consumerId, body, routingAdapter) {
|
|
5825
|
+
const payload = encodeWakeCallbackPayload(consumerId, body, (stream) => routingAdapter.toBackendStreamPath(service, stream));
|
|
5815
5826
|
return new TextEncoder().encode(JSON.stringify(payload));
|
|
5816
5827
|
}
|
|
5817
|
-
function
|
|
5828
|
+
function encodeWakeCallbackPayload(consumerId, body, mapStream) {
|
|
5818
5829
|
if (!body) return {};
|
|
5819
5830
|
const generation = body.generation ?? body.epoch;
|
|
5820
5831
|
const wakeId = body.wake_id ?? body.wakeId ?? consumerId;
|
|
@@ -7396,7 +7407,7 @@ var WakeRegistry = class {
|
|
|
7396
7407
|
try {
|
|
7397
7408
|
for (const message of messages) {
|
|
7398
7409
|
await this.applyShapeMessage(message);
|
|
7399
|
-
if (!settled &&
|
|
7410
|
+
if (!settled && isControlMessage(message) && message.headers.control === `up-to-date`) {
|
|
7400
7411
|
settled = true;
|
|
7401
7412
|
resolve$1();
|
|
7402
7413
|
}
|