@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.js
CHANGED
|
@@ -5,15 +5,15 @@ import { fileURLToPath } from "node:url";
|
|
|
5
5
|
import { drizzle } from "drizzle-orm/postgres-js";
|
|
6
6
|
import { migrate } from "drizzle-orm/postgres-js/migrator";
|
|
7
7
|
import postgres from "postgres";
|
|
8
|
-
import { and, desc, eq, lt, ne, sql } from "drizzle-orm";
|
|
8
|
+
import { and, desc, eq, inArray, lt, ne, sql } from "drizzle-orm";
|
|
9
9
|
import { bigint, bigserial, boolean, check, index, integer, jsonb, pgTable, primaryKey, serial, text, timestamp, unique } from "drizzle-orm/pg-core";
|
|
10
10
|
import { createHash, createPrivateKey, createPublicKey, generateKeyPairSync, randomUUID, sign } from "node:crypto";
|
|
11
|
-
import { appendPathToUrl, assertTags, buildEventSourceManifestEntry, buildTagsIndex, entityStateSchema, eventSourceSubscriptionManifestKey, getCronStreamPath, getCronStreamPathFromSpec, getEntitiesStreamPath, getNextCronFireAt, getSharedStateStreamPath, getWebhookStreamPath, manifestChildKey, manifestSharedStateKey, manifestSourceKey, normalizeTags, parseCronStreamPath, resolveCronScheduleSpec, resolveEventSourceSubscription, sourceRefForTags, verifyWebhookSignature } from "@electric-ax/agents-runtime";
|
|
11
|
+
import { COMPOSER_INPUT_MESSAGE_TYPE, appendPathToUrl, assertTags, buildEventSourceManifestEntry, buildTagsIndex, entityStateSchema, eventSourceSubscriptionManifestKey, getCronStreamPath, getCronStreamPathFromSpec, getEntitiesStreamPath, getNextCronFireAt, getSharedStateStreamPath, getWebhookStreamPath, hashString, manifestChildKey, manifestSharedStateKey, manifestSourceKey, normalizeTags, parseCronStreamPath, resolveCronScheduleSpec, resolveEventSourceSubscription, sourceRefForTags, validateComposerInputPayload, validateSlashCommandDefinitions, verifyWebhookSignature } from "@electric-ax/agents-runtime";
|
|
12
12
|
import { DurableStream, DurableStreamError, FetchError, IdempotentProducer } from "@durable-streams/client";
|
|
13
13
|
import { ShapeStream, isChangeMessage, isControlMessage } from "@electric-sql/client";
|
|
14
14
|
import pino from "pino";
|
|
15
|
-
import fastq from "fastq";
|
|
16
15
|
import { Type } from "@sinclair/typebox";
|
|
16
|
+
import fastq from "fastq";
|
|
17
17
|
import Ajv from "ajv";
|
|
18
18
|
import { SpanKind, SpanStatusCode, context, propagation, trace } from "@opentelemetry/api";
|
|
19
19
|
import { AutoRouter, Router, json, status, withParams } from "itty-router";
|
|
@@ -26,11 +26,16 @@ __export(schema_exports, {
|
|
|
26
26
|
entities: () => entities,
|
|
27
27
|
entityBridges: () => entityBridges,
|
|
28
28
|
entityDispatchState: () => entityDispatchState,
|
|
29
|
+
entityEffectivePermissions: () => entityEffectivePermissions,
|
|
30
|
+
entityLineage: () => entityLineage,
|
|
29
31
|
entityManifestSources: () => entityManifestSources,
|
|
32
|
+
entityPermissionGrants: () => entityPermissionGrants,
|
|
33
|
+
entityTypePermissionGrants: () => entityTypePermissionGrants,
|
|
30
34
|
entityTypes: () => entityTypes,
|
|
31
35
|
runnerRuntimeDiagnostics: () => runnerRuntimeDiagnostics,
|
|
32
36
|
runners: () => runners,
|
|
33
37
|
scheduledTasks: () => scheduledTasks,
|
|
38
|
+
sharedStateLinks: () => sharedStateLinks,
|
|
34
39
|
subscriptionWebhooks: () => subscriptionWebhooks,
|
|
35
40
|
tagStreamOutbox: () => tagStreamOutbox,
|
|
36
41
|
users: () => users,
|
|
@@ -44,6 +49,7 @@ const entityTypes = pgTable(`entity_types`, {
|
|
|
44
49
|
creationSchema: jsonb(`creation_schema`),
|
|
45
50
|
inboxSchemas: jsonb(`inbox_schemas`),
|
|
46
51
|
stateSchemas: jsonb(`state_schemas`),
|
|
52
|
+
slashCommands: jsonb(`slash_commands`),
|
|
47
53
|
serveEndpoint: text(`serve_endpoint`),
|
|
48
54
|
defaultDispatchPolicy: jsonb(`default_dispatch_policy`),
|
|
49
55
|
revision: integer(`revision`).notNull().default(1),
|
|
@@ -78,6 +84,94 @@ const entities = pgTable(`entities`, {
|
|
|
78
84
|
index(`entities_tags_index_gin`).using(`gin`, table.tagsIndex),
|
|
79
85
|
check(`chk_entities_status`, sql`${table.status} IN ('spawning', 'running', 'idle', 'paused', 'stopping', 'stopped', 'killed')`)
|
|
80
86
|
]);
|
|
87
|
+
const entityTypePermissionGrants = pgTable(`entity_type_permission_grants`, {
|
|
88
|
+
id: bigserial(`id`, { mode: `number` }).primaryKey(),
|
|
89
|
+
tenantId: text(`tenant_id`).notNull().default(`default`),
|
|
90
|
+
entityType: text(`entity_type`).notNull(),
|
|
91
|
+
permission: text(`permission`).notNull(),
|
|
92
|
+
subjectKind: text(`subject_kind`).notNull(),
|
|
93
|
+
subjectValue: text(`subject_value`).notNull(),
|
|
94
|
+
createdBy: text(`created_by`),
|
|
95
|
+
expiresAt: timestamp(`expires_at`, { withTimezone: true }),
|
|
96
|
+
createdAt: timestamp(`created_at`, { withTimezone: true }).notNull().defaultNow(),
|
|
97
|
+
updatedAt: timestamp(`updated_at`, { withTimezone: true }).notNull().defaultNow()
|
|
98
|
+
}, (table) => [
|
|
99
|
+
index(`idx_type_permission_grants_lookup`).on(table.tenantId, table.entityType, table.permission, table.subjectKind, table.subjectValue),
|
|
100
|
+
index(`idx_type_permission_grants_expiry`).on(table.tenantId, table.expiresAt),
|
|
101
|
+
check(`chk_type_permission_grants_permission`, sql`${table.permission} IN ('spawn', 'manage')`),
|
|
102
|
+
check(`chk_type_permission_grants_subject_kind`, sql`${table.subjectKind} IN ('principal', 'principal_kind')`)
|
|
103
|
+
]);
|
|
104
|
+
const entityLineage = pgTable(`entity_lineage`, {
|
|
105
|
+
tenantId: text(`tenant_id`).notNull().default(`default`),
|
|
106
|
+
ancestorUrl: text(`ancestor_url`).notNull(),
|
|
107
|
+
descendantUrl: text(`descendant_url`).notNull(),
|
|
108
|
+
depth: integer(`depth`).notNull(),
|
|
109
|
+
createdAt: timestamp(`created_at`, { withTimezone: true }).notNull().defaultNow()
|
|
110
|
+
}, (table) => [
|
|
111
|
+
primaryKey({ columns: [
|
|
112
|
+
table.tenantId,
|
|
113
|
+
table.ancestorUrl,
|
|
114
|
+
table.descendantUrl
|
|
115
|
+
] }),
|
|
116
|
+
index(`idx_entity_lineage_descendant`).on(table.tenantId, table.descendantUrl),
|
|
117
|
+
check(`chk_entity_lineage_depth`, sql`${table.depth} >= 0`)
|
|
118
|
+
]);
|
|
119
|
+
const entityPermissionGrants = pgTable(`entity_permission_grants`, {
|
|
120
|
+
id: bigserial(`id`, { mode: `number` }).primaryKey(),
|
|
121
|
+
tenantId: text(`tenant_id`).notNull().default(`default`),
|
|
122
|
+
entityUrl: text(`entity_url`).notNull(),
|
|
123
|
+
permission: text(`permission`).notNull(),
|
|
124
|
+
subjectKind: text(`subject_kind`).notNull(),
|
|
125
|
+
subjectValue: text(`subject_value`).notNull(),
|
|
126
|
+
propagation: text(`propagation`).notNull().default(`self`),
|
|
127
|
+
copyToChildren: boolean(`copy_to_children`).notNull().default(false),
|
|
128
|
+
createdBy: text(`created_by`),
|
|
129
|
+
expiresAt: timestamp(`expires_at`, { withTimezone: true }),
|
|
130
|
+
createdAt: timestamp(`created_at`, { withTimezone: true }).notNull().defaultNow(),
|
|
131
|
+
updatedAt: timestamp(`updated_at`, { withTimezone: true }).notNull().defaultNow()
|
|
132
|
+
}, (table) => [
|
|
133
|
+
index(`idx_entity_permission_grants_entity`).on(table.tenantId, table.entityUrl),
|
|
134
|
+
index(`idx_entity_permission_grants_subject`).on(table.tenantId, table.permission, table.subjectKind, table.subjectValue),
|
|
135
|
+
index(`idx_entity_permission_grants_expiry`).on(table.tenantId, table.expiresAt),
|
|
136
|
+
check(`chk_entity_permission_grants_permission`, sql`${table.permission} IN ('read', 'write', 'delete', 'signal', 'fork', 'schedule', 'spawn', 'manage')`),
|
|
137
|
+
check(`chk_entity_permission_grants_subject_kind`, sql`${table.subjectKind} IN ('principal', 'principal_kind')`),
|
|
138
|
+
check(`chk_entity_permission_grants_propagation`, sql`${table.propagation} IN ('self', 'descendants')`)
|
|
139
|
+
]);
|
|
140
|
+
const entityEffectivePermissions = pgTable(`entity_effective_permissions`, {
|
|
141
|
+
id: bigserial(`id`, { mode: `number` }).primaryKey(),
|
|
142
|
+
tenantId: text(`tenant_id`).notNull().default(`default`),
|
|
143
|
+
entityUrl: text(`entity_url`).notNull(),
|
|
144
|
+
sourceEntityUrl: text(`source_entity_url`).notNull(),
|
|
145
|
+
sourceGrantId: bigint(`source_grant_id`, { mode: `number` }).notNull(),
|
|
146
|
+
permission: text(`permission`).notNull(),
|
|
147
|
+
subjectKind: text(`subject_kind`).notNull(),
|
|
148
|
+
subjectValue: text(`subject_value`).notNull(),
|
|
149
|
+
expiresAt: timestamp(`expires_at`, { withTimezone: true }),
|
|
150
|
+
createdAt: timestamp(`created_at`, { withTimezone: true }).notNull().defaultNow()
|
|
151
|
+
}, (table) => [
|
|
152
|
+
unique(`uq_entity_effective_permission`).on(table.tenantId, table.entityUrl, table.sourceGrantId),
|
|
153
|
+
index(`idx_entity_effective_permissions_lookup`).on(table.tenantId, table.permission, table.subjectKind, table.subjectValue, table.entityUrl),
|
|
154
|
+
index(`idx_entity_effective_permissions_entity`).on(table.tenantId, table.entityUrl),
|
|
155
|
+
index(`idx_entity_effective_permissions_expiry`).on(table.tenantId, table.expiresAt),
|
|
156
|
+
check(`chk_entity_effective_permissions_permission`, sql`${table.permission} IN ('read', 'write', 'delete', 'signal', 'fork', 'schedule', 'spawn', 'manage')`),
|
|
157
|
+
check(`chk_entity_effective_permissions_subject_kind`, sql`${table.subjectKind} IN ('principal', 'principal_kind')`)
|
|
158
|
+
]);
|
|
159
|
+
const sharedStateLinks = pgTable(`shared_state_links`, {
|
|
160
|
+
tenantId: text(`tenant_id`).notNull().default(`default`),
|
|
161
|
+
sharedStateId: text(`shared_state_id`).notNull(),
|
|
162
|
+
ownerEntityUrl: text(`owner_entity_url`).notNull(),
|
|
163
|
+
manifestKey: text(`manifest_key`).notNull(),
|
|
164
|
+
createdAt: timestamp(`created_at`, { withTimezone: true }).notNull().defaultNow(),
|
|
165
|
+
updatedAt: timestamp(`updated_at`, { withTimezone: true }).notNull().defaultNow()
|
|
166
|
+
}, (table) => [
|
|
167
|
+
primaryKey({ columns: [
|
|
168
|
+
table.tenantId,
|
|
169
|
+
table.ownerEntityUrl,
|
|
170
|
+
table.manifestKey
|
|
171
|
+
] }),
|
|
172
|
+
index(`idx_shared_state_links_shared_state`).on(table.tenantId, table.sharedStateId),
|
|
173
|
+
index(`idx_shared_state_links_owner`).on(table.tenantId, table.ownerEntityUrl)
|
|
174
|
+
]);
|
|
81
175
|
const users = pgTable(`users`, {
|
|
82
176
|
tenantId: text(`tenant_id`).notNull().default(`default`),
|
|
83
177
|
id: text(`id`).notNull(),
|
|
@@ -264,12 +358,18 @@ const entityBridges = pgTable(`entity_bridges`, {
|
|
|
264
358
|
sourceRef: text(`source_ref`).notNull(),
|
|
265
359
|
tags: jsonb(`tags`).notNull(),
|
|
266
360
|
streamUrl: text(`stream_url`).notNull(),
|
|
361
|
+
principalUrl: text(`principal_url`),
|
|
362
|
+
principalKind: text(`principal_kind`),
|
|
267
363
|
shapeHandle: text(`shape_handle`),
|
|
268
364
|
shapeOffset: text(`shape_offset`),
|
|
269
365
|
lastObserverActivityAt: timestamp(`last_observer_activity_at`, { withTimezone: true }).notNull().defaultNow(),
|
|
270
366
|
createdAt: timestamp(`created_at`, { withTimezone: true }).notNull().defaultNow(),
|
|
271
367
|
updatedAt: timestamp(`updated_at`, { withTimezone: true }).notNull().defaultNow()
|
|
272
|
-
}, (table) => [
|
|
368
|
+
}, (table) => [
|
|
369
|
+
primaryKey({ columns: [table.tenantId, table.sourceRef] }),
|
|
370
|
+
unique(`uq_entity_bridges_stream_url`).on(table.tenantId, table.streamUrl),
|
|
371
|
+
index(`idx_entity_bridges_principal`).on(table.tenantId, table.principalKind, table.principalUrl)
|
|
372
|
+
]);
|
|
273
373
|
const entityManifestSources = pgTable(`entity_manifest_sources`, {
|
|
274
374
|
tenantId: text(`tenant_id`).notNull().default(`default`),
|
|
275
375
|
ownerEntityUrl: text(`owner_entity_url`).notNull(),
|
|
@@ -457,16 +557,26 @@ function isDuplicateUrlError(err) {
|
|
|
457
557
|
return e.code === `23505`;
|
|
458
558
|
}
|
|
459
559
|
const DEFAULT_RUNNER_LEASE_MS = 3e4;
|
|
560
|
+
const PERMISSION_PRUNE_INTERVAL_MS = 3e4;
|
|
460
561
|
function runnerWakeStream(runnerId) {
|
|
461
562
|
return `/runners/${runnerId}/wake`;
|
|
462
563
|
}
|
|
463
564
|
var PostgresRegistry = class {
|
|
565
|
+
lastPermissionPruneStartedAt = 0;
|
|
566
|
+
permissionPrunePromise = null;
|
|
464
567
|
constructor(db, tenantId = DEFAULT_TENANT_ID) {
|
|
465
568
|
this.db = db;
|
|
466
569
|
this.tenantId = tenantId;
|
|
467
570
|
}
|
|
468
571
|
async initialize() {}
|
|
469
572
|
close() {}
|
|
573
|
+
async ensureUserForPrincipal(principal) {
|
|
574
|
+
if (principal.kind !== `user`) return;
|
|
575
|
+
await this.db.insert(users).values({
|
|
576
|
+
tenantId: this.tenantId,
|
|
577
|
+
id: principal.id
|
|
578
|
+
}).onConflictDoNothing();
|
|
579
|
+
}
|
|
470
580
|
async createRunner(input) {
|
|
471
581
|
const now = new Date();
|
|
472
582
|
const wakeStream = input.wakeStream ?? runnerWakeStream(input.id);
|
|
@@ -711,6 +821,7 @@ var PostgresRegistry = class {
|
|
|
711
821
|
creationSchema: et.creation_schema ?? null,
|
|
712
822
|
inboxSchemas: et.inbox_schemas ?? null,
|
|
713
823
|
stateSchemas: et.state_schemas ?? null,
|
|
824
|
+
slashCommands: et.slash_commands ?? null,
|
|
714
825
|
serveEndpoint: et.serve_endpoint ?? null,
|
|
715
826
|
defaultDispatchPolicy: et.default_dispatch_policy ?? null,
|
|
716
827
|
revision: et.revision,
|
|
@@ -723,6 +834,7 @@ var PostgresRegistry = class {
|
|
|
723
834
|
creationSchema: et.creation_schema ?? null,
|
|
724
835
|
inboxSchemas: et.inbox_schemas ?? null,
|
|
725
836
|
stateSchemas: et.state_schemas ?? null,
|
|
837
|
+
slashCommands: et.slash_commands ?? null,
|
|
726
838
|
serveEndpoint: et.serve_endpoint ?? null,
|
|
727
839
|
defaultDispatchPolicy: et.default_dispatch_policy ?? null,
|
|
728
840
|
revision: et.revision,
|
|
@@ -740,6 +852,7 @@ var PostgresRegistry = class {
|
|
|
740
852
|
creationSchema: et.creation_schema ?? null,
|
|
741
853
|
inboxSchemas: et.inbox_schemas ?? null,
|
|
742
854
|
stateSchemas: et.state_schemas ?? null,
|
|
855
|
+
slashCommands: et.slash_commands ?? null,
|
|
743
856
|
serveEndpoint: et.serve_endpoint ?? null,
|
|
744
857
|
defaultDispatchPolicy: et.default_dispatch_policy ?? null,
|
|
745
858
|
revision: et.revision,
|
|
@@ -766,6 +879,7 @@ var PostgresRegistry = class {
|
|
|
766
879
|
creationSchema: et.creation_schema ?? null,
|
|
767
880
|
inboxSchemas: et.inbox_schemas ?? null,
|
|
768
881
|
stateSchemas: et.state_schemas ?? null,
|
|
882
|
+
slashCommands: et.slash_commands ?? null,
|
|
769
883
|
serveEndpoint: et.serve_endpoint ?? null,
|
|
770
884
|
defaultDispatchPolicy: et.default_dispatch_policy ?? null,
|
|
771
885
|
revision: et.revision,
|
|
@@ -801,6 +915,59 @@ var PostgresRegistry = class {
|
|
|
801
915
|
pendingSourceStreams: [],
|
|
802
916
|
updatedAt: new Date()
|
|
803
917
|
}).onConflictDoNothing();
|
|
918
|
+
await tx.insert(entityLineage).values({
|
|
919
|
+
tenantId: this.tenantId,
|
|
920
|
+
ancestorUrl: entity.url,
|
|
921
|
+
descendantUrl: entity.url,
|
|
922
|
+
depth: 0
|
|
923
|
+
}).onConflictDoNothing();
|
|
924
|
+
if (entity.parent) await tx.execute(sql`
|
|
925
|
+
INSERT INTO ${entityLineage} (
|
|
926
|
+
tenant_id,
|
|
927
|
+
ancestor_url,
|
|
928
|
+
descendant_url,
|
|
929
|
+
depth
|
|
930
|
+
)
|
|
931
|
+
SELECT
|
|
932
|
+
${this.tenantId},
|
|
933
|
+
ancestor_url,
|
|
934
|
+
${entity.url},
|
|
935
|
+
depth + 1
|
|
936
|
+
FROM ${entityLineage}
|
|
937
|
+
WHERE tenant_id = ${this.tenantId}
|
|
938
|
+
AND descendant_url = ${entity.parent}
|
|
939
|
+
ON CONFLICT DO NOTHING
|
|
940
|
+
`);
|
|
941
|
+
await tx.execute(sql`
|
|
942
|
+
INSERT INTO ${entityEffectivePermissions} (
|
|
943
|
+
tenant_id,
|
|
944
|
+
entity_url,
|
|
945
|
+
source_entity_url,
|
|
946
|
+
source_grant_id,
|
|
947
|
+
permission,
|
|
948
|
+
subject_kind,
|
|
949
|
+
subject_value,
|
|
950
|
+
expires_at
|
|
951
|
+
)
|
|
952
|
+
SELECT
|
|
953
|
+
${this.tenantId},
|
|
954
|
+
${entity.url},
|
|
955
|
+
grants.entity_url,
|
|
956
|
+
grants.id,
|
|
957
|
+
grants.permission,
|
|
958
|
+
grants.subject_kind,
|
|
959
|
+
grants.subject_value,
|
|
960
|
+
grants.expires_at
|
|
961
|
+
FROM ${entityPermissionGrants} grants
|
|
962
|
+
JOIN ${entityLineage} lineage
|
|
963
|
+
ON lineage.tenant_id = grants.tenant_id
|
|
964
|
+
AND lineage.ancestor_url = grants.entity_url
|
|
965
|
+
AND lineage.descendant_url = ${entity.url}
|
|
966
|
+
WHERE grants.tenant_id = ${this.tenantId}
|
|
967
|
+
AND grants.propagation = 'descendants'
|
|
968
|
+
AND (grants.expires_at IS NULL OR grants.expires_at > now())
|
|
969
|
+
ON CONFLICT DO NOTHING
|
|
970
|
+
`);
|
|
804
971
|
return parseInt(result[0].txid);
|
|
805
972
|
});
|
|
806
973
|
} catch (err) {
|
|
@@ -822,10 +989,8 @@ var PostgresRegistry = class {
|
|
|
822
989
|
}
|
|
823
990
|
async getEntityByStream(streamPath) {
|
|
824
991
|
const mainSuffix = `/main`;
|
|
825
|
-
const errorSuffix = `/error`;
|
|
826
992
|
let entityUrl = null;
|
|
827
993
|
if (streamPath.endsWith(mainSuffix)) entityUrl = streamPath.slice(0, -mainSuffix.length);
|
|
828
|
-
else if (streamPath.endsWith(errorSuffix)) entityUrl = streamPath.slice(0, -errorSuffix.length);
|
|
829
994
|
if (!entityUrl) return null;
|
|
830
995
|
return this.getEntity(entityUrl);
|
|
831
996
|
}
|
|
@@ -835,6 +1000,23 @@ var PostgresRegistry = class {
|
|
|
835
1000
|
if (filter?.status) conditions.push(eq(entities.status, filter.status));
|
|
836
1001
|
if (filter?.parent) conditions.push(eq(entities.parent, filter.parent));
|
|
837
1002
|
if (filter?.created_by) conditions.push(eq(entities.createdBy, filter.created_by));
|
|
1003
|
+
if (filter?.readableBy && !filter.readableBy.bypass) conditions.push(sql`(
|
|
1004
|
+
${entities.createdBy} = ${filter.readableBy.principalUrl}
|
|
1005
|
+
OR ${entities.url} IN (
|
|
1006
|
+
SELECT ${entityEffectivePermissions.entityUrl}
|
|
1007
|
+
FROM ${entityEffectivePermissions}
|
|
1008
|
+
WHERE ${entityEffectivePermissions.tenantId} = ${this.tenantId}
|
|
1009
|
+
AND ${entityEffectivePermissions.permission} IN ('read', 'manage')
|
|
1010
|
+
AND (${entityEffectivePermissions.expiresAt} IS NULL OR ${entityEffectivePermissions.expiresAt} > now())
|
|
1011
|
+
AND (
|
|
1012
|
+
(${entityEffectivePermissions.subjectKind} = 'principal'
|
|
1013
|
+
AND ${entityEffectivePermissions.subjectValue} = ${filter.readableBy.principalUrl})
|
|
1014
|
+
OR
|
|
1015
|
+
(${entityEffectivePermissions.subjectKind} = 'principal_kind'
|
|
1016
|
+
AND ${entityEffectivePermissions.subjectValue} = ${filter.readableBy.principalKind})
|
|
1017
|
+
)
|
|
1018
|
+
)
|
|
1019
|
+
)`);
|
|
838
1020
|
const whereClause = and(...conditions);
|
|
839
1021
|
const countResult = await this.db.select({ count: sql`count(*)` }).from(entities).where(whereClause);
|
|
840
1022
|
const total = Number(countResult[0].count);
|
|
@@ -847,6 +1029,189 @@ var PostgresRegistry = class {
|
|
|
847
1029
|
total
|
|
848
1030
|
};
|
|
849
1031
|
}
|
|
1032
|
+
async createEntityTypePermissionGrant(input) {
|
|
1033
|
+
const [row] = await this.db.insert(entityTypePermissionGrants).values({
|
|
1034
|
+
tenantId: this.tenantId,
|
|
1035
|
+
entityType: input.entityType,
|
|
1036
|
+
permission: input.permission,
|
|
1037
|
+
subjectKind: input.subjectKind,
|
|
1038
|
+
subjectValue: input.subjectValue,
|
|
1039
|
+
createdBy: input.createdBy ?? null,
|
|
1040
|
+
expiresAt: input.expiresAt ?? null
|
|
1041
|
+
}).returning();
|
|
1042
|
+
return this.rowToEntityTypePermissionGrant(row);
|
|
1043
|
+
}
|
|
1044
|
+
async ensureEntityTypePermissionGrant(input) {
|
|
1045
|
+
const [existing] = await this.db.select().from(entityTypePermissionGrants).where(and(eq(entityTypePermissionGrants.tenantId, this.tenantId), eq(entityTypePermissionGrants.entityType, input.entityType), eq(entityTypePermissionGrants.permission, input.permission), eq(entityTypePermissionGrants.subjectKind, input.subjectKind), eq(entityTypePermissionGrants.subjectValue, input.subjectValue), input.expiresAt ? eq(entityTypePermissionGrants.expiresAt, input.expiresAt) : sql`${entityTypePermissionGrants.expiresAt} IS NULL`)).limit(1);
|
|
1046
|
+
if (existing) return this.rowToEntityTypePermissionGrant(existing);
|
|
1047
|
+
return await this.createEntityTypePermissionGrant(input);
|
|
1048
|
+
}
|
|
1049
|
+
async listEntityTypePermissionGrants(entityType) {
|
|
1050
|
+
const rows = await this.db.select().from(entityTypePermissionGrants).where(and(eq(entityTypePermissionGrants.tenantId, this.tenantId), eq(entityTypePermissionGrants.entityType, entityType))).orderBy(entityTypePermissionGrants.id);
|
|
1051
|
+
return rows.map((row) => this.rowToEntityTypePermissionGrant(row));
|
|
1052
|
+
}
|
|
1053
|
+
async deleteEntityTypePermissionGrant(entityType, grantId) {
|
|
1054
|
+
const rows = await this.db.delete(entityTypePermissionGrants).where(and(eq(entityTypePermissionGrants.tenantId, this.tenantId), eq(entityTypePermissionGrants.entityType, entityType), eq(entityTypePermissionGrants.id, grantId))).returning({ id: entityTypePermissionGrants.id });
|
|
1055
|
+
return rows.length > 0;
|
|
1056
|
+
}
|
|
1057
|
+
async hasEntityTypePermission(entityType, permission, subject) {
|
|
1058
|
+
const permissions = [permission, `manage`];
|
|
1059
|
+
const rows = await this.db.select({ id: entityTypePermissionGrants.id }).from(entityTypePermissionGrants).where(and(eq(entityTypePermissionGrants.tenantId, this.tenantId), eq(entityTypePermissionGrants.entityType, entityType), inArray(entityTypePermissionGrants.permission, [...permissions]), sql`(${entityTypePermissionGrants.expiresAt} IS NULL OR ${entityTypePermissionGrants.expiresAt} > now())`, sql`(
|
|
1060
|
+
(${entityTypePermissionGrants.subjectKind} = 'principal'
|
|
1061
|
+
AND ${entityTypePermissionGrants.subjectValue} = ${subject.principalUrl})
|
|
1062
|
+
OR
|
|
1063
|
+
(${entityTypePermissionGrants.subjectKind} = 'principal_kind'
|
|
1064
|
+
AND ${entityTypePermissionGrants.subjectValue} = ${subject.principalKind})
|
|
1065
|
+
)`)).limit(1);
|
|
1066
|
+
return rows.length > 0;
|
|
1067
|
+
}
|
|
1068
|
+
async createEntityPermissionGrant(input) {
|
|
1069
|
+
return await this.db.transaction(async (tx) => {
|
|
1070
|
+
const [row] = await tx.insert(entityPermissionGrants).values({
|
|
1071
|
+
tenantId: this.tenantId,
|
|
1072
|
+
entityUrl: input.entityUrl,
|
|
1073
|
+
permission: input.permission,
|
|
1074
|
+
subjectKind: input.subjectKind,
|
|
1075
|
+
subjectValue: input.subjectValue,
|
|
1076
|
+
propagation: input.propagation ?? `self`,
|
|
1077
|
+
copyToChildren: input.copyToChildren ?? false,
|
|
1078
|
+
createdBy: input.createdBy ?? null,
|
|
1079
|
+
expiresAt: input.expiresAt ?? null
|
|
1080
|
+
}).returning();
|
|
1081
|
+
await this.materializeEntityPermissionGrant(tx, row);
|
|
1082
|
+
return this.rowToEntityPermissionGrant(row);
|
|
1083
|
+
});
|
|
1084
|
+
}
|
|
1085
|
+
async listEntityPermissionGrants(entityUrl) {
|
|
1086
|
+
const rows = await this.db.select().from(entityPermissionGrants).where(and(eq(entityPermissionGrants.tenantId, this.tenantId), eq(entityPermissionGrants.entityUrl, entityUrl))).orderBy(entityPermissionGrants.id);
|
|
1087
|
+
return rows.map((row) => this.rowToEntityPermissionGrant(row));
|
|
1088
|
+
}
|
|
1089
|
+
async deleteEntityPermissionGrant(entityUrl, grantId) {
|
|
1090
|
+
return await this.db.transaction(async (tx) => {
|
|
1091
|
+
await tx.delete(entityEffectivePermissions).where(and(eq(entityEffectivePermissions.tenantId, this.tenantId), eq(entityEffectivePermissions.sourceGrantId, grantId)));
|
|
1092
|
+
const rows = await tx.delete(entityPermissionGrants).where(and(eq(entityPermissionGrants.tenantId, this.tenantId), eq(entityPermissionGrants.entityUrl, entityUrl), eq(entityPermissionGrants.id, grantId))).returning({ id: entityPermissionGrants.id });
|
|
1093
|
+
return rows.length > 0;
|
|
1094
|
+
});
|
|
1095
|
+
}
|
|
1096
|
+
async copyEntityPermissionGrantsForSpawn(parentEntityUrl, childEntityUrl, createdBy) {
|
|
1097
|
+
const parentGrants = await this.db.select().from(entityPermissionGrants).where(and(eq(entityPermissionGrants.tenantId, this.tenantId), eq(entityPermissionGrants.entityUrl, parentEntityUrl), eq(entityPermissionGrants.copyToChildren, true), sql`(${entityPermissionGrants.expiresAt} IS NULL OR ${entityPermissionGrants.expiresAt} > now())`));
|
|
1098
|
+
const copied = [];
|
|
1099
|
+
for (const grant of parentGrants) copied.push(await this.createEntityPermissionGrant({
|
|
1100
|
+
entityUrl: childEntityUrl,
|
|
1101
|
+
permission: grant.permission,
|
|
1102
|
+
subjectKind: grant.subjectKind,
|
|
1103
|
+
subjectValue: grant.subjectValue,
|
|
1104
|
+
propagation: `self`,
|
|
1105
|
+
copyToChildren: grant.copyToChildren,
|
|
1106
|
+
createdBy,
|
|
1107
|
+
expiresAt: grant.expiresAt ?? void 0
|
|
1108
|
+
}));
|
|
1109
|
+
return copied;
|
|
1110
|
+
}
|
|
1111
|
+
async hasEntityPermission(entityUrl, permission, subject) {
|
|
1112
|
+
const permissions = [permission, `manage`];
|
|
1113
|
+
const rows = await this.db.select({ id: entityEffectivePermissions.id }).from(entityEffectivePermissions).where(and(eq(entityEffectivePermissions.tenantId, this.tenantId), eq(entityEffectivePermissions.entityUrl, entityUrl), inArray(entityEffectivePermissions.permission, [...permissions]), sql`(${entityEffectivePermissions.expiresAt} IS NULL OR ${entityEffectivePermissions.expiresAt} > now())`, sql`(
|
|
1114
|
+
(${entityEffectivePermissions.subjectKind} = 'principal'
|
|
1115
|
+
AND ${entityEffectivePermissions.subjectValue} = ${subject.principalUrl})
|
|
1116
|
+
OR
|
|
1117
|
+
(${entityEffectivePermissions.subjectKind} = 'principal_kind'
|
|
1118
|
+
AND ${entityEffectivePermissions.subjectValue} = ${subject.principalKind})
|
|
1119
|
+
)`)).limit(1);
|
|
1120
|
+
return rows.length > 0;
|
|
1121
|
+
}
|
|
1122
|
+
async replaceSharedStateLink(ownerEntityUrl, manifestKey, sharedStateId) {
|
|
1123
|
+
await this.db.delete(sharedStateLinks).where(and(eq(sharedStateLinks.tenantId, this.tenantId), eq(sharedStateLinks.ownerEntityUrl, ownerEntityUrl), eq(sharedStateLinks.manifestKey, manifestKey)));
|
|
1124
|
+
if (!sharedStateId) return;
|
|
1125
|
+
await this.db.insert(sharedStateLinks).values({
|
|
1126
|
+
tenantId: this.tenantId,
|
|
1127
|
+
ownerEntityUrl,
|
|
1128
|
+
manifestKey,
|
|
1129
|
+
sharedStateId
|
|
1130
|
+
}).onConflictDoUpdate({
|
|
1131
|
+
target: [
|
|
1132
|
+
sharedStateLinks.tenantId,
|
|
1133
|
+
sharedStateLinks.ownerEntityUrl,
|
|
1134
|
+
sharedStateLinks.manifestKey
|
|
1135
|
+
],
|
|
1136
|
+
set: {
|
|
1137
|
+
sharedStateId,
|
|
1138
|
+
updatedAt: new Date()
|
|
1139
|
+
}
|
|
1140
|
+
});
|
|
1141
|
+
}
|
|
1142
|
+
async listSharedStateLinkedEntityUrls(sharedStateId) {
|
|
1143
|
+
const rows = await this.db.selectDistinct({ ownerEntityUrl: sharedStateLinks.ownerEntityUrl }).from(sharedStateLinks).where(and(eq(sharedStateLinks.tenantId, this.tenantId), eq(sharedStateLinks.sharedStateId, sharedStateId)));
|
|
1144
|
+
return rows.map((row) => row.ownerEntityUrl);
|
|
1145
|
+
}
|
|
1146
|
+
async pruneExpiredPermissionGrants(now = new Date(), options = {}) {
|
|
1147
|
+
if (this.permissionPrunePromise) return await this.permissionPrunePromise;
|
|
1148
|
+
const startedAt = Date.now();
|
|
1149
|
+
if (!options.force && startedAt - this.lastPermissionPruneStartedAt < PERMISSION_PRUNE_INTERVAL_MS) return;
|
|
1150
|
+
this.lastPermissionPruneStartedAt = startedAt;
|
|
1151
|
+
const promise = this.pruneExpiredPermissionGrantsNow(now);
|
|
1152
|
+
this.permissionPrunePromise = promise;
|
|
1153
|
+
try {
|
|
1154
|
+
await promise;
|
|
1155
|
+
} catch (error) {
|
|
1156
|
+
this.lastPermissionPruneStartedAt = 0;
|
|
1157
|
+
throw error;
|
|
1158
|
+
} finally {
|
|
1159
|
+
if (this.permissionPrunePromise === promise) this.permissionPrunePromise = null;
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
async pruneExpiredPermissionGrantsNow(now) {
|
|
1163
|
+
await this.db.transaction(async (tx) => {
|
|
1164
|
+
const expiredEntityGrantIds = await tx.select({ id: entityPermissionGrants.id }).from(entityPermissionGrants).where(and(eq(entityPermissionGrants.tenantId, this.tenantId), sql`${entityPermissionGrants.expiresAt} IS NOT NULL`, lt(entityPermissionGrants.expiresAt, now)));
|
|
1165
|
+
const ids = expiredEntityGrantIds.map((row) => row.id);
|
|
1166
|
+
if (ids.length > 0) {
|
|
1167
|
+
await tx.delete(entityEffectivePermissions).where(and(eq(entityEffectivePermissions.tenantId, this.tenantId), inArray(entityEffectivePermissions.sourceGrantId, ids)));
|
|
1168
|
+
await tx.delete(entityPermissionGrants).where(and(eq(entityPermissionGrants.tenantId, this.tenantId), inArray(entityPermissionGrants.id, ids)));
|
|
1169
|
+
}
|
|
1170
|
+
await tx.delete(entityEffectivePermissions).where(and(eq(entityEffectivePermissions.tenantId, this.tenantId), sql`${entityEffectivePermissions.expiresAt} IS NOT NULL`, lt(entityEffectivePermissions.expiresAt, now)));
|
|
1171
|
+
await tx.delete(entityTypePermissionGrants).where(and(eq(entityTypePermissionGrants.tenantId, this.tenantId), sql`${entityTypePermissionGrants.expiresAt} IS NOT NULL`, lt(entityTypePermissionGrants.expiresAt, now)));
|
|
1172
|
+
});
|
|
1173
|
+
}
|
|
1174
|
+
async materializeEntityPermissionGrant(tx, grant) {
|
|
1175
|
+
await tx.delete(entityEffectivePermissions).where(and(eq(entityEffectivePermissions.tenantId, this.tenantId), eq(entityEffectivePermissions.sourceGrantId, grant.id)));
|
|
1176
|
+
if (grant.propagation === `descendants`) {
|
|
1177
|
+
await tx.execute(sql`
|
|
1178
|
+
INSERT INTO ${entityEffectivePermissions} (
|
|
1179
|
+
tenant_id,
|
|
1180
|
+
entity_url,
|
|
1181
|
+
source_entity_url,
|
|
1182
|
+
source_grant_id,
|
|
1183
|
+
permission,
|
|
1184
|
+
subject_kind,
|
|
1185
|
+
subject_value,
|
|
1186
|
+
expires_at
|
|
1187
|
+
)
|
|
1188
|
+
SELECT
|
|
1189
|
+
${this.tenantId},
|
|
1190
|
+
descendant_url,
|
|
1191
|
+
${grant.entityUrl},
|
|
1192
|
+
${grant.id},
|
|
1193
|
+
${grant.permission},
|
|
1194
|
+
${grant.subjectKind},
|
|
1195
|
+
${grant.subjectValue},
|
|
1196
|
+
${grant.expiresAt}
|
|
1197
|
+
FROM ${entityLineage}
|
|
1198
|
+
WHERE tenant_id = ${this.tenantId}
|
|
1199
|
+
AND ancestor_url = ${grant.entityUrl}
|
|
1200
|
+
ON CONFLICT DO NOTHING
|
|
1201
|
+
`);
|
|
1202
|
+
return;
|
|
1203
|
+
}
|
|
1204
|
+
await tx.insert(entityEffectivePermissions).values({
|
|
1205
|
+
tenantId: this.tenantId,
|
|
1206
|
+
entityUrl: grant.entityUrl,
|
|
1207
|
+
sourceEntityUrl: grant.entityUrl,
|
|
1208
|
+
sourceGrantId: grant.id,
|
|
1209
|
+
permission: grant.permission,
|
|
1210
|
+
subjectKind: grant.subjectKind,
|
|
1211
|
+
subjectValue: grant.subjectValue,
|
|
1212
|
+
expiresAt: grant.expiresAt
|
|
1213
|
+
}).onConflictDoNothing();
|
|
1214
|
+
}
|
|
850
1215
|
async updateStatus(entityUrl, status$1) {
|
|
851
1216
|
const whereClause = isTerminalEntityStatus(status$1) ? this.entityWhere(entityUrl) : and(this.entityWhere(entityUrl), ne(entities.status, `stopped`), ne(entities.status, `killed`));
|
|
852
1217
|
await this.db.update(entities).set({
|
|
@@ -948,7 +1313,9 @@ var PostgresRegistry = class {
|
|
|
948
1313
|
tenantId: this.tenantId,
|
|
949
1314
|
sourceRef: row.sourceRef,
|
|
950
1315
|
tags: normalizeTags(row.tags),
|
|
951
|
-
streamUrl: row.streamUrl
|
|
1316
|
+
streamUrl: row.streamUrl,
|
|
1317
|
+
principalUrl: row.principalUrl,
|
|
1318
|
+
principalKind: row.principalKind
|
|
952
1319
|
}).onConflictDoNothing();
|
|
953
1320
|
const existing = await this.getEntityBridge(row.sourceRef);
|
|
954
1321
|
if (!existing) throw new Error(`Failed to load entity bridge ${row.sourceRef}`);
|
|
@@ -1103,6 +1470,7 @@ var PostgresRegistry = class {
|
|
|
1103
1470
|
creation_schema: row.creationSchema,
|
|
1104
1471
|
inbox_schemas: row.inboxSchemas,
|
|
1105
1472
|
state_schemas: row.stateSchemas,
|
|
1473
|
+
slash_commands: row.slashCommands ?? void 0,
|
|
1106
1474
|
serve_endpoint: row.serveEndpoint ?? void 0,
|
|
1107
1475
|
default_dispatch_policy: row.defaultDispatchPolicy ?? void 0,
|
|
1108
1476
|
revision: row.revision,
|
|
@@ -1110,15 +1478,40 @@ var PostgresRegistry = class {
|
|
|
1110
1478
|
updated_at: row.updatedAt
|
|
1111
1479
|
};
|
|
1112
1480
|
}
|
|
1481
|
+
rowToEntityTypePermissionGrant(row) {
|
|
1482
|
+
return {
|
|
1483
|
+
id: row.id,
|
|
1484
|
+
entity_type: row.entityType,
|
|
1485
|
+
permission: row.permission,
|
|
1486
|
+
subject_kind: row.subjectKind,
|
|
1487
|
+
subject_value: row.subjectValue,
|
|
1488
|
+
created_by: row.createdBy ?? void 0,
|
|
1489
|
+
expires_at: row.expiresAt?.toISOString(),
|
|
1490
|
+
created_at: row.createdAt.toISOString(),
|
|
1491
|
+
updated_at: row.updatedAt.toISOString()
|
|
1492
|
+
};
|
|
1493
|
+
}
|
|
1494
|
+
rowToEntityPermissionGrant(row) {
|
|
1495
|
+
return {
|
|
1496
|
+
id: row.id,
|
|
1497
|
+
entity_url: row.entityUrl,
|
|
1498
|
+
permission: row.permission,
|
|
1499
|
+
subject_kind: row.subjectKind,
|
|
1500
|
+
subject_value: row.subjectValue,
|
|
1501
|
+
propagation: row.propagation,
|
|
1502
|
+
copy_to_children: row.copyToChildren,
|
|
1503
|
+
created_by: row.createdBy ?? void 0,
|
|
1504
|
+
expires_at: row.expiresAt?.toISOString(),
|
|
1505
|
+
created_at: row.createdAt.toISOString(),
|
|
1506
|
+
updated_at: row.updatedAt.toISOString()
|
|
1507
|
+
};
|
|
1508
|
+
}
|
|
1113
1509
|
rowToEntity(row) {
|
|
1114
1510
|
return {
|
|
1115
1511
|
url: row.url,
|
|
1116
1512
|
type: row.type,
|
|
1117
1513
|
status: assertEntityStatus(row.status),
|
|
1118
|
-
streams: {
|
|
1119
|
-
main: `${row.url}/main`,
|
|
1120
|
-
error: `${row.url}/error`
|
|
1121
|
-
},
|
|
1514
|
+
streams: { main: `${row.url}/main` },
|
|
1122
1515
|
subscription_id: row.subscriptionId,
|
|
1123
1516
|
dispatch_policy: row.dispatchPolicy ?? void 0,
|
|
1124
1517
|
write_token: row.writeToken,
|
|
@@ -1140,6 +1533,8 @@ var PostgresRegistry = class {
|
|
|
1140
1533
|
sourceRef: row.sourceRef,
|
|
1141
1534
|
tags: row.tags ?? {},
|
|
1142
1535
|
streamUrl: row.streamUrl,
|
|
1536
|
+
principalUrl: row.principalUrl ?? void 0,
|
|
1537
|
+
principalKind: row.principalKind ?? void 0,
|
|
1143
1538
|
shapeHandle: row.shapeHandle ?? void 0,
|
|
1144
1539
|
shapeOffset: row.shapeOffset ?? void 0,
|
|
1145
1540
|
lastObserverActivityAt: row.lastObserverActivityAt,
|
|
@@ -1294,6 +1689,93 @@ const serverLog = {
|
|
|
1294
1689
|
}
|
|
1295
1690
|
};
|
|
1296
1691
|
|
|
1692
|
+
//#endregion
|
|
1693
|
+
//#region src/principal.ts
|
|
1694
|
+
const ELECTRIC_PRINCIPAL_HEADER = `electric-principal`;
|
|
1695
|
+
const PRINCIPAL_KINDS = new Set([
|
|
1696
|
+
`user`,
|
|
1697
|
+
`agent`,
|
|
1698
|
+
`service`,
|
|
1699
|
+
`system`
|
|
1700
|
+
]);
|
|
1701
|
+
function parsePrincipalKey(input) {
|
|
1702
|
+
const colon = input.indexOf(`:`);
|
|
1703
|
+
if (colon <= 0) throw new Error(`Invalid principal identifier`);
|
|
1704
|
+
const kind = input.slice(0, colon);
|
|
1705
|
+
const id = input.slice(colon + 1);
|
|
1706
|
+
if (!PRINCIPAL_KINDS.has(kind)) throw new Error(`Invalid principal kind`);
|
|
1707
|
+
if (!id || id.includes(`/`)) throw new Error(`Invalid principal id`);
|
|
1708
|
+
const key = `${kind}:${id}`;
|
|
1709
|
+
return {
|
|
1710
|
+
kind,
|
|
1711
|
+
id,
|
|
1712
|
+
key,
|
|
1713
|
+
url: `/principal/${encodeURIComponent(key)}`
|
|
1714
|
+
};
|
|
1715
|
+
}
|
|
1716
|
+
function principalUrl(key) {
|
|
1717
|
+
return parsePrincipalKey(key).url;
|
|
1718
|
+
}
|
|
1719
|
+
function parsePrincipalUrl(url) {
|
|
1720
|
+
if (!url.startsWith(`/principal/`)) return null;
|
|
1721
|
+
const segment = url.slice(`/principal/`.length);
|
|
1722
|
+
if (!segment || segment.includes(`/`)) return null;
|
|
1723
|
+
try {
|
|
1724
|
+
return parsePrincipalKey(decodeURIComponent(segment));
|
|
1725
|
+
} catch {
|
|
1726
|
+
return null;
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1729
|
+
const BUILT_IN_SYSTEM_PRINCIPAL_IDS = new Set([
|
|
1730
|
+
`framework`,
|
|
1731
|
+
`auth-sync`,
|
|
1732
|
+
`dev-local`
|
|
1733
|
+
]);
|
|
1734
|
+
function isBuiltInSystemPrincipalUrl(url) {
|
|
1735
|
+
if (!url?.startsWith(`/principal/`)) return false;
|
|
1736
|
+
try {
|
|
1737
|
+
const principal = parsePrincipalUrl(url);
|
|
1738
|
+
if (!principal) return false;
|
|
1739
|
+
return principal.kind === `system` && BUILT_IN_SYSTEM_PRINCIPAL_IDS.has(principal.id);
|
|
1740
|
+
} catch {
|
|
1741
|
+
return false;
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
function principalFromCreatedBy(createdBy) {
|
|
1745
|
+
if (!createdBy) return void 0;
|
|
1746
|
+
const principal = parsePrincipalUrl(createdBy);
|
|
1747
|
+
if (!principal) return {
|
|
1748
|
+
url: createdBy,
|
|
1749
|
+
key: null
|
|
1750
|
+
};
|
|
1751
|
+
return {
|
|
1752
|
+
url: principal.url,
|
|
1753
|
+
key: principal.key,
|
|
1754
|
+
kind: principal.kind,
|
|
1755
|
+
id: principal.id
|
|
1756
|
+
};
|
|
1757
|
+
}
|
|
1758
|
+
const principalIdentityStateSchema = Type.Object({
|
|
1759
|
+
kind: Type.Union([
|
|
1760
|
+
Type.Literal(`user`),
|
|
1761
|
+
Type.Literal(`agent`),
|
|
1762
|
+
Type.Literal(`service`),
|
|
1763
|
+
Type.Literal(`system`)
|
|
1764
|
+
]),
|
|
1765
|
+
id: Type.String(),
|
|
1766
|
+
key: Type.String(),
|
|
1767
|
+
url: Type.String(),
|
|
1768
|
+
updated_at: Type.String(),
|
|
1769
|
+
display_name: Type.Optional(Type.String()),
|
|
1770
|
+
email: Type.Optional(Type.String()),
|
|
1771
|
+
avatar_url: Type.Optional(Type.String()),
|
|
1772
|
+
auth_provider: Type.Optional(Type.String()),
|
|
1773
|
+
auth_subject: Type.Optional(Type.String()),
|
|
1774
|
+
claims: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
|
|
1775
|
+
created_at: Type.Optional(Type.String())
|
|
1776
|
+
}, { additionalProperties: false });
|
|
1777
|
+
const principalUpdateIdentityMessageSchema = Type.Object({ identity: principalIdentityStateSchema }, { additionalProperties: false });
|
|
1778
|
+
|
|
1297
1779
|
//#endregion
|
|
1298
1780
|
//#region src/entity-projector.ts
|
|
1299
1781
|
const ENTITY_SHAPE_COLUMNS = [
|
|
@@ -1302,6 +1784,7 @@ const ENTITY_SHAPE_COLUMNS = [
|
|
|
1302
1784
|
`type`,
|
|
1303
1785
|
`status`,
|
|
1304
1786
|
`tags`,
|
|
1787
|
+
`created_by`,
|
|
1305
1788
|
`spawn_args`,
|
|
1306
1789
|
`sandbox`,
|
|
1307
1790
|
`parent`,
|
|
@@ -1321,6 +1804,12 @@ function sourceRefFromStreamPath(streamPath) {
|
|
|
1321
1804
|
const match = streamPath.match(/^\/_entities\/([^/]+)$/);
|
|
1322
1805
|
return match?.[1] ?? null;
|
|
1323
1806
|
}
|
|
1807
|
+
function principalScopedSourceRef(tagSourceRef, principalUrl$1, principalKind) {
|
|
1808
|
+
return `${tagSourceRef}-${hashString(JSON.stringify({
|
|
1809
|
+
principalKind,
|
|
1810
|
+
principalUrl: principalUrl$1
|
|
1811
|
+
}))}`;
|
|
1812
|
+
}
|
|
1324
1813
|
function sameMember(left, right) {
|
|
1325
1814
|
return JSON.stringify(left) === JSON.stringify(right);
|
|
1326
1815
|
}
|
|
@@ -1351,15 +1840,22 @@ var ProjectedEntityBridge = class {
|
|
|
1351
1840
|
sourceRef;
|
|
1352
1841
|
tags;
|
|
1353
1842
|
streamUrl;
|
|
1843
|
+
principalUrl;
|
|
1844
|
+
principalKind;
|
|
1845
|
+
permissionBypass;
|
|
1354
1846
|
currentMembers = new Map();
|
|
1355
1847
|
producer = null;
|
|
1356
1848
|
stopped = false;
|
|
1357
|
-
constructor(row, streamClient) {
|
|
1849
|
+
constructor(row, registry, streamClient) {
|
|
1850
|
+
this.registry = registry;
|
|
1358
1851
|
this.streamClient = streamClient;
|
|
1359
1852
|
this.tenantId = row.tenantId;
|
|
1360
1853
|
this.sourceRef = row.sourceRef;
|
|
1361
1854
|
this.tags = normalizeTags(row.tags);
|
|
1362
1855
|
this.streamUrl = row.streamUrl;
|
|
1856
|
+
this.principalUrl = row.principalUrl;
|
|
1857
|
+
this.principalKind = row.principalKind;
|
|
1858
|
+
this.permissionBypass = isBuiltInSystemPrincipalUrl(row.principalUrl);
|
|
1363
1859
|
}
|
|
1364
1860
|
async start(initialEntities) {
|
|
1365
1861
|
await this.ensureStream();
|
|
@@ -1373,7 +1869,7 @@ var ProjectedEntityBridge = class {
|
|
|
1373
1869
|
}
|
|
1374
1870
|
});
|
|
1375
1871
|
await this.loadCurrentMembers();
|
|
1376
|
-
this.reconcile(initialEntities);
|
|
1872
|
+
await this.reconcile(initialEntities);
|
|
1377
1873
|
}
|
|
1378
1874
|
async stop() {
|
|
1379
1875
|
this.stopped = true;
|
|
@@ -1385,12 +1881,13 @@ var ProjectedEntityBridge = class {
|
|
|
1385
1881
|
this.producer = null;
|
|
1386
1882
|
}
|
|
1387
1883
|
}
|
|
1388
|
-
reconcile(entities$1) {
|
|
1884
|
+
async reconcile(entities$1) {
|
|
1389
1885
|
if (this.stopped) return;
|
|
1390
1886
|
const staleMembers = new Map(this.currentMembers);
|
|
1391
1887
|
for (const entity of entities$1) {
|
|
1392
1888
|
if (entity.tenant_id !== this.tenantId) continue;
|
|
1393
1889
|
if (!entityMatchesTags(entity, this.tags)) continue;
|
|
1890
|
+
if (!await this.canReadEntity(entity)) continue;
|
|
1394
1891
|
staleMembers.delete(entity.url);
|
|
1395
1892
|
this.upsertEntity(entity);
|
|
1396
1893
|
}
|
|
@@ -1399,10 +1896,10 @@ var ProjectedEntityBridge = class {
|
|
|
1399
1896
|
this.currentMembers.delete(url);
|
|
1400
1897
|
}
|
|
1401
1898
|
}
|
|
1402
|
-
applyEntity(entity) {
|
|
1899
|
+
async applyEntity(entity) {
|
|
1403
1900
|
if (this.stopped) return;
|
|
1404
1901
|
if (entity.tenant_id !== this.tenantId) return;
|
|
1405
|
-
if (!entityMatchesTags(entity, this.tags)) {
|
|
1902
|
+
if (!entityMatchesTags(entity, this.tags) || !await this.canReadEntity(entity)) {
|
|
1406
1903
|
const existing = this.currentMembers.get(entity.url);
|
|
1407
1904
|
if (!existing) return;
|
|
1408
1905
|
this.append(`delete`, existing);
|
|
@@ -1431,6 +1928,15 @@ var ProjectedEntityBridge = class {
|
|
|
1431
1928
|
this.currentMembers.set(entity.url, next);
|
|
1432
1929
|
}
|
|
1433
1930
|
}
|
|
1931
|
+
async canReadEntity(entity) {
|
|
1932
|
+
if (this.permissionBypass) return true;
|
|
1933
|
+
if (!this.principalUrl || !this.principalKind) return false;
|
|
1934
|
+
if (entity.created_by === this.principalUrl) return true;
|
|
1935
|
+
return await this.registry.hasEntityPermission(entity.url, `read`, {
|
|
1936
|
+
principalUrl: this.principalUrl,
|
|
1937
|
+
principalKind: this.principalKind
|
|
1938
|
+
});
|
|
1939
|
+
}
|
|
1434
1940
|
async ensureStream() {
|
|
1435
1941
|
if (!await this.streamClient.exists(this.streamUrl)) await this.streamClient.create(this.streamUrl, { contentType: `application/json` });
|
|
1436
1942
|
}
|
|
@@ -1535,17 +2041,19 @@ var EntityProjector = class {
|
|
|
1535
2041
|
this.activeReaders.clear();
|
|
1536
2042
|
await Promise.all(projections.map((projection) => projection.stop()));
|
|
1537
2043
|
}
|
|
1538
|
-
async register(tenantId, registry, tagsInput) {
|
|
2044
|
+
async register(tenantId, registry, tagsInput, principalUrl$1, principalKind) {
|
|
1539
2045
|
if (!this.electricUrl) throw new Error(`[entity-projector] Electric URL is required for entities()`);
|
|
1540
2046
|
await this.start();
|
|
1541
2047
|
this.registries.set(tenantId, registry);
|
|
1542
2048
|
const tags = normalizeTags(assertTags(tagsInput));
|
|
1543
|
-
const sourceRef = sourceRefForTags(tags);
|
|
2049
|
+
const sourceRef = principalScopedSourceRef(sourceRefForTags(tags), principalUrl$1, principalKind);
|
|
1544
2050
|
const streamUrl = getEntitiesStreamPath(sourceRef);
|
|
1545
2051
|
const row = await registry.upsertEntityBridge({
|
|
1546
2052
|
sourceRef,
|
|
1547
2053
|
tags,
|
|
1548
|
-
streamUrl
|
|
2054
|
+
streamUrl,
|
|
2055
|
+
principalUrl: principalUrl$1,
|
|
2056
|
+
principalKind
|
|
1549
2057
|
});
|
|
1550
2058
|
await registry.touchEntityBridge(sourceRef);
|
|
1551
2059
|
await this.ensureProjection(row);
|
|
@@ -1574,7 +2082,11 @@ var EntityProjector = class {
|
|
|
1574
2082
|
await this.touchSourceRef(tenantId, registry, sourceRef, `read-close`);
|
|
1575
2083
|
};
|
|
1576
2084
|
}
|
|
1577
|
-
async onEntityChanged(
|
|
2085
|
+
async onEntityChanged(tenantId, entityUrl) {
|
|
2086
|
+
const entity = this.entities.get(entityKey(tenantId, entityUrl));
|
|
2087
|
+
if (!entity) return;
|
|
2088
|
+
for (const projection of this.projectionsForTenant(tenantId)) await projection.applyEntity(entity);
|
|
2089
|
+
}
|
|
1578
2090
|
async loadTenantBridges(tenantId, registry = this.registryForTenant(tenantId)) {
|
|
1579
2091
|
if (!this.started || !this.electricUrl) return;
|
|
1580
2092
|
await this.loadPersistedBridgesForTenant(tenantId, registry);
|
|
@@ -1635,16 +2147,16 @@ var EntityProjector = class {
|
|
|
1635
2147
|
}
|
|
1636
2148
|
if (message.headers.control === `up-to-date`) {
|
|
1637
2149
|
this.upToDate = true;
|
|
1638
|
-
this.reconcileAll();
|
|
2150
|
+
await this.reconcileAll();
|
|
1639
2151
|
this.readyResolve?.();
|
|
1640
2152
|
}
|
|
1641
2153
|
continue;
|
|
1642
2154
|
}
|
|
1643
2155
|
if (!isChangeMessage(message)) continue;
|
|
1644
|
-
this.applyChangeMessage(message);
|
|
2156
|
+
await this.applyChangeMessage(message);
|
|
1645
2157
|
}
|
|
1646
2158
|
}
|
|
1647
|
-
applyChangeMessage(message) {
|
|
2159
|
+
async applyChangeMessage(message) {
|
|
1648
2160
|
const entity = message.value;
|
|
1649
2161
|
const key = entityKey(entity.tenant_id, entity.url);
|
|
1650
2162
|
if (message.headers.operation === `delete`) {
|
|
@@ -1653,7 +2165,7 @@ var EntityProjector = class {
|
|
|
1653
2165
|
return;
|
|
1654
2166
|
}
|
|
1655
2167
|
this.entities.set(key, entity);
|
|
1656
|
-
if (this.upToDate) for (const projection of this.projectionsForTenant(entity.tenant_id)) projection.applyEntity(entity);
|
|
2168
|
+
if (this.upToDate) for (const projection of this.projectionsForTenant(entity.tenant_id)) await projection.applyEntity(entity);
|
|
1657
2169
|
}
|
|
1658
2170
|
async loadPersistedBridges() {
|
|
1659
2171
|
const registry = new PostgresRegistry(this.db);
|
|
@@ -1716,7 +2228,7 @@ var EntityProjector = class {
|
|
|
1716
2228
|
}
|
|
1717
2229
|
throw error;
|
|
1718
2230
|
}
|
|
1719
|
-
const projection = new ProjectedEntityBridge(row, streamClient);
|
|
2231
|
+
const projection = new ProjectedEntityBridge(row, this.registryForTenant(row.tenantId), streamClient);
|
|
1720
2232
|
await projection.start(this.entitiesForTenant(row.tenantId));
|
|
1721
2233
|
this.projections.set(key, projection);
|
|
1722
2234
|
})().finally(() => {
|
|
@@ -1731,8 +2243,8 @@ var EntityProjector = class {
|
|
|
1731
2243
|
projectionsForTenant(tenantId) {
|
|
1732
2244
|
return [...this.projections.values()].filter((projection) => projection.tenantId === tenantId);
|
|
1733
2245
|
}
|
|
1734
|
-
reconcileAll() {
|
|
1735
|
-
for (const projection of this.projections.values()) projection.reconcile(this.entitiesForTenant(projection.tenantId));
|
|
2246
|
+
async reconcileAll() {
|
|
2247
|
+
for (const projection of this.projections.values()) await projection.reconcile(this.entitiesForTenant(projection.tenantId));
|
|
1736
2248
|
}
|
|
1737
2249
|
async touchSourceRef(tenantId, registry, sourceRef, reason) {
|
|
1738
2250
|
try {
|
|
@@ -1774,8 +2286,8 @@ var EntityProjectorTenantFacade = class {
|
|
|
1774
2286
|
await this.projector.start();
|
|
1775
2287
|
}
|
|
1776
2288
|
async stop() {}
|
|
1777
|
-
async register(tagsInput) {
|
|
1778
|
-
return await this.projector.register(this.tenantId, this.registry, tagsInput);
|
|
2289
|
+
async register(tagsInput, principalUrl$1, principalKind) {
|
|
2290
|
+
return await this.projector.register(this.tenantId, this.registry, tagsInput, principalUrl$1, principalKind);
|
|
1779
2291
|
}
|
|
1780
2292
|
async onEntityChanged(entityUrl) {
|
|
1781
2293
|
await this.projector.onEntityChanged(this.tenantId, entityUrl);
|
|
@@ -2657,93 +3169,6 @@ function assertSharedSandboxColocated(key, chosenIsRemote, dispatchPolicy) {
|
|
|
2657
3169
|
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);
|
|
2658
3170
|
}
|
|
2659
3171
|
|
|
2660
|
-
//#endregion
|
|
2661
|
-
//#region src/principal.ts
|
|
2662
|
-
const ELECTRIC_PRINCIPAL_HEADER = `electric-principal`;
|
|
2663
|
-
const PRINCIPAL_KINDS = new Set([
|
|
2664
|
-
`user`,
|
|
2665
|
-
`agent`,
|
|
2666
|
-
`service`,
|
|
2667
|
-
`system`
|
|
2668
|
-
]);
|
|
2669
|
-
function parsePrincipalKey(input) {
|
|
2670
|
-
const colon = input.indexOf(`:`);
|
|
2671
|
-
if (colon <= 0) throw new Error(`Invalid principal identifier`);
|
|
2672
|
-
const kind = input.slice(0, colon);
|
|
2673
|
-
const id = input.slice(colon + 1);
|
|
2674
|
-
if (!PRINCIPAL_KINDS.has(kind)) throw new Error(`Invalid principal kind`);
|
|
2675
|
-
if (!id || id.includes(`/`)) throw new Error(`Invalid principal id`);
|
|
2676
|
-
const key = `${kind}:${id}`;
|
|
2677
|
-
return {
|
|
2678
|
-
kind,
|
|
2679
|
-
id,
|
|
2680
|
-
key,
|
|
2681
|
-
url: `/principal/${encodeURIComponent(key)}`
|
|
2682
|
-
};
|
|
2683
|
-
}
|
|
2684
|
-
function principalUrl(key) {
|
|
2685
|
-
return parsePrincipalKey(key).url;
|
|
2686
|
-
}
|
|
2687
|
-
function parsePrincipalUrl(url) {
|
|
2688
|
-
if (!url.startsWith(`/principal/`)) return null;
|
|
2689
|
-
const segment = url.slice(`/principal/`.length);
|
|
2690
|
-
if (!segment || segment.includes(`/`)) return null;
|
|
2691
|
-
try {
|
|
2692
|
-
return parsePrincipalKey(decodeURIComponent(segment));
|
|
2693
|
-
} catch {
|
|
2694
|
-
return null;
|
|
2695
|
-
}
|
|
2696
|
-
}
|
|
2697
|
-
const BUILT_IN_SYSTEM_PRINCIPAL_IDS = new Set([
|
|
2698
|
-
`framework`,
|
|
2699
|
-
`auth-sync`,
|
|
2700
|
-
`dev-local`
|
|
2701
|
-
]);
|
|
2702
|
-
function isBuiltInSystemPrincipalUrl(url) {
|
|
2703
|
-
if (!url?.startsWith(`/principal/`)) return false;
|
|
2704
|
-
try {
|
|
2705
|
-
const principal = parsePrincipalUrl(url);
|
|
2706
|
-
if (!principal) return false;
|
|
2707
|
-
return principal.kind === `system` && BUILT_IN_SYSTEM_PRINCIPAL_IDS.has(principal.id);
|
|
2708
|
-
} catch {
|
|
2709
|
-
return false;
|
|
2710
|
-
}
|
|
2711
|
-
}
|
|
2712
|
-
function principalFromCreatedBy(createdBy) {
|
|
2713
|
-
if (!createdBy) return void 0;
|
|
2714
|
-
const principal = parsePrincipalUrl(createdBy);
|
|
2715
|
-
if (!principal) return {
|
|
2716
|
-
url: createdBy,
|
|
2717
|
-
key: null
|
|
2718
|
-
};
|
|
2719
|
-
return {
|
|
2720
|
-
url: principal.url,
|
|
2721
|
-
key: principal.key,
|
|
2722
|
-
kind: principal.kind,
|
|
2723
|
-
id: principal.id
|
|
2724
|
-
};
|
|
2725
|
-
}
|
|
2726
|
-
const principalIdentityStateSchema = Type.Object({
|
|
2727
|
-
kind: Type.Union([
|
|
2728
|
-
Type.Literal(`user`),
|
|
2729
|
-
Type.Literal(`agent`),
|
|
2730
|
-
Type.Literal(`service`),
|
|
2731
|
-
Type.Literal(`system`)
|
|
2732
|
-
]),
|
|
2733
|
-
id: Type.String(),
|
|
2734
|
-
key: Type.String(),
|
|
2735
|
-
url: Type.String(),
|
|
2736
|
-
updated_at: Type.String(),
|
|
2737
|
-
display_name: Type.Optional(Type.String()),
|
|
2738
|
-
email: Type.Optional(Type.String()),
|
|
2739
|
-
avatar_url: Type.Optional(Type.String()),
|
|
2740
|
-
auth_provider: Type.Optional(Type.String()),
|
|
2741
|
-
auth_subject: Type.Optional(Type.String()),
|
|
2742
|
-
claims: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
|
|
2743
|
-
created_at: Type.Optional(Type.String())
|
|
2744
|
-
}, { additionalProperties: false });
|
|
2745
|
-
const principalUpdateIdentityMessageSchema = Type.Object({ identity: principalIdentityStateSchema }, { additionalProperties: false });
|
|
2746
|
-
|
|
2747
3172
|
//#endregion
|
|
2748
3173
|
//#region src/manifest-side-effects.ts
|
|
2749
3174
|
function isRecord$1(value) {
|
|
@@ -2939,6 +3364,7 @@ var EntityManager = class {
|
|
|
2939
3364
|
this.validateSchema(req.creation_schema);
|
|
2940
3365
|
this.validateSchemaMap(req.inbox_schemas);
|
|
2941
3366
|
this.validateSchemaMap(req.state_schemas);
|
|
3367
|
+
this.validateSlashCommands(req.slash_commands);
|
|
2942
3368
|
const defaultDispatchPolicy = req.default_dispatch_policy ? this.validateDispatchPolicy(req.default_dispatch_policy, { label: `default_dispatch_policy` }) : void 0;
|
|
2943
3369
|
const existing = await this.registry.getEntityType(req.name);
|
|
2944
3370
|
const now = new Date().toISOString();
|
|
@@ -2948,6 +3374,7 @@ var EntityManager = class {
|
|
|
2948
3374
|
creation_schema: req.creation_schema,
|
|
2949
3375
|
inbox_schemas: req.inbox_schemas,
|
|
2950
3376
|
state_schemas: req.state_schemas,
|
|
3377
|
+
slash_commands: req.slash_commands,
|
|
2951
3378
|
serve_endpoint: req.serve_endpoint,
|
|
2952
3379
|
default_dispatch_policy: defaultDispatchPolicy,
|
|
2953
3380
|
revision: existing ? existing.revision + 1 : 1,
|
|
@@ -2979,7 +3406,10 @@ var EntityManager = class {
|
|
|
2979
3406
|
}
|
|
2980
3407
|
async ensurePrincipal(principal) {
|
|
2981
3408
|
const existing = await this.registry.getEntity(principal.url);
|
|
2982
|
-
if (existing)
|
|
3409
|
+
if (existing) {
|
|
3410
|
+
await this.ensureUserPrincipal(principal);
|
|
3411
|
+
return existing;
|
|
3412
|
+
}
|
|
2983
3413
|
await this.ensurePrincipalEntityType();
|
|
2984
3414
|
try {
|
|
2985
3415
|
const entity = await this.spawn(`principal`, {
|
|
@@ -3008,15 +3438,22 @@ var EntityManager = class {
|
|
|
3008
3438
|
updated_at: now
|
|
3009
3439
|
}
|
|
3010
3440
|
}));
|
|
3441
|
+
await this.ensureUserPrincipal(principal);
|
|
3011
3442
|
return entity;
|
|
3012
3443
|
} catch (error) {
|
|
3013
3444
|
if (error instanceof ElectricAgentsError && error.code === ErrCodeDuplicateURL) {
|
|
3014
3445
|
const raced = await this.registry.getEntity(principal.url);
|
|
3015
|
-
if (raced)
|
|
3446
|
+
if (raced) {
|
|
3447
|
+
await this.ensureUserPrincipal(principal);
|
|
3448
|
+
return raced;
|
|
3449
|
+
}
|
|
3016
3450
|
}
|
|
3017
3451
|
throw error;
|
|
3018
3452
|
}
|
|
3019
3453
|
}
|
|
3454
|
+
async ensureUserPrincipal(principal) {
|
|
3455
|
+
if (principal.kind === `user`) await this.registry.ensureUserForPrincipal(principal);
|
|
3456
|
+
}
|
|
3020
3457
|
/**
|
|
3021
3458
|
* Spawn a new entity of the given type with durable streams.
|
|
3022
3459
|
*/
|
|
@@ -3046,7 +3483,6 @@ var EntityManager = class {
|
|
|
3046
3483
|
const writeToken = randomUUID();
|
|
3047
3484
|
const entityURL = typeName === `principal` ? principalUrl(instanceId) : `/${typeName}/${instanceId}`;
|
|
3048
3485
|
const mainPath = `${entityURL}/main`;
|
|
3049
|
-
const errorPath = `${entityURL}/error`;
|
|
3050
3486
|
const subscriptionId = `${typeName}-handler`;
|
|
3051
3487
|
const spawnT0 = performance.now();
|
|
3052
3488
|
const existingByURL = await this.registry.getEntity(entityURL);
|
|
@@ -3063,10 +3499,7 @@ var EntityManager = class {
|
|
|
3063
3499
|
type: typeName,
|
|
3064
3500
|
status: `idle`,
|
|
3065
3501
|
url: entityURL,
|
|
3066
|
-
streams: {
|
|
3067
|
-
main: mainPath,
|
|
3068
|
-
error: errorPath
|
|
3069
|
-
},
|
|
3502
|
+
streams: { main: mainPath },
|
|
3070
3503
|
subscription_id: subscriptionId,
|
|
3071
3504
|
dispatch_policy: dispatchPolicy,
|
|
3072
3505
|
write_token: writeToken,
|
|
@@ -3103,6 +3536,18 @@ var EntityManager = class {
|
|
|
3103
3536
|
}
|
|
3104
3537
|
});
|
|
3105
3538
|
const initialEvents = [createdEvent];
|
|
3539
|
+
const slashCommandTimestamp = new Date().toISOString();
|
|
3540
|
+
for (const command of entityType.slash_commands ?? []) {
|
|
3541
|
+
const slashCommandEvent = entityStateSchema.slashCommands.insert({
|
|
3542
|
+
key: command.name,
|
|
3543
|
+
value: {
|
|
3544
|
+
...command,
|
|
3545
|
+
source: `static`,
|
|
3546
|
+
updated_at: slashCommandTimestamp
|
|
3547
|
+
}
|
|
3548
|
+
});
|
|
3549
|
+
initialEvents.push(slashCommandEvent);
|
|
3550
|
+
}
|
|
3106
3551
|
if (req.initialMessage !== void 0) {
|
|
3107
3552
|
const msgNow = new Date().toISOString();
|
|
3108
3553
|
const inboxEvent = entityStateSchema.inbox.insert({
|
|
@@ -3110,6 +3555,7 @@ var EntityManager = class {
|
|
|
3110
3555
|
value: {
|
|
3111
3556
|
from: req.created_by ?? req.parent ?? `spawn`,
|
|
3112
3557
|
payload: req.initialMessage,
|
|
3558
|
+
message_type: req.initialMessageType,
|
|
3113
3559
|
timestamp: msgNow
|
|
3114
3560
|
}
|
|
3115
3561
|
});
|
|
@@ -3119,55 +3565,43 @@ var EntityManager = class {
|
|
|
3119
3565
|
const queueEnterT0 = performance.now();
|
|
3120
3566
|
const queueWaiting = this.spawnPersistQueue.length();
|
|
3121
3567
|
const queueRunning = this.spawnPersistQueue.running();
|
|
3122
|
-
const [mainStreamResult,
|
|
3568
|
+
const [mainStreamResult, entityResult] = await this.spawnPersistQueue.push(async () => {
|
|
3123
3569
|
let entityTxid;
|
|
3124
3570
|
try {
|
|
3125
3571
|
entityTxid = await withSpan(`db.createEntity`, () => this.registry.createEntity(entityData));
|
|
3126
3572
|
} catch (err) {
|
|
3127
|
-
return [
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
value: void 0
|
|
3135
|
-
},
|
|
3136
|
-
{
|
|
3137
|
-
status: `rejected`,
|
|
3138
|
-
reason: err
|
|
3139
|
-
}
|
|
3140
|
-
];
|
|
3573
|
+
return [{
|
|
3574
|
+
status: `fulfilled`,
|
|
3575
|
+
value: void 0
|
|
3576
|
+
}, {
|
|
3577
|
+
status: `rejected`,
|
|
3578
|
+
reason: err
|
|
3579
|
+
}];
|
|
3141
3580
|
}
|
|
3142
|
-
const [mainStreamResult$1
|
|
3581
|
+
const [mainStreamResult$1] = await Promise.allSettled([this.streamClient.create(mainPath, {
|
|
3143
3582
|
contentType,
|
|
3144
3583
|
body: initialBody
|
|
3145
|
-
})
|
|
3146
|
-
return [
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
status: `fulfilled`,
|
|
3151
|
-
value: entityTxid
|
|
3152
|
-
}
|
|
3153
|
-
];
|
|
3584
|
+
})]);
|
|
3585
|
+
return [mainStreamResult$1, {
|
|
3586
|
+
status: `fulfilled`,
|
|
3587
|
+
value: entityTxid
|
|
3588
|
+
}];
|
|
3154
3589
|
});
|
|
3155
3590
|
const parallelMs = +(performance.now() - queueEnterT0).toFixed(2);
|
|
3156
|
-
if (mainStreamResult.status === `rejected` ||
|
|
3591
|
+
if (mainStreamResult.status === `rejected` || entityResult.status === `rejected`) {
|
|
3157
3592
|
const entityReason = entityResult.status === `rejected` ? entityResult.reason : null;
|
|
3158
|
-
const streamReason = mainStreamResult.status === `rejected` ? mainStreamResult.reason :
|
|
3593
|
+
const streamReason = mainStreamResult.status === `rejected` ? mainStreamResult.reason : null;
|
|
3159
3594
|
const isDuplicate = entityReason instanceof EntityAlreadyExistsError;
|
|
3160
3595
|
const isStreamConflict = !!streamReason && typeof streamReason === `object` && (`status` in streamReason && streamReason.status === 409 || `code` in streamReason && streamReason.code === `CONFLICT_SEQ`);
|
|
3161
3596
|
const rollbacks = [];
|
|
3162
3597
|
if (!isDuplicate && !isStreamConflict) {
|
|
3163
3598
|
if (mainStreamResult.status === `fulfilled`) rollbacks.push(this.streamClient.delete(mainPath));
|
|
3164
|
-
if (errorStreamResult.status === `fulfilled`) rollbacks.push(this.streamClient.delete(errorPath));
|
|
3165
3599
|
if (entityResult.status === `fulfilled`) rollbacks.push(this.registry.deleteEntity(entityURL));
|
|
3166
3600
|
if (req.wake) rollbacks.push(this.wakeRegistry.unregisterBySubscriberAndSource(req.wake.subscriberUrl, entityURL, this.tenantId));
|
|
3167
3601
|
await Promise.allSettled(rollbacks);
|
|
3168
3602
|
}
|
|
3169
3603
|
if (isDuplicate || isStreamConflict) throw new ElectricAgentsError(ErrCodeDuplicateURL, `Entity already exists at URL "${entityURL}"`, 409);
|
|
3170
|
-
const failure = mainStreamResult.status === `rejected` ? mainStreamResult.reason :
|
|
3604
|
+
const failure = mainStreamResult.status === `rejected` ? mainStreamResult.reason : entityResult.reason;
|
|
3171
3605
|
if (failure instanceof Error) throw failure;
|
|
3172
3606
|
throw new ElectricAgentsError(`SPAWN_FAILED`, `Spawn failed: ${String(failure)}`, 500);
|
|
3173
3607
|
}
|
|
@@ -3252,7 +3686,7 @@ var EntityManager = class {
|
|
|
3252
3686
|
});
|
|
3253
3687
|
const sharedStateIdMap = await this.buildForkSharedStateIdMap(snapshot.sharedStateIds, suffix);
|
|
3254
3688
|
const stringMap = this.buildForkStringMap(entityUrlMap, sharedStateIdMap);
|
|
3255
|
-
const entityPlans = this.buildForkEntityPlans(effectiveSubtree, entityUrlMap, stringMap);
|
|
3689
|
+
const entityPlans = this.buildForkEntityPlans(effectiveSubtree, entityUrlMap, stringMap, opts.createdBy);
|
|
3256
3690
|
this.addForkLocks(this.forkWriteLockedEntities, effectiveSubtree.map((entity) => entity.url), writeEntityLocks);
|
|
3257
3691
|
this.addForkLocks(this.forkWriteLockedStreams, [...snapshot.sharedStateIds].map((id) => getSharedStateStreamPath(id)), writeStreamLocks);
|
|
3258
3692
|
const createdStreams = [];
|
|
@@ -3263,8 +3697,6 @@ var EntityManager = class {
|
|
|
3263
3697
|
const isRoot = plan.source.url === rootUrl;
|
|
3264
3698
|
await this.streamClient.fork(plan.fork.streams.main, plan.source.streams.main, isRoot && opts.forkPointer ? { forkPointer: opts.forkPointer } : void 0);
|
|
3265
3699
|
createdStreams.push(plan.fork.streams.main);
|
|
3266
|
-
await this.streamClient.fork(plan.fork.streams.error, plan.source.streams.error);
|
|
3267
|
-
createdStreams.push(plan.fork.streams.error);
|
|
3268
3700
|
}
|
|
3269
3701
|
for (const [sourceId, forkId] of sharedStateIdMap) {
|
|
3270
3702
|
const sourcePath = getSharedStateStreamPath(sourceId);
|
|
@@ -3598,7 +4030,6 @@ var EntityManager = class {
|
|
|
3598
4030
|
for (const [sourceUrl, forkUrl] of entityUrlMap) {
|
|
3599
4031
|
stringMap.set(sourceUrl, forkUrl);
|
|
3600
4032
|
stringMap.set(`${sourceUrl}/main`, `${forkUrl}/main`);
|
|
3601
|
-
stringMap.set(`${sourceUrl}/error`, `${forkUrl}/error`);
|
|
3602
4033
|
}
|
|
3603
4034
|
for (const [sourceId, forkId] of sharedStateIdMap) {
|
|
3604
4035
|
stringMap.set(sourceId, forkId);
|
|
@@ -3606,7 +4037,7 @@ var EntityManager = class {
|
|
|
3606
4037
|
}
|
|
3607
4038
|
return stringMap;
|
|
3608
4039
|
}
|
|
3609
|
-
buildForkEntityPlans(entitiesToFork, entityUrlMap, stringMap) {
|
|
4040
|
+
buildForkEntityPlans(entitiesToFork, entityUrlMap, stringMap, createdBy) {
|
|
3610
4041
|
const now = Date.now();
|
|
3611
4042
|
return entitiesToFork.map((source) => {
|
|
3612
4043
|
const forkUrl = entityUrlMap.get(source.url);
|
|
@@ -3619,14 +4050,12 @@ var EntityManager = class {
|
|
|
3619
4050
|
url: forkUrl,
|
|
3620
4051
|
type,
|
|
3621
4052
|
status: `idle`,
|
|
3622
|
-
streams: {
|
|
3623
|
-
main: `${forkUrl}/main`,
|
|
3624
|
-
error: `${forkUrl}/error`
|
|
3625
|
-
},
|
|
4053
|
+
streams: { main: `${forkUrl}/main` },
|
|
3626
4054
|
subscription_id: `${type}-handler`,
|
|
3627
4055
|
write_token: randomUUID(),
|
|
3628
4056
|
spawn_args: spawnArgs,
|
|
3629
4057
|
parent,
|
|
4058
|
+
created_by: createdBy ?? source.created_by,
|
|
3630
4059
|
created_at: now,
|
|
3631
4060
|
updated_at: now
|
|
3632
4061
|
};
|
|
@@ -3860,7 +4289,7 @@ var EntityManager = class {
|
|
|
3860
4289
|
}
|
|
3861
4290
|
async materializeForkManifestSideEffects(entityUrl, manifests) {
|
|
3862
4291
|
for (const [manifestKey, manifest] of manifests) {
|
|
3863
|
-
await this.
|
|
4292
|
+
await this.syncManifestLinks(entityUrl, manifestKey, `upsert`, manifest);
|
|
3864
4293
|
const wake = buildManifestWakeRegistration(entityUrl, manifest, manifestKey);
|
|
3865
4294
|
if (wake) await this.wakeRegistry.register({
|
|
3866
4295
|
...wake,
|
|
@@ -3890,6 +4319,7 @@ var EntityManager = class {
|
|
|
3890
4319
|
await this.scheduler.syncManifestDelayedSend(ownerEntityUrl, manifestKey, {
|
|
3891
4320
|
entityUrl: targetUrl,
|
|
3892
4321
|
from: senderUrl,
|
|
4322
|
+
from_agent: senderUrl,
|
|
3893
4323
|
payload: manifest.payload,
|
|
3894
4324
|
key: `scheduled-${producerId}`,
|
|
3895
4325
|
type: typeof manifest.messageType === `string` ? manifest.messageType : void 0,
|
|
@@ -3929,12 +4359,14 @@ var EntityManager = class {
|
|
|
3929
4359
|
const now = new Date().toISOString();
|
|
3930
4360
|
const key = req.key ?? `msg-in-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
3931
4361
|
const value = {
|
|
3932
|
-
from: req.from,
|
|
4362
|
+
from: req.from_principal ?? req.from,
|
|
3933
4363
|
payload: req.payload,
|
|
3934
4364
|
timestamp: now,
|
|
3935
4365
|
mode: req.mode ?? `immediate`,
|
|
3936
4366
|
status: req.mode === `queued` || req.mode === `paused` ? `pending` : `processed`
|
|
3937
4367
|
};
|
|
4368
|
+
if (req.from_principal) value.from_principal = req.from_principal;
|
|
4369
|
+
if (req.from_agent) value.from_agent = req.from_agent;
|
|
3938
4370
|
if (req.type) value.message_type = req.type;
|
|
3939
4371
|
if (req.position) value.position = req.position;
|
|
3940
4372
|
else if (value.mode === `queued` || value.mode === `paused`) value.position = createInitialQueuePosition(new Date(now));
|
|
@@ -4106,9 +4538,9 @@ var EntityManager = class {
|
|
|
4106
4538
|
if (result.changed && this.entityBridgeManager) await this.entityBridgeManager.onEntityChanged(entityUrl);
|
|
4107
4539
|
return updated;
|
|
4108
4540
|
}
|
|
4109
|
-
async ensureEntitiesMembershipStream(tags) {
|
|
4541
|
+
async ensureEntitiesMembershipStream(tags, principal) {
|
|
4110
4542
|
if (!this.entityBridgeManager) throw new Error(`Entity bridge manager not configured`);
|
|
4111
|
-
return this.entityBridgeManager.register(this.validateTags(tags));
|
|
4543
|
+
return this.entityBridgeManager.register(this.validateTags(tags), principal.url, principal.kind);
|
|
4112
4544
|
}
|
|
4113
4545
|
async writeManifestEntry(entityUrl, key, operation, value, opts) {
|
|
4114
4546
|
const entity = await this.registry.getEntity(entityUrl);
|
|
@@ -4126,11 +4558,11 @@ var EntityManager = class {
|
|
|
4126
4558
|
const encoded = this.encodeChangeEvent(event);
|
|
4127
4559
|
if (opts?.producerId) {
|
|
4128
4560
|
await this.streamClient.appendIdempotent(entity.streams.main, encoded, { producerId: opts.producerId });
|
|
4129
|
-
await this.
|
|
4561
|
+
await this.syncManifestLinks(entityUrl, key, operation, value);
|
|
4130
4562
|
return;
|
|
4131
4563
|
}
|
|
4132
4564
|
await this.streamClient.append(entity.streams.main, encoded);
|
|
4133
|
-
await this.
|
|
4565
|
+
await this.syncManifestLinks(entityUrl, key, operation, value);
|
|
4134
4566
|
}
|
|
4135
4567
|
async upsertCronSchedule(entityUrl, req) {
|
|
4136
4568
|
if (req.payload === void 0) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Missing required field: payload`, 400);
|
|
@@ -4279,6 +4711,8 @@ var EntityManager = class {
|
|
|
4279
4711
|
await this.scheduler.enqueueDelayedSend({
|
|
4280
4712
|
entityUrl,
|
|
4281
4713
|
from: req.from,
|
|
4714
|
+
from_principal: req.from_principal,
|
|
4715
|
+
from_agent: req.from_agent,
|
|
4282
4716
|
payload: req.payload,
|
|
4283
4717
|
key: req.key,
|
|
4284
4718
|
type: req.type,
|
|
@@ -4321,14 +4755,23 @@ var EntityManager = class {
|
|
|
4321
4755
|
await this.streamClient.appendIdempotent(subscriber.streams.main, this.encodeChangeEvent(wakeEvent), { producerId: `wake-reg-${result.registrationDbId}-${result.sourceEventKey}` });
|
|
4322
4756
|
});
|
|
4323
4757
|
}
|
|
4324
|
-
async
|
|
4758
|
+
async syncManifestLinks(entityUrl, manifestKey, operation, value) {
|
|
4325
4759
|
const sourceRef = operation === `delete` ? void 0 : this.extractEntitiesSourceRef(value);
|
|
4326
4760
|
await this.registry.replaceEntityManifestSource(entityUrl, manifestKey, sourceRef);
|
|
4761
|
+
const sharedStateId = operation === `delete` ? void 0 : this.extractSharedStateId(value);
|
|
4762
|
+
await this.registry.replaceSharedStateLink(entityUrl, manifestKey, sharedStateId);
|
|
4327
4763
|
}
|
|
4328
4764
|
extractEntitiesSourceRef(manifest) {
|
|
4329
4765
|
if (manifest?.kind === `source` && manifest.sourceType === `entities` && typeof manifest.sourceRef === `string`) return manifest.sourceRef;
|
|
4330
4766
|
return void 0;
|
|
4331
4767
|
}
|
|
4768
|
+
extractSharedStateId(manifest) {
|
|
4769
|
+
if (manifest?.kind === `shared-state` && typeof manifest.id === `string`) return manifest.id;
|
|
4770
|
+
if (manifest?.kind !== `source` || manifest.sourceType !== `db`) return void 0;
|
|
4771
|
+
if (typeof manifest.sourceRef === `string`) return manifest.sourceRef;
|
|
4772
|
+
const config = isRecord(manifest.config) ? manifest.config : void 0;
|
|
4773
|
+
return typeof config?.id === `string` ? config.id : void 0;
|
|
4774
|
+
}
|
|
4332
4775
|
/**
|
|
4333
4776
|
* Read a child entity's stream and extract concatenated text deltas
|
|
4334
4777
|
* for a specific run, plus any error messages for that run.
|
|
@@ -4492,14 +4935,7 @@ var EntityManager = class {
|
|
|
4492
4935
|
await this.streamClient.append(entity.streams.main, signalData);
|
|
4493
4936
|
return;
|
|
4494
4937
|
}
|
|
4495
|
-
const
|
|
4496
|
-
type: `signal`,
|
|
4497
|
-
key: signalEvent.key,
|
|
4498
|
-
value: signalEvent.value,
|
|
4499
|
-
headers: signalEvent.headers
|
|
4500
|
-
};
|
|
4501
|
-
const errorSignalData = this.encodeChangeEvent(errorCloseEvent);
|
|
4502
|
-
for (const [streamPath, data] of [[entity.streams.main, signalData], [entity.streams.error, errorSignalData]]) try {
|
|
4938
|
+
for (const [streamPath, data] of [[entity.streams.main, signalData]]) try {
|
|
4503
4939
|
await this.streamClient.append(streamPath, data, { close: true });
|
|
4504
4940
|
} catch (err) {
|
|
4505
4941
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -4565,7 +5001,9 @@ var EntityManager = class {
|
|
|
4565
5001
|
creation_schema: existing.creation_schema,
|
|
4566
5002
|
inbox_schemas: mergedInbox,
|
|
4567
5003
|
state_schemas: mergedState,
|
|
5004
|
+
slash_commands: existing.slash_commands,
|
|
4568
5005
|
serve_endpoint: existing.serve_endpoint,
|
|
5006
|
+
default_dispatch_policy: existing.default_dispatch_policy,
|
|
4569
5007
|
revision: nextRevision,
|
|
4570
5008
|
created_at: existing.created_at,
|
|
4571
5009
|
updated_at: now
|
|
@@ -4619,11 +5057,19 @@ var EntityManager = class {
|
|
|
4619
5057
|
throw new ElectricAgentsError(ErrCodeInvalidRequest, error instanceof Error ? error.message : `Invalid tags`, 400);
|
|
4620
5058
|
}
|
|
4621
5059
|
}
|
|
5060
|
+
validateSlashCommands(input) {
|
|
5061
|
+
const validationError = validateSlashCommandDefinitions(input);
|
|
5062
|
+
if (!validationError) return;
|
|
5063
|
+
throw new ElectricAgentsError(ErrCodeSchemaValidationFailed, validationError.message, 422, validationError.details);
|
|
5064
|
+
}
|
|
4622
5065
|
async validateSendRequest(entityUrl, req) {
|
|
4623
5066
|
const entity = await this.registry.getEntity(entityUrl);
|
|
4624
5067
|
if (!entity) throw new ElectricAgentsError(ErrCodeNotFound, `Entity not found`, 404);
|
|
4625
5068
|
if (rejectsNormalWrites(entity.status)) throw new ElectricAgentsError(ErrCodeNotRunning, `Entity is not accepting writes`, 409);
|
|
4626
|
-
if (req.type
|
|
5069
|
+
if (req.type === COMPOSER_INPUT_MESSAGE_TYPE) {
|
|
5070
|
+
const valErr = validateComposerInputPayload(req.payload);
|
|
5071
|
+
if (valErr) throw new ElectricAgentsError(ErrCodeSchemaValidationFailed, valErr.message, 422, valErr.details);
|
|
5072
|
+
} else if (req.type && entity.type) {
|
|
4627
5073
|
const { inboxSchemas } = await this.getEffectiveSchemas(entity);
|
|
4628
5074
|
if (inboxSchemas) {
|
|
4629
5075
|
const schema = inboxSchemas[req.type];
|
|
@@ -5482,6 +5928,8 @@ var ElectricAgentsTenantRuntime = class {
|
|
|
5482
5928
|
try {
|
|
5483
5929
|
await this.manager.send(payload.entityUrl, {
|
|
5484
5930
|
from: payload.from,
|
|
5931
|
+
from_principal: payload.from_principal,
|
|
5932
|
+
from_agent: payload.from_agent,
|
|
5485
5933
|
payload: payload.payload,
|
|
5486
5934
|
key: payload.key ?? `scheduled-task-${taskId}`,
|
|
5487
5935
|
type: payload.type
|
|
@@ -5554,6 +6002,7 @@ var ElectricAgentsTenantRuntime = class {
|
|
|
5554
6002
|
await this.scheduler.syncManifestDelayedSend(ownerEntityUrl, manifestKey, {
|
|
5555
6003
|
entityUrl: targetUrl,
|
|
5556
6004
|
from: senderUrl,
|
|
6005
|
+
from_agent: senderUrl,
|
|
5557
6006
|
payload: value.payload,
|
|
5558
6007
|
key: `scheduled-${producerId}`,
|
|
5559
6008
|
type: typeof value.messageType === `string` ? value.messageType : void 0,
|
|
@@ -5578,11 +6027,20 @@ var ElectricAgentsTenantRuntime = class {
|
|
|
5578
6027
|
async applyManifestEntitySource(ownerEntityUrl, manifestKey, operation, value) {
|
|
5579
6028
|
const sourceRef = operation === `delete` ? void 0 : this.extractEntitiesSourceRef(value);
|
|
5580
6029
|
await this.manager.registry.replaceEntityManifestSource(ownerEntityUrl, manifestKey, sourceRef);
|
|
6030
|
+
const sharedStateId = operation === `delete` ? void 0 : this.extractSharedStateId(value);
|
|
6031
|
+
await this.manager.registry.replaceSharedStateLink(ownerEntityUrl, manifestKey, sharedStateId);
|
|
5581
6032
|
}
|
|
5582
6033
|
extractEntitiesSourceRef(manifest) {
|
|
5583
6034
|
if (manifest?.kind === `source` && manifest.sourceType === `entities` && typeof manifest.sourceRef === `string`) return manifest.sourceRef;
|
|
5584
6035
|
return void 0;
|
|
5585
6036
|
}
|
|
6037
|
+
extractSharedStateId(manifest) {
|
|
6038
|
+
if (manifest?.kind === `shared-state` && typeof manifest.id === `string`) return manifest.id;
|
|
6039
|
+
if (manifest?.kind !== `source` || manifest.sourceType !== `db`) return void 0;
|
|
6040
|
+
if (typeof manifest.sourceRef === `string`) return manifest.sourceRef;
|
|
6041
|
+
const config = typeof manifest.config === `object` && manifest.config !== null && !Array.isArray(manifest.config) ? manifest.config : void 0;
|
|
6042
|
+
return typeof config?.id === `string` ? config.id : void 0;
|
|
6043
|
+
}
|
|
5586
6044
|
async maybeMarkEntityIdleAfterRunFinished(entityUrl) {
|
|
5587
6045
|
const primaryStream = `${entityUrl}/main`;
|
|
5588
6046
|
const callbacks = await this.db.select().from(consumerCallbacks).where(and(eq(consumerCallbacks.tenantId, this.serviceId), eq(consumerCallbacks.primaryStream, primaryStream))).limit(1);
|
|
@@ -6263,6 +6721,8 @@ var WakeRegistry = class {
|
|
|
6263
6721
|
if (eventType === `inbox`) {
|
|
6264
6722
|
const value = event.value;
|
|
6265
6723
|
if (typeof value?.from === `string`) change.from = value.from;
|
|
6724
|
+
if (typeof value?.from_principal === `string`) change.from_principal = value.from_principal;
|
|
6725
|
+
if (typeof value?.from_agent === `string`) change.from_agent = value.from_agent;
|
|
6266
6726
|
if (`payload` in (value ?? {})) change.payload = value?.payload;
|
|
6267
6727
|
if (typeof value?.timestamp === `string`) change.timestamp = value.timestamp;
|
|
6268
6728
|
if (typeof value?.message_type === `string`) change.message_type = value.message_type;
|
|
@@ -6674,29 +7134,136 @@ function buildElectricProxyTarget(options) {
|
|
|
6674
7134
|
if (options.electricSecret) target.searchParams.set(`secret`, options.electricSecret);
|
|
6675
7135
|
const table = options.incomingUrl.searchParams.get(`table`);
|
|
6676
7136
|
if (table === `entities`) {
|
|
6677
|
-
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"`);
|
|
6678
|
-
|
|
7137
|
+
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"`);
|
|
7138
|
+
applyShapeWhere(target, buildReadableEntitiesWhere({
|
|
7139
|
+
tenantId: options.tenantId,
|
|
7140
|
+
principalUrl: options.principalUrl ?? ``,
|
|
7141
|
+
principalKind: options.principalKind ?? ``,
|
|
7142
|
+
permissionBypass: options.permissionBypass
|
|
7143
|
+
}));
|
|
6679
7144
|
} else if (table === `entity_types`) {
|
|
6680
|
-
target.searchParams.set(`columns`, `"tenant_id","name","description","creation_schema","inbox_schemas","state_schemas","serve_endpoint","default_dispatch_policy","revision","created_at","updated_at"`);
|
|
6681
|
-
|
|
7145
|
+
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"`);
|
|
7146
|
+
applyShapeWhere(target, buildSpawnableEntityTypesWhere({
|
|
7147
|
+
tenantId: options.tenantId,
|
|
7148
|
+
principalUrl: options.principalUrl ?? ``,
|
|
7149
|
+
principalKind: options.principalKind ?? ``,
|
|
7150
|
+
permissionBypass: options.permissionBypass
|
|
7151
|
+
}));
|
|
6682
7152
|
} else if (table === `runners`) {
|
|
6683
7153
|
target.searchParams.set(`columns`, `"tenant_id","id","owner_principal","label","kind","admin_status","wake_stream","sandbox_profiles","created_at","updated_at"`);
|
|
6684
7154
|
applyTenantShapeWhere(target, options.tenantId, [`owner_principal = ${sqlStringLiteral(options.principalUrl ?? ``)}`]);
|
|
7155
|
+
} else if (table === `users`) {
|
|
7156
|
+
target.searchParams.set(`columns`, `"tenant_id","id","display_name","email","avatar_url","created_at","updated_at"`);
|
|
7157
|
+
applyTenantShapeWhere(target, options.tenantId);
|
|
7158
|
+
} else if (table === `entity_effective_permissions`) {
|
|
7159
|
+
target.searchParams.set(`columns`, `"tenant_id","id","entity_url","source_entity_url","source_grant_id","permission","subject_kind","subject_value","expires_at","created_at"`);
|
|
7160
|
+
applyShapeWhere(target, buildCurrentPrincipalEntityEffectivePermissionsWhere({
|
|
7161
|
+
tenantId: options.tenantId,
|
|
7162
|
+
principalUrl: options.principalUrl ?? ``,
|
|
7163
|
+
principalKind: options.principalKind ?? ``,
|
|
7164
|
+
permissionBypass: options.permissionBypass
|
|
7165
|
+
}));
|
|
6685
7166
|
} else if (table === `runner_runtime_diagnostics`) {
|
|
6686
7167
|
target.searchParams.set(`columns`, `"tenant_id","runner_id","owner_principal","wake_stream_offset","last_seen_at","liveness_lease_expires_at","diagnostics","updated_at"`);
|
|
6687
7168
|
applyTenantShapeWhere(target, options.tenantId, [`owner_principal = ${sqlStringLiteral(options.principalUrl ?? ``)}`]);
|
|
6688
7169
|
} else if (table === `entity_dispatch_state`) {
|
|
6689
7170
|
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"`);
|
|
6690
|
-
|
|
7171
|
+
applyShapeWhere(target, buildReadableEntityUrlWhere({
|
|
7172
|
+
tenantId: options.tenantId,
|
|
7173
|
+
principalUrl: options.principalUrl ?? ``,
|
|
7174
|
+
principalKind: options.principalKind ?? ``,
|
|
7175
|
+
permissionBypass: options.permissionBypass
|
|
7176
|
+
}));
|
|
6691
7177
|
} else if (table === `wake_notifications`) {
|
|
6692
7178
|
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"`);
|
|
6693
|
-
|
|
7179
|
+
applyShapeWhere(target, buildReadableEntityUrlWhere({
|
|
7180
|
+
tenantId: options.tenantId,
|
|
7181
|
+
principalUrl: options.principalUrl ?? ``,
|
|
7182
|
+
principalKind: options.principalKind ?? ``,
|
|
7183
|
+
permissionBypass: options.permissionBypass
|
|
7184
|
+
}));
|
|
6694
7185
|
} else if (table === `consumer_claims`) {
|
|
6695
7186
|
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"`);
|
|
6696
|
-
|
|
7187
|
+
applyShapeWhere(target, buildReadableEntityUrlWhere({
|
|
7188
|
+
tenantId: options.tenantId,
|
|
7189
|
+
principalUrl: options.principalUrl ?? ``,
|
|
7190
|
+
principalKind: options.principalKind ?? ``,
|
|
7191
|
+
permissionBypass: options.permissionBypass
|
|
7192
|
+
}));
|
|
6697
7193
|
}
|
|
6698
7194
|
return target;
|
|
6699
7195
|
}
|
|
7196
|
+
function buildReadableEntitiesWhere(options) {
|
|
7197
|
+
const tenant = sqlStringLiteral(options.tenantId);
|
|
7198
|
+
if (options.permissionBypass) return `tenant_id = ${tenant}`;
|
|
7199
|
+
const principalUrl$1 = sqlStringLiteral(options.principalUrl);
|
|
7200
|
+
const principalKind = sqlStringLiteral(options.principalKind);
|
|
7201
|
+
return [
|
|
7202
|
+
`tenant_id = ${tenant}`,
|
|
7203
|
+
`AND (`,
|
|
7204
|
+
` created_by = ${principalUrl$1}`,
|
|
7205
|
+
` OR url IN (`,
|
|
7206
|
+
` SELECT entity_url`,
|
|
7207
|
+
` FROM entity_effective_permissions`,
|
|
7208
|
+
` WHERE tenant_id = ${tenant}`,
|
|
7209
|
+
` AND permission IN ('read', 'manage')`,
|
|
7210
|
+
` AND (`,
|
|
7211
|
+
` (subject_kind = 'principal' AND subject_value = ${principalUrl$1})`,
|
|
7212
|
+
` OR (subject_kind = 'principal_kind' AND subject_value = ${principalKind})`,
|
|
7213
|
+
` )`,
|
|
7214
|
+
` )`,
|
|
7215
|
+
`)`
|
|
7216
|
+
].join(`\n`);
|
|
7217
|
+
}
|
|
7218
|
+
function buildReadableEntityUrlWhere(options) {
|
|
7219
|
+
const tenant = sqlStringLiteral(options.tenantId);
|
|
7220
|
+
if (options.permissionBypass) return `tenant_id = ${tenant}`;
|
|
7221
|
+
return [
|
|
7222
|
+
`tenant_id = ${tenant}`,
|
|
7223
|
+
`AND entity_url IN (`,
|
|
7224
|
+
` SELECT url`,
|
|
7225
|
+
` FROM entities`,
|
|
7226
|
+
` WHERE ${indentWhere(buildReadableEntitiesWhere(options), ` `).trimStart()}`,
|
|
7227
|
+
`)`
|
|
7228
|
+
].join(`\n`);
|
|
7229
|
+
}
|
|
7230
|
+
function buildCurrentPrincipalEntityEffectivePermissionsWhere(options) {
|
|
7231
|
+
const tenant = sqlStringLiteral(options.tenantId);
|
|
7232
|
+
if (options.permissionBypass) return `tenant_id = ${tenant}`;
|
|
7233
|
+
const principalUrl$1 = sqlStringLiteral(options.principalUrl);
|
|
7234
|
+
const principalKind = sqlStringLiteral(options.principalKind);
|
|
7235
|
+
return [
|
|
7236
|
+
`tenant_id = ${tenant}`,
|
|
7237
|
+
`AND (`,
|
|
7238
|
+
` (subject_kind = 'principal' AND subject_value = ${principalUrl$1})`,
|
|
7239
|
+
` OR (subject_kind = 'principal_kind' AND subject_value = ${principalKind})`,
|
|
7240
|
+
`)`,
|
|
7241
|
+
`AND entity_url IN (`,
|
|
7242
|
+
` SELECT url`,
|
|
7243
|
+
` FROM entities`,
|
|
7244
|
+
` WHERE ${buildReadableEntitiesWhere(options)}`,
|
|
7245
|
+
`)`
|
|
7246
|
+
].join(`\n`);
|
|
7247
|
+
}
|
|
7248
|
+
function buildSpawnableEntityTypesWhere(options) {
|
|
7249
|
+
const tenant = sqlStringLiteral(options.tenantId);
|
|
7250
|
+
if (options.permissionBypass) return `tenant_id = ${tenant}`;
|
|
7251
|
+
const principalUrl$1 = sqlStringLiteral(options.principalUrl);
|
|
7252
|
+
const principalKind = sqlStringLiteral(options.principalKind);
|
|
7253
|
+
return [
|
|
7254
|
+
`tenant_id = ${tenant}`,
|
|
7255
|
+
`AND name IN (`,
|
|
7256
|
+
` SELECT entity_type`,
|
|
7257
|
+
` FROM entity_type_permission_grants`,
|
|
7258
|
+
` WHERE tenant_id = ${tenant}`,
|
|
7259
|
+
` AND permission IN ('spawn', 'manage')`,
|
|
7260
|
+
` AND (`,
|
|
7261
|
+
` (subject_kind = 'principal' AND subject_value = ${principalUrl$1})`,
|
|
7262
|
+
` OR (subject_kind = 'principal_kind' AND subject_value = ${principalKind})`,
|
|
7263
|
+
` )`,
|
|
7264
|
+
`)`
|
|
7265
|
+
].join(`\n`);
|
|
7266
|
+
}
|
|
6700
7267
|
async function forwardFetchRequest(options) {
|
|
6701
7268
|
const routingAdapter = resolveDurableStreamsRoutingAdapter(options.durableStreamsRouting, options.durableStreamsUrl);
|
|
6702
7269
|
const routingInput = {
|
|
@@ -6731,13 +7298,170 @@ function decodeJsonObject(body) {
|
|
|
6731
7298
|
return null;
|
|
6732
7299
|
}
|
|
6733
7300
|
function applyTenantShapeWhere(target, tenantId, extraConditions = []) {
|
|
6734
|
-
|
|
7301
|
+
applyShapeWhere(target, [`tenant_id = ${sqlStringLiteral(tenantId)}`, ...extraConditions].join(` AND `));
|
|
7302
|
+
}
|
|
7303
|
+
function applyShapeWhere(target, enforcedWhere) {
|
|
6735
7304
|
const existingWhere = target.searchParams.get(`where`);
|
|
6736
|
-
target.searchParams.set(`where`, existingWhere ? `${
|
|
7305
|
+
target.searchParams.set(`where`, existingWhere ? `${enforcedWhere} AND (${existingWhere})` : enforcedWhere);
|
|
6737
7306
|
}
|
|
6738
7307
|
function sqlStringLiteral(value) {
|
|
6739
7308
|
return `'${value.replace(/'/g, `''`)}'`;
|
|
6740
7309
|
}
|
|
7310
|
+
function indentWhere(where, prefix) {
|
|
7311
|
+
return where.split(`\n`).map((line) => `${prefix}${line}`).join(`\n`);
|
|
7312
|
+
}
|
|
7313
|
+
|
|
7314
|
+
//#endregion
|
|
7315
|
+
//#region src/permissions.ts
|
|
7316
|
+
const authzDecisionCache = new WeakMap();
|
|
7317
|
+
function principalSubject(principal) {
|
|
7318
|
+
return {
|
|
7319
|
+
principalUrl: principal.url,
|
|
7320
|
+
principalKind: principal.kind
|
|
7321
|
+
};
|
|
7322
|
+
}
|
|
7323
|
+
function isPermissionBypassPrincipal(ctx) {
|
|
7324
|
+
return isBuiltInSystemPrincipalUrl(ctx.principal.url);
|
|
7325
|
+
}
|
|
7326
|
+
async function canAccessEntity(ctx, entity, permission, request) {
|
|
7327
|
+
if (isPermissionBypassPrincipal(ctx)) return true;
|
|
7328
|
+
await ctx.entityManager.registry.pruneExpiredPermissionGrants?.();
|
|
7329
|
+
const builtInAllowed = entity.created_by === ctx.principal.url || await ctx.entityManager.registry.hasEntityPermission(entity.url, permission, principalSubject(ctx.principal));
|
|
7330
|
+
return await applyAuthorizationHook(ctx, {
|
|
7331
|
+
verb: permission,
|
|
7332
|
+
resourceKey: `entity:${entity.url}`,
|
|
7333
|
+
resource: {
|
|
7334
|
+
kind: `entity`,
|
|
7335
|
+
entity
|
|
7336
|
+
},
|
|
7337
|
+
builtInAllowed,
|
|
7338
|
+
request
|
|
7339
|
+
});
|
|
7340
|
+
}
|
|
7341
|
+
async function canAccessEntityType(ctx, entityType, permission, request) {
|
|
7342
|
+
if (isPermissionBypassPrincipal(ctx)) return true;
|
|
7343
|
+
await ctx.entityManager.registry.pruneExpiredPermissionGrants?.();
|
|
7344
|
+
const builtInAllowed = await ctx.entityManager.registry.hasEntityTypePermission(entityType.name, permission, principalSubject(ctx.principal));
|
|
7345
|
+
return await applyAuthorizationHook(ctx, {
|
|
7346
|
+
verb: permission,
|
|
7347
|
+
resourceKey: `entity_type:${entityType.name}`,
|
|
7348
|
+
resource: {
|
|
7349
|
+
kind: `entity_type`,
|
|
7350
|
+
entityType
|
|
7351
|
+
},
|
|
7352
|
+
builtInAllowed,
|
|
7353
|
+
request
|
|
7354
|
+
});
|
|
7355
|
+
}
|
|
7356
|
+
async function canRegisterEntityType(ctx, input, request) {
|
|
7357
|
+
if (isPermissionBypassPrincipal(ctx)) return true;
|
|
7358
|
+
return await applyAuthorizationHook(ctx, {
|
|
7359
|
+
verb: `manage`,
|
|
7360
|
+
resourceKey: `entity_type_registration:${input.name}`,
|
|
7361
|
+
resource: {
|
|
7362
|
+
kind: `entity_type_registration`,
|
|
7363
|
+
entityTypeName: input.name
|
|
7364
|
+
},
|
|
7365
|
+
builtInAllowed: true,
|
|
7366
|
+
request
|
|
7367
|
+
});
|
|
7368
|
+
}
|
|
7369
|
+
async function canAccessSharedState(ctx, sharedStateId, permission, request, ownerEntityUrl) {
|
|
7370
|
+
if (isPermissionBypassPrincipal(ctx)) return true;
|
|
7371
|
+
await ctx.entityManager.registry.pruneExpiredPermissionGrants?.();
|
|
7372
|
+
const storedLinkedEntityUrls = await ctx.entityManager.registry.listSharedStateLinkedEntityUrls(sharedStateId);
|
|
7373
|
+
const bootstrapEntityUrls = storedLinkedEntityUrls.length === 0 && ownerEntityUrl ? [ownerEntityUrl] : [];
|
|
7374
|
+
const linkedEntityUrls = [...new Set([...storedLinkedEntityUrls, ...bootstrapEntityUrls])];
|
|
7375
|
+
for (const entityUrl of linkedEntityUrls) {
|
|
7376
|
+
const entity = await ctx.entityManager.registry.getEntity(entityUrl);
|
|
7377
|
+
if (!entity) continue;
|
|
7378
|
+
if (entity.created_by === ctx.principal.url || await ctx.entityManager.registry.hasEntityPermission(entity.url, permission, principalSubject(ctx.principal))) return await applyAuthorizationHook(ctx, {
|
|
7379
|
+
verb: permission,
|
|
7380
|
+
resourceKey: `shared_state:${sharedStateId}`,
|
|
7381
|
+
resource: {
|
|
7382
|
+
kind: `shared_state`,
|
|
7383
|
+
sharedStateId,
|
|
7384
|
+
linkedEntityUrls
|
|
7385
|
+
},
|
|
7386
|
+
builtInAllowed: true,
|
|
7387
|
+
request
|
|
7388
|
+
});
|
|
7389
|
+
}
|
|
7390
|
+
return await applyAuthorizationHook(ctx, {
|
|
7391
|
+
verb: permission,
|
|
7392
|
+
resourceKey: `shared_state:${sharedStateId}`,
|
|
7393
|
+
resource: {
|
|
7394
|
+
kind: `shared_state`,
|
|
7395
|
+
sharedStateId,
|
|
7396
|
+
linkedEntityUrls
|
|
7397
|
+
},
|
|
7398
|
+
builtInAllowed: false,
|
|
7399
|
+
request
|
|
7400
|
+
});
|
|
7401
|
+
}
|
|
7402
|
+
async function applyAuthorizationHook(ctx, input) {
|
|
7403
|
+
const hook = ctx.authorizeRequest;
|
|
7404
|
+
if (!hook) return input.builtInAllowed;
|
|
7405
|
+
const cacheKey = [
|
|
7406
|
+
ctx.service,
|
|
7407
|
+
ctx.principal.url,
|
|
7408
|
+
input.verb,
|
|
7409
|
+
input.resourceKey
|
|
7410
|
+
].join(`|`);
|
|
7411
|
+
const cached = getCachedDecision(hook, cacheKey);
|
|
7412
|
+
if (cached) return cached.decision === `allow`;
|
|
7413
|
+
let decision;
|
|
7414
|
+
try {
|
|
7415
|
+
decision = await hook({
|
|
7416
|
+
tenant: ctx.service,
|
|
7417
|
+
principal: ctx.principal,
|
|
7418
|
+
verb: input.verb,
|
|
7419
|
+
resource: input.resource,
|
|
7420
|
+
request: input.request ? requestMetadata(input.request) : void 0,
|
|
7421
|
+
builtInAllowed: input.builtInAllowed
|
|
7422
|
+
});
|
|
7423
|
+
} catch (error) {
|
|
7424
|
+
serverLog.warn(`[agent-server] authorization hook failed:`, error);
|
|
7425
|
+
return false;
|
|
7426
|
+
}
|
|
7427
|
+
cacheDecision(hook, cacheKey, decision);
|
|
7428
|
+
return decision.decision === `allow`;
|
|
7429
|
+
}
|
|
7430
|
+
function getCachedDecision(hook, cacheKey) {
|
|
7431
|
+
const cache = authzDecisionCache.get(hook);
|
|
7432
|
+
const entry = cache?.get(cacheKey);
|
|
7433
|
+
if (!entry) return null;
|
|
7434
|
+
if (entry.expiresAt <= Date.now()) {
|
|
7435
|
+
cache?.delete(cacheKey);
|
|
7436
|
+
return null;
|
|
7437
|
+
}
|
|
7438
|
+
return { decision: entry.decision };
|
|
7439
|
+
}
|
|
7440
|
+
function cacheDecision(hook, cacheKey, decision) {
|
|
7441
|
+
if (!decision.expires_at) return;
|
|
7442
|
+
const expiresAt = Date.parse(decision.expires_at);
|
|
7443
|
+
if (!Number.isFinite(expiresAt) || expiresAt <= Date.now()) return;
|
|
7444
|
+
let cache = authzDecisionCache.get(hook);
|
|
7445
|
+
if (!cache) {
|
|
7446
|
+
cache = new Map();
|
|
7447
|
+
authzDecisionCache.set(hook, cache);
|
|
7448
|
+
}
|
|
7449
|
+
cache.set(cacheKey, {
|
|
7450
|
+
decision: decision.decision,
|
|
7451
|
+
expiresAt
|
|
7452
|
+
});
|
|
7453
|
+
}
|
|
7454
|
+
function requestMetadata(request) {
|
|
7455
|
+
const headers = {};
|
|
7456
|
+
request.headers.forEach((value, key) => {
|
|
7457
|
+
headers[key] = value;
|
|
7458
|
+
});
|
|
7459
|
+
return {
|
|
7460
|
+
method: request.method,
|
|
7461
|
+
url: request.url,
|
|
7462
|
+
headers
|
|
7463
|
+
};
|
|
7464
|
+
}
|
|
6741
7465
|
|
|
6742
7466
|
//#endregion
|
|
6743
7467
|
//#region src/webhook-signing.ts
|
|
@@ -6829,6 +7553,7 @@ const subscriptionControlActions = [
|
|
|
6829
7553
|
`ack`,
|
|
6830
7554
|
`release`
|
|
6831
7555
|
];
|
|
7556
|
+
const SHARED_STATE_OWNER_ENTITY_HEADER = `electric-owner-entity`;
|
|
6832
7557
|
const durableStreamsRouter = Router();
|
|
6833
7558
|
durableStreamsRouter.put(`/__ds/subscriptions/:subscriptionId`, putSubscriptionBase);
|
|
6834
7559
|
durableStreamsRouter.get(`/__ds/subscriptions/:subscriptionId`, getSubscriptionBase);
|
|
@@ -7046,6 +7771,8 @@ async function webhookJwks(_request, ctx) {
|
|
|
7046
7771
|
});
|
|
7047
7772
|
}
|
|
7048
7773
|
async function streamAppend(request, ctx) {
|
|
7774
|
+
const auth = await authorizeDurableStreamAccess(request, ctx);
|
|
7775
|
+
if (auth) return auth;
|
|
7049
7776
|
return await electricAgentsStreamAppendRouter.fetch(createStreamAppendRouteRequest(request), ctx.runtime, (req, body) => forwardFetchRequest({
|
|
7050
7777
|
request: {
|
|
7051
7778
|
method: req.method,
|
|
@@ -7062,8 +7789,9 @@ async function streamAppend(request, ctx) {
|
|
|
7062
7789
|
}));
|
|
7063
7790
|
}
|
|
7064
7791
|
async function proxyPassThrough(request, ctx) {
|
|
7792
|
+
const auth = await authorizeDurableStreamAccess(request, ctx);
|
|
7793
|
+
if (auth) return auth;
|
|
7065
7794
|
const streamPath = new URL(request.url).pathname;
|
|
7066
|
-
if (ctx.entityManager?.isAttachmentStreamPath(streamPath)) return new Response(null, { status: 404 });
|
|
7067
7795
|
const upstream = await forwardToDurableStreams(ctx, request);
|
|
7068
7796
|
const method = request.method.toUpperCase();
|
|
7069
7797
|
const endTrackedRead = method === `GET` ? await ctx.entityBridgeManager.beginClientRead(streamPath) : null;
|
|
@@ -7074,6 +7802,51 @@ async function proxyPassThrough(request, ctx) {
|
|
|
7074
7802
|
await endTrackedRead?.();
|
|
7075
7803
|
}
|
|
7076
7804
|
}
|
|
7805
|
+
async function authorizeDurableStreamAccess(request, ctx) {
|
|
7806
|
+
const method = request.method.toUpperCase();
|
|
7807
|
+
const streamPath = new URL(request.url).pathname;
|
|
7808
|
+
if (method === `GET` || method === `HEAD`) {
|
|
7809
|
+
const registry = ctx.entityManager?.registry;
|
|
7810
|
+
const entity = registry?.getEntityByStream ? await registry.getEntityByStream(streamPath) : null;
|
|
7811
|
+
if (entity) {
|
|
7812
|
+
if (await canAccessEntity(ctx, entity, `read`, request)) return void 0;
|
|
7813
|
+
return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to read ${entity.url}`);
|
|
7814
|
+
}
|
|
7815
|
+
const attachmentEntityUrl = entityUrlFromAttachmentStreamPath(streamPath);
|
|
7816
|
+
if (attachmentEntityUrl) {
|
|
7817
|
+
const attachmentEntity = registry?.getEntity ? await registry.getEntity(attachmentEntityUrl) : null;
|
|
7818
|
+
if (!attachmentEntity) return apiError(404, ErrCodeNotFound, `Entity not found`);
|
|
7819
|
+
if (await canAccessEntity(ctx, attachmentEntity, `read`, request)) return void 0;
|
|
7820
|
+
return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to read ${attachmentEntity.url}`);
|
|
7821
|
+
}
|
|
7822
|
+
}
|
|
7823
|
+
const sharedStateId = sharedStateIdFromPath(streamPath);
|
|
7824
|
+
if (!sharedStateId) return void 0;
|
|
7825
|
+
if (method === `GET` || method === `HEAD`) {
|
|
7826
|
+
if (await canAccessSharedState(ctx, sharedStateId, `read`, request)) return void 0;
|
|
7827
|
+
return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to read shared state`);
|
|
7828
|
+
}
|
|
7829
|
+
if (method === `PUT` || method === `POST`) {
|
|
7830
|
+
const ownerEntityUrl = request.headers.get(SHARED_STATE_OWNER_ENTITY_HEADER)?.trim() || void 0;
|
|
7831
|
+
if (await canAccessSharedState(ctx, sharedStateId, `write`, request, ownerEntityUrl)) return void 0;
|
|
7832
|
+
return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to write shared state`);
|
|
7833
|
+
}
|
|
7834
|
+
return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to access shared state`);
|
|
7835
|
+
}
|
|
7836
|
+
function entityUrlFromAttachmentStreamPath(path$1) {
|
|
7837
|
+
const match = path$1.match(/^\/([^/]+)\/([^/]+)\/attachments\/[^/]+$/);
|
|
7838
|
+
if (!match) return null;
|
|
7839
|
+
return `/${match[1]}/${match[2]}`;
|
|
7840
|
+
}
|
|
7841
|
+
function sharedStateIdFromPath(path$1) {
|
|
7842
|
+
const match = path$1.match(/^\/_electric\/shared-state\/([^/]+)$/);
|
|
7843
|
+
if (!match) return null;
|
|
7844
|
+
try {
|
|
7845
|
+
return decodeURIComponent(match[1]);
|
|
7846
|
+
} catch {
|
|
7847
|
+
return match[1];
|
|
7848
|
+
}
|
|
7849
|
+
}
|
|
7077
7850
|
|
|
7078
7851
|
//#endregion
|
|
7079
7852
|
//#region src/routing/electric-proxy-router.ts
|
|
@@ -7081,12 +7854,15 @@ const electricProxyRouter = Router({ base: `/_electric/electric` });
|
|
|
7081
7854
|
electricProxyRouter.get(`/*`, proxyElectric);
|
|
7082
7855
|
async function proxyElectric(request, ctx) {
|
|
7083
7856
|
if (!ctx.electricUrl) return apiError(500, `ELECTRIC_PROXY_FAILED`, `Electric URL not configured`);
|
|
7857
|
+
await ctx.entityManager.registry.pruneExpiredPermissionGrants?.();
|
|
7084
7858
|
const target = buildElectricProxyTarget({
|
|
7085
7859
|
incomingUrl: new URL(request.url),
|
|
7086
7860
|
electricUrl: ctx.electricUrl,
|
|
7087
7861
|
electricSecret: ctx.electricSecret,
|
|
7088
7862
|
tenantId: ctx.service,
|
|
7089
|
-
principalUrl: ctx.principal.url
|
|
7863
|
+
principalUrl: ctx.principal.url,
|
|
7864
|
+
principalKind: ctx.principal.kind,
|
|
7865
|
+
permissionBypass: isPermissionBypassPrincipal(ctx)
|
|
7090
7866
|
});
|
|
7091
7867
|
const headers = new Headers(request.headers);
|
|
7092
7868
|
headers.delete(`host`);
|
|
@@ -7145,6 +7921,27 @@ const wakeConditionSchema = Type.Union([Type.Literal(`runFinished`), Type.Object
|
|
|
7145
7921
|
Type.Literal(`delete`)
|
|
7146
7922
|
])))
|
|
7147
7923
|
})]);
|
|
7924
|
+
const permissionSubjectSchema = Type.Object({
|
|
7925
|
+
subject_kind: Type.Union([Type.Literal(`principal`), Type.Literal(`principal_kind`)]),
|
|
7926
|
+
subject_value: Type.String()
|
|
7927
|
+
}, { additionalProperties: false });
|
|
7928
|
+
const entityPermissionSchema = Type.Union([
|
|
7929
|
+
Type.Literal(`read`),
|
|
7930
|
+
Type.Literal(`write`),
|
|
7931
|
+
Type.Literal(`delete`),
|
|
7932
|
+
Type.Literal(`signal`),
|
|
7933
|
+
Type.Literal(`fork`),
|
|
7934
|
+
Type.Literal(`schedule`),
|
|
7935
|
+
Type.Literal(`spawn`),
|
|
7936
|
+
Type.Literal(`manage`)
|
|
7937
|
+
]);
|
|
7938
|
+
const entityPermissionGrantInputSchema = Type.Object({
|
|
7939
|
+
...permissionSubjectSchema.properties,
|
|
7940
|
+
permission: entityPermissionSchema,
|
|
7941
|
+
propagation: Type.Optional(Type.Union([Type.Literal(`self`), Type.Literal(`descendants`)])),
|
|
7942
|
+
copy_to_children: Type.Optional(Type.Boolean()),
|
|
7943
|
+
expires_at: Type.Optional(Type.String())
|
|
7944
|
+
}, { additionalProperties: false });
|
|
7148
7945
|
const spawnBodySchema = Type.Object({
|
|
7149
7946
|
args: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
|
|
7150
7947
|
tags: Type.Optional(stringRecordSchema$1),
|
|
@@ -7152,6 +7949,8 @@ const spawnBodySchema = Type.Object({
|
|
|
7152
7949
|
dispatch_policy: Type.Optional(dispatchPolicySchema),
|
|
7153
7950
|
sandbox: Type.Optional(sandboxChoiceSchema),
|
|
7154
7951
|
initialMessage: Type.Optional(Type.Unknown()),
|
|
7952
|
+
grants: Type.Optional(Type.Array(entityPermissionGrantInputSchema)),
|
|
7953
|
+
initialMessageType: Type.Optional(Type.String()),
|
|
7155
7954
|
wake: Type.Optional(Type.Object({
|
|
7156
7955
|
subscriberUrl: Type.String(),
|
|
7157
7956
|
condition: wakeConditionSchema,
|
|
@@ -7173,8 +7972,22 @@ const sendBodySchema = Type.Object({
|
|
|
7173
7972
|
])),
|
|
7174
7973
|
position: Type.Optional(Type.String()),
|
|
7175
7974
|
afterMs: Type.Optional(Type.Number()),
|
|
7176
|
-
from: Type.Optional(Type.String())
|
|
7975
|
+
from: Type.Optional(Type.String()),
|
|
7976
|
+
from_principal: Type.Optional(Type.String()),
|
|
7977
|
+
from_agent: Type.Optional(Type.String())
|
|
7177
7978
|
});
|
|
7979
|
+
function agentUrlForPrincipal(principal) {
|
|
7980
|
+
if (principal.kind === `agent`) return `/${principal.id}`;
|
|
7981
|
+
if (principal.key.startsWith(`entity:`)) return `/${principal.key.slice(`entity:`.length)}`;
|
|
7982
|
+
return null;
|
|
7983
|
+
}
|
|
7984
|
+
function agentUrlPath(value) {
|
|
7985
|
+
try {
|
|
7986
|
+
return new URL(value).pathname;
|
|
7987
|
+
} catch {
|
|
7988
|
+
return value;
|
|
7989
|
+
}
|
|
7990
|
+
}
|
|
7178
7991
|
const inboxMessageBodySchema = Type.Object({
|
|
7179
7992
|
payload: Type.Optional(Type.Unknown()),
|
|
7180
7993
|
position: Type.Optional(Type.String()),
|
|
@@ -7253,24 +8066,27 @@ const attachmentSubjectTypes = new Set([
|
|
|
7253
8066
|
]);
|
|
7254
8067
|
const entitiesRouter = Router({ base: `/_electric/entities` });
|
|
7255
8068
|
entitiesRouter.get(`/`, listEntities);
|
|
7256
|
-
entitiesRouter.put(`/:type/:instanceId`, withSpawnableEntityType, withSchema(spawnBodySchema), spawnEntity);
|
|
7257
|
-
entitiesRouter.get(`/:type/:instanceId`, withExistingEntity, getEntity);
|
|
7258
|
-
entitiesRouter.head(`/:type/:instanceId`, withExistingEntity, headEntity);
|
|
7259
|
-
entitiesRouter.delete(`/:type/:instanceId`, withExistingEntity, killEntity);
|
|
7260
|
-
entitiesRouter.post(`/:type/:instanceId/signal`, withExistingEntity, withSchema(signalBodySchema), signalEntity);
|
|
7261
|
-
entitiesRouter.post(`/:type/:instanceId/send`, withExistingEntity, withSchema(sendBodySchema), sendEntity);
|
|
7262
|
-
entitiesRouter.post(`/:type/:instanceId/attachments`, withExistingEntity, createAttachment);
|
|
7263
|
-
entitiesRouter.get(`/:type/:instanceId/attachments/:attachmentId`, withExistingEntity, readAttachment);
|
|
7264
|
-
entitiesRouter.delete(`/:type/:instanceId/attachments/:attachmentId`, withExistingEntity, deleteAttachment);
|
|
7265
|
-
entitiesRouter.patch(`/:type/:instanceId/inbox/:messageKey`, withExistingEntity, withSchema(inboxMessageBodySchema), updateInboxMessage);
|
|
7266
|
-
entitiesRouter.delete(`/:type/:instanceId/inbox/:messageKey`, withExistingEntity, deleteInboxMessage);
|
|
7267
|
-
entitiesRouter.post(`/:type/:instanceId/fork`, withExistingEntity, withSchema(forkBodySchema), forkEntity);
|
|
7268
|
-
entitiesRouter.post(`/:type/:instanceId/tags/:tagKey`, withExistingEntity, withSchema(setTagBodySchema), setTag);
|
|
7269
|
-
entitiesRouter.delete(`/:type/:instanceId/tags/:tagKey`, withExistingEntity, deleteTag);
|
|
7270
|
-
entitiesRouter.put(`/:type/:instanceId/schedules/:scheduleId`, withExistingEntity, withSchema(scheduleBodySchema), upsertSchedule);
|
|
7271
|
-
entitiesRouter.delete(`/:type/:instanceId/schedules/:scheduleId`, withExistingEntity, deleteSchedule);
|
|
7272
|
-
entitiesRouter.put(`/:type/:instanceId/event-source-subscriptions/:subscriptionId`, withExistingEntity, withSchema(eventSourceSubscriptionBodySchema), upsertEventSourceSubscription);
|
|
7273
|
-
entitiesRouter.delete(`/:type/:instanceId/event-source-subscriptions/:subscriptionId`, withExistingEntity, deleteEventSourceSubscription);
|
|
8069
|
+
entitiesRouter.put(`/:type/:instanceId`, withSpawnableEntityType, withSchema(spawnBodySchema), withSpawnPermission, spawnEntity);
|
|
8070
|
+
entitiesRouter.get(`/:type/:instanceId`, withExistingEntity, withEntityPermission(`read`), getEntity);
|
|
8071
|
+
entitiesRouter.head(`/:type/:instanceId`, withExistingEntity, withEntityPermission(`read`), headEntity);
|
|
8072
|
+
entitiesRouter.delete(`/:type/:instanceId`, withExistingEntity, withEntityPermission(`delete`), killEntity);
|
|
8073
|
+
entitiesRouter.post(`/:type/:instanceId/signal`, withExistingEntity, withSchema(signalBodySchema), withEntityPermission(`signal`), signalEntity);
|
|
8074
|
+
entitiesRouter.post(`/:type/:instanceId/send`, withExistingEntity, withSchema(sendBodySchema), withEntityPermission(`write`), sendEntity);
|
|
8075
|
+
entitiesRouter.post(`/:type/:instanceId/attachments`, withExistingEntity, withEntityPermission(`write`), createAttachment);
|
|
8076
|
+
entitiesRouter.get(`/:type/:instanceId/attachments/:attachmentId`, withExistingEntity, withEntityPermission(`read`), readAttachment);
|
|
8077
|
+
entitiesRouter.delete(`/:type/:instanceId/attachments/:attachmentId`, withExistingEntity, withEntityPermission(`write`), deleteAttachment);
|
|
8078
|
+
entitiesRouter.patch(`/:type/:instanceId/inbox/:messageKey`, withExistingEntity, withSchema(inboxMessageBodySchema), withEntityPermission(`write`), updateInboxMessage);
|
|
8079
|
+
entitiesRouter.delete(`/:type/:instanceId/inbox/:messageKey`, withExistingEntity, withEntityPermission(`write`), deleteInboxMessage);
|
|
8080
|
+
entitiesRouter.post(`/:type/:instanceId/fork`, withExistingEntity, withSchema(forkBodySchema), withEntityPermission(`fork`), forkEntity);
|
|
8081
|
+
entitiesRouter.post(`/:type/:instanceId/tags/:tagKey`, withExistingEntity, withSchema(setTagBodySchema), withEntityPermission(`write`), setTag);
|
|
8082
|
+
entitiesRouter.delete(`/:type/:instanceId/tags/:tagKey`, withExistingEntity, withEntityPermission(`write`), deleteTag);
|
|
8083
|
+
entitiesRouter.put(`/:type/:instanceId/schedules/:scheduleId`, withExistingEntity, withSchema(scheduleBodySchema), withEntityPermission(`schedule`), upsertSchedule);
|
|
8084
|
+
entitiesRouter.delete(`/:type/:instanceId/schedules/:scheduleId`, withExistingEntity, withEntityPermission(`schedule`), deleteSchedule);
|
|
8085
|
+
entitiesRouter.put(`/:type/:instanceId/event-source-subscriptions/:subscriptionId`, withExistingEntity, withSchema(eventSourceSubscriptionBodySchema), withEntityPermission(`write`), upsertEventSourceSubscription);
|
|
8086
|
+
entitiesRouter.delete(`/:type/:instanceId/event-source-subscriptions/:subscriptionId`, withExistingEntity, withEntityPermission(`write`), deleteEventSourceSubscription);
|
|
8087
|
+
entitiesRouter.get(`/:type/:instanceId/grants`, withExistingEntity, withEntityPermission(`manage`), listEntityPermissionGrants);
|
|
8088
|
+
entitiesRouter.post(`/:type/:instanceId/grants`, withExistingEntity, withSchema(entityPermissionGrantInputSchema), withEntityPermission(`manage`), createEntityPermissionGrant);
|
|
8089
|
+
entitiesRouter.delete(`/:type/:instanceId/grants/:grantId`, withExistingEntity, withEntityPermission(`manage`), deleteEntityPermissionGrant);
|
|
7274
8090
|
function entityUrlFromSegments(type, instanceId) {
|
|
7275
8091
|
if (!type || !instanceId) return null;
|
|
7276
8092
|
if (type.startsWith(`_`) || type.includes(`*`) || instanceId.includes(`*`)) return null;
|
|
@@ -7369,6 +8185,17 @@ function rejectPrincipalEntityMutation(request, action) {
|
|
|
7369
8185
|
if (entity.type !== `principal`) return void 0;
|
|
7370
8186
|
return apiError(400, ErrCodeInvalidRequest, `Principal entities are built in and cannot be ${action}`);
|
|
7371
8187
|
}
|
|
8188
|
+
function parseExpiresAt$1(value) {
|
|
8189
|
+
if (value === void 0) return void 0;
|
|
8190
|
+
const expiresAt = new Date(value);
|
|
8191
|
+
if (Number.isNaN(expiresAt.getTime())) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Invalid expires_at timestamp`, 400);
|
|
8192
|
+
return expiresAt;
|
|
8193
|
+
}
|
|
8194
|
+
function parseGrantId$1(request) {
|
|
8195
|
+
const grantId = Number.parseInt(String(request.params.grantId), 10);
|
|
8196
|
+
if (!Number.isSafeInteger(grantId) || grantId <= 0) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Invalid grant id`, 400);
|
|
8197
|
+
return grantId;
|
|
8198
|
+
}
|
|
7372
8199
|
async function withExistingEntity(request, ctx) {
|
|
7373
8200
|
const entityUrl = entityUrlFromSegments(request.params.type, request.params.instanceId);
|
|
7374
8201
|
if (!entityUrl) return void 0;
|
|
@@ -7399,17 +8226,76 @@ async function withSpawnableEntityType(request, ctx) {
|
|
|
7399
8226
|
if (request.params.type === `principal`) return apiError(400, ErrCodeInvalidRequest, `Principal entities are built in and cannot be spawned directly`);
|
|
7400
8227
|
const entityType = await ctx.entityManager.registry.getEntityType(request.params.type);
|
|
7401
8228
|
if (!entityType) return apiError(404, ErrCodeUnknownEntityType, `Entity type "${request.params.type}" not found`);
|
|
8229
|
+
request.spawnRoute = { entityType };
|
|
7402
8230
|
return void 0;
|
|
7403
8231
|
}
|
|
8232
|
+
function withEntityPermission(permission) {
|
|
8233
|
+
return async (request, ctx) => {
|
|
8234
|
+
const { entity } = requireExistingEntityRoute(request);
|
|
8235
|
+
if (await canAccessEntity(ctx, entity, permission, request)) return void 0;
|
|
8236
|
+
return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to ${permission} ${entity.url}`);
|
|
8237
|
+
};
|
|
8238
|
+
}
|
|
8239
|
+
async function withSpawnPermission(request, ctx) {
|
|
8240
|
+
const parsed = routeBody(request);
|
|
8241
|
+
const entityType = request.spawnRoute?.entityType;
|
|
8242
|
+
if (!entityType) throw new Error(`spawnable entity type middleware did not run`);
|
|
8243
|
+
if (!await canAccessEntityType(ctx, entityType, `spawn`, request)) return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to spawn ${entityType.name}`);
|
|
8244
|
+
if (!parsed.parent) return void 0;
|
|
8245
|
+
const parent = await ctx.entityManager.registry.getEntity(parsed.parent);
|
|
8246
|
+
if (!parent) return apiError(404, ErrCodeNotFound, `Parent entity not found`);
|
|
8247
|
+
if (await canAccessEntity(ctx, parent, `spawn`, request)) return await validateParentedSpawnGrants(request, ctx, parent, parsed);
|
|
8248
|
+
return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to spawn children from ${parent.url}`);
|
|
8249
|
+
}
|
|
8250
|
+
async function validateParentedSpawnGrants(request, ctx, parent, parsed) {
|
|
8251
|
+
const needsParentManage = (parsed.grants ?? []).some(requiresParentManageForInitialGrant);
|
|
8252
|
+
if (!needsParentManage) return void 0;
|
|
8253
|
+
if (await canAccessEntity(ctx, parent, `manage`, request)) return void 0;
|
|
8254
|
+
return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to delegate broad grants from ${parent.url}`);
|
|
8255
|
+
}
|
|
8256
|
+
function requiresParentManageForInitialGrant(grant) {
|
|
8257
|
+
return grant.permission === `manage` || grant.subject_kind === `principal_kind` || grant.propagation === `descendants` || grant.copy_to_children === true;
|
|
8258
|
+
}
|
|
7404
8259
|
async function listEntities({ query }, ctx) {
|
|
7405
8260
|
const { entities: entities$1 } = await ctx.entityManager.registry.listEntities({
|
|
7406
8261
|
type: firstQueryValue$1(query.type),
|
|
7407
8262
|
status: firstQueryValue$1(query.status),
|
|
7408
8263
|
parent: firstQueryValue$1(query.parent),
|
|
7409
|
-
created_by: firstQueryValue$1(query.created_by)
|
|
8264
|
+
created_by: firstQueryValue$1(query.created_by),
|
|
8265
|
+
readableBy: {
|
|
8266
|
+
...principalSubject(ctx.principal),
|
|
8267
|
+
bypass: isPermissionBypassPrincipal(ctx)
|
|
8268
|
+
}
|
|
7410
8269
|
});
|
|
7411
8270
|
return json(entities$1.map((entity) => toPublicEntity(entity)));
|
|
7412
8271
|
}
|
|
8272
|
+
async function listEntityPermissionGrants(request, ctx) {
|
|
8273
|
+
const { entityUrl } = requireExistingEntityRoute(request);
|
|
8274
|
+
const grants = await ctx.entityManager.registry.listEntityPermissionGrants(entityUrl);
|
|
8275
|
+
return json({ grants });
|
|
8276
|
+
}
|
|
8277
|
+
async function createEntityPermissionGrant(request, ctx) {
|
|
8278
|
+
const { entityUrl } = requireExistingEntityRoute(request);
|
|
8279
|
+
const parsed = routeBody(request);
|
|
8280
|
+
const grant = await ctx.entityManager.registry.createEntityPermissionGrant({
|
|
8281
|
+
entityUrl,
|
|
8282
|
+
permission: parsed.permission,
|
|
8283
|
+
subjectKind: parsed.subject_kind,
|
|
8284
|
+
subjectValue: parsed.subject_value,
|
|
8285
|
+
propagation: parsed.propagation,
|
|
8286
|
+
copyToChildren: parsed.copy_to_children,
|
|
8287
|
+
expiresAt: parseExpiresAt$1(parsed.expires_at),
|
|
8288
|
+
createdBy: ctx.principal.url
|
|
8289
|
+
});
|
|
8290
|
+
await ctx.entityBridgeManager.onEntityChanged(entityUrl);
|
|
8291
|
+
return json(grant, { status: 201 });
|
|
8292
|
+
}
|
|
8293
|
+
async function deleteEntityPermissionGrant(request, ctx) {
|
|
8294
|
+
const { entityUrl } = requireExistingEntityRoute(request);
|
|
8295
|
+
const deleted = await ctx.entityManager.registry.deleteEntityPermissionGrant(entityUrl, parseGrantId$1(request));
|
|
8296
|
+
if (deleted) await ctx.entityBridgeManager.onEntityChanged(entityUrl);
|
|
8297
|
+
return deleted ? status(204) : apiError(404, ErrCodeNotFound, `Grant not found`);
|
|
8298
|
+
}
|
|
7413
8299
|
async function upsertSchedule(request, ctx) {
|
|
7414
8300
|
const principalMutationError = rejectPrincipalEntityMutation(request, `scheduled`);
|
|
7415
8301
|
if (principalMutationError) return principalMutationError;
|
|
@@ -7515,6 +8401,7 @@ async function forkEntity(request, ctx) {
|
|
|
7515
8401
|
const result = await ctx.entityManager.forkSubtree(entityUrl, {
|
|
7516
8402
|
rootInstanceId: parsed.instance_id,
|
|
7517
8403
|
waitTimeoutMs: parsed.waitTimeoutMs,
|
|
8404
|
+
createdBy: ctx.principal.url,
|
|
7518
8405
|
...parsed.fork_pointer && { forkPointer: {
|
|
7519
8406
|
offset: parsed.fork_pointer.offset,
|
|
7520
8407
|
subOffset: parsed.fork_pointer.sub_offset
|
|
@@ -7530,26 +8417,27 @@ async function sendEntity(request, ctx) {
|
|
|
7530
8417
|
const parsed = routeBody(request);
|
|
7531
8418
|
const principal = ctx.principal;
|
|
7532
8419
|
if (parsed.from !== void 0 && parsed.from !== principal.url) return apiError(400, ErrCodeInvalidRequest, `Request from must match Electric-Principal`);
|
|
8420
|
+
if (parsed.from_principal !== void 0 && parsed.from_principal !== principal.url) return apiError(400, ErrCodeInvalidRequest, `Request from_principal must match Electric-Principal`);
|
|
8421
|
+
if (parsed.from_agent !== void 0) {
|
|
8422
|
+
const principalAgentUrl = agentUrlForPrincipal(principal);
|
|
8423
|
+
if (agentUrlPath(parsed.from_agent) !== principalAgentUrl) return apiError(400, ErrCodeInvalidRequest, `Request from_agent must match authenticated agent principal`);
|
|
8424
|
+
}
|
|
7533
8425
|
await ctx.entityManager.ensurePrincipal(principal);
|
|
7534
8426
|
const { entityUrl, entity } = requireExistingEntityRoute(request);
|
|
7535
8427
|
const dispatchEntity = entity.dispatch_policy ? entity : await backfillEntityDispatchPolicy(ctx, entity);
|
|
7536
8428
|
await linkEntityDispatchSubscription(ctx, dispatchEntity);
|
|
7537
|
-
|
|
7538
|
-
from: principal.url,
|
|
7539
|
-
payload: parsed.payload,
|
|
7540
|
-
key: parsed.key,
|
|
7541
|
-
type: parsed.type,
|
|
7542
|
-
mode: parsed.mode,
|
|
7543
|
-
position: parsed.position
|
|
7544
|
-
}, new Date(Date.now() + parsed.afterMs));
|
|
7545
|
-
else await ctx.entityManager.send(entityUrl, {
|
|
8429
|
+
const sendReq = {
|
|
7546
8430
|
from: principal.url,
|
|
8431
|
+
from_principal: principal.url,
|
|
8432
|
+
from_agent: parsed.from_agent,
|
|
7547
8433
|
payload: parsed.payload,
|
|
7548
8434
|
key: parsed.key,
|
|
7549
8435
|
type: parsed.type,
|
|
7550
8436
|
mode: parsed.mode,
|
|
7551
8437
|
position: parsed.position
|
|
7552
|
-
}
|
|
8438
|
+
};
|
|
8439
|
+
if (parsed.afterMs && parsed.afterMs > 0) await ctx.entityManager.enqueueDelayedSend(entityUrl, sendReq, new Date(Date.now() + parsed.afterMs));
|
|
8440
|
+
else await ctx.entityManager.send(entityUrl, sendReq);
|
|
7553
8441
|
return status(204);
|
|
7554
8442
|
}
|
|
7555
8443
|
async function createAttachment(request, ctx) {
|
|
@@ -7618,14 +8506,27 @@ async function spawnEntity(request, ctx) {
|
|
|
7618
8506
|
dispatch_policy: dispatchPolicy,
|
|
7619
8507
|
sandbox: parsed.sandbox,
|
|
7620
8508
|
initialMessage: void 0,
|
|
8509
|
+
initialMessageType: void 0,
|
|
7621
8510
|
wake: parsed.wake,
|
|
7622
8511
|
created_by: principal.url
|
|
7623
8512
|
});
|
|
8513
|
+
if (parsed.parent) await ctx.entityManager.registry.copyEntityPermissionGrantsForSpawn(parsed.parent, entity.url, principal.url);
|
|
8514
|
+
for (const grant of parsed.grants ?? []) await ctx.entityManager.registry.createEntityPermissionGrant({
|
|
8515
|
+
entityUrl: entity.url,
|
|
8516
|
+
permission: grant.permission,
|
|
8517
|
+
subjectKind: grant.subject_kind,
|
|
8518
|
+
subjectValue: grant.subject_value,
|
|
8519
|
+
propagation: grant.propagation,
|
|
8520
|
+
copyToChildren: grant.copy_to_children,
|
|
8521
|
+
expiresAt: parseExpiresAt$1(grant.expires_at),
|
|
8522
|
+
createdBy: principal.url
|
|
8523
|
+
});
|
|
7624
8524
|
const linkBeforeInitialMessage = parsed.initialMessage !== void 0 && shouldLinkDispatchBeforeInitialMessage(dispatchPolicy);
|
|
7625
8525
|
if (linkBeforeInitialMessage) await linkEntityDispatchSubscription(ctx, entity);
|
|
7626
8526
|
if (parsed.initialMessage !== void 0) await ctx.entityManager.send(entity.url, {
|
|
7627
8527
|
from: principal.url,
|
|
7628
|
-
payload: parsed.initialMessage
|
|
8528
|
+
payload: parsed.initialMessage,
|
|
8529
|
+
type: parsed.initialMessageType
|
|
7629
8530
|
});
|
|
7630
8531
|
if (!linkBeforeInitialMessage) await linkEntityDispatchSubscription(ctx, entity);
|
|
7631
8532
|
return json({
|
|
@@ -7672,14 +8573,37 @@ async function signalEntity(request, ctx) {
|
|
|
7672
8573
|
//#region src/routing/entity-types-router.ts
|
|
7673
8574
|
const jsonObjectSchema = Type.Record(Type.String(), Type.Unknown());
|
|
7674
8575
|
const schemaMapSchema = Type.Record(Type.String(), jsonObjectSchema);
|
|
8576
|
+
const slashCommandArgumentSchema = Type.Object({
|
|
8577
|
+
name: Type.String(),
|
|
8578
|
+
type: Type.Union([
|
|
8579
|
+
Type.Literal(`string`),
|
|
8580
|
+
Type.Literal(`number`),
|
|
8581
|
+
Type.Literal(`boolean`)
|
|
8582
|
+
]),
|
|
8583
|
+
required: Type.Optional(Type.Boolean()),
|
|
8584
|
+
description: Type.Optional(Type.String())
|
|
8585
|
+
}, { additionalProperties: false });
|
|
8586
|
+
const slashCommandSchema = Type.Object({
|
|
8587
|
+
name: Type.String(),
|
|
8588
|
+
description: Type.Optional(Type.String()),
|
|
8589
|
+
arguments: Type.Optional(Type.Array(slashCommandArgumentSchema))
|
|
8590
|
+
}, { additionalProperties: false });
|
|
8591
|
+
const typePermissionGrantInputSchema = Type.Object({
|
|
8592
|
+
subject_kind: Type.Union([Type.Literal(`principal`), Type.Literal(`principal_kind`)]),
|
|
8593
|
+
subject_value: Type.String(),
|
|
8594
|
+
permission: Type.Union([Type.Literal(`spawn`), Type.Literal(`manage`)]),
|
|
8595
|
+
expires_at: Type.Optional(Type.String())
|
|
8596
|
+
}, { additionalProperties: false });
|
|
7675
8597
|
const registerEntityTypeBodySchema = Type.Object({
|
|
7676
8598
|
name: Type.Optional(Type.String()),
|
|
7677
8599
|
description: Type.Optional(Type.String()),
|
|
7678
8600
|
creation_schema: Type.Optional(jsonObjectSchema),
|
|
7679
8601
|
inbox_schemas: Type.Optional(schemaMapSchema),
|
|
7680
8602
|
state_schemas: Type.Optional(schemaMapSchema),
|
|
8603
|
+
slash_commands: Type.Optional(Type.Array(slashCommandSchema)),
|
|
7681
8604
|
serve_endpoint: Type.Optional(Type.String()),
|
|
7682
|
-
default_dispatch_policy: Type.Optional(dispatchPolicySchema)
|
|
8605
|
+
default_dispatch_policy: Type.Optional(dispatchPolicySchema),
|
|
8606
|
+
permission_grants: Type.Optional(Type.Array(typePermissionGrantInputSchema))
|
|
7683
8607
|
}, { additionalProperties: false });
|
|
7684
8608
|
const amendEntityTypeSchemasBodySchema = Type.Object({
|
|
7685
8609
|
inbox_schemas: Type.Optional(schemaMapSchema),
|
|
@@ -7687,20 +8611,56 @@ const amendEntityTypeSchemasBodySchema = Type.Object({
|
|
|
7687
8611
|
}, { additionalProperties: false });
|
|
7688
8612
|
const entityTypesRouter = Router({ base: `/_electric/entity-types` });
|
|
7689
8613
|
entityTypesRouter.get(`/`, listEntityTypes);
|
|
7690
|
-
entityTypesRouter.post(`/`, withSchema(registerEntityTypeBodySchema), registerEntityType);
|
|
7691
|
-
entityTypesRouter.patch(`/:name/schemas`, withSchema(amendEntityTypeSchemasBodySchema), amendSchemas);
|
|
7692
|
-
entityTypesRouter.get(`/:name`, getEntityType);
|
|
7693
|
-
entityTypesRouter.delete(`/:name`, deleteEntityType);
|
|
8614
|
+
entityTypesRouter.post(`/`, withSchema(registerEntityTypeBodySchema), withEntityTypeRegistrationPermission, registerEntityType);
|
|
8615
|
+
entityTypesRouter.patch(`/:name/schemas`, withExistingEntityType, withEntityTypeManagePermission, withSchema(amendEntityTypeSchemasBodySchema), amendSchemas);
|
|
8616
|
+
entityTypesRouter.get(`/:name`, withExistingEntityType, withEntityTypeSpawnPermission, getEntityType);
|
|
8617
|
+
entityTypesRouter.delete(`/:name`, withExistingEntityType, withEntityTypeManagePermission, deleteEntityType);
|
|
8618
|
+
entityTypesRouter.get(`/:name/grants`, withExistingEntityType, withEntityTypeManagePermission, listTypePermissionGrants);
|
|
8619
|
+
entityTypesRouter.post(`/:name/grants`, withExistingEntityType, withSchema(typePermissionGrantInputSchema), withEntityTypeManagePermission, createTypePermissionGrant);
|
|
8620
|
+
entityTypesRouter.delete(`/:name/grants/:grantId`, withExistingEntityType, withEntityTypeManagePermission, deleteTypePermissionGrant);
|
|
7694
8621
|
async function registerEntityType(request, ctx) {
|
|
7695
8622
|
const parsed = routeBody(request);
|
|
7696
8623
|
const normalized = normalizeEntityTypeRequest(parsed);
|
|
7697
8624
|
if (normalized.serve_endpoint && !normalized.description && !normalized.creation_schema) return await discoverServeEndpoint(ctx, normalized);
|
|
7698
8625
|
const entityType = await ctx.entityManager.registerEntityType(normalized);
|
|
8626
|
+
await applyRegistrationPermissionGrants(ctx, entityType.name, normalized);
|
|
7699
8627
|
return json(toPublicEntityType(entityType), { status: 201 });
|
|
7700
8628
|
}
|
|
7701
8629
|
async function listEntityTypes(_request, ctx) {
|
|
7702
8630
|
const entityTypes$1 = await ctx.entityManager.registry.listEntityTypes();
|
|
7703
|
-
|
|
8631
|
+
const visible = [];
|
|
8632
|
+
for (const entityType of entityTypes$1) if (await canAccessEntityType(ctx, entityType, `spawn`)) visible.push(entityType);
|
|
8633
|
+
return json(visible.map((entityType) => toPublicEntityType(entityType)));
|
|
8634
|
+
}
|
|
8635
|
+
async function withExistingEntityType(request, ctx) {
|
|
8636
|
+
const entityType = await ctx.entityManager.registry.getEntityType(request.params.name);
|
|
8637
|
+
if (!entityType) return apiError(404, ErrCodeNotFound, `Entity type not found`);
|
|
8638
|
+
request.entityTypeRoute = { entityType };
|
|
8639
|
+
return void 0;
|
|
8640
|
+
}
|
|
8641
|
+
async function withEntityTypeManagePermission(request, ctx) {
|
|
8642
|
+
const entityType = request.entityTypeRoute?.entityType;
|
|
8643
|
+
if (!entityType) throw new Error(`entity type middleware did not run`);
|
|
8644
|
+
if (await canAccessEntityType(ctx, entityType, `manage`, request)) return void 0;
|
|
8645
|
+
return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to manage ${entityType.name}`);
|
|
8646
|
+
}
|
|
8647
|
+
async function withEntityTypeSpawnPermission(request, ctx) {
|
|
8648
|
+
const entityType = request.entityTypeRoute?.entityType;
|
|
8649
|
+
if (!entityType) throw new Error(`entity type middleware did not run`);
|
|
8650
|
+
if (await canAccessEntityType(ctx, entityType, `spawn`, request)) return void 0;
|
|
8651
|
+
return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to spawn ${entityType.name}`);
|
|
8652
|
+
}
|
|
8653
|
+
async function withEntityTypeRegistrationPermission(request, ctx) {
|
|
8654
|
+
const parsed = normalizeEntityTypeRequest(routeBody(request));
|
|
8655
|
+
if (!parsed.name) return void 0;
|
|
8656
|
+
const existing = await ctx.entityManager.registry.getEntityType(parsed.name);
|
|
8657
|
+
if (existing) {
|
|
8658
|
+
request.entityTypeRoute = { entityType: existing };
|
|
8659
|
+
if (await canAccessEntityType(ctx, existing, `manage`, request)) return void 0;
|
|
8660
|
+
return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to manage ${existing.name}`);
|
|
8661
|
+
}
|
|
8662
|
+
if (await canRegisterEntityType(ctx, parsed, request)) return void 0;
|
|
8663
|
+
return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to register entity types`);
|
|
7704
8664
|
}
|
|
7705
8665
|
async function discoverServeEndpoint(ctx, parsed) {
|
|
7706
8666
|
try {
|
|
@@ -7709,17 +8669,17 @@ async function discoverServeEndpoint(ctx, parsed) {
|
|
|
7709
8669
|
const manifest = await response.json();
|
|
7710
8670
|
if (manifest.name !== parsed.name) return apiError(400, ErrCodeServeEndpointNameMismatch, `Serve endpoint returned name "${manifest.name}" but expected "${parsed.name}"`);
|
|
7711
8671
|
manifest.serve_endpoint = parsed.serve_endpoint;
|
|
8672
|
+
manifest.permission_grants = parsed.permission_grants;
|
|
7712
8673
|
const entityType = await ctx.entityManager.registerEntityType(normalizeEntityTypeRequest(manifest));
|
|
8674
|
+
await applyRegistrationPermissionGrants(ctx, entityType.name, manifest);
|
|
7713
8675
|
return json(toPublicEntityType(entityType), { status: 201 });
|
|
7714
8676
|
} catch (err) {
|
|
7715
8677
|
if (err instanceof ElectricAgentsError) throw err;
|
|
7716
8678
|
return apiError(502, ErrCodeServeEndpointUnreachable, `Failed to reach serve endpoint: ${err instanceof Error ? err.message : String(err)}`);
|
|
7717
8679
|
}
|
|
7718
8680
|
}
|
|
7719
|
-
async function getEntityType(request
|
|
7720
|
-
|
|
7721
|
-
if (!entityType) return apiError(404, ErrCodeNotFound, `Entity type not found`);
|
|
7722
|
-
return json(toPublicEntityType(entityType));
|
|
8681
|
+
async function getEntityType(request) {
|
|
8682
|
+
return json(toPublicEntityType(request.entityTypeRoute.entityType));
|
|
7723
8683
|
}
|
|
7724
8684
|
async function amendSchemas(request, ctx) {
|
|
7725
8685
|
const parsed = routeBody(request);
|
|
@@ -7733,6 +8693,47 @@ async function deleteEntityType(request, ctx) {
|
|
|
7733
8693
|
await ctx.entityManager.deleteEntityType(request.params.name);
|
|
7734
8694
|
return status(204);
|
|
7735
8695
|
}
|
|
8696
|
+
async function listTypePermissionGrants(request, ctx) {
|
|
8697
|
+
const grants = await ctx.entityManager.registry.listEntityTypePermissionGrants(request.entityTypeRoute.entityType.name);
|
|
8698
|
+
return json({ grants });
|
|
8699
|
+
}
|
|
8700
|
+
async function createTypePermissionGrant(request, ctx) {
|
|
8701
|
+
const parsed = routeBody(request);
|
|
8702
|
+
const grant = await ctx.entityManager.registry.createEntityTypePermissionGrant({
|
|
8703
|
+
entityType: request.entityTypeRoute.entityType.name,
|
|
8704
|
+
permission: parsed.permission,
|
|
8705
|
+
subjectKind: parsed.subject_kind,
|
|
8706
|
+
subjectValue: parsed.subject_value,
|
|
8707
|
+
expiresAt: parseExpiresAt(parsed.expires_at),
|
|
8708
|
+
createdBy: ctx.principal.url
|
|
8709
|
+
});
|
|
8710
|
+
return json(grant, { status: 201 });
|
|
8711
|
+
}
|
|
8712
|
+
async function deleteTypePermissionGrant(request, ctx) {
|
|
8713
|
+
const deleted = await ctx.entityManager.registry.deleteEntityTypePermissionGrant(request.entityTypeRoute.entityType.name, parseGrantId(request));
|
|
8714
|
+
return deleted ? status(204) : apiError(404, ErrCodeNotFound, `Grant not found`);
|
|
8715
|
+
}
|
|
8716
|
+
async function applyRegistrationPermissionGrants(ctx, entityType, request) {
|
|
8717
|
+
for (const grant of request.permission_grants ?? []) await ctx.entityManager.registry.ensureEntityTypePermissionGrant({
|
|
8718
|
+
entityType,
|
|
8719
|
+
permission: grant.permission,
|
|
8720
|
+
subjectKind: grant.subject_kind,
|
|
8721
|
+
subjectValue: grant.subject_value,
|
|
8722
|
+
expiresAt: parseExpiresAt(grant.expires_at),
|
|
8723
|
+
createdBy: ctx.principal.url
|
|
8724
|
+
});
|
|
8725
|
+
}
|
|
8726
|
+
function parseGrantId(request) {
|
|
8727
|
+
const grantId = Number.parseInt(String(request.params.grantId), 10);
|
|
8728
|
+
if (!Number.isSafeInteger(grantId) || grantId <= 0) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Invalid grant id`, 400);
|
|
8729
|
+
return grantId;
|
|
8730
|
+
}
|
|
8731
|
+
function parseExpiresAt(value) {
|
|
8732
|
+
if (value === void 0) return void 0;
|
|
8733
|
+
const expiresAt = new Date(value);
|
|
8734
|
+
if (Number.isNaN(expiresAt.getTime())) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Invalid expires_at timestamp`, 400);
|
|
8735
|
+
return expiresAt;
|
|
8736
|
+
}
|
|
7736
8737
|
function normalizeEntityTypeRequest(parsed) {
|
|
7737
8738
|
const serveEndpoint = rewriteLoopbackWebhookUrl(parsed.serve_endpoint);
|
|
7738
8739
|
return {
|
|
@@ -7741,11 +8742,13 @@ function normalizeEntityTypeRequest(parsed) {
|
|
|
7741
8742
|
creation_schema: parsed.creation_schema,
|
|
7742
8743
|
inbox_schemas: parsed.inbox_schemas,
|
|
7743
8744
|
state_schemas: parsed.state_schemas,
|
|
8745
|
+
slash_commands: parsed.slash_commands,
|
|
7744
8746
|
serve_endpoint: serveEndpoint,
|
|
7745
8747
|
default_dispatch_policy: parsed.default_dispatch_policy ?? (serveEndpoint ? { targets: [{
|
|
7746
8748
|
type: `webhook`,
|
|
7747
8749
|
url: serveEndpoint
|
|
7748
|
-
}] } : void 0)
|
|
8750
|
+
}] } : void 0),
|
|
8751
|
+
permission_grants: parsed.permission_grants
|
|
7749
8752
|
};
|
|
7750
8753
|
}
|
|
7751
8754
|
function toPublicEntityType(entityType) {
|
|
@@ -7804,6 +8807,7 @@ function applyCors(response) {
|
|
|
7804
8807
|
`content-type`,
|
|
7805
8808
|
`authorization`,
|
|
7806
8809
|
`electric-claim-token`,
|
|
8810
|
+
`electric-owner-entity`,
|
|
7807
8811
|
ELECTRIC_PRINCIPAL_HEADER,
|
|
7808
8812
|
`ngrok-skip-browser-warning`
|
|
7809
8813
|
].join(`, `));
|
|
@@ -7854,7 +8858,7 @@ observationsRouter.post(`/entities/ensure-stream`, withSchema(ensureEntitiesMemb
|
|
|
7854
8858
|
observationsRouter.post(`/cron/ensure-stream`, withSchema(ensureCronStreamBodySchema), ensureCronStream);
|
|
7855
8859
|
async function ensureEntitiesMembershipStream(request, ctx) {
|
|
7856
8860
|
const parsed = routeBody(request);
|
|
7857
|
-
const result = await ctx.entityManager.ensureEntitiesMembershipStream(parsed.tags ?? {});
|
|
8861
|
+
const result = await ctx.entityManager.ensureEntitiesMembershipStream(parsed.tags ?? {}, ctx.principal);
|
|
7858
8862
|
return json(result);
|
|
7859
8863
|
}
|
|
7860
8864
|
async function ensureCronStream(request, ctx) {
|