@electric-ax/agents-server 0.4.5 → 0.4.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -600,7 +600,7 @@ var PostgresRegistry = class {
600
600
  const heartbeatAt = input.heartbeatAt ?? new Date();
601
601
  await this.db.update(consumerClaims).set({
602
602
  lastHeartbeatAt: heartbeatAt,
603
- leaseExpiresAt: input.leaseExpiresAt ?? null,
603
+ ...input.leaseExpiresAt !== void 0 ? { leaseExpiresAt: input.leaseExpiresAt } : {},
604
604
  updatedAt: heartbeatAt
605
605
  }).where((0, drizzle_orm.and)((0, drizzle_orm.eq)(consumerClaims.tenantId, this.tenantId), (0, drizzle_orm.eq)(consumerClaims.consumerId, input.consumerId), (0, drizzle_orm.eq)(consumerClaims.epoch, input.epoch)));
606
606
  }
@@ -613,17 +613,24 @@ var PostgresRegistry = class {
613
613
  updatedAt: releasedAt
614
614
  }).where((0, drizzle_orm.and)((0, drizzle_orm.eq)(consumerClaims.tenantId, this.tenantId), (0, drizzle_orm.eq)(consumerClaims.consumerId, input.consumerId), (0, drizzle_orm.eq)(consumerClaims.epoch, input.epoch))).returning();
615
615
  const claim = rows[0] ? this.rowToConsumerClaim(rows[0]) : null;
616
- if (claim) await this.db.update(entityDispatchState).set({
617
- activeConsumerId: null,
618
- activeRunnerId: null,
619
- activeEpoch: null,
620
- activeClaimedAt: null,
621
- activeLeaseExpiresAt: null,
622
- lastReleasedAt: releasedAt,
623
- lastCompletedAt: releasedAt,
624
- updatedAt: releasedAt
625
- }).where((0, drizzle_orm.and)((0, drizzle_orm.eq)(entityDispatchState.tenantId, this.tenantId), (0, drizzle_orm.eq)(entityDispatchState.entityUrl, claim.entity_url), (0, drizzle_orm.eq)(entityDispatchState.activeConsumerId, input.consumerId), (0, drizzle_orm.eq)(entityDispatchState.activeEpoch, input.epoch)));
626
- return claim;
616
+ let entityCleared = false;
617
+ if (claim) {
618
+ const cleared = await this.db.update(entityDispatchState).set({
619
+ activeConsumerId: null,
620
+ activeRunnerId: null,
621
+ activeEpoch: null,
622
+ activeClaimedAt: null,
623
+ activeLeaseExpiresAt: null,
624
+ lastReleasedAt: releasedAt,
625
+ lastCompletedAt: releasedAt,
626
+ updatedAt: releasedAt
627
+ }).where((0, drizzle_orm.and)((0, drizzle_orm.eq)(entityDispatchState.tenantId, this.tenantId), (0, drizzle_orm.eq)(entityDispatchState.entityUrl, claim.entity_url), (0, drizzle_orm.eq)(entityDispatchState.activeConsumerId, input.consumerId), (0, drizzle_orm.eq)(entityDispatchState.activeEpoch, input.epoch))).returning({ entityUrl: entityDispatchState.entityUrl });
628
+ entityCleared = cleared.length > 0;
629
+ }
630
+ return {
631
+ claim,
632
+ entityCleared
633
+ };
627
634
  }
