@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.
package/dist/index.cjs CHANGED
@@ -61,6 +61,7 @@ __export(schema_exports, {
61
61
  entityPermissionGrants: () => entityPermissionGrants,
62
62
  entityTypePermissionGrants: () => entityTypePermissionGrants,
63
63
  entityTypes: () => entityTypes,
64
+ pgSyncBridges: () => pgSyncBridges,
64
65
  runnerRuntimeDiagnostics: () => runnerRuntimeDiagnostics,
65
66
  runners: () => runners,
66
67
  scheduledTasks: () => scheduledTasks,
@@ -382,6 +383,18 @@ const scheduledTasks = (0, drizzle_orm_pg_core.pgTable)(`scheduled_tasks`, {
382
383
  (0, drizzle_orm_pg_core.index)(`idx_scheduled_tasks_manifest_pending`).on(table.tenantId, table.ownerEntityUrl, table.manifestKey).where(drizzle_orm.sql`${table.kind} = 'delayed_send' AND ${table.completedAt} IS NULL AND ${table.manifestKey} IS NOT NULL`),
383
384
  (0, drizzle_orm_pg_core.index)(`idx_scheduled_tasks_stale_claims`).on(table.tenantId, table.claimedAt).where(drizzle_orm.sql`${table.completedAt} IS NULL AND ${table.claimedAt} IS NOT NULL`)
384
385
  ]);
386
+ const pgSyncBridges = (0, drizzle_orm_pg_core.pgTable)(`pg_sync_bridges`, {
387
+ tenantId: (0, drizzle_orm_pg_core.text)(`tenant_id`).notNull().default(`default`),
388
+ sourceRef: (0, drizzle_orm_pg_core.text)(`source_ref`).notNull(),
389
+ options: (0, drizzle_orm_pg_core.jsonb)(`options`).notNull(),
390
+ streamUrl: (0, drizzle_orm_pg_core.text)(`stream_url`).notNull(),
391
+ shapeHandle: (0, drizzle_orm_pg_core.text)(`shape_handle`),
392
+ shapeOffset: (0, drizzle_orm_pg_core.text)(`shape_offset`),
393
+ initialSnapshotComplete: (0, drizzle_orm_pg_core.boolean)(`initial_snapshot_complete`).notNull().default(false),
394
+ lastTouchedAt: (0, drizzle_orm_pg_core.timestamp)(`last_touched_at`, { withTimezone: true }).notNull().defaultNow(),
395
+ createdAt: (0, drizzle_orm_pg_core.timestamp)(`created_at`, { withTimezone: true }).notNull().defaultNow(),
396
+ updatedAt: (0, drizzle_orm_pg_core.timestamp)(`updated_at`, { withTimezone: true }).notNull().defaultNow()
397
+ }, (table) => [(0, drizzle_orm_pg_core.primaryKey)({ columns: [table.tenantId, table.sourceRef] }), (0, drizzle_orm_pg_core.unique)(`uq_pg_sync_bridges_stream_url`).on(table.tenantId, table.streamUrl)]);
385
398
  const entityBridges = (0, drizzle_orm_pg_core.pgTable)(`entity_bridges`, {
386
399
  tenantId: (0, drizzle_orm_pg_core.text)(`tenant_id`).notNull().default(`default`),
387
400
  sourceRef: (0, drizzle_orm_pg_core.text)(`source_ref`).notNull(),
@@ -842,6 +855,9 @@ var PostgresRegistry = class {
842
855
  entityBridgeWhere(sourceRef) {
843
856
  return (0, drizzle_orm.and)((0, drizzle_orm.eq)(entityBridges.tenantId, this.tenantId), (0, drizzle_orm.eq)(entityBridges.sourceRef, sourceRef));
844
857
  }
858
+ pgSyncBridgeWhere(sourceRef) {
859
+ return (0, drizzle_orm.and)((0, drizzle_orm.eq)(pgSyncBridges.tenantId, this.tenantId), (0, drizzle_orm.eq)(pgSyncBridges.sourceRef, sourceRef));
860
+ }
845
861
  async createEntityType(et) {
846
862
  await this.db.insert(entityTypes).values({
847
863
  tenantId: this.tenantId,
@@ -1311,11 +1327,12 @@ var PostgresRegistry = class {
1311
1327
  };
1312
1328
  const nextTags = (0, __electric_ax_agents_runtime.normalizeTags)(mutation.nextTags);
1313
1329
  const updatedAt = Date.now();
1314
- await tx.update(entities).set({
1330
+ const [updateResult] = await tx.update(entities).set({
1315
1331
  tags: nextTags,
1316
1332
  tagsIndex: (0, __electric_ax_agents_runtime.buildTagsIndex)(nextTags),
1317
1333
  updatedAt
1318
- }).where(this.entityWhere(url));
1334
+ }).where(this.entityWhere(url)).returning({ txid: drizzle_orm.sql`pg_current_xact_id()::xid::text` });
1335
+ const txid = updateResult ? parseInt(updateResult.txid) : void 0;
1319
1336
  await tx.insert(tagStreamOutbox).values({
1320
1337
  tenantId: this.tenantId,
1321
1338
  entityUrl: url,
@@ -1333,10 +1350,63 @@ var PostgresRegistry = class {
1333
1350
  return {
1334
1351
  entity,
1335
1352
  changed: true,
1336
- ...op === `insert` || op === `update` ? { op } : {}
1353
+ ...op === `insert` || op === `update` ? { op } : {},
1354
+ ...txid !== void 0 ? { txid } : {}
1337
1355
  };
1338
1356
  });
1339
1357
  }
1358
+ async upsertPgSyncBridge(row) {
1359
+ await this.db.insert(pgSyncBridges).values({
1360
+ tenantId: this.tenantId,
1361
+ sourceRef: row.sourceRef,
1362
+ options: row.options,
1363
+ streamUrl: row.streamUrl,
1364
+ lastTouchedAt: new Date(),
1365
+ updatedAt: new Date()
1366
+ }).onConflictDoUpdate({
1367
+ target: [pgSyncBridges.tenantId, pgSyncBridges.sourceRef],
1368
+ set: {
1369
+ options: row.options,
1370
+ streamUrl: row.streamUrl,
1371
+ initialSnapshotComplete: false,
1372
+ lastTouchedAt: new Date(),
1373
+ updatedAt: new Date()
1374
+ }
1375
+ });
1376
+ const existing = await this.getPgSyncBridge(row.sourceRef);
1377
+ if (!existing) throw new Error(`Failed to load pgSync bridge ${row.sourceRef}`);
1378
+ return existing;
1379
+ }
1380
+ async getPgSyncBridge(sourceRef) {
1381
+ const rows = await this.db.select().from(pgSyncBridges).where(this.pgSyncBridgeWhere(sourceRef)).limit(1);
1382
+ return rows[0] ? this.rowToPgSyncBridge(rows[0]) : null;
1383
+ }
1384
+ async listPgSyncBridges(tenantId = this.tenantId) {
1385
+ const rows = tenantId === null ? await this.db.select().from(pgSyncBridges) : await this.db.select().from(pgSyncBridges).where((0, drizzle_orm.eq)(pgSyncBridges.tenantId, tenantId));
1386
+ return rows.map((row) => this.rowToPgSyncBridge(row));
1387
+ }
1388
+ async touchPgSyncBridge(sourceRef) {
1389
+ await this.db.update(pgSyncBridges).set({
1390
+ lastTouchedAt: new Date(),
1391
+ updatedAt: new Date()
1392
+ }).where(this.pgSyncBridgeWhere(sourceRef));
1393
+ }
1394
+ async updatePgSyncBridgeCursor(sourceRef, shapeHandle, shapeOffset, initialSnapshotComplete) {
1395
+ await this.db.update(pgSyncBridges).set({
1396
+ shapeHandle,
1397
+ shapeOffset,
1398
+ ...initialSnapshotComplete !== void 0 ? { initialSnapshotComplete } : {},
1399
+ updatedAt: new Date()
1400
+ }).where(this.pgSyncBridgeWhere(sourceRef));
1401
+ }
1402
+ async clearPgSyncBridgeCursor(sourceRef) {
1403
+ await this.db.update(pgSyncBridges).set({
1404
+ shapeHandle: null,
1405
+ shapeOffset: null,
1406
+ initialSnapshotComplete: false,
1407
+ updatedAt: new Date()
1408
+ }).where(this.pgSyncBridgeWhere(sourceRef));
1409
+ }
1340
1410
  async upsertEntityBridge(row) {
1341
1411
  await this.db.insert(entityBridges).values({
1342
1412
  tenantId: this.tenantId,
@@ -1556,6 +1626,20 @@ var PostgresRegistry = class {
1556
1626
  updated_at: row.updatedAt
1557
1627
  };
1558
1628
  }
1629
+ rowToPgSyncBridge(row) {
1630
+ return {
1631
+ tenantId: row.tenantId,
1632
+ sourceRef: row.sourceRef,
1633
+ options: row.options,
1634
+ streamUrl: row.streamUrl,
1635
+ shapeHandle: row.shapeHandle ?? void 0,
1636
+ shapeOffset: row.shapeOffset ?? void 0,
1637
+ initialSnapshotComplete: row.initialSnapshotComplete,
1638
+ lastTouchedAt: row.lastTouchedAt,
1639
+ createdAt: row.createdAt,
1640
+ updatedAt: row.updatedAt
1641
+ };
1642
+ }
1559
1643
  rowToEntityBridge(row) {
1560
1644
  return {
1561
1645
  tenantId: row.tenantId,
@@ -3203,6 +3287,9 @@ function assertSharedSandboxColocated(key, chosenIsRemote, dispatchPolicy) {
3203
3287
  function isRecord$1(value) {
3204
3288
  return typeof value === `object` && value !== null && !Array.isArray(value);
3205
3289
  }
3290
+ function getPgSyncManifestStreamPath(sourceRef) {
3291
+ return `/_electric/pg-sync/${sourceRef}`;
3292
+ }
3206
3293
  function extractManifestSourceUrl(manifest) {
3207
3294
  if (!manifest) return void 0;
3208
3295
  if (manifest.kind === `child` || manifest.kind === `observe`) return typeof manifest.entity_url === `string` ? manifest.entity_url : void 0;
@@ -3215,6 +3302,7 @@ function extractManifestSourceUrl(manifest) {
3215
3302
  }
3216
3303
  if (manifest.sourceType === `entities`) return typeof manifest.sourceRef === `string` ? `/_entities/${manifest.sourceRef}` : void 0;
3217
3304
  if (manifest.sourceType === `db`) return typeof manifest.sourceRef === `string` ? (0, __electric_ax_agents_runtime.getSharedStateStreamPath)(manifest.sourceRef) : void 0;
3305
+ if (manifest.sourceType === `pgSync`) return typeof manifest.sourceRef === `string` ? getPgSyncManifestStreamPath(manifest.sourceRef) : void 0;
3218
3306
  if (manifest.sourceType === `webhook`) {
3219
3307
  if (typeof config?.streamUrl === `string`) return config.streamUrl;
3220
3308
  if (typeof config?.endpointKey === `string`) return (0, __electric_ax_agents_runtime.getWebhookStreamPath)(config.endpointKey, typeof config.bucket === `string` ? config.bucket : void 0);
@@ -3329,6 +3417,13 @@ function isRecord(value) {
3329
3417
  function cloneRecord(value) {
3330
3418
  return JSON.parse(JSON.stringify(value));
3331
3419
  }
3420
+ function withOptionalTxid(entity, txid) {
3421
+ if (txid === void 0) return entity;
3422
+ return {
3423
+ ...entity,
3424
+ txid
3425
+ };
3426
+ }
3332
3427
  /**
3333
3428
  * Orchestrates the Electric Agents entity lifecycle: register types, spawn, send, kill.
3334
3429
  *
@@ -4470,15 +4565,17 @@ var EntityManager = class {
4470
4565
  await this.registry.updateStatus(entityUrl, `idle`);
4471
4566
  await this.entityBridgeManager?.onEntityChanged(entityUrl);
4472
4567
  }
4568
+ const txid = crypto.randomUUID();
4473
4569
  const envelope = __electric_ax_agents_runtime.entityStateSchema.inbox.insert({
4474
4570
  key,
4475
- value
4571
+ value,
4572
+ headers: { txid }
4476
4573
  });
4477
4574
  const encoded = this.encodeChangeEvent(envelope);
4478
4575
  try {
4479
4576
  if (opts?.producerId) {
4480
4577
  await this.streamClient.appendIdempotent(entity.streams.main, encoded, { producerId: opts.producerId });
4481
- return;
4578
+ return { txid };
4482
4579
  }
4483
4580
  await this.streamClient.append(entity.streams.main, encoded);
4484
4581
  if (entity.type === `principal` && req.type === `update_identity`) {
@@ -4486,9 +4583,11 @@ var EntityManager = class {
4486
4583
  await this.streamClient.append(entity.streams.main, this.encodeChangeEvent({
4487
4584
  type: `identity`,
4488
4585
  key: `self`,
4489
- value: identity
4586
+ value: identity,
4587
+ headers: { txid }
4490
4588
  }));
4491
4589
  }
4590
+ return { txid };
4492
4591
  } catch (err) {
4493
4592
  if (this.isClosedStreamError(err)) throw new ElectricAgentsError(ErrCodeNotRunning, `Entity is stopped`, 409);
4494
4593
  throw err;
@@ -4509,18 +4608,26 @@ var EntityManager = class {
4509
4608
  if (req.status === `cancelled`) value.cancelled_at = now;
4510
4609
  }
4511
4610
  if (Object.keys(value).length === 0) throw new ElectricAgentsError(ErrCodeInvalidRequest, `No inbox fields to update`, 400);
4611
+ const txid = crypto.randomUUID();
4512
4612
  const envelope = __electric_ax_agents_runtime.entityStateSchema.inbox.update({
4513
4613
  key,
4514
- value
4614
+ value,
4615
+ headers: { txid }
4515
4616
  });
4516
4617
  await this.streamClient.append(entity.streams.main, this.encodeChangeEvent(envelope));
4618
+ return { txid };
4517
4619
  }
4518
4620
  async deleteInboxMessage(entityUrl, key) {
4519
4621
  const entity = await this.registry.getEntity(entityUrl);
4520
4622
  if (!entity) throw new ElectricAgentsError(ErrCodeNotFound, `Entity not found`, 404);
4521
4623
  if (rejectsNormalWrites(entity.status)) throw new ElectricAgentsError(ErrCodeNotRunning, `Entity is not accepting writes`, 409);
4522
- const envelope = __electric_ax_agents_runtime.entityStateSchema.inbox.delete({ key });
4624
+ const txid = crypto.randomUUID();
4625
+ const envelope = __electric_ax_agents_runtime.entityStateSchema.inbox.delete({
4626
+ key,
4627
+ headers: { txid }
4628
+ });
4523
4629
  await this.streamClient.append(entity.streams.main, this.encodeChangeEvent(envelope));
4630
+ return { txid };
4524
4631
  }
4525
4632
  isAttachmentStreamPath(path$2) {
4526
4633
  return /^\/[^/]+\/[^/]+\/attachments\/[^/]+$/.test(path$2);
@@ -4609,28 +4716,26 @@ var EntityManager = class {
4609
4716
  await this.streamClient.delete(attachment.streamPath).catch(() => void 0);
4610
4717
  return { txid };
4611
4718
  }
4612
- async setTag(entityUrl, key, req, token) {
4719
+ async setTag(entityUrl, key, req) {
4613
4720
  const entity = await this.registry.getEntity(entityUrl);
4614
4721
  if (!entity) throw new ElectricAgentsError(ErrCodeNotFound, `Entity not found`, 404);
4615
- if (!this.isValidWriteToken(entity, token)) throw new ElectricAgentsError(ErrCodeUnauthorized, `Invalid write token`, 401);
4616
4722
  if (rejectsNormalWrites(entity.status)) throw new ElectricAgentsError(ErrCodeNotRunning, `Entity is not accepting writes`, 409);
4617
4723
  if (typeof req.value !== `string`) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Tag values must be strings`, 400);
4618
4724
  const result = await this.registry.setEntityTag(entityUrl, key, req.value);
4619
4725
  const updated = result.entity;
4620
4726
  if (!updated) throw new ElectricAgentsError(ErrCodeEntityPersistFailed, `Entity not found after tag write`, 500);
4621
4727
  if (result.changed && this.entityBridgeManager) await this.entityBridgeManager.onEntityChanged(entityUrl);
4622
- return updated;
4728
+ return withOptionalTxid(updated, result.txid);
4623
4729
  }
4624
- async deleteTag(entityUrl, key, token) {
4730
+ async deleteTag(entityUrl, key) {
4625
4731
  const entity = await this.registry.getEntity(entityUrl);
4626
4732
  if (!entity) throw new ElectricAgentsError(ErrCodeNotFound, `Entity not found`, 404);
4627
- if (!this.isValidWriteToken(entity, token)) throw new ElectricAgentsError(ErrCodeUnauthorized, `Invalid write token`, 401);
4628
4733
  if (rejectsNormalWrites(entity.status)) throw new ElectricAgentsError(ErrCodeNotRunning, `Entity is not accepting writes`, 409);
4629
4734
  const result = await this.registry.removeEntityTag(entityUrl, key);
4630
4735
  const updated = result.entity;
4631
4736
  if (!updated) throw new ElectricAgentsError(ErrCodeEntityPersistFailed, `Entity not found after tag delete`, 500);
4632
4737
  if (result.changed && this.entityBridgeManager) await this.entityBridgeManager.onEntityChanged(entityUrl);
4633
- return updated;
4738
+ return withOptionalTxid(updated, result.txid);
4634
4739
  }
4635
4740
  async ensureEntitiesMembershipStream(tags, principal) {
4636
4741
  if (!this.entityBridgeManager) throw new Error(`Entity bridge manager not configured`);
@@ -5426,6 +5531,9 @@ function isPermanentElectricAgentsError(err) {
5426
5531
  const name = typeof err === `object` && err !== null && `name` in err ? err.name : void 0;
5427
5532
  return name === `ElectricAgentsError` && typeof status$4 === `number` && status$4 >= 400 && status$4 < 500;
5428
5533
  }
5534
+ function cronTaskStreamPath(payload) {
5535
+ return typeof payload.streamPath === `string` ? payload.streamPath : null;
5536
+ }
5429
5537
  function normalizeTask(row) {
5430
5538
  return {
5431
5539
  id: Number(row.id),
@@ -5768,6 +5876,15 @@ var Scheduler = class {
5768
5876
  `;
5769
5877
  if (completed.length === 0) return;
5770
5878
  const nextFireAt = (0, __electric_ax_agents_runtime.getNextCronFireAt)(task.cronExpression, task.cronTimezone, task.fireAt);
5879
+ const streamPath = cronTaskStreamPath(task.payload);
5880
+ const subscriberRows = streamPath ? await sql$2`
5881
+ select 1 as exists
5882
+ from wake_registrations
5883
+ where tenant_id = ${tenantId}
5884
+ and source_url = ${streamPath}
5885
+ limit 1
5886
+ ` : [];
5887
+ if (subscriberRows.length === 0) return;
5771
5888
  await sql$2`
5772
5889
  insert into scheduled_tasks (
5773
5890
  tenant_id,
@@ -5867,6 +5984,308 @@ var Scheduler = class {
5867
5984
  }
5868
5985
  };
5869
5986
 
5987
+ //#endregion
5988
+ //#region src/pg-sync-bridge-manager.ts
5989
+ const PG_SYNC_ELECTRIC_SHAPE_URL = process.env.ELECTRIC_AGENTS_PG_SYNC_ELECTRIC_URL ?? `http://localhost:3000/v1/shape`;
5990
+ const DEFAULT_RETRY_INITIAL_DELAY_MS = 1e3;
5991
+ const DEFAULT_RETRY_MAX_DELAY_MS = 3e4;
5992
+ function buildElectricShapeParams(options) {
5993
+ return {
5994
+ table: options.table,
5995
+ ...options.columns !== void 0 ? { columns: [...options.columns] } : {},
5996
+ ...options.where !== void 0 ? { where: options.where } : {},
5997
+ ...options.params !== void 0 ? { params: Array.isArray(options.params) ? [...options.params] : { ...options.params } } : {},
5998
+ ...options.replica !== void 0 ? { replica: options.replica } : {},
5999
+ ...options.metadata?.tenantId ? { electric_agents_tenant_id: options.metadata.tenantId } : {},
6000
+ ...options.metadata?.principalKind ? { electric_agents_principal_kind: options.metadata.principalKind } : {},
6001
+ ...options.metadata?.principalId ? { electric_agents_principal_id: options.metadata.principalId } : {},
6002
+ ...options.metadata?.principalKey ? { electric_agents_principal_key: options.metadata.principalKey } : {},
6003
+ ...options.metadata?.principalUrl ? { electric_agents_principal_url: options.metadata.principalUrl } : {},
6004
+ ...options.metadata?.entityUrl ? { electric_agents_entity_url: options.metadata.entityUrl } : {},
6005
+ ...options.metadata?.entityType ? { electric_agents_entity_type: options.metadata.entityType } : {},
6006
+ ...options.metadata?.streamPath ? { electric_agents_stream_path: options.metadata.streamPath } : {},
6007
+ ...options.metadata?.runtimeConsumerId ? { electric_agents_runtime_consumer_id: options.metadata.runtimeConsumerId } : {},
6008
+ ...options.metadata?.wakeId ? { electric_agents_wake_id: options.metadata.wakeId } : {}
6009
+ };
6010
+ }
6011
+ function jsonSafe(value) {
6012
+ if (typeof value === `bigint`) return value.toString();
6013
+ if (value === null || typeof value !== `object`) return value;
6014
+ if (Array.isArray(value)) return value.map(jsonSafe);
6015
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, jsonSafe(item)]));
6016
+ }
6017
+ function stableJson(value) {
6018
+ if (typeof value === `bigint`) return JSON.stringify(value.toString());
6019
+ if (value === null || typeof value !== `object`) return JSON.stringify(value);
6020
+ if (Array.isArray(value)) return `[${value.map(stableJson).join(`,`)}]`;
6021
+ return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`).join(`,`)}}`;
6022
+ }
6023
+ function parseElectricOffset(offset) {
6024
+ if (offset === `-1`) return offset;
6025
+ return /^\d+_\d+$/.test(offset) ? offset : null;
6026
+ }
6027
+ function rowKeyForMessage(message) {
6028
+ const headers = message.headers;
6029
+ const candidate = headers.key ?? headers.rowKey ?? message.value?.id ?? message.value?.key ?? message.old_value?.id ?? message.old_value?.key;
6030
+ return candidate === void 0 ? void 0 : stableJson(candidate);
6031
+ }
6032
+ function pgSyncMessageToDurableEvent(message, optionsOrSourceRef) {
6033
+ const operation = message.headers.operation;
6034
+ if (operation !== `insert` && operation !== `update` && operation !== `delete`) return null;
6035
+ const sourceRef = typeof optionsOrSourceRef === `string` ? optionsOrSourceRef : (0, __electric_ax_agents_runtime.sourceRefForPgSync)(optionsOrSourceRef);
6036
+ const rowKey = rowKeyForMessage(message);
6037
+ const offset = message.headers.offset;
6038
+ if (typeof offset !== `string` || offset.length === 0) return null;
6039
+ const messageKeyPart = offset;
6040
+ const messageKey = `${sourceRef}:${operation}:${messageKeyPart}`;
6041
+ const timestamp$1 = new Date().toISOString();
6042
+ const oldValue = message.old_value;
6043
+ const safeValue = jsonSafe(message.value);
6044
+ const safeOldValue = jsonSafe(oldValue);
6045
+ const safeHeaders = jsonSafe(message.headers);
6046
+ return {
6047
+ type: `pg_sync_change`,
6048
+ key: messageKey,
6049
+ value: {
6050
+ key: messageKey,
6051
+ table: typeof optionsOrSourceRef === `string` ? void 0 : optionsOrSourceRef.table,
6052
+ operation,
6053
+ ...rowKey !== void 0 ? { rowKey } : {},
6054
+ ...message.value !== void 0 ? { value: safeValue } : {},
6055
+ ...oldValue !== void 0 ? { oldValue: safeOldValue } : {},
6056
+ headers: safeHeaders,
6057
+ ...typeof offset === `string` ? { offset } : {},
6058
+ receivedAt: timestamp$1
6059
+ },
6060
+ headers: {
6061
+ operation,
6062
+ timestamp: timestamp$1
6063
+ }
6064
+ };
6065
+ }
6066
+ function cursorFromRow(row) {
6067
+ return row?.shapeHandle && row.shapeOffset ? {
6068
+ handle: row.shapeHandle,
6069
+ offset: row.shapeOffset,
6070
+ initialSnapshotComplete: row.initialSnapshotComplete
6071
+ } : void 0;
6072
+ }
6073
+ var PgSyncBridge = class {
6074
+ producer = null;
6075
+ unsubscribe = null;
6076
+ abortController = null;
6077
+ skipChangesUntilUpToDate = false;
6078
+ recovering = false;
6079
+ committedCursor;
6080
+ retryAttempt = 0;
6081
+ constructor(sourceRef, streamUrl, options, resolvedSource, retry, streamClient, registry, evaluateWakes, initialCursor) {
6082
+ this.sourceRef = sourceRef;
6083
+ this.streamUrl = streamUrl;
6084
+ this.options = options;
6085
+ this.resolvedSource = resolvedSource;
6086
+ this.retry = retry;
6087
+ this.streamClient = streamClient;
6088
+ this.registry = registry;
6089
+ this.evaluateWakes = evaluateWakes;
6090
+ this.initialCursor = initialCursor;
6091
+ this.committedCursor = initialCursor;
6092
+ }
6093
+ async start() {
6094
+ if (!this.producer) this.producer = new __durable_streams_client.IdempotentProducer(new __durable_streams_client.DurableStream({
6095
+ url: `${this.streamClient.baseUrl}${this.streamUrl}`,
6096
+ contentType: `application/json`
6097
+ }), `pg-sync-bridge-${this.sourceRef}`);
6098
+ if (this.initialCursor) {
6099
+ const offset = parseElectricOffset(this.initialCursor.offset);
6100
+ if (offset) {
6101
+ this.startStream(offset, this.initialCursor.handle, !this.initialCursor.initialSnapshotComplete);
6102
+ return;
6103
+ }
6104
+ }
6105
+ await this.registry?.clearPgSyncBridgeCursor(this.sourceRef);
6106
+ this.startStream(`now`, void 0, true);
6107
+ }
6108
+ async stop() {
6109
+ this.unsubscribe?.();
6110
+ this.abortController?.abort();
6111
+ this.unsubscribe = null;
6112
+ this.abortController = null;
6113
+ try {
6114
+ await this.producer?.flush();
6115
+ } finally {
6116
+ await this.producer?.detach();
6117
+ this.producer = null;
6118
+ }
6119
+ }
6120
+ startStream(offset, handle, skipChangesUntilUpToDate = false, log = offset === `now` ? `changes_only` : `full`) {
6121
+ this.unsubscribe?.();
6122
+ this.abortController?.abort();
6123
+ this.skipChangesUntilUpToDate = skipChangesUntilUpToDate;
6124
+ this.abortController = new AbortController();
6125
+ const stream = new __electric_sql_client.ShapeStream({
6126
+ url: this.resolvedSource.url,
6127
+ params: buildElectricShapeParams(this.options),
6128
+ offset,
6129
+ log,
6130
+ ...handle ? { handle } : {},
6131
+ signal: this.abortController.signal
6132
+ });
6133
+ this.unsubscribe = stream.subscribe(async (messages) => {
6134
+ try {
6135
+ for (const message of messages) {
6136
+ if ((0, __electric_sql_client.isControlMessage)(message)) {
6137
+ if (message.headers.control === `must-refetch`) {
6138
+ await this.registry?.clearPgSyncBridgeCursor(this.sourceRef);
6139
+ this.startStream(`now`, void 0, true);
6140
+ return;
6141
+ }
6142
+ if (message.headers.control === `up-to-date`) {
6143
+ this.skipChangesUntilUpToDate = false;
6144
+ await this.persistCursor(stream, true);
6145
+ continue;
6146
+ }
6147
+ await this.persistCursor(stream);
6148
+ continue;
6149
+ }
6150
+ if (!(0, __electric_sql_client.isChangeMessage)(message)) continue;
6151
+ if (!this.skipChangesUntilUpToDate) {
6152
+ const event = pgSyncMessageToDurableEvent(message, this.options);
6153
+ if (event) {
6154
+ if (!this.producer) throw new Error(`pg-sync producer is not started`);
6155
+ await this.producer.append(JSON.stringify(event));
6156
+ await this.producer.flush?.();
6157
+ await this.evaluateWakes?.(this.streamUrl, event);
6158
+ }
6159
+ }
6160
+ await this.persistCursor(stream);
6161
+ this.retryAttempt = 0;
6162
+ }
6163
+ } catch (error) {
6164
+ serverLog.warn(`[pg-sync-bridge] subscription callback failed for ${this.sourceRef}:`, error);
6165
+ await this.recoverStream();
6166
+ }
6167
+ }, (error) => {
6168
+ if (this.abortController?.signal.aborted) return;
6169
+ serverLog.warn(`[pg-sync-bridge] subscription failed for ${this.sourceRef}:`, error);
6170
+ this.recoverStream();
6171
+ });
6172
+ }
6173
+ async recoverStream() {
6174
+ if (this.recovering) return;
6175
+ this.recovering = true;
6176
+ try {
6177
+ const attempt = this.retryAttempt++;
6178
+ const baseDelay = Math.min(this.retry.initialDelayMs * 2 ** attempt, this.retry.maxDelayMs);
6179
+ const jitter = Math.floor(baseDelay * .2 * this.retry.random());
6180
+ const delay = baseDelay + jitter;
6181
+ if (delay > 0) await this.retry.sleep(delay);
6182
+ const offset = this.committedCursor ? parseElectricOffset(this.committedCursor.offset) : null;
6183
+ if (offset && this.committedCursor) this.startStream(offset, this.committedCursor.handle, !this.committedCursor.initialSnapshotComplete);
6184
+ else {
6185
+ await this.registry?.clearPgSyncBridgeCursor(this.sourceRef);
6186
+ this.startStream(`now`, void 0, true);
6187
+ }
6188
+ } finally {
6189
+ this.recovering = false;
6190
+ }
6191
+ }
6192
+ async persistCursor(stream, initialSnapshotComplete = !this.skipChangesUntilUpToDate) {
6193
+ const shapeHandle = stream.shapeHandle;
6194
+ const shapeOffset = stream.lastOffset;
6195
+ if (!shapeHandle || !shapeOffset || shapeOffset === `-1`) return;
6196
+ await this.registry?.updatePgSyncBridgeCursor(this.sourceRef, shapeHandle, shapeOffset, initialSnapshotComplete);
6197
+ this.committedCursor = {
6198
+ handle: shapeHandle,
6199
+ offset: shapeOffset,
6200
+ initialSnapshotComplete
6201
+ };
6202
+ }
6203
+ };
6204
+ var PgSyncBridgeManager = class {
6205
+ bridges = new Map();
6206
+ starting = new Map();
6207
+ url;
6208
+ retry;
6209
+ constructor(streamClient, evaluateWakes, registry, options = {}) {
6210
+ this.streamClient = streamClient;
6211
+ this.evaluateWakes = evaluateWakes;
6212
+ this.registry = registry;
6213
+ this.url = options.url ?? PG_SYNC_ELECTRIC_SHAPE_URL;
6214
+ this.retry = {
6215
+ initialDelayMs: options.retry?.initialDelayMs ?? DEFAULT_RETRY_INITIAL_DELAY_MS,
6216
+ maxDelayMs: options.retry?.maxDelayMs ?? DEFAULT_RETRY_MAX_DELAY_MS,
6217
+ random: options.retry?.random ?? Math.random,
6218
+ sleep: options.retry?.sleep ?? ((ms) => new Promise((resolve$1) => setTimeout(resolve$1, ms)))
6219
+ };
6220
+ }
6221
+ async start() {
6222
+ const rows = await this.registry?.listPgSyncBridges?.();
6223
+ if (!rows) return;
6224
+ await Promise.all(rows.map((row) => this.ensureBridge(row).catch((error) => {
6225
+ serverLog.warn(`[pg-sync-bridge] failed to start ${row.sourceRef}:`, error);
6226
+ })));
6227
+ }
6228
+ async register(options, metadata) {
6229
+ const mergedMetadata = {
6230
+ ...options.metadata,
6231
+ ...metadata
6232
+ };
6233
+ const canonicalOptions = {
6234
+ ...(0, __electric_ax_agents_runtime.canonicalPgSyncOptions)(options),
6235
+ ...Object.keys(mergedMetadata).length > 0 ? { metadata: mergedMetadata } : {}
6236
+ };
6237
+ const resolvedSource = this.resolveSource(canonicalOptions);
6238
+ const sourceRef = (0, __electric_ax_agents_runtime.sourceRefForPgSync)(canonicalOptions);
6239
+ const streamUrl = (0, __electric_ax_agents_runtime.getPgSyncStreamPath)(sourceRef, this.registry?.tenantId);
6240
+ const row = await this.registry?.upsertPgSyncBridge({
6241
+ sourceRef,
6242
+ options: canonicalOptions,
6243
+ streamUrl
6244
+ });
6245
+ await this.streamClient.ensure(streamUrl, { contentType: `application/json` });
6246
+ if (!this.bridges.has(sourceRef)) {
6247
+ let start = this.starting.get(sourceRef);
6248
+ if (!start) {
6249
+ start = (async () => {
6250
+ const bridge = new PgSyncBridge(sourceRef, streamUrl, canonicalOptions, resolvedSource, this.retry, this.streamClient, this.registry, this.evaluateWakes, cursorFromRow(row));
6251
+ await bridge.start();
6252
+ this.bridges.set(sourceRef, bridge);
6253
+ })().finally(() => this.starting.delete(sourceRef));
6254
+ this.starting.set(sourceRef, start);
6255
+ }
6256
+ await start;
6257
+ }
6258
+ return {
6259
+ sourceRef,
6260
+ streamUrl
6261
+ };
6262
+ }
6263
+ async ensureBridge(row) {
6264
+ if (this.bridges.has(row.sourceRef)) return;
6265
+ let start = this.starting.get(row.sourceRef);
6266
+ if (!start) {
6267
+ start = (async () => {
6268
+ await this.streamClient.ensure(row.streamUrl, { contentType: `application/json` });
6269
+ const canonicalOptions = (0, __electric_ax_agents_runtime.canonicalPgSyncOptions)(row.options);
6270
+ const resolvedSource = this.resolveSource(canonicalOptions);
6271
+ const bridge = new PgSyncBridge(row.sourceRef, row.streamUrl, canonicalOptions, resolvedSource, this.retry, this.streamClient, this.registry, this.evaluateWakes, cursorFromRow(row));
6272
+ await bridge.start();
6273
+ this.bridges.set(row.sourceRef, bridge);
6274
+ })().finally(() => this.starting.delete(row.sourceRef));
6275
+ this.starting.set(row.sourceRef, start);
6276
+ }
6277
+ await start;
6278
+ }
6279
+ resolveSource(options) {
6280
+ return { url: options.url ?? this.url };
6281
+ }
6282
+ async stop() {
6283
+ await Promise.allSettled(this.starting.values());
6284
+ await Promise.all([...this.bridges.values()].map((bridge) => bridge.stop()));
6285
+ this.bridges.clear();
6286
+ }
6287
+ };
6288
+
5870
6289
  //#endregion
5871
6290
  //#region src/runtime.ts
5872
6291
  function omitUndefined(value) {
@@ -5881,6 +6300,7 @@ var ElectricAgentsTenantRuntime = class {
5881
6300
  wakeRegistry;
5882
6301
  scheduler;
5883
6302
  entityBridgeManager;
6303
+ pgSyncBridgeManager;
5884
6304
  claimWriteTokens;
5885
6305
  manager;
5886
6306
  constructor(options) {
@@ -5905,9 +6325,10 @@ var ElectricAgentsTenantRuntime = class {
5905
6325
  writeTokenValidator: (entity, token) => this.claimWriteTokens.isValid(this.serviceId, entity.streams.main, token),
5906
6326
  stopWakeRegistryOnShutdown: options.stopWakeRegistryOnShutdown ?? false
5907
6327
  });
6328
+ this.pgSyncBridgeManager = options.pgSyncBridgeManager ?? new PgSyncBridgeManager(this.streamClient, (sourceUrl, event) => this.manager.evaluateWakes(sourceUrl, event), this.registry, options.pgSync);
5908
6329
  }
5909
6330
  async stop() {
5910
- await this.manager.shutdown();
6331
+ await Promise.all([this.manager.shutdown(), this.pgSyncBridgeManager.stop()]);
5911
6332
  }
5912
6333
  async rehydrateCronSchedules() {
5913
6334
  const rows = await this.db.select({ sourceUrl: wakeRegistrations.sourceUrl }).from(wakeRegistrations).where((0, drizzle_orm.eq)(wakeRegistrations.tenantId, this.serviceId));
@@ -6671,7 +7092,10 @@ var WakeRegistry = class {
6671
7092
  }
6672
7093
  if (!(0, __electric_sql_client.isChangeMessage)(message)) return;
6673
7094
  if (message.headers.operation === `delete`) {
6674
- this.removeCachedRegistrationByDbId(Number(message.key));
7095
+ const oldValue = message.old_value;
7096
+ const oldId = Number(oldValue?.id);
7097
+ if (Number.isFinite(oldId)) this.removeCachedRegistrationByDbId(oldId);
7098
+ else this.resetCachedRegistrations();
6675
7099
  return;
6676
7100
  }
6677
7101
  this.upsertCachedRegistration(this.normalizeShapeRow(message.value));
@@ -6782,9 +7206,9 @@ var WakeRegistry = class {
6782
7206
  matchCondition(reg, event) {
6783
7207
  if (reg.condition === `runFinished`) {
6784
7208
  if (event.type !== `run`) return null;
6785
- const value = event.value;
7209
+ const value$1 = event.value;
6786
7210
  const headers$1 = event.headers;
6787
- const status$4 = value?.status;
7211
+ const status$4 = value$1?.status;
6788
7212
  const operation$1 = headers$1?.operation;
6789
7213
  if (operation$1 !== `update`) return null;
6790
7214
  if (status$4 !== `completed` && status$4 !== `failed`) return null;
@@ -6807,13 +7231,15 @@ var WakeRegistry = class {
6807
7231
  }
6808
7232
  const kind = operation === `delete` ? `delete` : operation === `update` ? `update` : `insert`;
6809
7233
  if (condition.ops && condition.ops.length > 0 && !condition.ops.includes(kind)) return null;
7234
+ const value = event.value;
6810
7235
  const change = {
6811
7236
  collection: eventType,
6812
7237
  kind,
6813
7238
  key: event.key || ``
6814
7239
  };
7240
+ if (value && `value` in value) change.value = value.value;
7241
+ if (value && `oldValue` in value) change.oldValue = value.oldValue;
6815
7242
  if (eventType === `inbox`) {
6816
- const value = event.value;
6817
7243
  if (typeof value?.from === `string`) change.from = value.from;
6818
7244
  if (typeof value?.from_principal === `string`) change.from_principal = value.from_principal;
6819
7245
  if (typeof value?.from_agent === `string`) change.from_agent = value.from_agent;
@@ -7217,6 +7643,22 @@ function resolveDurableStreamsRoutingAdapter(adapter, _durableStreamsUrl) {
7217
7643
 
7218
7644
  //#endregion
7219
7645
  //#region src/utils/server-utils.ts
7646
+ /**
7647
+ * Raised when an Electric shape proxy request must be rejected for security
7648
+ * reasons (an un-scoped table, or a client `where` clause that could escape the
7649
+ * enforced per-tenant/per-principal scoping). The global `errorMapper` hook
7650
+ * maps this to an HTTP error response. Defined here (rather than reusing
7651
+ * `ElectricAgentsError`) to keep this module free of the heavy entity-manager
7652
+ * import graph.
7653
+ */
7654
+ var ElectricProxyError = class extends Error {
7655
+ constructor(code, message, status$4) {
7656
+ super(message);
7657
+ this.code = code;
7658
+ this.status = status$4;
7659
+ this.name = `ElectricProxyError`;
7660
+ }
7661
+ };
7220
7662
  function buildElectricProxyTarget(options) {
7221
7663
  const targetPath = options.incomingUrl.pathname.replace(`/_electric/electric`, ``);
7222
7664
  const target = electricUrlWithPath(options.electricUrl, targetPath);
@@ -7226,7 +7668,12 @@ function buildElectricProxyTarget(options) {
7226
7668
  applyElectricUrlQueryParams(target, options.electricUrl);
7227
7669
  if (targetPath !== `/v1/shape`) return target;
7228
7670
  if (options.electricSecret) target.searchParams.set(`secret`, options.electricSecret);
7229
- const table = options.incomingUrl.searchParams.get(`table`);
7671
+ const clientWhere = options.incomingUrl.searchParams.get(`where`);
7672
+ if (clientWhere !== null && !isSelfContainedWhereClause(clientWhere)) throw new ElectricProxyError(`INVALID_WHERE`, `Invalid where clause`, 400);
7673
+ const tableParams = options.incomingUrl.searchParams.getAll(`table`);
7674
+ if (tableParams.length !== 1) throw new ElectricProxyError(`TABLE_NOT_ALLOWED`, `Table is not available through the Electric proxy`, 403);
7675
+ const table = tableParams[0];
7676
+ target.searchParams.set(`table`, table);
7230
7677
  if (table === `entities`) {
7231
7678
  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"`);
7232
7679
  applyShapeWhere(target, buildReadableEntitiesWhere({
@@ -7284,9 +7731,39 @@ function buildElectricProxyTarget(options) {
7284
7731
  principalKind: options.principalKind ?? ``,
7285
7732
  permissionBypass: options.permissionBypass
7286
7733
  }));
7287
- }
7734
+ } else throw new ElectricProxyError(`TABLE_NOT_ALLOWED`, `Table is not available through the Electric proxy`, 403);
7288
7735
  return target;
7289
7736
  }
7737
+ /**
7738
+ * Returns true when a client-supplied Electric `where` clause is self-contained:
7739
+ * its parentheses are balanced, never close below the top level, all string
7740
+ * (`'`) and identifier (`"`) literals are terminated, and it contains no SQL
7741
+ * comment markers. Such a clause cannot break out of the `(...)` group it is
7742
+ * wrapped in when AND-combined with the enforced scoping predicate, nor comment
7743
+ * out the trailing paren the proxy appends. Characters inside string/identifier
7744
+ * literals are ignored. Comment markers are rejected unconditionally (even where
7745
+ * harmless) as a conservative defensive measure; dollar-quoted and `E''` strings
7746
+ * are not modeled and only ever cause fail-safe over-rejection, never a bypass.
7747
+ */
7748
+ function isSelfContainedWhereClause(where) {
7749
+ let depth = 0;
7750
+ let quote = null;
7751
+ for (let i = 0; i < where.length; i++) {
7752
+ const ch = where[i];
7753
+ if (quote !== null) {
7754
+ if (ch === quote) if (where[i + 1] === quote) i++;
7755
+ else quote = null;
7756
+ continue;
7757
+ }
7758
+ if (ch === `'` || ch === `"`) quote = ch;
7759
+ else if (ch === `(`) depth++;
7760
+ else if (ch === `)`) {
7761
+ depth--;
7762
+ if (depth < 0) return false;
7763
+ } else if (ch === `-` && where[i + 1] === `-` || ch === `/` && where[i + 1] === `*`) return false;
7764
+ }
7765
+ return depth === 0 && quote === null;
7766
+ }
7290
7767
  function buildReadableEntitiesWhere(options) {
7291
7768
  const tenant = sqlStringLiteral(options.tenantId);
7292
7769
  if (options.permissionBypass) return `tenant_id = ${tenant}`;
@@ -8294,7 +8771,7 @@ async function parseAttachmentForm(request) {
8294
8771
  };
8295
8772
  }
8296
8773
  function contentDisposition(filename) {
8297
- const fallback = filename.replace(/["\\\r\n]/g, `_`);
8774
+ const fallback = filename.replace(/[^\x20-\x7e]|["\\]/g, `_`);
8298
8775
  return `attachment; filename="${fallback}"; filename*=UTF-8''${encodeURIComponent(filename)}`;
8299
8776
  }
8300
8777
  function rejectPrincipalEntityMutation(request, action) {
@@ -8492,22 +8969,28 @@ async function deleteEventSourceSubscription(request, ctx) {
8492
8969
  const result = await ctx.entityManager.deleteEventSourceSubscription(entityUrl, { id: decodeURIComponent(request.params.subscriptionId) });
8493
8970
  return (0, itty_router.json)(result);
8494
8971
  }
8972
+ function tagResponseBody(entity) {
8973
+ const publicEntity = toPublicEntity(entity);
8974
+ if (entity.txid !== void 0) return {
8975
+ ...publicEntity,
8976
+ txid: entity.txid
8977
+ };
8978
+ return publicEntity;
8979
+ }
8495
8980
  async function setTag(request, ctx) {
8496
8981
  const principalMutationError = rejectPrincipalEntityMutation(request, `tag updated`);
8497
8982
  if (principalMutationError) return principalMutationError;
8498
8983
  const parsed = routeBody(request);
8499
8984
  const { entityUrl } = requireExistingEntityRoute(request);
8500
- const token = writeTokenFromRequest(request);
8501
- const updated = await ctx.entityManager.setTag(entityUrl, decodeURIComponent(request.params.tagKey), { value: parsed.value }, token);
8502
- return (0, itty_router.json)(toPublicEntity(updated));
8985
+ const updated = await ctx.entityManager.setTag(entityUrl, decodeURIComponent(request.params.tagKey), { value: parsed.value });
8986
+ return (0, itty_router.json)(tagResponseBody(updated));
8503
8987
  }
8504
8988
  async function deleteTag(request, ctx) {
8505
8989
  const principalMutationError = rejectPrincipalEntityMutation(request, `tag deleted`);
8506
8990
  if (principalMutationError) return principalMutationError;
8507
8991
  const { entityUrl } = requireExistingEntityRoute(request);
8508
- const token = writeTokenFromRequest(request);
8509
- const updated = await ctx.entityManager.deleteTag(entityUrl, decodeURIComponent(request.params.tagKey), token);
8510
- return (0, itty_router.json)(toPublicEntity(updated));
8992
+ const updated = await ctx.entityManager.deleteTag(entityUrl, decodeURIComponent(request.params.tagKey));
8993
+ return (0, itty_router.json)(tagResponseBody(updated));
8511
8994
  }
8512
8995
  async function forkEntity(request, ctx) {
8513
8996
  const principalMutationError = rejectPrincipalEntityMutation(request, `forked`);
@@ -8574,9 +9057,12 @@ async function sendEntity(request, ctx) {
8574
9057
  mode: parsed.mode,
8575
9058
  position: parsed.position
8576
9059
  };
8577
- if (parsed.afterMs && parsed.afterMs > 0) await ctx.entityManager.enqueueDelayedSend(entityUrl, sendReq, new Date(Date.now() + parsed.afterMs));
8578
- else await ctx.entityManager.send(entityUrl, sendReq);
8579
- return (0, itty_router.status)(204);
9060
+ if (parsed.afterMs && parsed.afterMs > 0) {
9061
+ await ctx.entityManager.enqueueDelayedSend(entityUrl, sendReq, new Date(Date.now() + parsed.afterMs));
9062
+ return (0, itty_router.status)(204);
9063
+ }
9064
+ const result = await ctx.entityManager.send(entityUrl, sendReq);
9065
+ return (0, itty_router.json)(result);
8580
9066
  }
8581
9067
  async function createAttachment(request, ctx) {
8582
9068
  const principalMutationError = rejectPrincipalEntityMutation(request, `given attachments`);
@@ -8619,13 +9105,13 @@ async function deleteAttachment(request, ctx) {
8619
9105
  async function updateInboxMessage(request, ctx) {
8620
9106
  const parsed = routeBody(request);
8621
9107
  const { entityUrl } = requireExistingEntityRoute(request);
8622
- await ctx.entityManager.updateInboxMessage(entityUrl, decodeURIComponent(request.params.messageKey), parsed);
8623
- return (0, itty_router.status)(204);
9108
+ const result = await ctx.entityManager.updateInboxMessage(entityUrl, decodeURIComponent(request.params.messageKey), parsed);
9109
+ return (0, itty_router.json)(result);
8624
9110
  }
8625
9111
  async function deleteInboxMessage(request, ctx) {
8626
9112
  const { entityUrl } = requireExistingEntityRoute(request);
8627
- await ctx.entityManager.deleteInboxMessage(entityUrl, decodeURIComponent(request.params.messageKey));
8628
- return (0, itty_router.status)(204);
9113
+ const result = await ctx.entityManager.deleteInboxMessage(entityUrl, decodeURIComponent(request.params.messageKey));
9114
+ return (0, itty_router.json)(result);
8629
9115
  }
8630
9116
  async function spawnEntity(request, ctx) {
8631
9117
  const parsed = routeBody(request);
@@ -8896,6 +9382,49 @@ function toPublicEntityType(entityType) {
8896
9382
  };
8897
9383
  }
8898
9384
 
9385
+ //#endregion
9386
+ //#region src/routing/pg-sync-router.ts
9387
+ const pgSyncOptionsSchema = __sinclair_typebox.Type.Object({
9388
+ url: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
9389
+ table: __sinclair_typebox.Type.String(),
9390
+ columns: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Array(__sinclair_typebox.Type.String())),
9391
+ where: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
9392
+ params: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Union([__sinclair_typebox.Type.Array(__sinclair_typebox.Type.String()), __sinclair_typebox.Type.Record(__sinclair_typebox.Type.String(), __sinclair_typebox.Type.String())])),
9393
+ replica: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Union([__sinclair_typebox.Type.Literal(`default`), __sinclair_typebox.Type.Literal(`full`)]))
9394
+ });
9395
+ const pgSyncRequestMetadataSchema = __sinclair_typebox.Type.Object({
9396
+ entityUrl: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
9397
+ entityType: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
9398
+ streamPath: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
9399
+ runtimeConsumerId: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
9400
+ wakeId: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String())
9401
+ });
9402
+ const pgSyncRegisterBodySchema = __sinclair_typebox.Type.Object({
9403
+ options: pgSyncOptionsSchema,
9404
+ metadata: __sinclair_typebox.Type.Optional(pgSyncRequestMetadataSchema)
9405
+ });
9406
+ const pgSyncRouter = (0, itty_router.Router)({ base: `/_electric/pg-sync` });
9407
+ pgSyncRouter.post(`/register`, withSchema(pgSyncRegisterBodySchema), registerPgSync);
9408
+ async function registerPgSync(request, ctx) {
9409
+ const { options, metadata } = routeBody(request);
9410
+ if (options.table.trim() === ``) return apiError(400, ErrCodeInvalidRequest, `pgSync table must be non-empty`);
9411
+ if (!ctx.pgSyncBridgeManager) return apiError(503, ErrCodeInvalidRequest, `pgSync bridge manager is not configured`);
9412
+ try {
9413
+ const requestMetadata$1 = {
9414
+ tenantId: ctx.service,
9415
+ principalKind: ctx.principal.kind,
9416
+ principalId: ctx.principal.id,
9417
+ principalKey: ctx.principal.key,
9418
+ principalUrl: ctx.principal.url,
9419
+ ...metadata ?? {}
9420
+ };
9421
+ const result = await ctx.pgSyncBridgeManager.register(options, requestMetadata$1);
9422
+ return (0, itty_router.json)(result);
9423
+ } catch (error) {
9424
+ return apiError(500, ErrCodeInvalidRequest, `pgSync registration failed: ${error instanceof Error ? error.message : String(error)}`);
9425
+ }
9426
+ }
9427
+
8899
9428
  //#endregion
8900
9429
  //#region src/routing/hooks.ts
8901
9430
  const SPAN_KEY = Symbol(`agents-server.otel-span`);
@@ -8970,6 +9499,10 @@ function errorMapper(err, req) {
8970
9499
  });
8971
9500
  }
8972
9501
  if (err instanceof ElectricAgentsError) return apiError(err.status, err.code, err.message, err.details);
9502
+ if (err instanceof ElectricProxyError) {
9503
+ serverLog.warn(`[agent-server] Electric proxy rejected request (${err.code}): ${req.url}`);
9504
+ return apiError(err.status, err.code, err.message);
9505
+ }
8973
9506
  serverLog.error(`[agent-server] Unhandled error:`, err);
8974
9507
  return apiError(500, `INTERNAL_SERVER_ERROR`, `Internal server error`);
8975
9508
  }
@@ -9420,6 +9953,7 @@ internalRouter.all(`/runners`, runnersRouter.fetch);
9420
9953
  internalRouter.all(`/runners/*`, runnersRouter.fetch);
9421
9954
  internalRouter.all(`/entities/*`, entitiesRouter.fetch);
9422
9955
  internalRouter.all(`/entity-types/*`, entityTypesRouter.fetch);
9956
+ internalRouter.all(`/pg-sync/*`, pgSyncRouter.fetch);
9423
9957
  internalRouter.all(`/observations/*`, observationsRouter.fetch);
9424
9958
  internalRouter.get(`/electric/*`, electricProxyRouter.fetch);
9425
9959
  internalRouter.all(`*`, () => (0, itty_router.status)(404));
@@ -9803,6 +10337,9 @@ const globalRouter = (0, itty_router.AutoRouter)({
9803
10337
  finally: [otelEndSpan, applyCors]
9804
10338
  });
9805
10339
  globalRouter.all(`/_electric/shared-state/*`, durableStreamsRouter.fetch);
10340
+ globalRouter.all(`/_electric/pg-sync/register`, internalRouter.fetch);
10341
+ globalRouter.get(`/_electric/pg-sync/*`, durableStreamsRouter.fetch);
10342
+ globalRouter.head(`/_electric/pg-sync/*`, durableStreamsRouter.fetch);
9806
10343
  globalRouter.all(`/_electric/*`, internalRouter.fetch);
9807
10344
  globalRouter.all(`*`, durableStreamsRouter.fetch);
9808
10345