@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.
@@ -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) => [primaryKey({ columns: [table.tenantId, table.sourceRef] }), unique(`uq_entity_bridges_stream_url`).on(table.tenantId, table.streamUrl)]);
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
- applyTenantShapeWhere(target, options.tenantId);
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
- applyTenantShapeWhere(target, options.tenantId);
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
- applyTenantShapeWhere(target, options.tenantId);
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
- applyTenantShapeWhere(target, options.tenantId);
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
- applyTenantShapeWhere(target, options.tenantId);
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
- const tenantWhere = [`tenant_id = ${sqlStringLiteral$2(tenantId)}`, ...extraConditions].join(` AND `);
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 ? `${tenantWhere} AND (${existingWhere})` : tenantWhere);
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) return 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) return 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, errorStreamResult, entityResult] = await this.spawnPersistQueue.push(async () => {
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
- status: `fulfilled`,
3182
- value: void 0
3183
- },
3184
- {
3185
- status: `fulfilled`,
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, errorStreamResult$1] = await Promise.allSettled([this.streamClient.create(mainPath, {
3898
+ const [mainStreamResult$1] = await Promise.allSettled([this.streamClient.create(mainPath, {
3195
3899
  contentType,
3196
3900
  body: initialBody
3197
- }), this.streamClient.create(errorPath, { contentType })]);
3198
- return [
3199
- mainStreamResult$1,
3200
- errorStreamResult$1,
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` || errorStreamResult.status === `rejected` || entityResult.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 : errorStreamResult.status === `rejected` ? errorStreamResult.reason : null;
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 : errorStreamResult.status === `rejected` ? errorStreamResult.reason : entityResult.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.syncEntitiesManifestSource(entityUrl, manifestKey, `upsert`, manifest);
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.syncEntitiesManifestSource(entityUrl, key, operation, value);
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.syncEntitiesManifestSource(entityUrl, key, operation, value);
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 syncEntitiesManifestSource(entityUrl, manifestKey, operation, value) {
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 errorCloseEvent = {
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
- if (parsed.afterMs && parsed.afterMs > 0) await ctx.entityManager.enqueueDelayedSend(entityUrl, {
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
- }, new Date(Date.now() + parsed.afterMs));
5327
- else await ctx.entityManager.send(entityUrl, {
5328
- from: principal.url,
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
- return json(entityTypes$1.map((entityType) => toPublicEntityType(entityType)));
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, ctx) {
5502
- const entityType = await ctx.entityManager.registry.getEntityType(request.params.name);
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
- return `tenant_id = ${sqlStringLiteral$1(tenantId)} AND (${buildTagsWhereClause(tags)})`;
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
  };