@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/src/entity-registry.ts
CHANGED
|
@@ -1,15 +1,21 @@
|
|
|
1
|
-
import { and, desc, eq, lt, ne, sql } from 'drizzle-orm'
|
|
1
|
+
import { and, desc, eq, inArray, lt, ne, sql } from 'drizzle-orm'
|
|
2
2
|
import { buildTagsIndex, normalizeTags } from '@electric-ax/agents-runtime'
|
|
3
3
|
import {
|
|
4
4
|
consumerClaims,
|
|
5
5
|
entities,
|
|
6
|
+
entityEffectivePermissions,
|
|
6
7
|
entityBridges,
|
|
7
8
|
entityDispatchState,
|
|
8
9
|
entityManifestSources,
|
|
10
|
+
entityLineage,
|
|
11
|
+
entityPermissionGrants,
|
|
9
12
|
entityTypes,
|
|
13
|
+
entityTypePermissionGrants,
|
|
10
14
|
runnerRuntimeDiagnostics,
|
|
11
15
|
runners,
|
|
16
|
+
sharedStateLinks,
|
|
12
17
|
tagStreamOutbox,
|
|
18
|
+
users,
|
|
13
19
|
} from './db/schema.js'
|
|
14
20
|
import {
|
|
15
21
|
assertEntityStatus,
|
|
@@ -30,8 +36,15 @@ import type {
|
|
|
30
36
|
SourceStreamOffset,
|
|
31
37
|
ConsumerClaim,
|
|
32
38
|
DispatchPolicy,
|
|
39
|
+
EntityPermission,
|
|
40
|
+
EntityPermissionGrant,
|
|
41
|
+
EntityPermissionPropagation,
|
|
42
|
+
EntityTypePermission,
|
|
43
|
+
EntityTypePermissionGrant,
|
|
44
|
+
PermissionSubjectKind,
|
|
33
45
|
} from './electric-agents-types.js'
|
|
34
46
|
import type { EntityTags } from '@electric-ax/agents-runtime'
|
|
47
|
+
import type { Principal } from './principal.js'
|
|
35
48
|
|
|
36
49
|
export class EntityAlreadyExistsError extends Error {
|
|
37
50
|
constructor(public readonly url: string) {
|
|
@@ -51,6 +64,8 @@ export interface EntityBridgeRow {
|
|
|
51
64
|
sourceRef: string
|
|
52
65
|
tags: EntityTags
|
|
53
66
|
streamUrl: string
|
|
67
|
+
principalUrl?: string
|
|
68
|
+
principalKind?: string
|
|
54
69
|
shapeHandle?: string
|
|
55
70
|
shapeOffset?: string
|
|
56
71
|
lastObserverActivityAt: Date
|
|
@@ -135,13 +150,43 @@ export interface MaterializeReleasedClaimInput {
|
|
|
135
150
|
releasedAt?: Date
|
|
136
151
|
}
|
|
137
152
|
|
|
153
|
+
export interface CreateEntityTypePermissionGrantInput {
|
|
154
|
+
entityType: string
|
|
155
|
+
permission: EntityTypePermission
|
|
156
|
+
subjectKind: PermissionSubjectKind
|
|
157
|
+
subjectValue: string
|
|
158
|
+
createdBy?: string
|
|
159
|
+
expiresAt?: Date
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export interface CreateEntityPermissionGrantInput {
|
|
163
|
+
entityUrl: string
|
|
164
|
+
permission: EntityPermission
|
|
165
|
+
subjectKind: PermissionSubjectKind
|
|
166
|
+
subjectValue: string
|
|
167
|
+
propagation?: EntityPermissionPropagation
|
|
168
|
+
copyToChildren?: boolean
|
|
169
|
+
createdBy?: string
|
|
170
|
+
expiresAt?: Date
|
|
171
|
+
}
|
|
172
|
+
|
|
138
173
|
const DEFAULT_RUNNER_LEASE_MS = 30_000
|
|
174
|
+
const PERMISSION_PRUNE_INTERVAL_MS = 30_000
|
|
175
|
+
|
|
176
|
+
type RegistryTransaction = Parameters<
|
|
177
|
+
Parameters<DrizzleDB[`transaction`]>[0]
|
|
178
|
+
>[0]
|
|
139
179
|
|
|
140
180
|
export function runnerWakeStream(runnerId: string): string {
|
|
141
181
|
return `/runners/${runnerId}/wake`
|
|
142
182
|
}
|
|
143
183
|
|
|
144
184
|
export class PostgresRegistry {
|
|
185
|
+
// Electric predicates cannot depend on now(), so expired effective rows need a
|
|
186
|
+
// server-side sweep. Debounce it to keep read/auth paths from writing on every request.
|
|
187
|
+
private lastPermissionPruneStartedAt = 0
|
|
188
|
+
private permissionPrunePromise: Promise<void> | null = null
|
|
189
|
+
|
|
145
190
|
constructor(
|
|
146
191
|
private db: DrizzleDB,
|
|
147
192
|
readonly tenantId: string = DEFAULT_TENANT_ID
|
|
@@ -151,6 +196,18 @@ export class PostgresRegistry {
|
|
|
151
196
|
|
|
152
197
|
close(): void {}
|
|
153
198
|
|
|
199
|
+
async ensureUserForPrincipal(principal: Principal): Promise<void> {
|
|
200
|
+
if (principal.kind !== `user`) return
|
|
201
|
+
|
|
202
|
+
await this.db
|
|
203
|
+
.insert(users)
|
|
204
|
+
.values({
|
|
205
|
+
tenantId: this.tenantId,
|
|
206
|
+
id: principal.id,
|
|
207
|
+
})
|
|
208
|
+
.onConflictDoNothing()
|
|
209
|
+
}
|
|
210
|
+
|
|
154
211
|
async createRunner(
|
|
155
212
|
input: RegisterRunnerInput
|
|
156
213
|
): Promise<ElectricAgentsRunner> {
|
|
@@ -576,6 +633,7 @@ export class PostgresRegistry {
|
|
|
576
633
|
creationSchema: et.creation_schema ?? null,
|
|
577
634
|
inboxSchemas: et.inbox_schemas ?? null,
|
|
578
635
|
stateSchemas: et.state_schemas ?? null,
|
|
636
|
+
slashCommands: et.slash_commands ?? null,
|
|
579
637
|
serveEndpoint: et.serve_endpoint ?? null,
|
|
580
638
|
defaultDispatchPolicy: et.default_dispatch_policy ?? null,
|
|
581
639
|
revision: et.revision,
|
|
@@ -589,6 +647,7 @@ export class PostgresRegistry {
|
|
|
589
647
|
creationSchema: et.creation_schema ?? null,
|
|
590
648
|
inboxSchemas: et.inbox_schemas ?? null,
|
|
591
649
|
stateSchemas: et.state_schemas ?? null,
|
|
650
|
+
slashCommands: et.slash_commands ?? null,
|
|
592
651
|
serveEndpoint: et.serve_endpoint ?? null,
|
|
593
652
|
defaultDispatchPolicy: et.default_dispatch_policy ?? null,
|
|
594
653
|
revision: et.revision,
|
|
@@ -611,6 +670,7 @@ export class PostgresRegistry {
|
|
|
611
670
|
creationSchema: et.creation_schema ?? null,
|
|
612
671
|
inboxSchemas: et.inbox_schemas ?? null,
|
|
613
672
|
stateSchemas: et.state_schemas ?? null,
|
|
673
|
+
slashCommands: et.slash_commands ?? null,
|
|
614
674
|
serveEndpoint: et.serve_endpoint ?? null,
|
|
615
675
|
defaultDispatchPolicy: et.default_dispatch_policy ?? null,
|
|
616
676
|
revision: et.revision,
|
|
@@ -652,6 +712,7 @@ export class PostgresRegistry {
|
|
|
652
712
|
creationSchema: et.creation_schema ?? null,
|
|
653
713
|
inboxSchemas: et.inbox_schemas ?? null,
|
|
654
714
|
stateSchemas: et.state_schemas ?? null,
|
|
715
|
+
slashCommands: et.slash_commands ?? null,
|
|
655
716
|
serveEndpoint: et.serve_endpoint ?? null,
|
|
656
717
|
defaultDispatchPolicy: et.default_dispatch_policy ?? null,
|
|
657
718
|
revision: et.revision,
|
|
@@ -699,6 +760,67 @@ export class PostgresRegistry {
|
|
|
699
760
|
})
|
|
700
761
|
.onConflictDoNothing()
|
|
701
762
|
|
|
763
|
+
await tx
|
|
764
|
+
.insert(entityLineage)
|
|
765
|
+
.values({
|
|
766
|
+
tenantId: this.tenantId,
|
|
767
|
+
ancestorUrl: entity.url,
|
|
768
|
+
descendantUrl: entity.url,
|
|
769
|
+
depth: 0,
|
|
770
|
+
})
|
|
771
|
+
.onConflictDoNothing()
|
|
772
|
+
|
|
773
|
+
if (entity.parent) {
|
|
774
|
+
await tx.execute(sql`
|
|
775
|
+
INSERT INTO ${entityLineage} (
|
|
776
|
+
tenant_id,
|
|
777
|
+
ancestor_url,
|
|
778
|
+
descendant_url,
|
|
779
|
+
depth
|
|
780
|
+
)
|
|
781
|
+
SELECT
|
|
782
|
+
${this.tenantId},
|
|
783
|
+
ancestor_url,
|
|
784
|
+
${entity.url},
|
|
785
|
+
depth + 1
|
|
786
|
+
FROM ${entityLineage}
|
|
787
|
+
WHERE tenant_id = ${this.tenantId}
|
|
788
|
+
AND descendant_url = ${entity.parent}
|
|
789
|
+
ON CONFLICT DO NOTHING
|
|
790
|
+
`)
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
await tx.execute(sql`
|
|
794
|
+
INSERT INTO ${entityEffectivePermissions} (
|
|
795
|
+
tenant_id,
|
|
796
|
+
entity_url,
|
|
797
|
+
source_entity_url,
|
|
798
|
+
source_grant_id,
|
|
799
|
+
permission,
|
|
800
|
+
subject_kind,
|
|
801
|
+
subject_value,
|
|
802
|
+
expires_at
|
|
803
|
+
)
|
|
804
|
+
SELECT
|
|
805
|
+
${this.tenantId},
|
|
806
|
+
${entity.url},
|
|
807
|
+
grants.entity_url,
|
|
808
|
+
grants.id,
|
|
809
|
+
grants.permission,
|
|
810
|
+
grants.subject_kind,
|
|
811
|
+
grants.subject_value,
|
|
812
|
+
grants.expires_at
|
|
813
|
+
FROM ${entityPermissionGrants} grants
|
|
814
|
+
JOIN ${entityLineage} lineage
|
|
815
|
+
ON lineage.tenant_id = grants.tenant_id
|
|
816
|
+
AND lineage.ancestor_url = grants.entity_url
|
|
817
|
+
AND lineage.descendant_url = ${entity.url}
|
|
818
|
+
WHERE grants.tenant_id = ${this.tenantId}
|
|
819
|
+
AND grants.propagation = 'descendants'
|
|
820
|
+
AND (grants.expires_at IS NULL OR grants.expires_at > now())
|
|
821
|
+
ON CONFLICT DO NOTHING
|
|
822
|
+
`)
|
|
823
|
+
|
|
702
824
|
return parseInt(result[0]!.txid)
|
|
703
825
|
})
|
|
704
826
|
} catch (err) {
|
|
@@ -735,12 +857,9 @@ export class PostgresRegistry {
|
|
|
735
857
|
streamPath: string
|
|
736
858
|
): Promise<ElectricAgentsEntity | null> {
|
|
737
859
|
const mainSuffix = `/main`
|
|
738
|
-
const errorSuffix = `/error`
|
|
739
860
|
let entityUrl: string | null = null
|
|
740
861
|
if (streamPath.endsWith(mainSuffix)) {
|
|
741
862
|
entityUrl = streamPath.slice(0, -mainSuffix.length)
|
|
742
|
-
} else if (streamPath.endsWith(errorSuffix)) {
|
|
743
|
-
entityUrl = streamPath.slice(0, -errorSuffix.length)
|
|
744
863
|
}
|
|
745
864
|
if (!entityUrl) return null
|
|
746
865
|
return this.getEntity(entityUrl)
|
|
@@ -753,6 +872,11 @@ export class PostgresRegistry {
|
|
|
753
872
|
limit?: number
|
|
754
873
|
offset?: number
|
|
755
874
|
created_by?: string
|
|
875
|
+
readableBy?: {
|
|
876
|
+
principalUrl: string
|
|
877
|
+
principalKind: string
|
|
878
|
+
bypass?: boolean
|
|
879
|
+
}
|
|
756
880
|
}): Promise<{ entities: Array<ElectricAgentsEntity>; total: number }> {
|
|
757
881
|
const conditions = [eq(entities.tenantId, this.tenantId)]
|
|
758
882
|
if (filter?.type) conditions.push(eq(entities.type, filter.type))
|
|
@@ -760,6 +884,25 @@ export class PostgresRegistry {
|
|
|
760
884
|
if (filter?.parent) conditions.push(eq(entities.parent, filter.parent))
|
|
761
885
|
if (filter?.created_by)
|
|
762
886
|
conditions.push(eq(entities.createdBy, filter.created_by))
|
|
887
|
+
if (filter?.readableBy && !filter.readableBy.bypass) {
|
|
888
|
+
conditions.push(sql`(
|
|
889
|
+
${entities.createdBy} = ${filter.readableBy.principalUrl}
|
|
890
|
+
OR ${entities.url} IN (
|
|
891
|
+
SELECT ${entityEffectivePermissions.entityUrl}
|
|
892
|
+
FROM ${entityEffectivePermissions}
|
|
893
|
+
WHERE ${entityEffectivePermissions.tenantId} = ${this.tenantId}
|
|
894
|
+
AND ${entityEffectivePermissions.permission} IN ('read', 'manage')
|
|
895
|
+
AND (${entityEffectivePermissions.expiresAt} IS NULL OR ${entityEffectivePermissions.expiresAt} > now())
|
|
896
|
+
AND (
|
|
897
|
+
(${entityEffectivePermissions.subjectKind} = 'principal'
|
|
898
|
+
AND ${entityEffectivePermissions.subjectValue} = ${filter.readableBy.principalUrl})
|
|
899
|
+
OR
|
|
900
|
+
(${entityEffectivePermissions.subjectKind} = 'principal_kind'
|
|
901
|
+
AND ${entityEffectivePermissions.subjectValue} = ${filter.readableBy.principalKind})
|
|
902
|
+
)
|
|
903
|
+
)
|
|
904
|
+
)`)
|
|
905
|
+
}
|
|
763
906
|
|
|
764
907
|
const whereClause = and(...conditions)
|
|
765
908
|
|
|
@@ -790,6 +933,431 @@ export class PostgresRegistry {
|
|
|
790
933
|
}
|
|
791
934
|
}
|
|
792
935
|
|
|
936
|
+
async createEntityTypePermissionGrant(
|
|
937
|
+
input: CreateEntityTypePermissionGrantInput
|
|
938
|
+
): Promise<EntityTypePermissionGrant> {
|
|
939
|
+
const [row] = await this.db
|
|
940
|
+
.insert(entityTypePermissionGrants)
|
|
941
|
+
.values({
|
|
942
|
+
tenantId: this.tenantId,
|
|
943
|
+
entityType: input.entityType,
|
|
944
|
+
permission: input.permission,
|
|
945
|
+
subjectKind: input.subjectKind,
|
|
946
|
+
subjectValue: input.subjectValue,
|
|
947
|
+
createdBy: input.createdBy ?? null,
|
|
948
|
+
expiresAt: input.expiresAt ?? null,
|
|
949
|
+
})
|
|
950
|
+
.returning()
|
|
951
|
+
return this.rowToEntityTypePermissionGrant(row!)
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
async ensureEntityTypePermissionGrant(
|
|
955
|
+
input: CreateEntityTypePermissionGrantInput
|
|
956
|
+
): Promise<EntityTypePermissionGrant> {
|
|
957
|
+
const [existing] = await this.db
|
|
958
|
+
.select()
|
|
959
|
+
.from(entityTypePermissionGrants)
|
|
960
|
+
.where(
|
|
961
|
+
and(
|
|
962
|
+
eq(entityTypePermissionGrants.tenantId, this.tenantId),
|
|
963
|
+
eq(entityTypePermissionGrants.entityType, input.entityType),
|
|
964
|
+
eq(entityTypePermissionGrants.permission, input.permission),
|
|
965
|
+
eq(entityTypePermissionGrants.subjectKind, input.subjectKind),
|
|
966
|
+
eq(entityTypePermissionGrants.subjectValue, input.subjectValue),
|
|
967
|
+
input.expiresAt
|
|
968
|
+
? eq(entityTypePermissionGrants.expiresAt, input.expiresAt)
|
|
969
|
+
: sql`${entityTypePermissionGrants.expiresAt} IS NULL`
|
|
970
|
+
)
|
|
971
|
+
)
|
|
972
|
+
.limit(1)
|
|
973
|
+
if (existing) return this.rowToEntityTypePermissionGrant(existing)
|
|
974
|
+
|
|
975
|
+
return await this.createEntityTypePermissionGrant(input)
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
async listEntityTypePermissionGrants(
|
|
979
|
+
entityType: string
|
|
980
|
+
): Promise<Array<EntityTypePermissionGrant>> {
|
|
981
|
+
const rows = await this.db
|
|
982
|
+
.select()
|
|
983
|
+
.from(entityTypePermissionGrants)
|
|
984
|
+
.where(
|
|
985
|
+
and(
|
|
986
|
+
eq(entityTypePermissionGrants.tenantId, this.tenantId),
|
|
987
|
+
eq(entityTypePermissionGrants.entityType, entityType)
|
|
988
|
+
)
|
|
989
|
+
)
|
|
990
|
+
.orderBy(entityTypePermissionGrants.id)
|
|
991
|
+
return rows.map((row) => this.rowToEntityTypePermissionGrant(row))
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
async deleteEntityTypePermissionGrant(
|
|
995
|
+
entityType: string,
|
|
996
|
+
grantId: number
|
|
997
|
+
): Promise<boolean> {
|
|
998
|
+
const rows = await this.db
|
|
999
|
+
.delete(entityTypePermissionGrants)
|
|
1000
|
+
.where(
|
|
1001
|
+
and(
|
|
1002
|
+
eq(entityTypePermissionGrants.tenantId, this.tenantId),
|
|
1003
|
+
eq(entityTypePermissionGrants.entityType, entityType),
|
|
1004
|
+
eq(entityTypePermissionGrants.id, grantId)
|
|
1005
|
+
)
|
|
1006
|
+
)
|
|
1007
|
+
.returning({ id: entityTypePermissionGrants.id })
|
|
1008
|
+
return rows.length > 0
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
async hasEntityTypePermission(
|
|
1012
|
+
entityType: string,
|
|
1013
|
+
permission: EntityTypePermission,
|
|
1014
|
+
subject: { principalUrl: string; principalKind: string }
|
|
1015
|
+
): Promise<boolean> {
|
|
1016
|
+
const permissions = [permission, `manage`] as const
|
|
1017
|
+
const rows = await this.db
|
|
1018
|
+
.select({ id: entityTypePermissionGrants.id })
|
|
1019
|
+
.from(entityTypePermissionGrants)
|
|
1020
|
+
.where(
|
|
1021
|
+
and(
|
|
1022
|
+
eq(entityTypePermissionGrants.tenantId, this.tenantId),
|
|
1023
|
+
eq(entityTypePermissionGrants.entityType, entityType),
|
|
1024
|
+
inArray(entityTypePermissionGrants.permission, [...permissions]),
|
|
1025
|
+
sql`(${entityTypePermissionGrants.expiresAt} IS NULL OR ${entityTypePermissionGrants.expiresAt} > now())`,
|
|
1026
|
+
sql`(
|
|
1027
|
+
(${entityTypePermissionGrants.subjectKind} = 'principal'
|
|
1028
|
+
AND ${entityTypePermissionGrants.subjectValue} = ${subject.principalUrl})
|
|
1029
|
+
OR
|
|
1030
|
+
(${entityTypePermissionGrants.subjectKind} = 'principal_kind'
|
|
1031
|
+
AND ${entityTypePermissionGrants.subjectValue} = ${subject.principalKind})
|
|
1032
|
+
)`
|
|
1033
|
+
)
|
|
1034
|
+
)
|
|
1035
|
+
.limit(1)
|
|
1036
|
+
return rows.length > 0
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
async createEntityPermissionGrant(
|
|
1040
|
+
input: CreateEntityPermissionGrantInput
|
|
1041
|
+
): Promise<EntityPermissionGrant> {
|
|
1042
|
+
return await this.db.transaction(async (tx) => {
|
|
1043
|
+
const [row] = await tx
|
|
1044
|
+
.insert(entityPermissionGrants)
|
|
1045
|
+
.values({
|
|
1046
|
+
tenantId: this.tenantId,
|
|
1047
|
+
entityUrl: input.entityUrl,
|
|
1048
|
+
permission: input.permission,
|
|
1049
|
+
subjectKind: input.subjectKind,
|
|
1050
|
+
subjectValue: input.subjectValue,
|
|
1051
|
+
propagation: input.propagation ?? `self`,
|
|
1052
|
+
copyToChildren: input.copyToChildren ?? false,
|
|
1053
|
+
createdBy: input.createdBy ?? null,
|
|
1054
|
+
expiresAt: input.expiresAt ?? null,
|
|
1055
|
+
})
|
|
1056
|
+
.returning()
|
|
1057
|
+
await this.materializeEntityPermissionGrant(tx, row!)
|
|
1058
|
+
return this.rowToEntityPermissionGrant(row!)
|
|
1059
|
+
})
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
async listEntityPermissionGrants(
|
|
1063
|
+
entityUrl: string
|
|
1064
|
+
): Promise<Array<EntityPermissionGrant>> {
|
|
1065
|
+
const rows = await this.db
|
|
1066
|
+
.select()
|
|
1067
|
+
.from(entityPermissionGrants)
|
|
1068
|
+
.where(
|
|
1069
|
+
and(
|
|
1070
|
+
eq(entityPermissionGrants.tenantId, this.tenantId),
|
|
1071
|
+
eq(entityPermissionGrants.entityUrl, entityUrl)
|
|
1072
|
+
)
|
|
1073
|
+
)
|
|
1074
|
+
.orderBy(entityPermissionGrants.id)
|
|
1075
|
+
return rows.map((row) => this.rowToEntityPermissionGrant(row))
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
async deleteEntityPermissionGrant(
|
|
1079
|
+
entityUrl: string,
|
|
1080
|
+
grantId: number
|
|
1081
|
+
): Promise<boolean> {
|
|
1082
|
+
return await this.db.transaction(async (tx) => {
|
|
1083
|
+
await tx
|
|
1084
|
+
.delete(entityEffectivePermissions)
|
|
1085
|
+
.where(
|
|
1086
|
+
and(
|
|
1087
|
+
eq(entityEffectivePermissions.tenantId, this.tenantId),
|
|
1088
|
+
eq(entityEffectivePermissions.sourceGrantId, grantId)
|
|
1089
|
+
)
|
|
1090
|
+
)
|
|
1091
|
+
const rows = await tx
|
|
1092
|
+
.delete(entityPermissionGrants)
|
|
1093
|
+
.where(
|
|
1094
|
+
and(
|
|
1095
|
+
eq(entityPermissionGrants.tenantId, this.tenantId),
|
|
1096
|
+
eq(entityPermissionGrants.entityUrl, entityUrl),
|
|
1097
|
+
eq(entityPermissionGrants.id, grantId)
|
|
1098
|
+
)
|
|
1099
|
+
)
|
|
1100
|
+
.returning({ id: entityPermissionGrants.id })
|
|
1101
|
+
return rows.length > 0
|
|
1102
|
+
})
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
async copyEntityPermissionGrantsForSpawn(
|
|
1106
|
+
parentEntityUrl: string,
|
|
1107
|
+
childEntityUrl: string,
|
|
1108
|
+
createdBy?: string
|
|
1109
|
+
): Promise<Array<EntityPermissionGrant>> {
|
|
1110
|
+
const parentGrants = await this.db
|
|
1111
|
+
.select()
|
|
1112
|
+
.from(entityPermissionGrants)
|
|
1113
|
+
.where(
|
|
1114
|
+
and(
|
|
1115
|
+
eq(entityPermissionGrants.tenantId, this.tenantId),
|
|
1116
|
+
eq(entityPermissionGrants.entityUrl, parentEntityUrl),
|
|
1117
|
+
eq(entityPermissionGrants.copyToChildren, true),
|
|
1118
|
+
sql`(${entityPermissionGrants.expiresAt} IS NULL OR ${entityPermissionGrants.expiresAt} > now())`
|
|
1119
|
+
)
|
|
1120
|
+
)
|
|
1121
|
+
|
|
1122
|
+
const copied: Array<EntityPermissionGrant> = []
|
|
1123
|
+
for (const grant of parentGrants) {
|
|
1124
|
+
copied.push(
|
|
1125
|
+
await this.createEntityPermissionGrant({
|
|
1126
|
+
entityUrl: childEntityUrl,
|
|
1127
|
+
permission: grant.permission as EntityPermission,
|
|
1128
|
+
subjectKind: grant.subjectKind as PermissionSubjectKind,
|
|
1129
|
+
subjectValue: grant.subjectValue,
|
|
1130
|
+
propagation: `self`,
|
|
1131
|
+
copyToChildren: grant.copyToChildren,
|
|
1132
|
+
createdBy,
|
|
1133
|
+
expiresAt: grant.expiresAt ?? undefined,
|
|
1134
|
+
})
|
|
1135
|
+
)
|
|
1136
|
+
}
|
|
1137
|
+
return copied
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
async hasEntityPermission(
|
|
1141
|
+
entityUrl: string,
|
|
1142
|
+
permission: EntityPermission,
|
|
1143
|
+
subject: { principalUrl: string; principalKind: string }
|
|
1144
|
+
): Promise<boolean> {
|
|
1145
|
+
const permissions = [permission, `manage`] as const
|
|
1146
|
+
const rows = await this.db
|
|
1147
|
+
.select({ id: entityEffectivePermissions.id })
|
|
1148
|
+
.from(entityEffectivePermissions)
|
|
1149
|
+
.where(
|
|
1150
|
+
and(
|
|
1151
|
+
eq(entityEffectivePermissions.tenantId, this.tenantId),
|
|
1152
|
+
eq(entityEffectivePermissions.entityUrl, entityUrl),
|
|
1153
|
+
inArray(entityEffectivePermissions.permission, [...permissions]),
|
|
1154
|
+
sql`(${entityEffectivePermissions.expiresAt} IS NULL OR ${entityEffectivePermissions.expiresAt} > now())`,
|
|
1155
|
+
sql`(
|
|
1156
|
+
(${entityEffectivePermissions.subjectKind} = 'principal'
|
|
1157
|
+
AND ${entityEffectivePermissions.subjectValue} = ${subject.principalUrl})
|
|
1158
|
+
OR
|
|
1159
|
+
(${entityEffectivePermissions.subjectKind} = 'principal_kind'
|
|
1160
|
+
AND ${entityEffectivePermissions.subjectValue} = ${subject.principalKind})
|
|
1161
|
+
)`
|
|
1162
|
+
)
|
|
1163
|
+
)
|
|
1164
|
+
.limit(1)
|
|
1165
|
+
return rows.length > 0
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
async replaceSharedStateLink(
|
|
1169
|
+
ownerEntityUrl: string,
|
|
1170
|
+
manifestKey: string,
|
|
1171
|
+
sharedStateId?: string
|
|
1172
|
+
): Promise<void> {
|
|
1173
|
+
await this.db
|
|
1174
|
+
.delete(sharedStateLinks)
|
|
1175
|
+
.where(
|
|
1176
|
+
and(
|
|
1177
|
+
eq(sharedStateLinks.tenantId, this.tenantId),
|
|
1178
|
+
eq(sharedStateLinks.ownerEntityUrl, ownerEntityUrl),
|
|
1179
|
+
eq(sharedStateLinks.manifestKey, manifestKey)
|
|
1180
|
+
)
|
|
1181
|
+
)
|
|
1182
|
+
|
|
1183
|
+
if (!sharedStateId) return
|
|
1184
|
+
|
|
1185
|
+
await this.db
|
|
1186
|
+
.insert(sharedStateLinks)
|
|
1187
|
+
.values({
|
|
1188
|
+
tenantId: this.tenantId,
|
|
1189
|
+
ownerEntityUrl,
|
|
1190
|
+
manifestKey,
|
|
1191
|
+
sharedStateId,
|
|
1192
|
+
})
|
|
1193
|
+
.onConflictDoUpdate({
|
|
1194
|
+
target: [
|
|
1195
|
+
sharedStateLinks.tenantId,
|
|
1196
|
+
sharedStateLinks.ownerEntityUrl,
|
|
1197
|
+
sharedStateLinks.manifestKey,
|
|
1198
|
+
],
|
|
1199
|
+
set: {
|
|
1200
|
+
sharedStateId,
|
|
1201
|
+
updatedAt: new Date(),
|
|
1202
|
+
},
|
|
1203
|
+
})
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
async listSharedStateLinkedEntityUrls(
|
|
1207
|
+
sharedStateId: string
|
|
1208
|
+
): Promise<Array<string>> {
|
|
1209
|
+
const rows = await this.db
|
|
1210
|
+
.selectDistinct({ ownerEntityUrl: sharedStateLinks.ownerEntityUrl })
|
|
1211
|
+
.from(sharedStateLinks)
|
|
1212
|
+
.where(
|
|
1213
|
+
and(
|
|
1214
|
+
eq(sharedStateLinks.tenantId, this.tenantId),
|
|
1215
|
+
eq(sharedStateLinks.sharedStateId, sharedStateId)
|
|
1216
|
+
)
|
|
1217
|
+
)
|
|
1218
|
+
return rows.map((row) => row.ownerEntityUrl)
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
async pruneExpiredPermissionGrants(
|
|
1222
|
+
now: Date = new Date(),
|
|
1223
|
+
options: { force?: boolean } = {}
|
|
1224
|
+
): Promise<void> {
|
|
1225
|
+
if (this.permissionPrunePromise) return await this.permissionPrunePromise
|
|
1226
|
+
|
|
1227
|
+
const startedAt = Date.now()
|
|
1228
|
+
if (
|
|
1229
|
+
!options.force &&
|
|
1230
|
+
startedAt - this.lastPermissionPruneStartedAt <
|
|
1231
|
+
PERMISSION_PRUNE_INTERVAL_MS
|
|
1232
|
+
) {
|
|
1233
|
+
return
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
this.lastPermissionPruneStartedAt = startedAt
|
|
1237
|
+
const promise = this.pruneExpiredPermissionGrantsNow(now)
|
|
1238
|
+
this.permissionPrunePromise = promise
|
|
1239
|
+
try {
|
|
1240
|
+
await promise
|
|
1241
|
+
} catch (error) {
|
|
1242
|
+
this.lastPermissionPruneStartedAt = 0
|
|
1243
|
+
throw error
|
|
1244
|
+
} finally {
|
|
1245
|
+
if (this.permissionPrunePromise === promise) {
|
|
1246
|
+
this.permissionPrunePromise = null
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
private async pruneExpiredPermissionGrantsNow(now: Date): Promise<void> {
|
|
1252
|
+
await this.db.transaction(async (tx) => {
|
|
1253
|
+
const expiredEntityGrantIds = await tx
|
|
1254
|
+
.select({ id: entityPermissionGrants.id })
|
|
1255
|
+
.from(entityPermissionGrants)
|
|
1256
|
+
.where(
|
|
1257
|
+
and(
|
|
1258
|
+
eq(entityPermissionGrants.tenantId, this.tenantId),
|
|
1259
|
+
sql`${entityPermissionGrants.expiresAt} IS NOT NULL`,
|
|
1260
|
+
lt(entityPermissionGrants.expiresAt, now)
|
|
1261
|
+
)
|
|
1262
|
+
)
|
|
1263
|
+
const ids = expiredEntityGrantIds.map((row) => row.id)
|
|
1264
|
+
if (ids.length > 0) {
|
|
1265
|
+
await tx
|
|
1266
|
+
.delete(entityEffectivePermissions)
|
|
1267
|
+
.where(
|
|
1268
|
+
and(
|
|
1269
|
+
eq(entityEffectivePermissions.tenantId, this.tenantId),
|
|
1270
|
+
inArray(entityEffectivePermissions.sourceGrantId, ids)
|
|
1271
|
+
)
|
|
1272
|
+
)
|
|
1273
|
+
await tx
|
|
1274
|
+
.delete(entityPermissionGrants)
|
|
1275
|
+
.where(
|
|
1276
|
+
and(
|
|
1277
|
+
eq(entityPermissionGrants.tenantId, this.tenantId),
|
|
1278
|
+
inArray(entityPermissionGrants.id, ids)
|
|
1279
|
+
)
|
|
1280
|
+
)
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
await tx
|
|
1284
|
+
.delete(entityEffectivePermissions)
|
|
1285
|
+
.where(
|
|
1286
|
+
and(
|
|
1287
|
+
eq(entityEffectivePermissions.tenantId, this.tenantId),
|
|
1288
|
+
sql`${entityEffectivePermissions.expiresAt} IS NOT NULL`,
|
|
1289
|
+
lt(entityEffectivePermissions.expiresAt, now)
|
|
1290
|
+
)
|
|
1291
|
+
)
|
|
1292
|
+
await tx
|
|
1293
|
+
.delete(entityTypePermissionGrants)
|
|
1294
|
+
.where(
|
|
1295
|
+
and(
|
|
1296
|
+
eq(entityTypePermissionGrants.tenantId, this.tenantId),
|
|
1297
|
+
sql`${entityTypePermissionGrants.expiresAt} IS NOT NULL`,
|
|
1298
|
+
lt(entityTypePermissionGrants.expiresAt, now)
|
|
1299
|
+
)
|
|
1300
|
+
)
|
|
1301
|
+
})
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
private async materializeEntityPermissionGrant(
|
|
1305
|
+
tx: RegistryTransaction,
|
|
1306
|
+
grant: typeof entityPermissionGrants.$inferSelect
|
|
1307
|
+
): Promise<void> {
|
|
1308
|
+
await tx
|
|
1309
|
+
.delete(entityEffectivePermissions)
|
|
1310
|
+
.where(
|
|
1311
|
+
and(
|
|
1312
|
+
eq(entityEffectivePermissions.tenantId, this.tenantId),
|
|
1313
|
+
eq(entityEffectivePermissions.sourceGrantId, grant.id)
|
|
1314
|
+
)
|
|
1315
|
+
)
|
|
1316
|
+
|
|
1317
|
+
if (grant.propagation === `descendants`) {
|
|
1318
|
+
await tx.execute(sql`
|
|
1319
|
+
INSERT INTO ${entityEffectivePermissions} (
|
|
1320
|
+
tenant_id,
|
|
1321
|
+
entity_url,
|
|
1322
|
+
source_entity_url,
|
|
1323
|
+
source_grant_id,
|
|
1324
|
+
permission,
|
|
1325
|
+
subject_kind,
|
|
1326
|
+
subject_value,
|
|
1327
|
+
expires_at
|
|
1328
|
+
)
|
|
1329
|
+
SELECT
|
|
1330
|
+
${this.tenantId},
|
|
1331
|
+
descendant_url,
|
|
1332
|
+
${grant.entityUrl},
|
|
1333
|
+
${grant.id},
|
|
1334
|
+
${grant.permission},
|
|
1335
|
+
${grant.subjectKind},
|
|
1336
|
+
${grant.subjectValue},
|
|
1337
|
+
${grant.expiresAt}
|
|
1338
|
+
FROM ${entityLineage}
|
|
1339
|
+
WHERE tenant_id = ${this.tenantId}
|
|
1340
|
+
AND ancestor_url = ${grant.entityUrl}
|
|
1341
|
+
ON CONFLICT DO NOTHING
|
|
1342
|
+
`)
|
|
1343
|
+
return
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
await tx
|
|
1347
|
+
.insert(entityEffectivePermissions)
|
|
1348
|
+
.values({
|
|
1349
|
+
tenantId: this.tenantId,
|
|
1350
|
+
entityUrl: grant.entityUrl,
|
|
1351
|
+
sourceEntityUrl: grant.entityUrl,
|
|
1352
|
+
sourceGrantId: grant.id,
|
|
1353
|
+
permission: grant.permission,
|
|
1354
|
+
subjectKind: grant.subjectKind,
|
|
1355
|
+
subjectValue: grant.subjectValue,
|
|
1356
|
+
expiresAt: grant.expiresAt,
|
|
1357
|
+
})
|
|
1358
|
+
.onConflictDoNothing()
|
|
1359
|
+
}
|
|
1360
|
+
|
|
793
1361
|
async updateStatus(entityUrl: string, status: EntityStatus): Promise<void> {
|
|
794
1362
|
const whereClause = isTerminalEntityStatus(status)
|
|
795
1363
|
? this.entityWhere(entityUrl)
|
|
@@ -953,6 +1521,8 @@ export class PostgresRegistry {
|
|
|
953
1521
|
sourceRef: string
|
|
954
1522
|
tags: EntityTags
|
|
955
1523
|
streamUrl: string
|
|
1524
|
+
principalUrl: string
|
|
1525
|
+
principalKind: string
|
|
956
1526
|
}): Promise<EntityBridgeRow> {
|
|
957
1527
|
await this.db
|
|
958
1528
|
.insert(entityBridges)
|
|
@@ -961,6 +1531,8 @@ export class PostgresRegistry {
|
|
|
961
1531
|
sourceRef: row.sourceRef,
|
|
962
1532
|
tags: normalizeTags(row.tags),
|
|
963
1533
|
streamUrl: row.streamUrl,
|
|
1534
|
+
principalUrl: row.principalUrl,
|
|
1535
|
+
principalKind: row.principalKind,
|
|
964
1536
|
})
|
|
965
1537
|
.onConflictDoNothing()
|
|
966
1538
|
|
|
@@ -1261,6 +1833,9 @@ export class PostgresRegistry {
|
|
|
1261
1833
|
state_schemas: row.stateSchemas as
|
|
1262
1834
|
| Record<string, Record<string, unknown>>
|
|
1263
1835
|
| undefined,
|
|
1836
|
+
slash_commands:
|
|
1837
|
+
(row.slashCommands as ElectricAgentsEntityType[`slash_commands`]) ??
|
|
1838
|
+
undefined,
|
|
1264
1839
|
serve_endpoint: row.serveEndpoint ?? undefined,
|
|
1265
1840
|
default_dispatch_policy:
|
|
1266
1841
|
(row.defaultDispatchPolicy as ElectricAgentsEntityType[`default_dispatch_policy`]) ??
|
|
@@ -1271,6 +1846,40 @@ export class PostgresRegistry {
|
|
|
1271
1846
|
}
|
|
1272
1847
|
}
|
|
1273
1848
|
|
|
1849
|
+
private rowToEntityTypePermissionGrant(
|
|
1850
|
+
row: typeof entityTypePermissionGrants.$inferSelect
|
|
1851
|
+
): EntityTypePermissionGrant {
|
|
1852
|
+
return {
|
|
1853
|
+
id: row.id,
|
|
1854
|
+
entity_type: row.entityType,
|
|
1855
|
+
permission: row.permission as EntityTypePermission,
|
|
1856
|
+
subject_kind: row.subjectKind as PermissionSubjectKind,
|
|
1857
|
+
subject_value: row.subjectValue,
|
|
1858
|
+
created_by: row.createdBy ?? undefined,
|
|
1859
|
+
expires_at: row.expiresAt?.toISOString(),
|
|
1860
|
+
created_at: row.createdAt.toISOString(),
|
|
1861
|
+
updated_at: row.updatedAt.toISOString(),
|
|
1862
|
+
}
|
|
1863
|
+
}
|
|
1864
|
+
|
|
1865
|
+
private rowToEntityPermissionGrant(
|
|
1866
|
+
row: typeof entityPermissionGrants.$inferSelect
|
|
1867
|
+
): EntityPermissionGrant {
|
|
1868
|
+
return {
|
|
1869
|
+
id: row.id,
|
|
1870
|
+
entity_url: row.entityUrl,
|
|
1871
|
+
permission: row.permission as EntityPermission,
|
|
1872
|
+
subject_kind: row.subjectKind as PermissionSubjectKind,
|
|
1873
|
+
subject_value: row.subjectValue,
|
|
1874
|
+
propagation: row.propagation as EntityPermissionPropagation,
|
|
1875
|
+
copy_to_children: row.copyToChildren,
|
|
1876
|
+
created_by: row.createdBy ?? undefined,
|
|
1877
|
+
expires_at: row.expiresAt?.toISOString(),
|
|
1878
|
+
created_at: row.createdAt.toISOString(),
|
|
1879
|
+
updated_at: row.updatedAt.toISOString(),
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
|
|
1274
1883
|
private rowToEntity(row: typeof entities.$inferSelect): ElectricAgentsEntity {
|
|
1275
1884
|
return {
|
|
1276
1885
|
url: row.url,
|
|
@@ -1278,7 +1887,6 @@ export class PostgresRegistry {
|
|
|
1278
1887
|
status: assertEntityStatus(row.status),
|
|
1279
1888
|
streams: {
|
|
1280
1889
|
main: `${row.url}/main`,
|
|
1281
|
-
error: `${row.url}/error`,
|
|
1282
1890
|
},
|
|
1283
1891
|
subscription_id: row.subscriptionId,
|
|
1284
1892
|
dispatch_policy:
|
|
@@ -1311,6 +1919,8 @@ export class PostgresRegistry {
|
|
|
1311
1919
|
sourceRef: row.sourceRef,
|
|
1312
1920
|
tags: (row.tags as EntityTags | null | undefined) ?? {},
|
|
1313
1921
|
streamUrl: row.streamUrl,
|
|
1922
|
+
principalUrl: row.principalUrl ?? undefined,
|
|
1923
|
+
principalKind: row.principalKind ?? undefined,
|
|
1314
1924
|
shapeHandle: row.shapeHandle ?? undefined,
|
|
1315
1925
|
shapeOffset: row.shapeOffset ?? undefined,
|
|
1316
1926
|
lastObserverActivityAt: row.lastObserverActivityAt,
|