@electric-ax/agents-server 0.4.15 → 0.4.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/entrypoint.js +1230 -235
- package/dist/index.cjs +1233 -229
- package/dist/index.d.cts +1319 -318
- package/dist/index.d.ts +1319 -318
- package/dist/index.js +1235 -231
- package/drizzle/0011_entity_permissions.sql +100 -0
- package/drizzle/0012_horton_user_manage_permission.sql +25 -0
- package/drizzle/0013_worker_user_manage_permission.sql +25 -0
- package/drizzle/0014_entity_type_slash_commands.sql +1 -0
- package/drizzle/meta/_journal.json +28 -0
- package/package.json +7 -7
- package/src/db/schema.ts +199 -0
- package/src/electric-agents-types.ts +80 -2
- package/src/entity-bridge-manager.ts +57 -6
- package/src/entity-manager.ts +124 -61
- package/src/entity-projector.ts +76 -17
- package/src/entity-registry.ts +615 -5
- package/src/index.ts +11 -0
- package/src/permissions.ts +239 -0
- package/src/routing/context.ts +2 -0
- package/src/routing/durable-streams-router.ts +125 -4
- package/src/routing/electric-proxy-router.ts +4 -0
- package/src/routing/entities-router.ts +347 -20
- package/src/routing/entity-types-router.ts +267 -15
- package/src/routing/hooks.ts +1 -0
- package/src/routing/observations-router.ts +2 -1
- package/src/runtime.ts +34 -0
- package/src/scheduler.ts +2 -0
- package/src/server.ts +5 -0
- package/src/utils/server-utils.ts +192 -12
- package/src/wake-registry.ts +8 -0
package/dist/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,
|
|
@@ -73,6 +78,7 @@ const entityTypes = (0, drizzle_orm_pg_core.pgTable)(`entity_types`, {
|
|
|
73
78
|
creationSchema: (0, drizzle_orm_pg_core.jsonb)(`creation_schema`),
|
|
74
79
|
inboxSchemas: (0, drizzle_orm_pg_core.jsonb)(`inbox_schemas`),
|
|
75
80
|
stateSchemas: (0, drizzle_orm_pg_core.jsonb)(`state_schemas`),
|
|
81
|
+
slashCommands: (0, drizzle_orm_pg_core.jsonb)(`slash_commands`),
|
|
76
82
|
serveEndpoint: (0, drizzle_orm_pg_core.text)(`serve_endpoint`),
|
|
77
83
|
defaultDispatchPolicy: (0, drizzle_orm_pg_core.jsonb)(`default_dispatch_policy`),
|
|
78
84
|
revision: (0, drizzle_orm_pg_core.integer)(`revision`).notNull().default(1),
|
|
@@ -107,6 +113,94 @@ const entities = (0, drizzle_orm_pg_core.pgTable)(`entities`, {
|
|
|
107
113
|
(0, drizzle_orm_pg_core.index)(`entities_tags_index_gin`).using(`gin`, table.tagsIndex),
|
|
108
114
|
(0, drizzle_orm_pg_core.check)(`chk_entities_status`, drizzle_orm.sql`${table.status} IN ('spawning', 'running', 'idle', 'paused', 'stopping', 'stopped', 'killed')`)
|
|
109
115
|
]);
|
|
116
|
+
const entityTypePermissionGrants = (0, drizzle_orm_pg_core.pgTable)(`entity_type_permission_grants`, {
|
|
117
|
+
id: (0, drizzle_orm_pg_core.bigserial)(`id`, { mode: `number` }).primaryKey(),
|
|
118
|
+
tenantId: (0, drizzle_orm_pg_core.text)(`tenant_id`).notNull().default(`default`),
|
|
119
|
+
entityType: (0, drizzle_orm_pg_core.text)(`entity_type`).notNull(),
|
|
120
|
+
permission: (0, drizzle_orm_pg_core.text)(`permission`).notNull(),
|
|
121
|
+
subjectKind: (0, drizzle_orm_pg_core.text)(`subject_kind`).notNull(),
|
|
122
|
+
subjectValue: (0, drizzle_orm_pg_core.text)(`subject_value`).notNull(),
|
|
123
|
+
createdBy: (0, drizzle_orm_pg_core.text)(`created_by`),
|
|
124
|
+
expiresAt: (0, drizzle_orm_pg_core.timestamp)(`expires_at`, { withTimezone: true }),
|
|
125
|
+
createdAt: (0, drizzle_orm_pg_core.timestamp)(`created_at`, { withTimezone: true }).notNull().defaultNow(),
|
|
126
|
+
updatedAt: (0, drizzle_orm_pg_core.timestamp)(`updated_at`, { withTimezone: true }).notNull().defaultNow()
|
|
127
|
+
}, (table) => [
|
|
128
|
+
(0, drizzle_orm_pg_core.index)(`idx_type_permission_grants_lookup`).on(table.tenantId, table.entityType, table.permission, table.subjectKind, table.subjectValue),
|
|
129
|
+
(0, drizzle_orm_pg_core.index)(`idx_type_permission_grants_expiry`).on(table.tenantId, table.expiresAt),
|
|
130
|
+
(0, drizzle_orm_pg_core.check)(`chk_type_permission_grants_permission`, drizzle_orm.sql`${table.permission} IN ('spawn', 'manage')`),
|
|
131
|
+
(0, drizzle_orm_pg_core.check)(`chk_type_permission_grants_subject_kind`, drizzle_orm.sql`${table.subjectKind} IN ('principal', 'principal_kind')`)
|
|
132
|
+
]);
|
|
133
|
+
const entityLineage = (0, drizzle_orm_pg_core.pgTable)(`entity_lineage`, {
|
|
134
|
+
tenantId: (0, drizzle_orm_pg_core.text)(`tenant_id`).notNull().default(`default`),
|
|
135
|
+
ancestorUrl: (0, drizzle_orm_pg_core.text)(`ancestor_url`).notNull(),
|
|
136
|
+
descendantUrl: (0, drizzle_orm_pg_core.text)(`descendant_url`).notNull(),
|
|
137
|
+
depth: (0, drizzle_orm_pg_core.integer)(`depth`).notNull(),
|
|
138
|
+
createdAt: (0, drizzle_orm_pg_core.timestamp)(`created_at`, { withTimezone: true }).notNull().defaultNow()
|
|
139
|
+
}, (table) => [
|
|
140
|
+
(0, drizzle_orm_pg_core.primaryKey)({ columns: [
|
|
141
|
+
table.tenantId,
|
|
142
|
+
table.ancestorUrl,
|
|
143
|
+
table.descendantUrl
|
|
144
|
+
] }),
|
|
145
|
+
(0, drizzle_orm_pg_core.index)(`idx_entity_lineage_descendant`).on(table.tenantId, table.descendantUrl),
|
|
146
|
+
(0, drizzle_orm_pg_core.check)(`chk_entity_lineage_depth`, drizzle_orm.sql`${table.depth} >= 0`)
|
|
147
|
+
]);
|
|
148
|
+
const entityPermissionGrants = (0, drizzle_orm_pg_core.pgTable)(`entity_permission_grants`, {
|
|
149
|
+
id: (0, drizzle_orm_pg_core.bigserial)(`id`, { mode: `number` }).primaryKey(),
|
|
150
|
+
tenantId: (0, drizzle_orm_pg_core.text)(`tenant_id`).notNull().default(`default`),
|
|
151
|
+
entityUrl: (0, drizzle_orm_pg_core.text)(`entity_url`).notNull(),
|
|
152
|
+
permission: (0, drizzle_orm_pg_core.text)(`permission`).notNull(),
|
|
153
|
+
subjectKind: (0, drizzle_orm_pg_core.text)(`subject_kind`).notNull(),
|
|
154
|
+
subjectValue: (0, drizzle_orm_pg_core.text)(`subject_value`).notNull(),
|
|
155
|
+
propagation: (0, drizzle_orm_pg_core.text)(`propagation`).notNull().default(`self`),
|
|
156
|
+
copyToChildren: (0, drizzle_orm_pg_core.boolean)(`copy_to_children`).notNull().default(false),
|
|
157
|
+
createdBy: (0, drizzle_orm_pg_core.text)(`created_by`),
|
|
158
|
+
expiresAt: (0, drizzle_orm_pg_core.timestamp)(`expires_at`, { withTimezone: true }),
|
|
159
|
+
createdAt: (0, drizzle_orm_pg_core.timestamp)(`created_at`, { withTimezone: true }).notNull().defaultNow(),
|
|
160
|
+
updatedAt: (0, drizzle_orm_pg_core.timestamp)(`updated_at`, { withTimezone: true }).notNull().defaultNow()
|
|
161
|
+
}, (table) => [
|
|
162
|
+
(0, drizzle_orm_pg_core.index)(`idx_entity_permission_grants_entity`).on(table.tenantId, table.entityUrl),
|
|
163
|
+
(0, drizzle_orm_pg_core.index)(`idx_entity_permission_grants_subject`).on(table.tenantId, table.permission, table.subjectKind, table.subjectValue),
|
|
164
|
+
(0, drizzle_orm_pg_core.index)(`idx_entity_permission_grants_expiry`).on(table.tenantId, table.expiresAt),
|
|
165
|
+
(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')`),
|
|
166
|
+
(0, drizzle_orm_pg_core.check)(`chk_entity_permission_grants_subject_kind`, drizzle_orm.sql`${table.subjectKind} IN ('principal', 'principal_kind')`),
|
|
167
|
+
(0, drizzle_orm_pg_core.check)(`chk_entity_permission_grants_propagation`, drizzle_orm.sql`${table.propagation} IN ('self', 'descendants')`)
|
|
168
|
+
]);
|
|
169
|
+
const entityEffectivePermissions = (0, drizzle_orm_pg_core.pgTable)(`entity_effective_permissions`, {
|
|
170
|
+
id: (0, drizzle_orm_pg_core.bigserial)(`id`, { mode: `number` }).primaryKey(),
|
|
171
|
+
tenantId: (0, drizzle_orm_pg_core.text)(`tenant_id`).notNull().default(`default`),
|
|
172
|
+
entityUrl: (0, drizzle_orm_pg_core.text)(`entity_url`).notNull(),
|
|
173
|
+
sourceEntityUrl: (0, drizzle_orm_pg_core.text)(`source_entity_url`).notNull(),
|
|
174
|
+
sourceGrantId: (0, drizzle_orm_pg_core.bigint)(`source_grant_id`, { mode: `number` }).notNull(),
|
|
175
|
+
permission: (0, drizzle_orm_pg_core.text)(`permission`).notNull(),
|
|
176
|
+
subjectKind: (0, drizzle_orm_pg_core.text)(`subject_kind`).notNull(),
|
|
177
|
+
subjectValue: (0, drizzle_orm_pg_core.text)(`subject_value`).notNull(),
|
|
178
|
+
expiresAt: (0, drizzle_orm_pg_core.timestamp)(`expires_at`, { withTimezone: true }),
|
|
179
|
+
createdAt: (0, drizzle_orm_pg_core.timestamp)(`created_at`, { withTimezone: true }).notNull().defaultNow()
|
|
180
|
+
}, (table) => [
|
|
181
|
+
(0, drizzle_orm_pg_core.unique)(`uq_entity_effective_permission`).on(table.tenantId, table.entityUrl, table.sourceGrantId),
|
|
182
|
+
(0, drizzle_orm_pg_core.index)(`idx_entity_effective_permissions_lookup`).on(table.tenantId, table.permission, table.subjectKind, table.subjectValue, table.entityUrl),
|
|
183
|
+
(0, drizzle_orm_pg_core.index)(`idx_entity_effective_permissions_entity`).on(table.tenantId, table.entityUrl),
|
|
184
|
+
(0, drizzle_orm_pg_core.index)(`idx_entity_effective_permissions_expiry`).on(table.tenantId, table.expiresAt),
|
|
185
|
+
(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')`),
|
|
186
|
+
(0, drizzle_orm_pg_core.check)(`chk_entity_effective_permissions_subject_kind`, drizzle_orm.sql`${table.subjectKind} IN ('principal', 'principal_kind')`)
|
|
187
|
+
]);
|
|
188
|
+
const sharedStateLinks = (0, drizzle_orm_pg_core.pgTable)(`shared_state_links`, {
|
|
189
|
+
tenantId: (0, drizzle_orm_pg_core.text)(`tenant_id`).notNull().default(`default`),
|
|
190
|
+
sharedStateId: (0, drizzle_orm_pg_core.text)(`shared_state_id`).notNull(),
|
|
191
|
+
ownerEntityUrl: (0, drizzle_orm_pg_core.text)(`owner_entity_url`).notNull(),
|
|
192
|
+
manifestKey: (0, drizzle_orm_pg_core.text)(`manifest_key`).notNull(),
|
|
193
|
+
createdAt: (0, drizzle_orm_pg_core.timestamp)(`created_at`, { withTimezone: true }).notNull().defaultNow(),
|
|
194
|
+
updatedAt: (0, drizzle_orm_pg_core.timestamp)(`updated_at`, { withTimezone: true }).notNull().defaultNow()
|
|
195
|
+
}, (table) => [
|
|
196
|
+
(0, drizzle_orm_pg_core.primaryKey)({ columns: [
|
|
197
|
+
table.tenantId,
|
|
198
|
+
table.ownerEntityUrl,
|
|
199
|
+
table.manifestKey
|
|
200
|
+
] }),
|
|
201
|
+
(0, drizzle_orm_pg_core.index)(`idx_shared_state_links_shared_state`).on(table.tenantId, table.sharedStateId),
|
|
202
|
+
(0, drizzle_orm_pg_core.index)(`idx_shared_state_links_owner`).on(table.tenantId, table.ownerEntityUrl)
|
|
203
|
+
]);
|
|
110
204
|
const users = (0, drizzle_orm_pg_core.pgTable)(`users`, {
|
|
111
205
|
tenantId: (0, drizzle_orm_pg_core.text)(`tenant_id`).notNull().default(`default`),
|
|
112
206
|
id: (0, drizzle_orm_pg_core.text)(`id`).notNull(),
|
|
@@ -293,12 +387,18 @@ const entityBridges = (0, drizzle_orm_pg_core.pgTable)(`entity_bridges`, {
|
|
|
293
387
|
sourceRef: (0, drizzle_orm_pg_core.text)(`source_ref`).notNull(),
|
|
294
388
|
tags: (0, drizzle_orm_pg_core.jsonb)(`tags`).notNull(),
|
|
295
389
|
streamUrl: (0, drizzle_orm_pg_core.text)(`stream_url`).notNull(),
|
|
390
|
+
principalUrl: (0, drizzle_orm_pg_core.text)(`principal_url`),
|
|
391
|
+
principalKind: (0, drizzle_orm_pg_core.text)(`principal_kind`),
|
|
296
392
|
shapeHandle: (0, drizzle_orm_pg_core.text)(`shape_handle`),
|
|
297
393
|
shapeOffset: (0, drizzle_orm_pg_core.text)(`shape_offset`),
|
|
298
394
|
lastObserverActivityAt: (0, drizzle_orm_pg_core.timestamp)(`last_observer_activity_at`, { withTimezone: true }).notNull().defaultNow(),
|
|
299
395
|
createdAt: (0, drizzle_orm_pg_core.timestamp)(`created_at`, { withTimezone: true }).notNull().defaultNow(),
|
|
300
396
|
updatedAt: (0, drizzle_orm_pg_core.timestamp)(`updated_at`, { withTimezone: true }).notNull().defaultNow()
|
|
301
|
-
}, (table) => [
|
|
397
|
+
}, (table) => [
|
|
398
|
+
(0, drizzle_orm_pg_core.primaryKey)({ columns: [table.tenantId, table.sourceRef] }),
|
|
399
|
+
(0, drizzle_orm_pg_core.unique)(`uq_entity_bridges_stream_url`).on(table.tenantId, table.streamUrl),
|
|
400
|
+
(0, drizzle_orm_pg_core.index)(`idx_entity_bridges_principal`).on(table.tenantId, table.principalKind, table.principalUrl)
|
|
401
|
+
]);
|
|
302
402
|
const entityManifestSources = (0, drizzle_orm_pg_core.pgTable)(`entity_manifest_sources`, {
|
|
303
403
|
tenantId: (0, drizzle_orm_pg_core.text)(`tenant_id`).notNull().default(`default`),
|
|
304
404
|
ownerEntityUrl: (0, drizzle_orm_pg_core.text)(`owner_entity_url`).notNull(),
|
|
@@ -486,16 +586,26 @@ function isDuplicateUrlError(err) {
|
|
|
486
586
|
return e.code === `23505`;
|
|
487
587
|
}
|
|
488
588
|
const DEFAULT_RUNNER_LEASE_MS = 3e4;
|
|
589
|
+
const PERMISSION_PRUNE_INTERVAL_MS = 3e4;
|
|
489
590
|
function runnerWakeStream(runnerId) {
|
|
490
591
|
return `/runners/${runnerId}/wake`;
|
|
491
592
|
}
|
|
492
593
|
var PostgresRegistry = class {
|
|
594
|
+
lastPermissionPruneStartedAt = 0;
|
|
595
|
+
permissionPrunePromise = null;
|
|
493
596
|
constructor(db, tenantId = DEFAULT_TENANT_ID) {
|
|
494
597
|
this.db = db;
|
|
495
598
|
this.tenantId = tenantId;
|
|
496
599
|
}
|
|
497
600
|
async initialize() {}
|
|
498
601
|
close() {}
|
|
602
|
+
async ensureUserForPrincipal(principal) {
|
|
603
|
+
if (principal.kind !== `user`) return;
|
|
604
|
+
await this.db.insert(users).values({
|
|
605
|
+
tenantId: this.tenantId,
|
|
606
|
+
id: principal.id
|
|
607
|
+
}).onConflictDoNothing();
|
|
608
|
+
}
|
|
499
609
|
async createRunner(input) {
|
|
500
610
|
const now = new Date();
|
|
501
611
|
const wakeStream = input.wakeStream ?? runnerWakeStream(input.id);
|
|
@@ -740,6 +850,7 @@ var PostgresRegistry = class {
|
|
|
740
850
|
creationSchema: et.creation_schema ?? null,
|
|
741
851
|
inboxSchemas: et.inbox_schemas ?? null,
|
|
742
852
|
stateSchemas: et.state_schemas ?? null,
|
|
853
|
+
slashCommands: et.slash_commands ?? null,
|
|
743
854
|
serveEndpoint: et.serve_endpoint ?? null,
|
|
744
855
|
defaultDispatchPolicy: et.default_dispatch_policy ?? null,
|
|
745
856
|
revision: et.revision,
|
|
@@ -752,6 +863,7 @@ var PostgresRegistry = class {
|
|
|
752
863
|
creationSchema: et.creation_schema ?? null,
|
|
753
864
|
inboxSchemas: et.inbox_schemas ?? null,
|
|
754
865
|
stateSchemas: et.state_schemas ?? null,
|
|
866
|
+
slashCommands: et.slash_commands ?? null,
|
|
755
867
|
serveEndpoint: et.serve_endpoint ?? null,
|
|
756
868
|
defaultDispatchPolicy: et.default_dispatch_policy ?? null,
|
|
757
869
|
revision: et.revision,
|
|
@@ -769,6 +881,7 @@ var PostgresRegistry = class {
|
|
|
769
881
|
creationSchema: et.creation_schema ?? null,
|
|
770
882
|
inboxSchemas: et.inbox_schemas ?? null,
|
|
771
883
|
stateSchemas: et.state_schemas ?? null,
|
|
884
|
+
slashCommands: et.slash_commands ?? null,
|
|
772
885
|
serveEndpoint: et.serve_endpoint ?? null,
|
|
773
886
|
defaultDispatchPolicy: et.default_dispatch_policy ?? null,
|
|
774
887
|
revision: et.revision,
|
|
@@ -795,6 +908,7 @@ var PostgresRegistry = class {
|
|
|
795
908
|
creationSchema: et.creation_schema ?? null,
|
|
796
909
|
inboxSchemas: et.inbox_schemas ?? null,
|
|
797
910
|
stateSchemas: et.state_schemas ?? null,
|
|
911
|
+
slashCommands: et.slash_commands ?? null,
|
|
798
912
|
serveEndpoint: et.serve_endpoint ?? null,
|
|
799
913
|
defaultDispatchPolicy: et.default_dispatch_policy ?? null,
|
|
800
914
|
revision: et.revision,
|
|
@@ -830,6 +944,59 @@ var PostgresRegistry = class {
|
|
|
830
944
|
pendingSourceStreams: [],
|
|
831
945
|
updatedAt: new Date()
|
|
832
946
|
}).onConflictDoNothing();
|
|
947
|
+
await tx.insert(entityLineage).values({
|
|
948
|
+
tenantId: this.tenantId,
|
|
949
|
+
ancestorUrl: entity.url,
|
|
950
|
+
descendantUrl: entity.url,
|
|
951
|
+
depth: 0
|
|
952
|
+
}).onConflictDoNothing();
|
|
953
|
+
if (entity.parent) await tx.execute(drizzle_orm.sql`
|
|
954
|
+
INSERT INTO ${entityLineage} (
|
|
955
|
+
tenant_id,
|
|
956
|
+
ancestor_url,
|
|
957
|
+
descendant_url,
|
|
958
|
+
depth
|
|
959
|
+
)
|
|
960
|
+
SELECT
|
|
961
|
+
${this.tenantId},
|
|
962
|
+
ancestor_url,
|
|
963
|
+
${entity.url},
|
|
964
|
+
depth + 1
|
|
965
|
+
FROM ${entityLineage}
|
|
966
|
+
WHERE tenant_id = ${this.tenantId}
|
|
967
|
+
AND descendant_url = ${entity.parent}
|
|
968
|
+
ON CONFLICT DO NOTHING
|
|
969
|
+
`);
|
|
970
|
+
await tx.execute(drizzle_orm.sql`
|
|
971
|
+
INSERT INTO ${entityEffectivePermissions} (
|
|
972
|
+
tenant_id,
|
|
973
|
+
entity_url,
|
|
974
|
+
source_entity_url,
|
|
975
|
+
source_grant_id,
|
|
976
|
+
permission,
|
|
977
|
+
subject_kind,
|
|
978
|
+
subject_value,
|
|
979
|
+
expires_at
|
|
980
|
+
)
|
|
981
|
+
SELECT
|
|
982
|
+
${this.tenantId},
|
|
983
|
+
${entity.url},
|
|
984
|
+
grants.entity_url,
|
|
985
|
+
grants.id,
|
|
986
|
+
grants.permission,
|
|
987
|
+
grants.subject_kind,
|
|
988
|
+
grants.subject_value,
|
|
989
|
+
grants.expires_at
|
|
990
|
+
FROM ${entityPermissionGrants} grants
|
|
991
|
+
JOIN ${entityLineage} lineage
|
|
992
|
+
ON lineage.tenant_id = grants.tenant_id
|
|
993
|
+
AND lineage.ancestor_url = grants.entity_url
|
|
994
|
+
AND lineage.descendant_url = ${entity.url}
|
|
995
|
+
WHERE grants.tenant_id = ${this.tenantId}
|
|
996
|
+
AND grants.propagation = 'descendants'
|
|
997
|
+
AND (grants.expires_at IS NULL OR grants.expires_at > now())
|
|
998
|
+
ON CONFLICT DO NOTHING
|
|
999
|
+
`);
|
|
833
1000
|
return parseInt(result[0].txid);
|
|
834
1001
|
});
|
|
835
1002
|
} catch (err) {
|
|
@@ -851,10 +1018,8 @@ var PostgresRegistry = class {
|
|
|
851
1018
|
}
|
|
852
1019
|
async getEntityByStream(streamPath) {
|
|
853
1020
|
const mainSuffix = `/main`;
|
|
854
|
-
const errorSuffix = `/error`;
|
|
855
1021
|
let entityUrl = null;
|
|
856
1022
|
if (streamPath.endsWith(mainSuffix)) entityUrl = streamPath.slice(0, -mainSuffix.length);
|
|
857
|
-
else if (streamPath.endsWith(errorSuffix)) entityUrl = streamPath.slice(0, -errorSuffix.length);
|
|
858
1023
|
if (!entityUrl) return null;
|
|
859
1024
|
return this.getEntity(entityUrl);
|
|
860
1025
|
}
|
|
@@ -864,6 +1029,23 @@ var PostgresRegistry = class {
|
|
|
864
1029
|
if (filter?.status) conditions.push((0, drizzle_orm.eq)(entities.status, filter.status));
|
|
865
1030
|
if (filter?.parent) conditions.push((0, drizzle_orm.eq)(entities.parent, filter.parent));
|
|
866
1031
|
if (filter?.created_by) conditions.push((0, drizzle_orm.eq)(entities.createdBy, filter.created_by));
|
|
1032
|
+
if (filter?.readableBy && !filter.readableBy.bypass) conditions.push(drizzle_orm.sql`(
|
|
1033
|
+
${entities.createdBy} = ${filter.readableBy.principalUrl}
|
|
1034
|
+
OR ${entities.url} IN (
|
|
1035
|
+
SELECT ${entityEffectivePermissions.entityUrl}
|
|
1036
|
+
FROM ${entityEffectivePermissions}
|
|
1037
|
+
WHERE ${entityEffectivePermissions.tenantId} = ${this.tenantId}
|
|
1038
|
+
AND ${entityEffectivePermissions.permission} IN ('read', 'manage')
|
|
1039
|
+
AND (${entityEffectivePermissions.expiresAt} IS NULL OR ${entityEffectivePermissions.expiresAt} > now())
|
|
1040
|
+
AND (
|
|
1041
|
+
(${entityEffectivePermissions.subjectKind} = 'principal'
|
|
1042
|
+
AND ${entityEffectivePermissions.subjectValue} = ${filter.readableBy.principalUrl})
|
|
1043
|
+
OR
|
|
1044
|
+
(${entityEffectivePermissions.subjectKind} = 'principal_kind'
|
|
1045
|
+
AND ${entityEffectivePermissions.subjectValue} = ${filter.readableBy.principalKind})
|
|
1046
|
+
)
|
|
1047
|
+
)
|
|
1048
|
+
)`);
|
|
867
1049
|
const whereClause = (0, drizzle_orm.and)(...conditions);
|
|
868
1050
|
const countResult = await this.db.select({ count: drizzle_orm.sql`count(*)` }).from(entities).where(whereClause);
|
|
869
1051
|
const total = Number(countResult[0].count);
|
|
@@ -876,6 +1058,189 @@ var PostgresRegistry = class {
|
|
|
876
1058
|
total
|
|
877
1059
|
};
|
|
878
1060
|
}
|
|
1061
|
+
async createEntityTypePermissionGrant(input) {
|
|
1062
|
+
const [row] = await this.db.insert(entityTypePermissionGrants).values({
|
|
1063
|
+
tenantId: this.tenantId,
|
|
1064
|
+
entityType: input.entityType,
|
|
1065
|
+
permission: input.permission,
|
|
1066
|
+
subjectKind: input.subjectKind,
|
|
1067
|
+
subjectValue: input.subjectValue,
|
|
1068
|
+
createdBy: input.createdBy ?? null,
|
|
1069
|
+
expiresAt: input.expiresAt ?? null
|
|
1070
|
+
}).returning();
|
|
1071
|
+
return this.rowToEntityTypePermissionGrant(row);
|
|
1072
|
+
}
|
|
1073
|
+
async ensureEntityTypePermissionGrant(input) {
|
|
1074
|
+
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);
|
|
1075
|
+
if (existing) return this.rowToEntityTypePermissionGrant(existing);
|
|
1076
|
+
return await this.createEntityTypePermissionGrant(input);
|
|
1077
|
+
}
|
|
1078
|
+
async listEntityTypePermissionGrants(entityType) {
|
|
1079
|
+
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);
|
|
1080
|
+
return rows.map((row) => this.rowToEntityTypePermissionGrant(row));
|
|
1081
|
+
}
|
|
1082
|
+
async deleteEntityTypePermissionGrant(entityType, grantId) {
|
|
1083
|
+
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 });
|
|
1084
|
+
return rows.length > 0;
|
|
1085
|
+
}
|
|
1086
|
+
async hasEntityTypePermission(entityType, permission, subject) {
|
|
1087
|
+
const permissions = [permission, `manage`];
|
|
1088
|
+
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`(
|
|
1089
|
+
(${entityTypePermissionGrants.subjectKind} = 'principal'
|
|
1090
|
+
AND ${entityTypePermissionGrants.subjectValue} = ${subject.principalUrl})
|
|
1091
|
+
OR
|
|
1092
|
+
(${entityTypePermissionGrants.subjectKind} = 'principal_kind'
|
|
1093
|
+
AND ${entityTypePermissionGrants.subjectValue} = ${subject.principalKind})
|
|
1094
|
+
)`)).limit(1);
|
|
1095
|
+
return rows.length > 0;
|
|
1096
|
+
}
|
|
1097
|
+
async createEntityPermissionGrant(input) {
|
|
1098
|
+
return await this.db.transaction(async (tx) => {
|
|
1099
|
+
const [row] = await tx.insert(entityPermissionGrants).values({
|
|
1100
|
+
tenantId: this.tenantId,
|
|
1101
|
+
entityUrl: input.entityUrl,
|
|
1102
|
+
permission: input.permission,
|
|
1103
|
+
subjectKind: input.subjectKind,
|
|
1104
|
+
subjectValue: input.subjectValue,
|
|
1105
|
+
propagation: input.propagation ?? `self`,
|
|
1106
|
+
copyToChildren: input.copyToChildren ?? false,
|
|
1107
|
+
createdBy: input.createdBy ?? null,
|
|
1108
|
+
expiresAt: input.expiresAt ?? null
|
|
1109
|
+
}).returning();
|
|
1110
|
+
await this.materializeEntityPermissionGrant(tx, row);
|
|
1111
|
+
return this.rowToEntityPermissionGrant(row);
|
|
1112
|
+
});
|
|
1113
|
+
}
|
|
1114
|
+
async listEntityPermissionGrants(entityUrl) {
|
|
1115
|
+
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);
|
|
1116
|
+
return rows.map((row) => this.rowToEntityPermissionGrant(row));
|
|
1117
|
+
}
|
|
1118
|
+
async deleteEntityPermissionGrant(entityUrl, grantId) {
|
|
1119
|
+
return await this.db.transaction(async (tx) => {
|
|
1120
|
+
await tx.delete(entityEffectivePermissions).where((0, drizzle_orm.and)((0, drizzle_orm.eq)(entityEffectivePermissions.tenantId, this.tenantId), (0, drizzle_orm.eq)(entityEffectivePermissions.sourceGrantId, grantId)));
|
|
1121
|
+
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 });
|
|
1122
|
+
return rows.length > 0;
|
|
1123
|
+
});
|
|
1124
|
+
}
|
|
1125
|
+
async copyEntityPermissionGrantsForSpawn(parentEntityUrl, childEntityUrl, createdBy) {
|
|
1126
|
+
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())`));
|
|
1127
|
+
const copied = [];
|
|
1128
|
+
for (const grant of parentGrants) copied.push(await this.createEntityPermissionGrant({
|
|
1129
|
+
entityUrl: childEntityUrl,
|
|
1130
|
+
permission: grant.permission,
|
|
1131
|
+
subjectKind: grant.subjectKind,
|
|
1132
|
+
subjectValue: grant.subjectValue,
|
|
1133
|
+
propagation: `self`,
|
|
1134
|
+
copyToChildren: grant.copyToChildren,
|
|
1135
|
+
createdBy,
|
|
1136
|
+
expiresAt: grant.expiresAt ?? void 0
|
|
1137
|
+
}));
|
|
1138
|
+
return copied;
|
|
1139
|
+
}
|
|
1140
|
+
async hasEntityPermission(entityUrl, permission, subject) {
|
|
1141
|
+
const permissions = [permission, `manage`];
|
|
1142
|
+
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`(
|
|
1143
|
+
(${entityEffectivePermissions.subjectKind} = 'principal'
|
|
1144
|
+
AND ${entityEffectivePermissions.subjectValue} = ${subject.principalUrl})
|
|
1145
|
+
OR
|
|
1146
|
+
(${entityEffectivePermissions.subjectKind} = 'principal_kind'
|
|
1147
|
+
AND ${entityEffectivePermissions.subjectValue} = ${subject.principalKind})
|
|
1148
|
+
)`)).limit(1);
|
|
1149
|
+
return rows.length > 0;
|
|
1150
|
+
}
|
|
1151
|
+
async replaceSharedStateLink(ownerEntityUrl, manifestKey, sharedStateId) {
|
|
1152
|
+
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)));
|
|
1153
|
+
if (!sharedStateId) return;
|
|
1154
|
+
await this.db.insert(sharedStateLinks).values({
|
|
1155
|
+
tenantId: this.tenantId,
|
|
1156
|
+
ownerEntityUrl,
|
|
1157
|
+
manifestKey,
|
|
1158
|
+
sharedStateId
|
|
1159
|
+
}).onConflictDoUpdate({
|
|
1160
|
+
target: [
|
|
1161
|
+
sharedStateLinks.tenantId,
|
|
1162
|
+
sharedStateLinks.ownerEntityUrl,
|
|
1163
|
+
sharedStateLinks.manifestKey
|
|
1164
|
+
],
|
|
1165
|
+
set: {
|
|
1166
|
+
sharedStateId,
|
|
1167
|
+
updatedAt: new Date()
|
|
1168
|
+
}
|
|
1169
|
+
});
|
|
1170
|
+
}
|
|
1171
|
+
async listSharedStateLinkedEntityUrls(sharedStateId) {
|
|
1172
|
+
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)));
|
|
1173
|
+
return rows.map((row) => row.ownerEntityUrl);
|
|
1174
|
+
}
|
|
1175
|
+
async pruneExpiredPermissionGrants(now = new Date(), options = {}) {
|
|
1176
|
+
if (this.permissionPrunePromise) return await this.permissionPrunePromise;
|
|
1177
|
+
const startedAt = Date.now();
|
|
1178
|
+
if (!options.force && startedAt - this.lastPermissionPruneStartedAt < PERMISSION_PRUNE_INTERVAL_MS) return;
|
|
1179
|
+
this.lastPermissionPruneStartedAt = startedAt;
|
|
1180
|
+
const promise = this.pruneExpiredPermissionGrantsNow(now);
|
|
1181
|
+
this.permissionPrunePromise = promise;
|
|
1182
|
+
try {
|
|
1183
|
+
await promise;
|
|
1184
|
+
} catch (error) {
|
|
1185
|
+
this.lastPermissionPruneStartedAt = 0;
|
|
1186
|
+
throw error;
|
|
1187
|
+
} finally {
|
|
1188
|
+
if (this.permissionPrunePromise === promise) this.permissionPrunePromise = null;
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
async pruneExpiredPermissionGrantsNow(now) {
|
|
1192
|
+
await this.db.transaction(async (tx) => {
|
|
1193
|
+
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)));
|
|
1194
|
+
const ids = expiredEntityGrantIds.map((row) => row.id);
|
|
1195
|
+
if (ids.length > 0) {
|
|
1196
|
+
await tx.delete(entityEffectivePermissions).where((0, drizzle_orm.and)((0, drizzle_orm.eq)(entityEffectivePermissions.tenantId, this.tenantId), (0, drizzle_orm.inArray)(entityEffectivePermissions.sourceGrantId, ids)));
|
|
1197
|
+
await tx.delete(entityPermissionGrants).where((0, drizzle_orm.and)((0, drizzle_orm.eq)(entityPermissionGrants.tenantId, this.tenantId), (0, drizzle_orm.inArray)(entityPermissionGrants.id, ids)));
|
|
1198
|
+
}
|
|
1199
|
+
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)));
|
|
1200
|
+
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)));
|
|
1201
|
+
});
|
|
1202
|
+
}
|
|
1203
|
+
async materializeEntityPermissionGrant(tx, grant) {
|
|
1204
|
+
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)));
|
|
1205
|
+
if (grant.propagation === `descendants`) {
|
|
1206
|
+
await tx.execute(drizzle_orm.sql`
|
|
1207
|
+
INSERT INTO ${entityEffectivePermissions} (
|
|
1208
|
+
tenant_id,
|
|
1209
|
+
entity_url,
|
|
1210
|
+
source_entity_url,
|
|
1211
|
+
source_grant_id,
|
|
1212
|
+
permission,
|
|
1213
|
+
subject_kind,
|
|
1214
|
+
subject_value,
|
|
1215
|
+
expires_at
|
|
1216
|
+
)
|
|
1217
|
+
SELECT
|
|
1218
|
+
${this.tenantId},
|
|
1219
|
+
descendant_url,
|
|
1220
|
+
${grant.entityUrl},
|
|
1221
|
+
${grant.id},
|
|
1222
|
+
${grant.permission},
|
|
1223
|
+
${grant.subjectKind},
|
|
1224
|
+
${grant.subjectValue},
|
|
1225
|
+
${grant.expiresAt}
|
|
1226
|
+
FROM ${entityLineage}
|
|
1227
|
+
WHERE tenant_id = ${this.tenantId}
|
|
1228
|
+
AND ancestor_url = ${grant.entityUrl}
|
|
1229
|
+
ON CONFLICT DO NOTHING
|
|
1230
|
+
`);
|
|
1231
|
+
return;
|
|
1232
|
+
}
|
|
1233
|
+
await tx.insert(entityEffectivePermissions).values({
|
|
1234
|
+
tenantId: this.tenantId,
|
|
1235
|
+
entityUrl: grant.entityUrl,
|
|
1236
|
+
sourceEntityUrl: grant.entityUrl,
|
|
1237
|
+
sourceGrantId: grant.id,
|
|
1238
|
+
permission: grant.permission,
|
|
1239
|
+
subjectKind: grant.subjectKind,
|
|
1240
|
+
subjectValue: grant.subjectValue,
|
|
1241
|
+
expiresAt: grant.expiresAt
|
|
1242
|
+
}).onConflictDoNothing();
|
|
1243
|
+
}
|
|
879
1244
|
async updateStatus(entityUrl, status$4) {
|
|
880
1245
|
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
1246
|
await this.db.update(entities).set({
|
|
@@ -977,7 +1342,9 @@ var PostgresRegistry = class {
|
|
|
977
1342
|
tenantId: this.tenantId,
|
|
978
1343
|
sourceRef: row.sourceRef,
|
|
979
1344
|
tags: (0, __electric_ax_agents_runtime.normalizeTags)(row.tags),
|
|
980
|
-
streamUrl: row.streamUrl
|
|
1345
|
+
streamUrl: row.streamUrl,
|
|
1346
|
+
principalUrl: row.principalUrl,
|
|
1347
|
+
principalKind: row.principalKind
|
|
981
1348
|
}).onConflictDoNothing();
|
|
982
1349
|
const existing = await this.getEntityBridge(row.sourceRef);
|
|
983
1350
|
if (!existing) throw new Error(`Failed to load entity bridge ${row.sourceRef}`);
|
|
@@ -1132,6 +1499,7 @@ var PostgresRegistry = class {
|
|
|
1132
1499
|
creation_schema: row.creationSchema,
|
|
1133
1500
|
inbox_schemas: row.inboxSchemas,
|
|
1134
1501
|
state_schemas: row.stateSchemas,
|
|
1502
|
+
slash_commands: row.slashCommands ?? void 0,
|
|
1135
1503
|
serve_endpoint: row.serveEndpoint ?? void 0,
|
|
1136
1504
|
default_dispatch_policy: row.defaultDispatchPolicy ?? void 0,
|
|
1137
1505
|
revision: row.revision,
|
|
@@ -1139,15 +1507,40 @@ var PostgresRegistry = class {
|
|
|
1139
1507
|
updated_at: row.updatedAt
|
|
1140
1508
|
};
|
|
1141
1509
|
}
|
|
1510
|
+
rowToEntityTypePermissionGrant(row) {
|
|
1511
|
+
return {
|
|
1512
|
+
id: row.id,
|
|
1513
|
+
entity_type: row.entityType,
|
|
1514
|
+
permission: row.permission,
|
|
1515
|
+
subject_kind: row.subjectKind,
|
|
1516
|
+
subject_value: row.subjectValue,
|
|
1517
|
+
created_by: row.createdBy ?? void 0,
|
|
1518
|
+
expires_at: row.expiresAt?.toISOString(),
|
|
1519
|
+
created_at: row.createdAt.toISOString(),
|
|
1520
|
+
updated_at: row.updatedAt.toISOString()
|
|
1521
|
+
};
|
|
1522
|
+
}
|
|
1523
|
+
rowToEntityPermissionGrant(row) {
|
|
1524
|
+
return {
|
|
1525
|
+
id: row.id,
|
|
1526
|
+
entity_url: row.entityUrl,
|
|
1527
|
+
permission: row.permission,
|
|
1528
|
+
subject_kind: row.subjectKind,
|
|
1529
|
+
subject_value: row.subjectValue,
|
|
1530
|
+
propagation: row.propagation,
|
|
1531
|
+
copy_to_children: row.copyToChildren,
|
|
1532
|
+
created_by: row.createdBy ?? void 0,
|
|
1533
|
+
expires_at: row.expiresAt?.toISOString(),
|
|
1534
|
+
created_at: row.createdAt.toISOString(),
|
|
1535
|
+
updated_at: row.updatedAt.toISOString()
|
|
1536
|
+
};
|
|
1537
|
+
}
|
|
1142
1538
|
rowToEntity(row) {
|
|
1143
1539
|
return {
|
|
1144
1540
|
url: row.url,
|
|
1145
1541
|
type: row.type,
|
|
1146
1542
|
status: assertEntityStatus(row.status),
|
|
1147
|
-
streams: {
|
|
1148
|
-
main: `${row.url}/main`,
|
|
1149
|
-
error: `${row.url}/error`
|
|
1150
|
-
},
|
|
1543
|
+
streams: { main: `${row.url}/main` },
|
|
1151
1544
|
subscription_id: row.subscriptionId,
|
|
1152
1545
|
dispatch_policy: row.dispatchPolicy ?? void 0,
|
|
1153
1546
|
write_token: row.writeToken,
|
|
@@ -1169,6 +1562,8 @@ var PostgresRegistry = class {
|
|
|
1169
1562
|
sourceRef: row.sourceRef,
|
|
1170
1563
|
tags: row.tags ?? {},
|
|
1171
1564
|
streamUrl: row.streamUrl,
|
|
1565
|
+
principalUrl: row.principalUrl ?? void 0,
|
|
1566
|
+
principalKind: row.principalKind ?? void 0,
|
|
1172
1567
|
shapeHandle: row.shapeHandle ?? void 0,
|
|
1173
1568
|
shapeOffset: row.shapeOffset ?? void 0,
|
|
1174
1569
|
lastObserverActivityAt: row.lastObserverActivityAt,
|
|
@@ -1323,6 +1718,93 @@ const serverLog = {
|
|
|
1323
1718
|
}
|
|
1324
1719
|
};
|
|
1325
1720
|
|
|
1721
|
+
//#endregion
|
|
1722
|
+
//#region src/principal.ts
|
|
1723
|
+
const ELECTRIC_PRINCIPAL_HEADER = `electric-principal`;
|
|
1724
|
+
const PRINCIPAL_KINDS = new Set([
|
|
1725
|
+
`user`,
|
|
1726
|
+
`agent`,
|
|
1727
|
+
`service`,
|
|
1728
|
+
`system`
|
|
1729
|
+
]);
|
|
1730
|
+
function parsePrincipalKey(input) {
|
|
1731
|
+
const colon = input.indexOf(`:`);
|
|
1732
|
+
if (colon <= 0) throw new Error(`Invalid principal identifier`);
|
|
1733
|
+
const kind = input.slice(0, colon);
|
|
1734
|
+
const id = input.slice(colon + 1);
|
|
1735
|
+
if (!PRINCIPAL_KINDS.has(kind)) throw new Error(`Invalid principal kind`);
|
|
1736
|
+
if (!id || id.includes(`/`)) throw new Error(`Invalid principal id`);
|
|
1737
|
+
const key = `${kind}:${id}`;
|
|
1738
|
+
return {
|
|
1739
|
+
kind,
|
|
1740
|
+
id,
|
|
1741
|
+
key,
|
|
1742
|
+
url: `/principal/${encodeURIComponent(key)}`
|
|
1743
|
+
};
|
|
1744
|
+
}
|
|
1745
|
+
function principalUrl(key) {
|
|
1746
|
+
return parsePrincipalKey(key).url;
|
|
1747
|
+
}
|
|
1748
|
+
function parsePrincipalUrl(url) {
|
|
1749
|
+
if (!url.startsWith(`/principal/`)) return null;
|
|
1750
|
+
const segment = url.slice(`/principal/`.length);
|
|
1751
|
+
if (!segment || segment.includes(`/`)) return null;
|
|
1752
|
+
try {
|
|
1753
|
+
return parsePrincipalKey(decodeURIComponent(segment));
|
|
1754
|
+
} catch {
|
|
1755
|
+
return null;
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
const BUILT_IN_SYSTEM_PRINCIPAL_IDS = new Set([
|
|
1759
|
+
`framework`,
|
|
1760
|
+
`auth-sync`,
|
|
1761
|
+
`dev-local`
|
|
1762
|
+
]);
|
|
1763
|
+
function isBuiltInSystemPrincipalUrl(url) {
|
|
1764
|
+
if (!url?.startsWith(`/principal/`)) return false;
|
|
1765
|
+
try {
|
|
1766
|
+
const principal = parsePrincipalUrl(url);
|
|
1767
|
+
if (!principal) return false;
|
|
1768
|
+
return principal.kind === `system` && BUILT_IN_SYSTEM_PRINCIPAL_IDS.has(principal.id);
|
|
1769
|
+
} catch {
|
|
1770
|
+
return false;
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
function principalFromCreatedBy(createdBy) {
|
|
1774
|
+
if (!createdBy) return void 0;
|
|
1775
|
+
const principal = parsePrincipalUrl(createdBy);
|
|
1776
|
+
if (!principal) return {
|
|
1777
|
+
url: createdBy,
|
|
1778
|
+
key: null
|
|
1779
|
+
};
|
|
1780
|
+
return {
|
|
1781
|
+
url: principal.url,
|
|
1782
|
+
key: principal.key,
|
|
1783
|
+
kind: principal.kind,
|
|
1784
|
+
id: principal.id
|
|
1785
|
+
};
|
|
1786
|
+
}
|
|
1787
|
+
const principalIdentityStateSchema = __sinclair_typebox.Type.Object({
|
|
1788
|
+
kind: __sinclair_typebox.Type.Union([
|
|
1789
|
+
__sinclair_typebox.Type.Literal(`user`),
|
|
1790
|
+
__sinclair_typebox.Type.Literal(`agent`),
|
|
1791
|
+
__sinclair_typebox.Type.Literal(`service`),
|
|
1792
|
+
__sinclair_typebox.Type.Literal(`system`)
|
|
1793
|
+
]),
|
|
1794
|
+
id: __sinclair_typebox.Type.String(),
|
|
1795
|
+
key: __sinclair_typebox.Type.String(),
|
|
1796
|
+
url: __sinclair_typebox.Type.String(),
|
|
1797
|
+
updated_at: __sinclair_typebox.Type.String(),
|
|
1798
|
+
display_name: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
|
|
1799
|
+
email: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
|
|
1800
|
+
avatar_url: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
|
|
1801
|
+
auth_provider: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
|
|
1802
|
+
auth_subject: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
|
|
1803
|
+
claims: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Record(__sinclair_typebox.Type.String(), __sinclair_typebox.Type.Unknown())),
|
|
1804
|
+
created_at: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String())
|
|
1805
|
+
}, { additionalProperties: false });
|
|
1806
|
+
const principalUpdateIdentityMessageSchema = __sinclair_typebox.Type.Object({ identity: principalIdentityStateSchema }, { additionalProperties: false });
|
|
1807
|
+
|
|
1326
1808
|
//#endregion
|
|
1327
1809
|
//#region src/entity-projector.ts
|
|
1328
1810
|
const ENTITY_SHAPE_COLUMNS = [
|
|
@@ -1331,6 +1813,7 @@ const ENTITY_SHAPE_COLUMNS = [
|
|
|
1331
1813
|
`type`,
|
|
1332
1814
|
`status`,
|
|
1333
1815
|
`tags`,
|
|
1816
|
+
`created_by`,
|
|
1334
1817
|
`spawn_args`,
|
|
1335
1818
|
`sandbox`,
|
|
1336
1819
|
`parent`,
|
|
@@ -1350,6 +1833,12 @@ function sourceRefFromStreamPath(streamPath) {
|
|
|
1350
1833
|
const match = streamPath.match(/^\/_entities\/([^/]+)$/);
|
|
1351
1834
|
return match?.[1] ?? null;
|
|
1352
1835
|
}
|
|
1836
|
+
function principalScopedSourceRef(tagSourceRef, principalUrl$1, principalKind) {
|
|
1837
|
+
return `${tagSourceRef}-${(0, __electric_ax_agents_runtime.hashString)(JSON.stringify({
|
|
1838
|
+
principalKind,
|
|
1839
|
+
principalUrl: principalUrl$1
|
|
1840
|
+
}))}`;
|
|
1841
|
+
}
|
|
1353
1842
|
function sameMember(left, right) {
|
|
1354
1843
|
return JSON.stringify(left) === JSON.stringify(right);
|
|
1355
1844
|
}
|
|
@@ -1380,15 +1869,22 @@ var ProjectedEntityBridge = class {
|
|
|
1380
1869
|
sourceRef;
|
|
1381
1870
|
tags;
|
|
1382
1871
|
streamUrl;
|
|
1872
|
+
principalUrl;
|
|
1873
|
+
principalKind;
|
|
1874
|
+
permissionBypass;
|
|
1383
1875
|
currentMembers = new Map();
|
|
1384
1876
|
producer = null;
|
|
1385
1877
|
stopped = false;
|
|
1386
|
-
constructor(row, streamClient) {
|
|
1878
|
+
constructor(row, registry, streamClient) {
|
|
1879
|
+
this.registry = registry;
|
|
1387
1880
|
this.streamClient = streamClient;
|
|
1388
1881
|
this.tenantId = row.tenantId;
|
|
1389
1882
|
this.sourceRef = row.sourceRef;
|
|
1390
1883
|
this.tags = (0, __electric_ax_agents_runtime.normalizeTags)(row.tags);
|
|
1391
1884
|
this.streamUrl = row.streamUrl;
|
|
1885
|
+
this.principalUrl = row.principalUrl;
|
|
1886
|
+
this.principalKind = row.principalKind;
|
|
1887
|
+
this.permissionBypass = isBuiltInSystemPrincipalUrl(row.principalUrl);
|
|
1392
1888
|
}
|
|
1393
1889
|
async start(initialEntities) {
|
|
1394
1890
|
await this.ensureStream();
|
|
@@ -1402,7 +1898,7 @@ var ProjectedEntityBridge = class {
|
|
|
1402
1898
|
}
|
|
1403
1899
|
});
|
|
1404
1900
|
await this.loadCurrentMembers();
|
|
1405
|
-
this.reconcile(initialEntities);
|
|
1901
|
+
await this.reconcile(initialEntities);
|
|
1406
1902
|
}
|
|
1407
1903
|
async stop() {
|
|
1408
1904
|
this.stopped = true;
|
|
@@ -1414,12 +1910,13 @@ var ProjectedEntityBridge = class {
|
|
|
1414
1910
|
this.producer = null;
|
|
1415
1911
|
}
|
|
1416
1912
|
}
|
|
1417
|
-
reconcile(entities$1) {
|
|
1913
|
+
async reconcile(entities$1) {
|
|
1418
1914
|
if (this.stopped) return;
|
|
1419
1915
|
const staleMembers = new Map(this.currentMembers);
|
|
1420
1916
|
for (const entity of entities$1) {
|
|
1421
1917
|
if (entity.tenant_id !== this.tenantId) continue;
|
|
1422
1918
|
if (!entityMatchesTags(entity, this.tags)) continue;
|
|
1919
|
+
if (!await this.canReadEntity(entity)) continue;
|
|
1423
1920
|
staleMembers.delete(entity.url);
|
|
1424
1921
|
this.upsertEntity(entity);
|
|
1425
1922
|
}
|
|
@@ -1428,10 +1925,10 @@ var ProjectedEntityBridge = class {
|
|
|
1428
1925
|
this.currentMembers.delete(url);
|
|
1429
1926
|
}
|
|
1430
1927
|
}
|
|
1431
|
-
applyEntity(entity) {
|
|
1928
|
+
async applyEntity(entity) {
|
|
1432
1929
|
if (this.stopped) return;
|
|
1433
1930
|
if (entity.tenant_id !== this.tenantId) return;
|
|
1434
|
-
if (!entityMatchesTags(entity, this.tags)) {
|
|
1931
|
+
if (!entityMatchesTags(entity, this.tags) || !await this.canReadEntity(entity)) {
|
|
1435
1932
|
const existing = this.currentMembers.get(entity.url);
|
|
1436
1933
|
if (!existing) return;
|
|
1437
1934
|
this.append(`delete`, existing);
|
|
@@ -1460,6 +1957,15 @@ var ProjectedEntityBridge = class {
|
|
|
1460
1957
|
this.currentMembers.set(entity.url, next);
|
|
1461
1958
|
}
|
|
1462
1959
|
}
|
|
1960
|
+
async canReadEntity(entity) {
|
|
1961
|
+
if (this.permissionBypass) return true;
|
|
1962
|
+
if (!this.principalUrl || !this.principalKind) return false;
|
|
1963
|
+
if (entity.created_by === this.principalUrl) return true;
|
|
1964
|
+
return await this.registry.hasEntityPermission(entity.url, `read`, {
|
|
1965
|
+
principalUrl: this.principalUrl,
|
|
1966
|
+
principalKind: this.principalKind
|
|
1967
|
+
});
|
|
1968
|
+
}
|
|
1463
1969
|
async ensureStream() {
|
|
1464
1970
|
if (!await this.streamClient.exists(this.streamUrl)) await this.streamClient.create(this.streamUrl, { contentType: `application/json` });
|
|
1465
1971
|
}
|
|
@@ -1564,17 +2070,19 @@ var EntityProjector = class {
|
|
|
1564
2070
|
this.activeReaders.clear();
|
|
1565
2071
|
await Promise.all(projections.map((projection) => projection.stop()));
|
|
1566
2072
|
}
|
|
1567
|
-
async register(tenantId, registry, tagsInput) {
|
|
2073
|
+
async register(tenantId, registry, tagsInput, principalUrl$1, principalKind) {
|
|
1568
2074
|
if (!this.electricUrl) throw new Error(`[entity-projector] Electric URL is required for entities()`);
|
|
1569
2075
|
await this.start();
|
|
1570
2076
|
this.registries.set(tenantId, registry);
|
|
1571
2077
|
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);
|
|
2078
|
+
const sourceRef = principalScopedSourceRef((0, __electric_ax_agents_runtime.sourceRefForTags)(tags), principalUrl$1, principalKind);
|
|
1573
2079
|
const streamUrl = (0, __electric_ax_agents_runtime.getEntitiesStreamPath)(sourceRef);
|
|
1574
2080
|
const row = await registry.upsertEntityBridge({
|
|
1575
2081
|
sourceRef,
|
|
1576
2082
|
tags,
|
|
1577
|
-
streamUrl
|
|
2083
|
+
streamUrl,
|
|
2084
|
+
principalUrl: principalUrl$1,
|
|
2085
|
+
principalKind
|
|
1578
2086
|
});
|
|
1579
2087
|
await registry.touchEntityBridge(sourceRef);
|
|
1580
2088
|
await this.ensureProjection(row);
|
|
@@ -1603,7 +2111,11 @@ var EntityProjector = class {
|
|
|
1603
2111
|
await this.touchSourceRef(tenantId, registry, sourceRef, `read-close`);
|
|
1604
2112
|
};
|
|
1605
2113
|
}
|
|
1606
|
-
async onEntityChanged(
|
|
2114
|
+
async onEntityChanged(tenantId, entityUrl) {
|
|
2115
|
+
const entity = this.entities.get(entityKey(tenantId, entityUrl));
|
|
2116
|
+
if (!entity) return;
|
|
2117
|
+
for (const projection of this.projectionsForTenant(tenantId)) await projection.applyEntity(entity);
|
|
2118
|
+
}
|
|
1607
2119
|
async loadTenantBridges(tenantId, registry = this.registryForTenant(tenantId)) {
|
|
1608
2120
|
if (!this.started || !this.electricUrl) return;
|
|
1609
2121
|
await this.loadPersistedBridgesForTenant(tenantId, registry);
|
|
@@ -1664,16 +2176,16 @@ var EntityProjector = class {
|
|
|
1664
2176
|
}
|
|
1665
2177
|
if (message.headers.control === `up-to-date`) {
|
|
1666
2178
|
this.upToDate = true;
|
|
1667
|
-
this.reconcileAll();
|
|
2179
|
+
await this.reconcileAll();
|
|
1668
2180
|
this.readyResolve?.();
|
|
1669
2181
|
}
|
|
1670
2182
|
continue;
|
|
1671
2183
|
}
|
|
1672
2184
|
if (!(0, __electric_sql_client.isChangeMessage)(message)) continue;
|
|
1673
|
-
this.applyChangeMessage(message);
|
|
2185
|
+
await this.applyChangeMessage(message);
|
|
1674
2186
|
}
|
|
1675
2187
|
}
|
|
1676
|
-
applyChangeMessage(message) {
|
|
2188
|
+
async applyChangeMessage(message) {
|
|
1677
2189
|
const entity = message.value;
|
|
1678
2190
|
const key = entityKey(entity.tenant_id, entity.url);
|
|
1679
2191
|
if (message.headers.operation === `delete`) {
|
|
@@ -1682,7 +2194,7 @@ var EntityProjector = class {
|
|
|
1682
2194
|
return;
|
|
1683
2195
|
}
|
|
1684
2196
|
this.entities.set(key, entity);
|
|
1685
|
-
if (this.upToDate) for (const projection of this.projectionsForTenant(entity.tenant_id)) projection.applyEntity(entity);
|
|
2197
|
+
if (this.upToDate) for (const projection of this.projectionsForTenant(entity.tenant_id)) await projection.applyEntity(entity);
|
|
1686
2198
|
}
|
|
1687
2199
|
async loadPersistedBridges() {
|
|
1688
2200
|
const registry = new PostgresRegistry(this.db);
|
|
@@ -1745,7 +2257,7 @@ var EntityProjector = class {
|
|
|
1745
2257
|
}
|
|
1746
2258
|
throw error;
|
|
1747
2259
|
}
|
|
1748
|
-
const projection = new ProjectedEntityBridge(row, streamClient);
|
|
2260
|
+
const projection = new ProjectedEntityBridge(row, this.registryForTenant(row.tenantId), streamClient);
|
|
1749
2261
|
await projection.start(this.entitiesForTenant(row.tenantId));
|
|
1750
2262
|
this.projections.set(key, projection);
|
|
1751
2263
|
})().finally(() => {
|
|
@@ -1760,8 +2272,8 @@ var EntityProjector = class {
|
|
|
1760
2272
|
projectionsForTenant(tenantId) {
|
|
1761
2273
|
return [...this.projections.values()].filter((projection) => projection.tenantId === tenantId);
|
|
1762
2274
|
}
|
|
1763
|
-
reconcileAll() {
|
|
1764
|
-
for (const projection of this.projections.values()) projection.reconcile(this.entitiesForTenant(projection.tenantId));
|
|
2275
|
+
async reconcileAll() {
|
|
2276
|
+
for (const projection of this.projections.values()) await projection.reconcile(this.entitiesForTenant(projection.tenantId));
|
|
1765
2277
|
}
|
|
1766
2278
|
async touchSourceRef(tenantId, registry, sourceRef, reason) {
|
|
1767
2279
|
try {
|
|
@@ -1803,8 +2315,8 @@ var EntityProjectorTenantFacade = class {
|
|
|
1803
2315
|
await this.projector.start();
|
|
1804
2316
|
}
|
|
1805
2317
|
async stop() {}
|
|
1806
|
-
async register(tagsInput) {
|
|
1807
|
-
return await this.projector.register(this.tenantId, this.registry, tagsInput);
|
|
2318
|
+
async register(tagsInput, principalUrl$1, principalKind) {
|
|
2319
|
+
return await this.projector.register(this.tenantId, this.registry, tagsInput, principalUrl$1, principalKind);
|
|
1808
2320
|
}
|
|
1809
2321
|
async onEntityChanged(entityUrl) {
|
|
1810
2322
|
await this.projector.onEntityChanged(this.tenantId, entityUrl);
|
|
@@ -2686,93 +3198,6 @@ function assertSharedSandboxColocated(key, chosenIsRemote, dispatchPolicy) {
|
|
|
2686
3198
|
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
3199
|
}
|
|
2688
3200
|
|
|
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
3201
|
//#endregion
|
|
2777
3202
|
//#region src/manifest-side-effects.ts
|
|
2778
3203
|
function isRecord$1(value) {
|
|
@@ -2968,6 +3393,7 @@ var EntityManager = class {
|
|
|
2968
3393
|
this.validateSchema(req.creation_schema);
|
|
2969
3394
|
this.validateSchemaMap(req.inbox_schemas);
|
|
2970
3395
|
this.validateSchemaMap(req.state_schemas);
|
|
3396
|
+
this.validateSlashCommands(req.slash_commands);
|
|
2971
3397
|
const defaultDispatchPolicy = req.default_dispatch_policy ? this.validateDispatchPolicy(req.default_dispatch_policy, { label: `default_dispatch_policy` }) : void 0;
|
|
2972
3398
|
const existing = await this.registry.getEntityType(req.name);
|
|
2973
3399
|
const now = new Date().toISOString();
|
|
@@ -2977,6 +3403,7 @@ var EntityManager = class {
|
|
|
2977
3403
|
creation_schema: req.creation_schema,
|
|
2978
3404
|
inbox_schemas: req.inbox_schemas,
|
|
2979
3405
|
state_schemas: req.state_schemas,
|
|
3406
|
+
slash_commands: req.slash_commands,
|
|
2980
3407
|
serve_endpoint: req.serve_endpoint,
|
|
2981
3408
|
default_dispatch_policy: defaultDispatchPolicy,
|
|
2982
3409
|
revision: existing ? existing.revision + 1 : 1,
|
|
@@ -3008,7 +3435,10 @@ var EntityManager = class {
|
|
|
3008
3435
|
}
|
|
3009
3436
|
async ensurePrincipal(principal) {
|
|
3010
3437
|
const existing = await this.registry.getEntity(principal.url);
|
|
3011
|
-
if (existing)
|
|
3438
|
+
if (existing) {
|
|
3439
|
+
await this.ensureUserPrincipal(principal);
|
|
3440
|
+
return existing;
|
|
3441
|
+
}
|
|
3012
3442
|
await this.ensurePrincipalEntityType();
|
|
3013
3443
|
try {
|
|
3014
3444
|
const entity = await this.spawn(`principal`, {
|
|
@@ -3037,15 +3467,22 @@ var EntityManager = class {
|
|
|
3037
3467
|
updated_at: now
|
|
3038
3468
|
}
|
|
3039
3469
|
}));
|
|
3470
|
+
await this.ensureUserPrincipal(principal);
|
|
3040
3471
|
return entity;
|
|
3041
3472
|
} catch (error) {
|
|
3042
3473
|
if (error instanceof ElectricAgentsError && error.code === ErrCodeDuplicateURL) {
|
|
3043
3474
|
const raced = await this.registry.getEntity(principal.url);
|
|
3044
|
-
if (raced)
|
|
3475
|
+
if (raced) {
|
|
3476
|
+
await this.ensureUserPrincipal(principal);
|
|
3477
|
+
return raced;
|
|
3478
|
+
}
|
|
3045
3479
|
}
|
|
3046
3480
|
throw error;
|
|
3047
3481
|
}
|
|
3048
3482
|
}
|
|
3483
|
+
async ensureUserPrincipal(principal) {
|
|
3484
|
+
if (principal.kind === `user`) await this.registry.ensureUserForPrincipal(principal);
|
|
3485
|
+
}
|
|
3049
3486
|
/**
|
|
3050
3487
|
* Spawn a new entity of the given type with durable streams.
|
|
3051
3488
|
*/
|
|
@@ -3075,7 +3512,6 @@ var EntityManager = class {
|
|
|
3075
3512
|
const writeToken = (0, node_crypto.randomUUID)();
|
|
3076
3513
|
const entityURL = typeName === `principal` ? principalUrl(instanceId) : `/${typeName}/${instanceId}`;
|
|
3077
3514
|
const mainPath = `${entityURL}/main`;
|
|
3078
|
-
const errorPath = `${entityURL}/error`;
|
|
3079
3515
|
const subscriptionId = `${typeName}-handler`;
|
|
3080
3516
|
const spawnT0 = performance.now();
|
|
3081
3517
|
const existingByURL = await this.registry.getEntity(entityURL);
|
|
@@ -3092,10 +3528,7 @@ var EntityManager = class {
|
|
|
3092
3528
|
type: typeName,
|
|
3093
3529
|
status: `idle`,
|
|
3094
3530
|
url: entityURL,
|
|
3095
|
-
streams: {
|
|
3096
|
-
main: mainPath,
|
|
3097
|
-
error: errorPath
|
|
3098
|
-
},
|
|
3531
|
+
streams: { main: mainPath },
|
|
3099
3532
|
subscription_id: subscriptionId,
|
|
3100
3533
|
dispatch_policy: dispatchPolicy,
|
|
3101
3534
|
write_token: writeToken,
|
|
@@ -3132,6 +3565,18 @@ var EntityManager = class {
|
|
|
3132
3565
|
}
|
|
3133
3566
|
});
|
|
3134
3567
|
const initialEvents = [createdEvent];
|
|
3568
|
+
const slashCommandTimestamp = new Date().toISOString();
|
|
3569
|
+
for (const command of entityType.slash_commands ?? []) {
|
|
3570
|
+
const slashCommandEvent = __electric_ax_agents_runtime.entityStateSchema.slashCommands.insert({
|
|
3571
|
+
key: command.name,
|
|
3572
|
+
value: {
|
|
3573
|
+
...command,
|
|
3574
|
+
source: `static`,
|
|
3575
|
+
updated_at: slashCommandTimestamp
|
|
3576
|
+
}
|
|
3577
|
+
});
|
|
3578
|
+
initialEvents.push(slashCommandEvent);
|
|
3579
|
+
}
|
|
3135
3580
|
if (req.initialMessage !== void 0) {
|
|
3136
3581
|
const msgNow = new Date().toISOString();
|
|
3137
3582
|
const inboxEvent = __electric_ax_agents_runtime.entityStateSchema.inbox.insert({
|
|
@@ -3139,6 +3584,7 @@ var EntityManager = class {
|
|
|
3139
3584
|
value: {
|
|
3140
3585
|
from: req.created_by ?? req.parent ?? `spawn`,
|
|
3141
3586
|
payload: req.initialMessage,
|
|
3587
|
+
message_type: req.initialMessageType,
|
|
3142
3588
|
timestamp: msgNow
|
|
3143
3589
|
}
|
|
3144
3590
|
});
|
|
@@ -3148,55 +3594,43 @@ var EntityManager = class {
|
|
|
3148
3594
|
const queueEnterT0 = performance.now();
|
|
3149
3595
|
const queueWaiting = this.spawnPersistQueue.length();
|
|
3150
3596
|
const queueRunning = this.spawnPersistQueue.running();
|
|
3151
|
-
const [mainStreamResult,
|
|
3597
|
+
const [mainStreamResult, entityResult] = await this.spawnPersistQueue.push(async () => {
|
|
3152
3598
|
let entityTxid;
|
|
3153
3599
|
try {
|
|
3154
3600
|
entityTxid = await withSpan(`db.createEntity`, () => this.registry.createEntity(entityData));
|
|
3155
3601
|
} catch (err) {
|
|
3156
|
-
return [
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
value: void 0
|
|
3164
|
-
},
|
|
3165
|
-
{
|
|
3166
|
-
status: `rejected`,
|
|
3167
|
-
reason: err
|
|
3168
|
-
}
|
|
3169
|
-
];
|
|
3602
|
+
return [{
|
|
3603
|
+
status: `fulfilled`,
|
|
3604
|
+
value: void 0
|
|
3605
|
+
}, {
|
|
3606
|
+
status: `rejected`,
|
|
3607
|
+
reason: err
|
|
3608
|
+
}];
|
|
3170
3609
|
}
|
|
3171
|
-
const [mainStreamResult$1
|
|
3610
|
+
const [mainStreamResult$1] = await Promise.allSettled([this.streamClient.create(mainPath, {
|
|
3172
3611
|
contentType,
|
|
3173
3612
|
body: initialBody
|
|
3174
|
-
})
|
|
3175
|
-
return [
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
status: `fulfilled`,
|
|
3180
|
-
value: entityTxid
|
|
3181
|
-
}
|
|
3182
|
-
];
|
|
3613
|
+
})]);
|
|
3614
|
+
return [mainStreamResult$1, {
|
|
3615
|
+
status: `fulfilled`,
|
|
3616
|
+
value: entityTxid
|
|
3617
|
+
}];
|
|
3183
3618
|
});
|
|
3184
3619
|
const parallelMs = +(performance.now() - queueEnterT0).toFixed(2);
|
|
3185
|
-
if (mainStreamResult.status === `rejected` ||
|
|
3620
|
+
if (mainStreamResult.status === `rejected` || entityResult.status === `rejected`) {
|
|
3186
3621
|
const entityReason = entityResult.status === `rejected` ? entityResult.reason : null;
|
|
3187
|
-
const streamReason = mainStreamResult.status === `rejected` ? mainStreamResult.reason :
|
|
3622
|
+
const streamReason = mainStreamResult.status === `rejected` ? mainStreamResult.reason : null;
|
|
3188
3623
|
const isDuplicate = entityReason instanceof EntityAlreadyExistsError;
|
|
3189
3624
|
const isStreamConflict = !!streamReason && typeof streamReason === `object` && (`status` in streamReason && streamReason.status === 409 || `code` in streamReason && streamReason.code === `CONFLICT_SEQ`);
|
|
3190
3625
|
const rollbacks = [];
|
|
3191
3626
|
if (!isDuplicate && !isStreamConflict) {
|
|
3192
3627
|
if (mainStreamResult.status === `fulfilled`) rollbacks.push(this.streamClient.delete(mainPath));
|
|
3193
|
-
if (errorStreamResult.status === `fulfilled`) rollbacks.push(this.streamClient.delete(errorPath));
|
|
3194
3628
|
if (entityResult.status === `fulfilled`) rollbacks.push(this.registry.deleteEntity(entityURL));
|
|
3195
3629
|
if (req.wake) rollbacks.push(this.wakeRegistry.unregisterBySubscriberAndSource(req.wake.subscriberUrl, entityURL, this.tenantId));
|
|
3196
3630
|
await Promise.allSettled(rollbacks);
|
|
3197
3631
|
}
|
|
3198
3632
|
if (isDuplicate || isStreamConflict) throw new ElectricAgentsError(ErrCodeDuplicateURL, `Entity already exists at URL "${entityURL}"`, 409);
|
|
3199
|
-
const failure = mainStreamResult.status === `rejected` ? mainStreamResult.reason :
|
|
3633
|
+
const failure = mainStreamResult.status === `rejected` ? mainStreamResult.reason : entityResult.reason;
|
|
3200
3634
|
if (failure instanceof Error) throw failure;
|
|
3201
3635
|
throw new ElectricAgentsError(`SPAWN_FAILED`, `Spawn failed: ${String(failure)}`, 500);
|
|
3202
3636
|
}
|
|
@@ -3281,7 +3715,7 @@ var EntityManager = class {
|
|
|
3281
3715
|
});
|
|
3282
3716
|
const sharedStateIdMap = await this.buildForkSharedStateIdMap(snapshot.sharedStateIds, suffix);
|
|
3283
3717
|
const stringMap = this.buildForkStringMap(entityUrlMap, sharedStateIdMap);
|
|
3284
|
-
const entityPlans = this.buildForkEntityPlans(effectiveSubtree, entityUrlMap, stringMap);
|
|
3718
|
+
const entityPlans = this.buildForkEntityPlans(effectiveSubtree, entityUrlMap, stringMap, opts.createdBy);
|
|
3285
3719
|
this.addForkLocks(this.forkWriteLockedEntities, effectiveSubtree.map((entity) => entity.url), writeEntityLocks);
|
|
3286
3720
|
this.addForkLocks(this.forkWriteLockedStreams, [...snapshot.sharedStateIds].map((id) => (0, __electric_ax_agents_runtime.getSharedStateStreamPath)(id)), writeStreamLocks);
|
|
3287
3721
|
const createdStreams = [];
|
|
@@ -3292,8 +3726,6 @@ var EntityManager = class {
|
|
|
3292
3726
|
const isRoot = plan.source.url === rootUrl;
|
|
3293
3727
|
await this.streamClient.fork(plan.fork.streams.main, plan.source.streams.main, isRoot && opts.forkPointer ? { forkPointer: opts.forkPointer } : void 0);
|
|
3294
3728
|
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
3729
|
}
|
|
3298
3730
|
for (const [sourceId, forkId] of sharedStateIdMap) {
|
|
3299
3731
|
const sourcePath = (0, __electric_ax_agents_runtime.getSharedStateStreamPath)(sourceId);
|
|
@@ -3627,7 +4059,6 @@ var EntityManager = class {
|
|
|
3627
4059
|
for (const [sourceUrl, forkUrl] of entityUrlMap) {
|
|
3628
4060
|
stringMap.set(sourceUrl, forkUrl);
|
|
3629
4061
|
stringMap.set(`${sourceUrl}/main`, `${forkUrl}/main`);
|
|
3630
|
-
stringMap.set(`${sourceUrl}/error`, `${forkUrl}/error`);
|
|
3631
4062
|
}
|
|
3632
4063
|
for (const [sourceId, forkId] of sharedStateIdMap) {
|
|
3633
4064
|
stringMap.set(sourceId, forkId);
|
|
@@ -3635,7 +4066,7 @@ var EntityManager = class {
|
|
|
3635
4066
|
}
|
|
3636
4067
|
return stringMap;
|
|
3637
4068
|
}
|
|
3638
|
-
buildForkEntityPlans(entitiesToFork, entityUrlMap, stringMap) {
|
|
4069
|
+
buildForkEntityPlans(entitiesToFork, entityUrlMap, stringMap, createdBy) {
|
|
3639
4070
|
const now = Date.now();
|
|
3640
4071
|
return entitiesToFork.map((source) => {
|
|
3641
4072
|
const forkUrl = entityUrlMap.get(source.url);
|
|
@@ -3648,14 +4079,12 @@ var EntityManager = class {
|
|
|
3648
4079
|
url: forkUrl,
|
|
3649
4080
|
type,
|
|
3650
4081
|
status: `idle`,
|
|
3651
|
-
streams: {
|
|
3652
|
-
main: `${forkUrl}/main`,
|
|
3653
|
-
error: `${forkUrl}/error`
|
|
3654
|
-
},
|
|
4082
|
+
streams: { main: `${forkUrl}/main` },
|
|
3655
4083
|
subscription_id: `${type}-handler`,
|
|
3656
4084
|
write_token: (0, node_crypto.randomUUID)(),
|
|
3657
4085
|
spawn_args: spawnArgs,
|
|
3658
4086
|
parent,
|
|
4087
|
+
created_by: createdBy ?? source.created_by,
|
|
3659
4088
|
created_at: now,
|
|
3660
4089
|
updated_at: now
|
|
3661
4090
|
};
|
|
@@ -3889,7 +4318,7 @@ var EntityManager = class {
|
|
|
3889
4318
|
}
|
|
3890
4319
|
async materializeForkManifestSideEffects(entityUrl, manifests) {
|
|
3891
4320
|
for (const [manifestKey, manifest] of manifests) {
|
|
3892
|
-
await this.
|
|
4321
|
+
await this.syncManifestLinks(entityUrl, manifestKey, `upsert`, manifest);
|
|
3893
4322
|
const wake = buildManifestWakeRegistration(entityUrl, manifest, manifestKey);
|
|
3894
4323
|
if (wake) await this.wakeRegistry.register({
|
|
3895
4324
|
...wake,
|
|
@@ -3919,6 +4348,7 @@ var EntityManager = class {
|
|
|
3919
4348
|
await this.scheduler.syncManifestDelayedSend(ownerEntityUrl, manifestKey, {
|
|
3920
4349
|
entityUrl: targetUrl,
|
|
3921
4350
|
from: senderUrl,
|
|
4351
|
+
from_agent: senderUrl,
|
|
3922
4352
|
payload: manifest.payload,
|
|
3923
4353
|
key: `scheduled-${producerId}`,
|
|
3924
4354
|
type: typeof manifest.messageType === `string` ? manifest.messageType : void 0,
|
|
@@ -3958,12 +4388,14 @@ var EntityManager = class {
|
|
|
3958
4388
|
const now = new Date().toISOString();
|
|
3959
4389
|
const key = req.key ?? `msg-in-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
3960
4390
|
const value = {
|
|
3961
|
-
from: req.from,
|
|
4391
|
+
from: req.from_principal ?? req.from,
|
|
3962
4392
|
payload: req.payload,
|
|
3963
4393
|
timestamp: now,
|
|
3964
4394
|
mode: req.mode ?? `immediate`,
|
|
3965
4395
|
status: req.mode === `queued` || req.mode === `paused` ? `pending` : `processed`
|
|
3966
4396
|
};
|
|
4397
|
+
if (req.from_principal) value.from_principal = req.from_principal;
|
|
4398
|
+
if (req.from_agent) value.from_agent = req.from_agent;
|
|
3967
4399
|
if (req.type) value.message_type = req.type;
|
|
3968
4400
|
if (req.position) value.position = req.position;
|
|
3969
4401
|
else if (value.mode === `queued` || value.mode === `paused`) value.position = createInitialQueuePosition(new Date(now));
|
|
@@ -4135,9 +4567,9 @@ var EntityManager = class {
|
|
|
4135
4567
|
if (result.changed && this.entityBridgeManager) await this.entityBridgeManager.onEntityChanged(entityUrl);
|
|
4136
4568
|
return updated;
|
|
4137
4569
|
}
|
|
4138
|
-
async ensureEntitiesMembershipStream(tags) {
|
|
4570
|
+
async ensureEntitiesMembershipStream(tags, principal) {
|
|
4139
4571
|
if (!this.entityBridgeManager) throw new Error(`Entity bridge manager not configured`);
|
|
4140
|
-
return this.entityBridgeManager.register(this.validateTags(tags));
|
|
4572
|
+
return this.entityBridgeManager.register(this.validateTags(tags), principal.url, principal.kind);
|
|
4141
4573
|
}
|
|
4142
4574
|
async writeManifestEntry(entityUrl, key, operation, value, opts) {
|
|
4143
4575
|
const entity = await this.registry.getEntity(entityUrl);
|
|
@@ -4155,11 +4587,11 @@ var EntityManager = class {
|
|
|
4155
4587
|
const encoded = this.encodeChangeEvent(event);
|
|
4156
4588
|
if (opts?.producerId) {
|
|
4157
4589
|
await this.streamClient.appendIdempotent(entity.streams.main, encoded, { producerId: opts.producerId });
|
|
4158
|
-
await this.
|
|
4590
|
+
await this.syncManifestLinks(entityUrl, key, operation, value);
|
|
4159
4591
|
return;
|
|
4160
4592
|
}
|
|
4161
4593
|
await this.streamClient.append(entity.streams.main, encoded);
|
|
4162
|
-
await this.
|
|
4594
|
+
await this.syncManifestLinks(entityUrl, key, operation, value);
|
|
4163
4595
|
}
|
|
4164
4596
|
async upsertCronSchedule(entityUrl, req) {
|
|
4165
4597
|
if (req.payload === void 0) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Missing required field: payload`, 400);
|
|
@@ -4308,6 +4740,8 @@ var EntityManager = class {
|
|
|
4308
4740
|
await this.scheduler.enqueueDelayedSend({
|
|
4309
4741
|
entityUrl,
|
|
4310
4742
|
from: req.from,
|
|
4743
|
+
from_principal: req.from_principal,
|
|
4744
|
+
from_agent: req.from_agent,
|
|
4311
4745
|
payload: req.payload,
|
|
4312
4746
|
key: req.key,
|
|
4313
4747
|
type: req.type,
|
|
@@ -4350,14 +4784,23 @@ var EntityManager = class {
|
|
|
4350
4784
|
await this.streamClient.appendIdempotent(subscriber.streams.main, this.encodeChangeEvent(wakeEvent), { producerId: `wake-reg-${result.registrationDbId}-${result.sourceEventKey}` });
|
|
4351
4785
|
});
|
|
4352
4786
|
}
|
|
4353
|
-
async
|
|
4787
|
+
async syncManifestLinks(entityUrl, manifestKey, operation, value) {
|
|
4354
4788
|
const sourceRef = operation === `delete` ? void 0 : this.extractEntitiesSourceRef(value);
|
|
4355
4789
|
await this.registry.replaceEntityManifestSource(entityUrl, manifestKey, sourceRef);
|
|
4790
|
+
const sharedStateId = operation === `delete` ? void 0 : this.extractSharedStateId(value);
|
|
4791
|
+
await this.registry.replaceSharedStateLink(entityUrl, manifestKey, sharedStateId);
|
|
4356
4792
|
}
|
|
4357
4793
|
extractEntitiesSourceRef(manifest) {
|
|
4358
4794
|
if (manifest?.kind === `source` && manifest.sourceType === `entities` && typeof manifest.sourceRef === `string`) return manifest.sourceRef;
|
|
4359
4795
|
return void 0;
|
|
4360
4796
|
}
|
|
4797
|
+
extractSharedStateId(manifest) {
|
|
4798
|
+
if (manifest?.kind === `shared-state` && typeof manifest.id === `string`) return manifest.id;
|
|
4799
|
+
if (manifest?.kind !== `source` || manifest.sourceType !== `db`) return void 0;
|
|
4800
|
+
if (typeof manifest.sourceRef === `string`) return manifest.sourceRef;
|
|
4801
|
+
const config = isRecord(manifest.config) ? manifest.config : void 0;
|
|
4802
|
+
return typeof config?.id === `string` ? config.id : void 0;
|
|
4803
|
+
}
|
|
4361
4804
|
/**
|
|
4362
4805
|
* Read a child entity's stream and extract concatenated text deltas
|
|
4363
4806
|
* for a specific run, plus any error messages for that run.
|
|
@@ -4521,14 +4964,7 @@ var EntityManager = class {
|
|
|
4521
4964
|
await this.streamClient.append(entity.streams.main, signalData);
|
|
4522
4965
|
return;
|
|
4523
4966
|
}
|
|
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 {
|
|
4967
|
+
for (const [streamPath, data] of [[entity.streams.main, signalData]]) try {
|
|
4532
4968
|
await this.streamClient.append(streamPath, data, { close: true });
|
|
4533
4969
|
} catch (err) {
|
|
4534
4970
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -4594,7 +5030,9 @@ var EntityManager = class {
|
|
|
4594
5030
|
creation_schema: existing.creation_schema,
|
|
4595
5031
|
inbox_schemas: mergedInbox,
|
|
4596
5032
|
state_schemas: mergedState,
|
|
5033
|
+
slash_commands: existing.slash_commands,
|
|
4597
5034
|
serve_endpoint: existing.serve_endpoint,
|
|
5035
|
+
default_dispatch_policy: existing.default_dispatch_policy,
|
|
4598
5036
|
revision: nextRevision,
|
|
4599
5037
|
created_at: existing.created_at,
|
|
4600
5038
|
updated_at: now
|
|
@@ -4648,11 +5086,19 @@ var EntityManager = class {
|
|
|
4648
5086
|
throw new ElectricAgentsError(ErrCodeInvalidRequest, error instanceof Error ? error.message : `Invalid tags`, 400);
|
|
4649
5087
|
}
|
|
4650
5088
|
}
|
|
5089
|
+
validateSlashCommands(input) {
|
|
5090
|
+
const validationError = (0, __electric_ax_agents_runtime.validateSlashCommandDefinitions)(input);
|
|
5091
|
+
if (!validationError) return;
|
|
5092
|
+
throw new ElectricAgentsError(ErrCodeSchemaValidationFailed, validationError.message, 422, validationError.details);
|
|
5093
|
+
}
|
|
4651
5094
|
async validateSendRequest(entityUrl, req) {
|
|
4652
5095
|
const entity = await this.registry.getEntity(entityUrl);
|
|
4653
5096
|
if (!entity) throw new ElectricAgentsError(ErrCodeNotFound, `Entity not found`, 404);
|
|
4654
5097
|
if (rejectsNormalWrites(entity.status)) throw new ElectricAgentsError(ErrCodeNotRunning, `Entity is not accepting writes`, 409);
|
|
4655
|
-
if (req.type
|
|
5098
|
+
if (req.type === __electric_ax_agents_runtime.COMPOSER_INPUT_MESSAGE_TYPE) {
|
|
5099
|
+
const valErr = (0, __electric_ax_agents_runtime.validateComposerInputPayload)(req.payload);
|
|
5100
|
+
if (valErr) throw new ElectricAgentsError(ErrCodeSchemaValidationFailed, valErr.message, 422, valErr.details);
|
|
5101
|
+
} else if (req.type && entity.type) {
|
|
4656
5102
|
const { inboxSchemas } = await this.getEffectiveSchemas(entity);
|
|
4657
5103
|
if (inboxSchemas) {
|
|
4658
5104
|
const schema = inboxSchemas[req.type];
|
|
@@ -5511,6 +5957,8 @@ var ElectricAgentsTenantRuntime = class {
|
|
|
5511
5957
|
try {
|
|
5512
5958
|
await this.manager.send(payload.entityUrl, {
|
|
5513
5959
|
from: payload.from,
|
|
5960
|
+
from_principal: payload.from_principal,
|
|
5961
|
+
from_agent: payload.from_agent,
|
|
5514
5962
|
payload: payload.payload,
|
|
5515
5963
|
key: payload.key ?? `scheduled-task-${taskId}`,
|
|
5516
5964
|
type: payload.type
|
|
@@ -5583,6 +6031,7 @@ var ElectricAgentsTenantRuntime = class {
|
|
|
5583
6031
|
await this.scheduler.syncManifestDelayedSend(ownerEntityUrl, manifestKey, {
|
|
5584
6032
|
entityUrl: targetUrl,
|
|
5585
6033
|
from: senderUrl,
|
|
6034
|
+
from_agent: senderUrl,
|
|
5586
6035
|
payload: value.payload,
|
|
5587
6036
|
key: `scheduled-${producerId}`,
|
|
5588
6037
|
type: typeof value.messageType === `string` ? value.messageType : void 0,
|
|
@@ -5607,11 +6056,20 @@ var ElectricAgentsTenantRuntime = class {
|
|
|
5607
6056
|
async applyManifestEntitySource(ownerEntityUrl, manifestKey, operation, value) {
|
|
5608
6057
|
const sourceRef = operation === `delete` ? void 0 : this.extractEntitiesSourceRef(value);
|
|
5609
6058
|
await this.manager.registry.replaceEntityManifestSource(ownerEntityUrl, manifestKey, sourceRef);
|
|
6059
|
+
const sharedStateId = operation === `delete` ? void 0 : this.extractSharedStateId(value);
|
|
6060
|
+
await this.manager.registry.replaceSharedStateLink(ownerEntityUrl, manifestKey, sharedStateId);
|
|
5610
6061
|
}
|
|
5611
6062
|
extractEntitiesSourceRef(manifest) {
|
|
5612
6063
|
if (manifest?.kind === `source` && manifest.sourceType === `entities` && typeof manifest.sourceRef === `string`) return manifest.sourceRef;
|
|
5613
6064
|
return void 0;
|
|
5614
6065
|
}
|
|
6066
|
+
extractSharedStateId(manifest) {
|
|
6067
|
+
if (manifest?.kind === `shared-state` && typeof manifest.id === `string`) return manifest.id;
|
|
6068
|
+
if (manifest?.kind !== `source` || manifest.sourceType !== `db`) return void 0;
|
|
6069
|
+
if (typeof manifest.sourceRef === `string`) return manifest.sourceRef;
|
|
6070
|
+
const config = typeof manifest.config === `object` && manifest.config !== null && !Array.isArray(manifest.config) ? manifest.config : void 0;
|
|
6071
|
+
return typeof config?.id === `string` ? config.id : void 0;
|
|
6072
|
+
}
|
|
5615
6073
|
async maybeMarkEntityIdleAfterRunFinished(entityUrl) {
|
|
5616
6074
|
const primaryStream = `${entityUrl}/main`;
|
|
5617
6075
|
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 +6750,8 @@ var WakeRegistry = class {
|
|
|
6292
6750
|
if (eventType === `inbox`) {
|
|
6293
6751
|
const value = event.value;
|
|
6294
6752
|
if (typeof value?.from === `string`) change.from = value.from;
|
|
6753
|
+
if (typeof value?.from_principal === `string`) change.from_principal = value.from_principal;
|
|
6754
|
+
if (typeof value?.from_agent === `string`) change.from_agent = value.from_agent;
|
|
6295
6755
|
if (`payload` in (value ?? {})) change.payload = value?.payload;
|
|
6296
6756
|
if (typeof value?.timestamp === `string`) change.timestamp = value.timestamp;
|
|
6297
6757
|
if (typeof value?.message_type === `string`) change.message_type = value.message_type;
|
|
@@ -6703,29 +7163,136 @@ function buildElectricProxyTarget(options) {
|
|
|
6703
7163
|
if (options.electricSecret) target.searchParams.set(`secret`, options.electricSecret);
|
|
6704
7164
|
const table = options.incomingUrl.searchParams.get(`table`);
|
|
6705
7165
|
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
|
-
|
|
7166
|
+
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"`);
|
|
7167
|
+
applyShapeWhere(target, buildReadableEntitiesWhere({
|
|
7168
|
+
tenantId: options.tenantId,
|
|
7169
|
+
principalUrl: options.principalUrl ?? ``,
|
|
7170
|
+
principalKind: options.principalKind ?? ``,
|
|
7171
|
+
permissionBypass: options.permissionBypass
|
|
7172
|
+
}));
|
|
6708
7173
|
} else if (table === `entity_types`) {
|
|
6709
|
-
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
|
-
|
|
7174
|
+
target.searchParams.set(`columns`, `"tenant_id","name","description","creation_schema","inbox_schemas","state_schemas","slash_commands","serve_endpoint","default_dispatch_policy","revision","created_at","updated_at"`);
|
|
7175
|
+
applyShapeWhere(target, buildSpawnableEntityTypesWhere({
|
|
7176
|
+
tenantId: options.tenantId,
|
|
7177
|
+
principalUrl: options.principalUrl ?? ``,
|
|
7178
|
+
principalKind: options.principalKind ?? ``,
|
|
7179
|
+
permissionBypass: options.permissionBypass
|
|
7180
|
+
}));
|
|
6711
7181
|
} else if (table === `runners`) {
|
|
6712
7182
|
target.searchParams.set(`columns`, `"tenant_id","id","owner_principal","label","kind","admin_status","wake_stream","sandbox_profiles","created_at","updated_at"`);
|
|
6713
7183
|
applyTenantShapeWhere(target, options.tenantId, [`owner_principal = ${sqlStringLiteral(options.principalUrl ?? ``)}`]);
|
|
7184
|
+
} else if (table === `users`) {
|
|
7185
|
+
target.searchParams.set(`columns`, `"tenant_id","id","display_name","email","avatar_url","created_at","updated_at"`);
|
|
7186
|
+
applyTenantShapeWhere(target, options.tenantId);
|
|
7187
|
+
} else if (table === `entity_effective_permissions`) {
|
|
7188
|
+
target.searchParams.set(`columns`, `"tenant_id","id","entity_url","source_entity_url","source_grant_id","permission","subject_kind","subject_value","expires_at","created_at"`);
|
|
7189
|
+
applyShapeWhere(target, buildCurrentPrincipalEntityEffectivePermissionsWhere({
|
|
7190
|
+
tenantId: options.tenantId,
|
|
7191
|
+
principalUrl: options.principalUrl ?? ``,
|
|
7192
|
+
principalKind: options.principalKind ?? ``,
|
|
7193
|
+
permissionBypass: options.permissionBypass
|
|
7194
|
+
}));
|
|
6714
7195
|
} else if (table === `runner_runtime_diagnostics`) {
|
|
6715
7196
|
target.searchParams.set(`columns`, `"tenant_id","runner_id","owner_principal","wake_stream_offset","last_seen_at","liveness_lease_expires_at","diagnostics","updated_at"`);
|
|
6716
7197
|
applyTenantShapeWhere(target, options.tenantId, [`owner_principal = ${sqlStringLiteral(options.principalUrl ?? ``)}`]);
|
|
6717
7198
|
} else if (table === `entity_dispatch_state`) {
|
|
6718
7199
|
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
|
-
|
|
7200
|
+
applyShapeWhere(target, buildReadableEntityUrlWhere({
|
|
7201
|
+
tenantId: options.tenantId,
|
|
7202
|
+
principalUrl: options.principalUrl ?? ``,
|
|
7203
|
+
principalKind: options.principalKind ?? ``,
|
|
7204
|
+
permissionBypass: options.permissionBypass
|
|
7205
|
+
}));
|
|
6720
7206
|
} else if (table === `wake_notifications`) {
|
|
6721
7207
|
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
|
-
|
|
7208
|
+
applyShapeWhere(target, buildReadableEntityUrlWhere({
|
|
7209
|
+
tenantId: options.tenantId,
|
|
7210
|
+
principalUrl: options.principalUrl ?? ``,
|
|
7211
|
+
principalKind: options.principalKind ?? ``,
|
|
7212
|
+
permissionBypass: options.permissionBypass
|
|
7213
|
+
}));
|
|
6723
7214
|
} else if (table === `consumer_claims`) {
|
|
6724
7215
|
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
|
-
|
|
7216
|
+
applyShapeWhere(target, buildReadableEntityUrlWhere({
|
|
7217
|
+
tenantId: options.tenantId,
|
|
7218
|
+
principalUrl: options.principalUrl ?? ``,
|
|
7219
|
+
principalKind: options.principalKind ?? ``,
|
|
7220
|
+
permissionBypass: options.permissionBypass
|
|
7221
|
+
}));
|
|
6726
7222
|
}
|
|
6727
7223
|
return target;
|
|
6728
7224
|
}
|
|
7225
|
+
function buildReadableEntitiesWhere(options) {
|
|
7226
|
+
const tenant = sqlStringLiteral(options.tenantId);
|
|
7227
|
+
if (options.permissionBypass) return `tenant_id = ${tenant}`;
|
|
7228
|
+
const principalUrl$1 = sqlStringLiteral(options.principalUrl);
|
|
7229
|
+
const principalKind = sqlStringLiteral(options.principalKind);
|
|
7230
|
+
return [
|
|
7231
|
+
`tenant_id = ${tenant}`,
|
|
7232
|
+
`AND (`,
|
|
7233
|
+
` created_by = ${principalUrl$1}`,
|
|
7234
|
+
` OR url IN (`,
|
|
7235
|
+
` SELECT entity_url`,
|
|
7236
|
+
` FROM entity_effective_permissions`,
|
|
7237
|
+
` WHERE tenant_id = ${tenant}`,
|
|
7238
|
+
` AND permission IN ('read', 'manage')`,
|
|
7239
|
+
` AND (`,
|
|
7240
|
+
` (subject_kind = 'principal' AND subject_value = ${principalUrl$1})`,
|
|
7241
|
+
` OR (subject_kind = 'principal_kind' AND subject_value = ${principalKind})`,
|
|
7242
|
+
` )`,
|
|
7243
|
+
` )`,
|
|
7244
|
+
`)`
|
|
7245
|
+
].join(`\n`);
|
|
7246
|
+
}
|
|
7247
|
+
function buildReadableEntityUrlWhere(options) {
|
|
7248
|
+
const tenant = sqlStringLiteral(options.tenantId);
|
|
7249
|
+
if (options.permissionBypass) return `tenant_id = ${tenant}`;
|
|
7250
|
+
return [
|
|
7251
|
+
`tenant_id = ${tenant}`,
|
|
7252
|
+
`AND entity_url IN (`,
|
|
7253
|
+
` SELECT url`,
|
|
7254
|
+
` FROM entities`,
|
|
7255
|
+
` WHERE ${indentWhere(buildReadableEntitiesWhere(options), ` `).trimStart()}`,
|
|
7256
|
+
`)`
|
|
7257
|
+
].join(`\n`);
|
|
7258
|
+
}
|
|
7259
|
+
function buildCurrentPrincipalEntityEffectivePermissionsWhere(options) {
|
|
7260
|
+
const tenant = sqlStringLiteral(options.tenantId);
|
|
7261
|
+
if (options.permissionBypass) return `tenant_id = ${tenant}`;
|
|
7262
|
+
const principalUrl$1 = sqlStringLiteral(options.principalUrl);
|
|
7263
|
+
const principalKind = sqlStringLiteral(options.principalKind);
|
|
7264
|
+
return [
|
|
7265
|
+
`tenant_id = ${tenant}`,
|
|
7266
|
+
`AND (`,
|
|
7267
|
+
` (subject_kind = 'principal' AND subject_value = ${principalUrl$1})`,
|
|
7268
|
+
` OR (subject_kind = 'principal_kind' AND subject_value = ${principalKind})`,
|
|
7269
|
+
`)`,
|
|
7270
|
+
`AND entity_url IN (`,
|
|
7271
|
+
` SELECT url`,
|
|
7272
|
+
` FROM entities`,
|
|
7273
|
+
` WHERE ${buildReadableEntitiesWhere(options)}`,
|
|
7274
|
+
`)`
|
|
7275
|
+
].join(`\n`);
|
|
7276
|
+
}
|
|
7277
|
+
function buildSpawnableEntityTypesWhere(options) {
|
|
7278
|
+
const tenant = sqlStringLiteral(options.tenantId);
|
|
7279
|
+
if (options.permissionBypass) return `tenant_id = ${tenant}`;
|
|
7280
|
+
const principalUrl$1 = sqlStringLiteral(options.principalUrl);
|
|
7281
|
+
const principalKind = sqlStringLiteral(options.principalKind);
|
|
7282
|
+
return [
|
|
7283
|
+
`tenant_id = ${tenant}`,
|
|
7284
|
+
`AND name IN (`,
|
|
7285
|
+
` SELECT entity_type`,
|
|
7286
|
+
` FROM entity_type_permission_grants`,
|
|
7287
|
+
` WHERE tenant_id = ${tenant}`,
|
|
7288
|
+
` AND permission IN ('spawn', 'manage')`,
|
|
7289
|
+
` AND (`,
|
|
7290
|
+
` (subject_kind = 'principal' AND subject_value = ${principalUrl$1})`,
|
|
7291
|
+
` OR (subject_kind = 'principal_kind' AND subject_value = ${principalKind})`,
|
|
7292
|
+
` )`,
|
|
7293
|
+
`)`
|
|
7294
|
+
].join(`\n`);
|
|
7295
|
+
}
|
|
6729
7296
|
async function forwardFetchRequest(options) {
|
|
6730
7297
|
const routingAdapter = resolveDurableStreamsRoutingAdapter(options.durableStreamsRouting, options.durableStreamsUrl);
|
|
6731
7298
|
const routingInput = {
|
|
@@ -6760,13 +7327,170 @@ function decodeJsonObject(body) {
|
|
|
6760
7327
|
return null;
|
|
6761
7328
|
}
|
|
6762
7329
|
function applyTenantShapeWhere(target, tenantId, extraConditions = []) {
|
|
6763
|
-
|
|
7330
|
+
applyShapeWhere(target, [`tenant_id = ${sqlStringLiteral(tenantId)}`, ...extraConditions].join(` AND `));
|
|
7331
|
+
}
|
|
7332
|
+
function applyShapeWhere(target, enforcedWhere) {
|
|
6764
7333
|
const existingWhere = target.searchParams.get(`where`);
|
|
6765
|
-
target.searchParams.set(`where`, existingWhere ? `${
|
|
7334
|
+
target.searchParams.set(`where`, existingWhere ? `${enforcedWhere} AND (${existingWhere})` : enforcedWhere);
|
|
6766
7335
|
}
|
|
6767
7336
|
function sqlStringLiteral(value) {
|
|
6768
7337
|
return `'${value.replace(/'/g, `''`)}'`;
|
|
6769
7338
|
}
|
|
7339
|
+
function indentWhere(where, prefix) {
|
|
7340
|
+
return where.split(`\n`).map((line) => `${prefix}${line}`).join(`\n`);
|
|
7341
|
+
}
|
|
7342
|
+
|
|
7343
|
+
//#endregion
|
|
7344
|
+
//#region src/permissions.ts
|
|
7345
|
+
const authzDecisionCache = new WeakMap();
|
|
7346
|
+
function principalSubject(principal) {
|
|
7347
|
+
return {
|
|
7348
|
+
principalUrl: principal.url,
|
|
7349
|
+
principalKind: principal.kind
|
|
7350
|
+
};
|
|
7351
|
+
}
|
|
7352
|
+
function isPermissionBypassPrincipal(ctx) {
|
|
7353
|
+
return isBuiltInSystemPrincipalUrl(ctx.principal.url);
|
|
7354
|
+
}
|
|
7355
|
+
async function canAccessEntity(ctx, entity, permission, request) {
|
|
7356
|
+
if (isPermissionBypassPrincipal(ctx)) return true;
|
|
7357
|
+
await ctx.entityManager.registry.pruneExpiredPermissionGrants?.();
|
|
7358
|
+
const builtInAllowed = entity.created_by === ctx.principal.url || await ctx.entityManager.registry.hasEntityPermission(entity.url, permission, principalSubject(ctx.principal));
|
|
7359
|
+
return await applyAuthorizationHook(ctx, {
|
|
7360
|
+
verb: permission,
|
|
7361
|
+
resourceKey: `entity:${entity.url}`,
|
|
7362
|
+
resource: {
|
|
7363
|
+
kind: `entity`,
|
|
7364
|
+
entity
|
|
7365
|
+
},
|
|
7366
|
+
builtInAllowed,
|
|
7367
|
+
request
|
|
7368
|
+
});
|
|
7369
|
+
}
|
|
7370
|
+
async function canAccessEntityType(ctx, entityType, permission, request) {
|
|
7371
|
+
if (isPermissionBypassPrincipal(ctx)) return true;
|
|
7372
|
+
await ctx.entityManager.registry.pruneExpiredPermissionGrants?.();
|
|
7373
|
+
const builtInAllowed = await ctx.entityManager.registry.hasEntityTypePermission(entityType.name, permission, principalSubject(ctx.principal));
|
|
7374
|
+
return await applyAuthorizationHook(ctx, {
|
|
7375
|
+
verb: permission,
|
|
7376
|
+
resourceKey: `entity_type:${entityType.name}`,
|
|
7377
|
+
resource: {
|
|
7378
|
+
kind: `entity_type`,
|
|
7379
|
+
entityType
|
|
7380
|
+
},
|
|
7381
|
+
builtInAllowed,
|
|
7382
|
+
request
|
|
7383
|
+
});
|
|
7384
|
+
}
|
|
7385
|
+
async function canRegisterEntityType(ctx, input, request) {
|
|
7386
|
+
if (isPermissionBypassPrincipal(ctx)) return true;
|
|
7387
|
+
return await applyAuthorizationHook(ctx, {
|
|
7388
|
+
verb: `manage`,
|
|
7389
|
+
resourceKey: `entity_type_registration:${input.name}`,
|
|
7390
|
+
resource: {
|
|
7391
|
+
kind: `entity_type_registration`,
|
|
7392
|
+
entityTypeName: input.name
|
|
7393
|
+
},
|
|
7394
|
+
builtInAllowed: true,
|
|
7395
|
+
request
|
|
7396
|
+
});
|
|
7397
|
+
}
|
|
7398
|
+
async function canAccessSharedState(ctx, sharedStateId, permission, request, ownerEntityUrl) {
|
|
7399
|
+
if (isPermissionBypassPrincipal(ctx)) return true;
|
|
7400
|
+
await ctx.entityManager.registry.pruneExpiredPermissionGrants?.();
|
|
7401
|
+
const storedLinkedEntityUrls = await ctx.entityManager.registry.listSharedStateLinkedEntityUrls(sharedStateId);
|
|
7402
|
+
const bootstrapEntityUrls = storedLinkedEntityUrls.length === 0 && ownerEntityUrl ? [ownerEntityUrl] : [];
|
|
7403
|
+
const linkedEntityUrls = [...new Set([...storedLinkedEntityUrls, ...bootstrapEntityUrls])];
|
|
7404
|
+
for (const entityUrl of linkedEntityUrls) {
|
|
7405
|
+
const entity = await ctx.entityManager.registry.getEntity(entityUrl);
|
|
7406
|
+
if (!entity) continue;
|
|
7407
|
+
if (entity.created_by === ctx.principal.url || await ctx.entityManager.registry.hasEntityPermission(entity.url, permission, principalSubject(ctx.principal))) return await applyAuthorizationHook(ctx, {
|
|
7408
|
+
verb: permission,
|
|
7409
|
+
resourceKey: `shared_state:${sharedStateId}`,
|
|
7410
|
+
resource: {
|
|
7411
|
+
kind: `shared_state`,
|
|
7412
|
+
sharedStateId,
|
|
7413
|
+
linkedEntityUrls
|
|
7414
|
+
},
|
|
7415
|
+
builtInAllowed: true,
|
|
7416
|
+
request
|
|
7417
|
+
});
|
|
7418
|
+
}
|
|
7419
|
+
return await applyAuthorizationHook(ctx, {
|
|
7420
|
+
verb: permission,
|
|
7421
|
+
resourceKey: `shared_state:${sharedStateId}`,
|
|
7422
|
+
resource: {
|
|
7423
|
+
kind: `shared_state`,
|
|
7424
|
+
sharedStateId,
|
|
7425
|
+
linkedEntityUrls
|
|
7426
|
+
},
|
|
7427
|
+
builtInAllowed: false,
|
|
7428
|
+
request
|
|
7429
|
+
});
|
|
7430
|
+
}
|
|
7431
|
+
async function applyAuthorizationHook(ctx, input) {
|
|
7432
|
+
const hook = ctx.authorizeRequest;
|
|
7433
|
+
if (!hook) return input.builtInAllowed;
|
|
7434
|
+
const cacheKey = [
|
|
7435
|
+
ctx.service,
|
|
7436
|
+
ctx.principal.url,
|
|
7437
|
+
input.verb,
|
|
7438
|
+
input.resourceKey
|
|
7439
|
+
].join(`|`);
|
|
7440
|
+
const cached = getCachedDecision(hook, cacheKey);
|
|
7441
|
+
if (cached) return cached.decision === `allow`;
|
|
7442
|
+
let decision;
|
|
7443
|
+
try {
|
|
7444
|
+
decision = await hook({
|
|
7445
|
+
tenant: ctx.service,
|
|
7446
|
+
principal: ctx.principal,
|
|
7447
|
+
verb: input.verb,
|
|
7448
|
+
resource: input.resource,
|
|
7449
|
+
request: input.request ? requestMetadata(input.request) : void 0,
|
|
7450
|
+
builtInAllowed: input.builtInAllowed
|
|
7451
|
+
});
|
|
7452
|
+
} catch (error) {
|
|
7453
|
+
serverLog.warn(`[agent-server] authorization hook failed:`, error);
|
|
7454
|
+
return false;
|
|
7455
|
+
}
|
|
7456
|
+
cacheDecision(hook, cacheKey, decision);
|
|
7457
|
+
return decision.decision === `allow`;
|
|
7458
|
+
}
|
|
7459
|
+
function getCachedDecision(hook, cacheKey) {
|
|
7460
|
+
const cache = authzDecisionCache.get(hook);
|
|
7461
|
+
const entry = cache?.get(cacheKey);
|
|
7462
|
+
if (!entry) return null;
|
|
7463
|
+
if (entry.expiresAt <= Date.now()) {
|
|
7464
|
+
cache?.delete(cacheKey);
|
|
7465
|
+
return null;
|
|
7466
|
+
}
|
|
7467
|
+
return { decision: entry.decision };
|
|
7468
|
+
}
|
|
7469
|
+
function cacheDecision(hook, cacheKey, decision) {
|
|
7470
|
+
if (!decision.expires_at) return;
|
|
7471
|
+
const expiresAt = Date.parse(decision.expires_at);
|
|
7472
|
+
if (!Number.isFinite(expiresAt) || expiresAt <= Date.now()) return;
|
|
7473
|
+
let cache = authzDecisionCache.get(hook);
|
|
7474
|
+
if (!cache) {
|
|
7475
|
+
cache = new Map();
|
|
7476
|
+
authzDecisionCache.set(hook, cache);
|
|
7477
|
+
}
|
|
7478
|
+
cache.set(cacheKey, {
|
|
7479
|
+
decision: decision.decision,
|
|
7480
|
+
expiresAt
|
|
7481
|
+
});
|
|
7482
|
+
}
|
|
7483
|
+
function requestMetadata(request) {
|
|
7484
|
+
const headers = {};
|
|
7485
|
+
request.headers.forEach((value, key) => {
|
|
7486
|
+
headers[key] = value;
|
|
7487
|
+
});
|
|
7488
|
+
return {
|
|
7489
|
+
method: request.method,
|
|
7490
|
+
url: request.url,
|
|
7491
|
+
headers
|
|
7492
|
+
};
|
|
7493
|
+
}
|
|
6770
7494
|
|
|
6771
7495
|
//#endregion
|
|
6772
7496
|
//#region src/webhook-signing.ts
|
|
@@ -6858,6 +7582,7 @@ const subscriptionControlActions = [
|
|
|
6858
7582
|
`ack`,
|
|
6859
7583
|
`release`
|
|
6860
7584
|
];
|
|
7585
|
+
const SHARED_STATE_OWNER_ENTITY_HEADER = `electric-owner-entity`;
|
|
6861
7586
|
const durableStreamsRouter = (0, itty_router.Router)();
|
|
6862
7587
|
durableStreamsRouter.put(`/__ds/subscriptions/:subscriptionId`, putSubscriptionBase);
|
|
6863
7588
|
durableStreamsRouter.get(`/__ds/subscriptions/:subscriptionId`, getSubscriptionBase);
|
|
@@ -7075,6 +7800,8 @@ async function webhookJwks(_request, ctx) {
|
|
|
7075
7800
|
});
|
|
7076
7801
|
}
|
|
7077
7802
|
async function streamAppend(request, ctx) {
|
|
7803
|
+
const auth = await authorizeDurableStreamAccess(request, ctx);
|
|
7804
|
+
if (auth) return auth;
|
|
7078
7805
|
return await electricAgentsStreamAppendRouter.fetch(createStreamAppendRouteRequest(request), ctx.runtime, (req, body) => forwardFetchRequest({
|
|
7079
7806
|
request: {
|
|
7080
7807
|
method: req.method,
|
|
@@ -7091,8 +7818,9 @@ async function streamAppend(request, ctx) {
|
|
|
7091
7818
|
}));
|
|
7092
7819
|
}
|
|
7093
7820
|
async function proxyPassThrough(request, ctx) {
|
|
7821
|
+
const auth = await authorizeDurableStreamAccess(request, ctx);
|
|
7822
|
+
if (auth) return auth;
|
|
7094
7823
|
const streamPath = new URL(request.url).pathname;
|
|
7095
|
-
if (ctx.entityManager?.isAttachmentStreamPath(streamPath)) return new Response(null, { status: 404 });
|
|
7096
7824
|
const upstream = await forwardToDurableStreams(ctx, request);
|
|
7097
7825
|
const method = request.method.toUpperCase();
|
|
7098
7826
|
const endTrackedRead = method === `GET` ? await ctx.entityBridgeManager.beginClientRead(streamPath) : null;
|
|
@@ -7103,6 +7831,51 @@ async function proxyPassThrough(request, ctx) {
|
|
|
7103
7831
|
await endTrackedRead?.();
|
|
7104
7832
|
}
|
|
7105
7833
|
}
|
|
7834
|
+
async function authorizeDurableStreamAccess(request, ctx) {
|
|
7835
|
+
const method = request.method.toUpperCase();
|
|
7836
|
+
const streamPath = new URL(request.url).pathname;
|
|
7837
|
+
if (method === `GET` || method === `HEAD`) {
|
|
7838
|
+
const registry = ctx.entityManager?.registry;
|
|
7839
|
+
const entity = registry?.getEntityByStream ? await registry.getEntityByStream(streamPath) : null;
|
|
7840
|
+
if (entity) {
|
|
7841
|
+
if (await canAccessEntity(ctx, entity, `read`, request)) return void 0;
|
|
7842
|
+
return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to read ${entity.url}`);
|
|
7843
|
+
}
|
|
7844
|
+
const attachmentEntityUrl = entityUrlFromAttachmentStreamPath(streamPath);
|
|
7845
|
+
if (attachmentEntityUrl) {
|
|
7846
|
+
const attachmentEntity = registry?.getEntity ? await registry.getEntity(attachmentEntityUrl) : null;
|
|
7847
|
+
if (!attachmentEntity) return apiError(404, ErrCodeNotFound, `Entity not found`);
|
|
7848
|
+
if (await canAccessEntity(ctx, attachmentEntity, `read`, request)) return void 0;
|
|
7849
|
+
return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to read ${attachmentEntity.url}`);
|
|
7850
|
+
}
|
|
7851
|
+
}
|
|
7852
|
+
const sharedStateId = sharedStateIdFromPath(streamPath);
|
|
7853
|
+
if (!sharedStateId) return void 0;
|
|
7854
|
+
if (method === `GET` || method === `HEAD`) {
|
|
7855
|
+
if (await canAccessSharedState(ctx, sharedStateId, `read`, request)) return void 0;
|
|
7856
|
+
return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to read shared state`);
|
|
7857
|
+
}
|
|
7858
|
+
if (method === `PUT` || method === `POST`) {
|
|
7859
|
+
const ownerEntityUrl = request.headers.get(SHARED_STATE_OWNER_ENTITY_HEADER)?.trim() || void 0;
|
|
7860
|
+
if (await canAccessSharedState(ctx, sharedStateId, `write`, request, ownerEntityUrl)) return void 0;
|
|
7861
|
+
return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to write shared state`);
|
|
7862
|
+
}
|
|
7863
|
+
return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to access shared state`);
|
|
7864
|
+
}
|
|
7865
|
+
function entityUrlFromAttachmentStreamPath(path$2) {
|
|
7866
|
+
const match = path$2.match(/^\/([^/]+)\/([^/]+)\/attachments\/[^/]+$/);
|
|
7867
|
+
if (!match) return null;
|
|
7868
|
+
return `/${match[1]}/${match[2]}`;
|
|
7869
|
+
}
|
|
7870
|
+
function sharedStateIdFromPath(path$2) {
|
|
7871
|
+
const match = path$2.match(/^\/_electric\/shared-state\/([^/]+)$/);
|
|
7872
|
+
if (!match) return null;
|
|
7873
|
+
try {
|
|
7874
|
+
return decodeURIComponent(match[1]);
|
|
7875
|
+
} catch {
|
|
7876
|
+
return match[1];
|
|
7877
|
+
}
|
|
7878
|
+
}
|
|
7106
7879
|
|
|
7107
7880
|
//#endregion
|
|
7108
7881
|
//#region src/routing/electric-proxy-router.ts
|
|
@@ -7110,12 +7883,15 @@ const electricProxyRouter = (0, itty_router.Router)({ base: `/_electric/electric
|
|
|
7110
7883
|
electricProxyRouter.get(`/*`, proxyElectric);
|
|
7111
7884
|
async function proxyElectric(request, ctx) {
|
|
7112
7885
|
if (!ctx.electricUrl) return apiError(500, `ELECTRIC_PROXY_FAILED`, `Electric URL not configured`);
|
|
7886
|
+
await ctx.entityManager.registry.pruneExpiredPermissionGrants?.();
|
|
7113
7887
|
const target = buildElectricProxyTarget({
|
|
7114
7888
|
incomingUrl: new URL(request.url),
|
|
7115
7889
|
electricUrl: ctx.electricUrl,
|
|
7116
7890
|
electricSecret: ctx.electricSecret,
|
|
7117
7891
|
tenantId: ctx.service,
|
|
7118
|
-
principalUrl: ctx.principal.url
|
|
7892
|
+
principalUrl: ctx.principal.url,
|
|
7893
|
+
principalKind: ctx.principal.kind,
|
|
7894
|
+
permissionBypass: isPermissionBypassPrincipal(ctx)
|
|
7119
7895
|
});
|
|
7120
7896
|
const headers = new Headers(request.headers);
|
|
7121
7897
|
headers.delete(`host`);
|
|
@@ -7174,6 +7950,27 @@ const wakeConditionSchema = __sinclair_typebox.Type.Union([__sinclair_typebox.Ty
|
|
|
7174
7950
|
__sinclair_typebox.Type.Literal(`delete`)
|
|
7175
7951
|
])))
|
|
7176
7952
|
})]);
|
|
7953
|
+
const permissionSubjectSchema = __sinclair_typebox.Type.Object({
|
|
7954
|
+
subject_kind: __sinclair_typebox.Type.Union([__sinclair_typebox.Type.Literal(`principal`), __sinclair_typebox.Type.Literal(`principal_kind`)]),
|
|
7955
|
+
subject_value: __sinclair_typebox.Type.String()
|
|
7956
|
+
}, { additionalProperties: false });
|
|
7957
|
+
const entityPermissionSchema = __sinclair_typebox.Type.Union([
|
|
7958
|
+
__sinclair_typebox.Type.Literal(`read`),
|
|
7959
|
+
__sinclair_typebox.Type.Literal(`write`),
|
|
7960
|
+
__sinclair_typebox.Type.Literal(`delete`),
|
|
7961
|
+
__sinclair_typebox.Type.Literal(`signal`),
|
|
7962
|
+
__sinclair_typebox.Type.Literal(`fork`),
|
|
7963
|
+
__sinclair_typebox.Type.Literal(`schedule`),
|
|
7964
|
+
__sinclair_typebox.Type.Literal(`spawn`),
|
|
7965
|
+
__sinclair_typebox.Type.Literal(`manage`)
|
|
7966
|
+
]);
|
|
7967
|
+
const entityPermissionGrantInputSchema = __sinclair_typebox.Type.Object({
|
|
7968
|
+
...permissionSubjectSchema.properties,
|
|
7969
|
+
permission: entityPermissionSchema,
|
|
7970
|
+
propagation: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Union([__sinclair_typebox.Type.Literal(`self`), __sinclair_typebox.Type.Literal(`descendants`)])),
|
|
7971
|
+
copy_to_children: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Boolean()),
|
|
7972
|
+
expires_at: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String())
|
|
7973
|
+
}, { additionalProperties: false });
|
|
7177
7974
|
const spawnBodySchema = __sinclair_typebox.Type.Object({
|
|
7178
7975
|
args: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Record(__sinclair_typebox.Type.String(), __sinclair_typebox.Type.Unknown())),
|
|
7179
7976
|
tags: __sinclair_typebox.Type.Optional(stringRecordSchema$1),
|
|
@@ -7181,6 +7978,8 @@ const spawnBodySchema = __sinclair_typebox.Type.Object({
|
|
|
7181
7978
|
dispatch_policy: __sinclair_typebox.Type.Optional(dispatchPolicySchema),
|
|
7182
7979
|
sandbox: __sinclair_typebox.Type.Optional(sandboxChoiceSchema),
|
|
7183
7980
|
initialMessage: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Unknown()),
|
|
7981
|
+
grants: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Array(entityPermissionGrantInputSchema)),
|
|
7982
|
+
initialMessageType: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
|
|
7184
7983
|
wake: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Object({
|
|
7185
7984
|
subscriberUrl: __sinclair_typebox.Type.String(),
|
|
7186
7985
|
condition: wakeConditionSchema,
|
|
@@ -7202,8 +8001,22 @@ const sendBodySchema = __sinclair_typebox.Type.Object({
|
|
|
7202
8001
|
])),
|
|
7203
8002
|
position: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
|
|
7204
8003
|
afterMs: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Number()),
|
|
7205
|
-
from: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String())
|
|
8004
|
+
from: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
|
|
8005
|
+
from_principal: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
|
|
8006
|
+
from_agent: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String())
|
|
7206
8007
|
});
|
|
8008
|
+
function agentUrlForPrincipal(principal) {
|
|
8009
|
+
if (principal.kind === `agent`) return `/${principal.id}`;
|
|
8010
|
+
if (principal.key.startsWith(`entity:`)) return `/${principal.key.slice(`entity:`.length)}`;
|
|
8011
|
+
return null;
|
|
8012
|
+
}
|
|
8013
|
+
function agentUrlPath(value) {
|
|
8014
|
+
try {
|
|
8015
|
+
return new URL(value).pathname;
|
|
8016
|
+
} catch {
|
|
8017
|
+
return value;
|
|
8018
|
+
}
|
|
8019
|
+
}
|
|
7207
8020
|
const inboxMessageBodySchema = __sinclair_typebox.Type.Object({
|
|
7208
8021
|
payload: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Unknown()),
|
|
7209
8022
|
position: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
|
|
@@ -7282,24 +8095,27 @@ const attachmentSubjectTypes = new Set([
|
|
|
7282
8095
|
]);
|
|
7283
8096
|
const entitiesRouter = (0, itty_router.Router)({ base: `/_electric/entities` });
|
|
7284
8097
|
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);
|
|
8098
|
+
entitiesRouter.put(`/:type/:instanceId`, withSpawnableEntityType, withSchema(spawnBodySchema), withSpawnPermission, spawnEntity);
|
|
8099
|
+
entitiesRouter.get(`/:type/:instanceId`, withExistingEntity, withEntityPermission(`read`), getEntity);
|
|
8100
|
+
entitiesRouter.head(`/:type/:instanceId`, withExistingEntity, withEntityPermission(`read`), headEntity);
|
|
8101
|
+
entitiesRouter.delete(`/:type/:instanceId`, withExistingEntity, withEntityPermission(`delete`), killEntity);
|
|
8102
|
+
entitiesRouter.post(`/:type/:instanceId/signal`, withExistingEntity, withSchema(signalBodySchema), withEntityPermission(`signal`), signalEntity);
|
|
8103
|
+
entitiesRouter.post(`/:type/:instanceId/send`, withExistingEntity, withSchema(sendBodySchema), withEntityPermission(`write`), sendEntity);
|
|
8104
|
+
entitiesRouter.post(`/:type/:instanceId/attachments`, withExistingEntity, withEntityPermission(`write`), createAttachment);
|
|
8105
|
+
entitiesRouter.get(`/:type/:instanceId/attachments/:attachmentId`, withExistingEntity, withEntityPermission(`read`), readAttachment);
|
|
8106
|
+
entitiesRouter.delete(`/:type/:instanceId/attachments/:attachmentId`, withExistingEntity, withEntityPermission(`write`), deleteAttachment);
|
|
8107
|
+
entitiesRouter.patch(`/:type/:instanceId/inbox/:messageKey`, withExistingEntity, withSchema(inboxMessageBodySchema), withEntityPermission(`write`), updateInboxMessage);
|
|
8108
|
+
entitiesRouter.delete(`/:type/:instanceId/inbox/:messageKey`, withExistingEntity, withEntityPermission(`write`), deleteInboxMessage);
|
|
8109
|
+
entitiesRouter.post(`/:type/:instanceId/fork`, withExistingEntity, withSchema(forkBodySchema), withEntityPermission(`fork`), forkEntity);
|
|
8110
|
+
entitiesRouter.post(`/:type/:instanceId/tags/:tagKey`, withExistingEntity, withSchema(setTagBodySchema), withEntityPermission(`write`), setTag);
|
|
8111
|
+
entitiesRouter.delete(`/:type/:instanceId/tags/:tagKey`, withExistingEntity, withEntityPermission(`write`), deleteTag);
|
|
8112
|
+
entitiesRouter.put(`/:type/:instanceId/schedules/:scheduleId`, withExistingEntity, withSchema(scheduleBodySchema), withEntityPermission(`schedule`), upsertSchedule);
|
|
8113
|
+
entitiesRouter.delete(`/:type/:instanceId/schedules/:scheduleId`, withExistingEntity, withEntityPermission(`schedule`), deleteSchedule);
|
|
8114
|
+
entitiesRouter.put(`/:type/:instanceId/event-source-subscriptions/:subscriptionId`, withExistingEntity, withSchema(eventSourceSubscriptionBodySchema), withEntityPermission(`write`), upsertEventSourceSubscription);
|
|
8115
|
+
entitiesRouter.delete(`/:type/:instanceId/event-source-subscriptions/:subscriptionId`, withExistingEntity, withEntityPermission(`write`), deleteEventSourceSubscription);
|
|
8116
|
+
entitiesRouter.get(`/:type/:instanceId/grants`, withExistingEntity, withEntityPermission(`manage`), listEntityPermissionGrants);
|
|
8117
|
+
entitiesRouter.post(`/:type/:instanceId/grants`, withExistingEntity, withSchema(entityPermissionGrantInputSchema), withEntityPermission(`manage`), createEntityPermissionGrant);
|
|
8118
|
+
entitiesRouter.delete(`/:type/:instanceId/grants/:grantId`, withExistingEntity, withEntityPermission(`manage`), deleteEntityPermissionGrant);
|
|
7303
8119
|
function entityUrlFromSegments(type, instanceId) {
|
|
7304
8120
|
if (!type || !instanceId) return null;
|
|
7305
8121
|
if (type.startsWith(`_`) || type.includes(`*`) || instanceId.includes(`*`)) return null;
|
|
@@ -7398,6 +8214,17 @@ function rejectPrincipalEntityMutation(request, action) {
|
|
|
7398
8214
|
if (entity.type !== `principal`) return void 0;
|
|
7399
8215
|
return apiError(400, ErrCodeInvalidRequest, `Principal entities are built in and cannot be ${action}`);
|
|
7400
8216
|
}
|
|
8217
|
+
function parseExpiresAt$1(value) {
|
|
8218
|
+
if (value === void 0) return void 0;
|
|
8219
|
+
const expiresAt = new Date(value);
|
|
8220
|
+
if (Number.isNaN(expiresAt.getTime())) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Invalid expires_at timestamp`, 400);
|
|
8221
|
+
return expiresAt;
|
|
8222
|
+
}
|
|
8223
|
+
function parseGrantId$1(request) {
|
|
8224
|
+
const grantId = Number.parseInt(String(request.params.grantId), 10);
|
|
8225
|
+
if (!Number.isSafeInteger(grantId) || grantId <= 0) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Invalid grant id`, 400);
|
|
8226
|
+
return grantId;
|
|
8227
|
+
}
|
|
7401
8228
|
async function withExistingEntity(request, ctx) {
|
|
7402
8229
|
const entityUrl = entityUrlFromSegments(request.params.type, request.params.instanceId);
|
|
7403
8230
|
if (!entityUrl) return void 0;
|
|
@@ -7428,17 +8255,76 @@ async function withSpawnableEntityType(request, ctx) {
|
|
|
7428
8255
|
if (request.params.type === `principal`) return apiError(400, ErrCodeInvalidRequest, `Principal entities are built in and cannot be spawned directly`);
|
|
7429
8256
|
const entityType = await ctx.entityManager.registry.getEntityType(request.params.type);
|
|
7430
8257
|
if (!entityType) return apiError(404, ErrCodeUnknownEntityType, `Entity type "${request.params.type}" not found`);
|
|
8258
|
+
request.spawnRoute = { entityType };
|
|
7431
8259
|
return void 0;
|
|
7432
8260
|
}
|
|
8261
|
+
function withEntityPermission(permission) {
|
|
8262
|
+
return async (request, ctx) => {
|
|
8263
|
+
const { entity } = requireExistingEntityRoute(request);
|
|
8264
|
+
if (await canAccessEntity(ctx, entity, permission, request)) return void 0;
|
|
8265
|
+
return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to ${permission} ${entity.url}`);
|
|
8266
|
+
};
|
|
8267
|
+
}
|
|
8268
|
+
async function withSpawnPermission(request, ctx) {
|
|
8269
|
+
const parsed = routeBody(request);
|
|
8270
|
+
const entityType = request.spawnRoute?.entityType;
|
|
8271
|
+
if (!entityType) throw new Error(`spawnable entity type middleware did not run`);
|
|
8272
|
+
if (!await canAccessEntityType(ctx, entityType, `spawn`, request)) return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to spawn ${entityType.name}`);
|
|
8273
|
+
if (!parsed.parent) return void 0;
|
|
8274
|
+
const parent = await ctx.entityManager.registry.getEntity(parsed.parent);
|
|
8275
|
+
if (!parent) return apiError(404, ErrCodeNotFound, `Parent entity not found`);
|
|
8276
|
+
if (await canAccessEntity(ctx, parent, `spawn`, request)) return await validateParentedSpawnGrants(request, ctx, parent, parsed);
|
|
8277
|
+
return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to spawn children from ${parent.url}`);
|
|
8278
|
+
}
|
|
8279
|
+
async function validateParentedSpawnGrants(request, ctx, parent, parsed) {
|
|
8280
|
+
const needsParentManage = (parsed.grants ?? []).some(requiresParentManageForInitialGrant);
|
|
8281
|
+
if (!needsParentManage) return void 0;
|
|
8282
|
+
if (await canAccessEntity(ctx, parent, `manage`, request)) return void 0;
|
|
8283
|
+
return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to delegate broad grants from ${parent.url}`);
|
|
8284
|
+
}
|
|
8285
|
+
function requiresParentManageForInitialGrant(grant) {
|
|
8286
|
+
return grant.permission === `manage` || grant.subject_kind === `principal_kind` || grant.propagation === `descendants` || grant.copy_to_children === true;
|
|
8287
|
+
}
|
|
7433
8288
|
async function listEntities({ query }, ctx) {
|
|
7434
8289
|
const { entities: entities$1 } = await ctx.entityManager.registry.listEntities({
|
|
7435
8290
|
type: firstQueryValue$1(query.type),
|
|
7436
8291
|
status: firstQueryValue$1(query.status),
|
|
7437
8292
|
parent: firstQueryValue$1(query.parent),
|
|
7438
|
-
created_by: firstQueryValue$1(query.created_by)
|
|
8293
|
+
created_by: firstQueryValue$1(query.created_by),
|
|
8294
|
+
readableBy: {
|
|
8295
|
+
...principalSubject(ctx.principal),
|
|
8296
|
+
bypass: isPermissionBypassPrincipal(ctx)
|
|
8297
|
+
}
|
|
7439
8298
|
});
|
|
7440
8299
|
return (0, itty_router.json)(entities$1.map((entity) => toPublicEntity(entity)));
|
|
7441
8300
|
}
|
|
8301
|
+
async function listEntityPermissionGrants(request, ctx) {
|
|
8302
|
+
const { entityUrl } = requireExistingEntityRoute(request);
|
|
8303
|
+
const grants = await ctx.entityManager.registry.listEntityPermissionGrants(entityUrl);
|
|
8304
|
+
return (0, itty_router.json)({ grants });
|
|
8305
|
+
}
|
|
8306
|
+
async function createEntityPermissionGrant(request, ctx) {
|
|
8307
|
+
const { entityUrl } = requireExistingEntityRoute(request);
|
|
8308
|
+
const parsed = routeBody(request);
|
|
8309
|
+
const grant = await ctx.entityManager.registry.createEntityPermissionGrant({
|
|
8310
|
+
entityUrl,
|
|
8311
|
+
permission: parsed.permission,
|
|
8312
|
+
subjectKind: parsed.subject_kind,
|
|
8313
|
+
subjectValue: parsed.subject_value,
|
|
8314
|
+
propagation: parsed.propagation,
|
|
8315
|
+
copyToChildren: parsed.copy_to_children,
|
|
8316
|
+
expiresAt: parseExpiresAt$1(parsed.expires_at),
|
|
8317
|
+
createdBy: ctx.principal.url
|
|
8318
|
+
});
|
|
8319
|
+
await ctx.entityBridgeManager.onEntityChanged(entityUrl);
|
|
8320
|
+
return (0, itty_router.json)(grant, { status: 201 });
|
|
8321
|
+
}
|
|
8322
|
+
async function deleteEntityPermissionGrant(request, ctx) {
|
|
8323
|
+
const { entityUrl } = requireExistingEntityRoute(request);
|
|
8324
|
+
const deleted = await ctx.entityManager.registry.deleteEntityPermissionGrant(entityUrl, parseGrantId$1(request));
|
|
8325
|
+
if (deleted) await ctx.entityBridgeManager.onEntityChanged(entityUrl);
|
|
8326
|
+
return deleted ? (0, itty_router.status)(204) : apiError(404, ErrCodeNotFound, `Grant not found`);
|
|
8327
|
+
}
|
|
7442
8328
|
async function upsertSchedule(request, ctx) {
|
|
7443
8329
|
const principalMutationError = rejectPrincipalEntityMutation(request, `scheduled`);
|
|
7444
8330
|
if (principalMutationError) return principalMutationError;
|
|
@@ -7544,6 +8430,7 @@ async function forkEntity(request, ctx) {
|
|
|
7544
8430
|
const result = await ctx.entityManager.forkSubtree(entityUrl, {
|
|
7545
8431
|
rootInstanceId: parsed.instance_id,
|
|
7546
8432
|
waitTimeoutMs: parsed.waitTimeoutMs,
|
|
8433
|
+
createdBy: ctx.principal.url,
|
|
7547
8434
|
...parsed.fork_pointer && { forkPointer: {
|
|
7548
8435
|
offset: parsed.fork_pointer.offset,
|
|
7549
8436
|
subOffset: parsed.fork_pointer.sub_offset
|
|
@@ -7559,26 +8446,27 @@ async function sendEntity(request, ctx) {
|
|
|
7559
8446
|
const parsed = routeBody(request);
|
|
7560
8447
|
const principal = ctx.principal;
|
|
7561
8448
|
if (parsed.from !== void 0 && parsed.from !== principal.url) return apiError(400, ErrCodeInvalidRequest, `Request from must match Electric-Principal`);
|
|
8449
|
+
if (parsed.from_principal !== void 0 && parsed.from_principal !== principal.url) return apiError(400, ErrCodeInvalidRequest, `Request from_principal must match Electric-Principal`);
|
|
8450
|
+
if (parsed.from_agent !== void 0) {
|
|
8451
|
+
const principalAgentUrl = agentUrlForPrincipal(principal);
|
|
8452
|
+
if (agentUrlPath(parsed.from_agent) !== principalAgentUrl) return apiError(400, ErrCodeInvalidRequest, `Request from_agent must match authenticated agent principal`);
|
|
8453
|
+
}
|
|
7562
8454
|
await ctx.entityManager.ensurePrincipal(principal);
|
|
7563
8455
|
const { entityUrl, entity } = requireExistingEntityRoute(request);
|
|
7564
8456
|
const dispatchEntity = entity.dispatch_policy ? entity : await backfillEntityDispatchPolicy(ctx, entity);
|
|
7565
8457
|
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, {
|
|
8458
|
+
const sendReq = {
|
|
7575
8459
|
from: principal.url,
|
|
8460
|
+
from_principal: principal.url,
|
|
8461
|
+
from_agent: parsed.from_agent,
|
|
7576
8462
|
payload: parsed.payload,
|
|
7577
8463
|
key: parsed.key,
|
|
7578
8464
|
type: parsed.type,
|
|
7579
8465
|
mode: parsed.mode,
|
|
7580
8466
|
position: parsed.position
|
|
7581
|
-
}
|
|
8467
|
+
};
|
|
8468
|
+
if (parsed.afterMs && parsed.afterMs > 0) await ctx.entityManager.enqueueDelayedSend(entityUrl, sendReq, new Date(Date.now() + parsed.afterMs));
|
|
8469
|
+
else await ctx.entityManager.send(entityUrl, sendReq);
|
|
7582
8470
|
return (0, itty_router.status)(204);
|
|
7583
8471
|
}
|
|
7584
8472
|
async function createAttachment(request, ctx) {
|
|
@@ -7647,14 +8535,27 @@ async function spawnEntity(request, ctx) {
|
|
|
7647
8535
|
dispatch_policy: dispatchPolicy,
|
|
7648
8536
|
sandbox: parsed.sandbox,
|
|
7649
8537
|
initialMessage: void 0,
|
|
8538
|
+
initialMessageType: void 0,
|
|
7650
8539
|
wake: parsed.wake,
|
|
7651
8540
|
created_by: principal.url
|
|
7652
8541
|
});
|
|
8542
|
+
if (parsed.parent) await ctx.entityManager.registry.copyEntityPermissionGrantsForSpawn(parsed.parent, entity.url, principal.url);
|
|
8543
|
+
for (const grant of parsed.grants ?? []) await ctx.entityManager.registry.createEntityPermissionGrant({
|
|
8544
|
+
entityUrl: entity.url,
|
|
8545
|
+
permission: grant.permission,
|
|
8546
|
+
subjectKind: grant.subject_kind,
|
|
8547
|
+
subjectValue: grant.subject_value,
|
|
8548
|
+
propagation: grant.propagation,
|
|
8549
|
+
copyToChildren: grant.copy_to_children,
|
|
8550
|
+
expiresAt: parseExpiresAt$1(grant.expires_at),
|
|
8551
|
+
createdBy: principal.url
|
|
8552
|
+
});
|
|
7653
8553
|
const linkBeforeInitialMessage = parsed.initialMessage !== void 0 && shouldLinkDispatchBeforeInitialMessage(dispatchPolicy);
|
|
7654
8554
|
if (linkBeforeInitialMessage) await linkEntityDispatchSubscription(ctx, entity);
|
|
7655
8555
|
if (parsed.initialMessage !== void 0) await ctx.entityManager.send(entity.url, {
|
|
7656
8556
|
from: principal.url,
|
|
7657
|
-
payload: parsed.initialMessage
|
|
8557
|
+
payload: parsed.initialMessage,
|
|
8558
|
+
type: parsed.initialMessageType
|
|
7658
8559
|
});
|
|
7659
8560
|
if (!linkBeforeInitialMessage) await linkEntityDispatchSubscription(ctx, entity);
|
|
7660
8561
|
return (0, itty_router.json)({
|
|
@@ -7701,14 +8602,37 @@ async function signalEntity(request, ctx) {
|
|
|
7701
8602
|
//#region src/routing/entity-types-router.ts
|
|
7702
8603
|
const jsonObjectSchema = __sinclair_typebox.Type.Record(__sinclair_typebox.Type.String(), __sinclair_typebox.Type.Unknown());
|
|
7703
8604
|
const schemaMapSchema = __sinclair_typebox.Type.Record(__sinclair_typebox.Type.String(), jsonObjectSchema);
|
|
8605
|
+
const slashCommandArgumentSchema = __sinclair_typebox.Type.Object({
|
|
8606
|
+
name: __sinclair_typebox.Type.String(),
|
|
8607
|
+
type: __sinclair_typebox.Type.Union([
|
|
8608
|
+
__sinclair_typebox.Type.Literal(`string`),
|
|
8609
|
+
__sinclair_typebox.Type.Literal(`number`),
|
|
8610
|
+
__sinclair_typebox.Type.Literal(`boolean`)
|
|
8611
|
+
]),
|
|
8612
|
+
required: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Boolean()),
|
|
8613
|
+
description: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String())
|
|
8614
|
+
}, { additionalProperties: false });
|
|
8615
|
+
const slashCommandSchema = __sinclair_typebox.Type.Object({
|
|
8616
|
+
name: __sinclair_typebox.Type.String(),
|
|
8617
|
+
description: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
|
|
8618
|
+
arguments: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Array(slashCommandArgumentSchema))
|
|
8619
|
+
}, { additionalProperties: false });
|
|
8620
|
+
const typePermissionGrantInputSchema = __sinclair_typebox.Type.Object({
|
|
8621
|
+
subject_kind: __sinclair_typebox.Type.Union([__sinclair_typebox.Type.Literal(`principal`), __sinclair_typebox.Type.Literal(`principal_kind`)]),
|
|
8622
|
+
subject_value: __sinclair_typebox.Type.String(),
|
|
8623
|
+
permission: __sinclair_typebox.Type.Union([__sinclair_typebox.Type.Literal(`spawn`), __sinclair_typebox.Type.Literal(`manage`)]),
|
|
8624
|
+
expires_at: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String())
|
|
8625
|
+
}, { additionalProperties: false });
|
|
7704
8626
|
const registerEntityTypeBodySchema = __sinclair_typebox.Type.Object({
|
|
7705
8627
|
name: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
|
|
7706
8628
|
description: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
|
|
7707
8629
|
creation_schema: __sinclair_typebox.Type.Optional(jsonObjectSchema),
|
|
7708
8630
|
inbox_schemas: __sinclair_typebox.Type.Optional(schemaMapSchema),
|
|
7709
8631
|
state_schemas: __sinclair_typebox.Type.Optional(schemaMapSchema),
|
|
8632
|
+
slash_commands: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Array(slashCommandSchema)),
|
|
7710
8633
|
serve_endpoint: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
|
|
7711
|
-
default_dispatch_policy: __sinclair_typebox.Type.Optional(dispatchPolicySchema)
|
|
8634
|
+
default_dispatch_policy: __sinclair_typebox.Type.Optional(dispatchPolicySchema),
|
|
8635
|
+
permission_grants: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Array(typePermissionGrantInputSchema))
|
|
7712
8636
|
}, { additionalProperties: false });
|
|
7713
8637
|
const amendEntityTypeSchemasBodySchema = __sinclair_typebox.Type.Object({
|
|
7714
8638
|
inbox_schemas: __sinclair_typebox.Type.Optional(schemaMapSchema),
|
|
@@ -7716,20 +8640,56 @@ const amendEntityTypeSchemasBodySchema = __sinclair_typebox.Type.Object({
|
|
|
7716
8640
|
}, { additionalProperties: false });
|
|
7717
8641
|
const entityTypesRouter = (0, itty_router.Router)({ base: `/_electric/entity-types` });
|
|
7718
8642
|
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);
|
|
8643
|
+
entityTypesRouter.post(`/`, withSchema(registerEntityTypeBodySchema), withEntityTypeRegistrationPermission, registerEntityType);
|
|
8644
|
+
entityTypesRouter.patch(`/:name/schemas`, withExistingEntityType, withEntityTypeManagePermission, withSchema(amendEntityTypeSchemasBodySchema), amendSchemas);
|
|
8645
|
+
entityTypesRouter.get(`/:name`, withExistingEntityType, withEntityTypeSpawnPermission, getEntityType);
|
|
8646
|
+
entityTypesRouter.delete(`/:name`, withExistingEntityType, withEntityTypeManagePermission, deleteEntityType);
|
|
8647
|
+
entityTypesRouter.get(`/:name/grants`, withExistingEntityType, withEntityTypeManagePermission, listTypePermissionGrants);
|
|
8648
|
+
entityTypesRouter.post(`/:name/grants`, withExistingEntityType, withSchema(typePermissionGrantInputSchema), withEntityTypeManagePermission, createTypePermissionGrant);
|
|
8649
|
+
entityTypesRouter.delete(`/:name/grants/:grantId`, withExistingEntityType, withEntityTypeManagePermission, deleteTypePermissionGrant);
|
|
7723
8650
|
async function registerEntityType(request, ctx) {
|
|
7724
8651
|
const parsed = routeBody(request);
|
|
7725
8652
|
const normalized = normalizeEntityTypeRequest(parsed);
|
|
7726
8653
|
if (normalized.serve_endpoint && !normalized.description && !normalized.creation_schema) return await discoverServeEndpoint(ctx, normalized);
|
|
7727
8654
|
const entityType = await ctx.entityManager.registerEntityType(normalized);
|
|
8655
|
+
await applyRegistrationPermissionGrants(ctx, entityType.name, normalized);
|
|
7728
8656
|
return (0, itty_router.json)(toPublicEntityType(entityType), { status: 201 });
|
|
7729
8657
|
}
|
|
7730
8658
|
async function listEntityTypes(_request, ctx) {
|
|
7731
8659
|
const entityTypes$1 = await ctx.entityManager.registry.listEntityTypes();
|
|
7732
|
-
|
|
8660
|
+
const visible = [];
|
|
8661
|
+
for (const entityType of entityTypes$1) if (await canAccessEntityType(ctx, entityType, `spawn`)) visible.push(entityType);
|
|
8662
|
+
return (0, itty_router.json)(visible.map((entityType) => toPublicEntityType(entityType)));
|
|
8663
|
+
}
|
|
8664
|
+
async function withExistingEntityType(request, ctx) {
|
|
8665
|
+
const entityType = await ctx.entityManager.registry.getEntityType(request.params.name);
|
|
8666
|
+
if (!entityType) return apiError(404, ErrCodeNotFound, `Entity type not found`);
|
|
8667
|
+
request.entityTypeRoute = { entityType };
|
|
8668
|
+
return void 0;
|
|
8669
|
+
}
|
|
8670
|
+
async function withEntityTypeManagePermission(request, ctx) {
|
|
8671
|
+
const entityType = request.entityTypeRoute?.entityType;
|
|
8672
|
+
if (!entityType) throw new Error(`entity type middleware did not run`);
|
|
8673
|
+
if (await canAccessEntityType(ctx, entityType, `manage`, request)) return void 0;
|
|
8674
|
+
return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to manage ${entityType.name}`);
|
|
8675
|
+
}
|
|
8676
|
+
async function withEntityTypeSpawnPermission(request, ctx) {
|
|
8677
|
+
const entityType = request.entityTypeRoute?.entityType;
|
|
8678
|
+
if (!entityType) throw new Error(`entity type middleware did not run`);
|
|
8679
|
+
if (await canAccessEntityType(ctx, entityType, `spawn`, request)) return void 0;
|
|
8680
|
+
return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to spawn ${entityType.name}`);
|
|
8681
|
+
}
|
|
8682
|
+
async function withEntityTypeRegistrationPermission(request, ctx) {
|
|
8683
|
+
const parsed = normalizeEntityTypeRequest(routeBody(request));
|
|
8684
|
+
if (!parsed.name) return void 0;
|
|
8685
|
+
const existing = await ctx.entityManager.registry.getEntityType(parsed.name);
|
|
8686
|
+
if (existing) {
|
|
8687
|
+
request.entityTypeRoute = { entityType: existing };
|
|
8688
|
+
if (await canAccessEntityType(ctx, existing, `manage`, request)) return void 0;
|
|
8689
|
+
return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to manage ${existing.name}`);
|
|
8690
|
+
}
|
|
8691
|
+
if (await canRegisterEntityType(ctx, parsed, request)) return void 0;
|
|
8692
|
+
return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to register entity types`);
|
|
7733
8693
|
}
|
|
7734
8694
|
async function discoverServeEndpoint(ctx, parsed) {
|
|
7735
8695
|
try {
|
|
@@ -7738,17 +8698,17 @@ async function discoverServeEndpoint(ctx, parsed) {
|
|
|
7738
8698
|
const manifest = await response.json();
|
|
7739
8699
|
if (manifest.name !== parsed.name) return apiError(400, ErrCodeServeEndpointNameMismatch, `Serve endpoint returned name "${manifest.name}" but expected "${parsed.name}"`);
|
|
7740
8700
|
manifest.serve_endpoint = parsed.serve_endpoint;
|
|
8701
|
+
manifest.permission_grants = parsed.permission_grants;
|
|
7741
8702
|
const entityType = await ctx.entityManager.registerEntityType(normalizeEntityTypeRequest(manifest));
|
|
8703
|
+
await applyRegistrationPermissionGrants(ctx, entityType.name, manifest);
|
|
7742
8704
|
return (0, itty_router.json)(toPublicEntityType(entityType), { status: 201 });
|
|
7743
8705
|
} catch (err) {
|
|
7744
8706
|
if (err instanceof ElectricAgentsError) throw err;
|
|
7745
8707
|
return apiError(502, ErrCodeServeEndpointUnreachable, `Failed to reach serve endpoint: ${err instanceof Error ? err.message : String(err)}`);
|
|
7746
8708
|
}
|
|
7747
8709
|
}
|
|
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));
|
|
8710
|
+
async function getEntityType(request) {
|
|
8711
|
+
return (0, itty_router.json)(toPublicEntityType(request.entityTypeRoute.entityType));
|
|
7752
8712
|
}
|
|
7753
8713
|
async function amendSchemas(request, ctx) {
|
|
7754
8714
|
const parsed = routeBody(request);
|
|
@@ -7762,6 +8722,47 @@ async function deleteEntityType(request, ctx) {
|
|
|
7762
8722
|
await ctx.entityManager.deleteEntityType(request.params.name);
|
|
7763
8723
|
return (0, itty_router.status)(204);
|
|
7764
8724
|
}
|
|
8725
|
+
async function listTypePermissionGrants(request, ctx) {
|
|
8726
|
+
const grants = await ctx.entityManager.registry.listEntityTypePermissionGrants(request.entityTypeRoute.entityType.name);
|
|
8727
|
+
return (0, itty_router.json)({ grants });
|
|
8728
|
+
}
|
|
8729
|
+
async function createTypePermissionGrant(request, ctx) {
|
|
8730
|
+
const parsed = routeBody(request);
|
|
8731
|
+
const grant = await ctx.entityManager.registry.createEntityTypePermissionGrant({
|
|
8732
|
+
entityType: request.entityTypeRoute.entityType.name,
|
|
8733
|
+
permission: parsed.permission,
|
|
8734
|
+
subjectKind: parsed.subject_kind,
|
|
8735
|
+
subjectValue: parsed.subject_value,
|
|
8736
|
+
expiresAt: parseExpiresAt(parsed.expires_at),
|
|
8737
|
+
createdBy: ctx.principal.url
|
|
8738
|
+
});
|
|
8739
|
+
return (0, itty_router.json)(grant, { status: 201 });
|
|
8740
|
+
}
|
|
8741
|
+
async function deleteTypePermissionGrant(request, ctx) {
|
|
8742
|
+
const deleted = await ctx.entityManager.registry.deleteEntityTypePermissionGrant(request.entityTypeRoute.entityType.name, parseGrantId(request));
|
|
8743
|
+
return deleted ? (0, itty_router.status)(204) : apiError(404, ErrCodeNotFound, `Grant not found`);
|
|
8744
|
+
}
|
|
8745
|
+
async function applyRegistrationPermissionGrants(ctx, entityType, request) {
|
|
8746
|
+
for (const grant of request.permission_grants ?? []) await ctx.entityManager.registry.ensureEntityTypePermissionGrant({
|
|
8747
|
+
entityType,
|
|
8748
|
+
permission: grant.permission,
|
|
8749
|
+
subjectKind: grant.subject_kind,
|
|
8750
|
+
subjectValue: grant.subject_value,
|
|
8751
|
+
expiresAt: parseExpiresAt(grant.expires_at),
|
|
8752
|
+
createdBy: ctx.principal.url
|
|
8753
|
+
});
|
|
8754
|
+
}
|
|
8755
|
+
function parseGrantId(request) {
|
|
8756
|
+
const grantId = Number.parseInt(String(request.params.grantId), 10);
|
|
8757
|
+
if (!Number.isSafeInteger(grantId) || grantId <= 0) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Invalid grant id`, 400);
|
|
8758
|
+
return grantId;
|
|
8759
|
+
}
|
|
8760
|
+
function parseExpiresAt(value) {
|
|
8761
|
+
if (value === void 0) return void 0;
|
|
8762
|
+
const expiresAt = new Date(value);
|
|
8763
|
+
if (Number.isNaN(expiresAt.getTime())) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Invalid expires_at timestamp`, 400);
|
|
8764
|
+
return expiresAt;
|
|
8765
|
+
}
|
|
7765
8766
|
function normalizeEntityTypeRequest(parsed) {
|
|
7766
8767
|
const serveEndpoint = rewriteLoopbackWebhookUrl(parsed.serve_endpoint);
|
|
7767
8768
|
return {
|
|
@@ -7770,11 +8771,13 @@ function normalizeEntityTypeRequest(parsed) {
|
|
|
7770
8771
|
creation_schema: parsed.creation_schema,
|
|
7771
8772
|
inbox_schemas: parsed.inbox_schemas,
|
|
7772
8773
|
state_schemas: parsed.state_schemas,
|
|
8774
|
+
slash_commands: parsed.slash_commands,
|
|
7773
8775
|
serve_endpoint: serveEndpoint,
|
|
7774
8776
|
default_dispatch_policy: parsed.default_dispatch_policy ?? (serveEndpoint ? { targets: [{
|
|
7775
8777
|
type: `webhook`,
|
|
7776
8778
|
url: serveEndpoint
|
|
7777
|
-
}] } : void 0)
|
|
8779
|
+
}] } : void 0),
|
|
8780
|
+
permission_grants: parsed.permission_grants
|
|
7778
8781
|
};
|
|
7779
8782
|
}
|
|
7780
8783
|
function toPublicEntityType(entityType) {
|
|
@@ -7833,6 +8836,7 @@ function applyCors(response) {
|
|
|
7833
8836
|
`content-type`,
|
|
7834
8837
|
`authorization`,
|
|
7835
8838
|
`electric-claim-token`,
|
|
8839
|
+
`electric-owner-entity`,
|
|
7836
8840
|
ELECTRIC_PRINCIPAL_HEADER,
|
|
7837
8841
|
`ngrok-skip-browser-warning`
|
|
7838
8842
|
].join(`, `));
|
|
@@ -7883,7 +8887,7 @@ observationsRouter.post(`/entities/ensure-stream`, withSchema(ensureEntitiesMemb
|
|
|
7883
8887
|
observationsRouter.post(`/cron/ensure-stream`, withSchema(ensureCronStreamBodySchema), ensureCronStream);
|
|
7884
8888
|
async function ensureEntitiesMembershipStream(request, ctx) {
|
|
7885
8889
|
const parsed = routeBody(request);
|
|
7886
|
-
const result = await ctx.entityManager.ensureEntitiesMembershipStream(parsed.tags ?? {});
|
|
8890
|
+
const result = await ctx.entityManager.ensureEntitiesMembershipStream(parsed.tags ?? {}, ctx.principal);
|
|
7887
8891
|
return (0, itty_router.json)(result);
|
|
7888
8892
|
}
|
|
7889
8893
|
async function ensureCronStream(request, ctx) {
|