628
635
  async getActiveClaimsForRunner(runnerId) {
629
636
  const rows = await this.db.select().from(consumerClaims).where((0, drizzle_orm.and)((0, drizzle_orm.eq)(consumerClaims.tenantId, this.tenantId), (0, drizzle_orm.eq)(consumerClaims.runnerId, runnerId), (0, drizzle_orm.eq)(consumerClaims.status, `active`)));
@@ -6144,6 +6151,87 @@ function sqlStringLiteral(value) {
6144
6151
  return `'${value.replace(/'/g, `''`)}'`;
6145
6152
  }
6146
6153
 
6154
+ //#endregion
6155
+ //#region src/webhook-signing.ts
6156
+ const encoder = new TextEncoder();
6157
+ const defaultWebhookSigner = createEd25519WebhookSigner();
6158
+ function createEd25519WebhookSigner(options = {}) {
6159
+ const privateKey = options.privateKey ? importPrivateKey(options.privateKey) : (0, node_crypto.generateKeyPairSync)(`ed25519`).privateKey;
6160
+ if (privateKey.asymmetricKeyType !== `ed25519`) throw new Error(`Webhook signing key must be an Ed25519 private key`);
6161
+ const publicJwk = buildPublicJwk(privateKey, options.kid);
6162
+ return {
6163
+ sign: (body) => signWebhookBody(privateKey, publicJwk.kid, body),
6164
+ jwks: () => ({ keys: [{ ...publicJwk }] })
6165
+ };
6166
+ }
6167
+ function getDefaultWebhookSigner() {
6168
+ return defaultWebhookSigner;
6169
+ }
6170
+ async function webhookSigningMetadata(signer, streamRootUrl) {
6171
+ const jwks = await signer.jwks();
6172
+ const key = jwks.keys[0];
6173
+ if (!key) throw new Error(`Webhook signer did not provide any public keys`);
6174
+ return {
6175
+ alg: `ed25519`,
6176
+ kid: key.kid,
6177
+ jwks_url: (0, __electric_ax_agents_runtime.appendPathToUrl)(streamRootUrl, `/__ds/jwks.json`)
6178
+ };
6179
+ }
6180
+ function signWebhookBody(privateKey, kid, body) {
6181
+ const timestamp$1 = Math.floor(Date.now() / 1e3);
6182
+ const payload = bytesWithTimestamp(timestamp$1, body);
6183
+ const signature = (0, node_crypto.sign)(null, payload, privateKey).toString(`base64url`);
6184
+ return `t=${timestamp$1},kid=${kid},ed25519=${signature}`;
6185
+ }
6186
+ function bytesWithTimestamp(timestamp$1, body) {
6187
+ const prefix = encoder.encode(`${timestamp$1}.`);
6188
+ const bodyBytes = typeof body === `string` ? encoder.encode(body) : body;
6189
+ return Buffer.concat([Buffer.from(prefix), Buffer.from(bodyBytes)]);
6190
+ }
6191
+ function importPrivateKey(input) {
6192
+ if (isKeyObject(input)) return input;
6193
+ if (typeof input === `string`) {
6194
+ const trimmed = input.trim();
6195
+ if (trimmed.startsWith(`{`)) return (0, node_crypto.createPrivateKey)({
6196
+ key: JSON.parse(trimmed),
6197
+ format: `jwk`
6198
+ });
6199
+ return (0, node_crypto.createPrivateKey)(trimmed.replace(/\\n/g, `\n`));
6200
+ }
6201
+ if (Buffer.isBuffer(input)) return (0, node_crypto.createPrivateKey)(input);
6202
+ return (0, node_crypto.createPrivateKey)({
6203
+ key: input,
6204
+ format: `jwk`
6205
+ });
6206
+ }
6207
+ function isKeyObject(input) {
6208
+ return typeof input === `object` && `type` in input && input.type === `private`;
6209
+ }
6210
+ function buildPublicJwk(privateKey, kid) {
6211
+ const exported = (0, node_crypto.createPublicKey)(privateKey).export({ format: `jwk` });
6212
+ if (exported.kty !== `OKP` || exported.crv !== `Ed25519` || !exported.x) throw new Error(`Failed to export Ed25519 webhook signing key`);
6213
+ return {
6214
+ kty: `OKP`,
6215
+ crv: `Ed25519`,
6216
+ x: exported.x,
6217
+ kid: kid ?? deriveKeyId({
6218
+ kty: exported.kty,
6219
+ crv: exported.crv,
6220
+ x: exported.x
6221
+ }),
6222
+ use: `sig`,
6223
+ alg: `EdDSA`
6224
+ };
6225
+ }
6226
+ function deriveKeyId(jwk) {
6227
+ const thumbprintInput = JSON.stringify({
6228
+ crv: jwk.crv,
6229
+ kty: jwk.kty,
6230
+ x: jwk.x
6231
+ });
6232
+ return `ds_${(0, node_crypto.createHash)(`sha256`).update(thumbprintInput).digest(`base64url`)}`;
6233
+ }
6234
+
6147
6235
  //#endregion
6148
6236
  //#region src/routing/durable-streams-router.ts
6149
6237
  const subscriptionProxyBodySchema = __sinclair_typebox.Type.Object({ webhook: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Object({ url: __sinclair_typebox.Type.String() }, { additionalProperties: true })) }, { additionalProperties: true });
