@llblab/pi-kit 0.6.0 → 0.7.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/CHANGELOG.md +8 -0
- package/README.md +4 -4
- package/node_modules/@llblab/pi-actors/AGENTS.md +1 -1
- package/node_modules/@llblab/pi-actors/CHANGELOG.md +6 -0
- package/node_modules/@llblab/pi-actors/README.md +1 -1
- package/node_modules/@llblab/pi-actors/dist/lib/async-runs.d.ts +3 -0
- package/node_modules/@llblab/pi-actors/dist/lib/async-runs.js +14 -1
- package/node_modules/@llblab/pi-actors/dist/lib/command-templates.js +45 -3
- package/node_modules/@llblab/pi-actors/dist/lib/extension-runtime.js +1 -1
- package/node_modules/@llblab/pi-actors/dist/lib/observability.d.ts +16 -3
- package/node_modules/@llblab/pi-actors/dist/lib/observability.js +92 -7
- package/node_modules/@llblab/pi-actors/dist/lib/pi.d.ts +0 -1
- package/node_modules/@llblab/pi-actors/dist/lib/pi.js +15 -24
- package/node_modules/@llblab/pi-actors/dist/lib/run-delivery-lineage.d.ts +17 -0
- package/node_modules/@llblab/pi-actors/dist/lib/run-delivery-lineage.js +44 -0
- package/node_modules/@llblab/pi-actors/dist/lib/run-delivery.d.ts +4 -0
- package/node_modules/@llblab/pi-actors/dist/lib/run-delivery.js +102 -4
- package/node_modules/@llblab/pi-actors/dist/lib/run-ui-runtime.js +58 -39
- package/node_modules/@llblab/pi-actors/dist/lib/runtime.js +14 -6
- package/node_modules/@llblab/pi-actors/dist/skills/actors/SKILL.md +1 -1
- package/node_modules/@llblab/pi-actors/docs/async-runs.md +1 -1
- package/node_modules/@llblab/pi-actors/docs/coordinator-delivery.md +18 -23
- package/node_modules/@llblab/pi-actors/lib/async-runs.ts +18 -1
- package/node_modules/@llblab/pi-actors/lib/command-templates.ts +41 -3
- package/node_modules/@llblab/pi-actors/lib/extension-runtime.ts +1 -1
- package/node_modules/@llblab/pi-actors/lib/observability.ts +119 -5
- package/node_modules/@llblab/pi-actors/lib/pi.ts +15 -28
- package/node_modules/@llblab/pi-actors/lib/run-delivery-lineage.ts +68 -0
- package/node_modules/@llblab/pi-actors/lib/run-delivery.ts +120 -4
- package/node_modules/@llblab/pi-actors/lib/run-ui-runtime.ts +69 -44
- package/node_modules/@llblab/pi-actors/lib/runtime.ts +17 -6
- package/node_modules/@llblab/pi-actors/package.json +1 -1
- package/node_modules/@llblab/pi-actors/skills/actors/SKILL.md +1 -1
- package/node_modules/@llblab/pi-state-flow/AGENTS.md +33 -12
- package/node_modules/@llblab/pi-state-flow/BACKLOG.md +125 -2
- package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +87 -45
- package/node_modules/@llblab/pi-state-flow/README.md +203 -107
- package/node_modules/@llblab/pi-state-flow/docs/README.md +4 -0
- package/node_modules/@llblab/pi-state-flow/docs/architecture.md +188 -0
- package/node_modules/@llblab/pi-state-flow/docs/temporal-acceptance.md +42 -0
- package/node_modules/@llblab/pi-state-flow/index.ts +164 -2
- package/node_modules/@llblab/pi-state-flow/lib/acquisition.ts +138 -0
- package/node_modules/@llblab/pi-state-flow/lib/artifact.ts +273 -0
- package/node_modules/@llblab/pi-state-flow/lib/config.ts +48 -0
- package/node_modules/@llblab/pi-state-flow/lib/context.ts +18 -5
- package/node_modules/@llblab/pi-state-flow/lib/continuation.ts +268 -0
- package/node_modules/@llblab/pi-state-flow/lib/discovery.ts +117 -0
- package/node_modules/@llblab/pi-state-flow/lib/durable.ts +562 -0
- package/node_modules/@llblab/pi-state-flow/lib/episode.ts +24 -12
- package/node_modules/@llblab/pi-state-flow/lib/extension.ts +606 -70
- package/node_modules/@llblab/pi-state-flow/lib/git.ts +666 -0
- package/node_modules/@llblab/pi-state-flow/lib/history.ts +95 -0
- package/node_modules/@llblab/pi-state-flow/lib/json.ts +24 -0
- package/node_modules/@llblab/pi-state-flow/lib/maintenance.ts +141 -0
- package/node_modules/@llblab/pi-state-flow/lib/memory.ts +52 -0
- package/node_modules/@llblab/pi-state-flow/lib/migration.ts +88 -0
- package/node_modules/@llblab/pi-state-flow/lib/publication.ts +296 -0
- package/node_modules/@llblab/pi-state-flow/lib/recovery.ts +23 -7
- package/node_modules/@llblab/pi-state-flow/lib/rehydration.ts +79 -0
- package/node_modules/@llblab/pi-state-flow/lib/runtime.ts +264 -0
- package/node_modules/@llblab/pi-state-flow/lib/session.ts +6 -0
- package/node_modules/@llblab/pi-state-flow/lib/skills.ts +99 -7
- package/node_modules/@llblab/pi-state-flow/lib/snapshot.ts +305 -48
- package/node_modules/@llblab/pi-state-flow/lib/state.ts +74 -7
- package/node_modules/@llblab/pi-state-flow/lib/status.ts +125 -6
- package/node_modules/@llblab/pi-state-flow/lib/storage.ts +196 -0
- package/node_modules/@llblab/pi-state-flow/lib/temporal.ts +233 -0
- package/node_modules/@llblab/pi-state-flow/lib/terminal.ts +70 -24
- package/node_modules/@llblab/pi-state-flow/lib/transition.ts +254 -29
- package/node_modules/@llblab/pi-state-flow/package.json +8 -2
- package/node_modules/@llblab/pi-state-flow/skills/state-flow-memory/SKILL.md +128 -0
- package/node_modules/@llblab/pi-telegram/AGENTS.md +14 -9
- package/node_modules/@llblab/pi-telegram/BACKLOG.md +23 -5
- package/node_modules/@llblab/pi-telegram/CHANGELOG.md +18 -0
- package/node_modules/@llblab/pi-telegram/README.md +13 -9
- package/node_modules/@llblab/pi-telegram/docs/README.md +1 -0
- package/node_modules/@llblab/pi-telegram/docs/architecture.md +227 -23
- package/node_modules/@llblab/pi-telegram/docs/generative-apps.md +1 -1
- package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +70 -19
- package/node_modules/@llblab/pi-telegram/docs/outbound.md +6 -6
- package/node_modules/@llblab/pi-telegram/docs/public-api.md +13 -6
- package/node_modules/@llblab/pi-telegram/docs/ui-style.md +3 -1
- package/node_modules/@llblab/pi-telegram/index.ts +4 -1418
- package/node_modules/@llblab/pi-telegram/lib/agent-messages.ts +6 -3
- package/node_modules/@llblab/pi-telegram/lib/bindings.ts +46 -1
- package/node_modules/@llblab/pi-telegram/lib/bus-api.ts +32 -19
- package/node_modules/@llblab/pi-telegram/lib/bus-follower.ts +600 -135
- package/node_modules/@llblab/pi-telegram/lib/bus-leader.ts +962 -55
- package/node_modules/@llblab/pi-telegram/lib/bus.ts +355 -26
- package/node_modules/@llblab/pi-telegram/lib/channel-posts.ts +718 -0
- package/node_modules/@llblab/pi-telegram/lib/commands.ts +237 -11
- package/node_modules/@llblab/pi-telegram/lib/config.ts +242 -26
- package/node_modules/@llblab/pi-telegram/lib/extension.ts +1851 -0
- package/node_modules/@llblab/pi-telegram/lib/generative-apps.ts +20 -2
- package/node_modules/@llblab/pi-telegram/lib/journal.ts +2184 -126
- package/node_modules/@llblab/pi-telegram/lib/locks.ts +44 -2
- package/node_modules/@llblab/pi-telegram/lib/menu-settings.ts +152 -13
- package/node_modules/@llblab/pi-telegram/lib/outbound-attachments.ts +97 -10
- package/node_modules/@llblab/pi-telegram/lib/paths.ts +29 -0
- package/node_modules/@llblab/pi-telegram/lib/polling.ts +85 -17
- package/node_modules/@llblab/pi-telegram/lib/preview.ts +17 -0
- package/node_modules/@llblab/pi-telegram/lib/prompts.ts +6 -2
- package/node_modules/@llblab/pi-telegram/lib/queue.ts +118 -26
- package/node_modules/@llblab/pi-telegram/lib/rendering.ts +4 -1
- package/node_modules/@llblab/pi-telegram/lib/replies.ts +21 -2
- package/node_modules/@llblab/pi-telegram/lib/routing.ts +344 -112
- package/node_modules/@llblab/pi-telegram/lib/setup.ts +44 -4
- package/node_modules/@llblab/pi-telegram/lib/status.ts +51 -4
- package/node_modules/@llblab/pi-telegram/lib/sync.ts +308 -39
- package/node_modules/@llblab/pi-telegram/lib/telegram-api.ts +353 -22
- package/node_modules/@llblab/pi-telegram/lib/thread-cleanup-manager.ts +664 -0
- package/node_modules/@llblab/pi-telegram/lib/thread-display.ts +226 -0
- package/node_modules/@llblab/pi-telegram/lib/thread-naming.ts +118 -0
- package/node_modules/@llblab/pi-telegram/lib/threads.ts +1686 -129
- package/node_modules/@llblab/pi-telegram/lib/turns.ts +7 -0
- package/node_modules/@llblab/pi-telegram/lib/updates.ts +1319 -97
- package/node_modules/@llblab/pi-telegram/lib/workspace-admission.ts +1643 -0
- package/node_modules/@llblab/pi-telegram/lib/workspace-retirement.ts +968 -0
- package/node_modules/@llblab/pi-telegram/lib/workspace-slots.ts +84 -0
- package/node_modules/@llblab/pi-telegram/package.json +1 -1
- package/node_modules/@llblab/pi-telegram/screenshot.png +0 -0
- package/node_modules/@llblab/pi-telegram/scripts/measure-bus.mjs +83 -0
- package/node_modules/@llblab/pi-telegram/scripts/measure-workspace.mjs +101 -0
- package/node_modules/@llblab/{skills → pi-telegram/skills}/show-me/SKILL.md +28 -6
- package/node_modules/@llblab/pi-telegram/skills/show-me/references/telegram-surfaces.md +43 -0
- package/node_modules/@llblab/pi-telegram/skills/telegram-bridge/references/delivery-and-threads.md +1 -1
- package/node_modules/@llblab/skills/package.json +2 -3
- package/package.json +6 -5
- /package/node_modules/@llblab/pi-telegram/lib/{logs.ts → logging.ts} +0 -0
|
@@ -19,6 +19,9 @@ import type {
|
|
|
19
19
|
} from "./bus.ts";
|
|
20
20
|
import type { TelegramMessageOwnershipStore } from "./ownership.ts";
|
|
21
21
|
import {
|
|
22
|
+
TELEGRAM_UPDATE_JOURNAL_VERSION,
|
|
23
|
+
TELEGRAM_UPDATE_JOURNAL_EXCLUSION_VERSION,
|
|
24
|
+
TELEGRAM_UPDATE_JOURNAL_CUSTODY_VERSION,
|
|
22
25
|
TELEGRAM_UPDATE_JOURNAL_FAILURE_CLASS_MAX_LENGTH,
|
|
23
26
|
TELEGRAM_UPDATE_JOURNAL_FAILURE_SUMMARY_MAX_LENGTH,
|
|
24
27
|
TELEGRAM_UPDATE_JOURNAL_QUEUE_OWNER_ID_MAX_LENGTH,
|
|
@@ -26,8 +29,13 @@ import {
|
|
|
26
29
|
getTelegramUpdateJournalBindingPath,
|
|
27
30
|
isTelegramUpdateJournalQueueOwnerProcess,
|
|
28
31
|
parseTelegramUpdateJournalQueueOwner,
|
|
32
|
+
type TelegramInputJournalReceipt,
|
|
33
|
+
type TelegramInputJournalSourceReference,
|
|
34
|
+
type TelegramInputJournalStore,
|
|
29
35
|
type TelegramJournaledUpdate,
|
|
30
36
|
type TelegramUpdateJournalDeadQueueOwnerRecoveryResult,
|
|
37
|
+
type TelegramUpdateJournalAppendResult,
|
|
38
|
+
type TelegramUpdateJournalInputClaim,
|
|
31
39
|
type TelegramUpdateJournalOperatorDispositionInput,
|
|
32
40
|
type TelegramUpdateJournalOperatorDispositionResult,
|
|
33
41
|
type TelegramUpdateJournalQueueDiscardResult,
|
|
@@ -445,6 +453,7 @@ function rejectTelegramForeignUpdateSettlement(
|
|
|
445
453
|
export interface TelegramMessageReactionUpdated {
|
|
446
454
|
chat: { id?: number; type: string };
|
|
447
455
|
user?: TelegramUser;
|
|
456
|
+
actor_chat?: unknown;
|
|
448
457
|
message_id: number;
|
|
449
458
|
old_reaction: TelegramReactionType[];
|
|
450
459
|
new_reaction: TelegramReactionType[];
|
|
@@ -1121,16 +1130,23 @@ function getForeignTelegramCallbackOwnership(
|
|
|
1121
1130
|
getTargetOwnership?: TelegramTargetOwnershipLookup;
|
|
1122
1131
|
},
|
|
1123
1132
|
): TelegramMessageOwnershipView | undefined {
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
deps,
|
|
1128
|
-
) ??
|
|
1129
|
-
getForeignTelegramTargetOwnership(
|
|
1130
|
-
query.message ? getTelegramMessageTarget(query.message) : undefined,
|
|
1131
|
-
deps,
|
|
1132
|
-
)
|
|
1133
|
+
const messageOwnership = getForeignTelegramMessageOwnership(
|
|
1134
|
+
getTelegramCallbackMessageTarget(query),
|
|
1135
|
+
deps,
|
|
1133
1136
|
);
|
|
1137
|
+
const targetOwnership = getForeignTelegramTargetOwnership(
|
|
1138
|
+
query.message ? getTelegramMessageTarget(query.message) : undefined,
|
|
1139
|
+
deps,
|
|
1140
|
+
);
|
|
1141
|
+
if (!messageOwnership) return targetOwnership;
|
|
1142
|
+
if (
|
|
1143
|
+
targetOwnership &&
|
|
1144
|
+
messageOwnership.recipientBindingKey &&
|
|
1145
|
+
messageOwnership.recipientBindingKey === targetOwnership.recipientBindingKey
|
|
1146
|
+
) {
|
|
1147
|
+
return targetOwnership;
|
|
1148
|
+
}
|
|
1149
|
+
return messageOwnership;
|
|
1134
1150
|
}
|
|
1135
1151
|
|
|
1136
1152
|
function getTelegramCallbackMessageTarget(
|
|
@@ -1226,8 +1242,7 @@ export function createTelegramPairedUpdateRuntime<
|
|
|
1226
1242
|
pairTelegramUserIfNeeded: (userId, ctx, assertExecutionCurrent) =>
|
|
1227
1243
|
createTelegramUserPairingRuntime({
|
|
1228
1244
|
getAllowedUserId: deps.getAllowedUserId,
|
|
1229
|
-
|
|
1230
|
-
persistConfig: deps.persistConfig,
|
|
1245
|
+
persistAllowedUserId: deps.persistAllowedUserId,
|
|
1231
1246
|
updateStatus: deps.updateStatus,
|
|
1232
1247
|
}).pairIfNeeded(userId, ctx, assertExecutionCurrent),
|
|
1233
1248
|
answerCallbackQuery: deps.answerCallbackQuery,
|
|
@@ -1328,6 +1343,13 @@ export async function handleAuthorizedTelegramReactionUpdate<TContext>(
|
|
|
1328
1343
|
reactionUpdate: TelegramMessageReactionUpdated,
|
|
1329
1344
|
deps: AuthorizedTelegramReactionUpdateDeps<TContext>,
|
|
1330
1345
|
): Promise<void> {
|
|
1346
|
+
const reactionUser = reactionUpdate.user;
|
|
1347
|
+
const allowedUserId = deps.allowedUserId;
|
|
1348
|
+
if (
|
|
1349
|
+
allowedUserId === undefined || !Number.isSafeInteger(allowedUserId) || allowedUserId <= 0 ||
|
|
1350
|
+
!reactionUser || reactionUser.is_bot || reactionUser.id !== allowedUserId ||
|
|
1351
|
+
reactionUpdate.actor_chat !== undefined
|
|
1352
|
+
) return;
|
|
1331
1353
|
const foreignOwnership = getForeignTelegramMessageOwnership(
|
|
1332
1354
|
getTelegramReactionMessageTarget(reactionUpdate),
|
|
1333
1355
|
deps,
|
|
@@ -1350,14 +1372,6 @@ export async function handleAuthorizedTelegramReactionUpdate<TContext>(
|
|
|
1350
1372
|
}
|
|
1351
1373
|
return;
|
|
1352
1374
|
}
|
|
1353
|
-
const reactionUser = reactionUpdate.user;
|
|
1354
|
-
if (!reactionUser || reactionUser.is_bot) return;
|
|
1355
|
-
if (
|
|
1356
|
-
reactionUpdate.chat.type !== "private" &&
|
|
1357
|
-
reactionUser.id !== deps.allowedUserId
|
|
1358
|
-
) {
|
|
1359
|
-
return;
|
|
1360
|
-
}
|
|
1361
1375
|
const reactionScope =
|
|
1362
1376
|
typeof reactionUpdate.chat.id === "number"
|
|
1363
1377
|
? { chatId: reactionUpdate.chat.id }
|
|
@@ -1428,6 +1442,26 @@ export async function executeTelegramUpdatePlan<
|
|
|
1428
1442
|
return;
|
|
1429
1443
|
}
|
|
1430
1444
|
if (plan.kind === "callback") {
|
|
1445
|
+
let pairingAllowed = true;
|
|
1446
|
+
if (plan.shouldPair) {
|
|
1447
|
+
assertExecutionCurrent();
|
|
1448
|
+
pairingAllowed = await deps.pairTelegramUserIfNeeded(
|
|
1449
|
+
plan.query.from.id,
|
|
1450
|
+
deps.ctx,
|
|
1451
|
+
assertExecutionCurrent,
|
|
1452
|
+
);
|
|
1453
|
+
}
|
|
1454
|
+
if (plan.shouldDeny || !pairingAllowed) {
|
|
1455
|
+
const callbackQueryId = getTelegramCallbackQueryId(plan.query);
|
|
1456
|
+
if (callbackQueryId) {
|
|
1457
|
+
assertExecutionCurrent();
|
|
1458
|
+
await deps.answerCallbackQuery(
|
|
1459
|
+
callbackQueryId,
|
|
1460
|
+
formatTelegramUnauthorizedDenial("plain"),
|
|
1461
|
+
);
|
|
1462
|
+
}
|
|
1463
|
+
return;
|
|
1464
|
+
}
|
|
1431
1465
|
const foreignOwnership = getForeignTelegramCallbackOwnership(
|
|
1432
1466
|
plan.query,
|
|
1433
1467
|
deps,
|
|
@@ -1461,25 +1495,6 @@ export async function executeTelegramUpdatePlan<
|
|
|
1461
1495
|
assertExecutionCurrent();
|
|
1462
1496
|
return;
|
|
1463
1497
|
}
|
|
1464
|
-
if (plan.shouldPair) {
|
|
1465
|
-
assertExecutionCurrent();
|
|
1466
|
-
await deps.pairTelegramUserIfNeeded(
|
|
1467
|
-
plan.query.from.id,
|
|
1468
|
-
deps.ctx,
|
|
1469
|
-
assertExecutionCurrent,
|
|
1470
|
-
);
|
|
1471
|
-
}
|
|
1472
|
-
if (plan.shouldDeny) {
|
|
1473
|
-
const callbackQueryId = getTelegramCallbackQueryId(plan.query);
|
|
1474
|
-
if (callbackQueryId) {
|
|
1475
|
-
assertExecutionCurrent();
|
|
1476
|
-
await deps.answerCallbackQuery(
|
|
1477
|
-
callbackQueryId,
|
|
1478
|
-
formatTelegramUnauthorizedDenial("plain"),
|
|
1479
|
-
);
|
|
1480
|
-
}
|
|
1481
|
-
return;
|
|
1482
|
-
}
|
|
1483
1498
|
assertExecutionCurrent();
|
|
1484
1499
|
await deps.handleAuthorizedTelegramCallbackQuery(plan.query, deps.ctx);
|
|
1485
1500
|
assertExecutionCurrent();
|
|
@@ -1505,8 +1520,44 @@ export async function executeTelegramUpdatePlan<
|
|
|
1505
1520
|
}
|
|
1506
1521
|
return;
|
|
1507
1522
|
}
|
|
1523
|
+
if (plan.shouldPair) assertExecutionCurrent();
|
|
1524
|
+
const pairedNow = plan.shouldPair
|
|
1525
|
+
? await deps.pairTelegramUserIfNeeded(
|
|
1526
|
+
plan.message.from.id,
|
|
1527
|
+
deps.ctx,
|
|
1528
|
+
assertExecutionCurrent,
|
|
1529
|
+
)
|
|
1530
|
+
: false;
|
|
1531
|
+
const replyTarget = getTelegramMessageReplyTarget(plan.message);
|
|
1532
|
+
if (plan.shouldDeny || (plan.shouldPair && !pairedNow)) {
|
|
1533
|
+
if (replyTarget) {
|
|
1534
|
+
assertExecutionCurrent();
|
|
1535
|
+
await deps.sendTextReply(
|
|
1536
|
+
replyTarget.chatId,
|
|
1537
|
+
replyTarget.messageId,
|
|
1538
|
+
formatTelegramUnauthorizedDenial("html"),
|
|
1539
|
+
{ parseMode: "HTML", target: replyTarget },
|
|
1540
|
+
);
|
|
1541
|
+
}
|
|
1542
|
+
return;
|
|
1543
|
+
}
|
|
1544
|
+
if (
|
|
1545
|
+
plan.kind === "message" &&
|
|
1546
|
+
pairedNow &&
|
|
1547
|
+
plan.shouldNotifyPaired &&
|
|
1548
|
+
replyTarget
|
|
1549
|
+
) {
|
|
1550
|
+
assertExecutionCurrent();
|
|
1551
|
+
await deps.sendTextReply(
|
|
1552
|
+
replyTarget.chatId,
|
|
1553
|
+
replyTarget.messageId,
|
|
1554
|
+
"Telegram bridge paired with this account.",
|
|
1555
|
+
{ target: replyTarget },
|
|
1556
|
+
);
|
|
1557
|
+
assertExecutionCurrent();
|
|
1558
|
+
}
|
|
1508
1559
|
const foreignMessageOwnership = getForeignTelegramMessageOwnership(
|
|
1509
|
-
|
|
1560
|
+
replyTarget,
|
|
1510
1561
|
deps,
|
|
1511
1562
|
);
|
|
1512
1563
|
if (foreignMessageOwnership) {
|
|
@@ -1581,42 +1632,6 @@ export async function executeTelegramUpdatePlan<
|
|
|
1581
1632
|
assertExecutionCurrent();
|
|
1582
1633
|
return;
|
|
1583
1634
|
}
|
|
1584
|
-
if (plan.shouldPair) assertExecutionCurrent();
|
|
1585
|
-
const pairedNow = plan.shouldPair
|
|
1586
|
-
? await deps.pairTelegramUserIfNeeded(
|
|
1587
|
-
plan.message.from.id,
|
|
1588
|
-
deps.ctx,
|
|
1589
|
-
assertExecutionCurrent,
|
|
1590
|
-
)
|
|
1591
|
-
: false;
|
|
1592
|
-
const replyTarget = getTelegramMessageReplyTarget(plan.message);
|
|
1593
|
-
if (
|
|
1594
|
-
plan.kind === "message" &&
|
|
1595
|
-
pairedNow &&
|
|
1596
|
-
plan.shouldNotifyPaired &&
|
|
1597
|
-
replyTarget
|
|
1598
|
-
) {
|
|
1599
|
-
assertExecutionCurrent();
|
|
1600
|
-
await deps.sendTextReply(
|
|
1601
|
-
replyTarget.chatId,
|
|
1602
|
-
replyTarget.messageId,
|
|
1603
|
-
"Telegram bridge paired with this account.",
|
|
1604
|
-
{ target: replyTarget },
|
|
1605
|
-
);
|
|
1606
|
-
assertExecutionCurrent();
|
|
1607
|
-
}
|
|
1608
|
-
if (plan.shouldDeny) {
|
|
1609
|
-
if (replyTarget) {
|
|
1610
|
-
assertExecutionCurrent();
|
|
1611
|
-
await deps.sendTextReply(
|
|
1612
|
-
replyTarget.chatId,
|
|
1613
|
-
replyTarget.messageId,
|
|
1614
|
-
formatTelegramUnauthorizedDenial("html"),
|
|
1615
|
-
{ parseMode: "HTML", target: replyTarget },
|
|
1616
|
-
);
|
|
1617
|
-
}
|
|
1618
|
-
return;
|
|
1619
|
-
}
|
|
1620
1635
|
if (plan.kind === "edited-message") {
|
|
1621
1636
|
assertExecutionCurrent();
|
|
1622
1637
|
await deps.handleAuthorizedTelegramEditedMessage(plan.message, deps.ctx);
|
|
@@ -1653,6 +1668,7 @@ export type TelegramUpdateWorkerBlockedReason =
|
|
|
1653
1668
|
| "journal-read"
|
|
1654
1669
|
| "journal-write"
|
|
1655
1670
|
| "execution"
|
|
1671
|
+
| "input-custody"
|
|
1656
1672
|
| "prior-generation-executing"
|
|
1657
1673
|
| "invalid-outcome";
|
|
1658
1674
|
|
|
@@ -1662,6 +1678,11 @@ export interface TelegramUpdateWorkerStateSnapshot {
|
|
|
1662
1678
|
phaseStartedAtMs?: number;
|
|
1663
1679
|
currentUpdateId?: number;
|
|
1664
1680
|
blockedReason?: TelegramUpdateWorkerBlockedReason;
|
|
1681
|
+
blockedInputCustody?: {
|
|
1682
|
+
updateId: number;
|
|
1683
|
+
kind: "running-outcome-unknown" | "foreign-ready" | "handoff-frozen" |
|
|
1684
|
+
"legacy-retry-state";
|
|
1685
|
+
};
|
|
1665
1686
|
journalEntryCount: number;
|
|
1666
1687
|
journalSerializedBytes: number;
|
|
1667
1688
|
oldestAdmittedAtMs?: number;
|
|
@@ -1690,12 +1711,17 @@ export interface TelegramUpdateWorkerStateSnapshot {
|
|
|
1690
1711
|
}
|
|
1691
1712
|
|
|
1692
1713
|
export interface TelegramUpdateWorkerJournalSnapshot {
|
|
1714
|
+
version: typeof TELEGRAM_UPDATE_JOURNAL_VERSION |
|
|
1715
|
+
typeof TELEGRAM_UPDATE_JOURNAL_EXCLUSION_VERSION |
|
|
1716
|
+
typeof TELEGRAM_UPDATE_JOURNAL_CUSTODY_VERSION;
|
|
1693
1717
|
acceptedThroughUpdateId?: number;
|
|
1694
1718
|
entries: readonly {
|
|
1695
1719
|
updateId: number;
|
|
1696
1720
|
update: TelegramJournaledUpdate;
|
|
1721
|
+
readonly preApprovalExcluded?: boolean;
|
|
1697
1722
|
admittedAtMs: number;
|
|
1698
1723
|
state: "pending" | "retry-wait" | "queued" | "failed";
|
|
1724
|
+
inputClaim?: TelegramUpdateJournalInputClaim;
|
|
1699
1725
|
queueKind?: "prompt" | "control";
|
|
1700
1726
|
queueReceiptId?: string;
|
|
1701
1727
|
queueOwner?: TelegramUpdateJournalQueueOwner;
|
|
@@ -1775,8 +1801,14 @@ export interface TelegramUpdateWorkerRuntimeDeps<TContext> {
|
|
|
1775
1801
|
ctx: TContext,
|
|
1776
1802
|
signal: AbortSignal,
|
|
1777
1803
|
) => Promise<TelegramUpdateAdmissionOutcome> | TelegramUpdateAdmissionOutcome;
|
|
1804
|
+
executeCustodiedUpdate?: (
|
|
1805
|
+
update: TelegramJournaledUpdate,
|
|
1806
|
+
ctx: TContext,
|
|
1807
|
+
signal: AbortSignal,
|
|
1808
|
+
) => Promise<TelegramCustodiedExecutionResult>;
|
|
1778
1809
|
hasAuthority: (ctx: TContext) => boolean;
|
|
1779
1810
|
getJournalBindingKey?: () => string | undefined;
|
|
1811
|
+
getRecipientBindingKey?: () => string | undefined;
|
|
1780
1812
|
getQueueOwnerIdentity?: (
|
|
1781
1813
|
ctx: TContext,
|
|
1782
1814
|
) => TelegramUpdateJournalQueueOwnerIdentity;
|
|
@@ -1818,6 +1850,11 @@ export interface TelegramUpdateWorkerRuntime<TContext> {
|
|
|
1818
1850
|
outcome: TelegramUpdateAdmissionOutcome;
|
|
1819
1851
|
signal: AbortSignal;
|
|
1820
1852
|
}) => void;
|
|
1853
|
+
settleCustodied: (input: {
|
|
1854
|
+
updateId: number;
|
|
1855
|
+
result: TelegramCustodiedExecutionResult;
|
|
1856
|
+
signal: AbortSignal;
|
|
1857
|
+
}) => void;
|
|
1821
1858
|
isQueueReceiptCommitted: (
|
|
1822
1859
|
receipt: TelegramQueueAdmissionReceiptLike,
|
|
1823
1860
|
) => boolean;
|
|
@@ -1851,7 +1888,8 @@ interface TelegramUpdateWorkerOwner<TContext> {
|
|
|
1851
1888
|
type TelegramUpdateWorkerClaim = "deferred" | "queued";
|
|
1852
1889
|
type TelegramUpdateWorkerDrainResult = "idle" | "blocked" | "aborted";
|
|
1853
1890
|
type TelegramUpdateWorkerExecutionSettlement =
|
|
1854
|
-
| { ok: true; outcome: TelegramUpdateAdmissionOutcome }
|
|
1891
|
+
| { ok: true; outcome: TelegramUpdateAdmissionOutcome; custodied?: false }
|
|
1892
|
+
| { ok: true; outcome: TelegramCustodiedExecutionResult; custodied: true }
|
|
1855
1893
|
| { ok: false; error: unknown };
|
|
1856
1894
|
|
|
1857
1895
|
const TELEGRAM_UPDATE_WORKER_EXECUTION_ABORTED = Symbol(
|
|
@@ -2279,6 +2317,18 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
|
|
|
2279
2317
|
snapshot: TelegramUpdateWorkerJournalSnapshot,
|
|
2280
2318
|
expectedOwner: TelegramUpdateWorkerOwner<TContext>,
|
|
2281
2319
|
): number | undefined => {
|
|
2320
|
+
// Validate the whole snapshot before reconstructing any queue authority.
|
|
2321
|
+
if ((snapshot.version !== TELEGRAM_UPDATE_JOURNAL_VERSION &&
|
|
2322
|
+
snapshot.version !== TELEGRAM_UPDATE_JOURNAL_EXCLUSION_VERSION &&
|
|
2323
|
+
snapshot.version !== TELEGRAM_UPDATE_JOURNAL_CUSTODY_VERSION) ||
|
|
2324
|
+
(snapshot.version === TELEGRAM_UPDATE_JOURNAL_CUSTODY_VERSION &&
|
|
2325
|
+
!deps.executeCustodiedUpdate) ||
|
|
2326
|
+
snapshot.entries.some((entry) =>
|
|
2327
|
+
((snapshot.version === TELEGRAM_UPDATE_JOURNAL_EXCLUSION_VERSION || entry.preApprovalExcluded !== undefined) &&
|
|
2328
|
+
typeof entry.preApprovalExcluded !== "boolean") ||
|
|
2329
|
+
(entry.preApprovalExcluded === true && entry.state === "queued"))) {
|
|
2330
|
+
throw new TelegramUpdateAdmissionOutcomeError("Telegram journal snapshot has invalid pairing exclusion evidence.");
|
|
2331
|
+
}
|
|
2282
2332
|
const availableUpdateIds = new Set<number>();
|
|
2283
2333
|
const queuedReceiptEntries = new Map<
|
|
2284
2334
|
string,
|
|
@@ -2464,16 +2514,17 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
|
|
|
2464
2514
|
if (expectedOwner.controller.signal.aborted) {
|
|
2465
2515
|
return TELEGRAM_UPDATE_WORKER_EXECUTION_ABORTED;
|
|
2466
2516
|
}
|
|
2467
|
-
const
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
)
|
|
2473
|
-
);
|
|
2517
|
+
const custodied = deps.executeCustodiedUpdate !== undefined;
|
|
2518
|
+
const execution = Promise.resolve().then(async (): Promise<
|
|
2519
|
+
TelegramUpdateAdmissionOutcome | TelegramCustodiedExecutionResult
|
|
2520
|
+
> => deps.executeCustodiedUpdate
|
|
2521
|
+
? deps.executeCustodiedUpdate(update, expectedOwner.ctx, expectedOwner.controller.signal)
|
|
2522
|
+
: deps.executeUpdate(update, expectedOwner.ctx, expectedOwner.controller.signal));
|
|
2474
2523
|
const settlement: Promise<TelegramUpdateWorkerExecutionSettlement> =
|
|
2475
2524
|
execution.then(
|
|
2476
|
-
(outcome) =>
|
|
2525
|
+
(outcome) => custodied
|
|
2526
|
+
? ({ ok: true, outcome: outcome as unknown as TelegramCustodiedExecutionResult, custodied: true })
|
|
2527
|
+
: ({ ok: true, outcome: outcome as TelegramUpdateAdmissionOutcome }),
|
|
2477
2528
|
(error: unknown) => ({ ok: false, error }),
|
|
2478
2529
|
);
|
|
2479
2530
|
unsettledExecutions.add(settlement);
|
|
@@ -2768,7 +2819,33 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
|
|
|
2768
2819
|
const entries: TelegramUpdateWorkerJournalSnapshot["entries"][number][] =
|
|
2769
2820
|
[];
|
|
2770
2821
|
let hasMoreEntries = false;
|
|
2822
|
+
let hasOutcomeUnknownInput = false;
|
|
2823
|
+
let hasUnavailableInputCustody = false;
|
|
2824
|
+
let blockedInputCustody: TelegramUpdateWorkerStateSnapshot["blockedInputCustody"];
|
|
2825
|
+
delete state.blockedInputCustody;
|
|
2771
2826
|
for (const candidate of snapshot.entries) {
|
|
2827
|
+
if (snapshot.version === TELEGRAM_UPDATE_JOURNAL_CUSTODY_VERSION &&
|
|
2828
|
+
(candidate.state === "retry-wait" || candidate.state === "failed")) {
|
|
2829
|
+
hasUnavailableInputCustody = true;
|
|
2830
|
+
blockedInputCustody ??= { updateId: candidate.updateId, kind: "legacy-retry-state" };
|
|
2831
|
+
continue;
|
|
2832
|
+
}
|
|
2833
|
+
if (snapshot.version === TELEGRAM_UPDATE_JOURNAL_CUSTODY_VERSION && candidate.inputClaim) {
|
|
2834
|
+
if (candidate.inputClaim.phase === "running") {
|
|
2835
|
+
hasOutcomeUnknownInput = true;
|
|
2836
|
+
if (!blockedInputCustody || blockedInputCustody.kind !== "running-outcome-unknown")
|
|
2837
|
+
blockedInputCustody = { updateId: candidate.updateId,
|
|
2838
|
+
kind: "running-outcome-unknown" };
|
|
2839
|
+
continue;
|
|
2840
|
+
}
|
|
2841
|
+
if (candidate.inputClaim.handoff || !isTelegramUpdateJournalQueueOwnerProcess(
|
|
2842
|
+
candidate.inputClaim.owner, expectedOwner.queueOwnerIdentity)) {
|
|
2843
|
+
hasUnavailableInputCustody = true;
|
|
2844
|
+
blockedInputCustody ??= { updateId: candidate.updateId,
|
|
2845
|
+
kind: candidate.inputClaim.handoff ? "handoff-frozen" : "foreign-ready" };
|
|
2846
|
+
continue;
|
|
2847
|
+
}
|
|
2848
|
+
}
|
|
2772
2849
|
if (
|
|
2773
2850
|
!claims.has(candidate.updateId) &&
|
|
2774
2851
|
(candidate.state === "pending" ||
|
|
@@ -2785,6 +2862,12 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
|
|
|
2785
2862
|
}
|
|
2786
2863
|
if (entries.length === 0) {
|
|
2787
2864
|
scheduleNextRetry(scheduledRetryAtMs, expectedOwner);
|
|
2865
|
+
if (hasOutcomeUnknownInput || hasUnavailableInputCustody) {
|
|
2866
|
+
if (blockedInputCustody) state.blockedInputCustody = blockedInputCustody;
|
|
2867
|
+
transition("blocked", blockedInputCustody?.updateId,
|
|
2868
|
+
hasOutcomeUnknownInput ? "execution" : "input-custody");
|
|
2869
|
+
return "blocked";
|
|
2870
|
+
}
|
|
2788
2871
|
transition("idle");
|
|
2789
2872
|
return "idle";
|
|
2790
2873
|
}
|
|
@@ -2809,6 +2892,10 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
|
|
|
2809
2892
|
snapshotInvalidated = true;
|
|
2810
2893
|
break;
|
|
2811
2894
|
}
|
|
2895
|
+
if (entry.preApprovalExcluded === true) {
|
|
2896
|
+
completedUpdateIds.push(entry.updateId);
|
|
2897
|
+
continue;
|
|
2898
|
+
}
|
|
2812
2899
|
clearRetryTimer();
|
|
2813
2900
|
transition("executing", entry.updateId);
|
|
2814
2901
|
const execution = await executeWithinOwner(expectedOwner, entry.update);
|
|
@@ -2816,6 +2903,10 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
|
|
|
2816
2903
|
return "aborted";
|
|
2817
2904
|
}
|
|
2818
2905
|
if (!execution.ok) {
|
|
2906
|
+
if (deps.executeCustodiedUpdate) {
|
|
2907
|
+
transition("blocked", entry.updateId, "execution");
|
|
2908
|
+
return "blocked";
|
|
2909
|
+
}
|
|
2819
2910
|
const completionResult = commitCompletedBatch(
|
|
2820
2911
|
expectedOwner,
|
|
2821
2912
|
completedUpdateIds,
|
|
@@ -2832,6 +2923,37 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
|
|
|
2832
2923
|
snapshotInvalidated = true;
|
|
2833
2924
|
break;
|
|
2834
2925
|
}
|
|
2926
|
+
if (execution.custodied) {
|
|
2927
|
+
const outcome = execution.outcome;
|
|
2928
|
+
if (outcome.status === "outcome-unknown") {
|
|
2929
|
+
transition("blocked", entry.updateId, "execution");
|
|
2930
|
+
return "blocked";
|
|
2931
|
+
}
|
|
2932
|
+
if (outcome.status === "deferred") {
|
|
2933
|
+
claims.set(entry.updateId, "deferred");
|
|
2934
|
+
transition("deferred", entry.updateId);
|
|
2935
|
+
continue;
|
|
2936
|
+
}
|
|
2937
|
+
if (outcome.status === "queued") {
|
|
2938
|
+
const receipt = normalizeQueueReceipt(outcome.queueReceipt);
|
|
2939
|
+
if (!isTelegramUpdateJournalQueueOwnerProcess(
|
|
2940
|
+
outcome.queueReceipt.queueOwner, expectedOwner.queueOwnerIdentity)) {
|
|
2941
|
+
return blockWithFailure("invalid-outcome", "custody-queue-owner",
|
|
2942
|
+
new Error("Telegram custodied queue receipt belongs to another process."),
|
|
2943
|
+
entry.updateId);
|
|
2944
|
+
}
|
|
2945
|
+
for (const sourceUpdateId of receipt.sourceUpdateIds) claims.set(sourceUpdateId, "queued");
|
|
2946
|
+
try {
|
|
2947
|
+
publishCommittedQueueReceipt(receipt, outcome.queueReceipt.queueOwner, expectedOwner.ctx);
|
|
2948
|
+
} catch (error) {
|
|
2949
|
+
return blockWithFailure("invalid-outcome", "queue-receipt-publish", error,
|
|
2950
|
+
entry.updateId);
|
|
2951
|
+
}
|
|
2952
|
+
transition("queued", entry.updateId);
|
|
2953
|
+
}
|
|
2954
|
+
snapshotInvalidated = true;
|
|
2955
|
+
break;
|
|
2956
|
+
}
|
|
2835
2957
|
const postExecutionAuthority = checkAuthority(
|
|
2836
2958
|
expectedOwner,
|
|
2837
2959
|
entry.updateId,
|
|
@@ -3095,6 +3217,61 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
|
|
|
3095
3217
|
if (result === "aborted") return;
|
|
3096
3218
|
transition("queued", input.updateId);
|
|
3097
3219
|
},
|
|
3220
|
+
settleCustodied(input) {
|
|
3221
|
+
const expectedOwner = owner;
|
|
3222
|
+
if (!expectedOwner || expectedOwner.controller.signal !== input.signal || input.signal.aborted)
|
|
3223
|
+
return;
|
|
3224
|
+
if (checkAuthority(expectedOwner, input.updateId)) return;
|
|
3225
|
+
const claim = claims.get(input.updateId);
|
|
3226
|
+
if (claim !== "deferred" && claim !== "queued") return;
|
|
3227
|
+
if (input.result.status === "deferred") return;
|
|
3228
|
+
if (input.result.status === "outcome-unknown") {
|
|
3229
|
+
transition("blocked", input.updateId, "execution");
|
|
3230
|
+
return;
|
|
3231
|
+
}
|
|
3232
|
+
if (input.result.status === "completed") {
|
|
3233
|
+
if (claim !== "deferred") return;
|
|
3234
|
+
claims.delete(input.updateId);
|
|
3235
|
+
transition("idle", input.updateId);
|
|
3236
|
+
pendingSignal = true;
|
|
3237
|
+
launchDrain();
|
|
3238
|
+
return;
|
|
3239
|
+
}
|
|
3240
|
+
const receipt = normalizeQueueReceipt(input.result.queueReceipt);
|
|
3241
|
+
const existing = committedQueueReceipts.get(receipt.receiptId);
|
|
3242
|
+
if (existing && areTelegramQueueAdmissionReceiptsEqual(existing.receipt, receipt) &&
|
|
3243
|
+
areTelegramUpdateJournalQueueOwnersEqual(
|
|
3244
|
+
existing.queueOwner, input.result.queueReceipt.queueOwner)) return;
|
|
3245
|
+
if (existing) {
|
|
3246
|
+
blockWithFailure("invalid-outcome", "queue-receipt-conflict",
|
|
3247
|
+
new TelegramUpdateAdmissionOutcomeError(
|
|
3248
|
+
`Telegram queue receipt ${receipt.receiptId} conflicts with custodied authority.`),
|
|
3249
|
+
input.updateId);
|
|
3250
|
+
return;
|
|
3251
|
+
}
|
|
3252
|
+
if (!receipt.sourceUpdateIds.includes(input.updateId) ||
|
|
3253
|
+
receipt.sourceUpdateIds.some(updateId => claims.get(updateId) !== "deferred")) {
|
|
3254
|
+
blockWithFailure("invalid-outcome", "late-custody-unclaimed",
|
|
3255
|
+
new TelegramUpdateAdmissionOutcomeError(
|
|
3256
|
+
`Telegram update ${input.updateId} reported custodied queue without exact deferred claims.`),
|
|
3257
|
+
input.updateId);
|
|
3258
|
+
return;
|
|
3259
|
+
}
|
|
3260
|
+
if (!isTelegramUpdateJournalQueueOwnerProcess(
|
|
3261
|
+
input.result.queueReceipt.queueOwner, expectedOwner.queueOwnerIdentity)) {
|
|
3262
|
+
blockWithFailure("invalid-outcome", "custody-queue-owner",
|
|
3263
|
+
new Error("Telegram custodied queue receipt belongs to another process."), input.updateId);
|
|
3264
|
+
return;
|
|
3265
|
+
}
|
|
3266
|
+
for (const sourceUpdateId of receipt.sourceUpdateIds) claims.set(sourceUpdateId, "queued");
|
|
3267
|
+
try {
|
|
3268
|
+
publishCommittedQueueReceipt(receipt, input.result.queueReceipt.queueOwner, expectedOwner.ctx);
|
|
3269
|
+
} catch (error) {
|
|
3270
|
+
blockWithFailure("invalid-outcome", "queue-receipt-publish", error, input.updateId);
|
|
3271
|
+
return;
|
|
3272
|
+
}
|
|
3273
|
+
transition("queued", input.updateId);
|
|
3274
|
+
},
|
|
3098
3275
|
isQueueReceiptCommitted(receipt) {
|
|
3099
3276
|
const committed = committedQueueReceipts.get(receipt.receiptId);
|
|
3100
3277
|
return (
|
|
@@ -3487,6 +3664,954 @@ function mergeTelegramReportedAdmissionOutcome(
|
|
|
3487
3664
|
);
|
|
3488
3665
|
}
|
|
3489
3666
|
|
|
3667
|
+
export type TelegramCustodiedExecutionResult =
|
|
3668
|
+
| { status: "completed" }
|
|
3669
|
+
| { status: "deferred"; receipt: TelegramInputJournalReceipt }
|
|
3670
|
+
| { status: "queued"; queueReceipt: ReturnType<TelegramInputJournalStore["queueInputs"]>["queueReceipt"] }
|
|
3671
|
+
| { status: "outcome-unknown"; receipt: TelegramInputJournalReceipt };
|
|
3672
|
+
|
|
3673
|
+
type TelegramCustodyExecutionJournal = Pick<TelegramInputJournalStore,
|
|
3674
|
+
"acquireInput" | "startInput" | "completeInput" | "queueInputs">;
|
|
3675
|
+
|
|
3676
|
+
export function createTelegramInputCustodyWorkerJournalPort(
|
|
3677
|
+
store: TelegramInputJournalStore,
|
|
3678
|
+
): TelegramUpdateWorkerJournalPort & { inputCustody: TelegramCustodyExecutionJournal } {
|
|
3679
|
+
const legacyMutation = (): never => {
|
|
3680
|
+
throw new TelegramUpdateAdmissionOutcomeError(
|
|
3681
|
+
"Telegram v3 custody forbids legacy raw worker settlement.",
|
|
3682
|
+
);
|
|
3683
|
+
};
|
|
3684
|
+
return {
|
|
3685
|
+
read: () => store.read() as unknown as TelegramUpdateWorkerJournalSnapshot,
|
|
3686
|
+
markQueued: legacyMutation,
|
|
3687
|
+
markExecutionFailure: legacyMutation,
|
|
3688
|
+
removeCompleted: legacyMutation,
|
|
3689
|
+
completeQueued: receipts => store.completeQueued(receipts),
|
|
3690
|
+
inputCustody: {
|
|
3691
|
+
acquireInput: store.acquireInput,
|
|
3692
|
+
startInput: store.startInput,
|
|
3693
|
+
completeInput: store.completeInput,
|
|
3694
|
+
queueInputs: store.queueInputs,
|
|
3695
|
+
},
|
|
3696
|
+
};
|
|
3697
|
+
}
|
|
3698
|
+
|
|
3699
|
+
export function createTelegramInputCustodyLegacyDispositionRuntime(deps: {
|
|
3700
|
+
withBindingReference<T>(recoveryKey: string, operation: (binding: {
|
|
3701
|
+
recoveryKey: string; journal: Pick<TelegramInputJournalStore,
|
|
3702
|
+
"listLegacyCustodyCandidates" | "applyLegacyCustodyDisposition">;
|
|
3703
|
+
}) => T): T;
|
|
3704
|
+
}) {
|
|
3705
|
+
const withBinding = <T>(recoveryKey: string, operation: (journal: Pick<
|
|
3706
|
+
TelegramInputJournalStore, "listLegacyCustodyCandidates" |
|
|
3707
|
+
"applyLegacyCustodyDisposition">) => T): T => {
|
|
3708
|
+
if (!recoveryKey) throw new TelegramUpdateAdmissionOutcomeError(
|
|
3709
|
+
"Telegram legacy custody disposition binding is unavailable.");
|
|
3710
|
+
return deps.withBindingReference(recoveryKey, binding => {
|
|
3711
|
+
if (binding.recoveryKey !== recoveryKey)
|
|
3712
|
+
throw new TelegramUpdateAdmissionOutcomeError(
|
|
3713
|
+
"Telegram legacy custody disposition binding is unavailable.");
|
|
3714
|
+
return operation(binding.journal);
|
|
3715
|
+
});
|
|
3716
|
+
};
|
|
3717
|
+
return {
|
|
3718
|
+
list(recoveryKey: string) {
|
|
3719
|
+
return withBinding(recoveryKey, journal => journal.listLegacyCustodyCandidates());
|
|
3720
|
+
},
|
|
3721
|
+
apply(recoveryKey: string,
|
|
3722
|
+
authority: Parameters<TelegramInputJournalStore["applyLegacyCustodyDisposition"]>[0]) {
|
|
3723
|
+
return withBinding(recoveryKey,
|
|
3724
|
+
journal => journal.applyLegacyCustodyDisposition(authority));
|
|
3725
|
+
},
|
|
3726
|
+
};
|
|
3727
|
+
}
|
|
3728
|
+
|
|
3729
|
+
export function createTelegramInputCustodyHandoffClient(deps: {
|
|
3730
|
+
journal: Pick<TelegramInputJournalStore, "offerInputHandoff">;
|
|
3731
|
+
resolveAcceptedReference?: (input: {
|
|
3732
|
+
sourceUpdateId: number; recipientBindingKey: string;
|
|
3733
|
+
}) => { sourceRecoveryKey: string; source: { updateId: number; owner: {
|
|
3734
|
+
acquisitionId: string; handoffId: string } } } | undefined;
|
|
3735
|
+
sendEnvelope(envelope: Extract<TelegramBusEnvelope,
|
|
3736
|
+
{ kind: "leader.offerInputCustodyHandoff" }>): Promise<TelegramBusEnvelope | undefined>;
|
|
3737
|
+
}): { transfer(input: {
|
|
3738
|
+
requestId: string;
|
|
3739
|
+
receipt: TelegramInputJournalReceipt;
|
|
3740
|
+
recipientInstanceId: string;
|
|
3741
|
+
recipientRegistrationGeneration: string;
|
|
3742
|
+
recipientBindingKey: string;
|
|
3743
|
+
recipientOwner: TelegramUpdateJournalQueueOwnerIdentity;
|
|
3744
|
+
handoffToken: string;
|
|
3745
|
+
sentAtMs: number;
|
|
3746
|
+
auth?: string;
|
|
3747
|
+
}): Promise<{ sourceRecoveryKey: string; source: { updateId: number; owner: {
|
|
3748
|
+
acquisitionId: string; handoffId: string } }; duplicate: boolean }> } {
|
|
3749
|
+
return {
|
|
3750
|
+
async transfer(input) {
|
|
3751
|
+
const reconciled = deps.resolveAcceptedReference?.({
|
|
3752
|
+
sourceUpdateId: input.receipt.updateId,
|
|
3753
|
+
recipientBindingKey: input.recipientBindingKey });
|
|
3754
|
+
if (reconciled) return { ...reconciled, duplicate: true };
|
|
3755
|
+
const offered = deps.journal.offerInputHandoff({ receipt: input.receipt,
|
|
3756
|
+
recipientOwner: input.recipientOwner, handoffToken: input.handoffToken });
|
|
3757
|
+
const envelope: Extract<TelegramBusEnvelope,
|
|
3758
|
+
{ kind: "leader.offerInputCustodyHandoff" }> = {
|
|
3759
|
+
kind: "leader.offerInputCustodyHandoff", requestId: input.requestId,
|
|
3760
|
+
recipientInstanceId: input.recipientInstanceId,
|
|
3761
|
+
recipientRegistrationGeneration: input.recipientRegistrationGeneration,
|
|
3762
|
+
recipientBindingKey: input.recipientBindingKey,
|
|
3763
|
+
sourceRecoveryKey: offered.source.journalBindingKey,
|
|
3764
|
+
source: offered.source, handoffId: offered.handoff.handoffId,
|
|
3765
|
+
sentAtMs: input.sentAtMs, ...(input.auth ? { auth: input.auth } : {}),
|
|
3766
|
+
};
|
|
3767
|
+
const response = await deps.sendEnvelope(envelope);
|
|
3768
|
+
if (response?.kind !== "bus.ack" || response.requestId !== input.requestId ||
|
|
3769
|
+
!response.ok || !response.result || typeof response.result !== "object" ||
|
|
3770
|
+
Array.isArray(response.result)) throw new Error(
|
|
3771
|
+
"Telegram input custody handoff acknowledgement is missing or rejected.",
|
|
3772
|
+
);
|
|
3773
|
+
const result = response.result as Record<string, unknown>;
|
|
3774
|
+
const source = result.source;
|
|
3775
|
+
if (result.sourceRecoveryKey !== offered.source.journalBindingKey ||
|
|
3776
|
+
!source || typeof source !== "object" || Array.isArray(source) ||
|
|
3777
|
+
(source as Record<string, unknown>).updateId !== offered.source.updateId ||
|
|
3778
|
+
!(source as Record<string, unknown>).owner ||
|
|
3779
|
+
typeof (source as Record<string, unknown>).owner !== "object" ||
|
|
3780
|
+
Array.isArray((source as Record<string, unknown>).owner) ||
|
|
3781
|
+
typeof ((source as Record<string, unknown>).owner as Record<string, unknown>).acquisitionId !== "string" ||
|
|
3782
|
+
((source as Record<string, unknown>).owner as Record<string, unknown>).handoffId !== offered.handoff.handoffId ||
|
|
3783
|
+
typeof result.duplicate !== "boolean") throw new Error(
|
|
3784
|
+
"Telegram input custody handoff acknowledgement returned mismatched authority.",
|
|
3785
|
+
);
|
|
3786
|
+
return { sourceRecoveryKey: result.sourceRecoveryKey as string,
|
|
3787
|
+
source: { updateId: (source as Record<string, unknown>).updateId as number, owner: {
|
|
3788
|
+
acquisitionId: ((source as Record<string, unknown>).owner as Record<string, unknown>).acquisitionId as string,
|
|
3789
|
+
handoffId: ((source as Record<string, unknown>).owner as Record<string, unknown>).handoffId as string } },
|
|
3790
|
+
duplicate: result.duplicate as boolean };
|
|
3791
|
+
},
|
|
3792
|
+
};
|
|
3793
|
+
}
|
|
3794
|
+
|
|
3795
|
+
export interface TelegramInputCustodyHandoffAcceptanceInput {
|
|
3796
|
+
sourceRecoveryKey: string;
|
|
3797
|
+
recipientBindingKey: string;
|
|
3798
|
+
source: TelegramInputJournalSourceReference;
|
|
3799
|
+
handoffId: string;
|
|
3800
|
+
}
|
|
3801
|
+
|
|
3802
|
+
export function createTelegramInputCustodyHandoffAcceptanceRuntime<TContext>(deps: {
|
|
3803
|
+
resolveBinding(recoveryKey: string): {
|
|
3804
|
+
recoveryKey: string;
|
|
3805
|
+
recipientBindingKey: string;
|
|
3806
|
+
recipientOwner: TelegramUpdateJournalQueueOwnerIdentity;
|
|
3807
|
+
journal: Pick<TelegramInputJournalStore, "acceptInputHandoff">;
|
|
3808
|
+
signalWorker(ctx: TContext): void;
|
|
3809
|
+
} | undefined;
|
|
3810
|
+
}): { accept(input: TelegramInputCustodyHandoffAcceptanceInput, ctx: TContext): {
|
|
3811
|
+
sourceRecoveryKey: string;
|
|
3812
|
+
source: { updateId: number; owner: { acquisitionId: string; handoffId: string } };
|
|
3813
|
+
duplicate: boolean;
|
|
3814
|
+
} } {
|
|
3815
|
+
return {
|
|
3816
|
+
accept(input, ctx) {
|
|
3817
|
+
const binding = deps.resolveBinding(input.sourceRecoveryKey);
|
|
3818
|
+
if (!binding || binding.recoveryKey !== input.sourceRecoveryKey ||
|
|
3819
|
+
binding.recipientBindingKey !== input.recipientBindingKey ||
|
|
3820
|
+
input.source.journalBindingKey !== input.sourceRecoveryKey) throw new Error(
|
|
3821
|
+
"Telegram input custody handoff binding is unavailable or changed.",
|
|
3822
|
+
);
|
|
3823
|
+
const accepted = binding.journal.acceptInputHandoff({ source: input.source,
|
|
3824
|
+
recipientOwner: binding.recipientOwner, handoffId: input.handoffId });
|
|
3825
|
+
const acceptedHandoffId = accepted.receipt.owner.handoffId;
|
|
3826
|
+
if (!acceptedHandoffId || acceptedHandoffId !== input.handoffId) throw new Error(
|
|
3827
|
+
"Telegram input custody handoff acceptance returned mismatched authority.",
|
|
3828
|
+
);
|
|
3829
|
+
binding.signalWorker(ctx);
|
|
3830
|
+
return { sourceRecoveryKey: binding.recoveryKey,
|
|
3831
|
+
source: { updateId: accepted.receipt.updateId,
|
|
3832
|
+
owner: { acquisitionId: accepted.receipt.owner.acquisitionId,
|
|
3833
|
+
handoffId: acceptedHandoffId } }, duplicate: accepted.duplicate };
|
|
3834
|
+
},
|
|
3835
|
+
};
|
|
3836
|
+
}
|
|
3837
|
+
|
|
3838
|
+
export function createTelegramInputCustodyForwardReferenceResolver(deps: {
|
|
3839
|
+
recoveryKey: string;
|
|
3840
|
+
recipientBindingKey: string;
|
|
3841
|
+
recipientOwner: TelegramUpdateJournalQueueOwnerIdentity;
|
|
3842
|
+
journal: Pick<TelegramInputJournalStore, "read">;
|
|
3843
|
+
}): (input: { sourceUpdateId: number; recipientBindingKey: string }) => {
|
|
3844
|
+
sourceRecoveryKey: string;
|
|
3845
|
+
source: { updateId: number; owner: {
|
|
3846
|
+
acquisitionId: string; handoffId: string;
|
|
3847
|
+
} };
|
|
3848
|
+
} | undefined {
|
|
3849
|
+
return input => {
|
|
3850
|
+
if (input.recipientBindingKey !== deps.recipientBindingKey) return undefined;
|
|
3851
|
+
const entry = deps.journal.read().entries.find(
|
|
3852
|
+
candidate => candidate.updateId === input.sourceUpdateId);
|
|
3853
|
+
const claim = entry?.inputClaim;
|
|
3854
|
+
if (!entry || entry.state !== "pending" || entry.preApprovalExcluded !== false ||
|
|
3855
|
+
!claim || claim.phase !== "ready" || claim.handoff ||
|
|
3856
|
+
claim.recipientBindingKey !== deps.recipientBindingKey ||
|
|
3857
|
+
!claim.owner.handoffId ||
|
|
3858
|
+
claim.owner.sessionGeneration !== deps.recipientOwner.sessionGeneration ||
|
|
3859
|
+
!isTelegramUpdateJournalQueueOwnerProcess(claim.owner, deps.recipientOwner)) return undefined;
|
|
3860
|
+
return { sourceRecoveryKey: deps.recoveryKey, source: { updateId: entry.updateId,
|
|
3861
|
+
owner: { acquisitionId: claim.owner.acquisitionId,
|
|
3862
|
+
handoffId: claim.owner.handoffId } } };
|
|
3863
|
+
};
|
|
3864
|
+
}
|
|
3865
|
+
|
|
3866
|
+
export interface TelegramCustodiedSourceReferenceWakeInput {
|
|
3867
|
+
deliveryId: string;
|
|
3868
|
+
sourceUpdateId: number;
|
|
3869
|
+
recipientBindingKey: string;
|
|
3870
|
+
sourceRecoveryKey: string;
|
|
3871
|
+
sourceClaim: { acquisitionId: string; handoffId: string };
|
|
3872
|
+
}
|
|
3873
|
+
|
|
3874
|
+
export function createTelegramInputCustodySourceReferenceWakeRuntime<TContext>(deps: {
|
|
3875
|
+
resolveBinding(recoveryKey: string): {
|
|
3876
|
+
recoveryKey: string;
|
|
3877
|
+
recipientBindingKey: string;
|
|
3878
|
+
recipientOwner: TelegramUpdateJournalQueueOwnerIdentity;
|
|
3879
|
+
journal: Pick<TelegramInputJournalStore, "read">;
|
|
3880
|
+
signalWorker(ctx: TContext): void;
|
|
3881
|
+
} | undefined;
|
|
3882
|
+
}): { wakeSource(input: TelegramCustodiedSourceReferenceWakeInput, ctx: TContext): void } {
|
|
3883
|
+
return {
|
|
3884
|
+
wakeSource(input, ctx) {
|
|
3885
|
+
const binding = deps.resolveBinding(input.sourceRecoveryKey);
|
|
3886
|
+
if (!binding || binding.recoveryKey !== input.sourceRecoveryKey ||
|
|
3887
|
+
binding.recipientBindingKey !== input.recipientBindingKey) throw new Error(
|
|
3888
|
+
"Telegram follower source-reference binding is unavailable or changed.",
|
|
3889
|
+
);
|
|
3890
|
+
const entry = binding.journal.read().entries.find(
|
|
3891
|
+
candidate => candidate.updateId === input.sourceUpdateId);
|
|
3892
|
+
const claim = entry?.inputClaim;
|
|
3893
|
+
if (!entry || entry.state !== "pending" || entry.preApprovalExcluded !== false ||
|
|
3894
|
+
!claim || claim.phase !== "ready" || claim.handoff ||
|
|
3895
|
+
claim.recipientBindingKey !== input.recipientBindingKey ||
|
|
3896
|
+
claim.owner.acquisitionId !== input.sourceClaim.acquisitionId ||
|
|
3897
|
+
claim.owner.handoffId !== input.sourceClaim.handoffId ||
|
|
3898
|
+
claim.owner.sessionGeneration !== binding.recipientOwner.sessionGeneration ||
|
|
3899
|
+
!isTelegramUpdateJournalQueueOwnerProcess(claim.owner, binding.recipientOwner)) throw new Error(
|
|
3900
|
+
"Telegram follower source-reference claim is unavailable or changed.",
|
|
3901
|
+
);
|
|
3902
|
+
binding.signalWorker(ctx);
|
|
3903
|
+
},
|
|
3904
|
+
};
|
|
3905
|
+
}
|
|
3906
|
+
|
|
3907
|
+
export interface TelegramInputCustodyBusBindingRuntime<TContext> {
|
|
3908
|
+
acceptHandoff(input: TelegramInputCustodyHandoffAcceptanceInput, ctx: TContext): {
|
|
3909
|
+
sourceRecoveryKey: string; source: { updateId: number; owner: {
|
|
3910
|
+
acquisitionId: string; handoffId: string } }; duplicate: boolean };
|
|
3911
|
+
wakeSource(input: TelegramCustodiedSourceReferenceWakeInput, ctx: TContext): void;
|
|
3912
|
+
resolveForwardReference(input: { sourceUpdateId: number; recipientBindingKey: string }): {
|
|
3913
|
+
sourceRecoveryKey: string; source: { updateId: number; owner: {
|
|
3914
|
+
acquisitionId: string; handoffId: string } } } | undefined;
|
|
3915
|
+
}
|
|
3916
|
+
|
|
3917
|
+
export function createTelegramInputCustodyBusBindingRuntime<TContext>(deps: {
|
|
3918
|
+
getForwardRecoveryKey(): string | undefined;
|
|
3919
|
+
resolveBinding(recoveryKey: string): {
|
|
3920
|
+
recoveryKey: string;
|
|
3921
|
+
recipientBindingKey: string;
|
|
3922
|
+
recipientOwner: TelegramUpdateJournalQueueOwnerIdentity;
|
|
3923
|
+
journal: Pick<TelegramInputJournalStore,
|
|
3924
|
+
"read" | "acceptInputHandoff">;
|
|
3925
|
+
signalWorker(ctx: TContext): void;
|
|
3926
|
+
} | undefined;
|
|
3927
|
+
}): TelegramInputCustodyBusBindingRuntime<TContext> {
|
|
3928
|
+
const acceptance = createTelegramInputCustodyHandoffAcceptanceRuntime<TContext>({
|
|
3929
|
+
resolveBinding: deps.resolveBinding });
|
|
3930
|
+
const wake = createTelegramInputCustodySourceReferenceWakeRuntime<TContext>({
|
|
3931
|
+
resolveBinding: deps.resolveBinding });
|
|
3932
|
+
return {
|
|
3933
|
+
acceptHandoff: acceptance.accept,
|
|
3934
|
+
wakeSource: wake.wakeSource,
|
|
3935
|
+
resolveForwardReference(input: {
|
|
3936
|
+
sourceUpdateId: number; recipientBindingKey: string;
|
|
3937
|
+
}) {
|
|
3938
|
+
const recoveryKey = deps.getForwardRecoveryKey();
|
|
3939
|
+
if (!recoveryKey) return undefined;
|
|
3940
|
+
const binding = deps.resolveBinding(recoveryKey);
|
|
3941
|
+
if (!binding || binding.recoveryKey !== recoveryKey) return undefined;
|
|
3942
|
+
return createTelegramInputCustodyForwardReferenceResolver({
|
|
3943
|
+
recoveryKey: binding.recoveryKey,
|
|
3944
|
+
recipientBindingKey: binding.recipientBindingKey,
|
|
3945
|
+
recipientOwner: binding.recipientOwner,
|
|
3946
|
+
journal: binding.journal,
|
|
3947
|
+
})(input);
|
|
3948
|
+
},
|
|
3949
|
+
};
|
|
3950
|
+
}
|
|
3951
|
+
|
|
3952
|
+
export type TelegramInputCustodyActivationBlocker =
|
|
3953
|
+
| "disabled"
|
|
3954
|
+
| "source-unready"
|
|
3955
|
+
| "legacy-writers-present"
|
|
3956
|
+
| "migration-incomplete"
|
|
3957
|
+
| "peer-capability-mismatch";
|
|
3958
|
+
|
|
3959
|
+
export function evaluateTelegramInputCustodyActivationReadiness(input: {
|
|
3960
|
+
requested: boolean;
|
|
3961
|
+
sourceStatus: "absent" | "v3" | "legacy" | "unsupported" | "ambiguous";
|
|
3962
|
+
legacyWritersExcluded: boolean;
|
|
3963
|
+
historicalMigrationComplete: boolean;
|
|
3964
|
+
peerReadiness: readonly ("ready" | "legacy" | "unknown")[];
|
|
3965
|
+
}): { enabled: true } | { enabled: false; blocker: TelegramInputCustodyActivationBlocker } {
|
|
3966
|
+
if (!input.requested) return { enabled: false, blocker: "disabled" };
|
|
3967
|
+
if (!input.legacyWritersExcluded) return { enabled: false, blocker: "legacy-writers-present" };
|
|
3968
|
+
if (!input.historicalMigrationComplete) return { enabled: false, blocker: "migration-incomplete" };
|
|
3969
|
+
if (input.sourceStatus !== "absent" && input.sourceStatus !== "v3")
|
|
3970
|
+
return { enabled: false, blocker: "source-unready" };
|
|
3971
|
+
if (input.peerReadiness.some(readiness => readiness !== "ready"))
|
|
3972
|
+
return { enabled: false, blocker: "peer-capability-mismatch" };
|
|
3973
|
+
return { enabled: true };
|
|
3974
|
+
}
|
|
3975
|
+
|
|
3976
|
+
export interface TelegramInputCustodyReadinessEvidenceSnapshot {
|
|
3977
|
+
version: 1;
|
|
3978
|
+
revision: number;
|
|
3979
|
+
writerExclusion?: TelegramInputCustodyWriterExclusionEvidence;
|
|
3980
|
+
migration?: TelegramInputCustodyMigrationEvidence;
|
|
3981
|
+
startupExclusion?: TelegramInputCustodyStartupExclusionAuthority;
|
|
3982
|
+
migrationCompletion?: TelegramInputCustodyMigrationCompletionAuthority;
|
|
3983
|
+
}
|
|
3984
|
+
|
|
3985
|
+
export function createTelegramInputCustodyReadinessEvidenceStore(deps: {
|
|
3986
|
+
readRetained(): string | undefined;
|
|
3987
|
+
publishRetained(serialized: string): void;
|
|
3988
|
+
withSerialization<T>(operation: () => T): T;
|
|
3989
|
+
authorizePublication(kind: "writer-exclusion" | "migration" | "startup-exclusion" |
|
|
3990
|
+
"migration-completion", evidence: TelegramInputCustodyWriterExclusionEvidence |
|
|
3991
|
+
TelegramInputCustodyMigrationEvidence | TelegramInputCustodyStartupExclusionAuthority |
|
|
3992
|
+
TelegramInputCustodyMigrationCompletionAuthority): boolean;
|
|
3993
|
+
}) {
|
|
3994
|
+
const decode = (serialized: string | undefined): TelegramInputCustodyReadinessEvidenceSnapshot => {
|
|
3995
|
+
if (serialized === undefined) return { version: 1, revision: 0 };
|
|
3996
|
+
if (serialized.length > 16_384) throw new Error("Telegram custody readiness evidence exceeds capacity.");
|
|
3997
|
+
let value: unknown;
|
|
3998
|
+
try { value = JSON.parse(serialized); } catch { throw new Error("Telegram custody readiness evidence is malformed."); }
|
|
3999
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
4000
|
+
throw new Error("Telegram custody readiness evidence is malformed.");
|
|
4001
|
+
const snapshot = value as Record<string, unknown>;
|
|
4002
|
+
if (snapshot.version !== 1 || !Number.isSafeInteger(snapshot.revision) ||
|
|
4003
|
+
(snapshot.revision as number) < 0 || Object.keys(snapshot).some(
|
|
4004
|
+
key => !["version", "revision", "writerExclusion", "migration", "startupExclusion",
|
|
4005
|
+
"migrationCompletion"].includes(key)))
|
|
4006
|
+
throw new Error("Telegram custody readiness evidence is malformed.");
|
|
4007
|
+
const validate = (evidence: unknown, kind: "writer" | "migration") => {
|
|
4008
|
+
if (evidence === undefined) return undefined;
|
|
4009
|
+
if (!evidence || typeof evidence !== "object" || Array.isArray(evidence))
|
|
4010
|
+
throw new Error("Telegram custody readiness evidence is malformed.");
|
|
4011
|
+
const record = evidence as Record<string, unknown>;
|
|
4012
|
+
const statuses = kind === "writer" ? ["excluded", "present", "unknown"]
|
|
4013
|
+
: ["complete", "incomplete", "unknown"];
|
|
4014
|
+
const writerKeys = ["startupAuthorityId", "closureOperationId", "writerInventorySha256"];
|
|
4015
|
+
const migrationKeys = ["migrationAuthorityId", "startupAuthorityId", "closureOperationId",
|
|
4016
|
+
"migrationInventorySha256", "resultingSourceFamily"];
|
|
4017
|
+
const linkedKeys = kind === "writer" ? writerKeys : migrationKeys;
|
|
4018
|
+
const suppliedLinkedKeys = linkedKeys.filter(key => record[key] !== undefined);
|
|
4019
|
+
const linkedMalformed = suppliedLinkedKeys.length === linkedKeys.length && (
|
|
4020
|
+
typeof record.startupAuthorityId !== "string" || !record.startupAuthorityId ||
|
|
4021
|
+
typeof record.closureOperationId !== "string" || !record.closureOperationId ||
|
|
4022
|
+
(kind === "writer"
|
|
4023
|
+
? typeof record.writerInventorySha256 !== "string" ||
|
|
4024
|
+
!/^[a-f0-9]{64}$/u.test(record.writerInventorySha256)
|
|
4025
|
+
: typeof record.migrationAuthorityId !== "string" || !record.migrationAuthorityId ||
|
|
4026
|
+
typeof record.migrationInventorySha256 !== "string" ||
|
|
4027
|
+
!/^[a-f0-9]{64}$/u.test(record.migrationInventorySha256) ||
|
|
4028
|
+
(record.resultingSourceFamily !== "absent" && record.resultingSourceFamily !== "v3")));
|
|
4029
|
+
if (record.version !== 1 || typeof record.profileKey !== "string" || !record.profileKey ||
|
|
4030
|
+
typeof record.recoveryKey !== "string" || !record.recoveryKey ||
|
|
4031
|
+
!statuses.includes(record.status as string) || Object.keys(record).some(
|
|
4032
|
+
key => !["version", "profileKey", "recoveryKey", "status", ...linkedKeys].includes(key)) ||
|
|
4033
|
+
(suppliedLinkedKeys.length !== 0 && suppliedLinkedKeys.length !== linkedKeys.length) ||
|
|
4034
|
+
linkedMalformed) throw new Error("Telegram custody readiness evidence is malformed.");
|
|
4035
|
+
return { version: 1 as const, profileKey: record.profileKey,
|
|
4036
|
+
recoveryKey: record.recoveryKey, status: record.status,
|
|
4037
|
+
...(suppliedLinkedKeys.length === linkedKeys.length ? kind === "writer" ? {
|
|
4038
|
+
startupAuthorityId: record.startupAuthorityId,
|
|
4039
|
+
closureOperationId: record.closureOperationId,
|
|
4040
|
+
writerInventorySha256: record.writerInventorySha256,
|
|
4041
|
+
} : {
|
|
4042
|
+
migrationAuthorityId: record.migrationAuthorityId,
|
|
4043
|
+
startupAuthorityId: record.startupAuthorityId,
|
|
4044
|
+
closureOperationId: record.closureOperationId,
|
|
4045
|
+
migrationInventorySha256: record.migrationInventorySha256,
|
|
4046
|
+
resultingSourceFamily: record.resultingSourceFamily,
|
|
4047
|
+
} : {}) };
|
|
4048
|
+
};
|
|
4049
|
+
const writerExclusion = validate(snapshot.writerExclusion, "writer") as
|
|
4050
|
+
TelegramInputCustodyWriterExclusionEvidence | undefined;
|
|
4051
|
+
const migration = validate(snapshot.migration, "migration") as
|
|
4052
|
+
TelegramInputCustodyMigrationEvidence | undefined;
|
|
4053
|
+
let startupExclusion: TelegramInputCustodyStartupExclusionAuthority | undefined;
|
|
4054
|
+
if (snapshot.startupExclusion !== undefined) {
|
|
4055
|
+
const candidate = snapshot.startupExclusion as Record<string, unknown>;
|
|
4056
|
+
if (!candidate || typeof candidate.profileKey !== "string" ||
|
|
4057
|
+
typeof candidate.recoveryKey !== "string")
|
|
4058
|
+
throw new Error("Telegram custody readiness evidence is malformed.");
|
|
4059
|
+
startupExclusion = normalizeTelegramInputCustodyStartupExclusionAuthority(candidate,
|
|
4060
|
+
{ profileKey: candidate.profileKey, recoveryKey: candidate.recoveryKey });
|
|
4061
|
+
if (!startupExclusion) throw new Error("Telegram custody readiness evidence is malformed.");
|
|
4062
|
+
}
|
|
4063
|
+
let migrationCompletion: TelegramInputCustodyMigrationCompletionAuthority | undefined;
|
|
4064
|
+
if (snapshot.migrationCompletion !== undefined) {
|
|
4065
|
+
const candidate = snapshot.migrationCompletion as Record<string, unknown>;
|
|
4066
|
+
if (!candidate || typeof candidate.profileKey !== "string" ||
|
|
4067
|
+
typeof candidate.recoveryKey !== "string")
|
|
4068
|
+
throw new Error("Telegram custody readiness evidence is malformed.");
|
|
4069
|
+
migrationCompletion = normalizeTelegramInputCustodyMigrationCompletionAuthority(candidate,
|
|
4070
|
+
{ profileKey: candidate.profileKey, recoveryKey: candidate.recoveryKey });
|
|
4071
|
+
if (!migrationCompletion)
|
|
4072
|
+
throw new Error("Telegram custody readiness evidence is malformed.");
|
|
4073
|
+
}
|
|
4074
|
+
const identities: TelegramInputCustodyActivationEvidenceIdentity[] = [];
|
|
4075
|
+
if (writerExclusion) identities.push(writerExclusion);
|
|
4076
|
+
if (migration) identities.push(migration);
|
|
4077
|
+
if (startupExclusion) identities.push(startupExclusion);
|
|
4078
|
+
if (migrationCompletion) identities.push(migrationCompletion);
|
|
4079
|
+
if (identities.some(evidence => evidence.profileKey !== identities[0]?.profileKey ||
|
|
4080
|
+
evidence.recoveryKey !== identities[0]?.recoveryKey)) throw new Error(
|
|
4081
|
+
"Telegram custody readiness evidence has conflicting identities.",
|
|
4082
|
+
);
|
|
4083
|
+
if (writerExclusion?.startupAuthorityId && startupExclusion &&
|
|
4084
|
+
(writerExclusion.startupAuthorityId !== startupExclusion.authorityId ||
|
|
4085
|
+
writerExclusion.closureOperationId !== startupExclusion.closureOperationId ||
|
|
4086
|
+
writerExclusion.writerInventorySha256 !== startupExclusion.writerInventorySha256))
|
|
4087
|
+
throw new Error("Telegram custody readiness evidence has conflicting authorities.");
|
|
4088
|
+
if (migration?.migrationAuthorityId && migrationCompletion &&
|
|
4089
|
+
(migration.migrationAuthorityId !== migrationCompletion.authorityId ||
|
|
4090
|
+
migration.startupAuthorityId !== migrationCompletion.startupAuthorityId ||
|
|
4091
|
+
migration.closureOperationId !== migrationCompletion.closureOperationId ||
|
|
4092
|
+
migration.migrationInventorySha256 !== migrationCompletion.migrationInventorySha256 ||
|
|
4093
|
+
migration.resultingSourceFamily !== migrationCompletion.resultingSourceFamily))
|
|
4094
|
+
throw new Error("Telegram custody readiness evidence has conflicting migration authorities.");
|
|
4095
|
+
if (migrationCompletion && startupExclusion &&
|
|
4096
|
+
(migrationCompletion.startupAuthorityId !== startupExclusion.authorityId ||
|
|
4097
|
+
migrationCompletion.closureOperationId !== startupExclusion.closureOperationId))
|
|
4098
|
+
throw new Error("Telegram custody readiness evidence has conflicting cutover authorities.");
|
|
4099
|
+
return { version: 1, revision: snapshot.revision as number,
|
|
4100
|
+
...(writerExclusion ? { writerExclusion } : {}),
|
|
4101
|
+
...(migration ? { migration } : {}),
|
|
4102
|
+
...(startupExclusion ? { startupExclusion } : {}),
|
|
4103
|
+
...(migrationCompletion ? { migrationCompletion } : {}) };
|
|
4104
|
+
};
|
|
4105
|
+
const read = () => decode(deps.readRetained());
|
|
4106
|
+
const publish = (input: { expectedRevision: number } & (
|
|
4107
|
+
| { kind: "writer-exclusion"; evidence: TelegramInputCustodyWriterExclusionEvidence }
|
|
4108
|
+
| { kind: "migration"; evidence: TelegramInputCustodyMigrationEvidence }
|
|
4109
|
+
| { kind: "startup-exclusion"; evidence: TelegramInputCustodyStartupExclusionAuthority }
|
|
4110
|
+
| { kind: "migration-completion"; evidence: TelegramInputCustodyMigrationCompletionAuthority }
|
|
4111
|
+
)) => deps.withSerialization(() => {
|
|
4112
|
+
const current = read();
|
|
4113
|
+
if (current.revision !== input.expectedRevision) throw new Error(
|
|
4114
|
+
"Telegram custody readiness evidence revision changed.",
|
|
4115
|
+
);
|
|
4116
|
+
const next = { ...current, revision: current.revision + 1,
|
|
4117
|
+
...(input.kind === "writer-exclusion" ? { writerExclusion: input.evidence } :
|
|
4118
|
+
input.kind === "migration" ? { migration: input.evidence } :
|
|
4119
|
+
input.kind === "startup-exclusion" ? { startupExclusion: input.evidence } :
|
|
4120
|
+
{ migrationCompletion: input.evidence }) };
|
|
4121
|
+
const serialized = `${JSON.stringify(next)}\n`;
|
|
4122
|
+
const validated = decode(serialized);
|
|
4123
|
+
const normalizedEvidence = input.kind === "writer-exclusion"
|
|
4124
|
+
? validated.writerExclusion : input.kind === "migration"
|
|
4125
|
+
? validated.migration : input.kind === "startup-exclusion"
|
|
4126
|
+
? validated.startupExclusion : validated.migrationCompletion;
|
|
4127
|
+
if (!normalizedEvidence || !deps.authorizePublication(input.kind, normalizedEvidence))
|
|
4128
|
+
throw new Error("Telegram custody readiness evidence publication is unauthorized.");
|
|
4129
|
+
deps.publishRetained(serialized);
|
|
4130
|
+
return validated;
|
|
4131
|
+
});
|
|
4132
|
+
return { read, publish };
|
|
4133
|
+
}
|
|
4134
|
+
|
|
4135
|
+
export interface TelegramInputCustodyActivationEvidenceIdentity {
|
|
4136
|
+
profileKey: string;
|
|
4137
|
+
recoveryKey: string;
|
|
4138
|
+
}
|
|
4139
|
+
|
|
4140
|
+
export interface TelegramInputCustodyStartupExclusionAuthority
|
|
4141
|
+
extends TelegramInputCustodyActivationEvidenceIdentity {
|
|
4142
|
+
version: 1;
|
|
4143
|
+
status: "enforced" | "revoked";
|
|
4144
|
+
authorityId: string;
|
|
4145
|
+
closureOperationId: string;
|
|
4146
|
+
writerInventorySha256: string;
|
|
4147
|
+
allowedWriterProtocol: "custody-v3";
|
|
4148
|
+
authorizedAtMs: number;
|
|
4149
|
+
}
|
|
4150
|
+
|
|
4151
|
+
export function normalizeTelegramInputCustodyStartupExclusionAuthority(
|
|
4152
|
+
value: unknown,
|
|
4153
|
+
expected: TelegramInputCustodyActivationEvidenceIdentity,
|
|
4154
|
+
): TelegramInputCustodyStartupExclusionAuthority | undefined {
|
|
4155
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
4156
|
+
const record = value as Record<string, unknown>;
|
|
4157
|
+
const keys = ["version", "status", "authorityId", "closureOperationId", "profileKey",
|
|
4158
|
+
"recoveryKey", "writerInventorySha256", "allowedWriterProtocol", "authorizedAtMs"];
|
|
4159
|
+
if (Object.keys(record).some(key => !keys.includes(key)) || record.version !== 1 ||
|
|
4160
|
+
(record.status !== "enforced" && record.status !== "revoked") ||
|
|
4161
|
+
record.profileKey !== expected.profileKey || record.recoveryKey !== expected.recoveryKey ||
|
|
4162
|
+
typeof record.authorityId !== "string" || !record.authorityId || record.authorityId.length > 512 ||
|
|
4163
|
+
typeof record.closureOperationId !== "string" || !record.closureOperationId ||
|
|
4164
|
+
record.closureOperationId.length > 512 ||
|
|
4165
|
+
typeof record.writerInventorySha256 !== "string" ||
|
|
4166
|
+
!/^[a-f0-9]{64}$/u.test(record.writerInventorySha256) ||
|
|
4167
|
+
record.allowedWriterProtocol !== "custody-v3" ||
|
|
4168
|
+
!Number.isSafeInteger(record.authorizedAtMs) || (record.authorizedAtMs as number) < 0)
|
|
4169
|
+
return undefined;
|
|
4170
|
+
return { version: 1, status: record.status, authorityId: record.authorityId,
|
|
4171
|
+
closureOperationId: record.closureOperationId, profileKey: expected.profileKey,
|
|
4172
|
+
recoveryKey: expected.recoveryKey, writerInventorySha256: record.writerInventorySha256,
|
|
4173
|
+
allowedWriterProtocol: "custody-v3", authorizedAtMs: record.authorizedAtMs as number };
|
|
4174
|
+
}
|
|
4175
|
+
|
|
4176
|
+
export interface TelegramInputCustodyWriterExclusionEvidence
|
|
4177
|
+
extends TelegramInputCustodyActivationEvidenceIdentity {
|
|
4178
|
+
version: 1;
|
|
4179
|
+
status: "excluded" | "present" | "unknown";
|
|
4180
|
+
startupAuthorityId?: string;
|
|
4181
|
+
closureOperationId?: string;
|
|
4182
|
+
writerInventorySha256?: string;
|
|
4183
|
+
}
|
|
4184
|
+
|
|
4185
|
+
export interface TelegramInputCustodyWriterInventory
|
|
4186
|
+
extends TelegramInputCustodyActivationEvidenceIdentity {
|
|
4187
|
+
complete: boolean;
|
|
4188
|
+
writerInventorySha256: string;
|
|
4189
|
+
writers: readonly { processId: number; processBirthId: string }[];
|
|
4190
|
+
}
|
|
4191
|
+
|
|
4192
|
+
export function evaluateTelegramInputCustodyWriterExclusionEvidence(input: {
|
|
4193
|
+
expected: TelegramInputCustodyActivationEvidenceIdentity;
|
|
4194
|
+
inventory: TelegramInputCustodyWriterInventory;
|
|
4195
|
+
startupAuthority: TelegramInputCustodyStartupExclusionAuthority | undefined;
|
|
4196
|
+
getProcessLiveness(writer: {
|
|
4197
|
+
processId: number; processBirthId: string;
|
|
4198
|
+
}): TelegramProcessLiveness;
|
|
4199
|
+
}): TelegramInputCustodyWriterExclusionEvidence {
|
|
4200
|
+
const identity = { ...input.expected };
|
|
4201
|
+
const authority = input.startupAuthority;
|
|
4202
|
+
if (!input.inventory.complete ||
|
|
4203
|
+
input.inventory.profileKey !== input.expected.profileKey ||
|
|
4204
|
+
input.inventory.recoveryKey !== input.expected.recoveryKey ||
|
|
4205
|
+
!/^[a-f0-9]{64}$/u.test(input.inventory.writerInventorySha256) ||
|
|
4206
|
+
!authority || authority.status !== "enforced" ||
|
|
4207
|
+
authority.profileKey !== input.expected.profileKey ||
|
|
4208
|
+
authority.recoveryKey !== input.expected.recoveryKey ||
|
|
4209
|
+
authority.writerInventorySha256 !== input.inventory.writerInventorySha256)
|
|
4210
|
+
return { version: 1, status: "unknown", ...identity };
|
|
4211
|
+
const authorityBinding = { startupAuthorityId: authority.authorityId,
|
|
4212
|
+
closureOperationId: authority.closureOperationId,
|
|
4213
|
+
writerInventorySha256: authority.writerInventorySha256 };
|
|
4214
|
+
let unknown = false;
|
|
4215
|
+
for (const writer of input.inventory.writers) {
|
|
4216
|
+
let liveness: TelegramProcessLiveness;
|
|
4217
|
+
try { liveness = input.getProcessLiveness(writer); } catch { liveness = "unverifiable"; }
|
|
4218
|
+
if (liveness === "alive") return { version: 1, status: "present", ...identity,
|
|
4219
|
+
...authorityBinding };
|
|
4220
|
+
if (liveness !== "dead") unknown = true;
|
|
4221
|
+
}
|
|
4222
|
+
return { version: 1, status: unknown ? "unknown" : "excluded", ...identity,
|
|
4223
|
+
...authorityBinding };
|
|
4224
|
+
}
|
|
4225
|
+
|
|
4226
|
+
export interface TelegramInputCustodyMigrationCompletionAuthority
|
|
4227
|
+
extends TelegramInputCustodyActivationEvidenceIdentity {
|
|
4228
|
+
version: 1;
|
|
4229
|
+
status: "authorized" | "revoked";
|
|
4230
|
+
authorityId: string;
|
|
4231
|
+
startupAuthorityId: string;
|
|
4232
|
+
closureOperationId: string;
|
|
4233
|
+
migrationInventorySha256: string;
|
|
4234
|
+
resultingSourceFamily: "absent" | "v3";
|
|
4235
|
+
authorizedAtMs: number;
|
|
4236
|
+
}
|
|
4237
|
+
|
|
4238
|
+
export function normalizeTelegramInputCustodyMigrationCompletionAuthority(
|
|
4239
|
+
value: unknown,
|
|
4240
|
+
expected: TelegramInputCustodyActivationEvidenceIdentity,
|
|
4241
|
+
): TelegramInputCustodyMigrationCompletionAuthority | undefined {
|
|
4242
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
4243
|
+
const record = value as Record<string, unknown>;
|
|
4244
|
+
const keys = ["version", "status", "authorityId", "startupAuthorityId",
|
|
4245
|
+
"closureOperationId", "profileKey", "recoveryKey", "migrationInventorySha256",
|
|
4246
|
+
"resultingSourceFamily", "authorizedAtMs"];
|
|
4247
|
+
if (Object.keys(record).some(key => !keys.includes(key)) || record.version !== 1 ||
|
|
4248
|
+
(record.status !== "authorized" && record.status !== "revoked") ||
|
|
4249
|
+
record.profileKey !== expected.profileKey || record.recoveryKey !== expected.recoveryKey ||
|
|
4250
|
+
typeof record.authorityId !== "string" || !record.authorityId || record.authorityId.length > 512 ||
|
|
4251
|
+
typeof record.startupAuthorityId !== "string" || !record.startupAuthorityId ||
|
|
4252
|
+
record.startupAuthorityId.length > 512 ||
|
|
4253
|
+
typeof record.closureOperationId !== "string" || !record.closureOperationId ||
|
|
4254
|
+
record.closureOperationId.length > 512 ||
|
|
4255
|
+
typeof record.migrationInventorySha256 !== "string" ||
|
|
4256
|
+
!/^[a-f0-9]{64}$/u.test(record.migrationInventorySha256) ||
|
|
4257
|
+
(record.resultingSourceFamily !== "absent" && record.resultingSourceFamily !== "v3") ||
|
|
4258
|
+
!Number.isSafeInteger(record.authorizedAtMs) || (record.authorizedAtMs as number) < 0)
|
|
4259
|
+
return undefined;
|
|
4260
|
+
return { version: 1, status: record.status, authorityId: record.authorityId,
|
|
4261
|
+
startupAuthorityId: record.startupAuthorityId,
|
|
4262
|
+
closureOperationId: record.closureOperationId, profileKey: expected.profileKey,
|
|
4263
|
+
recoveryKey: expected.recoveryKey,
|
|
4264
|
+
migrationInventorySha256: record.migrationInventorySha256,
|
|
4265
|
+
resultingSourceFamily: record.resultingSourceFamily,
|
|
4266
|
+
authorizedAtMs: record.authorizedAtMs as number };
|
|
4267
|
+
}
|
|
4268
|
+
|
|
4269
|
+
export interface TelegramInputCustodyMigrationEvidence
|
|
4270
|
+
extends TelegramInputCustodyActivationEvidenceIdentity {
|
|
4271
|
+
version: 1;
|
|
4272
|
+
status: "complete" | "incomplete" | "unknown";
|
|
4273
|
+
migrationAuthorityId?: string;
|
|
4274
|
+
startupAuthorityId?: string;
|
|
4275
|
+
closureOperationId?: string;
|
|
4276
|
+
migrationInventorySha256?: string;
|
|
4277
|
+
resultingSourceFamily?: "absent" | "v3";
|
|
4278
|
+
}
|
|
4279
|
+
|
|
4280
|
+
export type TelegramInputCustodyWriterCutoverResult<TMode> =
|
|
4281
|
+
| { kind: "blocked"; blocker: "startup-authority" | "writer-inventory";
|
|
4282
|
+
evidence?: TelegramInputCustodyWriterExclusionEvidence }
|
|
4283
|
+
| { kind: "completed"; mode: TMode; evidence: TelegramInputCustodyWriterExclusionEvidence;
|
|
4284
|
+
resumed: boolean };
|
|
4285
|
+
|
|
4286
|
+
export function executeTelegramInputCustodyWriterCutover<TClosure extends {
|
|
4287
|
+
operationId: string; profileKey: string; recoveryKey: string;
|
|
4288
|
+
}, TMode extends TelegramInputCustodyWriterProtocolModeEvidence>(input: {
|
|
4289
|
+
expected: TelegramInputCustodyActivationEvidenceIdentity;
|
|
4290
|
+
closure: TClosure;
|
|
4291
|
+
startupAuthority: TelegramInputCustodyStartupExclusionAuthority;
|
|
4292
|
+
inventory: TelegramInputCustodyWriterInventory;
|
|
4293
|
+
getProcessLiveness(writer: { processId: number; processBirthId: string }): TelegramProcessLiveness;
|
|
4294
|
+
installProtocolMode(closure: TClosure, authority: {
|
|
4295
|
+
startupAuthorityId: string; writerInventorySha256: string;
|
|
4296
|
+
}): { mode: TMode; resumed: boolean };
|
|
4297
|
+
evidenceStore: {
|
|
4298
|
+
read(): TelegramInputCustodyReadinessEvidenceSnapshot;
|
|
4299
|
+
publish(input: { expectedRevision: number; kind: "writer-exclusion";
|
|
4300
|
+
evidence: TelegramInputCustodyWriterExclusionEvidence }):
|
|
4301
|
+
TelegramInputCustodyReadinessEvidenceSnapshot;
|
|
4302
|
+
};
|
|
4303
|
+
}): TelegramInputCustodyWriterCutoverResult<TMode> {
|
|
4304
|
+
const matchesAuthority = (candidate: TelegramInputCustodyStartupExclusionAuthority | undefined) =>
|
|
4305
|
+
Boolean(candidate && candidate.status === "enforced" && candidate.version === 1 &&
|
|
4306
|
+
candidate.profileKey === input.expected.profileKey &&
|
|
4307
|
+
candidate.recoveryKey === input.expected.recoveryKey &&
|
|
4308
|
+
candidate.authorityId === input.startupAuthority.authorityId &&
|
|
4309
|
+
candidate.closureOperationId === input.closure.operationId &&
|
|
4310
|
+
candidate.writerInventorySha256 === input.startupAuthority.writerInventorySha256 &&
|
|
4311
|
+
candidate.allowedWriterProtocol === "custody-v3" &&
|
|
4312
|
+
candidate.authorizedAtMs === input.startupAuthority.authorizedAtMs);
|
|
4313
|
+
if (input.closure.profileKey !== input.expected.profileKey ||
|
|
4314
|
+
input.closure.recoveryKey !== input.expected.recoveryKey ||
|
|
4315
|
+
!matchesAuthority(input.evidenceStore.read().startupExclusion))
|
|
4316
|
+
return { kind: "blocked", blocker: "startup-authority" };
|
|
4317
|
+
const evidence = evaluateTelegramInputCustodyWriterExclusionEvidence({
|
|
4318
|
+
expected: input.expected, inventory: input.inventory,
|
|
4319
|
+
startupAuthority: input.startupAuthority, getProcessLiveness: input.getProcessLiveness });
|
|
4320
|
+
if (evidence.status !== "excluded")
|
|
4321
|
+
return { kind: "blocked", blocker: "writer-inventory", evidence };
|
|
4322
|
+
const installed = input.installProtocolMode(input.closure, {
|
|
4323
|
+
startupAuthorityId: input.startupAuthority.authorityId,
|
|
4324
|
+
writerInventorySha256: input.startupAuthority.writerInventorySha256,
|
|
4325
|
+
});
|
|
4326
|
+
if (installed.mode.protocol !== "custody-v3" ||
|
|
4327
|
+
installed.mode.profileKey !== input.expected.profileKey ||
|
|
4328
|
+
installed.mode.recoveryKey !== input.expected.recoveryKey ||
|
|
4329
|
+
installed.mode.startupAuthorityId !== evidence.startupAuthorityId ||
|
|
4330
|
+
installed.mode.closureOperationId !== evidence.closureOperationId ||
|
|
4331
|
+
installed.mode.writerInventorySha256 !== evidence.writerInventorySha256)
|
|
4332
|
+
throw new Error("Telegram custody writer protocol installation returned mismatched authority.");
|
|
4333
|
+
const current = input.evidenceStore.read();
|
|
4334
|
+
if (!matchesAuthority(current.startupExclusion))
|
|
4335
|
+
return { kind: "blocked", blocker: "startup-authority", evidence };
|
|
4336
|
+
const existing = current.writerExclusion;
|
|
4337
|
+
if (existing?.status === "excluded" && existing.profileKey === evidence.profileKey &&
|
|
4338
|
+
existing.recoveryKey === evidence.recoveryKey &&
|
|
4339
|
+
existing.startupAuthorityId === evidence.startupAuthorityId &&
|
|
4340
|
+
existing.closureOperationId === evidence.closureOperationId &&
|
|
4341
|
+
existing.writerInventorySha256 === evidence.writerInventorySha256)
|
|
4342
|
+
return { kind: "completed", mode: installed.mode, evidence: existing,
|
|
4343
|
+
resumed: installed.resumed };
|
|
4344
|
+
input.evidenceStore.publish({ expectedRevision: current.revision,
|
|
4345
|
+
kind: "writer-exclusion", evidence });
|
|
4346
|
+
return { kind: "completed", mode: installed.mode, evidence, resumed: false };
|
|
4347
|
+
}
|
|
4348
|
+
|
|
4349
|
+
function matchesTelegramInputCustodyActivationEvidence(
|
|
4350
|
+
evidence: TelegramInputCustodyActivationEvidenceIdentity | undefined,
|
|
4351
|
+
expected: TelegramInputCustodyActivationEvidenceIdentity,
|
|
4352
|
+
): boolean {
|
|
4353
|
+
return Boolean(evidence && evidence.profileKey === expected.profileKey &&
|
|
4354
|
+
evidence.recoveryKey === expected.recoveryKey);
|
|
4355
|
+
}
|
|
4356
|
+
|
|
4357
|
+
export type TelegramInputCustodyMigrationCompletionResult =
|
|
4358
|
+
| { kind: "blocked"; blocker: "migration-authority" | "source-drift" |
|
|
4359
|
+
"inventory-drift" }
|
|
4360
|
+
| { kind: "completed"; evidence: TelegramInputCustodyMigrationEvidence;
|
|
4361
|
+
resumed: boolean };
|
|
4362
|
+
|
|
4363
|
+
export function executeTelegramInputCustodyMigrationCompletion(input: {
|
|
4364
|
+
expected: TelegramInputCustodyActivationEvidenceIdentity;
|
|
4365
|
+
authority: TelegramInputCustodyMigrationCompletionAuthority;
|
|
4366
|
+
migrationInventorySha256: string;
|
|
4367
|
+
inspectSource(): "absent" | "v3" | "legacy" | "unsupported" | "ambiguous";
|
|
4368
|
+
evidenceStore: {
|
|
4369
|
+
read(): TelegramInputCustodyReadinessEvidenceSnapshot;
|
|
4370
|
+
publish(input: { expectedRevision: number; kind: "migration";
|
|
4371
|
+
evidence: TelegramInputCustodyMigrationEvidence }):
|
|
4372
|
+
TelegramInputCustodyReadinessEvidenceSnapshot;
|
|
4373
|
+
};
|
|
4374
|
+
}): TelegramInputCustodyMigrationCompletionResult {
|
|
4375
|
+
const matchesAuthority = (candidate: TelegramInputCustodyMigrationCompletionAuthority |
|
|
4376
|
+
undefined) => Boolean(candidate && candidate.version === 1 && candidate.status === "authorized" &&
|
|
4377
|
+
candidate.profileKey === input.expected.profileKey &&
|
|
4378
|
+
candidate.recoveryKey === input.expected.recoveryKey &&
|
|
4379
|
+
candidate.authorityId === input.authority.authorityId &&
|
|
4380
|
+
candidate.startupAuthorityId === input.authority.startupAuthorityId &&
|
|
4381
|
+
candidate.closureOperationId === input.authority.closureOperationId &&
|
|
4382
|
+
candidate.migrationInventorySha256 === input.authority.migrationInventorySha256 &&
|
|
4383
|
+
candidate.resultingSourceFamily === input.authority.resultingSourceFamily &&
|
|
4384
|
+
candidate.authorizedAtMs === input.authority.authorizedAtMs);
|
|
4385
|
+
const current = input.evidenceStore.read();
|
|
4386
|
+
if (!matchesAuthority(current.migrationCompletion))
|
|
4387
|
+
return { kind: "blocked", blocker: "migration-authority" };
|
|
4388
|
+
if (input.migrationInventorySha256 !== input.authority.migrationInventorySha256)
|
|
4389
|
+
return { kind: "blocked", blocker: "inventory-drift" };
|
|
4390
|
+
let source: ReturnType<typeof input.inspectSource>;
|
|
4391
|
+
try { source = input.inspectSource(); } catch { source = "ambiguous"; }
|
|
4392
|
+
if (source !== input.authority.resultingSourceFamily)
|
|
4393
|
+
return { kind: "blocked", blocker: "source-drift" };
|
|
4394
|
+
const evidence: TelegramInputCustodyMigrationEvidence = { version: 1,
|
|
4395
|
+
status: "complete", ...input.expected,
|
|
4396
|
+
migrationAuthorityId: input.authority.authorityId,
|
|
4397
|
+
startupAuthorityId: input.authority.startupAuthorityId,
|
|
4398
|
+
closureOperationId: input.authority.closureOperationId,
|
|
4399
|
+
migrationInventorySha256: input.authority.migrationInventorySha256,
|
|
4400
|
+
resultingSourceFamily: input.authority.resultingSourceFamily };
|
|
4401
|
+
const existing = current.migration;
|
|
4402
|
+
if (existing?.status === "complete" && existing.profileKey === evidence.profileKey &&
|
|
4403
|
+
existing.recoveryKey === evidence.recoveryKey &&
|
|
4404
|
+
existing.migrationAuthorityId === evidence.migrationAuthorityId &&
|
|
4405
|
+
existing.startupAuthorityId === evidence.startupAuthorityId &&
|
|
4406
|
+
existing.closureOperationId === evidence.closureOperationId &&
|
|
4407
|
+
existing.migrationInventorySha256 === evidence.migrationInventorySha256 &&
|
|
4408
|
+
existing.resultingSourceFamily === evidence.resultingSourceFamily)
|
|
4409
|
+
return { kind: "completed", evidence: existing, resumed: true };
|
|
4410
|
+
input.evidenceStore.publish({ expectedRevision: current.revision, kind: "migration", evidence });
|
|
4411
|
+
return { kind: "completed", evidence, resumed: false };
|
|
4412
|
+
}
|
|
4413
|
+
|
|
4414
|
+
export interface TelegramInputCustodyWriterProtocolModeEvidence
|
|
4415
|
+
extends TelegramInputCustodyActivationEvidenceIdentity {
|
|
4416
|
+
protocol: "custody-v3";
|
|
4417
|
+
startupAuthorityId: string;
|
|
4418
|
+
closureOperationId: string;
|
|
4419
|
+
writerInventorySha256: string;
|
|
4420
|
+
}
|
|
4421
|
+
|
|
4422
|
+
export function createTelegramInputCustodyProvenReadinessResolver(deps: {
|
|
4423
|
+
isRequested(): boolean;
|
|
4424
|
+
expectedIdentity(): TelegramInputCustodyActivationEvidenceIdentity;
|
|
4425
|
+
inspectSource(): "absent" | "v3" | "legacy" | "unsupported" | "ambiguous";
|
|
4426
|
+
readWriterExclusionEvidence(): TelegramInputCustodyWriterExclusionEvidence | undefined;
|
|
4427
|
+
readStartupExclusionAuthority(): TelegramInputCustodyStartupExclusionAuthority | undefined;
|
|
4428
|
+
readWriterProtocolMode(): TelegramInputCustodyWriterProtocolModeEvidence | undefined;
|
|
4429
|
+
readMigrationCompletionAuthority(): TelegramInputCustodyMigrationCompletionAuthority | undefined;
|
|
4430
|
+
readMigrationEvidence(): TelegramInputCustodyMigrationEvidence | undefined;
|
|
4431
|
+
listPeerReadiness(): readonly ("ready" | "legacy" | "unknown")[];
|
|
4432
|
+
}): () => ReturnType<typeof evaluateTelegramInputCustodyActivationReadiness> {
|
|
4433
|
+
return createTelegramInputCustodyActivationReadinessResolver({
|
|
4434
|
+
isRequested: deps.isRequested,
|
|
4435
|
+
inspectSource() {
|
|
4436
|
+
let source: "absent" | "v3" | "legacy" | "unsupported" | "ambiguous";
|
|
4437
|
+
try { source = deps.inspectSource(); } catch { return "ambiguous"; }
|
|
4438
|
+
const expected = deps.expectedIdentity();
|
|
4439
|
+
const authority = deps.readMigrationCompletionAuthority();
|
|
4440
|
+
return matchesTelegramInputCustodyActivationEvidence(authority, expected) &&
|
|
4441
|
+
authority?.status === "authorized" && authority.resultingSourceFamily === source
|
|
4442
|
+
? source : "ambiguous";
|
|
4443
|
+
},
|
|
4444
|
+
areLegacyWritersExcluded() {
|
|
4445
|
+
const expected = deps.expectedIdentity();
|
|
4446
|
+
const evidence = deps.readWriterExclusionEvidence();
|
|
4447
|
+
const authority = deps.readStartupExclusionAuthority();
|
|
4448
|
+
const mode = deps.readWriterProtocolMode();
|
|
4449
|
+
return matchesTelegramInputCustodyActivationEvidence(evidence, expected) &&
|
|
4450
|
+
matchesTelegramInputCustodyActivationEvidence(authority, expected) &&
|
|
4451
|
+
matchesTelegramInputCustodyActivationEvidence(mode, expected) &&
|
|
4452
|
+
evidence?.version === 1 && evidence.status === "excluded" &&
|
|
4453
|
+
authority?.version === 1 && authority.status === "enforced" &&
|
|
4454
|
+
mode?.protocol === "custody-v3" &&
|
|
4455
|
+
evidence.startupAuthorityId === authority.authorityId &&
|
|
4456
|
+
evidence.startupAuthorityId === mode.startupAuthorityId &&
|
|
4457
|
+
evidence.closureOperationId === authority.closureOperationId &&
|
|
4458
|
+
evidence.closureOperationId === mode.closureOperationId &&
|
|
4459
|
+
evidence.writerInventorySha256 === authority.writerInventorySha256 &&
|
|
4460
|
+
evidence.writerInventorySha256 === mode.writerInventorySha256;
|
|
4461
|
+
},
|
|
4462
|
+
isHistoricalMigrationComplete() {
|
|
4463
|
+
const expected = deps.expectedIdentity();
|
|
4464
|
+
const evidence = deps.readMigrationEvidence();
|
|
4465
|
+
const authority = deps.readMigrationCompletionAuthority();
|
|
4466
|
+
const startup = deps.readStartupExclusionAuthority();
|
|
4467
|
+
const mode = deps.readWriterProtocolMode();
|
|
4468
|
+
let source: "absent" | "v3" | "legacy" | "unsupported" | "ambiguous";
|
|
4469
|
+
try { source = deps.inspectSource(); } catch { source = "ambiguous"; }
|
|
4470
|
+
return matchesTelegramInputCustodyActivationEvidence(evidence, expected) &&
|
|
4471
|
+
matchesTelegramInputCustodyActivationEvidence(authority, expected) &&
|
|
4472
|
+
evidence?.version === 1 && evidence.status === "complete" &&
|
|
4473
|
+
authority?.version === 1 && authority.status === "authorized" &&
|
|
4474
|
+
authority.resultingSourceFamily === source &&
|
|
4475
|
+
evidence.migrationAuthorityId === authority.authorityId &&
|
|
4476
|
+
evidence.startupAuthorityId === authority.startupAuthorityId &&
|
|
4477
|
+
evidence.startupAuthorityId === startup?.authorityId &&
|
|
4478
|
+
evidence.startupAuthorityId === mode?.startupAuthorityId &&
|
|
4479
|
+
evidence.closureOperationId === authority.closureOperationId &&
|
|
4480
|
+
evidence.closureOperationId === mode?.closureOperationId &&
|
|
4481
|
+
evidence.migrationInventorySha256 === authority.migrationInventorySha256 &&
|
|
4482
|
+
evidence.resultingSourceFamily === authority.resultingSourceFamily;
|
|
4483
|
+
},
|
|
4484
|
+
listPeerReadiness: deps.listPeerReadiness,
|
|
4485
|
+
});
|
|
4486
|
+
}
|
|
4487
|
+
|
|
4488
|
+
export function createTelegramInputCustodyActivationReadinessResolver(deps: {
|
|
4489
|
+
isRequested(): boolean;
|
|
4490
|
+
inspectSource(): "absent" | "v3" | "legacy" | "unsupported" | "ambiguous";
|
|
4491
|
+
areLegacyWritersExcluded(): boolean;
|
|
4492
|
+
isHistoricalMigrationComplete(): boolean;
|
|
4493
|
+
listPeerReadiness(): readonly ("ready" | "legacy" | "unknown")[];
|
|
4494
|
+
}): () => ReturnType<typeof evaluateTelegramInputCustodyActivationReadiness> {
|
|
4495
|
+
return () => {
|
|
4496
|
+
const requested = deps.isRequested();
|
|
4497
|
+
if (!requested) return { enabled: false, blocker: "disabled" };
|
|
4498
|
+
let legacyWritersExcluded = false;
|
|
4499
|
+
try { legacyWritersExcluded = deps.areLegacyWritersExcluded(); } catch { /* blocked */ }
|
|
4500
|
+
if (!legacyWritersExcluded) return { enabled: false, blocker: "legacy-writers-present" };
|
|
4501
|
+
let historicalMigrationComplete = false;
|
|
4502
|
+
try { historicalMigrationComplete = deps.isHistoricalMigrationComplete(); } catch { /* blocked */ }
|
|
4503
|
+
if (!historicalMigrationComplete) return { enabled: false, blocker: "migration-incomplete" };
|
|
4504
|
+
let sourceStatus: "absent" | "v3" | "legacy" | "unsupported" | "ambiguous";
|
|
4505
|
+
try { sourceStatus = deps.inspectSource(); } catch { sourceStatus = "ambiguous"; }
|
|
4506
|
+
if (sourceStatus !== "absent" && sourceStatus !== "v3")
|
|
4507
|
+
return { enabled: false, blocker: "source-unready" };
|
|
4508
|
+
let peerReadiness: readonly ("ready" | "legacy" | "unknown")[];
|
|
4509
|
+
try { peerReadiness = deps.listPeerReadiness(); } catch { peerReadiness = ["unknown"]; }
|
|
4510
|
+
return evaluateTelegramInputCustodyActivationReadiness({ requested,
|
|
4511
|
+
sourceStatus, legacyWritersExcluded, historicalMigrationComplete, peerReadiness });
|
|
4512
|
+
};
|
|
4513
|
+
}
|
|
4514
|
+
|
|
4515
|
+
export function createTelegramInputCustodyLifecycleBindingResolver(deps: {
|
|
4516
|
+
isEnabled(): boolean;
|
|
4517
|
+
resolveInputJournal(): { runtimeKey: string; recoveryKey: string;
|
|
4518
|
+
journal: TelegramInputJournalStore } | undefined;
|
|
4519
|
+
getRecipientBindingKey(): string | undefined;
|
|
4520
|
+
}): () => TelegramUpdateAdmissionLifecycleJournalBinding | undefined {
|
|
4521
|
+
return () => {
|
|
4522
|
+
if (!deps.isEnabled()) return undefined;
|
|
4523
|
+
const source = deps.resolveInputJournal();
|
|
4524
|
+
const recipientBindingKey = deps.getRecipientBindingKey()?.trim();
|
|
4525
|
+
if (!source || !source.runtimeKey || !source.recoveryKey || !recipientBindingKey)
|
|
4526
|
+
return undefined;
|
|
4527
|
+
const port = createTelegramInputCustodyWorkerJournalPort(source.journal);
|
|
4528
|
+
return { runtimeKey: JSON.stringify({ source: source.runtimeKey, recipientBindingKey }),
|
|
4529
|
+
recoveryKey: source.recoveryKey,
|
|
4530
|
+
recipientBindingKey, journal: { ...port, appendBatch: source.journal.appendBatch,
|
|
4531
|
+
discardQueued: source.journal.discardQueued,
|
|
4532
|
+
recoverDeadQueueOwner: source.journal.recoverDeadQueueOwner,
|
|
4533
|
+
offerQueuedHandoff: source.journal.offerQueuedHandoff,
|
|
4534
|
+
acceptQueuedHandoff: source.journal.acceptQueuedHandoff,
|
|
4535
|
+
cancelQueuedHandoff: source.journal.cancelQueuedHandoff } };
|
|
4536
|
+
};
|
|
4537
|
+
}
|
|
4538
|
+
|
|
4539
|
+
export function createTelegramCustodiedExecutionSession(input: {
|
|
4540
|
+
journal: TelegramCustodyExecutionJournal;
|
|
4541
|
+
recipientBindingKey: string;
|
|
4542
|
+
}): {
|
|
4543
|
+
execute(update: TelegramJournaledUpdate,
|
|
4544
|
+
handler: (update: TelegramJournaledUpdate) => Promise<TelegramUpdateAdmissionOutcome>):
|
|
4545
|
+
Promise<TelegramCustodiedExecutionResult>;
|
|
4546
|
+
settle(updateId: number, outcome: TelegramUpdateAdmissionOutcome):
|
|
4547
|
+
Promise<TelegramCustodiedExecutionResult>;
|
|
4548
|
+
} {
|
|
4549
|
+
const receipts = new Map<number, TelegramInputJournalReceipt>();
|
|
4550
|
+
const settledQueues = new Map<number, Extract<TelegramCustodiedExecutionResult,
|
|
4551
|
+
{ status: "queued" }>>();
|
|
4552
|
+
const settle = async (updateId: number, outcome: TelegramUpdateAdmissionOutcome):
|
|
4553
|
+
Promise<TelegramCustodiedExecutionResult> => {
|
|
4554
|
+
const receipt = receipts.get(updateId);
|
|
4555
|
+
if (!receipt) {
|
|
4556
|
+
const duplicate = settledQueues.get(updateId);
|
|
4557
|
+
if (duplicate && outcome.kind === "queued" &&
|
|
4558
|
+
duplicate.queueReceipt.queueKind === outcome.queueKind &&
|
|
4559
|
+
duplicate.queueReceipt.receiptId === outcome.receiptId &&
|
|
4560
|
+
duplicate.queueReceipt.sourceUpdateIds.length === outcome.sourceUpdateIds.length &&
|
|
4561
|
+
duplicate.queueReceipt.sourceUpdateIds.every((id, index) => id === outcome.sourceUpdateIds[index]))
|
|
4562
|
+
{ settledQueues.delete(updateId); return duplicate; }
|
|
4563
|
+
throw new TelegramUpdateAdmissionOutcomeError(
|
|
4564
|
+
`Telegram deferred custody ${updateId} has no exact running receipt.`,
|
|
4565
|
+
);
|
|
4566
|
+
}
|
|
4567
|
+
if (outcome.kind === "deferred") return { status: "deferred", receipt };
|
|
4568
|
+
if (outcome.kind === "queued") {
|
|
4569
|
+
if (!outcome.sourceUpdateIds.includes(updateId))
|
|
4570
|
+
throw new TelegramUpdateAdmissionOutcomeError(
|
|
4571
|
+
"Telegram grouped queue custody omitted the settling input.",
|
|
4572
|
+
);
|
|
4573
|
+
const grouped = outcome.sourceUpdateIds.map(sourceUpdateId => receipts.get(sourceUpdateId));
|
|
4574
|
+
if (grouped.some(candidate => !candidate)) throw new TelegramUpdateAdmissionOutcomeError(
|
|
4575
|
+
"Telegram grouped queue custody omitted an exact running source receipt.",
|
|
4576
|
+
);
|
|
4577
|
+
const queued = input.journal.queueInputs({ queueKind: outcome.queueKind,
|
|
4578
|
+
receiptId: outcome.receiptId, receipts: grouped as TelegramInputJournalReceipt[] });
|
|
4579
|
+
const result = { status: "queued" as const, queueReceipt: queued.queueReceipt };
|
|
4580
|
+
for (const sourceUpdateId of outcome.sourceUpdateIds) {
|
|
4581
|
+
receipts.delete(sourceUpdateId);
|
|
4582
|
+
if (sourceUpdateId !== updateId) settledQueues.set(sourceUpdateId, result);
|
|
4583
|
+
}
|
|
4584
|
+
return result;
|
|
4585
|
+
}
|
|
4586
|
+
input.journal.completeInput(receipt);
|
|
4587
|
+
receipts.delete(updateId);
|
|
4588
|
+
return { status: "completed" };
|
|
4589
|
+
};
|
|
4590
|
+
return {
|
|
4591
|
+
settle,
|
|
4592
|
+
async execute(update, handler) {
|
|
4593
|
+
const acquired = input.journal.acquireInput({ updateId: update.update_id,
|
|
4594
|
+
recipientBindingKey: input.recipientBindingKey, executionUpdate: update });
|
|
4595
|
+
const started = input.journal.startInput(acquired.receipt);
|
|
4596
|
+
if (!started.started) return { status: "outcome-unknown", receipt: acquired.receipt };
|
|
4597
|
+
receipts.set(update.update_id, acquired.receipt);
|
|
4598
|
+
let outcome: TelegramUpdateAdmissionOutcome;
|
|
4599
|
+
try { outcome = await handler(started.update); }
|
|
4600
|
+
catch (error) { receipts.delete(update.update_id); throw error; }
|
|
4601
|
+
return settle(update.update_id, outcome);
|
|
4602
|
+
},
|
|
4603
|
+
};
|
|
4604
|
+
}
|
|
4605
|
+
|
|
4606
|
+
export async function executeTelegramCustodiedInput(input: {
|
|
4607
|
+
journal: TelegramCustodyExecutionJournal;
|
|
4608
|
+
update: TelegramJournaledUpdate;
|
|
4609
|
+
recipientBindingKey: string;
|
|
4610
|
+
execute(update: TelegramJournaledUpdate): Promise<TelegramUpdateAdmissionOutcome>;
|
|
4611
|
+
}): Promise<TelegramCustodiedExecutionResult> {
|
|
4612
|
+
return createTelegramCustodiedExecutionSession(input).execute(input.update, input.execute);
|
|
4613
|
+
}
|
|
4614
|
+
|
|
3490
4615
|
/**
|
|
3491
4616
|
* Compose the stable public handler registry with source-bound semantic
|
|
3492
4617
|
* admission. Production polling switches to this only with the journal worker.
|
|
@@ -3570,6 +4695,34 @@ export function createTelegramUpdateAdmissionHandle<
|
|
|
3570
4695
|
};
|
|
3571
4696
|
}
|
|
3572
4697
|
|
|
4698
|
+
export function createTelegramCustodiedUpdateAdmissionHandle<
|
|
4699
|
+
TUpdate extends TelegramJournaledUpdate & TelegramUpdateFlow,
|
|
4700
|
+
TContext,
|
|
4701
|
+
>(deps: Omit<TelegramUpdateAdmissionHandleDeps<TUpdate, TContext>,
|
|
4702
|
+
"onLateOutcome" | "onLateOutcomeError"> & {
|
|
4703
|
+
journal: TelegramCustodyExecutionJournal;
|
|
4704
|
+
recipientBindingKey: string;
|
|
4705
|
+
onLateOutcomeError(error: unknown, updateId: number): void;
|
|
4706
|
+
onCustodiedLateSettlement?: (result: TelegramCustodiedExecutionResult,
|
|
4707
|
+
details: { updateId: number; signal: AbortSignal }) => void;
|
|
4708
|
+
}): (update: TUpdate, ctx: TContext, signal: AbortSignal) =>
|
|
4709
|
+
Promise<TelegramCustodiedExecutionResult> {
|
|
4710
|
+
const session = createTelegramCustodiedExecutionSession({ journal: deps.journal,
|
|
4711
|
+
recipientBindingKey: deps.recipientBindingKey });
|
|
4712
|
+
const admission = createTelegramUpdateAdmissionHandle({
|
|
4713
|
+
defaultHandle: deps.defaultHandle,
|
|
4714
|
+
registry: deps.registry,
|
|
4715
|
+
async onLateOutcome(outcome, details) {
|
|
4716
|
+
const result = await session.settle(details.updateId, outcome);
|
|
4717
|
+
deps.onCustodiedLateSettlement?.(result,
|
|
4718
|
+
{ updateId: details.updateId, signal: details.signal });
|
|
4719
|
+
},
|
|
4720
|
+
onLateOutcomeError: deps.onLateOutcomeError,
|
|
4721
|
+
});
|
|
4722
|
+
return (update, ctx, signal) => session.execute(update,
|
|
4723
|
+
started => admission(started as TUpdate, ctx, signal));
|
|
4724
|
+
}
|
|
4725
|
+
|
|
3573
4726
|
export interface TelegramQueueAdmissionItemLike {
|
|
3574
4727
|
admissionReceipts?: readonly TelegramQueueAdmissionReceiptLike[];
|
|
3575
4728
|
}
|
|
@@ -3668,11 +4821,13 @@ export function createTelegramQueueAdmissionSettlementRuntime<TContext>(
|
|
|
3668
4821
|
export interface TelegramUpdateAdmissionLifecycleJournalBinding {
|
|
3669
4822
|
runtimeKey: string;
|
|
3670
4823
|
recoveryKey: string;
|
|
4824
|
+
recipientBindingKey?: string;
|
|
3671
4825
|
journal: TelegramUpdateWorkerJournalPort & {
|
|
4826
|
+
inputCustody?: TelegramCustodyExecutionJournal;
|
|
3672
4827
|
appendBatch: (
|
|
3673
4828
|
updates: readonly TelegramJournaledUpdate[],
|
|
3674
4829
|
acceptedThroughUpdateId?: number,
|
|
3675
|
-
) =>
|
|
4830
|
+
) => Pick<TelegramUpdateJournalAppendResult, "nonExcludedUpdateIds">;
|
|
3676
4831
|
applyOperatorDisposition?: (
|
|
3677
4832
|
input: TelegramUpdateJournalOperatorDispositionInput,
|
|
3678
4833
|
) => TelegramUpdateJournalOperatorDispositionResult;
|
|
@@ -4298,6 +5453,9 @@ export interface TelegramUpdateAdmissionLifecycleRuntimeDeps<TContext> {
|
|
|
4298
5453
|
journal: TelegramUpdateWorkerJournalPort,
|
|
4299
5454
|
binding: TelegramUpdateAdmissionLifecycleJournalBinding,
|
|
4300
5455
|
) => TelegramUpdateWorkerRuntime<TContext>;
|
|
5456
|
+
acquireSourceReference?: (
|
|
5457
|
+
binding: TelegramUpdateAdmissionLifecycleJournalBinding,
|
|
5458
|
+
) => () => void;
|
|
4301
5459
|
recordRuntimeEvent?: TelegramUpdateWorkerRuntimeDeps<TContext>["recordRuntimeEvent"];
|
|
4302
5460
|
}
|
|
4303
5461
|
|
|
@@ -4309,7 +5467,7 @@ export interface TelegramUpdateAdmissionLifecycleRuntime<TContext>
|
|
|
4309
5467
|
appendBatch: (
|
|
4310
5468
|
updates: readonly TelegramJournaledUpdate[],
|
|
4311
5469
|
acceptedThroughUpdateId?: number,
|
|
4312
|
-
) =>
|
|
5470
|
+
) => Pick<TelegramUpdateJournalAppendResult, "nonExcludedUpdateIds">;
|
|
4313
5471
|
discardQueueReceipt: (input: {
|
|
4314
5472
|
queueKind: "prompt" | "control";
|
|
4315
5473
|
receiptId: string;
|
|
@@ -4395,11 +5553,13 @@ export interface TelegramUpdateAdmissionRuntimeBinding<TContext> {
|
|
|
4395
5553
|
bind: (input: {
|
|
4396
5554
|
leader: TelegramUpdateAdmissionLifecycleRuntime<TContext>;
|
|
4397
5555
|
follower: TelegramUpdateAdmissionLifecycleRuntime<TContext>;
|
|
5556
|
+
inputCustodyBus?: TelegramInputCustodyBusBindingRuntime<TContext>;
|
|
4398
5557
|
}) => void;
|
|
4399
5558
|
getLeader: () => TelegramUpdateAdmissionLifecycleRuntime<TContext> | undefined;
|
|
4400
5559
|
getFollower: () => TelegramUpdateAdmissionLifecycleRuntime<TContext> | undefined;
|
|
4401
5560
|
getActive: () => TelegramUpdateAdmissionLifecycleRuntime<TContext> | undefined;
|
|
4402
5561
|
getSettlement: () => TelegramQueueAdmissionSettlementRuntime<TContext> | undefined;
|
|
5562
|
+
getInputCustodyBus: () => TelegramInputCustodyBusBindingRuntime<TContext> | undefined;
|
|
4403
5563
|
getLifecycleForJournalBinding: (
|
|
4404
5564
|
journalBindingKey: string,
|
|
4405
5565
|
) => TelegramUpdateAdmissionLifecycleRuntime<TContext> | undefined;
|
|
@@ -4415,6 +5575,7 @@ export function createTelegramUpdateAdmissionRuntimeBinding<TContext>(deps: {
|
|
|
4415
5575
|
let leader: TelegramUpdateAdmissionLifecycleRuntime<TContext> | undefined;
|
|
4416
5576
|
let follower: TelegramUpdateAdmissionLifecycleRuntime<TContext> | undefined;
|
|
4417
5577
|
let settlement: TelegramQueueAdmissionSettlementRuntime<TContext> | undefined;
|
|
5578
|
+
let inputCustodyBus: TelegramInputCustodyBusBindingRuntime<TContext> | undefined;
|
|
4418
5579
|
return {
|
|
4419
5580
|
bind(input) {
|
|
4420
5581
|
leader = input.leader;
|
|
@@ -4423,11 +5584,13 @@ export function createTelegramUpdateAdmissionRuntimeBinding<TContext>(deps: {
|
|
|
4423
5584
|
leader,
|
|
4424
5585
|
follower,
|
|
4425
5586
|
]);
|
|
5587
|
+
inputCustodyBus = input.inputCustodyBus;
|
|
4426
5588
|
},
|
|
4427
5589
|
getLeader: () => leader,
|
|
4428
5590
|
getFollower: () => follower,
|
|
4429
5591
|
getActive: () => (deps.isFollowerRegistered() ? follower : leader),
|
|
4430
5592
|
getSettlement: () => settlement,
|
|
5593
|
+
getInputCustodyBus: () => inputCustodyBus,
|
|
4431
5594
|
getLifecycleForJournalBinding(journalBindingKey) {
|
|
4432
5595
|
if (follower?.ownsJournalBinding(journalBindingKey)) return follower;
|
|
4433
5596
|
return leader?.ownsJournalBinding(journalBindingKey) ? leader : undefined;
|
|
@@ -4482,9 +5645,16 @@ export function createTelegramUpdateAdmissionLifecycleRuntime<TContext>(
|
|
|
4482
5645
|
let journalBindingKey: string | undefined;
|
|
4483
5646
|
let operation: Promise<void> = Promise.resolve();
|
|
4484
5647
|
let foreignQueueOwnerLiveness: TelegramProcessLiveness | undefined;
|
|
5648
|
+
let releaseSourceReference: (() => void) | undefined;
|
|
4485
5649
|
|
|
4486
5650
|
const stopCurrent = async (forget: boolean): Promise<void> => {
|
|
4487
5651
|
await worker?.stop();
|
|
5652
|
+
const release = releaseSourceReference;
|
|
5653
|
+
releaseSourceReference = undefined;
|
|
5654
|
+
try { release?.(); } catch (error) {
|
|
5655
|
+
deps.recordRuntimeEvent?.("inbound-worker", error,
|
|
5656
|
+
{ phase: "source-reference-release" });
|
|
5657
|
+
}
|
|
4488
5658
|
if (!forget) return;
|
|
4489
5659
|
journal = undefined;
|
|
4490
5660
|
worker = undefined;
|
|
@@ -4493,20 +5663,35 @@ export function createTelegramUpdateAdmissionLifecycleRuntime<TContext>(
|
|
|
4493
5663
|
activeRuntimeKey = undefined;
|
|
4494
5664
|
foreignQueueOwnerLiveness = undefined;
|
|
4495
5665
|
};
|
|
4496
|
-
const bind = async (ctx: TContext): Promise<void> => {
|
|
4497
|
-
|
|
5666
|
+
const bind = async (ctx: TContext, replace = false): Promise<void> => {
|
|
5667
|
+
let binding = deps.resolveBinding();
|
|
4498
5668
|
if (!binding) {
|
|
4499
5669
|
await stopCurrent(true);
|
|
4500
5670
|
return;
|
|
4501
5671
|
}
|
|
4502
|
-
if (!worker || activeRuntimeKey !== binding.runtimeKey) {
|
|
5672
|
+
if (replace || !worker || activeRuntimeKey !== binding.runtimeKey) {
|
|
5673
|
+
const { runtimeKey, recoveryKey } = binding;
|
|
4503
5674
|
await stopCurrent(true);
|
|
4504
|
-
|
|
4505
|
-
|
|
4506
|
-
|
|
5675
|
+
binding = deps.resolveBinding();
|
|
5676
|
+
if (!binding || binding.runtimeKey !== runtimeKey ||
|
|
5677
|
+
binding.recoveryKey !== recoveryKey) {
|
|
5678
|
+
throw new Error("Telegram update source binding changed during startup.");
|
|
5679
|
+
}
|
|
5680
|
+
const release = deps.acquireSourceReference?.(binding);
|
|
5681
|
+
try {
|
|
5682
|
+
journal = binding.journal;
|
|
5683
|
+
worker = deps.createWorker(binding.journal, binding);
|
|
5684
|
+
settlement = createTelegramQueueAdmissionSettlementRuntime(worker);
|
|
5685
|
+
releaseSourceReference = release;
|
|
5686
|
+
} catch (error) {
|
|
5687
|
+
release?.();
|
|
5688
|
+
throw error;
|
|
5689
|
+
}
|
|
4507
5690
|
journalBindingKey = binding.recoveryKey;
|
|
4508
5691
|
activeRuntimeKey = binding.runtimeKey;
|
|
4509
5692
|
}
|
|
5693
|
+
if (!releaseSourceReference && deps.acquireSourceReference)
|
|
5694
|
+
releaseSourceReference = deps.acquireSourceReference(binding);
|
|
4510
5695
|
if (binding.journal.applyOperatorDisposition) {
|
|
4511
5696
|
for (const entry of binding.journal.read().entries) {
|
|
4512
5697
|
if (entry.state !== "failed" || !entry.terminalFailureId) continue;
|
|
@@ -4547,6 +5732,7 @@ export function createTelegramUpdateAdmissionLifecycleRuntime<TContext>(
|
|
|
4547
5732
|
!entry.queueKind ||
|
|
4548
5733
|
!entry.queueReceiptId ||
|
|
4549
5734
|
!entry.queueOwner ||
|
|
5735
|
+
entry.queueHandoff ||
|
|
4550
5736
|
isTelegramUpdateJournalQueueOwnerProcess(
|
|
4551
5737
|
entry.queueOwner,
|
|
4552
5738
|
recoveryOwner,
|
|
@@ -4606,8 +5792,8 @@ export function createTelegramUpdateAdmissionLifecycleRuntime<TContext>(
|
|
|
4606
5792
|
onSessionShutdown: () => runExclusive(() => stopCurrent(false)),
|
|
4607
5793
|
onTransportChanged: (ctx) =>
|
|
4608
5794
|
runExclusive(async () => {
|
|
4609
|
-
await
|
|
4610
|
-
|
|
5795
|
+
if (ctx !== undefined) await bind(ctx, true);
|
|
5796
|
+
else await stopCurrent(true);
|
|
4611
5797
|
}),
|
|
4612
5798
|
appendBatch(updates, acceptedThroughUpdateId) {
|
|
4613
5799
|
if (!journal || !worker) {
|
|
@@ -4651,7 +5837,9 @@ export function createTelegramUpdateAdmissionLifecycleRuntime<TContext>(
|
|
|
4651
5837
|
"Telegram update journal queue handoff acceptance is not available.",
|
|
4652
5838
|
);
|
|
4653
5839
|
}
|
|
4654
|
-
|
|
5840
|
+
const result = journal.acceptQueuedHandoff(input);
|
|
5841
|
+
worker.signal();
|
|
5842
|
+
return result;
|
|
4655
5843
|
},
|
|
4656
5844
|
cancelQueueReceiptHandoff(input) {
|
|
4657
5845
|
if (!journal || !worker || !journal.cancelQueuedHandoff) {
|
|
@@ -4768,10 +5956,14 @@ export interface TelegramUpdateAdmissionLifecycleAssemblyDeps<
|
|
|
4768
5956
|
TContext,
|
|
4769
5957
|
> {
|
|
4770
5958
|
runtimeBinding: TelegramUpdateAdmissionRuntimeBinding<TContext>;
|
|
5959
|
+
inputCustodyBus?: TelegramInputCustodyBusBindingRuntime<TContext>;
|
|
5960
|
+
acquireSourceReference?: (role: "leader" | "follower",
|
|
5961
|
+
binding: TelegramUpdateAdmissionLifecycleJournalBinding) => () => void;
|
|
4771
5962
|
worker: Omit<
|
|
4772
5963
|
TelegramUpdateAdmissionWorkerRuntimeDeps<TUpdate, TContext>,
|
|
4773
5964
|
| "journal"
|
|
4774
5965
|
| "getJournalBindingKey"
|
|
5966
|
+
| "getRecipientBindingKey"
|
|
4775
5967
|
| "hasAuthority"
|
|
4776
5968
|
| "prepareUpdateForExecution"
|
|
4777
5969
|
>;
|
|
@@ -4795,7 +5987,9 @@ export interface TelegramUpdateAdmissionLifecycleAssemblyDeps<
|
|
|
4795
5987
|
export type TelegramUpdateAdmissionWorkerRuntimeDeps<
|
|
4796
5988
|
TUpdate extends TelegramJournaledUpdate & TelegramUpdateFlow,
|
|
4797
5989
|
TContext,
|
|
4798
|
-
> = Omit<TelegramUpdateWorkerRuntimeDeps<TContext>,
|
|
5990
|
+
> = Omit<TelegramUpdateWorkerRuntimeDeps<TContext>,
|
|
5991
|
+
"executeUpdate" | "executeCustodiedUpdate"> & {
|
|
5992
|
+
inputCustody?: TelegramCustodyExecutionJournal;
|
|
4799
5993
|
defaultHandle: (
|
|
4800
5994
|
update: TUpdate,
|
|
4801
5995
|
ctx: TContext,
|
|
@@ -4830,8 +6024,27 @@ export function createTelegramUpdateAdmissionWorkerRuntime<
|
|
|
4830
6024
|
});
|
|
4831
6025
|
},
|
|
4832
6026
|
});
|
|
6027
|
+
const custodyBindingKey = deps.inputCustody ? deps.getRecipientBindingKey?.() : undefined;
|
|
6028
|
+
if (deps.inputCustody && !custodyBindingKey) throw new Error(
|
|
6029
|
+
"Telegram custodied admission worker requires one recipient binding key.",
|
|
6030
|
+
);
|
|
6031
|
+
const executeCustodiedUpdate = deps.inputCustody && custodyBindingKey
|
|
6032
|
+
? createTelegramCustodiedUpdateAdmissionHandle<TUpdate, TContext>({
|
|
6033
|
+
journal: deps.inputCustody, recipientBindingKey: custodyBindingKey,
|
|
6034
|
+
defaultHandle: deps.defaultHandle, registry: deps.registry,
|
|
6035
|
+
onLateOutcomeError(error, updateId) {
|
|
6036
|
+
deps.recordRuntimeEvent?.("inbound-worker", error,
|
|
6037
|
+
{ phase: "late-custodied-admission-handler", updateId });
|
|
6038
|
+
},
|
|
6039
|
+
onCustodiedLateSettlement(result, details) {
|
|
6040
|
+
worker?.settleCustodied({ updateId: details.updateId, result, signal: details.signal });
|
|
6041
|
+
},
|
|
6042
|
+
}) : undefined;
|
|
4833
6043
|
worker = createTelegramUpdateWorkerRuntime({
|
|
4834
6044
|
...deps,
|
|
6045
|
+
...(executeCustodiedUpdate ? { executeCustodiedUpdate: async (update: TelegramJournaledUpdate,
|
|
6046
|
+
ctx: TContext, signal: AbortSignal) => executeCustodiedUpdate(
|
|
6047
|
+
deps.prepareUpdateForExecution?.(update as TUpdate) ?? update as TUpdate, ctx, signal) } : {}),
|
|
4835
6048
|
async executeUpdate(update, ctx, signal) {
|
|
4836
6049
|
const typedUpdate = update as TUpdate;
|
|
4837
6050
|
try {
|
|
@@ -4860,12 +6073,16 @@ export function createTelegramUpdateAdmissionLifecycleAssembly<
|
|
|
4860
6073
|
): TelegramUpdateAdmissionLifecycleAssembly<TContext> {
|
|
4861
6074
|
const leader = createTelegramUpdateAdmissionLifecycleRuntime({
|
|
4862
6075
|
resolveBinding: deps.leader.resolveBinding,
|
|
6076
|
+
...(deps.acquireSourceReference ? { acquireSourceReference: binding =>
|
|
6077
|
+
deps.acquireSourceReference!("leader", binding) } : {}),
|
|
4863
6078
|
getQueueOwnerIdentity: deps.worker.getQueueOwnerIdentity,
|
|
4864
6079
|
createWorker(journal, binding) {
|
|
4865
6080
|
return createTelegramUpdateAdmissionWorkerRuntime({
|
|
4866
6081
|
...deps.worker,
|
|
4867
6082
|
journal,
|
|
6083
|
+
...(binding.journal.inputCustody ? { inputCustody: binding.journal.inputCustody } : {}),
|
|
4868
6084
|
getJournalBindingKey: () => binding.recoveryKey,
|
|
6085
|
+
getRecipientBindingKey: () => binding.recipientBindingKey,
|
|
4869
6086
|
hasAuthority: deps.leader.hasAuthority,
|
|
4870
6087
|
});
|
|
4871
6088
|
},
|
|
@@ -4873,6 +6090,8 @@ export function createTelegramUpdateAdmissionLifecycleAssembly<
|
|
|
4873
6090
|
});
|
|
4874
6091
|
const follower = createTelegramUpdateAdmissionLifecycleRuntime({
|
|
4875
6092
|
getQueueOwnerIdentity: deps.worker.getQueueOwnerIdentity,
|
|
6093
|
+
...(deps.acquireSourceReference ? { acquireSourceReference: binding =>
|
|
6094
|
+
deps.acquireSourceReference!("follower", binding) } : {}),
|
|
4876
6095
|
resolveBinding() {
|
|
4877
6096
|
const generation = deps.follower.getGeneration();
|
|
4878
6097
|
if (!deps.follower.isRegistered() || !generation) return undefined;
|
|
@@ -4893,14 +6112,17 @@ export function createTelegramUpdateAdmissionLifecycleAssembly<
|
|
|
4893
6112
|
return createTelegramUpdateAdmissionWorkerRuntime({
|
|
4894
6113
|
...deps.worker,
|
|
4895
6114
|
journal,
|
|
6115
|
+
...(binding.journal.inputCustody ? { inputCustody: binding.journal.inputCustody } : {}),
|
|
4896
6116
|
getJournalBindingKey: () => binding.recoveryKey,
|
|
6117
|
+
getRecipientBindingKey: () => binding.recipientBindingKey,
|
|
4897
6118
|
hasAuthority: () => binding.hasAuthority?.() ?? false,
|
|
4898
6119
|
prepareUpdateForExecution: deps.follower.prepareUpdateForExecution,
|
|
4899
6120
|
});
|
|
4900
6121
|
},
|
|
4901
6122
|
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
4902
6123
|
});
|
|
4903
|
-
deps.runtimeBinding.bind({ leader, follower
|
|
6124
|
+
deps.runtimeBinding.bind({ leader, follower,
|
|
6125
|
+
...(deps.inputCustodyBus ? { inputCustodyBus: deps.inputCustodyBus } : {}) });
|
|
4904
6126
|
return { leader, follower };
|
|
4905
6127
|
}
|
|
4906
6128
|
|