@electric-ax/agents-server 0.4.0 → 0.4.2
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 +381 -96
- package/dist/index.cjs +491 -198
- package/dist/index.d.cts +286 -244
- package/dist/index.d.ts +286 -244
- package/dist/index.js +491 -198
- package/drizzle/0006_principals.sql +5 -0
- package/drizzle/meta/_journal.json +7 -0
- package/package.json +6 -6
- package/src/authenticated-user-format.ts +4 -14
- package/src/db/index.ts +1 -1
- package/src/db/schema.ts +2 -0
- package/src/electric-agents-types.ts +10 -7
- package/src/entity-bridge-manager.ts +1 -1
- package/src/entity-manager.ts +223 -41
- package/src/entity-registry.ts +29 -0
- package/src/entrypoint-lib.ts +0 -9
- package/src/host.ts +4 -0
- package/src/index.ts +2 -1
- package/src/principal.ts +124 -0
- package/src/routing/context.ts +2 -2
- package/src/routing/dispatch-policy.ts +10 -10
- package/src/routing/entities-router.ts +180 -7
- package/src/routing/hooks.ts +1 -1
- package/src/routing/runners-router.ts +10 -18
- package/src/runtime.ts +4 -1
- package/src/scheduler.ts +2 -0
- package/src/server.ts +59 -7
- package/src/dev-asserted-auth.ts +0 -46
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
import { routeBody, withSchema } from './schema.js'
|
|
14
14
|
import { subscriptionIdForDispatchTarget } from './dispatch-policy.js'
|
|
15
15
|
import { withLeadingSlash } from './tenant-stream-paths.js'
|
|
16
|
+
import { principalFromCreatedBy } from '../principal.js'
|
|
16
17
|
import type { JsonRouteRequest } from './schema.js'
|
|
17
18
|
import type { RouterType } from 'itty-router'
|
|
18
19
|
import type { TenantContext } from './context.js'
|
|
@@ -104,7 +105,7 @@ async function registerRunner(
|
|
|
104
105
|
ctx: TenantContext
|
|
105
106
|
): Promise<Response> {
|
|
106
107
|
const parsed = routeBody<RegisterRunnerBody>(request)
|
|
107
|
-
const ownerUserId = parsed.owner_user_id ?? ctx.
|
|
108
|
+
const ownerUserId = parsed.owner_user_id ?? ctx.principal?.key
|
|
108
109
|
if (!ownerUserId) {
|
|
109
110
|
throw new ElectricAgentsError(
|
|
110
111
|
ErrCodeInvalidRequest,
|
|
@@ -112,7 +113,7 @@ async function registerRunner(
|
|
|
112
113
|
400
|
|
113
114
|
)
|
|
114
115
|
}
|
|
115
|
-
if (ctx.
|
|
116
|
+
if (ctx.principal && ownerUserId !== ctx.principal.key) {
|
|
116
117
|
throw new ElectricAgentsError(
|
|
117
118
|
ErrCodeUnauthorized,
|
|
118
119
|
`owner_user_id must match the authenticated user`,
|
|
@@ -139,11 +140,7 @@ async function listRunners(
|
|
|
139
140
|
ctx: TenantContext
|
|
140
141
|
): Promise<Response> {
|
|
141
142
|
const requestedOwner = firstQueryValue(request.query.owner_user_id)
|
|
142
|
-
if (
|
|
143
|
-
ctx.authenticatedUser &&
|
|
144
|
-
requestedOwner &&
|
|
145
|
-
requestedOwner !== ctx.authenticatedUser.userId
|
|
146
|
-
) {
|
|
143
|
+
if (ctx.principal && requestedOwner && requestedOwner !== ctx.principal.key) {
|
|
147
144
|
throw new ElectricAgentsError(
|
|
148
145
|
ErrCodeUnauthorized,
|
|
149
146
|
`owner_user_id must match the authenticated user`,
|
|
@@ -151,7 +148,7 @@ async function listRunners(
|
|
|
151
148
|
)
|
|
152
149
|
}
|
|
153
150
|
const runners = await ctx.entityManager.registry.listRunners({
|
|
154
|
-
ownerUserId: ctx.
|
|
151
|
+
ownerUserId: ctx.principal?.key ?? requestedOwner,
|
|
155
152
|
})
|
|
156
153
|
return json(runners)
|
|
157
154
|
}
|
|
@@ -224,15 +221,8 @@ async function claimWake(
|
|
|
224
221
|
ctx: TenantContext
|
|
225
222
|
): Promise<Response> {
|
|
226
223
|
const runnerId = routeParam(request, `id`)
|
|
227
|
-
if (!ctx.authenticatedUser) {
|
|
228
|
-
throw new ElectricAgentsError(
|
|
229
|
-
ErrCodeUnauthorized,
|
|
230
|
-
`Authentication is required to claim runner work`,
|
|
231
|
-
401
|
|
232
|
-
)
|
|
233
|
-
}
|
|
234
224
|
const runner = await requireRunner(ctx, runnerId)
|
|
235
|
-
if (runner.owner_user_id !== ctx.
|
|
225
|
+
if (ctx.principal && runner.owner_user_id !== ctx.principal.key) {
|
|
236
226
|
throw new ElectricAgentsError(
|
|
237
227
|
ErrCodeUnauthorized,
|
|
238
228
|
`Runner claim requires the authenticated owner`,
|
|
@@ -308,8 +298,8 @@ function assertRunnerOwnerIfAuthenticated(
|
|
|
308
298
|
ctx: TenantContext,
|
|
309
299
|
ownerUserId: string
|
|
310
300
|
): void {
|
|
311
|
-
if (!ctx.
|
|
312
|
-
if (ownerUserId === ctx.
|
|
301
|
+
if (!ctx.principal) return
|
|
302
|
+
if (ownerUserId === ctx.principal.key) return
|
|
313
303
|
throw new ElectricAgentsError(
|
|
314
304
|
ErrCodeUnauthorized,
|
|
315
305
|
`Runner access requires the authenticated owner`,
|
|
@@ -411,6 +401,8 @@ async function notificationFromClaim(
|
|
|
411
401
|
streams: entity.streams,
|
|
412
402
|
tags: entity.tags,
|
|
413
403
|
spawnArgs: entity.spawn_args,
|
|
404
|
+
createdBy: entity.created_by,
|
|
414
405
|
},
|
|
406
|
+
principal: principalFromCreatedBy(entity.created_by),
|
|
415
407
|
}
|
|
416
408
|
}
|
package/src/runtime.ts
CHANGED
|
@@ -435,6 +435,8 @@ export class ElectricAgentsTenantRuntime {
|
|
|
435
435
|
const fireAtRaw = value.fireAt
|
|
436
436
|
const producerId = value.producerId
|
|
437
437
|
const targetUrl = value.targetUrl
|
|
438
|
+
const senderUrl =
|
|
439
|
+
typeof value.senderUrl === `string` ? value.senderUrl : ownerEntityUrl
|
|
438
440
|
if (
|
|
439
441
|
typeof fireAtRaw !== `string` ||
|
|
440
442
|
typeof producerId !== `string` ||
|
|
@@ -459,7 +461,7 @@ export class ElectricAgentsTenantRuntime {
|
|
|
459
461
|
manifestKey,
|
|
460
462
|
{
|
|
461
463
|
entityUrl: targetUrl,
|
|
462
|
-
from:
|
|
464
|
+
from: senderUrl,
|
|
463
465
|
payload: value.payload,
|
|
464
466
|
key: `scheduled-${producerId}`,
|
|
465
467
|
type:
|
|
@@ -474,6 +476,7 @@ export class ElectricAgentsTenantRuntime {
|
|
|
474
476
|
kind: `schedule`,
|
|
475
477
|
scheduleType: `future_send`,
|
|
476
478
|
targetUrl,
|
|
479
|
+
senderUrl,
|
|
477
480
|
fireAt: fireAt.toISOString(),
|
|
478
481
|
producerId,
|
|
479
482
|
status: `pending`,
|
package/src/scheduler.ts
CHANGED
package/src/server.ts
CHANGED
|
@@ -12,6 +12,13 @@ import { ossServerRouter } from './routing/oss-server-router.js'
|
|
|
12
12
|
import { startStandaloneAgentsRuntime } from './standalone-runtime.js'
|
|
13
13
|
import { StreamClient, durableStreamsServiceUrl } from './stream-client.js'
|
|
14
14
|
import { DEFAULT_TENANT_ID } from './tenant.js'
|
|
15
|
+
import { getDevPrincipal, getPrincipalFromRequest } from './principal.js'
|
|
16
|
+
import { apiError } from './electric-agents-http.js'
|
|
17
|
+
import {
|
|
18
|
+
ErrCodeInvalidRequest,
|
|
19
|
+
ErrCodeUnauthorized,
|
|
20
|
+
} from './electric-agents-types.js'
|
|
21
|
+
import { ElectricAgentsError } from './entity-manager.js'
|
|
15
22
|
import { serverLog } from './utils/log.js'
|
|
16
23
|
import type { DrizzleDB, PgClient } from './db/index.js'
|
|
17
24
|
import type { Server } from 'node:http'
|
|
@@ -22,7 +29,7 @@ import type {
|
|
|
22
29
|
EntityRegistry,
|
|
23
30
|
RuntimeHandler,
|
|
24
31
|
} from '@electric-ax/agents-runtime'
|
|
25
|
-
import type {
|
|
32
|
+
import type { Principal } from './principal.js'
|
|
26
33
|
import type { EntityBridgeCoordinator } from './entity-bridge-manager.js'
|
|
27
34
|
import type { DurableStreamsRoutingAdapter } from './routing/durable-streams-routing-adapter.js'
|
|
28
35
|
import type { OssServerContext } from './routing/oss-server-router.js'
|
|
@@ -46,7 +53,10 @@ export interface ElectricAgentsServerOptions {
|
|
|
46
53
|
postgresUrl: string
|
|
47
54
|
electricUrl?: string
|
|
48
55
|
electricSecret?: string
|
|
49
|
-
authenticateRequest?:
|
|
56
|
+
authenticateRequest?: (
|
|
57
|
+
request: Request
|
|
58
|
+
) => Promise<Principal | null> | Principal | null
|
|
59
|
+
allowDevPrincipalFallback?: boolean
|
|
50
60
|
/**
|
|
51
61
|
* Disabled by default. When set to a positive interval, periodically
|
|
52
62
|
* recovers expired dispatch claims and stale outstanding wakes.
|
|
@@ -207,6 +217,7 @@ export class ElectricAgentsServer {
|
|
|
207
217
|
})
|
|
208
218
|
this.electricAgentsManager = this.standaloneRuntime.manager
|
|
209
219
|
this.entityBridgeManager = this.standaloneRuntime.entityBridgeManager
|
|
220
|
+
await this.electricAgentsManager.ensurePrincipalEntityType()
|
|
210
221
|
|
|
211
222
|
const serverAdapter = createServerAdapter((request) =>
|
|
212
223
|
this.handleRequest(request)
|
|
@@ -323,9 +334,27 @@ export class ElectricAgentsServer {
|
|
|
323
334
|
return new Response(null, { status: 503 })
|
|
324
335
|
}
|
|
325
336
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
337
|
+
try {
|
|
338
|
+
return await ossServerRouter.fetch(
|
|
339
|
+
request as Parameters<typeof ossServerRouter.fetch>[0],
|
|
340
|
+
await this.buildTenantContext(request)
|
|
341
|
+
)
|
|
342
|
+
} catch (error) {
|
|
343
|
+
if (error instanceof ElectricAgentsError) {
|
|
344
|
+
return apiError(error.status, error.code, error.message, error.details)
|
|
345
|
+
}
|
|
346
|
+
throw error
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
private allowDevPrincipalFallback(): boolean {
|
|
351
|
+
if (this.options.allowDevPrincipalFallback !== undefined) {
|
|
352
|
+
return this.options.allowDevPrincipalFallback
|
|
353
|
+
}
|
|
354
|
+
return (
|
|
355
|
+
process.env.ELECTRIC_INSECURE === `true` ||
|
|
356
|
+
process.env.NODE_ENV !== `production` ||
|
|
357
|
+
Boolean(this.options.durableStreamsServer)
|
|
329
358
|
)
|
|
330
359
|
}
|
|
331
360
|
|
|
@@ -343,10 +372,33 @@ export class ElectricAgentsServer {
|
|
|
343
372
|
throw new Error(`agents-server runtime is not started`)
|
|
344
373
|
}
|
|
345
374
|
|
|
375
|
+
let principal: Principal | null
|
|
376
|
+
try {
|
|
377
|
+
principal =
|
|
378
|
+
(await this.options.authenticateRequest?.(request)) ??
|
|
379
|
+
getPrincipalFromRequest(request)
|
|
380
|
+
} catch (error) {
|
|
381
|
+
throw new ElectricAgentsError(
|
|
382
|
+
ErrCodeInvalidRequest,
|
|
383
|
+
error instanceof Error ? error.message : `Invalid principal`,
|
|
384
|
+
400
|
|
385
|
+
)
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
if (!principal && this.allowDevPrincipalFallback()) {
|
|
389
|
+
principal = getDevPrincipal()
|
|
390
|
+
}
|
|
391
|
+
if (!principal) {
|
|
392
|
+
throw new ElectricAgentsError(
|
|
393
|
+
ErrCodeUnauthorized,
|
|
394
|
+
`Missing Electric-Principal`,
|
|
395
|
+
401
|
|
396
|
+
)
|
|
397
|
+
}
|
|
398
|
+
|
|
346
399
|
return {
|
|
347
400
|
service: this.tenantId,
|
|
348
|
-
|
|
349
|
-
(await this.options.authenticateRequest?.(request)) ?? undefined,
|
|
401
|
+
principal,
|
|
350
402
|
publicUrl: this.publicUrl,
|
|
351
403
|
localUrl: this._url,
|
|
352
404
|
durableStreamsUrl: this.options.durableStreamsUrl,
|
package/src/dev-asserted-auth.ts
DELETED
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
AuthenticateRequest,
|
|
3
|
-
AuthenticatedRequestUser,
|
|
4
|
-
} from './electric-agents-types.js'
|
|
5
|
-
|
|
6
|
-
export interface DevAssertedAuthOptions {
|
|
7
|
-
enabled?: boolean
|
|
8
|
-
defaultEmail?: string
|
|
9
|
-
defaultName?: string
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export const DEV_ASSERTED_EMAIL_HEADER = `x-electric-asserted-email`
|
|
13
|
-
export const DEV_ASSERTED_NAME_HEADER = `x-electric-asserted-name`
|
|
14
|
-
|
|
15
|
-
function clean(value: string | undefined | null): string | undefined {
|
|
16
|
-
const trimmed = value?.trim()
|
|
17
|
-
return trimmed || undefined
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export function createDevAssertedAuthenticateRequest(
|
|
21
|
-
options: DevAssertedAuthOptions
|
|
22
|
-
): AuthenticateRequest | undefined {
|
|
23
|
-
if (!options.enabled) return undefined
|
|
24
|
-
|
|
25
|
-
return (request): AuthenticatedRequestUser | null => {
|
|
26
|
-
const email =
|
|
27
|
-
clean(request.headers.get(DEV_ASSERTED_EMAIL_HEADER)) ??
|
|
28
|
-
clean(options.defaultEmail)
|
|
29
|
-
const name =
|
|
30
|
-
clean(request.headers.get(DEV_ASSERTED_NAME_HEADER)) ??
|
|
31
|
-
clean(options.defaultName)
|
|
32
|
-
const userId = email ?? name
|
|
33
|
-
if (!userId) return null
|
|
34
|
-
return { userId, email, name }
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export function devAssertedAuthOptionsFromEnv(
|
|
39
|
-
env: Record<string, string | undefined> = process.env
|
|
40
|
-
): DevAssertedAuthOptions {
|
|
41
|
-
return {
|
|
42
|
-
enabled: env.ELECTRIC_AGENTS_DEV_ASSERTED_AUTH === `1`,
|
|
43
|
-
defaultEmail: env.ELECTRIC_ASSERTED_AUTH_EMAIL,
|
|
44
|
-
defaultName: env.ELECTRIC_ASSERTED_AUTH_NAME,
|
|
45
|
-
}
|
|
46
|
-
}
|