@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.
- package/dist/entrypoint.js +1003 -834
- package/dist/index.cjs +241 -72
- package/dist/index.d.cts +2507 -2440
- package/dist/index.d.ts +2506 -2441
- package/dist/index.js +242 -73
- package/drizzle/0016_entity_type_externally_writable_collections.sql +1 -0
- package/drizzle/meta/_journal.json +7 -0
- package/package.json +5 -5
- package/src/db/schema.ts +4 -0
- package/src/electric-agents-types.ts +23 -0
- package/src/entity-manager.ts +157 -7
- package/src/entity-registry.ts +25 -1
- package/src/index.ts +6 -6
- package/src/manifest-side-effects.ts +2 -6
- package/src/pg-sync-bridge-manager.ts +147 -47
- package/src/routing/context.ts +11 -11
- package/src/routing/entities-router.ts +112 -30
- package/src/routing/entity-types-router.ts +56 -0
- package/src/routing/internal-router.ts +9 -7
- package/src/routing/pg-sync-router.ts +14 -1
- package/src/server.ts +8 -8
- package/src/wake-registry.ts +2 -0
|
@@ -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
|
-
|
|
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(`/
|
|
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
|
|
369
|
+
async function listWebhookSources(
|
|
370
370
|
_request: IRequest,
|
|
371
371
|
ctx: TenantContext
|
|
372
372
|
): Promise<Response> {
|
|
373
|
-
const
|
|
374
|
-
? await ctx.
|
|
373
|
+
const webhookSources = ctx.webhookSources
|
|
374
|
+
? await ctx.webhookSources.listWebhookSources()
|
|
375
375
|
: []
|
|
376
|
-
return json({
|
|
376
|
+
return json({
|
|
377
|
+
webhookSources: webhookSources.filter(isAgentVisibleWebhookSource),
|
|
378
|
+
})
|
|
377
379
|
}
|
|
378
380
|
|
|
379
|
-
function
|
|
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.
|
|
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 {
|
|
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
|
-
|
|
75
|
-
|
|
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.
|
|
454
|
-
? {
|
|
453
|
+
...(this.options.webhookSources
|
|
454
|
+
? { webhookSources: this.options.webhookSources }
|
|
455
455
|
: {}),
|
|
456
|
-
...(this.options.
|
|
456
|
+
...(this.options.ensureWebhookSourceWakeSource
|
|
457
457
|
? {
|
|
458
|
-
|
|
459
|
-
this.options.
|
|
458
|
+
ensureWebhookSourceWakeSource:
|
|
459
|
+
this.options.ensureWebhookSourceWakeSource,
|
|
460
460
|
}
|
|
461
461
|
: {}),
|
|
462
462
|
...(this.options.authorizeRequest
|
package/src/wake-registry.ts
CHANGED