@electric-ax/agents-server 0.4.9 → 0.4.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/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/index.js
CHANGED
|
@@ -422,7 +422,7 @@ const ErrCodeForkInProgress = `FORK_IN_PROGRESS`;
|
|
|
422
422
|
const ErrCodeForkWaitTimeout = `FORK_WAIT_TIMEOUT`;
|
|
423
423
|
const ErrCodeEntityPersistFailed = `ENTITY_PERSIST_FAILED`;
|
|
424
424
|
const ErrCodeSubscriptionNotFound = `SUBSCRIPTION_NOT_FOUND`;
|
|
425
|
-
const
|
|
425
|
+
const ErrCodeWakeCallbackNotFound = `WAKE_CALLBACK_NOT_FOUND`;
|
|
426
426
|
|
|
427
427
|
//#endregion
|
|
428
428
|
//#region src/tenant.ts
|
|
@@ -2503,7 +2503,7 @@ async function linkStreamToTargetSubscription(ctx, target, entity, subscriptionI
|
|
|
2503
2503
|
}
|
|
2504
2504
|
const webhookUrl = rewriteLoopbackWebhookUrl(target.url);
|
|
2505
2505
|
if (!webhookUrl) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Webhook dispatch target must include a valid URL`, 400);
|
|
2506
|
-
const forwardUrl = appendPathToUrl(ctx.publicUrl, `/_electric/
|
|
2506
|
+
const forwardUrl = appendPathToUrl(ctx.publicUrl, `/_electric/subscription-webhooks/${encodeURIComponent(subscriptionId)}`);
|
|
2507
2507
|
await ensureSubscriptionIncludesStream(ctx, subscriptionId, streamPath, {
|
|
2508
2508
|
type: `webhook`,
|
|
2509
2509
|
streams: [streamPath],
|
|
@@ -3588,7 +3588,7 @@ var EntityManager = class {
|
|
|
3588
3588
|
};
|
|
3589
3589
|
}
|
|
3590
3590
|
/**
|
|
3591
|
-
* Deliver a message to an entity's main stream, with optional
|
|
3591
|
+
* Deliver a message to an entity's main stream, with optional inbox schema
|
|
3592
3592
|
* validation.
|
|
3593
3593
|
*/
|
|
3594
3594
|
async send(entityUrl, req, opts) {
|
|
@@ -3676,7 +3676,7 @@ var EntityManager = class {
|
|
|
3676
3676
|
if (result.changed && this.entityBridgeManager) await this.entityBridgeManager.onEntityChanged(entityUrl);
|
|
3677
3677
|
return updated;
|
|
3678
3678
|
}
|
|
3679
|
-
async
|
|
3679
|
+
async deleteTag(entityUrl, key, token) {
|
|
3680
3680
|
const entity = await this.registry.getEntity(entityUrl);
|
|
3681
3681
|
if (!entity) throw new ElectricAgentsError(ErrCodeNotFound, `Entity not found`, 404);
|
|
3682
3682
|
if (!this.isValidWriteToken(entity, token)) throw new ElectricAgentsError(ErrCodeUnauthorized, `Invalid write token`, 401);
|
|
@@ -3687,7 +3687,7 @@ var EntityManager = class {
|
|
|
3687
3687
|
if (result.changed && this.entityBridgeManager) await this.entityBridgeManager.onEntityChanged(entityUrl);
|
|
3688
3688
|
return updated;
|
|
3689
3689
|
}
|
|
3690
|
-
async
|
|
3690
|
+
async ensureEntitiesMembershipStream(tags) {
|
|
3691
3691
|
if (!this.entityBridgeManager) throw new Error(`Entity bridge manager not configured`);
|
|
3692
3692
|
return this.entityBridgeManager.register(this.validateTags(tags));
|
|
3693
3693
|
}
|
|
@@ -4116,7 +4116,7 @@ var EntityManager = class {
|
|
|
4116
4116
|
return null;
|
|
4117
4117
|
}
|
|
4118
4118
|
/**
|
|
4119
|
-
* Add new
|
|
4119
|
+
* Add new inbox/state schema keys to an entity type directly in Postgres.
|
|
4120
4120
|
*/
|
|
4121
4121
|
async amendSchemas(typeName, schemas) {
|
|
4122
4122
|
if (typeName === `principal`) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Entity type "principal" is built in and cannot be amended`, 400);
|
|
@@ -4156,7 +4156,7 @@ var EntityManager = class {
|
|
|
4156
4156
|
}
|
|
4157
4157
|
/**
|
|
4158
4158
|
* Enrich webhook payload with entity context.
|
|
4159
|
-
* Called by ElectricAgentsServer during webhook
|
|
4159
|
+
* Called by ElectricAgentsServer during subscription webhook dispatch to inject entity context.
|
|
4160
4160
|
*/
|
|
4161
4161
|
async enrichPayload(payload, consumer) {
|
|
4162
4162
|
const entity = await this.registry.getEntityByStream(consumer.primary_stream);
|
|
@@ -5415,7 +5415,7 @@ var WakeRegistry = class {
|
|
|
5415
5415
|
try {
|
|
5416
5416
|
for (const message of messages) {
|
|
5417
5417
|
await this.applyShapeMessage(message);
|
|
5418
|
-
if (!settled &&
|
|
5418
|
+
if (!settled && isControlMessage(message) && message.headers.control === `up-to-date`) {
|
|
5419
5419
|
settled = true;
|
|
5420
5420
|
resolve$1();
|
|
5421
5421
|
}
|
|
@@ -6202,7 +6202,7 @@ function validateParsedBody(schema, parsed) {
|
|
|
6202
6202
|
//#region src/routing/durable-streams-routing-adapter.ts
|
|
6203
6203
|
function appendSearch(target, source) {
|
|
6204
6204
|
source.searchParams.forEach((value, key) => {
|
|
6205
|
-
|
|
6205
|
+
target.searchParams.append(key, value);
|
|
6206
6206
|
});
|
|
6207
6207
|
return target;
|
|
6208
6208
|
}
|
|
@@ -6522,7 +6522,7 @@ async function rewriteSubscriptionRequestBody(request, ctx, subscriptionId, rout
|
|
|
6522
6522
|
let targetWebhookUrl = null;
|
|
6523
6523
|
if (payload.webhook?.url !== void 0) {
|
|
6524
6524
|
targetWebhookUrl = rewriteLoopbackWebhookUrl(payload.webhook.url) ?? null;
|
|
6525
|
-
payload.webhook.url = appendPathToUrl(ctx.publicUrl, `/_electric/
|
|
6525
|
+
payload.webhook.url = appendPathToUrl(ctx.publicUrl, `/_electric/subscription-webhooks/${encodeURIComponent(subscriptionId)}`);
|
|
6526
6526
|
}
|
|
6527
6527
|
rewriteSubscriptionBodyForBackend(payload, ctx.service, routingAdapter);
|
|
6528
6528
|
return {
|
|
@@ -6645,20 +6645,6 @@ async function proxyPassThrough(request, ctx) {
|
|
|
6645
6645
|
}
|
|
6646
6646
|
}
|
|
6647
6647
|
|
|
6648
|
-
//#endregion
|
|
6649
|
-
//#region src/routing/cron-router.ts
|
|
6650
|
-
const cronRegisterBodySchema = Type.Object({
|
|
6651
|
-
expression: Type.String(),
|
|
6652
|
-
timezone: Type.Optional(Type.String())
|
|
6653
|
-
});
|
|
6654
|
-
const cronRouter = Router({ base: `/_electric/cron` });
|
|
6655
|
-
cronRouter.post(`/register`, withSchema(cronRegisterBodySchema), registerCron);
|
|
6656
|
-
async function registerCron(request, ctx) {
|
|
6657
|
-
const parsed = routeBody(request);
|
|
6658
|
-
const streamPath = await ctx.entityManager.getOrCreateCronStream(parsed.expression, parsed.timezone);
|
|
6659
|
-
return json({ streamUrl: streamPath });
|
|
6660
|
-
}
|
|
6661
|
-
|
|
6662
6648
|
//#endregion
|
|
6663
6649
|
//#region src/routing/electric-proxy-router.ts
|
|
6664
6650
|
const electricProxyRouter = Router({ base: `/_electric/electric` });
|
|
@@ -6692,7 +6678,7 @@ async function proxyElectric(request, ctx) {
|
|
|
6692
6678
|
|
|
6693
6679
|
//#endregion
|
|
6694
6680
|
//#region src/routing/entities-router.ts
|
|
6695
|
-
const stringRecordSchema = Type.Record(Type.String(), Type.String());
|
|
6681
|
+
const stringRecordSchema$1 = Type.Record(Type.String(), Type.String());
|
|
6696
6682
|
function writeTokenFromRequest(request) {
|
|
6697
6683
|
const electricClaimToken = request.headers.get(`electric-claim-token`)?.trim();
|
|
6698
6684
|
if (electricClaimToken) return electricClaimToken;
|
|
@@ -6709,7 +6695,7 @@ const wakeConditionSchema = Type.Union([Type.Literal(`runFinished`), Type.Object
|
|
|
6709
6695
|
})]);
|
|
6710
6696
|
const spawnBodySchema = Type.Object({
|
|
6711
6697
|
args: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
|
|
6712
|
-
tags: Type.Optional(stringRecordSchema),
|
|
6698
|
+
tags: Type.Optional(stringRecordSchema$1),
|
|
6713
6699
|
parent: Type.Optional(Type.String()),
|
|
6714
6700
|
dispatch_policy: Type.Optional(dispatchPolicySchema),
|
|
6715
6701
|
initialMessage: Type.Optional(Type.Unknown()),
|
|
@@ -6801,10 +6787,8 @@ const eventSourceSubscriptionBodySchema = Type.Object({
|
|
|
6801
6787
|
lifetime: Type.Optional(subscriptionLifetimeSchema),
|
|
6802
6788
|
reason: Type.Optional(Type.String())
|
|
6803
6789
|
});
|
|
6804
|
-
const entitiesRegisterBodySchema = Type.Object({ tags: Type.Optional(stringRecordSchema) });
|
|
6805
6790
|
const entitiesRouter = Router({ base: `/_electric/entities` });
|
|
6806
6791
|
entitiesRouter.get(`/`, listEntities);
|
|
6807
|
-
entitiesRouter.post(`/register`, withSchema(entitiesRegisterBodySchema), registerEntitiesSource);
|
|
6808
6792
|
entitiesRouter.put(`/:type/:instanceId`, withSpawnableEntityType, withSchema(spawnBodySchema), spawnEntity);
|
|
6809
6793
|
entitiesRouter.get(`/:type/:instanceId`, withExistingEntity, getEntity);
|
|
6810
6794
|
entitiesRouter.head(`/:type/:instanceId`, withExistingEntity, headEntity);
|
|
@@ -6815,7 +6799,7 @@ entitiesRouter.patch(`/:type/:instanceId/inbox/:messageKey`, withExistingEntity,
|
|
|
6815
6799
|
entitiesRouter.delete(`/:type/:instanceId/inbox/:messageKey`, withExistingEntity, deleteInboxMessage);
|
|
6816
6800
|
entitiesRouter.post(`/:type/:instanceId/fork`, withExistingEntity, withSchema(forkBodySchema), forkEntity);
|
|
6817
6801
|
entitiesRouter.post(`/:type/:instanceId/tags/:tagKey`, withExistingEntity, withSchema(setTagBodySchema), setTag);
|
|
6818
|
-
entitiesRouter.delete(`/:type/:instanceId/tags/:tagKey`, withExistingEntity,
|
|
6802
|
+
entitiesRouter.delete(`/:type/:instanceId/tags/:tagKey`, withExistingEntity, deleteTag);
|
|
6819
6803
|
entitiesRouter.put(`/:type/:instanceId/schedules/:scheduleId`, withExistingEntity, withSchema(scheduleBodySchema), upsertSchedule);
|
|
6820
6804
|
entitiesRouter.delete(`/:type/:instanceId/schedules/:scheduleId`, withExistingEntity, deleteSchedule);
|
|
6821
6805
|
entitiesRouter.put(`/:type/:instanceId/event-source-subscriptions/:subscriptionId`, withExistingEntity, withSchema(eventSourceSubscriptionBodySchema), upsertEventSourceSubscription);
|
|
@@ -6883,11 +6867,6 @@ async function listEntities({ query }, ctx) {
|
|
|
6883
6867
|
});
|
|
6884
6868
|
return json(entities$1.map((entity) => toPublicEntity(entity)));
|
|
6885
6869
|
}
|
|
6886
|
-
async function registerEntitiesSource(request, ctx) {
|
|
6887
|
-
const parsed = routeBody(request);
|
|
6888
|
-
const result = await ctx.entityManager.registerEntitiesSource(parsed.tags ?? {});
|
|
6889
|
-
return json(result);
|
|
6890
|
-
}
|
|
6891
6870
|
async function upsertSchedule(request, ctx) {
|
|
6892
6871
|
const principalMutationError = rejectPrincipalEntityMutation(request, `scheduled`);
|
|
6893
6872
|
if (principalMutationError) return principalMutationError;
|
|
@@ -6968,7 +6947,7 @@ async function deleteEventSourceSubscription(request, ctx) {
|
|
|
6968
6947
|
return json(result);
|
|
6969
6948
|
}
|
|
6970
6949
|
async function setTag(request, ctx) {
|
|
6971
|
-
const principalMutationError = rejectPrincipalEntityMutation(request, `
|
|
6950
|
+
const principalMutationError = rejectPrincipalEntityMutation(request, `tag updated`);
|
|
6972
6951
|
if (principalMutationError) return principalMutationError;
|
|
6973
6952
|
const parsed = routeBody(request);
|
|
6974
6953
|
const { entityUrl } = requireExistingEntityRoute(request);
|
|
@@ -6976,12 +6955,12 @@ async function setTag(request, ctx) {
|
|
|
6976
6955
|
const updated = await ctx.entityManager.setTag(entityUrl, decodeURIComponent(request.params.tagKey), { value: parsed.value }, token);
|
|
6977
6956
|
return json(toPublicEntity(updated));
|
|
6978
6957
|
}
|
|
6979
|
-
async function
|
|
6980
|
-
const principalMutationError = rejectPrincipalEntityMutation(request, `
|
|
6958
|
+
async function deleteTag(request, ctx) {
|
|
6959
|
+
const principalMutationError = rejectPrincipalEntityMutation(request, `tag deleted`);
|
|
6981
6960
|
if (principalMutationError) return principalMutationError;
|
|
6982
6961
|
const { entityUrl } = requireExistingEntityRoute(request);
|
|
6983
6962
|
const token = writeTokenFromRequest(request);
|
|
6984
|
-
const updated = await ctx.entityManager.
|
|
6963
|
+
const updated = await ctx.entityManager.deleteTag(entityUrl, decodeURIComponent(request.params.tagKey), token);
|
|
6985
6964
|
return json(toPublicEntity(updated));
|
|
6986
6965
|
}
|
|
6987
6966
|
async function forkEntity(request, ctx) {
|
|
@@ -7113,17 +7092,13 @@ const registerEntityTypeBodySchema = Type.Object({
|
|
|
7113
7092
|
creation_schema: Type.Optional(jsonObjectSchema),
|
|
7114
7093
|
inbox_schemas: Type.Optional(schemaMapSchema),
|
|
7115
7094
|
state_schemas: Type.Optional(schemaMapSchema),
|
|
7116
|
-
input_schemas: Type.Optional(schemaMapSchema),
|
|
7117
|
-
output_schemas: Type.Optional(schemaMapSchema),
|
|
7118
7095
|
serve_endpoint: Type.Optional(Type.String()),
|
|
7119
7096
|
default_dispatch_policy: Type.Optional(dispatchPolicySchema)
|
|
7120
|
-
});
|
|
7097
|
+
}, { additionalProperties: false });
|
|
7121
7098
|
const amendEntityTypeSchemasBodySchema = Type.Object({
|
|
7122
|
-
input_schemas: Type.Optional(schemaMapSchema),
|
|
7123
|
-
output_schemas: Type.Optional(schemaMapSchema),
|
|
7124
7099
|
inbox_schemas: Type.Optional(schemaMapSchema),
|
|
7125
7100
|
state_schemas: Type.Optional(schemaMapSchema)
|
|
7126
|
-
});
|
|
7101
|
+
}, { additionalProperties: false });
|
|
7127
7102
|
const entityTypesRouter = Router({ base: `/_electric/entity-types` });
|
|
7128
7103
|
entityTypesRouter.get(`/`, listEntityTypes);
|
|
7129
7104
|
entityTypesRouter.post(`/`, withSchema(registerEntityTypeBodySchema), registerEntityType);
|
|
@@ -7163,8 +7138,8 @@ async function getEntityType(request, ctx) {
|
|
|
7163
7138
|
async function amendSchemas(request, ctx) {
|
|
7164
7139
|
const parsed = routeBody(request);
|
|
7165
7140
|
const updated = await ctx.entityManager.amendSchemas(request.params.name, {
|
|
7166
|
-
inbox_schemas: parsed.inbox_schemas
|
|
7167
|
-
state_schemas: parsed.state_schemas
|
|
7141
|
+
inbox_schemas: parsed.inbox_schemas,
|
|
7142
|
+
state_schemas: parsed.state_schemas
|
|
7168
7143
|
});
|
|
7169
7144
|
return json(toPublicEntityType(updated));
|
|
7170
7145
|
}
|
|
@@ -7174,13 +7149,12 @@ async function deleteEntityType(request, ctx) {
|
|
|
7174
7149
|
}
|
|
7175
7150
|
function normalizeEntityTypeRequest(parsed) {
|
|
7176
7151
|
const serveEndpoint = rewriteLoopbackWebhookUrl(parsed.serve_endpoint);
|
|
7177
|
-
const compatibilityFields = parsed;
|
|
7178
7152
|
return {
|
|
7179
7153
|
name: parsed.name ?? ``,
|
|
7180
7154
|
description: parsed.description ?? ``,
|
|
7181
7155
|
creation_schema: parsed.creation_schema,
|
|
7182
|
-
inbox_schemas: parsed.inbox_schemas
|
|
7183
|
-
state_schemas: parsed.state_schemas
|
|
7156
|
+
inbox_schemas: parsed.inbox_schemas,
|
|
7157
|
+
state_schemas: parsed.state_schemas,
|
|
7184
7158
|
serve_endpoint: serveEndpoint,
|
|
7185
7159
|
default_dispatch_policy: parsed.default_dispatch_policy ?? (serveEndpoint ? { targets: [{
|
|
7186
7160
|
type: `webhook`,
|
|
@@ -7191,8 +7165,6 @@ function normalizeEntityTypeRequest(parsed) {
|
|
|
7191
7165
|
function toPublicEntityType(entityType) {
|
|
7192
7166
|
return {
|
|
7193
7167
|
...entityType,
|
|
7194
|
-
input_schemas: entityType.inbox_schemas,
|
|
7195
|
-
output_schemas: entityType.state_schemas,
|
|
7196
7168
|
revision: entityType.revision
|
|
7197
7169
|
};
|
|
7198
7170
|
}
|
|
@@ -7276,13 +7248,35 @@ function errorMapper(err, req) {
|
|
|
7276
7248
|
function rejectIfShuttingDown(req, ctx) {
|
|
7277
7249
|
if (!ctx.isShuttingDown()) return void 0;
|
|
7278
7250
|
const path$1 = new URL(req.url).pathname;
|
|
7279
|
-
if (!path$1.startsWith(`/_electric/
|
|
7251
|
+
if (!path$1.startsWith(`/_electric/subscription-webhooks/`)) return void 0;
|
|
7280
7252
|
return apiError(503, `SERVER_STOPPING`, `Server is shutting down`);
|
|
7281
7253
|
}
|
|
7282
7254
|
function getRequestSpan(req) {
|
|
7283
7255
|
return carrier(req)[SPAN_KEY];
|
|
7284
7256
|
}
|
|
7285
7257
|
|
|
7258
|
+
//#endregion
|
|
7259
|
+
//#region src/routing/observations-router.ts
|
|
7260
|
+
const stringRecordSchema = Type.Record(Type.String(), Type.String());
|
|
7261
|
+
const ensureEntitiesMembershipStreamBodySchema = Type.Object({ tags: Type.Optional(stringRecordSchema) });
|
|
7262
|
+
const ensureCronStreamBodySchema = Type.Object({
|
|
7263
|
+
expression: Type.String(),
|
|
7264
|
+
timezone: Type.Optional(Type.String())
|
|
7265
|
+
});
|
|
7266
|
+
const observationsRouter = Router({ base: `/_electric/observations` });
|
|
7267
|
+
observationsRouter.post(`/entities/ensure-stream`, withSchema(ensureEntitiesMembershipStreamBodySchema), ensureEntitiesMembershipStream);
|
|
7268
|
+
observationsRouter.post(`/cron/ensure-stream`, withSchema(ensureCronStreamBodySchema), ensureCronStream);
|
|
7269
|
+
async function ensureEntitiesMembershipStream(request, ctx) {
|
|
7270
|
+
const parsed = routeBody(request);
|
|
7271
|
+
const result = await ctx.entityManager.ensureEntitiesMembershipStream(parsed.tags ?? {});
|
|
7272
|
+
return json(result);
|
|
7273
|
+
}
|
|
7274
|
+
async function ensureCronStream(request, ctx) {
|
|
7275
|
+
const parsed = routeBody(request);
|
|
7276
|
+
const streamPath = await ctx.entityManager.getOrCreateCronStream(parsed.expression, parsed.timezone);
|
|
7277
|
+
return json({ streamUrl: streamPath });
|
|
7278
|
+
}
|
|
7279
|
+
|
|
7286
7280
|
//#endregion
|
|
7287
7281
|
//#region src/routing/tenant-stream-paths.ts
|
|
7288
7282
|
function withLeadingSlash(path$1) {
|
|
@@ -7621,7 +7615,7 @@ async function notificationFromClaim(ctx, input) {
|
|
|
7621
7615
|
wakeId: input.claim.wake_id,
|
|
7622
7616
|
streamPath: primaryStream,
|
|
7623
7617
|
streams: streams$1,
|
|
7624
|
-
callback: appendPathToUrl(ctx.publicUrl, `/_electric/
|
|
7618
|
+
callback: appendPathToUrl(ctx.publicUrl, `/_electric/wake-callbacks/${encodeURIComponent(input.claim.wake_id)}`),
|
|
7625
7619
|
claimToken: input.claim.token,
|
|
7626
7620
|
triggerEvent: `message_received`,
|
|
7627
7621
|
entity: {
|
|
@@ -7656,7 +7650,7 @@ const wakeRegistrationBodySchema = Type.Object({
|
|
|
7656
7650
|
includeResponse: Type.Optional(Type.Boolean()),
|
|
7657
7651
|
manifestKey: Type.Optional(Type.String())
|
|
7658
7652
|
});
|
|
7659
|
-
const
|
|
7653
|
+
const subscriptionWebhookBodySchema = Type.Object({
|
|
7660
7654
|
subscription_id: Type.Optional(Type.String()),
|
|
7661
7655
|
wake_id: Type.Optional(Type.String()),
|
|
7662
7656
|
generation: Type.Optional(Type.Number()),
|
|
@@ -7670,7 +7664,7 @@ const webhookForwardBodySchema = Type.Object({
|
|
|
7670
7664
|
consumer_id: Type.Optional(Type.String()),
|
|
7671
7665
|
callback: Type.Optional(Type.String())
|
|
7672
7666
|
}, { additionalProperties: true });
|
|
7673
|
-
const
|
|
7667
|
+
const wakeCallbackBodySchema = Type.Object({
|
|
7674
7668
|
epoch: Type.Optional(Type.Number()),
|
|
7675
7669
|
generation: Type.Optional(Type.Number()),
|
|
7676
7670
|
wakeId: Type.Optional(Type.String()),
|
|
@@ -7683,13 +7677,13 @@ const internalRouter = Router({ base: `/_electric` });
|
|
|
7683
7677
|
internalRouter.get(`/health`, () => json({ status: `ok` }));
|
|
7684
7678
|
internalRouter.get(`/event-sources`, listEventSources);
|
|
7685
7679
|
internalRouter.post(`/wake`, withSchema(wakeRegistrationBodySchema), registerWake);
|
|
7686
|
-
internalRouter.post(`/
|
|
7687
|
-
internalRouter.post(`/
|
|
7680
|
+
internalRouter.post(`/subscription-webhooks/:subscriptionId`, subscriptionWebhook);
|
|
7681
|
+
internalRouter.post(`/wake-callbacks/:consumerId`, wakeCallback);
|
|
7688
7682
|
internalRouter.all(`/runners`, runnersRouter.fetch);
|
|
7689
7683
|
internalRouter.all(`/runners/*`, runnersRouter.fetch);
|
|
7690
7684
|
internalRouter.all(`/entities/*`, entitiesRouter.fetch);
|
|
7691
7685
|
internalRouter.all(`/entity-types/*`, entityTypesRouter.fetch);
|
|
7692
|
-
internalRouter.all(`/
|
|
7686
|
+
internalRouter.all(`/observations/*`, observationsRouter.fetch);
|
|
7693
7687
|
internalRouter.get(`/electric/*`, electricProxyRouter.fetch);
|
|
7694
7688
|
internalRouter.all(`*`, () => status(404));
|
|
7695
7689
|
function routeParam(request, name) {
|
|
@@ -7722,13 +7716,30 @@ function resolveWebhookSigner(ctx) {
|
|
|
7722
7716
|
return ctx.webhookSigner ?? getDefaultWebhookSigner();
|
|
7723
7717
|
}
|
|
7724
7718
|
function durableStreamsWebhookJwksUrl(ctx) {
|
|
7725
|
-
if (!ctx.durableStreamsRouting) return
|
|
7719
|
+
if (!ctx.durableStreamsRouting) return appendPathToBackendUrl(ctx.durableStreamsUrl, `/__ds/jwks.json`);
|
|
7726
7720
|
return resolveDurableStreamsRoutingAdapter(ctx.durableStreamsRouting, ctx.durableStreamsUrl).controlUrl({
|
|
7727
7721
|
durableStreamsUrl: ctx.durableStreamsUrl,
|
|
7728
7722
|
serviceId: ctx.service,
|
|
7729
7723
|
requestUrl: appendPathToUrl(ctx.publicUrl, `/__ds/jwks.json`)
|
|
7730
7724
|
}).toString();
|
|
7731
7725
|
}
|
|
7726
|
+
function appendPathToBackendUrl(baseUrl, path$1) {
|
|
7727
|
+
const base = new URL(baseUrl);
|
|
7728
|
+
const pathUrl = new URL(path$1, `http://electric-agents.local`);
|
|
7729
|
+
const basePath = base.pathname === `/` ? `` : base.pathname.replace(/\/+$/, ``);
|
|
7730
|
+
const suffix = pathUrl.pathname.startsWith(`/`) ? pathUrl.pathname : `/${pathUrl.pathname}`;
|
|
7731
|
+
const target = new URL(base);
|
|
7732
|
+
target.pathname = `${basePath}${suffix}`;
|
|
7733
|
+
target.search = ``;
|
|
7734
|
+
target.hash = pathUrl.hash;
|
|
7735
|
+
base.searchParams.forEach((value, key) => {
|
|
7736
|
+
target.searchParams.append(key, value);
|
|
7737
|
+
});
|
|
7738
|
+
pathUrl.searchParams.forEach((value, key) => {
|
|
7739
|
+
target.searchParams.append(key, value);
|
|
7740
|
+
});
|
|
7741
|
+
return target.toString();
|
|
7742
|
+
}
|
|
7732
7743
|
function durableStreamsJwksFetchClient(ctx) {
|
|
7733
7744
|
return async (input, init) => {
|
|
7734
7745
|
const headers = new Headers(init?.headers);
|
|
@@ -7792,10 +7803,10 @@ async function listEventSources(_request, ctx) {
|
|
|
7792
7803
|
function isAgentVisibleEventSource(source) {
|
|
7793
7804
|
return source.agentVisible === true && source.status === `active`;
|
|
7794
7805
|
}
|
|
7795
|
-
async function
|
|
7806
|
+
async function subscriptionWebhook(request, ctx) {
|
|
7796
7807
|
const subscriptionId = routeParam(request, `subscriptionId`);
|
|
7797
7808
|
const rootSpan = getRequestSpan(request);
|
|
7798
|
-
rootSpan?.updateName(`webhook
|
|
7809
|
+
rootSpan?.updateName(`subscription-webhook`);
|
|
7799
7810
|
rootSpan?.setAttribute(`electric_agents.webhook.subscription_id`, subscriptionId);
|
|
7800
7811
|
const body = await readRequestBody(request);
|
|
7801
7812
|
const signatureError = await verifyDurableStreamsWebhook(request, ctx, body);
|
|
@@ -7809,7 +7820,7 @@ async function webhookForward(request, ctx) {
|
|
|
7809
7820
|
}
|
|
7810
7821
|
});
|
|
7811
7822
|
if (!targetWebhookUrl) return apiError(404, ErrCodeSubscriptionNotFound, `Unknown webhook subscription`);
|
|
7812
|
-
const parsedBodyResult = validateOptionalJsonBody(
|
|
7823
|
+
const parsedBodyResult = validateOptionalJsonBody(subscriptionWebhookBodySchema, body, request.headers.get(`content-type`));
|
|
7813
7824
|
if (!parsedBodyResult.ok) return parsedBodyResult.response;
|
|
7814
7825
|
let forwardBody = body;
|
|
7815
7826
|
let runningEntityUrl = null;
|
|
@@ -7855,7 +7866,7 @@ async function webhookForward(request, ctx) {
|
|
|
7855
7866
|
span.end();
|
|
7856
7867
|
}
|
|
7857
7868
|
}).catch((err) => {
|
|
7858
|
-
serverLog.warn(`[webhook
|
|
7869
|
+
serverLog.warn(`[subscription-webhook] consumerCallbacks upsert failed (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
|
|
7859
7870
|
}) : void 0;
|
|
7860
7871
|
const [entity, enriched] = await Promise.all([entityPromise, enrichPromise]);
|
|
7861
7872
|
if (entity?.status === `stopped` || entity?.status === `paused`) {
|
|
@@ -7876,7 +7887,7 @@ async function webhookForward(request, ctx) {
|
|
|
7876
7887
|
runningEntityUrl = entity.url;
|
|
7877
7888
|
}
|
|
7878
7889
|
if (consumerId && callbackUrl) {
|
|
7879
|
-
const callback = appendPathToUrl(ctx.publicUrl, `/_electric/
|
|
7890
|
+
const callback = appendPathToUrl(ctx.publicUrl, `/_electric/wake-callbacks/${encodeURIComponent(consumerId)}`);
|
|
7880
7891
|
enriched.callback = callback;
|
|
7881
7892
|
if (newWebhook) {
|
|
7882
7893
|
enriched.consumerId = newWebhook.wakeId;
|
|
@@ -7913,21 +7924,21 @@ async function webhookForward(request, ctx) {
|
|
|
7913
7924
|
});
|
|
7914
7925
|
} catch (err) {
|
|
7915
7926
|
if (runningEntityUrl) await ctx.entityManager.registry.updateStatus(runningEntityUrl, `idle`);
|
|
7916
|
-
return apiError(502, `
|
|
7927
|
+
return apiError(502, `SUBSCRIPTION_WEBHOOK_FAILED`, err instanceof Error ? err.message : String(err));
|
|
7917
7928
|
}
|
|
7918
7929
|
const responseBytes = upstream.body ? new Uint8Array(await upstream.arrayBuffer()) : new Uint8Array();
|
|
7919
7930
|
return responseFromUpstream(upstream, responseBytes);
|
|
7920
7931
|
}
|
|
7921
|
-
async function
|
|
7932
|
+
async function wakeCallback(request, ctx) {
|
|
7922
7933
|
const consumerId = routeParam(request, `consumerId`);
|
|
7923
7934
|
const rows = await ctx.pgDb.select().from(consumerCallbacks).where(and(eq(consumerCallbacks.tenantId, ctx.service), eq(consumerCallbacks.consumerId, consumerId))).limit(1);
|
|
7924
7935
|
const target = rows[0] ? {
|
|
7925
7936
|
callbackUrl: rows[0].callbackUrl,
|
|
7926
7937
|
primaryStream: rows[0].primaryStream
|
|
7927
7938
|
} : void 0;
|
|
7928
|
-
if (!target) return apiError(404,
|
|
7939
|
+
if (!target) return apiError(404, ErrCodeWakeCallbackNotFound, `Unknown wake-callback consumer`);
|
|
7929
7940
|
const body = await readRequestBody(request);
|
|
7930
|
-
const parsedBodyResult = validateOptionalJsonBody(
|
|
7941
|
+
const parsedBodyResult = validateOptionalJsonBody(wakeCallbackBodySchema, body, request.headers.get(`content-type`));
|
|
7931
7942
|
if (!parsedBodyResult.ok) return parsedBodyResult.response;
|
|
7932
7943
|
const requestBody = parsedBodyResult.value;
|
|
7933
7944
|
const isClaimRequest = requestBody?.wakeId !== void 0 || requestBody?.wake_id !== void 0;
|
|
@@ -7945,14 +7956,14 @@ async function callbackForward(request, ctx) {
|
|
|
7945
7956
|
}
|
|
7946
7957
|
return json(responseBody);
|
|
7947
7958
|
}
|
|
7948
|
-
const upstreamBody =
|
|
7959
|
+
const upstreamBody = encodeWakeCallbackBody(ctx.service, consumerId, requestBody, resolveDurableStreamsRoutingAdapter(ctx.durableStreamsRouting, ctx.durableStreamsUrl));
|
|
7949
7960
|
let upstream;
|
|
7950
7961
|
try {
|
|
7951
7962
|
const subscriptionId = durableStreamsSubscriptionCallback(target.callbackUrl);
|
|
7952
7963
|
if (subscriptionId) {
|
|
7953
7964
|
const token = claimTokenFromRequest(request);
|
|
7954
7965
|
if (!token) return apiError(401, `UNAUTHORIZED`, `Missing claim token`);
|
|
7955
|
-
const upstreamPayload =
|
|
7966
|
+
const upstreamPayload = encodeWakeCallbackPayload(consumerId, requestBody, (stream) => stream.replace(/^\/+/, ``));
|
|
7956
7967
|
const result = await ctx.streamClient.ackSubscription(subscriptionId, token, upstreamPayload);
|
|
7957
7968
|
upstream = json(result);
|
|
7958
7969
|
} else upstream = await fetch(target.callbackUrl, {
|
|
@@ -7961,7 +7972,7 @@ async function callbackForward(request, ctx) {
|
|
|
7961
7972
|
body: bodyFromBytes(upstreamBody)
|
|
7962
7973
|
});
|
|
7963
7974
|
} catch (err) {
|
|
7964
|
-
return apiError(502, `
|
|
7975
|
+
return apiError(502, `WAKE_CALLBACK_FAILED`, err instanceof Error ? err.message : String(err));
|
|
7965
7976
|
}
|
|
7966
7977
|
let responseBytes = upstream.body ? new Uint8Array(await upstream.arrayBuffer()) : new Uint8Array();
|
|
7967
7978
|
if (isClaimRequest && upstream.ok && target.primaryStream) {
|
|
@@ -7981,7 +7992,7 @@ async function callbackForward(request, ctx) {
|
|
|
7981
7992
|
epoch
|
|
7982
7993
|
});
|
|
7983
7994
|
if (upstream.ok && isDoneRequest && target.primaryStream) {
|
|
7984
|
-
serverLog.info(`[callback
|
|
7995
|
+
serverLog.info(`[wake-callback] done received for stream=${target.primaryStream} consumer=${consumerId}`);
|
|
7985
7996
|
const stillOwnsClaim = ctx.runtime.claimWriteTokens.owns(ctx.service, target.primaryStream, consumerId);
|
|
7986
7997
|
const entity = await ctx.entityManager.registry.getEntityByStream(target.primaryStream);
|
|
7987
7998
|
let entityCleared = false;
|
|
@@ -8003,13 +8014,13 @@ async function callbackForward(request, ctx) {
|
|
|
8003
8014
|
if (entity && (entityCleared || stillOwnsClaim)) {
|
|
8004
8015
|
await ctx.entityManager.registry.updateStatus(entity.url, entity.status === `stopping` ? `stopped` : `idle`);
|
|
8005
8016
|
await ctx.entityBridgeManager.onEntityChanged(entity.url);
|
|
8006
|
-
serverLog.info(`[callback
|
|
8007
|
-
} else if (!entity) serverLog.warn(`[callback
|
|
8017
|
+
serverLog.info(`[wake-callback] status updated after done for ${entity.url}`);
|
|
8018
|
+
} else if (!entity) serverLog.warn(`[wake-callback] done received but no entity found for stream=${target.primaryStream}`);
|
|
8008
8019
|
if (stillOwnsClaim) ctx.runtime.claimWriteTokens.clearStream(ctx.service, target.primaryStream);
|
|
8009
|
-
else if (entity) serverLog.info(`[callback
|
|
8010
|
-
} else if (requestBody?.done === true) serverLog.warn(`[callback
|
|
8020
|
+
else if (entity) serverLog.info(`[wake-callback] done arrived after in-memory token evicted (stream=${target.primaryStream} consumer=${consumerId})`);
|
|
8021
|
+
} else if (requestBody?.done === true) serverLog.warn(`[wake-callback] done received but skipped: upstream.ok=${upstream.ok} primaryStream=${target.primaryStream ?? `null`} consumer=${consumerId}`);
|
|
8011
8022
|
} catch (err) {
|
|
8012
|
-
serverLog.error(`[callback
|
|
8023
|
+
serverLog.error(`[wake-callback] error processing done for consumer=${consumerId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
8013
8024
|
}
|
|
8014
8025
|
return responseFromUpstream(upstream, responseBytes);
|
|
8015
8026
|
}
|
|
@@ -8018,11 +8029,11 @@ async function mintClaimWriteToken(ctx, streamPath, consumerId) {
|
|
|
8018
8029
|
if (!entity) return void 0;
|
|
8019
8030
|
return ctx.runtime.claimWriteTokens.mint(ctx.service, streamPath, consumerId);
|
|
8020
8031
|
}
|
|
8021
|
-
function
|
|
8022
|
-
const payload =
|
|
8032
|
+
function encodeWakeCallbackBody(service, consumerId, body, routingAdapter) {
|
|
8033
|
+
const payload = encodeWakeCallbackPayload(consumerId, body, (stream) => routingAdapter.toBackendStreamPath(service, stream));
|
|
8023
8034
|
return new TextEncoder().encode(JSON.stringify(payload));
|
|
8024
8035
|
}
|
|
8025
|
-
function
|
|
8036
|
+
function encodeWakeCallbackPayload(consumerId, body, mapStream) {
|
|
8026
8037
|
if (!body) return {};
|
|
8027
8038
|
const generation = body.generation ?? body.epoch;
|
|
8028
8039
|
const wakeId = body.wake_id ?? body.wakeId ?? consumerId;
|
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.11",
|
|
4
4
|
"description": "Electric Agents entity runtime server",
|
|
5
5
|
"author": "Durable Stream contributors",
|
|
6
6
|
"bin": {
|
|
@@ -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.5"
|
|
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.
|
|
69
|
-
"@electric-ax/agents-server-
|
|
70
|
-
"@electric-ax/agents-server-
|
|
68
|
+
"@electric-ax/agents": "0.4.9",
|
|
69
|
+
"@electric-ax/agents-server-ui": "0.4.11",
|
|
70
|
+
"@electric-ax/agents-server-conformance-tests": "0.1.8"
|
|
71
71
|
},
|
|
72
72
|
"files": [
|
|
73
73
|
"dist",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { DEFAULT_STATE_SCHEMAS } from '@electric-ax/agents-runtime'
|
|
@@ -457,4 +457,4 @@ export const ErrCodeForkWaitTimeout = `FORK_WAIT_TIMEOUT`
|
|
|
457
457
|
export const ErrCodeEntityPersistFailed = `ENTITY_PERSIST_FAILED`
|
|
458
458
|
export const ErrCodeAgentUiNotFound = `AGENT_UI_NOT_FOUND`
|
|
459
459
|
export const ErrCodeSubscriptionNotFound = `SUBSCRIPTION_NOT_FOUND`
|
|
460
|
-
export const
|
|
460
|
+
export const ErrCodeWakeCallbackNotFound = `WAKE_CALLBACK_NOT_FOUND`
|
package/src/entity-manager.ts
CHANGED
|
@@ -1675,7 +1675,7 @@ export class EntityManager {
|
|
|
1675
1675
|
// ==========================================================================
|
|
1676
1676
|
|
|
1677
1677
|
/**
|
|
1678
|
-
* Deliver a message to an entity's main stream, with optional
|
|
1678
|
+
* Deliver a message to an entity's main stream, with optional inbox schema
|
|
1679
1679
|
* validation.
|
|
1680
1680
|
*/
|
|
1681
1681
|
async send(
|
|
@@ -1888,7 +1888,7 @@ export class EntityManager {
|
|
|
1888
1888
|
return updated
|
|
1889
1889
|
}
|
|
1890
1890
|
|
|
1891
|
-
async
|
|
1891
|
+
async deleteTag(
|
|
1892
1892
|
entityUrl: string,
|
|
1893
1893
|
key: string,
|
|
1894
1894
|
token: string
|
|
@@ -1930,7 +1930,7 @@ export class EntityManager {
|
|
|
1930
1930
|
return updated
|
|
1931
1931
|
}
|
|
1932
1932
|
|
|
1933
|
-
async
|
|
1933
|
+
async ensureEntitiesMembershipStream(tags: Record<string, string>): Promise<{
|
|
1934
1934
|
sourceRef: string
|
|
1935
1935
|
streamUrl: string
|
|
1936
1936
|
}> {
|
|
@@ -2742,7 +2742,7 @@ export class EntityManager {
|
|
|
2742
2742
|
// ==========================================================================
|
|
2743
2743
|
|
|
2744
2744
|
/**
|
|
2745
|
-
* Add new
|
|
2745
|
+
* Add new inbox/state schema keys to an entity type directly in Postgres.
|
|
2746
2746
|
*/
|
|
2747
2747
|
async amendSchemas(
|
|
2748
2748
|
typeName: string,
|
|
@@ -2831,7 +2831,7 @@ export class EntityManager {
|
|
|
2831
2831
|
|
|
2832
2832
|
/**
|
|
2833
2833
|
* Enrich webhook payload with entity context.
|
|
2834
|
-
* Called by ElectricAgentsServer during webhook
|
|
2834
|
+
* Called by ElectricAgentsServer during subscription webhook dispatch to inject entity context.
|
|
2835
2835
|
*/
|
|
2836
2836
|
async enrichPayload(
|
|
2837
2837
|
payload: Record<string, unknown>,
|
|
@@ -326,7 +326,7 @@ async function linkStreamToTargetSubscription(
|
|
|
326
326
|
}
|
|
327
327
|
const forwardUrl = appendPathToUrl(
|
|
328
328
|
ctx.publicUrl,
|
|
329
|
-
`/_electric/
|
|
329
|
+
`/_electric/subscription-webhooks/${encodeURIComponent(subscriptionId)}`
|
|
330
330
|
)
|
|
331
331
|
await ensureSubscriptionIncludesStream(
|
|
332
332
|
ctx,
|
|
@@ -332,7 +332,7 @@ async function rewriteSubscriptionRequestBody(
|
|
|
332
332
|
targetWebhookUrl = rewriteLoopbackWebhookUrl(payload.webhook.url) ?? null
|
|
333
333
|
payload.webhook.url = appendPathToUrl(
|
|
334
334
|
ctx.publicUrl,
|
|
335
|
-
`/_electric/
|
|
335
|
+
`/_electric/subscription-webhooks/${encodeURIComponent(subscriptionId)}`
|
|
336
336
|
)
|
|
337
337
|
}
|
|
338
338
|
|
|
@@ -13,9 +13,7 @@ export interface DurableStreamsRoutingAdapter {
|
|
|
13
13
|
|
|
14
14
|
function appendSearch(target: URL, source: URL): URL {
|
|
15
15
|
source.searchParams.forEach((value, key) => {
|
|
16
|
-
|
|
17
|
-
target.searchParams.append(key, value)
|
|
18
|
-
}
|
|
16
|
+
target.searchParams.append(key, value)
|
|
19
17
|
})
|
|
20
18
|
return target
|
|
21
19
|
}
|