@electric-ax/agents-server 0.4.15 → 0.4.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/entrypoint.js +1176 -232
- package/dist/index.cjs +1179 -226
- package/dist/index.d.cts +1146 -167
- package/dist/index.d.ts +1146 -167
- package/dist/index.js +1181 -228
- package/drizzle/0011_entity_permissions.sql +100 -0
- package/drizzle/0012_horton_user_manage_permission.sql +25 -0
- package/drizzle/0013_worker_user_manage_permission.sql +25 -0
- package/drizzle/meta/_journal.json +21 -0
- package/package.json +7 -7
- package/src/db/schema.ts +198 -0
- package/src/electric-agents-types.ts +76 -2
- package/src/entity-bridge-manager.ts +57 -6
- package/src/entity-manager.ts +78 -60
- package/src/entity-projector.ts +76 -17
- package/src/entity-registry.ts +608 -5
- package/src/index.ts +11 -0
- package/src/permissions.ts +239 -0
- package/src/routing/context.ts +2 -0
- package/src/routing/durable-streams-router.ts +125 -4
- package/src/routing/electric-proxy-router.ts +4 -0
- package/src/routing/entities-router.ts +344 -20
- package/src/routing/entity-types-router.ts +244 -15
- package/src/routing/hooks.ts +1 -0
- package/src/routing/observations-router.ts +2 -1
- package/src/runtime.ts +34 -0
- package/src/scheduler.ts +2 -0
- package/src/server.ts +5 -0
- package/src/utils/server-utils.ts +191 -11
- package/src/wake-registry.ts +8 -0
package/dist/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) => [
|
|
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(
|
|
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)
|
|
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)
|
|
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,
|
|
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
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
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
|
|
3589
|
+
const [mainStreamResult$1] = await Promise.allSettled([this.streamClient.create(mainPath, {
|
|
3172
3590
|
contentType,
|
|
3173
3591
|
body: initialBody
|
|
3174
|
-
})
|
|
3175
|
-
return [
|
|
3176
|
-
|
|
3177
|
-
|
|
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` ||
|
|
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 :
|
|
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 :
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 ? `${
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
7749
|
-
|
|
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) {
|