@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.js
CHANGED
|
@@ -61,6 +61,7 @@ const entities = pgTable(`entities`, {
|
|
|
61
61
|
tagsIndex: text(`tags_index`).array().notNull().default(sql`'{}'::text[]`),
|
|
62
62
|
spawnArgs: jsonb(`spawn_args`).default({}),
|
|
63
63
|
parent: text(`parent`),
|
|
64
|
+
createdBy: text(`created_by`),
|
|
64
65
|
typeRevision: integer(`type_revision`),
|
|
65
66
|
inboxSchemas: jsonb(`inbox_schemas`),
|
|
66
67
|
stateSchemas: jsonb(`state_schemas`),
|
|
@@ -71,6 +72,7 @@ const entities = pgTable(`entities`, {
|
|
|
71
72
|
index(`idx_entities_type`).on(table.tenantId, table.type),
|
|
72
73
|
index(`idx_entities_status`).on(table.tenantId, table.status),
|
|
73
74
|
index(`idx_entities_parent`).on(table.tenantId, table.parent),
|
|
75
|
+
index(`idx_entities_created_by`).on(table.tenantId, table.createdBy),
|
|
74
76
|
index(`entities_tags_index_gin`).using(`gin`, table.tagsIndex),
|
|
75
77
|
check(`chk_entities_status`, sql`${table.status} IN ('spawning', 'running', 'idle', 'stopped')`)
|
|
76
78
|
]);
|
|
@@ -353,6 +355,7 @@ function toPublicEntity(entity) {
|
|
|
353
355
|
tags: entity.tags,
|
|
354
356
|
spawn_args: entity.spawn_args,
|
|
355
357
|
parent: entity.parent,
|
|
358
|
+
created_by: entity.created_by,
|
|
356
359
|
created_at: entity.created_at,
|
|
357
360
|
updated_at: entity.updated_at
|
|
358
361
|
};
|
|
@@ -592,6 +595,24 @@ var PostgresRegistry = class {
|
|
|
592
595
|
}
|
|
593
596
|
});
|
|
594
597
|
}
|
|
598
|
+
async ensureEntityType(et) {
|
|
599
|
+
const existing = await this.getEntityType(et.name);
|
|
600
|
+
if (existing) return existing;
|
|
601
|
+
await this.db.insert(entityTypes).values({
|
|
602
|
+
tenantId: this.tenantId,
|
|
603
|
+
name: et.name,
|
|
604
|
+
description: et.description,
|
|
605
|
+
creationSchema: et.creation_schema ?? null,
|
|
606
|
+
inboxSchemas: et.inbox_schemas ?? null,
|
|
607
|
+
stateSchemas: et.state_schemas ?? null,
|
|
608
|
+
serveEndpoint: et.serve_endpoint ?? null,
|
|
609
|
+
defaultDispatchPolicy: et.default_dispatch_policy ?? null,
|
|
610
|
+
revision: et.revision,
|
|
611
|
+
createdAt: et.created_at,
|
|
612
|
+
updatedAt: et.updated_at
|
|
613
|
+
}).onConflictDoNothing();
|
|
614
|
+
return await this.getEntityType(et.name);
|
|
615
|
+
}
|
|
595
616
|
async getEntityType(name) {
|
|
596
617
|
const rows = await this.db.select().from(entityTypes).where(this.entityTypeWhere(name)).limit(1);
|
|
597
618
|
if (rows.length === 0) return null;
|
|
@@ -631,6 +652,7 @@ var PostgresRegistry = class {
|
|
|
631
652
|
tagsIndex: buildTagsIndex(entity.tags),
|
|
632
653
|
spawnArgs: entity.spawn_args ?? {},
|
|
633
654
|
parent: entity.parent ?? null,
|
|
655
|
+
createdBy: entity.created_by ?? null,
|
|
634
656
|
typeRevision: entity.type_revision ?? null,
|
|
635
657
|
inboxSchemas: entity.inbox_schemas ?? null,
|
|
636
658
|
stateSchemas: entity.state_schemas ?? null,
|
|
@@ -676,6 +698,7 @@ var PostgresRegistry = class {
|
|
|
676
698
|
if (filter?.type) conditions.push(eq(entities.type, filter.type));
|
|
677
699
|
if (filter?.status) conditions.push(eq(entities.status, filter.status));
|
|
678
700
|
if (filter?.parent) conditions.push(eq(entities.parent, filter.parent));
|
|
701
|
+
if (filter?.created_by) conditions.push(eq(entities.createdBy, filter.created_by));
|
|
679
702
|
const whereClause = and(...conditions);
|
|
680
703
|
const countResult = await this.db.select({ count: sql`count(*)` }).from(entities).where(whereClause);
|
|
681
704
|
const total = Number(countResult[0].count);
|
|
@@ -962,6 +985,7 @@ var PostgresRegistry = class {
|
|
|
962
985
|
tags: row.tags ?? {},
|
|
963
986
|
spawn_args: row.spawnArgs,
|
|
964
987
|
parent: row.parent ?? void 0,
|
|
988
|
+
created_by: row.createdBy ?? void 0,
|
|
965
989
|
type_revision: row.typeRevision ?? void 0,
|
|
966
990
|
inbox_schemas: row.inboxSchemas,
|
|
967
991
|
state_schemas: row.stateSchemas,
|
|
@@ -1687,6 +1711,239 @@ function parseDispatchPolicy(value, label = `dispatch_policy`) {
|
|
|
1687
1711
|
throw new Error(details ? `${label} does not match dispatch policy schema: ${details}` : `${label} does not match dispatch policy schema`);
|
|
1688
1712
|
}
|
|
1689
1713
|
|
|
1714
|
+
//#endregion
|
|
1715
|
+
//#region src/utils/webhook-url.ts
|
|
1716
|
+
function rewriteLoopbackWebhookUrl(value) {
|
|
1717
|
+
if (!value) return void 0;
|
|
1718
|
+
const rewriteTarget = process.env.ELECTRIC_AGENTS_REWRITE_LOOPBACK_WEBHOOKS_TO?.trim();
|
|
1719
|
+
if (!rewriteTarget) return value;
|
|
1720
|
+
const url = new URL(value);
|
|
1721
|
+
if (!isLoopbackHostname(url.hostname)) return value;
|
|
1722
|
+
if (rewriteTarget.includes(`://`)) {
|
|
1723
|
+
const target = new URL(rewriteTarget);
|
|
1724
|
+
url.protocol = target.protocol;
|
|
1725
|
+
url.username = target.username;
|
|
1726
|
+
url.password = target.password;
|
|
1727
|
+
url.hostname = target.hostname;
|
|
1728
|
+
url.port = target.port;
|
|
1729
|
+
return url.toString();
|
|
1730
|
+
}
|
|
1731
|
+
url.host = rewriteTarget;
|
|
1732
|
+
return url.toString();
|
|
1733
|
+
}
|
|
1734
|
+
function isLoopbackHostname(hostname) {
|
|
1735
|
+
return hostname === `localhost` || hostname === `127.0.0.1` || hostname === `::1`;
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
//#endregion
|
|
1739
|
+
//#region src/routing/dispatch-policy.ts
|
|
1740
|
+
function subscriptionIdForDispatchTarget(target) {
|
|
1741
|
+
if (target.subscription_id) return target.subscription_id;
|
|
1742
|
+
if (target.type === `runner`) return `runner:${target.runnerId}`;
|
|
1743
|
+
const digest = createHash(`sha256`).update(target.url).digest(`hex`);
|
|
1744
|
+
return `webhook:${digest.slice(0, 16)}`;
|
|
1745
|
+
}
|
|
1746
|
+
function subscriptionIdForEntityDispatchTarget(target, entityUrl) {
|
|
1747
|
+
const base = subscriptionIdForDispatchTarget(target);
|
|
1748
|
+
if (!target.subscription_id) return base;
|
|
1749
|
+
const digest = createHash(`sha256`).update(entityUrl).digest(`hex`);
|
|
1750
|
+
return `${base}:${digest.slice(0, 16)}`;
|
|
1751
|
+
}
|
|
1752
|
+
async function resolveEffectiveDispatchPolicyForSpawn(ctx, typeName, opts) {
|
|
1753
|
+
if (opts.dispatchPolicy) return opts.dispatchPolicy;
|
|
1754
|
+
const entityType = await ctx.entityManager.registry.getEntityType(typeName);
|
|
1755
|
+
if (opts.parent) {
|
|
1756
|
+
const parent = await ctx.entityManager.registry.getEntity(opts.parent);
|
|
1757
|
+
if (parent?.dispatch_policy) return applyTypeDefaultSubscriptionScope(parent.dispatch_policy, entityType?.default_dispatch_policy);
|
|
1758
|
+
}
|
|
1759
|
+
return entityType?.default_dispatch_policy;
|
|
1760
|
+
}
|
|
1761
|
+
async function resolveEffectiveDispatchPolicyForEntity(ctx, entity) {
|
|
1762
|
+
if (entity.dispatch_policy) return entity.dispatch_policy;
|
|
1763
|
+
const entityType = await ctx.entityManager.registry.getEntityType(entity.type);
|
|
1764
|
+
return entityType?.default_dispatch_policy;
|
|
1765
|
+
}
|
|
1766
|
+
async function backfillEntityDispatchPolicy(ctx, entity) {
|
|
1767
|
+
if (entity.dispatch_policy) return entity;
|
|
1768
|
+
const dispatchPolicy = await resolveEffectiveDispatchPolicyForEntity(ctx, entity);
|
|
1769
|
+
if (!dispatchPolicy) return entity;
|
|
1770
|
+
return await ctx.entityManager.registry.updateEntityDispatchPolicy(entity.url, dispatchPolicy) ?? {
|
|
1771
|
+
...entity,
|
|
1772
|
+
dispatch_policy: dispatchPolicy
|
|
1773
|
+
};
|
|
1774
|
+
}
|
|
1775
|
+
function applyTypeDefaultSubscriptionScope(policy, typeDefault) {
|
|
1776
|
+
const target = policy.targets[0];
|
|
1777
|
+
const defaultTarget = typeDefault?.targets[0];
|
|
1778
|
+
if (!target || !defaultTarget?.subscription_id) return policy;
|
|
1779
|
+
if (!sameDispatchDestination(target, defaultTarget)) return policy;
|
|
1780
|
+
if (target.subscription_id === defaultTarget.subscription_id) return policy;
|
|
1781
|
+
return { targets: [{
|
|
1782
|
+
...target,
|
|
1783
|
+
subscription_id: defaultTarget.subscription_id
|
|
1784
|
+
}] };
|
|
1785
|
+
}
|
|
1786
|
+
function sameDispatchDestination(a, b) {
|
|
1787
|
+
if (a.type !== b.type) return false;
|
|
1788
|
+
if (a.type === `runner` && b.type === `runner`) return a.runnerId === b.runnerId;
|
|
1789
|
+
if (a.type === `webhook` && b.type === `webhook`) return a.url === b.url;
|
|
1790
|
+
return false;
|
|
1791
|
+
}
|
|
1792
|
+
async function assertDispatchPolicyAllowed(ctx, policy) {
|
|
1793
|
+
const target = policy?.targets[0];
|
|
1794
|
+
if (!target || target.type !== `runner`) return;
|
|
1795
|
+
const runner = await ctx.entityManager.registry.getRunner(target.runnerId);
|
|
1796
|
+
if (!runner) throw new ElectricAgentsError(ErrCodeNotFound, `Runner "${target.runnerId}" not found`, 404);
|
|
1797
|
+
if (ctx.principal && runner.owner_user_id !== ctx.principal.key) throw new ElectricAgentsError(ErrCodeUnauthorized, `Runner dispatch requires the authenticated owner`, 403);
|
|
1798
|
+
}
|
|
1799
|
+
async function linkEntityDispatchSubscription(ctx, entity) {
|
|
1800
|
+
const dispatchPolicy = await resolveEffectiveDispatchPolicyForEntity(ctx, entity);
|
|
1801
|
+
const target = dispatchPolicy?.targets[0];
|
|
1802
|
+
if (!target) return;
|
|
1803
|
+
await linkStreamToTargetSubscription(ctx, target, entity);
|
|
1804
|
+
}
|
|
1805
|
+
async function unlinkEntityDispatchSubscription(ctx, entity) {
|
|
1806
|
+
const dispatchPolicy = await resolveEffectiveDispatchPolicyForEntity(ctx, entity);
|
|
1807
|
+
const target = dispatchPolicy?.targets[0];
|
|
1808
|
+
if (!target) return;
|
|
1809
|
+
const subscriptionId = subscriptionIdForEntityDispatchTarget(target, entity.url);
|
|
1810
|
+
await ctx.streamClient.removeSubscriptionStream(subscriptionId, entity.streams.main).catch((err) => {
|
|
1811
|
+
serverLog.warn(`[dispatch-policy] failed to remove stream from subscription`, {
|
|
1812
|
+
subscriptionId,
|
|
1813
|
+
stream: entity.streams.main
|
|
1814
|
+
}, err);
|
|
1815
|
+
});
|
|
1816
|
+
}
|
|
1817
|
+
async function linkStreamToTargetSubscription(ctx, target, entity) {
|
|
1818
|
+
const streamPath = entity.streams.main;
|
|
1819
|
+
const subscriptionId = subscriptionIdForEntityDispatchTarget(target, entity.url);
|
|
1820
|
+
const existing = await ctx.streamClient.getSubscription(subscriptionId);
|
|
1821
|
+
if (target.type === `runner`) {
|
|
1822
|
+
const runner = await ctx.entityManager.registry.getRunner(target.runnerId);
|
|
1823
|
+
if (!runner) throw new ElectricAgentsError(ErrCodeNotFound, `Runner "${target.runnerId}" not found`, 404);
|
|
1824
|
+
const wakeStream = runner.wake_stream || runnerWakeStream(target.runnerId);
|
|
1825
|
+
await ctx.streamClient.ensure(wakeStream, { contentType: `application/json` });
|
|
1826
|
+
if (!existing) {
|
|
1827
|
+
await ctx.streamClient.putSubscription(subscriptionId, {
|
|
1828
|
+
type: `pull-wake`,
|
|
1829
|
+
streams: [streamPath],
|
|
1830
|
+
wake_stream: wakeStream,
|
|
1831
|
+
description: `Electric Agents runner ${target.runnerId}`
|
|
1832
|
+
});
|
|
1833
|
+
return;
|
|
1834
|
+
}
|
|
1835
|
+
await ctx.streamClient.addSubscriptionStreams(subscriptionId, [streamPath]);
|
|
1836
|
+
return;
|
|
1837
|
+
}
|
|
1838
|
+
const webhookUrl = rewriteLoopbackWebhookUrl(target.url);
|
|
1839
|
+
if (!webhookUrl) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Webhook dispatch target must include a valid URL`, 400);
|
|
1840
|
+
const forwardUrl = appendPathToUrl(ctx.publicUrl, `/_electric/webhook-forward/${encodeURIComponent(subscriptionId)}`);
|
|
1841
|
+
if (!existing) await ctx.streamClient.putSubscription(subscriptionId, {
|
|
1842
|
+
type: `webhook`,
|
|
1843
|
+
streams: [streamPath],
|
|
1844
|
+
webhook: { url: forwardUrl },
|
|
1845
|
+
description: `Electric Agents webhook ${subscriptionId}`
|
|
1846
|
+
});
|
|
1847
|
+
else await ctx.streamClient.addSubscriptionStreams(subscriptionId, [streamPath]);
|
|
1848
|
+
await ctx.pgDb.insert(subscriptionWebhooks).values({
|
|
1849
|
+
tenantId: ctx.service,
|
|
1850
|
+
subscriptionId,
|
|
1851
|
+
webhookUrl
|
|
1852
|
+
}).onConflictDoUpdate({
|
|
1853
|
+
target: [subscriptionWebhooks.tenantId, subscriptionWebhooks.subscriptionId],
|
|
1854
|
+
set: { webhookUrl }
|
|
1855
|
+
});
|
|
1856
|
+
}
|
|
1857
|
+
|
|
1858
|
+
//#endregion
|
|
1859
|
+
//#region src/principal.ts
|
|
1860
|
+
const PRINCIPAL_KINDS = new Set([
|
|
1861
|
+
`user`,
|
|
1862
|
+
`agent`,
|
|
1863
|
+
`service`,
|
|
1864
|
+
`system`
|
|
1865
|
+
]);
|
|
1866
|
+
function parsePrincipalKey(input) {
|
|
1867
|
+
const colon = input.indexOf(`:`);
|
|
1868
|
+
if (colon <= 0) throw new Error(`Invalid principal key`);
|
|
1869
|
+
const kind = input.slice(0, colon);
|
|
1870
|
+
const id = input.slice(colon + 1);
|
|
1871
|
+
if (!PRINCIPAL_KINDS.has(kind)) throw new Error(`Invalid principal kind`);
|
|
1872
|
+
if (!id || id.includes(`/`)) throw new Error(`Invalid principal id`);
|
|
1873
|
+
const key = `${kind}:${id}`;
|
|
1874
|
+
return {
|
|
1875
|
+
kind,
|
|
1876
|
+
id,
|
|
1877
|
+
key,
|
|
1878
|
+
url: `/principal/${encodeURIComponent(key)}`
|
|
1879
|
+
};
|
|
1880
|
+
}
|
|
1881
|
+
function principalUrl(key) {
|
|
1882
|
+
return parsePrincipalKey(key).url;
|
|
1883
|
+
}
|
|
1884
|
+
function principalKeyFromUrl(url) {
|
|
1885
|
+
if (!url.startsWith(`/principal/`)) return null;
|
|
1886
|
+
const segment = url.slice(`/principal/`.length);
|
|
1887
|
+
if (!segment || segment.includes(`/`)) return null;
|
|
1888
|
+
try {
|
|
1889
|
+
const key = decodeURIComponent(segment);
|
|
1890
|
+
return parsePrincipalKey(key).key;
|
|
1891
|
+
} catch {
|
|
1892
|
+
return null;
|
|
1893
|
+
}
|
|
1894
|
+
}
|
|
1895
|
+
const BUILT_IN_SYSTEM_PRINCIPAL_IDS = new Set([
|
|
1896
|
+
`framework`,
|
|
1897
|
+
`auth-sync`,
|
|
1898
|
+
`dev-local`
|
|
1899
|
+
]);
|
|
1900
|
+
function isBuiltInSystemPrincipalUrl(url) {
|
|
1901
|
+
if (!url?.startsWith(`/principal/`)) return false;
|
|
1902
|
+
try {
|
|
1903
|
+
const key = principalKeyFromUrl(url);
|
|
1904
|
+
if (!key) return false;
|
|
1905
|
+
const principal = parsePrincipalKey(key);
|
|
1906
|
+
return principal.kind === `system` && BUILT_IN_SYSTEM_PRINCIPAL_IDS.has(principal.id);
|
|
1907
|
+
} catch {
|
|
1908
|
+
return false;
|
|
1909
|
+
}
|
|
1910
|
+
}
|
|
1911
|
+
function principalFromCreatedBy(createdBy) {
|
|
1912
|
+
if (!createdBy) return void 0;
|
|
1913
|
+
const key = principalKeyFromUrl(createdBy);
|
|
1914
|
+
if (!key) return {
|
|
1915
|
+
url: createdBy,
|
|
1916
|
+
key: null
|
|
1917
|
+
};
|
|
1918
|
+
const principal = parsePrincipalKey(key);
|
|
1919
|
+
return {
|
|
1920
|
+
url: principal.url,
|
|
1921
|
+
key: principal.key,
|
|
1922
|
+
kind: principal.kind,
|
|
1923
|
+
id: principal.id
|
|
1924
|
+
};
|
|
1925
|
+
}
|
|
1926
|
+
const principalIdentityStateSchema = Type.Object({
|
|
1927
|
+
kind: Type.Union([
|
|
1928
|
+
Type.Literal(`user`),
|
|
1929
|
+
Type.Literal(`agent`),
|
|
1930
|
+
Type.Literal(`service`),
|
|
1931
|
+
Type.Literal(`system`)
|
|
1932
|
+
]),
|
|
1933
|
+
id: Type.String(),
|
|
1934
|
+
key: Type.String(),
|
|
1935
|
+
url: Type.String(),
|
|
1936
|
+
updated_at: Type.String(),
|
|
1937
|
+
display_name: Type.Optional(Type.String()),
|
|
1938
|
+
email: Type.Optional(Type.String()),
|
|
1939
|
+
avatar_url: Type.Optional(Type.String()),
|
|
1940
|
+
auth_provider: Type.Optional(Type.String()),
|
|
1941
|
+
auth_subject: Type.Optional(Type.String()),
|
|
1942
|
+
claims: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
|
|
1943
|
+
created_at: Type.Optional(Type.String())
|
|
1944
|
+
}, { additionalProperties: false });
|
|
1945
|
+
const principalUpdateIdentityMessageSchema = Type.Object({ identity: principalIdentityStateSchema }, { additionalProperties: false });
|
|
1946
|
+
|
|
1690
1947
|
//#endregion
|
|
1691
1948
|
//#region src/manifest-side-effects.ts
|
|
1692
1949
|
function isRecord$1(value) {
|
|
@@ -1804,25 +2061,11 @@ function extractTraceContext(headers) {
|
|
|
1804
2061
|
|
|
1805
2062
|
//#endregion
|
|
1806
2063
|
//#region src/entity-manager.ts
|
|
2064
|
+
function createInitialQueuePosition(date) {
|
|
2065
|
+
return `${String(date.getTime()).padStart(16, `0`)}:a0`;
|
|
2066
|
+
}
|
|
1807
2067
|
const DEFAULT_FORK_WAIT_TIMEOUT_MS = 12e4;
|
|
1808
2068
|
const DEFAULT_FORK_WAIT_POLL_MS = 250;
|
|
1809
|
-
function applyTypeDefaultSubscriptionScope$1(policy, typeDefault) {
|
|
1810
|
-
const target = policy.targets[0];
|
|
1811
|
-
const defaultTarget = typeDefault?.targets[0];
|
|
1812
|
-
if (!target || !defaultTarget?.subscription_id) return policy;
|
|
1813
|
-
if (!sameDispatchDestination$1(target, defaultTarget)) return policy;
|
|
1814
|
-
if (target.subscription_id === defaultTarget.subscription_id) return policy;
|
|
1815
|
-
return { targets: [{
|
|
1816
|
-
...target,
|
|
1817
|
-
subscription_id: defaultTarget.subscription_id
|
|
1818
|
-
}] };
|
|
1819
|
-
}
|
|
1820
|
-
function sameDispatchDestination$1(a, b) {
|
|
1821
|
-
if (a.type !== b.type) return false;
|
|
1822
|
-
if (a.type === `runner` && b.type === `runner`) return a.runnerId === b.runnerId;
|
|
1823
|
-
if (a.type === `webhook` && b.type === `webhook`) return a.url === b.url;
|
|
1824
|
-
return false;
|
|
1825
|
-
}
|
|
1826
2069
|
function sleep(ms) {
|
|
1827
2070
|
return new Promise((resolve$1) => setTimeout(resolve$1, ms));
|
|
1828
2071
|
}
|
|
@@ -1893,6 +2136,7 @@ var EntityManager = class {
|
|
|
1893
2136
|
}
|
|
1894
2137
|
async registerEntityType(req) {
|
|
1895
2138
|
if (!req.name) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Missing required field: name`, 400);
|
|
2139
|
+
if (req.name === `principal`) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Entity type "principal" is built in and cannot be registered or updated`, 400);
|
|
1896
2140
|
if (req.name.startsWith(`_`)) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Entity type names starting with "_" are reserved`, 400);
|
|
1897
2141
|
if (!req.description) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Missing required field: description`, 400);
|
|
1898
2142
|
this.validateSchema(req.creation_schema);
|
|
@@ -1919,10 +2163,63 @@ var EntityManager = class {
|
|
|
1919
2163
|
return stored;
|
|
1920
2164
|
}
|
|
1921
2165
|
async deleteEntityType(name) {
|
|
2166
|
+
if (name === `principal`) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Entity type "principal" is built in and cannot be deleted`, 400);
|
|
1922
2167
|
const existing = await this.registry.getEntityType(name);
|
|
1923
2168
|
if (!existing) throw new ElectricAgentsError(ErrCodeNotFound, `Entity type "${name}" not found`, 404);
|
|
1924
2169
|
await this.registry.deleteEntityType(name);
|
|
1925
2170
|
}
|
|
2171
|
+
async ensurePrincipalEntityType() {
|
|
2172
|
+
const now = new Date().toISOString();
|
|
2173
|
+
return await this.registry.ensureEntityType({
|
|
2174
|
+
name: `principal`,
|
|
2175
|
+
description: `built-in principal entity`,
|
|
2176
|
+
inbox_schemas: { update_identity: principalUpdateIdentityMessageSchema },
|
|
2177
|
+
state_schemas: { identity: principalIdentityStateSchema },
|
|
2178
|
+
revision: 1,
|
|
2179
|
+
created_at: now,
|
|
2180
|
+
updated_at: now
|
|
2181
|
+
});
|
|
2182
|
+
}
|
|
2183
|
+
async ensurePrincipal(principal) {
|
|
2184
|
+
const existing = await this.registry.getEntity(principal.url);
|
|
2185
|
+
if (existing) return existing;
|
|
2186
|
+
await this.ensurePrincipalEntityType();
|
|
2187
|
+
try {
|
|
2188
|
+
const entity = await this.spawn(`principal`, {
|
|
2189
|
+
instance_id: principal.key,
|
|
2190
|
+
args: {
|
|
2191
|
+
kind: principal.kind,
|
|
2192
|
+
id: principal.id,
|
|
2193
|
+
key: principal.key
|
|
2194
|
+
},
|
|
2195
|
+
tags: {
|
|
2196
|
+
principal_kind: principal.kind,
|
|
2197
|
+
principal_id: principal.id
|
|
2198
|
+
},
|
|
2199
|
+
created_by: principal.url
|
|
2200
|
+
});
|
|
2201
|
+
const now = new Date().toISOString();
|
|
2202
|
+
await this.streamClient.append(entity.streams.main, this.encodeChangeEvent({
|
|
2203
|
+
type: `identity`,
|
|
2204
|
+
key: `self`,
|
|
2205
|
+
value: {
|
|
2206
|
+
kind: principal.kind,
|
|
2207
|
+
id: principal.id,
|
|
2208
|
+
key: principal.key,
|
|
2209
|
+
url: principal.url,
|
|
2210
|
+
created_at: now,
|
|
2211
|
+
updated_at: now
|
|
2212
|
+
}
|
|
2213
|
+
}));
|
|
2214
|
+
return entity;
|
|
2215
|
+
} catch (error) {
|
|
2216
|
+
if (error instanceof ElectricAgentsError && error.code === ErrCodeDuplicateURL) {
|
|
2217
|
+
const raced = await this.registry.getEntity(principal.url);
|
|
2218
|
+
if (raced) return raced;
|
|
2219
|
+
}
|
|
2220
|
+
throw error;
|
|
2221
|
+
}
|
|
2222
|
+
}
|
|
1926
2223
|
/**
|
|
1927
2224
|
* Spawn a new entity of the given type with durable streams.
|
|
1928
2225
|
*/
|
|
@@ -1938,6 +2235,7 @@ var EntityManager = class {
|
|
|
1938
2235
|
});
|
|
1939
2236
|
}
|
|
1940
2237
|
async spawnInner(typeName, req) {
|
|
2238
|
+
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);
|
|
1941
2239
|
if (typeName.startsWith(`_`)) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Entity type names starting with "_" are reserved`, 400);
|
|
1942
2240
|
const entityType = await this.registry.getEntityType(typeName);
|
|
1943
2241
|
if (!entityType) throw new ElectricAgentsError(ErrCodeUnknownEntityType, `Entity type "${typeName}" not found`, 404);
|
|
@@ -1949,7 +2247,7 @@ var EntityManager = class {
|
|
|
1949
2247
|
const instanceId = req.instance_id || randomUUID();
|
|
1950
2248
|
if (instanceId.includes(`/`)) throw new ElectricAgentsError(ErrCodeInvalidRequest, `instance_id must not contain forward slashes`, 400);
|
|
1951
2249
|
const writeToken = randomUUID();
|
|
1952
|
-
const entityURL = `/${typeName}/${instanceId}`;
|
|
2250
|
+
const entityURL = typeName === `principal` ? principalUrl(instanceId) : `/${typeName}/${instanceId}`;
|
|
1953
2251
|
const mainPath = `${entityURL}/main`;
|
|
1954
2252
|
const errorPath = `${entityURL}/error`;
|
|
1955
2253
|
const subscriptionId = `${typeName}-handler`;
|
|
@@ -1961,7 +2259,7 @@ var EntityManager = class {
|
|
|
1961
2259
|
parentEntity = await this.registry.getEntity(req.parent);
|
|
1962
2260
|
if (!parentEntity) throw new ElectricAgentsError(ErrCodeNotFound, `Parent entity "${req.parent}" not found`, 404);
|
|
1963
2261
|
}
|
|
1964
|
-
const dispatchPolicy = req.dispatch_policy ? this.validateDispatchPolicy(req.dispatch_policy, { label: `dispatch_policy` }) : parentEntity?.dispatch_policy ? applyTypeDefaultSubscriptionScope
|
|
2262
|
+
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;
|
|
1965
2263
|
const now = Date.now();
|
|
1966
2264
|
const entityData = {
|
|
1967
2265
|
type: typeName,
|
|
@@ -1980,6 +2278,7 @@ var EntityManager = class {
|
|
|
1980
2278
|
inbox_schemas: entityType.inbox_schemas,
|
|
1981
2279
|
state_schemas: entityType.state_schemas,
|
|
1982
2280
|
created_at: now,
|
|
2281
|
+
created_by: req.created_by ?? parentEntity?.created_by,
|
|
1983
2282
|
updated_at: now
|
|
1984
2283
|
};
|
|
1985
2284
|
if (req.parent) entityData.parent = req.parent;
|
|
@@ -2009,7 +2308,7 @@ var EntityManager = class {
|
|
|
2009
2308
|
const inboxEvent = entityStateSchema.inbox.insert({
|
|
2010
2309
|
key: `msg-in-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
2011
2310
|
value: {
|
|
2012
|
-
from: req.parent ?? `spawn`,
|
|
2311
|
+
from: req.created_by ?? req.parent ?? `spawn`,
|
|
2013
2312
|
payload: req.initialMessage,
|
|
2014
2313
|
timestamp: msgNow
|
|
2015
2314
|
}
|
|
@@ -2552,10 +2851,10 @@ var EntityManager = class {
|
|
|
2552
2851
|
changed = true;
|
|
2553
2852
|
}
|
|
2554
2853
|
}
|
|
2555
|
-
if (typeof next.
|
|
2556
|
-
const
|
|
2557
|
-
if (
|
|
2558
|
-
next.
|
|
2854
|
+
if (typeof next.senderUrl === `string`) {
|
|
2855
|
+
const forkSender = entityUrlMap.get(next.senderUrl);
|
|
2856
|
+
if (forkSender) {
|
|
2857
|
+
next.senderUrl = forkSender;
|
|
2559
2858
|
changed = true;
|
|
2560
2859
|
}
|
|
2561
2860
|
}
|
|
@@ -2621,6 +2920,7 @@ var EntityManager = class {
|
|
|
2621
2920
|
const fireAtRaw = manifest.fireAt;
|
|
2622
2921
|
const producerId = manifest.producerId;
|
|
2623
2922
|
const targetUrl = manifest.targetUrl;
|
|
2923
|
+
const senderUrl = typeof manifest.senderUrl === `string` ? manifest.senderUrl : ownerEntityUrl;
|
|
2624
2924
|
if (typeof fireAtRaw !== `string` || typeof producerId !== `string` || typeof targetUrl !== `string`) {
|
|
2625
2925
|
serverLog.warn(`[agent-server] invalid forked future_send manifest entry for ${ownerEntityUrl}/${manifestKey}`);
|
|
2626
2926
|
return;
|
|
@@ -2632,7 +2932,7 @@ var EntityManager = class {
|
|
|
2632
2932
|
}
|
|
2633
2933
|
await this.scheduler.syncManifestDelayedSend(ownerEntityUrl, manifestKey, {
|
|
2634
2934
|
entityUrl: targetUrl,
|
|
2635
|
-
from:
|
|
2935
|
+
from: senderUrl,
|
|
2636
2936
|
payload: manifest.payload,
|
|
2637
2937
|
key: `scheduled-${producerId}`,
|
|
2638
2938
|
type: typeof manifest.messageType === `string` ? manifest.messageType : void 0,
|
|
@@ -2646,6 +2946,7 @@ var EntityManager = class {
|
|
|
2646
2946
|
kind: `schedule`,
|
|
2647
2947
|
scheduleType: `future_send`,
|
|
2648
2948
|
targetUrl,
|
|
2949
|
+
senderUrl,
|
|
2649
2950
|
fireAt: fireAt.toISOString(),
|
|
2650
2951
|
producerId,
|
|
2651
2952
|
status: `pending`
|
|
@@ -2673,9 +2974,14 @@ var EntityManager = class {
|
|
|
2673
2974
|
const value = {
|
|
2674
2975
|
from: req.from,
|
|
2675
2976
|
payload: req.payload,
|
|
2676
|
-
timestamp: now
|
|
2977
|
+
timestamp: now,
|
|
2978
|
+
mode: req.mode ?? `immediate`,
|
|
2979
|
+
status: req.mode === `queued` || req.mode === `paused` ? `pending` : `processed`
|
|
2677
2980
|
};
|
|
2678
2981
|
if (req.type) value.message_type = req.type;
|
|
2982
|
+
if (req.position) value.position = req.position;
|
|
2983
|
+
else if (value.mode === `queued` || value.mode === `paused`) value.position = createInitialQueuePosition(new Date(now));
|
|
2984
|
+
if (value.status === `processed`) value.processed_at = now;
|
|
2679
2985
|
const envelope = entityStateSchema.inbox.insert({
|
|
2680
2986
|
key,
|
|
2681
2987
|
value
|
|
@@ -2687,11 +2993,47 @@ var EntityManager = class {
|
|
|
2687
2993
|
return;
|
|
2688
2994
|
}
|
|
2689
2995
|
await this.streamClient.append(entity.streams.main, encoded);
|
|
2996
|
+
if (entity.type === `principal` && req.type === `update_identity`) {
|
|
2997
|
+
const identity = req.payload?.identity;
|
|
2998
|
+
await this.streamClient.append(entity.streams.main, this.encodeChangeEvent({
|
|
2999
|
+
type: `identity`,
|
|
3000
|
+
key: `self`,
|
|
3001
|
+
value: identity
|
|
3002
|
+
}));
|
|
3003
|
+
}
|
|
2690
3004
|
} catch (err) {
|
|
2691
3005
|
if (this.isClosedStreamError(err)) throw new ElectricAgentsError(ErrCodeNotRunning, `Entity is stopped`, 409);
|
|
2692
3006
|
throw err;
|
|
2693
3007
|
}
|
|
2694
3008
|
}
|
|
3009
|
+
async updateInboxMessage(entityUrl, key, req) {
|
|
3010
|
+
const entity = await this.registry.getEntity(entityUrl);
|
|
3011
|
+
if (!entity) throw new ElectricAgentsError(ErrCodeNotFound, `Entity not found`, 404);
|
|
3012
|
+
if (entity.status === `stopped`) throw new ElectricAgentsError(ErrCodeNotRunning, `Entity is stopped`, 409);
|
|
3013
|
+
const now = new Date().toISOString();
|
|
3014
|
+
const value = {};
|
|
3015
|
+
if (`payload` in req) value.payload = req.payload;
|
|
3016
|
+
if (req.position !== void 0) value.position = req.position;
|
|
3017
|
+
if (req.mode !== void 0) value.mode = req.mode;
|
|
3018
|
+
if (req.status !== void 0) {
|
|
3019
|
+
value.status = req.status;
|
|
3020
|
+
if (req.status === `processed`) value.processed_at = now;
|
|
3021
|
+
if (req.status === `cancelled`) value.cancelled_at = now;
|
|
3022
|
+
}
|
|
3023
|
+
if (Object.keys(value).length === 0) throw new ElectricAgentsError(ErrCodeInvalidRequest, `No inbox fields to update`, 400);
|
|
3024
|
+
const envelope = entityStateSchema.inbox.update({
|
|
3025
|
+
key,
|
|
3026
|
+
value
|
|
3027
|
+
});
|
|
3028
|
+
await this.streamClient.append(entity.streams.main, this.encodeChangeEvent(envelope));
|
|
3029
|
+
}
|
|
3030
|
+
async deleteInboxMessage(entityUrl, key) {
|
|
3031
|
+
const entity = await this.registry.getEntity(entityUrl);
|
|
3032
|
+
if (!entity) throw new ElectricAgentsError(ErrCodeNotFound, `Entity not found`, 404);
|
|
3033
|
+
if (entity.status === `stopped`) throw new ElectricAgentsError(ErrCodeNotRunning, `Entity is stopped`, 409);
|
|
3034
|
+
const envelope = entityStateSchema.inbox.delete({ key });
|
|
3035
|
+
await this.streamClient.append(entity.streams.main, this.encodeChangeEvent(envelope));
|
|
3036
|
+
}
|
|
2695
3037
|
async setTag(entityUrl, key, req, token) {
|
|
2696
3038
|
const entity = await this.registry.getEntity(entityUrl);
|
|
2697
3039
|
if (!entity) throw new ElectricAgentsError(ErrCodeNotFound, `Entity not found`, 404);
|
|
@@ -2777,7 +3119,7 @@ var EntityManager = class {
|
|
|
2777
3119
|
async upsertFutureSendSchedule(ownerEntityUrl, req) {
|
|
2778
3120
|
if (!this.scheduler) throw new Error(`Scheduler not configured`);
|
|
2779
3121
|
const targetUrl = req.targetUrl ?? ownerEntityUrl;
|
|
2780
|
-
const from = req.
|
|
3122
|
+
const from = req.senderUrl ?? ownerEntityUrl;
|
|
2781
3123
|
const fireAt = new Date(req.fireAt);
|
|
2782
3124
|
if (Number.isNaN(fireAt.getTime())) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Invalid fireAt timestamp: ${req.fireAt}`, 400);
|
|
2783
3125
|
await this.validateSendRequest(targetUrl, {
|
|
@@ -2805,9 +3147,9 @@ var EntityManager = class {
|
|
|
2805
3147
|
scheduleType: `future_send`,
|
|
2806
3148
|
fireAt: fireAt.toISOString(),
|
|
2807
3149
|
targetUrl,
|
|
3150
|
+
senderUrl: from,
|
|
2808
3151
|
payload: req.payload,
|
|
2809
3152
|
producerId,
|
|
2810
|
-
...req.from ? { from: req.from } : {},
|
|
2811
3153
|
...req.messageType ? { messageType: req.messageType } : {},
|
|
2812
3154
|
status: `pending`
|
|
2813
3155
|
}
|
|
@@ -2821,9 +3163,9 @@ var EntityManager = class {
|
|
|
2821
3163
|
scheduleType: `future_send`,
|
|
2822
3164
|
fireAt: fireAt.toISOString(),
|
|
2823
3165
|
targetUrl,
|
|
3166
|
+
senderUrl: from,
|
|
2824
3167
|
payload: req.payload,
|
|
2825
3168
|
producerId,
|
|
2826
|
-
...req.from ? { from: req.from } : {},
|
|
2827
3169
|
...req.messageType ? { messageType: req.messageType } : {},
|
|
2828
3170
|
status: `pending`
|
|
2829
3171
|
}, { txid });
|
|
@@ -2861,7 +3203,9 @@ var EntityManager = class {
|
|
|
2861
3203
|
from: req.from,
|
|
2862
3204
|
payload: req.payload,
|
|
2863
3205
|
key: req.key,
|
|
2864
|
-
type: req.type
|
|
3206
|
+
type: req.type,
|
|
3207
|
+
mode: req.mode,
|
|
3208
|
+
position: req.position
|
|
2865
3209
|
}, fireAt);
|
|
2866
3210
|
}
|
|
2867
3211
|
/**
|
|
@@ -3011,6 +3355,7 @@ var EntityManager = class {
|
|
|
3011
3355
|
* Add new input/output schema keys to an entity type directly in Postgres.
|
|
3012
3356
|
*/
|
|
3013
3357
|
async amendSchemas(typeName, schemas) {
|
|
3358
|
+
if (typeName === `principal`) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Entity type "principal" is built in and cannot be amended`, 400);
|
|
3014
3359
|
this.validateSchemaMap(schemas.inbox_schemas);
|
|
3015
3360
|
this.validateSchemaMap(schemas.state_schemas);
|
|
3016
3361
|
const existing = await this.registry.getEntityType(typeName);
|
|
@@ -3060,9 +3405,11 @@ var EntityManager = class {
|
|
|
3060
3405
|
url: entity.url,
|
|
3061
3406
|
streams: entity.streams,
|
|
3062
3407
|
tags: entity.tags,
|
|
3063
|
-
spawnArgs: entity.spawn_args
|
|
3408
|
+
spawnArgs: entity.spawn_args,
|
|
3409
|
+
createdBy: entity.created_by
|
|
3064
3410
|
},
|
|
3065
|
-
|
|
3411
|
+
principal: principalFromCreatedBy(entity.created_by),
|
|
3412
|
+
triggerEvent: `inbox`
|
|
3066
3413
|
};
|
|
3067
3414
|
}
|
|
3068
3415
|
validateSchema(schema) {
|
|
@@ -3102,6 +3449,7 @@ var EntityManager = class {
|
|
|
3102
3449
|
}
|
|
3103
3450
|
}
|
|
3104
3451
|
if (!req.from) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Missing required field: from`, 400);
|
|
3452
|
+
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);
|
|
3105
3453
|
if (req.payload === void 0) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Missing required field: payload`, 400);
|
|
3106
3454
|
return entity;
|
|
3107
3455
|
}
|
|
@@ -4501,6 +4849,7 @@ var ElectricAgentsTenantRuntime = class {
|
|
|
4501
4849
|
const fireAtRaw = value.fireAt;
|
|
4502
4850
|
const producerId = value.producerId;
|
|
4503
4851
|
const targetUrl = value.targetUrl;
|
|
4852
|
+
const senderUrl = typeof value.senderUrl === `string` ? value.senderUrl : ownerEntityUrl;
|
|
4504
4853
|
if (typeof fireAtRaw !== `string` || typeof producerId !== `string` || typeof targetUrl !== `string`) {
|
|
4505
4854
|
serverLog.warn(`[agent-server] invalid future_send manifest entry for ${ownerEntityUrl}/${manifestKey}`);
|
|
4506
4855
|
return;
|
|
@@ -4512,7 +4861,7 @@ var ElectricAgentsTenantRuntime = class {
|
|
|
4512
4861
|
}
|
|
4513
4862
|
await this.scheduler.syncManifestDelayedSend(ownerEntityUrl, manifestKey, {
|
|
4514
4863
|
entityUrl: targetUrl,
|
|
4515
|
-
from:
|
|
4864
|
+
from: senderUrl,
|
|
4516
4865
|
payload: value.payload,
|
|
4517
4866
|
key: `scheduled-${producerId}`,
|
|
4518
4867
|
type: typeof value.messageType === `string` ? value.messageType : void 0,
|
|
@@ -4526,6 +4875,7 @@ var ElectricAgentsTenantRuntime = class {
|
|
|
4526
4875
|
kind: `schedule`,
|
|
4527
4876
|
scheduleType: `future_send`,
|
|
4528
4877
|
targetUrl,
|
|
4878
|
+
senderUrl,
|
|
4529
4879
|
fireAt: fireAt.toISOString(),
|
|
4530
4880
|
producerId,
|
|
4531
4881
|
status: `pending`
|
|
@@ -5374,6 +5724,7 @@ var AgentsHost = class {
|
|
|
5374
5724
|
for (const runtime of runtimes) await this.startTenantRuntime(runtime);
|
|
5375
5725
|
}
|
|
5376
5726
|
async startTenantRuntime(runtime) {
|
|
5727
|
+
await runtime.manager.ensurePrincipalEntityType();
|
|
5377
5728
|
if (this.rehydrateTenantOnStart) await runtime.rehydrateCronSchedules();
|
|
5378
5729
|
if (this.startEntityBridgeManager) await this.entityProjector.loadTenantBridges(runtime.serviceId, runtime.registry);
|
|
5379
5730
|
}
|
|
@@ -5391,6 +5742,7 @@ var AgentsHost = class {
|
|
|
5391
5742
|
scheduler,
|
|
5392
5743
|
entityBridgeManager: this.entityProjector.forTenant(serviceId, registry)
|
|
5393
5744
|
});
|
|
5745
|
+
await runtime.manager.ensurePrincipalEntityType();
|
|
5394
5746
|
return runtime;
|
|
5395
5747
|
}
|
|
5396
5748
|
createStreamClient(config) {
|
|
@@ -5573,30 +5925,6 @@ function validateParsedBody(schema, parsed) {
|
|
|
5573
5925
|
};
|
|
5574
5926
|
}
|
|
5575
5927
|
|
|
5576
|
-
//#endregion
|
|
5577
|
-
//#region src/utils/webhook-url.ts
|
|
5578
|
-
function rewriteLoopbackWebhookUrl(value) {
|
|
5579
|
-
if (!value) return void 0;
|
|
5580
|
-
const rewriteTarget = process.env.ELECTRIC_AGENTS_REWRITE_LOOPBACK_WEBHOOKS_TO?.trim();
|
|
5581
|
-
if (!rewriteTarget) return value;
|
|
5582
|
-
const url = new URL(value);
|
|
5583
|
-
if (!isLoopbackHostname(url.hostname)) return value;
|
|
5584
|
-
if (rewriteTarget.includes(`://`)) {
|
|
5585
|
-
const target = new URL(rewriteTarget);
|
|
5586
|
-
url.protocol = target.protocol;
|
|
5587
|
-
url.username = target.username;
|
|
5588
|
-
url.password = target.password;
|
|
5589
|
-
url.hostname = target.hostname;
|
|
5590
|
-
url.port = target.port;
|
|
5591
|
-
return url.toString();
|
|
5592
|
-
}
|
|
5593
|
-
url.host = rewriteTarget;
|
|
5594
|
-
return url.toString();
|
|
5595
|
-
}
|
|
5596
|
-
function isLoopbackHostname(hostname) {
|
|
5597
|
-
return hostname === `localhost` || hostname === `127.0.0.1` || hostname === `::1`;
|
|
5598
|
-
}
|
|
5599
|
-
|
|
5600
5928
|
//#endregion
|
|
5601
5929
|
//#region src/routing/tenant-stream-paths.ts
|
|
5602
5930
|
function withoutLeadingSlash(path$1) {
|
|
@@ -5956,122 +6284,6 @@ async function proxyElectric(request, ctx) {
|
|
|
5956
6284
|
});
|
|
5957
6285
|
}
|
|
5958
6286
|
|
|
5959
|
-
//#endregion
|
|
5960
|
-
//#region src/routing/dispatch-policy.ts
|
|
5961
|
-
function subscriptionIdForDispatchTarget(target) {
|
|
5962
|
-
if (target.subscription_id) return target.subscription_id;
|
|
5963
|
-
if (target.type === `runner`) return `runner:${target.runnerId}`;
|
|
5964
|
-
const digest = createHash(`sha256`).update(target.url).digest(`hex`);
|
|
5965
|
-
return `webhook:${digest.slice(0, 16)}`;
|
|
5966
|
-
}
|
|
5967
|
-
function subscriptionIdForEntityDispatchTarget(target, entityUrl) {
|
|
5968
|
-
const base = subscriptionIdForDispatchTarget(target);
|
|
5969
|
-
if (!target.subscription_id) return base;
|
|
5970
|
-
const digest = createHash(`sha256`).update(entityUrl).digest(`hex`);
|
|
5971
|
-
return `${base}:${digest.slice(0, 16)}`;
|
|
5972
|
-
}
|
|
5973
|
-
async function resolveEffectiveDispatchPolicyForSpawn(ctx, typeName, opts) {
|
|
5974
|
-
if (opts.dispatchPolicy) return opts.dispatchPolicy;
|
|
5975
|
-
const entityType = await ctx.entityManager.registry.getEntityType(typeName);
|
|
5976
|
-
if (opts.parent) {
|
|
5977
|
-
const parent = await ctx.entityManager.registry.getEntity(opts.parent);
|
|
5978
|
-
if (parent?.dispatch_policy) return applyTypeDefaultSubscriptionScope(parent.dispatch_policy, entityType?.default_dispatch_policy);
|
|
5979
|
-
}
|
|
5980
|
-
return entityType?.default_dispatch_policy;
|
|
5981
|
-
}
|
|
5982
|
-
async function resolveEffectiveDispatchPolicyForEntity(ctx, entity) {
|
|
5983
|
-
if (entity.dispatch_policy) return entity.dispatch_policy;
|
|
5984
|
-
const entityType = await ctx.entityManager.registry.getEntityType(entity.type);
|
|
5985
|
-
return entityType?.default_dispatch_policy;
|
|
5986
|
-
}
|
|
5987
|
-
async function backfillEntityDispatchPolicy(ctx, entity) {
|
|
5988
|
-
if (entity.dispatch_policy) return entity;
|
|
5989
|
-
const dispatchPolicy = await resolveEffectiveDispatchPolicyForEntity(ctx, entity);
|
|
5990
|
-
if (!dispatchPolicy) return entity;
|
|
5991
|
-
return await ctx.entityManager.registry.updateEntityDispatchPolicy(entity.url, dispatchPolicy) ?? {
|
|
5992
|
-
...entity,
|
|
5993
|
-
dispatch_policy: dispatchPolicy
|
|
5994
|
-
};
|
|
5995
|
-
}
|
|
5996
|
-
function applyTypeDefaultSubscriptionScope(policy, typeDefault) {
|
|
5997
|
-
const target = policy.targets[0];
|
|
5998
|
-
const defaultTarget = typeDefault?.targets[0];
|
|
5999
|
-
if (!target || !defaultTarget?.subscription_id) return policy;
|
|
6000
|
-
if (!sameDispatchDestination(target, defaultTarget)) return policy;
|
|
6001
|
-
if (target.subscription_id === defaultTarget.subscription_id) return policy;
|
|
6002
|
-
return { targets: [{
|
|
6003
|
-
...target,
|
|
6004
|
-
subscription_id: defaultTarget.subscription_id
|
|
6005
|
-
}] };
|
|
6006
|
-
}
|
|
6007
|
-
function sameDispatchDestination(a, b) {
|
|
6008
|
-
if (a.type !== b.type) return false;
|
|
6009
|
-
if (a.type === `runner` && b.type === `runner`) return a.runnerId === b.runnerId;
|
|
6010
|
-
if (a.type === `webhook` && b.type === `webhook`) return a.url === b.url;
|
|
6011
|
-
return false;
|
|
6012
|
-
}
|
|
6013
|
-
async function assertDispatchPolicyAllowed(ctx, policy) {
|
|
6014
|
-
const target = policy?.targets[0];
|
|
6015
|
-
if (!target || target.type !== `runner`) return;
|
|
6016
|
-
const runner = await ctx.entityManager.registry.getRunner(target.runnerId);
|
|
6017
|
-
if (!runner) throw new ElectricAgentsError(ErrCodeNotFound, `Runner "${target.runnerId}" not found`, 404);
|
|
6018
|
-
if (!ctx.authenticatedUser) throw new ElectricAgentsError(ErrCodeUnauthorized, `Authentication is required for runner-targeted dispatch`, 401);
|
|
6019
|
-
if (runner.owner_user_id !== ctx.authenticatedUser.userId) throw new ElectricAgentsError(ErrCodeUnauthorized, `Runner dispatch requires the authenticated owner`, 403);
|
|
6020
|
-
}
|
|
6021
|
-
async function linkEntityDispatchSubscription(ctx, entity) {
|
|
6022
|
-
const dispatchPolicy = await resolveEffectiveDispatchPolicyForEntity(ctx, entity);
|
|
6023
|
-
const target = dispatchPolicy?.targets[0];
|
|
6024
|
-
if (!target) return;
|
|
6025
|
-
await linkStreamToTargetSubscription(ctx, target, entity);
|
|
6026
|
-
}
|
|
6027
|
-
async function unlinkEntityDispatchSubscription(ctx, entity) {
|
|
6028
|
-
const dispatchPolicy = await resolveEffectiveDispatchPolicyForEntity(ctx, entity);
|
|
6029
|
-
const target = dispatchPolicy?.targets[0];
|
|
6030
|
-
if (!target) return;
|
|
6031
|
-
const subscriptionId = subscriptionIdForEntityDispatchTarget(target, entity.url);
|
|
6032
|
-
await ctx.streamClient.removeSubscriptionStream(subscriptionId, entity.streams.main).catch(() => {});
|
|
6033
|
-
}
|
|
6034
|
-
async function linkStreamToTargetSubscription(ctx, target, entity) {
|
|
6035
|
-
const streamPath = entity.streams.main;
|
|
6036
|
-
const subscriptionId = subscriptionIdForEntityDispatchTarget(target, entity.url);
|
|
6037
|
-
const existing = await ctx.streamClient.getSubscription(subscriptionId);
|
|
6038
|
-
if (target.type === `runner`) {
|
|
6039
|
-
const runner = await ctx.entityManager.registry.getRunner(target.runnerId);
|
|
6040
|
-
if (!runner) throw new ElectricAgentsError(ErrCodeNotFound, `Runner "${target.runnerId}" not found`, 404);
|
|
6041
|
-
const wakeStream = runner.wake_stream || runnerWakeStream(target.runnerId);
|
|
6042
|
-
await ctx.streamClient.ensure(wakeStream, { contentType: `application/json` });
|
|
6043
|
-
if (!existing) {
|
|
6044
|
-
await ctx.streamClient.putSubscription(subscriptionId, {
|
|
6045
|
-
type: `pull-wake`,
|
|
6046
|
-
streams: [streamPath],
|
|
6047
|
-
wake_stream: wakeStream,
|
|
6048
|
-
description: `Electric Agents runner ${target.runnerId}`
|
|
6049
|
-
});
|
|
6050
|
-
return;
|
|
6051
|
-
}
|
|
6052
|
-
await ctx.streamClient.addSubscriptionStreams(subscriptionId, [streamPath]);
|
|
6053
|
-
return;
|
|
6054
|
-
}
|
|
6055
|
-
const webhookUrl = rewriteLoopbackWebhookUrl(target.url);
|
|
6056
|
-
if (!webhookUrl) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Webhook dispatch target must include a valid URL`, 400);
|
|
6057
|
-
const forwardUrl = appendPathToUrl(ctx.publicUrl, `/_electric/webhook-forward/${encodeURIComponent(subscriptionId)}`);
|
|
6058
|
-
if (!existing) await ctx.streamClient.putSubscription(subscriptionId, {
|
|
6059
|
-
type: `webhook`,
|
|
6060
|
-
streams: [streamPath],
|
|
6061
|
-
webhook: { url: forwardUrl },
|
|
6062
|
-
description: `Electric Agents webhook ${subscriptionId}`
|
|
6063
|
-
});
|
|
6064
|
-
else await ctx.streamClient.addSubscriptionStreams(subscriptionId, [streamPath]);
|
|
6065
|
-
await ctx.pgDb.insert(subscriptionWebhooks).values({
|
|
6066
|
-
tenantId: ctx.service,
|
|
6067
|
-
subscriptionId,
|
|
6068
|
-
webhookUrl
|
|
6069
|
-
}).onConflictDoUpdate({
|
|
6070
|
-
target: [subscriptionWebhooks.tenantId, subscriptionWebhooks.subscriptionId],
|
|
6071
|
-
set: { webhookUrl }
|
|
6072
|
-
});
|
|
6073
|
-
}
|
|
6074
|
-
|
|
6075
6287
|
//#endregion
|
|
6076
6288
|
//#region src/routing/entities-router.ts
|
|
6077
6289
|
const stringRecordSchema = Type.Record(Type.String(), Type.String());
|
|
@@ -6104,11 +6316,33 @@ const spawnBodySchema = Type.Object({
|
|
|
6104
6316
|
}))
|
|
6105
6317
|
});
|
|
6106
6318
|
const sendBodySchema = Type.Object({
|
|
6107
|
-
from: Type.Optional(Type.String()),
|
|
6108
6319
|
payload: Type.Optional(Type.Unknown()),
|
|
6109
6320
|
key: Type.Optional(Type.String()),
|
|
6110
6321
|
type: Type.Optional(Type.String()),
|
|
6111
|
-
|
|
6322
|
+
mode: Type.Optional(Type.Union([
|
|
6323
|
+
Type.Literal(`immediate`),
|
|
6324
|
+
Type.Literal(`queued`),
|
|
6325
|
+
Type.Literal(`paused`),
|
|
6326
|
+
Type.Literal(`steer`)
|
|
6327
|
+
])),
|
|
6328
|
+
position: Type.Optional(Type.String()),
|
|
6329
|
+
afterMs: Type.Optional(Type.Number()),
|
|
6330
|
+
from: Type.Optional(Type.String())
|
|
6331
|
+
});
|
|
6332
|
+
const inboxMessageBodySchema = Type.Object({
|
|
6333
|
+
payload: Type.Optional(Type.Unknown()),
|
|
6334
|
+
position: Type.Optional(Type.String()),
|
|
6335
|
+
mode: Type.Optional(Type.Union([
|
|
6336
|
+
Type.Literal(`immediate`),
|
|
6337
|
+
Type.Literal(`queued`),
|
|
6338
|
+
Type.Literal(`paused`),
|
|
6339
|
+
Type.Literal(`steer`)
|
|
6340
|
+
])),
|
|
6341
|
+
status: Type.Optional(Type.Union([
|
|
6342
|
+
Type.Literal(`pending`),
|
|
6343
|
+
Type.Literal(`processed`),
|
|
6344
|
+
Type.Literal(`cancelled`)
|
|
6345
|
+
]))
|
|
6112
6346
|
});
|
|
6113
6347
|
const forkBodySchema = Type.Object({
|
|
6114
6348
|
instance_id: Type.Optional(Type.String()),
|
|
@@ -6127,8 +6361,8 @@ const scheduleBodySchema = Type.Union([Type.Object({
|
|
|
6127
6361
|
payload: Type.Unknown(),
|
|
6128
6362
|
targetUrl: Type.Optional(Type.String()),
|
|
6129
6363
|
fireAt: Type.String(),
|
|
6130
|
-
|
|
6131
|
-
|
|
6364
|
+
messageType: Type.Optional(Type.String()),
|
|
6365
|
+
from: Type.Optional(Type.String())
|
|
6132
6366
|
})]);
|
|
6133
6367
|
const entitiesRegisterBodySchema = Type.Object({ tags: Type.Optional(stringRecordSchema) });
|
|
6134
6368
|
const entitiesRouter = Router({ base: `/_electric/entities` });
|
|
@@ -6139,6 +6373,8 @@ entitiesRouter.get(`/:type/:instanceId`, withExistingEntity, getEntity);
|
|
|
6139
6373
|
entitiesRouter.head(`/:type/:instanceId`, withExistingEntity, headEntity);
|
|
6140
6374
|
entitiesRouter.delete(`/:type/:instanceId`, withExistingEntity, killEntity);
|
|
6141
6375
|
entitiesRouter.post(`/:type/:instanceId/send`, withExistingEntity, withSchema(sendBodySchema), sendEntity);
|
|
6376
|
+
entitiesRouter.patch(`/:type/:instanceId/inbox/:messageKey`, withExistingEntity, withSchema(inboxMessageBodySchema), updateInboxMessage);
|
|
6377
|
+
entitiesRouter.delete(`/:type/:instanceId/inbox/:messageKey`, withExistingEntity, deleteInboxMessage);
|
|
6142
6378
|
entitiesRouter.post(`/:type/:instanceId/fork`, withExistingEntity, withSchema(forkBodySchema), forkEntity);
|
|
6143
6379
|
entitiesRouter.post(`/:type/:instanceId/tags/:tagKey`, withExistingEntity, withSchema(setTagBodySchema), setTag);
|
|
6144
6380
|
entitiesRouter.delete(`/:type/:instanceId/tags/:tagKey`, withExistingEntity, removeTag);
|
|
@@ -6147,6 +6383,11 @@ entitiesRouter.delete(`/:type/:instanceId/schedules/:scheduleId`, withExistingEn
|
|
|
6147
6383
|
function entityUrlFromSegments(type, instanceId) {
|
|
6148
6384
|
if (!type || !instanceId) return null;
|
|
6149
6385
|
if (type.startsWith(`_`) || type.includes(`*`) || instanceId.includes(`*`)) return null;
|
|
6386
|
+
if (type === `principal`) try {
|
|
6387
|
+
return principalUrl(decodeURIComponent(instanceId));
|
|
6388
|
+
} catch {
|
|
6389
|
+
return null;
|
|
6390
|
+
}
|
|
6150
6391
|
return `/${type}/${instanceId}`;
|
|
6151
6392
|
}
|
|
6152
6393
|
function firstQueryValue$1(value) {
|
|
@@ -6156,12 +6397,27 @@ function requireExistingEntityRoute(request) {
|
|
|
6156
6397
|
if (!request.entityRoute) throw new Error(`existing entity middleware did not run`);
|
|
6157
6398
|
return request.entityRoute;
|
|
6158
6399
|
}
|
|
6400
|
+
function rejectPrincipalEntityMutation(request, action) {
|
|
6401
|
+
const { entity } = requireExistingEntityRoute(request);
|
|
6402
|
+
if (entity.type !== `principal`) return void 0;
|
|
6403
|
+
return apiError(400, ErrCodeInvalidRequest, `Principal entities are built in and cannot be ${action}`);
|
|
6404
|
+
}
|
|
6159
6405
|
async function withExistingEntity(request, ctx) {
|
|
6160
6406
|
const entityUrl = entityUrlFromSegments(request.params.type, request.params.instanceId);
|
|
6161
6407
|
if (!entityUrl) return void 0;
|
|
6162
6408
|
const entity = await ctx.entityManager.registry.getEntity(entityUrl);
|
|
6163
6409
|
if (!entity) {
|
|
6164
6410
|
const entityType = await ctx.entityManager.registry.getEntityType(request.params.type);
|
|
6411
|
+
if (request.params.type === `principal`) try {
|
|
6412
|
+
const materialized = await ctx.entityManager.ensurePrincipal(parsePrincipalKey(decodeURIComponent(request.params.instanceId)));
|
|
6413
|
+
request.entityRoute = {
|
|
6414
|
+
entityUrl,
|
|
6415
|
+
entity: materialized
|
|
6416
|
+
};
|
|
6417
|
+
return void 0;
|
|
6418
|
+
} catch (error) {
|
|
6419
|
+
return apiError(400, ErrCodeInvalidRequest, error instanceof Error ? error.message : `Invalid principal`);
|
|
6420
|
+
}
|
|
6165
6421
|
if (entityType) return apiError(404, ErrCodeNotFound, `Entity not found at ${entityUrl}`);
|
|
6166
6422
|
return apiError(404, ErrCodeUnknownEntityType, `Entity type "${request.params.type}" not found`);
|
|
6167
6423
|
}
|
|
@@ -6173,6 +6429,7 @@ async function withExistingEntity(request, ctx) {
|
|
|
6173
6429
|
}
|
|
6174
6430
|
async function withSpawnableEntityType(request, ctx) {
|
|
6175
6431
|
if (!entityUrlFromSegments(request.params.type, request.params.instanceId)) return void 0;
|
|
6432
|
+
if (request.params.type === `principal`) return apiError(400, ErrCodeInvalidRequest, `Principal entities are built in and cannot be spawned directly`);
|
|
6176
6433
|
const entityType = await ctx.entityManager.registry.getEntityType(request.params.type);
|
|
6177
6434
|
if (!entityType) return apiError(404, ErrCodeUnknownEntityType, `Entity type "${request.params.type}" not found`);
|
|
6178
6435
|
return void 0;
|
|
@@ -6181,7 +6438,8 @@ async function listEntities({ query }, ctx) {
|
|
|
6181
6438
|
const { entities: entities$1 } = await ctx.entityManager.registry.listEntities({
|
|
6182
6439
|
type: firstQueryValue$1(query.type),
|
|
6183
6440
|
status: firstQueryValue$1(query.status),
|
|
6184
|
-
parent: firstQueryValue$1(query.parent)
|
|
6441
|
+
parent: firstQueryValue$1(query.parent),
|
|
6442
|
+
created_by: firstQueryValue$1(query.created_by)
|
|
6185
6443
|
});
|
|
6186
6444
|
return json(entities$1.map((entity) => toPublicEntity(entity)));
|
|
6187
6445
|
}
|
|
@@ -6191,6 +6449,8 @@ async function registerEntitiesSource(request, ctx) {
|
|
|
6191
6449
|
return json(result);
|
|
6192
6450
|
}
|
|
6193
6451
|
async function upsertSchedule(request, ctx) {
|
|
6452
|
+
const principalMutationError = rejectPrincipalEntityMutation(request, `scheduled`);
|
|
6453
|
+
if (principalMutationError) return principalMutationError;
|
|
6194
6454
|
const parsed = routeBody(request);
|
|
6195
6455
|
const { entityUrl } = requireExistingEntityRoute(request);
|
|
6196
6456
|
const scheduleId = decodeURIComponent(request.params.scheduleId);
|
|
@@ -6206,12 +6466,13 @@ async function upsertSchedule(request, ctx) {
|
|
|
6206
6466
|
return json(result);
|
|
6207
6467
|
}
|
|
6208
6468
|
if (parsed.scheduleType === `future_send`) {
|
|
6469
|
+
if (parsed.from !== void 0 && parsed.from !== ctx.principal.url) return apiError(400, ErrCodeInvalidRequest, `Request from must match Electric-Principal`);
|
|
6209
6470
|
const result = await ctx.entityManager.upsertFutureSendSchedule(entityUrl, {
|
|
6210
6471
|
id: scheduleId,
|
|
6211
6472
|
payload: parsed.payload,
|
|
6212
6473
|
targetUrl: parsed.targetUrl,
|
|
6213
6474
|
fireAt: parsed.fireAt,
|
|
6214
|
-
|
|
6475
|
+
senderUrl: ctx.principal.url,
|
|
6215
6476
|
messageType: parsed.messageType
|
|
6216
6477
|
});
|
|
6217
6478
|
return json(result);
|
|
@@ -6219,11 +6480,15 @@ async function upsertSchedule(request, ctx) {
|
|
|
6219
6480
|
throw new Error(`schedule schema accepted an unknown scheduleType`);
|
|
6220
6481
|
}
|
|
6221
6482
|
async function deleteSchedule(request, ctx) {
|
|
6483
|
+
const principalMutationError = rejectPrincipalEntityMutation(request, `unscheduled`);
|
|
6484
|
+
if (principalMutationError) return principalMutationError;
|
|
6222
6485
|
const { entityUrl } = requireExistingEntityRoute(request);
|
|
6223
6486
|
const result = await ctx.entityManager.deleteSchedule(entityUrl, { id: decodeURIComponent(request.params.scheduleId) });
|
|
6224
6487
|
return json(result);
|
|
6225
6488
|
}
|
|
6226
6489
|
async function setTag(request, ctx) {
|
|
6490
|
+
const principalMutationError = rejectPrincipalEntityMutation(request, `tagged`);
|
|
6491
|
+
if (principalMutationError) return principalMutationError;
|
|
6227
6492
|
const parsed = routeBody(request);
|
|
6228
6493
|
const { entityUrl } = requireExistingEntityRoute(request);
|
|
6229
6494
|
const token = writeTokenFromRequest(request);
|
|
@@ -6231,12 +6496,16 @@ async function setTag(request, ctx) {
|
|
|
6231
6496
|
return json(toPublicEntity(updated));
|
|
6232
6497
|
}
|
|
6233
6498
|
async function removeTag(request, ctx) {
|
|
6499
|
+
const principalMutationError = rejectPrincipalEntityMutation(request, `untagged`);
|
|
6500
|
+
if (principalMutationError) return principalMutationError;
|
|
6234
6501
|
const { entityUrl } = requireExistingEntityRoute(request);
|
|
6235
6502
|
const token = writeTokenFromRequest(request);
|
|
6236
6503
|
const updated = await ctx.entityManager.removeTag(entityUrl, decodeURIComponent(request.params.tagKey), token);
|
|
6237
6504
|
return json(toPublicEntity(updated));
|
|
6238
6505
|
}
|
|
6239
6506
|
async function forkEntity(request, ctx) {
|
|
6507
|
+
const principalMutationError = rejectPrincipalEntityMutation(request, `forked`);
|
|
6508
|
+
if (principalMutationError) return principalMutationError;
|
|
6240
6509
|
const parsed = routeBody(request);
|
|
6241
6510
|
const { entityUrl, entity } = requireExistingEntityRoute(request);
|
|
6242
6511
|
await assertDispatchPolicyAllowed(ctx, entity.dispatch_policy);
|
|
@@ -6252,27 +6521,47 @@ async function forkEntity(request, ctx) {
|
|
|
6252
6521
|
}
|
|
6253
6522
|
async function sendEntity(request, ctx) {
|
|
6254
6523
|
const parsed = routeBody(request);
|
|
6524
|
+
const principal = ctx.principal;
|
|
6525
|
+
if (parsed.from !== void 0 && parsed.from !== principal.url) return apiError(400, ErrCodeInvalidRequest, `Request from must match Electric-Principal`);
|
|
6526
|
+
await ctx.entityManager.ensurePrincipal(principal);
|
|
6255
6527
|
const { entityUrl, entity } = requireExistingEntityRoute(request);
|
|
6256
6528
|
if (!entity.dispatch_policy) {
|
|
6257
6529
|
const updatedEntity = await backfillEntityDispatchPolicy(ctx, entity);
|
|
6258
6530
|
await linkEntityDispatchSubscription(ctx, updatedEntity);
|
|
6259
6531
|
}
|
|
6260
6532
|
if (parsed.afterMs && parsed.afterMs > 0) await ctx.entityManager.enqueueDelayedSend(entityUrl, {
|
|
6261
|
-
from:
|
|
6533
|
+
from: principal.url,
|
|
6262
6534
|
payload: parsed.payload,
|
|
6263
6535
|
key: parsed.key,
|
|
6264
|
-
type: parsed.type
|
|
6536
|
+
type: parsed.type,
|
|
6537
|
+
mode: parsed.mode,
|
|
6538
|
+
position: parsed.position
|
|
6265
6539
|
}, new Date(Date.now() + parsed.afterMs));
|
|
6266
6540
|
else await ctx.entityManager.send(entityUrl, {
|
|
6267
|
-
from:
|
|
6541
|
+
from: principal.url,
|
|
6268
6542
|
payload: parsed.payload,
|
|
6269
6543
|
key: parsed.key,
|
|
6270
|
-
type: parsed.type
|
|
6544
|
+
type: parsed.type,
|
|
6545
|
+
mode: parsed.mode,
|
|
6546
|
+
position: parsed.position
|
|
6271
6547
|
});
|
|
6272
6548
|
return status(204);
|
|
6273
6549
|
}
|
|
6550
|
+
async function updateInboxMessage(request, ctx) {
|
|
6551
|
+
const parsed = routeBody(request);
|
|
6552
|
+
const { entityUrl } = requireExistingEntityRoute(request);
|
|
6553
|
+
await ctx.entityManager.updateInboxMessage(entityUrl, decodeURIComponent(request.params.messageKey), parsed);
|
|
6554
|
+
return status(204);
|
|
6555
|
+
}
|
|
6556
|
+
async function deleteInboxMessage(request, ctx) {
|
|
6557
|
+
const { entityUrl } = requireExistingEntityRoute(request);
|
|
6558
|
+
await ctx.entityManager.deleteInboxMessage(entityUrl, decodeURIComponent(request.params.messageKey));
|
|
6559
|
+
return status(204);
|
|
6560
|
+
}
|
|
6274
6561
|
async function spawnEntity(request, ctx) {
|
|
6275
6562
|
const parsed = routeBody(request);
|
|
6563
|
+
const principal = ctx.principal;
|
|
6564
|
+
await ctx.entityManager.ensurePrincipal(principal);
|
|
6276
6565
|
const dispatchPolicy = await resolveEffectiveDispatchPolicyForSpawn(ctx, request.params.type, {
|
|
6277
6566
|
dispatchPolicy: parsed.dispatch_policy,
|
|
6278
6567
|
parent: parsed.parent
|
|
@@ -6285,11 +6574,12 @@ async function spawnEntity(request, ctx) {
|
|
|
6285
6574
|
parent: parsed.parent,
|
|
6286
6575
|
dispatch_policy: dispatchPolicy,
|
|
6287
6576
|
initialMessage: void 0,
|
|
6288
|
-
wake: parsed.wake
|
|
6577
|
+
wake: parsed.wake,
|
|
6578
|
+
created_by: principal.url
|
|
6289
6579
|
});
|
|
6290
6580
|
await linkEntityDispatchSubscription(ctx, entity);
|
|
6291
6581
|
if (parsed.initialMessage !== void 0) await ctx.entityManager.send(entity.url, {
|
|
6292
|
-
from:
|
|
6582
|
+
from: principal.url,
|
|
6293
6583
|
payload: parsed.initialMessage
|
|
6294
6584
|
});
|
|
6295
6585
|
return json({
|
|
@@ -6307,6 +6597,8 @@ function headEntity() {
|
|
|
6307
6597
|
return status(200);
|
|
6308
6598
|
}
|
|
6309
6599
|
async function killEntity(request, ctx) {
|
|
6600
|
+
const principalMutationError = rejectPrincipalEntityMutation(request, `killed`);
|
|
6601
|
+
if (principalMutationError) return principalMutationError;
|
|
6310
6602
|
const { entityUrl, entity } = requireExistingEntityRoute(request);
|
|
6311
6603
|
await unlinkEntityDispatchSubscription(ctx, entity);
|
|
6312
6604
|
const result = await ctx.entityManager.kill(entityUrl);
|
|
@@ -6453,7 +6745,7 @@ function applyCors(response) {
|
|
|
6453
6745
|
const headers = new Headers(response.headers);
|
|
6454
6746
|
headers.set(`access-control-allow-origin`, `*`);
|
|
6455
6747
|
headers.set(`access-control-allow-methods`, `GET, POST, PUT, PATCH, DELETE, OPTIONS`);
|
|
6456
|
-
headers.set(`access-control-allow-headers`, `content-type, authorization, electric-claim-token,
|
|
6748
|
+
headers.set(`access-control-allow-headers`, `content-type, authorization, electric-claim-token, ngrok-skip-browser-warning`);
|
|
6457
6749
|
headers.set(`access-control-expose-headers`, `*`);
|
|
6458
6750
|
return new Response(response.body, {
|
|
6459
6751
|
status: response.status,
|
|
@@ -6533,9 +6825,9 @@ function firstQueryValue(value) {
|
|
|
6533
6825
|
}
|
|
6534
6826
|
async function registerRunner(request, ctx) {
|
|
6535
6827
|
const parsed = routeBody(request);
|
|
6536
|
-
const ownerUserId = parsed.owner_user_id ?? ctx.
|
|
6828
|
+
const ownerUserId = parsed.owner_user_id ?? ctx.principal?.key;
|
|
6537
6829
|
if (!ownerUserId) throw new ElectricAgentsError(ErrCodeInvalidRequest, `owner_user_id is required when no authenticated user is present`, 400);
|
|
6538
|
-
if (ctx.
|
|
6830
|
+
if (ctx.principal && ownerUserId !== ctx.principal.key) throw new ElectricAgentsError(ErrCodeUnauthorized, `owner_user_id must match the authenticated user`, 403);
|
|
6539
6831
|
const runner = await ctx.entityManager.registry.createRunner({
|
|
6540
6832
|
id: parsed.id,
|
|
6541
6833
|
ownerUserId,
|
|
@@ -6549,8 +6841,8 @@ async function registerRunner(request, ctx) {
|
|
|
6549
6841
|
}
|
|
6550
6842
|
async function listRunners(request, ctx) {
|
|
6551
6843
|
const requestedOwner = firstQueryValue(request.query.owner_user_id);
|
|
6552
|
-
if (ctx.
|
|
6553
|
-
const runners$1 = await ctx.entityManager.registry.listRunners({ ownerUserId: ctx.
|
|
6844
|
+
if (ctx.principal && requestedOwner && requestedOwner !== ctx.principal.key) throw new ElectricAgentsError(ErrCodeUnauthorized, `owner_user_id must match the authenticated user`, 403);
|
|
6845
|
+
const runners$1 = await ctx.entityManager.registry.listRunners({ ownerUserId: ctx.principal?.key ?? requestedOwner });
|
|
6554
6846
|
return json(runners$1);
|
|
6555
6847
|
}
|
|
6556
6848
|
async function getRunner(request, ctx) {
|
|
@@ -6588,9 +6880,8 @@ async function setRunnerStatus(request, ctx, adminStatus) {
|
|
|
6588
6880
|
}
|
|
6589
6881
|
async function claimWake(request, ctx) {
|
|
6590
6882
|
const runnerId = routeParam$1(request, `id`);
|
|
6591
|
-
if (!ctx.authenticatedUser) throw new ElectricAgentsError(ErrCodeUnauthorized, `Authentication is required to claim runner work`, 401);
|
|
6592
6883
|
const runner = await requireRunner(ctx, runnerId);
|
|
6593
|
-
if (runner.owner_user_id !== ctx.
|
|
6884
|
+
if (ctx.principal && runner.owner_user_id !== ctx.principal.key) throw new ElectricAgentsError(ErrCodeUnauthorized, `Runner claim requires the authenticated owner`, 403);
|
|
6594
6885
|
if (runner.admin_status !== `enabled`) throw new ElectricAgentsError(ErrCodeNotRunning, `Runner is disabled`, 409);
|
|
6595
6886
|
const parsed = routeBody(request);
|
|
6596
6887
|
const expectedSubscriptionId = subscriptionIdForDispatchTarget({
|
|
@@ -6622,8 +6913,8 @@ async function requireRunner(ctx, runnerId) {
|
|
|
6622
6913
|
return runner;
|
|
6623
6914
|
}
|
|
6624
6915
|
function assertRunnerOwnerIfAuthenticated(ctx, ownerUserId) {
|
|
6625
|
-
if (!ctx.
|
|
6626
|
-
if (ownerUserId === ctx.
|
|
6916
|
+
if (!ctx.principal) return;
|
|
6917
|
+
if (ownerUserId === ctx.principal.key) return;
|
|
6627
6918
|
throw new ElectricAgentsError(ErrCodeUnauthorized, `Runner access requires the authenticated owner`, 403);
|
|
6628
6919
|
}
|
|
6629
6920
|
async function notificationFromClaim(ctx, input) {
|
|
@@ -6680,8 +6971,10 @@ async function notificationFromClaim(ctx, input) {
|
|
|
6680
6971
|
url: entity.url,
|
|
6681
6972
|
streams: entity.streams,
|
|
6682
6973
|
tags: entity.tags,
|
|
6683
|
-
spawnArgs: entity.spawn_args
|
|
6684
|
-
|
|
6974
|
+
spawnArgs: entity.spawn_args,
|
|
6975
|
+
createdBy: entity.created_by
|
|
6976
|
+
},
|
|
6977
|
+
principal: principalFromCreatedBy(entity.created_by)
|
|
6685
6978
|
};
|
|
6686
6979
|
}
|
|
6687
6980
|
|