@electric-ax/agents-server 0.4.15 → 0.4.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -41,8 +41,8 @@ const __electric_ax_agents_runtime = __toESM(require("@electric-ax/agents-runtim
41
41
  const __durable_streams_client = __toESM(require("@durable-streams/client"));
42
42
  const __electric_sql_client = __toESM(require("@electric-sql/client"));
43
43
  const pino = __toESM(require("pino"));
44
- const fastq = __toESM(require("fastq"));
45
44
  const __sinclair_typebox = __toESM(require("@sinclair/typebox"));
45
+ const fastq = __toESM(require("fastq"));
46
46
  const ajv = __toESM(require("ajv"));
47
47
  const __opentelemetry_api = __toESM(require("@opentelemetry/api"));
48
48
  const itty_router = __toESM(require("itty-router"));
@@ -55,11 +55,16 @@ __export(schema_exports, {
55
55
  entities: () => entities,
56
56
  entityBridges: () => entityBridges,
57
57
  entityDispatchState: () => entityDispatchState,
58
+ entityEffectivePermissions: () => entityEffectivePermissions,
59
+ entityLineage: () => entityLineage,
58
60
  entityManifestSources: () => entityManifestSources,
61
+ entityPermissionGrants: () => entityPermissionGrants,
62
+ entityTypePermissionGrants: () => entityTypePermissionGrants,
59
63
  entityTypes: () => entityTypes,
60
64
  runnerRuntimeDiagnostics: () => runnerRuntimeDiagnostics,
61
65
  runners: () => runners,
62
66
  scheduledTasks: () => scheduledTasks,
67
+ sharedStateLinks: () => sharedStateLinks,
63
68
  subscriptionWebhooks: () => subscriptionWebhooks,
64
69
  tagStreamOutbox: () => tagStreamOutbox,
65
70
  users: () => users,
@@ -107,6 +112,94 @@ const entities = (0, drizzle_orm_pg_core.pgTable)(`entities`, {
107
112
  (0, drizzle_orm_pg_core.index)(`entities_tags_index_gin`).using(`gin`, table.tagsIndex),
108
113
  (0, drizzle_orm_pg_core.check)(`chk_entities_status`, drizzle_orm.sql`${table.status} IN ('spawning', 'running', 'idle', 'paused', 'stopping', 'stopped', 'killed')`)
109
114
  ]);
115
+ const entityTypePermissionGrants = (0, drizzle_orm_pg_core.pgTable)(`entity_type_permission_grants`, {
116
+ id: (0, drizzle_orm_pg_core.bigserial)(`id`, { mode: `number` }).primaryKey(),
117
+ tenantId: (0, drizzle_orm_pg_core.text)(`tenant_id`).notNull().default(`default`),
118
+ entityType: (0, drizzle_orm_pg_core.text)(`entity_type`).notNull(),
119
+ permission: (0, drizzle_orm_pg_core.text)(`permission`).notNull(),
120
+ subjectKind: (0, drizzle_orm_pg_core.text)(`subject_kind`).notNull(),
121
+ subjectValue: (0, drizzle_orm_pg_core.text)(`subject_value`).notNull(),
122
+ createdBy: (0, drizzle_orm_pg_core.text)(`created_by`),
123
+ expiresAt: (0, drizzle_orm_pg_core.timestamp)(`expires_at`, { withTimezone: true }),
124
+ createdAt: (0, drizzle_orm_pg_core.timestamp)(`created_at`, { withTimezone: true }).notNull().defaultNow(),
125
+ updatedAt: (0, drizzle_orm_pg_core.timestamp)(`updated_at`, { withTimezone: true }).notNull().defaultNow()
126
+ }, (table) => [
127
+ (0, drizzle_orm_pg_core.index)(`idx_type_permission_grants_lookup`).on(table.tenantId, table.entityType, table.permission, table.subjectKind, table.subjectValue),
128
+ (0, drizzle_orm_pg_core.index)(`idx_type_permission_grants_expiry`).on(table.tenantId, table.expiresAt),
129
+ (0, drizzle_orm_pg_core.check)(`chk_type_permission_grants_permission`, drizzle_orm.sql`${table.permission} IN ('spawn', 'manage')`),
130
+ (0, drizzle_orm_pg_core.check)(`chk_type_permission_grants_subject_kind`, drizzle_orm.sql`${table.subjectKind} IN ('principal', 'principal_kind')`)
131
+ ]);
132
+ const entityLineage = (0, drizzle_orm_pg_core.pgTable)(`entity_lineage`, {
133
+ tenantId: (0, drizzle_orm_pg_core.text)(`tenant_id`).notNull().default(`default`),
134
+ ancestorUrl: (0, drizzle_orm_pg_core.text)(`ancestor_url`).notNull(),
135
+ descendantUrl: (0, drizzle_orm_pg_core.text)(`descendant_url`).notNull(),
136
+ depth: (0, drizzle_orm_pg_core.integer)(`depth`).notNull(),
137
+ createdAt: (0, drizzle_orm_pg_core.timestamp)(`created_at`, { withTimezone: true }).notNull().defaultNow()
138
+ }, (table) => [
139
+ (0, drizzle_orm_pg_core.primaryKey)({ columns: [
140
+ table.tenantId,
141
+ table.ancestorUrl,
142
+ table.descendantUrl
143
+ ] }),
144
+ (0, drizzle_orm_pg_core.index)(`idx_entity_lineage_descendant`).on(table.tenantId, table.descendantUrl),
145
+ (0, drizzle_orm_pg_core.check)(`chk_entity_lineage_depth`, drizzle_orm.sql`${table.depth} >= 0`)
146
+ ]);
147
+ const entityPermissionGrants = (0, drizzle_orm_pg_core.pgTable)(`entity_permission_grants`, {
148
+ id: (0, drizzle_orm_pg_core.bigserial)(`id`, { mode: `number` }).primaryKey(),
149
+ tenantId: (0, drizzle_orm_pg_core.text)(`tenant_id`).notNull().default(`default`),
150
+ entityUrl: (0, drizzle_orm_pg_core.text)(`entity_url`).notNull(),
151
+ permission: (0, drizzle_orm_pg_core.text)(`permission`).notNull(),
152
+ subjectKind: (0, drizzle_orm_pg_core.text)(`subject_kind`).notNull(),
153
+ subjectValue: (0, drizzle_orm_pg_core.text)(`subject_value`).notNull(),
154
+ propagation: (0, drizzle_orm_pg_core.text)(`propagation`).notNull().default(`self`),
155
+ copyToChildren: (0, drizzle_orm_pg_core.boolean)(`copy_to_children`).notNull().default(false),
156
+ createdBy: (0, drizzle_orm_pg_core.text)(`created_by`),
157
+ expiresAt: (0, drizzle_orm_pg_core.timestamp)(`expires_at`, { withTimezone: true }),
158
+ createdAt: (0, drizzle_orm_pg_core.timestamp)(`created_at`, { withTimezone: true }).notNull().defaultNow(),
159
+ updatedAt: (0, drizzle_orm_pg_core.timestamp)(`updated_at`, { withTimezone: true }).notNull().defaultNow()
160
+ }, (table) => [
161
+ (0, drizzle_orm_pg_core.index)(`idx_entity_permission_grants_entity`).on(table.tenantId, table.entityUrl),
162
+ (0, drizzle_orm_pg_core.index)(`idx_entity_permission_grants_subject`).on(table.tenantId, table.permission, table.subjectKind, table.subjectValue),
163
+ (0, drizzle_orm_pg_core.index)(`idx_entity_permission_grants_expiry`).on(table.tenantId, table.expiresAt),
164
+ (0, drizzle_orm_pg_core.check)(`chk_entity_permission_grants_permission`, drizzle_orm.sql`${table.permission} IN ('read', 'write', 'delete', 'signal', 'fork', 'schedule', 'spawn', 'manage')`),
165
+ (0, drizzle_orm_pg_core.check)(`chk_entity_permission_grants_subject_kind`, drizzle_orm.sql`${table.subjectKind} IN ('principal', 'principal_kind')`),
166
+ (0, drizzle_orm_pg_core.check)(`chk_entity_permission_grants_propagation`, drizzle_orm.sql`${table.propagation} IN ('self', 'descendants')`)
167
+ ]);
168
+ const entityEffectivePermissions = (0, drizzle_orm_pg_core.pgTable)(`entity_effective_permissions`, {
169
+ id: (0, drizzle_orm_pg_core.bigserial)(`id`, { mode: `number` }).primaryKey(),
170
+ tenantId: (0, drizzle_orm_pg_core.text)(`tenant_id`).notNull().default(`default`),
171
+ entityUrl: (0, drizzle_orm_pg_core.text)(`entity_url`).notNull(),
172
+ sourceEntityUrl: (0, drizzle_orm_pg_core.text)(`source_entity_url`).notNull(),
173
+ sourceGrantId: (0, drizzle_orm_pg_core.bigint)(`source_grant_id`, { mode: `number` }).notNull(),
174
+ permission: (0, drizzle_orm_pg_core.text)(`permission`).notNull(),
175
+ subjectKind: (0, drizzle_orm_pg_core.text)(`subject_kind`).notNull(),
176
+ subjectValue: (0, drizzle_orm_pg_core.text)(`subject_value`).notNull(),
177
+ expiresAt: (0, drizzle_orm_pg_core.timestamp)(`expires_at`, { withTimezone: true }),
178
+ createdAt: (0, drizzle_orm_pg_core.timestamp)(`created_at`, { withTimezone: true }).notNull().defaultNow()
179
+ }, (table) => [
180
+ (0, drizzle_orm_pg_core.unique)(`uq_entity_effective_permission`).on(table.tenantId, table.entityUrl, table.sourceGrantId),
181
+ (0, drizzle_orm_pg_core.index)(`idx_entity_effective_permissions_lookup`).on(table.tenantId, table.permission, table.subjectKind, table.subjectValue, table.entityUrl),
182
+ (0, drizzle_orm_pg_core.index)(`idx_entity_effective_permissions_entity`).on(table.tenantId, table.entityUrl),
183
+ (0, drizzle_orm_pg_core.index)(`idx_entity_effective_permissions_expiry`).on(table.tenantId, table.expiresAt),
184
+ (0, drizzle_orm_pg_core.check)(`chk_entity_effective_permissions_permission`, drizzle_orm.sql`${table.permission} IN ('read', 'write', 'delete', 'signal', 'fork', 'schedule', 'spawn', 'manage')`),
185
+ (0, drizzle_orm_pg_core.check)(`chk_entity_effective_permissions_subject_kind`, drizzle_orm.sql`${table.subjectKind} IN ('principal', 'principal_kind')`)
186
+ ]);
187
+ const sharedStateLinks = (0, drizzle_orm_pg_core.pgTable)(`shared_state_links`, {
188
+ tenantId: (0, drizzle_orm_pg_core.text)(`tenant_id`).notNull().default(`default`),
189
+ sharedStateId: (0, drizzle_orm_pg_core.text)(`shared_state_id`).notNull(),
190
+ ownerEntityUrl: (0, drizzle_orm_pg_core.text)(`owner_entity_url`).notNull(),
191
+ manifestKey: (0, drizzle_orm_pg_core.text)(`manifest_key`).notNull(),
192
+ createdAt: (0, drizzle_orm_pg_core.timestamp)(`created_at`, { withTimezone: true }).notNull().defaultNow(),
193
+ updatedAt: (0, drizzle_orm_pg_core.timestamp)(`updated_at`, { withTimezone: true }).notNull().defaultNow()
194
+ }, (table) => [
195
+ (0, drizzle_orm_pg_core.primaryKey)({ columns: [
196
+ table.tenantId,
197
+ table.ownerEntityUrl,
198
+ table.manifestKey
199
+ ] }),
200
+ (0, drizzle_orm_pg_core.index)(`idx_shared_state_links_shared_state`).on(table.tenantId, table.sharedStateId),
201
+ (0, drizzle_orm_pg_core.index)(`idx_shared_state_links_owner`).on(table.tenantId, table.ownerEntityUrl)
202
+ ]);
110
203
  const users = (0, drizzle_orm_pg_core.pgTable)(`users`, {
111
204
  tenantId: (0, drizzle_orm_pg_core.text)(`tenant_id`).notNull().default(`default`),
112
205
  id: (0, drizzle_orm_pg_core.text)(`id`).notNull(),
@@ -293,12 +386,18 @@ const entityBridges = (0, drizzle_orm_pg_core.pgTable)(`entity_bridges`, {
293
386
  sourceRef: (0, drizzle_orm_pg_core.text)(`source_ref`).notNull(),
294
387
  tags: (0, drizzle_orm_pg_core.jsonb)(`tags`).notNull(),
295
388
  streamUrl: (0, drizzle_orm_pg_core.text)(`stream_url`).notNull(),
389
+ principalUrl: (0, drizzle_orm_pg_core.text)(`principal_url`),
390
+ principalKind: (0, drizzle_orm_pg_core.text)(`principal_kind`),
296
391
  shapeHandle: (0, drizzle_orm_pg_core.text)(`shape_handle`),
297
392
  shapeOffset: (0, drizzle_orm_pg_core.text)(`shape_offset`),
298
393
  lastObserverActivityAt: (0, drizzle_orm_pg_core.timestamp)(`last_observer_activity_at`, { withTimezone: true }).notNull().defaultNow(),
299
394
  createdAt: (0, drizzle_orm_pg_core.timestamp)(`created_at`, { withTimezone: true }).notNull().defaultNow(),
300
395
  updatedAt: (0, drizzle_orm_pg_core.timestamp)(`updated_at`, { withTimezone: true }).notNull().defaultNow()
301
- }, (table) => [(0, drizzle_orm_pg_core.primaryKey)({ columns: [table.tenantId, table.sourceRef] }), (0, drizzle_orm_pg_core.unique)(`uq_entity_bridges_stream_url`).on(table.tenantId, table.streamUrl)]);
396
+ }, (table) => [
397
+ (0, drizzle_orm_pg_core.primaryKey)({ columns: [table.tenantId, table.sourceRef] }),
398
+ (0, drizzle_orm_pg_core.unique)(`uq_entity_bridges_stream_url`).on(table.tenantId, table.streamUrl),
399
+ (0, drizzle_orm_pg_core.index)(`idx_entity_bridges_principal`).on(table.tenantId, table.principalKind, table.principalUrl)
400
+ ]);
302
401
  const entityManifestSources = (0, drizzle_orm_pg_core.pgTable)(`entity_manifest_sources`, {
303
402
  tenantId: (0, drizzle_orm_pg_core.text)(`tenant_id`).notNull().default(`default`),
304
403
  ownerEntityUrl: (0, drizzle_orm_pg_core.text)(`owner_entity_url`).notNull(),
@@ -486,16 +585,26 @@ function isDuplicateUrlError(err) {
486
585
  return e.code === `23505`;
487
586
  }
488
587
  const DEFAULT_RUNNER_LEASE_MS = 3e4;
588
+ const PERMISSION_PRUNE_INTERVAL_MS = 3e4;
489
589
  function runnerWakeStream(runnerId) {
490
590
  return `/runners/${runnerId}/wake`;
491
591
  }
492
592
  var PostgresRegistry = class {
593
+ lastPermissionPruneStartedAt = 0;
594
+ permissionPrunePromise = null;
493
595
  constructor(db, tenantId = DEFAULT_TENANT_ID) {
494
596
  this.db = db;
495
597
  this.tenantId = tenantId;
496
598
  }
497
599
  async initialize() {}
498
600
  close() {}
601
+ async ensureUserForPrincipal(principal) {
602
+ if (principal.kind !== `user`) return;
603
+ await this.db.insert(users).values({
604
+ tenantId: this.tenantId,
605
+ id: principal.id
606
+ }).onConflictDoNothing();
607
+ }
499
608
  async createRunner(input) {
500
609
  const now = new Date();
501
610
  const wakeStream = input.wakeStream ?? runnerWakeStream(input.id);
@@ -830,6 +939,59 @@ var PostgresRegistry = class {
830
939
  pendingSourceStreams: [],
831
940
  updatedAt: new Date()
832
941
  }).onConflictDoNothing();
942
+ await tx.insert(entityLineage).values({
943
+ tenantId: this.tenantId,
944
+ ancestorUrl: entity.url,
945
+ descendantUrl: entity.url,
946
+ depth: 0
947
+ }).onConflictDoNothing();
948
+ if (entity.parent) await tx.execute(drizzle_orm.sql`
949
+ INSERT INTO ${entityLineage} (
950
+ tenant_id,
951
+ ancestor_url,
952
+ descendant_url,
953
+ depth
954
+ )
955
+ SELECT
956
+ ${this.tenantId},
957
+ ancestor_url,
958
+ ${entity.url},
959
+ depth + 1
960
+ FROM ${entityLineage}
961
+ WHERE tenant_id = ${this.tenantId}
962
+ AND descendant_url = ${entity.parent}
963
+ ON CONFLICT DO NOTHING
964
+ `);
965
+ await tx.execute(drizzle_orm.sql`
966
+ INSERT INTO ${entityEffectivePermissions} (
967
+ tenant_id,
968
+ entity_url,
969
+ source_entity_url,
970
+ source_grant_id,
971
+ permission,
972
+ subject_kind,
973
+ subject_value,
974
+ expires_at
975
+ )
976
+ SELECT
977
+ ${this.tenantId},
978
+ ${entity.url},
979
+ grants.entity_url,
980
+ grants.id,
981
+ grants.permission,
982
+ grants.subject_kind,
983
+ grants.subject_value,
984
+ grants.expires_at
985
+ FROM ${entityPermissionGrants} grants
986
+ JOIN ${entityLineage} lineage
987
+ ON lineage.tenant_id = grants.tenant_id
988
+ AND lineage.ancestor_url = grants.entity_url
989
+ AND lineage.descendant_url = ${entity.url}
990
+ WHERE grants.tenant_id = ${this.tenantId}
991
+ AND grants.propagation = 'descendants'
992
+ AND (grants.expires_at IS NULL OR grants.expires_at > now())
993
+ ON CONFLICT DO NOTHING
994
+ `);
833
995
  return parseInt(result[0].txid);
834
996
  });
835
997
  } catch (err) {
@@ -851,10 +1013,8 @@ var PostgresRegistry = class {
851
1013
  }
852
1014
  async getEntityByStream(streamPath) {
853
1015
  const mainSuffix = `/main`;
854
- const errorSuffix = `/error`;
855
1016
  let entityUrl = null;
856
1017
  if (streamPath.endsWith(mainSuffix)) entityUrl = streamPath.slice(0, -mainSuffix.length);
857
- else if (streamPath.endsWith(errorSuffix)) entityUrl = streamPath.slice(0, -errorSuffix.length);
858
1018
  if (!entityUrl) return null;
859
1019
  return this.getEntity(entityUrl);
860
1020
  }
@@ -864,6 +1024,23 @@ var PostgresRegistry = class {
864
1024
  if (filter?.status) conditions.push((0, drizzle_orm.eq)(entities.status, filter.status));
865
1025
  if (filter?.parent) conditions.push((0, drizzle_orm.eq)(entities.parent, filter.parent));
866
1026
  if (filter?.created_by) conditions.push((0, drizzle_orm.eq)(entities.createdBy, filter.created_by));
1027
+ if (filter?.readableBy && !filter.readableBy.bypass) conditions.push(drizzle_orm.sql`(
1028
+ ${entities.createdBy} = ${filter.readableBy.principalUrl}
1029
+ OR ${entities.url} IN (
1030
+ SELECT ${entityEffectivePermissions.entityUrl}
1031
+ FROM ${entityEffectivePermissions}
1032
+ WHERE ${entityEffectivePermissions.tenantId} = ${this.tenantId}
1033
+ AND ${entityEffectivePermissions.permission} IN ('read', 'manage')
1034
+ AND (${entityEffectivePermissions.expiresAt} IS NULL OR ${entityEffectivePermissions.expiresAt} > now())
1035
+ AND (
1036
+ (${entityEffectivePermissions.subjectKind} = 'principal'
1037
+ AND ${entityEffectivePermissions.subjectValue} = ${filter.readableBy.principalUrl})
1038
+ OR
1039
+ (${entityEffectivePermissions.subjectKind} = 'principal_kind'
1040
+ AND ${entityEffectivePermissions.subjectValue} = ${filter.readableBy.principalKind})
1041
+ )
1042
+ )
1043
+ )`);
867
1044
  const whereClause = (0, drizzle_orm.and)(...conditions);
868
1045
  const countResult = await this.db.select({ count: drizzle_orm.sql`count(*)` }).from(entities).where(whereClause);
869
1046
  const total = Number(countResult[0].count);
@@ -876,6 +1053,189 @@ var PostgresRegistry = class {
876
1053
  total
877
1054
  };
878
1055
  }
1056
+ async createEntityTypePermissionGrant(input) {
1057
+ const [row] = await this.db.insert(entityTypePermissionGrants).values({
1058
+ tenantId: this.tenantId,
1059
+ entityType: input.entityType,
1060
+ permission: input.permission,
1061
+ subjectKind: input.subjectKind,
1062
+ subjectValue: input.subjectValue,
1063
+ createdBy: input.createdBy ?? null,
1064
+ expiresAt: input.expiresAt ?? null
1065
+ }).returning();
1066
+ return this.rowToEntityTypePermissionGrant(row);
1067
+ }
1068
+ async ensureEntityTypePermissionGrant(input) {
1069
+ const [existing] = await this.db.select().from(entityTypePermissionGrants).where((0, drizzle_orm.and)((0, drizzle_orm.eq)(entityTypePermissionGrants.tenantId, this.tenantId), (0, drizzle_orm.eq)(entityTypePermissionGrants.entityType, input.entityType), (0, drizzle_orm.eq)(entityTypePermissionGrants.permission, input.permission), (0, drizzle_orm.eq)(entityTypePermissionGrants.subjectKind, input.subjectKind), (0, drizzle_orm.eq)(entityTypePermissionGrants.subjectValue, input.subjectValue), input.expiresAt ? (0, drizzle_orm.eq)(entityTypePermissionGrants.expiresAt, input.expiresAt) : drizzle_orm.sql`${entityTypePermissionGrants.expiresAt} IS NULL`)).limit(1);
1070
+ if (existing) return this.rowToEntityTypePermissionGrant(existing);
1071
+ return await this.createEntityTypePermissionGrant(input);
1072
+ }
1073
+ async listEntityTypePermissionGrants(entityType) {
1074
+ const rows = await this.db.select().from(entityTypePermissionGrants).where((0, drizzle_orm.and)((0, drizzle_orm.eq)(entityTypePermissionGrants.tenantId, this.tenantId), (0, drizzle_orm.eq)(entityTypePermissionGrants.entityType, entityType))).orderBy(entityTypePermissionGrants.id);
1075
+ return rows.map((row) => this.rowToEntityTypePermissionGrant(row));
1076
+ }
1077
+ async deleteEntityTypePermissionGrant(entityType, grantId) {
1078
+ const rows = await this.db.delete(entityTypePermissionGrants).where((0, drizzle_orm.and)((0, drizzle_orm.eq)(entityTypePermissionGrants.tenantId, this.tenantId), (0, drizzle_orm.eq)(entityTypePermissionGrants.entityType, entityType), (0, drizzle_orm.eq)(entityTypePermissionGrants.id, grantId))).returning({ id: entityTypePermissionGrants.id });
1079
+ return rows.length > 0;
1080
+ }
1081
+ async hasEntityTypePermission(entityType, permission, subject) {
1082
+ const permissions = [permission, `manage`];
1083
+ const rows = await this.db.select({ id: entityTypePermissionGrants.id }).from(entityTypePermissionGrants).where((0, drizzle_orm.and)((0, drizzle_orm.eq)(entityTypePermissionGrants.tenantId, this.tenantId), (0, drizzle_orm.eq)(entityTypePermissionGrants.entityType, entityType), (0, drizzle_orm.inArray)(entityTypePermissionGrants.permission, [...permissions]), drizzle_orm.sql`(${entityTypePermissionGrants.expiresAt} IS NULL OR ${entityTypePermissionGrants.expiresAt} > now())`, drizzle_orm.sql`(
1084
+ (${entityTypePermissionGrants.subjectKind} = 'principal'
1085
+ AND ${entityTypePermissionGrants.subjectValue} = ${subject.principalUrl})
1086
+ OR
1087
+ (${entityTypePermissionGrants.subjectKind} = 'principal_kind'
1088
+ AND ${entityTypePermissionGrants.subjectValue} = ${subject.principalKind})
1089
+ )`)).limit(1);
1090
+ return rows.length > 0;
1091
+ }
1092
+ async createEntityPermissionGrant(input) {
1093
+ return await this.db.transaction(async (tx) => {
1094
+ const [row] = await tx.insert(entityPermissionGrants).values({
1095
+ tenantId: this.tenantId,
1096
+ entityUrl: input.entityUrl,
1097
+ permission: input.permission,
1098
+ subjectKind: input.subjectKind,
1099
+ subjectValue: input.subjectValue,
1100
+ propagation: input.propagation ?? `self`,
1101
+ copyToChildren: input.copyToChildren ?? false,
1102
+ createdBy: input.createdBy ?? null,
1103
+ expiresAt: input.expiresAt ?? null
1104
+ }).returning();
1105
+ await this.materializeEntityPermissionGrant(tx, row);
1106
+ return this.rowToEntityPermissionGrant(row);
1107
+ });
1108
+ }
1109
+ async listEntityPermissionGrants(entityUrl) {
1110
+ const rows = await this.db.select().from(entityPermissionGrants).where((0, drizzle_orm.and)((0, drizzle_orm.eq)(entityPermissionGrants.tenantId, this.tenantId), (0, drizzle_orm.eq)(entityPermissionGrants.entityUrl, entityUrl))).orderBy(entityPermissionGrants.id);
1111
+ return rows.map((row) => this.rowToEntityPermissionGrant(row));
1112
+ }
1113
+ async deleteEntityPermissionGrant(entityUrl, grantId) {
1114
+ return await this.db.transaction(async (tx) => {
1115
+ await tx.delete(entityEffectivePermissions).where((0, drizzle_orm.and)((0, drizzle_orm.eq)(entityEffectivePermissions.tenantId, this.tenantId), (0, drizzle_orm.eq)(entityEffectivePermissions.sourceGrantId, grantId)));
1116
+ const rows = await tx.delete(entityPermissionGrants).where((0, drizzle_orm.and)((0, drizzle_orm.eq)(entityPermissionGrants.tenantId, this.tenantId), (0, drizzle_orm.eq)(entityPermissionGrants.entityUrl, entityUrl), (0, drizzle_orm.eq)(entityPermissionGrants.id, grantId))).returning({ id: entityPermissionGrants.id });
1117
+ return rows.length > 0;
1118
+ });
1119
+ }
1120
+ async copyEntityPermissionGrantsForSpawn(parentEntityUrl, childEntityUrl, createdBy) {
1121
+ const parentGrants = await this.db.select().from(entityPermissionGrants).where((0, drizzle_orm.and)((0, drizzle_orm.eq)(entityPermissionGrants.tenantId, this.tenantId), (0, drizzle_orm.eq)(entityPermissionGrants.entityUrl, parentEntityUrl), (0, drizzle_orm.eq)(entityPermissionGrants.copyToChildren, true), drizzle_orm.sql`(${entityPermissionGrants.expiresAt} IS NULL OR ${entityPermissionGrants.expiresAt} > now())`));
1122
+ const copied = [];
1123
+ for (const grant of parentGrants) copied.push(await this.createEntityPermissionGrant({
1124
+ entityUrl: childEntityUrl,
1125
+ permission: grant.permission,
1126
+ subjectKind: grant.subjectKind,
1127
+ subjectValue: grant.subjectValue,
1128
+ propagation: `self`,
1129
+ copyToChildren: grant.copyToChildren,
1130
+ createdBy,
1131
+ expiresAt: grant.expiresAt ?? void 0
1132
+ }));
1133
+ return copied;
1134
+ }
1135
+ async hasEntityPermission(entityUrl, permission, subject) {
1136
+ const permissions = [permission, `manage`];
1137
+ const rows = await this.db.select({ id: entityEffectivePermissions.id }).from(entityEffectivePermissions).where((0, drizzle_orm.and)((0, drizzle_orm.eq)(entityEffectivePermissions.tenantId, this.tenantId), (0, drizzle_orm.eq)(entityEffectivePermissions.entityUrl, entityUrl), (0, drizzle_orm.inArray)(entityEffectivePermissions.permission, [...permissions]), drizzle_orm.sql`(${entityEffectivePermissions.expiresAt} IS NULL OR ${entityEffectivePermissions.expiresAt} > now())`, drizzle_orm.sql`(
1138
+ (${entityEffectivePermissions.subjectKind} = 'principal'
1139
+ AND ${entityEffectivePermissions.subjectValue} = ${subject.principalUrl})
1140
+ OR
1141
+ (${entityEffectivePermissions.subjectKind} = 'principal_kind'
1142
+ AND ${entityEffectivePermissions.subjectValue} = ${subject.principalKind})
1143
+ )`)).limit(1);
1144
+ return rows.length > 0;
1145
+ }
1146
+ async replaceSharedStateLink(ownerEntityUrl, manifestKey, sharedStateId) {
1147
+ await this.db.delete(sharedStateLinks).where((0, drizzle_orm.and)((0, drizzle_orm.eq)(sharedStateLinks.tenantId, this.tenantId), (0, drizzle_orm.eq)(sharedStateLinks.ownerEntityUrl, ownerEntityUrl), (0, drizzle_orm.eq)(sharedStateLinks.manifestKey, manifestKey)));
1148
+ if (!sharedStateId) return;
1149
+ await this.db.insert(sharedStateLinks).values({
1150
+ tenantId: this.tenantId,
1151
+ ownerEntityUrl,
1152
+ manifestKey,
1153
+ sharedStateId
1154
+ }).onConflictDoUpdate({
1155
+ target: [
1156
+ sharedStateLinks.tenantId,
1157
+ sharedStateLinks.ownerEntityUrl,
1158
+ sharedStateLinks.manifestKey
1159
+ ],
1160
+ set: {
1161
+ sharedStateId,
1162
+ updatedAt: new Date()
1163
+ }
1164
+ });
1165
+ }
1166
+ async listSharedStateLinkedEntityUrls(sharedStateId) {
1167
+ const rows = await this.db.selectDistinct({ ownerEntityUrl: sharedStateLinks.ownerEntityUrl }).from(sharedStateLinks).where((0, drizzle_orm.and)((0, drizzle_orm.eq)(sharedStateLinks.tenantId, this.tenantId), (0, drizzle_orm.eq)(sharedStateLinks.sharedStateId, sharedStateId)));
1168
+ return rows.map((row) => row.ownerEntityUrl);
1169
+ }
1170
+ async pruneExpiredPermissionGrants(now = new Date(), options = {}) {
1171
+ if (this.permissionPrunePromise) return await this.permissionPrunePromise;
1172
+ const startedAt = Date.now();
1173
+ if (!options.force && startedAt - this.lastPermissionPruneStartedAt < PERMISSION_PRUNE_INTERVAL_MS) return;
1174
+ this.lastPermissionPruneStartedAt = startedAt;
1175
+ const promise = this.pruneExpiredPermissionGrantsNow(now);
1176
+ this.permissionPrunePromise = promise;
1177
+ try {
1178
+ await promise;
1179
+ } catch (error) {
1180
+ this.lastPermissionPruneStartedAt = 0;
1181
+ throw error;
1182
+ } finally {
1183
+ if (this.permissionPrunePromise === promise) this.permissionPrunePromise = null;
1184
+ }
1185
+ }
1186
+ async pruneExpiredPermissionGrantsNow(now) {
1187
+ await this.db.transaction(async (tx) => {
1188
+ const expiredEntityGrantIds = await tx.select({ id: entityPermissionGrants.id }).from(entityPermissionGrants).where((0, drizzle_orm.and)((0, drizzle_orm.eq)(entityPermissionGrants.tenantId, this.tenantId), drizzle_orm.sql`${entityPermissionGrants.expiresAt} IS NOT NULL`, (0, drizzle_orm.lt)(entityPermissionGrants.expiresAt, now)));
1189
+ const ids = expiredEntityGrantIds.map((row) => row.id);
1190
+ if (ids.length > 0) {
1191
+ await tx.delete(entityEffectivePermissions).where((0, drizzle_orm.and)((0, drizzle_orm.eq)(entityEffectivePermissions.tenantId, this.tenantId), (0, drizzle_orm.inArray)(entityEffectivePermissions.sourceGrantId, ids)));
1192
+ await tx.delete(entityPermissionGrants).where((0, drizzle_orm.and)((0, drizzle_orm.eq)(entityPermissionGrants.tenantId, this.tenantId), (0, drizzle_orm.inArray)(entityPermissionGrants.id, ids)));
1193
+ }
1194
+ await tx.delete(entityEffectivePermissions).where((0, drizzle_orm.and)((0, drizzle_orm.eq)(entityEffectivePermissions.tenantId, this.tenantId), drizzle_orm.sql`${entityEffectivePermissions.expiresAt} IS NOT NULL`, (0, drizzle_orm.lt)(entityEffectivePermissions.expiresAt, now)));
1195
+ await tx.delete(entityTypePermissionGrants).where((0, drizzle_orm.and)((0, drizzle_orm.eq)(entityTypePermissionGrants.tenantId, this.tenantId), drizzle_orm.sql`${entityTypePermissionGrants.expiresAt} IS NOT NULL`, (0, drizzle_orm.lt)(entityTypePermissionGrants.expiresAt, now)));
1196
+ });
1197
+ }
1198
+ async materializeEntityPermissionGrant(tx, grant) {
1199
+ await tx.delete(entityEffectivePermissions).where((0, drizzle_orm.and)((0, drizzle_orm.eq)(entityEffectivePermissions.tenantId, this.tenantId), (0, drizzle_orm.eq)(entityEffectivePermissions.sourceGrantId, grant.id)));
1200
+ if (grant.propagation === `descendants`) {
1201
+ await tx.execute(drizzle_orm.sql`
1202
+ INSERT INTO ${entityEffectivePermissions} (
1203
+ tenant_id,
1204
+ entity_url,
1205
+ source_entity_url,
1206
+ source_grant_id,
1207
+ permission,
1208
+ subject_kind,
1209
+ subject_value,
1210
+ expires_at
1211
+ )
1212
+ SELECT
1213
+ ${this.tenantId},
1214
+ descendant_url,
1215
+ ${grant.entityUrl},
1216
+ ${grant.id},
1217
+ ${grant.permission},
1218
+ ${grant.subjectKind},
1219
+ ${grant.subjectValue},
1220
+ ${grant.expiresAt}
1221
+ FROM ${entityLineage}
1222
+ WHERE tenant_id = ${this.tenantId}
1223
+ AND ancestor_url = ${grant.entityUrl}
1224
+ ON CONFLICT DO NOTHING
1225
+ `);
1226
+ return;
1227
+ }
1228
+ await tx.insert(entityEffectivePermissions).values({
1229
+ tenantId: this.tenantId,
1230
+ entityUrl: grant.entityUrl,
1231
+ sourceEntityUrl: grant.entityUrl,
1232
+ sourceGrantId: grant.id,
1233
+ permission: grant.permission,
1234
+ subjectKind: grant.subjectKind,
1235
+ subjectValue: grant.subjectValue,
1236
+ expiresAt: grant.expiresAt
1237
+ }).onConflictDoNothing();
1238
+ }
879
1239
  async updateStatus(entityUrl, status$4) {
880
1240
  const whereClause = isTerminalEntityStatus(status$4) ? this.entityWhere(entityUrl) : (0, drizzle_orm.and)(this.entityWhere(entityUrl), (0, drizzle_orm.ne)(entities.status, `stopped`), (0, drizzle_orm.ne)(entities.status, `killed`));
881
1241
  await this.db.update(entities).set({
@@ -977,7 +1337,9 @@ var PostgresRegistry = class {
977
1337
  tenantId: this.tenantId,
978
1338
  sourceRef: row.sourceRef,
979
1339
  tags: (0, __electric_ax_agents_runtime.normalizeTags)(row.tags),
980
- streamUrl: row.streamUrl
1340
+ streamUrl: row.streamUrl,
1341
+ principalUrl: row.principalUrl,
1342
+ principalKind: row.principalKind
981
1343
  }).onConflictDoNothing();
982
1344
  const existing = await this.getEntityBridge(row.sourceRef);
983
1345
  if (!existing) throw new Error(`Failed to load entity bridge ${row.sourceRef}`);
@@ -1139,15 +1501,40 @@ var PostgresRegistry = class {
1139
1501
  updated_at: row.updatedAt
1140
1502
  };
1141
1503
  }
1504
+ rowToEntityTypePermissionGrant(row) {
1505
+ return {
1506
+ id: row.id,
1507
+ entity_type: row.entityType,
1508
+ permission: row.permission,
1509
+ subject_kind: row.subjectKind,
1510
+ subject_value: row.subjectValue,
1511
+ created_by: row.createdBy ?? void 0,
1512
+ expires_at: row.expiresAt?.toISOString(),
1513
+ created_at: row.createdAt.toISOString(),
1514
+ updated_at: row.updatedAt.toISOString()
1515
+ };
1516
+ }
1517
+ rowToEntityPermissionGrant(row) {
1518
+ return {
1519
+ id: row.id,
1520
+ entity_url: row.entityUrl,
1521
+ permission: row.permission,
1522
+ subject_kind: row.subjectKind,
1523
+ subject_value: row.subjectValue,
1524
+ propagation: row.propagation,
1525
+ copy_to_children: row.copyToChildren,
1526
+ created_by: row.createdBy ?? void 0,
1527
+ expires_at: row.expiresAt?.toISOString(),
1528
+ created_at: row.createdAt.toISOString(),
1529
+ updated_at: row.updatedAt.toISOString()
1530
+ };
1531
+ }
1142
1532
  rowToEntity(row) {
1143
1533
  return {
1144
1534
  url: row.url,
1145
1535
  type: row.type,
1146
1536
  status: assertEntityStatus(row.status),
1147
- streams: {
1148
- main: `${row.url}/main`,
1149
- error: `${row.url}/error`
1150
- },
1537
+ streams: { main: `${row.url}/main` },
1151
1538
  subscription_id: row.subscriptionId,
1152
1539
  dispatch_policy: row.dispatchPolicy ?? void 0,
1153
1540
  write_token: row.writeToken,
@@ -1169,6 +1556,8 @@ var PostgresRegistry = class {
1169
1556
  sourceRef: row.sourceRef,
1170
1557
  tags: row.tags ?? {},
1171
1558
  streamUrl: row.streamUrl,
1559
+ principalUrl: row.principalUrl ?? void 0,
1560
+ principalKind: row.principalKind ?? void 0,
1172
1561
  shapeHandle: row.shapeHandle ?? void 0,
1173
1562
  shapeOffset: row.shapeOffset ?? void 0,
1174
1563
  lastObserverActivityAt: row.lastObserverActivityAt,
@@ -1323,6 +1712,93 @@ const serverLog = {
1323
1712
  }
1324
1713
  };
1325
1714
 
1715
+ //#endregion
1716
+ //#region src/principal.ts
1717
+ const ELECTRIC_PRINCIPAL_HEADER = `electric-principal`;
1718
+ const PRINCIPAL_KINDS = new Set([
1719
+ `user`,
1720
+ `agent`,
1721
+ `service`,
1722
+ `system`
1723
+ ]);
1724
+ function parsePrincipalKey(input) {
1725
+ const colon = input.indexOf(`:`);
1726
+ if (colon <= 0) throw new Error(`Invalid principal identifier`);
1727
+ const kind = input.slice(0, colon);
1728
+ const id = input.slice(colon + 1);
1729
+ if (!PRINCIPAL_KINDS.has(kind)) throw new Error(`Invalid principal kind`);
1730
+ if (!id || id.includes(`/`)) throw new Error(`Invalid principal id`);
1731
+ const key = `${kind}:${id}`;
1732
+ return {
1733
+ kind,
1734
+ id,
1735
+ key,
1736
+ url: `/principal/${encodeURIComponent(key)}`
1737
+ };
1738
+ }
1739
+ function principalUrl(key) {
1740
+ return parsePrincipalKey(key).url;
1741
+ }
1742
+ function parsePrincipalUrl(url) {
1743
+ if (!url.startsWith(`/principal/`)) return null;
1744
+ const segment = url.slice(`/principal/`.length);
1745
+ if (!segment || segment.includes(`/`)) return null;
1746
+ try {
1747
+ return parsePrincipalKey(decodeURIComponent(segment));
1748
+ } catch {
1749
+ return null;
1750
+ }
1751
+ }
1752
+ const BUILT_IN_SYSTEM_PRINCIPAL_IDS = new Set([
1753
+ `framework`,
1754
+ `auth-sync`,
1755
+ `dev-local`
1756
+ ]);
1757
+ function isBuiltInSystemPrincipalUrl(url) {
1758
+ if (!url?.startsWith(`/principal/`)) return false;
1759
+ try {
1760
+ const principal = parsePrincipalUrl(url);
1761
+ if (!principal) return false;
1762
+ return principal.kind === `system` && BUILT_IN_SYSTEM_PRINCIPAL_IDS.has(principal.id);
1763
+ } catch {
1764
+ return false;
1765
+ }
1766
+ }
1767
+ function principalFromCreatedBy(createdBy) {
1768
+ if (!createdBy) return void 0;
1769
+ const principal = parsePrincipalUrl(createdBy);
1770
+ if (!principal) return {
1771
+ url: createdBy,
1772
+ key: null
1773
+ };
1774
+ return {
1775
+ url: principal.url,
1776
+ key: principal.key,
1777
+ kind: principal.kind,
1778
+ id: principal.id
1779
+ };
1780
+ }
1781
+ const principalIdentityStateSchema = __sinclair_typebox.Type.Object({
1782
+ kind: __sinclair_typebox.Type.Union([
1783
+ __sinclair_typebox.Type.Literal(`user`),
1784
+ __sinclair_typebox.Type.Literal(`agent`),
1785
+ __sinclair_typebox.Type.Literal(`service`),
1786
+ __sinclair_typebox.Type.Literal(`system`)
1787
+ ]),
1788
+ id: __sinclair_typebox.Type.String(),
1789
+ key: __sinclair_typebox.Type.String(),
1790
+ url: __sinclair_typebox.Type.String(),
1791
+ updated_at: __sinclair_typebox.Type.String(),
1792
+ display_name: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
1793
+ email: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
1794
+ avatar_url: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
1795
+ auth_provider: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
1796
+ auth_subject: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
1797
+ claims: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Record(__sinclair_typebox.Type.String(), __sinclair_typebox.Type.Unknown())),
1798
+ created_at: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String())
1799
+ }, { additionalProperties: false });
1800
+ const principalUpdateIdentityMessageSchema = __sinclair_typebox.Type.Object({ identity: principalIdentityStateSchema }, { additionalProperties: false });
1801
+
1326
1802
  //#endregion
1327
1803
  //#region src/entity-projector.ts
1328
1804
  const ENTITY_SHAPE_COLUMNS = [
@@ -1331,6 +1807,7 @@ const ENTITY_SHAPE_COLUMNS = [
1331
1807
  `type`,
1332
1808
  `status`,
1333
1809
  `tags`,
1810
+ `created_by`,
1334
1811
  `spawn_args`,
1335
1812
  `sandbox`,
1336
1813
  `parent`,
@@ -1350,6 +1827,12 @@ function sourceRefFromStreamPath(streamPath) {
1350
1827
  const match = streamPath.match(/^\/_entities\/([^/]+)$/);
1351
1828
  return match?.[1] ?? null;
1352
1829
  }
1830
+ function principalScopedSourceRef(tagSourceRef, principalUrl$1, principalKind) {
1831
+ return `${tagSourceRef}-${(0, __electric_ax_agents_runtime.hashString)(JSON.stringify({
1832
+ principalKind,
1833
+ principalUrl: principalUrl$1
1834
+ }))}`;
1835
+ }
1353
1836
  function sameMember(left, right) {
1354
1837
  return JSON.stringify(left) === JSON.stringify(right);
1355
1838
  }
@@ -1380,15 +1863,22 @@ var ProjectedEntityBridge = class {
1380
1863
  sourceRef;
1381
1864
  tags;
1382
1865
  streamUrl;
1866
+ principalUrl;
1867
+ principalKind;
1868
+ permissionBypass;
1383
1869
  currentMembers = new Map();
1384
1870
  producer = null;
1385
1871
  stopped = false;
1386
- constructor(row, streamClient) {
1872
+ constructor(row, registry, streamClient) {
1873
+ this.registry = registry;
1387
1874
  this.streamClient = streamClient;
1388
1875
  this.tenantId = row.tenantId;
1389
1876
  this.sourceRef = row.sourceRef;
1390
1877
  this.tags = (0, __electric_ax_agents_runtime.normalizeTags)(row.tags);
1391
1878
  this.streamUrl = row.streamUrl;
1879
+ this.principalUrl = row.principalUrl;
1880
+ this.principalKind = row.principalKind;
1881
+ this.permissionBypass = isBuiltInSystemPrincipalUrl(row.principalUrl);
1392
1882
  }
1393
1883
  async start(initialEntities) {
1394
1884
  await this.ensureStream();
@@ -1402,7 +1892,7 @@ var ProjectedEntityBridge = class {
1402
1892
  }
1403
1893
  });
1404
1894
  await this.loadCurrentMembers();
1405
- this.reconcile(initialEntities);
1895
+ await this.reconcile(initialEntities);
1406
1896
  }
1407
1897
  async stop() {
1408
1898
  this.stopped = true;
@@ -1414,12 +1904,13 @@ var ProjectedEntityBridge = class {
1414
1904
  this.producer = null;
1415
1905
  }
1416
1906
  }
1417
- reconcile(entities$1) {
1907
+ async reconcile(entities$1) {
1418
1908
  if (this.stopped) return;
1419
1909
  const staleMembers = new Map(this.currentMembers);
1420
1910
  for (const entity of entities$1) {
1421
1911
  if (entity.tenant_id !== this.tenantId) continue;
1422
1912
  if (!entityMatchesTags(entity, this.tags)) continue;
1913
+ if (!await this.canReadEntity(entity)) continue;
1423
1914
  staleMembers.delete(entity.url);
1424
1915
  this.upsertEntity(entity);
1425
1916
  }
@@ -1428,10 +1919,10 @@ var ProjectedEntityBridge = class {
1428
1919
  this.currentMembers.delete(url);
1429
1920
  }
1430
1921
  }
1431
- applyEntity(entity) {
1922
+ async applyEntity(entity) {
1432
1923
  if (this.stopped) return;
1433
1924
  if (entity.tenant_id !== this.tenantId) return;
1434
- if (!entityMatchesTags(entity, this.tags)) {
1925
+ if (!entityMatchesTags(entity, this.tags) || !await this.canReadEntity(entity)) {
1435
1926
  const existing = this.currentMembers.get(entity.url);
1436
1927
  if (!existing) return;
1437
1928
  this.append(`delete`, existing);
@@ -1460,6 +1951,15 @@ var ProjectedEntityBridge = class {
1460
1951
  this.currentMembers.set(entity.url, next);
1461
1952
  }
1462
1953
  }
1954
+ async canReadEntity(entity) {
1955
+ if (this.permissionBypass) return true;
1956
+ if (!this.principalUrl || !this.principalKind) return false;
1957
+ if (entity.created_by === this.principalUrl) return true;
1958
+ return await this.registry.hasEntityPermission(entity.url, `read`, {
1959
+ principalUrl: this.principalUrl,
1960
+ principalKind: this.principalKind
1961
+ });
1962
+ }
1463
1963
  async ensureStream() {
1464
1964
  if (!await this.streamClient.exists(this.streamUrl)) await this.streamClient.create(this.streamUrl, { contentType: `application/json` });
1465
1965
  }
@@ -1564,17 +2064,19 @@ var EntityProjector = class {
1564
2064
  this.activeReaders.clear();
1565
2065
  await Promise.all(projections.map((projection) => projection.stop()));
1566
2066
  }
1567
- async register(tenantId, registry, tagsInput) {
2067
+ async register(tenantId, registry, tagsInput, principalUrl$1, principalKind) {
1568
2068
  if (!this.electricUrl) throw new Error(`[entity-projector] Electric URL is required for entities()`);
1569
2069
  await this.start();
1570
2070
  this.registries.set(tenantId, registry);
1571
2071
  const tags = (0, __electric_ax_agents_runtime.normalizeTags)((0, __electric_ax_agents_runtime.assertTags)(tagsInput));
1572
- const sourceRef = (0, __electric_ax_agents_runtime.sourceRefForTags)(tags);
2072
+ const sourceRef = principalScopedSourceRef((0, __electric_ax_agents_runtime.sourceRefForTags)(tags), principalUrl$1, principalKind);
1573
2073
  const streamUrl = (0, __electric_ax_agents_runtime.getEntitiesStreamPath)(sourceRef);
1574
2074
  const row = await registry.upsertEntityBridge({
1575
2075
  sourceRef,
1576
2076
  tags,
1577
- streamUrl
2077
+ streamUrl,
2078
+ principalUrl: principalUrl$1,
2079
+ principalKind
1578
2080
  });
1579
2081
  await registry.touchEntityBridge(sourceRef);
1580
2082
  await this.ensureProjection(row);
@@ -1603,7 +2105,11 @@ var EntityProjector = class {
1603
2105
  await this.touchSourceRef(tenantId, registry, sourceRef, `read-close`);
1604
2106
  };
1605
2107
  }
1606
- async onEntityChanged(_tenantId, _entityUrl) {}
2108
+ async onEntityChanged(tenantId, entityUrl) {
2109
+ const entity = this.entities.get(entityKey(tenantId, entityUrl));
2110
+ if (!entity) return;
2111
+ for (const projection of this.projectionsForTenant(tenantId)) await projection.applyEntity(entity);
2112
+ }
1607
2113
  async loadTenantBridges(tenantId, registry = this.registryForTenant(tenantId)) {
1608
2114
  if (!this.started || !this.electricUrl) return;
1609
2115
  await this.loadPersistedBridgesForTenant(tenantId, registry);
@@ -1664,16 +2170,16 @@ var EntityProjector = class {
1664
2170
  }
1665
2171
  if (message.headers.control === `up-to-date`) {
1666
2172
  this.upToDate = true;
1667
- this.reconcileAll();
2173
+ await this.reconcileAll();
1668
2174
  this.readyResolve?.();
1669
2175
  }
1670
2176
  continue;
1671
2177
  }
1672
2178
  if (!(0, __electric_sql_client.isChangeMessage)(message)) continue;
1673
- this.applyChangeMessage(message);
2179
+ await this.applyChangeMessage(message);
1674
2180
  }
1675
2181
  }
1676
- applyChangeMessage(message) {
2182
+ async applyChangeMessage(message) {
1677
2183
  const entity = message.value;
1678
2184
  const key = entityKey(entity.tenant_id, entity.url);
1679
2185
  if (message.headers.operation === `delete`) {
@@ -1682,7 +2188,7 @@ var EntityProjector = class {
1682
2188
  return;
1683
2189
  }
1684
2190
  this.entities.set(key, entity);
1685
- if (this.upToDate) for (const projection of this.projectionsForTenant(entity.tenant_id)) projection.applyEntity(entity);
2191
+ if (this.upToDate) for (const projection of this.projectionsForTenant(entity.tenant_id)) await projection.applyEntity(entity);
1686
2192
  }
1687
2193
  async loadPersistedBridges() {
1688
2194
  const registry = new PostgresRegistry(this.db);
@@ -1745,7 +2251,7 @@ var EntityProjector = class {
1745
2251
  }
1746
2252
  throw error;
1747
2253
  }
1748
- const projection = new ProjectedEntityBridge(row, streamClient);
2254
+ const projection = new ProjectedEntityBridge(row, this.registryForTenant(row.tenantId), streamClient);
1749
2255
  await projection.start(this.entitiesForTenant(row.tenantId));
1750
2256
  this.projections.set(key, projection);
1751
2257
  })().finally(() => {
@@ -1760,8 +2266,8 @@ var EntityProjector = class {
1760
2266
  projectionsForTenant(tenantId) {
1761
2267
  return [...this.projections.values()].filter((projection) => projection.tenantId === tenantId);
1762
2268
  }
1763
- reconcileAll() {
1764
- for (const projection of this.projections.values()) projection.reconcile(this.entitiesForTenant(projection.tenantId));
2269
+ async reconcileAll() {
2270
+ for (const projection of this.projections.values()) await projection.reconcile(this.entitiesForTenant(projection.tenantId));
1765
2271
  }
1766
2272
  async touchSourceRef(tenantId, registry, sourceRef, reason) {
1767
2273
  try {
@@ -1803,8 +2309,8 @@ var EntityProjectorTenantFacade = class {
1803
2309
  await this.projector.start();
1804
2310
  }
1805
2311
  async stop() {}
1806
- async register(tagsInput) {
1807
- return await this.projector.register(this.tenantId, this.registry, tagsInput);
2312
+ async register(tagsInput, principalUrl$1, principalKind) {
2313
+ return await this.projector.register(this.tenantId, this.registry, tagsInput, principalUrl$1, principalKind);
1808
2314
  }
1809
2315
  async onEntityChanged(entityUrl) {
1810
2316
  await this.projector.onEntityChanged(this.tenantId, entityUrl);
@@ -2686,93 +3192,6 @@ function assertSharedSandboxColocated(key, chosenIsRemote, dispatchPolicy) {
2686
3192
  if (!pinnedToSingleRunner) throw new ElectricAgentsError(ErrCodeInvalidRequest, `a shared sandbox (sandbox.key / sandbox.inherit) requires the entity to be pinned to a single runner via dispatch_policy, so all collaborators share one host.`, 400);
2687
3193
  }
2688
3194
 
2689
- //#endregion
2690
- //#region src/principal.ts
2691
- const ELECTRIC_PRINCIPAL_HEADER = `electric-principal`;
2692
- const PRINCIPAL_KINDS = new Set([
2693
- `user`,
2694
- `agent`,
2695
- `service`,
2696
- `system`
2697
- ]);
2698
- function parsePrincipalKey(input) {
2699
- const colon = input.indexOf(`:`);
2700
- if (colon <= 0) throw new Error(`Invalid principal identifier`);
2701
- const kind = input.slice(0, colon);
2702
- const id = input.slice(colon + 1);
2703
- if (!PRINCIPAL_KINDS.has(kind)) throw new Error(`Invalid principal kind`);
2704
- if (!id || id.includes(`/`)) throw new Error(`Invalid principal id`);
2705
- const key = `${kind}:${id}`;
2706
- return {
2707
- kind,
2708
- id,
2709
- key,
2710
- url: `/principal/${encodeURIComponent(key)}`
2711
- };
2712
- }
2713
- function principalUrl(key) {
2714
- return parsePrincipalKey(key).url;
2715
- }
2716
- function parsePrincipalUrl(url) {
2717
- if (!url.startsWith(`/principal/`)) return null;
2718
- const segment = url.slice(`/principal/`.length);
2719
- if (!segment || segment.includes(`/`)) return null;
2720
- try {
2721
- return parsePrincipalKey(decodeURIComponent(segment));
2722
- } catch {
2723
- return null;
2724
- }
2725
- }
2726
- const BUILT_IN_SYSTEM_PRINCIPAL_IDS = new Set([
2727
- `framework`,
2728
- `auth-sync`,
2729
- `dev-local`
2730
- ]);
2731
- function isBuiltInSystemPrincipalUrl(url) {
2732
- if (!url?.startsWith(`/principal/`)) return false;
2733
- try {
2734
- const principal = parsePrincipalUrl(url);
2735
- if (!principal) return false;
2736
- return principal.kind === `system` && BUILT_IN_SYSTEM_PRINCIPAL_IDS.has(principal.id);
2737
- } catch {
2738
- return false;
2739
- }
2740
- }
2741
- function principalFromCreatedBy(createdBy) {
2742
- if (!createdBy) return void 0;
2743
- const principal = parsePrincipalUrl(createdBy);
2744
- if (!principal) return {
2745
- url: createdBy,
2746
- key: null
2747
- };
2748
- return {
2749
- url: principal.url,
2750
- key: principal.key,
2751
- kind: principal.kind,
2752
- id: principal.id
2753
- };
2754
- }
2755
- const principalIdentityStateSchema = __sinclair_typebox.Type.Object({
2756
- kind: __sinclair_typebox.Type.Union([
2757
- __sinclair_typebox.Type.Literal(`user`),
2758
- __sinclair_typebox.Type.Literal(`agent`),
2759
- __sinclair_typebox.Type.Literal(`service`),
2760
- __sinclair_typebox.Type.Literal(`system`)
2761
- ]),
2762
- id: __sinclair_typebox.Type.String(),
2763
- key: __sinclair_typebox.Type.String(),
2764
- url: __sinclair_typebox.Type.String(),
2765
- updated_at: __sinclair_typebox.Type.String(),
2766
- display_name: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
2767
- email: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
2768
- avatar_url: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
2769
- auth_provider: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
2770
- auth_subject: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
2771
- claims: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Record(__sinclair_typebox.Type.String(), __sinclair_typebox.Type.Unknown())),
2772
- created_at: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String())
2773
- }, { additionalProperties: false });
2774
- const principalUpdateIdentityMessageSchema = __sinclair_typebox.Type.Object({ identity: principalIdentityStateSchema }, { additionalProperties: false });
2775
-
2776
3195
  //#endregion
2777
3196
  //#region src/manifest-side-effects.ts
2778
3197
  function isRecord$1(value) {
@@ -3008,7 +3427,10 @@ var EntityManager = class {
3008
3427
  }
3009
3428
  async ensurePrincipal(principal) {
3010
3429
  const existing = await this.registry.getEntity(principal.url);
3011
- if (existing) return existing;
3430
+ if (existing) {
3431
+ await this.ensureUserPrincipal(principal);
3432
+ return existing;
3433
+ }
3012
3434
  await this.ensurePrincipalEntityType();
3013
3435
  try {
3014
3436
  const entity = await this.spawn(`principal`, {
@@ -3037,15 +3459,22 @@ var EntityManager = class {
3037
3459
  updated_at: now
3038
3460
  }
3039
3461
  }));
3462
+ await this.ensureUserPrincipal(principal);
3040
3463
  return entity;
3041
3464
  } catch (error) {
3042
3465
  if (error instanceof ElectricAgentsError && error.code === ErrCodeDuplicateURL) {
3043
3466
  const raced = await this.registry.getEntity(principal.url);
3044
- if (raced) return raced;
3467
+ if (raced) {
3468
+ await this.ensureUserPrincipal(principal);
3469
+ return raced;
3470
+ }
3045
3471
  }
3046
3472
  throw error;
3047
3473
  }
3048
3474
  }
3475
+ async ensureUserPrincipal(principal) {
3476
+ if (principal.kind === `user`) await this.registry.ensureUserForPrincipal(principal);
3477
+ }
3049
3478
  /**
3050
3479
  * Spawn a new entity of the given type with durable streams.
3051
3480
  */
@@ -3075,7 +3504,6 @@ var EntityManager = class {
3075
3504
  const writeToken = (0, node_crypto.randomUUID)();
3076
3505
  const entityURL = typeName === `principal` ? principalUrl(instanceId) : `/${typeName}/${instanceId}`;
3077
3506
  const mainPath = `${entityURL}/main`;
3078
- const errorPath = `${entityURL}/error`;
3079
3507
  const subscriptionId = `${typeName}-handler`;
3080
3508
  const spawnT0 = performance.now();
3081
3509
  const existingByURL = await this.registry.getEntity(entityURL);
@@ -3092,10 +3520,7 @@ var EntityManager = class {
3092
3520
  type: typeName,
3093
3521
  status: `idle`,
3094
3522
  url: entityURL,
3095
- streams: {
3096
- main: mainPath,
3097
- error: errorPath
3098
- },
3523
+ streams: { main: mainPath },
3099
3524
  subscription_id: subscriptionId,
3100
3525
  dispatch_policy: dispatchPolicy,
3101
3526
  write_token: writeToken,
@@ -3148,55 +3573,43 @@ var EntityManager = class {
3148
3573
  const queueEnterT0 = performance.now();
3149
3574
  const queueWaiting = this.spawnPersistQueue.length();
3150
3575
  const queueRunning = this.spawnPersistQueue.running();
3151
- const [mainStreamResult, errorStreamResult, entityResult] = await this.spawnPersistQueue.push(async () => {
3576
+ const [mainStreamResult, entityResult] = await this.spawnPersistQueue.push(async () => {
3152
3577
  let entityTxid;
3153
3578
  try {
3154
3579
  entityTxid = await withSpan(`db.createEntity`, () => this.registry.createEntity(entityData));
3155
3580
  } catch (err) {
3156
- return [
3157
- {
3158
- status: `fulfilled`,
3159
- value: void 0
3160
- },
3161
- {
3162
- status: `fulfilled`,
3163
- value: void 0
3164
- },
3165
- {
3166
- status: `rejected`,
3167
- reason: err
3168
- }
3169
- ];
3581
+ return [{
3582
+ status: `fulfilled`,
3583
+ value: void 0
3584
+ }, {
3585
+ status: `rejected`,
3586
+ reason: err
3587
+ }];
3170
3588
  }
3171
- const [mainStreamResult$1, errorStreamResult$1] = await Promise.allSettled([this.streamClient.create(mainPath, {
3589
+ const [mainStreamResult$1] = await Promise.allSettled([this.streamClient.create(mainPath, {
3172
3590
  contentType,
3173
3591
  body: initialBody
3174
- }), this.streamClient.create(errorPath, { contentType })]);
3175
- return [
3176
- mainStreamResult$1,
3177
- errorStreamResult$1,
3178
- {
3179
- status: `fulfilled`,
3180
- value: entityTxid
3181
- }
3182
- ];
3592
+ })]);
3593
+ return [mainStreamResult$1, {
3594
+ status: `fulfilled`,
3595
+ value: entityTxid
3596
+ }];
3183
3597
  });
3184
3598
  const parallelMs = +(performance.now() - queueEnterT0).toFixed(2);
3185
- if (mainStreamResult.status === `rejected` || errorStreamResult.status === `rejected` || entityResult.status === `rejected`) {
3599
+ if (mainStreamResult.status === `rejected` || entityResult.status === `rejected`) {
3186
3600
  const entityReason = entityResult.status === `rejected` ? entityResult.reason : null;
3187
- const streamReason = mainStreamResult.status === `rejected` ? mainStreamResult.reason : errorStreamResult.status === `rejected` ? errorStreamResult.reason : null;
3601
+ const streamReason = mainStreamResult.status === `rejected` ? mainStreamResult.reason : null;
3188
3602
  const isDuplicate = entityReason instanceof EntityAlreadyExistsError;
3189
3603
  const isStreamConflict = !!streamReason && typeof streamReason === `object` && (`status` in streamReason && streamReason.status === 409 || `code` in streamReason && streamReason.code === `CONFLICT_SEQ`);
3190
3604
  const rollbacks = [];
3191
3605
  if (!isDuplicate && !isStreamConflict) {
3192
3606
  if (mainStreamResult.status === `fulfilled`) rollbacks.push(this.streamClient.delete(mainPath));
3193
- if (errorStreamResult.status === `fulfilled`) rollbacks.push(this.streamClient.delete(errorPath));
3194
3607
  if (entityResult.status === `fulfilled`) rollbacks.push(this.registry.deleteEntity(entityURL));
3195
3608
  if (req.wake) rollbacks.push(this.wakeRegistry.unregisterBySubscriberAndSource(req.wake.subscriberUrl, entityURL, this.tenantId));
3196
3609
  await Promise.allSettled(rollbacks);
3197
3610
  }
3198
3611
  if (isDuplicate || isStreamConflict) throw new ElectricAgentsError(ErrCodeDuplicateURL, `Entity already exists at URL "${entityURL}"`, 409);
3199
- const failure = mainStreamResult.status === `rejected` ? mainStreamResult.reason : errorStreamResult.status === `rejected` ? errorStreamResult.reason : entityResult.reason;
3612
+ const failure = mainStreamResult.status === `rejected` ? mainStreamResult.reason : entityResult.reason;
3200
3613
  if (failure instanceof Error) throw failure;
3201
3614
  throw new ElectricAgentsError(`SPAWN_FAILED`, `Spawn failed: ${String(failure)}`, 500);
3202
3615
  }
@@ -3281,7 +3694,7 @@ var EntityManager = class {
3281
3694
  });
3282
3695
  const sharedStateIdMap = await this.buildForkSharedStateIdMap(snapshot.sharedStateIds, suffix);
3283
3696
  const stringMap = this.buildForkStringMap(entityUrlMap, sharedStateIdMap);
3284
- const entityPlans = this.buildForkEntityPlans(effectiveSubtree, entityUrlMap, stringMap);
3697
+ const entityPlans = this.buildForkEntityPlans(effectiveSubtree, entityUrlMap, stringMap, opts.createdBy);
3285
3698
  this.addForkLocks(this.forkWriteLockedEntities, effectiveSubtree.map((entity) => entity.url), writeEntityLocks);
3286
3699
  this.addForkLocks(this.forkWriteLockedStreams, [...snapshot.sharedStateIds].map((id) => (0, __electric_ax_agents_runtime.getSharedStateStreamPath)(id)), writeStreamLocks);
3287
3700
  const createdStreams = [];
@@ -3292,8 +3705,6 @@ var EntityManager = class {
3292
3705
  const isRoot = plan.source.url === rootUrl;
3293
3706
  await this.streamClient.fork(plan.fork.streams.main, plan.source.streams.main, isRoot && opts.forkPointer ? { forkPointer: opts.forkPointer } : void 0);
3294
3707
  createdStreams.push(plan.fork.streams.main);
3295
- await this.streamClient.fork(plan.fork.streams.error, plan.source.streams.error);
3296
- createdStreams.push(plan.fork.streams.error);
3297
3708
  }
3298
3709
  for (const [sourceId, forkId] of sharedStateIdMap) {
3299
3710
  const sourcePath = (0, __electric_ax_agents_runtime.getSharedStateStreamPath)(sourceId);
@@ -3627,7 +4038,6 @@ var EntityManager = class {
3627
4038
  for (const [sourceUrl, forkUrl] of entityUrlMap) {
3628
4039
  stringMap.set(sourceUrl, forkUrl);
3629
4040
  stringMap.set(`${sourceUrl}/main`, `${forkUrl}/main`);
3630
- stringMap.set(`${sourceUrl}/error`, `${forkUrl}/error`);
3631
4041
  }
3632
4042
  for (const [sourceId, forkId] of sharedStateIdMap) {
3633
4043
  stringMap.set(sourceId, forkId);
@@ -3635,7 +4045,7 @@ var EntityManager = class {
3635
4045
  }
3636
4046
  return stringMap;
3637
4047
  }
3638
- buildForkEntityPlans(entitiesToFork, entityUrlMap, stringMap) {
4048
+ buildForkEntityPlans(entitiesToFork, entityUrlMap, stringMap, createdBy) {
3639
4049
  const now = Date.now();
3640
4050
  return entitiesToFork.map((source) => {
3641
4051
  const forkUrl = entityUrlMap.get(source.url);
@@ -3648,14 +4058,12 @@ var EntityManager = class {
3648
4058
  url: forkUrl,
3649
4059
  type,
3650
4060
  status: `idle`,
3651
- streams: {
3652
- main: `${forkUrl}/main`,
3653
- error: `${forkUrl}/error`
3654
- },
4061
+ streams: { main: `${forkUrl}/main` },
3655
4062
  subscription_id: `${type}-handler`,
3656
4063
  write_token: (0, node_crypto.randomUUID)(),
3657
4064
  spawn_args: spawnArgs,
3658
4065
  parent,
4066
+ created_by: createdBy ?? source.created_by,
3659
4067
  created_at: now,
3660
4068
  updated_at: now
3661
4069
  };
@@ -3889,7 +4297,7 @@ var EntityManager = class {
3889
4297
  }
3890
4298
  async materializeForkManifestSideEffects(entityUrl, manifests) {
3891
4299
  for (const [manifestKey, manifest] of manifests) {
3892
- await this.syncEntitiesManifestSource(entityUrl, manifestKey, `upsert`, manifest);
4300
+ await this.syncManifestLinks(entityUrl, manifestKey, `upsert`, manifest);
3893
4301
  const wake = buildManifestWakeRegistration(entityUrl, manifest, manifestKey);
3894
4302
  if (wake) await this.wakeRegistry.register({
3895
4303
  ...wake,
@@ -3919,6 +4327,7 @@ var EntityManager = class {
3919
4327
  await this.scheduler.syncManifestDelayedSend(ownerEntityUrl, manifestKey, {
3920
4328
  entityUrl: targetUrl,
3921
4329
  from: senderUrl,
4330
+ from_agent: senderUrl,
3922
4331
  payload: manifest.payload,
3923
4332
  key: `scheduled-${producerId}`,
3924
4333
  type: typeof manifest.messageType === `string` ? manifest.messageType : void 0,
@@ -3958,12 +4367,14 @@ var EntityManager = class {
3958
4367
  const now = new Date().toISOString();
3959
4368
  const key = req.key ?? `msg-in-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
3960
4369
  const value = {
3961
- from: req.from,
4370
+ from: req.from_principal ?? req.from,
3962
4371
  payload: req.payload,
3963
4372
  timestamp: now,
3964
4373
  mode: req.mode ?? `immediate`,
3965
4374
  status: req.mode === `queued` || req.mode === `paused` ? `pending` : `processed`
3966
4375
  };
4376
+ if (req.from_principal) value.from_principal = req.from_principal;
4377
+ if (req.from_agent) value.from_agent = req.from_agent;
3967
4378
  if (req.type) value.message_type = req.type;
3968
4379
  if (req.position) value.position = req.position;
3969
4380
  else if (value.mode === `queued` || value.mode === `paused`) value.position = createInitialQueuePosition(new Date(now));
@@ -4135,9 +4546,9 @@ var EntityManager = class {
4135
4546
  if (result.changed && this.entityBridgeManager) await this.entityBridgeManager.onEntityChanged(entityUrl);
4136
4547
  return updated;
4137
4548
  }
4138
- async ensureEntitiesMembershipStream(tags) {
4549
+ async ensureEntitiesMembershipStream(tags, principal) {
4139
4550
  if (!this.entityBridgeManager) throw new Error(`Entity bridge manager not configured`);
4140
- return this.entityBridgeManager.register(this.validateTags(tags));
4551
+ return this.entityBridgeManager.register(this.validateTags(tags), principal.url, principal.kind);
4141
4552
  }
4142
4553
  async writeManifestEntry(entityUrl, key, operation, value, opts) {
4143
4554
  const entity = await this.registry.getEntity(entityUrl);
@@ -4155,11 +4566,11 @@ var EntityManager = class {
4155
4566
  const encoded = this.encodeChangeEvent(event);
4156
4567
  if (opts?.producerId) {
4157
4568
  await this.streamClient.appendIdempotent(entity.streams.main, encoded, { producerId: opts.producerId });
4158
- await this.syncEntitiesManifestSource(entityUrl, key, operation, value);
4569
+ await this.syncManifestLinks(entityUrl, key, operation, value);
4159
4570
  return;
4160
4571
  }
4161
4572
  await this.streamClient.append(entity.streams.main, encoded);
4162
- await this.syncEntitiesManifestSource(entityUrl, key, operation, value);
4573
+ await this.syncManifestLinks(entityUrl, key, operation, value);
4163
4574
  }
4164
4575
  async upsertCronSchedule(entityUrl, req) {
4165
4576
  if (req.payload === void 0) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Missing required field: payload`, 400);
@@ -4308,6 +4719,8 @@ var EntityManager = class {
4308
4719
  await this.scheduler.enqueueDelayedSend({
4309
4720
  entityUrl,
4310
4721
  from: req.from,
4722
+ from_principal: req.from_principal,
4723
+ from_agent: req.from_agent,
4311
4724
  payload: req.payload,
4312
4725
  key: req.key,
4313
4726
  type: req.type,
@@ -4350,14 +4763,23 @@ var EntityManager = class {
4350
4763
  await this.streamClient.appendIdempotent(subscriber.streams.main, this.encodeChangeEvent(wakeEvent), { producerId: `wake-reg-${result.registrationDbId}-${result.sourceEventKey}` });
4351
4764
  });
4352
4765
  }
4353
- async syncEntitiesManifestSource(entityUrl, manifestKey, operation, value) {
4766
+ async syncManifestLinks(entityUrl, manifestKey, operation, value) {
4354
4767
  const sourceRef = operation === `delete` ? void 0 : this.extractEntitiesSourceRef(value);
4355
4768
  await this.registry.replaceEntityManifestSource(entityUrl, manifestKey, sourceRef);
4769
+ const sharedStateId = operation === `delete` ? void 0 : this.extractSharedStateId(value);
4770
+ await this.registry.replaceSharedStateLink(entityUrl, manifestKey, sharedStateId);
4356
4771
  }
4357
4772
  extractEntitiesSourceRef(manifest) {
4358
4773
  if (manifest?.kind === `source` && manifest.sourceType === `entities` && typeof manifest.sourceRef === `string`) return manifest.sourceRef;
4359
4774
  return void 0;
4360
4775
  }
4776
+ extractSharedStateId(manifest) {
4777
+ if (manifest?.kind === `shared-state` && typeof manifest.id === `string`) return manifest.id;
4778
+ if (manifest?.kind !== `source` || manifest.sourceType !== `db`) return void 0;
4779
+ if (typeof manifest.sourceRef === `string`) return manifest.sourceRef;
4780
+ const config = isRecord(manifest.config) ? manifest.config : void 0;
4781
+ return typeof config?.id === `string` ? config.id : void 0;
4782
+ }
4361
4783
  /**
4362
4784
  * Read a child entity's stream and extract concatenated text deltas
4363
4785
  * for a specific run, plus any error messages for that run.
@@ -4521,14 +4943,7 @@ var EntityManager = class {
4521
4943
  await this.streamClient.append(entity.streams.main, signalData);
4522
4944
  return;
4523
4945
  }
4524
- const errorCloseEvent = {
4525
- type: `signal`,
4526
- key: signalEvent.key,
4527
- value: signalEvent.value,
4528
- headers: signalEvent.headers
4529
- };
4530
- const errorSignalData = this.encodeChangeEvent(errorCloseEvent);
4531
- for (const [streamPath, data] of [[entity.streams.main, signalData], [entity.streams.error, errorSignalData]]) try {
4946
+ for (const [streamPath, data] of [[entity.streams.main, signalData]]) try {
4532
4947
  await this.streamClient.append(streamPath, data, { close: true });
4533
4948
  } catch (err) {
4534
4949
  const message = err instanceof Error ? err.message : String(err);
@@ -5511,6 +5926,8 @@ var ElectricAgentsTenantRuntime = class {
5511
5926
  try {
5512
5927
  await this.manager.send(payload.entityUrl, {
5513
5928
  from: payload.from,
5929
+ from_principal: payload.from_principal,
5930
+ from_agent: payload.from_agent,
5514
5931
  payload: payload.payload,
5515
5932
  key: payload.key ?? `scheduled-task-${taskId}`,
5516
5933
  type: payload.type
@@ -5583,6 +6000,7 @@ var ElectricAgentsTenantRuntime = class {
5583
6000
  await this.scheduler.syncManifestDelayedSend(ownerEntityUrl, manifestKey, {
5584
6001
  entityUrl: targetUrl,
5585
6002
  from: senderUrl,
6003
+ from_agent: senderUrl,
5586
6004
  payload: value.payload,
5587
6005
  key: `scheduled-${producerId}`,
5588
6006
  type: typeof value.messageType === `string` ? value.messageType : void 0,
@@ -5607,11 +6025,20 @@ var ElectricAgentsTenantRuntime = class {
5607
6025
  async applyManifestEntitySource(ownerEntityUrl, manifestKey, operation, value) {
5608
6026
  const sourceRef = operation === `delete` ? void 0 : this.extractEntitiesSourceRef(value);
5609
6027
  await this.manager.registry.replaceEntityManifestSource(ownerEntityUrl, manifestKey, sourceRef);
6028
+ const sharedStateId = operation === `delete` ? void 0 : this.extractSharedStateId(value);
6029
+ await this.manager.registry.replaceSharedStateLink(ownerEntityUrl, manifestKey, sharedStateId);
5610
6030
  }
5611
6031
  extractEntitiesSourceRef(manifest) {
5612
6032
  if (manifest?.kind === `source` && manifest.sourceType === `entities` && typeof manifest.sourceRef === `string`) return manifest.sourceRef;
5613
6033
  return void 0;
5614
6034
  }
6035
+ extractSharedStateId(manifest) {
6036
+ if (manifest?.kind === `shared-state` && typeof manifest.id === `string`) return manifest.id;
6037
+ if (manifest?.kind !== `source` || manifest.sourceType !== `db`) return void 0;
6038
+ if (typeof manifest.sourceRef === `string`) return manifest.sourceRef;
6039
+ const config = typeof manifest.config === `object` && manifest.config !== null && !Array.isArray(manifest.config) ? manifest.config : void 0;
6040
+ return typeof config?.id === `string` ? config.id : void 0;
6041
+ }
5615
6042
  async maybeMarkEntityIdleAfterRunFinished(entityUrl) {
5616
6043
  const primaryStream = `${entityUrl}/main`;
5617
6044
  const callbacks = await this.db.select().from(consumerCallbacks).where((0, drizzle_orm.and)((0, drizzle_orm.eq)(consumerCallbacks.tenantId, this.serviceId), (0, drizzle_orm.eq)(consumerCallbacks.primaryStream, primaryStream))).limit(1);
@@ -6292,6 +6719,8 @@ var WakeRegistry = class {
6292
6719
  if (eventType === `inbox`) {
6293
6720
  const value = event.value;
6294
6721
  if (typeof value?.from === `string`) change.from = value.from;
6722
+ if (typeof value?.from_principal === `string`) change.from_principal = value.from_principal;
6723
+ if (typeof value?.from_agent === `string`) change.from_agent = value.from_agent;
6295
6724
  if (`payload` in (value ?? {})) change.payload = value?.payload;
6296
6725
  if (typeof value?.timestamp === `string`) change.timestamp = value.timestamp;
6297
6726
  if (typeof value?.message_type === `string`) change.message_type = value.message_type;
@@ -6703,29 +7132,136 @@ function buildElectricProxyTarget(options) {
6703
7132
  if (options.electricSecret) target.searchParams.set(`secret`, options.electricSecret);
6704
7133
  const table = options.incomingUrl.searchParams.get(`table`);
6705
7134
  if (table === `entities`) {
6706
- 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"`);
6707
- applyTenantShapeWhere(target, options.tenantId);
7135
+ 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"`);
7136
+ applyShapeWhere(target, buildReadableEntitiesWhere({
7137
+ tenantId: options.tenantId,
7138
+ principalUrl: options.principalUrl ?? ``,
7139
+ principalKind: options.principalKind ?? ``,
7140
+ permissionBypass: options.permissionBypass
7141
+ }));
6708
7142
  } else if (table === `entity_types`) {
6709
7143
  target.searchParams.set(`columns`, `"tenant_id","name","description","creation_schema","inbox_schemas","state_schemas","serve_endpoint","default_dispatch_policy","revision","created_at","updated_at"`);
6710
- applyTenantShapeWhere(target, options.tenantId);
7144
+ applyShapeWhere(target, buildSpawnableEntityTypesWhere({
7145
+ tenantId: options.tenantId,
7146
+ principalUrl: options.principalUrl ?? ``,
7147
+ principalKind: options.principalKind ?? ``,
7148
+ permissionBypass: options.permissionBypass
7149
+ }));
6711
7150
  } else if (table === `runners`) {
6712
7151
  target.searchParams.set(`columns`, `"tenant_id","id","owner_principal","label","kind","admin_status","wake_stream","sandbox_profiles","created_at","updated_at"`);
6713
7152
  applyTenantShapeWhere(target, options.tenantId, [`owner_principal = ${sqlStringLiteral(options.principalUrl ?? ``)}`]);
7153
+ } else if (table === `users`) {
7154
+ target.searchParams.set(`columns`, `"tenant_id","id","display_name","email","avatar_url","created_at","updated_at"`);
7155
+ applyTenantShapeWhere(target, options.tenantId);
7156
+ } else if (table === `entity_effective_permissions`) {
7157
+ target.searchParams.set(`columns`, `"tenant_id","id","entity_url","source_entity_url","source_grant_id","permission","subject_kind","subject_value","expires_at","created_at"`);
7158
+ applyShapeWhere(target, buildCurrentPrincipalEntityEffectivePermissionsWhere({
7159
+ tenantId: options.tenantId,
7160
+ principalUrl: options.principalUrl ?? ``,
7161
+ principalKind: options.principalKind ?? ``,
7162
+ permissionBypass: options.permissionBypass
7163
+ }));
6714
7164
  } else if (table === `runner_runtime_diagnostics`) {
6715
7165
  target.searchParams.set(`columns`, `"tenant_id","runner_id","owner_principal","wake_stream_offset","last_seen_at","liveness_lease_expires_at","diagnostics","updated_at"`);
6716
7166
  applyTenantShapeWhere(target, options.tenantId, [`owner_principal = ${sqlStringLiteral(options.principalUrl ?? ``)}`]);
6717
7167
  } else if (table === `entity_dispatch_state`) {
6718
7168
  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"`);
6719
- applyTenantShapeWhere(target, options.tenantId);
7169
+ applyShapeWhere(target, buildReadableEntityUrlWhere({
7170
+ tenantId: options.tenantId,
7171
+ principalUrl: options.principalUrl ?? ``,
7172
+ principalKind: options.principalKind ?? ``,
7173
+ permissionBypass: options.permissionBypass
7174
+ }));
6720
7175
  } else if (table === `wake_notifications`) {
6721
7176
  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"`);
6722
- applyTenantShapeWhere(target, options.tenantId);
7177
+ applyShapeWhere(target, buildReadableEntityUrlWhere({
7178
+ tenantId: options.tenantId,
7179
+ principalUrl: options.principalUrl ?? ``,
7180
+ principalKind: options.principalKind ?? ``,
7181
+ permissionBypass: options.permissionBypass
7182
+ }));
6723
7183
  } else if (table === `consumer_claims`) {
6724
7184
  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"`);
6725
- applyTenantShapeWhere(target, options.tenantId);
7185
+ applyShapeWhere(target, buildReadableEntityUrlWhere({
7186
+ tenantId: options.tenantId,
7187
+ principalUrl: options.principalUrl ?? ``,
7188
+ principalKind: options.principalKind ?? ``,
7189
+ permissionBypass: options.permissionBypass
7190
+ }));
6726
7191
  }
6727
7192
  return target;
6728
7193
  }
7194
+ function buildReadableEntitiesWhere(options) {
7195
+ const tenant = sqlStringLiteral(options.tenantId);
7196
+ if (options.permissionBypass) return `tenant_id = ${tenant}`;
7197
+ const principalUrl$1 = sqlStringLiteral(options.principalUrl);
7198
+ const principalKind = sqlStringLiteral(options.principalKind);
7199
+ return [
7200
+ `tenant_id = ${tenant}`,
7201
+ `AND (`,
7202
+ ` created_by = ${principalUrl$1}`,
7203
+ ` OR url IN (`,
7204
+ ` SELECT entity_url`,
7205
+ ` FROM entity_effective_permissions`,
7206
+ ` WHERE tenant_id = ${tenant}`,
7207
+ ` AND permission IN ('read', 'manage')`,
7208
+ ` AND (`,
7209
+ ` (subject_kind = 'principal' AND subject_value = ${principalUrl$1})`,
7210
+ ` OR (subject_kind = 'principal_kind' AND subject_value = ${principalKind})`,
7211
+ ` )`,
7212
+ ` )`,
7213
+ `)`
7214
+ ].join(`\n`);
7215
+ }
7216
+ function buildReadableEntityUrlWhere(options) {
7217
+ const tenant = sqlStringLiteral(options.tenantId);
7218
+ if (options.permissionBypass) return `tenant_id = ${tenant}`;
7219
+ return [
7220
+ `tenant_id = ${tenant}`,
7221
+ `AND entity_url IN (`,
7222
+ ` SELECT url`,
7223
+ ` FROM entities`,
7224
+ ` WHERE ${indentWhere(buildReadableEntitiesWhere(options), ` `).trimStart()}`,
7225
+ `)`
7226
+ ].join(`\n`);
7227
+ }
7228
+ function buildCurrentPrincipalEntityEffectivePermissionsWhere(options) {
7229
+ const tenant = sqlStringLiteral(options.tenantId);
7230
+ if (options.permissionBypass) return `tenant_id = ${tenant}`;
7231
+ const principalUrl$1 = sqlStringLiteral(options.principalUrl);
7232
+ const principalKind = sqlStringLiteral(options.principalKind);
7233
+ return [
7234
+ `tenant_id = ${tenant}`,
7235
+ `AND (`,
7236
+ ` (subject_kind = 'principal' AND subject_value = ${principalUrl$1})`,
7237
+ ` OR (subject_kind = 'principal_kind' AND subject_value = ${principalKind})`,
7238
+ `)`,
7239
+ `AND entity_url IN (`,
7240
+ ` SELECT url`,
7241
+ ` FROM entities`,
7242
+ ` WHERE ${buildReadableEntitiesWhere(options)}`,
7243
+ `)`
7244
+ ].join(`\n`);
7245
+ }
7246
+ function buildSpawnableEntityTypesWhere(options) {
7247
+ const tenant = sqlStringLiteral(options.tenantId);
7248
+ if (options.permissionBypass) return `tenant_id = ${tenant}`;
7249
+ const principalUrl$1 = sqlStringLiteral(options.principalUrl);
7250
+ const principalKind = sqlStringLiteral(options.principalKind);
7251
+ return [
7252
+ `tenant_id = ${tenant}`,
7253
+ `AND name IN (`,
7254
+ ` SELECT entity_type`,
7255
+ ` FROM entity_type_permission_grants`,
7256
+ ` WHERE tenant_id = ${tenant}`,
7257
+ ` AND permission IN ('spawn', 'manage')`,
7258
+ ` AND (`,
7259
+ ` (subject_kind = 'principal' AND subject_value = ${principalUrl$1})`,
7260
+ ` OR (subject_kind = 'principal_kind' AND subject_value = ${principalKind})`,
7261
+ ` )`,
7262
+ `)`
7263
+ ].join(`\n`);
7264
+ }
6729
7265
  async function forwardFetchRequest(options) {
6730
7266
  const routingAdapter = resolveDurableStreamsRoutingAdapter(options.durableStreamsRouting, options.durableStreamsUrl);
6731
7267
  const routingInput = {
@@ -6760,13 +7296,170 @@ function decodeJsonObject(body) {
6760
7296
  return null;
6761
7297
  }
6762
7298
  function applyTenantShapeWhere(target, tenantId, extraConditions = []) {
6763
- const tenantWhere = [`tenant_id = ${sqlStringLiteral(tenantId)}`, ...extraConditions].join(` AND `);
7299
+ applyShapeWhere(target, [`tenant_id = ${sqlStringLiteral(tenantId)}`, ...extraConditions].join(` AND `));
7300
+ }
7301
+ function applyShapeWhere(target, enforcedWhere) {
6764
7302
  const existingWhere = target.searchParams.get(`where`);
6765
- target.searchParams.set(`where`, existingWhere ? `${tenantWhere} AND (${existingWhere})` : tenantWhere);
7303
+ target.searchParams.set(`where`, existingWhere ? `${enforcedWhere} AND (${existingWhere})` : enforcedWhere);
6766
7304
  }
6767
7305
  function sqlStringLiteral(value) {
6768
7306
  return `'${value.replace(/'/g, `''`)}'`;
6769
7307
  }
7308
+ function indentWhere(where, prefix) {
7309
+ return where.split(`\n`).map((line) => `${prefix}${line}`).join(`\n`);
7310
+ }
7311
+
7312
+ //#endregion
7313
+ //#region src/permissions.ts
7314
+ const authzDecisionCache = new WeakMap();
7315
+ function principalSubject(principal) {
7316
+ return {
7317
+ principalUrl: principal.url,
7318
+ principalKind: principal.kind
7319
+ };
7320
+ }
7321
+ function isPermissionBypassPrincipal(ctx) {
7322
+ return isBuiltInSystemPrincipalUrl(ctx.principal.url);
7323
+ }
7324
+ async function canAccessEntity(ctx, entity, permission, request) {
7325
+ if (isPermissionBypassPrincipal(ctx)) return true;
7326
+ await ctx.entityManager.registry.pruneExpiredPermissionGrants?.();
7327
+ const builtInAllowed = entity.created_by === ctx.principal.url || await ctx.entityManager.registry.hasEntityPermission(entity.url, permission, principalSubject(ctx.principal));
7328
+ return await applyAuthorizationHook(ctx, {
7329
+ verb: permission,
7330
+ resourceKey: `entity:${entity.url}`,
7331
+ resource: {
7332
+ kind: `entity`,
7333
+ entity
7334
+ },
7335
+ builtInAllowed,
7336
+ request
7337
+ });
7338
+ }
7339
+ async function canAccessEntityType(ctx, entityType, permission, request) {
7340
+ if (isPermissionBypassPrincipal(ctx)) return true;
7341
+ await ctx.entityManager.registry.pruneExpiredPermissionGrants?.();
7342
+ const builtInAllowed = await ctx.entityManager.registry.hasEntityTypePermission(entityType.name, permission, principalSubject(ctx.principal));
7343
+ return await applyAuthorizationHook(ctx, {
7344
+ verb: permission,
7345
+ resourceKey: `entity_type:${entityType.name}`,
7346
+ resource: {
7347
+ kind: `entity_type`,
7348
+ entityType
7349
+ },
7350
+ builtInAllowed,
7351
+ request
7352
+ });
7353
+ }
7354
+ async function canRegisterEntityType(ctx, input, request) {
7355
+ if (isPermissionBypassPrincipal(ctx)) return true;
7356
+ return await applyAuthorizationHook(ctx, {
7357
+ verb: `manage`,
7358
+ resourceKey: `entity_type_registration:${input.name}`,
7359
+ resource: {
7360
+ kind: `entity_type_registration`,
7361
+ entityTypeName: input.name
7362
+ },
7363
+ builtInAllowed: true,
7364
+ request
7365
+ });
7366
+ }
7367
+ async function canAccessSharedState(ctx, sharedStateId, permission, request, ownerEntityUrl) {
7368
+ if (isPermissionBypassPrincipal(ctx)) return true;
7369
+ await ctx.entityManager.registry.pruneExpiredPermissionGrants?.();
7370
+ const storedLinkedEntityUrls = await ctx.entityManager.registry.listSharedStateLinkedEntityUrls(sharedStateId);
7371
+ const bootstrapEntityUrls = storedLinkedEntityUrls.length === 0 && ownerEntityUrl ? [ownerEntityUrl] : [];
7372
+ const linkedEntityUrls = [...new Set([...storedLinkedEntityUrls, ...bootstrapEntityUrls])];
7373
+ for (const entityUrl of linkedEntityUrls) {
7374
+ const entity = await ctx.entityManager.registry.getEntity(entityUrl);
7375
+ if (!entity) continue;
7376
+ if (entity.created_by === ctx.principal.url || await ctx.entityManager.registry.hasEntityPermission(entity.url, permission, principalSubject(ctx.principal))) return await applyAuthorizationHook(ctx, {
7377
+ verb: permission,
7378
+ resourceKey: `shared_state:${sharedStateId}`,
7379
+ resource: {
7380
+ kind: `shared_state`,
7381
+ sharedStateId,
7382
+ linkedEntityUrls
7383
+ },
7384
+ builtInAllowed: true,
7385
+ request
7386
+ });
7387
+ }
7388
+ return await applyAuthorizationHook(ctx, {
7389
+ verb: permission,
7390
+ resourceKey: `shared_state:${sharedStateId}`,
7391
+ resource: {
7392
+ kind: `shared_state`,
7393
+ sharedStateId,
7394
+ linkedEntityUrls
7395
+ },
7396
+ builtInAllowed: false,
7397
+ request
7398
+ });
7399
+ }
7400
+ async function applyAuthorizationHook(ctx, input) {
7401
+ const hook = ctx.authorizeRequest;
7402
+ if (!hook) return input.builtInAllowed;
7403
+ const cacheKey = [
7404
+ ctx.service,
7405
+ ctx.principal.url,
7406
+ input.verb,
7407
+ input.resourceKey
7408
+ ].join(`|`);
7409
+ const cached = getCachedDecision(hook, cacheKey);
7410
+ if (cached) return cached.decision === `allow`;
7411
+ let decision;
7412
+ try {
7413
+ decision = await hook({
7414
+ tenant: ctx.service,
7415
+ principal: ctx.principal,
7416
+ verb: input.verb,
7417
+ resource: input.resource,
7418
+ request: input.request ? requestMetadata(input.request) : void 0,
7419
+ builtInAllowed: input.builtInAllowed
7420
+ });
7421
+ } catch (error) {
7422
+ serverLog.warn(`[agent-server] authorization hook failed:`, error);
7423
+ return false;
7424
+ }
7425
+ cacheDecision(hook, cacheKey, decision);
7426
+ return decision.decision === `allow`;
7427
+ }
7428
+ function getCachedDecision(hook, cacheKey) {
7429
+ const cache = authzDecisionCache.get(hook);
7430
+ const entry = cache?.get(cacheKey);
7431
+ if (!entry) return null;
7432
+ if (entry.expiresAt <= Date.now()) {
7433
+ cache?.delete(cacheKey);
7434
+ return null;
7435
+ }
7436
+ return { decision: entry.decision };
7437
+ }
7438
+ function cacheDecision(hook, cacheKey, decision) {
7439
+ if (!decision.expires_at) return;
7440
+ const expiresAt = Date.parse(decision.expires_at);
7441
+ if (!Number.isFinite(expiresAt) || expiresAt <= Date.now()) return;
7442
+ let cache = authzDecisionCache.get(hook);
7443
+ if (!cache) {
7444
+ cache = new Map();
7445
+ authzDecisionCache.set(hook, cache);
7446
+ }
7447
+ cache.set(cacheKey, {
7448
+ decision: decision.decision,
7449
+ expiresAt
7450
+ });
7451
+ }
7452
+ function requestMetadata(request) {
7453
+ const headers = {};
7454
+ request.headers.forEach((value, key) => {
7455
+ headers[key] = value;
7456
+ });
7457
+ return {
7458
+ method: request.method,
7459
+ url: request.url,
7460
+ headers
7461
+ };
7462
+ }
6770
7463
 
6771
7464
  //#endregion
6772
7465
  //#region src/webhook-signing.ts
@@ -6858,6 +7551,7 @@ const subscriptionControlActions = [
6858
7551
  `ack`,
6859
7552
  `release`
6860
7553
  ];
7554
+ const SHARED_STATE_OWNER_ENTITY_HEADER = `electric-owner-entity`;
6861
7555
  const durableStreamsRouter = (0, itty_router.Router)();
6862
7556
  durableStreamsRouter.put(`/__ds/subscriptions/:subscriptionId`, putSubscriptionBase);
6863
7557
  durableStreamsRouter.get(`/__ds/subscriptions/:subscriptionId`, getSubscriptionBase);
@@ -7075,6 +7769,8 @@ async function webhookJwks(_request, ctx) {
7075
7769
  });
7076
7770
  }
7077
7771
  async function streamAppend(request, ctx) {
7772
+ const auth = await authorizeDurableStreamAccess(request, ctx);
7773
+ if (auth) return auth;
7078
7774
  return await electricAgentsStreamAppendRouter.fetch(createStreamAppendRouteRequest(request), ctx.runtime, (req, body) => forwardFetchRequest({
7079
7775
  request: {
7080
7776
  method: req.method,
@@ -7091,8 +7787,9 @@ async function streamAppend(request, ctx) {
7091
7787
  }));
7092
7788
  }
7093
7789
  async function proxyPassThrough(request, ctx) {
7790
+ const auth = await authorizeDurableStreamAccess(request, ctx);
7791
+ if (auth) return auth;
7094
7792
  const streamPath = new URL(request.url).pathname;
7095
- if (ctx.entityManager?.isAttachmentStreamPath(streamPath)) return new Response(null, { status: 404 });
7096
7793
  const upstream = await forwardToDurableStreams(ctx, request);
7097
7794
  const method = request.method.toUpperCase();
7098
7795
  const endTrackedRead = method === `GET` ? await ctx.entityBridgeManager.beginClientRead(streamPath) : null;
@@ -7103,6 +7800,51 @@ async function proxyPassThrough(request, ctx) {
7103
7800
  await endTrackedRead?.();
7104
7801
  }
7105
7802
  }
7803
+ async function authorizeDurableStreamAccess(request, ctx) {
7804
+ const method = request.method.toUpperCase();
7805
+ const streamPath = new URL(request.url).pathname;
7806
+ if (method === `GET` || method === `HEAD`) {
7807
+ const registry = ctx.entityManager?.registry;
7808
+ const entity = registry?.getEntityByStream ? await registry.getEntityByStream(streamPath) : null;
7809
+ if (entity) {
7810
+ if (await canAccessEntity(ctx, entity, `read`, request)) return void 0;
7811
+ return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to read ${entity.url}`);
7812
+ }
7813
+ const attachmentEntityUrl = entityUrlFromAttachmentStreamPath(streamPath);
7814
+ if (attachmentEntityUrl) {
7815
+ const attachmentEntity = registry?.getEntity ? await registry.getEntity(attachmentEntityUrl) : null;
7816
+ if (!attachmentEntity) return apiError(404, ErrCodeNotFound, `Entity not found`);
7817
+ if (await canAccessEntity(ctx, attachmentEntity, `read`, request)) return void 0;
7818
+ return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to read ${attachmentEntity.url}`);
7819
+ }
7820
+ }
7821
+ const sharedStateId = sharedStateIdFromPath(streamPath);
7822
+ if (!sharedStateId) return void 0;
7823
+ if (method === `GET` || method === `HEAD`) {
7824
+ if (await canAccessSharedState(ctx, sharedStateId, `read`, request)) return void 0;
7825
+ return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to read shared state`);
7826
+ }
7827
+ if (method === `PUT` || method === `POST`) {
7828
+ const ownerEntityUrl = request.headers.get(SHARED_STATE_OWNER_ENTITY_HEADER)?.trim() || void 0;
7829
+ if (await canAccessSharedState(ctx, sharedStateId, `write`, request, ownerEntityUrl)) return void 0;
7830
+ return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to write shared state`);
7831
+ }
7832
+ return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to access shared state`);
7833
+ }
7834
+ function entityUrlFromAttachmentStreamPath(path$2) {
7835
+ const match = path$2.match(/^\/([^/]+)\/([^/]+)\/attachments\/[^/]+$/);
7836
+ if (!match) return null;
7837
+ return `/${match[1]}/${match[2]}`;
7838
+ }
7839
+ function sharedStateIdFromPath(path$2) {
7840
+ const match = path$2.match(/^\/_electric\/shared-state\/([^/]+)$/);
7841
+ if (!match) return null;
7842
+ try {
7843
+ return decodeURIComponent(match[1]);
7844
+ } catch {
7845
+ return match[1];
7846
+ }
7847
+ }
7106
7848
 
7107
7849
  //#endregion
7108
7850
  //#region src/routing/electric-proxy-router.ts
@@ -7110,12 +7852,15 @@ const electricProxyRouter = (0, itty_router.Router)({ base: `/_electric/electric
7110
7852
  electricProxyRouter.get(`/*`, proxyElectric);
7111
7853
  async function proxyElectric(request, ctx) {
7112
7854
  if (!ctx.electricUrl) return apiError(500, `ELECTRIC_PROXY_FAILED`, `Electric URL not configured`);
7855
+ await ctx.entityManager.registry.pruneExpiredPermissionGrants?.();
7113
7856
  const target = buildElectricProxyTarget({
7114
7857
  incomingUrl: new URL(request.url),
7115
7858
  electricUrl: ctx.electricUrl,
7116
7859
  electricSecret: ctx.electricSecret,
7117
7860
  tenantId: ctx.service,
7118
- principalUrl: ctx.principal.url
7861
+ principalUrl: ctx.principal.url,
7862
+ principalKind: ctx.principal.kind,
7863
+ permissionBypass: isPermissionBypassPrincipal(ctx)
7119
7864
  });
7120
7865
  const headers = new Headers(request.headers);
7121
7866
  headers.delete(`host`);
@@ -7174,6 +7919,27 @@ const wakeConditionSchema = __sinclair_typebox.Type.Union([__sinclair_typebox.Ty
7174
7919
  __sinclair_typebox.Type.Literal(`delete`)
7175
7920
  ])))
7176
7921
  })]);
7922
+ const permissionSubjectSchema = __sinclair_typebox.Type.Object({
7923
+ subject_kind: __sinclair_typebox.Type.Union([__sinclair_typebox.Type.Literal(`principal`), __sinclair_typebox.Type.Literal(`principal_kind`)]),
7924
+ subject_value: __sinclair_typebox.Type.String()
7925
+ }, { additionalProperties: false });
7926
+ const entityPermissionSchema = __sinclair_typebox.Type.Union([
7927
+ __sinclair_typebox.Type.Literal(`read`),
7928
+ __sinclair_typebox.Type.Literal(`write`),
7929
+ __sinclair_typebox.Type.Literal(`delete`),
7930
+ __sinclair_typebox.Type.Literal(`signal`),
7931
+ __sinclair_typebox.Type.Literal(`fork`),
7932
+ __sinclair_typebox.Type.Literal(`schedule`),
7933
+ __sinclair_typebox.Type.Literal(`spawn`),
7934
+ __sinclair_typebox.Type.Literal(`manage`)
7935
+ ]);
7936
+ const entityPermissionGrantInputSchema = __sinclair_typebox.Type.Object({
7937
+ ...permissionSubjectSchema.properties,
7938
+ permission: entityPermissionSchema,
7939
+ propagation: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Union([__sinclair_typebox.Type.Literal(`self`), __sinclair_typebox.Type.Literal(`descendants`)])),
7940
+ copy_to_children: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Boolean()),
7941
+ expires_at: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String())
7942
+ }, { additionalProperties: false });
7177
7943
  const spawnBodySchema = __sinclair_typebox.Type.Object({
7178
7944
  args: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Record(__sinclair_typebox.Type.String(), __sinclair_typebox.Type.Unknown())),
7179
7945
  tags: __sinclair_typebox.Type.Optional(stringRecordSchema$1),
@@ -7181,6 +7947,7 @@ const spawnBodySchema = __sinclair_typebox.Type.Object({
7181
7947
  dispatch_policy: __sinclair_typebox.Type.Optional(dispatchPolicySchema),
7182
7948
  sandbox: __sinclair_typebox.Type.Optional(sandboxChoiceSchema),
7183
7949
  initialMessage: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Unknown()),
7950
+ grants: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Array(entityPermissionGrantInputSchema)),
7184
7951
  wake: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Object({
7185
7952
  subscriberUrl: __sinclair_typebox.Type.String(),
7186
7953
  condition: wakeConditionSchema,
@@ -7202,8 +7969,22 @@ const sendBodySchema = __sinclair_typebox.Type.Object({
7202
7969
  ])),
7203
7970
  position: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
7204
7971
  afterMs: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Number()),
7205
- from: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String())
7972
+ from: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
7973
+ from_principal: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
7974
+ from_agent: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String())
7206
7975
  });
7976
+ function agentUrlForPrincipal(principal) {
7977
+ if (principal.kind === `agent`) return `/${principal.id}`;
7978
+ if (principal.key.startsWith(`entity:`)) return `/${principal.key.slice(`entity:`.length)}`;
7979
+ return null;
7980
+ }
7981
+ function agentUrlPath(value) {
7982
+ try {
7983
+ return new URL(value).pathname;
7984
+ } catch {
7985
+ return value;
7986
+ }
7987
+ }
7207
7988
  const inboxMessageBodySchema = __sinclair_typebox.Type.Object({
7208
7989
  payload: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Unknown()),
7209
7990
  position: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
@@ -7282,24 +8063,27 @@ const attachmentSubjectTypes = new Set([
7282
8063
  ]);
7283
8064
  const entitiesRouter = (0, itty_router.Router)({ base: `/_electric/entities` });
7284
8065
  entitiesRouter.get(`/`, listEntities);
7285
- entitiesRouter.put(`/:type/:instanceId`, withSpawnableEntityType, withSchema(spawnBodySchema), spawnEntity);
7286
- entitiesRouter.get(`/:type/:instanceId`, withExistingEntity, getEntity);
7287
- entitiesRouter.head(`/:type/:instanceId`, withExistingEntity, headEntity);
7288
- entitiesRouter.delete(`/:type/:instanceId`, withExistingEntity, killEntity);
7289
- entitiesRouter.post(`/:type/:instanceId/signal`, withExistingEntity, withSchema(signalBodySchema), signalEntity);
7290
- entitiesRouter.post(`/:type/:instanceId/send`, withExistingEntity, withSchema(sendBodySchema), sendEntity);
7291
- entitiesRouter.post(`/:type/:instanceId/attachments`, withExistingEntity, createAttachment);
7292
- entitiesRouter.get(`/:type/:instanceId/attachments/:attachmentId`, withExistingEntity, readAttachment);
7293
- entitiesRouter.delete(`/:type/:instanceId/attachments/:attachmentId`, withExistingEntity, deleteAttachment);
7294
- entitiesRouter.patch(`/:type/:instanceId/inbox/:messageKey`, withExistingEntity, withSchema(inboxMessageBodySchema), updateInboxMessage);
7295
- entitiesRouter.delete(`/:type/:instanceId/inbox/:messageKey`, withExistingEntity, deleteInboxMessage);
7296
- entitiesRouter.post(`/:type/:instanceId/fork`, withExistingEntity, withSchema(forkBodySchema), forkEntity);
7297
- entitiesRouter.post(`/:type/:instanceId/tags/:tagKey`, withExistingEntity, withSchema(setTagBodySchema), setTag);
7298
- entitiesRouter.delete(`/:type/:instanceId/tags/:tagKey`, withExistingEntity, deleteTag);
7299
- entitiesRouter.put(`/:type/:instanceId/schedules/:scheduleId`, withExistingEntity, withSchema(scheduleBodySchema), upsertSchedule);
7300
- entitiesRouter.delete(`/:type/:instanceId/schedules/:scheduleId`, withExistingEntity, deleteSchedule);
7301
- entitiesRouter.put(`/:type/:instanceId/event-source-subscriptions/:subscriptionId`, withExistingEntity, withSchema(eventSourceSubscriptionBodySchema), upsertEventSourceSubscription);
7302
- entitiesRouter.delete(`/:type/:instanceId/event-source-subscriptions/:subscriptionId`, withExistingEntity, deleteEventSourceSubscription);
8066
+ entitiesRouter.put(`/:type/:instanceId`, withSpawnableEntityType, withSchema(spawnBodySchema), withSpawnPermission, spawnEntity);
8067
+ entitiesRouter.get(`/:type/:instanceId`, withExistingEntity, withEntityPermission(`read`), getEntity);
8068
+ entitiesRouter.head(`/:type/:instanceId`, withExistingEntity, withEntityPermission(`read`), headEntity);
8069
+ entitiesRouter.delete(`/:type/:instanceId`, withExistingEntity, withEntityPermission(`delete`), killEntity);
8070
+ entitiesRouter.post(`/:type/:instanceId/signal`, withExistingEntity, withSchema(signalBodySchema), withEntityPermission(`signal`), signalEntity);
8071
+ entitiesRouter.post(`/:type/:instanceId/send`, withExistingEntity, withSchema(sendBodySchema), withEntityPermission(`write`), sendEntity);
8072
+ entitiesRouter.post(`/:type/:instanceId/attachments`, withExistingEntity, withEntityPermission(`write`), createAttachment);
8073
+ entitiesRouter.get(`/:type/:instanceId/attachments/:attachmentId`, withExistingEntity, withEntityPermission(`read`), readAttachment);
8074
+ entitiesRouter.delete(`/:type/:instanceId/attachments/:attachmentId`, withExistingEntity, withEntityPermission(`write`), deleteAttachment);
8075
+ entitiesRouter.patch(`/:type/:instanceId/inbox/:messageKey`, withExistingEntity, withSchema(inboxMessageBodySchema), withEntityPermission(`write`), updateInboxMessage);
8076
+ entitiesRouter.delete(`/:type/:instanceId/inbox/:messageKey`, withExistingEntity, withEntityPermission(`write`), deleteInboxMessage);
8077
+ entitiesRouter.post(`/:type/:instanceId/fork`, withExistingEntity, withSchema(forkBodySchema), withEntityPermission(`fork`), forkEntity);
8078
+ entitiesRouter.post(`/:type/:instanceId/tags/:tagKey`, withExistingEntity, withSchema(setTagBodySchema), withEntityPermission(`write`), setTag);
8079
+ entitiesRouter.delete(`/:type/:instanceId/tags/:tagKey`, withExistingEntity, withEntityPermission(`write`), deleteTag);
8080
+ entitiesRouter.put(`/:type/:instanceId/schedules/:scheduleId`, withExistingEntity, withSchema(scheduleBodySchema), withEntityPermission(`schedule`), upsertSchedule);
8081
+ entitiesRouter.delete(`/:type/:instanceId/schedules/:scheduleId`, withExistingEntity, withEntityPermission(`schedule`), deleteSchedule);
8082
+ entitiesRouter.put(`/:type/:instanceId/event-source-subscriptions/:subscriptionId`, withExistingEntity, withSchema(eventSourceSubscriptionBodySchema), withEntityPermission(`write`), upsertEventSourceSubscription);
8083
+ entitiesRouter.delete(`/:type/:instanceId/event-source-subscriptions/:subscriptionId`, withExistingEntity, withEntityPermission(`write`), deleteEventSourceSubscription);
8084
+ entitiesRouter.get(`/:type/:instanceId/grants`, withExistingEntity, withEntityPermission(`manage`), listEntityPermissionGrants);
8085
+ entitiesRouter.post(`/:type/:instanceId/grants`, withExistingEntity, withSchema(entityPermissionGrantInputSchema), withEntityPermission(`manage`), createEntityPermissionGrant);
8086
+ entitiesRouter.delete(`/:type/:instanceId/grants/:grantId`, withExistingEntity, withEntityPermission(`manage`), deleteEntityPermissionGrant);
7303
8087
  function entityUrlFromSegments(type, instanceId) {
7304
8088
  if (!type || !instanceId) return null;
7305
8089
  if (type.startsWith(`_`) || type.includes(`*`) || instanceId.includes(`*`)) return null;
@@ -7398,6 +8182,17 @@ function rejectPrincipalEntityMutation(request, action) {
7398
8182
  if (entity.type !== `principal`) return void 0;
7399
8183
  return apiError(400, ErrCodeInvalidRequest, `Principal entities are built in and cannot be ${action}`);
7400
8184
  }
8185
+ function parseExpiresAt$1(value) {
8186
+ if (value === void 0) return void 0;
8187
+ const expiresAt = new Date(value);
8188
+ if (Number.isNaN(expiresAt.getTime())) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Invalid expires_at timestamp`, 400);
8189
+ return expiresAt;
8190
+ }
8191
+ function parseGrantId$1(request) {
8192
+ const grantId = Number.parseInt(String(request.params.grantId), 10);
8193
+ if (!Number.isSafeInteger(grantId) || grantId <= 0) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Invalid grant id`, 400);
8194
+ return grantId;
8195
+ }
7401
8196
  async function withExistingEntity(request, ctx) {
7402
8197
  const entityUrl = entityUrlFromSegments(request.params.type, request.params.instanceId);
7403
8198
  if (!entityUrl) return void 0;
@@ -7428,17 +8223,76 @@ async function withSpawnableEntityType(request, ctx) {
7428
8223
  if (request.params.type === `principal`) return apiError(400, ErrCodeInvalidRequest, `Principal entities are built in and cannot be spawned directly`);
7429
8224
  const entityType = await ctx.entityManager.registry.getEntityType(request.params.type);
7430
8225
  if (!entityType) return apiError(404, ErrCodeUnknownEntityType, `Entity type "${request.params.type}" not found`);
8226
+ request.spawnRoute = { entityType };
7431
8227
  return void 0;
7432
8228
  }
8229
+ function withEntityPermission(permission) {
8230
+ return async (request, ctx) => {
8231
+ const { entity } = requireExistingEntityRoute(request);
8232
+ if (await canAccessEntity(ctx, entity, permission, request)) return void 0;
8233
+ return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to ${permission} ${entity.url}`);
8234
+ };
8235
+ }
8236
+ async function withSpawnPermission(request, ctx) {
8237
+ const parsed = routeBody(request);
8238
+ const entityType = request.spawnRoute?.entityType;
8239
+ if (!entityType) throw new Error(`spawnable entity type middleware did not run`);
8240
+ if (!await canAccessEntityType(ctx, entityType, `spawn`, request)) return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to spawn ${entityType.name}`);
8241
+ if (!parsed.parent) return void 0;
8242
+ const parent = await ctx.entityManager.registry.getEntity(parsed.parent);
8243
+ if (!parent) return apiError(404, ErrCodeNotFound, `Parent entity not found`);
8244
+ if (await canAccessEntity(ctx, parent, `spawn`, request)) return await validateParentedSpawnGrants(request, ctx, parent, parsed);
8245
+ return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to spawn children from ${parent.url}`);
8246
+ }
8247
+ async function validateParentedSpawnGrants(request, ctx, parent, parsed) {
8248
+ const needsParentManage = (parsed.grants ?? []).some(requiresParentManageForInitialGrant);
8249
+ if (!needsParentManage) return void 0;
8250
+ if (await canAccessEntity(ctx, parent, `manage`, request)) return void 0;
8251
+ return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to delegate broad grants from ${parent.url}`);
8252
+ }
8253
+ function requiresParentManageForInitialGrant(grant) {
8254
+ return grant.permission === `manage` || grant.subject_kind === `principal_kind` || grant.propagation === `descendants` || grant.copy_to_children === true;
8255
+ }
7433
8256
  async function listEntities({ query }, ctx) {
7434
8257
  const { entities: entities$1 } = await ctx.entityManager.registry.listEntities({
7435
8258
  type: firstQueryValue$1(query.type),
7436
8259
  status: firstQueryValue$1(query.status),
7437
8260
  parent: firstQueryValue$1(query.parent),
7438
- created_by: firstQueryValue$1(query.created_by)
8261
+ created_by: firstQueryValue$1(query.created_by),
8262
+ readableBy: {
8263
+ ...principalSubject(ctx.principal),
8264
+ bypass: isPermissionBypassPrincipal(ctx)
8265
+ }
7439
8266
  });
7440
8267
  return (0, itty_router.json)(entities$1.map((entity) => toPublicEntity(entity)));
7441
8268
  }
8269
+ async function listEntityPermissionGrants(request, ctx) {
8270
+ const { entityUrl } = requireExistingEntityRoute(request);
8271
+ const grants = await ctx.entityManager.registry.listEntityPermissionGrants(entityUrl);
8272
+ return (0, itty_router.json)({ grants });
8273
+ }
8274
+ async function createEntityPermissionGrant(request, ctx) {
8275
+ const { entityUrl } = requireExistingEntityRoute(request);
8276
+ const parsed = routeBody(request);
8277
+ const grant = await ctx.entityManager.registry.createEntityPermissionGrant({
8278
+ entityUrl,
8279
+ permission: parsed.permission,
8280
+ subjectKind: parsed.subject_kind,
8281
+ subjectValue: parsed.subject_value,
8282
+ propagation: parsed.propagation,
8283
+ copyToChildren: parsed.copy_to_children,
8284
+ expiresAt: parseExpiresAt$1(parsed.expires_at),
8285
+ createdBy: ctx.principal.url
8286
+ });
8287
+ await ctx.entityBridgeManager.onEntityChanged(entityUrl);
8288
+ return (0, itty_router.json)(grant, { status: 201 });
8289
+ }
8290
+ async function deleteEntityPermissionGrant(request, ctx) {
8291
+ const { entityUrl } = requireExistingEntityRoute(request);
8292
+ const deleted = await ctx.entityManager.registry.deleteEntityPermissionGrant(entityUrl, parseGrantId$1(request));
8293
+ if (deleted) await ctx.entityBridgeManager.onEntityChanged(entityUrl);
8294
+ return deleted ? (0, itty_router.status)(204) : apiError(404, ErrCodeNotFound, `Grant not found`);
8295
+ }
7442
8296
  async function upsertSchedule(request, ctx) {
7443
8297
  const principalMutationError = rejectPrincipalEntityMutation(request, `scheduled`);
7444
8298
  if (principalMutationError) return principalMutationError;
@@ -7544,6 +8398,7 @@ async function forkEntity(request, ctx) {
7544
8398
  const result = await ctx.entityManager.forkSubtree(entityUrl, {
7545
8399
  rootInstanceId: parsed.instance_id,
7546
8400
  waitTimeoutMs: parsed.waitTimeoutMs,
8401
+ createdBy: ctx.principal.url,
7547
8402
  ...parsed.fork_pointer && { forkPointer: {
7548
8403
  offset: parsed.fork_pointer.offset,
7549
8404
  subOffset: parsed.fork_pointer.sub_offset
@@ -7559,26 +8414,27 @@ async function sendEntity(request, ctx) {
7559
8414
  const parsed = routeBody(request);
7560
8415
  const principal = ctx.principal;
7561
8416
  if (parsed.from !== void 0 && parsed.from !== principal.url) return apiError(400, ErrCodeInvalidRequest, `Request from must match Electric-Principal`);
8417
+ if (parsed.from_principal !== void 0 && parsed.from_principal !== principal.url) return apiError(400, ErrCodeInvalidRequest, `Request from_principal must match Electric-Principal`);
8418
+ if (parsed.from_agent !== void 0) {
8419
+ const principalAgentUrl = agentUrlForPrincipal(principal);
8420
+ if (agentUrlPath(parsed.from_agent) !== principalAgentUrl) return apiError(400, ErrCodeInvalidRequest, `Request from_agent must match authenticated agent principal`);
8421
+ }
7562
8422
  await ctx.entityManager.ensurePrincipal(principal);
7563
8423
  const { entityUrl, entity } = requireExistingEntityRoute(request);
7564
8424
  const dispatchEntity = entity.dispatch_policy ? entity : await backfillEntityDispatchPolicy(ctx, entity);
7565
8425
  await linkEntityDispatchSubscription(ctx, dispatchEntity);
7566
- if (parsed.afterMs && parsed.afterMs > 0) await ctx.entityManager.enqueueDelayedSend(entityUrl, {
7567
- from: principal.url,
7568
- payload: parsed.payload,
7569
- key: parsed.key,
7570
- type: parsed.type,
7571
- mode: parsed.mode,
7572
- position: parsed.position
7573
- }, new Date(Date.now() + parsed.afterMs));
7574
- else await ctx.entityManager.send(entityUrl, {
8426
+ const sendReq = {
7575
8427
  from: principal.url,
8428
+ from_principal: principal.url,
8429
+ from_agent: parsed.from_agent,
7576
8430
  payload: parsed.payload,
7577
8431
  key: parsed.key,
7578
8432
  type: parsed.type,
7579
8433
  mode: parsed.mode,
7580
8434
  position: parsed.position
7581
- });
8435
+ };
8436
+ if (parsed.afterMs && parsed.afterMs > 0) await ctx.entityManager.enqueueDelayedSend(entityUrl, sendReq, new Date(Date.now() + parsed.afterMs));
8437
+ else await ctx.entityManager.send(entityUrl, sendReq);
7582
8438
  return (0, itty_router.status)(204);
7583
8439
  }
7584
8440
  async function createAttachment(request, ctx) {
@@ -7650,6 +8506,17 @@ async function spawnEntity(request, ctx) {
7650
8506
  wake: parsed.wake,
7651
8507
  created_by: principal.url
7652
8508
  });
8509
+ if (parsed.parent) await ctx.entityManager.registry.copyEntityPermissionGrantsForSpawn(parsed.parent, entity.url, principal.url);
8510
+ for (const grant of parsed.grants ?? []) await ctx.entityManager.registry.createEntityPermissionGrant({
8511
+ entityUrl: entity.url,
8512
+ permission: grant.permission,
8513
+ subjectKind: grant.subject_kind,
8514
+ subjectValue: grant.subject_value,
8515
+ propagation: grant.propagation,
8516
+ copyToChildren: grant.copy_to_children,
8517
+ expiresAt: parseExpiresAt$1(grant.expires_at),
8518
+ createdBy: principal.url
8519
+ });
7653
8520
  const linkBeforeInitialMessage = parsed.initialMessage !== void 0 && shouldLinkDispatchBeforeInitialMessage(dispatchPolicy);
7654
8521
  if (linkBeforeInitialMessage) await linkEntityDispatchSubscription(ctx, entity);
7655
8522
  if (parsed.initialMessage !== void 0) await ctx.entityManager.send(entity.url, {
@@ -7701,6 +8568,12 @@ async function signalEntity(request, ctx) {
7701
8568
  //#region src/routing/entity-types-router.ts
7702
8569
  const jsonObjectSchema = __sinclair_typebox.Type.Record(__sinclair_typebox.Type.String(), __sinclair_typebox.Type.Unknown());
7703
8570
  const schemaMapSchema = __sinclair_typebox.Type.Record(__sinclair_typebox.Type.String(), jsonObjectSchema);
8571
+ const typePermissionGrantInputSchema = __sinclair_typebox.Type.Object({
8572
+ subject_kind: __sinclair_typebox.Type.Union([__sinclair_typebox.Type.Literal(`principal`), __sinclair_typebox.Type.Literal(`principal_kind`)]),
8573
+ subject_value: __sinclair_typebox.Type.String(),
8574
+ permission: __sinclair_typebox.Type.Union([__sinclair_typebox.Type.Literal(`spawn`), __sinclair_typebox.Type.Literal(`manage`)]),
8575
+ expires_at: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String())
8576
+ }, { additionalProperties: false });
7704
8577
  const registerEntityTypeBodySchema = __sinclair_typebox.Type.Object({
7705
8578
  name: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
7706
8579
  description: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
@@ -7708,7 +8581,8 @@ const registerEntityTypeBodySchema = __sinclair_typebox.Type.Object({
7708
8581
  inbox_schemas: __sinclair_typebox.Type.Optional(schemaMapSchema),
7709
8582
  state_schemas: __sinclair_typebox.Type.Optional(schemaMapSchema),
7710
8583
  serve_endpoint: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
7711
- default_dispatch_policy: __sinclair_typebox.Type.Optional(dispatchPolicySchema)
8584
+ default_dispatch_policy: __sinclair_typebox.Type.Optional(dispatchPolicySchema),
8585
+ permission_grants: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Array(typePermissionGrantInputSchema))
7712
8586
  }, { additionalProperties: false });
7713
8587
  const amendEntityTypeSchemasBodySchema = __sinclair_typebox.Type.Object({
7714
8588
  inbox_schemas: __sinclair_typebox.Type.Optional(schemaMapSchema),
@@ -7716,20 +8590,56 @@ const amendEntityTypeSchemasBodySchema = __sinclair_typebox.Type.Object({
7716
8590
  }, { additionalProperties: false });
7717
8591
  const entityTypesRouter = (0, itty_router.Router)({ base: `/_electric/entity-types` });
7718
8592
  entityTypesRouter.get(`/`, listEntityTypes);
7719
- entityTypesRouter.post(`/`, withSchema(registerEntityTypeBodySchema), registerEntityType);
7720
- entityTypesRouter.patch(`/:name/schemas`, withSchema(amendEntityTypeSchemasBodySchema), amendSchemas);
7721
- entityTypesRouter.get(`/:name`, getEntityType);
7722
- entityTypesRouter.delete(`/:name`, deleteEntityType);
8593
+ entityTypesRouter.post(`/`, withSchema(registerEntityTypeBodySchema), withEntityTypeRegistrationPermission, registerEntityType);
8594
+ entityTypesRouter.patch(`/:name/schemas`, withExistingEntityType, withEntityTypeManagePermission, withSchema(amendEntityTypeSchemasBodySchema), amendSchemas);
8595
+ entityTypesRouter.get(`/:name`, withExistingEntityType, withEntityTypeSpawnPermission, getEntityType);
8596
+ entityTypesRouter.delete(`/:name`, withExistingEntityType, withEntityTypeManagePermission, deleteEntityType);
8597
+ entityTypesRouter.get(`/:name/grants`, withExistingEntityType, withEntityTypeManagePermission, listTypePermissionGrants);
8598
+ entityTypesRouter.post(`/:name/grants`, withExistingEntityType, withSchema(typePermissionGrantInputSchema), withEntityTypeManagePermission, createTypePermissionGrant);
8599
+ entityTypesRouter.delete(`/:name/grants/:grantId`, withExistingEntityType, withEntityTypeManagePermission, deleteTypePermissionGrant);
7723
8600
  async function registerEntityType(request, ctx) {
7724
8601
  const parsed = routeBody(request);
7725
8602
  const normalized = normalizeEntityTypeRequest(parsed);
7726
8603
  if (normalized.serve_endpoint && !normalized.description && !normalized.creation_schema) return await discoverServeEndpoint(ctx, normalized);
7727
8604
  const entityType = await ctx.entityManager.registerEntityType(normalized);
8605
+ await applyRegistrationPermissionGrants(ctx, entityType.name, normalized);
7728
8606
  return (0, itty_router.json)(toPublicEntityType(entityType), { status: 201 });
7729
8607
  }
7730
8608
  async function listEntityTypes(_request, ctx) {
7731
8609
  const entityTypes$1 = await ctx.entityManager.registry.listEntityTypes();
7732
- return (0, itty_router.json)(entityTypes$1.map((entityType) => toPublicEntityType(entityType)));
8610
+ const visible = [];
8611
+ for (const entityType of entityTypes$1) if (await canAccessEntityType(ctx, entityType, `spawn`)) visible.push(entityType);
8612
+ return (0, itty_router.json)(visible.map((entityType) => toPublicEntityType(entityType)));
8613
+ }
8614
+ async function withExistingEntityType(request, ctx) {
8615
+ const entityType = await ctx.entityManager.registry.getEntityType(request.params.name);
8616
+ if (!entityType) return apiError(404, ErrCodeNotFound, `Entity type not found`);
8617
+ request.entityTypeRoute = { entityType };
8618
+ return void 0;
8619
+ }
8620
+ async function withEntityTypeManagePermission(request, ctx) {
8621
+ const entityType = request.entityTypeRoute?.entityType;
8622
+ if (!entityType) throw new Error(`entity type middleware did not run`);
8623
+ if (await canAccessEntityType(ctx, entityType, `manage`, request)) return void 0;
8624
+ return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to manage ${entityType.name}`);
8625
+ }
8626
+ async function withEntityTypeSpawnPermission(request, ctx) {
8627
+ const entityType = request.entityTypeRoute?.entityType;
8628
+ if (!entityType) throw new Error(`entity type middleware did not run`);
8629
+ if (await canAccessEntityType(ctx, entityType, `spawn`, request)) return void 0;
8630
+ return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to spawn ${entityType.name}`);
8631
+ }
8632
+ async function withEntityTypeRegistrationPermission(request, ctx) {
8633
+ const parsed = normalizeEntityTypeRequest(routeBody(request));
8634
+ if (!parsed.name) return void 0;
8635
+ const existing = await ctx.entityManager.registry.getEntityType(parsed.name);
8636
+ if (existing) {
8637
+ request.entityTypeRoute = { entityType: existing };
8638
+ if (await canAccessEntityType(ctx, existing, `manage`, request)) return void 0;
8639
+ return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to manage ${existing.name}`);
8640
+ }
8641
+ if (await canRegisterEntityType(ctx, parsed, request)) return void 0;
8642
+ return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to register entity types`);
7733
8643
  }
7734
8644
  async function discoverServeEndpoint(ctx, parsed) {
7735
8645
  try {
@@ -7738,17 +8648,17 @@ async function discoverServeEndpoint(ctx, parsed) {
7738
8648
  const manifest = await response.json();
7739
8649
  if (manifest.name !== parsed.name) return apiError(400, ErrCodeServeEndpointNameMismatch, `Serve endpoint returned name "${manifest.name}" but expected "${parsed.name}"`);
7740
8650
  manifest.serve_endpoint = parsed.serve_endpoint;
8651
+ manifest.permission_grants = parsed.permission_grants;
7741
8652
  const entityType = await ctx.entityManager.registerEntityType(normalizeEntityTypeRequest(manifest));
8653
+ await applyRegistrationPermissionGrants(ctx, entityType.name, manifest);
7742
8654
  return (0, itty_router.json)(toPublicEntityType(entityType), { status: 201 });
7743
8655
  } catch (err) {
7744
8656
  if (err instanceof ElectricAgentsError) throw err;
7745
8657
  return apiError(502, ErrCodeServeEndpointUnreachable, `Failed to reach serve endpoint: ${err instanceof Error ? err.message : String(err)}`);
7746
8658
  }
7747
8659
  }
7748
- async function getEntityType(request, ctx) {
7749
- const entityType = await ctx.entityManager.registry.getEntityType(request.params.name);
7750
- if (!entityType) return apiError(404, ErrCodeNotFound, `Entity type not found`);
7751
- return (0, itty_router.json)(toPublicEntityType(entityType));
8660
+ async function getEntityType(request) {
8661
+ return (0, itty_router.json)(toPublicEntityType(request.entityTypeRoute.entityType));
7752
8662
  }
7753
8663
  async function amendSchemas(request, ctx) {
7754
8664
  const parsed = routeBody(request);
@@ -7762,6 +8672,47 @@ async function deleteEntityType(request, ctx) {
7762
8672
  await ctx.entityManager.deleteEntityType(request.params.name);
7763
8673
  return (0, itty_router.status)(204);
7764
8674
  }
8675
+ async function listTypePermissionGrants(request, ctx) {
8676
+ const grants = await ctx.entityManager.registry.listEntityTypePermissionGrants(request.entityTypeRoute.entityType.name);
8677
+ return (0, itty_router.json)({ grants });
8678
+ }
8679
+ async function createTypePermissionGrant(request, ctx) {
8680
+ const parsed = routeBody(request);
8681
+ const grant = await ctx.entityManager.registry.createEntityTypePermissionGrant({
8682
+ entityType: request.entityTypeRoute.entityType.name,
8683
+ permission: parsed.permission,
8684
+ subjectKind: parsed.subject_kind,
8685
+ subjectValue: parsed.subject_value,
8686
+ expiresAt: parseExpiresAt(parsed.expires_at),
8687
+ createdBy: ctx.principal.url
8688
+ });
8689
+ return (0, itty_router.json)(grant, { status: 201 });
8690
+ }
8691
+ async function deleteTypePermissionGrant(request, ctx) {
8692
+ const deleted = await ctx.entityManager.registry.deleteEntityTypePermissionGrant(request.entityTypeRoute.entityType.name, parseGrantId(request));
8693
+ return deleted ? (0, itty_router.status)(204) : apiError(404, ErrCodeNotFound, `Grant not found`);
8694
+ }
8695
+ async function applyRegistrationPermissionGrants(ctx, entityType, request) {
8696
+ for (const grant of request.permission_grants ?? []) await ctx.entityManager.registry.ensureEntityTypePermissionGrant({
8697
+ entityType,
8698
+ permission: grant.permission,
8699
+ subjectKind: grant.subject_kind,
8700
+ subjectValue: grant.subject_value,
8701
+ expiresAt: parseExpiresAt(grant.expires_at),
8702
+ createdBy: ctx.principal.url
8703
+ });
8704
+ }
8705
+ function parseGrantId(request) {
8706
+ const grantId = Number.parseInt(String(request.params.grantId), 10);
8707
+ if (!Number.isSafeInteger(grantId) || grantId <= 0) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Invalid grant id`, 400);
8708
+ return grantId;
8709
+ }
8710
+ function parseExpiresAt(value) {
8711
+ if (value === void 0) return void 0;
8712
+ const expiresAt = new Date(value);
8713
+ if (Number.isNaN(expiresAt.getTime())) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Invalid expires_at timestamp`, 400);
8714
+ return expiresAt;
8715
+ }
7765
8716
  function normalizeEntityTypeRequest(parsed) {
7766
8717
  const serveEndpoint = rewriteLoopbackWebhookUrl(parsed.serve_endpoint);
7767
8718
  return {
@@ -7774,7 +8725,8 @@ function normalizeEntityTypeRequest(parsed) {
7774
8725
  default_dispatch_policy: parsed.default_dispatch_policy ?? (serveEndpoint ? { targets: [{
7775
8726
  type: `webhook`,
7776
8727
  url: serveEndpoint
7777
- }] } : void 0)
8728
+ }] } : void 0),
8729
+ permission_grants: parsed.permission_grants
7778
8730
  };
7779
8731
  }
7780
8732
  function toPublicEntityType(entityType) {
@@ -7833,6 +8785,7 @@ function applyCors(response) {
7833
8785
  `content-type`,
7834
8786
  `authorization`,
7835
8787
  `electric-claim-token`,
8788
+ `electric-owner-entity`,
7836
8789
  ELECTRIC_PRINCIPAL_HEADER,
7837
8790
  `ngrok-skip-browser-warning`
7838
8791
  ].join(`, `));
@@ -7883,7 +8836,7 @@ observationsRouter.post(`/entities/ensure-stream`, withSchema(ensureEntitiesMemb
7883
8836
  observationsRouter.post(`/cron/ensure-stream`, withSchema(ensureCronStreamBodySchema), ensureCronStream);
7884
8837
  async function ensureEntitiesMembershipStream(request, ctx) {
7885
8838
  const parsed = routeBody(request);
7886
- const result = await ctx.entityManager.ensureEntitiesMembershipStream(parsed.tags ?? {});
8839
+ const result = await ctx.entityManager.ensureEntitiesMembershipStream(parsed.tags ?? {}, ctx.principal);
7887
8840
  return (0, itty_router.json)(result);
7888
8841
  }
7889
8842
  async function ensureCronStream(request, ctx) {