@llblab/pi-telegram 0.52.2 → 0.53.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/AGENTS.md +9 -7
- package/BACKLOG.md +1 -18
- package/CHANGELOG.md +15 -0
- package/README.md +7 -11
- package/api/commands.ts +3 -0
- package/dist/api/commands.d.ts +1 -1
- package/dist/lib/activity-verbosity.d.ts +1 -5
- package/dist/lib/activity-verbosity.js +15 -42
- package/dist/lib/activity.d.ts +9 -0
- package/dist/lib/activity.js +39 -10
- package/dist/lib/agent-messages.js +7 -3
- package/dist/lib/bindings.d.ts +43 -5
- package/dist/lib/bindings.js +163 -20
- package/dist/lib/bus-api.d.ts +12 -0
- package/dist/lib/bus-api.js +182 -79
- package/dist/lib/bus-follower.d.ts +111 -31
- package/dist/lib/bus-follower.js +1324 -359
- package/dist/lib/bus-leader.d.ts +41 -91
- package/dist/lib/bus-leader.js +969 -637
- package/dist/lib/bus-transport.js +16 -9
- package/dist/lib/bus.d.ts +215 -18
- package/dist/lib/bus.js +1048 -327
- package/dist/lib/channel-posts.d.ts +5 -0
- package/dist/lib/channel-posts.js +434 -261
- package/dist/lib/command-templates.d.ts +26 -5
- package/dist/lib/command-templates.js +92 -176
- package/dist/lib/commands.d.ts +168 -82
- package/dist/lib/commands.js +1409 -291
- package/dist/lib/config.d.ts +1 -8
- package/dist/lib/config.js +34 -76
- package/dist/lib/delivery.d.ts +2 -2
- package/dist/lib/delivery.js +16 -16
- package/dist/lib/extension.js +639 -171
- package/dist/lib/generative-app-worker.mjs +19 -7
- package/dist/lib/generative-apps.d.ts +0 -5
- package/dist/lib/generative-apps.js +61 -49
- package/dist/lib/inbound.d.ts +5 -0
- package/dist/lib/inbound.js +35 -59
- package/dist/lib/journal.d.ts +33 -3
- package/dist/lib/journal.js +2063 -782
- package/dist/lib/lifecycle.d.ts +2 -6
- package/dist/lib/lifecycle.js +75 -58
- package/dist/lib/locks.d.ts +29 -3
- package/dist/lib/locks.js +289 -108
- package/dist/lib/logging.js +50 -14
- package/dist/lib/media.d.ts +22 -1
- package/dist/lib/media.js +46 -28
- package/dist/lib/menu-model.d.ts +19 -5
- package/dist/lib/menu-model.js +53 -11
- package/dist/lib/menu-queue.d.ts +5 -4
- package/dist/lib/menu-queue.js +45 -63
- package/dist/lib/menu-settings.d.ts +5 -4
- package/dist/lib/menu-settings.js +114 -78
- package/dist/lib/menu-status.d.ts +1 -1
- package/dist/lib/menu-status.js +8 -25
- package/dist/lib/menu-thinking.d.ts +6 -1
- package/dist/lib/menu-thinking.js +14 -21
- package/dist/lib/menu.d.ts +8 -61
- package/dist/lib/menu.js +92 -164
- package/dist/lib/model.d.ts +1 -1
- package/dist/lib/model.js +2 -16
- package/dist/lib/outbound-attachments.d.ts +6 -27
- package/dist/lib/outbound-attachments.js +45 -38
- package/dist/lib/outbound-buttons.js +22 -16
- package/dist/lib/outbound-markup.d.ts +0 -2
- package/dist/lib/outbound-markup.js +51 -64
- package/dist/lib/outbound-voice.js +6 -3
- package/dist/lib/outbound.d.ts +6 -6
- package/dist/lib/outbound.js +65 -51
- package/dist/lib/ownership.js +6 -2
- package/dist/lib/paths.d.ts +5 -0
- package/dist/lib/paths.js +37 -21
- package/dist/lib/pi.d.ts +2 -0
- package/dist/lib/pi.js +8 -1
- package/dist/lib/polling.d.ts +21 -37
- package/dist/lib/polling.js +59 -50
- package/dist/lib/preview.js +39 -19
- package/dist/lib/process-identity.d.ts +24 -0
- package/dist/lib/process-identity.js +133 -0
- package/dist/lib/prompts.js +2 -7
- package/dist/lib/queue.d.ts +14 -16
- package/dist/lib/queue.js +252 -127
- package/dist/lib/recovery.d.ts +2 -83
- package/dist/lib/recovery.js +20 -274
- package/dist/lib/replies.d.ts +16 -24
- package/dist/lib/replies.js +99 -116
- package/dist/lib/routing.d.ts +120 -7
- package/dist/lib/routing.js +4667 -1152
- package/dist/lib/runtime.d.ts +1 -11
- package/dist/lib/sections.d.ts +4 -29
- package/dist/lib/sections.js +7 -20
- package/dist/lib/skills.js +4 -3
- package/dist/lib/status.js +86 -57
- package/dist/lib/sync.d.ts +1 -3
- package/dist/lib/sync.js +67 -43
- package/dist/lib/target.d.ts +4 -0
- package/dist/lib/target.js +18 -0
- package/dist/lib/telegram-api.d.ts +18 -13
- package/dist/lib/telegram-api.js +148 -53
- package/dist/lib/text-groups.js +4 -39
- package/dist/lib/thread-cleanup-manager.d.ts +6 -27
- package/dist/lib/thread-cleanup-manager.js +369 -186
- package/dist/lib/thread-display.d.ts +1 -2
- package/dist/lib/thread-display.js +58 -38
- package/dist/lib/thread-naming.d.ts +28 -8
- package/dist/lib/thread-naming.js +175 -66
- package/dist/lib/thread-reconciler.d.ts +42 -16
- package/dist/lib/thread-reconciler.js +90 -50
- package/dist/lib/threads.d.ts +82 -20
- package/dist/lib/threads.js +2247 -855
- package/dist/lib/time-injection.js +1 -2
- package/dist/lib/turns.js +1 -19
- package/dist/lib/updates.d.ts +169 -80
- package/dist/lib/updates.js +2418 -641
- package/dist/lib/voice.d.ts +7 -22
- package/dist/lib/voice.js +7 -22
- package/dist/lib/wire.js +1 -1
- package/dist/lib/workspace-admission.d.ts +3 -1
- package/dist/lib/workspace-admission.js +425 -211
- package/dist/lib/workspace-identity.js +8 -13
- package/dist/lib/workspace-retirement.d.ts +1 -1
- package/dist/lib/workspace-retirement.js +328 -140
- package/dist/lib/workspace-slots.js +8 -4
- package/dist/package.json +1 -1
- package/docs/architecture.md +31 -29
- package/docs/inbound.md +1 -1
- package/docs/multi-instance-bus.md +15 -3
- package/docs/outbound.md +1 -1
- package/docs/public-api.md +29 -1
- package/docs/ui-style.md +25 -8
- package/lib/activity-verbosity.ts +59 -82
- package/lib/activity.ts +146 -102
- package/lib/agent-messages.ts +12 -5
- package/lib/bindings.ts +389 -82
- package/lib/bus-api.ts +263 -102
- package/lib/bus-follower.ts +2440 -601
- package/lib/bus-leader.ts +2209 -1290
- package/lib/bus-transport.ts +46 -12
- package/lib/bus.ts +1673 -444
- package/lib/channel-posts.ts +848 -327
- package/lib/command-templates.ts +152 -265
- package/lib/commands.ts +2398 -555
- package/lib/config.ts +177 -170
- package/lib/delivery.ts +41 -29
- package/lib/extension.ts +1206 -433
- package/lib/generative-app-worker.mjs +19 -7
- package/lib/generative-apps.ts +324 -211
- package/lib/inbound.ts +95 -97
- package/lib/journal.ts +5311 -1763
- package/lib/keyboard.ts +1 -3
- package/lib/lifecycle.ts +128 -78
- package/lib/locks.ts +677 -203
- package/lib/logging.ts +147 -58
- package/lib/media.ts +82 -43
- package/lib/menu-model.ts +85 -18
- package/lib/menu-queue.ts +88 -129
- package/lib/menu-settings.ts +260 -155
- package/lib/menu-status.ts +20 -60
- package/lib/menu-thinking.ts +41 -56
- package/lib/menu.ts +112 -285
- package/lib/model.ts +8 -22
- package/lib/outbound-attachments.ts +156 -125
- package/lib/outbound-buttons.ts +76 -49
- package/lib/outbound-markup.ts +96 -94
- package/lib/outbound-voice.ts +33 -27
- package/lib/outbound.ts +128 -125
- package/lib/ownership.ts +19 -10
- package/lib/paths.ts +148 -49
- package/lib/pi.ts +35 -15
- package/lib/polling.ts +144 -147
- package/lib/preview.ts +93 -40
- package/lib/process-identity.ts +164 -0
- package/lib/prompts.ts +4 -11
- package/lib/queue.ts +516 -301
- package/lib/recovery.ts +50 -454
- package/lib/rendering.ts +1 -3
- package/lib/replies.ts +248 -209
- package/lib/routing.ts +8509 -2186
- package/lib/runtime.ts +1 -12
- package/lib/sections.ts +24 -89
- package/lib/setup.ts +1 -3
- package/lib/skills.ts +9 -4
- package/lib/status.ts +215 -110
- package/lib/sync.ts +448 -361
- package/lib/target.ts +22 -3
- package/lib/telegram-api.ts +302 -127
- package/lib/text-groups.ts +5 -39
- package/lib/thread-cleanup-manager.ts +833 -331
- package/lib/thread-display.ts +213 -97
- package/lib/thread-naming.ts +232 -87
- package/lib/thread-reconciler.ts +155 -72
- package/lib/threads.ts +5318 -1845
- package/lib/time-injection.ts +1 -4
- package/lib/turns.ts +15 -28
- package/lib/updates.ts +5184 -1612
- package/lib/voice.ts +7 -35
- package/lib/wire.ts +1 -1
- package/lib/workspace-admission.ts +996 -416
- package/lib/workspace-identity.ts +14 -17
- package/lib/workspace-retirement.ts +878 -376
- package/lib/workspace-slots.ts +22 -13
- package/package.json +1 -1
- package/scripts/check-downgrade.mjs +0 -472
- package/scripts/measure-workspace.mjs +0 -108
|
@@ -18,18 +18,24 @@ import { dirname } from "node:path";
|
|
|
18
18
|
import { isDeepStrictEqual } from "node:util";
|
|
19
19
|
|
|
20
20
|
import {
|
|
21
|
-
getTelegramProcessLiveness,
|
|
22
|
-
type TelegramProcessLiveness,
|
|
23
|
-
} from "./bus.ts";
|
|
24
|
-
import {
|
|
25
|
-
renameTelegramPathWithRetry,
|
|
26
|
-
withTelegramFileTransaction,
|
|
27
|
-
readTelegramRuntimeState,
|
|
28
21
|
mutateTelegramRuntimeStateSection,
|
|
22
|
+
readTelegramRuntimeState,
|
|
23
|
+
renameTelegramPathWithRetry,
|
|
29
24
|
TelegramRuntimeStateError,
|
|
25
|
+
withTelegramFileTransaction,
|
|
30
26
|
} from "./locks.ts";
|
|
31
|
-
import {
|
|
32
|
-
|
|
27
|
+
import {
|
|
28
|
+
getTelegramProcessLiveness,
|
|
29
|
+
type TelegramProcessLiveness,
|
|
30
|
+
} from "./process-identity.ts";
|
|
31
|
+
import {
|
|
32
|
+
areTelegramTargetsEqual as areTargetsEqual,
|
|
33
|
+
type TelegramTarget,
|
|
34
|
+
} from "./target.ts";
|
|
35
|
+
import {
|
|
36
|
+
isWireRecord as isRecord,
|
|
37
|
+
isNonNegativeWireInteger as isSafeTimestamp,
|
|
38
|
+
} from "./wire.ts";
|
|
33
39
|
|
|
34
40
|
const TELEGRAM_WORKSPACE_ADMISSION_VERSION = 1;
|
|
35
41
|
const TELEGRAM_WORKSPACE_ADMISSION_MAX_LEASES = 4096;
|
|
@@ -56,12 +62,12 @@ interface TelegramWorkspaceAdmissionLease {
|
|
|
56
62
|
}
|
|
57
63
|
|
|
58
64
|
type TelegramWorkspaceDestructiveFenceKind =
|
|
59
|
-
|
|
60
|
-
| "manual-thread-cleanup"
|
|
61
|
-
| "journal-writer-closure";
|
|
65
|
+
"pressure-retirement" | "manual-thread-cleanup" | "journal-writer-closure";
|
|
62
66
|
|
|
63
67
|
export type TelegramWorkspaceDeletionFenceKind = Exclude<
|
|
64
|
-
TelegramWorkspaceDestructiveFenceKind,
|
|
68
|
+
TelegramWorkspaceDestructiveFenceKind,
|
|
69
|
+
"journal-writer-closure"
|
|
70
|
+
>;
|
|
65
71
|
|
|
66
72
|
interface TelegramWorkspaceJournalWriterClosureFence {
|
|
67
73
|
destructiveKind: "journal-writer-closure";
|
|
@@ -78,18 +84,45 @@ export function normalizeTelegramWorkspaceJournalWriterClosureFence(
|
|
|
78
84
|
value: unknown,
|
|
79
85
|
profileKey: string,
|
|
80
86
|
): TelegramWorkspaceJournalWriterClosureFence | undefined {
|
|
81
|
-
if (
|
|
82
|
-
|
|
83
|
-
value.
|
|
84
|
-
|
|
87
|
+
if (
|
|
88
|
+
!isRecord(value) ||
|
|
89
|
+
value.destructiveKind !== "journal-writer-closure" ||
|
|
90
|
+
value.phase !== "fenced" ||
|
|
91
|
+
!isBoundedText(value.operationId) ||
|
|
92
|
+
value.profileKey !== profileKey ||
|
|
93
|
+
!isBoundedText(value.recoveryKey) ||
|
|
94
|
+
!isSafeTimestamp(value.requestedAtMs) ||
|
|
95
|
+
!isSafeTimestamp(value.acquiredAtMs)
|
|
96
|
+
)
|
|
97
|
+
return undefined;
|
|
85
98
|
const owner = normalizeOwner(value.owner);
|
|
86
|
-
if (
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
99
|
+
if (
|
|
100
|
+
!owner ||
|
|
101
|
+
Object.keys(value).some(
|
|
102
|
+
(key) =>
|
|
103
|
+
![
|
|
104
|
+
"destructiveKind",
|
|
105
|
+
"phase",
|
|
106
|
+
"operationId",
|
|
107
|
+
"profileKey",
|
|
108
|
+
"recoveryKey",
|
|
109
|
+
"owner",
|
|
110
|
+
"requestedAtMs",
|
|
111
|
+
"acquiredAtMs",
|
|
112
|
+
].includes(key),
|
|
113
|
+
)
|
|
114
|
+
)
|
|
115
|
+
return undefined;
|
|
116
|
+
return {
|
|
117
|
+
destructiveKind: "journal-writer-closure",
|
|
118
|
+
phase: "fenced",
|
|
119
|
+
operationId: value.operationId,
|
|
120
|
+
profileKey,
|
|
121
|
+
recoveryKey: value.recoveryKey,
|
|
122
|
+
owner,
|
|
123
|
+
requestedAtMs: value.requestedAtMs,
|
|
124
|
+
acquiredAtMs: value.acquiredAtMs,
|
|
125
|
+
};
|
|
93
126
|
}
|
|
94
127
|
|
|
95
128
|
interface TelegramWorkspaceJournalWriterProtocolMode {
|
|
@@ -109,21 +142,43 @@ export function normalizeTelegramWorkspaceJournalWriterProtocolMode(
|
|
|
109
142
|
profileKey: string,
|
|
110
143
|
): TelegramWorkspaceJournalWriterProtocolMode | undefined {
|
|
111
144
|
if (!isRecord(value)) return undefined;
|
|
112
|
-
const keys = [
|
|
113
|
-
"
|
|
145
|
+
const keys = [
|
|
146
|
+
"version",
|
|
147
|
+
"protocol",
|
|
148
|
+
"profileKey",
|
|
149
|
+
"recoveryKey",
|
|
150
|
+
"startupAuthorityId",
|
|
151
|
+
"closureOperationId",
|
|
152
|
+
"writerInventorySha256",
|
|
153
|
+
"installedBy",
|
|
154
|
+
"installedAtMs",
|
|
155
|
+
];
|
|
114
156
|
const installedBy = normalizeOwner(value.installedBy);
|
|
115
|
-
if (
|
|
116
|
-
value.
|
|
117
|
-
|
|
157
|
+
if (
|
|
158
|
+
Object.keys(value).some((key) => !keys.includes(key)) ||
|
|
159
|
+
value.version !== 1 ||
|
|
160
|
+
value.protocol !== "custody-v3" ||
|
|
161
|
+
value.profileKey !== profileKey ||
|
|
162
|
+
!isBoundedText(value.recoveryKey) ||
|
|
163
|
+
!isBoundedText(value.startupAuthorityId) ||
|
|
118
164
|
!isBoundedText(value.closureOperationId) ||
|
|
119
165
|
typeof value.writerInventorySha256 !== "string" ||
|
|
120
|
-
!/^[a-f0-9]{64}$/u.test(value.writerInventorySha256) ||
|
|
121
|
-
!
|
|
122
|
-
|
|
123
|
-
|
|
166
|
+
!/^[a-f0-9]{64}$/u.test(value.writerInventorySha256) ||
|
|
167
|
+
!installedBy ||
|
|
168
|
+
!isSafeTimestamp(value.installedAtMs)
|
|
169
|
+
)
|
|
170
|
+
return undefined;
|
|
171
|
+
return {
|
|
172
|
+
version: 1,
|
|
173
|
+
protocol: "custody-v3",
|
|
174
|
+
profileKey,
|
|
175
|
+
recoveryKey: value.recoveryKey,
|
|
176
|
+
startupAuthorityId: value.startupAuthorityId,
|
|
124
177
|
closureOperationId: value.closureOperationId,
|
|
125
178
|
writerInventorySha256: value.writerInventorySha256,
|
|
126
|
-
installedBy,
|
|
179
|
+
installedBy,
|
|
180
|
+
installedAtMs: value.installedAtMs,
|
|
181
|
+
};
|
|
127
182
|
}
|
|
128
183
|
|
|
129
184
|
interface TelegramWorkspaceRetirementFenceBase {
|
|
@@ -159,8 +214,7 @@ export type TelegramWorkspaceRetirementFence =
|
|
|
159
214
|
});
|
|
160
215
|
|
|
161
216
|
export type TelegramWorkspaceDestructiveFence =
|
|
162
|
-
|
|
|
163
|
-
| TelegramWorkspaceJournalWriterClosureFence;
|
|
217
|
+
TelegramWorkspaceRetirementFence | TelegramWorkspaceJournalWriterClosureFence;
|
|
164
218
|
|
|
165
219
|
export function isTelegramWorkspaceRetirementFence(
|
|
166
220
|
fence: TelegramWorkspaceDestructiveFence,
|
|
@@ -189,7 +243,11 @@ interface TelegramWorkspaceAdmissionState {
|
|
|
189
243
|
}
|
|
190
244
|
|
|
191
245
|
type TelegramWorkspaceAdmissionAcquireResult =
|
|
192
|
-
| {
|
|
246
|
+
| {
|
|
247
|
+
kind: "acquired";
|
|
248
|
+
lease: TelegramWorkspaceAdmissionLease;
|
|
249
|
+
resumed: boolean;
|
|
250
|
+
}
|
|
193
251
|
| { kind: "blocked"; reason: "retirement-fenced" };
|
|
194
252
|
|
|
195
253
|
type TelegramWorkspaceRetirementFenceAcquireResult =
|
|
@@ -209,7 +267,11 @@ type TelegramWorkspaceJournalWriterProtocolInstallResult = {
|
|
|
209
267
|
};
|
|
210
268
|
|
|
211
269
|
type TelegramWorkspaceJournalWriterClosureAcquireResult =
|
|
212
|
-
| {
|
|
270
|
+
| {
|
|
271
|
+
kind: "acquired";
|
|
272
|
+
fence: TelegramWorkspaceJournalWriterClosureFence;
|
|
273
|
+
resumed: boolean;
|
|
274
|
+
}
|
|
213
275
|
| { kind: "blocked"; reason: "admission-active" | "retirement-active" };
|
|
214
276
|
|
|
215
277
|
type TelegramWorkspaceDeletionPermitResult =
|
|
@@ -228,8 +290,7 @@ interface TelegramWorkspaceAdmissionLedgerSnapshot {
|
|
|
228
290
|
}
|
|
229
291
|
|
|
230
292
|
type TelegramWorkspaceAdmissionPublicationBoundary =
|
|
231
|
-
| "before-
|
|
232
|
-
| "after-write-before-rename";
|
|
293
|
+
"before-write" | "after-write-before-rename";
|
|
233
294
|
|
|
234
295
|
interface TelegramWorkspaceAdmissionLedgerOptions {
|
|
235
296
|
path: string;
|
|
@@ -283,15 +344,25 @@ export interface TelegramWorkspaceAdmissionLedger {
|
|
|
283
344
|
scope: TelegramWorkspaceAdmissionScope;
|
|
284
345
|
}) => TelegramWorkspaceAdmissionAcquireResult;
|
|
285
346
|
releaseAdmission: (expected: TelegramWorkspaceAdmissionLease) => boolean;
|
|
286
|
-
acquireJournalWriterClosure: (input: {
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
347
|
+
acquireJournalWriterClosure: (input: {
|
|
348
|
+
operationId: string;
|
|
349
|
+
recoveryKey: string;
|
|
350
|
+
requestedAtMs: number;
|
|
351
|
+
}) => TelegramWorkspaceJournalWriterClosureAcquireResult;
|
|
352
|
+
releaseJournalWriterClosure: (
|
|
353
|
+
expected: TelegramWorkspaceJournalWriterClosureFence,
|
|
354
|
+
) => boolean;
|
|
355
|
+
installJournalWriterProtocolMode: (
|
|
356
|
+
expected: TelegramWorkspaceJournalWriterClosureFence,
|
|
357
|
+
input: { startupAuthorityId: string; writerInventorySha256: string },
|
|
358
|
+
) => TelegramWorkspaceJournalWriterProtocolInstallResult;
|
|
359
|
+
acquireJournalWriterAdmission: (input: {
|
|
360
|
+
operationId: string;
|
|
361
|
+
recoveryKey: string;
|
|
362
|
+
startupAuthorityId: string;
|
|
363
|
+
closureOperationId: string;
|
|
364
|
+
writerInventorySha256: string;
|
|
365
|
+
}) => TelegramWorkspaceAdmissionAcquireResult;
|
|
295
366
|
acquireRetirementFence: (input: {
|
|
296
367
|
operationId: string;
|
|
297
368
|
retirementIntentId: string;
|
|
@@ -412,7 +483,8 @@ function normalizeScope(
|
|
|
412
483
|
if (!isRecord(value)) return undefined;
|
|
413
484
|
if (value.kind === "profile") return { kind: "profile" };
|
|
414
485
|
if (value.kind === "chat") {
|
|
415
|
-
if (!Number.isSafeInteger(value.chatId) || value.chatId === 0)
|
|
486
|
+
if (!Number.isSafeInteger(value.chatId) || value.chatId === 0)
|
|
487
|
+
return undefined;
|
|
416
488
|
return { kind: "chat", chatId: value.chatId as number };
|
|
417
489
|
}
|
|
418
490
|
if (value.kind === "target") {
|
|
@@ -455,7 +527,10 @@ function normalizeFence(
|
|
|
455
527
|
): TelegramWorkspaceDestructiveFence | undefined {
|
|
456
528
|
if (!isRecord(value)) return undefined;
|
|
457
529
|
if (value.destructiveKind === "journal-writer-closure")
|
|
458
|
-
return normalizeTelegramWorkspaceJournalWriterClosureFence(
|
|
530
|
+
return normalizeTelegramWorkspaceJournalWriterClosureFence(
|
|
531
|
+
value,
|
|
532
|
+
profileKey,
|
|
533
|
+
);
|
|
459
534
|
const target = normalizeTarget(value.target);
|
|
460
535
|
const owner = normalizeOwner(value.owner);
|
|
461
536
|
if (
|
|
@@ -473,10 +548,16 @@ function normalizeFence(
|
|
|
473
548
|
) {
|
|
474
549
|
return undefined;
|
|
475
550
|
}
|
|
476
|
-
if (
|
|
477
|
-
|
|
551
|
+
if (
|
|
552
|
+
value.destructiveKind !== undefined &&
|
|
553
|
+
value.destructiveKind !== "pressure-retirement" &&
|
|
554
|
+
value.destructiveKind !== "manual-thread-cleanup"
|
|
555
|
+
)
|
|
556
|
+
return undefined;
|
|
478
557
|
const base: TelegramWorkspaceRetirementFenceBase = {
|
|
479
|
-
...(value.destructiveKind === undefined
|
|
558
|
+
...(value.destructiveKind === undefined
|
|
559
|
+
? {}
|
|
560
|
+
: { destructiveKind: value.destructiveKind }),
|
|
480
561
|
operationId: value.operationId,
|
|
481
562
|
retirementIntentId: value.retirementIntentId,
|
|
482
563
|
profileKey,
|
|
@@ -499,12 +580,23 @@ function normalizeFence(
|
|
|
499
580
|
deletionIssuedAtMs: value.deletionIssuedAtMs,
|
|
500
581
|
};
|
|
501
582
|
}
|
|
502
|
-
if (
|
|
503
|
-
|
|
504
|
-
|
|
583
|
+
if (
|
|
584
|
+
value.phase === "deletion-rejected" &&
|
|
585
|
+
(value.destructiveKind === undefined ||
|
|
586
|
+
value.destructiveKind === "pressure-retirement") &&
|
|
587
|
+
isSafeTimestamp(value.deletionIssuedAtMs) &&
|
|
588
|
+
isSafeTimestamp(value.rejectionConfirmedAtMs)
|
|
589
|
+
) {
|
|
505
590
|
const { destructiveKind: _kind, ...pressureBase } = base;
|
|
506
|
-
return {
|
|
507
|
-
|
|
591
|
+
return {
|
|
592
|
+
...pressureBase,
|
|
593
|
+
...(value.destructiveKind === "pressure-retirement"
|
|
594
|
+
? { destructiveKind: "pressure-retirement" as const }
|
|
595
|
+
: {}),
|
|
596
|
+
phase: "deletion-rejected",
|
|
597
|
+
deletionIssuedAtMs: value.deletionIssuedAtMs,
|
|
598
|
+
rejectionConfirmedAtMs: value.rejectionConfirmedAtMs,
|
|
599
|
+
};
|
|
508
600
|
}
|
|
509
601
|
if (
|
|
510
602
|
value.phase === "commit-ready" &&
|
|
@@ -535,18 +627,28 @@ function cloneScope(
|
|
|
535
627
|
function cloneLease(
|
|
536
628
|
lease: TelegramWorkspaceAdmissionLease,
|
|
537
629
|
): TelegramWorkspaceAdmissionLease {
|
|
538
|
-
return {
|
|
630
|
+
return {
|
|
631
|
+
...lease,
|
|
632
|
+
scope: cloneScope(lease.scope),
|
|
633
|
+
owner: { ...lease.owner },
|
|
634
|
+
};
|
|
539
635
|
}
|
|
540
636
|
|
|
541
|
-
export function resolveTelegramWorkspaceDestructiveFenceKind(
|
|
542
|
-
|
|
543
|
-
): TelegramWorkspaceDestructiveFenceKind {
|
|
637
|
+
export function resolveTelegramWorkspaceDestructiveFenceKind(fence: {
|
|
638
|
+
destructiveKind?: TelegramWorkspaceDestructiveFenceKind;
|
|
639
|
+
}): TelegramWorkspaceDestructiveFenceKind {
|
|
544
640
|
return fence.destructiveKind ?? "pressure-retirement";
|
|
545
641
|
}
|
|
546
642
|
|
|
547
|
-
function cloneFence(
|
|
548
|
-
|
|
549
|
-
|
|
643
|
+
function cloneFence(
|
|
644
|
+
fence: TelegramWorkspaceRetirementFence,
|
|
645
|
+
): TelegramWorkspaceRetirementFence;
|
|
646
|
+
function cloneFence(
|
|
647
|
+
fence: TelegramWorkspaceJournalWriterClosureFence,
|
|
648
|
+
): TelegramWorkspaceJournalWriterClosureFence;
|
|
649
|
+
function cloneFence(
|
|
650
|
+
fence: TelegramWorkspaceDestructiveFence,
|
|
651
|
+
): TelegramWorkspaceDestructiveFence;
|
|
550
652
|
function cloneFence(
|
|
551
653
|
fence: TelegramWorkspaceDestructiveFence,
|
|
552
654
|
): TelegramWorkspaceDestructiveFence {
|
|
@@ -589,22 +691,65 @@ function isConsolidatedAdmissionKey(value: unknown, profile: string): boolean {
|
|
|
589
691
|
if (!isBoundedText(value)) return false;
|
|
590
692
|
try {
|
|
591
693
|
const key: unknown = JSON.parse(value);
|
|
592
|
-
return
|
|
593
|
-
isRecord(key
|
|
594
|
-
|
|
694
|
+
return (
|
|
695
|
+
isRecord(key) &&
|
|
696
|
+
Object.keys(key).length === 3 &&
|
|
697
|
+
key.version === TELEGRAM_WORKSPACE_ADMISSION_VERSION &&
|
|
698
|
+
key.profile === profile &&
|
|
699
|
+
isRecord(key.bot) &&
|
|
700
|
+
Object.keys(key.bot).length === 1 &&
|
|
701
|
+
typeof key.bot.tokenSha256 === "string" &&
|
|
702
|
+
/^[a-f0-9]{64}$/u.test(key.bot.tokenSha256)
|
|
703
|
+
);
|
|
704
|
+
} catch {
|
|
705
|
+
return false;
|
|
706
|
+
}
|
|
595
707
|
}
|
|
596
708
|
/** Pure damage check for one consolidated admission section under its own stored profile key; throws when invalid. */
|
|
597
|
-
export function assertTelegramConsolidatedAdmissionSection(
|
|
709
|
+
export function assertTelegramConsolidatedAdmissionSection(
|
|
710
|
+
value: unknown,
|
|
711
|
+
stateProfile: string,
|
|
712
|
+
): void {
|
|
598
713
|
if (value === undefined) return;
|
|
599
|
-
normalizeAdmissionState(
|
|
714
|
+
normalizeAdmissionState(
|
|
715
|
+
value,
|
|
716
|
+
isRecord(value) && typeof value.profileKey === "string"
|
|
717
|
+
? value.profileKey
|
|
718
|
+
: "",
|
|
719
|
+
stateProfile,
|
|
720
|
+
);
|
|
600
721
|
}
|
|
601
722
|
|
|
602
|
-
function normalizeAdmissionState(
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
723
|
+
function normalizeAdmissionState(
|
|
724
|
+
parsed: unknown,
|
|
725
|
+
profileKey: string,
|
|
726
|
+
stateProfile?: string,
|
|
727
|
+
): TelegramWorkspaceAdmissionState {
|
|
728
|
+
if (parsed === undefined)
|
|
729
|
+
return {
|
|
730
|
+
version: TELEGRAM_WORKSPACE_ADMISSION_VERSION,
|
|
731
|
+
profileKey,
|
|
732
|
+
leases: [],
|
|
733
|
+
};
|
|
734
|
+
if (!isRecord(parsed))
|
|
735
|
+
invalidState("Telegram Workspace admission state is invalid.");
|
|
736
|
+
if (
|
|
737
|
+
stateProfile !== undefined &&
|
|
738
|
+
(Object.keys(parsed).some(
|
|
739
|
+
(key) =>
|
|
740
|
+
![
|
|
741
|
+
"version",
|
|
742
|
+
"profileKey",
|
|
743
|
+
"leases",
|
|
744
|
+
"fence",
|
|
745
|
+
"writerProtocolMode",
|
|
746
|
+
].includes(key),
|
|
747
|
+
) ||
|
|
748
|
+
!isConsolidatedAdmissionKey(parsed.profileKey, stateProfile))
|
|
749
|
+
)
|
|
750
|
+
invalidState(
|
|
751
|
+
"Telegram consolidated admission section identity is invalid.",
|
|
752
|
+
);
|
|
608
753
|
if (
|
|
609
754
|
parsed.version !== TELEGRAM_WORKSPACE_ADMISSION_VERSION ||
|
|
610
755
|
!Array.isArray(parsed.leases) ||
|
|
@@ -613,8 +758,11 @@ function normalizeAdmissionState(parsed: unknown, profileKey: string, stateProfi
|
|
|
613
758
|
invalidState("Telegram Workspace admission state authority is invalid.");
|
|
614
759
|
}
|
|
615
760
|
if (parsed.profileKey !== profileKey) {
|
|
616
|
-
if (
|
|
617
|
-
|
|
761
|
+
if (
|
|
762
|
+
parsed.leases.length === 0 &&
|
|
763
|
+
parsed.fence === undefined &&
|
|
764
|
+
parsed.writerProtocolMode === undefined
|
|
765
|
+
) {
|
|
618
766
|
return {
|
|
619
767
|
version: TELEGRAM_WORKSPACE_ADMISSION_VERSION,
|
|
620
768
|
profileKey,
|
|
@@ -623,28 +771,48 @@ function normalizeAdmissionState(parsed: unknown, profileKey: string, stateProfi
|
|
|
623
771
|
}
|
|
624
772
|
invalidState("Telegram Workspace admission profile authority changed.");
|
|
625
773
|
}
|
|
626
|
-
const leases = parsed.leases.map((value) =>
|
|
774
|
+
const leases = parsed.leases.map((value) =>
|
|
775
|
+
normalizeLease(value, profileKey),
|
|
776
|
+
);
|
|
627
777
|
if (leases.some((lease) => lease === undefined)) {
|
|
628
778
|
invalidState("Telegram Workspace admission lease is invalid.");
|
|
629
779
|
}
|
|
630
780
|
const normalizedLeases = leases as TelegramWorkspaceAdmissionLease[];
|
|
631
|
-
if (
|
|
781
|
+
if (
|
|
782
|
+
new Set(normalizedLeases.map((lease) => lease.operationId)).size !==
|
|
783
|
+
normalizedLeases.length
|
|
784
|
+
) {
|
|
632
785
|
invalidState("Telegram Workspace admission lease identity is duplicated.");
|
|
633
786
|
}
|
|
634
787
|
const fence =
|
|
635
|
-
parsed.fence === undefined
|
|
788
|
+
parsed.fence === undefined
|
|
789
|
+
? undefined
|
|
790
|
+
: normalizeFence(parsed.fence, profileKey);
|
|
636
791
|
if (parsed.fence !== undefined && !fence) {
|
|
637
792
|
invalidState("Telegram Workspace retirement fence is invalid.");
|
|
638
793
|
}
|
|
639
|
-
const writerProtocolMode =
|
|
640
|
-
|
|
794
|
+
const writerProtocolMode =
|
|
795
|
+
parsed.writerProtocolMode === undefined
|
|
796
|
+
? undefined
|
|
797
|
+
: normalizeTelegramWorkspaceJournalWriterProtocolMode(
|
|
798
|
+
parsed.writerProtocolMode,
|
|
799
|
+
profileKey,
|
|
800
|
+
);
|
|
641
801
|
if (parsed.writerProtocolMode !== undefined && !writerProtocolMode)
|
|
642
802
|
invalidState("Telegram Workspace writer protocol mode is invalid.");
|
|
643
803
|
if (fence && writerProtocolMode)
|
|
644
|
-
invalidState(
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
804
|
+
invalidState(
|
|
805
|
+
"Telegram Workspace destructive fence conflicts with writer protocol mode.",
|
|
806
|
+
);
|
|
807
|
+
if (
|
|
808
|
+
stateProfile !== undefined &&
|
|
809
|
+
(!isDeepStrictEqual(parsed.leases, normalizedLeases) ||
|
|
810
|
+
!isDeepStrictEqual(parsed.fence, fence) ||
|
|
811
|
+
!isDeepStrictEqual(parsed.writerProtocolMode, writerProtocolMode))
|
|
812
|
+
)
|
|
813
|
+
invalidState(
|
|
814
|
+
"Telegram consolidated admission evidence cannot drop unknown fields.",
|
|
815
|
+
);
|
|
648
816
|
return {
|
|
649
817
|
version: TELEGRAM_WORKSPACE_ADMISSION_VERSION,
|
|
650
818
|
profileKey,
|
|
@@ -678,7 +846,9 @@ function writeState(
|
|
|
678
846
|
...(options.publishRename ? { rename: options.publishRename } : {}),
|
|
679
847
|
})
|
|
680
848
|
) {
|
|
681
|
-
throw new Error(
|
|
849
|
+
throw new Error(
|
|
850
|
+
"Temporary admission state disappeared before publication.",
|
|
851
|
+
);
|
|
682
852
|
}
|
|
683
853
|
chmodSync(path, 0o600);
|
|
684
854
|
} catch (error) {
|
|
@@ -770,38 +940,50 @@ function areWriterProtocolModesEqual(
|
|
|
770
940
|
left: TelegramWorkspaceJournalWriterProtocolMode,
|
|
771
941
|
right: TelegramWorkspaceJournalWriterProtocolMode,
|
|
772
942
|
): boolean {
|
|
773
|
-
return
|
|
943
|
+
return (
|
|
944
|
+
left.protocol === right.protocol &&
|
|
945
|
+
left.profileKey === right.profileKey &&
|
|
774
946
|
left.recoveryKey === right.recoveryKey &&
|
|
775
947
|
left.startupAuthorityId === right.startupAuthorityId &&
|
|
776
948
|
left.closureOperationId === right.closureOperationId &&
|
|
777
949
|
left.writerInventorySha256 === right.writerInventorySha256 &&
|
|
778
|
-
areOwnersEqual(left.installedBy, right.installedBy) &&
|
|
950
|
+
areOwnersEqual(left.installedBy, right.installedBy) &&
|
|
951
|
+
left.installedAtMs === right.installedAtMs
|
|
952
|
+
);
|
|
779
953
|
}
|
|
780
954
|
|
|
781
955
|
function areWriterClosureFencesEqual(
|
|
782
956
|
left: TelegramWorkspaceJournalWriterClosureFence,
|
|
783
957
|
right: TelegramWorkspaceJournalWriterClosureFence,
|
|
784
958
|
): boolean {
|
|
785
|
-
return
|
|
786
|
-
left.
|
|
787
|
-
left.
|
|
959
|
+
return (
|
|
960
|
+
left.operationId === right.operationId &&
|
|
961
|
+
left.profileKey === right.profileKey &&
|
|
962
|
+
left.recoveryKey === right.recoveryKey &&
|
|
963
|
+
areOwnersEqual(left.owner, right.owner) &&
|
|
964
|
+
left.requestedAtMs === right.requestedAtMs &&
|
|
965
|
+
left.acquiredAtMs === right.acquiredAtMs
|
|
966
|
+
);
|
|
788
967
|
}
|
|
789
968
|
|
|
790
969
|
function areFencesEqual(
|
|
791
970
|
left: TelegramWorkspaceRetirementFence,
|
|
792
971
|
right: TelegramWorkspaceRetirementFence,
|
|
793
972
|
): boolean {
|
|
794
|
-
if (!areFenceBasesEqual(left, right) || left.phase !== right.phase)
|
|
973
|
+
if (!areFenceBasesEqual(left, right) || left.phase !== right.phase)
|
|
974
|
+
return false;
|
|
795
975
|
if (left.phase === "fenced" && right.phase === "fenced") return true;
|
|
796
|
-
if (
|
|
797
|
-
left.phase === "deletion-issued" &&
|
|
798
|
-
right.phase === "deletion-issued"
|
|
799
|
-
) {
|
|
976
|
+
if (left.phase === "deletion-issued" && right.phase === "deletion-issued") {
|
|
800
977
|
return left.deletionIssuedAtMs === right.deletionIssuedAtMs;
|
|
801
978
|
}
|
|
802
|
-
if (
|
|
803
|
-
|
|
804
|
-
|
|
979
|
+
if (
|
|
980
|
+
left.phase === "deletion-rejected" &&
|
|
981
|
+
right.phase === "deletion-rejected"
|
|
982
|
+
) {
|
|
983
|
+
return (
|
|
984
|
+
left.deletionIssuedAtMs === right.deletionIssuedAtMs &&
|
|
985
|
+
left.rejectionConfirmedAtMs === right.rejectionConfirmedAtMs
|
|
986
|
+
);
|
|
805
987
|
}
|
|
806
988
|
return (
|
|
807
989
|
left.phase === "commit-ready" &&
|
|
@@ -811,7 +993,9 @@ function areFencesEqual(
|
|
|
811
993
|
);
|
|
812
994
|
}
|
|
813
995
|
|
|
814
|
-
function validateOptions(
|
|
996
|
+
function validateOptions(
|
|
997
|
+
options: TelegramWorkspaceAdmissionLedgerOptions,
|
|
998
|
+
): void {
|
|
815
999
|
if (!isBoundedText(options.path)) {
|
|
816
1000
|
invalidInput("Telegram Workspace admission path is required.");
|
|
817
1001
|
}
|
|
@@ -821,8 +1005,14 @@ function validateOptions(options: TelegramWorkspaceAdmissionLedgerOptions): void
|
|
|
821
1005
|
if (!normalizeOwner(options.owner)) {
|
|
822
1006
|
invalidInput("Telegram Workspace admission owner is invalid.");
|
|
823
1007
|
}
|
|
824
|
-
if (
|
|
825
|
-
|
|
1008
|
+
if (
|
|
1009
|
+
options.stateProfile !== undefined &&
|
|
1010
|
+
(!isBoundedText(options.stateProfile) ||
|
|
1011
|
+
!isConsolidatedAdmissionKey(options.profileKey, options.stateProfile))
|
|
1012
|
+
)
|
|
1013
|
+
invalidInput(
|
|
1014
|
+
"Telegram consolidated admission requires an exact bot/profile section identity.",
|
|
1015
|
+
);
|
|
826
1016
|
}
|
|
827
1017
|
|
|
828
1018
|
function validateOperationId(operationId: string): void {
|
|
@@ -865,7 +1055,9 @@ export function createTelegramWorkspaceAdmissionProfileKey(input: {
|
|
|
865
1055
|
}): string {
|
|
866
1056
|
const profileName = (input.profileName ?? "default").trim();
|
|
867
1057
|
if (!isBoundedText(profileName) || !isBoundedText(input.botToken)) {
|
|
868
|
-
invalidInput(
|
|
1058
|
+
invalidInput(
|
|
1059
|
+
"Telegram Workspace admission bot/profile identity is invalid.",
|
|
1060
|
+
);
|
|
869
1061
|
}
|
|
870
1062
|
return JSON.stringify({
|
|
871
1063
|
version: TELEGRAM_WORKSPACE_ADMISSION_VERSION,
|
|
@@ -880,18 +1072,25 @@ interface TelegramWorkspaceAdmissionRuntimeBinding {
|
|
|
880
1072
|
resolve: () => TelegramWorkspaceAdmissionLedger | undefined;
|
|
881
1073
|
}
|
|
882
1074
|
|
|
883
|
-
export function createTelegramWorkspaceAdmissionRuntimeBinding(
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
1075
|
+
export function createTelegramWorkspaceAdmissionRuntimeBinding(
|
|
1076
|
+
input: {
|
|
1077
|
+
getProfileName: () => string | undefined;
|
|
1078
|
+
getBotToken: () => string | undefined;
|
|
1079
|
+
owner: TelegramWorkspaceAdmissionOwner;
|
|
1080
|
+
getNowMs?: () => number;
|
|
1081
|
+
getProcessLiveness?: (
|
|
1082
|
+
owner: TelegramWorkspaceAdmissionOwner,
|
|
1083
|
+
) => TelegramProcessLiveness;
|
|
1084
|
+
} & (
|
|
1085
|
+
| { getPath: (profileName?: string) => string; getStatePath?: never }
|
|
1086
|
+
| { getStatePath: () => string; getPath?: never }
|
|
1087
|
+
),
|
|
1088
|
+
): TelegramWorkspaceAdmissionRuntimeBinding {
|
|
893
1089
|
const { getProfileName, getBotToken, getPath, getStatePath } = input;
|
|
894
|
-
if ((getPath === undefined) === (getStatePath === undefined))
|
|
1090
|
+
if ((getPath === undefined) === (getStatePath === undefined))
|
|
1091
|
+
invalidInput(
|
|
1092
|
+
"Telegram admission binding must select one storage identity.",
|
|
1093
|
+
);
|
|
895
1094
|
return {
|
|
896
1095
|
resolve() {
|
|
897
1096
|
const botToken = getBotToken();
|
|
@@ -903,7 +1102,9 @@ export function createTelegramWorkspaceAdmissionRuntimeBinding(input: {
|
|
|
903
1102
|
});
|
|
904
1103
|
return createTelegramWorkspaceAdmissionLedger({
|
|
905
1104
|
path: getStatePath ? getStatePath() : getPath!(configuredProfileName),
|
|
906
|
-
...(getStatePath
|
|
1105
|
+
...(getStatePath
|
|
1106
|
+
? { stateProfile: configuredProfileName ?? "default" }
|
|
1107
|
+
: {}),
|
|
907
1108
|
profileKey,
|
|
908
1109
|
owner: input.owner,
|
|
909
1110
|
getNowMs: input.getNowMs,
|
|
@@ -913,7 +1114,10 @@ export function createTelegramWorkspaceAdmissionRuntimeBinding(input: {
|
|
|
913
1114
|
};
|
|
914
1115
|
}
|
|
915
1116
|
|
|
916
|
-
|
|
1117
|
+
/** Stable admission-scope identity used by leases and admission-gated journal operations. */
|
|
1118
|
+
export function getTelegramWorkspaceAdmissionScopeKey(
|
|
1119
|
+
scope: TelegramWorkspaceAdmissionScope,
|
|
1120
|
+
): string {
|
|
917
1121
|
if (scope.kind === "profile") return "profile";
|
|
918
1122
|
if (scope.kind === "chat") return `chat:${scope.chatId}`;
|
|
919
1123
|
return `target:${scope.target.chatId}:${scope.target.threadId}`;
|
|
@@ -931,16 +1135,12 @@ function reserveTelegramWorkspaceAdmissionOperationId(
|
|
|
931
1135
|
};
|
|
932
1136
|
}
|
|
933
1137
|
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
TelegramWorkspaceAdmissionLedger,
|
|
937
|
-
"acquireAdmission" | "releaseAdmission"
|
|
938
|
-
>;
|
|
1138
|
+
/** Validated, de-duplicated scopes in the stable key order every admitted operation acquires. */
|
|
1139
|
+
function normalizeTelegramWorkspaceAdmittedScopes(input: {
|
|
939
1140
|
operationId: string;
|
|
940
1141
|
operationKind: string;
|
|
941
1142
|
scopes: readonly TelegramWorkspaceAdmissionScope[];
|
|
942
|
-
|
|
943
|
-
}): T {
|
|
1143
|
+
}): TelegramWorkspaceAdmissionScope[] {
|
|
944
1144
|
validateOperationId(input.operationId);
|
|
945
1145
|
if (!isBoundedText(input.operationKind) || input.scopes.length === 0) {
|
|
946
1146
|
invalidInput("Telegram Workspace admitted operation is invalid.");
|
|
@@ -948,35 +1148,65 @@ export function runWithTelegramWorkspaceAdmissions<T>(input: {
|
|
|
948
1148
|
const uniqueScopes = new Map<string, TelegramWorkspaceAdmissionScope>();
|
|
949
1149
|
for (const scope of input.scopes) {
|
|
950
1150
|
validateScope(scope);
|
|
951
|
-
uniqueScopes.set(
|
|
1151
|
+
uniqueScopes.set(
|
|
1152
|
+
getTelegramWorkspaceAdmissionScopeKey(scope),
|
|
1153
|
+
cloneScope(scope),
|
|
1154
|
+
);
|
|
952
1155
|
}
|
|
953
|
-
|
|
1156
|
+
return Array.from(uniqueScopes.entries())
|
|
954
1157
|
.sort(([left], [right]) => left.localeCompare(right))
|
|
955
1158
|
.map(([, scope]) => scope);
|
|
956
|
-
|
|
957
|
-
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
/** Appends each lease as it is acquired so a blocked or failed scope leaves earlier leases releasable. */
|
|
1162
|
+
function acquireTelegramWorkspaceAdmittedScopes(
|
|
1163
|
+
input: {
|
|
1164
|
+
ledger: Pick<TelegramWorkspaceAdmissionLedger, "acquireAdmission">;
|
|
1165
|
+
operationId: string;
|
|
1166
|
+
operationKind: string;
|
|
1167
|
+
},
|
|
1168
|
+
scopes: readonly TelegramWorkspaceAdmissionScope[],
|
|
1169
|
+
acquired: TelegramWorkspaceAdmissionLease[],
|
|
1170
|
+
): void {
|
|
1171
|
+
for (const scope of scopes) {
|
|
1172
|
+
const scopeOperationId = `admission:${createHash("sha256")
|
|
1173
|
+
.update(input.operationId)
|
|
1174
|
+
.update("\0")
|
|
1175
|
+
.update(getTelegramWorkspaceAdmissionScopeKey(scope))
|
|
1176
|
+
.digest("hex")}`;
|
|
1177
|
+
const result = input.ledger.acquireAdmission({
|
|
1178
|
+
operationId: scopeOperationId,
|
|
1179
|
+
operationKind: input.operationKind,
|
|
1180
|
+
scope,
|
|
1181
|
+
});
|
|
1182
|
+
if (result.kind === "blocked") {
|
|
1183
|
+
throw new TelegramWorkspaceAdmissionError(
|
|
1184
|
+
"admission-blocked",
|
|
1185
|
+
"Telegram Workspace operation is blocked by retirement.",
|
|
1186
|
+
);
|
|
1187
|
+
}
|
|
1188
|
+
acquired.push(result.lease);
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
export function runWithTelegramWorkspaceAdmissions<T>(input: {
|
|
1193
|
+
ledger: Pick<
|
|
1194
|
+
TelegramWorkspaceAdmissionLedger,
|
|
1195
|
+
"acquireAdmission" | "releaseAdmission"
|
|
1196
|
+
>;
|
|
1197
|
+
operationId: string;
|
|
1198
|
+
operationKind: string;
|
|
1199
|
+
scopes: readonly TelegramWorkspaceAdmissionScope[];
|
|
1200
|
+
operation: () => T;
|
|
1201
|
+
}): T {
|
|
1202
|
+
const scopes = normalizeTelegramWorkspaceAdmittedScopes(input);
|
|
1203
|
+
const releaseActiveOperation = reserveTelegramWorkspaceAdmissionOperationId(
|
|
1204
|
+
input.operationId,
|
|
1205
|
+
);
|
|
958
1206
|
const acquired: TelegramWorkspaceAdmissionLease[] = [];
|
|
959
1207
|
let outcome: { ok: true; value: T } | { ok: false; error: unknown };
|
|
960
1208
|
try {
|
|
961
|
-
|
|
962
|
-
const scopeOperationId = `admission:${createHash("sha256")
|
|
963
|
-
.update(input.operationId)
|
|
964
|
-
.update("\0")
|
|
965
|
-
.update(getAdmissionScopeKey(scope))
|
|
966
|
-
.digest("hex")}`;
|
|
967
|
-
const result = input.ledger.acquireAdmission({
|
|
968
|
-
operationId: scopeOperationId,
|
|
969
|
-
operationKind: input.operationKind,
|
|
970
|
-
scope,
|
|
971
|
-
});
|
|
972
|
-
if (result.kind === "blocked") {
|
|
973
|
-
throw new TelegramWorkspaceAdmissionError(
|
|
974
|
-
"admission-blocked",
|
|
975
|
-
"Telegram Workspace operation is blocked by retirement.",
|
|
976
|
-
);
|
|
977
|
-
}
|
|
978
|
-
acquired.push(result.lease);
|
|
979
|
-
}
|
|
1209
|
+
acquireTelegramWorkspaceAdmittedScopes(input, scopes, acquired);
|
|
980
1210
|
outcome = { ok: true, value: input.operation() };
|
|
981
1211
|
} catch (error) {
|
|
982
1212
|
outcome = { ok: false, error };
|
|
@@ -1001,28 +1231,47 @@ export function runWithTelegramWorkspaceAdmissions<T>(input: {
|
|
|
1001
1231
|
}
|
|
1002
1232
|
|
|
1003
1233
|
export function createTelegramWorkspaceJournalWriterAdmission(input: {
|
|
1004
|
-
ledger: Pick<
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1234
|
+
ledger: Pick<
|
|
1235
|
+
TelegramWorkspaceAdmissionLedger,
|
|
1236
|
+
"acquireAdmission" | "acquireJournalWriterAdmission" | "releaseAdmission"
|
|
1237
|
+
>;
|
|
1238
|
+
protocolAuthority?: Omit<
|
|
1239
|
+
Parameters<
|
|
1240
|
+
TelegramWorkspaceAdmissionLedger["acquireJournalWriterAdmission"]
|
|
1241
|
+
>[0],
|
|
1242
|
+
"operationId"
|
|
1243
|
+
>;
|
|
1008
1244
|
createOperationId?: () => string;
|
|
1009
1245
|
onReleaseError?: (error: unknown) => void;
|
|
1010
1246
|
}): <T>(operation: () => T) => T {
|
|
1011
1247
|
let pendingOperationId: string | undefined;
|
|
1012
1248
|
let active = false;
|
|
1013
1249
|
return <T>(operation: () => T): T => {
|
|
1014
|
-
if (active)
|
|
1015
|
-
|
|
1250
|
+
if (active)
|
|
1251
|
+
invalidState(
|
|
1252
|
+
"Telegram Workspace journal writer admission is already active.",
|
|
1253
|
+
);
|
|
1254
|
+
const operationId =
|
|
1255
|
+
pendingOperationId ??
|
|
1256
|
+
input.createOperationId?.() ??
|
|
1016
1257
|
`journal-writer:${randomUUID()}`;
|
|
1017
1258
|
validateOperationId(operationId);
|
|
1018
1259
|
pendingOperationId = operationId;
|
|
1019
1260
|
const result = input.protocolAuthority
|
|
1020
|
-
? input.ledger.acquireJournalWriterAdmission({
|
|
1021
|
-
|
|
1022
|
-
|
|
1261
|
+
? input.ledger.acquireJournalWriterAdmission({
|
|
1262
|
+
operationId,
|
|
1263
|
+
...input.protocolAuthority,
|
|
1264
|
+
})
|
|
1265
|
+
: input.ledger.acquireAdmission({
|
|
1266
|
+
operationId,
|
|
1267
|
+
operationKind: "journal-write",
|
|
1268
|
+
scope: { kind: "profile" },
|
|
1269
|
+
});
|
|
1023
1270
|
if (result.kind === "blocked") {
|
|
1024
|
-
throw new TelegramWorkspaceAdmissionError(
|
|
1025
|
-
"
|
|
1271
|
+
throw new TelegramWorkspaceAdmissionError(
|
|
1272
|
+
"admission-blocked",
|
|
1273
|
+
"Telegram Workspace journal writer is blocked by destructive closure.",
|
|
1274
|
+
);
|
|
1026
1275
|
}
|
|
1027
1276
|
pendingOperationId = undefined;
|
|
1028
1277
|
active = true;
|
|
@@ -1032,9 +1281,15 @@ export function createTelegramWorkspaceJournalWriterAdmission(input: {
|
|
|
1032
1281
|
active = false;
|
|
1033
1282
|
try {
|
|
1034
1283
|
if (!input.ledger.releaseAdmission(result.lease))
|
|
1035
|
-
authorityChanged(
|
|
1284
|
+
authorityChanged(
|
|
1285
|
+
"Telegram Workspace journal writer lease disappeared before release.",
|
|
1286
|
+
);
|
|
1036
1287
|
} catch (error) {
|
|
1037
|
-
try {
|
|
1288
|
+
try {
|
|
1289
|
+
input.onReleaseError?.(error);
|
|
1290
|
+
} catch {
|
|
1291
|
+
/* Diagnostics cannot change the journal outcome. */
|
|
1292
|
+
}
|
|
1038
1293
|
}
|
|
1039
1294
|
}
|
|
1040
1295
|
};
|
|
@@ -1051,20 +1306,10 @@ export async function runWithTelegramWorkspaceAdmissionsAsync<T>(input: {
|
|
|
1051
1306
|
operation: () => Promise<T>;
|
|
1052
1307
|
onReleaseError?: (error: unknown) => void;
|
|
1053
1308
|
}): Promise<T> {
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
const uniqueScopes = new Map<string, TelegramWorkspaceAdmissionScope>();
|
|
1059
|
-
for (const scope of input.scopes) {
|
|
1060
|
-
validateScope(scope);
|
|
1061
|
-
uniqueScopes.set(getAdmissionScopeKey(scope), cloneScope(scope));
|
|
1062
|
-
}
|
|
1063
|
-
const scopes = Array.from(uniqueScopes.entries())
|
|
1064
|
-
.sort(([left], [right]) => left.localeCompare(right))
|
|
1065
|
-
.map(([, scope]) => scope);
|
|
1066
|
-
const releaseActiveOperation =
|
|
1067
|
-
reserveTelegramWorkspaceAdmissionOperationId(input.operationId);
|
|
1309
|
+
const scopes = normalizeTelegramWorkspaceAdmittedScopes(input);
|
|
1310
|
+
const releaseActiveOperation = reserveTelegramWorkspaceAdmissionOperationId(
|
|
1311
|
+
input.operationId,
|
|
1312
|
+
);
|
|
1068
1313
|
const acquired: TelegramWorkspaceAdmissionLease[] = [];
|
|
1069
1314
|
const releaseAll = (): void => {
|
|
1070
1315
|
for (const lease of acquired.reverse()) {
|
|
@@ -1085,25 +1330,7 @@ export async function runWithTelegramWorkspaceAdmissionsAsync<T>(input: {
|
|
|
1085
1330
|
}
|
|
1086
1331
|
};
|
|
1087
1332
|
try {
|
|
1088
|
-
|
|
1089
|
-
const scopeOperationId = `admission:${createHash("sha256")
|
|
1090
|
-
.update(input.operationId)
|
|
1091
|
-
.update("\0")
|
|
1092
|
-
.update(getAdmissionScopeKey(scope))
|
|
1093
|
-
.digest("hex")}`;
|
|
1094
|
-
const result = input.ledger.acquireAdmission({
|
|
1095
|
-
operationId: scopeOperationId,
|
|
1096
|
-
operationKind: input.operationKind,
|
|
1097
|
-
scope,
|
|
1098
|
-
});
|
|
1099
|
-
if (result.kind === "blocked") {
|
|
1100
|
-
throw new TelegramWorkspaceAdmissionError(
|
|
1101
|
-
"admission-blocked",
|
|
1102
|
-
"Telegram Workspace operation is blocked by retirement.",
|
|
1103
|
-
);
|
|
1104
|
-
}
|
|
1105
|
-
acquired.push(result.lease);
|
|
1106
|
-
}
|
|
1333
|
+
acquireTelegramWorkspaceAdmittedScopes(input, scopes, acquired);
|
|
1107
1334
|
} catch (error) {
|
|
1108
1335
|
releaseAll();
|
|
1109
1336
|
releaseActiveOperation();
|
|
@@ -1122,20 +1349,34 @@ export function createTelegramWorkspaceAdmissionLedger(
|
|
|
1122
1349
|
): TelegramWorkspaceAdmissionLedger {
|
|
1123
1350
|
validateOptions(options);
|
|
1124
1351
|
const suppliedOptions = options;
|
|
1125
|
-
if (options.stateProfile !== undefined)
|
|
1352
|
+
if (options.stateProfile !== undefined)
|
|
1353
|
+
options = { ...options, owner: { ...options.owner } };
|
|
1126
1354
|
const getNowMs = options.getNowMs ?? Date.now;
|
|
1127
1355
|
const getProcessLiveness =
|
|
1128
1356
|
options.getProcessLiveness ??
|
|
1129
1357
|
((owner: TelegramWorkspaceAdmissionOwner) =>
|
|
1130
1358
|
getTelegramProcessLiveness(owner));
|
|
1131
1359
|
const transactionPath = `${options.path}.transaction`;
|
|
1132
|
-
const path = options.path,
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1360
|
+
const path = options.path,
|
|
1361
|
+
profileKey = options.profileKey,
|
|
1362
|
+
stateProfile = options.stateProfile,
|
|
1363
|
+
boundOwner = { ...options.owner };
|
|
1364
|
+
const publication = {
|
|
1365
|
+
publishRename: options.publishRename,
|
|
1366
|
+
onPublicationBoundary: options.onPublicationBoundary,
|
|
1367
|
+
};
|
|
1368
|
+
const isStorageCurrent = (): boolean =>
|
|
1369
|
+
suppliedOptions.path === path &&
|
|
1370
|
+
suppliedOptions.profileKey === profileKey &&
|
|
1371
|
+
suppliedOptions.stateProfile === stateProfile &&
|
|
1372
|
+
areOwnersEqual(suppliedOptions.owner, boundOwner);
|
|
1136
1373
|
const mapStateError = (error: unknown): never => {
|
|
1137
|
-
if (error instanceof TelegramRuntimeStateError)
|
|
1138
|
-
|
|
1374
|
+
if (error instanceof TelegramRuntimeStateError)
|
|
1375
|
+
throw new TelegramWorkspaceAdmissionError(
|
|
1376
|
+
error.code === "invalid" ? "invalid-state" : error.code,
|
|
1377
|
+
"Telegram consolidated admission state operation failed.",
|
|
1378
|
+
{ cause: error },
|
|
1379
|
+
);
|
|
1139
1380
|
throw error;
|
|
1140
1381
|
};
|
|
1141
1382
|
|
|
@@ -1147,17 +1388,42 @@ export function createTelegramWorkspaceAdmissionLedger(
|
|
|
1147
1388
|
): T {
|
|
1148
1389
|
if (stateProfile !== undefined) {
|
|
1149
1390
|
try {
|
|
1150
|
-
return mutateTelegramRuntimeStateSection(
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1391
|
+
return mutateTelegramRuntimeStateSection(
|
|
1392
|
+
path,
|
|
1393
|
+
stateProfile,
|
|
1394
|
+
"admission",
|
|
1395
|
+
(current) => {
|
|
1396
|
+
const state = normalizeAdmissionState(
|
|
1397
|
+
current,
|
|
1398
|
+
profileKey,
|
|
1399
|
+
stateProfile,
|
|
1400
|
+
);
|
|
1401
|
+
const retained = state.leases.filter(
|
|
1402
|
+
(lease) => getProcessLiveness(lease.owner) !== "dead",
|
|
1403
|
+
);
|
|
1404
|
+
const pruned = retained.length !== state.leases.length;
|
|
1405
|
+
state.leases = retained;
|
|
1406
|
+
const outcome = operation(state);
|
|
1407
|
+
return {
|
|
1408
|
+
value:
|
|
1409
|
+
pruned || outcome.changed
|
|
1410
|
+
? normalizeAdmissionState(state, profileKey, stateProfile)
|
|
1411
|
+
: current,
|
|
1412
|
+
result: outcome.result,
|
|
1413
|
+
};
|
|
1414
|
+
},
|
|
1415
|
+
{
|
|
1416
|
+
isCurrent: isStorageCurrent,
|
|
1417
|
+
publishRename: publication.publishRename,
|
|
1418
|
+
onPublicationBoundary(boundary) {
|
|
1419
|
+
if (boundary !== "after-rename")
|
|
1420
|
+
publication.onPublicationBoundary?.(boundary, path);
|
|
1421
|
+
},
|
|
1422
|
+
},
|
|
1423
|
+
);
|
|
1424
|
+
} catch (error) {
|
|
1425
|
+
return mapStateError(error);
|
|
1426
|
+
}
|
|
1161
1427
|
}
|
|
1162
1428
|
return withTelegramFileTransaction(transactionPath, () => {
|
|
1163
1429
|
const state = readState(options.path, options.profileKey);
|
|
@@ -1175,21 +1441,36 @@ export function createTelegramWorkspaceAdmissionLedger(
|
|
|
1175
1441
|
function read(): TelegramWorkspaceAdmissionLedgerSnapshot {
|
|
1176
1442
|
let state: TelegramWorkspaceAdmissionState;
|
|
1177
1443
|
if (stateProfile !== undefined) {
|
|
1178
|
-
if (!isStorageCurrent())
|
|
1444
|
+
if (!isStorageCurrent())
|
|
1445
|
+
authorityChanged(
|
|
1446
|
+
"Telegram consolidated admission storage authority changed.",
|
|
1447
|
+
);
|
|
1179
1448
|
try {
|
|
1180
1449
|
const file = readTelegramRuntimeState(path);
|
|
1181
|
-
const value = Object.hasOwn(file.profiles, stateProfile)
|
|
1450
|
+
const value = Object.hasOwn(file.profiles, stateProfile)
|
|
1451
|
+
? file.profiles[stateProfile]?.admission
|
|
1452
|
+
: undefined;
|
|
1182
1453
|
state = normalizeAdmissionState(value, profileKey, stateProfile);
|
|
1183
|
-
if (!isStorageCurrent())
|
|
1184
|
-
|
|
1454
|
+
if (!isStorageCurrent())
|
|
1455
|
+
authorityChanged(
|
|
1456
|
+
"Telegram consolidated admission storage authority changed.",
|
|
1457
|
+
);
|
|
1458
|
+
} catch (error) {
|
|
1459
|
+
return mapStateError(error);
|
|
1460
|
+
}
|
|
1185
1461
|
} else state = readState(options.path, options.profileKey);
|
|
1186
1462
|
return {
|
|
1187
1463
|
profileKey: state.profileKey,
|
|
1188
1464
|
leases: state.leases.map(cloneLease),
|
|
1189
1465
|
...(state.fence ? { fence: cloneFence(state.fence) } : {}),
|
|
1190
|
-
...(state.writerProtocolMode
|
|
1191
|
-
|
|
1192
|
-
|
|
1466
|
+
...(state.writerProtocolMode
|
|
1467
|
+
? {
|
|
1468
|
+
writerProtocolMode: {
|
|
1469
|
+
...state.writerProtocolMode,
|
|
1470
|
+
installedBy: { ...state.writerProtocolMode.installedBy },
|
|
1471
|
+
},
|
|
1472
|
+
}
|
|
1473
|
+
: {}),
|
|
1193
1474
|
};
|
|
1194
1475
|
}
|
|
1195
1476
|
|
|
@@ -1204,13 +1485,19 @@ export function createTelegramWorkspaceAdmissionLedger(
|
|
|
1204
1485
|
}
|
|
1205
1486
|
validateScope(input.scope);
|
|
1206
1487
|
return transact<TelegramWorkspaceAdmissionAcquireResult>((state) => {
|
|
1207
|
-
const journalOperation =
|
|
1488
|
+
const journalOperation =
|
|
1489
|
+
input.operationKind === "journal-write" ||
|
|
1208
1490
|
input.operationKind.startsWith("journal.");
|
|
1209
|
-
const protocolWriterHeld = state.leases.some(
|
|
1210
|
-
lease
|
|
1211
|
-
|
|
1491
|
+
const protocolWriterHeld = state.leases.some(
|
|
1492
|
+
(lease) =>
|
|
1493
|
+
lease.operationKind === "journal-write:custody-v3" &&
|
|
1494
|
+
lease.scope.kind === "profile" &&
|
|
1495
|
+
areOwnersEqual(lease.owner, options.owner),
|
|
1496
|
+
);
|
|
1212
1497
|
if (state.writerProtocolMode && journalOperation && !protocolWriterHeld)
|
|
1213
|
-
authorityChanged(
|
|
1498
|
+
authorityChanged(
|
|
1499
|
+
"Telegram Workspace journal writer requires protocol authority.",
|
|
1500
|
+
);
|
|
1214
1501
|
const existing = state.leases.find(
|
|
1215
1502
|
(lease) => lease.operationId === input.operationId,
|
|
1216
1503
|
);
|
|
@@ -1224,22 +1511,33 @@ export function createTelegramWorkspaceAdmissionLedger(
|
|
|
1224
1511
|
acquiredAtMs: existing.acquiredAtMs,
|
|
1225
1512
|
};
|
|
1226
1513
|
if (!areLeasesEqual(existing, retried)) {
|
|
1227
|
-
authorityChanged(
|
|
1514
|
+
authorityChanged(
|
|
1515
|
+
"Telegram Workspace admission operation identity changed.",
|
|
1516
|
+
);
|
|
1228
1517
|
}
|
|
1229
1518
|
return {
|
|
1230
|
-
result: {
|
|
1519
|
+
result: {
|
|
1520
|
+
kind: "acquired",
|
|
1521
|
+
lease: cloneLease(existing),
|
|
1522
|
+
resumed: true,
|
|
1523
|
+
},
|
|
1231
1524
|
changed: false,
|
|
1232
1525
|
};
|
|
1233
1526
|
}
|
|
1234
|
-
if (
|
|
1235
|
-
|
|
1527
|
+
if (
|
|
1528
|
+
state.fence &&
|
|
1529
|
+
(!isTelegramWorkspaceRetirementFence(state.fence) ||
|
|
1530
|
+
scopesConflict(input.scope, state.fence.target))
|
|
1531
|
+
) {
|
|
1236
1532
|
return {
|
|
1237
1533
|
result: { kind: "blocked", reason: "retirement-fenced" },
|
|
1238
1534
|
changed: false,
|
|
1239
1535
|
};
|
|
1240
1536
|
}
|
|
1241
1537
|
if (state.leases.length >= TELEGRAM_WORKSPACE_ADMISSION_MAX_LEASES) {
|
|
1242
|
-
invalidState(
|
|
1538
|
+
invalidState(
|
|
1539
|
+
"Telegram Workspace admission lease capacity is exhausted.",
|
|
1540
|
+
);
|
|
1243
1541
|
}
|
|
1244
1542
|
const lease: TelegramWorkspaceAdmissionLease = {
|
|
1245
1543
|
operationId: input.operationId,
|
|
@@ -1257,10 +1555,14 @@ export function createTelegramWorkspaceAdmissionLedger(
|
|
|
1257
1555
|
});
|
|
1258
1556
|
}
|
|
1259
1557
|
|
|
1260
|
-
function releaseAdmission(
|
|
1558
|
+
function releaseAdmission(
|
|
1559
|
+
expected: TelegramWorkspaceAdmissionLease,
|
|
1560
|
+
): boolean {
|
|
1261
1561
|
validateExpectedLease(expected, options.profileKey);
|
|
1262
1562
|
if (!areOwnersEqual(expected.owner, options.owner)) {
|
|
1263
|
-
authorityChanged(
|
|
1563
|
+
authorityChanged(
|
|
1564
|
+
"Telegram Workspace admission release requires lease owner authority.",
|
|
1565
|
+
);
|
|
1264
1566
|
}
|
|
1265
1567
|
return transact((state) => {
|
|
1266
1568
|
const index = state.leases.findIndex(
|
|
@@ -1269,67 +1571,138 @@ export function createTelegramWorkspaceAdmissionLedger(
|
|
|
1269
1571
|
if (index < 0) return { result: false, changed: false };
|
|
1270
1572
|
const current = state.leases[index];
|
|
1271
1573
|
if (!areLeasesEqual(current, expected)) {
|
|
1272
|
-
authorityChanged(
|
|
1574
|
+
authorityChanged(
|
|
1575
|
+
"Telegram Workspace admission lease authority changed.",
|
|
1576
|
+
);
|
|
1273
1577
|
}
|
|
1274
1578
|
state.leases.splice(index, 1);
|
|
1275
1579
|
return { result: true, changed: true };
|
|
1276
1580
|
});
|
|
1277
1581
|
}
|
|
1278
1582
|
|
|
1279
|
-
function acquireJournalWriterClosure(input: {
|
|
1280
|
-
|
|
1583
|
+
function acquireJournalWriterClosure(input: {
|
|
1584
|
+
operationId: string;
|
|
1585
|
+
recoveryKey: string;
|
|
1586
|
+
requestedAtMs: number;
|
|
1587
|
+
}): TelegramWorkspaceJournalWriterClosureAcquireResult {
|
|
1281
1588
|
validateOperationId(input.operationId);
|
|
1282
|
-
const candidate = normalizeTelegramWorkspaceJournalWriterClosureFence(
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1589
|
+
const candidate = normalizeTelegramWorkspaceJournalWriterClosureFence(
|
|
1590
|
+
{
|
|
1591
|
+
destructiveKind: "journal-writer-closure",
|
|
1592
|
+
phase: "fenced",
|
|
1593
|
+
...input,
|
|
1594
|
+
profileKey: options.profileKey,
|
|
1595
|
+
owner: options.owner,
|
|
1596
|
+
acquiredAtMs: getNowMs(),
|
|
1597
|
+
},
|
|
1598
|
+
options.profileKey,
|
|
1599
|
+
);
|
|
1600
|
+
if (!candidate)
|
|
1601
|
+
invalidInput("Telegram Workspace writer closure input is invalid.");
|
|
1602
|
+
return transact<TelegramWorkspaceJournalWriterClosureAcquireResult>(
|
|
1603
|
+
(state) => {
|
|
1604
|
+
if (state.writerProtocolMode) {
|
|
1605
|
+
if (state.leases.length > 0)
|
|
1606
|
+
return {
|
|
1607
|
+
result: { kind: "blocked", reason: "admission-active" },
|
|
1608
|
+
changed: false,
|
|
1609
|
+
};
|
|
1610
|
+
let authorized = false;
|
|
1611
|
+
if (state.writerProtocolMode.recoveryKey === candidate.recoveryKey) {
|
|
1612
|
+
try {
|
|
1613
|
+
authorized =
|
|
1614
|
+
options.authorizeJournalWriterProtocolClosure?.({
|
|
1615
|
+
mode: {
|
|
1616
|
+
...state.writerProtocolMode,
|
|
1617
|
+
installedBy: { ...state.writerProtocolMode.installedBy },
|
|
1618
|
+
},
|
|
1619
|
+
closure: cloneFence(candidate),
|
|
1620
|
+
}) === true;
|
|
1621
|
+
} catch {
|
|
1622
|
+
authorized = false;
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
if (!authorized)
|
|
1626
|
+
return {
|
|
1627
|
+
result: { kind: "blocked", reason: "retirement-active" },
|
|
1628
|
+
changed: false,
|
|
1629
|
+
};
|
|
1630
|
+
delete state.writerProtocolMode;
|
|
1631
|
+
state.fence = candidate;
|
|
1632
|
+
return {
|
|
1633
|
+
result: {
|
|
1634
|
+
kind: "acquired",
|
|
1635
|
+
fence: cloneFence(candidate),
|
|
1636
|
+
resumed: false,
|
|
1637
|
+
},
|
|
1638
|
+
changed: true,
|
|
1639
|
+
};
|
|
1298
1640
|
}
|
|
1299
|
-
if (
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1641
|
+
if (state.fence) {
|
|
1642
|
+
if (state.fence.destructiveKind === "journal-writer-closure") {
|
|
1643
|
+
const resumed = {
|
|
1644
|
+
...candidate,
|
|
1645
|
+
acquiredAtMs: state.fence.acquiredAtMs,
|
|
1646
|
+
};
|
|
1647
|
+
if (areWriterClosureFencesEqual(state.fence, resumed))
|
|
1648
|
+
return {
|
|
1649
|
+
result: {
|
|
1650
|
+
kind: "acquired",
|
|
1651
|
+
fence: cloneFence(state.fence),
|
|
1652
|
+
resumed: true,
|
|
1653
|
+
},
|
|
1654
|
+
changed: false,
|
|
1655
|
+
};
|
|
1656
|
+
if (state.fence.operationId === candidate.operationId)
|
|
1657
|
+
authorityChanged(
|
|
1658
|
+
"Telegram Workspace writer closure authority changed.",
|
|
1659
|
+
);
|
|
1660
|
+
}
|
|
1661
|
+
return {
|
|
1662
|
+
result: { kind: "blocked", reason: "retirement-active" },
|
|
1663
|
+
changed: false,
|
|
1664
|
+
};
|
|
1313
1665
|
}
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1666
|
+
if (state.leases.length > 0)
|
|
1667
|
+
return {
|
|
1668
|
+
result: { kind: "blocked", reason: "admission-active" },
|
|
1669
|
+
changed: false,
|
|
1670
|
+
};
|
|
1671
|
+
state.fence = candidate;
|
|
1672
|
+
return {
|
|
1673
|
+
result: {
|
|
1674
|
+
kind: "acquired",
|
|
1675
|
+
fence: cloneFence(candidate),
|
|
1676
|
+
resumed: false,
|
|
1677
|
+
},
|
|
1678
|
+
changed: true,
|
|
1679
|
+
};
|
|
1680
|
+
},
|
|
1681
|
+
);
|
|
1321
1682
|
}
|
|
1322
1683
|
|
|
1323
|
-
function releaseJournalWriterClosure(
|
|
1324
|
-
|
|
1325
|
-
|
|
1684
|
+
function releaseJournalWriterClosure(
|
|
1685
|
+
expected: TelegramWorkspaceJournalWriterClosureFence,
|
|
1686
|
+
): boolean {
|
|
1687
|
+
const normalized = normalizeTelegramWorkspaceJournalWriterClosureFence(
|
|
1688
|
+
expected,
|
|
1689
|
+
options.profileKey,
|
|
1690
|
+
);
|
|
1691
|
+
if (!normalized)
|
|
1692
|
+
invalidInput("Telegram Workspace writer closure authority is invalid.");
|
|
1326
1693
|
if (!areOwnersEqual(expected.owner, options.owner))
|
|
1327
|
-
authorityChanged(
|
|
1694
|
+
authorityChanged(
|
|
1695
|
+
"Telegram Workspace writer closure release requires owner authority.",
|
|
1696
|
+
);
|
|
1328
1697
|
return transact((state) => {
|
|
1329
1698
|
if (!state.fence) return { result: false, changed: false };
|
|
1330
|
-
if (
|
|
1331
|
-
|
|
1332
|
-
|
|
1699
|
+
if (
|
|
1700
|
+
state.fence.destructiveKind !== "journal-writer-closure" ||
|
|
1701
|
+
!areWriterClosureFencesEqual(state.fence, expected)
|
|
1702
|
+
)
|
|
1703
|
+
authorityChanged(
|
|
1704
|
+
"Telegram Workspace writer closure authority changed.",
|
|
1705
|
+
);
|
|
1333
1706
|
delete state.fence;
|
|
1334
1707
|
return { result: true, changed: true };
|
|
1335
1708
|
});
|
|
@@ -1339,81 +1712,170 @@ export function createTelegramWorkspaceAdmissionLedger(
|
|
|
1339
1712
|
expected: TelegramWorkspaceJournalWriterClosureFence,
|
|
1340
1713
|
input: { startupAuthorityId: string; writerInventorySha256: string },
|
|
1341
1714
|
): TelegramWorkspaceJournalWriterProtocolInstallResult {
|
|
1342
|
-
const normalizedClosure =
|
|
1343
|
-
|
|
1715
|
+
const normalizedClosure =
|
|
1716
|
+
normalizeTelegramWorkspaceJournalWriterClosureFence(
|
|
1717
|
+
expected,
|
|
1718
|
+
options.profileKey,
|
|
1719
|
+
);
|
|
1344
1720
|
if (!normalizedClosure || !areOwnersEqual(expected.owner, options.owner))
|
|
1345
|
-
authorityChanged(
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
}
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1721
|
+
authorityChanged(
|
|
1722
|
+
"Telegram Workspace writer protocol installation requires closure owner authority.",
|
|
1723
|
+
);
|
|
1724
|
+
const candidate = normalizeTelegramWorkspaceJournalWriterProtocolMode(
|
|
1725
|
+
{
|
|
1726
|
+
version: 1,
|
|
1727
|
+
protocol: "custody-v3",
|
|
1728
|
+
profileKey: options.profileKey,
|
|
1729
|
+
recoveryKey: expected.recoveryKey,
|
|
1730
|
+
startupAuthorityId: input.startupAuthorityId,
|
|
1731
|
+
closureOperationId: expected.operationId,
|
|
1732
|
+
writerInventorySha256: input.writerInventorySha256,
|
|
1733
|
+
installedBy: options.owner,
|
|
1734
|
+
installedAtMs: getNowMs(),
|
|
1735
|
+
},
|
|
1736
|
+
options.profileKey,
|
|
1737
|
+
);
|
|
1738
|
+
if (!candidate)
|
|
1739
|
+
invalidInput("Telegram Workspace writer protocol mode input is invalid.");
|
|
1740
|
+
return transact<TelegramWorkspaceJournalWriterProtocolInstallResult>(
|
|
1741
|
+
(state) => {
|
|
1742
|
+
if (state.writerProtocolMode) {
|
|
1743
|
+
const resumed = {
|
|
1744
|
+
...candidate,
|
|
1745
|
+
installedAtMs: state.writerProtocolMode.installedAtMs,
|
|
1746
|
+
};
|
|
1747
|
+
if (
|
|
1748
|
+
areWriterProtocolModesEqual(state.writerProtocolMode, resumed) &&
|
|
1749
|
+
!state.fence
|
|
1750
|
+
)
|
|
1751
|
+
return {
|
|
1752
|
+
result: {
|
|
1753
|
+
mode: {
|
|
1754
|
+
...state.writerProtocolMode,
|
|
1755
|
+
installedBy: { ...state.writerProtocolMode.installedBy },
|
|
1756
|
+
},
|
|
1757
|
+
resumed: true,
|
|
1758
|
+
},
|
|
1759
|
+
changed: false,
|
|
1760
|
+
};
|
|
1761
|
+
authorityChanged(
|
|
1762
|
+
"Telegram Workspace writer protocol mode authority changed.",
|
|
1763
|
+
);
|
|
1764
|
+
}
|
|
1765
|
+
if (
|
|
1766
|
+
!state.fence ||
|
|
1767
|
+
state.fence.destructiveKind !== "journal-writer-closure" ||
|
|
1768
|
+
!areWriterClosureFencesEqual(state.fence, expected) ||
|
|
1769
|
+
state.leases.length !== 0
|
|
1770
|
+
)
|
|
1771
|
+
authorityChanged(
|
|
1772
|
+
"Telegram Workspace writer closure authority changed before protocol installation.",
|
|
1773
|
+
);
|
|
1774
|
+
state.writerProtocolMode = candidate;
|
|
1775
|
+
delete state.fence;
|
|
1776
|
+
return {
|
|
1777
|
+
result: {
|
|
1778
|
+
mode: { ...candidate, installedBy: { ...candidate.installedBy } },
|
|
1779
|
+
resumed: false,
|
|
1780
|
+
},
|
|
1781
|
+
changed: true,
|
|
1782
|
+
};
|
|
1783
|
+
},
|
|
1784
|
+
);
|
|
1368
1785
|
}
|
|
1369
1786
|
|
|
1370
|
-
function acquireJournalWriterAdmission(input: {
|
|
1371
|
-
|
|
1372
|
-
|
|
1787
|
+
function acquireJournalWriterAdmission(input: {
|
|
1788
|
+
operationId: string;
|
|
1789
|
+
recoveryKey: string;
|
|
1790
|
+
startupAuthorityId: string;
|
|
1791
|
+
closureOperationId: string;
|
|
1792
|
+
writerInventorySha256: string;
|
|
1793
|
+
}): TelegramWorkspaceAdmissionAcquireResult {
|
|
1373
1794
|
validateOperationId(input.operationId);
|
|
1374
|
-
if (
|
|
1375
|
-
!isBoundedText(input.
|
|
1376
|
-
|
|
1795
|
+
if (
|
|
1796
|
+
!isBoundedText(input.recoveryKey) ||
|
|
1797
|
+
!isBoundedText(input.startupAuthorityId) ||
|
|
1798
|
+
!isBoundedText(input.closureOperationId) ||
|
|
1799
|
+
!/^[a-f0-9]{64}$/u.test(input.writerInventorySha256)
|
|
1800
|
+
)
|
|
1801
|
+
invalidInput(
|
|
1802
|
+
"Telegram Workspace journal writer protocol authority is invalid.",
|
|
1803
|
+
);
|
|
1377
1804
|
return transact<TelegramWorkspaceAdmissionAcquireResult>((state) => {
|
|
1378
1805
|
const mode = state.writerProtocolMode;
|
|
1379
|
-
if (
|
|
1806
|
+
if (
|
|
1807
|
+
!mode ||
|
|
1808
|
+
mode.recoveryKey !== input.recoveryKey ||
|
|
1380
1809
|
mode.startupAuthorityId !== input.startupAuthorityId ||
|
|
1381
1810
|
mode.closureOperationId !== input.closureOperationId ||
|
|
1382
|
-
mode.writerInventorySha256 !== input.writerInventorySha256
|
|
1383
|
-
|
|
1384
|
-
|
|
1811
|
+
mode.writerInventorySha256 !== input.writerInventorySha256
|
|
1812
|
+
)
|
|
1813
|
+
authorityChanged(
|
|
1814
|
+
"Telegram Workspace journal writer protocol authority changed.",
|
|
1815
|
+
);
|
|
1816
|
+
const existing = state.leases.find(
|
|
1817
|
+
(lease) => lease.operationId === input.operationId,
|
|
1818
|
+
);
|
|
1385
1819
|
const operationKind = "journal-write:custody-v3";
|
|
1386
1820
|
if (existing) {
|
|
1387
|
-
const retried: TelegramWorkspaceAdmissionLease = {
|
|
1388
|
-
|
|
1389
|
-
|
|
1821
|
+
const retried: TelegramWorkspaceAdmissionLease = {
|
|
1822
|
+
operationId: input.operationId,
|
|
1823
|
+
operationKind,
|
|
1824
|
+
profileKey: options.profileKey,
|
|
1825
|
+
scope: { kind: "profile" },
|
|
1826
|
+
owner: { ...options.owner },
|
|
1827
|
+
acquiredAtMs: existing.acquiredAtMs,
|
|
1828
|
+
};
|
|
1390
1829
|
if (!areLeasesEqual(existing, retried))
|
|
1391
|
-
authorityChanged(
|
|
1392
|
-
|
|
1393
|
-
|
|
1830
|
+
authorityChanged(
|
|
1831
|
+
"Telegram Workspace journal writer operation identity changed.",
|
|
1832
|
+
);
|
|
1833
|
+
return {
|
|
1834
|
+
result: {
|
|
1835
|
+
kind: "acquired",
|
|
1836
|
+
lease: cloneLease(existing),
|
|
1837
|
+
resumed: true,
|
|
1838
|
+
},
|
|
1839
|
+
changed: false,
|
|
1840
|
+
};
|
|
1394
1841
|
}
|
|
1395
|
-
if (state.fence)
|
|
1396
|
-
|
|
1842
|
+
if (state.fence)
|
|
1843
|
+
return {
|
|
1844
|
+
result: { kind: "blocked", reason: "retirement-fenced" },
|
|
1845
|
+
changed: false,
|
|
1846
|
+
};
|
|
1397
1847
|
if (state.leases.length >= TELEGRAM_WORKSPACE_ADMISSION_MAX_LEASES)
|
|
1398
|
-
invalidState(
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1848
|
+
invalidState(
|
|
1849
|
+
"Telegram Workspace admission lease capacity is exhausted.",
|
|
1850
|
+
);
|
|
1851
|
+
const lease: TelegramWorkspaceAdmissionLease = {
|
|
1852
|
+
operationId: input.operationId,
|
|
1853
|
+
operationKind,
|
|
1854
|
+
profileKey: options.profileKey,
|
|
1855
|
+
scope: { kind: "profile" },
|
|
1856
|
+
owner: { ...options.owner },
|
|
1857
|
+
acquiredAtMs: getNowMs(),
|
|
1858
|
+
};
|
|
1402
1859
|
state.leases.push(lease);
|
|
1403
|
-
return {
|
|
1404
|
-
|
|
1860
|
+
return {
|
|
1861
|
+
result: { kind: "acquired", lease: cloneLease(lease), resumed: false },
|
|
1862
|
+
changed: true,
|
|
1863
|
+
};
|
|
1405
1864
|
});
|
|
1406
1865
|
}
|
|
1407
1866
|
|
|
1408
|
-
function acquireDestructiveFence(
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1867
|
+
function acquireDestructiveFence(
|
|
1868
|
+
input: {
|
|
1869
|
+
operationId: string;
|
|
1870
|
+
retirementIntentId: string;
|
|
1871
|
+
bindingKey: string;
|
|
1872
|
+
slot: string;
|
|
1873
|
+
target: TelegramTarget & { threadId: number };
|
|
1874
|
+
leaderEpoch: number | string;
|
|
1875
|
+
retirementRequestedAtMs: number;
|
|
1876
|
+
},
|
|
1877
|
+
destructiveKind?: TelegramWorkspaceDeletionFenceKind,
|
|
1878
|
+
): TelegramWorkspaceRetirementFenceAcquireResult {
|
|
1417
1879
|
validateOperationId(input.operationId);
|
|
1418
1880
|
const candidate = normalizeFence(
|
|
1419
1881
|
{
|
|
@@ -1432,7 +1894,10 @@ export function createTelegramWorkspaceAdmissionLedger(
|
|
|
1432
1894
|
return transact<TelegramWorkspaceRetirementFenceAcquireResult>((state) => {
|
|
1433
1895
|
if (state.fence) {
|
|
1434
1896
|
if (!isTelegramWorkspaceRetirementFence(state.fence)) {
|
|
1435
|
-
return {
|
|
1897
|
+
return {
|
|
1898
|
+
result: { kind: "blocked", reason: "retirement-active" },
|
|
1899
|
+
changed: false,
|
|
1900
|
+
};
|
|
1436
1901
|
}
|
|
1437
1902
|
if (
|
|
1438
1903
|
state.fence.phase === "fenced" &&
|
|
@@ -1451,14 +1916,20 @@ export function createTelegramWorkspaceAdmissionLedger(
|
|
|
1451
1916
|
};
|
|
1452
1917
|
}
|
|
1453
1918
|
if (state.fence.operationId === candidate.operationId) {
|
|
1454
|
-
authorityChanged(
|
|
1919
|
+
authorityChanged(
|
|
1920
|
+
"Telegram Workspace retirement fence authority changed.",
|
|
1921
|
+
);
|
|
1455
1922
|
}
|
|
1456
1923
|
return {
|
|
1457
1924
|
result: { kind: "blocked", reason: "retirement-active" },
|
|
1458
1925
|
changed: false,
|
|
1459
1926
|
};
|
|
1460
1927
|
}
|
|
1461
|
-
if (
|
|
1928
|
+
if (
|
|
1929
|
+
state.leases.some((lease) =>
|
|
1930
|
+
scopesConflict(lease.scope, candidate.target),
|
|
1931
|
+
)
|
|
1932
|
+
) {
|
|
1462
1933
|
return {
|
|
1463
1934
|
result: { kind: "blocked", reason: "admission-active" },
|
|
1464
1935
|
changed: false,
|
|
@@ -1466,23 +1937,54 @@ export function createTelegramWorkspaceAdmissionLedger(
|
|
|
1466
1937
|
}
|
|
1467
1938
|
state.fence = candidate;
|
|
1468
1939
|
return {
|
|
1469
|
-
result: {
|
|
1940
|
+
result: {
|
|
1941
|
+
kind: "acquired",
|
|
1942
|
+
fence: cloneFence(candidate),
|
|
1943
|
+
resumed: false,
|
|
1944
|
+
},
|
|
1470
1945
|
changed: true,
|
|
1471
1946
|
};
|
|
1472
1947
|
});
|
|
1473
1948
|
}
|
|
1474
1949
|
|
|
1475
|
-
function acquireRetirementFence(
|
|
1950
|
+
function acquireRetirementFence(
|
|
1951
|
+
input: Parameters<
|
|
1952
|
+
TelegramWorkspaceAdmissionLedger["acquireRetirementFence"]
|
|
1953
|
+
>[0],
|
|
1954
|
+
) {
|
|
1476
1955
|
return acquireDestructiveFence(input);
|
|
1477
1956
|
}
|
|
1478
1957
|
|
|
1479
1958
|
function acquireThreadCleanupFence(
|
|
1480
|
-
input: Parameters<
|
|
1959
|
+
input: Parameters<
|
|
1960
|
+
TelegramWorkspaceAdmissionLedger["acquireThreadCleanupFence"]
|
|
1961
|
+
>[0],
|
|
1481
1962
|
): TelegramWorkspaceRetirementFenceAcquireResult {
|
|
1482
|
-
return acquireDestructiveFence(
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1963
|
+
return acquireDestructiveFence(
|
|
1964
|
+
{
|
|
1965
|
+
operationId: input.operationId,
|
|
1966
|
+
retirementIntentId: input.cleanupWorkSetId,
|
|
1967
|
+
bindingKey: input.bindingKey,
|
|
1968
|
+
slot: input.slot,
|
|
1969
|
+
target: input.target,
|
|
1970
|
+
leaderEpoch: input.leaderEpoch,
|
|
1971
|
+
retirementRequestedAtMs: input.cleanupRequestedAtMs,
|
|
1972
|
+
},
|
|
1973
|
+
"manual-thread-cleanup",
|
|
1974
|
+
);
|
|
1975
|
+
}
|
|
1976
|
+
|
|
1977
|
+
// Exact profile/kind/owner preflight shared by owner-held fence transitions; the transaction still rechecks state.
|
|
1978
|
+
function assertOwnedFence(
|
|
1979
|
+
expected: TelegramWorkspaceRetirementFence,
|
|
1980
|
+
expectedKind: TelegramWorkspaceDeletionFenceKind,
|
|
1981
|
+
ownerMessage: string,
|
|
1982
|
+
): void {
|
|
1983
|
+
validateExpectedFence(expected, options.profileKey);
|
|
1984
|
+
if (resolveTelegramWorkspaceDestructiveFenceKind(expected) !== expectedKind)
|
|
1985
|
+
authorityChanged("Telegram Workspace destructive fence kind changed.");
|
|
1986
|
+
if (!areOwnersEqual(expected.owner, options.owner))
|
|
1987
|
+
authorityChanged(ownerMessage);
|
|
1486
1988
|
}
|
|
1487
1989
|
|
|
1488
1990
|
function adoptRetirementFence(
|
|
@@ -1496,11 +1998,18 @@ export function createTelegramWorkspaceAdmissionLedger(
|
|
|
1496
1998
|
validateExpectedFence(expected, options.profileKey);
|
|
1497
1999
|
if (resolveTelegramWorkspaceDestructiveFenceKind(expected) !== expectedKind)
|
|
1498
2000
|
authorityChanged("Telegram Workspace destructive fence kind changed.");
|
|
1499
|
-
if (
|
|
1500
|
-
|
|
2001
|
+
if (
|
|
2002
|
+
!normalizeOwner(replacement.owner) ||
|
|
2003
|
+
!isLeaderEpoch(replacement.leaderEpoch)
|
|
2004
|
+
) {
|
|
2005
|
+
invalidInput(
|
|
2006
|
+
"Telegram Workspace retirement successor authority is invalid.",
|
|
2007
|
+
);
|
|
1501
2008
|
}
|
|
1502
2009
|
if (!areOwnersEqual(replacement.owner, options.owner)) {
|
|
1503
|
-
authorityChanged(
|
|
2010
|
+
authorityChanged(
|
|
2011
|
+
"Telegram Workspace retirement adoption requires successor owner authority.",
|
|
2012
|
+
);
|
|
1504
2013
|
}
|
|
1505
2014
|
const adopted = {
|
|
1506
2015
|
...cloneFence(expected),
|
|
@@ -1509,13 +2018,17 @@ export function createTelegramWorkspaceAdmissionLedger(
|
|
|
1509
2018
|
} satisfies TelegramWorkspaceRetirementFence;
|
|
1510
2019
|
return transact((state) => {
|
|
1511
2020
|
if (!state.fence || !isTelegramWorkspaceRetirementFence(state.fence)) {
|
|
1512
|
-
authorityChanged(
|
|
2021
|
+
authorityChanged(
|
|
2022
|
+
"Telegram Workspace retirement fence is absent or has another kind.",
|
|
2023
|
+
);
|
|
1513
2024
|
}
|
|
1514
2025
|
if (areFencesEqual(state.fence, adopted)) {
|
|
1515
2026
|
return { result: cloneFence(state.fence), changed: false };
|
|
1516
2027
|
}
|
|
1517
2028
|
if (!areFencesEqual(state.fence, expected)) {
|
|
1518
|
-
authorityChanged(
|
|
2029
|
+
authorityChanged(
|
|
2030
|
+
"Telegram Workspace retirement fence authority changed.",
|
|
2031
|
+
);
|
|
1519
2032
|
}
|
|
1520
2033
|
state.fence = adopted;
|
|
1521
2034
|
return { result: cloneFence(adopted), changed: true };
|
|
@@ -1526,15 +2039,16 @@ export function createTelegramWorkspaceAdmissionLedger(
|
|
|
1526
2039
|
expected: TelegramWorkspaceRetirementFence,
|
|
1527
2040
|
expectedKind: TelegramWorkspaceDeletionFenceKind = "pressure-retirement",
|
|
1528
2041
|
): TelegramWorkspaceDeletionPermitResult {
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
}
|
|
2042
|
+
assertOwnedFence(
|
|
2043
|
+
expected,
|
|
2044
|
+
expectedKind,
|
|
2045
|
+
"Telegram Workspace deletion permit requires fence owner authority.",
|
|
2046
|
+
);
|
|
1535
2047
|
return transact<TelegramWorkspaceDeletionPermitResult>((state) => {
|
|
1536
2048
|
if (!state.fence || !isTelegramWorkspaceRetirementFence(state.fence)) {
|
|
1537
|
-
authorityChanged(
|
|
2049
|
+
authorityChanged(
|
|
2050
|
+
"Telegram Workspace retirement fence is absent or has another kind.",
|
|
2051
|
+
);
|
|
1538
2052
|
}
|
|
1539
2053
|
if (
|
|
1540
2054
|
state.fence.operationId === expected.operationId &&
|
|
@@ -1547,7 +2061,9 @@ export function createTelegramWorkspaceAdmissionLedger(
|
|
|
1547
2061
|
};
|
|
1548
2062
|
}
|
|
1549
2063
|
if (!areFencesEqual(state.fence, expected)) {
|
|
1550
|
-
authorityChanged(
|
|
2064
|
+
authorityChanged(
|
|
2065
|
+
"Telegram Workspace retirement fence authority changed.",
|
|
2066
|
+
);
|
|
1551
2067
|
}
|
|
1552
2068
|
if (state.fence.phase !== "fenced") {
|
|
1553
2069
|
return {
|
|
@@ -1567,7 +2083,9 @@ export function createTelegramWorkspaceAdmissionLedger(
|
|
|
1567
2083
|
kind: "issued",
|
|
1568
2084
|
fence: cloneFence(issued),
|
|
1569
2085
|
permit: {
|
|
1570
|
-
...(issued.destructiveKind === undefined
|
|
2086
|
+
...(issued.destructiveKind === undefined
|
|
2087
|
+
? {}
|
|
2088
|
+
: { destructiveKind: issued.destructiveKind }),
|
|
1571
2089
|
operationId: issued.operationId,
|
|
1572
2090
|
retirementIntentId: issued.retirementIntentId,
|
|
1573
2091
|
profileKey: issued.profileKey,
|
|
@@ -1587,18 +2105,21 @@ export function createTelegramWorkspaceAdmissionLedger(
|
|
|
1587
2105
|
expected: TelegramWorkspaceRetirementFence,
|
|
1588
2106
|
expectedKind: TelegramWorkspaceDeletionFenceKind = "pressure-retirement",
|
|
1589
2107
|
): TelegramWorkspaceRetirementFence {
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
}
|
|
2108
|
+
assertOwnedFence(
|
|
2109
|
+
expected,
|
|
2110
|
+
expectedKind,
|
|
2111
|
+
"Telegram Workspace absence confirmation requires fence owner authority.",
|
|
2112
|
+
);
|
|
1596
2113
|
if (expected.phase === "deletion-rejected") {
|
|
1597
|
-
authorityChanged(
|
|
2114
|
+
authorityChanged(
|
|
2115
|
+
"Rejected Telegram Workspace deletion cannot confirm absence.",
|
|
2116
|
+
);
|
|
1598
2117
|
}
|
|
1599
2118
|
return transact((state) => {
|
|
1600
2119
|
if (!state.fence || !isTelegramWorkspaceRetirementFence(state.fence)) {
|
|
1601
|
-
authorityChanged(
|
|
2120
|
+
authorityChanged(
|
|
2121
|
+
"Telegram Workspace retirement fence is absent or has another kind.",
|
|
2122
|
+
);
|
|
1602
2123
|
}
|
|
1603
2124
|
if (
|
|
1604
2125
|
state.fence.operationId === expected.operationId &&
|
|
@@ -1608,7 +2129,9 @@ export function createTelegramWorkspaceAdmissionLedger(
|
|
|
1608
2129
|
return { result: cloneFence(state.fence), changed: false };
|
|
1609
2130
|
}
|
|
1610
2131
|
if (!areFencesEqual(state.fence, expected)) {
|
|
1611
|
-
authorityChanged(
|
|
2132
|
+
authorityChanged(
|
|
2133
|
+
"Telegram Workspace retirement fence authority changed.",
|
|
2134
|
+
);
|
|
1612
2135
|
}
|
|
1613
2136
|
const ready: TelegramWorkspaceRetirementFence = {
|
|
1614
2137
|
...state.fence,
|
|
@@ -1627,21 +2150,26 @@ export function createTelegramWorkspaceAdmissionLedger(
|
|
|
1627
2150
|
expected: TelegramWorkspaceRetirementFence,
|
|
1628
2151
|
expectedKind: TelegramWorkspaceDeletionFenceKind = "pressure-retirement",
|
|
1629
2152
|
): boolean {
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
}
|
|
2153
|
+
assertOwnedFence(
|
|
2154
|
+
expected,
|
|
2155
|
+
expectedKind,
|
|
2156
|
+
"Telegram Workspace fence release requires owner authority.",
|
|
2157
|
+
);
|
|
1636
2158
|
return transact((state) => {
|
|
1637
2159
|
if (!state.fence) return { result: false, changed: false };
|
|
1638
2160
|
if (!isTelegramWorkspaceRetirementFence(state.fence))
|
|
1639
|
-
authorityChanged(
|
|
2161
|
+
authorityChanged(
|
|
2162
|
+
"Telegram Workspace retirement fence has another kind.",
|
|
2163
|
+
);
|
|
1640
2164
|
if (!areFencesEqual(state.fence, expected)) {
|
|
1641
|
-
authorityChanged(
|
|
2165
|
+
authorityChanged(
|
|
2166
|
+
"Telegram Workspace retirement fence authority changed.",
|
|
2167
|
+
);
|
|
1642
2168
|
}
|
|
1643
2169
|
if (state.fence.phase !== "fenced") {
|
|
1644
|
-
authorityChanged(
|
|
2170
|
+
authorityChanged(
|
|
2171
|
+
"Issued Telegram Workspace retirement fence cannot be released.",
|
|
2172
|
+
);
|
|
1645
2173
|
}
|
|
1646
2174
|
delete state.fence;
|
|
1647
2175
|
return { result: true, changed: true };
|
|
@@ -1653,83 +2181,135 @@ export function createTelegramWorkspaceAdmissionLedger(
|
|
|
1653
2181
|
expectedKind: TelegramWorkspaceDeletionFenceKind,
|
|
1654
2182
|
requiredPhase: "commit-ready" | "deletion-rejected",
|
|
1655
2183
|
): boolean {
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
}
|
|
2184
|
+
assertOwnedFence(
|
|
2185
|
+
expected,
|
|
2186
|
+
expectedKind,
|
|
2187
|
+
"Telegram Workspace fence completion requires owner authority.",
|
|
2188
|
+
);
|
|
1662
2189
|
return transact((state) => {
|
|
1663
2190
|
if (!state.fence) return { result: false, changed: false };
|
|
1664
2191
|
if (!isTelegramWorkspaceRetirementFence(state.fence))
|
|
1665
|
-
authorityChanged(
|
|
2192
|
+
authorityChanged(
|
|
2193
|
+
"Telegram Workspace retirement fence has another kind.",
|
|
2194
|
+
);
|
|
1666
2195
|
if (!areFencesEqual(state.fence, expected)) {
|
|
1667
|
-
authorityChanged(
|
|
2196
|
+
authorityChanged(
|
|
2197
|
+
"Telegram Workspace retirement fence authority changed.",
|
|
2198
|
+
);
|
|
1668
2199
|
}
|
|
1669
2200
|
if (state.fence.phase !== requiredPhase) {
|
|
1670
|
-
authorityChanged(
|
|
1671
|
-
|
|
1672
|
-
|
|
2201
|
+
authorityChanged(
|
|
2202
|
+
requiredPhase === "commit-ready"
|
|
2203
|
+
? "Telegram Workspace retirement commit is not ready."
|
|
2204
|
+
: "Telegram Workspace retirement rejection is not confirmed.",
|
|
2205
|
+
);
|
|
1673
2206
|
}
|
|
1674
2207
|
delete state.fence;
|
|
1675
2208
|
return { result: true, changed: true };
|
|
1676
2209
|
});
|
|
1677
2210
|
}
|
|
1678
2211
|
|
|
1679
|
-
function confirmRetirementRejection(
|
|
2212
|
+
function confirmRetirementRejection(
|
|
2213
|
+
expected: TelegramWorkspaceRetirementFence,
|
|
2214
|
+
): TelegramWorkspaceRetirementFence {
|
|
1680
2215
|
validateExpectedFence(expected, options.profileKey);
|
|
1681
|
-
if (
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
2216
|
+
if (
|
|
2217
|
+
resolveTelegramWorkspaceDestructiveFenceKind(expected) !==
|
|
2218
|
+
"pressure-retirement" ||
|
|
2219
|
+
!areOwnersEqual(expected.owner, options.owner) ||
|
|
2220
|
+
(expected.phase !== "deletion-issued" &&
|
|
2221
|
+
expected.phase !== "deletion-rejected")
|
|
2222
|
+
) {
|
|
2223
|
+
authorityChanged(
|
|
2224
|
+
"Telegram Workspace rejection requires exact issued retirement authority.",
|
|
2225
|
+
);
|
|
1685
2226
|
}
|
|
1686
2227
|
return transact((state) => {
|
|
1687
2228
|
const current = state.fence;
|
|
1688
|
-
if (
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
2229
|
+
if (
|
|
2230
|
+
!current ||
|
|
2231
|
+
!isTelegramWorkspaceRetirementFence(current) ||
|
|
2232
|
+
!areFenceBasesEqual(current, expected) ||
|
|
2233
|
+
(current.phase !== "deletion-issued" &&
|
|
2234
|
+
current.phase !== "deletion-rejected") ||
|
|
2235
|
+
current.deletionIssuedAtMs !== expected.deletionIssuedAtMs ||
|
|
2236
|
+
(expected.phase === "deletion-rejected" &&
|
|
2237
|
+
!areFencesEqual(current, expected))
|
|
2238
|
+
) {
|
|
2239
|
+
authorityChanged(
|
|
2240
|
+
"Telegram Workspace retirement fence authority changed.",
|
|
2241
|
+
);
|
|
1693
2242
|
}
|
|
1694
|
-
if (current.phase === "deletion-rejected")
|
|
1695
|
-
|
|
1696
|
-
|
|
2243
|
+
if (current.phase === "deletion-rejected")
|
|
2244
|
+
return { result: cloneFence(current), changed: false };
|
|
2245
|
+
const rejected: TelegramWorkspaceRetirementFence = {
|
|
2246
|
+
...current,
|
|
2247
|
+
destructiveKind: "pressure-retirement",
|
|
2248
|
+
phase: "deletion-rejected",
|
|
2249
|
+
rejectionConfirmedAtMs: getNowMs(),
|
|
2250
|
+
};
|
|
1697
2251
|
state.fence = rejected;
|
|
1698
2252
|
return { result: cloneFence(rejected), changed: true };
|
|
1699
2253
|
});
|
|
1700
2254
|
}
|
|
1701
2255
|
|
|
1702
|
-
function completeRetirementFence(
|
|
1703
|
-
|
|
2256
|
+
function completeRetirementFence(
|
|
2257
|
+
expected: TelegramWorkspaceRetirementFence,
|
|
2258
|
+
expectedKind: TelegramWorkspaceDeletionFenceKind = "pressure-retirement",
|
|
2259
|
+
) {
|
|
1704
2260
|
return completeDestructiveFence(expected, expectedKind, "commit-ready");
|
|
1705
2261
|
}
|
|
1706
|
-
function completeRejectedRetirementFence(
|
|
1707
|
-
|
|
2262
|
+
function completeRejectedRetirementFence(
|
|
2263
|
+
expected: TelegramWorkspaceRetirementFence,
|
|
2264
|
+
) {
|
|
2265
|
+
return completeDestructiveFence(
|
|
2266
|
+
expected,
|
|
2267
|
+
"pressure-retirement",
|
|
2268
|
+
"deletion-rejected",
|
|
2269
|
+
);
|
|
1708
2270
|
}
|
|
1709
2271
|
|
|
1710
|
-
function adoptThreadCleanupFence(
|
|
1711
|
-
|
|
2272
|
+
function adoptThreadCleanupFence(
|
|
2273
|
+
expected: TelegramWorkspaceRetirementFence,
|
|
2274
|
+
replacement: {
|
|
2275
|
+
owner: TelegramWorkspaceAdmissionOwner;
|
|
2276
|
+
leaderEpoch: number | string;
|
|
2277
|
+
},
|
|
2278
|
+
) {
|
|
1712
2279
|
return adoptRetirementFence(expected, replacement, "manual-thread-cleanup");
|
|
1713
2280
|
}
|
|
1714
|
-
function issueThreadCleanupDeletionPermit(
|
|
2281
|
+
function issueThreadCleanupDeletionPermit(
|
|
2282
|
+
expected: TelegramWorkspaceRetirementFence,
|
|
2283
|
+
) {
|
|
1715
2284
|
return issueDeletionPermit(expected, "manual-thread-cleanup");
|
|
1716
2285
|
}
|
|
1717
|
-
function confirmThreadCleanupAbsence(
|
|
2286
|
+
function confirmThreadCleanupAbsence(
|
|
2287
|
+
expected: TelegramWorkspaceRetirementFence,
|
|
2288
|
+
) {
|
|
1718
2289
|
return confirmRetirementAbsence(expected, "manual-thread-cleanup");
|
|
1719
2290
|
}
|
|
1720
|
-
function releaseUnissuedThreadCleanupFence(
|
|
2291
|
+
function releaseUnissuedThreadCleanupFence(
|
|
2292
|
+
expected: TelegramWorkspaceRetirementFence,
|
|
2293
|
+
) {
|
|
1721
2294
|
return releaseUnissuedRetirementFence(expected, "manual-thread-cleanup");
|
|
1722
2295
|
}
|
|
1723
|
-
function completeThreadCleanupFence(
|
|
2296
|
+
function completeThreadCleanupFence(
|
|
2297
|
+
expected: TelegramWorkspaceRetirementFence,
|
|
2298
|
+
) {
|
|
1724
2299
|
return completeRetirementFence(expected, "manual-thread-cleanup");
|
|
1725
2300
|
}
|
|
1726
2301
|
|
|
1727
2302
|
return {
|
|
1728
|
-
getProfileKey: () =>
|
|
1729
|
-
|
|
2303
|
+
getProfileKey: () =>
|
|
2304
|
+
stateProfile === undefined ? options.profileKey : profileKey,
|
|
2305
|
+
getOwner: () => ({
|
|
2306
|
+
...(stateProfile === undefined ? options.owner : boundOwner),
|
|
2307
|
+
}),
|
|
1730
2308
|
listReservedSlots: () => {
|
|
1731
2309
|
const fence = read().fence;
|
|
1732
|
-
return fence && isTelegramWorkspaceRetirementFence(fence)
|
|
2310
|
+
return fence && isTelegramWorkspaceRetirementFence(fence)
|
|
2311
|
+
? [fence.slot]
|
|
2312
|
+
: [];
|
|
1733
2313
|
},
|
|
1734
2314
|
read,
|
|
1735
2315
|
acquireAdmission,
|