@electric-ax/agents-server 0.4.0 → 0.4.1
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 +379 -94
- package/dist/index.cjs +489 -196
- package/dist/index.d.cts +286 -244
- package/dist/index.d.ts +286 -244
- package/dist/index.js +489 -196
- package/drizzle/0006_principals.sql +5 -0
- package/drizzle/meta/_journal.json +7 -0
- package/package.json +6 -6
- package/src/authenticated-user-format.ts +4 -14
- package/src/db/schema.ts +2 -0
- package/src/electric-agents-types.ts +10 -7
- package/src/entity-bridge-manager.ts +1 -1
- package/src/entity-manager.ts +223 -41
- package/src/entity-registry.ts +29 -0
- package/src/entrypoint-lib.ts +0 -9
- package/src/host.ts +4 -0
- package/src/index.ts +2 -1
- package/src/principal.ts +124 -0
- package/src/routing/context.ts +2 -2
- package/src/routing/dispatch-policy.ts +10 -10
- package/src/routing/entities-router.ts +179 -6
- package/src/routing/hooks.ts +1 -1
- package/src/routing/runners-router.ts +10 -18
- package/src/runtime.ts +4 -1
- package/src/scheduler.ts +2 -0
- package/src/server.ts +59 -7
- package/src/dev-asserted-auth.ts +0 -46
package/dist/entrypoint.js
CHANGED
|
@@ -32,36 +32,6 @@ var __export = (target, all) => {
|
|
|
32
32
|
});
|
|
33
33
|
};
|
|
34
34
|
|
|
35
|
-
//#endregion
|
|
36
|
-
//#region src/dev-asserted-auth.ts
|
|
37
|
-
const DEV_ASSERTED_EMAIL_HEADER = `x-electric-asserted-email`;
|
|
38
|
-
const DEV_ASSERTED_NAME_HEADER = `x-electric-asserted-name`;
|
|
39
|
-
function clean(value) {
|
|
40
|
-
const trimmed = value?.trim();
|
|
41
|
-
return trimmed || void 0;
|
|
42
|
-
}
|
|
43
|
-
function createDevAssertedAuthenticateRequest(options) {
|
|
44
|
-
if (!options.enabled) return void 0;
|
|
45
|
-
return (request) => {
|
|
46
|
-
const email = clean(request.headers.get(DEV_ASSERTED_EMAIL_HEADER)) ?? clean(options.defaultEmail);
|
|
47
|
-
const name = clean(request.headers.get(DEV_ASSERTED_NAME_HEADER)) ?? clean(options.defaultName);
|
|
48
|
-
const userId = email ?? name;
|
|
49
|
-
if (!userId) return null;
|
|
50
|
-
return {
|
|
51
|
-
userId,
|
|
52
|
-
email,
|
|
53
|
-
name
|
|
54
|
-
};
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
function devAssertedAuthOptionsFromEnv(env = process.env) {
|
|
58
|
-
return {
|
|
59
|
-
enabled: env.ELECTRIC_AGENTS_DEV_ASSERTED_AUTH === `1`,
|
|
60
|
-
defaultEmail: env.ELECTRIC_ASSERTED_AUTH_EMAIL,
|
|
61
|
-
defaultName: env.ELECTRIC_ASSERTED_AUTH_NAME
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
|
|
65
35
|
//#endregion
|
|
66
36
|
//#region src/db/schema.ts
|
|
67
37
|
var schema_exports = {};
|
|
@@ -106,6 +76,7 @@ const entities = pgTable(`entities`, {
|
|
|
106
76
|
tagsIndex: text(`tags_index`).array().notNull().default(sql`'{}'::text[]`),
|
|
107
77
|
spawnArgs: jsonb(`spawn_args`).default({}),
|
|
108
78
|
parent: text(`parent`),
|
|
79
|
+
createdBy: text(`created_by`),
|
|
109
80
|
typeRevision: integer(`type_revision`),
|
|
110
81
|
inboxSchemas: jsonb(`inbox_schemas`),
|
|
111
82
|
stateSchemas: jsonb(`state_schemas`),
|
|
@@ -116,6 +87,7 @@ const entities = pgTable(`entities`, {
|
|
|
116
87
|
index(`idx_entities_type`).on(table.tenantId, table.type),
|
|
117
88
|
index(`idx_entities_status`).on(table.tenantId, table.status),
|
|
118
89
|
index(`idx_entities_parent`).on(table.tenantId, table.parent),
|
|
90
|
+
index(`idx_entities_created_by`).on(table.tenantId, table.createdBy),
|
|
119
91
|
index(`entities_tags_index_gin`).using(`gin`, table.tagsIndex),
|
|
120
92
|
check(`chk_entities_status`, sql`${table.status} IN ('spawning', 'running', 'idle', 'stopped')`)
|
|
121
93
|
]);
|
|
@@ -491,6 +463,7 @@ function toPublicEntity(entity) {
|
|
|
491
463
|
tags: entity.tags,
|
|
492
464
|
spawn_args: entity.spawn_args,
|
|
493
465
|
parent: entity.parent,
|
|
466
|
+
created_by: entity.created_by,
|
|
494
467
|
created_at: entity.created_at,
|
|
495
468
|
updated_at: entity.updated_at
|
|
496
469
|
};
|
|
@@ -1672,6 +1645,103 @@ async function proxyElectric(request, ctx) {
|
|
|
1672
1645
|
});
|
|
1673
1646
|
}
|
|
1674
1647
|
|
|
1648
|
+
//#endregion
|
|
1649
|
+
//#region src/principal.ts
|
|
1650
|
+
const ELECTRIC_PRINCIPAL_HEADER = `electric-principal`;
|
|
1651
|
+
const PRINCIPAL_KINDS = new Set([
|
|
1652
|
+
`user`,
|
|
1653
|
+
`agent`,
|
|
1654
|
+
`service`,
|
|
1655
|
+
`system`
|
|
1656
|
+
]);
|
|
1657
|
+
function parsePrincipalKey(input) {
|
|
1658
|
+
const colon = input.indexOf(`:`);
|
|
1659
|
+
if (colon <= 0) throw new Error(`Invalid principal key`);
|
|
1660
|
+
const kind = input.slice(0, colon);
|
|
1661
|
+
const id = input.slice(colon + 1);
|
|
1662
|
+
if (!PRINCIPAL_KINDS.has(kind)) throw new Error(`Invalid principal kind`);
|
|
1663
|
+
if (!id || id.includes(`/`)) throw new Error(`Invalid principal id`);
|
|
1664
|
+
const key = `${kind}:${id}`;
|
|
1665
|
+
return {
|
|
1666
|
+
kind,
|
|
1667
|
+
id,
|
|
1668
|
+
key,
|
|
1669
|
+
url: `/principal/${encodeURIComponent(key)}`
|
|
1670
|
+
};
|
|
1671
|
+
}
|
|
1672
|
+
function principalUrl(key) {
|
|
1673
|
+
return parsePrincipalKey(key).url;
|
|
1674
|
+
}
|
|
1675
|
+
function principalKeyFromUrl(url) {
|
|
1676
|
+
if (!url.startsWith(`/principal/`)) return null;
|
|
1677
|
+
const segment = url.slice(`/principal/`.length);
|
|
1678
|
+
if (!segment || segment.includes(`/`)) return null;
|
|
1679
|
+
try {
|
|
1680
|
+
const key = decodeURIComponent(segment);
|
|
1681
|
+
return parsePrincipalKey(key).key;
|
|
1682
|
+
} catch {
|
|
1683
|
+
return null;
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
function getPrincipalFromRequest(request) {
|
|
1687
|
+
const value = request.headers.get(ELECTRIC_PRINCIPAL_HEADER);
|
|
1688
|
+
return value ? parsePrincipalKey(value) : null;
|
|
1689
|
+
}
|
|
1690
|
+
function getDevPrincipal() {
|
|
1691
|
+
return parsePrincipalKey(`system:dev-local`);
|
|
1692
|
+
}
|
|
1693
|
+
const BUILT_IN_SYSTEM_PRINCIPAL_IDS = new Set([
|
|
1694
|
+
`framework`,
|
|
1695
|
+
`auth-sync`,
|
|
1696
|
+
`dev-local`
|
|
1697
|
+
]);
|
|
1698
|
+
function isBuiltInSystemPrincipalUrl(url) {
|
|
1699
|
+
if (!url?.startsWith(`/principal/`)) return false;
|
|
1700
|
+
try {
|
|
1701
|
+
const key = principalKeyFromUrl(url);
|
|
1702
|
+
if (!key) return false;
|
|
1703
|
+
const principal = parsePrincipalKey(key);
|
|
1704
|
+
return principal.kind === `system` && BUILT_IN_SYSTEM_PRINCIPAL_IDS.has(principal.id);
|
|
1705
|
+
} catch {
|
|
1706
|
+
return false;
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
function principalFromCreatedBy(createdBy) {
|
|
1710
|
+
if (!createdBy) return void 0;
|
|
1711
|
+
const key = principalKeyFromUrl(createdBy);
|
|
1712
|
+
if (!key) return {
|
|
1713
|
+
url: createdBy,
|
|
1714
|
+
key: null
|
|
1715
|
+
};
|
|
1716
|
+
const principal = parsePrincipalKey(key);
|
|
1717
|
+
return {
|
|
1718
|
+
url: principal.url,
|
|
1719
|
+
key: principal.key,
|
|
1720
|
+
kind: principal.kind,
|
|
1721
|
+
id: principal.id
|
|
1722
|
+
};
|
|
1723
|
+
}
|
|
1724
|
+
const principalIdentityStateSchema = Type.Object({
|
|
1725
|
+
kind: Type.Union([
|
|
1726
|
+
Type.Literal(`user`),
|
|
1727
|
+
Type.Literal(`agent`),
|
|
1728
|
+
Type.Literal(`service`),
|
|
1729
|
+
Type.Literal(`system`)
|
|
1730
|
+
]),
|
|
1731
|
+
id: Type.String(),
|
|
1732
|
+
key: Type.String(),
|
|
1733
|
+
url: Type.String(),
|
|
1734
|
+
updated_at: Type.String(),
|
|
1735
|
+
display_name: Type.Optional(Type.String()),
|
|
1736
|
+
email: Type.Optional(Type.String()),
|
|
1737
|
+
avatar_url: Type.Optional(Type.String()),
|
|
1738
|
+
auth_provider: Type.Optional(Type.String()),
|
|
1739
|
+
auth_subject: Type.Optional(Type.String()),
|
|
1740
|
+
claims: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
|
|
1741
|
+
created_at: Type.Optional(Type.String())
|
|
1742
|
+
}, { additionalProperties: false });
|
|
1743
|
+
const principalUpdateIdentityMessageSchema = Type.Object({ identity: principalIdentityStateSchema }, { additionalProperties: false });
|
|
1744
|
+
|
|
1675
1745
|
//#endregion
|
|
1676
1746
|
//#region src/dispatch-policy-schema.ts
|
|
1677
1747
|
const nonEmptyStringSchema = Type.String({ minLength: 1 });
|
|
@@ -1913,6 +1983,24 @@ var PostgresRegistry = class {
|
|
|
1913
1983
|
}
|
|
1914
1984
|
});
|
|
1915
1985
|
}
|
|
1986
|
+
async ensureEntityType(et) {
|
|
1987
|
+
const existing = await this.getEntityType(et.name);
|
|
1988
|
+
if (existing) return existing;
|
|
1989
|
+
await this.db.insert(entityTypes).values({
|
|
1990
|
+
tenantId: this.tenantId,
|
|
1991
|
+
name: et.name,
|
|
1992
|
+
description: et.description,
|
|
1993
|
+
creationSchema: et.creation_schema ?? null,
|
|
1994
|
+
inboxSchemas: et.inbox_schemas ?? null,
|
|
1995
|
+
stateSchemas: et.state_schemas ?? null,
|
|
1996
|
+
serveEndpoint: et.serve_endpoint ?? null,
|
|
1997
|
+
defaultDispatchPolicy: et.default_dispatch_policy ?? null,
|
|
1998
|
+
revision: et.revision,
|
|
1999
|
+
createdAt: et.created_at,
|
|
2000
|
+
updatedAt: et.updated_at
|
|
2001
|
+
}).onConflictDoNothing();
|
|
2002
|
+
return await this.getEntityType(et.name);
|
|
2003
|
+
}
|
|
1916
2004
|
async getEntityType(name) {
|
|
1917
2005
|
const rows = await this.db.select().from(entityTypes).where(this.entityTypeWhere(name)).limit(1);
|
|
1918
2006
|
if (rows.length === 0) return null;
|
|
@@ -1952,6 +2040,7 @@ var PostgresRegistry = class {
|
|
|
1952
2040
|
tagsIndex: buildTagsIndex(entity.tags),
|
|
1953
2041
|
spawnArgs: entity.spawn_args ?? {},
|
|
1954
2042
|
parent: entity.parent ?? null,
|
|
2043
|
+
createdBy: entity.created_by ?? null,
|
|
1955
2044
|
typeRevision: entity.type_revision ?? null,
|
|
1956
2045
|
inboxSchemas: entity.inbox_schemas ?? null,
|
|
1957
2046
|
stateSchemas: entity.state_schemas ?? null,
|
|
@@ -1997,6 +2086,7 @@ var PostgresRegistry = class {
|
|
|
1997
2086
|
if (filter?.type) conditions.push(eq(entities.type, filter.type));
|
|
1998
2087
|
if (filter?.status) conditions.push(eq(entities.status, filter.status));
|
|
1999
2088
|
if (filter?.parent) conditions.push(eq(entities.parent, filter.parent));
|
|
2089
|
+
if (filter?.created_by) conditions.push(eq(entities.createdBy, filter.created_by));
|
|
2000
2090
|
const whereClause = and(...conditions);
|
|
2001
2091
|
const countResult = await this.db.select({ count: sql`count(*)` }).from(entities).where(whereClause);
|
|
2002
2092
|
const total = Number(countResult[0].count);
|
|
@@ -2283,6 +2373,7 @@ var PostgresRegistry = class {
|
|
|
2283
2373
|
tags: row.tags ?? {},
|
|
2284
2374
|
spawn_args: row.spawnArgs,
|
|
2285
2375
|
parent: row.parent ?? void 0,
|
|
2376
|
+
created_by: row.createdBy ?? void 0,
|
|
2286
2377
|
type_revision: row.typeRevision ?? void 0,
|
|
2287
2378
|
inbox_schemas: row.inboxSchemas,
|
|
2288
2379
|
state_schemas: row.stateSchemas,
|
|
@@ -2430,25 +2521,11 @@ function buildManifestWakeRegistration(subscriberUrl, manifest, manifestKey) {
|
|
|
2430
2521
|
|
|
2431
2522
|
//#endregion
|
|
2432
2523
|
//#region src/entity-manager.ts
|
|
2524
|
+
function createInitialQueuePosition(date) {
|
|
2525
|
+
return `${String(date.getTime()).padStart(16, `0`)}:a0`;
|
|
2526
|
+
}
|
|
2433
2527
|
const DEFAULT_FORK_WAIT_TIMEOUT_MS = 12e4;
|
|
2434
2528
|
const DEFAULT_FORK_WAIT_POLL_MS = 250;
|
|
2435
|
-
function applyTypeDefaultSubscriptionScope$1(policy, typeDefault) {
|
|
2436
|
-
const target = policy.targets[0];
|
|
2437
|
-
const defaultTarget = typeDefault?.targets[0];
|
|
2438
|
-
if (!target || !defaultTarget?.subscription_id) return policy;
|
|
2439
|
-
if (!sameDispatchDestination$1(target, defaultTarget)) return policy;
|
|
2440
|
-
if (target.subscription_id === defaultTarget.subscription_id) return policy;
|
|
2441
|
-
return { targets: [{
|
|
2442
|
-
...target,
|
|
2443
|
-
subscription_id: defaultTarget.subscription_id
|
|
2444
|
-
}] };
|
|
2445
|
-
}
|
|
2446
|
-
function sameDispatchDestination$1(a, b) {
|
|
2447
|
-
if (a.type !== b.type) return false;
|
|
2448
|
-
if (a.type === `runner` && b.type === `runner`) return a.runnerId === b.runnerId;
|
|
2449
|
-
if (a.type === `webhook` && b.type === `webhook`) return a.url === b.url;
|
|
2450
|
-
return false;
|
|
2451
|
-
}
|
|
2452
2529
|
function sleep(ms) {
|
|
2453
2530
|
return new Promise((resolve$1) => setTimeout(resolve$1, ms));
|
|
2454
2531
|
}
|
|
@@ -2519,6 +2596,7 @@ var EntityManager = class {
|
|
|
2519
2596
|
}
|
|
2520
2597
|
async registerEntityType(req) {
|
|
2521
2598
|
if (!req.name) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Missing required field: name`, 400);
|
|
2599
|
+
if (req.name === `principal`) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Entity type "principal" is built in and cannot be registered or updated`, 400);
|
|
2522
2600
|
if (req.name.startsWith(`_`)) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Entity type names starting with "_" are reserved`, 400);
|
|
2523
2601
|
if (!req.description) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Missing required field: description`, 400);
|
|
2524
2602
|
this.validateSchema(req.creation_schema);
|
|
@@ -2545,10 +2623,63 @@ var EntityManager = class {
|
|
|
2545
2623
|
return stored;
|
|
2546
2624
|
}
|
|
2547
2625
|
async deleteEntityType(name) {
|
|
2626
|
+
if (name === `principal`) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Entity type "principal" is built in and cannot be deleted`, 400);
|
|
2548
2627
|
const existing = await this.registry.getEntityType(name);
|
|
2549
2628
|
if (!existing) throw new ElectricAgentsError(ErrCodeNotFound, `Entity type "${name}" not found`, 404);
|
|
2550
2629
|
await this.registry.deleteEntityType(name);
|
|
2551
2630
|
}
|
|
2631
|
+
async ensurePrincipalEntityType() {
|
|
2632
|
+
const now = new Date().toISOString();
|
|
2633
|
+
return await this.registry.ensureEntityType({
|
|
2634
|
+
name: `principal`,
|
|
2635
|
+
description: `built-in principal entity`,
|
|
2636
|
+
inbox_schemas: { update_identity: principalUpdateIdentityMessageSchema },
|
|
2637
|
+
state_schemas: { identity: principalIdentityStateSchema },
|
|
2638
|
+
revision: 1,
|
|
2639
|
+
created_at: now,
|
|
2640
|
+
updated_at: now
|
|
2641
|
+
});
|
|
2642
|
+
}
|
|
2643
|
+
async ensurePrincipal(principal) {
|
|
2644
|
+
const existing = await this.registry.getEntity(principal.url);
|
|
2645
|
+
if (existing) return existing;
|
|
2646
|
+
await this.ensurePrincipalEntityType();
|
|
2647
|
+
try {
|
|
2648
|
+
const entity = await this.spawn(`principal`, {
|
|
2649
|
+
instance_id: principal.key,
|
|
2650
|
+
args: {
|
|
2651
|
+
kind: principal.kind,
|
|
2652
|
+
id: principal.id,
|
|
2653
|
+
key: principal.key
|
|
2654
|
+
},
|
|
2655
|
+
tags: {
|
|
2656
|
+
principal_kind: principal.kind,
|
|
2657
|
+
principal_id: principal.id
|
|
2658
|
+
},
|
|
2659
|
+
created_by: principal.url
|
|
2660
|
+
});
|
|
2661
|
+
const now = new Date().toISOString();
|
|
2662
|
+
await this.streamClient.append(entity.streams.main, this.encodeChangeEvent({
|
|
2663
|
+
type: `identity`,
|
|
2664
|
+
key: `self`,
|
|
2665
|
+
value: {
|
|
2666
|
+
kind: principal.kind,
|
|
2667
|
+
id: principal.id,
|
|
2668
|
+
key: principal.key,
|
|
2669
|
+
url: principal.url,
|
|
2670
|
+
created_at: now,
|
|
2671
|
+
updated_at: now
|
|
2672
|
+
}
|
|
2673
|
+
}));
|
|
2674
|
+
return entity;
|
|
2675
|
+
} catch (error) {
|
|
2676
|
+
if (error instanceof ElectricAgentsError && error.code === ErrCodeDuplicateURL) {
|
|
2677
|
+
const raced = await this.registry.getEntity(principal.url);
|
|
2678
|
+
if (raced) return raced;
|
|
2679
|
+
}
|
|
2680
|
+
throw error;
|
|
2681
|
+
}
|
|
2682
|
+
}
|
|
2552
2683
|
/**
|
|
2553
2684
|
* Spawn a new entity of the given type with durable streams.
|
|
2554
2685
|
*/
|
|
@@ -2564,6 +2695,7 @@ var EntityManager = class {
|
|
|
2564
2695
|
});
|
|
2565
2696
|
}
|
|
2566
2697
|
async spawnInner(typeName, req) {
|
|
2698
|
+
if (typeName === `principal` && req.created_by !== principalUrl(req.instance_id)) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Principal entities are built in and can only be materialized by the system`, 400);
|
|
2567
2699
|
if (typeName.startsWith(`_`)) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Entity type names starting with "_" are reserved`, 400);
|
|
2568
2700
|
const entityType = await this.registry.getEntityType(typeName);
|
|
2569
2701
|
if (!entityType) throw new ElectricAgentsError(ErrCodeUnknownEntityType, `Entity type "${typeName}" not found`, 404);
|
|
@@ -2575,7 +2707,7 @@ var EntityManager = class {
|
|
|
2575
2707
|
const instanceId = req.instance_id || randomUUID();
|
|
2576
2708
|
if (instanceId.includes(`/`)) throw new ElectricAgentsError(ErrCodeInvalidRequest, `instance_id must not contain forward slashes`, 400);
|
|
2577
2709
|
const writeToken = randomUUID();
|
|
2578
|
-
const entityURL = `/${typeName}/${instanceId}`;
|
|
2710
|
+
const entityURL = typeName === `principal` ? principalUrl(instanceId) : `/${typeName}/${instanceId}`;
|
|
2579
2711
|
const mainPath = `${entityURL}/main`;
|
|
2580
2712
|
const errorPath = `${entityURL}/error`;
|
|
2581
2713
|
const subscriptionId = `${typeName}-handler`;
|
|
@@ -2587,7 +2719,7 @@ var EntityManager = class {
|
|
|
2587
2719
|
parentEntity = await this.registry.getEntity(req.parent);
|
|
2588
2720
|
if (!parentEntity) throw new ElectricAgentsError(ErrCodeNotFound, `Parent entity "${req.parent}" not found`, 404);
|
|
2589
2721
|
}
|
|
2590
|
-
const dispatchPolicy = req.dispatch_policy ? this.validateDispatchPolicy(req.dispatch_policy, { label: `dispatch_policy` }) : parentEntity?.dispatch_policy ? applyTypeDefaultSubscriptionScope
|
|
2722
|
+
const dispatchPolicy = req.dispatch_policy ? this.validateDispatchPolicy(req.dispatch_policy, { label: `dispatch_policy` }) : parentEntity?.dispatch_policy ? applyTypeDefaultSubscriptionScope(parentEntity.dispatch_policy, entityType.default_dispatch_policy) : entityType.default_dispatch_policy;
|
|
2591
2723
|
const now = Date.now();
|
|
2592
2724
|
const entityData = {
|
|
2593
2725
|
type: typeName,
|
|
@@ -2606,6 +2738,7 @@ var EntityManager = class {
|
|
|
2606
2738
|
inbox_schemas: entityType.inbox_schemas,
|
|
2607
2739
|
state_schemas: entityType.state_schemas,
|
|
2608
2740
|
created_at: now,
|
|
2741
|
+
created_by: req.created_by ?? parentEntity?.created_by,
|
|
2609
2742
|
updated_at: now
|
|
2610
2743
|
};
|
|
2611
2744
|
if (req.parent) entityData.parent = req.parent;
|
|
@@ -2635,7 +2768,7 @@ var EntityManager = class {
|
|
|
2635
2768
|
const inboxEvent = entityStateSchema.inbox.insert({
|
|
2636
2769
|
key: `msg-in-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
2637
2770
|
value: {
|
|
2638
|
-
from: req.parent ?? `spawn`,
|
|
2771
|
+
from: req.created_by ?? req.parent ?? `spawn`,
|
|
2639
2772
|
payload: req.initialMessage,
|
|
2640
2773
|
timestamp: msgNow
|
|
2641
2774
|
}
|
|
@@ -3178,10 +3311,10 @@ var EntityManager = class {
|
|
|
3178
3311
|
changed = true;
|
|
3179
3312
|
}
|
|
3180
3313
|
}
|
|
3181
|
-
if (typeof next.
|
|
3182
|
-
const
|
|
3183
|
-
if (
|
|
3184
|
-
next.
|
|
3314
|
+
if (typeof next.senderUrl === `string`) {
|
|
3315
|
+
const forkSender = entityUrlMap.get(next.senderUrl);
|
|
3316
|
+
if (forkSender) {
|
|
3317
|
+
next.senderUrl = forkSender;
|
|
3185
3318
|
changed = true;
|
|
3186
3319
|
}
|
|
3187
3320
|
}
|
|
@@ -3247,6 +3380,7 @@ var EntityManager = class {
|
|
|
3247
3380
|
const fireAtRaw = manifest.fireAt;
|
|
3248
3381
|
const producerId = manifest.producerId;
|
|
3249
3382
|
const targetUrl = manifest.targetUrl;
|
|
3383
|
+
const senderUrl = typeof manifest.senderUrl === `string` ? manifest.senderUrl : ownerEntityUrl;
|
|
3250
3384
|
if (typeof fireAtRaw !== `string` || typeof producerId !== `string` || typeof targetUrl !== `string`) {
|
|
3251
3385
|
serverLog.warn(`[agent-server] invalid forked future_send manifest entry for ${ownerEntityUrl}/${manifestKey}`);
|
|
3252
3386
|
return;
|
|
@@ -3258,7 +3392,7 @@ var EntityManager = class {
|
|
|
3258
3392
|
}
|
|
3259
3393
|
await this.scheduler.syncManifestDelayedSend(ownerEntityUrl, manifestKey, {
|
|
3260
3394
|
entityUrl: targetUrl,
|
|
3261
|
-
from:
|
|
3395
|
+
from: senderUrl,
|
|
3262
3396
|
payload: manifest.payload,
|
|
3263
3397
|
key: `scheduled-${producerId}`,
|
|
3264
3398
|
type: typeof manifest.messageType === `string` ? manifest.messageType : void 0,
|
|
@@ -3272,6 +3406,7 @@ var EntityManager = class {
|
|
|
3272
3406
|
kind: `schedule`,
|
|
3273
3407
|
scheduleType: `future_send`,
|
|
3274
3408
|
targetUrl,
|
|
3409
|
+
senderUrl,
|
|
3275
3410
|
fireAt: fireAt.toISOString(),
|
|
3276
3411
|
producerId,
|
|
3277
3412
|
status: `pending`
|
|
@@ -3299,9 +3434,14 @@ var EntityManager = class {
|
|
|
3299
3434
|
const value = {
|
|
3300
3435
|
from: req.from,
|
|
3301
3436
|
payload: req.payload,
|
|
3302
|
-
timestamp: now
|
|
3437
|
+
timestamp: now,
|
|
3438
|
+
mode: req.mode ?? `immediate`,
|
|
3439
|
+
status: req.mode === `queued` || req.mode === `paused` ? `pending` : `processed`
|
|
3303
3440
|
};
|
|
3304
3441
|
if (req.type) value.message_type = req.type;
|
|
3442
|
+
if (req.position) value.position = req.position;
|
|
3443
|
+
else if (value.mode === `queued` || value.mode === `paused`) value.position = createInitialQueuePosition(new Date(now));
|
|
3444
|
+
if (value.status === `processed`) value.processed_at = now;
|
|
3305
3445
|
const envelope = entityStateSchema.inbox.insert({
|
|
3306
3446
|
key,
|
|
3307
3447
|
value
|
|
@@ -3313,11 +3453,47 @@ var EntityManager = class {
|
|
|
3313
3453
|
return;
|
|
3314
3454
|
}
|
|
3315
3455
|
await this.streamClient.append(entity.streams.main, encoded);
|
|
3456
|
+
if (entity.type === `principal` && req.type === `update_identity`) {
|
|
3457
|
+
const identity = req.payload?.identity;
|
|
3458
|
+
await this.streamClient.append(entity.streams.main, this.encodeChangeEvent({
|
|
3459
|
+
type: `identity`,
|
|
3460
|
+
key: `self`,
|
|
3461
|
+
value: identity
|
|
3462
|
+
}));
|
|
3463
|
+
}
|
|
3316
3464
|
} catch (err) {
|
|
3317
3465
|
if (this.isClosedStreamError(err)) throw new ElectricAgentsError(ErrCodeNotRunning, `Entity is stopped`, 409);
|
|
3318
3466
|
throw err;
|
|
3319
3467
|
}
|
|
3320
3468
|
}
|
|
3469
|
+
async updateInboxMessage(entityUrl, key, req) {
|
|
3470
|
+
const entity = await this.registry.getEntity(entityUrl);
|
|
3471
|
+
if (!entity) throw new ElectricAgentsError(ErrCodeNotFound, `Entity not found`, 404);
|
|
3472
|
+
if (entity.status === `stopped`) throw new ElectricAgentsError(ErrCodeNotRunning, `Entity is stopped`, 409);
|
|
3473
|
+
const now = new Date().toISOString();
|
|
3474
|
+
const value = {};
|
|
3475
|
+
if (`payload` in req) value.payload = req.payload;
|
|
3476
|
+
if (req.position !== void 0) value.position = req.position;
|
|
3477
|
+
if (req.mode !== void 0) value.mode = req.mode;
|
|
3478
|
+
if (req.status !== void 0) {
|
|
3479
|
+
value.status = req.status;
|
|
3480
|
+
if (req.status === `processed`) value.processed_at = now;
|
|
3481
|
+
if (req.status === `cancelled`) value.cancelled_at = now;
|
|
3482
|
+
}
|
|
3483
|
+
if (Object.keys(value).length === 0) throw new ElectricAgentsError(ErrCodeInvalidRequest, `No inbox fields to update`, 400);
|
|
3484
|
+
const envelope = entityStateSchema.inbox.update({
|
|
3485
|
+
key,
|
|
3486
|
+
value
|
|
3487
|
+
});
|
|
3488
|
+
await this.streamClient.append(entity.streams.main, this.encodeChangeEvent(envelope));
|
|
3489
|
+
}
|
|
3490
|
+
async deleteInboxMessage(entityUrl, key) {
|
|
3491
|
+
const entity = await this.registry.getEntity(entityUrl);
|
|
3492
|
+
if (!entity) throw new ElectricAgentsError(ErrCodeNotFound, `Entity not found`, 404);
|
|
3493
|
+
if (entity.status === `stopped`) throw new ElectricAgentsError(ErrCodeNotRunning, `Entity is stopped`, 409);
|
|
3494
|
+
const envelope = entityStateSchema.inbox.delete({ key });
|
|
3495
|
+
await this.streamClient.append(entity.streams.main, this.encodeChangeEvent(envelope));
|
|
3496
|
+
}
|
|
3321
3497
|
async setTag(entityUrl, key, req, token) {
|
|
3322
3498
|
const entity = await this.registry.getEntity(entityUrl);
|
|
3323
3499
|
if (!entity) throw new ElectricAgentsError(ErrCodeNotFound, `Entity not found`, 404);
|
|
@@ -3403,7 +3579,7 @@ var EntityManager = class {
|
|
|
3403
3579
|
async upsertFutureSendSchedule(ownerEntityUrl, req) {
|
|
3404
3580
|
if (!this.scheduler) throw new Error(`Scheduler not configured`);
|
|
3405
3581
|
const targetUrl = req.targetUrl ?? ownerEntityUrl;
|
|
3406
|
-
const from = req.
|
|
3582
|
+
const from = req.senderUrl ?? ownerEntityUrl;
|
|
3407
3583
|
const fireAt = new Date(req.fireAt);
|
|
3408
3584
|
if (Number.isNaN(fireAt.getTime())) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Invalid fireAt timestamp: ${req.fireAt}`, 400);
|
|
3409
3585
|
await this.validateSendRequest(targetUrl, {
|
|
@@ -3431,9 +3607,9 @@ var EntityManager = class {
|
|
|
3431
3607
|
scheduleType: `future_send`,
|
|
3432
3608
|
fireAt: fireAt.toISOString(),
|
|
3433
3609
|
targetUrl,
|
|
3610
|
+
senderUrl: from,
|
|
3434
3611
|
payload: req.payload,
|
|
3435
3612
|
producerId,
|
|
3436
|
-
...req.from ? { from: req.from } : {},
|
|
3437
3613
|
...req.messageType ? { messageType: req.messageType } : {},
|
|
3438
3614
|
status: `pending`
|
|
3439
3615
|
}
|
|
@@ -3447,9 +3623,9 @@ var EntityManager = class {
|
|
|
3447
3623
|
scheduleType: `future_send`,
|
|
3448
3624
|
fireAt: fireAt.toISOString(),
|
|
3449
3625
|
targetUrl,
|
|
3626
|
+
senderUrl: from,
|
|
3450
3627
|
payload: req.payload,
|
|
3451
3628
|
producerId,
|
|
3452
|
-
...req.from ? { from: req.from } : {},
|
|
3453
3629
|
...req.messageType ? { messageType: req.messageType } : {},
|
|
3454
3630
|
status: `pending`
|
|
3455
3631
|
}, { txid });
|
|
@@ -3487,7 +3663,9 @@ var EntityManager = class {
|
|
|
3487
3663
|
from: req.from,
|
|
3488
3664
|
payload: req.payload,
|
|
3489
3665
|
key: req.key,
|
|
3490
|
-
type: req.type
|
|
3666
|
+
type: req.type,
|
|
3667
|
+
mode: req.mode,
|
|
3668
|
+
position: req.position
|
|
3491
3669
|
}, fireAt);
|
|
3492
3670
|
}
|
|
3493
3671
|
/**
|
|
@@ -3637,6 +3815,7 @@ var EntityManager = class {
|
|
|
3637
3815
|
* Add new input/output schema keys to an entity type directly in Postgres.
|
|
3638
3816
|
*/
|
|
3639
3817
|
async amendSchemas(typeName, schemas) {
|
|
3818
|
+
if (typeName === `principal`) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Entity type "principal" is built in and cannot be amended`, 400);
|
|
3640
3819
|
this.validateSchemaMap(schemas.inbox_schemas);
|
|
3641
3820
|
this.validateSchemaMap(schemas.state_schemas);
|
|
3642
3821
|
const existing = await this.registry.getEntityType(typeName);
|
|
@@ -3686,9 +3865,11 @@ var EntityManager = class {
|
|
|
3686
3865
|
url: entity.url,
|
|
3687
3866
|
streams: entity.streams,
|
|
3688
3867
|
tags: entity.tags,
|
|
3689
|
-
spawnArgs: entity.spawn_args
|
|
3868
|
+
spawnArgs: entity.spawn_args,
|
|
3869
|
+
createdBy: entity.created_by
|
|
3690
3870
|
},
|
|
3691
|
-
|
|
3871
|
+
principal: principalFromCreatedBy(entity.created_by),
|
|
3872
|
+
triggerEvent: `inbox`
|
|
3692
3873
|
};
|
|
3693
3874
|
}
|
|
3694
3875
|
validateSchema(schema) {
|
|
@@ -3728,6 +3909,7 @@ var EntityManager = class {
|
|
|
3728
3909
|
}
|
|
3729
3910
|
}
|
|
3730
3911
|
if (!req.from) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Missing required field: from`, 400);
|
|
3912
|
+
if (entity.type === `principal` && req.type === `update_identity` && !isBuiltInSystemPrincipalUrl(req.from)) throw new ElectricAgentsError(ErrCodeUnauthorized, `Only built-in system principals can update principal identity`, 403);
|
|
3731
3913
|
if (req.payload === void 0) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Missing required field: payload`, 400);
|
|
3732
3914
|
return entity;
|
|
3733
3915
|
}
|
|
@@ -3842,8 +4024,7 @@ async function assertDispatchPolicyAllowed(ctx, policy) {
|
|
|
3842
4024
|
if (!target || target.type !== `runner`) return;
|
|
3843
4025
|
const runner = await ctx.entityManager.registry.getRunner(target.runnerId);
|
|
3844
4026
|
if (!runner) throw new ElectricAgentsError(ErrCodeNotFound, `Runner "${target.runnerId}" not found`, 404);
|
|
3845
|
-
if (
|
|
3846
|
-
if (runner.owner_user_id !== ctx.authenticatedUser.userId) throw new ElectricAgentsError(ErrCodeUnauthorized, `Runner dispatch requires the authenticated owner`, 403);
|
|
4027
|
+
if (ctx.principal && runner.owner_user_id !== ctx.principal.key) throw new ElectricAgentsError(ErrCodeUnauthorized, `Runner dispatch requires the authenticated owner`, 403);
|
|
3847
4028
|
}
|
|
3848
4029
|
async function linkEntityDispatchSubscription(ctx, entity) {
|
|
3849
4030
|
const dispatchPolicy = await resolveEffectiveDispatchPolicyForEntity(ctx, entity);
|
|
@@ -3856,7 +4037,12 @@ async function unlinkEntityDispatchSubscription(ctx, entity) {
|
|
|
3856
4037
|
const target = dispatchPolicy?.targets[0];
|
|
3857
4038
|
if (!target) return;
|
|
3858
4039
|
const subscriptionId = subscriptionIdForEntityDispatchTarget(target, entity.url);
|
|
3859
|
-
await ctx.streamClient.removeSubscriptionStream(subscriptionId, entity.streams.main).catch(() => {
|
|
4040
|
+
await ctx.streamClient.removeSubscriptionStream(subscriptionId, entity.streams.main).catch((err) => {
|
|
4041
|
+
serverLog.warn(`[dispatch-policy] failed to remove stream from subscription`, {
|
|
4042
|
+
subscriptionId,
|
|
4043
|
+
stream: entity.streams.main
|
|
4044
|
+
}, err);
|
|
4045
|
+
});
|
|
3860
4046
|
}
|
|
3861
4047
|
async function linkStreamToTargetSubscription(ctx, target, entity) {
|
|
3862
4048
|
const streamPath = entity.streams.main;
|
|
@@ -3931,11 +4117,33 @@ const spawnBodySchema = Type.Object({
|
|
|
3931
4117
|
}))
|
|
3932
4118
|
});
|
|
3933
4119
|
const sendBodySchema = Type.Object({
|
|
3934
|
-
from: Type.Optional(Type.String()),
|
|
3935
4120
|
payload: Type.Optional(Type.Unknown()),
|
|
3936
4121
|
key: Type.Optional(Type.String()),
|
|
3937
4122
|
type: Type.Optional(Type.String()),
|
|
3938
|
-
|
|
4123
|
+
mode: Type.Optional(Type.Union([
|
|
4124
|
+
Type.Literal(`immediate`),
|
|
4125
|
+
Type.Literal(`queued`),
|
|
4126
|
+
Type.Literal(`paused`),
|
|
4127
|
+
Type.Literal(`steer`)
|
|
4128
|
+
])),
|
|
4129
|
+
position: Type.Optional(Type.String()),
|
|
4130
|
+
afterMs: Type.Optional(Type.Number()),
|
|
4131
|
+
from: Type.Optional(Type.String())
|
|
4132
|
+
});
|
|
4133
|
+
const inboxMessageBodySchema = Type.Object({
|
|
4134
|
+
payload: Type.Optional(Type.Unknown()),
|
|
4135
|
+
position: Type.Optional(Type.String()),
|
|
4136
|
+
mode: Type.Optional(Type.Union([
|
|
4137
|
+
Type.Literal(`immediate`),
|
|
4138
|
+
Type.Literal(`queued`),
|
|
4139
|
+
Type.Literal(`paused`),
|
|
4140
|
+
Type.Literal(`steer`)
|
|
4141
|
+
])),
|
|
4142
|
+
status: Type.Optional(Type.Union([
|
|
4143
|
+
Type.Literal(`pending`),
|
|
4144
|
+
Type.Literal(`processed`),
|
|
4145
|
+
Type.Literal(`cancelled`)
|
|
4146
|
+
]))
|
|
3939
4147
|
});
|
|
3940
4148
|
const forkBodySchema = Type.Object({
|
|
3941
4149
|
instance_id: Type.Optional(Type.String()),
|
|
@@ -3954,8 +4162,8 @@ const scheduleBodySchema = Type.Union([Type.Object({
|
|
|
3954
4162
|
payload: Type.Unknown(),
|
|
3955
4163
|
targetUrl: Type.Optional(Type.String()),
|
|
3956
4164
|
fireAt: Type.String(),
|
|
3957
|
-
|
|
3958
|
-
|
|
4165
|
+
messageType: Type.Optional(Type.String()),
|
|
4166
|
+
from: Type.Optional(Type.String())
|
|
3959
4167
|
})]);
|
|
3960
4168
|
const entitiesRegisterBodySchema = Type.Object({ tags: Type.Optional(stringRecordSchema) });
|
|
3961
4169
|
const entitiesRouter = Router({ base: `/_electric/entities` });
|
|
@@ -3966,6 +4174,8 @@ entitiesRouter.get(`/:type/:instanceId`, withExistingEntity, getEntity);
|
|
|
3966
4174
|
entitiesRouter.head(`/:type/:instanceId`, withExistingEntity, headEntity);
|
|
3967
4175
|
entitiesRouter.delete(`/:type/:instanceId`, withExistingEntity, killEntity);
|
|
3968
4176
|
entitiesRouter.post(`/:type/:instanceId/send`, withExistingEntity, withSchema(sendBodySchema), sendEntity);
|
|
4177
|
+
entitiesRouter.patch(`/:type/:instanceId/inbox/:messageKey`, withExistingEntity, withSchema(inboxMessageBodySchema), updateInboxMessage);
|
|
4178
|
+
entitiesRouter.delete(`/:type/:instanceId/inbox/:messageKey`, withExistingEntity, deleteInboxMessage);
|
|
3969
4179
|
entitiesRouter.post(`/:type/:instanceId/fork`, withExistingEntity, withSchema(forkBodySchema), forkEntity);
|
|
3970
4180
|
entitiesRouter.post(`/:type/:instanceId/tags/:tagKey`, withExistingEntity, withSchema(setTagBodySchema), setTag);
|
|
3971
4181
|
entitiesRouter.delete(`/:type/:instanceId/tags/:tagKey`, withExistingEntity, removeTag);
|
|
@@ -3974,6 +4184,11 @@ entitiesRouter.delete(`/:type/:instanceId/schedules/:scheduleId`, withExistingEn
|
|
|
3974
4184
|
function entityUrlFromSegments(type, instanceId) {
|
|
3975
4185
|
if (!type || !instanceId) return null;
|
|
3976
4186
|
if (type.startsWith(`_`) || type.includes(`*`) || instanceId.includes(`*`)) return null;
|
|
4187
|
+
if (type === `principal`) try {
|
|
4188
|
+
return principalUrl(decodeURIComponent(instanceId));
|
|
4189
|
+
} catch {
|
|
4190
|
+
return null;
|
|
4191
|
+
}
|
|
3977
4192
|
return `/${type}/${instanceId}`;
|
|
3978
4193
|
}
|
|
3979
4194
|
function firstQueryValue$1(value) {
|
|
@@ -3983,12 +4198,27 @@ function requireExistingEntityRoute(request) {
|
|
|
3983
4198
|
if (!request.entityRoute) throw new Error(`existing entity middleware did not run`);
|
|
3984
4199
|
return request.entityRoute;
|
|
3985
4200
|
}
|
|
4201
|
+
function rejectPrincipalEntityMutation(request, action) {
|
|
4202
|
+
const { entity } = requireExistingEntityRoute(request);
|
|
4203
|
+
if (entity.type !== `principal`) return void 0;
|
|
4204
|
+
return apiError(400, ErrCodeInvalidRequest, `Principal entities are built in and cannot be ${action}`);
|
|
4205
|
+
}
|
|
3986
4206
|
async function withExistingEntity(request, ctx) {
|
|
3987
4207
|
const entityUrl = entityUrlFromSegments(request.params.type, request.params.instanceId);
|
|
3988
4208
|
if (!entityUrl) return void 0;
|
|
3989
4209
|
const entity = await ctx.entityManager.registry.getEntity(entityUrl);
|
|
3990
4210
|
if (!entity) {
|
|
3991
4211
|
const entityType = await ctx.entityManager.registry.getEntityType(request.params.type);
|
|
4212
|
+
if (request.params.type === `principal`) try {
|
|
4213
|
+
const materialized = await ctx.entityManager.ensurePrincipal(parsePrincipalKey(decodeURIComponent(request.params.instanceId)));
|
|
4214
|
+
request.entityRoute = {
|
|
4215
|
+
entityUrl,
|
|
4216
|
+
entity: materialized
|
|
4217
|
+
};
|
|
4218
|
+
return void 0;
|
|
4219
|
+
} catch (error) {
|
|
4220
|
+
return apiError(400, ErrCodeInvalidRequest, error instanceof Error ? error.message : `Invalid principal`);
|
|
4221
|
+
}
|
|
3992
4222
|
if (entityType) return apiError(404, ErrCodeNotFound, `Entity not found at ${entityUrl}`);
|
|
3993
4223
|
return apiError(404, ErrCodeUnknownEntityType, `Entity type "${request.params.type}" not found`);
|
|
3994
4224
|
}
|
|
@@ -4000,6 +4230,7 @@ async function withExistingEntity(request, ctx) {
|
|
|
4000
4230
|
}
|
|
4001
4231
|
async function withSpawnableEntityType(request, ctx) {
|
|
4002
4232
|
if (!entityUrlFromSegments(request.params.type, request.params.instanceId)) return void 0;
|
|
4233
|
+
if (request.params.type === `principal`) return apiError(400, ErrCodeInvalidRequest, `Principal entities are built in and cannot be spawned directly`);
|
|
4003
4234
|
const entityType = await ctx.entityManager.registry.getEntityType(request.params.type);
|
|
4004
4235
|
if (!entityType) return apiError(404, ErrCodeUnknownEntityType, `Entity type "${request.params.type}" not found`);
|
|
4005
4236
|
return void 0;
|
|
@@ -4008,7 +4239,8 @@ async function listEntities({ query }, ctx) {
|
|
|
4008
4239
|
const { entities: entities$1 } = await ctx.entityManager.registry.listEntities({
|
|
4009
4240
|
type: firstQueryValue$1(query.type),
|
|
4010
4241
|
status: firstQueryValue$1(query.status),
|
|
4011
|
-
parent: firstQueryValue$1(query.parent)
|
|
4242
|
+
parent: firstQueryValue$1(query.parent),
|
|
4243
|
+
created_by: firstQueryValue$1(query.created_by)
|
|
4012
4244
|
});
|
|
4013
4245
|
return json(entities$1.map((entity) => toPublicEntity(entity)));
|
|
4014
4246
|
}
|
|
@@ -4018,6 +4250,8 @@ async function registerEntitiesSource(request, ctx) {
|
|
|
4018
4250
|
return json(result);
|
|
4019
4251
|
}
|
|
4020
4252
|
async function upsertSchedule(request, ctx) {
|
|
4253
|
+
const principalMutationError = rejectPrincipalEntityMutation(request, `scheduled`);
|
|
4254
|
+
if (principalMutationError) return principalMutationError;
|
|
4021
4255
|
const parsed = routeBody(request);
|
|
4022
4256
|
const { entityUrl } = requireExistingEntityRoute(request);
|
|
4023
4257
|
const scheduleId = decodeURIComponent(request.params.scheduleId);
|
|
@@ -4033,12 +4267,13 @@ async function upsertSchedule(request, ctx) {
|
|
|
4033
4267
|
return json(result);
|
|
4034
4268
|
}
|
|
4035
4269
|
if (parsed.scheduleType === `future_send`) {
|
|
4270
|
+
if (parsed.from !== void 0 && parsed.from !== ctx.principal.url) return apiError(400, ErrCodeInvalidRequest, `Request from must match Electric-Principal`);
|
|
4036
4271
|
const result = await ctx.entityManager.upsertFutureSendSchedule(entityUrl, {
|
|
4037
4272
|
id: scheduleId,
|
|
4038
4273
|
payload: parsed.payload,
|
|
4039
4274
|
targetUrl: parsed.targetUrl,
|
|
4040
4275
|
fireAt: parsed.fireAt,
|
|
4041
|
-
|
|
4276
|
+
senderUrl: ctx.principal.url,
|
|
4042
4277
|
messageType: parsed.messageType
|
|
4043
4278
|
});
|
|
4044
4279
|
return json(result);
|
|
@@ -4046,11 +4281,15 @@ async function upsertSchedule(request, ctx) {
|
|
|
4046
4281
|
throw new Error(`schedule schema accepted an unknown scheduleType`);
|
|
4047
4282
|
}
|
|
4048
4283
|
async function deleteSchedule(request, ctx) {
|
|
4284
|
+
const principalMutationError = rejectPrincipalEntityMutation(request, `unscheduled`);
|
|
4285
|
+
if (principalMutationError) return principalMutationError;
|
|
4049
4286
|
const { entityUrl } = requireExistingEntityRoute(request);
|
|
4050
4287
|
const result = await ctx.entityManager.deleteSchedule(entityUrl, { id: decodeURIComponent(request.params.scheduleId) });
|
|
4051
4288
|
return json(result);
|
|
4052
4289
|
}
|
|
4053
4290
|
async function setTag(request, ctx) {
|
|
4291
|
+
const principalMutationError = rejectPrincipalEntityMutation(request, `tagged`);
|
|
4292
|
+
if (principalMutationError) return principalMutationError;
|
|
4054
4293
|
const parsed = routeBody(request);
|
|
4055
4294
|
const { entityUrl } = requireExistingEntityRoute(request);
|
|
4056
4295
|
const token = writeTokenFromRequest(request);
|
|
@@ -4058,12 +4297,16 @@ async function setTag(request, ctx) {
|
|
|
4058
4297
|
return json(toPublicEntity(updated));
|
|
4059
4298
|
}
|
|
4060
4299
|
async function removeTag(request, ctx) {
|
|
4300
|
+
const principalMutationError = rejectPrincipalEntityMutation(request, `untagged`);
|
|
4301
|
+
if (principalMutationError) return principalMutationError;
|
|
4061
4302
|
const { entityUrl } = requireExistingEntityRoute(request);
|
|
4062
4303
|
const token = writeTokenFromRequest(request);
|
|
4063
4304
|
const updated = await ctx.entityManager.removeTag(entityUrl, decodeURIComponent(request.params.tagKey), token);
|
|
4064
4305
|
return json(toPublicEntity(updated));
|
|
4065
4306
|
}
|
|
4066
4307
|
async function forkEntity(request, ctx) {
|
|
4308
|
+
const principalMutationError = rejectPrincipalEntityMutation(request, `forked`);
|
|
4309
|
+
if (principalMutationError) return principalMutationError;
|
|
4067
4310
|
const parsed = routeBody(request);
|
|
4068
4311
|
const { entityUrl, entity } = requireExistingEntityRoute(request);
|
|
4069
4312
|
await assertDispatchPolicyAllowed(ctx, entity.dispatch_policy);
|
|
@@ -4079,27 +4322,47 @@ async function forkEntity(request, ctx) {
|
|
|
4079
4322
|
}
|
|
4080
4323
|
async function sendEntity(request, ctx) {
|
|
4081
4324
|
const parsed = routeBody(request);
|
|
4325
|
+
const principal = ctx.principal;
|
|
4326
|
+
if (parsed.from !== void 0 && parsed.from !== principal.url) return apiError(400, ErrCodeInvalidRequest, `Request from must match Electric-Principal`);
|
|
4327
|
+
await ctx.entityManager.ensurePrincipal(principal);
|
|
4082
4328
|
const { entityUrl, entity } = requireExistingEntityRoute(request);
|
|
4083
4329
|
if (!entity.dispatch_policy) {
|
|
4084
4330
|
const updatedEntity = await backfillEntityDispatchPolicy(ctx, entity);
|
|
4085
4331
|
await linkEntityDispatchSubscription(ctx, updatedEntity);
|
|
4086
4332
|
}
|
|
4087
4333
|
if (parsed.afterMs && parsed.afterMs > 0) await ctx.entityManager.enqueueDelayedSend(entityUrl, {
|
|
4088
|
-
from:
|
|
4334
|
+
from: principal.url,
|
|
4089
4335
|
payload: parsed.payload,
|
|
4090
4336
|
key: parsed.key,
|
|
4091
|
-
type: parsed.type
|
|
4337
|
+
type: parsed.type,
|
|
4338
|
+
mode: parsed.mode,
|
|
4339
|
+
position: parsed.position
|
|
4092
4340
|
}, new Date(Date.now() + parsed.afterMs));
|
|
4093
4341
|
else await ctx.entityManager.send(entityUrl, {
|
|
4094
|
-
from:
|
|
4342
|
+
from: principal.url,
|
|
4095
4343
|
payload: parsed.payload,
|
|
4096
4344
|
key: parsed.key,
|
|
4097
|
-
type: parsed.type
|
|
4345
|
+
type: parsed.type,
|
|
4346
|
+
mode: parsed.mode,
|
|
4347
|
+
position: parsed.position
|
|
4098
4348
|
});
|
|
4099
4349
|
return status(204);
|
|
4100
4350
|
}
|
|
4351
|
+
async function updateInboxMessage(request, ctx) {
|
|
4352
|
+
const parsed = routeBody(request);
|
|
4353
|
+
const { entityUrl } = requireExistingEntityRoute(request);
|
|
4354
|
+
await ctx.entityManager.updateInboxMessage(entityUrl, decodeURIComponent(request.params.messageKey), parsed);
|
|
4355
|
+
return status(204);
|
|
4356
|
+
}
|
|
4357
|
+
async function deleteInboxMessage(request, ctx) {
|
|
4358
|
+
const { entityUrl } = requireExistingEntityRoute(request);
|
|
4359
|
+
await ctx.entityManager.deleteInboxMessage(entityUrl, decodeURIComponent(request.params.messageKey));
|
|
4360
|
+
return status(204);
|
|
4361
|
+
}
|
|
4101
4362
|
async function spawnEntity(request, ctx) {
|
|
4102
4363
|
const parsed = routeBody(request);
|
|
4364
|
+
const principal = ctx.principal;
|
|
4365
|
+
await ctx.entityManager.ensurePrincipal(principal);
|
|
4103
4366
|
const dispatchPolicy = await resolveEffectiveDispatchPolicyForSpawn(ctx, request.params.type, {
|
|
4104
4367
|
dispatchPolicy: parsed.dispatch_policy,
|
|
4105
4368
|
parent: parsed.parent
|
|
@@ -4112,11 +4375,12 @@ async function spawnEntity(request, ctx) {
|
|
|
4112
4375
|
parent: parsed.parent,
|
|
4113
4376
|
dispatch_policy: dispatchPolicy,
|
|
4114
4377
|
initialMessage: void 0,
|
|
4115
|
-
wake: parsed.wake
|
|
4378
|
+
wake: parsed.wake,
|
|
4379
|
+
created_by: principal.url
|
|
4116
4380
|
});
|
|
4117
4381
|
await linkEntityDispatchSubscription(ctx, entity);
|
|
4118
4382
|
if (parsed.initialMessage !== void 0) await ctx.entityManager.send(entity.url, {
|
|
4119
|
-
from:
|
|
4383
|
+
from: principal.url,
|
|
4120
4384
|
payload: parsed.initialMessage
|
|
4121
4385
|
});
|
|
4122
4386
|
return json({
|
|
@@ -4134,6 +4398,8 @@ function headEntity() {
|
|
|
4134
4398
|
return status(200);
|
|
4135
4399
|
}
|
|
4136
4400
|
async function killEntity(request, ctx) {
|
|
4401
|
+
const principalMutationError = rejectPrincipalEntityMutation(request, `killed`);
|
|
4402
|
+
if (principalMutationError) return principalMutationError;
|
|
4137
4403
|
const { entityUrl, entity } = requireExistingEntityRoute(request);
|
|
4138
4404
|
await unlinkEntityDispatchSubscription(ctx, entity);
|
|
4139
4405
|
const result = await ctx.entityManager.kill(entityUrl);
|
|
@@ -4280,7 +4546,7 @@ function applyCors(response) {
|
|
|
4280
4546
|
const headers = new Headers(response.headers);
|
|
4281
4547
|
headers.set(`access-control-allow-origin`, `*`);
|
|
4282
4548
|
headers.set(`access-control-allow-methods`, `GET, POST, PUT, PATCH, DELETE, OPTIONS`);
|
|
4283
|
-
headers.set(`access-control-allow-headers`, `content-type, authorization, electric-claim-token,
|
|
4549
|
+
headers.set(`access-control-allow-headers`, `content-type, authorization, electric-claim-token, ngrok-skip-browser-warning`);
|
|
4284
4550
|
headers.set(`access-control-expose-headers`, `*`);
|
|
4285
4551
|
return new Response(response.body, {
|
|
4286
4552
|
status: response.status,
|
|
@@ -4360,9 +4626,9 @@ function firstQueryValue(value) {
|
|
|
4360
4626
|
}
|
|
4361
4627
|
async function registerRunner(request, ctx) {
|
|
4362
4628
|
const parsed = routeBody(request);
|
|
4363
|
-
const ownerUserId = parsed.owner_user_id ?? ctx.
|
|
4629
|
+
const ownerUserId = parsed.owner_user_id ?? ctx.principal?.key;
|
|
4364
4630
|
if (!ownerUserId) throw new ElectricAgentsError(ErrCodeInvalidRequest, `owner_user_id is required when no authenticated user is present`, 400);
|
|
4365
|
-
if (ctx.
|
|
4631
|
+
if (ctx.principal && ownerUserId !== ctx.principal.key) throw new ElectricAgentsError(ErrCodeUnauthorized, `owner_user_id must match the authenticated user`, 403);
|
|
4366
4632
|
const runner = await ctx.entityManager.registry.createRunner({
|
|
4367
4633
|
id: parsed.id,
|
|
4368
4634
|
ownerUserId,
|
|
@@ -4376,8 +4642,8 @@ async function registerRunner(request, ctx) {
|
|
|
4376
4642
|
}
|
|
4377
4643
|
async function listRunners(request, ctx) {
|
|
4378
4644
|
const requestedOwner = firstQueryValue(request.query.owner_user_id);
|
|
4379
|
-
if (ctx.
|
|
4380
|
-
const runners$1 = await ctx.entityManager.registry.listRunners({ ownerUserId: ctx.
|
|
4645
|
+
if (ctx.principal && requestedOwner && requestedOwner !== ctx.principal.key) throw new ElectricAgentsError(ErrCodeUnauthorized, `owner_user_id must match the authenticated user`, 403);
|
|
4646
|
+
const runners$1 = await ctx.entityManager.registry.listRunners({ ownerUserId: ctx.principal?.key ?? requestedOwner });
|
|
4381
4647
|
return json(runners$1);
|
|
4382
4648
|
}
|
|
4383
4649
|
async function getRunner(request, ctx) {
|
|
@@ -4415,9 +4681,8 @@ async function setRunnerStatus(request, ctx, adminStatus) {
|
|
|
4415
4681
|
}
|
|
4416
4682
|
async function claimWake(request, ctx) {
|
|
4417
4683
|
const runnerId = routeParam$1(request, `id`);
|
|
4418
|
-
if (!ctx.authenticatedUser) throw new ElectricAgentsError(ErrCodeUnauthorized, `Authentication is required to claim runner work`, 401);
|
|
4419
4684
|
const runner = await requireRunner(ctx, runnerId);
|
|
4420
|
-
if (runner.owner_user_id !== ctx.
|
|
4685
|
+
if (ctx.principal && runner.owner_user_id !== ctx.principal.key) throw new ElectricAgentsError(ErrCodeUnauthorized, `Runner claim requires the authenticated owner`, 403);
|
|
4421
4686
|
if (runner.admin_status !== `enabled`) throw new ElectricAgentsError(ErrCodeNotRunning, `Runner is disabled`, 409);
|
|
4422
4687
|
const parsed = routeBody(request);
|
|
4423
4688
|
const expectedSubscriptionId = subscriptionIdForDispatchTarget({
|
|
@@ -4449,8 +4714,8 @@ async function requireRunner(ctx, runnerId) {
|
|
|
4449
4714
|
return runner;
|
|
4450
4715
|
}
|
|
4451
4716
|
function assertRunnerOwnerIfAuthenticated(ctx, ownerUserId) {
|
|
4452
|
-
if (!ctx.
|
|
4453
|
-
if (ownerUserId === ctx.
|
|
4717
|
+
if (!ctx.principal) return;
|
|
4718
|
+
if (ownerUserId === ctx.principal.key) return;
|
|
4454
4719
|
throw new ElectricAgentsError(ErrCodeUnauthorized, `Runner access requires the authenticated owner`, 403);
|
|
4455
4720
|
}
|
|
4456
4721
|
async function notificationFromClaim(ctx, input) {
|
|
@@ -4507,8 +4772,10 @@ async function notificationFromClaim(ctx, input) {
|
|
|
4507
4772
|
url: entity.url,
|
|
4508
4773
|
streams: entity.streams,
|
|
4509
4774
|
tags: entity.tags,
|
|
4510
|
-
spawnArgs: entity.spawn_args
|
|
4511
|
-
|
|
4775
|
+
spawnArgs: entity.spawn_args,
|
|
4776
|
+
createdBy: entity.created_by
|
|
4777
|
+
},
|
|
4778
|
+
principal: principalFromCreatedBy(entity.created_by)
|
|
4512
4779
|
};
|
|
4513
4780
|
}
|
|
4514
4781
|
|
|
@@ -4916,7 +5183,7 @@ const ENTITY_SHAPE_COLUMNS = [
|
|
|
4916
5183
|
`updated_at`
|
|
4917
5184
|
];
|
|
4918
5185
|
function parseElectricOffset(offset) {
|
|
4919
|
-
if (offset === `-1`
|
|
5186
|
+
if (offset === `-1`) return offset;
|
|
4920
5187
|
return /^\d+_\d+$/.test(offset) ? offset : null;
|
|
4921
5188
|
}
|
|
4922
5189
|
function sameMember(left, right) {
|
|
@@ -6128,6 +6395,7 @@ var ElectricAgentsTenantRuntime = class {
|
|
|
6128
6395
|
const fireAtRaw = value.fireAt;
|
|
6129
6396
|
const producerId = value.producerId;
|
|
6130
6397
|
const targetUrl = value.targetUrl;
|
|
6398
|
+
const senderUrl = typeof value.senderUrl === `string` ? value.senderUrl : ownerEntityUrl;
|
|
6131
6399
|
if (typeof fireAtRaw !== `string` || typeof producerId !== `string` || typeof targetUrl !== `string`) {
|
|
6132
6400
|
serverLog.warn(`[agent-server] invalid future_send manifest entry for ${ownerEntityUrl}/${manifestKey}`);
|
|
6133
6401
|
return;
|
|
@@ -6139,7 +6407,7 @@ var ElectricAgentsTenantRuntime = class {
|
|
|
6139
6407
|
}
|
|
6140
6408
|
await this.scheduler.syncManifestDelayedSend(ownerEntityUrl, manifestKey, {
|
|
6141
6409
|
entityUrl: targetUrl,
|
|
6142
|
-
from:
|
|
6410
|
+
from: senderUrl,
|
|
6143
6411
|
payload: value.payload,
|
|
6144
6412
|
key: `scheduled-${producerId}`,
|
|
6145
6413
|
type: typeof value.messageType === `string` ? value.messageType : void 0,
|
|
@@ -6153,6 +6421,7 @@ var ElectricAgentsTenantRuntime = class {
|
|
|
6153
6421
|
kind: `schedule`,
|
|
6154
6422
|
scheduleType: `future_send`,
|
|
6155
6423
|
targetUrl,
|
|
6424
|
+
senderUrl,
|
|
6156
6425
|
fireAt: fireAt.toISOString(),
|
|
6157
6426
|
producerId,
|
|
6158
6427
|
status: `pending`
|
|
@@ -7062,6 +7331,7 @@ var ElectricAgentsServer = class {
|
|
|
7062
7331
|
});
|
|
7063
7332
|
this.electricAgentsManager = this.standaloneRuntime.manager;
|
|
7064
7333
|
this.entityBridgeManager = this.standaloneRuntime.entityBridgeManager;
|
|
7334
|
+
await this.electricAgentsManager.ensurePrincipalEntityType();
|
|
7065
7335
|
const serverAdapter = createServerAdapter((request) => this.handleRequest(request));
|
|
7066
7336
|
const server = createServer(serverAdapter);
|
|
7067
7337
|
this.server = server;
|
|
@@ -7139,13 +7409,30 @@ var ElectricAgentsServer = class {
|
|
|
7139
7409
|
}
|
|
7140
7410
|
async handleRequest(request) {
|
|
7141
7411
|
if (!this._url || !this.standaloneRuntime || !this.electricAgentsManager || !this.entityBridgeManager || !this.pgDb || !this.streamsAgent || !this.options.durableStreamsUrl) return new Response(null, { status: 503 });
|
|
7142
|
-
|
|
7412
|
+
try {
|
|
7413
|
+
return await ossServerRouter.fetch(request, await this.buildTenantContext(request));
|
|
7414
|
+
} catch (error) {
|
|
7415
|
+
if (error instanceof ElectricAgentsError) return apiError(error.status, error.code, error.message, error.details);
|
|
7416
|
+
throw error;
|
|
7417
|
+
}
|
|
7418
|
+
}
|
|
7419
|
+
allowDevPrincipalFallback() {
|
|
7420
|
+
if (this.options.allowDevPrincipalFallback !== void 0) return this.options.allowDevPrincipalFallback;
|
|
7421
|
+
return process.env.ELECTRIC_INSECURE === `true` || process.env.NODE_ENV !== `production` || Boolean(this.options.durableStreamsServer);
|
|
7143
7422
|
}
|
|
7144
7423
|
async buildTenantContext(request) {
|
|
7145
7424
|
if (!this.standaloneRuntime || !this.electricAgentsManager || !this.entityBridgeManager || !this.pgDb || !this.streamsAgent || !this.options.durableStreamsUrl) throw new Error(`agents-server runtime is not started`);
|
|
7425
|
+
let principal;
|
|
7426
|
+
try {
|
|
7427
|
+
principal = await this.options.authenticateRequest?.(request) ?? getPrincipalFromRequest(request);
|
|
7428
|
+
} catch (error) {
|
|
7429
|
+
throw new ElectricAgentsError(ErrCodeInvalidRequest, error instanceof Error ? error.message : `Invalid principal`, 400);
|
|
7430
|
+
}
|
|
7431
|
+
if (!principal && this.allowDevPrincipalFallback()) principal = getDevPrincipal();
|
|
7432
|
+
if (!principal) throw new ElectricAgentsError(ErrCodeUnauthorized, `Missing Electric-Principal`, 401);
|
|
7146
7433
|
return {
|
|
7147
7434
|
service: this.tenantId,
|
|
7148
|
-
|
|
7435
|
+
principal,
|
|
7149
7436
|
publicUrl: this.publicUrl,
|
|
7150
7437
|
localUrl: this._url,
|
|
7151
7438
|
durableStreamsUrl: this.options.durableStreamsUrl,
|
|
@@ -7223,9 +7510,7 @@ function resolveElectricAgentsEntrypointOptions(env = process.env, cwd = process
|
|
|
7223
7510
|
const electricUrl = readEnv(env, [`ELECTRIC_AGENTS_ELECTRIC_URL`, `ELECTRIC_URL`]);
|
|
7224
7511
|
const electricSecret = readEnv(env, [`ELECTRIC_AGENTS_ELECTRIC_SECRET`]);
|
|
7225
7512
|
const baseUrl = readEnv(env, [`ELECTRIC_AGENTS_BASE_URL`, `BASE_URL`]);
|
|
7226
|
-
const authenticateRequest = createDevAssertedAuthenticateRequest(devAssertedAuthOptionsFromEnv(env));
|
|
7227
7513
|
return {
|
|
7228
|
-
...authenticateRequest ? { authenticateRequest } : {},
|
|
7229
7514
|
service: readEnv(env, [`ELECTRIC_AGENTS_SERVICE`, `SERVICE`]),
|
|
7230
7515
|
tenantId: readEnv(env, [`ELECTRIC_AGENTS_TENANT_ID`, `TENANT_ID`]),
|
|
7231
7516
|
baseUrl: baseUrl ? validateUrl(`base URL`, baseUrl) : void 0,
|