@electric-ax/agents-server 0.4.19 → 0.4.20

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.
@@ -4,7 +4,7 @@ import { DurableStreamTestServer } from "@durable-streams/server";
4
4
  import { createServer } from "node:http";
5
5
  import { createServerAdapter } from "@whatwg-node/server";
6
6
  import { Agent } from "undici";
7
- import { COMPOSER_INPUT_MESSAGE_TYPE, appendPathToUrl, assertTags, buildEventSourceManifestEntry, buildTagsIndex, createEntityRegistry, createRuntimeHandler, entityStateSchema, eventSourceSubscriptionManifestKey, getCronStreamPath, getCronStreamPathFromSpec, getEntitiesStreamPath, getNextCronFireAt, getSharedStateStreamPath, getWebhookStreamPath, hashString, manifestChildKey, manifestSharedStateKey, manifestSourceKey, normalizeTags, parseCronStreamPath, resolveCronScheduleSpec, resolveEventSourceSubscription, sourceRefForTags, validateComposerInputPayload, validateSlashCommandDefinitions, verifyWebhookSignature } from "@electric-ax/agents-runtime";
7
+ import { COMPOSER_INPUT_MESSAGE_TYPE, appendPathToUrl, assertTags, buildEventSourceManifestEntry, buildTagsIndex, canonicalPgSyncOptions, createEntityRegistry, createRuntimeHandler, entityStateSchema, eventSourceSubscriptionManifestKey, getCronStreamPath, getCronStreamPathFromSpec, getEntitiesStreamPath, getNextCronFireAt, getPgSyncStreamPath, getSharedStateStreamPath, getWebhookStreamPath, hashString, manifestChildKey, manifestSharedStateKey, manifestSourceKey, normalizeTags, parseCronStreamPath, resolveCronScheduleSpec, resolveEventSourceSubscription, sourceRefForPgSync, sourceRefForTags, validateComposerInputPayload, validateSlashCommandDefinitions, verifyWebhookSignature } from "@electric-ax/agents-runtime";
8
8
  import fs, { existsSync } from "node:fs";
9
9
  import path, { dirname, resolve } from "node:path";
10
10
  import { drizzle } from "drizzle-orm/postgres-js";
