@electric-ax/agents-server 0.4.20 → 0.5.1

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,6 +4,7 @@
4
4
 
5
5
  import { Type, type Static } from '@sinclair/typebox'
6
6
  import { Router, json, status } from 'itty-router'
7
+ import { COMMENTS_CONTRACT } from '@electric-ax/agents-runtime'
7
8
  import { dispatchPolicySchema } from '../dispatch-policy-schema.js'
8
9
  import { ElectricAgentsError } from '../entity-manager.js'
9
10
  import {
@@ -45,6 +46,29 @@ type PublicEntityTypeResponse = ElectricAgentsEntityType & {
45
46
 
46
47
  const jsonObjectSchema = Type.Record(Type.String(), Type.Unknown())
47
48
  const schemaMapSchema = Type.Record(Type.String(), jsonObjectSchema)
49
+ // `principalColumn` is accepted and ignored: older runtimes still send it
50
+ // (the column is fixed to `_principal` now), and rejecting it would break
51
+ // registration during version skew.
52
+ const externallyWritableCollectionsSchema = Type.Record(
53
+ Type.String(),
54
+ Type.Object(
55
+ {
56
+ type: Type.String(),
57
+ contract: Type.Optional(Type.String()),
58
+ operations: Type.Optional(
59
+ Type.Array(
60
+ Type.Union([
61
+ Type.Literal(`insert`),
62
+ Type.Literal(`update`),
63
+ Type.Literal(`delete`),
64
+ ])
65
+ )
66
+ ),
67
+ principalColumn: Type.Optional(Type.String()),
68
+ },
69
+ { additionalProperties: false }
70
+ )
71
+ )
48
72
  const slashCommandArgumentSchema = Type.Object(
49
73
  {
50
74
  name: Type.String(),
@@ -93,6 +117,9 @@ const registerEntityTypeBodySchema = Type.Object(
93
117
  permission_grants: Type.Optional(
94
118
  Type.Array(typePermissionGrantInputSchema)
95
119
  ),
120
+ externally_writable_collections: Type.Optional(
121
+ externallyWritableCollectionsSchema
122
+ ),
96
123
  },
97
124
  { additionalProperties: false }
98
125
  )
@@ -445,9 +472,37 @@ function parseExpiresAt(value: string | undefined): Date | undefined {
445
472
  return expiresAt
446
473
  }
447
474
 
475
+ /**
476
+ * The `comments` collection name is reserved for the canonical comments
477
+ * contract: the UI keys its comment affordances on it, so a divergent
478
+ * collection registered under that name (or the contract mounted under
479
+ * another name) would break that assumption silently.
480
+ */
481
+ function validateExternallyWritableCollections(
482
+ collections: RegisterEntityTypeRequest[`externally_writable_collections`]
483
+ ): void {
484
+ for (const [name, config] of Object.entries(collections ?? {})) {
485
+ if (name === `comments` && config.contract !== COMMENTS_CONTRACT) {
486
+ throw new ElectricAgentsError(
487
+ ErrCodeInvalidRequest,
488
+ `The externally-writable collection name "comments" is reserved for the "${COMMENTS_CONTRACT}" contract`,
489
+ 400
490
+ )
491
+ }
492
+ if (config.contract === COMMENTS_CONTRACT && name !== `comments`) {
493
+ throw new ElectricAgentsError(
494
+ ErrCodeInvalidRequest,
495
+ `The "${COMMENTS_CONTRACT}" contract must be registered under the collection name "comments"`,
496
+ 400
497
+ )
498
+ }
499
+ }
500
+ }
501
+
448
502
  function normalizeEntityTypeRequest(
449
503
  parsed: RegisterEntityTypeBody | RegisterEntityTypeRequest
450
504
  ): RegisterEntityTypeRequest {
505
+ validateExternallyWritableCollections(parsed.externally_writable_collections)
451
506
  const serveEndpoint = rewriteLoopbackWebhookUrl(parsed.serve_endpoint)
452
507
  return {
453
508
  name: parsed.name ?? ``,
@@ -465,6 +520,7 @@ function normalizeEntityTypeRequest(
465
520
  } as RegisterEntityTypeRequest[`default_dispatch_policy`])
466
521
  : undefined),
467
522
  permission_grants: parsed.permission_grants,
523
+ externally_writable_collections: parsed.externally_writable_collections,
468
524
  }
469
525
  }
470
526
 
@@ -38,7 +38,7 @@ import { routeBody, validateOptionalJsonBody, withSchema } from './schema.js'
38
38
  import { withLeadingSlash } from './tenant-stream-paths.js'
39
39
  import type { IRequest, RouterType } from 'itty-router'
40
40
  import type {
41
- EventSourceContract,
41
+ WebhookSourceContract,
42
42
  WebhookSignatureVerifierConfig,
43
43
  } from '@electric-ax/agents-runtime'
44
44
  import type { TenantContext } from './context.js'
@@ -121,7 +121,7 @@ export const internalRouter: InternalRoutes = Router<
121
121
  })
122
122
 
123
123
  internalRouter.get(`/health`, () => json({ status: `ok` }))
