@electric-ax/agents-server 0.4.0 → 0.4.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 +379 -94
- package/dist/index.cjs +489 -196
- package/dist/index.d.cts +286 -244
- package/dist/index.d.ts +286 -244
- package/dist/index.js +489 -196
- 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/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 +179 -6
- 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
package/src/entrypoint-lib.ts
CHANGED
|
@@ -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'
|
package/src/principal.ts
ADDED
|
@@ -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
|
+
)
|
package/src/routing/context.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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,11 +612,12 @@ 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
617
|
await linkEntityDispatchSubscription(ctx, entity)
|
|
451
618
|
if (parsed.initialMessage !== undefined) {
|
|
452
619
|
await ctx.entityManager.send(entity.url, {
|
|
453
|
-
from:
|
|
620
|
+
from: principal.url,
|
|
454
621
|
payload: parsed.initialMessage,
|
|
455
622
|
})
|
|
456
623
|
}
|
|
@@ -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)
|
package/src/routing/hooks.ts
CHANGED
|
@@ -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,
|
|
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, {
|
|
@@ -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
|
}
|