@electric-ax/agents-server 0.4.15 → 0.4.16
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 +1176 -232
- package/dist/index.cjs +1179 -226
- package/dist/index.d.cts +1146 -167
- package/dist/index.d.ts +1146 -167
- package/dist/index.js +1181 -228
- 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/meta/_journal.json +21 -0
- package/package.json +7 -7
- package/src/db/schema.ts +198 -0
- package/src/electric-agents-types.ts +76 -2
- package/src/entity-bridge-manager.ts +57 -6
- package/src/entity-manager.ts +78 -60
- package/src/entity-projector.ts +76 -17
- package/src/entity-registry.ts +608 -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 +344 -20
- package/src/routing/entity-types-router.ts +244 -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 +191 -11
- 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 { appendPathToUrl, assertTags, buildEventSourceManifestEntry, buildTagsIndex, createEntityRegistry, createRuntimeHandler, entityStateSchema, eventSourceSubscriptionManifestKey, getCronStreamPath, getCronStreamPathFromSpec, getEntitiesStreamPath, getNextCronFireAt, getSharedStateStreamPath, getWebhookStreamPath, hashString, manifestChildKey, manifestSharedStateKey, manifestSourceKey, normalizeTags, parseCronStreamPath, resolveCronScheduleSpec, resolveEventSourceSubscription, sourceRefForTags, 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,
|
|
@@ -93,6 +98,94 @@ const entities = pgTable(`entities`, {
|
|
|
93
98
|
index(`entities_tags_index_gin`).using(`gin`, table.tagsIndex),
|
|
94
99
|
check(`chk_entities_status`, sql`${table.status} IN ('spawning', 'running', 'idle', 'paused', 'stopping', 'stopped', 'killed')`)
|
|
95
100
|
]);
|
|
101
|
+
const entityTypePermissionGrants = pgTable(`entity_type_permission_grants`, {
|
|
102
|
+
id: bigserial(`id`, { mode: `number` }).primaryKey(),
|
|
103
|
+
tenantId: text(`tenant_id`).notNull().default(`default`),
|
|
104
|
+
entityType: text(`entity_type`).notNull(),
|
|
105
|
+
permission: text(`permission`).notNull(),
|
|
106
|
+
subjectKind: text(`subject_kind`).notNull(),
|
|
107
|
+
subjectValue: text(`subject_value`).notNull(),
|
|
108
|
+
createdBy: text(`created_by`),
|
|
109
|
+
expiresAt: timestamp(`expires_at`, { withTimezone: true }),
|
|
110
|
+
createdAt: timestamp(`created_at`, { withTimezone: true }).notNull().defaultNow(),
|
|
111
|
+
updatedAt: timestamp(`updated_at`, { withTimezone: true }).notNull().defaultNow()
|
|
112
|
+
}, (table) => [
|
|
113
|
+
index(`idx_type_permission_grants_lookup`).on(table.tenantId, table.entityType, table.permission, table.subjectKind, table.subjectValue),
|
|
114
|
+
index(`idx_type_permission_grants_expiry`).on(table.tenantId, table.expiresAt),
|
|
115
|
+
check(`chk_type_permission_grants_permission`, sql`${table.permission} IN ('spawn', 'manage')`),
|
|
116
|
+
check(`chk_type_permission_grants_subject_kind`, sql`${table.subjectKind} IN ('principal', 'principal_kind')`)
|
|
117
|
+
]);
|
|
118
|
+
const entityLineage = pgTable(`entity_lineage`, {
|
|
119
|
+
tenantId: text(`tenant_id`).notNull().default(`default`),
|
|
120
|
+
ancestorUrl: text(`ancestor_url`).notNull(),
|
|
121
|
+
descendantUrl: text(`descendant_url`).notNull(),
|
|
122
|
+
depth: integer(`depth`).notNull(),
|
|
123
|
+
createdAt: timestamp(`created_at`, { withTimezone: true }).notNull().defaultNow()
|
|
124
|
+
}, (table) => [
|
|
125
|
+
primaryKey({ columns: [
|
|
126
|
+
table.tenantId,
|
|
127
|
+
table.ancestorUrl,
|
|
128
|
+
table.descendantUrl
|
|
129
|
+
] }),
|
|
130
|
+
index(`idx_entity_lineage_descendant`).on(table.tenantId, table.descendantUrl),
|
|
131
|
+
check(`chk_entity_lineage_depth`, sql`${table.depth} >= 0`)
|
|
132
|
+
]);
|
|
133
|
+
const entityPermissionGrants = pgTable(`entity_permission_grants`, {
|
|
134
|
+
id: bigserial(`id`, { mode: `number` }).primaryKey(),
|
|
135
|
+
tenantId: text(`tenant_id`).notNull().default(`default`),
|
|
136
|
+
entityUrl: text(`entity_url`).notNull(),
|
|
137
|
+
permission: text(`permission`).notNull(),
|
|
138
|
+
subjectKind: text(`subject_kind`).notNull(),
|
|
139
|
+
subjectValue: text(`subject_value`).notNull(),
|
|
140
|
+
propagation: text(`propagation`).notNull().default(`self`),
|
|
141
|
+
copyToChildren: boolean(`copy_to_children`).notNull().default(false),
|
|
142
|
+
createdBy: text(`created_by`),
|
|
143
|
+
expiresAt: timestamp(`expires_at`, { withTimezone: true }),
|
|
144
|
+
createdAt: timestamp(`created_at`, { withTimezone: true }).notNull().defaultNow(),
|
|
145
|
+
updatedAt: timestamp(`updated_at`, { withTimezone: true }).notNull().defaultNow()
|
|
146
|
+
}, (table) => [
|
|
147
|
+
index(`idx_entity_permission_grants_entity`).on(table.tenantId, table.entityUrl),
|
|
148
|
+
index(`idx_entity_permission_grants_subject`).on(table.tenantId, table.permission, table.subjectKind, table.subjectValue),
|
|
149
|
+
index(`idx_entity_permission_grants_expiry`).on(table.tenantId, table.expiresAt),
|
|
150
|
+
check(`chk_entity_permission_grants_permission`, sql`${table.permission} IN ('read', 'write', 'delete', 'signal', 'fork', 'schedule', 'spawn', 'manage')`),
|
|
151
|
+
check(`chk_entity_permission_grants_subject_kind`, sql`${table.subjectKind} IN ('principal', 'principal_kind')`),
|
|
152
|
+
check(`chk_entity_permission_grants_propagation`, sql`${table.propagation} IN ('self', 'descendants')`)
|
|
153
|
+
]);
|
|
154
|
+
const entityEffectivePermissions = pgTable(`entity_effective_permissions`, {
|
|
155
|
+
id: bigserial(`id`, { mode: `number` }).primaryKey(),
|
|
156
|
+
tenantId: text(`tenant_id`).notNull().default(`default`),
|
|
157
|
+
entityUrl: text(`entity_url`).notNull(),
|
|
158
|
+
sourceEntityUrl: text(`source_entity_url`).notNull(),
|
|
159
|
+
sourceGrantId: bigint(`source_grant_id`, { mode: `number` }).notNull(),
|
|
160
|
+
permission: text(`permission`).notNull(),
|
|
161
|
+
subjectKind: text(`subject_kind`).notNull(),
|
|
162
|
+
subjectValue: text(`subject_value`).notNull(),
|
|
163
|
+
expiresAt: timestamp(`expires_at`, { withTimezone: true }),
|
|
164
|
+
createdAt: timestamp(`created_at`, { withTimezone: true }).notNull().defaultNow()
|
|
165
|
+
}, (table) => [
|
|
166
|
+
unique(`uq_entity_effective_permission`).on(table.tenantId, table.entityUrl, table.sourceGrantId),
|
|
167
|
+
index(`idx_entity_effective_permissions_lookup`).on(table.tenantId, table.permission, table.subjectKind, table.subjectValue, table.entityUrl),
|
|
168
|
+
index(`idx_entity_effective_permissions_entity`).on(table.tenantId, table.entityUrl),
|
|
169
|
+
index(`idx_entity_effective_permissions_expiry`).on(table.tenantId, table.expiresAt),
|
|
170
|
+
check(`chk_entity_effective_permissions_permission`, sql`${table.permission} IN ('read', 'write', 'delete', 'signal', 'fork', 'schedule', 'spawn', 'manage')`),
|
|
171
|
+
check(`chk_entity_effective_permissions_subject_kind`, sql`${table.subjectKind} IN ('principal', 'principal_kind')`)
|
|
172
|
+
]);
|
|
173
|
+
const sharedStateLinks = pgTable(`shared_state_links`, {
|
|
174
|
+
tenantId: text(`tenant_id`).notNull().default(`default`),
|
|
175
|
+
sharedStateId: text(`shared_state_id`).notNull(),
|
|
176
|
+
ownerEntityUrl: text(`owner_entity_url`).notNull(),
|
|
177
|
+
manifestKey: text(`manifest_key`).notNull(),
|
|
178
|
+
createdAt: timestamp(`created_at`, { withTimezone: true }).notNull().defaultNow(),
|
|
179
|
+
updatedAt: timestamp(`updated_at`, { withTimezone: true }).notNull().defaultNow()
|
|
180
|
+
}, (table) => [
|
|
181
|
+
primaryKey({ columns: [
|
|
182
|
+
table.tenantId,
|
|
183
|
+
table.ownerEntityUrl,
|
|
184
|
+
table.manifestKey
|
|
185
|
+
] }),
|
|
186
|
+
index(`idx_shared_state_links_shared_state`).on(table.tenantId, table.sharedStateId),
|
|
187
|
+
index(`idx_shared_state_links_owner`).on(table.tenantId, table.ownerEntityUrl)
|
|
188
|
+
]);
|
|
96
189
|
const users = pgTable(`users`, {
|
|
97
190
|
tenantId: text(`tenant_id`).notNull().default(`default`),
|
|
98
191
|
id: text(`id`).notNull(),
|
|
@@ -279,12 +372,18 @@ const entityBridges = pgTable(`entity_bridges`, {
|
|
|
279
372
|
sourceRef: text(`source_ref`).notNull(),
|
|
280
373
|
tags: jsonb(`tags`).notNull(),
|
|
281
374
|
streamUrl: text(`stream_url`).notNull(),
|
|
375
|
+
principalUrl: text(`principal_url`),
|
|
376
|
+
principalKind: text(`principal_kind`),
|
|
282
377
|
shapeHandle: text(`shape_handle`),
|
|
283
378
|
shapeOffset: text(`shape_offset`),
|
|
284
379
|
lastObserverActivityAt: timestamp(`last_observer_activity_at`, { withTimezone: true }).notNull().defaultNow(),
|
|
285
380
|
createdAt: timestamp(`created_at`, { withTimezone: true }).notNull().defaultNow(),
|
|
286
381
|
updatedAt: timestamp(`updated_at`, { withTimezone: true }).notNull().defaultNow()
|
|
287
|
-
}, (table) => [
|
|
382
|
+
}, (table) => [
|
|
383
|
+
primaryKey({ columns: [table.tenantId, table.sourceRef] }),
|
|
384
|
+
unique(`uq_entity_bridges_stream_url`).on(table.tenantId, table.streamUrl),
|
|
385
|
+
index(`idx_entity_bridges_principal`).on(table.tenantId, table.principalKind, table.principalUrl)
|
|
386
|
+
]);
|
|
288
387
|
const entityManifestSources = pgTable(`entity_manifest_sources`, {
|
|
289
388
|
tenantId: text(`tenant_id`).notNull().default(`default`),
|
|
290
389
|
ownerEntityUrl: text(`owner_entity_url`).notNull(),
|
|
@@ -1045,29 +1144,136 @@ function buildElectricProxyTarget(options) {
|
|
|
1045
1144
|
if (options.electricSecret) target.searchParams.set(`secret`, options.electricSecret);
|
|
1046
1145
|
const table = options.incomingUrl.searchParams.get(`table`);
|
|
1047
1146
|
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
|
-
|
|
1147
|
+
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"`);
|
|
1148
|
+
applyShapeWhere(target, buildReadableEntitiesWhere({
|
|
1149
|
+
tenantId: options.tenantId,
|
|
1150
|
+
principalUrl: options.principalUrl ?? ``,
|
|
1151
|
+
principalKind: options.principalKind ?? ``,
|
|
1152
|
+
permissionBypass: options.permissionBypass
|
|
1153
|
+
}));
|
|
1050
1154
|
} else if (table === `entity_types`) {
|
|
1051
1155
|
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
|
+
applyShapeWhere(target, buildSpawnableEntityTypesWhere({
|
|
1157
|
+
tenantId: options.tenantId,
|
|
1158
|
+
principalUrl: options.principalUrl ?? ``,
|
|
1159
|
+
principalKind: options.principalKind ?? ``,
|
|
1160
|
+
permissionBypass: options.permissionBypass
|
|
1161
|
+
}));
|
|
1053
1162
|
} else if (table === `runners`) {
|
|
1054
1163
|
target.searchParams.set(`columns`, `"tenant_id","id","owner_principal","label","kind","admin_status","wake_stream","sandbox_profiles","created_at","updated_at"`);
|
|
1055
1164
|
applyTenantShapeWhere(target, options.tenantId, [`owner_principal = ${sqlStringLiteral$2(options.principalUrl ?? ``)}`]);
|
|
1165
|
+
} else if (table === `users`) {
|
|
1166
|
+
target.searchParams.set(`columns`, `"tenant_id","id","display_name","email","avatar_url","created_at","updated_at"`);
|
|
1167
|
+
applyTenantShapeWhere(target, options.tenantId);
|
|
1168
|
+
} else if (table === `entity_effective_permissions`) {
|
|
1169
|
+
target.searchParams.set(`columns`, `"tenant_id","id","entity_url","source_entity_url","source_grant_id","permission","subject_kind","subject_value","expires_at","created_at"`);
|
|
1170
|
+
applyShapeWhere(target, buildCurrentPrincipalEntityEffectivePermissionsWhere({
|
|
1171
|
+
tenantId: options.tenantId,
|
|
1172
|
+
principalUrl: options.principalUrl ?? ``,
|
|
1173
|
+
principalKind: options.principalKind ?? ``,
|
|
1174
|
+
permissionBypass: options.permissionBypass
|
|
1175
|
+
}));
|
|
1056
1176
|
} else if (table === `runner_runtime_diagnostics`) {
|
|
1057
1177
|
target.searchParams.set(`columns`, `"tenant_id","runner_id","owner_principal","wake_stream_offset","last_seen_at","liveness_lease_expires_at","diagnostics","updated_at"`);
|
|
1058
1178
|
applyTenantShapeWhere(target, options.tenantId, [`owner_principal = ${sqlStringLiteral$2(options.principalUrl ?? ``)}`]);
|
|
1059
1179
|
} else if (table === `entity_dispatch_state`) {
|
|
1060
1180
|
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
|
-
|
|
1181
|
+
applyShapeWhere(target, buildReadableEntityUrlWhere({
|
|
1182
|
+
tenantId: options.tenantId,
|
|
1183
|
+
principalUrl: options.principalUrl ?? ``,
|
|
1184
|
+
principalKind: options.principalKind ?? ``,
|
|
1185
|
+
permissionBypass: options.permissionBypass
|
|
1186
|
+
}));
|
|
1062
1187
|
} else if (table === `wake_notifications`) {
|
|
1063
1188
|
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
|
-
|
|
1189
|
+
applyShapeWhere(target, buildReadableEntityUrlWhere({
|
|
1190
|
+
tenantId: options.tenantId,
|
|
1191
|
+
principalUrl: options.principalUrl ?? ``,
|
|
1192
|
+
principalKind: options.principalKind ?? ``,
|
|
1193
|
+
permissionBypass: options.permissionBypass
|
|
1194
|
+
}));
|
|
1065
1195
|
} else if (table === `consumer_claims`) {
|
|
1066
1196
|
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
|
-
|
|
1197
|
+
applyShapeWhere(target, buildReadableEntityUrlWhere({
|
|
1198
|
+
tenantId: options.tenantId,
|
|
1199
|
+
principalUrl: options.principalUrl ?? ``,
|
|
1200
|
+
principalKind: options.principalKind ?? ``,
|
|
1201
|
+
permissionBypass: options.permissionBypass
|
|
1202
|
+
}));
|
|
1068
1203
|
}
|
|
1069
1204
|
return target;
|
|
1070
1205
|
}
|
|
1206
|
+
function buildReadableEntitiesWhere(options) {
|
|
1207
|
+
const tenant = sqlStringLiteral$2(options.tenantId);
|
|
1208
|
+
if (options.permissionBypass) return `tenant_id = ${tenant}`;
|
|
1209
|
+
const principalUrl$1 = sqlStringLiteral$2(options.principalUrl);
|
|
1210
|
+
const principalKind = sqlStringLiteral$2(options.principalKind);
|
|
1211
|
+
return [
|
|
1212
|
+
`tenant_id = ${tenant}`,
|
|
1213
|
+
`AND (`,
|
|
1214
|
+
` created_by = ${principalUrl$1}`,
|
|
1215
|
+
` OR url IN (`,
|
|
1216
|
+
` SELECT entity_url`,
|
|
1217
|
+
` FROM entity_effective_permissions`,
|
|
1218
|
+
` WHERE tenant_id = ${tenant}`,
|
|
1219
|
+
` AND permission IN ('read', 'manage')`,
|
|
1220
|
+
` AND (`,
|
|
1221
|
+
` (subject_kind = 'principal' AND subject_value = ${principalUrl$1})`,
|
|
1222
|
+
` OR (subject_kind = 'principal_kind' AND subject_value = ${principalKind})`,
|
|
1223
|
+
` )`,
|
|
1224
|
+
` )`,
|
|
1225
|
+
`)`
|
|
1226
|
+
].join(`\n`);
|
|
1227
|
+
}
|
|
1228
|
+
function buildReadableEntityUrlWhere(options) {
|
|
1229
|
+
const tenant = sqlStringLiteral$2(options.tenantId);
|
|
1230
|
+
if (options.permissionBypass) return `tenant_id = ${tenant}`;
|
|
1231
|
+
return [
|
|
1232
|
+
`tenant_id = ${tenant}`,
|
|
1233
|
+
`AND entity_url IN (`,
|
|
1234
|
+
` SELECT url`,
|
|
1235
|
+
` FROM entities`,
|
|
1236
|
+
` WHERE ${indentWhere(buildReadableEntitiesWhere(options), ` `).trimStart()}`,
|
|
1237
|
+
`)`
|
|
1238
|
+
].join(`\n`);
|
|
1239
|
+
}
|
|
1240
|
+
function buildCurrentPrincipalEntityEffectivePermissionsWhere(options) {
|
|
1241
|
+
const tenant = sqlStringLiteral$2(options.tenantId);
|
|
1242
|
+
if (options.permissionBypass) return `tenant_id = ${tenant}`;
|
|
1243
|
+
const principalUrl$1 = sqlStringLiteral$2(options.principalUrl);
|
|
1244
|
+
const principalKind = sqlStringLiteral$2(options.principalKind);
|
|
1245
|
+
return [
|
|
1246
|
+
`tenant_id = ${tenant}`,
|
|
1247
|
+
`AND (`,
|
|
1248
|
+
` (subject_kind = 'principal' AND subject_value = ${principalUrl$1})`,
|
|
1249
|
+
` OR (subject_kind = 'principal_kind' AND subject_value = ${principalKind})`,
|
|
1250
|
+
`)`,
|
|
1251
|
+
`AND entity_url IN (`,
|
|
1252
|
+
` SELECT url`,
|
|
1253
|
+
` FROM entities`,
|
|
1254
|
+
` WHERE ${buildReadableEntitiesWhere(options)}`,
|
|
1255
|
+
`)`
|
|
1256
|
+
].join(`\n`);
|
|
1257
|
+
}
|
|
1258
|
+
function buildSpawnableEntityTypesWhere(options) {
|
|
1259
|
+
const tenant = sqlStringLiteral$2(options.tenantId);
|
|
1260
|
+
if (options.permissionBypass) return `tenant_id = ${tenant}`;
|
|
1261
|
+
const principalUrl$1 = sqlStringLiteral$2(options.principalUrl);
|
|
1262
|
+
const principalKind = sqlStringLiteral$2(options.principalKind);
|
|
1263
|
+
return [
|
|
1264
|
+
`tenant_id = ${tenant}`,
|
|
1265
|
+
`AND name IN (`,
|
|
1266
|
+
` SELECT entity_type`,
|
|
1267
|
+
` FROM entity_type_permission_grants`,
|
|
1268
|
+
` WHERE tenant_id = ${tenant}`,
|
|
1269
|
+
` AND permission IN ('spawn', 'manage')`,
|
|
1270
|
+
` AND (`,
|
|
1271
|
+
` (subject_kind = 'principal' AND subject_value = ${principalUrl$1})`,
|
|
1272
|
+
` OR (subject_kind = 'principal_kind' AND subject_value = ${principalKind})`,
|
|
1273
|
+
` )`,
|
|
1274
|
+
`)`
|
|
1275
|
+
].join(`\n`);
|
|
1276
|
+
}
|
|
1071
1277
|
async function forwardFetchRequest(options) {
|
|
1072
1278
|
const routingAdapter = resolveDurableStreamsRoutingAdapter(options.durableStreamsRouting, options.durableStreamsUrl);
|
|
1073
1279
|
const routingInput = {
|
|
@@ -1102,13 +1308,18 @@ function decodeJsonObject(body) {
|
|
|
1102
1308
|
return null;
|
|
1103
1309
|
}
|
|
1104
1310
|
function applyTenantShapeWhere(target, tenantId, extraConditions = []) {
|
|
1105
|
-
|
|
1311
|
+
applyShapeWhere(target, [`tenant_id = ${sqlStringLiteral$2(tenantId)}`, ...extraConditions].join(` AND `));
|
|
1312
|
+
}
|
|
1313
|
+
function applyShapeWhere(target, enforcedWhere) {
|
|
1106
1314
|
const existingWhere = target.searchParams.get(`where`);
|
|
1107
|
-
target.searchParams.set(`where`, existingWhere ? `${
|
|
1315
|
+
target.searchParams.set(`where`, existingWhere ? `${enforcedWhere} AND (${existingWhere})` : enforcedWhere);
|
|
1108
1316
|
}
|
|
1109
1317
|
function sqlStringLiteral$2(value) {
|
|
1110
1318
|
return `'${value.replace(/'/g, `''`)}'`;
|
|
1111
1319
|
}
|
|
1320
|
+
function indentWhere(where, prefix) {
|
|
1321
|
+
return where.split(`\n`).map((line) => `${prefix}${line}`).join(`\n`);
|
|
1322
|
+
}
|
|
1112
1323
|
|
|
1113
1324
|
//#endregion
|
|
1114
1325
|
//#region src/routing/agent-ui-router.ts
|
|
@@ -1402,6 +1613,262 @@ function isLoopbackHostname(hostname) {
|
|
|
1402
1613
|
return hostname === `localhost` || hostname === `127.0.0.1` || hostname === `::1`;
|
|
1403
1614
|
}
|
|
1404
1615
|
|
|
1616
|
+
//#endregion
|
|
1617
|
+
//#region src/principal.ts
|
|
1618
|
+
const ELECTRIC_PRINCIPAL_HEADER = `electric-principal`;
|
|
1619
|
+
const PRINCIPAL_KINDS = new Set([
|
|
1620
|
+
`user`,
|
|
1621
|
+
`agent`,
|
|
1622
|
+
`service`,
|
|
1623
|
+
`system`
|
|
1624
|
+
]);
|
|
1625
|
+
function parsePrincipalKey(input) {
|
|
1626
|
+
const colon = input.indexOf(`:`);
|
|
1627
|
+
if (colon <= 0) throw new Error(`Invalid principal identifier`);
|
|
1628
|
+
const kind = input.slice(0, colon);
|
|
1629
|
+
const id = input.slice(colon + 1);
|
|
1630
|
+
if (!PRINCIPAL_KINDS.has(kind)) throw new Error(`Invalid principal kind`);
|
|
1631
|
+
if (!id || id.includes(`/`)) throw new Error(`Invalid principal id`);
|
|
1632
|
+
const key = `${kind}:${id}`;
|
|
1633
|
+
return {
|
|
1634
|
+
kind,
|
|
1635
|
+
id,
|
|
1636
|
+
key,
|
|
1637
|
+
url: `/principal/${encodeURIComponent(key)}`
|
|
1638
|
+
};
|
|
1639
|
+
}
|
|
1640
|
+
function principalUrl(key) {
|
|
1641
|
+
return parsePrincipalKey(key).url;
|
|
1642
|
+
}
|
|
1643
|
+
function parsePrincipalUrl(url) {
|
|
1644
|
+
if (!url.startsWith(`/principal/`)) return null;
|
|
1645
|
+
const segment = url.slice(`/principal/`.length);
|
|
1646
|
+
if (!segment || segment.includes(`/`)) return null;
|
|
1647
|
+
try {
|
|
1648
|
+
return parsePrincipalKey(decodeURIComponent(segment));
|
|
1649
|
+
} catch {
|
|
1650
|
+
return null;
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
function parsePrincipalInput(input) {
|
|
1654
|
+
const urlPrincipal = parsePrincipalUrl(input);
|
|
1655
|
+
if (urlPrincipal) return urlPrincipal;
|
|
1656
|
+
try {
|
|
1657
|
+
return parsePrincipalKey(input);
|
|
1658
|
+
} catch {
|
|
1659
|
+
return null;
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1662
|
+
function getPrincipalFromRequest(request) {
|
|
1663
|
+
const value = request.headers.get(ELECTRIC_PRINCIPAL_HEADER);
|
|
1664
|
+
if (!value) return null;
|
|
1665
|
+
return parsePrincipalInput(value);
|
|
1666
|
+
}
|
|
1667
|
+
function getDevPrincipal() {
|
|
1668
|
+
return parsePrincipalKey(`system:dev-local`);
|
|
1669
|
+
}
|
|
1670
|
+
const BUILT_IN_SYSTEM_PRINCIPAL_IDS = new Set([
|
|
1671
|
+
`framework`,
|
|
1672
|
+
`auth-sync`,
|
|
1673
|
+
`dev-local`
|
|
1674
|
+
]);
|
|
1675
|
+
function isBuiltInSystemPrincipalUrl(url) {
|
|
1676
|
+
if (!url?.startsWith(`/principal/`)) return false;
|
|
1677
|
+
try {
|
|
1678
|
+
const principal = parsePrincipalUrl(url);
|
|
1679
|
+
if (!principal) return false;
|
|
1680
|
+
return principal.kind === `system` && BUILT_IN_SYSTEM_PRINCIPAL_IDS.has(principal.id);
|
|
1681
|
+
} catch {
|
|
1682
|
+
return false;
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
function principalFromCreatedBy(createdBy) {
|
|
1686
|
+
if (!createdBy) return void 0;
|
|
1687
|
+
const principal = parsePrincipalUrl(createdBy);
|
|
1688
|
+
if (!principal) return {
|
|
1689
|
+
url: createdBy,
|
|
1690
|
+
key: null
|
|
1691
|
+
};
|
|
1692
|
+
return {
|
|
1693
|
+
url: principal.url,
|
|
1694
|
+
key: principal.key,
|
|
1695
|
+
kind: principal.kind,
|
|
1696
|
+
id: principal.id
|
|
1697
|
+
};
|
|
1698
|
+
}
|
|
1699
|
+
const principalIdentityStateSchema = Type.Object({
|
|
1700
|
+
kind: Type.Union([
|
|
1701
|
+
Type.Literal(`user`),
|
|
1702
|
+
Type.Literal(`agent`),
|
|
1703
|
+
Type.Literal(`service`),
|
|
1704
|
+
Type.Literal(`system`)
|
|
1705
|
+
]),
|
|
1706
|
+
id: Type.String(),
|
|
1707
|
+
key: Type.String(),
|
|
1708
|
+
url: Type.String(),
|
|
1709
|
+
updated_at: Type.String(),
|
|
1710
|
+
display_name: Type.Optional(Type.String()),
|
|
1711
|
+
email: Type.Optional(Type.String()),
|
|
1712
|
+
avatar_url: Type.Optional(Type.String()),
|
|
1713
|
+
auth_provider: Type.Optional(Type.String()),
|
|
1714
|
+
auth_subject: Type.Optional(Type.String()),
|
|
1715
|
+
claims: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
|
|
1716
|
+
created_at: Type.Optional(Type.String())
|
|
1717
|
+
}, { additionalProperties: false });
|
|
1718
|
+
const principalUpdateIdentityMessageSchema = Type.Object({ identity: principalIdentityStateSchema }, { additionalProperties: false });
|
|
1719
|
+
|
|
1720
|
+
//#endregion
|
|
1721
|
+
//#region src/permissions.ts
|
|
1722
|
+
const authzDecisionCache = new WeakMap();
|
|
1723
|
+
function principalSubject(principal) {
|
|
1724
|
+
return {
|
|
1725
|
+
principalUrl: principal.url,
|
|
1726
|
+
principalKind: principal.kind
|
|
1727
|
+
};
|
|
1728
|
+
}
|
|
1729
|
+
function isPermissionBypassPrincipal(ctx) {
|
|
1730
|
+
return isBuiltInSystemPrincipalUrl(ctx.principal.url);
|
|
1731
|
+
}
|
|
1732
|
+
async function canAccessEntity(ctx, entity, permission, request) {
|
|
1733
|
+
if (isPermissionBypassPrincipal(ctx)) return true;
|
|
1734
|
+
await ctx.entityManager.registry.pruneExpiredPermissionGrants?.();
|
|
1735
|
+
const builtInAllowed = entity.created_by === ctx.principal.url || await ctx.entityManager.registry.hasEntityPermission(entity.url, permission, principalSubject(ctx.principal));
|
|
1736
|
+
return await applyAuthorizationHook(ctx, {
|
|
1737
|
+
verb: permission,
|
|
1738
|
+
resourceKey: `entity:${entity.url}`,
|
|
1739
|
+
resource: {
|
|
1740
|
+
kind: `entity`,
|
|
1741
|
+
entity
|
|
1742
|
+
},
|
|
1743
|
+
builtInAllowed,
|
|
1744
|
+
request
|
|
1745
|
+
});
|
|
1746
|
+
}
|
|
1747
|
+
async function canAccessEntityType(ctx, entityType, permission, request) {
|
|
1748
|
+
if (isPermissionBypassPrincipal(ctx)) return true;
|
|
1749
|
+
await ctx.entityManager.registry.pruneExpiredPermissionGrants?.();
|
|
1750
|
+
const builtInAllowed = await ctx.entityManager.registry.hasEntityTypePermission(entityType.name, permission, principalSubject(ctx.principal));
|
|
1751
|
+
return await applyAuthorizationHook(ctx, {
|
|
1752
|
+
verb: permission,
|
|
1753
|
+
resourceKey: `entity_type:${entityType.name}`,
|
|
1754
|
+
resource: {
|
|
1755
|
+
kind: `entity_type`,
|
|
1756
|
+
entityType
|
|
1757
|
+
},
|
|
1758
|
+
builtInAllowed,
|
|
1759
|
+
request
|
|
1760
|
+
});
|
|
1761
|
+
}
|
|
1762
|
+
async function canRegisterEntityType(ctx, input, request) {
|
|
1763
|
+
if (isPermissionBypassPrincipal(ctx)) return true;
|
|
1764
|
+
return await applyAuthorizationHook(ctx, {
|
|
1765
|
+
verb: `manage`,
|
|
1766
|
+
resourceKey: `entity_type_registration:${input.name}`,
|
|
1767
|
+
resource: {
|
|
1768
|
+
kind: `entity_type_registration`,
|
|
1769
|
+
entityTypeName: input.name
|
|
1770
|
+
},
|
|
1771
|
+
builtInAllowed: true,
|
|
1772
|
+
request
|
|
1773
|
+
});
|
|
1774
|
+
}
|
|
1775
|
+
async function canAccessSharedState(ctx, sharedStateId, permission, request, ownerEntityUrl) {
|
|
1776
|
+
if (isPermissionBypassPrincipal(ctx)) return true;
|
|
1777
|
+
await ctx.entityManager.registry.pruneExpiredPermissionGrants?.();
|
|
1778
|
+
const storedLinkedEntityUrls = await ctx.entityManager.registry.listSharedStateLinkedEntityUrls(sharedStateId);
|
|
1779
|
+
const bootstrapEntityUrls = storedLinkedEntityUrls.length === 0 && ownerEntityUrl ? [ownerEntityUrl] : [];
|
|
1780
|
+
const linkedEntityUrls = [...new Set([...storedLinkedEntityUrls, ...bootstrapEntityUrls])];
|
|
1781
|
+
for (const entityUrl of linkedEntityUrls) {
|
|
1782
|
+
const entity = await ctx.entityManager.registry.getEntity(entityUrl);
|
|
1783
|
+
if (!entity) continue;
|
|
1784
|
+
if (entity.created_by === ctx.principal.url || await ctx.entityManager.registry.hasEntityPermission(entity.url, permission, principalSubject(ctx.principal))) return await applyAuthorizationHook(ctx, {
|
|
1785
|
+
verb: permission,
|
|
1786
|
+
resourceKey: `shared_state:${sharedStateId}`,
|
|
1787
|
+
resource: {
|
|
1788
|
+
kind: `shared_state`,
|
|
1789
|
+
sharedStateId,
|
|
1790
|
+
linkedEntityUrls
|
|
1791
|
+
},
|
|
1792
|
+
builtInAllowed: true,
|
|
1793
|
+
request
|
|
1794
|
+
});
|
|
1795
|
+
}
|
|
1796
|
+
return await applyAuthorizationHook(ctx, {
|
|
1797
|
+
verb: permission,
|
|
1798
|
+
resourceKey: `shared_state:${sharedStateId}`,
|
|
1799
|
+
resource: {
|
|
1800
|
+
kind: `shared_state`,
|
|
1801
|
+
sharedStateId,
|
|
1802
|
+
linkedEntityUrls
|
|
1803
|
+
},
|
|
1804
|
+
builtInAllowed: false,
|
|
1805
|
+
request
|
|
1806
|
+
});
|
|
1807
|
+
}
|
|
1808
|
+
async function applyAuthorizationHook(ctx, input) {
|
|
1809
|
+
const hook = ctx.authorizeRequest;
|
|
1810
|
+
if (!hook) return input.builtInAllowed;
|
|
1811
|
+
const cacheKey = [
|
|
1812
|
+
ctx.service,
|
|
1813
|
+
ctx.principal.url,
|
|
1814
|
+
input.verb,
|
|
1815
|
+
input.resourceKey
|
|
1816
|
+
].join(`|`);
|
|
1817
|
+
const cached = getCachedDecision(hook, cacheKey);
|
|
1818
|
+
if (cached) return cached.decision === `allow`;
|
|
1819
|
+
let decision;
|
|
1820
|
+
try {
|
|
1821
|
+
decision = await hook({
|
|
1822
|
+
tenant: ctx.service,
|
|
1823
|
+
principal: ctx.principal,
|
|
1824
|
+
verb: input.verb,
|
|
1825
|
+
resource: input.resource,
|
|
1826
|
+
request: input.request ? requestMetadata(input.request) : void 0,
|
|
1827
|
+
builtInAllowed: input.builtInAllowed
|
|
1828
|
+
});
|
|
1829
|
+
} catch (error) {
|
|
1830
|
+
serverLog.warn(`[agent-server] authorization hook failed:`, error);
|
|
1831
|
+
return false;
|
|
1832
|
+
}
|
|
1833
|
+
cacheDecision(hook, cacheKey, decision);
|
|
1834
|
+
return decision.decision === `allow`;
|
|
1835
|
+
}
|
|
1836
|
+
function getCachedDecision(hook, cacheKey) {
|
|
1837
|
+
const cache = authzDecisionCache.get(hook);
|
|
1838
|
+
const entry = cache?.get(cacheKey);
|
|
1839
|
+
if (!entry) return null;
|
|
1840
|
+
if (entry.expiresAt <= Date.now()) {
|
|
1841
|
+
cache?.delete(cacheKey);
|
|
1842
|
+
return null;
|
|
1843
|
+
}
|
|
1844
|
+
return { decision: entry.decision };
|
|
1845
|
+
}
|
|
1846
|
+
function cacheDecision(hook, cacheKey, decision) {
|
|
1847
|
+
if (!decision.expires_at) return;
|
|
1848
|
+
const expiresAt = Date.parse(decision.expires_at);
|
|
1849
|
+
if (!Number.isFinite(expiresAt) || expiresAt <= Date.now()) return;
|
|
1850
|
+
let cache = authzDecisionCache.get(hook);
|
|
1851
|
+
if (!cache) {
|
|
1852
|
+
cache = new Map();
|
|
1853
|
+
authzDecisionCache.set(hook, cache);
|
|
1854
|
+
}
|
|
1855
|
+
cache.set(cacheKey, {
|
|
1856
|
+
decision: decision.decision,
|
|
1857
|
+
expiresAt
|
|
1858
|
+
});
|
|
1859
|
+
}
|
|
1860
|
+
function requestMetadata(request) {
|
|
1861
|
+
const headers = {};
|
|
1862
|
+
request.headers.forEach((value, key) => {
|
|
1863
|
+
headers[key] = value;
|
|
1864
|
+
});
|
|
1865
|
+
return {
|
|
1866
|
+
method: request.method,
|
|
1867
|
+
url: request.url,
|
|
1868
|
+
headers
|
|
1869
|
+
};
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1405
1872
|
//#endregion
|
|
1406
1873
|
//#region src/webhook-signing.ts
|
|
1407
1874
|
const encoder = new TextEncoder();
|
|
@@ -1492,6 +1959,7 @@ const subscriptionControlActions = [
|
|
|
1492
1959
|
`ack`,
|
|
1493
1960
|
`release`
|
|
1494
1961
|
];
|
|
1962
|
+
const SHARED_STATE_OWNER_ENTITY_HEADER = `electric-owner-entity`;
|
|
1495
1963
|
const durableStreamsRouter = Router();
|
|
1496
1964
|
durableStreamsRouter.put(`/__ds/subscriptions/:subscriptionId`, putSubscriptionBase);
|
|
1497
1965
|
durableStreamsRouter.get(`/__ds/subscriptions/:subscriptionId`, getSubscriptionBase);
|
|
@@ -1709,6 +2177,8 @@ async function webhookJwks(_request, ctx) {
|
|
|
1709
2177
|
});
|
|
1710
2178
|
}
|
|
1711
2179
|
async function streamAppend(request, ctx) {
|
|
2180
|
+
const auth = await authorizeDurableStreamAccess(request, ctx);
|
|
2181
|
+
if (auth) return auth;
|
|
1712
2182
|
return await electricAgentsStreamAppendRouter.fetch(createStreamAppendRouteRequest(request), ctx.runtime, (req, body) => forwardFetchRequest({
|
|
1713
2183
|
request: {
|
|
1714
2184
|
method: req.method,
|
|
@@ -1725,8 +2195,9 @@ async function streamAppend(request, ctx) {
|
|
|
1725
2195
|
}));
|
|
1726
2196
|
}
|
|
1727
2197
|
async function proxyPassThrough(request, ctx) {
|
|
2198
|
+
const auth = await authorizeDurableStreamAccess(request, ctx);
|
|
2199
|
+
if (auth) return auth;
|
|
1728
2200
|
const streamPath = new URL(request.url).pathname;
|
|
1729
|
-
if (ctx.entityManager?.isAttachmentStreamPath(streamPath)) return new Response(null, { status: 404 });
|
|
1730
2201
|
const upstream = await forwardToDurableStreams(ctx, request);
|
|
1731
2202
|
const method = request.method.toUpperCase();
|
|
1732
2203
|
const endTrackedRead = method === `GET` ? await ctx.entityBridgeManager.beginClientRead(streamPath) : null;
|
|
@@ -1737,6 +2208,51 @@ async function proxyPassThrough(request, ctx) {
|
|
|
1737
2208
|
await endTrackedRead?.();
|
|
1738
2209
|
}
|
|
1739
2210
|
}
|
|
2211
|
+
async function authorizeDurableStreamAccess(request, ctx) {
|
|
2212
|
+
const method = request.method.toUpperCase();
|
|
2213
|
+
const streamPath = new URL(request.url).pathname;
|
|
2214
|
+
if (method === `GET` || method === `HEAD`) {
|
|
2215
|
+
const registry = ctx.entityManager?.registry;
|
|
2216
|
+
const entity = registry?.getEntityByStream ? await registry.getEntityByStream(streamPath) : null;
|
|
2217
|
+
if (entity) {
|
|
2218
|
+
if (await canAccessEntity(ctx, entity, `read`, request)) return void 0;
|
|
2219
|
+
return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to read ${entity.url}`);
|
|
2220
|
+
}
|
|
2221
|
+
const attachmentEntityUrl = entityUrlFromAttachmentStreamPath(streamPath);
|
|
2222
|
+
if (attachmentEntityUrl) {
|
|
2223
|
+
const attachmentEntity = registry?.getEntity ? await registry.getEntity(attachmentEntityUrl) : null;
|
|
2224
|
+
if (!attachmentEntity) return apiError(404, ErrCodeNotFound, `Entity not found`);
|
|
2225
|
+
if (await canAccessEntity(ctx, attachmentEntity, `read`, request)) return void 0;
|
|
2226
|
+
return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to read ${attachmentEntity.url}`);
|
|
2227
|
+
}
|
|
2228
|
+
}
|
|
2229
|
+
const sharedStateId = sharedStateIdFromPath(streamPath);
|
|
2230
|
+
if (!sharedStateId) return void 0;
|
|
2231
|
+
if (method === `GET` || method === `HEAD`) {
|
|
2232
|
+
if (await canAccessSharedState(ctx, sharedStateId, `read`, request)) return void 0;
|
|
2233
|
+
return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to read shared state`);
|
|
2234
|
+
}
|
|
2235
|
+
if (method === `PUT` || method === `POST`) {
|
|
2236
|
+
const ownerEntityUrl = request.headers.get(SHARED_STATE_OWNER_ENTITY_HEADER)?.trim() || void 0;
|
|
2237
|
+
if (await canAccessSharedState(ctx, sharedStateId, `write`, request, ownerEntityUrl)) return void 0;
|
|
2238
|
+
return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to write shared state`);
|
|
2239
|
+
}
|
|
2240
|
+
return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to access shared state`);
|
|
2241
|
+
}
|
|
2242
|
+
function entityUrlFromAttachmentStreamPath(path$1) {
|
|
2243
|
+
const match = path$1.match(/^\/([^/]+)\/([^/]+)\/attachments\/[^/]+$/);
|
|
2244
|
+
if (!match) return null;
|
|
2245
|
+
return `/${match[1]}/${match[2]}`;
|
|
2246
|
+
}
|
|
2247
|
+
function sharedStateIdFromPath(path$1) {
|
|
2248
|
+
const match = path$1.match(/^\/_electric\/shared-state\/([^/]+)$/);
|
|
2249
|
+
if (!match) return null;
|
|
2250
|
+
try {
|
|
2251
|
+
return decodeURIComponent(match[1]);
|
|
2252
|
+
} catch {
|
|
2253
|
+
return match[1];
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
1740
2256
|
|
|
1741
2257
|
//#endregion
|
|
1742
2258
|
//#region src/routing/electric-proxy-router.ts
|
|
@@ -1744,12 +2260,15 @@ const electricProxyRouter = Router({ base: `/_electric/electric` });
|
|
|
1744
2260
|
electricProxyRouter.get(`/*`, proxyElectric);
|
|
1745
2261
|
async function proxyElectric(request, ctx) {
|
|
1746
2262
|
if (!ctx.electricUrl) return apiError(500, `ELECTRIC_PROXY_FAILED`, `Electric URL not configured`);
|
|
2263
|
+
await ctx.entityManager.registry.pruneExpiredPermissionGrants?.();
|
|
1747
2264
|
const target = buildElectricProxyTarget({
|
|
1748
2265
|
incomingUrl: new URL(request.url),
|
|
1749
2266
|
electricUrl: ctx.electricUrl,
|
|
1750
2267
|
electricSecret: ctx.electricSecret,
|
|
1751
2268
|
tenantId: ctx.service,
|
|
1752
|
-
principalUrl: ctx.principal.url
|
|
2269
|
+
principalUrl: ctx.principal.url,
|
|
2270
|
+
principalKind: ctx.principal.kind,
|
|
2271
|
+
permissionBypass: isPermissionBypassPrincipal(ctx)
|
|
1753
2272
|
});
|
|
1754
2273
|
const headers = new Headers(request.headers);
|
|
1755
2274
|
headers.delete(`host`);
|
|
@@ -1769,110 +2288,6 @@ async function proxyElectric(request, ctx) {
|
|
|
1769
2288
|
});
|
|
1770
2289
|
}
|
|
1771
2290
|
|
|
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
2291
|
//#endregion
|
|
1877
2292
|
//#region src/dispatch-policy-schema.ts
|
|
1878
2293
|
const nonEmptyStringSchema = Type.String({ minLength: 1 });
|
|
@@ -2046,16 +2461,26 @@ function isDuplicateUrlError(err) {
|
|
|
2046
2461
|
return e.code === `23505`;
|
|
2047
2462
|
}
|
|
2048
2463
|
const DEFAULT_RUNNER_LEASE_MS = 3e4;
|
|
2464
|
+
const PERMISSION_PRUNE_INTERVAL_MS = 3e4;
|
|
2049
2465
|
function runnerWakeStream(runnerId) {
|
|
2050
2466
|
return `/runners/${runnerId}/wake`;
|
|
2051
2467
|
}
|
|
2052
2468
|
var PostgresRegistry = class {
|
|
2469
|
+
lastPermissionPruneStartedAt = 0;
|
|
2470
|
+
permissionPrunePromise = null;
|
|
2053
2471
|
constructor(db, tenantId = DEFAULT_TENANT_ID) {
|
|
2054
2472
|
this.db = db;
|
|
2055
2473
|
this.tenantId = tenantId;
|
|
2056
2474
|
}
|
|
2057
2475
|
async initialize() {}
|
|
2058
2476
|
close() {}
|
|
2477
|
+
async ensureUserForPrincipal(principal) {
|
|
2478
|
+
if (principal.kind !== `user`) return;
|
|
2479
|
+
await this.db.insert(users).values({
|
|
2480
|
+
tenantId: this.tenantId,
|
|
2481
|
+
id: principal.id
|
|
2482
|
+
}).onConflictDoNothing();
|
|
2483
|
+
}
|
|
2059
2484
|
async createRunner(input) {
|
|
2060
2485
|
const now = new Date();
|
|
2061
2486
|
const wakeStream = input.wakeStream ?? runnerWakeStream(input.id);
|
|
@@ -2390,6 +2815,59 @@ var PostgresRegistry = class {
|
|
|
2390
2815
|
pendingSourceStreams: [],
|
|
2391
2816
|
updatedAt: new Date()
|
|
2392
2817
|
}).onConflictDoNothing();
|
|
2818
|
+
await tx.insert(entityLineage).values({
|
|
2819
|
+
tenantId: this.tenantId,
|
|
2820
|
+
ancestorUrl: entity.url,
|
|
2821
|
+
descendantUrl: entity.url,
|
|
2822
|
+
depth: 0
|
|
2823
|
+
}).onConflictDoNothing();
|
|
2824
|
+
if (entity.parent) await tx.execute(sql`
|
|
2825
|
+
INSERT INTO ${entityLineage} (
|
|
2826
|
+
tenant_id,
|
|
2827
|
+
ancestor_url,
|
|
2828
|
+
descendant_url,
|
|
2829
|
+
depth
|
|
2830
|
+
)
|
|
2831
|
+
SELECT
|
|
2832
|
+
${this.tenantId},
|
|
2833
|
+
ancestor_url,
|
|
2834
|
+
${entity.url},
|
|
2835
|
+
depth + 1
|
|
2836
|
+
FROM ${entityLineage}
|
|
2837
|
+
WHERE tenant_id = ${this.tenantId}
|
|
2838
|
+
AND descendant_url = ${entity.parent}
|
|
2839
|
+
ON CONFLICT DO NOTHING
|
|
2840
|
+
`);
|
|
2841
|
+
await tx.execute(sql`
|
|
2842
|
+
INSERT INTO ${entityEffectivePermissions} (
|
|
2843
|
+
tenant_id,
|
|
2844
|
+
entity_url,
|
|
2845
|
+
source_entity_url,
|
|
2846
|
+
source_grant_id,
|
|
2847
|
+
permission,
|
|
2848
|
+
subject_kind,
|
|
2849
|
+
subject_value,
|
|
2850
|
+
expires_at
|
|
2851
|
+
)
|
|
2852
|
+
SELECT
|
|
2853
|
+
${this.tenantId},
|
|
2854
|
+
${entity.url},
|
|
2855
|
+
grants.entity_url,
|
|
2856
|
+
grants.id,
|
|
2857
|
+
grants.permission,
|
|
2858
|
+
grants.subject_kind,
|
|
2859
|
+
grants.subject_value,
|
|
2860
|
+
grants.expires_at
|
|
2861
|
+
FROM ${entityPermissionGrants} grants
|
|
2862
|
+
JOIN ${entityLineage} lineage
|
|
2863
|
+
ON lineage.tenant_id = grants.tenant_id
|
|
2864
|
+
AND lineage.ancestor_url = grants.entity_url
|
|
2865
|
+
AND lineage.descendant_url = ${entity.url}
|
|
2866
|
+
WHERE grants.tenant_id = ${this.tenantId}
|
|
2867
|
+
AND grants.propagation = 'descendants'
|
|
2868
|
+
AND (grants.expires_at IS NULL OR grants.expires_at > now())
|
|
2869
|
+
ON CONFLICT DO NOTHING
|
|
2870
|
+
`);
|
|
2393
2871
|
return parseInt(result[0].txid);
|
|
2394
2872
|
});
|
|
2395
2873
|
} catch (err) {
|
|
@@ -2411,10 +2889,8 @@ var PostgresRegistry = class {
|
|
|
2411
2889
|
}
|
|
2412
2890
|
async getEntityByStream(streamPath) {
|
|
2413
2891
|
const mainSuffix = `/main`;
|
|
2414
|
-
const errorSuffix = `/error`;
|
|
2415
2892
|
let entityUrl = null;
|
|
2416
2893
|
if (streamPath.endsWith(mainSuffix)) entityUrl = streamPath.slice(0, -mainSuffix.length);
|
|
2417
|
-
else if (streamPath.endsWith(errorSuffix)) entityUrl = streamPath.slice(0, -errorSuffix.length);
|
|
2418
2894
|
if (!entityUrl) return null;
|
|
2419
2895
|
return this.getEntity(entityUrl);
|
|
2420
2896
|
}
|
|
@@ -2424,6 +2900,23 @@ var PostgresRegistry = class {
|
|
|
2424
2900
|
if (filter?.status) conditions.push(eq(entities.status, filter.status));
|
|
2425
2901
|
if (filter?.parent) conditions.push(eq(entities.parent, filter.parent));
|
|
2426
2902
|
if (filter?.created_by) conditions.push(eq(entities.createdBy, filter.created_by));
|
|
2903
|
+
if (filter?.readableBy && !filter.readableBy.bypass) conditions.push(sql`(
|
|
2904
|
+
${entities.createdBy} = ${filter.readableBy.principalUrl}
|
|
2905
|
+
OR ${entities.url} IN (
|
|
2906
|
+
SELECT ${entityEffectivePermissions.entityUrl}
|
|
2907
|
+
FROM ${entityEffectivePermissions}
|
|
2908
|
+
WHERE ${entityEffectivePermissions.tenantId} = ${this.tenantId}
|
|
2909
|
+
AND ${entityEffectivePermissions.permission} IN ('read', 'manage')
|
|
2910
|
+
AND (${entityEffectivePermissions.expiresAt} IS NULL OR ${entityEffectivePermissions.expiresAt} > now())
|
|
2911
|
+
AND (
|
|
2912
|
+
(${entityEffectivePermissions.subjectKind} = 'principal'
|
|
2913
|
+
AND ${entityEffectivePermissions.subjectValue} = ${filter.readableBy.principalUrl})
|
|
2914
|
+
OR
|
|
2915
|
+
(${entityEffectivePermissions.subjectKind} = 'principal_kind'
|
|
2916
|
+
AND ${entityEffectivePermissions.subjectValue} = ${filter.readableBy.principalKind})
|
|
2917
|
+
)
|
|
2918
|
+
)
|
|
2919
|
+
)`);
|
|
2427
2920
|
const whereClause = and(...conditions);
|
|
2428
2921
|
const countResult = await this.db.select({ count: sql`count(*)` }).from(entities).where(whereClause);
|
|
2429
2922
|
const total = Number(countResult[0].count);
|
|
@@ -2436,6 +2929,189 @@ var PostgresRegistry = class {
|
|
|
2436
2929
|
total
|
|
2437
2930
|
};
|
|
2438
2931
|
}
|
|
2932
|
+
async createEntityTypePermissionGrant(input) {
|
|
2933
|
+
const [row] = await this.db.insert(entityTypePermissionGrants).values({
|
|
2934
|
+
tenantId: this.tenantId,
|
|
2935
|
+
entityType: input.entityType,
|
|
2936
|
+
permission: input.permission,
|
|
2937
|
+
subjectKind: input.subjectKind,
|
|
2938
|
+
subjectValue: input.subjectValue,
|
|
2939
|
+
createdBy: input.createdBy ?? null,
|
|
2940
|
+
expiresAt: input.expiresAt ?? null
|
|
2941
|
+
}).returning();
|
|
2942
|
+
return this.rowToEntityTypePermissionGrant(row);
|
|
2943
|
+
}
|
|
2944
|
+
async ensureEntityTypePermissionGrant(input) {
|
|
2945
|
+
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);
|
|
2946
|
+
if (existing) return this.rowToEntityTypePermissionGrant(existing);
|
|
2947
|
+
return await this.createEntityTypePermissionGrant(input);
|
|
2948
|
+
}
|
|
2949
|
+
async listEntityTypePermissionGrants(entityType) {
|
|
2950
|
+
const rows = await this.db.select().from(entityTypePermissionGrants).where(and(eq(entityTypePermissionGrants.tenantId, this.tenantId), eq(entityTypePermissionGrants.entityType, entityType))).orderBy(entityTypePermissionGrants.id);
|
|
2951
|
+
return rows.map((row) => this.rowToEntityTypePermissionGrant(row));
|
|
2952
|
+
}
|
|
2953
|
+
async deleteEntityTypePermissionGrant(entityType, grantId) {
|
|
2954
|
+
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 });
|
|
2955
|
+
return rows.length > 0;
|
|
2956
|
+
}
|
|
2957
|
+
async hasEntityTypePermission(entityType, permission, subject) {
|
|
2958
|
+
const permissions = [permission, `manage`];
|
|
2959
|
+
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`(
|
|
2960
|
+
(${entityTypePermissionGrants.subjectKind} = 'principal'
|
|
2961
|
+
AND ${entityTypePermissionGrants.subjectValue} = ${subject.principalUrl})
|
|
2962
|
+
OR
|
|
2963
|
+
(${entityTypePermissionGrants.subjectKind} = 'principal_kind'
|
|
2964
|
+
AND ${entityTypePermissionGrants.subjectValue} = ${subject.principalKind})
|
|
2965
|
+
)`)).limit(1);
|
|
2966
|
+
return rows.length > 0;
|
|
2967
|
+
}
|
|
2968
|
+
async createEntityPermissionGrant(input) {
|
|
2969
|
+
return await this.db.transaction(async (tx) => {
|
|
2970
|
+
const [row] = await tx.insert(entityPermissionGrants).values({
|
|
2971
|
+
tenantId: this.tenantId,
|
|
2972
|
+
entityUrl: input.entityUrl,
|
|
2973
|
+
permission: input.permission,
|
|
2974
|
+
subjectKind: input.subjectKind,
|
|
2975
|
+
subjectValue: input.subjectValue,
|
|
2976
|
+
propagation: input.propagation ?? `self`,
|
|
2977
|
+
copyToChildren: input.copyToChildren ?? false,
|
|
2978
|
+
createdBy: input.createdBy ?? null,
|
|
2979
|
+
expiresAt: input.expiresAt ?? null
|
|
2980
|
+
}).returning();
|
|
2981
|
+
await this.materializeEntityPermissionGrant(tx, row);
|
|
2982
|
+
return this.rowToEntityPermissionGrant(row);
|
|
2983
|
+
});
|
|
2984
|
+
}
|
|
2985
|
+
async listEntityPermissionGrants(entityUrl) {
|
|
2986
|
+
const rows = await this.db.select().from(entityPermissionGrants).where(and(eq(entityPermissionGrants.tenantId, this.tenantId), eq(entityPermissionGrants.entityUrl, entityUrl))).orderBy(entityPermissionGrants.id);
|
|
2987
|
+
return rows.map((row) => this.rowToEntityPermissionGrant(row));
|
|
2988
|
+
}
|
|
2989
|
+
async deleteEntityPermissionGrant(entityUrl, grantId) {
|
|
2990
|
+
return await this.db.transaction(async (tx) => {
|
|
2991
|
+
await tx.delete(entityEffectivePermissions).where(and(eq(entityEffectivePermissions.tenantId, this.tenantId), eq(entityEffectivePermissions.sourceGrantId, grantId)));
|
|
2992
|
+
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 });
|
|
2993
|
+
return rows.length > 0;
|
|
2994
|
+
});
|
|
2995
|
+
}
|
|
2996
|
+
async copyEntityPermissionGrantsForSpawn(parentEntityUrl, childEntityUrl, createdBy) {
|
|
2997
|
+
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())`));
|
|
2998
|
+
const copied = [];
|
|
2999
|
+
for (const grant of parentGrants) copied.push(await this.createEntityPermissionGrant({
|
|
3000
|
+
entityUrl: childEntityUrl,
|
|
3001
|
+
permission: grant.permission,
|
|
3002
|
+
subjectKind: grant.subjectKind,
|
|
3003
|
+
subjectValue: grant.subjectValue,
|
|
3004
|
+
propagation: `self`,
|
|
3005
|
+
copyToChildren: grant.copyToChildren,
|
|
3006
|
+
createdBy,
|
|
3007
|
+
expiresAt: grant.expiresAt ?? void 0
|
|
3008
|
+
}));
|
|
3009
|
+
return copied;
|
|
3010
|
+
}
|
|
3011
|
+
async hasEntityPermission(entityUrl, permission, subject) {
|
|
3012
|
+
const permissions = [permission, `manage`];
|
|
3013
|
+
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`(
|
|
3014
|
+
(${entityEffectivePermissions.subjectKind} = 'principal'
|
|
3015
|
+
AND ${entityEffectivePermissions.subjectValue} = ${subject.principalUrl})
|
|
3016
|
+
OR
|
|
3017
|
+
(${entityEffectivePermissions.subjectKind} = 'principal_kind'
|
|
3018
|
+
AND ${entityEffectivePermissions.subjectValue} = ${subject.principalKind})
|
|
3019
|
+
)`)).limit(1);
|
|
3020
|
+
return rows.length > 0;
|
|
3021
|
+
}
|
|
3022
|
+
async replaceSharedStateLink(ownerEntityUrl, manifestKey, sharedStateId) {
|
|
3023
|
+
await this.db.delete(sharedStateLinks).where(and(eq(sharedStateLinks.tenantId, this.tenantId), eq(sharedStateLinks.ownerEntityUrl, ownerEntityUrl), eq(sharedStateLinks.manifestKey, manifestKey)));
|
|
3024
|
+
if (!sharedStateId) return;
|
|
3025
|
+
await this.db.insert(sharedStateLinks).values({
|
|
3026
|
+
tenantId: this.tenantId,
|
|
3027
|
+
ownerEntityUrl,
|
|
3028
|
+
manifestKey,
|
|
3029
|
+
sharedStateId
|
|
3030
|
+
}).onConflictDoUpdate({
|
|
3031
|
+
target: [
|
|
3032
|
+
sharedStateLinks.tenantId,
|
|
3033
|
+
sharedStateLinks.ownerEntityUrl,
|
|
3034
|
+
sharedStateLinks.manifestKey
|
|
3035
|
+
],
|
|
3036
|
+
set: {
|
|
3037
|
+
sharedStateId,
|
|
3038
|
+
updatedAt: new Date()
|
|
3039
|
+
}
|
|
3040
|
+
});
|
|
3041
|
+
}
|
|
3042
|
+
async listSharedStateLinkedEntityUrls(sharedStateId) {
|
|
3043
|
+
const rows = await this.db.selectDistinct({ ownerEntityUrl: sharedStateLinks.ownerEntityUrl }).from(sharedStateLinks).where(and(eq(sharedStateLinks.tenantId, this.tenantId), eq(sharedStateLinks.sharedStateId, sharedStateId)));
|
|
3044
|
+
return rows.map((row) => row.ownerEntityUrl);
|
|
3045
|
+
}
|
|
3046
|
+
async pruneExpiredPermissionGrants(now = new Date(), options = {}) {
|
|
3047
|
+
if (this.permissionPrunePromise) return await this.permissionPrunePromise;
|
|
3048
|
+
const startedAt = Date.now();
|
|
3049
|
+
if (!options.force && startedAt - this.lastPermissionPruneStartedAt < PERMISSION_PRUNE_INTERVAL_MS) return;
|
|
3050
|
+
this.lastPermissionPruneStartedAt = startedAt;
|
|
3051
|
+
const promise = this.pruneExpiredPermissionGrantsNow(now);
|
|
3052
|
+
this.permissionPrunePromise = promise;
|
|
3053
|
+
try {
|
|
3054
|
+
await promise;
|
|
3055
|
+
} catch (error) {
|
|
3056
|
+
this.lastPermissionPruneStartedAt = 0;
|
|
3057
|
+
throw error;
|
|
3058
|
+
} finally {
|
|
3059
|
+
if (this.permissionPrunePromise === promise) this.permissionPrunePromise = null;
|
|
3060
|
+
}
|
|
3061
|
+
}
|
|
3062
|
+
async pruneExpiredPermissionGrantsNow(now) {
|
|
3063
|
+
await this.db.transaction(async (tx) => {
|
|
3064
|
+
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)));
|
|
3065
|
+
const ids = expiredEntityGrantIds.map((row) => row.id);
|
|
3066
|
+
if (ids.length > 0) {
|
|
3067
|
+
await tx.delete(entityEffectivePermissions).where(and(eq(entityEffectivePermissions.tenantId, this.tenantId), inArray(entityEffectivePermissions.sourceGrantId, ids)));
|
|
3068
|
+
await tx.delete(entityPermissionGrants).where(and(eq(entityPermissionGrants.tenantId, this.tenantId), inArray(entityPermissionGrants.id, ids)));
|
|
3069
|
+
}
|
|
3070
|
+
await tx.delete(entityEffectivePermissions).where(and(eq(entityEffectivePermissions.tenantId, this.tenantId), sql`${entityEffectivePermissions.expiresAt} IS NOT NULL`, lt(entityEffectivePermissions.expiresAt, now)));
|
|
3071
|
+
await tx.delete(entityTypePermissionGrants).where(and(eq(entityTypePermissionGrants.tenantId, this.tenantId), sql`${entityTypePermissionGrants.expiresAt} IS NOT NULL`, lt(entityTypePermissionGrants.expiresAt, now)));
|
|
3072
|
+
});
|
|
3073
|
+
}
|
|
3074
|
+
async materializeEntityPermissionGrant(tx, grant) {
|
|
3075
|
+
await tx.delete(entityEffectivePermissions).where(and(eq(entityEffectivePermissions.tenantId, this.tenantId), eq(entityEffectivePermissions.sourceGrantId, grant.id)));
|
|
3076
|
+
if (grant.propagation === `descendants`) {
|
|
3077
|
+
await tx.execute(sql`
|
|
3078
|
+
INSERT INTO ${entityEffectivePermissions} (
|
|
3079
|
+
tenant_id,
|
|
3080
|
+
entity_url,
|
|
3081
|
+
source_entity_url,
|
|
3082
|
+
source_grant_id,
|
|
3083
|
+
permission,
|
|
3084
|
+
subject_kind,
|
|
3085
|
+
subject_value,
|
|
3086
|
+
expires_at
|
|
3087
|
+
)
|
|
3088
|
+
SELECT
|
|
3089
|
+
${this.tenantId},
|
|
3090
|
+
descendant_url,
|
|
3091
|
+
${grant.entityUrl},
|
|
3092
|
+
${grant.id},
|
|
3093
|
+
${grant.permission},
|
|
3094
|
+
${grant.subjectKind},
|
|
3095
|
+
${grant.subjectValue},
|
|
3096
|
+
${grant.expiresAt}
|
|
3097
|
+
FROM ${entityLineage}
|
|
3098
|
+
WHERE tenant_id = ${this.tenantId}
|
|
3099
|
+
AND ancestor_url = ${grant.entityUrl}
|
|
3100
|
+
ON CONFLICT DO NOTHING
|
|
3101
|
+
`);
|
|
3102
|
+
return;
|
|
3103
|
+
}
|
|
3104
|
+
await tx.insert(entityEffectivePermissions).values({
|
|
3105
|
+
tenantId: this.tenantId,
|
|
3106
|
+
entityUrl: grant.entityUrl,
|
|
3107
|
+
sourceEntityUrl: grant.entityUrl,
|
|
3108
|
+
sourceGrantId: grant.id,
|
|
3109
|
+
permission: grant.permission,
|
|
3110
|
+
subjectKind: grant.subjectKind,
|
|
3111
|
+
subjectValue: grant.subjectValue,
|
|
3112
|
+
expiresAt: grant.expiresAt
|
|
3113
|
+
}).onConflictDoNothing();
|
|
3114
|
+
}
|
|
2439
3115
|
async updateStatus(entityUrl, status$1) {
|
|
2440
3116
|
const whereClause = isTerminalEntityStatus(status$1) ? this.entityWhere(entityUrl) : and(this.entityWhere(entityUrl), ne(entities.status, `stopped`), ne(entities.status, `killed`));
|
|
2441
3117
|
await this.db.update(entities).set({
|
|
@@ -2537,7 +3213,9 @@ var PostgresRegistry = class {
|
|
|
2537
3213
|
tenantId: this.tenantId,
|
|
2538
3214
|
sourceRef: row.sourceRef,
|
|
2539
3215
|
tags: normalizeTags(row.tags),
|
|
2540
|
-
streamUrl: row.streamUrl
|
|
3216
|
+
streamUrl: row.streamUrl,
|
|
3217
|
+
principalUrl: row.principalUrl,
|
|
3218
|
+
principalKind: row.principalKind
|
|
2541
3219
|
}).onConflictDoNothing();
|
|
2542
3220
|
const existing = await this.getEntityBridge(row.sourceRef);
|
|
2543
3221
|
if (!existing) throw new Error(`Failed to load entity bridge ${row.sourceRef}`);
|
|
@@ -2699,15 +3377,40 @@ var PostgresRegistry = class {
|
|
|
2699
3377
|
updated_at: row.updatedAt
|
|
2700
3378
|
};
|
|
2701
3379
|
}
|
|
3380
|
+
rowToEntityTypePermissionGrant(row) {
|
|
3381
|
+
return {
|
|
3382
|
+
id: row.id,
|
|
3383
|
+
entity_type: row.entityType,
|
|
3384
|
+
permission: row.permission,
|
|
3385
|
+
subject_kind: row.subjectKind,
|
|
3386
|
+
subject_value: row.subjectValue,
|
|
3387
|
+
created_by: row.createdBy ?? void 0,
|
|
3388
|
+
expires_at: row.expiresAt?.toISOString(),
|
|
3389
|
+
created_at: row.createdAt.toISOString(),
|
|
3390
|
+
updated_at: row.updatedAt.toISOString()
|
|
3391
|
+
};
|
|
3392
|
+
}
|
|
3393
|
+
rowToEntityPermissionGrant(row) {
|
|
3394
|
+
return {
|
|
3395
|
+
id: row.id,
|
|
3396
|
+
entity_url: row.entityUrl,
|
|
3397
|
+
permission: row.permission,
|
|
3398
|
+
subject_kind: row.subjectKind,
|
|
3399
|
+
subject_value: row.subjectValue,
|
|
3400
|
+
propagation: row.propagation,
|
|
3401
|
+
copy_to_children: row.copyToChildren,
|
|
3402
|
+
created_by: row.createdBy ?? void 0,
|
|
3403
|
+
expires_at: row.expiresAt?.toISOString(),
|
|
3404
|
+
created_at: row.createdAt.toISOString(),
|
|
3405
|
+
updated_at: row.updatedAt.toISOString()
|
|
3406
|
+
};
|
|
3407
|
+
}
|
|
2702
3408
|
rowToEntity(row) {
|
|
2703
3409
|
return {
|
|
2704
3410
|
url: row.url,
|
|
2705
3411
|
type: row.type,
|
|
2706
3412
|
status: assertEntityStatus(row.status),
|
|
2707
|
-
streams: {
|
|
2708
|
-
main: `${row.url}/main`,
|
|
2709
|
-
error: `${row.url}/error`
|
|
2710
|
-
},
|
|
3413
|
+
streams: { main: `${row.url}/main` },
|
|
2711
3414
|
subscription_id: row.subscriptionId,
|
|
2712
3415
|
dispatch_policy: row.dispatchPolicy ?? void 0,
|
|
2713
3416
|
write_token: row.writeToken,
|
|
@@ -2729,6 +3432,8 @@ var PostgresRegistry = class {
|
|
|
2729
3432
|
sourceRef: row.sourceRef,
|
|
2730
3433
|
tags: row.tags ?? {},
|
|
2731
3434
|
streamUrl: row.streamUrl,
|
|
3435
|
+
principalUrl: row.principalUrl ?? void 0,
|
|
3436
|
+
principalKind: row.principalKind ?? void 0,
|
|
2732
3437
|
shapeHandle: row.shapeHandle ?? void 0,
|
|
2733
3438
|
shapeOffset: row.shapeOffset ?? void 0,
|
|
2734
3439
|
lastObserverActivityAt: row.lastObserverActivityAt,
|
|
@@ -3031,7 +3736,10 @@ var EntityManager = class {
|
|
|
3031
3736
|
}
|
|
3032
3737
|
async ensurePrincipal(principal) {
|
|
3033
3738
|
const existing = await this.registry.getEntity(principal.url);
|
|
3034
|
-
if (existing)
|
|
3739
|
+
if (existing) {
|
|
3740
|
+
await this.ensureUserPrincipal(principal);
|
|
3741
|
+
return existing;
|
|
3742
|
+
}
|
|
3035
3743
|
await this.ensurePrincipalEntityType();
|
|
3036
3744
|
try {
|
|
3037
3745
|
const entity = await this.spawn(`principal`, {
|
|
@@ -3060,15 +3768,22 @@ var EntityManager = class {
|
|
|
3060
3768
|
updated_at: now
|
|
3061
3769
|
}
|
|
3062
3770
|
}));
|
|
3771
|
+
await this.ensureUserPrincipal(principal);
|
|
3063
3772
|
return entity;
|
|
3064
3773
|
} catch (error) {
|
|
3065
3774
|
if (error instanceof ElectricAgentsError && error.code === ErrCodeDuplicateURL) {
|
|
3066
3775
|
const raced = await this.registry.getEntity(principal.url);
|
|
3067
|
-
if (raced)
|
|
3776
|
+
if (raced) {
|
|
3777
|
+
await this.ensureUserPrincipal(principal);
|
|
3778
|
+
return raced;
|
|
3779
|
+
}
|
|
3068
3780
|
}
|
|
3069
3781
|
throw error;
|
|
3070
3782
|
}
|
|
3071
3783
|
}
|
|
3784
|
+
async ensureUserPrincipal(principal) {
|
|
3785
|
+
if (principal.kind === `user`) await this.registry.ensureUserForPrincipal(principal);
|
|
3786
|
+
}
|
|
3072
3787
|
/**
|
|
3073
3788
|
* Spawn a new entity of the given type with durable streams.
|
|
3074
3789
|
*/
|
|
@@ -3098,7 +3813,6 @@ var EntityManager = class {
|
|
|
3098
3813
|
const writeToken = randomUUID();
|
|
3099
3814
|
const entityURL = typeName === `principal` ? principalUrl(instanceId) : `/${typeName}/${instanceId}`;
|
|
3100
3815
|
const mainPath = `${entityURL}/main`;
|
|
3101
|
-
const errorPath = `${entityURL}/error`;
|
|
3102
3816
|
const subscriptionId = `${typeName}-handler`;
|
|
3103
3817
|
const spawnT0 = performance.now();
|
|
3104
3818
|
const existingByURL = await this.registry.getEntity(entityURL);
|
|
@@ -3115,10 +3829,7 @@ var EntityManager = class {
|
|
|
3115
3829
|
type: typeName,
|
|
3116
3830
|
status: `idle`,
|
|
3117
3831
|
url: entityURL,
|
|
3118
|
-
streams: {
|
|
3119
|
-
main: mainPath,
|
|
3120
|
-
error: errorPath
|
|
3121
|
-
},
|
|
3832
|
+
streams: { main: mainPath },
|
|
3122
3833
|
subscription_id: subscriptionId,
|
|
3123
3834
|
dispatch_policy: dispatchPolicy,
|
|
3124
3835
|
write_token: writeToken,
|
|
@@ -3171,55 +3882,43 @@ var EntityManager = class {
|
|
|
3171
3882
|
const queueEnterT0 = performance.now();
|
|
3172
3883
|
const queueWaiting = this.spawnPersistQueue.length();
|
|
3173
3884
|
const queueRunning = this.spawnPersistQueue.running();
|
|
3174
|
-
const [mainStreamResult,
|
|
3885
|
+
const [mainStreamResult, entityResult] = await this.spawnPersistQueue.push(async () => {
|
|
3175
3886
|
let entityTxid;
|
|
3176
3887
|
try {
|
|
3177
3888
|
entityTxid = await withSpan(`db.createEntity`, () => this.registry.createEntity(entityData));
|
|
3178
3889
|
} catch (err) {
|
|
3179
|
-
return [
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
value: void 0
|
|
3187
|
-
},
|
|
3188
|
-
{
|
|
3189
|
-
status: `rejected`,
|
|
3190
|
-
reason: err
|
|
3191
|
-
}
|
|
3192
|
-
];
|
|
3890
|
+
return [{
|
|
3891
|
+
status: `fulfilled`,
|
|
3892
|
+
value: void 0
|
|
3893
|
+
}, {
|
|
3894
|
+
status: `rejected`,
|
|
3895
|
+
reason: err
|
|
3896
|
+
}];
|
|
3193
3897
|
}
|
|
3194
|
-
const [mainStreamResult$1
|
|
3898
|
+
const [mainStreamResult$1] = await Promise.allSettled([this.streamClient.create(mainPath, {
|
|
3195
3899
|
contentType,
|
|
3196
3900
|
body: initialBody
|
|
3197
|
-
})
|
|
3198
|
-
return [
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
status: `fulfilled`,
|
|
3203
|
-
value: entityTxid
|
|
3204
|
-
}
|
|
3205
|
-
];
|
|
3901
|
+
})]);
|
|
3902
|
+
return [mainStreamResult$1, {
|
|
3903
|
+
status: `fulfilled`,
|
|
3904
|
+
value: entityTxid
|
|
3905
|
+
}];
|
|
3206
3906
|
});
|
|
3207
3907
|
const parallelMs = +(performance.now() - queueEnterT0).toFixed(2);
|
|
3208
|
-
if (mainStreamResult.status === `rejected` ||
|
|
3908
|
+
if (mainStreamResult.status === `rejected` || entityResult.status === `rejected`) {
|
|
3209
3909
|
const entityReason = entityResult.status === `rejected` ? entityResult.reason : null;
|
|
3210
|
-
const streamReason = mainStreamResult.status === `rejected` ? mainStreamResult.reason :
|
|
3910
|
+
const streamReason = mainStreamResult.status === `rejected` ? mainStreamResult.reason : null;
|
|
3211
3911
|
const isDuplicate = entityReason instanceof EntityAlreadyExistsError;
|
|
3212
3912
|
const isStreamConflict = !!streamReason && typeof streamReason === `object` && (`status` in streamReason && streamReason.status === 409 || `code` in streamReason && streamReason.code === `CONFLICT_SEQ`);
|
|
3213
3913
|
const rollbacks = [];
|
|
3214
3914
|
if (!isDuplicate && !isStreamConflict) {
|
|
3215
3915
|
if (mainStreamResult.status === `fulfilled`) rollbacks.push(this.streamClient.delete(mainPath));
|
|
3216
|
-
if (errorStreamResult.status === `fulfilled`) rollbacks.push(this.streamClient.delete(errorPath));
|
|
3217
3916
|
if (entityResult.status === `fulfilled`) rollbacks.push(this.registry.deleteEntity(entityURL));
|
|
3218
3917
|
if (req.wake) rollbacks.push(this.wakeRegistry.unregisterBySubscriberAndSource(req.wake.subscriberUrl, entityURL, this.tenantId));
|
|
3219
3918
|
await Promise.allSettled(rollbacks);
|
|
3220
3919
|
}
|
|
3221
3920
|
if (isDuplicate || isStreamConflict) throw new ElectricAgentsError(ErrCodeDuplicateURL, `Entity already exists at URL "${entityURL}"`, 409);
|
|
3222
|
-
const failure = mainStreamResult.status === `rejected` ? mainStreamResult.reason :
|
|
3921
|
+
const failure = mainStreamResult.status === `rejected` ? mainStreamResult.reason : entityResult.reason;
|
|
3223
3922
|
if (failure instanceof Error) throw failure;
|
|
3224
3923
|
throw new ElectricAgentsError(`SPAWN_FAILED`, `Spawn failed: ${String(failure)}`, 500);
|
|
3225
3924
|
}
|
|
@@ -3304,7 +4003,7 @@ var EntityManager = class {
|
|
|
3304
4003
|
});
|
|
3305
4004
|
const sharedStateIdMap = await this.buildForkSharedStateIdMap(snapshot.sharedStateIds, suffix);
|
|
3306
4005
|
const stringMap = this.buildForkStringMap(entityUrlMap, sharedStateIdMap);
|
|
3307
|
-
const entityPlans = this.buildForkEntityPlans(effectiveSubtree, entityUrlMap, stringMap);
|
|
4006
|
+
const entityPlans = this.buildForkEntityPlans(effectiveSubtree, entityUrlMap, stringMap, opts.createdBy);
|
|
3308
4007
|
this.addForkLocks(this.forkWriteLockedEntities, effectiveSubtree.map((entity) => entity.url), writeEntityLocks);
|
|
3309
4008
|
this.addForkLocks(this.forkWriteLockedStreams, [...snapshot.sharedStateIds].map((id) => getSharedStateStreamPath(id)), writeStreamLocks);
|
|
3310
4009
|
const createdStreams = [];
|
|
@@ -3315,8 +4014,6 @@ var EntityManager = class {
|
|
|
3315
4014
|
const isRoot = plan.source.url === rootUrl;
|
|
3316
4015
|
await this.streamClient.fork(plan.fork.streams.main, plan.source.streams.main, isRoot && opts.forkPointer ? { forkPointer: opts.forkPointer } : void 0);
|
|
3317
4016
|
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
4017
|
}
|
|
3321
4018
|
for (const [sourceId, forkId] of sharedStateIdMap) {
|
|
3322
4019
|
const sourcePath = getSharedStateStreamPath(sourceId);
|
|
@@ -3650,7 +4347,6 @@ var EntityManager = class {
|
|
|
3650
4347
|
for (const [sourceUrl, forkUrl] of entityUrlMap) {
|
|
3651
4348
|
stringMap.set(sourceUrl, forkUrl);
|
|
3652
4349
|
stringMap.set(`${sourceUrl}/main`, `${forkUrl}/main`);
|
|
3653
|
-
stringMap.set(`${sourceUrl}/error`, `${forkUrl}/error`);
|
|
3654
4350
|
}
|
|
3655
4351
|
for (const [sourceId, forkId] of sharedStateIdMap) {
|
|
3656
4352
|
stringMap.set(sourceId, forkId);
|
|
@@ -3658,7 +4354,7 @@ var EntityManager = class {
|
|
|
3658
4354
|
}
|
|
3659
4355
|
return stringMap;
|
|
3660
4356
|
}
|
|
3661
|
-
buildForkEntityPlans(entitiesToFork, entityUrlMap, stringMap) {
|
|
4357
|
+
buildForkEntityPlans(entitiesToFork, entityUrlMap, stringMap, createdBy) {
|
|
3662
4358
|
const now = Date.now();
|
|
3663
4359
|
return entitiesToFork.map((source) => {
|
|
3664
4360
|
const forkUrl = entityUrlMap.get(source.url);
|
|
@@ -3671,14 +4367,12 @@ var EntityManager = class {
|
|
|
3671
4367
|
url: forkUrl,
|
|
3672
4368
|
type,
|
|
3673
4369
|
status: `idle`,
|
|
3674
|
-
streams: {
|
|
3675
|
-
main: `${forkUrl}/main`,
|
|
3676
|
-
error: `${forkUrl}/error`
|
|
3677
|
-
},
|
|
4370
|
+
streams: { main: `${forkUrl}/main` },
|
|
3678
4371
|
subscription_id: `${type}-handler`,
|
|
3679
4372
|
write_token: randomUUID(),
|
|
3680
4373
|
spawn_args: spawnArgs,
|
|
3681
4374
|
parent,
|
|
4375
|
+
created_by: createdBy ?? source.created_by,
|
|
3682
4376
|
created_at: now,
|
|
3683
4377
|
updated_at: now
|
|
3684
4378
|
};
|
|
@@ -3912,7 +4606,7 @@ var EntityManager = class {
|
|
|
3912
4606
|
}
|
|
3913
4607
|
async materializeForkManifestSideEffects(entityUrl, manifests) {
|
|
3914
4608
|
for (const [manifestKey, manifest] of manifests) {
|
|
3915
|
-
await this.
|
|
4609
|
+
await this.syncManifestLinks(entityUrl, manifestKey, `upsert`, manifest);
|
|
3916
4610
|
const wake = buildManifestWakeRegistration(entityUrl, manifest, manifestKey);
|
|
3917
4611
|
if (wake) await this.wakeRegistry.register({
|
|
3918
4612
|
...wake,
|
|
@@ -3942,6 +4636,7 @@ var EntityManager = class {
|
|
|
3942
4636
|
await this.scheduler.syncManifestDelayedSend(ownerEntityUrl, manifestKey, {
|
|
3943
4637
|
entityUrl: targetUrl,
|
|
3944
4638
|
from: senderUrl,
|
|
4639
|
+
from_agent: senderUrl,
|
|
3945
4640
|
payload: manifest.payload,
|
|
3946
4641
|
key: `scheduled-${producerId}`,
|
|
3947
4642
|
type: typeof manifest.messageType === `string` ? manifest.messageType : void 0,
|
|
@@ -3981,12 +4676,14 @@ var EntityManager = class {
|
|
|
3981
4676
|
const now = new Date().toISOString();
|
|
3982
4677
|
const key = req.key ?? `msg-in-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
3983
4678
|
const value = {
|
|
3984
|
-
from: req.from,
|
|
4679
|
+
from: req.from_principal ?? req.from,
|
|
3985
4680
|
payload: req.payload,
|
|
3986
4681
|
timestamp: now,
|
|
3987
4682
|
mode: req.mode ?? `immediate`,
|
|
3988
4683
|
status: req.mode === `queued` || req.mode === `paused` ? `pending` : `processed`
|
|
3989
4684
|
};
|
|
4685
|
+
if (req.from_principal) value.from_principal = req.from_principal;
|
|
4686
|
+
if (req.from_agent) value.from_agent = req.from_agent;
|
|
3990
4687
|
if (req.type) value.message_type = req.type;
|
|
3991
4688
|
if (req.position) value.position = req.position;
|
|
3992
4689
|
else if (value.mode === `queued` || value.mode === `paused`) value.position = createInitialQueuePosition(new Date(now));
|
|
@@ -4158,9 +4855,9 @@ var EntityManager = class {
|
|
|
4158
4855
|
if (result.changed && this.entityBridgeManager) await this.entityBridgeManager.onEntityChanged(entityUrl);
|
|
4159
4856
|
return updated;
|
|
4160
4857
|
}
|
|
4161
|
-
async ensureEntitiesMembershipStream(tags) {
|
|
4858
|
+
async ensureEntitiesMembershipStream(tags, principal) {
|
|
4162
4859
|
if (!this.entityBridgeManager) throw new Error(`Entity bridge manager not configured`);
|
|
4163
|
-
return this.entityBridgeManager.register(this.validateTags(tags));
|
|
4860
|
+
return this.entityBridgeManager.register(this.validateTags(tags), principal.url, principal.kind);
|
|
4164
4861
|
}
|
|
4165
4862
|
async writeManifestEntry(entityUrl, key, operation, value, opts) {
|
|
4166
4863
|
const entity = await this.registry.getEntity(entityUrl);
|
|
@@ -4178,11 +4875,11 @@ var EntityManager = class {
|
|
|
4178
4875
|
const encoded = this.encodeChangeEvent(event);
|
|
4179
4876
|
if (opts?.producerId) {
|
|
4180
4877
|
await this.streamClient.appendIdempotent(entity.streams.main, encoded, { producerId: opts.producerId });
|
|
4181
|
-
await this.
|
|
4878
|
+
await this.syncManifestLinks(entityUrl, key, operation, value);
|
|
4182
4879
|
return;
|
|
4183
4880
|
}
|
|
4184
4881
|
await this.streamClient.append(entity.streams.main, encoded);
|
|
4185
|
-
await this.
|
|
4882
|
+
await this.syncManifestLinks(entityUrl, key, operation, value);
|
|
4186
4883
|
}
|
|
4187
4884
|
async upsertCronSchedule(entityUrl, req) {
|
|
4188
4885
|
if (req.payload === void 0) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Missing required field: payload`, 400);
|
|
@@ -4331,6 +5028,8 @@ var EntityManager = class {
|
|
|
4331
5028
|
await this.scheduler.enqueueDelayedSend({
|
|
4332
5029
|
entityUrl,
|
|
4333
5030
|
from: req.from,
|
|
5031
|
+
from_principal: req.from_principal,
|
|
5032
|
+
from_agent: req.from_agent,
|
|
4334
5033
|
payload: req.payload,
|
|
4335
5034
|
key: req.key,
|
|
4336
5035
|
type: req.type,
|
|
@@ -4373,14 +5072,23 @@ var EntityManager = class {
|
|
|
4373
5072
|
await this.streamClient.appendIdempotent(subscriber.streams.main, this.encodeChangeEvent(wakeEvent), { producerId: `wake-reg-${result.registrationDbId}-${result.sourceEventKey}` });
|
|
4374
5073
|
});
|
|
4375
5074
|
}
|
|
4376
|
-
async
|
|
5075
|
+
async syncManifestLinks(entityUrl, manifestKey, operation, value) {
|
|
4377
5076
|
const sourceRef = operation === `delete` ? void 0 : this.extractEntitiesSourceRef(value);
|
|
4378
5077
|
await this.registry.replaceEntityManifestSource(entityUrl, manifestKey, sourceRef);
|
|
5078
|
+
const sharedStateId = operation === `delete` ? void 0 : this.extractSharedStateId(value);
|
|
5079
|
+
await this.registry.replaceSharedStateLink(entityUrl, manifestKey, sharedStateId);
|
|
4379
5080
|
}
|
|
4380
5081
|
extractEntitiesSourceRef(manifest) {
|
|
4381
5082
|
if (manifest?.kind === `source` && manifest.sourceType === `entities` && typeof manifest.sourceRef === `string`) return manifest.sourceRef;
|
|
4382
5083
|
return void 0;
|
|
4383
5084
|
}
|
|
5085
|
+
extractSharedStateId(manifest) {
|
|
5086
|
+
if (manifest?.kind === `shared-state` && typeof manifest.id === `string`) return manifest.id;
|
|
5087
|
+
if (manifest?.kind !== `source` || manifest.sourceType !== `db`) return void 0;
|
|
5088
|
+
if (typeof manifest.sourceRef === `string`) return manifest.sourceRef;
|
|
5089
|
+
const config = isRecord(manifest.config) ? manifest.config : void 0;
|
|
5090
|
+
return typeof config?.id === `string` ? config.id : void 0;
|
|
5091
|
+
}
|
|
4384
5092
|
/**
|
|
4385
5093
|
* Read a child entity's stream and extract concatenated text deltas
|
|
4386
5094
|
* for a specific run, plus any error messages for that run.
|
|
@@ -4544,14 +5252,7 @@ var EntityManager = class {
|
|
|
4544
5252
|
await this.streamClient.append(entity.streams.main, signalData);
|
|
4545
5253
|
return;
|
|
4546
5254
|
}
|
|
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 {
|
|
5255
|
+
for (const [streamPath, data] of [[entity.streams.main, signalData]]) try {
|
|
4555
5256
|
await this.streamClient.append(streamPath, data, { close: true });
|
|
4556
5257
|
} catch (err) {
|
|
4557
5258
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -4927,6 +5628,27 @@ const wakeConditionSchema = Type.Union([Type.Literal(`runFinished`), Type.Object
|
|
|
4927
5628
|
Type.Literal(`delete`)
|
|
4928
5629
|
])))
|
|
4929
5630
|
})]);
|
|
5631
|
+
const permissionSubjectSchema = Type.Object({
|
|
5632
|
+
subject_kind: Type.Union([Type.Literal(`principal`), Type.Literal(`principal_kind`)]),
|
|
5633
|
+
subject_value: Type.String()
|
|
5634
|
+
}, { additionalProperties: false });
|
|
5635
|
+
const entityPermissionSchema = Type.Union([
|
|
5636
|
+
Type.Literal(`read`),
|
|
5637
|
+
Type.Literal(`write`),
|
|
5638
|
+
Type.Literal(`delete`),
|
|
5639
|
+
Type.Literal(`signal`),
|
|
5640
|
+
Type.Literal(`fork`),
|
|
5641
|
+
Type.Literal(`schedule`),
|
|
5642
|
+
Type.Literal(`spawn`),
|
|
5643
|
+
Type.Literal(`manage`)
|
|
5644
|
+
]);
|
|
5645
|
+
const entityPermissionGrantInputSchema = Type.Object({
|
|
5646
|
+
...permissionSubjectSchema.properties,
|
|
5647
|
+
permission: entityPermissionSchema,
|
|
5648
|
+
propagation: Type.Optional(Type.Union([Type.Literal(`self`), Type.Literal(`descendants`)])),
|
|
5649
|
+
copy_to_children: Type.Optional(Type.Boolean()),
|
|
5650
|
+
expires_at: Type.Optional(Type.String())
|
|
5651
|
+
}, { additionalProperties: false });
|
|
4930
5652
|
const spawnBodySchema = Type.Object({
|
|
4931
5653
|
args: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
|
|
4932
5654
|
tags: Type.Optional(stringRecordSchema$1),
|
|
@@ -4934,6 +5656,7 @@ const spawnBodySchema = Type.Object({
|
|
|
4934
5656
|
dispatch_policy: Type.Optional(dispatchPolicySchema),
|
|
4935
5657
|
sandbox: Type.Optional(sandboxChoiceSchema),
|
|
4936
5658
|
initialMessage: Type.Optional(Type.Unknown()),
|
|
5659
|
+
grants: Type.Optional(Type.Array(entityPermissionGrantInputSchema)),
|
|
4937
5660
|
wake: Type.Optional(Type.Object({
|
|
4938
5661
|
subscriberUrl: Type.String(),
|
|
4939
5662
|
condition: wakeConditionSchema,
|
|
@@ -4955,8 +5678,22 @@ const sendBodySchema = Type.Object({
|
|
|
4955
5678
|
])),
|
|
4956
5679
|
position: Type.Optional(Type.String()),
|
|
4957
5680
|
afterMs: Type.Optional(Type.Number()),
|
|
4958
|
-
from: Type.Optional(Type.String())
|
|
5681
|
+
from: Type.Optional(Type.String()),
|
|
5682
|
+
from_principal: Type.Optional(Type.String()),
|
|
5683
|
+
from_agent: Type.Optional(Type.String())
|
|
4959
5684
|
});
|
|
5685
|
+
function agentUrlForPrincipal(principal) {
|
|
5686
|
+
if (principal.kind === `agent`) return `/${principal.id}`;
|
|
5687
|
+
if (principal.key.startsWith(`entity:`)) return `/${principal.key.slice(`entity:`.length)}`;
|
|
5688
|
+
return null;
|
|
5689
|
+
}
|
|
5690
|
+
function agentUrlPath(value) {
|
|
5691
|
+
try {
|
|
5692
|
+
return new URL(value).pathname;
|
|
5693
|
+
} catch {
|
|
5694
|
+
return value;
|
|
5695
|
+
}
|
|
5696
|
+
}
|
|
4960
5697
|
const inboxMessageBodySchema = Type.Object({
|
|
4961
5698
|
payload: Type.Optional(Type.Unknown()),
|
|
4962
5699
|
position: Type.Optional(Type.String()),
|
|
@@ -5035,24 +5772,27 @@ const attachmentSubjectTypes = new Set([
|
|
|
5035
5772
|
]);
|
|
5036
5773
|
const entitiesRouter = Router({ base: `/_electric/entities` });
|
|
5037
5774
|
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);
|
|
5775
|
+
entitiesRouter.put(`/:type/:instanceId`, withSpawnableEntityType, withSchema(spawnBodySchema), withSpawnPermission, spawnEntity);
|
|
5776
|
+
entitiesRouter.get(`/:type/:instanceId`, withExistingEntity, withEntityPermission(`read`), getEntity);
|
|
5777
|
+
entitiesRouter.head(`/:type/:instanceId`, withExistingEntity, withEntityPermission(`read`), headEntity);
|
|
5778
|
+
entitiesRouter.delete(`/:type/:instanceId`, withExistingEntity, withEntityPermission(`delete`), killEntity);
|
|
5779
|
+
entitiesRouter.post(`/:type/:instanceId/signal`, withExistingEntity, withSchema(signalBodySchema), withEntityPermission(`signal`), signalEntity);
|
|
5780
|
+
entitiesRouter.post(`/:type/:instanceId/send`, withExistingEntity, withSchema(sendBodySchema), withEntityPermission(`write`), sendEntity);
|
|
5781
|
+
entitiesRouter.post(`/:type/:instanceId/attachments`, withExistingEntity, withEntityPermission(`write`), createAttachment);
|
|
5782
|
+
entitiesRouter.get(`/:type/:instanceId/attachments/:attachmentId`, withExistingEntity, withEntityPermission(`read`), readAttachment);
|
|
5783
|
+
entitiesRouter.delete(`/:type/:instanceId/attachments/:attachmentId`, withExistingEntity, withEntityPermission(`write`), deleteAttachment);
|
|
5784
|
+
entitiesRouter.patch(`/:type/:instanceId/inbox/:messageKey`, withExistingEntity, withSchema(inboxMessageBodySchema), withEntityPermission(`write`), updateInboxMessage);
|
|
5785
|
+
entitiesRouter.delete(`/:type/:instanceId/inbox/:messageKey`, withExistingEntity, withEntityPermission(`write`), deleteInboxMessage);
|
|
5786
|
+
entitiesRouter.post(`/:type/:instanceId/fork`, withExistingEntity, withSchema(forkBodySchema), withEntityPermission(`fork`), forkEntity);
|
|
5787
|
+
entitiesRouter.post(`/:type/:instanceId/tags/:tagKey`, withExistingEntity, withSchema(setTagBodySchema), withEntityPermission(`write`), setTag);
|
|
5788
|
+
entitiesRouter.delete(`/:type/:instanceId/tags/:tagKey`, withExistingEntity, withEntityPermission(`write`), deleteTag);
|
|
5789
|
+
entitiesRouter.put(`/:type/:instanceId/schedules/:scheduleId`, withExistingEntity, withSchema(scheduleBodySchema), withEntityPermission(`schedule`), upsertSchedule);
|
|
5790
|
+
entitiesRouter.delete(`/:type/:instanceId/schedules/:scheduleId`, withExistingEntity, withEntityPermission(`schedule`), deleteSchedule);
|
|
5791
|
+
entitiesRouter.put(`/:type/:instanceId/event-source-subscriptions/:subscriptionId`, withExistingEntity, withSchema(eventSourceSubscriptionBodySchema), withEntityPermission(`write`), upsertEventSourceSubscription);
|
|
5792
|
+
entitiesRouter.delete(`/:type/:instanceId/event-source-subscriptions/:subscriptionId`, withExistingEntity, withEntityPermission(`write`), deleteEventSourceSubscription);
|
|
5793
|
+
entitiesRouter.get(`/:type/:instanceId/grants`, withExistingEntity, withEntityPermission(`manage`), listEntityPermissionGrants);
|
|
5794
|
+
entitiesRouter.post(`/:type/:instanceId/grants`, withExistingEntity, withSchema(entityPermissionGrantInputSchema), withEntityPermission(`manage`), createEntityPermissionGrant);
|
|
5795
|
+
entitiesRouter.delete(`/:type/:instanceId/grants/:grantId`, withExistingEntity, withEntityPermission(`manage`), deleteEntityPermissionGrant);
|
|
5056
5796
|
function entityUrlFromSegments(type, instanceId) {
|
|
5057
5797
|
if (!type || !instanceId) return null;
|
|
5058
5798
|
if (type.startsWith(`_`) || type.includes(`*`) || instanceId.includes(`*`)) return null;
|
|
@@ -5151,6 +5891,17 @@ function rejectPrincipalEntityMutation(request, action) {
|
|
|
5151
5891
|
if (entity.type !== `principal`) return void 0;
|
|
5152
5892
|
return apiError(400, ErrCodeInvalidRequest, `Principal entities are built in and cannot be ${action}`);
|
|
5153
5893
|
}
|
|
5894
|
+
function parseExpiresAt$1(value) {
|
|
5895
|
+
if (value === void 0) return void 0;
|
|
5896
|
+
const expiresAt = new Date(value);
|
|
5897
|
+
if (Number.isNaN(expiresAt.getTime())) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Invalid expires_at timestamp`, 400);
|
|
5898
|
+
return expiresAt;
|
|
5899
|
+
}
|
|
5900
|
+
function parseGrantId$1(request) {
|
|
5901
|
+
const grantId = Number.parseInt(String(request.params.grantId), 10);
|
|
5902
|
+
if (!Number.isSafeInteger(grantId) || grantId <= 0) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Invalid grant id`, 400);
|
|
5903
|
+
return grantId;
|
|
5904
|
+
}
|
|
5154
5905
|
async function withExistingEntity(request, ctx) {
|
|
5155
5906
|
const entityUrl = entityUrlFromSegments(request.params.type, request.params.instanceId);
|
|
5156
5907
|
if (!entityUrl) return void 0;
|
|
@@ -5181,17 +5932,76 @@ async function withSpawnableEntityType(request, ctx) {
|
|
|
5181
5932
|
if (request.params.type === `principal`) return apiError(400, ErrCodeInvalidRequest, `Principal entities are built in and cannot be spawned directly`);
|
|
5182
5933
|
const entityType = await ctx.entityManager.registry.getEntityType(request.params.type);
|
|
5183
5934
|
if (!entityType) return apiError(404, ErrCodeUnknownEntityType, `Entity type "${request.params.type}" not found`);
|
|
5935
|
+
request.spawnRoute = { entityType };
|
|
5184
5936
|
return void 0;
|
|
5185
5937
|
}
|
|
5938
|
+
function withEntityPermission(permission) {
|
|
5939
|
+
return async (request, ctx) => {
|
|
5940
|
+
const { entity } = requireExistingEntityRoute(request);
|
|
5941
|
+
if (await canAccessEntity(ctx, entity, permission, request)) return void 0;
|
|
5942
|
+
return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to ${permission} ${entity.url}`);
|
|
5943
|
+
};
|
|
5944
|
+
}
|
|
5945
|
+
async function withSpawnPermission(request, ctx) {
|
|
5946
|
+
const parsed = routeBody(request);
|
|
5947
|
+
const entityType = request.spawnRoute?.entityType;
|
|
5948
|
+
if (!entityType) throw new Error(`spawnable entity type middleware did not run`);
|
|
5949
|
+
if (!await canAccessEntityType(ctx, entityType, `spawn`, request)) return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to spawn ${entityType.name}`);
|
|
5950
|
+
if (!parsed.parent) return void 0;
|
|
5951
|
+
const parent = await ctx.entityManager.registry.getEntity(parsed.parent);
|
|
5952
|
+
if (!parent) return apiError(404, ErrCodeNotFound, `Parent entity not found`);
|
|
5953
|
+
if (await canAccessEntity(ctx, parent, `spawn`, request)) return await validateParentedSpawnGrants(request, ctx, parent, parsed);
|
|
5954
|
+
return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to spawn children from ${parent.url}`);
|
|
5955
|
+
}
|
|
5956
|
+
async function validateParentedSpawnGrants(request, ctx, parent, parsed) {
|
|
5957
|
+
const needsParentManage = (parsed.grants ?? []).some(requiresParentManageForInitialGrant);
|
|
5958
|
+
if (!needsParentManage) return void 0;
|
|
5959
|
+
if (await canAccessEntity(ctx, parent, `manage`, request)) return void 0;
|
|
5960
|
+
return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to delegate broad grants from ${parent.url}`);
|
|
5961
|
+
}
|
|
5962
|
+
function requiresParentManageForInitialGrant(grant) {
|
|
5963
|
+
return grant.permission === `manage` || grant.subject_kind === `principal_kind` || grant.propagation === `descendants` || grant.copy_to_children === true;
|
|
5964
|
+
}
|
|
5186
5965
|
async function listEntities({ query }, ctx) {
|
|
5187
5966
|
const { entities: entities$1 } = await ctx.entityManager.registry.listEntities({
|
|
5188
5967
|
type: firstQueryValue$1(query.type),
|
|
5189
5968
|
status: firstQueryValue$1(query.status),
|
|
5190
5969
|
parent: firstQueryValue$1(query.parent),
|
|
5191
|
-
created_by: firstQueryValue$1(query.created_by)
|
|
5970
|
+
created_by: firstQueryValue$1(query.created_by),
|
|
5971
|
+
readableBy: {
|
|
5972
|
+
...principalSubject(ctx.principal),
|
|
5973
|
+
bypass: isPermissionBypassPrincipal(ctx)
|
|
5974
|
+
}
|
|
5192
5975
|
});
|
|
5193
5976
|
return json(entities$1.map((entity) => toPublicEntity(entity)));
|
|
5194
5977
|
}
|
|
5978
|
+
async function listEntityPermissionGrants(request, ctx) {
|
|
5979
|
+
const { entityUrl } = requireExistingEntityRoute(request);
|
|
5980
|
+
const grants = await ctx.entityManager.registry.listEntityPermissionGrants(entityUrl);
|
|
5981
|
+
return json({ grants });
|
|
5982
|
+
}
|
|
5983
|
+
async function createEntityPermissionGrant(request, ctx) {
|
|
5984
|
+
const { entityUrl } = requireExistingEntityRoute(request);
|
|
5985
|
+
const parsed = routeBody(request);
|
|
5986
|
+
const grant = await ctx.entityManager.registry.createEntityPermissionGrant({
|
|
5987
|
+
entityUrl,
|
|
5988
|
+
permission: parsed.permission,
|
|
5989
|
+
subjectKind: parsed.subject_kind,
|
|
5990
|
+
subjectValue: parsed.subject_value,
|
|
5991
|
+
propagation: parsed.propagation,
|
|
5992
|
+
copyToChildren: parsed.copy_to_children,
|
|
5993
|
+
expiresAt: parseExpiresAt$1(parsed.expires_at),
|
|
5994
|
+
createdBy: ctx.principal.url
|
|
5995
|
+
});
|
|
5996
|
+
await ctx.entityBridgeManager.onEntityChanged(entityUrl);
|
|
5997
|
+
return json(grant, { status: 201 });
|
|
5998
|
+
}
|
|
5999
|
+
async function deleteEntityPermissionGrant(request, ctx) {
|
|
6000
|
+
const { entityUrl } = requireExistingEntityRoute(request);
|
|
6001
|
+
const deleted = await ctx.entityManager.registry.deleteEntityPermissionGrant(entityUrl, parseGrantId$1(request));
|
|
6002
|
+
if (deleted) await ctx.entityBridgeManager.onEntityChanged(entityUrl);
|
|
6003
|
+
return deleted ? status(204) : apiError(404, ErrCodeNotFound, `Grant not found`);
|
|
6004
|
+
}
|
|
5195
6005
|
async function upsertSchedule(request, ctx) {
|
|
5196
6006
|
const principalMutationError = rejectPrincipalEntityMutation(request, `scheduled`);
|
|
5197
6007
|
if (principalMutationError) return principalMutationError;
|
|
@@ -5297,6 +6107,7 @@ async function forkEntity(request, ctx) {
|
|
|
5297
6107
|
const result = await ctx.entityManager.forkSubtree(entityUrl, {
|
|
5298
6108
|
rootInstanceId: parsed.instance_id,
|
|
5299
6109
|
waitTimeoutMs: parsed.waitTimeoutMs,
|
|
6110
|
+
createdBy: ctx.principal.url,
|
|
5300
6111
|
...parsed.fork_pointer && { forkPointer: {
|
|
5301
6112
|
offset: parsed.fork_pointer.offset,
|
|
5302
6113
|
subOffset: parsed.fork_pointer.sub_offset
|
|
@@ -5312,26 +6123,27 @@ async function sendEntity(request, ctx) {
|
|
|
5312
6123
|
const parsed = routeBody(request);
|
|
5313
6124
|
const principal = ctx.principal;
|
|
5314
6125
|
if (parsed.from !== void 0 && parsed.from !== principal.url) return apiError(400, ErrCodeInvalidRequest, `Request from must match Electric-Principal`);
|
|
6126
|
+
if (parsed.from_principal !== void 0 && parsed.from_principal !== principal.url) return apiError(400, ErrCodeInvalidRequest, `Request from_principal must match Electric-Principal`);
|
|
6127
|
+
if (parsed.from_agent !== void 0) {
|
|
6128
|
+
const principalAgentUrl = agentUrlForPrincipal(principal);
|
|
6129
|
+
if (agentUrlPath(parsed.from_agent) !== principalAgentUrl) return apiError(400, ErrCodeInvalidRequest, `Request from_agent must match authenticated agent principal`);
|
|
6130
|
+
}
|
|
5315
6131
|
await ctx.entityManager.ensurePrincipal(principal);
|
|
5316
6132
|
const { entityUrl, entity } = requireExistingEntityRoute(request);
|
|
5317
6133
|
const dispatchEntity = entity.dispatch_policy ? entity : await backfillEntityDispatchPolicy(ctx, entity);
|
|
5318
6134
|
await linkEntityDispatchSubscription(ctx, dispatchEntity);
|
|
5319
|
-
|
|
6135
|
+
const sendReq = {
|
|
5320
6136
|
from: principal.url,
|
|
6137
|
+
from_principal: principal.url,
|
|
6138
|
+
from_agent: parsed.from_agent,
|
|
5321
6139
|
payload: parsed.payload,
|
|
5322
6140
|
key: parsed.key,
|
|
5323
6141
|
type: parsed.type,
|
|
5324
6142
|
mode: parsed.mode,
|
|
5325
6143
|
position: parsed.position
|
|
5326
|
-
}
|
|
5327
|
-
|
|
5328
|
-
|
|
5329
|
-
payload: parsed.payload,
|
|
5330
|
-
key: parsed.key,
|
|
5331
|
-
type: parsed.type,
|
|
5332
|
-
mode: parsed.mode,
|
|
5333
|
-
position: parsed.position
|
|
5334
|
-
});
|
|
6144
|
+
};
|
|
6145
|
+
if (parsed.afterMs && parsed.afterMs > 0) await ctx.entityManager.enqueueDelayedSend(entityUrl, sendReq, new Date(Date.now() + parsed.afterMs));
|
|
6146
|
+
else await ctx.entityManager.send(entityUrl, sendReq);
|
|
5335
6147
|
return status(204);
|
|
5336
6148
|
}
|
|
5337
6149
|
async function createAttachment(request, ctx) {
|
|
@@ -5403,6 +6215,17 @@ async function spawnEntity(request, ctx) {
|
|
|
5403
6215
|
wake: parsed.wake,
|
|
5404
6216
|
created_by: principal.url
|
|
5405
6217
|
});
|
|
6218
|
+
if (parsed.parent) await ctx.entityManager.registry.copyEntityPermissionGrantsForSpawn(parsed.parent, entity.url, principal.url);
|
|
6219
|
+
for (const grant of parsed.grants ?? []) await ctx.entityManager.registry.createEntityPermissionGrant({
|
|
6220
|
+
entityUrl: entity.url,
|
|
6221
|
+
permission: grant.permission,
|
|
6222
|
+
subjectKind: grant.subject_kind,
|
|
6223
|
+
subjectValue: grant.subject_value,
|
|
6224
|
+
propagation: grant.propagation,
|
|
6225
|
+
copyToChildren: grant.copy_to_children,
|
|
6226
|
+
expiresAt: parseExpiresAt$1(grant.expires_at),
|
|
6227
|
+
createdBy: principal.url
|
|
6228
|
+
});
|
|
5406
6229
|
const linkBeforeInitialMessage = parsed.initialMessage !== void 0 && shouldLinkDispatchBeforeInitialMessage(dispatchPolicy);
|
|
5407
6230
|
if (linkBeforeInitialMessage) await linkEntityDispatchSubscription(ctx, entity);
|
|
5408
6231
|
if (parsed.initialMessage !== void 0) await ctx.entityManager.send(entity.url, {
|
|
@@ -5454,6 +6277,12 @@ async function signalEntity(request, ctx) {
|
|
|
5454
6277
|
//#region src/routing/entity-types-router.ts
|
|
5455
6278
|
const jsonObjectSchema = Type.Record(Type.String(), Type.Unknown());
|
|
5456
6279
|
const schemaMapSchema = Type.Record(Type.String(), jsonObjectSchema);
|
|
6280
|
+
const typePermissionGrantInputSchema = Type.Object({
|
|
6281
|
+
subject_kind: Type.Union([Type.Literal(`principal`), Type.Literal(`principal_kind`)]),
|
|
6282
|
+
subject_value: Type.String(),
|
|
6283
|
+
permission: Type.Union([Type.Literal(`spawn`), Type.Literal(`manage`)]),
|
|
6284
|
+
expires_at: Type.Optional(Type.String())
|
|
6285
|
+
}, { additionalProperties: false });
|
|
5457
6286
|
const registerEntityTypeBodySchema = Type.Object({
|
|
5458
6287
|
name: Type.Optional(Type.String()),
|
|
5459
6288
|
description: Type.Optional(Type.String()),
|
|
@@ -5461,7 +6290,8 @@ const registerEntityTypeBodySchema = Type.Object({
|
|
|
5461
6290
|
inbox_schemas: Type.Optional(schemaMapSchema),
|
|
5462
6291
|
state_schemas: Type.Optional(schemaMapSchema),
|
|
5463
6292
|
serve_endpoint: Type.Optional(Type.String()),
|
|
5464
|
-
default_dispatch_policy: Type.Optional(dispatchPolicySchema)
|
|
6293
|
+
default_dispatch_policy: Type.Optional(dispatchPolicySchema),
|
|
6294
|
+
permission_grants: Type.Optional(Type.Array(typePermissionGrantInputSchema))
|
|
5465
6295
|
}, { additionalProperties: false });
|
|
5466
6296
|
const amendEntityTypeSchemasBodySchema = Type.Object({
|
|
5467
6297
|
inbox_schemas: Type.Optional(schemaMapSchema),
|
|
@@ -5469,20 +6299,56 @@ const amendEntityTypeSchemasBodySchema = Type.Object({
|
|
|
5469
6299
|
}, { additionalProperties: false });
|
|
5470
6300
|
const entityTypesRouter = Router({ base: `/_electric/entity-types` });
|
|
5471
6301
|
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);
|
|
6302
|
+
entityTypesRouter.post(`/`, withSchema(registerEntityTypeBodySchema), withEntityTypeRegistrationPermission, registerEntityType);
|
|
6303
|
+
entityTypesRouter.patch(`/:name/schemas`, withExistingEntityType, withEntityTypeManagePermission, withSchema(amendEntityTypeSchemasBodySchema), amendSchemas);
|
|
6304
|
+
entityTypesRouter.get(`/:name`, withExistingEntityType, withEntityTypeSpawnPermission, getEntityType);
|
|
6305
|
+
entityTypesRouter.delete(`/:name`, withExistingEntityType, withEntityTypeManagePermission, deleteEntityType);
|
|
6306
|
+
entityTypesRouter.get(`/:name/grants`, withExistingEntityType, withEntityTypeManagePermission, listTypePermissionGrants);
|
|
6307
|
+
entityTypesRouter.post(`/:name/grants`, withExistingEntityType, withSchema(typePermissionGrantInputSchema), withEntityTypeManagePermission, createTypePermissionGrant);
|
|
6308
|
+
entityTypesRouter.delete(`/:name/grants/:grantId`, withExistingEntityType, withEntityTypeManagePermission, deleteTypePermissionGrant);
|
|
5476
6309
|
async function registerEntityType(request, ctx) {
|
|
5477
6310
|
const parsed = routeBody(request);
|
|
5478
6311
|
const normalized = normalizeEntityTypeRequest(parsed);
|
|
5479
6312
|
if (normalized.serve_endpoint && !normalized.description && !normalized.creation_schema) return await discoverServeEndpoint(ctx, normalized);
|
|
5480
6313
|
const entityType = await ctx.entityManager.registerEntityType(normalized);
|
|
6314
|
+
await applyRegistrationPermissionGrants(ctx, entityType.name, normalized);
|
|
5481
6315
|
return json(toPublicEntityType(entityType), { status: 201 });
|
|
5482
6316
|
}
|
|
5483
6317
|
async function listEntityTypes(_request, ctx) {
|
|
5484
6318
|
const entityTypes$1 = await ctx.entityManager.registry.listEntityTypes();
|
|
5485
|
-
|
|
6319
|
+
const visible = [];
|
|
6320
|
+
for (const entityType of entityTypes$1) if (await canAccessEntityType(ctx, entityType, `spawn`)) visible.push(entityType);
|
|
6321
|
+
return json(visible.map((entityType) => toPublicEntityType(entityType)));
|
|
6322
|
+
}
|
|
6323
|
+
async function withExistingEntityType(request, ctx) {
|
|
6324
|
+
const entityType = await ctx.entityManager.registry.getEntityType(request.params.name);
|
|
6325
|
+
if (!entityType) return apiError(404, ErrCodeNotFound, `Entity type not found`);
|
|
6326
|
+
request.entityTypeRoute = { entityType };
|
|
6327
|
+
return void 0;
|
|
6328
|
+
}
|
|
6329
|
+
async function withEntityTypeManagePermission(request, ctx) {
|
|
6330
|
+
const entityType = request.entityTypeRoute?.entityType;
|
|
6331
|
+
if (!entityType) throw new Error(`entity type middleware did not run`);
|
|
6332
|
+
if (await canAccessEntityType(ctx, entityType, `manage`, request)) return void 0;
|
|
6333
|
+
return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to manage ${entityType.name}`);
|
|
6334
|
+
}
|
|
6335
|
+
async function withEntityTypeSpawnPermission(request, ctx) {
|
|
6336
|
+
const entityType = request.entityTypeRoute?.entityType;
|
|
6337
|
+
if (!entityType) throw new Error(`entity type middleware did not run`);
|
|
6338
|
+
if (await canAccessEntityType(ctx, entityType, `spawn`, request)) return void 0;
|
|
6339
|
+
return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to spawn ${entityType.name}`);
|
|
6340
|
+
}
|
|
6341
|
+
async function withEntityTypeRegistrationPermission(request, ctx) {
|
|
6342
|
+
const parsed = normalizeEntityTypeRequest(routeBody(request));
|
|
6343
|
+
if (!parsed.name) return void 0;
|
|
6344
|
+
const existing = await ctx.entityManager.registry.getEntityType(parsed.name);
|
|
6345
|
+
if (existing) {
|
|
6346
|
+
request.entityTypeRoute = { entityType: existing };
|
|
6347
|
+
if (await canAccessEntityType(ctx, existing, `manage`, request)) return void 0;
|
|
6348
|
+
return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to manage ${existing.name}`);
|
|
6349
|
+
}
|
|
6350
|
+
if (await canRegisterEntityType(ctx, parsed, request)) return void 0;
|
|
6351
|
+
return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to register entity types`);
|
|
5486
6352
|
}
|
|
5487
6353
|
async function discoverServeEndpoint(ctx, parsed) {
|
|
5488
6354
|
try {
|
|
@@ -5491,17 +6357,17 @@ async function discoverServeEndpoint(ctx, parsed) {
|
|
|
5491
6357
|
const manifest = await response.json();
|
|
5492
6358
|
if (manifest.name !== parsed.name) return apiError(400, ErrCodeServeEndpointNameMismatch, `Serve endpoint returned name "${manifest.name}" but expected "${parsed.name}"`);
|
|
5493
6359
|
manifest.serve_endpoint = parsed.serve_endpoint;
|
|
6360
|
+
manifest.permission_grants = parsed.permission_grants;
|
|
5494
6361
|
const entityType = await ctx.entityManager.registerEntityType(normalizeEntityTypeRequest(manifest));
|
|
6362
|
+
await applyRegistrationPermissionGrants(ctx, entityType.name, manifest);
|
|
5495
6363
|
return json(toPublicEntityType(entityType), { status: 201 });
|
|
5496
6364
|
} catch (err) {
|
|
5497
6365
|
if (err instanceof ElectricAgentsError) throw err;
|
|
5498
6366
|
return apiError(502, ErrCodeServeEndpointUnreachable, `Failed to reach serve endpoint: ${err instanceof Error ? err.message : String(err)}`);
|
|
5499
6367
|
}
|
|
5500
6368
|
}
|
|
5501
|
-
async function getEntityType(request
|
|
5502
|
-
|
|
5503
|
-
if (!entityType) return apiError(404, ErrCodeNotFound, `Entity type not found`);
|
|
5504
|
-
return json(toPublicEntityType(entityType));
|
|
6369
|
+
async function getEntityType(request) {
|
|
6370
|
+
return json(toPublicEntityType(request.entityTypeRoute.entityType));
|
|
5505
6371
|
}
|
|
5506
6372
|
async function amendSchemas(request, ctx) {
|
|
5507
6373
|
const parsed = routeBody(request);
|
|
@@ -5515,6 +6381,47 @@ async function deleteEntityType(request, ctx) {
|
|
|
5515
6381
|
await ctx.entityManager.deleteEntityType(request.params.name);
|
|
5516
6382
|
return status(204);
|
|
5517
6383
|
}
|
|
6384
|
+
async function listTypePermissionGrants(request, ctx) {
|
|
6385
|
+
const grants = await ctx.entityManager.registry.listEntityTypePermissionGrants(request.entityTypeRoute.entityType.name);
|
|
6386
|
+
return json({ grants });
|
|
6387
|
+
}
|
|
6388
|
+
async function createTypePermissionGrant(request, ctx) {
|
|
6389
|
+
const parsed = routeBody(request);
|
|
6390
|
+
const grant = await ctx.entityManager.registry.createEntityTypePermissionGrant({
|
|
6391
|
+
entityType: request.entityTypeRoute.entityType.name,
|
|
6392
|
+
permission: parsed.permission,
|
|
6393
|
+
subjectKind: parsed.subject_kind,
|
|
6394
|
+
subjectValue: parsed.subject_value,
|
|
6395
|
+
expiresAt: parseExpiresAt(parsed.expires_at),
|
|
6396
|
+
createdBy: ctx.principal.url
|
|
6397
|
+
});
|
|
6398
|
+
return json(grant, { status: 201 });
|
|
6399
|
+
}
|
|
6400
|
+
async function deleteTypePermissionGrant(request, ctx) {
|
|
6401
|
+
const deleted = await ctx.entityManager.registry.deleteEntityTypePermissionGrant(request.entityTypeRoute.entityType.name, parseGrantId(request));
|
|
6402
|
+
return deleted ? status(204) : apiError(404, ErrCodeNotFound, `Grant not found`);
|
|
6403
|
+
}
|
|
6404
|
+
async function applyRegistrationPermissionGrants(ctx, entityType, request) {
|
|
6405
|
+
for (const grant of request.permission_grants ?? []) await ctx.entityManager.registry.ensureEntityTypePermissionGrant({
|
|
6406
|
+
entityType,
|
|
6407
|
+
permission: grant.permission,
|
|
6408
|
+
subjectKind: grant.subject_kind,
|
|
6409
|
+
subjectValue: grant.subject_value,
|
|
6410
|
+
expiresAt: parseExpiresAt(grant.expires_at),
|
|
6411
|
+
createdBy: ctx.principal.url
|
|
6412
|
+
});
|
|
6413
|
+
}
|
|
6414
|
+
function parseGrantId(request) {
|
|
6415
|
+
const grantId = Number.parseInt(String(request.params.grantId), 10);
|
|
6416
|
+
if (!Number.isSafeInteger(grantId) || grantId <= 0) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Invalid grant id`, 400);
|
|
6417
|
+
return grantId;
|
|
6418
|
+
}
|
|
6419
|
+
function parseExpiresAt(value) {
|
|
6420
|
+
if (value === void 0) return void 0;
|
|
6421
|
+
const expiresAt = new Date(value);
|
|
6422
|
+
if (Number.isNaN(expiresAt.getTime())) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Invalid expires_at timestamp`, 400);
|
|
6423
|
+
return expiresAt;
|
|
6424
|
+
}
|
|
5518
6425
|
function normalizeEntityTypeRequest(parsed) {
|
|
5519
6426
|
const serveEndpoint = rewriteLoopbackWebhookUrl(parsed.serve_endpoint);
|
|
5520
6427
|
return {
|
|
@@ -5527,7 +6434,8 @@ function normalizeEntityTypeRequest(parsed) {
|
|
|
5527
6434
|
default_dispatch_policy: parsed.default_dispatch_policy ?? (serveEndpoint ? { targets: [{
|
|
5528
6435
|
type: `webhook`,
|
|
5529
6436
|
url: serveEndpoint
|
|
5530
|
-
}] } : void 0)
|
|
6437
|
+
}] } : void 0),
|
|
6438
|
+
permission_grants: parsed.permission_grants
|
|
5531
6439
|
};
|
|
5532
6440
|
}
|
|
5533
6441
|
function toPublicEntityType(entityType) {
|
|
@@ -5586,6 +6494,7 @@ function applyCors(response) {
|
|
|
5586
6494
|
`content-type`,
|
|
5587
6495
|
`authorization`,
|
|
5588
6496
|
`electric-claim-token`,
|
|
6497
|
+
`electric-owner-entity`,
|
|
5589
6498
|
ELECTRIC_PRINCIPAL_HEADER,
|
|
5590
6499
|
`ngrok-skip-browser-warning`
|
|
5591
6500
|
].join(`, `));
|
|
@@ -5636,7 +6545,7 @@ observationsRouter.post(`/entities/ensure-stream`, withSchema(ensureEntitiesMemb
|
|
|
5636
6545
|
observationsRouter.post(`/cron/ensure-stream`, withSchema(ensureCronStreamBodySchema), ensureCronStream);
|
|
5637
6546
|
async function ensureEntitiesMembershipStream(request, ctx) {
|
|
5638
6547
|
const parsed = routeBody(request);
|
|
5639
|
-
const result = await ctx.entityManager.ensureEntitiesMembershipStream(parsed.tags ?? {});
|
|
6548
|
+
const result = await ctx.entityManager.ensureEntitiesMembershipStream(parsed.tags ?? {}, ctx.principal);
|
|
5640
6549
|
return json(result);
|
|
5641
6550
|
}
|
|
5642
6551
|
async function ensureCronStream(request, ctx) {
|
|
@@ -6515,16 +7424,31 @@ function buildTagsWhereClause(tags) {
|
|
|
6515
7424
|
function sqlStringLiteral$1(value) {
|
|
6516
7425
|
return `'${value.replace(/'/g, `''`)}'`;
|
|
6517
7426
|
}
|
|
6518
|
-
function buildTenantTagsWhereClause(tenantId, tags) {
|
|
6519
|
-
|
|
7427
|
+
function buildTenantTagsWhereClause(tenantId, tags, principalUrl$1, principalKind, permissionBypass) {
|
|
7428
|
+
const readableWhere = principalUrl$1 && principalKind ? buildReadableEntitiesWhere({
|
|
7429
|
+
tenantId,
|
|
7430
|
+
principalUrl: principalUrl$1,
|
|
7431
|
+
principalKind,
|
|
7432
|
+
permissionBypass
|
|
7433
|
+
}) : `tenant_id = ${sqlStringLiteral$1(tenantId)} AND FALSE`;
|
|
7434
|
+
return `${readableWhere} AND (${buildTagsWhereClause(tags)})`;
|
|
6520
7435
|
}
|
|
6521
7436
|
function shapeEntityKey(message) {
|
|
6522
7437
|
return message.value.url;
|
|
6523
7438
|
}
|
|
7439
|
+
function principalScopedSourceRef(tagSourceRef, principalUrl$1, principalKind) {
|
|
7440
|
+
return `${tagSourceRef}-${hashString(JSON.stringify({
|
|
7441
|
+
principalKind,
|
|
7442
|
+
principalUrl: principalUrl$1
|
|
7443
|
+
}))}`;
|
|
7444
|
+
}
|
|
6524
7445
|
var EntityBridge = class {
|
|
6525
7446
|
sourceRef;
|
|
6526
7447
|
tags;
|
|
6527
7448
|
streamUrl;
|
|
7449
|
+
principalUrl;
|
|
7450
|
+
principalKind;
|
|
7451
|
+
permissionBypass;
|
|
6528
7452
|
currentMembers = new Map();
|
|
6529
7453
|
producer = null;
|
|
6530
7454
|
liveAbortController = null;
|
|
@@ -6541,6 +7465,9 @@ var EntityBridge = class {
|
|
|
6541
7465
|
this.sourceRef = row.sourceRef;
|
|
6542
7466
|
this.tags = normalizeTags(row.tags);
|
|
6543
7467
|
this.streamUrl = row.streamUrl;
|
|
7468
|
+
this.principalUrl = row.principalUrl;
|
|
7469
|
+
this.principalKind = row.principalKind;
|
|
7470
|
+
this.permissionBypass = isBuiltInSystemPrincipalUrl(row.principalUrl);
|
|
6544
7471
|
this.initialShapeHandle = row.shapeHandle;
|
|
6545
7472
|
this.initialShapeOffset = row.shapeOffset;
|
|
6546
7473
|
}
|
|
@@ -6648,7 +7575,7 @@ var EntityBridge = class {
|
|
|
6648
7575
|
url: electricUrlWithPath(this.electricUrl, `/v1/shape`).toString(),
|
|
6649
7576
|
params: {
|
|
6650
7577
|
table: `entities`,
|
|
6651
|
-
where: buildTenantTagsWhereClause(this.tenantId, this.tags),
|
|
7578
|
+
where: buildTenantTagsWhereClause(this.tenantId, this.tags, this.principalUrl, this.principalKind, this.permissionBypass),
|
|
6652
7579
|
...this.electricSecret ? { secret: this.electricSecret } : {},
|
|
6653
7580
|
columns: [...ENTITY_SHAPE_COLUMNS],
|
|
6654
7581
|
replica: `full`
|
|
@@ -6811,15 +7738,17 @@ var EntityBridgeManager = class {
|
|
|
6811
7738
|
await bridge.stop();
|
|
6812
7739
|
}));
|
|
6813
7740
|
}
|
|
6814
|
-
async register(tagsInput) {
|
|
7741
|
+
async register(tagsInput, principalUrl$1, principalKind) {
|
|
6815
7742
|
if (!this.electricUrl) throw new Error(`[entity-bridge] Electric URL is required for entities()`);
|
|
6816
7743
|
const tags = normalizeTags(assertTags(tagsInput));
|
|
6817
|
-
const sourceRef = sourceRefForTags(tags);
|
|
7744
|
+
const sourceRef = principalScopedSourceRef(sourceRefForTags(tags), principalUrl$1, principalKind);
|
|
6818
7745
|
const streamUrl = getEntitiesStreamPath(sourceRef);
|
|
6819
7746
|
const row = await this.registry.upsertEntityBridge({
|
|
6820
7747
|
sourceRef,
|
|
6821
7748
|
tags,
|
|
6822
|
-
streamUrl
|
|
7749
|
+
streamUrl,
|
|
7750
|
+
principalUrl: principalUrl$1,
|
|
7751
|
+
principalKind
|
|
6823
7752
|
});
|
|
6824
7753
|
await this.registry.touchEntityBridge(sourceRef);
|
|
6825
7754
|
await this.ensureBridge(row);
|
|
@@ -7639,6 +8568,8 @@ var ElectricAgentsTenantRuntime = class {
|
|
|
7639
8568
|
try {
|
|
7640
8569
|
await this.manager.send(payload.entityUrl, {
|
|
7641
8570
|
from: payload.from,
|
|
8571
|
+
from_principal: payload.from_principal,
|
|
8572
|
+
from_agent: payload.from_agent,
|
|
7642
8573
|
payload: payload.payload,
|
|
7643
8574
|
key: payload.key ?? `scheduled-task-${taskId}`,
|
|
7644
8575
|
type: payload.type
|
|
@@ -7711,6 +8642,7 @@ var ElectricAgentsTenantRuntime = class {
|
|
|
7711
8642
|
await this.scheduler.syncManifestDelayedSend(ownerEntityUrl, manifestKey, {
|
|
7712
8643
|
entityUrl: targetUrl,
|
|
7713
8644
|
from: senderUrl,
|
|
8645
|
+
from_agent: senderUrl,
|
|
7714
8646
|
payload: value.payload,
|
|
7715
8647
|
key: `scheduled-${producerId}`,
|
|
7716
8648
|
type: typeof value.messageType === `string` ? value.messageType : void 0,
|
|
@@ -7735,11 +8667,20 @@ var ElectricAgentsTenantRuntime = class {
|
|
|
7735
8667
|
async applyManifestEntitySource(ownerEntityUrl, manifestKey, operation, value) {
|
|
7736
8668
|
const sourceRef = operation === `delete` ? void 0 : this.extractEntitiesSourceRef(value);
|
|
7737
8669
|
await this.manager.registry.replaceEntityManifestSource(ownerEntityUrl, manifestKey, sourceRef);
|
|
8670
|
+
const sharedStateId = operation === `delete` ? void 0 : this.extractSharedStateId(value);
|
|
8671
|
+
await this.manager.registry.replaceSharedStateLink(ownerEntityUrl, manifestKey, sharedStateId);
|
|
7738
8672
|
}
|
|
7739
8673
|
extractEntitiesSourceRef(manifest) {
|
|
7740
8674
|
if (manifest?.kind === `source` && manifest.sourceType === `entities` && typeof manifest.sourceRef === `string`) return manifest.sourceRef;
|
|
7741
8675
|
return void 0;
|
|
7742
8676
|
}
|
|
8677
|
+
extractSharedStateId(manifest) {
|
|
8678
|
+
if (manifest?.kind === `shared-state` && typeof manifest.id === `string`) return manifest.id;
|
|
8679
|
+
if (manifest?.kind !== `source` || manifest.sourceType !== `db`) return void 0;
|
|
8680
|
+
if (typeof manifest.sourceRef === `string`) return manifest.sourceRef;
|
|
8681
|
+
const config = typeof manifest.config === `object` && manifest.config !== null && !Array.isArray(manifest.config) ? manifest.config : void 0;
|
|
8682
|
+
return typeof config?.id === `string` ? config.id : void 0;
|
|
8683
|
+
}
|
|
7743
8684
|
async maybeMarkEntityIdleAfterRunFinished(entityUrl) {
|
|
7744
8685
|
const primaryStream = `${entityUrl}/main`;
|
|
7745
8686
|
const callbacks = await this.db.select().from(consumerCallbacks).where(and(eq(consumerCallbacks.tenantId, this.serviceId), eq(consumerCallbacks.primaryStream, primaryStream))).limit(1);
|
|
@@ -8420,6 +9361,8 @@ var WakeRegistry = class {
|
|
|
8420
9361
|
if (eventType === `inbox`) {
|
|
8421
9362
|
const value = event.value;
|
|
8422
9363
|
if (typeof value?.from === `string`) change.from = value.from;
|
|
9364
|
+
if (typeof value?.from_principal === `string`) change.from_principal = value.from_principal;
|
|
9365
|
+
if (typeof value?.from_agent === `string`) change.from_agent = value.from_agent;
|
|
8423
9366
|
if (`payload` in (value ?? {})) change.payload = value?.payload;
|
|
8424
9367
|
if (typeof value?.timestamp === `string`) change.timestamp = value.timestamp;
|
|
8425
9368
|
if (typeof value?.message_type === `string`) change.message_type = value.message_type;
|
|
@@ -8770,6 +9713,7 @@ var ElectricAgentsServer = class {
|
|
|
8770
9713
|
entityBridgeManager: this.entityBridgeManager,
|
|
8771
9714
|
...this.options.eventSources ? { eventSources: this.options.eventSources } : {},
|
|
8772
9715
|
...this.options.ensureEventSourceWakeSource ? { ensureEventSourceWakeSource: this.options.ensureEventSourceWakeSource } : {},
|
|
9716
|
+
...this.options.authorizeRequest ? { authorizeRequest: this.options.authorizeRequest } : {},
|
|
8773
9717
|
isShuttingDown: () => this.shuttingDown,
|
|
8774
9718
|
mockAgent: this.mockAgentBootstrap ? { runtime: this.mockAgentBootstrap.runtime } : void 0
|
|
8775
9719
|
};
|