@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.cjs
CHANGED
|
@@ -451,7 +451,7 @@ const ErrCodeForkInProgress = `FORK_IN_PROGRESS`;
|
|
|
451
451
|
const ErrCodeForkWaitTimeout = `FORK_WAIT_TIMEOUT`;
|
|
452
452
|
const ErrCodeEntityPersistFailed = `ENTITY_PERSIST_FAILED`;
|
|
453
453
|
const ErrCodeSubscriptionNotFound = `SUBSCRIPTION_NOT_FOUND`;
|
|
454
|
-
const
|
|
454
|
+
const ErrCodeWakeCallbackNotFound = `WAKE_CALLBACK_NOT_FOUND`;
|
|
455
455
|
|
|
456
456
|
//#endregion
|
|
457
457
|
//#region src/tenant.ts
|
|
@@ -2532,7 +2532,7 @@ async function linkStreamToTargetSubscription(ctx, target, entity, subscriptionI
|
|
|
2532
2532
|
}
|
|
2533
2533
|
const webhookUrl = rewriteLoopbackWebhookUrl(target.url);
|
|
2534
2534
|
if (!webhookUrl) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Webhook dispatch target must include a valid URL`, 400);
|
|
2535
|
-
const forwardUrl = (0, __electric_ax_agents_runtime.appendPathToUrl)(ctx.publicUrl, `/_electric/
|
|
2535
|
+
const forwardUrl = (0, __electric_ax_agents_runtime.appendPathToUrl)(ctx.publicUrl, `/_electric/subscription-webhooks/${encodeURIComponent(subscriptionId)}`);
|
|
2536
2536
|
await ensureSubscriptionIncludesStream(ctx, subscriptionId, streamPath, {
|
|
2537
2537
|
type: `webhook`,
|
|
2538
2538
|
streams: [streamPath],
|
|
@@ -3617,7 +3617,7 @@ var EntityManager = class {
|
|
|
3617
3617
|
};
|
|
3618
3618
|
}
|
|
3619
3619
|
/**
|
|
3620
|
-
* Deliver a message to an entity's main stream, with optional
|
|
3620
|
+
* Deliver a message to an entity's main stream, with optional inbox schema
|
|
3621
3621
|
* validation.
|
|
3622
3622
|
*/
|
|
3623
3623
|
async send(entityUrl, req, opts) {
|
|
@@ -3705,7 +3705,7 @@ var EntityManager = class {
|
|
|
3705
3705
|
if (result.changed && this.entityBridgeManager) await this.entityBridgeManager.onEntityChanged(entityUrl);
|
|
3706
3706
|
return updated;
|
|
3707
3707
|
}
|
|
3708
|
-
async
|
|
3708
|
+
async deleteTag(entityUrl, key, token) {
|
|
3709
3709
|
const entity = await this.registry.getEntity(entityUrl);
|
|
3710
3710
|
if (!entity) throw new ElectricAgentsError(ErrCodeNotFound, `Entity not found`, 404);
|
|
3711
3711
|
if (!this.isValidWriteToken(entity, token)) throw new ElectricAgentsError(ErrCodeUnauthorized, `Invalid write token`, 401);
|
|
@@ -3716,7 +3716,7 @@ var EntityManager = class {
|
|
|
3716
3716
|
if (result.changed && this.entityBridgeManager) await this.entityBridgeManager.onEntityChanged(entityUrl);
|
|
3717
3717
|
return updated;
|
|
3718
3718
|
}
|
|
3719
|
-
async
|
|
3719
|
+
async ensureEntitiesMembershipStream(tags) {
|
|
3720
3720
|
if (!this.entityBridgeManager) throw new Error(`Entity bridge manager not configured`);
|
|
3721
3721
|
return this.entityBridgeManager.register(this.validateTags(tags));
|
|
3722
3722
|
}
|
|
@@ -4145,7 +4145,7 @@ var EntityManager = class {
|
|
|
4145
4145
|
return null;
|
|
4146
4146
|
}
|
|
4147
4147
|
/**
|
|
4148
|
-
* Add new
|
|
4148
|
+
* Add new inbox/state schema keys to an entity type directly in Postgres.
|
|
4149
4149
|
*/
|
|
4150
4150
|
async amendSchemas(typeName, schemas) {
|
|
4151
4151
|
if (typeName === `principal`) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Entity type "principal" is built in and cannot be amended`, 400);
|
|
@@ -4185,7 +4185,7 @@ var EntityManager = class {
|
|
|
4185
4185
|
}
|
|
4186
4186
|
/**
|
|
4187
4187
|
* Enrich webhook payload with entity context.
|
|
4188
|
-
* Called by ElectricAgentsServer during webhook
|
|
4188
|
+
* Called by ElectricAgentsServer during subscription webhook dispatch to inject entity context.
|
|
4189
4189
|
*/
|
|
4190
4190
|
async enrichPayload(payload, consumer) {
|
|
4191
4191
|
const entity = await this.registry.getEntityByStream(consumer.primary_stream);
|
|
@@ -5444,7 +5444,7 @@ var WakeRegistry = class {
|
|
|
5444
5444
|
try {
|
|
5445
5445
|
for (const message of messages) {
|
|
5446
5446
|
await this.applyShapeMessage(message);
|
|
5447
|
-
if (!settled &&
|
|
5447
|
+
if (!settled && (0, __electric_sql_client.isControlMessage)(message) && message.headers.control === `up-to-date`) {
|
|
5448
5448
|
settled = true;
|
|
5449
5449
|
resolve$1();
|
|
5450
5450
|
}
|
|
@@ -6231,7 +6231,7 @@ function validateParsedBody(schema, parsed) {
|
|
|
6231
6231
|
//#region src/routing/durable-streams-routing-adapter.ts
|
|
6232
6232
|
function appendSearch(target, source) {
|
|
6233
6233
|
source.searchParams.forEach((value, key) => {
|
|
6234
|
-
|
|
6234
|
+
target.searchParams.append(key, value);
|
|
6235
6235
|
});
|
|
6236
6236
|
return target;
|
|
6237
6237
|
}
|
|
@@ -6551,7 +6551,7 @@ async function rewriteSubscriptionRequestBody(request, ctx, subscriptionId, rout
|
|
|
6551
6551
|
let targetWebhookUrl = null;
|
|
6552
6552
|
if (payload.webhook?.url !== void 0) {
|
|
6553
6553
|
targetWebhookUrl = rewriteLoopbackWebhookUrl(payload.webhook.url) ?? null;
|
|
6554
|
-
payload.webhook.url = (0, __electric_ax_agents_runtime.appendPathToUrl)(ctx.publicUrl, `/_electric/
|
|
6554
|
+
payload.webhook.url = (0, __electric_ax_agents_runtime.appendPathToUrl)(ctx.publicUrl, `/_electric/subscription-webhooks/${encodeURIComponent(subscriptionId)}`);
|
|
6555
6555
|
}
|
|
6556
6556
|
rewriteSubscriptionBodyForBackend(payload, ctx.service, routingAdapter);
|
|
6557
6557
|
return {
|
|
@@ -6674,20 +6674,6 @@ async function proxyPassThrough(request, ctx) {
|
|
|
6674
6674
|
}
|
|
6675
6675
|
}
|
|
6676
6676
|
|
|
6677
|
-
//#endregion
|
|
6678
|
-
//#region src/routing/cron-router.ts
|
|
6679
|
-
const cronRegisterBodySchema = __sinclair_typebox.Type.Object({
|
|
6680
|
-
expression: __sinclair_typebox.Type.String(),
|
|
6681
|
-
timezone: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String())
|
|
6682
|
-
});
|
|
6683
|
-
const cronRouter = (0, itty_router.Router)({ base: `/_electric/cron` });
|
|
6684
|
-
cronRouter.post(`/register`, withSchema(cronRegisterBodySchema), registerCron);
|
|
6685
|
-
async function registerCron(request, ctx) {
|
|
6686
|
-
const parsed = routeBody(request);
|
|
6687
|
-
const streamPath = await ctx.entityManager.getOrCreateCronStream(parsed.expression, parsed.timezone);
|
|
6688
|
-
return (0, itty_router.json)({ streamUrl: streamPath });
|
|
6689
|
-
}
|
|
6690
|
-
|
|
6691
6677
|
//#endregion
|
|
6692
6678
|
//#region src/routing/electric-proxy-router.ts
|
|
6693
6679
|
const electricProxyRouter = (0, itty_router.Router)({ base: `/_electric/electric` });
|
|
@@ -6721,7 +6707,7 @@ async function proxyElectric(request, ctx) {
|
|
|
6721
6707
|
|
|
6722
6708
|
//#endregion
|
|
6723
6709
|
//#region src/routing/entities-router.ts
|
|
6724
|
-
const stringRecordSchema = __sinclair_typebox.Type.Record(__sinclair_typebox.Type.String(), __sinclair_typebox.Type.String());
|
|
6710
|
+
const stringRecordSchema$1 = __sinclair_typebox.Type.Record(__sinclair_typebox.Type.String(), __sinclair_typebox.Type.String());
|
|
6725
6711
|
function writeTokenFromRequest(request) {
|
|
6726
6712
|
const electricClaimToken = request.headers.get(`electric-claim-token`)?.trim();
|
|
6727
6713
|
if (electricClaimToken) return electricClaimToken;
|
|
@@ -6738,7 +6724,7 @@ const wakeConditionSchema = __sinclair_typebox.Type.Union([__sinclair_typebox.Ty
|
|
|
6738
6724
|
})]);
|
|
6739
6725
|
const spawnBodySchema = __sinclair_typebox.Type.Object({
|
|
6740
6726
|
args: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Record(__sinclair_typebox.Type.String(), __sinclair_typebox.Type.Unknown())),
|
|
6741
|
-
tags: __sinclair_typebox.Type.Optional(stringRecordSchema),
|
|
6727
|
+
tags: __sinclair_typebox.Type.Optional(stringRecordSchema$1),
|
|
6742
6728
|
parent: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
|
|
6743
6729
|
dispatch_policy: __sinclair_typebox.Type.Optional(dispatchPolicySchema),
|
|
6744
6730
|
initialMessage: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Unknown()),
|
|
@@ -6830,10 +6816,8 @@ const eventSourceSubscriptionBodySchema = __sinclair_typebox.Type.Object({
|
|
|
6830
6816
|
lifetime: __sinclair_typebox.Type.Optional(subscriptionLifetimeSchema),
|
|
6831
6817
|
reason: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String())
|
|
6832
6818
|
});
|
|
6833
|
-
const entitiesRegisterBodySchema = __sinclair_typebox.Type.Object({ tags: __sinclair_typebox.Type.Optional(stringRecordSchema) });
|
|
6834
6819
|
const entitiesRouter = (0, itty_router.Router)({ base: `/_electric/entities` });
|
|
6835
6820
|
entitiesRouter.get(`/`, listEntities);
|
|
6836
|
-
entitiesRouter.post(`/register`, withSchema(entitiesRegisterBodySchema), registerEntitiesSource);
|
|
6837
6821
|
entitiesRouter.put(`/:type/:instanceId`, withSpawnableEntityType, withSchema(spawnBodySchema), spawnEntity);
|
|
6838
6822
|
entitiesRouter.get(`/:type/:instanceId`, withExistingEntity, getEntity);
|
|
6839
6823
|
entitiesRouter.head(`/:type/:instanceId`, withExistingEntity, headEntity);
|
|
@@ -6844,7 +6828,7 @@ entitiesRouter.patch(`/:type/:instanceId/inbox/:messageKey`, withExistingEntity,
|
|
|
6844
6828
|
entitiesRouter.delete(`/:type/:instanceId/inbox/:messageKey`, withExistingEntity, deleteInboxMessage);
|
|
6845
6829
|
entitiesRouter.post(`/:type/:instanceId/fork`, withExistingEntity, withSchema(forkBodySchema), forkEntity);
|
|
6846
6830
|
entitiesRouter.post(`/:type/:instanceId/tags/:tagKey`, withExistingEntity, withSchema(setTagBodySchema), setTag);
|
|
6847
|
-
entitiesRouter.delete(`/:type/:instanceId/tags/:tagKey`, withExistingEntity,
|
|
6831
|
+
entitiesRouter.delete(`/:type/:instanceId/tags/:tagKey`, withExistingEntity, deleteTag);
|
|
6848
6832
|
entitiesRouter.put(`/:type/:instanceId/schedules/:scheduleId`, withExistingEntity, withSchema(scheduleBodySchema), upsertSchedule);
|
|
6849
6833
|
entitiesRouter.delete(`/:type/:instanceId/schedules/:scheduleId`, withExistingEntity, deleteSchedule);
|
|
6850
6834
|
entitiesRouter.put(`/:type/:instanceId/event-source-subscriptions/:subscriptionId`, withExistingEntity, withSchema(eventSourceSubscriptionBodySchema), upsertEventSourceSubscription);
|
|
@@ -6912,11 +6896,6 @@ async function listEntities({ query }, ctx) {
|
|
|
6912
6896
|
});
|
|
6913
6897
|
return (0, itty_router.json)(entities$1.map((entity) => toPublicEntity(entity)));
|
|
6914
6898
|
}
|
|
6915
|
-
async function registerEntitiesSource(request, ctx) {
|
|
6916
|
-
const parsed = routeBody(request);
|
|
6917
|
-
const result = await ctx.entityManager.registerEntitiesSource(parsed.tags ?? {});
|
|
6918
|
-
return (0, itty_router.json)(result);
|
|
6919
|
-
}
|
|
6920
6899
|
async function upsertSchedule(request, ctx) {
|
|
6921
6900
|
const principalMutationError = rejectPrincipalEntityMutation(request, `scheduled`);
|
|
6922
6901
|
if (principalMutationError) return principalMutationError;
|
|
@@ -6997,7 +6976,7 @@ async function deleteEventSourceSubscription(request, ctx) {
|
|
|
6997
6976
|
return (0, itty_router.json)(result);
|
|
6998
6977
|
}
|
|
6999
6978
|
async function setTag(request, ctx) {
|
|
7000
|
-
const principalMutationError = rejectPrincipalEntityMutation(request, `
|
|
6979
|
+
const principalMutationError = rejectPrincipalEntityMutation(request, `tag updated`);
|
|
7001
6980
|
if (principalMutationError) return principalMutationError;
|
|
7002
6981
|
const parsed = routeBody(request);
|
|
7003
6982
|
const { entityUrl } = requireExistingEntityRoute(request);
|
|
@@ -7005,12 +6984,12 @@ async function setTag(request, ctx) {
|
|
|
7005
6984
|
const updated = await ctx.entityManager.setTag(entityUrl, decodeURIComponent(request.params.tagKey), { value: parsed.value }, token);
|
|
7006
6985
|
return (0, itty_router.json)(toPublicEntity(updated));
|
|
7007
6986
|
}
|
|
7008
|
-
async function
|
|
7009
|
-
const principalMutationError = rejectPrincipalEntityMutation(request, `
|
|
6987
|
+
async function deleteTag(request, ctx) {
|
|
6988
|
+
const principalMutationError = rejectPrincipalEntityMutation(request, `tag deleted`);
|
|
7010
6989
|
if (principalMutationError) return principalMutationError;
|
|
7011
6990
|
const { entityUrl } = requireExistingEntityRoute(request);
|
|
7012
6991
|
const token = writeTokenFromRequest(request);
|
|
7013
|
-
const updated = await ctx.entityManager.
|
|
6992
|
+
const updated = await ctx.entityManager.deleteTag(entityUrl, decodeURIComponent(request.params.tagKey), token);
|
|
7014
6993
|
return (0, itty_router.json)(toPublicEntity(updated));
|
|
7015
6994
|
}
|
|
7016
6995
|
async function forkEntity(request, ctx) {
|
|
@@ -7142,17 +7121,13 @@ const registerEntityTypeBodySchema = __sinclair_typebox.Type.Object({
|
|
|
7142
7121
|
creation_schema: __sinclair_typebox.Type.Optional(jsonObjectSchema),
|
|
7143
7122
|
inbox_schemas: __sinclair_typebox.Type.Optional(schemaMapSchema),
|
|
7144
7123
|
state_schemas: __sinclair_typebox.Type.Optional(schemaMapSchema),
|
|
7145
|
-
input_schemas: __sinclair_typebox.Type.Optional(schemaMapSchema),
|
|
7146
|
-
output_schemas: __sinclair_typebox.Type.Optional(schemaMapSchema),
|
|
7147
7124
|
serve_endpoint: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
|
|
7148
7125
|
default_dispatch_policy: __sinclair_typebox.Type.Optional(dispatchPolicySchema)
|
|
7149
|
-
});
|
|
7126
|
+
}, { additionalProperties: false });
|
|
7150
7127
|
const amendEntityTypeSchemasBodySchema = __sinclair_typebox.Type.Object({
|
|
7151
|
-
input_schemas: __sinclair_typebox.Type.Optional(schemaMapSchema),
|
|
7152
|
-
output_schemas: __sinclair_typebox.Type.Optional(schemaMapSchema),
|
|
7153
7128
|
inbox_schemas: __sinclair_typebox.Type.Optional(schemaMapSchema),
|
|
7154
7129
|
state_schemas: __sinclair_typebox.Type.Optional(schemaMapSchema)
|
|
7155
|
-
});
|
|
7130
|
+
}, { additionalProperties: false });
|
|
7156
7131
|
const entityTypesRouter = (0, itty_router.Router)({ base: `/_electric/entity-types` });
|
|
7157
7132
|
entityTypesRouter.get(`/`, listEntityTypes);
|
|
7158
7133
|
entityTypesRouter.post(`/`, withSchema(registerEntityTypeBodySchema), registerEntityType);
|
|
@@ -7192,8 +7167,8 @@ async function getEntityType(request, ctx) {
|
|
|
7192
7167
|
async function amendSchemas(request, ctx) {
|
|
7193
7168
|
const parsed = routeBody(request);
|
|
7194
7169
|
const updated = await ctx.entityManager.amendSchemas(request.params.name, {
|
|
7195
|
-
inbox_schemas: parsed.inbox_schemas
|
|
7196
|
-
state_schemas: parsed.state_schemas
|
|
7170
|
+
inbox_schemas: parsed.inbox_schemas,
|
|
7171
|
+
state_schemas: parsed.state_schemas
|
|
7197
7172
|
});
|
|
7198
7173
|
return (0, itty_router.json)(toPublicEntityType(updated));
|
|
7199
7174
|
}
|
|
@@ -7203,13 +7178,12 @@ async function deleteEntityType(request, ctx) {
|
|
|
7203
7178
|
}
|
|
7204
7179
|
function normalizeEntityTypeRequest(parsed) {
|
|
7205
7180
|
const serveEndpoint = rewriteLoopbackWebhookUrl(parsed.serve_endpoint);
|
|
7206
|
-
const compatibilityFields = parsed;
|
|
7207
7181
|
return {
|
|
7208
7182
|
name: parsed.name ?? ``,
|
|
7209
7183
|
description: parsed.description ?? ``,
|
|
7210
7184
|
creation_schema: parsed.creation_schema,
|
|
7211
|
-
inbox_schemas: parsed.inbox_schemas
|
|
7212
|
-
state_schemas: parsed.state_schemas
|
|
7185
|
+
inbox_schemas: parsed.inbox_schemas,
|
|
7186
|
+
state_schemas: parsed.state_schemas,
|
|
7213
7187
|
serve_endpoint: serveEndpoint,
|
|
7214
7188
|
default_dispatch_policy: parsed.default_dispatch_policy ?? (serveEndpoint ? { targets: [{
|
|
7215
7189
|
type: `webhook`,
|
|
@@ -7220,8 +7194,6 @@ function normalizeEntityTypeRequest(parsed) {
|
|
|
7220
7194
|
function toPublicEntityType(entityType) {
|
|
7221
7195
|
return {
|
|
7222
7196
|
...entityType,
|
|
7223
|
-
input_schemas: entityType.inbox_schemas,
|
|
7224
|
-
output_schemas: entityType.state_schemas,
|
|
7225
7197
|
revision: entityType.revision
|
|
7226
7198
|
};
|
|
7227
7199
|
}
|
|
@@ -7305,13 +7277,35 @@ function errorMapper(err, req) {
|
|
|
7305
7277
|
function rejectIfShuttingDown(req, ctx) {
|
|
7306
7278
|
if (!ctx.isShuttingDown()) return void 0;
|
|
7307
7279
|
const path$2 = new URL(req.url).pathname;
|
|
7308
|
-
if (!path$2.startsWith(`/_electric/
|
|
7280
|
+
if (!path$2.startsWith(`/_electric/subscription-webhooks/`)) return void 0;
|
|
7309
7281
|
return apiError(503, `SERVER_STOPPING`, `Server is shutting down`);
|
|
7310
7282
|
}
|
|
7311
7283
|
function getRequestSpan(req) {
|
|
7312
7284
|
return carrier(req)[SPAN_KEY];
|
|
7313
7285
|
}
|
|
7314
7286
|
|
|
7287
|
+
//#endregion
|
|
7288
|
+
//#region src/routing/observations-router.ts
|
|
7289
|
+
const stringRecordSchema = __sinclair_typebox.Type.Record(__sinclair_typebox.Type.String(), __sinclair_typebox.Type.String());
|
|
7290
|
+
const ensureEntitiesMembershipStreamBodySchema = __sinclair_typebox.Type.Object({ tags: __sinclair_typebox.Type.Optional(stringRecordSchema) });
|
|
7291
|
+
const ensureCronStreamBodySchema = __sinclair_typebox.Type.Object({
|
|
7292
|
+
expression: __sinclair_typebox.Type.String(),
|
|
7293
|
+
timezone: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String())
|
|
7294
|
+
});
|
|
7295
|
+
const observationsRouter = (0, itty_router.Router)({ base: `/_electric/observations` });
|
|
7296
|
+
observationsRouter.post(`/entities/ensure-stream`, withSchema(ensureEntitiesMembershipStreamBodySchema), ensureEntitiesMembershipStream);
|
|
7297
|
+
observationsRouter.post(`/cron/ensure-stream`, withSchema(ensureCronStreamBodySchema), ensureCronStream);
|
|
7298
|
+
async function ensureEntitiesMembershipStream(request, ctx) {
|
|
7299
|
+
const parsed = routeBody(request);
|
|
7300
|
+
const result = await ctx.entityManager.ensureEntitiesMembershipStream(parsed.tags ?? {});
|
|
7301
|
+
return (0, itty_router.json)(result);
|
|
7302
|
+
}
|
|
7303
|
+
async function ensureCronStream(request, ctx) {
|
|
7304
|
+
const parsed = routeBody(request);
|
|
7305
|
+
const streamPath = await ctx.entityManager.getOrCreateCronStream(parsed.expression, parsed.timezone);
|
|
7306
|
+
return (0, itty_router.json)({ streamUrl: streamPath });
|
|
7307
|
+
}
|
|
7308
|
+
|
|
7315
7309
|
//#endregion
|
|
7316
7310
|
//#region src/routing/tenant-stream-paths.ts
|
|
7317
7311
|
function withLeadingSlash(path$2) {
|
|
@@ -7650,7 +7644,7 @@ async function notificationFromClaim(ctx, input) {
|
|
|
7650
7644
|
wakeId: input.claim.wake_id,
|
|
7651
7645
|
streamPath: primaryStream,
|
|
7652
7646
|
streams: streams$1,
|
|
7653
|
-
callback: (0, __electric_ax_agents_runtime.appendPathToUrl)(ctx.publicUrl, `/_electric/
|
|
7647
|
+
callback: (0, __electric_ax_agents_runtime.appendPathToUrl)(ctx.publicUrl, `/_electric/wake-callbacks/${encodeURIComponent(input.claim.wake_id)}`),
|
|
7654
7648
|
claimToken: input.claim.token,
|
|
7655
7649
|
triggerEvent: `message_received`,
|
|
7656
7650
|
entity: {
|
|
@@ -7685,7 +7679,7 @@ const wakeRegistrationBodySchema = __sinclair_typebox.Type.Object({
|
|
|
7685
7679
|
includeResponse: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Boolean()),
|
|
7686
7680
|
manifestKey: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String())
|
|
7687
7681
|
});
|
|
7688
|
-
const
|
|
7682
|
+
const subscriptionWebhookBodySchema = __sinclair_typebox.Type.Object({
|
|
7689
7683
|
subscription_id: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
|
|
7690
7684
|
wake_id: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
|
|
7691
7685
|
generation: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Number()),
|
|
@@ -7699,7 +7693,7 @@ const webhookForwardBodySchema = __sinclair_typebox.Type.Object({
|
|
|
7699
7693
|
consumer_id: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
|
|
7700
7694
|
callback: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String())
|
|
7701
7695
|
}, { additionalProperties: true });
|
|
7702
|
-
const
|
|
7696
|
+
const wakeCallbackBodySchema = __sinclair_typebox.Type.Object({
|
|
7703
7697
|
epoch: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Number()),
|
|
7704
7698
|
generation: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Number()),
|
|
7705
7699
|
wakeId: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
|
|
@@ -7712,13 +7706,13 @@ const internalRouter = (0, itty_router.Router)({ base: `/_electric` });
|
|
|
7712
7706
|
internalRouter.get(`/health`, () => (0, itty_router.json)({ status: `ok` }));
|
|
7713
7707
|
internalRouter.get(`/event-sources`, listEventSources);
|
|
7714
7708
|
internalRouter.post(`/wake`, withSchema(wakeRegistrationBodySchema), registerWake);
|
|
7715
|
-
internalRouter.post(`/
|
|
7716
|
-
internalRouter.post(`/
|
|
7709
|
+
internalRouter.post(`/subscription-webhooks/:subscriptionId`, subscriptionWebhook);
|
|
7710
|
+
internalRouter.post(`/wake-callbacks/:consumerId`, wakeCallback);
|
|
7717
7711
|
internalRouter.all(`/runners`, runnersRouter.fetch);
|
|
7718
7712
|
internalRouter.all(`/runners/*`, runnersRouter.fetch);
|
|
7719
7713
|
internalRouter.all(`/entities/*`, entitiesRouter.fetch);
|
|
7720
7714
|
internalRouter.all(`/entity-types/*`, entityTypesRouter.fetch);
|
|
7721
|
-
internalRouter.all(`/
|
|
7715
|
+
internalRouter.all(`/observations/*`, observationsRouter.fetch);
|
|
7722
7716
|
internalRouter.get(`/electric/*`, electricProxyRouter.fetch);
|
|
7723
7717
|
internalRouter.all(`*`, () => (0, itty_router.status)(404));
|
|
7724
7718
|
function routeParam(request, name) {
|
|
@@ -7751,13 +7745,30 @@ function resolveWebhookSigner(ctx) {
|
|
|
7751
7745
|
return ctx.webhookSigner ?? getDefaultWebhookSigner();
|
|
7752
7746
|
}
|
|
7753
7747
|
function durableStreamsWebhookJwksUrl(ctx) {
|
|
7754
|
-
if (!ctx.durableStreamsRouting) return (
|
|
7748
|
+
if (!ctx.durableStreamsRouting) return appendPathToBackendUrl(ctx.durableStreamsUrl, `/__ds/jwks.json`);
|
|
7755
7749
|
return resolveDurableStreamsRoutingAdapter(ctx.durableStreamsRouting, ctx.durableStreamsUrl).controlUrl({
|
|
7756
7750
|
durableStreamsUrl: ctx.durableStreamsUrl,
|
|
7757
7751
|
serviceId: ctx.service,
|
|
7758
7752
|
requestUrl: (0, __electric_ax_agents_runtime.appendPathToUrl)(ctx.publicUrl, `/__ds/jwks.json`)
|
|
7759
7753
|
}).toString();
|
|
7760
7754
|
}
|
|
7755
|
+
function appendPathToBackendUrl(baseUrl, path$2) {
|
|
7756
|
+
const base = new URL(baseUrl);
|
|
7757
|
+
const pathUrl = new URL(path$2, `http://electric-agents.local`);
|
|
7758
|
+
const basePath = base.pathname === `/` ? `` : base.pathname.replace(/\/+$/, ``);
|
|
7759
|
+
const suffix = pathUrl.pathname.startsWith(`/`) ? pathUrl.pathname : `/${pathUrl.pathname}`;
|
|
7760
|
+
const target = new URL(base);
|
|
7761
|
+
target.pathname = `${basePath}${suffix}`;
|
|
7762
|
+
target.search = ``;
|
|
7763
|
+
target.hash = pathUrl.hash;
|
|
7764
|
+
base.searchParams.forEach((value, key) => {
|
|
7765
|
+
target.searchParams.append(key, value);
|
|
7766
|
+
});
|
|
7767
|
+
pathUrl.searchParams.forEach((value, key) => {
|
|
7768
|
+
target.searchParams.append(key, value);
|
|
7769
|
+
});
|
|
7770
|
+
return target.toString();
|
|
7771
|
+
}
|
|
7761
7772
|
function durableStreamsJwksFetchClient(ctx) {
|
|
7762
7773
|
return async (input, init) => {
|
|
7763
7774
|
const headers = new Headers(init?.headers);
|
|
@@ -7821,10 +7832,10 @@ async function listEventSources(_request, ctx) {
|
|
|
7821
7832
|
function isAgentVisibleEventSource(source) {
|
|
7822
7833
|
return source.agentVisible === true && source.status === `active`;
|
|
7823
7834
|
}
|
|
7824
|
-
async function
|
|
7835
|
+
async function subscriptionWebhook(request, ctx) {
|
|
7825
7836
|
const subscriptionId = routeParam(request, `subscriptionId`);
|
|
7826
7837
|
const rootSpan = getRequestSpan(request);
|
|
7827
|
-
rootSpan?.updateName(`webhook
|
|
7838
|
+
rootSpan?.updateName(`subscription-webhook`);
|
|
7828
7839
|
rootSpan?.setAttribute(`electric_agents.webhook.subscription_id`, subscriptionId);
|
|
7829
7840
|
const body = await readRequestBody(request);
|
|
7830
7841
|
const signatureError = await verifyDurableStreamsWebhook(request, ctx, body);
|
|
@@ -7838,7 +7849,7 @@ async function webhookForward(request, ctx) {
|
|
|
7838
7849
|
}
|
|
7839
7850
|
});
|
|
7840
7851
|
if (!targetWebhookUrl) return apiError(404, ErrCodeSubscriptionNotFound, `Unknown webhook subscription`);
|
|
7841
|
-
const parsedBodyResult = validateOptionalJsonBody(
|
|
7852
|
+
const parsedBodyResult = validateOptionalJsonBody(subscriptionWebhookBodySchema, body, request.headers.get(`content-type`));
|
|
7842
7853
|
if (!parsedBodyResult.ok) return parsedBodyResult.response;
|
|
7843
7854
|
let forwardBody = body;
|
|
7844
7855
|
let runningEntityUrl = null;
|
|
@@ -7884,7 +7895,7 @@ async function webhookForward(request, ctx) {
|
|
|
7884
7895
|
span.end();
|
|
7885
7896
|
}
|
|
7886
7897
|
}).catch((err) => {
|
|
7887
|
-
serverLog.warn(`[webhook
|
|
7898
|
+
serverLog.warn(`[subscription-webhook] consumerCallbacks upsert failed (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
|
|
7888
7899
|
}) : void 0;
|
|
7889
7900
|
const [entity, enriched] = await Promise.all([entityPromise, enrichPromise]);
|
|
7890
7901
|
if (entity?.status === `stopped` || entity?.status === `paused`) {
|
|
@@ -7905,7 +7916,7 @@ async function webhookForward(request, ctx) {
|
|
|
7905
7916
|
runningEntityUrl = entity.url;
|
|
7906
7917
|
}
|
|
7907
7918
|
if (consumerId && callbackUrl) {
|
|
7908
|
-
const callback = (0, __electric_ax_agents_runtime.appendPathToUrl)(ctx.publicUrl, `/_electric/
|
|
7919
|
+
const callback = (0, __electric_ax_agents_runtime.appendPathToUrl)(ctx.publicUrl, `/_electric/wake-callbacks/${encodeURIComponent(consumerId)}`);
|
|
7909
7920
|
enriched.callback = callback;
|
|
7910
7921
|
if (newWebhook) {
|
|
7911
7922
|
enriched.consumerId = newWebhook.wakeId;
|
|
@@ -7942,21 +7953,21 @@ async function webhookForward(request, ctx) {
|
|
|
7942
7953
|
});
|
|
7943
7954
|
} catch (err) {
|
|
7944
7955
|
if (runningEntityUrl) await ctx.entityManager.registry.updateStatus(runningEntityUrl, `idle`);
|
|
7945
|
-
return apiError(502, `
|
|
7956
|
+
return apiError(502, `SUBSCRIPTION_WEBHOOK_FAILED`, err instanceof Error ? err.message : String(err));
|
|
7946
7957
|
}
|
|
7947
7958
|
const responseBytes = upstream.body ? new Uint8Array(await upstream.arrayBuffer()) : new Uint8Array();
|
|
7948
7959
|
return responseFromUpstream(upstream, responseBytes);
|
|
7949
7960
|
}
|
|
7950
|
-
async function
|
|
7961
|
+
async function wakeCallback(request, ctx) {
|
|
7951
7962
|
const consumerId = routeParam(request, `consumerId`);
|
|
7952
7963
|
const rows = await ctx.pgDb.select().from(consumerCallbacks).where((0, drizzle_orm.and)((0, drizzle_orm.eq)(consumerCallbacks.tenantId, ctx.service), (0, drizzle_orm.eq)(consumerCallbacks.consumerId, consumerId))).limit(1);
|
|
7953
7964
|
const target = rows[0] ? {
|
|
7954
7965
|
callbackUrl: rows[0].callbackUrl,
|
|
7955
7966
|
primaryStream: rows[0].primaryStream
|
|
7956
7967
|
} : void 0;
|
|
7957
|
-
if (!target) return apiError(404,
|
|
7968
|
+
if (!target) return apiError(404, ErrCodeWakeCallbackNotFound, `Unknown wake-callback consumer`);
|
|
7958
7969
|
const body = await readRequestBody(request);
|
|
7959
|
-
const parsedBodyResult = validateOptionalJsonBody(
|
|
7970
|
+
const parsedBodyResult = validateOptionalJsonBody(wakeCallbackBodySchema, body, request.headers.get(`content-type`));
|
|
7960
7971
|
if (!parsedBodyResult.ok) return parsedBodyResult.response;
|
|
7961
7972
|
const requestBody = parsedBodyResult.value;
|
|
7962
7973
|
const isClaimRequest = requestBody?.wakeId !== void 0 || requestBody?.wake_id !== void 0;
|
|
@@ -7974,14 +7985,14 @@ async function callbackForward(request, ctx) {
|
|
|
7974
7985
|
}
|
|
7975
7986
|
return (0, itty_router.json)(responseBody);
|
|
7976
7987
|
}
|
|
7977
|
-
const upstreamBody =
|
|
7988
|
+
const upstreamBody = encodeWakeCallbackBody(ctx.service, consumerId, requestBody, resolveDurableStreamsRoutingAdapter(ctx.durableStreamsRouting, ctx.durableStreamsUrl));
|
|
7978
7989
|
let upstream;
|
|
7979
7990
|
try {
|
|
7980
7991
|
const subscriptionId = durableStreamsSubscriptionCallback(target.callbackUrl);
|
|
7981
7992
|
if (subscriptionId) {
|
|
7982
7993
|
const token = claimTokenFromRequest(request);
|
|
7983
7994
|
if (!token) return apiError(401, `UNAUTHORIZED`, `Missing claim token`);
|
|
7984
|
-
const upstreamPayload =
|
|
7995
|
+
const upstreamPayload = encodeWakeCallbackPayload(consumerId, requestBody, (stream) => stream.replace(/^\/+/, ``));
|
|
7985
7996
|
const result = await ctx.streamClient.ackSubscription(subscriptionId, token, upstreamPayload);
|
|
7986
7997
|
upstream = (0, itty_router.json)(result);
|
|
7987
7998
|
} else upstream = await fetch(target.callbackUrl, {
|
|
@@ -7990,7 +8001,7 @@ async function callbackForward(request, ctx) {
|
|
|
7990
8001
|
body: bodyFromBytes(upstreamBody)
|
|
7991
8002
|
});
|
|
7992
8003
|
} catch (err) {
|
|
7993
|
-
return apiError(502, `
|
|
8004
|
+
return apiError(502, `WAKE_CALLBACK_FAILED`, err instanceof Error ? err.message : String(err));
|
|
7994
8005
|
}
|
|
7995
8006
|
let responseBytes = upstream.body ? new Uint8Array(await upstream.arrayBuffer()) : new Uint8Array();
|
|
7996
8007
|
if (isClaimRequest && upstream.ok && target.primaryStream) {
|
|
@@ -8010,7 +8021,7 @@ async function callbackForward(request, ctx) {
|
|
|
8010
8021
|
epoch
|
|
8011
8022
|
});
|
|
8012
8023
|
if (upstream.ok && isDoneRequest && target.primaryStream) {
|
|
8013
|
-
serverLog.info(`[callback
|
|
8024
|
+
serverLog.info(`[wake-callback] done received for stream=${target.primaryStream} consumer=${consumerId}`);
|
|
8014
8025
|
const stillOwnsClaim = ctx.runtime.claimWriteTokens.owns(ctx.service, target.primaryStream, consumerId);
|
|
8015
8026
|
const entity = await ctx.entityManager.registry.getEntityByStream(target.primaryStream);
|
|
8016
8027
|
let entityCleared = false;
|
|
@@ -8032,13 +8043,13 @@ async function callbackForward(request, ctx) {
|
|
|
8032
8043
|
if (entity && (entityCleared || stillOwnsClaim)) {
|
|
8033
8044
|
await ctx.entityManager.registry.updateStatus(entity.url, entity.status === `stopping` ? `stopped` : `idle`);
|
|
8034
8045
|
await ctx.entityBridgeManager.onEntityChanged(entity.url);
|
|
8035
|
-
serverLog.info(`[callback
|
|
8036
|
-
} else if (!entity) serverLog.warn(`[callback
|
|
8046
|
+
serverLog.info(`[wake-callback] status updated after done for ${entity.url}`);
|
|
8047
|
+
} else if (!entity) serverLog.warn(`[wake-callback] done received but no entity found for stream=${target.primaryStream}`);
|
|
8037
8048
|
if (stillOwnsClaim) ctx.runtime.claimWriteTokens.clearStream(ctx.service, target.primaryStream);
|
|
8038
|
-
else if (entity) serverLog.info(`[callback
|
|
8039
|
-
} else if (requestBody?.done === true) serverLog.warn(`[callback
|
|
8049
|
+
else if (entity) serverLog.info(`[wake-callback] done arrived after in-memory token evicted (stream=${target.primaryStream} consumer=${consumerId})`);
|
|
8050
|
+
} else if (requestBody?.done === true) serverLog.warn(`[wake-callback] done received but skipped: upstream.ok=${upstream.ok} primaryStream=${target.primaryStream ?? `null`} consumer=${consumerId}`);
|
|
8040
8051
|
} catch (err) {
|
|
8041
|
-
serverLog.error(`[callback
|
|
8052
|
+
serverLog.error(`[wake-callback] error processing done for consumer=${consumerId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
8042
8053
|
}
|
|
8043
8054
|
return responseFromUpstream(upstream, responseBytes);
|
|
8044
8055
|
}
|
|
@@ -8047,11 +8058,11 @@ async function mintClaimWriteToken(ctx, streamPath, consumerId) {
|
|
|
8047
8058
|
if (!entity) return void 0;
|
|
8048
8059
|
return ctx.runtime.claimWriteTokens.mint(ctx.service, streamPath, consumerId);
|
|
8049
8060
|
}
|
|
8050
|
-
function
|
|
8051
|
-
const payload =
|
|
8061
|
+
function encodeWakeCallbackBody(service, consumerId, body, routingAdapter) {
|
|
8062
|
+
const payload = encodeWakeCallbackPayload(consumerId, body, (stream) => routingAdapter.toBackendStreamPath(service, stream));
|
|
8052
8063
|
return new TextEncoder().encode(JSON.stringify(payload));
|
|
8053
8064
|
}
|
|
8054
|
-
function
|
|
8065
|
+
function encodeWakeCallbackPayload(consumerId, body, mapStream) {
|
|
8055
8066
|
if (!body) return {};
|
|
8056
8067
|
const generation = body.generation ?? body.epoch;
|
|
8057
8068
|
const wakeId = body.wake_id ?? body.wakeId ?? consumerId;
|
package/dist/index.d.cts
CHANGED
|
@@ -4158,7 +4158,7 @@ declare class EntityManager {
|
|
|
4158
4158
|
private syncManifestFutureSendSchedule;
|
|
4159
4159
|
private parseEntityUrl;
|
|
4160
4160
|
/**
|
|
4161
|
-
* Deliver a message to an entity's main stream, with optional
|
|
4161
|
+
* Deliver a message to an entity's main stream, with optional inbox schema
|
|
4162
4162
|
* validation.
|
|
4163
4163
|
*/
|
|
4164
4164
|
send(entityUrl: string, req: SendRequest, opts?: {
|
|
@@ -4172,8 +4172,8 @@ declare class EntityManager {
|
|
|
4172
4172
|
}): Promise<void>;
|
|
4173
4173
|
deleteInboxMessage(entityUrl: string, key: string): Promise<void>;
|
|
4174
4174
|
setTag(entityUrl: string, key: string, req: SetTagRequest, token: string): Promise<ElectricAgentsEntity>;
|
|
4175
|
-
|
|
4176
|
-
|
|
4175
|
+
deleteTag(entityUrl: string, key: string, token: string): Promise<ElectricAgentsEntity>;
|
|
4176
|
+
ensureEntitiesMembershipStream(tags: Record<string, string>): Promise<{
|
|
4177
4177
|
sourceRef: string;
|
|
4178
4178
|
streamUrl: string;
|
|
4179
4179
|
}>;
|
|
@@ -4263,7 +4263,7 @@ declare class EntityManager {
|
|
|
4263
4263
|
status: number;
|
|
4264
4264
|
} | null>;
|
|
4265
4265
|
/**
|
|
4266
|
-
* Add new
|
|
4266
|
+
* Add new inbox/state schema keys to an entity type directly in Postgres.
|
|
4267
4267
|
*/
|
|
4268
4268
|
amendSchemas(typeName: string, schemas: {
|
|
4269
4269
|
inbox_schemas?: Record<string, Record<string, unknown>>;
|
|
@@ -4271,7 +4271,7 @@ declare class EntityManager {
|
|
|
4271
4271
|
}): Promise<ElectricAgentsEntityType>;
|
|
4272
4272
|
/**
|
|
4273
4273
|
* Enrich webhook payload with entity context.
|
|
4274
|
-
* Called by ElectricAgentsServer during webhook
|
|
4274
|
+
* Called by ElectricAgentsServer during subscription webhook dispatch to inject entity context.
|
|
4275
4275
|
*/
|
|
4276
4276
|
enrichPayload(payload: Record<string, unknown>, consumer: {
|
|
4277
4277
|
primary_stream: string;
|
package/dist/index.d.ts
CHANGED
|
@@ -4159,7 +4159,7 @@ declare class EntityManager {
|
|
|
4159
4159
|
private syncManifestFutureSendSchedule;
|
|
4160
4160
|
private parseEntityUrl;
|
|
4161
4161
|
/**
|
|
4162
|
-
* Deliver a message to an entity's main stream, with optional
|
|
4162
|
+
* Deliver a message to an entity's main stream, with optional inbox schema
|
|
4163
4163
|
* validation.
|
|
4164
4164
|
*/
|
|
4165
4165
|
send(entityUrl: string, req: SendRequest, opts?: {
|
|
@@ -4173,8 +4173,8 @@ declare class EntityManager {
|
|
|
4173
4173
|
}): Promise<void>;
|
|
4174
4174
|
deleteInboxMessage(entityUrl: string, key: string): Promise<void>;
|
|
4175
4175
|
setTag(entityUrl: string, key: string, req: SetTagRequest, token: string): Promise<ElectricAgentsEntity>;
|
|
4176
|
-
|
|
4177
|
-
|
|
4176
|
+
deleteTag(entityUrl: string, key: string, token: string): Promise<ElectricAgentsEntity>;
|
|
4177
|
+
ensureEntitiesMembershipStream(tags: Record<string, string>): Promise<{
|
|
4178
4178
|
sourceRef: string;
|
|
4179
4179
|
streamUrl: string;
|
|
4180
4180
|
}>;
|
|
@@ -4264,7 +4264,7 @@ declare class EntityManager {
|
|
|
4264
4264
|
status: number;
|
|
4265
4265
|
} | null>;
|
|
4266
4266
|
/**
|
|
4267
|
-
* Add new
|
|
4267
|
+
* Add new inbox/state schema keys to an entity type directly in Postgres.
|
|
4268
4268
|
*/
|
|
4269
4269
|
amendSchemas(typeName: string, schemas: {
|
|
4270
4270
|
inbox_schemas?: Record<string, Record<string, unknown>>;
|
|
@@ -4272,7 +4272,7 @@ declare class EntityManager {
|
|
|
4272
4272
|
}): Promise<ElectricAgentsEntityType>;
|
|
4273
4273
|
/**
|
|
4274
4274
|
* Enrich webhook payload with entity context.
|
|
4275
|
-
* Called by ElectricAgentsServer during webhook
|
|
4275
|
+
* Called by ElectricAgentsServer during subscription webhook dispatch to inject entity context.
|
|
4276
4276
|
*/
|
|
4277
4277
|
enrichPayload(payload: Record<string, unknown>, consumer: {
|
|
4278
4278
|
primary_stream: string;
|