124
- internalRouter.get(`/event-sources`, listEventSources)
124
+ internalRouter.get(`/webhook-sources`, listWebhookSources)
125
125
  internalRouter.post(
126
126
  `/wake`,
127
127
  withSchema(wakeRegistrationBodySchema),
@@ -366,17 +366,19 @@ async function registerWake(
366
366
  return status(204)
367
367
  }
368
368
 
369
- async function listEventSources(
369
+ async function listWebhookSources(
370
370
  _request: IRequest,
371
371
  ctx: TenantContext
372
372
  ): Promise<Response> {
373
- const eventSources = ctx.eventSources
374
- ? await ctx.eventSources.listEventSources()
373
+ const webhookSources = ctx.webhookSources
374
+ ? await ctx.webhookSources.listWebhookSources()
375
375
  : []
376
- return json({ eventSources: eventSources.filter(isAgentVisibleEventSource) })
376
+ return json({
377
+ webhookSources: webhookSources.filter(isAgentVisibleWebhookSource),
378
+ })
377
379
  }
378
380
 
379
- function isAgentVisibleEventSource(source: EventSourceContract): boolean {
381
+ function isAgentVisibleWebhookSource(source: WebhookSourceContract): boolean {
380
382
  return source.agentVisible === true && source.status === `active`
381
383
  }
382
384
 
@@ -8,6 +8,8 @@ import type {
8
8
  } from '@electric-ax/agents-runtime'
9
9
  import { Type, type Static } from '@sinclair/typebox'
10
10
  import { Router, json } from 'itty-router'
11
+ import { PgSyncSourceValidationError } from '../pg-sync-bridge-manager.js'
12
+ import { serverLog } from '../utils/log.js'
11
13
  import { apiError } from '../electric-agents-http.js'
12
14
  import { ErrCodeInvalidRequest } from '../electric-agents-types.js'
13
15
  import { routeBody, withSchema } from './schema.js'
@@ -16,7 +18,7 @@ import type { RouterType } from 'itty-router'
16
18
  import type { TenantContext } from './context.js'
17
19
 
18
20
  const pgSyncOptionsSchema = Type.Object({
19
- url: Type.Optional(Type.String()),
21
+ url: Type.String(),
20
22
  table: Type.String(),
21
23
  columns: Type.Optional(Type.Array(Type.String())),
22
24
  where: Type.Optional(Type.String()),
@@ -72,6 +74,10 @@ async function registerPgSync(
72
74
  ): Promise<Response> {
73
75
  const { options, metadata } = routeBody<PgSyncRegisterBody>(request)
74
76
 
77
+ if (options.url.trim() === ``) {
78
+ return apiError(400, ErrCodeInvalidRequest, `pgSync url must be non-empty`)
79
+ }
80
+
75
81
  if (options.table.trim() === ``) {
76
82
  return apiError(
77
83
  400,
@@ -104,6 +110,13 @@ async function registerPgSync(
104
110
 
105
111
  return json(result)
106
112
  } catch (error) {
113
+ if (error instanceof PgSyncSourceValidationError) {
114
+ return apiError(400, ErrCodeInvalidRequest, error.message)
115
+ }
116
+ serverLog.error(
117
+ `[pg-sync] registration failed for table "${options.table}":`,
118
+ error
119
+ )
107
120
  return apiError(
108
121
  500,
109
122
  ErrCodeInvalidRequest,
package/src/server.ts CHANGED
@@ -35,7 +35,7 @@ import type { Principal } from './principal.js'
35
35
  import type { EntityBridgeCoordinator } from './entity-bridge-manager.js'
36
36
  import type { DurableStreamsRoutingAdapter } from './routing/durable-streams-routing-adapter.js'
37
37
  import type { OssServerContext } from './routing/oss-server-router.js'
38
- import type { EventSourceCatalog } from './routing/context.js'
38
+ import type { WebhookSourceCatalog } from './routing/context.js'
39
39
  import type { PgSyncBridgeManagerOptions } from './pg-sync-bridge-manager.js'
40
40
  import type { StartedStandaloneAgentsRuntime } from './standalone-runtime.js'
41
41
  import type { DurableStreamsBearerProvider } from './stream-client.js'
@@ -71,8 +71,8 @@ export interface ElectricAgentsServerOptions {
71
71
  ) => Promise<Principal | null> | Principal | null
72
72
  authorizeRequest?: AuthorizeRequest
73
73
  allowDevPrincipalFallback?: boolean
74
- eventSources?: EventSourceCatalog
75
- ensureEventSourceWakeSource?: (sourceUrl: string) => Promise<void> | void
74
+ webhookSources?: WebhookSourceCatalog
75
+ ensureWebhookSourceWakeSource?: (sourceUrl: string) => Promise<void> | void
76
76
  pgSync?: PgSyncBridgeManagerOptions
77
77
  /**
78
78
  * Disabled by default. When set to a positive interval, periodically
@@ -450,13 +450,13 @@ export class ElectricAgentsServer {
450
450
  runtime: this.standaloneRuntime.runtime,
451
451
  entityBridgeManager: this.entityBridgeManager,
452
452
  pgSyncBridgeManager: this.standaloneRuntime.runtime.pgSyncBridgeManager,
453
- ...(this.options.eventSources
454
- ? { eventSources: this.options.eventSources }
453
+ ...(this.options.webhookSources
454
+ ? { webhookSources: this.options.webhookSources }
455
455
  : {}),
456
- ...(this.options.ensureEventSourceWakeSource
456
+ ...(this.options.ensureWebhookSourceWakeSource
457
457
  ? {
458
- ensureEventSourceWakeSource:
459
- this.options.ensureEventSourceWakeSource,
458
+ ensureWebhookSourceWakeSource:
459
+ this.options.ensureWebhookSourceWakeSource,
460
460
  }
461
461
  : {}),
462
462
  ...(this.options.authorizeRequest
@@ -967,6 +967,8 @@ export class WakeRegistry {
967
967
  }
968
968
  if (value && `oldValue` in value) {
969
969
  change.oldValue = value.oldValue
970
+ } else if (value && `old_value` in value) {
971
+ change.oldValue = value.old_value
970
972
  }
971
973
 
972
974
  if (eventType === `inbox`) {