@electric-ax/agents-server 0.4.15 → 0.4.17
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 +1230 -235
- package/dist/index.cjs +1233 -229
- package/dist/index.d.cts +1319 -318
- package/dist/index.d.ts +1319 -318
- package/dist/index.js +1235 -231
- package/drizzle/0011_entity_permissions.sql +100 -0
- package/drizzle/0012_horton_user_manage_permission.sql +25 -0
- package/drizzle/0013_worker_user_manage_permission.sql +25 -0
- package/drizzle/0014_entity_type_slash_commands.sql +1 -0
- package/drizzle/meta/_journal.json +28 -0
- package/package.json +7 -7
- package/src/db/schema.ts +199 -0
- package/src/electric-agents-types.ts +80 -2
- package/src/entity-bridge-manager.ts +57 -6
- package/src/entity-manager.ts +124 -61
- package/src/entity-projector.ts +76 -17
- package/src/entity-registry.ts +615 -5
- package/src/index.ts +11 -0
- package/src/permissions.ts +239 -0
- package/src/routing/context.ts +2 -0
- package/src/routing/durable-streams-router.ts +125 -4
- package/src/routing/electric-proxy-router.ts +4 -0
- package/src/routing/entities-router.ts +347 -20
- package/src/routing/entity-types-router.ts +267 -15
- package/src/routing/hooks.ts +1 -0
- package/src/routing/observations-router.ts +2 -1
- package/src/runtime.ts +34 -0
- package/src/scheduler.ts +2 -0
- package/src/server.ts +5 -0
- package/src/utils/server-utils.ts +192 -12
- package/src/wake-registry.ts +8 -0
package/dist/entrypoint.js
CHANGED
|
@@ -4,13 +4,13 @@ import { DurableStreamTestServer } from "@durable-streams/server";
|
|
|
4
4
|
import { createServer } from "node:http";
|
|
5
5
|
import { createServerAdapter } from "@whatwg-node/server";
|
|
6
6
|
import { Agent } from "undici";
|
|
7
|
-
import { appendPathToUrl, assertTags, buildEventSourceManifestEntry, buildTagsIndex, createEntityRegistry, createRuntimeHandler, entityStateSchema, eventSourceSubscriptionManifestKey, getCronStreamPath, getCronStreamPathFromSpec, getEntitiesStreamPath, getNextCronFireAt, getSharedStateStreamPath, getWebhookStreamPath, manifestChildKey, manifestSharedStateKey, manifestSourceKey, normalizeTags, parseCronStreamPath, resolveCronScheduleSpec, resolveEventSourceSubscription, sourceRefForTags, verifyWebhookSignature } from "@electric-ax/agents-runtime";
|
|
7
|
+
import { COMPOSER_INPUT_MESSAGE_TYPE, appendPathToUrl, assertTags, buildEventSourceManifestEntry, buildTagsIndex, createEntityRegistry, createRuntimeHandler, entityStateSchema, eventSourceSubscriptionManifestKey, getCronStreamPath, getCronStreamPathFromSpec, getEntitiesStreamPath, getNextCronFireAt, getSharedStateStreamPath, getWebhookStreamPath, hashString, manifestChildKey, manifestSharedStateKey, manifestSourceKey, normalizeTags, parseCronStreamPath, resolveCronScheduleSpec, resolveEventSourceSubscription, sourceRefForTags, validateComposerInputPayload, validateSlashCommandDefinitions, verifyWebhookSignature } from "@electric-ax/agents-runtime";
|
|
8
8
|
import fs, { existsSync } from "node:fs";
|
|
9
9
|
import path, { dirname, resolve } from "node:path";
|
|
10
10
|
import { drizzle } from "drizzle-orm/postgres-js";
|
|
11
11
|
import { migrate } from "drizzle-orm/postgres-js/migrator";
|
|
12
12
|
import postgres from "postgres";
|
|
13
|
-
import { and, desc, eq, lt, ne, sql } from "drizzle-orm";
|
|
13
|
+
import { and, desc, eq, inArray, lt, ne, sql } from "drizzle-orm";
|
|
14
14
|
import { bigint, bigserial, boolean, check, index, integer, jsonb, pgTable, primaryKey, serial, text, timestamp, unique } from "drizzle-orm/pg-core";
|
|
15
15
|
import { AutoRouter, Router, json, status, withParams } from "itty-router";
|
|
16
16
|
import { access, readFile } from "node:fs/promises";
|
|
@@ -41,11 +41,16 @@ __export(schema_exports, {
|
|
|
41
41
|
entities: () => entities,
|
|
42
42
|
entityBridges: () => entityBridges,
|
|
43
43
|
entityDispatchState: () => entityDispatchState,
|
|
44
|
+
entityEffectivePermissions: () => entityEffectivePermissions,
|
|
45
|
+
entityLineage: () => entityLineage,
|
|
44
46
|
entityManifestSources: () => entityManifestSources,
|
|
47
|
+
entityPermissionGrants: () => entityPermissionGrants,
|
|
48
|
+
entityTypePermissionGrants: () => entityTypePermissionGrants,
|
|
45
49
|
entityTypes: () => entityTypes,
|
|
46
50
|
runnerRuntimeDiagnostics: () => runnerRuntimeDiagnostics,
|
|
47
51
|
runners: () => runners,
|
|
48
52
|
scheduledTasks: () => scheduledTasks,
|
|
53
|
+
sharedStateLinks: () => sharedStateLinks,
|
|
49
54
|
subscriptionWebhooks: () => subscriptionWebhooks,
|
|
50
55
|
tagStreamOutbox: () => tagStreamOutbox,
|
|
51
56
|
users: () => users,
|
|
@@ -59,6 +64,7 @@ const entityTypes = pgTable(`entity_types`, {
|
|
|
59
64
|
creationSchema: jsonb(`creation_schema`),
|
|
60
65
|
inboxSchemas: jsonb(`inbox_schemas`),
|
|
61
66
|
stateSchemas: jsonb(`state_schemas`),
|
|
67
|
+
slashCommands: jsonb(`slash_commands`),
|
|
62
68
|
serveEndpoint: text(`serve_endpoint`),
|
|
63
69
|
defaultDispatchPolicy: jsonb(`default_dispatch_policy`),
|
|
64
70
|
revision: integer(`revision`).notNull().default(1),
|
|
@@ -93,6 +99,94 @@ const entities = pgTable(`entities`, {
|
|
|
93
99
|
index(`entities_tags_index_gin`).using(`gin`, table.tagsIndex),
|
|
94
100
|
check(`chk_entities_status`, sql`${table.status} IN ('spawning', 'running', 'idle', 'paused', 'stopping', 'stopped', 'killed')`)
|
|
95
101
|
]);
|
|
102
|
+
const entityTypePermissionGrants = pgTable(`entity_type_permission_grants`, {
|
|
103
|
+
id: bigserial(`id`, { mode: `number` }).primaryKey(),
|
|
104
|
+
tenantId: text(`tenant_id`).notNull().default(`default`),
|
|
105
|
+
entityType: text(`entity_type`).notNull(),
|
|
106
|
+
permission: text(`permission`).notNull(),
|
|
107
|
+
subjectKind: text(`subject_kind`).notNull(),
|
|
108
|
+
subjectValue: text(`subject_value`).notNull(),
|
|
109
|
+
createdBy: text(`created_by`),
|
|
110
|
+
expiresAt: timestamp(`expires_at`, { withTimezone: true }),
|
|
111
|
+
createdAt: timestamp(`created_at`, { withTimezone: true }).notNull().defaultNow(),
|
|
112
|
+
updatedAt: timestamp(`updated_at`, { withTimezone: true }).notNull().defaultNow()
|
|
113
|
+
}, (table) => [
|
|
114
|
+
index(`idx_type_permission_grants_lookup`).on(table.tenantId, table.entityType, table.permission, table.subjectKind, table.subjectValue),
|
|
115
|
+
index(`idx_type_permission_grants_expiry`).on(table.tenantId, table.expiresAt),
|
|
116
|
+
check(`chk_type_permission_grants_permission`, sql`${table.permission} IN ('spawn', 'manage')`),
|
|
117
|
+
check(`chk_type_permission_grants_subject_kind`, sql`${table.subjectKind} IN ('principal', 'principal_kind')`)
|
|
118
|
+
]);
|
|
119
|
+
const entityLineage = pgTable(`entity_lineage`, {
|
|
120
|
+
tenantId: text(`tenant_id`).notNull().default(`default`),
|
|
121
|
+
ancestorUrl: text(`ancestor_url`).notNull(),
|
|
122
|
+
descendantUrl: text(`descendant_url`).notNull(),
|
|
123
|
+
depth: integer(`depth`).notNull(),
|
|
124
|
+
createdAt: timestamp(`created_at`, { withTimezone: true }).notNull().defaultNow()
|
|
125
|
+
}, (table) => [
|
|
126
|
+
primaryKey({ columns: [
|
|
127
|
+
table.tenantId,
|
|
128
|
+
table.ancestorUrl,
|
|
129
|
+
table.descendantUrl
|
|
130
|
+
] }),
|
|
131
|
+
index(`idx_entity_lineage_descendant`).on(table.tenantId, table.descendantUrl),
|
|
132
|
+
check(`chk_entity_lineage_depth`, sql`${table.depth} >= 0`)
|
|
133
|
+
]);
|
|
134
|
+
const entityPermissionGrants = pgTable(`entity_permission_grants`, {
|
|
135
|
+
id: bigserial(`id`, { mode: `number` }).primaryKey(),
|
|
136
|
+
tenantId: text(`tenant_id`).notNull().default(`default`),
|
|
137
|
+
entityUrl: text(`entity_url`).notNull(),
|
|
138
|
+
permission: text(`permission`).notNull(),
|
|
139
|
+
subjectKind: text(`subject_kind`).notNull(),
|
|
140
|
+
subjectValue: text(`subject_value`).notNull(),
|
|
141
|
+
propagation: text(`propagation`).notNull().default(`self`),
|
|
142
|
+
copyToChildren: boolean(`copy_to_children`).notNull().default(false),
|
|
143
|
+
createdBy: text(`created_by`),
|
|
144
|
+
expiresAt: timestamp(`expires_at`, { withTimezone: true }),
|
|
145
|
+
createdAt: timestamp(`created_at`, { withTimezone: true }).notNull().defaultNow(),
|
|
146
|
+
updatedAt: timestamp(`updated_at`, { withTimezone: true }).notNull().defaultNow()
|
|
147
|
+
}, (table) => [
|
|
148
|
+
index(`idx_entity_permission_grants_entity`).on(table.tenantId, table.entityUrl),
|
|
149
|
+
index(`idx_entity_permission_grants_subject`).on(table.tenantId, table.permission, table.subjectKind, table.subjectValue),
|
|
150
|
+
index(`idx_entity_permission_grants_expiry`).on(table.tenantId, table.expiresAt),
|
|
151
|
+
check(`chk_entity_permission_grants_permission`, sql`${table.permission} IN ('read', 'write', 'delete', 'signal', 'fork', 'schedule', 'spawn', 'manage')`),
|
|
152
|
+
check(`chk_entity_permission_grants_subject_kind`, sql`${table.subjectKind} IN ('principal', 'principal_kind')`),
|
|
153
|
+
check(`chk_entity_permission_grants_propagation`, sql`${table.propagation} IN ('self', 'descendants')`)
|
|
154
|
+
]);
|
|
155
|
+
const entityEffectivePermissions = pgTable(`entity_effective_permissions`, {
|
|
156
|
+
id: bigserial(`id`, { mode: `number` }).primaryKey(),
|
|
157
|
+
tenantId: text(`tenant_id`).notNull().default(`default`),
|
|
158
|
+
entityUrl: text(`entity_url`).notNull(),
|
|
159
|
+
sourceEntityUrl: text(`source_entity_url`).notNull(),
|
|
160
|
+
sourceGrantId: bigint(`source_grant_id`, { mode: `number` }).notNull(),
|
|
161
|
+
permission: text(`permission`).notNull(),
|
|
162
|
+
subjectKind: text(`subject_kind`).notNull(),
|
|
163
|
+
subjectValue: text(`subject_value`).notNull(),
|
|
164
|
+
expiresAt: timestamp(`expires_at`, { withTimezone: true }),
|
|
165
|
+
createdAt: timestamp(`created_at`, { withTimezone: true }).notNull().defaultNow()
|
|
166
|
+
}, (table) => [
|
|
167
|
+
unique(`uq_entity_effective_permission`).on(table.tenantId, table.entityUrl, table.sourceGrantId),
|
|
168
|
+
index(`idx_entity_effective_permissions_lookup`).on(table.tenantId, table.permission, table.subjectKind, table.subjectValue, table.entityUrl),
|
|
169
|
+
index(`idx_entity_effective_permissions_entity`).on(table.tenantId, table.entityUrl),
|
|
170
|
+
index(`idx_entity_effective_permissions_expiry`).on(table.tenantId, table.expiresAt),
|
|
171
|
+
check(`chk_entity_effective_permissions_permission`, sql`${table.permission} IN ('read', 'write', 'delete', 'signal', 'fork', 'schedule', 'spawn', 'manage')`),
|
|
172
|
+
check(`chk_entity_effective_permissions_subject_kind`, sql`${table.subjectKind} IN ('principal', 'principal_kind')`)
|
|
173
|
+
]);
|
|
174
|
+
const sharedStateLinks = pgTable(`shared_state_links`, {
|
|
175
|
+
tenantId: text(`tenant_id`).notNull().default(`default`),
|
|
176
|
+
sharedStateId: text(`shared_state_id`).notNull(),
|
|
177
|
+
ownerEntityUrl: text(`owner_entity_url`).notNull(),
|
|
178
|
+
manifestKey: text(`manifest_key`).notNull(),
|
|
179
|
+
createdAt: timestamp(`created_at`, { withTimezone: true }).notNull().defaultNow(),
|
|
180
|
+
updatedAt: timestamp(`updated_at`, { withTimezone: true }).notNull().defaultNow()
|
|
181
|
+
}, (table) => [
|
|
182
|
+
primaryKey({ columns: [
|
|
183
|
+
table.tenantId,
|
|
184
|
+
table.ownerEntityUrl,
|
|
185
|
+
table.manifestKey
|
|
186
|
+
] }),
|
|
187
|
+
index(`idx_shared_state_links_shared_state`).on(table.tenantId, table.sharedStateId),
|
|
188
|
+
index(`idx_shared_state_links_owner`).on(table.tenantId, table.ownerEntityUrl)
|
|
189
|
+
]);
|
|
96
190
|
const users = pgTable(`users`, {
|
|
97
191
|
tenantId: text(`tenant_id`).notNull().default(`default`),
|
|
98
192
|
id: text(`id`).notNull(),
|
|
@@ -279,12 +373,18 @@ const entityBridges = pgTable(`entity_bridges`, {
|
|
|
279
373
|
sourceRef: text(`source_ref`).notNull(),
|
|
280
374
|
tags: jsonb(`tags`).notNull(),
|
|
281
375
|
streamUrl: text(`stream_url`).notNull(),
|
|
376
|
+
principalUrl: text(`principal_url`),
|
|
377
|
+
principalKind: text(`principal_kind`),
|
|
282
378
|
shapeHandle: text(`shape_handle`),
|
|
283
379
|
shapeOffset: text(`shape_offset`),
|
|
284
380
|
lastObserverActivityAt: timestamp(`last_observer_activity_at`, { withTimezone: true }).notNull().defaultNow(),
|
|
285
381
|
createdAt: timestamp(`created_at`, { withTimezone: true }).notNull().defaultNow(),
|
|
286
382
|
updatedAt: timestamp(`updated_at`, { withTimezone: true }).notNull().defaultNow()
|
|
287
|
-
}, (table) => [
|
|
383
|
+
}, (table) => [
|
|
384
|
+
primaryKey({ columns: [table.tenantId, table.sourceRef] }),
|
|
385
|
+
unique(`uq_entity_bridges_stream_url`).on(table.tenantId, table.streamUrl),
|
|
386
|
+
index(`idx_entity_bridges_principal`).on(table.tenantId, table.principalKind, table.principalUrl)
|
|
387
|
+
]);
|
|
288
388
|
const entityManifestSources = pgTable(`entity_manifest_sources`, {
|
|
289
389
|
tenantId: text(`tenant_id`).notNull().default(`default`),
|
|
290
390
|
ownerEntityUrl: text(`owner_entity_url`).notNull(),
|
|
@@ -1045,29 +1145,136 @@ function buildElectricProxyTarget(options) {
|
|
|
1045
1145
|
if (options.electricSecret) target.searchParams.set(`secret`, options.electricSecret);
|
|
1046
1146
|
const table = options.incomingUrl.searchParams.get(`table`);
|
|
1047
1147
|
if (table === `entities`) {
|
|
1048
|
-
target.searchParams.set(`columns`, `"tenant_id","url","type","status","dispatch_policy","tags","spawn_args","sandbox","parent","type_revision","inbox_schemas","state_schemas","created_at","updated_at"`);
|
|
1049
|
-
|
|
1148
|
+
target.searchParams.set(`columns`, `"tenant_id","url","type","status","dispatch_policy","tags","spawn_args","sandbox","parent","created_by","type_revision","inbox_schemas","state_schemas","created_at","updated_at"`);
|
|
1149
|
+
applyShapeWhere(target, buildReadableEntitiesWhere({
|
|
1150
|
+
tenantId: options.tenantId,
|
|
1151
|
+
principalUrl: options.principalUrl ?? ``,
|
|
1152
|
+
principalKind: options.principalKind ?? ``,
|
|
1153
|
+
permissionBypass: options.permissionBypass
|
|
1154
|
+
}));
|
|
1050
1155
|
} else if (table === `entity_types`) {
|
|
1051
|
-
target.searchParams.set(`columns`, `"tenant_id","name","description","creation_schema","inbox_schemas","state_schemas","serve_endpoint","default_dispatch_policy","revision","created_at","updated_at"`);
|
|
1052
|
-
|
|
1156
|
+
target.searchParams.set(`columns`, `"tenant_id","name","description","creation_schema","inbox_schemas","state_schemas","slash_commands","serve_endpoint","default_dispatch_policy","revision","created_at","updated_at"`);
|
|
1157
|
+
applyShapeWhere(target, buildSpawnableEntityTypesWhere({
|
|
1158
|
+
tenantId: options.tenantId,
|
|
1159
|
+
principalUrl: options.principalUrl ?? ``,
|
|
1160
|
+
principalKind: options.principalKind ?? ``,
|
|
1161
|
+
permissionBypass: options.permissionBypass
|
|
1162
|
+
}));
|
|
1053
1163
|
} else if (table === `runners`) {
|
|
1054
1164
|
target.searchParams.set(`columns`, `"tenant_id","id","owner_principal","label","kind","admin_status","wake_stream","sandbox_profiles","created_at","updated_at"`);
|
|
1055
1165
|
applyTenantShapeWhere(target, options.tenantId, [`owner_principal = ${sqlStringLiteral$2(options.principalUrl ?? ``)}`]);
|
|
1166
|
+
} else if (table === `users`) {
|
|
1167
|
+
target.searchParams.set(`columns`, `"tenant_id","id","display_name","email","avatar_url","created_at","updated_at"`);
|
|
1168
|
+
applyTenantShapeWhere(target, options.tenantId);
|
|
1169
|
+
} else if (table === `entity_effective_permissions`) {
|
|
1170
|
+
target.searchParams.set(`columns`, `"tenant_id","id","entity_url","source_entity_url","source_grant_id","permission","subject_kind","subject_value","expires_at","created_at"`);
|
|
1171
|
+
applyShapeWhere(target, buildCurrentPrincipalEntityEffectivePermissionsWhere({
|
|
1172
|
+
tenantId: options.tenantId,
|
|
1173
|
+
principalUrl: options.principalUrl ?? ``,
|
|
1174
|
+
principalKind: options.principalKind ?? ``,
|
|
1175
|
+
permissionBypass: options.permissionBypass
|
|
1176
|
+
}));
|
|
1056
1177
|
} else if (table === `runner_runtime_diagnostics`) {
|
|
1057
1178
|
target.searchParams.set(`columns`, `"tenant_id","runner_id","owner_principal","wake_stream_offset","last_seen_at","liveness_lease_expires_at","diagnostics","updated_at"`);
|
|
1058
1179
|
applyTenantShapeWhere(target, options.tenantId, [`owner_principal = ${sqlStringLiteral$2(options.principalUrl ?? ``)}`]);
|
|
1059
1180
|
} else if (table === `entity_dispatch_state`) {
|
|
1060
1181
|
target.searchParams.set(`columns`, `"tenant_id","entity_url","pending_source_streams","pending_reason","pending_since","outstanding_wake_id","outstanding_wake_target","outstanding_wake_created_at","active_consumer_id","active_runner_id","active_epoch","active_claimed_at","active_lease_expires_at","last_wake_id","last_claimed_at","last_released_at","last_completed_at","last_error","updated_at"`);
|
|
1061
|
-
|
|
1182
|
+
applyShapeWhere(target, buildReadableEntityUrlWhere({
|
|
1183
|
+
tenantId: options.tenantId,
|
|
1184
|
+
principalUrl: options.principalUrl ?? ``,
|
|
1185
|
+
principalKind: options.principalKind ?? ``,
|
|
1186
|
+
permissionBypass: options.permissionBypass
|
|
1187
|
+
}));
|
|
1062
1188
|
} else if (table === `wake_notifications`) {
|
|
1063
1189
|
target.searchParams.set(`columns`, `"tenant_id","wake_id","entity_url","target_type","target_runner_id","target_webhook_url","target_worker_pool_id","runner_wake_stream","runner_wake_stream_offset","notification_public","delivery_status","claim_status","created_at","delivered_at","claimed_at","resolved_at"`);
|
|
1064
|
-
|
|
1190
|
+
applyShapeWhere(target, buildReadableEntityUrlWhere({
|
|
1191
|
+
tenantId: options.tenantId,
|
|
1192
|
+
principalUrl: options.principalUrl ?? ``,
|
|
1193
|
+
principalKind: options.principalKind ?? ``,
|
|
1194
|
+
permissionBypass: options.permissionBypass
|
|
1195
|
+
}));
|
|
1065
1196
|
} else if (table === `consumer_claims`) {
|
|
1066
1197
|
target.searchParams.set(`columns`, `"tenant_id","consumer_id","epoch","wake_id","entity_url","stream_path","runner_id","status","claimed_at","last_heartbeat_at","lease_expires_at","released_at","acked_streams","updated_at"`);
|
|
1067
|
-
|
|
1198
|
+
applyShapeWhere(target, buildReadableEntityUrlWhere({
|
|
1199
|
+
tenantId: options.tenantId,
|
|
1200
|
+
principalUrl: options.principalUrl ?? ``,
|
|
1201
|
+
principalKind: options.principalKind ?? ``,
|
|
1202
|
+
permissionBypass: options.permissionBypass
|
|
1203
|
+
}));
|
|
1068
1204
|
}
|
|
1069
1205
|
return target;
|
|
1070
1206
|
}
|
|
1207
|
+
function buildReadableEntitiesWhere(options) {
|
|
1208
|
+
const tenant = sqlStringLiteral$2(options.tenantId);
|
|
1209
|
+
if (options.permissionBypass) return `tenant_id = ${tenant}`;
|
|
1210
|
+
const principalUrl$1 = sqlStringLiteral$2(options.principalUrl);
|
|
1211
|
+
const principalKind = sqlStringLiteral$2(options.principalKind);
|
|
1212
|
+
return [
|
|
1213
|
+
`tenant_id = ${tenant}`,
|
|
1214
|
+
`AND (`,
|
|
1215
|
+
` created_by = ${principalUrl$1}`,
|
|
1216
|
+
` OR url IN (`,
|
|
1217
|
+
` SELECT entity_url`,
|
|
1218
|
+
` FROM entity_effective_permissions`,
|
|
1219
|
+
` WHERE tenant_id = ${tenant}`,
|
|
1220
|
+
` AND permission IN ('read', 'manage')`,
|
|
1221
|
+
` AND (`,
|
|
1222
|
+
` (subject_kind = 'principal' AND subject_value = ${principalUrl$1})`,
|
|
1223
|
+
` OR (subject_kind = 'principal_kind' AND subject_value = ${principalKind})`,
|
|
1224
|
+
` )`,
|
|
1225
|
+
` )`,
|
|
1226
|
+
`)`
|
|
1227
|
+
].join(`\n`);
|
|
1228
|
+
}
|
|
1229
|
+
function buildReadableEntityUrlWhere(options) {
|
|
1230
|
+
const tenant = sqlStringLiteral$2(options.tenantId);
|
|
1231
|
+
if (options.permissionBypass) return `tenant_id = ${tenant}`;
|
|
1232
|
+
return [
|
|
1233
|
+
`tenant_id = ${tenant}`,
|
|
1234
|
+
`AND entity_url IN (`,
|
|
1235
|
+
` SELECT url`,
|
|
1236
|
+
` FROM entities`,
|
|
1237
|
+
` WHERE ${indentWhere(buildReadableEntitiesWhere(options), ` `).trimStart()}`,
|
|
1238
|
+
`)`
|
|
1239
|
+
].join(`\n`);
|
|
1240
|
+
}
|
|
1241
|
+
function buildCurrentPrincipalEntityEffectivePermissionsWhere(options) {
|
|
1242
|
+
const tenant = sqlStringLiteral$2(options.tenantId);
|
|
1243
|
+
if (options.permissionBypass) return `tenant_id = ${tenant}`;
|
|
1244
|
+
const principalUrl$1 = sqlStringLiteral$2(options.principalUrl);
|
|
1245
|
+
const principalKind = sqlStringLiteral$2(options.principalKind);
|
|
1246
|
+
return [
|
|
1247
|
+
`tenant_id = ${tenant}`,
|
|
1248
|
+
`AND (`,
|
|
1249
|
+
` (subject_kind = 'principal' AND subject_value = ${principalUrl$1})`,
|
|
1250
|
+
` OR (subject_kind = 'principal_kind' AND subject_value = ${principalKind})`,
|
|
1251
|
+
`)`,
|
|
1252
|
+
`AND entity_url IN (`,
|
|
1253
|
+
` SELECT url`,
|
|
1254
|
+
` FROM entities`,
|
|
1255
|
+
` WHERE ${buildReadableEntitiesWhere(options)}`,
|
|
1256
|
+
`)`
|
|
1257
|
+
].join(`\n`);
|
|
1258
|
+
}
|
|
1259
|
+
function buildSpawnableEntityTypesWhere(options) {
|
|
1260
|
+
const tenant = sqlStringLiteral$2(options.tenantId);
|
|
1261
|
+
if (options.permissionBypass) return `tenant_id = ${tenant}`;
|
|
1262
|
+
const principalUrl$1 = sqlStringLiteral$2(options.principalUrl);
|
|
1263
|
+
const principalKind = sqlStringLiteral$2(options.principalKind);
|
|
1264
|
+
return [
|
|
1265
|
+
`tenant_id = ${tenant}`,
|
|
1266
|
+
`AND name IN (`,
|
|
1267
|
+
` SELECT entity_type`,
|
|
1268
|
+
` FROM entity_type_permission_grants`,
|
|
1269
|
+
` WHERE tenant_id = ${tenant}`,
|
|
1270
|
+
` AND permission IN ('spawn', 'manage')`,
|
|
1271
|
+
` AND (`,
|
|
1272
|
+
` (subject_kind = 'principal' AND subject_value = ${principalUrl$1})`,
|
|
1273
|
+
` OR (subject_kind = 'principal_kind' AND subject_value = ${principalKind})`,
|
|
1274
|
+
` )`,
|
|
1275
|
+
`)`
|
|
1276
|
+
].join(`\n`);
|
|
1277
|
+
}
|
|
1071
1278
|
async function forwardFetchRequest(options) {
|
|
1072
1279
|
const routingAdapter = resolveDurableStreamsRoutingAdapter(options.durableStreamsRouting, options.durableStreamsUrl);
|
|
1073
1280
|
const routingInput = {
|
|
@@ -1102,13 +1309,18 @@ function decodeJsonObject(body) {
|
|
|
1102
1309
|
return null;
|
|
1103
1310
|
}
|
|
1104
1311
|
function applyTenantShapeWhere(target, tenantId, extraConditions = []) {
|
|
1105
|
-
|
|
1312
|
+
applyShapeWhere(target, [`tenant_id = ${sqlStringLiteral$2(tenantId)}`, ...extraConditions].join(` AND `));
|
|
1313
|
+
}
|
|
1314
|
+
function applyShapeWhere(target, enforcedWhere) {
|
|
1106
1315
|
const existingWhere = target.searchParams.get(`where`);
|
|
1107
|
-
target.searchParams.set(`where`, existingWhere ? `${
|
|
1316
|
+
target.searchParams.set(`where`, existingWhere ? `${enforcedWhere} AND (${existingWhere})` : enforcedWhere);
|
|
1108
1317
|
}
|
|
1109
1318
|
function sqlStringLiteral$2(value) {
|
|
1110
1319
|
return `'${value.replace(/'/g, `''`)}'`;
|
|
1111
1320
|
}
|
|
1321
|
+
function indentWhere(where, prefix) {
|
|
1322
|
+
return where.split(`\n`).map((line) => `${prefix}${line}`).join(`\n`);
|
|
1323
|
+
}
|
|
1112
1324
|
|
|
1113
1325
|
//#endregion
|
|
1114
1326
|
//#region src/routing/agent-ui-router.ts
|
|
@@ -1402,6 +1614,262 @@ function isLoopbackHostname(hostname) {
|
|
|
1402
1614
|
return hostname === `localhost` || hostname === `127.0.0.1` || hostname === `::1`;
|
|
1403
1615
|
}
|
|
1404
1616
|
|
|
1617
|
+
//#endregion
|
|
1618
|
+
//#region src/principal.ts
|
|
1619
|
+
const ELECTRIC_PRINCIPAL_HEADER = `electric-principal`;
|
|
1620
|
+
const PRINCIPAL_KINDS = new Set([
|
|
1621
|
+
`user`,
|
|
1622
|
+
`agent`,
|
|
1623
|
+
`service`,
|
|
1624
|
+
`system`
|
|
1625
|
+
]);
|
|
1626
|
+
function parsePrincipalKey(input) {
|
|
1627
|
+
const colon = input.indexOf(`:`);
|
|
1628
|
+
if (colon <= 0) throw new Error(`Invalid principal identifier`);
|
|
1629
|
+
const kind = input.slice(0, colon);
|
|
1630
|
+
const id = input.slice(colon + 1);
|
|
1631
|
+
if (!PRINCIPAL_KINDS.has(kind)) throw new Error(`Invalid principal kind`);
|
|
1632
|
+
if (!id || id.includes(`/`)) throw new Error(`Invalid principal id`);
|
|
1633
|
+
const key = `${kind}:${id}`;
|
|
1634
|
+
return {
|
|
1635
|
+
kind,
|
|
1636
|
+
id,
|
|
1637
|
+
key,
|
|
1638
|
+
url: `/principal/${encodeURIComponent(key)}`
|
|
1639
|
+
};
|
|
1640
|
+
}
|
|
1641
|
+
function principalUrl(key) {
|
|
1642
|
+
return parsePrincipalKey(key).url;
|
|
1643
|
+
}
|
|
1644
|
+
function parsePrincipalUrl(url) {
|
|
1645
|
+
if (!url.startsWith(`/principal/`)) return null;
|
|
1646
|
+
const segment = url.slice(`/principal/`.length);
|
|
1647
|
+
if (!segment || segment.includes(`/`)) return null;
|
|
1648
|
+
try {
|
|
1649
|
+
return parsePrincipalKey(decodeURIComponent(segment));
|
|
1650
|
+
} catch {
|
|
1651
|
+
return null;
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
function parsePrincipalInput(input) {
|
|
1655
|
+
const urlPrincipal = parsePrincipalUrl(input);
|
|
1656
|
+
if (urlPrincipal) return urlPrincipal;
|
|
1657
|
+
try {
|
|
1658
|
+
return parsePrincipalKey(input);
|
|
1659
|
+
} catch {
|
|
1660
|
+
return null;
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
function getPrincipalFromRequest(request) {
|
|
1664
|
+
const value = request.headers.get(ELECTRIC_PRINCIPAL_HEADER);
|
|
1665
|
+
if (!value) return null;
|
|
1666
|
+
return parsePrincipalInput(value);
|
|
1667
|
+
}
|
|
1668
|
+
function getDevPrincipal() {
|
|
1669
|
+
return parsePrincipalKey(`system:dev-local`);
|
|
1670
|
+
}
|
|
1671
|
+
const BUILT_IN_SYSTEM_PRINCIPAL_IDS = new Set([
|
|
1672
|
+
`framework`,
|
|
1673
|
+
`auth-sync`,
|
|
1674
|
+
`dev-local`
|
|
1675
|
+
]);
|
|
1676
|
+
function isBuiltInSystemPrincipalUrl(url) {
|
|
1677
|
+
if (!url?.startsWith(`/principal/`)) return false;
|
|
1678
|
+
try {
|
|
1679
|
+
const principal = parsePrincipalUrl(url);
|
|
1680
|
+
if (!principal) return false;
|
|
1681
|
+
return principal.kind === `system` && BUILT_IN_SYSTEM_PRINCIPAL_IDS.has(principal.id);
|
|
1682
|
+
} catch {
|
|
1683
|
+
return false;
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
function principalFromCreatedBy(createdBy) {
|
|
1687
|
+
if (!createdBy) return void 0;
|
|
1688
|
+
const principal = parsePrincipalUrl(createdBy);
|
|
1689
|
+
if (!principal) return {
|
|
1690
|
+
url: createdBy,
|
|
1691
|
+
key: null
|
|
1692
|
+
};
|
|
1693
|
+
return {
|
|
1694
|
+
url: principal.url,
|
|
1695
|
+
key: principal.key,
|
|
1696
|
+
kind: principal.kind,
|
|
1697
|
+
id: principal.id
|
|
1698
|
+
};
|
|
1699
|
+
}
|
|
1700
|
+
const principalIdentityStateSchema = Type.Object({
|
|
1701
|
+
kind: Type.Union([
|
|
1702
|
+
Type.Literal(`user`),
|
|
1703
|
+
Type.Literal(`agent`),
|
|
1704
|
+
Type.Literal(`service`),
|
|
1705
|
+
Type.Literal(`system`)
|
|
1706
|
+
]),
|
|
1707
|
+
id: Type.String(),
|
|
1708
|
+
key: Type.String(),
|
|
1709
|
+
url: Type.String(),
|
|
1710
|
+
updated_at: Type.String(),
|
|
1711
|
+
display_name: Type.Optional(Type.String()),
|
|
1712
|
+
email: Type.Optional(Type.String()),
|
|
1713
|
+
avatar_url: Type.Optional(Type.String()),
|
|
1714
|
+
auth_provider: Type.Optional(Type.String()),
|
|
1715
|
+
auth_subject: Type.Optional(Type.String()),
|
|
1716
|
+
claims: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
|
|
1717
|
+
created_at: Type.Optional(Type.String())
|
|
1718
|
+
}, { additionalProperties: false });
|
|
1719
|
+
const principalUpdateIdentityMessageSchema = Type.Object({ identity: principalIdentityStateSchema }, { additionalProperties: false });
|
|
1720
|
+
|
|
1721
|
+
//#endregion
|
|
1722
|
+
//#region src/permissions.ts
|
|
1723
|
+
const authzDecisionCache = new WeakMap();
|
|
1724
|
+
function principalSubject(principal) {
|
|
1725
|
+
return {
|
|
1726
|
+
principalUrl: principal.url,
|
|
1727
|
+
principalKind: principal.kind
|
|
1728
|
+
};
|
|
1729
|
+
}
|
|
1730
|
+
function isPermissionBypassPrincipal(ctx) {
|
|
1731
|
+
return isBuiltInSystemPrincipalUrl(ctx.principal.url);
|
|
1732
|
+
}
|
|
1733
|
+
async function canAccessEntity(ctx, entity, permission, request) {
|
|
1734
|
+
if (isPermissionBypassPrincipal(ctx)) return true;
|
|
1735
|
+
await ctx.entityManager.registry.pruneExpiredPermissionGrants?.();
|
|
1736
|
+
const builtInAllowed = entity.created_by === ctx.principal.url || await ctx.entityManager.registry.hasEntityPermission(entity.url, permission, principalSubject(ctx.principal));
|
|
1737
|
+
return await applyAuthorizationHook(ctx, {
|
|
1738
|
+
verb: permission,
|
|
1739
|
+
resourceKey: `entity:${entity.url}`,
|
|
1740
|
+
resource: {
|
|
1741
|
+
kind: `entity`,
|
|
1742
|
+
entity
|
|
1743
|
+
},
|
|
1744
|
+
builtInAllowed,
|
|
1745
|
+
request
|
|
1746
|
+
});
|
|
1747
|
+
}
|
|
1748
|
+
async function canAccessEntityType(ctx, entityType, permission, request) {
|
|
1749
|
+
if (isPermissionBypassPrincipal(ctx)) return true;
|
|
1750
|
+
await ctx.entityManager.registry.pruneExpiredPermissionGrants?.();
|
|
1751
|
+
const builtInAllowed = await ctx.entityManager.registry.hasEntityTypePermission(entityType.name, permission, principalSubject(ctx.principal));
|
|
1752
|
+
return await applyAuthorizationHook(ctx, {
|
|
1753
|
+
verb: permission,
|
|
1754
|
+
resourceKey: `entity_type:${entityType.name}`,
|
|
1755
|
+
resource: {
|
|
1756
|
+
kind: `entity_type`,
|
|
1757
|
+
entityType
|
|
1758
|
+
},
|
|
1759
|
+
builtInAllowed,
|
|
1760
|
+
request
|
|
1761
|
+
});
|
|
1762
|
+
}
|
|
1763
|
+
async function canRegisterEntityType(ctx, input, request) {
|
|
1764
|
+
if (isPermissionBypassPrincipal(ctx)) return true;
|
|
1765
|
+
return await applyAuthorizationHook(ctx, {
|
|
1766
|
+
verb: `manage`,
|
|
1767
|
+
resourceKey: `entity_type_registration:${input.name}`,
|
|
1768
|
+
resource: {
|
|
1769
|
+
kind: `entity_type_registration`,
|
|
1770
|
+
entityTypeName: input.name
|
|
1771
|
+
},
|
|
1772
|
+
builtInAllowed: true,
|
|
1773
|
+
request
|
|
1774
|
+
});
|
|
1775
|
+
}
|
|
1776
|
+
async function canAccessSharedState(ctx, sharedStateId, permission, request, ownerEntityUrl) {
|
|
1777
|
+
if (isPermissionBypassPrincipal(ctx)) return true;
|
|
1778
|
+
await ctx.entityManager.registry.pruneExpiredPermissionGrants?.();
|
|
1779
|
+
const storedLinkedEntityUrls = await ctx.entityManager.registry.listSharedStateLinkedEntityUrls(sharedStateId);
|
|
1780
|
+
const bootstrapEntityUrls = storedLinkedEntityUrls.length === 0 && ownerEntityUrl ? [ownerEntityUrl] : [];
|
|
1781
|
+
const linkedEntityUrls = [...new Set([...storedLinkedEntityUrls, ...bootstrapEntityUrls])];
|
|
1782
|
+
for (const entityUrl of linkedEntityUrls) {
|
|
1783
|
+
const entity = await ctx.entityManager.registry.getEntity(entityUrl);
|
|
1784
|
+
if (!entity) continue;
|
|
1785
|
+
if (entity.created_by === ctx.principal.url || await ctx.entityManager.registry.hasEntityPermission(entity.url, permission, principalSubject(ctx.principal))) return await applyAuthorizationHook(ctx, {
|
|
1786
|
+
verb: permission,
|
|
1787
|
+
resourceKey: `shared_state:${sharedStateId}`,
|
|
1788
|
+
resource: {
|
|
1789
|
+
kind: `shared_state`,
|
|
1790
|
+
sharedStateId,
|
|
1791
|
+
linkedEntityUrls
|
|
1792
|
+
},
|
|
1793
|
+
builtInAllowed: true,
|
|
1794
|
+
request
|
|
1795
|
+
});
|
|
1796
|
+
}
|
|
1797
|
+
return await applyAuthorizationHook(ctx, {
|
|
1798
|
+
verb: permission,
|
|
1799
|
+
resourceKey: `shared_state:${sharedStateId}`,
|
|
1800
|
+
resource: {
|
|
1801
|
+
kind: `shared_state`,
|
|
1802
|
+
sharedStateId,
|
|
1803
|
+
linkedEntityUrls
|
|
1804
|
+
},
|
|
1805
|
+
builtInAllowed: false,
|
|
1806
|
+
request
|
|
1807
|
+
});
|
|
1808
|
+
}
|
|
1809
|
+
async function applyAuthorizationHook(ctx, input) {
|
|
1810
|
+
const hook = ctx.authorizeRequest;
|
|
1811
|
+
if (!hook) return input.builtInAllowed;
|
|
1812
|
+
const cacheKey = [
|
|
1813
|
+
ctx.service,
|
|
1814
|
+
ctx.principal.url,
|
|
1815
|
+
input.verb,
|
|
1816
|
+
input.resourceKey
|
|
1817
|
+
].join(`|`);
|
|
1818
|
+
const cached = getCachedDecision(hook, cacheKey);
|
|
1819
|
+
if (cached) return cached.decision === `allow`;
|
|
1820
|
+
let decision;
|
|
1821
|
+
try {
|
|
1822
|
+
decision = await hook({
|
|
1823
|
+
tenant: ctx.service,
|
|
1824
|
+
principal: ctx.principal,
|
|
1825
|
+
verb: input.verb,
|
|
1826
|
+
resource: input.resource,
|
|
1827
|
+
request: input.request ? requestMetadata(input.request) : void 0,
|
|
1828
|
+
builtInAllowed: input.builtInAllowed
|
|
1829
|
+
});
|
|
1830
|
+
} catch (error) {
|
|
1831
|
+
serverLog.warn(`[agent-server] authorization hook failed:`, error);
|
|
1832
|
+
return false;
|
|
1833
|
+
}
|
|
1834
|
+
cacheDecision(hook, cacheKey, decision);
|
|
1835
|
+
return decision.decision === `allow`;
|
|
1836
|
+
}
|
|
1837
|
+
function getCachedDecision(hook, cacheKey) {
|
|
1838
|
+
const cache = authzDecisionCache.get(hook);
|
|
1839
|
+
const entry = cache?.get(cacheKey);
|
|
1840
|
+
if (!entry) return null;
|
|
1841
|
+
if (entry.expiresAt <= Date.now()) {
|
|
1842
|
+
cache?.delete(cacheKey);
|
|
1843
|
+
return null;
|
|
1844
|
+
}
|
|
1845
|
+
return { decision: entry.decision };
|
|
1846
|
+
}
|
|
1847
|
+
function cacheDecision(hook, cacheKey, decision) {
|
|
1848
|
+
if (!decision.expires_at) return;
|
|
1849
|
+
const expiresAt = Date.parse(decision.expires_at);
|
|
1850
|
+
if (!Number.isFinite(expiresAt) || expiresAt <= Date.now()) return;
|
|
1851
|
+
let cache = authzDecisionCache.get(hook);
|
|
1852
|
+
if (!cache) {
|
|
1853
|
+
cache = new Map();
|
|
1854
|
+
authzDecisionCache.set(hook, cache);
|
|
1855
|
+
}
|
|
1856
|
+
cache.set(cacheKey, {
|
|
1857
|
+
decision: decision.decision,
|
|
1858
|
+
expiresAt
|
|
1859
|
+
});
|
|
1860
|
+
}
|
|
1861
|
+
function requestMetadata(request) {
|
|
1862
|
+
const headers = {};
|
|
1863
|
+
request.headers.forEach((value, key) => {
|
|
1864
|
+
headers[key] = value;
|
|
1865
|
+
});
|
|
1866
|
+
return {
|
|
1867
|
+
method: request.method,
|
|
1868
|
+
url: request.url,
|
|
1869
|
+
headers
|
|
1870
|
+
};
|
|
1871
|
+
}
|
|
1872
|
+
|
|
1405
1873
|
//#endregion
|
|
1406
1874
|
//#region src/webhook-signing.ts
|
|
1407
1875
|
const encoder = new TextEncoder();
|
|
@@ -1492,6 +1960,7 @@ const subscriptionControlActions = [
|
|
|
1492
1960
|
`ack`,
|
|
1493
1961
|
`release`
|
|
1494
1962
|
];
|
|
1963
|
+
const SHARED_STATE_OWNER_ENTITY_HEADER = `electric-owner-entity`;
|
|
1495
1964
|
const durableStreamsRouter = Router();
|
|
1496
1965
|
durableStreamsRouter.put(`/__ds/subscriptions/:subscriptionId`, putSubscriptionBase);
|
|
1497
1966
|
durableStreamsRouter.get(`/__ds/subscriptions/:subscriptionId`, getSubscriptionBase);
|
|
@@ -1709,6 +2178,8 @@ async function webhookJwks(_request, ctx) {
|
|
|
1709
2178
|
});
|
|
1710
2179
|
}
|
|
1711
2180
|
async function streamAppend(request, ctx) {
|
|
2181
|
+
const auth = await authorizeDurableStreamAccess(request, ctx);
|
|
2182
|
+
if (auth) return auth;
|
|
1712
2183
|
return await electricAgentsStreamAppendRouter.fetch(createStreamAppendRouteRequest(request), ctx.runtime, (req, body) => forwardFetchRequest({
|
|
1713
2184
|
request: {
|
|
1714
2185
|
method: req.method,
|
|
@@ -1725,8 +2196,9 @@ async function streamAppend(request, ctx) {
|
|
|
1725
2196
|
}));
|
|
1726
2197
|
}
|
|
1727
2198
|
async function proxyPassThrough(request, ctx) {
|
|
2199
|
+
const auth = await authorizeDurableStreamAccess(request, ctx);
|
|
2200
|
+
if (auth) return auth;
|
|
1728
2201
|
const streamPath = new URL(request.url).pathname;
|
|
1729
|
-
if (ctx.entityManager?.isAttachmentStreamPath(streamPath)) return new Response(null, { status: 404 });
|
|
1730
2202
|
const upstream = await forwardToDurableStreams(ctx, request);
|
|
1731
2203
|
const method = request.method.toUpperCase();
|
|
1732
2204
|
const endTrackedRead = method === `GET` ? await ctx.entityBridgeManager.beginClientRead(streamPath) : null;
|
|
@@ -1737,6 +2209,51 @@ async function proxyPassThrough(request, ctx) {
|
|
|
1737
2209
|
await endTrackedRead?.();
|
|
1738
2210
|
}
|
|
1739
2211
|
}
|
|
2212
|
+
async function authorizeDurableStreamAccess(request, ctx) {
|
|
2213
|
+
const method = request.method.toUpperCase();
|
|
2214
|
+
const streamPath = new URL(request.url).pathname;
|
|
2215
|
+
if (method === `GET` || method === `HEAD`) {
|
|
2216
|
+
const registry = ctx.entityManager?.registry;
|
|
2217
|
+
const entity = registry?.getEntityByStream ? await registry.getEntityByStream(streamPath) : null;
|
|
2218
|
+
if (entity) {
|
|
2219
|
+
if (await canAccessEntity(ctx, entity, `read`, request)) return void 0;
|
|
2220
|
+
return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to read ${entity.url}`);
|
|
2221
|
+
}
|
|
2222
|
+
const attachmentEntityUrl = entityUrlFromAttachmentStreamPath(streamPath);
|
|
2223
|
+
if (attachmentEntityUrl) {
|
|
2224
|
+
const attachmentEntity = registry?.getEntity ? await registry.getEntity(attachmentEntityUrl) : null;
|
|
2225
|
+
if (!attachmentEntity) return apiError(404, ErrCodeNotFound, `Entity not found`);
|
|
2226
|
+
if (await canAccessEntity(ctx, attachmentEntity, `read`, request)) return void 0;
|
|
2227
|
+
return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to read ${attachmentEntity.url}`);
|
|
2228
|
+
}
|
|
2229
|
+
}
|
|
2230
|
+
const sharedStateId = sharedStateIdFromPath(streamPath);
|
|
2231
|
+
if (!sharedStateId) return void 0;
|
|
2232
|
+
if (method === `GET` || method === `HEAD`) {
|
|
2233
|
+
if (await canAccessSharedState(ctx, sharedStateId, `read`, request)) return void 0;
|
|
2234
|
+
return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to read shared state`);
|
|
2235
|
+
}
|
|
2236
|
+
if (method === `PUT` || method === `POST`) {
|
|
2237
|
+
const ownerEntityUrl = request.headers.get(SHARED_STATE_OWNER_ENTITY_HEADER)?.trim() || void 0;
|
|
2238
|
+
if (await canAccessSharedState(ctx, sharedStateId, `write`, request, ownerEntityUrl)) return void 0;
|
|
2239
|
+
return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to write shared state`);
|
|
2240
|
+
}
|
|
2241
|
+
return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to access shared state`);
|
|
2242
|
+
}
|
|
2243
|
+
function entityUrlFromAttachmentStreamPath(path$1) {
|
|
2244
|
+
const match = path$1.match(/^\/([^/]+)\/([^/]+)\/attachments\/[^/]+$/);
|
|
2245
|
+
if (!match) return null;
|
|
2246
|
+
return `/${match[1]}/${match[2]}`;
|
|
2247
|
+
}
|
|
2248
|
+
function sharedStateIdFromPath(path$1) {
|
|
2249
|
+
const match = path$1.match(/^\/_electric\/shared-state\/([^/]+)$/);
|
|
2250
|
+
if (!match) return null;
|
|
2251
|
+
try {
|
|
2252
|
+
return decodeURIComponent(match[1]);
|
|
2253
|
+
} catch {
|
|
2254
|
+
return match[1];
|
|
2255
|
+
}
|
|
2256
|
+
}
|
|
1740
2257
|
|
|
1741
2258
|
//#endregion
|
|
1742
2259
|
//#region src/routing/electric-proxy-router.ts
|
|
@@ -1744,12 +2261,15 @@ const electricProxyRouter = Router({ base: `/_electric/electric` });
|
|
|
1744
2261
|
electricProxyRouter.get(`/*`, proxyElectric);
|
|
1745
2262
|
async function proxyElectric(request, ctx) {
|
|
1746
2263
|
if (!ctx.electricUrl) return apiError(500, `ELECTRIC_PROXY_FAILED`, `Electric URL not configured`);
|
|
2264
|
+
await ctx.entityManager.registry.pruneExpiredPermissionGrants?.();
|
|
1747
2265
|
const target = buildElectricProxyTarget({
|
|
1748
2266
|
incomingUrl: new URL(request.url),
|
|
1749
2267
|
electricUrl: ctx.electricUrl,
|
|
1750
2268
|
electricSecret: ctx.electricSecret,
|
|
1751
2269
|
tenantId: ctx.service,
|
|
1752
|
-
principalUrl: ctx.principal.url
|
|
2270
|
+
principalUrl: ctx.principal.url,
|
|
2271
|
+
principalKind: ctx.principal.kind,
|
|
2272
|
+
permissionBypass: isPermissionBypassPrincipal(ctx)
|
|
1753
2273
|
});
|
|
1754
2274
|
const headers = new Headers(request.headers);
|
|
1755
2275
|
headers.delete(`host`);
|
|
@@ -1769,110 +2289,6 @@ async function proxyElectric(request, ctx) {
|
|
|
1769
2289
|
});
|
|
1770
2290
|
}
|
|
1771
2291
|
|
|
1772
|
-
//#endregion
|
|
1773
|
-
//#region src/principal.ts
|
|
1774
|
-
const ELECTRIC_PRINCIPAL_HEADER = `electric-principal`;
|
|
1775
|
-
const PRINCIPAL_KINDS = new Set([
|
|
1776
|
-
`user`,
|
|
1777
|
-
`agent`,
|
|
1778
|
-
`service`,
|
|
1779
|
-
`system`
|
|
1780
|
-
]);
|
|
1781
|
-
function parsePrincipalKey(input) {
|
|
1782
|
-
const colon = input.indexOf(`:`);
|
|
1783
|
-
if (colon <= 0) throw new Error(`Invalid principal identifier`);
|
|
1784
|
-
const kind = input.slice(0, colon);
|
|
1785
|
-
const id = input.slice(colon + 1);
|
|
1786
|
-
if (!PRINCIPAL_KINDS.has(kind)) throw new Error(`Invalid principal kind`);
|
|
1787
|
-
if (!id || id.includes(`/`)) throw new Error(`Invalid principal id`);
|
|
1788
|
-
const key = `${kind}:${id}`;
|
|
1789
|
-
return {
|
|
1790
|
-
kind,
|
|
1791
|
-
id,
|
|
1792
|
-
key,
|
|
1793
|
-
url: `/principal/${encodeURIComponent(key)}`
|
|
1794
|
-
};
|
|
1795
|
-
}
|
|
1796
|
-
function principalUrl(key) {
|
|
1797
|
-
return parsePrincipalKey(key).url;
|
|
1798
|
-
}
|
|
1799
|
-
function parsePrincipalUrl(url) {
|
|
1800
|
-
if (!url.startsWith(`/principal/`)) return null;
|
|
1801
|
-
const segment = url.slice(`/principal/`.length);
|
|
1802
|
-
if (!segment || segment.includes(`/`)) return null;
|
|
1803
|
-
try {
|
|
1804
|
-
return parsePrincipalKey(decodeURIComponent(segment));
|
|
1805
|
-
} catch {
|
|
1806
|
-
return null;
|
|
1807
|
-
}
|
|
1808
|
-
}
|
|
1809
|
-
function parsePrincipalInput(input) {
|
|
1810
|
-
const urlPrincipal = parsePrincipalUrl(input);
|
|
1811
|
-
if (urlPrincipal) return urlPrincipal;
|
|
1812
|
-
try {
|
|
1813
|
-
return parsePrincipalKey(input);
|
|
1814
|
-
} catch {
|
|
1815
|
-
return null;
|
|
1816
|
-
}
|
|
1817
|
-
}
|
|
1818
|
-
function getPrincipalFromRequest(request) {
|
|
1819
|
-
const value = request.headers.get(ELECTRIC_PRINCIPAL_HEADER);
|
|
1820
|
-
if (!value) return null;
|
|
1821
|
-
return parsePrincipalInput(value);
|
|
1822
|
-
}
|
|
1823
|
-
function getDevPrincipal() {
|
|
1824
|
-
return parsePrincipalKey(`system:dev-local`);
|
|
1825
|
-
}
|
|
1826
|
-
const BUILT_IN_SYSTEM_PRINCIPAL_IDS = new Set([
|
|
1827
|
-
`framework`,
|
|
1828
|
-
`auth-sync`,
|
|
1829
|
-
`dev-local`
|
|
1830
|
-
]);
|
|
1831
|
-
function isBuiltInSystemPrincipalUrl(url) {
|
|
1832
|
-
if (!url?.startsWith(`/principal/`)) return false;
|
|
1833
|
-
try {
|
|
1834
|
-
const principal = parsePrincipalUrl(url);
|
|
1835
|
-
if (!principal) return false;
|
|
1836
|
-
return principal.kind === `system` && BUILT_IN_SYSTEM_PRINCIPAL_IDS.has(principal.id);
|
|
1837
|
-
} catch {
|
|
1838
|
-
return false;
|
|
1839
|
-
}
|
|
1840
|
-
}
|
|
1841
|
-
function principalFromCreatedBy(createdBy) {
|
|
1842
|
-
if (!createdBy) return void 0;
|
|
1843
|
-
const principal = parsePrincipalUrl(createdBy);
|
|
1844
|
-
if (!principal) return {
|
|
1845
|
-
url: createdBy,
|
|
1846
|
-
key: null
|
|
1847
|
-
};
|
|
1848
|
-
return {
|
|
1849
|
-
url: principal.url,
|
|
1850
|
-
key: principal.key,
|
|
1851
|
-
kind: principal.kind,
|
|
1852
|
-
id: principal.id
|
|
1853
|
-
};
|
|
1854
|
-
}
|
|
1855
|
-
const principalIdentityStateSchema = Type.Object({
|
|
1856
|
-
kind: Type.Union([
|
|
1857
|
-
Type.Literal(`user`),
|
|
1858
|
-
Type.Literal(`agent`),
|
|
1859
|
-
Type.Literal(`service`),
|
|
1860
|
-
Type.Literal(`system`)
|
|
1861
|
-
]),
|
|
1862
|
-
id: Type.String(),
|
|
1863
|
-
key: Type.String(),
|
|
1864
|
-
url: Type.String(),
|
|
1865
|
-
updated_at: Type.String(),
|
|
1866
|
-
display_name: Type.Optional(Type.String()),
|
|
1867
|
-
email: Type.Optional(Type.String()),
|
|
1868
|
-
avatar_url: Type.Optional(Type.String()),
|
|
1869
|
-
auth_provider: Type.Optional(Type.String()),
|
|
1870
|
-
auth_subject: Type.Optional(Type.String()),
|
|
1871
|
-
claims: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
|
|
1872
|
-
created_at: Type.Optional(Type.String())
|
|
1873
|
-
}, { additionalProperties: false });
|
|
1874
|
-
const principalUpdateIdentityMessageSchema = Type.Object({ identity: principalIdentityStateSchema }, { additionalProperties: false });
|
|
1875
|
-
|
|
1876
2292
|
//#endregion
|
|
1877
2293
|
//#region src/dispatch-policy-schema.ts
|
|
1878
2294
|
const nonEmptyStringSchema = Type.String({ minLength: 1 });
|
|
@@ -2046,16 +2462,26 @@ function isDuplicateUrlError(err) {
|
|
|
2046
2462
|
return e.code === `23505`;
|
|
2047
2463
|
}
|
|
2048
2464
|
const DEFAULT_RUNNER_LEASE_MS = 3e4;
|
|
2465
|
+
const PERMISSION_PRUNE_INTERVAL_MS = 3e4;
|
|
2049
2466
|
function runnerWakeStream(runnerId) {
|
|
2050
2467
|
return `/runners/${runnerId}/wake`;
|
|
2051
2468
|
}
|
|
2052
2469
|
var PostgresRegistry = class {
|
|
2470
|
+
lastPermissionPruneStartedAt = 0;
|
|
2471
|
+
permissionPrunePromise = null;
|
|
2053
2472
|
constructor(db, tenantId = DEFAULT_TENANT_ID) {
|
|
2054
2473
|
this.db = db;
|
|
2055
2474
|
this.tenantId = tenantId;
|
|
2056
2475
|
}
|
|
2057
2476
|
async initialize() {}
|
|
2058
2477
|
close() {}
|
|
2478
|
+
async ensureUserForPrincipal(principal) {
|
|
2479
|
+
if (principal.kind !== `user`) return;
|
|
2480
|
+
await this.db.insert(users).values({
|
|
2481
|
+
tenantId: this.tenantId,
|
|
2482
|
+
id: principal.id
|
|
2483
|
+
}).onConflictDoNothing();
|
|
2484
|
+
}
|
|
2059
2485
|
async createRunner(input) {
|
|
2060
2486
|
const now = new Date();
|
|
2061
2487
|
const wakeStream = input.wakeStream ?? runnerWakeStream(input.id);
|
|
@@ -2300,6 +2726,7 @@ var PostgresRegistry = class {
|
|
|
2300
2726
|
creationSchema: et.creation_schema ?? null,
|
|
2301
2727
|
inboxSchemas: et.inbox_schemas ?? null,
|
|
2302
2728
|
stateSchemas: et.state_schemas ?? null,
|
|
2729
|
+
slashCommands: et.slash_commands ?? null,
|
|
2303
2730
|
serveEndpoint: et.serve_endpoint ?? null,
|
|
2304
2731
|
defaultDispatchPolicy: et.default_dispatch_policy ?? null,
|
|
2305
2732
|
revision: et.revision,
|
|
@@ -2312,6 +2739,7 @@ var PostgresRegistry = class {
|
|
|
2312
2739
|
creationSchema: et.creation_schema ?? null,
|
|
2313
2740
|
inboxSchemas: et.inbox_schemas ?? null,
|
|
2314
2741
|
stateSchemas: et.state_schemas ?? null,
|
|
2742
|
+
slashCommands: et.slash_commands ?? null,
|
|
2315
2743
|
serveEndpoint: et.serve_endpoint ?? null,
|
|
2316
2744
|
defaultDispatchPolicy: et.default_dispatch_policy ?? null,
|
|
2317
2745
|
revision: et.revision,
|
|
@@ -2329,6 +2757,7 @@ var PostgresRegistry = class {
|
|
|
2329
2757
|
creationSchema: et.creation_schema ?? null,
|
|
2330
2758
|
inboxSchemas: et.inbox_schemas ?? null,
|
|
2331
2759
|
stateSchemas: et.state_schemas ?? null,
|
|
2760
|
+
slashCommands: et.slash_commands ?? null,
|
|
2332
2761
|
serveEndpoint: et.serve_endpoint ?? null,
|
|
2333
2762
|
defaultDispatchPolicy: et.default_dispatch_policy ?? null,
|
|
2334
2763
|
revision: et.revision,
|
|
@@ -2355,6 +2784,7 @@ var PostgresRegistry = class {
|
|
|
2355
2784
|
creationSchema: et.creation_schema ?? null,
|
|
2356
2785
|
inboxSchemas: et.inbox_schemas ?? null,
|
|
2357
2786
|
stateSchemas: et.state_schemas ?? null,
|
|
2787
|
+
slashCommands: et.slash_commands ?? null,
|
|
2358
2788
|
serveEndpoint: et.serve_endpoint ?? null,
|
|
2359
2789
|
defaultDispatchPolicy: et.default_dispatch_policy ?? null,
|
|
2360
2790
|
revision: et.revision,
|
|
@@ -2390,6 +2820,59 @@ var PostgresRegistry = class {
|
|
|
2390
2820
|
pendingSourceStreams: [],
|
|
2391
2821
|
updatedAt: new Date()
|
|
2392
2822
|
}).onConflictDoNothing();
|
|
2823
|
+
await tx.insert(entityLineage).values({
|
|
2824
|
+
tenantId: this.tenantId,
|
|
2825
|
+
ancestorUrl: entity.url,
|
|
2826
|
+
descendantUrl: entity.url,
|
|
2827
|
+
depth: 0
|
|
2828
|
+
}).onConflictDoNothing();
|
|
2829
|
+
if (entity.parent) await tx.execute(sql`
|
|
2830
|
+
INSERT INTO ${entityLineage} (
|
|
2831
|
+
tenant_id,
|
|
2832
|
+
ancestor_url,
|
|
2833
|
+
descendant_url,
|
|
2834
|
+
depth
|
|
2835
|
+
)
|
|
2836
|
+
SELECT
|
|
2837
|
+
${this.tenantId},
|
|
2838
|
+
ancestor_url,
|
|
2839
|
+
${entity.url},
|
|
2840
|
+
depth + 1
|
|
2841
|
+
FROM ${entityLineage}
|
|
2842
|
+
WHERE tenant_id = ${this.tenantId}
|
|
2843
|
+
AND descendant_url = ${entity.parent}
|
|
2844
|
+
ON CONFLICT DO NOTHING
|
|
2845
|
+
`);
|
|
2846
|
+
await tx.execute(sql`
|
|
2847
|
+
INSERT INTO ${entityEffectivePermissions} (
|
|
2848
|
+
tenant_id,
|
|
2849
|
+
entity_url,
|
|
2850
|
+
source_entity_url,
|
|
2851
|
+
source_grant_id,
|
|
2852
|
+
permission,
|
|
2853
|
+
subject_kind,
|
|
2854
|
+
subject_value,
|
|
2855
|
+
expires_at
|
|
2856
|
+
)
|
|
2857
|
+
SELECT
|
|
2858
|
+
${this.tenantId},
|
|
2859
|
+
${entity.url},
|
|
2860
|
+
grants.entity_url,
|
|
2861
|
+
grants.id,
|
|
2862
|
+
grants.permission,
|
|
2863
|
+
grants.subject_kind,
|
|
2864
|
+
grants.subject_value,
|
|
2865
|
+
grants.expires_at
|
|
2866
|
+
FROM ${entityPermissionGrants} grants
|
|
2867
|
+
JOIN ${entityLineage} lineage
|
|
2868
|
+
ON lineage.tenant_id = grants.tenant_id
|
|
2869
|
+
AND lineage.ancestor_url = grants.entity_url
|
|
2870
|
+
AND lineage.descendant_url = ${entity.url}
|
|
2871
|
+
WHERE grants.tenant_id = ${this.tenantId}
|
|
2872
|
+
AND grants.propagation = 'descendants'
|
|
2873
|
+
AND (grants.expires_at IS NULL OR grants.expires_at > now())
|
|
2874
|
+
ON CONFLICT DO NOTHING
|
|
2875
|
+
`);
|
|
2393
2876
|
return parseInt(result[0].txid);
|
|
2394
2877
|
});
|
|
2395
2878
|
} catch (err) {
|
|
@@ -2411,10 +2894,8 @@ var PostgresRegistry = class {
|
|
|
2411
2894
|
}
|
|
2412
2895
|
async getEntityByStream(streamPath) {
|
|
2413
2896
|
const mainSuffix = `/main`;
|
|
2414
|
-
const errorSuffix = `/error`;
|
|
2415
2897
|
let entityUrl = null;
|
|
2416
2898
|
if (streamPath.endsWith(mainSuffix)) entityUrl = streamPath.slice(0, -mainSuffix.length);
|
|
2417
|
-
else if (streamPath.endsWith(errorSuffix)) entityUrl = streamPath.slice(0, -errorSuffix.length);
|
|
2418
2899
|
if (!entityUrl) return null;
|
|
2419
2900
|
return this.getEntity(entityUrl);
|
|
2420
2901
|
}
|
|
@@ -2424,6 +2905,23 @@ var PostgresRegistry = class {
|
|
|
2424
2905
|
if (filter?.status) conditions.push(eq(entities.status, filter.status));
|
|
2425
2906
|
if (filter?.parent) conditions.push(eq(entities.parent, filter.parent));
|
|
2426
2907
|
if (filter?.created_by) conditions.push(eq(entities.createdBy, filter.created_by));
|
|
2908
|
+
if (filter?.readableBy && !filter.readableBy.bypass) conditions.push(sql`(
|
|
2909
|
+
${entities.createdBy} = ${filter.readableBy.principalUrl}
|
|
2910
|
+
OR ${entities.url} IN (
|
|
2911
|
+
SELECT ${entityEffectivePermissions.entityUrl}
|
|
2912
|
+
FROM ${entityEffectivePermissions}
|
|
2913
|
+
WHERE ${entityEffectivePermissions.tenantId} = ${this.tenantId}
|
|
2914
|
+
AND ${entityEffectivePermissions.permission} IN ('read', 'manage')
|
|
2915
|
+
AND (${entityEffectivePermissions.expiresAt} IS NULL OR ${entityEffectivePermissions.expiresAt} > now())
|
|
2916
|
+
AND (
|
|
2917
|
+
(${entityEffectivePermissions.subjectKind} = 'principal'
|
|
2918
|
+
AND ${entityEffectivePermissions.subjectValue} = ${filter.readableBy.principalUrl})
|
|
2919
|
+
OR
|
|
2920
|
+
(${entityEffectivePermissions.subjectKind} = 'principal_kind'
|
|
2921
|
+
AND ${entityEffectivePermissions.subjectValue} = ${filter.readableBy.principalKind})
|
|
2922
|
+
)
|
|
2923
|
+
)
|
|
2924
|
+
)`);
|
|
2427
2925
|
const whereClause = and(...conditions);
|
|
2428
2926
|
const countResult = await this.db.select({ count: sql`count(*)` }).from(entities).where(whereClause);
|
|
2429
2927
|
const total = Number(countResult[0].count);
|
|
@@ -2436,6 +2934,189 @@ var PostgresRegistry = class {
|
|
|
2436
2934
|
total
|
|
2437
2935
|
};
|
|
2438
2936
|
}
|
|
2937
|
+
async createEntityTypePermissionGrant(input) {
|
|
2938
|
+
const [row] = await this.db.insert(entityTypePermissionGrants).values({
|
|
2939
|
+
tenantId: this.tenantId,
|
|
2940
|
+
entityType: input.entityType,
|
|
2941
|
+
permission: input.permission,
|
|
2942
|
+
subjectKind: input.subjectKind,
|
|
2943
|
+
subjectValue: input.subjectValue,
|
|
2944
|
+
createdBy: input.createdBy ?? null,
|
|
2945
|
+
expiresAt: input.expiresAt ?? null
|
|
2946
|
+
}).returning();
|
|
2947
|
+
return this.rowToEntityTypePermissionGrant(row);
|
|
2948
|
+
}
|
|
2949
|
+
async ensureEntityTypePermissionGrant(input) {
|
|
2950
|
+
const [existing] = await this.db.select().from(entityTypePermissionGrants).where(and(eq(entityTypePermissionGrants.tenantId, this.tenantId), eq(entityTypePermissionGrants.entityType, input.entityType), eq(entityTypePermissionGrants.permission, input.permission), eq(entityTypePermissionGrants.subjectKind, input.subjectKind), eq(entityTypePermissionGrants.subjectValue, input.subjectValue), input.expiresAt ? eq(entityTypePermissionGrants.expiresAt, input.expiresAt) : sql`${entityTypePermissionGrants.expiresAt} IS NULL`)).limit(1);
|
|
2951
|
+
if (existing) return this.rowToEntityTypePermissionGrant(existing);
|
|
2952
|
+
return await this.createEntityTypePermissionGrant(input);
|
|
2953
|
+
}
|
|
2954
|
+
async listEntityTypePermissionGrants(entityType) {
|
|
2955
|
+
const rows = await this.db.select().from(entityTypePermissionGrants).where(and(eq(entityTypePermissionGrants.tenantId, this.tenantId), eq(entityTypePermissionGrants.entityType, entityType))).orderBy(entityTypePermissionGrants.id);
|
|
2956
|
+
return rows.map((row) => this.rowToEntityTypePermissionGrant(row));
|
|
2957
|
+
}
|
|
2958
|
+
async deleteEntityTypePermissionGrant(entityType, grantId) {
|
|
2959
|
+
const rows = await this.db.delete(entityTypePermissionGrants).where(and(eq(entityTypePermissionGrants.tenantId, this.tenantId), eq(entityTypePermissionGrants.entityType, entityType), eq(entityTypePermissionGrants.id, grantId))).returning({ id: entityTypePermissionGrants.id });
|
|
2960
|
+
return rows.length > 0;
|
|
2961
|
+
}
|
|
2962
|
+
async hasEntityTypePermission(entityType, permission, subject) {
|
|
2963
|
+
const permissions = [permission, `manage`];
|
|
2964
|
+
const rows = await this.db.select({ id: entityTypePermissionGrants.id }).from(entityTypePermissionGrants).where(and(eq(entityTypePermissionGrants.tenantId, this.tenantId), eq(entityTypePermissionGrants.entityType, entityType), inArray(entityTypePermissionGrants.permission, [...permissions]), sql`(${entityTypePermissionGrants.expiresAt} IS NULL OR ${entityTypePermissionGrants.expiresAt} > now())`, sql`(
|
|
2965
|
+
(${entityTypePermissionGrants.subjectKind} = 'principal'
|
|
2966
|
+
AND ${entityTypePermissionGrants.subjectValue} = ${subject.principalUrl})
|
|
2967
|
+
OR
|
|
2968
|
+
(${entityTypePermissionGrants.subjectKind} = 'principal_kind'
|
|
2969
|
+
AND ${entityTypePermissionGrants.subjectValue} = ${subject.principalKind})
|
|
2970
|
+
)`)).limit(1);
|
|
2971
|
+
return rows.length > 0;
|
|
2972
|
+
}
|
|
2973
|
+
async createEntityPermissionGrant(input) {
|
|
2974
|
+
return await this.db.transaction(async (tx) => {
|
|
2975
|
+
const [row] = await tx.insert(entityPermissionGrants).values({
|
|
2976
|
+
tenantId: this.tenantId,
|
|
2977
|
+
entityUrl: input.entityUrl,
|
|
2978
|
+
permission: input.permission,
|
|
2979
|
+
subjectKind: input.subjectKind,
|
|
2980
|
+
subjectValue: input.subjectValue,
|
|
2981
|
+
propagation: input.propagation ?? `self`,
|
|
2982
|
+
copyToChildren: input.copyToChildren ?? false,
|
|
2983
|
+
createdBy: input.createdBy ?? null,
|
|
2984
|
+
expiresAt: input.expiresAt ?? null
|
|
2985
|
+
}).returning();
|
|
2986
|
+
await this.materializeEntityPermissionGrant(tx, row);
|
|
2987
|
+
return this.rowToEntityPermissionGrant(row);
|
|
2988
|
+
});
|
|
2989
|
+
}
|
|
2990
|
+
async listEntityPermissionGrants(entityUrl) {
|
|
2991
|
+
const rows = await this.db.select().from(entityPermissionGrants).where(and(eq(entityPermissionGrants.tenantId, this.tenantId), eq(entityPermissionGrants.entityUrl, entityUrl))).orderBy(entityPermissionGrants.id);
|
|
2992
|
+
return rows.map((row) => this.rowToEntityPermissionGrant(row));
|
|
2993
|
+
}
|
|
2994
|
+
async deleteEntityPermissionGrant(entityUrl, grantId) {
|
|
2995
|
+
return await this.db.transaction(async (tx) => {
|
|
2996
|
+
await tx.delete(entityEffectivePermissions).where(and(eq(entityEffectivePermissions.tenantId, this.tenantId), eq(entityEffectivePermissions.sourceGrantId, grantId)));
|
|
2997
|
+
const rows = await tx.delete(entityPermissionGrants).where(and(eq(entityPermissionGrants.tenantId, this.tenantId), eq(entityPermissionGrants.entityUrl, entityUrl), eq(entityPermissionGrants.id, grantId))).returning({ id: entityPermissionGrants.id });
|
|
2998
|
+
return rows.length > 0;
|
|
2999
|
+
});
|
|
3000
|
+
}
|
|
3001
|
+
async copyEntityPermissionGrantsForSpawn(parentEntityUrl, childEntityUrl, createdBy) {
|
|
3002
|
+
const parentGrants = await this.db.select().from(entityPermissionGrants).where(and(eq(entityPermissionGrants.tenantId, this.tenantId), eq(entityPermissionGrants.entityUrl, parentEntityUrl), eq(entityPermissionGrants.copyToChildren, true), sql`(${entityPermissionGrants.expiresAt} IS NULL OR ${entityPermissionGrants.expiresAt} > now())`));
|
|
3003
|
+
const copied = [];
|
|
3004
|
+
for (const grant of parentGrants) copied.push(await this.createEntityPermissionGrant({
|
|
3005
|
+
entityUrl: childEntityUrl,
|
|
3006
|
+
permission: grant.permission,
|
|
3007
|
+
subjectKind: grant.subjectKind,
|
|
3008
|
+
subjectValue: grant.subjectValue,
|
|
3009
|
+
propagation: `self`,
|
|
3010
|
+
copyToChildren: grant.copyToChildren,
|
|
3011
|
+
createdBy,
|
|
3012
|
+
expiresAt: grant.expiresAt ?? void 0
|
|
3013
|
+
}));
|
|
3014
|
+
return copied;
|
|
3015
|
+
}
|
|
3016
|
+
async hasEntityPermission(entityUrl, permission, subject) {
|
|
3017
|
+
const permissions = [permission, `manage`];
|
|
3018
|
+
const rows = await this.db.select({ id: entityEffectivePermissions.id }).from(entityEffectivePermissions).where(and(eq(entityEffectivePermissions.tenantId, this.tenantId), eq(entityEffectivePermissions.entityUrl, entityUrl), inArray(entityEffectivePermissions.permission, [...permissions]), sql`(${entityEffectivePermissions.expiresAt} IS NULL OR ${entityEffectivePermissions.expiresAt} > now())`, sql`(
|
|
3019
|
+
(${entityEffectivePermissions.subjectKind} = 'principal'
|
|
3020
|
+
AND ${entityEffectivePermissions.subjectValue} = ${subject.principalUrl})
|
|
3021
|
+
OR
|
|
3022
|
+
(${entityEffectivePermissions.subjectKind} = 'principal_kind'
|
|
3023
|
+
AND ${entityEffectivePermissions.subjectValue} = ${subject.principalKind})
|
|
3024
|
+
)`)).limit(1);
|
|
3025
|
+
return rows.length > 0;
|
|
3026
|
+
}
|
|
3027
|
+
async replaceSharedStateLink(ownerEntityUrl, manifestKey, sharedStateId) {
|
|
3028
|
+
await this.db.delete(sharedStateLinks).where(and(eq(sharedStateLinks.tenantId, this.tenantId), eq(sharedStateLinks.ownerEntityUrl, ownerEntityUrl), eq(sharedStateLinks.manifestKey, manifestKey)));
|
|
3029
|
+
if (!sharedStateId) return;
|
|
3030
|
+
await this.db.insert(sharedStateLinks).values({
|
|
3031
|
+
tenantId: this.tenantId,
|
|
3032
|
+
ownerEntityUrl,
|
|
3033
|
+
manifestKey,
|
|
3034
|
+
sharedStateId
|
|
3035
|
+
}).onConflictDoUpdate({
|
|
3036
|
+
target: [
|
|
3037
|
+
sharedStateLinks.tenantId,
|
|
3038
|
+
sharedStateLinks.ownerEntityUrl,
|
|
3039
|
+
sharedStateLinks.manifestKey
|
|
3040
|
+
],
|
|
3041
|
+
set: {
|
|
3042
|
+
sharedStateId,
|
|
3043
|
+
updatedAt: new Date()
|
|
3044
|
+
}
|
|
3045
|
+
});
|
|
3046
|
+
}
|
|
3047
|
+
async listSharedStateLinkedEntityUrls(sharedStateId) {
|
|
3048
|
+
const rows = await this.db.selectDistinct({ ownerEntityUrl: sharedStateLinks.ownerEntityUrl }).from(sharedStateLinks).where(and(eq(sharedStateLinks.tenantId, this.tenantId), eq(sharedStateLinks.sharedStateId, sharedStateId)));
|
|
3049
|
+
return rows.map((row) => row.ownerEntityUrl);
|
|
3050
|
+
}
|
|
3051
|
+
async pruneExpiredPermissionGrants(now = new Date(), options = {}) {
|
|
3052
|
+
if (this.permissionPrunePromise) return await this.permissionPrunePromise;
|
|
3053
|
+
const startedAt = Date.now();
|
|
3054
|
+
if (!options.force && startedAt - this.lastPermissionPruneStartedAt < PERMISSION_PRUNE_INTERVAL_MS) return;
|
|
3055
|
+
this.lastPermissionPruneStartedAt = startedAt;
|
|
3056
|
+
const promise = this.pruneExpiredPermissionGrantsNow(now);
|
|
3057
|
+
this.permissionPrunePromise = promise;
|
|
3058
|
+
try {
|
|
3059
|
+
await promise;
|
|
3060
|
+
} catch (error) {
|
|
3061
|
+
this.lastPermissionPruneStartedAt = 0;
|
|
3062
|
+
throw error;
|
|
3063
|
+
} finally {
|
|
3064
|
+
if (this.permissionPrunePromise === promise) this.permissionPrunePromise = null;
|
|
3065
|
+
}
|
|
3066
|
+
}
|
|
3067
|
+
async pruneExpiredPermissionGrantsNow(now) {
|
|
3068
|
+
await this.db.transaction(async (tx) => {
|
|
3069
|
+
const expiredEntityGrantIds = await tx.select({ id: entityPermissionGrants.id }).from(entityPermissionGrants).where(and(eq(entityPermissionGrants.tenantId, this.tenantId), sql`${entityPermissionGrants.expiresAt} IS NOT NULL`, lt(entityPermissionGrants.expiresAt, now)));
|
|
3070
|
+
const ids = expiredEntityGrantIds.map((row) => row.id);
|
|
3071
|
+
if (ids.length > 0) {
|
|
3072
|
+
await tx.delete(entityEffectivePermissions).where(and(eq(entityEffectivePermissions.tenantId, this.tenantId), inArray(entityEffectivePermissions.sourceGrantId, ids)));
|
|
3073
|
+
await tx.delete(entityPermissionGrants).where(and(eq(entityPermissionGrants.tenantId, this.tenantId), inArray(entityPermissionGrants.id, ids)));
|
|
3074
|
+
}
|
|
3075
|
+
await tx.delete(entityEffectivePermissions).where(and(eq(entityEffectivePermissions.tenantId, this.tenantId), sql`${entityEffectivePermissions.expiresAt} IS NOT NULL`, lt(entityEffectivePermissions.expiresAt, now)));
|
|
3076
|
+
await tx.delete(entityTypePermissionGrants).where(and(eq(entityTypePermissionGrants.tenantId, this.tenantId), sql`${entityTypePermissionGrants.expiresAt} IS NOT NULL`, lt(entityTypePermissionGrants.expiresAt, now)));
|
|
3077
|
+
});
|
|
3078
|
+
}
|
|
3079
|
+
async materializeEntityPermissionGrant(tx, grant) {
|
|
3080
|
+
await tx.delete(entityEffectivePermissions).where(and(eq(entityEffectivePermissions.tenantId, this.tenantId), eq(entityEffectivePermissions.sourceGrantId, grant.id)));
|
|
3081
|
+
if (grant.propagation === `descendants`) {
|
|
3082
|
+
await tx.execute(sql`
|
|
3083
|
+
INSERT INTO ${entityEffectivePermissions} (
|
|
3084
|
+
tenant_id,
|
|
3085
|
+
entity_url,
|
|
3086
|
+
source_entity_url,
|
|
3087
|
+
source_grant_id,
|
|
3088
|
+
permission,
|
|
3089
|
+
subject_kind,
|
|
3090
|
+
subject_value,
|
|
3091
|
+
expires_at
|
|
3092
|
+
)
|
|
3093
|
+
SELECT
|
|
3094
|
+
${this.tenantId},
|
|
3095
|
+
descendant_url,
|
|
3096
|
+
${grant.entityUrl},
|
|
3097
|
+
${grant.id},
|
|
3098
|
+
${grant.permission},
|
|
3099
|
+
${grant.subjectKind},
|
|
3100
|
+
${grant.subjectValue},
|
|
3101
|
+
${grant.expiresAt}
|
|
3102
|
+
FROM ${entityLineage}
|
|
3103
|
+
WHERE tenant_id = ${this.tenantId}
|
|
3104
|
+
AND ancestor_url = ${grant.entityUrl}
|
|
3105
|
+
ON CONFLICT DO NOTHING
|
|
3106
|
+
`);
|
|
3107
|
+
return;
|
|
3108
|
+
}
|
|
3109
|
+
await tx.insert(entityEffectivePermissions).values({
|
|
3110
|
+
tenantId: this.tenantId,
|
|
3111
|
+
entityUrl: grant.entityUrl,
|
|
3112
|
+
sourceEntityUrl: grant.entityUrl,
|
|
3113
|
+
sourceGrantId: grant.id,
|
|
3114
|
+
permission: grant.permission,
|
|
3115
|
+
subjectKind: grant.subjectKind,
|
|
3116
|
+
subjectValue: grant.subjectValue,
|
|
3117
|
+
expiresAt: grant.expiresAt
|
|
3118
|
+
}).onConflictDoNothing();
|
|
3119
|
+
}
|
|
2439
3120
|
async updateStatus(entityUrl, status$1) {
|
|
2440
3121
|
const whereClause = isTerminalEntityStatus(status$1) ? this.entityWhere(entityUrl) : and(this.entityWhere(entityUrl), ne(entities.status, `stopped`), ne(entities.status, `killed`));
|
|
2441
3122
|
await this.db.update(entities).set({
|
|
@@ -2537,7 +3218,9 @@ var PostgresRegistry = class {
|
|
|
2537
3218
|
tenantId: this.tenantId,
|
|
2538
3219
|
sourceRef: row.sourceRef,
|
|
2539
3220
|
tags: normalizeTags(row.tags),
|
|
2540
|
-
streamUrl: row.streamUrl
|
|
3221
|
+
streamUrl: row.streamUrl,
|
|
3222
|
+
principalUrl: row.principalUrl,
|
|
3223
|
+
principalKind: row.principalKind
|
|
2541
3224
|
}).onConflictDoNothing();
|
|
2542
3225
|
const existing = await this.getEntityBridge(row.sourceRef);
|
|
2543
3226
|
if (!existing) throw new Error(`Failed to load entity bridge ${row.sourceRef}`);
|
|
@@ -2692,6 +3375,7 @@ var PostgresRegistry = class {
|
|
|
2692
3375
|
creation_schema: row.creationSchema,
|
|
2693
3376
|
inbox_schemas: row.inboxSchemas,
|
|
2694
3377
|
state_schemas: row.stateSchemas,
|
|
3378
|
+
slash_commands: row.slashCommands ?? void 0,
|
|
2695
3379
|
serve_endpoint: row.serveEndpoint ?? void 0,
|
|
2696
3380
|
default_dispatch_policy: row.defaultDispatchPolicy ?? void 0,
|
|
2697
3381
|
revision: row.revision,
|
|
@@ -2699,15 +3383,40 @@ var PostgresRegistry = class {
|
|
|
2699
3383
|
updated_at: row.updatedAt
|
|
2700
3384
|
};
|
|
2701
3385
|
}
|
|
3386
|
+
rowToEntityTypePermissionGrant(row) {
|
|
3387
|
+
return {
|
|
3388
|
+
id: row.id,
|
|
3389
|
+
entity_type: row.entityType,
|
|
3390
|
+
permission: row.permission,
|
|
3391
|
+
subject_kind: row.subjectKind,
|
|
3392
|
+
subject_value: row.subjectValue,
|
|
3393
|
+
created_by: row.createdBy ?? void 0,
|
|
3394
|
+
expires_at: row.expiresAt?.toISOString(),
|
|
3395
|
+
created_at: row.createdAt.toISOString(),
|
|
3396
|
+
updated_at: row.updatedAt.toISOString()
|
|
3397
|
+
};
|
|
3398
|
+
}
|
|
3399
|
+
rowToEntityPermissionGrant(row) {
|
|
3400
|
+
return {
|
|
3401
|
+
id: row.id,
|
|
3402
|
+
entity_url: row.entityUrl,
|
|
3403
|
+
permission: row.permission,
|
|
3404
|
+
subject_kind: row.subjectKind,
|
|
3405
|
+
subject_value: row.subjectValue,
|
|
3406
|
+
propagation: row.propagation,
|
|
3407
|
+
copy_to_children: row.copyToChildren,
|
|
3408
|
+
created_by: row.createdBy ?? void 0,
|
|
3409
|
+
expires_at: row.expiresAt?.toISOString(),
|
|
3410
|
+
created_at: row.createdAt.toISOString(),
|
|
3411
|
+
updated_at: row.updatedAt.toISOString()
|
|
3412
|
+
};
|
|
3413
|
+
}
|
|
2702
3414
|
rowToEntity(row) {
|
|
2703
3415
|
return {
|
|
2704
3416
|
url: row.url,
|
|
2705
3417
|
type: row.type,
|
|
2706
3418
|
status: assertEntityStatus(row.status),
|
|
2707
|
-
streams: {
|
|
2708
|
-
main: `${row.url}/main`,
|
|
2709
|
-
error: `${row.url}/error`
|
|
2710
|
-
},
|
|
3419
|
+
streams: { main: `${row.url}/main` },
|
|
2711
3420
|
subscription_id: row.subscriptionId,
|
|
2712
3421
|
dispatch_policy: row.dispatchPolicy ?? void 0,
|
|
2713
3422
|
write_token: row.writeToken,
|
|
@@ -2729,6 +3438,8 @@ var PostgresRegistry = class {
|
|
|
2729
3438
|
sourceRef: row.sourceRef,
|
|
2730
3439
|
tags: row.tags ?? {},
|
|
2731
3440
|
streamUrl: row.streamUrl,
|
|
3441
|
+
principalUrl: row.principalUrl ?? void 0,
|
|
3442
|
+
principalKind: row.principalKind ?? void 0,
|
|
2732
3443
|
shapeHandle: row.shapeHandle ?? void 0,
|
|
2733
3444
|
shapeOffset: row.shapeOffset ?? void 0,
|
|
2734
3445
|
lastObserverActivityAt: row.lastObserverActivityAt,
|
|
@@ -2991,6 +3702,7 @@ var EntityManager = class {
|
|
|
2991
3702
|
this.validateSchema(req.creation_schema);
|
|
2992
3703
|
this.validateSchemaMap(req.inbox_schemas);
|
|
2993
3704
|
this.validateSchemaMap(req.state_schemas);
|
|
3705
|
+
this.validateSlashCommands(req.slash_commands);
|
|
2994
3706
|
const defaultDispatchPolicy = req.default_dispatch_policy ? this.validateDispatchPolicy(req.default_dispatch_policy, { label: `default_dispatch_policy` }) : void 0;
|
|
2995
3707
|
const existing = await this.registry.getEntityType(req.name);
|
|
2996
3708
|
const now = new Date().toISOString();
|
|
@@ -3000,6 +3712,7 @@ var EntityManager = class {
|
|
|
3000
3712
|
creation_schema: req.creation_schema,
|
|
3001
3713
|
inbox_schemas: req.inbox_schemas,
|
|
3002
3714
|
state_schemas: req.state_schemas,
|
|
3715
|
+
slash_commands: req.slash_commands,
|
|
3003
3716
|
serve_endpoint: req.serve_endpoint,
|
|
3004
3717
|
default_dispatch_policy: defaultDispatchPolicy,
|
|
3005
3718
|
revision: existing ? existing.revision + 1 : 1,
|
|
@@ -3031,7 +3744,10 @@ var EntityManager = class {
|
|
|
3031
3744
|
}
|
|
3032
3745
|
async ensurePrincipal(principal) {
|
|
3033
3746
|
const existing = await this.registry.getEntity(principal.url);
|
|
3034
|
-
if (existing)
|
|
3747
|
+
if (existing) {
|
|
3748
|
+
await this.ensureUserPrincipal(principal);
|
|
3749
|
+
return existing;
|
|
3750
|
+
}
|
|
3035
3751
|
await this.ensurePrincipalEntityType();
|
|
3036
3752
|
try {
|
|
3037
3753
|
const entity = await this.spawn(`principal`, {
|
|
@@ -3060,15 +3776,22 @@ var EntityManager = class {
|
|
|
3060
3776
|
updated_at: now
|
|
3061
3777
|
}
|
|
3062
3778
|
}));
|
|
3779
|
+
await this.ensureUserPrincipal(principal);
|
|
3063
3780
|
return entity;
|
|
3064
3781
|
} catch (error) {
|
|
3065
3782
|
if (error instanceof ElectricAgentsError && error.code === ErrCodeDuplicateURL) {
|
|
3066
3783
|
const raced = await this.registry.getEntity(principal.url);
|
|
3067
|
-
if (raced)
|
|
3784
|
+
if (raced) {
|
|
3785
|
+
await this.ensureUserPrincipal(principal);
|
|
3786
|
+
return raced;
|
|
3787
|
+
}
|
|
3068
3788
|
}
|
|
3069
3789
|
throw error;
|
|
3070
3790
|
}
|
|
3071
3791
|
}
|
|
3792
|
+
async ensureUserPrincipal(principal) {
|
|
3793
|
+
if (principal.kind === `user`) await this.registry.ensureUserForPrincipal(principal);
|
|
3794
|
+
}
|
|
3072
3795
|
/**
|
|
3073
3796
|
* Spawn a new entity of the given type with durable streams.
|
|
3074
3797
|
*/
|
|
@@ -3098,7 +3821,6 @@ var EntityManager = class {
|
|
|
3098
3821
|
const writeToken = randomUUID();
|
|
3099
3822
|
const entityURL = typeName === `principal` ? principalUrl(instanceId) : `/${typeName}/${instanceId}`;
|
|
3100
3823
|
const mainPath = `${entityURL}/main`;
|
|
3101
|
-
const errorPath = `${entityURL}/error`;
|
|
3102
3824
|
const subscriptionId = `${typeName}-handler`;
|
|
3103
3825
|
const spawnT0 = performance.now();
|
|
3104
3826
|
const existingByURL = await this.registry.getEntity(entityURL);
|
|
@@ -3115,10 +3837,7 @@ var EntityManager = class {
|
|
|
3115
3837
|
type: typeName,
|
|
3116
3838
|
status: `idle`,
|
|
3117
3839
|
url: entityURL,
|
|
3118
|
-
streams: {
|
|
3119
|
-
main: mainPath,
|
|
3120
|
-
error: errorPath
|
|
3121
|
-
},
|
|
3840
|
+
streams: { main: mainPath },
|
|
3122
3841
|
subscription_id: subscriptionId,
|
|
3123
3842
|
dispatch_policy: dispatchPolicy,
|
|
3124
3843
|
write_token: writeToken,
|
|
@@ -3155,6 +3874,18 @@ var EntityManager = class {
|
|
|
3155
3874
|
}
|
|
3156
3875
|
});
|
|
3157
3876
|
const initialEvents = [createdEvent];
|
|
3877
|
+
const slashCommandTimestamp = new Date().toISOString();
|
|
3878
|
+
for (const command of entityType.slash_commands ?? []) {
|
|
3879
|
+
const slashCommandEvent = entityStateSchema.slashCommands.insert({
|
|
3880
|
+
key: command.name,
|
|
3881
|
+
value: {
|
|
3882
|
+
...command,
|
|
3883
|
+
source: `static`,
|
|
3884
|
+
updated_at: slashCommandTimestamp
|
|
3885
|
+
}
|
|
3886
|
+
});
|
|
3887
|
+
initialEvents.push(slashCommandEvent);
|
|
3888
|
+
}
|
|
3158
3889
|
if (req.initialMessage !== void 0) {
|
|
3159
3890
|
const msgNow = new Date().toISOString();
|
|
3160
3891
|
const inboxEvent = entityStateSchema.inbox.insert({
|
|
@@ -3162,6 +3893,7 @@ var EntityManager = class {
|
|
|
3162
3893
|
value: {
|
|
3163
3894
|
from: req.created_by ?? req.parent ?? `spawn`,
|
|
3164
3895
|
payload: req.initialMessage,
|
|
3896
|
+
message_type: req.initialMessageType,
|
|
3165
3897
|
timestamp: msgNow
|
|
3166
3898
|
}
|
|
3167
3899
|
});
|
|
@@ -3171,55 +3903,43 @@ var EntityManager = class {
|
|
|
3171
3903
|
const queueEnterT0 = performance.now();
|
|
3172
3904
|
const queueWaiting = this.spawnPersistQueue.length();
|
|
3173
3905
|
const queueRunning = this.spawnPersistQueue.running();
|
|
3174
|
-
const [mainStreamResult,
|
|
3906
|
+
const [mainStreamResult, entityResult] = await this.spawnPersistQueue.push(async () => {
|
|
3175
3907
|
let entityTxid;
|
|
3176
3908
|
try {
|
|
3177
3909
|
entityTxid = await withSpan(`db.createEntity`, () => this.registry.createEntity(entityData));
|
|
3178
3910
|
} catch (err) {
|
|
3179
|
-
return [
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
value: void 0
|
|
3187
|
-
},
|
|
3188
|
-
{
|
|
3189
|
-
status: `rejected`,
|
|
3190
|
-
reason: err
|
|
3191
|
-
}
|
|
3192
|
-
];
|
|
3911
|
+
return [{
|
|
3912
|
+
status: `fulfilled`,
|
|
3913
|
+
value: void 0
|
|
3914
|
+
}, {
|
|
3915
|
+
status: `rejected`,
|
|
3916
|
+
reason: err
|
|
3917
|
+
}];
|
|
3193
3918
|
}
|
|
3194
|
-
const [mainStreamResult$1
|
|
3919
|
+
const [mainStreamResult$1] = await Promise.allSettled([this.streamClient.create(mainPath, {
|
|
3195
3920
|
contentType,
|
|
3196
3921
|
body: initialBody
|
|
3197
|
-
})
|
|
3198
|
-
return [
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
status: `fulfilled`,
|
|
3203
|
-
value: entityTxid
|
|
3204
|
-
}
|
|
3205
|
-
];
|
|
3922
|
+
})]);
|
|
3923
|
+
return [mainStreamResult$1, {
|
|
3924
|
+
status: `fulfilled`,
|
|
3925
|
+
value: entityTxid
|
|
3926
|
+
}];
|
|
3206
3927
|
});
|
|
3207
3928
|
const parallelMs = +(performance.now() - queueEnterT0).toFixed(2);
|
|
3208
|
-
if (mainStreamResult.status === `rejected` ||
|
|
3929
|
+
if (mainStreamResult.status === `rejected` || entityResult.status === `rejected`) {
|
|
3209
3930
|
const entityReason = entityResult.status === `rejected` ? entityResult.reason : null;
|
|
3210
|
-
const streamReason = mainStreamResult.status === `rejected` ? mainStreamResult.reason :
|
|
3931
|
+
const streamReason = mainStreamResult.status === `rejected` ? mainStreamResult.reason : null;
|
|
3211
3932
|
const isDuplicate = entityReason instanceof EntityAlreadyExistsError;
|
|
3212
3933
|
const isStreamConflict = !!streamReason && typeof streamReason === `object` && (`status` in streamReason && streamReason.status === 409 || `code` in streamReason && streamReason.code === `CONFLICT_SEQ`);
|
|
3213
3934
|
const rollbacks = [];
|
|
3214
3935
|
if (!isDuplicate && !isStreamConflict) {
|
|
3215
3936
|
if (mainStreamResult.status === `fulfilled`) rollbacks.push(this.streamClient.delete(mainPath));
|
|
3216
|
-
if (errorStreamResult.status === `fulfilled`) rollbacks.push(this.streamClient.delete(errorPath));
|
|
3217
3937
|
if (entityResult.status === `fulfilled`) rollbacks.push(this.registry.deleteEntity(entityURL));
|
|
3218
3938
|
if (req.wake) rollbacks.push(this.wakeRegistry.unregisterBySubscriberAndSource(req.wake.subscriberUrl, entityURL, this.tenantId));
|
|
3219
3939
|
await Promise.allSettled(rollbacks);
|
|
3220
3940
|
}
|
|
3221
3941
|
if (isDuplicate || isStreamConflict) throw new ElectricAgentsError(ErrCodeDuplicateURL, `Entity already exists at URL "${entityURL}"`, 409);
|
|
3222
|
-
const failure = mainStreamResult.status === `rejected` ? mainStreamResult.reason :
|
|
3942
|
+
const failure = mainStreamResult.status === `rejected` ? mainStreamResult.reason : entityResult.reason;
|
|
3223
3943
|
if (failure instanceof Error) throw failure;
|
|
3224
3944
|
throw new ElectricAgentsError(`SPAWN_FAILED`, `Spawn failed: ${String(failure)}`, 500);
|
|
3225
3945
|
}
|
|
@@ -3304,7 +4024,7 @@ var EntityManager = class {
|
|
|
3304
4024
|
});
|
|
3305
4025
|
const sharedStateIdMap = await this.buildForkSharedStateIdMap(snapshot.sharedStateIds, suffix);
|
|
3306
4026
|
const stringMap = this.buildForkStringMap(entityUrlMap, sharedStateIdMap);
|
|
3307
|
-
const entityPlans = this.buildForkEntityPlans(effectiveSubtree, entityUrlMap, stringMap);
|
|
4027
|
+
const entityPlans = this.buildForkEntityPlans(effectiveSubtree, entityUrlMap, stringMap, opts.createdBy);
|
|
3308
4028
|
this.addForkLocks(this.forkWriteLockedEntities, effectiveSubtree.map((entity) => entity.url), writeEntityLocks);
|
|
3309
4029
|
this.addForkLocks(this.forkWriteLockedStreams, [...snapshot.sharedStateIds].map((id) => getSharedStateStreamPath(id)), writeStreamLocks);
|
|
3310
4030
|
const createdStreams = [];
|
|
@@ -3315,8 +4035,6 @@ var EntityManager = class {
|
|
|
3315
4035
|
const isRoot = plan.source.url === rootUrl;
|
|
3316
4036
|
await this.streamClient.fork(plan.fork.streams.main, plan.source.streams.main, isRoot && opts.forkPointer ? { forkPointer: opts.forkPointer } : void 0);
|
|
3317
4037
|
createdStreams.push(plan.fork.streams.main);
|
|
3318
|
-
await this.streamClient.fork(plan.fork.streams.error, plan.source.streams.error);
|
|
3319
|
-
createdStreams.push(plan.fork.streams.error);
|
|
3320
4038
|
}
|
|
3321
4039
|
for (const [sourceId, forkId] of sharedStateIdMap) {
|
|
3322
4040
|
const sourcePath = getSharedStateStreamPath(sourceId);
|
|
@@ -3650,7 +4368,6 @@ var EntityManager = class {
|
|
|
3650
4368
|
for (const [sourceUrl, forkUrl] of entityUrlMap) {
|
|
3651
4369
|
stringMap.set(sourceUrl, forkUrl);
|
|
3652
4370
|
stringMap.set(`${sourceUrl}/main`, `${forkUrl}/main`);
|
|
3653
|
-
stringMap.set(`${sourceUrl}/error`, `${forkUrl}/error`);
|
|
3654
4371
|
}
|
|
3655
4372
|
for (const [sourceId, forkId] of sharedStateIdMap) {
|
|
3656
4373
|
stringMap.set(sourceId, forkId);
|
|
@@ -3658,7 +4375,7 @@ var EntityManager = class {
|
|
|
3658
4375
|
}
|
|
3659
4376
|
return stringMap;
|
|
3660
4377
|
}
|
|
3661
|
-
buildForkEntityPlans(entitiesToFork, entityUrlMap, stringMap) {
|
|
4378
|
+
buildForkEntityPlans(entitiesToFork, entityUrlMap, stringMap, createdBy) {
|
|
3662
4379
|
const now = Date.now();
|
|
3663
4380
|
return entitiesToFork.map((source) => {
|
|
3664
4381
|
const forkUrl = entityUrlMap.get(source.url);
|
|
@@ -3671,14 +4388,12 @@ var EntityManager = class {
|
|
|
3671
4388
|
url: forkUrl,
|
|
3672
4389
|
type,
|
|
3673
4390
|
status: `idle`,
|
|
3674
|
-
streams: {
|
|
3675
|
-
main: `${forkUrl}/main`,
|
|
3676
|
-
error: `${forkUrl}/error`
|
|
3677
|
-
},
|
|
4391
|
+
streams: { main: `${forkUrl}/main` },
|
|
3678
4392
|
subscription_id: `${type}-handler`,
|
|
3679
4393
|
write_token: randomUUID(),
|
|
3680
4394
|
spawn_args: spawnArgs,
|
|
3681
4395
|
parent,
|
|
4396
|
+
created_by: createdBy ?? source.created_by,
|
|
3682
4397
|
created_at: now,
|
|
3683
4398
|
updated_at: now
|
|
3684
4399
|
};
|
|
@@ -3912,7 +4627,7 @@ var EntityManager = class {
|
|
|
3912
4627
|
}
|
|
3913
4628
|
async materializeForkManifestSideEffects(entityUrl, manifests) {
|
|
3914
4629
|
for (const [manifestKey, manifest] of manifests) {
|
|
3915
|
-
await this.
|
|
4630
|
+
await this.syncManifestLinks(entityUrl, manifestKey, `upsert`, manifest);
|
|
3916
4631
|
const wake = buildManifestWakeRegistration(entityUrl, manifest, manifestKey);
|
|
3917
4632
|
if (wake) await this.wakeRegistry.register({
|
|
3918
4633
|
...wake,
|
|
@@ -3942,6 +4657,7 @@ var EntityManager = class {
|
|
|
3942
4657
|
await this.scheduler.syncManifestDelayedSend(ownerEntityUrl, manifestKey, {
|
|
3943
4658
|
entityUrl: targetUrl,
|
|
3944
4659
|
from: senderUrl,
|
|
4660
|
+
from_agent: senderUrl,
|
|
3945
4661
|
payload: manifest.payload,
|
|
3946
4662
|
key: `scheduled-${producerId}`,
|
|
3947
4663
|
type: typeof manifest.messageType === `string` ? manifest.messageType : void 0,
|
|
@@ -3981,12 +4697,14 @@ var EntityManager = class {
|
|
|
3981
4697
|
const now = new Date().toISOString();
|
|
3982
4698
|
const key = req.key ?? `msg-in-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
3983
4699
|
const value = {
|
|
3984
|
-
from: req.from,
|
|
4700
|
+
from: req.from_principal ?? req.from,
|
|
3985
4701
|
payload: req.payload,
|
|
3986
4702
|
timestamp: now,
|
|
3987
4703
|
mode: req.mode ?? `immediate`,
|
|
3988
4704
|
status: req.mode === `queued` || req.mode === `paused` ? `pending` : `processed`
|
|
3989
4705
|
};
|
|
4706
|
+
if (req.from_principal) value.from_principal = req.from_principal;
|
|
4707
|
+
if (req.from_agent) value.from_agent = req.from_agent;
|
|
3990
4708
|
if (req.type) value.message_type = req.type;
|
|
3991
4709
|
if (req.position) value.position = req.position;
|
|
3992
4710
|
else if (value.mode === `queued` || value.mode === `paused`) value.position = createInitialQueuePosition(new Date(now));
|
|
@@ -4158,9 +4876,9 @@ var EntityManager = class {
|
|
|
4158
4876
|
if (result.changed && this.entityBridgeManager) await this.entityBridgeManager.onEntityChanged(entityUrl);
|
|
4159
4877
|
return updated;
|
|
4160
4878
|
}
|
|
4161
|
-
async ensureEntitiesMembershipStream(tags) {
|
|
4879
|
+
async ensureEntitiesMembershipStream(tags, principal) {
|
|
4162
4880
|
if (!this.entityBridgeManager) throw new Error(`Entity bridge manager not configured`);
|
|
4163
|
-
return this.entityBridgeManager.register(this.validateTags(tags));
|
|
4881
|
+
return this.entityBridgeManager.register(this.validateTags(tags), principal.url, principal.kind);
|
|
4164
4882
|
}
|
|
4165
4883
|
async writeManifestEntry(entityUrl, key, operation, value, opts) {
|
|
4166
4884
|
const entity = await this.registry.getEntity(entityUrl);
|
|
@@ -4178,11 +4896,11 @@ var EntityManager = class {
|
|
|
4178
4896
|
const encoded = this.encodeChangeEvent(event);
|
|
4179
4897
|
if (opts?.producerId) {
|
|
4180
4898
|
await this.streamClient.appendIdempotent(entity.streams.main, encoded, { producerId: opts.producerId });
|
|
4181
|
-
await this.
|
|
4899
|
+
await this.syncManifestLinks(entityUrl, key, operation, value);
|
|
4182
4900
|
return;
|
|
4183
4901
|
}
|
|
4184
4902
|
await this.streamClient.append(entity.streams.main, encoded);
|
|
4185
|
-
await this.
|
|
4903
|
+
await this.syncManifestLinks(entityUrl, key, operation, value);
|
|
4186
4904
|
}
|
|
4187
4905
|
async upsertCronSchedule(entityUrl, req) {
|
|
4188
4906
|
if (req.payload === void 0) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Missing required field: payload`, 400);
|
|
@@ -4331,6 +5049,8 @@ var EntityManager = class {
|
|
|
4331
5049
|
await this.scheduler.enqueueDelayedSend({
|
|
4332
5050
|
entityUrl,
|
|
4333
5051
|
from: req.from,
|
|
5052
|
+
from_principal: req.from_principal,
|
|
5053
|
+
from_agent: req.from_agent,
|
|
4334
5054
|
payload: req.payload,
|
|
4335
5055
|
key: req.key,
|
|
4336
5056
|
type: req.type,
|
|
@@ -4373,14 +5093,23 @@ var EntityManager = class {
|
|
|
4373
5093
|
await this.streamClient.appendIdempotent(subscriber.streams.main, this.encodeChangeEvent(wakeEvent), { producerId: `wake-reg-${result.registrationDbId}-${result.sourceEventKey}` });
|
|
4374
5094
|
});
|
|
4375
5095
|
}
|
|
4376
|
-
async
|
|
5096
|
+
async syncManifestLinks(entityUrl, manifestKey, operation, value) {
|
|
4377
5097
|
const sourceRef = operation === `delete` ? void 0 : this.extractEntitiesSourceRef(value);
|
|
4378
5098
|
await this.registry.replaceEntityManifestSource(entityUrl, manifestKey, sourceRef);
|
|
5099
|
+
const sharedStateId = operation === `delete` ? void 0 : this.extractSharedStateId(value);
|
|
5100
|
+
await this.registry.replaceSharedStateLink(entityUrl, manifestKey, sharedStateId);
|
|
4379
5101
|
}
|
|
4380
5102
|
extractEntitiesSourceRef(manifest) {
|
|
4381
5103
|
if (manifest?.kind === `source` && manifest.sourceType === `entities` && typeof manifest.sourceRef === `string`) return manifest.sourceRef;
|
|
4382
5104
|
return void 0;
|
|
4383
5105
|
}
|
|
5106
|
+
extractSharedStateId(manifest) {
|
|
5107
|
+
if (manifest?.kind === `shared-state` && typeof manifest.id === `string`) return manifest.id;
|
|
5108
|
+
if (manifest?.kind !== `source` || manifest.sourceType !== `db`) return void 0;
|
|
5109
|
+
if (typeof manifest.sourceRef === `string`) return manifest.sourceRef;
|
|
5110
|
+
const config = isRecord(manifest.config) ? manifest.config : void 0;
|
|
5111
|
+
return typeof config?.id === `string` ? config.id : void 0;
|
|
5112
|
+
}
|
|
4384
5113
|
/**
|
|
4385
5114
|
* Read a child entity's stream and extract concatenated text deltas
|
|
4386
5115
|
* for a specific run, plus any error messages for that run.
|
|
@@ -4544,14 +5273,7 @@ var EntityManager = class {
|
|
|
4544
5273
|
await this.streamClient.append(entity.streams.main, signalData);
|
|
4545
5274
|
return;
|
|
4546
5275
|
}
|
|
4547
|
-
const
|
|
4548
|
-
type: `signal`,
|
|
4549
|
-
key: signalEvent.key,
|
|
4550
|
-
value: signalEvent.value,
|
|
4551
|
-
headers: signalEvent.headers
|
|
4552
|
-
};
|
|
4553
|
-
const errorSignalData = this.encodeChangeEvent(errorCloseEvent);
|
|
4554
|
-
for (const [streamPath, data] of [[entity.streams.main, signalData], [entity.streams.error, errorSignalData]]) try {
|
|
5276
|
+
for (const [streamPath, data] of [[entity.streams.main, signalData]]) try {
|
|
4555
5277
|
await this.streamClient.append(streamPath, data, { close: true });
|
|
4556
5278
|
} catch (err) {
|
|
4557
5279
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -4617,7 +5339,9 @@ var EntityManager = class {
|
|
|
4617
5339
|
creation_schema: existing.creation_schema,
|
|
4618
5340
|
inbox_schemas: mergedInbox,
|
|
4619
5341
|
state_schemas: mergedState,
|
|
5342
|
+
slash_commands: existing.slash_commands,
|
|
4620
5343
|
serve_endpoint: existing.serve_endpoint,
|
|
5344
|
+
default_dispatch_policy: existing.default_dispatch_policy,
|
|
4621
5345
|
revision: nextRevision,
|
|
4622
5346
|
created_at: existing.created_at,
|
|
4623
5347
|
updated_at: now
|
|
@@ -4671,11 +5395,19 @@ var EntityManager = class {
|
|
|
4671
5395
|
throw new ElectricAgentsError(ErrCodeInvalidRequest, error instanceof Error ? error.message : `Invalid tags`, 400);
|
|
4672
5396
|
}
|
|
4673
5397
|
}
|
|
5398
|
+
validateSlashCommands(input) {
|
|
5399
|
+
const validationError = validateSlashCommandDefinitions(input);
|
|
5400
|
+
if (!validationError) return;
|
|
5401
|
+
throw new ElectricAgentsError(ErrCodeSchemaValidationFailed, validationError.message, 422, validationError.details);
|
|
5402
|
+
}
|
|
4674
5403
|
async validateSendRequest(entityUrl, req) {
|
|
4675
5404
|
const entity = await this.registry.getEntity(entityUrl);
|
|
4676
5405
|
if (!entity) throw new ElectricAgentsError(ErrCodeNotFound, `Entity not found`, 404);
|
|
4677
5406
|
if (rejectsNormalWrites(entity.status)) throw new ElectricAgentsError(ErrCodeNotRunning, `Entity is not accepting writes`, 409);
|
|
4678
|
-
if (req.type
|
|
5407
|
+
if (req.type === COMPOSER_INPUT_MESSAGE_TYPE) {
|
|
5408
|
+
const valErr = validateComposerInputPayload(req.payload);
|
|
5409
|
+
if (valErr) throw new ElectricAgentsError(ErrCodeSchemaValidationFailed, valErr.message, 422, valErr.details);
|
|
5410
|
+
} else if (req.type && entity.type) {
|
|
4679
5411
|
const { inboxSchemas } = await this.getEffectiveSchemas(entity);
|
|
4680
5412
|
if (inboxSchemas) {
|
|
4681
5413
|
const schema = inboxSchemas[req.type];
|
|
@@ -4927,6 +5659,27 @@ const wakeConditionSchema = Type.Union([Type.Literal(`runFinished`), Type.Object
|
|
|
4927
5659
|
Type.Literal(`delete`)
|
|
4928
5660
|
])))
|
|
4929
5661
|
})]);
|
|
5662
|
+
const permissionSubjectSchema = Type.Object({
|
|
5663
|
+
subject_kind: Type.Union([Type.Literal(`principal`), Type.Literal(`principal_kind`)]),
|
|
5664
|
+
subject_value: Type.String()
|
|
5665
|
+
}, { additionalProperties: false });
|
|
5666
|
+
const entityPermissionSchema = Type.Union([
|
|
5667
|
+
Type.Literal(`read`),
|
|
5668
|
+
Type.Literal(`write`),
|
|
5669
|
+
Type.Literal(`delete`),
|
|
5670
|
+
Type.Literal(`signal`),
|
|
5671
|
+
Type.Literal(`fork`),
|
|
5672
|
+
Type.Literal(`schedule`),
|
|
5673
|
+
Type.Literal(`spawn`),
|
|
5674
|
+
Type.Literal(`manage`)
|
|
5675
|
+
]);
|
|
5676
|
+
const entityPermissionGrantInputSchema = Type.Object({
|
|
5677
|
+
...permissionSubjectSchema.properties,
|
|
5678
|
+
permission: entityPermissionSchema,
|
|
5679
|
+
propagation: Type.Optional(Type.Union([Type.Literal(`self`), Type.Literal(`descendants`)])),
|
|
5680
|
+
copy_to_children: Type.Optional(Type.Boolean()),
|
|
5681
|
+
expires_at: Type.Optional(Type.String())
|
|
5682
|
+
}, { additionalProperties: false });
|
|
4930
5683
|
const spawnBodySchema = Type.Object({
|
|
4931
5684
|
args: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
|
|
4932
5685
|
tags: Type.Optional(stringRecordSchema$1),
|
|
@@ -4934,6 +5687,8 @@ const spawnBodySchema = Type.Object({
|
|
|
4934
5687
|
dispatch_policy: Type.Optional(dispatchPolicySchema),
|
|
4935
5688
|
sandbox: Type.Optional(sandboxChoiceSchema),
|
|
4936
5689
|
initialMessage: Type.Optional(Type.Unknown()),
|
|
5690
|
+
grants: Type.Optional(Type.Array(entityPermissionGrantInputSchema)),
|
|
5691
|
+
initialMessageType: Type.Optional(Type.String()),
|
|
4937
5692
|
wake: Type.Optional(Type.Object({
|
|
4938
5693
|
subscriberUrl: Type.String(),
|
|
4939
5694
|
condition: wakeConditionSchema,
|
|
@@ -4955,8 +5710,22 @@ const sendBodySchema = Type.Object({
|
|
|
4955
5710
|
])),
|
|
4956
5711
|
position: Type.Optional(Type.String()),
|
|
4957
5712
|
afterMs: Type.Optional(Type.Number()),
|
|
4958
|
-
from: Type.Optional(Type.String())
|
|
5713
|
+
from: Type.Optional(Type.String()),
|
|
5714
|
+
from_principal: Type.Optional(Type.String()),
|
|
5715
|
+
from_agent: Type.Optional(Type.String())
|
|
4959
5716
|
});
|
|
5717
|
+
function agentUrlForPrincipal(principal) {
|
|
5718
|
+
if (principal.kind === `agent`) return `/${principal.id}`;
|
|
5719
|
+
if (principal.key.startsWith(`entity:`)) return `/${principal.key.slice(`entity:`.length)}`;
|
|
5720
|
+
return null;
|
|
5721
|
+
}
|
|
5722
|
+
function agentUrlPath(value) {
|
|
5723
|
+
try {
|
|
5724
|
+
return new URL(value).pathname;
|
|
5725
|
+
} catch {
|
|
5726
|
+
return value;
|
|
5727
|
+
}
|
|
5728
|
+
}
|
|
4960
5729
|
const inboxMessageBodySchema = Type.Object({
|
|
4961
5730
|
payload: Type.Optional(Type.Unknown()),
|
|
4962
5731
|
position: Type.Optional(Type.String()),
|
|
@@ -5035,24 +5804,27 @@ const attachmentSubjectTypes = new Set([
|
|
|
5035
5804
|
]);
|
|
5036
5805
|
const entitiesRouter = Router({ base: `/_electric/entities` });
|
|
5037
5806
|
entitiesRouter.get(`/`, listEntities);
|
|
5038
|
-
entitiesRouter.put(`/:type/:instanceId`, withSpawnableEntityType, withSchema(spawnBodySchema), spawnEntity);
|
|
5039
|
-
entitiesRouter.get(`/:type/:instanceId`, withExistingEntity, getEntity);
|
|
5040
|
-
entitiesRouter.head(`/:type/:instanceId`, withExistingEntity, headEntity);
|
|
5041
|
-
entitiesRouter.delete(`/:type/:instanceId`, withExistingEntity, killEntity);
|
|
5042
|
-
entitiesRouter.post(`/:type/:instanceId/signal`, withExistingEntity, withSchema(signalBodySchema), signalEntity);
|
|
5043
|
-
entitiesRouter.post(`/:type/:instanceId/send`, withExistingEntity, withSchema(sendBodySchema), sendEntity);
|
|
5044
|
-
entitiesRouter.post(`/:type/:instanceId/attachments`, withExistingEntity, createAttachment);
|
|
5045
|
-
entitiesRouter.get(`/:type/:instanceId/attachments/:attachmentId`, withExistingEntity, readAttachment);
|
|
5046
|
-
entitiesRouter.delete(`/:type/:instanceId/attachments/:attachmentId`, withExistingEntity, deleteAttachment);
|
|
5047
|
-
entitiesRouter.patch(`/:type/:instanceId/inbox/:messageKey`, withExistingEntity, withSchema(inboxMessageBodySchema), updateInboxMessage);
|
|
5048
|
-
entitiesRouter.delete(`/:type/:instanceId/inbox/:messageKey`, withExistingEntity, deleteInboxMessage);
|
|
5049
|
-
entitiesRouter.post(`/:type/:instanceId/fork`, withExistingEntity, withSchema(forkBodySchema), forkEntity);
|
|
5050
|
-
entitiesRouter.post(`/:type/:instanceId/tags/:tagKey`, withExistingEntity, withSchema(setTagBodySchema), setTag);
|
|
5051
|
-
entitiesRouter.delete(`/:type/:instanceId/tags/:tagKey`, withExistingEntity, deleteTag);
|
|
5052
|
-
entitiesRouter.put(`/:type/:instanceId/schedules/:scheduleId`, withExistingEntity, withSchema(scheduleBodySchema), upsertSchedule);
|
|
5053
|
-
entitiesRouter.delete(`/:type/:instanceId/schedules/:scheduleId`, withExistingEntity, deleteSchedule);
|
|
5054
|
-
entitiesRouter.put(`/:type/:instanceId/event-source-subscriptions/:subscriptionId`, withExistingEntity, withSchema(eventSourceSubscriptionBodySchema), upsertEventSourceSubscription);
|
|
5055
|
-
entitiesRouter.delete(`/:type/:instanceId/event-source-subscriptions/:subscriptionId`, withExistingEntity, deleteEventSourceSubscription);
|
|
5807
|
+
entitiesRouter.put(`/:type/:instanceId`, withSpawnableEntityType, withSchema(spawnBodySchema), withSpawnPermission, spawnEntity);
|
|
5808
|
+
entitiesRouter.get(`/:type/:instanceId`, withExistingEntity, withEntityPermission(`read`), getEntity);
|
|
5809
|
+
entitiesRouter.head(`/:type/:instanceId`, withExistingEntity, withEntityPermission(`read`), headEntity);
|
|
5810
|
+
entitiesRouter.delete(`/:type/:instanceId`, withExistingEntity, withEntityPermission(`delete`), killEntity);
|
|
5811
|
+
entitiesRouter.post(`/:type/:instanceId/signal`, withExistingEntity, withSchema(signalBodySchema), withEntityPermission(`signal`), signalEntity);
|
|
5812
|
+
entitiesRouter.post(`/:type/:instanceId/send`, withExistingEntity, withSchema(sendBodySchema), withEntityPermission(`write`), sendEntity);
|
|
5813
|
+
entitiesRouter.post(`/:type/:instanceId/attachments`, withExistingEntity, withEntityPermission(`write`), createAttachment);
|
|
5814
|
+
entitiesRouter.get(`/:type/:instanceId/attachments/:attachmentId`, withExistingEntity, withEntityPermission(`read`), readAttachment);
|
|
5815
|
+
entitiesRouter.delete(`/:type/:instanceId/attachments/:attachmentId`, withExistingEntity, withEntityPermission(`write`), deleteAttachment);
|
|
5816
|
+
entitiesRouter.patch(`/:type/:instanceId/inbox/:messageKey`, withExistingEntity, withSchema(inboxMessageBodySchema), withEntityPermission(`write`), updateInboxMessage);
|
|
5817
|
+
entitiesRouter.delete(`/:type/:instanceId/inbox/:messageKey`, withExistingEntity, withEntityPermission(`write`), deleteInboxMessage);
|
|
5818
|
+
entitiesRouter.post(`/:type/:instanceId/fork`, withExistingEntity, withSchema(forkBodySchema), withEntityPermission(`fork`), forkEntity);
|
|
5819
|
+
entitiesRouter.post(`/:type/:instanceId/tags/:tagKey`, withExistingEntity, withSchema(setTagBodySchema), withEntityPermission(`write`), setTag);
|
|
5820
|
+
entitiesRouter.delete(`/:type/:instanceId/tags/:tagKey`, withExistingEntity, withEntityPermission(`write`), deleteTag);
|
|
5821
|
+
entitiesRouter.put(`/:type/:instanceId/schedules/:scheduleId`, withExistingEntity, withSchema(scheduleBodySchema), withEntityPermission(`schedule`), upsertSchedule);
|
|
5822
|
+
entitiesRouter.delete(`/:type/:instanceId/schedules/:scheduleId`, withExistingEntity, withEntityPermission(`schedule`), deleteSchedule);
|
|
5823
|
+
entitiesRouter.put(`/:type/:instanceId/event-source-subscriptions/:subscriptionId`, withExistingEntity, withSchema(eventSourceSubscriptionBodySchema), withEntityPermission(`write`), upsertEventSourceSubscription);
|
|
5824
|
+
entitiesRouter.delete(`/:type/:instanceId/event-source-subscriptions/:subscriptionId`, withExistingEntity, withEntityPermission(`write`), deleteEventSourceSubscription);
|
|
5825
|
+
entitiesRouter.get(`/:type/:instanceId/grants`, withExistingEntity, withEntityPermission(`manage`), listEntityPermissionGrants);
|
|
5826
|
+
entitiesRouter.post(`/:type/:instanceId/grants`, withExistingEntity, withSchema(entityPermissionGrantInputSchema), withEntityPermission(`manage`), createEntityPermissionGrant);
|
|
5827
|
+
entitiesRouter.delete(`/:type/:instanceId/grants/:grantId`, withExistingEntity, withEntityPermission(`manage`), deleteEntityPermissionGrant);
|
|
5056
5828
|
function entityUrlFromSegments(type, instanceId) {
|
|
5057
5829
|
if (!type || !instanceId) return null;
|
|
5058
5830
|
if (type.startsWith(`_`) || type.includes(`*`) || instanceId.includes(`*`)) return null;
|
|
@@ -5151,6 +5923,17 @@ function rejectPrincipalEntityMutation(request, action) {
|
|
|
5151
5923
|
if (entity.type !== `principal`) return void 0;
|
|
5152
5924
|
return apiError(400, ErrCodeInvalidRequest, `Principal entities are built in and cannot be ${action}`);
|
|
5153
5925
|
}
|
|
5926
|
+
function parseExpiresAt$1(value) {
|
|
5927
|
+
if (value === void 0) return void 0;
|
|
5928
|
+
const expiresAt = new Date(value);
|
|
5929
|
+
if (Number.isNaN(expiresAt.getTime())) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Invalid expires_at timestamp`, 400);
|
|
5930
|
+
return expiresAt;
|
|
5931
|
+
}
|
|
5932
|
+
function parseGrantId$1(request) {
|
|
5933
|
+
const grantId = Number.parseInt(String(request.params.grantId), 10);
|
|
5934
|
+
if (!Number.isSafeInteger(grantId) || grantId <= 0) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Invalid grant id`, 400);
|
|
5935
|
+
return grantId;
|
|
5936
|
+
}
|
|
5154
5937
|
async function withExistingEntity(request, ctx) {
|
|
5155
5938
|
const entityUrl = entityUrlFromSegments(request.params.type, request.params.instanceId);
|
|
5156
5939
|
if (!entityUrl) return void 0;
|
|
@@ -5181,17 +5964,76 @@ async function withSpawnableEntityType(request, ctx) {
|
|
|
5181
5964
|
if (request.params.type === `principal`) return apiError(400, ErrCodeInvalidRequest, `Principal entities are built in and cannot be spawned directly`);
|
|
5182
5965
|
const entityType = await ctx.entityManager.registry.getEntityType(request.params.type);
|
|
5183
5966
|
if (!entityType) return apiError(404, ErrCodeUnknownEntityType, `Entity type "${request.params.type}" not found`);
|
|
5967
|
+
request.spawnRoute = { entityType };
|
|
5184
5968
|
return void 0;
|
|
5185
5969
|
}
|
|
5970
|
+
function withEntityPermission(permission) {
|
|
5971
|
+
return async (request, ctx) => {
|
|
5972
|
+
const { entity } = requireExistingEntityRoute(request);
|
|
5973
|
+
if (await canAccessEntity(ctx, entity, permission, request)) return void 0;
|
|
5974
|
+
return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to ${permission} ${entity.url}`);
|
|
5975
|
+
};
|
|
5976
|
+
}
|
|
5977
|
+
async function withSpawnPermission(request, ctx) {
|
|
5978
|
+
const parsed = routeBody(request);
|
|
5979
|
+
const entityType = request.spawnRoute?.entityType;
|
|
5980
|
+
if (!entityType) throw new Error(`spawnable entity type middleware did not run`);
|
|
5981
|
+
if (!await canAccessEntityType(ctx, entityType, `spawn`, request)) return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to spawn ${entityType.name}`);
|
|
5982
|
+
if (!parsed.parent) return void 0;
|
|
5983
|
+
const parent = await ctx.entityManager.registry.getEntity(parsed.parent);
|
|
5984
|
+
if (!parent) return apiError(404, ErrCodeNotFound, `Parent entity not found`);
|
|
5985
|
+
if (await canAccessEntity(ctx, parent, `spawn`, request)) return await validateParentedSpawnGrants(request, ctx, parent, parsed);
|
|
5986
|
+
return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to spawn children from ${parent.url}`);
|
|
5987
|
+
}
|
|
5988
|
+
async function validateParentedSpawnGrants(request, ctx, parent, parsed) {
|
|
5989
|
+
const needsParentManage = (parsed.grants ?? []).some(requiresParentManageForInitialGrant);
|
|
5990
|
+
if (!needsParentManage) return void 0;
|
|
5991
|
+
if (await canAccessEntity(ctx, parent, `manage`, request)) return void 0;
|
|
5992
|
+
return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to delegate broad grants from ${parent.url}`);
|
|
5993
|
+
}
|
|
5994
|
+
function requiresParentManageForInitialGrant(grant) {
|
|
5995
|
+
return grant.permission === `manage` || grant.subject_kind === `principal_kind` || grant.propagation === `descendants` || grant.copy_to_children === true;
|
|
5996
|
+
}
|
|
5186
5997
|
async function listEntities({ query }, ctx) {
|
|
5187
5998
|
const { entities: entities$1 } = await ctx.entityManager.registry.listEntities({
|
|
5188
5999
|
type: firstQueryValue$1(query.type),
|
|
5189
6000
|
status: firstQueryValue$1(query.status),
|
|
5190
6001
|
parent: firstQueryValue$1(query.parent),
|
|
5191
|
-
created_by: firstQueryValue$1(query.created_by)
|
|
6002
|
+
created_by: firstQueryValue$1(query.created_by),
|
|
6003
|
+
readableBy: {
|
|
6004
|
+
...principalSubject(ctx.principal),
|
|
6005
|
+
bypass: isPermissionBypassPrincipal(ctx)
|
|
6006
|
+
}
|
|
5192
6007
|
});
|
|
5193
6008
|
return json(entities$1.map((entity) => toPublicEntity(entity)));
|
|
5194
6009
|
}
|
|
6010
|
+
async function listEntityPermissionGrants(request, ctx) {
|
|
6011
|
+
const { entityUrl } = requireExistingEntityRoute(request);
|
|
6012
|
+
const grants = await ctx.entityManager.registry.listEntityPermissionGrants(entityUrl);
|
|
6013
|
+
return json({ grants });
|
|
6014
|
+
}
|
|
6015
|
+
async function createEntityPermissionGrant(request, ctx) {
|
|
6016
|
+
const { entityUrl } = requireExistingEntityRoute(request);
|
|
6017
|
+
const parsed = routeBody(request);
|
|
6018
|
+
const grant = await ctx.entityManager.registry.createEntityPermissionGrant({
|
|
6019
|
+
entityUrl,
|
|
6020
|
+
permission: parsed.permission,
|
|
6021
|
+
subjectKind: parsed.subject_kind,
|
|
6022
|
+
subjectValue: parsed.subject_value,
|
|
6023
|
+
propagation: parsed.propagation,
|
|
6024
|
+
copyToChildren: parsed.copy_to_children,
|
|
6025
|
+
expiresAt: parseExpiresAt$1(parsed.expires_at),
|
|
6026
|
+
createdBy: ctx.principal.url
|
|
6027
|
+
});
|
|
6028
|
+
await ctx.entityBridgeManager.onEntityChanged(entityUrl);
|
|
6029
|
+
return json(grant, { status: 201 });
|
|
6030
|
+
}
|
|
6031
|
+
async function deleteEntityPermissionGrant(request, ctx) {
|
|
6032
|
+
const { entityUrl } = requireExistingEntityRoute(request);
|
|
6033
|
+
const deleted = await ctx.entityManager.registry.deleteEntityPermissionGrant(entityUrl, parseGrantId$1(request));
|
|
6034
|
+
if (deleted) await ctx.entityBridgeManager.onEntityChanged(entityUrl);
|
|
6035
|
+
return deleted ? status(204) : apiError(404, ErrCodeNotFound, `Grant not found`);
|
|
6036
|
+
}
|
|
5195
6037
|
async function upsertSchedule(request, ctx) {
|
|
5196
6038
|
const principalMutationError = rejectPrincipalEntityMutation(request, `scheduled`);
|
|
5197
6039
|
if (principalMutationError) return principalMutationError;
|
|
@@ -5297,6 +6139,7 @@ async function forkEntity(request, ctx) {
|
|
|
5297
6139
|
const result = await ctx.entityManager.forkSubtree(entityUrl, {
|
|
5298
6140
|
rootInstanceId: parsed.instance_id,
|
|
5299
6141
|
waitTimeoutMs: parsed.waitTimeoutMs,
|
|
6142
|
+
createdBy: ctx.principal.url,
|
|
5300
6143
|
...parsed.fork_pointer && { forkPointer: {
|
|
5301
6144
|
offset: parsed.fork_pointer.offset,
|
|
5302
6145
|
subOffset: parsed.fork_pointer.sub_offset
|
|
@@ -5312,26 +6155,27 @@ async function sendEntity(request, ctx) {
|
|
|
5312
6155
|
const parsed = routeBody(request);
|
|
5313
6156
|
const principal = ctx.principal;
|
|
5314
6157
|
if (parsed.from !== void 0 && parsed.from !== principal.url) return apiError(400, ErrCodeInvalidRequest, `Request from must match Electric-Principal`);
|
|
6158
|
+
if (parsed.from_principal !== void 0 && parsed.from_principal !== principal.url) return apiError(400, ErrCodeInvalidRequest, `Request from_principal must match Electric-Principal`);
|
|
6159
|
+
if (parsed.from_agent !== void 0) {
|
|
6160
|
+
const principalAgentUrl = agentUrlForPrincipal(principal);
|
|
6161
|
+
if (agentUrlPath(parsed.from_agent) !== principalAgentUrl) return apiError(400, ErrCodeInvalidRequest, `Request from_agent must match authenticated agent principal`);
|
|
6162
|
+
}
|
|
5315
6163
|
await ctx.entityManager.ensurePrincipal(principal);
|
|
5316
6164
|
const { entityUrl, entity } = requireExistingEntityRoute(request);
|
|
5317
6165
|
const dispatchEntity = entity.dispatch_policy ? entity : await backfillEntityDispatchPolicy(ctx, entity);
|
|
5318
6166
|
await linkEntityDispatchSubscription(ctx, dispatchEntity);
|
|
5319
|
-
|
|
5320
|
-
from: principal.url,
|
|
5321
|
-
payload: parsed.payload,
|
|
5322
|
-
key: parsed.key,
|
|
5323
|
-
type: parsed.type,
|
|
5324
|
-
mode: parsed.mode,
|
|
5325
|
-
position: parsed.position
|
|
5326
|
-
}, new Date(Date.now() + parsed.afterMs));
|
|
5327
|
-
else await ctx.entityManager.send(entityUrl, {
|
|
6167
|
+
const sendReq = {
|
|
5328
6168
|
from: principal.url,
|
|
6169
|
+
from_principal: principal.url,
|
|
6170
|
+
from_agent: parsed.from_agent,
|
|
5329
6171
|
payload: parsed.payload,
|
|
5330
6172
|
key: parsed.key,
|
|
5331
6173
|
type: parsed.type,
|
|
5332
6174
|
mode: parsed.mode,
|
|
5333
6175
|
position: parsed.position
|
|
5334
|
-
}
|
|
6176
|
+
};
|
|
6177
|
+
if (parsed.afterMs && parsed.afterMs > 0) await ctx.entityManager.enqueueDelayedSend(entityUrl, sendReq, new Date(Date.now() + parsed.afterMs));
|
|
6178
|
+
else await ctx.entityManager.send(entityUrl, sendReq);
|
|
5335
6179
|
return status(204);
|
|
5336
6180
|
}
|
|
5337
6181
|
async function createAttachment(request, ctx) {
|
|
@@ -5400,14 +6244,27 @@ async function spawnEntity(request, ctx) {
|
|
|
5400
6244
|
dispatch_policy: dispatchPolicy,
|
|
5401
6245
|
sandbox: parsed.sandbox,
|
|
5402
6246
|
initialMessage: void 0,
|
|
6247
|
+
initialMessageType: void 0,
|
|
5403
6248
|
wake: parsed.wake,
|
|
5404
6249
|
created_by: principal.url
|
|
5405
6250
|
});
|
|
6251
|
+
if (parsed.parent) await ctx.entityManager.registry.copyEntityPermissionGrantsForSpawn(parsed.parent, entity.url, principal.url);
|
|
6252
|
+
for (const grant of parsed.grants ?? []) await ctx.entityManager.registry.createEntityPermissionGrant({
|
|
6253
|
+
entityUrl: entity.url,
|
|
6254
|
+
permission: grant.permission,
|
|
6255
|
+
subjectKind: grant.subject_kind,
|
|
6256
|
+
subjectValue: grant.subject_value,
|
|
6257
|
+
propagation: grant.propagation,
|
|
6258
|
+
copyToChildren: grant.copy_to_children,
|
|
6259
|
+
expiresAt: parseExpiresAt$1(grant.expires_at),
|
|
6260
|
+
createdBy: principal.url
|
|
6261
|
+
});
|
|
5406
6262
|
const linkBeforeInitialMessage = parsed.initialMessage !== void 0 && shouldLinkDispatchBeforeInitialMessage(dispatchPolicy);
|
|
5407
6263
|
if (linkBeforeInitialMessage) await linkEntityDispatchSubscription(ctx, entity);
|
|
5408
6264
|
if (parsed.initialMessage !== void 0) await ctx.entityManager.send(entity.url, {
|
|
5409
6265
|
from: principal.url,
|
|
5410
|
-
payload: parsed.initialMessage
|
|
6266
|
+
payload: parsed.initialMessage,
|
|
6267
|
+
type: parsed.initialMessageType
|
|
5411
6268
|
});
|
|
5412
6269
|
if (!linkBeforeInitialMessage) await linkEntityDispatchSubscription(ctx, entity);
|
|
5413
6270
|
return json({
|
|
@@ -5454,14 +6311,37 @@ async function signalEntity(request, ctx) {
|
|
|
5454
6311
|
//#region src/routing/entity-types-router.ts
|
|
5455
6312
|
const jsonObjectSchema = Type.Record(Type.String(), Type.Unknown());
|
|
5456
6313
|
const schemaMapSchema = Type.Record(Type.String(), jsonObjectSchema);
|
|
6314
|
+
const slashCommandArgumentSchema = Type.Object({
|
|
6315
|
+
name: Type.String(),
|
|
6316
|
+
type: Type.Union([
|
|
6317
|
+
Type.Literal(`string`),
|
|
6318
|
+
Type.Literal(`number`),
|
|
6319
|
+
Type.Literal(`boolean`)
|
|
6320
|
+
]),
|
|
6321
|
+
required: Type.Optional(Type.Boolean()),
|
|
6322
|
+
description: Type.Optional(Type.String())
|
|
6323
|
+
}, { additionalProperties: false });
|
|
6324
|
+
const slashCommandSchema = Type.Object({
|
|
6325
|
+
name: Type.String(),
|
|
6326
|
+
description: Type.Optional(Type.String()),
|
|
6327
|
+
arguments: Type.Optional(Type.Array(slashCommandArgumentSchema))
|
|
6328
|
+
}, { additionalProperties: false });
|
|
6329
|
+
const typePermissionGrantInputSchema = Type.Object({
|
|
6330
|
+
subject_kind: Type.Union([Type.Literal(`principal`), Type.Literal(`principal_kind`)]),
|
|
6331
|
+
subject_value: Type.String(),
|
|
6332
|
+
permission: Type.Union([Type.Literal(`spawn`), Type.Literal(`manage`)]),
|
|
6333
|
+
expires_at: Type.Optional(Type.String())
|
|
6334
|
+
}, { additionalProperties: false });
|
|
5457
6335
|
const registerEntityTypeBodySchema = Type.Object({
|
|
5458
6336
|
name: Type.Optional(Type.String()),
|
|
5459
6337
|
description: Type.Optional(Type.String()),
|
|
5460
6338
|
creation_schema: Type.Optional(jsonObjectSchema),
|
|
5461
6339
|
inbox_schemas: Type.Optional(schemaMapSchema),
|
|
5462
6340
|
state_schemas: Type.Optional(schemaMapSchema),
|
|
6341
|
+
slash_commands: Type.Optional(Type.Array(slashCommandSchema)),
|
|
5463
6342
|
serve_endpoint: Type.Optional(Type.String()),
|
|
5464
|
-
default_dispatch_policy: Type.Optional(dispatchPolicySchema)
|
|
6343
|
+
default_dispatch_policy: Type.Optional(dispatchPolicySchema),
|
|
6344
|
+
permission_grants: Type.Optional(Type.Array(typePermissionGrantInputSchema))
|
|
5465
6345
|
}, { additionalProperties: false });
|
|
5466
6346
|
const amendEntityTypeSchemasBodySchema = Type.Object({
|
|
5467
6347
|
inbox_schemas: Type.Optional(schemaMapSchema),
|
|
@@ -5469,20 +6349,56 @@ const amendEntityTypeSchemasBodySchema = Type.Object({
|
|
|
5469
6349
|
}, { additionalProperties: false });
|
|
5470
6350
|
const entityTypesRouter = Router({ base: `/_electric/entity-types` });
|
|
5471
6351
|
entityTypesRouter.get(`/`, listEntityTypes);
|
|
5472
|
-
entityTypesRouter.post(`/`, withSchema(registerEntityTypeBodySchema), registerEntityType);
|
|
5473
|
-
entityTypesRouter.patch(`/:name/schemas`, withSchema(amendEntityTypeSchemasBodySchema), amendSchemas);
|
|
5474
|
-
entityTypesRouter.get(`/:name`, getEntityType);
|
|
5475
|
-
entityTypesRouter.delete(`/:name`, deleteEntityType);
|
|
6352
|
+
entityTypesRouter.post(`/`, withSchema(registerEntityTypeBodySchema), withEntityTypeRegistrationPermission, registerEntityType);
|
|
6353
|
+
entityTypesRouter.patch(`/:name/schemas`, withExistingEntityType, withEntityTypeManagePermission, withSchema(amendEntityTypeSchemasBodySchema), amendSchemas);
|
|
6354
|
+
entityTypesRouter.get(`/:name`, withExistingEntityType, withEntityTypeSpawnPermission, getEntityType);
|
|
6355
|
+
entityTypesRouter.delete(`/:name`, withExistingEntityType, withEntityTypeManagePermission, deleteEntityType);
|
|
6356
|
+
entityTypesRouter.get(`/:name/grants`, withExistingEntityType, withEntityTypeManagePermission, listTypePermissionGrants);
|
|
6357
|
+
entityTypesRouter.post(`/:name/grants`, withExistingEntityType, withSchema(typePermissionGrantInputSchema), withEntityTypeManagePermission, createTypePermissionGrant);
|
|
6358
|
+
entityTypesRouter.delete(`/:name/grants/:grantId`, withExistingEntityType, withEntityTypeManagePermission, deleteTypePermissionGrant);
|
|
5476
6359
|
async function registerEntityType(request, ctx) {
|
|
5477
6360
|
const parsed = routeBody(request);
|
|
5478
6361
|
const normalized = normalizeEntityTypeRequest(parsed);
|
|
5479
6362
|
if (normalized.serve_endpoint && !normalized.description && !normalized.creation_schema) return await discoverServeEndpoint(ctx, normalized);
|
|
5480
6363
|
const entityType = await ctx.entityManager.registerEntityType(normalized);
|
|
6364
|
+
await applyRegistrationPermissionGrants(ctx, entityType.name, normalized);
|
|
5481
6365
|
return json(toPublicEntityType(entityType), { status: 201 });
|
|
5482
6366
|
}
|
|
5483
6367
|
async function listEntityTypes(_request, ctx) {
|
|
5484
6368
|
const entityTypes$1 = await ctx.entityManager.registry.listEntityTypes();
|
|
5485
|
-
|
|
6369
|
+
const visible = [];
|
|
6370
|
+
for (const entityType of entityTypes$1) if (await canAccessEntityType(ctx, entityType, `spawn`)) visible.push(entityType);
|
|
6371
|
+
return json(visible.map((entityType) => toPublicEntityType(entityType)));
|
|
6372
|
+
}
|
|
6373
|
+
async function withExistingEntityType(request, ctx) {
|
|
6374
|
+
const entityType = await ctx.entityManager.registry.getEntityType(request.params.name);
|
|
6375
|
+
if (!entityType) return apiError(404, ErrCodeNotFound, `Entity type not found`);
|
|
6376
|
+
request.entityTypeRoute = { entityType };
|
|
6377
|
+
return void 0;
|
|
6378
|
+
}
|
|
6379
|
+
async function withEntityTypeManagePermission(request, ctx) {
|
|
6380
|
+
const entityType = request.entityTypeRoute?.entityType;
|
|
6381
|
+
if (!entityType) throw new Error(`entity type middleware did not run`);
|
|
6382
|
+
if (await canAccessEntityType(ctx, entityType, `manage`, request)) return void 0;
|
|
6383
|
+
return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to manage ${entityType.name}`);
|
|
6384
|
+
}
|
|
6385
|
+
async function withEntityTypeSpawnPermission(request, ctx) {
|
|
6386
|
+
const entityType = request.entityTypeRoute?.entityType;
|
|
6387
|
+
if (!entityType) throw new Error(`entity type middleware did not run`);
|
|
6388
|
+
if (await canAccessEntityType(ctx, entityType, `spawn`, request)) return void 0;
|
|
6389
|
+
return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to spawn ${entityType.name}`);
|
|
6390
|
+
}
|
|
6391
|
+
async function withEntityTypeRegistrationPermission(request, ctx) {
|
|
6392
|
+
const parsed = normalizeEntityTypeRequest(routeBody(request));
|
|
6393
|
+
if (!parsed.name) return void 0;
|
|
6394
|
+
const existing = await ctx.entityManager.registry.getEntityType(parsed.name);
|
|
6395
|
+
if (existing) {
|
|
6396
|
+
request.entityTypeRoute = { entityType: existing };
|
|
6397
|
+
if (await canAccessEntityType(ctx, existing, `manage`, request)) return void 0;
|
|
6398
|
+
return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to manage ${existing.name}`);
|
|
6399
|
+
}
|
|
6400
|
+
if (await canRegisterEntityType(ctx, parsed, request)) return void 0;
|
|
6401
|
+
return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to register entity types`);
|
|
5486
6402
|
}
|
|
5487
6403
|
async function discoverServeEndpoint(ctx, parsed) {
|
|
5488
6404
|
try {
|
|
@@ -5491,17 +6407,17 @@ async function discoverServeEndpoint(ctx, parsed) {
|
|
|
5491
6407
|
const manifest = await response.json();
|
|
5492
6408
|
if (manifest.name !== parsed.name) return apiError(400, ErrCodeServeEndpointNameMismatch, `Serve endpoint returned name "${manifest.name}" but expected "${parsed.name}"`);
|
|
5493
6409
|
manifest.serve_endpoint = parsed.serve_endpoint;
|
|
6410
|
+
manifest.permission_grants = parsed.permission_grants;
|
|
5494
6411
|
const entityType = await ctx.entityManager.registerEntityType(normalizeEntityTypeRequest(manifest));
|
|
6412
|
+
await applyRegistrationPermissionGrants(ctx, entityType.name, manifest);
|
|
5495
6413
|
return json(toPublicEntityType(entityType), { status: 201 });
|
|
5496
6414
|
} catch (err) {
|
|
5497
6415
|
if (err instanceof ElectricAgentsError) throw err;
|
|
5498
6416
|
return apiError(502, ErrCodeServeEndpointUnreachable, `Failed to reach serve endpoint: ${err instanceof Error ? err.message : String(err)}`);
|
|
5499
6417
|
}
|
|
5500
6418
|
}
|
|
5501
|
-
async function getEntityType(request
|
|
5502
|
-
|
|
5503
|
-
if (!entityType) return apiError(404, ErrCodeNotFound, `Entity type not found`);
|
|
5504
|
-
return json(toPublicEntityType(entityType));
|
|
6419
|
+
async function getEntityType(request) {
|
|
6420
|
+
return json(toPublicEntityType(request.entityTypeRoute.entityType));
|
|
5505
6421
|
}
|
|
5506
6422
|
async function amendSchemas(request, ctx) {
|
|
5507
6423
|
const parsed = routeBody(request);
|
|
@@ -5515,6 +6431,47 @@ async function deleteEntityType(request, ctx) {
|
|
|
5515
6431
|
await ctx.entityManager.deleteEntityType(request.params.name);
|
|
5516
6432
|
return status(204);
|
|
5517
6433
|
}
|
|
6434
|
+
async function listTypePermissionGrants(request, ctx) {
|
|
6435
|
+
const grants = await ctx.entityManager.registry.listEntityTypePermissionGrants(request.entityTypeRoute.entityType.name);
|
|
6436
|
+
return json({ grants });
|
|
6437
|
+
}
|
|
6438
|
+
async function createTypePermissionGrant(request, ctx) {
|
|
6439
|
+
const parsed = routeBody(request);
|
|
6440
|
+
const grant = await ctx.entityManager.registry.createEntityTypePermissionGrant({
|
|
6441
|
+
entityType: request.entityTypeRoute.entityType.name,
|
|
6442
|
+
permission: parsed.permission,
|
|
6443
|
+
subjectKind: parsed.subject_kind,
|
|
6444
|
+
subjectValue: parsed.subject_value,
|
|
6445
|
+
expiresAt: parseExpiresAt(parsed.expires_at),
|
|
6446
|
+
createdBy: ctx.principal.url
|
|
6447
|
+
});
|
|
6448
|
+
return json(grant, { status: 201 });
|
|
6449
|
+
}
|
|
6450
|
+
async function deleteTypePermissionGrant(request, ctx) {
|
|
6451
|
+
const deleted = await ctx.entityManager.registry.deleteEntityTypePermissionGrant(request.entityTypeRoute.entityType.name, parseGrantId(request));
|
|
6452
|
+
return deleted ? status(204) : apiError(404, ErrCodeNotFound, `Grant not found`);
|
|
6453
|
+
}
|
|
6454
|
+
async function applyRegistrationPermissionGrants(ctx, entityType, request) {
|
|
6455
|
+
for (const grant of request.permission_grants ?? []) await ctx.entityManager.registry.ensureEntityTypePermissionGrant({
|
|
6456
|
+
entityType,
|
|
6457
|
+
permission: grant.permission,
|
|
6458
|
+
subjectKind: grant.subject_kind,
|
|
6459
|
+
subjectValue: grant.subject_value,
|
|
6460
|
+
expiresAt: parseExpiresAt(grant.expires_at),
|
|
6461
|
+
createdBy: ctx.principal.url
|
|
6462
|
+
});
|
|
6463
|
+
}
|
|
6464
|
+
function parseGrantId(request) {
|
|
6465
|
+
const grantId = Number.parseInt(String(request.params.grantId), 10);
|
|
6466
|
+
if (!Number.isSafeInteger(grantId) || grantId <= 0) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Invalid grant id`, 400);
|
|
6467
|
+
return grantId;
|
|
6468
|
+
}
|
|
6469
|
+
function parseExpiresAt(value) {
|
|
6470
|
+
if (value === void 0) return void 0;
|
|
6471
|
+
const expiresAt = new Date(value);
|
|
6472
|
+
if (Number.isNaN(expiresAt.getTime())) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Invalid expires_at timestamp`, 400);
|
|
6473
|
+
return expiresAt;
|
|
6474
|
+
}
|
|
5518
6475
|
function normalizeEntityTypeRequest(parsed) {
|
|
5519
6476
|
const serveEndpoint = rewriteLoopbackWebhookUrl(parsed.serve_endpoint);
|
|
5520
6477
|
return {
|
|
@@ -5523,11 +6480,13 @@ function normalizeEntityTypeRequest(parsed) {
|
|
|
5523
6480
|
creation_schema: parsed.creation_schema,
|
|
5524
6481
|
inbox_schemas: parsed.inbox_schemas,
|
|
5525
6482
|
state_schemas: parsed.state_schemas,
|
|
6483
|
+
slash_commands: parsed.slash_commands,
|
|
5526
6484
|
serve_endpoint: serveEndpoint,
|
|
5527
6485
|
default_dispatch_policy: parsed.default_dispatch_policy ?? (serveEndpoint ? { targets: [{
|
|
5528
6486
|
type: `webhook`,
|
|
5529
6487
|
url: serveEndpoint
|
|
5530
|
-
}] } : void 0)
|
|
6488
|
+
}] } : void 0),
|
|
6489
|
+
permission_grants: parsed.permission_grants
|
|
5531
6490
|
};
|
|
5532
6491
|
}
|
|
5533
6492
|
function toPublicEntityType(entityType) {
|
|
@@ -5586,6 +6545,7 @@ function applyCors(response) {
|
|
|
5586
6545
|
`content-type`,
|
|
5587
6546
|
`authorization`,
|
|
5588
6547
|
`electric-claim-token`,
|
|
6548
|
+
`electric-owner-entity`,
|
|
5589
6549
|
ELECTRIC_PRINCIPAL_HEADER,
|
|
5590
6550
|
`ngrok-skip-browser-warning`
|
|
5591
6551
|
].join(`, `));
|
|
@@ -5636,7 +6596,7 @@ observationsRouter.post(`/entities/ensure-stream`, withSchema(ensureEntitiesMemb
|
|
|
5636
6596
|
observationsRouter.post(`/cron/ensure-stream`, withSchema(ensureCronStreamBodySchema), ensureCronStream);
|
|
5637
6597
|
async function ensureEntitiesMembershipStream(request, ctx) {
|
|
5638
6598
|
const parsed = routeBody(request);
|
|
5639
|
-
const result = await ctx.entityManager.ensureEntitiesMembershipStream(parsed.tags ?? {});
|
|
6599
|
+
const result = await ctx.entityManager.ensureEntitiesMembershipStream(parsed.tags ?? {}, ctx.principal);
|
|
5640
6600
|
return json(result);
|
|
5641
6601
|
}
|
|
5642
6602
|
async function ensureCronStream(request, ctx) {
|
|
@@ -6515,16 +7475,31 @@ function buildTagsWhereClause(tags) {
|
|
|
6515
7475
|
function sqlStringLiteral$1(value) {
|
|
6516
7476
|
return `'${value.replace(/'/g, `''`)}'`;
|
|
6517
7477
|
}
|
|
6518
|
-
function buildTenantTagsWhereClause(tenantId, tags) {
|
|
6519
|
-
|
|
7478
|
+
function buildTenantTagsWhereClause(tenantId, tags, principalUrl$1, principalKind, permissionBypass) {
|
|
7479
|
+
const readableWhere = principalUrl$1 && principalKind ? buildReadableEntitiesWhere({
|
|
7480
|
+
tenantId,
|
|
7481
|
+
principalUrl: principalUrl$1,
|
|
7482
|
+
principalKind,
|
|
7483
|
+
permissionBypass
|
|
7484
|
+
}) : `tenant_id = ${sqlStringLiteral$1(tenantId)} AND FALSE`;
|
|
7485
|
+
return `${readableWhere} AND (${buildTagsWhereClause(tags)})`;
|
|
6520
7486
|
}
|
|
6521
7487
|
function shapeEntityKey(message) {
|
|
6522
7488
|
return message.value.url;
|
|
6523
7489
|
}
|
|
7490
|
+
function principalScopedSourceRef(tagSourceRef, principalUrl$1, principalKind) {
|
|
7491
|
+
return `${tagSourceRef}-${hashString(JSON.stringify({
|
|
7492
|
+
principalKind,
|
|
7493
|
+
principalUrl: principalUrl$1
|
|
7494
|
+
}))}`;
|
|
7495
|
+
}
|
|
6524
7496
|
var EntityBridge = class {
|
|
6525
7497
|
sourceRef;
|
|
6526
7498
|
tags;
|
|
6527
7499
|
streamUrl;
|
|
7500
|
+
principalUrl;
|
|
7501
|
+
principalKind;
|
|
7502
|
+
permissionBypass;
|
|
6528
7503
|
currentMembers = new Map();
|
|
6529
7504
|
producer = null;
|
|
6530
7505
|
liveAbortController = null;
|
|
@@ -6541,6 +7516,9 @@ var EntityBridge = class {
|
|
|
6541
7516
|
this.sourceRef = row.sourceRef;
|
|
6542
7517
|
this.tags = normalizeTags(row.tags);
|
|
6543
7518
|
this.streamUrl = row.streamUrl;
|
|
7519
|
+
this.principalUrl = row.principalUrl;
|
|
7520
|
+
this.principalKind = row.principalKind;
|
|
7521
|
+
this.permissionBypass = isBuiltInSystemPrincipalUrl(row.principalUrl);
|
|
6544
7522
|
this.initialShapeHandle = row.shapeHandle;
|
|
6545
7523
|
this.initialShapeOffset = row.shapeOffset;
|
|
6546
7524
|
}
|
|
@@ -6648,7 +7626,7 @@ var EntityBridge = class {
|
|
|
6648
7626
|
url: electricUrlWithPath(this.electricUrl, `/v1/shape`).toString(),
|
|
6649
7627
|
params: {
|
|
6650
7628
|
table: `entities`,
|
|
6651
|
-
where: buildTenantTagsWhereClause(this.tenantId, this.tags),
|
|
7629
|
+
where: buildTenantTagsWhereClause(this.tenantId, this.tags, this.principalUrl, this.principalKind, this.permissionBypass),
|
|
6652
7630
|
...this.electricSecret ? { secret: this.electricSecret } : {},
|
|
6653
7631
|
columns: [...ENTITY_SHAPE_COLUMNS],
|
|
6654
7632
|
replica: `full`
|
|
@@ -6811,15 +7789,17 @@ var EntityBridgeManager = class {
|
|
|
6811
7789
|
await bridge.stop();
|
|
6812
7790
|
}));
|
|
6813
7791
|
}
|
|
6814
|
-
async register(tagsInput) {
|
|
7792
|
+
async register(tagsInput, principalUrl$1, principalKind) {
|
|
6815
7793
|
if (!this.electricUrl) throw new Error(`[entity-bridge] Electric URL is required for entities()`);
|
|
6816
7794
|
const tags = normalizeTags(assertTags(tagsInput));
|
|
6817
|
-
const sourceRef = sourceRefForTags(tags);
|
|
7795
|
+
const sourceRef = principalScopedSourceRef(sourceRefForTags(tags), principalUrl$1, principalKind);
|
|
6818
7796
|
const streamUrl = getEntitiesStreamPath(sourceRef);
|
|
6819
7797
|
const row = await this.registry.upsertEntityBridge({
|
|
6820
7798
|
sourceRef,
|
|
6821
7799
|
tags,
|
|
6822
|
-
streamUrl
|
|
7800
|
+
streamUrl,
|
|
7801
|
+
principalUrl: principalUrl$1,
|
|
7802
|
+
principalKind
|
|
6823
7803
|
});
|
|
6824
7804
|
await this.registry.touchEntityBridge(sourceRef);
|
|
6825
7805
|
await this.ensureBridge(row);
|
|
@@ -7639,6 +8619,8 @@ var ElectricAgentsTenantRuntime = class {
|
|
|
7639
8619
|
try {
|
|
7640
8620
|
await this.manager.send(payload.entityUrl, {
|
|
7641
8621
|
from: payload.from,
|
|
8622
|
+
from_principal: payload.from_principal,
|
|
8623
|
+
from_agent: payload.from_agent,
|
|
7642
8624
|
payload: payload.payload,
|
|
7643
8625
|
key: payload.key ?? `scheduled-task-${taskId}`,
|
|
7644
8626
|
type: payload.type
|
|
@@ -7711,6 +8693,7 @@ var ElectricAgentsTenantRuntime = class {
|
|
|
7711
8693
|
await this.scheduler.syncManifestDelayedSend(ownerEntityUrl, manifestKey, {
|
|
7712
8694
|
entityUrl: targetUrl,
|
|
7713
8695
|
from: senderUrl,
|
|
8696
|
+
from_agent: senderUrl,
|
|
7714
8697
|
payload: value.payload,
|
|
7715
8698
|
key: `scheduled-${producerId}`,
|
|
7716
8699
|
type: typeof value.messageType === `string` ? value.messageType : void 0,
|
|
@@ -7735,11 +8718,20 @@ var ElectricAgentsTenantRuntime = class {
|
|
|
7735
8718
|
async applyManifestEntitySource(ownerEntityUrl, manifestKey, operation, value) {
|
|
7736
8719
|
const sourceRef = operation === `delete` ? void 0 : this.extractEntitiesSourceRef(value);
|
|
7737
8720
|
await this.manager.registry.replaceEntityManifestSource(ownerEntityUrl, manifestKey, sourceRef);
|
|
8721
|
+
const sharedStateId = operation === `delete` ? void 0 : this.extractSharedStateId(value);
|
|
8722
|
+
await this.manager.registry.replaceSharedStateLink(ownerEntityUrl, manifestKey, sharedStateId);
|
|
7738
8723
|
}
|
|
7739
8724
|
extractEntitiesSourceRef(manifest) {
|
|
7740
8725
|
if (manifest?.kind === `source` && manifest.sourceType === `entities` && typeof manifest.sourceRef === `string`) return manifest.sourceRef;
|
|
7741
8726
|
return void 0;
|
|
7742
8727
|
}
|
|
8728
|
+
extractSharedStateId(manifest) {
|
|
8729
|
+
if (manifest?.kind === `shared-state` && typeof manifest.id === `string`) return manifest.id;
|
|
8730
|
+
if (manifest?.kind !== `source` || manifest.sourceType !== `db`) return void 0;
|
|
8731
|
+
if (typeof manifest.sourceRef === `string`) return manifest.sourceRef;
|
|
8732
|
+
const config = typeof manifest.config === `object` && manifest.config !== null && !Array.isArray(manifest.config) ? manifest.config : void 0;
|
|
8733
|
+
return typeof config?.id === `string` ? config.id : void 0;
|
|
8734
|
+
}
|
|
7743
8735
|
async maybeMarkEntityIdleAfterRunFinished(entityUrl) {
|
|
7744
8736
|
const primaryStream = `${entityUrl}/main`;
|
|
7745
8737
|
const callbacks = await this.db.select().from(consumerCallbacks).where(and(eq(consumerCallbacks.tenantId, this.serviceId), eq(consumerCallbacks.primaryStream, primaryStream))).limit(1);
|
|
@@ -8420,6 +9412,8 @@ var WakeRegistry = class {
|
|
|
8420
9412
|
if (eventType === `inbox`) {
|
|
8421
9413
|
const value = event.value;
|
|
8422
9414
|
if (typeof value?.from === `string`) change.from = value.from;
|
|
9415
|
+
if (typeof value?.from_principal === `string`) change.from_principal = value.from_principal;
|
|
9416
|
+
if (typeof value?.from_agent === `string`) change.from_agent = value.from_agent;
|
|
8423
9417
|
if (`payload` in (value ?? {})) change.payload = value?.payload;
|
|
8424
9418
|
if (typeof value?.timestamp === `string`) change.timestamp = value.timestamp;
|
|
8425
9419
|
if (typeof value?.message_type === `string`) change.message_type = value.message_type;
|
|
@@ -8770,6 +9764,7 @@ var ElectricAgentsServer = class {
|
|
|
8770
9764
|
entityBridgeManager: this.entityBridgeManager,
|
|
8771
9765
|
...this.options.eventSources ? { eventSources: this.options.eventSources } : {},
|
|
8772
9766
|
...this.options.ensureEventSourceWakeSource ? { ensureEventSourceWakeSource: this.options.ensureEventSourceWakeSource } : {},
|
|
9767
|
+
...this.options.authorizeRequest ? { authorizeRequest: this.options.authorizeRequest } : {},
|
|
8773
9768
|
isShuttingDown: () => this.shuttingDown,
|
|
8774
9769
|
mockAgent: this.mockAgentBootstrap ? { runtime: this.mockAgentBootstrap.runtime } : void 0
|
|
8775
9770
|
};
|