@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.
@@ -415,6 +415,30 @@ export class PostgresRegistry {
415
415
  })
416
416
  }
417
417
 
418
+ async ensureEntityType(
419
+ et: ElectricAgentsEntityType
420
+ ): Promise<ElectricAgentsEntityType> {
421
+ const existing = await this.getEntityType(et.name)
422
+ if (existing) return existing
423
+ await this.db
424
+ .insert(entityTypes)
425
+ .values({
426
+ tenantId: this.tenantId,
427
+ name: et.name,
428
+ description: et.description,
429
+ creationSchema: et.creation_schema ?? null,
430
+ inboxSchemas: et.inbox_schemas ?? null,
431
+ stateSchemas: et.state_schemas ?? null,
432
+ serveEndpoint: et.serve_endpoint ?? null,
433
+ defaultDispatchPolicy: et.default_dispatch_policy ?? null,
434
+ revision: et.revision,
435
+ createdAt: et.created_at,
436
+ updatedAt: et.updated_at,
437
+ })
438
+ .onConflictDoNothing()
439
+ return (await this.getEntityType(et.name))!
440
+ }
441
+
418
442
  async getEntityType(name: string): Promise<ElectricAgentsEntityType | null> {
419
443
  const rows = await this.db
420
444
  .select()
@@ -471,6 +495,7 @@ export class PostgresRegistry {
471
495
  tagsIndex: buildTagsIndex(entity.tags),
472
496
  spawnArgs: entity.spawn_args ?? {},
473
497
  parent: entity.parent ?? null,
498
+ createdBy: entity.created_by ?? null,
474
499
  typeRevision: entity.type_revision ?? null,
475
500
  inboxSchemas: entity.inbox_schemas ?? null,
476
501
  stateSchemas: entity.state_schemas ?? null,
@@ -544,11 +569,14 @@ export class PostgresRegistry {
544
569
  parent?: string
545
570
  limit?: number
546
571
  offset?: number
572
+ created_by?: string
547
573
  }): Promise<{ entities: Array<ElectricAgentsEntity>; total: number }> {
548
574
  const conditions = [eq(entities.tenantId, this.tenantId)]
549
575
  if (filter?.type) conditions.push(eq(entities.type, filter.type))
550
576
  if (filter?.status) conditions.push(eq(entities.status, filter.status))
551
577
  if (filter?.parent) conditions.push(eq(entities.parent, filter.parent))
578
+ if (filter?.created_by)
579
+ conditions.push(eq(entities.createdBy, filter.created_by))
552
580
 
553
581
  const whereClause = and(...conditions)
554
582
 
@@ -1054,6 +1082,7 @@ export class PostgresRegistry {
1054
1082
  tags: (row.tags as EntityTags | null | undefined) ?? {},
1055
1083
  spawn_args: row.spawnArgs as Record<string, unknown> | undefined,
1056
1084
  parent: row.parent ?? undefined,
1085
+ created_by: row.createdBy ?? undefined,
1057
1086
  type_revision: row.typeRevision ?? undefined,
1058
1087
  inbox_schemas: row.inboxSchemas as
1059
1088
  | Record<string, Record<string, unknown>>
@@ -1,8 +1,4 @@
1
1
  import { DurableStreamTestServer } from '@durable-streams/server'
2
- import {
3
- createDevAssertedAuthenticateRequest,
4
- devAssertedAuthOptionsFromEnv,
5
- } from './dev-asserted-auth.js'
6
2
  import { ElectricAgentsServer } from './server.js'
7
3
  import type { ElectricAgentsServerOptions } from './server.js'
8
4
 
@@ -144,12 +140,7 @@ export function resolveElectricAgentsEntrypointOptions(
144
140
  ])
145
141
  const electricSecret = readEnv(env, [`ELECTRIC_AGENTS_ELECTRIC_SECRET`])
146
142
  const baseUrl = readEnv(env, [`ELECTRIC_AGENTS_BASE_URL`, `BASE_URL`])
147
- const authenticateRequest = createDevAssertedAuthenticateRequest(
148
- devAssertedAuthOptionsFromEnv(env)
149
- )
150
-
151
143
  return {
152
- ...(authenticateRequest ? { authenticateRequest } : {}),
153
144
  service: readEnv(env, [`ELECTRIC_AGENTS_SERVICE`, `SERVICE`]),
154
145
  tenantId: readEnv(env, [`ELECTRIC_AGENTS_TENANT_ID`, `TENANT_ID`]),
155
146
  baseUrl: baseUrl ? validateUrl(`base URL`, baseUrl) : undefined,
package/src/host.ts CHANGED
@@ -271,6 +271,8 @@ export class AgentsHost {
271
271
  private async startTenantRuntime(
272
272
  runtime: AgentsHostTenantRuntime
273
273
  ): Promise<void> {
274
+ await runtime.manager.ensurePrincipalEntityType()
275
+
274
276
  if (this.rehydrateTenantOnStart) {
275
277
  await runtime.rehydrateCronSchedules()
276
278
  }
@@ -303,6 +305,8 @@ export class AgentsHost {
303
305
  entityBridgeManager: this.entityProjector.forTenant(serviceId, registry),
304
306
  })
305
307
 
308
+ await runtime.manager.ensurePrincipalEntityType()
309
+
306
310
  return runtime
307
311
  }
308
312
 
package/src/index.ts CHANGED
@@ -16,7 +16,6 @@ export type {
16
16
  SubscriptionStreamInfo,
17
17
  } from './stream-client.js'
18
18
  export type {
19
- AuthenticatedRequestUser,
20
19
  AuthenticateRequest,
21
20
  ConsumerClaim,
22
21
  DispatchPolicy,
@@ -26,6 +25,7 @@ export type {
26
25
  EntityDispatchState,
27
26
  PublicWakeNotification,
28
27
  RegisterRunnerRequest,
28
+ RequestPrincipal,
29
29
  RunnerAdminStatus,
30
30
  RunnerHeartbeatRequest,
31
31
  RunnerKind,
@@ -33,6 +33,7 @@ export type {
33
33
  SourceStreamOffset,
34
34
  WakeNotificationRow,
35
35
  } from './electric-agents-types.js'
36
+ export type { Principal, PrincipalKind } from './principal.js'
36
37
  export { globalRouter } from './routing/global-router.js'
37
38
  export type { GlobalRoutes } from './routing/global-router.js'
38
39
  export type { TenantContext } from './routing/context.js'
@@ -0,0 +1,124 @@
1
+ import { Type } from '@sinclair/typebox'
2
+
3
+ export type PrincipalKind = `user` | `agent` | `service` | `system`
4
+
5
+ export interface Principal {
6
+ kind: PrincipalKind
7
+ id: string
8
+ key: string
9
+ url: string
10
+ }
11
+
12
+ export const ELECTRIC_PRINCIPAL_HEADER = `electric-principal`
13
+
14
+ const PRINCIPAL_KINDS = new Set<PrincipalKind>([
15
+ `user`,
16
+ `agent`,
17
+ `service`,
18
+ `system`,
19
+ ])
20
+
21
+ export function parsePrincipalKey(input: string): Principal {
22
+ const colon = input.indexOf(`:`)
23
+ if (colon <= 0) throw new Error(`Invalid principal key`)
24
+ const kind = input.slice(0, colon) as PrincipalKind
25
+ const id = input.slice(colon + 1)
26
+ if (!PRINCIPAL_KINDS.has(kind)) throw new Error(`Invalid principal kind`)
27
+ if (!id || id.includes(`/`)) throw new Error(`Invalid principal id`)
28
+ const key = `${kind}:${id}`
29
+ return { kind, id, key, url: `/principal/${encodeURIComponent(key)}` }
30
+ }
31
+
32
+ export function principalUrl(key: string): string {
33
+ return parsePrincipalKey(key).url
34
+ }
35
+
36
+ export function principalKeyFromUrl(url: string): string | null {
37
+ if (!url.startsWith(`/principal/`)) return null
38
+ const segment = url.slice(`/principal/`.length)
39
+ if (!segment || segment.includes(`/`)) return null
40
+ try {
41
+ const key = decodeURIComponent(segment)
42
+ // Principal URLs produced by parsePrincipalKey/principalUrl are canonical
43
+ // encoded single path segments, but accept legacy unencoded single-segment
44
+ // URLs here so callers can canonicalize them via parsePrincipalKey(key).url.
45
+ return parsePrincipalKey(key).key
46
+ } catch {
47
+ return null
48
+ }
49
+ }
50
+
51
+ export function getPrincipalFromRequest(request: Request): Principal | null {
52
+ const value = request.headers.get(ELECTRIC_PRINCIPAL_HEADER)
53
+ return value ? parsePrincipalKey(value) : null
54
+ }
55
+
56
+ export function getDevPrincipal(): Principal {
57
+ return parsePrincipalKey(`system:dev-local`)
58
+ }
59
+
60
+ const BUILT_IN_SYSTEM_PRINCIPAL_IDS = new Set([
61
+ `framework`,
62
+ `auth-sync`,
63
+ `dev-local`,
64
+ ])
65
+
66
+ export function isBuiltInSystemPrincipalUrl(url: string | undefined): boolean {
67
+ if (!url?.startsWith(`/principal/`)) return false
68
+ try {
69
+ const key = principalKeyFromUrl(url)
70
+ if (!key) return false
71
+ const principal = parsePrincipalKey(key)
72
+ return (
73
+ principal.kind === `system` &&
74
+ BUILT_IN_SYSTEM_PRINCIPAL_IDS.has(principal.id)
75
+ )
76
+ } catch {
77
+ return false
78
+ }
79
+ }
80
+
81
+ export function principalFromCreatedBy(
82
+ createdBy: string | undefined
83
+ ):
84
+ | { url: string; key?: string | null; kind?: string; id?: string }
85
+ | undefined {
86
+ if (!createdBy) return undefined
87
+ const key = principalKeyFromUrl(createdBy)
88
+ if (!key) return { url: createdBy, key: null }
89
+ const principal = parsePrincipalKey(key)
90
+ return {
91
+ url: principal.url,
92
+ key: principal.key,
93
+ kind: principal.kind,
94
+ id: principal.id,
95
+ }
96
+ }
97
+
98
+ export const principalIdentityStateSchema = Type.Object(
99
+ {
100
+ kind: Type.Union([
101
+ Type.Literal(`user`),
102
+ Type.Literal(`agent`),
103
+ Type.Literal(`service`),
104
+ Type.Literal(`system`),
105
+ ]),
106
+ id: Type.String(),
107
+ key: Type.String(),
108
+ url: Type.String(),
109
+ updated_at: Type.String(),
110
+ display_name: Type.Optional(Type.String()),
111
+ email: Type.Optional(Type.String()),
112
+ avatar_url: Type.Optional(Type.String()),
113
+ auth_provider: Type.Optional(Type.String()),
114
+ auth_subject: Type.Optional(Type.String()),
115
+ claims: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
116
+ created_at: Type.Optional(Type.String()),
117
+ },
118
+ { additionalProperties: false }
119
+ )
120
+
121
+ export const principalUpdateIdentityMessageSchema = Type.Object(
122
+ { identity: principalIdentityStateSchema },
123
+ { additionalProperties: false }
124
+ )
@@ -5,7 +5,7 @@ import type { EntityManager } from '../entity-manager.js'
5
5
  import type { ElectricAgentsTenantRuntime } from '../runtime.js'
6
6
  import type { StreamClient } from '../stream-client.js'
7
7
  import type { DurableStreamsRoutingAdapter } from './durable-streams-routing-adapter.js'
8
- import type { AuthenticatedRequestUser } from '../electric-agents-types.js'
8
+ import type { Principal } from '../principal.js'
9
9
  import type { DurableStreamsBearerProvider } from '../stream-client.js'
10
10
 
11
11
  /**
@@ -16,7 +16,7 @@ import type { DurableStreamsBearerProvider } from '../stream-client.js'
16
16
  */
17
17
  export interface TenantContext {
18
18
  service: string
19
- authenticatedUser?: AuthenticatedRequestUser
19
+ principal: Principal
20
20
  publicUrl: string
21
21
  localUrl?: string
22
22
  durableStreamsUrl: string
@@ -9,6 +9,7 @@ import {
9
9
  } from '../electric-agents-types.js'
10
10
  import { runnerWakeStream } from '../entity-registry.js'
11
11
  import { rewriteLoopbackWebhookUrl } from '../utils/webhook-url.js'
12
+ import { serverLog } from '../utils/log.js'
12
13
  import type {
13
14
  DispatchPolicy,
14
15
  DispatchTarget,
@@ -81,7 +82,7 @@ export async function backfillEntityDispatchPolicy(
81
82
  )
82
83
  }
83
84
 
84
- function applyTypeDefaultSubscriptionScope(
85
+ export function applyTypeDefaultSubscriptionScope(
85
86
  policy: DispatchPolicy,
86
87
  typeDefault: DispatchPolicy | undefined
87
88
  ): DispatchPolicy {
@@ -123,14 +124,7 @@ export async function assertDispatchPolicyAllowed(
123
124
  404
124
125
  )
125
126
  }
126
- if (!ctx.authenticatedUser) {
127
- throw new ElectricAgentsError(
128
- ErrCodeUnauthorized,
129
- `Authentication is required for runner-targeted dispatch`,
130
- 401
131
- )
132
- }
133
- if (runner.owner_user_id !== ctx.authenticatedUser.userId) {
127
+ if (ctx.principal && runner.owner_user_id !== ctx.principal.key) {
134
128
  throw new ElectricAgentsError(
135
129
  ErrCodeUnauthorized,
136
130
  `Runner dispatch requires the authenticated owner`,
@@ -168,7 +162,13 @@ export async function unlinkEntityDispatchSubscription(
168
162
  )
169
163
  await ctx.streamClient
170
164
  .removeSubscriptionStream(subscriptionId, entity.streams.main)
171
- .catch(() => {})
165
+ .catch((err) => {
166
+ serverLog.warn(
167
+ `[dispatch-policy] failed to remove stream from subscription`,
168
+ { subscriptionId, stream: entity.streams.main },
169
+ err
170
+ )
171
+ })
172
172
  }
173
173
 
174
174
  async function linkStreamToTargetSubscription(
@@ -5,10 +5,12 @@
5
5
  import { Type, type Static } from '@sinclair/typebox'
6
6
  import { Router, json, status } from 'itty-router'
7
7
  import { apiError } from '../electric-agents-http.js'
8
+ import { parsePrincipalKey, principalUrl } from '../principal.js'
8
9
  import { dispatchPolicySchema } from '../dispatch-policy-schema.js'
9
10
  import {
10
11
  ErrCodeNotFound,
11
12
  ErrCodeUnknownEntityType,
13
+ ErrCodeInvalidRequest,
12
14
  toPublicEntity,
13
15
  } from '../electric-agents-types.js'
14
16
  import {
@@ -86,11 +88,40 @@ const spawnBodySchema = Type.Object({
86
88
  })
87
89
 
88
90
  const sendBodySchema = Type.Object({
89
- from: Type.Optional(Type.String()),
90
91
  payload: Type.Optional(Type.Unknown()),
91
92
  key: Type.Optional(Type.String()),
92
93
  type: Type.Optional(Type.String()),
94
+ mode: Type.Optional(
95
+ Type.Union([
96
+ Type.Literal(`immediate`),
97
+ Type.Literal(`queued`),
98
+ Type.Literal(`paused`),
99
+ Type.Literal(`steer`),
100
+ ])
101
+ ),
102
+ position: Type.Optional(Type.String()),
93
103
  afterMs: Type.Optional(Type.Number()),
104
+ from: Type.Optional(Type.String()),
105
+ })
106
+
107
+ const inboxMessageBodySchema = Type.Object({
108
+ payload: Type.Optional(Type.Unknown()),
109
+ position: Type.Optional(Type.String()),
110
+ mode: Type.Optional(
111
+ Type.Union([
112
+ Type.Literal(`immediate`),
113
+ Type.Literal(`queued`),
114
+ Type.Literal(`paused`),
115
+ Type.Literal(`steer`),
116
+ ])
117
+ ),
118
+ status: Type.Optional(
119
+ Type.Union([
120
+ Type.Literal(`pending`),
121
+ Type.Literal(`processed`),
122
+ Type.Literal(`cancelled`),
123
+ ])
124
+ ),
94
125
  })
95
126
 
96
127
  const forkBodySchema = Type.Object({
@@ -116,8 +147,8 @@ const scheduleBodySchema = Type.Union([
116
147
  payload: Type.Unknown(),
117
148
  targetUrl: Type.Optional(Type.String()),
118
149
  fireAt: Type.String(),
119
- from: Type.Optional(Type.String()),
120
150
  messageType: Type.Optional(Type.String()),
151
+ from: Type.Optional(Type.String()),
121
152
  }),
122
153
  ])
123
154
 
@@ -127,6 +158,7 @@ const entitiesRegisterBodySchema = Type.Object({
127
158
 
128
159
  type SpawnBody = Static<typeof spawnBodySchema>
129
160
  type SendBody = Static<typeof sendBodySchema>
161
+ type InboxMessageBody = Static<typeof inboxMessageBodySchema>
130
162
  type ForkBody = Static<typeof forkBodySchema>
131
163
  type SetTagBody = Static<typeof setTagBodySchema>
132
164
  type ScheduleBody = Static<typeof scheduleBodySchema>
@@ -161,6 +193,17 @@ entitiesRouter.post(
161
193
  withSchema(sendBodySchema),
162
194
  sendEntity
163
195
  )
196
+ entitiesRouter.patch(
197
+ `/:type/:instanceId/inbox/:messageKey`,
198
+ withExistingEntity,
199
+ withSchema(inboxMessageBodySchema),
200
+ updateInboxMessage
201
+ )
202
+ entitiesRouter.delete(
203
+ `/:type/:instanceId/inbox/:messageKey`,
204
+ withExistingEntity,
205
+ deleteInboxMessage
206
+ )
164
207
  entitiesRouter.post(
165
208
  `/:type/:instanceId/fork`,
166
209
  withExistingEntity,
@@ -198,6 +241,13 @@ function entityUrlFromSegments(
198
241
  if (type.startsWith(`_`) || type.includes(`*`) || instanceId.includes(`*`)) {
199
242
  return null
200
243
  }
244
+ if (type === `principal`) {
245
+ try {
246
+ return principalUrl(decodeURIComponent(instanceId))
247
+ } catch {
248
+ return null
249
+ }
250
+ }
201
251
  return `/${type}/${instanceId}`
202
252
  }
203
253
 
@@ -216,6 +266,20 @@ function requireExistingEntityRoute(
216
266
  return request.entityRoute
217
267
  }
218
268
 
269
+ function rejectPrincipalEntityMutation(
270
+ request: AgentsRouteRequest,
271
+ action: string
272
+ ): Response | undefined {
273
+ const { entity } = requireExistingEntityRoute(request)
274
+ if (entity.type !== `principal`) return undefined
275
+
276
+ return apiError(
277
+ 400,
278
+ ErrCodeInvalidRequest,
279
+ `Principal entities are built in and cannot be ${action}`
280
+ )
281
+ }
282
+
219
283
  async function withExistingEntity(
220
284
  request: AgentsRouteRequest,
221
285
  ctx: TenantContext
@@ -231,6 +295,21 @@ async function withExistingEntity(
231
295
  const entityType = await ctx.entityManager.registry.getEntityType(
232
296
  request.params.type
233
297
  )
298
+ if (request.params.type === `principal`) {
299
+ try {
300
+ const materialized = await ctx.entityManager.ensurePrincipal(
301
+ parsePrincipalKey(decodeURIComponent(request.params.instanceId))
302
+ )
303
+ request.entityRoute = { entityUrl, entity: materialized }
304
+ return undefined
305
+ } catch (error) {
306
+ return apiError(
307
+ 400,
308
+ ErrCodeInvalidRequest,
309
+ error instanceof Error ? error.message : `Invalid principal`
310
+ )
311
+ }
312
+ }
234
313
  if (entityType) {
235
314
  return apiError(404, ErrCodeNotFound, `Entity not found at ${entityUrl}`)
236
315
  }
@@ -253,6 +332,14 @@ async function withSpawnableEntityType(
253
332
  return undefined
254
333
  }
255
334
 
335
+ if (request.params.type === `principal`) {
336
+ return apiError(
337
+ 400,
338
+ ErrCodeInvalidRequest,
339
+ `Principal entities are built in and cannot be spawned directly`
340
+ )
341
+ }
342
+
256
343
  const entityType = await ctx.entityManager.registry.getEntityType(
257
344
  request.params.type
258
345
  )
@@ -275,6 +362,7 @@ async function listEntities(
275
362
  type: firstQueryValue(query.type),
276
363
  status: firstQueryValue(query.status),
277
364
  parent: firstQueryValue(query.parent),
365
+ created_by: firstQueryValue(query.created_by),
278
366
  })
279
367
  return json(entities.map((entity) => toPublicEntity(entity)))
280
368
  }
@@ -294,6 +382,12 @@ async function upsertSchedule(
294
382
  request: AgentsRouteRequest,
295
383
  ctx: TenantContext
296
384
  ): Promise<Response> {
385
+ const principalMutationError = rejectPrincipalEntityMutation(
386
+ request,
387
+ `scheduled`
388
+ )
389
+ if (principalMutationError) return principalMutationError
390
+
297
391
  const parsed = routeBody<ScheduleBody>(request)
298
392
  const { entityUrl } = requireExistingEntityRoute(request)
299
393
  const scheduleId = decodeURIComponent(request.params.scheduleId)
@@ -311,12 +405,19 @@ async function upsertSchedule(
311
405
  }
312
406
 
313
407
  if (parsed.scheduleType === `future_send`) {
408
+ if (parsed.from !== undefined && parsed.from !== ctx.principal.url) {
409
+ return apiError(
410
+ 400,
411
+ ErrCodeInvalidRequest,
412
+ `Request from must match Electric-Principal`
413
+ )
414
+ }
314
415
  const result = await ctx.entityManager.upsertFutureSendSchedule(entityUrl, {
315
416
  id: scheduleId,
316
417
  payload: parsed.payload,
317
418
  targetUrl: parsed.targetUrl,
318
419
  fireAt: parsed.fireAt,
319
- from: parsed.from,
420
+ senderUrl: ctx.principal.url,
320
421
  messageType: parsed.messageType,
321
422
  })
322
423
  return json(result)
@@ -329,6 +430,12 @@ async function deleteSchedule(
329
430
  request: AgentsRouteRequest,
330
431
  ctx: TenantContext
331
432
  ): Promise<Response> {
433
+ const principalMutationError = rejectPrincipalEntityMutation(
434
+ request,
435
+ `unscheduled`
436
+ )
437
+ if (principalMutationError) return principalMutationError
438
+
332
439
  const { entityUrl } = requireExistingEntityRoute(request)
333
440
  const result = await ctx.entityManager.deleteSchedule(entityUrl, {
334
441
  id: decodeURIComponent(request.params.scheduleId),
@@ -340,6 +447,12 @@ async function setTag(
340
447
  request: AgentsRouteRequest,
341
448
  ctx: TenantContext
342
449
  ): Promise<Response> {
450
+ const principalMutationError = rejectPrincipalEntityMutation(
451
+ request,
452
+ `tagged`
453
+ )
454
+ if (principalMutationError) return principalMutationError
455
+
343
456
  const parsed = routeBody<SetTagBody>(request)
344
457
  const { entityUrl } = requireExistingEntityRoute(request)
345
458
  const token = writeTokenFromRequest(request)
@@ -356,6 +469,12 @@ async function removeTag(
356
469
  request: AgentsRouteRequest,
357
470
  ctx: TenantContext
358
471
  ): Promise<Response> {
472
+ const principalMutationError = rejectPrincipalEntityMutation(
473
+ request,
474
+ `untagged`
475
+ )
476
+ if (principalMutationError) return principalMutationError
477
+
359
478
  const { entityUrl } = requireExistingEntityRoute(request)
360
479
  const token = writeTokenFromRequest(request)
361
480
  const updated = await ctx.entityManager.removeTag(
@@ -370,6 +489,12 @@ async function forkEntity(
370
489
  request: AgentsRouteRequest,
371
490
  ctx: TenantContext
372
491
  ): Promise<Response> {
492
+ const principalMutationError = rejectPrincipalEntityMutation(
493
+ request,
494
+ `forked`
495
+ )
496
+ if (principalMutationError) return principalMutationError
497
+
373
498
  const parsed = routeBody<ForkBody>(request)
374
499
  const { entityUrl, entity } = requireExistingEntityRoute(request)
375
500
  await assertDispatchPolicyAllowed(ctx, entity.dispatch_policy)
@@ -394,6 +519,15 @@ async function sendEntity(
394
519
  ctx: TenantContext
395
520
  ): Promise<Response> {
396
521
  const parsed = routeBody<SendBody>(request)
522
+ const principal = ctx.principal
523
+ if (parsed.from !== undefined && parsed.from !== principal.url) {
524
+ return apiError(
525
+ 400,
526
+ ErrCodeInvalidRequest,
527
+ `Request from must match Electric-Principal`
528
+ )
529
+ }
530
+ await ctx.entityManager.ensurePrincipal(principal)
397
531
  const { entityUrl, entity } = requireExistingEntityRoute(request)
398
532
 
399
533
  if (!entity.dispatch_policy) {
@@ -405,30 +539,62 @@ async function sendEntity(
405
539
  await ctx.entityManager.enqueueDelayedSend(
406
540
  entityUrl,
407
541
  {
408
- from: parsed.from,
542
+ from: principal.url,
409
543
  payload: parsed.payload,
410
544
  key: parsed.key,
411
545
  type: parsed.type,
546
+ mode: parsed.mode,
547
+ position: parsed.position,
412
548
  },
413
549
  new Date(Date.now() + parsed.afterMs)
414
550
  )
415
551
  } else {
416
552
  await ctx.entityManager.send(entityUrl, {
417
- from: parsed.from,
553
+ from: principal.url,
418
554
  payload: parsed.payload,
419
555
  key: parsed.key,
420
556
  type: parsed.type,
557
+ mode: parsed.mode,
558
+ position: parsed.position,
421
559
  })
422
560
  }
423
561
 
424
562
  return status(204)
425
563
  }
426
564
 
565
+ async function updateInboxMessage(
566
+ request: AgentsRouteRequest,
567
+ ctx: TenantContext
568
+ ): Promise<Response> {
569
+ const parsed = routeBody<InboxMessageBody>(request)
570
+ const { entityUrl } = requireExistingEntityRoute(request)
571
+ await ctx.entityManager.updateInboxMessage(
572
+ entityUrl,
573
+ decodeURIComponent(request.params.messageKey),
574
+ parsed
575
+ )
576
+ return status(204)
577
+ }
578
+
579
+ async function deleteInboxMessage(
580
+ request: AgentsRouteRequest,
581
+ ctx: TenantContext
582
+ ): Promise<Response> {
583
+ const { entityUrl } = requireExistingEntityRoute(request)
584
+ await ctx.entityManager.deleteInboxMessage(
585
+ entityUrl,
586
+ decodeURIComponent(request.params.messageKey)
587
+ )
588
+ return status(204)
589
+ }
590
+
427
591
  async function spawnEntity(
428
592
  request: AgentsRouteRequest,
429
593
  ctx: TenantContext
430
594
  ): Promise<Response> {
431
595
  const parsed = routeBody<SpawnBody>(request)
596
+ const principal = ctx.principal
597
+ await ctx.entityManager.ensurePrincipal(principal)
432
598
  const dispatchPolicy = await resolveEffectiveDispatchPolicyForSpawn(
433
599
  ctx,
434
600
  request.params.type,
@@ -446,14 +612,15 @@ async function spawnEntity(
446
612
  dispatch_policy: dispatchPolicy,
447
613
  initialMessage: undefined,
448
614
  wake: parsed.wake,
615
+ created_by: principal.url,
449
616
  })
450
- await linkEntityDispatchSubscription(ctx, entity)
451
617
  if (parsed.initialMessage !== undefined) {
452
618
  await ctx.entityManager.send(entity.url, {
453
- from: parsed.parent ?? `spawn`,
619
+ from: principal.url,
454
620
  payload: parsed.initialMessage,
455
621
  })
456
622
  }
623
+ await linkEntityDispatchSubscription(ctx, entity)
457
624
 
458
625
  return json(
459
626
  { ...toPublicEntity(entity), txid: entity.txid },
@@ -476,6 +643,12 @@ async function killEntity(
476
643
  request: AgentsRouteRequest,
477
644
  ctx: TenantContext
478
645
  ): Promise<Response> {
646
+ const principalMutationError = rejectPrincipalEntityMutation(
647
+ request,
648
+ `killed`
649
+ )
650
+ if (principalMutationError) return principalMutationError
651
+
479
652
  const { entityUrl, entity } = requireExistingEntityRoute(request)
480
653
  await unlinkEntityDispatchSubscription(ctx, entity)
481
654
  const result = await ctx.entityManager.kill(entityUrl)
@@ -80,7 +80,7 @@ export function applyCors(
80
80
  )
81
81
  headers.set(
82
82
  `access-control-allow-headers`,
83
- `content-type, authorization, electric-claim-token, x-electric-asserted-email, x-electric-asserted-name, ngrok-skip-browser-warning`
83
+ `content-type, authorization, electric-claim-token, ngrok-skip-browser-warning`
84
84
  )
85
85
  headers.set(`access-control-expose-headers`, `*`)
86
86
  return new Response(response.body, {