@electric-ax/agents-server 0.4.5 → 0.4.7
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 +404 -68
- package/dist/index.cjs +421 -67
- package/dist/index.d.cts +97 -11
- package/dist/index.d.ts +97 -11
- package/dist/index.js +414 -69
- package/drizzle/0009_entity_signal_statuses.sql +3 -0
- package/drizzle/meta/_journal.json +7 -0
- package/package.json +6 -6
- package/src/db/schema.ts +1 -1
- package/src/electric-agents-types.ts +76 -1
- package/src/entity-manager.ts +256 -33
- package/src/entity-registry.ts +57 -20
- package/src/entrypoint-lib.ts +5 -0
- package/src/index.ts +33 -0
- package/src/routing/context.ts +6 -0
- package/src/routing/dispatch-policy.ts +6 -0
- package/src/routing/durable-streams-router.ts +62 -13
- package/src/routing/entities-router.ts +57 -1
- package/src/routing/internal-router.ts +147 -23
- package/src/routing/runners-router.ts +1 -1
- package/src/runtime.ts +5 -1
- package/src/server.ts +18 -0
- package/src/stream-client.ts +10 -4
- package/src/webhook-signing.ts +173 -0
package/dist/index.cjs
CHANGED
|
@@ -104,7 +104,7 @@ const entities = (0, drizzle_orm_pg_core.pgTable)(`entities`, {
|
|
|
104
104
|
(0, drizzle_orm_pg_core.index)(`idx_entities_parent`).on(table.tenantId, table.parent),
|
|
105
105
|
(0, drizzle_orm_pg_core.index)(`idx_entities_created_by`).on(table.tenantId, table.createdBy),
|
|
106
106
|
(0, drizzle_orm_pg_core.index)(`entities_tags_index_gin`).using(`gin`, table.tagsIndex),
|
|
107
|
-
(0, drizzle_orm_pg_core.check)(`chk_entities_status`, drizzle_orm.sql`${table.status} IN ('spawning', 'running', 'idle', 'stopped')`)
|
|
107
|
+
(0, drizzle_orm_pg_core.check)(`chk_entities_status`, drizzle_orm.sql`${table.status} IN ('spawning', 'running', 'idle', 'paused', 'stopping', 'stopped', 'killed')`)
|
|
108
108
|
]);
|
|
109
109
|
const users = (0, drizzle_orm_pg_core.pgTable)(`users`, {
|
|
110
110
|
tenantId: (0, drizzle_orm_pg_core.text)(`tenant_id`).notNull().default(`default`),
|
|
@@ -358,12 +358,25 @@ async function runMigrations(postgresUrl) {
|
|
|
358
358
|
|
|
359
359
|
//#endregion
|
|
360
360
|
//#region src/electric-agents-types.ts
|
|
361
|
+
const ENTITY_SIGNALS = [
|
|
362
|
+
`SIGINT`,
|
|
363
|
+
`SIGHUP`,
|
|
364
|
+
`SIGTERM`,
|
|
365
|
+
`SIGKILL`,
|
|
366
|
+
`SIGSTOP`,
|
|
367
|
+
`SIGCONT`,
|
|
368
|
+
`SIGUSR`
|
|
369
|
+
];
|
|
361
370
|
const VALID_ENTITY_STATUSES = new Set([
|
|
362
371
|
`spawning`,
|
|
363
372
|
`running`,
|
|
364
373
|
`idle`,
|
|
365
|
-
`
|
|
374
|
+
`paused`,
|
|
375
|
+
`stopping`,
|
|
376
|
+
`stopped`,
|
|
377
|
+
`killed`
|
|
366
378
|
]);
|
|
379
|
+
const VALID_ENTITY_SIGNALS = new Set(ENTITY_SIGNALS);
|
|
367
380
|
function assertEntityStatus(s) {
|
|
368
381
|
if (!VALID_ENTITY_STATUSES.has(s)) throw new Error(`Invalid entity status: "${s}"`);
|
|
369
382
|
return s;
|
|
@@ -384,6 +397,27 @@ function assertRunnerAdminStatus(s) {
|
|
|
384
397
|
if (!VALID_RUNNER_ADMIN_STATUSES.has(s)) throw new Error(`Invalid runner admin status: "${s}"`);
|
|
385
398
|
return s;
|
|
386
399
|
}
|
|
400
|
+
function assertEntitySignal(s) {
|
|
401
|
+
if (!VALID_ENTITY_SIGNALS.has(s)) throw new Error(`Invalid entity signal: "${s}"`);
|
|
402
|
+
return s;
|
|
403
|
+
}
|
|
404
|
+
function isTerminalEntityStatus(status$4) {
|
|
405
|
+
return status$4 === `stopped` || status$4 === `killed`;
|
|
406
|
+
}
|
|
407
|
+
function rejectsNormalWrites(status$4) {
|
|
408
|
+
return status$4 === `stopping` || isTerminalEntityStatus(status$4);
|
|
409
|
+
}
|
|
410
|
+
function expectedSignalStatus(status$4, signal) {
|
|
411
|
+
switch (signal) {
|
|
412
|
+
case `SIGKILL`: return `killed`;
|
|
413
|
+
case `SIGTERM`: return status$4 === `idle` ? `stopped` : `stopping`;
|
|
414
|
+
case `SIGSTOP`: return status$4 === `idle` ? `paused` : status$4;
|
|
415
|
+
case `SIGCONT`: return status$4 === `paused` ? `idle` : status$4;
|
|
416
|
+
case `SIGINT`:
|
|
417
|
+
case `SIGHUP`:
|
|
418
|
+
case `SIGUSR`: return status$4;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
387
421
|
/** Strip internal fields (write_token, subscription_id) from an entity. */
|
|
388
422
|
function toPublicEntity(entity) {
|
|
389
423
|
return {
|
|
@@ -405,6 +439,7 @@ const ErrCodeUnauthorized = `UNAUTHORIZED`;
|
|
|
405
439
|
const ErrCodeNotFound = `NOT_FOUND`;
|
|
406
440
|
const ErrCodeNotRunning = `NOT_RUNNING`;
|
|
407
441
|
const ErrCodeInvalidRequest = `INVALID_REQUEST`;
|
|
442
|
+
const ErrCodeInvalidSignal = `INVALID_SIGNAL`;
|
|
408
443
|
const ErrCodeUnknownEntityType = `UNKNOWN_ENTITY_TYPE`;
|
|
409
444
|
const ErrCodeSchemaValidationFailed = `SCHEMA_VALIDATION_FAILED`;
|
|
410
445
|
const ErrCodeUnknownMessageType = `UNKNOWN_MESSAGE_TYPE`;
|
|
@@ -600,7 +635,7 @@ var PostgresRegistry = class {
|
|
|
600
635
|
const heartbeatAt = input.heartbeatAt ?? new Date();
|
|
601
636
|
await this.db.update(consumerClaims).set({
|
|
602
637
|
lastHeartbeatAt: heartbeatAt,
|
|
603
|
-
leaseExpiresAt: input.leaseExpiresAt
|
|
638
|
+
...input.leaseExpiresAt !== void 0 ? { leaseExpiresAt: input.leaseExpiresAt } : {},
|
|
604
639
|
updatedAt: heartbeatAt
|
|
605
640
|
}).where((0, drizzle_orm.and)((0, drizzle_orm.eq)(consumerClaims.tenantId, this.tenantId), (0, drizzle_orm.eq)(consumerClaims.consumerId, input.consumerId), (0, drizzle_orm.eq)(consumerClaims.epoch, input.epoch)));
|
|
606
641
|
}
|
|
@@ -613,17 +648,24 @@ var PostgresRegistry = class {
|
|
|
613
648
|
updatedAt: releasedAt
|
|
614
649
|
}).where((0, drizzle_orm.and)((0, drizzle_orm.eq)(consumerClaims.tenantId, this.tenantId), (0, drizzle_orm.eq)(consumerClaims.consumerId, input.consumerId), (0, drizzle_orm.eq)(consumerClaims.epoch, input.epoch))).returning();
|
|
615
650
|
const claim = rows[0] ? this.rowToConsumerClaim(rows[0]) : null;
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
651
|
+
let entityCleared = false;
|
|
652
|
+
if (claim) {
|
|
653
|
+
const cleared = await this.db.update(entityDispatchState).set({
|
|
654
|
+
activeConsumerId: null,
|
|
655
|
+
activeRunnerId: null,
|
|
656
|
+
activeEpoch: null,
|
|
657
|
+
activeClaimedAt: null,
|
|
658
|
+
activeLeaseExpiresAt: null,
|
|
659
|
+
lastReleasedAt: releasedAt,
|
|
660
|
+
lastCompletedAt: releasedAt,
|
|
661
|
+
updatedAt: releasedAt
|
|
662
|
+
}).where((0, drizzle_orm.and)((0, drizzle_orm.eq)(entityDispatchState.tenantId, this.tenantId), (0, drizzle_orm.eq)(entityDispatchState.entityUrl, claim.entity_url), (0, drizzle_orm.eq)(entityDispatchState.activeConsumerId, input.consumerId), (0, drizzle_orm.eq)(entityDispatchState.activeEpoch, input.epoch))).returning({ entityUrl: entityDispatchState.entityUrl });
|
|
663
|
+
entityCleared = cleared.length > 0;
|
|
664
|
+
}
|
|
665
|
+
return {
|
|
666
|
+
claim,
|
|
667
|
+
entityCleared
|
|
668
|
+
};
|
|
627
669
|
}
|
|
628
670
|
async getActiveClaimsForRunner(runnerId) {
|
|
629
671
|
const rows = await this.db.select().from(consumerClaims).where((0, drizzle_orm.and)((0, drizzle_orm.eq)(consumerClaims.tenantId, this.tenantId), (0, drizzle_orm.eq)(consumerClaims.runnerId, runnerId), (0, drizzle_orm.eq)(consumerClaims.status, `active`)));
|
|
@@ -799,7 +841,7 @@ var PostgresRegistry = class {
|
|
|
799
841
|
};
|
|
800
842
|
}
|
|
801
843
|
async updateStatus(entityUrl, status$4) {
|
|
802
|
-
const whereClause = status$4
|
|
844
|
+
const whereClause = isTerminalEntityStatus(status$4) ? this.entityWhere(entityUrl) : (0, drizzle_orm.and)(this.entityWhere(entityUrl), (0, drizzle_orm.ne)(entities.status, `stopped`), (0, drizzle_orm.ne)(entities.status, `killed`));
|
|
803
845
|
await this.db.update(entities).set({
|
|
804
846
|
status: status$4,
|
|
805
847
|
updatedAt: Date.now()
|
|
@@ -807,13 +849,17 @@ var PostgresRegistry = class {
|
|
|
807
849
|
}
|
|
808
850
|
async updateStatusWithTxid(entityUrl, status$4) {
|
|
809
851
|
return await this.db.transaction(async (tx) => {
|
|
810
|
-
const
|
|
811
|
-
await tx.update(entities).set({
|
|
852
|
+
const rows = await tx.update(entities).set({
|
|
812
853
|
status: status$4,
|
|
813
854
|
updatedAt: Date.now()
|
|
814
|
-
}).where(
|
|
815
|
-
|
|
816
|
-
|
|
855
|
+
}).where((0, drizzle_orm.and)(this.entityWhere(entityUrl), (0, drizzle_orm.ne)(entities.status, `stopped`), (0, drizzle_orm.ne)(entities.status, `killed`))).returning({ txid: drizzle_orm.sql`pg_current_xact_id()::xid::text` });
|
|
856
|
+
return rows[0] ? parseInt(rows[0].txid) : null;
|
|
857
|
+
});
|
|
858
|
+
}
|
|
859
|
+
async touchEntityWithTxid(entityUrl) {
|
|
860
|
+
return await this.db.transaction(async (tx) => {
|
|
861
|
+
const rows = await tx.update(entities).set({ updatedAt: Date.now() }).where((0, drizzle_orm.and)((0, drizzle_orm.eq)(entities.url, entityUrl), (0, drizzle_orm.ne)(entities.status, `stopped`), (0, drizzle_orm.ne)(entities.status, `killed`))).returning({ txid: drizzle_orm.sql`pg_current_xact_id()::xid::text` });
|
|
862
|
+
return rows[0] ? parseInt(rows[0].txid) : null;
|
|
817
863
|
});
|
|
818
864
|
}
|
|
819
865
|
async setEntityTag(url, key, value) {
|
|
@@ -2440,6 +2486,9 @@ async function assertDispatchPolicyAllowed(ctx, policy) {
|
|
|
2440
2486
|
if (!runner) throw new ElectricAgentsError(ErrCodeNotFound, `Runner "${target.runnerId}" not found`, 404);
|
|
2441
2487
|
if (runner.owner_principal !== ctx.principal.url) throw new ElectricAgentsError(ErrCodeUnauthorized, `Runner dispatch requires the authenticated owner`, 403);
|
|
2442
2488
|
}
|
|
2489
|
+
function shouldLinkDispatchBeforeInitialMessage(policy) {
|
|
2490
|
+
return policy?.targets[0] !== void 0;
|
|
2491
|
+
}
|
|
2443
2492
|
async function linkEntityDispatchSubscription(ctx, entity) {
|
|
2444
2493
|
const dispatchPolicy = await resolveEffectiveDispatchPolicyForEntity(ctx, entity);
|
|
2445
2494
|
const target = dispatchPolicy?.targets[0];
|
|
@@ -2665,6 +2714,7 @@ function createInitialQueuePosition(date) {
|
|
|
2665
2714
|
}
|
|
2666
2715
|
const DEFAULT_FORK_WAIT_TIMEOUT_MS = 12e4;
|
|
2667
2716
|
const DEFAULT_FORK_WAIT_POLL_MS = 250;
|
|
2717
|
+
const SERVER_SIGNAL_SENDER = `/_electric/server`;
|
|
2668
2718
|
function sleep(ms) {
|
|
2669
2719
|
return new Promise((resolve$1) => setTimeout(resolve$1, ms));
|
|
2670
2720
|
}
|
|
@@ -3116,16 +3166,16 @@ var EntityManager = class {
|
|
|
3116
3166
|
if (!root) throw new ElectricAgentsError(ErrCodeNotFound, `Entity not found`, 404);
|
|
3117
3167
|
if (root.parent) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Only top-level entities can be forked`, 400);
|
|
3118
3168
|
const subtree = await this.listEntitySubtree(root);
|
|
3119
|
-
const stopped = subtree.find((entity) => entity.status
|
|
3120
|
-
if (stopped) throw new ElectricAgentsError(ErrCodeNotRunning, `Cannot fork
|
|
3121
|
-
let active = subtree.filter((entity) => entity.status !== `idle`);
|
|
3169
|
+
const stopped = subtree.find((entity) => isTerminalEntityStatus(entity.status));
|
|
3170
|
+
if (stopped) throw new ElectricAgentsError(ErrCodeNotRunning, `Cannot fork terminal entity "${stopped.url}"`, 409);
|
|
3171
|
+
let active = subtree.filter((entity) => entity.status !== `idle` && entity.status !== `paused`);
|
|
3122
3172
|
if (active.length === 0) {
|
|
3123
3173
|
this.addForkLocks(this.forkWorkLockedEntities, subtree.map((entity) => entity.url), workLocks);
|
|
3124
3174
|
const lockedRoot = await this.registry.getEntity(rootUrl);
|
|
3125
3175
|
if (!lockedRoot) throw new ElectricAgentsError(ErrCodeNotFound, `Entity not found`, 404);
|
|
3126
3176
|
const lockedSubtree = await this.listEntitySubtree(lockedRoot);
|
|
3127
3177
|
this.addForkLocks(this.forkWorkLockedEntities, lockedSubtree.map((entity) => entity.url), workLocks);
|
|
3128
|
-
const lockedActive = lockedSubtree.filter((entity) => entity.status !== `idle`);
|
|
3178
|
+
const lockedActive = lockedSubtree.filter((entity) => entity.status !== `idle` && entity.status !== `paused`);
|
|
3129
3179
|
if (lockedActive.length === 0) return lockedSubtree;
|
|
3130
3180
|
this.releaseForkLocks(this.forkWorkLockedEntities, workLocks);
|
|
3131
3181
|
active = lockedActive;
|
|
@@ -3581,6 +3631,11 @@ var EntityManager = class {
|
|
|
3581
3631
|
if (req.position) value.position = req.position;
|
|
3582
3632
|
else if (value.mode === `queued` || value.mode === `paused`) value.position = createInitialQueuePosition(new Date(now));
|
|
3583
3633
|
if (value.status === `processed`) value.processed_at = now;
|
|
3634
|
+
const wakePausedEntity = entity.status === `paused` && req.mode !== `paused`;
|
|
3635
|
+
if (wakePausedEntity) {
|
|
3636
|
+
await this.registry.updateStatus(entityUrl, `idle`);
|
|
3637
|
+
await this.entityBridgeManager?.onEntityChanged(entityUrl);
|
|
3638
|
+
}
|
|
3584
3639
|
const envelope = __electric_ax_agents_runtime.entityStateSchema.inbox.insert({
|
|
3585
3640
|
key,
|
|
3586
3641
|
value
|
|
@@ -3608,7 +3663,7 @@ var EntityManager = class {
|
|
|
3608
3663
|
async updateInboxMessage(entityUrl, key, req) {
|
|
3609
3664
|
const entity = await this.registry.getEntity(entityUrl);
|
|
3610
3665
|
if (!entity) throw new ElectricAgentsError(ErrCodeNotFound, `Entity not found`, 404);
|
|
3611
|
-
if (entity.status
|
|
3666
|
+
if (rejectsNormalWrites(entity.status)) throw new ElectricAgentsError(ErrCodeNotRunning, `Entity is not accepting writes`, 409);
|
|
3612
3667
|
const now = new Date().toISOString();
|
|
3613
3668
|
const value = {};
|
|
3614
3669
|
if (`payload` in req) value.payload = req.payload;
|
|
@@ -3629,7 +3684,7 @@ var EntityManager = class {
|
|
|
3629
3684
|
async deleteInboxMessage(entityUrl, key) {
|
|
3630
3685
|
const entity = await this.registry.getEntity(entityUrl);
|
|
3631
3686
|
if (!entity) throw new ElectricAgentsError(ErrCodeNotFound, `Entity not found`, 404);
|
|
3632
|
-
if (entity.status
|
|
3687
|
+
if (rejectsNormalWrites(entity.status)) throw new ElectricAgentsError(ErrCodeNotRunning, `Entity is not accepting writes`, 409);
|
|
3633
3688
|
const envelope = __electric_ax_agents_runtime.entityStateSchema.inbox.delete({ key });
|
|
3634
3689
|
await this.streamClient.append(entity.streams.main, this.encodeChangeEvent(envelope));
|
|
3635
3690
|
}
|
|
@@ -3637,7 +3692,7 @@ var EntityManager = class {
|
|
|
3637
3692
|
const entity = await this.registry.getEntity(entityUrl);
|
|
3638
3693
|
if (!entity) throw new ElectricAgentsError(ErrCodeNotFound, `Entity not found`, 404);
|
|
3639
3694
|
if (!this.isValidWriteToken(entity, token)) throw new ElectricAgentsError(ErrCodeUnauthorized, `Invalid write token`, 401);
|
|
3640
|
-
if (entity.status
|
|
3695
|
+
if (rejectsNormalWrites(entity.status)) throw new ElectricAgentsError(ErrCodeNotRunning, `Entity is not accepting writes`, 409);
|
|
3641
3696
|
if (typeof req.value !== `string`) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Tag values must be strings`, 400);
|
|
3642
3697
|
const result = await this.registry.setEntityTag(entityUrl, key, req.value);
|
|
3643
3698
|
const updated = result.entity;
|
|
@@ -3649,7 +3704,7 @@ var EntityManager = class {
|
|
|
3649
3704
|
const entity = await this.registry.getEntity(entityUrl);
|
|
3650
3705
|
if (!entity) throw new ElectricAgentsError(ErrCodeNotFound, `Entity not found`, 404);
|
|
3651
3706
|
if (!this.isValidWriteToken(entity, token)) throw new ElectricAgentsError(ErrCodeUnauthorized, `Invalid write token`, 401);
|
|
3652
|
-
if (entity.status
|
|
3707
|
+
if (rejectsNormalWrites(entity.status)) throw new ElectricAgentsError(ErrCodeNotRunning, `Entity is not accepting writes`, 409);
|
|
3653
3708
|
const result = await this.registry.removeEntityTag(entityUrl, key);
|
|
3654
3709
|
const updated = result.entity;
|
|
3655
3710
|
if (!updated) throw new ElectricAgentsError(ErrCodeEntityPersistFailed, `Entity not found after tag delete`, 500);
|
|
@@ -3902,26 +3957,131 @@ var EntityManager = class {
|
|
|
3902
3957
|
}
|
|
3903
3958
|
};
|
|
3904
3959
|
}
|
|
3905
|
-
async
|
|
3960
|
+
async signal(entityUrl, req) {
|
|
3906
3961
|
const entity = await this.registry.getEntity(entityUrl);
|
|
3907
3962
|
if (!entity) throw new ElectricAgentsError(ErrCodeNotFound, `Entity not found`, 404);
|
|
3908
|
-
|
|
3909
|
-
|
|
3910
|
-
const
|
|
3911
|
-
|
|
3912
|
-
const
|
|
3913
|
-
|
|
3914
|
-
|
|
3963
|
+
if (isTerminalEntityStatus(entity.status)) throw new ElectricAgentsError(ErrCodeInvalidSignal, `Cannot signal a ${entity.status} entity`, 409);
|
|
3964
|
+
const now = new Date();
|
|
3965
|
+
const previousState = entity.status;
|
|
3966
|
+
const handling = this.serverHandlingForSignal(previousState, req.signal);
|
|
3967
|
+
const txid = handling.status === previousState ? await this.registry.touchEntityWithTxid(entityUrl) : await this.registry.updateStatusWithTxid(entityUrl, handling.status);
|
|
3968
|
+
if (txid === null) throw new ElectricAgentsError(ErrCodeInvalidSignal, `Cannot signal entity because it is already terminal`, 409);
|
|
3969
|
+
const key = `sig-${now.getTime()}-${(0, node_crypto.randomUUID)().slice(0, 8)}`;
|
|
3970
|
+
const signalValue = {
|
|
3971
|
+
signal: req.signal,
|
|
3972
|
+
status: handling.handled ? `handled` : `unhandled`,
|
|
3973
|
+
sender: SERVER_SIGNAL_SENDER,
|
|
3974
|
+
timestamp: now.toISOString()
|
|
3975
|
+
};
|
|
3976
|
+
if (req.reason !== void 0) signalValue.reason = req.reason;
|
|
3977
|
+
if (req.payload !== void 0) signalValue.payload = req.payload;
|
|
3978
|
+
if (handling.handled) {
|
|
3979
|
+
signalValue.handled_at = now.toISOString();
|
|
3980
|
+
signalValue.handled_by = SERVER_SIGNAL_SENDER;
|
|
3981
|
+
signalValue.outcome = handling.outcome;
|
|
3982
|
+
signalValue.previous_state = previousState;
|
|
3983
|
+
signalValue.new_state = handling.status;
|
|
3984
|
+
}
|
|
3985
|
+
const signalEvent = {
|
|
3986
|
+
type: `signal`,
|
|
3987
|
+
key,
|
|
3988
|
+
value: signalValue,
|
|
3989
|
+
headers: {
|
|
3990
|
+
operation: `insert`,
|
|
3991
|
+
timestamp: now.toISOString(),
|
|
3992
|
+
txid: String(txid)
|
|
3993
|
+
}
|
|
3994
|
+
};
|
|
3995
|
+
const shouldCloseStreams = isTerminalEntityStatus(handling.status);
|
|
3996
|
+
await this.appendSignalEvent(entity, signalEvent, shouldCloseStreams);
|
|
3997
|
+
if (!shouldCloseStreams) await this.evaluateWakes(entityUrl, signalEvent);
|
|
3998
|
+
if (handling.unregisterWakes) {
|
|
3999
|
+
await this.wakeRegistry.unregisterBySubscriber(entityUrl, this.tenantId);
|
|
4000
|
+
await this.wakeRegistry.unregisterBySource(entityUrl, this.tenantId);
|
|
4001
|
+
}
|
|
4002
|
+
if (handling.status !== previousState && this.entityBridgeManager) await this.entityBridgeManager.onEntityChanged(entityUrl);
|
|
4003
|
+
return {
|
|
4004
|
+
url: entityUrl,
|
|
4005
|
+
signal: req.signal,
|
|
4006
|
+
previous_state: previousState,
|
|
4007
|
+
new_state: handling.status,
|
|
4008
|
+
created_at: now.getTime(),
|
|
4009
|
+
txid
|
|
4010
|
+
};
|
|
4011
|
+
}
|
|
4012
|
+
async kill(entityUrl) {
|
|
4013
|
+
const response = await this.signal(entityUrl, {
|
|
4014
|
+
signal: `SIGKILL`,
|
|
4015
|
+
reason: `Legacy kill command`
|
|
3915
4016
|
});
|
|
3916
|
-
|
|
3917
|
-
|
|
3918
|
-
|
|
4017
|
+
return { txid: response.txid };
|
|
4018
|
+
}
|
|
4019
|
+
serverHandlingForSignal(status$4, signal) {
|
|
4020
|
+
if (signal === `SIGKILL`) return {
|
|
4021
|
+
status: `killed`,
|
|
4022
|
+
handled: true,
|
|
4023
|
+
outcome: `transitioned`,
|
|
4024
|
+
unregisterWakes: true
|
|
4025
|
+
};
|
|
4026
|
+
if (signal === `SIGTERM`) {
|
|
4027
|
+
if (status$4 === `idle` || status$4 === `paused`) return {
|
|
4028
|
+
status: `stopped`,
|
|
4029
|
+
handled: true,
|
|
4030
|
+
outcome: `transitioned`,
|
|
4031
|
+
unregisterWakes: true
|
|
4032
|
+
};
|
|
4033
|
+
if (status$4 === `running`) return {
|
|
4034
|
+
status: `stopping`,
|
|
4035
|
+
handled: false,
|
|
4036
|
+
outcome: `transitioned`,
|
|
4037
|
+
unregisterWakes: false
|
|
4038
|
+
};
|
|
4039
|
+
}
|
|
4040
|
+
if (status$4 === `paused` && signal !== `SIGCONT`) return {
|
|
4041
|
+
status: status$4,
|
|
4042
|
+
handled: true,
|
|
4043
|
+
outcome: `ignored`,
|
|
4044
|
+
unregisterWakes: false
|
|
4045
|
+
};
|
|
4046
|
+
if (signal === `SIGSTOP` && (status$4 === `idle` || status$4 === `running`)) return {
|
|
4047
|
+
status: `paused`,
|
|
4048
|
+
handled: status$4 === `idle`,
|
|
4049
|
+
outcome: `transitioned`,
|
|
4050
|
+
unregisterWakes: false
|
|
4051
|
+
};
|
|
4052
|
+
if (signal === `SIGCONT` && status$4 === `paused`) return {
|
|
4053
|
+
status: `idle`,
|
|
4054
|
+
handled: false,
|
|
4055
|
+
outcome: `transitioned`,
|
|
4056
|
+
unregisterWakes: false
|
|
4057
|
+
};
|
|
4058
|
+
return {
|
|
4059
|
+
status: status$4,
|
|
4060
|
+
handled: false,
|
|
4061
|
+
outcome: `ignored`,
|
|
4062
|
+
unregisterWakes: false
|
|
4063
|
+
};
|
|
4064
|
+
}
|
|
4065
|
+
async appendSignalEvent(entity, signalEvent, closeStreams) {
|
|
4066
|
+
const signalData = this.encodeChangeEvent(signalEvent);
|
|
4067
|
+
if (!closeStreams) {
|
|
4068
|
+
await this.streamClient.append(entity.streams.main, signalData);
|
|
4069
|
+
return;
|
|
4070
|
+
}
|
|
4071
|
+
const errorCloseEvent = {
|
|
4072
|
+
type: `signal`,
|
|
4073
|
+
key: signalEvent.key,
|
|
4074
|
+
value: signalEvent.value,
|
|
4075
|
+
headers: signalEvent.headers
|
|
4076
|
+
};
|
|
4077
|
+
const errorSignalData = this.encodeChangeEvent(errorCloseEvent);
|
|
4078
|
+
for (const [streamPath, data] of [[entity.streams.main, signalData], [entity.streams.error, errorSignalData]]) try {
|
|
4079
|
+
await this.streamClient.append(streamPath, data, { close: true });
|
|
3919
4080
|
} catch (err) {
|
|
3920
4081
|
const message = err instanceof Error ? err.message : String(err);
|
|
3921
4082
|
if (/closed/i.test(message) || /not found/i.test(message) || /404/.test(message) || /409/.test(message)) continue;
|
|
3922
4083
|
throw err;
|
|
3923
4084
|
}
|
|
3924
|
-
return { txid };
|
|
3925
4085
|
}
|
|
3926
4086
|
async validateWriteEvent(entity, event) {
|
|
3927
4087
|
if (!entity.type) return null;
|
|
@@ -4037,7 +4197,7 @@ var EntityManager = class {
|
|
|
4037
4197
|
async validateSendRequest(entityUrl, req) {
|
|
4038
4198
|
const entity = await this.registry.getEntity(entityUrl);
|
|
4039
4199
|
if (!entity) throw new ElectricAgentsError(ErrCodeNotFound, `Entity not found`, 404);
|
|
4040
|
-
if (entity.status
|
|
4200
|
+
if (rejectsNormalWrites(entity.status)) throw new ElectricAgentsError(ErrCodeNotRunning, `Entity is not accepting writes`, 409);
|
|
4041
4201
|
if (req.type && entity.type) {
|
|
4042
4202
|
const { inboxSchemas } = await this.getEffectiveSchemas(entity);
|
|
4043
4203
|
if (inboxSchemas) {
|
|
@@ -5002,7 +5162,8 @@ var ElectricAgentsTenantRuntime = class {
|
|
|
5002
5162
|
const primaryStream = `${entityUrl}/main`;
|
|
5003
5163
|
const callbacks = await this.db.select().from(consumerCallbacks).where((0, drizzle_orm.and)((0, drizzle_orm.eq)(consumerCallbacks.tenantId, this.serviceId), (0, drizzle_orm.eq)(consumerCallbacks.primaryStream, primaryStream))).limit(1);
|
|
5004
5164
|
if (callbacks.length > 0) return;
|
|
5005
|
-
await this.manager.registry.
|
|
5165
|
+
const entity = await this.manager.registry.getEntity(entityUrl);
|
|
5166
|
+
await this.manager.registry.updateStatus(entityUrl, entity?.status === `stopping` ? `stopped` : `idle`);
|
|
5006
5167
|
await this.entityBridgeManager.onEntityChanged(entityUrl);
|
|
5007
5168
|
}
|
|
5008
5169
|
};
|
|
@@ -6144,6 +6305,87 @@ function sqlStringLiteral(value) {
|
|
|
6144
6305
|
return `'${value.replace(/'/g, `''`)}'`;
|
|
6145
6306
|
}
|
|
6146
6307
|
|
|
6308
|
+
//#endregion
|
|
6309
|
+
//#region src/webhook-signing.ts
|
|
6310
|
+
const encoder = new TextEncoder();
|
|
6311
|
+
const defaultWebhookSigner = createEd25519WebhookSigner();
|
|
6312
|
+
function createEd25519WebhookSigner(options = {}) {
|
|
6313
|
+
const privateKey = options.privateKey ? importPrivateKey(options.privateKey) : (0, node_crypto.generateKeyPairSync)(`ed25519`).privateKey;
|
|
6314
|
+
if (privateKey.asymmetricKeyType !== `ed25519`) throw new Error(`Webhook signing key must be an Ed25519 private key`);
|
|
6315
|
+
const publicJwk = buildPublicJwk(privateKey, options.kid);
|
|
6316
|
+
return {
|
|
6317
|
+
sign: (body) => signWebhookBody(privateKey, publicJwk.kid, body),
|
|
6318
|
+
jwks: () => ({ keys: [{ ...publicJwk }] })
|
|
6319
|
+
};
|
|
6320
|
+
}
|
|
6321
|
+
function getDefaultWebhookSigner() {
|
|
6322
|
+
return defaultWebhookSigner;
|
|
6323
|
+
}
|
|
6324
|
+
async function webhookSigningMetadata(signer, streamRootUrl) {
|
|
6325
|
+
const jwks = await signer.jwks();
|
|
6326
|
+
const key = jwks.keys[0];
|
|
6327
|
+
if (!key) throw new Error(`Webhook signer did not provide any public keys`);
|
|
6328
|
+
return {
|
|
6329
|
+
alg: `ed25519`,
|
|
6330
|
+
kid: key.kid,
|
|
6331
|
+
jwks_url: (0, __electric_ax_agents_runtime.appendPathToUrl)(streamRootUrl, `/__ds/jwks.json`)
|
|
6332
|
+
};
|
|
6333
|
+
}
|
|
6334
|
+
function signWebhookBody(privateKey, kid, body) {
|
|
6335
|
+
const timestamp$1 = Math.floor(Date.now() / 1e3);
|
|
6336
|
+
const payload = bytesWithTimestamp(timestamp$1, body);
|
|
6337
|
+
const signature = (0, node_crypto.sign)(null, payload, privateKey).toString(`base64url`);
|
|
6338
|
+
return `t=${timestamp$1},kid=${kid},ed25519=${signature}`;
|
|
6339
|
+
}
|
|
6340
|
+
function bytesWithTimestamp(timestamp$1, body) {
|
|
6341
|
+
const prefix = encoder.encode(`${timestamp$1}.`);
|
|
6342
|
+
const bodyBytes = typeof body === `string` ? encoder.encode(body) : body;
|
|
6343
|
+
return Buffer.concat([Buffer.from(prefix), Buffer.from(bodyBytes)]);
|
|
6344
|
+
}
|
|
6345
|
+
function importPrivateKey(input) {
|
|
6346
|
+
if (isKeyObject(input)) return input;
|
|
6347
|
+
if (typeof input === `string`) {
|
|
6348
|
+
const trimmed = input.trim();
|
|
6349
|
+
if (trimmed.startsWith(`{`)) return (0, node_crypto.createPrivateKey)({
|
|
6350
|
+
key: JSON.parse(trimmed),
|
|
6351
|
+
format: `jwk`
|
|
6352
|
+
});
|
|
6353
|
+
return (0, node_crypto.createPrivateKey)(trimmed.replace(/\\n/g, `\n`));
|
|
6354
|
+
}
|
|
6355
|
+
if (Buffer.isBuffer(input)) return (0, node_crypto.createPrivateKey)(input);
|
|
6356
|
+
return (0, node_crypto.createPrivateKey)({
|
|
6357
|
+
key: input,
|
|
6358
|
+
format: `jwk`
|
|
6359
|
+
});
|
|
6360
|
+
}
|
|
6361
|
+
function isKeyObject(input) {
|
|
6362
|
+
return typeof input === `object` && `type` in input && input.type === `private`;
|
|
6363
|
+
}
|
|
6364
|
+
function buildPublicJwk(privateKey, kid) {
|
|
6365
|
+
const exported = (0, node_crypto.createPublicKey)(privateKey).export({ format: `jwk` });
|
|
6366
|
+
if (exported.kty !== `OKP` || exported.crv !== `Ed25519` || !exported.x) throw new Error(`Failed to export Ed25519 webhook signing key`);
|
|
6367
|
+
return {
|
|
6368
|
+
kty: `OKP`,
|
|
6369
|
+
crv: `Ed25519`,
|
|
6370
|
+
x: exported.x,
|
|
6371
|
+
kid: kid ?? deriveKeyId({
|
|
6372
|
+
kty: exported.kty,
|
|
6373
|
+
crv: exported.crv,
|
|
6374
|
+
x: exported.x
|
|
6375
|
+
}),
|
|
6376
|
+
use: `sig`,
|
|
6377
|
+
alg: `EdDSA`
|
|
6378
|
+
};
|
|
6379
|
+
}
|
|
6380
|
+
function deriveKeyId(jwk) {
|
|
6381
|
+
const thumbprintInput = JSON.stringify({
|
|
6382
|
+
crv: jwk.crv,
|
|
6383
|
+
kty: jwk.kty,
|
|
6384
|
+
x: jwk.x
|
|
6385
|
+
});
|
|
6386
|
+
return `ds_${(0, node_crypto.createHash)(`sha256`).update(thumbprintInput).digest(`base64url`)}`;
|
|
6387
|
+
}
|
|
6388
|
+
|
|
6147
6389
|
//#endregion
|
|
6148
6390
|
//#region src/routing/durable-streams-router.ts
|
|
6149
6391
|
const subscriptionProxyBodySchema = __sinclair_typebox.Type.Object({ webhook: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Object({ url: __sinclair_typebox.Type.String() }, { additionalProperties: true })) }, { additionalProperties: true });
|
|
@@ -6160,6 +6402,7 @@ durableStreamsRouter.delete(`/__ds/subscriptions/:subscriptionId`, deleteSubscri
|
|
|
6160
6402
|
durableStreamsRouter.post(`/__ds/subscriptions/:subscriptionId/streams`, postSubscriptionStreams);
|
|
6161
6403
|
durableStreamsRouter.delete(`/__ds/subscriptions/:subscriptionId/streams/:streamPath+`, deleteSubscriptionStream);
|
|
6162
6404
|
for (const action of subscriptionControlActions) durableStreamsRouter.post(`/__ds/subscriptions/:subscriptionId/${action}`, subscriptionAction(action));
|
|
6405
|
+
durableStreamsRouter.get(`/__ds/jwks.json`, webhookJwks);
|
|
6163
6406
|
durableStreamsRouter.all(`/__ds`, controlPassThrough);
|
|
6164
6407
|
durableStreamsRouter.all(`/__ds/*`, controlPassThrough);
|
|
6165
6408
|
durableStreamsRouter.post(`*`, streamAppend);
|
|
@@ -6168,12 +6411,16 @@ function bodyFromBytes$1(body) {
|
|
|
6168
6411
|
return body.buffer.slice(body.byteOffset, body.byteOffset + body.byteLength);
|
|
6169
6412
|
}
|
|
6170
6413
|
function responseFromUpstream$1(response, body) {
|
|
6171
|
-
|
|
6414
|
+
const responseBody = forbidsResponseBody$1(response.status) ? null : body !== void 0 ? bodyFromBytes$1(body) : response.body;
|
|
6415
|
+
return new Response(responseBody, {
|
|
6172
6416
|
status: response.status,
|
|
6173
6417
|
statusText: response.statusText,
|
|
6174
6418
|
headers: responseHeaders(response)
|
|
6175
6419
|
});
|
|
6176
6420
|
}
|
|
6421
|
+
function forbidsResponseBody$1(status$4) {
|
|
6422
|
+
return status$4 === 204 || status$4 === 205 || status$4 === 304;
|
|
6423
|
+
}
|
|
6177
6424
|
async function forwardToDurableStreams(ctx, request, body, route = `stream`, urlOverride, durableStreamsBearerMode = `overwrite`) {
|
|
6178
6425
|
const headers = new Headers(request.headers);
|
|
6179
6426
|
headers.delete(`host`);
|
|
@@ -6207,28 +6454,32 @@ function rewriteSubscriptionBodyForBackend(payload, service, routingAdapter) {
|
|
|
6207
6454
|
return next;
|
|
6208
6455
|
});
|
|
6209
6456
|
}
|
|
6210
|
-
function rewriteSubscriptionResponseForClient(bytes, response,
|
|
6457
|
+
async function rewriteSubscriptionResponseForClient(bytes, response, ctx, routingAdapter) {
|
|
6211
6458
|
if (!response.headers.get(`content-type`)?.includes(`application/json`)) return bytes;
|
|
6212
6459
|
const payload = decodeJson(bytes);
|
|
6213
6460
|
if (!payload) return bytes;
|
|
6214
|
-
if (typeof payload.pattern === `string`) payload.pattern = routingAdapter.toRuntimeStreamPath(service, payload.pattern);
|
|
6461
|
+
if (typeof payload.pattern === `string`) payload.pattern = routingAdapter.toRuntimeStreamPath(ctx.service, payload.pattern);
|
|
6215
6462
|
if (Array.isArray(payload.streams)) payload.streams = payload.streams.map((stream) => {
|
|
6216
|
-
if (typeof stream === `string`) return routingAdapter.toRuntimeStreamPath(service, stream);
|
|
6463
|
+
if (typeof stream === `string`) return routingAdapter.toRuntimeStreamPath(ctx.service, stream);
|
|
6217
6464
|
if (stream && typeof stream === `object` && typeof stream.path === `string`) return {
|
|
6218
6465
|
...stream,
|
|
6219
|
-
path: routingAdapter.toRuntimeStreamPath(service, stream.path)
|
|
6466
|
+
path: routingAdapter.toRuntimeStreamPath(ctx.service, stream.path)
|
|
6220
6467
|
};
|
|
6221
6468
|
return stream;
|
|
6222
6469
|
});
|
|
6223
|
-
if (typeof payload.wake_stream === `string`) payload.wake_stream = routingAdapter.toRuntimeStreamPath(service, payload.wake_stream);
|
|
6224
|
-
if (typeof payload.stream === `string`) payload.stream = routingAdapter.toRuntimeStreamPath(service, payload.stream);
|
|
6470
|
+
if (typeof payload.wake_stream === `string`) payload.wake_stream = routingAdapter.toRuntimeStreamPath(ctx.service, payload.wake_stream);
|
|
6471
|
+
if (typeof payload.stream === `string`) payload.stream = routingAdapter.toRuntimeStreamPath(ctx.service, payload.stream);
|
|
6225
6472
|
if (Array.isArray(payload.acks)) payload.acks = payload.acks.map((ack) => {
|
|
6226
6473
|
if (!ack || typeof ack !== `object`) return ack;
|
|
6227
6474
|
const next = { ...ack };
|
|
6228
|
-
if (typeof next.stream === `string`) next.stream = routingAdapter.toRuntimeStreamPath(service, next.stream);
|
|
6229
|
-
if (typeof next.path === `string`) next.path = routingAdapter.toRuntimeStreamPath(service, next.path);
|
|
6475
|
+
if (typeof next.stream === `string`) next.stream = routingAdapter.toRuntimeStreamPath(ctx.service, next.stream);
|
|
6476
|
+
if (typeof next.path === `string`) next.path = routingAdapter.toRuntimeStreamPath(ctx.service, next.path);
|
|
6230
6477
|
return next;
|
|
6231
6478
|
});
|
|
6479
|
+
if (payload.webhook && typeof payload.webhook === `object` && !Array.isArray(payload.webhook)) {
|
|
6480
|
+
const webhook = payload.webhook;
|
|
6481
|
+
webhook.signing = await webhookSigningMetadata(resolveWebhookSigner$1(ctx), ctx.publicUrl);
|
|
6482
|
+
}
|
|
6232
6483
|
return new TextEncoder().encode(JSON.stringify(payload));
|
|
6233
6484
|
}
|
|
6234
6485
|
function decodeJson(bytes) {
|
|
@@ -6247,6 +6498,9 @@ function routeParam$2(request, name) {
|
|
|
6247
6498
|
function subscriptionRoutingAdapter(ctx) {
|
|
6248
6499
|
return resolveDurableStreamsRoutingAdapter(ctx.durableStreamsRouting, ctx.durableStreamsUrl);
|
|
6249
6500
|
}
|
|
6501
|
+
function resolveWebhookSigner$1(ctx) {
|
|
6502
|
+
return ctx.webhookSigner ?? getDefaultWebhookSigner();
|
|
6503
|
+
}
|
|
6250
6504
|
async function rewriteSubscriptionRequestBody(request, ctx, subscriptionId, routingAdapter) {
|
|
6251
6505
|
const body = await readRequestBody(request);
|
|
6252
6506
|
if (body.length === 0) return {
|
|
@@ -6275,7 +6529,7 @@ async function rewriteSubscriptionRequestBody(request, ctx, subscriptionId, rout
|
|
|
6275
6529
|
async function forwardSubscriptionRequest(request, ctx, routingAdapter, opts = {}) {
|
|
6276
6530
|
const upstream = await forwardToDurableStreams(ctx, request, opts.body, `control`, opts.requestUrl, opts.bearerMode ?? `overwrite`);
|
|
6277
6531
|
let responseBytes = upstream.body ? new Uint8Array(await upstream.arrayBuffer()) : new Uint8Array();
|
|
6278
|
-
responseBytes = rewriteSubscriptionResponseForClient(responseBytes, upstream, ctx
|
|
6532
|
+
responseBytes = await rewriteSubscriptionResponseForClient(responseBytes, upstream, ctx, routingAdapter);
|
|
6279
6533
|
return {
|
|
6280
6534
|
upstream,
|
|
6281
6535
|
response: responseFromUpstream$1(upstream, responseBytes)
|
|
@@ -6348,6 +6602,15 @@ async function controlPassThrough(request, ctx) {
|
|
|
6348
6602
|
const upstream = await forwardToDurableStreams(ctx, request, void 0, `control`);
|
|
6349
6603
|
return responseFromUpstream$1(upstream);
|
|
6350
6604
|
}
|
|
6605
|
+
async function webhookJwks(_request, ctx) {
|
|
6606
|
+
return new Response(JSON.stringify(await resolveWebhookSigner$1(ctx).jwks()), {
|
|
6607
|
+
status: 200,
|
|
6608
|
+
headers: {
|
|
6609
|
+
"content-type": `application/jwk-set+json`,
|
|
6610
|
+
"cache-control": `public, max-age=300`
|
|
6611
|
+
}
|
|
6612
|
+
});
|
|
6613
|
+
}
|
|
6351
6614
|
async function streamAppend(request, ctx) {
|
|
6352
6615
|
return await electricAgentsStreamAppendRouter.fetch(createStreamAppendRouteRequest(request), ctx.runtime, (req, body) => forwardFetchRequest({
|
|
6353
6616
|
request: {
|
|
@@ -6487,6 +6750,20 @@ const forkBodySchema = __sinclair_typebox.Type.Object({
|
|
|
6487
6750
|
waitTimeoutMs: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Number())
|
|
6488
6751
|
});
|
|
6489
6752
|
const setTagBodySchema = __sinclair_typebox.Type.Object({ value: __sinclair_typebox.Type.String() });
|
|
6753
|
+
const entitySignalSchema = __sinclair_typebox.Type.Union([
|
|
6754
|
+
__sinclair_typebox.Type.Literal(`SIGINT`),
|
|
6755
|
+
__sinclair_typebox.Type.Literal(`SIGHUP`),
|
|
6756
|
+
__sinclair_typebox.Type.Literal(`SIGTERM`),
|
|
6757
|
+
__sinclair_typebox.Type.Literal(`SIGKILL`),
|
|
6758
|
+
__sinclair_typebox.Type.Literal(`SIGSTOP`),
|
|
6759
|
+
__sinclair_typebox.Type.Literal(`SIGCONT`),
|
|
6760
|
+
__sinclair_typebox.Type.Literal(`SIGUSR`)
|
|
6761
|
+
]);
|
|
6762
|
+
const signalBodySchema = __sinclair_typebox.Type.Object({
|
|
6763
|
+
signal: entitySignalSchema,
|
|
6764
|
+
reason: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
|
|
6765
|
+
payload: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Unknown())
|
|
6766
|
+
});
|
|
6490
6767
|
const scheduleBodySchema = __sinclair_typebox.Type.Union([__sinclair_typebox.Type.Object({
|
|
6491
6768
|
scheduleType: __sinclair_typebox.Type.Literal(`cron`),
|
|
6492
6769
|
expression: __sinclair_typebox.Type.String(),
|
|
@@ -6510,6 +6787,7 @@ entitiesRouter.put(`/:type/:instanceId`, withSpawnableEntityType, withSchema(spa
|
|
|
6510
6787
|
entitiesRouter.get(`/:type/:instanceId`, withExistingEntity, getEntity);
|
|
6511
6788
|
entitiesRouter.head(`/:type/:instanceId`, withExistingEntity, headEntity);
|
|
6512
6789
|
entitiesRouter.delete(`/:type/:instanceId`, withExistingEntity, killEntity);
|
|
6790
|
+
entitiesRouter.post(`/:type/:instanceId/signal`, withExistingEntity, withSchema(signalBodySchema), signalEntity);
|
|
6513
6791
|
entitiesRouter.post(`/:type/:instanceId/send`, withExistingEntity, withSchema(sendBodySchema), sendEntity);
|
|
6514
6792
|
entitiesRouter.patch(`/:type/:instanceId/inbox/:messageKey`, withExistingEntity, withSchema(inboxMessageBodySchema), updateInboxMessage);
|
|
6515
6793
|
entitiesRouter.delete(`/:type/:instanceId/inbox/:messageKey`, withExistingEntity, deleteInboxMessage);
|
|
@@ -6713,11 +6991,13 @@ async function spawnEntity(request, ctx) {
|
|
|
6713
6991
|
wake: parsed.wake,
|
|
6714
6992
|
created_by: principal.url
|
|
6715
6993
|
});
|
|
6716
|
-
|
|
6994
|
+
const linkBeforeInitialMessage = parsed.initialMessage !== void 0 && shouldLinkDispatchBeforeInitialMessage(dispatchPolicy);
|
|
6995
|
+
if (linkBeforeInitialMessage) await linkEntityDispatchSubscription(ctx, entity);
|
|
6717
6996
|
if (parsed.initialMessage !== void 0) await ctx.entityManager.send(entity.url, {
|
|
6718
6997
|
from: principal.url,
|
|
6719
6998
|
payload: parsed.initialMessage
|
|
6720
6999
|
});
|
|
7000
|
+
if (!linkBeforeInitialMessage) await linkEntityDispatchSubscription(ctx, entity);
|
|
6721
7001
|
return (0, itty_router.json)({
|
|
6722
7002
|
...toPublicEntity(entity),
|
|
6723
7003
|
txid: entity.txid
|
|
@@ -6741,6 +7021,22 @@ async function killEntity(request, ctx) {
|
|
|
6741
7021
|
ctx.runtime.claimWriteTokens.clearStream(ctx.service, entity.streams.main);
|
|
6742
7022
|
return (0, itty_router.json)(result);
|
|
6743
7023
|
}
|
|
7024
|
+
async function signalEntity(request, ctx) {
|
|
7025
|
+
const principalMutationError = rejectPrincipalEntityMutation(request, `signaled`);
|
|
7026
|
+
if (principalMutationError) return principalMutationError;
|
|
7027
|
+
const parsed = routeBody(request);
|
|
7028
|
+
const { entityUrl, entity } = requireExistingEntityRoute(request);
|
|
7029
|
+
const result = await ctx.entityManager.signal(entityUrl, {
|
|
7030
|
+
signal: parsed.signal,
|
|
7031
|
+
reason: parsed.reason,
|
|
7032
|
+
payload: parsed.payload
|
|
7033
|
+
});
|
|
7034
|
+
if (result.new_state === `stopped` || result.new_state === `killed`) {
|
|
7035
|
+
await unlinkEntityDispatchSubscription(ctx, entity);
|
|
7036
|
+
ctx.runtime.claimWriteTokens.clearStream(ctx.service, entity.streams.main);
|
|
7037
|
+
}
|
|
7038
|
+
return (0, itty_router.json)(result);
|
|
7039
|
+
}
|
|
6744
7040
|
|
|
6745
7041
|
//#endregion
|
|
6746
7042
|
//#region src/routing/entity-types-router.ts
|
|
@@ -7221,7 +7517,7 @@ async function notificationFromClaim(ctx, input) {
|
|
|
7221
7517
|
const primaryStream = withLeadingSlash(primary.path);
|
|
7222
7518
|
const entity = await ctx.entityManager.registry.getEntityByStream(primaryStream);
|
|
7223
7519
|
if (!entity) throw new ElectricAgentsError(ErrCodeNotFound, `Claim stream is not attached to an entity`, 404);
|
|
7224
|
-
if (entity.status === `stopped`) {
|
|
7520
|
+
if (entity.status === `stopped` || entity.status === `paused`) {
|
|
7225
7521
|
await ctx.streamClient.releaseSubscription(input.subscriptionId, input.claim.token, {
|
|
7226
7522
|
wake_id: input.claim.wake_id,
|
|
7227
7523
|
generation: input.claim.generation
|
|
@@ -7338,12 +7634,16 @@ function bodyFromBytes(body) {
|
|
|
7338
7634
|
return body.buffer.slice(body.byteOffset, body.byteOffset + body.byteLength);
|
|
7339
7635
|
}
|
|
7340
7636
|
function responseFromUpstream(response, body) {
|
|
7341
|
-
|
|
7637
|
+
const responseBody = forbidsResponseBody(response.status) ? null : body !== void 0 ? bodyFromBytes(body) : response.body;
|
|
7638
|
+
return new Response(responseBody, {
|
|
7342
7639
|
status: response.status,
|
|
7343
7640
|
statusText: response.statusText,
|
|
7344
7641
|
headers: responseHeaders(response)
|
|
7345
7642
|
});
|
|
7346
7643
|
}
|
|
7644
|
+
function forbidsResponseBody(status$4) {
|
|
7645
|
+
return status$4 === 204 || status$4 === 205 || status$4 === 304;
|
|
7646
|
+
}
|
|
7347
7647
|
function forwardHeadersFromRequest(request) {
|
|
7348
7648
|
const headers = new Headers(request.headers);
|
|
7349
7649
|
headers.delete(`host`);
|
|
@@ -7352,6 +7652,45 @@ function forwardHeadersFromRequest(request) {
|
|
|
7352
7652
|
function durableStreamsSubscriptionCallback(value) {
|
|
7353
7653
|
return value.startsWith(DS_SUBSCRIPTION_CALLBACK_PREFIX) ? value.slice(DS_SUBSCRIPTION_CALLBACK_PREFIX.length) : null;
|
|
7354
7654
|
}
|
|
7655
|
+
function resolveWebhookSigner(ctx) {
|
|
7656
|
+
return ctx.webhookSigner ?? getDefaultWebhookSigner();
|
|
7657
|
+
}
|
|
7658
|
+
function durableStreamsWebhookJwksUrl(ctx) {
|
|
7659
|
+
if (!ctx.durableStreamsRouting) return (0, __electric_ax_agents_runtime.appendPathToUrl)(ctx.durableStreamsUrl, `/__ds/jwks.json`);
|
|
7660
|
+
return resolveDurableStreamsRoutingAdapter(ctx.durableStreamsRouting, ctx.durableStreamsUrl).controlUrl({
|
|
7661
|
+
durableStreamsUrl: ctx.durableStreamsUrl,
|
|
7662
|
+
serviceId: ctx.service,
|
|
7663
|
+
requestUrl: (0, __electric_ax_agents_runtime.appendPathToUrl)(ctx.publicUrl, `/__ds/jwks.json`)
|
|
7664
|
+
}).toString();
|
|
7665
|
+
}
|
|
7666
|
+
function durableStreamsJwksFetchClient(ctx) {
|
|
7667
|
+
return async (input, init) => {
|
|
7668
|
+
const headers = new Headers(init?.headers);
|
|
7669
|
+
await applyDurableStreamsBearer(headers, ctx.durableStreamsBearer, { overwrite: false });
|
|
7670
|
+
const nextInit = {
|
|
7671
|
+
...init ?? {},
|
|
7672
|
+
headers
|
|
7673
|
+
};
|
|
7674
|
+
if (ctx.durableStreamsDispatcher) nextInit.dispatcher = ctx.durableStreamsDispatcher;
|
|
7675
|
+
return await fetch(input, nextInit);
|
|
7676
|
+
};
|
|
7677
|
+
}
|
|
7678
|
+
function resolveDurableStreamsWebhookSignature(ctx) {
|
|
7679
|
+
if (ctx.durableStreamsWebhookSignature === false) return false;
|
|
7680
|
+
return {
|
|
7681
|
+
jwksUrl: ctx.durableStreamsWebhookSignature?.jwksUrl ?? durableStreamsWebhookJwksUrl(ctx),
|
|
7682
|
+
toleranceSeconds: ctx.durableStreamsWebhookSignature?.toleranceSeconds,
|
|
7683
|
+
cacheTtlMs: ctx.durableStreamsWebhookSignature?.cacheTtlMs,
|
|
7684
|
+
fetchClient: ctx.durableStreamsWebhookSignature?.fetchClient ?? durableStreamsJwksFetchClient(ctx)
|
|
7685
|
+
};
|
|
7686
|
+
}
|
|
7687
|
+
async function verifyDurableStreamsWebhook(request, ctx, body) {
|
|
7688
|
+
const config = resolveDurableStreamsWebhookSignature(ctx);
|
|
7689
|
+
if (config === false) return null;
|
|
7690
|
+
const verification = await (0, __electric_ax_agents_runtime.verifyWebhookSignature)(body, request.headers.get(`webhook-signature`), config);
|
|
7691
|
+
if (verification.ok) return null;
|
|
7692
|
+
return apiError(verification.status, verification.status === 401 ? ErrCodeUnauthorized : `WEBHOOK_SIGNATURE_UNAVAILABLE`, verification.error);
|
|
7693
|
+
}
|
|
7355
7694
|
function claimTokenFromRequest(request) {
|
|
7356
7695
|
const electricClaimToken = request.headers.get(`electric-claim-token`)?.trim();
|
|
7357
7696
|
if (electricClaimToken) return electricClaimToken;
|
|
@@ -7385,7 +7724,10 @@ async function webhookForward(request, ctx) {
|
|
|
7385
7724
|
const rootSpan = getRequestSpan(request);
|
|
7386
7725
|
rootSpan?.updateName(`webhook-forward`);
|
|
7387
7726
|
rootSpan?.setAttribute(`electric_agents.webhook.subscription_id`, subscriptionId);
|
|
7388
|
-
const
|
|
7727
|
+
const body = await readRequestBody(request);
|
|
7728
|
+
const signatureError = await verifyDurableStreamsWebhook(request, ctx, body);
|
|
7729
|
+
if (signatureError) return signatureError;
|
|
7730
|
+
const targetWebhookUrl = await tracer.startActiveSpan(`db.lookupSubscription`, async (span) => {
|
|
7389
7731
|
try {
|
|
7390
7732
|
const rows = await ctx.pgDb.select().from(subscriptionWebhooks).where((0, drizzle_orm.and)((0, drizzle_orm.eq)(subscriptionWebhooks.tenantId, ctx.service), (0, drizzle_orm.eq)(subscriptionWebhooks.subscriptionId, subscriptionId))).limit(1);
|
|
7391
7733
|
return rows[0]?.webhookUrl ?? null;
|
|
@@ -7393,7 +7735,6 @@ async function webhookForward(request, ctx) {
|
|
|
7393
7735
|
span.end();
|
|
7394
7736
|
}
|
|
7395
7737
|
});
|
|
7396
|
-
const [targetWebhookUrl, body] = await Promise.all([lookupPromise, readRequestBody(request)]);
|
|
7397
7738
|
if (!targetWebhookUrl) return apiError(404, ErrCodeSubscriptionNotFound, `Unknown webhook subscription`);
|
|
7398
7739
|
const parsedBodyResult = validateOptionalJsonBody(webhookForwardBodySchema, body, request.headers.get(`content-type`));
|
|
7399
7740
|
if (!parsedBodyResult.ok) return parsedBodyResult.response;
|
|
@@ -7444,7 +7785,7 @@ async function webhookForward(request, ctx) {
|
|
|
7444
7785
|
serverLog.warn(`[webhook-forward] consumerCallbacks upsert failed (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
|
|
7445
7786
|
}) : void 0;
|
|
7446
7787
|
const [entity, enriched] = await Promise.all([entityPromise, enrichPromise]);
|
|
7447
|
-
if (entity?.status === `stopped`) {
|
|
7788
|
+
if (entity?.status === `stopped` || entity?.status === `paused`) {
|
|
7448
7789
|
if (upsertPromise) await upsertPromise;
|
|
7449
7790
|
return (0, itty_router.json)({ done: true });
|
|
7450
7791
|
}
|
|
@@ -7482,6 +7823,7 @@ async function webhookForward(request, ctx) {
|
|
|
7482
7823
|
const headers = forwardHeadersFromRequest(request);
|
|
7483
7824
|
headers.set(`content-type`, `application/json`);
|
|
7484
7825
|
headers.delete(`content-length`);
|
|
7826
|
+
headers.set(`webhook-signature`, await resolveWebhookSigner(ctx).sign(forwardBody));
|
|
7485
7827
|
let upstream;
|
|
7486
7828
|
try {
|
|
7487
7829
|
upstream = await tracer.startActiveSpan(`fetch.agent-handler`, async (span) => {
|
|
@@ -7569,8 +7911,9 @@ async function callbackForward(request, ctx) {
|
|
|
7569
7911
|
serverLog.info(`[callback-forward] done received for stream=${target.primaryStream} consumer=${consumerId}`);
|
|
7570
7912
|
const stillOwnsClaim = ctx.runtime.claimWriteTokens.owns(ctx.service, target.primaryStream, consumerId);
|
|
7571
7913
|
const entity = await ctx.entityManager.registry.getEntityByStream(target.primaryStream);
|
|
7572
|
-
|
|
7573
|
-
|
|
7914
|
+
let entityCleared = false;
|
|
7915
|
+
if (epoch !== void 0) {
|
|
7916
|
+
const result = await ctx.entityManager.registry.materializeReleasedClaim?.({
|
|
7574
7917
|
consumerId,
|
|
7575
7918
|
epoch,
|
|
7576
7919
|
ackedStreams: Array.isArray(requestBody?.acks) ? requestBody.acks.flatMap((ack) => {
|
|
@@ -7582,13 +7925,15 @@ async function callbackForward(request, ctx) {
|
|
|
7582
7925
|
}] : [];
|
|
7583
7926
|
}) : void 0
|
|
7584
7927
|
});
|
|
7585
|
-
|
|
7586
|
-
|
|
7928
|
+
entityCleared = result?.entityCleared ?? false;
|
|
7929
|
+
}
|
|
7930
|
+
if (entity && (entityCleared || stillOwnsClaim)) {
|
|
7931
|
+
await ctx.entityManager.registry.updateStatus(entity.url, entity.status === `stopping` ? `stopped` : `idle`);
|
|
7587
7932
|
await ctx.entityBridgeManager.onEntityChanged(entity.url);
|
|
7588
|
-
serverLog.info(`[callback-forward] status updated
|
|
7589
|
-
} else if (
|
|
7590
|
-
|
|
7591
|
-
else serverLog.
|
|
7933
|
+
serverLog.info(`[callback-forward] status updated after done for ${entity.url}`);
|
|
7934
|
+
} else if (!entity) serverLog.warn(`[callback-forward] done received but no entity found for stream=${target.primaryStream}`);
|
|
7935
|
+
if (stillOwnsClaim) ctx.runtime.claimWriteTokens.clearStream(ctx.service, target.primaryStream);
|
|
7936
|
+
else if (entity) serverLog.info(`[callback-forward] done arrived after in-memory token evicted (stream=${target.primaryStream} consumer=${consumerId})`);
|
|
7592
7937
|
} else if (requestBody?.done === true) serverLog.warn(`[callback-forward] done received but skipped: upstream.ok=${upstream.ok} primaryStream=${target.primaryStream ?? `null`} consumer=${consumerId}`);
|
|
7593
7938
|
} catch (err) {
|
|
7594
7939
|
serverLog.error(`[callback-forward] error processing done for consumer=${consumerId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -7645,10 +7990,19 @@ exports.AgentsHost = AgentsHost
|
|
|
7645
7990
|
exports.DEFAULT_TENANT_ID = DEFAULT_TENANT_ID
|
|
7646
7991
|
exports.StreamClient = StreamClient
|
|
7647
7992
|
exports.UnregisteredTenantError = UnregisteredTenantError
|
|
7993
|
+
exports.assertEntitySignal = assertEntitySignal
|
|
7994
|
+
exports.assertEntityStatus = assertEntityStatus
|
|
7648
7995
|
exports.createDb = createDb
|
|
7996
|
+
exports.createEd25519WebhookSigner = createEd25519WebhookSigner
|
|
7997
|
+
exports.expectedSignalStatus = expectedSignalStatus
|
|
7998
|
+
exports.getDefaultWebhookSigner = getDefaultWebhookSigner
|
|
7649
7999
|
exports.globalRouter = globalRouter
|
|
8000
|
+
exports.isTerminalEntityStatus = isTerminalEntityStatus
|
|
7650
8001
|
exports.isUnregisteredTenantError = isUnregisteredTenantError
|
|
7651
8002
|
exports.pathPrefixedSingleTenantDurableStreamsRoutingAdapter = pathPrefixedSingleTenantDurableStreamsRoutingAdapter
|
|
8003
|
+
exports.rejectsNormalWrites = rejectsNormalWrites
|
|
7652
8004
|
exports.runMigrations = runMigrations
|
|
7653
8005
|
exports.streamRootDurableStreamsRoutingAdapter = streamRootDurableStreamsRoutingAdapter
|
|
7654
|
-
exports.tenantRootDurableStreamsRoutingAdapter = tenantRootDurableStreamsRoutingAdapter
|
|
8006
|
+
exports.tenantRootDurableStreamsRoutingAdapter = tenantRootDurableStreamsRoutingAdapter
|
|
8007
|
+
exports.toPublicEntity = toPublicEntity
|
|
8008
|
+
exports.webhookSigningMetadata = webhookSigningMetadata
|