@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/index.cjs
CHANGED
|
@@ -90,6 +90,7 @@ const entities = (0, drizzle_orm_pg_core.pgTable)(`entities`, {
|
|
|
90
90
|
tagsIndex: (0, drizzle_orm_pg_core.text)(`tags_index`).array().notNull().default(drizzle_orm.sql`'{}'::text[]`),
|
|
91
91
|
spawnArgs: (0, drizzle_orm_pg_core.jsonb)(`spawn_args`).default({}),
|
|
92
92
|
parent: (0, drizzle_orm_pg_core.text)(`parent`),
|
|
93
|
+
createdBy: (0, drizzle_orm_pg_core.text)(`created_by`),
|
|
93
94
|
typeRevision: (0, drizzle_orm_pg_core.integer)(`type_revision`),
|
|
94
95
|
inboxSchemas: (0, drizzle_orm_pg_core.jsonb)(`inbox_schemas`),
|
|
95
96
|
stateSchemas: (0, drizzle_orm_pg_core.jsonb)(`state_schemas`),
|
|
@@ -100,6 +101,7 @@ const entities = (0, drizzle_orm_pg_core.pgTable)(`entities`, {
|
|
|
100
101
|
(0, drizzle_orm_pg_core.index)(`idx_entities_type`).on(table.tenantId, table.type),
|
|
101
102
|
(0, drizzle_orm_pg_core.index)(`idx_entities_status`).on(table.tenantId, table.status),
|
|
102
103
|
(0, drizzle_orm_pg_core.index)(`idx_entities_parent`).on(table.tenantId, table.parent),
|
|
104
|
+
(0, drizzle_orm_pg_core.index)(`idx_entities_created_by`).on(table.tenantId, table.createdBy),
|
|
103
105
|
(0, drizzle_orm_pg_core.index)(`entities_tags_index_gin`).using(`gin`, table.tagsIndex),
|
|
104
106
|
(0, drizzle_orm_pg_core.check)(`chk_entities_status`, drizzle_orm.sql`${table.status} IN ('spawning', 'running', 'idle', 'stopped')`)
|
|
105
107
|
]);
|
|
@@ -382,6 +384,7 @@ function toPublicEntity(entity) {
|
|
|
382
384
|
tags: entity.tags,
|
|
383
385
|
spawn_args: entity.spawn_args,
|
|
384
386
|
parent: entity.parent,
|
|
387
|
+
created_by: entity.created_by,
|
|
385
388
|
created_at: entity.created_at,
|
|
386
389
|
updated_at: entity.updated_at
|
|
387
390
|
};
|
|
@@ -621,6 +624,24 @@ var PostgresRegistry = class {
|
|
|
621
624
|
}
|
|
622
625
|
});
|
|
623
626
|
}
|
|
627
|
+
async ensureEntityType(et) {
|
|
628
|
+
const existing = await this.getEntityType(et.name);
|
|
629
|
+
if (existing) return existing;
|
|
630
|
+
await this.db.insert(entityTypes).values({
|
|
631
|
+
tenantId: this.tenantId,
|
|
632
|
+
name: et.name,
|
|
633
|
+
description: et.description,
|
|
634
|
+
creationSchema: et.creation_schema ?? null,
|
|
635
|
+
inboxSchemas: et.inbox_schemas ?? null,
|
|
636
|
+
stateSchemas: et.state_schemas ?? null,
|
|
637
|
+
serveEndpoint: et.serve_endpoint ?? null,
|
|
638
|
+
defaultDispatchPolicy: et.default_dispatch_policy ?? null,
|
|
639
|
+
revision: et.revision,
|
|
640
|
+
createdAt: et.created_at,
|
|
641
|
+
updatedAt: et.updated_at
|
|
642
|
+
}).onConflictDoNothing();
|
|
643
|
+
return await this.getEntityType(et.name);
|
|
644
|
+
}
|
|
624
645
|
async getEntityType(name) {
|
|
625
646
|
const rows = await this.db.select().from(entityTypes).where(this.entityTypeWhere(name)).limit(1);
|
|
626
647
|
if (rows.length === 0) return null;
|
|
@@ -660,6 +681,7 @@ var PostgresRegistry = class {
|
|
|
660
681
|
tagsIndex: (0, __electric_ax_agents_runtime.buildTagsIndex)(entity.tags),
|
|
661
682
|
spawnArgs: entity.spawn_args ?? {},
|
|
662
683
|
parent: entity.parent ?? null,
|
|
684
|
+
createdBy: entity.created_by ?? null,
|
|
663
685
|
typeRevision: entity.type_revision ?? null,
|
|
664
686
|
inboxSchemas: entity.inbox_schemas ?? null,
|
|
665
687
|
stateSchemas: entity.state_schemas ?? null,
|
|
@@ -705,6 +727,7 @@ var PostgresRegistry = class {
|
|
|
705
727
|
if (filter?.type) conditions.push((0, drizzle_orm.eq)(entities.type, filter.type));
|
|
706
728
|
if (filter?.status) conditions.push((0, drizzle_orm.eq)(entities.status, filter.status));
|
|
707
729
|
if (filter?.parent) conditions.push((0, drizzle_orm.eq)(entities.parent, filter.parent));
|
|
730
|
+
if (filter?.created_by) conditions.push((0, drizzle_orm.eq)(entities.createdBy, filter.created_by));
|
|
708
731
|
const whereClause = (0, drizzle_orm.and)(...conditions);
|
|
709
732
|
const countResult = await this.db.select({ count: drizzle_orm.sql`count(*)` }).from(entities).where(whereClause);
|
|
710
733
|
const total = Number(countResult[0].count);
|
|
@@ -991,6 +1014,7 @@ var PostgresRegistry = class {
|
|
|
991
1014
|
tags: row.tags ?? {},
|
|
992
1015
|
spawn_args: row.spawnArgs,
|
|
993
1016
|
parent: row.parent ?? void 0,
|
|
1017
|
+
created_by: row.createdBy ?? void 0,
|
|
994
1018
|
type_revision: row.typeRevision ?? void 0,
|
|
995
1019
|
inbox_schemas: row.inboxSchemas,
|
|
996
1020
|
state_schemas: row.stateSchemas,
|
|
@@ -1716,6 +1740,239 @@ function parseDispatchPolicy(value, label = `dispatch_policy`) {
|
|
|
1716
1740
|
throw new Error(details ? `${label} does not match dispatch policy schema: ${details}` : `${label} does not match dispatch policy schema`);
|
|
1717
1741
|
}
|
|
1718
1742
|
|
|
1743
|
+
//#endregion
|
|
1744
|
+
//#region src/utils/webhook-url.ts
|
|
1745
|
+
function rewriteLoopbackWebhookUrl(value) {
|
|
1746
|
+
if (!value) return void 0;
|
|
1747
|
+
const rewriteTarget = process.env.ELECTRIC_AGENTS_REWRITE_LOOPBACK_WEBHOOKS_TO?.trim();
|
|
1748
|
+
if (!rewriteTarget) return value;
|
|
1749
|
+
const url = new URL(value);
|
|
1750
|
+
if (!isLoopbackHostname(url.hostname)) return value;
|
|
1751
|
+
if (rewriteTarget.includes(`://`)) {
|
|
1752
|
+
const target = new URL(rewriteTarget);
|
|
1753
|
+
url.protocol = target.protocol;
|
|
1754
|
+
url.username = target.username;
|
|
1755
|
+
url.password = target.password;
|
|
1756
|
+
url.hostname = target.hostname;
|
|
1757
|
+
url.port = target.port;
|
|
1758
|
+
return url.toString();
|
|
1759
|
+
}
|
|
1760
|
+
url.host = rewriteTarget;
|
|
1761
|
+
return url.toString();
|
|
1762
|
+
}
|
|
1763
|
+
function isLoopbackHostname(hostname) {
|
|
1764
|
+
return hostname === `localhost` || hostname === `127.0.0.1` || hostname === `::1`;
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
//#endregion
|
|
1768
|
+
//#region src/routing/dispatch-policy.ts
|
|
1769
|
+
function subscriptionIdForDispatchTarget(target) {
|
|
1770
|
+
if (target.subscription_id) return target.subscription_id;
|
|
1771
|
+
if (target.type === `runner`) return `runner:${target.runnerId}`;
|
|
1772
|
+
const digest = (0, node_crypto.createHash)(`sha256`).update(target.url).digest(`hex`);
|
|
1773
|
+
return `webhook:${digest.slice(0, 16)}`;
|
|
1774
|
+
}
|
|
1775
|
+
function subscriptionIdForEntityDispatchTarget(target, entityUrl) {
|
|
1776
|
+
const base = subscriptionIdForDispatchTarget(target);
|
|
1777
|
+
if (!target.subscription_id) return base;
|
|
1778
|
+
const digest = (0, node_crypto.createHash)(`sha256`).update(entityUrl).digest(`hex`);
|
|
1779
|
+
return `${base}:${digest.slice(0, 16)}`;
|
|
1780
|
+
}
|
|
1781
|
+
async function resolveEffectiveDispatchPolicyForSpawn(ctx, typeName, opts) {
|
|
1782
|
+
if (opts.dispatchPolicy) return opts.dispatchPolicy;
|
|
1783
|
+
const entityType = await ctx.entityManager.registry.getEntityType(typeName);
|
|
1784
|
+
if (opts.parent) {
|
|
1785
|
+
const parent = await ctx.entityManager.registry.getEntity(opts.parent);
|
|
1786
|
+
if (parent?.dispatch_policy) return applyTypeDefaultSubscriptionScope(parent.dispatch_policy, entityType?.default_dispatch_policy);
|
|
1787
|
+
}
|
|
1788
|
+
return entityType?.default_dispatch_policy;
|
|
1789
|
+
}
|
|
1790
|
+
async function resolveEffectiveDispatchPolicyForEntity(ctx, entity) {
|
|
1791
|
+
if (entity.dispatch_policy) return entity.dispatch_policy;
|
|
1792
|
+
const entityType = await ctx.entityManager.registry.getEntityType(entity.type);
|
|
1793
|
+
return entityType?.default_dispatch_policy;
|
|
1794
|
+
}
|
|
1795
|
+
async function backfillEntityDispatchPolicy(ctx, entity) {
|
|
1796
|
+
if (entity.dispatch_policy) return entity;
|
|
1797
|
+
const dispatchPolicy = await resolveEffectiveDispatchPolicyForEntity(ctx, entity);
|
|
1798
|
+
if (!dispatchPolicy) return entity;
|
|
1799
|
+
return await ctx.entityManager.registry.updateEntityDispatchPolicy(entity.url, dispatchPolicy) ?? {
|
|
1800
|
+
...entity,
|
|
1801
|
+
dispatch_policy: dispatchPolicy
|
|
1802
|
+
};
|
|
1803
|
+
}
|
|
1804
|
+
function applyTypeDefaultSubscriptionScope(policy, typeDefault) {
|
|
1805
|
+
const target = policy.targets[0];
|
|
1806
|
+
const defaultTarget = typeDefault?.targets[0];
|
|
1807
|
+
if (!target || !defaultTarget?.subscription_id) return policy;
|
|
1808
|
+
if (!sameDispatchDestination(target, defaultTarget)) return policy;
|
|
1809
|
+
if (target.subscription_id === defaultTarget.subscription_id) return policy;
|
|
1810
|
+
return { targets: [{
|
|
1811
|
+
...target,
|
|
1812
|
+
subscription_id: defaultTarget.subscription_id
|
|
1813
|
+
}] };
|
|
1814
|
+
}
|
|
1815
|
+
function sameDispatchDestination(a, b) {
|
|
1816
|
+
if (a.type !== b.type) return false;
|
|
1817
|
+
if (a.type === `runner` && b.type === `runner`) return a.runnerId === b.runnerId;
|
|
1818
|
+
if (a.type === `webhook` && b.type === `webhook`) return a.url === b.url;
|
|
1819
|
+
return false;
|
|
1820
|
+
}
|
|
1821
|
+
async function assertDispatchPolicyAllowed(ctx, policy) {
|
|
1822
|
+
const target = policy?.targets[0];
|
|
1823
|
+
if (!target || target.type !== `runner`) return;
|
|
1824
|
+
const runner = await ctx.entityManager.registry.getRunner(target.runnerId);
|
|
1825
|
+
if (!runner) throw new ElectricAgentsError(ErrCodeNotFound, `Runner "${target.runnerId}" not found`, 404);
|
|
1826
|
+
if (ctx.principal && runner.owner_user_id !== ctx.principal.key) throw new ElectricAgentsError(ErrCodeUnauthorized, `Runner dispatch requires the authenticated owner`, 403);
|
|
1827
|
+
}
|
|
1828
|
+
async function linkEntityDispatchSubscription(ctx, entity) {
|
|
1829
|
+
const dispatchPolicy = await resolveEffectiveDispatchPolicyForEntity(ctx, entity);
|
|
1830
|
+
const target = dispatchPolicy?.targets[0];
|
|
1831
|
+
if (!target) return;
|
|
1832
|
+
await linkStreamToTargetSubscription(ctx, target, entity);
|
|
1833
|
+
}
|
|
1834
|
+
async function unlinkEntityDispatchSubscription(ctx, entity) {
|
|
1835
|
+
const dispatchPolicy = await resolveEffectiveDispatchPolicyForEntity(ctx, entity);
|
|
1836
|
+
const target = dispatchPolicy?.targets[0];
|
|
1837
|
+
if (!target) return;
|
|
1838
|
+
const subscriptionId = subscriptionIdForEntityDispatchTarget(target, entity.url);
|
|
1839
|
+
await ctx.streamClient.removeSubscriptionStream(subscriptionId, entity.streams.main).catch((err) => {
|
|
1840
|
+
serverLog.warn(`[dispatch-policy] failed to remove stream from subscription`, {
|
|
1841
|
+
subscriptionId,
|
|
1842
|
+
stream: entity.streams.main
|
|
1843
|
+
}, err);
|
|
1844
|
+
});
|
|
1845
|
+
}
|
|
1846
|
+
async function linkStreamToTargetSubscription(ctx, target, entity) {
|
|
1847
|
+
const streamPath = entity.streams.main;
|
|
1848
|
+
const subscriptionId = subscriptionIdForEntityDispatchTarget(target, entity.url);
|
|
1849
|
+
const existing = await ctx.streamClient.getSubscription(subscriptionId);
|
|
1850
|
+
if (target.type === `runner`) {
|
|
1851
|
+
const runner = await ctx.entityManager.registry.getRunner(target.runnerId);
|
|
1852
|
+
if (!runner) throw new ElectricAgentsError(ErrCodeNotFound, `Runner "${target.runnerId}" not found`, 404);
|
|
1853
|
+
const wakeStream = runner.wake_stream || runnerWakeStream(target.runnerId);
|
|
1854
|
+
await ctx.streamClient.ensure(wakeStream, { contentType: `application/json` });
|
|
1855
|
+
if (!existing) {
|
|
1856
|
+
await ctx.streamClient.putSubscription(subscriptionId, {
|
|
1857
|
+
type: `pull-wake`,
|
|
1858
|
+
streams: [streamPath],
|
|
1859
|
+
wake_stream: wakeStream,
|
|
1860
|
+
description: `Electric Agents runner ${target.runnerId}`
|
|
1861
|
+
});
|
|
1862
|
+
return;
|
|
1863
|
+
}
|
|
1864
|
+
await ctx.streamClient.addSubscriptionStreams(subscriptionId, [streamPath]);
|
|
1865
|
+
return;
|
|
1866
|
+
}
|
|
1867
|
+
const webhookUrl = rewriteLoopbackWebhookUrl(target.url);
|
|
1868
|
+
if (!webhookUrl) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Webhook dispatch target must include a valid URL`, 400);
|
|
1869
|
+
const forwardUrl = (0, __electric_ax_agents_runtime.appendPathToUrl)(ctx.publicUrl, `/_electric/webhook-forward/${encodeURIComponent(subscriptionId)}`);
|
|
1870
|
+
if (!existing) await ctx.streamClient.putSubscription(subscriptionId, {
|
|
1871
|
+
type: `webhook`,
|
|
1872
|
+
streams: [streamPath],
|
|
1873
|
+
webhook: { url: forwardUrl },
|
|
1874
|
+
description: `Electric Agents webhook ${subscriptionId}`
|
|
1875
|
+
});
|
|
1876
|
+
else await ctx.streamClient.addSubscriptionStreams(subscriptionId, [streamPath]);
|
|
1877
|
+
await ctx.pgDb.insert(subscriptionWebhooks).values({
|
|
1878
|
+
tenantId: ctx.service,
|
|
1879
|
+
subscriptionId,
|
|
1880
|
+
webhookUrl
|
|
1881
|
+
}).onConflictDoUpdate({
|
|
1882
|
+
target: [subscriptionWebhooks.tenantId, subscriptionWebhooks.subscriptionId],
|
|
1883
|
+
set: { webhookUrl }
|
|
1884
|
+
});
|
|
1885
|
+
}
|
|
1886
|
+
|
|
1887
|
+
//#endregion
|
|
1888
|
+
//#region src/principal.ts
|
|
1889
|
+
const PRINCIPAL_KINDS = new Set([
|
|
1890
|
+
`user`,
|
|
1891
|
+
`agent`,
|
|
1892
|
+
`service`,
|
|
1893
|
+
`system`
|
|
1894
|
+
]);
|
|
1895
|
+
function parsePrincipalKey(input) {
|
|
1896
|
+
const colon = input.indexOf(`:`);
|
|
1897
|
+
if (colon <= 0) throw new Error(`Invalid principal key`);
|
|
1898
|
+
const kind = input.slice(0, colon);
|
|
1899
|
+
const id = input.slice(colon + 1);
|
|
1900
|
+
if (!PRINCIPAL_KINDS.has(kind)) throw new Error(`Invalid principal kind`);
|
|
1901
|
+
if (!id || id.includes(`/`)) throw new Error(`Invalid principal id`);
|
|
1902
|
+
const key = `${kind}:${id}`;
|
|
1903
|
+
return {
|
|
1904
|
+
kind,
|
|
1905
|
+
id,
|
|
1906
|
+
key,
|
|
1907
|
+
url: `/principal/${encodeURIComponent(key)}`
|
|
1908
|
+
};
|
|
1909
|
+
}
|
|
1910
|
+
function principalUrl(key) {
|
|
1911
|
+
return parsePrincipalKey(key).url;
|
|
1912
|
+
}
|
|
1913
|
+
function principalKeyFromUrl(url) {
|
|
1914
|
+
if (!url.startsWith(`/principal/`)) return null;
|
|
1915
|
+
const segment = url.slice(`/principal/`.length);
|
|
1916
|
+
if (!segment || segment.includes(`/`)) return null;
|
|
1917
|
+
try {
|
|
1918
|
+
const key = decodeURIComponent(segment);
|
|
1919
|
+
return parsePrincipalKey(key).key;
|
|
1920
|
+
} catch {
|
|
1921
|
+
return null;
|
|
1922
|
+
}
|
|
1923
|
+
}
|
|
1924
|
+
const BUILT_IN_SYSTEM_PRINCIPAL_IDS = new Set([
|
|
1925
|
+
`framework`,
|
|
1926
|
+
`auth-sync`,
|
|
1927
|
+
`dev-local`
|
|
1928
|
+
]);
|
|
1929
|
+
function isBuiltInSystemPrincipalUrl(url) {
|
|
1930
|
+
if (!url?.startsWith(`/principal/`)) return false;
|
|
1931
|
+
try {
|
|
1932
|
+
const key = principalKeyFromUrl(url);
|
|
1933
|
+
if (!key) return false;
|
|
1934
|
+
const principal = parsePrincipalKey(key);
|
|
1935
|
+
return principal.kind === `system` && BUILT_IN_SYSTEM_PRINCIPAL_IDS.has(principal.id);
|
|
1936
|
+
} catch {
|
|
1937
|
+
return false;
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
function principalFromCreatedBy(createdBy) {
|
|
1941
|
+
if (!createdBy) return void 0;
|
|
1942
|
+
const key = principalKeyFromUrl(createdBy);
|
|
1943
|
+
if (!key) return {
|
|
1944
|
+
url: createdBy,
|
|
1945
|
+
key: null
|
|
1946
|
+
};
|
|
1947
|
+
const principal = parsePrincipalKey(key);
|
|
1948
|
+
return {
|
|
1949
|
+
url: principal.url,
|
|
1950
|
+
key: principal.key,
|
|
1951
|
+
kind: principal.kind,
|
|
1952
|
+
id: principal.id
|
|
1953
|
+
};
|
|
1954
|
+
}
|
|
1955
|
+
const principalIdentityStateSchema = __sinclair_typebox.Type.Object({
|
|
1956
|
+
kind: __sinclair_typebox.Type.Union([
|
|
1957
|
+
__sinclair_typebox.Type.Literal(`user`),
|
|
1958
|
+
__sinclair_typebox.Type.Literal(`agent`),
|
|
1959
|
+
__sinclair_typebox.Type.Literal(`service`),
|
|
1960
|
+
__sinclair_typebox.Type.Literal(`system`)
|
|
1961
|
+
]),
|
|
1962
|
+
id: __sinclair_typebox.Type.String(),
|
|
1963
|
+
key: __sinclair_typebox.Type.String(),
|
|
1964
|
+
url: __sinclair_typebox.Type.String(),
|
|
1965
|
+
updated_at: __sinclair_typebox.Type.String(),
|
|
1966
|
+
display_name: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
|
|
1967
|
+
email: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
|
|
1968
|
+
avatar_url: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
|
|
1969
|
+
auth_provider: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
|
|
1970
|
+
auth_subject: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
|
|
1971
|
+
claims: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Record(__sinclair_typebox.Type.String(), __sinclair_typebox.Type.Unknown())),
|
|
1972
|
+
created_at: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String())
|
|
1973
|
+
}, { additionalProperties: false });
|
|
1974
|
+
const principalUpdateIdentityMessageSchema = __sinclair_typebox.Type.Object({ identity: principalIdentityStateSchema }, { additionalProperties: false });
|
|
1975
|
+
|
|
1719
1976
|
//#endregion
|
|
1720
1977
|
//#region src/manifest-side-effects.ts
|
|
1721
1978
|
function isRecord$1(value) {
|
|
@@ -1833,25 +2090,11 @@ function extractTraceContext(headers) {
|
|
|
1833
2090
|
|
|
1834
2091
|
//#endregion
|
|
1835
2092
|
//#region src/entity-manager.ts
|
|
2093
|
+
function createInitialQueuePosition(date) {
|
|
2094
|
+
return `${String(date.getTime()).padStart(16, `0`)}:a0`;
|
|
2095
|
+
}
|
|
1836
2096
|
const DEFAULT_FORK_WAIT_TIMEOUT_MS = 12e4;
|
|
1837
2097
|
const DEFAULT_FORK_WAIT_POLL_MS = 250;
|
|
1838
|
-
function applyTypeDefaultSubscriptionScope$1(policy, typeDefault) {
|
|
1839
|
-
const target = policy.targets[0];
|
|
1840
|
-
const defaultTarget = typeDefault?.targets[0];
|
|
1841
|
-
if (!target || !defaultTarget?.subscription_id) return policy;
|
|
1842
|
-
if (!sameDispatchDestination$1(target, defaultTarget)) return policy;
|
|
1843
|
-
if (target.subscription_id === defaultTarget.subscription_id) return policy;
|
|
1844
|
-
return { targets: [{
|
|
1845
|
-
...target,
|
|
1846
|
-
subscription_id: defaultTarget.subscription_id
|
|
1847
|
-
}] };
|
|
1848
|
-
}
|
|
1849
|
-
function sameDispatchDestination$1(a, b) {
|
|
1850
|
-
if (a.type !== b.type) return false;
|
|
1851
|
-
if (a.type === `runner` && b.type === `runner`) return a.runnerId === b.runnerId;
|
|
1852
|
-
if (a.type === `webhook` && b.type === `webhook`) return a.url === b.url;
|
|
1853
|
-
return false;
|
|
1854
|
-
}
|
|
1855
2098
|
function sleep(ms) {
|
|
1856
2099
|
return new Promise((resolve$1) => setTimeout(resolve$1, ms));
|
|
1857
2100
|
}
|
|
@@ -1922,6 +2165,7 @@ var EntityManager = class {
|
|
|
1922
2165
|
}
|
|
1923
2166
|
async registerEntityType(req) {
|
|
1924
2167
|
if (!req.name) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Missing required field: name`, 400);
|
|
2168
|
+
if (req.name === `principal`) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Entity type "principal" is built in and cannot be registered or updated`, 400);
|
|
1925
2169
|
if (req.name.startsWith(`_`)) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Entity type names starting with "_" are reserved`, 400);
|
|
1926
2170
|
if (!req.description) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Missing required field: description`, 400);
|
|
1927
2171
|
this.validateSchema(req.creation_schema);
|
|
@@ -1948,10 +2192,63 @@ var EntityManager = class {
|
|
|
1948
2192
|
return stored;
|
|
1949
2193
|
}
|
|
1950
2194
|
async deleteEntityType(name) {
|
|
2195
|
+
if (name === `principal`) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Entity type "principal" is built in and cannot be deleted`, 400);
|
|
1951
2196
|
const existing = await this.registry.getEntityType(name);
|
|
1952
2197
|
if (!existing) throw new ElectricAgentsError(ErrCodeNotFound, `Entity type "${name}" not found`, 404);
|
|
1953
2198
|
await this.registry.deleteEntityType(name);
|
|
1954
2199
|
}
|
|
2200
|
+
async ensurePrincipalEntityType() {
|
|
2201
|
+
const now = new Date().toISOString();
|
|
2202
|
+
return await this.registry.ensureEntityType({
|
|
2203
|
+
name: `principal`,
|
|
2204
|
+
description: `built-in principal entity`,
|
|
2205
|
+
inbox_schemas: { update_identity: principalUpdateIdentityMessageSchema },
|
|
2206
|
+
state_schemas: { identity: principalIdentityStateSchema },
|
|
2207
|
+
revision: 1,
|
|
2208
|
+
created_at: now,
|
|
2209
|
+
updated_at: now
|
|
2210
|
+
});
|
|
2211
|
+
}
|
|
2212
|
+
async ensurePrincipal(principal) {
|
|
2213
|
+
const existing = await this.registry.getEntity(principal.url);
|
|
2214
|
+
if (existing) return existing;
|
|
2215
|
+
await this.ensurePrincipalEntityType();
|
|
2216
|
+
try {
|
|
2217
|
+
const entity = await this.spawn(`principal`, {
|
|
2218
|
+
instance_id: principal.key,
|
|
2219
|
+
args: {
|
|
2220
|
+
kind: principal.kind,
|
|
2221
|
+
id: principal.id,
|
|
2222
|
+
key: principal.key
|
|
2223
|
+
},
|
|
2224
|
+
tags: {
|
|
2225
|
+
principal_kind: principal.kind,
|
|
2226
|
+
principal_id: principal.id
|
|
2227
|
+
},
|
|
2228
|
+
created_by: principal.url
|
|
2229
|
+
});
|
|
2230
|
+
const now = new Date().toISOString();
|
|
2231
|
+
await this.streamClient.append(entity.streams.main, this.encodeChangeEvent({
|
|
2232
|
+
type: `identity`,
|
|
2233
|
+
key: `self`,
|
|
2234
|
+
value: {
|
|
2235
|
+
kind: principal.kind,
|
|
2236
|
+
id: principal.id,
|
|
2237
|
+
key: principal.key,
|
|
2238
|
+
url: principal.url,
|
|
2239
|
+
created_at: now,
|
|
2240
|
+
updated_at: now
|
|
2241
|
+
}
|
|
2242
|
+
}));
|
|
2243
|
+
return entity;
|
|
2244
|
+
} catch (error) {
|
|
2245
|
+
if (error instanceof ElectricAgentsError && error.code === ErrCodeDuplicateURL) {
|
|
2246
|
+
const raced = await this.registry.getEntity(principal.url);
|
|
2247
|
+
if (raced) return raced;
|
|
2248
|
+
}
|
|
2249
|
+
throw error;
|
|
2250
|
+
}
|
|
2251
|
+
}
|
|
1955
2252
|
/**
|
|
1956
2253
|
* Spawn a new entity of the given type with durable streams.
|
|
1957
2254
|
*/
|
|
@@ -1967,6 +2264,7 @@ var EntityManager = class {
|
|
|
1967
2264
|
});
|
|
1968
2265
|
}
|
|
1969
2266
|
async spawnInner(typeName, req) {
|
|
2267
|
+
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);
|
|
1970
2268
|
if (typeName.startsWith(`_`)) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Entity type names starting with "_" are reserved`, 400);
|
|
1971
2269
|
const entityType = await this.registry.getEntityType(typeName);
|
|
1972
2270
|
if (!entityType) throw new ElectricAgentsError(ErrCodeUnknownEntityType, `Entity type "${typeName}" not found`, 404);
|
|
@@ -1978,7 +2276,7 @@ var EntityManager = class {
|
|
|
1978
2276
|
const instanceId = req.instance_id || (0, node_crypto.randomUUID)();
|
|
1979
2277
|
if (instanceId.includes(`/`)) throw new ElectricAgentsError(ErrCodeInvalidRequest, `instance_id must not contain forward slashes`, 400);
|
|
1980
2278
|
const writeToken = (0, node_crypto.randomUUID)();
|
|
1981
|
-
const entityURL = `/${typeName}/${instanceId}`;
|
|
2279
|
+
const entityURL = typeName === `principal` ? principalUrl(instanceId) : `/${typeName}/${instanceId}`;
|
|
1982
2280
|
const mainPath = `${entityURL}/main`;
|
|
1983
2281
|
const errorPath = `${entityURL}/error`;
|
|
1984
2282
|
const subscriptionId = `${typeName}-handler`;
|
|
@@ -1990,7 +2288,7 @@ var EntityManager = class {
|
|
|
1990
2288
|
parentEntity = await this.registry.getEntity(req.parent);
|
|
1991
2289
|
if (!parentEntity) throw new ElectricAgentsError(ErrCodeNotFound, `Parent entity "${req.parent}" not found`, 404);
|
|
1992
2290
|
}
|
|
1993
|
-
const dispatchPolicy = req.dispatch_policy ? this.validateDispatchPolicy(req.dispatch_policy, { label: `dispatch_policy` }) : parentEntity?.dispatch_policy ? applyTypeDefaultSubscriptionScope
|
|
2291
|
+
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;
|
|
1994
2292
|
const now = Date.now();
|
|
1995
2293
|
const entityData = {
|
|
1996
2294
|
type: typeName,
|
|
@@ -2009,6 +2307,7 @@ var EntityManager = class {
|
|
|
2009
2307
|
inbox_schemas: entityType.inbox_schemas,
|
|
2010
2308
|
state_schemas: entityType.state_schemas,
|
|
2011
2309
|
created_at: now,
|
|
2310
|
+
created_by: req.created_by ?? parentEntity?.created_by,
|
|
2012
2311
|
updated_at: now
|
|
2013
2312
|
};
|
|
2014
2313
|
if (req.parent) entityData.parent = req.parent;
|
|
@@ -2038,7 +2337,7 @@ var EntityManager = class {
|
|
|
2038
2337
|
const inboxEvent = __electric_ax_agents_runtime.entityStateSchema.inbox.insert({
|
|
2039
2338
|
key: `msg-in-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
2040
2339
|
value: {
|
|
2041
|
-
from: req.parent ?? `spawn`,
|
|
2340
|
+
from: req.created_by ?? req.parent ?? `spawn`,
|
|
2042
2341
|
payload: req.initialMessage,
|
|
2043
2342
|
timestamp: msgNow
|
|
2044
2343
|
}
|
|
@@ -2581,10 +2880,10 @@ var EntityManager = class {
|
|
|
2581
2880
|
changed = true;
|
|
2582
2881
|
}
|
|
2583
2882
|
}
|
|
2584
|
-
if (typeof next.
|
|
2585
|
-
const
|
|
2586
|
-
if (
|
|
2587
|
-
next.
|
|
2883
|
+
if (typeof next.senderUrl === `string`) {
|
|
2884
|
+
const forkSender = entityUrlMap.get(next.senderUrl);
|
|
2885
|
+
if (forkSender) {
|
|
2886
|
+
next.senderUrl = forkSender;
|
|
2588
2887
|
changed = true;
|
|
2589
2888
|
}
|
|
2590
2889
|
}
|
|
@@ -2650,6 +2949,7 @@ var EntityManager = class {
|
|
|
2650
2949
|
const fireAtRaw = manifest.fireAt;
|
|
2651
2950
|
const producerId = manifest.producerId;
|
|
2652
2951
|
const targetUrl = manifest.targetUrl;
|
|
2952
|
+
const senderUrl = typeof manifest.senderUrl === `string` ? manifest.senderUrl : ownerEntityUrl;
|
|
2653
2953
|
if (typeof fireAtRaw !== `string` || typeof producerId !== `string` || typeof targetUrl !== `string`) {
|
|
2654
2954
|
serverLog.warn(`[agent-server] invalid forked future_send manifest entry for ${ownerEntityUrl}/${manifestKey}`);
|
|
2655
2955
|
return;
|
|
@@ -2661,7 +2961,7 @@ var EntityManager = class {
|
|
|
2661
2961
|
}
|
|
2662
2962
|
await this.scheduler.syncManifestDelayedSend(ownerEntityUrl, manifestKey, {
|
|
2663
2963
|
entityUrl: targetUrl,
|
|
2664
|
-
from:
|
|
2964
|
+
from: senderUrl,
|
|
2665
2965
|
payload: manifest.payload,
|
|
2666
2966
|
key: `scheduled-${producerId}`,
|
|
2667
2967
|
type: typeof manifest.messageType === `string` ? manifest.messageType : void 0,
|
|
@@ -2675,6 +2975,7 @@ var EntityManager = class {
|
|
|
2675
2975
|
kind: `schedule`,
|
|
2676
2976
|
scheduleType: `future_send`,
|
|
2677
2977
|
targetUrl,
|
|
2978
|
+
senderUrl,
|
|
2678
2979
|
fireAt: fireAt.toISOString(),
|
|
2679
2980
|
producerId,
|
|
2680
2981
|
status: `pending`
|
|
@@ -2702,9 +3003,14 @@ var EntityManager = class {
|
|
|
2702
3003
|
const value = {
|
|
2703
3004
|
from: req.from,
|
|
2704
3005
|
payload: req.payload,
|
|
2705
|
-
timestamp: now
|
|
3006
|
+
timestamp: now,
|
|
3007
|
+
mode: req.mode ?? `immediate`,
|
|
3008
|
+
status: req.mode === `queued` || req.mode === `paused` ? `pending` : `processed`
|
|
2706
3009
|
};
|
|
2707
3010
|
if (req.type) value.message_type = req.type;
|
|
3011
|
+
if (req.position) value.position = req.position;
|
|
3012
|
+
else if (value.mode === `queued` || value.mode === `paused`) value.position = createInitialQueuePosition(new Date(now));
|
|
3013
|
+
if (value.status === `processed`) value.processed_at = now;
|
|
2708
3014
|
const envelope = __electric_ax_agents_runtime.entityStateSchema.inbox.insert({
|
|
2709
3015
|
key,
|
|
2710
3016
|
value
|
|
@@ -2716,11 +3022,47 @@ var EntityManager = class {
|
|
|
2716
3022
|
return;
|
|
2717
3023
|
}
|
|
2718
3024
|
await this.streamClient.append(entity.streams.main, encoded);
|
|
3025
|
+
if (entity.type === `principal` && req.type === `update_identity`) {
|
|
3026
|
+
const identity = req.payload?.identity;
|
|
3027
|
+
await this.streamClient.append(entity.streams.main, this.encodeChangeEvent({
|
|
3028
|
+
type: `identity`,
|
|
3029
|
+
key: `self`,
|
|
3030
|
+
value: identity
|
|
3031
|
+
}));
|
|
3032
|
+
}
|
|
2719
3033
|
} catch (err) {
|
|
2720
3034
|
if (this.isClosedStreamError(err)) throw new ElectricAgentsError(ErrCodeNotRunning, `Entity is stopped`, 409);
|
|
2721
3035
|
throw err;
|
|
2722
3036
|
}
|
|
2723
3037
|
}
|
|
3038
|
+
async updateInboxMessage(entityUrl, key, req) {
|
|
3039
|
+
const entity = await this.registry.getEntity(entityUrl);
|
|
3040
|
+
if (!entity) throw new ElectricAgentsError(ErrCodeNotFound, `Entity not found`, 404);
|
|
3041
|
+
if (entity.status === `stopped`) throw new ElectricAgentsError(ErrCodeNotRunning, `Entity is stopped`, 409);
|
|
3042
|
+
const now = new Date().toISOString();
|
|
3043
|
+
const value = {};
|
|
3044
|
+
if (`payload` in req) value.payload = req.payload;
|
|
3045
|
+
if (req.position !== void 0) value.position = req.position;
|
|
3046
|
+
if (req.mode !== void 0) value.mode = req.mode;
|
|
3047
|
+
if (req.status !== void 0) {
|
|
3048
|
+
value.status = req.status;
|
|
3049
|
+
if (req.status === `processed`) value.processed_at = now;
|
|
3050
|
+
if (req.status === `cancelled`) value.cancelled_at = now;
|
|
3051
|
+
}
|
|
3052
|
+
if (Object.keys(value).length === 0) throw new ElectricAgentsError(ErrCodeInvalidRequest, `No inbox fields to update`, 400);
|
|
3053
|
+
const envelope = __electric_ax_agents_runtime.entityStateSchema.inbox.update({
|
|
3054
|
+
key,
|
|
3055
|
+
value
|
|
3056
|
+
});
|
|
3057
|
+
await this.streamClient.append(entity.streams.main, this.encodeChangeEvent(envelope));
|
|
3058
|
+
}
|
|
3059
|
+
async deleteInboxMessage(entityUrl, key) {
|
|
3060
|
+
const entity = await this.registry.getEntity(entityUrl);
|
|
3061
|
+
if (!entity) throw new ElectricAgentsError(ErrCodeNotFound, `Entity not found`, 404);
|
|
3062
|
+
if (entity.status === `stopped`) throw new ElectricAgentsError(ErrCodeNotRunning, `Entity is stopped`, 409);
|
|
3063
|
+
const envelope = __electric_ax_agents_runtime.entityStateSchema.inbox.delete({ key });
|
|
3064
|
+
await this.streamClient.append(entity.streams.main, this.encodeChangeEvent(envelope));
|
|
3065
|
+
}
|
|
2724
3066
|
async setTag(entityUrl, key, req, token) {
|
|
2725
3067
|
const entity = await this.registry.getEntity(entityUrl);
|
|
2726
3068
|
if (!entity) throw new ElectricAgentsError(ErrCodeNotFound, `Entity not found`, 404);
|
|
@@ -2806,7 +3148,7 @@ var EntityManager = class {
|
|
|
2806
3148
|
async upsertFutureSendSchedule(ownerEntityUrl, req) {
|
|
2807
3149
|
if (!this.scheduler) throw new Error(`Scheduler not configured`);
|
|
2808
3150
|
const targetUrl = req.targetUrl ?? ownerEntityUrl;
|
|
2809
|
-
const from = req.
|
|
3151
|
+
const from = req.senderUrl ?? ownerEntityUrl;
|
|
2810
3152
|
const fireAt = new Date(req.fireAt);
|
|
2811
3153
|
if (Number.isNaN(fireAt.getTime())) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Invalid fireAt timestamp: ${req.fireAt}`, 400);
|
|
2812
3154
|
await this.validateSendRequest(targetUrl, {
|
|
@@ -2834,9 +3176,9 @@ var EntityManager = class {
|
|
|
2834
3176
|
scheduleType: `future_send`,
|
|
2835
3177
|
fireAt: fireAt.toISOString(),
|
|
2836
3178
|
targetUrl,
|
|
3179
|
+
senderUrl: from,
|
|
2837
3180
|
payload: req.payload,
|
|
2838
3181
|
producerId,
|
|
2839
|
-
...req.from ? { from: req.from } : {},
|
|
2840
3182
|
...req.messageType ? { messageType: req.messageType } : {},
|
|
2841
3183
|
status: `pending`
|
|
2842
3184
|
}
|
|
@@ -2850,9 +3192,9 @@ var EntityManager = class {
|
|
|
2850
3192
|
scheduleType: `future_send`,
|
|
2851
3193
|
fireAt: fireAt.toISOString(),
|
|
2852
3194
|
targetUrl,
|
|
3195
|
+
senderUrl: from,
|
|
2853
3196
|
payload: req.payload,
|
|
2854
3197
|
producerId,
|
|
2855
|
-
...req.from ? { from: req.from } : {},
|
|
2856
3198
|
...req.messageType ? { messageType: req.messageType } : {},
|
|
2857
3199
|
status: `pending`
|
|
2858
3200
|
}, { txid });
|
|
@@ -2890,7 +3232,9 @@ var EntityManager = class {
|
|
|
2890
3232
|
from: req.from,
|
|
2891
3233
|
payload: req.payload,
|
|
2892
3234
|
key: req.key,
|
|
2893
|
-
type: req.type
|
|
3235
|
+
type: req.type,
|
|
3236
|
+
mode: req.mode,
|
|
3237
|
+
position: req.position
|
|
2894
3238
|
}, fireAt);
|
|
2895
3239
|
}
|
|
2896
3240
|
/**
|
|
@@ -3040,6 +3384,7 @@ var EntityManager = class {
|
|
|
3040
3384
|
* Add new input/output schema keys to an entity type directly in Postgres.
|
|
3041
3385
|
*/
|
|
3042
3386
|
async amendSchemas(typeName, schemas) {
|
|
3387
|
+
if (typeName === `principal`) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Entity type "principal" is built in and cannot be amended`, 400);
|
|
3043
3388
|
this.validateSchemaMap(schemas.inbox_schemas);
|
|
3044
3389
|
this.validateSchemaMap(schemas.state_schemas);
|
|
3045
3390
|
const existing = await this.registry.getEntityType(typeName);
|
|
@@ -3089,9 +3434,11 @@ var EntityManager = class {
|
|
|
3089
3434
|
url: entity.url,
|
|
3090
3435
|
streams: entity.streams,
|
|
3091
3436
|
tags: entity.tags,
|
|
3092
|
-
spawnArgs: entity.spawn_args
|
|
3437
|
+
spawnArgs: entity.spawn_args,
|
|
3438
|
+
createdBy: entity.created_by
|
|
3093
3439
|
},
|
|
3094
|
-
|
|
3440
|
+
principal: principalFromCreatedBy(entity.created_by),
|
|
3441
|
+
triggerEvent: `inbox`
|
|
3095
3442
|
};
|
|
3096
3443
|
}
|
|
3097
3444
|
validateSchema(schema) {
|
|
@@ -3131,6 +3478,7 @@ var EntityManager = class {
|
|
|
3131
3478
|
}
|
|
3132
3479
|
}
|
|
3133
3480
|
if (!req.from) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Missing required field: from`, 400);
|
|
3481
|
+
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);
|
|
3134
3482
|
if (req.payload === void 0) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Missing required field: payload`, 400);
|
|
3135
3483
|
return entity;
|
|
3136
3484
|
}
|
|
@@ -4530,6 +4878,7 @@ var ElectricAgentsTenantRuntime = class {
|
|
|
4530
4878
|
const fireAtRaw = value.fireAt;
|
|
4531
4879
|
const producerId = value.producerId;
|
|
4532
4880
|
const targetUrl = value.targetUrl;
|
|
4881
|
+
const senderUrl = typeof value.senderUrl === `string` ? value.senderUrl : ownerEntityUrl;
|
|
4533
4882
|
if (typeof fireAtRaw !== `string` || typeof producerId !== `string` || typeof targetUrl !== `string`) {
|
|
4534
4883
|
serverLog.warn(`[agent-server] invalid future_send manifest entry for ${ownerEntityUrl}/${manifestKey}`);
|
|
4535
4884
|
return;
|
|
@@ -4541,7 +4890,7 @@ var ElectricAgentsTenantRuntime = class {
|
|
|
4541
4890
|
}
|
|
4542
4891
|
await this.scheduler.syncManifestDelayedSend(ownerEntityUrl, manifestKey, {
|
|
4543
4892
|
entityUrl: targetUrl,
|
|
4544
|
-
from:
|
|
4893
|
+
from: senderUrl,
|
|
4545
4894
|
payload: value.payload,
|
|
4546
4895
|
key: `scheduled-${producerId}`,
|
|
4547
4896
|
type: typeof value.messageType === `string` ? value.messageType : void 0,
|
|
@@ -4555,6 +4904,7 @@ var ElectricAgentsTenantRuntime = class {
|
|
|
4555
4904
|
kind: `schedule`,
|
|
4556
4905
|
scheduleType: `future_send`,
|
|
4557
4906
|
targetUrl,
|
|
4907
|
+
senderUrl,
|
|
4558
4908
|
fireAt: fireAt.toISOString(),
|
|
4559
4909
|
producerId,
|
|
4560
4910
|
status: `pending`
|
|
@@ -5403,6 +5753,7 @@ var AgentsHost = class {
|
|
|
5403
5753
|
for (const runtime of runtimes) await this.startTenantRuntime(runtime);
|
|
5404
5754
|
}
|
|
5405
5755
|
async startTenantRuntime(runtime) {
|
|
5756
|
+
await runtime.manager.ensurePrincipalEntityType();
|
|
5406
5757
|
if (this.rehydrateTenantOnStart) await runtime.rehydrateCronSchedules();
|
|
5407
5758
|
if (this.startEntityBridgeManager) await this.entityProjector.loadTenantBridges(runtime.serviceId, runtime.registry);
|
|
5408
5759
|
}
|
|
@@ -5420,6 +5771,7 @@ var AgentsHost = class {
|
|
|
5420
5771
|
scheduler,
|
|
5421
5772
|
entityBridgeManager: this.entityProjector.forTenant(serviceId, registry)
|
|
5422
5773
|
});
|
|
5774
|
+
await runtime.manager.ensurePrincipalEntityType();
|
|
5423
5775
|
return runtime;
|
|
5424
5776
|
}
|
|
5425
5777
|
createStreamClient(config) {
|
|
@@ -5602,30 +5954,6 @@ function validateParsedBody(schema, parsed) {
|
|
|
5602
5954
|
};
|
|
5603
5955
|
}
|
|
5604
5956
|
|
|
5605
|
-
//#endregion
|
|
5606
|
-
//#region src/utils/webhook-url.ts
|
|
5607
|
-
function rewriteLoopbackWebhookUrl(value) {
|
|
5608
|
-
if (!value) return void 0;
|
|
5609
|
-
const rewriteTarget = process.env.ELECTRIC_AGENTS_REWRITE_LOOPBACK_WEBHOOKS_TO?.trim();
|
|
5610
|
-
if (!rewriteTarget) return value;
|
|
5611
|
-
const url = new URL(value);
|
|
5612
|
-
if (!isLoopbackHostname(url.hostname)) return value;
|
|
5613
|
-
if (rewriteTarget.includes(`://`)) {
|
|
5614
|
-
const target = new URL(rewriteTarget);
|
|
5615
|
-
url.protocol = target.protocol;
|
|
5616
|
-
url.username = target.username;
|
|
5617
|
-
url.password = target.password;
|
|
5618
|
-
url.hostname = target.hostname;
|
|
5619
|
-
url.port = target.port;
|
|
5620
|
-
return url.toString();
|
|
5621
|
-
}
|
|
5622
|
-
url.host = rewriteTarget;
|
|
5623
|
-
return url.toString();
|
|
5624
|
-
}
|
|
5625
|
-
function isLoopbackHostname(hostname) {
|
|
5626
|
-
return hostname === `localhost` || hostname === `127.0.0.1` || hostname === `::1`;
|
|
5627
|
-
}
|
|
5628
|
-
|
|
5629
5957
|
//#endregion
|
|
5630
5958
|
//#region src/routing/tenant-stream-paths.ts
|
|
5631
5959
|
function withoutLeadingSlash(path$2) {
|
|
@@ -5985,122 +6313,6 @@ async function proxyElectric(request, ctx) {
|
|
|
5985
6313
|
});
|
|
5986
6314
|
}
|
|
5987
6315
|
|
|
5988
|
-
//#endregion
|
|
5989
|
-
//#region src/routing/dispatch-policy.ts
|
|
5990
|
-
function subscriptionIdForDispatchTarget(target) {
|
|
5991
|
-
if (target.subscription_id) return target.subscription_id;
|
|
5992
|
-
if (target.type === `runner`) return `runner:${target.runnerId}`;
|
|
5993
|
-
const digest = (0, node_crypto.createHash)(`sha256`).update(target.url).digest(`hex`);
|
|
5994
|
-
return `webhook:${digest.slice(0, 16)}`;
|
|
5995
|
-
}
|
|
5996
|
-
function subscriptionIdForEntityDispatchTarget(target, entityUrl) {
|
|
5997
|
-
const base = subscriptionIdForDispatchTarget(target);
|
|
5998
|
-
if (!target.subscription_id) return base;
|
|
5999
|
-
const digest = (0, node_crypto.createHash)(`sha256`).update(entityUrl).digest(`hex`);
|
|
6000
|
-
return `${base}:${digest.slice(0, 16)}`;
|
|
6001
|
-
}
|
|
6002
|
-
async function resolveEffectiveDispatchPolicyForSpawn(ctx, typeName, opts) {
|
|
6003
|
-
if (opts.dispatchPolicy) return opts.dispatchPolicy;
|
|
6004
|
-
const entityType = await ctx.entityManager.registry.getEntityType(typeName);
|
|
6005
|
-
if (opts.parent) {
|
|
6006
|
-
const parent = await ctx.entityManager.registry.getEntity(opts.parent);
|
|
6007
|
-
if (parent?.dispatch_policy) return applyTypeDefaultSubscriptionScope(parent.dispatch_policy, entityType?.default_dispatch_policy);
|
|
6008
|
-
}
|
|
6009
|
-
return entityType?.default_dispatch_policy;
|
|
6010
|
-
}
|
|
6011
|
-
async function resolveEffectiveDispatchPolicyForEntity(ctx, entity) {
|
|
6012
|
-
if (entity.dispatch_policy) return entity.dispatch_policy;
|
|
6013
|
-
const entityType = await ctx.entityManager.registry.getEntityType(entity.type);
|
|
6014
|
-
return entityType?.default_dispatch_policy;
|
|
6015
|
-
}
|
|
6016
|
-
async function backfillEntityDispatchPolicy(ctx, entity) {
|
|
6017
|
-
if (entity.dispatch_policy) return entity;
|
|
6018
|
-
const dispatchPolicy = await resolveEffectiveDispatchPolicyForEntity(ctx, entity);
|
|
6019
|
-
if (!dispatchPolicy) return entity;
|
|
6020
|
-
return await ctx.entityManager.registry.updateEntityDispatchPolicy(entity.url, dispatchPolicy) ?? {
|
|
6021
|
-
...entity,
|
|
6022
|
-
dispatch_policy: dispatchPolicy
|
|
6023
|
-
};
|
|
6024
|
-
}
|
|
6025
|
-
function applyTypeDefaultSubscriptionScope(policy, typeDefault) {
|
|
6026
|
-
const target = policy.targets[0];
|
|
6027
|
-
const defaultTarget = typeDefault?.targets[0];
|
|
6028
|
-
if (!target || !defaultTarget?.subscription_id) return policy;
|
|
6029
|
-
if (!sameDispatchDestination(target, defaultTarget)) return policy;
|
|
6030
|
-
if (target.subscription_id === defaultTarget.subscription_id) return policy;
|
|
6031
|
-
return { targets: [{
|
|
6032
|
-
...target,
|
|
6033
|
-
subscription_id: defaultTarget.subscription_id
|
|
6034
|
-
}] };
|
|
6035
|
-
}
|
|
6036
|
-
function sameDispatchDestination(a, b) {
|
|
6037
|
-
if (a.type !== b.type) return false;
|
|
6038
|
-
if (a.type === `runner` && b.type === `runner`) return a.runnerId === b.runnerId;
|
|
6039
|
-
if (a.type === `webhook` && b.type === `webhook`) return a.url === b.url;
|
|
6040
|
-
return false;
|
|
6041
|
-
}
|
|
6042
|
-
async function assertDispatchPolicyAllowed(ctx, policy) {
|
|
6043
|
-
const target = policy?.targets[0];
|
|
6044
|
-
if (!target || target.type !== `runner`) return;
|
|
6045
|
-
const runner = await ctx.entityManager.registry.getRunner(target.runnerId);
|
|
6046
|
-
if (!runner) throw new ElectricAgentsError(ErrCodeNotFound, `Runner "${target.runnerId}" not found`, 404);
|
|
6047
|
-
if (!ctx.authenticatedUser) throw new ElectricAgentsError(ErrCodeUnauthorized, `Authentication is required for runner-targeted dispatch`, 401);
|
|
6048
|
-
if (runner.owner_user_id !== ctx.authenticatedUser.userId) throw new ElectricAgentsError(ErrCodeUnauthorized, `Runner dispatch requires the authenticated owner`, 403);
|
|
6049
|
-
}
|
|
6050
|
-
async function linkEntityDispatchSubscription(ctx, entity) {
|
|
6051
|
-
const dispatchPolicy = await resolveEffectiveDispatchPolicyForEntity(ctx, entity);
|
|
6052
|
-
const target = dispatchPolicy?.targets[0];
|
|
6053
|
-
if (!target) return;
|
|
6054
|
-
await linkStreamToTargetSubscription(ctx, target, entity);
|
|
6055
|
-
}
|
|
6056
|
-
async function unlinkEntityDispatchSubscription(ctx, entity) {
|
|
6057
|
-
const dispatchPolicy = await resolveEffectiveDispatchPolicyForEntity(ctx, entity);
|
|
6058
|
-
const target = dispatchPolicy?.targets[0];
|
|
6059
|
-
if (!target) return;
|
|
6060
|
-
const subscriptionId = subscriptionIdForEntityDispatchTarget(target, entity.url);
|
|
6061
|
-
await ctx.streamClient.removeSubscriptionStream(subscriptionId, entity.streams.main).catch(() => {});
|
|
6062
|
-
}
|
|
6063
|
-
async function linkStreamToTargetSubscription(ctx, target, entity) {
|
|
6064
|
-
const streamPath = entity.streams.main;
|
|
6065
|
-
const subscriptionId = subscriptionIdForEntityDispatchTarget(target, entity.url);
|
|
6066
|
-
const existing = await ctx.streamClient.getSubscription(subscriptionId);
|
|
6067
|
-
if (target.type === `runner`) {
|
|
6068
|
-
const runner = await ctx.entityManager.registry.getRunner(target.runnerId);
|
|
6069
|
-
if (!runner) throw new ElectricAgentsError(ErrCodeNotFound, `Runner "${target.runnerId}" not found`, 404);
|
|
6070
|
-
const wakeStream = runner.wake_stream || runnerWakeStream(target.runnerId);
|
|
6071
|
-
await ctx.streamClient.ensure(wakeStream, { contentType: `application/json` });
|
|
6072
|
-
if (!existing) {
|
|
6073
|
-
await ctx.streamClient.putSubscription(subscriptionId, {
|
|
6074
|
-
type: `pull-wake`,
|
|
6075
|
-
streams: [streamPath],
|
|
6076
|
-
wake_stream: wakeStream,
|
|
6077
|
-
description: `Electric Agents runner ${target.runnerId}`
|
|
6078
|
-
});
|
|
6079
|
-
return;
|
|
6080
|
-
}
|
|
6081
|
-
await ctx.streamClient.addSubscriptionStreams(subscriptionId, [streamPath]);
|
|
6082
|
-
return;
|
|
6083
|
-
}
|
|
6084
|
-
const webhookUrl = rewriteLoopbackWebhookUrl(target.url);
|
|
6085
|
-
if (!webhookUrl) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Webhook dispatch target must include a valid URL`, 400);
|
|
6086
|
-
const forwardUrl = (0, __electric_ax_agents_runtime.appendPathToUrl)(ctx.publicUrl, `/_electric/webhook-forward/${encodeURIComponent(subscriptionId)}`);
|
|
6087
|
-
if (!existing) await ctx.streamClient.putSubscription(subscriptionId, {
|
|
6088
|
-
type: `webhook`,
|
|
6089
|
-
streams: [streamPath],
|
|
6090
|
-
webhook: { url: forwardUrl },
|
|
6091
|
-
description: `Electric Agents webhook ${subscriptionId}`
|
|
6092
|
-
});
|
|
6093
|
-
else await ctx.streamClient.addSubscriptionStreams(subscriptionId, [streamPath]);
|
|
6094
|
-
await ctx.pgDb.insert(subscriptionWebhooks).values({
|
|
6095
|
-
tenantId: ctx.service,
|
|
6096
|
-
subscriptionId,
|
|
6097
|
-
webhookUrl
|
|
6098
|
-
}).onConflictDoUpdate({
|
|
6099
|
-
target: [subscriptionWebhooks.tenantId, subscriptionWebhooks.subscriptionId],
|
|
6100
|
-
set: { webhookUrl }
|
|
6101
|
-
});
|
|
6102
|
-
}
|
|
6103
|
-
|
|
6104
6316
|
//#endregion
|
|
6105
6317
|
//#region src/routing/entities-router.ts
|
|
6106
6318
|
const stringRecordSchema = __sinclair_typebox.Type.Record(__sinclair_typebox.Type.String(), __sinclair_typebox.Type.String());
|
|
@@ -6133,11 +6345,33 @@ const spawnBodySchema = __sinclair_typebox.Type.Object({
|
|
|
6133
6345
|
}))
|
|
6134
6346
|
});
|
|
6135
6347
|
const sendBodySchema = __sinclair_typebox.Type.Object({
|
|
6136
|
-
from: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
|
|
6137
6348
|
payload: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Unknown()),
|
|
6138
6349
|
key: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
|
|
6139
6350
|
type: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
|
|
6140
|
-
|
|
6351
|
+
mode: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Union([
|
|
6352
|
+
__sinclair_typebox.Type.Literal(`immediate`),
|
|
6353
|
+
__sinclair_typebox.Type.Literal(`queued`),
|
|
6354
|
+
__sinclair_typebox.Type.Literal(`paused`),
|
|
6355
|
+
__sinclair_typebox.Type.Literal(`steer`)
|
|
6356
|
+
])),
|
|
6357
|
+
position: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
|
|
6358
|
+
afterMs: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Number()),
|
|
6359
|
+
from: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String())
|
|
6360
|
+
});
|
|
6361
|
+
const inboxMessageBodySchema = __sinclair_typebox.Type.Object({
|
|
6362
|
+
payload: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Unknown()),
|
|
6363
|
+
position: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
|
|
6364
|
+
mode: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Union([
|
|
6365
|
+
__sinclair_typebox.Type.Literal(`immediate`),
|
|
6366
|
+
__sinclair_typebox.Type.Literal(`queued`),
|
|
6367
|
+
__sinclair_typebox.Type.Literal(`paused`),
|
|
6368
|
+
__sinclair_typebox.Type.Literal(`steer`)
|
|
6369
|
+
])),
|
|
6370
|
+
status: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Union([
|
|
6371
|
+
__sinclair_typebox.Type.Literal(`pending`),
|
|
6372
|
+
__sinclair_typebox.Type.Literal(`processed`),
|
|
6373
|
+
__sinclair_typebox.Type.Literal(`cancelled`)
|
|
6374
|
+
]))
|
|
6141
6375
|
});
|
|
6142
6376
|
const forkBodySchema = __sinclair_typebox.Type.Object({
|
|
6143
6377
|
instance_id: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
|
|
@@ -6156,8 +6390,8 @@ const scheduleBodySchema = __sinclair_typebox.Type.Union([__sinclair_typebox.Typ
|
|
|
6156
6390
|
payload: __sinclair_typebox.Type.Unknown(),
|
|
6157
6391
|
targetUrl: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
|
|
6158
6392
|
fireAt: __sinclair_typebox.Type.String(),
|
|
6159
|
-
|
|
6160
|
-
|
|
6393
|
+
messageType: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
|
|
6394
|
+
from: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String())
|
|
6161
6395
|
})]);
|
|
6162
6396
|
const entitiesRegisterBodySchema = __sinclair_typebox.Type.Object({ tags: __sinclair_typebox.Type.Optional(stringRecordSchema) });
|
|
6163
6397
|
const entitiesRouter = (0, itty_router.Router)({ base: `/_electric/entities` });
|
|
@@ -6168,6 +6402,8 @@ entitiesRouter.get(`/:type/:instanceId`, withExistingEntity, getEntity);
|
|
|
6168
6402
|
entitiesRouter.head(`/:type/:instanceId`, withExistingEntity, headEntity);
|
|
6169
6403
|
entitiesRouter.delete(`/:type/:instanceId`, withExistingEntity, killEntity);
|
|
6170
6404
|
entitiesRouter.post(`/:type/:instanceId/send`, withExistingEntity, withSchema(sendBodySchema), sendEntity);
|
|
6405
|
+
entitiesRouter.patch(`/:type/:instanceId/inbox/:messageKey`, withExistingEntity, withSchema(inboxMessageBodySchema), updateInboxMessage);
|
|
6406
|
+
entitiesRouter.delete(`/:type/:instanceId/inbox/:messageKey`, withExistingEntity, deleteInboxMessage);
|
|
6171
6407
|
entitiesRouter.post(`/:type/:instanceId/fork`, withExistingEntity, withSchema(forkBodySchema), forkEntity);
|
|
6172
6408
|
entitiesRouter.post(`/:type/:instanceId/tags/:tagKey`, withExistingEntity, withSchema(setTagBodySchema), setTag);
|
|
6173
6409
|
entitiesRouter.delete(`/:type/:instanceId/tags/:tagKey`, withExistingEntity, removeTag);
|
|
@@ -6176,6 +6412,11 @@ entitiesRouter.delete(`/:type/:instanceId/schedules/:scheduleId`, withExistingEn
|
|
|
6176
6412
|
function entityUrlFromSegments(type, instanceId) {
|
|
6177
6413
|
if (!type || !instanceId) return null;
|
|
6178
6414
|
if (type.startsWith(`_`) || type.includes(`*`) || instanceId.includes(`*`)) return null;
|
|
6415
|
+
if (type === `principal`) try {
|
|
6416
|
+
return principalUrl(decodeURIComponent(instanceId));
|
|
6417
|
+
} catch {
|
|
6418
|
+
return null;
|
|
6419
|
+
}
|
|
6179
6420
|
return `/${type}/${instanceId}`;
|
|
6180
6421
|
}
|
|
6181
6422
|
function firstQueryValue$1(value) {
|
|
@@ -6185,12 +6426,27 @@ function requireExistingEntityRoute(request) {
|
|
|
6185
6426
|
if (!request.entityRoute) throw new Error(`existing entity middleware did not run`);
|
|
6186
6427
|
return request.entityRoute;
|
|
6187
6428
|
}
|
|
6429
|
+
function rejectPrincipalEntityMutation(request, action) {
|
|
6430
|
+
const { entity } = requireExistingEntityRoute(request);
|
|
6431
|
+
if (entity.type !== `principal`) return void 0;
|
|
6432
|
+
return apiError(400, ErrCodeInvalidRequest, `Principal entities are built in and cannot be ${action}`);
|
|
6433
|
+
}
|
|
6188
6434
|
async function withExistingEntity(request, ctx) {
|
|
6189
6435
|
const entityUrl = entityUrlFromSegments(request.params.type, request.params.instanceId);
|
|
6190
6436
|
if (!entityUrl) return void 0;
|
|
6191
6437
|
const entity = await ctx.entityManager.registry.getEntity(entityUrl);
|
|
6192
6438
|
if (!entity) {
|
|
6193
6439
|
const entityType = await ctx.entityManager.registry.getEntityType(request.params.type);
|
|
6440
|
+
if (request.params.type === `principal`) try {
|
|
6441
|
+
const materialized = await ctx.entityManager.ensurePrincipal(parsePrincipalKey(decodeURIComponent(request.params.instanceId)));
|
|
6442
|
+
request.entityRoute = {
|
|
6443
|
+
entityUrl,
|
|
6444
|
+
entity: materialized
|
|
6445
|
+
};
|
|
6446
|
+
return void 0;
|
|
6447
|
+
} catch (error) {
|
|
6448
|
+
return apiError(400, ErrCodeInvalidRequest, error instanceof Error ? error.message : `Invalid principal`);
|
|
6449
|
+
}
|
|
6194
6450
|
if (entityType) return apiError(404, ErrCodeNotFound, `Entity not found at ${entityUrl}`);
|
|
6195
6451
|
return apiError(404, ErrCodeUnknownEntityType, `Entity type "${request.params.type}" not found`);
|
|
6196
6452
|
}
|
|
@@ -6202,6 +6458,7 @@ async function withExistingEntity(request, ctx) {
|
|
|
6202
6458
|
}
|
|
6203
6459
|
async function withSpawnableEntityType(request, ctx) {
|
|
6204
6460
|
if (!entityUrlFromSegments(request.params.type, request.params.instanceId)) return void 0;
|
|
6461
|
+
if (request.params.type === `principal`) return apiError(400, ErrCodeInvalidRequest, `Principal entities are built in and cannot be spawned directly`);
|
|
6205
6462
|
const entityType = await ctx.entityManager.registry.getEntityType(request.params.type);
|
|
6206
6463
|
if (!entityType) return apiError(404, ErrCodeUnknownEntityType, `Entity type "${request.params.type}" not found`);
|
|
6207
6464
|
return void 0;
|
|
@@ -6210,7 +6467,8 @@ async function listEntities({ query }, ctx) {
|
|
|
6210
6467
|
const { entities: entities$1 } = await ctx.entityManager.registry.listEntities({
|
|
6211
6468
|
type: firstQueryValue$1(query.type),
|
|
6212
6469
|
status: firstQueryValue$1(query.status),
|
|
6213
|
-
parent: firstQueryValue$1(query.parent)
|
|
6470
|
+
parent: firstQueryValue$1(query.parent),
|
|
6471
|
+
created_by: firstQueryValue$1(query.created_by)
|
|
6214
6472
|
});
|
|
6215
6473
|
return (0, itty_router.json)(entities$1.map((entity) => toPublicEntity(entity)));
|
|
6216
6474
|
}
|
|
@@ -6220,6 +6478,8 @@ async function registerEntitiesSource(request, ctx) {
|
|
|
6220
6478
|
return (0, itty_router.json)(result);
|
|
6221
6479
|
}
|
|
6222
6480
|
async function upsertSchedule(request, ctx) {
|
|
6481
|
+
const principalMutationError = rejectPrincipalEntityMutation(request, `scheduled`);
|
|
6482
|
+
if (principalMutationError) return principalMutationError;
|
|
6223
6483
|
const parsed = routeBody(request);
|
|
6224
6484
|
const { entityUrl } = requireExistingEntityRoute(request);
|
|
6225
6485
|
const scheduleId = decodeURIComponent(request.params.scheduleId);
|
|
@@ -6235,12 +6495,13 @@ async function upsertSchedule(request, ctx) {
|
|
|
6235
6495
|
return (0, itty_router.json)(result);
|
|
6236
6496
|
}
|
|
6237
6497
|
if (parsed.scheduleType === `future_send`) {
|
|
6498
|
+
if (parsed.from !== void 0 && parsed.from !== ctx.principal.url) return apiError(400, ErrCodeInvalidRequest, `Request from must match Electric-Principal`);
|
|
6238
6499
|
const result = await ctx.entityManager.upsertFutureSendSchedule(entityUrl, {
|
|
6239
6500
|
id: scheduleId,
|
|
6240
6501
|
payload: parsed.payload,
|
|
6241
6502
|
targetUrl: parsed.targetUrl,
|
|
6242
6503
|
fireAt: parsed.fireAt,
|
|
6243
|
-
|
|
6504
|
+
senderUrl: ctx.principal.url,
|
|
6244
6505
|
messageType: parsed.messageType
|
|
6245
6506
|
});
|
|
6246
6507
|
return (0, itty_router.json)(result);
|
|
@@ -6248,11 +6509,15 @@ async function upsertSchedule(request, ctx) {
|
|
|
6248
6509
|
throw new Error(`schedule schema accepted an unknown scheduleType`);
|
|
6249
6510
|
}
|
|
6250
6511
|
async function deleteSchedule(request, ctx) {
|
|
6512
|
+
const principalMutationError = rejectPrincipalEntityMutation(request, `unscheduled`);
|
|
6513
|
+
if (principalMutationError) return principalMutationError;
|
|
6251
6514
|
const { entityUrl } = requireExistingEntityRoute(request);
|
|
6252
6515
|
const result = await ctx.entityManager.deleteSchedule(entityUrl, { id: decodeURIComponent(request.params.scheduleId) });
|
|
6253
6516
|
return (0, itty_router.json)(result);
|
|
6254
6517
|
}
|
|
6255
6518
|
async function setTag(request, ctx) {
|
|
6519
|
+
const principalMutationError = rejectPrincipalEntityMutation(request, `tagged`);
|
|
6520
|
+
if (principalMutationError) return principalMutationError;
|
|
6256
6521
|
const parsed = routeBody(request);
|
|
6257
6522
|
const { entityUrl } = requireExistingEntityRoute(request);
|
|
6258
6523
|
const token = writeTokenFromRequest(request);
|
|
@@ -6260,12 +6525,16 @@ async function setTag(request, ctx) {
|
|
|
6260
6525
|
return (0, itty_router.json)(toPublicEntity(updated));
|
|
6261
6526
|
}
|
|
6262
6527
|
async function removeTag(request, ctx) {
|
|
6528
|
+
const principalMutationError = rejectPrincipalEntityMutation(request, `untagged`);
|
|
6529
|
+
if (principalMutationError) return principalMutationError;
|
|
6263
6530
|
const { entityUrl } = requireExistingEntityRoute(request);
|
|
6264
6531
|
const token = writeTokenFromRequest(request);
|
|
6265
6532
|
const updated = await ctx.entityManager.removeTag(entityUrl, decodeURIComponent(request.params.tagKey), token);
|
|
6266
6533
|
return (0, itty_router.json)(toPublicEntity(updated));
|
|
6267
6534
|
}
|
|
6268
6535
|
async function forkEntity(request, ctx) {
|
|
6536
|
+
const principalMutationError = rejectPrincipalEntityMutation(request, `forked`);
|
|
6537
|
+
if (principalMutationError) return principalMutationError;
|
|
6269
6538
|
const parsed = routeBody(request);
|
|
6270
6539
|
const { entityUrl, entity } = requireExistingEntityRoute(request);
|
|
6271
6540
|
await assertDispatchPolicyAllowed(ctx, entity.dispatch_policy);
|
|
@@ -6281,27 +6550,47 @@ async function forkEntity(request, ctx) {
|
|
|
6281
6550
|
}
|
|
6282
6551
|
async function sendEntity(request, ctx) {
|
|
6283
6552
|
const parsed = routeBody(request);
|
|
6553
|
+
const principal = ctx.principal;
|
|
6554
|
+
if (parsed.from !== void 0 && parsed.from !== principal.url) return apiError(400, ErrCodeInvalidRequest, `Request from must match Electric-Principal`);
|
|
6555
|
+
await ctx.entityManager.ensurePrincipal(principal);
|
|
6284
6556
|
const { entityUrl, entity } = requireExistingEntityRoute(request);
|
|
6285
6557
|
if (!entity.dispatch_policy) {
|
|
6286
6558
|
const updatedEntity = await backfillEntityDispatchPolicy(ctx, entity);
|
|
6287
6559
|
await linkEntityDispatchSubscription(ctx, updatedEntity);
|
|
6288
6560
|
}
|
|
6289
6561
|
if (parsed.afterMs && parsed.afterMs > 0) await ctx.entityManager.enqueueDelayedSend(entityUrl, {
|
|
6290
|
-
from:
|
|
6562
|
+
from: principal.url,
|
|
6291
6563
|
payload: parsed.payload,
|
|
6292
6564
|
key: parsed.key,
|
|
6293
|
-
type: parsed.type
|
|
6565
|
+
type: parsed.type,
|
|
6566
|
+
mode: parsed.mode,
|
|
6567
|
+
position: parsed.position
|
|
6294
6568
|
}, new Date(Date.now() + parsed.afterMs));
|
|
6295
6569
|
else await ctx.entityManager.send(entityUrl, {
|
|
6296
|
-
from:
|
|
6570
|
+
from: principal.url,
|
|
6297
6571
|
payload: parsed.payload,
|
|
6298
6572
|
key: parsed.key,
|
|
6299
|
-
type: parsed.type
|
|
6573
|
+
type: parsed.type,
|
|
6574
|
+
mode: parsed.mode,
|
|
6575
|
+
position: parsed.position
|
|
6300
6576
|
});
|
|
6301
6577
|
return (0, itty_router.status)(204);
|
|
6302
6578
|
}
|
|
6579
|
+
async function updateInboxMessage(request, ctx) {
|
|
6580
|
+
const parsed = routeBody(request);
|
|
6581
|
+
const { entityUrl } = requireExistingEntityRoute(request);
|
|
6582
|
+
await ctx.entityManager.updateInboxMessage(entityUrl, decodeURIComponent(request.params.messageKey), parsed);
|
|
6583
|
+
return (0, itty_router.status)(204);
|
|
6584
|
+
}
|
|
6585
|
+
async function deleteInboxMessage(request, ctx) {
|
|
6586
|
+
const { entityUrl } = requireExistingEntityRoute(request);
|
|
6587
|
+
await ctx.entityManager.deleteInboxMessage(entityUrl, decodeURIComponent(request.params.messageKey));
|
|
6588
|
+
return (0, itty_router.status)(204);
|
|
6589
|
+
}
|
|
6303
6590
|
async function spawnEntity(request, ctx) {
|
|
6304
6591
|
const parsed = routeBody(request);
|
|
6592
|
+
const principal = ctx.principal;
|
|
6593
|
+
await ctx.entityManager.ensurePrincipal(principal);
|
|
6305
6594
|
const dispatchPolicy = await resolveEffectiveDispatchPolicyForSpawn(ctx, request.params.type, {
|
|
6306
6595
|
dispatchPolicy: parsed.dispatch_policy,
|
|
6307
6596
|
parent: parsed.parent
|
|
@@ -6314,11 +6603,12 @@ async function spawnEntity(request, ctx) {
|
|
|
6314
6603
|
parent: parsed.parent,
|
|
6315
6604
|
dispatch_policy: dispatchPolicy,
|
|
6316
6605
|
initialMessage: void 0,
|
|
6317
|
-
wake: parsed.wake
|
|
6606
|
+
wake: parsed.wake,
|
|
6607
|
+
created_by: principal.url
|
|
6318
6608
|
});
|
|
6319
6609
|
await linkEntityDispatchSubscription(ctx, entity);
|
|
6320
6610
|
if (parsed.initialMessage !== void 0) await ctx.entityManager.send(entity.url, {
|
|
6321
|
-
from:
|
|
6611
|
+
from: principal.url,
|
|
6322
6612
|
payload: parsed.initialMessage
|
|
6323
6613
|
});
|
|
6324
6614
|
return (0, itty_router.json)({
|
|
@@ -6336,6 +6626,8 @@ function headEntity() {
|
|
|
6336
6626
|
return (0, itty_router.status)(200);
|
|
6337
6627
|
}
|
|
6338
6628
|
async function killEntity(request, ctx) {
|
|
6629
|
+
const principalMutationError = rejectPrincipalEntityMutation(request, `killed`);
|
|
6630
|
+
if (principalMutationError) return principalMutationError;
|
|
6339
6631
|
const { entityUrl, entity } = requireExistingEntityRoute(request);
|
|
6340
6632
|
await unlinkEntityDispatchSubscription(ctx, entity);
|
|
6341
6633
|
const result = await ctx.entityManager.kill(entityUrl);
|
|
@@ -6482,7 +6774,7 @@ function applyCors(response) {
|
|
|
6482
6774
|
const headers = new Headers(response.headers);
|
|
6483
6775
|
headers.set(`access-control-allow-origin`, `*`);
|
|
6484
6776
|
headers.set(`access-control-allow-methods`, `GET, POST, PUT, PATCH, DELETE, OPTIONS`);
|
|
6485
|
-
headers.set(`access-control-allow-headers`, `content-type, authorization, electric-claim-token,
|
|
6777
|
+
headers.set(`access-control-allow-headers`, `content-type, authorization, electric-claim-token, ngrok-skip-browser-warning`);
|
|
6486
6778
|
headers.set(`access-control-expose-headers`, `*`);
|
|
6487
6779
|
return new Response(response.body, {
|
|
6488
6780
|
status: response.status,
|
|
@@ -6562,9 +6854,9 @@ function firstQueryValue(value) {
|
|
|
6562
6854
|
}
|
|
6563
6855
|
async function registerRunner(request, ctx) {
|
|
6564
6856
|
const parsed = routeBody(request);
|
|
6565
|
-
const ownerUserId = parsed.owner_user_id ?? ctx.
|
|
6857
|
+
const ownerUserId = parsed.owner_user_id ?? ctx.principal?.key;
|
|
6566
6858
|
if (!ownerUserId) throw new ElectricAgentsError(ErrCodeInvalidRequest, `owner_user_id is required when no authenticated user is present`, 400);
|
|
6567
|
-
if (ctx.
|
|
6859
|
+
if (ctx.principal && ownerUserId !== ctx.principal.key) throw new ElectricAgentsError(ErrCodeUnauthorized, `owner_user_id must match the authenticated user`, 403);
|
|
6568
6860
|
const runner = await ctx.entityManager.registry.createRunner({
|
|
6569
6861
|
id: parsed.id,
|
|
6570
6862
|
ownerUserId,
|
|
@@ -6578,8 +6870,8 @@ async function registerRunner(request, ctx) {
|
|
|
6578
6870
|
}
|
|
6579
6871
|
async function listRunners(request, ctx) {
|
|
6580
6872
|
const requestedOwner = firstQueryValue(request.query.owner_user_id);
|
|
6581
|
-
if (ctx.
|
|
6582
|
-
const runners$1 = await ctx.entityManager.registry.listRunners({ ownerUserId: ctx.
|
|
6873
|
+
if (ctx.principal && requestedOwner && requestedOwner !== ctx.principal.key) throw new ElectricAgentsError(ErrCodeUnauthorized, `owner_user_id must match the authenticated user`, 403);
|
|
6874
|
+
const runners$1 = await ctx.entityManager.registry.listRunners({ ownerUserId: ctx.principal?.key ?? requestedOwner });
|
|
6583
6875
|
return (0, itty_router.json)(runners$1);
|
|
6584
6876
|
}
|
|
6585
6877
|
async function getRunner(request, ctx) {
|
|
@@ -6617,9 +6909,8 @@ async function setRunnerStatus(request, ctx, adminStatus) {
|
|
|
6617
6909
|
}
|
|
6618
6910
|
async function claimWake(request, ctx) {
|
|
6619
6911
|
const runnerId = routeParam$1(request, `id`);
|
|
6620
|
-
if (!ctx.authenticatedUser) throw new ElectricAgentsError(ErrCodeUnauthorized, `Authentication is required to claim runner work`, 401);
|
|
6621
6912
|
const runner = await requireRunner(ctx, runnerId);
|
|
6622
|
-
if (runner.owner_user_id !== ctx.
|
|
6913
|
+
if (ctx.principal && runner.owner_user_id !== ctx.principal.key) throw new ElectricAgentsError(ErrCodeUnauthorized, `Runner claim requires the authenticated owner`, 403);
|
|
6623
6914
|
if (runner.admin_status !== `enabled`) throw new ElectricAgentsError(ErrCodeNotRunning, `Runner is disabled`, 409);
|
|
6624
6915
|
const parsed = routeBody(request);
|
|
6625
6916
|
const expectedSubscriptionId = subscriptionIdForDispatchTarget({
|
|
@@ -6651,8 +6942,8 @@ async function requireRunner(ctx, runnerId) {
|
|
|
6651
6942
|
return runner;
|
|
6652
6943
|
}
|
|
6653
6944
|
function assertRunnerOwnerIfAuthenticated(ctx, ownerUserId) {
|
|
6654
|
-
if (!ctx.
|
|
6655
|
-
if (ownerUserId === ctx.
|
|
6945
|
+
if (!ctx.principal) return;
|
|
6946
|
+
if (ownerUserId === ctx.principal.key) return;
|
|
6656
6947
|
throw new ElectricAgentsError(ErrCodeUnauthorized, `Runner access requires the authenticated owner`, 403);
|
|
6657
6948
|
}
|
|
6658
6949
|
async function notificationFromClaim(ctx, input) {
|
|
@@ -6709,8 +7000,10 @@ async function notificationFromClaim(ctx, input) {
|
|
|
6709
7000
|
url: entity.url,
|
|
6710
7001
|
streams: entity.streams,
|
|
6711
7002
|
tags: entity.tags,
|
|
6712
|
-
spawnArgs: entity.spawn_args
|
|
6713
|
-
|
|
7003
|
+
spawnArgs: entity.spawn_args,
|
|
7004
|
+
createdBy: entity.created_by
|
|
7005
|
+
},
|
|
7006
|
+
principal: principalFromCreatedBy(entity.created_by)
|
|
6714
7007
|
};
|
|
6715
7008
|
}
|
|
6716
7009
|
|