@@ -6160,6 +6248,7 @@ durableStreamsRouter.delete(`/__ds/subscriptions/:subscriptionId`, deleteSubscri
6160
6248
  durableStreamsRouter.post(`/__ds/subscriptions/:subscriptionId/streams`, postSubscriptionStreams);
6161
6249
  durableStreamsRouter.delete(`/__ds/subscriptions/:subscriptionId/streams/:streamPath+`, deleteSubscriptionStream);
6162
6250
  for (const action of subscriptionControlActions) durableStreamsRouter.post(`/__ds/subscriptions/:subscriptionId/${action}`, subscriptionAction(action));
6251
+ durableStreamsRouter.get(`/__ds/jwks.json`, webhookJwks);
6163
6252
  durableStreamsRouter.all(`/__ds`, controlPassThrough);
6164
6253
  durableStreamsRouter.all(`/__ds/*`, controlPassThrough);
6165
6254
  durableStreamsRouter.post(`*`, streamAppend);
@@ -6168,12 +6257,16 @@ function bodyFromBytes$1(body) {
6168
6257
  return body.buffer.slice(body.byteOffset, body.byteOffset + body.byteLength);
6169
6258
  }
6170
6259
  function responseFromUpstream$1(response, body) {
6171
- return new Response(body ? bodyFromBytes$1(body) : response.body, {
6260
+ const responseBody = forbidsResponseBody$1(response.status) ? null : body !== void 0 ? bodyFromBytes$1(body) : response.body;
6261
+ return new Response(responseBody, {
6172
6262
  status: response.status,
6173
6263
  statusText: response.statusText,
6174
6264
  headers: responseHeaders(response)
6175
6265
  });
6176
6266
  }
6267
+ function forbidsResponseBody$1(status$4) {
6268
+ return status$4 === 204 || status$4 === 205 || status$4 === 304;
6269
+ }
6177
6270
  async function forwardToDurableStreams(ctx, request, body, route = `stream`, urlOverride, durableStreamsBearerMode = `overwrite`) {
6178
6271
  const headers = new Headers(request.headers);
6179
6272
  headers.delete(`host`);
@@ -6207,28 +6300,32 @@ function rewriteSubscriptionBodyForBackend(payload, service, routingAdapter) {
6207
6300
  return next;
6208
6301
  });
6209
6302
  }
6210
- function rewriteSubscriptionResponseForClient(bytes, response, service, routingAdapter) {
6303
+ async function rewriteSubscriptionResponseForClient(bytes, response, ctx, routingAdapter) {
6211
6304
  if (!response.headers.get(`content-type`)?.includes(`application/json`)) return bytes;
6212
6305
  const payload = decodeJson(bytes);
6213
6306
  if (!payload) return bytes;
6214
- if (typeof payload.pattern === `string`) payload.pattern = routingAdapter.toRuntimeStreamPath(service, payload.pattern);
6307
+ if (typeof payload.pattern === `string`) payload.pattern = routingAdapter.toRuntimeStreamPath(ctx.service, payload.pattern);
6215
6308
  if (Array.isArray(payload.streams)) payload.streams = payload.streams.map((stream) => {
6216
- if (typeof stream === `string`) return routingAdapter.toRuntimeStreamPath(service, stream);
6309
+ if (typeof stream === `string`) return routingAdapter.toRuntimeStreamPath(ctx.service, stream);
6217
6310
  if (stream && typeof stream === `object` && typeof stream.path === `string`) return {
6218
6311
  ...stream,
6219
- path: routingAdapter.toRuntimeStreamPath(service, stream.path)
6312
+ path: routingAdapter.toRuntimeStreamPath(ctx.service, stream.path)
6220
6313
  };
6221
6314
  return stream;
6222
6315
  });
6223
- if (typeof payload.wake_stream === `string`) payload.wake_stream = routingAdapter.toRuntimeStreamPath(service, payload.wake_stream);
6224
- if (typeof payload.stream === `string`) payload.stream = routingAdapter.toRuntimeStreamPath(service, payload.stream);
6316
+ if (typeof payload.wake_stream === `string`) payload.wake_stream = routingAdapter.toRuntimeStreamPath(ctx.service, payload.wake_stream);
6317
+ if (typeof payload.stream === `string`) payload.stream = routingAdapter.toRuntimeStreamPath(ctx.service, payload.stream);
6225
6318
  if (Array.isArray(payload.acks)) payload.acks = payload.acks.map((ack) => {
6226
6319
  if (!ack || typeof ack !== `object`) return ack;
6227
6320
  const next = { ...ack };
6228
- if (typeof next.stream === `string`) next.stream = routingAdapter.toRuntimeStreamPath(service, next.stream);
6229
- if (typeof next.path === `string`) next.path = routingAdapter.toRuntimeStreamPath(service, next.path);
6321
+ if (typeof next.stream === `string`) next.stream = routingAdapter.toRuntimeStreamPath(ctx.service, next.stream);
6322
+ if (typeof next.path === `string`) next.path = routingAdapter.toRuntimeStreamPath(ctx.service, next.path);
6230
6323
  return next;
6231
6324
  });
6325
+ if (payload.webhook && typeof payload.webhook === `object` && !Array.isArray(payload.webhook)) {
6326
+ const webhook = payload.webhook;
6327
+ webhook.signing = await webhookSigningMetadata(resolveWebhookSigner$1(ctx), ctx.publicUrl);
6328
+ }
6232
6329
  return new TextEncoder().encode(JSON.stringify(payload));
6233
6330
  }
6234
6331
  function decodeJson(bytes) {
@@ -6247,6 +6344,9 @@ function routeParam$2(request, name) {
6247
6344
  function subscriptionRoutingAdapter(ctx) {
6248
6345
  return resolveDurableStreamsRoutingAdapter(ctx.durableStreamsRouting, ctx.durableStreamsUrl);
6249
6346
  }
6347
+ function resolveWebhookSigner$1(ctx) {
6348
+ return ctx.webhookSigner ?? getDefaultWebhookSigner();
6349
+ }
6250
6350
  async function rewriteSubscriptionRequestBody(request, ctx, subscriptionId, routingAdapter) {
6251
6351
  const body = await readRequestBody(request);
6252
6352
  if (body.length === 0) return {
@@ -6275,7 +6375,7 @@ async function rewriteSubscriptionRequestBody(request, ctx, subscriptionId, rout
6275
6375
  async function forwardSubscriptionRequest(request, ctx, routingAdapter, opts = {}) {
6276
6376
  const upstream = await forwardToDurableStreams(ctx, request, opts.body, `control`, opts.requestUrl, opts.bearerMode ?? `overwrite`);
6277
6377
  let responseBytes = upstream.body ? new Uint8Array(await upstream.arrayBuffer()) : new Uint8Array();
6278
- responseBytes = rewriteSubscriptionResponseForClient(responseBytes, upstream, ctx.service, routingAdapter);
6378
+ responseBytes = await rewriteSubscriptionResponseForClient(responseBytes, upstream, ctx, routingAdapter);
6279
6379
  return {
6280
6380
  upstream,
6281
6381
  response: responseFromUpstream$1(upstream, responseBytes)
@@ -6348,6 +6448,15 @@ async function controlPassThrough(request, ctx) {
6348
6448
  const upstream = await forwardToDurableStreams(ctx, request, void 0, `control`);
6349
6449
  return responseFromUpstream$1(upstream);
6350
6450
  }
6451
+ async function webhookJwks(_request, ctx) {
6452
+ return new Response(JSON.stringify(await resolveWebhookSigner$1(ctx).jwks()), {
6453
+ status: 200,
6454
+ headers: {
6455
+ "content-type": `application/jwk-set+json`,
6456
+ "cache-control": `public, max-age=300`
6457
+ }
6458
+ });
6459
+ }
6351
6460
  async function streamAppend(request, ctx) {
6352
6461
  return await electricAgentsStreamAppendRouter.fetch(createStreamAppendRouteRequest(request), ctx.runtime, (req, body) => forwardFetchRequest({
6353
6462
  request: {
@@ -7338,12 +7447,16 @@ function bodyFromBytes(body) {
7338
7447
  return body.buffer.slice(body.byteOffset, body.byteOffset + body.byteLength);
7339
7448
  }
7340
7449
  function responseFromUpstream(response, body) {
7341
- return new Response(body ? bodyFromBytes(body) : response.body, {
7450
+ const responseBody = forbidsResponseBody(response.status) ? null : body !== void 0 ? bodyFromBytes(body) : response.body;
7451
+ return new Response(responseBody, {
7342
7452
  status: response.status,
7343
7453
  statusText: response.statusText,
7344
7454
  headers: responseHeaders(response)
7345
7455
  });
7346
7456
  }
7457
+ function forbidsResponseBody(status$4) {
7458
+ return status$4 === 204 || status$4 === 205 || status$4 === 304;
7459
+ }
7347
7460
  function forwardHeadersFromRequest(request) {
7348
7461
  const headers = new Headers(request.headers);
7349
7462
  headers.delete(`host`);
@@ -7352,6 +7465,45 @@ function forwardHeadersFromRequest(request) {
7352
7465
  function durableStreamsSubscriptionCallback(value) {
7353
7466
  return value.startsWith(DS_SUBSCRIPTION_CALLBACK_PREFIX) ? value.slice(DS_SUBSCRIPTION_CALLBACK_PREFIX.length) : null;
7354
7467
  }
7468
+ function resolveWebhookSigner(ctx) {
7469
+ return ctx.webhookSigner ?? getDefaultWebhookSigner();
7470
+ }
7471
+ function durableStreamsWebhookJwksUrl(ctx) {
7472
+ if (!ctx.durableStreamsRouting) return (0, __electric_ax_agents_runtime.appendPathToUrl)(ctx.durableStreamsUrl, `/__ds/jwks.json`);
7473
+ return resolveDurableStreamsRoutingAdapter(ctx.durableStreamsRouting, ctx.durableStreamsUrl).controlUrl({
7474
+ durableStreamsUrl: ctx.durableStreamsUrl,
7475
+ serviceId: ctx.service,
7476
+ requestUrl: (0, __electric_ax_agents_runtime.appendPathToUrl)(ctx.publicUrl, `/__ds/jwks.json`)
7477
+ }).toString();
7478
+ }
7479
+ function durableStreamsJwksFetchClient(ctx) {
7480
+ return async (input, init) => {
7481
+ const headers = new Headers(init?.headers);
7482
+ await applyDurableStreamsBearer(headers, ctx.durableStreamsBearer, { overwrite: false });
7483
+ const nextInit = {
7484
+ ...init ?? {},
7485
+ headers
7486
+ };
7487
+ if (ctx.durableStreamsDispatcher) nextInit.dispatcher = ctx.durableStreamsDispatcher;
7488
+ return await fetch(input, nextInit);
7489
+ };
7490
+ }
7491
+ function resolveDurableStreamsWebhookSignature(ctx) {
7492
+ if (ctx.durableStreamsWebhookSignature === false) return false;
7493
+ return {
7494
+ jwksUrl: ctx.durableStreamsWebhookSignature?.jwksUrl ?? durableStreamsWebhookJwksUrl(ctx),
7495
+ toleranceSeconds: ctx.durableStreamsWebhookSignature?.toleranceSeconds,
7496
+ cacheTtlMs: ctx.durableStreamsWebhookSignature?.cacheTtlMs,
7497
+ fetchClient: ctx.durableStreamsWebhookSignature?.fetchClient ?? durableStreamsJwksFetchClient(ctx)
7498
+ };
7499
+ }
7500
+ async function verifyDurableStreamsWebhook(request, ctx, body) {
7501
+ const config = resolveDurableStreamsWebhookSignature(ctx);
7502
+ if (config === false) return null;
7503
+ const verification = await (0, __electric_ax_agents_runtime.verifyWebhookSignature)(body, request.headers.get(`webhook-signature`), config);
7504
+ if (verification.ok) return null;
7505
+ return apiError(verification.status, verification.status === 401 ? ErrCodeUnauthorized : `WEBHOOK_SIGNATURE_UNAVAILABLE`, verification.error);
7506
+ }
7355
7507
  function claimTokenFromRequest(request) {
7356
7508
  const electricClaimToken = request.headers.get(`electric-claim-token`)?.trim();
7357
7509
  if (electricClaimToken) return electricClaimToken;
@@ -7385,7 +7537,10 @@ async function webhookForward(request, ctx) {
7385
7537
  const rootSpan = getRequestSpan(request);
7386
7538
  rootSpan?.updateName(`webhook-forward`);
7387
7539
  rootSpan?.setAttribute(`electric_agents.webhook.subscription_id`, subscriptionId);
7388
- const lookupPromise = tracer.startActiveSpan(`db.lookupSubscription`, async (span) => {
7540
+ const body = await readRequestBody(request);
7541
+ const signatureError = await verifyDurableStreamsWebhook(request, ctx, body);
7542
+ if (signatureError) return signatureError;
7543
+ const targetWebhookUrl = await tracer.startActiveSpan(`db.lookupSubscription`, async (span) => {
7389
7544
  try {
7390
7545
  const rows = await ctx.pgDb.select().from(subscriptionWebhooks).where((0, drizzle_orm.and)((0, drizzle_orm.eq)(subscriptionWebhooks.tenantId, ctx.service), (0, drizzle_orm.eq)(subscriptionWebhooks.subscriptionId, subscriptionId))).limit(1);
7391
7546
  return rows[0]?.webhookUrl ?? null;
@@ -7393,7 +7548,6 @@ async function webhookForward(request, ctx) {
7393
7548
  span.end();
7394
7549
  }
7395
7550
  });
7396
- const [targetWebhookUrl, body] = await Promise.all([lookupPromise, readRequestBody(request)]);
7397
7551
  if (!targetWebhookUrl) return apiError(404, ErrCodeSubscriptionNotFound, `Unknown webhook subscription`);
7398
7552
  const parsedBodyResult = validateOptionalJsonBody(webhookForwardBodySchema, body, request.headers.get(`content-type`));
7399
7553
  if (!parsedBodyResult.ok) return parsedBodyResult.response;
@@ -7482,6 +7636,7 @@ async function webhookForward(request, ctx) {
7482
7636
  const headers = forwardHeadersFromRequest(request);
7483
7637
  headers.set(`content-type`, `application/json`);
7484
7638
  headers.delete(`content-length`);
7639
+ headers.set(`webhook-signature`, await resolveWebhookSigner(ctx).sign(forwardBody));
7485
7640
  let upstream;
7486
7641
  try {
7487
7642
  upstream = await tracer.startActiveSpan(`fetch.agent-handler`, async (span) => {
@@ -7569,8 +7724,9 @@ async function callbackForward(request, ctx) {
7569
7724
  serverLog.info(`[callback-forward] done received for stream=${target.primaryStream} consumer=${consumerId}`);
7570
7725
  const stillOwnsClaim = ctx.runtime.claimWriteTokens.owns(ctx.service, target.primaryStream, consumerId);
7571
7726
  const entity = await ctx.entityManager.registry.getEntityByStream(target.primaryStream);
7572
- if (entity && stillOwnsClaim) {
7573
- if (epoch !== void 0) await ctx.entityManager.registry.materializeReleasedClaim?.({
7727
+ let entityCleared = false;
7728
+ if (epoch !== void 0) {
7729
+ const result = await ctx.entityManager.registry.materializeReleasedClaim?.({
7574
7730
  consumerId,
7575
7731
  epoch,
7576
7732
  ackedStreams: Array.isArray(requestBody?.acks) ? requestBody.acks.flatMap((ack) => {
@@ -7582,13 +7738,15 @@ async function callbackForward(request, ctx) {
7582
7738
  }] : [];
7583
7739
  }) : void 0
7584
7740
  });
7741
+ entityCleared = result?.entityCleared ?? false;
7742
+ }
7743
+ if (entity && (entityCleared || stillOwnsClaim)) {
7585
7744
  await ctx.entityManager.registry.updateStatus(entity.url, `idle`);
7586
- ctx.runtime.claimWriteTokens.clearStream(ctx.service, target.primaryStream);
7587
7745
  await ctx.entityBridgeManager.onEntityChanged(entity.url);
7588
7746
  serverLog.info(`[callback-forward] status updated to idle for ${entity.url}`);
7589
- } else if (stillOwnsClaim) ctx.runtime.claimWriteTokens.clearStream(ctx.service, target.primaryStream);
7590
- else if (entity) serverLog.info(`[callback-forward] done ignored for stale claim stream=${target.primaryStream} consumer=${consumerId}`);
7591
- else serverLog.warn(`[callback-forward] done received but no entity found for stream=${target.primaryStream}`);
7747
+ } else if (!entity) serverLog.warn(`[callback-forward] done received but no entity found for stream=${target.primaryStream}`);
7748
+ if (stillOwnsClaim) ctx.runtime.claimWriteTokens.clearStream(ctx.service, target.primaryStream);
7749
+ else if (entity) serverLog.info(`[callback-forward] done arrived after in-memory token evicted (stream=${target.primaryStream} consumer=${consumerId})`);
7592
7750
  } else if (requestBody?.done === true) serverLog.warn(`[callback-forward] done received but skipped: upstream.ok=${upstream.ok} primaryStream=${target.primaryStream ?? `null`} consumer=${consumerId}`);
7593
7751
  } catch (err) {
7594
7752
  serverLog.error(`[callback-forward] error processing done for consumer=${consumerId}: ${err instanceof Error ? err.message : String(err)}`);
@@ -7646,9 +7804,12 @@ exports.DEFAULT_TENANT_ID = DEFAULT_TENANT_ID
7646
7804
  exports.StreamClient = StreamClient
7647
7805
  exports.UnregisteredTenantError = UnregisteredTenantError
7648
7806
  exports.createDb = createDb
7807
+ exports.createEd25519WebhookSigner = createEd25519WebhookSigner
7808
+ exports.getDefaultWebhookSigner = getDefaultWebhookSigner
7649
7809
  exports.globalRouter = globalRouter
7650
7810
  exports.isUnregisteredTenantError = isUnregisteredTenantError
7651
7811
  exports.pathPrefixedSingleTenantDurableStreamsRoutingAdapter = pathPrefixedSingleTenantDurableStreamsRoutingAdapter
7652
7812
  exports.runMigrations = runMigrations
7653
7813
  exports.streamRootDurableStreamsRoutingAdapter = streamRootDurableStreamsRoutingAdapter
7654
- exports.tenantRootDurableStreamsRoutingAdapter = tenantRootDurableStreamsRoutingAdapter
7814
+ exports.tenantRootDurableStreamsRoutingAdapter = tenantRootDurableStreamsRoutingAdapter
7815
+ exports.webhookSigningMetadata = webhookSigningMetadata
package/dist/index.d.cts CHANGED
@@ -187,11 +187,12 @@ import * as drizzle_orm_pg_core238 from "drizzle-orm/pg-core";
187
187
  import * as drizzle_orm_pg_core239 from "drizzle-orm/pg-core";
188
188
  import * as drizzle_orm73 from "drizzle-orm";
189
189
  import * as drizzle_orm74 from "drizzle-orm";
190
- import { EntityTags, WebhookNotification } from "@electric-ax/agents-runtime";
190
+ import { EntityTags, WebhookNotification, WebhookSignatureVerifierConfig } from "@electric-ax/agents-runtime";
191
191
  import "@sinclair/typebox";
192
192
  import { MaybePromise } from "@durable-streams/client";
193
193
  import { AutoRouterType, IRequest } from "itty-router";
194
194
  import { Agent } from "undici";
195
+ import { JsonWebKey, KeyObject } from "node:crypto";
195
196
 
196
197
  //#region rolldown:runtime
197
198
  declare namespace schema_d_exports {
@@ -3523,7 +3524,10 @@ declare class PostgresRegistry {
3523
3524
  setRunnerAdminStatus(runnerId: string, adminStatus: RunnerAdminStatus): Promise<ElectricAgentsRunner | null>;
3524
3525
  materializeActiveClaim(input: MaterializeActiveClaimInput): Promise<void>;
3525
3526
  materializeHeartbeatClaim(input: MaterializeHeartbeatClaimInput): Promise<void>;
3526
- materializeReleasedClaim(input: MaterializeReleasedClaimInput): Promise<ConsumerClaim | null>;
3527
+ materializeReleasedClaim(input: MaterializeReleasedClaimInput): Promise<{
3528
+ claim: ConsumerClaim | null;
3529
+ entityCleared: boolean;
3530
+ }>;
3527
3531
  getActiveClaimsForRunner(runnerId: string): Promise<Array<ConsumerClaim>>;
3528
3532
  getDispatchStatsForRunner(runnerId: string): Promise<{
3529
3533
  entities_with_active_claim: number;
@@ -3631,11 +3635,15 @@ interface SubscriptionResponse {
3631
3635
  streams?: Array<string | SubscriptionStreamInfo>;
3632
3636
  webhook?: {
3633
3637
  url?: string;
3638
+ signing?: {
3639
+ alg?: string;
3640
+ kid?: string;
3641
+ jwks_url?: string;
3642
+ };
3634
3643
  };
3635
3644
  wake_stream?: string;
3636
3645
  callback_url?: string;
3637
3646
  callback_token?: string;
3638
- webhook_secret?: string;
3639
3647
  }
3640
3648
  interface SubscriptionCreateInput {
3641
3649
  type: `webhook` | `pull-wake`;
@@ -3691,10 +3699,7 @@ declare class StreamClient {
3691
3699
  contentType: string;
3692
3700
  }): Promise<void>;
3693
3701
  exists(path: string): Promise<boolean>;
3694
- createSubscription(pattern: string, subscriptionId: string, webhookUrl: string, description?: string): Promise<{
3695
- subscription_id: string;
3696
- webhook_secret?: string;
3697
- }>;
3702
+ createSubscription(pattern: string, subscriptionId: string, webhookUrl: string, description?: string): Promise<SubscriptionResponse>;
3698
3703
  putSubscription(subscriptionId: string, input: SubscriptionCreateInput): Promise<SubscriptionResponse>;
3699
3704
  getSubscription(subscriptionId: string): Promise<SubscriptionResponse | null>;
3700
3705
  deleteSubscription(subscriptionId: string): Promise<void>;
@@ -4368,6 +4373,37 @@ declare const streamRootDurableStreamsRoutingAdapter: DurableStreamsRoutingAdapt
4368
4373
  declare const pathPrefixedSingleTenantDurableStreamsRoutingAdapter: DurableStreamsRoutingAdapter;
4369
4374
  declare const tenantRootDurableStreamsRoutingAdapter: DurableStreamsRoutingAdapter;
4370
4375
 
4376
+ //#endregion
4377
+ //#region src/webhook-signing.d.ts
4378
+ interface WebhookPublicJwk {
4379
+ kty: `OKP`;
4380
+ crv: `Ed25519`;
4381
+ x: string;
4382
+ kid: string;
4383
+ use: `sig`;
4384
+ alg: `EdDSA`;
4385
+ }
4386
+ interface WebhookJwks {
4387
+ keys: Array<WebhookPublicJwk>;
4388
+ }
4389
+ interface WebhookSigningMetadata {
4390
+ alg: `ed25519`;
4391
+ kid: string;
4392
+ jwks_url: string;
4393
+ }
4394
+ interface WebhookSigner {
4395
+ sign: (body: Uint8Array | string) => string | Promise<string>;
4396
+ jwks: () => WebhookJwks | Promise<WebhookJwks>;
4397
+ }
4398
+ type WebhookSigningKeyInput = string | Buffer | JsonWebKey | KeyObject;
4399
+ interface Ed25519WebhookSignerOptions {
4400
+ privateKey?: WebhookSigningKeyInput;
4401
+ kid?: string;
4402
+ }
4403
+ declare function createEd25519WebhookSigner(options?: Ed25519WebhookSignerOptions): WebhookSigner;
4404
+ declare function getDefaultWebhookSigner(): WebhookSigner;
4405
+ declare function webhookSigningMetadata(signer: WebhookSigner, streamRootUrl: string): Promise<WebhookSigningMetadata>;
4406
+
4371
4407
  //#endregion
4372
4408
  //#region src/routing/context.d.ts
4373
4409
  /**
@@ -4386,6 +4422,8 @@ interface TenantContext {
4386
4422
  durableStreamsBearer?: DurableStreamsBearerProvider;
4387
4423
  durableStreamsRouting?: DurableStreamsRoutingAdapter;
4388
4424
  durableStreamsDispatcher: Agent;
4425
+ durableStreamsWebhookSignature?: false | Partial<WebhookSignatureVerifierConfig>;
4426
+ webhookSigner?: WebhookSigner;
4389
4427
  electricUrl?: string;
4390
4428
  electricSecret?: string;
4391
4429
  ownAgentHandlerPaths?: ReadonlyArray<string>;
@@ -4413,4 +4451,4 @@ declare class UnregisteredTenantError extends Error {
4413
4451
  declare function isUnregisteredTenantError(error: unknown): error is UnregisteredTenantError;
4414
4452
 
4415
4453
  //#endregion
4416
- export { AgentsHost, AgentsHostOptions, AgentsHostTenantConfig, AgentsHostTenantRuntime, AuthenticateRequest, ConsumerClaim, DEFAULT_TENANT_ID, DispatchPolicy, DispatchTarget, DrizzleDB, DurableStreamsBearerProvider, DurableStreamsRoutingAdapter, DurableStreamsRoutingInput, ElectricAgentsRunner, ElectricAgentsUser, EntityBridgeCoordinator, EntityDispatchState, GlobalRoutes, PgClient, Principal, PrincipalKind, PublicWakeNotification, RegisterRunnerRequest, RequestPrincipal, RunnerAdminStatus, RunnerHeartbeatRequest, RunnerKind, RunnerLiveness, SourceStreamOffset, StreamClient, StreamClientOptions, SubscriptionClaimResponse, SubscriptionCreateInput, SubscriptionResponse, SubscriptionStreamInfo, TenantContext, UnregisteredTenantError, WakeNotificationRow, createDb, globalRouter, isUnregisteredTenantError, pathPrefixedSingleTenantDurableStreamsRoutingAdapter, runMigrations, streamRootDurableStreamsRoutingAdapter, tenantRootDurableStreamsRoutingAdapter };
4454
+ export { AgentsHost, AgentsHostOptions, AgentsHostTenantConfig, AgentsHostTenantRuntime, AuthenticateRequest, ConsumerClaim, DEFAULT_TENANT_ID, DispatchPolicy, DispatchTarget, DrizzleDB, DurableStreamsBearerProvider, DurableStreamsRoutingAdapter, DurableStreamsRoutingInput, Ed25519WebhookSignerOptions, ElectricAgentsRunner, ElectricAgentsUser, EntityBridgeCoordinator, EntityDispatchState, GlobalRoutes, PgClient, Principal, PrincipalKind, PublicWakeNotification, RegisterRunnerRequest, RequestPrincipal, RunnerAdminStatus, RunnerHeartbeatRequest, RunnerKind, RunnerLiveness, SourceStreamOffset, StreamClient, StreamClientOptions, SubscriptionClaimResponse, SubscriptionCreateInput, SubscriptionResponse, SubscriptionStreamInfo, TenantContext, UnregisteredTenantError, WakeNotificationRow, WebhookJwks, WebhookPublicJwk, WebhookSigner, WebhookSigningKeyInput, WebhookSigningMetadata, createDb, createEd25519WebhookSigner, getDefaultWebhookSigner, globalRouter, isUnregisteredTenantError, pathPrefixedSingleTenantDurableStreamsRoutingAdapter, runMigrations, streamRootDurableStreamsRoutingAdapter, tenantRootDurableStreamsRoutingAdapter, webhookSigningMetadata };
package/dist/index.d.ts CHANGED
@@ -188,7 +188,8 @@ import * as drizzle_orm_pg_core236 from "drizzle-orm/pg-core";
188
188
  import * as drizzle_orm_pg_core237 from "drizzle-orm/pg-core";
189
189
  import * as drizzle_orm_pg_core238 from "drizzle-orm/pg-core";
190
190
  import * as drizzle_orm_pg_core239 from "drizzle-orm/pg-core";
191
- import { EntityTags, WebhookNotification } from "@electric-ax/agents-runtime";
191
+ import { JsonWebKey, KeyObject } from "node:crypto";
192
+ import { EntityTags, WebhookNotification, WebhookSignatureVerifierConfig } from "@electric-ax/agents-runtime";
192
193
  import { MaybePromise } from "@durable-streams/client";
193
194
  import "@sinclair/typebox";
194
195
  import { AutoRouterType, IRequest } from "itty-router";
@@ -3524,7 +3525,10 @@ declare class PostgresRegistry {
3524
3525
  setRunnerAdminStatus(runnerId: string, adminStatus: RunnerAdminStatus): Promise<ElectricAgentsRunner | null>;
3525
3526
  materializeActiveClaim(input: MaterializeActiveClaimInput): Promise<void>;
3526
3527
  materializeHeartbeatClaim(input: MaterializeHeartbeatClaimInput): Promise<void>;
3527
- materializeReleasedClaim(input: MaterializeReleasedClaimInput): Promise<ConsumerClaim | null>;
3528
+ materializeReleasedClaim(input: MaterializeReleasedClaimInput): Promise<{
3529
+ claim: ConsumerClaim | null;
3530
+ entityCleared: boolean;
3531
+ }>;
3528
3532
  getActiveClaimsForRunner(runnerId: string): Promise<Array<ConsumerClaim>>;
3529
3533
  getDispatchStatsForRunner(runnerId: string): Promise<{
3530
3534
  entities_with_active_claim: number;
@@ -3632,11 +3636,15 @@ interface SubscriptionResponse {
3632
3636
  streams?: Array<string | SubscriptionStreamInfo>;
3633
3637
  webhook?: {
3634
3638
  url?: string;
3639
+ signing?: {
3640
+ alg?: string;
3641
+ kid?: string;
3642
+ jwks_url?: string;
3643
+ };
3635
3644
  };
3636
3645
  wake_stream?: string;
3637
3646
  callback_url?: string;
3638
3647
  callback_token?: string;
3639
- webhook_secret?: string;
3640
3648
  }
3641
3649
  interface SubscriptionCreateInput {
3642
3650
  type: `webhook` | `pull-wake`;
@@ -3692,10 +3700,7 @@ declare class StreamClient {
3692
3700
  contentType: string;
3693
3701
  }): Promise<void>;
3694
3702
  exists(path: string): Promise<boolean>;
3695
- createSubscription(pattern: string, subscriptionId: string, webhookUrl: string, description?: string): Promise<{
3696
- subscription_id: string;
3697
- webhook_secret?: string;
3698
- }>;
3703
+ createSubscription(pattern: string, subscriptionId: string, webhookUrl: string, description?: string): Promise<SubscriptionResponse>;
3699
3704
  putSubscription(subscriptionId: string, input: SubscriptionCreateInput): Promise<SubscriptionResponse>;
3700
3705
  getSubscription(subscriptionId: string): Promise<SubscriptionResponse | null>;
3701
3706
  deleteSubscription(subscriptionId: string): Promise<void>;
@@ -4369,6 +4374,37 @@ declare const streamRootDurableStreamsRoutingAdapter: DurableStreamsRoutingAdapt
4369
4374
  declare const pathPrefixedSingleTenantDurableStreamsRoutingAdapter: DurableStreamsRoutingAdapter;
4370
4375
  declare const tenantRootDurableStreamsRoutingAdapter: DurableStreamsRoutingAdapter;
4371
4376
 
4377
+ //#endregion
4378
+ //#region src/webhook-signing.d.ts
4379
+ interface WebhookPublicJwk {
4380
+ kty: `OKP`;
4381
+ crv: `Ed25519`;
4382
+ x: string;
4383
+ kid: string;
4384
+ use: `sig`;
4385
+ alg: `EdDSA`;
4386
+ }
4387
+ interface WebhookJwks {
4388
+ keys: Array<WebhookPublicJwk>;
4389
+ }
4390
+ interface WebhookSigningMetadata {
4391
+ alg: `ed25519`;
4392
+ kid: string;
4393
+ jwks_url: string;
4394
+ }
4395
+ interface WebhookSigner {
4396
+ sign: (body: Uint8Array | string) => string | Promise<string>;
4397
+ jwks: () => WebhookJwks | Promise<WebhookJwks>;
4398
+ }
4399
+ type WebhookSigningKeyInput = string | Buffer | JsonWebKey | KeyObject;
4400
+ interface Ed25519WebhookSignerOptions {
4401
+ privateKey?: WebhookSigningKeyInput;
4402
+ kid?: string;
4403
+ }
4404
+ declare function createEd25519WebhookSigner(options?: Ed25519WebhookSignerOptions): WebhookSigner;
4405
+ declare function getDefaultWebhookSigner(): WebhookSigner;
4406
+ declare function webhookSigningMetadata(signer: WebhookSigner, streamRootUrl: string): Promise<WebhookSigningMetadata>;
4407
+
4372
4408
  //#endregion
4373
4409
  //#region src/routing/context.d.ts
4374
4410
  /**
@@ -4387,6 +4423,8 @@ interface TenantContext {
4387
4423
  durableStreamsBearer?: DurableStreamsBearerProvider;
4388
4424
  durableStreamsRouting?: DurableStreamsRoutingAdapter;
4389
4425
  durableStreamsDispatcher: Agent;
4426
+ durableStreamsWebhookSignature?: false | Partial<WebhookSignatureVerifierConfig>;
4427
+ webhookSigner?: WebhookSigner;
4390
4428
  electricUrl?: string;
4391
4429
  electricSecret?: string;
4392
4430
  ownAgentHandlerPaths?: ReadonlyArray<string>;
@@ -4414,4 +4452,4 @@ declare class UnregisteredTenantError extends Error {
4414
4452
  declare function isUnregisteredTenantError(error: unknown): error is UnregisteredTenantError;
4415
4453
 
4416
4454
  //#endregion
4417
- export { AgentsHost, AgentsHostOptions, AgentsHostTenantConfig, AgentsHostTenantRuntime, AuthenticateRequest, ConsumerClaim, DEFAULT_TENANT_ID, DispatchPolicy, DispatchTarget, DrizzleDB, DurableStreamsBearerProvider, DurableStreamsRoutingAdapter, DurableStreamsRoutingInput, ElectricAgentsRunner, ElectricAgentsUser, EntityBridgeCoordinator, EntityDispatchState, GlobalRoutes, PgClient, Principal, PrincipalKind, PublicWakeNotification, RegisterRunnerRequest, RequestPrincipal, RunnerAdminStatus, RunnerHeartbeatRequest, RunnerKind, RunnerLiveness, SourceStreamOffset, StreamClient, StreamClientOptions, SubscriptionClaimResponse, SubscriptionCreateInput, SubscriptionResponse, SubscriptionStreamInfo, TenantContext, UnregisteredTenantError, WakeNotificationRow, createDb, globalRouter, isUnregisteredTenantError, pathPrefixedSingleTenantDurableStreamsRoutingAdapter, runMigrations, streamRootDurableStreamsRoutingAdapter, tenantRootDurableStreamsRoutingAdapter };
4455
+ export { AgentsHost, AgentsHostOptions, AgentsHostTenantConfig, AgentsHostTenantRuntime, AuthenticateRequest, ConsumerClaim, DEFAULT_TENANT_ID, DispatchPolicy, DispatchTarget, DrizzleDB, DurableStreamsBearerProvider, DurableStreamsRoutingAdapter, DurableStreamsRoutingInput, Ed25519WebhookSignerOptions, ElectricAgentsRunner, ElectricAgentsUser, EntityBridgeCoordinator, EntityDispatchState, GlobalRoutes, PgClient, Principal, PrincipalKind, PublicWakeNotification, RegisterRunnerRequest, RequestPrincipal, RunnerAdminStatus, RunnerHeartbeatRequest, RunnerKind, RunnerLiveness, SourceStreamOffset, StreamClient, StreamClientOptions, SubscriptionClaimResponse, SubscriptionCreateInput, SubscriptionResponse, SubscriptionStreamInfo, TenantContext, UnregisteredTenantError, WakeNotificationRow, WebhookJwks, WebhookPublicJwk, WebhookSigner, WebhookSigningKeyInput, WebhookSigningMetadata, createDb, createEd25519WebhookSigner, getDefaultWebhookSigner, globalRouter, isUnregisteredTenantError, pathPrefixedSingleTenantDurableStreamsRoutingAdapter, runMigrations, streamRootDurableStreamsRoutingAdapter, tenantRootDurableStreamsRoutingAdapter, webhookSigningMetadata };