@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.
@@ -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.authenticatedUser?.userId
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.authenticatedUser && ownerUserId !== ctx.authenticatedUser.userId) {
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.authenticatedUser?.userId ?? requestedOwner,
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.authenticatedUser.userId) {
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.authenticatedUser) return
312
- if (ownerUserId === ctx.authenticatedUser.userId) return
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: typeof value.from === `string` ? value.from : ownerEntityUrl,
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
@@ -9,6 +9,8 @@ export interface DelayedSendPayload {
9
9
  payload: unknown
10
10
  key?: string
11
11
  type?: string
12
+ mode?: `immediate` | `queued` | `paused` | `steer`
13
+ position?: string
12
14
  producerId?: string
13
15
  manifest?: {
14
16
  ownerEntityUrl: string
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 { AuthenticateRequest } from './electric-agents-types.js'
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?: 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
- return await ossServerRouter.fetch(
327
- request as Parameters<typeof ossServerRouter.fetch>[0],
328
- await this.buildTenantContext(request)
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
- authenticatedUser:
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,
@@ -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
- }