@@ -47,6 +47,7 @@ __export(schema_exports, {
47
47
  entityPermissionGrants: () => entityPermissionGrants,
48
48
  entityTypePermissionGrants: () => entityTypePermissionGrants,
49
49
  entityTypes: () => entityTypes,
50
+ pgSyncBridges: () => pgSyncBridges,
50
51
  runnerRuntimeDiagnostics: () => runnerRuntimeDiagnostics,
51
52
  runners: () => runners,
52
53
  scheduledTasks: () => scheduledTasks,
@@ -368,6 +369,18 @@ const scheduledTasks = pgTable(`scheduled_tasks`, {
368
369
  index(`idx_scheduled_tasks_manifest_pending`).on(table.tenantId, table.ownerEntityUrl, table.manifestKey).where(sql`${table.kind} = 'delayed_send' AND ${table.completedAt} IS NULL AND ${table.manifestKey} IS NOT NULL`),
369
370
  index(`idx_scheduled_tasks_stale_claims`).on(table.tenantId, table.claimedAt).where(sql`${table.completedAt} IS NULL AND ${table.claimedAt} IS NOT NULL`)
370
371
  ]);
372
+ const pgSyncBridges = pgTable(`pg_sync_bridges`, {
373
+ tenantId: text(`tenant_id`).notNull().default(`default`),
374
+ sourceRef: text(`source_ref`).notNull(),
375
+ options: jsonb(`options`).notNull(),
376
+ streamUrl: text(`stream_url`).notNull(),
377
+ shapeHandle: text(`shape_handle`),
378
+ shapeOffset: text(`shape_offset`),
379
+ initialSnapshotComplete: boolean(`initial_snapshot_complete`).notNull().default(false),
380
+ lastTouchedAt: timestamp(`last_touched_at`, { withTimezone: true }).notNull().defaultNow(),
381
+ createdAt: timestamp(`created_at`, { withTimezone: true }).notNull().defaultNow(),
382
+ updatedAt: timestamp(`updated_at`, { withTimezone: true }).notNull().defaultNow()
383
+ }, (table) => [primaryKey({ columns: [table.tenantId, table.sourceRef] }), unique(`uq_pg_sync_bridges_stream_url`).on(table.tenantId, table.streamUrl)]);
371
384
  const entityBridges = pgTable(`entity_bridges`, {
372
385
  tenantId: text(`tenant_id`).notNull().default(`default`),
373
386
  sourceRef: text(`source_ref`).notNull(),
@@ -1134,6 +1147,22 @@ async function fileExists(filePath) {
1134
1147
  return false;
1135
1148
  }
1136
1149
  }
1150
+ /**
1151
+ * Raised when an Electric shape proxy request must be rejected for security
1152
+ * reasons (an un-scoped table, or a client `where` clause that could escape the
1153
+ * enforced per-tenant/per-principal scoping). The global `errorMapper` hook
1154
+ * maps this to an HTTP error response. Defined here (rather than reusing
1155
+ * `ElectricAgentsError`) to keep this module free of the heavy entity-manager
1156
+ * import graph.
1157
+ */
1158
+ var ElectricProxyError = class extends Error {
1159
+ constructor(code, message, status$1) {
1160
+ super(message);
1161
+ this.code = code;
1162
+ this.status = status$1;
1163
+ this.name = `ElectricProxyError`;
1164
+ }
1165
+ };
1137
1166
  function buildElectricProxyTarget(options) {
1138
1167
  const targetPath = options.incomingUrl.pathname.replace(`/_electric/electric`, ``);
1139
1168
  const target = electricUrlWithPath(options.electricUrl, targetPath);
@@ -1143,7 +1172,12 @@ function buildElectricProxyTarget(options) {
1143
1172
  applyElectricUrlQueryParams(target, options.electricUrl);
1144
1173
  if (targetPath !== `/v1/shape`) return target;
1145
1174
  if (options.electricSecret) target.searchParams.set(`secret`, options.electricSecret);
1146
- const table = options.incomingUrl.searchParams.get(`table`);
1175
+ const clientWhere = options.incomingUrl.searchParams.get(`where`);
1176
+ if (clientWhere !== null && !isSelfContainedWhereClause(clientWhere)) throw new ElectricProxyError(`INVALID_WHERE`, `Invalid where clause`, 400);
1177
+ const tableParams = options.incomingUrl.searchParams.getAll(`table`);
1178
+ if (tableParams.length !== 1) throw new ElectricProxyError(`TABLE_NOT_ALLOWED`, `Table is not available through the Electric proxy`, 403);
1179
+ const table = tableParams[0];
1180
+ target.searchParams.set(`table`, table);
1147
1181
  if (table === `entities`) {
1148
1182
  target.searchParams.set(`columns`, `"tenant_id","url","type","status","dispatch_policy","tags","spawn_args","sandbox","parent","created_by","type_revision","inbox_schemas","state_schemas","created_at","updated_at"`);
1149
1183
  applyShapeWhere(target, buildReadableEntitiesWhere({
@@ -1201,9 +1235,39 @@ function buildElectricProxyTarget(options) {
1201
1235
  principalKind: options.principalKind ?? ``,
1202
1236
  permissionBypass: options.permissionBypass
1203
1237
  }));
1204
- }
1238
+ } else throw new ElectricProxyError(`TABLE_NOT_ALLOWED`, `Table is not available through the Electric proxy`, 403);
1205
1239
  return target;
1206
1240
  }
1241
+ /**
1242
+ * Returns true when a client-supplied Electric `where` clause is self-contained:
1243
+ * its parentheses are balanced, never close below the top level, all string
1244
+ * (`'`) and identifier (`"`) literals are terminated, and it contains no SQL
1245
+ * comment markers. Such a clause cannot break out of the `(...)` group it is
1246
+ * wrapped in when AND-combined with the enforced scoping predicate, nor comment
1247
+ * out the trailing paren the proxy appends. Characters inside string/identifier
1248
+ * literals are ignored. Comment markers are rejected unconditionally (even where
1249
+ * harmless) as a conservative defensive measure; dollar-quoted and `E''` strings
1250
+ * are not modeled and only ever cause fail-safe over-rejection, never a bypass.
1251
+ */
1252
+ function isSelfContainedWhereClause(where) {
1253
+ let depth = 0;
1254
+ let quote = null;
1255
+ for (let i = 0; i < where.length; i++) {
1256
+ const ch = where[i];
1257
+ if (quote !== null) {
1258
+ if (ch === quote) if (where[i + 1] === quote) i++;
1259
+ else quote = null;
1260
+ continue;
1261
+ }
1262
+ if (ch === `'` || ch === `"`) quote = ch;
1263
+ else if (ch === `(`) depth++;
1264
+ else if (ch === `)`) {
1265
+ depth--;
1266
+ if (depth < 0) return false;
1267
+ } else if (ch === `-` && where[i + 1] === `-` || ch === `/` && where[i + 1] === `*`) return false;
1268
+ }
1269
+ return depth === 0 && quote === null;
1270
+ }
1207
1271
  function buildReadableEntitiesWhere(options) {
1208
1272
  const tenant = sqlStringLiteral$2(options.tenantId);
1209
1273
  if (options.permissionBypass) return `tenant_id = ${tenant}`;
@@ -2721,6 +2785,9 @@ var PostgresRegistry = class {
2721
2785
  entityBridgeWhere(sourceRef) {
2722
2786
  return and(eq(entityBridges.tenantId, this.tenantId), eq(entityBridges.sourceRef, sourceRef));
2723
2787
  }
2788
+ pgSyncBridgeWhere(sourceRef) {
2789
+ return and(eq(pgSyncBridges.tenantId, this.tenantId), eq(pgSyncBridges.sourceRef, sourceRef));
2790
+ }
2724
2791
  async createEntityType(et) {
2725
2792
  await this.db.insert(entityTypes).values({
2726
2793
  tenantId: this.tenantId,
@@ -3190,11 +3257,12 @@ var PostgresRegistry = class {
3190
3257
  };
3191
3258
  const nextTags = normalizeTags(mutation.nextTags);
3192
3259
  const updatedAt = Date.now();
3193
- await tx.update(entities).set({
3260
+ const [updateResult] = await tx.update(entities).set({
3194
3261
  tags: nextTags,
3195
3262
  tagsIndex: buildTagsIndex(nextTags),
3196
3263
  updatedAt
3197
- }).where(this.entityWhere(url));
3264
+ }).where(this.entityWhere(url)).returning({ txid: sql`pg_current_xact_id()::xid::text` });
3265
+ const txid = updateResult ? parseInt(updateResult.txid) : void 0;
3198
3266
  await tx.insert(tagStreamOutbox).values({
3199
3267
  tenantId: this.tenantId,
3200
3268
  entityUrl: url,
@@ -3212,10 +3280,63 @@ var PostgresRegistry = class {
3212
3280
  return {
3213
3281
  entity,
3214
3282
  changed: true,
3215
- ...op === `insert` || op === `update` ? { op } : {}
3283
+ ...op === `insert` || op === `update` ? { op } : {},
3284
+ ...txid !== void 0 ? { txid } : {}
3216
3285
  };
3217
3286
  });
3218
3287
  }
3288
+ async upsertPgSyncBridge(row) {
3289
+ await this.db.insert(pgSyncBridges).values({
3290
+ tenantId: this.tenantId,
3291
+ sourceRef: row.sourceRef,
3292
+ options: row.options,
3293
+ streamUrl: row.streamUrl,
3294
+ lastTouchedAt: new Date(),
3295
+ updatedAt: new Date()
3296
+ }).onConflictDoUpdate({
3297
+ target: [pgSyncBridges.tenantId, pgSyncBridges.sourceRef],
3298
+ set: {
3299
+ options: row.options,
3300
+ streamUrl: row.streamUrl,
3301
+ initialSnapshotComplete: false,
3302
+ lastTouchedAt: new Date(),
3303
+ updatedAt: new Date()
3304
+ }
3305
+ });
3306
+ const existing = await this.getPgSyncBridge(row.sourceRef);
3307
+ if (!existing) throw new Error(`Failed to load pgSync bridge ${row.sourceRef}`);
3308
+ return existing;
3309
+ }
3310
+ async getPgSyncBridge(sourceRef) {
3311
+ const rows = await this.db.select().from(pgSyncBridges).where(this.pgSyncBridgeWhere(sourceRef)).limit(1);
3312
+ return rows[0] ? this.rowToPgSyncBridge(rows[0]) : null;
3313
+ }
3314
+ async listPgSyncBridges(tenantId = this.tenantId) {
3315
+ const rows = tenantId === null ? await this.db.select().from(pgSyncBridges) : await this.db.select().from(pgSyncBridges).where(eq(pgSyncBridges.tenantId, tenantId));
3316
+ return rows.map((row) => this.rowToPgSyncBridge(row));
3317
+ }
3318
+ async touchPgSyncBridge(sourceRef) {
3319
+ await this.db.update(pgSyncBridges).set({
3320
+ lastTouchedAt: new Date(),
3321
+ updatedAt: new Date()
3322
+ }).where(this.pgSyncBridgeWhere(sourceRef));
3323
+ }
3324
+ async updatePgSyncBridgeCursor(sourceRef, shapeHandle, shapeOffset, initialSnapshotComplete) {
3325
+ await this.db.update(pgSyncBridges).set({
3326
+ shapeHandle,
3327
+ shapeOffset,
3328
+ ...initialSnapshotComplete !== void 0 ? { initialSnapshotComplete } : {},
3329
+ updatedAt: new Date()
3330
+ }).where(this.pgSyncBridgeWhere(sourceRef));
3331
+ }
3332
+ async clearPgSyncBridgeCursor(sourceRef) {
3333
+ await this.db.update(pgSyncBridges).set({
3334
+ shapeHandle: null,
3335
+ shapeOffset: null,
3336
+ initialSnapshotComplete: false,
3337
+ updatedAt: new Date()
3338
+ }).where(this.pgSyncBridgeWhere(sourceRef));
3339
+ }
3219
3340
  async upsertEntityBridge(row) {
3220
3341
  await this.db.insert(entityBridges).values({
3221
3342
  tenantId: this.tenantId,
@@ -3435,6 +3556,20 @@ var PostgresRegistry = class {
3435
3556
  updated_at: row.updatedAt
3436
3557
  };
3437
3558
  }
3559
+ rowToPgSyncBridge(row) {
3560
+ return {
3561
+ tenantId: row.tenantId,
3562
+ sourceRef: row.sourceRef,
3563
+ options: row.options,
3564
+ streamUrl: row.streamUrl,
3565
+ shapeHandle: row.shapeHandle ?? void 0,
3566
+ shapeOffset: row.shapeOffset ?? void 0,
3567
+ initialSnapshotComplete: row.initialSnapshotComplete,
3568
+ lastTouchedAt: row.lastTouchedAt,
3569
+ createdAt: row.createdAt,
3570
+ updatedAt: row.updatedAt
3571
+ };
3572
+ }
3438
3573
  rowToEntityBridge(row) {
3439
3574
  return {
3440
3575
  tenantId: row.tenantId,
@@ -3515,6 +3650,9 @@ var PostgresRegistry = class {
3515
3650
  function isRecord$1(value) {
3516
3651
  return typeof value === `object` && value !== null && !Array.isArray(value);
3517
3652
  }
3653
+ function getPgSyncManifestStreamPath(sourceRef) {
3654
+ return `/_electric/pg-sync/${sourceRef}`;
3655
+ }
3518
3656
  function extractManifestSourceUrl(manifest) {
3519
3657
  if (!manifest) return void 0;
3520
3658
  if (manifest.kind === `child` || manifest.kind === `observe`) return typeof manifest.entity_url === `string` ? manifest.entity_url : void 0;
@@ -3527,6 +3665,7 @@ function extractManifestSourceUrl(manifest) {
3527
3665
  }
3528
3666
  if (manifest.sourceType === `entities`) return typeof manifest.sourceRef === `string` ? `/_entities/${manifest.sourceRef}` : void 0;
3529
3667
  if (manifest.sourceType === `db`) return typeof manifest.sourceRef === `string` ? getSharedStateStreamPath(manifest.sourceRef) : void 0;
3668
+ if (manifest.sourceType === `pgSync`) return typeof manifest.sourceRef === `string` ? getPgSyncManifestStreamPath(manifest.sourceRef) : void 0;
3530
3669
  if (manifest.sourceType === `webhook`) {
3531
3670
  if (typeof config?.streamUrl === `string`) return config.streamUrl;
3532
3671
  if (typeof config?.endpointKey === `string`) return getWebhookStreamPath(config.endpointKey, typeof config.bucket === `string` ? config.bucket : void 0);
@@ -3641,6 +3780,13 @@ function isRecord(value) {
3641
3780
  function cloneRecord(value) {
3642
3781
  return JSON.parse(JSON.stringify(value));
3643
3782
  }
3783
+ function withOptionalTxid(entity, txid) {
3784
+ if (txid === void 0) return entity;
3785
+ return {
3786
+ ...entity,
3787
+ txid
3788
+ };
3789
+ }
3644
3790
  /**
3645
3791
  * Orchestrates the Electric Agents entity lifecycle: register types, spawn, send, kill.
3646
3792
  *
@@ -4782,15 +4928,17 @@ var EntityManager = class {
4782
4928
  await this.registry.updateStatus(entityUrl, `idle`);
4783
4929
  await this.entityBridgeManager?.onEntityChanged(entityUrl);
4784
4930
  }
4931
+ const txid = crypto.randomUUID();
4785
4932
  const envelope = entityStateSchema.inbox.insert({
4786
4933
  key,
4787
- value
4934
+ value,
4935
+ headers: { txid }
4788
4936
  });
4789
4937
  const encoded = this.encodeChangeEvent(envelope);
4790
4938
  try {
4791
4939
  if (opts?.producerId) {
4792
4940
  await this.streamClient.appendIdempotent(entity.streams.main, encoded, { producerId: opts.producerId });
4793
- return;
4941
+ return { txid };
4794
4942
  }
4795
4943
  await this.streamClient.append(entity.streams.main, encoded);
4796
4944
  if (entity.type === `principal` && req.type === `update_identity`) {
@@ -4798,9 +4946,11 @@ var EntityManager = class {
4798
4946
  await this.streamClient.append(entity.streams.main, this.encodeChangeEvent({
4799
4947
  type: `identity`,
4800
4948
  key: `self`,
4801
- value: identity
4949
+ value: identity,
4950
+ headers: { txid }
4802
4951
  }));
4803
4952
  }
4953
+ return { txid };
4804
4954
  } catch (err) {
4805
4955
  if (this.isClosedStreamError(err)) throw new ElectricAgentsError(ErrCodeNotRunning, `Entity is stopped`, 409);
4806
4956
  throw err;
@@ -4821,18 +4971,26 @@ var EntityManager = class {
4821
4971
  if (req.status === `cancelled`) value.cancelled_at = now;
4822
4972
  }
4823
4973
  if (Object.keys(value).length === 0) throw new ElectricAgentsError(ErrCodeInvalidRequest, `No inbox fields to update`, 400);
4974
+ const txid = crypto.randomUUID();
4824
4975
  const envelope = entityStateSchema.inbox.update({
4825
4976
  key,
4826
- value
4977
+ value,
4978
+ headers: { txid }
4827
4979
  });
4828
4980
  await this.streamClient.append(entity.streams.main, this.encodeChangeEvent(envelope));
4981
+ return { txid };
4829
4982
  }
4830
4983
  async deleteInboxMessage(entityUrl, key) {
4831
4984
  const entity = await this.registry.getEntity(entityUrl);
4832
4985
  if (!entity) throw new ElectricAgentsError(ErrCodeNotFound, `Entity not found`, 404);
4833
4986
  if (rejectsNormalWrites(entity.status)) throw new ElectricAgentsError(ErrCodeNotRunning, `Entity is not accepting writes`, 409);
4834
- const envelope = entityStateSchema.inbox.delete({ key });
4987
+ const txid = crypto.randomUUID();
4988
+ const envelope = entityStateSchema.inbox.delete({
4989
+ key,
4990
+ headers: { txid }
4991
+ });
4835
4992
  await this.streamClient.append(entity.streams.main, this.encodeChangeEvent(envelope));
4993
+ return { txid };
4836
4994
  }
4837
4995
  isAttachmentStreamPath(path$1) {
4838
4996
  return /^\/[^/]+\/[^/]+\/attachments\/[^/]+$/.test(path$1);
@@ -4921,28 +5079,26 @@ var EntityManager = class {
4921
5079
  await this.streamClient.delete(attachment.streamPath).catch(() => void 0);
4922
5080
  return { txid };
4923
5081
  }
4924
- async setTag(entityUrl, key, req, token) {
5082
+ async setTag(entityUrl, key, req) {
4925
5083
  const entity = await this.registry.getEntity(entityUrl);
4926
5084
  if (!entity) throw new ElectricAgentsError(ErrCodeNotFound, `Entity not found`, 404);
4927
- if (!this.isValidWriteToken(entity, token)) throw new ElectricAgentsError(ErrCodeUnauthorized, `Invalid write token`, 401);
4928
5085
  if (rejectsNormalWrites(entity.status)) throw new ElectricAgentsError(ErrCodeNotRunning, `Entity is not accepting writes`, 409);
4929
5086
  if (typeof req.value !== `string`) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Tag values must be strings`, 400);
4930
5087
  const result = await this.registry.setEntityTag(entityUrl, key, req.value);
4931
5088
  const updated = result.entity;
4932
5089
  if (!updated) throw new ElectricAgentsError(ErrCodeEntityPersistFailed, `Entity not found after tag write`, 500);
4933
5090
  if (result.changed && this.entityBridgeManager) await this.entityBridgeManager.onEntityChanged(entityUrl);
4934
- return updated;
5091
+ return withOptionalTxid(updated, result.txid);
4935
5092
  }
4936
- async deleteTag(entityUrl, key, token) {
5093
+ async deleteTag(entityUrl, key) {
4937
5094
  const entity = await this.registry.getEntity(entityUrl);
4938
5095
  if (!entity) throw new ElectricAgentsError(ErrCodeNotFound, `Entity not found`, 404);
4939
- if (!this.isValidWriteToken(entity, token)) throw new ElectricAgentsError(ErrCodeUnauthorized, `Invalid write token`, 401);
4940
5096
  if (rejectsNormalWrites(entity.status)) throw new ElectricAgentsError(ErrCodeNotRunning, `Entity is not accepting writes`, 409);
4941
5097
  const result = await this.registry.removeEntityTag(entityUrl, key);
4942
5098
  const updated = result.entity;
4943
5099
  if (!updated) throw new ElectricAgentsError(ErrCodeEntityPersistFailed, `Entity not found after tag delete`, 500);
4944
5100
  if (result.changed && this.entityBridgeManager) await this.entityBridgeManager.onEntityChanged(entityUrl);
4945
- return updated;
5101
+ return withOptionalTxid(updated, result.txid);
4946
5102
  }
4947
5103
  async ensureEntitiesMembershipStream(tags, principal) {
4948
5104
  if (!this.entityBridgeManager) throw new Error(`Entity bridge manager not configured`);
@@ -6003,7 +6159,7 @@ async function parseAttachmentForm(request) {
6003
6159
  };
6004
6160
  }
6005
6161
  function contentDisposition(filename) {
6006
- const fallback = filename.replace(/["\\\r\n]/g, `_`);
6162
+ const fallback = filename.replace(/[^\x20-\x7e]|["\\]/g, `_`);
6007
6163
  return `attachment; filename="${fallback}"; filename*=UTF-8''${encodeURIComponent(filename)}`;
6008
6164
  }
6009
6165
  function rejectPrincipalEntityMutation(request, action) {
@@ -6201,22 +6357,28 @@ async function deleteEventSourceSubscription(request, ctx) {
6201
6357
  const result = await ctx.entityManager.deleteEventSourceSubscription(entityUrl, { id: decodeURIComponent(request.params.subscriptionId) });
6202
6358
  return json(result);
6203
6359
  }
6360
+ function tagResponseBody(entity) {
6361
+ const publicEntity = toPublicEntity(entity);
6362
+ if (entity.txid !== void 0) return {
6363
+ ...publicEntity,
6364
+ txid: entity.txid
6365
+ };
6366
+ return publicEntity;
6367
+ }
6204
6368
  async function setTag(request, ctx) {
6205
6369
  const principalMutationError = rejectPrincipalEntityMutation(request, `tag updated`);
6206
6370
  if (principalMutationError) return principalMutationError;
6207
6371
  const parsed = routeBody(request);
6208
6372
  const { entityUrl } = requireExistingEntityRoute(request);
6209
- const token = writeTokenFromRequest(request);
6210
- const updated = await ctx.entityManager.setTag(entityUrl, decodeURIComponent(request.params.tagKey), { value: parsed.value }, token);
6211
- return json(toPublicEntity(updated));
6373
+ const updated = await ctx.entityManager.setTag(entityUrl, decodeURIComponent(request.params.tagKey), { value: parsed.value });
6374
+ return json(tagResponseBody(updated));
6212
6375
  }
6213
6376
  async function deleteTag(request, ctx) {
6214
6377
  const principalMutationError = rejectPrincipalEntityMutation(request, `tag deleted`);
6215
6378
  if (principalMutationError) return principalMutationError;
6216
6379
  const { entityUrl } = requireExistingEntityRoute(request);
6217
- const token = writeTokenFromRequest(request);
6218
- const updated = await ctx.entityManager.deleteTag(entityUrl, decodeURIComponent(request.params.tagKey), token);
6219
- return json(toPublicEntity(updated));
6380
+ const updated = await ctx.entityManager.deleteTag(entityUrl, decodeURIComponent(request.params.tagKey));
6381
+ return json(tagResponseBody(updated));
6220
6382
  }
6221
6383
  async function forkEntity(request, ctx) {
6222
6384
  const principalMutationError = rejectPrincipalEntityMutation(request, `forked`);
@@ -6283,9 +6445,12 @@ async function sendEntity(request, ctx) {
6283
6445
  mode: parsed.mode,
6284
6446
  position: parsed.position
6285
6447
  };
6286
- if (parsed.afterMs && parsed.afterMs > 0) await ctx.entityManager.enqueueDelayedSend(entityUrl, sendReq, new Date(Date.now() + parsed.afterMs));
6287
- else await ctx.entityManager.send(entityUrl, sendReq);
6288
- return status(204);
6448
+ if (parsed.afterMs && parsed.afterMs > 0) {
6449
+ await ctx.entityManager.enqueueDelayedSend(entityUrl, sendReq, new Date(Date.now() + parsed.afterMs));
6450
+ return status(204);
6451
+ }
6452
+ const result = await ctx.entityManager.send(entityUrl, sendReq);
6453
+ return json(result);
6289
6454
  }
6290
6455
  async function createAttachment(request, ctx) {
6291
6456
  const principalMutationError = rejectPrincipalEntityMutation(request, `given attachments`);
@@ -6328,13 +6493,13 @@ async function deleteAttachment(request, ctx) {
6328
6493
  async function updateInboxMessage(request, ctx) {
6329
6494
  const parsed = routeBody(request);
6330
6495
  const { entityUrl } = requireExistingEntityRoute(request);
6331
- await ctx.entityManager.updateInboxMessage(entityUrl, decodeURIComponent(request.params.messageKey), parsed);
6332
- return status(204);
6496
+ const result = await ctx.entityManager.updateInboxMessage(entityUrl, decodeURIComponent(request.params.messageKey), parsed);
6497
+ return json(result);
6333
6498
  }
6334
6499
  async function deleteInboxMessage(request, ctx) {
6335
6500
  const { entityUrl } = requireExistingEntityRoute(request);
6336
- await ctx.entityManager.deleteInboxMessage(entityUrl, decodeURIComponent(request.params.messageKey));
6337
- return status(204);
6501
+ const result = await ctx.entityManager.deleteInboxMessage(entityUrl, decodeURIComponent(request.params.messageKey));
6502
+ return json(result);
6338
6503
  }
6339
6504
  async function spawnEntity(request, ctx) {
6340
6505
  const parsed = routeBody(request);
@@ -6605,6 +6770,49 @@ function toPublicEntityType(entityType) {
6605
6770
  };
6606
6771
  }
6607
6772
 
6773
+ //#endregion
6774
+ //#region src/routing/pg-sync-router.ts
6775
+ const pgSyncOptionsSchema = Type.Object({
6776
+ url: Type.Optional(Type.String()),
6777
+ table: Type.String(),
6778
+ columns: Type.Optional(Type.Array(Type.String())),
6779
+ where: Type.Optional(Type.String()),
6780
+ params: Type.Optional(Type.Union([Type.Array(Type.String()), Type.Record(Type.String(), Type.String())])),
6781
+ replica: Type.Optional(Type.Union([Type.Literal(`default`), Type.Literal(`full`)]))
6782
+ });
6783
+ const pgSyncRequestMetadataSchema = Type.Object({
6784
+ entityUrl: Type.Optional(Type.String()),
6785
+ entityType: Type.Optional(Type.String()),
6786
+ streamPath: Type.Optional(Type.String()),
6787
+ runtimeConsumerId: Type.Optional(Type.String()),
6788
+ wakeId: Type.Optional(Type.String())
6789
+ });
6790
+ const pgSyncRegisterBodySchema = Type.Object({
6791
+ options: pgSyncOptionsSchema,
6792
+ metadata: Type.Optional(pgSyncRequestMetadataSchema)
6793
+ });
6794
+ const pgSyncRouter = Router({ base: `/_electric/pg-sync` });
6795
+ pgSyncRouter.post(`/register`, withSchema(pgSyncRegisterBodySchema), registerPgSync);
6796
+ async function registerPgSync(request, ctx) {
6797
+ const { options, metadata } = routeBody(request);
6798
+ if (options.table.trim() === ``) return apiError(400, ErrCodeInvalidRequest, `pgSync table must be non-empty`);
6799
+ if (!ctx.pgSyncBridgeManager) return apiError(503, ErrCodeInvalidRequest, `pgSync bridge manager is not configured`);
6800
+ try {
6801
+ const requestMetadata$1 = {
6802
+ tenantId: ctx.service,
6803
+ principalKind: ctx.principal.kind,
6804
+ principalId: ctx.principal.id,
6805
+ principalKey: ctx.principal.key,
6806
+ principalUrl: ctx.principal.url,
6807
+ ...metadata ?? {}
6808
+ };
6809
+ const result = await ctx.pgSyncBridgeManager.register(options, requestMetadata$1);
6810
+ return json(result);
6811
+ } catch (error) {
6812
+ return apiError(500, ErrCodeInvalidRequest, `pgSync registration failed: ${error instanceof Error ? error.message : String(error)}`);
6813
+ }
6814
+ }
6815
+
6608
6816
  //#endregion
6609
6817
  //#region src/routing/hooks.ts
6610
6818
  const SPAN_KEY = Symbol(`agents-server.otel-span`);
@@ -6679,6 +6887,10 @@ function errorMapper(err, req) {
6679
6887
  });
6680
6888
  }
6681
6889
  if (err instanceof ElectricAgentsError) return apiError(err.status, err.code, err.message, err.details);
6890
+ if (err instanceof ElectricProxyError) {
6891
+ serverLog.warn(`[agent-server] Electric proxy rejected request (${err.code}): ${req.url}`);
6892
+ return apiError(err.status, err.code, err.message);
6893
+ }
6682
6894
  serverLog.error(`[agent-server] Unhandled error:`, err);
6683
6895
  return apiError(500, `INTERNAL_SERVER_ERROR`, `Internal server error`);
6684
6896
  }
@@ -7129,6 +7341,7 @@ internalRouter.all(`/runners`, runnersRouter.fetch);
7129
7341
  internalRouter.all(`/runners/*`, runnersRouter.fetch);
7130
7342
  internalRouter.all(`/entities/*`, entitiesRouter.fetch);
7131
7343
  internalRouter.all(`/entity-types/*`, entityTypesRouter.fetch);
7344
+ internalRouter.all(`/pg-sync/*`, pgSyncRouter.fetch);
7132
7345
  internalRouter.all(`/observations/*`, observationsRouter.fetch);
7133
7346
  internalRouter.get(`/electric/*`, electricProxyRouter.fetch);
7134
7347
  internalRouter.all(`*`, () => status(404));
@@ -7512,6 +7725,9 @@ const globalRouter = AutoRouter({
7512
7725
  finally: [otelEndSpan, applyCors]
7513
7726
  });
7514
7727
  globalRouter.all(`/_electric/shared-state/*`, durableStreamsRouter.fetch);
7728
+ globalRouter.all(`/_electric/pg-sync/register`, internalRouter.fetch);
7729
+ globalRouter.get(`/_electric/pg-sync/*`, durableStreamsRouter.fetch);
7730
+ globalRouter.head(`/_electric/pg-sync/*`, durableStreamsRouter.fetch);
7515
7731
  globalRouter.all(`/_electric/*`, internalRouter.fetch);
7516
7732
  globalRouter.all(`*`, durableStreamsRouter.fetch);
7517
7733
 
@@ -7554,7 +7770,7 @@ const ENTITY_SHAPE_COLUMNS = [
7554
7770
  `created_at`,
7555
7771
  `updated_at`
7556
7772
  ];
7557
- function parseElectricOffset(offset) {
7773
+ function parseElectricOffset$1(offset) {
7558
7774
  if (offset === `-1`) return offset;
7559
7775
  return /^\d+_\d+$/.test(offset) ? offset : null;
7560
7776
  }
@@ -7646,7 +7862,7 @@ var EntityBridge = class {
7646
7862
  });
7647
7863
  await this.loadCurrentMembers();
7648
7864
  if (this.initialShapeHandle && this.initialShapeOffset) {
7649
- const initialOffset = parseElectricOffset(this.initialShapeOffset);
7865
+ const initialOffset = parseElectricOffset$1(this.initialShapeOffset);
7650
7866
  if (initialOffset) {
7651
7867
  this.startLiveStream(initialOffset, this.initialShapeHandle);
7652
7868
  return;
@@ -8132,6 +8348,9 @@ function isPermanentElectricAgentsError(err) {
8132
8348
  const name = typeof err === `object` && err !== null && `name` in err ? err.name : void 0;
8133
8349
  return name === `ElectricAgentsError` && typeof status$1 === `number` && status$1 >= 400 && status$1 < 500;
8134
8350
  }
8351
+ function cronTaskStreamPath(payload) {
8352
+ return typeof payload.streamPath === `string` ? payload.streamPath : null;
8353
+ }
8135
8354
  function normalizeTask(row) {
8136
8355
  return {
8137
8356
  id: Number(row.id),
@@ -8474,6 +8693,15 @@ var Scheduler = class {
8474
8693
  `;
8475
8694
  if (completed.length === 0) return;
8476
8695
  const nextFireAt = getNextCronFireAt(task.cronExpression, task.cronTimezone, task.fireAt);
8696
+ const streamPath = cronTaskStreamPath(task.payload);
8697
+ const subscriberRows = streamPath ? await sql$1`
8698
+ select 1 as exists
8699
+ from wake_registrations
8700
+ where tenant_id = ${tenantId}
8701
+ and source_url = ${streamPath}
8702
+ limit 1
8703
+ ` : [];
8704
+ if (subscriberRows.length === 0) return;
8477
8705
  await sql$1`
8478
8706
  insert into scheduled_tasks (
8479
8707
  tenant_id,
@@ -8573,6 +8801,308 @@ var Scheduler = class {
8573
8801
  }
8574
8802
  };
8575
8803
 
8804
+ //#endregion
8805
+ //#region src/pg-sync-bridge-manager.ts
8806
+ const PG_SYNC_ELECTRIC_SHAPE_URL = process.env.ELECTRIC_AGENTS_PG_SYNC_ELECTRIC_URL ?? `http://localhost:3000/v1/shape`;
8807
+ const DEFAULT_RETRY_INITIAL_DELAY_MS = 1e3;
8808
+ const DEFAULT_RETRY_MAX_DELAY_MS = 3e4;
8809
+ function buildElectricShapeParams(options) {
8810
+ return {
8811
+ table: options.table,
8812
+ ...options.columns !== void 0 ? { columns: [...options.columns] } : {},
8813
+ ...options.where !== void 0 ? { where: options.where } : {},
8814
+ ...options.params !== void 0 ? { params: Array.isArray(options.params) ? [...options.params] : { ...options.params } } : {},
8815
+ ...options.replica !== void 0 ? { replica: options.replica } : {},
8816
+ ...options.metadata?.tenantId ? { electric_agents_tenant_id: options.metadata.tenantId } : {},
8817
+ ...options.metadata?.principalKind ? { electric_agents_principal_kind: options.metadata.principalKind } : {},
8818
+ ...options.metadata?.principalId ? { electric_agents_principal_id: options.metadata.principalId } : {},
8819
+ ...options.metadata?.principalKey ? { electric_agents_principal_key: options.metadata.principalKey } : {},
8820
+ ...options.metadata?.principalUrl ? { electric_agents_principal_url: options.metadata.principalUrl } : {},
8821
+ ...options.metadata?.entityUrl ? { electric_agents_entity_url: options.metadata.entityUrl } : {},
8822
+ ...options.metadata?.entityType ? { electric_agents_entity_type: options.metadata.entityType } : {},
8823
+ ...options.metadata?.streamPath ? { electric_agents_stream_path: options.metadata.streamPath } : {},
8824
+ ...options.metadata?.runtimeConsumerId ? { electric_agents_runtime_consumer_id: options.metadata.runtimeConsumerId } : {},
8825
+ ...options.metadata?.wakeId ? { electric_agents_wake_id: options.metadata.wakeId } : {}
8826
+ };
8827
+ }
8828
+ function jsonSafe(value) {
8829
+ if (typeof value === `bigint`) return value.toString();
8830
+ if (value === null || typeof value !== `object`) return value;
8831
+ if (Array.isArray(value)) return value.map(jsonSafe);
8832
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, jsonSafe(item)]));
8833
+ }
8834
+ function stableJson(value) {
8835
+ if (typeof value === `bigint`) return JSON.stringify(value.toString());
8836
+ if (value === null || typeof value !== `object`) return JSON.stringify(value);
8837
+ if (Array.isArray(value)) return `[${value.map(stableJson).join(`,`)}]`;
8838
+ return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`).join(`,`)}}`;
8839
+ }
8840
+ function parseElectricOffset(offset) {
8841
+ if (offset === `-1`) return offset;
8842
+ return /^\d+_\d+$/.test(offset) ? offset : null;
8843
+ }
8844
+ function rowKeyForMessage(message) {
8845
+ const headers = message.headers;
8846
+ const candidate = headers.key ?? headers.rowKey ?? message.value?.id ?? message.value?.key ?? message.old_value?.id ?? message.old_value?.key;
8847
+ return candidate === void 0 ? void 0 : stableJson(candidate);
8848
+ }
8849
+ function pgSyncMessageToDurableEvent(message, optionsOrSourceRef) {
8850
+ const operation = message.headers.operation;
8851
+ if (operation !== `insert` && operation !== `update` && operation !== `delete`) return null;
8852
+ const sourceRef = typeof optionsOrSourceRef === `string` ? optionsOrSourceRef : sourceRefForPgSync(optionsOrSourceRef);
8853
+ const rowKey = rowKeyForMessage(message);
8854
+ const offset = message.headers.offset;
8855
+ if (typeof offset !== `string` || offset.length === 0) return null;
8856
+ const messageKeyPart = offset;
8857
+ const messageKey = `${sourceRef}:${operation}:${messageKeyPart}`;
8858
+ const timestamp$1 = new Date().toISOString();
8859
+ const oldValue = message.old_value;
8860
+ const safeValue = jsonSafe(message.value);
8861
+ const safeOldValue = jsonSafe(oldValue);
8862
+ const safeHeaders = jsonSafe(message.headers);
8863
+ return {
8864
+ type: `pg_sync_change`,
8865
+ key: messageKey,
8866
+ value: {
8867
+ key: messageKey,
8868
+ table: typeof optionsOrSourceRef === `string` ? void 0 : optionsOrSourceRef.table,
8869
+ operation,
8870
+ ...rowKey !== void 0 ? { rowKey } : {},
8871
+ ...message.value !== void 0 ? { value: safeValue } : {},
8872
+ ...oldValue !== void 0 ? { oldValue: safeOldValue } : {},
8873
+ headers: safeHeaders,
8874
+ ...typeof offset === `string` ? { offset } : {},
8875
+ receivedAt: timestamp$1
8876
+ },
8877
+ headers: {
8878
+ operation,
8879
+ timestamp: timestamp$1
8880
+ }
8881
+ };
8882
+ }
8883
+ function cursorFromRow(row) {
8884
+ return row?.shapeHandle && row.shapeOffset ? {
8885
+ handle: row.shapeHandle,
8886
+ offset: row.shapeOffset,
8887
+ initialSnapshotComplete: row.initialSnapshotComplete
8888
+ } : void 0;
8889
+ }
8890
+ var PgSyncBridge = class {
8891
+ producer = null;
8892
+ unsubscribe = null;
8893
+ abortController = null;
8894
+ skipChangesUntilUpToDate = false;
8895
+ recovering = false;
8896
+ committedCursor;
8897
+ retryAttempt = 0;
8898
+ constructor(sourceRef, streamUrl, options, resolvedSource, retry, streamClient, registry, evaluateWakes, initialCursor) {
8899
+ this.sourceRef = sourceRef;
8900
+ this.streamUrl = streamUrl;
8901
+ this.options = options;
8902
+ this.resolvedSource = resolvedSource;
8903
+ this.retry = retry;
8904
+ this.streamClient = streamClient;
8905
+ this.registry = registry;
8906
+ this.evaluateWakes = evaluateWakes;
8907
+ this.initialCursor = initialCursor;
8908
+ this.committedCursor = initialCursor;
8909
+ }
8910
+ async start() {
8911
+ if (!this.producer) this.producer = new IdempotentProducer(new DurableStream({
8912
+ url: `${this.streamClient.baseUrl}${this.streamUrl}`,
8913
+ contentType: `application/json`
8914
+ }), `pg-sync-bridge-${this.sourceRef}`);
8915
+ if (this.initialCursor) {
8916
+ const offset = parseElectricOffset(this.initialCursor.offset);
8917
+ if (offset) {
8918
+ this.startStream(offset, this.initialCursor.handle, !this.initialCursor.initialSnapshotComplete);
8919
+ return;
8920
+ }
8921
+ }
8922
+ await this.registry?.clearPgSyncBridgeCursor(this.sourceRef);
8923
+ this.startStream(`now`, void 0, true);
8924
+ }
8925
+ async stop() {
8926
+ this.unsubscribe?.();
8927
+ this.abortController?.abort();
8928
+ this.unsubscribe = null;
8929
+ this.abortController = null;
8930
+ try {
8931
+ await this.producer?.flush();
8932
+ } finally {
8933
+ await this.producer?.detach();
8934
+ this.producer = null;
8935
+ }
8936
+ }
8937
+ startStream(offset, handle, skipChangesUntilUpToDate = false, log = offset === `now` ? `changes_only` : `full`) {
8938
+ this.unsubscribe?.();
8939
+ this.abortController?.abort();
8940
+ this.skipChangesUntilUpToDate = skipChangesUntilUpToDate;
8941
+ this.abortController = new AbortController();
8942
+ const stream = new ShapeStream({
8943
+ url: this.resolvedSource.url,
8944
+ params: buildElectricShapeParams(this.options),
8945
+ offset,
8946
+ log,
8947
+ ...handle ? { handle } : {},
8948
+ signal: this.abortController.signal
8949
+ });
8950
+ this.unsubscribe = stream.subscribe(async (messages) => {
8951
+ try {
8952
+ for (const message of messages) {
8953
+ if (isControlMessage(message)) {
8954
+ if (message.headers.control === `must-refetch`) {
8955
+ await this.registry?.clearPgSyncBridgeCursor(this.sourceRef);
8956
+ this.startStream(`now`, void 0, true);
8957
+ return;
8958
+ }
8959
+ if (message.headers.control === `up-to-date`) {
8960
+ this.skipChangesUntilUpToDate = false;
8961
+ await this.persistCursor(stream, true);
8962
+ continue;
8963
+ }
8964
+ await this.persistCursor(stream);
8965
+ continue;
8966
+ }
8967
+ if (!isChangeMessage(message)) continue;
8968
+ if (!this.skipChangesUntilUpToDate) {
8969
+ const event = pgSyncMessageToDurableEvent(message, this.options);
8970
+ if (event) {
8971
+ if (!this.producer) throw new Error(`pg-sync producer is not started`);
8972
+ await this.producer.append(JSON.stringify(event));
8973
+ await this.producer.flush?.();
8974
+ await this.evaluateWakes?.(this.streamUrl, event);
8975
+ }
8976
+ }
8977
+ await this.persistCursor(stream);
8978
+ this.retryAttempt = 0;
8979
+ }
8980
+ } catch (error) {
8981
+ serverLog.warn(`[pg-sync-bridge] subscription callback failed for ${this.sourceRef}:`, error);
8982
+ await this.recoverStream();
8983
+ }
8984
+ }, (error) => {
8985
+ if (this.abortController?.signal.aborted) return;
8986
+ serverLog.warn(`[pg-sync-bridge] subscription failed for ${this.sourceRef}:`, error);
8987
+ this.recoverStream();
8988
+ });
8989
+ }
8990
+ async recoverStream() {
8991
+ if (this.recovering) return;
8992
+ this.recovering = true;
8993
+ try {
8994
+ const attempt = this.retryAttempt++;
8995
+ const baseDelay = Math.min(this.retry.initialDelayMs * 2 ** attempt, this.retry.maxDelayMs);
8996
+ const jitter = Math.floor(baseDelay * .2 * this.retry.random());
8997
+ const delay = baseDelay + jitter;
8998
+ if (delay > 0) await this.retry.sleep(delay);
8999
+ const offset = this.committedCursor ? parseElectricOffset(this.committedCursor.offset) : null;
9000
+ if (offset && this.committedCursor) this.startStream(offset, this.committedCursor.handle, !this.committedCursor.initialSnapshotComplete);
9001
+ else {
9002
+ await this.registry?.clearPgSyncBridgeCursor(this.sourceRef);
9003
+ this.startStream(`now`, void 0, true);
9004
+ }
9005
+ } finally {
9006
+ this.recovering = false;
9007
+ }
9008
+ }
9009
+ async persistCursor(stream, initialSnapshotComplete = !this.skipChangesUntilUpToDate) {
9010
+ const shapeHandle = stream.shapeHandle;
9011
+ const shapeOffset = stream.lastOffset;
9012
+ if (!shapeHandle || !shapeOffset || shapeOffset === `-1`) return;
9013
+ await this.registry?.updatePgSyncBridgeCursor(this.sourceRef, shapeHandle, shapeOffset, initialSnapshotComplete);
9014
+ this.committedCursor = {
9015
+ handle: shapeHandle,
9016
+ offset: shapeOffset,
9017
+ initialSnapshotComplete
9018
+ };
9019
+ }
9020
+ };
9021
+ var PgSyncBridgeManager = class {
9022
+ bridges = new Map();
9023
+ starting = new Map();
9024
+ url;
9025
+ retry;
9026
+ constructor(streamClient, evaluateWakes, registry, options = {}) {
9027
+ this.streamClient = streamClient;
9028
+ this.evaluateWakes = evaluateWakes;
9029
+ this.registry = registry;
9030
+ this.url = options.url ?? PG_SYNC_ELECTRIC_SHAPE_URL;
9031
+ this.retry = {
9032
+ initialDelayMs: options.retry?.initialDelayMs ?? DEFAULT_RETRY_INITIAL_DELAY_MS,
9033
+ maxDelayMs: options.retry?.maxDelayMs ?? DEFAULT_RETRY_MAX_DELAY_MS,
9034
+ random: options.retry?.random ?? Math.random,
9035
+ sleep: options.retry?.sleep ?? ((ms) => new Promise((resolve$1) => setTimeout(resolve$1, ms)))
9036
+ };
9037
+ }
9038
+ async start() {
9039
+ const rows = await this.registry?.listPgSyncBridges?.();
9040
+ if (!rows) return;
9041
+ await Promise.all(rows.map((row) => this.ensureBridge(row).catch((error) => {
9042
+ serverLog.warn(`[pg-sync-bridge] failed to start ${row.sourceRef}:`, error);
9043
+ })));
9044
+ }
9045
+ async register(options, metadata) {
9046
+ const mergedMetadata = {
9047
+ ...options.metadata,
9048
+ ...metadata
9049
+ };
9050
+ const canonicalOptions = {
9051
+ ...canonicalPgSyncOptions(options),
9052
+ ...Object.keys(mergedMetadata).length > 0 ? { metadata: mergedMetadata } : {}
9053
+ };
9054
+ const resolvedSource = this.resolveSource(canonicalOptions);
9055
+ const sourceRef = sourceRefForPgSync(canonicalOptions);
9056
+ const streamUrl = getPgSyncStreamPath(sourceRef, this.registry?.tenantId);
9057
+ const row = await this.registry?.upsertPgSyncBridge({
9058
+ sourceRef,
9059
+ options: canonicalOptions,
9060
+ streamUrl
9061
+ });
9062
+ await this.streamClient.ensure(streamUrl, { contentType: `application/json` });
9063
+ if (!this.bridges.has(sourceRef)) {
9064
+ let start = this.starting.get(sourceRef);
9065
+ if (!start) {
9066
+ start = (async () => {
9067
+ const bridge = new PgSyncBridge(sourceRef, streamUrl, canonicalOptions, resolvedSource, this.retry, this.streamClient, this.registry, this.evaluateWakes, cursorFromRow(row));
9068
+ await bridge.start();
9069
+ this.bridges.set(sourceRef, bridge);
9070
+ })().finally(() => this.starting.delete(sourceRef));
9071
+ this.starting.set(sourceRef, start);
9072
+ }
9073
+ await start;
9074
+ }
9075
+ return {
9076
+ sourceRef,
9077
+ streamUrl
9078
+ };
9079
+ }
9080
+ async ensureBridge(row) {
9081
+ if (this.bridges.has(row.sourceRef)) return;
9082
+ let start = this.starting.get(row.sourceRef);
9083
+ if (!start) {
9084
+ start = (async () => {
9085
+ await this.streamClient.ensure(row.streamUrl, { contentType: `application/json` });
9086
+ const canonicalOptions = canonicalPgSyncOptions(row.options);
9087
+ const resolvedSource = this.resolveSource(canonicalOptions);
9088
+ const bridge = new PgSyncBridge(row.sourceRef, row.streamUrl, canonicalOptions, resolvedSource, this.retry, this.streamClient, this.registry, this.evaluateWakes, cursorFromRow(row));
9089
+ await bridge.start();
9090
+ this.bridges.set(row.sourceRef, bridge);
9091
+ })().finally(() => this.starting.delete(row.sourceRef));
9092
+ this.starting.set(row.sourceRef, start);
9093
+ }
9094
+ await start;
9095
+ }
9096
+ resolveSource(options) {
9097
+ return { url: options.url ?? this.url };
9098
+ }
9099
+ async stop() {
9100
+ await Promise.allSettled(this.starting.values());
9101
+ await Promise.all([...this.bridges.values()].map((bridge) => bridge.stop()));
9102
+ this.bridges.clear();
9103
+ }
9104
+ };
9105
+
8576
9106
  //#endregion
8577
9107
  //#region src/runtime.ts
8578
9108
  function omitUndefined(value) {
@@ -8587,6 +9117,7 @@ var ElectricAgentsTenantRuntime = class {
8587
9117
  wakeRegistry;
8588
9118
  scheduler;
8589
9119
  entityBridgeManager;
9120
+ pgSyncBridgeManager;
8590
9121
  claimWriteTokens;
8591
9122
  manager;
8592
9123
  constructor(options) {
@@ -8611,9 +9142,10 @@ var ElectricAgentsTenantRuntime = class {
8611
9142
  writeTokenValidator: (entity, token) => this.claimWriteTokens.isValid(this.serviceId, entity.streams.main, token),
8612
9143
  stopWakeRegistryOnShutdown: options.stopWakeRegistryOnShutdown ?? false
8613
9144
  });
9145
+ this.pgSyncBridgeManager = options.pgSyncBridgeManager ?? new PgSyncBridgeManager(this.streamClient, (sourceUrl, event) => this.manager.evaluateWakes(sourceUrl, event), this.registry, options.pgSync);
8614
9146
  }
8615
9147
  async stop() {
8616
- await this.manager.shutdown();
9148
+ await Promise.all([this.manager.shutdown(), this.pgSyncBridgeManager.stop()]);
8617
9149
  }
8618
9150
  async rehydrateCronSchedules() {
8619
9151
  const rows = await this.db.select({ sourceUrl: wakeRegistrations.sourceUrl }).from(wakeRegistrations).where(eq(wakeRegistrations.tenantId, this.serviceId));
@@ -9377,7 +9909,10 @@ var WakeRegistry = class {
9377
9909
  }
9378
9910
  if (!isChangeMessage(message)) return;
9379
9911
  if (message.headers.operation === `delete`) {
9380
- this.removeCachedRegistrationByDbId(Number(message.key));
9912
+ const oldValue = message.old_value;
9913
+ const oldId = Number(oldValue?.id);
9914
+ if (Number.isFinite(oldId)) this.removeCachedRegistrationByDbId(oldId);
9915
+ else this.resetCachedRegistrations();
9381
9916
  return;
9382
9917
  }
9383
9918
  this.upsertCachedRegistration(this.normalizeShapeRow(message.value));
@@ -9488,9 +10023,9 @@ var WakeRegistry = class {
9488
10023
  matchCondition(reg, event) {
9489
10024
  if (reg.condition === `runFinished`) {
9490
10025
  if (event.type !== `run`) return null;
9491
- const value = event.value;
10026
+ const value$1 = event.value;
9492
10027
  const headers$1 = event.headers;
9493
- const status$1 = value?.status;
10028
+ const status$1 = value$1?.status;
9494
10029
  const operation$1 = headers$1?.operation;
9495
10030
  if (operation$1 !== `update`) return null;
9496
10031
  if (status$1 !== `completed` && status$1 !== `failed`) return null;
@@ -9513,13 +10048,15 @@ var WakeRegistry = class {
9513
10048
  }
9514
10049
  const kind = operation === `delete` ? `delete` : operation === `update` ? `update` : `insert`;
9515
10050
  if (condition.ops && condition.ops.length > 0 && !condition.ops.includes(kind)) return null;
10051
+ const value = event.value;
9516
10052
  const change = {
9517
10053
  collection: eventType,
9518
10054
  kind,
9519
10055
  key: event.key || ``
9520
10056
  };
10057
+ if (value && `value` in value) change.value = value.value;
10058
+ if (value && `oldValue` in value) change.oldValue = value.oldValue;
9521
10059
  if (eventType === `inbox`) {
9522
- const value = event.value;
9523
10060
  if (typeof value?.from === `string`) change.from = value.from;
9524
10061
  if (typeof value?.from_principal === `string`) change.from_principal = value.from_principal;
9525
10062
  if (typeof value?.from_agent === `string`) change.from_agent = value.from_agent;
@@ -9563,12 +10100,15 @@ async function startStandaloneAgentsRuntime(options) {
9563
10100
  wakeRegistry,
9564
10101
  scheduler,
9565
10102
  entityBridgeManager,
10103
+ pgSyncBridgeManager: options.pgSyncBridgeManager,
10104
+ pgSync: options.pgSync,
9566
10105
  stopWakeRegistryOnShutdown: options.wakeRegistry ? false : true
9567
10106
  });
9568
10107
  const startWakeRegistry = options.startWakeRegistry ?? true;
9569
10108
  const startScheduler = options.startScheduler ?? true;
9570
10109
  const startTagStreamOutboxDrainer = options.startTagStreamOutboxDrainer ?? true;
9571
10110
  const startEntityBridgeManager = options.startEntityBridgeManager ?? true;
10111
+ const startPgSyncBridgeManager = options.startPgSyncBridgeManager ?? true;
9572
10112
  const rehydrateOnStart = options.rehydrateOnStart ?? true;
9573
10113
  let entityBridgeManagerStarted = false;
9574
10114
  let tagStreamOutboxDrainerStarted = false;
@@ -9605,6 +10145,10 @@ async function startStandaloneAgentsRuntime(options) {
9605
10145
  await entityBridgeManager.start();
9606
10146
  entityBridgeManagerStarted = true;
9607
10147
  }
10148
+ if (startPgSyncBridgeManager) {
10149
+ serverLog.info(`[agent-server] starting pg-sync bridge manager...`);
10150
+ await runtime.pgSyncBridgeManager.start?.();
10151
+ }
9608
10152
  if (startTagStreamOutboxDrainer) {
9609
10153
  serverLog.info(`[agent-server] starting tag stream outbox drainer...`);
9610
10154
  tagStreamOutboxDrainer.start();
@@ -9632,6 +10176,7 @@ async function startStandaloneAgentsRuntime(options) {
9632
10176
  manager: runtime.manager,
9633
10177
  scheduler,
9634
10178
  entityBridgeManager,
10179
+ pgSyncBridgeManager: runtime.pgSyncBridgeManager,
9635
10180
  tagStreamOutboxDrainer,
9636
10181
  stop
9637
10182
  };
@@ -9749,7 +10294,8 @@ var ElectricAgentsServer = class {
9749
10294
  pgClient: client,
9750
10295
  streamClient: this.streamClient,
9751
10296
  electricUrl: this.options.electricUrl,
9752
- electricSecret: this.options.electricSecret
10297
+ electricSecret: this.options.electricSecret,
10298
+ pgSync: this.options.pgSync
9753
10299
  });
9754
10300
  this.electricAgentsManager = this.standaloneRuntime.manager;
9755
10301
  this.entityBridgeManager = this.standaloneRuntime.entityBridgeManager;
@@ -9871,6 +10417,7 @@ var ElectricAgentsServer = class {
9871
10417
  streamClient: this.streamClient,
9872
10418
  runtime: this.standaloneRuntime.runtime,
9873
10419
  entityBridgeManager: this.entityBridgeManager,
10420
+ pgSyncBridgeManager: this.standaloneRuntime.runtime.pgSyncBridgeManager,
9874
10421
  ...this.options.eventSources ? { eventSources: this.options.eventSources } : {},
9875
10422
  ...this.options.ensureEventSourceWakeSource ? { ensureEventSourceWakeSource: this.options.ensureEventSourceWakeSource } : {},
9876
10423
  ...this.options.authorizeRequest ? { authorizeRequest: this.options.authorizeRequest } : {},